别再背八股文了!Redis 缓存穿透、缓存雪崩?先搞懂这 30 行查询代码再说 别再背八股文了Redis 缓存穿透、缓存雪崩先搞懂这 30 行查询代码再说前言你刷了三天 Redis 缓存八股文什么穿透、击穿、雪崩背得滚瓜烂熟。然后打开 IDE对着屏幕上这 30 行代码光标闪了五分钟——第一行应该写什么我就是这样。之前看到网上各种缓存策略缓存一致性的文章每个字都认识凑在一起就是看不懂。后来我花了整整两周一行一行地啃把同事写的这段查询代码彻底搞懂了。等你真正搞懂这 30 行代码怎么写、为什么这么写之后那些八股文里的概念不用背也自然会了。环境说明组件版本 / 说明JDK1.8Spring Boot2.xRedis任意版本Spring Data Redis 操作MyBatis-PlusgetById()是它提供的一句 SQL 不用写HutoolStrUtil、JSONUtil来自这里工具库这段代码到底在干什么先把代码完整贴出来然后我拆开给你讲。看完这段你就能理解我当时的感受——代码不多但每一行都扛着一个知识点。OverridepublicResultqueryById(Longid){// 1. 构造 Redis 的 keyStringkeySystemConstants.CACHE_SHOP_KEY_PREFIXid;// 2. 从 Redis 查缓存StringshopJsonstringRedisTemplate.opsForValue().get(key);// 3. 判断缓存是否命中if(StrUtil.isNotBlank(shopJson)){// 命中 → JSON 字符串转成 Java 对象 → 直接返回ShopshopJSONUtil.toBean(shopJson,Shop.class);returnResult.ok(shop);}// 4. 没命中 → 从数据库查ShopshopgetById(id);if(shopnull){returnResult.fail(店铺不存在);}// 5. 数据库查到了 → 写入 Redis下次就不用查库了stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));returnResult.ok(shop);}这段代码的学名叫Cache-Aside Pattern旁路缓存模式也叫缓存穿透防护的基础写法。不急着记名字我们先看每一行到底在干嘛。逐行拆解初学者友好版第 1 行定义 keyStringkeySystemConstants.CACHE_SHOP_KEY_PREFIXid;这里你可能会问为什么不直接用id当 key因为 Redis 里存的不是只有店铺数据。如果所有数据的 key 都直接用 id万一将来存了用户数据key 也是1、订单数据key 也是1就全串了。所以 Redis 的 key 一般有个命名规范数据Key 示例说明店铺cache:shop:1001前缀区分数据类型用户cache:user:1001和店铺的 id1001 不冲突订单cache:order:20240718001前缀 唯一标识SystemConstants.CACHE_SHOP_KEY_PREFIX就是一个常量比如cache:shop:拼上 id 就变成了cache:shop:1001。看到冒号了吗Redis 客户端会把它显示成文件夹层级方便你在可视化工具里按前缀浏览。第 2 行从 Redis 查缓存StringshopJsonstringRedisTemplate.opsForValue().get(key);这一排方法名很长我当初看到就头大。拆开看其实很简单部分什么意思stringRedisTemplate一个现成的工具对象专门操作 Redis 中的字符串opsForValue()告诉它我要操作String 类型的值Redis 有 5 种数据类型get(key)拿着 key 去 Redis 里取值有就返回没有返回 null所以整句话翻译成人话就是“嘿Redis帮我看看cache:shop:1001这个 key 有没有值”注意这里拿到的是一个String。因为 Redis 里存的是 JSON 字符串后面会讲为什么所以变量的类型是String shopJson。第 3-6 行判断缓存是否命中if(StrUtil.isNotBlank(shopJson)){ShopshopJSONUtil.toBean(shopJson,Shop.class);returnResult.ok(shop);}StrUtil.isNotBlank()是什么StrUtil.isNotBlank()是 Hutool 工具包的一个静态方法。要理解它先看这张表方法传入null传入传入 空格传入abcisBlank()truetruetruefalseisNotBlank()falsefalsefalsetrueisEmpty()truetruefalsefalse为什么用isNotBlank而不是! null因为在 Redis 里get(key)返回null确实表示没有这个 key。但如果有人在 Redis 里存了一个空字符串或一堆空格呢isNotBlank把这些脏数据也拦住了更加安全。一个小 TIPStrUtil的isNotBlank内部先检查了null再检查了长度最后检查了是否全是空格。不用你写三行 if 了。JSONUtil.toBean()在干什么现在shopJson的内容长这样{id:1001,name:茶百道,type:2,score:4.5}这是个 JSON 字符串。但 Java 是强类型语言你不能直接拿这个字符串去调shop.getName()。必须转成一个 Shop 对象。JSONUtil.toBean(shopJson, Shop.class)做的就是这件事JSON 字符串 Java 对象 {id:1001,name:茶百道} → Shop{id1001, name茶百道}它把 JSON 里的每个字段名去 Shop 类里找同名的属性把值填进去。这就是反序列化Deserialize。第 7-10 行缓存没命中查数据库ShopshopgetById(id);if(shopnull){returnResult.fail(店铺不存在);}getById(id)是 MyBatis-Plus 提供的。它自动拼出 SQLSELECT * FROM shop WHERE id ?把结果封装成 Shop 对象返回。这里有个细节MyBatis-Plus 只需要你写一个继承BaseMapperT的接口getById就能用了。对初学者来说你只需要知道它帮你查数据库就行具体怎么做到的以后慢慢看。如果数据库里也没这个 id说明这个 id 压根不存在直接返回店铺不存在。第 11-13 行数据库查到了回写 RedisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));returnResult.ok(shop);这是整套流程的点睛之笔。来我把这行拆开shop是一个 Shop 对象Java 对象JSONUtil.toJsonStr(shop)把 Shop 对象**序列化Serialize**成 JSON 字符串stringRedisTemplate.opsForValue().set(key, json字符串)把这个字符串存进 Redis为什么要存 JSON 字符串而不是直接把 Shop 对象存进去存储方式说明问题存 Java 对象需要用RedisTemplateObject, Object存进去是一堆乱码字节别的语言读不了存 JSON 字符串用StringRedisTemplate人类可读任何语言都能解析这就是用StringRedisTemplate而不是RedisTemplate的原因。StringRedisTemplate 只处理字符串序列化策略就是简单的 UTF-8 字符串不存在乱码问题。这张图把整个流程说清楚了下面这张时序图搞懂了整段代码你就真会了MySQLRedisService本段代码客户端MySQLRedisService本段代码客户端alt[数据库有数据][数据库也没有]alt[缓存命中][缓存未命中]queryById(1001)查 cache:shop:1001返回 JSON 字符串JSON → Shop 对象反序列化直接返回不查数据库返回 nullSELECT * FROM shop WHERE id 1001返回 Shop 对象Shop 对象 → JSON 字符串序列化写入缓存 cache:shop:1001返回结果返回 null店铺不存在你看核心逻辑只有两条路缓存有 → 直接返回秒级响应缓存没有 → 查数据库 → 写缓存 → 返回下次就不用查库了这就是Cache-Aside 模式的核心思想以数据库为准缓存只是加速。几个你可能会问的问题Q1为什么缓存命中后不用再查数据库因为 Redis 存在内存里读取速度是微秒级的。MySQL 存在磁盘上读取速度是毫秒级的。差了 1000 倍。第一次查询虽然慢查库 写缓存但从第二次开始Redis 直接命中速度跟飞一样。Q2如果数据库的数据变了怎么办好问题。这段代码只处理了查询。如果用户更新了店铺信息你需要把 Redis 里对应的缓存删掉或更新否则用户看到的是旧数据。这就是缓存一致性问题八股文里常说的缓存更新策略。简单说更新数据库 → 删除缓存下次查询时会重新从数据库加载这叫延迟双删策略的简化版。Q3如果有人一直用一个不存在的 id 来查呢比如 id -1这段代码的逻辑是查 Redis → 没有查 MySQL → 也没有返回店铺不存在Redis 里不会存这个 -1。那下次又有人查 -1 呢又走一遍查库流程。每次都穿透到数据库这就是缓存穿透。解决办法是把不存在的 id 也缓存一个空值进去给个很短的过期时间。总结这段代码虽然只有 30 行但把它彻底搞懂你就能理解Redis 缓存的基本流程先查缓存命中返回未命中查库并回写序列化与反序列化Java 对象 ↔ JSON 字符串跨语言可读Key 命名规范用前缀区分数据类型避免 key 冲突查询过程的两种状态缓存命中快和缓存未命中慢缓存穿透的概念不存在的 key 每次都打穿到数据库如果你现在能对着这张时序图把每一行代码的流程讲出来——恭喜你你已经超过了 90% 只背八股文的人。延伸思考这段代码如果改成高并发场景会有什么问题两个线程同时查到同一个不存在的 id会不会同时查两次数据库这就是缓存击穿和缓存雪崩的话题了——留到下篇文章聊。参考资料Hutool 官方文档 — JSONUtilHutool 官方文档 — StrUtilSpring Data Redis 官方文档MyBatis-Plus 官方文档 — BaseMapperRedis 数据类型介绍本文为原创内容转载请注明出处。如果这篇文章对你有帮助欢迎点赞 、收藏 ⭐、关注 ➕你的支持是我持续输出的动力