在 Redis 中保证缓存与数据库的数据一致性是一个复杂的挑战,因为两者属于不同的系统(缓存是内存数据库,数据库是持久化存储),无法通过分布式事务直接实现强一致性。以下是常见的解决方案和最佳实践,根据业务场景选择合适策略:
缓存和数据库的同步可以通过以下几种方式:
1)先更新缓存,再更新数据库
2)先更新数据库存,再更新缓存
3)先删除缓存,再更新数据库,后续等查询把数据库的数据回种到缓存中
4)先更新数据库,再删除缓存,后续等查询把数据库的数据回种到缓存中
5)缓存双删策略。更新数据库之前,删除一次缓存;更新完数据库后,再进行一次延迟删除
6)使用 Binlog 异步更新缓存,监听数据库的 Binlog 变化,通过异步方式更新 Redis 缓存
以上就是实现数据库与缓存一致性的六种方式,这里前面三种都不太推荐使用,后面三种的话其主要根据实际场景:
如果是要考虑实时一致性的话,先写 MySQL,再删除 Redis 应该是较为优的方案,虽然短期内数据可能不一致,不过其能尽量保证数据的一致性。
如果考虑最终一致性的话,推荐的是使用 binlog + 消息队列的方式,这个方案其有重试和顺序消费,能够最大限度地保证缓存与数据库的最终一致性。
1. Cache-Aside(旁路缓存)模式
最常见的模式,核心思想是应用层主动管理缓存:
- 读流程:
- 先读缓存,命中则返回数据。
- 缓存未命中时,从数据库读取数据。
- 将数据写入缓存(设置合理的过期时间)。
- 写流程:
- 先更新数据库。
- 删除缓存(而非更新缓存,避免并发写导致脏数据)。
关键问题与优化:
- 并发读写导致脏数据:
- 场景:A 写数据库 → 删除缓存 → B 读缓存未命中 → B 读旧数据并回填缓存。
- 优化:使用延迟双删(写后 sleep 短暂时间再删一次缓存)或设置较短的缓存过期时间。
- 删除缓存失败:
- 引入重试机制(如通过消息队列异步重试),确保最终删除成功。
2. Write-Through/Read-Through(穿透读写)模式
缓存层代理数据库的读写操作:
- 写流程:
- 先更新缓存。
- 缓存层同步更新数据库。
- 读流程:
- 请求直接访问缓存,若未命中则由缓存层从数据库加载。
适用场景:
- 对一致性要求较高,且缓存支持此类逻辑(如某些缓存库或中间件)。
3. Write-Behind(异步回写)模式
- 写流程:
- 先更新缓存。
- 异步批量更新数据库(如合并多次写操作)。
- 优点:高性能,减少数据库压力。
- 缺点:存在数据丢失风险(缓存宕机时),仅适用于允许数据最终一致的场景。
4. 基于消息队列的最终一致性
- 流程:
- 更新数据库后,发送一条消息到 MQ(如 Kafka/RabbitMQ)。
- 消费者监听 MQ,异步删除或更新缓存。
- 优点:解耦数据库与缓存操作,支持重试。
- 注意:需处理消息重复消费(幂等设计)。
5. 订阅数据库变更日志(如 Binlog)
- 流程:
- 使用工具(如 Canal、Debezium)订阅数据库的 Binlog。
- 解析日志后,触发缓存删除/更新操作。
- 优点:无侵入性,保证缓存与数据库严格同步。
- 适用场景:对一致性要求高,且架构支持监听数据库日志。
6. 强一致性方案(慎用)
通过分布式锁或事务保证原子性,但性能代价高:
- 流程:
- 加锁(如 Redis 分布式锁)。
- 更新数据库 → 删除缓存 → 释放锁。
- 缺点:锁竞争可能成为瓶颈,仅适用于对一致性要求极高的场景。
策略选择建议
场景 | 推荐方案 | 一致性级别 |
---|---|---|
读多写少,允许短暂不一致 | Cache-Aside + 缓存过期时间 | 最终一致性 |
写频繁,允许异步 | Write-Behind + 消息队列 | 最终一致性 |
高一致性要求 | 订阅 Binlog 或强一致性方案 | 强/最终一致性 |
简单业务场景 | 延迟双删 + 重试机制 | 最终一致性 |
关键注意事项
- 优先删除缓存,而非更新缓存:避免并发写导致缓存与数据库不一致。
- 设置缓存过期时间:作为兜底策略,防止永久不一致。
- 降级策略:缓存故障时,应有直接读数据库的能力。
- 监控与告警:跟踪缓存命中率、延迟、不一致情况。
通过合理组合上述策略,可以在性能与一致性之间找到平衡。通常推荐以 Cache-Aside 为主,结合消息队列或 Binlog 监听实现最终一致性。