ARTICLE DETAIL

资讯详情

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

金融科技后端实战:从账户体系到对账结算的完整架构设计

金融科技后端实战:从账户体系到对账结算的完整架构设计 1. financial-services这个项目到底在做一件什么事可能有人看到financial-services这个标题会觉得太宽泛以为就是给银行做个网站、对接个支付接口之类的轻量活儿。我最初接到这个项目需求时也差点这么想结果一拆解才发现这背后其实是一整套金融服务基础设施的搭建。今天想把这套从零到落地的东西掰开揉碎讲清楚尤其适合正在做金融科技类后端、支付清结算系统、或者打算进入这个领域的朋友参考。所谓financial-services从字面上看就是金融服务但真正落到工程上指的是一个能承接资金流转、账户管理、交易记录、风控校验、对账结算等一系列业务的系统。换句话说它不是一个单点功能而是一套完整的技术方案。我参与的这个项目目标很明确搭建一个支持多业务方接入、资金流转可追溯、交易状态最终一致、且满足合规审计要求的基础金融服务平台。项目启动的时候团队不算大后端加前端加测试加产品满打满算十几个人。技术栈选了Java为主Spring Boot做应用层MySQL存核心业务数据Redis做缓存和分布式锁Kafka做异步消息和解耦这套组合在金融类后端项目里算是很经典的选型。为什么这么选后面我会分模块细讲每个选择都有它的理由不是随手拍的。如果你也想从零做一个类似的项目或者正在做相关的模块这篇文章会把架构设计、关键技术点、踩坑记录、还有我个人的一些经验全部写出来。内容比较长每一段都是实际落地中摸出来的东西不是教科书式罗列。2. 前期方案设计里的几个关键取舍2.1 账户体系为什么不能用一张表搞定所有账户先讲账户体系。这是整个金融服务系统的地基地基没打好后面盖多少层楼都得返工。我见过不少新手项目上来就建一张account表字段无非是account_id、user_id、balance、currency、status。看起来挺简单真实业务一跑就出问题。问题出在哪第一一个用户可能同时有多个账户——人民币账户、美元账户、积分账户、冻结账户甚至还有子账户。一张表硬塞这些数据字段冗余和业务耦合会越来越重。第二金融系统里账户不只是记录余额它还要承载冻结金额、可用金额、待清算金额这类状态机变化如果表结构设计得太随意后续任何一笔交易都会变得寸步难行。我们最终采用的方案是账户分层模型。底层是一张account表但通过account_type字段区分账户类型通过account_status字段管理账户状态生命周期。同时每个账户关联一个独立的账户流水表所有余额变化都必须通过流水记录来驱动而不是直接update余额字段。这个设计的核心思路是“流水驱动余额”也就是说余额只是一个汇总结果流水才是唯一的真相来源。这样做的好处很明显出了问题可以直接追溯每一笔变动的来龙去脉对账时也只需要汇总流水就能算出余额。线上问题排查的时候这种设计能救命。2.2 币种与多币种记账的坑如果系统只处理人民币单一币种那货币这块确实省心。但一旦涉及美元、港币、欧元等多币种麻烦就来了——不是简单加一个currency字段就完事的。多币种记账有两个绕不开的问题。第一个是精度问题。金融系统涉及钱精度永远排在第一位。数据库的浮点数类型直接排除decimal才是基本要求而且小数位必须统一约定。我们内部约定所有币种统一使用小数点后四位存储展示层再按各币种惯例截取。第二个是汇率与折算问题。不同币种的账户之间做资金划转必须依赖实时汇率换算而且折算过程本身要记账、要留痕。这个折算记录不是为了给用户看是为了审计和账务平衡。我们当时做了一个currency_rate表专门记录每日汇率快照和实时汇率调整记录。所有跨币种操作都从这张表取汇率而不是对接外部API实时拉取。为什么因为外部汇率源可能会抖动而且审计需要的是“当时那笔交易用的什么汇率”不是“现在什么汇率”。每天生成一次汇率快照交易时刻锁定快照版本就能保证每笔跨币种交易都有确定性。2.3 账务与业务分离看似麻烦其实是最省事的路还有一个容易被忽略但极其重要的设计决策账务系统和业务系统的分离。很多小团队做项目喜欢在业务表里直接记金额比如订单表里放一个pay_amount心想“这不就行了么”。从功能角度看确实行但从金融系统的角度看这种耦合是灾难。因为业务表是面向业务的它的生命周期跟着业务走而账务是面向资金的它的生命周期跟着资金走。两者混在一起之后业务状态的一点点变动都会让账务变得不可控。我们采用的是业务系统与账务系统完全解耦。业务系统只管订单、交易、营销活动账务系统只关心账户和流水。业务系统需要记账时通过异步消息发送一个记账指令账务系统消费后生成流水、更新余额再通过回调通知业务系统结果。这样两个系统的演进互不干扰账务系统可以单独做水平扩展业务系统要调整也只需要改自己的状态机。这套设计的代价是开发量稍大一些但项目上线之后每次需求变更我都庆幸当初做了这个决定。金融项目最怕改出账不平解耦之后这种风险被压到了最低。3. 交易链路核心模块的落地细节3.1 从下单到入账一条完整资金流要经过哪些环节金融服务的核心业务链路其实是一条资金流动链路。从用户下单一笔理财或者一次充值或者一次转账到最后资金真正入账、账户余额更新中间要经过非常多环节。一个看似简单的手续背后往往是一整套流程编排。我以转账为例拆解一下这条链路。用户发起转账申请首先进入的是交易系统这里会创建一笔交易订单状态是“待处理”。订单创建之后系统先做基础校验——账户是否存在、状态是否正常、余额是否充足、交易金额是否在限额内。校验通过后交易请求被丢进消息队列异步进入账务系统。账务系统拿到指令后开始记账先冻结付款方账户的金额再生成付款流水、收款流水两端账户余额同步更新最后标记交易成功。如果中途任何一步异常链路会触发冲正逻辑把已经冻结的钱解冻、已生成的流水冲销保证资金不会凭空消失或凭空出现。听起来步骤多但每一步都有它存在的必要性。比如“冻结”这一步是用来保证资金在交易完成前不会被其他并发操作挪走。要是没有冻结你这边转账还没完成用户那边又发起一笔支付两笔交易同时扣同一个账户的余额就容易出现超扣。金融系统里超扣就是事故。3.2 幂等与并发同一笔请求不能造成两次扣款说到并发就不得不提幂等。幂等是分布式系统里的老话题但金融场景对幂等的要求是最严苛的。客户端因为网络超时重试同一条转账指令被发送了三次。如果系统不做幂等处理用户会被扣三次钱。这不是理论上的风险是真实发生过的线上问题。我们当时花了很多精力在幂等上最终采用的方式是“业务唯一键状态机校验”双重保障。业务唯一键是外部传入的请求ID或者内部生成的交易单号。每次接收到交易指令系统先在Redis里查这个单号有没有处理过。如果已经处理过直接返回上次的结果如果没有就用SETNX命令抢占一个分布式锁抢到锁的请求才允许继续执行账务逻辑。锁的过期时间和业务超时时间关联设置避免锁永久占用。然后在数据库层面交易表对交易单号建唯一索引作为兜底——即使Redis里的锁因为极端情况失效数据库的唯一索引也能拦住重复请求。这个方案不能说百分之百杜绝问题但三层防护下来线上遇到重复请求的概率已经低到可以忽略。我做金融项目最大的体会就是不要相信任何单一环节不出错要做的是让任何一个环节出错系统都能兜住。3.3 状态机设计交易状态的流转要严格收敛交易链路里最怕的是什么是状态混乱。一笔交易到底是成功、失败还是处理中系统内部必须有清晰且严格的定义不能说“好像成功了”“应该没成功”。我们把交易状态定义成一个严格收敛的状态机INIT(初始)、PROCESSING(处理中)、SUCCESS(成功)、FAILED(失败)、CANCELLED(取消)。允许的流转路径是固定的比如INIT可以到PROCESSINGPROCESSING可以到SUCCESS或者FAILEDFAILED在某些场景下可以到CANCELLED但绝不允许SUCCESS之后再变成FAILED。状态流转全部通过状态机组件来驱动而不是在业务代码里随意set。这么做的好处除了逻辑清晰更重要的是方便排查问题。每次状态变化都会记录状态变更日志时间、操作人、变更前、变更后、触发原因全部留痕。出了纠纷或者审计要求问“这笔交易为什么失败”我们拉出状态机日志就能看到完整的生命周期不用靠猜。4. 风控与合规金融项目里最容易被技术忽略的部分4.1 为什么技术团队必须把风控当一等公民很多从互联网行业转过来做金融项目的工程师一开始都会低估风控的复杂程度。在一些人看来风控就是“检查一下用户是不是黑名单、金额大不大”做一个简单判断就完事。真做起来你会发现风控是整个系统里牵涉面最广、业务规则最容易变化、也是最需要和数据打交道的模块。金融风控的本质是用尽可能低的误杀率在交易链路中拦截掉可疑行为。这就需要在每个关键节点插入风控检查登录时查设备风险、交易时查金额与频次、提现时查身份认证强度。风控的规则不是一次定死的而是持续迭代的。今天正常的交易模式明天可能就成了盗刷特征今天拦截的策略可能误伤了一大批正常用户。所以风控系统必须支持规则动态配置、策略灰度发布、效果实时监控。我们的做法是把风控拆成一个独立的服务提供统一的check接口。业务系统要发起交易前先同步调用风控接口风控返回“通过”才放行返回“拒绝”就终止。风控内部先走规则引擎规则引擎跑完打分再决定是否触发人工审核。这种同步检查模式虽然多了一次RPC调用但对资金安全来说多这几十毫秒完全值得。4.2 规则引擎选型不用羡慕复杂的开源方案说到规则引擎很多人第一反应是Drools强大是强大但学习成本和维护成本也不低。我们的实际经验是除非你的团队有专职规则工程师否则别轻易上重型规则引擎。金融业务的风控规则虽然多但绝大多数是“条件阈值”的组合判断用一套轻量的表达式引擎完全够用。我们最终选了Groovy脚本配合一个自定义规则配置表来实现。每一条风控规则存成一行配置包含规则名称、条件表达式、阈值参数、生效时段、动作类型。规则引擎加载配置后通过预编译的Groovy脚本执行判断逻辑。这样产品或风控团队要调整规则时只需要改配置不用改代码、不用发版。当然Groovy脚本是动态执行的有安全风险所以我们限制了脚本运行环境关闭了文件访问、网络访问等危险能力并且只允许风控核心成员修改规则配置。这里要强调一点规则引擎最重要的是可观测性。每一条规则的命中次数、拦截率、误杀率都要埋点统计。如果一条规则天天拦截却从来没确认过一笔真实风险那这条规则大概率误伤了正常用户需要及时调优。没有可观测性的风控系统等于蒙着眼睛开枪。4.3 KYC、AML与审计日志合规审查看似麻烦实则保护你金融项目绕不开合规。KYC了解你的客户和AML反洗钱是合规检查中最基础也最关键的部分。很多技术团队觉得这些是法务的活儿跟开发没关系。其实关系大了——KYC要求你在用户注册、交易前完成身份核验这意味着系统要从一开始就设计好身份信息采集、证件上传、活体检测的流程AML要求对异常交易模式做监控和上报这意味着你要埋点记录每一笔交易的特征数据并能按监管要求导出报表。审计日志更是重中之重。我这里说的审计日志不是普通的应用日志而是专门为合规审计准备的、不可篡改的操作记录。谁在什么时间操作了哪笔交易、看了哪个用户的资料、改了什么配置全部要有记录并且日志要存够法律要求的时限。为了单纯的技术价值这个模块看起来又不产生任何业务收益很多团队拖着不做。但一旦遇到监管检查或者用户投诉没有审计日志系统连自证清白的能力都没有。技术上审计日志可以单独写一张表也可以同步到独立的日志平台。我们当时把审计日志和业务日志分开存储审计日志只有专门的授权接口能写普通业务代码没有写入权限最大程度避免被链路中其他操作污染。5. 对账、结算与安全上线后最磨人的三个环节5.1 对账不能只靠人肉核对要有自动化机制对账环节是金融项目上线后最先暴露问题的地方。每天产生的交易流水成千上万你不可能让运营同事下载Excel一笔一笔去核。自动化的对账机制必不可少。我们搭建的对账系统分为内部对账和外部对账。内部对账在账务系统内部跑每天凌晨定时任务把当天的交易流水、账户流水、余额变动三方的汇总数据拉出来做一致性校验。外部对账则是和渠道方比如支付通道、银行拉取对方账单和我们系统内的交易记录做比对。比对结果分成三类双方一致的、我方有对方无的、对方有我方无的。后面两类就是差错需要进入差错处理流程。对账逻辑本身不难难的是数据量上来之后的性能问题。几百万条流水做关联比对SQL怎么写都会慢。我们的方案是把双方数据都导到ClickHouse里用列存储做高效的聚合与关联查询跑一批对账任务从几十分钟降到了几分钟。这个小优化解决了一个大痛点。5.2 结算系统为什么要单独拆一个模块出来结算和交易很容易被人当成一回事但它们在业务上是严格分离的。交易负责把资金从A账户挪到B账户瞬间完成结算负责把一段时间内的交易汇总计算向商户或合作方支付应得的款项。一个清晰的理解是交易是过程结算是结果。我们当时的做法是单独建了结算系统每天按业务规则计算各商户的应收款项、手续费、退款冲抵生成结算单再通过出款通道发起打款。这个系统关键点在于结算规则的维护和结算单的审批流程。结算规则涉及费率、分润、阶梯价写死在代码里就是给自己埋雷。我们是把费率配置表化并且提供试算接口运营人员可以随时模拟结算结果确认无误后再正式生成结算单。结算单审批则走了工作流引擎金额达到一定阈值必须有财务负责人审批才能出款。这套流程虽然加上了一些管理成本但有效防止了Ops误操作把不该打的钱打出去。金融服务宁可慢一点不能错一分。5.3 数据安全加密、脱敏与权限控制的实践经验金融服务的数据安全是不能妥协的。用户手机号、身份证号、银行卡号、交易金额任何一个泄露出去都是重大事故。我们落地的安全措施有三层。第一层是存储加密。敏感字段使用AES-256加密后入库密钥由独立的密钥管理系统KMS管理应用本身不保存明文密钥。就算数据库被人拖走拿到的是密文没有密钥也解不开。第二层是传输加密。内部服务之间调用统一走TLS禁止裸HTTP。第三层是权限控制。数据库账号最小权限原则应用账号只能访问应用需要的表平台侧的管理后台则按角色分配权限每个账号能看到的字段都不一样比如客服能看到用户手机号后四位但看不到完整号码。脱敏这件事也有讲究。不是所有场景都需要全量数据能脱敏就脱敏。我们的原则是“展示端默认脱敏确需明文才申请查看权限且查看行为留痕”。这条原则看着简单执行起来需要前端、后端、网关、审计多方配合但一旦落地很多安全漏洞自然就堵上了。6. 监控告警与性能压测上线之前没人关心出事之后人人追问6.1 监控指标从技术指标到业务指标一条都不能少金融服务的监控分两层一层是技术监控一层是业务监控。技术监控大家比较熟悉——CPU、内存、磁盘、接口响应时间、错误率、MQ堆积量这些属于必须覆盖的基础项。业务监控就相对容易遗漏但金融项目里业务监控的价值甚至更高。我重点说一下我们针对业务侧的监控设计。首先是对账差异监控每天对账任务跑完只要出现差异条目立刻发告警到企业微信和邮件不等第二天早上才发现前一天账对不平。然后是交易成功率监控按渠道、按业务类型、按支付产品分别统计成功率任何一个维度的成功率跌出阈值就触发告警。还有一个容易被忽略的是金额异常波动监控比如某段时间充值金额突然暴增或锐减背后可能是渠道故障、可能是营销活动异常、也可能是盗刷团伙在测试不管哪种都需要人工介入确认。实时监控的底层用的是Prometheus加Grafana告警规则在Prometheus里配置。业务指标的采集则是在应用里埋点通过Micrometer暴露给Prometheus拉取。这个架构不算复杂但能把业务异常暴露的窗口从“用户投诉才知道”缩短到“分钟级发现”。6.2 压测不能只压happy path要故意搞破坏性能压测是类比“体检”不能只测健康状态还得测极限状态下的反应。很多团队压测只压最主流程接口全部成功响应时间一片绿就觉得系统没问题。但这种压测意义很有限因为生产环境最大的挑战是异常不是正常。我们的压测分三类。第一类是基准压测测单接口的正常吞吐量和响应时间第二类是容量压测模拟真实业务配比把多接口混合压到系统瓶颈找出哪个模块最先扛不住第三类是故障演练人为制造故障——杀掉一个实例、断开数据库连接、让Redis超时看系统能不能自动摘除故障节点、降级、重试。第三类最有用。我们第一次做故障演练时杀掉一个Kafka消费者实例结果发现消息消费出现了分钟级延迟原因是消费者组内的重平衡机制没有合理配置。这个问题在正常的容量压测里完全暴露不出来但在真实事故中可能就是一笔交易迟迟不到账的用户投诉。压测的价值就在于提前暴露这些隐患让问题死在测试环境而不是生产环境。6.3 线上事故的应急响应流程必须演练到形成肌肉记忆最后还想聊聊应急响应。金融项目出事故和普通项目不一样普通项目挂了还能说是“服务不可用”金融项目如果资金数据有问题哪怕一分钟都是大事。所以一套成熟的应急响应流程非常重要。我们的流程是发现告警后第一时间确认影响范围然后根据影响范围决定响应级别。P0级资金数据异常、核心交易大面积失败要在5分钟内拉起应急小组同时通知所有相关研发上线P1级局部功能异常、部分用户受影响在15分钟内响应。响应过程中有一个关键动作——先止损再排查。宁可让部分交易暂停也不能让错误交易继续扩大。很多新手工程师习惯先排查原因再恢复服务但在金融场景里先止损永远是第一优先级。每一次事故处理完之后还必须做复盘而且复盘不是走过场。我会要求写出时间线、根本原因、改进措施、验证方案并指定负责人和截止日期。这些复盘报告最后都会沉淀成团队的wiki文档下次再遇到类似问题直接查阅历史能省下大量排查时间。7. 一些只能从实践中得来的体会写到这里financial-services的核心模块基本都过了一遍。最后想聊几句纯个人层面的感受。第一句体会是金融项目的核心不是技术炫技而是确定性。用户发起一笔交易他需要知道这笔交易一定不会丢、不会错、不会重复扣款。技术架构里的每一个设计都应该是为了增加这种确定性服务。那些花里胡哨的、难维护的、只有少数人能看懂的方案在金融项目里几乎都不该用。第二句体会是账务系统是牵一发动全身的系统改任何一行代码都要有敬畏之心。我见过团队里最资深的工程师在改动账务代码时第一件事不是写代码而是打开原有代码逐行读一遍把所有分支和边界条件全部列出来才敢动手。这种谨慎是金融项目从业者的基本素养。第三句体会是文档和复盘比写代码还要重要。金融系统里很多逻辑是“一个人写出来十个人维护”没有清晰的设计文档和变更记录后续维护者根本不知道当初为什么这么设计。踩过的坑如果只是记在自己脑子里下次别人还会再踩一遍。把这些沉淀下来是整个团队往后少走弯路的捷径。如果你正在规划或者正在做类似的金融服务项目这套方案里的每一个模块都可以作为参考骨架。做金融不容易但把基础设施打扎实之后后面的路会越走越顺。
返回列表