Redis实战入门:从缓存原理到Spring Boot集成与分布式锁
很多零基础的同学跑来问我Redis 到底该怎么入门背了一堆命令看不懂项目里怎么用搜教程又总在“原理”和“代码”之间断层。这篇文章我就直接用一套完整的实战线来讲透从缓存原理——也就是高性能背后的设计逻辑到 Spring Boot 集成——包括序列化、缓存注解、分布式锁和常见报错排查全部串起来。如果你是刚学完 Spring Boot 的后端新手或者准备面试被问“缓存三兄弟”的 Java 开发又或者正在做电商、推荐系统这类高并发项目这篇都值得跟下去。我自己带项目时反复强调一句话中间件学得好不好不在命令背得多而在出了问题能不能推演因果。Redis 恰恰是最适合用来建立这种“因果感”的中间件因为它功能专注、玩法清晰把内存、单线程、IO多路复用、持久化这些底层概念吃透了你再看 Spring Boot 里的集成代码基本就是看说明书。1. 零基础学 Redis先搞懂它到底解决什么问题1.1 一个真实场景数据库被读爆了先模拟一个非常常见的电商项目。商品详情页的用户访问频率远高于下单频率一台数据库在峰值期可能每秒被请求几千次但数据库的每一次查询都要走“网络解析、SQL解析、磁盘IO、返回结果”这条链路。磁盘IO的物理瓶颈摆在那里连接数一多数据库CPU和连接池迅速打满页面开始转圈。这时候我们把商品详情数据复制一份放到 Redis 里Redis 把数据存在内存中读取一个 key 通常只需要微秒级。用户请求过来先查 Redis命中就直接返回只有缓存没有命中才去查数据库再回填缓存。数据库的查询压力被砍掉一大半整个系统的吞吐量立刻上来了。这就是 Redis 在系统架构里最常见的角色位于业务层和持久化数据库之间的一款高速中间件。除了做缓存它还经常承担分布式锁、排行榜、计数器、简单消息队列等职责。理解了它“夹在中间做缓冲”的定位后面学什么都不会歪。1.2 Redis 凭什么能扛住高并发很多人对“高并发”有误解以为它是凭空变出来的能力。Redis 的快主要有三个原因必须逐个理解。第一数据全在内存。内存随机访问延迟在几十纳秒而磁盘随机访问通常要几毫秒差距接近十万倍。这就好比你会把最常用的电话号码记在脑子里而不是每次翻通讯录。第二单线程模型。Redis 的核心命令执行是单线程的好处是避免了多线程切换和锁竞争的开销。你可能会疑惑“单线程还能高并发”没错它靠的是 IO 多路复用——一个线程同时监听大量客户端连接谁有请求就处理谁。可以类比成只有一个服务员的小餐馆服务员拿着本子记下所有桌子的点菜需求谁喊结账就先服务谁不用为了每桌单独派一个人。第三协议简单、数据结构高效。Redis 的请求响应模型非常紧凑核心又围绕着哈希表、跳表这类高性价比结构省掉了传统数据库解析 SQL 和事务管理的复杂度。1.3 给零基础画一条学习路线我不建议零基础同学一上来就刷命令手册而是按下面这条路线走60% 的精力能落在实际项目里先学五大基本数据类型每种类型配合一个业务场景去记忆。搞懂过期策略、内存淘汰和持久化机制这些是缓存原理的地基。引入 Spring Boot 项目掌握 RedisTemplate 配置与缓存注解。再进阶到缓存穿透、击穿、雪崩以及分布式锁这些直接对标面试和高并发设计。最后了解主从、哨兵、集群和容器化部署能动手搭一套开发环境即可。跟着这条线走你基本不会出现“学完 Redis 还是不知道在项目里怎么用”的尴尬。2. 本地环境搭建Windows、macOS、Linux 一条龙2.1 Windows 安装绿色免安装版本最省心Windows 上用安装包容易污染系统环境我更推荐直接到 Redis 官网下载 zip 版本现在 Windows 安装包也有官方构建。解压后你会看到 redis-server.exe 和 redis-cli.exe 等文件。打开命令行进入解压目录先启动服务端redis-server.exe看到“Ready to accept connections”说明启动成功。新开一个终端窗口测试连通性redis-cli.exe ping返回 PONG 就表示客户端和服务端正常通信了。需要长期运行的话可以注册为 Windows 服务redis-server.exe --service-install redis.windows.conf redis-server.exe --service-start注意如果用了默认配置Redis 只监听本机 127.0.0.1端口 6379保护模式默认开启。本地学习不用改但如果虚拟机或容器里要访问就得调整 bind 和 protected-mode。2.2 macOS 安装一行命令搞定macOS 上最直接的方式是用 Homebrewbrew install redis brew services start redis如果你只想在前台跑不想注册为开机服务可以用redis-server /usr/local/etc/redis.conf然后用 brew services list 查看服务状态。装完同样是 redis-cli ping 验证。macOS 上最常见的坑是端口被系统自带的某个进程占用如果启动报错说 6379 被 bind可以用 lsof -i :6379 查一下是谁在用。2.3 使用 Docker 快速创建开发环境现在团队开发基本都默认 Docker。本地起一个 Redis 7 开发环境只要一条命令docker run -d --name redis-dev -p 6379:6379 redis:7-alpine如果需要持久化可以挂载数据目录并指定配置文件docker run -d --name redis-dev \ -p 6379:6379 \ -v /myredis/data:/data \ -v /myredis/conf/redis.conf:/etc/redis/redis.conf \ redis:7-alpine redis-server /etc/redis/redis.conf用 Docker 跑 Redis 最大的好处是你可以在几分钟内同时拉起主节点、从节点甚至一整个集群坏了直接删容器重建完整复现生产问题。后面讲主从和集群我还会用 docker-compose 做演示。2.4 可视化客户端怎么选命令行玩熟了可以配一个可视化工具提高效率。我这些年实际用下来最顺手的是这三款工具特点适用场景Another Redis Desktop Manager开源免费、跨平台、支持多语言日常开发调试首选RedisInsightRedis 官方出品、支持 RedisJSON 等模块、内置分析面板诊断大 key、内存分析Redis Desktop Manager老牌工具新版本收费老版本可用老项目历史习惯连接时不外乎填 host、port、password 三项。我特别提醒一句可视化工具里看到 key 出现 \xac\xed 开头的一堆乱码十有八九是 Java 端用了 JDK 默认序列化不是工具坏了这个问题我们放在 Spring Boot 集成那节专门解决。3. 五个核心数据类型命令与业务场景对照3.1 string缓存、计数器、限流的主力string 是 Redis 最基础也最高频的类型存 JSON 字符串、验证码、计数器和分布式锁都用得上。常用命令其实不多SET、GET、DEL、SETNX、INCR、DECR、EXPIRE、TTL。做计数器是 string 的经典场景。比如用户签到、商品点击量直接 INCR 一个 key 就能自增性能远好于先 GET 再 SETINCR article:read:1001注意 INCR 是原子操作。多个请求并发自增时不会出现互相覆盖的问题因为 Redis 命令本身就是串行执行的。做验证码时配合过期时间SET phone:verify:13812345678 8899 EX 120EX 表示 120 秒后自动删除比手动 DEL 安全得多。判断 key 是否存在但不能覆盖时用 SETNX这是后面分布式锁的基础。3.2 hash对象结构化存储比 string 更省心hash 适合存“一个对象的多字段”比如用户信息、商品扩展属性。一个 key 对应一个 hash里面的 field 对应对象字段HSET user:1001 name zhangsan age 25 city beijing HGET user:1001 name HGETALL user:1001Java 里用 RedisTemplate 操作 hash天然对应 Map 结构。为什么比 string 存整段 JSON 好因为你可以只修改或读取其中某个字段不用把整个对象取出来反序列化再写回去。针对用户积分这种高频小字段更新用 HINCRBY 就能局部自增HINCRBY user:1001 score 10我在商城项目里就喜欢用 hash 存购物车每个用户一个 key商品 skuId 做 field购买数量做 value增删改查都非常直观。3.3 list有序队列的最简实现list 本质是双向链表支持头尾插入和弹出。LPUSH 从左侧插入RPUSH 从右侧插入LPOP 和 RPOP 取出数据。经典用途是“最新消息列表”和轻量消息队列。比如一个简单的任务队列生产者 LPUSH 任务消费者 BRPOP 阻塞弹出LPUSH task:queue job-1 BRPOP task:queue 0BRPOP 的 B 是 blocking最后一个参数 0 表示一直阻塞等待。注意Redis 的 list 队列并不支持消费者确认机制也带不来复杂路由生产环境真正靠谱的队列还是得用 MQ。把 list 当队列只适合内部小任务、异步化处理等简单场景。3.4 set 和 zset去重与排序两件套set 是无序集合适合做去重、交集、并集。抽奖系统里防止同一用户重复中奖直接 SADD user:lottery:1 1001返回值是 1 表示新增返回 0 表示已存在连判断都省了。求两个用户的共同好友SINTER user:1001:friends user:1002:friendszset 则是带分数的有序集合每个成员有个 scoreRedis 按 score 排序。排行榜、热门商品榜单就是 zset 的天下ZADD rank:game 100 playerA ZADD rank:game 90 playerB ZREVRANGE rank:game 0 9 WITHSCORES上面命令取分数最高的前 10 名。zset 底层是跳表加哈希表插入和查询都是 O(logN)数据量大时也很稳。我团队里的直播项目就是用它做礼物榜更新用户分数用 ZINCRBY一条命令完成加分和重新排序。3.5 数据类型怎么选才对很多人学完数据结构再看业务还是不知道选哪个。我提供一个简单判断法单条简单值用 string对象多字段用 hash有序追加用 list唯一性判断用 set需要按分数排序用 zset。选错了类型最直接的后果是代码绕路比如用 string 存整个对象更新一个字段都要整读整写网络开销大且容易产生并发覆盖。4. 缓存原理拆解过期删除、内存淘汰与持久化4.1 单线程模型为什么还要学一学你要明白 Redis 不是所有操作都单线程严格来说是“命令执行”单线程而持久化、异步删除等任务会由后台线程处理。单线程模型给开发带来的最大价值是你不用考虑命令之间的并发竞争所有命令天然串行执行。这也是为什么 INCR、SETNX 这类原子操作在并发场景里特别香。理解单线程还能帮你形成排查思路如果 Redis 执行了一大堆耗时命令比如 KEYS * 扫描全库或者阻塞在对象序列化上整个实例的所有请求都会排队变慢。后面讲超时报错时这个意识非常关键。4.2 过期键删除策略惰性删除加定期删除给 key 设置 TTL 后Redis 并不是“时间一到立即扫描删除”而是组合了两种策略惰性删除每次访问 key 时发现已过期则删除。好处是省 CPU坏处是一批过期 key 如果不被访问会一直占用内存。定期删除后台周期性地抽取一部分带过期时间的 key 进行检查删除其中已过期的。这是一种折中避免了一次性扫描全库的卡顿。所以你会看到明明设置了过期时间内存却还是被过期 key 占着的情况。这时候有两种处理思路一是内存压力紧张就靠下面的淘汰策略强制清理二是对某些大 key 考虑主动清理或迁移冷数据。4.3 八种内存淘汰策略生产怎么选当 Redis 内存达到 maxmemory 上限会按配置的 maxmemory-policy 进行淘汰。常见策略如下策略淘汰逻辑适合场景noeviction不淘汰写命令直接报错数据必须全量的场景allkeys-lru所有 key 按最近最少使用淘汰常规缓存场景推荐volatile-lru仅在设置了过期时间的 key 里淘汰最近最少使用同时存在长期 key 和过期 keyallkeys-lfu所有 key 按最低访问频率淘汰访问频率差异明显的热点数据volatile-lfu仅在设置了过期时间的 key 里淘汰最低访问频率同上但保留长期 keyallkeys-random所有 key 随机淘汰热点不明显的场景volatile-random从过期 key 里随机淘汰不常用volatile-ttl从过期 key 里选剩余存活时间最短的淘汰接近过期的先淘汰我自己的项目默认使用 volatile-lru 的场景居多但如果是纯粹的业务缓存直接 allkeys-lru 更简单。注意如果所有业务 key 都没设置 TTLvolatile 开头的那几个策略就永远不会淘汰任何数据内存照样会爆。4.4 持久化机制详解RDB 与 AOF 怎么配合Redis 是内存数据库进程一挂数据就可能丢。持久化机制必须搞懂因为面试爱问生产环境也靠它兜底。RDB 是快照式持久化。它会把整个数据集打成二进制快照文件默认 dump.rdb。手动执行 SAVE 会阻塞主线程所以生产配置一般用 BGSAVE 或定时策略让后台子进程完成。RDB 的优点是文件紧凑、恢复速度快缺点是你无法保证最后一次快照之后的数据不丢因为快照之间发生的数据变更不会记录。AOF 则是记录写指令。每次写命令都会按 appendfsync 配置刷盘always每条写命令都刷盘最安全但性能最低everysec每秒刷一次最多丢一秒数据生产默认推荐no交给操作系统决定刷盘时机性能最高但丢数据风险最大AOF 的问题是文件体积持续膨胀恢复重放相对慢。Redis 4.0 起提供了混合持久化开启后 BGSAVE 生成 RDB 快照快照之间的写操作仍以 AOF 增量记录恢复时既快又少丢数据。生产上我一般建议 RDB 和 AOF 同时开启或者用混合持久化最大化数据安全性。4.5 一份可以直接用的最小生产配置不管你是拿 Redis 做缓存还是中间件下面几个配置参数都应该提前设置maxmemory 2gb maxmemory-policy allkeys-lru appendonly yes appendfsync everysec protected-mode yes额外推荐开启慢查询日志把执行时间超过阈值的命令记录下来排查卡顿和超时非常有帮助slowlog-log-slower-than 10000 slowlog-max-len 128单位是微秒10 毫秒以上慢命令会被记录。排查时用 SLOWLOG GET 查看具体是哪条命令拖慢了实例。5. Spring Boot 集成前的准备依赖、配置与序列化5.1 引入依赖和连接池配置Spring Boot 项目里集成 Redis 主要靠 spring-boot-starter-data-redis底层连接器首选 Lettuce它是线程安全的性能比老牌 Jedis 更稳。先在 pom.xml 加入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency如果需要连接池还要显式加入 commons-pool2dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency然后在 application.yml 中配置spring: data: redis: host: 127.0.0.1 port: 6379 password: # 有密码就填 database: 0 timeout: 3000ms lettuce: pool: max-active: 8 max-wait: -1ms max-idle: 8 min-idle: 0这里有个小知识点Spring Boot 2.x 和 3.x 的配置前缀有差异。2.x 用 spring.redis3.x 改为 spring.data.redis。如果你的项目是 3.x千万别照抄旧配置导致全不生效。5.2 为什么 RedisTemplate 默认序列化会“乱码”Spring Boot 自动装配的 RedisTemplate 默认使用 JdkSerializationRedisSerializer。什么意思就是 Java 对象序列化成二进制字节再存进 Redis。于是你在 Redis Desktop Manager 里看到的 key 长这样\xac\xed\x00\x05t\x00……value 也是一堆看不懂的二进制。这个“乱码”不是 Redis 坏了而是 JDK 序列化的编码表现。它的坏处很明显一是人类不可读排障和 Debug 很痛苦二是字节体积大占用内存多三是跨语言互操作很差别的服务很难直接用。所以几乎每个项目接入第一件事就是把 RedisTemplate 序列化器换成字符串加 JSON。5.3 一套通用的 RedisTemplate 配置下面是我在多个项目里反复使用的配置模板化程度很高直接抄即可。注意需要引入 Jackson 的 ObjectMapperConfiguration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, 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; } }这套配置的效果是key 用普通字符串一眼能认出来value 用 JSON 格式跨语言也能读。因为 GenericJackson2JsonRedisSerializer 会在 JSON 里写入类型信息 class所以反序列化时可以还原成原来的对象类型。如果不需要保留类型可以换成 Jackson2JsonRedisSerializer 并指定目标类型。5.4 StringRedisTemplate 和 RedisTemplate 别混用Spring Boot 同时提供了 StringRedisTemplate它是 RedisTemplateString, String 的简化版key 和 value 都用字符串序列化。因为你现在自己配了 RedisTemplate 的 key 为字符串value 为 JSON日常简单读写其实完全可以用 StringRedisTemplate 手动操作字符串。最大的坑是混用两种序列化器读写同一个 key。比如你用 StringRedisTemplate 写入的 value再用自定义 RedisTemplate 去读因为反序列化方式不匹配大概率会报类型转换错误。规范做法是项目里统一用同一套 RedisTemplate Bean不要一半地方用自带的 StringRedisTemplate一半地方用自定义的很容易埋雷。6. 缓存注解实战Cacheable、CachePut、CacheEvict 的正确打开方式6.1 开启缓存并让注解生效在启动类或任意配置类上加 EnableCaching 开启缓存抽象。然后在 Service 方法上标注缓存注解即可。Spring Cache 抽象的好处是方法里的业务逻辑不用写任何 Redis 命令只要配好注解就能自动完成“先查缓存、命中返回、未命中执行方法并回填”的整套流程。先定义一个基础场景比如根据 id 查询用户Cacheable(value userCache, key #id) public User getUserById(Long id) { return userMapper.selectById(id); }第一次调用该方法缓存中没有 userCache::1方法执行查库并写入缓存第二次再查直接走缓存方法体根本不会执行。你可以在 mapper 里加个日志打印来验证。6.2 三个注解的分工和 key 设计Cacheable 的核心逻辑是“查询前先看缓存没有才执行方法”。CachePut 则是无论如何都执行方法并把结果写回缓存适合更新数据后刷新缓存CachePut(value userCache, key #user.id) public User updateUser(User user) { userMapper.updateById(user); return user; }CacheEvict 用来删除缓存适合删除数据后清理CacheEvict(value userCache, key #id) public void deleteUser(Long id) { userMapper.deleteById(id); }key 这里用的是 Spring 的 SpEL 表达式#id 表示取方法参数 idvalue 表示缓存分区名最终 Redis 里的真实 key 是 userCache::1。如果有多个参数可以用 userCache::#id:#type 形式拼出唯一标识。千万不要把 key 写死成常量否则所有用户共用一条缓存会出严重乱套。6.3 自定义 RedisCacheManager 设置过期时间默认的 RedisCacheManager 写进 Redis 的 key 是永不过期的这不符合大部分缓存场景。所以通常要自定义 CacheManager为缓存统一指定 TTLBean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues(); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); }注意 disableCachingNullValues它会阻止把 null 结果写入缓存。通常建议开启防止缓存空值引起额外一致性问题。如果需要让不同缓存分区使用不同 TTL可以用 config 方法为特定缓存单独设置。6.4 同类内部调用导致缓存失效的坑这是我见过最典型的注解坑。假设 Service 里有这样一个方法public User getUserWrapper(Long id) { return this.getUserById(id); } Cacheable(value userCache, key #id) public User getUserById(Long id) { return userMapper.selectById(id); }你从 controller 调用 getUserWrapper会发现在方法内部通过 this 调用时getUserById 上的 Cacheable 完全不生效。原因是 Spring Cache 基于 AOP 代理实现只有“从外部进入代理对象”时注解逻辑才被织入内部 this 调用相当于直接进入目标对象原生方法绕过了代理注解自然失效。解决办法有三条外部直接调用被注解方法把缓存方法拆到另一个 Service Bean 里或者注入一个自身代理Service public class UserService { Autowired private UserService self; public User getUserWrapper(Long id) { return self.getUserById(id); } Cacheable(value userCache, key #id) public User getUserById(Long id) { return userMapper.selectById(id); } }用 self 而不是 this实际上是调用 Spring 生成的代理对象注解才能拦截。这个坑还会出现在 Transactional 上原理一模一样值得举一反三。7. 面试高频的缓存三兄弟穿透、击穿、雪崩7.1 缓存穿透布隆过滤器加空值缓存缓存穿透指的是请求查询一个根本不存在的数据比如恶意攻击者不停请求一个不存在的用户 id。因为 Redis 里没有每次都打到数据库数据库压力直接被打满。解决思路一把查不到的结果也缓存一份空值并设置较短过期时间。这样后续相同请求会先命中空值缓存不再打到数据库Object cached redisTemplate.opsForValue().get(key); if (cached ! null) { return cached; } User user userMapper.selectById(id); if (user null) { redisTemplate.opsForValue().set(key, , Duration.ofMinutes(5)); } else { redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30)); }思路二使用布隆过滤器在缓存前加一道“可能不存在”的判断。布隆过滤器的特点是“判断不存在时一定不存在判断存在时可能误判”。把合法 id 预先放入布隆过滤器如果 id 在过滤器中不存在直接拒绝查询。布隆过滤器本身可以基于 Redis bitmap 实现也可以引入 Guava 的 BloomFilter 做本地过滤。注意布隆过滤器有误判率生产上最好配合空值缓存一起用。7.2 缓存击穿热点 key 过期瞬间的并发风暴击穿和穿透最大的区别是击穿打的是一个热点 key。它原本在缓存里好好的但在过期的那一瞬间大量并发请求同时发现缓存未命中于是同时回源数据库瞬间打垮数据库。最常见的解决方案是互斥锁。在重建缓存之前先尝试获取锁拿到锁的线程去查数据库回填缓存其他线程等缓存出现。String key hot:item: itemId; Object value redisTemplate.opsForValue().get(key); if (value ! null) { return value; } String lockKey lock:hot:item: itemId; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofSeconds(3)); if (Boolean.TRUE.equals(locked)) { try { value queryDb(itemId); redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(30)); } finally { redisTemplate.delete(lockKey); } } else { Thread.sleep(50); return redisTemplate.opsForValue().get(key); }另一种思路是逻辑过期。缓存本身不设置 TTL而是把“过期时间”放在 value 里一起存。读的时候判断逻辑过期如果过期则开启异步线程去更新缓存。这种方式不怕同时击穿但实现复杂度高一些适合对数据实时性要求不那么高的榜单类数据。7.3 缓存雪崩大批 key 同时过期或 Redis 宕机雪崩说的是大量 key 在同一时间窗口过期或者 Redis 整个实例宕机导致大量请求全部落到数据库。区别于击穿雪崩是面不是点。解决大批 key 同时过期最简单可靠的办法是给过期时间加随机值long base 30 * 60; long random ThreadLocalRandom.current().nextLong(600); Duration ttl Duration.ofSeconds(base random); redisTemplate.opsForValue().set(key, value, ttl);所有 key 的基础过期时间相同但加了 0 到 10 分钟左右的随机偏移就不会整批同时失效。如果是固定用 Spring Cache 注解也可以在 CacheManager 里配置随机 TTL 的策略或者由业务层统一生成过期时间后再写入。处理 Redis 宕机引发的雪崩思路是分层防护在 Redis 之上再叠一层本地缓存比如 Caffeine同时给数据库访问加熔断降级比如用 Sentinel 配置数据库调用限流超过阈值直接返回兜底数据。记住一个原则缓存的作用是保护数据库不是雪上加霜。7.4 缓存与数据库的一致性怎么取舍面试时你大概率会被问“缓存更新和数据库更新怎么保证一致”。我的经验是不要追求强一致而是采用最终一致性。理论上最简做法是更新数据库后删除缓存而不是先删缓存再写库。因为先删缓存再写库如果写库失败缓存已经被删了下一次查询会把旧数据回填进去而先写库再删缓存即使删缓存失败也只是多一次缓存旧值返回数据库很快会成为最新值来源。更稳妥的方案还可以用延迟双删先删除缓存更新数据库等几百毫秒再次删除缓存把并发间隙中回填的旧缓存清掉。延迟双删的问题是删除操作的可靠性。生产上可以配合消息队列或者订阅数据库 binlog 变更删除缓存失败时重试。只要记住这里的核心目标是让缓存最终和数据库一致期间少量短暂不一致是可以接受的。8. Redis 分布式锁从 SET NX 到 Redisson8.1 用 SET NX EX 实现一把最简锁分布式锁是分布式系统中协调多个进程访问共享资源的利器。Redis 实现锁的核心命令是 SETNX它只在 key 不存在时才能设置成功天然具备“互斥”语义。为了避免持锁进程挂掉导致锁永不过期一定要在设置时同时给锁加过期时间String lockKey lock:order: orderId; String requestId UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, Duration.ofSeconds(30));注意这里保存了一个唯一的 requestId而不是写死某个常量。为什么因为释放锁时要先判断 value 是否属于自己防止“持锁线程 A 超时后锁被 B 获取A 执行完去释放锁时误删了 B 的锁”。8.2 释放锁的原子性Lua 脚本解决“不是自己锁”问题释放锁不能简单地 get 判断再 delete因为“判断”和“删除”是两个命令中间可能插入别的命令导致误删。需要把判断和删除合入一个原子操作Redis 里用 Lua 脚本实现if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end通过 RedisTemplate execute 传入这段脚本Redis 会保证整个脚本执行过程中不被其他命令干扰。这是每个做分布式锁的人都应该掌握的姿势面试问到“怎么安全释放锁”时标准答案就是这条 Lua。8.3 用 Redisson 省掉繁琐细节自己用 RedisTemplate 撸锁要处理续期、重试、可重入等一堆问题。生产上我更推荐直接用 Redisson它是 Redis 官方推荐的 Java 客户端之一提供现成的分布式锁实现RLock lock redissonClient.getLock(lock:order: orderId); boolean acquired lock.tryLock(100, 30, TimeUnit.SECONDS); if (acquired) { try { // 业务逻辑 } finally { lock.unlock(); } }Redisson 内部默认通过“看门狗”机制给锁自动续期只要业务没执行完锁就不会因为超时被误删。它还支持可重入同一个线程可以多次加锁而不会死锁。这些能力自己手写要处理很多边界情况用现成库明显更稳。8.4 主从切换下锁安全性的边界认知必须坦白一个现实问题如果 Redis 使用主从架构主节点刚把锁写成功还没同步到从节点主节点就宕机了从节点升为主节点另一个线程在新主节点上又可以加同一把锁分布式锁就失效了。针对这种极端情况业界有 RedLock 方案要求向多个独立的 Redis 节点申请锁达到半数以上才算加锁成功。但 RedLock 本身争议不小尤其是在系统时钟跳跃时同样有隐患。我的建议是绝大多数业务场景主从切换导致锁失效的概率远低于自己写错锁代码的概率。先保证锁代码正确再把 Redis Sentinel 和主从架构做扎实已经是 90% 项目的合格水平。9. 部署形态主从复制、哨兵、集群与容器化9.1 主从复制与 docker-compose 快速搭建主从复制是最基础的高可用方案。Redis 会把主节点的数据异步同步到一个或多个从节点从节点可以分担读压力。搭建方式很简单在从节点的配置文件中加入replicaof 127.0.0.1 6379用 docker-compose 可以快速拉起一个一主一从services: redis-master: image: redis:7-alpine ports: - 6379:6379 redis-slave: image: redis:7-alpine command: redis-server --replicaof redis-master 6379 depends_on: - redis-master启动后执行 redis-cli -p 6380 INFO replication看到 role:slave 且 master_link_status:up 说明复制成功。主从复制的短板也很明显主节点故障时不会自动切换需要人为干预或者交给哨兵。9.2 哨兵和 Redis Cluster 什么时候用哨兵模式Sentinel解决主节点故障自动切换的问题。哨兵会监控主从节点主节点挂掉后投票选出一个从节点升为新主节点并将新主节点信息通知客户端。它适合“读多写少、单主实例能扛住写压力”的场景。Redis Cluster 则是真正的分布式集群。数据被自动分片到 16384 个哈希槽中每个节点负责一部分槽客户端可以直接访问任意节点节点并路由到正确的槽。它解决了单节点内存和写能力上限适合数据量大、需要水平扩容的场景。集群一般要求至少 3 主 3 从配置开启 cluster-enabled yes 后启动多个节点即可组成。注意集群模式下客户端要使用 Cluster 连接方式Spring Boot 里配置 spring.data.redis.cluster.nodes而不是简单填一个 host 和 port。9.3 K8s 部署 Redis 的几种方式生产环境里 Redis 上 K8s 已经是主流。最简单的玩法是用 Helm Chart一条命令部署主从或哨兵helm repo add bitnami https://charts.bitnami.com/community helm install my-redis bitnami/redis-cluster更完整的做法是用 StatefulSet 编排 Redis 节点配合 Headless Service 解决 Pod 间稳定网络标识再用 PVC 给数据做持久化。如果集群规模大、维护复杂也可以直接用 Redis Operator 这类方案托管。无论哪种方式都要把 Redis 的持久化存储和节点故障恢复考虑清楚否则 Kubernetes 重建 Pod 后数据全丢比不用 Redis 还危险。10. 常见报错与排查技巧实录10.1 Lettuce 连接超时与 READ TIMEOUT很多人在 Spring Boot 里跑 Redis 时会遇到类似报错RedisCommandTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException出现这种超时先不要急着调大 timeout。按下面的顺序排查第一Redis 本身是否卡顿比如存在大 key、慢命令阻塞了单线程第二Lettuce 连接池是否耗尽大量请求在等待连接第三业务侧是否用了阻塞操作或不合理的事务。排查确认都没有问题后再适度增大连接池和超时时间spring: data: redis: timeout: 5000ms lettuce: pool: max-active: 32 max-wait: 3000ms把 max-active 从默认的 8 提到 32基本能应对普通项目的峰值连接需求。如果你是 Spring Boot 3.x注意检查是否引入了 commons-pool2没有这个依赖连接池参数根本不生效。10.2 Docker 拉取或搜索 Redis 镜像报 500Docker 方式运行 Redis 偶尔会遇到“docker search redis”返回 500 的错误提示 Docker Desktop Linux engine 的 API 出了问题。这类问题通常不是 Redis 本身而是 Docker 的本地引擎或镜像源问题。常规操作是先重启 Docker Desktop再确认当前使用的镜像源是否稳定必要时配置可靠镜像加速后重试。如果依然报 500卸载重装 Docker 也能解决大部分本地引擎异常。10.3 Redis 里 key 全是乱码用可视化工具看到一堆 \xac\xed 开头的 key几乎都是 JDK 序列化造成的。解决方案已经在第 5 节给出把 RedisTemplate 的 key 和 value 序列化器替换成 String 和 JSON。这里补充一个排查小技巧如果历史数据已经被 JDK 序列化污染了先写一段一次性迁移脚本从旧 key 中读取数据再写入新 key然后删除旧 key。不要指望线上手动一个个改名数据量一大绝对崩溃。10.4 缓存更新时先删还是先更我刚工作时也犯过这个错先删除缓存再更新数据库结果并发请求在数据库更新完成前把旧值读入缓存导致缓存永远显示旧值。正确做法是先更新数据库再删缓存。如果你有强一致的诉求可以用延迟双删。但这里必须提醒一句延迟双删不是银弹删除失败仍然要引入重试机制。考虑系统复杂度更多时候我们应该评估这个缓存是不是真的有必要以及能不能接受短时间的不一致。10.5 我的排查习惯和踩坑心得做 Redis 排障这几年我养成了固定习惯先确认 Redis 实例存活和状态再看慢日志最后才查代码。很多人一出问题就怀疑自己代码逻辑结果浪费几个小时最后发现是 Redis 内存满了触发淘汰或者某个大 key 把整个实例拖慢。另一个心得是本地学习和测试直接上 Docker 最舒服。开发环境下 Redis 崩溃了重建容器只要几秒钟完全不用心疼环境。但生产环境务必把持久化、主从、哨兵这些部署细节提前想清楚。最后再分享一句Redis 确实很强但它不是万能的。数据库查询压力大先看 SQL 有没有问题再看能不能上缓存顺序反了会越优化越乱。