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

Redis缓存那些事儿,先从注解说起再聊聊实际应用和坑

说到Redis缓存,咱们先从最常用的Spring注解@Cacheable说起吧,这个注解就像是给方法结果贴了个“已缓存,下次直接取”的标签,比如你有个查用户信息的方法,第一次调用,它会老老实实去数据库里查,查回来后,Spring会偷偷地按照你设定的规则(比如key=“user:123”),把结果塞进Redis里,等第二次再用同样的参数调用这个方法,它就不去数据库了,直接拐个弯从Redis里把结果拿出来返回,速度飞快,这招对付那些经常被查询但又不太变化的数据,特别管用,比如商品分类、城市列表、用户基础信息这些。

Redis缓存那些事儿,先从注解说起再聊聊实际应用和坑

光会查还不够,数据总会变的,这时候就得用上@CachePut和@CacheEvict这两个兄弟注解。@CachePut是“不管三七二十一,我先更新了缓存再说”,通常用在更新方法上,比如你修改了用户昵称,调用updateUser方法,这个方法执行成功后,@CachePut会强制把最新的用户信息重新塞回缓存,保证缓存里的数据是最新的,而@CacheEvict更直接,它的意思是“把这个缓存标记为无效,给我删了”,比如删除一个用户,你肯定不希望缓存里还留着这个“幽灵用户”,所以在删除方法上加上@CacheEvict,删数据库的同时,顺手就把对应的缓存也给清理了,这样下次再查这个用户,因为缓存没了,就会去数据库查,自然也就查不到了,保持了数据的一致性。

Redis缓存那些事儿,先从注解说起再聊聊实际应用和坑

听起来很美好是吧?但坑也随之而来了,第一个大坑就是“缓存穿透”,这可不是数据没缓存上,而是有人使坏或者系统bug,不停地请求一个数据库里根本不存在的数据,比如疯狂请求用户ID为-1的用户信息,因为数据库里没有,所以缓存里肯定也不会有(或者有过期了),这下好了,这个请求每次都会绕过缓存,直接砸在数据库上,短时间内大量这样的请求,能把数据库打趴下,解决办法也简单,对于这种明确不存在的数据,我们也把它缓存起来,比如缓存一个空值(null),并设置一个较短的过期时间(比如30秒),这样后续的请求在缓存层就被挡住了,不会再去折腾数据库。

Redis缓存那些事儿,先从注解说起再聊聊实际应用和坑

第二个坑叫“缓存雪崩”,想象一下,如果系统启动时,你把一大批数据同时加载到Redis里,并且设置了相同的过期时间,比如都是半夜2点过期,好了,到了凌晨2点,这批缓存“集体阵亡”,这时候突然来了大量请求,发现缓存全都失效了,于是所有的请求都涌向数据库,数据库瞬间压力山大,可能就直接崩溃了,解决这个问题的关键就是“错峰”,给不同的缓存数据设置一个随机的过期时间,比如基础过期时间是30分钟,然后在此基础上加一个-5分钟到+5分钟的随机值,这样就能保证缓存不会在同一时刻大面积失效,请求的压力会平摊开。

第三个是“缓存击穿”,这个有点像是缓存雪崩的“单体强化版”,说的是某一个非常热点的数据(比如某个顶流明星发布的新闻),在它缓存过期的瞬间,有海量的请求同时过来,都发现缓存失效了,于是这些请求同时去访问数据库,就像一颗子弹击穿了缓存这层防护,直接打在数据库这一个点上,后果也很严重,对付它的常用办法是“加锁”,在第一个请求发现缓存失效去查数据库时,给它加个分布式锁,这样其他请求就只能在外面等着,等第一个请求把数据从数据库取回来并重新设置好缓存后,后面的请求就可以直接从缓存里拿了,这样可以避免数据库被重复查询。

在实际应用里,除了这些经典的坑,还有一些细节要注意,缓存的数据序列化方式要选对,不然可能浪费空间或者出现乱码,还有,缓存不是万能的,它消耗内存,所以不是什么数据都值得往里放,要权衡数据的访问频率和大小,双写一致性也是个麻烦事,尤其是在分布式环境下,更新了数据库再删缓存,这个过程中间也可能有别的请求读到旧数据,需要根据业务场景选择最合适的策略,比如延迟双删之类的。

Redis缓存是个提升性能的神器,但用不好也是个“坑王”,关键是要理解它的工作原理,预见到可能出问题的地方,然后有针对性地做好防护。