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

259 阅读6分钟

本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~

高并发场景分为高并发读和高并发写,账户余额扣减毫无疑问属于后者,处理起来比高并发读难一些。

在高并发写场景中,最难处理的就是具有写热点的场景。

换句话说,每秒钟有一万个账户同时进行一笔余额扣减操作,比每秒钟有一个账户进行一万笔余额扣减操作简单得多,数据库行锁机制是后者的最大天敌。

有的同学可能会说,怎么可能存在每秒钟有一个账户进行一万笔余额扣减操作的场景呢,总不能为了高并发而高并发吧。

其实真的有,广告计费平台就是这样的场景,用户每点击一次或者浏览一次广告,都会对广告主的账户余额扣减一次。
而且这种扣减操作一定要实时,否则广告主的账户没钱了,但广告还一直在展示,那就会对平台造成损失。
广告核心业务流程如下图所示,用户点击广告后,会在广告计费平台完成账户余额扣减操作,并将此算作平台收益。

图片(图片来自于网络,侵删)

接下来,我们就对此业务场景进行分析设计,看看如何实现高并发场景下的账户余额扣减。

原始状态系统最初没有进行任何高并发方面的优化,只是在按部就班地实现记录流水、反作弊验证、账户扣费、更新流水、后续处理等业务流程。

图片

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的时候,就马上通知上游系统停止广告展示。