高并发下限额-库存的处理

213 阅读3分钟

高并发下限额-库存的处理

业务场景分析

之前讲过,在大部分场景可以借助数据库的行锁而不是悲观锁实现限额更新,避免性能差问题(并发要求极高场景可以用redis加Lua脚本实现)。

UPDATE daily_limit 
SET used_quota = used_quota + 100 
WHERE user_id = 123 
  AND (total_quota - used_quota >= 100);

但在日限额控制类似场景中,当天的首次交易请求需要完成限额记录的初始化。由于初始记录不存在,直接更新会失效。下面通过两种方案对比说明处理逻辑:

方案对比

方案一:先插入再更新

执行流程

  1. 尝试插入当日记录
  2. 若记录已存在(唯一键冲突),转为执行更新操作
  3. 校验剩余额度是否充足
-- 使用合并操作
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);

优点:逻辑直观
缺点

  • 产生额外的插入操作
  • 性能开销较大,尤其在大部分请求不会触发额度不足的场景下

方案二:先更新再插入

执行流程

  1. 优先尝试更新操作
  2. 若更新失败(记录不存在),尝试插入初始化记录
  3. 处理可能发生的唯一键冲突(多个请求同时尝试插入)

流程图如下(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。两者的对比如下:

  1. 数据库行锁(update加上条件) 在常规配置和轻到中等并发场景下,单次操作的耗时通常在毫秒级(大约 1~5 毫秒)。 对于高并发场景,TPS(每秒事务数)可能在数千到上万之间
  2. redis的lua脚本实现额度或库存扣减 单次操作延迟通常在微秒到低毫秒级,特别是在同一局域网内,网络延时可以控制在 100 微秒左右。 在高并发场景下,Redis 能够支持的请求数量往往远高于数据库,在经过优化的环境中,每秒处理的锁操作可能达到十万级甚至更高

实践建议

推荐方案二。如果

  1. 零点前预生成初始化数据,则需增加对该批量的监控
  2. 使用redis则需考虑redis宕机后的降级方案

总结

在日限额场景中,优先推荐使用“先更新再插入”的方案,可以有效提升系统性能和稳定性。

另外insert时死锁问题也是在这个场景下出现的,可以参考同时insert加update就死锁-来看mysql锁机制