Redis提供的5种数据结构已经足够强大,但除此之外,Redis还提供了诸如慢查询分析、功能强大的Redis Shell、Pipeline、事务与Lua脚本、Bitmaps、HyperLogLog、发布订阅、GEO等附加功能,这些功能可以在某些场景发挥重要的作用。
慢查询分析
许多存储系统(例如MySQL)提供慢查询日志帮助开发和运维人员定位系统存在的慢操作。所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阀值,就将这条命令的相关信息(例如:发生时间,耗时,命令的详细信息)记录下来,Redis也提供了类似的功能。
Redis客户端执行一条命令经历4个过程:发送命令、命令排队、命令执行、返回结果
慢查询的两个配置参数
slowlog-log-slower-than: 它的单位是微秒,默认值是10000,假如执行了一条“很慢”的命令(例如keys*),如果它的执行时间超过了10000微秒,那么它将被记录在慢查询日志中。slowlog-log-slower-than=0会记录所有的命令,slowlog-log-slower-than<0对于任何命令都不会进行记录。
slowlog-max-len:Redis使用了一个列表来存储慢查询日志,slowlog-max-len就是列表的最大长度。一个新的命令满足慢查询条件时插入到这个列表中,当慢查询日志列表已处于其最大长度时,最早插入的一个命令将从列表中移出,例如slowlog-max-len设置为5,当有第6条慢查询插入的话,那么队头的第一条数据就出列,第6条慢查询就会入列。
在Redis中有两种修改配置的方法:
- 修改配置文件
 - 使用
config set命令动态修改 
下面使用config set命令将slowlog-log-slower-than设置为20000微秒,slowlog-max-len设置为1000,config rewrite将配置持久化到本地配置文件:
config set slowlog-log-slower-than 20000
config set slowlog-max-len 1000
config rewrite
获取慢查询日志
slowlog get [n]
参数n可以指定条数:
127.0.0.1:6379> slowlog get
1) 1) (integer) 666
2) (integer) 1456786500
3) (integer) 11615
4) 1) "BGREWRITEAOF"
2) 1) (integer) 665
2) (integer) 1456718400
3) (integer) 12006
4) 1) "SETEX"
2) "video_info_200"
3) "300"
4) "2"
可以看到每个慢查询日志有4个属性组成,分别是慢查询日志的标识id、发生时间戳、命令耗时、执行命令和参数。
获取慢查询日志列表当前的长度
127.0.0.1:6379> slowlog len
(integer) 45
慢查询日志重置
127.0.0.1:6379> slowlog reset
OK
127.0.0.1:6379> slowlog len
(integer) 0
Pipeline
Redis提供了批量操作命令(例如mget、mset等),有效地节约RTT。但大部分命令是不支持批量操作的,例如要执行n次hgetall命令,并没有mhgetall命令存在,需要消耗n次RTT。
Pipeline(流水线)机制能改善上面这类问题,它能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端。
可以使用Pipeline模拟出批量操作的效果,但是在使用时要注意它与原生批量命令的区别,具体包含以下几点:
- 原生批量命令是原子的,
Pipeline是非原子的。 - 原生批量命令是一个命令对应多个key,
Pipeline支持多个命令。 - 原生批量命令是
Redis服务端支持实现的,而Pipeline需要服务端和客户端的共同实现。 
每次Pipeline组装的命令个数不能没有节制,否则一次组装Pipeline数据量过大,一方面会增加客户端的等待时间,另一方面会造成一定的网络阻塞,可以将一次包含大量命令的Pipeline拆分成多次较小的Pipeline来完成。
事务与Lua
为了保证多条命令组合的原子性,Redis提供了简单的事务功能以及集成Lua脚本来解决这个问题。
事务
事务表示一组动作,要么全部执行,要么全部不执行。例如在社交网站上用户A关注了用户B,那么需要在用户A的关注表中加入用户B,并且在用户B的粉丝表中添加用户A,这两个行为要么全部执行,要么全部不执行,否则会出现数据不一致的情况。
Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd user:a:follow user:b
QUEUED
127.0.0.1:6379> sadd user:b:fans user:a
QUEUED
sadd命令此时的返回结果是QUEUED,代表命令并没有真正执行,而是暂时保存在Redis中。如果此时另一个客户端执行sismember user:a:follow user:b返回结果应该为0。
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 0
只有当exec执行后,用户A关注用户B的行为才算完成:
127.0.0.1:6379> exec
1) (integer) 1
2) (integer) 1
另一个客户端:
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 1
如果要停止事务的执行,可以使用discard命令代替exec命令即可。
如果事务中的命令出现错误,Redis的处理机制也不尽相同:
命令错误
例如下面操作错将set写成了sett,属于语法错误,会造成整个事务无法执行,key和counter的值未发生变化:
127.0.0.1:6379> mget key counter
1) "hello"
2) "100"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sett key world
(error) ERR unknown command 'sett'
127.0.0.1:6379> incr counter
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
运行时错误
例如用户B在添加粉丝列表时,误把sadd命令写成了zadd命令,这种就是运行时命令,因为语法是正确的:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd user:a:follow user:b
QUEUED
127.0.0.1:6379> zadd user:b:fans 1 user:a
QUEUED
127.0.0.1:6379> exec
1) (integer) 0
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 1
可以看到Redis并不支持回滚功能,sadd user:a:follow user:b命令已经执行成功,开发人员需要自己修复这类问题。
有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修改过,才执行事务,否则不执行(类似乐观锁)。Redis提供了watch命令来解决这类问题。
“客户端-1”在执行multi之前执行了watch命令,“客户端-2”在“客户端-1”执行exec之前修改了key值,造成事务没有执行(exec结果为nil)
客户端1:
127.0.0.1:6379> set key "java"
OK
127.0.0.1:6379> watch key
OK
127.0.0.1:6379> multi
OK
客户端2:
127.0.0.1:6379> append key python
(integer) 10
客户端1:
127.0.0.1:6379> append key jedis
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get key
"javapython"
Redis提供了简单的事务,之所以说它简单,主要是因为它不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算。
Redis与Lua
在Redis中使用Lua
在Redis中执行Lua脚本有两种方法:eval和evalsha。
eval
eval 脚本内容 key个数 key列表 参数列表
127.0.0.1:6379> eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world
"hello redisworld"
此时KEYS[1]=”redis”,ARGV[1]=”world”,所以最终的返回结果是”hello redisworld”。
如果Lua脚本较长,还可以使用redis-cli--eval直接执行文件。
eval命令和--eval参数本质是一样的,客户端如果想执行Lua脚本,首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端。
evalsha
除了使用eval,Redis还提供了evalsha命令来执行Lua脚本。首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和,evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用。
加载脚本: script load命令可以将脚本内容加载到Redis内存中,例如下面将lua_get.lua加载到Redis中,得到SHA1:
[heql@ubuntu ~]$ redis-cli script load "$(cat lua_get.lua)"
"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
执行脚本: evalsha的使用方法如下,参数使用SHA1值,执行逻辑和eval一致。
evalsha 脚本SHA1值 key个数 key列表 参数列表
127.0.0.1:6379> evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world
"hello redisworld"
Lua的Redis API
Lua可以使用redis.call函数实现对Redis的访问:
127.0.0.1:6379> eval 'return redis.call("set", KEYS[1], ARGV[1])' 1 hello world
OK
127.0.0.1:6379> eval 'return redis.call("get", KEYS[1])' 1 hello
"world"
除此之外Lua还可以使用redis.pcall函数实现对Redis的调用,redis.call和redis.pcall的不同在于,如果redis.call执行失败,那么脚本执行结束会直接返回错误,而redis.pcall会忽略错误继续执行脚本。
Lua脚本功能为Redis开发和运维人员带来如下三个好处:
Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis内存中,实现复用的效果。Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
Redis如何管理Lua脚本
Redis提供了4个命令实现对Lua脚本的管理:
script load
此命令用于将Lua脚本加载到Redis内存中。
script exists
scripts exists sha1 [sha1 …]
此命令用于判断sha1是否已经加载到Redis内存中:
127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
1) (integer) 1
返回结果代表sha1[sha1…]被加载到Redis内存的个数。
script flush
此命令用于清除Redis内存已经加载的所有Lua脚本。
script kill
此命令用于杀掉正在执行的Lua脚本。如果Lua脚本比较耗时,甚至Lua脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或者外部进行干预将其结束。
执行Lua脚本,进入死循环,当前客户端会阻塞:
127.0.0.1:6379> eval 'while 1==1 do end' 0
Redis提供了一个lua-time-limit参数,默认是5秒,它是Lua脚本的“超时时间”,但这个超时时间仅仅是当Lua脚本时间超过lua-time-limit后,向其他命令调用发送BUSY的信号,但是并不会停止掉服务端和客户端的脚本执行,所以当达到lua-time-limit值之后,其他客户端在执行正常的命令时,将会收到“Busy Redis is busy running a script”错误,并且提示使用script kill或者shutdown nosave命令来杀掉这个busy的脚本:
127.0.0.1:6379> get hello
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
但是有一点需要注意,如果当前Lua脚本正在执行写操作,那么script kill将不会生效:
127.0.0.1:6379> eval 'while 1 == 1 do redis.call("set", "k", "v") end' 0
此时如果执行script kill,会收到如下异常信息:
127.0.0.1:6379> script kill
(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.
上面提示Lua脚本正在向Redis执行写命令,要么等待脚本执行结束要么使用shutdown save停掉Redis服务。
Bitmaps
Redis提供了Bitmaps这个可以实现对位的操作:
Bitmaps本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作。- 在
Redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。 
命令
设置值
setbit key offset value
127.0.0.1:6379> setbit a 0 1
(integer) 0
127.0.0.1:6379> setbit a 5 1
(integer) 0
127.0.0.1:6379> setbit a 11 1
(integer) 0
127.0.0.1:6379> setbit a 15 1
(integer) 0
127.0.0.1:6379> setbit a 19 1
(integer) 0
在第一次初始化Bitmaps时,假如偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞。
获取值
gitbit key offset
返回0说明没有设置过或offset根本不存在:
127.0.0.1:6379> getbit a 19
(integer) 1
127.0.0.1:6379> getbit a 200
(integer) 0
获取Bitmaps指定范围值为1的个数
bitcount [start][end]
[start]和[end]代表起始和结束字节数:
127.0.0.1:6379> bitcount a 
(integer) 5
127.0.0.1:6379> bitcount a 1 3
(integer) 3
Bitmaps间的运算
bitop op destkey key[key….]
bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中:
127.0.0.1:6379> bitop and c a b 
(integer) 2
计算Bitmaps中第一个值为targetBit的偏移量
bitpos key targetBit [start] [end]
127.0.0.1:6379> bitpos a 1
(integer) 1
[start]和[end]代表起始和结束字节数。例如计算第0个字节到第1个字节之间,第一个值为0的偏移量:
127.0.0.1:6379> bitpos a 0 0 1
(integer) 1
HyperLogLog
HyperLogLog并不是一种新的数据结构(实际类型为字符串类型),而是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数的统计。
添加
pfadd用于向HyperLogLog添加元素,如果添加成功返回1:
127.0.0.1:6379> pfadd ids "uuid-1" "uuid-2" "uuid-3" "uuid-4"
(integer) 1
计算独立用户数
pfcount用于计算一个或多个HyperLogLog的独立总数:
127.0.0.1:6379> pfcount ids
(integer) 4
合并
pfmerge destkey sourcekey [sourcekey …]
pfmerge可以求出多个HyperLogLog的并集并赋值给destkey:
127.0.0.1:6379> pfadd ids_1 "uuid-1" "uuid-2" "uuid-3" "uuid-4"
(integer) 1
127.0.0.1:6379> pfadd ids_2 "uuid-4" "uuid-5" "uuid-6" "uuid-7"
(integer) 1
127.0.0.1:6379> pfmerge ids_3 ids_1 ids_2
OK
127.0.0.1:6379> pfcount ids_3
(integer) 7
HyperLogLog内存占用量非常小,但是存在错误率,选取使用HyperLogLog应当确认:
- 只为了计算独立总数,不需要获取单条数据。
 - 可以容忍一定误差率,毕竟
HyperLogLog在内存的占用量上有很大的优势。 
发布订阅
Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消息,订阅该频道的每个客户端都可以收到该消息。
命令
Redis主要提供了发布消息、订阅频道、取消订阅以及按照模式订阅和取消订阅等命令。
发布消息
publish channel message
返回结果为订阅者个数:
127.0.0.1:6379> publish channel:sports "Tim won the championship"
(integer) 0
订阅消息
subscribe channel [channel …]
订阅者可以订阅一个或多个频道:
127.0.0.1:6379> subscribe channel:sports
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel:sports"
3) (integer) 1
此时另一个客户端发布一条消息:
27.0.0.1:6379> publish channel:sports "James lost the championship"
(integer) 1
当前订阅者客户端会收到如下消息:
127.0.0.1:6379> subscribe channel:sports
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel:sports"
3) (integer) 1
1) "message"
2) "channel:sports"
3) "James lost the championship"
有关订阅命令有两点需要注意:
- 客户端在执行订阅命令之后进入了订阅状态,只能接收
subscribe、psubscribe、unsubscribe、punsubscribe的四个命令。 - 新开启的订阅客户端,无法收到该频道之前的消息,因为
Redis不会对发布的消息进行持久化。 
取消订阅
客户端可以通过unsubscribe命令取消对指定频道的订阅,取消成功后,不会再收到该频道的发布消息:
127.0.0.1:6379> unsubscribe channel:sports
1) "unsubscribe"
2) "channel:sports"
3) (integer) 0
按照模式订阅和取消订阅
psubscribe pattern [pattern…]
punsubscribe [pattern [pattern …]]
除了subcribe和unsubscribe命令,Redis命令还支持glob风格的订阅命令psubscribe和取消订阅命令punsubscribe,例如下面操作订阅以it开头的所有频道:
127.0.0.1:6379> psubscribe it*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "it*"
3) (integer) 1
查询订阅
查看活跃的频道:
pubsub channels [pattern]
所谓活跃的频道是指当前频道至少有一个订阅者,其中[pattern]是可以指定具体的模式:
127.0.0.1:6379> pubsub channels
1) "channel:sports"
127.0.0.1:6379> 
127.0.0.1:6379> pubsub channels channel:*s
1) "channel:sports"
查看频道订阅数:
pubsub numsub [channel …]
127.0.0.1:6379> pubsub numsub channel:sports
1) "channel:sports"
2) (integer) 1