
做金融服务的系统和做普通业务系统完全是两码事。这个叫 financial-services 的项目核心不在“功能多”而在“每一笔钱都不能错、每一个请求都能追溯、每一类风险都要挡在门外”。我把这套东西从零搭起来之后最大的感受就是架构设计里省下来的每一分“麻烦”最后都会变成生产事故里加倍奉还的代价。这篇文章就把我当时拆解这个项目、落地核心模块、处理线上问题的完整过程写出来包含踩过的坑和沉淀下来的经验供准备做同类系统的同学参考。1. 整体架构为什么把金融服务拆成六个能力域1.1 从业务动作倒推系统边界拿到“financial-services”这个标题很多人的第一反应是“做一个金融业务平台”。但真正动工之前得先把业务动作列出来用户在开户、用户向账户充钱、用户消费、商户结算、平台对账、异常交易拦截。每一个动作背后对应一类系统能力而不是一张大表、一个大接口。我最终把整个系统拆成六个能力域账户域、支付域、交易域、风控域、合规域、对账域。账户域负责开户、余额、冻结支付域承接充值、消费、退款交易域负责订单、流水、冲正风控域负责实时决策和黑白名单合规域负责实名认证、反洗钱监控和审计日志对账域拉平内部账和外部渠道账。六个域之间通过 API 和异步消息协作互不穿透数据库。这个拆法最直接的好处是故障隔离。支付渠道超时抖动不会拖垮账户查询风控模型升级不需要重启交易服务。坏的影响被挡在单域内这是金融系统最基本也最重要的生存法则。1.2 域间协作核心链路用同步外围逻辑走异步六个域之间的关系不是简单的“微服务调用”要分清主链路和旁路。用户发起支付时下单、锁定余额、扣款、记账这条链路必须走同步调用前后依赖强、状态必须实时可见而发送通知、更新用户画像、清洗行为特征、写审计日志这些环节走异步消息失败可以重试、可以补偿不阻塞主流程。所以我在域间交互上做了双通道设计。强一致性需求走 Feign 同步调用带超时控制和重试非关键路径走 RocketMQ 异步消息按业务类型分 Topic消费失败进入死信队列人工处理。这么做的原因是核心链路的延迟预算有限每一跳不能超过30毫秒异步化是保住这条预算的唯一办法。这里有一个很值得注意的点异步消息不是“丢了也无所谓”的。审计日志走异步但必须用事务消息或者本地消息表保证不丢。我当时在审计环节用了本地消息表方案先写业务数据时同时写一条审计消息到本地库后台任务扫描并投递确认收到后才标记完成。这样既不影响交易性能又不会丢记录。1.3 选型取舍稳定压倒一切新技术靠边站金融服务场景下技术选型的标准跟互联网高并发场景有本质区别。我的优先级是稳定 可观测 性能 开发效率。数据库用的 MySQL 8.0 一主两从缓存用的 Redis 5.0 哨兵模式消息用 RocketMQ 4.9注册中心用 Nacos配置中心也用 Nacos服务框架 Spring Cloud Alibaba。没有引入任何“听上去很酷”的中间件。举一个选型的例子。分库分表当时也纠结过看着用户数会涨听到 ShardingSphere 很成熟但是真实业务量在初期并没有突破单库瓶颈贸然上分库分表只会增加事务一致性的维护成本。所以我的决定是单库起步但表结构自带 tenant_id 和 user_id 路由字段预留水平扩展位在数据库真正到瓶颈前绝不分片。事实证明这个决定很正确省掉了好几个晚上做数据迁移的麻烦。2. 账务与资金安全整个系统的地基工程2.1 复式记账借贷永远平衡一分钱都不能差金融系统最底层的逻辑是复式记账。每一笔资金变动至少涉及两个账户一借一贷金额相等。我在设计账务表结构的时候没有沿用业务系统的流水表而是单独建了账务流水表和账户余额表中间用事务保证强一致。账户余额表的结构大概是acct_id账号、balance可用余额、frozen_balance冻结余额、total_balance总余额等于前两者之和、version乐观锁版本号、update_time。账务流水表则是流水号、记账方向借/贷、发生金额、关联单号、账户编号、记账时间、摘要。用户看余额是一个数但内部永远是三列并存任何一笔业务的入账出账都同时动这些字段。记账时候的关键就是“一借一贷必须出现在同一个数据库事务里”。用户支付100块买家账户贷方减少100商户账户借方增加100这两个操作要么同时成功要么同时回滚不允许出现“扣了用户的钱但商户没到账”的中间态。2.2 事务边界与幂等设计同一个请求来了两次怎么办资金操作最怕重复。网络超时之后客户端重试如果服务端没有幂等保护用户被扣两次钱这种事故足够上行业头条。我在设计阶段就做了一个全局幂等表幂等键由请求方生成通常是“业务类型 渠道 商户订单号”拼接后的哈希值。流程是这样的收到支付请求后先用幂等键去查幂等表存在且状态为成功直接返回上一次的响应结果存在且状态为处理中说明上一个请求还没结束返回“请稍后查询结果”都不存在则插入一条处理中的记录然后进入正式的余额锁定、扣款、记账流程。最终把结果回写幂等表。这里有个细节值得提醒幂等表的插入动作不能使用常规先查询后插入要用数据库唯一索引来兜底。即使两个请求并发到来唯一索引也能保证只有一个插入成功另一个直接报错再从异常分支里走重复查询逻辑。我在上线前专门用压测工具跑过并发重试单机压到 800 QPS 也没有出现重复扣款。2.3 日终对账系统说自己平了不算对完账才算每天凌晨两点定时任务启动日终对账。对账分三个层级内部账务试算平衡、内部订单与账务流水比对、我方渠道交易数据与第三方支付渠道账单比对。内部试算平衡是基础检查总账的借方发生额是否等于贷方发生额所有账户的 balance frozen_balance 是否等于 total_balance。订单与流水比对核对每笔订单的支付金额、状态、流水数是否一致。外部渠道对账最复杂要把我方记录的渠道交易明细和第三方支付机构提供的账单文件逐笔核对核对维度包括渠道交易号、金额、手续费、交易时间、状态。对账处理中我特别在意“单边账”。比如我方订单显示成功但渠道账单里没有这笔记录那就是渠道漏清算或者我方多记了。这种差异需要自动工单化转给财务和清算同学人工介入同时冻结对应的账户余额变动防止资金风险扩散。日终对账类的定时任务本身也必须做执行状态监控任务失败要在10分钟内报警而不是等第二天早上看报表才发现。3. 风控引擎实时决策怎么从规则变成代码3.1 分层规则不是一套规则打天下风控体系如果只做一层规则最后一定会落入“防住了脚本党却误杀了正常用户”的困境。我当时把规则拆成四层第一层是硬性准入规则不满足直接拒绝例如用户未完成实名认证不能交易、账户状态异常不能支付。第二层是反欺诈规则识别行为特征例如同一设备短时间内关联超过5个账号、注册后未完善资料直接大额交易。第三层是额度管控规则例如单笔限额、单日累计限额、单月累计限额主要依据用户的历史交易习惯动态调整。第四层是关联风险规则检测设备、IP、手机号、收货地址这些维度上的关联风险比如某个IP在过去一小时内关联过超过20个账号本身就是一个高危信号。规则引擎我一开始用的 Drools后来发现规则数量上百条以后Drools 的维护成本和性能都不太可控最终换成了自研的 Groovy 脚本规则引擎。每条规则就是一个 Groovy 脚本输入统一的特征上下文输出是否命中、评分和处置建议。好处是规则热加载不用重启服务坏处是脚本要写得非常规范必须做沙箱校验和版本管理。3.2 特征计算规则引擎之前先解决“看得到问题”规则引擎再强拿不到特征就是空转。特征这块我划分成三类用户基础特征、交易行为特征、环境设备特征。用户基础特征包括注册时长、实名状态、历史交易频次交易行为特征包括该用户近5分钟交易笔数、近1小时交易金额、与历史平均值的偏离程度环境特征包括设备ID、IP归属地、GPS位置与常用地址的距离。计算这些特征我用了两层设计。实时性要求高的特征在交易请求内同步计算但上限是每次请求最多计算8个特征超出的部分进异步计算实时性要求低的特征比如“近7天活跃度”用离线任务每十分钟批量刷新到 Redis查询时直接拿。实测这种情况下风控环节的整体耗时可以压在 35ms 以内。特征计算有个常见的坑要特别说特征不是算出来就完了要有稳定性和覆盖率监控。我们有一次新上线一个“设备指纹”特征结果因为SDK兼容问题覆盖率只有60%导致大量请求走了“特征缺失即放行”的兜底分支事后排查发现那段时间欺诈率明显上升。所以特征上线后必须配置覆盖率报表低于95%就要主动告警。3.3 策略编排与灰度新规则先让少量流量体验风控策略的调整不能直接全量上线也尽量不要直接改老规则的阈值最稳妥的方式是“新增规则 观察模式”。我在整个风控引擎里单独做了一个策略编排模块支持给每条规则配置三个动作之一观察、建议、拒绝。观察模式下的规则只记录命中情况和模型打分不干预交易用于确认规则的实际命中率和误报率。建议模式下会标记风险但只返回给业务方参考不阻断。确认效果稳定后才切换成拒绝模式。整个流程走配置管理后台操作无需发版。我记得第一条新规则“新注册1小时内单笔交易超过2000元”在观察模式下跑了两周命中率5.8%误报率低到0.3%才切了拒绝。3.4 实时决策链路最多只能花45毫秒金融交易场景里风控环节不能拖后腿。我当时定的性能目标风控决策全流程不超过45毫秒超过就降级放行绝不让风控成为交易链路的瓶颈。这个原则听上去反直觉但细想就明白——风控再严用户被卡了两次就不会再用产品了。链路设计上走了纯异步编排。交易服务把特征上下文投递到风控Topic风控引擎消费、计算、决策再把结果通过回调方式返回给交易服务。这个设计的好处是不阻塞同步调用但坏处是回调链路变长。最终实现方案是同一笔交易的所有风控特征从Redis批量加载决策结果通过短连接直接回写交易服务的内存缓存由交易服务主动轮询拿到结果。实测全链路平均耗时38ms达标。4. KYC与合规建设实名认证和反洗钱监控那些事4.1 实名认证流程三要素和活体一个都别省合规是金融服务的生命线KYCKnow Your Customer是第一道闸门。用户注册后要完成实名认证才能进行转账、提现、大额支付。我当时接的认证链路分三步身份证OCR识别、公安三要素核验、人脸活体检测。三要素是指姓名、身份证号、手机号活体检测保证“是本人且是活人”。这套流程看起来简单实际落地时坑非常多。OCR识别在弱光环境、证件反光、照片倾斜时准确率会显著下降一定要接多帧质检而不是单帧识别。人脸活体检测要防视频翻拍和面具攻击所以必须选用有对抗能力的服务商不能贪便宜。三要素核验则要注意频控同一个身份证号一天最多发起5次核验超过就要人工介入否则容易被黑产拿来批量验证身份。实名认证的状态也要进风控规则。未实名用户不允许收付款实名状态审核中的用户限额发放已实名的正常放行。实名信息修改是高风险操作修改后24小时内冻结提现功能很多黑产盗号就是先改手机号再改实名信息这个冻结期能卡掉一大批。4.2 异常交易监控规则加模型双跑反洗钱监控不能等到监管发现才处理要提前在系统里建立拦截能力。我搭建的异常交易监控分两条线规则线负责显性异常信号模型线负责隐性可疑行为。规则线的核心是识别几类典型信号单日资金快进快出入账后十分钟内发起等额提现多笔小额拆整交易把一笔大额拆成十几笔小额走不同渠道频繁夜间交易没有合理商业模式短期内更换绑定手机号或设备并转移资金。每条信号配独立规则命中后进入人工复核队列。模型线用的是一套异常评分模型特征维度覆盖交易频次、金额分布、对手方数、渠道偏好、设备更换次数等。模型给每个账户打一个风险分分数超过阈值进入监控名单超过高阈值则限制交易。两条线独立运行、交叉验证规则线负责可解释性模型线负责覆盖规则没定义的隐蔽模式实际效果是可以多发现30%左右的异常交易。4.3 审计日志与数据留存金融系统被问“这笔操作是谁做的、什么时候做的、为什么做”时不能答不上来。我在系统设计里把审计日志当作一等公民不是业务日志的附属品。所有敏感操作都会写审计日志包括登录、开户、实名认证、绑定手机号、修改银行卡、转账、提现、风控规则变更、后台人员操作。审计日志字段包含操作者ID、操作类型、对象ID、请求参数摘要、IP、设备、操作结果、时间戳。日志存到独立库表只追加不修改保留期限至少五年。这里有一个容易被忽视的点审计日志的时序性要保证不能因为异步投递导致后发生的操作先落库。我的做法是每条日志携带服务端统一时钟生成的时间戳并且用自增序列保证插入顺序。事后如果要还原黑产作恶路径时间线反着推就能查清楚这个顺序太重要了。5. 高可用与安全加固上线之后怎么长期活下去5.1 多活与容灾不能把鸡蛋放在一个篮子里金融服务最忌讳单点。我当时部署了同城双可用区两个可用区之间网络专线互联数据库主从跨可用区切换RPO小于5秒。应用层无状态化任何一台机器挂了流量自动切换。具体到部署结构数据库主库放在A可用区从库放在B可用区双可用区之间通过DRDS同步Redis哨兵集群分布在两个可用区RocketMQ的NameServer和Broker同样跨区部署。真实做过一次断网演练模拟A区整体不可用RTO大约在90秒内完成主从切换和服务自动发现消息最多丢失少量延迟消息。金融业务如果能接受秒级中断这个方案已经能覆盖绝大多数故障场景。5.2 限流降级优先保护转账提现砍掉非核心线上故障时最怕全链路雪崩。我在网关层配置了分维度限流按用户维度单用户每秒最多5笔请求按IP维度单IP每秒最多50笔按接口维度核心交易接口单机 QPS 上限为500。超出的流量直接返回“系统繁忙请稍后再试”用高并发场景下的通俗话说就是“让消息排队不让系统排队”。降级策略上我明确区分了核心链路和非核心链路。用户余额查询、转账、提现是核心任何情况下必须保障优惠券展示、营销活动、消息通知是非核心可以被熔断。服务降级用的是Sentinel的熔断规则针对非核心依赖设置慢调用比例超过50%就熔断半分钟。压测数据告诉我只要核心链路不被非核心拖死整体体验就能保住。5.3 密钥与敏感数据保护金融系统里密钥泄露等于裸奔。我做了几层防护数据库密码、第三方支付密钥、短信签名密钥全部存放在 KMS 托管服务启动时通过 KMS SDK 动态获取不写在配置文件里每个服务用独立的服务账号最小权限原则支付回调验签的密钥每年强制轮换两次。用户敏感字段在存储层做了加密。手机号、身份证号、银行卡号这些字段入库时先AES-256加密查询时解密后脱敏展示。这里要注意加密字段会影响查询效率所以我额外建了一张映射表存“加密值的哈希”用于精确匹配查询比如登录时用手机号查用户走哈希索引而不是解密后LIKE实测查询性能几乎无损。敏感数据操作的有账本也很重要。任何后台员工查询用户信息、导出数据都必须走申请审批流程操作行为全量审计。对内要防内鬼对外要防拖库这两头都得堵住。6. 常见问题与排查实录我亲自踩过的几个大坑6.1 高频问题速查表问题现象可能原因排查方法解决方案用户被重复扣款重试请求没有幂等兜底查幂等表是否有重复插入记录幂等键加唯一索引插入冲突时走查询分支跨天对账出现单边账渠道通知丢失或我方状态更新失败对比我方订单与渠道账单明细自动工单人工介入差错处理完成后解冻账户风控规则命中率异常高特征数据覆盖不全导致误判查特征覆盖率报表和命中样本修正特征缺失兜底逻辑调整规则阈值支付回调超时但仍扣款同步调用超时但事务最终成功查订单状态和账务流水最终结果回调幂等超时后主动查单而不是重发指令余额扣减出现负数缺少锁定余额的前置校验查应用日志中扣款前可用余额值扣款前二次校验余额并放弃乐观锁更新审计日志时间顺序错乱异步投递带来的乱序比对多条日志的时间戳和自增序列服务端统一时钟 自增序列保证落库顺序6.2 真实案例掉单问题的完整排错过程上线两周后客服反馈有零星用户“支付成功但订单未更新”。第一反应是消息消费有问题但查看日志发现支付回调已经到达服务端也返回了成功就是订单状态没有变成已支付。排查后发现支付回调处理逻辑里先更新订单表再发送“支付成功”的MQ消息。某个下游消费者处理出现异常消息被重新投递重新投递时又触发了对渠道的验签请求验签接口偶发超时。整条链路看起来只是慢但订单状态更新因为事务回滚一直没成功。问题根因是控制器里对异常的处理不够细把“单笔消息失败”和“系统级失败”混在一起都走重试造成重复验签和回调风暴。修复方案是消息消费失败后先判断错误类型可重试错误最多重试3次间隔递增不可重试错误直接进死信队列由定时任务小时级汇总人工处理。同时在消息处理入口增加了幂等检查有序消费和去重同时保障。修完之后掉单率降到十万分之一以下。6.3 真实案例并发扣款导致余额负数的自救上线初期并发量不大一直没暴露问题。后来做秒杀类营销活动短时间内大量用户同时下单监控报警显示几个账户的余额变成了负数。第一反应是校验逻辑被绕过了后来看代码才发现原因余额扣减用的SQL是“UPDATE account SET balance balance - 100 WHERE acct_id ?”没有带任何条件校验而业务代码里虽然有余额判断但查到扣款之间隔了几十毫秒并发请求全都通过了预检。等发现问题的时候已经把扣款SQL改成了带余额条件的原子更新UPDATE account SET balance balance - 100 WHERE acct_id ? AND balance 100。这个是最快的止血手段。同时还在应用层加了分布式锁同一个账户的扣款操作串行化。后续还把余额字段和扣款流水绑定了事务扣款和记账要么一起成功要么一起回滚从根本上杜绝了负数。这个案例完美解释了为什么金融系统里“先查再扣”都是不安全的所有资金变更必须用带条件的原子SQL完成。6.4 真实案例冷热数据分离单表突破3亿行之后增长到一定量级之后账务流水表单表行数突破了3亿查询性能明显下滑日终对账的耗时从30分钟涨到两小时。最开始考虑过分库分表但评估完成本之后决定优先做冷热分离。方案是把流水表按时间分表比如流水表按月分月表高频查询的近3个月数据保留在热表3个月前的数据迁移到冷库。冷热之间的访问通过一个统一查询服务封装不透出分表逻辑。同时日终对账任务不再跑全量流水改成分批次按小时切片执行部分内容预聚合。最终日终对账耗时降回40分钟以内查询P95从2.5秒降到了120毫秒。这个方案的成本远低于分库分表而且效果立竿见影适合单表中等规模但没有海量并发写入的金融场景。做冷热分离的时候要注意数据迁移的校验迁移后随机抽检几条保证冷库数据没有缺失和错位。6.5 关于监控与告警别等用户骂了才知道系统出了问题我在整套系统里埋了全链路监控。核心指标包括每个接口的QPS、P99延迟、错误率数据库的连接数、慢查询、主从延迟消息队列的堆积量、消费失败数风控决策的命中率和平均耗时日终对账任务的执行状态和耗时。告警阈值不是拍脑袋定的都是压测得出的经验值。比如支付接口P99超过300毫秒就报警数据库主从延迟超过5秒就报警消息堆积量超过10万条就报警风控决策耗时超过45毫秒就报警。告警渠道接了电话和短信规则分级严重级别电话加短信警告级别只短信。大半夜遇到报警电话虽然烦但至少说明系统还活着。真正有用的监控是面向问题域的不是面向指标的。比如“用户支付成功率”这个指标如果降到99.9%就说明已经有用户支付失败了虽然看起来只是0.1%但对于那千分之一的用户来说就是无法挽回的坏体验。所以这个指标的告警阈值我设得比任何技术指标都敏感。写在最后这套 financial-services 系统从头到尾做下来我个人最大的体会是金融系统真正考验人的不是代码能力而是对“资金安全”这四个字有多敬畏。每一次技术选型都要问自己一句“如果这里挂了用户的钱会不会出问题”。为了性能牺牲一致性为了效率跳过风控为了省事不做对账这些念头动一次就是埋一个雷。如果你正准备做类似的项目我的建议很直接先把账务模型和对账机制做扎实再去折腾引擎和算法先把幂等和限额做好再去谈高并发先把审计日志做全再去谈增长。这些看起来“不性感”的模块恰恰是金融服务的铠甲。踩过几次坑之后你会明白这个领域的成就感不在于系统撑住了多少QPS而在于每一天结束时账能对平、钱能算清、风险能看到边界。