实战3:如何使用乐观锁

1. 前言

一节中,我们从粒度管理两个角度来阐述了锁。如果你还不熟悉锁,请先阅读该小节,再来进行本小节的学习。

本小节我们将继续深挖,以开发者和实战的角度来谈锁。

2. 为什么需要锁

2.1 什么是数据竞争

在本节的开头,我们来谈一谈为什么开发程序需要使用锁?如果你有一点并发编程的基础,又或者对多线程有一点熟悉,那么你肯定知道答案,那就是数据竞争

2.2 数据竞争实例

我们举一个生活的例子。在中学的时候,教室的前面会放上一块小黑板,小黑板上会记载某一天上交作业或谁谁谁打扫卫生之类的。那块小黑板是所有同学都可使用的,只要你有事情要公布,就可以写在上面。

那么问题来了,假设同学A用小黑板写上明天要上交的作业,此时同学B也需要写上明天值日的同学,对于AB来说,他们之间存在竞争关系,而小黑板就是竞争点。

直观上来说,如果AB早到,那么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表存放了订单信息(简略信息),而订单的价格并非一成不变的,它可能会同时被多个人改变。

那么如何能够安全地修改它的价格,且不会跟别人冲突了。

现在默认有两个人,现在拿到了id1的订单,想要修改它的价格:

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. 小结

  • 乐观锁和悲观锁同等重要,乐观锁是很多高并发场景下的基石。
  • 大多数时候,程序使用的都是悲观锁,如常见的自旋锁
  • 乐观锁与悲观锁都是一种思路,熟悉并掌握该思路,任何面试都拦不到你。