百万订单踩坑记——我注释了财务流水

接上文 《百万订单踩坑记-迷信框架》 。上次是框架的坑,这两次事故是我们自己埋的:一个是财务流水,一个是脚本履约。

一张账户表,一张流水表

财务的设计很简单,一张账户表,一张流水表。

流水表核心字段

字段 说明
userId 用户 ID
platformId 平台 ID
ioValue 收支金额
oldValue 变动前余额
newValue 变动后余额

账户表核心字段

字段 说明
userId 用户 ID
balanceValue 账户余额
freezeValue 冻结金额
pointValue 积分

余额每动一次,就写一条流水。谁的钱、怎么变的、变完剩多少,事后都能对出来。

这里有个现在看挺骚的操作:系统里会有一个财务账号,专门用来做财务核对、算账,提现等一系列操作。用户下一单跑腿,流水表里会写这个 user_id 支出了多少钱;对应的,财务账号的 user_id 上就有一笔收入。为了让财务核对得更明细,用户下单对自己只是一条支出流水,对财务账号却可能是多条收入:服务费一条、打包费一条,分开记。

为什么这样设计,因为这是前任留下来的。一开始量不大,这些流水都是同步写,没出过问题,所以也没人去动它。直到有一次过节。

节日当天

也是某个节日当天,用户反馈下跑腿单特别慢。我接到反馈赶紧查,先看服务器,CPU 都很低,不像被打满的样子;那问题大概率在数据库,一看,果然有大量的锁等待。

原因就在写流水这一步。写流水要先锁住这个账号的余额行,对余额做操作,然后再落流水。用户账号各锁各的,互不相干;可财务账号全站只有一个 user_id,所有请求都往这一行上挤。更麻烦的是,一个用户操作会产生多条财务流水,每条都要走一遍「锁行、改余额、写流水」。用户一多,全在争同一行的锁,只能排队,一个一个来。

找到原因,立马把财务流水的代码注释掉,发布上去,就正常了。这个操作挺粗暴,但节日当天保命要紧,财务的账可以事后再对,系统卡死不行。

后期优化

节日过后,把这块补了回来:财务流水不再同步写,改成先推到一个 Redis 队列,后面用脚本消费。这里刻意用的是单队列单消费。流水不要求实时,消费慢一点没关系;单消费者能保证同一时刻只有一个进程去动财务账号那一行,就不会再挤成一团。

复制代码
同步:订单完成 → 扣用户余额 → 流水任务入队 → 接口返回
异步:流水任务出队 → 单消费者 → 财务账号

脚本履约

外卖订单有个履约的概念:订单在我们平台上的操作,要把相关状态回传给外卖平台。比如订单在我们这儿完成了,就得把完成状态同步过去,平台才知道这单履约了。

一开始是脚本履约,每分钟跑一次,把待回传的订单捞出来同步。当时看也合理:回传不要求秒级,一分钟一次够了。

同样是节日期间,客户找过来说店铺被关了。查下来才知道,订单量上来之后,脚本一轮跑不完,下一轮又压上来,根本跑不完。订单大量积压,很多早该回传的状态一直没传过去,平台那边看到的就是这些订单迟迟没履约,判定违约,店就关了。

临时解决

先用 Swoole 起了多个进程,每个进程跑一部分订单,速度一下就上来了。但轮询这个模式没变,量再涨,还是会积压。

后期解决

解法还是异步延迟队列。订单进入某个状态时,按业务算一个大概的完成时间,推一条延迟消息进去,到点由消费者去做回传。单个订单自己盯着自己的时间点,不用等下一轮脚本扫到它。

总结

财务系统这东西,说大能做很大,说小也能很小。我们初期就是怎么简单怎么来,两笔债都是那时候埋下的,等到节日才集中还上。

你们现在的财务系统是怎么做的,有没有踩过类似的坑,欢迎留言聊聊。

欢迎关注公众号,第一时间更新