在高并发系统中,Redis通常作为缓存层,而MySQL等数据库作为持久化存储层。
如果设计不当,就会出现 缓存和数据库数据不一致 的问题。
常见场景:
用户修改数据
↓
数据库更新成功
↓
Redis仍然是旧数据
用户读取时就会出现 脏数据问题。
因此需要设计合理的 缓存一致性策略。
一、常见缓存模式
缓存与数据库通常有三种模式:
| 模式 | 描述 |
|---|---|
| Cache Aside | 应用程序控制缓存 |
| Read Through | 缓存自动加载 |
| Write Through | 同步写缓存 |
互联网系统最常用的是:
Cache Aside Pattern
也叫:
旁路缓存模式
二、Cache Aside 模式
读流程
请求
↓
查询Redis
↓
缓存命中 → 返回
↓
缓存未命中
↓
查询数据库
↓
写入Redis
↓
返回数据
示例代码逻辑:
```java
User user = redis.get(key);
if(user == null){
user = db.query(id);
redis.set(key, user);
}
return user;
写流程
写数据时:
更新数据库
↓
删除缓存
示例:
update user set name = 'Tom' where id = 1
redis.del(user:1)
这样下一次读取时:
重新加载数据库数据
三、为什么不先删除缓存再更新数据库?
如果顺序是:
删除缓存
↓
更新数据库
可能出现:
线程A 删除缓存
线程B 查询数据库
线程B 写入旧缓存
线程A 更新数据库
最终:
缓存 = 旧数据
数据库 = 新数据
出现 数据不一致。
四、正确写法
推荐顺序:
先更新数据库
再删除缓存
流程:
更新数据库
↓
删除Redis缓存
这样即使并发出现:
短暂不一致
缓存很快会被刷新。
五、延迟双删策略
在高并发情况下可能仍存在极小概率问题。
解决方案:
延迟双删
流程:
更新数据库
↓
删除缓存
↓
sleep 500ms
↓
再次删除缓存
示例:
redis.del(key);
Thread.sleep(500);
redis.del(key);
这样可以防止:
旧数据重新写入缓存
六、使用消息队列保证一致性
在大型系统中,通常会使用 MQ异步删除缓存。
流程:
更新数据库
↓
发送MQ消息
↓
消费者删除缓存
架构:
业务服务
↓
数据库
↓
MQ
↓
缓存服务
↓
Redis
优点:
- 解耦
- 高可靠
- 可重试
七、订阅数据库 Binlog
大型互联网系统会使用 Binlog同步缓存。
流程:
数据库更新
↓
Binlog
↓
Canal
↓
更新Redis
架构:
MySQL
↓
Binlog
↓
Canal
↓
缓存更新服务
↓
Redis
优点:
- 自动同步
- 数据一致性更强
八、设置缓存过期时间
为了避免长期不一致:
缓存必须设置TTL
例如:
TTL = 5分钟
即使出现问题:
缓存最终会过期
保证 最终一致性。
九、最终一致性模型
互联网系统通常采用:
最终一致性
而不是:
强一致性
原因:
- 强一致性性能低
- 分布式系统复杂
最终一致性意味着:
短时间可能不一致
但最终数据会一致
十、最佳实践总结
Redis缓存与数据库一致性推荐方案:
Cache Aside Pattern
更新数据库
删除缓存
增强方案:
延迟双删
MQ删除缓存
Binlog同步
设置TTL
完整流程:
更新数据库
↓
删除缓存
↓
MQ通知
↓
延迟删除缓存
十一、面试高分回答总结
如果面试官问:
Redis缓存和数据库如何保证一致性?
可以这样回答:
通常采用 Cache Aside 模式,读数据时先查缓存,缓存未命中再查数据库并写入缓存;写数据时先更新数据库再删除缓存。同时可以通过延迟双删、消息队列异步删除缓存、订阅数据库Binlog等方式进一步提高缓存和数据库的一致性,整体保证最终一致性。


