Files
rikako-note/中间件/redis/redis.md

17 KiB
Raw Blame History

redis

Using Command

Keys and values

Content of Keys

key为data model中拥有含义的文本名称。Redis key在命名格式方面几乎没有限制故而key name中可以包含空格或标点符号。redis key并不支持namespace或categories,故而在命名时应当避免命名冲突。

通常,会使用:符号来将redis的命名分割为多个sections例如office:London,可以使用该命名规范来实现类似categories的效果。

尽管keys通常是文本的redis中也实现了binary-safe key。可以使用任何byte sequence来作为有效的key并且在redis中empty string也可以作为有效的key。

redis key在命名时存在如下规范

  • 不推荐使用长度很长的key会在存储和key-comparisions方面带来负面影响
  • 不推荐使用长度非常短的key其会减少可读性通常user:1000:followers的可读性相较u1000flw来说可读性要更好,并且前者带来的额外存储开销也较小
  • 应在命名时使用统一的命名模式,例如object-type:id在section中包含多个单词时可以使用.-符号来进行分隔,例如comment:4321:reply.tocomment:4321:reply-to
  • key size的最大允许大小为512MB

Hashtags

redis通过hashing来获取key关联的value。

通常整个key都会被用作hash index的计算但是在部分场景下开发者可能只希望使用key中的一部分来计算hash index。此时可以通过{}包围key中想要计算hash index的部分该部分被称为hash-tag`。

例如,person:1person:2这两个key会计算出不同的hash index但是{persion}:1{person}:2这两个key计算出的hash index却是相同的因为只有person会被用于计算hash index。

通常hashtag的应用场景是在集群场景下进行multi-key operations。在集群场景下除非所有key计算出的hash index相同否则集群并不允许执行multi-key操作。

例如,SINTER命令用于查询两个不同set values的交集可以接收多个key。在集群场景下

SINTER group:1 group:2

上述命名并无法成功执行,因为group:1group:2两个key的hash index不同。

但是,如下命令在集群环境下则是可以正常执行:

SINTER {group}:1 {group}:2

hashtag让两个key产生相同的hash值。

虽然hashtag在上述场景下有效但是不应该过度的使用hashtag。因为hashtag相同的key其hash index都相同故而会被散列到同一个slot中当同一slot中元素过多时会导致redis的性能下降。

altering and querying the key space

存在部分命令其并不针对特定的类型而是用于和key space进行交互其可以被用于所有类型的key。

例如,EXISTS命令会返回0和1代表给定key在redis中是否存在DEL命令则是用于删除key和key关联的value无论value是什么类型。

示例如下:

> set mykey hello
OK
> exists mykey
(integer) 1
> del mykey
(integer) 1
> exists mykey
(integer) 0

在上述示例中,DEL命令返回的值为1或0代表要被删除的值在redis中是否存在。

TYPE命令则是可以返回key所关联value的类型:

> set mykey x
OK
> type mykey
string
> del mykey
(integer) 1
> type mykey
none

key expiration

在redis中不管key对应的value为何种类型都支持key expiration特性。key exipiration支持为key设置超时key expiration也被称为time to live/TTL,当ttl指定的时间过去后key将会被自动移除。

对于key expiration

  • 在对key设置key expiration时可以按照秒或毫秒的精度进行设置
  • 但是,expire time在解析时单位永远为毫秒
  • expire相关的信息会被replicated并存储在磁盘中即使在redis宕机时time virtually passes即redis key其expire若为1天宕机4小时后恢复其expire会变为8小时宕机并不会导致key expire停止计算

可以通过EXPIRE命令来设置key expiration

> set key some-value
OK
> expire key 5
(integer) 1
> get key (immediately)
"some-value"
> get key (after some time)
(nil)

在第二次调用时delay超过5skey已经不存在。

上述示例中,expire key 5将key的超时时间设置为了5sEXPIRE用于为key指定不同的超时时间。

类似的,可以通过PERSIST命令来取消key的超时设置让key永久被保留。

除了使用expire来设置超时外在创建时也能会key指定expiration

> set key 100 ex 10
OK
> ttl key
(integer) 9

上述示例中,使用ttl命令来检查key的超时时间。

如果想要按照毫秒来设置超时,可以使用PEXPIREPTTL命令。

Navigating the keyspace

Scan

SCAN命令支持对redis中key的增量迭代在每次调用时只会返回一小部分数据。该命令可以在生产中使用并不会像keyssmembers等命令一样在处理大量elements或keys时可能产生长时间的阻塞。

scan使用实例如下

> scan 0
1) "17"
2)  1) "key:12"
    2) "key:8"
    3) "key:4"
    4) "key:14"
    5) "key:16"
    6) "key:17"
    7) "key:15"
    8) "key:10"
    9) "key:3"
   10) "key:7"
   11) "key:1"
> scan 17
1) "0"
2) 1) "key:5"
   2) "key:18"
   3) "key:0"
   4) "key:2"
   5) "key:19"
   6) "key:13"
   7) "key:6"
   8) "key:9"
   9) "key:11"

scan是一个cursor based iterator每次在调用scan命令时都会返回一个update cursor并且在下次调用scan时需要使用上次返回的cursor。

当cursor被设置为0时iteration将会开始并且当server返回的cursor为0时iteration结束。

keys

除了scan外还可以通过keys命令来迭代redis中所有的key。但是scan的增量迭代不同的是keys会一次性返回所有的key在返回前会阻塞redis-server。

keys命令支持glob-style pattern

  • h?llo?用于匹配单个字符
  • h*llo: *用于匹配除/外的任何内容
  • h[ae]llo: 匹配hallohello
  • h[^e]llo: [^e]匹配除e外的任何字符
  • h[a-b]llo: 匹配hallohbllo

global-style pattern中转义符为\

pipelining

redis pipelining支持一次发送多条命令而非逐条发送命令,并且发送后一条命令之前必须要等待前一条请求执行完成。pipelining被绝大多数redis client支持能够提高性能。

Request/Response protocols and round-trip time(RTT)

redis是一个使用client-server model的tcp server在请求完成前会经历如下步骤

  • client向server发送query并且阻塞的从socket中读取server的响应
  • server接收到client的请求后处理命令并且将处理结果返回给client

例如包含4条命令的命令序列如下

  1. client: incr x
  2. server: 1
  3. client: incr x
  4. server: 2
  5. client: incr x
  6. server: 3
  7. client: incr x
  8. server: 4

client和server之间通过网络进行连接每次client和server的请求/响应,都需要经历client发送请求server发送响应的过程,该过程会经过网络来传输,带来传输延迟。

该延迟被称为RTT(round trip time), 如果在一次请求中能够发送n条命令,那么将能够节省n-1次网络传输的往返时间。例如RTT如果为250ms即使server能够以100K/s的速度处理请求对于同一client其每秒也只能处理4条命令。

redis pipelining

在redis server处理命令时其处理新请求前并不要求client接收到旧请求并且client在发送多条命令后会一次性统一读取执行结果。

该技术被称为Pipelining,在其他协议中也有广泛使用,例如POP3

pipelining在redis的所有版本中都被支持示例如下

$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG

通过pipelining并不需要对每个命令都花费RTT用于网络传输而是在一次网络传输时就包含3条命令。

当client使用pipelining发送commands时server会在内存中对replies进行排队。故而在client需要使用pipeline向server发送大量的请求时其需要分批发送每批中包含合适数量的命令。

pipeline会积累多条命令并一次性发送给server。

Performance Imporvement

pipeline不仅能够减少RTT的次数其也能够增加redis server在每秒的执行操作数。

在redis server处理command时实际的处理逻辑开销很小但是和socket io的交互开销却很大。在进行socket io时会进行writeread的系统调用,其涉及到用户态和内核态的切换,这将带来巨大的开销。

如果使用pipeline多条指令只需要调用一次read系统调用,并且多条执行的执行结果只需要通过一次write系统调用即能够执行。通过使用pipeline能够有效降低redis server的系统调用次数这将减少socket io带来的开销故而redis server能够在一秒内执行更多的commands。

pipelining vs scripting

相比于pipeliningscripting可以在read, compute, write的场景下带来更高的性能。pipelining并无法处理read, compute, write的场景因为在执行write command之前需要先获取read command的执行结果故而无法将read和write命令通过pipeline同时发送给server。

Transactions

redis transaction支持execute a group of commands in a single step,其涉及到multi, exec, discard, watch命令。

  • 在redis事务中所有命令都会被串行、按顺序执行在redis transaction执行时其他client发送的请求永远不会插入到redis transaction中间。在redis transaction中所有命令都会executed as a single siolated operation,事务执行的过程中不会被其他命令打断
  • EXEC命令会触发事务中所有命令的执行故而当client在事务上下文中exec命令调用之前失去了与server的连接事务中的命令都不会被执行。只有当exec被调用后事务中的命令才会实际开始执行

Usage

可以通过multi命令来进入redis事务该命令总会返回ok。在进入事务后,可以向事务中添加多条命令,这些命令并不会被立马执行,而是被排队。只有当发送EXEC命令后,之前排队的命令才会被实际执行。

DISCARD命令可以清空被排队的命令,并且退出事务的上下文。

如下示例展示了如何通过事务原子的执行一系列命令:

> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

EXEC命令会返回一个数组数组中元素为之前QUEUED COMMANDS的返回结果顺序和命令被添加到队列中的顺序相同。

在事务上下文中,所有命令(EXEC除外)都会返回QUEUED

Errors inside transaction

在使用事务时可能遇到如下两种errors

  • 将命令添加到queue中时可能发生失败该时机在EXEC被执行之前。例如command可能存在语法错误或是存在内存错误等都可能导致命令添加到queue失败
  • 调用EXEC对入队的命令实际执行时可能发生异常例如在实际执行command时对string类型的value执行了list操作

对于EXEC时产生的错误,并没有特殊处理:即使事务中部分命令实际执行失败,其他的命令也都会被执行

示例如下所示:

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
MULTI
+OK
SET a abc
+QUEUED
LPOP a
+QUEUED
EXEC
*2
+OK
-WRONGTYPE Operation against a key holding the wrong kind of value

上述示例中执行了两个命令其中命令1执行成功而命令2执行失败。

需要注意的是,事务中即使某个命令执行失败queue中的其他命令仍然会被执行redis在执行某条命令失败时并不会对别的命令执行造成影响。

rollback for redis Transaction

对于redis transaction其并不对rollback做支持rollback会对redis的性能造成巨大影响也会影响redis的易用性。

Discard the command queue

如果想要对事务进行abort可以调用DISCARD命令,在该场景下,并不会有命令被实际执行,并且连接状态也会恢复为正常:

> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"

Optimistic locking using check-and-set

在redis transaction中WATCH命令用于提供CAS行为。watched keys将会被监控,并探知其是否发生变化。

在执行EXEC命令前如果存在任一key发生过修改那么整个事务都会发生abort,并且会返回NULL,用以提示事务失败。

如下是一个read, compute, write的示例:

val = GET mykey
val = val + 1
SET mykey val

上述逻辑在只存在一个客户端的场景下工作正常,但是当存在多个客户端时,将会发生竞争。由于上述逻辑并不是原子的,故而可能出现如下场景:

  1. client A read old value
  2. client B read old value
  3. client A compute old value + 1
  4. client B compute old value + 1
  5. client A set new value with old value + 1
  6. client B set new value with old value + 1

故而在多client场景下假设old value为10即使client A和client B都对value进行了incr最后new value的值仍有可能为11而不是12

通过WATCH机制能够解决该问题

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

在上述示例中在进入事务上下文前client对mykey进行了watch并完成新值的计算之后进入事务上下文后用new value设置mykey并调用EXEC命令。

如果WATCH和EXEC之间存在其他client修改了mykey的值那么当前事务将会失败。

只需要在发生竞争时重新执行上述流程,那么其即是乐观锁。

WATCH

WATCH命令会让EXEC是条件的:

  • 只有当所有watched keys都未修改的前提下才会让redis实际执行transaction

watched keys可能发生如下的修改:

  • watched keys可能被其他client修改
  • watched keys可能被redis本身修改redis本身的修改包含如下场景
    • expiration
    • eviction

如果在对keys进行watch和实际调用exec之间keys发生的变化整个transaction都会被abort。

在redis 6.0.9之前expired keys并不会造成redis transaction被abort

在本事务内的命令并不会造成WATCH condition被触发因为WATCH机制的时间范围为keys watched的时间点到exec调用前的时间点而queued commands在调用exec后才会实际执行

watch命令可以被多次调用所有的watch命令都会生效并且在watch被调用后就开始监控key的变化监控一直到EXEC被调用后才结束。

对于WATCH命令,可以传递任意数量的参数。

EXEC命令被调用后所有的watched keys都会被unwatched不管事务是否被aborted。并且当client连接关闭后所有keys都会被unwatched。

UNWATCH

可以通过UNWATCH命令无参数来清空所有的watched keys。

通常,在调用MULTI进入事务前,会执行如下操作:

  • WATCH mykey
  • GET mykey

如果在GET mykey后,调用MULTI之前如果在读取mykey的值后不再想执行后续事务了那么可以直接调用UNWATCH对先前监视的所有key取消监视。

Using WATCH to implement ZPOP

如下是一个使用WATCH的示例

WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC