10.2 Redis
10.2.1 Redis数据结构
关系型数据库 vs 非关系型数据库有什么区别?
-
关系型数据库(RDBMS)
-
采用表格(表)的形式存储数据,数据以行和列组织。
-
表与表之间可以通过主键和外键建立关系,支持复杂的关联查询。
-
数据结构是预定义的(Schema-based),需要事先定义好表结构。
-
-
非关系型数据库(NoSQL)
-
数据模型灵活多样,常见的有:
-
键值存储(Key-Value):如 Redis,数据以键值对形式存储。
-
文档存储(Document):如 MongoDB,数据以类似 JSON 的文档形式存储。
-
列族存储(Column-family):如 Cassandra,适合海量数据的高效读写。
-
图数据库(Graph):如 Neo4j,适合处理节点和关系复杂的数据。
-
-
大多数 NoSQL 是无模式(Schema-less)或灵活模式,可以动态存储不同结构的数据。
-
为什么有了关系型数据库还要非关系型数据库?
关系型数据库的局限性:
-
扩展性差(尤其是水平扩展)
-
RDBMS 通常依赖垂直扩展(提升单机配置如 CPU、内存),但硬件是有上限的。
-
水平扩展(加机器)比较困难,分库分表复杂,维护成本高。
-
-
模式固定(Schema)
-
表结构需要预先定义,新增字段或修改结构往往需要停机维护或复杂迁移。
-
不适合数据结构多变、快速迭代的业务(如用户行为日志、IoT 数据)。
-
-
不适合海量数据和高并发
-
在数据量极大(如 TB/PB 级别)或并发请求非常高(如秒杀、社交网络)时,传统 RDBMS 容易成为瓶颈。
-
-
复杂关联查询虽强,但性能开销大
-
多表 JOIN 操作在数据量大时性能下降明显,有时不如 NoSQL 的扁平化数据模型高效。
-
【必问】Redis为什么快?
-
基于内存存储,避免了磁盘 I/O 的性能瓶颈;
-
单线程模型,避免了多线程的锁竞争和上下文切换开销;
-
高效的数据结构,如 String、Hash、List 等都做了底层优化;
-
使用 I/O 多路复用模型(如 epoll/kqueue),高效处理大量并发连接;
-
语言层面使用 C 语言编写,执行效率高,无额外运行时开销。
为什么Redis设计为单线程?
-
避免多线程带来的复杂性和开销
-
内存操作本身非常快,没有迫切需要引入多线程来提升性能
-
单线程模型简化了设计与实现
-
从 Redis 6.0 开始,引入了多线程 I/O(I/O Threads),用于处理网络读写(如接收客户端请求和发送响应),但命令执行仍然是单线程的。这样可以在高并发网络环境下进一步提升吞吐量,同时保持命令执行的简单性和原子性
【必问】Redis中你用过哪些数据类型?
在Redis里我主要用过String、Hash、List、Set和Sorted Set这几种核心数据类型。
String是最基础的,存字符串或数字都行,比如缓存用户信息或者做简单的计数器,像统计文章访问量,用INCR命令就挺方便。Hash特别适合存对象,比如用户资料,字段名当key,字段值当value,比把整个对象序列化成String更灵活,修改单个字段也不用读出全部数据。List我用来实现消息队列或者最新消息排行,比如用LPUSH放新消息,BRPOP消费,保证顺序性。Set是无序且去重的,适合存标签或者共同好友这种场景,用SINTER能快速算交集。Sorted Set我用的也多,比如排行榜,按分数排序,ZRANGE就能按名次取数据,还能用ZRANK查某个元素的排名。
这些类型基本覆盖了大部分业务场景,选对类型对性能影响挺大的,比如频繁更新的计数器用String比用Hash更高效。
为什么Redis Zset用跳表实现而不是红黑树?
-
实现简单,开发和维护成本低
-
支持范围查询(Range Query)更高效
-
性能差距不大,跳表平均也是 O(log N)
为什么Redis Zset用跳表实现而不是B+树?
-
跳表实现更简单,代码易于维护和调试。
-
跳表专为内存设计,无 I/O 考虑,而B+树为磁盘存储优化,节点大小适配页大小,更复杂。
Redis中跳表的实现原理是什么?
跳表是一种基于有序链表的数据结构,通过添加多级索引来加速查找,可以理解为“多层的有序链表”。
跳表由多层链表组成:
-
第 0 层:是一个普通的有序单向链表,包含所有元素。
-
第 1 层及以上:是索引层,每个节点以一定概率(如 50%)“晋升”到上一层,形成快速通道,用于加速查找。
-
每个节点包含多个指针,分别指向同一层的前进节点,以及指向下一个层的节点。
通过这种多层索引结构,跳表能够在查找时“跳跃式”前进,从而实现接近二分查找的效率。
-
有序集合(ZSet) 的实现中:当 ZSet 中的元素较多,或者元素的成员(member)是字符串等无法直接用整数编码时,Redis 会使用 跳表 + 哈希表 的组合:
-
哈希表:用于实现 O(1) 的成员到分数的映射(快速查找某个 member 的 score)。
-
跳表:按 score 排序,支持范围查询(如 ZRANGE, ZRANGEBYSCORE 等操作)。
-
Redis的ListPack数据结构是什么?
Redis 的 ListPack 是一种用于高效存储有序字符串序列的紧凑型数据结构,它是 Redis 在 Redis 5.0 之后引入的一种新数据类型,用来替代早期的 ZipList(压缩列表)在某些场景下的使用,尤其是为了克服 ZipList 的一些固有缺陷,同时保持内存紧凑、访问高效的特点。
在 Redis 早期,ZipList(压缩列表) 被广泛用于以下数据类型的底层存储,当元素数量较少且每个元素较小时,使用 Ziplist 可以节省内存,避免使用链表或哈希表带来的额外开销。
ZipList 的痛点:
-
连锁更新问题(Cascade Update):
-
当你在 Ziplist 中插入或删除一个元素,导致某个 entry 的长度发生变化,且这个 entry 前面有其它 entry,就可能触发一连串的 entry 重新分配内存和地址偏移重写,造成性能抖动。
-
这种问题在数据量大或元素变长时尤其明显,可能导致插入/删除的复杂度从 O(N) 恶化到实际表现更差。
-
-
修改风险高,容易引发大规模内存重写。
ListPack 与 ZipList 类似,也是一段连续的内存空间,存储了多个变长 Entry(元素),但它做了重要的改进:
主要特性:
-
连续内存存储,无额外内存碎片
-
元素可变长,支持多种数据类型
-
没有前置长度限制导致的连锁更新问题
-
尾部有总长度信息
-
支持双向遍历(有 prev raw len,但优化了设计)
Redis dict是什么数据类型?底层是什么?
在 Redis 中,Dict(字典) 是一个用于存储键值对(key-value)的数据结构,类似于其他编程语言中的 Map / Dictionary / HashMap。
它是 Redis 中非常核心的一个内部数据结构,虽然用户平时直接操作的是 String、Hash、Set 等高级类型,但 很多底层都是用 Dict 来实现的。
Redis dict扩容触发条件是什么?
Dict 的底层是一个哈希表(HashTable),它通过 哈希函数 + 数组(桶)+ 链表解决冲突(链地址法) 来实现 key 的高效存取。
Redis 引入了 动态扩容机制:即当哈希表的负载因子(load factor)达到一定阈值时,自动触发扩容,增加桶的数量,减少冲突,提升性能。
扩容触发条件(满足以下条件之一时,可能触发扩容):
-
负载因子 ≥ 1,并且:
-
没有正在执行 RDB / AOF 重写操作
-
即正常情况下,只要 负载因子 ≥ 1,就会触发扩容
-
-
负载因子 ≥ 5,并且正在执行 BGSAVE / BGREWRITEAOF(后台持久化)
-
为了在持久化期间尽量减少内存分配和阻碍,Redis 会放宽扩容条件,即使负载因子很高也允许扩容
-
【必问】Redis dict如何扩容?渐进式rehash?
Redis 的 Dict 扩容,本质上就是 创建一个更大的哈希表(通常是原大小的 2 倍),然后将旧表中的所有键值对,重新哈希并迁移到新表中,这个过程叫做 Rehash(重哈希)。
但与传统一次性迁移所有数据的方式不同,Redis 为了避免服务阻塞,采用了一种非常巧妙的方式:渐进式 Rehash(Incremental Rehashing)。
Redis 的 Dict 结构中维护了 两张哈希表:ht[0] 和 ht[1]
-
ht[0]:当前正在使用的哈希表
-
ht[1]:用于扩容时作为新表(目标表)
-
触发扩容时:
-
Redis 会创建一个新的、更大的哈希表 ht[1](通常是 ht[0].size 的 2 倍,且一般是 2^n 大小)
-
设置 rehashidx = 0,表示开始 rehash,从第 0 号桶开始迁移
-
但并不会一次性迁移所有数据!
-
渐进式迁移(核心机制):
-
在 每次对 Dict 的增、删、查、改操作时,Redis 会顺带从 ht[0] 的 rehashidx 索引位置开始,迁移 1~2 个 bucket(哈希桶)中的数据到 ht[1]
-
每次操作只迁移一点点,把 rehash 的开销均摊到各个日常操作中
-
rehashidx 会逐步递增,表示当前已迁移到哪个桶
-
迁移完成:
-
当 所有桶都迁移完毕(即 ht[0] 为空)时
-
Redis 会将 ht[1] 设置为 ht[0],并释放旧的 ht[0],重置 rehashidx = -1,表示 rehash 完成
Redis Zset的dict和跳表是怎么对应的?
它们并不是总是成对出现,但在 ZSet 中,Dict 和 SkipList 是配合使用,各自发挥优势,共同完成复杂功能。
ZSet 需要支持以下功能:
-
根据 member(如 "Alice")快速查找到 score(如 100) → 需要快速查找,O(1)
-
根据 score 排序,支持范围查询(如 ZRANGE score1 score2) → 需要有序结构,支持排序和范围查找
-
支持根据 score 查找 member,或获取排名(ZRANK) → 需要有序 + 排名
这些功能单一数据结构无法高效同时满足,所以Redis ZSet 的底层是:
“Dict + SkipList” 的组合结构,两者分工合作,优势互补!
-
Dict(字典):member → score
-
作用:以 member 为 key,score 为 value,提供 O(1) 的快速查找。
-
-
SkipList(跳表):score → member(按 score 排序)
-
作用:将所有 member 按照 score 排序存储,并建立多级索引
-
Redis有大key会发生什么?读写耗时高?
在 Redis 中,大 Key 指的是某个 Key 对应的 Value 占用的内存空间比较大,具体来说:
-
String 类型:Value > 10KB(或更大,如 1MB)就是大 Key
-
集合类型(Hash/List/Set/ZSet):元素数量 > 5000 ~ 10000,或总大小较大,也视为大 Key
-
读写耗时高,性能下降(核心问题 ✅)
-
阻塞 Redis 单线程,影响整体 QPS(关键!)
-
网络传输压力大
-
内存分配与回收成本高
-
主从同步与持久化 RDB/AOF 延迟变大
如何排查大key?
-
使用 Redis 命令(简单但不够全面)
-
DEBUG OBJECT key:可以查看某个 Key 的序列化长度(serializedlength),但不够直观。 -
STRLEN key(String 类型) -
HLEN key、LLEN key、SCARD key、ZCARD key:查看集合大小,但看不到单个元素大小。
-
-
推荐工具:redis-rdb-tools / redis-cli --bigkeys
-
redis-cli --bigkeys(Redis 自带):-
扫描整个 Redis 数据库,找出 可能的大 Key(按类型统计最大 Key)
-
命令:
redis-cli -h yourhost -p yourport --bigkeys -
缺点:有一定的抽样性质,不够精准,但对快速排查很有用。
-
-
redis-rdb-tools(第三方,离线分析 RDB 文件):-
可以精确分析内存使用情况,找出真正的大 Key,包括每个 Key 的内存占用、元素数量等。
-
更精准,适合深度分析。
-
-
10.2.2 Redis用途
【必问】Redis有哪些用途?
-
实现分布式锁。
-
实现排行榜:使用 ZSET(有序集合),通过
ZADD添加成员与分数,ZRANGE或ZREVRANGE获取排行榜。 -
实现分布式Session:使用 String 或 Hash,以
sessionId为 key 存储用户 session 数据,结合 Redis 的分布式特性共享 session。 -
实现点赞:使用 String 或 Hash 记录点赞总数(如
like:postId),用 Set(如likes:postId)存储点赞用户 ID 防重复。 -
实现IP限流:使用 String + Lua 或 ZSET,以 IP 为 key,记录访问时间戳,通过 ZSET 范围查询统计时间窗口内请求数,或用 String + Lua 原子限流。
-
实现在线用户列表:使用 Set 存储在线用户 ID(如
online:users),登录时SADD,登出时SREM,SMEMBERS或SCARD获取列表或数量。 -
实现读写锁:读锁为乐观锁,用计数器控制,如果没有写锁(返回nil),则获得读锁,使用完释放读锁。写锁直接SET NX,释放锁使用Lua脚本。
如何用Redis实现分布式锁?
见分布式专栏。
如何用Redis集群做一个分布式延迟队列?(该问题=分布式定时器升级版)
分布式延迟队列设计要点:
-
可排序数据结构 需要一种高效的数据结构,能够按照任务的执行时间进行排序或索引,便于快速获取当前应该被执行的任务。
-
任务触发机制 系统需要定期或在特定时机检查哪些延时任务已经到达或即将到达其设定的执行时间,并将其“激活”供消费者处理。
-
高可用与一致性 在分布式环境下,任务不能因为节点宕机等原因而丢失,同时要保证任务只被消费一次(至少一次或精确一次语义)。
-
可扩展性与性能 延迟队列应支持大量任务的写入和读取,且在高并发下保持稳定性能。
-
可持久化。
为什么选择 Redis 而非 MySQL?
-
数据结构:提供如 Sorted Set(ZSET) 这种天然适合按分数(如执行时间戳)排序的结构
-
性能:内存操作,读写速度极快,适合高并发场景
-
原子性操作:提供丰富且高效的原子操作,如 ZRANGEBYSCORE、ZREM 等
-
持久化与集群:支持持久化和 Redis Cluster,可满足高可用和扩展需求
分布式延迟队列中的生产者和消费者
一、生产者(Producer)
-
职责:创建延时任务,并将其写入到延迟队列中。
-
实现方式:
-
为每个延时任务生成一个唯一 ID(如 UUID 或业务主键)。
-
设定该任务的 执行时间戳(例如:未来某一时刻的 Unix 时间戳)。
-
将任务信息(如任务ID、执行内容、执行时间等)以某种形式(如 JSON)存储,并利用 Redis 的 Sorted Set(ZSET) 结构,以 执行时间戳作为 score,任务ID(或任务内容)作为 member 进行插入。
-
例如:
ZADD delayed_queue <future_timestamp> <task_id_or_payload>
-
-
-
特点:
-
生产者无需关心任务何时被消费,只需确保任务被正确投递到延迟队列中。
-
可以是任意数量的分布式服务节点,只要能访问 Redis 集群即可。
-
二、消费者(Consumer)
-
职责:定时或在轮询机制下检查延迟队列中是否有已到期的任务,并将其取出进行消费。
-
实现方式:
-
消费者定期(比如每隔几秒)使用
ZRANGEBYSCORE命令查询 Sorted Set 中 score(即执行时间戳)小于或等于当前时间戳 的所有任务。-
例如:
ZRANGEBYSCORE delayed_queue -inf <current_timestamp>
-
-
获取到这些任务后,将其从 Sorted Set 中移除(如使用
ZREM),以避免重复消费。-
为保证原子性,可使用 Lua 脚本将查询和移除操作合并为一个操作,防止并发时多个消费者获取到同一任务。
-
-
拿到任务后,消费者根据任务内容进行实际的业务处理。
-
-
特点:
-
消费者也可以是多实例部署,形成消费者集群,共同分担任务处理压力。
-
为了提高实时性,消费者可以部署为常驻进程并轮询;也可以结合消息通知机制(如 Redis 的 Pub/Sub 或外部的定时器服务)优化触发机制。
-
若任务未被成功处理,可根据业务需求做重试、死信队列等后续处理。
-
10.2.3 缓存问题
MySQL不是有buffer pool吗?为什么还需要Redis来做缓存?
MySQL 的 Buffer Pool 是数据库自身的内存缓存,用来优化磁盘 I/O,对开发者透明。Buffer Pool一般只有LRU这一种淘汰策略,不易于自己设策略。
而 Redis 是独立于数据库的高性能缓存系统,通常由应用层主动使用,目的是加速热点数据访问、降低数据库压力、提升系统整体性能与并发能力。
两者可以配合使用,但解决的问题不同。
【必问】Redis缓存穿透是什么,如何解决?
缓存穿透(Cache Penetration) 是指:客户端请求的数据既不在缓存(Redis)中,也不在数据库中,导致每次请求都要去访问数据库,缓存失去了应有的作用。
-
方案 1:缓存空对象(Cache Null)
-
方案 2:使用布隆过滤器(Bloom Filter)【推荐】
-
方案 3:接口层校验 / 参数合法性检查
-
方案 4:热点参数限流 / 黑名单机制
布隆过滤器是什么机制?能细说吗?
-
在缓存层之前加一个布隆过滤器,所有 合法的数据 key 都预先加入布隆过滤器(比如所有存在的商品 ID)。
-
当用户请求某个 key 时,先经过布隆过滤器检查:这个 key 是否可能存在?
-
如果布隆过滤器判断 “不存在”,则直接返回“数据不存在”,无需查询 Redis 和数据库。
-
如果判断 “可能存在”,再去查 Redis,Redis 没有再查数据库。
-
布隆过滤器底层机制:
-
添加元素(写入)
当我们要往布隆过滤器中加入一个元素时,比如字符串 "apple",会进行如下操作:
-
使用 k 个不同的哈希函数 分别对
"apple"进行哈希计算; -
每个哈希函数会得到一个整数索引值,对应位数组中的某一位;
-
将这 k 个索引位置上的 bit 值全部置为 1。
-
查询元素(判断是否存在)
当我们要判断一个元素(比如 "apple")是否可能在集合中时,进行如下操作:
-
同样使用那 k 个哈希函数 对该元素进行哈希,得到 k 个索引;
-
检查这 k 个索引对应的 bit 位是否都为 1:
-
如果全部为 1 → 元素“可能存在”于集合中(可能有误判)
-
如果有任意一个为 0 → 元素“一定不存在”于集合中(不会漏判)
-
【必问】Redis缓存击穿是什么,如何解决?
缓存击穿(Cache Breakdown)是指:某个热点 Key(访问非常频繁的数据)在 Redis 中的缓存突然失效(过期),此时大量并发请求同时访问这个 Key,这些请求发现缓存中没有,就都会直接穿透到数据库去查询,导致数据库瞬间压力剧增,甚至可能被打挂。
-
方案 1:设置热点数据永不过期(不推荐单独使用)
-
方案 2:使用互斥锁(Mutex Lock)【推荐】
-
方案 3:逻辑过期(逻辑上不过期,异步更新缓存)
-
方案 4:后台定时刷新缓存
【必问】Redis缓存雪崩是什么,如何解决?
缓存雪崩(Cache Avalanche)是指:Redis 中大量热点 Key 在同一时间失效(过期),或者 Redis 服务突然宕机,导致大量请求直接穿透到数据库,造成数据库压力暴增,甚至崩溃,整个系统无法正常提供服务。
-
方案 1:给缓存 Key 设置随机的过期时间(避免同时失效)【推荐】
-
方案 2:使用 Redis 高可用架构(防止 Redis 宕机)【强烈推荐】
-
方案 3:设置缓存永不过期 + 后台定时更新(逻辑控制过期)
-
方案 4:使用互斥锁 / 逻辑过期 等手段防止大量请求同时重建缓存
-
方案 5:限流、降级、熔断保护数据库
10.2.4 缓存一致性
【必问】Redis缓存更新策略有哪几种?具体的读写策略是什么?
-
Cache Aside(旁路缓存 / 懒加载)—— 最常用
-
读操作:先查缓存,命中则直接返回;未命中则查数据库,将数据写入缓存后再返回。
-
写操作:先更新数据库,再删除缓存(推荐,而不是更新缓存)。
-
Read/Write Through(读写穿透)
-
读操作:如果缓存命中,直接返回;如果未命中,缓存层从数据库加载数据并更新缓存,然后返回。
-
写操作:应用更新缓存,缓存负责同步更新数据库。
-
Write Behind Caching(异步写回 / 写后更新)
-
应用只更新缓存,由缓存异步批量将数据写入数据库。
一条热点数据在Redis中和数据库中可能的状态有几种?
-
MySQL 有 Redis 无:因为是热点数据,所以需要从数据库同步到缓存
-
MySQL 无 Redis 有:出错!
-
MySQL 有 Redis 有 数据一致:OK
-
MySQL 有 Redis 有 数据不一致:出错!
-
MySQL 和 Redis 无:还没写到数据库
【必问】旁路缓存如何保证缓存和数据库的一致性?
简单实现:先更新数据库,再删除缓存。
企业级实战:延迟双删策略。
(追问)先删缓存,再更新数据库为什么会导致不一致?
-
请求 A 删除缓存;
-
请求 B 读缓存未命中,去数据库读取旧值,并回填缓存;
-
请求 A 再更新数据库;
-
结果:缓存里是旧数据,数据库是新数据 → 更容易产生不一致!
这种顺序更容易引发问题,因此不推荐。
(追问)先更新数据库,再更新缓存为什么会导致不一致?
-
请求 A 更新数据库;
-
请求 B 再更新数据库;
-
请求 B 更新缓存;
-
请求 A 再更新缓存;
-
结果:缓存里是A数据,数据库是B数据 → 更容易产生不一致!
(追问)先更新数据库,再删除缓存为什么还是不具有一致性?
这是标准做法,但仍可能在极端并发情况下出现不一致:
-
请求 A 更新数据库(比如将 age 从 20 改为 30);
-
请求 A 删除缓存;
-
请求 B 读取数据:
-
缓存未命中 → 去数据库读取旧值 20(如果请求 A 的数据库更新尚未完成,或发生主从延迟等);
-
将旧值 20 回填到缓存;
-
-
结果:缓存中是旧数据 20,数据库是 30,不一致。
但这种情况是窗口极小的,尤其是在数据库主从一致、事务完成后再删缓存时,概率较低。
(追问)延时双删策略如何保证最终一致性?
改进:延时双删策略(Double Delete with Delay)
为了进一步降低并发导致的脏数据回填问题,可以采用如下三步:
-
请求 A 先删除缓存;或者带有过期时间地更新缓存
-
请求 A 更新数据库;
-
请求 A 延迟一段时间(如 200~500ms)后,再次删除缓存;
延迟删除的目的是:防止在更新数据库期间,有其他请求读了旧数据并回填缓存,再删一次可尽量把可能的旧数据清理掉。
这是一种增强版 Cache Aside,对一致性要求较高的场景可以考虑。
注意:延迟时间需要根据实际业务读取延迟估算,比如数据库主从同步延迟、请求间间隔等。
数据库更新成功,但是删除缓存失败,如何解决?
当数据库数据发生变化时,通过消息队列通知相关服务 删除对应的 Redis 缓存 key,确保删除成功。
使用 MySQL binlog(如 Canal、Maxwell)监听数据库变更,删除失败就一直重试。
MySQL的数据同步到Redis使用什么中间件?
-
go-mysql-transfer
-
canal:阿里开源
-
mysql-build
-
触发器+udf(user define function)
前三者和MySQL主从复制原理相同。
只写的主库和只读的从库,更新主库数据删除缓存,另一个请求读从库写入缓存导致缓存不一致,怎么办?
可能可以通过同时监听主从数据库的binlog来更新缓存,怎么搞我也不清楚。
10.2.5 持久化
【必问】Redis的RDB持久化方法是什么?
Redis 的 RDB(Redis DataBase)持久化 是 Redis 提供的一种将内存中的数据保存到磁盘上的快照(snapshot)方式的持久化机制。它通过生成数据的二进制快照文件(通常命名为 dump.rdb),在服务器重启时可以基于该文件恢复数据。
RDB 持久化的核心是 在某个时间点将 Redis 当前内存中的所有数据生成一个紧凑的二进制快照文件,并将其写入磁盘。这个过程是 非阻塞的(在默认配置下,Redis 会 fork 一个子进程来做持久化,主进程继续处理客户端请求)。
RDB 持久化可以通过以下两种方式触发:
-
手动触发(主动保存)
-
自动触发(通过配置规则)
-
RDB 的优点:
-
文件紧凑,适合备份与灾难恢复:RDB 是一个紧凑的二进制文件,很适合用于数据备份,可以定期将 RDB 文件拷贝到其他存储介质。
-
恢复速度快:相比 AOF,RDB 文件恢复数据的速度通常更快,尤其数据量较大时。
-
子进程持久化,主进程影响小:通过 fork 子进程进行持久化,主进程可继续处理请求,性能较高。
-
-
RDB 的缺点:
-
可能丢失数据:RDB 是 时间点快照,如果在两次快照之间 Redis 发生了宕机,则这段时间内的数据修改会丢失。
-
大数据量时 fork 可能阻塞:如果 Redis 数据量非常大,fork 子进程的过程可能会较耗时,造成主线程短暂阻塞(尤其在内存大的情况下)。
-
不适合实时持久化需求:如果你对数据的完整性要求非常高,希望尽可能不丢数据,RDB 可能不是最佳选择。
-
【必问】Redis的AOF持久化方法是什么?
Redis 的 AOF(Append Only File,追加日志文件)持久化 是 Redis 提供的另一种数据持久化方式,它通过记录 所有修改数据的写命令(如 SET、DEL 等)来实现数据的持久化,从而在 Redis 重启时通过 重新执行这些命令 来恢复数据。
与 RDB 保存数据快照不同,AOF 持久化是 保存所有对数据库进行写操作的命令,并将这些命令以日志的形式 追加写入到一个文件中(默认是 appendonly.aof)。当 Redis 重启时,它会通过 重新执行 AOF 文件中的命令 来还原数据集,从而达到持久化的目的。
AOF 的工作流程:
-
客户端发送写命令(如 SET、DEL)
-
Redis 执行该命令,并同时将该命令以协议格式追加到 AOF 缓冲区
-
根据配置的同步策略,将 AOF 缓冲区的数据写入磁盘(appendonly.aof 文件)
-
Redis 重启时,读取 AOF 文件并重放命令,恢复数据
-
AOF 的优点
-
数据安全性更高:AOF 持久化可以配置为 每秒同步甚至每次写都同步,数据丢失风险更小。
-
可读性强(原始格式):AOF 文件本质是命令日志,便于人工分析和排查问题(特别是在早期未重写的情况下)。
-
适合对数据完整性要求高的场景:比如金融、交易类业务,更倾向于使用 AOF 或 AOF + RDB 混合方式。
-
-
AOF 的缺点
-
文件通常比 RDB 大:因为记录的是命令,尤其在没有重写之前,AOF 文件可能膨胀得比较厉害。
-
恢复速度比 RDB 慢:因为需要 重新执行命令,尤其是数据量大时,恢复时间更长。
-
写操作频繁时可能影响性能:尤其是
appendfsync always的情况下,性能开销较大。
-
AOF为什么要设计先执行命令再写入,而不是像MySQL一样先记录日志?
为什么 MySQL 要先写日志?1、确保事务的持久性(Durability):即使系统崩溃,也可以通过日志恢复数据。2、提高性能:写日志是顺序 IO,比随机写数据文件快得多。3、支持事务回滚与崩溃恢复。
而Redis 是一个内存数据库,设计目标是高性能与低延迟。
-
Redis 的核心定位是:内存数据库,强调速度、低延迟、高并发。
-
如果采用 “先写日志,再执行命令” 的模式:
-
每个写操作都得 先写磁盘日志(顺序 IO),再执行内存修改。
-
这意味着 每个写请求都要等待一次磁盘写入成功,哪怕只是追加日志。
-
对于 Redis 这种 每秒可能处理数十万请求 的系统来说,这种同步写磁盘的开销是 不可接受的。
-
结论:先写日志再执行,会显著拖慢 Redis 的性能,与它 “内存速度” 的设计目标冲突。
AOF文件记录的指令过多文件变得很大怎么办?
随着时间推移,AOF 文件会因为不断追加命令而变得越来越大。为了解决这个问题,Redis 提供了 AOF 重写机制,通过该机制可以生成一个 更精简、等效的新 AOF 文件。
AOF 重写是 Redis 创建一个新的 AOF 文件,这个文件只包含 恢复当前数据集所需的最少命令,而不是记录所有历史操作。
比如:
-
原始 AOF 中可能多次对同一个 key 执行修改,而重写后只保留最后一次有效操作。
-
如果一个 set 操作后又被 del,重写时可能直接不记录这些无效操作。
如何触发AOF重写?
-
手动触发:使用命令
BGREWRITEAOF -
自动触发:通过配置文件设置重写条件,如:
满足以上两个条件之一时,Redis 会在后台自动触发 BGREWRITEAOF,生成更小的 AOF 文件。
AOF重写的原理是什么?
-
Redis 会 fork 一个子进程,子进程基于当前内存中的数据,生成一组 最精简的写命令集合,写入到一个临时文件。
-
主进程继续处理客户端请求,并将新命令 同时写入 AOF 缓冲区 和 重写缓冲区。
-
当子进程重写完成后,主进程会将重写缓冲区的内容追加到新 AOF 文件,并替换旧的 AOF 文件。
整体过程对主线程影响较小,类似 RDB 的 BGSAVE 机制。
【必问】Redis的混合持久化方案是什么?
Redis 的 混合持久化(Mixed Persistence)方案 是 Redis 从 4.0 版本开始引入 的一种结合了 RDB 快照与 AOF 日志两者优点的持久化机制,目的是在保证数据恢复速度的同时,也尽量不丢失数据,提高数据的可靠性与恢复效率。
在 AOF 文件中,不仅记录写命令(纯文本或重写后的格式),还包含一份最新的 RDB 快照数据。也就是说:
AOF 文件的前半部分是一个 RDB 格式的快照,后半部分是后续的增量 AOF 命令日志。
这样,在 Redis 重启进行数据恢复时:
-
先快速加载 AOF 文件中的 RDB 部分(二进制快照),恢复大部分数据
-
再重放 AOF 文件后面追加的增量命令,恢复最近的数据变更
混合持久化的触发时机:
混合持久化 不会自动一直开启写入 RDB 格式,而是在 AOF 重写(BGREWRITEAOF)时,Redis 会将当前内存数据的 RDB 快照写入到 AOF 文件中,然后继续以 AOF 命令日志的形式记录后续的写操作。
典型流程:
-
Redis 正常运行,使用 AOF 记录写命令。
-
某个时刻,Redis 执行了 AOF 重写(比如通过 BGREWRITEAOF 命令或者自动触发)
-
在 重写 AOF 文件时,如果
aof-use-rdb-preamble yes,Redis 会先写入一个 RDB 格式的快照到新的 AOF 文件中 -
然后再将 重写期间以及之后的新命令以 AOF 命令格式继续追加到文件末尾
-
最终生成的 AOF 文件就是 RDB + AOF 混合格式
-
Redis 重启时,会智能识别该文件格式,并采用对应方式恢复数据
10.2.6 高可用
【必问】Redis主从模式是什么?主从复制策略有哪些?
Redis 的主从模式(Master-Slave Replication) 是一种数据复制与高可用架构方案,它允许一个 Redis 主节点(Master) 将自己的数据异步地复制到一个或多个从节点(Slave/Replica) 上。
-
主节点(Master):负责处理所有的写操作(Write)和部分读操作(Read),并将数据的修改同步到从节点。
-
从节点(Slave/Replica):只接收主节点同步过来的数据,通常用于处理读操作(Read),以分担主节点的读压力,同时也可以作为数据的备份,提高系统的可用性与容灾能力。
Redis 的主从复制主要涉及以下几种同步策略:
-
全量同步(Full Synchronization)
-
增量同步(Partial Synchronization / 增量复制)
为什么全量同步使用RDB而非AOF?
-
RDB 是一个紧凑的、二进制的数据快照,文件体积小、传输速度快,适合用于大数据量的初次同步;
-
AOF 记录的是所有写命令的日志,文件通常较大、解析成本高,不适合作为数据初始化或批量传输的载体;
-
生成 RDB 快照并传输,可以让从节点快速加载一个完整一致的数据状态,而不用重放大量命令,效率更高;
-
RDB 的加载速度比 AOF 快,尤其当数据量大的时候,优势更加明显。
Redis复制延迟的常见原因有哪些?
复制延迟(Replication Lag) 是指:从节点接收并应用主节点的写命令比主节点执行这些写操作慢,导致从节点的数据“落后”于主节点,即从节点上的数据版本比主节点旧。
-
网络延迟或带宽不足
-
主节点写入压力大,写命令过多
-
从节点性能不足(CPU / 内存 / IO 瓶颈)
-
大 Key 或批量操作导致复制阻塞
-
从节点开启了持久化(AOF / RDB),且配置不合理
-
复制积压缓冲区(Backlog Buffer)不足或被覆盖
-
从节点数量过多,主节点负载大
【必问】Redis哨兵模式是什么?
Redis Sentinel(哨兵) 是 Redis 官方提供的一种 高可用性(High Availability, HA)解决方案,它主要用于:
-
监控(Monitoring):持续监控 Redis 主节点(Master)和从节点(Slave/Replica)是否正常运行;
-
自动故障转移(Automatic Failover):当主节点发生故障时,自动将一个从节点升级为新的主节点,并让其他从节点复制新的主节点,同时通知客户端更新主节点地址;
-
配置中心(Configuration Provider):为客户端提供当前主节点的准确地址(哨兵可以充当轻量级的配置服务);
-
提醒(Notification):当 Redis 实例发生故障或发生切换时,可以通过 API 通知管理员或其他系统。
哨兵工作流程:
Sentinel 进程:它们本质上是独立的 Redis 客户端程序,以守护进程方式运行,负责监控 Redis 实例。
-
Sentinel 启动后,会与配置文件中指定的 主节点 + 从节点 建立连接,并定期发送 PING 命令检测其是否存活;
-
监控阶段:Sentinel 每隔一段时间向主从节点发送
PING,判断它们是否在线;如果某个节点在一定时间内未响应(主观下线),Sentinel 会进一步与其他 Sentinel 协商判断是否为客观下线; -
故障判定(客观下线,ODOWN):当足够数量的 Sentinel(一般超过 quorum 数量)都认为主节点不可用时,就认为主节点发生了客观下线,需要触发故障转移;
-
故障转移(Failover):Sentinel 集群会通过选举机制选出一个 Leader Sentinel,由它来负责执行故障转移操作;Leader Sentinel 会从现有的从节点中,选出一个合适的从节点提升为新的主节点;其他从节点会被重新配置为复制这个新的主节点;Sentinel 还会通知客户端主节点已经变更(通过发布订阅或者配置文件)。
-
恢复服务:新的主节点开始接收写请求,系统恢复正常。
哨兵模式主节点挂了如何选出新的主节点?
-
步骤 1:监控与主观下线(Subjectively Down, SDOWN)
-
每个 Sentinel 会定期向主节点发送
PING命令(默认每秒一次); -
如果主节点在指定时间内(
is-master-down-after-milliseconds,默认 30 秒)没有正确响应 PING(比如返回 PONG 或有效响应),则该 Sentinel 会认为这个主节点 主观下线(SDOWN);
-
-
步骤 2:客观下线(Objectively Down, ODOWN)
-
当一个 Sentinel 认为主节点主观下线后,它会向其他 Sentinel 发送询问,统计有多少 Sentinel 也认为该主节点不可用;
-
如果 超过预先配置的 quorum 数量(例如:有 3 个 Sentinel,quorum = 2) 的 Sentinel 都认为主节点不可用,则达成客观下线(ODOWN)的共识;
-
-
步骤 3:选举 Leader Sentinel(Sentinel 领导者选举)
-
当主节点被判定为客观下线后,多个 Sentinel 中需要选出一个 Leader(领头 Sentinel)来执行故障转移操作;
-
这个选举过程基于 Raft 算法的简化版(类似分布式共识机制);
-
-
步骤 4:从现有从节点中选出一个新的主节点(Failover)
-
Leader Sentinel 负责从当前所有的 从节点(Slaves/Replicas) 中,按照一定的规则选出一个“最合适”的从节点,将其提升为新的主节点。
-
-
步骤 5:执行故障转移(Failover 操作)
-
将选中的从节点提升为新的主节点:
-
发送命令让其停止复制旧主节点;
-
将其角色切换为 master;
-
清空其从节点身份,准备接收写请求;
-
-
让其他从节点改为复制新的主节点;
-
将旧的主节点(如果恢复后)设置为新主节点的从节点(可选,或者标记为故障节点);
-
通知客户端主节点已变更:
-
通过 Sentinel 的 发布订阅机制 或者让客户端定期查询 Sentinel 获取最新主节点地址;
-
应用程序通常会与 Sentinel 配合,实现自动发现主节点的功能;
-
-
【必问】Redis集群模式是什么?(一致性哈希、哈希环)
Redis 集群模式旨在解决 单机内存有限、单节点并发有限、单点故障风险 的问题,实现 “大数据量 + 高并发 + 高可用” 的 Redis 服务架构。
核心架构:
-
数据分片(Sharding):Redis Cluster 使用哈希槽(Hash Slot)机制
-
Redis 集群将整个数据空间划分为 16384 个哈希槽(hash slots),编号为
0 ~ 16383; -
每个键(Key)都会通过 CRC16 算法计算后,再对 16384 取模,决定它属于哪个槽;
-
每个 Redis 节点负责一部分哈希槽(比如节点 A 负责 0~5000,节点 B 负责 5001~10000,节点 C 负责 10001~16383);
-
当客户端访问某个 Key 时,Redis 会根据该 Key 所归属的槽,将请求路由到对应的节点上;
-
-
节点(Node)
-
Redis 集群由多个 Redis 节点(Node)组成,每个节点都是一个独立的 Redis 服务进程;
-
节点之间通过 Gossip 协议 相互通信,用于交换节点状态、槽分配信息、故障检测等;
-
每个节点可以是一个:
-
主节点(Master):负责处理读写请求,拥有部分哈希槽;
-
从节点(Slave/Replica):作为某个主节点的备份,用于故障转移,不直接处理客户端请求(除非主节点挂了);
-
-
-
客户端路由
-
Redis 集群支持 “智能客户端”(Smart Client),客户端可以缓存槽与节点的映射关系,直接连接目标节点,减少跳转;
-
如果客户端访问了一个错误的节点(比如 key 不属于该节点的槽),Redis 会返回一个重定向错误(MOVED 或 ASK),客户端根据提示重新连接正确的节点;
-
一些 Redis 客户端库(如 Jedis、Lettuce、redis-py-cluster)已经支持集群模式,能够自动处理这些重定向逻辑;
-
Redis集群的工作流程?
-
启动多个 Redis 实例(节点),通过
redis-cli --cluster create或手动方式组成集群,并分配哈希槽; -
每个主节点负责一部分哈希槽(比如 5461、5462、5461 等组合,总和为 16384);
-
客户端连接集群中任意节点,发起读写请求:
-
如果 key 的槽属于当前节点 → 直接处理;
-
如果 key 的槽不属于当前节点 → 返回 MOVED 错误,告知客户端正确的节点地址;
-
-
如果某个主节点宕机:
-
对应的从节点会被自动选举并提升为新的主节点(故障转移);
-
集群状态重新调整,继续提供服务;
-
-
如果从节点宕机:不影响整体服务,只是失去了一个备份;
Redis集群如何根据key定位到对应的节点?
-
客户端发送命令到某个 Redis 节点
-
节点计算 Key 的哈希槽
-
步骤 3:节点判断该 Slot 是否由自己管理
-
如果
slot = 1234是由当前节点负责的,那么该节点会直接执行这个命令(比如 SET 操作); -
如果 不是由当前节点管理,则 Redis 集群节点会返回一个 重定向错误 给客户端,
-
-
客户端根据重定向信息访问正确的节点
10.2.7 事务
Redis支持事务吗?如何实现?
Redis 提供了基本的事务功能,允许用户将一组命令打包在一起,按顺序一次性、原子性地执行,但不支持传统数据库中的回滚(Rollback)、隔离级别、多命令并发控制等高级事务特性。
Redis事务的执行流程?
当客户端使用 MULTI 命令后:
-
Redis 会将客户端状态切换为 “事务模式”。
-
之后客户端发送的所有命令,不会立即执行,而是被放入一个队列中(事务队列)。
-
当客户端发送
EXEC命令时:-
Redis 会一次性按顺序执行队列中的所有命令;
-
执行过程是原子的(不会被其他客户端的命令打断);
-
执行结果会按顺序返回给客户端。
-
-
如果发送的是
DISCARD,则清空队列,事务取消。
Redis事务WATCH命令与乐观锁机制?
Redis 提供了 WATCH 命令,用于实现一种称为 乐观锁(Optimistic Locking) 的机制,这是 Redis 事务中一个非常有用的功能,用来保证事务执行时某些关键数据没有被其他客户端修改。
WATCH 的作用:
-
在
MULTI之前,使用WATCH key1 key2...监视一个或多个 key; -
如果在 EXEC 执行之前,这些 key 中的任意一个被其他客户端修改了(比如被 SET、DEL 等),那么当前客户端的事务将执行失败(EXEC 返回 nil);
-
如果没有发生修改,则事务正常执行。
这是 Redis 提供的一种轻量级并发控制机制,非常适合实现乐观锁、CAS(Check-And-Set) 类型的操作。
Redis事务与关系型数据库事务的主要区别是?
-
如果某个命令执行出错(比如对 String 执行 LPUSH),其他命令仍然会继续执行,不会回滚。Redis 不提供回滚机制!这是与关系型数据库最核心的区别之一。
-
依赖命令正确,不保证业务强一致。
-
持久性依赖配置(RDB/AOF)。
10.2.8 优化
Redis中有哪些内存淘汰策略?
-
maxmemory:设置 Redis 最大可使用的内存大小(单位通常是字节)。如果不设置,默认是 0,表示不限制(但实际受系统内存约束)。生产环境强烈建议设置maxmemory!否则可能导致系统 OOM(内存耗尽)。 -
maxmemory-policy:当内存达到maxmemory限制时,Redis 应该采用哪种策略来淘汰数据。-
不淘汰策略
noeviction(禁止淘汰 / 报错):当内存不足时,新的写入操作会返回错误(比如 SET、LPUSH 等),但不会删除任何键。读取操作仍然可以正常进行。 -
淘汰策略
allkeys-lru:从所有键中(不管有没有设置过期时间),选择最近最少使用的 key 进行淘汰。最常用的策略之一,适合以缓存为主,希望保留热点数据的场景。 -
淘汰策略
allkeys-lfu:从所有 key 中,选择访问频率最低的 key 淘汰。Redis 4.0 后支持,适合全局基于访问频次做淘汰。
-
Redis中的内存碎片化是什么?
在 Redis 中,内存碎片化(Memory Fragmentation) 是一个常见且重要的性能与资源管理问题。它指的是:Redis 实际使用的内存总量比数据本身占用的内存更多,原因是内存分配不连续,产生了许多“空闲但无法利用的小块内存”,导致内存利用率下降。
可以通过 INFO memory 命令查看相关信息,重点关注如下字段:
-
mem_fragmentation_ratio:> 1.5:存在较多内存碎片
(追问)为什么会发生内存碎片化?
-
频繁修改、删除 key
-
内存分配器的行为:分配器为了性能,通常不会立即合并空闲内存块,而是保留它们供后续可能的分配使用,导致碎片积累
-
数据更新导致扩容/缩容
Redis中的内存碎片化如何进行优化?
-
使用 Redis 的内存碎片整理功能(Active Defragmentation)
-
重启 Redis 实例(冷启动)
-
避免频繁修改大 key 或大量小 key
Redis过期时间的底层实现是什么?
Redis 并不会在设置过期时间时立即启动一个定时器去精准删除 Key,因为那样会非常消耗 CPU。相反,Redis 采用了一套高效且兼顾性能与内存管理的“惰性删除 + 定期删除”的策略,并通过过期字典(Expires Dict)来管理过期时间。
Redis数据过期后的删除策略是什么?
在 Redis 中,可以通过 EXPIRE 命令为 key 设置过期时间。也可以通过 TTL key 命令查看一个 key 还有多少秒存活,返回 -2 表示 key 已过期并被删除,-1 表示没有设置过期时间。
当一个 key 设置了过期时间后,它并不会在过期的那个时间点被立即删除,而是由 Redis 通过一定的策略在后续某个时间进行删除。
-
惰性删除(Lazy Expiration / Passive Deletion)【主要方式】
-
Key 过期了不会马上删,而是在你访问这个 key 的时候,Redis 会先检查它是否已过期
-
如果发现 key 已经过期,则立即删除该 key,并返回 nil(表示 key 不存在)
-
如果没过期,则正常返回 key 的值
-
-
定期删除(Active Expiration / Periodic Deletion)【辅助方式】
-
Redis 内部会定期启动一个后台任务(定时器/循环),随机抽取一部分设置了过期时间的 key,检查它们是否过期
-
如果发现过期了,就将其删除
-
如何避免过期key堆积导致内存浪费?
-
合理设置 key 的过期时间
-
监控 Redis 的内存使用情况,配合 Redis 的内存淘汰策略(maxmemory + maxmemory-policy)
-
对于特别重要的缓存场景,可以手动清理或使用更精确的过期控制逻辑
如何保证Redis中的数据都是热点数据?
这个问题跟【内存淘汰策略】类似。
-
使用 LRU / LFU 淘汰策略,自动保留热点数据
-
设置合理的过期时间(TTL),避免数据长期滞留
-
在业务代码中,可以通过一些策略,主动控制哪些数据可以进入 Redis,哪些不应该缓存
-
使用本地缓存 + Redis 多级缓存,减轻 Redis 压力
Redis高频面试题解析
8万+

被折叠的 条评论
为什么被折叠?



