1. 金融数据服务从零搭建的核心思路拆解
1.1 为什么我要自己搭一套金融数据服务
先说清楚这套东西是干什么的。financial-services这个名字听起来很泛,但在实际工程语境里,它通常指的是一套面向金融业务场景的数据服务层——把行情、账户、交易、风控、对账这些模块的数据统一收口,对外提供稳定的接口,对内屏蔽底层数据源的差异。你可以把它理解成一个“金融数据中转站”:上游对接各种数据源(数据库、第三方行情接口、内部核心系统),下游对接前端页面、移动端、策略引擎、报表系统。
我之所以动手做这件事,起因很朴素:业务方要一个“查账户余额和最近流水”的接口,结果发现后端有四个团队各写了一套,字段命名不一样,时间格式不一样,连“余额”到底含不含冻结金额都没统一。每次加一个查询维度就要改一遍所有调用方。这种重复劳动和口径混乱,是压垮我的最后一根稻草。所以我决定抽一层统一的服务出来,把数据口径、鉴权、限流、缓存、审计全部收敛到一处。
这套服务适合谁参考?如果你正在做金融类后台、交易系统、账务系统,或者你所在团队正被“接口口径不统一”折磨,那这篇内容应该能帮到你。哪怕你只是想做一个小型的个人记账或行情看板,里面的分层思路和缓存策略同样适用。我不打算讲空泛的架构理论,而是把我在实际搭建过程中做的每一个取舍、踩的每一个坑,原原本本摊开来讲。
1.2 整体分层设计与选型考量
我把整套服务分成四层,从下往上依次是:数据接入层、领域服务层、聚合编排层、接口网关层。这个分层不是拍脑袋定的,而是根据“变更频率”来切的——越底层的越稳定,越上层的越容易变。
数据接入层负责和外部数据源打交道,包括关系型数据库、缓存、消息队列、第三方行情推送。这一层的核心目标是“隔离变化”:今天用 MySQL,明天可能换成分库分表;今天行情走 HTTP 轮询,明天可能改成 WebSocket 推送。只要接入层的接口契约不变,上层就无感。
领域服务层是真正的业务逻辑所在,比如账户服务、交易服务、风控服务。每个服务只管自己领域内的事,账户服务不关心交易怎么撮合,交易服务也不直接读账户表,而是通过账户服务暴露的接口拿数据。这样做的好处是职责清晰,坏处是调用链变长,所以后面要靠聚合层来收敛。
聚合编排层解决的就是“一次请求要跨多个领域服务”的问题。比如前端要一个“用户资产总览”,需要账户余额、持仓市值、今日盈亏三块数据,聚合层就并行调用三个领域服务,拼装后返回。这一层我特意做得很薄,只做编排和裁剪,不写业务规则,避免它变成新的“上帝服务”。
接口网关层负责鉴权、限流、参数校验、日志埋点、协议转换。对外只暴露 RESTful 接口,内部服务之间走 RPC。为什么对外用 REST 而不是直接暴露 RPC?因为调用方五花八门,有前端、有第三方、有脚本,REST 的通用性最好,调试也方便,用 curl 就能测。
提示:分层的关键不是层数多,而是每层只做一件事。我见过太多项目把鉴权写在业务逻辑里,结果换个入口就要复制一遍鉴权代码,这是典型的层次混乱。
选型上,我坚持“成熟优先”原则。数据库用 PostgreSQL,因为金融场景对事务和数值精度要求高,numeric类型处理金额比浮点数靠谱得多。缓存用 Redis,主要扛热点账户的读请求。消息队列用 Kafka,处理交易流水的异步落库和对账。这些组件都是经过大规模验证的,出了问题资料也好找。我没有选那些花哨的新框架,因为金融系统最怕的就是“新”,稳定压倒一切。
2. 核心模块的细节解析与实操要点
2.1 账户服务:金额精度与并发扣减是命门
账户服务是整个系统的地基,也是最容易出事故的地方。我先说金额精度这个老生常谈但总有人栽跟头的问题。绝对不要用 float 或 double 存金额,这是铁律。0.1 + 0.2 在浮点数里不等于 0.3,这个误差在金融场景里就是事故。我用的是 PostgreSQL 的numeric(20, 4),Java 侧对应BigDecimal,序列化时统一转成字符串传输,前端拿到字符串再自己格式化。这样从存储到传输到展示,全程不丢精度。
并发扣减是另一个命门。假设两个请求同时扣同一账户的余额,如果不加控制,就会出现“超扣”。我试过三种方案,最后选了乐观锁加重试。具体做法是账户表加一个version字段,更新时带上版本号:
UPDATE account SET balance = balance - :amount, version = version + 1 WHERE id = :id AND version = :version AND balance >= :amount;如果影响行数为 0,说明要么版本被改了,要么余额不足,这时候区分处理:查一下当前余额,够就重试,不够就返回失败。重试次数我设成 3 次,超过就报错让上层处理。为什么不用悲观锁SELECT ... FOR UPDATE?因为热点账户并发高的时候,悲观锁会让大量请求排队,响应时间飙升,而乐观锁在冲突不严重时性能好得多。实测下来,在日均百万级交易量下,乐观锁重试率不到 0.5%,完全可接受。
注意:
balance >= :amount这个条件必须和版本判断放在同一条 SQL 里,不能先查再判断再更新,否则中间有窗口期,等于没锁住。
还有一个细节是冻结金额的处理。很多业务场景(比如下单未支付、提现审核中)需要先把钱冻住,而不是直接扣掉。我的设计是账户表里同时有balance(可用余额)和frozen(冻结金额)两个字段,冻结时balance减、frozen加,解冻时反向操作,真正扣款时frozen减。这样任何时刻balance + frozen就是总资产,对账时一目了然。
2.2 交易流水:幂等设计与对账机制
交易流水模块的核心诉求是“不重不漏”。不重,靠的是幂等;不漏,靠的是可靠投递和对账。
幂等的实现我用了业务唯一键 + 唯一索引的组合。每笔交易在发起时生成一个全局唯一的request_id(我用的是雪花算法生成的 ID),在流水表上给request_id建唯一索引。这样即使同一个请求因为网络重试发了两次,第二次插入时会因为唯一索引冲突而失败,捕获这个异常后直接返回第一次的结果即可。这比“先查再插”可靠得多,因为“先查再插”在并发下有竞态。
CREATE TABLE transaction_flow ( id BIGSERIAL PRIMARY KEY, request_id VARCHAR(64) NOT NULL, account_id BIGINT NOT NULL, amount NUMERIC(20,4) NOT NULL, direction SMALLINT NOT NULL, -- 1 收入, -1 支出 status SMALLINT NOT NULL, -- 0 处理中, 1 成功, 2 失败 created_at TIMESTAMP DEFAULT NOW(), UNIQUE (request_id) );对账机制我分成两块:实时对账和日终对账。实时对账是在每笔交易完成后,异步发一条消息到 Kafka,对账服务消费后核对账户余额变动和流水是否一致,不一致就告警。日终对账是每天凌晨跑批,把当天的所有流水按账户汇总,和账户表的余额变动做比对,生成对账报告。这里有个经验:对账一定要用“快照 + 增量”的方式,不要每次都全量扫表,否则数据量一大就跑不动。我的做法是每天零点给所有账户存一份余额快照,日终对账时只需要“快照余额 + 当日流水汇总 = 当前余额”即可验证。
提示:对账发现的差异不要自动修,一定要人工介入。我见过自动修数据把问题掩盖掉的案例,最后查账查了三天。差异本身就是信息,先搞清楚为什么差异,再决定怎么处理。
2.3 风控服务:规则引擎与实时拦截
风控服务听起来高大上,但落到工程上,核心就是“在交易发生前,根据一堆规则判断这笔交易要不要拦”。我一开始想自己写 if-else,后来发现规则会频繁变,每改一次就要发版,于是引入了一个轻量级规则引擎。
规则我用 JSON 来描述,比如“单笔超过 5 万且收款方是新账户就拦截”:
{ "ruleId": "R001", "condition": { "and": [ {"field": "amount", "op": ">", "value": 50000}, {"field": "payee.isNew", "op": "==", "value": true} ] }, "action": "REJECT", "reason": "大额新账户交易" }规则引擎解析这个 JSON,对交易上下文求值,命中就执行对应动作(拦截、告警、人工审核)。这样做的好处是运营人员可以自己在后台配规则,不用开发介入。当然,规则引擎的性能要控制好,我在里面加了缓存,规则编译一次后缓存起来,避免每次请求都解析 JSON。
实时拦截的响应时间要求很高,我给自己定的指标是 P99 在 50ms 以内。为了达标,做了两件事:一是规则求值全部在内存里做,不查库;二是把需要查库的数据(比如账户历史交易次数)提前异步加载到本地缓存,定时刷新。这样风控判断基本是纯内存计算,速度很快。
2.4 接口网关:鉴权、限流与审计一个都不能少
网关层是流量的入口,也是安全的第一道防线。鉴权我用的是 JWT,但做了两点改造:一是 token 里只放用户 ID 和角色,不放敏感信息;二是 token 有效期设得短(15 分钟),配合 refresh token 续期。为什么不用 session?因为服务要水平扩展,session 共享麻烦,JWT 无状态更适合。
限流我用了令牌桶算法,按用户维度和接口维度双重限流。用户维度防止单个用户刷接口,接口维度防止某个接口被整体打爆。令牌桶的参数怎么定?我的经验是先压测出单机 QPS 上限,然后按集群规模折算,留 30% 余量。比如单机扛 1000 QPS,5 台机器就是 5000,限流阈值设 3500 比较稳妥。
审计日志是金融系统的刚需,监管和排查都靠它。我记录的内容包括:请求 ID、用户 ID、接口路径、请求参数(脱敏后)、响应状态、耗时、客户端 IP。这里有个坑:请求参数里的敏感字段(密码、身份证、卡号)必须脱敏,否则日志本身就是泄露源。我写了一个脱敏工具,对指定字段做掩码处理,比如卡号只留后四位。
public String maskCardNo(String cardNo) { if (cardNo == null || cardNo.length() < 8) return "****"; return cardNo.substring(0, 4) + "****" + cardNo.substring(cardNo.length() - 4); }审计日志我存到独立的日志库,和业务库分开,避免日志写入影响业务性能。查询审计日志走专门的只读接口,权限控制到人。
3. 完整实操流程与关键环节实现
3.1 环境准备与依赖安装
我先把环境要求列清楚,避免你踩版本坑。操作系统我用的是 Ubuntu 22.04 LTS,内核 5.15。数据库 PostgreSQL 15,Redis 7.0,Kafka 3.5,JDK 17。为什么选这些版本?因为都是长期支持版,社区资料多,且相互兼容性经过验证。JDK 17 的 ZGC 对低延迟场景友好,金融接口对 GC 停顿敏感,这点很重要。
安装 PostgreSQL 我用的是 apt 源,但要注意默认源里的版本可能偏旧,需要加官方源:
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - sudo apt update sudo apt install postgresql-15装完后要调几个参数。shared_buffers设成内存的 25%,work_mem设成 64MB,max_connections设成 500。这些参数直接影响数据库性能,默认值偏保守。改完重启生效。
Redis 的安装相对简单,但配置上我关掉了 RDB 持久化,只开 AOF,因为金融场景更看重数据不丢,AOF 的everysec策略在性能和可靠性之间平衡得比较好。Kafka 我用了 KRaft 模式,不再依赖 ZooKeeper,部署更简单。
3.2 数据库表结构设计与索引优化
表结构设计我遵循几个原则:金额用 numeric、时间用 timestamp with time zone、状态用 smallint、主键用 bigserial。下面是我核心的几张表。
账户表:
CREATE TABLE account ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, account_no VARCHAR(32) NOT NULL, balance NUMERIC(20,4) NOT NULL DEFAULT 0, frozen NUMERIC(20,4) NOT NULL DEFAULT 0, currency VARCHAR(8) NOT NULL DEFAULT 'CNY', version BIGINT NOT NULL DEFAULT 0, status SMALLINT NOT NULL DEFAULT 1, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE (account_no) ); CREATE INDEX idx_account_user ON account(user_id);流水表除了前面提到的唯一索引,我还按account_id + created_at建了联合索引,因为查询流水基本都是“某账户某时间段”。这个索引的顺序很重要,account_id在前是因为区分度高,先过滤掉大部分数据,再按时间范围扫。
注意:索引不是越多越好。每加一个索引,写入就慢一分。我见过一张表建了十几个索引,写入性能惨不忍睹。原则是只为高频查询建索引,低频查询走全表扫或者离线跑。
3.3 核心接口的开发与联调
我以“查询账户资产总览”这个接口为例,走一遍完整流程。接口定义是GET /api/v1/asset/overview,需要登录态。
第一步,网关层校验 JWT,解析出user_id,然后限流检查。第二步,聚合层收到请求,并行调用账户服务(拿余额和冻结)、持仓服务(拿持仓市值)、盈亏服务(拿今日盈亏)。这里用CompletableFuture做并行调用:
CompletableFuture<Balance> balanceFuture = CompletableFuture.supplyAsync(() -> accountService.getBalance(userId), executor); CompletableFuture<BigDecimal> positionFuture = CompletableFuture.supplyAsync(() -> positionService.getMarketValue(userId), executor); CompletableFuture<BigDecimal> pnlFuture = CompletableFuture.supplyAsync(() -> pnlService.getTodayPnl(userId), executor); CompletableFuture.allOf(balanceFuture, positionFuture, pnlFuture).join();第三步,拼装结果返回。这里有个细节:并行调用要设超时,不能无限等。我给每个 future 设了 200ms 超时,超时就返回降级数据(比如持仓市值返回 null,前端显示“加载中”),而不是整个接口失败。金融接口的可用性比完整性更重要,部分数据缺失好过整个页面打不开。
联调阶段我用 Postman 和 curl 交叉验证,重点测边界情况:余额为 0、账户不存在、token 过期、并发请求。并发测试我用 JMeter,模拟 100 个用户同时查资产,观察响应时间和错误率。实测下来 P99 在 80ms 左右,满足预期。
3.4 缓存策略与热点账户处理
缓存是提升读性能的关键,但金融场景的缓存要格外小心,因为缓存和数据库不一致会导致用户看到错误的余额。我的策略是:缓存只用于“读多写少”且“允许短暂不一致”的场景,比如行情数据、用户基本信息。账户余额这种强一致要求的,缓存只做“防穿透”,不做“防不一致”。
具体做法是:查余额时先查 Redis,命中就返回,但每次写操作(扣款、入账)都会主动删除对应的缓存 key,下次读时回源数据库并重新写入缓存。这样最坏情况是读到一次旧数据,但很快会被修正。为了进一步降低风险,我给余额缓存设了很短的过期时间(5 秒),即使删除失败,5 秒后也会自动失效。
热点账户是另一个难题。比如某个大商户的账户,每秒被查几千次。我的处理是给这类账户单独开一个本地缓存(Caffeine),存在应用进程内存里,过期时间 1 秒。这样大部分请求根本不走 Redis,直接内存返回。1 秒的过期时间意味着最多 1 秒的延迟,对查询场景可以接受。
提示:本地缓存要注意多实例一致性问题。我的做法是写操作时通过消息队列广播一个“失效消息”,所有实例收到后清掉本地缓存。虽然有一点点延迟,但比不一致好。
4. 常见问题与排查技巧实录
4.1 金额计算误差与精度丢失排查
问题现象:用户反馈账户余额少了 0.01 元,查流水发现每笔都对,但汇总就是差一分钱。
排查思路:先确认存储层有没有用浮点数。我查了表结构,是numeric,没问题。然后查应用层,发现有个地方在计算手续费时用了double,算完再转BigDecimal。问题就出在这——double计算过程中已经产生了误差,转BigDecimal只是把误差带过来了。
解决方法:把所有涉及金额的计算全部改成BigDecimal,并且指定精度和舍入模式。比如手续费计算:
BigDecimal fee = amount.multiply(new BigDecimal("0.001")) .setScale(2, RoundingMode.HALF_UP);setScale的第二个参数一定要显式指定,不要用默认的,默认行为在不同场景下可能不符合预期。金融场景一般用HALF_UP(四舍五入),但具体要看业务规则,有些场景要求DOWN(直接截断)。
避坑技巧:在代码里加一个静态检查,禁止double/float和金额字段出现在同一行。我用的是 Checkstyle 自定义规则,CI 阶段就会拦下来。
4.2 并发扣款导致余额超扣的定位过程
问题现象:压测时发现某个账户余额变成了负数,但业务上不允许透支。
排查思路:先看日志,发现有两笔扣款请求几乎同时到达,都读到了相同的version,然后都执行了更新。按理说乐观锁应该拦住一个,为什么没拦住?查 SQL 发现,更新语句写成了:
UPDATE account SET balance = balance - :amount, version = version + 1 WHERE id = :id AND version = :version;少了balance >= :amount这个条件!所以即使余额不够,只要版本对得上,照样扣成功,扣成负数。
解决方法:把余额判断加回 SQL,并且确保它在同一条语句里。改完后重新压测,负数问题消失。
避坑技巧:所有涉及“扣减”的操作,都要在 SQL 层面加“余额充足”判断,不能只靠应用层先查后扣。应用层的检查只是优化,数据库层的约束才是底线。另外,给balance字段加一个CHECK (balance >= 0)约束,作为最后一道防线,即使代码写错了,数据库也会拒绝。
4.3 接口超时与慢查询的优化实录
问题现象:资产总览接口偶尔超时,P99 从 80ms 飙到 2s。
排查思路:先看监控,发现超时集中在整点附近。查慢查询日志,发现持仓服务的查询在整点特别慢。原来持仓服务的数据是整点批量更新的,更新时锁表,导致查询被阻塞。
解决方法:把批量更新改成“影子表 + 原子切换”。先更新一张影子表,更新完用RENAME TABLE原子切换,切换瞬间完成,查询几乎无感。另外给持仓查询加了缓存,整点前后缓存过期时间延长到 30 秒,避开更新窗口。
避坑技巧:金融系统的批量任务要避开业务高峰,能凌晨跑就别白天跑。如果必须白天跑,一定要用“影子表”或“分区切换”这种对读无影响的方式,不要直接UPDATE大表。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 余额差几分钱 | 浮点数计算 | 检查代码中是否有 double/float | 全部改用 BigDecimal 并指定舍入 |
| 余额被扣成负数 | 缺少余额判断 | 检查更新 SQL 是否有 balance >= amount | SQL 加条件 + 数据库加 CHECK 约束 |
| 接口整点变慢 | 批量任务锁表 | 查慢查询日志时间分布 | 影子表切换或错峰执行 |
| 缓存与数据库不一致 | 写后未删缓存 | 对比缓存和数据库值 | 写操作主动删缓存 + 短过期时间 |
| 幂等失效重复扣款 | 唯一键未生效 | 检查唯一索引是否存在 | 补唯一索引 + 捕获冲突异常 |
| 风控规则不生效 | 规则未刷新 | 检查规则缓存刷新时间 | 加规则变更通知机制 |
4.5 我踩过的三个印象最深的坑
第一个坑是时区问题。服务器用 UTC,数据库存timestamptz,但前端展示时没转时区,导致用户看到的交易时间差了 8 小时。排查了半天才反应过来。后来统一规定:存储用 UTC,传输用 ISO 8601 带时区,展示层负责转本地时区。这个规范写进了团队文档,再没出过问题。
第二个坑是连接池配置不当。一开始用默认配置,最大连接数 10,结果并发一高就排队。后来调到 50,又发现数据库max_connections不够,因为每个服务实例都开 50 个连接,5 个实例就是 250,超过了数据库上限。最后用 PgBouncer 做连接池代理,应用侧连接数降下来,数据库侧压力也小了。
第三个坑是日志打印过多。为了排查问题,我在循环里打了日志,结果一个批量任务打了上百万行日志,磁盘直接写满,服务挂了。后来规定:循环内禁止打日志,只打汇总;日志级别生产环境默认 INFO,DEBUG 只在排查时临时开。
5. 性能压测与容量规划经验
5.1 压测方案设计与指标解读
压测不是随便跑跑,要有明确的指标。我关注的指标有四个:QPS、P99 延迟、错误率、资源利用率。QPS 反映吞吐,P99 反映长尾体验,错误率反映稳定性,资源利用率反映瓶颈在哪。
压测工具我用 JMeter 做 HTTP 层压测,用 wrk 做极限 QPS 测试。压测数据要贴近真实,不能只用几个固定用户,否则缓存命中率虚高。我用脚本生成了 10 万个用户,随机分布请求,这样缓存命中率接近真实场景。
压测分三轮:第一轮找瓶颈,逐步加压直到错误率超过 1%,记录此时的 QPS 和资源占用;第二轮验证稳定性,在 80% 瓶颈压力下持续跑 1 小时,看有没有内存泄漏或延迟漂移;第三轮做故障注入,手动杀掉一个实例,看剩余实例能否接管,验证高可用。
实测数据:单实例(4 核 8G)能扛 1200 QPS,P99 在 60ms,错误率 0。5 个实例集群能扛 5000 QPS,P99 在 90ms。这个容量支撑日均千万级请求没问题。
5.2 容量规划与扩容策略
容量规划的核心是“留余量”。我的经验是按峰值流量的 1.5 倍规划容量。比如预计峰值 3000 QPS,就按 4500 QPS 准备机器。为什么是 1.5 倍?因为金融场景流量波动大,遇到活动或行情波动,流量可能瞬间翻倍,留 50% 余量能扛住突发。
扩容策略我分两种:垂直扩容和水平扩容。垂直扩容是加 CPU 内存,适合数据库这种不好水平拆的组件。水平扩容是加实例,适合无状态的应用服务。我的应用服务全部无状态,扩容就是加机器,配合负载均衡自动分流。数据库做了读写分离,写走主库,读走从库,从库可以按需加。
提示:扩容不是万能的,先优化再扩容。我见过一个接口慢,加机器没用,最后发现是 SQL 没走索引。加索引后性能提升 100 倍,机器都不用加了。所以扩容前先做性能分析,确认瓶颈在哪。
5.3 监控告警体系搭建
监控是系统的眼睛,没有监控就是盲人摸象。我搭了三层监控:基础设施层、应用层、业务层。
基础设施层监控 CPU、内存、磁盘、网络,用 Prometheus + Node Exporter。应用层监控 JVM、连接池、缓存命中率、接口 QPS 和延迟,用 Micrometer 埋点。业务层监控交易量、成功率、对账差异数,这些是金融系统特有的指标。
告警规则我设了分级:P0 是“服务不可用”或“对账差异”,电话告警;P1 是“错误率超 1%”或“P99 超 500ms”,短信告警;P2 是“磁盘超 80%”或“缓存命中率下降”,邮件告警。告警要克制,不能什么都告,否则会“告警疲劳”,真出事了反而没人看。
我踩过的一个坑是告警阈值设得太敏感。有次网络抖动导致 P99 短暂升高,告警响了一晚上,大家都没睡好,结果啥事没有。后来把告警改成“持续 5 分钟超过阈值才触发”,过滤掉了瞬时抖动。
6. 安全加固与合规实践
6.1 数据加密与脱敏处理
金融数据的安全是底线。我做了三层加密:传输加密、存储加密、字段加密。
传输层全站 HTTPS,TLS 1.3,禁用弱加密套件。存储层用数据库自带的透明加密(TDE),磁盘被偷了也读不出数据。字段层对敏感字段(身份证、卡号、手机号)单独加密,用 AES-256,密钥存在独立的密钥管理服务里,不和数据库放一起。
脱敏主要用在日志和测试环境。日志里的敏感字段全部掩码,测试环境用生成的数据,不用生产数据。我写了一个数据生成器,能生成格式正确但内容虚假的身份证、卡号,测试用足够了。
注意:加密密钥的轮换要有计划。我设的是 90 天轮换一次,轮换时旧密钥保留一段时间用于解密历史数据,新数据用新密钥。轮换过程要自动化,手动轮换迟早会忘。
6.2 权限控制与操作审计
权限控制我用了 RBAC 模型:用户关联角色,角色关联权限。权限粒度到接口级别,比如“查询余额”和“转账”是两个独立权限。这样做的好处是灵活,加一个新角色只需要配权限,不用改代码。
操作审计记录所有“写”操作,包括谁、什么时候、做了什么、结果如何。审计日志不可篡改,我用了只追加的日志表,禁止 UPDATE 和 DELETE。查询审计日志需要单独的权限,且查询行为本身也被记录,防止有人查审计日志掩盖痕迹。
6.3 合规要点与数据留存策略
金融数据有留存要求,交易流水一般要保存 5 年以上。我的策略是“热数据 + 冷数据”分离:最近 3 个月的流水放主库,查询快;3 个月以上的归档到冷库,用列式存储压缩,查询慢但便宜。归档过程自动化,每天凌晨跑批。
数据删除要谨慎。用户注销账户时,不能直接物理删除,而是标记为“已注销”,数据保留但不可访问。这样既满足合规要求,又避免了误删风险。
7. 后续扩展方向与个人体会
这套服务上线跑了半年多,支撑了日均千万级请求,没出过 P0 事故。回头看,我觉得最值得分享的经验是:金融系统的核心不是功能多,而是不出错。每一个设计决策都要问自己“如果这里出错了,后果是什么”。金额算错、余额超扣、数据泄露,这些都是不可接受的。所以宁可牺牲一点性能,也要保证正确性;宁可多写一层校验,也不要心存侥幸。
后续我打算在这几个方向继续优化:一是引入分布式事务框架,解决跨服务的一致性;二是把风控规则引擎升级成支持复杂事件处理(CEP),能识别更隐蔽的欺诈模式;三是做多活容灾,现在还是主备,切换有分钟级中断,多活能做到秒级。
最后分享一个小技巧:给每个接口写一个“故障剧本”。比如“如果账户服务挂了,资产总览接口降级返回缓存数据”,把这个剧本写成代码里的降级逻辑,并且定期演练。真出事的时候,有剧本和没剧本,恢复速度差十倍。这个习惯是我从一次线上事故里学来的,那次因为没有降级方案,一个小故障拖成了大事故。从那以后,我做的每个服务都必须有降级预案,这已经成了我的肌肉记忆。