事务经常和 commit、rollback 一起出现。但真正让我觉得事务重要的,是开始理解“多个请求同时操作数据库”之后。
例如,一个订单系统里:
用户 A 正在修改订单;
用户 B 正在查看同一笔订单;
用户 C 又准备取消它。
如果没有合适的规则,不同请求可能会读到不一致的数据。MySQL 的事务隔离级别,就是用来规定并发事务之间“能看见彼此多少变化”的机制。
事务的四个基本特性
事务通常用 ACID 来概括:
原子性(Atomicity):一组操作要么全部成功,要么全部失败。
一致性(Consistency):事务执行前后,数据仍满足业务规则。
隔离性(Isolation):并发事务之间不会随意互相干扰。
持久性(Durability):事务提交后,数据不会因为程序崩溃轻易丢失。
隔离性。
并发事务可能出现的三个问题
为了方便理解,假设有一张账户表:
CREATE TABLE accounts (
id INT PRIMARY KEY,
balance DECIMAL(10, 2)
);账户余额初始为 100。
脏读:读到了还没提交的数据
事务 A 修改余额,但还没有提交:
-- 事务 A
UPDATE accounts
SET balance = 50
WHERE id = 1;这时事务 B 读到了余额 50:
-- 事务 B
SELECT balance FROM accounts WHERE id = 1;随后事务 A 发现操作有误,执行回滚:
ROLLBACK;余额又变回 100。
事务 B 刚才读到的 50 从来没有真正生效,这就是脏读。
不可重复读:同一个事务读两次,结果不同
事务 A 第一次查询余额:
-- 事务 A
SELECT balance FROM accounts WHERE id = 1;
-- 结果:100这时事务 B 修改并提交:
-- 事务 B
UPDATE accounts
SET balance = 80
WHERE id = 1;
COMMIT;事务 A 再次查询:
SELECT balance FROM accounts WHERE id = 1;
-- 结果:80同一事务、同一条查询,结果却变了,这叫不可重复读。
幻读:同一个条件查询,出现了“新行”
事务 A 查询所有金额大于 100 的订单:
SELECT * FROM orders WHERE amount > 100;事务 B 插入一条符合条件的订单并提交:
INSERT INTO orders (id, amount)
VALUES (101, 200);
COMMIT;事务 A 再次执行同样的查询,发现多出一条记录。
这类“像凭空多出来一行”的现象,叫做幻读。
MySQL 的四种隔离级别
从低到高,MySQL 定义了四种标准隔离级别:
隔离越严格,数据越稳定;但锁等待和并发冲突通常也会更多。
1. READ UNCOMMITTED:读未提交
这是最低的隔离级别。一个事务可以读到其他事务尚未提交的修改。
它可能导致脏读,因此实际业务中很少使用。
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;除非是对一致性要求极低的场景,否则不建议选择它。
2. READ COMMITTED:读已提交
在这个级别下,只能读到其他事务已经提交的数据,因此避免了脏读。
但同一个事务里执行两次查询,若中间有其他事务提交修改,结果可能不同,也就是仍然会出现不可重复读。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;很多数据库默认使用这个级别。它适合读写比较频繁、能接受“下一次查询看到最新已提交数据”的业务。
3. REPEATABLE READ:可重复读
这是 InnoDB 常见的默认隔离级别。
在一个事务中,多次执行普通查询,通常会看到事务开始时的一致性视图。因此可以避免不可重复读。
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;例如事务开始后第一次读到余额 100,即使其他事务后来修改并提交了余额,当前事务再次普通查询,仍可能看到 100。
这里有一个容易混淆的点:MySQL 的 InnoDB 通过 MVCC(多版本并发控制)和 next-key lock 等机制,对幻读的处理比标准定义更复杂。
简单理解即可:
普通
SELECT通常是快照读,读取事务一致性视图;SELECT ... FOR UPDATE、UPDATE、DELETE等属于当前读,会读取较新的已提交版本,并可能使用锁防止其他事务插入符合条件的新记录。
因此,不能只背“可重复读会出现幻读”,还要结合具体 SQL 类型和是否加锁来看。
4. SERIALIZABLE:串行化
这是最严格的隔离级别。数据库尽量让并发事务表现得像一个接一个串行执行。
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;它能避免脏读、不可重复读和幻读,但代价是并发能力下降,锁等待和死锁风险也会增加。
它适合对一致性要求极高、并发量相对有限的场景,但不适合不加评估地用于高并发业务。
一个常见误区:隔离级别不能解决所有并发问题
即使使用了可重复读,也不代表业务一定安全。
例如库存初始为 1,两个事务都先查询库存:
SELECT stock FROM products WHERE id = 1;它们都读到库存为 1,于是都认为可以下单。如果后续分别扣减,仍然可能出现超卖或更新丢失等问题。
更可靠的做法是把“判断库存”和“扣减库存”放在一条 SQL 中:
UPDATE products
SET stock = stock - 1
WHERE id = 1
AND stock > 0;然后检查受影响行数:
影响 1 行:扣减成功;
影响 0 行:库存不足,或商品不存在。
这比“先查库存,再更新库存”更安全。必要时还可以配合事务、行锁、版本号或分布式锁。
如何查看和设置隔离级别
查看当前会话隔离级别:
SELECT @@transaction_isolation;设置当前会话:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;注意,隔离级别最好在开启事务之前设置:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- 执行业务 SQL
COMMIT;应用开发中,也要确认数据库连接池会不会复用连接。某个请求修改了 session 级别配置后,如果连接回到池中,后续请求可能继承这个设置。因此通常更推荐统一在数据库或应用初始化阶段配置,而不是随意在业务代码中修改。
总结
隔离级别决定了一个事务在并发环境中,何时能看见其他事务提交的数据,以及数据库为了保证这种规则需要付出多少并发代价。
实际开发中,最常见的关注点是:
READ COMMITTED:每次查询看到最新已提交的数据;REPEATABLE READ:同一事务中的普通查询保持较稳定的快照;关键业务不要只依赖隔离级别;
库存、余额、抢购等场景要通过条件更新、事务和锁保证最终正确性。
理解隔离级别的价值,不只是为了记住四个英文名称,而是在遇到并发数据问题时,知道该从“事务、SQL 写法、锁和业务约束”几个方向一起排查。