乐观锁
# 乐观锁概述
乐观锁一般需要借助几种东西:
- 时间戳
- version版本号
- CAS机制
CAS:Compare and Swap(比较并交换),假定有变量X,若等于旧值A,则可以变为新值B。
例子:修改密码(需要输入旧的密码,匹配成功才能进行修改)
# 代码实现乐观锁
在数据库表中增加一个version字段,每次更新,递增1,用于指定版本号信息:
代码执行逻辑:
select * from db_stock where product_code = '1001'
update db_stock set count = count-1, version = version+1 where id = 1 and version = version
2
3
改造业务层代码:
@Transactional
public void deduct() {
//1.查询库存信息
List<Stock> stocks = stockMapper.selectList(new QueryWrapper<Stock>().eq("product_code", "1001"));
//这里取第一个仓库库存来计算
Stock stock = stocks.get(0);
//2.判断库存是否充足
if (stock != null && stock.getCount() > 0) {
//3.扣减库存
stock.setCount(stock.getCount() - 1);
Integer version = stock.getVersion();
stock.setVersion(version + 1);
int update = stockMapper.update(stock, new UpdateWrapper<Stock>().eq("id", stock.getId()).eq("version", version));
if (update <= 0) {
//如果更新失败,重试
deduct();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在集群下进行压力测试,出现栈溢出问题:
原因是因为我们代码中使用的是递归调用,所以会导致栈溢出,除此之外,还会有数据库的连接超时异常。
解决方法:
我们可以在代码中加入睡眠时间:
if (update <= 0) {
//如果更新失败,重试
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
deduct();
}
2
3
4
5
6
7
8
9
我们还需要解决数据库连接超时异常问题,@Transactional
代表着我们开启了一个手动事务,就是说整一块代码逻辑就是一个事务,事务在执行MDL操作的时候,就会加锁,后续代码一直在重试,后续请求都会被阻塞,阻塞三十秒,就会报连接超时异常。
解决方法:
将@Transactional
注释掉,这样的话,虽然update操作也会锁定数据,但是这是一个悲观锁,锁的粒度就不一样了,执行失败的时候,会立刻把锁给释放掉,不会阻塞后续请求。
# 最终测试
经过调式,在压力测试下,没有错误请求:
数据库中的库存和版本号也是正确的:
# 乐观锁存在的问题
在高并发情况下,性能极低
在压力测试下,一开始并发量不高的时候,吞吐量还是比较高的,随着并发量的上升,吞吐量越来越低,因为高并发时,重试的次数会越来越多。
ABA问题 查询X值,X=A,更新时,需要判断X是否仍等于刚刚的A值。在这两个过程之间,可能会有外力将X改变为B,然后又改回为A,但是我们代码是不会感知到的。
读写分离情况下,导致乐观锁不可靠 读写分离一般为主从复制,在主库写数据,在从库读数据。主库新增的数据,会以二进制日志形式写入binlog中,从库不停的发送请求从主库拉取日志到自己的relay日志(中继日志)中,然后不断地在从库里也relay重演一次,做到同步数据的效果。而在这一过程中,需要进行几次的IU、IO和网络传输操作,会有一定的延迟,所以在查询从库数据的时候,我们查询的一直是旧的数据,会一直更新不超过。