做金融科技(financial-services)方向的项目,和做普通业务系统完全是两码事。我前后参与过好几套金融服务系统的从零搭建,从账户体系、支付转账到风控合规都摸过一遍,踩过的坑比写过的代码还多。这篇文章不摆理论架子,就把这些年做这类项目沉淀下来的设计思路、实操步骤和排查技巧理一遍,给正在做或者准备做金融服务平台的朋友当个参考。
所谓的金融服务项目,核心就一句话:在保证资金安全、数据一致和合规的前提下,把账户、支付、风控、账务这些模块串起来,让用户能顺畅地完成资金操作。听起来和电商差不多,但金融系统对一致性的要求是变态级别的。订单状态错了可以改,资金账错了是要出大事的。所以整个设计的底层逻辑都围绕“钱不能算错”展开,所有架构取舍、技术选型、流程设计,最终都服务于这个目标。
1. 项目定位与整体设计思路
1.1 金融服务项目到底在解决什么问题
先想清楚一个问题:用户打开你的金融服务平台,他需要的不是“一个APP”,而是“钱能安全地进来、转走、查到账”。这个简单目标拆开之后,涉及的东西一点都不简单:
- 用户要注册、实名认证,平台得知道“你是谁”,这是合规的底线,也是风控的基础。
- 用户要充钱、提现、转账、消费,平台得有一套账户体系记录“你有多少钱”,并且每一笔变动都要可追溯。
- 平台要接银行或者第三方支付渠道,渠道信息要加密传输,回调要验签,成功失败的判定要准确。
- 每一笔交易都要经过风控检查,可疑的拦下来,正常的放行,误伤率还不能太高。
- 每天结束之后,平台内部账和渠道账单要对平,差一分钱都得查出来是为什么。
很多人做这类项目失败,不是因为技术不够,而是低估了资金一致性的难度。普通业务系统里,数据库更新失败大不了报个错;金融系统里,同一个操作可能在多个服务里异步执行,任何一步失败都可能造成账实不符。所以设计的第一原则不是“功能多全”,而是“资金流和信息流必须完整闭环”。
我习惯把一个金融服务项目比作开银行网点。用户看到的是一扇柜台窗口(APP/网页),背后必须有金库(账户系统)、点钞员(账务处理)、保安(风控)和摄像头(审计日志)。少任何一个角色,这个网点都开不长久。项目规划的时候,别急着堆功能,先把这些“角色”在架构里安排好。
1.2 核心模块划分与边界
金融服务平台刚起步的时候,很容易把所有逻辑写在一个服务里,用户、账户、订单、支付全揉在一起。这种玩法在Demo阶段没问题,一旦交易量上来,马上会遭遇“改一处崩全身”的噩梦。按领域划分模块是金融服务项目的常规做法,我一般会拆出下面这些服务:
| 模块 | 核心职责 | 关键点 |
|---|---|---|
| 用户与KYC服务 | 注册、登录、实名认证、资料管理 | 证件信息加密存储,KYC等级变更要留痕 |
| 账户服务 | 虚拟账户开立、余额管理、冻结/解冻 | 可用余额和冻结余额分离,行锁更新 |
| 交易/订单服务 | 交易订单创建、状态流转 | 状态机设计,幂等控制,防止重复下单 |
| 账务服务(清结算) | 记账、试算平衡、日终清算 | 复式记账,借贷分录,账证分离 |
| 支付网关服务 | 渠道对接、路由、回调处理 | 渠道统一抽象,验签、查单、补单 |
| 风控服务 | 规则引擎、黑白名单、额度管控 | 实时决策,超时降级,灰度放量 |
| 通知与消息服务 | 短信/推送、站内信、异步事件通知 | 与核心交易链路解耦 |
这里有个很容易走歪的地方:账户和交易要不要分开?我的答案是必须分。账户管“有多少钱”,交易管“这笔钱从哪来到哪去”。如果两个职责混在一起,后面做对账和审计会很痛苦。你查一笔异常账,需要同时翻订单表、账户流水表、渠道回调记录,如果它们都塞在一张表里,查询逻辑复杂不说,锁竞争还会拖垮性能。
另外,交易服务和账务服务也要分清楚。交易服务负责业务状态(比如“支付成功”),账务服务负责资金记录(比如“入账”)。业务成功和资金到账不是同一件事,尤其是涉及渠道回调异步通知时,订单状态可能先更新,账务入账后一步完成。两者拆开之后,即便入账失败,也可以只重放账务消息,而不是把整个交易流程重跑一遍。
1.3 为什么选微服务而不是一把梭的单体应用
很多人一提到微服务就头疼,觉得是过度设计。但金融服务场景下,微服务的价值不是“技术时髦”,而是“故障隔离”和“独立扩容”。
举一个实际例子:某次活动带来大流量,支付渠道回调瞬间涌入大量请求。如果支付网关和账务服务在同一个应用里,回调抖动会直接拖慢记账,甚至影响用户查询余额。拆分之后,我可以单独给支付网关加实例,限流回调请求,账务服务通过MQ异步消费,流量再大也只是队列堆积,不会把核心账户服务打挂。
但这不意味着所有金融服务项目都必须上全套微服务。如果只是一个内部工具或者交易量很小的平台,模块化单体+消息队列也能达到类似的稳定效果。我见过不少项目上来就拆十几个服务,结果团队连部署都搞不定。合理的路线是:按领域把代码分成独立模块,边界清晰,等某个模块确实出现了独立的性能或者发布诉求,再拆成独立服务。这一点对中小团队尤其重要,别为了微服务而微服务。
2. 核心业务细节与账务设计
2.1 账户体系设计:虚拟账户与资金账
金融服务的账户体系和普通会员体系最大的区别是:账户里存的不是积分,是真金白银,所以“余额”不能只是一个数字字段。我强烈建议账户表把“总余额、可用余额、冻结余额”拆开设计,核心更新语句必须保证并发安全。
常见的账户表结构是这样的:
CREATE TABLE account ( account_id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, account_type TINYINT NOT NULL COMMENT '1-主账户 2-冻结账户 3-在途账户', balance DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '总余额', available_balance DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额', frozen_balance DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '冻结余额', currency CHAR(3) NOT NULL DEFAULT 'CNY', status TINYINT NOT NULL DEFAULT 1, version BIGINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE KEY uk_user_type (user_id, account_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;更新余额的时候,别用先查后改的流程,直接利用数据库的原子操作:
UPDATE account SET available_balance = available_balance - 100, frozen_balance = frozen_balance + 100, version = version + 1 WHERE account_id = 123 AND available_balance >= 100;这条SQL是关键。WHERE available_balance >= 100这个条件被很多人忽略,实际上它是防止余额扣成负数最重要的一道防线。如果受影响行数为0,说明账户余额不够,直接返回“余额不足”,不需要在代码里额外加锁。
还要说明一点:虚拟账户额度其实分为“用户看到的是可用余额,账务系统看到的是账面余额”。用户发起支付时,先做“可用余额 -> 冻结余额”的转入,等支付成功再扣减冻结余额;支付失败或者超时就解冻回可用余额。这个过程保证了业务处理期间资金不会被其他并发操作挪用,和电商的“锁库存”一个道理,只不过资金比库存更敏感。
2.2 复式记账:金融系统为什么不能只存余额
这是金融服务项目里最容易被半路出家团队忽略的地方。普通系统记录“用户余额从1000变成900”就可以了,金融系统不行,因为这种记录方式无法证明账是平的。金融服务项目必须用复式记账法:每一笔业务至少产生两条分录,有借必有贷,借贷必相等。
举例:用户A向用户B转账100元,平台收手续费1元。在账务系统里会产生这样的分录:
借:A账户(资产类) -100 贷:B账户(资产类) +100 借:A账户(资产类) -1 贷:手续费收入(收入类)+1这里资产类科目用负号表示减少,借贷方向在不同科目类型里意义不同,但核心逻辑是不变的:所有科目的借贷发生额合计必须为0。只要每天日终跑一遍科目余额试算平衡,借贷不平的差错立刻会浮出水面。
很多团队觉得复式记账太重了,一笔转账明明只需要更新两个余额,搞什么分录表?等到某天渠道账单和本地账差了0.01元,你看着一堆余额快照根本不知道这0.01从哪冒出来的时候,就知道分录表的价值了。分录表让每一分钱的来龙去脉都有迹可循,这也是金融审计的基本要求。
实际实现时,我建议把“记账流水表”和“账户余额表”分开。流水表只增不改,记录每一笔的分录,账户表存当前余额。查询余额看账户表,查历史对账看流水表。这种“账证分离”的模式,能有效避免业务代码误改历史流水,也是金融系统数据不被篡改的底线。
2.3 支付渠道对接与撮合逻辑
一个金融服务平台通常会接多个支付渠道,包括银行直连、第三方支付、聚合支付等。写死某一家渠道的API是最愚蠢的做法,因为渠道经常变更接口、调整费率、甚至因为合作中止被替换。所以支付网关服务要做一层渠道抽象,把所有渠道统一成一套内部接口,上层业务根本不需要关心钱具体走的是哪一家。
一个支付网关至少包括这些能力:
- 下单接口:内部统一入参(金额、用户、业务单号),网关负责组装各家渠道的参数格式。
- 渠道路由:根据金额、渠道可用性、成本、限额、历史成功率选渠道。
- 回调处理:接收渠道异步通知,验签、幂等校验、更新交易状态。
- 主动查单:对于状态“未知”的单据,定时向渠道发起查询,防止漏单。
- 对账文件解析:每日拉取渠道账单,配合对账系统完成资金核对。
支付渠道回调和主动查单是坑最多的环节。渠道回调可能延迟几个小时甚至第二天才到(比如银行卡代扣),主动查单需要设计好轮询策略,频率太低会超时,频率太高容易被渠道限流。我通常的做法是:支付单状态进入“处理中”后,先等30秒,用递增间隔(30s、1min、2min、5min)主动查单,最多查5次,仍不确认就置为“待人工处理”并告警。
回调验签这块,严格意义上所有渠道都有自己的签名算法,有的用MD5加盐,有的用RSA,还有的用国密SM2,网关要做统一的验签入口。验签失败的请求要记日志并告警,这往往是渠道参数变更或者中间人攻击的信号。我见过有人图省事直接把验签跳过,说白了是把资金安全的大门敞开着,出一次事就够喝一壶的。
3. 合规与风控的关键环节
3.1 KYC与反洗钱设计
很多人觉得KYC(Know Your Customer,了解你的客户)是应付监管的形式主义,实际上它是金融服务平台的第一道防线。线上欺诈、洗钱、盗号、刷单,大量风险都发生在匿名或虚假身份场景下。KYC做得扎实,后续的风控压力会小很多。
基础KYC流程至少包含实名认证、证件识别、联网核查三个环节:
- 实名认证:姓名+身份证号,通过公安接口比对,校验通过后才算完成“实名”。
- 证件识别:对用户上传的身份证照片做OCR,自动抽取信息,并做活体检测(张嘴、摇头等),确保是真人而不是照片。
- 联网核查:同步到权威数据源,确认证件状态正常,不是临时身份证或者挂失证件。
KYC做完之后,要给用户打风险等级。这个等级不是死的,要结合用户的交易行为动态调整。比如一个新注册用户,实名等级高、交易金额小、设备稳定,风险等级就低;一个注册当天就频繁转账给不同陌生账户的用户,就算实名没问题,风险等级也要拉高。
这里有个实操细节:KYC信息一旦变更(比如手机号换绑),原有的风控画像要重新评估,不能沿用旧数据。很多平台被“养号”薅羊毛,就是KYC变更之后没有重新建模导致的。
3.2 交易风控规则引擎
风控是整个金融服务项目里最需要迭代的模块。最初级的手段是写死一堆if/else,比如“单笔超过5000拦截”,但业务一旦复杂,这种代码根本维护不了。我建议从一开始就设计一个规则引擎,把风控规则做成可配置的,运营人员能自己调整阈值,而不是每次改风控都找开发发版。
一个可用规则引擎的关键要素:
- 输入特征:用户ID、设备指纹、IP、地理位置、交易金额、收款方、交易频次、历史行为等。
- 规则类型:黑白名单规则、阈值规则、频次规则、组合规则。
- 决策动作:放行、人工审核、拦截。
- 规则优先级:高优先级规则先执行,比如黑名单直接拦截。
- 日志与留痕:每次风控决策都要记录命中了哪些规则,用于复盘和调优。
举例说明规则配置:
{ "ruleId": "R1001", "name": "新用户单日转账限额", "conditions": { "registerDays": {"lt": 7}, "transferDailyTotal": {"gt": 2000} }, "action": "REVIEW", "priority": 10 }这条规则表示注册不满7天的用户,单日累计转账超过2000元就要进入人工审核。规则引擎的价值在于它把“风险和策略”从代码里解放出来,运营可以实时调整,策略变化不需要重新部署服务。
风控系统上线初期千万不要直接“拦截”,要设计“观察模式”。先让规则只记录日志不下决策,跑一段时间看误伤率,再逐步切到“审核模式”和“拦截模式”。一上来就硬拦,大概率会把正常用户误伤到投诉爆表。
3.3 限额与分级管理
限额体系是和KYC等级强相关的。同一个用户,实名等级不同,能做的交易额度完全不同。这是监管的硬性要求,也是平台自我保护的手段。
限额通常分三个维度:
- 单笔限额:一笔交易最多能转多少钱。
- 单日累计限额:一天之内累计交易金额上限。
- 单月累计限额:一个自然月内累计交易金额上限。
比如我做过的一个平台,用户未完成高级实名(只绑定手机)时,单笔不超过1000,单日累计不超过3000,月累计不超过20000;完成身份证实名和视频面签后,单笔提到50000,单日累计提到200000,月累计提到500000。限额等级可以在风控规则里做成一张配置表,后台可调。
限额设计有一个容易被忽略的问题:累计金额的统计口径要统一。用数据库SUM临时算的话性能很慢,必须用流水表定期预聚合。我当时是建了一张“用户日累计交易表”,每个账户每天当笔交易成功后累加,凌晨跑批清零。查询限额时直接查这张表的汇总字段,毫秒级返回。
3.4 日志审计与监管报送
金融服务平台的审计日志和普通日志不一样,普通日志是给程序员排错看的,审计日志是给监管和司法机构看的。审计日志要求不可篡改、可追溯、可恢复。
关键操作必须记录全链路:
- 用户注册、登录、实名认证、绑卡、改密。
- 每一笔交易的请求参数、渠道返回、状态流转。
- 风控规则命中记录,包括决策人/系统、决策结果。
- 后台运营人员的所有操作,尤其是涉及资金变动的操作。
日志存储建议单独建库,业务库和审计库物理隔离。更严格的做法是日志写入后做哈希链式签名,任何一条日志被改动都能检测出来。虽然这个对大多数初创平台有点重,但至少要做好数据库权限隔离,不能让应用层DBA直接改日志表。
数据保留期限方面,按监管要求交易记录至少保存5年,用户身份资料至少保存5年,而且不能随意删除。这带来的存储成本要提前规划,历史日志可以归档到冷存储,但检索入口要保留。
4. 实操过程与核心环节实现
4.1 技术选型与架构落地
金融服务项目的技术选型,核心不是追新,而是稳。我一般推荐以成熟技术栈为主,团队熟悉什么用什么,语言本身不是瓶颈。
我常用的一套组合:
- 后端框架:Spring Cloud / Spring Boot(Java)或 Go 微服务框架,看团队技术储备。
- 数据库:MySQL(账户、用户、订单),Redis(缓存、分布式锁、幂等去重键)。
- 消息队列:RocketMQ 或 Kafka,用于交易事件异步通知。
- 定时任务:XXL-Job 或自研调度,用于日终对账、渠道查单。
- 部署:Docker + K8s,环境一致性靠容器化保证。
- 监控:Prometheus + Grafana,交易成功率、接口RT、账务对平率都要有大屏。
数据库层面,账户表和流水表用 InnoDB,事务隔离级别默认的 READ COMMITTED 就够用,别乱调成 SERIALIZABLE,会严重拖慢并发。余额更新用行锁+条件更新,不要用SELECT FOR UPDATE锁一大片。分库分表不要过早做,一般单表数据量过了千万且持续增长再考虑,分片键优先选用户ID或者账户ID。
消息队列在金融服务里承担的是“削峰填谷”和“流程解耦”的角色。比如支付成功后,系统发一条“支付成功事件”,下游的账务服务、通知服务、积分服务各自消费处理。账务服务消费不成功就重试,重试次数耗尽进入死信队列人工处理,这个机制能兜住很多不稳定的边界情况。
4.2 支付下单到入账的完整流程
这里我把一个完整的支付流程拆开,从用户APP下单到最终账务入账,涉及的步骤和状态要点都列出来。
第一步,用户发起支付请求,交易服务创建支付单:
交易单号:T20250115001 用户ID:10001 支付金额:100.00 支付方式:余额支付交易服务先做幂等检查,防止用户狂点导致重复单。幂等键用“业务单号+业务类型”,比如order_no + "PAY"。有唯一约束兜底,数据库层面再防一次。
第二步,调用账户服务冻结余额:
UPDATE account SET frozen_balance = frozen_balance + 100, available_balance = available_balance - 100 WHERE user_id = 10001 AND available_balance >= 100;受影响行数为0就返回余额不足,交易状态置为“支付失败”。
第三步,调用交易服务更新状态:
支付单状态:PROCESSING(处理中)这一步做完,用户看到的是“支付中”,系统还没真正扣款,只是资金被冻结了。
第四步,如果走的是第三方渠道,交易服务会调用支付网关,网关选好渠道后请求渠道下单。渠道返回“受理成功”之后,系统等待异步回调。这里要注意,渠道受理成功不代表扣款成功,一定要以后续回调为准,不能因为下单成功就把订单置为成功。
第五步,渠道回调支付网关。网关验签通过后,查询本地支付单,确认这笔回调是对应哪笔订单。回调处理必须是幂等的:同一笔交易回调两次,第二次不能再记一次账。
第六步,确认支付成功后,更新支付单状态为“支付成功”,同时发送“支付成功事件”到消息队列。
第七步,账务服务消费事件,对这笔交易做复式记账:
借:资产类-用户主账户 -100 贷:负债类-用户备付金 +100最后对账时,用账务流水里的借贷平衡校验,确认不会出现用户账户多了钱但平台备付金没对应增加的情况。
完整流程里,最容易出问题的环节是“回调丢失”。渠道返回受理成功之后,如果一直没有回调,支付单就一直停在PROCESSING状态。所以主动查单任务非常重要,我一般是每分钟扫一次超过5分钟还在PROCESSING的单据,调用渠道查询接口确认真实状态,再驱动后续流程。
4.3 对账系统的实现思路
日终对账是金融服务平台的定海神针。没有对账,你根本不知道昨天交易是不是每一笔都有清晰的资金记录和渠道记录。我建议对账系统从项目一开始就做,而不是等上线后再补,因为补对账的难度远超想象。
对账的整体流程分四步:
第一步,拉取渠道账单。支付网关解析渠道生成的清算文件,落库成“渠道流水表”,字段包括渠道交易号、金额、时间、状态等。
第二步,拉取本地交易流水。从交易表、账务流水表里捞出和渠道同口径的交易记录。
第三步,逐笔比对。比对维度包括金额、状态、时间、订单号。比对结果分成几类:
- 匹配一致:正常交易。
- 长款(渠道有,本地无):可能是渠道回调没收到,需要补单或者退款。
- 短款(本地有,渠道无):可能是下单未请求成功,需要检查本地是否存在虚假成功单。
- 金额不一致:需要重点排查手续费、优惠、折扣是否计算错误。
第四步,差错处理。查出差错之后进入待处理队列,人工或者半自动处理,处理完更新对账状态。
对账时最容易忽略的是手续费。有些渠道的手续费是扣款后才能确认,比如银行卡支付成功率不是100%,被退回的交易手续费怎么处理,各家渠道规则差异很大。对账程序要把手续费和本金分开算,不然几百笔下来差个几块钱是常事。
5. 常见问题与排查技巧实录
5.1 重复支付与幂等
金融服务项目最常见的线上故障就是用户支付了多次。触发场景通常有两个:用户手快连点支付按钮;渠道对同一笔交易重复回调。
前端防重复是基础手段:
- 支付按钮点击后立即置灰,禁用期间不允许再次点击。
- 使用
uuid做客户端请求唯一标识,服务端根据这个标识去重。
服务端要做的核心是幂等控制。我的做法是三层防线:
第一层,请求拦截。交易服务在Redis里放一个“业务单号+操作类型”的幂等标记,SETNX成功才能继续:
SETNX pay_idempotent:T20250115001 1 EX 300SETNX返回1说明第一次执行,返回0说明已经执行过,直接返回之前的处理结果。
第二层,数据库唯一约束。交易流程落库时,支付流水表的transaction_no字段建唯一索引。即使Redis失效,数据库也会拦住重复记录。
第三层,状态机校验。支付单从PROCESSING到SUCCESS只能走一次,状态机里明确合法的流转路径。如果发现SUCCESS状态的单子进来一笔新支付,直接拒绝。
排查重复支付问题时,先翻支付流水表看同一业务单号有几条记录,再看Redis里的幂等标记为什么没拦住。通常不是Redis过期时间太短,就是回调处理逻辑里幂等键用错了字段(比如用了渠道流水号而不是业务单号)。
5.2 账务不平怎么查
“今天总账借贷差了92块”这个告警,能让任何金融项目负责人的心凉半截。查不平的账是技术活,但有一套标准的排查顺序。
第一步,先重算日切余额。确认所有账户的余额合计与总账科目余额是否一致。不一致的话,缩小范围到昨天、前天,做二分排查,找到第一个不平的时点。
第二步,找流水断点。检查账务流水表里有没有“只有借没有贷”的分录。最简单的方法:
SELECT transaction_no, debit_summary, credit_summary FROM ledger_entry WHERE transaction_no IN ( SELECT transaction_no FROM ledger_entry GROUP BY transaction_no HAVING SUM(debit) <> SUM(credit) );这条SQL能直接筛出所有借贷不平的会计分录。正常情况下结果为空,一旦有结果,问题分录基本就在这了。
第三步,看是不是漏了手续费或者优惠科目。渠道账单里实际扣了100元,本地只记了本金99元,手续费1元没记,对平自然就差1块。我遇到过好几次这种问题,最后都是因为“手续费科目还没有建好就上了新渠道”导致的。
第四步,如果是历史账不平,先打补丁再复盘。保留原始错误流水,补做一笔“差错调整凭证”,不要直接删改原流水。金融审计要求原始记录不能被篡改,错了就再记一笔修正,而不是把错误的痕迹抹掉。
5.3 风控误伤怎么调
风控系统最尴尬的状态是“黑产拦住了,正常用户也拦住了”。一个用户正常给妈妈转5000块,结果因为“新用户+大额+夜间+陌生设备”触发了拦截,体验非常糟糕。但取消风控又是找死,所以必须在误伤率和风险率之间找平衡。
我犯过的错和踩过的坑,总结成几条经验:
第一,所有新规则都先进观察模式。观察模式下系统只标记“如果命中会怎样”,不下发拦截。跑7天看命中率、误伤率,再决定是否切换成拦截。
第二,规则要分优先级,高优规则负责绝对的恶意行为(比如黑名单IP),低优规则负责聚合风险行为。低优规则命中后,可以降级为“二次验证”而不是直接拦截——比如要求短信验证码或者人脸识别。
第三,同一个用户频繁误伤后,要有申诉和解冻通道。风控不只负责拦,还要设计一个“申诉后人工复核”的闭环。我已经见过很多平台因为申诉流程太繁琐,用户干脆卸载APP。
第四,规则参数要支持灰度发布。风控规则迭代不像普通功能发布那么随意,一旦误伤新买量进来的老用户,损失的不只是交易额,还有用户信任。我记得有一次调整了“单日累计限额”从5000改成2000,没有加灰度,瞬间触发几百个正常用户拦截,客服电话被打爆。从那以后,所有风控参数改动必须走灰度配置,支持按用户ID哈希百分比放量。
5.4 性能瓶颈与数据增长
金融服务跑起来之后,最先撞上的墙就是流水表数据爆炸。每天几十万笔交易,每笔交易至少两条分录,一年下来几千万条记录,查询和统计开始明显变慢。
流水表性能问题有几套组合拳:
第一,按时分表或者按用户ID分库分表。金融业务天然有时序性,按月分表是最简洁的方案。比如ledger_entry_202501、ledger_entry_202502,按月建表,老表归档到历史库。
第二,读写分离。交易写入走主库,风控、对账、报表查询走只读从库。金融系统对延迟敏感,读写分离能显著缓解主库压力。
第三,定期归档。超过两年的流水,从在线库搬到冷存储。监管要求保留5年,但不需要都在热库。归档时保留摘要索引,需要时再按条件拉取。
另外还有一类隐蔽问题:账户表的version乐观锁字段冲突。高并发场景下同一账户频繁操作,乐观锁不断更新失败,用户体验就是“请求超时”。这种情况往往不是代金羊毛,而是用户开了多个页面同时下单。我的做法是更新失败后重试3次,重试间隔50ms,大部分冲突都能消掉。
5.5 渠道回调与查单补偿的边界
最后专门讲讲查单补偿,这是金融服务线上事故的重灾区。渠道回调丢失是非常常见的情况,尤其是一些小银行渠道,回调服务稳定性堪忧。没有完善的查单机制,用户的钱被扣了但平台不知道,只能等用户主动投诉才发现,这就是典型的“漏单”。
我的做法是:
- 支付单进入
PROCESSING状态后,延迟队列延迟30秒查单,不行再递进(1分钟、5分钟、15分钟),总共查5次。 - 查单接口返回成功,直接驱动“支付成功”流程。
- 查单接口返回失败,标记
CHANNEL_FAIL,进入人工复核队列。 - 查单接口返回“未知”,继续下一轮延迟查询,直到渠道有明确结果。
渠道查单本身也要防止并发问题。同一笔支付单的查单任务不能同时执行两个,否则渠道那边如果对重复查单比较敏感,可能返回状态错乱。查单任务执行前,同样用RedisSETNX加锁,避免定时任务重复调度。
另外一个容易忽略的点:渠道查单状态和回调状态冲突时怎么处理?比如渠道回调先到,显示“失败”,但查单显示“成功”。这种矛盾大概率出在渠道侧状态同步延迟上,不能简单取一个信一个。我的处理原则:以渠道最终状态为准,不一致的产生告警进入人工复核,不允许自动改状态。人工核对渠道后台订单视图后再修复,千万别让系统瞎猜。
6. 最后分享几点我的真实体会
做financial-services项目,最大的体会是“慢就是快”。账务设计想不清,代码写再多也是白搭,一次账实不符的故障,可能摧毁好几个月的信任积累。别怕流程繁琐,KYC、对账、审计日志这些基础能力,都是在保护平台的长期安全。
第二个体会是“先做对账再谈功能”。很多团队喜欢先把充值、提现、转账都做出来,对账系统拖到后面。结果上线第一天渠道账单就对不平,几百笔交易人工核对到凌晨。对账不是一个后期功能,它是金融系统从第一天起就要跑起来的基础设施。
第三个体会是“风控和体验不是对立的”。规则引擎里的拦截、审核、放行是一套光谱,不是非黑即白。给正常用户留申诉通道,给可疑用户增加验证步骤,这套分层的风险决策逻辑,才是成熟平台的标志。
最后,如果让我给准备做金融服务平台的朋友一句建议,那就是:先把用户、账户、账务三张表的关系理清楚,再多接一个渠道做联调测试,最后再上微服务、分布式事务这些重型手段。基础不牢,地动山摇,基础扎实了,后面的路反而顺。