MySQL中Select+Update并发的更新问题

小知识补充

首先,我们要知道在mysql中update操作都是线程安全的,mysql引擎会update的行加上***排他锁***,其他对该行的update操作需要等到第一个update操作提交成功或者回滚,才能获取这个***排他锁***,从而对该行进行操作。

例子表结构

MySQL中Select+Update并发的更新问题

小知识点:表必备三字段:id, create_time, update_time。
说明:其中id 必为主键,类型为bigint unsigned、单表时自增、步长为 1。create_time, update_time 的类型均为 datetime 类型。 (来自《阿里巴巴Java开发手册(华山版)》)

代码环境

场景演示

现在假设我们要写一个买书的代码(这里为了简单就一次卖一本啦),并使用线程池模拟并发开启30个线程去买20本书,那我们可以十分随意的写出这样的代码。

数据

MySQL中Select+Update并发的更新问题

无锁更新

    @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;
    }
结果

MySQL中Select+Update并发的更新问题

结果很明显超卖啦,虽然我们有使用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;
    }
结果

MySQL中Select+Update并发的更新问题

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;
    }
结果

MySQL中Select+Update并发的更新问题

MySQL中Select+Update并发的更新问题

我们可以看到在尝试3次后,任然有22个线程尝试失败,这是因为并发太过激烈的原因。那么什么时候使用CAS操作呢?

阿里巴巴的开发规范上提到,在并发不高的情况下(尝试失败率不超过20%的情况下),推荐用CAS更新。

总结

  • 使用独占锁来解决并发问题是不错,但我觉得不太常用。
  • 使用CAS来解决并发问题也不错,甚至不用加事务,而且不会堵塞读取操作。
  1. 如果对读的响应度要求非常高,比如证券交易系统,那么适合用乐观锁,因为悲观锁会阻塞读
  2. 如果读远多于写,那么也适合用乐观锁,因为用悲观锁会导致大量读被少量的写阻塞
  3. 如果写操作频繁并且冲突比例很高,那么适合用悲观写独占锁

学习资料

简书

思否

上一篇:04-MyBatisPlus条件构造器


下一篇:JQuery日记 6.3 JQuery遍历模块