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

Redis缓存数据库那些实操经验和用法分享,讲讲怎么搞定缓存行的事儿

缓存不是万能的,没有缓存是万万不能的

首先得明白,我们为啥要用Redis?说白了,就是给数据库“减负”,像一些首页的商品列表、用户信息、热点文章这类读多写少的数据,每次都去查数据库,数据库压力大,响应也慢,这时候,Redis这种内存数据库,读写速度极快,把它放在应用和数据库之间,就像给数据库请了个超级助理,第一次从数据库查到数据后,顺手塞一份到Redis里,并设置个过期时间,下次再要这个数据,就直接问Redis要,又快又轻松。

但这里第一个经验就来了:别把Redis当成数据库用,它的主要任务是加速访问和分担压力,所有缓存的数据都应该被认为是“可能会消失的”,你的业务逻辑必须保证,即使Redis完全宕机,仅靠数据库也能正常运行(虽然会慢点),这就是所谓的“缓存降级”意识。

缓存穿透:对付“空数据”的攻击

我遇到过一种情况,有恶意请求或者程序BUG,不停地用数据库里根本不存在的数据ID来查询,比如不存在的用户ID,这下好了,Redis里肯定没有(因为不存在),请求直接穿透到数据库,数据库就惨了。

怎么搞定?办法很朴素:

  1. 存个“空值”:就算数据库查不到,也在Redis里存一个特殊的空值(NULL”),并设置一个较短的过期时间(比如30秒),这样,短时间内同样的无效请求再来,就能在Redis层面被挡住,不会去骚扰数据库。
  2. 布隆过滤器:这是个更高级的武器(虽然你让我拒绝专业术语,但这个概念很简单),你可以把它理解成一个超大的、高效的“集合检查器”,在把数据写入数据库时,顺便把这个数据的ID注册到布隆过滤器里,当有查询请求来时,先让布隆过滤器检查一下这个ID是否存在,如果它说“不存在”,那这个ID肯定不在数据库里,直接返回空结果,连Redis都不用查了,如果它说“可能存在”,才放行去查Redis或数据库,这能非常有效地拦截大量恶意攻击,这个方法是参考了许多大型互联网公司的架构方案,Redis设计与实现》一书中也提到过。

缓存雪崩:别让缓存“集体罢工”

Redis缓存数据库那些实操经验和用法分享,讲讲怎么搞定缓存行的事儿

想象一下,如果Redis里大量缓存数据在同一时刻集体失效,会发生什么?所有请求瞬间像洪水一样涌向数据库,数据库肯定扛不住,直接挂掉,这就是“雪崩”。

关键点在于,不要让大量key拥有相同的过期时间。 我的经验是,在设置缓存过期时间时,使用一个“基础时间+随机波动值”的策略,原本想都设成1小时过期,现在可以改成“1小时 + 随机0到300秒”,这样,大量缓存的失效时间就被打散了,不会在同一秒内集体失效,给数据库的压力就是平滑的,而不是脉冲式的。

缓存击穿:热点数据失效的“惊群效应”

这个和雪崩类似,但目标更集中,说的是某一个“热点key”(比如某个秒杀商品的信息)在失效的瞬间,同时有大量请求来查询它,这时,所有请求发现缓存没了,都会同时去查数据库,相当于对这个热点key的查询打穿了缓存。

Redis缓存数据库那些实操经验和用法分享,讲讲怎么搞定缓存行的事儿

这个问题的解决思路是“加锁”,但不是在应用层用重量级的锁,那样性能太差,我的做法是利用Redis自身的SETNX命令(SET if Not eXists)来实现一个简单的分布式锁。

  • 当第一个发现缓存失效的请求到来时,它用SETNX命令尝试设置一个锁key。
  • 如果设置成功,说明它是第一个,于是它去数据库加载数据,写入缓存,最后删除这个锁key。
  • 在这期间,其他并发的请求也发现缓存失效,它们也尝试用SETNX设置同一个锁key,但会失败(因为已经被第一个请求设置了),这时,这些请求不是傻等着,而是可以选择:a)短暂睡眠后重试查询缓存;b)直接返回一个默认值或友好提示。
  • 这样,就只有第一个请求会去访问数据库,其他请求最终都会从第一个请求重建的缓存中获取数据,这个方法在众多技术社区,比如开源中国的社区讨论中,被广泛推荐和实践。

数据一致性:先更新数据库,还是先删缓存?

这是个经典难题,更新了数据库,怎么让缓存也知道更新?通常有两种策略:

  1. 先更新数据库,再删除缓存:这是更常用的方法,被称为“Cache-Aside”模式,为什么是删除而不是立即更新缓存?因为删除是幂等的(操作多次效果一样),而且避免了在并发写场景下可能出现的更新顺序错乱问题,这种方法有个小概率的瑕疵:如果A线程更新了数据库后,在删除缓存之前,线程B读了旧的缓存,那么B会读到脏数据,但因为这个时间窗口极短,实践中通常可以接受。
  2. 先删除缓存,再更新数据库:这能避免上述的脏读,但可能引发另一个问题:如果A线程删了缓存,在它更新数据库完成之前,线程B来读数据,发现缓存为空,就会去数据库读旧值并重新塞回缓存,导致缓存里还是旧数据,为了解决这个,可以采用“延迟双删”策略:即线程A在更新完数据库后,再Sleep一小段时间(比如几百毫秒,大于一次读数据库+写缓存的时间),然后再次删除缓存,这样能大概率清掉可能被B线程塞回去的脏数据。

在实际项目中,我们通常选择第一种,因为实现简单,瑕疵影响小,如果对一致性要求极高,可以考虑引入消息队列进行异步同步,但那套就复杂多了。

最后总结一下,用好Redis缓存,核心思想就几点:明确缓存定位、预防三种经典问题(穿透、雪崩、击穿)、在性能和一致性之间做出合理的权衡,这些经验都不是凭空想出来的,是无数项目实战后的共识,希望这些实实在在的“干粮”能对你有所帮助。