高并发扣款指南:如何终结超花危机?
背景
-
业务场景
- 新活动刚开始,会有一段时间的高峰期。
- 部分用户会利用脚本发起批量操作,导致系统面临以下挑战:
- 超卖风险:账户余额仅10元,但并发请求可能导致实际扣减超过10元(如两笔6元扣减后余额变为-2元)。
-
核心问题
- 传统方案仅依赖数据库事务(如先查余额再扣减),无法应对高并发竞态条件。
-
业务目的
- 零资损:100%规避超卖问题
- 低延迟:单请求响应时间<50ms(满足上万QPS需求)
- 高可用:系统可用性 ≥99.99%(7×24小时运行)
回答
这是一个典型的超卖问题。
-
传统方案:
-
先查一下余额,判断下够不够。如果够就扣减;不够就不扣减。
这种方式,最简单的,也最容易出错。
-
问题:出现多个并发请求时,就会出现查询的时候金额都是够的,然后,就更新成负数了。
-
1、 分布式锁
- 所有的余额扣减的请求排队执行!
- 在查询用户余额之前,针对账户先加一把锁。只有抢到锁的线程才能进行余额的查询和扣减。
- 缺陷:加锁会降低并发度
2、 数据库乐观锁
只有剩余金额(balance)大于本次扣减金额(amount)的时候,SQL才能执行成功,否则SQL更新结果就是0条,无法更新成功。
-
优点:锁的粒度很小。并发度很高。
-
缺点:压力全给到 MySQL,受限于 MySQL 的单行更新的热点瓶颈。
MySQL 热点更新是一个非常大的问题,它有一个物理极限,超过这个 QPS 就抗不住了。
3、 Redis + lua(最常见的方案)
-
最常见的方案,就是 Redis + lua 的方案来防超卖。
-
Redis 是单线程,所以,命令的执行天然就是排队的。
-
lua 脚本,可以保证多个命令以原子性的方式执行。
我们可以,在一个 lua 脚本中完成余额的查询、判断以及扣减的一系列组合动作。
-
思考
引入 redis + lua 的方案来解决超卖问题,会带来 Redis中的余额和数据库中的余额的一致性问题。
通常的解决方案: MQ的重试+旁路+对账
看看这个方案怎么实现?会有什么问题?