Chapter 07

事务与并发控制

ACID 特性、四种隔离级别、并发问题分析与锁机制详解

7.1 什么是事务

事务(Transaction)是一组作为整体执行的 SQL 操作集合:要么全部成功(COMMIT),要么全部回滚到初始状态(ROLLBACK),不会存在"做了一半"的中间状态。

🏦

经典示例:银行转账 Alice 向 Bob 转账 100 元,需要两步:
① 从 Alice 账户扣 100 元
② 向 Bob 账户加 100 元
如果 ① 成功但 ② 失败(网络中断、程序崩溃),钱就凭空消失了!事务保证两步要么都成功,要么 ① 也回滚,账户余额保持不变。

SQLBEGIN;   -- 或 START TRANSACTION(MySQL 两者等价)

UPDATE accounts SET balance = balance - 100
WHERE user_id = 1 AND balance >= 100;  -- 扣款,同时检查余额

-- 检查是否成功(受影响行数为 0 表示余额不足)
-- 应用程序检查 affected_rows,为 0 则 ROLLBACK

UPDATE accounts SET balance = balance + 100
WHERE user_id = 2;  -- 入账

COMMIT;   -- 所有操作成功,提交事务
-- 如果中途出错,执行 ROLLBACK; 回滚所有变更

7.2 ACID 特性

ACID 是关系型数据库事务的四个核心性质:

7.3 事务控制语句

SQL-- 开始事务
BEGIN;
START TRANSACTION;  -- MySQL 等价写法

-- 提交事务(永久保存所有变更)
COMMIT;

-- 回滚事务(撤销所有未提交的变更)
ROLLBACK;

-- SAVEPOINT:在事务中设置保存点,可部分回滚
BEGIN;
INSERT INTO orders (user_id, total_price) VALUES (1, 100);
SAVEPOINT sp1;  -- 设置保存点

INSERT INTO order_items (...) VALUES (...);  -- 如果这里失败
ROLLBACK TO sp1;  -- 只回滚到 sp1,orders 的插入保留

COMMIT;

-- 查看当前自动提交状态(MySQL 默认开启 autocommit)
SHOW VARIABLES LIKE 'autocommit';
-- 关闭 autocommit(每条语句不再自动提交,需手动 COMMIT)
SET autocommit = 0;

7.4 四种并发问题

多个事务并发运行时,如果不加以控制,会出现以下问题:

① 脏读(Dirty Read)

事务 A 读取了事务 B 尚未提交的数据。如果 B 随后回滚,A 读到的就是"脏数据"。

时间事务 A事务 B
T1UPDATE accounts SET balance = 200 WHERE id = 1;(未提交)
T2SELECT balance FROM accounts WHERE id = 1; → 读到 200
T3ROLLBACK; (余额恢复为原来的值)
T4基于 200 进行业务计算 → 错误!

② 不可重复读(Non-Repeatable Read)

同一事务内,两次读取同一行,结果不一样(另一事务在两次读取之间修改并提交了数据)。

时间事务 A事务 B
T1SELECT balance → 100
T2UPDATE ... SET balance = 200; COMMIT;
T3SELECT balance → 200(同一事务,两次读取结果不同!)

③ 幻读(Phantom Read)

同一事务内,两次执行相同的范围查询,行数不同(另一事务插入了新行并提交)。

时间事务 A事务 B
T1SELECT COUNT(*) FROM orders WHERE user_id = 1; → 5 行
T2INSERT INTO orders (user_id,...) VALUES (1,...); COMMIT;
T3SELECT COUNT(*) FROM orders WHERE user_id = 1; → 6 行(幻行出现!)

④ 丢失更新(Lost Update)

两个事务同时读取同一数据,各自修改后写回,后写入的覆盖了先写入的结果。

时间事务 A(+100)事务 B(+200)
T1读取 balance = 1000读取 balance = 1000
T2计算 1000 + 100 = 1100计算 1000 + 200 = 1200
T3写入 balance = 1100; COMMIT;
T4写入 balance = 1200; COMMIT;(A 的修改丢失!)

7.5 四种隔离级别

隔离级别脏读不可重复读幻读性能
READ UNCOMMITTED❌ 存在❌ 存在❌ 存在最高
READ COMMITTED✅ 解决❌ 存在❌ 存在
REPEATABLE READ✅ 解决✅ 解决⚠️ 部分*
SERIALIZABLE✅ 解决✅ 解决✅ 解决最低

* MySQL InnoDB 的 REPEATABLE READ 通过 MVCC + Next-Key Lock 解决了大部分幻读问题。PostgreSQL 的 REPEATABLE READ 也解决了幻读。

SQL-- 查看当前隔离级别
SELECT @@transaction_isolation;  -- MySQL
SHOW transaction_isolation;      -- MySQL
SHOW default_transaction_isolation; -- PostgreSQL

-- 设置会话级别的隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;   -- MySQL
SET default_transaction_isolation TO 'READ COMMITTED';    -- PostgreSQL

-- 为单个事务设置隔离级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- ... 操作 ...
COMMIT;

各数据库默认隔离级别

7.6 锁机制

锁的粒度

锁类型粒度并发性适用场景
表级锁整张表MyISAM、DDL 操作
行级锁单行InnoDB(默认),高并发 OLTP
间隙锁(Gap Lock)索引范围InnoDB REPEATABLE READ,防幻读

共享锁(S)与排他锁(X)

SQL-- FOR UPDATE:锁定查询到的行,防止其他事务修改(适合秒杀、库存扣减)
BEGIN;
SELECT stock FROM products
WHERE id = 42
FOR UPDATE;          -- 加排他锁,其他事务尝试修改这行会等待

-- 检查库存并扣减
UPDATE products
SET stock = stock - 1
WHERE id = 42 AND stock > 0;

COMMIT;  -- 提交后释放锁

7.7 死锁

死锁(Deadlock):事务 A 持有资源1的锁并等待资源2;事务 B 持有资源2的锁并等待资源1。两者互相等待,永远无法推进。

🔒

死锁示例
事务A:锁定 accounts 表 id=1,然后想锁 id=2
事务B:锁定 accounts 表 id=2,然后想锁 id=1
两者互相等待 → 死锁

数据库的死锁处理

InnoDB 有死锁检测机制:发现死锁时,选择一个"代价最小"的事务(通常是修改行数较少的)作为牺牲者(victim),强制回滚,并向该事务返回错误(Error Code: 1213)。应用层收到此错误后应重试事务。

如何避免死锁

SQL-- 查看当前锁等待情况(MySQL)
SELECT * FROM information_schema.INNODB_TRX;
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

-- MySQL 8.0+ 更简洁的方式
SELECT * FROM performance_schema.data_locks;

-- PostgreSQL 查看锁
SELECT * FROM pg_locks;
SELECT * FROM pg_stat_activity;