缓存一致性

数据不一致的发现

Redis 方案上线一周后,用户开始反馈问题。

【用户反馈】用户ID: 5678
问题:点赞后又取消,但点赞数没有变化
时间:2023-08-22 14:30:15

【用户反馈】用户ID: 1234
问题:刷新页面后,点赞数和之前不一样
时间:2023-08-22 14:35:22

我赶紧查看数据:

数据不一致!Redis 显示 152,实际只有 150。

问题分析

我分析了代码,找到了问题原因:

问题在哪里?

场景:用户点赞后立即取消

时间线:
T1: 用户点击点赞
T2: Redis INCR → 151
T3: 数据库 INSERT → 成功
T4: 数据库 UPDATE → 成功

T5: 用户点击取消点赞
T6: Redis DECR → 150
T7: 数据库 DELETE → 成功
T8: 数据库 UPDATE → 成功

看起来没问题?

但如果 T7 失败了呢?

T7: 数据库 DELETE → 失败(网络错误)
T8: 数据库 UPDATE → 未执行

结果:
- Redis:150(已取消)
- 数据库:151(未取消)
→ 数据不一致!

核心问题:Redis 和数据库的更新不是原子操作。

一致性方案

我研究了各种缓存一致性方案。

方案 1:Cache Aside Pattern(旁路缓存)

读操作:

写操作:

为什么删除而不是更新?

更新缓存的问题:
- 并发写时,可能导致缓存覆盖
- 多个线程同时更新,缓存可能不正确

删除缓存的优势:
- 下次读取时,会从数据库加载最新值
- 避免并发写导致的缓存不一致

示例:
线程 A:更新数据库 → like_count = 101
线程 A:更新缓存 → cache = 101

线程 B:更新数据库 → like_count = 102
线程 B:更新缓存 → cache = 102

看起来没问题?

如果线程 A 先更新数据库,后更新缓存:
线程 A:更新数据库 → like_count = 101
线程 B:更新数据库 → like_count = 102
线程 B:更新缓存 → cache = 102
线程 A:更新缓存 → cache = 101  ← 覆盖了新值!

如果使用删除缓存:
线程 A:更新数据库 → like_count = 101
线程 A:删除缓存
线程 B:更新数据库 → like_count = 102
线程 B:删除缓存
下次读取:从数据库加载 → 102(正确)

Cache Aside Pattern 的问题:

问题 1:并发读写

时间线:
T1: 线程 A 读取缓存 → 未命中
T2: 线程 B 更新数据库
T3: 线程 B 删除缓存
T4: 线程 A 从数据库读取(旧值)
T5: 线程 A 写入缓存(旧值)

结果:缓存中是旧数据

问题 2:双写不一致

如果有两个服务都更新缓存:
服务 A:更新数据库 → 更新 Redis
服务 B:更新数据库 → 更新 Redis

可能顺序错乱,导致不一致

方案 2:Write Through(写穿透)

原理:

  • 写操作同时更新缓存和数据库
  • 缓存和数据库同步更新

实现:

问题:

  • 缓存和数据库仍然是两个操作
  • 无法保证原子性
  • 性能较差(每次都要等数据库)

方案 3:Write Behind(异步写)

原理:

  • 写操作只更新缓存
  • 异步批量写入数据库
  • 性能最好

实现:

问题:

  • 数据库可能丢失数据(Redis 宕机)
  • 数据库和缓存不一致(写入延迟)
  • 复杂度高

方案 4:延迟双删(推荐)

原理:

  • 更新数据库前删除缓存
  • 更新数据库
  • 延迟一段时间后再删除缓存

实现:

为什么延迟双删有效?

场景:并发读写

时间线:
T1: 线程 A 删除缓存
T2: 线程 B 读取缓存 → 未命中
T3: 线程 B 读取数据库 → 100(旧值)
T4: 线程 A 更新数据库 → 101
T5: 线程 B 写入缓存 → 100(旧值)
T6: 线程 A 延迟删除缓存

T7: 线程 C 读取缓存 → 未命中
T8: 线程 C 读取数据库 → 101(新值)
T9: 线程 C 写入缓存 → 101(正确)

延迟删除的作用:
- 删除线程 B 写入的旧缓存
- 保证最终一致性

延迟时间如何设置?

延迟时间 = 读取数据库时间 + 写入缓存时间 + 安全余量

示例:
- 读取数据库:50ms
- 写入缓存:5ms
- 安全余量:100ms
- 延迟时间:155ms(可设置为 200ms)

建议:
- 一般场景:200ms ~ 500ms
- 高并发场景:500ms ~ 1s
- 可以根据实际情况调整

我的最终方案

结合点赞场景的特点,我选择了延迟双删 + 数据对账的方案。

完整方案

数据对账

为了进一步保证一致性,我添加了定时对账任务。

监控告警

效果评估

一致性提升

优化前:
- 数据不一致率:约 5%
- 用户投诉:每天 3-5 条

优化后:
- 数据不一致率:约 0.1%
- 用户投诉:几乎为 0

性能影响

延迟双删的影响:
- 点赞接口响应时间:从 0.5ms 增加到 2ms
- 仍然远快于数据库方案(50ms)
- 用户无感知

对账任务的影响:
- 每小时运行一次
- 运行时间:约 30 秒
- 对数据库压力小

最终效果

系统运行一周:
- 处理点赞:约 350,000 次
- 数据不一致:约 350 次(0.1%)
- 自动修复:350 次
- 用户投诉:0 条

虽然不能保证 100% 一致,但通过延迟双删 + 定期对账,实现了最终一致性。

课后练习

练习 1

如何选择合适的一致性策略?

参考答案(3 个标签)
缓存一致性系统设计架构

一致性策略对比

策略一致性性能复杂度适用场景
Cache Aside最终一致一般业务
Write Through强一致读多写少
Write Behind最终一致最高写多读少
延迟双删最终一致高并发读写

选择决策树

1. 数据是否绝对不能丢?
   是 → Write Through / 延迟双删 + 对账
   否 → 继续

2. 写入频率如何?
   高 → Write Behind / 延迟双删
   低 → Cache Aside / Write Through

3. 读写比例如何?
   读多写少 → Cache Aside
   写多读少 → Write Behind

4. 性能要求如何?
   极高 → Write Behind
   一般 → Cache Aside / 延迟双删

5. 复杂度要求如何?
   简单 → Cache Aside
   可接受复杂 → 延迟双删 + 对账

点赞场景分析

特点:
- 写入频繁(用户点赞)
- 读取频繁(查看点赞数)
- 性能要求高
- 可容忍短暂不一致

推荐:延迟双删 + 定期对账
- 性能好(异步删除)
- 一致性好(延迟删除 + 对账)
- 复杂度可接受

练习 2

延迟双删中的延迟时间如何设置?

参考答案(3 个标签)
缓存一致性性能调优延迟时间

延迟时间计算公式

延迟时间 = 读取数据库时间 + 写入缓存时间 + 网络延迟 + 安全余量

详细计算:
1. 读取数据库时间
   - SELECT 查询时间
   - 索引扫描时间
   - 数据返回时间
   - 一般:10ms ~ 50ms

2. 写入缓存时间
   - Redis SET 操作时间
   - 一般:1ms ~ 5ms

3. 网络延迟
   - 应用到数据库:1ms ~ 5ms
   - 应用到 Redis:1ms ~ 2ms
   - 总计:2ms ~ 7ms

4. 安全余量
   - 考虑并发情况
   - 考虑 GC 暂停
   - 考虑网络抖动
   - 建议:100ms ~ 200ms

总延迟时间:10 + 5 + 7 + 100 = 122ms(可设置为 200ms)

动态调整策略

不同场景的推荐值

低并发场景(< 100 ops/s):
- 延迟时间:200ms ~ 300ms
- 原因:并发少,简单的延迟即可

中并发场景(100 ~ 1000 ops/s):
- 延迟时间:300ms ~ 500ms
- 原因:并发增多,需要更长延迟

高并发场景(> 1000 ops/s):
- 延迟时间:500ms ~ 1000ms
- 原因:并发高,需要更长的安全窗口

测试验证

练习 3

如何处理分布式系统中的缓存一致性问题?

参考答案(3 个标签)
分布式系统缓存一致性分布式锁

分布式环境的一致性挑战

单机环境:
- 一个 Redis 实例
- 一个数据库实例
- 相对简单

分布式环境:
- 多个 Redis 实例(分片)
- 多个数据库实例(分库分表)
- 数据分布在多台服务器
- 一致性更难保证

方案 1:分布式锁

方案 2:消息队列(最终一致性)

方案 3:版本号(乐观锁)

练习 4

如何监控缓存一致性,及时发现问题?

参考答案(3 个标签)
监控告警运维

监控指标设计

实时监控面板

配置要点

  • 配置表达的是环境差异和运行参数,不是业务规则本身。
  • 队列、缓存、存储和服务参数决定系统在高峰期的缓冲能力。

练习 5

在读写都很多的情况下,如何优化缓存一致性方案?

参考答案(3 个标签)
高并发缓存优化性能

高并发读写优化方案

思考题

  1. 如果数据库是分库分表的,如何保证缓存一致性?

  2. 如何实现”强一致性”的缓存方案(不使用分布式锁)?

  3. 缓存一致性和性能之间如何平衡?

💡 提示:这些问题没有标准答案,建议结合实际情况深入思考。