MySQL 事务 1. 什么是事务? 事务就是“要么全做,要么全不做”的一组数据库操作,可以类比以前学过的原子操作。 打个比方:银行转账 。假设要给朋友转账 100 块钱,这在数据库里其实分两步走:
自己的账户余额减去 100 元。 朋友的账户余额加上 100 元。 如果没有事务会怎样? 万一第一步执行完了(自己扣了钱),突然停电了、断网了,第二步没执行。结果就是:自己的钱没了,朋友也没收到钱,这 100 块凭空消失了。这肯定不行!
有了事务之后: 我们把这两步打包成一个“事务”。
如果两步都成功,我们就 提交 ,转账生效。 如果中间出错了,我们就 回滚 ,数据库自动恢复到转账之前的样子,就像什么都没发生过一样。 事务有四大特性(ACID),我们只需要记住核心两点:
原子性: 像原子一样不可分割。要么全成功,要么全失败回滚。一致性: 数据库总是从一个一致状态变到另一个一致状态。转账前后,总金额是不变的。事务四大特性 ACID:
A(Atomic)原子性 :要么全做,要么全不做。C(Consistency)一致性 :操作前后数据规则不破坏。I(Isolation)隔离性 :多个事务同时做,互不影响。D(Durability)持久性 :提交后的数据,不会丢。2. 事务的版本支持 在 MySQL 中,并不是所有的存储引擎都支持事务 。最常用的两个引擎是:
InnoDB: 支持事务 (这也是 MySQL 8.0 的默认引擎,也是我们最常用的)。MyISAM: 不支持事务 (以前的老引擎,适合只读场景,现在用得少)。如何查看当前数据库支持哪些事务引擎?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 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. 事务的提交方式 事务的提交方式分为两种:
自动提交: 每写一条 SQL(比如 INSERT),数据库立马悄悄帮我们“保存”,这是默认模式。手动提交: 每写一条 SQL,必须手动输入“保存”命令,数据才会真正落盘。1. 查看事务的提交方式 输入以下命令查看当前的状态(一般默认开):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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,必须成功了一起保存。这时就要关闭自动提交。
场景 B:想恢复自动提交
设置命令: 1 SET autocommit = 1 ; # 或者写成 ON
3. 注意事项 DDL 语句会隐式提交: 即便开了事务 (autocommit = 0),只要敲了 CREATE、ALTER、DROP、TRUNCATE,MySQL 会 强制 COMMIT 自动提交 当前的事务,之前的操作全被焊死了,回滚不了 。退出连接自动提交: 崩了/断网:必然回滚(ROLLBACK);正常退出(exit):不同工具表现不同,可能提交也可能回滚。永远记住:后端开发里,没写 COMMIT 的一律当没存成功! 4. 示例 用一个 银行账户转账 的场景来演示,先创建一张表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 元。但后来发现李四给王五的转账有问题,要撤销这一步,保留第一步。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 # 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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 # 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):
1 2 3 4 5 6 7 8 9 10 # 在会话1 退出的同时或之后,立即查询 mysql> select * from test1; + | id | name | balance | + | 1 | 张三 | 900.00 | | 2 | 李四 | 2100.00 | | 3 | 王五 | 3000.00 | + # 结果:张三900 ,李四2100 ,王五3000 ,王五给张三的转账根本没生效!
重新连接后的验证(会话 1 重新登录):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 ,刚才的转账操作全部丢失!
正确的做法:
1 2 3 4 5 6 7 8 9 10 11 12 # 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
重新登录后查询,这次数据会永久保存。所以(保证数据的一致性和安全性):
Savepoint :事务中的 “存档点”,可以部分回滚。
autocommit = 0: 关闭自动提交后,必须手动 COMMIT,否则:退出 MySQL 时自动回滚、其他会话看不到之前的修改、数据不会持久化!
4. 事务隔离级别 1. 什么是事务隔离级别 隔离级别是 MySQL 控制并发事务同时操作同一数据时,是否会出现脏读、不可重复读、幻读等异常的规则,MySQL 有四种隔离级别(从宽松到严格):
隔离级别 特点 说明 READ UNCOMMITTED(读未提交) 最宽松,事务可以看到别的事务未提交的数据 脏读、不可重复读、幻读、并发性能最高 、安全性最低,基本不用 READ COMMITTED(读提交) 只能看到别的事务提交后的数据 不可重复读、幻读、高并发性能,Oracle、SQL Server 默认 ,只能读别人已提交的数据 REPEATABLE READ(可重复读) 同一事务里多次读取结果一样 幻读,中并发性能、InnoDB 默认,确保同一事务内读取数据一致 SERIALIZABLE(串行化) 最严格,事务一个个排队执行 安全性最高,性能最差。相当于“排队”执行,并发极低
隔离级别越低:并发能力通常越强、出错概率越高。
隔离级别越高:数据通常越稳、等待和锁冲突通常越多。
简记:越严格越安全,越慢;越宽松越快,可能看到脏数据!
脏读: 读到了别人“还没提交”的数据。如果别人回滚了,自己读到的就是脏数据。不可重复读: 在同一个事务里,两次读取同一条数据,结果不一样。因为中间被别人 修改并提交 了。幻读: 在同一个事务里,两次查询的数据 条数 不一样。因为中间被别人 插入或删除 了数据。2. 查看当前隔离级别 查看全局隔离级别(影响新连接):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 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 | +
查看当前会话隔离级别(只影响当前连接):
1 2 3 4 5 6 7 8 9 10 11 12 13 mysql> SELECT @@transaction_isolation ; + | @@transaction_isolation | + | REPEATABLE- READ | + mysql> SELECT @@session .transaction_isolation; + | @@session .transaction_isolation | + | REPEATABLE- READ | +
3. 设置事务隔离级别 设置当前会话(临时):
1 2 3 4 5 6 7 8 9 # SET SESSION TRANSACTION ISOLATION LEVEL 隔离级别 mysql> set session transaction isolation level READ COMMITTED; mysql> select @@global .transaction_isolation; + | @@global .transaction_isolation | + | REPEATABLE- READ | +
设置全局(影响新连接,老连接不会变):
1 2 3 4 5 6 7 8 9 10 # 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 个终端,并初始化环境:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 # 建库 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 -0 ubuntu0.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 个终端都执行:
1 2 3 4 USE tx_demo; # 设置当前会话的事务隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
终端 1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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:
1 2 3 4 5 6 7 8 9 10 11 12 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:
1 2 3 4 5 6 7 8 mysql> ROLLBACK ; # 此时终端2 : mysql> SELECT id, owner_name, balance FROM demo WHERE id = 1 ; + | id | owner_name | balance | + | 1 | Alice | 1000.00 | +
3. 读已提交 一个事务 只能 读到别的事务 已经提交 的数据,这就解决了脏读。只有别人提交了的数据,你才能看见 ,它解决不了:不可重复读 ,也就是在同一个事务里,两次查同一行,结果可能不一样。
理解:只看正式版,不看草稿;但正式版可能在查询期间被别人更新。
2 个终端都执行:
1 2 USE tx_demo; SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
终端 2 先执行:
1 2 3 4 5 6 7 8 9 10 11 12 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 执行:
1 2 3 4 5 6 7 8 9 mysql> USE tx_demo; mysql> START TRANSACTION; # A 改余额并提交 mysql> UPDATE demo - > SET balance = balance - 100.00 - > WHERE id = 1 ; mysql> COMMIT ;
回到终端 2,继续执行:
1 2 3 4 5 6 7 8 9 # B 还在自己的事务里,再查一次同一行 mysql> SELECT id, owner_name, balance - > FROM demo - > WHERE id = 1 ; + | id | owner_name | balance | + | 1 | Alice | 900.00 | +
问题来了:
Session B 的事务还没结束。但它前后两次读同一行,结果变了。 这就是 不可重复读 。
4. 可重复读 同一个事务里,多次读取同一行数据,结果要保持一致。事务一开始,数据库像拍了一张快照。只要当前事务没结束,就会一直看这张快照,不管别人怎么改、怎么提交,自己查到的数据永远是事务刚开始时的样子。开始 2 个终端都执行:
1 2 USE tx_demo; SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
终端 2 先执行:
1 2 3 4 5 6 7 8 9 10 11 12 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 执行:
1 2 3 4 5 6 7 8 9 mysql> USE tx_demo; mysql> START TRANSACTION; # A 把 Alice 余额改成 700 并提交 mysql> UPDATE demo - > SET balance = balance - 300.00 - > WHERE id = 1 ; mysql> COMMIT ;
终端 2 执行:
1 2 3 4 5 6 7 8 mysql> SELECT id, owner_name, balance - > FROM demo - > WHERE id = 1 ; + | id | owner_name | balance | + | 1 | Alice | 1000.00 | +
虽然数据库里真实最新值已经变成 700.00,但终端 2 在自己这个事务没结束前,看到的还是事务开始时那份结果,这就是 可重复读 。终端 2:
1 2 3 4 5 6 7 8 9 COMMIT ;# 事务结束后再查,就能看到最新值 mysql> SELECT id, owner_name, balance FROM demo WHERE id = 1 ; + | id | owner_name | balance | + | 1 | Alice | 700.00 | +
理论上该级别会有“幻读”问题(即查询条数不一致),但 MySQL 的 InnoDB 有两套很关键的机制:
MVCC,让普通查询常常读的是一致性快照。Next-Key Lock,在锁定读、更新范围时,会顺手把“间隙”也管起来。已经很好地解决了 大部分 幻读问题,所以课本说 RR 可能幻读,自己用 SELECT 试了半天,也不一定能轻松复现。
5. 串行化 这是最高级别,它强制大家 排队 ,只要有一个人在用这张表(读或写),其他人就得等着。性能差,等待多,锁冲突明显。2 个终端都执行:
1 2 USE tx_demo; SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
终端 1 执行:
1 2 3 4 5 6 7 8 9 10 11 12 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 执行:
1 2 3 4 5 6 7 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 特性中最综合的一个,它依赖于其他三个特性的协同工作,同时也依赖于业务逻辑:
原子性保证(要么全做,要么全不做): 如果事务执行中途出错,必须回滚到初始状态,不能留下一半的残缺数据。持久性保证: 一旦提交,数据就 永久保存 ,系统崩溃也不能丢失结果。隔离性保证: 多个事务并发执行时不乱套,不会互相干扰导致数据错乱(如脏写、脏读)。业务逻辑保证: 数据库只能管技术层面的“对错”,管不了业务层面的“逻辑”。例如,转账逻辑写成了“扣款不收款”,虽然事务提交成功了,但数据在业务层面上是不一致的。总之:一致性是原子性、持久性、隔离性加上正确业务逻辑共同作用的结果。
6. 多版本并发控制(MVCC) MySQL 事务(重点) | CSDN
读-读并发: 没问题,不需要管。写-写并发: 需要加锁(如行锁),保证数据安全。读-写并发: 这是最高频的场景。如果每次读都要加锁,性能会极差。MVCC 就是为了解决这个问题。传统做法:读的时候加锁,写的时候也加锁。读写互斥,性能差。MVCC 做法:读旧版本,写新版本。读写不互斥,性能高。其 核心思想 是每条数据都存多个 “历史版本”,读的时候,根据事务开始时间,挑一个 “能看到” 的版本读,写的时候,创建新版本,不影响别人读旧版本。
实现依赖: 隐藏字段、Undo Log(回滚日志)、Read View(读视图)。
1. 三个隐藏字段(InnoDB 每条记录都有) 虽然用 DESC 看不到,但底层真有:
字段 大小 作用 DB_TRX_ID6 字节 事务 ID 。记录创建或最近一次修改该行数据的事务 ID,是判断数据可见性的关键DB_ROW_ID6 字节 隐含主键 。如果表没有显式主键,InnoDB 会自动生成这个字段作为聚簇索引的依据DB_ROLL_PTR7 字节 回滚指针 。指向该行记录的上一个版本(存储在 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,其中包含生成时刻系统的活跃事务信息。核心字段如下:
m_ids: 生成 Read View 时,当前系统中 活跃(未提交) 的事务 ID 列表。m_up_limit_id (低水位): 活跃事务列表中最小的事务 ID。m_low_limit_id (高水位): 生成 Read View 时,系统预分配给下一个事务的 ID(即当前最大事务 ID + 1)。m_creator_trx_id: 创建该 Read View 的事务 ID。可见性判断规则(版本匹配算法): 拿着数据行版本中的 DB_TRX_ID,与 Read View 进行比对:
如果是自己改的(DB_TRX_ID == m_creator_trx_id): 可见。如果是很老的事务(DB_TRX_ID < m_up_limit_id): 说明该版本在 Read View 生成前就已经提交了,可见 。如果是未来的事务(DB_TRX_ID >= m_low_limit_id): 说明该版本是在 Read View 生成后才开启的事务修改的,不可见 。如果是在活跃列表范围内(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 级别):
1 2 3 T0: 事务 A 开始,第一次 SELECT → 创建 Read View(记录此时活跃事务) T1: 事务 B 修改并提交数据 T2: 事务 A 第二次 SELECT → 复用 T0 的 Read View → 还是看到旧数据
举个栗子(RC 级别):
1 2 3 T0: 事务 A 开始,第一次 SELECT → 创建 Read View1 T1: 事务 B 修改并提交数据 T2: 事务 A 第二次 SELECT → 创建新的 Read View2 → 看到 B 提交的新数据