Redis事务和乐观锁
一、Redis的事务
如果学过MySQL就知道MySQL的事务需要遵循ACID原则,其中A就表示原子性,意思就是一个事务中的所有操作要么都执行成功,要么都不执行,不存在部分执行部位未执行的情况。
但是在Redis中,Redis的单条命令是保证原子性的,但是事务不保证原子性!并且没有隔离级别的概念!
Redis事务的本质:简单来说Redis的事务就是一组命令的集合,它把一组命令放入到一个队列中,并没有立即执行。在发起执行命令(exec)才会按顺序执行,并且在执行的过程中,不会收到其他请求的影响。
Redis执行一个事务的过程:开启事务(multi)–> 命令入队(…)–> 执行事务(exec)/ 放弃事务(discard),表面上看来Redis的事务也是一组命令要么都执行,要么都不执行,那么为什么说Redis事务不保证原子性呢?
首先我们要知道Redis事务在执行的过程中会出现的两种异常:编译时异常和运行时异常
# 编译时异常(指代码有问题,命令有错)
127.0.0.1:6380> multi # 开启一个事务
OK
127.0.0.1:6380(TX)> set k1 v1
QUEUED
127.0.0.1:6380(TX)> set k2 v2
QUEUED
127.0.0.1:6380(TX)> set k3 # 这是一个错误的命令
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6380(TX)> get k1
QUEUED
127.0.0.1:6380(TX)> get k2
QUEUED
127.0.0.1:6380(TX)> exec # 执行事务,发现由于队列中的命令有错所以取消了事务,事务并没有执行
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6380>
# 运行时异常(代码没有问题,逻辑有问题导致运行的时候出现异常)
127.0.0.1:6380> multi # 开启一个事务
OK
127.0.0.1:6380(TX)> set k1 v1
QUEUED
127.0.0.1:6380(TX)> set k2 v2
QUEUED
127.0.0.1:6380(TX)> incr k1 # k1的值是v1,不是一个数值型的值,理论上是无法+1的,但是由于命令没有错,所以成功入队了
QUEUED
127.0.0.1:6380(TX)> get k1
QUEUED
127.0.0.1:6380(TX)> get k2
QUEUED
127.0.0.1:6380(TX)> exec # 执行事务
1) OK
2) OK
3) (error) ERR value is not an integer or out of range # incr k1 命令执行失败,但是其他的命令依然执行成功了
4) "v1"
5) "v2"
127.0.0.1:6380> get k1
"v1"
127.0.0.1:6380> get k2
"v2"
127.0.0.1:6380>
在MySQL中,我们知道如果事务中的某一个操作出现了异常是会回滚事务的,也就是其他命令并不会执行或者已经执行的命令会撤销掉,但是在Redis的事务中并没有回滚这个操作,在事务的执行过程中,某一个命令的执行出错了并不会影响其他命令的执行,所以说Redis事务是不保证原子性的。
为什么Redis不支持回滚?
官方给出了这样的解释:
如果你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。
以下是这种做法的优点:
- Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
- 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
简单粗暴的讲,Redis命令执行失败是由于编程错误,是你们编程人员的锅,跟我们Redis一毛钱关系都没有!!!
二、Redis可以实现乐观锁
先了解一下悲观锁和乐观锁:
悲观锁:假设数据操作不管在什么时候都会发生冲突,无论做什么都需要加锁来保证读写数据的过程中数据不会被其他线程读写
乐观锁:认为数据操作不管在什么时候都不会发生冲突,所以不会锁上,在数据更新的时候才会对数据的冲突与否进行检查。
虽然Redis是单线程的,且Redis事务不保证原子性,但是由于在Redis事务中存在一个监控功能(watch),所以可以使用该功能来实现乐观锁。
# 正常执行,没有出现错误的情况
127.0.0.1:6380> set money 100 # 设置一个k-v
OK
127.0.0.1:6380> watch money # 监视money
OK
127.0.0.1:6380> multi # 开启事务
OK
127.0.0.1:6380(TX)> decrby money 20 # 对money进行修改
QUEUED
127.0.0.1:6380(TX)> exec # 执行事务,在执行过程中money并没有被其他客户端进程进行修改
1) (integer) 80
127.0.0.1:6380> get money # 成功获取正确的money的值
"80"
127.0.0.1:6380>
# 非正常执行,执行不成功的情况
# 客户端1
127.0.0.1:6380> set money 100 #设置一个k-v
OK
127.0.0.1:6380> watch money # 监视money
OK
127.0.0.1:6380> multi # 开启事务
OK
127.0.0.1:6380(TX)> decrby money 20 # 对money进行修改
QUEUED
127.0.0.1:6380(TX)> exec # 执行事务之前,先打开另一个客户端2对money进行修改,修改后再执行该客户端的事务
(nil) # 发现没有数据被修改成功
127.0.0.1:6380> get money # 得到的money的值是客户端2修改过后的值
"0"
127.0.0.1:6380>
# 客户端2
127.0.0.1:6380> set money 0 # 在客户端1执行事务之前先对money进行修改
OK
127.0.0.1:6380>
这就是一个简单的通过watch来实现乐观锁的例子,如果客户端1还想继续对money进行修改,那么可以通过unwatch接触监控后重新监控再进行修改。
(由于官方把watch和unwatch归类于事务这一块,所以我将乐观锁和事务放在一块了)
参考视频:
https://www.bilibili.com/video/BV1S54y1R7SB?p=21
https://www.bilibili.com/video/BV1S54y1R7SB?p=22