Redis
Redis:数据结构、持久化、缓存三大问题、分布式锁、高可用。
Redis
Redis 是单线程还是多线程呢?
为什么我们常说Redis是单线程的
「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是单线程。
4.0版本之前,Redis 的持久化、集群同步还是使用其他线程来完成。
4.0版本之后,多线程主要是体现在大数据的异步删除功能上,异步释放 Redis 内存unlink key、flushdb async、flushall async
Redis为什么快
内存存储 + 高效数据结构 + 单线程模型(无上下文开销、无锁竞争、cpu并非性能瓶颈) +非阻塞的 I/O多路复用(同时存在多个监听 Socket 和已连接 Socket)
IO多路复用:
- 非阻塞网络模型: Redis 采用了类似
epoll(Linux 环境下)的机制,允许单个线程同时监听多个客户端 Socket。 - 事件驱动: 当连接、读取或写入事件真正发生时,内核才会通知 Redis 去处理。这就像一个餐厅服务员(单线程)在大厅里看着所有桌子(多路复用),哪桌客人举手(事件就绪)就去哪桌服务,而不是傻傻地在一桌前死等。
redis数据结构有哪几种,它们分别对应的应用场景
- String OpsForValue
- 分布式锁
- 缓存层:存储序列化的对象
- List
- 消息队列
- Hash
- 也可以存储对象,update对象时可以不重新序列化
- Set
- 社交网络中的共同好友 利用
SINTER(交集)、SUNION(并集)
- 社交网络中的共同好友 利用
- ZSet
- 排行榜,点赞,Feed流
- BitMap
- HyperLogLog
- Geospatial
- Stream流
Redis 是如何实现数据不丢失的呢?
- AOF 日志(Append Only File,文件追加方式) 写后日志
- Redis 为什么要先执行命令,再把数据写入日志呢?
- 避免出现记录错误命令
- 写日志不会阻塞当前的写操作
- 后写日志的风险
- 数据可能会丢失
- 可能阻塞其他操作
- Redis 为什么要先执行命令,再把数据写入日志呢?
- RDB 快照(Redis DataBase):将某一个时刻的内存数据,以二进制的方式写入磁盘。核心是内存快照。
- RDB 做快照时会阻塞线程吗?
- save会
- bgsave不会
- Redis 是怎么解决在 bgsave 做快照的时候允许数据修改呢?
- 如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。
- RDB 做快照时会阻塞线程吗?
- 混合持久化方式:Redis 4.0 新增了混合持久化的方式,集成了 RDB 和 AOF 的优点。
缓存穿透、击穿、雪崩是什么?怎么解决?
- 缓存穿透: 当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
- 缓存空数据
- 布隆过滤器: 它可以明确告诉你一个元素“绝对不在集合里”,但只能告诉你一个元素“可能在集合里”。 位数组 + 多个哈希函数
- 缓存击穿: 如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
- 互斥锁,强一致,性能差: 只允许第一个拿到锁的线程去数据库查询并重建缓存,其他线程等待重试即可
- 逻辑过期,高可用,性能优,不能保证数据绝对一致
- 缓存雪崩: 当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
- 给不同的 Key 的 TTL 添加随机值
- 利用 Redis 集群提高服务的可用性
- 给缓存业务添加降级限流策略 降级可做为系统的保底策略,适用于穿透、击穿、雪崩
双写一致性问题
- Cache Aside Pattern 旁路缓存 - 最常见的方案
- 读请求: 先查缓存,命中则返回;未命中则查 DB,然后将结果写入缓存。
- 写请求: 先更新数据库,成功后再删除缓存。
- 延时双删策略
- 先删除缓存
- 更新数据库
- 休息一小段时间如500ms(防止有线程拿旧数据后写回缓存)延迟时间不好评估
- 再删除缓存
- 异步监听Binlog策略 - 最终策略
- Canel 阿里 监听Binlog发送至MQ,异步地更新Redis
- 解耦、利用MQ重试机制
强一致性要求
读写锁 读的时候添加共享锁,更新数据的时候添加排他锁
弱一致性要求
延时双闪 删缓存 - 删数据库 - 删缓存 这个“延时”的不确定性和实现上的额外复杂度
在项目中是先删数据库再删缓存
分布式锁
在分布式(多服务器、多进程)环境下,保证多个节点对同一个共享资源的互斥访问,从而避免数据不一致的问题。
一般来说的锁是在进程内的锁
SET原子命令 + Lua 脚本解锁
- 如果用Redis实现一个基本的分布式锁
SET lock_key unique_value NX PX 30000 保障是原子命令
- 如何安全的释放锁
防止误删别人的锁,加锁时加入一个属于自己的唯一标识
判断和删除必须是原子的(使用Lua脚本)
- 业务没执行完,锁却过期了怎么办
看门狗机制:启动一个后台线程每隔一段时间检查一下。如果持有则给锁续命。
看门狗机制
分布式锁续期(以 Redisson 为例) 在微服务中,如果你的节点获取了 Redis 分布式锁,但由于某些原因(比如 JVM 发生长时间的 Full GC,或者处理大批数据变慢),业务执行时间超过了锁的过期时间,锁被释放就会导致并发问题。
- 看门狗应用: Redisson 客户端底层实现了一个典型的“看门狗”。只要当前节点拿到锁,看门狗就会启动一个后台定时任务,每隔一段时间去给 Redis 里的这把锁续期(延长过期时间,即喂狗)。
- 异常兜底: 如果当前服务节点直接宕机(比如进程被 Kill),看门狗线程也会随之死亡,不再续期。Redis 里的锁在倒计时结束后就会自然过期并释放,从而避免死锁。
ZSET 底层数据结构
如何
Zset 的底层采用了跳表(Skip List)和哈希表(Hash Table)两种数据结构相结合的方式来实现。
哈希表:字典将 member 映射到 score
跳表: 跳表是 Zset 实现有序性的核心。它是一种“空间换时间”的概率性数据结构,可以看作是多层链表的集合,支持平均 O(log N) 时间复杂度的节点查找、插入和删除。每一层的节点都是从下一层链表中随机抽取一部分节点组成的。层数越高,节点越稀疏。
Hash底层数据结构
listpack(列表包) - 节省内存的实现 (用于小 Hash) 连续的、紧凑的内存块dict(字典) - 保证查询效率的实现 (用于大 Hash) 经典的“数组 + 链表”结构,也就是“拉链法”
结构升级为单向升级
数量超512 或 任意一个字段超64字节会升级
8钟淘汰策略
Redis 目前提供了 8 种主要策略,可以归纳为以下四大类:
1. 不淘汰策略
- noeviction(默认策略):不删除任何数据。当内存满时,对于写请求(如
SET,LPUSH)直接报错返回,只允许读请求(如GET)。在生产环境中,这通常是危险的,除非你对写入量和内存有绝对的预估。
2. 对设置了过期时间的数据进行淘汰 (Volatile)
这种策略只在设置了 TTL 的 Key 集合中进行筛选,保护那些没有过期时间的“永久”数据。
- volatile-lru:在设置了过期时间的 Key 中,使用 LRU 算法淘汰。
- volatile-lfu:在设置了过期时间的 Key 中,使用 LFU 算法淘汰。
- volatile-ttl:根据 Key 的剩余生存时间(TTL),优先淘汰最快过期的 Key。
- volatile-random:在设置了过期时间的 Key 中,随机淘汰。
3. 对所有数据进行淘汰 (Allkeys)
不区分是否设置了过期时间,全局筛选。
- allkeys-lru:在所有 Key 中,使用 LRU 算法淘汰。这是最常用的缓存场景配置。
- allkeys-lfu:在所有 Key 中,使用 LFU 算法淘汰。
- allkeys-random:在所有 Key 中,随机淘汰。
Redis 如何实现高可用
- 主从复制
- 哨兵模式
- 监控 - 心跳
- 通知
- 自动故障转移
- Redis Cluster Redis 集群 去中心化的一种模式
- 扩展写能力和存储能力,我们就需要引入集群模式。
- Redis Cluster 在存储的时候如何确定选择哪个节点呢?
- 将自己分成了 16384 个 Slot(槽位),哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中
主从复制
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。般都是一主多从,主节点负责写数据,从节点负责读数据
主从数据同步原理
- 全量同步(首次建立连接)
- 主节点通过Replid是否一致判断是否是第一次同步
- 若replid一致则发送repl_backlog中的命令(offset影响同步量),否则则生成RDB
- 主节点执行bgsave,生成RDB
- 主节点发送RDB到从节点
- 握手与请求(判断是不是第一次同步): 从节点向主节点发送
psync ? -1命令,请求同步。主节点回复+FULLRESYNC,带上自己的 Replid(主节点运行ID)和当前的复制偏移量(Offset)。 - RDB 快照生成与传输: 主节点执行
bgsave命令,fork出一个子进程生成 RDB 文件,并将 RDB 文件发送给从节点。从节点收到后,会清空本地旧数据,加载 RDB 文件。 - 增量命令回放(易漏点):面试官很看重这点! 主节点在生成和传输 RDB 的这段时间里,依然会接收新的写请求。这些新请求会被暂存在复制缓冲区(Replication Buffer)中。等从节点加载完 RDB 后,主节点会把这个缓冲区里的增量命令发给从节点执行,从而达到数据最终一致。
- 增量同步(salve重启或者后期数据变化)
- 从节点重连后,发送
psync <RunID> <Offset>给主节点,告诉主库:“我之前同步到这个位置了”。 - 主节点收到后,会去自己的
repl_backlog_buffer(复制积压缓冲区)里找。 - 如果这个 Offset 还在这块缓冲区里,主库就把断线期间丢失的那部分命令发给从库。
- 如果断线太久,Offset 已经被覆盖掉了,那就只能退化成全量复制。
- 从节点重连后,发送
两个 Buffer 的严格区分(绝佳亮点):
- Replication Buffer(复制缓冲区): 用于全量同步期间暂存主库接收的写命令。每个从库连上来,主库都会为它分配一个。如果这个 Buffer 满了,主从连接会被断开。
- repl_backlog_buffer(复制积压缓冲区): 用于增量同步。它是一个固定大小的环形缓冲区(默认 1MB),主库不管有多少个从库,都只维护这一个。如果写满了,就会覆盖最早的数据。
哨兵模式
在 Redis 的主从架构中,由于主从模式是读写分离的,如果主节点(master)挂了,那么将没有主节点来服务客户端的写操作请求,也没有主节点给从节点(slave)进行数据同步了。如果需要恢复则需要人工介入,但这样不方便,就有了哨兵机制。
哨兵是一个运行在特殊模式下的 Redis 进程,所以它也是一个节点。哨兵节点主要负责三件事情:监控、选主、通知。
监控:哨兵怎么判断主库真的挂了?
在网络环境中,单次 Ping 不通不能代表节点真的死了,可能只是因为网络抖动或者拥塞。(“防误判”)
- 主观下线(S_DOWN): 单个哨兵节点每秒钟会向主库发送
PING命令。如果主库在设定时间内(down-after-milliseconds)没有回复,这个哨兵就会“单方面”认为主库挂了。 - 客观下线(O_DOWN): 为了防止误判,当一个哨兵主观认为主库挂了之后,它会向其他哨兵发消息,询问“你们觉得它挂了吗?”。只有当认为主库挂掉的哨兵数量达到了配置的 Quorum(法定人数) 时,哨兵集群才会正式宣布主库客观下线。
💡 老师敲黑板: 答出是关键。这体现了你对计算机网络中不可靠通信机制的深刻理解。
选主: 既然确定主库挂了,由哪个哨兵来执行主从切换?
主从切换是一个非常复杂的动作,不能多个哨兵同时去抢着干,否则就乱套了。所以哨兵内部需要选出一个“领导者”(Leader)来全权负责故障转移。
- 选举机制(Raft 算法): 发现主库客观下线的那个哨兵,会率先发起投票,要求其他哨兵选举自己当领导。
- 规则: 每个哨兵在同一个选举轮次中只有一票。谁先拿到半数以上的赞成票,且票数大于等于 Quorum,谁就成了领导者。
选主: 新的主库是怎么挑出来的?(新主选举规则)
Leader 哨兵要在剩下的从库中挑一个最好的晋升为主库。这个筛选和打分过程分为三步:
- 第一轮:淘汰不合格的。 过滤掉那些已经下线的、网络状态不好的(比如经常断连)从库。
- 第二轮:看优先级(Priority)。 Redis 配置里有个
replica-priority参数,数值越小优先级越高。如果有人优先级最高,直接胜出。 - 第三轮:比对数据偏移量(Offset)。 如果优先级一样,Leader 会看哪个从库的 Offset 最大(我们在上一题讲过这个概念)。Offset 最大意味着这个从库同步的数据最全、丢失的数据最少,它当然最有资格当主库。
- 第四轮:比对运行 ID(Run ID)。 如果数据也一样全,那就只能拼运气了,选 Run ID 最小的那个。
分片集群
为什么需要Redis分片集群
- 单机内存瓶颈/并发写瓶颈
Redis 集群是如何分片的?为什么不直接用一致性哈希?
- 哈希槽机制: Redis Cluster 并没有使用一致性哈希,而是引入了哈希槽(Hash Slot)的概念。整个集群有 16384 个哈希槽。(16384 bits/8=2048 bytes=2KB)
- 路由算法: 对 Key 使用 CRC16 算法计算哈希值,然后对 16384 取模:
HASH_SLOT = CRC16(key) mod 16384。集群中的每个主节点负责一部分槽位。 - 不用一致性哈希的原因: 一致性哈希在节点增删时,数据迁移的粒度是整个节点上的数据,容易造成数据倾斜。而哈希槽将数据管理的粒度缩小到了“槽”,在进行扩缩容(节点增删)时,只需要手动或自动将部分槽位从一个节点迁移到另一个节点,非常灵活可控。
集群节点之间是如何判断节点宕机并进行故障转移的?
- Gossip 协议: 节点之间互相 PING/PONG,交换彼此的状态信息。
- PFAIL(主观下线): 节点 A 发现节点 B 长时间没响应,A 会在本地将 B 标记为 PFAIL(Possible Fail)。
- FAIL(客观下线): 节点 A 在心跳中把 B PFAIL 的状态传播出去。当集群中超过半数的主节点都认为 B 是 PFAIL 时,B 就被正式标记为 FAIL(客观下线),并向全网广播。
- 选举机制: B 的所有从节点开始发起选举。它们会根据自己的复制偏移量(数据新旧程度)来决定选举延迟,数据越新的从节点越早发起投票。其他主节点进行投票,获得半数以上选票的从节点上位成为新主节点。
Redis集群是如何扩容的
- 节点握手 建立连接
- 哈希槽重分片 16384个哈希槽
- 数据迁移
MIGRATE是一个原子操作,它会将 Key 序列化发送给目标节点,并在目标节点确认接收后,从源节点删除该 Key。 - 迁移中的请求处理:ASK重定向
- 情况 A:Key 还在源节点。源节点直接处理。
- 情况 B:Key 已经迁移到了目标节点,或者还没创建。
- 源节点会返回一个
ASKING错误(ASK 重定向),告诉客户端:“这个槽位正在搬家,你去新节点问问看。” - 客户端先向新节点发送一条
ASKING指令(打破新节点此时的IMPORTING限制),然后再发送具体的业务指令。
- 源节点会返回一个