【Redis从入门到精通】第37篇:Redis服务器启动全流程——从redis-server到ready to accept
上一篇【第36篇】Redis客户端属性大揭秘——一个连接背后有多少状态
下一篇【第38篇】serverCron——Redis的"心跳"定时任务干了哪些活
你肯定无数次在终端敲过redis-server,看着日志翻滚几行后定格在那一句经典的Ready to accept connections。有没有想过,在这短短的几个字符背后,Redis到底经历了什么?今天我们就来解剖Redis服务器启动的全过程,看看从二进制启动到可以接收客户端请求,中间到底跨过了多少山河。
启动六阶段概览
Redis的启动过程可以清晰地划分为六个阶段,每个阶段各司其职:
redis-server 启动时间线 ┌────────────────────────────────────────────────────────────────┐ │ Phase 1 ──→ Phase 2 ──→ Phase 3 ──→ Phase 4 ──→ Phase 5 ──→ Phase 6 │ │ │ │ 初始配置 载入配置 初始化结构 恢复数据 启动循环 接受连接 │ │ (默认值) (redis.conf)(数据结构) (RDB/AOF) (aeMain) (ready!) │ │ │ │ ~0ms ~5ms ~15ms ~50ms+ ~60ms+ ~65ms+ │ └────────────────────────────────────────────────────────────────┘下面我们逐一拆解。
Phase 1:初始化服务器配置(initServerConfig)
这是Redis启动的第一站。函数initServerConfig()的作用非常纯粹——把所有服务器配置项设置为默认值。在这个阶段,Redis还没读任何配置文件,它只是构建了一个"出厂设置"。
voidinitServerConfig(void){// 网络相关默认值server.port=CONFIG_DEFAULT_SERVER_PORT;// 6379server.bindaddr_count=0;// 默认绑定所有地址server.tcp_backlog=CONFIG_DEFAULT_TCP_BACKLOG;// 511// 性能相关server.hz=CONFIG_DEFAULT_HZ;// 10 (每秒10次serverCron)server.maxclients=CONFIG_DEFAULT_MAX_CLIENTS;// 10000// 内存相关server.maxmemory=0;// 0表示不限制server.maxmemory_policy=MAXMEMORY_NO_EVICTION;// 默认不淘汰// 持久化server.saveparams=NULL;// 默认不自动RDBserver.aof_state=AOF_OFF;// 默认关闭AOF// 数据库server.dbnum=CONFIG_DEFAULT_DBNUM;// 16个数据库// 日志server.verbosity=CONFIG_DEFAULT_VERBOSITY;// NOTICE级别server.loglevel=LL_NOTICE;// 慢查询server.slowlog_log_slower_than=CONFIG_DEFAULT_SLOWLOG_LOG_SLOWER_THAN;// 10000微秒server.slowlog_max_len=CONFIG_DEFAULT_SLOWLOG_MAX_LEN;// 128条// 创建命令表populateCommandTable();// ... 还有大量其他默认值}关键默认值速览表:
| 配置项 | 默认值 | 影响范围 |
|---|---|---|
port | 6379 | 监听端口 |
hz | 10 | serverCron执行频率 |
maxclients | 10000 | 最大客户端连接数 |
maxmemory | 0(不限制) | 内存上限 |
dbnum | 16 | 数据库数量 |
slowlog_log_slower_than | 10000微秒 | 慢查询阈值 |
slowlog_max_len | 128 | 慢查询记录条数 |
tcp_backlog | 511 | TCP半连接队列长度 |
aof_state | AOF_OFF | AOF默认关闭 |
loglevel | NOTICE | 日志级别 |
populateCommandTable()在这个阶段被调用,它会构建Redis的命令表。这个表是一个巨大的字典(dict),key是命令名字符串,value是redisCommand结构体。
Phase 2:载入配置文件(loadServerConfig)
有了默认值,接下来就是用用户配置覆盖它们。这个阶段有两个来源:
配置文件解析来源 ┌──────────────────┐ │ redis.conf 文件 │──┐ └──────────────────┘ │ ├──→ loadServerConfig() ──→ 更新 server 全局变量 ┌──────────────────┐ │ │ 命令行参数 │──┘ │ --port 6380 │ │ --maxmemory 4gb │ └──────────────────┘配置文件解析过程:
voidloadServerConfig(char*filename,char*options){// 1. 读取配置文件到sds字符串sds config=sdsempty();charbuf[CONFIG_MAX_LINE+1];// 2. 逐行解析// 跳过空行和#注释// 识别 directive argument 格式// include 指令会递归加载子配置文件// 3. 处理命令行参数(覆盖配置文件中的值)// --port 6380 会覆盖 port 6379// 4. 设置配置文件修改标志server.configfile=filename;}Redis的配置指令与Server变量之间有映射表configs[],这是一个巨大的结构体数组,定义了每个指令名、对应变量、类型、允许修改方式等。比如:
// 配置映射表的一个条目(简化示意){"port",&server.port,REDIS_CONFIG_INT,0,65535,MODIFIABLE_CONFIG,NULL,updatePort},{"maxmemory",&server.maxmemory,REDIS_CONFIG_MEMORY,0,LLONG_MAX,MODIFIABLE_CONFIG,NULL,NULL},{"loglevel",&server.loglevel,REDIS_CONFIG_ENUM,LL_DEBUG,LL_WARNING,MODIFIABLE_CONFIG,loglevel_enum,NULL},Phase 3:初始化服务器数据结构(initServer)
这是最"重"的阶段。initServer()函数做了大量实质性工作:
initServer() 工作清单 ┌──────────────────────────────────────┐ │ 1. 设置信号处理器 │ │ signal(SIGTERM, sigtermHandler) │ │ signal(SIGINT, ...) │ ├──────────────────────────────────────┤ │ 2. 创建共享对象 │ │ shared.ok = createObject("+OK") │ │ shared.err = createObject("-ERR") │ │ 共享整数对象(0~9999) │ ├──────────────────────────────────────┤ │ 3. 创建事件循环 (aeCreateEventLoop) │ │ server.el = aeCreateEventLoop() │ │ 基于epoll/select/kqueue │ ├──────────────────────────────────────┤ │ 4. 初始化 redisDb 数组 │ │ for (i=0; i<server.dbnum; i++) │ │ server.db[i] = 空字典+空过期字典 │ ├──────────────────────────────────────┤ │ 5. 绑定监听端口 │ │ listenToPort(server.port) │ │ 创建TCP监听socket │ ├──────────────────────────────────────┤ │ 6. 注册时间事件 serverCron │ │ aeCreateTimeEvent(server.el, │ │ 1, serverCron, NULL, NULL) │ ├──────────────────────────────────────┤ │ 7. 注册读事件 acceptTcpHandler │ │ aeCreateFileEvent(server.el, │ │ server.ipfd, AE_READABLE, │ │ acceptTcpHandler, NULL) │ ├──────────────────────────────────────┤ │ 8. 打开AOF/RDB文件句柄 │ ├──────────────────────────────────────┤ │ 9. 初始化慢查询日志 │ ├──────────────────────────────────────┤ │10. 初始化Lua脚本环境 │ │ scriptingInit() │ └──────────────────────────────────────┘几个值得展开的细节:
共享对象:Redis预创建了大量的共享对象。比如+OK\r\n回复、-ERR回复、整数0到9999的字符串对象。任何命令返回这些值时,都直接返回共享对象的指针,避免重复创建。这个优化在大量小回复的场景下效果显著。
事件循环:aeCreateEventLoop()创建的是Redis自己封装的事件循环框架。在Linux上底层使用epoll,macOS上使用kqueue。这个事件循环是整个Redis运行的核心引擎,所有网络IO和定时任务都由它驱动。
初始化数据库数组:server.dbnum默认16,所以Redis默认创建16个数据库,每个数据库包含两个字典——一个存key-value,一个存过期时间。
Phase 4:还原数据库状态(loadDataFromDisk)
如果配置了持久化,这个阶段会将之前保存的数据加载到内存:
loadDataFromDisk() ┌──────────────┐ │ AOF开启? │ └──┬────────┬──┘ YES NO │ │ ▼ ▼ ┌──────────┐ ┌──────────┐ │ 载入AOF │ │ 载入RDB │ └────┬─────┘ └────┬─────┘ │ │ └──────┬───────┘ ▼ 数据恢复到内存中这里有一个优先级:AOF > RDB。如果同时开启了AOF和RDB,Redis优先使用AOF,因为AOF的数据更完整。
载入过程通过伪客户端完成(还记得上一篇讲的fd=-1的客户端吗?):
// 伪客户端执行AOF命令(简化)voidloadAppendOnlyFile(char*filename){fakeClient=createAOFClient();// 创建伪客户端// 逐条读取AOF文件中的命令while(readCommand(fakeClient)){fakeClient->argc=argc;fakeClient->argv=argv;cmd=lookupCommand(fakeClient->argv[0]);fakeClient->cmd=cmd;// 执行命令(跳过权限检查等)cmd->proc(fakeClient);}}这个阶段的耗时取决于数据量。一个10GB的RDB文件可能需要几十秒甚至几分钟来载入。
Phase 5:执行事件循环(aeMain)
aeMain()是整个Redis运行的核心——它进入一个无限循环,不停地处理文件事件和时间事件:
voidaeMain(aeEventLoop*eventLoop){eventLoop->stop=0;while(!eventLoop->stop){// 处理时间事件之前的前置工作aeProcessEvents(eventLoop,AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);}}这就是为什么Redis启动后一直"跑着"的原因。这个循环中没有任何花哨的东西——就是持续不断地调用aeProcessEvents,把"就绪的网络事件"和"到期的时间事件"派发给对应的处理器。
但注意:在aeMain启动之前,Redis会先输出一句重要的日志。
Phase 6:Ready to accept connections
在进入aeMain()的前一刻,Redis打出那句经典的日志:
// server.c main函数最后几行redisLog(LL_NOTICE,"Ready to accept connections");aeMain(server.el);这行日志的意义是:
- 所有初始化已完成
- 数据已恢复
- 端口已经开始监听
- 事件循环即将启动
- 现在、此刻、马上,可以接收客户端请求了
"Ready to accept connections" 的含义 初始化 运行中 ┌──────────┐ ┌──────────────┐ │ Phase 1~4 │ ═══════════════════→ │ Phase 5 aeMain│ └──────────┘ ^^^^^^^^ └──────────────┘ 这行日志在这里输出 "Ready to accept connections"命令请求处理的完整生命周期
服务器启动后,客户端命令如何在Redis内部走完全程?这是理解Redis工作方式的核心。
命令处理时序图(一次 SET key value 的生命周期) 客户端 Redis服务器 │ │ │── SET key value ──────→ │ 1. TCP数据到达 │ │ │ ┌────▼────────────┐ │ │ 2. epoll触发 │ │ │ AE_READABLE │ │ └────┬────────────┘ │ │ │ ┌────▼────────────┐ │ │ 3. readQueryFrom │ │ │ Client() │ │ │ read()到querybuf │ │ └────┬────────────┘ │ │ │ ┌────▼────────────┐ │ │ 4. processInput │ │ │ Buffer() │ │ │ RESP协议解析 │ │ │ *3\r\n$3\r\nSET │ │ │ $3\r\nkey\r\n │ │ │ $5\r\nvalue\r\n │ │ └────┬────────────┘ │ │ │ ┌────▼────────────┐ │ │ 5. 组装argc/argv │ │ │ argc=3 │ │ │ argv=[SET,key, │ │ │ value] │ │ └────┬────────────┘ │ │ │ ┌────▼────────────┐ │ │ 6. lookupCommand │ │ │ 在命令表中查找"SET"│ │ └────┬────────────┘ │ │ 找到! │ ┌────▼────────────┐ │ │ 7. 参数校验(arity) │ │ │ SET的arity=3 │ │ │ argc=3 ✓ │ │ └────┬────────────┘ │ │ │ ┌────▼────────────┐ │ │ 8. 慢查询打点开始 │ │ │ start = ustime() │ │ └────┬────────────┘ │ │ │ ┌────▼────────────┐ │ │ 9. 鉴权检查 │ │ │ authenticated? │ │ │ flags检查 │ │ └────┬────────────┘ │ │ │ ┌────▼────────────┐ │ │10. call(c, │ │ │ CMD_CALL_FULL │ │ │ 执行 setCommand()│ │ └────┬────────────┘ │ │ │ ┌────▼────────────┐ │ │11. 慢查询打点结束 │ │ │ duration=150us │ │ │ < 10000us ✓ │ │ └────┬────────────┘ │ │ │ ┌────▼────────────┐ │ │12. addReply() │ │ │ 回复"+OK\r\n" │ │ │ 放入输出缓冲区 │ │ └────┬────────────┘ │ │ │ │ 13. (下次可写事件触发时) │ ┌────▼────────────┐ │ │14. sendReplyTo │ │ │ Client() │ │ │ write()发送响应 │ │ └────┬────────────┘ │ │ │ ←── +OK ────────────────┘ 15. 响应送回客户端整个过程可以浓缩为五个关键步骤:接收 → 解析 → 查找 → 执行 → 回复。
慢查询日志的打点时机
注意步骤8和11,慢查询日志在命令执行前后各打一个时间戳:
voidcall(client*c,intflags){longlongdirty;ustime_tstart,duration;// 记录开始时间start=server.ustime;// 执行命令c->cmd->proc(c);duration=ustime()-start;// 执行完毕,检查是否需要记录慢查询if(duration>=server.slowlog_log_slower_than){// 命令执行时间超过阈值(默认10000微秒),记录slowlogPushEntryIfNeeded(c,c->argv,c->argc,duration);}}踩坑提示:慢查询的时间统计不包括网络IO和排队等待的时间。如果一条命令本身只执行了50微秒,但客户端等了100毫秒才收到回复,这条命令不会被记入慢查询日志。排查延迟问题时要区分"服务端慢"还是"网络/客户端慢"。
Redis命令表:所有命令的注册中心
redisCommandTable是Redis命令体系的根基。它是一个包含所有命令定义的结构体数组:
structredisCommandredisCommandTable[]={{"get",getCommand,2,"read-only fast @string",0,NULL,1,1,1,0,0,0},{"set",setCommand,-3,"write use-memory @string",0,NULL,1,1,1,0,0,0},{"keys",keysCommand,2,"read-only sort-for-scripts @keyspace @dangerous",0,NULL,0,0,0,0,0,0},{"slowlog",slowlogCommand,-2,"admin random ok-loading ok-stale",0,NULL,0,0,0,0,0,0},// ... 200+ 条命令定义};每个条目包含的关键字段:
| 字段 | 含义 | 示例(SET命令) |
|---|---|---|
name | 命令名 | “set” |
proc | 命令处理函数指针 | setCommand |
arity | 参数数量 | -3(最少3个参数) |
sflags | 字符串标志 | “write use-memory @string” |
flags | 整型标志(由sflags转换) | CMD_WRITE | CMD_DENYOOM |
getkeys_proc | 提取key的函数 | NULL(使用默认方式) |
firstkey | 第一个key位置 | 1 |
lastkey | 最后一个key位置 | 1 |
keystep | key间隔 | 1 |
arity:参数个数规则
arity字段很精妙:
- 正数:固定参数个数。如
GET的arity=2,表示必须是GET key(2个参数,命令名算一个)。 - 负数:最少参数个数(绝对值)。如
SET的arity=-3,表示至少3个参数(SET key value),但可以更多(SET key value EX 10 NX)。 - 0:不限制参数个数。
flags:命令标志位大全
命令的flags描述了权限和行为特征:
命令标志位含义 ┌────────────────────────────────────────────────┐ │ 读写属性: │ │ r(CMD_READONLY) — 只读命令 │ │ w(CMD_WRITE) — 写命令,会修改数据 │ │ m(CMD_DENYOOM) — 内存不足时拒绝执行 │ │ │ │ 管理属性: │ │ a(CMD_ADMIN) — 管理员命令(需特殊权限) │ │ A(CMD_NO_AUTH) — 不需要认证就能执行 │ │ │ │ 特殊场景: │ │ F(CMD_FAST) — O(1)或O(logN)命令 │ │ S(CMD_STALE) — 从库过期数据也能执行 │ │ k(CMD_ASKING) — 集群ASK转向 │ │ t(CMD_TOUCHES_ARBITRARY_KEYS) — 触碰任意key │ │ │ │ 状态标志: │ │ R(CMD_RANDOM) — 随机结果(主从不一致) │ │ M(CMD_SORT_FOR_SCRIPT) — 脚本中排序输出 │ │ l(CMD_LOADING) — 载入数据时可用 │ │ s(CMD_STALE) — 从库可用 │ │ │ │ Redis 7.0+ ACL分类: │ │ @keyspace @read @write @set @sortedset ... │ └────────────────────────────────────────────────┘总结
Redis服务器从启动到就绪,经历了六个精心设计的阶段。每一阶段都有明确的职责边界:
| 阶段 | 函数 | 核心任务 | 大致耗时 |
|---|---|---|---|
| Phase 1 | initServerConfig | 设置所有默认配置值 | < 1ms |
| Phase 2 | loadServerConfig | 解析redis.conf和命令行参数 | 1~5ms |
| Phase 3 | initServer | 分配数据结构、创建事件循环、绑定端口 | 10~50ms |
| Phase 4 | loadDataFromDisk | 通过伪客户端执行AOF或载入RDB | 毫秒到分钟级 |
| Phase 5 | aeMain | 事件循环,持续运行 | 无限 |
| Phase 6 | — | “Ready to accept connections” | — |
命令处理的核心流程可以记住五个字:接→拆→查→跑→回。分别对应读事件触发、RESP协议解析、命令表查找、命令函数执行、写入输出缓冲区。
慢查询日志在命令执行前后打点,帮你定位"Redis为什么慢了"。
命令表中的arity和flags定义了每个命令的"身份证"——它能接几个参数,在什么状态下能执行,哪些场景下被禁用。
下一篇,我们将深入事件循环的核心——serverCron,看看这个每100毫秒执行一次的"心跳任务"到底干了哪些活,以及为什么不合理的Key过期会导致整个Redis"僵住"几秒。
上一篇【第36篇】Redis客户端属性大揭秘——一个连接背后有多少状态
下一篇【第38篇】serverCron——Redis的"心跳"定时任务干了哪些活
