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

8,342 阅读3分钟

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

场景

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

步骤操作数据库语句举例
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结束事务

总结

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

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