Mysql 事务和锁的一些概念和理解
·
事务的并发性和隔离性由存储引擎(如 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个数据页(为了简化说明,实际远大于此)。
第一次查询
- 执行SQL: 你执行了
SELECT * FROM users WHERE id = 100;- 查找过程:
- MySQL的InnoDB引擎收到这个请求,需要找到ID为100的用户记录。
- 它首先去Buffer Pool里查找。因为是第一次查询,Buffer Pool里没有任何
users表的数据。- 查找失败。
- 磁盘读取:
- 引擎必须去磁盘上读取包含ID=100这条记录的那个16KB的数据页。
- 假设这个记录在磁盘上的第5个数据页(Page 5)中。
- 引擎会将整个"Page 5"从磁盘读取到内存的Buffer Pool中。
- 返回结果: 在内存中的"Page 5"里找到ID=100的记录,返回给客户端。
- Buffer Pool状态: 此刻,Buffer Pool里有了1个页,即"Page 5"。
第二次查询(命中缓存)
- 执行SQL: 你紧接着又执行了
SELECT * FROM users WHERE id = 100;(还是查ID=100)- 查找过程:
- InnoDB引擎再次收到请求。
- 它去Buffer Pool里查找ID=100的记录。
- 发现"Page 5"已经在Buffer Pool里了!
- 返回结果: 引擎直接在内存中的"Page 5"里找到记录,快速返回给客户端。这次没有发生任何磁盘I/O! 这就是缓存带来的巨大性能提升。
访问其他数据
- 执行SQL:
SELECT * FROM users WHERE id = 200;- 查找过程: Buffer Pool里没有包含ID=200的页。
- 磁盘读取: 假设ID=200在磁盘的"Page 8"中,引擎将"Page 8"读入Buffer Pool。
- 更新LRU列表: 为了管理缓存,InnoDB会维护一个LRU(最近最少使用)链表。
- 刚刚读入的"Page 8"被认为是最“热”的,会被放到LRU链表的头部。
- 之前被访问的"Page 5"则会被移到链表的后面一点。
- Buffer Pool状态: 现在Buffer Pool里有"Page 5"和"Page 8"两个页。
插入数据
- 执行SQL:
INSERT INTO users (username, email, password_hash) VALUES ('new_user', 'new@example.com', '...');- 查找过程: InnoDB需要更新聚集索引(主键索引),找到插入位置。同样,它会先在Buffer Pool中查找相关的索引页。
- 缓存命中/未命中: 如果相关的索引页(比如根节点或中间节点的页)在Buffer Pool中,就直接修改内存;如果没有,也需要先从磁盘读取到Buffer Pool。
- 修改内存: 将新记录插入到Buffer Pool中的相应页里。
- 标记脏页: 这个被修改过的页现在被称为“脏页”(Dirty Page),因为它内存中的内容与磁盘上的不一致。
- 后续处理: 这个脏页不会立即写回磁盘,而是等待后台线程(如
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 是最近最少访问的)
- 执行SQL:
SELECT * FROM users WHERE id = 300;假设这条记录在 Page 15 中。- Buffer Pool已满: Buffer Pool已经没有空闲空间了。
- 触发淘汰: InnoDB需要从Buffer Pool中“踢”出一个页,为即将读入的"Page 15"腾地方。根据LRU算法,它会选择LRU链表尾部的页,也就是
Page 10。- 检查脏页: InnoDB检查
Page 10是否是脏页。
- 如果不是脏页 (Clean Page): 直接将其从Buffer Pool中移除即可。
- 如果是脏页 (Dirty Page): 需要先将
Page 10的内容写回到磁盘,然后再将其从Buffer Pool中移除。- 加载新页: 将磁盘上的
Page 15读取到刚刚释放出来的内存空间中。- 更新LRU列表: 将
Page 15插入到LRU链表的头部。- 最终LRU列表: 可能变成
[Page 15, Page 7, Page 3, Page 9, Page 2, Page 8, Page 5, Page 1, Page 4, Page 6](Page 10 已被移除)
更多推荐



所有评论(0)