实战3:如何使用乐观锁
1. 前言
在锁一节中,我们从粒度
和管理
两个角度来阐述了锁。如果你还不熟悉锁,请先阅读该小节,再来进行本小节的学习。
本小节我们将继续深挖锁
,以开发者和实战的角度来谈锁。
2. 为什么需要锁
2.1 什么是数据竞争
在本节的开头,我们来谈一谈为什么开发程序需要使用锁?如果你有一点并发编程的基础,又或者对多线程有一点熟悉,那么你肯定知道答案,那就是数据竞争
。
2.2 数据竞争实例
我们举一个生活的例子。在中学的时候,教室的前面会放上一块小黑板,小黑板上会记载某一天上交作业或谁谁谁打扫卫生之类的。那块小黑板是所有同学都可使用的,只要你有事情要公布,就可以写在上面。
那么问题来了,假设同学A
用小黑板写上明天要上交的作业,此时同学B
也需要写上明天值日的同学,对于A
和B
来说,他们之间存在竞争关系,而小黑板就是竞争点。
直观上来说,如果A
比B
早到,那么A
就可以占有小黑板,换言之A
给小黑板加上了一把锁,B
不能使用小黑板。A
写完了,把小黑板再次放到了教室前,相当于释放了锁,此时B
才可书写小黑板,即B
拿到了锁。
因此,锁的出现是为了解决并发
中存在的数据竞争
问题。
3. 乐观锁和悲观锁
乐观
与悲观
是两种不同的态度,从名字上看,二者就是以开发者的态度作为边界来分类的。
乐观锁认为,同一数据在并发条件下,发生冲突是小概率事件,因此我们不加锁,而是加上版本号判断修改是否成功。
悲观锁认为,同一数据在并发条件下,冲突是大概率事件,因此我们必须先加锁,不允许别人修改。
悲观锁和乐观锁其实是一种思想,主要取决于开发者对待它的态度。在锁这一小节中,里面谈到的所有锁宏观上(可能实现的思想是乐观锁)来说都是悲观锁,因此一旦加锁,都会锁定数据,直到解锁才会释放。
3.1 乐观锁实施方案
乐观锁不全依赖于数据库,一般情况下我们都是在代码层面上来完成它的,主流的设计思路是这样的:
我们在数据表中添加一个字段version
,version 代表版本号,字段类型为整型。当我们获取数据时,假设得到它的version
字段为n
,执行完其它操作对该数据进行更新时,会执行UPDATE ... SET version=n+1 WHERE version=n
。
如果在更新时,数据已经被别人更新过了,那么该数据的version
字段已经不是n
了,那么此时修改就会失败,反之修改就会成功。
可以看到,乐观锁就像它的名称一样乐观,适合数据读多写少
的场景,因为实际上并没锁住数据,所以性能十分可观;而悲观锁则与之相反,适合写多读少的场景,盲目的排他性一定程度上会大幅影响性能。
4. 实践
4.1 乐观锁数据表
乐观锁的使用十分广泛,我们也推荐你在实际的开发中使用乐观锁,接下来,我们以一个例子来详细的说明一下乐观锁。
我们新建一个测试数据表 imooc_order :
DROP TABLE IF EXISTS imooc_order;
CREATE TABLE imooc_order
(
id int PRIMARY KEY,
price decimal(10,2),
-- version 字段作为乐观锁版本控制位
version int NOT NULL DEFAULT 0
);
INSERT INTO imooc_order(id,price,version)
VALUES (1,23.2,1);
注意: 我们已经在表中添加了 version 字段
4.2 乐观锁实例
imooc_order
表存放了订单信息(简略信息),而订单的价格并非一成不变的,它可能会同时被多个人改变。
那么如何能够安全地修改它的价格,且不会跟别人冲突了。
现在默认有两个人,甲
现在拿到了id
为1
的订单,想要修改它的价格:
SELECT * FROM imooc_order WHERE id = 1;
在甲
拿到的同时,乙
也同样拿到了订单数据,且订单此时的价格为23.2
,版本号为1
。
甲
决定修改订单的价格为33.3
,于是他执行了如下语句:
UPDATE imooc_order SET version = 1 + 1, price=33.3 WHERE id = 1 AND version = 1;
甲
执行成功了,而此时乙
也需要修改价格,但是他并不知道价格已经修改:
UPDATE imooc_order SET version = 1 + 1, price=22.1 WHERE id = 1 AND version = 1;
很明显,乙
修改失败了,因为在他修改价格之前,甲
以微弱的速度优势已经修改了价格,且修改了 version
字段,此时 version
等于2
。
而乙
提交 SQL 语句时,Where 中明确的写到 version 等于 1。即使乙修改失败,但是数据仍然是正确的,乙
完全可以在失败的情况下重复获取一次数据再修改。
如下图所示:
4.3 乐观锁总结
可以看到,乐观锁虽然有缺陷,它会使更新失败,因此必须重复获取数据然后重试,但是它保证了数据的正确性和完整性。在读多写少的场景下,乐观锁不会出现太多的重试,当然如果出现了很多重试,证明场景已经可能不是读多写少了,可以尝试换方案了。
乐观锁的实现也颇为简单,不需要任何第三方依赖,你完全可以自己直接实现,不过仍然有一些第三方框架提供了开箱即用的乐观锁,你可以根据自己的使用语言和生态去查找相应的乐观锁框架。
5. 小结
- 乐观锁和悲观锁同等重要,乐观锁是很多高并发场景下的基石。
- 大多数时候,程序使用的都是悲观锁,如常见的
自旋锁
。 - 乐观锁与悲观锁都是一种思路,熟悉并掌握该思路,任何面试都拦不到你。