晚上八点运维值班的同学电话打进来语气已经有点绷不住了账户服务的数据库连接池被打满核心账务系统CPU持续在99%徘徊营销活动页面的用户越滚越多支付交易开始大面积超时。那晚我们被迫在网关层做了限流活动页面挂出“系统繁忙”的公告内部收到好几个渠道方的投诉工单。也就是从那一刻起我彻底意识到一件事——这家金融机构的老核心真的到了必须动手术的时候。今天想聊的不是“金融科技趋势”这类空泛的东西而是过去两年间我亲身参与的一整套核心账务系统现代化改造。我们把它从传统单体结构拆成分布式的、单元化的服务集群期间踩过账务模型的坑、分布式事务的坑、灰度切换的坑也积累了一些教科书上不会写的实战判断。如果你也在金融服务行业做系统建设无论是银行、支付、消费金融还是保险这篇文章里的大部分问题你都大概率会遇到。1. 传统单体核心打不赢“双11式”洪峰的老底子1.1 一个真实事故营销活动把核心打蒙了先复盘一下开头那场事故因为它的根因非常典型。那天晚上的营销活动其实并不复杂就是消费达标抽红包规则简单、入口醒目用户一进来就疯狂点击“参与抽奖”按钮。抽奖接口不长但它背后要校验账户状态、查询余额、冻结奖品资金、写流水、更新积分一整条链路下来要调用账务核心的十几个接口。问题出在账务核心的数据库上。我们的老核心是典型的集中式架构账户余额就存在一张大表里转账、消费、冻结全都会去更新这一行。并发一上来行锁竞争立刻飙升数据库的锁等待时间从几毫秒膨胀到几百毫秒。更惨的是日终批处理任务在晚上八点半启动批任务扫描全量账户跑利息、计提、归集大量读写操作和联机交易抢同一个数据库的资源。那晚的监控曲线非常难看数据库活跃会话数从几十冲到几百连接池被耗尽应用层开始排队超时又触发重试重试又加重数据库压力典型的雪崩循环。最后我们只能靠人工限流止损但这治标不治本下一次活动、下下一次大促同样的事情还会发生。1.2 单体核心的“稳”和“贵”是一体两面老一辈金融系统的工程师都清楚集中式架构最大的优点就是稳。数据在一个库里事务在本地就能完成ACID资金安全天然可控。这个架构逻辑上极其简洁一次扣款就是一个update语句要么成功要么失败不存在跨节点的一致性问题。问题在于这种“稳”是靠堆硬件换来的。垂直扩容是单体核心唯一的出路CPU不够就加CPU内存不够就加内存磁盘不够就换更大的存储设备。业务量翻一倍你就得买一台更贵的主机业务量再翻一倍整个团队就要开始焦虑这个级别的机器再往上已经很难买了。我见过不少金融机构的核心系统还跑在物理大机上机柜里的设备动辄投资几千万。可这种投资的边际效益越来越低大机再快单库的并发连接数、事务吞吐量都有物理上限到了一定规模钱花了瓶颈还是卡在那里。1.3 三座大山弹性、发版、链路除了容量问题老架构还有三座大山压得开发团队喘不过气。第一是弹性。传统核心的容量是静态的上线前评估好峰值容量买好设备之后就固定了。但金融服务现在具有很强的潮汐效应月初发工资代发、节假日消费高峰、电商大促期间支付渠道冲量流量可能瞬间是平时的十倍。静态架构要么按峰值配资源造成巨大浪费要么干脆不够用没有第三条路。第二是发版。单体核心代码量巨大模块之间耦合极深。改一个存储过程可能影响十几条业务链加一个字段所有下游消费方都得跟着改。我们当时的发版节奏基本是三周一个版本但真正能上线的功能很少大部分时间都花在回归测试上因为“核心系统”三个字本身就意味着不能出错。第三是链路。业务需求是叠加出来的早期加功能都是在一个项目里加几个接口。几年下来一个扣款流程里嵌了几个内部模块调用、几个外部渠道回调谁也说不清。排查一个问题从应用到数据库从账务到清算全靠人肉翻日志链路像一团乱麻。所以当业务方提出要支持“亿级用户规模”“秒级响应”这类目标时我们清楚老核心已经不具备改造的底子了只能重建。2. 账务中心拆分先把“钱”这件事想清楚2.1 从余额表到流水账账务模型重构很多人以为拆核心就是把大单体拆成微服务、把数据库分库分表技术选型定了就能开工。真正动起来才发现最难的从来不是技术而是把“钱”这件事重新想清楚。老核心的账务模型是余额驱动的。账户表里有“余额”字段每次交易直接更新余额查询余额就是查这一行。这个模型简单可靠但有一个致命缺陷并发更新余额必然产生行锁。同一个账户哪怕发生十笔并发交易也得排队一笔一笔更新。新模型我们改成流水驱动、余额为辅。账户余额仍然保留但它是从交易流水计算出来的快照值通过异步对账保证余额和流水最终一致。核心事实是流水每一笔交易都是不可变的一行记录余额只是一个缓存性质的加速查询手段。这个转变听起来简单落地时牵扯到很多细节。比如冻结和扣款要分开处理冻结资金不能直接改余额而是在“可用余额”上扣减同时记录冻结流水真正消费完成后再出账把余额和可用余额一起变。更关键的是所有操作都要能对冲、能冲正这样即使系统异常资金也不会凭空消失。2.2 拆服务先拆业务交易、会计、账户各管一段老核心把交易逻辑、账务逻辑、会计逻辑全揉在一个项目里。拆新系统时我们花了整整一个月做业务建模最终把账务链路拆成了三个核心服务。交易服务负责受理和校验检查账户状态、校验交易参数、判断业务规则它完全不关心资金怎么记账。账务服务才是真记账的地方处理冻结、解冻、扣款、入账维护账户余额和流水。会计引擎单独拆出来负责生成会计分录把每笔业务翻译成借贷两条分录确保会计恒等式永远平。拆完之后最直观的感受是每个服务可以独立发版、独立伸缩、独立配容量。大促前我只扩容账务服务和交易服务没必要把整条链路全量扩一遍。这看似是运维层面的便利其实是整个架构从“单体巨石”走向“可编排积木”的关键一步。2.3 读路径怎么办实时余额缓存与异步对账拆分模型的过程中我们被问得最多的一个问题是余额既然是快照那用户查余额准不准答案是准但要靠机制保证。新系统里余额查询走缓存账户余额变更时同步更新缓存并异步追加流水。正常情况下缓存和数据库是一致的极端情况下缓存抖动、更新丢失我们靠一个对账任务在毫秒级延迟内把差异拉平。对账任务是这套体系里最不能省的部分。它周期性地扫描当天的交易流水重新计算每个账户的应余余额和缓存中的当前余额做差发现不一致就告警并自动修正。我们把这套机制叫作“余额守护”。上线初期它确实抓出过几个bug比如某个扣款场景漏发了缓存更新事件余额显示偏大守护任务在十几秒内就发现并自动纠正了。对用户来说最多是刷新后余额变了资金安全完全不受影响。3. 单元化与数据分片让核心真的能“横着长”3.1 为什么要走到单元化这一步服务拆完之后我们以为问题解决了。但实际上拆分只能解决单机扩容问题解决不了数据层的容量上限。你可以给账务服务开五十个Pod但它后面的数据库还是那一台所有Pod连的是同一个库。连接数、CPU、IO、内存全卡在这一个点上。打个比方拆微服务等于把一个大厨房隔成几个小厨房每个厨房做不同的菜但不分库就等于所有小厨房共用同一个水龙头。局部的并发是分流了水龙头的流量一点没减。真正要“横着长”必须让数据和流量一起分片。这就是单元化架构的核心思路把整个系统复制成N个完全独立的“单元”每个单元只负责一部分客户的数据和请求单元之间尽量不互相调用。3.2 客户维度路由与单元内闭环分片的粒度我们选了客户维度而不是账号维度。这是踩过坑之后的教训。最初我们按账号哈希分片结果发现一个客户开十个账户十个账户可能落在五个不同的单元里。跨单元转账、合并对账单、汇总资产这些操作全都变成分布式事务复杂度直接爆炸。换成客户维度之后一个客户的所有账户天然落在同一个单元。转账操作在这个单元内部就能闭环不需要跨单元协商。对整个系统来说每个单元是一个自包含的迷你核心有自己独立的数据库、缓存、消息队列和应用集群。路由层负责按客户号计算哈希把请求分发到对应单元。我们实现了一个轻量级的路由组件嵌入在网关层根据路径参数里携带的客户号做一致性哈希。一致性哈希的好处是增加单元时只需要迁移少量数据而不是全量重新分布。3.3 分片键选错会怎样一次真实的教训分片键这件事我们曾经差点在错误的方向上一路走到黑。早期方案选择用手机号做分片键理由是用户登录和查询都从手机号开始路由起来很直接。但金融服务有一个特殊属性手机号是会变的。用户更换手机号意味着他的数据归属单元要整体迁移。这个迁移可不只是挪几行数据而是牵连账户、流水、存款、持仓在一大堆表之间按新哈希重新分布稍有不慎就丢数据。我们评估后发现迁移成本实在太高最终改成了客户号分片。客户号由系统生成终身不变天生的稳定路由键。这个教训也验证了一个原则分片键选的不是“访问最频繁的字段”而是“生命周期最稳定的字段”。访问问题可以用缓存解决数据搬家的问题无法用缓存解决。3.4 全局流水号与发号器的设计细节分片之后还面临一个看似小、实际很麻烦的问题流水号怎么生成。以前是单库自增主键简单省事分片后这个方案直接失效。两个单元可能同时生成同一个流水号资金流水一对齐就乱了。我们引入了全局发号器基于数据库号段模式实现发号器预分配一批连续号段给每个单元单元内部从本地号段里取号。现在每个单元不必每次请求都远程取号本地直接递增就行性能几乎没有损耗。同时我们还在流水号里嵌入了单元标识字段光看流水号就能判断这笔交易发生在哪个单元排错时非常有帮助。4. 分布式事务的取舍放弃强一致后的那些事4.1 拆分带来的天然矛盾账务系统拆开之后最让人心里没底的是原本一个本地事务就能搞定的操作现在要跨服务、跨库、跨网络。举一个最简单的场景用户绑定银行卡需要从资金账户里冻结一笔保证金然后在会员账户里增加一笔权益记录。老系统里这两个操作在一个库、一个事务里要么全成功要么全回滚。新系统里资金账户在单元A会员服务在单元B跨库跨服务事务语义被彻底打破。我们面临三条路TCC、可靠消息、SAGA。这套组合拳今天我可以直接告诉你结论没有任何一种方案能覆盖所有场景必须按资金风险等级分类处理。4.2 资金强相关场景TCC是底线涉及真实资金扣减和冻结的场景我们全部采用TCC模式。Try阶段做资源预留冻结资金、锁定额度Confirm阶段真正扣减资金、写入流水Cancel阶段释放冻结、恢复额度。这个模式有一个核心优点每个阶段都是幂等操作配合全局事务ID做防重数据不会重复、不会错乱。实际操作中我们给每个TCC事务加了一张事务控制表记录每个事务的当前状态初始、Try成功、Confirm完成、Cancel完成。任何阶段异常都能从控制表里找到具体状态做人工介入或者自动恢复。TCC的代价是业务侵入性很强。每个参与方都要实现Try、Confirm、Cancel三个方法代码量明显增加。但考虑到资金安全这笔成本必须花。4.3 资金弱相关场景可靠消息是我的默认方案TCC不能覆盖所有场景比如交易成功后要发积分、要推送通知、要异步生成历史明细归档。这些场景如果也走TCC等于为了一个最终能一致的目标牺牲了太多系统性能。我们的默认方案是本地消息表加MQ。核心思路是把业务写入和消息写入放在同一个本地事务里。业务操作成功消息表里就多一条待发送记录业务操作失败消息不会产生。扫表程序轮询待发送消息投递到MQ消费者收到后处理处理成功后回调确认。万一消息发送失败消息表里的记录会一直重试不会丢失。这套方案保障的是最终一致性不保证强一致。它最大的优点是实现成本低、对业务侵入小而且消息表天然具备审计功能出了事还能回溯。4.4 对账兜底无论如何都要有最后防线还有一点必须强调再完善的分布式事务方案也不能保证百分百不丢数据。我们的最终防线是日终对账。每天凌晨三点对账任务从清算文件、交易流水、会计分录三个角度做交叉核对。哪个时间段的交易流水和清算记录对不上就自动触发缺失重放。这一步过去两年里帮我们抓住了三次线上问题其中一次是TCC控制表脏数据导致的资金重复入账如果不是对账兜底问题可能会在月结时才被发现。做分布式账务系统永远要把“即使出了bug也不能丢钱”当作设计底线。对账就是这条底线的重锤。5. 真正难的不是技术是切换5.1 并行运行新老系统必须“双跑对账”新系统开发完成之后真正艰难的工作才刚刚开始。把新老两套核心放在一起跑逐笔对比结果确认新系统的账务处理和老系统完全一致。我们搭建了一套“双跑验证”通道。线上真实的交易流量会同时打到新老两套系统上新系统处理的每一笔结果都不会影响真实资金只用于对比。两边处理完成后一个独立的比对模块按交易流水号逐笔核对交易金额是否一致、余额变动是否一致、会计分录是否一致任何差异都会告警。这个阶段整整跑了三周抓出来十几个差异。大部分是取整规则的细微差别老系统四舍五入新系统银行家舍入差几分钱还有一笔差异是冻结顺序不同导致的余额变动时间点不一致。这些差异在单笔层面微乎其微但如果不提前发现真实切流后会酿成大麻烦。5.2 灰度切流的节奏先读后写、按白名单分批并行验证通过后我开始切真实流量。切流的节奏绝不能一把梭而是按“灰度白名单”分批推进。第一批只切查询流量余额查询、流水查询、账户状态查询全部走新系统。查询只读数据风险最小但能充分验证新系统在真实读写压力下的响应速度。第二批切非资金交易修改手机号、更新联系地址、开通电子账单这类操作不动资金即使出问题影响也可控。第三批才切资金交易而且是按客户白名单小步放开先放一万个内部员工客户观察一周再放内部员工加部分白名单客户观察两周各项指标正常后才逐步全网放开。资金交易的切流周期我们整整走了四十五天这四十五天里每天最紧张的时刻是看对账报表是否有差异告警。5.3 双写与回切切过去了还要能“退”灰度切流进入第三周时我们做了一次完整回切演练。原因是如果新系统运行过程中出现重大事故必须能在短时间内切回老系统而且数据不能丢、资金不能乱。回切的基础是双写。在整个过渡周期内每笔成功交易除了写入新系统还会同步写入老系统的“扩展账务表”。这个扩展表不是老核心原来的账户表而是平滑叠加的旁路数据老系统自身仍然正常运行。这样一旦需要回切老系统的扩展表里已经有完整的交易数据只需要把它合并进老系统的正式账务体系即可。我们演练过两次整个过程大约需要三小时能接受的业务停机时间窗口是四小时刚好卡得过去。5.4 一次回切演练暴露出的脏数据问题第一次回切演练并不顺利。数据合并阶段对账模块发现老系统扩展表里有753笔交易的金额方向反了。排查原因后发现新系统某个转账接口在回写老系统扩展表时把金额序列化的字段顺序搞反了——入账和出账对调了。幸好这只是在老系统的旁路数据里没有影响真实资金。但这个bug如果等到真实回切时才暴露后果不堪设想。所以我的建议是回切演练绝不能在系统稳定后只做一次一定要在切流的不同阶段反复演练而且每次都要故意注入故障验证对账模块能不能及时发现。切流上线后的前三个月我们每一周都做一次完整的回切验证。看似折腾但正是这种重复训练给了运营团队足够的安全感。6. 落地两年后的运维复盘6.1 全链路追踪分布式之后排错靠的是“一根线”单体核心时代排错很简单日志都在一台机器上grep一下就行。分布式之后一次用户请求可能横跨网关、路由、交易服务、账务服务、会计引擎、缓存、MQ七八个节点没有链路追踪几乎寸步难行。我们在全链路接入阶段做了三件事。第一所有入口统一生成TraceId并在HTTP调用、MQ消息、异步任务中透传。第二每个服务在日志中嵌入TraceId日志平台支持按TraceId聚合检索。第三关键路径埋点记录每一步的耗时和状态码。这套体系上线后最大的改变是排查问题的时间从小时级降到分钟级。有一次客户反馈转账成功但积分没到账我用TraceId从网关一路查到积分服务发现是MQ消息消费时序列化失败被静默吞掉了定位到具体代码行整个排查过程不到半小时。6.2 慢SQL治理和大事务拆分分布式系统的性能问题八成以上最后都落在数据库上。我们后期花了大量精力做慢SQL治理。新系统刚上线时慢SQL数量一度很多。排查下来有几类典型问题账务流水表忘记加必要索引按时间范围查询直接全表扫描部分报表查询把大量数据加载到应用内存再过滤还有几个大事务一个事务里处理了几千条流水锁持有时间过长。大事务是金融系统的隐形杀手。它不像慢SQL那样立刻暴露但会在并发高峰期慢慢放大延迟让人误以为只是网络抖动。我们后来给所有事务性接口立了一个规矩单个事务处理的数据量必须控制在两百条以内超了强制拆批。这个约束看起来简单但有效倒逼了业务逻辑的拆解。6.3 混沌工程敢于主动制造故障系统稳定运行半年后我们的信心越来越足但我开始有另一种担心真正的故障从没来来了我们真的能扛住吗于是组织了两次混沌演习。第一次模拟账务服务的一个Pod被kill -9观察系统的自动恢复机制。结果是恢复成功了但引发了重试风暴网关层和交易服务短时间内发起大量重试一下子把账务服务的新Pod打爆。这个教训让我们给所有重试逻辑加了退避策略和最大重试次数限制。第二次模拟缓存集群整体故障观察账户余额查询的降级路径是否正常。结果发现降级逻辑写得有问题一个异常分支竟然走了死循环个别查询接口直接卡死。这两次演习暴露的问题如果等真实故障来检验代价会大得多。混沌工程在金融服务领域听起来像“引狼入室”但它恰恰是让系统真正变稳的最快路径。主动制造可控的故障远好过被动等待不可控的事故。6.4 监控指标不要只看平均延迟要看TP999最后聊聊监控指标。很多团队喜欢拿“平均响应时间”说事但平均值的迷惑性非常大。一次调用耗时10毫秒九十九次耗时50毫秒平均下来49.5毫秒看起来非常健康。可真正影响用户体验的是那百分之一的尾部延迟可能已经达到两秒。我们监控体系的核心指标全部改为TP999和错误率。账务服务、交易服务的TP999必须控制在500毫秒以下超过就触发预警。容量水位监控则是按单元维度做的每个单元的CPU、内存、数据库连接使用率、MQ积压量都单独监控任何一个单元接近阈值都能提前扩容或者切流量。曾经有一次促销活动前监控发现某个单元的数据库连接数使用率达到85%我们提前把该单元的读流量切了一部分到其他单元整个过程业务无感。事后复盘如果按平均延迟看当时数据根本不会触发预警。做完这套改造我的一个强烈感受是金融服务的核心系统真正重要的不只是“技术先进”而是“可预测”。你可以在一天之内把容量预测得准、把故障定位得快、把业务恢复得稳用户感受到的就是安全感和信任感。这套经验说到底也不是银行专属任何处理大规模资金交易的互联网金融机构逻辑都是通的。
阅读完成 · 觉得有帮助?