Skip to content

1. 什么是 IO 多路复用?

为了理解它,我们先从最基础的网络 IO 模型说起。

🌊 背景:一次网络请求发生了什么?

当客户端向服务器发送请求时,数据需要经过两个阶段:

  1. 等待数据准备(Waiting for data):数据从磁盘读取到内核缓冲区,或者从网卡读入。
  2. 将数据从内核拷贝到用户空间(Copying the data):CPU 将数据从内核缓冲区复制到应用程序的内存中。

传统的 阻塞 IO 模型下,服务器为每个客户端连接创建一个线程,线程会一直“卡住”(阻塞)直到上述两个步骤都完成。这种方式在连接数很少时没问题,但连接数一多(比如上万),线程切换的开销会压垮服务器。

🔑 IO 多路复用的定义

IO 多路复用(IO Multiplexing) 是一种同步 IO 模型。它的核心思想是:一个线程可以同时监听多个文件描述符(连接)的读写情况

你可以把它想象成一个餐厅服务员

  • 传统模式:一个服务员只服务一桌客人,客人没按铃要服务,服务员就一直站在那等(阻塞)。
  • 多路复用模式:一个服务员同时照看 10 桌客人。他不需要一直盯着某一桌,而是手里拿个“呼叫器”(select/poll/epoll)。哪一桌按了呼叫器,服务员就去哪一桌服务。

⚙️ 核心机制(以 Linux 的 epoll 为例)

这是目前最高效的实现方式(Redis 和 Nginx 都在用):

  1. 注册:服务器把所有客户端的连接(Socket)都注册到 epoll 实例中,告诉操作系统:“这些连接如果有数据来了,你通知我”。
  2. 等待:线程调用 epoll_wait,此时线程是阻塞的,但它是阻塞在“等待任何一个连接有消息”上,而不是阻塞在某一个特定的连接上。
  3. 通知:当某个连接有数据到达,操作系统内核会立刻“推”通知给应用程序,并返回所有就绪的连接列表。
  4. 处理:应用程序拿到就绪的连接列表,挨个处理读写。

📌 总结

IO 多路复用 = 一个线程 + 一个事件通知机制 + 处理海量连接。
它避免了为每个连接创建线程的开销,也避免了线程间频繁切换的 CPU 消耗。


2. B+树相比于 B 树有什么优势?

B 树和 B+ 树都是为了减少磁盘 I/O 次数而设计的多路平衡查找树。在数据库(如 MySQL InnoDB)中,B+ 树是绝对的主流,因为它在以下三个方面完胜 B 树:

🧱 结构差异

  • B 树每个节点(包括根节点和中间节点)既存索引键,也存数据。这意味着,只要在某个节点找到了 key,就能直接拿到数据。
  • B+ 树
    • 非叶子节点:只存索引键和指针,不存数据。
    • 叶子节点:存索引键数据,并且叶子节点之间通过双向链表连接。

🚀 三大优势详解

优势一:磁盘读写能力更强(树更矮)
由于 B+ 树的非叶子节点不存数据,只存键。这意味着同样大小的一个磁盘页(Page),B+ 树的非叶子节点可以存储更多的键,从而拥有更多的子节点指针。

  • 结果:在数据量相同的情况下,B+ 树的树高通常比 B 树更低
  • 意义:查找数据时,B+ 树需要的磁盘 I/O 次数更少(通常 3-4 次 I/O 就能查到数据)。

优势二:查询性能更稳定

  • B 树:运气好(数据在根节点)一次 I/O 就查到了,运气差(数据在叶子)需要多次 I/O。查询性能不稳定。
  • B+ 树所有数据都在叶子节点。无论查哪个数据,都必须从根节点一路走到叶子节点。这种“自底向上”的查询方式保证了每次查询的性能是一致的

优势三:范围查询和全表扫描更高效
这是 B+ 树最大的杀手锏。

  • B 树:进行范围查询(如 WHERE id > 100)时,需要不断在节点之间跳跃,甚至可能需要回溯,效率很低。
  • B+ 树:叶子节点之间有双向链表连接。一旦定位到起始数据,只需要顺着链表指针向后扫描即可,不需要回溯到父节点。这对 ORDER BY>< 操作非常友好。

3. 详细讲解 MVCC(多版本并发控制)

MVCC 是数据库解决读写冲突的终极手段。它的目标是:读操作不阻塞写操作,写操作不阻塞读操作

🎯 核心思想

保存数据的历史版本。当一行数据被修改时,旧版本的数据不会被直接覆盖或删除,而是保留下来,并通过链表连接起来。这样,正在读数据的事务可以读旧版本,正在写数据的事务修改新版本,互不干扰。

🧰 实现三剑客(InnoDB 为例)

MVCC 的实现依赖于三个核心组件:

  1. 隐藏字段:InnoDB 自动为每行数据添加几个隐藏列:
    • DB_TRX_ID:最后修改该行的事务 ID
    • DB_ROLL_PTR回滚指针,指向该行的上一个版本(在 Undo Log 中)。
    • DB_ROW_ID:隐藏主键(如果没有显式主键)。
  2. Undo Log(回滚日志):记录数据修改前的旧值。这些旧值通过 DB_ROLL_PTR 连接起来,形成一条版本链
  3. Read View(读视图):这是判断哪个版本可见的“裁判”。它记录了当前系统中活跃事务(已开启但未提交)的 ID 列表。

⛓️ 版本链示例

假设有一行数据,被事务 10、20、30 依次修改:

  • 最新数据:name='C'trx_id=30roll_ptr 指向版本 B。
  • 版本 B:name='B'trx_id=20roll_ptr 指向版本 A。
  • 版本 A:name='A'trx_id=10roll_ptr 为 null。
    读取时,数据库会从最新版本开始,顺着链表往前找,直到找到一个“对当前事务可见”的版本。

👀 可见性判断规则(Read View)

当一个事务要读取某一行数据时,数据库会拿这行数据的 trx_id 和当前的 Read View 做对比:

  1. 如果 trx_id < Read View 中最小的活跃事务 ID:说明修改该行的事务在当前事务开始前就已经提交了,可见
  2. 如果 trx_id >= Read View 中最大的事务 ID:说明这个版本是在当前事务创建 Read View 之后才生成的,不可见
  3. 如果 trx_id 在活跃事务列表中:说明修改该行的事务还没提交,不可见
  4. 如果 trx_id 不在活跃事务列表中且在范围内:说明修改该行的事务已经提交,可见

📊 不同隔离级别的应用

  • Read Committed (读已提交)每次执行 SELECT 都会生成一个新的 Read View。因此,同一个事务中两次读取,可能会读到不同的数据(因为中间有其他事务提交了修改),即存在不可重复读
  • Repeatable Read (可重复读,MySQL 默认)事务开始时(第一次执行 SELECT)生成 Read View,并一直复用。因此,即使其他事务修改并提交了数据,当前事务读到的依然是第一次读取时的快照,解决了不可重复读问题。

📌 总结