【每日鲜蘑】从数据库看乐观锁、悲观锁
乐观锁
和悲观锁
主要是用于解决并发问题的,而且是比较低级的并发问题。
场景
一般是多个用户对同一资源进行处理时,会出现并发问题。比如,一篇文章,用户进行了点赞操作。我们的业务处理一般如下:
步骤 | 操作 | 数据库语句举例 |
---|---|---|
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
次; - 控制好锁的范围,减小锁定对象的范围,比如使用行锁。
转载自:https://juejin.cn/post/6844904104410497031