事务

事务(Transaction)是 SQL 区别于 NoSQL 的一大杀器——它保证一组操作要么全部成功、要么全部失败。没有事务,转账时 A 扣了钱但 B 没加上,数据就不一致;有了事务,这种"半成品"状态永远不会发生。金融、电商、ERP 等所有严肃业务都依赖事务。

1. 事务的基本用法

事务用三个关键字控制:BEGIN 开启、COMMIT 提交、ROLLBACK 回滚:

-- 事务(Transaction):一组操作,要么全部成功、要么全部失败
-- 经典场景:转账
-- A 减 100 元 + B 加 100 元,必须同时成功或同时失败

-- MySQL 默认 autocommit=ON,每条 SQL 自动提交
-- 显式开启事务:BEGIN 或 START TRANSACTION
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT;      -- 提交:两条同时生效
-- 或 ROLLBACK; -- 回滚:两条都撤销,回到 BEGIN 之前的状态

-- 一旦 COMMIT,数据就持久化了,无法用 ROLLBACK 撤销
-- 所以 COMMIT 前要再三确认逻辑无误

关键认知:COMMIT 之前,数据只是"暂存"在事务里——其他会话(取决于隔离级别)可能看到也可能看不到,断电就消失。一旦 COMMIT,数据就持久化了。ROLLBACK 只能在 COMMIT 之前用

2. 经典场景:银行转账

转账是事务最经典的应用。A 给 B 转 100 元,涉及两条 UPDATE,必须同时成功:

-- 完整的转账事务(带余额检查和异常处理)
START TRANSACTION;

-- 1. 检查 A 的余额是否足够
SELECT balance FROM accounts WHERE user_id = 1 FOR UPDATE;  -- 加行锁
-- 假设查到余额是 500,够转

-- 2. 扣款
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;

-- 3. 加款
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;

-- 4. 确认无误,提交
COMMIT;

-- 如果中途发现 A 余额不足(查询后判断),执行:
-- ROLLBACK;
-- 两步 UPDATE 都撤销,A 的余额不变

注意 SELECT ... FOR UPDATE——它给查到的行加排他锁,防止其他事务在转账过程中修改 A 的余额。这种"先查再改"的事务一定要加锁,否则会丢更新。

3. SAVEPOINT 部分回滚

事务默认是"全有或全无"。如果你想在事务内部分步回滚(某一步失败只回滚那步,前面的保留),就用 SAVEPOINT:

-- SAVEPOINT:在事务里设"部分回滚点"
-- 出错时可以只回滚到 SAVEPOINT,不必整个事务回滚

BEGIN;
INSERT INTO orders (user_id, amount) VALUES (1, 100);   -- 步骤 1
SAVEPOINT sp1;                                            -- 设保存点

INSERT INTO order_items (order_id, product_id) VALUES (1, 100);  -- 步骤 2
-- 假设这一步出错了

ROLLBACK TO sp1;     -- 只回滚到 sp1,步骤 1 仍然有效
-- 继续其他操作
INSERT INTO order_items (order_id, product_id) VALUES (1, 101);

COMMIT;              -- 最终提交:步骤 1 + 后续 INSERT

SAVEPOINT 像游戏里的"存档点"——你可以回到任意存档,而不必从头开始。复杂事务(比如订单创建 + 多个子任务)经常配合 SAVEPOINT,提高容错能力。

4. ACID 四大特性

事务的四大特性是面试必考、工作必懂的基础:

-- ACID 是事务的四大特性(必须背下来)

-- A: Atomicity 原子性
--    事务里的操作【不可分割】,要么全部成功,要么全部失败
--    由 undo log(回滚日志)实现

-- C: Consistency 一致性
--    事务执行前后,数据库必须处于【合法状态】
--    比如转账后 A + B 的总和必须不变,约束(主键、外键)必须满足

-- I: Isolation 隔离性
--    并发事务之间【互不干扰】
--    由锁 + MVCC(多版本并发控制)实现

-- D: Durability 持久性
--    事务 COMMIT 后,数据【永久保存】,即使断电也不丢
--    由 redo log(重做日志)实现

四个特性的实现机制各不相同:原子性靠 undo log(回滚日志);持久性靠 redo log(重做日志);隔离性靠锁 + MVCC;一致性是前三者共同保证的结果。理解了日志机制,你就能解释"为什么断电不丢数据"。

5. 四种隔离级别

多个事务并发执行时,如何平衡隔离性性能?标准 SQL 定义了 4 个隔离级别,从宽松到严格:

-- 隔离级别(Isolation Level):解决并发事务的相互干扰
-- 标准 SQL 定义 4 个级别,从宽松到严格:

-- 1. READ UNCOMMITTED 读未提交
--    能读到其他事务【未提交】的数据(脏读)
--    几乎没人用,仅供学术研究

-- 2. READ COMMITTED 读已提交(Oracle / PostgreSQL 默认)
--    只能读到其他事务【已提交】的数据
--    避免脏读,但存在【不可重复读】(同一查询两次结果不同)

-- 3. REPEATABLE READ 可重复读(MySQL 默认)
--    同一事务里多次读取结果一致
--    避免脏读、不可重复读,但理论上有【幻读】(范围查询结果变化)

-- 4. SERIALIZABLE 串行化
--    事务完全串行执行,最强隔离
--    性能最差,几乎不用

-- 查看当前隔离级别(MySQL)
SELECT @@transaction_isolation;

-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

MySQL InnoDB 默认是 REPEATABLE READ,而且通过 MVCC 解决了幻读,实际效果接近 SERIALIZABLE。PostgreSQL 和 Oracle 默认 READ COMMITTED。互联网项目大部分用 MySQL 默认级别即可。

6. 脏读 / 不可重复读 / 幻读

隔离级别要解决的三个问题,理解它们才能选对级别:

-- 并发事务的 3 种异常现象(隔离级别要解决的问题)

-- 1. 脏读(Dirty Read)
--    事务 A 读到了事务 B【未提交】的修改,B 后来回滚了,A 读到的是"假数据"
--    READ UNCOMMITTED 会出现,READ COMMITTED 及以上不会

-- 2. 不可重复读(Non-repeatable Read)
--    事务 A 里同一行【读两次】,结果不同(因为 B 提交了 UPDATE)
--    READ COMMITTED 会出现,REPEATABLE READ 及以上不会

-- 3. 幻读(Phantom Read)
--    事务 A 里同一范围查询【执行两次】,结果集行数不同(B 提交了 INSERT)
--    REPEATABLE READ 会出现(MySQL InnoDB 通过 MVCC 解决了)
--    SERIALIZABLE 不会

-- 记忆窍门:
-- 脏读最严重(读未提交) → 不可重复读(改) → 幻读(增删)

记忆窍门:严重程度从大到小——脏读(读到假数据)、不可重复读(读同一行结果变了)、幻读(范围查询行数变了)。隔离级别越高,能解决的问题越多,但性能开销也越大。

7. 锁机制

锁是事务隔离的底层实现。行级锁是 InnoDB 的默认级别,粒度细、并发好:

-- 锁:并发事务互斥访问数据的机制

-- 行级锁(最常用)
-- SELECT ... FOR UPDATE  加排他锁(写锁)
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 1 FOR UPDATE;
-- 其他事务想改这一行会被阻塞,直到当前事务 COMMIT
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
COMMIT;

-- SELECT ... LOCK IN SHARE MODE  加共享锁(读锁)
-- 多个事务可以同时读,但不能写

-- 表级锁(MySQL)
LOCK TABLES students WRITE;
UNLOCK TABLES;

-- 乐观锁 vs 悲观锁(应用层策略)
-- 悲观锁:假设会冲突,先加锁再操作(SELECT ... FOR UPDATE)
-- 乐观锁:假设不冲突,UPDATE 时检查版本号
UPDATE products SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;     -- 版本号变了就更新失败,重试

应用层有两种并发策略:悲观锁(假设会冲突,先锁再操作)适合竞争激烈场景;乐观锁(假设不冲突,UPDATE 时检查版本号)适合读多写少。死锁是并发事务的常见问题——两个事务互相等待对方释放锁。InnoDB 有自动死锁检测,会主动回滚代价小的事务。

实战要点

小结

事务是关系型数据库保证数据一致性的核心机制。掌握 ACID、BEGIN/COMMIT/ROLLBACK、隔离级别、锁机制,你就能写出可靠的金融级业务代码。下一篇我们看 SQL 的复用利器:视图

← 上一篇 索引

下一篇 视图

✈️💬