几个月前帮同事排查一个线上问题服务里有一段定时任务业务上要求同一时间只能有一个实例执行。代码里明明加了锁用的RedisTemplate的setIfAbsent也设置了过期时间但压测时就是会出现两个实例同时进方法体。查到最后发现判断锁是否属于自己、释放锁时删除Key以及锁意外过期后线程A没跑完、线程B就拿到锁了这些边界情况根本没有处理好。如果你也遇到过类似问题大概率会走上同一条路在Spring Boot项目里引入Redisson。Redisson能解决的问题远比“加锁”多但多数人最初整合它都是冲着分布式锁去的。这篇文章就整理一下Spring Boot整合Redisson的两种主流方式官方Starter自动装配以及手动编程式配置。我会把各自的原理、操作步骤、适用场景和容易踩的坑都讲清楚你可以直接照着选一种方式落地。1. 先回应一个疑问RedisTemplate明明能用为什么还要Redisson1.1 原生Redis命令写分布式锁的三道坎用Redis原生命令做分布式锁最经典的套路是SET key value NX EX seconds。看起来没问题实际一跑就露馅。第一道坎是释放锁时的误删。线程A加锁后业务执行超过锁的过期时间锁自动释放了线程B随后加锁成功这时线程A业务终于执行完顺手执行DEL key把线程B刚持有的锁给删了。B在临界区里还没跑完C线程又拿到锁进来并发保护直接失效。解决办法是加锁时写入唯一标识释放前先比较标识是否属于自己再删除。这个比较和删除两步骤必须原子执行又得靠Lua脚本。第二道坎是锁过期但业务没跑完。如果锁过期时间设置得很短比如2秒但业务方法在4秒内才执行完那么从第2秒开始锁就形同虚设。设成10秒呢遇到慢SQL、GC停顿或者下游接口耗时突增锁照样会先于业务结束而超时。要彻底解决需要一把“自动续期”的锁检测到业务还在执行就不断给锁延期。第三道坎是可重入。同一个线程递归或嵌套调用加锁逻辑时普通Set命令不感知owner无法做可重入容易在嵌套场景下把自己锁死。要支持可重入就得用Hash结构记录线程标识和重入次数每加一次锁计数加一释放一次减一。这套逻辑用原生命令写出来已经是一个不大不小的轮子了。1.2 Redisson干的活是把分布式组件“伪装”成本地JUC工具Redisson把Redis的对象操作封装成了类似Java并发包的接口风格。你要分布式锁它给你RLock长得像java.util.concurrent.locks.Lock你要分布式队列它给你RQueue要分布式Map它就是RMap用起来接近ConcurrentHashMap。拿锁来说redisson.getLock(myLock)返回一个RLock调用lock()时不需要自己操心过期时间。Redisson里有个看门狗机制默认锁超时时间是30秒业务没执行完会自动续期进程意外宕机又会在租约到期后自动释放不会死锁。这不只是省代码而是把分布式场景里最麻烦的边界问题都处理掉了。明白了这一点Spring Boot整合Redisson的意义就清楚了让RedissonClient的生命周期交给Spring容器管理在应用启动时建立连接池关闭时优雅释放连接同时让分布式锁、分布式缓存这些能力能以常规Spring Bean的形式注入业务代码。2. 方式一官方Starter自动装配配置最少也能跑2.1 依赖引入和Spring Boot版本怎么对号入座第一种方式最简单引入官方Starterdependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.25.2/version /dependency版本号建议你去Maven中央仓库看一下当前Release别盲目Copy。这个Starter内部会再依赖一层redisson-spring-data-xx其中的xx要和Spring Boot的小版本对应。Spring Boot 3.x项目如果引了一个针对Spring Boot 2.x设计的旧版Redisson启动时会莫名其妙报NoClassDefFoundError而且报错位置往往不在你自己的代码里排查起来很绕。我现在的习惯是先确认自己的Spring Boot大版本再去Redisson官方Wiki或GitHub的redisson-spring-data模块说明里核对版本对应关系。如果项目从Spring Boot 2.7升级到3.2Redisson版本也要跟着升不要指望老版本红isson能在Spring Boot 3.x下平稳运行。如果项目不使用Spring Boot单独引入redisson核心包再自己封装也可以但Spring Boot项目直接用Starter更省事。2.2 application配置如何被自动读取引入Starter后配置工作可以完全放在application.yml里。Spring Boot 3.x的Redis配置前缀是spring.data.redisSpring Boot 2.x则是spring.redis别混了spring: data: redis: host: 127.0.0.1 port: 6379 password: yourPassword database: 0 timeout: 3000msRedisson的自动装配会读取这些配置构建出内部的Config对象再创建RedissonClient。如果你需要更细粒度的调优可以在resources目录放一个redisson.yaml或redisson.json里面写连接池、线程池、序列化方式等参数Starter会优先按这个文件构建。我自己遇到过一种情况项目里application.yml和redisson.yaml都配了Redis地址结果启动时Redisson用的并不是application.yml里的配置而是redisson.yaml里的。所以如果你的项目两种配置同时存在最好在启动日志里看一眼Redisson实际加载的配置来源不要想当然。2.3 Starter到底往容器里塞了哪些Bean官方Starter在自动装配阶段会做几件事了解后对你排查问题很有帮助自动注册RedissonClient这是最核心的Bean业务代码里直接注入RedissonClient就能用。如果你的项目里已有spring-data-redis依赖它会把自己作为Redis连接工厂的实现替换掉Lettuce或Jedis让RedisTemplate底层也走Redisson客户端。这意味着你可以统一用Redisson管理连接避免一个项目里存在两套Redis连接体系。自动注册一个CacheManager底层是RedissonSpringCacheManager。有了它Spring Cache注解如Cacheable、CacheEvict直接可以用Redisson做分布式缓存实现。提供一个RedissonAutoConfigurationCustomizer扩展点允许你在自动装配的基础上一层层改Config对象。这是Starter方式里保持可控性的重要手段。换句话说用Starter不仅解决了分布式锁连RedisTemplate连接和Spring Cache都被一同接管了。这个特性在微服务架构里很舒服一套客户端锁、缓存、数据结构操作全搞定。2.4 写一个最小可运行Demo一个典型的扣减库存场景用RLock避免超卖Service public class StockService { private final RedissonClient redissonClient; public StockService(RedissonClient redissonClient) { this.redissonClient redissonClient; } public boolean deductStock(Long skuId, int count) { RLock lock redissonClient.getLock(stock:deduct: skuId); try { // 尝试获取锁最多等800毫秒拿到锁后30秒内必须完成业务 if (lock.tryLock(800, 30, TimeUnit.SECONDS)) { // 这里是真正的业务临界区扣减库存、写流水等 return true; } else { return false; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }注意这里tryLock传了leaseTime30秒一旦显式指定租约时间Redisson就不会启动看门狗自动续期锁到30秒必定释放。如果你希望锁能跟着业务执行时长自动续期就不要传leaseTime改用lock.lock()或lock.tryLock(waitTime, TimeUnit.SECONDS)。这个差异很多人不知道后面还会专门说。3. 方式二手动编程式配置把RedissonClient当成可控的Bean3.1 Config对象是Redisson的装配图第二种方式不引Starter直接在代码里创建Config对象手动构建RedissonClient。核心思路就是先把装配图画出来再让Spring容器管理RedissonClient的生命周期。Configuration public class RedissonConfig { Bean(destroyMethod shutdown) public RedissonClient redissonClient() { Config config new Config(); config.useSingleServer() .setAddress(redis://127.0.0.1:6379) .setPassword(yourPassword) .setConnectionPoolSize(64) .setConnectionMinimumIdleSize(24) .setTimeout(3000) .setRetryAttempts(3) .setRetryInterval(1500); config.setCodec(new JsonJacksonCodec()); return Redisson.create(config); } }这段代码有几点值得说明setAddress里的地址必须带redis://或rediss://前缀直接写127.0.0.1:6379会抛异常。rediss://是加了TLS的地址格式安全要求高的环境要用。connectionPoolSize和connectionMinimumIdleSize控制连接池大小timeout是等待Redis响应的最大毫秒数retryAttempts和retryInterval决定一次命令失败后重试几次、间隔多久。这些参数可以按业务量调整不要照抄默认值。setCodec(new JsonJacksonCodec())是我特别想强调的一步。Redisson默认序列化方案在跨系统读写数据时可能和你Redis里的历史数据格式不兼容统一用JsonJacksonCodec能显著降低理解成本也方便排查问题。如果项目里没有Jackson依赖Spring Boot默认自带放心使用。很多教程到这就结束了但我建议你再补一个RedissonClient关闭的钩子。上面代码里写了destroyMethod shutdown这样Spring容器关闭时会调用Redisson的shutdown()方法优雅释放连接池。漏掉这一步应用重启时连接不会立即回收极端情况下会被系统判定为异常占用。3.2 注册RedissonClient Bean并顺手解决Spring Cache整合手动方式同样可以享用Spring Cache。配置一个CacheManagerBean public CacheManager cacheManager(RedissonClient redissonClient) { return new RedissonSpringCacheManager(redissonClient); }注册之后Cacheable这类注解就自动和Redisson的RMapCache打通了。相比Starter方式手动方式的优势在于所有配置都在代码里IDE跳转、断点、配置中心动态刷新都能直接作用到Config对象上排错路径非常清晰。手动配置也更适合多套Redis共存的场景。比如一个系统同时连业务Redis和单独做存储的Redis手动方式可以创建多个RedissonClientBean命名区分即可Bean(redisClientA) public RedissonClient redisClientA() { ... } Bean(redisClientB) public RedissonClient redisClientB() { ... }用Qualifier在业务代码里指定用哪一套灵活度比Starter高很多。3.3 手动方式与Starter方式真的“二选一”吗这里我想先说个结论它们不是非此即彼的关系而是互补关系。用了Starter不代表你完全不能碰Config想手动精细配置也可以不引Starter直接把Redisson核心包加进来。有一种常见操作项目用了Starter希望在自动装配基础上改改连接池大小。正确做法是定义一个RedissonAutoConfigurationCustomizer的实现Bean public RedissonAutoConfigurationCustomizer customizer() { return config - config.useSingleServer().setConnectionPoolSize(128); }千万别在Starter项目里再手动定义一个RedissonClientBean否则启动时会因为容器里存在两个RedissonClient实例而冲突报错。反过来如果你已经在用一个旧项目Redis连接配置来自加密配置中心或者同一服务需要连接多套Redis这类场景我更推荐纯手动方式。把Starter从依赖里拿掉核心包自己管代码即配置看起来更啰嗦但问题出现时你能以最快速度定位到是哪一行代码构建了错误的连接。4. 两种方式同台对比配置差异只是表面坑在细节4.1 一张表看清两种方式的差异对比维度官方Starter自动装配手动编程式配置依赖复杂度低一个starter包搞定较高需自己管理redisson核心包与相关模块配置入口application.yml 可选redisson.yaml代码里的Config对象核心Bean注册自动完成省心且不容易遗漏手动Bean完全可控Spring Cache集成自动注册CacheManager需自行声明CacheManager自定义序列化通过customizer或yaml配置代码里直接setCodec多套Redis支持难度较高需要侵入额外Bean定义天然支持声明多个Client即可可调试性一般自动装配是半黑盒高代码即文档断点随便打适合场景新项目、标准化配置、快速落地配置复杂、多Redis、加密配置、平台型项目表格里没有对错只有合适不合适。大多数团队新项目起步时选Starter是对的因为配置量最少踩坑成本最低。4.2 最容易踩的三个坑第一个坑是RedissonClientBean冲突。这是Starter方式手动方式混用的典型问题。自动装配已经注册了一个原生RedissonClient你又手动注册一个Spring容器启动时发现两个同类Bean直接NoUniqueBeanDefinitionException。我之前看到过有人为了改一个连接池参数在config类里写Bean返回RedissonClient结果启动报错后来才知道应该用RedissonAutoConfigurationCustomizer。第二个坑是版本不匹配。Redisson版本和Spring Boot版本不是完全无关的尤其在使用Starter时它内部要适配spring-data-redis的版本。Spring Boot 3.x强行用Redisson老版本常见的现象是启动时java.lang.NoClassDefFoundError错误指向org.springframework.data.redis.*下的类。这不是代码逻辑问题纯粹是依赖矩阵没对上。第三个坑是序列化不兼容。Redisson默认的序列化方案可能和你系统里已有的字节数据不兼容。我遇到过用Jedis写进去的普通字符串用Redisson读出来变成一串乱码最后统一Codec为JsonJacksonCodec并保证写入方和读取方一致才解决。如果你改造一个存量系统建议先在测试环境对存量Key做一轮读写验证再上线。4.3 我推荐的选择逻辑如果你要写一个新服务团队又没有历史包袱直接用Starter。它帮你省下的配置量足够抵掉那一点点“黑盒感”。如果你要做中间件、基础平台或者项目里Redis拓扑复杂、需要连接多套集群那就手动方式。从我的实际经验看Starter项目的日常业务开发基本感知不到Redisson的存在因为只需要注入RedissonClient就行而手动方式的项目在交接时需要多花十分钟把配置代码讲清楚。但长期运维时手动方式又更容易排查问题。两边的天平随着项目复杂度变化而倾斜你不能只背一个答案。5. 整合完之后连接参数、集群切换和故障排查5.1 连接池与超时参数建议值不管用哪种方式整合最终落到Config上的连接参数都值得认真对待。以单机模式为例我惯用的一组参考值setConnectionPoolSize(128)连接池上限。默认64高并发扣库存、秒杀类场景建议调到128避免线程等待连接。setConnectionMinimumIdleSize(32)最少保持空闲连接数。默认24定时任务密集但平时流量低的系统这个值可以保持默认甚至调低。setTimeout(3000)客户端等待Redis响应超时单位毫秒。业务对延迟敏感可以降到1500但别低于Redis慢查询耗时否则正常请求也会被误杀。setRetryAttempts(3)、setRetryInterval(1500)命令失败重试3次间隔1.5秒。对实时性要求高的场景重试次数改成2更合适。这些参数没有银弹核心思路是不要让连接池满了以后大量线程直接卡死也不要让超时设置小于Redis正常响应耗时。5.2 从单机切到哨兵/集群的改造点很多项目一开始单机Redis后面要切哨兵或集群。手动方式改动量很小// 哨兵模式 config.useSentinelServers() .addSentinelAddress(redis://127.0.0.1:26379) .setMasterName(myMaster) .setPassword(yourPassword); // 集群模式 config.useClusterServers() .addNodeAddress(redis://127.0.0.1:6379, redis://127.0.0.1:6380) .setPassword(yourPassword);用Starter方式的话在spring.data.redis下配置sentinel或cluster节点即可。这里有两个必须注意的变化第一集群模式下多节点的连接语句、密码配置都会变化千万别沿用单机那一套第二Redis Cluster不支持多个database单机模式下的database: 2在集群里不生效代码里对库的选择逻辑要提前改造。从单机迁移时先验证锁可以正常获取和释放再看缓存读写最后跑一轮并发压测顺序不要反。5.3 验证分布式锁正确性的压测与故障演练锁加完不是终点得证明它真的能抗并发。最简单的验证在业务临界区代码里加一行日志记录“线程名-时间戳-实例IP”用JMeter开20个并发线程打同一个接口观察日志里是否存在相同时间戳下两个不同实例同时进入临界区的记录。如果现象复现说明锁没起作用回去检查锁Key是否同一个。故障演练也很重要。模拟Redis宕机观察业务线程的表现。Redisson的默认行为是尝试连接失败、超时抛出异常你要保证异常路径不会把核心接口拖死。最简单的手段是给tryLock的等待时间设一个合理的上限拿不到锁快速失败返回而不是无限自旋。释放锁的姿势也要养成肌肉记忆unlock()之前先isHeldByCurrentThread()判断一下避免拿着别人的锁或者锁已过期时调unlock()抛IllegalMonitorStateException。我在过去几个项目里的最终体会是前期无脑上Starter开发效率最高后期遇到配置加密和多套Redis共存的场景又退回来用手动方式。两种方式不是对立关系关键是你是否清楚RedissonClient的生命周期由谁管理、配置怎么来的。如果让我接手一个新项目我会先看团队的现有Redis配置体系再做选择不会为了形式上“优雅”去手写一堆Config。Redisson这种工具真正该关注的不是连接代码怎么写而是你用它的锁和缓存时有没有把边界问题想清楚。
