一、什么是事务
事务(Transaction)是一组 SQL 操作的集合,这些操作要么全部成功执行 ,要么全部回滚,不允许只执行一半。经典例子就是银行转账------A 扣钱和 B 加钱必须同时成功或同时失败。

二、ACID 四大特性
这是事务最核心的概念,面试必考。

三、事务控制语句
sql
-- 方式一:手动控制
START TRANSACTION; -- 或者 BEGIN
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT; -- 提交,所有操作永久生效
-- ROLLBACK; -- 回滚,所有操作撤销
-- 方式二:查看/关闭自动提交
SELECT @@autocommit; -- 默认为 1(开启)
SET autocommit = 0; -- 关闭自动提交,需手动 COMMIT
**保存点(SAVEPOINT)**允许部分回滚,不必回到事务起点:
START TRANSACTION;
INSERT INTO orders VALUES (1, 'A');
SAVEPOINT sp1; -- 设置保存点
INSERT INTO orders VALUES (2, 'B');
ROLLBACK TO SAVEPOINT sp1; -- 只回滚到 sp1,第一条 INSERT 保留
COMMIT;
回滚 = 撤销本次事务里已经执行过的所有 SQL,让数据回到事务开始前的样子。
就像你写了一半的字,发现写错了,直接橡皮擦全部擦掉,跟没写过一样。
四、并发问题
多个事务并发执行时,如果不加控制,会出现以下三类经典问题:
sql

不可重复读和幻读的区别:前者是同一行数据两次读到不同的值 ,后者是同一查询条件两次读到不同数量的行。

| 问题 | 核心描述 |
|---|---|
| 脏读 | 读了别人没提交的(可能回滚的)数据 |
| 不可重复读 | 同一事务内,同一条数据前后读到的值不一样(被改并提交了) |
| 幻读 | 同一事务内,同一查询条件前后查到的行数不一样(被插入或删除了) |
1. 脏读(Dirty Read)

定义 :事务 A 读取了事务 B 已修改但尚未提交的数据。随后事务 B 发生回滚,导致事务 A 读取到的数据是"无效的"或"从未真实存在过的"。
示例场景
| 时间 | 事务 A(查询) | 事务 B(更新) |
|---|---|---|
| T1 | 开始事务 | 开始事务 |
| T2 | 将账户余额从 100 改为 200 | |
| T3 | 读取余额:200 | |
| T4 | 回滚(余额恢复 100) | |
| T5 | 提交事务(基于 200 做后续计算) |
后果:事务 A 基于一个"脏数据" 200 进行了业务操作,导致数据逻辑错误。
2. 不可重复读(Non-Repeatable Read)
定义 :事务 A 内多次读取同一行数据 ,但在两次读取之间,该行数据被事务 B 修改并提交 ,导致事务 A 前后读取的值不一致。

示例场景
| 时间 | 事务 A(两次读取) | 事务 B(修改并提交) |
|---|---|---|
| T1 | 开始事务 | |
| T2 | 读取余额:100 | |
| T3 | 开始事务 | |
| T4 | 将余额改为 200 | |
| T5 | 提交事务 | |
| T6 | 再次读取余额:200 | |
| T7 | 提交事务 |
后果:事务 A 在同一个逻辑上下文中看到数据"变了",违反了事务内部的一致视图原则。
3. 幻读(Phantom Read)
定义 :事务 A 内多次查询符合某条件的记录集 (范围查询),在两次查询之间,事务 B 插入或删除了符合该条件的行 并提交,导致事务 A 前后看到的记录数量(或集合)不一致。
示例场景
| 时间 | 事务 A(范围查询) | 事务 B(插入) |
|---|---|---|
| T1 | 开始事务 | |
| T2 | SELECT * FROM t WHERE age>18 → 10 条 |
|
| T3 | 开始事务 | |
| T4 | INSERT INTO t (age) VALUES (20) |
|
| T5 | 提交事务 | |
| T6 | SELECT * FROM t WHERE age>18 → 11 条 |
|
| T7 | 提交事务 |
后果:就像出现了"幻觉"一样,多出了之前不存在的数据行。
五、隔离级别
SQL 标准定义了四个隔离级别,级别越高越安全,但并发性能越低:

MySQL InnoDB 默认隔离级别是可重复读(REPEATABLE READ) 。表中"基本无"幻读,是因为 InnoDB 通过 MVCC + 间隙锁(Gap Lock) 在这个级别下解决了大多数幻读场景,但在某些情况下(比如当前读)仍然可能出现。
查看和设置隔离级别的命令:
sql
-- 查看当前隔离级别
SELECT @@transaction_isolation; --mysql80 默认为repeatable readl
-- 设置当前会话的隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
set session transaction isolation level repeatable read;
-- 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
六、完整的转账事务示例
把以上所有知识点串联起来:
sql
-- 确保关闭自动提交
SET autocommit = 0;
START TRANSACTION;
-- 检查余额
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 加排他锁
-- 扣款
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
-- 收款
UPDATE accounts SET balance = balance + 500 WHERE id = 2;
-- 检查余额是否合法(一致性由应用层保证)
-- 如果余额不够,回滚
-- ROLLBACK;
COMMIT; -- 持久化到磁盘(redo log 保证)
知识点总结
| 维度 | 要点 |
|---|---|
| ACID | 原子/一致/隔离/持久,A靠undo log,D靠redo log |
| 并发问题 | 脏读 > 不可重复读(行值变)> 幻读(行数变) |
| 隔离级别 | 默认 REPEATABLE READ,InnoDB用MVCC解决幻读 |
| 控制语句 | BEGIN → 操作 → COMMIT / ROLLBACK |
| 保存点 | SAVEPOINT 支持部分回滚 |
| 自动提交 | autocommit=1 默认开启,每条语句都是独立事务 |
测试过程:



问题:
我执行完select* from account,在这之前没有commit的结果是第二张图,但是我在datagrid后面真实查看的时候是第三张图,为什么会这样?
回答:
你在客户端(DataGrip 的 SQL 编辑器)里执行的 select * from account 看到的是 1900/2100,因为那条 SELECT 和前面的两条 UPDATE 在**同一个会话(session)**里,读到了同一个事务内还未提交的修改。
而 DataGrip 的 Data 面板 (第三张图那种表格视图)用的是另一个独立的数据库连接去查询数据,它看到的是已提交到数据库的数据。由于你没有执行 COMMIT,两条 UPDATE 的修改对其他连接是不可见的,所以它看到的仍然是初始值 2000/2000。
用一张图来理解:

这正是 MySQL **隔离性(Isolation)**在起作用:一个事务未提交的数据,对其他事务不可见(MySQL 默认隔离级别是可重复读)。
解决方法
执行完两条 UPDATE 之后,加上 COMMIT 即可:
insert into account (id, name, money) values (null, '张三', 2000), (null, '李四', 2000);
start transaction;
update account set money = money - 100 where name = '张三';
update account set money = money + 100 where name = '李四';
commit; -- 加上这行,Data 面板就能看到 1900/2100 了
select * from account;
提交之后,DataGrip 的 Data 面板刷新就会和编辑器里查到的结果一致了。
总结:
