Redis 事务
事务
事务指的程序中一系列严密的逻辑操作,其中包含的操作必须要完成,否则在每个操作中的更改都会被撤销。
举个简单的例子:一群鸭子过河,要么都过去,要么都不过去。
事务的特性
- 原子性(Atomicity):操作这些指令时,要么全部执行成功,要么全部不执行。只要其中一个指令执行失败,所有的指令都执行失败,数据进行回滚,回到执行指令前的数据状态。
- 一致性(Consistency):事务的执行使数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定。(可理解为:即A账户只要减去了100,B账户则必定加上了100)
- 隔离性(Isolation):隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
- 持久性(Durability):当事务正确完成后,它对于数据的改变是永久性的。
为什么使用事务
在传统的关系型数据库中,常常使用事务的ACID 性质来保证数据的一致性、完整性。
Redis事务
Redis事务:把多个redis命令放到队列,然后一次性的顺序执行。并且在执行过程中不会被中断,执行完所有队列命令后才执行其他客户端其他命令。
下面举个简单的例子,MULTI 开启一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令
一个事务从开始到执行经历三个阶段
- 开始事务
- 命令入队
- 执行事务
开始事务
MULTI 命令的执行标志着事务的开始,这个命令做的就是, 将客户端的 REDIS_MULTI
选项打开, 让客户端从非事务状态切换到事务状态。
命令入队
当客户端处于非事务状态下,所有发送给服务端的命令会立即被执行。
但客户端切换到事务状态后,服务端接受到客户端的命令不会立即执行,而是把这些命令放到事务队列里,然后返回 QUEUED
, 表示命令已入队。
可以由一下流程图表示:
事务队列是一个数组, 每个数组项是都包含三个属性:
- 要执行的命令(cmd)
- 命令的参数(argv)
- 参数的个数(argc)
以上图命令为例子,那么程序将为客户端创建以下事务队列:
数组索引 | cmd | argv | argc |
0 | SET | ["name","xiaoming"] | 2 |
1 | SET | ["age","25"] | 2 |
2 | INCR | ["age"] | 1 |
执行事务
客户端进入到事务状态后,客户端发送的命令不会直接被执行,而是会放到事务队列里。
但并不是所有命令都会放到事务队列,如 EXEC 、 DISCARD 、 MULTI 和 WATCH 这四个命令,无视事务状态,直接被服务器执行。
如果客户端正处于事务状态, 那么当 EXEC 命令执行时, 服务器根据客户端所保存的事务队列, 以先进先出(FIFO)的方式执行事务队列中的命令: 最先入队的命令最先执行, 而最后入队的命令最后执行。
执行事务中的命令所得的结果会以 FIFO 的顺序保存到一个回复队列中。
如上图,程序将为队列中的命令创建如下回复队列:
数组索引 | 回复类型 | 回复内容 |
0 | status code reply | OK |
1 | status code reply | OK |
2 | integer reply | 26 |
当事务队列里的所有命令被执行完之后, EXEC 命令会将回复队列作为自己的执行结果返回给客户端, 客户端从事务状态返回到非事务状态, 至此, 事务执行完毕。
事务过程伪代码:
def execute_transaction(): # 创建空白的回复队列 reply_queue = [] # 取出事务队列里的所有命令、参数和参数数量 for cmd, argv, argc in client.transaction_queue: # 执行命令,并取得命令的返回值 reply = execute_redis_command(cmd, argv, argc) # 将返回值追加到回复队列末尾 reply_queue.append(reply) # 清除客户端的事务状态 clear_transaction_state(client) # 清空事务队列 clear_transaction_queue(client) # 将事务的执行结果返回给客户端 send_reply_to_client(client, reply_queue)
DISCARD、MULTI、WATCH命令
- DISCRAD 命令用于取消一个事务, 它清空客户端的整个事务队列, 然后将客户端从事务状态调整回非事务状态, 最后返回字符串
OK
给客户端, 说明事务已被取消。 - MULTI命令开启一个事务,Redis 的事务是不可嵌套的, 当客户端已经处于事务状态, 而客户端又再向服务器发送 MULTI 时, 服务器只是简单地向客户端发送一个错误, 然后继续等待其他命令的入队。 MULTI 命令的发送不会造成整个事务失败, 也不会修改事务队列中已有的数据。
- WATCH命令只能在客户端进入事务状态之前执行, 在事务状态下发送 WATCH 命令会引发一个错误, 但它不会造成整个事务失败, 也不会修改事务队列中已有的数据(和前面处理 MULTI 的情况一样)
带 WATCH 的事务
WATCH 命令用于在事务开始之前监视任意数量的键: 当调用 EXEC 命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务不再执行, 直接返回失败。
如下图例子:
客户端A
此时客户端B修改了name
客户端A执行事务失败
watch命令实现
在每个代表数据库的 redis.h/redisDb
结构类型中, 都保存了一个 watched_keys
字典, 字典的键是这个数据库被监视的键, 而字典的值则是一个链表, 链表中保存了所有监视这个键的客户端。
如下图:
WATCH 命令的作用, 就是将当前客户端和要监视的键在 watched_keys
中进行关联。
举个例子,如果客户端client5执行 WATCH key1 key2 时,上图将变成下面这样。
通过 watched_keys
字典, 如果程序想检查某个键是否被监视, 那么它只要检查字典中是否存在这个键即可; 如果程序要获取监视某个键的所有客户端, 那么只要取出键的值(一个链表), 然后对链表进行遍历即可。
WATCH 触发
任何对Redis键值的修改操作成功后,multi.c/touchWatchedKey
函数都会被调用,它检查数据库的 watched_keys
字典, 看是否有客户端在监视已经被命令修改的键, 如果有的话, 程序将所有监视这个/这些被修改键的客户端的 REDIS_DIRTY_CAS
选项打开:
当客户端发送 EXEC 命令、触发事务执行时, 服务器会对客户端的状态进行检查:
- 如果客户端的
REDIS_DIRTY_CAS
选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败。 - 如果
REDIS_DIRTY_CAS
选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务。
伪代码如下
def check_safety_before_execute_trasaction(): if client.state & REDIS_DIRTY_CAS: # 安全性已破坏,清除事务状态 clear_transaction_state(client) # 清空事务队列 clear_transaction_queue(client) # 返回空回复给客户端 send_empty_reply(client) else: # 安全性完好,执行事务 execute_transaction()
最后,当一个客户端结束它的事务时,无论事务是成功执行,还是失败, watched_keys
字典中和这个客户端相关的资料都会被清除。
Redis事务的ACID性质
Redis事务具有原子性、一致性、隔离性,并且当Redis运行在某种特定的持久化模式下,也具有持久性。
原子性
Redis事务,事务队列中的命令要么全部执行,要么一个都不执行。所以Redis事务具有原子性。
Redis事务与传统的关系型数据库事务最大的区别在于:Redis事务不支持回滚机制(rollback),即使事务队列中的命令执行期间出现了错误,整个事务会继续下去,直到事务队列里面的命令都执行完。
如下例子:
一致性
Redis通过错误检测和简单的设计保证事务一致性。
- 入队错误
如果一个事务在入队过程中,出现了命令不存在,或者命令格式不正确,Redis拒绝执行这个事务。
如下例子
因为拒掉入队错误的事务,所以一致性不会被入队错误的事务影响。
- 执行错误
事务执行过程中,错误的命令会被识别出来,并进行相应的错误处理,所以一致性不会受到影响。
- 服务器停机
如果服务器运行在没有持久化的内存模式下,那么重启后数据库是空白的,因此数据是一致的。
如果服务器运行在RDB或者AOF模式下,中途停机可以用两种模式恢复,如果找不到恢复文件,数据库是空白的,数据是一致的。
隔离性
Redis采用的单线程的方式实行事务,并且在事务执行过程中不会对事务中断,因此Redis事务以串行的方式运行的,所以Redis事务具有隔离性。
持久性
Redis事务,用队列保存了一些列命令,没有为事务提供额外的持久化功能,所有Redis的持久化由Redis的持久化模式决定。
- 没有持久化
Redis没有开启持久化模式,事务不具有持久性,一旦服务器停机,包括事务在内的数据都将丢失
- RDB
当服务器运行在RDB模式下,服务器只在特定的条件满足时,才会执行BGSAVE命令保存数据库数据。然而异步执行的BGSAVE命令不能保证事务数据第一时间保存到磁盘。
因此RDB模式下的事务不具备持久性。
- AOF
当服务器运行在AOF模式下,并且appendfsync 选项的值为 always 时,程序总会在执行命令后调用调用 sync 函数,将数据同步到磁盘中,因此在此场景中AOF是具有持久性的。
其他情况下,不能保证每执行一个命令就能将数据同步到磁盘,所以事务不具备持久性。
参考文献
https://cloud.tencent.com/developer/article/1133074
http://redisdoc.com/topic/transaction.html
reids设计与实现