Java并发扣库存案例怎么实现

wen java案例 28

Java并发扣库存案例实现:从基础到企业级解决方案

文章导读目录

  • 为什么并发扣库存是Java开发者必会的硬技能?

    Java并发扣库存案例怎么实现

  • 基础实现:单机版synchronized与Lock方案

  • 进阶方案:Redis + Lua脚本保证原子性

  • 分布式场景:数据库乐观锁与悲观锁抉择

  • 高性能方案:分段锁与库存预扣机制

  • 常见问题FAQ(含面试高频问题)


为什么并发扣库存是Java开发者必会的硬技能?

在电商秒杀、票务系统、库存管理等场景中,“超卖”是致命问题,假设一个商品库存只有100件,若1000个用户同时发起购买请求,如何保证最终扣减成功且库存不为负数?这就是并发扣库存需要解决的核心矛盾。

案例场景:某电商平台iPhone促销,库存50台,瞬间涌入5000请求,每个请求扣减1台库存。

核心挑战

  • 原子性:扣减操作(读取→判断→更新)必须不可中断
  • 一致性:多个线程/服务间的库存数据必须同步
  • 性能:高并发下不能把系统锁死

基础实现:单机版synchronized与Lock方案

1 使用synchronized关键字

public class LocalInventoryService {
    private int stock = 100;
    public synchronized boolean deductStock() {
        if (stock <= 0) return false;
        stock--;
        return true;
    }
}

缺点:synchronized是JVM级别的重量级锁,高并发下线程排队效率低。

2 使用ReentrantLock

private final Lock lock = new ReentrantLock();
public boolean deductStock() {
    lock.lock();
    try {
        if (stock <= 0) return false;
        stock--;
        return true;
    } finally {
        lock.unlock();
    }
}

优势:可中断、可尝试获取锁,比synchronized灵活
局限性:仅适用于单进程应用,分布式场景失效


进阶方案:Redis + Lua脚本保证原子性

Redis的Lua脚本执行是原子性的,可有效解决分布式高并发扣库存问题。

1 核心脚本(lua/stock.lua)

local key = KEYS[1]          -- 库存key
local quantity = tonumber(ARGV[1])  -- 扣减数量
local current = redis.call('get', key)
if not current or tonumber(current) < quantity then
    return 0  -- 库存不足
end
redis.call('decrby', key, quantity)
return 1

2 Java调用代码

public class RedisInventoryService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    public boolean deductStock(Long goodsId, Integer quantity) {
        String luaScript = Files.readString(Paths.get("lua/stock.lua"));
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = redisTemplate.execute(script, 
            List.of("stock:" + goodsId), 
            quantity.toString());
        return result != null && result == 1;
    }
}

为什么用Lua?

  • 单个Lua脚本在Redis中是原子执行,等同于事务
  • 避免了多次网络请求(get→check→decr)的竞态条件
  • 性能极高,单机可达数万QPS

分布式场景:数据库乐观锁与悲观锁抉择

1 数据库悲观锁(SELECT ... FOR UPDATE)

BEGIN;
SELECT stock FROM product WHERE id=? FOR UPDATE;
-- 业务判断stock>=1
UPDATE product SET stock=stock-1 WHERE id=?;
COMMIT;

优点:绝对安全,数据库锁级别保证
缺点:行锁持有时间长,并发下极易死锁,压测1000并发时数据库CPU飙升

2 数据库乐观锁(CAS思路)

UPDATE product SET stock = stock - 1 
WHERE id = ? AND stock >= 1;

关键:利用数据库行锁自带的原子操作,stock=stock-1本身是原子减法
配套检查:stock >= 1 确保不出现负数
效率:远高于悲观锁,适合秒杀场景,但需配合重试机制

3 企业级最优实践(混合方案)

  • 前端用Redis Lua拦截80%请求(快速过滤库存不足)
  • 数据库用乐观锁做最终一致性确认
  • 少量请求可能发生“扣Redis成功但数据库失败”,通过补偿任务回滚Redis

高性能方案:分段锁与库存预扣机制

当单商品库存过大(如100万件),单一Redis key会成为热点,可以引入库存分片思想。

1 分段哈希设计

// 库存分10段,每段10万件
public class SegmentInventoryService {
    private static final int SEGMENT_COUNT = 10;
    public boolean deductStock(Long goodsId, int quantity) {
        int segmentId = ThreadLocalRandom.current().nextInt(SEGMENT_COUNT);
        String segmentKey = "stock:" + goodsId + ":segment:" + segmentId;
        // 用Lua脚本扣减该段库存,若不足则循环尝试其他段
    }
}

效果:分散了单key压力,Redis热点从1个变成10个
注意:需配合“库存余量汇总”监控,避免前端展示不准确

2 预扣+异步结算模式

  1. 预扣:秒杀成功时先在Redis中扣减并锁定30分钟
  2. 结算:支付成功则同步数据库并释放预扣
  3. 回滚:超时未支付则自动归还Redis库存

优点:用户体验极佳(秒杀秒出结果),数据库毫秒级写入


常见问题FAQ

Q1:synchronized和ReentrantLock哪个更适合高并发扣库存?
A:两者单机下都可,但ReentrantLock支持尝试锁定(tryLock),避免线程无限等待,实际生产单机场景已较少见,更多转向分布式方案。

Q2:Redis扣库存一定会比数据库快吗?
A:Redis纯内存+单线程模型,纯扣减操作可达5万QPS,但若需“扣库存+写订单”的事务,数据库的ACID特性不可替代,正确做法是:Redis做高性能预扣,DB做最终数据落盘。

Q3:如何测试并发扣库存是否安全?
A:使用Jmeter或Locust进行1000并发请求,验证最终库存是否为0,更专业的做法:写单元测试,用CountDownLatch模拟500线程同时扣减,断言库存最终值=初始值-批量总数。

Q4:如果Redis挂了,怎么办?
A:建议降级方案:1) Redis集群(哨兵/Cluster)解决单点;2) 降级到数据库乐观锁兜底;3) 本地内存缓存+热点检测,防止缓存雪崩。

Q5:用户连续点击(多次扣库存)如何处理?
A:前端做按钮防重复(1秒内禁止重发),后端基于用户ID+商品ID做幂等校验(如用Redis setnx,30秒内唯一key)。


一套可落地的架构参考

用户请求 → 限流层(Nginx+Lua限流)→ Redis Lua扣库存(高性能拦截) 
          ↓ 成功                     ↓ 失败
      订单写入MQ              返回“库存不足”
          ↓ 
      数据库乐观锁最终扣减
          ↓ 
      支付成功 → 释放预扣锁
      支付失败 → 回滚库存

关键点

  • 不使用数据库悲观锁(秒杀场景不要用FOR UPDATE
  • Redis Lua脚本必须幂等(库存不足返回0而非抛异常)
  • 补偿机制:如订单超时、支付失败,都需设计库存回退

通过本文的案例实现,你将能够应对从单体应用到分布式系统的并发扣库存场景,建议在生产环境中先进行压测,确保性能与一致性达到平衡。

抱歉,评论功能暂时关闭!