美文网首页JAVA
高并发下库存问题,新增version字段来解决超卖

高并发下库存问题,新增version字段来解决超卖

作者: flyjar | 来源:发表于2022-07-20 17:33 被阅读0次

    在通过多线程来解决高并发的问题上,线程安全往往是最先需要考虑的问题,其次才是性能。库存超卖问题是有很多种技术解决方案的,比如悲观锁,分布式锁,乐观锁,队列串行化,Redis原子操作等。本篇通过MySQL乐观锁来演示基本实现。

    一、Goods和Order
    @Data
    public class Goods {
        private int id;
        private String name;
        private int stock;
        private int version;
    }
    
    @Data
    public class Order {
        private int id;
        private int uid;
        private int gid;
    }
    
    二、OrderDao和GoodsDao
    @Mapper
    public interface OrderDao {
    
        /**
         * 插入订单
         * 注意: 由于order是sql中的关键字,所以表名需要加上反引号
         * @param order
         * @return int
         */
        @Insert("INSERT INTO `order` (uid, gid) VALUES (#{uid}, #{gid})")
        @Options(useGeneratedKeys = true, keyProperty = "id")
        int insertOrder(Order order);
    }
    
    @Mapper
    public interface GoodsDao {
    
        /**
         * 查询商品库存
         * @param id 商品id
         * @return
         */
        @Select("SELECT * FROM goods WHERE id = #{id}")
        Goods getStock(@Param("id") int id);
    
        /**
         * 乐观锁方案扣减库存
         * @param id 商品id
         * @param version 版本号
         * @return
         */
        @Update("UPDATE goods SET stock = stock - 1, version = version + 1 WHERE id = #{id} AND stock > 0 AND version = #{version}")
        int decreaseStockForVersion(@Param("id") int id, @Param("version") int version);
    }
    
    三、GoodsService(重点)
    @Service
    @Slf4j
    public class GoodsService {
    
        @Autowired
        private GoodsDao goodsDao;
        @Autowired
        private OrderDao orderDao;
    
        /**
         * 扣减库存
         * @param gid 商品id
         * @param uid 用户id
         * @return SUCCESS 1 FAILURE 0
         */
        public int sellGoods(int gid, int uid) {
            int retryCount = 0;
            int update = 0;
            // 获取库存
            Goods goods = goodsDao.getStock(gid);
            if (goods.getStock() > 0) {
                // 乐观锁更新库存
                // 更新失败,说明其他线程已经修改过数据,本次扣减库存失败,可以重试一定次数或者返回
                // 最多重试3次
    
    
    //为什么要重试呢?
    //因为 Goods goods = goodsDao.getStock(gid);的时候同时执行的,查询出来的version都是1,库存都是10。但是去执行reduceStock() 的时候,只会有一个执行成功。但是的这个时候实际上还有库存,只是version变化了,导致扣减失败,所以这里尝试了多次进行扣减,直至成功或超出三次
    
                while(retryCount < 3 && update == 0){
                    update = this.reduceStock(gid);
                    retryCount++;
                }
                if(update == 0){
                    log.error("库存不足");
                    return 0;
                }
                // 库存扣减成功,生成订单
                Order order = new Order();
                order.setUid(uid);
                order.setGid(gid);
                int result = orderDao.insertOrder(order);
                return result;
            }
            // 失败返回
            return 0;
        }
    
    
        /**
         * 减库存
         *
         * 由于默认的事务隔离级别是可重复读,会导致在同一个事务中查询3次goodsDao.getStock()
         * 得到的数据始终是相同的,所以需要单独提取reduceStock方法。每次循环都启动新的事务尝试扣减库存操作。
         */
        @Transactional(rollbackFor = Exception.class)
        public  int  reduceStock(int gid){
            int result = 0;
            //1、查询商品库存
            Goods goods = goodsDao.getStock(gid);
            //2、判断库存是否充足
            if(goods.getStock() >0){
                //3、减库存
                // 乐观锁更新库存
                result = goodsDao.decreaseStockForVersion(gid, goods.getVersion());
            }
            return result;
        }
    }
    
    四、单元测试GoodsServiceTest
    @SpringBootTest
    class GoodsServiceTest {
    
        @Autowired
        GoodsService goodsService;
    
        @Test
        void seckill() throws InterruptedException {
    
            // 库存初始化为10,这里通过CountDownLatch和线程池模拟100个并发
            int threadTotal = 100;
    
            ExecutorService executorService = Executors.newCachedThreadPool();
    
            final CountDownLatch countDownLatch = new CountDownLatch(threadTotal);
            for (int i = 0; i < threadTotal ; i++) {
                int uid = i;
                executorService.execute(() -> {
                    try {
                        goodsService.sellGoods(1, uid);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                });
            }
    
            countDownLatch.await();
            executorService.shutdown();
    
        }
    }
    
    五、最终结果

    库存由10减到了0,并且生产了10条订单记录。

    相关文章

      网友评论

        本文标题:高并发下库存问题,新增version字段来解决超卖

        本文链接:https://www.haomeiwen.com/subject/aiqyirtx.html