作者介绍:大家好,我是 CodeStats。一个在底层技术上"考古"了四年的硬核爱好者,也是 WWAIC(全周项目AI编程)范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架(从 IoC 容器到嵌入式 Tomcat,代码全开源),也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。
📑 本文目录
-
提问一:内核Socket通信全流程是怎样的?
-
提问二:Redis和MySQL认证后,如何"记住"已登录状态?
-
提问三:Redis单线程模型如何实现原子性操作?
-
附加专题:Tomcat、Redis、MySQL的BIO与NIO模型对比
前言
我一直相信,计算机科学没有魔法 。所有看似神奇的效果------无论是 AUTH password 之后 Redis 就"记住"了你,还是 MySQL 登录后执行无数条 SQL 都无需再输密码------底层都是简单的规则层层组合。
今天,我们就从内核到应用,一层层把这些"魔法"拆解干净。
全文核心脉络 :网卡收到字节流 → 内核通过五元组找到连接 → 应用通过FD找到客户端上下文 → 上下文中存着"已认证"标记 → 后续请求直接放行。这就是认证状态持久化的全部秘密。
提问一:内核Socket通信全流程是怎样的?
1.1 从网卡到进程:数据包的"快递之旅"
当你在客户端执行 redis-cli SET name "zhangsan" 时,数据包经历了以下完整旅程:
第一步:用户态 → 内核态(发送端)
应用程序调用 write() 系统调用后,数据首先被写入 Socket发送缓冲区(SendQ) 。随后,数据依次流经:
-
传输层:TCP协议对数据分段,添加TCP头部(包含源端口、目标端口、序列号等)
-
网络层:IP协议添加IP头部(包含源IP、目标IP)
-
链路层:添加以太网头部(包含MAC地址)
最终数据落入 网卡发送环形队列(Tx RingBuffer),由网卡真正发送出去。
第二步:网卡 → 内核(接收端)
数据包到达目标服务器的网卡后,触发硬件中断 ,通知内核有数据到达。内核通过五元组(协议 + 源IP + 源端口 + 目标IP + 目标端口)在连接跟踪表中查找对应的Socket。
内核将数据从网卡拷贝到内核空间的Socket接收缓冲区,然后应用程序通过 read() 系统调用将数据从内核态拷贝到用户态。
▶ 本段总结:一次网络请求的本质,是数据在"用户态缓冲区 → 内核态Socket缓冲区 → 网卡"之间的两次拷贝。内核是中间人,应用程序从不直接触碰网卡。
1.2 五元组:TCP连接的"身份证"
为什么服务器能同时处理成千上万个客户端的请求而不会搞混?答案就在五元组:
| 组成部分 | 说明 | 示例 |
|---|---|---|
| 协议 | TCP或UDP | TCP |
| 源IP | 客户端IP | 192.168.1.100 |
| 源端口 | 客户端随机分配的端口 | 52341 |
| 目标IP | 服务器IP | 192.168.1.200 |
| 目标端口 | 服务器监听的端口 | 6379 (Redis) / 3306 (MySQL) |
源端口 是操作系统在调用 connect() 时,从临时端口范围(Linux上通常是32768~60999)中随机分配的一个未被占用的数字。它的作用是让服务器知道把响应数据回传给谁------就像快递单上的"寄件人地址",没有它,服务器就算处理完请求也不知道该把结果寄回哪里。
内核正是通过这五个要素,精准地将每个数据包投递到对应的进程和连接。
▶ 本段总结:五元组是内核区分不同连接的唯一凭证。客户端IP和端口共同构成"回信地址",让服务器能精准响应对应的客户端进程。
1.3 文件描述符(FD):内核给应用的"取号单"
当TCP三次握手完成,内核会为这个连接分配一个文件描述符(File Descriptor,FD) ,它是一个非负整数。内核维护着一张 FD → 五元组 的映射表。
应用程序(如Redis、MySQL)通过I/O多路复用机制(如Linux的epoll)监听这些FD。当某个FD上有数据可读时,内核通知应用程序。应用程序根据FD找到对应的客户端上下文,处理数据。
▶ 本段总结:内核通过五元组识别连接,但交给应用程序的是一张叫"FD"的取号单。应用不关心五元组细节,只凭FD号就能找到对应的客户端档案。
提问二:Redis和MySQL认证后,如何"记住"已登录状态?
2.1 Redis的认证机制:AUTH 命令与 redisClient 结构体
Redis的认证非常简单直接。当客户端发送 AUTH <password> 命令时:
-
Redis服务器收到命令,将客户端传来的密码与配置文件
redis.conf中的requirepass进行比对 -
如果密码正确,Redis会在内存中为该连接对应的
redisClient结构体 中设置一个标志位 -
此后,该连接上的所有后续命令都会检查这个标志位,已认证则直接执行
关键点 :认证状态是绑定在TCP连接上的 。一旦认证通过,只要这个TCP连接不断开,客户端就无需再次发送 AUTH 命令。
如果用 telnet 连接Redis,每次 telnet 都会创建一个新的 TCP连接,因此需要重新执行 AUTH。而应用程序通过连接池维护的长连接,只需在连接建立时认证一次即可。
▶ 本段总结:Redis认证的本质,就是在内存中的客户端档案上盖一个"已认证"的戳。只要TCP连接还在,这个戳就一直有效。
2.2 MySQL的认证机制:挑战-响应与 THD 对象
MySQL的认证比Redis更安全,它采用了挑战-响应(Challenge-Response)机制:
-
客户端发起TCP连接后,MySQL服务器发送一个随机的挑战码(Salt)
-
客户端将密码与挑战码一起通过哈希算法(如
caching_sha2_password)计算出一个响应值 -
服务器用自己存储的密码哈希值做同样的计算,比对结果
密码从不通过网络明文传输,这是MySQL认证安全性的核心。
认证通过后,MySQL会在内存中为该连接创建一个 THD(Thread Descriptor) 对象。这个对象包含了:
-
认证状态(
authenticated = true) -
当前用户信息(
Security_context) -
当前默认数据库
-
字符集设置
-
事务状态
后续所有的SQL查询,MySQL都会通过FD找到对应的 THD 对象,检查其认证状态。只要TCP连接不断开,就不需要重新认证。
▶ 本段总结:MySQL用"挑战-响应"机制确保密码不在网络上明文传输,但认证通过后的状态管理逻辑与Redis完全一致------在内存对象中标记"已认证",后续请求凭FD找到这个对象直接放行。
2.3 对比总结
| 对比项 | Redis | MySQL |
|---|---|---|
| 密码传输 | 明文(AUTH命令直接发送密码) | 密文(挑战-响应机制,密码不出现在网络上) |
| 认证状态存储 | redisClient 结构体中的标志位 |
THD 对象中的 Security_context |
| 状态绑定 | 绑定到TCP连接(FD) | 绑定到TCP连接(FD) |
| 认证一次管终身? | ✅ 是(连接不断开就有效) | ✅ 是(连接不断开就有效) |
▶ 全文核心总结 :无论是Redis还是MySQL,认证状态都是绑定在TCP连接上的 。服务器通过FD找到对应的客户端上下文,检查其中的认证标志。这就是"登录一次,后续无需重复认证"的底层原理。一个连接一份档案,档案在,状态就在。
提问三:Redis单线程模型如何实现原子性操作?
3.1 单线程 = 天然原子性
Redis采用单线程事件循环(Event Loop) 作为其核心I/O处理模型。这意味着:
-
在任何时刻,Redis服务器只执行一个命令
-
所有命令按顺序执行,后一个命令必须等前一个命令执行完毕才能开始
-
不存在并发冲突,因为根本没有多个线程同时操作数据
这就是Redis原子性的最根本保证:单线程模型天然避免了多线程环境下的锁竞争和竞态条件。
▶ 本段总结:原子性不是靠锁"加"出来的,而是靠单线程"天然免锁"。顺序执行即原子,这是Redis设计哲学的第一性原理。
3.2 事件循环(Event Loop)的工作原理
Redis的事件循环基于Reactor模式:
text
while (eventLoop未停止) {
处理时间事件(定时任务、持久化等)
处理文件事件(网络读写)
}
文件事件处理器通过I/O多路复用(Linux的epoll、macOS的kqueue)监听多个客户端连接上的可读/可写事件。当某个FD上有数据可读时,Redis读取数据、解析命令、执行命令、返回结果------整个过程都在同一个线程中完成。
▶ 本段总结:I/O多路复用让单线程能监听海量连接,但真正执行命令时,依然是一个一个排队处理------没有并发,就没有冲突。
3.3 复合命令与Lua脚本:更复杂的原子操作
单线程只能保证单个命令的原子性。如果需要多个操作一起原子执行,Redis提供了三种方式:
① 复合指令
Redis内置了一些"一个顶多个"的复合指令,如:
-
MSET key1 value1 key2 value2:同时设置多个键值对 -
GETSET key new_value:设置新值并返回旧值 -
SETNX key value:键不存在时才设置(分布式锁的基础)
这些指令在Redis内部是一个完整的操作,不会被其他命令打断。
② Lua脚本
通过 EVAL 或 EVALSHA 执行一段Lua脚本,可以将GET、IF判断、SET等多个操作打包成一个原子单元。脚本执行期间,Redis主线程被完全占用,不会执行任何其他客户端的命令。
③ MULTI/EXEC事务
Redis的事务通过 MULTI 开启排队,EXEC 一次性执行。但需要注意:Redis事务不支持回滚------如果EXEC后某个命令执行失败,其他命令依然会执行成功。
▶ 本段总结:单命令原子是"天生"的,多命令原子则需要"打包"------复合指令是官方打包,Lua脚本是自定义打包,事务是排队打包。打包方式不同,但本质都是让一组操作在单线程中连续执行、不被插队。
3.4 Pipeline ≠ 原子性
需要特别强调的是:Pipeline(管道)不具备原子性。Pipeline只是客户端将多个命令一次性发送给服务器,减少网络往返次数(RTT),但服务器仍然是逐个执行这些命令,中间可能被其他客户端的命令插队。
▶ 本段总结:Pipeline解决的是网络延迟问题,不是原子性问题。它让客户端少跑几趟路,但服务器该怎么排队还是怎么排队。
附加专题:Tomcat、Redis、MySQL的BIO与NIO模型对比
4.1 BIO(阻塞I/O):一连接一线程
BIO(Blocking I/O) 是最传统的I/O模型:线程发起I/O请求后,一直阻塞等待,直到数据就绪或操作完成。
Tomcat(传统BIO模式) :每个客户端连接对应一个线程。线程在读取请求或发送响应时被阻塞。优点是编程简单,缺点是线程数 = 连接数,高并发下线程资源耗尽。
MySQL :MySQL的连接处理更倾向于BIO模型。因为MySQL的性能瓶颈通常在磁盘I/O(查询、索引、事务日志),而非网络I/O。即使使用NIO,查询请求仍然要等待磁盘操作完成,对性能提升意义不大。
▶ 本段总结:BIO的代价是线程资源,优势是编程简单。MySQL选择BIO是因为它的真正瓶颈在磁盘,不在网络。
4.2 NIO(非阻塞I/O):一线程N连接
NIO(Non-blocking I/O) :发起I/O请求后立即返回,如果数据未就绪,返回"数据未就绪"状态,线程可以继续做其他事。
Tomcat(NIO模式) :使用Selector监听多个连接的事件,一个线程可以管理成千上万个连接。适合高并发Web场景。
Redis :Redis的网络层核心就是I/O多路复用 (本质是NIO的一种高效实现),通过epoll/kqueue让单线程处理海量连接。但Redis的处理逻辑(命令执行)是单线程的,这与Tomcat NIO的多线程处理业务逻辑有本质区别。
▶ 本段总结:NIO让一个线程管N个连接,大幅降低线程开销。Redis和Tomcat NIO都用了这个思路,但区别在于------Redis拿到数据后单线程处理,Tomcat拿到数据后丢给线程池处理。
4.3 对比总结
| 特性 | Tomcat (BIO) | Tomcat (NIO) | MySQL | Redis |
|---|---|---|---|---|
| I/O模型 | BIO | NIO | BIO为主 | NIO(多路复用) |
| 线程模型 | 1连接1线程 | 1线程N连接 | 1连接1线程 | 单线程事件循环 |
| 瓶颈 | 线程数受限 | 业务逻辑处理 | 磁盘I/O | CPU/内存 |
| 适用场景 | 低并发 | 高并发Web | 事务型OLTP | 高性能缓存 |
▶ 本段总结:选BIO还是NIO,本质是在"编程复杂度"和"资源利用率"之间做权衡。Redis用NIO+单线程实现了极致性能,MySQL用BIO是因为磁盘I/O才是它真正的瓶颈。
写在最后:层层递进的底层真相
现在,让我们把整篇文章的思考链条串起来:
第一层(物理世界) :网卡收到电信号,转化成字节流,触发硬件中断,内核开始接管。
第二层(内核世界) :内核提取五元组,在连接表中找到对应的Socket,把数据放入接收缓冲区,然后通过FD通知应用程序"有数据来了"。
第三层(应用世界) :Redis/MySQL收到内核的通知,根据FD找到内存中的客户端档案(redisClient 或 THD),检查档案里的认证标志------有戳就放行,没戳就拒绝。
第四层(逻辑世界) :认证通过后,命令进入执行阶段。Redis用单线程事件循环保证命令顺序执行、天然原子;MySQL用多线程配合行锁、MVCC来保证事务的ACID。
从网卡的电压高低,到认证标志的0和1------中间隔了五元组、Socket缓冲区、文件描述符、事件循环、客户端结构体五层抽象。每一层都只做一件简单的事,但层层叠加,就构成了我们今天使用的数据库系统。
计算机科学没有魔法。只有一层层叠起来的确定性。
📢 如果这篇文章帮到了你,欢迎:
-
👍 点赞 让更多人看到
-
⭐ 收藏 方便随时回看
-
👀 关注 CodeStats,一起在底层技术上"考古"
我们下篇见! 🚀