【八股文】Java面试突击深度解析(Redis篇)

3 阅读28分钟

我们继续展开Redis相关的内容。Redis作为当前最流行的内存数据库之一,在互联网公司中广泛应用,面试中也是重点。下面我将从Redis基本使用、线程模型、持久化、淘汰策略、集群模式、数据结构底层原理、热点key和大key问题、集群高可用、分布式锁等方面进行深度解析。

一、Redis基本使用与线程模型

1.1 Redis基本使用

Redis支持多种数据结构:字符串(String)、列表(List)、哈希(Hash)、集合(Set)、有序集合(Sorted Set)、位图(Bitmap)、HyperLogLog、地理空间索引(Geospatial Index)和流(Stream)。此外,Redis还提供了事务、发布订阅、Lua脚本、管道等功能。

1.2 Redis线程模型

Redis是单线程的,但为什么还能达到如此高的性能?原因如下:

  1. 基于内存操作:所有数据都在内存中,操作速度非常快。
  2. IO多路复用:使用epoll、kqueue、select等系统调用,实现一个线程处理多个连接。
  3. 单线程避免上下文切换和竞争条件:不需要考虑多线程带来的锁和上下文切换问题。

但是,在Redis 6.0之后,引入了多线程处理IO,但核心的业务逻辑(命令执行)仍然是单线程。这样的设计是为了利用多核CPU处理网络IO,提高吞吐量。

Redis 6.0多线程模型

  • 主线程负责接收连接,将连接分配给IO线程。
  • IO线程负责读取请求和回写响应,而命令执行由主线程串行执行。
  • 默认情况下,多线程是禁用的,可以通过配置开启。

二、Redis持久化机制

Redis提供了两种持久化方式:RDB(Redis Database)和AOF(Append Only File)。

2.1 RDB持久化

RDB是快照持久化,在指定的时间间隔内将内存中的数据快照写入磁盘。

触发方式

  1. 自动触发:配置文件中设置,如save 900 1表示900秒内至少1个key发生变化,则触发。
  2. 手动触发:执行SAVEBGSAVE命令。SAVE会阻塞服务器,BGSAVE会fork一个子进程来执行。

优点

  • 文件紧凑,体积小,适合备份和灾难恢复。
  • 恢复速度快。

缺点

  • 会丢失最后一次快照之后的数据。
  • 当数据量较大时,fork子进程的过程可能会阻塞主进程。

2.2 AOF持久化

AOF记录每次写操作命令,以追加的方式写入文件。

同步策略

  • appendfsync always:每次写操作都同步,数据最安全,但性能最差。
  • appendfsync everysec:每秒同步一次,兼顾性能和数据安全(默认)。
  • appendfsync no:由操作系统决定何时同步,性能最好,但数据可能丢失。

AOF重写:随着写操作的增多,AOF文件会越来越大。Redis提供了AOF重写机制,通过fork子进程,根据内存中的数据重新生成一个更小的AOF文件。

优点

  • 数据丢失少(取决于同步策略)。
  • AOF文件易于理解和解析。

缺点

  • 文件体积通常比RDB大。
  • 恢复速度较慢。

2.3 混合持久化(Redis 4.0+)

混合持久化结合了RDB和AOF的优点。在AOF重写时,子进程将内存数据以RDB格式写入AOF文件,然后主进程将重写缓冲区中的增量命令以AOF格式追加到文件。这样得到的文件既包含RDB的数据,也包含增量命令,恢复时先加载RDB部分,再执行增量命令。

三、Redis淘汰策略

当内存不足时,Redis会根据配置的淘汰策略删除一些key。Redis提供了8种淘汰策略:

  1. noeviction:不删除,当内存不足时,新写入操作会报错(默认)。
  2. allkeys-lru:从所有key中删除最近最少使用的key。
  3. volatile-lru:从设置了过期时间的key中删除最近最少使用的key。
  4. allkeys-random:从所有key中随机删除。
  5. volatile-random:从设置了过期时间的key中随机删除。
  6. volatile-ttl:从设置了过期时间的key中删除剩余时间最短的key。
  7. allkeys-lfu:从所有key中删除最不经常使用的key(4.0+)。
  8. volatile-lfu:从设置了过期时间的key中删除最不经常使用的key(4.0+)。

四、Redis集群模式

4.1 主从复制

主从复制实现数据的备份和读写分离。主节点负责写,从节点负责读。当主节点宕机时,需要手动将一个从节点提升为主节点。

4.2 哨兵模式(Sentinel)

哨兵模式是主从复制的升级版,引入了哨兵集群来监控主从节点,当主节点故障时,自动将一个从节点升级为主节点。

哨兵的功能

  • 监控:检查主从节点是否正常工作。
  • 通知:当被监控的节点出现问题时,向管理员发送通知。
  • 自动故障转移:当主节点故障时,自动将一个从节点升级为主节点,并让其他从节点复制新的主节点。

4.3 集群模式(Cluster)

Redis集群采用去中心化的架构,数据分片存储在多个节点上,每个节点存储一部分数据。集群通过哈希槽(hash slot)来分配数据,共有16384个槽。

集群的特点

  • 数据分片:每个节点负责一部分哈希槽。
  • 高可用:每个分片可以有一个或多个从节点,当主节点故障时,从节点可以提升为主节点。
  • 无中心节点:每个节点都保存整个集群的状态,并通过Gossip协议进行通信。

集群的写操作

  • 客户端向集群中的任意节点发送命令,如果该节点不是负责该键的节点,会返回MOVED错误,并指引客户端转向正确的节点。

五、Redis核心数据结构底层原理

5.1 简单动态字符串(SDS)

Redis没有直接使用C字符串,而是自己构建了简单动态字符串(Simple Dynamic String,SDS)。

SDS结构

c

struct sdshdr {
    int len;        // 已使用的长度
    int free;       // 未使用的长度
    char buf[];     // 字节数组
};

优点

  • 常数复杂度获取字符串长度。
  • 避免缓冲区溢出。
  • 减少修改字符串时内存重分配的次数(通过预分配和惰性释放)。
  • 二进制安全。

5.2 字典(Hash)

字典使用哈希表实现,使用链地址法解决哈希冲突。当负载因子过高时,会进行渐进式rehash。

渐进式rehash

  • 为了避免rehash期间服务停顿,rehash是分多次、渐进式地进行的。
  • 在rehash期间,同时使用两个哈希表,查找、更新、删除操作会在两个表上进行,而新增操作只会在新表上进行。

5.3 跳跃表(Skip List)

跳跃表是一种有序数据结构,支持平均O(logN)复杂度的节点查找。

Redis使用跳跃表作为有序集合(Sorted Set)的底层实现之一(当元素数量较多或元素较长时)。

5.4 压缩列表(ZipList)

压缩列表是为了节约内存而开发的顺序型数据结构。当列表或哈希的元素较少且较小时,Redis会使用压缩列表作为底层实现。

5.5 快速列表(QuickList)

快速列表是列表(List)的底层实现,它是由压缩列表组成的双向链表。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

六、热点key和大key问题

6.1 热点key

热点key是指被高频访问的key,可能会导致单个节点负载过高。

解决方案

  1. 使用本地缓存:在应用层使用本地缓存(如Guava Cache)缓存热点key,减少对Redis的访问。
  2. 分散热点:将热点key拆分成多个key,分散到不同节点。
  3. 使用Redis集群:将热点key分配到不同的节点。

6.2 大key

大key是指key对应的value很大,如一个list包含数百万个元素。大key会导致操作缓慢,网络阻塞,甚至导致内存不足。

解决方案

  1. 拆分大key:将大key拆分成多个小key。
  2. 使用适合的数据结构:例如,如果使用一个string存储一个大对象,可以考虑使用hash来存储。
  3. 定期清理:对于不需要的大key,定期删除。

七、Redis集群高可用

7.1 故障检测与转移

在哨兵模式和集群模式中,Redis都实现了故障检测和自动转移。

哨兵模式的故障转移步骤

  1. 主观下线:一个哨兵发现主节点不可用。
  2. 客观下线:多个哨兵确认主节点不可用。
  3. 选举领头哨兵:由领头哨兵执行故障转移。
  4. 故障转移:选择一个从节点升级为主节点,并让其他从节点复制新的主节点。

集群模式的故障转移步骤

  1. 主观下线:集群中的每个节点都会定期向其他节点发送PING消息,如果目标节点未在指定时间内回复,则标记为主观下线。
  2. 客观下线:当超过半数的主节点都标记某个主节点为主观下线时,则标记为客观下线。
  3. 故障转移:从该主节点的从节点中选举一个成为新的主节点。

7.2 集群扩展与缩容

扩容

  1. 准备新节点。
  2. 使用redis-cli --cluster add-node命令将新节点加入集群。
  3. 重新分配哈希槽。

缩容

  1. 将待删除节点上的哈希槽迁移到其他节点。
  2. 使用redis-cli --cluster del-node命令删除节点。

八、Redis分布式锁

8.1 实现原理

使用SET命令的NXEX选项来实现:

redis

SET lock_key unique_value NX EX 30
  • NX:只有键不存在时才对键进行设置操作。
  • EX:设置键的过期时间为30秒。

8.2 释放锁

释放锁时,需要验证锁的值是否为当前客户端设置的值,避免释放其他客户端的锁。可以使用Lua脚本来保证原子性:

lua

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

8.3 锁的续期(Redlock算法)

Redlock算法是Redis官方提出的分布式锁算法,用于在多个Redis实例上实现分布式锁。但该算法也存在争议,需要根据具体场景选择。

在实际应用中,也可以使用Redisson客户端,它实现了可重入锁、锁的续期等功能。

九、深度面试题解析

9.1 Redis为什么快?

  1. 基于内存操作。
  2. 单线程避免上下文切换和竞争条件。
  3. IO多路复用。
  4. 高效的数据结构。

9.2 Redis的持久化策略如何选择?

  • 如果对数据安全性要求高,应该同时使用RDB和AOF,或者使用混合持久化。
  • 如果可以承受数分钟的数据丢失,可以只使用RDB。
  • 不推荐只使用AOF,因为RDB更适合备份和快速恢复。

9.3 Redis集群如何保证数据一致性?

Redis集群采用异步复制,当客户端向主节点写入数据后,主节点会立即返回,然后异步地将数据复制给从节点。因此,在故障转移时,可能会丢失一部分数据。如果需要强一致性,可以使用Redis的WAIT命令,但会降低性能。

9.4 如何解决Redis的并发竞争问题?

并发竞争是指多个客户端同时修改同一个key。可以使用以下方法:

  1. 使用事务(WATCH/MULTI/EXEC)来监控key,如果被其他客户端修改,则当前事务不执行。
  2. 使用分布式锁,确保同一时间只有一个客户端可以修改key。

9.5 Redis的内存优化策略有哪些?

  1. 使用合适的数据结构。
  2. 使用对象共享(对于小整数等)。
  3. 使用内存淘汰策略。
  4. 使用位操作和位图来存储布尔值。
  5. 使用哈希、列表、集合、有序集合的压缩编码。

十、总结

Redis是一个功能丰富、性能优异的内存数据库。在实际使用中,需要根据业务场景选择合适的持久化策略、淘汰策略和集群模式。同时,要注意热点key和大key问题,保证集群的高可用。在分布式锁的实现上,要注意锁的安全性、死锁和锁的续期问题。

以上是对Redis相关知识的深度解析,希望对你的面试准备有所帮助。如果需要进一步探讨某个问题,请随时提问。

Redis深度面试题解

一、Redis核心架构与线程模型深度解析

1.1 Redis单线程模型原理

1.1.1 传统单线程模型(Redis 6.0前)

核心设计哲学:
"CPU不是瓶颈,内存和网络才是"  - Redis作者Salvatore Sanfilippo

c

// 简化的事件循环伪代码
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        // 1. 处理定时事件(如过期key清理)
        aeProcessEvents(eventLoop, AE_TIME_EVENTS);
        
        // 2. 处理文件事件(网络IO)
        aeProcessEvents(eventLoop, AE_FILE_EVENTS);
    }
}

// 事件处理流程
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
    // 获取就绪的事件
    int numevents = aeApiPoll(eventLoop, tvp);
    
    for (int j = 0; j < numevents; j++) {
        aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
        
        // 处理读事件(客户端请求)
        if (fe->mask & mask & AE_READABLE) {
            fe->rfileProc(eventLoop, fd, fe->clientData, mask);
        }
        
        // 处理写事件(响应客户端)
        if (fe->mask & mask & AE_WRITABLE) {
            fe->wfileProc(eventLoop, fd, fe->clientData, mask);
        }
    }
}

单线程优势分析:

  1. 无锁设计:避免线程切换、锁竞争开销
  2. 原子性保证:所有操作天然原子,无需额外同步
  3. 代码简洁:避免复杂的并发控制逻辑
  4. 缓存友好:数据局部性好,CPU缓存命中率高

性能数据支撑:

text

单线程Redis QPS测试结果(8核CPU,16GB内存):
- SET操作:约120,000次/秒
- GET操作:约180,000次/秒
- LPUSH操作:约150,000次/秒
- LRANGE(100元素):约80,000次/秒
1.1.2 Redis 6.0+多线程模型

引入背景:  网络IO成为瓶颈(特别是大value或高并发场景)

多线程IO架构:

text

主线程(单线程,负责命令执行)
    ↓ 分发任务
IO线程池(多个线程,负责网络读写)
    ↓
网络连接(客户端连接)

配置参数:
io-threads 4          # IO线程数(不包括主线程)
io-threads-do-reads no # 是否启用读多线程(默认关闭)

多线程实现细节:

c

// Redis 6.0多线程IO核心逻辑
void initThreadedIO(void) {
    // 创建IO线程
    for (int i = 0; i < server.io_threads_num; i++) {
        pthread_create(&io_threads[i], NULL, IOThreadMain, (void*)(long)i);
    }
}

void *IOThreadMain(void *myid) {
    long id = (unsigned long)myid;
    
    while(1) {
        // 等待主线程分配任务
        for (int j = 0; j < server.io_threads_num; j++) {
            if (io_threads_pending[id] == 0) {
                pthread_mutex_lock(&io_threads_mutex[id]);
                pthread_cond_wait(&io_threads_cond[id], &io_threads_mutex[id]);
                pthread_mutex_unlock(&io_threads_mutex[id]);
            }
        }
        
        // 处理读/写任务
        listIter li;
        listNode *ln;
        listRewind(io_threads_list[id], &li);
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            if (io_threads_op == IO_THREADS_OP_READ) {
                readQueryFromClient(c->conn);
            } else {
                writeToClient(c, 0);
            }
        }
    }
}

多线程使用场景:

  1. 大value场景:value > 10KB,网络传输耗时明显
  2. 高并发场景:QPS > 10万,网络IO成为瓶颈
  3. 高带宽场景:千兆/万兆网络,可充分利用多线程

1.2 事件驱动模型深度解析

1.2.1 多路复用技术选型

Redis适配不同操作系统的多路复用:

c

#ifdef HAVE_EVPORT
#include "ae_evport.c"
#elif defined(HAVE_EPOLL)
#include "ae_epoll.c"
#elif defined(HAVE_KQUEUE)
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif

不同多路复用技术对比:

技术时间复杂度最大连接数内存拷贝适用场景
selectO(n)1024每次全量拷贝低并发,跨平台
pollO(n)无限制每次全量拷贝连接数中等
epollO(1)无限制共享内存Linux高并发
kqueueO(1)无限制共享内存BSD/MacOS
IOCPO(1)无限制共享内存Windows
1.2.2 事件处理优化技巧

1. 时间事件处理优化:

c

// 时间事件链表,按执行时间排序
typedef struct aeTimeEvent {
    long long id;           // 事件ID
    long when_sec;          // 秒
    long when_ms;           // 毫秒
    aeTimeProc *timeProc;   // 处理函数
    aeEventFinalizerProc *finalizerProc;
    void *clientData;       // 客户端数据
    struct aeTimeEvent *next;
} aeTimeEvent;

// 优化:最近要执行的时间事件缓存
long long aeSearchNearestTimer(aeEventLoop *eventLoop) {
    aeTimeEvent *te = eventLoop->timeEventHead;
    long long nearest = LONG_LONG_MAX;
    
    while(te) {
        long long now = getCurrentTime();
        if (te->when_sec < now_sec || 
            (te->when_sec == now_sec && te->when_ms < now_ms)) {
            // 需要立即执行
            return 0;
        }
        
        long long diff = (te->when_sec - now_sec) * 1000 
                       + (te->when_ms - now_ms);
        if (diff < nearest) {
            nearest = diff;
        }
        te = te->next;
    }
    return nearest;
}

2. 文件事件批处理:

c

// 一次处理多个事件,减少系统调用
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    int numevents = 0;
    
    // 获取就绪事件
    int retval = epoll_wait(state->epfd, state->events, eventLoop->setsize,
                           tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
    
    // 处理事件
    for (int j = 0; j < retval; j++) {
        int mask = 0;
        struct epoll_event *e = state->events+j;
        
        if (e->events & EPOLLIN) mask |= AE_READABLE;
        if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
        if (e->events & EPOLLERR) mask |= AE_WRITABLE;
        if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
        
        eventLoop->fired[j].fd = e->data.fd;
        eventLoop->fired[j].mask = mask;
    }
    
    return retval;
}

1.3 性能调优实践

1.3.1 网络参数优化

bash

# redis.conf配置优化
# 连接相关
maxclients 10000                     # 最大连接数
tcp-backlog 511                      # TCP连接队列大小
tcp-keepalive 300                    # TCP keepalive时间

# 网络缓冲区
client-output-buffer-limit normal 0 0 0  # 普通客户端无限制
client-output-buffer-limit replica 256mb 64mb 60  # 从节点限制
client-output-buffer-limit pubsub 32mb 8mb 60     # 发布订阅限制

# 多线程IO(Redis 6.0+)
io-threads 4                          # IO线程数(建议CPU核数-1)
io-threads-do-reads yes               # 启用读多线程(大value场景)
1.3.2 内存与性能平衡

bash

# 内存分配策略
maxmemory 16gb                        # 最大内存限制
maxmemory-policy allkeys-lru          # 淘汰策略
maxmemory-samples 10                  # LRU采样精度

# 内存碎片优化
activedefrag yes                      # 开启主动碎片整理
active-defrag-ignore-bytes 100mb      # 碎片超过100MB时整理
active-defrag-threshold-lower 10      # 碎片率>10%时整理
active-defrag-threshold-upper 100     # 碎片率>100%时整理
active-defrag-cycle-min 25            # 最小CPU使用率
active-defrag-cycle-max 75            # 最大CPU使用率
1.3.3 客户端连接优化

java

// Jedis连接池优化配置
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(100);              // 最大连接数
config.setMaxIdle(20);                // 最大空闲连接
config.setMinIdle(5);                 // 最小空闲连接
config.setMaxWaitMillis(1000);        // 获取连接最大等待时间
config.setTestOnBorrow(true);         // 借出时测试连接
config.setTestOnReturn(true);         // 归还时测试连接
config.setTestWhileIdle(true);        // 空闲时测试连接
config.setMinEvictableIdleTimeMillis(30000); // 最小空闲时间
config.setTimeBetweenEvictionRunsMillis(30000); // 清理间隔

// Lettuce连接优化(异步客户端)
RedisClient client = RedisClient.create("redis://localhost");
client.setOptions(ClientOptions.builder()
    .socketOptions(SocketOptions.builder()
        .connectTimeout(Duration.ofSeconds(2))
        .keepAlive(true)
        .build())
    .timeoutOptions(TimeoutOptions.builder()
        .fixedTimeout(Duration.ofSeconds(1))
        .build())
    .publishOnScheduler(true)          // 使用独立线程处理响应
    .build());

二、Redis持久化机制深度解析

2.1 RDB持久化机制

2.1.1 RDB文件格式解析

RDB文件结构:

text

|-------------------|
| REDIS             |  # 5字节,魔数"REDIS"
|-------------------|
| RDB_VERSION       |  # 4字节,版本号(如"0009")
|-------------------|
| AUX_FIELDS        |  # 辅助字段(可选)
|-------------------|
| DATABASES         |  # 数据库数据
|   0xFE            |  # 数据库选择器(0xFE)
|   DB_NUMBER       |  # 数据库号(1字节)
|   KEY_VALUE_PAIRS |  # 键值对数据
|       0xFD        |  # 过期时间(秒级)
|       TIMESTAMP   |  # 8字节时间戳
|  或 0xFC          |  # 过期时间(毫秒级)
|       TIMESTAMP   |  # 8字节时间戳
|       TYPE        |  # 值类型(1字节)
|       KEY         |  # 键
|       VALUE       |  # 值
|       0xFF        |  # 数据库结束标记
|-------------------|
| EOF               |  # 结束符(0xFF)
|-------------------|
| CHECKSUM          |  # 8字节CRC64校验和(可选)
|-------------------|

RDB值类型编码:

c

#define RDB_TYPE_STRING 0    // 字符串
#define RDB_TYPE_LIST   1    // 列表
#define RDB_TYPE_SET    2    // 集合
#define RDB_TYPE_ZSET   3    // 有序集合
#define RDB_TYPE_HASH   4    // 哈希
#define RDB_TYPE_ZSET_2 5    // ZSET版本2
#define RDB_TYPE_MODULE 6    // 模块
#define RDB_TYPE_MODULE_2 7  // 模块版本2
#define RDB_TYPE_HASH_ZIPMAP 9      // 已废弃
#define RDB_TYPE_LIST_ZIPLIST 10    // 压缩列表
#define RDB_TYPE_SET_INTSET 11      // 整数集合
#define RDB_TYPE_ZSET_ZIPLIST 12    // 压缩有序集合
#define RDB_TYPE_HASH_ZIPLIST 13    // 压缩哈希
#define RDB_TYPE_LIST_QUICKLIST 14  // 快速列表
#define RDB_TYPE_STREAM_LISTPACKS 15 // 流(listpack)
2.1.2 RDB生成过程深度分析

SAVE vs BGSAVE:

c

// SAVE命令实现(阻塞)
void saveCommand(client *c) {
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
        return;
    }
    
    // 同步保存,会阻塞所有客户端
    if (rdbSave(server.rdb_filename) == C_OK) {
        addReply(c,shared.ok);
    } else {
        addReply(c,shared.err);
    }
}

// BGSAVE命令实现(后台)
void bgsaveCommand(client *c) {
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
    } else if (server.aof_child_pid != -1) {
        addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress");
    } else if (rdbSaveBackground(server.rdb_filename) == C_OK) {
        addReplyStatus(c,"Background saving started");
    } else {
        addReply(c,shared.err);
    }
}

// 后台保存核心逻辑
int rdbSaveBackground(char *filename) {
    pid_t childpid;
    
    if ((childpid = fork()) == 0) {
        // 子进程
        closeListeningSockets(0);
        redisSetProcTitle("redis-rdb-bgsave");
        
        // 执行RDB保存
        if (rdbSave(filename) == C_OK) {
            exitFromChild(0);
        } else {
            exitFromChild(1);
        }
    } else {
        // 父进程
        if (childpid == -1) {
            return C_ERR;
        }
        server.rdb_child_pid = childpid;
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
        return C_OK;
    }
}

写时复制(Copy-On-Write)优化:

c

// Redis利用Linux的Copy-On-Write机制
// 父进程(主进程)          子进程(BGSAVE进程)
// 共享内存页(只读)
// 当父进程修改数据时:
// 1. 内核检测到写操作
// 2. 复制被修改的页(4KB)
// 3. 父进程修改复制后的页
// 4. 子进程继续使用原页

// 内存使用估算公式:
// 子进程内存 ≈ 父进程内存 × 写比例 × 持续时间
// 例如:16GB内存,50%写操作,持续10秒
// 子进程内存 ≈ 16GB × 0.5 × (10s × 写速率)
2.1.3 RDB配置与优化

bash

# redis.conf RDB配置
# 自动保存条件
save 900 1      # 900秒内至少有1个key变化
save 300 10     # 300秒内至少有10个key变化  
save 60 10000   # 60秒内至少有10000个key变化

# RDB文件配置
dbfilename dump.rdb          # RDB文件名
dir /var/lib/redis           # 保存目录
rdbcompression yes           # 启用压缩(LZF算法)
rdbchecksum yes              # 启用校验和

# 性能优化
rdb-save-incremental-fsync yes  # 增量式fsync,减少延迟
rdb-del-sync-files no           # 是否删除同步中的临时文件

# 监控指标
rdb_last_save_time:1630000000    # 上次保存时间戳
rdb_changes_since_last_save:1500 # 上次保存后的修改数
rdb_last_bgsave_status:ok        # 上次bgsave状态
rdb_last_bgsave_time_sec:3       # 上次bgsave耗时(秒)
rdb_current_bgsave_time_sec:-1   # 当前bgsave已耗时

2.2 AOF持久化机制

2.2.1 AOF文件格式与重写

AOF命令格式:

bash

# 原始命令:SET key value
*3          # 参数个数
$3          # 第一个参数长度
SET         # 命令
$3          # 第二个参数长度
key         # key
$5          # 第三个参数长度
value       # value

AOF重写(Rewrite)过程:

c

// AOF重写伪代码
void rewriteAppendOnlyFile(char *filename) {
    // 1. 创建临时文件
    FILE *fp = fopen(tmpfile, "w");
    
    // 2. 遍历数据库
    for (int j = 0; j < server.dbnum; j++) {
        // 跳过空数据库
        if (dictSize(server.db[j].dict) == 0) continue;
        
        // 写入SELECT命令
        fprintf(fp, "*2\r\n$6\r\nSELECT\r\n$%d\r\n%d\r\n", ...);
        
        // 3. 遍历键空间
        dictIterator *di = dictGetIterator(server.db[j].dict);
        dictEntry *de;
        while((de = dictNext(di)) != NULL) {
            sds keystr = dictGetKey(de);
            robj *o = dictGetVal(de);
            long long expiretime = getExpire(server.db[j], keystr);
            
            // 4. 写入过期时间(如果有)
            if (expiretime != -1) {
                fprintf(fp, "*3\r\n$9\r\nPEXPIREAT\r\n$%d\r\n%s\r\n$%d\r\n%lld\r\n",
                        ...);
            }
            
            // 5. 根据类型写入命令
            switch(o->type) {
                case OBJ_STRING:
                    // 写入SET命令
                    fprintf(fp, "*3\r\n$3\r\nSET\r\n$%d\r\n%s\r\n$%d\r\n%s\r\n",
                            ...);
                    break;
                case OBJ_LIST:
                    // 写入RPUSH命令
                    // 遍历列表元素
                    break;
                // ... 其他类型类似
            }
        }
        dictReleaseIterator(di);
    }
    
    // 6. 刷盘并重命名
    fflush(fp);
    fsync(fileno(fp));
    fclose(fp);
    rename(tmpfile, filename);
}
2.2.2 AOF同步策略深度分析

三种同步策略实现差异:

c

// 1. appendfsync always(每次写都同步)
void flushAppendOnlyFile(int force) {
    if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
        // 每次写入都调用fsync
        latencyStartMonitor(latency);
        redis_fsync(server.aof_fd); // 阻塞调用
        latencyEndMonitor(latency);
        latencyAddSampleIfNeeded("aof-fsync-always", latency);
        server.aof_last_fsync = server.unixtime;
    }
}

// 2. appendfsync everysec(每秒同步)
void flushAppendOnlyFile(int force) {
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC) {
        // 每秒同步一次
        if (server.aof_flush_postponed_start) {
            // 有延迟的刷盘
            long long ms = mstime() - server.aof_flush_postponed_start;
            if (ms < 2000) {
                return; // 2秒内只同步一次
            }
        }
        
        // 后台线程刷盘(如果支持)
        if (sync_in_progress) {
            if (server.aof_flush_postponed_start == 0) {
                server.aof_flush_postponed_start = server.unixtime;
                return;
            }
        }
        
        // 执行同步
        aof_background_fsync(server.aof_fd);
        server.aof_last_fsync = server.unixtime;
    }
}

// 3. appendfsync no(由操作系统决定)
void flushAppendOnlyFile(int force) {
    if (server.aof_fsync == AOF_FSYNC_NO) {
        // 不主动调用fsync,依赖操作系统刷新
        // 通常30秒刷新一次(Linux默认)
        // 性能最好,但数据丢失风险最大
        server.aof_last_fsync = server.unixtime;
    }
}

AOF性能对比数据:

策略吞吐量数据丢失风险适用场景
always最低(~10K ops/sec)几乎为0金融交易、计费系统
everysec中等(~50K ops/sec)最多1秒电商、社交应用
no最高(~80K ops/sec)最多30秒缓存、日志收集
2.2.3 AOF重写优化策略

增量重写(AOF Rewrite)优化:

c

// AOF重写缓冲区(Incremental AOF)
struct aofRewriteBuffer {
    list *blocks;           // 数据块链表
    size_t block_size;      // 块大小(默认10MB)
    size_t used;            // 已使用大小
};

// 重写期间的增量写入
void aofRewriteBufferAppend(unsigned char *s, size_t len) {
    // 将重写期间的新命令追加到缓冲区
    
    // 当缓冲区满时,写入临时文件
    if (server.aof_rewrite_buffer_blocks->used + len > server.aof_rewrite_buffer_size) {
        aofRewriteBufferWriteToFile();
    }
    
    // 追加到内存缓冲区
    memcpy(server.aof_rewrite_buffer_blocks->current_block + 
           server.aof_rewrite_buffer_blocks->used, s, len);
    server.aof_rewrite_buffer_blocks->used += len;
}

// 重写完成后合并
void aofRewriteBufferComplete(void) {
    // 1. 将原始AOF文件与重写缓冲区合并
    // 2. 确保数据一致性
    // 3. 原子替换旧AOF文件
}

2.3 混合持久化(RDB+AOF)

2.3.1 混合持久化实现原理

文件格式:

text

|---------------------------|
| RDB数据部分               |  # 完整的RDB格式数据
|---------------------------|
| AOF增量命令部分           |  # 重写开始后的AOF命令
|---------------------------|

启用混合持久化:

bash

# redis.conf配置
aof-use-rdb-preamble yes  # Redis 4.0+,默认yes

# 混合持久化文件示例
# 前部分是RDB二进制格式
# 后部分是AOF文本格式
2.3.2 混合持久化优势分析

1. 启动速度优化:

text

传统AOF恢复:需要重放所有AOF命令(O(N))
混合持久化恢复:先加载RDB快照(O(1)),再重放增量命令(O(M),M<<N)
恢复时间对比:16GB数据
- 纯AOF:约3-5分钟
- 混合持久化:约30-60

2. 文件大小优化:

text

数据示例:1000万用户session数据
- 纯RDB:约2GB(压缩后)
- 纯AOF:约8GB(命令格式冗余)
- 混合持久化:约2.1GB(RDB快照 + 少量AOF命令)
2.3.3 混合持久化配置实践

bash

# 生产环境推荐配置
# 基本持久化配置
save 900 1
save 300 10
save 60 10000

# AOF配置
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec

# 混合持久化
aof-use-rdb-preamble yes

# AOF重写配置
auto-aof-rewrite-percentage 100   # 增长100%触发重写
auto-aof-rewrite-min-size 64mb    # 最小64MB触发重写

# 性能优化
aof-rewrite-incremental-fsync yes
no-appendfsync-on-rewrite no

# 监控指标
INFO persistence
# aof_enabled:1
# aof_rewrite_in_progress:0
# aof_last_rewrite_time_sec:5
# aof_current_rewrite_time_sec:-1
# aof_last_bgrewrite_status:ok
# aof_last_write_status:ok

三、Redis内存淘汰策略深度解析

3.1 八种淘汰策略实现原理

3.1.1 策略分类与选择

c

// Redis淘汰策略枚举
#define MAXMEMORY_VOLATILE_LRU 0       // 对设置了过期时间的key使用LRU
#define MAXMEMORY_VOLATILE_LFU 1       // 对设置了过期时间的key使用LFU
#define MAXMEMORY_VOLATILE_TTL 2       // 删除过期时间最小的key
#define MAXMEMORY_VOLATILE_RANDOM 3    // 随机删除设置了过期时间的key
#define MAXMEMORY_ALLKEYS_LRU 4        // 对所有key使用LRU
#define MAXMEMORY_ALLKEYS_LFU 5        // 对所有key使用LFU
#define MAXMEMORY_ALLKEYS_RANDOM 6     // 随机删除所有key
#define MAXMEMORY_NO_EVICTION 7        // 不删除,返回错误
3.1.2 LRU算法实现与优化

传统LRU问题:

  • 需要维护链表,每次访问需移动节点(O(1)但常数大)
  • 需要额外内存存储链表指针

Redis近似LRU实现:

c

// Redis对象中的LRU时钟(24位)
typedef struct redisObject {
    unsigned type:4;        // 类型
    unsigned encoding:4;    // 编码
    unsigned lru:LRU_BITS;  // LRU时间(秒)或LFU计数
    int refcount;           // 引用计数
    void *ptr;              // 指向实际数据
} robj;

// LRU时钟更新(每10秒更新一次)
unsigned int LRU_CLOCK(void) {
    return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
}

// 获取对象空闲时间
unsigned long long estimateObjectIdleTime(robj *o) {
    unsigned long long lruclock = LRU_CLOCK();
    if (lruclock >= o->lru) {
        return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
    } else {
        return (lruclock + (LRU_CLOCK_MAX - o->lru)) * LRU_CLOCK_RESOLUTION;
    }
}

// 近似LRU采样淘汰
robj *evictionPoolPopulate(dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
    int j, k, count;
    dictEntry *samples[server.maxmemory_samples];
    
    // 随机采样
    count = dictGetSomeKeys(sampledict, samples, server.maxmemory_samples);
    
    for (j = 0; j < count; j++) {
        unsigned long long idle;
        robj *o;
        
        o = dictGetVal(samples[j]);
        idle = estimateObjectIdleTime(o);
        
        // 插入淘汰池(维护空闲时间最大的key)
        k = 0;
        while (k < EVPOOL_SIZE && pool[k].key && 
               pool[k].idle < idle) k++;
        
        if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
            // 池满且空闲时间不够大,跳过
            continue;
        } else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
            // 插入新条目
        } else {
            // 移动并插入
            if (pool[EVPOOL_SIZE-1].key == NULL) {
                // 移动
            } else {
                // 替换
            }
        }
    }
}
3.1.3 LFU算法实现(Redis 4.0+)

LFU实现技巧:

c

// LFU计数器(8位)存储在lru字段中
// 结构:16位分钟时间戳 + 8位计数器
//       00000000 00000000  00000000
//       └─分钟时间戳─┘ └─计数器─┘

// 访问频率更新
void updateLFU(robj *val) {
    unsigned long counter = LFUDecrAndReturn(val);
    counter = LFULogIncr(counter);
    val->lru = (LFUGetTimeInMinutes() << 8) | counter;
}

// 计数器衰减(随时间减少)
unsigned long LFUDecrAndReturn(robj *o) {
    unsigned long ldt = o->lru >> 8;
    unsigned long counter = o->lru & 255;
    unsigned long num_periods = server.lfu_decay_time ? 
        LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
    
    if (num_periods)
        counter = (num_periods > counter) ? 0 : counter - num_periods;
    
    return counter;
}

// 对数递增(新访问增加较少,频繁访问增加较多)
unsigned long LFULogIncr(unsigned long counter) {
    if (counter == 255) return 255;
    
    double r = (double)rand()/RAND_MAX;
    double baseval = counter - LFU_INIT_VAL;
    if (baseval < 0) baseval = 0;
    
    double p = 1.0/(baseval*server.lfu_log_factor+1);
    if (r < p) counter++;
    return counter;
}

LFU配置参数:

bash

# redis.conf LFU配置
lfu-log-factor 10    # 对数因子,越大counter增长越慢
lfu-decay-time 1     # 衰减时间(分钟),0表示不衰减

# LFU counter增长示例:
# 访问次数 | counter值
# 1        | 5
# 10       | 10
# 100      | 18
# 1000     | 31
# 10000    | 43

3.2 淘汰策略性能对比

3.2.1 不同场景策略选择

策略选择决策树:

text

是否有明确的数据重要性区分?
├─ 是 → 是否可接受随机淘汰?
│   ├─ 是 → volatile-random/allkeys-random
│   └─ 否 → 数据是否有过期时间?
│       ├─ 是 → volatile-lru/volatile-lfu/volatile-ttl
│       └─ 否 → allkeys-lru/allkeys-lfu
└─ 否 → 是否需要保证数据不丢失?
    ├─ 是 → noeviction + 监控报警
    └─ 否 → 根据访问模式选择LRU/LFU
3.2.2 性能测试数据

bash

# 测试环境:Redis 6.28核CPU,16GB内存
# 测试方法:YCSB基准测试,80%读20%写

# 结果对比(QPS):
策略                  | 命中率 | QPS     | 内存使用
---------------------|--------|---------|---------
noeviction           | 99.8%  | 120,000 | 持续增长
allkeys-lru          | 85.2%  | 105,000 | 稳定
allkeys-lfu          | 87.5%  | 108,000 | 稳定  
allkeys-random       | 72.3%  | 95,000  | 稳定
volatile-lru         | 83.1%  | 102,000 | 稳定
volatile-lfu         | 84.7%  | 106,000 | 稳定
3.2.3 生产环境配置建议

bash

# 缓存场景(可接受数据丢失)
maxmemory 12gb
maxmemory-policy allkeys-lru  # 通用场景
# 或
maxmemory-policy allkeys-lfu  # 有明显热点数据

# 持久化存储(部分数据可丢失)
maxmemory 14gb
maxmemory-policy volatile-lru
# 关键数据不设置过期时间,不会被淘汰

# 严格数据保护(不可丢失)
maxmemory 16gb
maxmemory-policy noeviction
# 配合监控,内存使用>85%时报警

# 采样精度调整(提高准确性)
maxmemory-samples 10  # 默认5,增加提高精度但消耗CPU

# 监控淘汰情况
redis-cli info stats | grep evicted
# evicted_keys:0           # 已淘汰key数
# evicted_keys_per_sec:0   # 每秒淘汰数

四、Redis集群模式深度解析

4.1 Redis Cluster架构设计

4.1.1 数据分片与哈希槽

哈希槽分配原理:

c

// Redis Cluster共有16384个槽(0-16383)
#define CLUSTER_SLOTS 16384

// 槽位分配算法
unsigned int keyHashSlot(char *key, int keylen) {
    int s, e; // 开始和结束位置
    
    // 查找第一个'{'
    for (s = 0; s < keylen; s++)
        if (key[s] == '{') break;
    
    // 如果找到'{',查找对应的'}'
    if (s != keylen) {
        for (e = s+1; e < keylen; e++)
            if (key[e] == '}') break;
        
        // 如果找到'}'并且中间有内容,使用hash tag
        if (e != keylen && e != s+1) {
            key = key + s + 1;
            keylen = e - s - 1;
        }
    }
    
    // 计算CRC16并取模
    return crc16(key, keylen) & (CLUSTER_SLOTS-1);
}

// Hash Tag示例:
// user:{1000}:profile → 使用1000计算槽位
// order:{5000}:items  → 使用5000计算槽位
// 这样可以保证相关数据在同一个节点

集群节点槽位分配:

bash

# 查看集群槽位分布
redis-cli -c cluster slots

# 输出示例:
1) 1) (integer) 0           # 起始槽
   2) (integer) 5460        # 结束槽
   3) 1) "192.168.1.101"    # 主节点IP
      2) (integer) 6379     # 端口
   4) 1) "192.168.1.102"    # 从节点IP
      2) (integer) 6379
2) 1) (integer) 5461
   2) (integer) 10922
   3) 1) "192.168.1.103"
      2) (integer) 6379
   4) 1) "192.168.1.104"
      2) (integer) 6379
3) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "192.168.1.105"
      2) (integer) 6379
   4) 1) "192.168.1.106"
      2) (integer) 6379
4.1.2 Gossip协议实现

节点间通信消息类型:

c

// 消息类型定义
#define CLUSTERMSG_TYPE_PING 0          // 心跳
#define CLUSTERMSG_TYPE_PONG 1          // 心跳响应
#define CLUSTERMSG_TYPE_MEET 2          // 请求加入集群
#define CLUSTERMSG_TYPE_FAIL 3          // 节点失败
#define CLUSTERMSG_TYPE_PUBLISH 4       // 发布订阅
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 5 // 故障转移投票请求
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 6     // 故障转移投票确认
#define CLUSTERMSG_TYPE_UPDATE 7        // 槽位信息更新
#define CLUSTERMSG_TYPE_MFSTART 8       // 手动故障转移开始

// Gossip消息结构
typedef struct {
    char sig[4];                        // 签名"RCmb"
    uint32_t totlen;                    // 消息总长度
    uint16_t ver;                       // 协议版本
    uint16_t port;                      // 发送方端口
    uint16_t type;                      // 消息类型
    uint16_t count;                     // 包含的节点数
    uint64_t currentEpoch;              // 当前纪元
    uint64_t configEpoch;               // 配置纪元
    uint64_t offset;                    // 复制偏移量
    char sender[CLUSTER_NAMELEN];       // 发送者节点ID
    unsigned char myslots[CLUSTER_SLOTS/8]; // 槽位位图
    char slaveof[CLUSTER_NAMELEN];      // 主节点ID
    char myip[NET_IP_STR_LEN];          // 发送者IP
    char notused1[34];                  // 未使用
    uint16_t cport;                     // 集群总线端口
    uint16_t flags;                     // 节点标志
    unsigned char state;                // 集群状态
    unsigned char mflags[3];            // 消息标志
    union clusterMsgData data;          // 消息数据
} clusterMsg;

// Gossip部分包含的随机节点信息
typedef struct {
    char nodename[CLUSTER_NAMELEN];     // 节点名
    uint32_t ping_sent;                 // 上次发送ping时间
    uint32_t pong_received;             // 上次收到pong时间
    char ip[NET_IP_STR_LEN];            // 节点IP
    uint16_t port;                      // 节点端口
    uint16_t cport;                     // 集群总线端口
    uint16_t flags;                     // 节点标志
    uint32_t notused;                   // 未使用
} clusterMsgDataGossip;

Gossip传播算法:

c

// 每次ping消息携带其他节点的gossip信息
void clusterSendPing(clusterLink *link, int type) {
    unsigned char *buf;
    clusterMsg *hdr;
    int gossipcount = 0;
    
    // 随机选择几个节点加入gossip部分
    int maxgossip = CLUSTER_SLOTS/32;  // 默认512个槽/32=16个节点
    int wanted = floor(maxgossip/2);   // 期望携带的gossip节点数
    
    // 构建候选节点列表
    dictEntry *de;
    dictIterator *di = dictGetSafeIterator(server.cluster->nodes);
    while((de = dictNext(di)) != NULL && gossipcount < wanted) {
        clusterNode *node = dictGetVal(de);
        
        // 排除自己和已断开连接的节点
        if (node == myself || node->flags & (CLUSTER_NODE_HANDSHAKE|CLUSTER_NODE_NOADDR))
            continue;
        
        // 根据最后通信时间排序,优先选择通信少的节点
        // 确保所有节点信息都能传播
        addNodeToGossipList(hdr, node, gossipcount++);
    }
    dictReleaseIterator(di);
    
    // 发送消息
    clusterSendMessage(link, buf, totlen);
}

4.2 集群高可用与故障转移

4.2.1 故障检测机制

主观下线与客观下线:

c

// 主观下线(PFAIL)
void clusterNodeAddFailureReport(clusterNode *failing, clusterNode *sender) {
    list *l = failing->fail_reports;
    listNode *ln;
    
    // 检查是否已有此发送者的报告
    listIter li;
    listRewind(l, &li);
    while((ln = listNext(&li))) {
        clusterNodeFailReport *report = listNodeValue(ln);
        if (report->node == sender) {
            // 更新时间
            report->time = mstime();
            return;
        }
    }
    
    // 添加新报告
    clusterNodeFailReport *report = zmalloc(sizeof(*report));
    report->node = sender;
    report->time = mstime();
    listAddNodeTail(l, report);
}

// 客观下线(FAIL)
int clusterNodeIsFailed(clusterNode *node) {
    int failures = 0;
    int needed_quorum = (server.cluster->size / 2) + 1;
    
    // 统计不同节点的失败报告
    if (listLength(node->fail_reports) >= needed_quorum) {
        // 检查报告是否过期
        mstime_t expire = mstime() - CLUSTER_FAIL_REPORT_VALIDITY_TIME;
        listIter li;
        listNode *ln;
        listRewind(node->fail_reports, &li);
        
        while((ln = listNext(&li))) {
            clusterNodeFailReport *report = listNodeValue(ln);
            if (report->time > expire) {
                failures++;
                if (failures >= needed_quorum) {
                    return 1;
                }
            }
        }
    }
    return 0;
}
4.2.2 故障转移流程

Raft-like选举算法:

c

// 故障转移流程
void clusterHandleSlaveFailover(void) {
    // 1. 检查是否满足故障转移条件
    if (!isSlaveFailoverAllowed()) return;
    
    // 2. 延迟开始(确保主节点真的故障)
    mstime_t delay = CLUSTER_SLAVE_FAILOVER_DELAY + 
                     random() % CLUSTER_SLAVE_FAILOVER_DELAY;
    
    if (server.cluster->failover_auth_time == 0) {
        server.cluster->failover_auth_time = mstime() + delay;
        return;
    }
    
    // 3. 请求投票
    if (server.cluster->failover_auth_time <= mstime()) {
        server.cluster->failover_auth_time = mstime() + 500 + random() % 500;
        clusterRequestFailoverAuth();
    }
}

// 请求投票
void clusterRequestFailoverAuth(void) {
    clusterMsg buf[1];
    clusterMsg *hdr = (clusterMsg*) buf;
    
    // 构建投票请求消息
    clusterBuildMessageHdr(hdr, CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST);
    
    // 发送给所有主节点
    dictIterator *di = dictGetIterator(server.cluster->nodes);
    dictEntry *de;
    while((de = dictNext(di)) != NULL) {
        clusterNode *node = dictGetVal(de);
        if (!node->master) continue;  // 只发送给主节点
        
        clusterSendMessage(node->link, (unsigned char*)buf, ntohl(hdr->totlen));
    }
    dictReleaseIterator(di);
}

// 处理投票
void clusterProcessFailoverAuthRequest(clusterMsg *hdr) {
    // 检查请求合法性
    // 1. 当前纪元必须相同或更高
    // 2. 故障主节点必须已客观下线
    // 3. 每个主节点在每个纪元只能投一票
    
    if (isValidFailoverRequest(hdr)) {
        // 发送投票确认
        clusterSendFailoverAuth(hdr->sender);
    }
}
4.2.3 集群脑裂与解决方案

Redis Cluster防脑裂机制:

bash

# 配置参数
cluster-require-full-coverage yes  # 需要所有槽被覆盖才能提供服务
cluster-node-timeout 15000         # 节点超时时间(毫秒)

# 最小主节点数
min-slaves-to-write 1              # 至少有一个从节点才接受写
min-slaves-max-lag 10              # 从节点延迟不超过10秒

网络分区处理:

text

场景:6节点集群(33从)发生网络分区
分区A:主节点A、从节点A1、从节点B1
分区B:主节点B、主节点C、从节点C1

处理过程:
1. 分区A:主节点A正常,从节点A1正常,从节点B1尝试故障转移
2. 分区B:主节点B、C正常,从节点C1正常
3. 网络恢复后:原主节点B变为从节点,B1成为新主节点
4. 数据同步:原主节点B同步B1的数据(可能丢失分区期间写入B的数据)

4.3 集群运维与监控

4.3.1 集群扩缩容

扩容流程:

bash

# 1. 准备新节点
redis-server /path/to/redis.conf

# 2. 加入集群
redis-cli --cluster add-node 192.168.1.107:6379 192.168.1.101:6379

# 3. 重新分片
redis-cli --cluster reshard 192.168.1.101:6379 --cluster-from <node-id> --cluster-to <node-id> --cluster-slots 1000 --cluster-yes

# 4. 添加从节点
redis-cli --cluster add-node 192.168.1.108:6379 192.168.1.101:6379 --cluster-slave --cluster-master-id <node-id>

缩容流程:

bash

# 1. 迁移槽位
redis-cli --cluster reshard 192.168.1.101:6379 --cluster-from <node-id> --cluster-to <node-id> --cluster-slots 1000 --cluster-yes

# 2. 删除节点
redis-cli --cluster del-node 192.168.1.101:6379 <node-id>
4.3.2 集群监控指标

bash

# 集群健康检查
redis-cli --cluster check 192.168.1.101:6379

# 集群信息
redis-cli cluster info

# 关键指标监控
cluster_state:ok                  # 集群状态
cluster_slots_assigned:16384      # 已分配槽数
cluster_slots_ok:16384            # 正常槽数
cluster_slots_pfail:0             # 疑似失败槽数
cluster_slots_fail:0              # 失败槽数
cluster_known_nodes:6             # 已知节点数
cluster_size:3                    # 主节点数
cluster_current_epoch:6           # 当前纪元
cluster_my_epoch:2                # 节点纪元
cluster_stats_messages_sent:248   # 发送消息数
cluster_stats_messages_received:236 # 接收消息数

# 节点监控
redis-cli cluster nodes
# 字段:节点ID、IP:端口、标志、最后ping发送时间、最后pong接收时间、
#       配置纪元、连接状态、槽位范围
4.3.3 集群性能优化

bash

# redis.conf集群优化配置
# 网络优化
cluster-node-timeout 15000               # 节点超时时间
cluster-replica-validity-factor 10       # 从节点有效性因子
cluster-migration-barrier 1              # 迁移屏障

# 性能优化
cluster-require-full-coverage yes        # 需要完整覆盖
repl-disable-tcp-nodelay no              # 启用TCP_NODELAY
repl-backlog-size 128mb                  # 复制积压缓冲区
repl-backlog-ttl 3600                    # 积压缓冲区TTL

# 安全配置
cluster-announce-ip 192.168.1.101        # 宣告IP
cluster-announce-port 6379               # 宣告端口
cluster-announce-bus-port 16379          # 集群总线端口

五、Redis核心数据结构底层原理

5.1 SDS(简单动态字符串)

5.1.1 SDS结构设计

SDS类型定义:

c

// Redis 3.2之前的SDS结构
struct sdshdr {
    unsigned int len;    // 已使用长度
    unsigned int free;   // 未使用长度
    char buf[];          // 字节数组
};

// Redis 3.2+优化后的SDS结构
// 根据字符串长度使用不同的结构
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; // 低3位存储类型,高5位存储长度
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;         // 已使用长度
    uint8_t alloc;       // 分配的总长度
    unsigned char flags; // 类型标志
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len;
    uint16_t alloc;
    unsigned char flags;
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len;
    uint32_t alloc;
    unsigned char flags;
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len;
    uint64_t alloc;
    unsigned char flags;
    char buf[];
};

// 类型定义
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
5.1.2 SDS优化策略

1. 内存预分配:

c

// 空间预分配策略
sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);      // 获取可用空间
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    
    // 如果空间足够,直接返回
    if (avail >= addlen) return s;
    
    len = sdslen(s);
    sh = (char*)s - sdsHdrSize(oldtype);
    newlen = (len + addlen);
    
    // 预分配策略:如果新长度小于1MB,分配2倍空间;否则多分配1MB
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    
    // 根据新长度选择SDS类型
    type = sdsReqType(newlen);
    
    // 类型5不支持扩容,需要升级
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    
    hdrlen = sdsHdrSize(type);
    if (oldtype == type) {
        // 类型不变,原地扩容
        newsh = s_realloc(sh, hdrlen + newlen + 1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh + hdrlen;
    } else {
        // 类型变化,需要重新分配
        newsh = s_malloc(hdrlen + newlen + 1);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh + hdrlen, s, len + 1);
        s_free(sh);
        s = (char*)newsh + hdrlen;
        s[-1] = type;
        sdssetlen(s, len);
    }
    sdssetalloc(s, newlen);
    return s;
}

2. 惰性空间释放:

c

// 缩短字符串时不立即释放内存
void sdsclear(sds s) {
    sdssetlen(s, 0);  // 只是将长度设为0,不释放内存
    s[0] = '\0';
}

// 真正释放多余空间
sds sdsRemoveFreeSpace(sds s) {
    void *sh, *newsh;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen, oldhdrlen = sdsHdrSize(oldtype);
    size_t len = sdslen(s);
    size_t avail = sdsavail(s);
    sh = (char*)s - oldhdrlen;
    
    // 没有多余空间,直接返回
    if (avail == 0) return s;
    
    // 根据实际长度重新分配
    type = sdsReqType(len);
    hdrlen = sdsHdrSize(type);
    
    if (oldtype == type) {
        newsh = s_realloc(sh, oldhdrlen + len + 1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh + oldhdrlen;
    } else {
        newsh = s_malloc(hdrlen + len + 1);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh + hdrlen, s, len + 1);
        s_free(sh);
        s = (char*)newsh + hdrlen;
        s[-1] = type;
        sdssetlen(s, len);
    }
    sdssetalloc(s, len);
    return s;
}

5.2 字典(Hash Table)

5.2.1 字典结构设计

Redis字典实现:

c

// 哈希表节点
typedef struct dictEntry {
    void *key;                  // 键
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;                        // 值
    struct dictEntry *next;     // 下一个节点(拉链法)
} dictEntry;

// 哈希表
typedef struct dictht {
    dictEntry **table;          // 节点数组
    unsigned long size;         // 表大小
    unsigned long sizemask;     // 掩码(size-1)
    unsigned long used;         // 已使用节点数
} dictht;

// 字典
typedef struct dict {
    dictType *type;             // 类型特定函数
    void *privdata;             // 私有数据
    dictht ht[2];               // 两个哈希表(用于rehash)
    long rehashidx;             // rehash索引(-1表示未进行)
    unsigned long iterators;    // 正在运行的迭代器数
} dict;

// 字典类型函数
typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);          // 哈希函数
    void *(*keyDup)(void *privdata, const void *key);   // 键复制
    void *(*valDup)(void *privdata, const void *obj);   // 值复制
    int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 键比较
    void (*keyDestructor)(void *privdata, void *key);   // 键销毁
    void (*valDestructor)(void *privdata, void *obj);   // 值销毁
} dictType;
5.2.2 渐进式Rehash

Rehash过程:

c

// 开始rehash
int dictRehash(dict *d, int n) {
    int empty_visits = n * 10;  // 最多访问的空桶数
    
    if (!dictIsRehashing(d)) return 0;
    
    while (n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        
        // 找到非空桶
        while (d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        
        // 迁移该桶的所有节点
        de = d->ht[0].table[d->rehashidx];
        while (de) {
            unsigned int h;
            
            nextde = de->next;
            
            // 计算新哈希表中的位置
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            
            // 插入新表
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            
            d->ht[0].used--;
            d->ht[1].used++;
            
            de = nextde;
        }
        
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }
    
    // 检查是否完成rehash
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;  // rehash完成
    }
    
    return 1;  // 还需要继续rehash
}

// 在每次操作时执行一步rehash
static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d, 1);
}

哈希表扩容/缩容条件:

c

// 扩容条件
if (d->ht[0].used >= d->ht[0].size && 
    (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) {
    return dictExpand(d, d->ht[0].used * 2);
}

// 缩容条件
if (d->ht[0].used > DICT_HT_INITIAL_SIZE && 
    d->ht[0].used < d->ht[0].size/4) {
    return dictResize(d);
}

5.3 跳跃表(Skip List)

5.3.1 跳跃表结构

Redis跳跃表实现:

c

// 跳跃表节点
typedef struct zskiplistNode {
    sds ele;                            // 成员(SDS)
    double score;                       // 分值
    struct zskiplistNode *backward;     // 后退指针
    struct zskiplistLevel {
        struct zskiplistNode *forward;  // 前进指针
        unsigned long span;             // 跨度
    } level[];                          // 柔性数组,表示层级
} zskiplistNode;

// 跳跃表
typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 头尾节点
    unsigned long length;                // 节点数量
    int level;                           // 最大层数
} zskiplist;

// 有序集合同时使用跳跃表和字典
typedef struct zset {
    dict *dict;          // 字典,用于O(1)查找
    zskiplist *zsl;      // 跳跃表,用于范围操作和排序
} zset;
5.3.2 跳跃表操作算法

插入算法:

c

zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;
    
    // 1. 查找插入位置
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        
        while (x->level[i].forward &&
               (x->level[i].forward->score < score ||
                (x->level[i].forward->score == score &&
                 sdscmp(x->level[i].forward->ele, ele) < 0))) {
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    
    // 2. 随机生成层数
    level = zslRandomLevel();
    if (level > zsl->level) {
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            update[i] = zsl->header;
            update[i]->level[i].span = zsl->length;
        }
        zsl->level = level;
    }
    
    // 3. 创建新节点
    x = zslCreateNode(level, score, ele);
    
    // 4. 插入节点并更新指针
    for (i = 0; i < level; i++) {
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;
        
        // 更新跨度
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }
    
    // 5. 更新未触及的层级跨度
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
    
    // 6. 设置后退指针
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    
    zsl->length++;
    return x;
}

// 随机层数生成算法(幂次定律)
int zslRandomLevel(void) {
    int level = 1;
    while ((random() & 0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

5.4 压缩数据结构

5.4.1 压缩列表(ZipList)

ZipList结构:

text

|--------|--------|--------|--------|--------|--------|
| zlbytes| zltail | zllen  | entry1 | entry2 | ...   | zlend |
|--------|--------|--------|--------|--------|--------|
          4字节    4字节    2字节   变长     变长      1字节

每个entry结构:
|--------|--------|--------|
| prevlen| encoding| content|
|--------|--------|--------|
  变长     变长     变长

ZipList编码:

c

// 字符串编码
if (len <= 63) {
    // |00pppppp| - 1字节,长度<=63
    encoding[0] = ZIP_STR_06B | len;
} else if (len <= 16383) {
    // |01pppppp|qqqqqqqq| - 2字节,长度<=16383
    encoding[0] = ZIP_STR_14B | ((len >> 8) & 0x3f);
    encoding[1] = len & 0xff;
} else {
    // |10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5字节
    encoding[0] = ZIP_STR_32B;
    encoding[1] = (len >> 24) & 0xff;
    encoding[2] = (len >> 16) & 0xff;
    encoding[3] = (len >> 8) & 0xff;
    encoding[4] = len & 0xff;
}

// 整数编码
if (value >= 0 && value <= 12) {
    // |1111xxxx| - 1字节,xxxx在0001-1101之间
    encoding[0] = ZIP_INT_4B | ((value - 1) & 0x0f);
} else if (value >= INT8_MIN && value <= INT8_MAX) {
    // |11111110| - 1字节标志,后跟1字节整数
    encoding[0] = ZIP_INT_8B;
} else if (value >= INT16_MIN && value <= INT16_MAX) {
    // |11000000| - 1字节标志,后跟2字节整数
    encoding[0] = ZIP_INT_16B;
} else if (value >= INT24_MIN && value <= INT24_MAX) {
    // |11110000| - 1字节标志,后跟3字节整数
    encoding[0] = ZIP_INT_24B;
} else if (value >= INT32_MIN && value <= INT32_MAX) {
    // |11010000| - 1字节标志,后跟4字节整数
    encoding[0] = ZIP_INT_32B;
} else {
    // |11100000| - 1字节标志,后跟8字节整数
    encoding[0] = ZIP_INT_64B;
}
5.4.2 快速列表(QuickList)

QuickList结构:

c

// 快速列表节点
typedef struct quicklistNode {
    struct quicklistNode *prev;  // 前驱节点
    struct quicklistNode *next;  // 后继节点
    unsigned char *zl;           // 指向压缩列表或packed ziplist
    unsigned int sz;             // ziplist字节大小
    unsigned int count : 16;     // ziplist元素数量
    unsigned int encoding : 2;   // RAW=1, LZF=2
    unsigned int container : 2;  // NONE=1, ZIPLIST=2
    unsigned int recompress : 1; // 是否被压缩
    unsigned int attempted_compress : 1; // 测试用
    unsigned int extra : 10;     // 预留位
} quicklistNode;

// 快速列表
typedef struct quicklist {
    quicklistNode *head;         // 头节点
    quicklistNode *tail;         // 尾节点
    unsigned long count;         // 所有元素总数
    unsigned long len;           // 节点数量
    int fill : 16;               // 每个节点最大元素数
    unsigned int compress : 16;  // 压缩深度,0表示不压缩
} quicklist;

QuickList配置参数:

bash

# redis.conf配置
list-max-ziplist-size -2         # 每个ziplist最大大小
# 正数:最大元素个数
# 负数:最大字节数
# -1: 4KB, -2: 8KB, -3: 16KB, -4: 32KB, -5: 64KB

list-compress-depth 0           # 压缩深度
# 0: 不压缩
# 1: 头尾各1个节点不压缩
# 2: 头尾各2个节点不压缩

六、热点Key与大Key问题深度解析

6.1 热点Key问题

6.1.1 热点Key检测方法

监控检测:

bash

# 1. 使用redis-cli监控
redis-cli --hotkeys
# 输出示例:
# 1) "key:user:1000:profile" - 5 hits
# 2) "key:product:5000:info" - 3 hits

# 2. 使用MONITOR命令(谨慎使用,性能影响大)
redis-cli monitor | grep -E "GET|SET|INCR" | head -100

# 3. 使用redis-rdb-tools分析RDB文件
rdb -c memory dump.rdb --bytes 128 -f memory.csv
awk -F, '{print $3,$4}' memory.csv | sort -nr -k2 | head -20

# 4. 使用Redis内置命令
redis-cli info stats | grep instantaneous_ops_per_sec
redis-cli slowlog get 10  # 查看慢查询

客户端埋点检测:

java

public class HotKeyDetector {
    private final ConcurrentHashMap<String, AtomicLong> counter = 
        new ConcurrentHashMap<>();
    private final ScheduledExecutorService scheduler = 
        Executors.newScheduledThreadPool(1);
    
    public HotKeyDetector() {
        // 每10秒统计一次
        scheduler.scheduleAtFixedRate(this::reportHotKeys, 10, 10, TimeUnit.SECONDS);
    }
    
    public void trackKey(String key) {
        counter.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet();
    }
    
    private void reportHotKeys() {
        long threshold = 1000; // 10秒内访问超过1000次
        counter.entrySet().stream()
            .filter(entry -> entry.getValue().get() > threshold)
            .forEach(entry -> {
                System.out.println("Hot Key: " + entry.getKey() + 
                                 ", Count: " + entry.getValue());
                // 上报到监控系统
                reportToMonitor(entry.getKey(), entry.getValue().get());
            });
        
        // 清空计数器
        counter.clear();
    }
}
6.1.2 热点Key解决方案

1. 本地缓存方案:

java

public class LocalCacheWithHotKey {
    private final Cache<String, Object> localCache = Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(5, TimeUnit.SECONDS)  // 短期缓存
        .recordStats()
        .build();
    
    private final RedisTemplate<String, Object> redisTemplate;
    
    public Object getWithLocalCache(String key) {
        // 1. 检查本地缓存
        Object value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // 2. 查询Redis
        value = redisTemplate.opsForValue().get(key);
        
        // 3. 如果是热点Key,放入本地缓存
        if (isHotKey(key)) {
            localCache.put(key, value);
        }
        
        return value;
    }
    
    private boolean isHotKey(String key) {
        // 基于历史访问频率判断
        // 或从监控系统获取热点Key列表
        return HotKeyMonitor.isHotKey(key);
    }
}

2. Key拆分方案:

java

public class HotKeySharding {
    // 原始热点Key
    // product:1000:info → 单个Key压力大
    
    // 拆分方案1:按字段拆分
    public Product getProductInfo(Long productId) {
        Map<String, Object> fields = new HashMap<>();
        fields.put("name", redisTemplate.opsForValue().get("product:" + productId + ":name"));
        fields.put("price", redisTemplate.opsForValue().get("product:" + productId + ":price"));
        fields.put("stock", redisTemplate.opsForValue().get("product:" + productId + ":stock"));
        // ... 其他字段
        return mapToProduct(fields);
    }
    
    // 拆分方案2:Hash分片
    public Product getProductInfoSharded(Long productId) {
        String hashKey = "product:" + (productId % 10);  // 分10片
        String field = "info:" + productId;
        return (Product) redisTemplate.opsForHash().get(hashKey, field);
    }
}

3. 多级缓存方案:

text

架构设计:
客户端 → Nginx缓存 → 应用本地缓存 → Redis集群 → DB
       (L1)         (L2)           (L3)        (L4)

缓存策略:
- L1(Nginx):5秒过期,命中率约30%
- L2(本地):30秒过期,命中率约50%
- L3(Redis):永不过期,命中率约95%
- L4(DB):持久化存储

6.2 大Key问题

6.2.1 大Key检测与诊断

大Key检测方法:

bash

# 1. 使用redis-cli --bigkeys
redis-cli --bigkeys
# 输出示例:
# Biggest string found 'user:session:xyz' has 1000000 bytes
# Biggest list   found 'task:queue' has 500000 items
# Biggest set    found 'user:tags' has 80000 members
# Biggest hash   found 'product:stats' has 30000 fields
# Biggest zset   found 'leaderboard' has 100000 members

# 2. 使用redis-rdb-tools分析
rdb --command memory dump.rdb --bytes 1024 > bigkeys.txt
# 输出每个Key的内存使用情况

# 3. 使用Redis内部命令
# 查看特定Key的大小
redis-cli debug object user:session:xyz
# 输出中的serializedlength表示序列化后的长度

# 4. 自定义脚本扫描
redis-cli scan 0 match "user:*" count 1000 | \
while read line; do 
    size=$(redis-cli memory usage "$line" | tail -1)
    if [ $size -gt 1048576 ]; then  # 大于1MB
        echo "Big Key: $line, Size: $size"
    fi
done
6.2.2 大Key拆分方案

Hash大Key拆分:

java

public class BigHashSplitter {
    // 原始大Hash:user:1000:profile 包含100个字段
    
    // 拆分方案1:按字段分组
    public void splitUserProfile(Long userId, Map<String, String> profile) {
        Map<String, Map<String, String>> groups = new HashMap<>();
        
        // 分组规则
        groups.put("user:" + userId + ":basic", 
            extractBasicInfo(profile));
        groups.put("user:" + userId + ":contact",
            extractContactInfo(profile));
        groups.put("user:" + userId + ":preference",
            extractPreferenceInfo(profile));
        
        // 分批存储
        for (Map.Entry<String, Map<String, String>> entry : groups.entrySet()) {
            redisTemplate.opsForHash().putAll(entry.getKey(), entry.getValue());
        }
    }
    
    // 拆分方案2:Hash分片
    public void putHashShard(String bigKey, String field, String value) {
        // 计算field的哈希值决定分片
        int shard = Math.abs(field.hashCode()) % 16;
        String shardKey = bigKey + ":shard:" + shard;
        redisTemplate.opsForHash().put(shardKey, field, value);
    }
    
    public String getHashShard(String bigKey, String field) {
        int shard = Math.abs(field.hashCode()) % 16;
        String shardKey = bigKey + ":shard:" + shard;
        return (String) redisTemplate.opsForHash().get(shardKey, field);
    }
}

List/Set大Key拆分:

java

public class BigListSplitter {
    // 原始大List:task:queue 包含100万个元素
    
    // 拆分方案:按时间或ID范围分片
    public void pushTask(Task task) {
        // 按小时分片
        String hourKey = "task:queue:" + 
            DateTimeFormatter.ofPattern("yyyyMMddHH").format(LocalDateTime.now());
        
        // 或按任务ID范围分片
        String rangeKey = "task:queue:shard:" + (task.getId() % 100);
        
        redisTemplate.opsForList().leftPush(hourKey, task);
    }
    
    public Task popTask() {
        // 轮询多个分片
        for (int i = 0; i < 24; i++) {
            String hourKey = "task:queue:" + 
                DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDateTime.now()) +
                String.format("%02d", i);
            
            Task task = (Task) redisTemplate.opsForList().rightPop(hourKey);
            if (task != null) {
                return task;
            }
        }
        return null;
    }
}
6.2.3 大Key删除优化

渐进式删除:

java

public class BigKeyDeleter {
    // 错误:直接删除大Key可能导致阻塞
    // redisTemplate.delete("big:key"); // 可能阻塞几秒甚至几十秒
    
    // 正确:渐进式删除
    public void deleteBigHash(String key) {
        // 方案1:分批删除Hash字段
        int batchSize = 100;
        String cursor = "0";
        
        do {
            ScanOptions options = ScanOptions.scanOptions()
                .count(batchSize)
                .match("*")
                .build();
            
            Cursor<Map.Entry<Object, Object>> cursorResult = 
                redisTemplate.opsForHash().scan(key, options);
            
            List<Object> fieldsToDelete = new ArrayList<>();
            while (cursorResult.hasNext()) {
                fieldsToDelete.add(cursorResult.next().getKey());
                if (fieldsToDelete.size() >= batchSize) {
                    redisTemplate.opsForHash().delete(key, 
                        fieldsToDelete.toArray(new Object[0]));
                    fieldsToDelete.clear();
                    Thread.sleep(10); // 短暂休眠,避免阻塞
                }
            }
            
            if (!fieldsToDelete.isEmpty()) {
                redisTemplate.opsForHash().delete(key, 
                    fieldsToDelete.toArray(new Object[0]));
            }
            
            cursor = cursorResult.getCursorId();
        } while (!cursor.equals("0"));
        
        // 最后删除Key本身
        redisTemplate.delete(key);
    }
    
    // 方案2:UNLINK命令(Redis 4.0+)
    public void unlinkBigKey(String key) {
        // UNLINK命令在后台异步删除
        redisTemplate.unlink(key);
    }
}

6.3 生产环境最佳实践

6.3.1 预防与监控体系

yaml

# 监控告警规则
big_key_detection:
  rules:
    - key: string_key_size
      threshold: 10KB      # String类型超过10KB告警
      action: alert
      
    - key: hash_field_count
      threshold: 1000      # Hash字段超过1000个告警
      action: alert
      
    - key: list_item_count  
      threshold: 5000      # List元素超过5000个告警
      action: alert
      
    - key: set_member_count
      threshold: 10000     # Set成员超过10000个告警
      action: alert
      
    - key: zset_member_count
      threshold: 10000     # ZSet成员超过10000个告警
      action: alert

# 自动处理流程
auto_remediation:
  enabled: true
  actions:
    - type: split_big_hash
      threshold: 5000 fields
      strategy: by_field_group
      
    - type: archive_big_list  
      threshold: 10000 items
      strategy: move_to_db
      
    - type: notify_developer
      threshold: critical
      channels: [slack, email]
6.3.2 客户端规范

java

// 客户端使用规范
public class RedisClientSpec {
    // 1. 键设计规范
    // 格式:业务:子业务:ID[:字段]
    // 示例:user:profile:1000:basic
    
    // 2. 值大小限制
    public static final int MAX_STRING_SIZE = 10 * 1024;      // 10KB
    public static final int MAX_HASH_FIELDS = 1000;           // 1000字段
    public static final int MAX_LIST_ITEMS = 5000;            // 5000元素
    public static final int MAX_SET_MEMBERS = 10000;          // 10000成员
    
    // 3. 批量操作限制
    public static final int MAX_PIPELINE_SIZE = 50;           // 管道批次数
    public static final int MAX_MGET_SIZE = 100;              // MGET批次数
    
    // 4. 超时设置
    public static final int OPERATION_TIMEOUT = 1000;         // 1秒
    public static final int CONNECTION_TIMEOUT = 2000;        // 2秒
    
    // 5. 重试策略
    public static final int MAX_RETRIES = 3;
    public static final long RETRY_DELAY = 100;               // 100ms
}

七、Redis分布式锁深度解析

7.1 分布式锁核心要求

CAP理论下的分布式锁:

text

在分布式系统中,分布式锁需要满足:
1. 安全性(Safety):互斥性,同一时刻只有一个客户端能持有锁
2. 活性(Liveness):
   - 无死锁:最终一定能获取到锁
   - 容错性:客户端崩溃后锁最终能被释放
3. 性能(Performance):
   - 低延迟:获取和释放锁要快
   - 高吞吐:支持高并发场景

7.2 基础分布式锁实现

7.2.1 SET NX EX实现

lua

-- 加锁脚本
-- KEYS[1]: 锁的key
-- ARGV[1]: 锁的值(客户端唯一标识)
-- ARGV[2]: 过期时间(秒)
local key = KEYS[1]
local value = ARGV[1]
local ttl = ARGV[2]

-- 尝试设置锁
local result = redis.call('SET', key, value, 'NX', 'EX', ttl)

if result then
    return 1  -- 加锁成功
else
    -- 检查锁是否是自己持有的(防止误删)
    local currentValue = redis.call('GET', key)
    if currentValue == value then
        -- 续期
        redis.call('EXPIRE', key, ttl)
        return 1
    else
        return 0  -- 加锁失败
    end
end
7.2.2 释放锁实现

lua

-- 释放锁脚本
-- KEYS[1]: 锁的key
-- ARGV[1]: 期望的锁值
local key = KEYS[1]
local expectedValue = ARGV[1]

local actualValue = redis.call('GET', key)

if actualValue == expectedValue then
    -- 释放锁
    redis.call('DEL', key)
    return 1
elseif actualValue == false then
    -- 锁已不存在
    return 0
else
    -- 锁被其他客户端持有
    return -1
end

7.3 Redlock算法深度分析

7.3.1 Redlock算法流程

算法步骤:

  1. 获取当前时间(毫秒)
  2. 依次向N个Redis节点请求加锁
  3. 计算获取锁花费的时间
  4. 判断是否获取锁成功
  5. 锁释放

Redlock实现:

java

public class RedLock {
    private final List<RedisClient> clients;
    private final String resource;
    private final String value;
    private final int ttl;
    
    public boolean lock() {
        long startTime = System.currentTimeMillis();
        
        // 尝试向所有节点加锁
        int successCount = 0;
        for (RedisClient client : clients) {
            if (tryLock(client)) {
                successCount++;
            }
            
            // 检查是否超时
            if (System.currentTimeMillis() - startTime > ttl) {
                break;
            }
        }
        
        // 计算获取锁花费的时间
        long elapsedTime = System.currentTimeMillis() - startTime;
        
        // 判断是否成功:大多数节点成功且未超时
        boolean success = successCount >= majority() && 
                         elapsedTime < ttl;
        
        if (!success) {
            // 失败,释放已获得的锁
            unlock();
        }
        
        return success;
    }
    
    private boolean tryLock(RedisClient client) {
        String result = client.set(resource, value, "NX", "PX", ttl);
        return "OK".equals(result);
    }
    
    private int majority() {
        return clients.size() / 2 + 1;
    }
    
    public void unlock() {
        for (RedisClient client : clients) {
            try {
                // 使用Lua脚本释放锁
                String script = 
                    "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                    "   return redis.call('del', KEYS[1]) " +
                    "else " +
                    "   return 0 " +
                    "end";
                
                client.eval(script, 
                    Collections.singletonList(resource),
                    Collections.singletonList(value));
            } catch (Exception e) {
                // 记录日志,但不影响其他节点
                log.error("Failed to unlock on node: " + client, e);
            }
        }
    }
}
7.3.2 Redlock争议与改进

争议点分析:

  1. 时钟跳跃问题:系统时钟回拨可能导致锁提前释放
  2. GC停顿问题:客户端GC停顿可能导致锁过期
  3. 网络延迟问题:网络分区时可能产生脑裂

改进方案:

1. 令牌机制(Token Mechanism):

java

public class TokenBasedLock {
    private final RedisClient redis;
    private final String lockKey;
    private final String token;
    private final ScheduledExecutorService scheduler;
    
    public TokenBasedLock() {
        this.token = UUID.randomUUID().toString();
        this.scheduler = Executors.newScheduledThreadPool(1);
    }
    
    public boolean lock(long ttl) {
        // 获取锁
        boolean acquired = redis.set(lockKey, token, "NX", "PX", ttl);
        
        if (acquired) {
            // 启动续期任务
            startRenewalTask(ttl);
        }
        
        return acquired;
    }
    
    private void startRenewalTask(long ttl) {
        // 在ttl/3时间后开始续期
        long renewalInterval = ttl / 3;
        
        scheduler.scheduleAtFixedRate(() -> {
            try {
                // 检查锁是否仍属于自己
                String currentToken = redis.get(lockKey);
                if (token.equals(currentToken)) {
                    // 续期
                    redis.pexpire(lockKey, ttl);
                } else {
                    // 锁已丢失,停止续期
                    scheduler.shutdown();
                }
            } catch (Exception e) {
                log.error("Failed to renew lock", e);
            }
        }, renewalInterval, renewalInterval, TimeUnit.MILLISECONDS);
    }
}

2. 基于fencing token的方案:

java

public class FencingTokenLock {
    private final RedisClient redis;
    private final String lockKey;
    private final String tokenKey;
    
    public long lock() {
        // 1. 获取锁
        String token = UUID.randomUUID().toString();
        boolean acquired = redis.set(lockKey, token, "NX", "PX", 30000);
        
        if (!acquired) {
            throw new LockAcquisitionException("Failed to acquire lock");
        }
        
        // 2. 获取fencing token(单调递增)
        long fencingToken = redis.incr(tokenKey);
        
        return fencingToken;
    }
    
    public void unlock() {
        // 使用Lua脚本释放锁
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end";
        
        redis.eval(script, 
            Collections.singletonList(lockKey),
            Collections.singletonList(getCurrentToken()));
    }
}

7.4 生产环境分布式锁最佳实践

7.4.1 锁设计规范

java

public class DistributedLockSpec {
    // 1. 锁Key设计
    // 格式:lock:{业务}:{资源标识}
    // 示例:lock:order:pay:{orderId}
    
    // 2. 锁值设计
    // 使用客户端唯一标识 + 线程ID
    // 格式:{clientId}:{threadId}:{random}
    // 示例:app-server-01:thread-15:abc123
    
    // 3. 超时时间设置
    // 原则:业务操作最长时间 + 网络延迟 + 时钟误差
    public static final long DEFAULT_LOCK_TIMEOUT = 30000;  // 30秒
    
    // 4. 重试策略
    public static final int MAX_RETRIES = 3;
    public static final long RETRY_INTERVAL = 100;  // 100ms
    
    // 5. 锁续期
    public static final long RENEWAL_INTERVAL = 10000;  // 10秒续期一次
    public static final double RENEWAL_THRESHOLD = 0.3;  // 剩余30%时间时续期
}
7.4.2 锁监控与治理

java

public class LockMonitor {
    private final MeterRegistry meterRegistry;
    private final RedisClient redis;
    
    // 监控指标
    public void monitorLock(String lockKey) {
        // 1. 锁持有时间
        Timer.Sample sample = Timer.start(meterRegistry);
        
        try {
            // 获取锁并执行业务
            if (acquireLock(lockKey)) {
                doBusiness();
            }
        } finally {
            sample.stop(Timer.builder("lock.hold.time")
                .tag("lock_key", lockKey)
                .register(meterRegistry));
        }
        
        // 2. 锁等待时间
        // 3. 锁获取成功率
        // 4. 死锁检测
    }
    
    // 死锁检测
    public void detectDeadlock() {
        // 扫描所有锁
        Set<String> lockKeys = scanKeys("lock:*");
        
        for (String lockKey : lockKeys) {
            long ttl = redis.ttl(lockKey);
            
            if (ttl < 0) {
                // 锁已过期但未释放,可能死锁
                String lockValue = redis.get(lockKey);
                log.warn("Possible deadlock detected: key={}, value={}", 
                    lockKey, lockValue);
                
                // 自动解锁(谨慎使用)
                // autoUnlock(lockKey, lockValue);
            }
        }
    }
}
7.4.3 高可用锁服务架构

text

架构设计:
应用层 → 锁服务代理 → Redis集群(多机房部署)
         ↓
     监控告警
         ↓
     管理控制台

组件职责:
1. 锁服务代理:
   - 统一锁API
   - 负载均衡
   - 熔断降级
   
2. Redis集群:
   - 多机房部署(同城双活/异地灾备)
   - 集群模式保障可用性
   
3. 监控告警:
   - 锁状态监控
   - 死锁检测
   - 性能指标收集
   
4. 管理控制台:
   - 锁可视化
   - 手动干预
   - 历史记录

7.5 特殊场景锁优化

7.5.1 可重入锁实现

lua

-- 可重入锁Lua脚本
-- KEYS[1]: 锁key
-- ARGV[1]: 锁值
-- ARGV[2]: 过期时间
-- ARGV[3]: 重入计数key
local lockKey = KEYS[1]
local lockValue = ARGV[1]
local ttl = tonumber(ARGV[2])
local reentrantKey = ARGV[3]

-- 检查是否已持有锁
local currentValue = redis.call('GET', lockKey)

if currentValue == lockValue then
    -- 重入:增加计数
    local count = redis.call('INCR', reentrantKey)
    redis.call('EXPIRE', reentrantKey, ttl)
    return 1
elseif currentValue == false then
    -- 首次获取锁
    local result = redis.call('SET', lockKey, lockValue, 'NX', 'EX', ttl)
    if result then
        -- 设置重入计数为1
        redis.call('SET', reentrantKey, 1, 'EX', ttl)
        return 1
    else
        return 0
    end
else
    -- 锁被其他客户端持有
    return 0
end
7.5.2 读写锁实现

lua

-- 读写锁Lua脚本
-- 读锁:可多个客户端同时持有
-- 写锁:独占锁

-- 获取读锁
local function acquireReadLock(lockKey, clientId, ttl)
    local writeLockKey = lockKey .. ":write"
    local readLockKey = lockKey .. ":read"
    
    -- 检查是否有写锁
    local hasWriteLock = redis.call('EXISTS', writeLockKey)
    if hasWriteLock == 1 then
        return 0  -- 存在写锁,无法获取读锁
    end
    
    -- 增加读锁计数
    local readers = redis.call('HINCRBY', readLockKey, clientId, 1)
    if readers == 1 then
        redis.call('EXPIRE', readLockKey, ttl)
    end
    
    return 1
end

-- 获取写锁
local function acquireWriteLock(lockKey, clientId, ttl)
    local writeLockKey = lockKey .. ":write"
    local readLockKey = lockKey .. ":read"
    
    -- 检查是否有其他读锁或写锁
    local hasWriteLock = redis.call('EXISTS', writeLockKey)
    local hasReaders = redis.call('HLEN', readLockKey)
    
    if hasWriteLock == 1 or hasReaders > 0 then
        return 0  -- 存在其他锁
    end
    
    -- 获取写锁
    local result = redis.call('SET', writeLockKey, clientId, 'NX', 'EX', ttl)
    return result and 1 or 0
end
7.5.3 分段锁优化

java

public class SegmentedLock {
    // 场景:库存扣减,商品ID=1000,库存=10000
    // 问题:单个锁竞争激烈
    // 方案:分段锁,将库存分成多个段
    
    private final int segments;
    private final DistributedLock[] locks;
    
    public SegmentedLock(int segments) {
        this.segments = segments;
        this.locks = new DistributedLock[segments];
        
        for (int i = 0; i < segments; i++) {
            locks[i] = new DistributedLock("inventory:segment:" + i);
        }
    }
    
    public boolean deductStock(Long productId, int quantity) {
        // 计算涉及哪些段
        List<Integer> affectedSegments = calculateSegments(quantity);
        
        // 按顺序获取锁(避免死锁)
        List<DistributedLock> acquiredLocks = new ArrayList<>();
        
        try {
            for (int segment : affectedSegments) {
                if (!locks[segment].tryLock(1000)) {
                    // 获取失败,释放已获得的锁
                    releaseLocks(acquiredLocks);
                    return false;
                }
                acquiredLocks.add(locks[segment]);
            }
            
            // 执行扣减
            return doDeductStock(productId, quantity, affectedSegments);
        } finally {
            releaseLocks(acquiredLocks);
        }
    }
    
    private List<Integer> calculateSegments(int quantity) {
        // 根据扣减数量计算影响的段
        // 例如:每段100个库存,扣减250个,需要段0、1、2
        List<Integer> segments = new ArrayList<>();
        int remaining = quantity;
        int currentSegment = 0;
        
        while (remaining > 0) {
            segments.add(currentSegment);
            remaining -= 100;  // 每段容量
            currentSegment++;
        }
        
        return segments;
    }
}

总结

通过以上深度解析,我们全面覆盖了Redis的核心知识点:

  1. 线程模型:从单线程到多线程IO的演进,事件驱动机制
  2. 持久化机制:RDB、AOF、混合持久化的原理与优化
  3. 淘汰策略:LRU、LFU等算法的实现与选择
  4. 集群模式:Cluster架构、Gossip协议、故障转移
  5. 数据结构:SDS、字典、跳跃表、压缩列表等底层实现
  6. 热点Key与大Key:检测方法、解决方案、最佳实践
  7. 分布式锁:基础实现、Redlock算法、生产实践

这些知识点不仅帮助应对面试,更能指导实际工作中的Redis使用和优化。在实际应用中,需要根据业务场景选择合适的方案,并建立完善的监控和治理体系。