☰
Redisson实战指南:分布式锁与数据结构封装全解析
2026/10/4 6:12:03 网站建设 项目流程

1. 为什么Redis生态里最终选了Redisson

Redis本身只是个键值存储,真正让它在业务系统里发光发热的,是各种客户端库把它们包装成了开发人员顺手的东西。这个系列前面十篇把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> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.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还是8,Redisson升级到了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类型,存一个对象:

RBucket<User> bucket = redissonClient.getBucket("user:123"); bucket.set(user); User cached = bucket.get();

RMap对应Hash类型,用法和ConcurrentHashMap非常像:

RMap<String, 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直接把这个操作封装成了RAtomicLong:

RAtomicLong 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里。

RBlockingQueue<String> queue = redissonClient.getBlockingQueue("delayed:order:close"); RDelayedQueue<String> 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) { Map<String, 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(value="userCache", key="#userId")。

还有一个坑是序列化匹配。Redisson分别有key的序列化和value的序列化,如果缓存对象里有自定义类,且类结构变了(比如加了字段),反序列化可能抛异常。遇到这种情况,尽量让缓存对象保持简单的DTO结构,别把Entity直接扔进去。

5.5 和本系列前面内容的衔接:Redisson读写操作的本质

最后说点贯穿性的理解。Redisson不管封装得多漂亮,它落到Redis上的命令仍然是SET、GET、HSET、ZADD这些基础指令。所以本系列前面学的Redis数据结构、持久化策略、淘汰策略、主从同步,在Redisson场景下依然全部生效。举个直观例子:RMap底层就是HSET/HGET,RScoredSortedSet底层就是ZADD/ZRANGEBYSCORE,RDelayedQueue底层结合了zset和list。你懂Redis本身,排查Redisson问题时,用redis-cli盯着这些key的数据变化,基本能看出端倪。

这也是为什么我一直强调:Redisson是使用Redis的上层建筑,不是替代品。先搞懂Redis本身,再拥抱Redisson的便利性,遇到问题才能两头通透。整个系列走到这一篇,Redis的完整使用体系就算是闭环了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询