1. 为什么我把 Redis 列为 Java 学习者第一个必学的中间件做后端开发这几年我带过不少实习生和转行的朋友被问得最多的一个问题就是“Java 基础学完了Spring Boot 也会用了接下来到底该学什么才能开始找工作/开始做项目”我的建议永远很明确如果只选一个中间件先学那就学 Redis。原因其实很简单。你去翻一下真实的招聘 JDJava 后端岗位里 Redis 的出现频率高得离谱基本和 MySQL 平起平坐。再打开任何一个开源项目或者你们公司随便一个业务系统Redis 一定在里面。它不像消息队列那样需要踩准复杂业务场景才用得上也不像搜索引擎那样有较高的入门门槛。Redis 的定位非常朴素——缓存、计数、分布式锁、排行榜、Session 共享这几件事几乎覆盖了中小型系统 80% 的非业务核心需求而每件事你用 Java 都能在半小时内把 Demo 跑起来。这篇内容就是为纯零基础的读者准备的。我会从下载安装开始一直讲到 Java 里怎么用 Redis中间会穿插大量我在真实项目里踩过的坑和验证过的做法。你不需要提前掌握数据结构、操作系统、网络协议这些底层知识跟着步骤走完你会对“项目里 Redis 到底扮演什么角色”有一个非常清晰的体感。如果你已经会用一点 Redis也可以直接跳到 Java 集成和实战拆解部分看。另外说句实话Redis 的学习曲线在中间件里算最平缓的。它核心就五种数据类型命令总共一百多个日常高频使用的连一半都不到。但恰恰因为入门门槛低很多人在第一步就姿势不对——比如在 Windows 上装了个来路不明的版本或者直接跳过配置环节后面出问题根本不知道怎么排查。所以这篇文章我要从安装开始讲把环境这个地基打扎实。2. 安装与配置从零跑起你的第一个 Redis 实例2.1 先搞清楚 Redis 在什么系统上跑最稳很多人学 Redis 第一个困惑就是官网下载页面没有 Windows 版本那 Windows 用户怎么办先说结论Redis 官方确实不提供 Windows 原生版本官方文档也明确写着 Redis 是针对 Linux 开发和优化的。但在 Windows 上做学习和开发验证完全有可用的替代方案不必被这一步卡死。目前 Windows 上用的比较多的有两类来源微软开源技术团队早期维护的 Windows 移植版上升到了 Redis 3.x 就停止维护了第三方社区编译的版本比如 tporadowski 的 Redis 5.0.14 Windows 构建版目前最常用。我个人在 Windows 上做演示时用的是 tporadowski 提供的 5.0 版本压缩包解压就能用不自带安装步骤也不会污染注册表。它的问题是很久不更新功能停留在 5.0 时代但学基础数据类型、Java 集成、分布式锁完全够用。如果你是要部署到生产环境或者认真学完想自己搭一套完整环境请务必用 Linux。我用的是 CentOS 7 和 Ubuntu 20.04 两个环境都跑过这里给出最省事的安装方式# Ubuntu / Debian apt update apt install redis-server # CentOS / RHEL 7 yum install redis用系统包管理器装的好处是环境变量、服务脚本、默认配置都被系统接管了启动和开机自启一条命令搞定。但包管理器装的版本可能偏旧如果你需要最新版本推荐用官方源码编译安装三步走wget https://download.redis.io/releases/redis-7.2.4.tar.gz tar xzf redis-7.2.4.tar.gz cd redis-7.2.4 make make install编译前确保系统里有 gcc 和 make装过 build-essential 之类的开发工具包就没问题。这里提醒一下新手make一定要看到最终输出没有 error 才算编译成功如果中途有报错先补依赖再重新 make不要跳过错误往下走。不过说句实在话我后来最推荐的是 Docker 方式一键启动和宿主机完全隔离想换版本随时换docker run -d --name redis \ -p 6379:6379 \ -v /data/redis:/data \ redis:7.2 redis-server --appendonly yes这条命令创建了名为 redis 的容器把宿主机 6379 端口映射到容器的 6379数据目录挂在宿主机/data/redis下同时开启了 AOF 持久化。生产环境和学习环境我都是用这个方式省心很多。2.2 启动服务并验证连通性Windows 解压版的启动最简单。打开压缩包目录你能看到redis-server.exe和redis-cli.exe两个核心文件。双击redis-server.exe或在其所在目录执行redis-server.exe看到一段 ASCII logo 和Ready to accept connections字样说明服务起来了。默认端口是 6379默认没有任何密码。接着另开一个终端窗口执行redis-cli.exe ping如果服务正常Redis 会回复PONG。这个PING/PONG是 Redis 的“心跳测试”命令凡是连接类的问题先拿它试一定会第一时间暴露出来。Linux 和 Docker 环境下启动后可以用同样的方式验证redis-cli ping # 输出 PONG这里我要专门讲一个新手最容易踩的坑Windows 双击启动的 redis-server.exe窗口一关服务就没了。你以为 Redis 在跑过几分钟系统变慢或者连接不上怎么查都查不到原因最后发现是那个终端窗口被顺手关了。我的习惯是学习阶段无所谓但如果是本地调试一定要用服务方式注册好别用前台窗口跑。2.3 必须知道的几个配置项Redis 的行为由redis.conf配置文件和启动参数共同决定。Linux 用系统包安装的话配置文件一般在/etc/redis/redis.confWindows 解压版则直接在解压目录下Docker 方式可以用-v把宿主机上的配置文件挂载到容器内/etc/redis/redis.conf。以下四个配置项你在学习阶段就会接触到强烈建议逐个亲手改一遍体会一下。1. bind默认是127.0.0.1意思是只允许本机连接。如果你在云服务器上装 Redis想让你自己电脑连上去就必须改成0.0.0.0并配好密码。否则外部连接请求会直接失败报错信息通常类似Connection refused很多人第一反应是防火墙问题实际上可能是 bind 没改。这里我建议远程连接测试时用0.0.0.0但生产环境不要随便暴露到公网收口到内网或安全组规则才是正解。2. port默认 6379除非你有特殊的安全要求或端口冲突一般不建议改。改了之后所有客户端连接都要跟着改排查问题的心智负担更高。3. requirepass设置访问密码。格式是requirepass yourpassword。设置后redis-cli连接时要用redis-cli -a yourpasswordJava 客户端连接时要在 URL 或配置里带上密码。学习阶段本地环境不设密码问题不大但只要你把服务暴露到非本机网络这行必须是第一件做的事。4. maxmemory限制 Redis 最大可用内存比如maxmemory 256mb。不设置的话Redis 会一直存数据直到把服务器内存吃光这是真实事故的重要来源之一。设置了之后配合maxmemory-policy指定的淘汰策略Redis 才具备“内存满了怎么处理”的规则。我在后面的实战部分会专门展开内存淘汰策略怎么选。改完配置后记得重启服务。用 Linux 系统服务方式执行systemctl restart redis用 Docker 方式执行docker restart redis。2.4 可视化客户端选哪个顺手用redis-cli敲命令是基本功但可视化客户端能极大提升排查效率尤其看 key 的分布、查某个 key 的类型、看过期时间这些操作图形界面比一条条敲命令直观太多。我长期用的是Another Redis Desktop Manager它对 Windows/macOS/Linux 都有支持连接 Redis 时只需要填主机、端口、密码三项实测对中文 key 和大量 key 的加载都很流畅。还有一个Redis Desktop ManagerRDM是老牌工具如果连新版 RDM注意它从 2022 年起部分版本变为收费模式去官网找免费的开源版本下载也费了点劲。综合体验下来我更建议新手直接用 Another Redis Desktop Manager。装好可视化客户端之后你可以先在本机连一下刚刚启动的 Redis确认能搜到、能看到默认的 16 个 databasedb0 到 db15再继续往下学。这一步的目的不是让你依赖图形工具而是帮助你建立对 Redis 数据的直观感知后面的每个命令敲下去你能在界面里看到数据真的进去了学习正向反馈会强很多。3. 五种核心数据类型先学会用再理解为什么这样设计很多人一上来就背命令背完就忘。我的经验是换个角度学先理解 Redis 的数据类型各自对应了业务里的哪类场景然后你就会发现命令是自然记忆的不需要死记硬背。Redis 的核心数据结构是五种String、Hash、List、Set、ZSet。3.1 String不止是“字符串”String 是 Redis 最基础、最常用的类型。Java 的 String 只能存字符串Redis 的 String 除了能存普通文本还能存数字并且带原子性的自增自减操作。这是它最值钱的地方。典型命令SET user:name zhangsan GET user:name INCR page:view INCRBY page:view 5 DECR page:view SETNX lock:order 1INCR之所以重要是因为它是原子操作。多个客户端同时执行INCR不会出现“读到了同一个值再各自写回去导致计数丢失”的问题。这在场景里对应的是文章阅读数、商品点击数、库存扣减前的预占数等。我在一个真实项目里就遇到过把阅读数存数据库的坑。流量一高每次刷文章详情就要UPDATE一次 MySQL 行行锁竞争严重后来改成先INCRRedis 计数再定期批量刷回数据库效果立竿见影。这里也提醒你计数场景的准确性和数据库一致性需要额外设计不是无脑把 Redis 当唯一数据源但先学会用INCR解决并发计数问题是第一步。SETNX是“SET if Not eXists”的意思只有当 key 不存在时才设置成功。这是后面实现分布式锁的核心命令我在实战部分会专门讲。3.2 Hash对象存储的天然选择Hash 相当于一个小的 map适合存对象字段。Java 里你有一个 User 对象有 id、name、age 三个字段用 String 存需要拼 JSON 或分多个 key用 Hash 可以这么存HSET user:1001 name zhangsan age 25 city beijing HGET user:1001 name HGETALL user:1001 HINCRBY user:1001 age 1HSET可以一次设置多个字段比一个个 set 少了网络往返。HINCRBY可以对 Hash 里的某个字段做原子自增这种能力在做“商品详情页的浏览量分维度统计”时非常好用。使用 Hash 还有一个工程上的好处需要更新对象某一个字段时不用把整个对象读出来再写回去直接对一个 field 操作即可大大减少了大数据量下的内存和带宽开销。3.3 List不只是列表还是轻量消息队列List 底层是双向链表支持从左边和右边插入、弹出。常用命令LPUSH task:queue job-1 RPUSH task:queue job-2 LPOP task:queue RPOP task:queue LRANGE task:queue 0 -1 BLPOP task:queue 5LPUSH加BRPOP阻塞式弹出的组合可以在不引入 Kafka、RabbitMQ 的情况下实现一个简单的生产者消费者队列。BLPOP task:queue 5表示如果队列为空最多阻塞等待 5 秒期间有新数据进来就立刻弹出超时返回 nil。这个模式在项目里非常实用比如削峰填谷、异步处理耗时任务。流量高峰期先把请求塞进 List后台 Worker 通过BRPOP慢慢消费避免瞬时压力打垮数据库。注意这只是一个轻量方案它不提供消息确认、死信队列这些高级特性真正生产级别还是要上专业 MQ但入门阶段自己搭个小 Demo 完全够用了。3.4 Set去重与集合运算一把好手Set 是元素不重复的无序集合天然支持交集、并集、差集运算。SADD user:1001:tags java redis spring SADD user:1002:tags java python SMEMBERS user:1001:tags SINTER user:1001:tags user:1002:tags SCARD user:1001:tagsSINTER求交集直接就能算“两个用户共同关注了哪些话题”SCARD获取集合元素个数适合“这个标签下有多少文章/商品”的实时统计。我做过的一个社区项目里用 Set 存储每篇文章的点赞用户 id检查某个用户是否点过赞就是一条SISMEMBER post:123:likes 789的事O(1) 复杂度比数据库SELECT COUNT(*) WHERE user_id? AND post_id?快几个数量级。3.5 ZSet排行榜的命根子ZSet 是 Set 的升级版给每个元素绑定一个 score分数元素按 score 从小到大排序。ZADD rank:score 100 user-1 ZADD rank:score 200 user-2 ZRANGE rank:score 0 -1 ZREVRANGE rank:score 0 -1 ZSCORE rank:score user-1 ZINCRBY rank:score 10 user-1ZREVRANGE rank:score 0 -1从大到小取出全部这就是一个活生生的排行榜。ZINCRBY可以原子地给某个用户加分。游戏积分榜、热销商品榜、热搜话题榜全是这个套路。我当时给一个学习平台做“每日学习时长排行”每天零点是新榜单的 key用户每学习完一段时间就ZINCRBY累加页面直接ZREVRANGE取前 50 展示。全程 Redis 操作复杂度接近 O(log n) 级别完全感受不到性能压力。这里建议你学每种类型时都做一个“场景映射”String 对应计数器、Hash 对应对象、List 对应队列、Set 对应去重和关系运算、ZSet 对应排序和排行。场景对上了命令记得特别牢固面试被问“Redis 五种数据类型应用场景”也就是顺手拈来。4. Java 集成三套方案怎么选底层逻辑是什么4.1 Jedis、Lettuce 和 Spring Data Redis 的区别进入 Java 集成阶段第一个选择就是客户端库。现在主流的有三套搞清楚它们的底层区别选型就不再蒙圈了。Jedis老牌 Java 客户端设计直白。Jedis 的底层是同步阻塞的创建一个连接就对应一个 TCP 连接。它的优点是 API 和 Redis 命令几乎一一对应非常适合学习阶段理解 Redis 的交互方式。缺点也很明显Jedis 实例本身不是线程安全的多线程场景必须用连接池JedisPool来管理连接。经典写法是JedisPool pool new JedisPool(localhost, 6379); try (Jedis jedis pool.getResource()) { jedis.set(foo, bar); String value jedis.get(foo); }Lettuce基于 Netty 实现底层支持同步、异步和响应式三种编程模型连接是线程安全的底层能复用同一条连接处理并发请求。性能上限比 Jedis 高资源占用更低。Spring Boot 2.x 之后spring-boot-starter-data-redis 默认的客户端就是 Lettuce。所以你现在用 Spring Boot 写 Redis 相关代码大概率是在跟 Lettuce 打交道只是这些细节被框架封装好了。Spring Data Redis它不是与 Jedis/Lettuce 那一层的通信客户端而是对底层客户端的一层统一抽象。你用RedisTemplate或StringRedisTemplate写业务代码底层连接由它自动管理。当前 Spring Boot 项目里最推荐走这套因为它把连接工厂、序列化器、事务管理都给你安排好了你只需要关心业务逻辑。选型结论直白一点如果是纯学习想摸清 Redis 交互逻辑或者你的项目不是 Spring Boot 生态可以直接用 Jedis 连接池。如果是在 Spring Boot 里写业务直接用 Spring Data Redis。最早我踩过一个坑是想强行在 Spring Boot 里用 Jedis结果连接管理全靠手写后来切到 Spring Data Redis代码量少了不止一半。4.2 Spring Boot 集成 Redis 的完整过程我用 Spring Boot 3.x 为例因为现在新项目基本都是这个版本线了。第一步引入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency这个 starter 自带 Lettuce 核心依赖不需要额外引。第二步在application.yml配连接信息spring: data: redis: host: localhost port: 6379 password: # 如果没设置密码就留空 database: 0 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0这里提醒一个细节Spring Boot 2.x 对应配置前缀是spring.redis.*Spring Boot 3.x 改成了spring.data.redis.*。很多老教程上的配置直接复制到新项目里不生效就是因为这个前缀差异。排查这类问题看控制台提示和配置文件绑定日志就能发现。第三步写一个最简的 Service 验证链路通没通Service public class RedisDemoService { private final StringRedisTemplate stringRedisTemplate; public RedisDemoService(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate stringRedisTemplate; } public void demo() { stringRedisTemplate.opsForValue().set(java:demo, hello-redis); String value stringRedisTemplate.opsForValue().get(java:demo); System.out.println(读取到的值: value); } }StringRedisTemplate与RedisTemplate的核心差别一个在序列化器上StringRedisTemplate默认 key 和 value 都走 String 序列化存的都是可读的字符串调试时肉眼看得懂。而RedisTemplate默认用的是 JDK 序列化存进 Redis 的 value 是一坨二进制乱码。这就是下面要说的坑。4.3 序列化器选择很多人被乱码 key 坑过一晚上Redis 里只能存字节序列Java 对象要放进去必须先序列化。Spring Data Redis 的序列化器决定了你写进去的 key 和 value 长什么样。网上经常有人发帖问为什么我用 Redis Desktop Manager 看到的 key 是\xac\xed\x00\x05t\x00...这样的乱码这就是因为用了RedisTemplate的默认 JDK 序列化。JDK 序列化性能低、占用空间大、可读性差而且只有 Java 能反序列化其他语言的消费者根本接不了这些数据。我推荐的做法是通常直接用StringRedisTemplate存字符串和 JSON。业务对象需要缓存时自己把对象手动转成 JSON 字符串存进去读取时再手动反序列化回对象。这套方案最直观也最容易排查数据问题。如果你确实需要RedisTemplate的自动类型转换能力那就显式配置序列化器Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate( RedisConnectionFactory connectionFactory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); // key 用 String 序列化保证可读 StringRedisSerializer keySerializer new StringRedisSerializer(); template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); // value 用 JSON 序列化 Jackson2JsonRedisSerializerObject valueSerializer new Jackson2JsonRedisSerializer(Object.class); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); return template; } }这里还要专门说一个反例网上很多教程会让你把 value 序列化器配成GenericJackson2JsonRedisSerializer说是能保留类型信息。它确实好用但会在 JSON 里塞一个class字段记录类路径。如果你的数据要跨服务消费或者将来反序列化的类做了包名重构这条路就会埋雷。我现在的偏好是尽量 String 序列化 手动 JSON实在复杂再考虑 GenericJackson。很多线上问题不是 Redis 本身出的是序列化方案埋的隐患越简单越可控。5. 实战落地缓存穿透、缓存击穿、缓存雪崩与分布式锁学会了基础命令和 Java 集成接下来要解决的是真实项目里一定会遇到的三座大山缓存穿透、缓存击穿、缓存雪崩。这三件事面试也常问但面试回答和真实处理的方式还是有差别的。5.1 缓存穿透用空值缓存和布隆过滤器挡流量缓存穿透指的是查询一个根本不存在的数据。请求先查 RedisRedis 没有再去查数据库数据库也没有于是这个请求每次都会打到数据库。如果瞬间来了十万个恶意请求都在查同一个不存在的 id数据库压力直接失控。我见过一个典型事故某个开放接口允许用户按订单号查询攻击者构造大量不存在的订单号并发请求数据库每秒 QPS 被打到逼近上限接口大面积超时。最笨但最有效的方案是缓存空值。查数据库发现没有这个订单往 Redis 里写一个特殊占位值如空字符串并设置一个较短的过期时间比如 60 秒。这样同一批不存在的 key 在 60 秒内不会再打到数据库public Order getOrderById(String orderId) { String cacheKey order: orderId; Object cached stringRedisTemplate.opsForValue().get(cacheKey); if (cached null) { Order order queryDb(orderId); if (order null) { stringRedisTemplate.opsForValue().set(cacheKey, , 60, TimeUnit.SECONDS); return null; } stringRedisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(order), 30, TimeUnit.MINUTES); return order; } if (.equals(cached)) { return null; } return JSON.parseObject((String) cached, Order.class); }这个方案简单可靠缺点是“空值”也可能被大量不同 key 打满内存所以要给空值设置短过期时间。追求更严谨的方案用到的是布隆过滤器。它能用很小的内存空间判断“某个 key 一定不存在”但它有一定的误判率可能把不存在的判断成存在。做法是初始化时把所有合法订单 id 加载进布隆过滤器查询前先过布隆过滤器判断不存在就直接返回不再查任何存储。我自己的项目里数据量几十万量级直接用空值缓存就够了数据量到了千万量级、非法 key 基数很大的时候再引入布隆过滤器才有性价比。5.2 缓存击穿一个热点 key 过期请求全打到数据库缓存击穿和穿透容易混淆。穿透是“查的数据本来就不存在”击穿是“某个热点 key 在缓存过期的瞬间大量并发请求同时绕开缓存打到了数据库”。经典场景某爆款商品详情页缓存 keyproduct:hot:1001正好在零点过期而零点是一天流量最高峰几万个请求同时发现缓存没了全部去数据库查这个商品。两种主流解法我分别给出实际写法。解法一互斥锁。缓存过期后只放一个请求去查数据库并重建缓存其他请求阻塞等待或直接返回旧值。用 Redis 实现互斥锁的核心就是前面提到的SETNXpublic String getHotData(String key) { String cacheVal stringRedisTemplate.opsForValue().get(key); if (cacheVal ! null) { return cacheVal; } // 尝试获取锁 String lockKey lock: key; Boolean locked stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 双重检查可能别的线程已经重建完了 cacheVal stringRedisTemplate.opsForValue().get(key); if (cacheVal ! null) { return cacheVal; } String dbVal queryDb(); stringRedisTemplate.opsForValue().set(key, dbVal, 30, TimeUnit.MINUTES); return dbVal; } finally { stringRedisTemplate.delete(lockKey); } } // 没拿到锁的线程短暂 sleep 后重试 try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getHotData(key); // 递归重试实际项目要注意加次数上限 }这个方案我用过好多次逻辑本身不难但注意点不少setIfAbsent要带过期时间防止持锁线程崩了导致死锁释放锁用了finally保证异常也能释放递归重试一定要有上限否则无限递归容易出问题。解法二逻辑过期。给缓存 value 里塞一个过期时间戳而不是依赖 Redis 的 TTL。每次读取时判断是否到期到期了先返回旧数据再异步起一个线程去更新缓存。public String getDataWithLogicalExpire(String key) { String cacheVal stringRedisTemplate.opsForValue().get(key); CacheObj obj JSON.parseObject(cacheVal, CacheObj.class); if (obj.getExpireTime() System.currentTimeMillis()) { return obj.getData(); // 未过期直接返回 } // 已过期异步重建 executor.submit(() - rebuildCache(key)); return obj.getData(); // 先返回旧值 }这个方案的优点是用户体验无感知不会阻塞等锁但缺点是要保证缓存里始终有旧数据如果第一次缓存创建就失败了后面没有兜底。它更适合“允许短暂旧数据”的业务。5.3 缓存雪崩批量 key 同时过期的问题雪崩和击穿的区别在于击穿是一个热点 key雪崩是大量 key 在同一个时间段集中过期导致数据库请求瞬间暴增。最常见的触发原因是设置了统一的过期时间。比如把所有商品的缓存都设为 30 分钟那么到了整点几万个 key 一起过期瞬间形成一波数据库洪峰。我也见过因为重启缓存服务后大量请求全部打库导致的“伪雪崩”本质上也是缓存重建风暴。解决思路有三个层级第一过期时间加随机偏移量。设置过期时间时在基础值上叠加一个随机数把过期时间点打散。代码写出来特别简单int baseExpire 30 * 60; int randomExpire baseExpire ThreadLocalRandom.current().nextInt(300); stringRedisTemplate.opsForValue().set(cacheKey, value, randomExpire, TimeUnit.SECONDS);第二多级缓存。本地缓存 Redis 缓存双层Redis 挂了或重建期间本地缓存还能挡一阵。我用过 Caffeine 做本地缓存热点数据在本地直接命中Redis 压力大幅下降。第三服务降级与熔断。如果数据库确实扛不住瞬时流量可以在网关或 Service 层做限流超出阈值的请求直接返回旧缓存或友好提示而不是傻傻地把数据库打崩。这个属于高可用设计的范畴等你把 Redis 本身吃透后再深入。5.4 分布式锁SETNX 的正确姿势单机应用里解决并发用synchronized就够了但应用部署了多个节点不同 JVM 之间无法互斥就需要一个所有节点都能访问的公共协调者——Redis 就是最常见的实现载体。分布式锁的核心要求有三个互斥、防死锁、防误删。我在团队里 review 代码时最常见的错误写法是// 错误示范单独 SETNX不加过期时间 Boolean locked stringRedisTemplate.opsForValue().setIfAbsent(lockKey, token); if (Boolean.TRUE.equals(locked)) { try { // 业务逻辑 } finally { stringRedisTemplate.delete(lockKey); // 业务还没执行完锁就过期了怎么办 } }这个写法至少有三个隐患第一没有过期时间线程崩溃锁永远不释放形成死锁。第二设置了过期时间但如果业务执行超过过期时间锁被自动释放其他线程拿到锁后第一个线程跑完把锁删了实际上删的是别人刚创建的锁这就是误删。第三没有重入机制。我推荐的写法是// 加锁setIfAbsent 是 SETNX同时绑定线程唯一标识和过期时间 String token UUID.randomUUID().toString(); Boolean locked stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, token, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 业务逻辑尽量控制在锁过期时间以内 } finally { // 释放锁前先判断是否是自己持有的锁防止误删 String currentToken stringRedisTemplate.opsForValue().get(lockKey); if (token.equals(currentToken)) { stringRedisTemplate.delete(lockKey); } } }判断和删除锁之间不是原子的极端场景下仍可能误删。要彻底解决可以用 Lua 脚本保证判断和删除原子执行。但如果你对这个方向有更高的要求建议直接使用Redisson框架它的RLock是官方推荐的分布式锁方案内部实现了自动续期、可重入、原子释放还支持看门狗机制。我在几个生产项目里的做法是业务简单自己用 SETNX 唯一标识控制业务复杂或锁粒度比较重要直接上 Redisson。不要自己造轮子造出线上事故这个成本比引入一个成熟库高太多。6. 从编码到生产我踩过的坑和验证过的调优经验到这里基础用法和核心实战都过了一遍。最后这部分是我过去几年实际操作中沉淀下来的一些经验不一定出现在任何官方文档里但遇到问题的时候特别好用。6.1 连接池参数不是越大越好使用 Lettuce 在 Spring Boot 里其实默认是不显式配置连接池参数的但当你用了连接池就有一个很容易被忽略的点连接数设置过高会拖垮 Redis 服务本身。我之前有个项目开发环境配了max-active: 200几十个服务实例连同一个 Redis瞬间把 Redis 的连接数打爆然后运维发现 Redis 的 CPU 和内存飙升。后来统一调低到max-active: 50、max-idle: 20问题就消失了。Redis 是单线程处理命令的连接太多并不会提升吞吐反而会让命令的排队和上下文切换变多。对大多数业务系统来说20 到 100 个最大连接已经非常宽裕了不要贪多。6.2 慢日志和大 key排查性能问题的第一抓手Redis 变慢了先别急着加机器先看存在什么问题。Redis 提供了慢日志功能默认记录执行时间超过 10 毫秒的命令。查看方式SLOWLOG GET 10如果慢日志里频繁出现KEYS *、HGETALL、LRANGE这种命令大概率是遇到了大 key。所谓大 key是指某个 key 的 value 特别大比如一个 Hash 里有几十万个字段或者一个 String 存了几兆数据。大 key 在执行读写时会让 Redis 单线程长时间阻塞影响所有其他命令。处理大 key 的思路很简单拆。大 String 拆成多个小 key大 Hash 按字段分片大 List 按时间/业务维度拆列表。如果只是排查不阻塞业务可以用redis-cli --bigkeys扫描整个实例找出大 key 分布。这个命令我每次接手“Redis 卡顿”类问题都会先跑一遍基本能定位 80% 的问题。6.3 持久化策略RDB 和 AOF 怎么选Redis 被当作缓存用时丢了数据还能从数据库回源很多人就不开持久化了。但如果 Redis 里存了计数、排行榜这类允许一定丢失但不能全丢的数据持久化策略就要认真考虑了。RDB 是定期生成全量快照恢复快但可能丢失最后一次快照之后的数据。AOF 是追加写命令日志默认每秒写一次最多丢一秒数据文件体积大但可靠性高。我的建议是分场景纯缓存可接受少量丢数据不开持久化或开 RDB 做定期兜底有计数、排行榜、分布式锁状态等业务数据开 AOFappendfsync everysec是性价比最高的配置生产环境可以 RDB AOF 同时开两者机制不冲突。这个决策的依据很简单先想清楚这个 key 丢了业务能否接受。能接受持久化永远是性能开销不能接受那就老老实实开。最后再分享一个我长期组装的环境查验习惯每次搭完一个 Redis 环境我会用 Redis Desktop Manager 连上去做一次“体检”——看内存INFO memory、看连接数INFO clients、看持久化状态INFO persistence。这三项数据能快速判断一个 Redis 实例健不健康。这套习惯帮我在很多次环境交接、排障和排查慢接口时节省了大量时间。你把这个习惯带进日常开发里会比多背二十条命令值钱得多。
