Redis事务的本质:将多个命令打包,然后一次性,按顺序的执行!
保证一个队列中,一次性、顺序性、排他性的执行一串命令(作用是防止别的命令插队)
一、Mysql事务四大特性
理解Redis事务之前,先来复习传统关系性数据库Mysql 中具有事务的四大特性:ACID
◈ 原子性 (Atomicity) 一个事务中的所有操作,要么全部完成,要么全部不完成!
如果事务中某条语句执行失败了,前面已经执行了的语句,会回滚到未执行前的状态,就像这个事务从来没有执行过一样。
◈ 一致性 (Consistency) 从一个正确状态切换到另一个正确状态,数据库的完整性约束未被破坏
事务执行前后都必须处于一致性状态,数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变),保证数据的正确和一致性
◈ 隔离性 (Isolation) 事务内部的操作与其他事务是隔离的,多个事务同时执行互不影响
当多个用户同时访问数据库时,比如操作同一张表,数据库为每一个用户开启的事务具有隔离性,不会被互相干扰,各搞各的互不影响!
◈ 持久性 (Durability) 事务结束后,对数据的修改是永久的!
一个事务一旦被提交了,则对数据库中的数据的改变就是永久性的,即便是在数据库发生了故障,也不会丢失提交事务的操作。
二、Redis 事务
1、Redis事务特性
◈ Redis没有隔离级别的概念!
开启事务后,所有命令会被放入队列中,实际并没有真正执行命令,只有发起exec执行命令时才会被顺序依次执行!如图:
◈ Redis单条命令是保证原子性的,但是事务不保证原子性!
同一个事务中如果有一条命令执行失败,其他命令仍然会被执行,不存在事务回滚概念,如图:
2、事务执行命令
命令 | 描述 |
multi | 标记一个事务块的开始 |
exec | 执行所有事务块内的命令 |
discard | 取消事务,放弃执行事务块内的所有命令 |
watch | 监视多个 key ,如果在事务执行之前这些key 被其他命令所改动,那么事务将被打断 |
unwatch | 取消 WATCH 命令对所有 key 的监视 |
3、事务执行过程
- 开启事务(multi)
- 命令入队(...)
- 执行事务(exec)
▏开启事务
127.0.0.1:6379> multi # 1. multi开启事务
OK
127.0.0.1:6379(TX)> set k1 v1 # 2. 命令依次入队
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec # 3. exec执行事务,并返回执行结果
1) OK
2) OK
3) OK
4) "v2"
▶ 流程如下:
1. multi 开启事务:目的是将客户端的 REDIS_MULTI
选项打开,从非事务状态切换到事务状态。
2. 命令入队:当客户端进入事务状态之后,继续发送命令,服务器会将这些命令全部放进一个事务队列里, 然后返回QUEUED
, 表示命令已入队,如图:
注意:不是所有的命令都会被放进事务队列,以下情况命令不会放入队列,而是直接执行:
- 客户端处于非事务状态下
- 发送exec、discard、multi、watch这四个命令
3. exec 执行事务:服务器根据客户端所保存的事务队列, 依次按照顺序执行,即采用先进先出(FIFO)方式,执行的结果也会以 FIFO 的顺序保存到一个回复队列中
当队列内的所有命令执行完毕后,exec命令会将回复队列作为执行结果返回给客户端, 客户端从事务状态返回到非事务状态, 至此, 事务执行完毕。
▏重复事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> multi
(error) ERR MULTI calls can not be nested # 报错:multi命令无法嵌套使用
127.0.0.1:6379(TX)>
结论:当客户端处于事务状态时,再次向服务端发送multi命令时,直接就会向客户端返回错误
▏取消事务
127.0.0.1:6379> multi # 1. multi开启事务
OK
127.0.0.1:6379(TX)> set k1 v1 # 2. 命令依次入队
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> discard # 3. discard取消事务
OK
127.0.0.1:6379> mget k1 k2 k3 # 4. 查看数据是否修改
1) (nil)
2) (nil)
3) (nil)
结论:执行discard命令时, 事务会被放弃, 事务队列会被清空, 并且客户端会从事务状态中退出
▏事务错误
1、编译异常
127.0.0.1:6379> multi # 1. multi开启事务
OK
127.0.0.1:6379(TX)> set k1 v1 # 2. 命令依次入队
QUEUED
127.0.0.1:6379(TX)> set k2 # --故意不设置k2的value值,产生语法错误
(error) ERR wrong number of arguments for 'set' command #--提示语法错误
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec # 3. exec执行事务报错:由于先前的错误导致事务取消
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> mget k1 k2 k3 # 4. 查看数据是否操作成功
1) (nil)
2) (nil)
3) (nil)
说明:在上述中,执行 set k2 命令时,故意不设置k2的value值,让其产生语法错误,从而导致命令入队失败,服务端会立即返回错误信息给客户端,并且客户端在调用 exec 命令执行事务时,服务端会拒绝执行,并自动取消这个事务!
注意:命令入队成功会返回:QUEUED 入队失败返回错误信息提示
2、运行异常
127.0.0.1:6379> set k1 v1 #前提:设置k1的为字符串的值 “v1"
OK
127.0.0.1:6379> multi # 1. multi开启事务
OK
127.0.0.1:6379(TX)> set k2 v2 # 2. 命令依次入队
QUEUED
127.0.0.1:6379(TX)> incr k1 #incr命令是只支持对整数型的值执行:+1操作
QUEUED
127.0.0.1:6379(TX)> exec # 3.exec执行事务:命令2执行失败,k1值不是整数,无法执行+1
1) OK
2) (error) ERR value is not an integer or out of range
127.0.0.1:6379> mget k1 k2 # 4. 查看数据变化
1) "v1"
2) "v2"
说明:在上述中,对一个字符串类型的值,执行整数型的+1计算,命令没有问题,问题在于处理了错误类型的键值,从而导致运行失败了,在同一个事务中,其他命令都执行成功了,只有运行失败的命令没有成功,这就是为什么:Redis中单条命令具有原子性,但事务不具备原子性!
原子性:要么同时成功!要么同时失败!
结论:事务错误分2种
- 编译异常:执行exec命令之前,入队命令产生语法错误,造成入队失败,或redis内存不足
- 运行异常:调用exec之后,命令没错,而是运行导致失败了,例如处理了错误类型的键等
Redis 针对如上两种错误采用了不同的处理策略:
- 在 exec 执行之前的错误,服务器会对命令入队失败的情况进行记录,客户端调用 exec 命令时,拒绝执行并自动放弃这个事务
- 在 exec 执行之后所产生的错误,没有进行特别处理: 即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行。
4、Watch 监视
Redis Watch 命令用于监视一个(或多个) key ,如果在exec事务执行之前,任意一个被监视的key被其他客户端所改动,则整个事务将被打断,不再执行, 直接返回失败。
事务只能在被监视下的键,都没有被修改的前提下执行, 否则事务就不会被执行。
什么情况下,key的监视会被取消?
- 调用 unwatch 命令会取消对所有key的监控
- 当
exec
被调用时, 不管事务是否成功执行, 对所有key的监视都会被取消 - 当客户端断开连接时, 该客户端对键的监视也会被取消
▏带watch的事务
1. 假设服务端有:100块钱、2个苹果,此时客户端1 执行 “watch” 命令监视 money这个key
2. 在客户端1 发起监视money这个key后,客户端2 在客户端1 消费前,先消费了5块钱,此时服务端moeny还剩95块,
3. 紧接着,客户端1 调用exec执行事务,但是消费失败了!因为在客户端1 发起监视 "moeny" 这个key后,客户端2 修改了 "moeny" 这个key的值,所以导致客户端1 取消了事务
如果还是同样的场景,我们没有 watch moeny
,事务不会失败,并且都能消费成功
★ 结论
watch指令,类似乐观锁,通过 watch 命令在事务执行之前监控了多个 keys,倘若在 watch 之后有任何 key 的值发生了变化,exec 命令执行的事务都将被放弃,整个事务队列将被取消,同时返回 Null 以通知调用者事务执行失败。
5、Watch 命令的实现
在每个代表数据库的 redis.h/redisDb
结构类型中, 都保存了一个 watched_keys
字典, 字典的键是这个数据库被监视的键, 字典的值则是一个链表, 链表中保存所有监视这个键的客户端
如图:
如上图所示: 键 key1
正在被 client2
、 client5
和 client1
三个客户端监视, 其他一些键也分别被其他别的客户端监视着。
watch 命令的作用, 就是将当前客户端和要监视的键在
watched_keys
中进行关联。
如果程序想检查某个键是否被监视, 只要检查watched_keys
字典中是否存在这个键; 如果程序要获取监视某个键的所有客户端, 则只要取出键的值(一个链表), 然后对链表进行遍历即可。
6、Watch 命令的实现
在任何对数据库键空间(key space)进行修改的命令成功执行之后 (比如 flushdb、set、del、lpush、sadd、zrem 诸如此类),multi.c/touchWatchedKey
函数都会被调用 —— 它检查数据库的watched_keys
字典, 看是否有客户端在监视已经被命令修改的键, 如果有的话, 程序将所有监视这些被修改key的客户端的REDIS_DIRTY_CAS
选项打开:
当客户端发送 exec 命令、触发事务执行时, 服务器会对客户端的状态进行检查:
- 客户端的
REDIS_DIRTY_CAS
选项已经被打开,说明被客户端监视的key至少有一个已被其他客户端修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务并返回nil REDIS_DIRTY_CAS
选项没有被打开,说明所有监视的key 都安全,服务器正式执行事务。
三、为什么 Redis 不支持回滚(roll back)
同一事务内,某个命令执行失败,其他执行成功的命令照常执行,不会回滚到未执行前状态
只有程序错误才会导致Redis命令执行失败(例如语法错误等),这种错误很有可能在程序开发期间发现。并且事务回滚并不能解决任何程序错误。例如,如果某个查询会将一个键的值递增2,而不是1,或者递增错误的键,那么事务回滚机制是没有办法解决这些程序问题的。
没有人能解决程序员自己的错误,这种错误可能会导致Redis命令执行失败。正因为这些程序错误不大可能会进入生产环境,所以Redis选择更加简单和快速的无回滚方式来处理事务。