小知识补充
首先,我们要知道在mysql中update操作都是线程安全的,mysql引擎会update的行加上***排他锁***,其他对该行的update操作需要等到第一个update操作提交成功或者回滚,才能获取这个***排他锁***,从而对该行进行操作。
例子表结构
小知识点:表必备三字段:id, create_time, update_time。
说明:其中id 必为主键,类型为bigint unsigned、单表时自增、步长为 1。create_time, update_time 的类型均为 datetime 类型。 (来自《阿里巴巴Java开发手册(华山版)》)
代码环境
- jdk1.8
- idea
- SpringBoot
- Mybatis Plus
场景演示
现在假设我们要写一个买书的代码(这里为了简单就一次卖一本啦),并使用线程池模拟并发开启30个线程去买20本书,那我们可以十分随意的写出这样的代码。
数据
无锁更新
@Override
public int decrGoodsAmount(long id) {
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("id",id);
queryWrapper.last("for update");
Goods goods = baseMapper.selectOne(queryWrapper);
if (goods.getAmount()>0){
UpdateWrapper updateWrapper = new UpdateWrapper();
updateWrapper.setSql("amount=amount-1");
updateWrapper.eq("id",id);
return baseMapper.update(null,updateWrapper);
}
return 0;
}
结果
结果很明显超卖啦,虽然我们有使用if去判断库存,但是在并发情况下,你***观察到的情况可能已经被改变啦***。
写独占锁更新
我们首先来了解一下 for update语法:
for update是在数据库中上锁用的,可以为数据库中的行上一个排它锁。当一个事务的操作未完成时候,其他事务可以读取但是不能写入或更新。InnoDB默认是行级别的锁,当有明确指定的主键时候,是行级锁。否则是表级别。
小知识:for update 仅适用于InnoDB,并且必须开启事务,在begin与commit之间才生效。
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
public int decrGoodsAmountByLock(long id) {
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("id",id);
queryWrapper.last("for update");
Goods goods = baseMapper.selectOne(queryWrapper);
if (goods.getAmount()>0){
UpdateWrapper updateWrapper = new UpdateWrapper();
updateWrapper.setSql("amount=amount-1");
updateWrapper.eq("id",id);
return baseMapper.update(null,updateWrapper);
}
return 0;
}
结果
CAS更新
除了使用独占锁或者说是悲观锁来控制数据并发安全,还有什么方法呢?我们还可以使用乐观锁CAS来实现。
@Override
public int decrGoodsAmountByCAS(long id) {
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("id",id);
int flag = 0,num = 0;
while (flag==0) {
if (++num==3){
return 0;
}
Goods goods = baseMapper.selectOne(queryWrapper);
if (goods.getAmount() > 0) {
UpdateWrapper updateWrapper = new UpdateWrapper();
updateWrapper.setSql("amount=amount-1,update_time=CURRENT_TIMESTAMP()");
updateWrapper.eq("id", id);
updateWrapper.eq("amount",goods.getAmount());
flag = baseMapper.update(null, updateWrapper);
}else{
return 0;
}
}
return 1;
}
结果
我们可以看到在尝试3次后,任然有22个线程尝试失败,这是因为并发太过激烈的原因。那么什么时候使用CAS操作呢?
阿里巴巴的开发规范上提到,在并发不高的情况下(尝试失败率不超过20%的情况下),推荐用CAS更新。
总结
- 使用独占锁来解决并发问题是不错,但我觉得不太常用。
- 使用CAS来解决并发问题也不错,甚至不用加事务,而且不会堵塞读取操作。
- 如果对读的响应度要求非常高,比如证券交易系统,那么适合用乐观锁,因为悲观锁会阻塞读
- 如果读远多于写,那么也适合用乐观锁,因为用悲观锁会导致大量读被少量的写阻塞
- 如果写操作频繁并且冲突比例很高,那么适合用悲观写独占锁