42 KiB
- redis
- Using Command
- Data Types
- Redis Strings
- JSON
- Redis lists
- Blocking commands
- Queue(first in, first out)
- Stack(first in, last out)
- check length of list
- Atomically pop one element from one list and push to another
- trim the list
- Redis List Impl
LPUSH, RPUSHLRANGELPOP, RPOP- Common use cases for lists
- Capped lists -
latest n - Blocking operations on lists
- Automatic Creation and removal of keys
- redis sets
- redis hashes
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.to或comment:4321:reply-to - key size的最大允许大小为512MB
Hashtags
redis通过hashing来获取key关联的value。
通常,整个key都会被用作hash index的计算,但是,在部分场景下,开发者可能只希望使用key中的一部分来计算hash index。此时,可以通过{}包围key中想要计算hash index的部分,该部分被称为hash-tag`。
例如,person:1和person: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:1和group: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超过5s,key已经不存在。
上述示例中,expire key 5将key的超时时间设置为了5s,EXPIRE用于为key指定不同的超时时间。
类似的,可以通过PERSIST命令来取消key的超时设置,让key永久被保留。
除了使用expire来设置超时外,在创建时也能会key指定expiration:
> set key 100 ex 10
OK
> ttl key
(integer) 9
上述示例中,使用ttl命令来检查key的超时时间。
如果想要按照毫秒来设置超时,可以使用PEXPIRE和PTTL命令。
Navigating the keyspace
Scan
SCAN命令支持对redis中key的增量迭代,在每次调用时只会返回一小部分数据。该命令可以在生产中使用,并不会像keys或smembers等命令一样,在处理大量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: 匹配hallo和helloh[^e]llo: [^e]匹配除e外的任何字符h[a-b]llo: 匹配hallo和hbllo
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条命令的命令序列如下:
- client: incr x
- server: 1
- client: incr x
- server: 2
- client: incr x
- server: 3
- client: incr x
- 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时,会进行write和read的系统调用,其涉及到用户态和内核态的切换,这将带来巨大的开销。
如果使用pipeline,多条指令只需要调用一次read系统调用,并且多条执行的执行结果只需要通过一次write系统调用即能够执行。通过使用pipeline,能够有效降低redis server的系统调用次数,这将减少socket io带来的开销,故而redis server能够在一秒内执行更多的commands。
pipelining vs scripting
相比于pipelining,scripting可以在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
上述逻辑在只存在一个客户端的场景下工作正常,但是当存在多个客户端时,将会发生竞争。由于上述逻辑并不是原子的,故而可能出现如下场景:
- client A read old value
- client B read old value
- client A compute
old value + 1 - client B compute
old value + 1 - client A set new value with
old value + 1 - 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 mykeyGET 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支持increment和multiply操作:
> 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来进行格式化
INDENTNEWLINESPACE
$ 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实现支持BRPOP和BLPOP命令,其命令在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返回的是一个包含两个元素的array,arrary[0]为list对应的key,array[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 memberSREM: 从set中移除指定memberSISMEMBER: 检查给定的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:france和bikes: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可以用于获取一个field,HMGET可以用于获取多个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 secondsHPEXPIRE: set the remaining TTL in millisecondsHEXPIREAT: set expiration time to a timestamp specified in secondsHPEXPIREAT: set the expiration time to a timestamp specified in milliseconds
如上所示,在指定超时时,可以通过时间戳来指定,也可以通过TTL来指定。
同时,获取超时事件也可以通过时间戳和TTL来获取:
HEXPIRETIME: get the expiration time as timestamp in secondsHPEXPIRETIME: get the expiration time as timestamp in millisecondsHTTL: get the remaining ttl in secondsHPTTL: 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