事务基础
一、事务 ACID 四大特性
1.1 原子性(Atomicity)
事务中的所有操作要么全部成功,要么全部失败回滚。
sql
-- 转账案例:A 扣钱,B 加钱,两个操作必须同时成功或同时失败
START TRANSACTION;
UPDATE account SET balance = balance - 1000 WHERE id = 1; -- A 扣钱
UPDATE account SET balance = balance + 1000 WHERE id = 2; -- B 加钱
COMMIT; -- 都成功
-- 或 ROLLBACK; -- 任何一个失败,全部回滚
1.2 一致性(Consistency)
事务执行前后,数据库从一个一致性状态转变为另一个一致性状态。
sql
-- 一致性约束:转账后总金额不变
-- 事务前:A(1000) + B(0) = 1000
-- 事务后:A(0) + B(1000) = 1000
-- 数据库层面的约束帮助保证一致性
ALTER TABLE account ADD CONSTRAINT ck_balance CHECK (balance >= 0);
1.3 隔离性(Isolation)
并发执行的事务之间互不干扰。
sql
-- 隔离级别控制一个事务对另一个事务的可见程度
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
1.4 持久性(Durability)
事务一旦提交,其结果就是永久的,即使系统崩溃也不会丢失。
sql
-- 依赖 redo log 保证持久性
-- 事务提交时,先将修改写入 redo log(磁盘),再写入数据页
COMMIT; -- 写入 redo log 后即使宕机也能恢复
二、事务操作语法
2.1 基本语法
sql
-- 方式1:显式事务
START TRANSACTION;
-- 或 BEGIN;
-- 执行 SQL 操作
UPDATE inventory SET stock = stock - 1 WHERE product_id = 100;
INSERT INTO order_log(product_id, quantity) VALUES(100, 1);
-- 提交或回滚
COMMIT; -- 确认所有操作
-- ROLLBACK; -- 撤销所有操作
-- 方式2:自动提交(默认)
-- 每一条 DML 语句都是一个独立的事务
SHOW VARIABLES LIKE 'autocommit'; -- 默认 ON
SET autocommit = 0; -- 关闭自动提交
2.2 保存点(SAVEPOINT)
sql
START TRANSACTION;
UPDATE account SET balance = balance - 1000 WHERE id = 1;
SAVEPOINT after_step1; -- 设置保存点
UPDATE account SET balance = balance + 1000 WHERE id = 2;
-- 发现第二步有问题,回滚到保存点
ROLLBACK TO SAVEPOINT after_step1;
-- 此时第一步的变更还在,可以修正后继续执行
UPDATE account SET balance = balance + 1000 WHERE id = 3;
COMMIT;
2.3 实战:完整的订单事务
sql
-- 下单事务:扣库存、生成订单、记录日志
START TRANSACTION;
-- 1. 扣减库存(带条件防止超卖)
UPDATE product
SET stock = stock - 1
WHERE id = 100 AND stock >= 1;
-- 检查受影响行数
IF ROW_COUNT() = 0 THEN
ROLLBACK;
SELECT '库存不足' AS msg;
ELSE
-- 2. 创建订单
INSERT INTO orders (user_id, product_id, amount, status)
VALUES (1, 100, 99.99, '已支付');
-- 3. 记录支付日志
INSERT INTO payment_log (order_id, amount, status)
VALUES (LAST_INSERT_ID(), 99.99, '成功');
COMMIT;
END IF;
三、四大事务隔离级别
3.1 隔离级别概览
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED(读未提交) | ✔ | ✔ | ✔ |
| READ COMMITTED(读已提交) | ✘ | ✔ | ✔ |
| REPEATABLE READ(可重复读,MySQL默认) | ✘ | ✘ | ✔ |
| SERIALIZABLE(串行化) | ✘ | ✘ | ✘ |
3.2 脏读(Dirty Read)
一个事务读到另一个事务未提交的数据。
sql
-- 事务A(未提交)
START TRANSACTION;
UPDATE account SET balance = 0 WHERE id = 1;
-- 还未提交
-- 事务B(此时读取,如果隔离级别是 READ UNCOMMITTED)
SELECT balance FROM account WHERE id = 1; -- 读到 0(脏数据)
-- 事务A 回滚
ROLLBACK;
-- 事务B 之前读到的 0 就成了脏数据
3.3 不可重复读(Non-Repeatable Read)
一个事务内两次读取同一行数据,结果不同(另一事务修改并提交了)。
sql
-- 事务A 先查询
START TRANSACTION;
SELECT balance FROM account WHERE id = 1; -- 读到 1000
-- 事务B 修改并提交
UPDATE account SET balance = 500 WHERE id = 1;
COMMIT;
-- 事务A 再次查询(READ COMMITTED 级别)
SELECT balance FROM account WHERE id = 1; -- 读到 500(两次结果不同)
COMMIT;
3.4 幻读(Phantom Read)
一个事务内两次查询同一范围,第二次多出一些行(另一事务插入了新数据)。
sql
-- 事务A 查询
START TRANSACTION;
SELECT * FROM account WHERE balance > 0; -- 查到 2 条记录
-- 事务B 插入新数据并提交
INSERT INTO account (id, balance) VALUES (3, 200);
COMMIT;
-- 事务A 再次查询(REPEATABLE READ 级别)
SELECT * FROM account WHERE balance > 0; -- 查到 3 条记录(多了一条)
COMMIT;
3.5 设置隔离级别
sql
-- 全局级别(所有新会话生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 当前会话级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 下一次事务
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 或
SHOW VARIABLES LIKE 'transaction_isolation';
四、实战案例
案例1:库存扣减防超卖
sql
-- 正确的库存扣减事务
START TRANSACTION;
-- 使用行锁 + 条件检查防止超卖
UPDATE inventory
SET stock = stock - 1
WHERE product_id = 100 AND stock > 0;
-- 检查是否扣减成功
SELECT ROW_COUNT() AS affected_rows;
-- 如果 affected_rows = 0,说明库存不足
COMMIT;
案例2:选择合适的隔离级别
sql
-- 报表统计:需要一致的快照 -> REPEATABLE READ
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
-- 统计所有订单数据(整个事务看到同一个快照)
SELECT COUNT(*) FROM orders;
SELECT SUM(amount) FROM orders;
SELECT AVG(amount) FROM orders;
COMMIT;
-- 对账系统:只需要已提交数据 -> READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- 读取所有已完成的订单(读到的都是已提交的数据)
SELECT * FROM orders WHERE status = '已完成';
COMMIT;
案例3:事务超时与死锁处理
sql
-- 设置事务锁等待超时(默认 50 秒)
SET innodb_lock_wait_timeout = 5; -- 5 秒超时
-- 场景:两个事务互相等待
-- 事务A:
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 事务B 此时更新了 id=2 的记录
UPDATE account SET balance = balance + 100 WHERE id = 2; -- 等待事务B释放id=2的锁
-- 事务B:
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 2;
-- 事务A 此时更新了 id=1 的记录
UPDATE account SET balance = balance + 100 WHERE id = 1; -- 等待事务A释放id=1的锁
-- MySQL 会自动检测死锁,回滚其中一个事务
-- 避免死锁:固定更新顺序(总是先 id=1 再 id=2)
五、常见问题
5.1 长事务的危害
sql
-- 1. 锁定资源时间过长,导致并发降低
-- 2. 产生大量 undo log,占用空间
-- 3. MVCC 需要保留旧版本,影响查询性能
-- 如何查找长事务
SELECT * FROM information_schema.INNODB_TRX
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;
5.2 事务使用最佳实践
sql
-- 1. 事务要短小,不要跨网络请求
START TRANSACTION;
UPDATE SET ...; -- 快速执行
UPDATE SET ...; -- 快速执行
COMMIT; -- 尽快提交
-- 2. 避免在事务中做耗时操作(发送HTTP请求等)
-- 3. 根据业务选择合适的隔离级别
-- 大部分业务 READ COMMITTED 即可
-- 只有需要一致性快照的场景才用 REPEATABLE READ
总结
| 概念 | 核心要点 |
|---|---|
| ACID | 原子性、一致性、隔离性、持久性 |
| 事务操作 | START TRANSACTION / COMMIT / ROLLBACK / SAVEPOINT |
| 隔离级别 | RC 防脏读、RR 防脏读+不可重复读、Serializable 全防 |
| 脏读 | 读到未提交数据 |
| 不可重复读 | 同行两次读取结果不同 |
| 幻读 | 同范围两次查询结果行数不同 |
| 实践原则 | 事务短小、快进快出、合理选隔离级别 |