当前位置: 首页 > news >正文

项目中如何防止超卖

什么是超卖?假如只剩下一个库存,却被多个订单买到了,简单理解就是库存不够了还能正常下单。


方案1:数据库行级锁

1. 实体类

@Data
@TableName("product")
public class Product {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String name;
    
    private Integer stock;
    
    @Version // MyBatis-Plus乐观锁注解
    private Integer version;
}

2. Mapper接口

public interface ProductMapper extends BaseMapper<Product> {
    /​**​
     * 使用悲观锁查询商品
     * @param id 商品ID
     * @return 商品实体
     */
    @Select("SELECT * FROM product WHERE id = #{id} FOR UPDATE")
    Product selectByIdWithLock(Long id);

    /​**​
     * 自定义乐观锁更新方法
     * @param product 商品实体
     * @param oldVersion 旧版本号
     * @return 更新影响行数
     */
    @Update("UPDATE product SET stock = #{stock}, version = version + 1 " +
            "WHERE id = #{id} AND version = #{oldVersion}")
    int updateWithOptimisticLock(Product product, @Param("oldVersion") int oldVersion);
}

3. Service层实现

@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductMapper productMapper;
    
    /​**​
     * 使用行级锁扣减库存
     * @param productId 商品ID
     * @param quantity 扣减数量
     * @return 操作结果
     */
    @Transactional(rollbackFor = Exception.class)
    public String reduceStock(Long productId, Integer quantity) {
        try {
            // 1. 加行级锁查询商品
            Product product = productMapper.selectByIdWithLock(productId);
            if (product == null) {
                return "商品不存在";
            }
            
            // 2. 检查库存是否充足
            if (product.getStock() < quantity) {
                return "库存不足";
            }
            
            // 3. 扣减库存
            product.setStock(product.getStock() - quantity);
            
            // 4. 更新库存
            int result = productMapper.updateById(product);
            if (result <= 0) {
                throw new RuntimeException("更新库存失败");
            }
            
            return "扣减库存成功";
        } catch (Exception e) {
            // 事务回滚
            throw new RuntimeException("扣减库存异常: " + e.getMessage());
        }
    }
    
    /​**​
     * 使用MyBatis-Plus内置乐观锁机制扣减库存
     * @param productId 商品ID
     * @param quantity 扣减数量
     * @return 操作结果
     */
    @Transactional(rollbackFor = Exception.class)
    public String reduceStockWithOptimisticLock(Long productId, Integer quantity) {
        // 1. 查询商品(不加锁)
        Product product = productMapper.selectById(productId);
        if (product == null) {
            return "商品不存在";
        }
        
        // 2. 检查库存
        if (product.getStock() < quantity) {
            return "库存不足";
        }
        
        // 3. 扣减库存
        product.setStock(product.getStock() - quantity);
        
        // 4. 乐观锁更新
        int result = productMapper.updateById(product);
        if (result <= 0) {
            // 版本号不一致,说明数据已被修改
            return "操作失败,请重试";
        }
        
        return "扣减库存成功";
    }

    /​**​
     * 手动实现乐观锁扣减库存(带重试机制)
     * @param productId 商品ID
     * @param quantity 扣减数量
     * @return 操作结果
     */
    @Transactional(rollbackFor = Exception.class)
    public String reduceStockManual(Long productId, int quantity) {
        // 1. 查询商品
        Product product = productMapper.selectById(productId);
        if (product == null) {
           return "商品不存在";
        }
            
        // 2. 检查库存是否充足
        if (product.getStock() < quantity) {
           return "库存不足";
        }
            
        // 3. 扣减库存
        product.setStock(product.getStock() - quantity);
            
        // 4. 手动乐观锁更新
        int result = productMapper.updateWithOptimisticLock(
        product, product.getVersion());
            
        if (result > 0) {
           return "扣减库存成功";
        }
            
        
        return "操作失败,请重试";
    }
}

方案2:分布式锁

1. 实体类

@Data
@TableName("product")
public class Product {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    private String name;
    
    private Integer stock;
}

2. Mapper接口

public interface ProductMapper extends BaseMapper<Product> {
    // 使用MyBatis-Plus自带方法
    
    /​**​
     * 自定义扣减库存方法
     * @param productId 商品ID
     * @param quantity 扣减数量
     * @return 影响行数
     */
    @Update("UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}")
    int reduceStock(@Param("productId") Long productId, @Param("quantity") int quantity);
}

3. Service层实现

@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductMapper productMapper;
    private final RedissonClient redissonClient;
    
    
    /​**​
     * Redisson分布式锁
     */
    public String reduceStockWithLock(Long productId, int quantity) {
        // 1. 创建分布式锁key
        String lockKey = "product:lock:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 2. 尝试获取锁(等待5秒,锁自动释放时间30秒)
            boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
            if (!locked) {
                return "系统繁忙,请稍后再试";
            }
            
            try {
                // 3. 查询商品
                Product product = productMapper.selectById(productId);
                if (product == null) {
                    return "商品不存在";
                }
                
                // 4. 检查库存是否充足
                if (product.getStock() < quantity) {
                    return "库存不足";
                }
                
                // 5. 扣减库存
                int result = productMapper.reduceStock(productId, quantity);
                if (result <= 0) {
                    return "扣减失败,请重试";
                }
                
                return "扣减成功";
            } finally {
                // 6. 释放锁
                // lock.isLocked()检查锁是否仍然被持有(未被释放)
                // lock.isHeldByCurrentThread()检查当前线程是否持有该锁
                if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "系统异常,请重试";
        }
    }
}

相关文章:

  • Redis高频面试题及深度解析(20大核心问题+场景化答案)
  • Python 序列构成的数组(list.sort方法和内置函数sorted)
  • PostgreSQL数据库重放攻击测试
  • 【大模型理论篇】SWIFT: 可扩展轻量级的大模型微调基础设施
  • [ctfshow web入门] web26
  • 通过发票四要素信息核验增值税发票真伪-iOS发票查验接口
  • 第12/100节:关键路径
  • HTTP GET 和 POST 请求有什么区别
  • spring-cloud-starter-alibaba-sentinel使用说明
  • linux--------------进程控制(下)
  • WPF如何修改三方控件库的样式
  • AudioRecord 录制pcm转wav
  • 每日一题(小白)数组娱乐篇17
  • 滑动窗口7:30. 串联所有单词的子串
  • 分布式数据库LSM树
  • 多模态大语言模型arxiv论文略读(七)
  • Unity-Xlua热更和AssetBundle详解
  • 上下拉电阻详解
  • RAG 系统中的偏差是什么?
  • 自定义数据结构的QVariant序列化 ASSERT failure in QVariant::save: “invalid type to save“
  • 万玲、胡春平调任江西省鹰潭市副市长
  • Meta正为AI眼镜开发人脸识别功能
  • 第1现场 | 50多年来首次!印度举行大规模民防演习
  • 上市不足一年,吉利汽车拟私有化极氪并合并:整合资源,杜绝重复投入
  • 动物只有在被认为对人类有用时,它们的建筑才会被特别设计
  • 中方对原产印度进口氯氰菊酯实施反倾销措施,商务部回应