%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # 事务 Transaction > <font color="#c00000">❓什么是事务?</font> 数据库中的 "**==事务==**" 是保证 "**==数据一致性==**" 的机制,用于管理**一组 SQL 操作**的执行,使其**具有 "ACID" 特性**。 使用场景: - **银行转账操作**:必须确保**转账的两步操作(从账户 A 扣款、向账户 B 存款)要么全部成功,要么全部回滚**。 - **订单处理**:处理订单时,**必须同时更新库存和订单状态**(新增一笔订单记录,同时库存量减一) > [!NOTE] MySQL 中,InnoDB 引擎支持事务处理(支持完整 ACID 特性),MyISAM 引擎不支持事务处理。 <br><br><br> # 事务的四大特性(ACID) | | 定义说明 | | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | | **原子性**(Atomicity) | 事务中的所有操作**要么全部执行成功,要么全部回滚**,不可分割。 | | **一致性**(Consistency) | 事务执行前后,**数据库必须保持一致的状态**,**满足所有完整性约束**。即**所有数据库==约束和规则==都必须得到满足**。<br>(例如主键约束、外键约束、`NOT NULL` 约束等,事务中包含的处理必须满足这些约束,否则会出错回滚) | | **隔离性**(Isolation) | 事务的**执行是相互隔离的**,**并发事务间不会相互干扰**时,**每个事务的中间状态(在该事务结束前)对其他事务是不可见的**。<br>(保证事物之间不会互相嵌套、影响) | | **持久性**(Durability) | 事务**一旦提交**,所做的修改就会**永久保存到数据库中**,即使数据库崩溃也不会丢失。 | <br> ### MySQL 事务的实现 MySQL 中主要通过下列 4 个机制实现 **对事务的一致性支持**: - **锁**:利用**表锁**、**行锁**,**实现对数据并发修改的控制**,支持事务的==**隔离性**==。 - **Redo log**:记录**事务对数据库的所有修改**,当 MySQL 宕机崩溃时,可通过该日志恢复数据,实现 "**==持久性==**" - **Undo log**:记录**事务的反向操作**(保留数据历史版本),用于**事务回滚**,恢复事务执行前状态,实现 "**==原子性==**"。 - **MVCC**:提供无锁的 "**快照读**",实现 "**读已提交**" 与 "**可重复读**" 两种隔离级别,提供 "**==隔离性==**"。 > [!NOTE] MySQL 数据库中对 "事务" 的实现 > > - ==**原子性**==:通过 **Undo log** 实现; > - **==隔离性==**:通过 **MVCC** 和 **行级锁**(包括记录锁、间隙锁、临键锁)实现,MySQL 中提供了 4 种事务隔离级别; > - **==持久性==**:通过 **Redo Log** 实现——InnoDB 通过 **WAL (Write-Ahead Logging)** **在事务提交前先写到 Redo log**,确保数据不丢失。 > - "**一致性**" 是**通过前三个特性来共同实现的**。 > <br><br><br> # 事务的隔离级别 SQL 提供了四种隔离级别(从低到高),用于控制并发事务的影响 - (1)**==读未提交== (READ UNCOMMITTED)** - 最低的隔离级别 - 事务 a **可以读取另一个事务 b 尚未提交的数据修改** => 可能会造成 **脏读** 、**不可重复读**、**幻读**。 - (2)**==读已提交==(READ COMMITTED)** - 事务 a **可以读取另一个事务 b ==已提交==的数据** => 可防止 "脏读",但可能会出现 "**不可重复读**"、"**幻读**"。 - (3)**==可重复读==**(**REPEATABLE READ**) - MySQL **默认级别**; - 保证在**一个事务内**,**多个查询返回的结果始终一致** => 可防止不可重复读,但可能导致 "**幻读**"(对 MySQL ) - (4)**==串行化==**(**SERIALIZABLE**) - 该隔离级别下,保证并发 SQL 事务的执行相当于 "**按特定==顺序串行执行==**"——即每个 SQL 事务**在上一事务结束后才会开始**。 - **完全隔离**,**所有事务按顺序执行**,防止幻读,但性能大幅下降。 > [!NOTE] SQL 通过 "表级锁" 实现 "串行化" 隔离级别,保证数据一致性。 <br> ### 不同隔离级别下可能出现的问题 - **脏读**(Dirty Read) - 事务 A **读取了另一个事务 B ==未提交==的数据**,而这些数据可能会被事务 B 回滚,导致读取到的数据不可靠。 - **不可重复读**(Non-Repeatable Read) - 同一事务中**多次读取同一数据**,但先后**读取得到的==数据不一致==**(由于另一事务 B 的提交所导致) - **幻读**(Phantom Read) - 同一事务**在多次相同的查询中,返回不同数量的行**——另一事务 B 对查询范围内的**数据发生了增删**。 | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 备注 | | ---- | --- | ----- | --- | -------------- | | 读未提交 | ✔️ | ✔️ | ✔️ | 最低的隔离级别 | | 读已提交 | ❌ | ✔️ | ✔️ | | | 可重复读 | ❌ | ❌ | ✔️ | MySQL **默认级别** | | 串行化 | ❌ | ❌ | ❌ | | > [!info] 关于 "可重复读" 隔离级别下 "幻读" 的说明 > > - 在标准 SQL 定义下,**`REPEATABLE READ` 允许发生幻读**,在**其他数据库(如 Oracle、PostgreSQL)中均是如此**。 > - 在 MySQL 的 InnoDB 引擎中,**`REPEATABLE READ`** 下支持引入 "**==临键锁==**(Next-Key Lock)"来 **==避免幻读==**: > > > [!info] MySQL InnoDB 中在 `REPEATABLE READ` 下防止幻读的方式 > > `REPEATABLE READ` 级别下: > > - 针对 "**快照读**",即在事务里使用**普通 `SELECTE` 语句**时, **==MVCC 机制==** 保证**数据与事务启动时一致**,避免幻读问题。 > - 针对 "**当前读**",即在事务里**使用==加锁==的 `SELECT ... FOR UPDATE` 或 `SELECT ... LOCK IN SHARE MODE` 语句**时, > InnoDB 底层将基于 "**==临键锁==**" (包含**间隙锁**)提供支持,锁住查询范围,**阻塞其他事务在此范围内插入/删除数据**,从而防止幻读。 > > ![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL 事务.assets/IMG-MySQL 事务-61CC26105AEBB76C679F3D91B2644EC0.png|681]] > <br><br> ### 设置不同隔离级别 ```SQL -- 设置读未提交 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITED; -- 设置读已提交 SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 设置可重复读 (MySQL默认级别) SET TRANSACTION ISOLATION LEVEL REAPEATABLE READ; -- 设置串行化 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; ``` <br><br><br> # MySQL 事务并发控制 MySQL 提供的**事务并发控制**机制包括: 1. **锁**,参见 [[02-开发笔记/08-数据库/MySQL 相关/MySQL 锁|MySQL 锁]]。 2. InnoDB 提供的 **MVCC 机制**。 <br> ## 「快照读」与「当前读」 MySQL InnoDB 引擎中,区分 "**当前读**" 与 "**快照读**" 两种**不同读取方式**,涉及**事务隔离级别**和**一致性试图**。 - **==当前读==**(Current Read):**读取==最新==数据**,保证数据一致性。 - 执行 "**当前读**" 的操作: - (1)**==加锁==的 `SELECT` 语句**; - (2)**读未提交 & 串行化** 级别下:**普通的==无锁== `select` 语句**; - (3)**数据的增删改语句**(`INSERT`,`UPDATE`,`DELETE`)(**自动加锁**) - **==快照读==**(Snapshot Read):**基于 ==MVCC== 机制**,**无锁读取==快照==数据**(某一快照版本的数据) - 执行 "**快照读**" 的操作: - `READ COMMITTED` 与 `REPEATABLE READ` 级别下:**==未加锁==的 `SELECET` 语句** | 隔离级别 | 当前读 | 快照读 | 备注 | | ---- | ------- | ------------- | ----------------------------------------------------- | | 读未提交 | ✔️ | ❌ | 无 MVCC 机制,不存在 "**快照读**",所有查询都是 "**当前读**",故可能存在**脏读**。 | | 读已提交 | ✔️(需加锁) | ✔️(每次查询创建新快照) | 基于 MVCC 机制,无锁 `select` 执行 "**快照读**"; | | 可重复读 | ✔️(需加锁) | ✔️(事务期间快照一致) | 基于 MVCC 机制,无锁 `select` 执行 "**快照读**"; | | 串行化 | ✔️ | ❌(所有查询加锁) | 无 MVCC 机制,**所有查询自动加锁**,无锁 `select` 也执行 "**当前读**"。 | > [!summary] > > - "**==加锁==**" 一定是 "**当前读**" > - "**读未提交 or 串行化**" 级别下,"**==无锁==**" 也是 "当前读"! > > [!example] "当前读" 操作示例 > > ```SQL > -- 共享锁(S 锁):当前读,阻止其他事务修改该行 > SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; > > -- 排他锁(X 锁):当前读,阻止其他事务读取或修改该行 > SELECT * FROM users WHERE id = 1 FOR UPDATE; > > -- 更新操作(会进行当前读) > UPDATE users SET age = 30 WHERE id = 1; > > -- 删除操作(会进行当前读) > DELETE FROM users WHERE id = 1; > > -- 插入操作(隐式当前读) > INSERT INTO users(name, age) VALUES ('Alice', 25); > ``` > > > [!example] "快照读" 示意 > > ![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL 事务.assets/IMG-MySQL 事务-F3CB6A247CB3A7E34D0C15D5F3B41A71.png|586]] > > <br><br><br> # MVCC 机制 > MVCC:Multi-Version Concurrency Control,**多版本并发控制**。 MVCC 机制用于在 **`READ COMMITED` 和 `REPEATABLE READ` 隔离级别**下实现 "**==无锁==的事务并发控制**",提供 "**==快照读==**"。 其基本思想是[^1]: 1. 数据更新时会将 "**旧版本**" 写入 **==Undo Log==**; 2. 每行数据中的 "**隐藏字段**" 结合 Undo Log 形成了 "**==版本链==**"; 3. `READ COMMITED` 和 `REPEATABLE READ` 两隔离级别下,将**创建 "ReadView"**(不同时机),**维护一组事务 ID 记录**。 4. 在执行 "**快照读**" 时,MVCC 将沿着 "**版本链**" 查找返回 **"==匹配该 ReadView==" 的数据版本**(**当前表中数据** or **Undo Log 中历史版本**)。 > [!NOTE] MVCC 正是通过控制 ReadView 的 "**==创建时机==**" 来实现两个不同隔离级别 > > - `READ COMMITTED`:**==每次查询时==都会生成新的 Read View**; > - `REPEATABLE READ`(默认):**==事务开始时==生成 Read View**,在整个事务期不变; > <br> ## (1)三个基本概念 - **隐藏字段** - **Undo ==版本链==** - **Read View ==一致性视图==** ### 隐藏字段 MySQL 数据库的每行记录中,除自定义字段外,包含一些**隐藏字段**: - `db_row_id`(行 id): 当未给表创建主键时,将按该字段创建 "**聚簇索引**"; - `db_trx_id`(事务 ID):**最后==修改得到==该行记录的==事务的 ID==**。 - `db_roll_ptr`(回滚指针):指向该行记录的上一个版本,即 **Undo Log 中上一个版本的快照地址**。 ### Undo 版本链 - 事务通过 `UPDATE` 修改数据时,**旧数据将被写入 Undo Log**,用于回滚和 MVCC 版本管理。 - ""Undo Log 中记录了一行数据的 "**历史版本**"; - "**表中最新数据**" 以及 "**Undo Log 中历史版本**" 里的隐藏字段 `db_roll_ptr` 构成了 "**==版本链==**"。 > [!example] > > ![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL 事务.assets/IMG-MySQL 事务-EF2CB363CFE7E81DB2A70F92748758B1.png|534]] > > [!NOTE] Undo Log 中实际存储的是 "**写操作的==反向操作==**" > ![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL 事务.assets/IMG-MySQL 事务-1255ABFEC8E113B4C8DB8295F8281330.png|652]] <br> ### Read View 一致性视图 ReadView 本质上是**维护了一个集合**,创建时记录下列字段: - `trx_ids`:生成 ReadView 时,系统中**活跃的读写事务的事务 ID 列表**(即还未提交的) - `low_limit_id`:生成 ReadView 时,**应当分配给下一个事务的 ID**; - `up_limit_id`:生成 ReadView 时,**未提交**的事务中的**最小事务 ID**; - `creator_trx_id`:创建该 Read View 的事务 ID。 <br><br> ## (2)ReadView 快照版本查找过程 MVCC 查找 "**合适的快照版本**"时的过程,即是沿着 **==版本链==**, 用 "**表中记录" or "Undo Log 历史记录**" 中的 **`db_trx_id`字段** 依次与 **==Read View 中多个字段==** 进行比较: | | 修改该版本数据的 "事务" | 结果 | | ----------------------------------------------------------------------------------- | --------------------------- | --------------------- | | `db_trx_id == creator_trx_id` | 即为 **当前事务** | 可返回该版本 | | `db_trx_id < up_limit_id` | 该事务**已提交** | 可返回该版本 | | `db_trx_id > low_limit_id` | 该事务是 **在该 ReadView 创建后生成的** | 不可返回,沿着**版本链**查找上一旧版本 | | `up_limit_id < db_trx_id < low_limit_id` && <br>**`db_trx_id` 在 `trx_ids` 中** <br> | 该事务 "**尚未提交**" | 不可返回,沿着**版本链**查找上一旧版本 | MVCC 正是通过控制 ReadView 的 "**==创建时机==**" 来实现 `READ COMMITED` 和 `REPEATABLE READ` 两个隔离级别: - `READ COMMITTED`:**==每次查询时==都会生成新的 Read View**; - 故只要相较于 "**查询时**" 已提交,就会返回该数据版本。 - `REPEATABLE READ`(默认):**==事务开始时==生成 Read View**,在整个事务期不变; - 在**事务 A 开始后才提交**的其他事务 B,相较于 **Read View 创建时**而言均视为 "**==未提交==**"的,因此只会沿着版本链找到 "**==事务开始时==的最新数据版本**" 。 > [!NOTE] 数据版本查找过程示意 > ![[_attachment/02-开发笔记/08-数据库/MySQL 相关/MySQL 事务.assets/IMG-MySQL 事务-1573F2194BF58A11B14CBA8A593FCD74.png|733]] <br><br><br> # 事务管理命令 在 MySQL 中,事务的管理主要通过以下语句进行: | | 语句 | 说明 | | ------------- | ------------------------------- | --------------------------------- | | **开始事务** | `START TRANSACTION;` 或 `BEGIN;` | | | **==提交==事务** | `COMMIT;` | **将所有操作持久化到数据库中** | | **==回滚==事务** | `ROLLBACK;` | **取消所有操作并将数据库恢复到事务开始时的状态**。 | | **设置==保留点==** | `SAVEPOINT <保留点占位符>;` | 在事务处理块中**创建占位符**,以便实现 "**部分回滚**"。 | | **回滚至某个保留点** | `ROLLBACK TO <保留点占位符>;` | 实现部分回滚,仅撤销保留点之后的操作。 | | **查看事务状态** | `SHOW ENGINE INNODB STATUS` | 查看事务锁、死锁等信息。 | 自 `START TRANSACTION` 语句之后,直至 `COMMIT` 或 `ROLLBACK` 语句之间的部分,称为 "**==事务处理块==**"。 **执行 `COMMIT` 或 `ROLLBACK` 语句后,"事务" 会==自动关闭==**。 > [!NOTE] MySQL 默认开启"==**自动提交**==",即"单条 SQL 语句"都会作为事务并自动提交(立即生效)。 > > - 关闭自动提交: `SET AUTOCOMMIT=0;` > - 恢复自动提交:`SET AUTOCOMMIT=1;` > [!NOTE] 保留点会在事务处理完成后自动释放,也可通过 `RELEASE SAVEPOINT` 手动释放。 #### 示例 示例一:事务提交 ```sql -- 下例中, 只有当两个 `UPDATE` 操作都成功时,事务才会提交,否则可以通过 `ROLLBACK` 撤销这两个操作。 -- 标记开始事务 START TRANSACTION; -- 或者 `BEGIN;` -- 执行一系列SQL语句 UPDATE accounts SET balance = balance - 1000 WHERE account_id = 1; UPDATE accounts SET balance = balance + 1000 WHERE account_id = 2; -- 提交事务 COMMIT; ``` 示例二:事务回滚 ```sql START TRANSACTION; DELETE FROM ordertotals; -- 清空该表 SELECT * FROM ordertotals; -- SELECT 检查确实清空了表 ROLLBACK; -- 回滚, 回退START TRANSACTION之后的所有语句, 撤销DELETE操作. ``` 示例三:回滚部分事务 ```sql START TRANSACTION; -- 执行第一步操作 UPDATE account SET balance = balance - 500 WHERE account_id = 1; -- 创建一个保存点 SAVEPOINT savepoint_one; -- 执行第二步、第三步操作 UPDATE account SET balance = balance + 500 WHERE account_id = 2; UPDATE account SET balance = balance + 100 WHERE account_id = 3; -- 若发现第三部操作有问题, 可回滚到保存点, 撤销第二步、第三步的操作. ROLLBACK TO savepoint_one; -- 提交事务(仅保留第一步操作) COMMIT; ``` <br><br><br> # ♾️参考资料 # Footnotes [^1]: [MySQL 中的 MVCC 是什么? - MySQL 面试题 - 面试鸭 - 程序员求职面试刷题神器](https://www.mianshiya.com/bank/1791003439968264194/question/1780933295484203009#heading-8)