← 返回 MYSQL 列表

事务基础

事务基础

一、事务 ACID 四大特性

1.1 原子性(Atomicity)

事务中的所有操作要么全部成功,要么全部失败回滚。

sql
-- 转账案例:A 扣钱,B 加钱,两个操作必须同时成功或同时失败
START TRANSACTION;

UPDATE account SET balance = balance - 1000 WHERE id = 1;  -- A 扣钱
UPDATE account SET balance = balance + 1000 WHERE id = 2;  -- B 加钱

COMMIT;  -- 都成功
-- 或 ROLLBACK;  -- 任何一个失败,全部回滚

1.2 一致性(Consistency)

事务执行前后,数据库从一个一致性状态转变为另一个一致性状态。

sql
-- 一致性约束:转账后总金额不变
-- 事务前:A(1000) + B(0) = 1000
-- 事务后:A(0) + B(1000) = 1000

-- 数据库层面的约束帮助保证一致性
ALTER TABLE account ADD CONSTRAINT ck_balance CHECK (balance >= 0);

1.3 隔离性(Isolation)

并发执行的事务之间互不干扰。

sql
-- 隔离级别控制一个事务对另一个事务的可见程度
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

1.4 持久性(Durability)

事务一旦提交,其结果就是永久的,即使系统崩溃也不会丢失。

sql
-- 依赖 redo log 保证持久性
-- 事务提交时,先将修改写入 redo log(磁盘),再写入数据页
COMMIT;  -- 写入 redo log 后即使宕机也能恢复

二、事务操作语法

2.1 基本语法

sql
-- 方式1:显式事务
START TRANSACTION;
-- 或 BEGIN;

-- 执行 SQL 操作
UPDATE inventory SET stock = stock - 1 WHERE product_id = 100;
INSERT INTO order_log(product_id, quantity) VALUES(100, 1);

-- 提交或回滚
COMMIT;      -- 确认所有操作
-- ROLLBACK; -- 撤销所有操作

-- 方式2:自动提交(默认)
-- 每一条 DML 语句都是一个独立的事务
SHOW VARIABLES LIKE 'autocommit';  -- 默认 ON
SET autocommit = 0;  -- 关闭自动提交

2.2 保存点(SAVEPOINT)

sql
START TRANSACTION;

UPDATE account SET balance = balance - 1000 WHERE id = 1;

SAVEPOINT after_step1;  -- 设置保存点

UPDATE account SET balance = balance + 1000 WHERE id = 2;

-- 发现第二步有问题,回滚到保存点
ROLLBACK TO SAVEPOINT after_step1;
-- 此时第一步的变更还在,可以修正后继续执行

UPDATE account SET balance = balance + 1000 WHERE id = 3;
COMMIT;

2.3 实战:完整的订单事务

sql
-- 下单事务:扣库存、生成订单、记录日志
START TRANSACTION;

-- 1. 扣减库存(带条件防止超卖)
UPDATE product
SET stock = stock - 1
WHERE id = 100 AND stock >= 1;

-- 检查受影响行数
IF ROW_COUNT() = 0 THEN
    ROLLBACK;
    SELECT '库存不足' AS msg;
ELSE
    -- 2. 创建订单
    INSERT INTO orders (user_id, product_id, amount, status)
    VALUES (1, 100, 99.99, '已支付');

    -- 3. 记录支付日志
    INSERT INTO payment_log (order_id, amount, status)
    VALUES (LAST_INSERT_ID(), 99.99, '成功');

    COMMIT;
END IF;

三、四大事务隔离级别

3.1 隔离级别概览

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED(读未提交)
READ COMMITTED(读已提交)
REPEATABLE READ(可重复读,MySQL默认)
SERIALIZABLE(串行化)

3.2 脏读(Dirty Read)

一个事务读到另一个事务未提交的数据。

sql
-- 事务A(未提交)
START TRANSACTION;
UPDATE account SET balance = 0 WHERE id = 1;
-- 还未提交

-- 事务B(此时读取,如果隔离级别是 READ UNCOMMITTED)
SELECT balance FROM account WHERE id = 1;  -- 读到 0(脏数据)

-- 事务A 回滚
ROLLBACK;
-- 事务B 之前读到的 0 就成了脏数据

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

一个事务内两次读取同一行数据,结果不同(另一事务修改并提交了)。

sql
-- 事务A 先查询
START TRANSACTION;
SELECT balance FROM account WHERE id = 1;  -- 读到 1000

-- 事务B 修改并提交
UPDATE account SET balance = 500 WHERE id = 1;
COMMIT;

-- 事务A 再次查询(READ COMMITTED 级别)
SELECT balance FROM account WHERE id = 1;  -- 读到 500(两次结果不同)
COMMIT;

3.4 幻读(Phantom Read)

一个事务内两次查询同一范围,第二次多出一些行(另一事务插入了新数据)。

sql
-- 事务A 查询
START TRANSACTION;
SELECT * FROM account WHERE balance > 0;  -- 查到 2 条记录

-- 事务B 插入新数据并提交
INSERT INTO account (id, balance) VALUES (3, 200);
COMMIT;

-- 事务A 再次查询(REPEATABLE READ 级别)
SELECT * FROM account WHERE balance > 0;  -- 查到 3 条记录(多了一条)
COMMIT;

3.5 设置隔离级别

sql
-- 全局级别(所有新会话生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 当前会话级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 下一次事务
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 或
SHOW VARIABLES LIKE 'transaction_isolation';

四、实战案例

案例1:库存扣减防超卖

sql
-- 正确的库存扣减事务
START TRANSACTION;

-- 使用行锁 + 条件检查防止超卖
UPDATE inventory
SET stock = stock - 1
WHERE product_id = 100 AND stock > 0;

-- 检查是否扣减成功
SELECT ROW_COUNT() AS affected_rows;
-- 如果 affected_rows = 0,说明库存不足

COMMIT;

案例2:选择合适的隔离级别

sql
-- 报表统计:需要一致的快照 -> REPEATABLE READ
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

START TRANSACTION;
-- 统计所有订单数据(整个事务看到同一个快照)
SELECT COUNT(*) FROM orders;
SELECT SUM(amount) FROM orders;
SELECT AVG(amount) FROM orders;
COMMIT;

-- 对账系统:只需要已提交数据 -> READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

START TRANSACTION;
-- 读取所有已完成的订单(读到的都是已提交的数据)
SELECT * FROM orders WHERE status = '已完成';
COMMIT;

案例3:事务超时与死锁处理

sql
-- 设置事务锁等待超时(默认 50 秒)
SET innodb_lock_wait_timeout = 5;  -- 5 秒超时

-- 场景:两个事务互相等待
-- 事务A:
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 事务B 此时更新了 id=2 的记录
UPDATE account SET balance = balance + 100 WHERE id = 2;  -- 等待事务B释放id=2的锁

-- 事务B:
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 2;
-- 事务A 此时更新了 id=1 的记录
UPDATE account SET balance = balance + 100 WHERE id = 1;  -- 等待事务A释放id=1的锁

-- MySQL 会自动检测死锁,回滚其中一个事务
-- 避免死锁:固定更新顺序(总是先 id=1 再 id=2)

五、常见问题

5.1 长事务的危害

sql
-- 1. 锁定资源时间过长,导致并发降低
-- 2. 产生大量 undo log,占用空间
-- 3. MVCC 需要保留旧版本,影响查询性能

-- 如何查找长事务
SELECT * FROM information_schema.INNODB_TRX
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;

5.2 事务使用最佳实践

sql
-- 1. 事务要短小,不要跨网络请求
START TRANSACTION;
UPDATE SET ...;  -- 快速执行
UPDATE SET ...;  -- 快速执行
COMMIT;           -- 尽快提交

-- 2. 避免在事务中做耗时操作(发送HTTP请求等)
-- 3. 根据业务选择合适的隔离级别
-- 大部分业务 READ COMMITTED 即可
-- 只有需要一致性快照的场景才用 REPEATABLE READ

总结

概念 核心要点
ACID 原子性、一致性、隔离性、持久性
事务操作 START TRANSACTION / COMMIT / ROLLBACK / SAVEPOINT
隔离级别 RC 防脏读、RR 防脏读+不可重复读、Serializable 全防
脏读 读到未提交数据
不可重复读 同行两次读取结果不同
幻读 同范围两次查询结果行数不同
实践原则 事务短小、快进快出、合理选隔离级别