1. 高并发库存扣减的核心挑战
在电商、票务等需要精确库存控制的系统中,高并发下的库存扣减一直是个令人头疼的问题。我经历过多次大促活动,亲眼目睹过因为库存不一致导致的严重事故——用户下单成功却发现无货可发,或者更糟,超卖导致无法履约。
1.1 分布式环境下的数据一致性困境
典型的库存系统架构中,Redis作为缓存层承担着抗高并发的重任,而数据库则是数据的最终存储地。这种架构带来了两个关键特性:
- 性能分层:Redis的QPS可以达到10万级别,而MySQL单机通常只能处理几千QPS
- 数据生命周期不同:Redis数据可能丢失(重启或过期),而数据库数据具有持久性
这种分层设计在带来性能优势的同时,也引入了数据一致性的复杂问题。特别是在库存扣减这个场景下,我们需要同时满足三个看似矛盾的需求:
- 原子性:扣减操作要么完全成功,要么完全失败
- 高性能:必须支撑瞬时高并发请求
- 准确性:绝不能出现超卖或少卖
1.2 典型故障场景分析
在实际生产环境中,我遇到过以下几种典型的故障模式:
-
Redis扣减成功但数据库失败:这是最常见的问题,可能由以下原因导致:
- 数据库死锁(特别是在热点商品场景)
- 数据库连接池耗尽
- 网络闪断
- 数据库主从切换
-
双重故障:更危险的情况是Redis扣减成功,数据库失败,而回滚Redis时也失败了。这种情况虽然罕见,但在大促期间确实发生过。
-
延迟不一致:当采用异步写库方案时,Redis和数据库之间存在秒级延迟,这段时间内的数据查询可能出现不一致。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 同步回滚方案详解
2.1 基础实现原理
同步回滚是最直观的解决方案,其核心思想是:在同一个请求线程中,如果数据库操作失败,立即回滚Redis中的预扣减。这种方案适合大多数中小型电商场景。
关键实现要点:
- Redis预扣减的原子性:必须使用Lua脚本保证"检查+扣减"的原子性
- 数据库操作的乐观锁:通过WHERE条件防止超卖
- 异常捕获与回滚:在catch块中执行Redis回滚操作
2.2 完整代码实现与解析
以下是经过生产验证的PHP实现代码,我对其中的关键点做了详细注释:
php复制<?php
class InventoryService {
private $redis;
private $db;
public function __construct($redis, $db) {
$this->redis = $redis;
$this->db = $db;
}
/**
* 扣减库存(同步回滚方案)
* @param int $productId 商品ID
* @param int $quantity 扣减数量
* @throws Exception
*/
public function deductStock($productId, $quantity) {
$redisKey = "inventory:{$productId}";
// 1. Redis原子扣减(Lua脚本保证原子性)
$lua = <<<LUA
local current = tonumber(redis.call('GET', KEYS[1])) or 0
if current < tonumber(ARGV[1]) then
return {err = "INSUFFICIENT_STOCK"}
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return {ok = curre
