16 MySQL 事务

🦄 个人主页 : 小米里的大麦-CSDN博客

🎏 所属专栏 : MySQL_小米里的大麦的博客-CSDN博客

🎁 GitHub主页 : 小米里的大麦的 GitHub

文章目录

  • [MySQL 事务](#MySQL 事务)
    • [1. 什么是事务?](#1. 什么是事务?)
    • [2. 事务的版本支持](#2. 事务的版本支持)
    • [3. 事务的提交方式](#3. 事务的提交方式)
      • [1. 查看事务的提交方式](#1. 查看事务的提交方式)
      • [2. 设置事务的提交方式](#2. 设置事务的提交方式)
      • [3. 注意事项](#3. 注意事项)
      • [4. 示例](#4. 示例)
        • [1. Savepoint 和 Rollback](#1. Savepoint 和 Rollback)
        • [2. autocommit = 0 不提交就退出](#2. autocommit = 0 不提交就退出)
    • [4. 事务隔离级别](#4. 事务隔离级别)
      • [1. 什么是事务隔离级别](#1. 什么是事务隔离级别)
      • [2. 查看当前隔离级别](#2. 查看当前隔离级别)
      • [3. 设置事务隔离级别](#3. 设置事务隔离级别)
      • [4. 展开讨论](#4. 展开讨论)
        • [1. 环境准备](#1. 环境准备)
        • [2. 读未提交](#2. 读未提交)
        • [3. 读已提交](#3. 读已提交)
        • [4. 可重复读](#4. 可重复读)
        • [5. 串行化](#5. 串行化)
        • [6. 小结](#6. 小结)
    • [5. 一致性](#5. 一致性)
    • [6. 多版本并发控制(MVCC)](#6. 多版本并发控制(MVCC))
      • [1. 三个隐藏字段(InnoDB 每条记录都有)](#1. 三个隐藏字段(InnoDB 每条记录都有))
      • [2. undo log:回滚日志](#2. undo log:回滚日志)
      • [3. 快照](#3. 快照)
      • [4. Read View:读视图](#4. Read View:读视图)
      • [5. RR 和 RC 的本质区别](#5. RR 和 RC 的本质区别)
    • 共勉

MySQL 事务

1. 什么是事务?

事务就是"要么全做,要么全不做"的一组数据库操作,可以类比以前学过的原子操作。 打个比方:银行转账。假设要给朋友转账 100 块钱,这在数据库里其实分两步走:

  1. 自己的账户余额减去 100 元。
  2. 朋友的账户余额加上 100 元。

如果没有事务会怎样? 万一第一步执行完了(自己扣了钱),突然停电了、断网了,第二步没执行。结果就是:自己的钱没了,朋友也没收到钱,这 100 块凭空消失了。这肯定不行!

有了事务之后: 我们把这两步打包成一个"事务"。

  • 如果两步都成功,我们就 提交,转账生效。
  • 如果中间出错了,我们就 回滚,数据库自动恢复到转账之前的样子,就像什么都没发生过一样。

事务有四大特性(ACID),我们只需要记住核心两点:

  1. 原子性: 像原子一样不可分割。要么全成功,要么全失败回滚。
  2. 一致性: 数据库总是从一个一致状态变到另一个一致状态。转账前后,总金额是不变的。

事务四大特性 ACID:

  • A(Atomic)原子性:要么全做,要么全不做。
  • C(Consistency)一致性:操作前后数据规则不破坏。
  • I(Isolation)隔离性:多个事务同时做,互不影响。
  • D(Durability)持久性:提交后的数据,不会丢。

2. 事务的版本支持

在 MySQL 中,并不是所有的存储引擎都支持事务。最常用的两个引擎是:

  • InnoDB: 支持事务(这也是 MySQL 8.0 的默认引擎,也是我们最常用的)。
  • MyISAM: 不支持事务(以前的老引擎,适合只读场景,现在用得少)。

如何查看当前数据库支持哪些事务引擎?

sql 复制代码
mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                        | Transactions | XA   | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+

mysql> show engines\G
*************************** 1. row ***************************
      Engine: ARCHIVE
     Support: YES
     Comment: Archive storage engine
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 2. row ***************************
      Engine: BLACKHOLE
     Support: YES
     Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 3. row ***************************
      Engine: MRG_MYISAM
     Support: YES
     Comment: Collection of identical MyISAM tables
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 4. row ***************************
      Engine: FEDERATED
     Support: NO
     Comment: Federated MySQL storage engine
Transactions: NULL
          XA: NULL
  Savepoints: NULL
*************************** 5. row ***************************
      Engine: MyISAM
     Support: YES
     Comment: MyISAM storage engine
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 6. row ***************************
      Engine: PERFORMANCE_SCHEMA
     Support: YES
     Comment: Performance Schema
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 7. row ***************************
      Engine: InnoDB
     Support: DEFAULT
     Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES
          XA: YES
  Savepoints: YES
*************************** 8. row ***************************
      Engine: MEMORY
     Support: YES
     Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
          XA: NO
  Savepoints: NO
*************************** 9. row ***************************
      Engine: CSV
     Support: YES
     Comment: CSV storage engine
Transactions: NO
          XA: NO
  Savepoints: NO

真正能看事务支持的,是 Transactions 列,它显示 YES/NO/NULL。在上面输出里:

  • InnoDB → Transactions: YES → 支持事务
  • 其他大部分 → Transactions: NO → 不支持事务
  • FEDERATED → Transactions: NULL → 不可用

总之:Transactions 列 YES = 支持事务,NO/NULL = 不支持。

其他字段含义(了解):

  • Engine: 引擎名。
  • Support: 这引擎能不能用。YES 能用,DEFAULT 默认用它,NO 不能用。
  • Comment: 备注,简单说说这引擎干嘛的。
  • Transactions: 支不支持事务。YES 支持,NO 不支持。
  • XA: 支不支持分布式事务。一般开发不用管。
  • Savepoints: 支不支持保存点。事务里能回滚到中间某一步。

3. 事务的提交方式

事务的提交方式分为两种:

  1. 自动提交: 每写一条 SQL(比如 INSERT),数据库立马悄悄帮我们"保存",这是默认模式。
  2. 手动提交: 每写一条 SQL,必须手动输入"保存"命令,数据才会真正落盘。

1. 查看事务的提交方式

输入以下命令查看当前的状态(一般默认开):

sql 复制代码
SHOW VARIABLES LIKE 'autocommit';	# 返回 ON 就是自动提交开,OFF 就是关
SELECT @@autocommit;	# 返回 1 就是自动提交开,0 是关

mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+

mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
|            1 |
+--------------+

结果解读:

  • Value 为 ON :表示开启了 自动提交。每一条 SQL 语句都被当成一个独立的事务,执行完立刻生效。
  • Value 为 OFF :表示关闭了自动提交。你必须显式地写 COMMIT; 才能保存数据。

2. 设置事务的提交方式

场景 A:想临时关闭自动提交(手动事务模式)

有时候我们需要一次性执行多条 SQL,必须成功了一起保存。这时就要关闭自动提交。

  • 设置命令:

    sql 复制代码
    SET autocommit = 0;  # 或者写成 OFF
    # START TRANSACTION;  # 开始事务

    执行完这条命令后,当前连接的窗口就进入了"手动模式"。

  • 操作流程(经典三部曲):

    sql 复制代码
    # 1. 显式开启事务(虽然关闭了autocommit后默认就在事务中,但为了规范,建议写上)
    START TRANSACTION; 
    
    # 2. 执行自己的 SQL 语句
    UPDATE account SET balance = balance - 100 WHERE name = '我';
    UPDATE account SET balance = balance + 100 WHERE name = '朋友';
    
    # 3. 确认没问题,手动提交(事务)
    COMMIT; 
    
    # 如果中间发现问题,想反悔,可以用:
    # ROLLBACK; 	# 回滚事务

场景 B:想恢复自动提交

  • 设置命令:

    sql 复制代码
    SET autocommit = 1;  # 或者写成 ON

3. 注意事项

  • DDL 语句会隐式提交: 即便开了事务 (autocommit = 0),只要敲了 CREATE、ALTER、DROP、TRUNCATE,MySQL 会 强制 COMMIT 自动提交 当前的事务,之前的操作全被焊死了,回滚不了。
  • 退出连接自动提交: 崩了/断网:必然回滚(ROLLBACK);正常退出(exit):不同工具表现不同,可能提交也可能回滚。永远记住:后端开发里,没写 COMMIT 的一律当没存成功!

4. 示例

用一个 银行账户转账 的场景来演示,先创建一张表:

sql 复制代码
mysql> create database transactions;
mysql> use transactions;

mysql> create table test1 (
    ->     id int primary key,
    ->     name varchar(20),
    ->     balance decimal(10,2)
    -> );
    
mysql> insert into test1 values 
    -> (1, '张三', 1000),
    -> (2, '李四', 2000),
    -> (3, '王五', 3000);
Query OK, 3 rows affected (0.03 sec)
Records: 3  Duplicates: 0  Warnings: 0
1. Savepoint 和 Rollback

场景:张三给李四转账 100 元,然后李四给王五转账 200 元。但后来发现李四给王五的转账有问题,要撤销这一步,保留第一步。

sql 复制代码
# 1. 开启事务
mysql> start transaction;

# 2. 张三给李四转账100元
mysql> update test1 set balance = balance - 100 where id = 1;
mysql> update test1 set balance = balance + 100 where id = 2;

# 3. 打个保存点(存档)
mysql> savepoint step1;

# 4. 查看当前状态
mysql> select * from test1;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 2100.00 |
|  3 | 王五   | 3000.00 |
+----+--------+---------+
# 结果:张三900,李四2100,王五3000

# 5. 李四给王五转账200元
mysql> update test1 set balance = balance - 200 where id = 2;
mysql> update test1 set balance = balance + 200 where id = 3;

# 6. 再次查看
mysql> select * from test1;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 1900.00 |
|  3 | 王五   | 3200.00 |
+----+--------+---------+
# 结果:张三900,李四1900,王五3200

# 7. 发现问题,回滚到保存点(撤销李四给王五的转账)
mysql> rollback to step1;

# 8. 查看回滚后的状态
select * from test1;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 2100.00 |
|  3 | 王五   | 3000.00 |
+----+--------+---------+
# 结果:张三900,李四2100,王五3000(李四给王五的转账被撤销了)

# 9. 最后提交事务
mysql> commit;

# 10. 最终确认
mysql> select * from test1;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 2100.00 |
|  3 | 王五   | 3000.00 |
+----+--------+---------+
# 结果:张三900,李四2100,王五3000
2. autocommit = 0 不提交就退出

继续上面的账户表,演示关闭自动提交后,不 commit 就退出的后果。第一个终端窗口(会话 1):

sql 复制代码
# 1. 关闭自动提交
mysql> set autocommit=0;

# 2. 确认已关闭
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | OFF   |
+---------------+-------+
# 结果:Value = OFF

# 3. 查看当前数据
mysql> select * from test1;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 2100.00 |
|  3 | 王五   | 3000.00 |
+----+--------+---------+
# 结果:张三900,李四2100,王五3000

# 4. 王五给张三转账500元
mysql> update test1 set balance=balance-500 where id=3;
mysql> update test1 set balance=balance+500 where id=1;

# 5. 查看当前会话的结果
mysql> select * from test1;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   | 1400.00 |
|  2 | 李四   | 2100.00 |
|  3 | 王五   | 2500.00 |
+----+--------+---------+
# 结果:张三1400,李四2100,王五2500
# 注意:这个变化只有当前会话能看到

# 6. 【关键】不执行 COMMIT,直接退出
mysql> quit
Bye

第二个终端窗口(会话 2):

sql 复制代码
# 在会话1退出的同时或之后,立即查询
mysql> select * from test1;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 2100.00 |
|  3 | 王五   | 3000.00 |
+----+--------+---------+
# 结果:张三900,李四2100,王五3000,王五给张三的转账根本没生效!

重新连接后的验证(会话 1 重新登录):

sql 复制代码
-- 重新登录 MySQL
mysql -u root -p
mysql> use transactions;

# 查询数据
mysql> select * from test1;
+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | 张三   |  900.00 |
|  2 | 李四   | 2100.00 |
|  3 | 王五   | 3000.00 |
+----+--------+---------+
# 结果:张三900,李四2100,王五3000,刚才的转账操作全部丢失!

正确的做法:

sql 复制代码
# 1. 关闭自动提交
mysql> set autocommit=0;

# 2. 执行转账
mysql> update test1 set balance=balance-500 where id=3;
mysql> update test1 set balance=balance+500 where id=1;

# 3. 【关键】提交事务
mysql> commit;

# 4. 退出
mysql> quit

重新登录后查询,这次数据会永久保存。所以(保证数据的一致性和安全性):

  1. Savepoint:事务中的 "存档点",可以部分回滚。

  2. autocommit = 0: 关闭自动提交后,必须手动 COMMIT,否则:退出 MySQL 时自动回滚、其他会话看不到之前的修改、数据不会持久化!

4. 事务隔离级别

1. 什么是事务隔离级别

隔离级别是 MySQL 控制并发事务同时操作同一数据时,是否会出现脏读、不可重复读、幻读等异常的规则,MySQL 有四种隔离级别(从宽松到严格):

隔离级别 特点 说明
READ UNCOMMITTED(读未提交) 最宽松,事务可以看到别的事务未提交的数据 脏读、不可重复读、幻读、并发性能最高、安全性最低,基本不用
READ COMMITTED(读提交) 只能看到别的事务提交后的数据 不可重复读、幻读、高并发性能,Oracle、SQL Server 默认,只能读别人已提交的数据
REPEATABLE READ(可重复读) 同一事务里多次读取结果一样 幻读,中并发性能、InnoDB 默认,确保同一事务内读取数据一致
SERIALIZABLE(串行化) 最严格,事务一个个排队执行 安全性最高,性能最差。相当于"排队"执行,并发极低
  1. 隔离级别越低:并发能力通常越强、出错概率越高。

  2. 隔离级别越高:数据通常越稳、等待和锁冲突通常越多。

简记:越严格越安全,越慢;越宽松越快,可能看到脏数据!

  • 脏读: 读到了别人"还没提交"的数据。如果别人回滚了,自己读到的就是脏数据。
  • 不可重复读: 在同一个事务里,两次读取同一条数据,结果不一样。因为中间被别人 修改并提交 了。
  • 幻读: 在同一个事务里,两次查询的数据 条数 不一样。因为中间被别人 插入或删除 了数据。

2. 查看当前隔离级别

查看全局隔离级别(影响新连接):

sql 复制代码
# MySQL 8.0+ 新写法
mysql> SELECT @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ                |
+--------------------------------+

mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+

查看当前会话隔离级别(只影响当前连接):

sql 复制代码
mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+

mysql> SELECT @@session.transaction_isolation;
+---------------------------------+
| @@session.transaction_isolation |
+---------------------------------+
| REPEATABLE-READ                 |
+---------------------------------+

3. 设置事务隔离级别

设置当前会话(临时):

sql 复制代码
# SET SESSION TRANSACTION ISOLATION LEVEL 隔离级别
mysql> set session transaction isolation level READ COMMITTED;

mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ                |
+--------------------------------+

设置全局(影响新连接,老连接不会变):

sql 复制代码
# SET GLOBAL TRANSACTION ISOLATION LEVEL 隔离级别;
mysql> set global transaction isolation level read committed;

# 验证:需要新开一个窗口查看
mysql> SELECT @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-COMMITTED                 |
+--------------------------------+

设置全局隔离级别会影响后续的新会话,当前会话的隔离级别不会发生变化,要让当前会话的隔离级别也改变,需要重启会话。

4. 展开讨论

1. 环境准备

先开 2 个终端,并初始化环境:

sql 复制代码
# 建库
mysql> CREATE DATABASE IF NOT EXISTS tx_demo;
mysql> USE tx_demo;

# InnoDB 是 MySQL 8 默认事务引擎,这里写出来更直观
mysql> CREATE TABLE demo (
    ->     id BIGINT PRIMARY KEY,
    ->     owner_name VARCHAR(20) NOT NULL,
    ->     balance DECIMAL(10,2) NOT NULL,
    ->     KEY idx_balance (balance)
    -> ) ENGINE=InnoDB;

# 初始数据
mysql> INSERT INTO demo (id, owner_name, balance) VALUES
    ->     (1, 'Alice', 1000.00),
    ->     (2, 'Bob', 1000.00);
    
# 可选:查看mysql版本
mysql> SELECT VERSION();
+-------------------------+
| VERSION()               |
+-------------------------+
| 8.0.45-0ubuntu0.24.04.1 |
+-------------------------+

# 初始实验数据
mysql> UPDATE demo
    -> SET balance = CASE id
    ->     WHEN 1 THEN 1000.00
    ->     WHEN 2 THEN 1000.00
    -> END
    -> WHERE id IN (1, 2);
    
mysql> COMMIT;    

2. 读未提交

一个事务可以读取到其他事务 尚未提交 的数据,但是会 脏读,也就是说,即使是看到的数据,可能也不会真正落库。

理解: 好比自己写作业,刚写了个草稿还没定稿呢,就被同桌偷看到了,后来自己把草稿擦了,同桌抄的答案全是错的。同桌读到了自己最后回滚了(撤销了)的数据,这就是"脏数据"。

先在 2 个终端都执行:

sql 复制代码
USE tx_demo;

# 设置当前会话的事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

终端 1:

sql 复制代码
mysql> USE tx_demo;
mysql> START TRANSACTION;

# Alice 扣掉 200 元,但先不提交
mysql> UPDATE demo
    -> SET balance = balance - 200.00
    -> WHERE id = 1;

# A 自己能看到 800
mysql> SELECT id, owner_name, balance
    -> FROM demo
    -> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
|  1 | Alice      |  800.00 |
+----+------------+---------+

终端 2:

sql 复制代码
mysql> USE tx_demo;
mysql> USE tx_demo;
# 按理说 A 还没提交,B 不该看到 800
# 但在 READ UNCOMMITTED 下,B 就是能看到
mysql> SELECT id, owner_name, balance
    -> FROM demo
    -> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
|  1 | Alice      |  800.00 |
+----+------------+---------+

紧接着终端 1:

sql 复制代码
mysql> ROLLBACK;
# 此时终端2:
mysql> SELECT id, owner_name, balance FROM demo WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
|  1 | Alice      | 1000.00 |
+----+------------+---------+
3. 读已提交

一个事务 只能 读到别的事务 已经提交 的数据,这就解决了脏读。只有别人提交了的数据,你才能看见 ,它解决不了:不可重复读,也就是在同一个事务里,两次查同一行,结果可能不一样。

理解:只看正式版,不看草稿;但正式版可能在查询期间被别人更新。

2 个终端都执行:

sql 复制代码
USE tx_demo;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

终端 2 先执行:

sql 复制代码
mysql> USE tx_demo;
mysql> START TRANSACTION;

# 第一次查 Alice 的余额
mysql> SELECT id, owner_name, balance
    -> FROM demo
    -> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
|  1 | Alice      | 1000.00 |
+----+------------+---------+

终端 1 执行:

sql 复制代码
mysql> USE tx_demo;
mysql> START TRANSACTION;

# A 改余额并提交
mysql> UPDATE demo
    -> SET balance = balance - 100.00
    -> WHERE id = 1;

mysql> COMMIT;

回到终端 2,继续执行:

sql 复制代码
# B 还在自己的事务里,再查一次同一行
mysql> SELECT id, owner_name, balance
    -> FROM demo
    -> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
|  1 | Alice      |  900.00 |
+----+------------+---------+

问题来了:

  1. Session B 的事务还没结束。
  2. 但它前后两次读同一行,结果变了。

这就是 不可重复读。

4. 可重复读

同一个事务里,多次读取同一行数据,结果要保持一致。事务一开始,数据库像拍了一张快照。只要当前事务没结束,就会一直看这张快照,不管别人怎么改、怎么提交,自己查到的数据永远是事务刚开始时的样子。开始 2 个终端都执行:

sql 复制代码
USE tx_demo;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

终端 2 先执行:

sql 复制代码
mysql> USE tx_demo;
mysql> START TRANSACTION;

# 第一次查
mysql> SELECT id, owner_name, balance
    -> FROM demo
    -> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
|  1 | Alice      | 1000.00 |
+----+------------+---------+

终端 1 执行:

sql 复制代码
mysql> USE tx_demo;
mysql> START TRANSACTION;

# A 把 Alice 余额改成 700 并提交
mysql> UPDATE demo
    -> SET balance = balance - 300.00
    -> WHERE id = 1;

mysql> COMMIT;

终端 2 执行:

sql 复制代码
mysql> SELECT id, owner_name, balance
    -> FROM demo
    -> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
|  1 | Alice      | 1000.00 |
+----+------------+---------+

虽然数据库里真实最新值已经变成 700.00,但终端 2 在自己这个事务没结束前,看到的还是事务开始时那份结果,这就是 可重复读 。终端 2:

sql 复制代码
COMMIT;

# 事务结束后再查,就能看到最新值
mysql> SELECT id, owner_name, balance FROM demo WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
|  1 | Alice      |  700.00 |
+----+------------+---------+

理论上该级别会有"幻读"问题(即查询条数不一致),但 MySQL 的 InnoDB 有两套很关键的机制:

  1. MVCC,让普通查询常常读的是一致性快照。
  2. Next-Key Lock,在锁定读、更新范围时,会顺手把"间隙"也管起来。

已经很好地解决了 大部分 幻读问题,所以课本说 RR 可能幻读,自己用 SELECT 试了半天,也不一定能轻松复现。

5. 串行化

这是最高级别,它强制大家 排队 ,只要有一个人在用这张表(读或写),其他人就得等着。性能差,等待多,锁冲突明显。2 个终端都执行:

sql 复制代码
USE tx_demo;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

终端 1 执行:

sql 复制代码
mysql> USE tx_demo;
mysql> START TRANSACTION;

# A 先查一把
mysql> SELECT id, owner_name, balance
    -> FROM demo
    -> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
|  1 | Alice      |  600.00 |
+----+------------+---------+

这时先别提交,然后终端 2 执行:

sql 复制代码
mysql> USE tx_demo;
mysql> START TRANSACTION;

# B 尝试修改同一行
mysql> UPDATE demo
    -> SET balance = balance - 50.00
    -> WHERE id = 1;

这时会发现:终端 2 卡住了,在等 ,只有当终端 1 执行 COMMIT; 或 ROLLBACK;,终端 2 才会继续。

6. 小结
隔离级别 英文名称 脏读 不可重复读 幻读 性能 常用场景
读未提交 Read Uncommitted ✅ ✅ ✅ 极快 几乎不用
读已提交 Read Committed ❌ ✅ ✅ 快 Oracle/SQL Server 默认
可重复读 Repeatable Read ❌ ❌ ❌ (基本解决) 中 MySQL 默认,推荐
串行化 Serializable ❌ ❌ ❌ 极慢 极度严格的数据一致性要求

5. 一致性

一致性就是:事务执行前后,数据库得是 "合法" 的,如何保证?

一致性是事务 ACID 特性中最综合的一个,它依赖于其他三个特性的协同工作,同时也依赖于业务逻辑:

  1. 原子性保证(要么全做,要么全不做): 如果事务执行中途出错,必须回滚到初始状态,不能留下一半的残缺数据。
  2. 持久性保证: 一旦提交,数据就 永久保存,系统崩溃也不能丢失结果。
  3. 隔离性保证: 多个事务并发执行时不乱套,不会互相干扰导致数据错乱(如脏写、脏读)。
  4. 业务逻辑保证: 数据库只能管技术层面的"对错",管不了业务层面的"逻辑"。例如,转账逻辑写成了"扣款不收款",虽然事务提交成功了,但数据在业务层面上是不一致的。

总之:一致性是原子性、持久性、隔离性加上正确业务逻辑共同作用的结果。


6. 多版本并发控制(MVCC)

MySQL 事务(重点) | CSDN

  • 读-读并发: 没问题,不需要管。
  • 写-写并发: 需要加锁(如行锁),保证数据安全。
  • 读-写并发: 这是最高频的场景。如果每次读都要加锁,性能会极差。MVCC 就是为了解决这个问题。

传统做法:读的时候加锁,写的时候也加锁。读写互斥,性能差。MVCC 做法:读旧版本,写新版本。读写不互斥,性能高。其 核心思想 是每条数据都存多个 "历史版本",读的时候,根据事务开始时间,挑一个 "能看到" 的版本读,写的时候,创建新版本,不影响别人读旧版本。

实现依赖: 隐藏字段、Undo Log(回滚日志)、Read View(读视图)。

1. 三个隐藏字段(InnoDB 每条记录都有)

虽然用 DESC 看不到,但底层真有:

字段 大小 作用
DB_TRX_ID 6 字节 事务 ID。记录创建或最近一次修改该行数据的事务 ID,是判断数据可见性的关键
DB_ROW_ID 6 字节 隐含主键。如果表没有显式主键,InnoDB 会自动生成这个字段作为聚簇索引的依据
DB_ROLL_PTR 7 字节 回滚指针。指向该行记录的上一个版本(存储在 Undo Log 中),通过这个指针能把数据的不同版本串起来

2. undo log:回滚日志

MySQL 三大日志:

  • redo log:崩溃恢复用,保证持久性。
  • bin log:主从同步用,保证一致性。
  • undo log:回滚用,保证原子性 + 隔离性(MVCC)历史版本。

写操作前会把当前数据拷贝到 undo log,这个副本就构成快照,那么版本链啥时候清理?

  • 新插入的记录,提交后就能清(没人会读它的历史)
  • 修改/删除的记录,要等 所有可能读它历史版本的事务都结束 才能清
  • 所以热点数据的 undo log 可能留很久,正常现象

3. 快照

Undo Log 中串联起来的历史数据链表,就是 版本链 。链表中的每一个节点,都是一个 快照。最新数据: 在缓冲池/磁盘的主索引上,历史快照: 在 Undo Log 链表中。

当前读 vs 快照读:

类型 触发场景 加锁吗 读哪个版本
当前读 UPDATE/DELETE/SELECT ... FOR UPDATE 加锁 最新版本
快照读 普通 SELECT 不加锁 历史版本(根据 Read View 判断)

4. Read View:读视图

Read View 是事务进行 快照读 时的"裁判",它决定了当前事务能看到版本链中的哪一个版本。当一个事务执行快照读时,会生成一个 Read View,其中包含生成时刻系统的活跃事务信息。核心字段如下:

  1. m_ids: 生成 Read View 时,当前系统中 活跃(未提交) 的事务 ID 列表。
  2. m_up_limit_id (低水位): 活跃事务列表中最小的事务 ID。
  3. m_low_limit_id (高水位): 生成 Read View 时,系统预分配给下一个事务的 ID(即当前最大事务 ID + 1)。
  4. m_creator_trx_id: 创建该 Read View 的事务 ID。

可见性判断规则(版本匹配算法): 拿着数据行版本中的 DB_TRX_ID,与 Read View 进行比对:

  1. 如果是自己改的(DB_TRX_ID == m_creator_trx_id): 可见。
  2. 如果是很老的事务(DB_TRX_ID < m_up_limit_id): 说明该版本在 Read View 生成前就已经提交了,可见。
  3. 如果是未来的事务(DB_TRX_ID >= m_low_limit_id): 说明该版本是在 Read View 生成后才开启的事务修改的,不可见。
  4. 如果是在活跃列表范围内(m_up_limit_id <= DB_TRX_ID < m_low_limit_id):
    • 再次判断 DB_TRX_ID 是否在 m_ids 列表中:
      • 在列表中: 说明生成 Read View 时该事务还未提交(活跃),不可见。
      • 不在列表中: 说明生成 Read View 时该事务已经提交了,可见。

如果不满足可见性怎么办? 根据 DB_ROLL_PTR 回滚指针,去找 Undo Log 中的下一个旧版本,再次进行上述判断,直到找到可见版本或链表结束。

5. RR 和 RC 的本质区别

现象:

  • RC(读提交):同一个事务里,两次 SELECT 可能读到不同结果(别人提交了就能看见)
  • RR(可重复读):同一个事务里,两次 SELECT 结果一样(不管别人提没提交)

本质原因:Read View 创建时机不同!

隔离级别 Read View 创建时机 结果
RC 在每条 SELECT 语句开始时生成新的 Read View 每次都能看见最新已提交的数据 → 不可重复读
RR 仅第一次快照读创建 Read View,后面复用 一直看见第一次读时的快照 → 可重复读

举个栗子(RR 级别):

text 复制代码
T0: 事务 A 开始,第一次 SELECT → 创建 Read View(记录此时活跃事务)
T1: 事务 B 修改并提交数据
T2: 事务 A 第二次 SELECT → 复用 T0 的 Read View → 还是看到旧数据

举个栗子(RC 级别):

text 复制代码
T0: 事务 A 开始,第一次 SELECT → 创建 Read View1
T1: 事务 B 修改并提交数据
T2: 事务 A 第二次 SELECT → 创建新的 Read View2 → 看到 B 提交的新数据

共勉

相关推荐
西柚小萌新3 小时前
【LLM&&AI应用开发 八股文】--4.3.Agent智能体(下)
java·开发语言·数据库
Titan20244 小时前
MySQL访问个人学习笔记
笔记·学习·mysql
邓工说电5 小时前
智慧断路器安全吗?数据加密、离线保护与合规认证全解读
大数据·数据库·人工智能·智能断路器·炜晔科技
for_ever_love__5 小时前
MySQL 全文索引实战:FULLTEXT、ngram 中文分词与 MATCH AGAINST 到底该怎么用
java·python·mysql·全文检索·分词·索引·ngram
adinnet20265 小时前
企业微调大模型(Fine-tuning):先吃透 RAG 还是先训自己的模型
大数据·数据库
上位机妹子6 小时前
EF Core 两种继承映射:每层次一张表(TPH)与每类型一张表(TPT)
数据库·c#·.netcore·ef core
小马同学-6 小时前
Redis消息队列与客户端编程
数据库·redis
for_ever_love__6 小时前
MySQL 高频面试题 30 问:索引、事务、锁、日志、主从与分库分表(MySQL 8.0 版)
java·数据库·spring boot·mysql·spring cloud·面试·总结