参考源
https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0
版本
本文章基于 Redis 6.2.6
Redis 事务的本质是一组命令的集合
事务支持一次执行多个命令,一个事务中所有命令都会被序列化。
在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
所以说:Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis 事务没有隔离级别的概念
批量操作在发送 EXEC
命令前被放入队列缓存,并不会被实际执行。
Redis 事务不保证原子性
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。
事务中任意命令执行失败,其余的命令仍会被执行。
Redis事务的三个阶段
开始事务
命令入队
执行事务
watch
watch key1 key2 ...
监视一或多个 key,如果在事务执行之前,被监视的 key 被其他命令改动,则事务被打断(类似乐观锁)。
unwatch
取消对所有 key 的监控。
multi
标记一个事务块的开始,形成队列(queued)。
exec
执行所有事务块(一旦执行 exec
后,之前加的监控锁都会被取消掉)。
discard
取消事务,放弃事务块中的所有命令。
正常执行
127.0.0.1:6379> multi # 开启事务 OK 127.0.0.1:6379(TX)> set k1 v1 # 命令入队 QUEUED 127.0.0.1:6379(TX)> set k2 v2 # 命令入队 QUEUED 127.0.0.1:6379(TX)> get k2 # 命令入队 QUEUED 127.0.0.1:6379(TX)> set k3 v3 # 命令入队 QUEUED 127.0.0.1:6379(TX)> exec # 执行事务 1) OK 2) OK 3) "v2" 4) OK 127.0.0.1:6379> get k1 # set命令执行成功 "v1" 127.0.0.1:6379> get k2 # set命令执行成功 "v2"
开启事务后,会出现 TX 标志,此时所有的操作不会马上有结果,而是形成队列(QUEUED),待执行事务后,会将所有命令按顺序执行。
放弃事务
127.0.0.1:6379> multi # 开启事务 OK 127.0.0.1:6379(TX)> set k1 v1 # 命令入队 QUEUED 127.0.0.1:6379(TX)> set k2 v2 # 命令入队 QUEUED 127.0.0.1:6379(TX)> set k3 33 # 命令入队 QUEUED 127.0.0.1:6379(TX)> discard # 取消事务 OK 127.0.0.1:6379> get k3 # set命令未执行 "v3"
事务中存在命令性错误
若在事务队列中存在命令性错误(类似于java编译性错误),则执行 exec
命令时,所有命令都不会执行。
127.0.0.1:6379> multi # 开启事务 OK 127.0.0.1:6379(TX)> set k1 11 # 命令入队 QUEUED 127.0.0.1:6379(TX)> getset k2 # 错误命令 (error) ERR wrong number of arguments for 'getset' command 127.0.0.1:6379(TX)> set k2 22 # 命令入队 QUEUED 127.0.0.1:6379(TX)> exec # 执行事务,报错 (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get k1 # set命令未执行 "v1" 127.0.0.1:6379> get k2 # set命令未执行 "v2"
事务中存在语法性错误
若在事务队列中存在语法性错误(类似于 Java 的的运行时异常),则执行 exec
命令时,其他正确命令会被执行,错误命令抛出异常。
127.0.0.1:6379> multi # 开启事务 OK 127.0.0.1:6379(TX)> set k4 v4 # 命令入队 QUEUED 127.0.0.1:6379(TX)> incr k4 # 命令入队(对“v4”进行 +1 ,会报语法错误) QUEUED 127.0.0.1:6379(TX)> set k5 v5 # 命令入队 QUEUED 127.0.0.1:6379(TX)> exec # 执行事务 1) OK 2) (error) ERR value is not an integer or out of range # 执行错误的命令会报错,其余命令正常执行 3) OK 127.0.0.1:6379> get k4 # set命令执行成功 "v4" 127.0.0.1:6379> get k5 # set命令执行成功 "v5"
悲观锁(Pessimistic Lock),顾名思义,就是很悲观。
每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。
这样别人想拿到这个数据就会 block 直到它拿到锁。
传统的关系型数据库里面就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在操作之前先上锁。
乐观锁(Optimistic Lock),顾名思义,就是很乐观。
每次去拿数据的时候都认为别人不会修改,所以不会上锁。
但是在更新的时候会判断一下再此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁适用于多读的应用类型,这样可以提高吞吐量。
乐观锁策略:提交版本必须大于记录当前版本才能执行更新。
初始化信用卡可用余额和欠额
127.0.0.1:6379> set balance 100 OK 127.0.0.1:6379> set debt 0 OK
使用 watch 监听 balance,事务期间 balance 数据未变动,事务执行成功。
127.0.0.1:6379> watch balance OK 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> decrby balance 20 QUEUED 127.0.0.1:6379(TX)> incrby debt 20 QUEUED 127.0.0.1:6379(TX)> exec 1) (integer) 80 2) (integer) 20
使用 watch 监听 balance,事务期间 balance 数据变动,事务执行失败。
窗口 1:
127.0.0.1:6379> watch balance OK 127.0.0.1:6379> multi OK
窗口 2:
127.0.0.1:6379> get balance "80" 127.0.0.1:6379> set balance 200 OK
窗口 1:
127.0.0.1:6379(TX)> decrby balance 20 QUEUED 127.0.0.1:6379(TX)> incrby detb 20 QUEUED 127.0.0.1:6379(TX)> exec (nil) 127.0.0.1:6379> get balance "200"
由于窗口 1 监听 balance 并开启事务后,窗口 2 修改了 balance 的值,导致窗口 1 的监听失败,执行事务后展示为空,且 balance 的值不是预期值。
监听失败后放弃监听,然后重来
窗口 1:
127.0.0.1:6379> unwatch OK 127.0.0.1:6379> watch balance OK 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> decrby balance 20 QUEUED 127.0.0.1:6379(TX)> incrby debt 20 QUEUED 127.0.0.1:6379(TX)> exec 1) (integer) 180 2) (integer) 40
一旦执行 exec
开启事务后,无论事务是否执行成功, watch
对变量的监听都将被取消。
当事务执行失败后,需重新执行 watch
命令对变量进行监听,并开启新的事务进行操作。
watch
指令类似于乐观锁,在事务提交时,如果 watch
监控的多个 key 中任何 key 的值已经被其他客户端更改。
则使用 exec
执行事务时,事务队列将不会被执行,同时返回 (nil) 应答以通知调用者事务执行失败。