第四部分 独立功能的实现
第18章 发布与订阅
Redis的发布与订阅功能由PUBLISH,SUBSCRIBE、PSUBSCRIBE等命令组成。
通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道,从而成为这些频道的订阅者(subscriber):每当有其他客户端向被订阅的频道发送消息(message)时,频道的所有订阅者都会收到这条消息。
举个例子,假设A.B,C三个客户端都技SUBSCRIBE"news.it"
那么这三个客户端就是"news.it"频道的订阅者,如图18-1所示。
如果这时某个客户端执行命令PUBLISH “news.it” “hello”
向"news.it"频道发送消息"hello",那么"news.it"=ii&&&i这条消息。
除了订阅频道之外,客户端还可以通过执行PSUBSCRIBE命令订阅一个或多个模式,从而成为这些模式的订阅者:每当有其他客户端向某个频道发送消息时,消息不仅会被发送给这个频道的所有订阅者,它还会被发送给所有与这个频道相匹配的模式的订阅者。
举个例子:
- 客户端A正在订阅频道"news.it"。口客户端B正在订阅频道"news.et"
- 客户端C和客户端D正在订阅与"news.it"频道和"news.et"频道相匹配的模式"news.[ie]t"
如果这时某个客户端执行命令
PUBIISH “news.it” “hello”
向"news.it"频道发送消息"hello",那么不仅正在订阅"news.it"频道的客户端A会收到消息,客户端C和客户端D也同样会收到消息,因为这两个客户端正在订阅匹配"news.it"频道的"news.[ie]t"模式。
18.1 频道的订阅与退订
当一个客户端执行 SUBSCRIBE 命令, 订阅某个或某些频道的时候, 这个客户端与被订阅频道之间就建立起了一种订阅关系。
Redis 将所有频道的订阅关系都保存在服务器状态的 pubsub_channels 字典里面, 这个字典的键是某个被订阅的频道, 而键的值则是一个链表, 链表里面记录了所有订阅这个频道的客户端。
比如说, 图 IMAGE_PUBSUB_CHANNELS 就展示了一个 pubsub_channels 字典示例, 这个字典记录了以下信息:
- client-1 、 client-2 、 client-3 三个客户端正在订阅 “news.it” 频道。
- 客户端 client-4 正在订阅 “news.sport” 频道。
- client-5 和 client-6 两个客户端正在订阅 “news.business” 频道。
18.1.1 订阅频道
每当客户端执行 SUBSCRIBE 命令, 订阅某个或某些频道的时候, 服务器都会将客户端与被订阅的频道在 pubsub_channels 字典中进行关联。
根据频道是否已经有其他订阅者, 关联操作分为两种情况执行:
- 如果频道已经有其他订阅者, 那么它在 pubsub_channels 字典中必然有相应的订阅者链表,程序唯一要做的就是将客户端添加到订阅者链表的末尾。
- 如果频道还未有任何订阅者, 那么它必然不存在于 pubsub_channels 字典, 程序首先要在 pubsub_channels 字典中为频道创建一个键, 并将这个键的值设置为空链表, 然后再将客户端添加到链表, 成为链表的第一个元素。
18.1.2 退订频道
UNSUBSCRIBE 命令的行为和 SUBSCRIBE 命令的行为正好相反 —— 当一个客户端退订某个或某些频道的时候, 服务器将从 pubsub_channels 中解除客户端与被退订频道之间的关联:
- 程序会根据被退订频道的名字, 在 pubsub_channels 字典中找到频道对应的订阅者链表,然后从订阅者链表中删除退订客户端的信息。
- 如果删除退订客户端之后, 频道的订阅者链表变成了空链表, 那么说明这个频道已经没有任何订阅者了, 程序将从 pubsub_channels 字典中删除频道对应的键。
18.2 模式的订阅与退订
前面说过,服务器将所有频道的订阅关系都保存在服务器状态的pubsub_channels属性里面,与此类似,服务器也将所有模式的订阅关系都保存在服务器状态的pubsub_patterns属性里面。
pubsub_patterns属性是一个链表,链表中的每个节点都包含着一个pubsub Pattern结构,这个结构的pattern属性记录了被订阅的模式,而client属性则记录了订阅模式的客户端。
18.2.1 订阅模式
每当客户端执行PSUBSCRIBE命令订阅某个或某些模式的时候,服务器会对每个被订阅的模式执行以下两个操作:
- 新建一个pubsubPattern结构,将结构的pattern属性设置为被订阅的模式,client属性设置为订阅模式的客户端。
- 将pubsubPattern结构添加到pubsubpatterns链表的表尾。
18.2.2 退订模式
模式的退订命令PUNSUBSCRIBE是PSUBSCRIBE命令的反操作:当一个客户端退订某个或某些模式的时候,服务器将在pubsub_patterns链表中查找并删除那些pattern属性为被退订模式,并且client属性为执行退订命令的客户端的pubsubPattern结构。
18.3 发送消息
当一个Redis客户端执行PUBLISH <channel> <message> 命令将消息message发送给频道channel的时候,服务器需要执行以下两个动作:
- 将消息message发送给channe1频道的所有订阅者。
- 如果有一个或多个模式pattern与频道channe1相匹配,那么将消息message发送给pattern模式的订阅者。
18.3.1 将消息发送给频道订阅者
因为服务器状态中的pubsub_channels字典记录了所有频道的订阅关系,所以为了将消息发送给channe1频道的所有订阅者,PUBLISH命令要做的就是在pubsub_channels字典里找到频道channe1的订阅者名单(一个链表),然后将消息发送给名单上的所有客户端。
18.3.2 将消息发送给模式订阅者
因为服务器状态中的pubsub_patterns链表记录了所有模式的订阅关系,所以为了将消息发送给所有与channe1频道相匹配的模式的订阅者,PUBLISH命令要做的就是遍历整个pubsub_patterns链表,查找那些与channe1频道相匹配的模式,并将消息发送给订阅了这些模式的客户端。
18.4 查看订阅信息
PUBSUB命令是Redis 2.8新增加的命令之一,客户端可以通过这个命令来查看频道或者模式的相关信息,比如某个频道目前有多少订阅者,又或者某个模式目前有多少订阅者,诸如此类。
18.4.1 PUBSUB CHANNELS
PUBSUB CHANNELS(pattern)子命令用于返回服务器当前被订阅的频道,其中pattern参数是可选的:
- 如果不给定pattern参数,那么命令返回服务器当前被订阅的所有频道。
- 如果给定pattern参数,那么命令返回服务器当前被订阅的频道中那些与pattern模式相匹配的频道。
这个子命令是通过遍历服务器pubsub channels字典的所有键(每个键都是一个被订阅的频道),然后记录并返回所有符合条件的频道来实现的。
举个例子,对于图18-18所示的pubsub-channels字典来说,执行PUBSUB CHANNELS命令将返回服务器目前被ta的四个频道:
redis> PUBSUB CHANNELS
1)"news.it"
2)"news.sport"
3)"news.business"
4)"news.movie"
另一方面,执行PUBSUB CHANNELS "news.[is]*"命令将返回"news.it"和"news.sport"两个频道,因为只有这两个频道和"news.[is]*"模式相匹配:
redis> PUBSUB CHANNELS"news.[is]*"
1)"news.it
2)"news.sport"
18.4.2 PUBSUB NUMSUB
PUBSUB NUMSUB [channel-1 channel-2…channel-n]子命令接受任意多个频道作为输人参数,并返回这些频道的订阅者数量。
这个子命令是通过在pubsub_channels字典中找到频道对应的订阅者链表,然后返回订阅者链表的长度来实现的(订阅者链表的长度就是频道订阅者的数量)。
18.4.3 PUBSUB NUMPAT
PUBSUB NUMPAT子命令用于返回服务器当前被订阅模式的数量。
这个子命令是通过返回pubsub-patterns链表的长度来实现的,因为这个链表的长度就是服务器被订阅模式的数量。
18.5 重点回顾
- 服务器状态在 pubsub_channels 字典保存了所有频道的订阅关系: SUBSCRIBE命令负责将客户端和被订阅的频道关联到这个字典里面, 而 UNSUBSCRIBE 命令则负责解除客户端和被退订频道之间的关联。
- 服务器状态在 pubsub_patterns 链表保存了所有模式的订阅关系: PSUBSCRIBE 命令负责将客户端和被订阅的模式记录到这个链表中, 而 UNSUBSCRIBE 命令则负责移除客户端和被退订模式在链表中的记录。
- PUBLISH 命令通过访问 pubsub_channels 字典来向频道的所有订阅者发送消息, 通过访问 pubsub_patterns 链表来向所有匹配频道的模式的订阅者发送消息。
- PUBSUB 命令的三个子命令都是通过读取 pubsub_channels 字典和 pubsub_patterns 链表中的信息来实现的。
第19章 事务
Redis 通过MULTl、EXEC、WATCH等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
19.1 事务的实现
一个事务从开始到结束通常会经历以下三个阶段:
- 事务开始。
- 命令入队。
- 事务执行。
19.1.1 事务开始
MULTI 命令的执行标志着事务的开始:
redis> MULTI
OK
MULTI 命令可以将执行该命令的客户端从非事务状态切换至事务状态, 这一切换是通过在客户端状态的 flags 属性中打开 REDIS_MULTI 标识来完成的。
19.1.2 命令入队
当一个客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:
- 如果客户端发送的命令为 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令的其中一个, 那么服务器立即执行这个命令。
- 与此相反, 如果客户端发送的命令是 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令以外的其他命令, 那么服务器并不立即执行这个命令, 而是将这个命令放入一个事务队列里面, 然后向客户端返回 QUEUED 回复。
19.1.4 执行事务
当一个处于事务状态的客户端向服务器发送 EXEC 命令时, 这个 EXEC 命令将立即被服务器执行: 服务器会遍历这个客户端的事务队列, 执行队列中保存的所有命令, 最后将执行命令所得的结果全部返回给客户端。
正常执行事务
127.0.0.1:6379> multi #开启事务
OK
#命令入队
127.0.0.1:6379> set key1 v1
QUEUED
127.0.0.1:6379> get key1
QUEUED
127.0.0.1:6379> set key v2
QUEUED
127.0.0.1:6379> get key
QUEUED
127.0.0.1:6379> exec #执行事务
1) OK
2) "v1"
3) OK
4) "v2"
放弃事务
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> discard #取消事务
OK
127.0.0.1:6379> get k4 #事务队列中的命令都不会被执行!
(nil)
19.2 WATCH命令的实现
WATCH命令是一个乐观锁(optimistic locking),它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
以下是一个事务执行失败的例子:
redis>WATCH "name"
OK
redis> MULTI
OK
redis> SET "name" "peter"
QUEUED
redis> EXEC
(nil)
表19-1展示了上面的例子是如何失败的。
在时间T4,客户端B修改了“name“键的值,当客户端A在T5执行EXEC命令时,服务器会发现WATCH监视的键“name“已经被修改,因此服务器拒绝执行客户端A的事务,并向客户端A返回空回复。
19.2.2 监控机制的触发
所有对数据库进行修改的命令,比如SET、LPUSH、SADD、ZREM、DEL、FLUSHDB等等,在执行之后都会调用multi.c/touchwatchKey函数对watched_keys字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有的话,那么touchwatchKey函数会将监视被修改键的客户端的REDIS_DIRTY_CAS标识打开,表示该客户端的事务安全性已经被破坏。
19.2.3 判断事务是否安全
当服务器接收到一个客户端发来的EXEC命令时,服务器会根据这个客户端是否打开了REDIS DIRTY CAS标识来决定是否执行事务:
- 如果客户端的REDIS_DIRTY_CAS标识已经被打开,那么说明客户端所监视的键当中,至少有一个键已经被修改过了,在这种情况下,客户端提交的事务已经不再安全,所以服务器会拒绝执行客户端提交的事务。
- 如果客户端的REDIS_DIRTY_CAS标识没有被打开,服务器将执行客户端提交的这个事务。
19.3 事务的ACID特性
在Redis中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当Redis运行在某种特定的持久化模式下时,事务也具有耐久性(Durability)。
19.3.1 原子性
事务具有原子性指的是,数据库将事务中的多个操作当作一个整体来执行,服务器要么就执行事务中的所有操作,要么就一个操作也不执行。
Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。
可用discard放弃事务。
19.3.2 一致性
事务具有一致性指的是,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然是一致的。
19.3.3 隔离性
事务的隔离性指的是,即使数据库中有多个事务并发地执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。
因为Redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis的事务总是以串行的方式运行的,并且事务也总是具有隔离性的。
19.3.4 耐久性(持续性)
事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质(比如硬盘)里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失。
19.4 重点回顾
- 事务提供了一种将多个命令打包, 然后一次性、有序地执行的机制。
- 多个命令会被入队到事务队列中, 然后按先进先出(FIFO)的顺序执行。
- 事务在执行过程中不会被中断, 当事务队列中的所有命令都被执行完毕之后, 事务才会结束。
- 带有 WATCH 命令的事务会将客户端和被监视的键在数据库的 watched_keys 字典中进行关联, 当键被修改时, 程序会将所有监视被修改键的客户端的 REDIS_DIRTY_CAS 标志打开。
- 只有在客户端的 REDIS_DIRTY_CAS 标志未被打开时, 服务器才会执行客户端提交的事务, 否则的话, 服务器将拒绝执行客户端提交的事务。
- Redis 的事务总是保证 ACID 中的原子性、一致性和隔离性, 当服务器运行在 AOF 持久化模式下, 并且 appendfsync 选项的值为 always 时, 事务也具有耐久性。