拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

金融后端系统实战:账务、风控与高可用架构设计

金融后端系统实战:账务、风控与高可用架构设计 1. 从“financial-services”这个标题里我读出了什么“financial-services”这个标题看起来简单甚至有点过于宽泛但它恰恰是那种最考验拆解能力的题目。没有项目正文没有关键词没有摘要描述只有一个孤零零的标题。这种情况下我反而觉得更有意思——因为它逼着我去想一个从业者看到“financial-services”这个词脑子里第一时间浮现的到底是什么我的第一反应不是“金融行业”而是“金融行业里那些真正需要被解决的技术问题”。这两者之间的差别很大。前者是一个行业标签后者才是能落地的东西。金融服务的范畴太广了银行、保险、证券、支付、借贷、理财、风控、清算、合规每一个细分方向都有自己独特的技术栈和业务逻辑。但不管哪个方向有几个核心命题是绕不开的数据的一致性、交易的安全性、系统的可用性、以及合规的严肃性。我之所以这么拆是因为在实际工作中我见过太多人一上来就聊“金融级架构”“分布式事务”“高并发”但连最基本的账务对平逻辑都没搞清楚。金融服务的本质是什么说到底就是记账、算账、对账这三件事。所有的技术选型、架构设计、流程规范最终都要服务于这三个动作的准确性和可靠性。你可以在上面叠加风控模型、实时计算、智能推荐但底层如果账记错了上面盖再多楼都是空中楼阁。所以这篇内容我打算从“一个金融服务的后端系统到底该怎么搭”这个角度切入。不聊虚的就聊我在实际项目里踩过的坑、做过的取舍、以及那些文档里不会写的经验。适合谁看如果你正在做支付、账务、清结算、风控相关的系统或者你所在的项目突然被要求“支持金融业务”那这篇内容应该能帮你少走一些弯路。如果你只是对金融科技感兴趣想了解这个领域的技术特点那也可以当个入门参考。2. 账务核心为什么“记对一笔账”比“扛住一万并发”更难2.1 复式记账不是历史包袱而是数据一致性的基石很多人第一次接触金融系统时会觉得复式记账Double-Entry Bookkeeping是个老古董——不就是每笔交易记两条记录嘛一条借一条贷有什么了不起的我一开始也这么想直到有一次我们做对账时发现差了三分钱排查了整整两天才定位到问题一个退款场景下我们只更新了用户余额忘了同步更新平台收入账户。单边记账的隐患就在这儿——你永远不知道钱从哪儿来、到哪儿去出了问题只能靠翻日志大海捞针。复式记账的核心价值不在于“记两笔”而在于它强制你描述资金的流向。每一笔交易都必须有明确的借方和贷方而且借贷必须相等。这个约束看起来简单但它带来的好处是巨大的任何时候你都可以通过“资产 负债 所有者权益”这个恒等式来验证账务是否正确。如果不等那一定有问题而且问题一定出在某个具体的分录上而不是“系统整体状态不明”。在实际实现中我建议把账务系统拆成三个层次交易层、分录层、余额层。交易层负责接收业务请求比如“用户A向用户B转账100元”分录层负责把交易翻译成借贷分录比如“借用户A账户 100贷用户B账户 100”余额层负责维护每个账户的实时余额。这三层之间通过消息队列解耦交易层只管受理分录层只管记账余额层只管更新。这样做的好处是每一层都可以独立扩展和容错而且出了问题可以精确定位到某一层。注意分录层的借贷方向一定要和业务语义严格对应。我见过有的团队把“借”和“贷”当成纯粹的技术字段结果业务方看不懂财务方对不上最后只能推倒重来。建议在代码里用枚举明确标注每个分录的业务含义比如“DEBIT_USER_BALANCE”和“CREDIT_PLATFORM_REVENUE”别偷懒用0和1。2.2 余额更新为什么“先查后改”是灾难的开始余额更新看起来简单——查一下当前余额加上或减去交易金额再写回去。但如果你真这么写恭喜你你已经埋下了一个经典的并发bug。两个请求同时读到余额100一个加50一个减30最后写回去的可能是150或者70取决于谁后写。这就是典型的丢失更新问题。正确的做法有三种我按推荐程度从高到低排列第一种是数据库行锁。在查询余额时加上FOR UPDATE这样第二个请求会阻塞直到第一个请求提交。这种方式最简单但性能最差因为锁的粒度是行级别的高并发下会形成排队。适合交易量不大的场景比如企业内部财务系统。第二种是乐观锁。在余额表里加一个版本号字段更新时检查版本号是否变化。如果变了就重试。这种方式性能好但需要处理重试逻辑而且在高冲突场景下重试次数会很多。适合冲突概率低的场景比如用户余额变动不频繁的理财系统。第三种是原子更新。直接用UPDATE account SET balance balance - 100 WHERE account_id ? AND balance 100这样的语句把计算和更新合并成一个原子操作。这种方式性能最好而且天然避免了超扣问题。但缺点是只能做简单的加减复杂的业务逻辑比如手续费计算、分润没法直接塞进去。我个人的经验是核心账务用原子更新复杂业务用乐观锁内部管理系统用行锁。不要试图用一种方案解决所有问题那只会让系统变得又慢又难维护。2.3 对账不是“出了问题才做”而是“每天必须做”对账这个词很多技术同学觉得是财务的事跟自己没关系。但我要说对账是金融系统最后一道防线。没有对账你永远不知道系统里的钱和实际的钱是不是一致的。我见过一个团队上线三个月没做过一次完整对账结果某天发现平台多出了几十万查了半天才发现是某个退款接口重复调用了。对账的核心逻辑很简单拿系统内的账务记录和外部渠道银行、支付网关、清算所的记录逐笔比对。但实际操作中难点在于数据格式不一致、时间窗口不一致、以及异常处理。比如银行给你的对账文件是T1的但你的系统是实时的那你就需要把系统内的交易按银行的时间窗口重新聚合。再比如有些交易在系统内是成功的但银行那边是失败的这就需要做冲正处理。我的建议是对账系统要独立于交易系统用单独的数据库和计算资源。因为对账往往是批量任务会消耗大量IO和CPU如果和交易系统混在一起高峰期会互相影响。另外对账结果一定要有可视化报表让财务和运营能直接看到差异明细而不是只给技术看日志。我见过最离谱的对账系统差异结果只输出到一个文本文件里财务要看还得找技术导数据这完全失去了对账的意义。3. 风控与合规那些“业务说不用管”但技术必须管的事3.1 风控不是“加个规则引擎”就完事了一提到风控很多人第一反应是“上个规则引擎配置几条规则”。但规则引擎只是工具真正难的是规则的设计和迭代。我见过一个团队花了大价钱买了某商业规则引擎结果配了三条规则就上线了风控效果约等于零。为什么因为规则不是拍脑袋想出来的而是从数据里挖出来的。一个基本的风控体系应该包含三个层次事前拦截、事中监控、事后分析。事前拦截就是实时判断这笔交易要不要放行比如“同一IP一分钟内下单超过10次”就拒绝。事中监控是交易发生后的短时间内比如5分钟做二次校验比如“用户突然在异地登录并大额转账”就触发人工审核。事后分析是T1的离线计算用来发现新的风险模式比如“某个商户的退款率突然飙升”。在技术实现上事前拦截对延迟极其敏感通常要求P99在50毫秒以内所以规则要尽量简单能用内存计算的就别查数据库。事中监控可以稍微宽松一些几百毫秒也能接受可以用更复杂的模型。事后分析就是离线任务了随便跑跑几个小时都没关系。提示风控规则一定要有灰度发布和快速回滚机制。我经历过一次事故一个新规则上线后误杀了大量正常交易结果因为没有回滚开关只能紧急发版耽误了将近一个小时。从那以后我要求所有风控规则都必须支持热更新和秒级回滚。3.2 合规要求技术方案必须留出的“审计接口”合规这个词听起来很虚但落到技术上非常具体。比如**反洗钱AML**要求你能够追溯每一笔资金的来源和去向**了解你的客户KYC**要求你保存用户的身份信息和交易记录数据保护要求你对敏感信息加密存储。这些都不是“业务说不用管”就能糊弄过去的。我的经验是在系统设计初期就要预留审计日志和数据留存的能力。审计日志不是普通的业务日志它需要满足几个特殊要求不可篡改、可追溯、保留时间长。不可篡改意味着你不能用普通的文件日志因为文件可以被删除或修改。通常的做法是写入专门的审计表并且只允许插入不允许更新和删除。可追溯意味着每条审计记录都要包含完整的上下文比如操作人、操作时间、操作对象、操作前后的值。保留时间长意味着你可能需要保存5年甚至更久这对存储成本是个考验。另外数据脱敏也是合规的硬要求。用户的身份证号、银行卡号、手机号在日志和数据库中不能明文存储。我见过有的团队为了排查问题方便直接把完整卡号打到日志里这是非常危险的。正确的做法是日志里只记录卡号的后四位数据库里存储加密后的密文密钥单独管理。3.3 权限控制为什么“最小权限原则”在金融系统里是生死线在普通互联网系统里权限控制往往比较粗放比如“管理员能看所有数据”。但在金融系统里这种粗放是致命的。一个客服人员如果能随意查看用户的完整交易记录那他就可能泄露隐私一个运营人员如果能随意修改费率那他就可能造成资金损失。最小权限原则Principle of Least Privilege要求每个角色只能访问完成其工作所必需的最小数据集和操作集。比如客服只能看到用户的基本信息和最近三笔交易不能看到完整的历史记录运营只能配置费率但不能修改已经生效的费率开发只能访问测试环境不能访问生产数据。实现上我推荐用基于角色的访问控制RBAC加上属性基访问控制ABAC的混合模型。RBAC负责粗粒度的角色划分比如“客服”“运营”“财务”“管理员”。ABAC负责细粒度的动态判断比如“客服只能在工作时间访问用户数据”“运营只能修改自己负责的产品线的费率”。这样既能保证灵活性又能保证安全性。4. 高可用与容灾金融系统“不能停”背后的技术账4.1 多活架构不是“多部署几套”那么简单金融系统对可用性的要求极高通常要求99.99%甚至99.999%的可用性。这意味着一年只能停几分钟。单机房部署显然无法满足这个要求所以需要多活架构。但多活不是“在多个机房部署几套系统”就完事了它涉及到数据同步、流量调度、故障切换等一系列复杂问题。数据同步是多活架构里最难的。如果两个机房都接受写入那就需要考虑冲突解决。比如用户在北京机房发起了一笔转账同时在上海机房发起了另一笔转账两边的余额更新如何合并常见的方案有最后写入胜出LWW、向量时钟、CRDT等但每种方案都有其适用场景和局限性。我的经验是如果业务允许尽量采用单元化架构即按用户维度把流量切分到不同机房同一个用户的请求永远路由到同一个机房。这样就不存在跨机房的写冲突只需要做异步的数据复制。流量调度方面通常用DNS或者专用的负载均衡设备来做机房级别的切换。但要注意切换不是瞬间完成的因为DNS有缓存客户端可能还在往旧机房发请求。所以切换过程中需要双写或者请求转发确保不丢数据。4.2 降级与熔断什么时候“主动放弃”比“硬扛”更明智金融系统在高压力下最怕的不是某个功能不可用而是整个系统雪崩。比如一个查询接口因为数据库慢查询导致线程池耗尽进而拖垮了整个应用。这时候熔断和降级就是保命的手段。熔断的意思是当某个依赖服务的错误率超过阈值时自动切断对该服务的调用直接返回兜底结果。比如风控服务挂了那就暂时放行所有交易而不是让所有交易都卡在风控调用上。降级的意思是当系统压力过大时主动关闭一些非核心功能把资源留给核心功能。比如关闭交易记录的实时查询只保留交易提交功能。我见过一个团队他们的降级策略是“按用户等级降级”——VIP用户的查询功能保留普通用户的查询功能关闭。这个策略看起来很合理但实际执行时出了问题普通用户虽然查询不了但他们的交易请求还在结果交易和查询抢资源最后还是雪崩了。所以降级一定要按功能维度做而不是按用户维度。核心交易链路必须优先保障查询、报表、推荐这些都可以牺牲。注意降级开关一定要做成动态配置不能写死在代码里。我推荐用配置中心来管理降级开关这样出问题时可以秒级生效不用等发版。另外降级开关要有自动触发和手动触发两种模式自动触发基于监控指标手动触发留给人工判断。4.3 数据备份与恢复你备份了但你真的能恢复吗备份这件事很多团队都在做但真正能恢复的没几个。我见过一个团队每天定时备份数据库备份文件也存到了对象存储看起来万无一失。结果有一次真的需要恢复时发现备份文件是坏的因为备份脚本没有做校验。还有的团队备份了但恢复流程从来没演练过真到用时手忙脚乱恢复花了十几个小时。我的建议是备份必须做校验恢复必须做演练。校验的方式很简单备份完成后随机抽取几条记录和源库比对。恢复演练至少每季度做一次在独立的隔离环境里用最新的备份文件完整恢复一遍记录恢复时间和遇到的问题。只有演练过的恢复流程才是真正可靠的。另外备份的保留策略也很重要。金融数据通常要求保留5年以上但你不能把所有备份都放在高性能存储上成本太高。通常的做法是最近7天的备份放在高性能存储上方便快速恢复7天到1个月的备份放在普通存储上1个月以上的备份归档到冷存储。这样既能满足合规要求又能控制成本。5. 技术选型金融场景下那些“反直觉”的决策5.1 为什么我们放弃了微服务回归了单体微服务这几年很火很多团队一上来就搞微服务觉得这样才“先进”。但我在金融项目里的经验恰恰相反核心账务系统单体比微服务更合适。原因有三点。第一事务一致性。微服务架构下跨服务的事务只能用分布式事务或者最终一致性。分布式事务性能差、复杂度高最终一致性又需要业务方做补偿逻辑。而账务系统对一致性的要求是强一致的任何中间状态都可能导致资金差错。单体架构下一个本地事务就能搞定简单可靠。第二调用链路。微服务架构下一笔交易可能要经过网关、交易服务、账务服务、风控服务、通知服务等好几个环节每个环节都有网络开销和故障风险。单体架构下这些都是进程内调用延迟低、故障点少。第三运维复杂度。微服务意味着更多的部署单元、更多的监控指标、更多的故障排查路径。对于金融系统来说稳定性是第一位的运维复杂度越低越好。当然我不是说金融系统不能用微服务。外围系统比如用户管理、产品管理、报表查询用微服务没问题。但核心账务我强烈建议用单体或者至少是“模块化单体”——代码层面分模块部署层面是一个应用。5.2 数据库选型为什么我们最终选了关系型数据库现在NoSQL很流行MongoDB、Cassandra、HBase各有各的优势。但在金融核心系统里我依然推荐关系型数据库比如MySQL或者PostgreSQL。原因很简单ACID。金融交易需要原子性、一致性、隔离性、持久性这些是关系型数据库几十年积累下来的强项。NoSQL通常在CAP理论里选择了AP牺牲了一致性这在金融场景里是不可接受的。当然关系型数据库也有它的局限比如水平扩展困难。但我的经验是金融系统的瓶颈通常不在数据库而在业务逻辑和网络。通过合理的分库分表、读写分离、缓存策略关系型数据库完全可以支撑千万级甚至亿级的交易量。我参与过的一个支付系统用MySQL分库分表单日交易量超过5000万笔运行了三年多没出过大的数据问题。如果非要用NoSQL我建议只用在非核心场景比如交易日志、用户行为分析、风控特征存储。这些场景对一致性的要求没那么高但对写入性能和扩展性要求高NoSQL更合适。5.3 消息队列Kafka还是RocketMQ消息队列在金融系统里主要用来解耦和削峰。比如交易提交后通过消息队列异步通知账务系统记账、通知风控系统做检查、通知通知系统发短信。选型上Kafka和RocketMQ是最常见的两个选择。Kafka的优势是吞吐量极高适合大数据场景。但它的延迟相对较高而且消息顺序只能保证分区内有序全局有序需要额外设计。RocketMQ的优势是低延迟和事务消息特别适合金融场景。事务消息可以保证“本地事务和消息发送”的原子性这在账务系统里非常有用。我的建议是核心交易链路用RocketMQ因为它的低延迟和事务消息特性更贴合金融需求。数据分析和日志采集用Kafka因为它的高吞吐量更适合大数据场景。当然如果团队已经有Kafka的运维经验用Kafka也不是不行但需要额外注意消息顺序和事务问题。提示不管用哪个消息队列消息幂等都是必须处理的。因为网络抖动、消费者重启等原因消息可能重复投递。金融系统里重复消费意味着重复记账这是绝对不能接受的。通常的做法是在消费者端用唯一业务ID做去重比如交易流水号。6. 那些只有踩过坑才知道的实操细节6.1 金额计算为什么我们放弃了浮点数浮点数在金融计算里是绝对禁止的。原因很简单0.1 0.2在浮点数里不等于0.3而是0.30000000000000004。这种精度误差在普通应用里可能无所谓但在金融系统里一分钱的误差都可能导致对账失败。正确的做法是用整数存储金额单位是“分”。比如100元存储为10000分。所有计算都用整数运算最后展示时再除以100。这样完全避免了精度问题。如果业务需要更小的单位比如“厘”或者“毫”那就把单位再缩小用“厘”存储展示时除以1000。Java里可以用BigDecimal但要注意BigDecimal的equals和compareTo行为不同。equals会比较精度compareTo只比较数值。比如new BigDecimal(1.0).equals(new BigDecimal(1.00))返回false但compareTo返回0。在金融系统里我建议统一用compareTo来比较金额避免精度带来的意外。6.2 时间处理时区、闰秒、以及“为什么你的对账总是差一天”时间处理在金融系统里是个大坑。首先所有时间都必须带时区。我见过一个系统服务器用UTC数据库用本地时间结果跨时区交易的时间戳全乱了。正确的做法是存储用UTC展示用本地时区。数据库里统一存UTC时间前端根据用户时区做转换。其次闰秒问题。虽然闰秒不常见但金融系统对时间精度要求高闰秒可能导致时间戳重复或跳跃。通常的做法是用TAI时间或者单调时钟来避免闰秒影响。不过大多数业务系统用UTC就够了只要注意不要在闰秒发生时做时间敏感的操作。最后对账的时间窗口。银行给你的对账文件通常是按银行所在时区的自然日切分的而你的系统可能是按UTC切分的。如果不做转换对账就会差一天。我的经验是对账系统里要明确记录渠道时区和系统时区并且在比对前统一转换到同一个时区。6.3 日志与监控出了事你怎么在5分钟内定位问题金融系统出问题时定位速度比修复速度更重要。因为修复可能需要发版、回滚、数据订正这些都需要时间。但定位如果慢了影响面就会扩大。我的经验是日志和监控要围绕“交易链路”来组织。具体来说每一笔交易都要有一个全局唯一ID比如交易流水号这个ID要贯穿整个链路——从网关接收到交易提交、账务记账、风控检查、通知发送每个环节的日志都要带上这个ID。这样出问题时你只需要拿这个ID去日志系统里搜一下就能看到完整的链路。我推荐用ELK或者Loki来做日志聚合支持按ID快速检索。监控方面除了常规的CPU、内存、QPS、延迟还要有业务指标监控。比如“每分钟成功交易数”“每分钟失败交易数”“账务借贷不平的记录数”“对账差异笔数”。这些业务指标比技术指标更能反映系统的真实健康状态。我见过一个系统技术指标一切正常但业务指标显示失败交易数在飙升原因是某个渠道的接口返回了新的错误码而系统没有正确处理。注意监控告警一定要有分级。P0告警比如核心交易失败率超过1%要打电话P1告警比如对账差异超过10笔要发短信P2告警比如某个非核心接口延迟升高发邮件就行。不分级的告警等于没有告警因为大家会麻木。6.4 上线与回滚金融系统的“手术”怎么做才安全金融系统的上线我习惯称之为“手术”。因为一旦出错影响的是真金白银。所以上线流程必须极其严谨。我的经验是任何上线都必须有回滚方案而且回滚方案必须经过验证。具体来说上线前要做几件事第一代码审查至少两个人看过重点检查资金相关的逻辑。第二测试覆盖核心链路的单元测试覆盖率要达到80%以上集成测试要覆盖所有异常分支。第三灰度发布先切1%的流量观察至少30分钟确认没问题再逐步扩大。第四回滚演练在测试环境里模拟回滚确保回滚脚本能正常工作。上线后要密切监控至少观察2小时。重点看业务指标比如成功率、失败率、对账差异。如果发现异常立即回滚不要犹豫。我见过有的团队上线后发现失败率升高但觉得“再观察观察”结果拖了半小时影响了几万笔交易。回滚虽然麻烦但比数据订正简单多了。7. 写在最后一些个人的体会做金融系统这些年我最大的体会是技术很重要但比技术更重要的是对业务的理解和对风险的敬畏。你可以用最先进的技术栈但如果不懂账务逻辑照样会写出有问题的系统。你可以用最牛逼的架构但如果对风险没有敬畏一次疏忽就可能造成不可挽回的损失。另一个体会是金融系统没有“差不多”。普通系统里一个bug可能只是页面显示错了用户刷新一下就好。金融系统里一个bug可能就是资金损失而且很难追回。所以做金融系统必须要有“如履薄冰”的心态每一个决策都要问自己如果这里出错了最坏的结果是什么我能不能承受最后如果你正在做金融系统或者准备做金融系统我建议你多和财务、运营、风控的同事聊天。他们可能不懂技术但他们懂业务懂风险。很多技术上的坑其实业务方早就踩过了只是你不知道而已。技术是为业务服务的脱离业务的技术方案再优雅也是空中楼阁。
返回列表