redis 系列之——事物及乐观锁

用户头像
诸葛小猿
关注
发布于: 2020 年 07 月 19 日
redis系列之——事物及乐观锁

Redis系列目录



redis系列之——分布式锁



redis系列之——缓存穿透、缓存击穿、缓存雪崩



redis系列之——Redis为什么这么快?



redis系列之——数据持久化(RDB和AOF)



redis系列之——一致性hash算法



redis系列之——高可用(主从、哨兵、集群)



redis系列之——事物及乐观锁



redis系列之——数据类型geospatial:你隔壁有没有老王?



redis系列之——数据类型bitmaps:今天你签到了吗?



布隆过滤器是个啥!



学习mysql的时候,我们常说mysql是有事物的,事物有ACID四个特性,原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。



redis有事物吗?是怎样的呢?下面就使用实际测试的情况,告诉大家结果。



事物 (multi / exec /discard)



在redis中,是有事物的。但是redis的事物是弱事物。事物没有隔离级别,事物中的多条命令也不是原子性的。正是这些原因,在实际的生产中,也很少用到redis的事物。



redis事物本质:一组命令的集合。事物中的所有命令都会被序列化,存放到队列中,事物执行过程中,命令按顺序往下执行。



redis单条命令保证原子性,多条命令不保证原子性。



1.正常事物执行



redis的事物使用有三步:



  • 开启事物 (multi)



  • 命令入队 (需要执行的命令写入队列,先进先出,队列中是一组命令。)



  • 执行事物 (exec)



正常事物展示:



127.0.0.1:6379>
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> multi #开启事物
OK
127.0.0.1:6379> set name wuxl #命令入队
QUEUED
127.0.0.1:6379> set age 30 #命令入队
QUEUED
127.0.0.1:6379> get name #命令入队
QUEUED
127.0.0.1:6379> set addr shanghai #命令入队
QUEUED
127.0.0.1:6379> exec #执行事物
1) OK
2) OK
3) "wuxl"
4) OK
127.0.0.1:6379>



上面的事物提交后,会按顺序依次执行四个命令,执行完成后退出事物。



2.取消事物



事物开启后,也可以取消事物(discard):



127.0.0.1:6379>
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name wuxl
QUEUED
127.0.0.1:6379> set age 30
QUEUED
127.0.0.1:6379> discard #取消事物
OK
127.0.0.1:6379> get age #事物中的命令未执行,这里查询不到
(nil)
127.0.0.1:6379>



3.事物报错



编译错误



编译时报错,是因为队列中的命令本身有问题,导致在命令入队的时候就报错;有编译错误的时候,执行exec会提示失败,所有的命令都不能执行。



127.0.0.1:6379> flushall
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name wuxl
QUEUED
127.0.0.1:6379> get # 错误命令,入队时报错
(error) ERR wrong number of arguments for 'get' command
127.0.0.1:6379> set age 30
QUEUED
127.0.0.1:6379> exec # 事物提交报错,所有的命令都不能执行
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get name # 查询不到结果
(nil)
127.0.0.1:6379> get age
(nil)
127.0.0.1:6379>



运行错误



运行时错误,是入栈的命令本身没有错误,但是在出队执行的时候报错,比如下面对String做自增操作。



127.0.0.1:6379> flushall
OK
127.0.0.1:6379> set name wuxl # 初始化name,是string
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set age 30
QUEUED
127.0.0.1:6379> incr name # 入队的命令,对name做自增。命令本身没有问题,但是执行时会报错
QUEUED
127.0.0.1:6379> set addr shanghai
QUEUED
127.0.0.1:6379> exec # 提交任务,依次执行每一个命令
1) OK
2) (error) ERR value is not an integer or out of range # 第二个命令报错
3) OK
127.0.0.1:6379> get name
"wuxl"
127.0.0.1:6379> get age # 其他的命令都执行成功了
"30"
127.0.0.1:6379> get addr
"shanghai"
127.0.0.1:6379>



这里可以看出,运行时报错了,但是事物不会回滚,而且,出错后不会影响后续的命令执行,只会有出错的那一条命令执行失败。所以,对于队列中的命令,是不存在原子性的



乐观锁 (watch)



1.乐观锁和悲观锁



悲观锁



认为出现并发问题的可能性比较大,比较悲观。这时需要真正的加锁处理。加锁会降低性能。



乐观锁



认为出现并发问题的可能性比较小,比较乐观。这时不需要加锁,只需要在执行修改操作的时候,比较一下原来的数据是否发生变化,如果没有变化就修改,有变化就不修改。在mysl中通常是使用version字段处理。



redis提供了watch命令,可以监控修改数据时,数据是否被其他线程修改过,如果修改过,则本次修改失败,如果没有修改过,则修改成功。其实watch命令就可以看做是redis的乐观锁的实现。



2.转账模拟



下面,模拟的场景是两个账户转账的业务。



单线程模拟



正常转账过程:



127.0.0.1:6379> flushall #清空数据库
OK
127.0.0.1:6379> set acc1 1000 #付钱账户有1000元
OK
127.0.0.1:6379> set acc2 0 #收钱账户有0元
OK
127.0.0.1:6379> multi #开启事物
OK
127.0.0.1:6379> decrby acc1 100 #付钱账户扣款100
QUEUED
127.0.0.1:6379> incrby acc2 100 #收钱账户收款100
QUEUED
127.0.0.1:6379> exec #执行事物
1) (integer) 900
2) (integer) 100
127.0.0.1:6379> get acc1 #付钱账户现在有900
"900"
127.0.0.1:6379> get acc2 #收钱账户现在有100
"100"
127.0.0.1:6379>



上面单线程模拟转账后,付钱账户付完钱后还有900,收钱账户现在有100。这是正常过程。



并发模拟



在这个过程中,如果在执行exec前,有人想acc1中充了1000元,这个时候就会出现并发问题,如果这时不使用锁,执行完成exec后,结果会怎样呢?结果会是acc1有1900,acc1有100,这个结果也是正确的。为啥?因为redis的事物没有隔离性,两个事物会相互影响



如果需要在执行exec时,比较acc1有没有发生变化,如果变化了,就转账失败。该如何处理呢,这就可以使用redis的watch做乐观锁。下面模拟两个客户端同时修改redis数据,使用watch做乐观锁的情况。



第一步:初始化两个账户的金额。acc1是付钱账户,acc2是收钱账户。



127.0.0.1:6379> flushall
OK
127.0.0.1:6379> set acc1 1000 #付钱账户
OK
127.0.0.1:6379> set acc2 0 #收钱账户
OK
127.0.0.1:6379>



第二步:使用客户端一,开启watch监听acc1是否发生变化,同时开启事物,命令入队(转账100元),先不执行事物。



127.0.0.1:6379>
127.0.0.1:6379> watch acc1 # 使用watch监控acc1的账户在执行事物时是否发生变化
OK
127.0.0.1:6379> multi # 开启事物
OK
127.0.0.1:6379> decrby acc1 100 #模拟付钱
QUEUED
127.0.0.1:6379> incrby acc2 100 #模拟收钱
QUEUED
127.0.0.1:6379>



第三步:使用客户端二,修改acc1账户的金额。



127.0.0.1:6379>
127.0.0.1:6379> incrby acc1 1000 # 模拟向acc1的账户再存款1000
(integer) 2000
127.0.0.1:6379> get acc1 # 这是acc1的账户发生变化,有2000
"2000"
127.0.0.1:6379>



这里可以看到,客户端二执行成功了!!!如果是mysql,这个时候,客户端二应该是被阻塞的,必须要等客户端一执行完成后,这里才能成功。这也就是上面说的redis的事物没有隔离性,会相互影响。



第四步:使用客户端一,执行事物。



127.0.0.1:6379> exec # 执行事物,执行时,会比较acc1是否发生变化,如果变化,就执行失败;如果acc1未变化,就执行成功
(nil) #执行失败
127.0.0.1:6379> get acc1
"2000"
127.0.0.1:6379> get acc2
"0"
127.0.0.1:6379>



这里可以看出由于客户端二修改了acc2的账户金额,在客户端一执行exec前,watch监控到acc1的金额发送了变化,所以客户端一的转账过程就失败了。这里其实就是使用watch实现了一个乐观锁。



完成,收工!





传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工!!!





发布于: 2020 年 07 月 19 日 阅读数: 94
用户头像

诸葛小猿

关注

我是诸葛小猿,一个彷徨中奋斗的互联网民工 2020.07.08 加入

公众号:foolish_man_xl

评论

发布
暂无评论
redis系列之——事物及乐观锁