本文内容为《redis设计与实现》一书学习笔记。本文主要概述十八和十九章内容。
第十八章
通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道,假设A、B、C三个客户端都执行命令
SUBSCRIBE "news.it"
那么这三个客户端就是"news.it"频道的订阅者。如果另一个客户端执行发布命令:
PUBLISH "news.it" "hello"
向"news.it"频道发送消息"hello",那么A、B、C三个客户端都将收到这条消息。
还可以通过执行PSUBSCRIBE命令订阅一个或多个模式(一个模式可以匹配多个频道)。
还是向"news.it"频道发送消息"hello",那么A、C、D三个客户端都将收到这条消息。
18.1 频道的订阅与退订
Redis将所有频道的订阅关系都保存在服务器状态的pubsub_channels字典里面,这个字典的键是某个被订阅的频道,而键的值则是一个链表,链表里面记录了所有订阅这个频道的客户端。
SUBSCRIBE和UNSUBSCRIBE实际就是对这个链表的操作。
18.2 模式的订阅与退订
服务器将所有模式的订阅关系都保存在服务器状态的pubsub_patterns属性里面。pubsub_patterns属性是一个链表,链表中的每个节点都包含着一个pubsub Pattern结构,这个结构的pattern属性记录了被订阅的模式,而client属性则记录了订阅模式的客户端。
18.3 发送消息
当一个Redis客户端执行PUBLISH<channel><message> 命令将消息message发送给频道channel的时候,服务器需要执行以下两个动作:
- 将消息message发送给channel频道的所有订阅者。(以PUBLISH "news.it" "hello"为例,先在字典中查找"news.it"对应的链表值,遍历链表将"hello"发送给"news.it"的订阅者。)
- 如果有一个或多个模式pattern与频道channel相匹配,那么将消息message发送给pattern模式的订阅者。(同样是遍历链表)
18.4 查看订阅消息
PUBSUB命令是Redis 2.8新增加的命令之一,客户端可以通过这个命令来查看频道或者模式的相关信息,比如某个频道目前有多少订阅者,又或者某个模式目前有多少订阅者。
18.4.1 PUBSUB CHANNELS
PUBSUB CHANNELS[pattern]子命令用于返回服务器当前被订阅的列表,其中pattern是可选参数:
- 不给定pattern参数,命令返回服务器当前被订阅的所有频道。
- 给定pattern参数,命令返回服务器当前被订阅的频道中那些与pattern模式相匹配的频道。
18.4.2 PUBSUB NUMSUB
PUBSUB NUMSUB[channel-1 channel-2 channel-n]子命令接受任意多个频道作为输入参数,并返回这些频道的订阅量数量。
该子命令通过在pubsub_channels字典中找到频道对应的订阅者链表,然后返回订阅者链表的长度来实现的(订阅者链表的长度就是频道订阅者的数量)。
18.4.3 PUBSUB NUMPAT
PUBSUB NUMPAT子命令用于返回服务器当前被订阅模式的数量。
该子命令通过返回pubsub_patterns链表的长度来实现,该链表的长度就是服务器被订阅模式的数量。
第十九章 事务
Redis通过MULTI、EXEC、WATCH等命令实现事务功能,事务提供了一种将多个命令请求打包,然后一次性的,按顺序的执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而去执行其他客户端的请求,它会将事务中的命令都执行完成,然后才去执行其他客户端的命令。
下面是一个事务执行的过程,以一个MULTI命令作为开始,接着将多个命令放入事务当中,最后由EXEC命令将这个事务提交(commit)给服务器执行:
19.1 事务的实现
一个事务从开始到结束通常会经历以下三个阶段:
- 事务开始。
MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态,这一切换是通过在客户端状态的flags属性中打开REDIS_MULTI标识来完成的。 - 命令入队。
如果一个客户端处于非事务状态时,那么这个客户端发送的命令就会立即被服务器执行。当客户端切换到事务状态后,服务器会根据客户端发来的不同命令执行不同的操作:
- 如果客户端发送的命令为EXEC、DISCARD、WATCH、MULTI四个命令的其中一个,那么服务器立即执行这个命令。
- 否则将这个命令放入一个事务队列里面,然后向客户端返回QUEUED回复。
- 事务执行。 当一个处于事务状态的客户端向服务器发送EXEC命令时,这个EXEC命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行命令所得的结果全部返回给客户端。
整个过程的流程图如下:
19.2 WATCH命令的实现
WATCH命令是一个乐观锁(optimistic locking),它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
下表是个事务执行失败的例子:
在时间T4,客户端B修改了"name"键的值,当客户端A在T5执行EXEC命令时,服务器会发现WATCH监视的键“name”已经被修改,因此服务器拒绝执行客户端A的事务,并向客户端A返回空回复。
19.2.1 使用WATCH命令监视数据库键
每个Redis数据库都保存着一个watched_keys字典,这个字典的键是某个被WATCH命令监视数据库键,而字典的值则是一个链表,链表中记录了所有监视相应数据库键的客户端。
举个例子,当前客户端为c10086,客户端执行以下WATCH命令之后:
redis> WATCH "name" "age"
OK
watched_keys字典更新如下图所示:
19.2.2 监视机制的触发
所有对数据库进行修改的命令,比如SET、LPUSH、SADD、ZREM、DEL、FLUSHDB等等,在执行之后都会调用multi.c/touchWatchKey函数对watched_keys字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有的话,那么touchWatchKey函数会将监视被修改键的客户端的REDIS_DIRTY_CAS标识打开,表示该客户端的事务安全性已经被破坏。
touchWatchKey函数的定义可以用以下伪代码描述:
19.2.3 判断事务是否安全
当服务器接收到一个客户端发来的EXEC命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务:
- 如果标识已经被打开,说明客户端所监视的键中,至少有一个键已经被修改过,此时客户端提交的事务已不再安全,所以服务器会拒绝执行客户端提交的事务。
- 如果标识没有被打开,说明事务是安全的,服务器会执行客户端提交的事务。
19.3 事务的ACID性质
19.3.1 原子性
原子性指的是数据库事务将多个操作当做一个整体执行,要么都执行,要么一个也不执行。
对于Redis的事务功能来说,事务队列中的命令要么就全部都执行,要么就一个都不执行。
Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制,即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,并且之前执行的命令也不会有任何影响,直到将事务队列中的所有命令都执行完毕为止。
不支持回滚是因为这种复杂的功能和Redis追求简单高效的设计主旨不符。Redis事务的执行时错误通常都是编程错误产生的,这种错误通常只会出现在开发环境中,很少会在实际的生产环境中出现,所以没有必要支持回滚。
19.3.2 一致性
一致性是指,如果数据库在执行事务之前是一致的,那么事务执行之后也应该是一致的,无论事务是否执行成功。“一致”是指数据符合数据库本身的定义,没有包含非法或者无效的错误数据。
Redis通过谨慎的错误检测和简单的设计来保证事务的一致性,以下是三个Redis事务可能出错的地方以及Redis的处理方法:
-
入队错误。如果命令在入队的时候出现了命令不存在或命令的格式不正确等错误,那么Redis将拒绝执行这个事务。下图这个例子中,客户端尝试向事务入队一个不存在的命令YAHOOOO,客户端提交的事务会被服务器拒绝执行。
因为服务器会拒绝执行入队过程中出现的事务,所以Redis事务的一致性不会被带有入队错误的事务影响。
-
执行错误。执行错误一般是对数据库键执行了错误的类型操作导致,下面这个例子中,先用SET命令将键"msg"设置成一个字符串键,然后在事务里尝试对"msg"键执行只能用于列表键的RPUSH命令,这将引发一个错误,且这种错误只能在事务执行(也即是命令执行)的期间被发现。
这种错误会被服务器识别并进行错误处理,所以这些命令不会对数据库进行任何修改,也不会对事务的一致性产生任何影响。
- 服务器停机。
- 服务器停机之后,如果没有开启持久化,那么Redis重启之后数据库是空白的,是一致的状态。
- 如果开启了Redis持久化(RDB模式或AOF模式),那么服务器重启之后会利用RDB文件或者AOF文件恢复数据,从而将数据库状态还原到一个一致的状态。如果找不到可供使用的RDB文件或者AOF文件,那么重启之后的数据库将是空白的,而空白数据库总是一致的。
综上,无论Redis服务器运行在哪种持久化模式下,事务执行中途发生的停机都不会影响数据库的一致性。
19.3.3 隔离性
事务的隔离性是指,即使数据库中多个事务并发的执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果一致。
对于Redis来说,它使用单线程的方式执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间,不会对事务进行中断,因此,Redis事务总是以串行的方式执行的,并且事务也总是有隔离性的。
19.3.4 耐久性
事务的耐久性是指,当一个事物执行完毕时,执行这个事务所得的结果已被保存到永久存储介质(比如硬盘)里面了,即使服务器停机,执行事务的结果也不会丢失。
因为Redis的事务不过是简单地用队列包裹起了一组Redis命令,Redis并没有为事务提供任何额外的持久化功能,所以Redis事务的耐久性由Redis所使用的持久化模式决定。
不论Redis在什么模式下运作,在一个事务的最后加上SAVE命令总可以保证事务的耐久性,不过因为这种做法的效率太低,所以并不具有实用性。
网友评论