三种基本锁,举例解决订单超卖问题

环境准备:复现超卖现象

1、环境基本准备:新建一个count表,写几条模拟数据,做一条库存为1的数据

 2、新建springboot项目,用mybatis-generator快速生成所需代码mybatis-generator插件实现代码自动生成_p&f°的博客-CSDN博客

3、自己在mapper层和xml文件中写一个扣减库存的方法

 //根据id找到对应商品,扣减库存
    int updateCountAfterBuyPro(@Param("id") Integer id, @Param("buyCount") Integer buyCount, @Param("updateTime") Date updateTime);

<update id="updateCountAfterBuyPro" parameterType="java.lang.Integer">
    update count
    set count = count - #{buyCount}
    where id = #{id, jdbcType=INTEGER}
</update>

4、写一个server类,模拟扣减库存,生成订单操作

/**
 * @auther xpf
 * @date 2022/6/2 10:40
 * @description 超卖现象解决
 */

@Service
public class OrderServer {

    @Resource
    private CountMapper countMapper;

//    @Transactional(rollbackFor = Exception.class)
    public void updateCount(int id, int buyCount){

        //1、扣库存
        Count selectProduct = countMapper.selectByPrimaryKey(id);
        if (selectProduct == null){
            throw new RuntimeException("找不到对应的商品");
        }

        Integer productCount = selectProduct.getCount();
        System.out.println(Thread.currentThread().getName() + "获取的库存值是:" + productCount);
        if (productCount < buyCount){
            throw new RuntimeException("库存不足无法购买");
        }

        Date date = new Date();
        countMapper.updateCountAfterBuyPro(id, productCount, date);

        //2、生成订单快照(模拟)
        //3、生成订单详情表(模拟)
    }

}

5、写一个测试类,假设多人同时访问

@SpringBootTest
class DistributeLockApplicationTests {

    @Autowired
    private OrderServer orderServer;

    @Test
    void overCountTest() throws InterruptedException {
        CountDownLatch cdl = new CountDownLatch(5);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

        ExecutorService es = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            es.execute(()->{
                try {
                    //让线程同时到达
                    cyclicBarrier.await();
                    orderServer.updateCount(1, 1);
                    System.err.println(Thread.currentThread().getName() + "线程执行完毕");
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    cdl.countDown();
                }
            });
        }

        cdl.await();
        es.shutdown();
    }

}

6、运行程序,查看结果

可以看到,五个线程同时到达,同时获取到数据库id为1的商品的库存为1,然后都去更新库存

 然后从数据库看到库存变为-4,不符合日常逻辑,超卖现象就产生了!

一、基于 synchronized 的方法锁(最原始的锁)

1、用方法锁在 updateCount 加上一个 synchronized 关键字。

 2、将库存改为1,测试发现,当第一个线程获取到方法锁之后,其他线程只能等待线程释放锁,才能进入方法,可以解决超卖问题,数据库库存也是正常的0

题外:解决@Transactional加上事务后,还是会产生超卖问题

 3、但是,一般情况下,下单步骤,在第一步扣减完库存后,会生成订单快照和生成订单详情表,那么这个时候方法往往会加上 @Transactional 事务回滚机制。

 在加完事务回滚后,继续将库存改为1,测试发现,还是会产生超卖现象。

数据库库存字段为 -1

 分析输出结果:

找到问题,原来是线程1先获得方法锁,进入方法后,扣减库存,但是由于开启了事务,在线程1释放锁后,提交事务之前,库存还是为1。此时,线程4获得方法锁,读取到还未提交的库存数量为1。然后线程1提交完毕,库存 1-1 = 0。线程4继续执行,剩余库存 0 - 1 = -1,然后提交。其他线程因为线程1提交完就已经是0了,所以抛出异常。

4、解决方案。

分析可以发现,事务提交是在锁之外的,所以会出现这种情况。那么把事务也锁起来,在释放锁之前,确保事务已经提交即可!

所以需要使用手动控制事务,而不用spring自带的。将原来的

@Transactional(rollbackFor = Exception.class)注释掉,修改代码如下

@Service
public class OrderServer {

    @Resource
    private CountMapper countMapper;

    //平台事务管理器
    @Autowired
    private PlatformTransactionManager platformTransactionManager;

    //事务的定义
    @Autowired
    private TransactionDefinition transactionDefinition;

//    @Transactional(rollbackFor = Exception.class)
    public synchronized void updateCount(int id, int buyCount){

        TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);

        //1、扣库存
        Count selectProduct = countMapper.selectByPrimaryKey(id);
        if (selectProduct == null){
            platformTransactionManager.rollback(transaction);
            throw new RuntimeException("找不到对应的商品");
        }

        Integer productCount = selectProduct.getCount();
        System.out.println(Thread.currentThread().getName() + "获取的库存值是:" + productCount);
        if (productCount < buyCount){
            platformTransactionManager.rollback(transaction);
            throw new RuntimeException("库存不足无法购买");
        }

        Date date = new Date();
        countMapper.updateCountAfterBuyPro(id, productCount, date);

        //2、生成订单快照(模拟)
        //3、生成订单详情表(模拟)
        platformTransactionManager.commit(transaction);
    }

}

5、将数据库库存恢复为1,测试通过,数据库库存正常为0,结果如下。

 二、基于 synchronized 的块锁(最原始的锁)

对象锁:

synchronized (this){
    
}

或者

Object object = new Object();
 
synchronized (object){ 
    //业务代码 }

类锁:

//括号里写的是 本类.class
synchronized (OrderServer.class){

        }

对象锁和类锁的区别:类锁只可能有一个,而对象锁可能可以通过new创建多个,还是会产生并发执行情况。

使用快锁修改代码后结果:

@Service
public class OrderServer {

    @Resource
    private CountMapper countMapper;

    //平台事务管理器
    @Autowired
    private PlatformTransactionManager platformTransactionManager;

    //事务的定义
    @Autowired
    private TransactionDefinition transactionDefinition;

//    @Transactional(rollbackFor = Exception.class)
    public synchronized void updateCount(int id, int buyCount){

        synchronized (OrderServer.class){
            TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
            //1、扣库存
            Count selectProduct = countMapper.selectByPrimaryKey(id);
            if (selectProduct == null){
                platformTransactionManager.rollback(transaction);
                throw new RuntimeException("找不到对应的商品");
            }

            Integer productCount = selectProduct.getCount();
            System.out.println(Thread.currentThread().getName() + "获取的库存值是:" + productCount);
            if (productCount < buyCount){
                platformTransactionManager.rollback(transaction);
                throw new RuntimeException("库存不足无法购买");
            }

            Date date = new Date();
            countMapper.updateCountAfterBuyPro(id, productCount, date);

            //2、生成订单快照(模拟)
            //3、生成订单详情表(模拟)
            platformTransactionManager.commit(transaction);
        }
        
    }

}

执行结果符合预期。

三、使用可重入锁 ReentranLock (jdk1.5以后并发包中的锁)

setver 代码修改如下:

@Service
public class OrderServer {

    @Resource
    private CountMapper countMapper;

    //平台事务管理器
    @Autowired
    private PlatformTransactionManager platformTransactionManager;

    //事务的定义
    @Autowired
    private TransactionDefinition transactionDefinition;

//    @Transactional(rollbackFor = Exception.class)
    public synchronized void updateCount(int id, int buyCount){

        ReentrantLock lock = new ReentrantLock();

        lock.lock();
        try {
            TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
            //1、扣库存
            Count selectProduct = countMapper.selectByPrimaryKey(id);
            if (selectProduct == null){
                platformTransactionManager.rollback(transaction);
                throw new RuntimeException("找不到对应的商品");
            }

            Integer productCount = selectProduct.getCount();
            System.out.println(Thread.currentThread().getName() + "获取的库存值是:" + productCount);
            if (productCount < buyCount){
                platformTransactionManager.rollback(transaction);
                throw new RuntimeException("库存不足无法购买");
            }

            Date date = new Date();
            countMapper.updateCountAfterBuyPro(id, productCount, date);

            //2、生成订单快照(模拟)
            //3、生成订单详情表(模拟)
            platformTransactionManager.commit(transaction);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }

    }

}

 结果符合预期。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值