第一次在生产环境里被 Redis 教育,是因为一个商品的库存 Key 忘了设过期时间,凌晨两点报警电话响起来,8G 内存的实例被撑到 OOM,重启之后缓存全部失效,数据库瞬间被打到 100% 连接数。那次事故之后我把 Redis 的使用手册从头到尾啃了一遍,也把踩过的坑一条条记了下来。下面这份内容,就是这些年做 Redis 基本使用的沉淀——包括怎么装、怎么连、五种数据类型到底该用在什么业务上、图形化工具怎么选、Spring Boot 里怎么集成、缓存该怎么设计才不会被自己坑到,以及出问题的时候从哪几个口子去查。适合刚接触 Redis 的后端同学照着抄作业,也适合用了两三年但一直停留在 set/get 层面的人补一下底层逻辑。全文没有玄学,都是我实际跑过的命令和配置,你可以直接拿去改。
1. Redis 环境搭建:从本地到容器的三条线路
1.1 先想清楚你要的是一台"玩具"还是"试验田"
装 Redis 之前先问自己一个问题:这台实例是拿来学命令的,还是要跑业务验证的?这两个诉求对应的装法完全不同。如果你只是想敲几行 set、get 看看效果,Windows 上搞个便携版解压就能跑,五分钟搞定;但如果你要测主从、测持久化、测内存淘汰策略,我建议直接上 Docker,因为 Docker 里换配置、换版本、删库重来都只需要一条命令,不用把系统环境搞得一团糟。
还有一个容易被忽略的点:Redis 官方并不维护 Windows 原生版本,社区有人在维护移植版,版本通常会落后主线不少。所以我的习惯是,本机用容器跑,服务器上用源码编译或者官方镜像。容器方案最大的好处是配置文件的路径清晰,映射出来之后你能直接看到 redis.conf 的每一行,出问题的时候不用猜它是从哪里读的配置。
1.2 Docker 拉起一个可用的实例
我日常最常用的一条命令长这样:
docker run -d --name redis-dev \ -p 6379:6379 \ -v /data/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf \ -v /data/redis/data:/data \ redis:7.2 redis-server /usr/local/etc/redis/redis.conf这里有几个点值得单独说。第一,-v挂载配置文件,前提是你宿主机的 redis.conf 得先准备好,否则容器启动会直接报找不到文件;我一般先从官方镜像里拷一份模板出来:
docker run --rm redis:7.2 cat /usr/local/etc/redis/redis.conf > /data/redis/conf/redis.conf第二,数据目录一定要挂出来,不然容器一删,RDB 和 AOF 文件就跟着没了,这对于做实验的人来说是灾难。第三,端口映射我用的是 6379:6379,如果你本机已经有一个 Redis 在跑,改成 6380:6379 就行,容器内部永远是 6379。
启动之后想进容器敲命令:
docker exec -it redis-dev redis-cli1.3 配置文件里必须改的几个参数
默认的 redis.conf 是"开发友好、生产不友好"的,直接拿去用迟早出事。下面这几个参数是我每次都会动的:
| 参数 | 默认值 | 建议值 | 为什么要改 |
|---|---|---|---|
| bind | 127.0.0.1 | 按需放开内网地址 | 默认只监听本机,容器或跨机访问连不上 |
| protected-mode | yes | 开启密码后保持 yes | 无密码且对外暴露时会被扫,风险很高 |
| requirepass | 注释状态 | 设置强密码 | 生产环境裸奔等于把数据送人 |
| daemonize | no | 容器里保持 no | 容器里守护进程化会导致容器直接退出 |
| maxmemory | 0(不限) | 按物理内存 60%~70% | 不限制就会吃满内存触发系统 OOM |
| maxmemory-policy | noeviction | allkeys-lru 或 volatile-lru | 内存满了默认是写报错,业务会直接失败 |
| appendonly | no | yes | 只用 RDB 会丢最后一段时间的数据 |
maxmemory这个值很多人喜欢设成物理内存的 100%,这是典型的新手坑。Redis 在做 RDB 持久化的时候会 fork 子进程,fork 的瞬间需要复制页表,虽然是写时复制,但写入量大的时候内存占用会接近翻倍。留出 30% 的余量,就是为了防止 fork 的时候被系统直接杀掉。
1.4 启动后的第一轮自检
服务起来之后别急着写业务,先用几条命令确认它是健康的:
redis-cli -h 127.0.0.1 -p 6379 -a yourpassword ping # 返回 PONG 说明通了 redis-cli -h 127.0.0.1 -p 6379 -a yourpassword info server # 看 redis_version、uptime_in_seconds、config_file redis-cli -h 127.0.0.1 -p 6379 -a yourpassword config get maxmemory # 确认配置文件真的生效了,而不是被启动参数覆盖config_file这个字段特别有用,它告诉你实例到底加载的是哪个配置文件。我遇到过好几次"改了配置不生效"的情况,最后发现是启动的时候命令行又传了一遍参数,把文件里的值覆盖了。
注意:用
-a传密码会在命令行历史里留痕,生产机器上建议用REDISCLI_AUTH环境变量,或者干脆走redis-cli交互模式里auth。
2. 五种基础数据类型与真实业务映射
2.1 String:不只是缓存,更是计数器和锁的底座
String 是 Redis 里最基础也最容易被用窄的类型。大部分人只拿它做对象缓存,其实它在计数场景下才是真的香。
SET user:1001:name "张三" GET user:1001:name SETEX sms:code:13800000000 300 "482913" TTL sms:code:13800000000 INCR article:1001:views INCRBY article:1001:views 10 SETNX lock:order:2001 "uuid-abc"INCR是原子的,这一点非常重要。你用数据库做计数器,一并发就得加行锁;用 Redis 的 INCR,十万 QPS 也不会出现计数丢失。这就是为什么秒杀库存扣减、文章阅读量、接口限流这些场景全都往 Redis 上放。
SETEX是"设置值 + 设置过期时间"的原子组合,验证码场景必用。很多人写成先 SET 再 EXPIRE,中间如果进程挂了,这个 Key 就变成永久键了,日积月累就是内存泄漏。
SETNX是分布式锁的雏形,但它有两个缺陷:一是不能设过期时间(后面可以用SET key value NX PX 30000一步到位),二是释放锁的时候不能直接 DEL,否则可能删掉别人的锁。这个在后面第 5 节会详细展开。
2.2 Hash:对象存储和局部更新的正确姿势
把一个用户对象整体序列化成 JSON 塞进 String,改一个字段就要读出整个对象、反序列化、改、再序列化写回——这个流程在字段多、写频繁的场景下非常浪费。
Hash 解决的正是这个问题:
HSET user:1001 name "张三" age 28 city "杭州" HGET user:1001 name HGETALL user:1001 HINCRBY user:1001 age 1 HDEL user:1001 cityHINCRBY可以直接对某个字段做原子自增,做"用户积分"这种场景就特别合适,不用整对象来回搬。HGETALL要慎用,如果这个 Hash 有几千个字段,一次拉回来会阻塞主线程,正确做法是用HSCAN分批取。
Hash 还有一个隐藏优势:小 Hash 在 Redis 内部用的是 ziplist(新版本叫 listpack)编码,内存利用率比一堆独立 String Key 高很多。同样存 100 万个用户的三四个字段,用 Hash 比用 100 万个 String Key 能省下相当可观的内存。判断编码用什么命令呢:
OBJECT ENCODING user:1001返回listpack说明是小对象紧凑存储,返回hashtable说明字段数或值长度超过了阈值(默认 field 数超过 128 或单个 value 超过 64 字节就转成 hashtable)。
2.3 List:队列、时间线与有限长度陷阱
List 是双向链表结构,两头进出都快。最常见的用法是做简单消息队列:
LPUSH queue:email "task-001" RPOP queue:email BRPOP queue:email 30BRPOP是阻塞版本,队列空的时候会挂起等待,超时返回 nil,比轮询省资源得多。不过要提醒一句,用 List 做队列,消费者拿到消息之后如果处理失败,消息就丢了,没有 ACK 机制。要求可靠性高的场景,得上 Stream 或者专业消息队列,别硬扛。
List 的另一个高频用法是"只保留最近 N 条"的时间线:
LPUSH feed:user:1001 "post-9527" LTRIM feed:user:1001 0 99LTRIM是很多人不知道的命令,它把 List 裁剪成指定区间。上面这两条组合起来,就是一个永远只保留最新 100 条动态的时间线。如果你不做 LTRIM,这个 List 会无限增长,最后变成 bigkey,删都删不掉——因为DEL一个几百万元素的 List 会阻塞主线程好几秒。
提示:删除大 Key 的正确方式是
UNLINK,它在后台线程里回收内存,不阻塞主线程。
2.4 Set 与 ZSet:去重、交并集和排行榜
Set 是无序去重集合,典型场景是标签、抽奖去重、共同好友:
SADD user:1001:tags "java" "redis" "mysql" SISMEMBER user:1001:tags "redis" SMEMBERS user:1001:tags SINTER user:1001:tags user:1002:tags SCARD user:1001:tagsSINTER求交集就是"共同关注",SUNION求并集是"合并标签",SDIFF求差集是"我关注了他没关注"。这些运算在 Redis 内部是纯内存操作,比在数据库里写 join 快非常多。
ZSet 是我个人认为设计最精妙的类型,每个元素带一个 score,按 score 排序:
ZADD rank:weekly 100 "user:1001" ZADD rank:weekly 250 "user:1002" ZINCRBY rank:weekly 50 "user:1001" ZREVRANGE rank:weekly 0 9 WITHSCORES ZRANK rank:weekly "user:1001"做排行榜只需要这几条命令,而且取 Top10、查某人的排名都是 O(logN)。用 ZSet 还能做延迟队列:score 存执行时间戳,消费者用ZRANGEBYSCORE key 0 now LIMIT 0 10捞到期任务。这个方案我在小体量项目里用过,比引入消息中间件轻量太多。
2.5 键命名规范与过期策略
Key 的命名我坚持一条原则:用冒号分层,业务前缀在最前。比如order:20240501:detail:2001。这样做的好处是,用可视化工具按前缀搜索的时候能一眼看出归属,做统计和批量删除也方便。
过期时间必须显式设置,这是硬规矩。缓存类 Key 一定要有 TTL,而且要加随机抖动,比如基础 30 分钟加上 0 到 5 分钟的随机值。全都设成同一个时间点,到期那一刻数据库会被瞬间打穿,这就是典型的缓存雪崩。至于 TTL 怎么定,我的经验是看数据的更新频率:更新极其频繁的设 1 到 5 分钟,变化不多的设 30 分钟到几小时,几乎不变的可以设一天以上,但绝不设永久。永久 Key 是内存泄漏的头号来源。
3. 客户端与可视化管理工具选型
3.1 命令行才是主力,图形工具是辅助
我见过有人只用图形化工具操作 Redis,结果连SCAN都不会用。我的态度很明确:命令行客户端必须是主力,图形工具只是用来"看"的。原因很简单,图形工具点一下背后执行的命令你不清楚,压力大的时候一个全量查询就能把线上搞出事故。
redis-cli有几个我常用的姿势:
# 交互模式下看某个前缀的所有 Key,用 SCAN 而不是 KEYS redis-cli --scan --pattern "order:*" --count 100 # 统计大 Key,生产环境慎用,最好在从库上跑 redis-cli --bigkeys # 查看某个 Key 占用的内存 redis-cli memory usage user:1001 # 监控命令执行,只在排查问题时短时间开启 redis-cli monitorKEYS *这条命令我要单独拎出来说。它在数据量大时会遍历整个键空间,因为是单线程模型,执行期间所有其他请求全部被阻塞。线上敲一次KEYS *,可能就是一次故障。替代方案永远是SCAN,它用游标分批返回,每次只扫一小部分,不会长时间占用主线程,代价是可能返回重复元素,需要业务侧自己去重。
3.2 图形化工具怎么选
可视化工具这块,我现在主要是两个搭配使用。RedisInsight 是官方出的,对内存分析、慢日志、Pub/Sub 的支持比较全面,界面偏工程化,适合排查问题时看细节。另一个是 Another Redis Desktop Manager,轻量、启动快、支持多标签页和树形展示,日常查看 Key、临时改个值我更喜欢用它,因为它打开就是秒开,不带一堆分析面板。
选择的时候我一般看三点:一是能不能正确展示中文和二进制值(有些工具序列化处理不好会显示乱码);二是支不支持 SCAN 分页而不是一上来就全量拉取;三是连接配置能不能保存和导出。第三点在做多环境切换的时候特别省事,测试、预发、生产的连接各存一份,不用每次手敲。
注意:生产环境的图形工具连接一定要走只读账号或者从库,避免手滑删掉线上 Key。Redis 没有回收站,删了就真没了。
3.3 连接池配置比想象中更重要
客户端连接 Redis 有两种模式:直连和连接池。业务代码里绝对不要每次请求都新建连接,TCP 三次握手加认证的开销在高并发下非常可观。
以 Java 生态为例,Lettuce 是 Spring Boot 2.x 之后的默认客户端,它基于 Netty,是线程安全的,多个线程共享同一个连接。配置上重点看这几个参数:
| 参数 | 说明 | 经验值 |
|---|---|---|
| max-active | 最大连接数 | 按并发量的 1/10 估算,通常 16~64 |
| max-idle | 最大空闲连接 | 与 max-active 接近 |
| min-idle | 最小空闲连接 | 不低于 4,避免冷启动抖动 |
| max-wait | 获取连接最大等待时间 | 设 500ms 到 2s,不要设 -1(无限等) |
| timeout | 命令执行超时 | 1s 到 3s,超过就该走降级 |
max-wait设成 -1 是我见过最危险的做法,一旦连接池被打满,请求会无限阻塞,线程池很快被拖死,整个服务雪崩。宁可快速失败返回降级数据,也不要让线程卡在那里。
4. Spring Boot 集成 Redis 的完整落地
4.1 依赖引入与配置文件写法
Spring Boot 集成 Redis 的起步非常快,加依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>然后写配置:
spring: data: redis: host: 127.0.0.1 port: 6379 password: yourpassword database: 0 timeout: 2000ms lettuce: pool: max-active: 32 max-idle: 16 min-idle: 4 max-wait: 1000ms注意 Spring Boot 2.x 的配置前缀是spring.redis,3.x 改成了spring.data.redis,这个坑我替你们踩过了——版本升上去之后配置没改,连的还是默认 localhost,排查了半天才发现是前缀变了。
database这个参数在单机模式下可以选 0 到 15,一共 16 个库。但我要提醒:不同业务用不同 db 隔离这种做法,在集群模式下是行不通的,因为集群只支持 db0。所以从一开始就养成用 Key 前缀区分的习惯,别依赖 db 隔离。
4.2 序列化选型:默认 JDK 序列化必须换掉
这是集成环节最关键的一步。Spring Boot 自动装配的RedisTemplate默认用 JdkSerializationRedisSerializer,问题有三个:序列化出来的内容是一堆不可读的二进制,图形化工具里看就是乱码;占空间比 JSON 大不少;跨语言完全没法读。
正确做法是手动定义 Template,Key 用 String 序列化,Value 用 JSON 序列化:
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); StringRedisSerializer keySerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); return template; } }GenericJackson2JsonRedisSerializer会在 JSON 里额外写入@class字段记录类型信息,反序列化时能自动还原成原对象。代价是体积稍大,而且如果类名改了,老数据就反序列化失败。如果对体积敏感,可以换成自己配置的Jackson2JsonRedisSerializer,但那样就得在每个读取点显式指定类型。
提示:如果只是做字符串缓存,直接用
StringRedisTemplate更省事,不用任何额外配置,而且和图形化工具配合最友好。
4.3 常用封装与 Hash 操作
实际项目里我一般会再包一层工具类,把常用操作收口,避免到处散落redisTemplate.opsForValue():
@Component public class RedisService { @Autowired private StringRedisTemplate redisTemplate; public void set(String key, String value, Duration ttl) { redisTemplate.opsForValue().set(key, value, ttl); } public String get(String key) { return redisTemplate.opsForValue().get(key); } public boolean setIfAbsent(String key, String value, Duration ttl) { return Boolean.TRUE.equals( redisTemplate.opsForValue().setIfAbsent(key, value, ttl)); } public void hSet(String key, String field, String value) { redisTemplate.opsForHash().put(key, field, value); } public void expire(String key, Duration ttl) { redisTemplate.expire(key, ttl); } }setIfAbsent带 TTL 的重载版本很关键,它就是SET key value NX PX的封装。不带 TTL 的那个版本别用在锁场景,否则一旦释放逻辑没走到,锁就永远留在那里了。
4.4 注解缓存 @Cacheable 的坑
@Cacheable用起来很爽,但默认配置下它会用 JDK 序列化,而且不设过期时间。所以必须自己配 CacheManager:
@Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) .computePrefixWith(name -> "cache:" + name + ":") .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues(); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); }这里有两个细节值得说。computePrefixWith用来给不同的缓存名加统一前缀,方便可视化工具里按前缀过滤。disableCachingNullValues表示不缓存空值,能防止内存里塞满 null 占位;但反过来,如果你要防缓存穿透,就需要主动缓存空值,这时候把它去掉,同时给空值单独设一个短 TTL。
还有一个经典坑:@Cacheable是代理生效的,类内部方法自调用不会走缓存。同一个类里 A 方法调 B 方法,B 上的@Cacheable是失效的,这个必须靠拆分 Bean 或者注入自身代理来解决。
5. 缓存设计与高并发下的治理思路
5.1 穿透、击穿、雪崩三兄弟的成因与对策
这三个概念面试问得最多,但很多人只是背答案。我结合实际场景讲一遍。
缓存穿透是查一个数据库里根本不存在的数据,缓存里没有,每次都打到数据库。恶意攻击者用一个不存在的 ID 疯狂请求,数据库就顶不住了。对策有两个:一是缓存空值并设置短 TTL,比如 60 秒;二是用布隆过滤器在入口层拦截,把所有存在的 ID 预先放进去,查询前先判断,不存在直接返回。
缓存击穿是某个热点 Key 突然过期,瞬间大量请求同时打到数据库。注意关键词是"单个热点 Key"。对策是加互斥锁,只让一个线程去重建缓存,其他线程短暂等待后重试。伪代码大致是:
String value = redis.get(key); if (value == null) { if (redis.setIfAbsent("lock:" + key, "1", Duration.ofSeconds(5))) { try { value = loadFromDb(key); redis.set(key, value, Duration.ofMinutes(30)); } finally { redis.delete("lock:" + key); } } else { Thread.sleep(50); return getWithCache(key); } } return value;缓存雪崩是大量 Key 在同一时刻集中过期,或者 Redis 实例整体挂掉。对策分两层:过期时间加上随机抖动,把压力摊开;同时做好降级,Redis 不可用时走本地缓存或者直接读库并限流。熔断降级这块别省,我经历过一次 Redis 集群网络抖动,因为没有降级逻辑,整个订单链路全军覆没。
5.2 热点 Key 与 bigkey 的发现与拆分
bigkey 的定义没有绝对标准,我的经验线是:String 超过 10KB,集合类元素超过 5000 个,就得注意了。危害主要在删除和过期的时候——主线程回收内存是同步的,一个几 MB 的 Key 删掉能阻塞上百毫秒。
排查手段:
redis-cli --bigkeys redis-cli memory usage your:key redis-cli --scan --pattern "your:prefix:*" --count 1000拆分思路按业务定:Hash 按 field 分片成多个 Key;List 和 ZSet 按时间或者 ID 取模分桶。分桶之后注意,聚合查询就得在应用层做了,这是一笔额外的复杂度账,值不值得要看你实际的 Key 大小。
热点 Key 是另一个维度的问题,指的是访问量极度集中在单个 Key 上。由于 Redis 单线程处理命令,一个 QPS 十万的热点 Key 会成为瓶颈。对策是做多级缓存,在应用本地用 Caffeine 挡一层,把 Redis 的访问量降下来;或者把热点 Key 复制成多份,请求时随机选一个读。
5.3 分布式锁的正确实现方式
分布式锁的基础版本必须一次成型,别分两步:
SET lock:order:2001 "uuid-abc-123" NX PX 30000三个要点:NX保证只有第一个请求能设置成功;PX 30000保证即使持锁方挂了,锁也会在 30 秒后自动释放;value 存一个唯一 ID,是为了释放的时候校验是不是自己的锁。
释放锁必须用 Lua 脚本保证"判断 + 删除"的原子性:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end为什么要校验 value?因为存在这种情况:A 拿到锁执行了 35 秒,锁在 30 秒时自动过期,B 拿到了锁;这时 A 执行完去删锁,删掉的其实是 B 的锁。校验唯一值就能避免误删。
如果业务逻辑执行时间不确定,手写续期太麻烦,我建议直接用 Redisson,它的看门狗机制会在持锁期间自动续期,而且可重入、支持公平锁。代价是引入一个依赖,锁的语义也变复杂了,得先读懂再用。
注意:主从架构下,主节点写入锁之后还没同步到从节点就宕机了,新主节点上锁就丢了。对一致性要求极高的场景,得用 RedLock 或者干脆用数据库的唯一约束兜底。别在关键资金链路上只靠单机 Redis 锁。
5.4 内存淘汰策略与持久化选择
maxmemory-policy选哪个,取决于你的数据能不能丢:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| noeviction | 内存满时写操作报错 | 当数据库用,数据不能丢 |
| allkeys-lru | 所有 Key 中淘汰最近最少使用 | 纯缓存场景,最常用 |
| volatile-lru | 只在设了过期时间的 Key 中淘汰 | 缓存和持久数据混存 |
| allkeys-random | 随机淘汰 | 访问分布均匀,无所谓冷热 |
| volatile-ttl | 优先淘汰剩余时间短的 | 有明确时效的数据 |
我大部分缓存项目用的是allkeys-lru。注意 lru 在 Redis 里是近似实现,采样几个 Key 挑最久没用的,不是精确 LRU,但对缓存场景足够了。
持久化方面,RDB 是快照,恢复快但会丢最后一次快照之后的数据;AOF 是追加日志,最多丢 1 秒(appendfsync everysec),但文件大、恢复慢。我的做法是生产环境两个都开,定期用 RDB 做冷备,日常靠 AOF 保证数据完整性。如果只是做纯缓存,数据丢了能重建,那 RDB 单独用也够,省磁盘 IO。
6. 运维排查:常见问题与速查表
6.1 连不上、超时、报错的排查路径
连不上 Redis 是最常见的求助,我一般按这个顺序查:
第一步,确认服务在不在。ps -ef | grep redis看进程,netstat -anp | grep 6379看端口有没有监听。容器环境下用docker logs 容器名看有没有启动失败。
第二步,确认网络通不通。从客户端机器上telnet 目标IP 6379,不通就是网络或者防火墙问题,跟 Redis 本身无关。
第三步,确认认证和配置。报NOAUTH Authentication required就是没带密码;报DENIED Redis is running in protected mode是保护模式拦住了,通常是没设密码又绑了非本机地址;报Connection refused是端口没监听或者防火墙拦了。
第四步,看客户端日志。连接池超时(Could not get a resource from the pool)通常是连接数打满或者某个命令执行太慢卡住了连接。这时候去服务端CLIENT LIST看连接状态,看有没有大量的cmd=blpop这种阻塞命令占着连接。
6.2 慢查询、日志与监控
Redis 有内置的慢日志,配置项是slowlog-log-slower-than,单位微秒,默认 10000 也就是 10 毫秒。我的建议是生产环境设成 5000 甚至 2000,因为 Redis 处理命令通常在微秒级别,超过 5 毫秒已经算慢了。
redis-cli config set slowlog-log-slower-than 5000 redis-cli config set slowlog-max-len 512 redis-cli slowlog get 10慢日志里能看到命令、耗时、客户端地址,排查的时候非常直接。我碰到过的典型慢命令包括:KEYS *、HGETALL大对象、SMEMBERS大集合、DEL大 Key、以及一次性的MGET几百个 Key。
日常监控看INFO的几个关键指标:used_memory(已用内存)、connected_clients(连接数)、instantaneous_ops_per_sec(瞬时 QPS)、keyspace_hits和keyspace_misses(命中率)、evicted_keys(被淘汰的 Key 数量)。命中率明显下降或者淘汰量突然飙升,都是缓存策略要调整的信号。
MONITOR命令可以实时看到所有执行的命令,但它是同步阻塞的,会显著降低 Redis 吞吐,只能在开发环境或者线上排查时开几秒钟,用完立刻 ctrl+c。
6.3 常见问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 内存持续上涨不降 | 存在无 TTL 的 Key | 用 SCAN 配合 TTL 排查,补上过期时间 |
| 写入报 OOM command not allowed | 内存达到 maxmemory 且策略为 noeviction | 调大内存或改用 lru 策略 |
| 大量 Key 同时失效 | 过期时间集中,雪崩前兆 | 加随机抖动,分批预热 |
| 命中率低 | Key 设计不合理或 TTL 太短 | 分析访问模式,调整粒度和过期时间 |
| 连接池频繁超时 | 慢命令阻塞或连接数配置过小 | 查慢日志,调大 max-active |
| 主从延迟变大 | 写入量超过同步带宽或从库阻塞 | 看master_link_status,排查从库慢命令 |
| RDB 落盘失败 | 磁盘满或 fork 内存不足 | 清理磁盘,调小 maxmemory |
| 命令返回 MOVED | 客户端连的是集群但走单机模式 | 使用支持集群的客户端并配置节点列表 |
6.4 我实际踩过的几个坑
第一个坑是序列化不一致。项目里一个模块用StringRedisTemplate写,另一个模块用默认序列化的RedisTemplate读,结果双方都读不到数据,但 Key 明明存在。排查思路就是打开可视化工具看 Key 的值到底长什么样,发现一边是纯字符串一边是带\xac\xed前缀的二进制,问题就清楚了。所以团队里一定要统一序列化方式,写进规范。
第二个坑是@Transactional和缓存的顺序。事务还没提交就更新缓存,其他线程可能读到新缓存但数据库还是老数据,出现短暂不一致。解决办法是把缓存操作放到事务提交之后,用TransactionSynchronizationManager注册回调,或者用@TransactionalEventListener监听提交事件。这个细节不写出来,很多人会一直以为是"缓存偶尔抽风"。
第三个坑是扫描大 Key 时把线上搞慢。我在业务高峰跑--bigkeys,它内部用的就是 SCAN 加 TYPE,虽然不阻塞,但会额外占用 CPU 和网络,导致那段时间的响应时间抖动明显。后来的做法是固定到业务低峰执行,或者直接在从库上跑。
第四个坑是误以为expire能延长已有 TTL。实际上EXPIRE是覆盖式设置,不是叠加。想延长应该先TTL查当前剩余时间再加。这个理解偏差导致我做"访问即续期"的逻辑时,把一些本该 5 分钟过期的会话延长到了 30 分钟,内存占用翻了好几倍。
最后一个体会是关于选型心态的。Redis 用起来太顺手,很容易让人产生"什么事都往 Redis 上放"的冲动,比如拿它当唯一存储、拿它做复杂事务、拿它做大数据量聚合。我的经验是,Redis 的定位就是高速缓存和轻量数据结构服务,超出这个范围的诉求,要么在应用层补逻辑,要么换合适的存储。想清楚这一点,很多架构上的纠结其实就迎刃而解了。