高并发下限额-库存的处理
业务场景分析
之前讲过,在大部分场景可以借助数据库的行锁而不是悲观锁实现限额更新,避免性能差问题(并发要求极高场景可以用redis加Lua脚本实现)。
UPDATE daily_limit
SET used_quota = used_quota + 100
WHERE user_id = 123
AND (total_quota - used_quota >= 100);
但在日限额控制类似场景中,当天的首次交易请求需要完成限额记录的初始化。由于初始记录不存在,直接更新会失效。下面通过两种方案对比说明处理逻辑:
方案对比
方案一:先插入再更新
执行流程:
- 尝试插入当日记录
- 若记录已存在(唯一键冲突),转为执行更新操作
- 校验剩余额度是否充足
-- 使用合并操作
INSERT INTO daily_limit(user_id, date, used_quota, total_quota)
VALUES (123, CURDATE(), 100, 1000)
ON DUPLICATE KEY UPDATE
used_quota = IF((total_quota - used_quota >= 100),
used_quota + 100,
used_quota),
total_quota = VALUES(total_quota);
优点:逻辑直观
缺点:
- 产生额外的插入操作
- 性能开销较大,尤其在大部分请求不会触发额度不足的场景下
方案二:先更新再插入
执行流程:
- 优先尝试更新操作
- 若更新失败(记录不存在),尝试插入初始化记录
- 处理可能发生的唯一键冲突(多个请求同时尝试插入)
流程图如下(update失败后的插入更新可以使用上边的合并Sql):
flowchart TD
A[开始] --> B[Update]
B -->|Succ| C[结束]
B -->|Fail:限额不足或未初始化| D[Insert]
D -->|Succ| F[Update]
D -->|Fail:并发插入失败| F
F -->|Succ| G[限额足够]
F -->|Fail| H[限额不足]
优点:减少无效插入操作
缺点:需要处理二次竞争条件,逻辑复杂度较高
是否用redis实现
有观点认为数据库性能较差,应采用 Redis。两者的对比如下:
- 数据库行锁(update加上条件) 在常规配置和轻到中等并发场景下,单次操作的耗时通常在毫秒级(大约 1~5 毫秒)。 对于高并发场景,TPS(每秒事务数)可能在数千到上万之间
- redis的lua脚本实现额度或库存扣减 单次操作延迟通常在微秒到低毫秒级,特别是在同一局域网内,网络延时可以控制在 100 微秒左右。 在高并发场景下,Redis 能够支持的请求数量往往远高于数据库,在经过优化的环境中,每秒处理的锁操作可能达到十万级甚至更高
实践建议
推荐方案二。如果
- 零点前预生成初始化数据,则需增加对该批量的监控
- 使用redis则需考虑redis宕机后的降级方案
总结
在日限额场景中,优先推荐使用“先更新再插入”的方案,可以有效提升系统性能和稳定性。
另外insert时死锁问题也是在这个场景下出现的,可以参考同时insert加update就死锁-来看mysql锁机制