redis的事务
redis通过 multi, exec等命令来实现事务功能,是一种交互式事务
工作机制:在一个事务内,将事务内的多个请求打包,然后一次性、按顺序地执行多个命令,在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求
相关命令
- MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列
- EXEC:执行事务中的所有操作命令
- DISCARD:取消事务,放弃执行事务块中的所有命令,不等于回滚
- WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令
- UNWATCH:取消WATCH对所有key的监视
基本使用
通过上文命令执行,很显然Redis事务执行是三个阶段:
- 开启事务:以MULTI开始一个事务
- 命令入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
- 执行事务:由EXEC命令触发事务
假设 a:stock、b:stock 两个键的初始值是 5 和 10
#初始化
127.0.0.1:6379> set a:stock 5
OK
127.0.0.1:6379> set b:stock 10
OK
#开启事务
127.0.0.1:6379> multi
OK
#将a:stock减1,
127.0.0.1:6379(TX)> decr a:stock
QUEUED
#将b:stock减1
127.0.0.1:6379(TX)> decr b:stock
QUEUED
#实际执行事务
127.0.0.1:6379(TX)> exec
1) (integer) 4
2) (integer) 9
执行步骤
当一个客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:
- 如果客户端发送的命令为 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令的其中一个, 那么服务器立即执行这个命令
- 与此相反, 如果客户端发送的命令是 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令以外的其他命令, 那么服务器并不立即执行这个命令, 而是将这个命令放入一个事务队列里面, 然后向客户端返回 QUEUED 回复
ACID性质
原子性
⚠️ 不支持原子性,没有回滚机制
- 对于编译错误,exec之前就能识别出来,exec时事务会失败,队列中的命令都不执行
127.0.0.1:6379> get a:stock
"3"
127.0.0.1:6379> get b:stock
"9"
#开启事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr a:stock
QUEUED
#错误命令
127.0.0.1:6379(TX)> lpush a:stock
(error) ERR wrong number of arguments for 'lpush' command
#执行事务,因为错误命令导致事务被取消
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get a:stock
"3"
- 对于运行时错误,例如类型使用错误,当前错误用法会失败,其他命令会执行成功且不会回滚
127.0.0.1:6379> get a:stock
"3"
127.0.0.1:6379> get b:stock
"9"
#开启事务
127.0.0.1:6379> multi
OK
#命令1
127.0.0.1:6379(TX)> incr a:stock
QUEUED
#错误命令2
127.0.0.1:6379(TX)> lpush b:stock 1
QUEUED
#命令3
127.0.0.1:6379(TX)> incr a:stock
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 4 #命令1执行成功
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value #命令2执行失败
3) (integer) 5 #命令3执行成功
隔离性
⚠️ 总是具有隔离性的
因为redis使用单线程的方式来执行命令,并且服务器保证了在执行事务期间不会对事务进行中断,因此redis的事务总是以串行的方式运行的,总是具有隔离性的
注意:事务真正的执行是从exec开始的,所以串行也是以exec为标志的,multi开始后的key可能被更新,所以redis提供了watch命令实现一个CAS乐观锁
watch命令
watch命令是一个CAS乐观锁,它在exec命令执行之前,监视任意数量的数据库键,并在exec命令执行时,检查被监视的键,如果有一个已经被修改过了,redis会取消事务,返回nil
- watch命令需要客户端主动显式在multi命令外执行
- watch命令不存在ABA问题
watch的实现机制
每个redis数据库都保存着一个watched_keys字典,字典的键是这个数据库被监视的键, 而字典的值则是一个链表, 链表中保存了所有监视这个键的客户端
如果当前客户端为 client10086 , 那么当客户端执行 WATCH key1 key2 时, 前面展示的 watched_keys 将被修改成这个样子:
通过 watched_keys 字典, 如果程序想检查某个键是否被监视, 那么它只要检查字典中是否存在这个键即可; 如果程序要获取监视某个键的所有客户端, 那么只要取出键的值(一个链表), 然后对链表进行遍历即可
所有对数据库进行修改的命令如:set,lpush,del等,在执行后都会对watched_keys字典检查,若有客户端正在监视被修改的key,则将监视的客户端的REDIS_DIRTY_CAS标识打开,表示该客户端的事务安全性已经被破坏
当服务器接收到一个客户端发来的exec命令时,会根据该客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务,若客户端的REDIS_DIRTY_CAS被打开,服务器会拒绝事务
持久性
⚠️ 不具有持久性
Redis并没有未事务提供任何额外的持久化功能,所以Redis事务的持久性由redis所使用的持久化模式决定:
- RDB 适合做冷备,保存的是某一个时间的数据快照
- AOF 适合热备,一般 AOF 会每隔 1 秒,通过一个后台线程执行一次 fsync 操作,最多丢失 1 秒钟的数据
但是不论在什么模式下运行,在一个事务的最后加上SAVE命令总是可以保证事务的持久性的,但是效率太低,不实用。所以,持久性,也不一定保证
一致性
⚠️ 个人观点:redis事务不能保证一致性,首先不具有回滚能力无法保证原子性,事务执行后无法保证数据的状态一起变更,其次不具备持久性,丢失数据后也会导致数据不一致
网上和一些书籍的观点是 Redis事务是支持一致性的,侧重点是没有包含非法或者无效的错误数据
小结
Redis事务只是一组打包好的命令,保证其能一次性、有序地、不被打断的执行,不具备原子性,并不是真正意义上的事务