如果你写过几年 Java,肯定遇到过这种场景:项目里引入了 Redis,但一重启服务,缓存全没了;或者往 Redis 里写数据的时候一切正常,读出来却是一堆带 \xAC\xED\x00\x05 前缀的乱码;又或者 Spring Boot 项目刚起步,连 RedisTemplate 和 StringRedisTemplate 的区别都没搞明白就上手开写,结果 key 设计得乱七八糟,排查问题的时候对着 redis-cli 里的各种二进制串直挠头。
我最初接触 Redis 是在一个电商后台项目里,需求很简单:把商品列表页的数据缓存起来。那会儿我抱着“Redis 就是个 key-value 数据库”的想法,用了 Jedis + JSON 的土办法,确实跑通了。但后来项目切到 Spring Boot,缓存对象一多,序列化、连接池、分布式锁、主从切换这些问题接踵而至,我才意识到:单纯会调用 set 和 get 只是管中窥豹,真正要在 Java 和 Spring 环境下把 Redis 用好,需要一套相对完整的知识体系。
这篇文章我就以自己实际开发和排查异常的经验为主线,把“Java 操作 Redis”和“Spring 环境下操作 Redis”这两条线完整地过一遍。我会先讲环境怎么搭、客户端怎么选,再说 Spring Boot 里最容易被忽视的序列化坑,然后是分布式锁、缓存治理这些实战场景,最后补充主从架构和数据排查技巧。文章尽量用大白话,适合刚上手 Redis 的 Java 开发者,也适合已经在用但想系统补一补细节的同学。
1. 先把 Redis 跑起来:安装、启动和基础自检
1.1 Windows / macOS / Linux 下的安装姿势
想用 Redis,第一步当然是把它装好。在很多新手教程里,这一步被一笔带过,但实际操作系统不同,安装方式差异挺大。
- Windows:Redis 官方其实没有原生 Windows 版本!官方文档里只保证 Linux/macOS 的支持。Windows 上常见方案有三个:一是用 WSL(Windows Subsystem for Linux)里装 Ubuntu,然后
sudo apt install redis-server;二是用 Docker Desktop 直接拉redis:7镜像;三是用 Memurai 这类 Redis 兼容实现。我个人最推荐 WSL 或 Docker,因为和 Linux 生产环境行为一致,避免出现“本机没问题、服务器出问题”的诡异情况。 - macOS:最简单的是
brew install redis。装完直接用redis-server启动,默认端口 6379。想开机自启可以用brew services start redis。 - Linux(CentOS/Ubuntu):Ubuntu 用
apt-get install redis-server,CentOS 用yum install redis或者从官网下载源码包编译。编译方式为make && make install,需要注意先装好 gcc。
启动成功后,你可以新开一个终端,执行下面的自检命令:
bash复制redis-cli ping
如果返回 PONG,说明服务端和客户端都正常。接着执行 redis-cli info server 可以看版本号和运行信息,redis-cli config get maxmemory 可以看内存上限配置。我建议新手把所有命令先过一遍,keys *、dbsize、flushdb、flushall 这几个尤其要弄懂,因为后面排查缓存问题时一定会用到。
注意:
flushdb会清空当前库的 key,flushall会清空所有库的 key。在测试环境随便用没关系,在线上环境如果不小心执行了,后果非常严重。我见过有人把生产库的 session 缓存整个清掉,之后再也不敢碰这条命令了。
1.2 数据落盘:为什么 Redis 重启后缓存会丢
另一个让新手困惑的问题是:“我明明 set 了值,怎么重启服务就没了?”这跟 Redis 的持久化机制有关。Redis 默认配置下开启了 RDB 快照(默认 save 3600 1 等规则),但它不是实时落盘的,更不是像 MySQL 那样每条写操作都记 binlog。
如果你的 Redis 数据允许少量丢失,RDB 足够;如果希望最大化保证可靠性,就要开启 AOF(Append Only File),在配置文件 redis.conf 里设置:
conf复制appendonly yes
appendfsync everysec
everysec 表示每秒刷一次盘,性能和数据可靠性比较均衡。你在 Java 侧写缓存时,根本感知不到这个过程,但一旦出现 Redis 宕机重启,有没有 AOF 会直接影响缓存是否“失忆”。这不是本节重点,只想提醒:缓存可以丢,业务数据不能只靠 Redis 扛,要结合 MySQL 或者消息队列做持久化兜底。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Java 客户端选型:Jedis、Lettuce 和 Redisson
2.1 三个主流客户端各自干吗的
Java 生态里操作 Redis 的客户端很多,但最主流就三个:Jedis、Lettuce 和 Redisson。它们之间不是替代关系,而是定位不同。
| 客户端 | 定位 | 特点 | 使用场景 |
|---|---|---|---|
| Jedis | 轻量级同步客户端 | 直接封装 Redis 命令,API 直观 | 简单项目、快速验证、需要原生命令控制 |
| Lettuce | 高性能异步客户端 | 基于 Netty,连接复用,支持同步/异步/响应式 | Spring Boot 默认客户端,适合中大型项目 |
| Redisson | 高级分布式服务框架 | 提供分布式锁、分布式集合、消息队列等开箱即用能力 | 需要分布式协同、锁、限流等复杂场景 |
我刚工作那会儿项目里普遍用 Jedis,因为它够简单:new Jedis("localhost", 6379) 然后调 jedis.set()。但它有几个问题:连接不是线程安全的,得靠连接池复用;网络交互没有 Lettuce 那么高效。后来 Spring Boot 2.x 把默认客户端从 Jedis 换成了 Lettuce,原因是 Lettuce 基于 Netty 可以共享连接,在高并发下性能更好,还能支持异步和响应式编程。
Redisson 则更“上层”。它不强调你直接操作单个命令,而是把 Redis 封装成类似本地集合、分布式锁、Semaphore 等对象。比如分布式锁,原生命令得自己写 SET NX EX,Redisson 里直接 lock.lock() 就行。我的经验是:纯缓存项目用 Lettuce 就够了;一旦涉及分布式锁、信号量、延迟队列这些功能,Redisson 能帮你少写大量底层代码。
2.2 Jedis 基础用法和连接池
Jedis 的用法非常直白。项目里引入依赖:
xml复制<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.1.0</version>
</dependency>
然后写一段最朴素的代码:
java复制Jedis jedis = new Jedis("127.0.0.1", 6379);
// 如果 Redis 设置了密码,还要指定 auth
// jedis.auth("yourpassword");
jedis.set("test:key", "hello");
String value = jedis.get("test:key");
System.out.println(value);
jedis.close();
这段代码能跑通,但生产环境绝对不能这么写。原因有二:第一,每次 new Jedis 都会新建 TCP 连接,费时费力;第二,Jedis 实例不是线程安全的,并发共用同一个实例会出问题。正确做法是使用连接池:
java复制JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(64);
config.setMaxIdle(32);
config.setMinIdle(8);
config.setMaxWaitMillis(3000);
JedisPool pool = new JedisPool(config, "127.0.0.1", 6379, 3000, "password");
try (Jedis jedis = pool.getResource()) {
jedis.set("test:key", "hello");
String value = jedis.get("test:key");
}
pool.close();
这里 JedisPoolConfig 的几个参数需要根据实际业务调整:maxTotal 是最大连接数,不是越大越好,太大会占用文件描述符;maxIdle 是空闲连接上限,控制的是回收策略;minIdle 是保证的常驻连接数,用于应对突发流量。初期拿不准,就按 maxTotal=64,maxIdle=32 起步,压测后再调优。
2.3 Pipeline 和事务:为什么批量操作性能能翻几倍
Jedis 的另一个实用技巧是 Pipeline。假设你要往 Redis 里写入一万个 key,用 for 循环逐条 set,每一条都要走一次完整网络往返(RTT),一万条就是一万个 RTT。而 Pipeline 可以把多条命令一次性发给服务端,再一次性接收响应,性能能提升一个数量级。
java复制try (Jedis jedis = pool.getResource()) {
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
pipeline.set("batch:key:" + i, String.valueOf(i));
}
pipeline.sync(); // 真正发送并接收响应
}
需要注意,Pipeline 不是事务。事务要配合 MULTI / EXEC,它能保证多条命令原子性地执行,但不能像 MySQL 那样回滚,中间某条命令出错了,其他命令还是会被执行。所以不要把 Pipeline 当成事务用,它只是减少网络开销的手段。
3. Spring Boot 集成 Redis:自动配置与核心对象
3.1 依赖引入和 yml 配置
Spring Boot 环境下的标准姿势是引入 spring-boot-starter-data-redis:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然后配置 application.yml。这里有个比较坑的版本差异:Spring Boot 2.x 的配置前缀是 spring.redis,Spring Boot 3.x 改成了 spring.data.redis。如果你从旧项目升过级,务必检查一下这个前缀,否则配置不会生效。
yaml复制spring:
data:
redis:
host: 127.0.0.1
port: 6379
password: # 没有密码就留空
timeout: 3s
lettuce:
pool:
max-active: 32
max-idle: 16
min-idle: 4
max-wait: 3s
我在配置 lettuce.pool 时有个切身体会:如果项目里把 Redis 当缓存用,但同时又用它做分布式锁,连接池的 max-active 不能只按缓存 QPS 估算。因为分布式锁的获取和释放操作同样占连接,锁竞争激烈时,连接数不够会导致大量线程阻塞在获取连接上。我见过一个项目把 max-active 设成 8,结果缓存 QPS 不高,但锁操作的等待时间很长,最后把连接池调大到 64 才缓解。
3.2 RedisTemplate 和 StringRedisTemplate:到底该用哪个
Spring Data Redis 提供了两个核心操作类:RedisTemplate 和 StringRedisTemplate。很多初学者拿过来就用 RedisTemplate,写完了发现数据在 Redis 客户端里变成了二进制串,还一脸懵。
StringRedisTemplate 严格来说是 RedisTemplate<String, String> 的特例:key 和 value 都使用 StringRedisSerializer,序列化出来的是纯字符串,可读性很好。而 RedisTemplate 默认情况下 key 和 value 用的都是 JdkSerializationRedisSerializer,也就是 Java 自带的序列化方案。问题就出在这里:Java 对象经过 JDK 序列化之后,会带上一串类似 \xAC\xED\x00\x05 的头部,存进 Redis 里不仅占空间,别的语言(比如 Python 或 Go 服务)也读不了。
我踩过最惨的一次坑:同一个 Redis 实例,Java 服务往 user:info:1001 里写入了一个 User 对象,后面 Python 服务想读这个 key 做数据分析,结果解析出来的是一堆乱码。当时排查了很久才发现是序列化方式不一致。所以你在选型时要明确:
- 如果只存字符串、整数、JSON 文本,直接上
StringRedisTemplate; - 如果要存对象而且只在 Java 服务内部使用,可以用
RedisTemplate但必须自己配置序列化器; - 如果多个语言共用同一份 Redis 数据,key 一律字符串,value 一律 JSON 字符串,不要用 Java 原生的
JdkSerializationRedisSerializer。
3.3 手写一个 RedisTemplate 配置类:序列化怎么配才合理
既然要配序列化器,就给你一个我常年在用的配置类。核心思路一句话:key 用 StringRedisSerializer,value 用 GenericJackson2JsonRedisSerializer。这样 key 可读,value 是标准 JSON,跨语言也没问题。
java复制@Configuration
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(RedisTemplate.class)
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;
}
}
注意两个细节:
第一,RedisTemplate 的 hashKey 和 hashValue 也要单独设置。很多人的配置只处理了普通 key/value,忘了 Hash 结构里的内部 key,导致 opsForHash() 存进去的数据还是 JDK 序列化格式。我一开始也没注意到 setHashKeySerializer,排查了半个下午才发现真相。
第二,GenericJackson2JsonRedisSerializer 在反序列化时需要知道对象类型,所以它会在 JSON 里追加一个 @class 字段记录类的全限定名。这意味着如果你改了包名、类名或字段结构,老数据可能反序列化失败。解决思路有两个:一是用 Jackson2JsonRedisSerializer 指定具体类型,但只能处理一种类型的 value;二是尽量只存 Map 或 List,不存复杂自定义对象,把类型信息牢牢掌握在自己手里。
3.4 缓存读写的最佳实践:防击穿、防穿透、防雪崩
有了 RedisTemplate,紧接着要面对的就是缓存治理。很多人的第一版代码长这样:
java复制String key = "product:" + productId;
Object value = redisTemplate.opsForValue().get(key);
if (value == null) {
// 查数据库
Product product = productMapper.findById(productId);
if (product != null) {
redisTemplate.opsForValue().set(key, product, 10, TimeUnit.MINUTES);
}
return product;
}
return (Product) value;
这段代码“看起来能跑”,但高并发下有严重问题:如果某个 key 在缓存中不存在,而它的请求量非常大(比如热点商品),所有请求都会同时穿透到数据库,这就是缓存击穿。为了防击穿,常用的做法是加互斥锁,保证只有一个线程去查库:
java复制String key = "product:" + productId;
Object value = redisTemplate.opsForValue().get(key);
if (value != null) {
return (Product) value;
}
// 获取锁,防止大量请求同时打到数据库
String lockKey = "lock:product:" + productId;
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(30));
if (locked) {
try {
value = productMapper.findById(productId);
if (value == null) {
// 对不存在的数据也做短暂缓存,防止穿透
redisTemplate.opsForValue().setIfAbsent(key, "", Duration.ofMinutes(1));
} else {
redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(10));
}
return value;
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 没拿到锁就短暂自旋等待
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getWithLock(productId); // 递归调用自己
}
这里补充两个点:缓存穿透对应的是“查询数据库不存在的数据”,常见做法是空值短缓存,更高级一点用布隆过滤器(Bloom Filter)在查库之前直接过滤掉不存在的 id;缓存雪崩则是大量 key 同时过期,比如缓存读取接口会缓存 1 小时,如果这些 key 都在同一时间写入、同一时间过期,下一瞬间的流量会全部冲进数据库。解法很简单:过期时间加随机值,比如 10 分钟正负 1 分钟的随机偏移。
java复制long ttl = 600 + ThreadLocalRandom.current().nextLong(120);
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(ttl));
4. 实战争议区:Redis 数据类型怎么选,分布式锁怎么落地
4.1 五大常用数据类型在 Java 里的对应操作
Redis 不是“大号 HashMap”,它自己有一套完整的数据结构。我在面试时发现很多 Java 开发者对 Redis 数据结构背得滚瓜烂熟,用起来却永远只会 String。下面把我自己的使用习惯整理成一张表:
| 数据类型 | 常见业务场景 | Java 侧的操作示例 |
|---|---|---|
| String | 缓存、计数器、限流、分布式 id | opsForValue().set(key, value) increment(key) |
| Hash | 对象属性缓存、购物车 | opsForHash().put(key, field, value) get(key, field) |
| List | 消息队列、粉丝列表、时间线 | opsForList().leftPush(key, value) rightPop(key) |
| Set | 去重集合、共同好友 | opsForSet().add(key, value) isMember(key, value) sinter 求交集 |
| ZSet | 排行榜、延迟队列 | opsForZSet().add(key, value, score) reverseRange(key, start, end) |
举几个实际例子。计数器场景,比如统计文章浏览数,用 opsForValue().increment("article:view:1001") 就够了,Redis 单线程执行这一步是原子的。排行榜场景,王者荣耀里的大区排行榜就是典型的 ZSet,score 是排位分或者段位分,opsForZSet().add("rank:server:1", "player:123", 1300) 就能更新分数,取榜单直接用 reverseRange 取前 100。延迟队列场景,比较轻量但有效的做法是 ZSet + 定时轮询:score 存到期时间戳,定时任务用 rangeByScore(key, 0, currentTimeMillis()) 取出到期的任务,处理完再删除。
还有一种常见需求:在线用户列表、签到用户列表,这种天然适合 Set。比如你需要快速判断某个用户是否已经领取过优惠券,用 Set 的 isMember("coupon:2025:q1", userId),一条命令解决,非常有说服力。
4.2 分布式锁:setIfAbsent 还是 Redisson
分布式锁是 Redis 在微服务场景下的一个重要用途。为什么要用 Redis 做锁?因为多个应用实例同时修改一份资源时,Java 的 synchronized 只能锁住单台 JVM,没法跨实例。Redis 的 SET key value NX EX 命令天然满足“分布式锁”的两个特性:NX 表示 key 不存在才能设置,EX 表示过期时间。
先看最基础的实现:
java复制private static final String LOCK_KEY_PREFIX = "lock:";
private static final Duration LOCK_TTL = Duration.ofSeconds(30);
public boolean tryLock(String businessKey, String requestId) {
return redisTemplate.opsForValue()
.setIfAbsent(LOCK_KEY_PREFIX + businessKey, requestId, LOCK_TTL);
}
public void unlock(String businessKey, String requestId) {
String currentLock = redisTemplate.opsForValue().get(LOCK_KEY_PREFIX + businessKey);
if (requestId.equals(currentLock)) {
redisTemplate.delete(LOCK_KEY_PREFIX + businessKey);
}
}
这里的 requestId 可不是随意传的。如果只按 key 删除锁,很可能出现这么个问题:线程 A 拿到锁,执行时间超过了 30 秒,锁自动过期了;线程 B 拿到锁开始执行业务;此时线程 A 终于执行完,直接 delete(lockKey),把线程 B 的锁给删了。线程 B 的执行就不安全了。所以释放锁时一定要校验当前锁的 value 是不是自己写进去的 requestId,也就是“谁加的锁谁解”。
这个实现依然不完美:锁续期问题没有解决。如果业务执行超过 30 秒,锁已经自动过期,其他线程照样能进来。这时建议直接用 Redisson 的分布式锁,它自带“看门狗”机制,会为持有锁的线程自动续期,不用担心业务没执行完锁就过期了。
java复制RLock lock = redissonClient.getLock("lock:order:1001");
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑,锁会自动续期
doBusiness();
} finally {
lock.unlock();
}
}
Redisson 里 tryLock(waitTime, leaseTime, unit) 的语义是:最多等待 5 秒获取锁,拿到锁后最多持锁 30 秒,同时看门狗会在锁快过期时自动续期。如果你的业务符合“持锁时间不确定、需要自动续期”的场景,用它比手动 SET NX EX 更省心。我个人在支付防重、订单幂等、库存扣减这类场景里,默认都用 Redisson,几乎没再手写过锁的底层逻辑。
4.3 连不上、超时、连接池耗尽:Redis 客户端有哪些典型故障
实际操作里经常遇到莫名的报错。整理几个我亲身经历并解决过的:
错误一:RedisConnectionFailureException: Unable to connect to Redis
原因通常是 Redis 服务没启动、网络不通、密码错误。排查顺序:redis-cli ping → 检查配置里的 host/port → 检查防火墙和安全组。
错误二:RedisCommandTimeoutException: Command timed out after 3 second(s)
原因一般是 Redis 服务端阻塞(比如 KEYS * 这种大命令)、网络延迟、或者连接池等待时间过长。用 redis-cli --latency 可以看网络延迟是否正常。如果只是偶发超时,调大 timeout 和 max-wait 即可;如果是持续超时,优先看 Redis 的慢查询日志。
错误三:Cannot get a connection from pool
连接池被打满了。排查思路:查看代码里是否忘记归还连接(用 RedisConnection 的时候没遵守 try-with-resources 规范);将 max-active 调大;检查是否有太慢的命令占着连接不放。Spring Data Redis 里 RedisTemplate 默认会在执行完命令后归还连接,所以这种情况大部分是连接池配置太小或者突发流量过大。
5. 从单机到主从:Docker 快速搭主从,Java 代码无感迁移
5.1 Docker 部署 Redis 主从
实际生产环境基本不跑单机 Redis,至少是主从架构。主从的好处:主节点负责写,从节点负责读,实现读写分离;主节点挂了可以从从节点提升为主节点,保证高可用。这里不说太复杂的集群方案,只讲最实用的一套 Docker 主从命令。
先建一个自定义网络,让容器之间可以互相解析:
bash复制docker network create redis-net
启动主节点:
bash复制docker run -d --name redis-master --network redis-net -p 6379:6379 redis:7
启动从节点,--replicaof 指定主节点地址:
bash复制docker run -d --name redis-slave --network redis-net -p 6380:6379 redis:7 \
redis-server --replicaof redis-master 6379
接着验证主从关系:
bash复制redis-cli -p 6380 info replication
输出里的 role:slave、master_link_status:up 就代表从节点连上了主节点。注意:从节点默认只读,在从节点上执行 set 会报错 READONLY,这是正常现象。
主从架构下,Java 客户端配置不需要改动主机名,因为仍然通过主节点 6379 对外提供读写服务。真正的读写分离需要在客户端侧做额外配置,这里不展开。值得留意的是:Replication 是异步的,从节点的数据有一定延迟。如果你的业务要求“写完立马能读到”,比如下单后立即查询订单状态,直接读从节点会出现数据不一致,这种情况要么强制走主节点,要么给业务留出极短的重试窗口。
5.2 哨兵模式下的 Spring 配置
主从解决了容灾问题,但没有解决“主节点挂了自动切换”的问题。Redis 官方方案是 Sentinel(哨兵)。哨兵会监控主节点的健康状态,发现主节点不可用后,自动在从节点中选出一个新的主节点。Java 侧连接 Sentinel 的配置很简单:
yaml复制spring:
data:
redis:
sentinel:
master: mymaster
nodes:
- 127.0.0.1:26379
- 127.0.0.1:26380
- 127.0.0.1:26381
master 对应哨兵配置里监控的主节点名称(默认叫 mymaster)。这样配置后,Spring Data Redis 的底层会自动与哨兵通信,获取当前真正的主节点地址。主从切换期间,Redis 操作可能会短暂报错,这就需要代码里做好重试。我的经验是:在分布式锁这种强一致操作上,失败后不要无限重试,而要快速失败并通知上层做补偿;在缓存这种非关键数据上,可以直接降级为查数据库。
6. 排查与可视化:让 Redis 数据“看得到、查得清”
6.1 可视化管理工具选择
命令行用惯了确实高效,但要查看复杂 key 的 value、检查过期时间、统计内存占用,图形化工具更直观。我用过的工具有不少,目前主力是 Another Redis Desktop Manager,免费、跨平台,Windows/macOS 都有版本,支持 Redis Cluster、哨兵模式和 SSH 隧道。官方出的 RedisInsight 也不错,UI 更现代化,还能看节点的实时指标、慢查询分析。真心建议不要再用那些年久失修的老工具,问题多、时常崩溃,影响排障效率。
6.2 排查事故的四个高效命令
图形化工具之外,有些排查必须回到命令行。这里分享四个我高频使用的命令:
redis-cli --bigkeys:扫描所有 key,按数据类型统计最大的 key 和占比。用于排查大 key 问题。大 key 会导致 Redis 阻塞、内存暴涨,是线上事故的头号嫌疑对象。redis-cli --latency -h <host> -p <port>:测试当前客户端到 Redis 服务器的网络延迟。如果延迟太高,先排查网络而不是写代码。redis-cli -p 6379 SLOWLOG GET 10:看最近 10 条慢查询命令。慢命令往往是KEYS *、HGETALL大 hash、SMEMBERS大 set 这类全量操作。redis-cli -p 6379 MONITOR:实时监控所有命令请求。这个命令在测试环境非常有用,但在生产环境要慎用,它会把所有命令刷到终端,瞬间拖垮 Redis 性能,属于“核武器”级别。
6.3 踩坑实录:常见问题速查表
最后把我在 Java、Spring 操作 Redis 过程中踩过的高频问题整理成一张速查表,你可以直接对照排查:
| 现象 | 原因 | 解决方法 |
|---|---|---|
redis-cli 看到 set key 的 value 是二进制乱码 |
RedisTemplate 默认使用 JDK 序列化 |
自定义序列化器,见 3.3 节配置类 |
| 同一个 Redis 实例,Java 写的数据其他语言读不了 | 序列化协议不一致 | key 用 String,value 统一 JSON 字符串 |
| 缓存总是查不到,数据库压力大 | key 设计不一致,比如 Java 写 user:1001,查询时用了 user:001 |
统一 key 生成规范,建议封装一个 RedisKeyUtil |
| 重启应用服务后缓存全丢 | Redis 持久化配置不完善,或缓存过期时间太短 | 按业务区分缓存 TTL,重要数据开启 AOF |
| 分布式锁偶尔失效 | 没有设置 value 标识、没有锁续期 | 改用 Redisson,或者手写 requestId + Lua 脚本 |
| 接口偶发 Redis 超时 | 连接池过小、慢查询命令阻塞 | 调大连接池参数,用 SLOWLOG 找慢命令 |
keys * 在测试环境正常,线上巨卡 |
keys * 会阻塞主线程,key 过多时影响所有命令 |
用 SCAN 分段遍历,禁用 keys * |
6.4 一个小技巧:key 怎么命名才能“不后悔”
很多人一开始不注意 key 命名,后面排查问题痛不欲生。我自己的规范是这样的:业务场景:实体类型:唯一标识[:字段],比如 order:info:1001、user:session:abc123、rank:score:server1。这种命名方式有几大优势:第一,一眼能看出 key 属于哪个业务模块;第二,可以用 SCAN order:info:* 之类的模糊匹配批量排查;第三,在分布式环境中,不同团队维护各自业务前缀,不容易互相覆盖。有一段时间还会在 key 前缀里加环境标识,比如 dev:order:info:1001,但后来发现这会给运维增加不少负担,就不推荐了,环境隔离优先靠独立的 Redis 实例完成,而不是靠 key 前缀。
最后的体会
玩 Redis 这几年,我个人的体会是:不要在工具本身上炫技,而是在架构上省心。能用 String 解决的别硬上 Hash,能用缓存解决的别锁库,能用 Redis 锁解决的别引入一套复杂组件。很多问题不是 Redis 不好,而是没有把序列化、连接池、过期策略、主从架构这些基础环节提前设计好。我第一次用 Redis 是被“看着文档写代码”推着走的,后来踩坑多了才明白,真正决定 Redis 使用体验的,往往是那些写在文档角落里的配置项——序列化器的选择、连接池的大小、锁的续期策略。把这篇内容里的核心配置和排查思路跑一遍,基本能把 Java 和 Spring 环境下的 Redis 从“能跑”提升到“稳定、可查、可扩展”的水平。
