Files
rikako-note/中间件/redis/redis.md
2025-09-09 21:02:33 +08:00

63 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。

对于discard命令其在调用后所有watched 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

如果EXEC失败那么其将返回Null,所以仅需对之前操作进行重试即可

Data Types

Redis Strings

Redis strings存储字节序列包含文本、serialized objects、binary array。strings通常被用于缓存但是支持额外的功能例如counters和位操作。

redis keys也为strings。

string data type可用于诸多用例例如对html fragement/html page的缓存。

SET / GET

通过set和get命令可以对string value进行设置和获取。

    > SET bike:1 Deimos
    OK
    > GET bike:1
    "Deimos"

在使用SET命令时如果key已存在对应的值那么set指定的value将会对已经存在的值进行替换。即使key对应的旧值并不是strings类型set也会对其进行替换

values可以是strings of every kind包含binary data故而支持在value中存储jpeg image。value的值不能超过512MB.

set with additional arguments

在使用set命令时,可以为其提供额外的参数,例如NX | XX.

  • NX: 仅当redis不存在对应的key时才进行设置否则失败返回nil
  • XX: 仅当redis存在对应的key时的才进行设置否则失败返回nil

GETSET

GETSET命令将会将key设置为指定的new value并且返回oldvalue的值。

127.0.0.1:6379> get bike:1
(nil)
127.0.0.1:6379> GETSET bike:1 3
(nil)
127.0.0.1:6379> GET bike:1
"3"

MSET / MGET

strings类型支持通过mset, mget命令来一次性获取和设置多个keys这将能降低RTT带来的延迟。

    > mset bike:1 "Deimos" bike:2 "Ares" bike:3 "Vanth"
    OK
    > mget bike:1 bike:2 bike:3
    1) "Deimos"
    2) "Ares"
    3) "Vanth"

strings as counters

strings类型支持atomic increment

    > set total_crashes 0
    OK
    > incr total_crashes
    (integer) 1
    > incrby total_crashes 10
    (integer) 11

incr命令会将string value转化为integer并且对其进行加一操作。类似命令还有incrby, decr, drcrby

Limits

默认情况下单个redis string的最大限制为512MB

JSON

redis支持对json值的存储、更新和获取。redis json可以和redis query engine进行协作从而允许index and query json documents

在redis 8中内置支持了RedisJSON否则需要手动安装RedisJSON module。

JSON.SET

JSON.SET命令支持将redis的key设置为JSON value示例如下

127.0.0.1:6379> JSON.SET bike $ '"Hyperion"'
OK
127.0.0.1:6379> JSON.GET bike $
"[\"Hyperion\"]"
127.0.0.1:6379> type bike
ReJSON-RL
127.0.0.1:6379> JSON.TYPE bike $
1) "string"

在上述示例中,$代表的是指向json document中value的path

  • 在上述示例中,$代表root

除此之外JSON还支持其他string operation。JSON.STRLNE支持获取string长度并且可以通过JSON.STRAPPEND来在当前字符串后追加其他字符串:

> JSON.STRLEN bike $
1) (integer) 8
> JSON.STRAPPEND bike $ '" (Enduro bikes)"'
1) (integer) 23
> JSON.GET bike $
"[\"Hyperion (Enduro bikes)\"]"

json数值操作

RedisJSON支持incrementmultiply操作:

> JSON.SET crashes $ 0
OK
> JSON.NUMINCRBY crashes $ 1
"[1]"
> JSON.NUMINCRBY crashes $ 1.5
"[2.5]"
> JSON.NUMINCRBY crashes $ -0.75
"[1.75]"
> JSON.NUMMULTBY crashes $ 24
"[42]"

json数组操作

RedisJSON支持通过JSON.SET将值赋值为数组,path expression支持数组操作

> JSON.SET newbike $ '["Deimos", {"crashes": 0}, null]'
OK
> JSON.GET newbike $
"[[\"Deimos\",{\"crashes\":0},null]]"
> JSON.GET newbike $[1].crashes
"[0]"
> JSON.DEL newbike $[-1]
(integer) 1
> JSON.GET newbike $
"[[\"Deimos\",{\"crashes\":0}]]"
JSON.DEL

JSON.DEL支持通过path对json值进行删除。

JSON.ARRAPPEND

支持向json array中追加值。

JSON.ARRTRIM

支持对json array进行裁剪。

> JSON.SET riders $ []
OK
> JSON.ARRAPPEND riders $ '"Norem"'
1) (integer) 1
> JSON.GET riders $
"[[\"Norem\"]]"
> JSON.ARRINSERT riders $ 1 '"Prickett"' '"Royce"' '"Castilla"'
1) (integer) 4
> JSON.GET riders $
"[[\"Norem\",\"Prickett\",\"Royce\",\"Castilla\"]]"
> JSON.ARRTRIM riders $ 1 1
1) (integer) 1
> JSON.GET riders $
"[[\"Prickett\"]]"
> JSON.ARRPOP riders $
1) "\"Prickett\""
> JSON.ARRPOP riders $
1) (nil)

json object操作

json oject操作同样有其自己的命令示例如下

> JSON.SET bike:1 $ '{"model": "Deimos", "brand": "Ergonom", "price": 4972}'
OK
> JSON.OBJLEN bike:1 $
1) (integer) 3
> JSON.OBJKEYS bike:1 $
1) 1) "model"
   2) "brand"
   3) "price"> JSON.SET bike:1 $ '{"model": "Deimos", "brand": "Ergonom", "price": 4972}'

format output

redis-cli支持对json内容的输出进行格式化步骤如下

  • 在执行redis-cli时指定--raw选项
  • 通过formatting keywords来进行格式化
    • INDENT
    • NEWLINE
    • SPACE
$ redis-cli --raw
> JSON.GET obj INDENT "\t" NEWLINE "\n" SPACE " " $
[
  {
    "name": "Leonard Cohen",
    "lastSeen": 1478476800,
    "loggedOut": true
  }
]

Limitation

传递给command的json值最大深度只能为128如果嵌套深度大于128那么command将返回错误。

Redis lists

Redis lists为string values的链表redis list通常用于如下场景

  • 实现stack和queue
  • 用于backgroup worker system的队列管理

Blocking commands

redis lists中支持阻塞命令

  • BLPOP: 从head of a list移除并且返回一个element如果list为空该command会阻塞直至list被填充元素或发生超时
  • BLMOVE: 从source list中pop一个element并且将其push到target list中。如果source list为空那么command将会被阻塞直到source list中出现new element

在上述文档描述中,阻塞实际指的是针对客户端的阻塞。该Blocking Command命令的调用会实际的阻塞客户端直到redis server返回结果。

但是,server并不会被blocking command所阻塞。redis server为单线程模型当用户发送blocking command给server并且该command无法立即被执行而导致客户端阻塞时server会挂起该客户端的连接,并且转而处理其他请求,直至该客户端的阻塞命令满足执行条件时,才会将挂起的连接唤醒重新执行。

故而,Blocking Command并不会对server造成阻塞而是会阻塞客户端的调用。

Queue(first in, first out)

依次调用LPUSH后再依次调用RPOP,可模拟队列行为,元素的弹出顺序和元素的添加顺序相同:

> LPUSH bikes:repairs bike:1
(integer) 1
> LPUSH bikes:repairs bike:2
(integer) 2
> RPOP bikes:repairs
"bike:1"
> RPOP bikes:repairs
"bike:2"

Stack(first in, last out)

依次调用LPUSH后再依次调用LPOP,可模拟栈的行为,元素的移除顺序和添加顺序相反:

> LPUSH bikes:repairs bike:1
(integer) 1
> LPUSH bikes:repairs bike:2
(integer) 2
> LPOP bikes:repairs
"bike:2"
> LPOP bikes:repairs
"bike:1"

check length of list

可通过LLEN命令来检查list的长度

> LLEN bikes:repairs
(integer) 0

Atomically pop one element from one list and push to another

通过lmove命令能够实现原子的从srclist移除并添加到dstlist的操作

> LPUSH bikes:repairs bike:1
(integer) 1
> LPUSH bikes:repairs bike:2
(integer) 2
> LMOVE bikes:repairs bikes:finished LEFT LEFT
"bike:2"
> LRANGE bikes:repairs 0 -1
1) "bike:1"
> LRANGE bikes:finished 0 -1
1) "bike:2"

trim the list

可以通过LTRIM命令来完成对list的裁剪操作

> LPUSH bikes:repairs bike:1
(integer) 1
> LPUSH bikes:repairs bike:2
(integer) 2
> LMOVE bikes:repairs bikes:finished LEFT LEFT
"bike:2"
> LRANGE bikes:repairs 0 -1
1) "bike:1"
> LRANGE bikes:finished 0 -1
1) "bike:2"

Redis List Impl

redis lists是通过Linked List来实现的其元素的添加操作并开销永远是常量的并不会和Array一样因扩容而可能导致内存的复制。

LPUSH, RPUSH

LPUSH会将元素添加到list的最左端头部RPUSH则会将新元素添加奥list的最右端尾部

LPUSH和RPUSH接收的参数都是可变的在单次调用中可以向list中添加多个元素。

例如向空list中调用lpush 1 2 3时,其等价于lpush 1; lpush 2; lpush3即调用后list中元素为3,2,1

LRANGE

LRANGE命令能够从list中解析范围内的数据其接收两个indexes为range中开始和结束元素的位置。index可以是负的负数代表从尾端开始计数

  • -1代表最后的元素
  • -2代表倒数第二个元素
> RPUSH bikes:repairs bike:1
(integer) 1
> RPUSH bikes:repairs bike:2
(integer) 2
> LPUSH bikes:repairs bike:important_bike
(integer) 3
> LRANGE bikes:repairs 0 -1
1) "bike:important_bike"
2) "bike:1"
3) "bike:2"

LPOP, RPOP

lists支持pop元素从list中移除元素并且获取元素的值。lists支持从list的左端和右端pop元素使用示例如下所示

> RPUSH bikes:repairs bike:1 bike:2 bike:3
(integer) 3
> RPOP bikes:repairs
"bike:3"
> LPOP bikes:repairs
"bike:1"
> RPOP bikes:repairs
"bike:2"
> RPOP bikes:repairs
(nil)

Common use cases for lists

redis lists拥有如下有代表性的用例场景

  • 记录用户最新上传的推文
  • 用于进程间的通信使用consumer-producer pattern其中生产者向lists中推送内容而消费者消费lists中的内容

Capped lists - latest n

在许多用例场景下会使用lists来存储latest items例如social network updates, logs等。

redis允许将lists作为拥有容量上限的集合使用,可以通过LTRIM命令来实现only remembering the latest N items and discarding all the oldest items

LTRIM命令和LRANGE类似,但是LRANGE用于获取获取list中指定范围内的元素LTRIM会将选中的范围作为new list value所有位于选中范围之外的元素都会从list中被移除。

LTRIM的使用示例如下所示:

> RPUSH bikes:repairs bike:1 bike:2 bike:3 bike:4 bike:5
(integer) 5
> LTRIM bikes:repairs 0 2
OK
> LRANGE bikes:repairs 0 -1
1) "bike:1"
2) "bike:2"
3) "bike:3"

LTRIM 0 2会令redis保留index位于[0, 2]范围内的3个元素并且移除其他的元素。将push操作LTRIM操作组合,可以实现add a new element and discard elements exceeding a limt的操作。

例如,使用LRANGE -3 -1可用于实现仅保留最近添加的三个元素的场景

> RPUSH bikes:repairs bike:1 bike:2 bike:3 bike:4 bike:5
(integer) 5
> LTRIM bikes:repairs -3 -1
OK
> LRANGE bikes:repairs 0 -1
1) "bike:3"
2) "bike:4"
3) "bike:5"

Blocking operations on lists

lists的blocking operation特性令其适合用于实现queues并广泛用于进程间通信系统。

在通过redis lists实现进程间通信系统时如果某些时刻list为空并不存在任何元素那么此时消费者client在调用pop操作时只会返回为空。通常来讲consumer会等待一定的时间并且重新尝试调用pop该操作被称为polling,其通常被认为是一种不好的实现:

  • 其会强制redis/client来处理无用的命令(当list为空时pop请求只会返回为空而不会任何的实际处理)
  • 会增加delay to processing of items因为worker在接收到redis server返回的null时其会等待一定的时间。为了令delay更小可以在调用POP操作之间等待更短的时间但是其可能方法前一个问题(当pop调用之间的时间间隔更小时redis server可能会处理更多的无用命令)

故而redis实现支持BRPOPBLPOP命令其命令在list为空时会阻塞上述命令造成的阻塞会在list中被添加新元素时返回如果直到设置的超时到达后该操作也会返回

BRPOP的使用示例如下所示:

> RPUSH bikes:repairs bike:1 bike:2
(integer) 2
> BRPOP bikes:repairs 1
1) "bikes:repairs"
2) "bike:2"
> BRPOP bikes:repairs 1
1) "bikes:repairs"
2) "bike:1"
> BRPOP bikes:repairs 1
(nil)
(2.01s)

上述示例中,BRPOP bikes:repairs 1代表wait for elements in the list bikes:repairs,但是当list中元素为空时最多等待1s。

当为BRPOP指定timeout为0时代表会永久等待elements。并且可以为BRPOP命令指定多个lists其会等待所有的list并且当任何一个list中接收到元素时当前BRPOP命令会立刻返回

示例如下:

# client 1 等待 event-queue:1, event-queue:2, event-queue:3三个list
client 1> brpop event-queue:1 event-queue:2 event-queue:3 1000

# client 2 向event-queue:2 中追加元素
client 2> rpush event-queue:2 baka
(integer) 1

# client 1 立刻返回,返回结果如下
1) "event-queue:2"
2) "baka"
(19.55s)
BRPOP
  • 对于BRPOP命令造成的阻塞,其处理是按照顺序的:the first client that blocked waiting for a list, is served first when an element is pushed by some other client, and so forth
  • BRPOP命令的返回结果其结构和RPOP命令不同:BRPOP返回的是一个包含两个元素的arrayarrary[0]为list对应的keyarray[1]为弹出的元素因为BRPOP可以等待多个list
  • 如果超时后list中仍然没有可获取的元素那么将会返回null

Automatic Creation and removal of keys

在先前示例中向list中添加元素时并没有预先创建空的list或是在list中没有元素时将list手动移除。

在redis中list的创建和删除都是redis的职责

  • 当list中不再包含元素时redis会自动删除list对应的key
  • 当想要对不存在的key中添加元素时redis会自动创建一个empty list

故而,可以整理除如下准则:

  • 当将元素添加到一个聚合数据类型时如果target key不存在那么在添加元素前一个empty aggregate data type将会被自动创建
  • 当从aggregate data type中移除元素时如果移除后该aggregate data type中为空那么key将会被自动销毁stream data type除外)
  • 调用一个read-only command例如LLEN或write command用于移除元素对一个empty key做操作时,其返回结果和针对an key holding an empty aggregate type of type the command expects to find的操作一直

上述三个准则的示例如下:

准则1示例如下所示当new_bikes不存在时通过LPUSH命令向其中添加元素一个empty list在添加前会自动创建

> DEL new_bikes
(integer) 0
> LPUSH new_bikes bike:1 bike:2 bike:3
(integer) 3

准则2示例如下所示当pop出所有的元素后key将会被自动销毁通过EXISTS命令返回的结果为0

> LPUSH bikes:repairs bike:1 bike:2 bike:3
(integer) 3
> EXISTS bikes:repairs
(integer) 1
> LPOP bikes:repairs
"bike:3"
> LPOP bikes:repairs
"bike:2"
> LPOP bikes:repairs
"bike:1"
> EXISTS bikes:repairs
(integer) 0

准则3的示例如下所示当key不存在时对该key进行read-only操作和remove element操作所返回的结果,和对empty aggregated data type操作所返回的结果一致:

> DEL bikes:repairs
(integer) 0
> LLEN bikes:repairs
(integer) 0
> LPOP bikes:repairs
(nil)

redis sets

redis set是一个unordered collection of unique strings通过redis sets可以高效进行如下操作

  • track unique items
  • represent relations
  • perform common set operations such as intersection, unions, and differences

basic commands

  • SADD: 向set中添加new member
  • SREM: 从set中移除指定member
  • SISMEMBER: 检查给定的string是否位于set中
  • SINTER: 返回两个或更多set的交集
  • SCARD: 返回set的大小cardinality

SADD

SADD命令会向set中添加新的元素示例如下

> SADD bikes:racing:france bike:1 bike:2 bike:3
(integer) 3
> SMEMBERS bikes:racing:france
1) bike:3
2) bike:1
3) bike:2

SMEMBERS

在上述示例中,SMEMBERS命令会返回set中所有的元素。redis并不保证元素的返回顺序,每次调用SMEMBERS命令都可能以任何顺序返回set中的元素。

SDIFF

可以通过SDIFF来返回两个sets的差异差集。例如可以通过SDIFF命令查看有哪些元素位于set1中但是不位于set2中,示例如下:

> SADD bikes:racing:usa bike:1 bike:4
(integer) 2
> SDIFF bikes:racing:france bikes:racing:usa
1) "bike:3"
2) "bike:2"

上述示例中,则通过SDIFF命令展示了bikes:racing:francebikes:racing:usa两个set的差集。

SDIFF命令在difference between all sets is empty会返回一个empty array。

SINTER

可以通过SINTER命令来取多个sets的交集。

> SADD bikes:racing:france bike:1 bike:2 bike:3
(integer) 3
> SADD bikes:racing:usa bike:1 bike:4
(integer) 2
> SADD bikes:racing:italy bike:1 bike:2 bike:3 bike:4
(integer) 4
> SINTER bikes:racing:france bikes:racing:usa bikes:racing:italy
1) "bike:1"
> SUNION bikes:racing:france bikes:racing:usa bikes:racing:italy
1) "bike:2"
2) "bike:1"
3) "bike:4"
4) "bike:3"
> SDIFF bikes:racing:france bikes:racing:usa bikes:racing:italy
(empty array)
> SDIFF bikes:racing:france bikes:racing:usa
1) "bike:3"
2) "bike:2"
> SDIFF bikes:racing:usa bikes:racing:france
1) "bike:4"

SREM

可以通过SREM命令来移除set中的元素可以一次性移除一个或多个。

SPOP

SPOP命令支持随机移除一个element。

SRANDMEMBER

SRANDMEMBER命令支持随机返回一个set中的元素但是不对其实际移除

上述命令的使用示例如下所示:

> SADD bikes:racing:france bike:1 bike:2 bike:3 bike:4 bike:5
(integer) 5
> SREM bikes:racing:france bike:1
(integer) 1
> SPOP bikes:racing:france
"bike:3"
> SMEMBERS bikes:racing:france
1) "bike:2"
2) "bike:4"
3) "bike:5"
> SRANDMEMBER bikes:racing:france
"bike:2"

redis hashes

redis hashes为记录field-value pair集合的数据结构可以使用hashes来表示基本对象或存储counter的分组示例如下

对象表示

> HSET bike:1 model Deimos brand Ergonom type 'Enduro bikes' price 4972
(integer) 4
> HGET bike:1 model
"Deimos"
> HGET bike:1 price
"4972"
> HGETALL bike:1
1) "model"
2) "Deimos"
3) "brand"
4) "Ergonom"
5) "type"
6) "Enduro bikes"
7) "price"
8) "4972"

通常来讲可以存储在hash中的fields数量并没有限制。

命令HSET可用于向hash中设置多个fields而命令HGET可以用于获取一个fieldHMGET可以用于获取多个field。

> HMGET bike:1 model price no-such-field
1) "Deimos"
2) "4972"
3) (nil)

同样的hash结构支持对单个field进行操作例如HINCRBY

> HINCRBY bike:1 price 100
(integer) 5072
> HINCRBY bike:1 price -100
(integer) 4972

counters

将hash用于存储counters分组的示例如下所示

> HINCRBY bike:1:stats rides 1
(integer) 1
> HINCRBY bike:1:stats rides 1
(integer) 2
> HINCRBY bike:1:stats rides 1
(integer) 3
> HINCRBY bike:1:stats crashes 1
(integer) 1
> HINCRBY bike:1:stats owners 1
(integer) 1
> HGET bike:1:stats rides
"3"
> HMGET bike:1:stats owners crashes
1) "1"
2) "1"

Field Expiration

redis open source 7.4支持为独立的hash field指定超时

  • HEXPIRE: set the remaining TTL in seconds
  • HPEXPIRE: set the remaining TTL in milliseconds
  • HEXPIREAT: set expiration time to a timestamp specified in seconds
  • HPEXPIREAT: set the expiration time to a timestamp specified in milliseconds

如上所示在指定超时时可以通过时间戳来指定也可以通过TTL来指定。

同时,获取超时事件也可以通过时间戳TTL来获取:

  • HEXPIRETIME: get the expiration time as timestamp in seconds
  • HPEXPIRETIME: get the expiration time as timestamp in milliseconds
  • HTTL: get the remaining ttl in seconds
  • HPTTL: get the remaining ttl in milliseconds

如果想要移除指定hash field的expration可以通过如下方式

  • HPERSIST: 移除hash field的超时
Common field expiration use cases
  • Event Tracking使用hash key来存储最后一小时的事件。其中field为每个事件而设置事件field的ttl为1小时并可使用HLEN来统计最后一小时的事件数量。
  • Fraud Detection通常用户行为进行分析时可按小时记录用户的事件数量。可通过hash结果记录过去48小时中每小时的操作数量其中hash field代表用户某一个小时内的操作数。每个hash field的过期时间都为48h。
  • Customer session management: 可以通过hash来存储用户数据。为每个session创建一个hash key并且向hash key中添加session field。当session过期时自动对session key和session field进行expire操作。
  • Active Session Tracking: 将所有的active sessions存储再一个hash key中。每当session变为inactive时将session的TTL设置为过期。可以使用HLEN来统计活跃的sessions数量。
Field Expiration examples

对于hash field ixpiration的支持在官方client libraries中尚不可用,但是可以在python(redis-py)java(jedis)的beta版本client libraries中使用。

如下python示例展示了如何使用field expiration

event = {
      'air_quality': 256,
        'battery_level':89
}

r.hset('sensor:sensor1', mapping=event)

# set the TTL for two hash fields to 60 seconds
r.hexpire('sensor:sensor1', 60, 'air_quality', 'battery_level')
ttl = r.httl('sensor:sensor1', 'air_quality', 'battery_level')
print(ttl)
# prints [60, 60]

# set the TTL of the 'air_quality' field in milliseconds
r.hpexpire('sensor:sensor1', 60000, 'air_quality')
# and retrieve it
pttl = r.hpttl('sensor:sensor1', 'air_quality')
print(pttl)
# prints [59994] # your actual value may vary

# set the expiration of 'air_quality' to now + 24 hours
# (similar to setting the TTL to 24 hours)
r.hexpireat('sensor:sensor1', 
                datetime.now() + timedelta(hours=24), 
                'air_quality')
# and retrieve it
expire_time = r.hexpiretime('sensor:sensor1', 'air_quality')
print(expire_time)
# prints [1717668041] # your actual value may vary

Sorted sets

redis sorted set是一个包含unique strings的集合其中unique strings会关联一个score并且strings会按照score进行排序。如果多个string存在相同的score那么拥有相同score的strings将会按照字典序进行排序

sorted sets的用例场景包括如下

  • Leaderboards: 可以通过sorted sets维护一个ordered list其可被用于实现排行榜
  • RateLimiter: 通过sorted list可以构建一个sliding-window rate limiter从而避免过多的api调用
    • 在实现sliding-window rate limiter可以将时间戳作为score因为可以快速获取一个时间范围内的调用数量

可以将sorted sets看作是set和hash的混合

  • 和set一样sorted sets由unique strings组成
  • 和hash一样sorted sets中元素由一个关联的floating point value被称为score

sorted set的使用示例如下

> ZADD racer_scores 10 "Norem"
(integer) 1
> ZADD racer_scores 12 "Castilla"
(integer) 1
> ZADD racer_scores 8 "Sam-Bodden" 10 "Royce" 6 "Ford" 14 "Prickett"
(integer) 4

ZADDSADD类似,但是其接收一个用于表示score的额外参数。和SADD类似,可以使用ZADD来添加多个score-value pairs

Implementation

sorted sets实现的数据结构中同时使用了skip listhash table两种数据结构故而每次向zset中添加元素时其操作的复杂度为o(log(n))`。

并且,在获取元素时,由于元素已经被排序,获取操作无需其他的额外开销。

ZRANGE的顺序为从小到大,ZREVRANGE的顺序则是从大到小

> ZRANGE racer_scores 0 -1
1) "Ford"
2) "Sam-Bodden"
3) "Norem"
4) "Royce"
5) "Castilla"
6) "Prickett"
> ZREVRANGE racer_scores 0 -1
1) "Prickett"
2) "Castilla"
3) "Royce"
4) "Norem"
5) "Sam-Bodden"
6) "Ford"

在上述示例中0和-1代表index位于[0, len-1]范围内的元素。(负数代表的含义和LRANGE命令中相同)

ZRANGE命令中指定withscores也能够在返回zset中value时同时返回score

> ZRANGE racer_scores 0 -1 withscores
 1) "Ford"
 2) "6"
 3) "Sam-Bodden"
 4) "8"
 5) "Norem"
 6) "10"
 7) "Royce"
 8) "10"
 9) "Castilla"
10) "12"
11) "Prickett"
12) "14"

ZRANGEBYSCORE

除了上述操作外sorted sets还支持对operate on ranges。可以通过ZRANGEBYSCORE来实现get all racers with 10 or fewer points的操作:

> ZRANGEBYSCORE racer_scores -inf 10
1) "Ford"
2) "Sam-Bodden"
3) "Norem"
4) "Royce"

上述示例中,ZRANGEBYSCORE racer_score -inf 10向redis请求返回score位于negative infinity和10之间both included的元素

ZREMRANGEBYSCORE / ZREM

如果想要丛zset中移除元素可以调用ZREM命令。同样的sorted sets也支持remove ranges of elements的操作,可通过ZREMRANGEBYSCORE命令来实现。

> ZREM racer_scores "Castilla"
(integer) 1
> ZREMRANGEBYSCORE racer_scores -inf 9
(integer) 2
> ZRANGE racer_scores 0 -1
1) "Norem"
2) "Royce"
3) "Prickett"

上述示例中,通过ZREMRANGEBYSCORE racer_scores -inf 9命令实现了remove all the racers with strictly fewer than 10 points的操作。

ZREMRANGEBYSCORE会返回其移除的元素个数。

Inclusive and exclusive

在使用ZRANGEBYSCORE指定minmax时,可以分别将其指定为-inf+inf。故而在获取zset中比xx大或比xx小的所有元素时,可以使用-inf+inf此时无需知道当前zset中的最大/最小元素。

默认情况下,interval specified by min and max is closed(inclusive)。但是,可以将其指定为open interval(exclusive)只需要在score前添加(符号即可,示例如下:

ZRANGEBYSCORE zset (1 5

上述示例中,会返回位于(1, 5]区间内的元素。

ZRANGEBYSCORE zset (5 (10命令则是会返回(5, 10)区间内的元素。

ZRANK / ZREVRANK

sorted sets还支持get-rank operation,通过ZRANK可以返回position of an element in the set of ordered elements

ZREVRANK命令的作用和ZRANK类似,但是ZREVRANK返回的值为从大到小的降序ranking

使用示例如下所示:

> ZRANK racer_scores "Norem"
(integer) 0
> ZREVRANK racer_scores "Norem"
(integer) 2

Lexicographical scores

自redis 2.8其,引入了getting ranges lexicograhpically的新特性其假设sorted set中的所有元素都拥有相同的score。

lexicographical ranges进行交互的主要命令如下:

  • ZRANGEBYLEX
  • ZREVRANGEBYLEX
  • ZREMRANGEBYLEX
  • ZLEXCOUNT

使用示例如下所示:

> ZADD racer_scores 0 "Norem" 0 "Sam-Bodden" 0 "Royce" 0 "Castilla" 0 "Prickett" 0 "Ford"
(integer) 3
> ZRANGE racer_scores 0 -1
1) "Castilla"
2) "Ford"
3) "Norem"
4) "Prickett"
5) "Royce"
6) "Sam-Bodden"
> ZRANGEBYLEX racer_scores [A [L
1) "Castilla"
2) "Ford"

在上述示例中,可以通过ZRANGEBYLEX按照字典序对range进行请求。

ZRANGEBYLEX

ZRANGEBYLEX的语法如下

ZRANGEBYLEX key min max [limit offset count]

ZRANGEBYSCORE命令不同的是,ZRANGEBYSCORE在指定范围时,默认是included的;而ZRANGEBYLEX必须通过[(来显式指定inclusive或exclusive。

而在指定min和max时-+分别则代表negatively infinite和positive infinite。故而ZRANGEBYLEX myzset - +命令代表返回zset中所有的元素。

Updating the score: leaderboards

支持对sorted set中元素的score进行更新。在更新sorted set中元素的score时只需要再次调用ZADD命令即可sorted set会更新score更新操作的时间复杂度为O(log(N))

Leaderboard Example

在通过zset实现leaderboard时由如下两种方式对user score进行更新

  • 在得知user当前score的情况下可以直接通过ZADD命令来进行覆盖
  • 如果想要针对当前的score进行增加操作时,可以使用ZINCRBY命令
> ZADD racer_scores 100 "Wood"
(integer) 1
> ZADD racer_scores 100 "Henshaw"
(integer) 1
> ZADD racer_scores 150 "Henshaw"
(integer) 0
> ZINCRBY racer_scores 50 "Wood"
"150"
> ZINCRBY racer_scores 50 "Henshaw"
"200"
ZADD

当当前添加的元素已经在sorted set中存在时ZADD命令会返回0,否则ZADD命令会返回1

ZINCRBY

ZINCRBY命令则是会返回更新后的new score。

redis Streams

Redis Stream类型数据结构的行为类似于append-only log但是实现了o(1)时间复杂度的random access和复杂的消费策略、consumer groups。通过redis stream可以实时的记录事件并对事件做同步分发。

通用的redis stream用例如下

  • event sourcing(e.g., tracking user actions)
  • sensor monitoring
  • notifications(e.g., storing a record of each user's notifications in a separate stream)

redis会为每个stream entry生成一个unique ID。可以使用IDs来获取其关联的entries或读取和处理stream中所有的后续entries。

redis stream支持一些trimming strategies用于避免stream的无尽增长。并且redis stream也支持多种消费策略XREAD, XREADGROUP, XRANGE

Examples

添加stream entry

在如下示例中当racers通过检查点时将会为每个racer添加一个stream entrystream entry中包含racer name, speed, position, location ID信息示例如下

> XADD race:france * rider Castilla speed 30.2 position 1 location_id 1
"1692632086370-0"
> XADD race:france * rider Norem speed 28.8 position 3 location_id 1
"1692632094485-0"
> XADD race:france * rider Prickett speed 29.7 position 2 location_id 1
"1692632102976-0"

从指定id开始读取stream entries

在如下示例中将从stream entry ID 1692632086370-0开始读取两条stream entries

> XRANGE race:france 1692632086370-0 + COUNT 2
1) 1) "1692632086370-0"
   2) 1) "rider"
      2) "Castilla"
      3) "speed"
      4) "30.2"
      5) "position"
      6) "1"
      7) "location_id"
      8) "1"
2) 1) "1692632094485-0"
   2) 1) "rider"
      2) "Norem"
      3) "speed"
      4) "28.8"
      5) "position"
      6) "3"
      7) "location_id"
      8) "1"

从末尾开始读取

在如下示例中,会从end of the stream开始读取entries最多读取100条entries并在没有entries被写入的情况下最多阻塞300ms

> XREAD COUNT 100 BLOCK 300 STREAMS race:france $
(nil)

Stream basics

stream为append-only数据结构其基础的write command为XADD会向指定stream中添加一个新的entry。

每个stream entry都由一个或多个field-value pairs组成类似dictionary或redis hash

> XADD race:france * rider Castilla speed 29.9 position 1 location_id 2
"1692632147973-0"
XADD

上述示例中,通过XADD向key为race:france中添加了值为rider: Castilla, speed:29.9, position: 1, location_id: 2的entry并使用了auto-generated entry ID 1692632147973-0作为返回值。

XADD命令的描述如下

XADD key [NOMKSTREAM] [KEEPREF | DELREF | ACKED] [<MAXLEN | MINID> [= | ~] threshold [LIMIT count]] <* | id> field value [field value ...]

XADD race:france * rider Castilla speed 29.9 position 1 location_id 2的命令示例中,

  • 第一个参数race:france代表key name
  • 第二个参数为entry id*代表stream中所有的entry id
    • 在上述示例中,为第二个参数传入*代表希望server生成一个新的ID
    • 所有新生成的ID都应该单调递增即新生成的ID将会比过去所有entries的ID都要大
    • 需要显式指定ID而不是新生成的场景比较罕见
  • 位于第一个和第二个参数之后的为field-value pairs
XLEN

可以通过XLEN命令来获取Stream中items的个数

> XLEN race:france
(integer) 4

Entry IDs

entry ID由XADD命令返回并且可以对给定stream中的entries进行唯一标识。entry ID由两部分组成

<millisecondsTime>-<sequenceNumber>
  • millisecondsTime: 该部分代表生成stream id时redis node的本地时间。但是如果current milliseconds time比previous entry time要小,则会使用previous entry time而不是current milliseconds
    • 该设计主要是为了解决时钟回拨的问题即使在redis node回拨本地时间的场景下新生成的ID仍然能够保证单调递增
  • sequenceNumber: 该部分主要是为了处理同一ms内创建多条entries的问题
    • sequence number的宽度为64bit
entry ID设计

entry ID中包含millisecondsTime的原因是Reids Stream支持range queries by ID。因为ID关联entry的生成时间故而可以不花费额外成本的情况下按照time range对entries进行查询。

当在某种场景下,用户可能需要incremental IDs that are not related to time but are actually associated to another external system ID,此时XADD则可以在第二个参数接收一个实际的ID而不是*通配符。

  • *符号会触发auto-generation

手动指定entry ID的示例如下所示

> XADD race:usa 0-1 racer Castilla
0-1
> XADD race:usa 0-2 racer Norem
0-2

在通过XADD手动指定entry ID时后添加的entry ID必须大于先前指定的entry ID否则将会返回error。

> XADD race:usa 0-1 racer Prickett
(error) ERR The ID specified in XADD is equal or smaller than the target stream top item

在redis 7及之后可以仅显式指定millisecondsTime的部分,指定后sequenceNumber的部分将会自动生成并填充,示例如下所示:

> XADD race:usa 0-* racer Prickett
0-3

Redis Stream consumer query model

向redis stream中添加entry的操作可以通过XADD进行实现。而从redis stream从读取数据redis stream支持如下query model

  • Listening for new items: 和unix command tail -f类似reids stream consumer会监听new messages that are appended to the stream
    • 但是和BLPOP这类阻塞操作不同的是对于BLPOP,一个给定元素只会被传递给一个client而在使用stream时我们希望new messages appended to the stream时新消息对多个consumers可见。tail -f同样支持新被添加到log文件中的内容被多个进程可见
    • 故而,在Listening for new items这种query model下stream is able to fan out messages to multiple clients
  • Query by range: 除了上述类似tail -f的query model外可能还希望将redis stream以另一种方式进行使用并不将redis stream作为messaging system而是将其看作time series store
    • 在将其作为time series store的场景下其仍然能访问最新的messages但是其还支持get messages by ranges of time的操作,或是iterate the messages using a cursor to incrementally check all the history
  • Consumer group: 上述两种场景都是从consuemrs的视角来读取/处理消息但是redis stream支持另一种抽象a stream of messages that can be partitioned to multiple consumers that are processing such messages
    • 在该场景下并非每个consumer都必须处理所有的messsages每个consumer可以获取不同的messages并进行处理

redis stream通过不同的命令支持了以上三种query model。

Querying by range: XRANGE and XREVRANGE

为了根据范围查询stream需要指定两个idstart IDend ID。指定的范围为inclusive的,包含start IDend ID

+-则是代表greatest IDsmallest ID,示例如下所示:

> XRANGE race:france - +
1) 1) "1692632086370-0"
   2) 1) "rider"
      2) "Castilla"
      3) "speed"
      4) "30.2"
      5) "position"
      6) "1"
      7) "location_id"
      8) "1"
2) 1) "1692632094485-0"
   2) 1) "rider"
      2) "Norem"
      3) "speed"
      4) "28.8"
      5) "position"
      6) "3"
      7) "location_id"
      8) "1"
3) 1) "1692632102976-0"
   2) 1) "rider"
      2) "Prickett"
      3) "speed"
      4) "29.7"
      5) "position"
      6) "2"
      7) "location_id"
      8) "1"
4) 1) "1692632147973-0"
   2) 1) "rider"
      2) "Castilla"
      3) "speed"
      4) "29.9"
      5) "position"
      6) "1"
      7) "location_id"
      8) "2"

如上所示返回的每个entry都是由IDfield-value pairs组成的数组。由于entry ID和时间currentMillisTime相关联故而可以通过XRANGE根据时间范围来查询records。

query by time range

并且在根据时间范围查询records时可以省略sequence part的部分

  • 在省略sequence part时start的sequence part将会被设置为0end的sequence part将会被设置为最大值。
  • 故而可以通过两个milliseconds unix time来进行时间范围内的查询可以获取在该时间范围内生成的entriesrange is inclusive

示例如下

> XRANGE race:france 1692632086369 1692632086371
1) 1) "1692632086370-0"
   2) 1) "rider"
      2) "Castilla"
      3) "speed"
      4) "30.2"
      5) "position"
      6) "1"
      7) "location_id"
      8) "1"
count option

XRANGE在范围查询时还支持指定一个COUNT选项用于get first N items。如果想要进行增量查询,可以用上次查询的最大ID + 1作为新一轮查询的start

增量迭代示例如下:

> XRANGE race:france - + COUNT 2
1) 1) "1692632086370-0"
   2) 1) "rider"
      2) "Castilla"
      3) "speed"
      4) "30.2"
      5) "position"
      6) "1"
      7) "location_id"
      8) "1"
2) 1) "1692632094485-0"
   2) 1) "rider"
      2) "Norem"
      3) "speed"
      4) "28.8"
      5) "position"
      6) "3"
      7) "location_id"
      8) "1"

# 通过(符号指定了左开右闭的区间
> XRANGE race:france (1692632094485-0 + COUNT 2
1) 1) "1692632102976-0"
   2) 1) "rider"
      2) "Prickett"
      3) "speed"
      4) "29.7"
      5) "position"
      6) "2"
      7) "location_id"
      8) "1"
2) 1) "1692632147973-0"
   2) 1) "rider"
      2) "Castilla"
      3) "speed"
      4) "29.9"
      5) "position"
      6) "1"
      7) "location_id"
      8) "2"

# 返回为空,迭代完成
> XRANGE race:france (1692632147973-0 + COUNT 2
(empty array)

XRANGE操作查找的时间复杂度为O(long(N))切返回M个元素的时间复杂度为O(M)

命令XREVRANGEXRANGE集合等价,但是会按照逆序来返回元素,故而,针对XREVRANGE传参时参数的顺序也需要颠倒

> XREVRANGE race:france + - COUNT 1
1) 1) "1692632147973-0"
   2) 1) "rider"
      2) "Castilla"
      3) "speed"
      4) "29.9"
      5) "position"
      6) "1"
      7) "location_id"
      8) "2"

Listening for new items with XREAD

在某些场景下,我们可能希望对new items arriving to the stream进行订阅,如下两种场景都希望对new items进行订阅

  • Redis Pub/Sub场景中通过redis stream来对channel进行订阅
  • 在Redis blocking lists场景中从redis stream中等并并获取new element

但是,上述两种场景在如下方面有所不同

  • 一个stream可以包含多个clients(consumers)对于new item默认情况下会从传递给每一个等待当前stream的consumer
    • Pub/Sub场景和上述行为相符其会将new item传递给每一个consumerfan out
    • 在blocking lists场景和上述行为并不相符每个consumer都会获取到不同的element相同的element只会被传递给一个consumer
  • 其对message的处理也有不同
    • Pub/Sub场景下,将会对消息进行fire and forget操作,消息将永远不会被存储
    • 在使用blocking lists如果消息被client接收其将会从list中移除
  • Stream Consumer Groups提供了Pub/Subblocking lsits都无法实现的控制不同的groups可以针对相同stream进行订阅并且对被处理items进行显式的ack可以对unprocessed messages进行声明;只有private past history of messages对client可见

对于提供Listening for new items arriving into stream支持的命令,被称为XREAD。其使用示例如下:

> XREAD COUNT 2 STREAMS race:france 0
1) 1) "race:france"
   2) 1) 1) "1692632086370-0"
         2) 1) "rider"
            2) "Castilla"
            3) "speed"
            4) "30.2"
            5) "position"
            6) "1"
            7) "location_id"
            8) "1"
      2) 1) "1692632094485-0"
         2) 1) "rider"
            2) "Norem"
            3) "speed"
            4) "28.8"
            5) "position"
            6) "3"
            7) "location_id"
            8) "1"
XREAD with STREAMS option

上述示例为XREAD命令的non-blocking形式,并且COUNT option并非强制的唯一的强制option为STREAMS

XREAD在和STREAMS一起使用时并且需要指定a list of keysmaximum ID already seen for each stream by the calling consumer故而该命令只会向consumer返回messages with and ID greater than the one we specified

在上述示例中,命令为STREAMS race:france 0,其代表race:france流中所有ID比0-0大的消息。故而返回的结构中顶层为stream的key name因为上述命令可以指定多个stream针对多个stream进行监听