事务的并发性和隔离性由存储引擎(如 InnoDB)和事务隔离级别共同决定。

MySQL(尤其是 InnoDB 引擎)使用 行级锁(row-level locking)来实现事务的隔离性

  • 如果事务 A 修改了某一行,它会对该行加排他锁(X 锁)。
  • 此时事务 B 若也尝试修改或读取(取决于隔离级别)同一行,就可能被阻塞,直到事务 A 提交或回滚。

MySQL 默认隔离级别是 REPEATABLE READ(可重复读),不同级别对并发的影响不同:

  • READ UNCOMMITTED:几乎无阻塞(但可能读到脏数据)。
  • READ COMMITTED:读操作一般不加锁(使用快照读),写操作加行锁。
  • REPEATABLE READ:InnoDB 使用 MVCC + Next-Key Lock 防止幻读,可能在范围查询时加间隙锁(Gap Lock),增加锁竞争。
  • SERIALIZABLE:所有读操作都加共享锁,显著增加阻塞可能性,但依然不是“全局”锁。

两个概念

脏读

脏读是指一个事务读取了另一个尚未提交的事务所修改的数据。
如果那个未提交的事务后来回滚(ROLLBACK)了,那么当前事务读到的数据就是“不存在”的、无效的——这就是所谓的“脏数据”。

例子:

  • MySQL 的 InnoDB 引擎在默认隔离级别(REPEATABLE READ)下禁止脏读

说明一下:

-- 事务A
UPDATE accounts SET balance = balance - 100 WHERE user = 'Alice';  -- 余额从500变成400
-- (但还没 COMMIT)

       Mysql执行逻辑:

       执行这个sql后,数据在物理/内存层面已经被修改了。

       InnoDB 会在内存中(Buffer Pool)修改该行的数据页,将 balance 从 500 改为 400

  • 同时,它会:
    • 在 Undo Log 中保留旧值(500),用于回滚或 MVCC 快照读。
    • 在 Redo Log 中记录这次修改,用于崩溃恢复。
    • 对该行加上 排他锁(X 锁),防止其他事务同时修改。

在正常隔离级别下,其他事务是看不到这个Update后的400的

原理:

   其他事务使用 READ COMMITTED / REPEATABLE READ(默认)
  • InnoDB 使用 MVCC(多版本并发控制)
  • 其他事务执行 SELECT balance FROM accounts WHERE user = 'Alice'; 时:
    • 会通过 Read View 判断当前事务 A 是否已提交。
    • 因为事务 A 未提交,所以其他事务看不到 400,而是通过 Undo Log 读到旧值 500
  • 只有当事务 A COMMIT 后,这个 400 才对其他事务可见。
    (通俗的讲就是会检查事务A是否全部提交完了,隔离性和原子性的体现)

幻读

    幻读是指在一个事务中,两次执行相同的查询,但第二次查询返回了第一次没有的结果(即“凭空出现”的新行),就像出现了“幻影”。

幻读通常发生在插入(INSERT)操作上,而不是更新。

-- 事务A 开始
START TRANSACTION;

-- 第一次查询
SELECT * FROM students WHERE score > 80;  -- 返回 0 行

-- 此时,事务B 插入了一条新记录:
INSERT INTO students (id, name, score) VALUES (3, 'Tom', 90);
COMMIT;

-- 事务A 再次执行相同查询
SELECT * FROM students WHERE score > 80;  -- 现在返回 Tom!
  • MySQL InnoDB 在 REPEATABLE READ 级别下通过“Next-Key Lock”(行锁 + 间隙锁)也防止了幻读(这是 InnoDB 的特殊优化,不同于 SQL 标准)。

  • 幻读新增行导致查询结果集变大(针对新插入的行)---结果集会变大。

区别:

总结就是 放心使用。。遇到其他问题再说

MySQL 默认(InnoDB + REPEATABLE READ)

  • 不会出现 脏读
  • 不会出现 不可重复读
  • 也不会出现幻读(得益于间隙锁)

Buffer Pool

基本概念:

  • Buffer Pool是MySQL InnoDB存储引擎的一个全局内存结构。它不是按表划分的,也不是按数据库划分的,而是整个MySQL服务器实例独享的一个大内存池
  • 本质: Buffer Pool(缓冲池)是MySQL InnoDB存储引擎在内存中开辟的一片区域,本质上是一片连续的内存空间。
  • 目的: 主要目的是为了缓存磁盘上的数据页(Data Pages)和索引页(Index Pages)。当MySQL需要读取或修改数据时,会首先尝试在Buffer Pool中进行操作,而不是直接访问速度较慢的磁盘。这极大地减少了磁盘I/O次数,显著提升了数据库的性能。

为什么需要 Buffer Pool?

  • 磁盘I/O是数据库性能的主要瓶颈。相比于内存访问,从磁盘读取数据的速度要慢得多。
  • 通过将热点数据(经常被访问的数据)保留在内存中的Buffer Pool里,可以避免频繁的磁盘随机读取,从而大幅提升查询和修改数据的速度。

 Buffer Pool 的基本结构与内容

  • 单位: Buffer Pool以页(Page)为基本单位进行管理,默认每页大小为16KB。
  • 缓存内容:
    • 数据页 (Data Pages): 表中的实际行数据所在的页。
    • 索引页 (Index Pages): 表上各种索引(如主键索引、二级索引)的B+树节点所在的页。
  • 管理方式: 其底层通常采用链表等数据结构来管理这些缓存页。

Buffer Pool 的工作机制 - LRU算法

  • 核心思想: 为了高效利用有限的内存空间,Buffer Pool使用LRU(Least Recently Used,最近最少使用)算法来决定哪些页应该保留在内存中,哪些页应该被淘汰出去。
  • 实现: 内部维护一个LRU链表,当一个页被访问(读取或修改)时,它会被移动到链表的头部。长时间未被访问的页会逐渐移向链表的尾部。
  • 淘汰策略: 当Buffer Pool空间不足时,InnoDB会将LRU链表尾部的页(即最近最少使用的页)刷回磁盘(如果该页被修改过,即脏页),然后将其从Buffer Pool中移除,为新的数据页腾出空间。

 Buffer Pool 相关配置

  • 大小设置: Buffer Pool的大小可以通过参数 innodb_buffer_pool_size 来配置。
    • 默认值: 早期版本通常是128MB,新版本可能更大。
    • 推荐配置: 通常建议将其设置为服务器物理内存的60%到80%,具体取决于服务器上是否只运行MySQL以及应用的特点。
  • 启动分配: MySQL服务器启动时,会根据配置的大小一次性向操作系统申请这片内存空间

例子说明:

初始状态
  • MySQL刚刚启动,Buffer Pool是空的,或者只包含一些系统数据页。
  • 假设你的Buffer Pool总共能存放10个数据页(为了简化说明,实际远大于此)。
第一次查询
  1. 执行SQL: 你执行了 SELECT * FROM users WHERE id = 100;
  2. 查找过程:
    • MySQL的InnoDB引擎收到这个请求,需要找到ID为100的用户记录。
    • 它首先去Buffer Pool里查找。因为是第一次查询,Buffer Pool里没有任何users表的数据。
    • 查找失败。
  3. 磁盘读取:
    • 引擎必须去磁盘上读取包含ID=100这条记录的那个16KB的数据页。
    • 假设这个记录在磁盘上的第5个数据页(Page 5)中。
    • 引擎会将整个"Page 5"从磁盘读取到内存的Buffer Pool中。
  4. 返回结果: 在内存中的"Page 5"里找到ID=100的记录,返回给客户端。
  5. Buffer Pool状态: 此刻,Buffer Pool里有了1个页,即"Page 5"。

第二次查询(命中缓存)

  1. 执行SQL: 你紧接着又执行了 SELECT * FROM users WHERE id = 100; (还是查ID=100)
  2. 查找过程:
    • InnoDB引擎再次收到请求。
    • 它去Buffer Pool里查找ID=100的记录。
    • 发现"Page 5"已经在Buffer Pool里了!
  3. 返回结果: 引擎直接在内存中的"Page 5"里找到记录,快速返回给客户端。这次没有发生任何磁盘I/O! 这就是缓存带来的巨大性能提升。

访问其他数据

  1. 执行SQL: SELECT * FROM users WHERE id = 200;
  2. 查找过程: Buffer Pool里没有包含ID=200的页。
  3. 磁盘读取: 假设ID=200在磁盘的"Page 8"中,引擎将"Page 8"读入Buffer Pool。
  4. 更新LRU列表: 为了管理缓存,InnoDB会维护一个LRU(最近最少使用)链表。
    • 刚刚读入的"Page 8"被认为是最“热”的,会被放到LRU链表的头部
    • 之前被访问的"Page 5"则会被移到链表的后面一点。
  5. Buffer Pool状态: 现在Buffer Pool里有"Page 5"和"Page 8"两个页。

插入数据

  1. 执行SQL: INSERT INTO users (username, email, password_hash) VALUES ('new_user', 'new@example.com', '...');
  2. 查找过程: InnoDB需要更新聚集索引(主键索引),找到插入位置。同样,它会先在Buffer Pool中查找相关的索引页。
  3. 缓存命中/未命中: 如果相关的索引页(比如根节点或中间节点的页)在Buffer Pool中,就直接修改内存;如果没有,也需要先从磁盘读取到Buffer Pool。
  4. 修改内存: 将新记录插入到Buffer Pool中的相应页里。
  5. 标记脏页: 这个被修改过的页现在被称为“脏页”(Dirty Page),因为它内存中的内容与磁盘上的不一致。
  6. 后续处理: 这个脏页不会立即写回磁盘,而是等待后台线程(如innodb_io_capacity控制的刷新线程)在合适的时机将其刷回磁盘,以保证数据持久化。

Buffer Pool满了怎么办?(LRU算法详解)

假设经过一系列操作后,Buffer Pool的10个页都已经被占满,分别是 Page 1, 2, 3, ..., 10。并且,根据它们的访问频率,它们在LRU链表中的顺序是这样的(越靠近头部表示越“热”):
[Page 7, Page 3, Page 9, Page 2, Page 8, Page 5, Page 1, Page 4, Page 6, Page 10]
(Page 7 是最近最常访问的,Page 10 是最近最少访问的)

  1. 执行SQL: SELECT * FROM users WHERE id = 300; 假设这条记录在 Page 15 中。
  2. Buffer Pool已满: Buffer Pool已经没有空闲空间了。
  3. 触发淘汰: InnoDB需要从Buffer Pool中“踢”出一个页,为即将读入的"Page 15"腾地方。根据LRU算法,它会选择LRU链表尾部的页,也就是Page 10
  4. 检查脏页: InnoDB检查Page 10是否是脏页。
    • 如果不是脏页 (Clean Page): 直接将其从Buffer Pool中移除即可。
    • 如果是脏页 (Dirty Page): 需要先将Page 10的内容写回到磁盘,然后再将其从Buffer Pool中移除。
  5. 加载新页: 将磁盘上的Page 15读取到刚刚释放出来的内存空间中。
  6. 更新LRU列表: 将Page 15插入到LRU链表的头部
  7. 最终LRU列表: 可能变成 [Page 15, Page 7, Page 3, Page 9, Page 2, Page 8, Page 5, Page 1, Page 4, Page 6] (Page 10 已被移除)
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐