ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

金融级服务架构实战:分布式事务、高可用与可靠性设计

金融级服务架构实战:分布式事务、高可用与可靠性设计 1. 一个凌晨两点的告警电话金融级服务到底难在哪做金融服务的同学应该都有过这种经历凌晨两点手机震动不是闹钟是告警群里的消息。第一次遇到这种事的人可能会慌经历过几次之后你才会明白告警本身不吓人吓人的是你不知道这个告警背后藏的是什么——是流量突增是代码 bug是依赖的基础组件抖动还是某个你没考虑到的边界条件被触发我在做金融服务这个方向之前一直觉得所谓金融级就是个营销词无非是加个加密、走个证书、数据库用 Oracle。真正深入进去之后才知道这想法幼稚得离谱。金融服务的难点不在于某一个单一技术点而在于它是一个全链路可靠性的问题从你接收请求的那一刻起到资金状态落库再到对账、结算、报表、审计每个环节都不能出错而且是在高并发、大数据量、极端场景下都不能出错。我参与过的这一类项目核心链路大概长这样用户发起一笔交易系统先做风控校验再做账户余额检查然后冻结资金、执行交易、更新余额、记录流水最后还要异步对账。这中间每一步失败怎么处理、部分成功怎么处理、重复请求怎么处理都是要命的细节。这篇文章我就从实战角度聊聊做金融服务绕不开的几个核心问题分布式事务与幂等、高可用架构、容量规划与压测、安全合规落地、可观测性与故障应急。每个问题我都会讲清楚底层逻辑和容易踩的坑希望能给准备入这行的同学一些参考。2. 资金流转的根本底线分布式事务与幂等设计2.1 分布式事务方案选型的现实考量金融服务的核心是资金流转而资金流转一定涉及多个系统或者多个数据分片之间的状态变动。比如用户从银行卡充值到平台钱包这个操作至少涉及三方支付渠道侧、平台账户侧、交易流水侧。任何一方成功而另一方失败都会造成资金不一致。我见过不少团队在分布式事务方案上纠结拿 TCC、SAGA、本地消息表各种对比。这里我分享一下自己的判断逻辑如果交易链路在同一个服务内部优先用本地事务别为了分布式而分布式。如果跨服务但可以接受最终一致性优先考虑本地消息表或事务消息尽量避免强一致性的 TCC因为 TCC 的 Confirm 和 Cancel 逻辑复杂度非常高业务侵入性极强开发和测试成本都很大。如果确实需要实时强一致比如账户余额扣减和积分变动再考虑 TCC但要做好补偿逻辑的精心设计。我倾向于把资金操作收敛到一个独立的账务服务里这个服务内部用本地事务保证强一致对外通过接口暴露能力。其他业务服务通过异步消息或事务消息跟账务服务交互。这样既减少了分布式事务的使用面又让核心资金链路的可控性大大增强。2.2 幂等设计高并发下不能靠运气幂等是金融服务最容易被忽视却最重要的设计。简单说同一个请求无论被客户端提交多少次服务端处理的结果都必须一致——不能因为用户手抖点了两次确认支付就真的扣两次钱。实现幂等有几个层次第一层是接口层的幂等。通常靠请求方传入的业务流水号bizSeqNo实现。服务端收到请求后先去幂等表查一下这个流水号是否已经存在。存在就直接返回原结果不存在就插入流水号并执行后续逻辑。这里要注意的是先查后插存在并发问题两个相同请求同时进来可能都查不到。所以需要在数据库层面给流水号加唯一约束插入失败说明请求已处理过直接查询并返回已有结果即可。下面是一个我用得很顺手的幂等实现结构public class IdempotentHandler { private final IdempotentRepository repository; private final TransactionTemplate transactionTemplate; public BizResult handle(String bizSeqNo, SupplierBizResult action) { // 1. 尝试占位利用数据库唯一索引保证并发安全 boolean occupied repository.tryOccupy(bizSeqNo); if (!occupied) { // 重复请求直接返回之前的结果 return repository.queryResult(bizSeqNo); } // 2. 执行业务逻辑和占位操作在同一事务内 try { return transactionTemplate.execute(status - { BizResult result action.get(); repository.saveResult(bizSeqNo, result); return result; }); } catch (Exception e) { // 标记失败允许重试 repository.markFailed(bizSeqNo, e.getMessage()); throw e; } } }第二层是账务层的幂等。账户余额的增加和扣减操作本身要携带原始凭证号比如支付单号在更新余额的 SQL 里带上条件where account_id ? and not exists (select 1 from account_flow where ref_no ?)。这个 SQL 会走得比较重但它是资金安全的底裤不能省。第三层是异步状态的幂等。回调通知可能到达多次所以状态机的状态流转要设计成可重复执行的。比如交易状态从处理中到成功的转换要允许在已是成功时重复接收成功回调而不会报错。2.3 对账体系最后一道保险即便把事务和幂等都做到位也依然有可能出现数据库没报错但业务逻辑错了的情况——比如某个回调被错误地处理了两次或者代码判断条件写反了。所以金融服务必须有对账体系这是最后一道保险。日常对账的做法是每天定时任务拉取支付渠道的账单文件跟平台内部的交易流水逐笔比对找出以下三类差异差异类型典型原因处理方案平台有记录、渠道没有请求在渠道侧失败但回调未正确通知发起冲正并通知用户渠道有记录、平台没有回调丢失导致平台侧漏单补偿入账并补齐流水金额不一致手续费、优惠券计算偏差人工介入调查对账体系的建设优先级其实应该比分布式事务更高——因为分布式事务是在事中避免不一致而对账是在事后兜底。没有对账体系的金融服务就像开车不系安全带不出事则已出事就是大事。3. 高可用架构不要把高可用做成PPT上的词3.1 同城双活与多AZ部署金融服务的可用性要求通常是很苛刻的核心链路的可用性目标往往定在 99.99% 以上。这意味着全年不可用时间不能超过 52.6 分钟。很多团队嘴上说着高可用实际架构却是一台数据库、一个应用实例、没有做跨机房容灾这显然是不合格的。在可用性方案上我比较推荐的方式是同城双活架构两个可用区AZ同时承担流量任何一个 AZ 故障另一个 AZ 可以独立承接全部流量。数据库层面可以采用主备模式正常情况下主库在 AZ1备库实时同步到 AZ2。AZ1 故障时自动切换让 AZ2 的备库接管服务。但有个关键细节多活不只是部署多份就行了。数据分片、缓存、会话、定时任务全部都要做跨 AZ 的冗余和同步。比如定时任务如果两台服务器都启动就可能重复执行——所以分布式锁、任务调度框架如 xxl-job、ElasticJob这些基础设施必须提前规划好。3.2 冗余不等于高可用切换演练才是这是我最想强调的一点。很多团队搭了冗余架构以为就高可用了。但真正的可靠性是在故障发生时系统能自动恢复、流量能平滑切换而不是靠人工去改 DNS、改配置。没有演练过的高可用架构跟没有高可用没有任何区别。我们当时做了一套混沌演练机制每个月选择一个业务低峰期主动杀掉一个可用区的所有应用实例观察系统是否能够自动恢复、依赖的组件是否能够降级、流量是否会自动切换到另一个可用区。前两次演练是非常狼狈的暴露了一堆平时根本想不到的问题比如某个内部 RPC 的超时时间设置过长导致上游线程池被打满比如某个缓存 key 没有跨 AZ 同步切换后大量请求击穿缓存打到数据库比如某个分布式调度任务的 leader 节点被杀了但 follower 没有正确接管。这些问题如果等到真实故障时才暴露那就是生产线事故。演练的意义正在于此——在可控的范围内制造故障让问题暴露在造成实际影响之前。3.3 降级策略与业务开关降级是高可用架构里的重要一环。像账务查询、流水明细这类非核心功能在系统压力过大时可以选择降级将资源让给交易核心链路。但降级需要有明确的策略可降级的服务需要提前梳理出来并制定具体的降级阈值指标比如当交易系统 CPU 使用率超过 80% 或 RT 超过 1 秒时自动降级非核心功能。降级开关需要做成配置中心里的一个开关动一下配置就能生效而不是要重新发版。降级的粒度要足够细。比如账务服务可以降级查询类接口但不能降级交易类接口可以降级积分服务但不能降级支付服务。降级不是一刀切地关掉所有非核心功能而是要精细设计。有些团队做了降级开关但开关粒度太粗一降级把所有非核心服务全部关掉结果用户登录都登不上反而引发更大的问题。我的建议是把服务按重要性分成 P0/P1/P2 三个等级降级只降 P1 和 P2。4. 容量规划与压测先于流量一步发现问题4.1 压测不能只在测试环境做金融服务的流量波动常常是脉冲式的——比如秒杀活动、发工资日、节假日转账高峰期。高峰期流量可能是平日的几十倍甚至上百倍。如果容量规划做得不好一到高峰期系统就会打满要么响应变慢要么直接拒绝服务。压测是容量规划的基础。我见过很多团队做压测只把测试环境跑一遍得出一个所谓QPS 能达到 5000的结论就结束了。这完全不够因为测试环境的数据量、机器配置、网络环境都和生产环境不一致压出来的数据往往没有参考价值。比较靠谱的做法是在生产环境做压测但前提是消息要隔离好——压测流量要打上特殊标记不能真正去操作资金账户。可以准备一批专门的压测账户和压测商品不走真实风控规则用影子表存储压测数据压完之后清理掉。我当时做压力测试就是靠这样的方式在生产环境把核心链路压到了接近极限踩到了一些在测试环境完全发现不了的问题数据库连接池在达到某个并发度后会出现获取连接超时必须调整连接池参数。某个内部服务的线程池隔离没有做导致一个慢接口把所有线程都占住了。某些热点账户的数据库行锁冲突严重导致大量更新操作用户等待。4.2 限流阈值怎么定的实操思路限流是为了保护系统不被突发流量击穿。但限流阈值的设定是有讲究的——设得过低会误杀正常流量设得过高压测又没意义。设阈值之前要摸清楚系统的真实处理能力上限。通过压测得出核心应用的单机 QPS 上限之后乘以机器数得出集群总容量再乘以 0.7~0.8 的冗余系数作为限流阈值。比如单机上限是 1000 QPS集群有 10 台机器那么集群总容量是 10000 QPS限流阈值就应该设在 7000~8000 QPS预留一部分空间应对流量毛刺和个别机器宕机的情况。在实践中限流不能只做接口级别的全局限流建议结合业务场景做精细化的配额管理按接口重要接口的限流阈值可以单独设置。按用户比如登录接口、短信发送接口防止单个用户刷接口。按来源不同的调用方App端、H5端、开放平台API分别设置配额。限流算法上我比较推荐滑动窗口或令牌桶。滑动窗口能更好地应对突刺流量避免固定窗口在临界点出现双倍流量的问题。实现上可以直接用现成的组件比如 Sentinel、Resilience4j不建议自己从零写限流算法。4.3 容量规划的年度预算逻辑容量规划不能等到系统快扛不住了再扩容。比较务实的做法是建立容量预算的机制每年做一次业务增长预估比如明年交易量预计增长 50%那么核心应用的机器数、数据库实例数、缓存集群容量都要按这个预估提前扩好。这里要说一下最容易被忽略的存储容量。相比应用层可以快速扩容数据库的扩容要麻烦得多。尤其当数据量达到一定规模之后单库单表根本无法支撑必须提前做分库分表。分库分表的数量不是随意定的要根据未来三年的数据增长量来算。比如日流水量 1000 万条每条流水大概 500 字节一天就是 5GB一年就是 1.8TB加上索引和冗余实际存储可能要 4TB 以上。这样推算下来分 64 个库表每个表大约 64GB才算比较稳妥。容量规划要形成文档并定期复盘。每次大促活动结束后把实际峰值和预估进行对比找出偏差原因校准下一轮的估算系数。5. 安全与合规贯穿研发流程的工程实践5.1 密钥管理与加密体系金融领域的数据安全要求非常严格尤其是涉及用户敏感信息手机号、身份证号、银行卡号和资金数据。这里先说一个我踩过的坑早期我们为了图方便把加密密钥写死在配置文件里后来做安全审计时被明确指出这是严重违规。即使代码仓库是私有的密钥也不能以明文形式存在于任何配置文件中。正确的做法是通过专门的密钥管理系统KMS来管理密钥。应用启动时从 KMS 获取密钥密钥的轮转由 KMS 统一调度应用侧无感知。数据库存储敏感字段时需要加密但不能用同一个密钥加密所有字段——建议做密钥分级主密钥加密数据密钥数据密钥才用于具体的字段加密。这样即使某个数据密钥泄露了也能快速单独轮转不需要全量解密重加密。加密算法的选择也要注意老旧的算法如 MD5、SHA1不能用于安全敏感场景密码存储要使用带盐的 bcrypt 或 PBKDF2。数据库传输链路要启用 TLS 加密防止在传输层被抓包。这些在金融安全规范里都是底线要求。5.2 风控规则引擎金融服务离不开风控。交易金额异常、频率异常、设备异常、地理位置突变这些场景都靠风控系统识别拦截。风控规则引擎的设计要点是规则和代码分离——业务人员要能通过配置界面调整规则而不是每次改规则都要重新发版。我当时参与设计的规则引擎包含两层逻辑第一层是实时规则跑在交易链路上。比如单笔交易金额上限、单日累计交易次数、新设备首笔大额交易拦截等。这些规则用简单的条件表达式即可描述但要注意规则量大了以后性能会下降建议加上规则索引和缓存。第二层是离线模型跑在批处理链路里。通过历史数据挖掘用户的交易行为模式识别团伙欺诈。离线模型通常依赖大数据平台做特征计算输出结果同步到在线风控缓存中供实时规则调用。风控系统最怕的是误杀——明明是好用户却被拦截体验会很差。所以风控规则一定要配置白名单机制并且每次命中拦截规则都要记录留痕命中规则、拦截原因、用户申诉入口方便判定是否误伤。5.3 日志与审计金融服务的日志记录核心目的不是排查问题而是发生问题后能还原全过程。交易链路的每一步操作谁在什么时间做了什么操作、参数是什么、结果是什么都必须记录在案且日志不能被篡改。日志审计实践中要注意的点操作日志和业务流水要分开存储操作日志可以存到 ES 或专门日志平台业务流水必须入数据库。日志要包含完整的 traceId方便从入口请求一路追踪到最底层的数据库操作。对于用户敏感信息日志里要脱敏存储不能把明文手机号、身份证号打进日志。日志保留时间要满足审计要求建议至少保留半年配合定期归档。对金融系统来说日志不仅是为了自己排查问题也是在应对监管审查或用户投诉时拿出证据链证明系统处理过程没有问题的重要依据。在没出问题时你可能觉得日志没什么用一旦出现资金争议日志就是你唯一的护身符。6. 可观测性与应急响应让系统开口说话6.1 全链路追踪与黄金指标金融服务链路复杂一次请求要经过网关、风控、账户、交易、通知等多个服务。当这条链路变慢或者出错时如果不能快速定位是哪一环出了问题排查效率会非常低。可观测性的建设我建议从三个维度入手指标Metrics、日志Logs、追踪Traces。指标方面重点关注黄金四指标延迟Latency、流量Traffic、错误Errors、饱和度Saturation。每个服务都要暴露这些指标并且设置合理的告警阈值。延迟要区分平均延迟和 P99 延迟——平均延迟掩盖了长尾问题P99 才是真正需要关注的指标。追踪方面使用 OpenTelemetry 作为统一标准每个服务在入口处生成 traceId通过 HTTP header 或消息队列的消息头传递下去。这样从用户发起请求到最后的账务落库所有环节的日志都能通过 traceId 串联起来。出了问题查一下 traceId就能看到全链路的调用关系和各环节耗时。还需要强调的是可观测性不是搭好了工具就完事。指标面板要经常看告警规则要根据实际情况迭代调整。我见过不少团队搭了 Prometheus 和 Grafana但面板一个月没人打开一次告警配置也从不维护结果工具成了摆设。6.2 应急预案与故障复盘任何时候都要有应急预案而且要定期演练。应急预案不是写文档交差而是要具体到某人负责什么操作、在什么系统上执行什么命令、预计多长时间恢复。每个核心系统的负责人必须对自己负责的系统的应急流程烂熟于心。应急处理的原则我总结成一句话先恢复后定位再复盘。发生故障时第一优先级是尽快恢复服务而不是现场调试找原因。如果重启可以快速恢复就果断重启如果切换流量可以缓解就立刻切。等系统稳定了再冷静分析根因。很多事故之所以从小问题变成大故障就是因为处理人员想顺便把问题找出来结果越搞越乱恢复时间不断延长。故障复盘的时候重点不是谁做错了什么而是流程上有什么漏洞、机制上有什么不足。一个故障往往是多重因素叠加的结果可能是配置不规范导致的可能是监控缺失导致的可能是代码审查遗漏了边界情况导致的。复盘要做的就是找出这些因素逐一补齐防护措施防止同类事件再次发生。我在实践中还有一个体会故障复盘报告写完后要确保改进措施真正落地。约定一个时间节点回头检查监控是否补上了、代码是否修复了、演练是否执行了。没有闭环的复盘等于白复盘。金融服务的工程实践远不止我上面列的这些还有资金对账、单元化架构、灰度发布与回滚、多活容灾等等太多内容。但核心逻辑是清晰的做金融服务本质上是和不确定性做对抗。你能做的就是在每一个环节都把可靠性设计到极致然后在系统真正出问题的时候做到有预案、能快速恢复、事后有复盘。这条路没有捷径只有不断打磨每一个细节才能让系统在极端情况下依然值得信赖。
返回列表