1. 高并发库存管理的核心挑战
电商秒杀、票务系统、限时促销等场景下,库存扣减是最典型的业务痛点。去年双十一我们系统峰值QPS冲到2万+时,就遇到过Redis库存显示充足但MySQL更新失败的情况。这种数据不一致轻则导致超卖资损,重则引发用户投诉和公关危机。
库存管理的本质是保证"查询-计算-扣减"操作的原子性。传统方案直接用MySQL的UPDATE inventory SET stock=stock-1 WHERE id=? AND stock>=1,虽然能保证原子性,但在万级QPS下根本扛不住。这就是为什么大家都会用Redis做库存预扣减——它的QPS能轻松突破10万级。
2. 混合存储架构的设计要点
2.1 Redis预扣减的核心逻辑
我们采用的方案是Redis Lua脚本实现原子扣减:
lua复制local key = KEYS[1]
local change = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key))
if current >= change then
return redis.call('DECRBY', key, change)
else
return -1
end
这个脚本的精妙之处在于:
- GET和DECRBY的原子性组合
- 负数库存的预先拦截
- 单次网络开销完成所有操作
2.2 MySQL最终一致性的保障
Redis扣减成功后,需要通过事务保证MySQL的更新:
php复制$db->beginTransaction();
try {
$affected = $db->executeUpdate(
'UPDATE inventory SET stock=stock-? WHERE id=? AND stock>=?',
[$quantity, $itemId, $quantity]
);
if ($affected === 0) {
throw new \RuntimeException('Stock update failed');
}
$db->commit();
} catch (\Exception $e) {
$db->rollBack();
// 触发Redis库存回滚
}
3. 分布式事务的补偿机制
3.1 定时任务对账方案
我们部署了每分钟执行的补偿Job:
php复制$inventories = $redis->hGetAll('inventory_locks');
foreach ($inventories as $itemId => $locked) {
$dbStock = $db->fetchColumn('SELECT stock FROM inventory WHERE id=?', [$itemId]);
if ($dbStock != $locked) {
$redis->hSet('inventory_locks', $itemId, $dbStock);
$logger->alert('Inventory mismatch corrected', ['item'=>$itemId]);
}
}
3.2 消息队列的可靠事件
更优雅的方案是用RabbitMQ的confirm模式:
php复制$channel->confirm_select();
$channel->basic_publish(
new AMQPMessage(json_encode([
'item_id' => $itemId,
'quantity' => $quantity
])),
'inventory'
);
if (!$channel->wait_for_pending_acks(5000)) {
$redis->incrBy("inventory_$itemId", $quantity);
}
4. 异常处理的最佳实践
4.1 网络分区时的处理策略
当Redis和MySQL出现网络隔离时,我们采用保守策略:
- 立即熔断库存服务
- 界面显示"库存计算中"
- 启动应急同步脚本
4.2 双重扣减的预防措施
为防止消息重复消费导致的双重扣减:
php复制$dedupKey = "dedup_{$orderId}_{$itemId}";
if ($redis->setnx($dedupKey, 1)) {
$redis->expire($dedupKey, 86400);
// 处理扣减逻辑
} else {
throw new \RuntimeException('Duplicate request');
}
5. 性能优化实战技巧
5.1 热点库存的分片方案
对秒杀商品采用库存分片:
php复制$slot = crc32($itemId) % 10;
$redisKey = "inventory_{$itemId}_$slot";
$shardQty = ceil($quantity / 10);
// 对每个分片执行扣减
5.2 本地缓存的巧妙运用
在PHP-FPM环境下,使用apcu扩展做本地缓存:
php复制if (!apcu_exists('inventory_alarm')) {
$stock = $redis->get("inventory_$itemId");
if ($stock < WARNING_THRESHOLD) {
apcu_store('inventory_alarm', 1, 60);
triggerMonitor();
}
}
6. 监控体系的建设
我们配置了以下监控指标:
- Redis与MySQL库存差值告警
- 扣减操作平均响应时间
- 补偿任务执行成功率
- 库存回滚频率统计
使用Prometheus收集数据:
php复制$counter = $prometheus->getCounter(
'inventory_operations_total',
'Total inventory operations',
['type']
);
$counter->inc(['rollback']);
7. 压力测试的注意事项
模拟真实场景时需要特别关注:
- Redis连接池耗尽的情况
- MySQL死锁发生率
- 网络延迟波动的影响
- 补偿任务积压时的表现
建议使用JMeter构造阶梯式压力:
bash复制jmeter -n -t inventory_test.jmx -l result.jtl -Jthreads=500 -Jrampup=60
8. 容灾演练的必须项
每季度必须验证:
- Redis宕机时降级方案
- MySQL主从切换影响
- 补偿任务手动触发流程
- 库存数据修复工具的有效性
我们编写的自动化演练脚本包含:
php复制$faultType = $argv[1] ?? 'redis';
switch ($faultType) {
case 'redis':
$redis->config('SET', 'save', '');
$redis->config('SET', 'appendonly', 'no');
break;
case 'mysql':
$db->executeUpdate('SET GLOBAL innodb_fast_shutdown=0');
break;
}
9. 业务层面的防护策略
除了技术方案,还需要:
- 设置合理的购买限制
- 实施人机验证
- 建立超额预订buffer
- 准备人工核销流程
比如在扣减前校验:
php复制if ($quantity > MAX_PER_USER) {
throw new \RuntimeException('Exceed purchase limit');
}
if (!$captcha->verify($request->get('captcha'))) {
throw new \RuntimeException('Invalid captcha');
}
10. 新一代解决方案的演进
我们正在测试的方案包括:
- Redis 7.0的Function特性替代Lua
- 使用TiDB替代MySQL
- 基于CRDT的最终一致性算法
- 服务网格的熔断配置
例如使用Redis Function:
javascript复制#!js api_version=1.0 name=lib
redis.registerFunction('decrement_if_enough',
(client, key, value) => {
let current = client.call('GET', key);
if (parseInt(current) >= parseInt(value)) {
return client.call('DECRBY', key, value);
}
return -1;
}
);