MySQL vs MongoDB 深度对比(底层存储数据结构与并发控制篇)

MySQL vs MongoDB 深度对比(底层存储与并发控制篇)

目录

  1. 底层存储结构
  2. 数据模型
  3. 查询语言
  4. 事务支持
  5. 持久化机制对比
  6. 扩展性

1. 底层存储结构

1.1 MySQL - InnoDB 存储引擎

InnoDB 是 MySQL 的默认存储引擎,其核心数据结构是 B+树

B+树索引结构
复制代码
                        ┌─────────────┐
                        │   根节点     │
                        │  (非叶子)    │
                        └──────┬──────┘
                               │
           ┌───────────────────┼───────────────────┐
           │                   │                   │
    ┌──────▼──────┐     ┌──────▼──────┐     ┌──────▼──────┐
    │  中间节点    │     │  中间节点    │     │  中间节点    │
    │  (非叶子)    │     │  (非叶子)    │     │  (非叶子)    │
    └──────┬──────┘     └──────┬──────┘     └──────┬──────┘
           │                   │                   │
    ┌──────┴──────┐     ┌──────┴──────┐     ┌──────┴──────┐
    │             │     │             │     │             │
┌───▼───┐   ┌───▼───┐ ┌───▼───┐   ┌───▼───┐ ┌───▼───┐   ┌───▼───┐
│叶子节点│   │叶子节点│ │叶子节点│   │叶子节点│ │叶子节点│   │叶子节点│
│(数据)  │   │(数据)  │ │(数据)  │   │(数据)  │ │(数据)  │   │(数据)  │
└───────┘   └───────┘ └───────┘   └───────┘ └───────┘   └───────┘
    ◄─────────────────────────────────────────────────►
                    叶子节点双向链表连接

B+树特点:

  • 非叶子节点只存储键值和指针,不存储数据
  • 所有数据都存储在叶子节点
  • 叶子节点通过双向链表连接,支持范围查询
  • 树高度通常为 2-4 层,查询效率稳定
InnoDB 页结构

InnoDB 以 页 (Page) 为基本存储单位,默认大小为 16KB

复制代码
┌────────────────────────────────────────────────────┐
│                    InnoDB 页结构                   │
├────────────────────────────────────────────────────┤
│  File Header (38字节) - 页通用信息                  │
├────────────────────────────────────────────────────┤
│  Page Header (56字节) - 页状态信息                  │
├────────────────────────────────────────────────────┤
│  Infimum + Supremum (26字节) - 最小最大记录         │
├────────────────────────────────────────────────────┤
│                                                    │
│              User Records (用户数据)               │
│                 (行格式存储)                        │
│                                                    │
├────────────────────────────────────────────────────┤
│              Free Space (空闲空间)                 │
├────────────────────────────────────────────────────┤
│                                                    │
│           Page Directory (页目录)                  │
│              (槽点,加速查找)                       │
├────────────────────────────────────────────────────┤
│  File Trailer (8字节) - 校验和                     │
└────────────────────────────────────────────────────┘
聚簇索引 vs 二级索引
复制代码
聚簇索引 (主键索引):
┌─────────────────────────────────────┐
│  叶子节点存储完整行数据              │
│  数据按主键顺序存储                  │
│  一张表只有一个聚簇索引              │
└─────────────────────────────────────┘

二级索引 (辅助索引):
┌─────────────────────────────────────┐
│  叶子节点存储主键值                  │
│  查询需要回表 (通过主键查聚簇索引)   │
│  可以有多个二级索引                  │
└─────────────────────────────────────┘

回表示意图:

复制代码
二级索引                          聚簇索引
┌─────────┐                      ┌─────────┐
│ name='张三' │ ──► 主键id=1 ──► │ id=1    │
│ id=1     │                      │ 完整行数据│
└─────────┘                      └─────────┘
InnoDB 内存结构
复制代码
┌─────────────────────────────────────────────────────────┐
│                    InnoDB 内存架构                       │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌───────────────────────────────────────────────────┐ │
│  │              Buffer Pool (缓冲池)                   │ │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐              │ │
│  │  │数据页    │ │索引页    │ │自适应哈希│              │ │
│  │  └─────────┘ └─────────┘ └─────────┘              │ │
│  └───────────────────────────────────────────────────┘ │
│                                                         │
│  ┌─────────────────┐  ┌─────────────────────────────┐  │
│  │  Change Buffer  │  │      Log Buffer             │  │
│  │ (变更缓冲区)     │  │    (日志缓冲区)              │  │
│  └─────────────────┘  └─────────────────────────────┘  │
│                                                         │
└─────────────────────────────────────────────────────────┘

关键组件:

  • Buffer Pool: 缓存数据和索引页,减少磁盘I/O
  • Change Buffer: 缓存二级索引的修改操作
  • Log Buffer: 缓存重做日志,保证持久性

1.2 MongoDB - WiredTiger 存储引擎

MongoDB 默认使用 WiredTiger 存储引擎,使用 B树 结构。

索引结构详解

重要澄清 : MongoDB 索引节点存储的是 键值 + 指针,不是完整文档数据!

复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│              MongoDB 索引B树 vs 数据B树 (两个独立的树)                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   索引B树 (如 name 索引)                数据B树 (存储完整文档)           │
│   ┌─────────────────────┐              ┌─────────────────────┐         │
│   │     非叶子节点       │              │     非叶子节点       │         │
│   │  索引键值 + 子指针   │              │  RecordId + 子指针   │         │
│   └──────────┬──────────┘              └──────────┬──────────┘         │
│              │                                    │                    │
│   ┌──────────▼──────────┐              ┌──────────▼──────────┐         │
│   │     叶子节点         │              │     叶子节点         │         │
│   │  索引键值 + RecordId │──回表查询──► │  完整BSON文档        │         │
│   │  (指针,非完整数据)   │              │  {                   │         │
│   │                     │              │    "_id": ...,      │         │
│   │  无链表连接          │              │    "name": "张三",  │         │
│   └─────────────────────┘              │    "age": 25,       │         │
│                                        │    ...               │         │
│                                        │  }                   │         │
│                                        └─────────────────────┘         │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
关键问题:那和B+树有什么区别?

您说得对!如果索引节点都只存键值+指针,那MongoDB索引和B+树的非叶子节点是一样的!

复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                      索引结构对比                                       │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   MySQL B+树 二级索引                MongoDB B树 索引                   │
│   ┌─────────────────────┐          ┌─────────────────────┐             │
│   │     非叶子节点       │          │     非叶子节点       │             │
│   │  索引键值 + 子指针   │  ◄─────► │  索引键值 + 子指针   │  ◄── 一样! │
│   └──────────┬──────────┘          └──────────┬──────────┘             │
│              │                                 │                        │
│   ┌──────────▼──────────┐          ┌──────────▼──────────┐             │
│   │     叶子节点         │          │     叶子节点         │             │
│   │  索引键值 + 主键值   │          │  索引键值 + RecordId │             │
│   │  (存主键,需回表)    │          │  (存指针,需回表)    │ ◄── 类似! │
│   │                     │          │                     │             │
│   │  有双向链表连接 ✓    │          │  无链表连接 ✗       │ ◄── 区别!  │
│   └─────────────────────┘          └─────────────────────┘             │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

真正的区别:

对比项 MySQL B+树二级索引 MongoDB B树索引
非叶子节点 键值 + 子指针 键值 + 子指针
叶子节点存储 键值 + 主键值 键值 + RecordId指针
叶子节点链表 有双向链表 无链表
回表方式 通过主键查聚簇索引 通过RecordId直接定位
范围查询 遍历链表,高效 中序遍历树,较慢

总结 : MongoDB索引本质上是 没有叶子链表的B+树变体,官方称为B树。

为什么叫B树而不是B+树?
  1. 没有叶子节点链表 - 这是B+树的典型特征
  2. 历史命名 - MongoDB早期文档就称为B树

1.3 MongoDB 数据B树详解

您问到了核心问题!MongoDB有两套B树

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                    MongoDB 存储架构                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌─────────────────────────────────┐    ┌─────────────────────────────────┐
│   │     索引B树 (Index B-Tree)      │    │     数据B树 (Data B-Tree)       │
│   │     索引文件                    │    │     集合数据文件                 │
│   ├─────────────────────────────────┤    ├─────────────────────────────────┤
│   │                                 │    │                                 │
│   │  按索引字段组织                  │    │  按RecordId组织                  │
│   │  (如name字段)                   │    │                                 │
│   │                                 │    │                                 │
│   │  非叶子: 键值+子指针            │    │  非叶子: RecordId范围+子指针     │
│   │  叶子: 键值+RecordId指针        │    │  叶子: 完整BSON文档              │
│   │                                 │    │                                 │
│   │  不存完整文档!                 │    │  存完整文档!                    │
│   │                                 │    │                                 │
│   └─────────────────────────────────┘    └─────────────────────────────────┘
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
数据B树存储的是什么?

关键问题:数据B树的非叶子节点是否存储完整文档?

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│              WiredTiger 数据B树实际结构                                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   WiredTiger的数据存储更接近B+树:                                          │
│                                                                             │
│   非叶子节点: 存储RecordId范围 + 子页面指针                                  │
│              (不存完整文档!)                                                │
│                                                                             │
│   叶子节点: 存储完整BSON文档                                                │
│              (按RecordId排序)                                              │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

                        ┌────────────────────────┐
                        │   根节点 (内部页)       │
                        │  RecordId范围: 1-1000  │
                        │  子指针...             │
                        └───────────┬────────────┘
                                    │
              ┌─────────────────────┼─────────────────────┐
              │                     │                     │
      ┌───────▼───────┐     ┌───────▼───────┐     ┌───────▼───────┐
      │ 内部页         │     │ 内部页         │     │ 内部页         │
      │ 范围: 1-300   │     │ 范围: 301-600 │     │ 范围: 601-1000│
      │ 不存完整文档! │     │ 不存完整文档! │     │ 不存完整文档! │
      └───────┬───────┘     └───────┬───────┘     └───────┬───────┘
              │                     │                     │
      ┌───────▼───────┐     ┌───────▼───────┘     ┌───────▼───────┐
      │ 叶子页         │     │ 叶子页         │     │ 叶子页         │
      │ RecordId=1    │     │ RecordId=301  │     │ RecordId=601  │
      │ {完整BSON文档} │     │ {完整BSON文档} │     │ {完整BSON文档} │
      │ RecordId=2    │     │ RecordId=302  │     │ RecordId=602  │
      │ {...}         │     │ {...}         │     │ {...}         │
      └───────────────┘     └───────────────┘     └───────────────┘

结论 :WiredTiger的数据存储实际上也是 叶子节点存完整数据,和MySQL聚簇索引类似!


1.4 真正的区别:回表方式不同

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                        MySQL 回表方式                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   二级索引                        聚簇索引                                  │
│   ┌─────────────┐                ┌─────────────────────┐                   │
│   │ name='张三'  │                │ id=1 (主键)         │                   │
│   │ id=1        │ ────────────►  │ name='张三'         │                   │
│   │             │  通过主键值    │ age=25              │                   │
│   └─────────────┘  查聚簇索引    │ [完整行数据]        │                   │
│                                   └─────────────────────┘                   │
│                                                                             │
│   特点: 需要在聚簇索引中「从根到叶」遍历查找                                │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│                      MongoDB 回表方式                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   索引B树                         数据B树                                  │
│   ┌─────────────┐                ┌─────────────────────┐                   │
│   │ name='张三'  │                │ RecordId=1          │                   │
│   │ RecordId=1  │ ────────────►  │ {完整BSON文档}      │                   │
│   │             │  通过RecordId  │                     │                   │
│   └─────────────┘  直接定位      └─────────────────────┘                   │
│                                                                             │
│   特点: RecordId是物理地址,可以直接定位到数据页                            │
│         但仍需在数据B树中遍历查找该RecordId                                 │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
回表效率对比
维度 MySQL MongoDB
二级索引存储 主键值 RecordId
回表查找方式 主键值在聚簇索引中查找 RecordId在数据B+树中查找
是否需要遍历树 是,从根到叶 是,从根到叶
区别 主键值需要比较查找 RecordId也需要遍历查找

重要修正:两者都需要遍历树!

根据MongoDB官方技术文档:

"RecordId can be considered a physical address by the query layer, but it is not directly mapped to an address in the filesystem because files are B+Tree structures."

翻译:RecordId可以被查询层视为物理地址,但它并不直接映射到文件系统中的地址,因为文件是B+Tree结构。

结论:MongoDB的RecordId和MySQL的主键值一样,都需要在B+树中遍历查找,不存在"直接定位"!


1.5 那B树到底有什么优点?

您问得对!如果数据都在叶子节点,B树相比B+树还有什么优点?

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                    B树 vs B+树 优缺点对比                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   传统B树优点 (如果非叶子节点存数据):                                       │
│   ✓ 单点查询可能更快 - 可能在中间节点就找到数据,少一次IO                  │
│   ✗ 范围查询更慢 - 需要中序遍历整棵树                                       │
│   ✗ 树高度更高 - 节点存数据,扇出小                                         │
│                                                                             │
│   B+树优点:                                                                 │
│   ✓ 范围查询快 - 叶子节点有链表,直接遍历                                  │
│   ✓ 树高度低 - 非叶子节点只存键值,扇出大                                   │
│   ✓ 查询稳定 - 都要走到叶子节点                                            │
│   ✗ 单点查询 - 必须走到叶子节点                                             │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

但是!WiredTiger的实现实际上是B+树变种!

复制代码
MongoDB WiredTiger 所谓"B树"的实际特点:

1. 索引B树:
   - 非叶子节点: 不存数据
   - 叶子节点: 存RecordId指针
   - 没有叶子链表
   
   → 本质是「没有链表的B+树」

2. 数据B树:
   - 非叶子节点: 不存数据  
   - 叶子节点: 存完整BSON文档
   - 没有叶子链表
   
   → 本质是「没有链表的B+树」
那为什么MongoDB不用B+树?

真正的区别:叶子节点没有链表

复制代码
MySQL B+树:
┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐
│ 1 │◄─►│ 2 │◄─►│ 3 │◄─►│ 4 │◄─►│ 5 │◄─►│ 6 │  叶子节点双向链表
└───┘   └───┘   └───┘   └───┘   └───┘   └───┘
   ↓
范围查询: 找到起点后,沿链表遍历即可

MongoDB B树:
┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐   ┌───┐
│ 1 │   │ 2 │   │ 3 │   │ 4 │   │ 5 │   │ 6 │  没有链表!
└───┘   └───┘   └───┘   └───┘   └───┘   └───┘
   ↓
范围查询: 需要中序遍历,在节点间跳转

1.6 总结:MongoDB为什么叫B树

方面 实际情况
非叶子节点 不存完整数据 (和B+树一样)
叶子节点 存完整数据 (和B+树一样)
叶子链表 没有 (和B+树不同)
树高度 和B+树相近
单点查询 没有优势
范围查询 比B+树慢

结论 : MongoDB的"B树"实际上是没有叶子链表的B+树变种

为什么不用链表?

  1. 文档数据库更关注单文档操作
  2. 避免维护链表的开销
  3. 支持更灵活的压缩策略
  4. 历史设计选择

1.7 重要澄清:关于"直接定位文档"的误解

很多人误以为MongoDB的RecordId可以"直接定位"文档,这是错误的!

根据MongoDB官方技术文档(Franck Pachot,MongoDB开发者布道师):

"RecordId can be considered a physical address by the query layer, but it is not directly mapped to an address in the filesystem because files are B+Tree structures."

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                    回表过程对比(修正版)                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   MySQL 回表:                                                               │
│   ┌─────────────┐          ┌─────────────────────┐                         │
│   │ 二级索引     │          │ 聚簇索引            │                         │
│   │ name='张三'  │          │ id=1 (主键)         │                         │
│   │ id=1        │ ───────► │ ↓ 从根到叶遍历      │                         │
│   └─────────────┘          │ name='张三'         │                         │
│                            │ [完整行数据]        │                         │
│                            └─────────────────────┘                         │
│                                                                             │
│   MongoDB 回表:                                                             │
│   ┌─────────────┐          ┌─────────────────────┐                         │
│   │ 索引B+树     │          │ 数据B+树            │                         │
│   │ name='张三'  │          │ RecordId=1          │                         │
│   │ RecordId=1  │ ───────► │ ↓ 从根到叶遍历      │                         │
│   └─────────────┘          │ {完整BSON文档}      │                         │
│                            └─────────────────────┘                         │
│                                                                             │
│   结论: 两者都需要遍历B+树!RecordId和主键值的作用是一样的!               │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

为什么会有"直接定位"的误解?

  1. 历史原因:早期文档说RecordId是"物理地址"
  2. 类比错误:与Oracle的ROWID、PostgreSQL的CTID混淆
  3. 官方命名:MongoDB称之为"B树",让人以为有B树的"中间节点存数据"优势

实际情况

  • RecordId是一个逻辑地址,不是物理偏移量
  • 获取完整文档仍需要在数据B+树中从根到叶遍历
  • MongoDB的"B树"实际上是B+树变种,没有"在中间节点找到数据"的优势

那RecordId有什么用?

  1. 文档移动时不需更新索引:文档在数据B+树中移动(如页分裂),RecordId不变
  2. 简化索引维护:所有索引只存RecordId,不需要存完整的_id或其他字段
  3. 类似MySQL聚簇索引的主键:提供稳定的"指针"来引用文档

1.8 存储单元对比

WiredTiger 也使用页作为存储单元!

复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                      存储单元对比                                       │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   MySQL InnoDB                        MongoDB WiredTiger               │
│   ┌─────────────────────┐            ┌─────────────────────┐            │
│   │   页 (Page)        │            │   页 (Page)        │            │
│   │   默认: 16KB         │            │   默认: 32KB         │            │
│   │   固定大小           │            │   可配置大小         │            │
│   └─────────────────────┘            └─────────────────────┘            │
│                                                                         │
│   页类型:                             页类型:                           │
│   - 数据页                            - 内部页 (Internal)              │
│   - 索引页                            - 叶子页 (Leaf)                  │
│   - Undo页                            - 溢出页 (Overflow)              │
│   - 系统页                            - 校验页 (Addr)                  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

WiredTiger 页详细配置:

复制代码
默认配置:
┌────────────────────────────────────────────────────────────┐
│  internalPageMaxSize = 32KB    (内部页最大大小)             │
│  leafPageMaxSize = 32KB        (叶子页最大大小)             │
│  memoryPageMax = 32MB          (内存页最大大小)             │
│  allocationSize = 4KB          (分配单元大小)               │
└────────────────────────────────────────────────────────────┘

可通过配置修改:
mongod --wiredTigerEngineConfigString="internal_page_max=64KB,leaf_page_max=64KB"

1.9 并发控制对比 - 为什么MongoDB并发更高

这是MongoDB相对于MySQL的一个重要优势,下面从写入读取两个角度详细分析。

1.9.1 锁粒度对比
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                    锁粒度对比                                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   MySQL InnoDB:                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │  锁粒度: 行级锁                                                      │  │
│   │                                                                     │  │
│   │  锁类型:                                                             │  │
│   │  - Record Lock (记录锁)                                             │  │
│   │  - Gap Lock (间隙锁)                                                │  │
│   │  - Next-Key Lock (临键锁 = Record + Gap)                            │  │
│   │  - Insert Intention Lock (插入意向锁)                               │  │
│   │                                                                     │  │
│   │  问题:                                                               │  │
│   │  - Gap Lock会锁定不存在的记录,阻止其他事务插入                      │  │
│   │  - 复杂的锁类型导致锁冲突概率高                                      │  │
│   │  - 需要维护锁管理器,内存开销大                                      │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│   MongoDB WiredTiger:                                                       │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │  锁粒度: 文档级锁                                                    │  │
│   │                                                                     │  │
│   │  并发控制: 乐观并发控制 (Optimistic Concurrency Control)            │  │
│   │                                                                     │  │
│   │  特点:                                                               │  │
│   │  - 写操作只锁定正在修改的单个文档                                    │  │
│   │  - 不存在Gap Lock,不阻止其他文档插入                                │  │
│   │  - 使用MVCC,读写互不阻塞                                            │  │
│   │  - 写冲突时透明重试                                                  │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1.9.2 写入并发对比

MySQL写入流程(有锁竞争):

复制代码
场景: 100个并发事务更新同一张表的不同行

┌─────────────────────────────────────────────────────────────────────────────┐
│                    MySQL InnoDB 写入流程                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   1. 获取意向排他锁 (IX Lock) - 数据库级别                                  │
│   2. 获取意向排他锁 (IX Lock) - 表级别                                      │
│   3. 查找目标行 (需要读取索引页)                                            │
│      └─ 如果使用二级索引: 需要先读二级索引,再回表读聚簇索引               │
│   4. 对目标行加 Record Lock                                                │
│      └─ 如果是范围查询: 还需要加 Gap Lock                                  │
│   5. 写入Undo Log (用于回滚和MVCC)                                         │
│   6. 修改数据页                                                            │
│   7. 写入Redo Log                                                          │
│   8. 释放锁                                                                 │
│                                                                             │
│   问题:                                                                      │
│   ─────────────────────────────────────────────────────────────             │
│   - Gap Lock会导致"假冲突": 不同事务插入不同位置,但Gap重叠就被阻塞         │
│   - 锁管理器开销: 需要维护大量锁对象                                        │
│   - 死锁检测开销: 复杂的锁关系导致死锁概率高                                │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

MongoDB写入流程(乐观并发控制):

复制代码
场景: 100个并发事务更新同一集合的不同文档

┌─────────────────────────────────────────────────────────────────────────────┐
│                    MongoDB WiredTiger 写入流程                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   1. 获取意向锁 (IX) - 仅在全局/数据库/集合级别                             │
│      └─ 意图锁不阻塞其他读写操作!                                          │
│   2. 直接修改目标文档                                                       │
│      └─ 文档级锁,只锁定正在修改的那一个文档                                │
│   3. 写入Journal                                                            │
│      └─ 使用无锁Slot机制(后面详解)                                        │
│   4. 提交事务                                                               │
│      └─ 如果检测到写冲突,透明重试                                          │
│                                                                             │
│   优势:                                                                      │
│   ─────────────────────────────────────────────────────────────             │
│   - 没有Gap Lock: 不同文档的操作完全不互相影响                              │
│   - 乐观并发控制: 假设冲突少,只在提交时检查                                │
│   - 透明重试: MongoDB自动重试冲突的事务                                     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1.9.3 并发性能总结
复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                    并发性能对比总结                                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   维度                    MySQL InnoDB          MongoDB WiredTiger         │
│   ─────────────────────────────────────────────────────────────             │
│   锁粒度                  行级锁                文档级锁                    │
│   锁类型                  复杂(4种以上)         简单(S/X/IS/IX)             │
│   Gap Lock                有                    无                         │
│   MVCC实现                Undo Log链            时间戳                      │
│   读取开销                可能需要遍历版本链    直接定位                    │
│   写入冲突                阻塞等待              乐观重试                    │
│   死锁概率                较高                  较低                        │
│   锁管理开销              高                    低                          │
│                                                                             │
│   结论: MongoDB在高并发场景下性能更好的原因:                                │
│   1. 没有Gap Lock,不会"假冲突"                                            │
│   2. 乐观并发控制,减少锁等待                                              │
│   3. MVCC实现更简单,读取更快                                              │
│   4. 文档级锁,粒度更细                                                    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1.9.4 MVCC详解:为什么读写互不阻塞

核心原理:读和写操作的是不同的数据版本!

根据 WiredTiger官方文档

"The history store in WiredTiger tracks historical versions of records required to service older readers. By having these records in storage separate from the current version, they can be used to service long running transactions and be evicted as necessary, without interfering with activity that uses the most recent committed versions."

根据 MongoDB官方文章(Franck Pachot)

"MongoDB uses the WiredTiger storage engine, which implements Multi‑Version Concurrency Control (MVCC) to provide lock‑free read consistency"

MVCC工作原理:

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                    MVCC 如何实现读写不阻塞                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   时间线:                                                                    │
│                                                                             │
│   T1: 事务A开始读取 (快照时间戳 = 100)                                       │
│   T2: 事务B修改文档 (写入新版本, 时间戳 = 200)                               │
│   T3: 事务A继续读取 (仍然读取时间戳100的版本)                                │
│   T4: 事务B提交                                                              │
│   T5: 事务A继续读取 (仍然读取时间戳100的版本,不受影响)                      │
│   T6: 事务A结束                                                              │
│                                                                             │
│   文档版本:                                                                  │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │  当前版本 (时间戳200):  { name: "李四", age: 30 }  ← 写操作修改这个   │  │
│   │  历史版本 (时间戳100):  { name: "张三", age: 25 }  ← 读操作读取这个   │  │
│   │                   ↑                                               │  │
│   │                   │                                               │  │
│   │            存储在 History Store                                   │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│   关键: 读操作和写操作访问的是不同的数据版本,所以不需要锁!                 │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

MySQL和MongoDB的MVCC对比:

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                    MySQL MVCC                                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   写操作:                                                                    │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │  1. 获取行锁 (X Lock)                                               │  │
│   │  2. 写入 Undo Log (记录旧值)                                        │  │
│   │  3. 修改当前行                                                      │  │
│   │  4. 释放行锁                                                        │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│   读操作:                                                                    │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │  1. 不需要获取锁!                                                  │  │
│   │  2. 根据快照判断哪一行版本可见                                       │  │
│   │  3. 如果当前行不可见,从 Undo Log 找历史版本                        │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│   关键: 读不获取锁,但写需要获取行锁                                        │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│                    MongoDB WiredTiger MVCC                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   写操作:                                                                    │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │  1. 创建新版本 (带时间戳)                                           │  │
│   │  2. 旧版本移到 History Store                                        │  │
│   │  3. 新版本成为当前版本                                              │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│   读操作:                                                                    │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │  1. 完全无锁!                                                      │  │
│   │  2. 根据快照时间戳,决定读哪个版本                                   │  │
│   │  3. 可能读当前版本,也可能读 History Store 中的历史版本              │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│   关键: 读操作完全不阻塞,写操作只锁定正在修改的那一个文档                  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

MVCC对比总结:

维度 MySQL MVCC MongoDB MVCC
读操作 不加锁,但可能需要遍历Undo Log链 完全无锁,直接定位时间戳版本
写操作 需要获取行锁 只锁定正在修改的文档
历史版本存储 Undo Log(同一行链表) History Store(独立存储)
写是否阻塞读 不阻塞(读历史版本) 不阻塞(读历史版本)
写是否阻塞写 阻塞(行锁冲突时) 只阻塞同一文档的写

为什么MVCC能实现读写不阻塞?

复制代码
核心原理:
┌─────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│   写操作: 创建新版本,不直接覆盖旧数据                                      │
│   读操作: 根据快照读取对应版本                                              │
│                                                                             │
│   → 写操作不破坏读操作正在读的数据                                          │
│   → 读操作不阻塞写操作创建新版本                                            │
│   → 两者互不干扰!                                                          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

类比理解:
┌─────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│   想象一个文档有多个"副本":                                                 │
│                                                                             │
│   版本1 (时间戳100): { name: "张三" }  ← 事务A在读这个                     │
│   版本2 (时间戳200): { name: "李四" }  ← 事务B写完存这个                    │
│                                                                             │
│   事务A读版本1,事务B写版本2                                                │
│   两者操作的是不同的"副本",所以不需要锁!                                  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

2.1 MySQL - 关系模型

sql 复制代码
-- 用户表
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 订单表
CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    product_name VARCHAR(100),
    amount DECIMAL(10, 2),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

特点:

  • 预定义表结构
  • 数据规范化,减少冗余
  • 关系通过外键关联
  • 需要多表JOIN查询

2.2 MongoDB - 文档模型

javascript 复制代码
// 用户文档
{
    "_id": ObjectId("..."),
    "username": "john_doe",
    "email": "john@example.com",
    "created_at": ISODate("2024-01-01"),
    "orders": [
        {
            "product_name": "Laptop",
            "amount": 999.99,
            "order_date": ISODate("2024-01-15")
        }
    ],
    "profile": {
        "age": 25,
        "address": {
            "city": "Shanghai",
            "country": "China"
        }
    }
}

特点:

  • 嵌套文档结构
  • 无需预定义Schema
  • 数据可以冗余存储
  • 单文档查询即可获取关联数据

3. 查询语言

3.1 MySQL - SQL

sql 复制代码
-- 插入
INSERT INTO users (username, email) VALUES ('john', 'john@example.com');

-- 查询
SELECT * FROM users WHERE username = 'john';

-- 更新
UPDATE users SET email = 'new@example.com' WHERE id = 1;

-- 删除
DELETE FROM users WHERE id = 1;

-- 复杂查询 (JOIN)
SELECT u.username, o.product_name
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.email LIKE '%@example.com';

3.2 MongoDB - MQL

javascript 复制代码
// 插入
db.users.insertOne({
    username: "john",
    email: "john@example.com"
});

// 查询
db.users.find({ username: "john" });

// 更新
db.users.updateOne(
    { _id: ObjectId("...") },
    { $set: { email: "new@example.com" } }
);

// 删除
db.users.deleteOne({ _id: ObjectId("...") });

// 聚合管道 (类似JOIN)
db.orders.aggregate([
    {
        $lookup: {
            from: "users",
            localField: "user_id",
            foreignField: "_id",
            as: "user_info"
        }
    }
]);

4. 事务支持

4.1 MySQL

sql 复制代码
START TRANSACTION;

-- 转账操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- 提交或回滚
COMMIT;
-- 或 ROLLBACK;

特点:

  • 完整ACID事务支持
  • 支持多表事务
  • 支持事务隔离级别
  • 成熟的事务机制

4.2 MongoDB

javascript 复制代码
// MongoDB 4.0+ 支持多文档事务
const session = db.getMongo().startSession();
session.startTransaction();

try {
    const accounts = session.getDatabase('test').accounts;
    
    accounts.updateOne(
        { _id: 1 },
        { $inc: { balance: -100 } }
    );
    
    accounts.updateOne(
        { _id: 2 },
        { $inc: { balance: 100 } }
    );
    
    session.commitTransaction();
} catch (error) {
    session.abortTransaction();
} finally {
    session.endSession();
}

特点:

  • 4.0版本开始支持多文档事务
  • 事务性能开销较大
  • 建议尽量使用单文档原子操作
  • 适用于必须保证一致性的场景

5. 持久化机制对比

5.1 概述

MySQL 使用 Redo Log(重做日志) 实现持久化。

MongoDB 使用 Journal(预写日志) 实现持久化。

两者都采用 WAL (Write-Ahead Logging) 机制,但在实现细节上有很大差异。

5.2 MySQL Redo Log

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                    MySQL InnoDB Redo Log 架构                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌─────────────────┐      ┌─────────────────┐      ┌─────────────────┐   │
│   │  用户事务        │      │  Log Buffer     │      │  Redo Log Files │   │
│   │  (修改数据)      │ ───► │  (内存缓冲区)    │ ───► │  (磁盘文件)      │   │
│   └─────────────────┘      └─────────────────┘      └─────────────────┘   │
│                                    │                        │             │
│                                    │ 刷盘时机                │             │
│                                    ▼                        ▼             │
│                           ┌─────────────────────────────────────────┐    │
│                           │  innodb_flush_log_at_trx_commit:        │    │
│                           │  - 0: 每秒刷盘 (可能丢1秒数据)           │    │
│                           │  - 1: 每次提交刷盘 (最安全, 默认)        │    │
│                           │  - 2: 写入OS缓存, 每秒刷盘              │    │
│                           └─────────────────────────────────────────┘    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Redo Log 关键特性:

维度 说明
文件结构 ib_logfile0/1,固定大小,循环写入
页大小 512字节 / 4KB
记录格式 Physiological Logging (物理逻辑日志)
I/O模式 Buffered I/O / Direct I/O (可配置)
压缩 不支持
并发写入 Mutex锁

5.3 MongoDB Journal

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                    MongoDB WiredTiger Journal 架构                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌─────────────────┐      ┌─────────────────┐      ┌─────────────────┐   │
│   │  用户操作        │      │  Log Buffer     │      │  Journal Files  │   │
│   │  (修改文档)      │ ───► │  (Slot内存缓冲)  │ ───► │  (磁盘文件)      │   │
│   └─────────────────┘      └─────────────────┘      └─────────────────┘   │
│                                    │                        │             │
│                                    │ 无锁并发写入            │             │
│                                    ▼                        ▼             │
│                           ┌─────────────────────────────────────────┐    │
│                           │  Write Concern (写入关注级别):          │    │
│                           │  - w:0  不确认 (最快)                    │    │
│                           │  - w:1  内存确认 (默认)                  │    │
│                           │  - j:true  Journal刷盘确认              │    │
│                           │  - w:majority  多数节点确认             │    │
│                           └─────────────────────────────────────────┘    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Journal 关键特性:

维度 说明
文件结构 WiredTigerLog.*,动态增长,自动清理
页大小 8KB
记录格式 事务操作记录,支持压缩/加密
I/O模式 Direct I/O (强制)
压缩 Snappy (默认开启)
并发写入 无锁Slot机制

5.4 无锁并发写入详解

MySQL有锁方式:

复制代码
MySQL InnoDB Redo Log 写入流程:

┌─────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│   Thread1 ──┐                                                               │
│   Thread2 ──┼──► 获取Mutex锁 ──► 写入Log Buffer ──► 释放锁 ──► 返回        │
│   Thread3 ──┘           │                                                   │
│                         ▼                                                   │
│                    其他线程等待...                                          │
│                                                                             │
│   时间线:                                                                    │
│   Thread1: ════════════════════════════════════►                            │
│            [获取锁] [写Buffer] [释放锁]                                      │
│                                                                             │
│   Thread2:              ══════════════════════════════════════►             │
│                        [等待...] [获取锁] [写Buffer] [释放锁]                │
│                                                                             │
│   问题: 线程多的时候,大量时间花在等待锁上                                    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

MongoDB无锁方式:

复制代码
Step 1: 线程用原子操作预留空间
┌─────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│   假设: buffer_size = 100KB, slot_state = READY                             │
│                                                                             │
│   Thread1 要写 2KB:                                                         │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │  // 原子操作: 比较并交换 (Compare And Swap)                         │  │
│   │  CAS(slot_state, READY, READY + 2KB)                               │  │
│   │                                                                     │  │
│   │  如果slot_state == READY, 就把它改成 READY + 2KB                   │  │
│   │  返回旧值 READY                                                      │  │
│   │  Thread1知道自己的offset = 0 (因为旧值是READY)                      │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│   Thread2 要写 3KB (同时发生):                                               │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │  第一次CAS失败: slot_state已经是 READY + 2KB了                      │  │
│   │  第二次CAS成功:                                                      │  │
│   │  CAS(slot_state, READY + 2KB, READY + 5KB)                         │  │
│   │  Thread2知道自己的offset = 2KB                                       │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│   结果: slot_state = READY + 5KB (已预留5KB)                                │
│   Thread1: offset=0, 写2KB | Thread2: offset=2KB, 写3KB                    │
│                                                                             │
│   注意: 没有任何锁! 只有原子操作!                                            │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Step 2: 所有线程并行拷贝数据
┌─────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│   Buffer布局:                                                                │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │  Offset 0      │  Offset 2KB    │  Offset 5KB    │                   │  │
│   │  Thread1的数据  │  Thread2的数据  │  (空闲)        │                   │  │
│   │  2KB           │  3KB           │                │                   │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│   Thread1 拷贝完成后: atomic_add(slot_state, 2KB)                           │
│   Thread2 拷贝完成后: atomic_add(slot_state, 3KB)                           │
│                                                                             │
│   最后一个完成的线程:                                                        │
│   - 发现 slot_state == 0                                                    │
│   - 负责把整个Buffer写入磁盘                                                │
│   - 其他线程已经返回了, 不需要等待                                          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

性能对比:

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                    有锁 vs 无锁 性能对比                                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   100个线程同时写日志:                                                       │
│                                                                             │
│   MySQL (有锁):                                                             │
│   - 只有1个线程在写Buffer                                                   │
│   - 99个线程在等锁                                                          │
│   - 大量CPU时间浪费在锁等待上                                               │
│                                                                             │
│   MongoDB (无锁):                                                           │
│   - 100个线程都在写Buffer (各自不同的区域)                                  │
│   - 只有最后1个线程等I/O                                                    │
│   - 没有锁等待开销                                                          │
│                                                                             │
│   结论: 高并发场景下, 无锁方式的吞吐量远高于有锁方式                         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

6. 扩展性

6.1 MySQL - 垂直扩展为主

复制代码
单机扩展:
┌─────────────────┐
│   Application   │
├─────────────────┤
│     MySQL       │
│  (更大服务器)     │
│  - 更多CPU       │
│  - 更多内存       │
│  - 更快存储       │
└─────────────────┘

集群方案:
- 主从复制 (读写分离)
- 分库分表 (应用层实现)
- MySQL Cluster (NDB)

特点:

  • 垂直扩展简单
  • 水平扩展复杂
  • 需要应用层或中间件支持
  • 扩展成本较高

6.2 MongoDB - 水平扩展优先

复制代码
分片集群:
┌─────────────────────────────────────────────────────────┐
│         mongos (路由)               │
└────────────┬────────────────────────┘
             │
    ┌────────┴────────┐
    │                 │
┌───▼───┐         ┌───▼───┐
│Shard 1│         │Shard 2│
│(副本集) │         │(副本集) │
└───────┘         └───────┘

Config Servers (配置服务器)

特点:

  • 原生支持分片
  • 自动数据分布
  • 线性扩展能力
  • 扩展成本低

总结

维度 MySQL InnoDB MongoDB WiredTiger
存储结构 B+树 B+树变种(无叶子链表)
锁粒度 行级锁(含Gap Lock) 文档级锁
并发控制 悲观锁 + MVCC 乐观并发控制 + MVCC
并发性能 中等 更高
日志写入 有锁(Mutex) 无锁(Slot机制)
持久化 Redo Log Journal
扩展性 垂直扩展为主 水平扩展优先

MongoDB并发更高的核心原因:

  1. 没有Gap Lock - 不会出现"假冲突"
  2. 乐观并发控制 - 减少锁等待
  3. 无锁日志写入 - Slot机制实现高并发写入
  4. 简单的MVCC - 时间戳机制比Undo Log链更高效
相关推荐
nbsaas-boot2 小时前
SQL JOIN 图解说明
android·数据库·sql
软件资深者2 小时前
阿里云轻量服务器部署 OpenClaw 完整教程
数据库·人工智能·ai·open claw·龙虾·openclaw安装·clawx
li星野2 小时前
QT面试题
java·数据库·qt
Albert Tan2 小时前
Oracle EBS PO 报错 -- 非买手
android·数据库·oracle
一切为了实战2 小时前
10001-需求管理新增字段【ADB 模块】、【YUNTI 模块】需求文档(测试文档)
数据库·adb
好学且牛逼的马2 小时前
Spring Boot 核心注解完全手册
java·spring boot·后端
ShoreKiten2 小时前
Flask/ssti --by vulhub
后端·python·flask
小鸡脚来咯2 小时前
SQL 语法面试考点
数据库·sql·oracle
xuxie992 小时前
Next 13 sqlite3 查找、网页
java·数据库·oracle