mysql中select+update并发的更新问题
小知识补充
首先,我们要知道在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本书,那我们可以十分随意的写出这样的代码。
数据
无锁更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @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之间才生效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @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来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @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来解决并发问题也不错,甚至不用加事务,而且不会堵塞读取操作。
- 如果对读的响应度要求非常高,比如证券交易系统,那么适合用乐观锁,因为悲观锁会阻塞读
- 如果读远多于写,那么也适合用乐观锁,因为用悲观锁会导致大量读被少量的写阻塞
- 如果写操作频繁并且冲突比例很高,那么适合用悲观写独占锁
学习资料
简书
思否