探索MySQL InnoDB:事务、日志与锁的奥秘
目录
- 1 事务
- 1.1 事务的基本概念
- 1.2 ACID特性
- 1.3 隔离级别
- 1.4 MVCC
- 2 锁
- 2.1 锁的类型
- 2.2 锁算法(自动加锁)
- 3 两个重要日志
- 3.1 unlog
- 3.2 redolog
- 3.3 综合示例
- 4 内存池
1 事务
1.1 事务的基本概念
目的
事务将数据库从一种一致性状态转换为另一种一致性状态;
组成
事务可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成;
特征
在数据库提交事务时,可以确保要么所有修改都已经保存,要么所有修改都不保存;
事务是访问并更新数据库各种数据项的一个程序执行单元。
在 MySQL innodb 下,每一条语句都是事务;可以通过 set auto commit = 0; 设置当前会话手动提交
基本语法
start transaction;
sql 语句;
...
commit; 或者rollback
commit 是提交更改
rollback : 回滚事务,结束用户的事务,并撤销正在进行的所有未提交的修改
还有一个保存点
START TRANSACTION;INSERT INTO class (caption) VALUES ('2031班');
SAVEPOINT my_savepoint; -- 设置保存点INSERT INTO class (caption) VALUES ('2032班');
-- 假设这里发生错误,需要回滚到保存点
ROLLBACK TO SAVEPOINT my_savepoint;INSERT INTO class (caption) VALUES ('2033班');
COMMIT;
补充知识 github的版本控制
初始化仓库
git init
添加文件
git add README.md
提交更改
git commit -m “Initial commit”
修改文件
假设你修改了 README.md 文件,然后再次提交:
git add README.md
git commit -m “Update README.md”
现在还没推送到远程仓库
git log
git revert <commit-hash>//回滚
Git 中的 commit-hash
commit-hash 是 Git 中用于唯一标识一个提交(commit)的哈希值。每次提交时,Git 会生成一个唯一的哈希值来标识这次提交。
它是一个 40 位的十六进制字符串,例如:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0。
1.2 ACID特性
**原子性(A):**事务操作要么都做(提交),要么都不做(回滚);事务是访问并更新数据库各种数据项的一个程序执行单元,是不可分割的工作单位;通过 undolog 来实现回滚操作。undolog 记录的是事务每步具体操作,当回滚时,回放事务具体操作的逆运算;
假设有一个银行转账事务,从账户 A 转 100 元到账户 B:
从账户 A 中扣除 100 元。
向账户 B 中增加 100 元。
如果在执行第二步时发生错误(例如,账户 B 不存在),整个事务将回滚到初始状态,账户 A 的余额将恢复到转账前的状态。
隔离性(I)
事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,并发事务之间不会相互影响,设定了不同程度的隔离级别,通过适度破环一致性,得以提高性能;通过 MVCC 和 锁来实现;MVCC 时多版本并发控制,主要解决一致性非锁定读,通过记录和获取行版本,而不是使用锁来限制读操作,从而实现高效并发读性能。锁用来处理并发 DML 操作;数据库中提供粒度锁的策略,针对表(聚集索引B+树)、页(聚集索引B+树叶子节点)、行(叶子节点当中某一段记录行)三种粒度加锁;
持久性(D)
事务提交后,事务DML操作将会持久化(写入 redolog 磁盘文件 哪一个页 页偏移值 具体数据);即使发生宕机等故障,数据库也能将数据恢复。redolog 记录的是物理日志;
一致性(C)
一致性指事务将数据库从一种一致性状态转变为下一种一致性的状态,在事务执行前后,数据库完整性约束没有被破坏;一个事务单元需要提交之后才会被其他事务可见。
假设有一个表 students,其中 name 是唯一键。如果一个事务尝试插入一个已经存在的名字,事务将失败并回滚,以维护一致性。
总结
原子性:确保事务中的所有操作要么全部成功,要么全部失败。
隔离性:通过 MVCC 和锁,确保并发事务不会相互干扰。
持久性:通过 redolog,确保事务提交后的结果将永久保存。
一致性:通过完整性约束检查,确保事务不会破坏数据库的一致性状态。
1.3 隔离级别
- READ UNCOMMITTED(读未提交)(RU)
特点:
读操作不加锁,写操作加排他锁。
写锁在事务提交或回滚后释放。
可能发生的错误:
脏读(Dirty Read):一个事务读取了另一个事务尚未提交的数据。
假设有一个表 accounts,包含两个账户:
复制
account_id balance
1 1000
2 2000
事务 1 和事务 2 同时运行:
事务 1:
START TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;-- 事务 1 暂时挂起,未提交
事务 2:
START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 1; -- 读取到 balance = 500
COMMIT;
事务 1 回滚:
ROLLBACK;
**
在这种情况下,事务 2 读取到了事务 1 未提交的数据(balance = 500)**,但实际上事务 1 最终回滚了,导致事务 2 读取到的数据是错误的。
2 READ COMMITTED(读已提交)(RC)
特点:
支持 MVCC,读取操作读取历史快照数据。
读取的是已提交的数据。
避免了脏读,但可能出现不可重复读。
可能发生的错误:
不可重复读(Non-Repeatable Read):一个事务在两次读取同一数据时,读取到的数据不一致。
事务 1:
START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 1; -- 读取到 balance = 1000
事务 2:
START TRANSACTION;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;COMMIT;
事务 1 再次读取:
SELECT balance FROM accounts WHERE account_id = 1; -- 读取到 balance = 500
COMMIT;
在这种情况下,事务 1 在两次读取同一数据时,读取到的数据不一致(第一次是 1000,第二次是 500),这就是不可重复读。
3. REPEATABLE READ(可重复读)
特点:
支持 MVCC,读取操作读取事务开始时的版本数据。
保证在同一个事务中多次读取同一数据时,读取到的数据一致。
避免了不可重复读,但可能出现幻读。
可能发生的错误:
幻读(Phantom Read):一个事务在两次读取同一范围的数据时,读取到的数据不一致。
事务一
START TRANSACTION;
SELECT * FROM accounts WHERE balance > 1500; -- 读取到 account_id = 2
事务二
START TRANSACTION;
INSERT INTO accounts (account_id, balance) VALUES (3, 1800);
COMMIT;
事务一
SELECT * FROM accounts WHERE balance > 1500; -- 读取到 account_id = 2 和 account_id = 3
COMMIT;
在这种情况下,事务 1 在两次读取同一范围的数据时,读取到的数据不一致(第一次只读到 account_id = 2,第二次读到 account_id = 2 和 account_id = 3),这就是幻读。
4. SERIALIZABLE(可串行化)
特点:
最严格的隔离级别。
给读加了共享锁,事务完全串行化执行。
避免了脏读、不可重复读和幻读。
可能发生的错误:
性能问题:由于事务完全串行化,可能导致性能下降。
死锁:由于锁的粒度较细,容易导致死锁。
讲一下
MySQL innodb默认支持的隔离级别是 REPEATABLE READ 甲骨文数据库是RC
语法
- 设置全局隔离级别(用户)
全局隔离级别会影响所有新创建的会话(连接)。可以通过以下命令设置全局隔离级别:
SET GLOBAL TRANSACTION ISOLATION LEVEL <隔离级别>;
--例如,设置全局隔离级别为 READ COMMITTED:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 设置会话隔离级别
会话隔离级别仅影响当前会话(连接)。可以通过以下命令设置会话隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL <隔离级别>;、
SET TRANSACTION ISOLATION LEVEL <隔离级别>;
-- 例如,设置当前会话的隔离级别为 REPEATABLE READ:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
3 查看当前会话级别
SELECT @@transaction_isolation;
1.4 MVCC
MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种并发控制机制,它允许数据库在同一时间支持多个事务对数据的并发访问,同时保证数据的一致性和隔离性。MVCC 的核心思想是为数据的每个修改操作生成一个新的版本,并保留旧版本,这样多个事务可以基于不同的版本进行操作,从而减少事务之间的阻塞
一致性非锁定读(Consistent Non-locking Read):
MVCC 允许事务进行一致性非锁定读,即读操作不需要加锁,事务可以直接读取数据的某个历史版本,这大大提高了并发性能。
Read View(读视图):
Read View 是 MVCC 的核心之一,用于记录事务可见的快照数据。它包括以下几个关键部分:
- m_ids:在创建 Read View 时,记录所有未提交事务的 ID 列表。
- min_trx_id:在创建 Read View 时,记录未提交事务的最小 ID。
- max_trx_id:在创建 Read View 时,预分配给下一个未开启事务的 ID。
- creator_trx_id:创建 Read View 的事务 ID。
- [min_trx_id, max_trx_id)正在执行事务的区间
事务可见性:
MVCC 通过事务 ID(trx_id)来判断事务之间的可见性: - 如果事务的 trx_id 小于 min_trx_id,则事务已提交,数据可见。
- 如果事务的 trx_id 大于或等于 max_trx_id,则事务是后启动的,数据不可见。
- 如果事务的 trx_id 在 min_trx_id 和 max_trx_id 之间,且不在 m_ids 列表中,则数据可见。
快照读与当前读: - 快照读:读取数据的快照版本(历史版本),用于一致性非锁定读。
- 当前读:读取数据的最新版本,通常需要加锁以保证数据的一致性。
MVCC 的工作原理
数据版本链:
每个数据记录都有一个隐藏的版本链,记录了数据的历史版本。每个版本包含指向旧版本的指针(roll_pointer)。
Read View 的创建:
事务开始时,会创建一个 Read View,该 Read View 记录了当前所有未提交事务的 ID 列表(m_ids)、最小事务 ID(min_trx_id)、最大事务 ID(max_trx_id)等信息。
事务可见性判断:
当事务读取数据时,会根据 Read View 中的 min_trx_id、max_trx_id 和 m_ids 来判断是否可以看到某个版本的数据。
快照读与当前读:
快照读:事务读取的是其 Read View 中可见的快照版本。
当前读:事务读取的是数据的最新版本,可能需要加锁以保证一致性。
MVCC 在不同隔离级别中的表现
- READ COMMITTED(读已提交):
每次读取数据时生成新的 Read View。
事务只能看到在读取数据时已经提交的事务所做的修改。当前读 - REPEATABLE READ(可重复读):
事务开始时生成一个 Read View,并在整个事务期间一直使用该 Read View。
事务只能看到在事务开始时已经提交的事务所做的修改。
2 锁
2.1 锁的类型
共享锁和排他锁都是行级锁;MySQL当中事务采用的是粒度锁;针对表(B+树)、页(B+树叶子节点)、行(B+树叶子节点当中某一段记录行)三种粒度加锁
(一)共享锁(S)
共享锁用于事务读操作。在SERIALIZABLE隔离级别下,默认为读操作加共享锁;在REPEATABLE READ隔离级别下,需手动添加共享锁以解决幻读问题;在READ COMMITTED隔离级别下,无需共享锁,采用MVCC机制;在READ UNCOMMITTED隔离级别下,既不加锁也不使用MVCC。
(二)排他锁(X)
排他锁用于事务的删除或更新操作。在所有隔离级别下,事务提交或回滚后会释放排他锁。
(三)意向锁
意向锁分为意向共享锁(IS)和意向排他锁(IX),是表级别的锁,用于指示事务打算在表的某行上加锁的意图,为数据库管理系统提供了更高效的锁管理机制。
2.2 锁算法(自动加锁)
1 记录锁(Record Lock)
记录锁是最基本的锁类型,用于锁定单个数据行。当事务对某一行数据进行更新、删除或插入操作时,会在这行数据上加记录锁,防止其他事务同时修改同一行数据。这保证了数据在修改过程中的原子性和一致性。
2 间隙锁(Gap Lock)
间隙锁用于锁定索引之间的间隙,而不是数据行本身。它的主要目的是防止其他事务在该间隙中插入新的数据行,从而避免幻读现象。幻读是指在一个事务中,两次执行相同的查询语句,但由于其他事务的插入操作,导致第二次查询返回了更多的行。
应用场景:在 REPEATABLE READ 隔离级别下,间隙锁用于防止幻读,确保事务在多次读取相同范围的数据时能够得到一致的结果。
3 临键锁(Next-Key Lock)
临键锁是记录锁和间隙锁的结合体。它不仅锁定数据行本身,还锁定该数据行前面的间隙。临键锁主要用于 REPEATABLE READ 隔离级别,以确保事务在读取数据时的一致性和防止幻读。
作用范围:临键锁的作用范围包括数据行及其前面的间隙,这意味着它能够防止其他事务在锁定的间隙中插入新数据行,同时也能防止对同一数据行的并发修改。
id | value |
---|---|
1 | 10 |
3 | 30 |
5 | 50 |
在 REPEATABLE READ 隔离级别下,事务 A 执行以下操作: |
SELECT * FROM numbers WHERE id > 0 AND id < 6 FOR SHARE;
此时,InnoDB 会加临键锁,锁住的范围包括每行记录及其前面的间隙:
锁住的范围是:(-∞, 1], (1,3], (3,5], (5, +∞)
如果事务 B 尝试插入一条新记录:
INSERT INTO numbers (id, value) VALUES (2, 20);
会发现插入操作被阻塞,因为事务 A 加的临键锁阻止了在间隙 (1,3) 中插入新数据行。
解决幻读
未使用适当的锁
即使在 REPEATABLE READ 隔离级别下,如果你没有对查询结果集加适当的锁,也可能导致幻读
解决办法
SELECT * FROM table_name WHERE condition FOR SHARE;
3 两个重要日志
3.1 unlog
undolog 是 回滚日志(Undo Log),它记录了事务对数据库所做的更改的反向操作,目的是在事务回滚时能够将数据恢复到事务开始前的状态。它对实现事务的 原子性 和 一致性 至关重要。
undolog 的核心作用
- 支持事务回滚:当事务回滚时,undolog 中记录的操作会被反向执行,将数据恢复到事务开始前的状态。
- 提供一致性非锁定读:在多版本并发控制(MVCC)机制中,undolog 用于生成数据的旧版本,支持快照读。
undolog 的工作原理
- 记录更改前的数据:在事务对数据进行修改(如 INSERT、UPDATE、DELETE)之前,undolog 会记录下数据的原始值。
- 支持回滚操作:如果事务回滚,系统会利用 undolog 中的记录,将数据恢复到事务开始前的状态。
- 维护数据版本链:在 MVCC 中,undolog 用于生成数据的历史版本,支持其他事务进行一致性非锁定读。
流程
数据记录 --> 修改前 --> undolog记录 --> 修改后
<-- 回滚 – <-- 反向操作 –
3.2 redolog
redolog 是什么?
redolog 是 重做日志(Redo Log),它记录了事务对数据库所做的物理更改操作。redolog 的主要作用是保证事务的持久性,即事务一旦提交,其更改将被永久保存,即使在数据库崩溃的情况下也能恢复这些更改。
redolog 的核心作用
- 保证事务持久性:事务提交后,其更改会被记录到 redolog 中,即使数据库发生崩溃,也可以通过 redolog 恢复数据。
- 提高性能:redolog 先写入内存缓冲区,再定期批量写入磁盘,减少磁盘 I/O 操作,提高性能。
redolog 的工作原理
- 事务开始:事务对数据进行修改(如 INSERT、UPDATE、DELETE)时,redolog 会记录这些修改操作。
- 日志写入:redolog 先写入内存缓冲区,当事务提交时,redolog 会将缓冲区中的日志写入磁盘。
- 数据页更新:在数据库正常运行时,数据页的更新可能滞后于 redolog 的写入。如果数据库崩溃,可以通过 redolog 恢复数据页。
redolog 的存储结构 - 缓冲区(Buffer):redolog 先写入内存缓冲区,缓冲区的大小可以通过配置参数 innodb_log_buffer_size 调整。
- 日志文件(Log Files):缓冲区中的日志定期写入磁盘上的日志文件,日志文件的大小和数量可以通过配置参数 innodb_log_file_size 和 innodb_log_files_in_group 调整。
redolog 的写入策略 - 即时写入(Immediate Write):事务提交时,redolog 立即写入磁盘。
- 批量写入(Batch Write):redolog 缓冲区定期批量写入磁盘,减少磁盘 I/O 操作。
流程
事务提交 --> 数据修改 --> redolog缓冲区 --> 写入redolog文件 --> 数据页更新
数据库崩溃 --> 恢复 --> 读取redolog文件 --> 恢复数据页
redolog 与 undolog 的对比
特性 | redolog | undolog |
---|---|---|
用途 | 保证事务持久性,用于故障恢复 | 支持事务回滚和一致性非锁定读 |
记录内容 | 数据的物理更改操作,用于前滚(Redo) | 数据的原始值,用于回滚(Undo) |
存储位置 | 持久化存储在磁盘上的日志文件中 | 通常存储在内存中,事务完成后可能删除 |
对事务的影响 | 事务提交后,更改通过 redolog 持久化 | 事务回滚依赖 undolog |
3.3 综合示例
更新操作执行流程(MySQL 8.0)
- 客户端请求
客户端向 MySQL 服务器发送更新请求(如更新 ID=2 的行)。 - 连接器
管理连接:建立和管理客户端与服务器之间的连接。
权限验证:验证用户是否有权限执行请求的操作。 - 分析器
词法/语法分析:解析 SQL 语句,检查语法是否正确,并生成解析树。 - 优化器
执行计划生成:生成查询的执行计划,选择最优的索引和执行策略。 - 执行器
调用存储引擎接口:根据优化器生成的执行计划,调用存储引擎(如 InnoDB)的接口执行操作。
InnoDB 引擎层 - 写 undo log
记录事务的更改操作:在事务对数据进行修改之前,记录下数据的原始值到 undo log,以便在事务回滚时恢复数据。
支持 MVCC:通过 undo log 生成数据的旧版本,支持其他事务进行一致性非锁定读。 - 检查记录是否在内存中
是(Y):
直接更新内存中的数据:如果记录所在的 数据页 已在内存中,直接对内存中的数据进行更新操作。
否(N):
将数据页从磁盘读入内存:如果记录所在的 数据页 不在内存中,将整个数据页从磁盘读取到内存中,然后对内存中的数据进行更新操作。 - 通过 change buffer 更新记录
普通索引:对于普通索引页(非唯一索引页),如果索引页尚未在内存中,InnoDB 会使用 change buffer 来缓存对索引页的更新操作。这样可以减少磁盘 I/O 操作,提高性能。
change buffer 合并更新:change buffer 中的更新操作会定期由后台线程合并到磁盘中的对应数据页。这一过程是异步进行的,目的是进一步优化性能。 - 写 redo log
记录物理更改操作:将事务对数据的物理更改操作记录到 redo log,确保事务提交后的更改能够永久保存。
WAL(Write-Ahead Logging):写前日志技术,确保数据更改先记录到 redo log 中,再更新数据页。
刷 redo log 到磁盘:redo log 先写入内存缓冲区,事务提交时刷入磁盘。 - 写 binlog
记录逻辑操作:将事务的逻辑操作记录到 binlog,用于主从复制和数据恢复。 - 提交事务
两阶段提交:为了确保 binlog 和 redo log 的一致性,MySQL 使用两阶段提交:
prepare 阶段:将事务标记为准备提交。
commit 阶段:正式提交事务。
4 内存池
在 MySQL 的 InnoDB 存储引擎中,内存池(Buffer Pool)和 Change Buffer 是两个关键的内存管理组件,它们协同工作以优化数据库的性能。
- 内存池(Buffer Pool)
内存池是 InnoDB 用于缓存数据页和索引页的内存区域。它的主要作用是减少磁盘 I/O 操作,提高数据访问速度。内存池中可以包含以下内容: - 数据页:实际存储表数据的页面。
- 索引页:存储索引数据的页面。
其他辅助数据结构:如插入缓冲(Insert Buffer)、自适应哈希索引(Adaptive Hash Index)等。
Change Buffer
Change Buffer 是内存池中的一个专用区域,用于缓存对非唯一索引页(普通索引页)的插入、更新和删除操作。它的主要作用是减少磁盘 I/O 操作,特别是在索引页尚未加载到内存池中的情况下。
工作原理
- 数据页和索引页的缓存:
当需要访问某个数据页或索引页时,InnoDB 会首先检查该页是否在内存池中。
- 如果页已在内存池中(缓存命中),直接对内存中的页进行操作。
- 如果页不在内存池中(缓存未命中),InnoDB 会从磁盘读取整个页到内存池中,然后对内存中的页进行操作。
- Change Buffer 的缓存机制:
- 对于普通索引页(非唯一索引页),如果该索引页尚未在内存池中,InnoDB 会将对索引页的插入、更新和删除操作记录到 Change Buffer 中。
- Change Buffer 中的操作会定期由后台线程合并到磁盘中的对应数据页。这一过程是异步进行的,减少了直接磁盘 I/O 的次数。
为什么唯一索引不使用 Change Buffer
唯一索引需要保证索引值的唯一性,因此每次插入或更新唯一索引时,必须立即检查唯一性约束。如果使用 Change Buffer 缓存唯一索引的更新操作,可能会导致唯一性检查的延迟,从而引发数据不一致的问题。因此,唯一索引的更新操作不会被缓存到 Change Buffer 中,而是直接对内存池中的页进行操作,确保唯一性约束的实时性