ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java程序员一周搞定Redis:从命令到分布式锁的实战路线

Java程序员一周搞定Redis:从命令到分布式锁的实战路线

又一个周末,我在群里看到有人在问“Java程序员怎么用一周把Redis捡起来”。底下回复五花八门,有让直接刷面试题的,有让去看源码的,还有让先去背两万字命令文档的。看得我直摇头——这套路我太熟了,当年就是这么劝退自己的。说句实在话,一周时间想成为Redis专家不现实,但对一个写过CRUD、用过缓存、被分布式问题折腾过的Java后端来说,一周从“会用”到“会选型、会避坑、会排查”,完全做得到。

这篇东西就是给这类Java程序员准备的。我把一周的路线拆成五个阶段,从命令行手感、Java客户端集成、核心场景落地、可靠性兜底,一直到最后的线上排查。不走弯路,不堆术语,每个阶段都有可以直接抄的配置和代码。你能得到什么?一套经过验证的学习路径,一套能应对大多数缓存场景的代码模板,以及一堆常规文档里压根不会写的踩坑体验。

1. 第一天的核心任务:先忘掉各种注解,把Redis当成一个有网络的HashMap

Java程序员上手Redis,最容易犯的毛病就是跳过了基本功,直接冲到缓存注解里去。结果就是东西确实跑起来了,但出了问题完全不知道从哪里下手排查。你要问我第一天该干什么,我的建议特别朴素:打开命令行,把它当成一个“通过网络访问的HashMap”来玩。

1.1 为什么说先玩命令行比写Java代码更重要

你想想,你在业务代码里写redisTemplate.opsForValue().set("key", value),背后真正发生的事情,无非就是通过TCP协议往Redis服务器发了一条命令。这条命令长什么样?长这样:

SET key value

如果你连这条命令都说不出来,那你写的那层Java封装就等于是在一个黑盒上面盖了另一层黑盒。以后线上Redis出现慢查询、出现CPU飙升、出现大Key告警,你连排查入口都找不到。我见过不少同事,Redis用了两三年,连redis-cli进去敲INFO看一下内存和命中率都不会,这种基础不补上,后面全是窟窿。

第一天最该做的,是在本机把Redis跑起来。别用Docker,别偷懒,直接编译安装或者用发行版自带的包管理器装一个。为什么?因为你得亲手感受一下redis-server启动时打印的那些日志,得知道数据目录在哪,得搞明白redis-cli连接时用什么端口和密码,这些是你后面排查问题时的“肌肉记忆”。装好之后,把下面这几组命令老老实实敲一遍:

# 基础读写 SET user:1:name "zhangsan" GET user:1:name EXPIRE user:1:name 60 TTL user:1:name # 判存在、删Key EXISTS user:1:name DEL user:1:name TYPE user:1:name # 查看当前库所有Key(生产环境慎用) KEYS *

KEYS *在生产环境是个危险动作,会在Key数量大的时候阻塞Redis主线程,这个我们后面会专门聊。第一天你用它来观察数据写入情况没问题,但脑子里要刻下一句话:这玩意儿线上碰都别碰。

1.2 五种数据结构用生活化的方式去理解

很多教程喜欢用晦涩的定义去解释Redis的五种基础数据结构,我不这么干。我用的是最土的办法——把它映射成Java里已经有的东西,你瞬间就通了:

Redis数据结构等价Java概念典型用途
String单个字符串或数字缓存JSON、计数器、分布式ID
HashMap<String, String>缓存对象字段,单字段读写
ListLinkedList消息队列、时间线列表
SetHashSet去重、共同好友、随机抽奖
ZSetTreeMap+ 分数排序排行榜、延迟队列

比如Hash,你就记住它是“节约内存的对象缓存”。你要缓存一个用户信息,用String你得把整个对象序列化成一个JSON串塞进去,改一个字段就得整体覆盖;用Hash你就能把用户ID当Key,把姓名、年龄、手机号分别当Field存,改哪个字段就动哪个字段,这在某些高并发场景下能省下不少序列化开销。

ZSet是另一个线上出场率极高的结构。它的本质是每个成员绑定一个分数,Redis按分数排序存储。你不光可以做排行榜(分数就是得分),还能做延时队列的底层实现——把执行时间当分数,轮询时用ZRANGEBYSCORE把到期任务取出来处理。很多Java程序员第一反应是这不就是个带排序的集合吗,但真到用的时候,能想到用ZSet的人反而不多。

1.3 关于Key设计,早养成习惯早省心

这一小节不写代码,但我觉得比任何代码都重要。Redis没有表结构的概念,Key设计全靠自觉。我见过最离谱的线上事故,是有人把Key命名成user_12345_info,过两天又有人写成了userInfo:12345,两个人各写各的,缓存数据互相覆盖,查了一整天最后才发现是Key命名冲突。

我给自己定的命名规则,用到现在没出过岔子:

业务域:对象类型:ID:字段修饰符

比如:

  • user:profile:10001
  • order:detail:202401150001
  • product:stock:SKU888

这套规则的好处是,你用冒号分段,Redis里这些Key可以被前缀搜索(虽然生产环境要慎用),更主要是所有团队成员能一眼看出这个Key是干什么的、属于哪个模块。同时要养成给Key加上过期时间的习惯,哪怕你设计的时候认为这个数据永远不会变,也请设置一个TTL兜底。Redis不是你的硬盘,内存是有限的,没有过期策略的缓存只会让内存慢慢涨到不可收拾。

2. 第二天到第三天:Java客户端选型和Spring Boot集成

命令行玩顺了,数据结构心里有数了,接下来要干的事就是把它接进Java工程。很多新手在这个环节会陷入选择困难:Jedis、Lettuce、Redisson,到底该用哪个?网上答案众说纷纭,今天这个推荐那个,明天那个又翻案。我直接给你一个没有广告法风险的实用结论。

2.1 Jedis、Lettuce、Redisson的定位差异

先把三者的本质说透。

  • Jedis:最老牌的客户端,直来直去,API基本是Redis命令的Java映射。它的特点是简单透明,但线程安全方面做得比较差。多线程环境要共用实例,得靠连接池来保证,不然会有线程安全问题。
  • Lettuce:基于Netty的异步客户端,线程安全,你的应用可以和Redis保持长连接,多个线程共享一个连接实例也没问题。Spring Data Redis在Spring Boot 2.x之后默认用的就是它。
  • Redisson:定位不只是客户端,更像一个“住在Redis上的Java分布式工具包”。分布式锁、分布式集合、信号量、延迟队列这些高阶能力,它有现成的模板。封装程度很高,用起来很爽,但代价是你的代码和它的API绑得比较死,出问题的时候需要理解它的内部逻辑。

选型的判断标准不复杂:

大部分Java项目用了Spring Boot,那就以Spring Data Redis + Lettuce作为标配。如果你有大量的分布式协同需求,比如分布式锁、分布式Map,直接把Redisson加进来,没必要自己用Lettuce去拼这些轮子。

不推荐上来就用Jedis写裸命令,除非你的项目就是简单的Redis操作,不想引入Spring Data那层封装。但即使如此,我仍然建议Spring Boot项目优先考虑默认的Lettuce,因为社区更新和踩坑人数决定了它的稳定性上限。

2.2 Spring Boot里的基础配置和序列化坑

先看一个最基础的配置:

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

这套配置里,max-active是最大连接数,并不是越大越好。你想想数据库连接池的道理,连接数是受Redis服务端性能和应用线程数共同制约的。一般压测时逐步往上调,看到一个拐点,再往上加连接数反而导致线程切换开销变大,吞吐量掉头向下。max-wait设成3秒是防止连接池被耗尽时请求无限等下去,客户端快速失败后能尽早触发降级逻辑。

配置落定,紧接着就会踩到第一个大坑——序列化。Spring Data Redis默认用的是JDK序列化,听我一句劝,这个默认配置在真实项目里就是个坑。JDK序列化出来的二进制内容,人类完全不可读。出现线上问题,你拿redis-cli去查数据,看到的是一堆\xAC\xED\x00\x05t...这样的乱码。查错靠猜,调试靠蒙。

更麻烦的是对象结构发生变化之后,JDK序列化的反序列化兼容性非常差,经常直接抛异常。所以我在项目里必改配置,换成JSON序列化器:

@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); StringRedisSerializer stringSerializer = new StringRedisSerializer(); // Key用String序列化,可读性强 template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); // Value用JSON序列化,方便排查 template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }

这里有个容易忽略的细节:Key和HashKey用String序列化,Value和HashValue用JSON序列化。为什么Key要区分开?因为Redis里Key是查找依据,你用JDK序列化出来的Key,在命令行里根本没法读,也没法按前缀匹配。而Value是可读性要求相对低的,但用JSON时人类还能看懂,排查问题方便很多。

2.3 为什么我不建议在业务代码里到处用RedisTemplate

RedisTemplate用起来很顺手,功能全面,但顺手不意味着你应该到处滥用。最简单的例子——查询用户详情。新手写法通常是:

public UserVO getUserById(Long id) { String cacheKey = "user:profile:" + id; UserVO vo = (UserVO) redisTemplate.opsForValue().get(cacheKey); if (vo != null) { return vo; } UserVO dbVO = userMapper.selectById(id); redisTemplate.opsForValue().set(cacheKey, dbVO, 30, TimeUnit.MINUTES); return dbVO; }

这段代码问题在哪里?缓存击穿、缓存穿透、缓存雪崩这三个经典问题,一个都没防。后面我们会专门讲怎么在代码里挡这三位“老朋友”。这里先给你一个设计上的建议:把缓存操作收敛到一个独立的CacheService里,不要让业务代码直接操作RedisTemplate。

为什么要这么做?因为缓存策略在业务里是会被反复调整的——今天你要加空值缓存,明天要加逻辑过期,后天可能在缓存前面再加一层本地缓存。如果你每个Service都直接操作RedisTemplate,改一次策略就得把所有调用点翻出来改一遍,改到后面你会发现不敢动了。收敛成几个带语义的接口,比如queryWithCache、deleteCache、tryLockAndQuery,业务代码干净,缓存策略也可维护。

3. 第四天到第五天:Java业务里最高频的四个Redis场景

到了这一阶段,工具层面的东西基本够了,真正的分水岭在于你有没有处理过真实业务场景。我带过的开发者里,有人命令背得滚瓜烂熟,一遇到需求就不知道用哪个结构;有人RedisTemplate用得溜,但问起缓存一致性就支支吾吾。这一阶段的重点是四个场景——缓存读写、分布式锁、计数器与排行榜、轻量消息队列。

3.1 缓存穿透、击穿、雪崩:三位老朋友的防御姿势

这三个名字听着吓人,本质就三句话:

  • 穿透:查一个缓存和数据库里都不存在的Key,每次请求都直打数据库。
  • 击穿:某个热点Key过期的一瞬间,大量请求同时打到数据库。
  • 雪崩:大量Key在同一时间批量过期,数据库瞬间压力暴增。

我见过太多人把这三个概念倒背如流,一写代码还是裸奔。所以直接给你一套可以抄的防御组合:

穿透,核心是让“不存在”也能被缓存。查询结果为空时,往缓存里写一个空值并设置短期过期时间,比如3到5分钟。另一个更优雅的姿势是布隆过滤器,启动时加载全量Key,查询前先判断Key是否存在,不存在直接挡回去。布隆的缺点是存在误判率,以及数据新增时需要同步更新过滤器,工程上稍微重一些。我的建议:大部分项目直接用空值缓存就够了,轻量且有效。

击穿,核心是给重建缓存这个动作加锁。同一时刻只允许一个线程去查数据库、回填缓存,其他线程等待后直接命中缓存。要注意这里的锁必须是分布式环境下的锁,而不是单机JVM的synchronized。实现手段可以直接上Redisson的RLock,这也是我推荐引入Redisson的一个重要理由。重置缓存时记得做双重检查——拿到锁后再查一次缓存,防止在等待锁的过程中缓存已经被别人重建了。

雪崩,防御核心是打散过期时间。不要在设置TTL时全用同一个数值,随机加一个偏移量,比如基础过期时间30分钟,实际TTL是30 + random(0~300)秒。这样即使一批Key同时被写入,过期时间也会天然错开,避免了集体壮烈。

这里把三兄弟的防御策略、实现位置和代价整理成一张表,以后面试和工作都用得上:

问题核心防御手段实现位置代价
穿透空值缓存 / 布隆过滤器读取缓存后、查DB前额外内存开销;布隆需维护
击穿互斥锁重建缓存查DB的回填逻辑处增加锁等待时间
雪崩过期时间加随机偏移写入缓存设置TTL处几乎为零

3.2 分布式锁:这么用才算是锁对了

用Redis做分布式锁,是Java后端绕不开的话题。但我想先说一个反直觉的结论:如果只用SETNX去实现一个分布式锁,绝对不是只有“坑”,而是生产事故级别的挑战。这里面的关键问题有三层:

第一层,锁没有设置过期时间。拿到锁的线程执行到一半挂了,锁永远不释放,其他线程全部饥饿。解决方式是用SET key value NX PX 30000一次性设置锁和过期时间,保证原子性。这里有个细节,很多人用两步走——先SETNX再EXPIRE,程序在执行完SETNX之后崩溃了,锁依然会永久存在。所以必须一股脑完成,绝不能分开。

第二层,锁过期时间到了但业务还没执行完。A线程拿到锁,处理耗时2秒,锁过期时间设了1秒,锁自动释放,B线程拿到同一把锁进来了,并发问题复燃。Redisson对这个问题的处理方式是看门狗机制——锁快到期时自动续期。如果你不用Redisson,得自己在代码里起一个定时任务给锁续期,非常麻烦。这也是我建议直接用Redisson的原因。

第三层,误删别人的锁。A线程处理完准备释放锁,但它持锁期间锁已过期且被B线程获取。A释放锁的时候,DEL掉的其实是B的锁。这个问题的标准解法是:加锁时设置一个唯一标识如UUID,释放锁时先判断当前持有者的标识是不是自己,是才DEL。判断和删除两步又要保证原子性,所以得用Lua脚本:

if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

我用Redisson的时候,这部分细节它内部已经处理了,但你要理解它底层在干什么,不然遇到锁突然失效的诡异场景,你连排查方向都没有。

3.3 计数器和排行榜:用对数据结构,省下的不只是内存

再举两个最常见的真实需求。

计数器。比如文章阅读量,用户每点一次就加一。你可以用INCR article:read:10086,Redis单线程执行INCR天然是原子的,避免了在应用层加锁。但注意一个细节——如果这是热点文章,用一个Key承载所有点击,这个Key的访问频率可能达到每秒上万次,就成了“热Key”。一个常用的优化方案是:把计数的Key拆成多个,比如article:read:10086:0、:1、:2,每个Key只承担部分流量,读取时把多个Key的值累加。这就是分片计数,本质上是在单点写瓶颈和读复杂度之间做权衡。

排行榜。ZSet就是为这个场景设计的。每个用户ID作为成员,得分作为排名依据,ZADD写入,ZREVRANGE取出前N名。整个过程不涉及数据库的ORDER BY,性能好得离谱。但要提醒一点:ZSet的基本操作复杂度是O(log(N)),对于万级、十万级用户毫无压力,对于千万级用户,ZSet占用的内存和维护成本会迅速上升,这时候就得考虑降级方案,比如只维护TopN的过滤逻辑,或者定期归档冷数据。

3.4 轻量消息队列和延迟队列的快落

如果项目里还没有引入专门的消息中间件,Redis的List结构完全可以顶一阵子。生产者用LPUSH往队列里塞任务,消费者用BRPOP阻塞弹出,这是一个极简但可用的队列模型。BRPOP的B代表Blocking,没有任务时消费者线程阻塞等待而不是空轮询,可以大幅降低CPU消耗。

延迟队列则可以用ZSet实现——任务执行时间戳作为分数,消费者线程每隔一段时间用ZRANGEBYSCORE把小于当前时间戳的任务取出来执行。不需要引入定时任务框架,Redis自己就能扛。这套方案能支撑的量级取决于任务量和消费速度,通常中小业务用起来绰绰有余。

不过我得说句实在话,如果队列场景是核心链路,或者需要消息确认、死信、有序消费、消息回溯这些能力,还是老老实实引入专业的消息中间件。Redis这套方案适合的场景是“轻量、临时、后续好替换”,用得太重只会给自己挖坑。

4. 第六天的份量:持久化、高可用和性能兜底

到了这个阶段,基础用法已经掌握得差不多了,能处理日常业务场景了。最后这两天的重点不是新功能,而是可靠性。你要在脑子里建立一套关于“Redis什么时候会挂、挂了会怎样、怎么恢复”的完整认知。

4.1 持久化选择:RDB和AOF不是二选一

Redis是一个内存数据库,数据默认是放在内存里的,如果不配置持久化,进程一退出,数据就全部蒸发。很多Java程序员平时写代码没太在意这个,真到线上宕机才发现缓存数据全丢,所谓“缓存没了再查库就行”只是理想情况——如果缓存里存的是正在给外部系统处理的中间状态,或者热点数据刚被缓存就被清空,数据库会瞬间被流量冲垮。

Redis的持久化有两种方案:

  • RDB:按时间间隔把内存里的全量数据生成快照写入磁盘。优点是恢复快,文件紧凑;缺点是你设置的间隔内如果宕机,这段时间的写入会全部丢失。
  • AOF:把每次写命令追加到日志文件,恢复时重放日志。你可以配置成每秒刷盘一次,最多丢一秒数据;缺点是文件越来越大,恢复速度比RDB慢。
  • 混合持久化:Redis 4.0之后支持RDB和AOF混用,AOF文件头部用RDB格式存全量数据,后面追加增量命令,兼顾恢复速度和数据安全性。

我的配置建议不折腾,直接Redis 6.x以上版本,打开aof-use-rdb-preamble yes,同时开启RDB和AOF,这基本是当前最稳妥的姿势。数据安全优先级高的业务,appendfsync设置为everysec就够了,没必要用always,因为每次写命令都刷盘会显著拉低性能。

4.2 主从、哨兵、集群:到底该上哪个

这个问题堪称Java面试里的常青树,但在真实项目里,很多人把它理解成了一种“配置选择题”。我按线上部署的场景给你拆开:

  • 主从复制:一个主节点负责写,多个从节点负责读。读写分离降低了单节点压力,但主节点宕机时,从节点不会自动上位,需要人工干预。
  • 哨兵模式:在主从复制基础上加了一层哨兵进程,监控主节点状态,主节点挂了能自动把一个从节点提升为新的主节点。对于大多数中小项目,哨兵模式已经足够。
  • 集群模式:Redis官方集群。数据分片放在多个主节点上,每个主节点又有从节点做高可用。容量和吞吐量可以水平扩展,但客户端和运维复杂度都上一个台阶。

我的经验判断标准特别朴素:缓存数据量预计不超过单机内存,用哨兵模式;超过单机内存且持续增长,直接上集群。不要一上来就搞个大集群,运维成本、客户端配置成本、排查问题的成本,都会成倍上升。

4.3 内存淘汰策略:驱逐哪条数据,Redis也得讲道理

内存是有上限的,到达上限之后新写入的数据怎么办?这就是内存淘汰策略要回答的问题。我把几个主流策略用大白话翻译一下:

策略含义适用场景
noeviction内存满了直接报错必须保证数据完整,不建议开启
allkeys-lru从所有Key中淘汰最久没访问的通用缓存场景,最推荐
volatile-lru只从设置了过期时间的Key里淘汰最久没访问的希望未设置过期时间的Key永远不被驱逐
allkeys-lfu淘汰访问频率最低的Key有明显冷热分布的场景
volatile-ttl优先淘汰剩余存活时间短的Key适合短时效数据

实际项目里我基本上就推荐allkeys-lru。原因很简单:缓存场景下,数据是否被淘汰本来就不该由业务逻辑来保障,业务保障靠的是数据库,Redis这层就是尽力而为。如果担心某些核心Key被淘汰后雪崩,那就给这些Key加长TTL,或者单独部署一套不淘汰的Redis。

4.4 大Key和热Key:Java程序员最该学会的体检项目

这个问题,前面提了好几次,到这里展开说。

什么是大Key?字符串值超过10KB,或者集合类型成员数量巨大、总内存占用过高,都算。大Key的问题在于,Redis是单线程处理命令的,操作一个几百KB的Hash,整个Redis实例都会卡一下。如果多个大Key集中被访问,线上服务延迟曲线会直接拉出一条长长的尖峰。

排查办法,命令是现成的:

redis-cli --bigkeys

它会扫描整个实例并输出各个类型里最大的几个Key。线上执行时建议用--i 0.1加一个间隔,慢慢扫,别一口气把CPU打满。

热Key就更隐蔽了。某个Key的访问频率高到让单个Redis实例的CPU饱和,就算你开再多客户端也没用,瓶颈全在这个Key上。排查用:

redis-cli --hotkeys

前提是开启了maxmemory-policy allkeys-lfu,因为热Key统计依赖LFU的访问频次信息。找到热Key后,应对方案有三板斧:加本地缓存前置拦截大多数请求、Key内容分片、必要时对该Key做读写分离。记住一点:Redis单线程的执行模型决定了单个Key的操作是串行的,同构于单线程Java程序里你要对一段热点代码做优化——拆细、分散、减少冲突。

5. 第七天:性能排查和那些不得不说的经验教训

到了最后一天,不再学新功能,而是用一套“体检+排障”的方法把前面学的东西串起来。很多人学了七天Redis,命令也熟,代码也会写,一遇到线上问题还是抓瞎,就是因为缺少一套排查的路径依赖。

5.1 排查问题时的第一反应:先看慢查询

Redis自身的慢查询日志,是排查命令问题的第一入口。慢查询阈值设置如下:

CONFIG SET slowlog-log-slower-than 5000 CONFIG SET slowlog-max-len 128

单位是微秒,也就是命令执行超过5毫秒就记录下来。然后随时可以看:

SLOWLOG GET 50

注意一个关键逻辑:Redis是单线程的,慢命令不光慢自己,还阻塞整个实例。一个执行了30秒的KEYS *,会让所有其他命令排队30秒。很多时候你感觉服务变慢,去查数据库、查网络,其实问题就在这——某个同事执行了一条慢命令,把整个Redis拖垮了。所以看到SLOWLOG里出现命令的第一反应不是“这条命令有毛病”,而是“这条路命令影响了谁”。能改业务就改业务,比如KEYS换成SCAN,集合操作别再全量捞。

5.2 排查响应延迟时的第二反应:网络和连接池

另一个常见的“伪Redis问题”,其实是连接池配置不当。比如连接池max-active设了8,应用有20个线程同时去读缓存,剩下12个线程全部在max-wait那里排队等待,性能表现就明显恶化。这时候Redis本身速度快得飞起,慢的是你应用端的获取连接逻辑。

排查方法不复杂:看看连接池的活跃连接数、等待获取连接的线程数、等待时间是不是接近max-wait上限。如果是,调大max-active和max-idle后再压测对比,持续观察拐点。还有一个容易被忽略的细节——每次操作Redis时频繁创建连接而不是从连接池复用,对Lettuce来说这是致命的反模式,一定要保证连接池配置生效,而不是走默认单连接。

5.3 监控指标里最值得盯的四项

真正解决线上问题,光靠直觉和临时排查是不够的,得有长期数据支撑。我强烈建议每个Redis实例都接入监控,重点盯下面四项:

  • 命中率:INFO stats里看keyspace_hits和keyspace_misses。命中率过低说明缓存设计有问题,八成是Key设置不合理或者过期时间太短。
  • 内存:INFO memory里看used_memory。持续上涨不加控制,最终会触发内存淘汰或OOM。配套定期执行redis-cli --bigkeys诊断内存分布。
  • 连接数:INFO clients里看connected_clients。异常升高往往是连接泄漏或者流量突增,需要结合客户端连接池配置一起看。
  • 命令延迟:INFO commandstats里可以看到每个命令的调用次数和耗时。结合慢查询一起分析,能快速定位是哪个命令在拖性能。

这里要特别说一句:不要只等出事了才来盯指标。我建议在项目上线前就把这些监控项配好,让告警在最早期就暴露风险。Redis这种组件,很多问题是一点点积累出来的,比如内存缓慢增长、大Key逐渐扩大,在告警触发之前,往往已经有几周甚至几个月的潜伏期了。

5.4 实战踩坑记录:这些经验是我真金白银换来的

最后分享几个我在实际项目里踩过、也帮别人排过的坑,都很典型,而且常规文档里不会写这么细。

第一坑:RedisTemplate的@Cacheable缓存穿透。Spring Cache注解用起来真爽,@Cacheable(key = "#id", unless = "#result == null")一行搞定,你以为unless挡住空值就不会穿透了吗?错了,unless只是不缓存空值,查询照常打到数据库。防线还得自己加,返回空时手动写入一个短TTL的空值Key,或者统一封装一层空值判断逻辑。注解只是帮你省了代码,没帮你省掉系统设计。

第二坑:大批量写入时单条循环调用。有一次同事导入十万条数据,代码是for循环里一条条redisTemplate.opsForValue().set(),结果导入任务跑了快四十分钟。Redis本身处理十万次命令很快,但这个循环里每次调用都要走一次网络往返,一次0.1毫秒,十万次就是十秒起步,再加上应用端的序列化和连接池排队,时间一下子就上去了。优化时分批用pipeline或者RedisTemplate的executePipelined,把命令批量发给服务端,合并网络往返,导入时间直接降到十几秒。

第三坑:上线前没有验证序列化配置。Spring Data Redis默认JDK序列化,如果生产环境的Redis里存的是JDK序列化的数据,你改了配置切到JSON序列化,这部分老Key全都解析不了。很多人上线后才发现,然后只能一边滚配置一边清脏数据。我的建议是,在开发早期就确定序列化方案,一旦线上有数据,更改成本就会陡增。

第四坑:把Redis当成数据库用。有人在Redis里存订单数据,而且不设置TTL,问起来还理直气壮——“反正内存够大”。后来数据量涨到几十个G,一次主从切换导致全量同步直接拖垮了主节点。Redis是缓存,不是数据库,这条界限必须清晰。它的数据再快,都要接受“随时可能被淘汰、被清空”的事实,真正的持久化存储,必须在数据库里有一份。

6. 最后一天应该怎么收尾

前面五块内容,其实已经把一周的路线铺完了。第一天建手感,第二天到第三天做集成,第四天到第五天过场景,第六天补可靠性,第七天练排查。这个节奏,我自己带人验证过不止一次。不是让人背完所有命令才动手,而是边用边学,用场景驱动知识,把Redis的每个能力点和真实的业务需求对上号。

只剩最后一点建议:把这一周学到的内容,想办法落到一个真实或模拟的项目改造里。比如找一个你手头普通的“查详情”接口,自己试着加上缓存、加上空值防御、加上分布式锁、加上热Key分析。光看不练,五天后你连SET带NX的语法都会觉得陌生,练过一次,这些点才会真正长在你身上。

我见过很多学Redis的人,上来就啃源码或者啃一本几百页的文档,最后往往是坚持不下去。反倒是像这样“带着具体问题去查、去用、去试错”的路径,最能在短时间内形成战斗力。一周的时间很紧,但足够把Redis从熟悉的陌生人,变成一个顺手好用的工具。希望这篇东西,能让你少走几步弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进