MySQL乐观锁解决库存超卖问题

在通过多线程来解决高并发的问题上,线程安全往往是最先需要考虑的问题,其次才是性能。库存超卖问题是有很多种技术解决方案的,比如悲观锁,分布式锁,乐观锁,队列串行化,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次
            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条订单记录。
MySQL乐观锁解决库存超卖问题_第1张图片
MySQL乐观锁解决库存超卖问题_第2张图片

参考资料
更新库存数量 - 乐观锁
通过乐观锁解决库存超卖的问题
mysql 乐观锁_使用MySQL乐观锁解决超卖问题
商品超买超卖问题分析及实战

你可能感兴趣的:(Mybatis-Plus,spring,boot,事务,乐观锁)