Redis事务的实现原理
Redis通过MULTI、EXEC、WATCH、DISCARD等命令来实现事务功能。主要有以下三个阶段:
事务开始
MULTI命令的执行,标识着一个事务的开始。MULTI命令会将客户端状态的flags
属性中打开REDIS_MULTI
标识来完成的。
命令入队
当一个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客户端发送的命令为MULTI、EXEC、WATCH、DISCARD中的一个,立即执行这个命令,否则将命令放入一个事务队列里面,然后向客户端返回QUEUED
回复
- Redis客户端拥有自己的事务状态
typedef struct client {
// ......
// 事务状态(MULTI或者EXEC)
multiState mstate;
// ......
} client;
- 事务状态包含一个事务队列
typedef struct multiState {
// 事务队列,FIFO
multiCmd *commands;
// 已入队命令计数
int count;
// ......
} multiState;
- 事务队列中multiCmd类型保存了一个已入队命令的相关信息
typedef struct multiCmd {
// 参数
robj **argv;
// 参数数量
int argc;
// 命令指针
struct redisCommand *cmd;
} multiCmd;
事务队列是按照FIFO的方式保存入队的命令
事务执行
当一个处于事务状态的客户端向服务器发送EXEC命令时,服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行完的结果全部返回给客户端(每个命令对应一个返回)
WATCH命令的实现
WATCH命令是一个乐观锁,它可以在EXEC命令执行之前,监视任意数量的数据库键,并在执行EXEC命令时,检查被监视的键是否至少有一个已经被修改,如果有,服务器拒绝执行事务,向客户端返回代表事务执行失败的空回复
实现原理
- 使用WATCH命令监视数据库键:通过
watched_keys
字典,服务器可以清除地知道哪些数据库键正在被监视,以及哪些客户端正在监视这些数据库键
typedef struct redisDb {
// ......
// 正在被WATCH命令监视的键
dict *watched_keys;
// ......
} redisDb;
- 监视机制的触发:所有对数据库进行修改的命令,在执行之后都会调用
touchWatchKey
函数对watched_keys
字典进行检查,如果有客户端监视的key被修改过,那么touchWatchKey
函数会将监视被修改的客户端的REDIS_DIRTY_CAS
标识打开,表示事务已经被破坏 - 判断事务是否安全:如果客户端的
REDIS_DIRTY_CAS
标识被打开了,说明客户端提交的事务已经不再安全,所以服务器会拒绝执行客户端提交的事务
事务的ACID性质
-
原子性:对于Redis的事务功能来说,事务队列中的命令要么就全部执行,要么就一个都不执行,但是Redis的事务是不支持回滚操作的
-
一致性:Redis通过谨慎的错误检测和简单的设计保证事务的一致性。Redis事务可能出错的地方以及解决方案:
- 入队错误:如果一个事务在入队命令的过程中发现命令不存在或者命令格式不正确,Redis将拒绝执行这个事务
- 执行错误:事务在执行的过程中发生错误的命令会被服务器识别出来,并进行相应的错误处理,所以这些出错的命令不会对数据库做任何修改,也不会对事务的一致性产生任何影响
- 服务器停机:如果Redis服务器在执行事务的过程中停机,且服务器运行在任意模式下(无持久化的内存模式、RDB模式或者AOF模式),事务执行中途发生的停机都不会影响数据库的一致性
-
隔离性:Redis使用单线程的方式执行事务,并且服务器保证在执行事务期间不会对事务进行中断,因此,Redis的事务总是串行的方式运行,并且事务总是具有隔离性的
-
耐久性:当服务器运行在AOF持久化模式下,并且
appendfsync
选项的值是always
时,事务是具有耐久性的,其他情况不具有耐久性