面试官:如何实现高并发场景下的账户余额扣减?

张开发
2026/5/9 15:04:52 15 分钟阅读
面试官:如何实现高并发场景下的账户余额扣减?
高并发场景分为高并发读和高并发写账户余额扣减毫无疑问属于后者处理起来比高并发读难一些。在高并发写场景中最难处理的就是具有写热点的场景。换句话说每秒钟有一万个账户同时进行一笔余额扣减操作比每秒钟有一个账户进行一万笔余额扣减操作简单得多数据库行锁机制是后者的最大天敌。有的同学可能会说怎么可能存在每秒钟有一个账户进行一万笔余额扣减操作的场景呢总不能为了高并发而高并发吧。其实真的有广告计费平台就是这样的场景用户每点击一次或者浏览一次广告都会对广告主的账户余额扣减一次。而且这种扣减操作一定要实时否则广告主的账户没钱了但广告还一直在展示那就会对平台造成损失。广告核心业务流程如下图所示用户点击广告后会在广告计费平台完成账户余额扣减操作并将此算作平台收益。图片来自于网络侵删接下来我们就对此业务场景进行分析设计看看如何实现高并发场景下的账户余额扣减。原始状态系统最初没有进行任何高并发方面的优化只是在按部就班地实现记录流水、反作弊验证、账户扣费、更新流水、后续处理等业务流程。btw步骤(1)中的所记录的扣费流水其实只是一个初始状态主要是防止Kafka宕机或消费失败导致无法补偿的问题。步骤(4)中的更新流水是在该请求通过反作弊校验并成功扣费后将流水状态更新为“已扣费”才是一条真正意义上的扣费流水记录。这里的后续处理流程中最关键的一步是当广告主的账户余额预算为0时通知上游系统将其广告下线。当遇到大广告主进行投放的时候系统就扛不住压力了数据库的负载和IO成为了瓶颈。异步消峰系统若要承载高并发的瞬时流量第一件事就是引入Kafka进行异步消峰。如图中所示当用户点击广告后计费平台接收到该请求并将流水持久化使其具备可回溯性、可补偿性。该步骤只是往数据库中顺序新增数据一般情况下不会产生性能瓶颈然后将请求数据投递到Kafka中进行异步消峰。再由Kafka的消费者按照自己所能承载的节奏消费数据完成反作弊、扣费、更新流水和后续处理操作。该方案的不足之处在于瞬时的高并发流量会导致Kafka消息积压。当消费者获取消息进行处理判定广告主的账户余额预算是否为0并通知上游系统将其广告下线时会由于消息积压导致广告下线不及时从而产生平台资损。因此在必要的时候我们还是要提升Kafka消费者的消息处理吞吐量的。并行化处理通过Kafka进行消峰只是权宜之举若流量长时间居高不下通过并行化处理的方式提升吞吐量才是正解。我们都知道消费者是通过调用poll()方法拉取一批消息进行处理的默认值为500可根据max.poll.records参数进行合理配置。拉取该批次消息后接下来将执行消费者的消息处理逻辑我们可以通过线程池的方式并行处理消息的方式来提升吞吐量。当然线程池并行消费的方式不能保证有序性但广告计费的业务场景并不需要严格的有序性。分库分表对于广告计费的反作弊、扣费、更新流水和后续处理等步骤来说一旦通过线程池并行消费的方式来提升吞吐量很有可能会将瓶颈落在数据库上导致数据库服务器的CPU使用率和负载飙高。数据库中应该会涉及到两张重要的主表扣费流水表和广告主账户预算余额表。其中扣费流水表的数据量会非常庞大而广告主账户预算余额表对于单条记录的更新操作比较频繁。此时我们可以用广告主ID作为Sharding Key对这两个表同时进行分库分表且余额表还需要具备对冷数据进行归档的功能防止单表数据量过大产生性能瓶颈。分散热点本文开头的一句话每秒钟有一万个账户同时进行一笔余额扣减操作比每秒钟有一个账户进行一万笔余额扣减操作简单得多。原因很简单广告主账户预算余额表的数据库行锁机制就是后者的最大天敌。因此当真出现一个大广告主每秒钟产生上万次广告展示并进行扣费操作那现有的这套分库分表的方案是无法支撑的。对于该场景行业内的主流解决方案是进行账户拆分将一个广告主的主账户拆分成多个子账户并均匀地分配在各个库表中以分散热点的方式提升业务并发度。举个例子原本广告主的主账户的余额是1个亿为其分配10个子账户每个账户的金额为1000万当进行广告计费扣款时可对其任意一个子账户进行扣款并记录扣费流水。除了拆分子账户外还有另一个减少热点写操作的方式那就是对广告主账户预算余额表单笔多次扣款操作改为一次性批量扣款操作。可以通过Kafka消费者调用poll()方法拉取一批消息并累加到一起进行扣减也可以通过定时任务轮询计算一段时间范围内的扣费流水记录进行扣减。后者需要注意的是避免账户超扣所带来的平台资损问题可通过如下两种方式进行控制。1控制好定时任务的间隔期一次定时任务的轮询间隔不要太久可控制在10秒钟左右。2当账户余额不足、而不是被扣费为0的时候就马上通知上游系统停止广告展示。

更多文章