【每日鲜蘑】从数据库看乐观锁、悲观锁
乐观锁和悲观锁主要是用于解决并发问题的,而且是比较低级的并发问题。
场景
一般是多个用户对同一资源进行处理时,会出现并发问题。比如,一篇文章,用户进行了点赞操作。我们的业务处理一般如下:
| 步骤 | 操作 | 数据库语句举例 |
|---|---|---|
| 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