1. 高并发场景下的库存扣减难题
在电商、秒杀等高并发业务场景中,库存管理一直是系统设计的核心痛点。我经历过一个典型的618大促案例:某商品库存5000件,活动开始后3秒内涌入10万请求,结果数据库显示超卖200多件。事后排查发现,Redis库存扣减成功但数据库更新失败,导致数据不一致。
这种"Redis扣减成功+数据库扣减失败"的异常场景,本质上源于分布式系统的不确定性。Redis作为内存数据库,操作速度是MySQL的10-100倍,在高并发下两者处理速度差异会被放大。当数据库因网络抖动、死锁或连接池耗尽导致操作失败时,Redis已完成的扣减就成为了"脏数据"。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术方案选型与对比
2.1 常规解决方案的局限性
最常见的处理方式是使用数据库事务,但在分布式环境下存在明显缺陷:
php复制// 典型的事务写法(不适用于跨存储引擎)
$db->beginTransaction();
try {
$db->query("UPDATE stock SET count=count-1 WHERE product_id=123");
$redis->decr("stock:123");
$db->commit();
} catch (Exception $e) {
$db->rollBack();
// Redis操作无法回滚!
}
这种方案的问题在于:
- Redis不支持跨存储引擎的事务
- 高并发下数据库事务成功率会急剧下降
- 网络分区时可能出现部分成功
2.2 可靠方案设计原则
经过多个项目的实践验证,我认为可靠的解决方案需要满足:
- 最终一致性:允许短暂不一致,但要保证最终正确
- 操作幂等:重复操作不会产生副作用
- 可补偿:失败操作要有修复机制
- 性能优先:99%的请求应在Redis层处理完毕
3. 完整实现方案与代码解析
3.1 预扣减+异步确认模式
这是目前最成熟的解决方案,核心流程如下:
- 预扣减阶段:
php复制// 使用Redis Lua脚本保证原子性
$lua = <<<LUA
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock > 0 then
redis.call('DECR', KEYS[1])
return 1
end
return 0
LUA;
$result = $redis->eval($lua, ["stock:123"], 1);
if (!$result) {
throw new Exception("库存不足");
}
// 记录预扣减日志
$logId = uniqid();
$redis->hMSet("stock:log:$logId", [
"product_id" => 123,
"status" => "pre_deduct",
"create_time" => time()
]);
- 异步确认阶段:
php复制// 后台任务处理数据库扣减
function confirmStockDeduction($logId) {
$log = $redis->hGetAll("stock:log:$logId");
if ($log['status'] != 'pre_deduct') return;
try {
$db->query("UPDATE stock SET count=count-1 WHERE product_id={$log['product_id']}");
$redis->hSet("stock:log:$logId", "status", "confirmed");
} catch (Exception $e) {
// 失败时恢复Redis库存
$redis->incr("stock:{$log['product_id']}");
$redis->hSet("stock:log:$logId", "status", "rollback");
}
}
3.2 补偿机制设计
对于极端情况下的不一致问题,需要设计补偿策略:
- 定时核对脚本:
php复制// 每小时执行一次库存核对
function checkStockConsistency() {
$products = $db->query("SELECT id,count FROM products");
foreach ($products as $product) {
$redisCount = $redis->get("stock:{$product['id']}");
if ($redisCount != $product['count']) {
// 触发报警并自动修复
repairStock($product['id']);
}
}
}
- 修复策略优先级:
- 以数据库为准恢复Redis(适用于对账场景)
- 以Redis为准更新数据库(适用于秒杀场景)
- 人工介入处理(极端不一致情况)
4. 高并发优化实践
4.1 Redis性能调优
- 连接池配置:
php复制$redis = new Redis();
$redis->pconnect('127.0.0.1', 6379, 0.1); // 0.1秒超时
$redis->setOption(Redis::OPT_READ_TIMEOUT, 1);
- Pipeline批量操作:
php复制$pipe = $redis->pipeline();
for ($i = 0; $i < 100; $i++) {
$pipe->get("stock:$i");
}
$results = $pipe->exec();
4.2 数据库优化方案
- 批量更新代替单条更新:
sql复制UPDATE stock
SET count = CASE
WHEN id=1 THEN count-1
WHEN id=2 THEN count-2
END
WHERE id IN (1,2)
- 热点数据分离:
- 将库存数据单独放在一个库实例
- 使用数据库中间件做分片
5. 异常处理与监控
5.1 熔断降级策略
当数据库压力过大时自动降级:
php复制// 使用滑动窗口统计失败率
if ($failureRate > 0.3) {
// 进入降级模式,只操作Redis
$redis->decr("stock:123");
queueDeductionTask(123); // 异步队列处理
}
5.2 监控指标设计
关键监控指标应包括:
- Redis库存与数据库库存差异值
- 预扣减日志积压数量
- 数据库更新平均延迟
- 补偿任务执行成功率
6. 实战经验与避坑指南
- Lua脚本的坑:
- 脚本中不要写复杂逻辑,执行时间控制在1ms内
- 确保所有Redis操作都有错误处理
- 连接泄漏问题:
- 使用连接池时必须显式释放
- 建议采用连接池中间件
- 数据预热技巧:
php复制// 活动开始前预热Redis
$stocks = $db->query("SELECT id,count FROM products");
foreach ($stocks as $stock) {
$redis->set("stock:{$stock['id']}", $stock['count']);
}
- 库存超卖的最后防线:
- 支付前再次检查库存
- 订单创建时记录当时库存快照
在实际项目中,我们通过这套方案将某电商平台大促期间的库存不一致率从0.5%降到了0.002%。核心在于理解分布式系统的不确定性本质,采用异步化+最终一致性的设计思路,而不是强求实时一致性。
