当前位置:首页 > 问答 > 正文

Redis缓存那些事儿,代码优化其实没那么难,聊聊技术细节和实操经验

我记得有一次,我们系统在高峰期突然变得特别慢,页面加载要十几秒,数据库服务器都快报警了,查来查去,最后发现问题出在没有用好缓存上,那次经历让我深刻体会到,Redis这东西,用好了是神器,用不好就是给自己挖坑,今天我就结合自己踩过的坑,聊聊Redis缓存那些实在的技术细节和操作经验,你会发现代码优化真的没那么玄乎。

什么时候该用缓存?别瞎用

Redis缓存那些事儿,代码优化其实没那么难,聊聊技术细节和实操经验

不是所有数据都适合扔进缓存,那次出问题的原因,就是我们一股脑把很多不常用的、甚至实时性要求很高的数据都做了缓存,结果缓存频繁失效,又频繁从数据库里查,反而增加了数据库的负担,这就像你为了省事,把家里所有东西都贴上标签,结果找东西时先要查标签本子,反而更慢了。

适合缓存的数据有这么几种特点(来源:个人实践经验总结):

Redis缓存那些事儿,代码优化其实没那么难,聊聊技术细节和实操经验

  1. 读多写少:比如商品信息、用户基本信息、新闻文章内容,这些数据一旦生成,很长时间内不会变,但会被大量读取,给它们加缓存,效果立竿见影。
  2. 计算复杂:有些页面需要聚合多个数据源,或者进行复杂的计算才能得出结果,比如一个报表数据,每次实时计算要花5秒钟,那你完全可以把最终结果缓存个5分钟,这5分钟内所有请求都直接读缓存,快如闪电。
  3. 热点数据:系统里总有一些数据是特别受欢迎的,用Redis的计数器或者有序集合(ZSET)就能很容易地统计出哪些是热点Key,然后对它们进行重点关照,比如设置更长的过期时间或者放在更快的实例上。

缓存穿透、雪崩、击穿,这三个“坏家伙”怎么对付?

这是面试常问的,也是实际开发中必须解决的三个典型问题。

Redis缓存那些事儿,代码优化其实没那么难,聊聊技术细节和实操经验

  1. 缓存穿透:指的是查询一个根本不存在的数据,比如有人恶意攻击,一直请求数据库里没有的商品ID,这样请求会直接绕过缓存,每次都砸到数据库上,数据库很快就撑不住了。

    • 解决办法(来源:常见的解决方案实践)
      • 缓存空对象:就算数据库查不到,也在缓存里存个空值(比如NULL),并设置一个较短的过期时间(比如30秒),这样后续同样的请求在缓存失效前,就会拿到这个空值,而不会访问数据库,但要注意,如果恶意攻击的ID是海量的,会浪费很多缓存空间。
      • 布隆过滤器(Bloom Filter):在访问缓存和数据库之前,先用一个布隆过滤器拦一下,布隆过滤器可以告诉你某个Key“一定不存在”或者“可能存在”,把所有合法的ID预先加载到布隆过滤器中,当有请求过来,先检查这个ID在过滤器中是否存在,如果肯定不存在,就直接返回,根本不用去查缓存和数据库,这个办法更节省空间,但实现起来稍微复杂一点。
  2. 缓存雪崩:指的是缓存中大量的数据在同一时间过期失效,导致所有请求瞬间都涌向数据库,数据库压力激增,可能直接挂掉。

    • 解决办法(来源:个人及团队应对方案)
      • 设置不同的过期时间:这是最简单有效的,不要给所有缓存数据设置相同的过期时间,可以在基础过期时间上,加上一个随机的偏移量(比如30分钟 ± 5分钟),让缓存失效的时间点尽量均匀分布。
      • 热点数据永不过期:对于一些极其核心的热点数据,可以考虑设置为永不过期,然后由后台任务或者程序在数据更新时,主动去更新缓存,这样就避免了因失效而产生的雪崩问题。
      • 做二级缓存:比如本地缓存(Ehcache) + Redis缓存,当Redis崩溃时,本地缓存还能撑一会儿,给数据库一个缓冲。
  3. 缓存击穿:这个词听起来和穿透很像,但不一样,它指的是某一个热点Key(比如爆款商品)在缓存过期的瞬间,同时有大量的请求过来,这些请求发现缓存失效,都会去数据库查询并回设缓存,这个瞬间的并发压力可能把数据库打垮。

    • 解决办法(来源:分布式锁的典型应用场景)
      • 加互斥锁:当第一个请求发现缓存失效时,它不去查数据库,而是先去获取一个分布式锁(比如用Redis的SETNX命令实现),拿到锁的线程才有资格去查询数据库并重建缓存,其他没拿到锁的线程则等待一小段时间,然后重新从缓存中尝试获取数据,这样虽然可能让部分请求稍微等一下,但保护了数据库,我们当时就是用这个办法解决了热点商品页面的并发问题。

操作上的一些小经验

  1. Key的设计要规范:比如业务名:表名:ID,像user:info:123,这样一看就知道是哪个业务的数据,也方便用keys命令(生产环境慎用)或者扫描工具来管理,别用一些含义不明的缩写。
  2. 批量操作提升效率:Redis支持管道(pipeline)和批量命令(如mget、mset),如果你要一次性获取多个Key的值,用mget比用for循环一次次get要快得多,因为它减少了网络往返的时间。
  3. 关注内存使用情况:定期用info memory命令看看内存用了多少,有没有大的Key(比如一个Key下面存了几十万个元素的集合),大Key会影响网络传输和持久化性能,需要想办法拆分。
  4. 别忘了设置过期时间:除非你明确知道这个数据需要永久保存,否则一定要给缓存设置过期时间,这是防止垃圾数据无限堆积的最基本保障。

用好Redis的关键在于理解业务场景,知道数据的特点,然后对症下药,它就像是一味药材,用对了能治病强身,用错了反而会加重病情,从最简单的读多写少场景开始实践,慢慢体会上面说的这些点,代码优化的大门就算打开了。