如果你在金融科技公司待过,大概率见过一个叫financial-services的项目仓库。名字起得非常大,但凡是做券商、银行、支付、理财代销、信贷核心这类业务的团队,迟早都会在某个版本迭代里碰到它。这个项目通常不是某一个独立应用,而是一组面向金融业务的服务集群:账户、交易、产品、清算、渠道网关、风控、通知,全部被塞进同一个大仓库里,由不同小组维护,互相之间通过 RPC 或消息队列协作。
这篇文章我想用一次真实落地的视角,聊聊这类金融级服务项目到底该怎么设计、怎么拆、怎么保证资金安全,以及那些线上踩过的坑。内容偏向后端架构和核心交易链路,适合在金融行业做开发、架构的同学,也适合准备从互联网电商转金融科技的人。看完之后你会发现,金融系统真正难的从来不是高并发,而是"每一笔账都不能错"这件事。
1. 项目整体设计与思路拆解
1.1 金融级服务到底"级"在哪里
很多人第一次接触金融项目时,会觉得技术上跟电商没什么区别:都是 Spring Cloud、Dubbo、Redis、Kafka 那一套。但真正动手做账务、做支付、做清结算时,才会意识到约束条件完全不一样。
电商系统允许超卖,最坏情况是发不出货,补个补偿券就过去了。但金融系统里,余额多扣一分钱、转账重复执行一次、冻结金额对不上,都是生产事故,严重一点直接触发资损。所以"金融级"这三个字,核心体现在四个词上:资金安全、强一致、可追溯、合规留痕。
这是设计层面最大的分水岭。电商可以把"库存扣减"做成异步最终一致,因为用户可以接受"下单成功但稍后确认库存"。但金融的扣款、冻结、解冻、清分,每一笔都必须有前置校验、过程留痕、事后可对账。哪怕性能慢一点,也不能让资金状态处于"不确定"。
我见过最典型的反面案例是:某个团队把余额存在 Redis 里,先扣缓存再异步落库,结果 Redis 抖动导致扣款丢失,日终对账发现少了 18 万。从那天起,项目组定了一条铁律:余额类数据禁止以缓存为准,缓存只能做读加速,一切以数据库流水为唯一事实源。这条规矩,后面所有金融项目都沿用了。
1.2 拆分:谁该是一个独立服务
financial-services 这种仓库,最容易出现的病就是"上帝服务":一个订单服务里既管用户、又管账户、又管产品配置、又管渠道对接,代码几千个文件,部署包 200MB 起步。
正确的拆法,不是按页面功能拆,而是按领域边界拆。我这边最终落地的拆分如下:
| 服务名 | 核心职责 | 核心数据 | 谁在调用它 |
|---|---|---|---|
| 用户中心 | 注册、实名认证(KYC)、登录态、客户分层 | 客户表、证件表、风险等级表 | App、开放平台 |
| 账户中心 | 账户开立、余额查询、冻结/解冻、记账 | 账户表、流水表、冻结明细表 | 交易中心、清结算中心 |
| 产品中心 | 产品上下架、收益率配置、净值查询、份额规则 | 产品表、产品份额表、净值表 | 渠道端、交易中心 |
| 交易中心 | 申购、赎回、转入、转出等交易编排 | 交易订单表、持仓变动表 | App、柜台、开放API |
| 渠道网关 | 对接银行、第三方支付、银证转账等外部渠道 | 渠道配置表、渠道报文日志表 | 交易中心、支付服务 |
| 清结算中心 | T+1 对账、日切、资金划拨、差错处理 | 对账批次表、差异流水表、清算结果表 | 财务系统、总账系统 |
| 风控中心 | 实名校验、反欺诈规则、限额管理、黑白名单 | 风控规则表、命中记录表、限额配置表 | 交易中心、账户中心 |
| 通知中心 | 短信、Push、站内信、邮件等触达 | 通知任务表、渠道回执表 | 交易中心、用户中心 |
拆完之后最重要的一条规则是:跨服务的数据一律通过接口或消息获取,禁止直连对方的数据库。账户中心的库只有账户中心能写,交易中心需要扣余额时必须调用账户中心的 API,由账户中心保证流水和余额的一致性。
这样拆表面上看多了一次网络调用,实际上是把"资金安全"的职责收敛到了一个团队手里。不然每个业务方都自己改余额,最后账一定平不了。
1.3 为什么不能直接照搬互联网电商那套
这里不是否定电商架构,而是强调一个事实:电商的很多"高级玩法",在金融场景里是要打折扣的。
异步化要谨慎。电商下单后,订单状态可以异步流转,支付回调晚几分钟无感。但金融里,一个申购请求发出后,如果接口没有明确返回"成功"或"失败",用户端就会显示"处理中",客服电话立刻被打爆。所以关键写操作要么同步返回明确结果,要么通过可靠消息保证最终可查。
缓存的使用要分级。商品详情、产品收益率列表这种只读展示数据,可以放心上 Redis,缓存过期了再回源。但账户余额、可用额度、冻结金额这类资金状态,绝对不能只走缓存,必须每次读库或至少做到缓存与数据库强一致的双读。
分布式事务要克制。两阶段提交(2PC)在金融圈不是没人用,但性能代价太大,协调者本身还容易成为单点。更多团队宁可把一个大事务拆成多个本地事务,配上事务消息和定期对账兜底,用工程手段保证"最终一致但每步可靠"。这个思路下一节详细展开。
2. 核心技术选型与架构落地
2.1 账务写入:本地事务 + 事务消息,而不是两阶段提交
核心交易链路上,我推荐的是"本地事务 + 事务消息(Transactional Outbox)"模式。它比 2PC 轻量,又比裸发 MQ 可靠。
你可以把它理解成寄挂号信:你要给下游发消息,不是直接塞进邮筒就完事,而是先把信的内容和收件人登记在一个"发件登记本"上,和你的业务操作放在同一个事务里提交。提交成功之后,后台有专门的投递员照着登记本去寄信,寄完再在登记本上打个勾。
落到代码上就是这样的流程:
- 账户中心在扣款事务里,同时写余额流水和一条 outbox 消息记录。
- 事务提交后,一个定时任务扫 outbox 表,把状态为"待发送"的记录投递到 Kafka。
- 下游消费成功后在 outbox 表标记"已发送"。
- 如果投递失败,任务会重试;如果重试次数超限,则进人工告警。
对应的表设计长这样:
CREATE TABLE `t_account_flow` ( `id` bigint NOT NULL AUTO_INCREMENT, `account_no` varchar(32) NOT NULL COMMENT '账号', `flow_no` varchar(64) NOT NULL COMMENT '全局唯一流水号', `change_type` tinyint NOT NULL COMMENT '变动类型:1加钱 2扣钱 3冻结 4解冻', `amount` decimal(20,4) NOT NULL, `balance_after` decimal(20,4) NOT NULL, `create_time` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_flow_no` (`flow_no`), KEY `idx_account_time` (`account_no`, `create_time`) ) COMMENT '账户流水表'; CREATE TABLE `t_outbox_message` ( `id` bigint NOT NULL AUTO_INCREMENT, `biz_type` varchar(32) NOT NULL COMMENT '业务类型', `biz_id` varchar(64) NOT NULL COMMENT '业务单号', `payload` text NOT NULL COMMENT '消息内容(JSON)', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待发送 1已发送 2失败', `retry_count` int NOT NULL DEFAULT '0', `next_retry_time` datetime NOT NULL, `create_time` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_status_time` (`status`, `next_retry_time`) ) COMMENT '事务消息表';这里的核心是:t_account_flow和t_outbox_message的写入必须在一个数据库事务里。这样就不会出现"账扣了但消息没发"或"消息发了但账没扣"的中间状态。下游哪怕延迟消费,最终也能查得到。
2.2 幂等与去重:宁可错杀,不可漏杀
金融系统里,"重复扣款"比"扣款失败"严重得多。扣款失败顶多让用户重试,重复扣款则直接引发投诉和资损。
任何可能被重试的接口都必须做幂等。幂等的实现不能只靠业务代码里判断一下"订单状态是不是已支付",因为并发请求下两个线程可能同时通过判断,然后同时去扣钱。正确的做法是数据库唯一约束兜底。
我常用的幂等表设计如下:
CREATE TABLE `t_idempotent_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `idempotent_key` varchar(128) NOT NULL COMMENT '幂等键,通常是 渠道号+商户单号+业务类型', `biz_type` varchar(32) NOT NULL, `request_body` text COMMENT '首次请求报文', `response_body` text COMMENT '首次响应报文', `status` tinyint NOT NULL COMMENT '1处理中 2成功', `create_time` datetime NOT NULL, `update_time` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_idempotent_key` (`idempotent_key`) ) COMMENT '幂等记录表';流程是:请求进来,先 insert 一条幂等记录,status=1。如果 insert 报唯一键冲突,说明之前来过,直接查旧记录返回旧响应。如果 insert 成功,再执行真正的账务逻辑,执行完成后 update status=2 并保存响应。
注意这里有个并发细节:insert 幂等记录和执行业务逻辑不在同一个事务里也没关系,因为唯一键已经挡住了并发。但插入幂等记录之后、业务逻辑执行完成之前,如果有重复请求进来,它查到的是"处理中"状态。此时不要直接报错,可以返回一个"重复请求"的提示或短暂等待后重查。我们生产上是把这种情况转成"处理中请稍后"给调用方,并配合 Redis SETNX 做几秒内的快速拦截,减少数据库压力。
幂等键的生成规则也很关键。每个渠道的商户单号可能不全局唯一,所以幂等键必须是"渠道编号 + 商户单号 + 业务动作"的组合。我们曾经吃过亏:同一个商户单号,用户在 App 端下单和客服后台代下单是同一笔订单,但动作不同,如果幂等键不区分动作,就会把两笔合法操作误判成重复请求。
2.3 账户模型与资金安全的最小规则集
账户中心是 financial-services 的心脏,它的数据模型直接决定了系统能不能做到"一分不差"。我梳理了五个必须遵守的规则:
第一,必须使用复式记账。单式记账只记录"钱少了多少",复式记账会同时记录"哪个账户少了、哪个账户多了"。比如用户申购理财产品,一边扣银行卡账户余额,一边增产品持仓。只有两边同时入账,日终对账才有抓手。哪怕你的系统不是严格意义上的会计系统,也要保证每一笔资金变动都有借贷双方。
第二,余额只能通过记账流水来变更。禁止直接执行UPDATE t_account SET balance = balance - 100。正确的姿势是先插入流水,再更新余额,二者在同一事务里,余额更新语句带乐观锁版本号。这样万一并发出错,流水是完整的,可以追溯并重建余额。
-- 扣款:先插流水 INSERT INTO t_account_flow (account_no, flow_no, change_type, amount, balance_after, create_time) VALUES ('10001', 'F20250101120000123', 2, 100.00, 0, NOW()); -- 再更新余额,version 是乐观锁 UPDATE t_account SET balance = balance - 100, version = version + 1 WHERE account_no = '10001' AND version = 1;如果 update 影响行数为 0,说明版本冲突,整个事务回滚,流水也会一起消失。
第三,账户余额必须拆成四个口径:可用余额、冻结余额、在途金额、总余额。用户发起一笔支付时,先冻结 100 元,可用余额减少、冻结余额增加;等渠道确认成功后再解冻并扣减。如果渠道确认失败,则解冻回滚。这四个字段分开记录,能清晰表达资金在每一刻的状态,也方便日终核对。
第四,流水号全局唯一,禁止复用。我们用的是自研雪花 ID 服务。不推荐直接用数据库自增 ID 当流水号,因为多库多表场景下会撞;也不推荐纯随机 UUID,因为太长,索引性能差,而且没有时间语义,排查问题时没法快速判断先后顺序。
第五,流水不可删除、不可修改。哪怕账记错了,也只能做一笔红字冲正或反向流水,把错误路径保留下来。这是审计的硬要求,也是后期排查的救命稻草。
3. 实操过程与核心环节实现
3.1 一条支付交易的完整链路,从下单到对账
把上面的设计串起来,看一笔典型的购买理财支付交易怎么走。假设用户通过 App 购买了 10000 元的某理财产品,实际执行链路如下:
- 交易中心收到下单请求,先做参数校验与风控预检。
- 交易中心调账户中心"冻结余额"接口,账户中心插入冻结流水并更新可用余额、冻结余额。
- 交易中心落交易订单,状态为"待支付",同时向渠道网关发起扣款指令。
- 渠道网关调用外部支付渠道,拿到渠道受理号并异步等待回调。
- 渠道支付成功后回调渠道网关,网关验签通过后通知交易中心"支付成功"。
- 交易中心调账户中心"确认扣款"接口,把冻结余额转为实际扣减,并更新持仓。
- 交易中心更新订单状态为"已确认",发送通知消息给用户。
这里最容易出问题的是第 5 步和第 6 步之间的衔接。渠道回调可能延迟、重复、丢失,所以交易中心不能只在回调里处理状态,还必须有一个查单任务兜底:定期把状态为"待支付"的订单拿到渠道侧查询真实结果。
落地上,我们为交易订单设计了这样的状态机:
| 状态 | 含义 | 可流转至 |
|---|---|---|
| INIT | 已创建待支付 | PROCESSING |
| PROCESSING | 渠道处理中 | SUCCESS / FAILED / CLOSED |
| SUCCESS | 交易成功 | REFUNDING |
| FAILED | 交易失败 | CLOSED |
| REFUNDING | 退款中 | REFUNDED / FAILED |
| REFUNDED | 已退款 | 终态 |
| CLOSED | 已关闭 | 终态 |
状态机的好处是,每一步流转都有明确的条件和动作,不会出现"从 SUCCESS 直接跳到 INIT"这种脏状态。
3.2 渠道网关:抽象、路由与切换的实操细节
金融项目里渠道网关是个"吃力不讨好"的模块:外部渠道的接口千奇百怪,报文格式有 XML 有 JSON,鉴权方式有证书有密钥,响应码各家还都不一致。所以网关的第一要务是把差异挡在网关内部,对外暴露统一接口。
我们对外只暴露几个核心方法:下单、查单、退款、冲正、回调解析。每个渠道实现一套适配器,内部把渠道返回的"乱七八糟"响应码映射成我们统一的枚举。
渠道配置表我建议至少包含这几列:
CREATE TABLE `t_channel_config` ( `id` bigint NOT NULL AUTO_INCREMENT, `channel_code` varchar(32) NOT NULL COMMENT '渠道编码', `channel_name` varchar(64) NOT NULL, `biz_type` varchar(32) NOT NULL COMMENT '支持的业务类型', `priority` int NOT NULL COMMENT '路由优先级,数值越小越优先', `status` tinyint NOT NULL COMMENT '1启用 0停用', `weight` int NOT NULL DEFAULT '100' COMMENT '同优先级下的流量权重', `timeout_ms` int NOT NULL DEFAULT '5000' COMMENT '超时时间', `max_qps` int NOT NULL DEFAULT '1000' COMMENT '渠道最大QPS', PRIMARY KEY (`id`), KEY `idx_biz_status_priority` (`biz_type`, `status`, `priority`) ) COMMENT '渠道配置表';路由逻辑:先查该业务类型下所有启用的渠道,按优先级排序;取优先级最高的一组,如果其中有渠道的最近一分钟错误率超过阈值,则自动剔除;剩余渠道按权重分配流量。这样当一个渠道抖动时,流量会自动迁移到备渠道,不需要人工干预。
渠道切换是高风险操作,我踩过一次大坑:某渠道商户号即将到期,我直接在生产配置中心改了路由,把该渠道流量切到备份渠道,结果备份渠道的限额配置没提前确认,导致一批大额支付失败。后来定了一条规矩:任何渠道切换必须走灰度。先切 5% 流量观察 10 分钟,没有异常再逐步放大到 50%、100%。同时切换前必须核对清楚:限额、结算周期、支持的银行卡类型、回调字段差异。
3.3 清结算与对账:日切怎么切,差异怎么查
如果账户中心是心脏,清结算就是对账的审判官。金融项目不可能保证每一笔都永远正确,但可以通过T+1 对账把错误找出来。
日切时间我们定在自然日 24:00,但因为渠道侧结算批次不同,实际对账通常从凌晨 2 点开始。原因是大部分渠道的账单在 1 点后才生成完整。对账流程三步走:
- 拉单:从各渠道下载前一交易日的结算账单,解析为标准格式。
- 核对:以我们系统的流水为准,与渠道账单逐笔比对。核对维度包括:金额、时间、流水号、状态。
- 差异处理:把差异数据分三类。长款(渠道有、我们没有)先挂账,查是不是渠道侧测试单或我们漏登记;短款(我们有、渠道没有)查是不是渠道漏结算或我们重复入账;掉单(用户已扣款但我们订单未完成)则触发补单流程,主动向渠道发查单请求。
对账差异处理有个原则:先挂账、再核实、严禁直接抹平。哪怕差一分钱,也要有人工说明原因,财务那边才能销账。这个流程写出来可能觉得繁琐,但资损往往就藏在那些没人解释的差异里。
3.4 高并发兜底:限流、熔断与降级的可落地配置
金融系统倒不要求像电商大促那样扛几十万 QPS,但有几个脉冲场景很折磨人:工资发放日、理财产品开售日、年中年末的扎账期。我们的经验是把兜底方案前置,而不是等报警了再救火。
限流用的是令牌桶,推荐在网关层统一做。配置可以这样定:总入口限流 2000 QPS,突发缓冲 3000,也就是允许短时间内超过均值 50% 的突发流量,但持续超过就会触发排队或拒绝。具体数值要拿压测数据反推,先压测确定数据库连接池在健康状态下的最大吞吐,再除以 1.5 到 2 的安全系数。
resilience: ratelimiter: instances: gateway-entry: limitForPeriod: 2000 limitRefreshPeriod: 1s timeoutDuration: 500ms circuitbreaker: instances: channel-pay: slidingWindowSize: 100 failureRateThreshold: 50 waitDurationInOpenState: 10s permittedNumberOfCallsInHalfOpenState: 30熔断参数看着简单,但"最小请求数"很容易被忽略。如果滑动窗口是 100 个请求,而某个渠道每秒只有 5 个请求,那要 20 秒才凑够 100 个样本,熔断反应会非常迟钝。所以对于流量小的渠道,滑动窗口要调小,比如 30 个请求就够评估了。
降级一般分三级:一级降级关闭非核心功能,比如积分、抽奖;二级降级把只读查询切换到缓存副本,比如产品行情、历史收益;三级降级是兜底,直接停止部分渠道的支付能力,只保留查询服务。降级开关要提前配置好,并定期演练,否则真到故障时没人敢动开关。
4. 常见问题与排查技巧实录
4.1 掉单:用户付款了,订单还挂着
这是我们上线初期遇到最多的工单类型。用户的银行卡已经扣款,但 App 上订单状态一直是"处理中"。
排查路径基本固定:先去交易流水表查这笔订单有没有渠道回调记录。如果没有,去渠道网关日志查回调报文有没有到达。如果到达但处理失败,多半是回调报文里的签名验签没过,或者渠道返回的商户单号与我们本地订单号不一致。
解决这类问题不能只靠人工点"补单按钮",必须有一个定时查单任务兜底。我们写了一个每 5 分钟扫描一次待支付订单的任务,对超过 10 分钟仍未终态的订单主动调用渠道查单接口,根据渠道返回结果推进本地状态。
还要考虑一个极端场景:渠道回调到了,但通知消息队列积压,导致状态更新延迟。所以监控大盘上必须有一个"回调处理延迟"的指标,超过阈值立刻报警。我们曾经有一次 Kafka 分区 leader 切换,回调积压了 40 分钟,用户投诉瞬间涨了三倍。
4.2 幂等键冲突:重复扣款的隐藏场景
有一次 QA 在测试环境复现了一个诡异的问题:用户反复点击"购买"按钮,日志显示两个请求的幂等键不同,但最后用户被扣了两次款。
查下来发现,两个请求的商户单号一个是 "P20250101001",一个是 "P20250101001 ",后面多了个空格。渠道那边对商户单号的 trim 逻辑不一样,导致渠道认为这是两笔不同订单。
这个问题暴露了一个工程细节:幂等键在生成时必须做归一化,所有字段 trim 后拼接,并且拼好后统一转大写。另外,HTTP 请求参数里的商户单号在入口处就要清洗,不能等到入库了才发现脏数据。后来我们加了一个规则引擎校验,所有外部传入的单号必须符合正则^[A-Za-z0-9_-]{1,64}$,否则直接拒绝。
4.3 余额负数与流水缺失的排查快照
出现过一次告警:某用户账户余额变成负的。这种问题的排查思路,不是去看余额数据本身,而是去翻流水。
先把该用户某段时间内的全部流水捞出来,按时间排序,逐笔计算余额,看看从哪一笔开始歪掉。大多数情况都是并发写导致的:两个事务同时读到同一个初始余额,各自算出 new_balance 后写回,后写的覆盖了先写的,但流水却记了两笔。加乐观锁版本号之后,这个问题基本绝迹。
还有一个容易漏的场景:冻结与扣款不一致。比如冻结了 100 元,但确认扣款时只扣了 90 元,剩下的 10 元冻结余额变成"僵尸资金",永远卡在冻结账户里。所以我们每月会跑一次"冻结资金清理脚本",把超过 30 天未解冻的明细列出来,人工核对后做解冻或补扣。
4.4 热点账户与数据库锁等待
金融系统也有热点问题。比如某个大客户单日转账特别频繁,或者某个渠道的清算专户被频繁写入,数据库行锁竞争剧烈,导致接口超时。
最有效的解法不是换数据库,而是错峰合并。把一段时间内的多笔小额变动,合并成一批进行记账。具体做法是:热点账户的流水先入 Kafka 或本地队列,由单线程消费者按顺序逐笔处理,避免了数据库层面的行锁争抢。
注意这个方案只能用于对时间不敏感的场景,比如账户端的汇总记账。对实时性要求高的交易链路,还是老老实实走同步事务,不要把简单问题复杂化。
4.5 排查工具与团队协作习惯
到后期我们发现,排查效率高的团队往往赢在工具上,不只是个人技术强。一个可用的排查界面至少包含:按用户查全链路日志、按订单号查所有服务调用轨迹、按流水号查账务明细。这些数据散落在不同服务里,需要用统一的 traceId 贯穿起来。
我们当时给每个服务接入了全链路 trace,订单号与 traceId 做了绑定。排查问题时,输入一个订单号就能拉出从交易中心到账户中心到渠道网关的完整调用链,节省了大量时间。建议任何做金融系统的团队,第一天就上全链路追踪,不要等出事再补。
5. 安全生产:上线、监控与数据合规
5.1 上线前 checklist:灰度、回滚、演练
金融项目上线,比功能开发更重要的是"上线预案"。我整理了一份老团队内部通用的 checklist,每次发版前逐项打勾:
- 数据库变更是否已评审?是否存在长事务锁表风险?
- 是否做了灰度开关?默认状态是"关闭"吗?
- 回滚方案是回退版本还是切流量?回滚后数据是否能保持一致?
- 涉及资金状态的变更,是否准备了补偿脚本或人工操作手册?
- 新功能是否做了故障演练?比如渠道超时、MQ 积压、数据库主从切换。
有一件事我特别想强调:回滚不是"把版本换回去"这么简单。如果你的新版本改了账户表结构,回滚旧版本后新旧代码同时在跑,数据字段对不上,反而更乱。所以上线前要想清楚,回滚到底回什么。最稳妥的方案是:新功能通过配置开关隐藏,出问题时先关开关,而不是直接回滚版本。
5.2 监控指标与告警分级
金融系统的监控指标,我分成三层:
第一层:可用性指标。接口成功率、平均耗时、P99 耗时、核心依赖的健康状态。
第二层:资金安全指标。这部分是金融项目独有的:对账差异笔数、差异金额、短款长款笔数、冻结资金异常增长、单笔流水超时未闭环。
第三层:业务体检指标。指定时任务的成功率、消息队列积压数量、渠道回调延迟、订单状态长时间卡在中间态的笔数。
告警分级可以参考这个模板:
| 级别 | 场景 | 响应要求 |
|---|---|---|
| P0 | 资损、核心链路大面积失败、账务不平 | 立即响应,5 分钟内拉起应急群 |
| P1 | 单个渠道不可用、定时任务失败、对账差异超阈值 | 15 分钟内响应,优先恢复再查因 |
| P2 | 非核心接口耗时长、通知积压、账单延迟 | 当日处理,记录到迭代排期 |
告警最怕的是"狼来了":告警太多,值班的人麻木了,真出事也没人看。所以阈值要持续调优,把那些"永远不触发实际问题"的告警直接删掉,宁可少而精,不要多而滥。
5.3 敏感数据与审计留痕的工程实践
金融系统从建设第一天就要把"留痕"刻进骨子里。登录、查询、修改、审核、导出,所有敏感操作都要有审计日志。日志里要记录:操作人、操作时间、操作对象、操作内容摘要、结果。不要记录完整的敏感字段,比如身份证号、银行卡号要脱敏后存储。
数据加密这块,我的经验是分层处理。传输层走 TLS;存储层敏感字段用 AES-256-GCM 加密;密钥管理用独立的密钥服务,定期轮换,应用侧拿到的只是解密接口的访问凭证,而不是密钥本身。
敏感数据加密要特别小心性能损耗和查询问题。银行卡号加密后无法走普通索引查询,需要额外维护一个"密文指纹"字段(用固定盐 Hash 后生成的定长值)用于精确匹配。这个细节如果没提前设计,后期改造会非常痛苦。
监管报送接口建议在架构上预留扩展位。很多报送数据不是交易实时产生的,而是日终批量汇总的。所以清结算中心要提前把"按机构、按产品、按交易类型汇总"的统计接口设计好,不要等技术债累积到监管突然提需求的时候再临时拼 SQL。
前段时间我们做了一次全链路故障演练:模拟银行的支付渠道整体不可用 15 分钟,从网关超时、熔断触发、降级开关打开,到对账任务积累差异数据,再到恢复后自动补单。整个过程跑了将近两个小时,中间暴露了三个之前完全没注意到的问题:一个降级开关在配置中心没有同步到生产,一个查单任务的定时表达式在闰年二月会跳过,还有一个是渠道恢复后的流量瞬间打满导致网关 CPU 飙到 90%。
这些都是写代码的时候根本想不到的坑。我个人在金融行业做了这么多年,最深的体会就是八个字:钱的事,慢比快好。功能上线慢一点没关系,账务数据稳比快更重要;高并发没那么可怕,真正可怕的是并发场景下的不一致。如果你也在做 financial-services 这类项目,希望这篇文章能帮你少走几条弯路。