在实际项目中,用 Redis 做分布式锁、缓存预热、接口幂等性控制,几乎是 Java 后端开发绕不开的日常操作。而 Redisson 作为 Redisson 官方出品的 Java 客户端,能提供分布式锁、限流器、消息队列、本地缓存等一系列高级能力,很多团队都选择了它,却在集成方式上纠结过:到底是直接引入redisson原始依赖、手工配置RedissonClient,还是走 Spring Boot Starter 的自动配置?这两条路线我都完整走过一遍,这篇就把两套流程从头到尾掰开讲清楚,包括项目里的配置代码、YAML 写法、Bean 初始化细节、实测下来的坑,尽量让初次接手的开发者照着文档也能搞定,不再踩我当年踩过的坑。
Redisson 的集成难点不在 API,而在 Spring 容器管理、配置源绑定、版本匹配这几块的取舍。原始依赖方式灵活、轻量,适合你希望完全掌控客户端生命周期、不依赖自动配置魔法的场景;Starter 方式封装度高、配置量小,适合走 Spring Boot 习惯路线、想快速接入并依赖官方默认约定的项目。下面我按项目实际落地的顺序来写,从依赖、配置、Bean、使用到验证,一条龙走完,再补一个避坑清单。
1. 项目整体设计与两种集成路线的选择
1.1 核心需求梳理:为什么需要 Redisson
先说清楚我们为什么引入 Redisson,而不只是用RedisTemplate或者直接操作 Jedis/Lettuce。
在项目里,Redis 作为分布式缓存和数据共享存储,本身可以解决很多问题。但一旦涉及分布式场景下的并发控制,比如秒杀减库存、订单创建幂等、定时任务多实例互斥,就需要一个可靠的分布式锁。原生 API 手工SETNX加锁、lua脚本解锁,虽然也能实现,但有很多细节需要自己维护:锁自动续期、锁超时、可重入、公平锁、红锁等。Redisson 把这一整套封装成了开箱即用的RLock,还顺带提供了RMap、RAtomicLong、RCountDownLatch、分布式限流RRateLimiter等高级结构,省下的开发时间相当可观。
除此之外,Redisson 自身实现了一个高效且具备本地缓存能力的RLocalCachedMap,可以在 Redis 和本地内存之间做两级缓存同步。对于热点数据频繁读取的场景,这个能力对降低 Redis 负载有立竿见影的效果。所以项目引入 Redisson,往往是冲着分布式锁和缓存加速这两大核心能力去的。
1.2 两套方案对比:原始依赖与 Spring Boot Starter
我开始集成时走得是 "原始依赖" 路线:在pom.xml中只加入redisson的坐标,然后用Config类手动创建RedissonClient实例,并把这个实例以@Bean的形式注册到 Spring 容器。好处是依赖只有一个,配置逻辑完全掌握在自己手里,不依赖 Spring Boot 自动配置机制,适合项目对依赖精简、启动过程可控性要求高的场景。坏处也很明显:所有配置项都要手写代码映射,配置文件中配置类属性需要手工绑定,稍微不够"Spring Boot 化"。
后来在另一个微服务项目里,我换成了redisson-spring-boot-starter自动配置方式。只需要在pom.xml中引入官方 Starter,再在application.yml中写 Redis 的地址、密码等配置项,Spring Boot 就会自动创建RedissonClientBean。这个方式的配置非常简洁,约定大于配置,对团队协作也更友好。不过它也引入了一个隐患——如果版本与 Spring Boot 主版本匹配不当,容易出现自动配置不生效或者客户端初始化失败的问题。
下面先看原始依赖路线怎么走通。
2. 原始依赖集成方式:手动配置才是彻底理解底层的捷径
2.1 引入原始依赖:需要注意的坐标和版本
原始依赖集成,第一步当然是加入 Maven 坐标。注意 Redisson 的 groupId 是org.redisson,artifactId 是redisson。这里容易踩的第一个坑是版本选择:我在早期项目里随手引入了一个较新的3.x版本,结果与项目里的 Netty 版本冲突,导致启动时一堆NoSuchMethodError。
建议不要手动指定太随意的版本,最好在 Spring Boot 项目的properties中显式声明版本,并且优先选用与当前 Spring Boot 大版本兼容的官方 Release 版本。Redisson 的 GitHub 仓库会发布针对不同 Spring Boot 版本的适配版本,你可以在仓库的 Issue 或 Release Notes 中看到推荐版本号。合理做法是在依赖配置中声明如下:
<properties> <java.version>17</java.version> <spring-boot.version>3.2.5</spring-boot.version> <redisson.version>3.27.2</redisson.version> </properties> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>${redisson.version}</version> </dependency>如果项目已经通过 Spring Boot 父 POM 引入了spring-boot-dependencies,Redisson 并非其托管依赖,所以必须显式写版本号。我见过不少同事在复制粘贴后忘了版本号,导致 Maven 直接强制版本为RELEASE的情况,编译虽然能过,但启动时一堆奇奇怪怪的兼容问题,排查起来很费时间。
2.2 配置核心对象 RedissonClient:单机、集群与 Sentinel 方式
在原始依赖模式下,RedissonClient的初始化是通过org.redisson.config.Config对象完成的。这也是两条集成路线中我强烈建议开发者至少手写一遍的地方,因为你对底层连接方式的理解会深很多。
单机模式的配置示例:
@Configuration public class RedissonConfig { @Value("${spring.redis.host:127.0.0.1}") private String redisHost; @Value("${spring.redis.port:6379}") private int redisPort; @Value("${spring.redis.password:}") private String redisPassword; @Bean(destroyMethod = "shutdown") public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer() .setAddress("redis://" + redisHost + ":" + redisPort) .setPassword(redisPassword.isEmpty() ? null : redisPassword) .setConnectionMinimumIdleSize(10) .setConnectionPoolSize(50) .setIdleConnectionTimeout(60000) .setConnectTimeout(10000) .setTimeout(3000) .setRetryAttempts(3) .setRetryInterval(1500) .setDatabase(0); return Redisson.create(config); } }注意这里destroyMethod = "shutdown"很关键。RedissonClient并不是 Spring 标准生命周期组件,如果不在 Bean 销毁时调用shutdown(),应用关闭时 Netty 线程池不会优雅释放,在频繁重启的测试环境容易造成连接泄漏。我见过一个同事因为漏写这个属性,在本地重启了三十多次之后 Redis 连接数暴涨,被 DBA 直接发了警告邮件。
集群模式也很常见,配置方式变成了useClusterServers():
@Bean(destroyMethod = "shutdown") public RedissonClient redissonClientCluster() { Config config = new Config(); config.useClusterServers() .addNodeAddress("redis://192.168.1.101:6379", "redis://192.168.1.102:6379") .setScanInterval(2000) .setMasterConnectionMinimumIdleSize(10) .setMasterConnectionPoolSize(50) .setSlaveConnectionMinimumIdleSize(10) .setSlaveConnectionPoolSize(50); return Redisson.create(config); }集群模式下scanInterval是节点变动感知间隔,如果集群节点扩容或故障转移,客户端能更及时发现。这套手动配置模式的好处是每个参数都在代码里显式可见,出错时可以直接定位;坏处是配置文件中的连接参数无法自动绑定,我上面的写法通过@Value注入,是一种折中做法,比硬编码好维护多了。
2.3 编写一个可直接注入使用的工具类或封装Service
光有RedissonClient还不够,通常还需要封装一层 Service,避免业务代码直接散落一堆getLock()、unlock()的样板代码。我的做法比较简单:
@Service public class DistributedLockService { private final RedissonClient redissonClient; public DistributedLockService(RedissonClient redissonClient) { this.redissonClient = redissonClient; } public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit, Supplier<T> supplier) { RLock lock = redissonClient.getLock(lockKey); boolean locked = false; try { locked = lock.tryLock(waitTime, leaseTime, unit); if (!locked) { throw new RuntimeException("获取分布式锁超时, key=" + lockKey); } return supplier.get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("线程中断获取锁失败", e); } finally { if (locked) { lock.unlock(); } } } }为什么要封装成Supplier<T>形式?因为锁的目的是保护一段资源操作,拿到锁之后执行的逻辑往往是动态的。封装成函数式接口后,业务侧代码只需要一行调用就能完成加锁、执行、解锁全流程,代码变得清爽。同时,把锁的 key 设计、超时时间划分收敛到集中的工具类里,后续调整waitTime、leaseTime时不用满项目找。
关于leaseTime要注意:如果传了leaseTime,Redisson 不会自动开启看门狗续期机制,锁在指定时间后会自动释放;如果不传leaseTime(使用tryLock(waitTime, timeUnit)重载),Redisson 默认开启看门狗续期,默认锁过期时间是 30 秒,每 10 秒自动续期一次。这个选择要结合业务场景,如果明确知道业务执行时间不会超过某个阈值,传leaseTime是安全的;如果是未知耗时的长任务,建议不传leaseTime靠看门狗续期。
2.4 原始集成方式的适用边界与取舍
原始依赖集成方式并不是银弹,它的适用边界主要在于:对第三方组件初始化有强控制要求的项目,或者不想让 Spring Boot 自动配置参与太多"隐式魔法"的场景。比如你手头项目里已经有一个自己的 Redis 连接工厂类的私有封装,或者你依赖的 Redisson 版本是内部定制的二次开发包,这种场景下 Starter 自动配置反而碍事,手动 Bean 方式才是正解。
不过,一旦项目里有多个RedissonClient实例(比如连接多个 Redis 集群),手动方式的可读性就会快速下降,因为你不得不在配置类里为每个实例编写@ConfigurationProperties绑定类或复制粘贴一堆@Value。这个时候 Spring Boot 的自动配置机制 +Properties类绑定的优势就很明显了。下一个章节展开 Starter 集成方式。
3. Spring Boot Starter 集成方式:自动化配置的核心流程
3.1 引入 Starter 依赖:关注版本矩阵
Starter 集成的第一步是替换依赖,把org.redisson:redisson换成org.redisson:redisson-spring-boot-starter。坐标如下:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>${redisson.version}</version> </dependency>这个 Starter 与传统依赖共存会有冲突风险。如果同时引入了redisson和redisson-spring-boot-starter,会自动触发两套初始化路径,极易产生RedissonClientBean 重复或 Netty 线程池资源浪费,排查起来云里雾里。所以用 Starter 时,记得移除手动redisson依赖。
版本匹配是 Starter 集成最需要认真对待的事情。Redisson 官方在发布时会对齐 Spring Boot 版本,比如面向 Spring Boot 2.x 的 starter 与面向 3.x 的 starter 是有差异的。看一眼redisson-spring-boot-starter的 POM 就会发现,它内部传递引入了org.redisson:redisson-spring-boot-autoconfigure,这个模块又区分javax与jakarta两种命名空间,分别对应 Spring Boot 2.x 和 3.x。简单说:Spring Boot 3.x 项目必须选择 Redisson 3.20+ 且确认 autoconfigure 模块用的是jakarta,否则启动时会报ClassNotFoundException: javax.annotation.Resource之类的错误。
我建议在项目初始化时就统一整理一张版本清单,明确 Spring Boot、JDK、Redisson 三者的对应关系,防止后续升级时踩坑。
3.2 application.yml 配置项详解
引入 Starter 后,配置工作大幅简化。核心配置只需要在application.yml中按官方 schema 编写:
spring: data: redis: host: 127.0.0.1 port: 6379 password: database: 0 redisson: config: | singleServerConfig: address: "redis://127.0.0.1:6379" password: null connectionMinimumIdleSize: 10 connectionPoolSize: 64 timeout: 3000 connectTimeout: 10000 retryAttempts: 3这里有一个重大变化值得注意:从 Spring Boot 2.x 到 3.x,官方 Redis 的配置前缀从spring.redis变更为spring.data.redis,而 Redisson 的配置前缀是spring.redisson。不少迁移项目在升级 Boot 3.x 时只改了spring.redis→spring.data.redis,却忘了检查spring.redisson的配置结构,导致 RedissonClient 静默回退到本地默认地址127.0.0.1:6379,连不上远程 Redis 才发现问题。
Starter 支持两种配置风格:一种是上面这种通过config直接内联一段 YAML 字符串;另一种是使用spring.redisson.file-config指定一份外部 Redisson 配置文件。我更推荐内联config方式,因为所有配置都在主配置文件中,环境隔离时只需要区分不同 profile 的 YAML 即可,不用额外维护 redisson 独立配置文件的打包问题。
3.3 Starter 自动装配机制解读:RedissonAutoConfiguration 干了什么
很多人把 Starter 当作黑盒,用完就忘。但在排查问题时不懂自动装配机制会非常吃亏。简单拆解一下RedissonAutoConfiguration的工作过程。
Starter 通过spring.factories(Boot 2.x)或AutoConfiguration.imports(Boot 3.x)注册自动配置类。自动配置类核心逻辑是:
- 读取
spring.redisson下的配置属性,如果找不到则检查spring.data.redis(或spring.redis)下的配置; - 构造
Config对象; - 调用
Redisson.create(config)创建RedissonClient; - 同时注册
RedissonTemplate、RedissonRxClient、RedissonReactiveClient等可选扩展 Bean。
关键细节在于:自动配置类使用了@ConditionalOnMissingBean、@ConditionalOnClass这类条件注解,也就是说如果你自己已经定义了一个RedissonClientBean,自动配置会直接让位,不会重复创建。这种方式很灵巧,但也会带来一个隐蔽问题——调试时如果你觉得 Starter 配置没生效,可能不是它没生效,而是你的自定义 Bean 把自动配置屏蔽了。
3.4 Starter 集成的典型使用代码
Starter 集成后,业务代码与原始依赖方式几乎没有差别,因为最终拿到的都是RedissonClient。例如:
@RestController @RequestMapping("/demo") public class DemoController { private final RedissonClient redissonClient; public DemoController(RedissonClient redissonClient) { this.redissonClient = redissonClient; } @GetMapping("/lock") public String lockDemo() { RLock lock = redissonClient.getLock("demoLockKey"); boolean locked = false; try { locked = lock.tryLock(3, 10, TimeUnit.SECONDS); if (!locked) { return "未获取到锁"; } // 模拟业务 Thread.sleep(2000); return "成功执行业务逻辑"; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "执行中断"; } finally { if (locked && lock.isHeldByCurrentThread()) { lock.unlock(); } } } }这段代码里lock.isHeldByCurrentThread()是最后一道保险。在可重入锁场景下,同一线程多次lock(),需要在finally中判断当前线程确实持有锁再释放,否则可能把别人的锁误删。Redisson 底层解锁是通过 Lua 脚本校验线程标识的,直接unlock不会误删别人的锁,但会在当前线程未持锁时抛IllegalMonitorStateException,因此这个判断能保证不出现异常堆栈。
3.5 Starter 集成方式的核心优势
Starter 最大的优势是把配置与 Spring 环境绑定,配置文件即代码。spring.redisson.config内联配置能利用 Spring Boot 的@ConfigurationProperties机制做配置源绑定,配合@RefreshScope、Nacos 配置中心,可以动态刷新连接配置——这是原生依赖方式需要额外开发工作量才能实现的。
另外,Starter 还自动注册了RedissonTemplate,封装了常用操作:
@Autowired private RedissonTemplate redissonTemplate;它可以简化 Redis 的 key/value 存取操作,内部已经做了序列化处理,比直接用RedissonClient.getBucket()方便一些。不过我这里要提醒一句:RedissonTemplate的 API 覆盖范围远不如原生客户端全面,如果你的业务流程大量使用RMap、RQueue、RTopic等高级结构,直接用RedissonClient更顺手。
4. 两条路线对比:选择依据与项目实战取舍
4.1 核心差异对照表
为了让你在架构评审时能拿出有说服力的材料,我整理了一张对照表,从多个角度对比两条路线:
| 对比维度 | 原始依赖(redisson) | Spring Boot Starter |
|---|---|---|
| 依赖数量 | 仅一个核心包 | Starter + autoconfigure |
| 配置方式 | Java Config 手动创建 Bean | YAML 声明式绑定 |
| 自动配置 | 不使用,完全手动 | 自动装配,条件注解控制 |
| 版本管理 | 自行管理,需手动查兼容性 | 依赖内部 POM 管理一部分传递依赖 |
| 配置中心支持 | 需自研属性刷新逻辑 | 天然支持配置源与环境绑定 |
| 多数据源支持 | 可定义多个 Bean,但代码量大 | 自动配置只创建一个 Bean,多数据源需绕行 |
| 学习成本 | 需理解 Config API | 需理解自动装配与版本矩阵 |
| 启动可控性 | 完全可控,启动过程透明 | 自动装入,部分行为有隐藏逻辑 |
| 适合场景 | 定制需求多、二次开发、内部统一封装 | 标准单集群、快速接入、规范团队协作 |
从我的项目经验看,如果团队有多个服务共享一个自定义的 Redis 初始化工具包,原始依赖方式更适合做统一收敛;如果是常规微服务,Starter 方式写起来最舒服,也更容易让新成员接手。
4.2 混合使用与迁移策略
在实际维护中,我也见过不少项目走了一个"混合路径":保留 Starter 依赖,但通过自定义@Configuration里的@Bean方法手动覆盖RedissonClient。这样既能享受 Starter 带来的配置绑定便利,又能对客户端进行二次定制(比如加入自定义NettyHook、自定义Codec、统一的连接监听器)。实现方式很简单——在@Configuration类中自己声明 Bean,由于自动配置类有@ConditionalOnMissingBean,你的自定义 Bean 会优先生效。
如果你现在还在使用原始依赖方式,想要平滑迁移到 Starter,我建议按这种顺序操作:
- 先在项目中引入 Starter 依赖,同时保留原始
redisson依赖,但把Config的@Bean方法注释掉,观察启动日志确认自动配置是否正确创建了 Bean; - 验证自动配置创建的客户端能正常加锁解锁;
- 删除自定义
@Bean方法及Config类,把所有参数迁移到 YAML; - 跑两轮集成测试,重点检查 Redis 连接池、锁的公平性、看门狗续期行为。
迁移后还要留意一个细节:Starter 方式在默认情况下使用的Codec是Kryo5Codec(一些旧版是MarshallingCodec),而手动配置时很多人习惯用JsonJacksonCodec。这会导致 Redis 中已有缓存数据反序列化时类型不对应,报错或者读出 null。遇到这种情况,直接在 YAML 中指定 codec 即可:
spring: redisson: config: | codec: !<org.redisson.codec.JsonJacksonCodec> {}这个语法是 YAML 的标签类型表示法,用来在 Redisson Config 反序列化时指定具体类。第一次写时很容易漏掉!<...>格式,直接写JsonJacksonCodec字符串,读取配置时会报类型转换错误。
4.3 真实项目中的选型倾向
从我接触到的多个中大型后端项目来看,绝大多数标准 Spring Boot 服务最终都落到了 Starter 方式上。原因很现实:团队协作中配置维护成本最低,接入监控、修改参数都可以通过配置中心热更新,不用频繁发版。但如果你维护的是底层公共组件或公司级中间件,原始依赖方式更合适——Starter 自动装配的"黑盒"特性会让下游使用者的异常排查路径变长,不利于快速定位问题。
有一次我在公司内部做组件升级时,就是因为某个基础库从原始依赖切换成 Starter,结果下游十几个服务的启动时间变长了一点,排查下来发现是 Starter 自动配置加载了本地缓存、事件总线等额外组件,导致初始化时多了许多无用的 Bean 创建。这类隐式开销在原始依赖方式下是完全不会出现的。
5. 核心实操经验:常见问题、踩坑记录与排查技巧
5.1 版本匹配问题:ClassNotFoundException 与 NoSuchMethodError
我在不同项目里遇到最多的两类问题,一是javax与jakarta命名空间冲突,二是Netty版本冲突。
javax问题通常出现在 Spring Boot 3.x 项目配了旧版 Redisson Starter 时。报错信息很直白:
java.lang.ClassNotFoundException: javax.annotation.Resource但其实真正的原因可能是 Starter 内部引入了javax.annotation-api或者spring-boot-starter-web中的旧传递依赖。快速解法是升级 Redisson 到 3.25+,并确认redisson-spring-boot-starter依赖的redisson-spring-boot-autoconfigure中pom.xml的依赖是jakarta.servlet-api而不是javax.servlet-api。不放心的话,在 IDE 中直接搜索项目的依赖树,过滤javax.annotation相关的包即可。
Netty 版本冲突的表现则是编译通过、运行时NoSuchMethodError。由于 Spring Boot 3.x 内置的 Netty 版本与 Redisson 内部要求的 Netty 版本可能不同,我建议在 POM 中直接使用 Redisson 传递过来的 Netty 版本为主,或者显式指定一个同时兼容双方的版本。遇到这个报错时,别慌,在 Maven 依赖树中筛出所有io.netty:netty-*依赖,检查版本号是否统一,再决定是exclusion还是dependencyManagement强制定版。这两种方式我都在不同项目里验证过,后者更稳定,避免重复引包。
5.2 序列化导致的数据错乱
Redisson 的默认序列化策略在不同版本中不同,如果你往 Redis 中写入了一堆缓存数据后升级了 Redisson 版本,很可能发现数据读取不了。这个问题的根因是Codec的变化。例如,手动配置方式默认Codec是JsonJacksonCodec,而 Starter 自动配置模式下可能是Kryo5Codec。这两种编解码器对同一个对象的序列化结果完全不同。
所以,重要数据写入时最好在全局配置里指定Codec,并且在升级 Redisson 前分别读取和验证 Redis 中现有数据。我的建议是:
spring: redisson: config: | codec: !<org.redisson.codec.Kryo5Codec> {}或者如果团队普遍对 JSON 更熟悉:
codec: !<org.redisson.codec.JsonJacksonCodec> {}但注意,JsonJacksonCodec序列化后的对象在 Redis 中不带类名元数据,反序列化时依赖Class<?>推断,所以RMap读取时如果值对象类型是接口或父类,有可能反序列化错误。这种情况下,使用TypedJsonJacksonCodec能减少问题。
5.3 看门狗续期:锁为什么没有在预期时间释放
Redisson 官方宣传的看门狗机制很美好,但也坑过不少人。看门狗生效的前提是:在获取锁时没有指定leaseTime。如果业务代码里调用了lock.tryLock(3, 30, TimeUnit.SECONDS),那么看门狗不会启动,锁在 30 秒后必然释放。注意,如果业务执行时间超过了leaseTime,其他线程就会拿到锁,产生并发冲突。
我踩过的一个实际案例是:一个定时任务在集群部署下需要互斥执行,我一开始用了tryLock(10, 30, TimeUnit.SECONDS),看似稳妥,结果某个批次数据量暴增,任务实际跑了 40 多秒,另一个节点在第 30 秒时成功拿到了锁,同一批数据被处理了两遍。后来改为不传leaseTime,只传waitTime,让看门狗自动续期,问题才解决。
所以我的经验是:如果业务执行时长不确定,优先使用tryLock(waitTime, TimeUnit.SECONDS),让 Redisson 自动续期。并且要理解看门狗的续期原理——Redisson 在获取锁后,会定时向 Redis 发送 Lua 续期指令,但客户端长时间阻塞或 GC 停顿可能导致续期不及时,锁提前释放。因此核心业务中,锁内操作要避免长时间网络 IO、避免大对象 GC 停顿。
5.4 连接池参数设置不当导致锁获取超时
在压测阶段,我发现锁获取超时比例很高,但 Redis 本身压力并不大,最后定位是connectionPoolSize设置过小。当并发量大时,多个线程同时竞争调用 tryLock,连接池被占满,队列中等待的请求排队太久,看起来就像获取锁超时。
合理设置连接池的思路是:结合应用的单机 QPS、平均锁持有时间、Redis 实例规格推算。假设平均锁持有时间 200ms,单机对锁操作的 QPS 是 500,那么理论上需要的并发连接数为500 * 0.2 = 100,而 Redisson 默认connectionPoolSize是 64,此时可能不够。因此在压测后我会把connectionMinimumIdleSize(默认 10)和connectionPoolSize(默认 64)都调大一些,比如前者 32、后者 128。也要防止过大,连接数太多会占用 Redis 端连接文件描述符,反而拖垮性能。
5.5 在 Spring AOP / 事务中使用的注意事项
Redisson 的锁操作与 Spring 事务是两套机制,没有自动协同。如果在@Transactional方法内使用 Redisson 锁,要注意锁的释放是在业务方法退出后,但事务可能还要等到数据库提交完成才真正结束。这个窗口期内,其他线程可能已经拿到锁读到旧数据。要规避这个场景,我一般建议把锁的范围放大到事务方法之外,或者使用编程式事务,确保锁的释放发生在事务提交之后。
另外一个细节是:RLock的锁粒度默认是进程内可重入,如果你在一个请求中多次调用getLock获取同一个 key 的锁,Redisson 会维护同一客户端下的加锁计数。但如果锁内发起了异步线程且异步线程也去获取同一个锁,它会因为缺少线程标识而拿不到锁,表现就是异步线程死等或者获取锁失败。这类跨线程加锁问题,官方没有直接 API 支持,我用RCountDownLatch或者其他信号量结构绕过。
5.6 主从切换与锁安全:RedLock 的必要性分析
还有一个团队经常讨论的话题:主从模式下如果 Redis master 挂掉,锁怎么办。默认情况下,Redisson 的分布式锁不保证强一致,master 保存着锁数据,如果 master 宕机且数据未同步到 slave,锁就丢了,其他客户端可能同时获得锁。在资金类、库存类等强一致场景下,可以启用 Redisson 的多节点 RedLock 方案,但 RedLock 本身有争议且对 Redis 部署有额外要求。我的看法是:对于大多数非资金类的互联网业务,单集群足够的可靠性已经能接受了,没必要为了极端场景付出高成本;但如果业务对一致性要求极高,就要架构层面单独评估,而不是简单套个 RedLock 就当万事大吉。
5.7 常见问题速查表
下面这张表是我在实际项目中养成的排查习惯,遇到问题先对照一遍:
| 现象描述 | 最可能原因 | 解决方向 |
|---|---|---|
启动时ClassNotFoundException: javax.annotation.Resource | Redisson 版本与 Spring Boot 3.x 不匹配 | 升级 Redisson,确认 autoconfigure 使用 jakarta |
运行时报NoSuchMethodError | Netty 传递依赖冲突 | 排查依赖树,统一 Netty 版本 |
| RedissonClient 连不上远程 Redis | spring.redisson配置前缀不对或未配置 | 确认spring.redisson.config配置块内容 |
| 锁提前释放导致并发问题 | 指定了leaseTime,看门狗未启动 | 改用不传leaseTime的方式 |
| 缓存数据读取出错 | Codec 不匹配 | 统一指定 codec,或清缓存后重新加载 |
| 锁获取超时频频 | 连接池连接数过小 | 增大connectionPoolSize,控制锁内耗时 |
| 自动配置不生效 | 自定义RedissonClientBean 存在 | 移除自定义 Bean 让自动配置生效 |
| 锁丢失 | 主从切换未同步 | 评估 RedLock 或多方案组合 |
6. 一些个人实战心得
在 Spring Boot 项目里集成 Redisson,本质是处理"一个第三方客户端与 IoC 容器"之间的配合关系。原始依赖方式让我把Config、RedissonClient、RLock这些底层概念理解透了,Starter 方式则让我体会到自动配置在标准场景下的高效率。如果你现在正卡在"为什么我这样配不行"的问题上,我建议先把自动配置逻辑关闭,手写一遍原始依赖,往往能更快发现真正的问题点。
我再分享一个常用的小技巧:在本地测试时,经常需要验证锁是否真的生效。可以启动两个 Spring Boot 实例(修改端口),然后用同一个 key 并发触发接口,观察是否只有一个实例能进入临界区。如果两个实例都进入了,十有八九是实例之间用了不同的 Redis database,或者配置文件的连接地址写成了localhost导致连接的不是同一个 Redis。这个验证方式虽然土,但比单测可靠得多。
另外,如果是重构现有项目,接口的幂等性控制、缓存延时双删这些场景也都可以顺势用 Redisson 简化。比如延迟双删可以改用RTopic的发布订阅机制,让所有节点同时收到删除缓存的指令,比轮询数据库变更表的方式优雅不少。Redisson 集成工作做完之后,项目的分布式基础设施也算是补齐了一块重要拼图。