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

第一次用Redis缓存,结果设置失败了,后来才发现这些坑和问题

第一次用Redis缓存,结果设置失败了,后来才发现这些坑和问题,这事儿得从我刚接手一个用户查询功能优化说起,当时为了赶进度,想着Redis不是号称性能神器嘛,简单看了下基本命令,就兴冲冲地用上了,结果上线没多久,就发现缓存时灵时不灵,有时候能命中,有时候又直接去查数据库了,搞得数据库压力没减反增,被领导一顿说,后来静下心来仔细研究,又请教了组里的大佬,才发现自己踩了那么多“坑”。

第一个大坑就是,我根本没弄明白缓存数据到底该什么时候过期,我一开始图省事,给所有用户数据都设置了一个固定的过期时间,比如10分钟,心想10分钟更新一次,数据也够新了,结果问题来了,有些用户信息其实很少变动,比如用户名、注册时间这些,根本没必要10分钟就清掉缓存再重来一次,白白浪费了内存,而有些热门商品的价格或者库存,可能运营同学后台一秒内就修改了好几次,我这缓存10分钟才更新,导致前端用户看到的价格居然是旧的,差点引发客诉,我才明白,过期时间不能“一刀切”,得根据数据的变化频率和重要性来灵活设置,比如不变的基础数据,可以设置很长的时间甚至永不过期;而变化频繁的关键数据,过期时间要设得很短,或者采用其他策略保证及时更新。(来源:实际项目踩坑经验总结)

第一次用Redis缓存,结果设置失败了,后来才发现这些坑和问题

第二个让我栽跟头的问题是,我忽略了缓存穿透的可能性,什么叫缓存穿透?就是我当时写的代码逻辑是:先查Redis,如果Redis里有,就直接返回;如果Redis里没有(缓存未命中),就去数据库查,查到结果后再塞回Redis,这听起来没问题对吧?但有一天,突然有一批恶意请求过来,查询的根本不存在的用户ID,我的程序每次都会先查Redis(当然没有),然后去查数据库(也肯定没有),既然数据库返回空,我就没把空结果存到Redis里,这下坏了,这批恶意请求每次都绕过缓存,直接压到数据库上,数据库差点被打挂,后来才知道,对于这种查询不到的数据,也应该在Redis里缓存一个空值(比如null),并设置一个较短的过期时间,这样下次再有同样的无效请求,就能在缓存层被挡住,避免对数据库的持续攻击。(来源:团队技术分享中提到的常见缓存问题)

第一次用Redis缓存,结果设置失败了,后来才发现这些坑和问题

第三个问题是关于数据序列化的,我用的Java语言,一开始顺手就把从数据库查出来的User对象直接序列化成Java原生格式存进了Redis,本地测试的时候一切正常,但有一天需要重启应用服务,重启之后问题来了:程序从Redis里反序列化之前缓存的数据时,居然报错了!排查了半天才发现,是因为我修改了User类的源码,增加了一个字段,而Redis里存的是老版本类的序列化数据,新版的类自然不认识,导致反序列化失败,这下所有缓存瞬间失效,全部请求又都涌向了数据库,吃一堑长一智,后来我改用了JSON这种通用的序列化格式来存储数据,这样即使实体类有增减字段,只要核心字段不变,基本都能兼容,避免了因为应用发布导致的缓存雪崩问题。(来源:个人故障排查记录)

还有一个细节问题,是关于Redis连接管理的,我一开始在代码里,每次要操作Redis时,都新建一个连接,用完就关,自以为这样很规范,避免了连接泄露,结果在压测时,性能简直惨不忍睹,远远达不到Redis应有的水准,请教同事后才恍然大悟,频繁地创建和关闭TCP连接是非常耗时的操作,正确的做法是使用连接池,预先建立好一批连接,需要的时候从池子里取,用完了还回去,这样就避免了重复建立连接的开销,仅仅做了这个改动,接口的响应时间就下降了一大截。(来源:性能优化实践中的发现)

经过这次惨痛的教训,我算是明白了,用Redis这种看似简单的组件,背后也有很多需要深思熟虑的地方,它不是简单的一个setget就完事了,从缓存的粒度、键的设计、过期策略,到如何防止穿透、雪崩,再到序列化方式、连接管理,每一个点都可能成为线上的隐患,光知道基本命令是远远不够的,必须理解其背后的原理和最佳实践,才能让它真正成为提升系统性能的利器,而不是埋下故障的雷。