Redis发布订阅和事务实现原理
- 发布订阅
- 实现
- 频道订阅与退订
- 频道模式订阅与退订
- 发送消息
- 事务
- 事务队列
- 执行事务
- WATCH命令实现
- ACID
- 原子性
- 一致性
- 隔离性
- 持久性
发布订阅
Redis的发布订阅由PUBLISH,SUBSCRIBE,PSUBSCRIBE等命令组成,例子如下:
redis中我们还可以通过PSUBSCRIBE "user.*"命令完成频道的模式订阅,也就是模糊匹配,而SUBSCRIBE命令是明确订阅某个频道,也就是精确匹配。
当我们通过publish向某个频道发送命令时,该消息不仅会发送给订阅该频道的所有用户,同时也会发送给与该频道相匹配的模式的订阅者。
实现
频道订阅与退订
redis服务器全局状态由redisServer结构体对象保存,该对象内部保存了所有频道的订阅关系:
struct redisServer{
//...
//保存所有频道的订阅关系
dict *pubsub_channels;
//...
}
pubsub_channels属性的数据类型是字典类型,该字典中的key保存了频道名,value链表将订阅该频道的所有客户端串联起来:
-
当我们通过subscribe命令订阅某个频道的时候,所做的工作如下:
-
当我们通过unsubscribe命令退订某个频道时,所做的工作如下:
频道模式订阅与退订
struct redisServer{
//...
//保存所有频道的订阅关系
dict *pubsub_channels;
//保存所有模式订阅关系
list *pubsub_patterns;
//...
}
typedef struct pubsubPattern{
//订阅模式的客户端
redisClient *client;
//被订阅的模式
robj *pattern
}pubsubPattern
pubsub_patterns链表中保存的元素类型是pubsubPattern,该结构体记录了每个客户端所订阅的频道模式。
- 订阅模式
- 退订模式
发送消息
当一个redis客户端执行PUBLISH channel message命令时,服务器需要执行以下两步:
- 将消息发送给channel频道的所有订阅者
- 如果有一个或多个模式pattern与channel匹配,那么将消息发送给pattern模式的订阅者
事务
Redis通过MULTI,EXEC,WATCH等命令来实现事务功能,事务提供了将多个命令请求打包,然后一次性,按顺序执行的机制,并且在事务执行期间,服务器不会中断事务去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才会去处理其他客户端的请求。
事务执行分为三个阶段:
-
事务开始: 通过multi命令表示开启事务,标记当前客户端进入事务状态
-
命令入队
-
事务执行
事务队列
每个Redis客户度都通过multiState属性来记录当前事务状态:
struct redisClient {
//事务状态
multistate mstate;
...
}
事务状态包含一个事务队列和已经入队的命令计数器:
struct multistate {
//事务队列--FIFO顺序
multicmd *cmds;
//已经入队的命令计数器
int count;
}
事务队列中每个multicmd都保存了当前已入队命令的信息:
struct multicmd {
//参数
robj **argv;
//参数个数
int argc;
//命令指针
struct redisCommand *cmd;
}
事务队列以先入先出顺序保存命令,例如:
事务队列中保存命令顺序:
执行事务
当一个处于事务状态的客户端向服务器发送EXEC命令时,该命令将会立刻执行,服务器会遍历当前客户端的事务队列,执行队列中保存的所有命令,最后将命令执行的结果全部返回给客户端:
WATCH命令实现
WATCH命令是一个乐观锁,它可以在EXEC命令执行前,监视任意数量的key,并在EXEC命令执行时,检查被监视的key是否至少有一个已经被修改了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
每个Redis数据库都保存着一个watched_keys字典,该字典的key是某个被watch命令监视的数据库键,而值是一个链表,链表中记录了所有监视当前key的客户端。
struct redisDb{
//正在被watch命令监视的key
dict *watched_keys;
}
所有对数据库进行修改的命令,如: SET,LPUSH,SADD,ZREM,DEL等,在执行后都会调用touchWatchKey函数对watched_keys字典进行检查,如果字典中存在该key,那么会将监视该key对应的客户端的REDIS_DIRTY_CAS标记打开,表示当前客户端的安全性已经被破坏了。
当exec事务执行命令被调用时,服务器会检查当前客户端对应的REDIS_DIRTY_CAS标识是否已经被打开了,如果被打开了,就拒绝执行事务:
ACID
原子性
redis事务队列中的命令要么全部执行,要么全部不执行。
如果命令在入队过程中,出现了命令语法格式错误导致命令入队失败,那么当前事务中所有命令都不会被执行。
如果事务队列中命令执行时,发生错误,那么redis不提供回滚机制,并且命令将会继续执行下去,直到执行完毕:
一致性
- 出现入队错误会导致当前事务被拒绝执行
- 事务执行时出现错误,不会中断事务执行
- redis服务器执行事务过程中停机不会导致数据不一致,服务器重启时可以通过rdb或者aof文件恢复数据
空白数据库总是可以看做是一致的
隔离性
数据库的隔离性指的是多个并发执行事务互不干扰,并且并行事务执行结果要与串行执行一致。
Redis使用单线程执行事务,并且执行事务期间不会对事务进行中断,因此,redis的事务总是以串行化方式运行。
持久性
因为Redis的事务不过是简单地用队列包裹起了一组Redis命令,Redis并没有为事务提供任何额外的持久化功能,所以Redis事务的耐久性由Redis所使用的持久化模式决定:
□ 当服务器在无持久化的内存模式下运作时,事务不具有耐久性:一旦服务器停机,包括事务数据在内的所有服务器数据都将丢失。
□ 当服务器在RDB持久化模式下运作时,服务器只会在特定的保存条件被满足时,才会执行BGSAVE 命令,对数据库进行保存操作,并且异步执行的BGSAVE 不能保证事务数据被第一时间保存到硬盘里面,因此RDB持久化模式下的事务也不具有耐久性。
□ 当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always 时,程序总会在执行命令之后调用同步(sync)函数,将命令数据真正地保存到硬盘里面,因此这种配置下的事务是具有耐久性的。
□ 当服务器运行在AOF持久化模式下,并且appendfsync选项的值为everysec 时,程序会每秒同步一次命令数据到硬盘。因为停机可能会恰好发生在等待同步的那一秒钟之内,这可能会造成事务数据丢失,所以这种配置下的事务不具有耐久性。
□ 当服务器运行在AOF持久化模式下,并且appendfsync 选项的值为no时,程序会交由操作系统来决定何时将命令数据同步到硬盘。因为事务数据可能在等待同步的过程中丢失,所以这种配置下的事务不具有耐久性。