当前位置: 代码网 > it编程>数据库>Redis > Redis缓存与数据库的一致性的原理与最佳实践

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

2026年07月27日 Redis 我要评论
1. 为什么会出现不一致?(通俗原理)缓存(redis)和数据库(mysql)是两个独立的存储。更新数据时,就像同时给两座房子送信。如果先给 redis 送信(删/改),再给 mysql 送信,但 m

1. 为什么会出现不一致?(通俗原理)

缓存(redis)和数据库(mysql)是两个独立的存储。更新数据时,就像同时给两座房子送信。

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

核心结论:我们只能追求“最终一致”(短暂允许不一样,但很快修复),无法低成本实现绝对强一致。

2. 为什么是“删除缓存”而不是“更新缓存”?

假设两个线程同时更新数据:

  • 线程a 把 age 改成 18,更新缓存为 18。
  • 线程b 把 age 改成 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缓存与数据库一致性的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com