什么是乐观锁与悲观锁?
一. 前言
强调: 无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想
二. 悲观锁
介绍: 悲观的认为操作数据库就是修改数据, 为了避免数据库中的数据同时被修改, 直接对该操作加锁处理
称呼: 悲观并发控制 或者 ‘悲观锁’
针对: 并发修改的概率较大情况
本质: 使用锁进行处理, 只有前面枪锁的释放了以后, 之后的访问用户才能接着进行操作
流程:
在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
示例: 拿比较常用的MySql Innodb引擎举例,来说明一下在SQL中如何使用悲观锁。
"""淘宝下单过程中扣减库存的需求说明一下如何使用悲观锁?"""
# 1. 开始事务
begin;
# 2. 查询出商品库存信息
select quantity from items where id=1 for update; # for update数据库提供锁
# 3. 修改商品库存为2
update items set quantity=2 where id=1;
# 4.提交事务
commit;
'''
在对id = 1的记录修改前,先通过for update的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。
如果以上修改库存的代码发生并发,同一时间只有一个线程可以开启事务并获得id=1的锁,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。
上面我们提到,使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别.
MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。
'''
缺点:
- 数据库产生格外开销
- 增加死锁的风险
- 降低并发性
三. 乐观锁
介绍: 乐观的认为操作数据库不会造成冲突, 只有在对数据进行更新的时候才会进行校验. 如果冲突就返回错误让用户决定如何去做.
称呼: ‘乐观锁’
针对: 数据竞争概率较小的情况
本质: 不加锁进行处理, 而是通过对数据库中
流程: 冲突检测和数据更新
其实现方式有一种比较典型的就是Compare and Swap(CAS)。
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
示例:
"""思路1: 先查询库存, 以库存作为更改条件"""
# 查询出商品库存信息,quantity = 3
select quantity from items where id=1
# 修改商品库存为2
update items set quantity=2 whereid=1 and quantity = 3;
'''
# 描述
以上,我们在更新之前,先查询一下库存表中当前库存数(quantity),然后在做update的时候,以库存数作为一个修改条件。
当我们提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。
# 问题:
以上更新语句存在一个比较重要的问题,即传说中的ABA问题。
比如说一个线程one从数据库中取出库存数3,这时候另一个线程two也从数据库中库存数3,并且two进行了一些操作变成了2,然后two又将库存数变成3,这时候线程one进行CAS操作发现数据库中仍然是3,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
'''
"""思路2: 通过一个单独的可以顺序递增的version字段"""
# 查询出商品信息,version = 1
select version from items where id=1
# 修改商品库存为2
update items set quantity=2,version=3 where id=1 and version= 2; # 修改失败了
'''
# 描述
乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。
# 思路拓展: 除了version以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。
# 问题:
一旦发上高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。
对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。所以,还是要想办法减少乐观锁的力度的。
'''
"""思路3: 减小乐观锁力度,最大程度的提升吞吐率,进而提高并发能力"""
# 修改商品库存
update item set quantity=quantity-1 where id=1 and quantity-1>0;
'''
以上SQL语句中,如果用户下单数为1,则通过quantity-1 > 0的方式进行乐观锁控制。
以上update语句,在执行过程中,会在一次原子操作中自己查询一遍quantity的值,并将其扣减掉1。
高并发环境下锁粒度把控是一门重要的学问,选择一个好的锁,在保证数据安全的情况下,可以大大提升吞吐率,进而提升性能。
'''
四. 总结: 如何选择
# 乐观锁
并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
# 悲观锁
依赖数据库锁,效率低。更新失败的概率比较低。
随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被使用到生产环境中了,尤其是并发量比较大的业务场景。