我刚接手代号financial-services这个项目时,收到的输入少得可怜:一个空目录、一个史诗级 Jira 标题、一段不到三行的需求描述——“打通各类金融服务,统一客户视图,提升响应速度”。说白了,客户方只给了名字,没给答案。但恰恰是这个看似宽泛的代号,决定了整个项目不能只做一个 App 或单一接口,而是要建一套能长期支撑金融业务的基座系统。
这篇内容就是围绕financial-services项目的完整落地记录。我会从需求拆解、技术选型、数据整合、交易链路优化、权限安全、可观测性建设一直讲到复盘反思,尽量把每一步“为什么这么做”和“踩过的坑”都写清楚。适合正在做金融行业信息化、中后台系统改造、或者准备从单体向微服务迁移的团队参考,哪怕你不是做金融的,里面关于边界梳理、数据建模和性能治理的思路也照样能抄作业。
1. “financial-services”不只是一个代号:需求边界的确认过程
1.1 需求文档只有三行,怎么往下拆
很多技术朋友拿到这种项目第一反应是“开始搭架构”,这是大忌。financial-services这种命名摆明了是一个领域级项目,里面藏着的业务链路比目录名复杂得多。我拿到手的第一件事,不是写代码,而是拉上业务方在白板前把“金融服务”四个字翻译成具体动作。
当时我们拆出来的核心业务链路大致是这样:客户进入系统、完成身份准入、建立账户、发生交易、产生产品推荐、形成各类通知触达、最后走日终对账。如果再往下拆,每一环还能继续细分,比如“账户”可能包括主账户、子账户、冻结账户、利息账户;“交易”又分支付、转账、退款、调账。第一版需求描述里根本没有这些细节,但作为技术负责人,如果不去主动问清楚,等系统上线再做补偿就是灾难。
一个很实用的方法:把当前阶段能列出的所有业务能力列成一张能力地图,然后标注哪些是现有系统已经有的,哪些是本次必须新建的,哪些是以后再说的。financial-services项目最终圈定的能力边界不是“全部金融服务”,而是“客户、账户、交易、产品、通知、审计”这六个核心域。后面所有架构决策都围绕这六个域展开,超出范围的诉求先记录、不实现。
1.2 架构目标:统一入口、统一数据、统一处理
需求边界确认完之后,客户方业务负责人说了一句很关键的话:“我们现在最大的问题不是功能少,而是每个功能散落在不同系统里,客户信息对不上,交易数据对不上,连客户打客服电话都要问三遍身份信息。”这直接决定了financial-services的架构目标,不是做一堆新功能,而是做一个统一层:统一入口接收请求,统一数据口径,统一处理流程。
落地时我定了一个量化目标:客户主数据唯一率不低于 99%,核心交易接口 TP99 小于 200ms,日终对账差错率低于万分之一。这些数字不是拍脑袋,而是跟业务方确认过现有痛点和期望值后定下来的。没有这些数字,后面的性能优化、数据治理工作都会失去方向。
你可能会问,既然只是做统一层,为什么不直接在老系统上改?原因也很简单:老系统之间有大量点对点接口,接口格式和字段口径互相不一致,改造成本甚至高于重新构建一套新基座。而且历史系统的部署方式、数据库选型都各不相同,强行原地改造只会让系统更脆弱。
2. 技术栈选型与最终落地的第一手评估
2.1 为什么不选“全家桶”
这个项目启动时,团队内部对技术选型是有争论的。有人提 Spring Cloud 全家桶,有人提 Service Mesh,还有人建议直接把核心链路换成 Go 重写。我最后给出的方案是:应用层以 Java 为主,Spring Boot 做服务框架,Spring Cloud 只保留注册发现和配置管理两个组件;数据层采用 MySQL + Redis + ClickHouse 的组合;消息队列选 Kafka。团队规模不大,技术栈越贴近团队既有能力,越容易落地。
这里我必须说一句大实话:financial-services这种项目,技术上真正难的不是用多高新的框架,而是怎么把数据一致性、幂等、对账这些金融基本功做扎实。全家桶带来的额外复杂度,在项目初期只会拖慢进度。服务注册发现用 Nacos,配置中心也用 Nacos,其他如网关、熔断、链路追踪单独引入轻量组件,而不是把一个全家桶整体梭哈进来。
2.2 服务拆分的执行细节
六个核心域对应六组微服务:客户中心、账户中心、交易中心、产品中心、消息中心、审计中心。每个中心内部又拆出独立的读写服务和异步任务。拆分原则是“数据归属决定服务边界”:客户相关数据只归客户中心管,账户余额只由账户中心变更新,交易中心不直接操作客户表,只能通过接口调用。
| 服务中心 | 核心职责 | 主要数据 |
|---|---|---|
| 客户中心 | 客户准入、身份信息维护、客户标签 | 客户主表、联系方式、证件信息 |
| 账户中心 | 账户开立、余额冻结/解冻、账户状态管理 | 账户表、余额表、冻结流水 |
| 交易中心 | 交易受理、风控预检、记账、冲正 | 交易流水、交易明细 |
| 产品中心 | 产品定义、推荐策略、价格计算 | 产品表、产品规则配置 |
| 消息中心 | 短信/站内信/APP推送触达 | 消息模板、发送记录 |
| 审计中心 | 操作日志、登入登出、权限变更留痕 | 审计日志、风控事件 |
服务拆分时最容易犯的错就是“过度抽取公共模块”。我们早期为了减少重复代码,抽了一个通用“状态机组件”,结果每个业务域的状态流转规则根本不一样,硬套通用组件导致配置比写代码还复杂。后来把这个组件打散,改成每个中心内部的简单枚举驱动,复杂度立刻降下来了。微服务边界宁可先粗一点,也不要一开始就追求完美的领域划分。
2.3 测试与联调环境的搭建
这个项目联调环境的搭建比预想的花时间。六个中心外加网关和基础组件,近 20 个服务实例,如果全部起在本地开发机上,配置管理就是一场噩梦。我们最终用 Docker Compose 编排了一套最小联调环境:每个服务只保留一个实例,数据库用独立的测试实例,消息队列直接连共用的 Kafka,这样一套环境足以支撑前后端联调和大部分集成测试。
测试环境还有个细节值得说:所有测试数据必须打上环境标识,比如手机号前缀加TEST,证件号用固定的测试号段,避免联调时误发真实短信或调用真实第三方接口。我们当时因为没有统一测试数据规范,出现过短信网关把测试消息发给真实用户的事故,后面专门加了一层测试号码白名单才解决。这个坑,做金融项目的团队建议直接当成标准配置来做。
3. 统一客户视图:主数据清洗与ID-Mapping实战
3.1 重复客户的识别
客户中心上线前,最让人头疼的不是写接口,而是把几套老系统里的存量客户数据合并成一套干净的主数据。当时老系统里同一客户可能有三个不同档案:手机号 A 注册过一个账号,身份证号 B 又注册过另一个账号,邮箱 C 还关联着第三个账号。如果不去识别这些重复数据,后面所有客户标签、资产汇总、风险评估都会出问题。
第一轮清洗用的是规则匹配:身份证号完全一致、手机号完全一致、身份证号+姓名同时一致的,直接合并。规则匹配可以解决大部分问题,但还有一批数据连证件号都没填,只能靠相似度计算。我们当时给每个客户生成一个由昵称、手机尾号、地区等字段构成的向量,用 SimHash 做粗筛,再对候选对做字段级加权相似度计算,相似度超过阈值的进入人工审核队列。
这里有一个非常关键的实操细节:合并客户数据必须保留“合并前历史档案”和“合并轨迹”。不能直接把 A 档案删掉把数据挪到 B 档案里,因为下游系统的历史交易记录可能还引用着 A 档案的 ID。我们用的办法是增加一个客户映射表,记录的是新主客户 ID 和所有历史客户 ID 的对应关系,所有下游查询先走映射表,再查主数据。这样历史数据不丢,新老链路都能跑通。
3.2 客户全景模型设计
客户全景视图不是简单地把老系统的字段拼在一起,而是重新建模。最终落地的核心表结构大致是:客户主表存储基础属性(姓名、性别、出生日期、状态);联系方式表存储手机号、邮箱、地址,并标记是否验证过;资产汇总表实时或准实时地冗余客户在各账户下的总资产;标签表用 KV 结构存客户偏好、风险等级、渠道来源等扩展属性。
给账户和产品做关联时,我们踩了一个设计上的坑:一开始想把“客户持有产品”的关系直接存在客户主表里,后来发现一个客户可以同时持有存款、理财、保险多种产品,而且产品状态独立变化,这种多对多关系塞在主表里纯属自讨苦吃。最后拆成了独立的客户产品关系表,每条关系记录带业务角色字段,比如“持有人”“代办人”“受益人”。这个调整花了一周重构,但换来的是后面推荐系统和资产汇总逻辑都清晰了很多。
3.3 数据时效与同步策略
统一客户视图不能只靠实时接口,否则业务高峰期能把数据库打爆。我们采用了混合时效策略:客户基础信息、账户状态这类对实时性要求高的数据走接口同步;资产汇总、产品持有关系、标签计算这类对实时性要求不那么高的数据,走 T+1 离线任务或者分钟级准实时任务。ClickHouse 在这里承担了大部分聚合查询的压力,业务库只保留当前状态,历史分析全部走数据仓库。
准实时同步链路用的是 Kafka + Flink:源系统变更后发出 CDC 事件,Flink 做简单的字段映射和清洗,再写入目标存储。这套链路支撑了后续所有看板、报表和推荐场景。实测下来,分钟级延迟完全能满足业务需要,而且比强行追求秒级同步少了非常多运维负担。金融项目里,数据时效够用就好,过度追求实时就是给自己挖坑。
4. 实时交易链路上的性能与一致性问题
4.1 慢SQL与连接池的调整过程
交易链路压测时第一个暴露出来的问题是慢 SQL。排查日志发现,交易明细表的查询经常耗时超过 800ms,严重拖慢接口响应。Explain 一看,主查询条件里的trade_no字段虽然建了索引,但查询语句里对trade_no做了函数处理,导致索引直接失效,全表扫描了几百万行。
这个案例特别典型,很多人建索引时不考虑查询语句写法,结果到了线上才被发现。修复方式是改写 SQL,把函数处理改成参数预处理;同时把交易明细表按月分表,单表数据量控制在千万以内,查询响应立刻降到几十毫秒。另一个调整是连接池参数:Druid 连接池的初始连接数、最大连接数、最大等待时间需要按实际并发压测结果配置,不能照搬默认值。我们压测时发现最大连接数设置过大,反而导致数据库端连接风暴,后调整为 60 个连接并配合合理的等待队列,才稳定下来。
4.2 热点账户扣减:Redis+Lua方案与对账兜底
金融系统里最典型的并发场景是热点账户扣款:比如一个头部商户的结算账户,高峰期每秒有几百笔交易同时扣余额。如果每次都去数据库做select for update,数据库行锁会直接把吞吐拖死。我们的解法是:账户中心维护一个 Redis 缓存账户可用余额,交易前先在 Redis 里用 Lua 脚本做原子扣减,再异步落库记账。
Lua 脚本的核心逻辑大致是这样:
-- 检查账户状态 local accountKey = KEYS[1] local status = redis.call('HGET', accountKey, 'status') if status ~= 'normal' then return -1 end -- 检查冻结标记 if redis.call('HEXISTS', accountKey, 'frozen') == 1 then return -2 end -- 校验可用余额 local available = tonumber(redis.call('HGET', accountKey, 'available')) if available < tonumber(ARGV[1]) then return -3 end -- 原子扣减 redis.call('HINCRBY', accountKey, 'available', -ARGV[1]) return 0这个方案能把单账户的扣减 QPS 提升一个量级。但有一点必须想清楚:Redis 里的余额只是一个“预扣减视图”,真正的记账和最终一致性必须靠数据库事务。我们在后面接了 Kafka 消息,交易中心消费消息后落交易流水,账户中心消费消息后做最终账务变更。每天晚上跑对账任务,把 Redis 余额、交易流水、账户余额三方做比对,偏差数据自动触发冲正。这套机制上线至今,靠它对出了 3 次因消息重复消费导致的余额偏差,所以对账不是可选项,是必选项。
4.3 查询链路优化:读写分离与聚合查询
交易明细的查询场景和写入场景差别很大:写入是单条高频,查询是批量、条件复杂、偶尔还有大范围时间扫描。把高频写和重查询混在同一个库时,互相干扰特别明显。我们做了读写分离改造,写入走主库,查询走从库,核心交易列表页走只读节点的宽表。宽表在写入时通过消费消息异步构建,数据延迟控制在秒级,但查询响应提升了近 10 倍。
对账、风控分析这类更重型的查询,我们全部走 ClickHouse,不占用在线交易库资源。ClickHouse 在这类场景下的性能优势非常明显,几亿条明细的聚合分析可以做到秒级返回。不过 ClickHouse 不适合作为业务主库,它没有完整的事务能力和行级更新能力,定位就是分析引擎,架构上别把边界搞混。
5. 权限与敏感数据:金融服务项目最容易翻车的地方
5.1 两层权限模型:功能权限和行级权限
金融服务项目里,权限模型如果做不好,就不是“功能不可用”的问题,而是“数据泄露”的问题。我们最终落地的是两层权限:第一层是功能权限,控制的是“你能不能进某个菜单、调某个接口”,典型实现是 RBAC;第二层是行级数据权限,控制的是“你打开客户列表后能看到哪些客户的数据”,这层往往最容易被忽略。
行级权限的实现不能靠查出来后再程序过滤,那样性能太差。我们用的方案是:在 MyBatis 拦截器层做 SQL 改写,根据当前用户的机构范围和角色约束,自动拼接数据权限条件。比如一个客户经理登录后,系统会限制他只能查看branch_id属于自己且owner_id为自己的客户;分行运营人员可以看全行客户,但不能看超过自己层级的机构数据。测试时必须覆盖两个不同角色的用户同时访问同一条 API,验证返回结果是否出现越权数据。这个验证步骤我们写进了发布检查单,每次上线都要跑一遍。
5.2 敏感字段的加密与脱敏边界
客户姓名、手机号、证件号、银行卡号这些都算敏感数据,不能明文存储。我们统一封装了一个加密 SDK,业务服务只调用加密和解密接口,不直接感知底层加密算法切换。加密后的数据在数据库里是密文,但这就带出一个常见痛点:加密字段无法直接用 SQL 做模糊查询。
我们当时为了支持“手机号前几位查询”,引入了密文分词索引方案:把手机号按固定规则脱敏后生成一个可检索的索引列,查询条件也做同样的脱敏处理后走索引列匹配。这个方案查询速度和精度都能接受,代价是存储成本增加了一些。另外,日志打印必须走脱敏过滤器,所有 Json 序列化时对敏感字段做掩码输出,日志脱敏这一关不卡住,后面加密存储做得再好也会从日志侧泄露数据。
5.3 审计日志的完整链路
安全审计要求每个用户的关键操作都可以回溯到人、到时间、到请求链路。金融项目在这一块有硬性要求,功能上线前审计链路必须完整,否则上线审批根本过不去。我们的审计中心会在网关层统一记录每个请求的关键信息,包括调用方应用、调用用户、目标服务、操作类型、结果码等,再转发到 Kafka 由审计中心落库。特别敏感的写操作,审计明细里还会额外记录修改前后的关键字段变化。
这里有一个容易踩的坑:审计日志不能只记录业务层成功操作,那些因为权限不足被拦截的请求、触发风控规则被拒绝的交易同样要记录。这类“失败日志”对事后追溯的价值极高,很多时候排查安全事件就是靠这些被拒绝的请求找线索。审计日志的保存周期建议不低于两年,存储成本虽然高,但真到排查问题时你才会庆幸当初没省这一点钱。
6. 上线前后:可观测性与稳定性治理的实操记录
6.1 统一日志规范与TraceID
微服务架构下排查问题的第一痛点是“日志散落各处、难以串联”。我们上线前专门做了一次日志规范治理,强制所有服务的日志格式统一为 JSON 结构,包含时间戳、服务名、环境、日志级别、TraceID、业务流水号、用户标识、耗时等字段。TraceID 从网关入口生成,全链路透传,这样一次用户请求可以串联从网关到各中心服务的完整日志链路。
日志采集采用 Filebeat + Kafka + ELK,业务日志实时入 Elasticsearch。为了控制存储成本,我们设置了按日索引和冷热数据分层,超过 30 天的日志转入冷存储。真正提高排查效率的不是采集,而是“TraceID + 业务流水号”双索引,报障时只要客户端带上流水号,我们就能快速捞出整条链路的日志,不需要反复问用户“你什么时候操作的、操作了什么”。
6.2 告警阈值不是拍脑袋定的
稳定性治理不能等线上真的出故障了才去复盘,监控告警必须在上线前就配置完整。告警阈值怎么定,很多团队都是乱定的:CPU 使用率超过 80% 就告警、内存超过 85% 就告警,结果每天产生几百条无效告警,真正出问题时反而被淹没。我们这次对所有核心指标都基于压测数据做了基数评估。
| 指标 | 告警阈值 | 说明 |
|---|---|---|
| 核心交易接口 TP99 | > 500ms 持续 5 分钟 | 正常压测 TP99 在 120ms 左右 |
| 接口错误率 | > 0.5% 持续 3 分钟 | 排除网络抖动瞬时尖峰 |
| Kafka 消费积压 | 积压量 > 10 万条持续 10 分钟 | 正常积压在千条级别 |
| Redis 可用连接数 | 使用率 > 80% 持续 5 分钟 | 居高会导致扣减超时 |
| 数据库慢 SQL 数量 | 单节点 > 5 条/分钟 | 慢 SQL 是性能恶化的前兆 |
每个告警都必须配置正确的通知渠道和责任人,否则告警就是摆设。我们的分工是:基础资源告警走运维值班组,业务链路告警走研发 Oncall,重大告警直接电话升级。告警文案也会写明“影响范围”和“查看链接”,减少一线处理时的信息拉取成本。
6.3 上线灰度与回滚预案
金融服务项目上线不允许“一把梭”,我们的发布流程是标准灰度流程:先发布到灰度节点,引入少量真实业务流量,观察日志、错误率和核心指标;确认没问题后再分批发布到其他节点;最后才全量生效。这个流程看着慢,但实际上比出故障后紧急回滚要快得多。
灰度发布有点位要求:客户端流量入口的负载均衡策略要有权重调整能力,基础设施不支持的话,至少要在网关层加一份按机构或用户 ID 分流的配置。我们还要求每个微服务发布时提供回滚预案,核心服务必须验证“数据库变更是否向前兼容”,比如新增字段不允许在发布时一次性写入非空约束,否则回滚时老代码写不进新字段,直接导致全链路故障。这个经验是用一次严重的发布事故换来的,现在写进了团队的发布检查清单。
7. 复盘:重新做一次我会改掉的决策
7.1 过度设计的教训
复盘时大家公认的第一个问题是过度设计。项目初期,团队把“可扩展性”看得太重,引入大量抽象类和配置化的业务流程编排。结果是基础代码写了一大堆,真实业务功能却推进缓慢,业务方看到延期的系统自然不满。后来我们砍掉了一半的抽象层,用清晰的接口文档替代设计上的过度灵活,开发速度明显加快。工程的本质是在约束下做平衡,不是为了未来的可能性牺牲当下的确定性。
7.2 测试环境与生产环境的差异
测试环境的数据量只有生产环境的几十分之一,很多性能问题在预发环境根本无法暴露。比如上面的慢 SQL 问题,在测试环境执行只需要 20ms,根本感知不到,但在生产几百万数据下就是 800ms 慢查询。后来我们专门搭建了一套脱敏的准生产环境,定期从生产同步数据,所有核心链路的性能验证都在准生产环境做。数据脱敏工具用了开源方案,同步数据前自动遮蔽证件号、手机号、卡号等敏感字段,这个环境也是后续安全测试、故障演练的主要场地。
7.3 文档与知识的沉淀
项目中期人员变动,交接时发现大量决策都散落在会议纪要和口头沟通里,新接手的人完全不知道“当初为什么不选 xx 方案”。后来我们强制要求:每个模块的 README 必须包含“背景说明、关键决策、已知限制、常见坑”四部分,新模块没有 README 不允许提交代码评审。刚开始大家觉得麻烦,但项目后期反而因为文档齐全省至少两三成的沟通成本。
系统稳定运行至今,我最深的感受是:financial-services这类项目,难点从来不在某个技术亮点有多炫,而在那些繁琐的、容易被忽视的“基本面”工作——需求边界的确认、主数据的治理、幂等和重试机制、权限安全的审计链路、监控告警的准确度。把这些基本面做扎实了,系统自然稳定;一旦基本面偷懒,后续所有技术修补都是在还债。希望这份实战记录能帮你少踩几个我们踩过的坑。