 Redis
Redis
  # Redis缓存数据库
# 什么是 Redis?
Redis (opens new window) (REmote DIctionary Server)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。与传统数据库不同的是,Redis 的数据是保存在内存中的(内存数据库,支持持久化),因此读写速度非常快,被广泛应用于分布式缓存方向。并且,Redis 存储的是 KV 键值对数据。
# 为什么使用 Redis?
Redis(Remote Dictionary Server)是一个高性能的开源内存数据结构存储系统,常被用作数据库、缓存和消息代理。以下是使用 Redis 的主要原因:
# 1. 访问速度更快
- 内存存储:传统关系型数据库(如 MySQL)的数据通常存储在磁盘上,而 Redis 将数据存储在内存中。内存的读写速度比磁盘快得多(通常是微秒级别 vs 毫秒级别)。
- 应用场景:通过将高频访问的数据(如热点数据、会话信息)存储在 Redis 中,可以避免频繁访问磁盘数据库,从而将访问速度提升几十倍甚至上百倍。
# 2. 支持高并发
- 高 QPS:传统数据库(如 MySQL)单机 QPS(每秒查询次数)通常在几千左右(例如 4 核 8GB 配置下约 4000 QPS),而单机 Redis 的 QPS 轻松达到 5 万以上,甚至 10 万+。如果使用 Redis 集群,性能还能进一步提升。
- 减轻数据库压力:将部分请求从数据库转移到 Redis 缓存,可以显著减少数据库的负载,提升系统的整体并发能力。例如,用户请求先查 Redis,若缓存命中则无需访问数据库。
# 3. 功能全面
Redis 不仅是一个简单的键值存储,还支持多种数据结构(字符串、哈希、列表、集合、有序集合等),并提供了丰富的功能:
- 分布式锁:利用 Redis 的原子性操作(如 SETNX),可以实现分布式系统中的锁机制。
- 限流:通过计数器或滑动窗口算法,实现接口访问频率限制。
- 消息队列:支持简单的发布/订阅模式,或通过列表实现队列功能。
- 延时队列:结合有序集合(Sorted Set),可以实现定时任务或延迟处理。
- 其他:如排行榜(Sorted Set)、地理位置计算(GeoHash)等。
# redis为什么快?
Redis 之所以“快”,是因为它在设计和实现上做了很多高效的优化,下面我们可以从多个角度来详细分析 Redis 为什么这么快:
# 1.内存存储:访问速度极快
- Redis 所有数据都存储在 内存(RAM) 中,内存的访问速度远远快于磁盘(比如 SSD、HDD)。
- 相比传统的基于磁盘的数据库(如 MySQL、PostgreSQL),内存读写速度快几个数量级。
| 存储介质 | 访问延迟 | 
|---|---|
| 内存(RAM) | 纳秒级(ns) | 
| SSD 磁盘 | 微秒级(μs) | 
| 机械硬盘 | 毫秒级(ms) | 
# 2.单线程模型:避免上下文切换与锁竞争
- Redis 使用单线程(从 Redis 6 开始部分 I/O 可多线程)处理命令请求。
- 这样做可以避免多线程带来的 线程上下文切换 和 加锁开销,逻辑简单、高效。
🚫 避免的代价:
- 无需加锁 ➜ 没有锁竞争
- 无上下文切换 ➜ CPU 缓存利用率高
- 请求队列串行执行 ➜ 避免并发冲突
Redis 大多数时候不是算力密集型应用,而是“内存+网络 IO 密集型”,所以 CPU 通常不是瓶颈
# 3.高效的数据结构设计
Redis 提供多种数据结构,每种都为不同的应用场景做了高度优化。这些结构都为了“时间复杂度低、空间利用高”而设计,比如 O(1) 的插入、删除、查找等。
具体参考后面章节
# 4. I/O 多路复用:网络处理效率高
Redis 使用 epoll(Linux)/ kqueue(BSD/macOS) 实现的 I/O 多路复用模型:
- 监听多个连接,只用一个线程处理多个客户端请求。
- 和传统的“一个连接一个线程”模型相比,系统资源占用低、上下文切换少。
- epoll 本身是操作系统提供的高效系统调用。
# 5. 持久化机制不影响主流程
虽然 Redis 是内存数据库,但支持持久化(RDB 和 AOF):
- 持久化在 后台线程异步 执行,不影响主线程服务请求。
- 同时通过 fork 子进程 来做快照或日志写入,确保主线程高性能运行。
# Redis的线程模型
Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。
- 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。

# redis是单线程吗?
redis的单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发生数据给客户端」这个过程是由一个线程(主线程)来完成的,也就是时说redis的文事件处理器是单线程运行的,文件事件分派器需要一个个的去处理,这也是我们常说 Redis 是单线程的原因。
Redis 在启动时会创建一些后台 I/O 线程(BIO,Background I/O threads),这些线程专门用来执行一些比较耗时、不能阻塞主线程的任务,比如:
- 异步关闭文件(如关闭 AOF 或 RDB 文件)
- 异步删除大 key
- 异步刷新 AOF 文件到磁盘
这样做的好处是:主线程可以继续快速处理客户端请求,而这些耗时操作就交给后台线程异步处理,从而提升性能和响应速度。
# redis 6.0 之后为何引入多线程?
Redis 6.0 引入多线程的主要目的,是提升网络 I/O(尤其是数据读写部分)的性能。
在 Redis 的执行流程中,数据操作本身非常快(内存级读写),但在高并发场景下,网络收发数据成为主要性能瓶颈。Redis 的性能在此时往往受限于内存带宽与网络 I/O,而非 CPU 运算。
“命令执行依然由一个线程完成,但网络数据的收发(IO)可以交给多个线程并行做。”
因此:
- 多线程 仅用于网络数据的读写阶段(parse/read/write);
- 命令的解析与执行仍由主线程串行完成。
换句话说,多线程的引入仅用于加速数据包的接收与发送过程,而非改变 Redis 的核心单线程执行模型。
+-------------------------+
| Redis 6.0 I/O Pipeline |
+-------------------------+
   Client Requests
        │
        ▼
 [I/O 线程池]   ← 多线程同时从多个socket读取命令数据
        │
        ▼
 [主线程执行命令] ← 仍然是单线程(保持一致性)
        │
        ▼
 [I/O 线程池]   ← 多线程同时将结果写回各个客户端
2
3
4
5
6
7
8
9
10
11
12
13
14
# redis的数据类型
Redis 是一个高性能的键值存储数据库,它支持多种丰富的数据类型,这也是它功能强大的重要原因之一。以下是 Redis 目前支持的主要数据类型及其特点和常见使用场景:
# 1. 字符串(String)
- 描述:最基本的数据类型,可以存储字符串、整数或浮点数,虽然你去type获取value的的类型都是string但是底层数据结构不一样,也就是为什么可以incr。
- 最大容量:单个字符串值最大为 512MB。
- 常用命令:
- SET key value:设置键值对。
- GET key:获取键对应的值。
- INCR key:将键值(整数)加 1。
- APPEND key value:追加内容到字符串。
 
- 使用场景:
- 缓存:存储 JSON 或简单字符串(如用户信息)。
- 计数器:如文章阅读量、点赞数(利用 INCR/DECR)。
- 会话管理:存储用户 Session ID。
 
# 2. 哈希(Hash)
- 描述:键值对的集合,适合存储对象。一个键对应一个字段-值映射表。
- 特点:比存储整个 JSON 字符串更高效,可以单独操作某个字段。
- 常用命令:
- HSET key field value:设置哈希表中的字段值。
- HGET key field:获取指定字段的值。
- HGETALL key:获取所有字段和值。
 
- 使用场景:
- 存储对象:如用户信息(字段:name、age、email)。
- 购物车:用户 ID 作为键,商品 ID 和数量作为字段。
 
# 3. 列表(List)
- 描述:有序的字符串列表,按照插入顺序排列,支持双端操作。
- 特点:可以用作队列或栈。
- 常用命令:
- LPUSH key value:左侧插入元素。
- RPUSH key value:右侧插入元素。
- LPOP key:左侧弹出元素。
- LRANGE key start stop:获取指定范围的元素。
 
- 使用场景:
- 消息队列:通过 LPUSH 和 RPOP 实现简单的队列。
- 最新列表:如最新的评论、动态(按时间顺序存储)。
 
# 4. 集合(Set)
- 描述:无序且唯一的字符串集合。
- 特点:支持集合运算(如交集、并集、差集)。
- 常用命令:
- SADD key member:添加元素到集合。
- SMEMBERS key:获取集合所有成员。
- SINTER key1 key2:计算两个集合的交集。
 
- 使用场景:
- 去重:如用户标签、抽奖参与者。
- 共同好友:利用交集运算查找共同关注的人。
- 随机抽取:如 SRANDMEMBER随机获取元素。
 
# 5. 有序集合(Sorted Set)
- 描述:类似于集合,但每个元素关联一个分数(score),按分数排序。
- 特点:既保持唯一性,又支持排序。
- 常用命令:
- ZADD key score member:添加元素及其分数。
- ZRANGE key start stop:按分数顺序获取元素。
- ZREVRANGE key start stop:按分数倒序获取元素。
- ZINCRBY key increment member:增加元素的分数。
 
- 使用场景:
- 排行榜:如游戏积分榜、热度排行(分数动态更新)。
- 延时队列:用分数作为时间戳,实现定时任务。
 
# 6. 其他高级数据类型
Redis 在后续版本中引入了一些扩展数据类型(部分需要启用特定模块):
- 位图(Bitmap) - 描述:基于字符串的位操作,用于存储二进制数据。
- 常用命令:SETBIT key offset value、GETBIT key offset。
- 使用场景:统计活跃用户(0/1 表示是否登录)、布隆过滤器基础。
 
- HyperLogLog - 描述:用于基数统计的概率数据结构,占用空间极小。
- 常用命令:PFADD key element、PFCOUNT key。
- 使用场景:统计网页 UV(独立访客数)。
 
- 地理空间(Geo) - 描述:存储经纬度坐标,支持距离计算和范围查询。
- 常用命令:GEOADD key longitude latitude member、GEODIST key member1 member2。
- 使用场景:附近的人、店铺位置查询。
 
- Stream - 描述:日志式数据结构,支持消息队列和消费者组。
- 常用命令:XADD key ID field value、XREAD key。
- 使用场景:事件流处理、日志收集。
 
# Redis 底层数据结构
Redis 是一个高性能的内存键值数据库,以其速度快、支持多种数据类型而闻名。Redis 的核心在于其底层数据结构的实现,尤其是键(Key)和值(Value)的存储方式。本教程将深入探讨 Redis 的底层数据结构,包括键的实现、各种值类型对应的数据结构,以及一些设计上的权衡和演变。

# 1. Redis 键(Key)的底层实现
Redis 的键本质上是一个字符串,而其底层存储方式类似于 Java 中的 HashMap 或 HashTable。具体来说,Redis 使用**哈希表(Hash Table)**来存储键值对,其结构可以简单描述为:
- 数组 + 链表:哈希表由一个数组组成,每个数组元素(称为“桶”)指向一个链表。当发生哈希冲突时,通过链表将多个键值对链接起来。
- 哈希函数:Redis 使用哈希函数(比如 MurmurHash)将键映射到数组的某个索引位置。
- 动态扩容:当哈希表中的元素过多(负载因子过高)时,Redis 会进行 rehash 操作,动态扩展数组大小,并重新分配键值对。
这种设计使得 Redis 的键查找效率接近 O(1),但具体性能还与链表长度(冲突情况)有关。
# 2. Redis 值(Value)的底层数据结构
Redis 的强大之处在于它支持多种数据类型(String、Hash、List、Set、ZSet 等),而每种数据类型的底层实现并不完全相同。根据值的类型,Redis 会选择不同的数据结构来优化存储和操作效率。下面逐一讲解:
# 1. String(字符串类型)
- 底层实现:int+SDS(Simple Dynamic String,简单动态字符串)
- 特点:
- SDS 是 Redis 自定义的字符串结构,相比 C 的传统字符串(以 \0结尾),SDS 更高效。
- SDS 包含三个字段:len(字符串长度)、free(未使用字节数)、buf(实际字符数组)。
- 通过 len可以直接获取字符串长度(O(1) 时间),无需遍历。
 
- SDS 是 Redis 自定义的字符串结构,相比 C 的传统字符串(以 
- 优点:
- 避免缓冲区溢出,支持动态扩展。
- 二进制安全,可以存储任意数据(如图片、序列化对象)。
 
- 使用场景:计数器、缓存文本、序列化对象等。

# 2. Hash(哈希表类型)
- 底层实现:两种数据结构(根据数据量动态切换)
- Listpack(较新版本中替代了 Ziplist):当哈希表元素较少时使用。
- Listpack 是一个紧凑的序列化数据结构,连续存储键值对,节省内存。
- 适合小规模数据,查询和插入效率较高。
 
- Hashtable:当元素数量较多或键值对较大时使用。
- 与 Redis 的键存储类似,是标准的数组 + 链表结构。
 
 
- Listpack(较新版本中替代了 Ziplist):当哈希表元素较少时使用。
- 切换条件:
- 当哈希表中键值对数量较少(默认小于 512)且每个键值对长度较短(默认小于 64 字节)时,使用 Listpack。
- 超过阈值后,转换为 Hashtable。
 
- 使用场景:存储对象(如用户信息:name、age 等键值对)。
struct listpack<T> {
        int32 total_bytes; // 整个listpack,占用的总字节数
        int16 size; // 整个listpack中元素个数
        T[] entries; // 紧凑排列的元素列表
        int8 end; // listpack的结束符,恒为 0xFF
    }
2
3
4
5
6

# 3. List(列表类型)
- 底层实现: - LinkedList(双向链表):Redis 早期版本使用较多。
- 每个节点包含指向前一个和后一个节点的指针,适合快速在两端插入或删除。
 
- Ziplist(压缩列表):在元素较少时使用,后被 Listpack 替代。
- Ziplist 是连续内存块,按顺序存储元素,节省空间。
 
 - 但是在 Redis 3.2版本之后,List数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压 
 缩列表。
- LinkedList(双向链表):Redis 早期版本使用较多。
- 特点: - 双向链表适合频繁的头尾操作(如 LPUSH、RPOP),但随机访问效率低(O(n))。
- Ziplist/Listpack 则更紧凑,适合小型列表。
 
- 使用场景:消息队列、任务列表。 
struct ziplist<T> {
      int32 zlbytes; // 整个压缩列表占用的字节数
      int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
      int16 zllength; // 真个ziplist中元素的个数
      T[] entries; // 元素内容列表,挨个挨个紧凑存储     int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
  }
2
3
4
5
6
7
8


quicklist 实际上是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。
struct quicklist {
    quicklistNode* head; //头节点指针
    quicklistNode* tail; //尾节点指针
    long count; // 元素总数
    int nodes; // ziplist 节点的个数
    int compressDepth; // LZF 算法压缩深度
}
2
3
4
5
6
7

# 4. Set(集合类型)
- 底层实现:两种数据结构(根据元素类型和数量切换)
- Intset(整数集合):当集合中所有元素都是整数且数量较少时使用。
- Intset 是一个有序的整数数组,内存紧凑,支持二分查找。
 
- Hashtable:当集合包含非整数元素或数量较多时使用。
- 键存储集合元素,值为空,保证唯一性。
 
 
- Intset(整数集合):当集合中所有元素都是整数且数量较少时使用。
- 特点:
- Intset 适用于小规模整数集合,节省内存。
- Hashtable 适合大规模或复杂数据,查询效率高(O(1))。
 
- 使用场景:唯一性检查(如用户 ID 集合)。
/* 整数集合结构 */
typedef struct intset {
    uint32_t encoding;  // 编码方式
    uint32_t length;    // 集合数组中元素个数
    int8_t contents[];  // 集合数组
} intset;
2
3
4
5
6

# 5. ZSet(有序集合类型)
- 底层实现: - Ziplist(压缩列表):早期版本用于小规模数据,后被淘汰现用listpack。
- 连续存储元素和分数(score),按分数排序。
 
- Skiplist(跳表):现代 Redis 的主要实现。
- 跳表:用于维护元素的有序性,支持快速插入、删除和范围查询。
 
 
- Ziplist(压缩列表):早期版本用于小规模数据,后被淘汰现用listpack。
- 特点: - 跳表是一种多层链表结构,通过“跳跃”指针加速查找,平均时间复杂度为 O(log n)。
 
- 使用场景:排行榜、按权重排序的任务队列。 
# 3. 跳表(Skiplist)的深入剖析
# 1. 为什么从 Ziplist 切换到跳表?
- Ziplist 的局限性:
- Ziplist 是连续内存块,插入和删除需要移动大量元素(O(n) 时间复杂度)。
- 查询效率依赖偏移量计算,虽然比链表快,但在大规模数据下仍显不足。
 
- 跳表的优势:
- 跳表基于链表,但通过多层索引(类似 B+ 树的层次结构)加速查找。
- 插入和删除只需调整指针(O(log n)),无需移动大量元素。
- 实现简单,相比平衡树(如 AVL 树)更容易维护。
 
# 2. 跳表的结构
- 底层:普通的双向链表,按分数从小到大排序。
- 上层:随机生成的索引层,每层节点指向下层节点,形成“跳跃”路径。
- 查找过程:
- 从最高层开始,沿索引快速定位目标范围。
- 逐层下降,最终在底层链表中找到精确位置。
 
- 时间复杂度:
- 查找、插入、删除:平均 O(log n)。
- 空间复杂度:O(n),因为需要额外存储索引。
 

# Redis支持事务吗?
Redis 支持基本的事务功能,但与传统关系型数据库的事务机制相比,它的事务模型相对简单。Redis 的事务机制主要通过 MULTI、EXEC、DISCARD 和 WATCH 命令实现。
# Redis 事务模型概述
在 Redis 中,事务并不像传统的数据库事务那样支持回滚和隔离级别,而是一个简单的事务队列,允许多个命令按顺序执行。Redis 事务的关键点是将多个命令打包在一起,保证它们以原子方式执行,即所有命令会顺序执行,且在执行过程中不会被其他客户端命令打断。
# 1. 基本操作
- MULTI:开始一个事务块,标记后续命令为事务的一部分。
- EXEC:执行事务中的所有命令。此时,所有在- MULTI后执行的命令会依次按顺序执行。
- DISCARD:放弃事务,清除事务队列中的所有命令。
- WATCH:监视一个或多个键,在事务执行之前,如果监视的键发生了变化,事务会被中止,避免出现数据不一致的情况。
# 2. 事务执行流程
事务的基本流程如下:
- 启动事务: 客户端通过发送 - MULTI命令来开始一个事务。此时 Redis 会进入事务模式,接下来执行的命令都不会立即执行,而是会被放入事务队列中等待。- MULTI1
- 将命令排入队列: 在事务模式下,客户端可以发送多个命令,这些命令不会立即被执行,而是被排入事务队列。例如: - SET key1 value1 INCR key21
 2
- 执行事务: 发送 - EXEC命令后,Redis 会按顺序执行事务队列中的所有命令。此时,所有命令会原子性地执行,整个事务中的命令不会被其他客户端的命令中断。- EXEC1
- 取消事务: 如果事务中的命令还没有执行,客户端可以使用 - DISCARD命令取消事务,清除事务队列中的所有命令。- DISCARD1
# 3. 事务的原子性
Redis 事务并不支持传统意义上的 回滚(rollback)机制。如果事务中的某个命令执行失败,Redis 会继续执行剩余的命令,因此并没有严格的“全成功或全失败”的保证。
- 例如,如果 MULTI后的某个命令失败了(例如操作一个不存在的键),其他命令仍会按顺序执行。这种行为与传统关系数据库中的回滚机制有所不同。
# 4. WATCH 和乐观锁
为了增强事务的一致性,Redis 提供了 WATCH 命令,支持 乐观锁,即在执行事务前,可以监视某些键。如果这些键在事务执行前被其他客户端修改了,事务会被中止。
- WATCH:监视指定的一个或多个键。如果这些键在事务执行期间被修改,- EXEC命令执行时会返回空数组,表示事务被中止。- WATCH key1 MULTI SET key1 new_value EXEC1
 2
 3
 4- 如果 - key1在- MULTI和- EXEC之间被其他客户端修改,- EXEC会返回空数组,表明事务未能执行。
- 适用场景: - WATCH适用于需要确保某些数据在事务执行期间没有被其他客户端修改的情况。
# 5. 事务的缺陷和局限性
- 缺乏隔离性:Redis 事务在执行时不能提供类似传统关系数据库的隔离性(如串行化、可重复读等)。所有命令都在同一个线程中按顺序执行,且无法并发处理,因此存在其他客户端在事务执行过程中修改数据的风险。
- 没有回滚机制:如果事务中的某个命令失败,Redis 并不会回滚之前的命令,只会继续执行后续的命令。
- 事务不是“全-or-无”:虽然事务中的命令是按顺序执行的,但如果某个命令失败,其他命令依然会继续执行。
# 6. 事务 vs Lua 脚本
虽然 Redis 的事务机制能够提供命令的原子性执行,但它并不如 Lua 脚本那样原子性强大。Redis 通过 Lua 脚本 提供了更高的原子性保障。
- Lua 脚本:Redis 在执行 Lua 脚本时,所有操作都在同一个事务中执行,不会被中断,且无其他客户端命令可以插入。因此,Lua 脚本可以看作是比事务更强的原子性操作。 - EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 key1 value11- Lua 脚本的原子性可以确保在脚本执行过程中不会出现中断,即使是发生故障也不会导致数据不一致。 
# Reids持久化
Redis 的持久化机制包括 RDB 和 AOF 两种方式,确保数据在服务器重启后不丢失。RDB 通过快照保存数据状态,AOF 通过记录写操作命令重建数据集。这两种方式可以单独使用或同时启用。

# 什么是 RDB
RDB(Redis 数据库快照)通过创建数据集的快照,在指定时间间隔内将数据保存到磁盘的 RDB 文件中。
- 手动触发: 使用 SAVE(阻塞所有客户端请求,适合小数据集)或BGSAVE(后台异步保存,推荐生产环境)。
- 自动触发: 通过配置文件(如 save 900 1表示 900 秒内至少 1 个键修改后自动保存)或服务器关闭时触发。
- 令人惊讶的是: RDB 文件非常紧凑,适合备份,但可能会丢失最后一次快照后的数据。
# 什么是 AOF
AOF(追加文件)通过记录每个写操作命令(如 SET、HSET)并追加到 AOF 文件,恢复时重新执行这些命令重建数据集。
- 工作流程: 包括命令写入、文件同步(每秒、每次写或由操作系统决定)、文件重写(压缩文件)、重启加载。
- 令人惊讶的是: AOF 文件可能变大,恢复较慢,但实时性好,适合需要高数据完整性的场景。
# RDB 和 AOF 的对比与选择
- RDB 优点: 恢复快,文件小,适合备份;缺点: 数据丢失风险高。
- AOF 优点: 数据完整性高,适合关键数据;缺点: 文件可能较大,恢复慢。
- 选择建议: 如果追求性能且能接受少量数据丢失,用 RDB;如果数据耐久性关键,用 AOF(推荐每秒同步)。生产环境常同时启用两者:RDB 备份,AOF 保证完整性。
Redis 4.0 的混合持久化:
从 Redis 4.0 开始,AOF 重写时可嵌入 RDB 快照,恢复时先加载快照再应用 AOF 命令,既快又可靠。
# 大 Key 产生的原因
常见原因包括:
- 存储大型数据结构:未经过拆分或序列化压缩的对象直接写入 Redis。
- 缓存滥用:将原本不适合缓存的完整数据结构(如大日志文件、大列表)写入 Redis。
- 应用设计不合理:未对业务数据进行分片,导致热点数据持续堆积在某个 Key 中。
- 数据累计:未设置过期时间或清理机制,数据不断增加。
# 如何快速定位大 Key?
Redis 提供了一些工具与命令帮助排查:
- SCAN 命令:支持渐进式遍历,不会阻塞 Redis 主线程(KEY * 阻塞的),可用于批量扫描 key,对扫描出来的 key,结合 - MEMORY USAGE或集合类命令统计大小:。
- --bigkeys 参数:在使用 - redis-cli时加上- --bigkeys参数,可以统计不同数据结构的大 Key 分布情况(基于SCAN,也不会阻塞)。- redis-cli --bigkeys1
- Redis RDB Tools:对 RDB 文件进行分析,统计 key 的大小、类型、分布情况。 
# 优化与解决方案
针对大 Key 问题,可以从以下几个角度优化:
- 拆分成小 Key - 将一个大集合拆分为多个小集合,按业务维度或时间维度切分。
- 例如,将一个存储所有用户数据的 hash,拆分为多个用户 ID 分片存储。
 
- 优化数据结构 - 根据场景选择合适的 Redis 数据类型,避免存储冗余数据。
- 使用压缩编码(如 ziplist、listpack)或序列化方式减小数据体积。
 
- 设置合理的过期时间 - 防止数据长期累积,定期清理历史数据。
- 对临时性数据(如会话信息、缓存结果)设置合理 TTL。
 
- 启用内存淘汰策略 - 在 redis.conf中设置maxmemory-policy,避免内存占满导致 OOM。
- 常用策略有 volatile-lru、allkeys-lru等。
 
- 在 
- 数据分片 - 在 Redis Cluster 或分布式场景下,将数据按业务维度打散,避免数据倾斜。
 
- 删除大 Key - 对历史遗留大 Key,采用 UNLINK(异步删除)替代DEL,避免阻塞。
- 对集合类大 Key,可以分批删除子元素,避免一次性清理导致卡顿。
 
- 对历史遗留大 Key,采用 
- 增加内存容量 - 在硬件允许的情况下,适当增加 Redis 实例内存,缓解大 Key 占用问题。
 
# Redis分布式锁
分布式锁是保证在分布式系统中,多个进程或服务能够协调共享资源的一种机制,确保同一时刻只有一个进程能访问某个共享资源,避免并发冲突和数据不一致的问题。
在传统的单机环境下,可以通过简单的线程锁来保证资源的独占访问,但在分布式环境中,锁的实现和管理变得更加复杂,因为系统中有多个节点,且可能分布在不同的物理机上。Redis 提供了一个很好的分布式锁实现方式,常用的机制包括:
# 1. Redis 实现分布式锁的基本原理
Redis 本身是单线程的,处理命令的顺序是严格按照请求的顺序执行的,因此可以利用 Redis 的原子性操作来实现分布式锁。常见的做法是利用 Redis 的 SETNX 命令(即“Set if Not Exists”)来尝试获取锁。
# 2. 基本实现方式
最常见的分布式锁实现方式是通过 SETNX(或 SET 带 NX 参数)来创建锁,并设置一个过期时间(防止死锁)。具体流程如下:
- 获取锁: - 客户端通过 SET key value NX PX timeout命令向 Redis 请求设置一个唯一的锁标识符 (value)。- NX:只有当键不存在时,才会设置成功(相当于 “Set if Not Exists”)。
- PX timeout:设置键的过期时间(避免长时间无法释放锁,防止死锁)。
 
 - 例如: - SET lock_key unique_lock_value NX PX 300001- 如果返回值为 - OK,则表示获取锁成功。否则,表示锁已经被其他客户端占用。
- 客户端通过 
- 释放锁: - 释放锁时需要保证只有持有锁的客户端才能删除锁,防止其他客户端误删锁。
- 常见的做法是使用 DEL命令删除锁键,但需要加上检查,确保只有锁持有者才能删除锁**(通过UUID+线程名)**。
 
# 3. 分布式锁问题
分布式锁的基本实现有时会有一些问题,比如“锁超时”或“死锁”情况。为了解决这些问题,通常会采取以下一些改进措施:锁在获得后通常会设置一个过期时间 (timeout),防止锁在某些客户端崩溃或发生故障时无法释放,从而导致其他客户端无法获取锁,如果时间过期了业务还没处理完,可以用一个子进程10秒钟判断一次锁重新设置过期时间。
# Redis高可用性
Redis 提供了多种机制来实现高可用性,确保即使某些节点发生故障,系统仍能正常运行。具体包括 主从复制、哨兵模式 和 集群模式,每种方式适应不同的需求和场景。
# Redis 主从复制
在 Redis 的主从架构(Master-Slave Replication)中,从节点(Slave)通过同步主节点(Master)的数据来实现数据冗余与高可用。Redis 的同步机制主要分为 完全同步(Full Resynchronization) 和 增量同步(Partial Resynchronization) 两种方式。本文将系统介绍这两种同步方式的实现原理与流程。
Redis 主从复制的整体流程可以划分为三个阶段:
- 建立连接与协商同步
 从服务器通过SYNC或PSYNC命令向主服务器发起同步请求。
- 数据同步阶段
 主服务器将当前数据集同步给从服务器。
- 命令传播阶段
 主服务器将新产生的写操作命令实时发送给从服务器,以保证数据一致性。
# 1. 完全同步(Full Resynchronization)
完全同步通常发生在以下几种情况:
- 初次同步:从节点第一次连接主节点;
- 从节点数据丢失:例如宕机或内存清空;
- 数据差异过大:主从长时间未同步,导致无法增量恢复。

- 从服务器发送 SYNC 命令
 从服务器向主服务器发送SYNC命令,请求进行同步。
- 主服务器生成 RDB 快照
 主服务器接收命令后,会生成一个 RDB 快照文件,用于保存当前的数据集。
- 传输 RDB 文件
 主服务器将生成的 RDB 文件发送给从服务器。
- 从服务器接收并载入 RDB 文件
 从服务器接收 RDB 文件后,会清空当前数据并加载新的数据。
- 主服务器记录写操作命令
 在生成与传输 RDB 文件的过程中,主服务器会将新的写命令记录到repl_backlog_buffer中。
- 传输 backlog 中的写命令
 RDB 传输完成后,主服务器会将 backlog 中的命令发送给从服务器,从服务器执行这些命令,完成数据同步。
# 2. 增量同步(Partial Resynchronization)
完全同步代价较高,因此 Redis 引入了 增量同步 机制,使从节点能从上次中断的位置继续同步,而无需重新加载整个数据集。
增量同步基于 PSYNC 命令,通过 运行 ID(run ID) 和 复制偏移量(offset) 实现。

- 从服务器发送 PSYNC 命令
 网络恢复后,从服务器发送PSYNC <runid> <offset>命令给主服务器,offset 不为 -1。
- 主服务器响应 CONTINUE
 如果主服务器判断可以增量同步,会回复+CONTINUE,表示使用增量复制。
- 传输增量命令
 主服务器将从断线到现在的写命令发送给从服务器执行,完成数据补齐。
# 3. repl_backlog_buffer 作用
repl_backlog_buffer(复制积压缓冲区)
- 作用:保存最近传播的写命令;
- 结构:环形缓冲区;
- 默认大小:1MB。
主服务器在命令传播阶段,不仅将命令发送给从服务器,也会写入到 repl_backlog_buffer。当网络中断后,从服务器重新连接时,会携带自己的复制偏移量(slave_repl_offset),主服务器根据自身的偏移量(master_repl_offset)计算差距,从而决定同步方式:
- 若差异数据仍存在于 repl_backlog_buffer中 → 执行增量同步;
- 若差异数据已被覆盖 → 执行完全同步。

由于 repl_backlog_buffer 是环形结构,当写入速度超过从服务器的读取速度时,缓冲区旧数据会被覆盖。
若此时从服务器要读取的数据被覆盖,主服务器只能执行完全同步,性能开销会显著增大。因此,应根据主从延迟和写入量合理调整缓冲区大小,以尽量避免频繁的全量同步。
# 哨兵模式 (Sentinel)
在 Redis 的主从架构中,由于主从模式是读写分离的,如果主节点(master)挂了,那么将没有主节点来
服务客户端的写操作请求,也没有主节点给从节点(slave)进行数据同步了。

- 工作原理:
- 监控:哨兵不断监控 Redis 主节点和从节点的健康状态。
- 故障转移:当哨兵检测到主节点故障时,它会自动将一个从节点提升为新的主节点,并更新配置文件。然后,客户端可以通过新的主节点继续执行写操作。
- 通知:哨兵可以向其他客户端或服务提供通知,告知主节点发生了故障和故障转移的信息。
 
- 优缺点:
- 优点:通过自动故障转移机制,增强了系统的高可用性,即使主节点故障,服务仍然可以继续提供。
- 缺点:配置和管理相对较复杂,需要更多的资源来运行哨兵进程。
 
# 集群模式 (Cluster Mode)
当 Redis 缓存的数据量增长到单台服务器无法容纳时,就需要通过 Redis Cluster(集群模式) 来实现水平扩展。Redis Cluster 将数据分布在多个节点上,实现 数据分片(Sharding),以此降低系统对单个主节点的依赖,提高系统的整体读写性能和可用性。
# 1. 实现原理
Redis Cluster 采用 哈希槽(Hash Slot) 的机制来实现数据与节点之间的映射关系。整个集群共有 16384 个哈希槽,每个键(key)都会根据其哈希值被分配到其中的某个槽(Slot)中。
# 2. 哈希槽的计算过程
Redis Cluster 通过以下两步将一个键映射到具体的哈希槽中:
- 计算哈希值:
 根据键名(key)使用CRC16算法计算一个 16-bit 的整数值。
- 计算哈希槽编号:
 将计算结果对 16384 取模,得到一个介于0 ~ 16383之间的整数。
 这个整数就是该 key 所在的哈希槽编号。
例如:
slot = CRC16(key) % 16384
这样,Redis 不需要记录每个 key 的具体分布,而是只需知道哪些槽属于哪些节点即可完成定位。
# 3. 哈希槽与节点的映射关系
Redis Cluster 中的关键问题是:**16384 个哈希槽如何映射到不同的节点上?**Redis 提供了两种映射方式:
# 平均分配(自动方式)
当我们使用 redis-cli --cluster create 命令创建 Redis 集群时,系统会自动将 16384 个哈希槽平均分配给所有节点。
例如,一个包含 9 个节点的集群,每个节点将分配大约:
16384 / 9 ≈ 1820 个哈希槽
Redis 会在集群初始化阶段自动完成这种均衡分配。
# 手动分配(自定义方式)
我们也可以手动指定每个节点负责的哈希槽范围。
典型的做法是:
- 使用 CLUSTER MEET命令手动让各节点彼此认识,组成集群;
- 使用 CLUSTER ADDSLOTS命令为每个节点分配哈希槽。
例如,假设集群中有两个节点,并且只使用 4 个槽(Slot 0~3)进行演示:

命令如下:
redis-cli -h 192.168.1.10 -p 6379 cluster addslots 0 1
redis-cli -h 192.168.1.11 -p 6379 cluster addslots 2 3
2
这样:
- 节点1 负责哈希槽 0 和 1;
- 节点2 负责哈希槽 2 和 3。
在运行过程中,假设有两个键:
| Key | CRC16 结果 | 取模结果(%4) | 所属哈希槽 | 存储节点 | 
|---|---|---|---|---|
| key1 | 34521 | 1 | Slot 1 | 节点1 | 
| key2 | 56790 | 2 | Slot 2 | 节点2 | 
Redis 会根据键的哈希槽编号自动将请求路由到对应的节点上。
⚠️ 注意:在手动分配哈希槽时,必须将 16384 个槽全部分配完,否则 Redis 集群将无法正常工作。
# 4.数据迁移与槽重分配
在实际生产环境中,当集群扩容或缩容时,Redis 允许动态地将哈希槽从一个节点迁移到另一个节点。
这意味着可以 在线增加或减少节点,Redis 会自动迁移对应槽内的数据,保证集群持续可用。
迁移命令:
redis-cli --cluster reshard <ip>:<port>
Redis 会提示你选择目标节点、迁移槽数量以及源节点范围,然后自动完成数据迁移。
# 5. 优缺点
- 优点:支持 水平扩展,通过添加更多节点可以增加存储容量和计算能力。集群模式支持 自动分区 和 自动故障转移,提高了系统的容错性和可扩展性。
- 缺点:配置和管理相对复杂,需要处理分片、节点间通信等问题。集群模式下,不支持对单一键进行跨节点的多键事务操作。
# Redis脑裂问题怎么解决?
在生产环境中,Redis 通常采用 主从 + Sentinel(哨兵) 的高可用架构:
一个 Master 主机负责写操作,多个 Slave 从机负责读操作,哨兵(Sentinel)负责监控主从节点的健康状态,并在主节点宕机时自动执行故障转移(Failover)。
然而,在网络异常场景下,这种机制可能会出现“脑裂(Split-Brain)”问题。
所谓“脑裂”,指的是:
由于网络分区(Network Partition)或通信异常,导致 Redis 集群中出现两个主节点(Master)并同时对外提供写服务的情况。

# 1. 带来的危害
突然这时原主节点网络好了,然而此时故障转移已完毕,已经有新主节点了,那么原主节点就会降级为新主节点的从节点,新主节点会向所有实例发送slave of命令,让所有实例重新全量同步,在全量同步之前,从节点会先清空自己的数据,那么客户端在原主节点故障期间写入的数据就丢失了。
# 2. 防护机制
Redis 从 3.0 起提供了两项关键参数,可有效避免脑裂场景下的“孤立写入”问题:
min-slaves-to-write
限制主节点在缺少可用从节点时的写操作。
min-slaves-max-lag
限制主从间的复制延迟阈值(单位:秒)。
配置示例:
min-slaves-to-write 1  #当主节点检测到少于1个从节点可用
min-slaves-max-lag 10 # 所有从节点与主节点的复制延迟(lag)超过10秒时 → Redis 主节点将拒绝写入请求
2
✅ 效果:
即使旧 Master 被网络隔离,它因为检测不到健康的 Slave,会自动拒绝写入,从而防止出现“旧 Master 继续写、数据分裂”的情况。
# 如何保证缓存和数据库的数据⼀致性?
如果只有读操作,那么就不会造成不一致现象,当出险了写,和读并发的时候就会出现不一致现象。由于redis的更新代价比删除代价大,因此我们只考虑删除缓存。那么我们是先操作缓存再操作数据库,还是先操作数据库再操作缓存?
先操作缓存再操作数据库,如果写线程先删除缓存,再去更新数据库,在更新数据库这个操作过程中由于网络阻塞,这个时候读线程就会读取到旧数据来更新缓存,从而导致了数据的不一致,为了解决这个问题我们可以在写进程删除数据库后再进行一次删除缓存,,那么还会出现一个问题,如果是在读的进程去更新缓存前触发了删除那么缓存还是被更新成旧数据,这个时候可以用延迟双删,在延迟的这段时间内数据库是不一致,最终是一致的,如果非得保证一致性那么只能上锁了保证写的操作是原子的。
先操作数据库再操作缓存,我们先去更新数据库,再去删除缓存,在删除缓存前读到的数据可能是脏数据,但最终能保证数据的一致性,推荐这种方式,但是如果缓存删除失败了怎么办,这里可以用多种技术例如中间件等
# 雪崩、击穿、穿透
在分布式缓存系统中,常常会提到“缓存雪崩”、“缓存击穿”和“缓存穿透”这些问题,它们通常都是在缓存失效或不一致时引发的一系列问题。这些问题不仅影响缓存的性能,还可能对后端数据库或整个系统造成较大负担。以下是它们的具体含义和常见的解决方案。
# 1. 缓存雪崩(Cache Avalanche)
缓存雪崩是指在同一时间内大量缓存失效(通常是缓存过期),导致大量的请求直接访问数据库,从而给数据库带来巨大的压力,甚至可能导致数据库崩溃。
- 原因:缓存雪崩通常是由于所有缓存的过期时间都相同,或者缓存失效的时间设置得过短,导致大量请求同时访问缓存过期的数据,直接访问数据库。
- 举个例子:
- 假设缓存中的某些数据过期时间是 10 点过期,如果在 10 点的时刻,所有缓存的数据都失效了,用户的请求就会同时访问数据库,给数据库带来巨大的压力,可能导致数据库宕机。
 
解决方案:
- 缓存过期时间随机化:为缓存设置一个随机的过期时间,避免所有缓存同时过期。例如,可以在原本的过期时间上加上一个随机值,避免多个缓存同时失效。
- 分布式锁:在缓存失效的情况下,通过分布式锁机制,限制同一时间只有一个请求能够从数据库加载数据并更新缓存,防止缓存穿透对数据库造成的过大压力。
- 异步更新缓存:在缓存过期时,异步去数据库加载数据,避免由于同时访问数据库导致的压力。
- 双缓存策略:通过双缓存(即设置两层缓存,第一层缓存为短期缓存,第二层为长期缓存)来避免缓存雪崩的情况。
# 2. 缓存击穿(Cache Breakdown)
缓存击穿是指某个特定的缓存数据在短时间内失效或者被删除,而有大量请求并发访问这个缓存时,这些请求都会访问数据库,导致数据库压力激增。常见的情景是缓存中的某个数据在短时间内过期,且数据访问量非常高。
- 原因:缓存失效的数据恰好是高并发的热点数据,导致大量并发请求同时访问数据库。
- 举个例子:
- 假设某个热点数据的缓存失效,多个请求同时访问该数据,并且数据库没有缓存的情况下,所有请求都需要查询数据库并返回结果。
 
解决方案:
- 加锁(互斥锁):对于缓存失效的数据,可以通过加锁来保证在同一时刻只有一个请求去数据库查询数据并更新缓存,其他请求等待数据更新后直接读取缓存,避免大量请求同时访问数据库。
- 实现方法:使用 Redis 分布式锁或数据库锁,在缓存失效时,只有第一个请求能从数据库加载数据,其他请求等待数据加载完成后再获取缓存。
 
- 请求合并:将多个请求合并成一个请求,通过队列等方式处理并发请求,防止多次查询数据库。
- 预加载(提前加载缓存):当热点数据即将失效时,提前加载并更新缓存,避免大量请求同时查询数据库。
# 3. 缓存穿透(Cache Penetration)
缓存穿透是指查询的请求数据既不在缓存中,也不在数据库中。由于缓存无法命中且数据库也没有该数据,导致每次请求都要去数据库查询,增加数据库的压力。缓存穿透的发生通常是由于查询的数据不存在,导致每次都从数据库查询。
- 原因:客户端查询的数据不存在,缓存无法命中,数据库也没有该数据,而每次请求都绕过了缓存,直接访问数据库,造成数据库压力过大。
- 举个例子:
- 假设用户每次查询一个不存在的数据(比如用户输入的 ID 错误),这时数据库会返回空值,而缓存也没有这条数据。每次请求都会触发数据库查询,造成数据库压力,且缓存永远无法存储该数据。
 
解决方案:
- 缓存空值(Cache Null Value):当查询的数据不存在时,可以将一个特定的空值(如 null或特殊标记)缓存一段时间,避免相同的请求再次访问数据库。例如,如果数据库查询返回null,可以将null缓存一段时间(如 5 分钟),这样下次相同的请求就会直接返回缓存的空值,而不是再查询数据库。
- 数据验证:在访问数据库前,通过某种方式验证请求的数据是否合法。如果查询的数据是无效的,可以直接返回错误,避免请求进入数据库查询。
- 布隆过滤器(Bloom Filter):使用布隆过滤器来判断数据是否存在。布隆过滤器是一种空间效率高的概率数据结构,能够快速判断一个元素是否在集合中。如果通过布隆过滤器检查某个请求的数据不在数据库中,则直接返回,避免请求数据库,减少对数据库的压力。
