事务
导学
转账操作"A 账户扣款 + B 账户收款"必须同时成功或同时失败——这就是事务的典型场景。事务是数据库保证数据一致性的核心机制,也是面试中最常被问到的数据库概念之一。
定义
事务(Transaction):一组逻辑上的数据库操作单元,这些操作要么全部成功执行并提交(Commit),要么全部不执行并回滚(Rollback)。事务具有ACID四大特性。
ACID 详解
| 特性 | 含义 | 通俗解释 |
|---|---|---|
| 原子性(Atomicity) | 事务是最小执行单元,不可再分 | 要么全做,要么全不做 |
| 一致性(Consistency) | 事务执行前后,数据库处于一致状态 | 转账前后,总余额不变 |
| 隔离性(Isolation) | 并发事务之间相互隔离 | 你的事务看不到别人未提交的数据 |
| 持久性(Durability) | 提交后数据永久保存 | 就算数据库崩溃,已提交的数据也不会丢失 |
事务控制语句
START TRANSACTION; -- 或 BEGIN
-- 执行 SQL 操作
COMMIT; -- 提交事务
ROLLBACK; -- 回滚事务
SAVEPOINT sp1; -- 设置保存点
ROLLBACK TO sp1; -- 回滚到保存点
SQL 示例
准备测试数据
CREATE TABLE employees (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20),
dept VARCHAR(20),
score DECIMAL(5,2)
);
INSERT INTO employees (emp_name, dept, score) VALUES
('大翔', '技术部', 100),
('白歌', '技术部', NULL);
CREATE TABLE logs (
log_id INT PRIMARY KEY AUTO_INCREMENT,
emp_id INT,
action VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
employees 表初始数据:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 100 |
| 2 | 白歌 | 技术部 | NULL |
logs 表初始状态:空表
场景一:事务提交演示
操作过程
-- 查看事务前的数据状态
SELECT * FROM employees;
操作前的数据状态:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 100 |
| 2 | 白歌 | 技术部 | NULL |
START TRANSACTION;
-- 1. 查询大翔的当前分数
SELECT score FROM employees WHERE emp_id = 1 FOR UPDATE;
查询结果:
| score |
|---|
| 100 |
-- 2. 大翔分数减少 50
UPDATE employees SET score = score - 50 WHERE emp_id = 1;
-- 3. 白歌分数设置为 50(直接赋值,避开 NULL 计算)
UPDATE employees SET score = 50 WHERE emp_id = 2;
-- 4. 记录操作日志
INSERT INTO logs (emp_id, action)
VALUES (1, '分数调整:大翔 -50,白歌 =50');
COMMIT;
操作后的数据状态:
SELECT * FROM employees;
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 50 |
| 2 | 白歌 | 技术部 | 50 |
SELECT * FROM logs;
| log_id | emp_id | action | created_at |
|---|---|---|---|
| 1 | 1 | 分数调整:大翔 -50,白歌 =50 | 2024-01-15 10:00:00 |
结果解读:
- 四个操作(查分数、大翔减分、白歌赋分、记日志)要么全部成功(
COMMIT),要么全部回滚(ROLLBACK) - 如果第 3 步失败,前 2 步不会留下"大翔扣了分但白歌没加上"的中间状态
FOR UPDATE锁住大翔的行,防止并发修改(详见《锁机制》文档)- 操作前后总分数保持 100(50 + 50),体现了一致性
场景二:事务回滚演示
操作过程
-- 查看初始状态
SELECT * FROM employees;
操作前的数据状态:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 50 |
| 2 | 白歌 | 技术部 | 50 |
START TRANSACTION;
-- 1. 大翔分数增加 50
UPDATE employees SET score = score + 50 WHERE emp_id = 1;
-- 2. 白歌部门调整为产品部
UPDATE employees SET dept = '产品部' WHERE emp_id = 2;
-- 此时查看中间状态(在当前事务内可见)
SELECT * FROM employees;
事务中的中间状态:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 100 |
| 2 | 白歌 | 产品部 | 50 |
-- 3. 假设发现业务错误,执行回滚
ROLLBACK;
-- 查看回滚后的状态
SELECT * FROM employees;
操作后的数据状态(回滚后):
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 50 |
| 2 | 白歌 | 技术部 | 50 |
结果解读:
ROLLBACK后,事务中的所有修改都被撤销- 大翔的分数和白歌的部门恢复到事务开始前的状态
- 这体现了事务的原子性:要么全成功,要么全失败
场景三:使用保存点
操作过程
-- 查看初始状态
SELECT * FROM employees;
SELECT * FROM logs;
操作前的数据状态:employees 表已有大翔和白歌,logs 表为空
START TRANSACTION;
-- 1. 记录第一条日志
INSERT INTO logs (emp_id, action) VALUES (1, '操作开始');
-- 查看当前日志
SELECT * FROM logs;
当前日志状态:
| log_id | emp_id | action | created_at |
|---|---|---|---|
| 1 | 1 | 操作开始 | 2024-01-15 10:00:00 |
-- 2. 设置保存点
SAVEPOINT after_start;
-- 3. 记录第二条日志(中间操作)
INSERT INTO logs (emp_id, action) VALUES (2, '中间操作');
-- 查看当前日志
SELECT * FROM logs;
当前日志状态:
| log_id | emp_id | action | created_at |
|---|---|---|---|
| 1 | 1 | 操作开始 | 2024-01-15 10:00:00 |
| 2 | 2 | 中间操作 | 2024-01-15 10:00:01 |
-- 4. 假设发现中间操作有误,回滚到保存点
ROLLBACK TO SAVEPOINT after_start;
-- 查看回滚后的日志状态
SELECT * FROM logs;
回滚后的日志状态:
| log_id | emp_id | action | created_at |
|---|---|---|---|
| 1 | 1 | 操作开始 | 2024-01-15 10:00:00 |
-- 5. 继续其他操作(第一条日志保留,中间操作被回滚)
INSERT INTO logs (emp_id, action) VALUES (1, '操作完成');
COMMIT;
-- 查看最终日志状态
SELECT * FROM logs;
最终日志状态:
| log_id | emp_id | action | created_at |
|---|---|---|---|
| 1 | 1 | 操作开始 | 2024-01-15 10:00:00 |
| 3 | 1 | 操作完成 | 2024-01-15 10:00:02 |
结果解读:
SAVEPOINT after_start在插入第一条日志后创建了一个"检查点"ROLLBACK TO SAVEPOINT after_start只回滚到保存点,第一条日志仍然保留- 这允许在事务内部进行部分回滚,而不是全部回滚
- 保存点适合复杂业务流程中的错误处理
场景四:自动提交模式
-- 查看当前自动提交模式
SELECT @@autocommit;
当前状态:
| @@autocommit |
|---|
| 1 |
结果解读:
@@autocommit = 1表示开启自动提交(MySQL 默认)- 每条 SQL 语句自动作为一个事务提交
-- 关闭自动提交
SET autocommit = 0;
SELECT @@autocommit;
操作后的状态:
| @@autocommit |
|---|
| 0 |
-- 此时每条 SQL 都在事务中,需要手动 COMMIT
UPDATE employees SET score = score + 10 WHERE emp_id = 1;
-- 查看数据(在当前会话中已修改)
SELECT * FROM employees WHERE emp_id = 1;
当前会话中的数据:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 60 |
-- 如果不 COMMIT,其他会话看不到这个修改
-- 现在执行提交
COMMIT;
-- 恢复自动提交模式
SET autocommit = 1;
结果解读:
autocommit = 0时,每个 SQL 都在一个隐式事务中,需要手动COMMIT- 显式
START TRANSACTION会临时覆盖autocommit的设置 - 事务结束后(COMMIT/ROLLBACK),
autocommit恢复为原来的值
场景五:DDL 隐式提交
-- 查看初始状态
SELECT * FROM employees;
操作前的数据状态:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 60 |
| 2 | 白歌 | 技术部 | 50 |
START TRANSACTION;
-- 执行 DML 操作
UPDATE employees SET score = score + 50 WHERE emp_id = 1;
-- 查看当前状态
SELECT * FROM employees WHERE emp_id = 1;
事务中的状态:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 110 |
-- 执行 DDL 操作(会触发隐式提交)
CREATE TABLE temp_table (id INT);
-- DDL 导致之前的 UPDATE 被自动提交!
-- 此时事务已经结束,ROLLBACK 无法回滚 UPDATE
ROLLBACK;
-- 查看最终状态(UPDATE 没有被回滚)
SELECT * FROM employees WHERE emp_id = 1;
操作后的数据状态:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 110 |
结果解读:
- DDL(
CREATE、ALTER、DROP等)会触发隐式提交,导致当前事务被提交 ROLLBACK执行时事务已经结束,所以 UPDATE 没有被回滚- 生产环境中应避免在事务中混用 DDL 和 DML
常见误区
| 误区 | 正解 |
|---|---|
| 事务越大越好 | 大事务持有锁时间长,影响并发。应在保证原子性的前提下尽量缩小事务范围。 |
ROLLBACK 可以回滚 DDL | 不可以。DDL(如 DROP TABLE)会隐式提交,无法回滚。 |
| 所有存储引擎都支持事务 | 只有 InnoDB 支持事务。MyISAM 不支持。 |
面试考点
Q:什么是 ACID?
原子性(Atomicity):事务要么全成功,要么全失败;一致性(Consistency):事务前后数据一致;隔离性(Isolation):并发事务互不干扰;持久性(Durability):提交后数据永久保存。
Q:MySQL 默认的自动提交模式是什么?
autocommit = 1,即每条 SQL 语句自动作为一个独立事务提交。显式START TRANSACTION会临时禁用自动提交,直到COMMIT或ROLLBACK。
Q:事务中可以使用 DDL 吗?
可以,但 DDL(
CREATE、ALTER、DROP等)会触发隐式提交,导致当前事务被提交,之前的操作无法回滚。
小结
- 事务是数据库保证数据一致性的核心机制
- ACID:原子性、一致性、隔离性、持久性
START TRANSACTION开始事务,COMMIT提交,ROLLBACK回滚- 默认
autocommit = 1,显式事务会临时覆盖 - DDL 会隐式提交,不可回滚
下一章引子:事务的隔离性通过隔离级别实现,不同隔离级别在并发性能和数据一致性之间做不同的权衡。