☰
一文搞懂后端缓存:Redis原理、穿透击穿雪崩与一致性实践
2026/10/1 4:34:56 网站建设 项目流程

后端开发里有个很有意思的现象:没接触过缓存的开发者总觉得它是高深莫测的东西,真正上手之后才发现它是一把非常典型的两面刃。用好了,接口响应时间能从几百毫秒降到几毫秒,数据库压力大幅减小;用不好,轻则数据不一致,重则一条热点数据直接打崩整个数据库连接池。我最初开始系统整理“缓存数据”这个主题,是因为线上的一个真实事故:业务高峰时一个热门的查询接口因为没做缓存,把单库的CPU直接打到了100%,慢查询堆积,最后整条业务链路都受到了影响。

那次事故之后,我开始从头梳理缓存数据的知识体系:为什么需要缓存、缓存应该加在哪一层、Redis的基本原理、缓存和数据库怎么保持一致性、以及缓存穿透、缓存击穿、缓存雪崩这些经典问题。这篇笔记算是我阶段性的总结,适合正在学习后端基础知识的朋友、准备后端面试的开发者,以及那些已经开始写业务代码、但对自己的缓存方案心里没底的同学。我尽量把理论原理、落地代码和实战排查都写到,读完你至少能有一套自己的缓存设计思路。

1. 为什么后端系统离不开缓存

1.1 从一次线上故障说起

先讲那个让我开始整理笔记的事故。当时的业务有一个排行榜查询接口,逻辑并不复杂:从数据库里取出近一个月的数据,按积分排序后返回前100名。原本日活不高的时候一切正常,但有一次做活动运营,用户量翻了几倍,这个接口的请求量也跟着暴涨。数据库的连接池先被打满,接着出现了大量慢查询和连接超时,服务端的错误率直线上升。

当时我还没意识到问题出在“没有缓存”上,第一反应是加索引、加连接数、优化SQL。等把这些都折腾了一遍,发现数据库的压力也只是从“要死”变成了“半死”。后来一位老同事提醒我:这种读多写少、实时性要求不高的数据,为什么不加一层缓存?我这才第一次认真思考缓存的意义。

这个场景几乎解释了后端缓存最核心的价值:用内存的读写速度,去挡掉绝大多数重复的数据库请求。一个接口一天被调用100万次,如果其中90万次都命中缓存,数据库实际只需要扛住10万次请求,压力降到十分之一,响应速度却快了几十倍。

1.2 缓存到底缓的是什么

缓存数据,本质上是在“快设备”和“慢设备”之间加一个中间层。计算机体系结构里,CPU有L1、L2、L3多级缓存,操作系统有内存页缓存,MySQL自己有Buffer Pool,这些全都是同一套思想:把热数据放在离使用者更近、速度更快的地方。

数据库的访问延时代入具体数据的话,单次普通磁盘IO大约在10毫秒左右,而内存读取只要大约100纳秒,相差差不多两个数量级。Redis这类内存缓存之所以能把接口延迟打下来,靠的就是这个量级差异。而内存之所以不能完全替代数据库,因为内存容量相对小、成本高,而且进程一重启数据就没了,所以它只能作为数据库前面的“缓冲层”,不是真正的持久化存储。

理解了这一点,再去看缓存的性能指标,核心就一个词:命中率。命中率高,缓存的价值就大;命中率低,缓存基本就是摆设。我刚学缓存的时候总喜欢堆各种各样的key,后来发现一批key一天都没被访问几次,白白占着内存,这就是典型的没有基于真实访问特征做设计。

1.3 链路上的每一层缓存

学习缓存数据,第一件事不是写代码,而是建立一张全局图:从用户点击到数据返回,中间其实有很多层缓存。

缓存位置常见实现作用范围特点
客户端缓存浏览器HTTP缓存、本地存储单用户减少重复网络请求
CDN缓存静态资源分发节点地域级用户静态资源就近返回
反向代理缓存Nginx proxy_cache网关层挡掉重复的请求转发
本地缓存Caffeine、Guava单服务实例速度最快,但各实例数据独立
分布式缓存Redis、Memcached整个服务集群全局共享,容量大

后端初学者最容易犯的错是把缓存问题窄化成“Redis问题”,实际上Redis只是其中一层。一个完整的后端系统,往往是多层缓存配合使用。我在后来的项目里经常这样设计:热点数据用本地缓存挡第一层,命中不了再查Redis,Redis再没有才落到数据库。每一层都分担一部分压力,整体系统会稳很多。

2. 缓存数据的技术选型与核心原理

2.1 为什么默认方案是 Redis

聊到分布式缓存,绕不开Redis。它是目前后端生态里事实上的标准方案。你可能听说过它快是因为“单线程、基于内存”,但更准确地说,Redis的快来自几个因素的组合。

第一是纯内存操作,数据都在内存里,没有磁盘IO的等待。第二是IO多路复用机制,单个线程可以同时监听大量客户端连接的事件,不需要每个连接都占用一个线程。第三是单线程执行命令,这意味着没有多线程的上下文切换和锁竞争开销,也不会有数据竞争的问题。单线程模型在CPU多核时代看起来像是浪费硬件,但Redis的瓶颈从来不在CPU,而在于网络IO和内存带宽,所以单线程反而带来了极高的稳定性和可预测性。

对比一下Memcached,它是典型的多线程、纯KV结构,支持的数据类型只有简单的字符串。Redis则提供了丰富的数据结构,而且支持持久化、事务、Lua脚本、发布订阅等能力。对于后端业务开发来说,Redis这些特性在实际编码中能省下非常多功夫。我的建议是直接学Redis,不用犹豫。

2.2 Redis 常用数据结构与业务场景

Redis有五种最基础的数据结构,后端开发必须滚瓜烂熟:

  • String(字符串):最常用的结构,可以存任意字符串、数字、二进制数据。典型场景有计数器(点赞数、库存扣减)、对象序列化存储、分布式锁的setnx。
  • Hash(哈希):适合存一个对象的多个字段,比如用户信息里的昵称、头像、积分,可以只更新其中一个字段,不用整条覆盖。
  • List(列表):底层是双向链表,适合做消息队列、最新列表。比如用户的消息通知列表,lpush加rpop实现先进先出。
  • Set(集合):元素唯一且无序,适合做去重、交集并集运算。比如共同好友、抽奖去重。
  • ZSet(有序集合):每个元素带一个score,按分数排序,适合做排行榜。我前面提到的排行榜场景,就是典型的ZSet应用。

每种数据结构都有强烈的场景指向。我见过不少初学者用String一把梭,什么数据都序列化成JSON塞进去,查出列表后自己在代码里排序、去重。这样做当然也能跑,但效率、代码复杂度和维护性都远远不如直接用对数据结构。做缓存数据设计的时候,把“用什么结构承载什么数据”想清楚,本身就是一次很好的架构练习。

2.3 过期策略与内存淘汰机制

缓存数据不能只进不出,否则内存会被消耗完。Redis针对“数据怎么删除”提供了两种机制:惰性删除和定期删除。

惰性删除是当客户端访问某个key时,才检查它是否过期,如果过期就删掉。这个策略最大的优点是节省CPU,缺点是过期的key如果没有被访问,就会一直占着内存。定期删除则是每隔一段时间随机抽取一部分key检查,把过期的删掉。两者配合使用,才能比较均衡地控制内存占用。

但无论怎么删,总有可能在短时间内写入量大于删除量,导致内存不够用。这时候会触发内存淘汰策略。Redis提供了多种淘汰策略,生产环境最常用的几个:

策略含义适用场景
noeviction内存不足时不淘汰,直接报错严格不想丢数据的场景
allkeys-lru从所有key中淘汰最久没使用的通用缓存场景
volatile-lru只从设置了过期时间的key中淘汰希望未设置过期时间的key不被淘汰
allkeys-lfu从所有key中淘汰访问频率最低的需要识别热冷数据的场景

我在配置Redis内存的时候,一般会结合业务特性来选择。纯缓存用途的,用allkeys-lru最省心;如果Redis里还存了一些不能丢的配置型数据,就用volatile-lru,给这些key不设过期时间,它们就不会被淘汰。

2.4 Spring Boot 集成 Redis 的几个关键配置

后端日常开发中,Java和Spring Boot的组合依然非常主流。集成Redis的时候,有典型的几个配置坑。

先看基础配置,核心是redis相关连接信息:

spring: data: redis: host: 127.0.0.1 port: 6379 password: yourpassword database: 0 timeout: 3s lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0

配置里容易忽略的是连接池参数。max-active设得太小,高并发下客户端拿不到连接会直接报错;设得太大,又可能占用过多资源。这个值需要结合接口的QPS和Redis的平均耗时来估算,比如接口QPS是1000、Redis平均耗时2ms,那连接池同时需要的连接数大约就是1000乘以0.002约等于2。再加上一些余量,一般8到16是比较常见的起步值。

另一个高频问题是序列化方式。Spring Boot默认通过RedisTemplate操作数据时,如果不指定序列化器,key和value都会走JDK的序列化,结果就是你在命令行里看到的key是一长串乱码,其他人也没法直接用redis-cli排查。我遇到这个问题后,直接在配置类里指定了StringRedisSerializer以及JSON的value序列化器,这样存进去的数据在Redis命令行里完全可读,排查问题会舒服非常多。

@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }

实际项目中,存取简单字符串一般直接用StringRedisTemplate就够了,它默认就是字符串序列化,没有任何坑。只有当你需要存储对象、列表这类复杂结构时,才需要自定义RedisTemplate配合JSON序列化。

3. 缓存读写流程与数据一致性实操

3.1 标准 Cache Aside 模式

缓存数据和数据库之间的一致性,是后端开发者绕不过去的核心问题。业界使用最广泛的方案叫Cache Aside Pattern,也叫旁路缓存。它的读写逻辑都很直接。

读请求到来时,先查缓存,命中就直接返回;没命中就去查数据库,查到了回写缓存,再返回给调用方。写请求到来时,有两个步骤:先更新数据库,再删除缓存。

你可能马上会问两个问题。第一,为什么写的时候是“删缓存”而不是“更新缓存”?因为更新缓存的操作开销更大、更容易出错。举个例子:一个用户对象的缓存,包含了昵称、头像、积分等十几个字段。如果写操作只更新了积分,你要把整个对象重新序列化并写入缓存,如果还有并发写,缓存里的数据很容易被旧值覆盖。而删缓存就简单得多,下次读请求自然会去数据库拿最新数据重新构建缓存。

第二,为什么一定要“先更新数据库,再删缓存”,而不是先删缓存再更新数据库?如果先删缓存,在缓存被删除到数据库更新完成这一段极短的时间里,读请求会穿透到数据库,读到旧值,然后把它回填进缓存,这时数据库更新完成,缓存里存的却还是旧值。这会造成长时间的数据不一致。反过来,先更新数据库再删缓存,即使缓存删除失败,最多是下次读请求继续穿透,问题影响时间窗口更短,也更可控。

3.2 一个完整的业务代码示例

我把这套流程整理成实际代码,以最常见的“根据用户ID查询用户信息”为例。

@Service public class UserService { private static final String USER_CACHE_KEY_PREFIX = "user:info:"; @Resource private StringRedisTemplate stringRedisTemplate; @Resource private ObjectMapper objectMapper; @Resource private UserMapper userMapper; public UserVO getUserById(Long userId) { // 1. 查询缓存 String key = USER_CACHE_KEY_PREFIX + userId; String json = stringRedisTemplate.opsForValue().get(key); if (StringUtils.hasText(json)) { // 缓存命中 return objectMapper.readValue(json, UserVO.class); } // 2. 缓存未命中,查询数据库 User user = userMapper.selectById(userId); if (user == null) { // 业务上可以在这里缓存空值,防止穿透 return null; } // 3. 回写缓存,设置过期时间 UserVO vo = UserVO.from(user); stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(vo), 30, TimeUnit.MINUTES); return vo; } @Transactional public void updateUser(UserUpdateDTO dto) { // 1. 更新数据库 userMapper.updateById(dto.toUser()); // 2. 删除缓存 String key = USER_CACHE_KEY_PREFIX + dto.getUserId(); stringRedisTemplate.delete(key); } }

这段代码看起来简单,但里面藏着很多细节。比如key的设计用了“业务名:实体名:ID”的格式,这样在Redis命令行里看到一堆key,能一眼就知道是哪个业务、哪个实体、是哪个用户。比如缓存过期时间设成30分钟,这是根据业务容忍的数据不一致窗口来定的。还比如读请求里对空值做了返回处理,防止一个不存在的数据被多次查询打穿到数据库。

3.3 缓存与数据库一致性的坑

前面说的Cache Aside是理论上的标准答案,但在真实的高并发场景下,还是有几个坑。

最典型的坑是并发读写导致的缓存脏数据。设想这样一个时间线:请求A先更新数据库为“新值”,请求B此时读缓存未命中,查询数据库拿到的是“旧值”,请求A还没执行删缓存,请求B先把旧值写回了缓存。结果缓存里存的是旧值,后续所有读请求都拿到旧数据,直到缓存过期或被下一次写操作删除。这个问题的根本原因是“读缓存回写”和“删缓存”之间没有互斥。

业界常见的有两个方向的解法。一个是延迟双删,在更新数据库并删除缓存后,再等几百毫秒,第二次删除缓存。这个延迟时间要大于“从数据库读数据并回写缓存”的耗时,从而把并发窗口覆盖掉。但延迟时间很难绝对精确,属于概率性抢修方案。

另一个更可靠的思路是把数据库变更和缓存操作做成最终一致,比如订阅MySQL的binlog,通过Canal这样的中间件把变更消息发出来,再异步去删除缓存。这样做的好处是删除缓存的操作从业务代码中剥离出来,不再受事务边界影响,即使删除失败也能通过消息重试补偿。缺点是要额外引入一套基础设施。我的建议是:业务初期用Cache Aside加合理的过期时间就够了,不要一上来就上Canal,把架构搞复杂;当团队体量到了能投入专门的基础设施时,再演进。

3.4 本地缓存与分布式缓存的协作

高并发的后端系统里,还有一条常用的缓存路线:本地缓存 + Redis 两级缓存。本地缓存用的是进程内内存,速度最快,但每个服务实例都有一份数据,写的时候要保证所有实例的数据同步;Redis是全局共享的,能保证各实例数据一致,但跨网络访问总有一段IO开销。

两级缓存的读写流程一般是:先查本地缓存,没命中再查Redis,Redis也没有再去数据库,回写时先写Redis,再写本地缓存。这里最难的是本地缓存的失效通知。常见的办法是:每个服务实例启动一个后台任务,每隔几秒或几十秒从Redis拉一次数据版本号或业务数据的变更信号,发现变了就清理本地的旧缓存。这个方案对时效性要求不是极端苛刻的场景很实用,因为保证的是“最终一致”,而不是“强一致”。

我在做双级缓存时踩过一个坑:本地缓存一开始用的过期时间是5分钟,Redis里的数据更新后,要等最长5分钟才在本地生效。后来把本地缓存的有效期缩到30秒,并在更新接口里通过发布订阅的方式主动通知各实例清理本地缓存,才把数据延迟控制在了可接受范围内。这里提醒一点:任何两级缓存方案,必须想清楚“本地缓存可容忍的滞后时间”是多少,再决定失效策略。

4. 缓存数据三大经典陷阱与排查实录

4.1 缓存穿透

缓存穿透是所有缓存问题里理解成本最低、破坏力却相当可观的坑。它指的是:请求的数据在缓存和数据库中都不存在。这时候缓存永远不会命中,所有请求都会直接打到数据库。

举个例子,电商系统里查询一个不存在的商品ID,比如用户手动构造了“-1”或者一个根本不存在的编号。这样的请求第一次会穿透缓存到数据库,数据库也没查到数据,如果代码里不把空结果缓存下来,那以后每个相同的请求都会继续打数据库,攻击者甚至可以用大量随机的假ID,让数据库承受远超预期的压力。

解决思路有三个层次。第一,参数校验。最简单有效,请求进来先做基础校验,明显不合法或者格式不存在的ID直接返回错误。第二,空值缓存。数据库查不到数据时,在Redis里也存一个空值或特殊标记,设置一个较短的过期时间,比如2到5分钟,这样同一个不存在的key短时间内不会反复穿透。第三,布隆过滤器。在缓存前面加一层BloomFilter,把数据库里所有存在的ID预加载进去,请求先问布隆过滤器:这个ID存在吗?过滤器说“不存在”,那一定不存在,直接返回,省掉后续所有查询;说“存在”,才继续走缓存和数据库。注意,布隆过滤器存在误判率,它说“存在”可能是不存在的,所以只能挡掉一部分穿透,不能完全依赖它。

从成本角度说,参数校验和空值缓存是性价比最高的组合,很多项目做到这一步就够了。布隆过滤器适合那种ID总量可控、热点数据明确的场景。

4.2 缓存击穿

缓存击穿和缓存穿透只差一个字,问题却完全不同。缓存击穿指的是:某个热点key在缓存过期的一瞬间,大量并发请求同时发现缓存失效,一起打到数据库。这个热点key平时可能扛住了99%的读流量,它一过期,那几个瞬间的请求直接把数据库压垮。

最常见的场景:一条爆款商品的信息、一条热点新闻的详情、一个热门活动的配置,QPS可能上万,一旦缓存到期,所有请求一起去查库,数据库从低负载瞬间变成满负载。

解决击穿的主流方案有两个。方案一是互斥锁。当缓存没有命中的时候,不是所有请求都去查库,而是先尝试获取一把分布式锁,拿到锁的请求去查库并重建缓存,其他请求在锁外面稍等一下,然后重新查缓存。实现可以用Redis的setnx命令:

public UserVO getUserWithMutex(Long userId) { String key = "user:info:" + userId; String json = stringRedisTemplate.opsForValue().get(key); if (StringUtils.hasText(json)) { return objectMapper.readValue(json, UserVO.class); } // 尝试加锁,防止大量请求同时打到数据库 String lockKey = "lock:user:info:" + userId; String requestId = UUID.randomUUID().toString(); Boolean locked = stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, 3, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 加锁成功,再查一次缓存,防止在自己等待时缓存已被他人重建 json = stringRedisTemplate.opsForValue().get(key); if (StringUtils.hasText(json)) { return objectMapper.readValue(json, UserVO.class); } User user = userMapper.selectById(userId); // 模拟业务逻辑处理 UserVO vo = UserVO.from(user); stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(vo), 30, TimeUnit.MINUTES); return vo; } finally { // 释放锁,注意只能删除自己设置的锁 if (requestId.equals(stringRedisTemplate.opsForValue().get(lockKey))) { stringRedisTemplate.delete(lockKey); } } } else { // 没拿到锁,休眠一小段时间后重试 try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getUserWithMutex(userId); } }

方案二是逻辑过期。不设置真正的物理过期时间,而是在value里额外存一个业务过期时间戳。每次读取时判断逻辑上是否过期,没过期直接返回;过期了,先返回旧数据,同时只让一个线程去数据库更新缓存。这个方案的优势是“永远不直接打到数据库”,用户体验平滑,但实现更复杂,还需要处理旧数据的更新时机。

我个人的选择习惯是:对数据一致性要求没那么苛刻、但绝对不想有瞬时高数据库压力的场景,用逻辑过期;对一致性更敏感的场景,用互斥锁,虽然有一小段时间的阻塞,但保证缓存中的数据永远是新的。

4.3 缓存雪崩

缓存雪崩是三个问题里影响面最大的:大量key在同一时间段集中过期,或者Redis实例整个挂了,导致大量请求直接压到数据库。和击穿的区别是,击穿是单个热点key失效,雪崩是大面积key同时失效。

一个常见的触发原因很朴素:我们给缓存设置过期时间的时候,都喜欢设同一个值,比如“统一30分钟”。如果一批数据在同一时间写入缓存,它们的过期时间也完全一样,30分钟后就会集体过期。这时如果系统面临的是高流量,请求就会在同一瞬间涌入数据库。

解决雪崩的基础手段是过期时间加随机值。比如同样是30分钟过期,写成“30分钟 + 一个0到60秒的随机值”,让每个key的过期时间散开,从“集体过期”变成“错峰过期”。这个方案成本极低,效果非常明显,我建议所有缓存写入都默认加上随机偏差。

另一个手段是多级缓存兜底。Redis前加Caffeine本地缓存,即使Redis因网络故障或者集群迁移暂时不可用,本地缓存还能扛住一部分流量。再外层还可以在网关或服务入口做限流降级,把超出承载能力的请求挡在外面,而不是让它们全部打到数据库。

如果雪崩的根因是Redis实例本身挂了,那就要从高可用架构上解决问题:主从复制加哨兵模式,或者Redis Cluster集群模式。后端学习到一定阶段,Redis的高可用方案是必须补上的一课。

4.4 线上排查实录

聊完了理论,说说我实际排查缓存问题的一些方法。

一个很有用的排查入口是缓存命中率。Redis INFO命令里有一个keyspace_hits和keyspace_misses两个计数器,命中率等于hits除以(hits加上misses)。如果命中率长期低于60%,说明缓存设计可能有问题,要么是key粒度不对,要么是过期时间太短,要么是缓存的数据根本不是热点数据。

排查缓存穿透,我一般是看数据库慢查询里有没有大量“查不到数据的SELECT”。这类SQL特征是条件里带了一个在表里不可能存在的ID,频次又很高。排查缓存击穿,重点看是不是某个特定的热点key在某个时间点同时引发了大量数据库查询,这个需要把Redis的监控数据和业务的接口监控对齐。排查缓存雪崩,直接去看Redis过期key数量的监控曲线,如果发现周期性尖峰,大概率就是过期时间设置太集中了。

另一个非常值得养成的习惯是观察Redis的慢日志。Redis执行命令超过一定阈值就会被记录到slowlog,默认阈值是10毫秒。出现慢日志时,多半跟大key有关,比如一个hash里存了几十万个字段,或者一个value是几兆的大字符串。大key在删除或序列化时会阻塞Redis主线程好几个毫秒甚至更久,影响的是整个实例上的所有业务。线上操作的时候,遇到这种大key要分批删除,或者干脆避免把过大的数据直接扔进Redis。

5. 缓存数据日常运维与调优经验

5.1 缓存预热怎么做

系统上线或者大促前,最怕的是缓存里空空如也,第一批用户请求全部穿透到数据库。这时候就需要缓存预热:在用户真正访问之前,把热门数据提前加载到缓存里。

预热方案一般两种。一种是启动时加载,项目启动后写一个ApplicationRunner,把配置表、基础字典、热门商品等数据批量写入Redis,有效期可以设置得比普通缓存更长一些。另一种是基于历史访问统计,从日志或者数据库里把过去几天访问量最高的前N个对象捞出来,提前刷进缓存。

做缓存预热时有个细节要提醒大家:预热数据的过期时间一定要设计好。如果你预热的数据和平时请求写入的数据过期时间一样,那高峰期恰好赶上集体过期,反而触发了雪崩。一般预热数据的过期时间会故意拉长一点,比如平时30分钟,预热数据设为60到120分钟,扛住峰值再说。

5.2 监控指标

缓存数据跑起来之后,光会写代码不够,得会看监控。我平时重点关注这些指标:

指标说明关注原因
命中率缓存命中的请求比例直接反映缓存的业务价值
内存使用率Redis已用内存和maxmemory的比值接近上限会触发淘汰策略
连接数当前客户端连接数连接池打满是性能问题的前兆
命令耗时平均/最大命令执行时间判断是否出现慢命令或阻塞
key过期数量每秒过期的key数目发现过期集中的风险

这些监控数据光靠Redis自带的INFO命令不够直观,需要配合监控系统把指标可视化。小团队没有完整监控平台的时候,可以先写个定时任务把INFO的关键字段收集到日志里,应急排查时也能用。

5.3 几个实用小技巧

根据我的经验,再列几个缓存数据开发中的实用技巧。

热点key加版本号。如果缓存的数据是某种可以批量刷新的配置,可以给key加上版本号后缀,比如user:info:v2:12345。发版升级时直接全量切换版本,避免删除大量旧key带来的瞬时缓存穿透。

分布式锁的value一定要带唯一标识。释放锁的时候先判断是不是自己加的锁,防止因为操作超时把别人刚获取的锁误删掉。

缓存key一定要可读、可管理。我见过有人用一串无规则的MD5当key,出了问题完全没法排查。合理的方式是“业务域:实体类型:实体ID:附加信息”,同时把可能变动的维度放进key,比如语言、店铺ID等。

关注缓存重建的耗时。如果缓存里存的是一个复杂对象,需要查好几张表才能组装出来,那重建缓存的时间本身就不可忽略。这种场景要额外小心,可以考虑异步重建缓存,而不是让每个穿透请求都在接口里同步做重活。

6. 写在后端学习笔记最后的一点体会

缓存数据这块我从“踩坑”到“看明白”,花了大概两三个项目的时间。最大的体会是:缓存的难点从来不是Redis的命令有多复杂,而是你愿不愿意在动手前把业务场景、访问特征、一致性要求想清楚。很多线上事故,新课上都讲过,但只有真正遇到一次,你才会对“缓存穿透”“缓存击穿”“缓存雪崩”这几个词产生肌肉记忆。

最后分享一个我在所有项目里都会坚持的细节:给每条缓存数据都想明白“丢了会怎样、要不要尽快恢复、可容忍多久不更新”。这三个问题的答案,决定了过期时间、更新策略和兜底方案。把它们装进脑子,再写缓存代码,你会发现自己下的每个配置都是有依据的,而不是照着别人的博客抄一遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询