Redis的事务是指将多个命令请求打包,一次性地,按顺序执行的机制。通过MULTI、EXEC、WATCH等命令实现事务功能。
19.1 事务的实现
19.1.1 事务的开始
MULTI命令的执行代表了一个事务的开始,会将执行该命令的客户端由非事务状态切换成事务状态(在客户端状态的flags属性中打开REDIS_MULTI标识)
19.1.2 命令入队
- 非事务状态下,一个命令会被服务器立即执行。
- 事务状态
- EXEC、DISCARD、WATCH、MULTI命令会被立即执行
- 其余命令放入一个事务队列,向客户端返回QUEUED回复
19.1.3 事务队列
每个Redis客户端由自己的事务状态,保存在mstate属性中
typedef struct redisClient{ //事务状态 multiState mstate; //... }
事务状态包含了一个事务队列(一个multiCmd类型的数组),以及入队命令的计数器
typedef struct multiState{ //事务队列,FIFO顺序 multiCmd *commands; //已入队命令计数器 int count; }multiState;
typedef struct multiCmd{ //参数 robj **argv; //参数数量 int argc; //命令指针 struct redisCommand *cmd; }multiCmd;
19.1.4 执行事务
当处于事务状态的客户端向服务器发送EXEC命令时,服务器会将客户端状态事务队列中的所有命令顺序执行,每执行完一个命令将结果暂存,全部执行完之后,移除客户端的事务状态标识,并清空客户端的事务状态,包括入队命令计数器和释放事务队列,最后将所有结果返回给客户端
19.2 WATCH命令的实现
WATCH命令是一个乐观锁,用于监测任意数量的数据库键,在EXEC执行时,检测被监视的键是否至少有一个已经被修改了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
19.2.1 使用WATCH命令监视数据库键
每个Redis数据库保存了一个watched_keys字典,字典的键是被监视的数据库的键,字典的值是一个链表,保存了监视这个键的所有客户端
typedef struct redisDb{ //正在被WATCH命令监视的键 dict *watched_keys; //... }redisDb;
客户端通过执行WATCH命令,将自身添加到watched_keys字典当中
19.2.2 监视机制的触发
所有对数据库进行键相关的操作命令,在执行完毕之后,都会检查watched_keys字典,查看相应的键是否有正在监视的客户端,有的话,会打开对应客户端的REDIS_DIRTY_CAS标识,表示该客户端的事务安全性被破坏
19.2.3 判断事务是否安全
当客户端发起EXEC命令时,服务器会检查当前客户端是否打开了REDIS_DIRTY_CAS标识,打开则拒绝执行事务
19.3 事务的ACID性质
A原子性,C一致性,I隔离性,D持久性
19.3.1 原子性
事务队列中的命令要么都执行,要么都不执行。这中间会存在某条命令执行出错的情况,Redis的事务不支持回滚,命令执行出错依然继续执行后续的命令
19.3.2 一致性
事务执行前后,数据库状态都是一致的。一致是指,数据库中没有非法或者无效的错误数据。可能存在的错误如下
- 入队错误:命令格式错误或者命令的执行函数不存在,会拒绝执行整个事务
- 执行错误:执行出错会有相应的错误处理,不会修改数据库
- 服务器停机:如果有持久化操作,还原之后不会存在错误数据,如果没有,空白的数据库也不存在错误数据
19.3.3 隔离性
多个时间的执行不会相互干扰,因为Redis使用单线程的方式执行事务,执行事务期间不会中断,即Redis中的事务均是以串行的方式执行
19.3.4 持久性
只有在事务执行完成之后,这次事务中的命令全部被持久化,才具备持久性。只有AOF持久化模式,并且appendfsync选项为always时,事务具有持久性
19.4 单线程处理请求的Redis为何需要事务
单个服务器可以处理多个客户端的请求,中间的处理过程可能是交替执行的,某个客户端想要自己的命令被连续执行,则需要开启事务