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实现了一个乐观锁。

完成,收工!

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

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值