支付系统设计评审:一次增量讨论补足的五个缺陷
一份支付设计方案,是怎么被审出五个洞的 某开发者在设计一套统一支付服务。初版方案自认为考虑周全:支付订单表、退款表、渠道配置、对账批次,表格一张比一张漂亮。然后跟组里人过了一遍设计,被一个问题接一个问题地问到改稿——每一问都补出一个之前没想透的缺陷。 这篇就把这次增量讨论里补上的五个点记下来。它们不是"支付系统特有的冷知识",而是任何一个会分库分表、会对账、要复用的系统都可能踩的坑。 场景:一个要复用的统一支付服务 先交代背景。这套支付服务的目标是: 从"模拟支付"改造成真实支付——聚合支付宝、微信支付、微信小程序支付,未来还可能接更多国内渠道 拥有自己的数据库(此前完全没有,支付数据存在别处) 带一套每日对账系统——拉取渠道对账单、解析、批量入库、逐笔对比、算差异 日后作为独立支付服务接入其他项目(商城只是第一个业务方) 正是"要复用"和"要对账"这两点,引出了下面五个洞。 洞一:对账的逐笔对比写了 JOIN 初版对账设计里,逐笔对比是这么写的: -- 渠道账单临时表 LEFT JOIN 本地支付订单表 SELECT t.trade_no, t.amount, o.pay_amount, ... FROM recon_temp t LEFT JOIN pay_order o ON t.trade_no = o.merchant_order_no; 乍看没毛病——两张表同库、有公共键。但评审的人问了一句:"pay_order 分库分表之后,这个 JOIN 还能跑吗?" 不能。跨表 JOIN 需要分片键能路由到同一分片,而 recon_temp 和 pay_order 的分片键不同(一个按批次、一个按用户),JOIN 会退化成全分片广播扫描——每一个分片都扫一遍再合并,数据量一大就是灾难,更别说分片规则一变直接报错。 📌 前置知识:分库分表后,跨表 JOIN 要保证两张表的行落在同一个分片(同分片键)才能路由;分片键不一致时只能广播到所有分片再内存合并。 改法:双批查询 + 内存撮合。 flowchart TD A["渠道对账单文件"] -->|"解析"| B["recon_temp 当日明细"] C["pay_order 支付订单表"] -->|"按渠道×时间窗查询"| D["当日支付单子集"] B -->|"批查加载"| E["渠道侧内存 Map"] D -->|"批查加载"| F["平台侧内存 Map"] E -->|"按 merchant_order_no 匹配"| G["逐笔撮合"] F -->|"按 merchant_order_no 匹配"| G G --> H["命中 / 仅渠道 / 仅平台"] H --> I["差异写 recon_result"] classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb; classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold; classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold; class A,D process class B,C,F data class G,E condition class H,I process 两个单表查询各自按自己的分片键路由( recon_temp 按 batch_no 、 pay_order 按 user_id ),各自只取撮合需要的列,在内存里建 HashMap 按 merchant_order_no 精确匹配。全程无跨表 JOIN——这就是"无联表"原则,也是后续分库分表的前提条件。 ...