并发控制--乐观锁&悲观锁

现如今大多数网站都会遇到高并发的场景,比我我们所熟知的抢购环节。在高并发场景下对数据库进行读写操作时,如果并发控制做的不好,就很有可能出现超卖的情况。那么如何去做到合理的并发控制呢。

1.悲观锁:

1.1 概念

悲观锁是就是悲观思想,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。

悲观锁主要分为共享锁或排他锁

  1. 共享锁【Shared lock】又称为读锁,简称S锁。共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  2. 排他锁【Exclusive lock】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和其他排他锁。

1.2 流程

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。
在这里插入图片描述

  1. 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
    如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
  2. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
    期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。

1.3 实现

在MySql Innodb引擎中要使用悲观锁,必须关闭MySQL数据库的自动提交属性。因为MySQL默认使用autocommit模式,也就是说,当执行一个更新操作后,MySQL会立刻将结果进行提交。(sql语句:set autocommit=0)

以电商下单扣减库存的过程说明一下悲观锁的使用

# 开始事务
begin;
# 查询出商品库存信息
select quantity from items where id=1 for update;
# 修改商品库存为2
update items set quantity=2 where id=1;
# 提交事务
commit;

以上,在对id = 1的记录修改前,先通过for update的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。

如果以上修改库存的代码发生并发,同一时间只有一个线程可以开启事务并获得id=1的锁,其它的事务必须等本次事务提交之后才能执行。这样可以保证当前的数据不会被其它事务修改。

1.4 问题

上面提到,使用select…for update会把数据给锁住,不过需要注意一些锁的级别,MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。

2. 乐观锁

2.1 概念

乐观锁是一种乐观思想,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

2.2 流程

使用乐观锁就不需要借助数据库的锁机制了, 主要就是两个步骤:冲突检测和数据更新。
在这里插入图片描述

上图的CAS(Compare and Swap)是一种比较典型的乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

2.3 实现

比如前面的扣减库存问题,通过乐观锁可以实现如下:

# 查询出商品库存信息
select quantity from items where id=1
# 修改商品库存为2
update items set quantity=2 where id=1 and quantity=3

以上,在更新之前,先查询一下库存表中当前库存数(quantity),然后在做update的时候,以库存数作为一个修改条件。当提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。

2.4 问题

**注意:**以上更新语句存在一个比较重要的问题,即传说中的ABA问题。

ABA问题通俗的来讲就是你大爷还是你大爷,而你大妈却不是你大妈了。

有一个比较好的办法可以解决ABA问题,那就是通过一个单独的可以顺序递增的version字段。或者使用还可以使用时间戳,因为时间戳天然具有顺序递增性。
乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

以上SQL其实还是有一定的问题的,就是一旦遇上高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。

2.5 优化

还是要想办法减少乐观锁的粒度的。有一条比较好的建议,可以减小乐观锁力度,最大程度的提升吞吐率,提高并发能力!如下:

# 修改商品库存
update item
set quantity=quantity-1 where id=1 and quantity-1>0

以上SQL语句中,如果用户下单数为1,则通过quantity - 1 > 0的方式进行乐观锁控制

以上update语句,在执行过程中,会在一次原子操作中自己查询一遍quantity的值,并将其扣减掉1

高并发环境下锁粒度把控是一门重要的学问,选择一个好的锁,在保证数据安全的情况下,可以大大提升吞吐率,进而提升性能

3. 总结

乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败
悲观锁依赖数据库锁,效率低。更新失败的概率比较低
随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被应用到生产环境中了,尤其是并发量比较大的业务场景

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值