2013年,我第一次接触Redis。

那时候Redis还不算太火,Memcached还是缓存的主流。但是我用了Redis之后,就被它征服了——数据结构丰富(string、hash、list、set、zset)、性能极高(单线程10万+ QPS)、支持持久化、支持主从复制。

从那以后,我就成了Redis的忠实粉丝。项目中能用缓存的地方,我都用Redis缓存——用户信息、商品信息、文章列表、排行榜、计数器、分布式锁、消息队列……我甚至一度觉得,"有了Redis,性能问题都能解决"。

但是,用了三年Redis,踩了各种坑之后,我才明白:缓存不是万能的。

缓存能解决很多性能问题,但是缓存也带来了很多新的问题——缓存穿透、缓存击穿、缓存雪崩、数据不一致、缓存污染、内存淘汰……如果用不好,缓存反而会成为系统的负担,甚至导致系统崩溃。

今天就来聊聊,Redis用了三年,我踩过的那些坑,以及我才明白的那些道理。

一、Redis的基本用法和好处

在聊坑之前,先简单说说Redis的基本用法和好处。

Redis的常用数据结构:

  • String:字符串,最基本的数据结构,可以存字符串、数字、二进制数据。常用于缓存用户信息、商品信息、计数器。
  • Hash:哈希,类似PHP的关联数组,适合存对象。常用于缓存用户信息、商品信息(一个key存一个对象的多个字段)。
  • List:列表,有序可重复。常用于消息队列、最新列表、时间线。
  • Set:集合,无序不可重复。常用于标签、共同好友、去重。
  • ZSet:有序集合,每个元素有一个score,按score排序。常用于排行榜、积分系统、延迟队列。

Redis的好处:

  1. 性能极高:Redis是内存数据库,读写速度极快,单线程能达到10万+ QPS
  2. 数据结构丰富:不像Memcached只有string,Redis有5种基本数据结构,能满足各种场景
  3. 支持持久化:RDB和AOF两种持久化方式,重启后数据不丢失
  4. 支持主从复制:读写分离,高可用
  5. 单线程原子操作:所有操作都是原子的,不需要考虑并发问题
  6. 丰富的功能:除了缓存,还能做计数器、分布式锁、消息队列、排行榜、限流等

正因为Redis有这么多好处,我才成了它的忠实粉丝,项目中到处都用Redis。

二、踩过的坑

坑1:缓存穿透

缓存穿透是指查询一个数据库中不存在的数据,由于缓存中也没有,每次请求都会打到数据库,导致数据库压力过大。

比如,用户查询一个不存在的文章ID(如id=999999),缓存中没有,数据库中也没有。每次查询都会先查缓存(没命中),再查数据库(没数据),然后缓存也不存(因为没数据)。如果有人恶意大量查询不存在的ID,数据库就会被打垮。

我遇到的情况:有一次,我们的API被人恶意扫描,大量请求查询不存在的用户ID,导致数据库连接数飙升,差点宕机。

解决方案

  1. 缓存空值:查询数据库不存在的数据,也缓存一个空值(如null或特殊标记),设置较短的过期时间(如5分钟)。这样下次查询同样的key,会直接命中缓存的空值,不会打到数据库。
  2. 布隆过滤器(Bloom Filter):在缓存之前加一层布隆过滤器,把所有可能存在的key都存在布隆过滤器中。查询时先查布隆过滤器,如果不存在,直接返回,不查缓存和数据库。
  3. 参数校验:对请求参数做校验,过滤掉明显不合理的请求(如id为负数、id过大)。

我们最后用了"缓存空值+参数校验"的方案,简单有效,解决了缓存穿透的问题。

坑2:缓存击穿

缓存击穿是指一个热点key,在缓存过期的瞬间,大量并发请求同时打到数据库,导致数据库压力过大。

比如,一篇热门文章,缓存了1小时。在缓存过期的那一刻,正好有1000个用户同时访问这篇文章。这1000个请求都会发现缓存没命中,然后同时去查数据库,同时写缓存。数据库瞬间承受1000个相同的查询,可能会被打垮。

我遇到的情况:有一次,我们的首页热点数据缓存过期,正好赶上流量高峰,大量请求同时打到数据库,数据库CPU飙升到100%,页面卡顿了好几分钟。

解决方案

  1. 互斥锁(Mutex Lock):缓存没命中时,先加一个分布式锁,只有拿到锁的请求去查数据库并写缓存,其他请求等待然后重试读缓存。这样可以避免大量请求同时打到数据库。
  2. 永不过期:热点key不设置过期时间,而是后台异步更新缓存。这样缓存永远不会过期,就不会有击穿的问题。
  3. 提前续期:在缓存快过期的时候,后台异步提前刷新缓存。这样用户访问的时候,缓存总是存在的。

我们最后用了"互斥锁"的方案,用Redis的SETNX实现分布式锁,简单有效。

坑3:缓存雪崩

缓存雪崩是指大量缓存同时过期,或者Redis宕机,导致大量请求同时打到数据库,导致数据库压力过大甚至崩溃。

缓存雪崩有两种情况:

  1. 大量key同时过期:比如,批量设置了相同过期时间的key,在同一时间全部过期,大量请求打到数据库。
  2. Redis宕机:Redis服务挂了,所有缓存都不可用,所有请求都打到数据库。

我遇到的情况:有一次,我们做活动,批量缓存了大量商品数据,过期时间都设成了1小时。活动开始1小时后,这些缓存同时过期,大量请求打到数据库,数据库直接宕机了,活动页面打不开,损失惨重。

解决方案

  1. 过期时间加随机值:设置缓存过期时间时,在基础时间上加一个随机值(如1小时+随机0-300秒),避免大量key同时过期。
  2. 多级缓存:本地缓存(如Caffeine、APCu)+ Redis缓存,Redis挂了还有本地缓存兜底。
  3. Redis高可用:Redis主从+哨兵,或者Redis Cluster,避免单点故障。
  4. 限流降级:数据库压力过大时,限流或者降级(返回默认值、缓存值、友好提示),保护数据库不被打垮。

我们最后用了"过期时间加随机值+Redis高可用+限流降级"的方案,再也没有发生过缓存雪崩。

坑4:数据不一致

缓存和数据库的数据不一致,是缓存最常见的问题之一。

比如,用户信息更新了,数据库更新了,但是缓存没更新,用户看到的还是旧信息。或者,缓存更新了,但是数据库更新失败,缓存和数据库不一致。

数据不一致的原因有很多:

  • 更新数据库后,忘记更新缓存
  • 更新缓存失败
  • 并发情况下,读写顺序错乱
  • 缓存过期后,读到旧数据

我遇到的情况:有一次,用户修改了昵称,数据库更新成功了,但是缓存更新失败了(网络抖动)。用户看到的还是旧昵称,投诉了好几次,我们查了半天才发现是缓存没更新。

解决方案

  1. Cache Aside Pattern:最常用的缓存模式——读的时候,先读缓存,没命中再读数据库,然后写缓存;写的时候,先更新数据库,再删除缓存(不是更新缓存,是删除缓存)。
  2. 延迟双删:更新数据库后,先删除缓存,然后延迟一段时间(如500ms),再删除一次缓存。这样可以避免并发情况下的脏数据。
  3. 消息队列异步更新:更新数据库后,发消息到消息队列,消费者异步更新缓存。
  4. 设置合理的过期时间:即使数据不一致,缓存过期后也会重新从数据库加载最新数据。过期时间不能太长(不一致时间太长),也不能太短(缓存命中率低)。

我们最后用了"Cache Aside Pattern(先更数据库再删缓存)+ 合理过期时间"的方案,数据不一致的问题大大减少。

坑5:缓存污染

缓存污染是指大量不常用的数据被缓存,占用了Redis内存,导致真正的热点数据被淘汰,缓存命中率下降。

比如,一个爬虫批量爬取了网站的所有文章,每篇文章都被缓存了,但是这些文章大部分再也不会被访问。这些"僵尸缓存"占用了大量内存,导致真正的热点文章被淘汰,用户访问的时候缓存没命中,还要查数据库。

我遇到的情况:有一次,我们的网站被爬虫爬了,Redis内存使用率飙升到95%,触发了maxmemory策略,大量热点key被淘汰,缓存命中率从90%降到了50%,数据库压力大增。

解决方案

  1. 设置合理的过期时间:所有缓存都设置过期时间,避免永久缓存。不常用的数据,过期时间设短一点。
  2. LRU/LFU淘汰策略:Redis的maxmemory-policy设置为allkeys-lru(最近最少使用淘汰)或allkeys-lfu(最不经常使用淘汰),让不常用的数据优先被淘汰。
  3. 限制缓存大小:给不同类型的缓存设置不同的内存上限,避免某一类缓存占用过多内存。
  4. 监控缓存命中率:监控缓存命中率,如果命中率下降,分析原因,及时调整。

我们最后用了"合理过期时间+allkeys-lru淘汰策略+监控命中率"的方案,缓存污染的问题得到了控制。

坑6:大key和热key

大key:指value很大的key,比如一个string存了几MB的数据,或者一个hash/list/set/zset有几百万个元素。大key会导致:

  • 网络传输慢
  • Redis阻塞(单线程,处理大key会阻塞其他请求)
  • 内存占用大
  • 淘汰大key时卡顿

热key:指访问量特别大的key,比如热门商品、热门文章。热key会导致:

  • 单台Redis服务器压力过大
  • 网络带宽瓶颈
  • 缓存击穿(热key过期时)

我遇到的情况:有一次,我们把一个用户的所有文章(几千篇)存在一个hash里,value有几十MB。每次读取这个hash,Redis都会卡顿几秒钟,影响其他请求。

解决方案

  • 大key拆分:把大key拆分成多个小key,比如把一个大hash拆成多个小hash,按ID分片。
  • 压缩:大value可以压缩后再存(如gzip、snappy),减少内存和网络传输。
  • 热key多副本:热key复制多份,分散到不同的Redis服务器,或者本地缓存热key,减轻Redis压力。
  • 监控:监控大key和热key,及时发现和处理。

坑7:Redis持久化和主从的坑

Redis虽然是内存数据库,但是支持持久化(RDB和AOF)和主从复制。这些功能也有坑。

RDB持久化的坑

  • RDB是定时快照,fork子进程生成快照。fork的时候,如果内存很大,会耗时较长,可能导致Redis短暂不可用。
  • RDB快照间隔期间,如果Redis宕机,会丢失最后一次快照之后的数据。

AOF持久化的坑

  • AOF是追加写日志,文件会越来越大,需要定期rewrite(重写)。rewrite的时候也会fork子进程,可能导致短暂不可用。
  • AOF的appendfsync策略,如果设为always,每条命令都刷盘,性能差;如果设为no,由操作系统刷盘,可能丢数据;everysec是折中方案,每秒刷一次,最多丢1秒数据。

主从复制的坑

  • 主从复制是异步的,主库宕机可能丢失数据。
  • 从库同步延迟,读写分离时可能读到旧数据。
  • 主从切换(故障转移)时,可能出现脑裂、数据不一致。

这些坑,我在使用Redis的过程中都遇到过。解决方案主要是:合理配置持久化策略、监控主从延迟、做好高可用(哨兵/集群)、重要数据不要只存在Redis。

三、缓存的正确使用姿势

踩了这么多坑,我总结了一些缓存的正确使用姿势:

1. 不是所有数据都要缓存

缓存不是万能的,不是所有数据都要缓存。以下数据不适合缓存:

  • 频繁更新的数据(如实时库存、实时价格)
  • 一致性要求极高的数据(如账户余额)
  • 访问频率低的数据(缓存了也没用,反而占内存)
  • 数据量太大且访问分散的数据(缓存命中率低,不如直接查数据库)

适合缓存的数据:

  • 读多写少的数据(如文章、商品信息、用户信息)
  • 热点数据(访问频率高)
  • 一致性要求不高的数据(允许短暂不一致)
  • 数据库查询成本高的数据(复杂查询、多表关联)

2. 设置合理的过期时间

所有缓存都应该设置过期时间,不要永久缓存。过期时间的选择:

  • 热点数据:过期时间长一些(如1小时-1天),提高命中率
  • 非热点数据:过期时间短一些(如5分钟-30分钟),避免缓存污染
  • 一致性要求高的数据:过期时间短一些(如1分钟-5分钟),减少不一致时间
  • 过期时间加随机值,避免大量key同时过期

3. 选择合适的缓存模式

常用的缓存模式:

  • Cache Aside(旁路缓存):最常用,读先查缓存没命中查库再写缓存,写先更库再删缓存。适合大多数场景。
  • Read Through(读穿透):缓存层负责读数据库,应用只和缓存交互。适合缓存层封装好的场景。
  • Write Through(写穿透):写的时候同时写缓存和数据库,缓存层负责。适合写少读多的场景。
  • Write Behind(异步写):写的时候只写缓存,异步批量写数据库。性能好,但是可能丢数据,适合对一致性要求不高的场景。

大多数场景用Cache Aside就够了。

4. 做好缓存异常的兜底

缓存不是100%可靠的,Redis可能宕机、可能超时、可能连接失败。要做好缓存异常的兜底:

  • 缓存操作失败时,降级到直接查数据库
  • 数据库压力过大时,限流或者返回默认值
  • 多级缓存(本地缓存+Redis),Redis挂了还有本地缓存
  • 熔断机制,缓存层故障时快速失败,不要拖垮整个系统

5. 监控和告警

缓存不是"加上就完事了",要做好监控和告警:

  • 监控Redis的内存使用率、QPS、命中率、连接数、延迟
  • 监控大key、热key、慢查询
  • 监控缓存命中率,命中率下降时及时分析原因
  • 设置告警阈值,异常时及时通知

四、写在最后

Redis用了三年,我才明白缓存不是万能的。

缓存能解决很多性能问题,但是缓存也带来了很多新的问题——缓存穿透、缓存击穿、缓存雪崩、数据不一致、缓存污染、大key热key、持久化和主从的坑……如果用不好,缓存反而会成为系统的负担。

但是,这不是说缓存不好。缓存是好东西,是高性能系统不可或缺的组件。关键是,要正确地使用缓存,了解缓存的坑,做好预案和兜底。

三年前,我觉得"有了Redis,性能问题都能解决";三年后,我明白"缓存是银弹,不是万能药"。缓存能解决很多问题,但是不能解决所有问题,而且缓存本身也需要精心维护。

最后,用一句话总结:缓存不是万能的,但是没有缓存是万万不能的。关键是,用对场景、用对方法、做好兜底。

愿每一个用Redis的开发者,都能避开这些坑,用好缓存,让系统又快又稳。愿Redis这只可爱的鲸鱼,载着你的缓存,畅游在高性能的海洋里。