Redis基础与核心原理

发布时间:2026/7/13 2:16:27
Redis基础与核心原理 Redis 基础与核心原理一、Redis 是什么RedisRemote Dictionary Server的本质定位Redis 的全称是Remote Dictionary Server即远程字典服务。从名字可以看出Redis 的核心抽象就是一个通过网络远程访问的字典Dictionary。它是一个开源的、基于内存的、支持持久化的键值对Key-Value存储系统由 Salvatore Sanfilippoantirez于 2009 年开发目前由 Redis Ltd. 维护。Redis 最初的设计目标非常简单——解决传统关系型数据库在高并发场景下的性能瓶颈。随着生态的不断发展Redis 已经从一个单纯的内存缓存工具演变为一个功能丰富的多模数据平台Multi-model Data Platform。官方定义的五大角色Redis 承担以下五大核心角色角色英文说明数据库Database作为持久化的内存数据库支持事务Transaction、Lua 脚本、Module 扩展缓存Cache最经典的用法利用内存的高速读写特性缓存热点数据减轻后端数据库压力消息代理Message Broker通过 Pub/Sub 机制实现消息的发布与订阅支持消息分发流处理引擎Streaming EngineRedis 5.0 引入的 Stream 数据类型支持消费者组Consumer Group可实现类似 Kafka 的消息流处理向量数据库Vector StoreRedis 8 原生支持向量搜索Vector Search可用于 AI/ML 场景中的相似性检索Similarity Search值得注意的是这五大角色并非互斥在实际生产环境中一个 Redis 实例往往同时承担多种角色。核心特点Redis 之所以能在工业界被广泛采用源于以下核心特点的协同内存存储In-memory Storage所有数据驻留在内存中读写延迟极低微秒级单线程事件循环Single-threaded Event Loop命令执行线程为单线程避免了锁竞争等并发问题高性能High Performance官方基准测试可达百万级 QPSQueries Per Second丰富数据结构Rich Data StructuresString、List、Hash、Set、ZSet、Stream、HyperLogLog、Bitmap、Geospatial 等持久化Persistence支持 RDB 快照和 AOF 日志两种持久化方式内存数据可落盘复制Replication主从复制Master-Slave Replication支持读写分离集群ClusterRedis Cluster 提供分布式分片方案支持水平扩展事务Transaction通过 MULTI/EXEC/WATCH 提供事务支持Lua 脚本Lua Scripting支持原子性执行的 Lua 脚本实现复杂逻辑Module 扩展Module System可加载第三方模块扩展功能如 RediSearch、RedisJSON、RedisTimeSeries 等Redis 与 Memcached 的关键区别对比维度RedisMemcached数据结构丰富String/List/Hash/Set/ZSet/Stream 等仅支持简单的 Key-ValueString持久化支持 RDB AOF数据可落盘纯内存重启数据丢失集群原生支持 Redis Cluster 分片需要客户端一致性哈希分片线程模型命令执行单线程IO 多线程Redis 6多线程内存管理自定义内存分配器jemalloc支持多种淘汰策略Slab AllocatorLRU 淘汰事务支持 MULTI/EXEC/WATCH不支持事务消息模型支持 Pub/Sub 和 Stream不支持Lua 脚本支持不支持适用场景缓存 数据库 消息队列等多角色纯缓存场景总结Memcached 是一个纯粹的内存缓存系统而 Redis 是一个多功能的内存数据平台。在新项目中Redis 几乎完全取代了 Memcached 的生态位。二、Redis 为什么这么快Redis 的高性能并非由单一因素决定而是多个设计决策协同作用的结果。下面逐一拆解。2.1 基于内存内存 vs SSD vs HDD 的访问延迟对比存储介质典型访问延迟数量级CPU L1 Cache0.5 ns纳秒级CPU L2 Cache5 ns纳秒级内存DRAM100 ns纳秒级SSDSolid State Drive100,000 ns (0.1 ms)微秒级HDDHard Disk Drive10,000,000 ns (10 ms)毫秒级从上表可以看出内存的访问速度是 SSD 的约 1000 倍是 HDD 的约 100,000 倍。这意味着即使磁盘数据库使用了再精妙的索引结构如 BTree其访问延迟也无法与直接操作内存相提并论。Redis 快的本质不是算法快而是数据存储位置不同很多人误以为 Redis 快是因为使用了某种黑科技算法。实际上Redis 快的根本原因是数据存储在内存中。内存的随机访问延迟约为 100 纳秒而磁盘的随机访问延迟约为 10 毫秒两者差了 5 个数量级。这意味着即使 Redis 使用最简单的线性查找在内存中的效率也远超磁盘数据库使用 BTree 的效率。当然Redis 并非只靠内存——它在数据结构和网络模型上也做了大量优化这些优化让 Redis 把内存的性能潜力发挥到了极致。伪代码内存读取 vs 磁盘读取的耗时对比// 内存读取 function memoryRead(key): address hash(key) % memoryTableSize // O(1) 哈希定位 value memory[address] // 约 100ns return value // 磁盘读取即使有 BTree 索引 function diskRead(key): rootPage readFromDisk(rootPageOffset) // 第1次磁盘IO约 10ms childPage readFromDisk(rootPage.child) // 第2次磁盘IO约 10ms leafPage readFromDisk(childPage.child) // 第3次磁盘IO约 10ms value leafPage.find(key) return value // 耗时对比 // memoryRead: ~100ns 0.0001ms // diskRead: ~30ms // 内存读取比磁盘读取快约 300,000 倍2.2 单线程模型为什么 Redis 选择单线程瓶颈在网络 IO 而非 CPURedis 的核心设计哲学是对于内存数据库来说性能瓶颈不在 CPU 计算而在网络 IO。一次 Redis 命令的典型执行流程如下从网络读取请求网络 IO—— 微秒级解析命令CPU—— 纳秒级在内存中查找并执行命令CPU 内存—— 纳秒级将结果写回网络网络 IO—— 微秒级可以看到CPU 计算只占极小的比例真正耗时的是网络 IO。因此用多线程来加速 CPU 计算意义不大反而会引入复杂的并发控制问题。单线程避免的问题锁竞争Lock Contention多线程访问共享数据结构需要加锁锁竞争会导致线程频繁阻塞和唤醒降低吞吐量上下文切换Context Switch线程切换需要保存和恢复寄存器、缓存等状态频繁切换浪费 CPU 资源Cache Miss多线程在不同 CPU 核心上运行可能导致 CPU Cache Line 的频繁失效False Sharing增加内存访问延迟Redis 6 的多线程 IO仅网络 IO 多线程命令执行仍单线程Redis 6 引入了 IO 多线程Threaded I/O但需要注意的是多线程仅用于网络读写命令的执行仍然是单线程的。这样既提升了网络 IO 的吞吐量又保证了命令执行的原子性和无锁特性。伪代码单线程事件循环的核心逻辑// Redis 单线程事件循环的核心逻辑 function eventLoop(): while server is running: // 1. 通过 epoll 收集就绪的事件 readyEvents epoll_wait(epfd, events, timeout) // 2. 遍历所有就绪事件 for event in readyEvents: if event.type ACCEPT: // 新连接到达 clientFd accept(listenFd) setNonBlocking(clientFd) epoll_ctl(epfd, ADD, clientFd, READ_EVENT) else if event.type READ: // 客户端请求到达 request read(event.fd) command parseCommand(request) // 关键命令执行是单线程的无需加锁 result executeCommand(command) // 将结果加入写缓冲区 appendToWriteBuffer(event.fd, result) else if event.type WRITE: // 将缓冲区数据写回客户端 flushWriteBuffer(event.fd) // 3. 执行定时任务 processTimedEvents()2.3 IO 多路复用Linux IO 模型演进select - poll - epoll在介绍 Redis 的 IO 模型之前先理解 Linux IO 多路复用的演进select早期方案使用固定长度的位图bitmap表示文件描述符File Descriptor, FD集合每次调用需要将整个 FD 集合从用户态拷贝到内核态支持的最大 FD 数量受限默认 1024由 FD_SETSIZE 决定时间复杂度 O(n)每次都需要遍历全部 FDpoll改进方案用动态数组替代位图取消了 1024 的数量限制但仍需将整个 FD 集合拷贝到内核态时间复杂度仍为 O(n)epoll现代方案使用红黑树管理 FD支持百万级连接通过事件回调机制Callback只返回就绪的 FD时间复杂度 O(1)使用 mmap 共享内存区域避免用户态和内核态之间的数据拷贝支持 ET边缘触发和 LT水平触发两种模式Redis 默认使用 epoll一个线程监听数万 TCP 连接Redis 在 Linux 上默认使用 epoll 作为 IO 多路复用的实现在 macOS 上使用 kqueue在 Windows 上使用 select。epoll 的高效事件通知机制使得 Redis 仅用一个线程就能同时监听数万个 TCP 连接并在连接就绪时进行处理。epoll 的 ET边缘触发vs LT水平触发模式模式英文触发条件特点LTLevel Triggered只要缓冲区有数据就持续触发编程简单不容易漏事件但可能频繁触发ETEdge Triggered缓冲区从空变为非空时触发一次效率更高但必须一次性读完所有数据否则会丢失事件Redis 使用的是LT水平触发模式。原因在于 Redis 的事件循环是单线程的LT 模式编程更简单不需要担心一次性未读完数据导致的事件丢失问题。伪代码epoll 事件循环伪代码// epoll 事件循环伪代码 function epollEventLoop(): // 1. 创建 epoll 实例 epfd epoll_create1(0) // 2. 将监听套接字注册到 epoll epoll_ctl(epfd, EPOLL_CTL_ADD, listenFd, { events: EPOLLIN, // 关注可读事件 data: { type: ACCEPT } // 标记为连接事件 }) // 3. 事件循环 while True: // 等待事件就绪返回就绪的 FD 列表 // 只返回有事件的 FD不需要遍历全部连接 readyList epoll_wait(epfd, maxEvents, timeout) for event in readyList: if event.data.type ACCEPT: // 新连接到达 clientFd accept(listenFd) setNonBlocking(clientFd) // 设置为非阻塞 epoll_ctl(epfd, EPOLL_CTL_ADD, clientFd, { events: EPOLLIN, data: { type: CLIENT, fd: clientFd } }) else if event.data.type CLIENT: if event.events EPOLLIN: // 客户端数据可读 data read(event.data.fd) if data.length 0: processCommand(data) else: // 连接关闭 epoll_ctl(epfd, EPOLL_CTL_DEL, event.data.fd) if event.events EPOLLOUT: // 可以向客户端写数据 flushWriteBuffer(event.data.fd)2.4 非阻塞 IOaccept()/read()/write() 全部非阻塞Redis 将所有网络套接字都设置为非阻塞模式Non-blocking IO。这意味着accept()如果没有新连接到达立即返回 EAGAIN不会阻塞等待read()如果缓冲区没有数据立即返回 EAGAIN不会阻塞等待write()如果缓冲区已满立即返回 EAGAIN不会阻塞等待非阻塞 IO 配合 epoll 的事件通知机制使得 Redis 的事件循环永远不会因为某个慢连接而卡住。为什么一个线程就能处理大量连接核心原理不要等待轮询就绪。传统阻塞 IO 模型中一个线程只能处理一个连接因为 read() 会阻塞。而 Redis 的做法是将所有连接设置为非阻塞用 epoll 监听所有连接的就绪状态只有当连接有数据可读/可写时才去处理处理完立即返回事件循环继续处理其他连接这样一个线程可以高效地在数万个连接之间切换处理没有任何阻塞等待的时间浪费。2.5 高效数据结构Redis 底层不是简单 HashMap而是精心优化的数据结构很多人以为 Redis 就是把数据放在一个大的 HashMap 里实际上 Redis 对每种数据类型的底层实现都做了深度的优化针对不同场景使用不同的编码方式在内存占用和操作效率之间取得了极佳的平衡。数据结构概览表数据类型底层编码核心优势StringSDSSimple Dynamic StringO(1) 获取长度二进制安全杜绝缓冲区溢出预分配惰性释放减少内存重分配HashListPack小数据/ Hashtable大数据ListPack 紧凑存储省内存Hashtable O(1) 查找自动根据数据量切换编码ListQuickList双向链表 ListPack兼顾链表的快速插入删除和 ListPack 的紧凑存储节点间用 ziplist/ListPack 压缩SetIntSet纯整数/ Hashtable混合类型IntSet 有序紧凑存储纯整数集合Hashtable O(1) 查找ZSetSkipList HashtableSkipList O(logN) 范围查询Hashtable O(1) 按成员查分数双重索引每种底层编码的核心优势简述SDSSimple Dynamic StringRedis 自己实现的字符串而非直接使用 C 语言的 char*。SDS 在头部记录了字符串长度因此获取长度的时间复杂度为 O(1)C 语言的 strlen 需要 O(N) 遍历。SDS 还通过空间预分配和惰性释放策略大幅减少了内存重分配的次数。ListPackRedis 7.0 用 ListPack 替代了 ziplist。ListPack 是一种紧凑的线性存储结构所有元素紧密排列在一块连续内存中内存利用率极高。当 Hash 中的字段数量较少时使用 ListPack 存储比 Hashtable 节省大量内存。QuickListQuickList 是 List 的底层实现它是一个双向链表每个节点是一个 ListPack或 ziplist。这种设计兼顾了链表在两端快速插入/删除的优势以及 ListPack 紧凑存储节省内存的优势。IntSet当 Set 中的元素全部是整数且数量较少时使用 IntSet 存储。IntSet 是一个有序的整数数组虽然查找是 O(logN)但内存占用极小。SkipList跳表ZSet 的范围查询核心。跳表通过多级索引实现 O(logN) 的查找效率同时保持有序性。相比于红黑树跳表的实现更简单范围查询更高效只需找到起点后沿底层链表遍历即可。三、Redis 为什么不用 BTreeBTree 的设计目标减少磁盘 IOBTree 是为磁盘数据库量身定制的索引结构。它的核心设计目标是将随机 IO 转化为顺序 IO尽量减少磁盘 IO 次数。BTree 的每个节点通常设置为 4KB~16KB与磁盘页大小对齐这样一次磁盘 IO 就能读取一个完整的节点。一棵 3 层的 BTree根节点常驻内存只需 2 次磁盘 IO 就能定位到数据可以索引约 2000 万条记录。但这一切优化都建立在一个前提之上——数据存储在磁盘上磁盘 IO 是主要瓶颈。Redis 是内存数据库不需要考虑磁盘 IO 优化Redis 的数据全部驻留在内存中内存的随机访问延迟约为 100 纳秒不存在减少 IO 次数的问题。在内存中随机访问和顺序访问的速度几乎一样快BTree 的核心优势在内存场景下不复存在。Hash O(1) 和 SkipList O(logN) 在内存中已经足够高效Hash 表等值查找的时间复杂度为 O(1)在内存中性能极佳SkipList范围查找的时间复杂度为 O(logN)在内存中纳秒级的操作logN 的常数极小以 1000 万条数据为例Hash 查找O(1)约 100nsSkipList 查找O(logN) O(24)约 2.4usBTree 查找O(logN)约 2.4us 额外的节点分裂/合并维护开销在内存场景下Hash 和 SkipList 的性能已经足够好而 BTree 反而因为需要维护平衡节点分裂/合并而增加了额外的复杂度和开销。对比表格Redis vs MySQL 的索引结构选择对比维度RedisMySQL (InnoDB)存储介质内存磁盘核心瓶颈网络 IO磁盘 IO等值查找Hash O(1)BTree O(logN)范围查询SkipList O(logN M)BTree O(logN M)叶子节点链表顺序读维护成本Hash 扩容 / SkipList 插入简单BTree 节点分裂/合并开销大内存效率不同数据结构各有优化BTree 节点对齐页大小内存利用率一般设计哲学内存场景下追求极致速度磁盘场景下减少 IO 次数四、Redis 的线程模型演进4.1 Redis 4.x 及之前纯单线程在 Redis 4.x 及之前的版本中Redis 采用严格的单线程模型网络 IO单线程通过 epoll 处理命令执行单线程串行执行持久化Fork 子进程处理 RDB/AOF但不影响主线程的命令执行删除操作同步删除DEL 命令大 Key 删除会阻塞主线程这个阶段的优点是简单可靠无需考虑并发安全问题缺点是在高并发场景下网络 IO 成为瓶颈单线程无法充分利用多核 CPU。4.2 Redis 6.xIO 多线程io-thread-read 和 io-thread-write 配置Redis 6 引入了 Threaded I/O通过以下配置控制io-threads 4 // IO 线程数含主线程 io-threads-do-reads yes // 是否开启读线程注意io-threads的值包含主线程本身。例如设置为 4表示 1 个主线程 3 个 IO 线程。多线程仅处理网络读写命令执行仍单线程Threaded I/O 的工作流程如下主线程通过 epoll 获取就绪的客户端连接读阶段将客户端的读任务分配给 IO 线程IO 线程负责读取和解析请求执行阶段主线程串行执行所有命令这是保证原子性的关键写阶段将写回任务分配给 IO 线程IO 线程负责将结果写回客户端关键保证命令执行仍然在主线程中串行进行不存在并发安全问题。伪代码IO 多线程的处理流程// Redis 6 IO 多线程处理流程 function processClientsWithThreadedIO(): // 读阶段多线程 // 将客户端分配给 IO 线程读取请求 for client in readableClients: assignToIOThread(client, READ_TASK) // 等待所有 IO 线程完成读取 waitForIOThreadsCompletion() // 执行阶段单线程 // 主线程串行执行所有命令 for client in readableClients: while client.hasPendingCommand(): command client.nextCommand() result executeCommand(command) // 单线程执行无锁 client.setResponse(result) // 写阶段多线程 // 将客户端分配给 IO 线程写回响应 for client in writableClients: assignToIOThread(client, WRITE_TASK) // 等待所有 IO 线程完成写入 waitForIOThreadsCompletion()4.3 Redis 7.x进一步优化lazy-free异步删除的引入Redis 4.0 就引入了 lazy-free 机制但在 Redis 7.x 中得到进一步优化和广泛应用。lazy-free 的核心思想是将耗时的删除操作放到后台线程异步执行避免阻塞主线程。相关配置lazyfree-lazy-eviction yes // 异步淘汰 lazyfree-lazy-expire yes // 异步过期 lazyfree-lazy-server-del yes // 异步删除如 rename 覆盖旧值 replica-lazy-flush yes // 从节点异步全量同步时异步清空 lazyfree-lazy-user-del yes // 用户调用 DEL 时异步删除 lazyfree-lazy-user-flush yes // 用户调用 FLUSHDB/FLUSHALL 时异步清空多线程对大 Key 删除的优化当一个包含数百万元素的 Hash 或 Set 被删除时同步删除DEL主线程遍历并释放所有元素可能导致数百毫秒的阻塞异步删除UNLINK主线程仅将 Key 从数据库中摘除O(1)实际的内存释放交给后台线程异步完成// 同步删除 vs 异步删除 function DEL(key): value db.remove(key) // 从字典中摘除 O(1) freeMemory(value) // 同步释放内存 —— 大 Key 时阻塞 return OK function UNLINK(key): value db.remove(key) // 从字典中摘除 O(1) enqueueToLazyFreeQueue(value) // 放入异步释放队列 —— 立即返回 return OK // 后台 lazy-free 线程 function lazyFreeThread(): while True: value dequeueFromLazyFreeQueue() freeMemory(value) // 在后台线程中释放内存不阻塞主线程五、Redis 的全局数据库结构16 个默认数据库db0 - db15Redis 默认提供 16 个数据库编号从 0 到 15。客户端连接后默认使用 db0可以通过SELECT index命令切换数据库。配置项databases 16可在 redis.conf 中修改注意Redis Cluster 模式下只能使用 db0不支持多数据库。每个数据库是一个 dict字典每个 Redis 数据库的核心就是一个 dict字典即哈希表。所有 Key-Value 都存储在这个字典中。Redis 的 dict 使用链地址法Separate Chaining解决哈希冲突并支持渐进式 RehashIncremental Rehashing避免一次性 Rehash 导致的长时间阻塞。过期键存储在 expires 字典中Redis 为每个数据库维护了两个字典dict存储所有 Key-Value 数据expires存储设置了过期时间的 Key 及其过期时间戳这种分离存储的设计使得过期检查非常高效只需在 expires 字典中查找 Key 即可判断是否过期无需在主字典中嵌入过期时间字段。伪代码redisDb 结构体// Redis 数据库结构简化版 struct redisDb: dict *mainDict // 主字典存储所有 Key-Value // Key - RedisObject 的映射 dict *expires // 过期字典存储设置了 TTL 的 Key // Key - 过期时间戳毫秒精度 dict *blocking_keys // 阻塞字典记录正在阻塞等待的 Key // 如 BLPOP 等待的列表 Key dict *ready_keys // 就绪字典阻塞 Key 被推送后标记为就绪 dict *watched_keys // 事务监控字典WATCH 命令监控的 Key // 用于乐观锁Optimistic Locking int id // 数据库编号0-15 long long avg_ttl // 平均 TTL用于统计 unsigned long expires_cursor // 过期扫描游标用于渐进式过期 // 使用示例 redisDb db0: mainDict { user:1 - RedisObject(Hash, {name: Tom, age: 25}), counter - RedisObject(String, 100), session - RedisObject(String, abc123) } expires { session - 1720000000000 // session 将在此时间戳过期 }