Redis中的全局hash表
redis是非关系型的key-value数据库,存储数据时需要一个唯一的key。redis中所有的key存储在全局hash表中。当key出现hash冲突时会生成链表。当链表过长时会触发rehash扩容,因为redis是单线程数据量大的时候直接全部扩容会导致redis服务阻塞,所以采用了渐进式rehash,在内部维护两张hash表,需要扩容时初始化扩容表长度为原来的2两倍,每次执行命令时顺便完成迁移。
Redis实现消息队列
1、普通List。生产者将消息放入List,消费者读取List中数据,可以循环读取也可以阻塞等待,一个消息只能被一个消费者处理,消费失败没有重试机制。
java
# 生产消息
LPUSH mq:order "msg1"
# 不阻塞消费消息,有消息就返回,没有则为空
RPOP mq:order
# 队列无消息阻塞10s,有消息立刻返回
BRPOP mq:order 10
2、发布订阅。一对多广播,一个队列可以添加多个消费者,消息不持久化,消费者离线则无法收到消息,不支持消息回溯。
java
# 发布
PUBLISH channel:order "order-1001"
# 订阅
SUBSCRIBE channel:order
3、延时消息。利用 score 存时间戳,实现延迟消息。需要业务轮询;高并发下存在竞态,需要 Lua 保证原子查询 + 删除。
java
# core = 当前时间戳 + 延迟毫秒
ZADD mq:delay 1788888888000 '{"id":1,"payload":"xxx"}'
# 消费者 消费端轮询:拿已经到期的消息
ZRANGEBYSCORE mq:delay 0 当前时间戳 LIMIT 0 1
# ZREM 删除消息(注意并发问题,建议 ZPOPMIN)
ZPOPMIN mq:delay 1
4、Stream。Stream 是专门设计的消息队列结构,支持:消息持久化,消息 ID 自增、ACK 确认机制、消费组 (Consumer Group),多消费者负载均衡、消息回溯,读取历史消息。
java
# 生产消息
# * 自动生成消息ID
XADD mq:stream:order * id 1001 status paid
# > 读取组内未分配给消费者的新消息
XREADGROUP GROUP g1 consumer-1 COUNT 1 BLOCK 5000 STREAMS mq:stream:order >
# 业务处理完成调用 ACK确认
XACK mq:stream:order g1 1720000000000-0
缺点:Redis 是内存数据库,消息堆积过大内存打爆、Redis 主从异步复制,主节点宕机,可能丢失少量未同步消息、没有内置重试死信队列,死信需要业务自己处理 pending 超时消息。
Redis的持久化
redis支持将内存中的数据持久化到磁盘。一共有两种方式:RDB和AOF。生产建议开启混合模式aof-use-rdb-preamble yes,aof重写时新的文件前面用rdb生成的快照,后面追加增量aof命令。redis重启时先加载aof文件若是aof不存在再加载rdb。
1、RDB
rdb方式是将内存中的快照全部持久化到磁盘。有两种触发方式:自动触发和手动触发。自动触发通过save配置:
java
#查看配置
config get save
# 900秒至少1个key修改;300秒至少10个;60秒至少10000个
save 900 1
save 300 10
save 60 10000
# 若查询到的配置为 save "" 表示没有开启自动RDB
自动触发会调用系统的fork()方法,创建子进程完成持久化,主进程只在创建子进程时阻塞后续持久化期间主进程不阻塞。记录的是fork()那一瞬间的内存快照。采用了写时复制技术(Copy On Write),避免了将所有内存全部复制一份。主进程执行写命名时会将涉及到的数据页进行复制,然后操作复制的数据。也可以通过命令手动触发,save或bgsave。save是主进程去完成备份会造成redis服务阻塞,bgsave同自动触发的备份,通过fork()子进程完成。RDB备份的是内存快照,即使触发时间调小还是有丢失数据风险。
2、AOF
将写命令保存到文件,redis重启后通过重新执行文件中的命令恢复数据。
相关配置:
java
# 开启aof
appendonly yes
# aof文件名
appendfilename "appendonly.aof"
# 持久化触发时机
# 1. appendfsync always:每条写都fsync。几乎不丢数据,性能极差
appendfsync always
# 2. appendfsync everysec:每秒fsync一次(默认推荐)。最多丢1秒数据
appendfsync everysec
# 3. appendfsync no:交给操作系统刷盘。性能最好,丢数据最多
appendfsync no
# aof文件重写相关
auto-aof-rewrite-percentage 100 # aof文件比上次重写增长100%触发重写
auto-aof-rewrite-min-size 64mb # 至少64MB才触发
redis会先将命令写入内存中的aof缓冲区,触发持久化时再将命令写入到aof文件。aof理论上会一直变大,为了节约磁盘空间,当文件增大到一定程度时会触发aof文件重写(bgrewriteaof),redis会将记录的命令进行压缩,比如对一个string类型的key进行多次set,重写后的aof只会记录最后一次设置的值。
Redis实现分布式锁
reids可用于实现分布式锁。
1、简单分布式锁
通过redis的 SETNX + EXPIRE
java
SETNX lock:order 1
EXPIRE lock:order 30
风险:SETNX 拿到锁之后,还没执行 EXPIRE,进程宕机 → key永远不会过期,死锁。
加锁和设置超时时间有原子命令:SET key value NX EX
java
# key:锁key;value:唯一标识(UUID/线程ID);NX:不存在才设置;EX:过期秒数
SET lock:order ${unique_id} NX EX 30
``
注意:
- value需要设置为唯一标识,用于锁释放时判断是否为持有锁的线程,否则其他线程都能执行锁删除。
- 释放锁的get值和删除key 需使用Lua脚本保证原子性,否则存在锁误删除风险(A获得锁设置过期时间30s,30s后锁到期被redis删除,且A执行了get准备释放锁,同时B又重新获得了锁,这时A再释放锁实际是误释放了B持有的锁)。
Lua脚本实现get+del原子性:
```java
-- lua脚本:key[1]锁key;argv[1]客户端唯一id
if redis.call('GET',KEYS[1]) == ARGV[1] then
return redis.call('DEL',KEYS[1])
else
return 0
end
简单锁即使实现了 加锁和设置过期时间同时执行并通过设置唯一value和Lua脚本保证get+del原子性避免锁被误删除,还是存在锁到期后被异常释放问题。
2、Redisson 分布式锁
Redisson 是 Redis 官方推荐的 Java 分布式锁客户端,封装了锁获取、释放、看门狗自动续期、可重入、锁等待、异常处理
使用模版:
java
RLock lock = redissonClient.getLock("lock:order");
try {
/**
* waitTime:抢锁最大等待时间,10秒抢不到直接返回false
* leaseTime:锁持有超时时间,-1 → 使用看门狗机制,默认30s
*/
boolean acquire = lock.tryLock(10, -1, TimeUnit.SECONDS);
if (acquire) {
// =========临界区 业务逻辑=========
System.out.println("拿到锁执行业务");
Thread.sleep(20000);
} else {
System.out.println("抢锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// ⚠️必须判断:只能释放自己线程持有的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
看门狗 WatchDog
- 当
leaseTime = -1,看门狗才会启动。 - Redis 锁默认过期时间:30 秒。
- 后台定时任务,每 10 秒(30/3)执行 Lua 脚本续期,把锁过期重置回 30 秒。
- 业务正常 unlock → 停止看门狗定时任务。
- 如果 JVM 进程宕机,看门狗线程直接消失,不再续期;锁等待 30s 自动过期释放,避免死锁。
注意:
主从异步复制锁丢失风险依旧存在:master 成功加锁,锁 key 还没同步 slave,master 宕机;slave 晋升新 master,其他线程可以获取锁。可以使用红锁(redlock)解决该问题。
3、红锁RedLock
Redlock 是 Redis 作者 antirez 提出的多节点分布式锁算法 ,目的解决单节点 + 主从架构锁丢失问题 。
实现红锁需要多台独立的奇数个主节点,官方推荐至少5个。加锁时需要同时向全部主节点发送加锁请求,超过半数的节点在超时时间前返回加锁成功才算获得锁。成本高、时钟依赖、有理论缺陷生产一般不使用。
Redis的高可用
reids可以搭建主从集群,避免发生点单故障实现高可用。
1. 主从复制 Replication
- master 写,slave 复制数据;slave 只读。
- 作用:数据备份、读写分离。
- ❗缺陷:没有自动故障转移。master 宕机,整个写服务不可用,需要人工把 slave 提升 master,其他 slave 重新指向新 master。
生产不会单独只用主从。
2. Sentinel 哨兵模式
Sentinel 本身也是 Redis 实例,不存业务数据,专门做监控。一般部署奇数个哨兵,3 个 / 5 个。
哨兵三大核心能力
1、监控(Monitor)
哨兵持续心跳检测 master、slave 实例状态。
2、主观下线、客观下线
- 主观下线 SDOWN:一个哨兵判定某个节点 ping 不通,只是自己认为挂了。
- 客观下线 ODOWN:多个哨兵达成共识(quorum 法定票数),才判定 master 真的挂掉。
3、自动故障转移
master 客观下线之后:
1.哨兵集群选举出一个领头哨兵;
2.在 slave 列表挑选一个 slave 升级为新 master(挑选优先级:slave‑priority 优先级 > 复制偏移量 offset 越大(数据越新) > runId);
3.执行 slaveof no one,把选中 slave 变成 master;
4.通知其余 slave,重新 slaveof 指向新 master;
5.旧 master 恢复后,哨兵自动把它变成新 master 的 slave。
Redis‑Cluster 集群模式
集群模式需要配置多台主节点,根据所选策略将key分散到不同的主,分散数据提升性能。redis选用的分配方案为固定hash槽。
主节点选取规则
1、按节点数取模
得到key的hashcode后,直接与主节点数取模,得到节点位置。该方式实现简单,但扩容缩容会导致大量key重新迁移。若是每次扩容为2n次幂可减少key迁移但不够灵活。
2、一致性hash
构建一个0~232-1的hash环,将节点hash后放到环上,每个key做hash后在hash环上顺时针寻找第一个大于等于keyHash的节点。直接将真实节点方法环上有很大可能形成hash倾斜导致数据分配不均,可以为每个真实节点添加虚拟节点,例如真实节点为"ip:port",可以构建多个"ip:port#0、ip:port#1"虚拟节点,将虚拟节点添加到hash环减少数据倾斜可能。
3、hash槽
- 多个 master 节点,每个 master 负责一部分槽位(slot);一共
16384个哈希槽。 - key 通过
CRC16(key) % 16384计算槽位,落到对应 master。 - 每个 master 可以挂若干 slave;master 宕机,slave 自动升级。
Cluster 内置简易哨兵能力,不需要额外部署 Sentinel 进程。
为什么选取16384个槽?
16384是权衡网络开销和分片粒度的最终结果。
因为集群间每秒都会同步自身负责的槽位数据给其他节点,16483个槽位可以用214位图表示实现大小只需2kb,不会给网络传输造成太大负担。若是槽位太小,每台主节点有分分到更多数据,找出分片粒度过大。同时槽位数也决定了集群个数上限。
槽位
16384 个 hash 槽,分散分配给各个 master。
写入 key:CRC16 (key) 取模,算出 slot,访问对应 master。
‑‑cluster‑reshard:在线迁移槽位,实现数据扩容。
集群中主节点数发生改变,需要手动重新分配hash槽。
集群故障转移
某个 master 宕机:
- 集群内其他节点 Gossip 协议检测故障;
- 达到多数节点确认客观下线;
- 该 master 的 slave 提升为新 master 接管槽位;
- 如果 master 连同它所有 slave 全部宕机:这部分槽位完全不可用,集群整体挂掉。
Redis主从间的数据同步
Redis 主从:master 负责写,slave 复制 master 数据;读写分离,备份数据。首次同步redis主节点将生成RDB文件,将RDB文件异步同步给从节点,后续主节点和从节点将建立长连接,主节点将写命令同步给从节点,保持主从间数据一致。
同步模式
核心概念
- runId:每个 Redis 实例启动生成唯一 ID。
- 复制偏移量 offset:master 和 slave 各自维护一个 offset;master 每发送字节,master offset 增加;slave 接收字节,slave offset 增加。offset 代表复制流的位置。
- 复制积压缓冲区 repl‑backlog‑buffer :master 上一块环形缓冲区。master 把写命令除了发给 slave,还写入这个环形 buffer。用于增量同步。大小可配置。
1. 从节点初次连接主节点:全量同步
slave 第一次连接 master,没有历史复制上下文,执行全量同步 。
完整流程:
- slave 向 master 发送
PSYNC ? -1:? 代表不知道 master runId,‑1 强制全量同步。 - master 执行 bgsave,后台 fork 子进程生成 RDB 快照。
- master 把 RDB 文件发送给 slave。
⚠️bgsave 期间,master 继续接收客户端写命令;这部分新写命令,会同时写入repl‑backlog‑buffer 复制积压缓冲区。
- slave 收到 RDB,清空自己本地数据,加载 RDB。
- RDB 传输完毕之后,master 把 bgsave 期间产生、存放在 repl‑backlog‑buffer 里面的写命令,发送给 slave;slave 回放这些命令。
- 主从 offset 对齐,进入持续增量同步。
全量同步开销大:fork 子进程内存开销、RDB 网络传输;大实例尽量避免频繁全量同步。
2. 断线续传:增量同步
slave 和 master 网络短暂断开(网络抖动),短时间内重连。
- slave 重连 master,发送
PSYNC <runId> <slave_offset>,带上之前 master 的 runId、自己的复制偏移量。 - master 校验:
- runId 匹配;
- slave 的 offset,还在 master 的 repl‑backlog‑buffer 环形缓冲区范围内 。
✅满足:增量同步。master 直接把 buffer 中 offset 之后的命令流发给 slave;slave 回放命令,不需要 RDB 全量。
❌不满足:
- runId 不匹配(master 重启了 runId 变了)
- offset 已经被环形缓冲区覆盖掉(断线太久,buffer 已经循环覆盖旧数据)
→ 触发全量同步。
repl‑backlog‑buffer 是环形固定大小。如果 slave 断线时间过长,master 写命令太多,offset 对应数据被冲掉,就只能全量。
slave 初次连接 master 触发全量同步:master bgsave 生成 RDB 传给 slave,RDB 期间新命令写入复制积压缓冲区;slave 加载 RDB 之后回放缓冲区命令。正常运行 master 异步传播写命令。slave 短暂断线重连,如果 runId 一致且 offset 还在复制积压缓冲区,走增量同步;否则全量同步。复制默认异步,master 不等 slave,存在宕机数据丢失风险。