
凌晨两点零七分我盯着监控大屏右上角的数字那是一条几乎垂直向上的曲线。交易峰值每秒十一万笔账务流水以三倍于交易量的速率在存储层滚动数据库的CPU像被掐住脖子一样往上顶。这是16年金融架构生涯里又一个618的凌晨也是我第一次在百亿交易规模的系统上亲眼看到“日9亿笔”这个数字稳稳落地。这些年我踩过的坑比很多人写过的架构方案都多。分布式架构、微服务拆分、单元化改造、缓存穿透、分布式事务、对账差账、扩容回滚……每一个关键词背后都是真实的事故报告和凌晨三点的紧急会议。这篇文章不聊理论只聊我在百亿交易、618日9亿这种量级下用真金白银换回来的踩坑全记录。适合正在做金融交易系统、支付系统或者任何高并发强一致场景的同学参考也适合那些觉得“架构就是画图”的同行冷静一下。1. 从单体到单元化16年架构演进路线图1.1 我经历的三代核心系统先交代背景。我入行时金融核心系统还是典型的单体架构一套大而全的应用连着Oracle数据库跑在小型机上。那时候的架构很简单简单到让人怀念——一个应用一个库出了问题重启、扩容靠加CPU。但简单也就意味着天花板低当交易量从每天几百万笔涨到几千万笔时数据库的连接数和单表数据量首先撑不住了。第二代是服务化改造。我们花了两年时间把交易、账务、风控、清结算拆成独立服务数据库做读写分离引入消息队列做异步解耦。这阶段最大的收获不是性能提升而是让团队具备了“并发开发”的能力——十多个小组可以在不同服务上并行迭代不再挤在一个代码库里互相踩脚。但服务化解决不了核心账务库的单点瓶颈账务库还是那个唯一真源所有服务最终都要回到它身上读写。第三代才是单元化。这是金融行业应对海量交易比较成熟的一套打法核心思想是“把流量和数据的处理能力水平复制成多个单元每个单元都能独立处理一部分完整的业务请求”。说得直白些以前是所有请求都打到一个机房的一套系统上单元化之后我们把用户按某种规则分片每个分片的数据和计算能力在多个机房、多个单元里各自部署一份。用户请求进了网关直接路由到属于他的那个单元在单元内部完成绝大部分读写只有跨单元的查询才走全局路由。1.2 单元化架构不是炫技是扩容刚需很多团队一听单元化就觉得高大上觉得是在追赶大厂潮流。我的实际体会是做单元化不是撑面子是被数据容量和数据库连接数活活逼出来的。算一笔账日9亿笔交易按每笔交易产生主流水和明细流水多条记录计算一天的账务数据增量就在几十亿行以上。传统单库单表的数据库一个库能承载的连接数、事务并发和存储容量是有限的。即便分库分表如果所有分片还共用一个数据库实例数据库实例本身依然是瓶颈。单元化之后每个单元有自己完整的数据库分片单元之间物理隔离扩容时加一个单元就是加一整套“应用缓存数据库”的部署能力而不是只加几台应用服务器。但单元化的代价非常大。数据分片规则一旦定错后续数据迁移和查询会非常痛苦。我们当时把用户ID作为分片键按单元维度做二级路由规则看起来简单实际落地时遇到一个麻烦——用户跨单元查询交易记录怎么办解决方案是全局索引加异步复制用户的核心数据在本单元但他的基础档案和跨单元引用关系会异步同步到全局索引集群查询时先查全局索引再回本单元拉详情。这套机制听着简单做起来涉及数据一致性、延迟、索引存储成本任何一个环节没想清楚都容易埋雷。1.3 架构演进的时机判断老兵的建议架构演进最大的坑不是技术选型而是时机判断。我见过太多团队在交易量还没起来时就照着大厂的终极形态设计系统结果业务没等到那一天团队先被复杂度和成本拖垮了。我的建议很朴素当现有架构出现“明确的、可量化的瓶颈”时再演进不要因为“别人都在做”而动手。什么叫明确瓶颈数据库CPU持续超过70%连接数告警单表数据量超过亿级后索引维护成本飙升发布一次要协调十几个团队线上故障定位超过半小时。这些信号出现两个以上才说明架构真的该动了。反过来如果只是交易量增长但系统还很稳那说明当前架构的冗余度还没吃完这时候优化的重点应该是“降低单笔交易成本”和“提升交付效率”而不是盲目重构。架构师的价值不是把系统做成艺术品而是在正确的时间做正确的事。2. 618日9亿背后的容量与峰值治理2.1 容量评估从拍脑袋到压测闭环618这种大促系统能不能扛住不是上线当天才知道的而是提前两三个月就要做容量评估。早年间我们评估容量靠的是经验公式日常峰值乘以大促系数再加安全冗余。这个方法在业务平稳时勉强够用但大促系数怎么定业务说增长30%运营说增长100%市场说翻两倍最后拍板的人往往取个最大值的折中。现实是大促流量结构跟平时完全不同这个系数根本不具备普适性。后来我们改成“全链路压测拉动容量评估”的方式。每年大促前一个半月组织一次全链路压测用线上真实流量回放加模拟峰值流量混合加压直接打到预发环境或隔离的压测环境。压测的目标不是看系统什么时候挂而是找出每个链路的性能和容量拐点。比如账务数据库在2000 TPS时CPU是40%到4000 TPS时CPU直接顶到95%这个拐点意味着事务冲突开始指数级上升必须提前做参数优化或扩容。容量评估里最容易忽略的是“木桶的短板效应”。交易链路只是入口压测一跑起来发现最先挂掉的往往是那些不起眼的环节——对账文件处理、短信通知服务、消息堆积后的下游消费端。618当天如果用户支付成功但放款通知短信延迟六小时投诉量会直接爆掉这同样是事故。所以容量治理不是只看核心链路要沿着用户请求画完整的调用链图每个节点都要有容量水位监控。2.2 热点冲突锁、队列与最终一致日9亿的交易量意味着峰值每秒数万甚至十万级的并发请求。这个量级下最怕的不是大流量而是流量集中在少数热点上。比如某个爆款商品被秒杀或者某个头部商家集中结算所有请求都去读写同一行数据数据库的行锁竞争会瞬间把TPS打到地板。我们踩过最痛的一个坑是资金账户余额更新的热点冲突。用户支付时扣账户余额一笔交易更新一行看似正常但秒杀场景下几千个并发请求同时扣同一个用户或者同一个商户的余额数据库行锁等待直接拉满事务超时一堆。解决热点冲突常规路子是排队和异步化。我们最终采用的是“异步账务削峰”交易请求进来后先在内存队列里做请求合并和顺序化同一账户的扣款请求被路由到同一个队列由单线程顺序执行写数据库的压力就从几千并发变成串行写。代价是资金账户的余额不再实时更新而是秒级异步更新这就是“最终一致性”在金融场景里最常见的体现。用户感知上支付成功后余额延迟一两秒更新这是可以接受的但系统设计上必须保证这短暂的延迟不会导致超卖、重复支付或者账务不平。这里要特别提醒异步化虽然解决了性能问题却引入了对账复杂度。账务系统从“动一笔账立刻平”变成“异步记账定时对平”这个转变让很多金融背景的同学非常不安。我的经验是异步化之前必须先设计好幂等和冲正机制否则一旦消息丢失或重复消费账目会乱到无法收拾。这个后面专门展开。2.3 多级缓存与数据库保护金融交易系统里缓存是个危险又诱人的东西。诱人在于缓存能把热点数据的查询RT从几十毫秒降到微秒级危险在于一旦缓存失效而流量还在请求会瞬间穿透到数据库形成缓存雪崩。我们618当天就发生过一次差点让核心库挂掉的事故。那次是用户基础信息缓存设置的过期时间是固定的凌晨两点集体失效。偏偏当天凌晨有一次版本发布部分缓存节点重启冷启动后缓存命中率从95%掉到40%大量请求穿透到数据库数据库连接池瞬间被打满最后是靠着限流熔断和手动切流才稳住。事后复盘问题出在“缓存过期策略过于集中”和“数据更新后缓存未主动预热”两个点上。后来我们建立了一套数据库保护的三级机制。第一级是本地缓存JVM内缓存热点数据过期时间随机化避免集体失效第二级是分布式缓存作为本地缓存和数据库之间的缓冲第三级是数据库连接池的动态保护设置最大等待时间超过就直接快速失败绝不无限制等待拖死整个应用。这套机制的核心逻辑是系统可以短暂地让用户看到稍旧的缓存数据但绝不能让数据库在无防护的情况下被流量冲垮。大促期间数据库的健康比数据的实时性重要得多。2.4 削峰填谷的取舍让用户等3秒但不让资金出错618日9亿这个数字如果全部靠“实时同步处理”硬扛需要的机器资源是惊人的。后来我们想明白了一件事交易请求的峰值是客观存在的但很多环节并不需要绝对实时。下单要实时支付要实时但账户记账、积分累计、优惠券核销、对账文件生成这些环节完全可以在峰值过后异步消化。这就叫削峰填谷。我们的做法是消息队列削峰。支付成功后交易系统发送一条“支付成功消息”到消息队列下游记账、通知、营销等系统按自己的消费能力去拉取处理。峰值期间的百万条消息堆积在队列里等流量回落后自然消化。这个方案在技术层面非常成熟难点在于业务层面怎么说服产品和运营接受“结果延迟”——用户支付成功了积分为什么没立刻到账余额为什么还显示旧数字我的处理方式是把“实时反馈”和“最终结果”分开设计。对用户展示层支付成功页立即显示成功状态账户余额展示走异步刷新配合前端轮询两三秒内更新积分、优惠券等非核心资产放到“明细查询”里展示最终结果并明确标注“T1更新”。落地的时候产品经理比架构师更紧张但用户体验数据告诉我们只要核心资金动作扣款、退款、支付结果是实时的用户对非核心资产的延迟感知很弱。架构上的取舍最终都要回到“用户真正关心什么”这个原点。3. 资金安全的“保命设计”一致性、幂等与对账3.1 金融架构的底线账不能错、钱不能多、单不能丢做金融系统这么多年我给团队立过一条铁律你可以让请求慢一点可以让系统短暂降级但绝不能出现账务不平、重复扣款、交易丢单。性能和可用性是架构的颜面资金安全是架构的命。这三条底线落实到技术上对应的分别是强一致或可校验的最终一致、严格的幂等控制、不丢消息的可靠投递。每一条做起来都不轻松但有一条最容易被新人忽略——幂等。日常开发中网络抖动导致客户端重试是最常见的事支付网关超时了用户下意识多点了一下“确认支付”如果系统没有幂等保护同一个订单就可能被扣两次款。这类事故在金融行业属于最高级别的故障处理流程非常痛苦不光要退款还要向监管写报告、向用户道歉、向公司高管解释。幂等设计其实不复杂核心就是一个唯一业务键加一张去重表。比如每一笔支付请求都带上业务订单号账务系统记账前先查去重表如果这个订单号已经处理过就直接返回之前的结果不再重复记账。难点不在实现而在于“所有入口都要做”。支付回调、退款请求、转账操作、账户开立、优惠券发放凡是可能因重试而重复执行的接口都必须做幂等。我在代码评审时看到过太多“这次先不加等有问题再说”的论断这种话在金融系统里就是埋雷。3.2 分布式事务的选型与妥协微服务化之后一个业务操作往往跨越多个服务交易系统扣了用户的款账户系统要加余额积分系统要加积分营销系统要核销优惠券。这些操作要么全部成功要么全部失败这就涉及分布式事务。业内方案很多两阶段提交2PC、TCCTry-Confirm-Cancel、Saga、本地消息表、事务消息。我的选型经验是分场景、分等级。资金类核心操作比如“扣款记账”采用TCC保证强一致非资金类操作比如“支付成功发通知加积分”采用本地消息表加消息队列的最终一致性方案允许秒级延迟。TCC的坑在于空回滚和悬挂问题。Try阶段成功但Confirm阶段失败或者Try阶段超时后Cancel先到这时候如果Cancel处理不当很容易出现资金被错误退回的情况。我们的做法是给每个TCC事务生成全局事务ID所有分支事务都记录事务状态Cancel和Confirm都做幂等校验只有Try成功的分支才允许Cancel绝不盲目反向操作。最终一致性的方案核心是“本地消息表”。业务操作和消息写入放在同一个本地事务里事务提交后通过一个定时任务将消息投递到消息队列消费端处理成功后删除消息。这个方案的优点是不需要消息中间件支持事务消息实现简单可靠缺点是不能无限堆积消费能力必须大于生产速度且消息投递必须有重试和监控。我们618期间消息积压最严重时达到过千万级但因为有本地消息表的可靠性保证最终全部消费干净账目一分不差。3.3 幂等重复请求是最大的隐形炸弹前面提过幂等这里我想展开讲讲实践中的细节。幂等不能只依赖数据库唯一索引因为在分布式环境下高并发时两个请求同时查到去重表都不存在记录然后同时插入唯一索引能防住一个但那个被防住的请求怎么响应这就需要把所有请求的状态收敛到一个“处理中”的中间态。我们最终用的是“状态机唯一索引”的双保险。每个核心单据订单、流水、支付单都有一个状态字段从创建、处理中、成功、失败之间流转。请求进来先查状态处理中就返回“请求已受理”成功就返回成功结果失败则判断是否可以重试。唯一索引作为最后的兜底防止极端并发下状态检查失效。这个设计还有一个隐藏好处它天然支持了交易系统的“查询与操作分离”。用户支付后客户端频繁查询支付状态系统不需要实时去账务系统查询直接从状态字段就能准确回答极大地减轻了下游压力。所以幂等设计做好之后不只是安全性能和用户体验也会提升。3.4 对账体系最后一道防线任何人写的代码都有可能出Bug再严谨的团队也做不到零缺陷所以对账体系是金融系统最后一道防线。对账的本质很简单找两个口径的数据进行比较差异部分触发告警和差错处理。但海量数据场景下对账的设计非常讲究。我们做了三层对账。第一层是实时对账支付成功的交易与资金渠道银行、第三方支付的回调结果做一致性校验数据不一致立即告警常见原因包括渠道延迟、回调丢失第二层是T1批量对账第二天把系统内的交易流水和渠道提供的清算文件逐笔比对金额、笔数、手续费都要平第三层是业务级对账把交易系统的订单、账务系统的流水、会计系统的科目余额三方比对确保从业务数据到财务数据的链路没有任何缝隙。早期T1对账是纯靠批处理跑数几天的数据量就要跑好几个小时。后来改成“分片并行增量对账”之后半小时内能完成全量比对。对账发现的差异必须能自动生成差错单据进入差错处理流程不能只是打个日志让人去查。因为一旦对账告警多了团队会麻木真正严重的问题反而被淹没。我给团队定的目标是对账告警必须做到“每日清零”当天的问题当天消化绝不拖到第二天。4. 压垮系统的往往不是流量稳定性治理与踩坑实录4.1 案例一缓存雪崩引发的数据库连接风暴这个事故发生在一次普通的版本发布后流量并不大却差点让核心库宕机教训极其深刻。现象是发布后十分钟数据库连接数飙升到历史最高值大量交易请求超时系统进入半瘫状态。定位过程很曲折。先看应用日志发现大量查询数据库超时看数据库发现活跃会话数暴涨SQL都是同一个——查询用户基础信息。再查缓存发现缓存命中率只有20%。进一步排查发现版本发布时更新了用户基础信息表的结构缓存服务做了部分节点的滚动重启而代码里的缓存加载逻辑有个Bug缓存Miss时不是先加分布式锁再回源数据库而是所有并发请求都直接穿透到数据库。一次冷启动几千个并发请求同时打到数据库连接池瞬间被打爆。这个事故给了我们两个教训。第一缓存回源必须加“防击穿”设计——单飞模式Singleflight或者分布式锁保证同一时刻只有一个请求回源数据库加载缓存其他请求等待结果。第二发布流程里必须包含“缓存预热”步骤尤其是那些缓存节点需要重启的场景。后来我们把缓存预热做成了发布系统的一个固定环节数据库连接数的尖峰再也没有出现过。4.2 案例二分库分表扩容差点回不了头分库分表是很多高并发系统的必经之路但扩容的过程比想象中危险得多。我们的账务流水表在两年内数据量翻了近十倍原计划单库64张表结果单个库的容量和连接数都告急了需要从单库扩容到四库。一开始我们想用“双写方案”新库和旧库同时写入同步完成后再切流。这个方案听上去稳但实现起来非常复杂——双写期间的任何数据不一致都会导致账目错误而且旧库的历史数据要全量迁移到新库数据量太大迁移时间窗口根本不够。后来我们换成了“灰度迁移在线切换”的方案。核心思路是新库提前建好表结构先把最近一段时间的新数据通过同步工具从旧库实时复制到新库然后通过开关控制流量把一部分用户的读写切到新库验证数据正确后再逐步扩大灰度范围最后全量切到新库旧库保留作为历史查询库只读使用。这个方案的难点是切换期间的“分布式事务”问题。一个用户的数据可能一部分在旧库一部分在新库跨库查询和事务怎么办我们的做法是切换维度按用户分片一个用户的所有数据要么在旧库要么在新库绝不允许一个用户的数据分落在两个库。这个“最小数据单元”的原则非常重要它让数据迁移从“全量混合”变成“按用户分批”每一批都是完整可验证的。整个扩容过程耗时三个月但线上交易完全没停过切换当天零事故。4.3 案例三定时任务重复执行同一个用户被扣了两次款这是一个典型的“分布式环境下的定时任务事故”。我们的还款代扣系统每天凌晨会跑一个定时任务扫描当天应还款的用户并执行扣款。在单机时代一个任务在一台机器上执行天然不会重复。但微服务化之后任务部署在多台机器上如果任务框架没有分布式锁同一时刻两台机器同时扫描同一批用户就会重复扣款。事故就是这么发生的。那是一个周末的凌晨后台监控提示有异常退款操作一查发现是还款任务在五台机器上同时执行了同一个用户的同一笔还款被扣了两次涉及几千个用户。虽然事后全部做了退款处理但这属于最严重的资金安全事故处理成本极高。解决方案很成熟用分布式锁保证任务同时在只有一个节点执行或者用分布式任务调度平台如XXL-Job、Elastic-Job它们天然支持分片和唯一执行。但我要提醒的是技术方案再成熟也要在业务代码里做一层“防重保护”——执行扣款前先查还款记录如果该用户当天已经还款成功就跳过。因为分布式锁可能出现极端情况下的失效比如锁过期、网络分区唯一能兜底的还是业务状态本身。4.4 故障定位三板斧链路、日志、指标在百亿交易的系统里故障定位的难点不是没有数据而是数据太多。一次普通的请求经过网关、交易服务、账务服务、缓存、数据库、消息队列可能产生上百条日志和几十个监控指标。没有高效的排查手段故障半小时都定位不到根因。我的三板斧是全链路追踪、结构化日志、核心指标面板。全链路追踪解决的是“这个请求到底经过了哪些服务、用了多少时间”的问题我们用的是SkyWalking每个请求分配一个TraceId贯穿所有服务的日志结构化日志解决的是“日志能不能被机器高效检索”的问题所有日志统一JSON格式关键是包含TraceId、业务ID、时间戳、服务名核心指标面板解决的是“全局态势判断”的问题把每类服务的QPS、RT、错误率、CPU、内存、数据库连接数放在一个看板上出现异常时先看哪个指标异常再顺着链路往下查。618大促期间我要求所有核心研发都开着这个看板异常出现第一件事不是翻日志而是先看指标定位到具体服务和接口再根据TraceId查日志找具体请求。这套方法在历次大促中救过我们很多次有一次线上出现少量支付超时十分钟内就通过链路追踪定位到一个第三方渠道的HTTP连接池配置过小而不是像以前那样整个团队大海捞针。5. 给后来者的几点真心话5.1 不过度设计架构要为业务留出进化空间做了16年架构我最大的转变是从“技术完美主义”变成“业务适配主义”。现在的我看到那些一上来就要上Service Mesh、要搞单元化、要搞多活的团队都会劝他们冷静。架构不是越复杂越好而是越匹配越好。一个日活几万的系统单体加读写分离就足够一个日活百万的系统微服务加缓存就够只有到了日交易量上亿的规模单元化、多活、分库分表才是刚需。但“不过度设计”不代表“不预留空间”。我的建议是接口设计保持语义化避免把业务逻辑写死到接口签名里数据库设计预留扩展字段避免未来加字段要锁表模块边界按业务域划分避免将来做微服务拆分时要推倒重来。架构演进是常态好的架构不是一步到位设计出来的而是留下了被重构、被演进的空间。5.2 稳定性不是事后灭火是前置投入很多人理解稳定性就是监控、告警、应急预案但真正做过大促系统的人会告诉你稳定性是一个从开发到发布到运行的系统工程。前置投入体现在代码评审要盯住并发、幂等、事务边界测试环境要做故障注入演练而不是只跑功能用例发布要有灰度、有回滚预案、有流量控制运行要有容量水位管理而不是等告警响了再处理。我在团队里推行过一个“稳定性预算”的概念每个迭代周期至少有20%的研发投入在稳定性相关的工作上包括压测、演练、监控完善、技术债务清理。618这种大促不是靠当天通宵盯屏盯出来的而是靠前三个月的稳定性积累。真正的架构师功夫在平时。5.3 新人避坑清单最后给新入行的同学一份实操避坑清单都是我亲眼见过或者亲身踩过的问题任何涉及资金的操作先想“这个接口被调用两次会怎样”再做设计。缓存不是银弹缓存失效、缓存穿透、缓存雪崩的排查成本远高于收益使用前先把三种场景的预案写好。消息队列的消息消费一定要做幂等不要相信“这个消费者只消费一次”的鬼话。数据库连接池的大小不是越大越好默认配置顶多是个参考必须根据实际压测结果调整。写日志要想着“别人能不能看懂”一个好日志的标准是不查代码就能还原完整的请求上下文。发布永远要有回滚方案哪怕你非常有信心没有回滚方案就是裸奔。线上问题处理完毕复盘报告里最重要的不是“怎么修复”而是“为什么这个Bug在开发、测试、Review三道关口都没被发现”。我在这个行业里见过的系统从单体到分布式从集中式到单元化从日交易百万到百亿技术的形态一直在变但最基本的原则从没变过敬畏数据、敬畏资金、敬畏线上。每一次事故背后都是一群人通宵达旦的代价每一行代码背后都是真金白银的信任。这16年教会我的不是怎么设计一个完美的系统而是怎么在系统不完美的情况下让它稳定地扛住一次次的峰值挑战让用户相信钱放在这里是安全的。