Cluster集群:Redis的水平扩展方案

引言 单个Redis实例内存有限(通常16-64GB),如何存储TB级数据?如何实现水平扩展? Redis Cluster提供了原生的分布式解决方案。 一、Cluster概述 1.1 为什么需要Cluster? 主从复制+哨兵的局限: 主从架构: ┌──────────┐ │ Master │ (单节点存储所有数据) └────┬─────┘ │ 复制 ┌────┴────┬────────┐ │ Slave1 │ Slave2 │ (仅读扩展,不能写扩展) └─────────┴────────┘ 问题: ❌ 单点容量瓶颈(受限于单机内存) ❌ 写性能无法扩展(所有写操作都在主节点) ❌ 主节点故障影响所有数据访问 Cluster分片架构: Cluster: ┌───────┐ ┌───────┐ ┌───────┐ │Master1│ │Master2│ │Master3│ (数据分片存储) │Slave1 │ │Slave2 │ │Slave3 │ └───────┘ └───────┘ └───────┘ ↓ ↓ ↓ 存储1/3 存储1/3 存储1/3 数据 数据 数据 优势: ✅ 容量扩展(添加节点) ✅ 写性能扩展(多个主节点) ✅ 高可用(每个主节点有从节点) 1.2 核心概念 分片(Sharding):数据分散存储在多个节点 槽位(Slot):16384个槽位,分配给不同节点 节点(Node):独立的Redis实例 主从复制:每个主节点可有多个从节点 二、槽位机制 2.1 什么是槽位? Redis Cluster共有16384个槽位(0-16383): ...

2025-01-21 · maneng

哨兵Sentinel:Redis的自动故障转移

引言 主从复制虽然实现了数据备份,但主节点宕机后需要手动切换。哨兵(Sentinel)提供了自动故障检测和故障转移能力,实现真正的高可用。 一、哨兵概述 1.1 什么是哨兵? 监控 ↓ [哨兵1] ←→ [哨兵2] ←→ [哨兵3] (多个哨兵相互通信) ↓ ↓ ↓ 监控 监控 监控 ↓ ↓ ↓ [主节点] ← [从节点1] ← [从节点2] 功能: 1. 监控:检查主从节点是否正常 2. 通知:故障通知管理员 3. 故障转移:自动提升从节点为主节点 4. 配置中心:客户端通过哨兵发现主节点 1.2 核心功能 监控(Monitoring) 检查主从节点健康状态 周期性发送PING命令 故障检测(Failure Detection) 主观下线(SDOWN):单个哨兵判断 客观下线(ODOWN):多数哨兵同意 自动故障转移(Failover) 选举领导者哨兵 选择新主节点 通知从节点切换 通知客户端 配置中心(Configuration Provider) 客户端从哨兵获取主节点地址 主节点变更自动通知客户端 二、哨兵部署 2.1 最小配置 # sentinel.conf port 26379 sentinel monitor mymaster 127.0.0.1 6379 2 # 2个哨兵同意才判定下线 sentinel down-after-milliseconds mymaster 5000 # 5秒无响应判断下线 sentinel failover-timeout mymaster 180000 # 故障转移超时时间 sentinel parallel-syncs mymaster 1 # 同时同步的从节点数量 2.2 启动哨兵 # 方式1: redis-sentinel sentinel.conf # 方式2: redis-server sentinel.conf --sentinel # 查看哨兵状态 127.0.0.1:26379> SENTINEL masters 127.0.0.1:26379> SENTINEL slaves mymaster 127.0.0.1:26379> SENTINEL sentinels mymaster 2.3 推荐部署架构 机器1:Redis Master + Sentinel1 机器2:Redis Slave1 + Sentinel2 机器3:Redis Slave2 + Sentinel3 好处: - 3个哨兵互相监控,防止误判 - 分布式部署,避免单点故障 三、故障检测机制 3.1 主观下线(SDOWN) 哨兵1每秒向主节点发送PING: PING → 主节点 PING → 主节点 PING → (超时5秒无响应) 判断:主节点主观下线(SDOWN) 配置: ...

2025-01-21 · maneng

主从复制:Redis的数据同步机制

引言 单个Redis实例存在风险:硬件故障、数据丢失、无法扩展读能力。**主从复制(Master-Slave Replication)**是Redis高可用的基础。 今天我们深入剖析Redis主从复制的实现原理。 一、主从复制概述 1.1 什么是主从复制? 主节点(Master) 从节点(Slave1、Slave2) ↓ ↓ [写操作] SET key value [只读] GET key ↓ ↑ 自动同步数据 ───────────────────────┘ 特点: - 主节点:可读可写 - 从节点:只读(默认) - 数据自动同步:主 → 从 1.2 应用场景 读写分离:主写从读,提升读性能 数据备份:从节点作为数据备份 高可用:主节点宕机,从节点可提升为主 异地容灾:从节点部署在不同机房 二、配置主从复制 2.1 基本配置 # 从节点配置 redis-server --port 6380 --replicaof 127.0.0.1 6379 # 或修改redis.conf replicaof 127.0.0.1 6379 # 指定主节点 2.2 运行时配置 # 从节点执行 127.0.0.1:6380> REPLICAOF 127.0.0.1 6379 OK # 取消复制 127.0.0.1:6380> REPLICAOF NO ONE OK # 变为独立节点 2.3 查看复制状态 # 主节点 127.0.0.1:6379> INFO replication role:master connected_slaves:2 slave0:ip=127.0.0.1,port=6380,state=online,offset=1234 slave1:ip=127.0.0.1,port=6381,state=online,offset=1234 # 从节点 127.0.0.1:6380> INFO replication role:slave master_host:127.0.0.1 master_port:6379 master_link_status:up 三、复制流程 3.1 全量复制(Full Resynchronization) 触发时机: ...

2025-01-21 · maneng

IO多路复用:Reactor模式与epoll原理

引言 Redis单线程却能处理10000+并发连接,秘密就在于IO多路复用(I/O Multiplexing)。今天我们深入理解这个高并发的核心机制。 一、什么是IO多路复用? 核心思想: 一个线程监听多个文件描述符(socket连接),哪个准备好就处理哪个 传统阻塞I/O: 线程1 → 监听客户端1 → 阻塞等待 线程2 → 监听客户端2 → 阻塞等待 ... 线程N → 监听客户端N → 阻塞等待 IO多路复用: 线程1 → 监听所有客户端 → 有事件就处理,没事件就等待 二、三种实现:select、poll、epoll 2.1 select(最古老,1983年) fd_set readfds; FD_ZERO(&readfds); FD_SET(fd1, &readfds); FD_SET(fd2, &readfds); select(max_fd + 1, &readfds, NULL, NULL, NULL); // 缺点: // 1. fd数量限制(默认1024) // 2. 需要遍历所有fd检查哪个就绪(O(n)) // 3. 每次调用需要拷贝fd_set到内核 2.2 poll(1997年) struct pollfd fds[N]; fds[0].fd = fd1; fds[0].events = POLLIN; poll(fds, N, -1); // 改进:无fd数量限制 // 缺点:仍需遍历所有fd(O(n)) 2.3 epoll(Linux 2.6,2002年) // 1. 创建epoll实例 int epfd = epoll_create(1); // 2. 添加监听的fd struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = fd1; epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev); // 3. 等待事件 struct epoll_event events[MAX_EVENTS]; int n = epoll_wait(epfd, events, MAX_EVENTS, -1); // 优势: // ✅ O(1)时间复杂度(只返回就绪的fd) // ✅ 无fd数量限制 // ✅ 无需每次拷贝fd列表 性能对比: ...

2025-01-21 · maneng

单线程模型:为什么Redis单线程却这么快?

引言 Redis是单线程的,却能达到10万+QPS,这听起来很矛盾。多线程不是更快吗?为什么Redis坚持单线程设计?单线程如何实现如此高的性能? 今天我们深入Redis的单线程模型,揭秘高性能背后的设计哲学。 一、Redis真的是单线程吗? 1.1 核心工作线程确实是单线程 准确的说法: Redis的核心数据处理逻辑是单线程的(主线程处理所有客户端请求) 客户端1 \ 客户端2 → [主线程] → 串行执行命令 客户端3 / 单线程处理的内容: 接收客户端连接 读取请求命令 执行命令(操作数据结构) 返回响应 处理定时任务 1.2 但Redis不是完全单线程 Redis 4.0+引入多线程(后台线程): 版本 多线程功能 用途 Redis 4.0+ 后台异步删除线程(unlink、flushdb async) 避免删除大key阻塞 Redis 4.0+ AOF重写线程 后台重写AOF文件 Redis 6.0+ I/O多线程 多线程读取请求、发送响应(数据处理仍是单线程) 关键点: 数据操作:仍然是单线程(避免锁的开销) I/O操作:Redis 6.0+支持多线程(提高网络吞吐) 后台任务:多线程(避免阻塞主线程) 二、为什么选择单线程? 2.1 多线程的问题 问题1:锁的开销 // 多线程环境 void increment_counter() { pthread_mutex_lock(&mutex); // 加锁,耗时约25ns counter++; // 操作,耗时1ns pthread_mutex_unlock(&mutex); // 解锁,耗时25ns } // 总耗时:50ns(锁开销占98%) // 单线程环境 void increment_counter() { counter++; // 直接操作,1ns } // 无锁开销! 结论:对于内存操作(纳秒级),锁的开销反而成为瓶颈。 ...

2025-01-21 · maneng

整数集合与压缩列表:Redis的极致内存优化

引言 你是否好奇:为什么同样存储100个整数,Redis的内存占用只有其他数据库的1/10?为什么小Hash比大Hash节省这么多内存?编码转换是如何发生的? 今天我们深入Redis的两大内存优化数据结构:整数集合(intset)和压缩列表(ziplist/listpack),揭秘Redis在小数据量场景下的极致优化。 一、为什么需要内存优化数据结构? 1.1 性能 vs 内存的权衡 通用数据结构的问题: 字典(hashtable): - 查询:O(1) ✅ - 内存:每个节点 ~50字节(dictEntry + 指针) ❌ 跳表(skiplist): - 查询:O(logN) ✅ - 内存:每个节点 ~80字节(多层指针) ❌ 问题:对于小数据量(如10个元素的Set),使用通用结构浪费内存 Redis的解决方案: 针对小数据量场景,设计紧凑的专用数据结构,牺牲一点性能换取内存节省。 1.2 编码策略 Redis采用双重编码策略: 数据类型 小数据量编码 大数据量编码 转换阈值 Set intset(所有元素都是整数) hashtable 512个元素 Hash ziplist/listpack hashtable 512个字段 或 64字节值 ZSet ziplist/listpack skiplist + hashtable 128个元素 或 64字节值 List quicklist(内部使用ziplist/listpack) quicklist - 核心思想: 小数据量:节省内存优先(连续内存,紧凑编码) 大数据量:性能优先(O(1)查询) 二、整数集合(intset) 2.1 核心结构 typedef struct intset { uint32_t encoding; // 编码方式(int16/int32/int64) uint32_t length; // 元素数量 int8_t contents[]; // 柔性数组,实际存储整数 } intset; 编码类型: ...

2025-01-21 · maneng

字典与哈希表:渐进式rehash如何避免阻塞?

引言 你是否好奇:Redis的Hash类型如何实现O(1)的查询速度?当哈希表需要扩容时,如何避免长时间阻塞?数百万个key同时存在,Redis如何快速找到目标key? 今天我们深入Redis字典(dict)的底层实现,揭秘渐进式rehash的精妙设计。 一、为什么需要字典? 1.1 Redis的核心数据结构 Redis数据库本身就是一个巨大的字典: Redis数据库: ┌──────────────────────┐ │ key1 → value1 (String)│ │ key2 → value2 (Hash) │ │ key3 → value3 (List) │ │ ... │ │ keyN → valueN (ZSet) │ └──────────────────────┘ 所有key → redisObject的映射,都存储在字典中 字典的使用场景: 数据库键空间:整个Redis数据库(key → value) Hash类型:Hash对象的底层实现 ZSet类型:ZSet的成员 → 分数映射 过期键字典:记录key的过期时间 集群节点:记录槽位与节点的映射 1.2 性能需求 操作 时间复杂度要求 添加键值对 O(1) 查找key O(1) 删除key O(1) 扩容 不能阻塞服务 二、哈希表基础 2.1 哈希表原理 哈希表 = 数组 + 链表(或其他冲突解决方法) Key → Hash Function → Index → Value 示例: "name" → hash("name") → 12345 → 12345 % 16 = 13 → table[13] 核心优势:O(1)时间复杂度 ...

2025-01-21 · maneng

跳表SkipList:为什么Redis选择它而不是红黑树?

引言 你是否好奇:为什么Redis的有序集合(ZSet)能在O(logN)时间内完成插入、删除、查找和范围查询?为什么Redis选择跳表(SkipList)而不是更常见的红黑树或AVL树? 今天我们深入剖析跳表的设计思想和实现细节,理解这个优雅而高效的数据结构。 一、从问题出发:有序集合的需求 1.1 ZSet的典型操作 # 添加元素(带分数) 127.0.0.1:6379> ZADD ranking 95 "张三" 88 "李四" 92 "王五" (integer) 3 # 按分数范围查询 127.0.0.1:6379> ZRANGEBYSCORE ranking 90 100 1) "王五" # 92分 2) "张三" # 95分 # 查询排名 127.0.0.1:6379> ZRANK ranking "李四" (integer) 0 # 第0名(分数最低) # 删除元素 127.0.0.1:6379> ZREM ranking "李四" (integer) 1 1.2 性能需求 操作 时间复杂度要求 插入元素 O(logN) 删除元素 O(logN) 查找元素 O(logN) 范围查询 O(logN + M),M为结果数量 获取排名 O(logN) 1.3 候选数据结构 数据结构 插入/删除/查找 范围查询 实现复杂度 内存占用 有序数组 O(N) O(logN + M) 简单 低 平衡二叉树(AVL/红黑树) O(logN) O(logN + M) 复杂 中 B树/B+树 O(logN) O(logN + M) 复杂 中 跳表(SkipList) O(logN) O(logN + M) 简单 中 Redis选择跳表的理由: ...

2025-01-21 · maneng

SDS简单动态字符串:为什么Redis不用C字符串?

引言 在Redis中,我们使用SET和GET命令操作字符串,看起来和C语言的字符串没什么区别。但实际上,Redis并没有使用C语言传统的字符串表示(以空字符’\0’结尾的字符数组),而是自己实现了一种名为SDS(Simple Dynamic String,简单动态字符串)的抽象类型。 为什么要重新实现字符串?SDS有什么优势?今天我们深入源码,揭开SDS的神秘面纱。 一、C字符串的局限性 1.1 C字符串的表示 // C语言字符串 char str[] = "Redis"; // 内存布局 +---+---+---+---+---+---+ | R | e | d | i | s | \0| +---+---+---+---+---+---+ 0 1 2 3 4 5 1.2 存在的问题 问题1:获取字符串长度O(n) // 需要遍历整个字符串直到遇到'\0' size_t len = strlen(str); // O(n) 时间复杂度 // 对于频繁获取长度的场景(如Redis命令),性能损失严重 问题2:不支持二进制数据 // C字符串以'\0'作为结尾标志 char binary_data[] = {0x01, 0x02, 0x00, 0x03}; // ❌ 无法正确处理 // strlen会在遇到0x00时停止,认为字符串结束 // 导致无法存储图片、音频等二进制数据 问题3:容易缓冲区溢出 char dest[5] = "Hi"; char src[] = "Redis"; // ❌ 危险操作:没有检查dest空间是否足够 strcat(dest, src); // 缓冲区溢出!破坏相邻内存 问题4:内存重分配频繁 // 字符串拼接需要重新分配内存 char *str = malloc(6); strcpy(str, "Redis"); // 追加内容,需要重新分配 str = realloc(str, 12); // 每次都要重新分配,性能差 strcat(str, " Fast"); 问题5:不兼容部分C函数 // 很多C函数假设字符串以'\0'结尾 // 对于包含'\0'的二进制数据,这些函数无法正常工作 1.3 Redis的需求 Redis作为高性能数据库,对字符串的需求: ...

2025-01-21 · maneng

Redis内存模型与对象系统:深入理解redisObject

引言 在使用Redis时,我们操作的是String、Hash、List这些高层数据类型。但你是否想过:Redis是如何在内存中存储和管理这些对象的?为什么同样是存储字符串,Redis能做到如此高的性能和内存利用率? 今天我们深入Redis底层,揭开对象系统的神秘面纱。理解对象系统,是理解Redis高性能和内存优化的基础。 一、为什么需要对象系统? 1.1 直接存储的问题 假设我们不用对象系统,直接在内存中存储数据,会遇到什么问题? // 简单粗暴的方式 char* key = "user:1001"; char* value = "张三"; // 问题1:如何知道value的类型?是字符串?整数?还是列表? // 问题2:如何实现引用计数?避免重复拷贝 // 问题3:如何记录对象的访问时间?用于LRU淘汰 // 问题4:如何选择最优的底层编码?节省内存 1.2 对象系统的价值 Redis设计了一个统一的对象系统redisObject,解决了以下问题: 类型识别:明确知道对象是什么类型(String/Hash/List/Set/ZSet) 编码优化:根据数据特点自动选择最优编码方式(节省内存) 内存管理:引用计数、LRU/LFU淘汰、内存回收 命令多态:同一个命令可以作用于不同编码的对象 类型安全:避免类型错误操作(如对字符串执行LPUSH) 二、redisObject核心结构 2.1 源码定义 typedef struct redisObject { unsigned type:4; // 类型(4 bits):String、Hash、List、Set、ZSet unsigned encoding:4; // 编码(4 bits):底层实现方式 unsigned lru:24; // LRU时间戳(24 bits)或LFU数据 int refcount; // 引用计数(32 bits) void *ptr; // 指向实际数据的指针(64 bits) } robj; 结构说明: 总大小:16字节(紧凑设计,节省内存) 4 bits + 4 bits + 24 bits = 32 bits = 4字节 refcount:4字节 ptr:8字节(64位系统) 2.2 字段详解 2.2.1 type:对象类型(4 bits) #define OBJ_STRING 0 // 字符串 #define OBJ_LIST 1 // 列表 #define OBJ_SET 2 // 集合 #define OBJ_ZSET 3 // 有序集合 #define OBJ_HASH 4 // 哈希表 示例: ...

2025-01-21 · maneng

如约数科科技工作室

浙ICP备2025203501号

👀 本站总访问量 ...| 👤 访客数 ...| 📅 今日访问 ...