Skip to content
清晨的一缕阳光
返回

事务 ACID 特性详解

引言

事务是数据库系统的核心特性,是保证数据正确性和一致性的关键机制。理解事务的 ACID 特性,对于编写正确的数据库应用至关重要。

本章将深入讲解:

核心概念

什么是事务

事务(Transaction) 是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。这些操作要么全部执行成功,要么全部不执行,是一个不可分割的工作单位。

典型场景:

-- 银行转账:A 向 B 转账 100 元
START TRANSACTION;

-- 步骤 1:A 账户扣 100 元
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';

-- 步骤 2:B 账户加 100 元
UPDATE accounts SET balance = balance + 100 WHERE account_id = 'B';

-- 步骤 3:提交事务
COMMIT;

事务的特点:

事务的 ACID 特性

graph TD
    A[事务 ACID] --> B[原子性 Atomicity]
    A --> C[一致性 Consistency]
    A --> D[隔离性 Isolation]
    A --> E[持久性 Durability]
    
    B --> B1[Undo Log 实现]
    C --> C1[原子性 + 隔离性保证]
    D --> D1[锁+MVCC 实现]
    E --> E1[Redo Log 实现]
    
    style A fill:#ff9999
    style B fill:#99ccff
    style C fill:#99ccff
    style D fill:#99ccff
    style E fill:#99ccff

原子性(Atomicity)

概念

原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部发生,要么全部不发生。

示例:

START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';
-- 假设这里发生错误或断电

UPDATE accounts SET balance = balance + 100 WHERE account_id = 'B';

COMMIT;

如果第 2 步失败:

MySQL 实现原理

Undo Log(回滚日志) 实现原子性:

-- 执行更新时,先记录 Undo Log
-- 事务 101:UPDATE accounts SET balance = 900 WHERE id = 1

-- Undo Log 记录:
-- {transaction_id: 101, prev_balance: 1000, operation: "UPDATE", row_id: 1}

-- 如果事务回滚或崩溃恢复:
-- 根据 Undo Log 将 balance 恢复为 1000

Undo Log 的作用:

  1. 回滚:事务执行失败时,撤销已做的修改
  2. MVCC:提供数据的历史版本,实现多版本并发控制

崩溃恢复流程:

graph LR
    A[数据库崩溃] --> B[重启恢复]
    B --> C[读取 Undo Log]
    C --> D{事务是否提交?}
    D -->|未提交 | E[回滚操作]
    D -->|已提交 | F[保持修改]
    E --> G[数据一致性恢复]
    F --> G

实战示例

手动回滚:

START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';

-- 检查条件
SELECT balance FROM accounts WHERE account_id = 'A';
-- 发现余额不足

-- 回滚事务
ROLLBACK;
-- A 账户余额恢复原状

异常自动回滚:

-- 设置自动回滚
SET autocommit = 0;

START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';

-- 假设发生错误(如违反约束)
-- MySQL 会自动回滚整个事务

一致性(Consistency)

概念

一致性是指事务执行前后,数据库从一个一致性状态变换到另一个一致性状态。

一致性状态包括:

示例:

-- 约束:balance >= 0
ALTER TABLE accounts ADD CONSTRAINT chk_balance CHECK (balance >= 0);

START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';
-- 如果 A 账户余额只有 50,违反 CHECK 约束

-- MySQL 会拒绝执行,事务回滚
-- 保证了数据的一致性

一致性的保证

一致性是 ACID 的最终目的,由其他三个特性共同保证:

-- 原子性保证:操作要么全做,要么全不做
-- 隔离性保证:并发事务不互相干扰
-- 持久性保证:提交后数据永久保存

-- 三者共同作用 → 数据一致性

业务一致性示例:

-- 场景:订单支付
START TRANSACTION;

-- 1. 更新订单状态
UPDATE orders SET status = 'PAID' WHERE order_id = 'ORD001';

-- 2. 扣减库存
UPDATE products SET stock = stock - 1 WHERE product_id = 'PROD001';

-- 3. 记录支付流水
INSERT INTO payment_logs (order_id, amount, create_time) 
VALUES ('ORD001', 100.00, NOW());

COMMIT;
-- 三个操作要么都成功,要么都失败
-- 保证了业务数据的一致性

隔离性(Isolation)

概念

隔离性是指多个事务并发执行时,一个事务的执行不应影响其他事务的执行。

并发事务可能出现的问题:

问题描述示例
脏读读到未提交的数据T1 修改未提交,T2 读取
不可重复读同一事务内多次读取结果不同T1 读取后,T2 修改并提交,T1 再次读取
幻读同一事务内多次查询,结果集数量不同T1 查询后,T2 插入新数据,T1 再次查询

脏读(Dirty Read)

场景:

时间线:
T1: START TRANSACTION;
T1: UPDATE accounts SET balance = 900 WHERE id = 1;  -- 余额从 1000 改为 900
T2: START TRANSACTION;
T2: SELECT balance FROM accounts WHERE id = 1;  -- 读到 900(未提交)❌
T1: ROLLBACK;  -- T1 回滚,余额恢复为 1000
T2: SELECT balance FROM accounts WHERE id = 1;  -- 读到 1000
-- T2 发现前后读取结果不同,之前读到的是脏数据!

避免方法:

-- 设置隔离级别为 READ COMMITTED 或更高
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

不可重复读(Non-repeatable Read)

场景:

时间线:
T1: START TRANSACTION;
T1: SELECT balance FROM accounts WHERE id = 1;  -- 读到 1000
T2: START TRANSACTION;
T2: UPDATE accounts SET balance = 900 WHERE id = 1;
T2: COMMIT;  -- T2 提交
T1: SELECT balance FROM accounts WHERE id = 1;  -- 读到 900 ❌
-- T1 在同一事务内两次读取结果不同!

避免方法:

-- 设置隔离级别为 REPEATABLE READ 或更高
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

幻读(Phantom Read)

场景:

时间线:
T1: START TRANSACTION;
T1: SELECT * FROM accounts WHERE balance > 500;  -- 返回 10 条记录
T2: START TRANSACTION;
T2: INSERT INTO accounts (id, balance) VALUES (101, 600);
T2: COMMIT;  -- T2 提交
T1: SELECT * FROM accounts WHERE balance > 500;  -- 返回 11 条记录 ❌
-- T1 发现"凭空"多了一条记录,像幻影一样!

避免方法:

-- 设置隔离级别为 SERIALIZABLE
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 或使用 InnoDB 的 MVCC + 间隙锁
-- MySQL 默认隔离级别 REPEATABLE READ 已能避免大部分幻读

四种隔离级别

SQL 标准定义了四种隔离级别,从低到高:

graph TD
    A[READ UNCOMMITTED] -->|解决 | B[脏读 ❌]
    B --> C[READ COMMITTED]
    C -->|解决 | D[不可重复读 ❌]
    D --> E[REPEATABLE READ]
    E -->|解决 | F[幻读 ❌]
    F --> G[SERIALIZABLE]
    
    style A fill:#ffcccc
    style C fill:#ffeb99
    style E fill:#ccffcc
    style G fill:#99ccff

详细对比:

隔离级别脏读不可重复读幻读性能
READ UNCOMMITTED⚠️ 可能⚠️ 可能⚠️ 可能⭐⭐⭐⭐⭐
READ COMMITTED✅ 避免⚠️ 可能⚠️ 可能⭐⭐⭐⭐
REPEATABLE READ✅ 避免✅ 避免⚠️ 部分避免⭐⭐⭐
SERIALIZABLE✅ 避免✅ 避免✅ 避免⭐⭐

MySQL 的隔离级别

查看当前隔离级别:

-- 查看会话级别
SELECT @@transaction_isolation;
-- 或
SELECT @@tx_isolation;

-- 查看全局级别
SELECT @@global.transaction_isolation;

设置隔离级别:

-- 设置会话级别(只对当前连接生效)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 设置全局级别(对新连接生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 设置单个事务
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- 事务结束后恢复原级别

MySQL 8.0 默认: REPEATABLE READ

持久性(Durability)

概念

持久性是指事务一旦提交,其对数据库的修改就是永久性的,即使发生故障也不会丢失。

示例:

START TRANSACTION;

UPDATE accounts SET balance = 900 WHERE id = 1;

COMMIT;  -- 提交后,即使立即断电,修改也不会丢失

MySQL 实现原理

Redo Log(重做日志) 实现持久性:

-- 事务执行流程:
-- 1. 修改数据页(在内存 Buffer Pool 中)
-- 2. 写入 Redo Log(顺序写磁盘,速度快)
-- 3. 提交事务

-- 崩溃恢复流程:
-- 1. 数据库重启
-- 2. 读取 Redo Log
-- 3. 重做已提交的事务
-- 4. 数据恢复到一致状态

Redo Log 的特点:

  1. 顺序写:比随机写数据页快得多
  2. 循环写:写满后从头开始覆盖
  3. 崩溃安全:即使数据页没刷盘,Redo Log 已落盘

WAL(Write-Ahead Logging)技术:

graph LR
    A[事务修改] --> B[写 Redo Log]
    B --> C{提交事务?}
    C -->|是 | D[刷盘 Redo Log]
    C -->|否 | E[继续执行]
    D --> F[异步刷数据页]
    F --> G[持久化完成]

双 1 设置(最安全)

# my.cnf 配置
[mysqld]
# 每次事务提交都刷盘 Redo Log
innodb_flush_log_at_trx_commit = 1

# 每次提交都同步刷盘
sync_binlog = 1

性能影响:

性能优化配置

# 性能优先配置
[mysqld]
# 每秒刷盘一次 Redo Log
innodb_flush_log_at_trx_commit = 2

# 操作系统控制 binlog 刷盘
sync_binlog = 0

风险:

事务控制实战

事务控制语句

-- 开启事务
START TRANSACTION;
-- 或
BEGIN;

-- 提交事务
COMMIT;

-- 回滚事务
ROLLBACK;

-- 设置保存点
SAVEPOINT sp1;
ROLLBACK TO sp1;  -- 回滚到保存点
RELEASE SAVEPOINT sp1;  -- 释放保存点

自动提交

-- 查看自动提交状态
SELECT @@autocommit;

-- 关闭自动提交(手动控制事务)
SET autocommit = 0;

-- 开启自动提交
SET autocommit = 1;

注意:

-- autocommit = 1 时,每条 SQL 都是一个事务
UPDATE accounts SET balance = 100 WHERE id = 1;  -- 自动提交

-- autocommit = 0 时,需要手动提交
SET autocommit = 0;
UPDATE accounts SET balance = 100 WHERE id = 1;
UPDATE accounts SET balance = 200 WHERE id = 2;
COMMIT;  -- 两条语句作为一个事务提交

隐式提交

以下操作会导致隐式提交:

-- DDL 语句
CREATE TABLE ...
ALTER TABLE ...
DROP TABLE ...

-- 管理语句
SET autocommit = 1;
START TRANSACTION;  -- 会先提交当前事务

注意:

START TRANSACTION;
UPDATE accounts SET balance = 100 WHERE id = 1;
CREATE TABLE logs (...);  -- 隐式提交!前面的 UPDATE 被提交
ROLLBACK;  -- 无法回滚 UPDATE 操作

事务超时与死锁

锁等待超时:

-- 查看超时时间(秒)
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- 默认:50 秒

-- 设置超时时间
SET innodb_lock_wait_timeout = 10;

-- 超时后,语句失败但事务可以继续

死锁检测:

-- 查看死锁日志
SHOW ENGINE INNODB STATUS;

-- 死锁处理:
-- 1. 自动回滚权重小的事务
-- 2. 应用层重试机制

避免死锁:

-- 1. 固定顺序访问表
-- 事务 1 和事务 2 都按相同顺序操作

-- 2. 降低锁粒度
-- 使用索引减少锁范围

-- 3. 减少事务持有时间
-- 大事务拆分为小事务

注意事项

1. 长事务问题

问题:

START TRANSACTION;
-- 大量操作...
-- 长时间未提交

-- 问题:
-- 1. 占用 Undo Log,影响性能
-- 2. 持有锁,阻塞其他事务
-- 3. 主从延迟

监控长事务:

-- 查询运行超过 10 秒的事务
SELECT * FROM information_schema.INNODB_TRX 
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 10;

-- 杀掉长事务
KILL <thread_id>;

2. 事务大小

避免大事务:

-- ❌ 错误:一次性删除 100 万行
START TRANSACTION;
DELETE FROM logs WHERE create_time < '2023-01-01';
COMMIT;

-- ✅ 正确:分批删除
SET @batch_size = 10000;
REPEAT
    START TRANSACTION;
    DELETE FROM logs WHERE create_time < '2023-01-01' LIMIT @batch_size;
    COMMIT;
    DO SLEEP(0.1);
UNTIL ROW_COUNT() < @batch_size END REPEAT;

3. 隔离级别选择

推荐配置:

# 大多数场景
transaction_isolation = REPEATABLE READ

# 需要与 Oracle 兼容
transaction_isolation = READ COMMITTED

# 特殊场景(如统计报表)
# 可以考虑 READ UNCOMMITTED(允许脏读)

4. 应用层事务管理

Java Spring 示例:

@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, BigDecimal amount) {
    // 扣款
    accountDao.debit(from, amount);
    
    // 收款
    accountDao.credit(to, amount);
    
    // 异常时自动回滚
}

总结

ACID 特性总结

特性保证实现机制
原子性操作不可分割Undo Log
一致性数据状态一致原子性 + 隔离性 + 持久性
隔离性并发不干扰锁 + MVCC
持久性提交不丢失Redo Log

隔离级别选择

场景推荐隔离级别
金融系统REPEATABLE READ / SERIALIZABLE
电商系统READ COMMITTED / REPEATABLE READ
日志统计READ UNCOMMITTED / READ COMMITTED
默认配置REPEATABLE READ

最佳实践

  1. 保持事务短小:避免长事务和大事务
  2. 合理选择隔离级别:平衡一致性和性能
  3. 注意隐式提交:DDL 语句会提交事务
  4. 监控事务状态:及时发现长事务和死锁
  5. 应用层重试:处理死锁和超时异常

下一步

理解 ACID 特性后,下一章我们将深入学习:

参考资料


分享这篇文章到:

上一篇文章
Go Context 上下文实战
下一篇文章
Optional 深度解析