锁 + 事务还是超卖了?

        前两天看了大佬 why 的一篇博客 几行烂代码,我赔了16万。 感觉挺有意思的,直接把spring里面的Transaction直接撕开了一道口子,干货满满,受益匪浅!

   这个问题的起源是Segmentfault上面的一个问题,描述是这样的。

这个问题,ReentrantLock + Transaction 结果还是超卖了!

看到这个问题的一瞬间,我首先想到的是,哦,现在都玩分布式了,你还用ReentrantLock?活该你超卖!

使用分布式锁不就好了?安全又可靠!

第二个问题就是,你这里加了事务,哪怕ReentrantLock不起作用,MySQL里面的innerDB引擎还有默认的行锁啊,直接操作扣除商品数量,咋还超卖了呢?

看了下具体的代码实现,然后:

先看下我的想法:

 @Override
    @Transactional(rollbackFor = Exception.class)
    public Integer saleGoods(Integer num) {
        lock.lock();
        try {
            // num 是扣减数量,假设就一条数据
            //修改库存
            Integer flag = goodsMapper.updateGoodsNum(num);
            // 插入数据
            if (flag > 0 && num >= 0){   
             orderMapper.insert(Order.builder().name("保温杯")
                            .num(num)
                            .build()
                    );
                
             return num;
            }
        } finally {
            lock.unlock();
        }
        return null;
    }

 修改操作updateGoodsNum:

    @Update("update goods set count = count-#{num} where id = 1 and count >= #{num}")
    Integer updateGoodsNum(Integer num);

这样哪怕锁失效,也不会出现超卖啊,毕竟行锁在那兜着呢!这边事务没提交,想修改这条数据,没戏!

然而,现实却是这样的:

 Goods goods = goodsMapper.selectByPrimaryKey(1);
            Integer count = goods.getCount();
            if (count > 0 && (count - num >0)){
                goods.setCount(count - num);
                goodsMapper.updateByPrimaryKeySelective(goods);
                
                orderMapper.insert(Order.builder().name("保温杯")
                            .num(num)
                            .build()
                    );
                return num;

好家伙!先把数量加载到JVM内存,修改设置,然后再去修改!这就相当于在应用程序中缓存了一份数据,这个时候如果缓存数据和数据库不一致,那肯定...凉凉啊!

我们知道,spring事务分为声明式事务和编程式事务,一般情况下,我们都是直接加个@Transactional注解,然后就开启事务了。

那事务什么时候开始什么时候结束呢?why大佬分析的很明确了,源码就不在这里扯了。

如果不开启事务,那么每个SQLSession都会重新获取一个连接,然后作为一个事务进行处理,然后直接提交。

事务开启之后,自动提交自然是关闭了,声明式事务中,当调用数据库连接的时候,事务开启,进行数据操作。

那么什么时候关闭呢?当然是方法执行完毕啊!AOP么,进入方法之前,自动提交其实就关闭了,然后离开方法之后,事务结束,确定是否commit!

那么在 lock.unlock(); 之后,就有的操作喽,锁就失效了呗,然后事务没提交,goods的库存数据还缓存在应用程序的缓存中,然后新的线程重新去获取库存,并发问题不就来了。

画个图看看:

 蓝色圈住的那段时间,当前事务的库存修改还没有提交,再有事务进来,查询的库存肯定是之前未修改的库存,再去扣减,肯定是有问题了。

接着看呗,上面说到了使用ReentrantLock!分布式环境,用啥ReentrantLock啊,分布式锁他不香么?

改!用分布式锁!

/**
 * desc: 简单定义个redis配置
 */
@Configuration
public class MyRedissonConfig {

    //注册RedissonClient对象
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws Exception {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}



    @Override
    @Transactional(rollbackFor = Exception.class)
    public Integer saleGoods(Integer num) {
        RLock lock = redissonClient.getLock("myGooodsLock");
        lock.lock();
        try {
            // num 是扣减数量,假设就一条数据
            //修改库存
            Goods goods = goodsMapper.selectByPrimaryKey(1);
            Integer count = goods.getCount();
            System.out.println("剩余数量"+count);
            if (count > 0 && (count - num >= 0)) {
                goods.setCount(count - num);
                goodsMapper.updateByPrimaryKeySelective(goods);
                orderMapper.insert(Order.builder().name("保温杯")
                        .num(num)
                        .build()
                );
                System.out.println("出售保温杯"+ num + "个");
            } else {
                System.out.println("库存不足");
            }
        } finally {
            lock.unlock();
        }
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }


改成分布式锁又怎样?逻辑图还不是和之前一样?当然,分布式锁的话上锁解锁肯定是没有ReentrantLock快的,所以并发就没有那么尖锐,加个睡眠时间处理,问题依然是可以重现!

所以么,刨根问底就很有必要了,你看这个,幂等性方法没用,数据加载缓存;事务+锁 位置错乱;真是完美避开了行锁,ReentrantLock锁以及事务的处理。如果这里使用声明式事务,那还能有啥子问题?

 

@Override
    public Integer saleGoods(Integer num) {
        lock.lock();
        try{
            transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    //修改库存
                    Goods goods = goodsMapper.selectByPrimaryKey(1);
                    Integer count = goods.getCount();
                    System.out.println("剩余数量"+count);
                    if (count > 0 && (count - num >= 0)) {
                        goods.setCount(count - num);
                        goodsMapper.updateByPrimaryKeySelective(goods);
                        orderMapper.insert(Order.builder().name("保温杯")
                                .num(num)
                                .build()
                        );
                        System.out.println("出售保温杯"+ num + "个");
                    } else {
                        System.out.println("库存不足");
                    }
                }
            });
        } finally {
            lock.unlock();
        }
        return null;
    }

把事务包裹在 锁的中间就行了呗,事务提交了,锁才解锁,事务的并发问题?独占锁已经帮忙解决了!当然,如果是分布式部署的话,最好还是使用分布式锁处理。

当然,编程式事务不想使用的话,就重新抽出一个方法呗,当然,要注意下,如果在同一个方法中,小心AOP代理不生效,容器注入,方法调用,还是要牢记滴。

来碗鸡汤吧:

勤学似春起之苗,不见其增,日有所长;辍学如磨刀之石,不见其损,日有所亏。      ---陶渊明

no sacrifice,no victory~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笔下天地宽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值