事务和隔离级别

事务保证一组 SQL 要么都成功,要么都失败。

最典型的例子是转账:

① A 账户扣 100
② B 账户加 100

这两步必须作为一个整体。不能只扣了 A 却没给 B 加钱。


一、事务基本语法

START TRANSACTION;   -- 开启事务
-- 执行一组 SQL
COMMIT;              -- 提交:全部生效
ROLLBACK;            -- 回滚:全部撤销

完整示例:转账

-- 准备数据
CREATE TABLE accounts (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    balance DECIMAL(10, 2) NOT NULL DEFAULT 0.00
) ENGINE=InnoDB;

INSERT INTO accounts (username, balance) VALUES ('alice', 1000.00), ('bob', 500.00);

-- 转账:alice 给 bob 转 100
START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE username = 'alice';
UPDATE accounts SET balance = balance + 100 WHERE username = 'bob';

COMMIT;

如果中间出问题:

START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE username = 'alice';
-- 发现有问题,撤销
ROLLBACK;

ROLLBACK 会撤销当前事务里所有未提交的修改,回到 START TRANSACTION 之前的状态。

BEGIN 和 START TRANSACTION 的区别

BEGIN;                -- 等价于 START TRANSACTION
START TRANSACTION;    -- 标准写法

两者基本一样,START TRANSACTION 是 MySQL 推荐写法。


二、自动提交 autocommit

MySQL 默认开启自动提交:每条 SQL 执行完自动 COMMIT

-- 这条 SQL 执行后自动提交,不需要手动 COMMIT
UPDATE accounts SET balance = balance + 10 WHERE id = 1;

查看自动提交状态:

SELECT @@autocommit;   -- 1 = 开启,0 = 关闭

关闭自动提交:

SET autocommit = 0;

关闭后每条 SQL 都在事务里,需要手动 COMMITROLLBACK

START TRANSACTION 显式开启事务时,会临时关闭自动提交,直到 COMMITROLLBACK


三、ACID 四个特性

特性含义转账例子
原子性 Atomicity事务中的操作要么全做,要么全不做扣款和加钱必须一起成功或一起失败
一致性 Consistency事务前后数据满足约束和业务规则转账前后总额不变
隔离性 Isolation多个事务并发执行时互不干扰A 转账时 B 读到的余额不能是中间状态
持久性 Durability提交后的数据不会因故障丢失COMMIT 后即使数据库重启钱也不会丢

先重点理解原子性:"要么都成功,要么都失败"。


四、SAVEPOINT 保存点

事务太长时,可以设置保存点,回滚到指定位置而不是全部撤销。

START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE username = 'alice';
SAVEPOINT sp1;   -- 设置保存点

UPDATE accounts SET balance = balance + 100 WHERE username = 'bob';
-- 第二步出问题,只回滚到 sp1,第一步的扣款保留
ROLLBACK TO sp1;

-- 可以继续执行其他操作
UPDATE accounts SET balance = balance + 50 WHERE username = 'bob';

COMMIT;

语法

SAVEPOINT 保存点名;           -- 创建保存点
ROLLBACK TO 保存点名;         -- 回滚到保存点
RELEASE SAVEPOINT 保存点名;   -- 删除保存点

五、并发问题

多个事务同时操作同一份数据时,会出现以下问题。

① 脏读(Dirty Read)

读到了别人还没提交的数据。

事务 A:把 alice 余额改成 800(还没 COMMIT)
事务 B:读到 alice 余额是 800
事务 A:ROLLBACK,余额回到 1000
事务 B:以为是 800,实际是 1000 → 脏读

② 不可重复读(Non-Repeatable Read)

同一个事务里,两次读同一行,结果不一样(被别人 UPDATE 了)。

事务 B:读 alice 余额 = 1000
事务 A:UPDATE alice 余额为 800,COMMIT
事务 B:再读 alice 余额 = 800 → 不可重复读

③ 幻读(Phantom Read)

同一个事务里,两次同样的查询,结果集行数不一样(被别人 INSERT 或 DELETE 了)。

事务 B:SELECT COUNT(*) FROM accounts WHERE balance > 500 → 结果是 2
事务 A:INSERT 一个新账户 balance=600,COMMIT
事务 B:再查 SELECT COUNT(*) → 结果是 3 → 幻读

三种问题的区别

问题谁导致的现象
脏读读了未提交的数据读到可能不存在的值
不可重复读别人 UPDATE + COMMIT同一行两次读结果不同
幻读别人 INSERT / DELETE + COMMIT同一查询两次结果集行数不同

六、隔离级别

隔离级别越高,问题越少,但并发性能越差。

隔离级别脏读不可重复读幻读
READ UNCOMMITTED✅ 会出现✅ 会出现✅ 会出现
READ COMMITTED❌ 不会✅ 会出现✅ 会出现
REPEATABLE READ❌ 不会❌ 不会⚠️ 基本不会(InnoDB 有优化)
SERIALIZABLE❌ 不会❌ 不会❌ 不会
READ UNCOMMITTED → 什么都挡不住
READ COMMITTED   → 挡住脏读
REPEATABLE READ  → 挡住脏读和不可重复读(MySQL InnoDB 默认)
SERIALIZABLE     → 全挡住,但并发最差

MySQL InnoDB 默认 REPEATABLE READ,大多数场景够用。

查看当前隔离级别

SELECT @@transaction_isolation;

设置隔离级别

-- 当前会话生效
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 全局生效(需要权限)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

各级别适合什么场景

级别适合场景
READ UNCOMMITTED几乎不用,除非能容忍脏数据
READ COMMITTED对一致性要求不高但并发要求高
REPEATABLE READ大多数业务场景(MySQL 默认)
SERIALIZABLE对数据一致性要求极高的场景,如金融

七、SELECT ... FOR UPDATE

先查询再更新时,用 FOR UPDATE 加锁,防止并发问题。

扣库存场景

START TRANSACTION;

-- 查库存并加排他锁,其他事务不能修改这行
SELECT stock FROM products WHERE id = 1 FOR UPDATE;

-- 库存 > 0 才扣减
UPDATE products SET stock = stock - 1 WHERE id = 1 AND stock > 0;

COMMIT;

FOR UPDATE 会对查到的行加排他锁

  • 其他事务读这行可以,但不能修改,必须等当前事务 COMMITROLLBACK
  • 必须在事务里使用,否则没意义

注意事项

① 必须在事务里使用(START TRANSACTION 之后)
② 查询条件尽量走索引,否则可能锁表
③ 锁持有时间越短越好,用完马上 COMMIT

FOR UPDATE vs 普通 SELECT

-- 普通查询:不加锁,并发时可能读到旧数据
SELECT stock FROM products WHERE id = 1;

-- 加锁查询:其他事务修改这行时会等待
SELECT stock FROM products WHERE id = 1 FOR UPDATE;

八、并发更新的正确姿势

错误做法:应用层读改写

① 读出 view_count = 10
② 应用里 10 + 1 = 11
③ 写回 view_count = 11

并发时两个请求都读到 10,都写回 11,少加了一次。

正确做法:SQL 里直接算

UPDATE posts SET view_count = view_count + 1 WHERE id = 1;

数据库层面处理并发,不会丢更新。


九、事务只对 InnoDB 有效

引擎事务支持
InnoDB✅ 支持
MyISAM❌ 不支持
CREATE TABLE accounts (
    ...
) ENGINE=InnoDB;   -- 业务表必须用 InnoDB

如果表是 MyISAM,即使写了 START TRANSACTION,也没有事务能力。


十、事务使用原则

原则为什么
多步关键操作放事务保证原子性,要么全成功要么全失败
事务越短越好长事务持有锁时间长,影响并发
按固定顺序更新表降低死锁概率(A先锁表1再锁表2,B也按同样顺序)
失败要回滚后端代码捕获异常后必须执行 ROLLBACK
用 InnoDB不支持事务的引擎写了事务也没用