交易成功还要记 3 笔账?我们直接砍到 1 笔—— 网络红包记账的一次自顶向下业务层重构
交易成功还记 3 笔账?我们把它砍到 1 笔
—— 网络红包记账的一次自顶向下业务层重构
先给结论:性能优化,立足于业务层优化,收益最大。当一段代码的"保险"价值前提已经不存在时,它就不再是保护,而是纯粹的负债。我们把网络红包的记账从两阶段(冻结→解冻→扣减)改成直扣模式,RPC 请求量省掉一半,记账流水砍掉 ⅔。这不是炫技,是一次业务分析驱动的架构简化。
背景:为什么是现在
随着 j'j'l 系统交易量的增多,并发交易量也跟着增多(每秒高达 80 多笔),系统优化、尤其是性能优化,成了首要任务。
j'j'l 网络红包的交易涉及企业校验、风控&延迟风控、交易单&通道单落库、资金冻结记账、请求三方通道、结果落库、同步与异步查单等环节。
这个时候,必须自顶向下,从宏观角度分析业务——因为业务层的优化,往往收益最大。
一、先说结论
优化收益往往和改动层级成反比:越往底层抠(SQL、缓存、线程池),收益越薄;越往业务层看,空间越大。
本文要讲的,不是调优某个 SQL,也不是给某个接口加缓存。而是质疑一个"约定俗成"的记账模式本身——当业务前提变了,模式该不该跟着变。
答案是:该变。我们改了,效果立竿见影。
二、传统的两阶段记账:一套"防御式"设计
先看现状。j'j'l 网络红包发放交易的记账,沿用的是我司各产品线虚拟账户的通用做法——两阶段记账:
| 阶段 | 动作 | 方法 |
|---|---|---|
| 交易发起 | 冻结账户余额 | EnterpriseAccountingService#freeze |
| 交易成功 | 解冻并扣减余额 | EnterpriseAccountingService#transDeduction |
| 交易失败 | 解冻还原余额 | EnterpriseAccountingService#unfreeze |
拆开看,一次成功的交易,实际产生了 3 笔记账流水:
- 冻结(freeze)
- 解冻(transDeduction 内部的解冻动作)
- 扣减(transDeduction 内部的扣减动作)
为什么会有两阶段?
这套模式背后有一个明确的业务前提:
交易要请求三方通道(微信红包 / 支付宝零钱),而通道返回结果是异步的、不确定的、甚至可能长时间不返回的。
在这种前提下,资金必须有一个"中间态":
- 扣早了——商户如果看到交易还没完成呢,账户余额却减少了,会质疑我们系统的安全性;
- 不扣——万一通道成功,商户余额不足,就产生了资损。
于是引入"冻结"作为中间态:先把钱锁住(既没划走,也动不了),等通道结果明确了,再决定是"解冻+扣减"还是"解冻还原"。
这是一套为"结果不确定"设计的防御式记账,逻辑自洽,没有错。
三、关键一步:回到业务,重新审视前提
针对两阶段记账,我们能做什么优化?大胆假设,小心求证。我们的做法是自顶向下,先问业务:
两阶段记账的前提——"交易结果不确定、可能长时间悬置"——在 j'j'l 网络红包这个场景下,还成立吗?
结论是:不成立了。 依据是两条硬数据:
1. 时效性快。
依赖的三方通道是微信红包、支付宝零钱接口,交易通常在 10 秒内完成。没有"挂起几小时等结果"的情况。
2. 成功率高。
交易成功率 98% 以上。也就是说,绝大多数交易走的是"成功"这条路。
把这两条放一起,两阶段记账的价值链条就断了:
- 因为快,"中间态需要长时间保持"的理由消失了;
- 因为成功率高,"失败需要频繁回滚"的理由也被大幅削弱。
于是问题浮出水面:为了那不到 2% 的失败场景,我们让 98% 的交易都多付了 2 笔本不该有的记账开销(一次 freeze + 一次解冻)。
这不是优化不够狠的问题,是模式本身开始与业务失配了。
四、新模式:直扣 + 失败补偿
既然结果几乎总是"成功",那就反过来设计:
默认当成会成功,直接扣减;真失败了,再补回来。
| 阶段 | 动作 | 方法 |
|---|---|---|
| 交易发起 | 直接扣减 | EnterpriseAccountingService#deduct |
| 交易成功 | 无需记账 | —— |
| 交易失败 | 解冻还原余额 | EnterpriseAccountingService#deductRollback |
对比一下就清楚了:
- 旧模式:成功交易 = freeze + 解冻 + 扣减 = 3 笔流水
- 新模式:成功交易 = deduct = 1 笔流水
失败的交易,回滚逻辑和原来一致(deductRollback),没有引入新的复杂度和风险。
这就是"直扣模式"——用"事后补偿"替代"事前防御"。
五、收益量化
上线后,系统压力尤其是记账服务的压力,明显缓解。具体到数字:
| 指标 | 旧模式 | 新模式 | 降幅 |
|---|---|---|---|
| RPC 接口请求量 | 基准 | —— | 省掉约 ½ |
| 成功交易的记账流水 | 3 笔 | 1 笔 | 减少约 ⅔ |
在每秒 80+ 笔的并发量下,这笔账是很可观的——记账服务往往是资金链路上的瓶颈,流水每少一笔,都是一次实打实的吞吐释放。
六、总结:比"改代码"更重要的是"改前提"
回头看,这次优化真正的价值不在那几行代码,而在于三件事:
-
自顶向下,业务先行。 先从宏观业务找空间,而不是一头扎进技术细节。业务层的一个模式调整,收益远超底层几十处微调。
-
质疑"约定俗成"。 两阶段记账是行业通用做法,但不代表在每个场景都最优。当"结果不确定"这个前提不成立时,防御式设计就成了纯开销。
-
用数据支撑决策。 "10 秒内完成"和"98% 成功率"这两条,是把"我觉得可以改"变成"必须改"的关键。没有数据,这只叫赌;有了数据,才叫重构。
一句话收尾:性能优化,先看业务对不对,再看代码快不快。业务模式没对齐,代码再快也是给错误的设计打补丁。
【公众号版本】 交易成功还要记 3 笔账?我们直接砍到 1 笔
事情是这样的。
我们有个系统叫 j'j'l,做网络红包发放的。随着交易量增多,并发也跟着涨,每秒高达 80 多笔 ————系统优化,尤其是性能优化,成了首要任务。
这笔交易的链路可不短:企业校验、风控和延迟风控、交易单和通道单落库、资金冻结记账、请求三方通道、结果落库、同步与异步查单…… 一串环节环环相扣。
这种时候,必须自顶向下,先从宏观角度分析业务。为什么?因为业务层的优化,往往收益最大。如果一上来就往下抠 SQL 慢查、加缓存、调线程池——那些有用,但收益薄。
顺着这个思路,我盯上了"记账"这块。
原来的记账,是两阶段的
先交代背景。我们发红包,需要给商户虚拟账户记账,钱不是直接扣的,中间有个"冻结"环节:
- 交易发起 → 冻结余额
- 三方通道(微信红包 / 支付宝零钱)返回发放成功 → 解冻 + 扣减
- 返回发放失败 → 解冻,还原余额
这套逻辑,我司各产品线的虚拟账户都在用,是"约定俗成"的标准做法。
但注意:一笔成功的交易,背后其实记了 3 笔账——冻结、解冻、扣减。
这里先明确一个概念,免得后面看岔:文中的"扣减",指的是系统内虚拟账户的余额的减少(本地记账动作)。
为什么搞这么复杂?因为它有个前提:
三方通道的结果是异步的、不确定的,可能半天不回来。
所以钱不能直接扣,得先"锁"住,等结果明确了再决定扣还是还。
这逻辑没毛病,是给"结果不确定"做的防御式设计。
但我想问一句:这个前提,在 j'j'l系统 还成立吗?
j'j'l 交易存在两个显著的特征:
- 第一,交易时效特别快。 微信红包、支付宝零钱,基本都是 10 秒内完成,根本不存在"挂几个小时等结果"的情况。
- 第二,成功率高达 98% 以上。 绝大多数交易,走的是"成功"这条路。
这两条一摆出来,问题就暴露了:
为了那不到 2% 的失败场景,我们让 98% 的交易,都多付了 2 笔本不该有的记账开销。
这不叫优化不够狠,这叫模式本身开始和业务失配了。
那怎么办?反过来设计
既然结果几乎总是成功,那就别防御了,直接:
- 交易发起 → 直接扣减账户余额
- 交易成功 → 啥都不用做
- 交易失败 → 再解冻还原(和原来一样)
对比一下就很直观:
- 旧模式成功交易 = 冻结 + 解冻 + 扣减 = 3 笔流水
- 新模式成功交易 = 扣减 = 1 笔流水
失败的回滚逻辑没动,没引入新复杂度。就是把"事前防御"换成了"事后补偿"。
收益呢?实打实的
- RPC 请求量,省了将近一半
- 成功交易的记账流水,从 3 笔降到 1 笔,砍掉 ⅔
每秒 80 多的并发下,记账服务本来就是资金链路的瓶颈。每少一笔流水,都是一次实打实的吞吐释放。上线后,压力明显缓解。
最后说点实在的
这次改动代码量不大,但我觉得真正的价值在三件事:
- 自顶向下,业务先行。 一个业务层的模式调整,顶得上底层几十处微调。
- 敢质疑"约定俗成"。 两阶段记账是行业通用做法,但前提变了,它就不再是保护,而是纯开销。
- 用数据说话。 "10 秒"和"98%",是把"我觉得可以改"变成"必须改"的关键。没数据那叫赌,有数据才叫重构。
一句话收尾:
性能优化,先看业务对不对,再看代码快不快。业务模式没对齐,代码写得再快,也是给错误的设计打补丁。
当看到一些不好的代码时,会发现我还算优秀;当看到优秀的代码时,也才意识到持续学习的重要!--buguge
本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/buguge/p/22916613
浙公网安备 33010602011771号