Redis缓存与数据库一致性的原理及最佳实践

作者:袖梨 2026-07-29

1. 不一致为何会发生?(通俗原理)

数据库(MySQL)与缓存(Redis)彼此独立,更新数据就像要把同一封信分别送到两座房子。

Redis缓存与数据库的一致性的原理与最佳实践

  • 如果先给 Redis 送信(删/改),再给 MySQL 送信,但 MySQL 送信慢了,别人来 Redis 查发现没数据,去 MySQL 查到了旧数据,又写回 Redis,导致 Redis 变成旧数据。
  • 如果先给 MySQL 送信,再给 Redis 送信(删缓存),中间有极短时间(毫秒级)Redis 还是旧数据,但一删掉就没了,别人再来只能查 MySQL 的新数据。

我们只能追求这一核心结论:“最终一致”(允许短时间存在差异,但会迅速修复),绝对强一致无法用低成本实现。

2. 为何选择“删除缓存”而非“更新缓存”?

假定两个线程在同一时间更新数据:

  • 线程A 将 age 修改为 18,并把缓存更新成 18。
  • age 的值被线程B改为 20,缓存随后由它更新为 20(后执行)。

    受网络延迟影响,若 B 的请求先抵达缓存、A 后抵达,缓存最后会成为旧值 18。

但“删除缓存”不会带来这一困扰:无论执行有先有后,删除后缓存都不存在;下次读取会先查库再回填,从而写入最新的 DB 值。

3. Cache-Aside 模式的最佳实践核心流程

  • 读取数据:查询从 Redis 开始,若有命中便返回;否则转查 MySQL,并将结果写回 Redis(过期时间设为 TTL)。
  • 写入数据:先对 MySQL 执行更新(事务提交),然后删除 Redis

4. 并发中的终极陷阱(延迟双删的来由)

在极少见的情形下:

  1. 线程A 更新 MySQL 为 100,删除了 Redis。
  2. 线程B 读 Redis 未命中,去 MySQL 读到 100,准备回填 Redis。
  3. 线程C 更新 MySQL 为 200,删除了 Redis(此时 Redis 是空的)。
  4. 由于网络慢,线程B 的回填此刻才完成,100 被写入 Redis。结果是 Redis 保存了旧值 100。

解决办法:延迟双删。即在更新 MySQL 并删除 Redis 后,等待 1~2 秒(等待可能的并发读回填完成),再删除一次 Redis,把旧值清理掉。

5. 最佳实践示例代码(Java Spring Boot + Redis)

5.1 基础方案(使用最广,适用于多数场景)

完成 DB 更新并提交事务后,再删除缓存,同时设置 TTL 作为兜底。

代码示例:

  @Service  @Slf4j  public class UserService {      @Autowired      private UserDao userDao;      @Autowired      private RedisTemplate<String, Object> redisTemplate;      private static final String CACHE_KEY = "user:";      // 查询:旁路缓存      public User getById(Long id) {          String key = CACHE_KEY + id;          // 1. 查缓存          User user = (User) redisTemplate.opsForValue().get(key);          if (user != null) {              return user;          }          // 2. 查数据库          user = userDao.selectById(id);          if (user != null) {              // 3. 回填缓存,必须设置过期时间(兜底)              redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);          }          return user;      }      // 更新:先更新DB,后删缓存      @Transactional      public void updateUser(User user) {          // 1. 更新数据库          userDao.updateById(user);          // 2. 删除缓存(注意:必须确保事务提交后再删,避免DB回滚导致缓存被误删)          String key = CACHE_KEY + user.getId();          redisTemplate.delete(key);          log.info("更新用户 id={}, 缓存已删除", user.getId());      }  }

5.2 事务提交后删除与延迟双删(严谨版)

缓存删除由事务同步器安排在 DB 真正提交之后,同时还要执行延迟二次删除。

代码示例:

  @Service  @Slf4j  public class UserServiceStrict {      @Autowired      private UserDao userDao;      @Autowired      private RedisTemplate<String, Object> redisTemplate;      // 用于延迟任务的线程池      private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(4);      @Transactional      public void updateUser(User user) {          // 1. 更新数据库          userDao.updateById(user);          String key = CACHE_KEY + user.getId();          // 2. 注册事务同步器,确保事务提交后再操作缓存          TransactionSynchronizationManager.registerSynchronization(              new TransactionSynchronization() {                  @Override                  public void afterCommit() {                      // 2.1 立即删除缓存                      redisTemplate.delete(key);                      log.info("事务提交后立即删除缓存 key={}", key);                      // 2.2 延迟 1.5 秒后再次删除(防止并发读回填旧数据)                      scheduler.schedule(() -> {                          try {                              redisTemplate.delete(key);                              log.info("延迟删除缓存 key={}", key);                          } catch (Exception e) {                              log.error("延迟删除缓存失败", e);                          }                      }, 1500, TimeUnit.MILLISECONDS);                  }              }          );      }  }

5.3 高并发终极兜底(订阅 MySQL Binlog)

业务若对最终一致性要求极高,可让 Canal 监听 binlog 并异步删除缓存。应用层仅更新 DB,变更事件由 Canal 解析,随后自动删除 Redis。

由框架回调触发的 Canal 消费端伪代码示例:

  @Component  @Slf4j  public class CanalCacheListener {      @Autowired      private RedisTemplate<String, Object> redisTemplate;      public void onUserChange(Long userId, String eventType) {          if ("UPDATE".equals(eventType) || "DELETE".equals(eventType)) {              String key = "user:" + userId;              redisTemplate.delete(key);              log.info("Binlog 异步清理缓存 userId={}", userId);          }      }  }

6. 最佳实践归纳(必记口诀)

  1. :缓存优先,随后 DB,回填时设置 TTL。
  2. :缓存须等 DB 操作完成且事务提交后再删除。
  3. 防并发:采用延迟双删(1~2 秒)。
  4. 保命符:缓存务必设置过期时间(如 30 分钟);即使删除失败,稍后也会自动消失。
  5. 大杀器:一致性要求极高的场景,可异步解耦删除,方式是由 Canal 订阅 Binlog。
  6. 禁区:更新 DB 之前删除缓存,以及用缓存更新取代缓存删除,这两种做法都绝对不可采用。

7. 不同场景的选型建议

你的业务场景建议采用方案
普通管理后台,低并发使用基础版(5.1)+ TTL 即可
用户端读取并发高,要求数据较新采用严谨版(5.2)延迟双删
金融/库存需要强校验,绝不允许出错不采用缓存,而是直接读取数据库
分布式微服务,优先考虑解耦采用 Binlog 异步删除(5.3)

以上详细介绍了Redis缓存与数据库一致性的原理及最佳实践,想了解更多Redis缓存与数据库一致性资料,可继续关注本站其他相关文章!

您或许感兴趣的文章:
  • Redis缓存与数据库一致性全程指南
  • 数据库与Redis缓存数据一致性的解决思路
  • 解决redis缓存和数据库之间的一致性问题
  • 保持MySQL数据库、Redis缓存一致的更新策略
  • 保证数据库与Redis缓存一致性的方法浅析

相关文章

精彩推荐