分布式缓存:三万字详解Redis
文章目录
- 缓存全景图
- Pre
- Redis 整体认知框架
- 一、Redis 简介
- 二、核心特性
- 三、性能模型
- 四、持久化详解
- 五、复制与高可用
- 六、集群与分片方案
- Redis 核心数据类型
- 概述
- 1. String
- 2. List
- 3. Set
- 4. Sorted Set(有序集合)
- 5. Hash
- 6. Bitmap
- 7. Geo
- 8. HyperLogLog
- Redis 协议分析
- 1. RESP 设计原则
- 2. 三种响应模型与特殊模式
- 3. 两种请求格式
- 3.1 Inline 命令格式
- 3.2 Array(数组)格式
- 4. 五种响应格式详解
- 5. 协议分类概览
- 6. Redis Client 选型与改进建议
- Redis 的核心组件
- 一、系统架构概览
- 二、事件处理机制
- 三、数据管理
- 四、功能扩展(Module System)
- 五、系统扩展(Replication & Cluster)
- Redis的事件驱动模型
- 一、事件驱动模型概述
- 二、文件事件处理详解
- 2.1 Reactor 模式四部分
- 2.2 IO 多路复用的四种实现及选型逻辑
- 2.3 aeProcessEvents:事件收集与派发流程
- 2.4 三类文件事件处理函数
- 三、时间事件机制剖析
- Redis 协议解析及处理
- 一、协议解析
- 二、协议执行
- Redis 内部数据结构
- 一、RedisDb 结构
- 二、redisObject 抽象
- 三、dict 哈希表
- 四、sds 简单动态字符串
- 五、压缩列表(ziplist)
- 六、快速列表(quicklist)
- 七、跳跃表(zskiplist)
- 八、数据类型与内部结构映射
- Redis 淘汰策略
- 一、淘汰原理
- 二、淘汰方式
- 三、淘汰策略与 Eviction Pool
- 四、八种淘汰策略详解
- Redis 的三种持久化方案及崩溃后数据恢复流程
- 一、RDB 持久化
- 二、AOF 持久化
- 三、混合持久化
- Redis 后台异步 IO(BIO)
- 一、BIO 线程设计动机
- 二、BIO 线程模型
- 三、BIO 任务类型
- 四、BIO 处理流程
- Redis 多线程架构
- 一、主线程职责
- 二、IO 线程设计
- 三、命令处理完整流程
- 四、多线程方案优劣
- 复制架构原理
- 一、复制架构原理
- 二、同步方式对比
- 三、psync 与 psync2 优化
- 四、复制连接与授权流程
- 五、复制过程详析
- 5.1 增量同步流程
- 5.2 全量同步流程
- 六、注意事项
- Redis 集群的分布式方案
- 1. Client 端分区
- 1.1 原理与哈希算法
- 1.2 DNS 动态管理
- 1.3 优缺点
- 2. Proxy 分区方案
- 2.1 架构概览
- 2.2 典型实现
- 2.3 优缺点
- 3. 原生 Redis Cluster
- 3.1 Slot 与 Gossip 架构
- 3.2 读写与重定向
- 3.3 在线扩缩容与数据迁移
- 3.4 优缺点
- 4. 对比与选型建议
缓存全景图
Pre
分布式缓存:缓存设计三大核心思想
分布式缓存:缓存的三种读写模式及分类
分布式缓存:缓存架构设计的“四步走”方法
分布式缓存:缓存设计中的 7 大经典问题_缓存失效、缓存穿透、缓存雪崩
分布式缓存:缓存设计中的 7 大经典问题_数据不一致与数据并发竞争
分布式缓存:缓存设计中的 7 大经典问题_Hot Key和Big Key
Redis 整体认知框架
一、Redis 简介
- 实现与授权:Redis 基于 ANSI C 语言编写,采用 BSD 许可,代码轻量、易于嵌入。
- 内存存储:所有数据均保存在内存中,因此具有极低的读写延迟,可做缓存、数据库、消息中间件等多种角色。
- 多库支持:Redis 即 Remote Dictionary Server,实例内部维护多个逻辑数据库(默认为 16 个),通过
SELECT
命令切换操作目标。
二、核心特性
-
丰富的数据类型:除基本的字符串(String)外,Redis 还原生支持
List、Set、Sorted Set(ZSet)、Hash
;以及Bitmap、HyperLogLog、Geo
等特殊结构,一机多用。 -
双重持久化:
- RDB 快照:定时或达到修改阈值时,将内存全量快照写入
.rdb
文件,适合冷备份; - AOF 追加:将每条写命令追加到
.aof
文件,可配置同步频率,保障最小数据丢失。
线上系统常用“RDB+ AOF 混合”策略:平时频繁追加 AOF,低峰期触发BGSAVE
生成新快照;遇到 AOF 文件过大时,用BGREWRITEAOF
重写精简。
- RDB 快照:定时或达到修改阈值时,将内存全量快照写入
-
读写分离:一主多从架构,将写请求指向 Master,读请求分发至多个 Slave,显著提高读吞吐。
-
Lua 脚本与事务:
- Lua 脚本:从 Redis 2.6 起支持,脚本内多命令打包,可实现原子性操作并减少网络往返;
- 事务:通过
MULTI/EXEC
打包命令,确保命令序列原子执行,中途出错则全部丢弃。
-
集群支持:Redis Cluster 原生实现分布式,基于 Slot 哈希机制,无中心节点,实现自动扩缩容与故障转移。
三、性能模型
-
单线程+事件驱动:网络 IO 与命令处理均在主线程中完成,基于 epoll(或 kqueue、evport)无阻塞多路复用,避免锁竞争与上下文切换。
-
高 QPS:单实例可轻松突破 100k QPS,得益于纯内存操作与无锁设计。
-
后台子进程/线程:
-
BGSAVE/BGREWRITEAOF/全量复制:主进程遇到重负荷持久化或复制任务时,fork 子进程执 行,主进程继续提供服务;
-
BIO 线程池:三个后台线程负责文件关闭、AOF 缓冲刷盘、对象释放,进一步减轻主线程压力。
-
四、持久化详解
- RDB:快速生成紧凑快照,恢复速度快;适合冷备份,但数据持久性依赖触发频率。
- AOF:按命令追加,能做到每秒或每次写入同步,重放日志恢复更完整;但文件体积随命令量增长,需定期重写。
- 混合策略:推荐生产环境开启 AOF 并定期重写,同时在低峰期执行 RDB 快照,以兼顾恢复速度与数据完整性。
五、复制与高可用
- 全量同步:Slave 首次连接或复制缓冲不足时,Master fork 子进程生成 RDB 快照并传输,Slave 接收后加载;
- 增量复制:Slave 重连且累积命令量在缓冲区可承载范围内时,仅传输缺失命令,降低复制开销。
- 故障切换:当 Master 宕机,可手动或通过哨兵(Sentinel)将任意 Slave 提升为 Master,保障业务连续性。
六、集群与分片方案
- Client 分片:客户端根据一致性哈希或取模自行路由到不同实例,简单但扩缩容麻烦;
- Proxy 层:如 Twemproxy,在前端做路由与健康检查,后端实际节点变动只需更新 Proxy 配置;
- Redis Cluster:官方原生集群,使用 16384 个 Slot 管理键空间,支持在线迁移、故障转移与自动均衡。
Redis 核心数据类型
概述
Redis 共支持以下 及 种核心数据类型:
- String:二进制安全的字符串类型
- List:按插入顺序排列的双向链表
- Set:无序且元素唯一的集合
- Sorted Set(ZSet):带分值的有序集合
- Hash:字段–值映射表
- Bitmap:基于 String 的位图封装
- Geo:地理位置类型,基于 ZSet 实现
- HyperLogLog:基数统计的近似算法
- …
1. String
-
存储方式:
- 小于 1 MB 时,采用
raw encoding
,预分配两倍长度来减少频繁扩容; - 超过 1 MB 时,每次额外预分配 1 MB。
- 小于 1 MB 时,采用
-
整型编码:对于纯数字字符串,使用整型编码,以节省内存并加速算术运算。
-
常用指令:
SET
、GET
、MSET
、INCR
、DECR
等。 -
典型场景:
-
缓存普通文本、序列化对象;
-
计数器(PV、UV、限流);
-
分布式锁的简单实现。
SET user:1001:token "abcd1234" INCR page:views
-
2. List
-
底层实现:快速双向链表,支持头尾 O(1) 插入/弹出。
-
指令摘要:
- 插入:
LPUSH
、RPUSH
、LINSERT
- 弹出:
LPOP
、RPOP
、阻塞式BLPOP
、BRPOP
- 范围查询:
LRANGE
(支持负索引)
- 插入:
-
时间复杂度:对头/尾操作为 O(1),随机访问或插入为 O(N)。
-
典型场景:
-
消息队列(工作队列、发布/订阅前端缓冲);
-
Feed Timeline(用户动态按时间顺序追加);
-
简易栈/队列。
RPUSH queue:tasks task1 task2 BLPOP queue:tasks 0 # 阻塞直到有新任务 LRANGE queue:tasks 0 9 # 获取前 10 个元素
-
3. Set
-
底层实现:哈希表,保证元素唯一且无序。
-
指令摘要:
SADD
、SREM
、SISMEMBER
、SDIFF
、SINTER
、SUNION
、SPOP
、SRANDMEMBER
。 -
时间复杂度:插入、删除、查找均为 O(1)。
-
典型场景:
-
好友关注列表、互关判断;
-
推荐系统中的离线/在线标签去重;
-
来源 IP 白名单/黑名单。
SADD user:1001:friends 1002 1003 SISMEMBER user:1001:friends 1003 # 返回 1
-
4. Sorted Set(有序集合)
-
底层实现:跳表 + 哈希,按分值升序排列。
-
指令摘要:
ZADD
、ZREM
、ZSCORE
、ZRANGE
、ZINCRBY
、ZINTERSTORE
、ZUNIONSTORE
。 -
特点:元素唯一,分值可重复;快速算分与排名。
-
典型场景:
-
实时排行榜(游戏分数、热度榜单);
-
按权重排序的数据展示;
-
定时任务系统(利用分值表示时间戳)。
ZADD leaderboard 100 user:1001 ZRANGE leaderboard 0 9 WITHSCORES # TOP10
-
5. Hash
-
底层实现:field–value 映射,内部也是哈希表。
-
指令摘要:
HSET
/HMSET
、HGET
/HMGET
、HEXISTS
、HINCRBY
、HGETALL
。 -
时间复杂度:单 field 操作为 O(1)。
-
典型场景:
-
存储对象属性,如用户资料、商品信息;
-
实现类似关系型数据库表的一行;
-
业务统计字段聚合。
HMSET user:1001 name "Alice" age 30 HINCRBY user:1001:stats login_count 1
-
6. Bitmap
-
底层实现:基于 String 的位操作。
-
指令摘要:
SETBIT
、GETBIT
、BITCOUNT
、BITOP
、BITFIELD
、BITPOS
。 -
特点:按位存储,内存占用极低;位运算高效。
-
典型场景:
-
用户活跃打卡(N 天登录);
-
标签属性存储与多维统计;
-
简易布隆过滤器原型。
SETBIT login:20250525 1001 1 BITCOUNT login:20250525 # 当天活跃用户数
-
7. Geo
-
底层实现:封装于 Sorted Set,通过 GeoHash 将经纬度映射为分值。
-
指令摘要:
GEOADD
、GEOPOS
、GEODIST
、GEORADIUS
、GEORADIUSBYMEMBER
。 -
特点:支持范围查询与距离计算。
-
典型场景:
-
附近的人/店铺/车辆搜索;
-
地理围栏告警;
-
实时位置服务(LBS)。
GEOADD restaurants 116.397128 39.916527 "PekingDuck" GEORADIUS restaurants 116.40 39.92 5 km WITHDIST
-
8. HyperLogLog
-
底层实现:近似基数统计算法,稀疏与稠密两种存储,自适应切换。
-
指令摘要:
PFADD
、PFCOUNT
、PFMERGE
。 -
特点:固定 ≈12KB 内存;误差率 ≈0.81%。
-
典型场景:
-
大规模 UV 统计;
-
海量搜索词汇去重;
-
日志中的独立源 IP 计数。
PFADD uv:202505 user:1001 user:1002 PFCOUNT uv:202505 # 当月独立访客数(近似)
-
Redis 协议分析
1. RESP 设计原则
Redis 序列化协议 RESP 的设计坚持三条原则:
- 实现简单:协议格式直观,便于不同语言的客户端快速实现。
- 可快速解析:结构清晰、前缀标记,使得解析器能够以最低开销完成读写。
- 便于阅读:即便用 Telnet 交互,也能通过简单的符号轻松定位请求与响应边界。
2. 三种响应模型与特殊模式
Redis 默认使用“Ping-Pong”模型:客户端发起一个请求,服务端立即返回一个响应,实现一问一答。
此外还有两种特殊模式:
- Pipeline 模式:客户端一次性发送多条命令,不等待中间响应,待全部发送完后再按序接收服务端响应,减少网络往返。
- Pub/Sub 模式:客户端通过
SUBSCRIBE
进入订阅状态,此后无需再次发起请求,即可持续接收服务端基于频道推送的消息;除订阅相关命令,其他命令均失效。
3. 两种请求格式
3.1 Inline 命令格式
适用于交互式会话(如 Telnet),命令与参数以空格分隔,结尾以 \r\n
:
mget key1 key2\r\n
3.2 Array(数组)格式
更规范的二进制安全格式,也是生产环境客户端默认使用:
*3\r\n$4\r\nMGET\r\n$4\r\nkey1\r\n$4\r\nkey2\r\n
其中 *3
表示数组长度为 3,每个元素前以 $<字节数>
声明。
4. 五种响应格式详解
Redis 响应客户端请求时,基于 RESP 定义了 5 类格式:
-
Simple String(简单字符串)
- 前缀
+
,不可包含\r
或\n
,以\r\n
结束。 - 用于返回 OK、PONG 等简短状态。
+OK\r\n
- 前缀
-
Error(错误)
- 前缀
-
,后跟错误类型(ERR/WRONGTYPE 等)及描述,以\r\n
结束。
-ERR unknown command 'foo'\r\n
- 前缀
-
Integer(整数)
- 前缀
:
,后跟整数字符串,以\r\n
结束。 - 代表计数、长度或布尔(0/1)等。
:1000\r\n
- 前缀
-
Bulk String(字符串块)
- 前缀
$
,后跟内容字节长度,再\r\n
;随后是真实内容,再\r\n
。 - 支持二进制安全,最大可达 512MB。
$6\r\nfoobar\r\n
- 空字符串:
$0\r\n\r\n
;NULL:$-1\r\n
。
- 前缀
-
Array(数组)
- 前缀
*
,后跟元素个数,再\r\n
;随后依次是各元素(可嵌套上述任何格式)。
*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n
- 空数组:
*0\r\n
;NULL 数组:*-1\r\n
。
- 前缀
5. 协议分类概览
除了与 8 种数据结构直接对应的命令协议,Redis 还定义了以下 8 类协议:
- Pub/Sub 协议:
SUBSCRIBE
/PUBLISH
- 事务协议:
MULTI
/EXEC
/DISCARD
- 脚本协议:
EVAL
/EVALSHA
/SCRIPT
- 连接协议:
AUTH
/SELECT
/QUIT
- 复制协议:
REPLICAOF
/PSYNC
/ROLE
- 配置协议:
CONFIG GET
/CONFIG SET
- 调试统计协议:
INFO
/MONITOR
/SLOWLOG
- 内部命令:
MIGRATE
/DUMP
/RESTORE
6. Redis Client 选型与改进建议
以 Java 为例,目前主流客户端有:
- Jedis:轻量、直观,支持连接池,几乎覆盖所有命令,但原生不支持读写分离。
- Redisson:基于 Netty 的非阻塞 IO,支持异步调用、读写分离、负载均衡及 Spring Session 集成,但实现较为复杂。
- Lettuce:也是基于 Netty,完全非阻塞、线程安全,可在多线程环境中共享同一连接;提供同步、异步(Future)、响应式(Reactive Streams)和 RxJava 风格的多种调用方式;原生支持 Redis Cluster、Sentinel、读写分离,自动故障转移;客户端实现简洁,依赖少,适合高并发、低延迟场景。
改进建议:
- 在异常访问时实现重试与熔断;
- 动态感知主从切换,自动调整连接;
- 多 Slave 场景下添加负载均衡策略;
- 与配置中心和集群管理平台集成,实现实时路由和高可用。
Redis 的核心组件
一、系统架构概览
Redis 的核心组件主要包括以下四大模块:
- 事件处理(Event Loop):基于作者开发的
ae
事件驱动模型,实现高效网络 IO 和定时任务调度 - 数据存储与管理:内存数据库
redisDB
,支持多库、多数据类型、多底层结构 - 功能扩展(Module System):可插拔模块化设计,无需改动核心即可引入新数据类型与命令
- 系统扩展(Replication & Cluster):主从复制与 Cluster 分片,满足高可用与横向扩容需求
二、事件处理机制
-
ae
事件驱动模型概述- 封装
select
/epoll
/kqueue
/evport
,实现 IO 多路复用 - 监听多个 socket,把网络读写、命令执行、定时任务整合到同一个循环
- 封装
-
客户端连接管理
- 收到新连接时,创建
client
结构体,维护状态、读写缓冲 - 请求到达后将命令读取到缓冲区,并解析成参数列表
- 收到新连接时,创建
-
命令处理流程
- 根据命令名称映射到
redisCommand
- 对参数进行进一步解析与校验
- 执行命令对应的处理函数
- 根据命令名称映射到
-
时间事件(Time Events)
- 周期性执行
serverCron
:包括统计更新、过期键清理、AOF/RDB 持久化触发等
- 周期性执行
三、数据管理
-
内存数据库结构
- 每个逻辑库对应一个
redisDB
结构,内部通过dict
存储 key/value - 八种数据类型(String、List、Set、Hash、ZSet、Stream、Bitmap、HyperLogLog)各自采用一或多种底层结构
- 每个逻辑库对应一个
-
持久化策略
- AOF(Append Only File):将每次写操作追加到缓冲,按策略刷盘
- RDB(Redis DataBase Snapshot):定期将全量数据快照落地,生成紧凑的二进制文件
-
线程模型与非阻塞
- 核心线程为单线程,避免任何内核阻塞
- BIO 线程池:专门处理可能阻塞的文件 close、fsync 等操作,保证主线程性能
-
内存淘汰与过期
- 过期键及时清理,空闲扫描或惰性删除相结合
- 八种淘汰策略(如 LRU、LFU、TTL 优先等),结合
eviction pool
高效回收内存
四、功能扩展(Module System)
-
模块加载:动态链接库,可在启动时或运行时加载/卸载
-
API 接口:
RedisModule_Init
:初始化模块RedisModule_CreateCommand
:注册新命令
-
应用场景:自定义数据结构、高级功能(例如图数据库、机器学习推理)
五、系统扩展(Replication & Cluster)
-
主从复制(Replication)
- 支持全量同步与增量复制
- Slave 重连、主从切换后均可继续增量复制,提升可用性
- 读写分离:将读请求分摊到多个节点,减轻主节点负载
-
分片集群(Cluster)
- 16384 个 slot,按 Hash 分布到不同节点
- 客户端计算 slot,根据 slot 定位节点
- 错误节点自动重定向(MOVED/ASK)
- 在线扩容:通过迁移 slot 实现节点增减
Redis的事件驱动模型
一、事件驱动模型概述
Redis 作为一个高性能的内存数据库,充分利用事件驱动模式来处理几乎所有核心操作。与 Memcached 依赖 libevent/ libev 不同,Redis 作者从零开始,开发了自研的事件循环组件,封装在 aeEventLoop
及相关结构体中。这样做的动机是:
- 最小化外部依赖:减少因第三方库升级或兼容性带来的不确定性;
- 轻量可控:自研实现更契合 Redis 的业务场景,代码更简洁,性能更容易优化;
- 灵活扩展:可在事件模型中无缝接入文件事件与时间事件的统一调度。
Redis 的事件驱动模型主要处理两类事件:
- 文件事件:与 socket 读写、连接建立/关闭直接相关的 IO 事件;
- 时间事件:周期性或单次需要在指定时间点执行的任务,例如定期统计、Key 淘汰、缓冲写出等。
二、文件事件处理详解
Redis 在文件事件处理上采用经典的Reactor 模式,将整个流程拆分为四部分:连接 socket、IO 多路复用、文件事件分派器与事件处理器。
2.1 Reactor 模式四部分
- 连接 Socket:监听客户端连接的 TCP 端口与已建立连接的客户端 Socket;
- IO 多路复用:通过底层操作系统接口同时监控多个描述符的可读写状态;
- 文件事件分派器:调用
aeProcessEvents
,从多路复用层获取触发的事件; - 事件处理器:根据事件类型(可读/可写)调用注册好的回调函数执行实际逻辑。
2.2 IO 多路复用的四种实现及选型逻辑
Redis 封装了四种主流的多路复用方案,编译时按优先级自动选择:
- evport(Solaris 专有)
- epoll(Linux 最佳选择)
- kqueue(大多数 BSD 系统)
- select(通用但性能最低)
前三者直接调用内核机制,能同时服务数十万文件描述符;select
则每次需扫描全部描述符,时间复杂度 O(n),且受描述符数量上限(默认 1024/2048)限制,不适合线上高并发场景。对应实现分布在 ae_evport.c
、ae_epoll.c
、ae_kqueue.c
、ae_select.c
四个代码文件中。
2.3 aeProcessEvents:事件收集与派发流程
aeProcessEvents
是 Redis 文件事件的核心分派器,执行流程大致如下:
- 计算下一次阻塞等待的超时时间(兼顾时间事件);
- 调用
aeApiPoll
(内置封装)阻塞或非阻塞等待文件事件; - 收集触发的事件,将它们封装到
aeFiredEvents
数组中,每项记录文件描述符与事件类型; - 将底层事件类型(如 EPOLLIN/EPOLLOUT/EPOLLERR)映射为 Redis 事件标志(AE_READABLE/AE_WRITABLE);
- 依次遍历
aeFiredEvents
,先读后写地 dispatch 到注册在aeEventLoop
中的具体事件处理器。
2.4 三类文件事件处理函数
Redis 对文件事件的注册与处理主要分为:
-
连接处理:
acceptTcpHandler
- 在
initServer
阶段注册监听 socket 的读事件; - 有新连接时,接受连接、创建
client
结构,获取远端 IP/端口; - 单次循环最多处理 1000 个新连接请求;
- 在
-
请求读取:
readQueryFromClient
- 为每个 client socket 注册读事件;
- 读取客户端发来的命令数据,填充到
client->query_buf
; - 按 inline 或 multibulk 格式解析命令,校验参数及当前实例状态后,执行对应的
redisCommand
; - 将执行结果写入
client->reply_buf
;
-
回复发送:
sendReplyToClient
- 在命令执行完将结果放入写缓冲后,注册写事件;
- 当 socket 可写时,将缓冲区数据发送给客户端。
三、时间事件机制剖析
与文件事件并行,Redis 的时间事件在同一个 aeEventLoop
内作为链表管理。每个时间事件包含五个核心属性:
- 事件 ID:全局唯一自增;
- 执行时间:
when_sec
与when_ms
,精确到毫秒; - 处理器:
timeProc
函数指针; - 关联数据:
clientData
传递给处理器使用; - 双向链表指针:
prev
、next
,便于插入与遍历。
时间事件分为:
- 单次事件:执行一次后即标记删除;
- 周期事件:执行后更新下一次执行时间,保持循环。
在 aeProcessEvents
中,文件事件处理前后都会遍历一次时间事件链表,执行所有到期的事件:
- 逐一比较事件时间与当前时钟;
- 对可执行事件调用
timeProc(clientData)
; - 若周期事件,更新
when_sec
/when_ms
;若单次事件,标记id=-1
,下一轮清除。
Redis 默认主要的时间事件包括:
serverCron
:定期执行统计、淘汰、维护缓冲等任务;moduleTimerHandler
:模块化扩展的定时回调。
Redis 协议解析及处理
当事件循环检测到客户端有请求到来时,Redis 如何从网络读入原始数据、解析成命令与参数,最终执行并返回结果。
一、协议解析
-
读取请求到 Query Buffer
- 当 socket 可读事件触发,Redis 会调用
readQueryFromClient
,从客户端连接的文件描述符读取数据到client->querybuf
。 - 默认读缓冲大小为 16 KB;若单次请求长度超过 1 GB,Redis 会报错并关闭连接,防止恶意或异常请求耗尽内存。
- 当 socket 可读事件触发,Redis 会调用
-
判断协议类型:MULTIBULK vs INLINE
-
MULTIBULK(以
*
开头)-
首字节为
*
,表示后续是一个块数组。格式为:*<参数个数>\r\n $<第1个参数字节数>\r\n <参数1内容>\r\n …
-
逐行读取,根据
$
指示读取固定长度数据,直到完整填充所有参数。
-
-
INLINE(单行字符串)
-
首字节非
*
,整个请求以\r\n
结尾。命令和参数用空格分隔:set mykey hello\r\n
-
Redis 会将整行切分,再按空格拆分命令及参数。
-
-
-
填充
client->argc
与client->argv
- 解析结束后,将参数个数写入
client->argc
。 - 对于每个参数,创建一个
robj
(Redis 对象),存入client->argv
数组,以便后续命令执行使用。
- 解析结束后,将参数个数写入
二、协议执行
Redis 协议解析及处理
-
处理特殊命令:
QUIT
- 若
argv[0]
为quit
,Redis 直接返回+OK\r\n
并将CLIENT_CLOSE_AFTER_REPLY
标记置位,表示回复后关闭连接。
- 若
-
查找并执行命令
-
使用
lookupCommand
在全局命令表(server.commands
)中查找argv[0]
对应的redisCommand
结构。 -
若找不到,则调用
addReplyErrorFormat(c,"ERR unknown command '%s'",c->argv[0]->ptr)
,向客户端返回未知命令错误。 -
找到后,进入命令执行阶段:
c->cmd = cmd; c->cmd->proc(c);
proc
是命令对应的函数指针,如setCommand
、getCommand
等。
-
-
写入响应与副作用
-
命令执行完成后,依照命令逻辑通过
addReply*
系列函数将响应数据写入client->buf
(写缓冲区)。 -
若该命令为写操作,且开启了 AOF 或者当前角色为主节点,还需将写命令推送给 AOF 线程与所有从节点:
- 调用
feedAppendOnlyFile(c, ...);
- 调用
replicationFeedSlaves(c, ...);
- 调用
-
同时,更新命令统计,如
server.stat_numcommands++
。
-
Redis 内部数据结构
Redis 的内存数据结构层——Redis 如何在内存中组织和管理各种对象,才能在单线程模型下实现高性能与高扩展性。
一、RedisDb 结构
-
多库支持:每个实例默认可配置 16 个逻辑库(
db0
~db15
),通过命令SELECT $dbID
切换。 -
核心字典
dict
(主字典):存储 key → value 映射expires
:存储 key → 过期时间
-
非核心字典
blocking_keys
:记录 BLPOP/BRPOP 等阻塞列表的 key → client 列表ready_keys
:当元素入队触发唤醒时,将 key 加入此字典与全局server.read_keys
列表中watched_keys
:用于事务 WATCH 监控的 key → client 列表
二、redisObject 抽象
Redis 中任何存储的值都封装为 redisObject
,包含五个核心字段:
- type:对象类型(
OBJ_STRING
、OBJ_LIST
、OBJ_SET
、OBJ_ZSET
、OBJ_HASH
、OBJ_MODULE
、OBJ_STREAM
) - encoding:底层编码(如
RAW
、INT
、HT
、ZIPLIST
等) - LRU:用于 LRU/LFU 淘汰策略的访问记录
- refcount:引用计数,支持对象共享与内存自动回收
- ptr:指向具体底层数据结构(如
sds
、dict
、ziplist
、quicklist
、zskiplist
等)
三、dict 哈希表
-
双表设计:
dict
结构内维护长度为 2 的哈希表数组ht[0]
、ht[1]
-
渐进式 rehash
- 当
ht[0]
装载因子超阈值,分配ht[1]
(容量为ht[0]
的两倍) - 每次哈希操作顺带迁移部分桶,使用
rehashidx
记录迁移进度
- 当
-
冲突解决:每个桶为
dictEntry
的单向链表
-
灵活可扩展:
dict
可用于主字典、过期字典,也可作为 Set、Hash 类型的内部存储
四、sds 简单动态字符串
-
基本结构:底层为
sdshdr
+char buf[]
len
:当前字符串长度alloc
:已分配空间大小flags
:类型与子类型标志buf
:字符数据(二进制安全,允许包含\0
)
-
多种子类型(从 Redis 3.2 起)
sdshdr5
:极短字符串,仅flags
+buf
sdshdr8/16/32/64
:根据长度选择合适的整型字段,节省内存
-
优势
- O(1) 获取长度,无需遍历
- 动态扩展与收缩,二进制安全
五、压缩列表(ziplist)
为了节约内存,并减少内存碎片,Redis 设计了 ziplist 压缩列表内部数据结构。压缩列表是一块连续的内存空间,可以连续存储多个元素,没有冗余空间,是一种连续内存数据块组成的顺序型内存结构。
-
连续内存布局,减少指针开销与碎片
-
结构字段
zlbytes
:总字节数zltail
:尾节点距起始偏移zllen
:节点数量entry…entry…
:各节点数据zlend
:结束标志(255)
-
节点格式
- 前驱长度、编码长度、实际数据长度、编码类型、数据
-
适用场景
- 小型 Hash(默认 ≤512 项、值 ≤64B)
- 小型 ZSet(默认 ≤128 项、值 ≤64B)
六、快速列表(quicklist)
Redis 在 3.2 版本之后引入 quicklist,用以替换 linkedlist。因为 linkedlist 每个节点有前后指针,要占用 16 字节,而且每个节点独立分配内存,很容易加剧内存的碎片化。而 ziplist 由于紧凑型存储,增加元素需要 realloc,删除元素需要内存拷贝,天然不适合元素太多、value 太大的存储。
-
设计目标:结合 ziplist 的紧凑与 linkedlist 的灵活
-
结构
- 双向链表节点
quicklistNode
,每节点包含一个 ziplist head
、tail
指针;count
(总元素数);len
(节点数);compress
(LZF 压缩深度)
- 双向链表节点
-
优点
- 头尾操作 O(1)
- 避免过多内存碎片
- 支持中间位置操作(O(n))
七、跳跃表(zskiplist)
跳跃表 zskiplist 是一种有序数据结构,它通过在每个节点维持多个指向其他节点的指针,从而可以加速访问。跳跃表支持平均 O(logN) 和最差 O(n) 复杂度的节点查找。在大部分场景,跳跃表的效率和平衡树接近,但跳跃表的实现比平衡树要简单,所以不少程序都用跳跃表来替换平衡树。
-
多级索引:在每个节点维护多层前进指针与跨度,近似平衡树性能
-
结构
zskiplist
:header
、tail
、length
、level
zskiplistNode
:ele
(sds)、score
、backward
、多级level[i]
(forward
+span
)
-
性能
- 平均 O(log N) 查找/插入/删除
- 同分数元素按字典序排序
-
适用场景
- 大型 Sorted Set、Geo 类型(超出 ziplist 阈值)
八、数据类型与内部结构映射
数据类型 | 内部存储结构 |
---|---|
String | sds / 整数对象 |
List | quicklist |
Set | dict |
Hash | ziplist(小型)/ dict(大型) |
Sorted Set | ziplist(小型)/ zskiplist(大型) |
Stream | radix tree + listpacks |
HyperLogLog | sds |
Bitmap | sds |
Geo | ziplist(小型)/ zskiplist(大型) |
Redis 淘汰策略
当 Redis 内存到达或超过 maxmemory
限制时,系统如何精确、高效地清理无用或不活跃的数据,保障缓存的命中率与访问性能。
一、淘汰原理
-
内存阈值与触发条件
- 通过配置
maxmemory
设置 Redis 可用的最大内存。 - 当内存使用量超过阈值,或在定期过期检查时发现过期 key,均触发淘汰动作。
- 通过配置
-
场景一:定期过期扫描(serverCron)
-
周期性执行
serverCron
,对每个redisDb
的expires
过期字典进行采样:- 随机取 20 个带过期时间的 key 样本;
- 若其中超过 5 个已过期(比例 >25%),继续取样并清理,直至过期比例 ≤25% 或 时间耗尽;
-
若某 DB 的过期字典填充率 <1%,则跳过采样。
-
为避免阻塞主线程,清理时限:
- Redis 5.0 及之前:慢循环策略,默认 25ms;
- Redis 6.0:快循环策略,限时 1ms。
-
-
场景二:命令执行时检查
- 在每次执行命令前,检查当前内存占用是否已超限;
- 若超限,则立即依据所选
maxmemory-policy
进行 key 淘汰,释放内存后再继续执行写命令。
二、淘汰方式
Redis 提供两种删除方式,以平衡主线程响应和内存回收的及时性:
-
同步删除
- 直接在主线程中删除 key 及其 value,并同步回收内存;
- 适用于简单值或复合类型元素数 ≤64 的情况。
-
异步删除(Lazy Free)
-
依赖 BIO 线程池异步回收内存,避免主线程因大对象删除而阻塞;
-
触发条件:
lazyfree-lazy-expire
:延迟过期清理;lazyfree-lazy-eviction
:延迟淘汰时;
-
对象类型:list、set、hash、zset 中,元素数 >64 时使用。
-
三、淘汰策略与 Eviction Pool
为在维持高性能的同时,尽可能剔除最“冷”的数据,Redis 在淘汰前会:
-
随机采样 N 个 key(默认为 5)
-
计算每个样本的“Idle”值
- 对于 LRU,用空闲时间;LFU 则用
255 – 频率
;TTL 策略以UINT_MAX – 过期时间
;
- 对于 LRU,用空闲时间;LFU 则用
-
维护大小为 N 的 Eviction Pool
- 按 Idle 从小到大插入,始终保留 Idle 最大的样本;
-
最终剔除 Pool 中 Idle 最大的 key
四、八种淘汰策略详解
策略名称 | 作用范围 | 算法原理 | 适用场景 |
---|---|---|---|
noeviction | 不淘汰任何 key | 达到内存上限后,对写命令返回错误,读命令正常 | 小规模数据,Redis 作为持久存储而非缓存 |
volatile-lru | 带过期时间的 key | 基于 LRU,从 expires 中随机 N 样本,剔除空闲时间最长的 key | 热点数据明显,且淘汰对象均已设置过期时间的缓存场景 |
volatile-lfu | 带过期时间的 key | 基于 LFU,从 expires 随机 N 样本,剔除使用频率最低的 key(Idle = 255–freq) | 访问频率具有明显冷热区分的业务,且仅淘汰已设置过期时间的对象 |
volatile-ttl | 带过期时间的 key | 剔除最近到期的 key(Idle = UINT_MAX–TTL) | 按剩余生命周期冷热分区,优先清理即将过期的数据 |
volatile-random | 带过期时间的 key | 从 expires 随机选一个 key 直接剔除 | 无明显访问热点,且仅对带过期时间的对象进行随机清理 |
allkeys-lru | 所有 key | 与 volatile-lru 类似,但样本来自主字典 dict | 全局范围的 LRU 淘汰,适合全量缓存且有热点区分的场景 |
allkeys-lfu | 所有 key | 与 volatile-lfu 类似,样本来自主字典 dict | 访问频率冷热明显,需要对所有 key 进行频率淘汰的场景 |
allkeys-random | 所有 key | 从主字典 dict 随机选一个 key 直接剔除 | 随机访问场景,无明显热点,全局随机淘汰 |
Redis 的三种持久化方案及崩溃后数据恢复流程
Redis 持久化是一个将内存数据转储到磁盘的过程。Redis 目前支持 RDB、AOF,以及混合存储三种模式。
一、RDB 持久化
-
原理概述
- 以快照方式将内存全量数据序列化为二进制格式,包含:过期时间、数据类型、key 与 value;
- 重启时(
appendonly
关闭),直接加载 RDB 文件恢复数据。
-
触发场景
- 手动执行
SAVE
(阻塞主进程)或BGSAVE
(子进程异步); - 配置
save <秒> <次数>
:在指定时间内写操作次数达到阈值自动触发; - 主从复制全量同步时,主库为了生成同步快照会执行
BGSAVE
; - 执行
FLUSHALL
或优雅SHUTDOWN
时,自动触发快照。
- 手动执行
-
RDB 文件结构
- 头部:版本信息、Redis 版本、生成时间、内存占用等;
- 数据区:按 DBID 分块,依次写入每个
redisDb
的主字典与过期字典条目,记录过期时间及 LRU/LFU 元数据; - 尾部:Lua 脚本等附加信息、EOF 标记(255)、校验和(cksum)。
-
优缺点
- 优点:文件紧凑、加载快;
- 缺点:全量快照只能反映触发时刻数据,之后变更丢失;子进程构建仍消耗 CPU,不能频繁在高峰期执行;格式二进制,可读性差,跨版本兼容性需谨慎。
二、AOF 持久化
-
原理概述
- 将每条写命令以 Redis 协议的 MULTIBULK 格式追加到 AOF 文件;
- 重启时,按序加载并重放写命令,恢复到最近状态。
-
落地流程
- 写命令执行后写入 AOF 缓冲;
serverCron
周期将缓冲写入文件系统缓冲;- 按
appendfsync
策略fsync
同步到磁盘。
-
同步策略
no
:不主动fsync
,依赖操作系统(约 30s 同步),风险大;always
:每次写缓冲后都fsync
,最安全但性能和磁盘寿命受影响;everysec
:每秒一次异步fsync
(BIO 线程),在安全性与性能间折中。
-
AOF 重写(Rewrite)
-
通过
BGREWRITEAOF
或自动触发,fork
子进程生成精简命令集:- 子进程扫描每个
redisDb
,将内存快照转为写命令写入临时文件; - 主进程继续处理请求,并将写命令同时写入旧 AOF 与 rewrite 缓冲;
- 子进程完成后,主进程合并 rewrite 缓冲并替换旧文件;旧文件由 BIO 异步关闭。
- 子进程扫描每个
-
-
优缺点
- 优点:记录所有写操作,最多丢失 1–2 秒;兼容性好、可读;
- 缺点:文件随时间增大,包含大量中间状态;恢复时需重放命令,速度相对较慢。
三、混合持久化
-
原理与配置
- 自 Redis 4.0 引入,5.0 默认开启;配置
aof-use-rdb-preamble yes
。 BGREWRITEAOF
时,子进程先将全量内存数据以 RDB 格式写入 AOF 临时文件,再追加期间新增写命令;
- 自 Redis 4.0 引入,5.0 默认开启;配置
-
流程
fork
子进程;- 子进程将内存快照写为 RDB 格式到临时文件;
- 追加子进程运行期间主进程缓冲的写命令;
- 通知主进程替换 AOF,旧文件异步关闭。
-
优缺点
- 优点:兼具 RDB 加载快与 AOF 新数据保留特性;恢复速度快且几乎无数据丢失;
- 缺点:头部 RDB 部分依然为二进制,不易阅读;跨版本兼容需测试。
- RDB:全量快照,文件小、加载快,但存在数据丢失窗口;
- AOF:命令追加,几乎无丢失,兼容性高,但文件大、重放慢;
- 混合:RDB+AOF,一体化折中方案。
Redis 后台异步 IO(BIO)
Redis 核心线程单线程模型虽能高效处理多数操作,但对文件关闭、磁盘同步、以及大对象的逐一回收等系统调用依然容易导致短时阻塞,从而影响整体吞吐和响应延迟。接下来深入介绍 Redis 如何通过后台 IO(BIO)线程,将这些“慢任务”异步化,保证主线程的高可用性与低延迟。
一、BIO 线程设计动机
-
单线程模型的挑战
- 主线程需处理所有客户端请求、过期清理、淘汰等,性能极高;
- 若再执行如
close()
、fsync()
、大对象释放等系统调用,短则数毫秒、长则上百毫秒,都将阻塞请求处理,造成卡顿;
-
异步化解决思路
- 将这些“慢任务”提交给后台线程异步执行;
- 主线程仅需快速入队并继续服务,显著降低响应延迟波动。
二、BIO 线程模型
Redis 采用经典的生产者-消费者模式:
- 生产者:主线程在检测到慢任务时,构建相应的 BIO 任务结构并入队;
- 消费者:专属的 BIO 线程阻塞等待队列中的新任务,一旦被唤醒即取出并执行;
- 同步机制:使用互斥锁保护队列,条件变量实现高效唤醒/等待,确保线程安全与低开销。
三、BIO 任务类型
Redis 启动时,为三类任务分别创建独立的任务队列与线程:
BIO 线程名称 | 任务队列 | 主要用途 |
---|---|---|
close | closeQ | 关闭旧 AOF/客户端/其他文件描述符,避免主线程被 close() 阻塞 |
fsync | fsyncQ | 将内核文件缓冲区的内容强制同步到磁盘(fsync() ),保障数据持久化 |
lazyfree | lazyfreeQ | 异步回收大对象(元素数 >64 的 list/set/hash/zset),避免主线程长时间释放 |
四、BIO 处理流程
-
任务提交(主线程)
- 根据任务类型分配 BIO 任务;
- 加锁后将任务追加到对应队列尾部;
- 通过条件变量唤醒等待的 BIO 线程;
-
任务消费(BIO 线程)
- 阻塞等待新任务到来;
- 取出并执行对应系统调用或对象释放;
- 任务完成后释放任务结构,继续等待;
通过引入专门的 BIO 后台线程队列,Redis 将所有可能导致短时阻塞的系统调用与大对象回收异步化处理,从而最大限度地保障主线程的低延迟和高吞吐能力。
Redis 多线程架构
Redis 自身单进程单线程模型极大简化了并发控制、保证了命令执行的原子性,但也限制了吞吐能力。相比 Memcached 能够通过多线程轻松跑出百万级 TPS,Redis 单实例 TPS 往往在 10–12 万左右,线上峰值也多在 2–4 万,难以充分利用现代 16+ 核服务器。为解决这一痛点,Redis 6.0 在不改动现有核心执行逻辑的前提下,引入了可选的 IO 多线程模型,以并行化网络读写与协议解析,从而在保留主线程执行安全的同时,实现 1–2 倍的性能提升。
一、主线程职责
- 事件驱动 Loop(
ae
):监听客户端连接、读写事件与定时任务,不变; - 命令执行:所有实际的业务命令处理逻辑继续在单一主线程中运行,保持原子性与简单的调度;
- 核心任务分发:在网络 IO 上,主线程将读写请求委派给 IO 线程;在命令执行完毕后,将回复操作同样回交给 IO 线程处理。
二、IO 线程设计
- 目标:并行化耗时主要集中在三处:网络读取、协议解析与响应写入;
- 配置:可通过
io-threads
和io-threads-do-reads
参数开启与设置线程数(典型 4–8 个); - 模型:主线程负责将待读或待写的
client
对象加入对应队列,IO 线程异步批量拉取并行处理,处理完后主线程再继续后续逻辑。
三、命令处理完整流程
Redis 6.0 的多线程处理流程如图所示。主线程负责监听端口,注册连接读事件。当有新连接进入时,主线程 accept 新连接,创建 client,并为新连接注册请求读事件。
-
连接与读事件注册
- 主线程
accept()
新连接,创建client
,注册可读事件;
- 主线程
-
并行读取与解析
- 读事件触发时,主线程不直接读取,而将
client
加入待读取队列; - 当一轮事件循环结束,发现待读取链表非空,主线程将所有待读
client
分派给 IO 线程; - IO 线程并行读取各自
client->fd
,将原始数据填入client->querybuf
,并执行协议解析,填充client->argc/argv
; - IO 线程处理完所有任务后更新待处理计数,主线程自旋等待计数归零。
- 读事件触发时,主线程不直接读取,而将
-
命令执行
- 主线程依次取出已解析的命令,调用原有
redisCommand
处理函数执行;
- 主线程依次取出已解析的命令,调用原有
-
并行响应写入
- 执行结束后,主线程通过
addReply*
系列将结果填入client->buf
,并将client
加入待写队列; - 将队列再次分派给 IO 线程并自旋等待;
- IO 线程并行将
client->buf
写回各自连接,完成响应; - 主线程检测所有写入完成后,继续下一轮事件循环。
- 执行结束后,主线程通过
四、多线程方案优劣
优点 | 缺点与瓶颈 |
---|---|
- 并行化网络 IO 和协议解析,减少主线程阻塞,整体 TPS 提升 1–2 倍 | - 命令执行与事件调度仍集中于单一主线程,难以突破核心逻辑瓶颈 |
- 利用多核 CPU 优化网络吞吐,客户端连接并发性能更好 | - IO 批量处理模式需要“先读完再写回”,客户端间相互等待,增加延迟抖动 |
- 保留原子性与简洁性,无需改动现有命令实现 | - 主线程自旋等待 IO 线程完成,若任务少也会高频自旋,浪费 CPU 资源 |
- 部分场景下性能提升有限,无法替代真正的多线程命令处理模型 |
整体来看,Redis 6.0 的 IO 多线程是一次低侵入式的性能优化,能在不破坏兼容性与原子性的前提下带来可观提升。但要实现数量级跃升,还需将命令处理、事件调度等核心逻辑多线程化,解耦互斥等待,并逐步演进为真正的全栈并行模型。
复制架构原理
为了避免单点故障、提高可用性与读性能,必须对数据进行多副本存储。Redis 作为高性能的内存数据库,从一开始就内建了主从复制功能,并在各个版本迭代中不断优化复制策略。
一、复制架构原理
- 多层嵌套复制:一个 Master 可以挂载多个 Slave,Slave 也可继续挂载更多下游 Slave,形成树状层级结构。
- 写操作分发:所有写命令仅在 Master 节点执行,执行完后即时分发给下游所有 Slave,保证数据一致。
- 读写分离:Master 只负责写请求,所有读请求由 Slave 处理。这种架构既消除了单点故障风险,又通过 N 倍 Slave 并发提升了读 TPS。
此外,Master 在向 Slave 分发写命令的同时,会将写指令保存到复制积压缓冲区(replication backlog),以便短时断连的 Slave 重连后增量同步。
二、同步方式对比
同步类型 | 描述 | 优势 | 劣势 |
---|---|---|---|
全量同步 | Master 生成 RDB 快照并传输给 Slave,同时发送缓冲区积压命令,Slave 全量重建数据。 | 数据完整,适用于首次同步 | 构建 RDB 和网络传输压力大,耗时长 |
增量同步 | Master 仅发送自上次同步位置之后的写命令,无需生成 RDB。 | 轻量、带宽占用极低、无 RDB 构建延迟 | 依赖缓冲区容量和断连时长,容易导致全量重试 |
三、psync 与 psync2 优化
-
psync(Redis 2.8+)
- 引入复制积压缓冲区
- Slave 重连时上报 runid 与偏移量
- 若 runid 一致且偏移仍在缓冲区,则返回
CONTINUE
,进行增量同步;否则触发全量同步
-
psync2(Redis 4.0+)
- runid 升级为 replid 与 replid2
- RDB 文件中存储 replid 作为 aux 信息,重启后可保留 replid
- 切主时,通过 replid2 支持跨主机增量同步
相比早期版本,psync2 在短链路抖动、Slave 重启和主库切换等场景中,均能在更多情况下保持增量同步,显著降低性能开销与恢复时间。
四、复制连接与授权流程
-
连接检测
- Slave 向 Master 发送
PING
→ 收到PONG
则可用
- Slave 向 Master 发送
-
鉴权(若启用密码)
- Slave 发送
AUTH <masterauth>
- Slave 发送
-
能力协商
- Slave 通过
REPLCONF
上报自身 IP、端口及支持的eof
、psync2
能力
- Slave 通过
-
同步请求
- Slave 发送
PSYNC <replid> <offset>
- Master 根据 replid、replid2 与复制积压缓冲,决定全量或增量
- Slave 发送
五、复制过程详析
5.1 增量同步流程
- Master 返回
CONTINUE <replid>
- Slave 将自身 replid 更新为 Master 返回的 replid,将原 replid 存为 replid2
- Master 从偏移量继续推送写命令
5.2 全量同步流程
- Master 返回
FULLRESYNC <replid> <offset>
- Master 执行
BGSAVE
生成新的 RDB 快照 - 将 RDB 与复制缓冲区命令一起推送给 Slave
- Slave 关闭下游子 Slave 连接,清空本地缓冲
- Slave 写入临时 RDB 文件(每 8MB fsync)→ 重命名 → 清库 → 加载 RDB
- 重建与 Master 的命令推送通道,并开启 AOF 持久化
六、注意事项
- 缓冲区大小:过大会占用过多内存;过小易导致缓冲刷出,触发全量复制
- 网络稳定性:长连接抖动或丢包会影响复制效率
- 监控复制延迟:及时预警和扩容,避免生产环境中读数据不一致或延迟过高
- 主库切换策略:在切换 Master 前保证所有 Slave 与当前 Master 完成同步
Redis 集群的分布式方案
Redis 的分布式方案主要分为三类:
- Client 端分区
- Proxy 分区
- 原生 Redis Cluster
下面将逐一介绍它们的设计思路、实现方式及各自优缺点
1. Client 端分区
1.1 原理与哈希算法
客户端通过哈希算法决定某个 key 应存储在哪个分片(Shard)上,常见算法包括:
- 取模哈希:
hash(key) % N
- 一致性哈希:在哈希环上分配虚拟节点,实现动态扩缩容的平滑性
- 区间分布哈希:实际是取模的变种,将 Hash 输出映射到固定区间,再由区间决定分片
对于单 key 请求,客户端直接计算哈希并路由;对于包含多个 key 的请求,客户端先对 key 按分片分组,再拆分成多条请求并发执行。
1.2 DNS 动态管理
由于每个 Redis 分片的 Master/Slave 都有独立 IP:Port,当发生故障切换或新增 Slave 时,客户端需更新连接列表。
- DNS 管理:为每个分片的主/从分别配置不同域名,客户端定时异步解析域名、更新连接池
- 负载均衡:按权重将请求在各 Slave 之间轮询,既可分散读压,又无需业务侧改动
1.3 优缺点
- 优点:无中心依赖、逻辑简单、性能最优(无额外代理),客户端可灵活控制
- 缺点:扩展不够平滑(新增分片需修改客户端逻辑并重启)、业务端分片逻辑耦合
2. Proxy 分区方案
2.1 架构概览
客户端只需连接到统一的 Proxy 层,由 Proxy 完成路由、拆分及聚合:
- 接收请求 → 解析命令 → 哈希计算 → 路由到对应 Redis → 聚合响应 → 返回客户端
这样,客户端免维护分片信息,真正的分布式逻辑都隐藏在 Proxy 之下。
2.2 典型实现
方案 | 特点 | 扩缩容 | 性能损耗 |
---|---|---|---|
Twemproxy | 单进程单线程;实现简单、稳定;不支持平滑扩缩 | 重启 Proxy | ~5–15% |
Codis | 支持在线数据迁移;丰富的 Dashboard;多实例 | Dashboard+ZK/etcd | 略高于 Twemproxy |
- Twemproxy:适合小规模、几乎不扩缩容的场景;但单线程模型对多 key 请求性能有限
- Codis:基于 Redis 扩展的 Slot 方案,提供 dashboard 管理,支持在线扩缩容
2.3 优缺点
- 优点:客户端无需感知分片;扩缩容仅改 Proxy,运维便利
- 缺点:增加访问中间层,带来约 5–15% 的性能开销;系统更复杂
3. 原生 Redis Cluster
3.1 Slot 与 Gossip 架构
- 16384 个 Slot:启动时通过
CLUSTER ADDSLOTS
将 Slot 分配到各节点,key 经 CRC16 哈希后落在具体 Slot - Gossip 协议:节点间去中心化通信,更新拓扑无需中心节点,操作通过
cluster meet
等命令扩散
3.2 读写与重定向
- Smart Client 缓存 Slot→节点映射
- 若请求到错节点,返回
MOVED
或ASK
,包含正确节点信息,客户端解析后重定向 - 迁移过程中,新旧节点返回
ASK
并引导客户端临时访问迁移节点
3.3 在线扩缩容与数据迁移
cluster meet
加入新节点(无 Slot,不可读写)- 源节点
cluster setslot slot migrating
,目标节点… importing
cluster getkeysinslot
+migrate
迁移 key 数据;迁移期间阻塞该进程cluster setslot slot nodeid
分配 Slot- 为新主节点添加 Slave:使用
cluster replicate
,Slave 只能挂到 Master
缩容则相反:先迁移 Slot,再 cluster forget
下线节点(并加入禁止列表)。
3.4 优缺点
- 优点:社区官方实现;在线扩缩容;无中心依赖;自动故障转移
- 缺点:Slot 与 key 映射占内存;迁移阻塞导致卡顿;复制链路单层限制了读扩展
4. 对比与选型建议
维度 | Client 分区 | Proxy 分区 | Redis Cluster |
---|---|---|---|
客户端维护 | 高 | 无 | 需 Smart Client |
扩缩容平滑度 | 低 | 中(Proxy 重启) | 高(在线迁移) |
性能开销 | 最小 | 中等(5–15%) | 较低 |
运维复杂度 | 业务侧 | Proxy 层 | 集群管理 |
成熟度 | 通用方案 | Codis、Twemproxy | 官方支持 |
- 读密集场景:若对扩缩容需求不强,且对性能最敏感,可考虑 Client 分区。
- 写扩展与在线迁移:需平滑扩缩容,且可接受少量代理层开销,推荐 Codis 或 Redis Cluster。
- 大规模复杂部署:倾向官方 Redis Cluster,享受社区生态和原生工具支持。
三种方案各有侧重:Client 分区最轻量、性能最高;Proxy 分区运维便利;Redis Cluster 原生、弹性最佳。实际生产中,可根据业务特性和运维成本做权衡。