Redis 使用全景与避坑手册:数据类型、Spring Boot 集成及缓存治理

Redis 使用全景与避坑手册:数据类型、Spring Boot 集成及缓存治理 第一次在生产环境里被 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 是开发友好、生产不友好的直接拿去用迟早出事。下面这几个参数是我每次都会动的参数默认值建议值为什么要改bind127.0.0.1按需放开内网地址默认只监听本机容器或跨机访问连不上protected-modeyes开启密码后保持 yes无密码且对外暴露时会被扫风险很高requirepass注释状态设置强密码生产环境裸奔等于把数据送人daemonizeno容器里保持 no容器里守护进程化会导致容器直接退出maxmemory0不限按物理内存 60%~70%不限制就会吃满内存触发系统 OOMmaxmemory-policynoevictionallkeys-lru 或 volatile-lru内存满了默认是写报错业务会直接失败appendonlynoyes只用 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-abcINCR是原子的这一点非常重要。你用数据库做计数器一并发就得加行锁用 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~64max-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 groupIdorg.springframework.boot/groupId artifactIdspring-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.redis3.x 改成了spring.data.redis这个坑我替你们踩过了——版本升上去之后配置没改连的还是默认 localhost排查了半天才发现是前缀变了。database这个参数在单机模式下可以选 0 到 15一共 16 个库。但我要提醒不同业务用不同 db 隔离这种做法在集群模式下是行不通的因为集群只支持 db0。所以从一开始就养成用 Key 前缀区分的习惯别依赖 db 隔离。4.2 序列化选型默认 JDK 序列化必须换掉这是集成环节最关键的一步。Spring Boot 自动装配的RedisTemplate默认用 JdkSerializationRedisSerializer问题有三个序列化出来的内容是一堆不可读的二进制图形化工具里看就是乱码占空间比 JSON 大不少跨语言完全没法读。正确做法是手动定义 TemplateKey 用 String 序列化Value 用 JSON 序列化Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, 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 序列化而且不设过期时间。所以必须自己配 CacheManagerBean 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 分片成多个 KeyList 和 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看连接状态看有没有大量的cmdblpop这种阻塞命令占着连接。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 吞吐只能在开发环境或者线上排查时开几秒钟用完立刻 ctrlc。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 的定位就是高速缓存和轻量数据结构服务超出这个范围的诉求要么在应用层补逻辑要么换合适的存储。想清楚这一点很多架构上的纠结其实就迎刃而解了。