--- icon: logos:redis title: Redis 面试 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202503110803916.jpg date: 2020-07-13 17:03:42 categories: - 数据库 - KV数据库 - redis tags: - 数据库 - KV 数据库 - redis - 面试 permalink: /pages/ce594f11/ --- # Redis 面试 ## Redis 简介 ### 【简单】什么是 Redis? > 什么是 Redis? **Redis 是一个开源的、数据存于内存中的 K-V 数据库**。由于,Redis 的读写操作都是在内存中完成,因此其**读写速度非常快**。 - **高性能** - 由于,Redis 的读写操作都是在内存中完成,因此性能极高。 - **高并发** - Redis 单机 QPS 能达到 10w+,将近是 Mysql 的 10 倍。 Redis 常被用于**缓存,消息队列、分布式锁等场景**。 > Redis 有什么功能和特性? Redis 的功能和特性: - **Redis 支持多种数据类型**。如:String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理空间)、Stream(流)。 - **Redis 的读写采用“单线程”模型**,因此,其操作天然就具有**原子性**。需要注意的是,Redis 6.0 后在其网络模块中引入了多线程 I/O 机制。 - Redis 支持两种持久化策略:RDB 和 AOF。 - Redis 有多种高可用方案:**主从复制**模式、**哨兵**模式、**集群**模式。 - Redis 支持很多丰富的特性,如:**事务** 、**Lua 脚本**、**发布订阅**、**过期删除**、**内存淘汰**等等。 ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202411231010326.png) _图来自 [Redis Explained](https://architecturenotes.co/p/redis)_ ### 【简单】Redis 有哪些应用场景? Redis 常见应用场景如下: - **缓存** - 将热点数据放到内存中,设置内存的最大使用量以及过期淘汰策略来保证缓存的命中率。 - **计数器** - Redis 这种内存数据库能支持计数器频繁的读写操作。 - **应用限流** - 限制一个网站访问流量。 - **消息队列** - 使用 List 数据类型,它是双向链表。 - **查找表** - 使用 HASH 数据类型。 - **聚合运算** - 使用 SET 类型,例如求两个用户的共同好友。 - **排行榜** - 使用 ZSET 数据类型。 - **分布式 Session** - 多个应用服务器的 Session 都存储到 Redis 中来保证 Session 的一致性。 - **分布式锁** - 除了可以使用 SETNX 实现分布式锁之外,还可以使用官方提供的 RedLock 分布式锁实现。 ### 【简单】Redis 有哪些里程碑版本? Redis 里程碑版本如下: - Redis 1.0(2010 年) - Redis 1.0 发布,采用单机架构,一般作为业务应用的缓存。但是 Redis 的数据是存在内存中的,重启 Redis 时,数据会全部丢失,流量直接打到数据库。 - Redis 2.8(2013 年) - 持久化 - Redis 引入了 RDB 内存快照来持久化数据。它还支持 AOF(仅追加文件),其中每个写入命令都写入 AOF 文件。 - 复制 - 添加了复制功能以提高可用性。主实例处理实时读写请求,而副本同步主实例的数据。 - 哨兵 - 引入了 Sentinel 来实时监控 Redis 实例。Sentinel 是一个旨在帮助管理 Redis 实例的系统。它执行以下四个任务:监控、通知、自动故障转移和共享配置。 - Redis 3.0(2015 年) - 官方提供了 redis-cluster。redis-cluster 是一种分布式数据库解决方案,通过分片管理数据。数据被分成 16384 个槽,每个节点负责槽的一部分。 - Redis 5.0(2017 年) - 新增 Stream 数据类型。 - Redis 6.0(2020 年) - 在网络模块中引入了多线程 I/O。Redis 模型分为网络模块和主处理模块。特别注意:Redis 不再完全是单线程架构。 ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202503270820508.gif) ### 【简单】对比一下 Redis 和 Memcached? > - Redis 和 Memcached 有什么相同点? > - Redis 和 Memcached 有什么差异? > - 分布式缓存技术选型,选 Redis 还是 Memcached,为什么? Redis 与 Memcached 的**共性**: - 都是内存数据库,因此性能都很高。 - 都有过期策略。 因为以上两点,所以常被作为缓存使用。 Redis 与 Memcached 的**差异**: ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202504010716308.jpg) 核心差异对比: | | Memcached | Redis | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | | 数据类型 | 只支持 String 类型 | 支持多种数据类型:String、Hash、List、Set、ZSet 等 | | 持久化 | 不支持持久化,一旦重启或宕机就会丢失数据 | 支持两种持久化策略:RDB 和 AOF | | 分布式 | 本身不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点 | 支持分布式 | | 线程模型 | 采用多线程+IO 多路复用。在 100k 以上的数据中,Memcached 性能要高于 Redis | 读写采用单线程+IO 多路复用。因此存储小数据时比 Memcached 性能更高 | | 其他功能 | 不支持 | 支持发布订阅模型、Lua 脚本、事务等功能 | 通过以上分析,可以看出,Redis 在很多方面都占有优势。因此,绝大多数情况下,优先选择 Redis 作为分布式缓存。 > **扩展**:[《脚踏两只船的困惑 - Memcached 与 Redis》](https://www.imooc.com/article/23549) ### 【简单】Redis 有哪些 Java 客户端?各有什么优劣? Redis 的主流 Java 客户端有三种,对比如下: | 客户端 | 线程安全 | 自动重连 | 编程模型 | 适用场景 | | :----------- | :------- | :------- | :--------------- | :----------------------- | | **Jedis** | ❌ | ❌ | 同步 | 简单应用、快速开发 | | **Lettuce** | ✅ | ✅ | 同步/异步/响应式 | 高并发、Spring Boot 项目 | | **Redisson** | ✅ | ✅ | 同步/异步 | 分布式系统、高级功能需求 | **推荐选择**: - **基础需求** → Jedis(简单直接)。 - **高并发/Spring 项目** → Lettuce(默认选择)。 - **分布式锁/队列等** → Redisson(功能强大)。 #### Jedis **✅ 优点** - **简单易用**:API 直观,适合快速上手。 - **广泛使用**:社区支持丰富,文档齐全。 - **性能良好**:常规操作高效。 - **功能全面**:支持字符串、哈希、列表等基础数据结构。 **❌ 缺点** - **非线程安全**:需为每个线程创建独立实例。 - **无自动重连**:网络异常需手动处理。 - **同步阻塞**:高并发时可能成为性能瓶颈。 #### Lettuce **✅ 优点** - **线程安全**:多线程共享同一连接。 - **高性能**:基于 Netty 实现,支持高并发。 - **自动重连**:网络中断后自动恢复。 - **多编程模型**:支持同步、异步、响应式(如 Reactive API)。 **❌ 缺点** - **API 较复杂**:学习成本高于 Jedis。 - **资源消耗**:异步模式可能占用更多内存/CPU。 #### Redisson **✅ 优点** - **分布式支持**:内置分布式锁、队列、缓存等高级功能。 - **线程安全**:天然适配多线程场景。 - **集群友好**:完善支持 Redis 集群模式。 - **稳定性高**:企业级应用验证。 **❌ 缺点** - **学习曲线陡峭**:需掌握分布式概念。 - **依赖兼容性**:可能与其他库冲突需调优。 ## Redis 内存管理 ### 【中等】Redis 支持哪些过期删除策略? > - Redis 支持哪些过期删除策略? > - 常见的过期策略有哪些,Redis 的选择考量是什么? Redis 采用的过期策略是:**定期删除+惰性删除**。 - **定时删除** - 在设置 key 的过期时间的同时,创建一个定时器,让定时器在 key 的过期时间来临时,立即执行 key 的删除操作。 - **优点** - 保证过期 key 被尽可能快的删除,释放内存。 - **缺点** - **如果过期 key 较多,可能会占用相当一部分的 CPU,从而影响服务器的吞吐量和响应时延**。 - **惰性删除** - 放任 key 过期不管,但是每次访问 key 时,都检查 key 是否过期,如果过期的话,就删除该 key ;如果没有过期,就返回该 key。 - **优点** - 占用 CPU 最少。程序只会在读写键时,对当前键进行过期检查,因此不会有额外的 CPU 开销。 - **缺点** - **过期的 key 可能因为没有被访问,而一直无法释放,造成内存的浪费,有内存泄漏的风险**。 - **定期删除** - 每隔一段时间,程序就对数据库进行一次检查,删除里面的过期 key。至于要删除多少过期 key ,以及要检查多少个数据库,则由算法决定。定期删除是前两种策略的一种折中方案。定期删除策略的难点是删除操作执行的时长和频率。 - 执行太频或执行时间过长,就会出现和定时删除相同的问题; - 执行太少或执行时间过短,就会出现和惰性删除相同的问题; ### 【中等】Redis 有哪些内存淘汰策略? > - Redis 内存不足时,怎么办? > - Redis 有哪些内存淘汰策略? > - 如何选择内存淘汰策略? > **Redis 内存淘汰要点** - **失效时间** - 作为一种定期清理无效数据的重要机制,在 Redis 提供的诸多命令中,`EXPIRE`、`EXPIREAT`、`PEXPIRE`、`PEXPIREAT` 以及 `SETEX` 和 `PSETEX` 均可以用来设置一条键值对的失效时间。而一条键值对一旦被关联了失效时间就会在到期后自动删除(或者说变得无法访问更为准确)。 - **最大缓存** - Redis 允许通过 `maxmemory` 参数来设置内存最大值。当内存达设定的阀值,就会触发**内存淘汰**。 - **内存淘汰** - 内存淘汰是为了更好的利用内存——清理部分缓存,以此换取内存的利用率,即尽量保证 Redis 缓存中存储的是热点数据。 > **Redis 内存淘汰策略** - **不淘汰** - **`noeviction`** - 当内存使用达到阈值的时候,所有引起申请内存的命令会报错。这是 Redis 默认的策略。 - **在过期键中进行淘汰** - **`volatile-random`** - 在设置了过期时间的键空间中,随机移除某个 key。 - **`volatile-ttl`** - 在设置了过期时间的键空间中,具有更早过期时间的 key 优先移除。 - **`volatile-lru`** - 在设置了过期时间的键空间中,优先移除最近未使用的 key。 - **`volatile-lfu`** (Redis 4.0 新增)- 淘汰所有设置了过期时间的键值中,最少使用的键值。 - **在所有键中进行淘汰** - **`allkeys-random`** - 在主键空间中,随机移除某个 key。 - **`allkeys-lru`** - 在主键空间中,优先移除最近未使用的 key。 - **`allkeys-lfu`** (Redis 4.0 新增) - 淘汰整个键值中最少使用的键值。 > 如何选择内存淘汰策略 - 如果数据呈现正态分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 `allkeys-lru` 或 `allkeys-lfu`。 - 如果数据呈现平均分布,也就是所有的数据访问频率都相同,则使用 `allkeys-random`。 - 若 Redis 既用于缓存,也用于持久化存储时,适用 `volatile-lru` 、`volatile-lfu`、`volatile-random`。但是,这种情况下,也可以部署两个 Redis 集群来达到同样目的。 - 为 key 设置过期时间实际上会消耗更多的内存。因此,如果条件允许,建议使用 `allkeys-lru` 或 `allkeys-lfu`,从而更高效的使用内存。 ### 【中等】Redis 持久化时,对过期键会如何处理? **RDB 持久化** - **RDB 文件生成阶段** - 从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,**过期的键“不会”被保存到新的 RDB 文件中**,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。 - **RDB 加载阶段** - RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况: - **如果 Redis 是“主服务器”运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键“不会”被载入到数据库中**。所以过期键不会对载入 RDB 文件的主服务器造成影响; - **如果 Redis 是“从服务器”运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中**。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。 **AOF 持久化** - **AOF 文件写入阶段** - 当 Redis 以 AOF 模式持久化时,**如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值**。 - **AOF 重写阶段** - 执行 AOF 重写时,会对 Redis 中的键值对进行检查,**已过期的键不会被保存到重写后的 AOF 文件中**,因此不会对 AOF 重写造成任何影响。 ### 【中等】Redis 主从复制时,对过期键会如何处理? 当 Redis 运行在主从模式下时,**从库不会进行过期扫描,从库对过期的处理是被动的**。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。 从库的过期键处理依靠主服务器控制,**主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库**,从库通过执行这条 del 指令来删除过期的 key。 ### 【中等】Redis 中的内存碎片化是什么?如何进行优化? Redis 内存碎片化是指已分配的内存无法被有效利用,导致内存浪费的现象。 可以通过 Redis 的 `INFO memory` 命令查看: ```shell mem_fragmentation_ratio: 1.86 # 大于 1.5 表示碎片较多 ``` **内存碎片的原因**: - **内存分配器机制** - Redis 使用 Jemalloc 或 glibc 的 malloc 等内存分配器,这些分配器为了性能不会总是精确分配请求的大小,可能分配稍大的块。 - **键值对频繁修改** - 当键值对被频繁修改(特别是大小变化时),旧的内存空间可能无法重用。例如:字符串值从 128B 改为 256B,原空间不够需要新分配。 - **键过期/删除** - 删除键释放的内存块可能无法与相邻空闲块合并,这些碎片空间可能无法满足新的大内存请求。 - **不同大小的数据混合存储** - Redis 存储各种大小的键值对,导致内存中出现大小不一的空闲块。 **内存碎片的影响**: - **内存分配策略**:不同的分配器 (Jemalloc/libc 等)碎片率不同 - **工作负载模式**:频繁修改和删除操作会增加碎片 - **数据大小分布**:大小差异大的数据混合存储更容易产生碎片 **内存碎片的解决**: - 重启 Redis(会丢失数据) - 使用 `MEMORY PURGE` 命令(需要特定分配器支持) - 配置合理的 maxmemory 和淘汰策略 - 对于高碎片环境,可考虑使用 Redis 4.0+ 的主动碎片整理功能 ## Redis 持久化 ### 【中等】Redis 支持哪些持久化方式? 为了追求性能,Redis 的读写都是在内存中完成的。一旦重启,内存中的数据就会清空,为了保证数据不丢失,Redis 支持持久化机制。 Redis 有三种持久化方式 - RDB 快照 - AOF 日志 - 混合持久化 #### RDB > - RDB 的实现原理是什么? > - 生成 RDB 快照时,Redis 可以响应请求吗? 有两个 Redis 命令可以用于生成 RDB 文件:[**`SAVE`**](https://redis.io/commands/save) 和 [**`BGSAVE`**](https://redis.io/commands/bgsave)。 [**`SAVE`**](https://redis.io/commands/save) 命令由服务器进程直接执行保存操作,直到 RDB 创建完成为止。所以**该命令“会阻塞”服务器**,在阻塞期间,服务器不能响应任何命令请求。 [**`BGSAVE`**](https://redis.io/commands/bgsave) 命令会**派生**(fork)一个子进程,由子进程负责创建 RDB 文件,服务器进程继续处理命令请求,所以**该命令“不会阻塞”服务器**。 ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202503272238061.png) > 🔔 **【注意】** > > `BGSAVE` 命令的实现采用的是写时复制技术(Copy-On-Write,缩写为 CoW)。 > > `BGSAVE` 命令执行期间,`SAVE`、`BGSAVE`、`BGREWRITEAOF` 三个命令会被拒绝,以免与当前的 `BGSAVE` 操作产生竞态条件,降低性能。 #### AOF > - AOF 的实现原理是什么? > - 为什么先执行命令,再把数据写入日志呢? **Redis 命令请求会先保存到 AOF 缓冲区,再定期写入并同步到 AOF 文件**。 AOF 的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。 - **命令追加** - 当 Redis 服务器开启 AOF 功能时,服务器在执行完一个写命令后,会以 Redis 命令协议格式将被执行的写命令追加到 AOF 缓冲区的末尾。 - **文件写入**和**文件同步** - Redis 的服务器进程就是一个事件循环,这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复。而时间事件则负责执行想 `serverCron` 这样的定时运行的函数。 - 因为服务器在处理文件事件时可能会执行写命令,这些写命令会被追加到 AOF 缓冲区,服务器每次结束事件循环前,都会根据 `appendfsync` 选项来判断 AOF 缓冲区内容是否需要写入和同步到 AOF 文件中。 先执行命令,再把数据写入 AOF 日志有两个好处: - **避免额外的检查开销** - **不会阻塞当前写操作命令的执行** 当然,这样做也会有弊端: - **数据可能会丢失:** - **可能阻塞其他操作:** **Redis 命令请求会先保存到 AOF 缓冲区,再定期写入并同步到 AOF 文件**。 `appendfsync` 不同选项决定了不同的持久化行为: - **`always`** - 将 AOF 缓冲区中所有内容写入并同步到 AOF 文件。这种方式是最数据最安全的,但也是性能最差的。 - **`no`** - 将 AOF 缓冲区所有内容写入到 AOF 文件,但并不对 AOF 文件进行同步,何时同步由操作系统决定。这种方式是数据最不安全的,一旦出现故障,未来得及同步的所有数据都会丢失。 - **`everysec`** - `appendfsync` 默认选项。将 AOF 缓冲区所有内容写入到 AOF 文件,如果上次同步 AOF 文件的时间距离现在超过一秒钟,那么再次对 AOF 文件进行同步,这个同步操作是有一个线程专门负责执行的。这张方式是前面两种的这种方案——性能足够好,且即使出现故障,仅丢失一秒钟内的数据。 `appendfsync` 选项的不同值对 AOF 持久化功能的安全性、以及 Redis 服务器的性能有很大的影响。 #### 混合持久化 Redis 4.0 提出了**混合使用 AOF 日志和内存快照**,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。 **混合持久化的工作机制** - **触发时机**:在 AOF 重写过程中启用。 - **执行流程**: 1. **子进程**将共享内存数据以 **RDB 格式**写入 AOF 文件(全量数据)。 2. **主线程**将操作命令记录到重写缓冲区,再以 **AOF 格式**追加到 AOF 文件(增量数据)。 3. 替换旧 AOF 文件,新文件包含 **RDB(前半部分) + AOF(后半部分)**。 **混合持久化的优点** - **重启速度快**:优先加载 RDB 部分(全量数据恢复快)。 - **数据丢失少**:后续加载 AOF 部分(增量数据补充)。 **混合持久化的缺点** - **可读性差**:AOF 文件包含二进制 RDB 数据,不易阅读。 - **兼容性差**:仅支持 Redis 4.0+ 版本,旧版本无法识别。 ### 【中等】AOF 的重写机制是怎样的? - AOF 日志过大时,怎么办? - AOF 重写流程是怎样的? - AOF 重写时,可以处理请求吗? **知识点** 当 AOF 日志过大时,恢复过程就会很久。为了避免此问题,Redis 提供了 AOF 重写机制,即 AOF 日志大小超过所设阈值后,启动 AOF 重写,压缩 AOF 文件。 AOF 重写机制是,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到新的 AOF 日志中,等到全部记录完成后,就使用新的 AOF 日志替换现有的 AOF 日志。 作为一种辅助性功能,显然 Redis 并不想在 AOF 重写时阻塞 Redis 服务接收其他命令。因此,Redis 决定通过 `BGREWRITEAOF` 命令创建一个子进程,然后由子进程负责对 AOF 文件进行重写,这与 `BGSAVE` 原理类似。 - 在执行 `BGREWRITEAOF` 命令时,Redis 服务器会维护一个 AOF 重写缓冲区。当 AOF 重写子进程开始工作后,Redis 每执行完一个写命令,会同时将这个命令发送给 AOF 缓冲区和 AOF 重写缓冲区。 - 由于彼此不是在同一个进程中工作,AOF 重写不影响 AOF 写入和同步。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。 - 最后,服务器用新的 AOF 文件替换就的 AOF 文件,以此来完成 AOF 重写操作。 ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202503272248959.png) ## Redis 批处理 ### 【中等】Redis 支持事务吗? ::: info Redis 支持事务吗? ::: Redis 支持事务。[`MULTI`](https://redis.io/commands/multi)、[`EXEC`](https://redis.io/commands/exec)、[`DISCARD`](https://redis.io/commands/discard) 和 [`WATCH`](https://redis.io/commands/watch) 是 Redis 事务相关的命令。 **[`MULTI`](https://redis.io/commands/multi) 命令用于开启一个事务,它总是返回 OK。**`MULTI` 执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当 EXEC 命令被调用时,所有队列中的命令才会被执行。 **[`EXEC`](https://redis.io/commands/exec) 命令负责触发并执行事务中的所有命令。** - 如果客户端在使用 `MULTI` 开启了一个事务之后,却因为断线而没有成功执行 `EXEC` ,那么事务中的所有命令都不会被执行。 - 另一方面,如果客户端成功在开启事务之后执行 `EXEC` ,那么事务中的所有命令都会被执行。 **当执行 [`DISCARD`](https://redis.io/commands/discard) 命令时,事务会被放弃,事务队列会被清空,并且客户端会从事务状态中退出。** **[`WATCH`](https://redis.io/commands/watch) 命令可以为 Redis 事务提供 check-and-set (CAS)行为**。被 `WATCH` 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 `EXEC` 执行之前被修改了,那么整个事务都会被取消,`EXEC` 返回 `nil-reply` 来表示事务已经失败。 `WATCH` 可以用于创建 Redis 没有内置的原子操作。 举个例子,以下代码实现了原创的 `ZPOP` 命令,它可以原子地弹出有序集合中分值(`score`)最小的元素: ```shell WATCH zset element = ZRANGE zset 0 0 MULTI ZREM zset element EXEC ``` ::: info Redis 事务是严格意义的事务吗? ::: ACID 是数据库事务正确执行的四个基本要素。 - **原子性(Atomicity)** - 事务被视为不可分割的最小单元,事务中的所有操作**要么全部提交成功,要么全部失败回滚**。 - 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。 - **一致性(Consistency)** - 数据库在事务执行前后都保持一致性状态。 - 在一致性状态下,所有事务对一个数据的读取结果都是相同的。 - **隔离性(Isolation)** - 一个事务所做的修改在最终提交以前,对其它事务是不可见的。 - **持久性(Durability)** - 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。 - 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。 **一个支持事务(Transaction)中的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性。** **Redis 仅支持“非严格”的事务**。所谓“非严格”是指: - **Redis 事务保证全部执行命令** - Redis 事务中的多个命令会被打包到事务队列中,然后按先进先出(FIFO)的顺序执行。事务在执行过程中不会被中断,当事务队列中的所有命令都被执行完毕之后,事务才会结束。 - **Redis 事务不支持回滚** - 如果命令执行失败不会回滚,而是会继续执行下去。 Redis 官方的 [事务特性文档](https://redis.io/docs/interact/transactions/) 给出的不支持回滚的理由是: - Redis 命令只会因为错误的语法而失败,或是命令用在了错误类型的键上面。 - 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。 ### 【中等】Redis Pipeline 能保证原子性吗? 先说结论:**Redis Pipeline 不保证原子性**。 Redis Pipeline(管道)是一种**客户端技术**,用于将多个 Redis 命令批量发送到服务器,减少网络往返时间(RTT),提高吞吐量。 - **传统模式**:客户端发送一条命令 → 等待响应 → 再发送下一条(高延迟)。 - **Pipeline 模式**:客户端一次性发送多条命令 → 服务器按顺序执行 → 一次性返回所有结果(低延迟)。 ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202503272224006.jpg) **核心特性** | **特性** | **说明** | | :----------- | :------------------------------------------------------------------------ | | **批量发送** | 客户端打包多条命令,一次性发送,减少网络开销 | | **非原子性** | Pipeline 只是批量发送,不保证所有命令连续执行(可能被其他客户端命令打断) | | **高性能** | 相比单条命令模式,吞吐量可提升 5~10 倍 | | **无回滚** | 如果某条命令失败,不会影响其他命令的执行 | **注意事项** - **命令数量控制**:避免单次 Pipeline 发送过多命令(建议每批 ≤ 1 万条),否则可能阻塞 Redis。 - **集群模式限制**:Redis Cluster 要求 Pipeline 的所有 Key 必须在**同一个 Slot**(可用 `{hash_tag}` 确保,如 `user:{123}:name`)。 - **错误处理**:Pipeline 返回的是一个列表,需逐条检查命令是否成功。 - **与事务的区别**:Pipeline 不保证原子性。 **适用场景** ✅ **适合**: - 批量写入(如日志上报、缓存预热) - 批量查询(如获取多个 Key 的值) - 对原子性无要求的高并发场景 ❌ **不适合**: - 需要事务保证原子性的操作(改用 `MULTI/EXEC`) - 命令之间有依赖关系(如后一条命令依赖前一条的结果) ### 【中等】Redis Lua 脚本有什么用? Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。Redis 的 Lua 脚本提供了一种**原子性执行多个命令**的方式。也就是说,一段 **Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行**,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。 并且,Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理,这同样是 pipeline 所不具备的。 不过, Lua 脚本依然存在下面这些缺陷: - **如果 Lua 脚本运行时出错并中途结束,之后的操作不会进行,但是之前已经发生的写操作不会撤销**,所以即使使用了 Lua 脚本,也不能实现类似数据库回滚的原子性。 - **Redis Cluster 下 Lua 脚本的原子操作也无法保证**,原因同样是无法保证所有的 key 都在同一个 hash slot(哈希槽)上。 另外,Redis 7.0 新增了 [Redis functions](https://redis.io/docs/manual/programmability/functions-intro/) 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。 ## Redis 高可用 ### 【中等】Redis 如何实现主从复制? > - Redis 复制的工作原理?Redis 旧版复制和新版复制有何不同? > - Redis 主从节点间如何复制数据? > - Redis 的数据一致性是强一致性吗? (1)旧版复制基于 `SYNC` 命令实现。分为同步(sync)和命令传播(command propagate)两个操作。这种方式存在缺陷:不能高效处理断线重连后的复制情况。 (2)新版复制基于 `PSYNC` 命令实现。同步操作分为了两块: - **`完整重同步(full resychronization)`** 用于初次复制; - **`部分重同步(partial resychronization)`** 用于断线后重复制。 - 主从服务器的**复制偏移量(replication offset)** - 主服务器的**复制积压缓冲区(replication backlog)** - **服务器的运行 ID** (3)Redis 集群主从节点复制的工作流程: 1. 设置主从服务器 2. 主从服务器建立 TCP 连接。 3. 发送 PING 检查通信状态。 4. 身份验证。 5. 发送端口信息。 6. 同步。 7. 命令传播。 (4)由于主从复制是**异步**的,具体来说,在主从服务器命令传播阶段,主服务器收到新的写命令后,会发送给从服务器。但是,主服务器并不会等到从服务器实际执行完命令后,再把结果返回给客户端,而是主服务器自己在本地执行完命令后,就会向客户端返回结果了。如果从服务器还没有执行主服务器同步过来的命令,主从服务器间的数据就不一致了。所以,无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的。 ### 【中等】Redis 哨兵是如何工作的? > - Redis 哨兵的功能? > - Redis 哨兵的原理? > - Redis 哨兵如何选举 Leader? > - Redis 如何实现故障转移? (1)Redis 主从复制模式无法自动故障转移,也就是说,一旦主服务器宕机,需要手动恢复。为了解决此问题,Redis 增加了哨兵模式(Sentinel)。 (2)由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及这些主服务器的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。 ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202503282134261.png) ### 【中等】Redis 集群是如何工作的? **集群架构** - **节点类型**: - 主节点 (master):负责处理槽 - 从节点 (slave):复制主节点,主节点下线时可接管 - **节点通信**:使用 Gossip 协议 (PING/PONG/MEET/FAIL/PUBLISH 消息) - **数据分区**: - 哈希槽分配:CRC16(key) mod 16384 - 槽指派命令:CLUSTER ADDSLOTS - 所有槽必须分配,否则集群下线 **关键机制** - **请求路由**: - 计算 key 所属槽 - MOVED 错误重定向(永久重定向) - ASK 错误重定向(临时重定向,迁移过程中使用) - **故障转移**: - 故障检测:PING 消息+半数以上主节点确认为 FAIL 状态 - 选主流程:基于 Raft 协议,需要获得 N/2+1 票 - 转移步骤:从节点升级为主节点并接管槽 - **重新分区**: - 在线迁移槽和键值对 - 使用 redis-trib 工具操作 **使用限制** - **功能限制**: - 批量操作/事务/Pipeline/lua 仅支持相同 slot 的 key - 不支持多数据库(仅 db0) - 复制结构只支持一层 - **规模限制**: - 适合中小规模集群(几个到几十个节点) - Gossip 协议在大规模集群中传播效率低 ### 【困难】Redis 中的脑裂问题是如何产生的? 在 Redis 主从架构中,部署方式一般是“一主多从”,主节点提供写操作,从节点提供读操作。 如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道 Redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据(过程 A),此时这些数据被旧主节点缓存到了缓冲区里,因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。 这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在“从节点”中选举出一个 leader 作为主节点,这时集群就有两个主节点了 —— **脑裂出现了**。 然后,网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A),然后从节点(A)会向新主节点请求数据同步,**因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题**。 总结一句话就是:由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。 分布式系统的脑裂问题(Split-Brain Problem)是一个严重的一致性问题,通常发生在分布式系统中的节点之间失去通信或部分通信时。这个问题的名称源自脑裂的比喻,就像一个分布式系统被分成多个部分的"脑",每个部分独立运行,而没有协调一致的方式。 脑裂问题通常发生在以下情况下: 1. **网络分区**:当分布式系统中的网络发生问题,导致节点之间无法互相通信或只能部分通信时。这可能是由于网络故障、硬件故障、防火墙配置问题等原因引起的。 2. **节点故障**:当分布式系统的某个节点崩溃或出现故障,但其他节点无法确定该节点的状态,可能导致脑裂问题。 脑裂问题的典型情况是,在网络分区或节点故障后,分布式系统的一部分节点认为另一部分节点已经不可用,因此开始采取某种措施,比如选举新的领袖或切换到备份模式。然而,在某些情况下,网络分区可能会解除,或者节点故障可能会自行修复,导致系统中存在多个独立运行的子系统,每个子系统都认为自己是正确的。 这种情况下,脑裂问题可能导致以下问题: 1. **数据不一致性**:不同子系统可能具有不同的数据状态,这可能会导致数据不一致性和冲突。 2. **资源冲突**:如果不同的子系统尝试访问相同的资源,可能会发生资源冲突和竞争条件。 3. **性能问题**:系统中的资源可能被多次分配,从而浪费了资源并降低了性能。 为了解决脑裂问题,分布式系统通常需要采用一些机制,如投票算法、选举协议、心跳检测等,以确保在出现网络分区或节点故障时,系统能够正确地识别和处理问题,并维护一致性。这些机制可以帮助系统中的节点协同工作,避免脑裂问题的发生。然而,脑裂问题是分布式系统设计和管理中的复杂挑战之一,需要细致的规划和测试来确保系统的可靠性和稳定性。 ### 【困难】如何解决 Redis 中的脑裂问题? 当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。 在 Redis 的配置文件中有两个参数我们可以设置: - `min-slaves-to-write x`,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。 - `min-slaves-max-lag x`,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。 我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。 这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的写请求了。 即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slaves-max-lag 的组合要求就无法得到满足,**原主库就会被限制接收客户端写请求,客户端也就不能在原主库中写入新数据了**。 **等到新主库上线时,就只有新主库能接收和处理客户端请求,此时,新写的数据会被直接写到新主库中。而原主库会被哨兵降为从库,即使它的数据被清空了,也不会有新数据丢失。** 再来举个例子。 假设我们将 min-slaves-to-write 设置为 1,把 min-slaves-max-lag 设置为 12s,把哨兵的 down-after-milliseconds 设置为 10s,主库因为某些原因卡住了 15s,导致哨兵判断主库客观下线,开始进行主从切换。 同时,因为原主库卡住了 15s,没有一个从库能和原主库在 12s 内进行数据复制,原主库也无法接收客户端请求了。 这样一来,主从切换完成后,也只有新主库能接收请求,不会发生脑裂,也就不会发生数据丢失的问题了。 ## Redis 架构 ### 【中等】Redis 为什么快? 根据 [Redis 官方 Benchmark](https://redis.io/docs/management/optimization/benchmarks/) 文档的描述,Redis 单机 QPS 能达到 10w+,将近是 Mysql 的 10 倍。 ![Redis 官方 Benchmark QPS 图](https://raw.githubusercontent.com/dunwu/images/master/snap/202503270821660.png) Redis 是单线程模型(Redis 6.0 已经支持多线程模型),为什么还能有这么高的并发? - **Redis 读写基于内存** - **IO 多路复用** + **读写单线程模型** - IO 多路复用是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。 - 单线程模型避免了由于并发而产生的线程切换、锁竞争等开销。 - 由于,Redis 读写基于内存,性能很高,所以 CPU 并不是制约 Redis 性能表现的瓶颈所在。更多情况下是受到内存大小和网络 I/O 的限制,所以 Redis 核心网络模型使用单线程并没有什么问题。 - **高效的数据结构** ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202503272229177.jpg) 图来自 [Why is redis so fast?](https://blog.bytebytego.com/p/why-is-redis-so-fast) > **扩展**:[【视频】Why is Redis so FAST](https://www.youtube.com/shorts/x8lcdDbKZto) ### 【中等】Redis 单线程模式是怎样的? Redis 单线程模式指的是其核心网络模型为单线程模式。这个模式为 IO 多路复用+单线程读写请求,其中,IO 多路复用使得 Redis 可以同时处理多个客户端连接。 ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202309241133046.png) > Redis 真的只有单线程吗? Redis 并非真的只有单线。 - Redis 的主要工作包括接收客户端请求、解析请求和进行数据读写等操作,是由单线程来执行的,这也是常说 Redis 是单线程程序的原因。 - Redis 还启动了 3 个线程来执行**文件关闭**、**AOF 同步写**和**惰性删除**等操作。 - 此外,Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。 ### 【中等】Redis 6.0 之后为什么引入了多线程? 随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说,**单个主线程处理网络请求的速度跟不上底层网络硬件的速度。** 为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是,对于命令的执行,Redis 仍然使用单线程来处理。 Redis 官方表示,**Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上**。 ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202309241148273.png) ### 【中等】什么是 Redis 模块?有什么用? Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特殊的需求。这些 Module 以动态链接库(so 文件)的形式被加载到 Redis 中,这是一种非常灵活的动态扩展功能的实现方式,值得借鉴学习! 我们每个人都可以基于 Redis 去定制化开发自己的 Module,比如实现搜索引擎功能、自定义分布式锁和分布式限流。 目前,被 Redis 官方推荐的 Module 有: - [RediSearch](https://github.com/RediSearch/RediSearch):用于实现搜索引擎的模块。 - [RedisJSON](https://github.com/RedisJSON/RedisJSON):用于处理 JSON 数据的模块。 - [RedisGraph](https://github.com/RedisGraph/RedisGraph):用于实现图形数据库的模块。 - [RedisTimeSeries](https://github.com/RedisTimeSeries/RedisTimeSeries):用于处理时间序列数据的模块。 - [RedisBloom](https://github.com/RedisBloom/RedisBloom):用于实现布隆过滤器的模块。 - [RedisAI](https://github.com/RedisAI/RedisAI):用于执行深度学习/机器学习模型并管理其数据的模块。 - [RedisCell](https://github.com/brandur/redis-cell):用于实现分布式限流的模块。 关于 Redis 模块的详细介绍,可以查看官方文档:https://redis.io/modules。 ### 【困难】Redis 有哪些巧妙的设计? Redis 的巧妙设计体现在: - **单线程 + 非阻塞 I/O** → 高吞吐、低延迟。 - **精细优化的数据结构** → 节省内存,提升访问速度。 - **异步化处理**(持久化、删除、复制)→ 减少主线程阻塞。 - **扩展性设计**(模块化、集群)→ 适应不同场景需求。 Redis 作为高性能的内存数据库,有许多巧妙的设计理念和实现细节,使其在性能、简洁性和功能性之间取得平衡。以下是 Redis 的一些巧妙设计: #### 单线程模型(核心命令处理) Redis 采用**单线程处理命令**(6.0+ 后支持多线程 I/O,但核心逻辑仍单线程),避免了锁竞争和上下文切换的开销,同时利用以下优化: - **非阻塞 I/O**:基于 `epoll/kqueue` 实现高效事件驱动模型。 - **纯内存操作**:绝大多数操作在内存中完成,单线程即可高效处理。 - **原子性保证**:单线程天然支持原子操作,简化了事务、Lua 脚本等实现。 **巧妙点**:牺牲多线程并行性换取无锁设计的简单性和高性能。 #### 高效数据结构实现 Redis 的核心数据结构经过高度优化: - **SDS (Simple Dynamic String)**: - 预分配内存、惰性释放,减少内存重分配。 - 二进制安全(可存储任意数据,不像 C 字符串以 `\0` 结尾)。 - **压缩列表 (ziplist)**: - 对小数据(如短列表、小哈希)使用紧凑存储,节省内存。 - **快速列表 (quicklist)**: - 结合 `ziplist` 和双向链表,优化 `List` 的内存和访问效率。 - **跳跃表 (skiplist)**: - 实现 `ZSET`,支持 `O(logN)` 范围查询和高效插入。 - **渐进式 Rehash**: - `Hash` 扩容时不阻塞服务,分批次迁移数据。 **巧妙点**:针对不同场景选择最优底层结构,平衡内存和速度。 #### 异步持久化 Redis 提供两种持久化方式: - **RDB (快照)**: - `fork()` 子进程生成快照,主进程继续服务。 - 使用 `Copy-On-Write (COW)` 机制减少内存开销。 - **AOF (日志追加)**: - 先执行命令再记录日志,避免日志错误影响数据。 - 支持 `AOF Rewrite` 压缩日志(类似 RDB 的快照逻辑)。 **巧妙点**:通过 `fork()` + `COW` 实现后台持久化,避免阻塞主线程。 #### 多路复用+零拷贝 - **I/O 多路复用**: - 使用 `epoll/kqueue` 监听大量连接,避免线程/进程切换。 - **零拷贝优化**: - 网络发送数据时,直接引用内存缓冲区,减少拷贝(如 `sendfile`)。 **巧妙点**:最大化利用系统调用,减少 CPU 和内存开销。 #### 惰性删除 (Lazy Free) - **DEL 命令不立即释放内存**,而是异步回收(避免大 Key 删除卡住主线程)。 - 适用于 `UNLINK`、`FLUSHDB ASYNC` 等场景。 **巧妙点**:用空间换时间,避免同步删除导致服务延迟。 #### 过期键的混合淘汰策略 - **定期删除**:随机抽查部分 Key,清理已过期的。 - **惰性删除**:访问 Key 时检查是否过期,再决定删除。 **巧妙点**:平衡 CPU 和内存,避免全局扫描影响性能。 #### 模块化设计 (Redis Modules) - 支持动态加载模块(如 `RedisSearch`、`RedisGraph`),扩展功能而不改核心代码。 **巧妙点**:保持核心精简,通过插件机制扩展能力。 #### 集群分片的无中心化设计 - **Gossip 协议**:节点间自动发现和状态同步。 - **哈希槽 (Hash Slot)**:数据分片到 16384 个槽,而非一致性哈希,简化迁移。 **巧妙点**:去中心化设计,避免单点瓶颈,支持动态扩缩容。 ## Redis 优化 ### 【中等】为什么会有慢查询命令? 一个 Redis 命令的执行可以简化为以下 4 步: 1. 发送命令 2. 命令排队 3. 命令执行 4. 返回结果 Redis 慢查询统计的是命令执行这一步骤的耗时,慢查询命令也就是那些命令执行时间较长的命令。 Redis 为什么会有慢查询命令呢? Redis 中的大部分命令都是 O(1) 时间复杂度,但也有少部分 O(n) 时间复杂度的命令,例如: - `KEYS *`:会返回所有符合规则的 key。 - `HGETALL`:会返回一个 Hash 中所有的键值对。 - `LRANGE`:会返回 List 中指定范围内的元素。 - `SMEMBERS`:返回 Set 中的所有元素。 - `SINTER`/`SUNION`/`SDIFF`:计算多个 Set 的交集/并集/差集。 - …… 由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长。不过,这些命令并不是一定不能使用,但是需要明确 N 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。 除了这些 O(n) 时间复杂度的命令可能会导致慢查询之外,还有一些时间复杂度可能在 O(N) 以上的命令,例如: - `ZRANGE`/`ZREVRANGE`:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量,m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 - `ZREMRANGEBYRANK`/`ZREMRANGEBYSCORE`:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量,m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 - …… ### 【中等】如何找到慢查询命令? 在 `redis.conf` 文件中,我们可以使用 `slowlog-log-slower-than` 参数设置耗时命令的阈值,并使用 `slowlog-max-len` 参数设置耗时命令的最大记录条数。 当 Redis 服务器检测到执行时间超过 `slowlog-log-slower-than`阈值的命令时,就会将该命令记录在慢查询日志 (slow log) 中,这点和 MySQL 记录慢查询语句类似。当慢查询日志超过设定的最大记录条数之后,Redis 会把最早的执行命令依次舍弃。 ⚠️注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。 `slowlog-log-slower-than`和`slowlog-max-len`的默认配置如下(可以自行修改): ```shell # The following time is expressed in microseconds, so 1000000 is equivalent # to one second. Note that a negative number disables the slow log, while # a value of zero forces the logging of every command. slowlog-log-slower-than 10000 # There is no limit to this length. Just be aware that it will consume memory. # You can reclaim memory used by the slow log with SLOWLOG RESET. slowlog-max-len 128 ``` 除了修改配置文件之外,你也可以直接通过 `CONFIG` 命令直接设置: ```shell # 命令执行耗时超过 10000 微妙(即 10 毫秒)就会被记录 CONFIG SET slowlog-log-slower-than 10000 # 只保留最近 128 条耗时命令 CONFIG SET slowlog-max-len 128 ``` 获取慢查询日志的内容很简单,直接使用`SLOWLOG GET` 命令即可。 ```shell 127.0.0.1:6379> SLOWLOG GET #慢日志查询 1) 1) (integer) 5 2) (integer) 1684326682 3) (integer) 12000 4) 1) "KEYS" 2) "*" 5) "172.17.0.1:61152" 6) "" // ... ``` 慢查询日志中的每个条目都由以下六个值组成: 1. 唯一渐进的日志标识符。 2. 处理记录命令的 Unix 时间戳。 3. 执行所需的时间量,以微秒为单位。 4. 组成命令参数的数组。 5. 客户端 IP 地址和端口。 6. 客户端名称。 `SLOWLOG GET` 命令默认返回最近 10 条的的慢查询命令,你也自己可以指定返回的慢查询命令的数量 `SLOWLOG GET N`。 下面是其他比较常用的慢查询相关的命令: ```shell # 返回慢查询命令的数量 127.0.0.1:6379> SLOWLOG LEN (integer) 128 # 清空慢查询命令 127.0.0.1:6379> SLOWLOG RESET OK ``` ### 【中等】Redis 中的 Big Key 问题是什么?如何解决? > 什么是 Redis Big Key? Big Key 并不是指 key 的值很大,而是 key 对应的 value 很大。 一般而言,下面这两种情况被称为 Big Key: - String 类型的值大于 10 KB; - Hash、List、Set、ZSet 类型的元素的个数超过 5000 个,或总大小超过 10MB > Big Key 会造成什么问题? Big Key 会带来以下四种影响: - **内存分布不均**:集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有 Big Key 的 Redis 节点占用内存多,QPS 也会比较大。 - **命令阻塞**:Redis 单线程模型,操作大 Key 耗时,阻塞其他命令。 - **网络传输压力**:每次获取 Big Key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。 - **客户端超时**:由于 Redis 执行命令是单线程处理,然后在操作 Big Key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。 **解决方案** **(1)开发优化** - **数据压缩**:存储前压缩 Value(如 Gzip、Snappy)。 - **拆分大对象**:将单个大 Key 拆分为多个小 Key(如 `user:1000:info` → `user:1000:basic` + `user:1000:details`)。 - **优化数据结构**: - 避免巨型 String,改用 Hash、List 等分片存储。 - 使用 `ziplist`、`quicklist` 等紧凑结构。 **(2)业务调整** - **精简存储数据**:仅保留高频访问字段(如不存用户全部信息,只存 ID + 核心字段)。 - **逻辑优化**:避免业务层生成大 Key(如限制缓存数据大小、分页查询)。 **(3)数据分布优化** - **集群分片**:通过 Redis Cluster 分散大 Key 到不同节点。 - **本地缓存**:对冷数据使用本地缓存(如 Caffeine),减少 Redis 压力。 **关键点** - **预防优于治理**:在设计和开发阶段规避大 Key。 - **监控与巡检**:通过 `redis-cli --bigkeys` 或自定义脚本定期检测大 Key。 > **如何查找 Redis Big Key?** **(1)使用 `redis-cli --bigkeys`** **命令**: ```bash redis-cli -h 127.0.0.1 -p 6379 -a "password" --bigkeys ``` **注意事项**: - **推荐在从节点执行**(主节点执行可能阻塞业务) - **低峰期执行** 或 **加 `-i` 参数控制扫描间隔**(如 `-i 0.1` 表示每 100ms 扫描一次) **缺点**: - 只能返回**每种数据类型最大的 1 个 Key**(无法获取 Top N) - 对集合类型**只统计元素个数**,而非实际内存占用 **(2)使用 `SCAN` + 内存分析命令** **遍历所有 Key**(避免 `KEYS *` 阻塞 Redis): ```bash redis-cli --scan --pattern "*" | while read key; do ...; done ``` **分析 Key 大小**: - **String**:`STRLEN $key`(字节数) - **集合类型**(List/Hash/Set/ZSet): - **方法 1**:`LLEN`/`HLEN`/`SCARD`/`ZCARD`(元素个数 × 预估元素大小) - **方法 2**(Redis 4.0+):`MEMORY USAGE $key`(精确内存占用) **优点**: - 可自定义筛选条件(如大小 Top 10) - 精确计算内存占用 **(3)使用 `RdbTools` 分析 RDB 文件** **命令**: ```bash rdb dump.rdb -c memory --bytes 10240 -f redis.csv # 导出 >10KB 的 Key 到 CSV ``` **适用场景**: - 离线分析,不影响线上 Redis - 精准统计**所有 Key 的内存分布** **缺点**:需要 Redis 生成 RDB 快照 **总结:3 种方法对比** | 方法 | 适用场景 | 优点 | 缺点 | | --------------- | ---------------- | ---------- | ------------- | | **`--bigkeys`** | 快速找出最大 Key | 简单易用 | 结果不全面 | | **`SCAN`+命令** | 精确分析内存 | 可定制化 | 需脚本支持 | | **`RdbTools`** | 离线全面分析 | 精准无遗漏 | 依赖 RDB 文件 | **推荐组合**: - 日常监控用 `--bigkeys`(低峰期执行) - 深度分析用 `RdbTools`(定期检查 RDB) - 排查问题时用 `SCAN`+`MEMORY USAGE`(实时精准定位) ### 【中等】如何解决 Redis 中的热点 key 问题? 解决 Redis 中的热点 key 问题的方法: - **热点 Key 拆分** - **垂直分片**:`user:123` → `user:123:base` + `user:123:detail` - **水平分片**:`product:views` → `product:views:shard1`/`shard2` - **多级缓存**:CDN → 本地缓存(Caffeine/Guava) → Redis → DB - **读写分离**:读请求分流到从节点(配置 `replica-read-only yes`) - **流量控制**: - **Sentinel / Hystrix 等流控中间件** - **Redis + Lua 限流** ```lua -- 示例:每秒限 100 次访问 local count = redis.call('INCR', KEYS[1]) if count == 1 then redis.call('EXPIRE', KEYS[1], 1) end return count <= 100 ``` - **数据预热**:定时任务提前加载热点数据到缓存 - **负载均衡**: - **Redis Cluster**:分散热点 Key 到不同节点 - **代理层**:Twemproxy / Redis Proxy / Nginx 实现负载均衡 ## 参考资料 - [面试中关于 Redis 的问题看这篇就够了](https://juejin.im/post/5ad6e4066fb9a028d82c4b66) - [advanced-java](https://github.com/doocs/advanced-java#缓存) - [Redis 常见面试题](https://xiaolincoding.com/redis/base/redis_interview.html)