mysql 开启事物_redis系列之——事物及乐观锁

edac3618c1db729ed8e0cdebd92d4a6a.png

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

完成,收工!

496a030ef82d3fab3a12bfe2b98f0d79.gif

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值