Skip to content

定义 ​

ACID 是数据库事务的四个核心特性的缩写,分别是:

  • A tomicity(原子性):事务中的所有操作要么全部成功,要么全部失败回滚
  • C onsistency(一致性):事务执行前后,数据库从一个一致状态变换到另一个一致状态
  • I solation(隔离性):多个并发事务之间互不干扰
  • D urability(持久性):事务一旦提交,对数据的修改就是永久的

ACID 是保证数据库可靠性的基石,任何支持事务的数据库都必须实现这四个特性。

详细笔记 ​

核心原理 ​

一、Atomicity(原子性) ​

定义:事务是不可分割的最小工作单元,事务中的所有操作要么全部成功,要么全部失败回滚。

实现机制: Undo Log

sql
-- 银行转账示例
START TRANSACTION;

-- 步骤1:从账户A扣款
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- 步骤2:向账户B加款
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

COMMIT;

原子性保证:

正常流程:
  1. 执行步骤1 → 记录 Undo Log
  2. 执行步骤2 → 记录 Undo Log
  3. COMMIT → 事务成功

异常流程(步骤2失败):
  1. 执行步骤1 → 记录 Undo Log
  2. 执行步骤2 → 失败!
  3. ROLLBACK → 根据 Undo Log 撤销步骤1
  4. 数据恢复到事务前的状态

InnoDB 源码实现:

cpp
// storage/innobase/trx/trx0trx.cc

/**
 * 事务回滚
 */
void trx_rollback(trx_t* trx)
{
  // 1. 检查事务状态
  if (trx->state == TRX_STATE_COMMITTED) {
    return;  // 已提交,无法回滚
  }
  
  // 2. 获取 Undo Log
  undo_no_t undo_no = trx->undo_no;
  
  // 3. 逐条撤销操作
  while (undo_no > 0) {
    // 从 Undo Log 读取旧值
    undo_rec_t* undo_rec = trx_undo_get_undo_rec(trx, undo_no);
    
    // 恢复数据
    trx_undo_apply(undo_rec);
    
    undo_no--;
  }
  
  // 4. 释放锁
  lock_release(trx);
  
  // 5. 标记事务为回滚状态
  trx->state = TRX_STATE_ROLLED_BACK;
}

关键点:

  • Undo Log 记录的是逻辑日志(如"将某行的某列从值A改为值B")
  • 回滚时按相反顺序执行逆向操作
  • 即使系统崩溃,重启后也能根据 Undo Log 回滚未完成的事务

二、Consistency(一致性) ​

定义:事务执行前后,数据库必须保持一致性状态,即满足所有的约束条件(主键、外键、唯一约束、检查约束等)。

注意:一致性是事务的最终目标,原子性、隔离性、持久性都是为了保障一致性。

一致性检查示例:

sql
-- 表结构
CREATE TABLE accounts (
  id INT PRIMARY KEY,
  balance DECIMAL(10, 2) CHECK (balance >= 0)  -- 余额不能为负
);

-- 事务
START TRANSACTION;

UPDATE accounts SET balance = balance - 200 WHERE id = 1;
-- 如果 balance 原本只有 100,现在变成 -100

COMMIT;
-- ❌ 违反 CHECK 约束,事务失败,自动回滚

InnoDB 的一致性保障:

1. 约束检查:
 - INSERT/UPDATE 时检查主键、外键、唯一约束
 - 违反约束则语句失败

2. 崩溃恢复:
 - Redo Log 重做已提交事务
 - Undo Log 回滚未提交事务
 - 确保重启后数据一致

3. 隔离级别:
 - 通过 MVCC 或锁机制
 - 避免脏读、不可重复读、幻读
 - 保证并发下的一致性

源码层面:

cpp
// storage/innobase/row/row0ins.cc

/**
 * 插入行时检查约束
 */
dberr_t row_ins_check_constraints(
  dict_index_t* index,  // 索引
  const dtuple_t* entry)  // 要插入的数据
{
  // 1. 检查 NOT NULL 约束
  if (dict_col_is_not_null(index->fields[0].col) && 
    dfield_is_null(&entry->fields[0])) {
    return DB_CONSTRAINT_ERROR;
  }
  
  // 2. 检查唯一约束
  if (dict_index_is_unique(index)) {
    if (row_ins_duplicates(entry, index)) {
    return DB_DUPLICATE_KEY;
    }
  }
  
  // 3. 检查外键约束
  if (dict_table_has_foreign_keys(index->table)) {
    if (row_ins_check_foreign_keys(entry, index)) {
    return DB_FK_VIOLATION;
    }
  }
  
  return DB_SUCCESS;
}

三、Isolation(隔离性) ​

定义:多个并发事务同时访问数据库时,每个事务都应该感觉不到其他事务的存在,事务之间互不干扰。

实现机制: MVCC + 锁

隔离级别回顾:

隔离级别实现方式并发度
READ UNCOMMITTED无隔离最高
READ COMMITTEDMVCC(每次查询新快照)高
REPEATABLE READMVCC(复用同一快照) + Next-Key Lock中
SERIALIZABLE完全串行化最低

MVCC 实现隔离性:

sql
-- 事务 A
START TRANSACTION;
SELECT * FROM users WHERE id = 1;
-- 读到: name = '张三'

-- 事务 B(并发执行)
START TRANSACTION;
UPDATE users SET name = '李四' WHERE id = 1;
COMMIT;

-- 事务 A(READ COMMITTED 级别)
SELECT * FROM users WHERE id = 1;
-- 读到: name = '李四' ✅ 看到已提交的更改

-- 事务 A(REPEATABLE READ 级别)
SELECT * FROM users WHERE id = 1;
-- 读到: name = '张三' ✅ 与第一次读取一致

详见: [[MVCC]] 和 [[事务隔离级别]]


四、Durability(持久性) ​

定义:事务一旦提交,对数据的修改就是永久的,即使系统崩溃也不会丢失。

实现机制: Redo Log(WAL - Write Ahead Logging)

WAL 原理:

传统方式(先写数据文件):
  1. 修改 Buffer Pool 中的数据页
  2. 异步刷盘到数据文件
  3. 如果在刷盘前崩溃 → 数据丢失! ❌

WAL 方式(先写日志):
  1. 修改 Buffer Pool 中的数据页
  2. 立即写入 Redo Log(顺序 I/O,很快)
  3. 异步刷盘到数据文件
  4. 如果崩溃 → 根据 Redo Log 恢复 ✅

InnoDB Redo Log 实现:

cpp
// storage/innobase/log/log0log.cc

/**
 * 事务提交时刷新 Redo Log
 */
void trx_commit(trx_t* trx)
{
  // 1. 生成 Redo Log 记录
  mtr_t mtr;
  mtr_start(&mtr);
  
  // 记录所有修改
  for each modified page in transaction {
    log_write_redo_record(&mtr, page_id, old_value, new_value);
  }
  
  // 2. 强制刷 Redo Log 到磁盘(fsync)
  log_buffer_flush_to_disk();
  
  // 3. 标记事务为已提交
  trx->state = TRX_STATE_COMMITTED;
  
  // 4. 数据页可以异步刷盘(不需要立即 fsync)
  buf_flush_page_async(modified_pages);
}

/**
 * 崩溃恢复时重做 Redo Log
 */
void recovery_redo()
{
  // 1. 扫描 Redo Log
  log_scan_from_checkpoint();
  
  // 2. 重做所有已提交事务的修改
  while (has_next_redo_record()) {
    redo_rec = read_next_redo_record();
    
    // 应用到数据页
    apply_redo_record(redo_rec);
  }
  
  // 3. 回滚未提交事务(使用 Undo Log)
  recovery_undo();
}

Redo Log vs Undo Log:

特性Redo LogUndo Log
作用重做已提交事务回滚未提交事务
保证持久性(D)原子性(A)
内容物理日志(某页的某偏移从A改为B)逻辑日志(某行的某列从A改为B)
写入时机事务提交时强制刷盘数据修改时立即记录
清理时机Checkpoint 后可覆盖事务提交且无其他事务需要时可清理
文件格式循环写入的固定大小文件存储在表空间中

ACID 的综合示例 ​

完整事务流程 ​

sql
-- 电商下单事务
START TRANSACTION;

-- 1. 创建订单
INSERT INTO orders (order_id, user_id, amount, status) 
VALUES (1001, 100, 299.00, 0);

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

-- 3. 扣减余额
UPDATE users SET balance = balance - 299.00 WHERE id = 100 AND balance >= 299.00;

-- 4. 添加订单明细
INSERT INTO order_items (order_id, product_id, quantity, price) 
VALUES (1001, 50, 1, 299.00);

COMMIT;

ACID 保障过程:

执行阶段:
  1. 每条 SQL 执行时:
   - 记录 Undo Log(用于回滚)
   - 记录 Redo Log(用于恢复)
   - 检查约束(保证一致性)
   - 获取锁(保证隔离性)

提交阶段:
  2. COMMIT 时:
   - 强制刷 Redo Log 到磁盘(保证持久性)
   - 释放锁
   - 标记事务为已提交

异常处理:
  3. 如果任何步骤失败:
   - 根据 Undo Log 回滚所有操作(保证原子性)
   - 释放锁
   - 返回错误

崩溃恢复:
  4. 如果提交后、刷盘前崩溃:
   - 重启时扫描 Redo Log
   - 重做已提交事务(保证持久性)
   - 回滚未提交事务(保证原子性)

ACID 的性能权衡 ​

CAP 定理的启示 ​

分布式系统中,无法同时满足:
- C: Consistency(一致性)
- A: Availability(可用性)
- P: Partition Tolerance(分区容错性)

传统关系型数据库:
- 选择 CP(一致性 + 分区容错性)
- 牺牲部分可用性(锁等待、事务阻塞)

NoSQL 数据库(如 Cassandra):
- 选择 AP(可用性 + 分区容错性)
- 牺牲强一致性(最终一致性)

性能优化策略 ​

1. 降低隔离级别

sql
-- 从高隔离到低隔离,性能提升
SERIALIZABLE → REPEATABLE READ → READ COMMITTED → READ UNCOMMITTED

-- 大多数 OLTP 系统使用 READ COMMITTED 或 REPEATABLE READ

2. 缩短事务长度

sql
-- ❌ 长事务
START TRANSACTION;
SELECT ...;  -- 耗时查询
UPDATE ...;
INSERT ...;
COMMIT;

-- ✅ 短事务
START TRANSACTION;
UPDATE ...;
INSERT ...;
COMMIT;

3. 批量操作

sql
-- ❌ 逐行提交
FOR i IN 1..10000 LOOP
  START TRANSACTION;
  INSERT INTO ...;
  COMMIT;
END LOOP;

-- ✅ 批量提交
START TRANSACTION;
FOR i IN 1..10000 LOOP
  INSERT INTO ...;
END LOOP;
COMMIT;

4. 合理配置 Redo Log

sql
-- MySQL 配置
SHOW VARIABLES LIKE 'innodb_log_file%';

-- innodb_log_file_size: Redo Log 文件大小
-- - 越大越好(减少 checkpoint 频率)
-- - 但恢复时间更长

-- innodb_flush_log_at_trx_commit: 刷盘策略
-- - 1: 每次提交都 fsync(最安全,默认)
-- - 2: 每秒 fsync 一次(性能更好,可能丢失1秒数据)
-- - 0: 由 OS 决定何时刷盘(最快,崩溃可能丢失数据)

不同数据库的 ACID 实现 ​

MySQL InnoDB ​

原子性: Undo Log
一致性: 约束检查 + Redo/Undo 恢复
隔离性: MVCC + 锁
持久性: Redo Log (WAL)

PostgreSQL ​

原子性: MVCC(多版本) + WAL
一致性: 约束检查 + WAL 恢复
隔离性: MVCC(快照隔离)
持久性: WAL (Write Ahead Log)

特点:
- 使用 MVCC 实现原子性和隔离性
- 不需要 Undo Log,旧版本直接在数据页中

Oracle ​

原子性: Undo Segments
一致性: 约束检查 + SCN(System Change Number)
隔离性: MVCC(基于 Undo)
持久性: Redo Log

特点:
- 只支持 READ COMMITTED 和 SERIALIZABLE
- 不支持 REPEATABLE READ

SQL Server ​

原子性: Transaction Log
一致性: 约束检查 + Transaction Log 恢复
隔离性: 锁机制(默认) + SNAPSHOT(可选)
持久性: Transaction Log (WAL)

特点:
- 默认使用锁机制实现隔离性
- 可选择启用 SNAPSHOT ISOLATION(MVCC)

实际案例 ​

案例一:银行转账的 ACID 保障 ​

sql
-- 完整的转账存储过程
DELIMITER $$
CREATE PROCEDURE transfer_money(
  from_account INT,
  to_account INT,
  amount DECIMAL(10, 2)
)
BEGIN
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK;  -- 原子性:失败回滚
    RESIGNAL;
  END;
  
  START TRANSACTION;
  
  -- 一致性:检查余额充足
  IF (SELECT balance FROM accounts WHERE id = from_account) < amount THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不足';
  END IF;
  
  -- 隔离性:锁定账户(防止并发修改)
  SELECT balance FROM accounts WHERE id = from_account FOR UPDATE;
  SELECT balance FROM accounts WHERE id = to_account FOR UPDATE;
  
  -- 执行转账
  UPDATE accounts SET balance = balance - amount WHERE id = from_account;
  UPDATE accounts SET balance = balance + amount WHERE id = to_account;
  
  -- 持久性:提交事务
  COMMIT;
END$$
DELIMITER ;

-- ACID 保障:
-- A: 要么两个账户都更新,要么都不更新
-- C: 转账前后,总金额不变
-- I: 并发转账不会相互干扰
-- D: 提交后,即使断电也不会丢失

案例二:电商超卖问题的 ACID 分析 ​

sql
-- 问题:库存扣减不一致

-- 错误实现(缺少隔离性)
START TRANSACTION;

-- 步骤1:查询库存
SELECT stock FROM products WHERE id = 1;
-- 读到: stock = 10

-- 此时,其他 10 个事务也读到 stock = 10

-- 步骤2:扣减库存
UPDATE products SET stock = stock - 1 WHERE id = 1;

COMMIT;

-- 结果:10 个事务都成功,stock 变为 0
-- 但实际卖出 10 件,库存只有 10 件 → 超卖!❌

-- 正确实现(保证隔离性)
START TRANSACTION;

-- 当前读 + 排他锁
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 读到: stock = 10
-- 其他事务的 SELECT ... FOR UPDATE 被阻塞

UPDATE products SET stock = stock - 1 WHERE id = 1;

COMMIT;
-- 下一个事务才能继续

-- ACID 保障:
-- I: 隔离性保证并发安全
-- C: 一致性保证库存不为负

最佳实践 ​

  1. 理解 ACID 的代价
  • 强一致性 = 低并发
  • 根据业务需求选择合适的隔离级别
  1. 保持事务简短
  • 事务越短,锁持有时间越短
  • 减少冲突和死锁概率
  1. 避免在事务中进行耗时操作
  • 不要在事务中调用外部 API
  • 不要进行复杂的计算
  1. 合理配置持久性
  • 金融系统:innodb_flush_log_at_trx_commit = 1
  • 日志系统:innodb_flush_log_at_trx_commit = 2
  1. 监控长事务
sql
SELECT * FROM information_schema.INNODB_TRX
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60;
  1. 测试异常场景
  • 模拟崩溃恢复
  • 验证数据一致性
  • 确保回滚正确

关联术语 ​

  • [[MVCC]]
  • [[事务隔离级别]]
  • [[Redo Log]]
  • [[Undo Log]]
  • [[死锁]]

参考资料 ​

  • SQL 标准: ISO/IEC 9075-2:2016
  • MySQL 官方文档: ACID Compliance
  • InnoDB 源码: storage/innobase/trx/trx0trx.cc
  • 《高性能 MySQL》第 7 章:事务与锁
  • PostgreSQL 文档: ACID

Released under MIT License.