Press "Enter" to skip to content

【每日鲜蘑】从数据库看乐观锁、悲观锁

乐观锁和悲观锁主要是用于解决并发问题的,而且是比较低级的并发问题。

场景

一般是多个用户对同一资源进行处理时,会出现并发问题。比如,一篇文章,用户进行了点赞操作。我们的业务处理一般如下:

步骤
操作
数据库语句举例

1
查询这篇文章
select id, praise_points from t_article where id = 1

2
将用户ID和文章ID的点赞关系写入点赞关系表中
insert into t_praise_link (id, user_id, article_id) values (…)

其他线程
更新了文章的点赞数
……

3
更新文章的点赞数
update t_article set praise_points = ? where id = 1

此时是不加锁的,在高并发时,会出现文章表记录的点赞数比实际点赞数少的情况。下面我们使用加锁的方式来解决这个并发问题。

悲观锁

总假设最坏的情况,所以每次拿【select】时总是上锁,不允许其他线程修改。数据库中的行锁、表锁、共享锁、排他锁、Java 中的synchronized都属于悲观锁的范畴。

数据库加锁的实现方式

锁类型
实现举例

共享锁
select id, praise_points from t_article where id = 1 lock in share mode

排他锁
select id, praise_points from t_article where id = 1 for update

排他锁
innoDB 引擎下,update,insert,delete 默认自动加了排他锁

应用悲观锁

首先分析应该用共享锁(允许其它事务也增加共享锁读取,但不允许其它事务修改或者加入排他锁)还是排他锁,这很重要。首先,我们看此时的业务场景,我们锁定的数据和我们修改的数据都是文章表,此时使用共享锁就不合适了,容易出现死锁。原因是:共享锁,事务都加,都能读。修改是惟一的,必须等待前一个事务commit,才可。

步骤
操作
数据库语句举例

begin
开始事务

1
查询这篇文章(加排他锁)
select id, praise_points from t_article where id = 1 for update

2
将用户ID和文章ID的点赞关系写入点赞关系表中
insert into t_praise_link (id, user_id, article_id) values (…)

3
更新文章的点赞数
update t_article set praise_points = ? where id = 1

end
结束事务

乐观锁

在更新的时候才会去判断一下别人有没有去更新这个数据。

应用乐观锁

一般会使用版本号机制或CAS算法(潜在ABA问题)实现。最常用的是版本号机制,主要是实现起来比较简单,常用的ORM都有完善的实现机制。

步骤
操作
数据库语句举例

begin
开始事务

1
查询这篇文章(加排他锁)
select id, praise_points, version from t_article where id = 1

2
将用户ID和文章ID的点赞关系写入点赞关系表中
insert into t_praise_link (id, user_id, article_id) values (…)

3
更新文章的点赞数
update t_article set praise_points = ? where id = 1 and version = 1

end
结束事务

总结

本文基于数据库层面简单介绍了乐观锁和悲观锁的概念,但在开发生活中,锁的种类是非常多的,比如偏向锁、轻量级锁、重量级锁、间隙锁等等,针对不同的并发问题,其实解决方法都是不一样的,但还是有一些巨人们积累的经验可供借鉴。

悲观锁适合写多读少的场景;
乐观锁适合写少读多的场景;
阿里巴巴的建议:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次 数不得小于 3 次;
控制好锁的范围,减小锁定对象的范围,比如使用行锁。

本文使用 mdnice 排版