资讯动态

Spring Boot整合Redisson:自动配置与手动Bean全解析

发布时间:2026/10/3 4:34:43 来源:尧图企业网站定制
在项目里碰到“多实例部署后原本的单机锁失效了”这种问题的时候我第一次把目光从 Jedis 转向了 Redisson。当时订单服务要限流、要分布式锁、要缓存一致性网上一搜全是 Redisson 的零星片段大多只告诉你“加个依赖就行”但真正上手的时候我发现“Spring Boot 整合 Redisson”这个短语背后其实有两条完全不同的路一条是官方 Starter 的自动配置一条是自己写Configuration手动建 Bean。这两条路我都完整走过踩过的坑也算不少所以打算把两种方式的来龙去脉、配置细节、适用场景全部写清楚给后来的人省点时间。1. 为什么需要“整合方案”Redisson 到底解决了我的哪些问题1.1 它和 Jedis/Lettuce 完全不是一个物种很多人一提到 Redis 客户端脑海里就是 Jedis 和 Lettuce。Jedis 是直连式的经典客户端API 简单但实例本身不是线程安全的并发场景下必须靠连接池来保证安全Lettuce 是 Spring Boot 2.x 以来默认的客户端基于 Netty 实现线程安全、支持响应式编程性能也足够好。但这两个客户端本质上做的事情是一样的把 Redis 命令翻译成 Java 方法调用。Redisson 不一样。它同样基于 Netty但它更准确地说是“基于 Redis 的分布式 Java 对象框架”。它把 Redis 操作用分布式锁、分布式队列、分布式信号量、原子计数器、限流器这些高层 API 封装好了。换句话说Jedis 和 Lettuce 是拿着 Redis 协议去敲命令Redisson 是直接给你一把现成的RLock你调用lock()和unlock()就行。我在迁移之前自己也尝试过用 Redis 的SETNX加 Lua 脚本去写分布式锁初版逻辑是对的。可一旦服务节点增多、业务执行时间变长、过程中发生 GC 停顿或者网络抖动问题就冒出来了没有续期机制、锁提前过期、两个实例同时进入临界区。这些边界细节靠手写很难一次搞定而 Redisson 内置的看门狗机制和可重入锁设计恰好把这些问题全部兜住了。1.2 两个让我决定迁移 Redisson 的真实案例第一个案例是定时任务。我们的清算服务部署了两台实例每天晚上要跑一次批量结算任务。如果用 Spring 自带的Scheduled两台实例会同时触发重复计算造成的资损问题非常直接。当时临时方案是“一台实例手动停掉定时器”这显然不是长久之计。引入 Redisson 之后任务执行前先获取分布式锁只让拿到锁的实例干活问题就消失了。第二个案例是秒杀抢购场景。商品库存只有几百件但入口流量能到几十万。直接用数据库行锁扛不住压力用 Redis 计数器又面临超卖风险。用 Redisson 的RLock把“库存扣减”这段代码锁住配合RAtomicLong做库存扣减整个流程的并发安全性立刻有了保障。当然锁住了并不是说业务就完美了限流还要配合RRateLimiter来做但这个思路从一开始就让设计简单了很多。1.3 “整合”一个 Spring Bean 的标准到底要解决什么所谓整合本质上是三件事让RedissonClient这个核心对象由 Spring 容器统一管理让 Redis 地址、密码、连接池这些配置来源于应用配置而非写死的代码让应用关闭时可以优雅地释放连接资源。不管选择哪种方式最终目标都是得到一个可注入的RedissonClient。第一种方式由 Starter 自动完成上述所有工作第二种方式由我们自己手动完成。明白了这一点后面再选哪种方式就不纠结了。2. 方式一用 redisson-spring-boot-starter 让自动配置接管一切2.1 引入依赖之前先想清楚版本关系使用官方 Starter 只需要在pom.xml里增加一个依赖dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.17.0/version /dependency版本号这里值得多说一句。Redisson 的版本迭代比较活跃不同版本对 Spring Boot 版本和 JDK 版本都有兼容性要求。我实际踩过的例子是项目基于 Spring Boot 2.3.x JDK 8当时直接引入最新版 Redisson 3.18启动时出现类冲突最后退化到 3.16.5 才稳定。所以建议引入前先看一眼 Redisson 官方仓库的版本说明或者直接使用与当前 Spring Boot 大版本匹配的稳定版。如果不希望引入编排信息被 Starter 干扰也可以使用 BOM 方式统一管理版本dependencyManagement dependencies dependency groupIdorg.redisson/groupId artifactIdredisson-bom/artifactId version3.17.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement这样后续引入 Redisson 相关模块时就不用写版本号了升级管理也更方便。2.2 配置文件的两种写法内嵌 config 与外置 redisson.yamlStarter 默认读取spring.redis.redisson.*前缀的配置支持两种写法。第一种是直接在application.yml里内嵌一段 Redisson 配置spring: redis: redisson: config: | singleServerConfig: address: redis://127.0.0.1:6379 database: 0 password: timeout: 3000 connectionPoolSize: 16 connectionMinimumIdleSize: 4注意config后面是一个多行字符串每行缩进必须严格对齐。这个写法的好处是全部配置都在一个文件里部署时无需额外分发文件坏处是要小心 YAML 多行字符串的缩进一旦少了一个空格Redisson 解析配置时直接报错错误信息还比较隐晦经常要看半天才知道是缩进问题。第二种是单独维护一个redisson.yaml文件放到src/main/resources目录下singleServerConfig: address: redis://127.0.0.1:6379 database: 0 password: timeout: 3000 connectionPoolSize: 16 connectionMinimumIdleSize: 4然后在application.yml里指向它spring: redis: redisson: file: classpath:redisson.yaml我个人更推荐第二种。原因很实际Redisson 的配置文件一旦复杂起来比如启用集群模式、哨兵模式、加 SSL 参数、自定线程池参数内容会变得很长。把这些内容塞到 Spring Boot 的主配置里会让application.yml显得臃肿单独放一个redisson.yaml既方便阅读也能直接拿到 Redisson 提供的配置文件模板来改还不容易出现缩进问题。2.3 自动配置背后 Spring Boot 做了什么redisson-spring-boot-starter的底层逻辑并不神秘。Starter 包里包含一个RedissonAutoConfiguration这个类在META-INF/spring.factories中被声明。Spring Boot 启动时会根据条件装配把RedissonClient自动注册为容器中的一个 Bean。大致流程自动配置会解析刚才说的spring.redis.redisson.config或spring.redis.redisson.file属性然后把 YAML/JSON 内容解析成一个Config对象最后调用Redisson.create(config)生成RedissonClient实例并注册到 Spring 容器。与此同时它还会注册一个RedissonClient的关闭回调确保应用退出时资源能释放。了解这个流程对排查问题很有用。比如有时候你发现RedissonClient已经注入了但连接的 Redis 地址不是自己预期的那台这时候就应该去查配置属性是否真的被读取到了而不是去翻代码。熟悉自动配置的运作方式也能理解为什么这个方式被称为“开箱即用”。2.4 在业务代码里以注入方式使用配置完成之后代码里就非常简单了Service public class OrderService { Autowired private RedissonClient redissonClient; public void createOrder(OrderRequest request) { RLock lock redissonClient.getLock(order:create: request.getUserId()); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); } } }这种方式的优点很明显一行注入零模板代码。团队里哪怕没有专门研究过 Redisson 的成员也能快速上手。这也是网上大多数项目快速启动时选择的方式。3. 方式二不依赖 Starter自己写 Bean 拿到完全控制权3.1 最小依赖怎么加第二种方式不引入 Starter而是直接引入 Redisson 核心包dependency groupIdorg.redisson/groupId artifactIdredisson/artifactId version3.17.0/version /dependency这里要注意没有 Starter 的情况下Spring Boot 不会自动注册任何 Redisson 相关的 Bean。你需要在配置类里自己创建RedissonClient。3.2 一个完整的 Configuration 示例我项目里实际使用的配置类大概是这样的Configuration public class RedissonConfig { Value(${redisson.address:redis://127.0.0.1:6379}) private String address; Value(${redisson.password:}) private String password; Value(${redisson.database:0}) private int database; Value(${redisson.connection-pool-size:16}) private int connectionPoolSize; Value(${redisson.connection-minimum-idle-size:4}) private int connectionMinimumIdleSize; Bean(destroyMethod shutdown) public RedissonClient redissonClient() { Config config new Config(); config.useSingleServer() .setAddress(address) .setPassword(password) .setDatabase(database) .setConnectionPoolSize(connectionPoolSize) .setConnectionMinimumIdleSize(connectionMinimumIdleSize) .setTimeout(3000) .setRetryAttempts(3) .setRetryInterval(1500); return Redisson.create(config); } }Bean(destroyMethod shutdown)这行很关键。它告诉 Spring 容器当应用关闭时调用RedissonClient的shutdown()方法优雅释放连接。如果不写这句Spring 默认可能调用其他关闭方法或者干脆不处理很容易在应用重启时留下大量 TIME_WAIT 连接。3.3 外部化配置从 yml 里读取参数的常见做法上面代码里用Value读取单个配置项对于参数不多的情况足够用了。但如果你有十几项配置每个都写一个Value会很难维护。更好的做法是定义一个配置属性类ConfigurationProperties(prefix redisson) Data public class RedissonProperties { private String address redis://127.0.0.1:6379; private String password ; private int database 0; private int connectionPoolSize 16; private int connectionMinimumIdleSize 4; private int timeout 3000; private int retryAttempts 3; private int retryInterval 1500; }然后在配置类里注入这个属性类并创建 BeanConfiguration EnableConfigurationProperties(RedissonProperties.class) public class RedissonConfig { Bean(destroyMethod shutdown) public RedissonClient redissonClient(RedissonProperties properties) { Config config new Config(); config.useSingleServer() .setAddress(properties.getAddress()) .setPassword(properties.getPassword()) .setDatabase(properties.getDatabase()) .setConnectionPoolSize(properties.getConnectionPoolSize()) .setConnectionMinimumIdleSize(properties.getConnectionMinimumIdleSize()) .setTimeout(properties.getTimeout()) .setRetryAttempts(properties.getRetryAttempts()) .setRetryInterval(properties.getRetryInterval()); return Redisson.create(config); } }对应的application.yml看起来是这样redisson: address: redis://127.0.0.1:6379 password: database: 0 connection-pool-size: 16 connection-minimum-idle-size: 4 timeout: 3000 retry-attempts: 3 retry-interval: 1500使用ConfigurationProperties的好处是 IDE 会有属性提示配置写错了也能在启动阶段直接暴露出来而不是运行到一半才报错。3.4 为什么“手动”不等于“麻烦”可能有人会觉得Starter 一行依赖加一叠配置就完事了为何还要费劲手写配置类我实际体会下来“手动”换来的是三个实实在在的好处。第一灵活性。手写Config可以让你随时切换 Redis 部署模式比如从单机变成哨兵或者集群只需要在配置类里换成useSentinelServers()或useClusterServers()而不是去调整一堆不可见的自动配置行为。第二可控性。测试环境下我可以直接 mockRedissonClient或者用一个单独的配置类指向本地 Redis环境隔离变得很清楚。Starter 方式下自动配置会在项目各处生效想要在测试环境“特殊处理”反而要额外排除自动配置麻烦不少。第三可读性。所有与 Redisson 相关的配置集中在一个类里代码评审时一眼就能看出参数是否合理。Starter 那种“配置藏在框架里”的爽快感到后期维护时会变成隐蔽的复杂性。4. 两种方式对比放在真实项目里怎么选4.1 多维度对比表这里我整理了一张对比表按我自己实际项目中关注的点来列的对比维度Starter 自动配置手动配置 Bean上手成本低依赖加配置即可中需要写配置类和属性类配置灵活性一般受自动配置约束高代码里可以用条件分支动态创建部署模式切换需要改配置但模式通过 YAML 描述直接在代码中切换useXxxServers()对已有 Spring Data Redis 的影响可能产生双连接池完全独立不影响原有 RedisTemplate测试隔离较难需要排除自动配置容易可按环境注入不同 Bean运维排查配置魔改空间小容易定位排查时需要看代码逻辑定位成本略高适合场景普通单体应用、快速落地复杂集群、多环境、自定义强需求需要说明的是这里的“适合场景”不是绝对的。我曾经在微服务项目里用过 Starter也在小工具项目里手写过 Bean最后发现两者都能跑真正的分歧点在于团队对 Redisson 配置的掌控需求有多高。4.2 同时用会不会打架Bean 冲突与连接“双轨制”这是一个容易踩坑的重灾区。如果你的项目已经引入了spring-boot-starter-data-redis此时再引入redisson-spring-boot-starterSpring 容器里会出现两套连接体系一套是RedisConnectionFactory供RedisTemplate使用另一套是RedissonClient供分布式组件使用。表面上看二者可以共存因为 Bean 名字不同、类型不同Spring 不会因为类型冲突直接报错。但实际上存在两个隐患一是两套连接都各自维护连接池线程数和文件描述符翻倍二是配置分散在两处spring.redis.host管着 RedisTemplatespring.redis.redisson.config管着 RedissonClient很容易出现“只改了一处”的配置不一致。我就在一次上线时遇到过运维只改了spring.redis.host结果 Redisson 还在连接旧地址缓存双写后出现数据不一致排查了很久。如果你决定使用手动配置 Bean 的方式就可以完全绕开这个坑Redisson 和 RedisTemplate 各用各的连接彼此互不干扰配置也能按照项目规范统一管理。4.3 我的选型建议给一个直接可用的判断标准项目里只需要简单的分布式锁、信号量不想写太多代码团队成员对 Redisson 的原理不求甚解直接选 Starter。项目同时用 RedisTemplate 做常规缓存又需要用 Redisson 做分布式协调我建议二选一要么统一用 Redisson 来缓存要么手动配置 Bean 以减少连接资源的叠加开销。项目是微服务架构配置要上配置中心集中管理选择手动配置 Bean 会更友好因为ConfigurationProperties读取配置中心的属性完全没有障碍而 Starter 的多行字符串配置在上配置中心时偶尔会有换行符、缩进的兼容问题。5. 实战分布式锁的完整落地方案5.1 一个能被两种方式共用锁的代码模板两种整合方式最终都生成RedissonClient所以锁的相关代码完全是一样的强烈建议把锁的逻辑封装成一个工具服务避免业务代码里到处写重复的 try-finallyComponent 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, SupplierT supplier) { RLock lock redissonClient.getLock(lockKey); boolean locked false; try { locked lock.tryLock(waitTime, leaseTime, unit); if (!locked) { throw new RuntimeException(获取锁失败请稍后重试); } return supplier.get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(获取锁被中断, e); } finally { if (locked lock.isHeldByCurrentThread()) { lock.unlock(); } } } }调用方式Autowired private DistributedLockService lockService; public void submitOrder(OrderRequest request) { lockService.executeWithLock(order:submit: request.getUserId(), 2, 10, TimeUnit.SECONDS, () - { // 真正的业务逻辑 orderMapper.insert(request); return Boolean.TRUE; }); }这里有几个细节值得注意。finally块里除了判断locked还要判断isHeldByCurrentThread()因为在极端情况下如果锁因为过期被 Redis 释放当前线程在unlock()之前其实已经失去了锁的所有权直接调用unlock()会抛出IllegalMonitorStateException。这个异常一般不致命但会造成日志告警而且会让排查人员误以为业务异常。判断当前线程是否还持有锁能从根源上避免这个问题。5.2 把 tryLock 的参数和 watchdog 逻辑彻底说清楚tryLock(waitTime, leaseTime, unit)的三个参数意义waitTime尝试获取锁的最大等待时间。当锁被其他线程持有时当前线程会在等待时间内不断试探。超过这个时间还没获取到锁方法返回false。leaseTime锁的自动释放时间。到达这个时间后Redis 会自动删除锁键。如果leaseTime不设置Redisson 会启动看门狗机制默认配置的锁超时时间是 30 秒看门狗每隔 10 秒会自动把锁续期到新的 30 秒只要当前线程还活着且持有锁锁就不会提前过期。这个机制非常实用但也很容易踩坑。如果你给leaseTime设置了一个明确的值比如 10 秒那么 10 秒后锁会被强制释放看门狗不再介入。如果业务逻辑执行时间超过了 10 秒第二个线程就可能拿到锁并进入临界区。等第一个线程业务结束执行unlock()时它可能已经不再持有这把锁了于是IllegalMonitorStateException就出现了。我的经验是如果对业务执行时间没有非常明确的预期优先不传leaseTime把续期交给看门狗如果必须设置leaseTime宁可多给一点余量也不要抠得太紧。否则锁带来的安全性会因为一个“看似合理的超时参数”全部瓦解。5.3 小扩展限流器 RRateLimiter 的用法Redisson 除了锁还有非常好用的限流器。我之前在做秒杀接口时用RRateLimiter做了单用户维度的限流Autowired private RedissonClient redissonClient; public boolean canSubmit(String userId) { RRateLimiter rateLimiter redissonClient.getRateLimiter(rate:submit: userId); rateLimiter.trySetRate(RateType.PER_CLIENT, 1, 1, RateIntervalUnit.SECONDS); return rateLimiter.tryAcquire(); }这段代码的含义是对同一个用户 ID每秒钟最多放行 1 次请求。trySetRate只有在限流器不存在时才会设置速率参数重复调用不会重置已有速率。这里的RateType.PER_CLIENT表示以客户端维度计数另一种RateType.OVERALL则是全局计数适合接口级整体限流使用。实际场景里“一个用户 1 秒只能提交一次”和“全站 1 秒最多放行 1000 个请求”是两个完全不同的诉求选错类型可能导致限流效果完全失效。6. 按照踩坑顺序列出的配置细节清单6.1 地址前缀就是“百问不厌”的问题Redisson 的address参数要求必须带协议前缀。单机 Redis 写redis://127.0.0.1:6379如果启用了 TLS写rediss://127.0.0.1:6379。我第一次配置时随手写了127.0.0.1:6379启动后报的是连接超时看错误日志很难想到是前缀缺失。这个错误其实很容易自查但每到新项目总会有人再犯一次所以建议在配置类里对地址做一层校验格式不对就直接抛异常。6.2 连接池大小与 Redis maxclients 的矛盾Redisson 默认的connectionMinimumIdleSize是 32也就是说应用启动后Redisson 会预先创建一批连接到 Redis。如果你的 Redis 服务端maxclients设置得比较小比如只有 64那么应用多了几个实例之后连接数极容易被占满。这时候新请求进不来Redis 日志里满是Cannot assign requested address或者连接拒绝。我在一个小型测试环境里遇到过这个问题Redis 是单机部署在低配虚拟机上Redisson 的默认连接参数直接把maxclients撑爆了。后来我把参数下调到connectionPoolSize8、connectionMinimumIdleSize2问题立刻缓解。对于中等规模的项目连接池不一定越大越好合理评估业务并发量更重要。6.3 序列化与反序列化Redisson 默认使用 Kryo 序列化对 Java 对象来说性能不错但有一个隐藏问题如果对象的类结构发生改变比如删除了某个字段、改了类名Redis 里已存在的旧数据可能反序列化失败。实际项目中我们遇到过发布新版本后某个缓存数据读取时报ClassNotFoundException原因就是旧数据是用旧类名序列化的。应对策略有两个方向一是给 Redisson 配置 JSON 序列化方式把存储格式调整为易于兼容的 JSON二是对于长期存在的缓存 key升级版本时考虑先清缓存或使用版本号后缀。我最终在项目里选择了 JSON 序列化虽然存储空间大一点但换来的是前后端、新旧代码都能读懂缓存的响应排障时也能直接get看到内容这是很值得的取舍。配置方式是在Config里设置config.setCodec(new JsonJacksonCodec());注意如果项目里同时有 Spring 的 Jackson 配置要注意版本兼容否则会出现InvalidDefinitionException这类错误。比较省事的方案是让JsonJacksonCodec使用与项目一致的 ObjectMapper避免配置分叉。6.4 检查 Starter 是否真的加载了配置用了 Starter 但配置不生效时不要急着怀疑代码。可以在application.yml里临时打开 Spring Boot 的自动配置报告debug: true启动日志里会出现Positive matches和Negative matches从中搜索RedissonAutoConfiguration就能看到配置条件是否被满足。比如你写了spring.redis.redisson.file但日志显示这个配置没有被识别那大概率是前缀写错或者配置类没有扫描到。这一招在排查“为什么 Redisson 连接了错误的地址”时特别有效。还有一种情况是 Spring Boot 多 Profile 导致配置覆盖混乱。比如application-dev.yml和application-prod.yml里的 Redisson 配置结构不一致启动时一个属性取到了另一个 profile 的残留值。建议 Redisson 相关配置尽量保持单一来源不要在多个 Profile 文件中复制粘贴大段配置而是通过配置中心或者环境变量来差异化维护从源头避免覆盖问题。6.5 关闭钩子防止进程退出时连接泄漏使用 Starter 时框架会处理关闭逻辑但手动配置 Bean 时很容易忘记设置destroyMethod。我见过一个后台任务应用每次发版之后 Redis 的info clients都显示大量残留连接直到 Redis 服务被拖垮。后来排查发现就是RedissonClientBean 没有配置关闭方法应用停机时连接没有被释放。所以手动配置时Bean(destroyMethod shutdown)这一行绝对不能省。最后聊一点我的使用体会。两种整合方式没有绝对的优劣之分我在不同项目里分别用过最后留下来的偏好是除非项目只是临时把 Redisson 当作一个增强版客户端来用否则我更倾向于手动配置 Bean。原因很简单分布式系统里最怕的就是“看起来配置好了但你不知道它背后怎么运作的”。自动配置帮我们省掉的每一行代码都会在将来的某个调试深夜以更隐蔽的形式还回来。如果你和我一样属于那种“一定要把连接池、超时时间、序列化方式都握在自己手里”的人手写一个配置类其实花不了多少时间却能带来长期的可控性和安全感。而如果你的团队刚接触 Redisson希望快速看到效果从 Starter 开始也完全没问题——这两种方式生成的RedissonClient是一样的业务代码完全复用将来想切换付出的成本也很低。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑