Redis缓存一致性
Redis 缓存一致性是指 Redis 中的数据与数据库中的数据保持一致。由于数据库操作和缓存操作通常无法处于同一个本地事务中,因此在并发、网络异常或缓存删除失败时,可能出现数据库已经更新但 Redis 仍保存旧数据的问题。
常见方案是采用 Cache Aside 模式:查询时先查缓存,缓存未命中再查数据库并回填;更新时先更新数据库,再删除缓存,而不是直接更新缓存。同时为缓存设置 TTL,并通过消息队列、重试机制或监听 MySQL Binlog 的方式保证缓存删除最终成功。对于余额、支付等强一致性业务,不能只依赖普通缓存,应以数据库或事务系统中的数据为准。
所谓缓存一致性,通常指:
Redis中缓存的数据,与数据库中的真实数据保持一致。
public User gerUser(Long id) {
String key = "user:" + id;
// 1、从Redis缓存中取数据
User user = redisTemplate.posForValue().get(key);
if(user != null) {
return user;
}
// 2、缓存未命中,查数据库
user = userMapper.selectById(id);
//3、将查到的结果写入Redis缓存
redisTemplate.opsForValue().set(key, user);
return user;
}- 读取数据时先从Redis缓存中查找数据,如果没找到再去数据库中查找,最后将数据库结果写入Redis;
- 更新数据时先更新数据库,然后删除Redis缓存
常见的更新方案:
先更新数据库,再更新缓存
一般不推荐,例如:线程A:数据库更新为A 线程B:数据库更新为B 线程 B:缓存更新为 B 线程 A:缓存更新为 A最终的结果:
数据库:B 缓存:A因为数据库更新顺序和缓存更新顺序可能不一致。
另外,更新缓存可能需要复杂计算,而这份缓存之后不一定会被读取,存在无效写入。先删除缓存,再更新数据库
同样存在问题:线程 A:删除缓存 线程 B:查询缓存,发现不存在 线程 B:读取数据库旧数据 线程 A:更新数据库新数据 线程 B:把旧数据写入缓存结果:
数据库:新数据 缓存:旧数据先更新数据库,再删除缓存
Cache Aside Pattern,旁路缓存模式
示例:@Transactional public void updateUser(User user) { userMapper.updateById(user); redisTemplate.delete("user:" + user.getId()); }然而先更新数据库,在删除缓存的方式并不能保证数据库和Redis缓存绝对一致,如果存在两个并发线程,线程A查询Redis未命中,再去读取数据库旧数据,线程B更新数据库为新数据,并删除Redis缓存,之后线程A将刚刚读取到的旧数据写入Redis,最终Redis中仍然可能是旧数据。(这种情况成立的条件比较苛刻:读数据的操作必须比更新数据库+删除缓存的过程还慢。因此大多数业务中,概率相对较低)为了进一步降低风险,可以采用以下措施:
设置缓存过期时间TTL
redisTemplate.opsForValue().set( "user:" + id, user, 20, TimeUnit.MINUTES );TTL无法解决实时一致性问题
延迟双删
1、删除Redis缓存 2、更新数据库 3、等待一段时间 4、再次删除缓存public void updateUser(User user) { String key = "user:" + user.getId(); // 第一次删除缓存 redisTemplate.delete(key); userMapper.updateById(user); CompletableFuture.runAsync(() -> { try { Thread.sleep(500); // 第二次删除用于清理并发线程可能重新写入的旧缓存。 redisTemplate.delete(key); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); }- 删除失败后进行重试
最典型的失败:数据库更新成功,但是Redis删除失败。
为解决这个问题,可以把缓存删除操作发送到消息队列:数据库更新成功-->发送“删除缓存”消息-->消费者删除Redis-->失败则继续重试。
为了保证消息最终被消费,通常还会配合消息重试、死信队列、操作日志、人工补偿、幂等处理。因为删除同一个Redis Ley多次没有副作用,因此缓存删除操作适合做幂等重试。 监听数据库Binlog
更可靠的方案是监听MySQL Binlog:业务程序更新 MySQL ↓ MySQL 产生 Binlog ↓ Canal / Debezium 监听 Binlog ↓ 发送变更消息 ↓ 删除或更新 Redis常见架构为:MySQL → Binlog → Canal → Kafka/RocketMQ → Redis
使用分布式锁
对一致性要求较高的热点数据,可以让缓存重建和数据库更新互斥:获取锁 ↓ 检查缓存 ↓ 查询数据库 ↓ 写入缓存 ↓ 释放锁但是这回增加系统复杂度和延迟,通常只对热点Key使用,不会给所有缓存操作都加锁
实际系统需要根据业务选择一致性等级:
最终一致性
允许短时间内缓存与数据库不同,但最终会恢复一致。
适用于:- 商品详情
- 用户资料
- 文章内容
- 商品列表
- 普通配置信息
强一致性
每次读取都必须获得最新数据。
适用于:- 余额扣减
- 库存最终确认
- 支付状态
- 交易结果
- 权限立即撤销
这类数据不能简单依赖普通 Redis 缓存。通常应以数据库或专门的事务系统为准,或者在关键操作中直接绕过缓存。
大部分 Redis 缓存场景追求的是最终一致性。
| 手段 | 作用 | 说明 |
|---|---|---|
| 设置过期时间TTL | 最终兜底 | 即使删除失败,缓存也会自动过期 |
| 缓存空值 | 防止缓存穿透 | 数据库查不到时也缓存一个空对象,设置较短过期时间 |
| 分布式锁 | 防止缓存击穿 | 热点 key 失效时,只允许一个线程去查数据库 |
| Canal / Maxwell | 强一致性同步 | 监听 MySQL binlog,自动更新/删除 Redis(最彻底但复杂度高) |
标签:无