用商品id粒度锁(concurrenthashmap缓存锁对象)+数据库行锁+where校验,确保库存扣减原子性,避免超卖与全局阻塞。

用 synchronized 保护商品库存扣减,核心是确保同一商品的多次扣减操作串行执行,避免超卖。关键不在于“锁整个方法”,而在于“锁住具体商品的唯一标识”,否则会引发严重性能瓶颈甚至全局阻塞。
锁粒度必须落到具体商品ID上
不能写 synchronized(this) 或锁类对象(如 synchronized(InventoryService.class)),否则所有商品库存操作互斥,系统瞬间变单线程。正确做法是为每个商品ID生成一个独立锁对象:
- 用
ConcurrentHashMap<string object></string>缓存商品ID对应的锁对象,例如:
private static final ConcurrentHashMapLOCKS = new ConcurrentHashMap(); - 获取锁时: Object lock = LOCKS.computeIfAbsent(productId, k -> new Object());
- 再对
lock加同步块:
synchronized(lock) { /* 扣减逻辑 */ }
扣减逻辑必须包含“查-判-减”原子校验
仅靠 synchronized 不足以防止超卖,必须在锁内完成“读取当前库存 → 判断是否足够 → 执行扣减 → 更新数据库”全过程,并加数据库层面校验:
- 先查库存(建议用
SELECT ... FOR UPDATE或乐观锁版本号字段) - 判断
stock >= requiredQty,不满足直接抛异常或返回失败 - 执行
UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?,检查affectedRows == 1 - 若更新失败(比如并发下库存被其他请求抢先扣完),说明校验失效,需重试或拒绝
注意锁对象生命周期与内存泄漏风险
商品ID锁对象长期驻留内存会导致 OOM,尤其促销期间大量临时SKU涌入:
- 不要用商品ID直接作为锁对象(
synchronized(productId)),字符串常量池不可控 - 使用
ConcurrentHashMap时,配合定时清理或弱引用缓存(如MapMaker.makeComputingMap()已废弃,可用Caffeine.newBuilder().weakValues().build()) - 更稳妥的做法:锁对象只在扣减请求生命周期内存在,用
computeIfAbsent+ 定期remove(例如 5 分钟无访问自动清除)
实际代码片段示意
以下为简化但可落地的核心逻辑:
public boolean deductStock(String productId, int quantity) {
Object lock = LOCKS.computeIfAbsent(productId, k -> new Object());
synchronized (lock) {
// 1. 查询当前库存(带行锁)
Product product = productMapper.selectForUpdateById(productId);
if (product == null || product.getStock() Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











