1. Redis在Java工程里的定位为什么实习生必须先过这一关带过几届实习生之后我发现一个规律简历上写着“熟悉Redis”的人不少但真到写工程代码的时候能把缓存三大问题讲清楚、能说出为什么线上不能用KEYS命令的人一只手数得过来。Redis在Java后端项目里的地位很像工具箱里那把活动扳手——你未必每天都要拿它炫技但几乎每个项目都离不开它。这篇文章不打算背面试八股我会从工程落地的角度把Java实习生真正需要掌握的Redis核心知识点拆开讲透顺便把那些课程里不会教、但入职第二天就会碰到的坑一并说清楚。先聊定位。Redis在Java后端里承担的职能我总结下来主要是四个方向。第一是缓存这是绝对主力所有读多写少、对实时性要求高的数据都可以往里面放第二是分布式场景下的共享状态比如分布式锁、计数器、榜单、布隆过滤器这些跨实例共享的数据只有放在Redis里才靠谱第三是短生命周期的临时数据验证码、登录会话、接口限流计数都在这一类第四是轻量级的消息通道比如发布订阅或者基于List实现的简单延迟队列。搞清楚这四个方向之后学习路线就清楚了。实习生最容易犯的错误是上来就钻研RDB和AOF的持久化细节折腾AOF重写策略研究主从复制的底层日志。这些内容有价值但工程里90%的日常需求根本用不上。真正每天要用的是键怎么命名、数据用什么类型存、过期时间怎么设、缓存和数据库不一致了怎么处理、连接超时了怎么排查。把时间花在这上面产出比高得多。事实就是这么残酷你的同事不会因为你背出了跳跃表的时间复杂度而认可你但会因为你用一个ZSet解决了实时排行榜、因为你上线前主动想着给缓存加一个空值兜底而信任你。这篇文章接下来就按工程顺序展开逐个拆解数据类型选型、客户端与序列化、缓存治理、分布式锁、故障排查这几个硬主题最后给实习生几条能直接照做的习惯建议。2. 数据类型能背八股更要选对场景2.1 String最常用也最容易埋坑String是使用率最高的Redis类型缓存JSON、计数、验证码、分布式锁全是它。很多资料把String讲得太简单了好像谁都会但实际上工程里至少有四个细节经常被忽略。第一个细节是计数必须用INCR而不是先GET再SET。比如文章阅读量如果代码写成“先查出来、加一、再写回去”两个线程并发操作就会互相覆盖计数必然丢。INCR是原子的直接返回递增后的结果性能和正确性一起保证Long viewCount stringRedisTemplate.opsForValue().increment(article:view: articleId);第二个细节是设值和过期时间要一次完成。很多老教程喜欢先set一个键再单独调用expire。这两步之间只要应用重启或者网络抖动就会出现一个永远不过期的“孤儿键”慢慢把内存撑爆。正确写法是set的时候直接带上过期时间stringRedisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);第三个细节是value别太大。有人喜欢把一个对象的所有字段拼成一个超长字符串塞进Redis结果业务只需要其中一个字段时也得把整个大字符串拉回来反序列化。这个习惯会在流量上来之后变成大Key隐患后面排查章节我会专门展开。第四个细节是分布式锁场景的SETNX这是String在并发控制上的经典用法。用法很简单但正确姿势有点讲究包括value要带上唯一标识、必须给锁设置过期时间、释放时要校验持有者。这部分我会在第五章单独讲。2.2 Hash对象的天然映射别拿String硬扛Hash类型在Java工程师手里最直观的用法就是存对象。user:id:1001这个键下面的field可以是name、age、email每一项都能单独读取和更新。这个特性在“只改一个字段”的场景里非常有用。举个例子用户资料里有个“最近登录时间”如果整个用户数据用StringJSON存每次登录都要先get完整字符串反序列化改一个字段再序列化写回。并发高一点还会出现丢失更新。换成Hash之后只需要一条HSet命令改对应字段stringRedisTemplate.opsForHash().put(user:info: userId, lastLoginTime, String.valueOf(System.currentTimeMillis()));Hash的第二个价值是内存效率。Redis对field数量少、value小的Hash做了特殊编码比如ziplist或listpack内存占用远小于同等数据的String结构。我自己在项目里做过实测存一万个用户对象Hash方案比StringJSON方案能省下40%以上的内存。这个结论在不同版本里略有差异但方向是确定的。第三个价值是支持批量操作。比如需要给一批商品补库存HMSet、HMGet一次搞定网络IO省一大截。注意一点Spring Data Redis里Hash相关的API走的是opsForHash()操作的是Map结构用起来比Jedis原生API直观得多。2.3 List、Set、ZSet三个容易混淆的兄弟List类型最典型的工程场景是简单的消息队列和“最新消息”列表。比如一个公告列表用LPUSH往左边推用LRANGE取前20条天然就是按时间倒序。Redis的List操作是O(1)复杂度的头尾操作很快。网上很多教程会把List当成正统消息队列讲我要提醒一句List队列不支持消费确认、不支持重复消费去重生产环境真要保证消息可靠投递还是得上专门的队列中间件。List只能解决“临时缓冲”和“轻量异步”这两类问题。Set类型的核心特性是去重和集合运算。点赞数、收藏数、在线用户ID集合这些都是Set的舒适区。更值钱的是SINTER、SUNION、SDIFF这三个集合运算命令可以做共同关注、好友推荐、标签交集这些逻辑。以前有个项目用数据库SQL算“两个用户共同关注了哪些人”数据量一大就慢换成Set的SINTER以后响应时间从几百毫秒降到个位数毫秒。ZSet则是带权重的有序集合每个成员关联一个scoreRedis按score排序。排行榜、延时队列、滑动窗口限流都是它的主场。比如直播间的礼物榜每次有人送礼就执行一次ZINCRBYstringRedisTemplate.opsForZSet().incrementScore(live:rank: roomId, userId, giftValue);取Top10只需要一条ZREVRANGE命令。ZSet的工程价值在于“排序”这个动作被下推到了Redis里Java这边不需要在内存里做任何排序运算。2.4 数据类型选型速查表我把几种类型的选型依据整理成一张表实习生可以直接拿来当参考类型底层结构典型场景推荐理由慎用场景StringSDS动态字符串缓存JSON、计数、验证码、锁通用性强、命令简单value过大、需要频繁改单字段Hash哈希表/压缩编码对象存储、属性修改单字段更新、省内存field过多且value过大的对象List双向链表最新列表、简单队列头尾操作O(1)要求可靠投递的正式队列Set哈希集合去重、标签、关注关系集合运算强大需要排序的场景ZSet跳跃表哈希表排行榜、延时队列天然有序、可按权重取范围不需要排序时别用浪费内存选型的时候记住一句话能用Hash存对象就别用String拼JSON能明确去重用Set就别用List手动去重需要排序就用ZSet而不是取出后在Java里排序。这条原则能帮你避开九成的不合理设计。3. 客户端与序列化连不上、看到乱码九成是这里的坑3.1 Jedis、Lettuce、Redisson到底怎么选很多Java新人第一次写Redis代码用的是Jedis因为当年的教程都是它。但如果你用的是Spring Boot 2.x以上版本默认的Redis客户端其实是Lettuce不是Jedis。新手最容易困惑的就是这一点为什么我引入spring-boot-starter-data-redis之后没有手动创建任何客户端就能直接用StringRedisTemplate那是因为Spring Boot自动配置帮你搞定了。Lettuce是默认选择它基于Netty支持异步和响应式编程连接是共享的性能比Jedis那套“一个连接一个实例”的方式要好。Jedis的优势是API直白但线程不安全每个线程要拿独立的连接实例在高并发下容易成为瓶颈。Redisson则是另一个维度的东西。它不是一个单纯的连接客户端而是基于Redis封装的高级库提供了分布式锁、分布式集合、延迟队列这些开箱即用的Java对象。如果你要用Redis做分布式锁我非常建议直接用Redisson的RLock而不是自己用SETNX封装。第五章我会详细对比。三者的选择建议很简单常规Spring Boot工程直接用默认Lettuce就好不要瞎折腾分布式锁这类高级场景加一个Redisson依赖各管各的只有你手头有老项目还在用Jedis时才需要学它的连接池配置。3.2 连接池参数为什么不能照抄网上的实习生态度都很认真喜欢在网上找一段连接池配置就粘进项目。这个习惯很危险因为连接池参数跟你的Redis负载、网络环境、业务并发量强相关抄来的配置大概率不合适。Lettuce本身是共享连接模型默认情况下并不是每个线程都建立独立连接所以连接池的意义和Jedis不太一样。如果你配置了Lettuce连接池重点要看几个参数maxTotal最大连接数、maxIdle最大空闲连接、minIdle最小空闲连接、maxWaitMillis获取连接的最大等待时间。我见过最典型的事故是这样本机压测时设置maxTotal为50上了生产之后业务峰值时线程数远超过50所有线程都在排队等连接接口超时率直接飙升而Redis本身的CPU和内存都还闲着。实际操作里我一般建议把maxTotal设置成业务峰值并发线程数的1.5到2倍minIdle设置成能应对日常波动的一半左右maxWaitMillis不要给太长500毫秒以内比较合理。设置完之后一定要做一次简单的压测观察连接数的曲线再微调数字。连接池不是越大越好每个连接都占着文件描述符和内存太大了反而拖垮应用。3.3 序列化方式决定了你会不会看到乱码这个坑几乎每个刚接触Spring Data Redis的实习生都会踩。你用RedisTemplate存了一个对象然后在Redis Desktop Manager里面打开一看key前面多了一串“\xAC\xED\x00\x05t...”value更是乱码一堆。为什么会这样因为RedisTemplate默认使用的是JdkSerializationRedisSerializer它把Java对象序列化成二进制字节流其中包含了Java序列化的头信息显示出来就是乱码。这不是Redis坏了而是序列化方式的选择问题。解决思路有两条。第一条是用StringRedisTemplate。它默认使用String序列化器key和value都是纯字符串适合存JSON、计数这些场景。代码里先手动把对象转成JSON字符串再存读取时再反序列化回来这是最稳妥、最好排查问题的方案。我现在大部分项目都是这个套路。第二条是如果你确实想让RedisTemplate直接操作对象那就需要显式配置RedisSerializer。比如把key设置为StringRedisSerializer把value设置为Jackson2JsonRedisSerializer这样存进去的是JSON格式可读性好跨语言也能解析。要注意的是Jackson序列化对象时默认会在JSON里带上类信息或者受默认类型处理影响产生一些意料之外的字段建议配置ObjectMapper时把activateDefaultTyping和日期格式都显式设置好。序列化这块我给你的最终建议项目里尽量统一用StringRedisTemplate加JSON字符串别让团队成员一会儿用JDK序列化、一会儿用Jackson。序列化方式不统一轻则缓存里出现乱码重则版本升级后反序列化失败线上直接报错。这是个约定问题得从代码规范层面管起来。4. 缓存治理穿透、击穿、雪崩不能只背概念4.1 三个问题的本质区别面试的时候实习生能把“穿透、击穿、雪崩”三个词背出来但一问到“你项目里怎么防的”就愣住了。这三个问题名字像成因和方法完全不同必须分开理解。缓存穿透指的是查询一个根本不存在的数据。比如按ID查用户ID99999这个用户压根不存在缓存里不会有数据库里也不会有。如果攻击者循环请求各种不存在的ID每个请求都直接打到数据库数据库压力就会被无限放大。这个问题的核心是“无效请求绕过缓存”。缓存击穿指的是一个热点key在过期的一瞬间大量并发请求同时打到数据库。比如某个爆款商品的详情缓存30分钟过期过期那一刻正好有几千个请求涌进来它们都发现缓存没有于是都去查数据库。这个问题的核心是“单个热点key失效”。缓存雪崩指的是大量key在同一时间段集中过期或者Redis节点出现故障导致一大批请求直接打到数据库。这个问题的核心是“大规模缓存同时失效”破坏力比前两个更大。三种问题的应对思路截然不同穿透要拦截无效请求击穿要让热点key的重建过程互斥雪崩要把过期时间错开、并且做好降级。下面我把具体做法写出来。4.2 从代码层面把兜底做好防缓存穿透最朴素也最有效的办法是缓存空值。查询结果为空时也往Redis里写入一个空对象设置一个较短的过期时间比如3到5分钟。这样下次同样的无效请求直接命中缓存的空值不会打到数据库。注意空值的过期时间要比正常数据短否则万一数据被创建了用户反而查不到。public User getUserById(Long id) { String key user:info: id; String json stringRedisTemplate.opsForValue().get(key); if (json ! null) { return JSONUtil.toBean(json, User.class); } User user userMapper.selectById(id); if (user ! null) { stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(user), 30, TimeUnit.MINUTES); } else { stringRedisTemplate.opsForValue().set(key, , 3, TimeUnit.MINUTES); } return user; }比空值缓存更强的是布隆过滤器。它可以在请求进来之前就判断这个ID大概率不存在直接把请求挡在Redis和数据库外面。但布隆过滤器有误判率而且实现起来要额外维护一个数据结构一般用在ID空间巨大且固定、无效请求占比很高的场景比如短链系统、风控系统。实习阶段能写清楚空值缓存就够了布隆过滤器作为加分项了解即可。防缓存击穿常用的手段是互斥重建。在缓存失效后让第一个请求去重建缓存其他请求先等一会儿或者降级处理。实现上可以用分布式锁也可以用单机JVM锁加双重检查。对于热点数据还有一种更高级的思路逻辑过期。也就是缓存里先存一个“逻辑过期时间”如果读出来发现过了这个时间就返回旧值同时异步发起一个线程去刷新缓存。这个方案的好处是永远不会让请求堵在数据库前面但实现稍微复杂适合对数据新鲜度要求不高的场景。防缓存雪崩第一招是过期时间加随机值。比如原来是固定的30分钟现在改成“30分钟加一个0到300秒的随机数”让大量key的过期时间自然错开。这不是什么高深技术但效果立竿见影。第二招是热点数据不过期后台任务主动更新这是一种“物理不失效”的思路。第三招是多级缓存本地Caffeine加RedisRedis挂了还有本地缓存兜底虽然一致性更难保证但至少系统不会雪崩。最后一招是服务降级Redis不可用时直接返回默认值或旧数据而不是无限等待超时。4.3 缓存与数据库的一致性“先更新数据库还是先删缓存”这个问题几乎每个实习生入职后都会被问到。正确的主流方案是Cache Aside模式读的时候先读缓存没有就查数据库并回填写的时候先更新数据库然后删除缓存。为什么是删缓存而不是更新缓存因为更新缓存需要额外的计算成本而且并发下容易造成数据不一致。举个例子。两个线程同时更新同一个用户线程A把数据库改成姓名“张三”线程B改成姓名“李四”。如果更新数据库之后直接更新缓存可能出现数据库最终是“李四”而缓存先被线程A写入“张三”、后被线程B写入“李四”碰巧一致但如果顺序反过来数据库是“张三”缓存却被线程B覆盖成了“李四”就出问题了。删缓存则不一样下一次读取时缓存为空直接查数据库回填天然拿到的就是最新值。当然删缓存也不是绝对安全。极端情况下“线程A更新数据库→线程B读取未命中→线程B查询数据库旧值→线程A删除缓存→线程B回填旧值”还是会造成脏数据。要彻底解决需要引入binlog订阅、版本号、延迟双删这些手段。但工程上要明白一个道理一致性保障是有成本分层的。对于大多数业务Cache Aside模式加过期时间兜底已经足够你付出的代价只是偶尔一次缓存未命中。没必要为了极端一致性把系统设计得极度复杂先保证“最终一致”再用过期时间兜住下限。5. 分布式锁从SETNX到Redisson的进化5.1 什么时候才需要分布式锁很多同学在公司写代码遇到并发问题第一反应是加synchronized。这在单机应用里没问题JVM的锁只在当前进程内有效。但现在的Java后端基本都是多实例部署请求通过负载均衡分发到不同的机器上你在机器A加的锁机器B完全感知不到。这种时候就需要一个所有实例都能访问的“公共锁”Redis就是最常用的载体。分布式锁的典型场景有这些定时任务在多个实例上同时触发要保证同一时刻只有一个实例在执行秒杀扣减库存要防止超卖订单支付回调里对同一笔订单的重复处理要保证幂等。这些场景共同的特点是“跨进程、共享资源、需要互斥”。注意如果你的业务只是单实例部署用Redis分布式锁就是自找麻烦引入的是无谓的复杂度和新的故障点。5.2 手写SETNX锁的正确姿势用Redis实现分布式锁最原始的方式是SETNX加过期时间。为什么需要过期时间因为如果拿到锁的程序在释放锁之前崩溃了没有过期时间的话这把锁就永远解不开了其他线程全部被卡死。所以必须加过期时间作为兜底。正确姿势是这样一个命令SET lock_key lock_value NX PX 30000。NX表示只有当key不存在时才设置PX表示过期时间。这保证了原子性避免了“先SETNX再EXPIRE”两步操作中间崩溃导致锁永不释放的问题。在Spring Data Redis里对应的是Boolean locked stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);这个requestId是什么是当前请求的唯一标识比如UUID。为什么要它因为释放锁的时候要校验“这把锁是不是我自己的”。如果A线程拿到锁执行时间超过了过期时间锁自动释放了这时候B线程拿到了锁开始执行A线程执行完执行DEL命令把B的锁给删了——这就出大问题了。用requestId标识持有者释放锁时先比较再删除可以避免误删。if (requestId.equals(stringRedisTemplate.opsForValue().get(lockKey))) { stringRedisTemplate.delete(lockKey); }但这里还有一个隐藏问题比较和删除这两步不是原子的还是存在时间窗。严谨的做法是用Lua脚本把“比较删除”原子化if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end看到这里你应该明白了手写分布式锁的细节非常多几乎每个环节都是坑。这就是为什么我强烈建议生产环境直接用Redisson。5.3 Redisson帮你省掉一半的心Redisson官方封装了分布式锁用法极其简单RLock lock redissonClient.getLock(order:lock: orderId); boolean locked false; try { locked lock.tryLock(3, 30, TimeUnit.SECONDS); if (locked) { // 执行业务逻辑 } } finally { if (locked) { lock.unlock(); } }你不用担心锁的过期时间设置得太短导致业务没执行完锁就释放因为Redisson有一个看门狗机制。默认情况下如果锁的租约时间没有显式指定Redisson会每隔一段时间自动给锁续期直到业务执行完主动释放。这个机制帮程序员省掉了“业务执行时间超过锁过期时间”这个最头疼的问题。Redisson做的还不止这些。它还处理了可重入问题同一个线程可以多次加锁而不会死锁锁释放失败有兜底锁等待支持可配置超时。相比你自己手写SETNX那一堆代码Redisson的工程健壮性明显高一档。5.4 那些你迟早会踩的坑第一个坑是在finally里释放锁时没有判断当前线程是否持有锁。如果用Redisson直接unlock()会报IllegalMonitorStateException尤其是 tryLock 超时没拿到锁的时候。所以要先判断locked变量再决定是否解锁。第二个坑是锁的粒度。有人图省事把整个方法体用一把大锁包起来锁住所有订单结果并发全部串行化性能暴跌。锁的粒度应该尽可能小比如按订单ID加锁而不是按用户加锁更不是全局锁。第三个坑是数据库事务和锁的交互。如果业务方法上有Transactional注解Spring的事务会在方法返回后提交但如果你在方法内部先释放了Redis锁再提交数据库事务就可能出现“锁已经释放但数据库还没提交”的窗口。其他线程拿到锁后读到的是旧数据。解决办法是让锁的作用范围包住整个事务提交过程比如把锁放到事务方法的外层去调用。第四个坑要提一下分布式锁方案的局限。Redis主从架构下如果Master节点宕机锁的数据还没有同步到从节点就可能出现锁丢失。Redis作者推荐的RedLock方案理论上可以解决但它本身也有争议而且实现复杂。对于大多数Java业务系统来说锁丢失的概率远低于锁设计不当带来的危害所以我的建议是优先保证锁的粒度合理、释放逻辑正确没有必要去追求极致的可靠性。6. 现场排查command timed out、大Key、慢日志6.1 “Redis command timed out”到底在说什么这应该是Java后端生产环境里Redis最常见的报错之一。完整报错通常是“Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException”看到这个错误很多实习生第一反应是“Redis挂了”。实际上Redis大概率没挂问题出在命令执行超时。排查思路要按层次来。第一步看Redis本身的状态用redis-cli执行INFO命令看connected_clients、used_memory、ops_per_sec这几项。如果连接数奇高可能是应用侧连接池配置过大或者连接泄漏如果内存很高可能要检查大Key和淘汰策略如果ops很高可能是业务把Redis当数据库用了做了太多无谓的读写。第二步查慢命令。Redis虽然快但某些命令天生就是性能杀手最典型的是KEYS命令。KEYS会扫描整个键空间数据量大的时候直接阻塞Redis事件循环其他所有命令全部排队表现就是客户端这边统一超时。用SCAN替代KEYS是基本操作。另外SMEMBERS取整个大Set、LRANGE取大List的全量数据、HGETALL取大Hash的全量数据都可能造成阻塞排查时重点看这些命令是否出现在慢日志里。第三步查网络和客户端配置。Lettuce的commandTimeout默认是60秒如果你在配置文件里把它改成了几百毫秒而网络或者GC抖动一下就会频繁超时。这里要平衡超时时间太短会误伤太长会拖住线程。还有一点应用服务器和Redis服务器之间如果跨机房、有防火墙限流也会出现间歇性超时这个要从监控图上才能看出来。我自己的经验是这类问题排到后来大部分都归结到三类原因有大Key命令阻塞、连接池耗尽、或者GC停顿导致客户端请求超时。每次排查都从这三条路出发基本不会跑偏。6.2 大Key和热Key这两个隐形杀手单个键的value特别大比如一个String存了几MB数据或者一个Hash里有几十万个field就叫大Key。大Key的危害是读和写都慢因为数据量大删除也慢比如DEL一个几MB的String会阻塞Redis一小段时间网络传输也慢每次请求都要搬一个大块头。发现大Key的方式是用redis-cli的--bigkeys参数扫描或者用SCAN配合类型分别统计。处理手段无非几种对大String拆分成多个小key用hash结构分片存储对大Hash、大ZSet做数据裁剪保留最近N条如果必须保留全量考虑迁移到其他存储因为Redis不适合存大对象。热Key则是某些key被超高频率访问比如一个爆款商品的缓存每秒被请求几千次。热Key会让单个Redis实例的CPU升高如果用了集群还会造成节点间负载不均。处理热Key的常见方案一是加本地缓存把热点数据缓存在应用进程内二是利用Redis集群的多副本读取或者读取从节点三是对key做后缀打散比如把同一个热key拆成多个带随机后缀的副本key读的时候随机取一个。对大Key和热Key的治理应该是日常巡检的一部分而不是等出事了再补。监控里应该能看到单key的访问频率和大小数据量变化趋势也能反映在实例指标上。实习生在项目里可以主动观察这些指标发现异常及时提出来这就是工程敏感度的体现。6.3 慢日志、日志和可视化管理工具Redis自带的慢日志配置很好用执行时间超过阈值的命令会记录下来。配置文件里设置slowlog-log-slower-than为10000微秒也就是10毫秒然后通过SLOWLOG GET命令查看。注意Redis的慢日志记录的是命令执行时间不包含网络和排队时间所以如果慢日志里什么都没有但客户端还是超时问题多半不在Redis命令本身。Java应用侧的日志也要会看。Spring Boot项目里开启Redis的debug日志可以看到每一次命令的发出时间。假如日志显示命令发出后到返回前耗了1秒而Redis慢日志里没这条命令那时间就消耗在网络或者Lettuce的连接排队上。日志和监控配合起来才能把问题定位准。工具方面我建议实习生至少会用一款可视化管理工具。Redis官方没有Windows版很多同学在Windows本地上装Redis时会卡在编译那一步最简单的办法是下载官方的Windows移植版或者用Docker跑一个容器。可视化管理工具里Redis Desktop Manager和它的社区分支Another Redis Desktop Manager用得最广能直观看到所有key、查看TTL、执行命令、看内存分布。另外RedisInsight是Redis官方出品功能比第三方工具更全也值得一试。学会用工具能大幅提升排查效率别总是依赖命令行。7. 实习期就能养成的几个Redis工程习惯7.1 键命名和过期时间要从第一天开始规范我见过的老项目里Redis的key命名简直是灾难现场有的叫a1、b2这种无意义名字有的直接用类名加主键ID有的甚至把整个SQL语句当key。等你接手维护的时候根本不知道这个key是干嘛的、属于哪个业务、能不能删。规范的命名应该是“业务名:实体名:标识符”这样的分层结构比如user:info:1001、order:detail:20250101。这样做有几个好处可读性强一眼知道归属用通配符scan的时候方便按前缀筛选团队协作时不会跟别的业务冲突。命名规范应该写进团队约定而不是靠个人自觉。过期时间的设置也要有意识。每次set的时候都问自己一句这个数据会变吗多久变一次如果会变就设一个和变化频率匹配的TTL如果几乎不变也要设一个兜底过期时间防止数据变成永久的垃圾。养成“写缓存必想过期时间”的习惯比学会任何高级命令都重要。7.2 本地环境、测试和上线前自检实习阶段一定要把本地开发环境跑顺。Mac用户装Redis很简单brew install redis一条命令搞定Windows用户更推荐用Docker跑redis官方镜像避免折腾安装编译的问题。本地环境装好之后把连接配置、可视化管理工具全部调试通过能省掉大量沟通成本。写缓存相关的代码时测试要覆盖几个边界缓存未命中时回源数据库是否正确数据库返回空时是否写了空值缓存过期时间是否生效并发请求同一个key是否会导致数据库压力过大。这些测试不一定都要自动化手动用JMeter或者写个简单的并发脚本也能验证。上线前自检清单可以随时拿出来对照有没有没用KEYS有没有大String过期时间设了吗序列化方式统一吗7.3 代码评审和压测里最容易被抓的问题实习生第一次参加代码评审Redis相关的代码往往是review的重点。评审官一般会看这么几件事key命名是否规范过期时间是否合理是否在循环里调用Redis命令是否存在不必要的序列化开销缓存和数据库的一致性方案是否成立。还有一个高频问题是循环调用Redis。比如批量查100个用户的缓存有人写成for循环里一个个get这不仅慢而且每次都是一次网络往返。正确做法是用mget批量获取或者用pipeline。Spring Data Redis里可以用executePipelined来一次性发送多条命令能显著降低耗时。这个优化在数据量大时差距是数量级的评审官看到for循环里的get几乎是必提的意见。压测的时候也有考察点。实习生通常只关注接口的TPS和平均耗时但Redis相关的压测还要额外看连接数曲线是否平稳、有没有大量命令超时、内存增长是否在预期内、慢日志里有没有出现阻塞命令。这些数据能帮你判断缓存层的设计是否真的健康而不只是“看起来能跑”。我在实际带人的过程中最欣慰的时刻就是实习生说“我昨天翻了一下Redis的慢日志发现有个大key每次查询都很慢我准备把它拆一下”。这就是工程敏感度的觉醒。技术本身不难学难的是把这些约束内化成习惯。Redis对Java工程师来说门槛不在于命令有多少条而在于你遇到一个场景时能不能下意识地做出合适的选择用什么类型、设多长过期、怎么防穿透、怎么保一致、挂了怎么查。把这套思路走通了你就不只是一个会调接口的实习生而是一个能对线上系统负责任的工程师。