redisson
核心架构分层
1. 业务层(RLOCK,RMap,RQueue,RateLimiter。。。)
↓
2. 命令封装层(RedisCommandsExecutor,统一分发命令)
↓
3. Netty网络层(连接池,channel,编解码,重连)
↓
4. Redis服务端(单机/哨兵/集群)
分布式锁RLock实现原理
# 1.锁底层命令:Lua脚本,(原子保证)
Redisson加锁,续锁,释放锁,全部使用Lua脚本,能够保证多条Redis命令原子执行。
(Hash数据结构 + Lua脚本 + WatchDog看门狗线程 + PubSub发布订阅阻塞)
# 2.数据结构:Hash
key:锁名称 lock:stock
field:UUID:线程ID(区分不同客户端 + 线程)
value:重入次数
优势:支持锁可重入,同一个线程多次 lock 不会死锁
lua脚本加锁
-- KEYS[1]:锁key;
-- ARGV[1]:过期时间;
-- ARGV[2]:当前线程唯一标识(uuid:threadId)
-- 判断key不存在,直接写入锁,设置过期时间
if redis.call('exists', KEYS[1]) == 0 then
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 锁存在且是当前持有线程,重入计数+1,重置过期时间
if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 被其他线程占用,返回锁剩余过期时间
return redis.call('pttl', KEYS[1]);
lua脚本解锁
-- 只能释放自己持有的锁,不会删除其他线程的锁;
-- 解锁后发布 Pub/Sub 消息,实现公平锁、可阻塞锁唤醒机制。
-- KEYS[1] 锁key
-- ARGV[1] 客户端线程标识
-- 判断锁持有者是否自己,不是直接返回
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then
return nil;
end;
-- 重入计数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
if counter > 0 then
-- 还有重入次数,只重置过期,不删锁
redis.call('pexpire', KEYS[1], ARGV[2]);
return nil;
end;
-- 计数为0,删除锁
redis.call('del', KEYS[1]);
-- 发布解锁消息,唤醒阻塞等锁的线程
-- KEYS[2] publish 的频道名称(channel)
-- ARGV[1] 推送的消息内容
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
Watch Dog自动续期
# 解决问题:
解决 "业务逻辑未执行完成,锁已过期释放"的经典问题。
# 触发条件:
调用无过期时间的lock()方法时,会自动启动看门狗;如果手动指定了锁持有时间,则不会启动看门狗,到期自动释放。
# 默认过期时间:
看门狗默认的锁过期时间是30秒(可通过lockWatchdogTimeout 全局配置)
# 续期逻辑:
每隔1/3过期时间(30/3=10s),Redisson会在后台执行依次续期命令,重新将锁的过期时间设置为30s
# 终止条件:
线程主动释放锁,客户端宕机(无法续期,锁到期自动释放)
Redisson核心执行流程
1.业务调用rLock.lock()
↓
2.Redisson生成当前客户端唯一UUID + 当前线程ID
↓
3.构造加锁Lua脚本,通过Netty发送到Redis
↓
4.Redis执行Lua,返回nil(加锁成功)ttl(被占用)
↓
5.加锁成功:启动看门狗定时续期任务
↓
6.加锁失败:订阅锁频道,阻塞等待解锁消息
↓
7.收到解锁Pub/Sub通知,重新执行抢锁逻辑
↓
8.业务执行完调用unlock,执行释放锁Lua脚本,删除定时续期任务
哈希槽 + 哈希标签
# Hash Slot哈希槽
哈希槽是Redis集群用来分配数据分片的虚拟分区(是数据分片的最小单位),是Redis集群实现分布式存储的核心机制。
Redis集群固定划分16384个哈希槽(0~16383),所有键都会映射到这16384个槽里,再把不同槽分配给不同主节点,实现数据拆分。
# 取键名做 CRC16 校验,得到一个 16 位数值, 数值对 16384 取模,结果就是这个键所属的哈希槽编号
公式:
slot = CRC16(key) % 16384
举个例子key = user:1001
CRC16 计算后取模,算出结果是 568,那这个键就固定存在 568 号哈希槽 对应的主节点。
# 哈希槽的作用
分片存储,扩容均衡
集群多个主节点,管理员手动把 16384 个槽拆分分配给各节点。
比如 3 节点集群:
节点 A:0~5460
节点 B:5461~10922
节点 C:10923~16383
新增节点时,只需要迁移一部分哈希槽,不用全量迁移数据,扩容平滑。
数据迁移最小粒度是 "槽"
迁移时以整个哈希槽为单位批量转移一批 key,不是一条一条迁移,效率高。
路由定位 key
客户端随便连任意节点,节点计算 key 对应的槽,如果槽不归自己管理,就返回 MOVED 重定向,客户端再去对应节点操作。
# 哈希标签Hash Tag
= 控制 key 到同一个槽
业务场景:希望一批关联 key 落在同一个槽,方便批量操作 / 事务。
规则:key 中 {} 包裹的部分参与 CRC16 计算,只有大括号内内容参与哈希。
例:
order{100}:info
order{100}:goods
两者大括号里都是100,CRC16 结果一致,分到同一个哈希槽,可以在同一节点执行事务(如msetnx批量操作)。
Redis Cluster 哈希槽位计算 + Redisson 集群路由原理
# 一、基础前置:Redis Cluster 总槽位规则
Redis Cluster 一共固定 16384 个槽位(0 ~ 16383),所有数据全部存放在这 16384 个 slot 中。
集群中每个主节点负责一部分 连续/离散槽位,比如:
节点 A:0--5460
节点 B:5461--10922
节点 C:10923--16383
写入 / 读取 key 时,必须先算出 key 属于哪个 slot,再路由到持有该 slot 的主节点,不能随便发给任意节点,否则会返回 *MOVED重定向错误*。
# 二、key 计算 slot 的官方算法
1.提取 key 的有效部分(支持哈希标签 {})
2.CRC16 哈希取模 16384
slot = CRC16(key) % 16384
## 哈希标签 HashTag(解决跨槽批量操作)
如果 key 包含 {xxx},只对大括号内部字符串计算 CRC16,忽略外部字符:
user:{100}:info → 只计算 100
order:{200}:pay → 只计算 200
作用:把一批关联数据强制落到同一个 slot,支持 mget、mset、 pipeline、事务等批量操作,否则跨槽不支持批量原子操作。
示例:
{shop}:goods1、{shop}:goods2 计算出同一个 slot,可批量操作
# 三、Redis 原生流程(客户端直连集群)
1 客户端发送命令到任意集群节点
2 节点算出 key 对应 slot,判断当前节点是否负责该 slot
3 是:直接执行命令返回结果
4 否:返回 MOVED slot ip:port,告诉客户端真正节点地址
5 客户端收到 MOVED,重新连接目标节点重试命令
(缺点:频繁重定向,网络损耗大)
# 四、Redisson 优化路由机制(重点)
Redisson 不会每次都等服务端返回 MOVED,本地缓存槽位节点映射表,直接本地算出节点,一步到位路由
## 步骤1:初始化拉取槽位分配表
Redisson启动时,随机连接任意集群点,执行:CLUSTER SLOTS
Redisson返回完整信息:每隔slot区间对应主节点IP+端口,从节点。
Redisson在本地内存构建一张映射表:slot → RedisNode(连接、地址、channel)
## 步骤2:本地计算key对应slot
客户端执行get/set的时候:
Redisson内部执行CRC16(key)%16384,算出slot
查询本地缓存映射表,直接拿到该slot对应的主节点连接
通过Netty连接池,把命令直接发给对应主节点执行(全称不需要redis服务端返回MOVED,省去依次往返网络开销)
## 步骤3:槽位变更自动刷新映射表
两种场景触发本地缓存更新:
1.收到 MOVED 响应 = 槽迁移完成,永久归属新节点
节点发生槽位迁移(扩容、分片调整),本地缓存失效,Redis 返回 MOVED。
Redisson 立刻重新执行 CLUSTER SLOTS,刷新本地 slot-node 映射,再重试命令。
2.收到 ASK 响应(迁移中临时状态) =
槽位正在从节点 A 迁移到节点 B,数据两边都不完整,Redis 返回 ASK。
Redisson 先发送 ASKING 命令到目标新节点,再执行业务命令,不更新本地缓存,迁移完成后才会出MOVED
## 步骤 4:读写分离路由
Redisson 支持读从节点:
写操作:强制路由 slot 对应的主节点
读操作:可配置路由策略
读请求提供多种策略可配置:
MASTER:只读主
SLAVE:只读从
MASTER_SLAVE:主从均衡
SLAVE_RANDOM:随机选一台从节点,分摊主节点压力
Redisson 内部会缓存每个主的从节点列表,读请求自动分发。
# 五、特殊场景:多 key 跨 slot 怎么处理?
普通多条 key(无 HashTag,分到不同 slot)
原生 Redis 不支持 mset/mget,直接报错。
## Redisson 解决方案
方案 1:用 HashTag 强制所有 key 落到同一个 slot,正常批量操作
如:user{test1000}:key001 user{test1000}:key002
方案 2:RedissonMultiLock / 分批并发请求,分别路由各自节点聚合结果
# 六、故障自动转移原理(主从切换)
哨兵 / 集群内部完成主从切换后,旧主下线、从晋升新主;
Redisson 发送命令访问旧主会触发连接失败 / MOVED;
自动重新拉取 CLUSTER SLOTS,更新 slot 与主节点映射;
自动关闭失效旧连接池,为新主创建连接池,业务代码无需改动
# 一句话总结整条链路
业务代码传入 key
→ Redisson 本地 CRC16 算出 slot
→ 查询内存缓存的「slot - 节点映射表」
→ 获取对应节点 Netty 连接
→ 直接发送命令到持有该槽位的Redis主节点执行 (槽位迁移时自动刷新本地映射表)
→ 返回结果序列化后交给业务(全程无 MOVED重定向 跳转,一次网络往返)
Redisson 优势对比原生 Jedis/Lettuce
1.Redisson拥有开箱即用分布式组件(原生客户端需要手写分布式锁,极易出现死锁、不可重入、误删锁问题)
2.Redisson内置看门狗、RLock可重入锁、Pub/Sub 阻塞等待。
3.Redisson对redis集群路由封装完善
4.Redisson使用Netty作为网络层(异步高性能,连接池、自动重连)
Redis集群3主3从架构
# Redis Cluster 3 主 3 从架构流程图
客户端(Redisson/JedisCluster)
↓ 自动缓存16384槽映射,自动转发请求
┌─────────────┬─────────────┬─────────────┐
│ Master-A │ Master-B │ Master-C │ 3台主节点(分配哈希槽)
│ 槽0~5460 │ 槽5461~10922│ 槽10923~16383│
│ ↓同步 │ ↓同步 │ ↓同步
│ Replica-A1 │ Replica-B1 │ Replica-C1 │ 对应从节点(备份、故障顶替)
└─────────────┴─────────────┴─────────────┘
↓集群总线(16379端口)
节点互相心跳检测、同步元数据(节点ID,IP,端口主从对应关系,自己持有哪些哈希槽,槽迁移进度)
# 数据分片存储流程(哈希槽分配)
1. 客户端操作 `key = user:1001`
2. 集群计算:`CRC16("user:1001") % 16384` → 得到槽位编号
3. 根据槽位映射路由到对应 Master 节点读写
4. Master 同步数据到自身 Replica 从节点做备份