Reentrantlock锁+事务Transaction的漏洞,正常超卖场景实战!

Reentrantlock锁+事务@Transaction

项目中遇到一个问题。对售出商品业务的代码加上该锁,保证不能超卖。
首先分析一下,保证多线程的并发安全,

1、引入锁Reentrantlock,2 、开启spring的事务管理,保证出现异常进行事务回滚。

这一个开发设计的代码在这里插入图片描述

代码业务流程

在这里插入图片描述

这段代码 库存 10个 执行结果 卖出 14个,直接血亏4个 没有货可就是欺诈消费者咱可担待不起!!!!

package com.szj.videoblog.articleservice.service.impl;

import com.szj.videoblog.articleservice.mapper.StockMapper;
import com.szj.videoblog.articleservice.mapper.StockOrderMapper;
import com.szj.videoblog.articleservice.model.entity.Stock;
import com.szj.videoblog.articleservice.model.entity.StockOrder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.concurrent.locks.ReentrantLock;

@Service
@Slf4j
public class StockServiceImpl {

    ReentrantLock lock = new ReentrantLock();

    @Resource
    private StockMapper stockMapper;

    @Resource
    private StockOrderMapper stockOrderMapper;

    @Transactional
    public void lock() {
        //上锁
        lock.lock();
        try {
            //查库存
            Stock stock = stockMapper.selectById(1L);
            if (stock.getStockNumber() > 0) {
                log.info("下单");
                //下单
                StockOrder stockOrder = new StockOrder();
                stockOrder.setName("平板");
                stockOrderMapper.insert(stockOrder);
                //减库存
                Stock stock1 = new Stock();
                stock1.setId(1L);
                stock1.setStockNumber(stock.getStockNumber() - 1);
                stockMapper.updateById(stock1);
            } else {
                //商品售空
                log.info("商品售空");
            }
        } finally {
            //解锁
            lock.unlock();
        }
    }
}

我贴一下我的订单表,这是血淋淋的超卖了4个!!是哪里出错了,我这代码写没错啊。锁也加了,事务也加了,为什么会超卖啊。我直接告诉你答案,spring的@Transaction 事务是在方法unlock之后提交的,总结方法结束后提交事务。所有必然是先解锁这里就出现了问题。

在这里插入图片描述

一、首先分析spring事务是什么时候开启,又什么时候结束。
根据spring的事务源码发现是在执行到第一个操作 InnoDB 表的语句,事务才算是真正启动,
很显然是先加锁lock.lock(); 执行查询库存触发第一个sql语句,此时任务开启。 
流程: 第一步,是先加锁 —> 执行sql开启spring事务;
二、那么spring事务什么时候结束呢。
在上面的示例代码的情况下那么事务的提交到底是在 unlock 之前还是之后呢?

根据我测试 100个线程执行下单发现超卖了,从而发现spring事务是在方法结束之后提交,

那么必然是先解锁-->>在提交spring事务

三、超卖的原因:

这里就有一个bug A线程解锁之后此时事务还没有被提交,B线程获取到锁查到库存,此时查的还是旧的库存数,执行下单流程导致超卖。

小明说:我还有点深信不疑,难道敲了一年的代码我才发现这个锁和事务存在这个bug,我不信,我要换锁,我要用redis的分布式锁试试。

Redisson 使用这个组件,简单粗暴 分布式锁。 very 好用。代码我就不贴了。
 <!--大家也可以单独引入Redisson依赖 ->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.6</version>
</dependency>

分布式锁的使用 案例自己把业务改一下

 public void testLockOne(){
        try {
            RLock lock = redissonClient.getLock("bravo1988_distributed_lock");
            log.info("testLockOne尝试加锁...");
            lock.lock();
            log.info("testLockOne加锁成功...");
            log.info("testLockOne业务开始...");
            TimeUnit.SECONDS.sleep(5);
            log.info("testLockOne业务结束...");
            lock.unlock();
            log.info("testLockOne解锁成功...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
经过我测试,一开始竟然被他蒙混过了,开启了100个线程 竟然没有超卖,说明情况很理想。没有出现获取锁之后,拿到就得库存值。
此时我就去问了分享这个bug的博主。博主让我在finally 里 执行unlock,在延迟一秒试试。
延迟1秒的原因:测试 先解锁 延迟1秒 判断 spring的事务是不是在方法执行结束之后提交的。

果然 :延迟1秒后,超卖了,那么又一次说明,spring事务的提交是在方法之后,所以此时A线程执行的sql 并没有被提交到mysql上,从而库存没有被减一。B线程在解锁后的1秒内 读取到的库存是旧值。此时就多卖了一个。

小明说:又多卖了一个,假一赔三,老铁,;老板需要这个场景打折10个苹果电脑,我却卖了14个,我还要补差价太难了。

解决方案

第一种 :手动提交事务。自己开启事务,自己在提交事务。

核心代码 在 解锁unlock之前提交事务。

第二种:既然是方法之后提交事务,

那么用A方法(负责上锁解锁) 调用B方法 (负责处理减库存操作)

测试流程:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

正好是个10个订单记录 ,此时代码用的锁依然是 Reentrantlock

在这里插入图片描述

在这里插入图片描述

最好补充一句 @Transaction 在同一个service中 多个方法声明 Transaction 会出现不生效,这点下次再分析,可以百度自己注意一下。

这里只讲了spring的事务 ,mysql事务隔离级别默认RR 不可重复读,当查询库存的保证了读取的已提交事务 库存数。(mvcc会生成一个事务id 有版本号,查询大于当前版本号)保证了重复查询库存数都不会改变,这里就是Mvcc知识点,学习了,大致理解 mvcc 解决了以前读写要上锁的问题,解决了并发阻塞问题。

还有今天测试,mysql inndb下 当时我在思考假设 A线程 B 线程事务同时修改一条数据,

mysql执行流程如下 此时出现并发,inndb引擎 支持行锁,这时候A修改一条记录,但是未提交,B也去修改这条记录的时候会发生阻塞,这就是所谓的行锁,(也叫做排它锁);只有当A提交了事务,B的 update语句才会被执行,B提交事务,最终结果是 最后一次事务的更新值。

这里分享下,mysql 阻塞时间默认是50s 这个可自行修改,如果B事务被则塞超过50s 此时B事务提交不会生效。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

正常情况,B会等到A提交了事务,执行update B提交事务,所有最后结果是B更新的值。

实践出真理。肝肝肝肝肝肝肝肝肝肝肝肝肝肝!!!

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值