5 分钟搞懂事务
ACID、提交回滚、隔离级别——从 0 到能写正确事务代码
事务是什么?
一组数据库操作,要么全做、要么全不做。
经典例子:
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- A 转出 100
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- B 收入 100
COMMIT;如果第二条 UPDATE 失败(断电、网络断、磁盘满),第一条会自动撤销。A 的钱不会消失到空气里。
三个关键词
START TRANSACTION; -- 开始
COMMIT; -- 全部生效
ROLLBACK; -- 全部撤销BEGIN 和 START TRANSACTION 是同义词。客户端连接的"自动提交"模式(autocommit)默认是开的,意味着每一条 SQL 都是独立事务——不显式 START TRANSACTION 就没事务。
-- 看当前是否自动提交
SELECT @@autocommit; -- 1 = 开
SET autocommit = 0; -- 关掉(不推荐全局关)ACID 四个字
- Atomicity 原子性:要么全做要么全不做
- Consistency 一致性:事务前后数据库都满足约束(FK、唯一、CHECK)
- Isolation 隔离性:并发事务之间不互相打架(详见下面)
- Durability 持久性:COMMIT 后数据落盘,断电不丢
隔离级别(最容易踩坑)
SELECT @@transaction_isolation;
-- MariaDB 默认: REPEATABLE-READ| 级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✗ | ✗ | ✗ | 最快 |
| READ COMMITTED | ✓ | ✗ | ✗ | 快 |
| REPEATABLE READ | ✓ | ✓ | InnoDB 已避免 | 中(MariaDB 默认) |
| SERIALIZABLE | ✓ | ✓ | ✓ | 慢 |
✓ = 避免 ✗ = 会发生
实际怎么选:
- 绝大多数业务用默认的 REPEATABLE READ 就够了
- 报表 / 长查询不影响热数据的,用
READ COMMITTED,并发更好 - 金融、库存扣减这种强一致场景,要么 SERIALIZABLE,要么用悲观锁/乐观锁
死锁示例
-- 事务 A
START TRANSACTION;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
-- 同时事务 B
START TRANSACTION;
UPDATE accounts SET balance = balance - 10 WHERE id = 2;
-- A 接着:
UPDATE accounts SET balance = balance + 10 WHERE id = 2; -- 等 B
-- B 接着:
UPDATE accounts SET balance = balance + 10 WHERE id = 1; -- 等 A → 死锁!MariaDB 会自动检测死锁、回滚其中一个事务并报 Error 1213 Deadlock found。应用层必须 retry:
import mariadb
for attempt in range(3):
try:
cur.execute('START TRANSACTION')
# ... 业务
cur.execute('COMMIT')
break
except mariadb.OperationalError as e:
if e.errno == 1213:
time.sleep(0.05 * (2 ** attempt))
continue
raiseSAVEPOINT(部分回滚)
START TRANSACTION;
INSERT INTO logs (msg) VALUES ('start');
SAVEPOINT s1;
INSERT INTO logs (msg) VALUES ('maybe');
ROLLBACK TO s1; -- 只撤销 'maybe' 这条
COMMIT; -- 'start' 还在ORM 里"嵌套事务"通常就是用 SAVEPOINT 实现的。
常见错误
1. 忘了 COMMIT:长时间不提交,会一直持锁,阻塞其他事务。如果用了连接池更糟糕——连接归还时事务还在,下一个使用者继承了未提交事务。
2. DDL 不在事务内:CREATE/ALTER/DROP TABLE 在 MariaDB 中会隐式提交当前事务,并且自身不可回滚。迁移要小心。
3. MyISAM 不支持事务:如果你看到表引擎是 MyISAM,事务语句不报错但无效。改成 InnoDB。
配合 ORM
// Prisma
await prisma.$transaction(async (tx) => {
await tx.account.update({ where: { id: 1 }, data: { balance: { decrement: 100 } } });
await tx.account.update({ where: { id: 2 }, data: { balance: { increment: 100 } } });
});
// SQLAlchemy
with session.begin():
a = session.get(Account, 1)
a.balance -= 100
b = session.get(Account, 2)
b.balance += 100ORM 都会自动加上 START TRANSACTION / COMMIT / 失败时 ROLLBACK。