前两卷里,「回声」解决的是「内容能不能被看到」和「关系能不能成立」。这一卷要解决一个更硬的问题:钱和库存不能算错。
内容推荐错了,用户刷新一下就好;消息晚到几秒,用户大多能接受。但一笔订单多扣了十块、一张券被用了两次、活动库存超卖了三百件,这些都不是「体验问题」,而是需要人工介入、需要向用户和财务解释的事故。
交易系统的工程难度,不在于接口多,而在于每个数字都要能回答三个问题:它从哪来、谁改过它、出错时怎么补。
从「能用」到「算得对」
一个能下单、能支付的系统,离一个能对账的系统还有很长的距离。
「能用」的标准是:用户点购买,扣款成功,订单显示已支付。这个标准在低并发、少渠道、没有退款的情况下很容易满足。
「算得对」的标准是:
- 用户付的每一笔钱,都能在渠道账单里找到对应记录。
- 平台记的每一笔收入,都能说清是哪个订单、哪个商品、用了哪张券。
- 每一次退款,都有独立的流水,而不是把原支付记录改掉。
- 库存的每一次减少,都能对应到一笔订单或一次人工调整。
- 财务的报表和业务的报表对不上时,能定位到具体差异,而不是「大概是因为退款」。
这两个标准之间,隔着状态机、流水、幂等、对账、补偿。本卷讲的就是这些东西。
一次对账差异的事故
先讲一个抽象过的真实事故。
某个电商产品上线了新的优惠券系统。券的核销逻辑很直接:用户下单时,如果订单里有可用券,就把券的状态从「未使用」改成「已使用」,同时从订单金额里减去优惠。
上线第一个月没问题。第二个月做了一次大促,退款量上来后,财务发现:退款金额比支付金额多出了一些。排查后发现,用户退款时,系统退了「实付金额」,但券没有退回。用户重新下单时又用了一次券,平台相当于给了两次优惠。
团队的第一个修复是「退款时把券退回」。但退回逻辑写在了退款成功的回调里,而回调可能重试。于是又出现了新问题:一次退款重试两次,券被退了两张,用户下次可以叠加使用。
第二个修复是「退券加幂等键」。这次券不会被重复退了,但财务又发现另一类差异:部分订单的退款金额和渠道账单对不上。原因是退款接口在超时后重试,渠道实际退了一次,平台记了两次,但第二次因为幂等被吞掉了,平台状态和渠道状态不一致。
第三个修复是「退款也要对账」。团队开始每天拉渠道退款账单,和平台退款流水比对。差异从「月底发现」变成「次日发现」,但依然存在,因为对账任务本身没有幂等,重跑一次就把差异补了两遍。
这个故事没有哪一步是「明显的低级错误」。每一步修复都解决了当前最痛的问题,也都引入了新的边界。交易系统的典型状态就是这样:你很难一次设计对,只能在资金流、状态机、重试与对账之间不断收紧约束。
本卷希望做的,是把这些约束提前讲清楚:什么时候该用预扣、什么时候该用冲正、什么时候必须对账、什么时候可以接受最终一致。
交易系统的三个约束
整卷 13 章,其实在反复处理三个约束。
约束一:状态不能靠覆盖,要靠迁移。
订单、支付、退款、券、活动、权益,每一个对象都有状态。状态的变化必须是「从哪个状态、因为什么事件、迁移到哪个状态」,而不是「把字段改成新值」。
覆盖式更新会丢失历史:退款后把订单状态改成「已退款」,就看不出它曾经支付过、发货过、退过多少。迁移式更新保留每一步,才能回答「这笔钱什么时候退的、退了多少、为什么退」。
约束二:金额不能靠推算,要靠流水。
很多系统只在订单上存一个「实付金额」,需要退款时用「实付 - 已退」推算。这个做法在只有一次退款时没问题,一旦有部分退款、多次退款、优惠分摊,推算就会出错。
正确做法是为每一次资金变动记一条流水:支付、退款、冲正、补贴、分账。订单上的金额是流水聚合的结果,不是真相本身。对账时比的是流水,不是订单字段。
约束三:并发不能靠锁,要靠约束。
秒杀、领券、抢库存,流量会集中到少数热点上。用一把大锁能保证正确,但会把吞吐压到无法接受;用无锁能提高吞吐,但容易出现超卖。
实际方案通常是「预扣 + 异步确认 + 对账兜底」:热点路径只做最小化的原子操作,复杂逻辑异步处理,最终用对账发现并修复偏差。这不是「放弃一致性」,而是把一致性放到正确的层次上。
为什么先写库存
很多交易系统的文章从订单开始,因为订单是用户最直观看到的东西。但订单的正确性依赖库存:如果库存扣减错了,订单再多也无法履约。
库存有三个容易混淆的概念:
- 物理库存:仓库里实际有多少。
- 可用库存:现在能卖多少,等于物理库存减去已占用。
- 预留库存:下单未支付占用的部分。
把这三个概念混成一个字段,会出现两类问题:要么下单不占库存,支付时才发现没了;要么下单就扣物理库存,用户不付款导致库存被锁死。
本卷第 1 章用「预扣 + 确认 + 释放」三操作把这三个概念分开,第 6 章的秒杀会在这个模型上再加一层活动库存。先把库存写对,后面的订单、支付、活动才有基础。
增长为什么放在交易之后
增长动作看起来和交易无关:邀请、实验、推送、召回。但它们都依赖交易系统提供的事实。
- 邀请奖励要发钱或发券,依赖账户与券系统。
- 增长实验的指标往往是支付转化率、复购率,依赖订单与支付数据。
- 召回推送的落地页往往是活动或商品,依赖库存与价格。
- 防刷要判断设备与支付工具,依赖支付流水。
如果交易系统本身算不准,增长系统会放大错误:一次错误的奖励规则,可能在一夜之间发出几十万;一次没有频控的推送,可能把正常用户推走。所以本卷先讲交易,再讲增长。
平台化不是规模化的同义词
第 10–12 章讲开放平台、国际化、运营后台。它们看起来像「大规模才需要的东西」,其实不是。
- 开放平台解决的是「别人能不能安全地调用你的能力」,只要有第三方接入就需要。
- 国际化解决的是「同一套系统能不能服务不同地区」,只要业务跨区域就需要。
- 运营后台解决的是「人能不能安全地改数据」,只要有运营角色就需要。
这三章的共同点是:它们都在处理「边界」——应用与用户的边界、区域与区域的边界、人与系统的边界。边界不清,规模越大,风险越大。
真正的规模化——多活、单元化、容量治理、成本优化——留到卷四。本卷的平台化只讲到「能安全地对外开放和运营」,不展开多机房。
这一卷的判断标准
读每一章时,可以用三个问题检验自己是否理解:
- 这个状态/金额/库存,谁拥有写权限?变动有没有流水?
- 这个操作重复执行两次,会不会多扣、多发、多退?
- 流量涨十倍,先坏的是哪一段?有没有兜底?
这三个问题没有标准答案,但它们能把讨论从「用什么技术」拉回到「业务承担什么风险」。
写给谁
这一卷主要写给三类人:
- 正在做交易、支付、营销或增长系统的后端工程师,需要一套能落地的状态与对账模型。
- 从内容或社交转过来的工程师,已经会写高并发读,但没处理过「钱不能错」的约束。
- 技术负责人,需要在业务早期判断「哪些交易能力现在做、哪些可以晚点做」。
如果你只做过 To C 的内容产品,这一卷会有一些陌生感:内容系统允许最终一致,交易系统在很多地方不允许。但底层思维是相通的:先定义状态,再定义谁能改,最后定义失败时怎么办。
与卷一、卷二的关系
卷一讲基础能力:账号、资料、BFF、API、入口、缓存、异步、发布、观测、容量。卷二讲内容与社交:内容模型、Feed、关系、IM、推送、搜索、推荐。
卷三会反复用到它们,但不重复讲。比如缓存,卷三讲的是「库存能不能缓存」「券的可用性能不能缓存」;比如异步,卷三讲的是「支付回调能不能异步」「履约失败怎么补偿」;比如观测,卷三讲的是「对账差异率怎么监控」。
如果读本卷时发现某个基础概念不熟,回到卷一或卷二对应章节即可。三卷的代码在同一个实验工程里,包名不冲突。
交易链路的五个数字
在交易系统里,有五个数字贯穿始终。理解它们之间的关系,比记住任何一个框架都重要。
| 数字 | 含义 | 常见错误 |
|---|---|---|
| 商品金额 | 商品原价 × 数量之和 | 用展示价计算,忽略 SKU 实际价 |
| 优惠金额 | 券、满减、会员价、积分的总和 | 各项优惠独立计算,叠加后超过商品金额 |
| 应付金额 | 商品金额 − 优惠金额 | 出现负数,或与前端展示不一致 |
| 实付金额 | 用户实际支付的钱 | 与应付混淆,渠道手续费被算进实付 |
| 退款金额 | 已退回用户的钱 | 超过实付,或多次退款累计超过实付 |
这五个数字必须在同一套口径下计算,并且在订单、支付、退款、对账四个环节保持一致。任何一个环节用了不同的口径,都会在月底变成一笔说不清的差异。
一个实用的检查方法是:拿一笔真实的订单,把五个数字手算一遍,再和系统里的记录逐个比对。如果对不上,先别急着改代码,先确认是哪个数字的定义不清楚。
一次秒杀事故
再讲一个和并发有关的事故。
某个产品做限时秒杀,库存 1000 件。技术方案是:用户点击购买,服务端查库存、判断大于 0、扣减、创建订单。为了防止超卖,扣减操作用了数据库行锁。
活动开始后,流量远超预期。行锁导致大量请求排队,接口超时;超时的请求被客户端重试,重试又打到同一行锁上。数据库连接池很快耗尽,连正常的商品查询也失败了。
团队的第一个修复是「把库存放到 Redis,用 DECR 扣减」。超卖问题解决了,接口也快了。但新的问题出现了:Redis 扣减成功、创建订单失败时,库存没有回补,1000 件库存实际只卖出 600 件,剩下 400 件「消失」了。
第二个修复是「创建订单失败时把库存加回去」。但加回操作也可能失败,而且重试会导致重复加回。团队只好再加一个定时对账任务,用订单表反推库存。
第三个修复是对了,但代价是:系统里同时存在 Redis 库存、数据库库存、订单反推库存三个来源,任何两个对不上都要人工排查。
这个事故的教训不是「不要用 Redis」,而是:热点路径上的每一个操作,都要定义失败后的补偿方式。 扣减失败怎么办、创建订单失败怎么办、补偿失败又怎么办,这三个问题必须在设计时回答,而不是上线后靠对账兜底。
三个「看起来对」的陷阱
交易系统里有三类问题特别隐蔽,因为它们在功能测试里完全正常。
第一类:单笔正确,累计错误。
一笔退款算对了,十笔退款也各自算对了,但第十一笔退款时,累计退款超过了实付。原因是每次退款只校验「本次金额 <= 实付」,没有校验「历史退款 + 本次 <= 实付」。
第二类:正常路径正确,重试路径错误。
支付回调正常到达时,订单状态更新正确;回调重试时,因为缺少幂等键,订单被更新了两次,或者发货了两次。重试不是异常,是分布式系统的常态。
第三类:单个服务正确,跨服务错误。
库存服务扣减成功,订单服务创建失败,两个服务各自的数据都「正确」,但合起来是错的。跨服务的一致性不能靠「每个服务都做对」,要靠补偿、对账或事务消息。
这三类问题的共同点是:它们都发生在「边界」上——时间边界、重试边界、服务边界。设计时多问一句「这个操作在边界上还成立吗」,能省下很多线上排查。
本卷不回答的问题
- 不回答「选哪个支付渠道」。渠道的费率、覆盖、结算周期各产品不同,书里只讲统一抽象与对账。
- 不回答「用什么数据库」。库存、订单、流水对存储的要求不同,书里只讲模型与约束。
- 不回答「怎么做风控模型」。防刷只讲到规则与维度,不展开模型训练。
- 不回答「怎么做多机房」。多活、单元化、容灾留到卷四。
- 不回答「怎么做财务系统」。只讲业务侧的对账接口与差异处理。
把边界写清楚,比假装覆盖全部更有用。读者如果需要这些内容,站内有对应专题,本卷会在相关章节给出入口。
读完这一卷应该能做到什么
如果读者认真读完并跑过代码,应该能做到下面几件事:
- 给定一个商品模型,能区分商品与 SKU,并说明价格为什么放在 SKU 上。
- 给定一个下单流程,能画出库存的预扣、确认、释放三个动作,并说明每一步失败后怎么补。
- 给定一个订单状态机,能列出所有合法迁移,并指出哪些迁移可能并发冲突。
- 给定一次支付回调,能说明验签、验金额、幂等、补偿四个步骤的顺序与理由。
- 给定一组券,能算出最优叠加方案,并说明贪心为什么不一定最优。
- 给定一次活动,能设计库存预热、分层限流与风控兜底。
- 给定一个增长实验,能判断分流是否稳定、样本是否足够、结论是否可信。
- 给定一次对账差异,能定位到具体流水,并说明是补记、冲正还是人工处理。
这八件事不是「知识点」,而是八种判断。书里的代码只是这些判断的一种实现。
关于「对账」的补充
对账经常被当成「财务的事」,其实它是交易系统最后一道正确性防线。
对账至少有三个层次:
- 渠道对账:平台支付流水 vs 渠道账单,发现掉单、重复、金额不符。
- 业务对账:订单金额 vs 支付流水 vs 退款流水,发现状态与资金不一致。
- 库存对账:库存流水 vs 订单占用 vs 实际盘点,发现超卖、漏扣、错补。
三个层次的对账任务都要满足两个条件:幂等(重跑不会重复补)和可解释(差异能定位到具体记录)。做不到这两点,对账任务本身就会成为新的故障源。
交易系统里的四类时间
时间在交易系统里不只是「创建时间」,至少有四类,混用会直接导致业务错误。
| 时间 | 含义 | 用错的后果 |
|---|---|---|
| 业务时间 | 订单归属的日期,通常按业务时区 | 跨时区订单算错日,报表对不上 |
| 事件时间 | 支付、发货、退款实际发生的时间 | 用创建时间代替支付时间,账期错位 |
| 记录时间 | 数据写入数据库的时间 | 补录数据被算进当天流水 |
| 过期时间 | 订单超时、券失效、活动结束的边界 | 边界含不含,决定是否多给一次机会 |
这四类时间要分开存,不要用一个 created_at 代表全部。尤其是「业务时间」与「事件时间」:用户 23:59 下单、00:01 支付,订单算哪一天,需要产品与财务共同定义,而不是由数据库时间戳决定。
时间边界还有一个常见问题:区间是开区间还是闭区间。代码里写 now < expires 还是 now <= expires,会让「最后一秒」的用户得到完全不同的结果。本书在涉及边界的函数里会明确写出规则,并在测试里覆盖边界值。
时间相关的检查,可以压缩成下面几条:
- 订单归属日按业务时区算,不按数据库时区算。
- 支付成功时间用渠道回调或查单结果,不用创建时间。
- 补录数据要标记来源,不能混进自然流水。
- 过期判断统一用「小于」还是「小于等于」,并在测试里覆盖边界。
- 跨时区的「今天」必须显式传时区,不依赖服务器默认值。
- 报表的日切时间要与财务约定,不要各自实现。
一个提醒
交易系统最危险的状态,不是「明显算错」,而是「看起来都对」。
用户下单成功、支付成功、订单显示已完成,每一屏都正常;但财务月底对账时发现少了几万块,或者库存系统显示还有货、仓库实际已经空了。这类问题不会在功能测试里暴露,只会在对账、盘点、退款时暴露。
所以这一卷会反复强调「流水」「幂等」「对账」这三个词。它们不像「高并发」「分布式」那样吸引人,但它们是交易系统能不能长期运行的底线。
技术方案从来不是中立的。你选择用覆盖还是迁移,决定了半年后能不能解释一笔退款;你选择同步扣减还是异步确认,决定了高峰期会不会超卖。这一卷希望读者在做选择时,知道自己选了什么。
最后一句话:交易系统的正确性,不是一次设计出来的,是一笔一笔对出来的。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。