
1. 为什么Redis生态里最终选了RedissonRedis本身只是个键值存储真正让它在业务系统里发光发热的是各种客户端库把它们包装成了开发人员顺手的东西。这个系列前面十篇把Redis的数据类型、持久化、主从、哨兵、集群都过了一遍到了第十一篇该聊聊实际项目里最常用的那把瑞士军刀了——Redisson。先说说我自己的经历。早些年用Java操作Redis主力是Jedis后来Spring Boot默认的Lettuce也用了很久。它们本质上都是传输工具你发一条命令它帮你把命令发给Redis再把结果拿回来。这套模式够用但遇到分布式场景就开始难受了。比如你要实现一个分布式锁拿Jedis得自己写SETNX加Lua脚本还得处理过期时间、锁续期、可重入这些边界问题每一行代码都是坑。我的团队早期就有一套自己封装的锁工具维护了大半年修了不少bug最后还是决定换掉。换掉之后用的就是Redisson。它和Jedis、Lettuce最大的区别在于Redisson不只是一个命令发送器它把Redis的各种能力封装成了Java里非常自然的数据结构和工具——你拿到的是一把看着像ReentrantLock的锁一个用起来像ConcurrentHashMap的RMap一个像AtomicLong的计数器。底层那些Redis命令、Lua脚本、序列化细节全部被藏起来了。对于正在学Redis的读者我的建议是前几篇你要是已经把Redis基础操作搞熟了这一篇的Redisson就是你从会操作Redis到能在生产环境用好Redis的转折点。它不改变Redis本身但彻底改变了你使用Redis的方式。这篇文章我尽量把Redisson最核心的几个能力拆开讲重点放在分布式锁、数据结构封装、以及一堆只有在实战里才会发现的坑。2. 引入Redisson前的准备依赖、配置与序列化2.1 Maven依赖怎么选版本Redisson的Java客户端坐标很简单就是一个核心包dependency groupIdorg.redisson/groupId artifactIdredisson/artifactId version3.27.2/version /dependency版本选择上有个建议如果项目用的是Spring Boot优先找带-spring-boot-starter后缀的版本比如redisson-spring-boot-starter。这样Spring容器会自动帮你装配好RedissonClient配置文件里的spring.redis.*参数它也能识别一部分。不过我自己更习惯用原生包手动创建RedissonClient原因后面说。另外要注意版本和JDK的兼容性Redisson 3.2x系列要求JDK 8如果你用的是JDK 17甚至21反而更省心。曾经踩过一个坑项目JDK还是8Redisson升级到了3.2x最新版虽然能用但某些新特性会走兼容模式后来干脆锁了一个稳定版本不动。2.2 三种部署模式下的客户端配置Redisson支持单机、哨兵、集群三种部署模式每种模式一个Config类就能搞定。单机模式最简单开发环境最常见Config config new Config(); config.useSingleServer() .setAddress(redis://127.0.0.1:6379) .setPassword(yourpassword) .setDatabase(0) .setConnectionPoolSize(64) .setConnectionMinimumIdleSize(16); RedissonClient redisson Redisson.create(config);哨兵模式适合高可用场景Config config new Config(); config.useSentinelServers() .addSentinelAddress(redis://192.168.1.101:26379, redis://192.168.1.102:26379) .setMasterName(mymaster) .setPassword(yourpassword) .setMasterConnectionPoolSize(64) .setSlaveConnectionPoolSize(64); RedissonClient redisson Redisson.create(config);集群模式则是这样Config config new Config(); config.useClusterServers() .addNodeAddress(redis://192.168.1.201:7001, redis://192.168.1.202:7002) .setScanInterval(2000) .setMasterConnectionPoolSize(64) .setSlaveConnectionPoolSize(64); RedissonClient redisson Redisson.create(config);这里setScanInterval是集群拓扑扫描间隔单位毫秒。Redisson会定期去探测集群节点变化默认就是2000毫秒一般不用改。连接池参数我后面专门讲这里不展开。2.3 序列化器必须单独配置Redisson默认序列化是FST线程安全性能不错但它有个问题存进去的字符串键值对用redis-cli看是一堆二进制乱码。如果你是纯Java应用这无所谓但团队里如果有运维同事要用命令行排查数据或者有其他语言的服务需要读同一份Redis数据就尴尬了。我的做法是统一用JSON序列化尤其是Value那一层config.setCodec(new JsonJacksonCodec());这个改动会让数据可读性大幅提升。举例来说用默认FST存一个User对象redis-cli拿到的是类似\xAC\xED\x00\x05t...的内容换成JsonJacksonCodec之后存进去的就是{id:1,name:张三}一眼能看懂。唯一需要注意的地方如果Redis里同时存在老版本FST序列化的数据切换Codec之后会反序列化失败。生产环境切换前要么先让旧数据自然过期要么做一个兼容读取的过渡。读者如果在已有系统上引入Redisson这一步一定不能省。3. 分布式锁Redisson最核心的王牌3.1 自己写分布式锁为什么会翻车分布式锁是Redis最热门的应用场景之一。很多人面试的时候能背出SETNX 过期时间 Lua脚本这套方案真到生产环境落地就会发现远没那么简单。第一层坑只用了setIfAbsent没设过期时间结果业务代码崩溃锁永远不释放其他线程全部卡死。这个算是低级错误了。第二层坑加了过期时间但业务执行时间超过过期时间锁被自动释放另一个线程拿到了锁临界区里有两个线程同时运行。解决思路是续期——起一个后台线程不断给锁续期直到业务结束。第三层坑续期逻辑判断这个锁是不是自己的。如果只按key释放锁线程A的锁过期了线程B拿到锁线程A业务结束把B的锁删了——这就是经典的误删别人的锁。第四层坑可重入怎么办同一个线程嵌套调用需要能重入还有锁等待超时、锁获取后的看门狗续期、以及Redis主从切换时锁丢失的极端场景。自己写这些一两百行Lua脚本加调度线程起步还要反复压测边界。Redisson把这些问题全部封装好了这也是我用它的最大理由。3.2 RedissonLock的底层原理Redisson的分布式锁核心类和实现流程值得了解一下至少面试和排障时用得上。锁的key是你在代码里指定的名字value里存的是一个UUID加线程ID的标识用来区分这个锁是谁持有的。加锁的时候Redisson并不是简单发一条SETNX而是执行一段Lua脚本脚本内部做了三件事判断锁的key是否存在不存在就直接加锁并设置初始过期时间默认30秒同时记录持有者标识和重入次数如果key存在但持有者标识是当前线程就把重入次数加1否则返回锁剩余存活时间调用方据此决定是否进入等待。释放锁时也走Lua脚本先判断持有者标识防止上面说的误删别人的锁确认是本人之后递减重入次数减到0才真正删除key。这个Lua脚本是原子的Redis单线程执行模型保证不会出现并发判断错乱。整个锁机制对外看起来和Java的ReentrantLock几乎一样。3.3 看门狗续期机制默认30秒不会提前过期Redisson锁默认的过期时间是30秒但如果你拿锁之后没有指定leaseTime参数Redisson会启动一个看门狗后台任务。这个任务每隔10秒锁过期时间的三分之一检查一次锁是否还存在存在就重新设置为30秒。这样业务代码无论跑多久只要线程没挂锁就不会在业务执行中被自动释放。这里有个值得重视的细节看门狗只在未指定leaseTime时启用。如果你调用lock(10, TimeUnit.SECONDS)这种带leaseTime的重载方法Redisson会认为你明确了锁的持有上限直接按10秒过期不启动续期。很多人踩过锁提前释放的坑回头一看代码发现正是在调用时传了leaseTime。生产环境我的建议很直接除非有必须防止持有锁超过固定时长的强需求否则一律用不带leaseTime的lock()方法把续期交给看门狗。要设置等待时间用这个重载// 最多等5秒拿不到就放弃 boolean locked redissonClient.getLock(order:pay:1001).tryLock(5, TimeUnit.SECONDS);注意这个5秒是等待获取锁的超时不是锁的过期时间两者容易搞混。业务执行完之后手动unlock()或者配合finally块统一释放。3.4 生产环境正确的加锁姿势直接上一段我项目里的标准写法Autowired private RedissonClient redissonClient; public void deductStock(Long skuId, int count) { RLock lock redissonClient.getLock(stock:deduct: skuId); // 持有锁看门狗自动续期 lock.lock(); try { int stock getStockFromCache(skuId); if (stock count) { throw new BizException(库存不足); } // 业务操作扣库存、写订单等 updateStock(skuId, stock - count); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }几个关键点lock.lock()拿不到锁的时候会阻塞等待不像tryLock可以控制等待时间。高并发场景一般用tryLock更稳妥。isHeldByCurrentThread()判断当前线程是否还持有锁防止业务异常导致锁状态混乱时误调用unlock。unlock放在finally里但是要包一层判断。虽然Redisson内部有防误删机制但主动判断一次能避免一些异常路径下的重复释放。3.5 公平锁、读写锁、RedLock怎么选Redisson还提供了几种特殊锁我这里把每个的适用场景说清楚。公平锁getFairLock内部通过Redis的zset维护了一个等待队列保证先到先得。适合秒杀、抢兑这类讲究公平性的场景。代价是性能比默认的不可重入锁低因为每次排队都要操作有序集合。读写锁getReadWriteLock读锁之间可以共享写锁独占。适合读多写少、数据一致性要求不高的场景比如配置读取、商品详情缓存更新。实测在高并发读场景下读写锁吞吐量比全量互斥锁高一截。RedLock红锁是Redisson提供的一种多节点锁方案需要向N个独立的Redis主节点分别加锁超过半数成功才算拿到锁。这个方案理论上能解决Redis主从切换时锁丢失的问题但实际工程里争议很大。我在生产环境没用过RedLock原因很简单多数业务场景其实用不到跨数据中心的强一致锁而且RedLock本身也存在分布式系统里关于时钟漂移的争论。如果你的系统已经有ZooKeeper或etcd强一致锁优先考虑它们Redis锁更适合够用就好的场景。4. 除了锁Redisson这些数据结构才是真宝藏4.1 RBucket、RMap、RList、RSet面向对象的Redis操作很多人学Redis数据类型的时候是分开记的String干什么、Hash干什么、List干什么、Set干什么。用Redisson之后这些维度收敛成了Java对象的使用习惯。RBucket相当于一个泛型容器对应Redis的String类型存一个对象RBucketUser bucket redissonClient.getBucket(user:123); bucket.set(user); User cached bucket.get();RMap对应Hash类型用法和ConcurrentHashMap非常像RMapString, String map redissonClient.getMap(product:attrs); map.putIfAbsent(color, red); map.fastPut(weight, 1.5kg); map.addAndGet(stock, -1);注意fastPut这个API很有意思它跳过了同步回调直接异步写入性能比put高但拿不到返回值。适合只写不管结果的场景。RList、RSet、RScoredSortedSet也是类似的封装思路。用这些封装的巨大好处是你不在业务代码里拼接Redis命令字符串了编译器能帮你提前发现拼写错误对象的序列化也统一由Codec处理。4.2 RAtomicLong分布式计数器不用再自己写Lua了上一篇文章讲Redis事务的时候我演示过用WATCH和Lua实现计数器递增。Redisson直接把这个操作封装成了RAtomicLongRAtomicLong counter redissonClient.getAtomicLong(visit:count); long value counter.incrementAndGet();这个类还支持decrementAndGet、addAndGet、compareAndSet这些方法背后走的就是Redis的原子操作。我在项目里用它做过接口防刷的计数器、灰度开关的流量分配、以及生成跨节点的自增序号配合每日零点重置。4.3 RDelayedQueue不用MQ也能做延迟任务这个是我个人最喜欢的Redisson特性。业务里经常有下单后30分钟未支付自动关闭、确认收货后7天自动打款这种延迟任务需求。引入整套延迟消息中间件太重用Redis的Key过期监听又不可靠Redis 5.0之后好在有Keyspace通知但生产环境对过期事件丢失比较敏感。Redisson的RDelayedQueue把这个问题解决得很优雅。原理是这样的数据先放进一个普通BlockingQueue同时注册一个DelayedQueue与其关联。Redisson通过定时扫描和zset的score机制实现延迟控制到期后元素会转移到目标队列BlockingQueue里。RBlockingQueueString queue redissonClient.getBlockingQueue(delayed:order:close); RDelayedQueueString delayedQueue redissonClient.getDelayedQueue(queue); // 30分钟后把订单号放入目标队列 delayedQueue.offer(order_10086, 30, TimeUnit.MINUTES); // 消费端 while (true) { String orderId queue.take(); closeOrder(orderId); }这个方案配合项目里一个简单的后台消费线程就能跑。要注意的是Redisson在Redis侧存储这些延迟数据会有额外的key空间消耗量特别大时要注意清理策略。我线上跑过每天几万条延迟数据问题不大。4.4 本地缓存和分布式缓存的联合用药Redisson有个RLocalCachedMap是在RMap的基础上加了JVM本地缓存层。查询时优先走本地缓存本地没有才访问Redis减少了远程调用次数。它的失效机制支持本地缓存同步失效本地JVM检测到Redis端数据更新后会通过广播机制通知其他节点清理本地缓存。听起来很美好但在集群部署时要特别小心。本地缓存是存储在JVM内存里的如果Redis数据更新没来得及同步到某个节点那个节点就会读到旧数据。Redisson默认提供失效广播机制但网络抖动时可能出现短暂的数据不一致。我的经验是对一致性要求高的数据不要用RLocalCachedMap对最终一致即可的高频读数据比如商品标签、活动开关可以用它把QPS打上去。5. 实战里绕不开的坑和性能调优经验5.1 看门狗失效的几种场景前面说了看门狗但看门狗不是万能的。这里讲几个我实测遇到过的失效场景。第一个是线程阻塞。看门狗续期本质上靠的是锁所属线程的TASK调度如果业务代码里调用了Thread.sleep()或者等一个很慢的外部接口这个期间看门狗任务依赖的是Redisson的netty事件循环线程一般不受影响。但如果你用的是lock.lockInterruptibly()并且线程被中断锁的持有状态和看门狗之间的关系会变得微妙这种情况直接会导致锁提前释放。第二个是主从切换。锁的key写在主节点上master宕机后数据还没来得及同步到slave哨兵提升新master锁信息丢了。Redisson官方提供RedLock试图解决这个问题但它要求至少3个独立节点很多业务系统根本搭不起这个架构。真要严谨得在基础设施层面接受极端故障时锁可能丢失这个事实再用幂等设计兜底。第三个是长事务。如果业务方法里有数据库事务整个事务可能要几十秒甚至更久而看门狗默认30秒续期逻辑没问题但如果你显式配置了leaseTime就一定要保证leaseTime大于最大业务耗时。我见过一个同事把leaseTime设成5秒接口偶尔要跑8秒线上间歇性出现并发问题排查了整整一天才定位到。5.2 锁粒度别把整个方法都锁住这是使用分布式锁最容易走偏的地方。很多人图省事直接在方法入口加锁public Order createOrder(CreateOrderRequest req) { RLock lock redissonClient.getLock(order:create); lock.lock(); try { // 查询商品 // 校验库存 // 扣减库存 // 生成订单 // 发送消息 } finally { lock.unlock(); } }这个写法的后果是所有订单创建请求串行化了Redis再快也扛不住全局互斥。正确做法是只锁真正需要互斥的资源比如某个商品的库存扣减锁key带上商品ID让不同商品之间的并发请求互不影响RLock lock redissonClient.getLock(stock:sku: skuId);锁的粒度越小系统并发能力越高。真正的实践是先把需要原子操作的代码抽出来再决定锁一段代码还是锁一个数据。5.3 连接池参数怎么调Redisson默认的连接池参数其实比较保守高并发场景下容易成为瓶颈。我给出一个实测可用的配置参考参数默认值建议值说明connectionPoolSize64128~256最大连接数取决于Redis实例的最大连接配置connectionMinimumIdleSize2416~32最小空闲连接数预热所需timeout3000ms3000ms连接超时别设太长否则故障时接口被拖死retryAttempts32重试次数太多会放大Redis故障影响retryInterval1500ms1000ms重试间隔调连接池有个前提Redis服务端也需要相应调大maxclients配置。我曾经把客户端连接池改成256没改Redis配置结果Redis服务端连接被打满一堆客户端报max number of clients reached。另外线程模型方面Redisson默认使用Netty事件循环不需要额外配置线程数但要注意业务线程池不要和它混淆。5.4 Spring Cache整合以及序列化匹配问题不少项目用Spring Cache做缓存抽象Redisson提供了RedissonSpringCacheManager可以无缝接入Cacheable、CacheEvict这些注解。Bean public CacheManager cacheManager(RedissonClient redissonClient) { MapString, CacheConfig config new HashMap(); config.put(userCache, new CacheConfig(24*60*60*1000, 12*60*60*1000)); return new RedissonSpringCacheManager(redissonClient, config); }它的好处是走了Redisson的RMapCache实现支持每个缓存单独的TTL。但这里有个大坑Spring Cache的key默认是方法的参数组合值如果你没有自定义KeyGenerator各种类型拼接出来的字符串可能长得很丑而且容易碰撞。我建议缓存key统一用SpEL表达式指定比如Cacheable(valueuserCache, key#userId)。还有一个坑是序列化匹配。Redisson分别有key的序列化和value的序列化如果缓存对象里有自定义类且类结构变了比如加了字段反序列化可能抛异常。遇到这种情况尽量让缓存对象保持简单的DTO结构别把Entity直接扔进去。5.5 和本系列前面内容的衔接Redisson读写操作的本质最后说点贯穿性的理解。Redisson不管封装得多漂亮它落到Redis上的命令仍然是SET、GET、HSET、ZADD这些基础指令。所以本系列前面学的Redis数据结构、持久化策略、淘汰策略、主从同步在Redisson场景下依然全部生效。举个直观例子RMap底层就是HSET/HGETRScoredSortedSet底层就是ZADD/ZRANGEBYSCORERDelayedQueue底层结合了zset和list。你懂Redis本身排查Redisson问题时用redis-cli盯着这些key的数据变化基本能看出端倪。这也是为什么我一直强调Redisson是使用Redis的上层建筑不是替代品。先搞懂Redis本身再拥抱Redisson的便利性遇到问题才能两头通透。整个系列走到这一篇Redis的完整使用体系就算是闭环了。