epoll / kqueue 在 Redis 中的封装实现(下) 一、概述 在上一篇文章中,我们详细分析了 Redis 对 epoll 的封装实现。本文将继续分析 Redis 对 kqueue 的封装,这是 BSD 和 macOS 系统上的高性能 I/O 多路复用机制。
kqueue 与 epoll 设计理念相似但 API 差异较大,Redis 通过统一的抽象层屏蔽了这些差异,使得上层代码可以无缝运行在不同操作系统上。
二、kqueue 基础知识 2.1 kqueue 的核心优势 kqueue 是 FreeBSD 4.1 引入的可扩展事件通知机制,后来被 macOS、NetBSD、OpenBSD 等系统采用:
特性 说明 统一接口 可监听文件描述符、信号、进程、定时器等多种事件 批量操作 支持一次注册/删除多个事件 高效 内核态维护事件状态,避免每次调用传入大量数据 可扩展 支持自定义事件源(如文件系统变化) 2.2 kqueue 的核心系统调用 1// 创建 kqueue 实例,返回文件描述符 2int kqueue(void); 3 4// 注册/修改/删除事件 5int kevent(int kq, const struct kevent *changelist, int nchanges, 6 struct kevent *eventlist, int nevents, 7 const struct timespec *timeout); 说明:
Redis Reactor 模型源码实现解析(上) Redis 以单线程扛住海量并发(单机10万QPS)而闻名,其核心秘密就藏在 Reactor 模型的实现中。本文从源码角度深入剖析 Redis 是如何实现高效的事件驱动模型的。
一、什么是 Reactor 模型 Reactor 模型是一种事件驱动的设计模式,核心思想是:
单线程事件循环:一个死循环不断检测事件 IO 多路复用:用一个线程同时监听多个连接 回调机制:事件发生时调用对应的处理器 Redis 的实现简洁优雅,核心代码不过 500 行,却支撑了每秒十万级的请求处理。
二、核心数据结构 2.1 事件循环主体:aeEventLoop 1// src/ae.h:97 2typedef struct aeEventLoop { 3 int maxfd; // 当前注册的最大 fd 4 int setsize; // 最多能跟踪多少个 fd 5 long long timeEventNextId; // 时间事件 ID 生成器 6 time_t lastTime; // 用于检测系统时钟跳变 7 aeFileEvent *events; // 注册的文件事件数组(下标即 fd) 8 aeFiredEvent *fired; // 已触发的事件数组 9 aeTimeEvent *timeEventHead; // 时间事件链表头 10 int stop; // 停止标志 11 void *apidata; // 多路复用私有数据(epoll/kqueue 等) 12 aeBeforeSleepProc *beforesleep; // 每轮循环前的回调 13 aeBeforeSleepProc *aftersleep; // 每轮循环后的回调 14} aeEventLoop; events是数组:下标就是fd,O(1)时间定位事件,这是一个精妙的"空间换时间"设计 fired存触发结果:poll返回后填充,避免重复计算 时间事件用链表:数量少,简单够用 2.
Redis Reactor 模型源码实现解析(下) 在《Redis Reactor 模型源码实现解析(上)》中 我们分析了Redis Reactor模型的核心数据结构、事件循环主流程和事件注册机制。本文将继续深入分析Redis中的实际应用、性能优化技巧、AE_BARRIER深度解析、与典型Reactor对比等内容。
七、Redis中的实际应用 7.1 连接接受:acceptTcpHandler 1// src/networking.c:726 2void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) { 3 int cport, cfd, max = MAX_ACCEPTS_PER_CALL; 4 char cip[NET_IP_STR_LEN]; 5 6 while(max--) { 7 cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport); 8 if (cfd == ANET_ERR) { 9 if (errno != EWOULDBLOCK) 10 serverLog(LL_WARNING, 11 "Accepting client connection: %s", server.neterr); 12 return; 13 } 14 serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport); 15 acceptCommonHandler(cfd,0,cip); 16 } 17} 一次循环最多接受 MAX_ACCEPTS_PER_CALL 个连接,提高吞吐。
Redis 为什么这么快?从源码角度拆解性能根因 面试官问"Redis 为什么快",很多人脱口而出"单线程没锁"。但这只是表象。真正的原因是:精心设计的数据结构、极致的内存优化、非阻塞I/O、渐进式算法……这篇文章从源码层面逐一拆解,带你彻底理解 Redis 为什么快的底层逻辑。
一、先看数据:Redis 真的快吗? 基准测试(redis-benchmark):
1$ redis-benchmark -t set,get -n 100000 -q 2SET: 110231.24 requests per second 3GET: 118121.12 requests per second 单实例10万QPS是常态,好的机器能到15万。这个数字对单线程程序来说相当惊人。
二、单线程:是优势,也是选择 2.1 为什么选单线程? Redis作者antirez解释过:
Redis是内存数据库,瓶颈在内存带宽和网络I/O,不在CPU。多线程的锁竞争、上下文切换开销,可能比收益还大。
核心逻辑:
1客户端请求 → 解析命令 → 查找 key → 执行命令 → 返回结果 2 ↓ 3 全程内存操作,微秒级 内存访问延迟约100ns,网络往返约100μs——差三个数量级。多线程加速CPU部分意义不大,反而引入锁开销。
2.2 单线程快在哪? 无锁:
1// db.c - 执行GET命令,直接读,不加锁 2robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) { 3 dictEntry *de; 4 if (expireIfNeeded(db, key) == 1) { 5 // key 过期了 6 } 7 de = dictFind(db->dict, key->ptr); 8 if (de) { 9 robj *val = dictGetVal(de); 10 return val; 11 } 12 return NULL; 13} 多线程的话,这里要加读锁。写操作加写锁时,读操作全阻塞。
Redis 事件循环模型全景解析(ae.c) Redis 是单线程的,却能处理大量并发连接,核心在于它的事件循环机制。ae.c 是 Redis 自己实现的一个轻量级事件库,支持 I/O 多路复用和定时器。这篇文章拆解它的实现原理。
一、整体架构 Redis 事件循环处理两类事件:
类型 说明 例子 文件事件(File Event) socket 可读/可写 客户端连接、命令请求、回复发送 时间事件(Time Event) 定时执行的回调 serverCron(定时清理过期 key、持久化等) 核心数据结构:
1// ae.h 2typedef struct aeEventLoop { 3 int maxfd; // 当前注册的最大 fd,用于优化遍历 4 int setsize; // events 数组大小,等于 maxclients + CONFIG_FDSET_INCR 5 long long timeEventNextId; // 时间事件 ID 生成器 6 time_t lastTime; // 上次处理时间事件的时间,用于检测时钟跳变 7 aeFileEvent *events; // 注册的文件事件数组,下标就是 fd 8 aeFiredEvent *fired; // 已触发的文件事件数组,由 aeApiPoll 填充 9 aeTimeEvent *timeEventHead; // 时间事件链表头节点 10 int stop; // 停止标志 11 void *apidata; // 多路复用 API 的私有数据(epoll 实例等) 12 aeBeforeSleepProc *beforesleep; // 每轮循环前的回调 13 aeBeforeSleepProc *aftersleep; // aeApiPoll 返回后的回调 14} aeEventLoop; 文件事件结构:
Redis 内存模型设计全解析(serverCron 在干什么?) Redis有过期策略、内存淘汰,但具体怎么实现的?serverCron每隔一段时间跑一次,到底在做什么?这篇文章从源码层面拆解Redis的内存管理机制。
一、Redis内存管理概览 Redis 的内存管理涉及三个层面:
层面 触发时机 机制 key 过期删除 访问时 + 定期扫描 惰性删除 + 定期删除 内存淘汰 内存超限 LRU/LFU/TTL/随机 后台释放 UNLINK/FLUSHDB ASYNC bio线程 二、过期策略:惰性删除 + 定期删除 Redis不是用一个线程专门扫描过期key,而是两种策略结合。
2.1 惰性删除(Lazy Expiration) 访问key时检查是否过期,过期就删除:
1// db.c 2robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) { 3 robj *val; 4 5 if (expireIfNeeded(db,key) == 1) { 6 /* Key expired. If we are in the context of a master, expireIfNeeded() 7 * returns 0 only when the key does not exist at all, so it's safe 8 * to return NULL ASAP.
Redis 单线程真的是单线程吗?源码角度全面解析 Redis 是单线程的——这句话流传太广了,以至于很多人真的以为 Redis 就一个线程在跑。但实际上,如果你 ps -ef 或者 top 看一眼正在运行的 Redis 进程,会发现线程数不止一个。
到底怎么回事?这篇文章从源码角度把这个问题彻底说清楚。
先说结论 Redis 的"单线程"指的是:命令处理的主逻辑是单线程的。
但 Redis 进程里实际上有:
主线程:处理网络请求、执行命令、事件循环 3 个后台线程:异步处理关闭文件、AOF fsync、惰性释放 子进程:RDB 持久化、AOF 重写时 fork 出来的 所以 Redis 不是严格意义上的单线程,而是"命令处理单线程"。这个设计非常聪明,后面会解释为什么。
后台线程:bio.c 打开 bio.c,文件开头的注释写得很清楚:
This file implements operations that we need to perform in the background. Currently there is a single operation, that is a background close(2) system call.
说"currently a single operation"是早期版本,现在已经扩展了。看 bio.h 的定义:
1#define BIO_CLOSE_FILE 0 // 异步关闭文件 2#define BIO_AOF_FSYNC 1 // 异步 AOF fsync 3#define BIO_LAZY_FREE 2 // 异步释放内存 4#define BIO_NUM_OPS 3 // 共 3 种后台任务 Redis 启动时会创建 3 个后台线程:
Is Redis Really Single-Threaded? A Comprehensive Analysis from Source Code Perspective Redis is single-threaded — this statement has spread so widely that many people believe Redis runs with just one thread. But if you check a running Redis process with ps -ef or top, you’ll find there’s more than one thread.
What’s going on? This article will clarify this question thoroughly from the source code perspective.
The Short Answer Redis’s “single-threaded” nature refers to: the main logic for command processing is single-threaded.
Redis 启动流程全解析(server.c 到 main 函数) 启动一个 Redis 实例看起来很简单,redis-server 一敲就完了。但你有没有想过,从按下回车到 Redis 开始接受连接,中间发生了什么?
这篇文章从 server.c 的 main 函数开始,一步步拆解 Redis 的启动流程。
先看 main 函数的全貌 main 函数在 server.c 的第 4000 行附近,核心流程可以概括为:
1初始化基础库 → 加载配置 → 初始化服务器 → 加载数据 → 进入事件循环 代码骨架是这样的:
1int main(int argc, char **argv) { 2 // 1. 基础初始化 3 initServerConfig(); 4 moduleInitModulesSystem(); 5 6 // Sentinel 模式初始化(如果是) 7 if (server.sentinel_mode) { 8 initSentinelConfig(); 9 initSentinel(); 10 } 11 12 // 2. 加载配置文件和命令行参数 13 resetServerSaveParams(); 14 loadServerConfig(configfile, options); 15 16 // 3.
Redis 如何处理高并发连接(accept + read + write 流程) 一、概述 Redis 作为单线程的高性能内存数据库,能够轻松处理数万甚至数十万的并发连接。其核心秘诀在于采用了 事件驱动 + 非阻塞 I/O 的架构设计。本文将深入分析 Redis 处理高并发连接的完整流程,包括:
accept 流程:如何接收新连接 read 流程:如何读取客户端请求 write 流程:如何发送响应数据 二、整体架构图 1┌─────────────────────────────────────────────────────────────────────────┐ 2│ Redis 主线程 │ 3├─────────────────────────────────────────────────────────────────────────┤ 4│ │ 5│ ┌───────────────┐ ┌───────────────┐ ┌─────────────┐ │ 6│ │ 监听 socket │ │ 客户端 socket │ │ 客户端 socket│ ... │ 7│ │ (listen_fd) │ │ (conn_fd) │ │ (conn_fd) │ │ 8│ └──────┬────────┘ └──────┬────────┘ └──────┬──────┘ │ 9│ │ │ │ │ 10│ ▼ ▼ ▼ │ 11│ ┌─────────────────────────────────────────────────────────┐ │ 12│ │ I/O 多路复用器 │ │ 13│ │ (epoll/kqueue) │ │ 14│ └─────────────────────────┬───────────────────────────────┘ │ 15│ │ │ 16│ ▼ │ 17│ ┌─────────────────────────────────────────────────────────┐ │ 18│ │ 事件循环 (aeMain) │ │ 19│ │ │ │ 20│ │ while(!