简介:资源定位:这份资源是基于 Redis 的“黑马点评”项目实战源码包,面向正在学习 Redis 缓存、高并发处理与 Spring Boot 整合开发的 Java 开发者。项目模拟在线点评平台的典型业务场景,重点展示 Redis 在热点数据缓存、点赞与排行榜、会话维持、发布订阅等功能中的落地方式,适合有 Java 基础、希望提升服务端性能优化能力的读者参考。包体信息:压缩包共 79 个文件,大小约 90KB,包含 72 个 Java 源码文件、2 个 XML 配置、2 个 Lua 脚本、1 个 YAML 配置文件与 1 个 SQL 脚本;Lua 脚本可用于实现原子性操作,SQL 脚本用于初始化点评平台相关数据表。学习热度:该资源已有 313 人学习下载。价值感:资源目录按 Maven 工程规范组织,覆盖项目主代码、测试代码与配置文件,便于直接导入 IDE 后结合业务理解 Redis 整合思路,也可作为课设、毕业设计或企业级点评类项目初版参考。
1. 黑马点评这个项目,到底值不值得花两天跑一遍
如果你正处在“Redis 命令背得熟、但一进业务就不知道怎么用”的阶段,黑马点评是绕不开的那份源码。它表面上是一个仿大众点评的单机项目,实际上是把 Redis 最常见的六类真实场景全部串了一遍:短信验证码用 String 存,店铺查询用缓存扛穿透,秒杀用分布式锁防击穿,关注 Feed 流用 List 和 ZSet 做推拉,点赞排行榜用 ZSet 排序。花两天跑通这份实战源码,你能回答的不再是“Redis 快在哪”,而是“缓存什么时候失效、锁什么时候会被误删、数据对不上时谁背锅”。本文按“设计动机 → 本地跑通 → 核心代码 → 踩坑 → 体检”的顺序,把我实际调这个项目的经验完整写出来。
2. Redis 在点评项目里的真实分工:5 种数据类型和它们的业务落脚点
2.1 从 Redis 命令反推设计动机:基础类型不是背出来的
很多人在学 Redis 时是把五种类型当“数据结构”背的:String 是字符串、List 是列表、Set 是无序集合。但在黑马点评里,每种类型的出现都对应一个业务约束。String 承担验证码和登录令牌,是因为这两个数据“一个 key 只存一个值”,而且都需要 TTL 自动过期;List 承担 Feed 流,是因为关注信息流的读取顺序是“新在前、旧在后”,LPUSH + LRANGE天然满足;ZSet 承担点赞排行榜,是因为业务要求“按点赞时间排序”,而 Set 只能保证去重,无法排序。
这里最反直觉的是 Hash 居然不是主角。很多新手拿到“店铺缓存”第一反应是用 Hash 存字段,但源码里用的是 String 存 JSON。原因很好理解:商户查询是按 id 整体读取,返回给前端时也要整个对象,String 存JSON.toJSONString(shop)一次写入一次读出,几乎没有字段级修改的需求。Hash 适合“热点字段单独更新”的场景,比如评分要频繁 +0.1 而店铺名不变;黑马点评里没有这种操作,所以用 String 更省事。
理解这一点很重要,因为同样的业务换到别的项目里,选型就可能翻转。黑马点评的价值不在于它用了 Redis,而在于每个命令都能对应到“为什么是它”的答案。我建议你拉一份数据类型速查表放在手边,边看源码边对照,效果比先背命令再找项目好得多。
2.2 业务场景到命令的映射表与序列化选型
把黑马点评的 Redis 使用场景拆开,核心映射如下表。这张表建议贴在你 IDE 旁边,排查问题时先定位到类型,再想命令行为。
| 业务模块 | 数据类型 | 核心命令 | 为什么这样选 |
|---|---|---|---|
| 短信验证码 | String | SET、GET、DELETE | 单值存储,TTL 自动过期 |
| 登录令牌 | String | SET、GET、DELETE | 随机 token 对应登录用户 JSON |
| 店铺缓存 | String | SET、GET | 整体读写,序列化为 JSON |
| 点赞去重 | Set | SADD、SISMEMBER、SCARD | 天然去重,判断是否点过赞 |
| 点赞排行榜 | ZSet | ZADD、ZREVRANGE、ZSCORE | 按时间/分数排序,翻页方便 |
| 关注 Feed 流 | List | LPUSH、LRANGE | 新内容在头部,滚动分页取区间 |
序列化选型是这个项目里最容易让新手翻车的地方。源码里如果用默认的RedisTemplate<Object, Object>,key 和 value 都会走 JDK 序列化,存进 Redis 的 key 会变成\xAC\xED\x00\x05t...一串乱码,value 也是二进制,肉眼没法排查。我一般直接改用StringRedisTemplate或给 RedisTemplate 显式指定StringRedisSerializer做 key、GenericJackson2JsonRedisSerializer做 value,这样redis-cli能看到可读的 key,出问题时能用命令直接定位。这个配置在第 5 章会给出完整代码。
另一个序列化细节是 LocalDateTime。Jackson 默认序列化 Java 8 时间类型会抛异常,需要在 ObjectMapper 里注册JavaTimeModule,或者把时间字段转成字符串再塞进 JSON。黑马点评里缓存店铺、缓存登录用户都涉及时间字段,漏掉这步会出现“缓存写入失败”或“反序列化出来时间是 null”的隐蔽问题。后面第 5 章的乱码排查会再次提到这个坑。
3. 从 zip 到能登录:本地跑通黑马点评的完整流程
3.1 解压结构与环境清单:JDK、MySQL、Redis 版本怎么定
拿到“基于 Redis 的黑马点评项目实战源码.zip”,第一步不是急着改代码,而是先确认三个核心环境:JDK、MySQL、Redis。这个项目是 Spring Boot 2.x 技术栈,JDK 1.8 或 11 都能跑,别直接用 JDK 17 硬扛,除非你愿意处理 Spring Boot 2.x 和最新版兼容性的额外麻烦。MySQL 5.7 和 8.0 都行,但要注意 8.0 的驱动配置必须带cj前缀,时区参数也不能省。Redis 版本建议 6.x 以上,因为 6.0 之后 ACL 和更好的内存策略让排查更方便,5.0 也能跑但没必要为难自己。
解压后的目录一般包含后端 Maven 工程、前端静态资源和一份 SQL 脚本或 db 目录。后端是标准的 Spring Boot 工程,依赖里能看到spring-boot-starter-data-redis、mybatis-plus、mysql-connector-java这些关键件。前端通常是 Vue 打包后的 dist 目录配一个 nginx,也有部分教程版会给前端源码。你不需要把前端源码重新npm run build一遍,直接用 dist 静态文件最快。
启动之前先做一个体检:mvn -v、java -version、mysql --version、redis-cli ping四条命令各跑一遍,任何一条报错都先解决再继续。这个项目的报错 80% 出在环境不一致,不是代码问题。提前确认版本能少走一小时弯路。
3.2 MySQL 建库与 Redis 服务:最容易翻车的前置步骤
初始化数据库时,我见过最多的失败是把 SQL 文件用 Navicat 手动执行,结果因为字符集或外键顺序跑到一半中断。我一般直接在终端用命令行导入,干净利落:
mysql -uroot -p --default-character-set=utf8mb4 < init.sql这条命令做的事情是把项目自带的初始化脚本导入本地 MySQL。--default-character-set=utf8mb4必须显式指定,否则中文注释和 emoji 昵称在部分 MySQL 版本下会变成乱码或直接报错。导入完成后,用show tables;检查核心业务表,至少能看到tb_user、tb_shop、tb_blog、tb_voucher、tb_seckill_voucher这几张,缺表说明导入中断,需要 drop 库重新导,不要增量补。
Redis 服务我建议用 Docker 起,干净又可控:
docker run -d --name redis \ -p 6379:6379 \ -v /data/redis:/data \ redis:6.2 \ --appendonly yes-v /data/redis:/data是把容器内的持久化目录挂载到宿主机,--appendonly yes开启 AOF,防止容器重启后缓存数据全部蒸发。本地学习可以不设密码,但 application.yml 里如果把密码写死成 123456,Redis 服务端没密码时启动会报ERR Client sent AUTH, but no password is set,这一点在后面的避坑章会展开。启动完成后,一定先执行一次redis-cli ping,看到PONG再进下一步。
3.3 启动后端与前端:Vue 项目连不上后端的第一步排查
后端的 Redis 连接配置在 application.yml 里,我建议把 host 显式写成127.0.0.1,不要写localhost。原因在第 5 章会细说,这里先记住结论。timeout 也不要留默认值,写短一点方便快速失败:
spring: data: redis: host: 127.0.0.1 port: 6379 timeout: 2000mstimeout 设成 2 秒,意味着连不上 Redis 时后端快速报错,而不是等 60 秒让你以为项目卡死了。这个配置在调试阶段能省下大量等待时间。MySQL 的连接配置同理,注意serverTimezone=Asia/Shanghai必须加,否则驱动会拿 UTC 时区去解析时间字段,导致查询结果差 8 小时。
前端启动方式有两种。教程版一般配好 nginx,直接把 nginx 指到 dist 目录,proxy_pass指到 8080 后端端口。用 nginx 方案时最常见的坑是刷新页面 404,因为 Vue 路由是 history 模式,需要在location /里加try_files $uri $uri/ /index.html;。如果手头只有前端源码,也可以npm run dev起本地开发服务器,并在 vite.config 里配 proxy 把/api代理到 8080。前端连不上后端时,第一步永远不是改前端代码,而是先在浏览器直接访问http://127.0.0.1:8080/api/...看后端通不通。
4. 逐个模块看核心代码:登录、缓存穿透、分布式锁和 Feed 流的落地写法
4.1 短信验证码登录:String 和 TTL 的边界
验证码登录是黑马点评第一个 Redis 场景,代码量不大,但把 String 的过期、覆盖、删除三个行为全用到了。核心逻辑分成“发送验证码”和“校验并登录”两步:
@Service public class UserService { private final StringRedisTemplate redis; public void sendCode(String phone) { // 6位随机验证码,\u8fd9\u91cc\u7528 Math.random() \u53ea\u4e3a\u793a\u610f\uff0c\u751f\u4ea7\u73af\u5883\u5efa\u8bae\u7528 SecureRandom String code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000)); String key = "login:code:" + phone; // 有效期5分钟,重复发送会覆盖旧验证码 redis.opsForValue().set(key, code, 5, TimeUnit.MINUTES); } public String login(String phone, String inputCode) { String key = "login:code:" + phone; String right = redis.opsForValue().get(key); if (right == null || !right.equals(inputCode)) { throw new BizException("验证码错误或已过期"); } // 一次性凭证,校验完立刻删除 redis.delete(key); String token = UUID.randomUUID().toString().replace("-", ""); String userJson = JSON.toJSONString(getUserByPhone(phone)); // 登录状态保持30分钟,无操作自动失效 redis.opsForValue().set("login:token:" + token, userJson, 30, TimeUnit.MINUTES); return token; } }这段代码有两个关键设计。第一,key 都用业务前缀加冒号,login:code:和login:token:区分两类数据,也方便redis-cli keys 'login:*'一条命令看全部登录相关缓存。第二,验证码校验成功后先delete再创建 token,避免同一个验证码被重放攻击。顺序上有讲究——如果先发 token 再删验证码,极端情况下二次请求还能用旧验证码再换一个 token,等于多了一个未注销的会话。TTL 是这套逻辑的兜底:验证码 5 分钟过期,token 30 分钟过期,都不需要额外的定时任务清理,Redis 自己处理。
从参数角度看,验证码有效期 5 分钟对大多数短信通道的到达时间都够,太短会导致用户还没收到就过期,太长则增加爆破面。token 有效期 30 分钟适合“短期免登录”,如果这个项目要改成长会话,可以把 TTL 拉到 7 天,并配合redisTemplate.expire()做滑动续期——每次请求接口时重置一次过期时间。但在黑马点评的教学场景里,固定 TTL 更直观。
4.2 店铺缓存与逻辑过期:缓存穿透之后谁来兜底
店铺查询是黑马点评里 Redis 缓存的核心场景,课程里给的是“逻辑过期”方案,不是简单的先查缓存再查库。逻辑过期的思路是:缓存里不只存业务 JSON,还带一个过期时间字段,查询时先比较这个时间。没过期直接返回,过期了也不立刻去查库,而是先返回旧数据,同时让一个线程去后台重建缓存。
// 写入缓存的包装结构 class RedisData { LocalDateTime expireTime; Object data; } public Shop queryShop(Long id) { String key = "cache:shop:" + id; String json = redis.opsForValue().get(key); if (StrUtil.isBlank(json)) { // 双检:可能另一个线程刚重建完,这里回源DB一次 return queryFromDbAndCache(id); } RedisData redisData = JSON.parseObject(json, RedisData.class); if (redisData.getExpireTime().isAfter(LocalDateTime.now())) { // 未过期,直接返回 return (Shop) redisData.getData(); } // 逻辑过期:尝试拿锁,拿不到就返回旧数据(保证可用) boolean locked = tryLock("lock:shop:" + id); if (locked) { try { Shop shop = queryFromDbAndCache(id); // 重建缓存 return shop; } finally { unlock("lock:shop:" + id); } } return (Shop) redisData.getData(); // 拿不到锁,先返回旧值 }逻辑过期方案和“缓存空值”方案的区别是本质性的。缓存空值解决的是缓存穿透——DB 里没有这个 id,Redis 里也查不到,请求打到 DB 上,所以把“查不到”的结果也缓存成空对象,给一个 5 分钟 TTL。逻辑过期解决的是缓存击穿——热点 key 过期的一瞬间并发请求全部打到 DB,所以让缓存永远有值,值里带时间戳,过期后由一个线程去重建。
参数上,逻辑过期时间我建议设 30 分钟,重建线程执行时间要远小于这个值。如果重建一次要 2 秒,锁的 TTL 必须大于 2 秒,否则锁提前释放会放进来第二个重建线程,逻辑过期方案就退化成“多次击穿”。重建失败时的兜底是保留旧缓存继续返回,所以这个方案牺牲了一点数据新鲜度,换来了极端情况下的可用性,对店铺信息这种“晚几分钟更新无感知”的业务是划算的。
4.3 分布式锁:Setnx 加锁和 Lua 解锁的完整写法
黑马点评里分布式锁出现在两个地方:逻辑过期重建缓存,以及秒杀扣减库存。这里的锁不用 Redisson 的RLock,而是基于 Redis 的SETNX手写,教程意图是让你理解锁的本质。但手写锁最容易翻车的是不是加锁,而是解锁——直接DEL key会把别的线程的锁删掉。正确写法是加锁时带一个唯一 value,解锁时先用 Lua 脚本比对再删:
public boolean tryLock(String key, String value, long timeout, TimeUnit unit) { // 关键:setIfAbsent + 过期时间,原子操作 return redis.opsForValue().setIfAbsent(key, value, timeout, unit); } public void unlock(String key, String value) { // Lua脚本保证“比对value + 删除”是原子操作 String script = "if redis.call('get', KEYS[1]) == ARGV[1] " + "then return redis.call('del', KEYS[1]) " + "else return 0 end"; redis.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(key), value); }setIfAbsent(key, value, timeout, unit)是原子的,等价于SET key value NX EX,不存在“先 setnx 再 expire 两步走导致中途宕机锁永不过期”的风险。value 必须传一个每次加锁都不同的值,我一般用UUID.randomUUID(),这样 unlock 时才能确认“这把锁是我加的”。如果不比对 value 直接 DEL,线程 A 执行超时锁过期,线程 B 加锁成功,A 执行完一个 DEL 把 B 的锁删了,B 的临界区就失去保护,库存就会超卖。
锁的 TTL 参数是这门手写锁的艺术。设 10 秒听起来合理,但如果业务执行要 15 秒,锁 10 秒后自动过期,第二个线程进来了,等于锁白加。我一般按“预估执行耗时的 3 倍”做最小可用值,配合监控调优。真正生产环境还是建议上 Redisson 这种带看门狗续期的实现,但黑马点评存在的意义就是让你先理解超时时间为什么是个问题。参数配置上,锁 key 也要带业务维度,lock:shop:{id}和lock:stock:{id}要分开,否则“抢店铺锁”和“抢库存锁”会互相阻塞。
4.4 Feed 流与点赞排行榜:List 当消息队列、ZSet 当计数器的参数细节
Feed 流是黑马点评里最具“工程味”的部分。核心业务是用户发布博客后,把动态推送给粉丝。实现上有两种流派:推模式和拉模式。推模式是发布时直接往每个粉丝的收件箱 List 里写一条内容,读的时候LRANGE取一页;拉模式是读的时候去查所有关注对象的动态再聚合,Redis 主要用来加速。黑马点评用的是推模式,因为关注关系是有限集合,写扩散可以接受,读的时候延迟最低:
// 发布博客后推送 public void pushFeed(Long blogId, Long authorId) { List<Long> fans = getFans(authorId); // 查询粉丝id列表 for (Long fanId : fans) { String key = "feed:" + fanId; // LEFT_PUSH: 新内容永远在列表头部 redis.opsForList().leftPush(key, String.valueOf(blogId)); } } // 读者拉取一页Feed public List<Long> queryFeed(Long userId, int offset) { String key = "feed:" + userId; // 取从offset开始的5条,滚动分页不用游标 List<String> ids = redis.opsForList().range(key, offset, offset + 4); return ids.stream().map(Long::valueOf).collect(Collectors.toList()); }这里有几个容易被忽略的参数决策。leftPush的方向决定了新内容在列表左端,range 0 4拿的是最新的 5 条;如果误用rightPush,列表变成先进先出,新内容在右端,分页逻辑全部要反过来。成员统一用String.valueOf(blogId),而不是直接放 Long,是为了避免 RedisTemplate 的序列化器把 Long 和 String 当成两种 member,导致同一个 id 出现两条数据。滚动分页的 offset 由前端传,每页 5 条是我调试时比较舒服的默认值,压测后可以按业务Hero调成 10 或 20。
点赞排行榜用 ZSet 是另一个亮点。Set 能去重但无法排序,ZSet 在去重基础上给每个成员一个 score,业务上把“点赞时间”当 score,就能实现“按点赞先后排序”的榜单:
// 点赞/取消点赞 public void likeBlog(Long blogId, Long userId) { String key = "blog:liked:" + blogId; Double score = redis.opsForZSet().score(key, userId.toString()); if (score == null) { // 未点赞:score存当前时间戳,单位毫秒 redis.opsForZSet().add(key, userId.toString(), System.currentTimeMillis()); } else { // 已点赞:取消 redis.opsForZSet().remove(key, userId.toString()); } }score 用时间戳是为了两点:一是能排序,二是能知道“什么时候点的赞”。如果你把 score 直接存 1 表示“点过赞”,那就退化成了 Set,无法回答“谁点赞最早”的问题。这里还有一个精度细节——System.currentTimeMillis()是 long 类型,转成 double 后在毫秒量级完全不会有精度损失,放心用。只有当时间戳大到超过 2^53 才可能误差,那得等几万年。
5. 避坑指南:黑马点评跑不动或数据不对,先查这 5 个地方
5.1 现象:启动后接口卡死,报 RedisCommandTimeoutException
日志里出现Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException时,第一反应不要改代码,先看连接地址。这个报错在本地最常见的诱因是 application.yml 里写了localhost,而本机 Redis 监听的是 IPv4 的127.0.0.1。Java 解析 localhost 时可能优先解析到 IPv6 的::1,Lettuce 拿着 IPv6 地址去连 IPv4 的 Redis,Socket 层不会立刻拒绝,而是等到 timeout 才报错。解决方式是把 host 显式改成127.0.0.1,或在 Redis 配置里把bind从127.0.0.1改成0.0.0.0。还有一个加速排查的技巧:把 timeout 从默认值调成2000ms,让问题“快速失败”,别等 60 秒。
5.2 现象:登录后 Redis 里全是\xAC\xED开头的乱码 key
用 Redis Desktop Manager 或redis-cli keys '*'看到 key 是\xAC\xED\x00\x05t...login:token:xxx这种不可读字符串,原因是 RedisTemplate 默认用的 JdkSerializationRedisSerializer。这个序列化器会把 key 也做 Java 对象序列化,于是每个 key 前面都带了一长串二进制头。表象是“看着乱”,实际危害是keys login:*这种通配符匹配失效,用习惯了很难 Debug。解决方式是在配置类里显式替换序列化器,key 和 hashKey 用 StringRedisSerializer,value 用 GenericJackson2JsonRedisSerializer。替换后切记重启服务并清空旧缓存,否则新代码读旧二进制数据会反序列化失败。
5.3 现象:凌晨零点一过,DB 压力飙升,接口突然变慢
一个典型的缓存雪崩场景:店铺缓存统一设置 12 小时过期,23:59 写入,11:59 到期,第二天中午 12 点整所有热点 key 同时失效,并发请求全部穿透到 MySQL,数据库 CPU 瞬间打满。这不是玄学,是固定过期时间必然导致的结果。解决方式有三个层次:最常做的是“过期时间加随机值”,基础 TTL 设 30 分钟,再叠加一个 60 到 120 秒的随机数,让 key 的失效时间均匀分散;第二步是第 4 章讲的逻辑过期方案,缓存永远有值;第三步才是考虑 Redis 集群预热,本地学习做到前两层就够了。排查时先看监控里 MySQL QPS 是否有规律的尖峰,再抽查 Redis 中 key 的 TTL 是否大量相同。
5.4 现象:加了分布式锁,库存还是出现超卖
锁没起作用的常见原因不是加锁代码写错,而是解锁逻辑把别人的锁删了。线程 A 加锁后因为 GC 或网络抖动执行超过锁 TTL,锁自动过期,线程 B 加锁成功;A 执行完后直接delete key,把 B 的锁释放,B 和 C 同时进入临界区,超卖就发生了。解决方案就是 4.3 节里那段 Lua 脚本——解锁前先比对 value 是否等于自己加的 UUID,相等才删除。注意这里必须用 Lua 脚本保证“比对 + 删除”两步的原子性,不能用先get再del的 Java 代码,因为两条命令之间可能有另一个线程介入。排查时在日志里打印每次加锁和解锁的 value,定位到删错锁的线程,去看它的执行耗时是否超 TTL。
5.5 现象:Feed 流刷不出新内容,或点赞列表里同一个人出现两次
ZSet 和 List 的数据对不上,十有八九是 member 类型不一致导致的。比如点赞时存的是user.getId()返回的 Long,而查询榜单时转成了 String,Redis 会认为这是两个不同的 member,同一个用户就出现了两次。解决方式是统一成员类型,进 Redis 之前全部String.valueOf转一道。还有 Feed 流刷不出新内容,常发生在分页 offset 计算错误。如果用range(key, offset, offset + 4)做滚动分页,前端需要每次把上次返回的最后一条记录的下标传回来,而不是传“第几页”,因为 List 左端不断有新数据插入,按页码取会出现数据重复或跳过。排查思路是先LRANGE key 0 -1看整体数据对不对,再验证前端传参逻辑。
6. 从跑通到能扛住:用排查命令和压测给项目做体检
6.1 上线前必跑:慢查询日志和 bigkey 扫描两条命令
跑通黑马点评只是起点,项目要“能用”还得做一轮 Redis 体检。我一般先开慢查询日志,统计 Redis 执行超过 10 毫秒的命令:
redis-cli CONFIG SET slowlog-log-slower-than 10000 redis-cli SLOWLOG GET 1010000是微秒,等于 10 毫秒。对于本地缓存来说,超过 10 毫秒的命令就值得警惕,一般是KEYS、大LRANGE或复杂ZREVRANGE。SLOWLOG 只看最近 N 条,适合在压测后抓典型。另一条是 bigkey 扫描:
redis-cli --bigkeys输出会列出每个类型里最大的 key。黑马点评里常见的 bigkey 是 Feed 流 List,粉丝多的大 V 发一条动态,feed:{userId}列表会被推到每个粉丝的收件箱,单个 list 可能膨胀到几十万条。如果发现某类 key 过大,要么限制 feed 长度,要么改成 ZSet 分页,而不是靠 Redis 硬扛。
6.2 用 JMeter 做一轮压测:怎么判断缓存真的生效了
压测对象选“查询店铺详情”最合适,因为它是纯读缓存场景,最容易看出效果。JMeter 里建一个线程组,50 个线程、Ramp-Up 5 秒、循环 10 次,请求GET /shop/{id},id 固定用同一个热点值,这样能压出缓存击穿时的真实表现。跑完后看聚合报告的 Avg 和 TP99,再对比压测前后 Redis 的命中率:
redis-cli INFO stats | grep keyspace_hits判断标准不是“平均响应时间从 50ms 降到 5ms”,而是看keyspace_hits和keyspace_misses的比例。如果 miss 率超过 20%,说明缓存设计有大量穿透,先查热点 key 的过期时间分布。我在调这个项目时养成的习惯是:每次改完缓存策略,先用SLOWLOG RESET清空慢查询,再压一遍,最后对比三组数据——TP99、miss 率、DB 连接数。三组都良性,才敢说这个方案能扛住。这里的每一步都可以直接用命令复现,不需要额外平台,这也是 Redis 项目最好的一点——所有问题都能在redis-cli里找到答案。希望帮到你。
本文还有配套的精品资源,点击获取