悲观锁和乐观锁

一、悲观锁

对数据抱悲观态度,认为数据并发修改的概率比较大,因此在数据修改之前先加锁。采用 “先取锁再访问” 的策略,为数据处理的安全提高了保证。

但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加死锁的机会。另外,还会降低并行度,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。

1. 实现方式

悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:

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

注意:

  • 要使用悲观锁,注意按业务场景关闭 mysql 数据库中自动提交的属性, set autocommit=0; 因为 mysql 默认是自动提交的。
  • 对记录的修改通过 for update 的方式加锁,然后再进行修改。这就是比较典型的悲观锁策略
  • mysql InnoDB 默认行级锁。行级锁是基于索引的,如果一个 SQL 语句用不到索引是不会使用行级锁,会使用表级锁把整张表锁住

二、乐观锁

假设数据一般不会造成冲突,因此在数据进行提交更新时,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何做。乐观锁一般的方式就是记录数据版本。

因为没有使用到锁,因此不会产生死锁

1. 实现方式

主要有两个步骤:冲突检测和数据更新。实现方式比较典型的就是 CAS(Compare and Swap)技术。当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。比如:

1
2
select quantity from items where id = 1; // 查询
update items set quantity = 2 where id = 1 and quantity = 3; // 更新

我们在更新之前,先查询一下 quantity ,然后在更新 quantity 的时候,判断数据库表对应记录的当前 quantity 与第一次取出的进行对比,如果相等,则更新,否则认为是过期数据。产生 ABA 问题。

  • ABA 问题

ABA 问题:比如一个线程A取出来一个值为 3,另外一个线程B这时先把这个值 3 改为 2,然后又改为 3;然后线程A 发现值没有变,继续操作。这个过程是有问题的。

如何解决 ABA 问题,通过一个单独的可以顺序递增的 version 字段,改为以下方式:

1
2
select version from items where id = 1; // 先查询到 version 信息
update items set quantity = 2, version = version + 1 where id = 1 and version = 2;

这种方式并没有解决 ABA 问题,而是避免了 ABA 问题,因为默认了 version 这个字段是递增的。乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。因为每次操作的版本号都会增加。

除了 version 字段,还可以使用时间戳,因为时间戳天然具有顺序递增性。

  • 性能问题

一旦高并发的时候,就只有一个线程可以修改成功,其他线程就会存在大量失败。应该减小乐观锁的粒度,最大程度的提升吞吐率,提高并发能力。

在某些场景可以使用条件限制来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 一个表的结构如下,status 表示产品状态, 1 是在售。 2 是暂停出售
mysql> select * from t_goods;
+----+--------+------+---------+
| id | status | name | num |
+----+--------+------+---------+
| 1 | 1 | 道具 | 10 |
| 2 | 2 | 装备 | 10 |
+----+--------+------+---------+


update t_goods
set num = num - $(buynum)
where
id = 1
and num - $(buynum) >= 0
and status = 1;

num - $(buynum) >= 0 这种情况适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高

注意:乐观锁的更新操作,最好用主键或者唯一索引来更新,这样是行锁,否则会加表锁。