MySQL乐观锁和悲观锁,业务中怎么选

前言

做后端开发的同学肯定都遇到过并发修改数据的问题:两个人同时改同一条数据,结果后提交的把先提交的覆盖了,数据就不对了。这时候怎么办?就需要用到并发控制。

常见的并发控制方案有两种:乐观锁和悲观锁。很多同学搞不懂这两个有什么区别,也不知道业务里应该选哪个。这篇文章就把乐观锁和悲观锁的概念、实现方法、怎么选讲清楚,看完之后你就知道什么时候用哪个了。

一、什么是悲观锁

悲观锁,顾名思义,就是很悲观。它认为并发冲突的概率很高,所以在操作数据之前,一定要先把数据锁住,然后再操作。

就像你要拿一个贵重物品,你怕别人也拿,所以先把它锁起来,你操作完了再放开。

悲观锁的特点

  • 先锁再操作:操作之前先加锁
  • 并发度低:同一时间只能有一个事务操作
  • 适合冲突多的场景:如果并发冲突很频繁,悲观锁比较合适

二、什么是乐观锁

乐观锁,顾名思义,就是很乐观。它认为并发冲突的概率很低,所以操作数据的时候不加锁,等更新的时候再检查一下,看看有没有人在这期间改过数据。

就像你要拿一个东西,你觉得没人会跟你抢,所以直接去拿,拿的时候再看一眼,如果发现已经被别人拿了,那就重新来。

乐观锁的特点

  • 操作的时候不加锁:先查出来,更新的时候再检查
  • 并发度高:不用提前加锁,多个事务可以同时操作
  • 适合冲突少的场景:如果并发冲突不频繁,乐观锁性能更好

三、悲观锁怎么实现

悲观锁一般用数据库的行锁来实现。MySQL里有两种方式:

1. SELECT ... FOR UPDATE

这个语句会给查询到的行加排他锁,其他事务就不能再改这些行了。

举个例子:

-- 开启事务
BEGIN;

-- 查询商品库存,并且加锁
SELECT stock FROM goods WHERE id = 1 FOR UPDATE;

-- 如果库存够的话,扣减库存
UPDATE goods SET stock = stock - 1 WHERE id = 1;

-- 提交事务
COMMIT;

这个流程就是:先把这行锁住,然后查库存,然后扣减库存,提交事务的时候释放锁。在这个过程中,其他事务想要修改这行数据,就必须等你释放锁。

2. 用UPDATE语句自带的锁

其实UPDATE语句本身就会加排他锁。所以你也可以不用SELECT FOR UPDATE,直接用UPDATE:

UPDATE goods SET stock = stock - 1 WHERE id = 1 AND stock > 0;

这样的话,如果库存够,就更新成功;如果库存不够,就更新0行。这其实也是一种悲观锁的实现。

四、乐观锁怎么实现

乐观锁一般用版本号或者时间戳来实现。

1. 版本号方式

在表里加一个version字段,每次更新的时候,把version加1。更新的时候检查一下version是不是跟之前查出来的一样,如果一样就更新,不一样就说明已经被别人改过了。

举个例子:

-- 先查出来数据,得到version
SELECT stock, version FROM goods WHERE id = 1;

-- 假设查出来version是1,stock是10

-- 更新的时候,带上version条件
UPDATE goods 
SET stock = stock - 1, version = version + 1 
WHERE id = 1 AND version = 1;

如果这个UPDATE语句执行之后,影响行数是1,说明更新成功了;如果影响行数是0,说明这期间已经有人改过数据了,更新失败,这时候就需要重试。

2. 时间戳方式

跟版本号方式差不多,只不过用时间戳代替版本号。每次更新的时候,把更新时间改成当前时间,更新的时候检查一下时间是不是跟之前查出来的一样。

五、乐观锁和悲观锁怎么选

很多同学问:业务里到底应该选乐观锁还是悲观锁?

其实没有绝对的答案,要看具体场景:

选悲观锁的场景

  1. 并发冲突很频繁:如果很多人同时操作同一条数据,冲突很频繁,用悲观锁比较好。不然乐观锁每次都更新失败,要不断重试,反而更麻烦
  2. 事务很长:如果事务里要做很多事情,持锁时间长,用悲观锁可以避免别人在你操作的期间改数据
  3. 不能重试的场景:如果业务上不能重试,那就用悲观锁,提前锁住,保证能成功

选乐观锁的场景

  1. 并发冲突很少:如果并发冲突的概率很低,用乐观锁性能更好,不用提前加锁,并发度高
  2. 事务很短:如果事务里只做一点点事情,那用乐观锁就够了
  3. 读多写少:如果大部分是读,只有很少的写,用乐观锁比较合适

总结一下: 冲突多、事务长 → 悲观锁;冲突少、读多写少 → 乐观锁。

六、乐观锁的ABA问题

乐观锁有一个经典的问题,就是ABA问题。

什么是ABA问题?比如:

  • 事务A查出来数据是version=1,值是100
  • 这时候事务B把数据改成了200,又改回了100,version变成了3
  • 然后事务A更新的时候,它以为值还是100,所以更新成功了

但是中间数据其实已经被改过了,只是最后又改回来了。这就是ABA问题。

怎么解决ABA问题

一般用版本号就能解决ABA问题,因为每次修改版本号都会加1,就算最后改回来了,版本号也不一样了。

如果用时间戳的话,也能解决ABA问题。

常见坑

坑1:以为乐观锁是不加锁

很多同学以为乐观锁就是不加锁,其实不是。乐观锁是更新的时候再加锁,只是操作之前不加锁。它本质上还是要靠数据库的行锁来保证更新的原子性。

坑2:乐观锁更新失败了不知道重试

很多同学用乐观锁,更新失败了就直接报错给用户了。其实更新失败是正常的,这时候应该自动重试几次,重试成功了就没事了。如果重试几次都失败,再报错。

坑3:悲观锁加的锁太大

很多同学用SELECT FOR UPDATE,结果WHERE条件没有用到索引,导致锁了整张表,而不是某一行。这样并发度就太低了,性能很差。一定要确保WHERE条件用到了索引,这样才会加行锁。

坑4:事务太长,持锁时间久

用悲观锁的时候,如果事务很长,持锁时间久,其他事务都在等锁,并发性能就很差。所以一定要把事务尽量缩短,不要在事务里做耗时操作。

坑5:版本号用的不对

很多同学用版本号,但是更新的时候忘了把version加1,或者更新的时候没有带上version条件,这样乐观锁就失效了。一定要确保更新的时候,version加了1,并且WHERE条件里带上了原来的version。

总结

乐观锁和悲观锁的核心知识点:

  1. 悲观锁:认为冲突多,先加锁再操作,并发度低,适合冲突多的场景
  2. 乐观锁:认为冲突少,操作的时候不加锁,更新的时候再检查,并发度高,适合冲突少的场景
  3. 悲观锁实现:SELECT ... FOR UPDATE,或者直接用UPDATE语句自带的锁
  4. 乐观锁实现:版本号方式,或者时间戳方式
  5. 怎么选:冲突多、事务长用悲观锁;冲突少、读多写少用乐观锁
  6. ABA问题:乐观锁的经典问题,用版本号可以解决
  7. 乐观锁更新失败要自动重试:不要直接报错给用户,重试几次就好了

记住:没有绝对的好和坏,看具体场景选。大部分互联网场景,读多写少,用乐观锁比较多,性能更好。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-10-5 09:01