MariaDB 中文社区

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;            -- 全部撤销

BEGINSTART 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 READInnoDB 已避免中(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
        raise

SAVEPOINT(部分回滚)

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 += 100

ORM 都会自动加上 START TRANSACTION / COMMIT / 失败时 ROLLBACK

下一步

本页目录