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. 时间戳方式
跟版本号方式差不多,只不过用时间戳代替版本号。每次更新的时候,把更新时间改成当前时间,更新的时候检查一下时间是不是跟之前查出来的一样。
五、乐观锁和悲观锁怎么选
很多同学问:业务里到底应该选乐观锁还是悲观锁?
其实没有绝对的答案,要看具体场景:
选悲观锁的场景
- 并发冲突很频繁:如果很多人同时操作同一条数据,冲突很频繁,用悲观锁比较好。不然乐观锁每次都更新失败,要不断重试,反而更麻烦
- 事务很长:如果事务里要做很多事情,持锁时间长,用悲观锁可以避免别人在你操作的期间改数据
- 不能重试的场景:如果业务上不能重试,那就用悲观锁,提前锁住,保证能成功
选乐观锁的场景
- 并发冲突很少:如果并发冲突的概率很低,用乐观锁性能更好,不用提前加锁,并发度高
- 事务很短:如果事务里只做一点点事情,那用乐观锁就够了
- 读多写少:如果大部分是读,只有很少的写,用乐观锁比较合适
总结一下: 冲突多、事务长 → 悲观锁;冲突少、读多写少 → 乐观锁。
六、乐观锁的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。
总结
乐观锁和悲观锁的核心知识点:
- 悲观锁:认为冲突多,先加锁再操作,并发度低,适合冲突多的场景
- 乐观锁:认为冲突少,操作的时候不加锁,更新的时候再检查,并发度高,适合冲突少的场景
- 悲观锁实现:SELECT ... FOR UPDATE,或者直接用UPDATE语句自带的锁
- 乐观锁实现:版本号方式,或者时间戳方式
- 怎么选:冲突多、事务长用悲观锁;冲突少、读多写少用乐观锁
- ABA问题:乐观锁的经典问题,用版本号可以解决
- 乐观锁更新失败要自动重试:不要直接报错给用户,重试几次就好了
记住:没有绝对的好和坏,看具体场景选。大部分互联网场景,读多写少,用乐观锁比较多,性能更好。
遇到问题加QQ23979811 协助处理