金融系统最稳妥的缓存更新策略是先更新数据库再删缓存,通过事务性日志表补偿删除失败、逻辑过期防击穿、收敛数据入口控主从延迟、白名单替代布隆过滤器。

先更新数据库再删缓存是金融系统最稳妥的起点
金融场景对数据准确性零容忍,不能接受“缓存里存着旧余额”这种事。直接更新缓存(set)在并发写时极易出错——比如两个线程同时扣款,一个写入99,一个写入98,最终缓存定格在99,但DB已是98。所以必须放弃“更新缓存”,改用“删缓存+让下次读自动重建”的模式。
关键不是“能不能快”,而是“有没有兜底”。删缓存失败必须可感知、可重试,否则就等于没删。建议把 redisTemplate.delete(key) 包裹进事务性日志表,失败时由定时任务扫描补偿;不要依赖单次网络调用的成功返回。
- 删缓存操作必须和DB更新在同一个本地事务中提交(如Spring
@Transactional),但不强求Redis命令本身参与XA - 删除失败时,记录到
cache_delete_log表,含key、retry_count、next_retry_at - 避免在高并发写接口里同步等待Redis响应,超时设为
50ms,超时即记日志走异步补偿
击穿防护必须带逻辑过期,空对象不够用
金融系统里“查账户余额”这种接口,一旦某个账号被高频恶意刷(比如测试环境误配定时任务),SET key "" EX 60 这种空对象方案会把无效请求稳稳接住,但代价是:后续60秒内所有对该账号的真实查询都会命中空值,直接穿透到DB——这在资金类接口里是不可接受的。
真正有效的做法是用逻辑过期字段封装真实数据,例如存一个 RedisData<account></account> 对象,含 data 和 expireTime(毫秒时间戳)。读取时只判断逻辑时间,不依赖Redis本身的TTL;写入时原子更新整个结构体。
- 读流程:先
get(key)→ 解析出expireTime→ 若已过期,用setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS)争锁 → 拿到锁才查DB并写回新逻辑过期时间 - 锁key要带业务标识,如
"lock:account:" + accountId,防止不同账号互相阻塞 - 逻辑过期时间建议比物理TTL长3~5倍(如物理设2h,逻辑设10h),避免因Redis内存淘汰策略提前丢数据
延迟双删只是补丁,不是主干方案
延迟双删(第一次删缓存 → 更新DB → 延迟几百毫秒再删一次)常被当成“防主从延迟”的银弹,但在金融系统里它反而引入新风险:第二次删除可能失败、可能误删刚重建的正确缓存、还增加了调用链路复杂度。
真正该做的是收敛数据变更入口——所有资金变动必须走统一服务(比如 FundTransferService),并在该服务里完成DB更新 + 缓存清理 + 落库审计日志三件事。主从延迟问题应通过MySQL半同步复制 + sync_binlog=1 + innodb_flush_log_at_trx_commit=1 来压到毫秒级,而不是靠应用层反复删缓存来掩盖。
- 延迟时间不能拍脑袋定,需实测主库写入到从库
SHOW SLAVE STATUS中Seconds_Behind_Master的P99值 - 如果DB本身存在明显主从延迟(>200ms),优先优化DB配置或切主,别在缓存层打补丁
- 延迟双删的第二次删除必须带条件:仅当缓存当前值仍为旧版本时才执行(用
eval脚本比对value哈希)
布隆过滤器不适合账户ID这类精确主键查询
有人想用布隆过滤器防穿透,但账户ID是递增数字或UUID,分布高度离散,布隆过滤器的误判率在稀疏场景下会飙升。更糟的是,它无法区分“账号不存在”和“账号被冻结”,而这两者在金融系统里处理路径完全不同。
真要前置拦截,应该用轻量级白名单机制:把已开通服务的账号前缀(如 account:2026:)预热进一个 Set 结构,查询时先 sismember account_prefix_set "20261019"。这个操作O(1),无序列化开销,且能精准控制生效范围。
- 不要对全量账号建布隆,成本高、维护难、误判伤业务
- 白名单前缀粒度按业务周期切分(如按日/按月),避免单key过大
- 冷启动时用异步线程批量
scan账户表填充,不阻塞主流程
金融系统里,缓存不是性能加速器,而是数据一致性链条上的一环。最容易被忽略的点是:缓存操作日志必须和DB事务日志对齐,且能被下游审计系统消费——否则出了资损,连问题发生在哪一层都定位不了。











