为了账号安全,请及时绑定邮箱和手机立即绑定

并发业务中的乐观锁与悲观锁详解

并发业务是一种非常常见且重要的业务场景。比较典型的业务场景是电商业务,尤其是秒杀场景,这里面会涉及到非常多的并发事务,像金融业务等等交易相关的业务也都是如此。这在数据库的应用中称之为TP场景的业务,与AP业务相对应。

        当前主流的互联网业务架构大致可以分为三层,第一层是前端层,主要与用户产生交互式关系;第二层可以认为是业务层,所有的业务处理逻辑,诸多if-else判断逻辑等都可以抽象到此类;第三层便是数据持久层,数据持久层即与数据库打交道的一层。对于这种TP业务来讲,对事务的要求非常高,除了数据库能够提供ACID外,还要数据库系统能够保证高并发性能。
        通常,在处理数据库并发业务上,我们要通过加锁的方式来保障数据并发修改中的版本一致。通常的方法可以分为乐观锁和悲观锁,下面我们来探讨一下乐观锁和悲观锁这两种锁的类别。

乐观锁与悲观锁

        数据库在涉及到并发修改时,就会面临并发修改后再进行数据读取时,读取出来的数据与预期数据是否一致的问题。要想使实际读取上来的数据与预期数据一致,那么加锁是一种比较简单的方法。值得一提的是,乐观锁与悲观锁是抽象在思想上的,并不是指具体的加锁方法,而是一种针对不同业务而提出的并发控制方法论。
        所谓乐观锁就是认为业务场景中并发修改造成的数据冲突的概率不会太高,只需要在业务完成后提交时加锁即可。说白了就不真正加锁,而是通过逻辑判断的方法来实现并发控制,这样的好处就是轻量,性能好;缺点就是一旦并发冲突超过预期,乐观锁就会造成业务上的大量失败线程(更新失败的线程不会等待合适的时机去更新,而是直接报错)。例如淘宝中下单时正常,但是在付款时却提示库存不足。这很明显是在下单和付款之间并没有通过一把锁来保障整个业务的原子性,也就导致了下单时和付款时预期数据与实际数据不一致的问题。这个场景也很好解决,只需要在从下单到付款这个过程中用一把锁来保障就好了,即在下单时加锁,在付款结束后释放锁。这个过程调用数据库的事务,故而高度依赖数据库的锁性能,相对于乐观锁更重,性能开销更大。

悲观锁

        在上面我们介绍过悲观锁与乐观锁,下面我们详细说一下悲观锁(pessimistic concurrency control, PCC)。
我们前面提到过,悲观锁要求将整个业务过程作为一个整体的事务提交,锁的粒度大且重,也就是在数据被修改之前先加上锁。
       悲观锁的具体实现方法主要是:
       在数据修改之前加排他锁(exclusive locking). 我们知道,排它锁是会对读、写都排斥的锁。这样在数据被修改之前其他的任务就没有办法访问被加锁对象,自然可以保障数据不会被并发修改或访问。如果其他任务发现被访问对象已经被加锁,那么其他任务可以报异常或者一直等着。这样除了会有性能损耗之外,还可能造成死锁。
ps. MySQL 关闭事务自动提交属性:

    set autocommit = 0;

我们可以通过SQL语句来描述:

begin;
select 库存余量;
update 库存余量 - 1;
commit;

乐观锁

        相对于悲观锁而言,乐观锁(optimistic locking) 不是通过数据库的方式进行并发控制的,而是在业务逻辑层通过增加一些判断条件来检测冲突的。乐观锁的一个假设条件是:一般情况下数据不会造成冲突。因此,乐观锁在数据提交后,准备更新时才会对数据进行并发检测。
        CAS(compare and swap)是一种常用的乐观锁思想,这个过程可以描述如下:

select 库存余量 as q0
Update 库存余量 - 1 where 库存余量 = 刚刚select的结果q0

        上面的SQL伪代码中第一个select先查询,第二个update在更新的时候添加一个判断条件,这个判断条件是上一个select的返回结果。这样表面上看第二个update语句会与第一个select到的结果相关连,二者存在逻辑上的关联关系,因此是不可分的。
但是,这有个问题,就是:如果在 select 和 update 语句中,存在另外一个并发任务先把这个select 到的结果q0 更改为 q1 然后又偷偷改回了q0, 这样对于第二个update语句来讲,是不可知的。这就是传说中的ABA问题。

ABA问题问题发生了又能怎样?下面这个例子引用自维基百科:

你拿着一个装满钱的手提箱在飞机场,此时过来了一个火辣性感的美女,然后她很暖昧地挑逗着你,并趁你不注意的时候,把用一个一模一样的手提箱和你那装满钱的箱子调了个包,然后就离开了,你看到你的手提箱还在那,于是就提着手提箱去赶飞机去了。

        ABA问题是CAS自带的一个副作用,可以理解为你表面上认为数据没有变化,但是数据实际上已经发生了变化。上面的sql语句是一个具体的常数q0,所以你感觉不到什么。加入这个q0是一个指针,指向了某个地址,该地址的内容已经变化了,但是指针的值却没有变,你能鉴别出来吗?当然可以。但是你仍然会存在没有进行深度鉴别的可能。这就造成了ABA隐患。
        消除ABA隐患的方法也比较简单,就是在每次更新的时候,加一个记录字段,这个记录字段设置为自增的,没更新一次就增长一个。这样,在update语句中,where条件再多加一个,就可以避免ABA问题了。例如:

select 库存余量 as q0, version as v0
update 库存余量-1,version + 1 where 库存余量 = q0 and version = v0

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
全栈工程师
手记
粉丝
6753
获赞与收藏
471

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消