Redis 和 MySQL 的数据很难直接实现 强一致性,但可以通过一些策略尽量接近或实现 最终一致性。
为什么 Redis 和 MySQL 难以实现强一致性?
- 两者的数据更新机制不同
- Redis 的数据更新非常快,但可能异步刷盘或存在短暂的数据丢失风险。数据更新通常是异步传播,存在瞬时不一致,且默认不是事务性强一致的。
- MySQL 保证数据的事务性写入,保证数据的强一致性,但性能相对 Redis 较慢。
- 分布式 CAP 理论限制
- Redis 和 MySQL 在不同的系统中,相当于分布式场景。根据 CAP 理论:
- 如果优先保证高可用性和分区容错性,就难以完全保证一致性。
- Redis 通常更注重性能和高可用性。
- 数据的写入顺序问题
- 如果数据先写入 Redis 再写入 MySQL,或反之,可能因网络延迟、宕机等问题导致数据不一致。
🎯 几种常用策略来保证 MySQL 与 Redis 的数据一致性:
1. 先删除缓存,后更新数据库(推荐 ✅)
delete_cache(key)
update_db(data)
- 适合:最终一致性容忍的系统。
- 优点:更新成功后即使并发读也不会读到旧数据。
- 缺点:更新数据库失败,缓存已被删,可能导致缓存穿透。
2. 先更新数据库,后延迟删除缓存(推荐 ✅)
update_db(data)
sleep(10ms)
delete_cache(key)
- 避免并发写 + 并发读时缓存数据“脏读”的问题。
- 可结合消息队列异步延迟删除缓存。
- 比第一种方案稍安全,但需处理好延迟期间的并发问题。
🔍 延迟删除的目的
延迟的目的是为了应对一种并发场景下的数据不一致问题:
❗ 场景举例:
- 线程 A 正在从 Redis 读取旧数据;
- 线程 B 更新了数据库,然后立即删除缓存;
- 线程 A 在删除缓存之前拿到了旧数据,并将旧数据重新写入 Redis;
- 最终缓存中是旧值,数据被“脏写”了。
为了避免这种情况,我们给缓存删除操作加一点点延迟,让线程 A 的读取逻辑走完,避免把老数据回写进缓存。
🧠 延迟多久合适?
✅ 经验值:
- 10ms ~ 500ms 是常见的延迟区间。
- 如果你系统写入 QPS 不高,延迟 50ms 左右就足够了。
- 对于高并发系统,可以通过压力测试评估最小能保证一致性的延迟值。
⚙️ 动态控制方式
- 配置可调参数
把延迟时间做成配置项,可以按实际负载灵活调整,比如在application.yml
或config.json
里写:cache: delete-delay-ms: 50
- 基于负载自适应(进阶)
通过观察系统中的并发程度、缓存命中率,动态调整延时(如高并发时延迟增加,低并发时减少),这就需要接入你的监控系统。
“为什么高并发时延迟删除缓存的延迟时间要增加,而低并发时反而可以减少?”
这其实和并发冲突风险有直接关系,
🔺 为什么高并发时延迟需要“加长”?
因为:
- 高并发下,线程 A 和线程 B 的“撞车”概率更大;
- 请求越密集,发生“读到旧数据 + 写回缓存”的机会越多;
- 所以需要加长延迟时间,让“危险窗口”彻底过去,再删缓存,确保脏数据不再写回。
✅ 延迟时间越长,线程 A 写缓存的机会就越小。
🔻 为什么低并发时可以“缩短”延迟?
因为:
- 并发少,线程 A/B 几乎不会“撞车”;
- 没必要延迟太久浪费资源(比如 sleep、异步延迟处理);
- 小系统可以设 10ms 左右即可满足一致性保障。
🧪 举个数据级别的例子
并发强度 | 请求间隔 | 推荐延迟时间 |
---|---|---|
高并发 | < 10ms | 100 ~ 300ms |
中等并发 | 10~50ms | 30 ~ 100ms |
低并发 | > 100ms | 10 ~ 30ms |
这些值不是硬性规定,实际要根据系统测试和 QPS 来微调。
✅ 总结一句话:
高并发时延迟增加是为了“降低读写并发冲突带来的脏写概率”;低并发下这种冲突少,延迟可以更短,提升效率。
🔄 更优解:消息队列异步删除缓存(推荐)
相比手动 sleep 延时,更推荐用 消息队列 异步处理删除逻辑,原理:
- 更新数据库成功;
- 向 MQ 发送一条“删除缓存”的消息;
- 消费端延迟 N 毫秒处理该消息,删除缓存。
比如:
x-delay-message: 延迟100ms后消费,执行删除缓存操作
- RabbitMQ / Kafka / RocketMQ 都支持延迟消费。
- 优点:不阻塞主流程,可靠性高,便于失败重试。
✅ 总结
控制方式 | 说明 |
---|---|
固定延迟 | 通常设置在 10ms - 500ms,足以防止并发脏写 |
配置化延迟 | 可从配置文件中动态调整,方便调优 |
自适应延迟 | 根据系统负载动态调整延时(高级方案) |
消息队列方式 | 最推荐,异步、解耦、可重试、可监控 |
3. 使用消息队列异步更新缓存
- 步骤:
- 更新数据库;
- 写一条消息队列;
- 消费者异步更新或删除 Redis。
- 优点:解耦,支持异步处理。
- 缺点:消息丢失或延迟仍可能导致缓存与数据库不一致。
4. 双写机制(不推荐 ❌)
update_cache(data)
update_db(data)
- 缺点:
- 更新缓存成功但数据库失败会导致缓存是“脏数据”;
- 高并发下不一致概率大;
- 缓存回写风险高。
🔄 最理想方案(综合推荐):
- 所有写操作只更新数据库,不更新缓存;
- 数据更新后,主动删除缓存;
- 下一次读取数据时,再从数据库查并回填缓存(缓存懒加载);
- 辅助使用 布隆过滤器 + 限流 + 锁机制 解决缓存穿透/击穿问题。
🧠 补充建议:
技术手段 | 用途 |
---|---|
缓存预热 | 系统启动时预加载热门数据 |
缓存雪崩防护 | 缓存加随机过期时间,避免同时失效 |
缓存穿透防护 | 布隆过滤器 or 空值缓存 |
缓存击穿防护 | 分布式锁 or 单飞保护机制 |
异步更新缓存 | 消息队列、定时任务、数据订阅等方式 |
✅ 总结一句话:
Redis 和 MySQL 的数据一致性不是 100% 靠某个单点保证的,而是通过一整套机制来控制不一致的时间窗口、发生概率,并做好异常补偿机制。