
写这一篇之前我犹豫了一下。网上讲 Redis 的文章太多了面试题背得滚瓜烂熟但真正到项目里要用分布式锁了却发现拿原生 Redis 命令手写一套锁处理锁过期、看门狗续期、重入、集群模式下主从切换丢锁……每一环都是坑。刚好这个系列写到第十一篇我决定把 Redisson 拿出来单独讲透。Redisson 是个好东西它不只是一个客户端更像是在 Redis 之上给你封装了一层分布式工具集号称 Java 分布式编程的工具箱。这篇内容适合那些已经会用 Redis 基本命令、但对分布式场景还有点发怵的 Java 后端开发也适合准备面试时被问到“Redis 分布式锁怎么实现”却只能背出 SETNX 的人——看完你会明白为什么 Redisson 是生产环境的首选。1. Redisson 到底是什么它的定位和设计初衷1.1 从原生 Redis 客户端到分布式基础设施的进化很多人第一次接触 Redis 的 Java 客户端用的要么是 Jedis 要么是 Lettuce。Jedis 简单粗暴API 直接对应 Redis 命令Lettuce 基于 Netty天生支持异步和响应式编程性能也确实强。但这两者本质上做的都是同一件事把 Redis 协议翻译成 Java 对象。你拿到的是一个个命令的返回结果碰到分布式场景里的复杂需求——分布式锁、分布式队列、布隆过滤器、限流器就得自己动手封装。Redisson 的思路不一样。它把 Redis 当作一个内存数据网格来看待对外提供的是 Java 集合框架的很多实现比如RMap、RSet、RList、RQueue以及一系列分布式服务RLock、RSemaphore、RCountDownLatch、RBloomFilter、RRateLimiter等等。你在单机应用里用惯了 ConcurrentHashMap、ReentrantLock、DelayQueue到了分布式环境Redisson 几乎给了你一套等价物。这就省去了从零造轮子的时间和踩坑风险。再往深一层讲Redisson 解决了分布式场景里一个很核心的问题状态的一致性。单机应用里 JVM 锁只能锁住当前进程多实例部署的时候A 实例加锁 B 实例根本不知道请求照样进来了。Redisson 把锁的状态放到了 Redis 服务端所有实例都去 Redis 上争抢同一把锁锁的语义就从“进程内互斥”升级成了“跨进程互斥”。1.2 Redisson 的核心能力和技术亮点我第一次用 Redisson 的时候最震撼的不是它的功能多而是它把很多分布式难题直接隐藏在了 API 背后。举个例子RLock加锁之后如果业务代码执行时间超过了锁的过期时间锁自动释放了别的线程就闯进来了。原生手写 SETNX 遇到这种情况几乎无解除非你起一个后台线程不停地给锁续期。Redisson 的处理方式完全不同它内置了一个 Watchdog看门狗机制默认情况下加锁后不会设置过期时间而是由后台调度任务每 10 秒检查一次只要锁还持有就自动给锁续期到 30 秒。这意味着你在业务代码里写得再久只要进程不崩锁就不会因为超时被提前释放。再比如 Redisson 的公平锁RLock的tryLock还有带排队机制的版本——它可以保证线程获取锁的顺序和请求顺序一致避免饥饿问题。这一点在很多高并发下单场景里特别重要因为非公平锁大概率会让个别线程一直抢不到锁最终导致请求超时、用户疯狂点按钮重试、系统压力成倍放大。Redisson 底层通过两个机制保证了分布式协调的可用性一个是发布订阅Pub/Sub锁释放时向等待的线程发送通知让它们立刻重新尝试获取而不是傻傻地轮询等待另一个是 Lua 脚本所有涉及锁状态变更的操作都会用 Lua 脚本在 Redis 服务端原子执行保证检查锁、加锁、设置过期时间这个复合过程不被其他并发请求打断。这种把并发控制下沉到服务端的做法是我觉得 Redisson 设计上最漂亮的地方。2. 环境准备和项目接入三步跑通第一个 Redisson 程序2.1 本地 Redis 与 Java 环境要跑通 Redisson第一步当然是把 Redis 准备好。我这里用的是 Redis 6.2 以上版本主要还是图它稳定而且 Redisson 对 Redis 3.0 到 7.x 全都做过兼容选新不选旧没什么好说的。Windows 用户可以下载官方提供的 Redis 压缩包解压后直接运行redis-server.exe注意官方原版只支持到某个老版本生产环境最好还是用 WSL 或者 Docker 跑 Linux 容器。我个人测试环境更习惯用 Dockerdocker run -d --name redis-dev \ -p 6379:6379 \ -e REDIS_PASSWORDredispass \ redis:6.2 redis-server --requirepass redispassJava 环境我用的是 JDK 8 和 Spring Boot 2.7.xRedisson 版本用 3.17.7 左右。为什么用这个版本因为它对 Redis 6.x 支持很完整同时兼容 JDK 8不需要额外适配。如果你用的是 JDK 17 和 Spring Boot 3那 Redisson 3.22 之后的版本才更稳妥——这里面的版本兼容问题挺容易踩坑的后面专门讲。2.2 Maven 依赖和基础配置在 pom.xml 里加 Redisson 的依赖其实非常简单dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.17.7/version /dependency这个 starter 会自动帮你把 RedissonClient 注册为 Spring Bean同时也会替代 Spring Data Redis 对 Lettuce/Jedis 的默认配置。使用的时候直接把RedissonClient注入到任意地方就行了Autowired private RedissonClient redissonClient;配置文件里连接信息是这么写的spring: redis: redisson: config: | singleServerConfig: address: redis://127.0.0.1:6379 password: redispass database: 0 connectionPoolSize: 64 connectionMinimumIdleSize: 24 timeout: 10000config里给的是 Redisson 原生的 YAML 配置。你也可以用代码方式构建 Config 对象灵活度更高适合在测试环境里动态切换连接地址。我个人偏好代码方式多一点尤其是做单元测试的时候直接 mock 一个 RedissonClient 比去连真实 Redis 方便一万倍。2.3 第一个操作用 Redisson 写一个分布式字符串连接建立好了先来一个最基础的例子感受一下。Redisson 提供了RBucket对应 Redis 的 String 类型String key user:login:count: userId; RBucketInteger bucket redissonClient.getBucket(key); bucket.set(1, 24, TimeUnit.HOURS); Integer count bucket.getAndSet(count 1);这段代码干了什么getBucket拿到一个分布式对象set方法设置值并给它 24 小时过期时间。getAndSet原子地把旧值拿出来再写入新值。这个操作如果你用原生命令其实就是GETSET。它解决的是什么问题统计用户 24 小时登录次数不用任何额外加锁因为底层的读写是原子的。可能有人会问这不就是opsForValue().set()吗跟 RedisTemplate 有什么区别最大的区别在于Redisson 的每个分布式对象都内置了监听器和序列化策略接口设计也更贴近 Java 集合的习惯后续你从字符串换到 Map、Set、List 的时候代码风格基本是统一的没有学习成本上的跳变。这一点在做大型系统的时候特别有价值——团队里不同水平的人协作统一的 API 风格可以明显降低沟通成本。3. 序列化机制深度解析为什么你的 key 总是带着乱码前缀3.1 Redisson 的 Codec 机制不管是什么 Redis 客户端序列化都是绕不过去的坎。Redis 本身只存字节数组Java 对象怎么转成字节、反过来怎么还原全靠序列化器说了算。Redisson 把这一层抽象成了Codec接口默认实现是Kryo5Codec也有旧版本用KryoCodec。用 Kryo 的好处是序列化体积小、速度快坏处是不跨语言——如果你的 Redis 同时被 Go、Python 或者其他 Java 框架读写序列化格式不兼容的话数据根本对不上。热词里有人专门搜redisson stringcodec说明这个痛点非常普遍。StringCodec就是把所有 key 和 value 都当字符串处理的编解码器不做对象序列化。你写入的时候传字符串出来也是字符串Redis 里看到的是什么就是什么不存在乱码前缀。我们做一个简单的对比看看不同 Codec 的差异Codec 类型存储格式适用场景是否跨语言StringCodec字符串key 和 value 都是明文字符串是JsonJacksonCodecJSON 字符串与前端或其他语言对接是Kryo5Codec二进制纯 Java 内部使用追求速度否ProtobufCodecProtocol Buffers高性能 跨语言是需定义 .proto3.2 设置 StringCodec 的正确姿势如果遇到 Redis 里 key 变成了一段很长的乱码比如\xAC\xED\x00\x05t\x00...那说明默认序列化器选了 Java 原生序列化或 Kryo 的二进制格式跟你预期的不一致。解决办法很简单配置里指定 CodecConfig config new Config(); config.setCodec(new StringCodec()); config.useSingleServer() .setAddress(redis://127.0.0.1:6379) .setPassword(redispass); RedissonClient client Redisson.create(config);用 Spring Boot starter 的话可以在application.yml里写spring: redis: redisson: config: | codec: !org.redisson.client.codec.StringCodec {} singleServerConfig: address: redis://127.0.0.1:6379有一点要特别提醒StringCodec并不适合存储所有类型。如果你用RMapString, Object存一个对象的多个字段对象会转成 JSON 字符串还是 Java 对象的二进制答案取决于具体 Codec 的实现。StringCodec 会把非字符串类型的值先转成字符串所以对于有嵌套结构的复杂对象最好还是用JsonJacksonCodec它能在可读性和对象还原能力之间取得平衡。3.3 跨团队协作时序列化冲突的问题我踩过一个大坑前端团队和 Java 后端同时写一个 Redis keyJava 这边用 Kryo 序列化数据根本读不出来。为什么Kryo 写入时带上类名和内部结构信息前端拿到的是二进制没法解析。后来我们把和业务方共享的 key 全部改成StringCodec或JsonJacksonCodec只有纯内部数据才保留 Kryo。这个经验后来成了团队规范只要 Redis key 存在跨系统访问的可能性一律使用 JSON 或纯字符串格式宁可牺牲一点反序列化性能也要保证可读性和兼容性。Codec 的选型本质上是在性能、可读性、兼容性这三个维度上做权衡。如果只是内部组件通信、吞吐量感人Kryo 绝对首选如果涉及对外接口或前端直连JSON 系是底线。不要在这个地方省事因为序列化格式的更换几乎是原子性的——线上数据已经用旧格式写了改完 Codec 之后老数据读不出来迁移成本非常麻烦。4. 分布式锁的实战拆解从一个抢购场景说起4.1 为什么不用 SETNX 手写分布式锁在引出 Redisson 的锁之前先看看原生方案的问题在哪里。最简单的分布式锁逻辑就是Boolean success stringRedisTemplate.opsForValue() .setIfAbsent(lockkey, client-id, 30, TimeUnit.SECONDS); if (success) { try { // 业务代码 } finally { stringRedisTemplate.delete(lockkey); } }这段代码有一个致命问题持有锁的线程 A 业务执行时间超过了 30 秒锁被 Redis 自动释放线程 B 拿到了锁开始干活。这时 A 代码终于跑完了执行 finally 里的delete直接把 B 的锁给删了。这就不是互斥了是互相伤害的结果。要解决这个问题你得在删除前比对一下锁里的值是不是自己的——也就是使用 Lua 脚本先检查再删除。但这只是第一层麻烦。更麻烦的是还有一个问题如果 A 在 25 秒的时候还没干完锁却只剩下 5 秒了眼看着就要被自动释放怎么办你得自己写续期调度每 5 秒执行一次续期。是不是听着就头疼这还只是一个需要考虑的边角问题。再往深处想如果 Redis 是主从架构主节点刚把锁写进去还没同步到从节点就宕机了从节点顶上变成主节点发现根本没有这把锁B 就可以加锁成功。原生命令解决不了这个问题Redisson 的 RedLock 机制才是为此而生的。这个本质问题在面试里高频出现但很多背了 SETNX 答案的人根本想不到还有这层。4.2 Redisson 分布式锁的用法和底层原理Redisson 的RLock完整解决了上面的续期、释放、重入、主从一致性问题。基本用法如下RLock lock redissonClient.getLock(order:pay:lock: orderId); boolean locked lock.tryLock(3, 30, TimeUnit.SECONDS); if (!locked) { // 返回客户端“排队中”提示 } try { // 执行关键业务 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }tryLock(waitTime, leaseTime, unit)这三个参数是什么意思waitTime是尝试获取锁的等待时间超过这个时间就放弃避免线程无限阻塞leaseTime是锁的最长持有时间到了时间自动释放防止死锁。注意如果你指定了leaseTimeRedisson 就不会启用 Watchdog 自动续期锁在到期之后一定释放所以这个值必须比你预计的业务最大耗时更长如果不传leaseTime默认启用看门狗续期锁每 10 秒自动续到 30 秒。推荐生产业务里显式给一个相对合理的leaseTime这样即使 Redis 出现极端情况锁也不会一直占着不放——宁可提前释放也不能死锁。关于 Watchdog 的实际机制我简单画一下流程线程 T 获取锁后Redisson 内部会新建一个定时任务每 10 秒执行一次检查锁是否还被 T 持有如果是就把过期时间续到 30 秒。T 宕机时定时任务也跟着销毁锁到了原定时间自然过期不会造成永久死锁。如果 T 正常运行了 35 秒那么它在 10 秒时续期一次、20 秒时续期一次、30 秒时又续期一次——锁始终被续到 30 秒之后。4.3 锁粒度与业务模型的匹配分布式锁的粒度设计非常考验经验。一个常见的反面教材是给整个用户维度加锁比如RLock lock redissonClient.getLock(user: userId :all);这样会导致同一个用户的所有下单请求全部串行排队即使他同时提交的不同订单根本互不干扰。我一般建议能细分就细分按orderId锁按skuId锁按accountId 业务类型锁。锁粒度越细并发度越高系统吞吐量越大。但粒度太细也有问题——比如涉及账户总余额变动所有订单都去锁同一个账户显然不合理这时就该考虑用 Redisson 的RSemaphore做信号量限流或者用数据库行锁兜底。没有银弹只有场景适配。5. Redisson 的分布式对象和服务不止锁这一件事5.1 RMap 和 RSet分布式版本的 Java 集合分布式锁只是 Redisson 的冰山一角它更适合被当做一个分布式数据结构库来使用。举个例子RMap对应 Redis 的 Hash但 API 设计非常像 Java 的ConcurrentHashMap支持putIfAbsent、computeIfAbsent等原子操作。这在处理缓存穿透场景时特别有价值——多线程同时读到空值用putIfAbsent只有一个线程能把占位值写进去其余线程直接返回默认值天然防击穿。RSet对应 Redis 的 Set内部直接用 Set 的命令实现可以用于做分布式去重。比如批量任务里要判断某个 ID 是否已经处理过把处理过的 ID 放进RSet后续任务用add方法的返回值得知是否重复。因为底层走 Redis 的SADD原子性有保障多个 Worker 同时跑也不会重复处理。5.2 RRateLimiter 和 RBloomFilter高并发系统的两件趁手兵器限流是每个高并发系统都绕不过去的事情。Redisson 的RRateLimiter基于令牌桶算法实现可以在多实例之间共享同一个限流器。比如某个接口想限制每秒钟最多 100 次请求RRateLimiter limiter redissonClient.getRateLimiter(api:limit); limiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.SECONDS); boolean allowed limiter.tryAcquire(1);RateType.OVERALL表示总速率也就是所有实例共享的 100 次/秒而不是每个实例 100 次/秒。这一点“分布式”的意义就完全体现出来了如果每个实例单独限流部署五个实例接口压力就放大五倍限流形同虚设。用 Redisson 统一走 Redis 计数语义才成立。RBloomFilter解决的是“某个值可能存在”这种判断典型场景是防缓存穿透的布隆过滤。它可以提前把全量用户 ID 放入过滤器查询时先判断是否存在不存在直接拦截避免打到数据库RBloomFilterString bloom redissonClient .getBloomFilter(user:bloom, new StringCodec()); bloom.tryInit(1000000L, 0.03); // 初始化时装载 bloom.add(String.valueOf(userId)); // 判断时查询 boolean mightExist bloom.contains(String.valueOf(userId));tryInit两个参数是预估元素量和误判率误判率设得越低内部位数组越大这个可以根据业务容忍度灵活调整。布隆过滤器最怕的是初始化后忘记装载数据——空过滤器和 null 没区别所有查询都会走下游。这个地方我踩过很深的坑后来统一封装了一个 InitRunner 在应用启动时强制预加载。6. 生产环境里的 Redisson 配置要点和常见问题6.1 连接池、超时和 Redisson 的性能调优连接池配置直接决定高并发下的稳定性YAML 里的几个参数怎么配我根据自己的经验整理一下参数推荐值含义connectionPoolSize64连接池最大连接数够大但别太大connectionMinimumIdleSize24空闲保持的最小连接数timeout10000命令等待响应超时单位毫秒retryAttempts3命令失败重试次数retryInterval1500每次重试的时间间隔单位毫秒pingConnectionInterval30000空闲连接健康检查间隔连接池大小不是越大越好如果 Tomcat 线程池最多 200 个线程连接池建个 500 条纯属浪费内存。理论上连接池大小应该和最大并发请求数匹配但要留一点缓冲。我这边单实例 QPS 在 8000 左右连接池 64 条完全够用——因为连接是复用的不是每请求一条连接。连接池配太大反而会增加 Redis 服务端的文件句柄压力得不偿失。6.2 集群模式和哨兵模式的配置差异生产级应用很少有人再用单机 Redis要么主从哨兵要么 Cluster。Redisson 对不同部署模式都有原生支持。哨兵模式sentinelServersConfig: sentinelAddresses: - redis://127.0.0.1:26379 - redis://127.0.0.1:26380 masterName: mymaster password: redispass集群模式clusterServersConfig: nodeAddresses: - redis://127.0.0.1:7001 - redis://127.0.0.1:7002 - redis://127.0.0.1:7003 scanInterval: 1000两种模式配置放在application.yml里的spring.redis.redisson.config下即可。需要特别注意的是集群模式下的分布式锁如果想做到严格一致不能只用普通的RLock得用 RedLock 的封装也就是同时向多个主节点申请锁超过半数成功才算加锁成功。但红锁也有自己的争议——比如在极端网络分区场景下可能造成死锁或安全降级很多团队会用 Zookeeper 或者 etcd 替代因为它们的共识协议天然保证一致性。这里不深入展开但你们选型时要清楚 Redis 分布式锁的强一致边界在哪里。6.3 常见异常排查实战我把自己遇到的频率最高的四类问题和排查思路整理成了表格照着做基本都能解决异常或现象可能原因排查方案启动报java.net.UnknownHostExceptionRedis 地址填写错误核对address前缀必须是redis://或rediss://端口正确调用lock()时卡住很久网络延迟 等待获取锁时间过长用tryLock并设置合理的waitTime锁一释放立刻被别的线程抢到业务处理时间超过leaseTime锁已提前过期调大leaseTime或者不传让它走 Watchdog 续期Redis key 出现乱码Codec 序列化兼容性问题切换StringCodec或JsonJacksonCodec检查跨语言共享的 key还有一个很隐蔽的问题容易出现在 Spring 事务里在Transactional方法中先给 Redis 加锁再去操作数据库锁释放和事务提交的时序要小心处理。如果在事务提交之前释放锁另一个线程就能拿到锁读到数据库旧数据出现“脏读”一样的业务问题。我的做法是尽量把锁放在事务外层或者在事务提交成功后再释放锁否则就扩展事务边界。6.4 Redisson 版本升级时的兼容性清单Redisson 的版本更新比较频繁每次升级除了看 Release Note下面这几个点必须重新验证Sentinel 模式下的版本号解析是否兼容你的 Redis 版本Codec 是否新增了默认实现老数据的反序列化是否不受影响分布式锁的底层 Lua 脚本有没有改动如果有本地要重新压测Spring Boot Starter 的自动配置类有没有变化配置项是否被废弃我曾经从 3.15 升到 3.17 之后发现RedissonClient在某些低配机器上启动变慢了排查半天发现是新版本加了健康检查连不上 Redis 会反复重试。最后在配置里新增了合理的retryAttempts和pingConnectionInterval才解决。升级本身不是坏事但在生产环境里稳定压倒一切没看懂 Release Note 之前不要轻易上版本。7. 最后一公里Redisson 落地时的几条个人经验用 Redisson 这么长时间如果让我给后来者只提几个建议我的选择是不要所有场景都用分布式锁。锁是最后手段不是第一手段。很多并发问题靠幂等键、乐观锁、消息队列的顺序消费就能解决引入分布式锁会带来网络开销、锁竞争、排查复杂度全面上升。能用PUT覆盖写解决的问题就不需要 CAS能用 Redis 原子自增来解决的计数器就不用锁。用 Redisson 的RAtomicLong做计数器、用RMap做缓存、用RQueue做延迟队列这些操作道门槛很低但能帮你避开很多手写原生命令时才会踩的并发大坑。比如分布式延迟队列Redisson 的RDelayedQueue底层帮你封装了 zset 的分数和时间轮询的机制你在业务里只要offer任务并指定延迟时间到点了消费者就能收到消息完全不用自己写轮询扫描。另外想说一下Redisson 的源码值得花时间读一读。它的 Lua 脚本写得非常精炼比如解锁脚本同时检查 owner 和过期时间这几个命令的原子性设计本身就是教科书。读一遍源码你对 Redis 的分布式语义、Watchdog 调度策略、甚至 Netty 连接的线程模型都会有更深层的理解面试时遇到“Redis 为什么快”这类问题你的答案也会比背八股文的朋友更扎实。