浅谈Redis之发布订阅、事务、排序、位图、慢查询、监视器

这里就Redis的发布订阅、事务、排序、位图、慢查询、监视器等方面进行介绍

abstract.png

发布与订阅

基本实践

Redis在发布与订阅方面提供了以下命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 发布消息到指定频道
PUBLISH <channel> <message>

# 订阅一个或多个频道
SUBSCRIBE <channel> [channel ...]

# 退订一个或多个频道
UNSUBSCRIBE <channel> [channel ...]

# 订阅一个或多个符合给定模式的频道
PSUBSCRIBE <pattern> [pattern ...]

# 退订一个或多个符合给定模式的频道
PUNSUBSCRIBE [pattern [pattern ...]]

在A、B两个客户端订阅了某个频道后,当其他客户端向该频道发送消息后,A、B两个客户端均会收到这条消息。测试结果如下所示

figure 1.jpeg

当其他客户端向某个频道发送消息时,不仅订阅了该频道的客户端会收到该消息。与此同时,该消息也会被发送给所有与该频道相匹配的模式订阅者。值得一提的是,这里的模式规则为Glob模式

figure 2.jpeg

实现原理

对于频道订阅而言。Redis服务端内部会通过一个dict字典来进行记录维护。具体地,字典的Key为频道名,字典的Value则是一个所有订阅该频道的客户端链表。这样向某频道发送消息时,即可通过字典获取相应的客户端链表。然后遍历该链表依次推送消息即可

对于模式订阅而言,Redis服务端内部则是通过一个链表进行维护的。其中链表节点中仅包含一个模式和客户端信息。注意在一个链表节点中仅仅包含一个模式。换言之,如果某客户端发送 psubscribe “new.“ “old.“ 命令。即该客户端同时订阅了”new.“、”old.“这两个模式。则Redis服务端会针对这两个模式分别创建两个链表节点,然后依次加入链表中。这样向某频道发送消息时,即可遍历该链表。判断该频道名称是否匹配当前链表节点中的模式,如果匹配就把消息推送给相应的客户端

PUBSUB命令

Redis从2.8版本开始引入了PUBSUB命令,用于查看订阅信息。具体地,其提供了三个子命令

1. PUBSUB CHANNELS [pattern]

该子命令用于查看Redis服务端当前被订阅的频道。其中patter模式参数是可选的。如果不给定patter模式参数,则会返回服务端当前被订阅的所有频道;反之,只会返回与模式相匹配的频道

figure 3.jpeg

2. PUBSUB NUMSUB <channel> [channel …]

该子命令用于查看Redis服务端订阅指定的若干个频道的订阅者数量

figure 4.jpeg

3. PUBSUB NUMPAT

该子命令用于查看Redis服务端被订阅模式的数量

figure 5.jpeg

事务

基本实践

Redis同样支持事务,具体地

  • MULTI :该命令表示一个事务的开始
  • EXEC :提交事务
  • Discard :取消事务
  • WATCH <key> [key …] :监视若干个指定Key
  • UNWATCH :取消对所有Key的监视

figure 6.jpeg

特别地,Redis中还提供了WATCH命令,其本质上是一个乐观锁。该命令用于在EXEC命令之前执行,用于对若干个指定的Key进行监视。这样EXEC执行过程中即会对所有被监视的Key进行检查。只要有一个被监视的Key被修改过了,则Redis服务端将会拒绝执行事务;反之,如果所有被监视的Key均未被修改过,则Redis将会执行该事务。示例如下所示

figure 7.jpeg

而UNWATCH命令则用于取消对所有Key的监视

figure 8.jpeg

这里对Redis的乐观锁实现进行一定的补充说明。在Redis各数据库内部均会维护以一个字典。其中,字典的Key为被监视的Key;字典的Value则是一个所有监视该Key的客户端链表。这样后续每次对Redis执行写命令后,可以快速通过该字典判断被修改的Key是否有客户端在监视。如果有,即可直接获取监视该Key的客户端链表。遍历该链表、打开客户端节点中的相应标志位。这样Redis服务端在收到EXEC命令准备执行事务之前,只需检查该客户端的相应标志位是否被打开即可。如果提交事务的客户端的相应标志位被打开,则服务端将会拒绝执行事务;反之则执行事务

ACID

1. Atomicity 原子性

关于事务的原子性的含义非常简单。即一个事务中的所有操作要么全部执行、要么一个也不执行。但对于Redis而言,这个问题我们要一分为二来看

  • 存在语法错误(例如命令不存在、命令参数非法等)的情况下,事务中的所有命令都不会执行
  • 存在运行错误(例如对String类型的Key执行RPUSH操作命令)的情况下,除执行中出现错误的命令外,其他命令都能正常执行

从上不难看出,Redis与传统的关系型数据库事务的最大区别在于:Redis事务不支持Rollback回滚机制。即事务在执行过程中如果某条命令执行失败,整个事务依然会继续执行下去。而不会回滚。而之所以Redis的事务不支持回滚,是因为Redis的作者认为运行错误应该在开发环境中就应该被测试出来,而不应该在生产环境中发生。故从这个角度来说,Redis是具有原子性的

2. Consistency 一致性

一致性则是指数据符合约束、要求,没有错误的、非法的数据。总的来说,Redis具有一致性。具体地,我们从几个方面进行分析

  • 存在语法错误的事务,Redis会拒绝执行该事务。故Redis一致性不会被该事务影响
  • 存在运行错误的事务,Redis会继续执行事务中不存在错误的命令,且执行结果不会被出错的命令影响。故Redis一致性不会被该事务影响
  • 事务执行过程中Redis服务发生宕机。如果没有相应的持久化文件(RDB/AOF),则服务重启后数据库为空。而空库总是一致性的;而如果存在相应的持久化文件(RDB/AOF),服务在重启后将会根据持久化文件恢复数据。故同样也是符合一致性的

3. Isolation 隔离性

由于Redis采用单线程的方式执行事务,且保证在执行事务过程中不会对其进打断。故Redis的事务总是以串行的方式执行。所以Redis天然具有隔离性

4. Durability 持久性

在持久性方面,需要分类进行讨论

  • 如果服务端未启用持久化,则显然此时事务不具备持久性
  • 如果服务端采用RDB进行持久化,服务端只会在特定条件下才会执行BGSAVE命令。并且BGSAVE命令是异步的,不能在第一时间保证事务执行结果被存储到硬盘中。故RDB持久化模式下不具备持久性
  • 如果服务端采用AOF进行持久化,当appendfsync配置为always时,则服务端每次执行完命令即会进行同步。故此时是具有持久性的。需要注意的是,no-appendfsync-on-rewrite配置项也必须设置no、而不能使用yes。否则此时事务将不具有持久性。其默认值为no
  • 如果服务端采用AOF进行持久化,当appendfsync配置为everysec、no时,由于不能保证事务执行结果被立即同步。故此时不具有持久性

排序

基本实践

Redis的SORT命令可以对列表、集合、有序集合等类型的数据进行排序。默认按升序排列

1
2
# 对指定Key中的元素进行升序排序
sort <key>

figure 9.jpeg

1. ALPHA 选项

如果元素值不是数字,而是字符串。则可以使用 ALPHA 选项,实现按字典序排序

1
2
# 对指定Key中的元素按字典序进行升序排序
sort <key> alpha

figure 10.jpeg

2. ASC、DESC 选项

SORT命令默认进行升序,当然也可以通过 ASC 选项显式指定;而降序则可以通过 DESC 选项实现

1
2
3
4
# 对指定Key中的元素进行升序排序
sort <key> asc
# 对指定Key中的元素进行降序排序
sort <key> desc

figure 11.jpeg

3. BY 选项

SORT命令默认使用被排序元素的值作为排序的权重。但其同时支持通过BY选择指定外部Key作为排序的权重

1
2
# 对指定Key中的元素, 按外部Key进行排序
sort <key> by <pattern>

外部Key可以为String类型的数据。其中在pattern模式中*表示占位符,用于指代被排序元素的值。例如下面我们使用mset命令设置多个String类型的数据。然后通过*Age模式来获取相应的外部Key

figure 12.jpeg

此外还可以使用Hash类型的数据作为外部Key。其中,可以使用 key->field 格式来获取Hash键中域的值,其中key表示键,而 field则表示域。具体地,我们可以通过不同的Hash键分别保存各外部Key的值

figure 13.jpeg

4. LIMIT 选项

SORT命令会返回排序的全部结果,故可以LIMIT选项实现返回部分排序结果。其中,offset参数表示返回结果时跳过前offset个;count参数表示返回结果时跳过指定数量后,要返回排序结果的最大数量

1
2
# 对指定Key中的元素进行升序排序,返回结果时跳过前offset个元素,并且返回结果的数量最大为count
sort <key> limit <offset> <count>

figure 14.jpeg

5. GET 选项

SORT命令在排序后,默认会返回被排序元素自身。但通过GET选项可以实现在排序后,将 被排序元素、GET选项指定的模式 作为新Key。并将新Key的值作为结果返回。其中pattern模式中*表示占位符,用于指代被排序元素的值。此外支持使用多个GET选项

1
2
# 对指定Key中的元素进行升序排序,并根据GET选项的模式返回相应键的值
sort <key> get <pattern> [get <pattern>]

figure 15.jpeg

6. STORE 选项

SORT命令只会向客户端返回排序结果,而不会进行保存。故可通过STORE选项保存排序结果到指定键,以便后续再次利用该排序结果

1
2
# 对指定Key中的元素进行升序排序,并将排序结果保存到指定的key2当中
sort <key> store <key2>

figure 16.jpeg

综上所述,对于SORT命令完整格式如下所示

1
SORT <key> [ALPHA] [ASC|DESC] [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [STORE key2]

故在实际实践中,我们可以按需自由组合、选用上面的其各种选项。并且除了GET选项外,改变其他各选项在SORT命令中的摆放位置。并不会影响SORT命令执行这些选项的先后顺序。因为SORT命令中各选项在Redis内部是有固定的执行顺序、流程规则。故Sort命令的大体流程顺序如下所示:

  1. 排序:其会利用ALPHA、ASC、DESC、BY选项,对指定Key进行排序以得到一个排序结果集
  2. 截取:其会利用LIMIT选项,对上一步获得的排序结果集进行截取,并作为新的排序结果集
  3. 获取外部键:其会利用GET选项,根据上一步获得的排序结果集的元素、GET选项的模式获取相应相应外部键的值。并将外部键的值作为新的排序结果集
  4. 保存:其会利用STORE选项,将上一步获得对排序结果集保存到指定Key当中

位图

Redis 2.2.0版本开始引入了位图这一数据类型,其使用位数组进行存储。这里补充说明下Redis内部对于位图采用SDS简单动态字符串进行实现。其支持以下命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 针对指定位图,将指定偏移量的二进制位设置为指定值。其中,offset从0开始计数;value为0或1
setbit <key> <offset> <value>

# 针对指定位图,获取指定偏移量的二进制位的值
getbit <key> <offset>

# 统计指定位图中值为1的二进制位数量
bitcount <key>

# 对任意数量的位图进行按位与、按位或、按位异或操作,并将结果保存至resKey当中
bitop [and|or|xor] <resKey> <Key1> <Key2> [Key3]

# 对名为Key1的位图进行取反操作,并将结果保存至resKey当中
bitop not <resKey> <Key1>

这里对于bitcount命令进行一些补充说明,其目的在于统计一个位图中非零二进制的数量。换言之,即计算它的汉明重量。朴素的思想是直接暴力遍历位图进行统计,但对于特别大的位图来说,效率太低。而另外一种改进方法是查表法,比如表中记录8个二进制位的各种排列情况所对应的非零二进制的数量。即典型的以空间换时间,但问题在于这个表该多大?比如表中记录8个二进制位的各种排列情况所对应的非零二进制的数量,还是表中记录16个二进制位的各种排列情况所对应的非零二进制的数量,甚至更多呢?因为仅仅记录8个二进制位,效率相比暴力法也仅仅提升了8倍。对于大位图提升不明显。但表一旦过大,又会造成严重的内存消耗

幸运地是统计一个位图中非零二进制的数量。在数学上该问题可以被称之为计算汉明重量。由于汉明重量被广泛应用于信息论、密码学当中。故学届针对该问题提出了很多算法,目前已知效率最好的通用算法是variable-precision SWAR算法。该算法可以以常数时间复杂度计算多个字节的汉明重量。同时该算法的空间复杂度也是常数

故在Redis当中,bitcount命令的实现方式是将variable-precision SWAR算法、查表法有机结合起来

  1. 对于前者而言,每128个二进制位时,即会调用四次32位版本的variable-precision SWAR算法来计算这128个二进制位的汉明重量
  2. 对于后者而言,通过表存储、记录8个二进制位的各种排列情况所对应的非零二进制的数量

这样在bitcount命令的实现中:当位图中剩余未计算的二进制位数量大于等于128时,使用variable-precision SWAR算法。这样每次即可计算128个二进制位的汉明重量;当位图中剩余未计算的二进制位数量小于128时,再去使用查表法

慢查询日志

Redis的慢查询日志可以记录执行时间超过指定阈值的命令请求。用户可以据此进行监控、分析。故Redis的配置文件提供了如下相关的配置项

  • slowlog-log-slower-than :命令执行时间的阈值。超过该值的即会被视为慢查询,并被记录到日志当中。单位:微秒
  • slowlog-max-len :慢查询日志记录日志的数量上限。因为Redis使用FIFO的方式保存慢查询记录。故当数量超过上限后,即会移除最久的一条慢查询日志记录

当然还可以通过config set/get命令,实现对上述两个配置项的实时修改/查看

1
2
3
4
5
6
7
8
# 设置慢查询的时间阈值:10000微秒=10毫秒
config set slowlog-log-slower-than 10000
# 设置慢查询日志记录日志的数量上限
config set slowlog-max-len 10

# 查看配置项
config get slowlog-log-slower-than
config get slowlog-max-len

为此Redis通过SLOWLOG命令提供了三个子命令

1. SLOWLOG GET [count]

查看当前最新的慢日志记录,其中可选参数count用于指定返回的最大记录数量。如下图所示,其返回了3条慢查询记录。并对其中一条日志为例,对内容含义进行介绍

figure 17.jpeg

2. SLOWLOG LEN

获取当前慢日志记录的总数

3. SLOWLOG RESET

清空所有慢日志记录

监视器

客户端可以通过执行MONITOR命令,将其自身变为一个监视器。实时接收、打印Redis服务端当前处理来自其他客户端的命令请求。具体地,Redis内部通过一个链表来维护所有的监视器。这样当服务端收到其他客户端的命令请求后,即可遍历该链表将命令请求转发给正在监视该服务端的所有监视器

figure 18.jpeg

参考文献

  1. Redis设计与实现 黄健宏著
0%