电商系统中的Java:库存扣减方案选型与架构权衡
本文适合正在设计高并发库存系统的后端开发者,如果你只关注业务逻辑可以跳过代码部分直接看选型结论。前置知识要求:熟悉Redis和MySQL事务机制。
需求背景:代购系统的库存扣减为什么特殊
反向海淘场景下,一个爆款商品可能同时在1688、淘宝、拼多多多个平台销售。用户下单后,系统需要实时扣减库存,同时还要处理多平台采购、物流分仓、退货退款等复杂链路。库存扣减的原子性和性能,直接决定了订单的准确率和用户体验。
传统电商系统的库存扣减,核心就两个问题:**扣减操作的原子性**和**并发控制**。但在代购场景中,库存数据还涉及跨境采购的时效性——你扣了库存,1688那边可能已经没货了,这种“库存漂移”让问题更复杂。
方案对比:Redis Lua vs MySQL行锁
做库存扣减时,社区里最常见的两个方案是 **Redis Lua脚本**和 **MySQL行锁**。我们来对比一下它们在代购场景下的表现。
Redis Lua脚本方案
```php
// Redis Lua扣减库存脚本
$script = << local stock = redis.call('GET', KEYS[1]) if not stock then return -1 -- 商品不存在 end if tonumber(stock) < tonumber(ARGV[1]) then return -2 -- 库存不足 end redis.call('DECRBY', KEYS[1], ARGV[1]) return 1 -- 扣减成功 LUA; $result = Redis::eval($script, 1, 'stock:product_123', 1); ``` 这个方案的核心优势是**原子性**——Lua脚本在Redis中是串行执行的,不会有并发问题。性能也极好,单机Redis可以支撑每秒数万次扣减。 但代价也很明显:**数据持久化依赖Redis配置**,如果开启AOF,写入性能下降;如果不开,宕机丢数据。而且Lua脚本不支持复杂业务逻辑,比如扣减后需要同时更新订单状态,就得额外写代码。 ```php // MySQL行锁扣减库存 DB::beginTransaction(); try { $product = Product::where('id', $productId) ->lockForUpdate() ->first(); if ($product->stock < $quantity) { throw new \Exception('库存不足'); } $product->decrement('stock', $quantity); // 创建订单记录 Order::create([...]); DB::commit(); } catch (\Exception $e) { DB::rollBack(); throw $e; } ``` `lockForUpdate()` 加的是行级排他锁,同一时间只有一个事务能更新这条记录。优点是**事务完整**——库存扣减和订单创建在同一个事务里,要么都成功要么都失败。而且数据持久化有保障,MySQL的ACID特性在这里很稳。 但行锁的代价是**性能瓶颈**。高并发下,大量请求排队等待锁释放,吞吐量会急剧下降。实测在1000并发下,MySQL行锁方案的TPS大约只有Redis Lua方案的15% 左右。 这两个方案没有绝对的好坏,关键看你的业务场景。 但在代购系统中,实际情况往往介于两者之间。比如一个爆款商品,用户下单后需要同时扣库存、创建订单、触发采购任务、记录物流信息。如果用Redis Lua,订单和采购的原子性就没了;如果用MySQL行锁,并发一高就卡死。 实际工程中,代购系统往往采用 **Redis Lua + 消息队列补偿** 的混合方案: ```php // 先扣Redis库存(高性能) $result = Redis::eval($script, 1, 'stock:product_123', 1); if ($result === 1) { // 异步发送消息,创建订单和采购任务 RocketMQ::send('order_create', [ 'product_id' => 123, 'quantity' => 1, 'user_id' => 456, ]); } else { // 库存不足,返回错误 throw new \Exception('库存不足'); } ``` 这个方案的关键是**消息队列的可靠性**。如果消息发送失败,Redis库存已经被扣了,就会出现“幽灵库存”。所以需要引入**库存补偿机制**——定时任务扫描Redis中的库存数据,与MySQL中的实际订单数做对账,发现差异就自动修正。 在开源社区,**Apache RocketMQ 4.9.4**(GitHub 10k+ stars)的事务消息功能可以很好地解决这个问题。它保证本地事务和消息发送的原子性,不会出现扣了库存但没发消息的情况。 另一个值得关注的项目是 **Redisson 3.17.0**(GitHub 20k+ stars),它提供了基于Redis的分布式锁和信号量,可以替代手写Lua脚本,而且支持RedLock算法,在高可用场景下更可靠。 库存扣减没有银弹。Redis Lua快但不稳,MySQL行锁稳但不快。代购系统的实践表明,**混合方案 + 补偿机制**是当前最务实的选择。开源社区的工具链已经足够成熟,关键是根据你的业务场景做取舍。 如果你也在做类似系统,建议先从Redis Lua起步,配合RocketMQ事务消息和定时对账。等业务量上来后,再考虑引入Redisson的分布式锁做更精细的控制。 欢迎star和fork,一起来完善这个方案。 --- 做了十年电商后端,参与过 Taocarts 代购系统和 AuctionGIt 日本竞拍平台(60+拍卖网站统一对接)的开发。有问题欢迎交流。MySQL行锁方案
Trade-off:选型决策的边界条件
决策:混合方案与开源生态
总结