MySQL 事务
事务概述
事务(Transaction)是数据库操作的最小单元,具有 ACID 特性。
sql
-- 转账示例
START TRANSACTION;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
COMMIT; -- 提交事务
-- ROLLBACK; -- 或回滚1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
ACID 特性
| 特性 | 说明 |
|---|---|
| Atomic(原子性) | 事务是最小执行单元,不可分割 |
| Consistency(一致性) | 事务执行前后,数据库状态一致 |
| Isolation(隔离性) | 并发执行时,事务之间互不干扰 |
| Durability(持久性) | 提交后,修改永久生效 |
隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 |
| READ COMMITTED | ❌ 不可能 | ✅ 可能 | ✅ 可能 |
| REPEATABLE READ(默认) | ❌ 不可能 | ❌ 不可能 | ✅ 可能 |
| SERIALIZABLE | ❌ 不可能 | ❌ 不可能 | ❌ 不可能 |
sql
-- 查看当前隔离级别
SELECT @@tx_isolation;
-- 设置隔离级别(会话级)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置隔离级别(全局级)
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
脏读、不可重复读、幻读
脏读(Dirty Read)
事务 A 读取了事务 B 未提交的数据,
事务 B 回滚,数据不存在 → A 读到了脏数据1
2
2
不可重复读(Non-repeatable Read)
事务 A 两次读取同一行,数据不同
因为事务 B 在 A 两次读取之间修改并提交了数据1
2
2
幻读(Phantom Read)
事务 A 两次查询结果集不同
因为事务 B 在 A 两次查询之间插入了新行1
2
2
MVCC 原理
InnoDB 使用 MVCC(多版本并发控制)实现隔离:
每行数据有两个隐藏列:
- DB_TRX_ID:最后修改的事务 ID
- DB_ROLL_PTR:指向 undo log 的指针
读操作:
- READ COMMITTED:读取最新已提交版本
- REPEATABLE READ:读取事务开始时的版本
写操作:创建新版本,不影响旧版本(Snapshot Read)1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
事务语法
sql
-- 开启事务
START TRANSACTION;
-- 或
BEGIN;
-- 提交
COMMIT;
-- 回滚
ROLLBACK;
-- 设置保存点
SAVEPOINT savepoint_name;
-- 回滚到保存点
ROLLBACK TO savepoint_name;
-- 释放保存点
RELEASE SAVEPOINT savepoint_name;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
隔离级别实践
READ COMMITTED
sql
-- 窗口 1
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 1000
-- 窗口 2(在窗口1事务未提交时执行)
UPDATE accounts SET balance = 2000 WHERE id = 1;
COMMIT;
-- 窗口 1(再次读取)
SELECT balance FROM accounts WHERE id = 1; -- 2000(读到了新值)1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
REPEATABLE READ(默认)
sql
-- 窗口 1
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 1000(快照版本)
-- 窗口 2(修改并提交)
UPDATE accounts SET balance = 2000 WHERE id = 1;
COMMIT;
-- 窗口 1(再次读取)
SELECT balance FROM accounts WHERE id = 1; -- 仍然是 1000(旧版本)
-- 只有提交后,才看到新值
COMMIT;
SELECT balance FROM accounts WHERE id = 1; -- 2000(新值)1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
事务与锁
sql
-- 查看当前锁
SHOW ENGINE INNODB STATUS;
-- 查看锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 查看事务
SELECT * FROM information_schema.INNODB_TRX;
-- 死锁处理
-- InnoDB 会自动检测死锁,回滚最小的事务1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
分布式事务
sql
-- 两阶段提交(2PC)
XA START 'xa_transaction';
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
XA END 'xa_transaction';
XA PREPARE 'xa_transaction';
XA COMMIT 'xa_transaction';1
2
3
4
5
6
2
3
4
5
6
实践建议
尽量使用短事务
sql-- ❌ 长事务(锁定大量数据) BEGIN; SELECT * FROM huge_table; -- 锁定整个表 -- 处理业务... COMMIT; -- ✅ 短事务 BEGIN; SELECT * FROM huge_table WHERE id = 1; -- 只锁定一行 UPDATE huge_table SET ... WHERE id = 1; COMMIT;1
2
3
4
5
6
7
8
9
10
11批量操作分批提交
sql-- 处理 10000 条数据 -- 每 1000 条提交一次 for batch in range(10): BEGIN; UPDATE table SET status = 1 WHERE id BETWEEN batch*1000 AND (batch+1)*1000; COMMIT;1
2
3
4
5
6读写分离
sql-- 主库写 INSERT INTO orders ...; -- 从库读 SELECT * FROM orders WHERE ...;1
2
3
4
5
[[返回 MySQL 首页|../index]]