1. 为什么数据库与缓存一致性是个难题
第一次在线上环境遇到缓存与数据库不一致的问题,是在一个促销活动的高峰期。当时商品库存显示还有1000+,但实际下单时却提示库存不足。排查后发现Redis里的库存数据比数据库慢了整整30秒,这30秒里已经有500多笔订单创建。这种数据不一致问题在分布式系统中几乎无法完全避免,但我们可以通过合理的架构设计将其影响降到最低。
数据库与缓存的不一致主要源于三种场景:
- 更新数据库成功,但更新缓存失败
- 缓存自动过期后,数据库有更新但未及时回填
- 并发写操作导致执行顺序错乱
关键认知:缓存不是数据库的镜像,而是性能与一致性的权衡产物。我们需要根据业务场景选择合适的一致性级别。
2. 主流解决方案的技术实现
2.1 Cache Aside Pattern(旁路缓存)
这是最常用的模式,核心逻辑:
// 读流程 1. 先查缓存,命中则返回 2. 未命中时查数据库 3. 将结果写入缓存(设置合理TTL) // 写流程 1. 更新数据库 2. 删除缓存(非更新!)为什么是删除而不是更新缓存?
- 避免两个并发的写操作导致缓存脏数据
- 减少计算开销(可能涉及复杂聚合)
实测案例:某电商平台商品详情页采用该模式后,缓存不一致时间从分钟级降至秒级。
2.2 Read/Write Through(读写穿透)
由缓存组件统一管理数据流动:
- 读时自动回填缓存
- 写时同步更新缓存和数据库
适合场景:
- 缓存组件支持事务(如Redis 6.0+)
- 数据模型较简单
2.3 Write Behind Caching(异步回写)
高风险但高性能的方案:
- 先更新内存缓存
- 异步批量持久化到数据库
使用禁忌:
- 金融交易类业务绝对禁用
- 必须配合WAL日志防丢失
3. 进阶场景的解决方案
3.1 双写一致性保障
对于核心业务数据,可以采用:
def update_data(key, value): # 先写数据库 db.update(key, value) # 再删缓存(带重试) retry(3, lambda: cache.delete(key)) # 最终一致性检查 async_check_consistency(key)3.2 分布式锁方案
高并发下的优化写法:
RLock lock = redisson.getLock("product_" + sku); try { lock.lock(); // 执行更新操作 updateDBAndCache(); } finally { lock.unlock(); }3.3 延迟双删策略
针对极端并发场景:
- 先删除缓存
- 更新数据库
- 休眠500ms(取决于业务耗时)
- 再次删除缓存
4. 实战避坑指南
4.1 缓存雪崩预防
错误做法:
- 所有key设置相同TTL
正确方案:
- 基础TTL + 随机抖动(如30min±5min)
- 永不过期+后台更新
4.2 热点Key处理
我们曾遇到单商品QPS 10w+的案例:
- 本地缓存+Redis多级缓存
- 使用bloom filter过滤无效请求
4.3 监控指标建设
必须监控的核心指标:
| 指标项 | 报警阈值 |
|---|---|
| 缓存命中率 | <90% |
| 数据库查询突增 | 同比上涨300% |
| 不一致持续时间 | >5s |
5. 不同业务场景的选型建议
5.1 强一致性要求(如支付系统)
- 分布式事务(TCC/SAGA)
- 同步写+串行化读
- 牺牲部分性能
5.2 最终一致性(如商品信息)
- 消息队列补偿
- 版本号比对
- 定时对账任务
5.3 弱一致性(如UV统计)
- 直接内存计算
- 定期全量刷新
- 允许短期误差
在最近一次大促中,我们通过组合使用延迟双删+本地缓存降级,将核心业务的不一致时间窗口控制在200ms内。记住:没有银弹方案,只有适合业务现状的权衡选择。