【技术篇】高并发下作余额扣减的一些经验

【技术篇】高并发下作余额扣减的一些经验
最新回答
佐佐木惠理

2023-08-19 05:15:50

高并发下余额扣减的核心问题在于高频账号对数据库单记录的并发访问导致行锁竞争,进而引发系统性能瓶颈。以下是基于实践经验总结的解决方案及适用场景分析

一、不设置余额字段,通过流水明细计算
  • 原理:完全移除数据库中的余额字段,仅记录每笔交易的流水明细,余额通过实时查询流水并求和得出。
  • 适用场景:交易频率低、单笔金额大的场景(如电商订单支付)。
  • 局限性

    高频小额交易不适用:广告计费等场景中,每秒数千笔交易会导致计算压力远超数据库查询能力。

    实时性要求高:若需实时展示余额,需频繁全表扫描流水表,性能极差。

二、合并与拆分:降低单记录访问压力
  • 合并请求

    原理:将同一账号的多次扣减请求合并为单次批量操作(如每秒汇总一次后写入数据库)。

    实现方式:通过消息队列(如Kafka)缓冲请求,按时间窗口批量处理。

    适用场景:允许延迟扣费行卜的业务(如月结类服务)。

    局限性:延迟期间余额可能被耗尽,需配合预警机制。

  • 拆分账号

    原理:将主账号拆分为多个子账号(如按用户ID哈希取模分片),请求均匀分配至子账号。

    实现方式

    数据库分表:按子账号ID分库分表,减少单表压力。

    读写分离链冲:子账号数据可独立读写,主账号数据定时同步。

    适用场景:用户量极大且交易档唤穗分布均匀的场景(如游戏币扣减)。

    局限性:增加系统复杂度,需处理子账号与主账号的数据一致性。

三、减少行锁占用时间:代码层优化
  • 关键操作

    缩短事务时间:将非必要操作(如日志记录、外部API调用)移至事务外执行。

    避免SELECT ... FOR UPDATE:改用UPDATE直接扣减并校验余额,示例:

    UPDATE accounts SET balance = balance - 10 WHERE user_id = 123 AND balance >= 10;

    条件判断:若业务允许余额为负,可移除WHERE条件;否则需检查受影响行数(如MySQL的ROW_COUNT()),若为0则抛出异常。

  • 适用场景:所有需行锁的余额扣减场景。
  • 局限性:需确保业务逻辑允许“先扣减后校验”的乐观锁模式。
四、限流:控制并发量
  • 实现方式

    令牌桶算法:限制每个账号的请求速率(如每秒最多10次)。

    分布式锁:对高频账号加锁,同一时间仅允许一个请求处理(如Redis的SETNX)。

  • 适用场景:已知高频账号列表或可预测流量峰值的场景。
  • 局限性

    延迟扣费:限流可能导致部分请求被丢弃或延迟,需记录失败请求并重试。

    动态账号问题:若高频账号动态变化,需实时调整限流策略。

五、缓存:异步同步降低数据库压力
  • 实现方式

    本地缓存:在应用层缓存余额,扣减时直接操作缓存,定时批量同步至数据库。

    分布式缓存:使用Redis等缓存余额,通过Lua脚本保证原子性,示例:

    local key = "balance:123"local balance = redis.call("GET", key)if balance and tonumber(balance) >= 10 then redis.call("DECRBY", key, 10) return 1else return 0end
  • 适用场景:对实时性要求不高且可接受最终一致性的场景(如积分系统)。
  • 局限性

    数据一致性风险:缓存与数据库同步延迟可能导致超扣。

    缓存穿透:恶意请求高频账号可能导致缓存失效,需结合布隆过滤器过滤无效请求。

六、综合方案选择建议
  1. 严格实时性要求

    优先选择减少行锁占用时间+缓存(如Redis原子操作),确保每次扣减立即生效。

  2. 允许延迟扣费

    采用合并请求+限流,平衡性能与数据准确性。

  3. 超高并发场景

    结合账号拆分+分布式缓存,将压力分散至多个节点。

  4. 业务兼容性

    若余额不可为负,需在方案中增加校验逻辑(如UPDATE返回0时拒绝请求)。

核心原则:根据业务对实时性、一致性和吞吐量的需求,选择最适合的组合方案,而非单一技术。例如,电商支付系统可能同时采用“行锁优化+缓存+限流”以应对促销期间的流量峰值。