数据库的路上

从数据库源码角度解析事务的ACID四大特性

事务的ACID特性(原子性、一致性、隔离性、持久性)是关系型数据库的核心基石。本文将以PostgreSQL为切入点,通过源码分析和调试示例,深入解读ACID特性在数据库底层的实现机制。我们将以PostgreSQL数据库为例,结合具体的SQL语句和调试过程,带您一窥事务背后的神秘世界。


一、原子性(Atomicity):要么全成功,要么全失败

1.1 原理概述

原子性要求事务中的所有操作要么全部成功提交,要么全部回滚。PostgreSQL通过预写式日志(WAL, Write-Ahead Logging)和回滚段(Undo Log)实现这一特性。事务提交前,所有修改必须先写入WAL日志,确保在崩溃后能通过日志恢复。

1.2 源码实现

关键文件:src/backend/access/transam/xact.c
核心函数:CommitTransaction()AbortTransaction()

示例SQL:

CREATE TABLE accounts (
    id SERIAL PRIMARY KEY,
    balance NUMERIC(10, 2) NOT NULL
);

-- 初始化两个账户,初始余额均为 500
INSERT INTO accounts (balance) VALUES (500.00);
INSERT INTO accounts (balance) VALUES (500.00);

-- 开启事务
BEGIN;

-- 更新账户余额(模拟转账)
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- 提交事务
COMMIT;

--回滚事务
ROLLBACK;

调试过程:

要么全成功
  1. xact.c中设置断点:
    // 源码位置:src/backend/access/transam/xact.c
    static void CommitTransaction(void)
    {
        // 提交事务前,将所有修改写入WAL日志
        RecordTransactionCommit();
    }
    

代码运行到提交事务阶段

未提交前数据情况

提交后数据情况 ProcArrayEndTransaction()函数具体功能是通知系统当前某个事务已经结束

此时事务已经提交完成了。查看数据发现两个sql都成功了。

要么全失败
 static void AbortTransaction(void)
 {
     // 回滚事务,恢复数据到事务开始前的状态
     RecordTransactionAbort();
 }
 

恢复测试数据并模拟全失败的sql

BEGIN;

-- 更新账户余额(模拟转账)
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

ROLLBACK;

执行sql

开始rollback

查询rollback 之前的数据,没有发生改变

rollback 事务完成操作


二、一致性(Consistency):数据始终合法

2.1 原理概述

一致性要求事务执行前后,数据库必须从一个合法状态转换到另一个合法状态。 一致性体现在多个方面,是通过数据库的约束检查、事务的原子性、隔离性和持久性共同保证的,确保数据始终满足业务规则和完整性要求。

PostgreSQL通过约束检查(如主键、外键、唯一性约束)和触发器确保一致性。

这里我们演示通唯一性约束实现一致性。

2.2 源码实现

PostgreSQL 中的 UNIQUE 约束是通过唯一性索引(UNIQUE index)实现的。 在执行 INSERT 或 UPDATE 操作时,会调用 heap_insert() 或 heap_update()。 如果字段上有唯一性约束,就会存在一个对应的唯一性索引。 插入元组时,系统会调用 index_insert() 向索引插入记录。 在 nbtinsert.c 中的 _bt_doinsert() 函数会进一步调用 _bt_check_unique() 来判断是否违反唯一性约束。

示例SQL:

-- 创建带有唯一性约束的表
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE
);

-- 开启事务
BEGIN;

-- 插入数据(第一次成功,第二次失败)
INSERT INTO users (email) VALUES ('user@example.com');
INSERT INTO users (email) VALUES ('user@example.com');  -- 违反唯一性约束

-- 提交事务
COMMIT;

调试过程:

  1. _bt_doinsert中设置断点:

检测到冲突,最终会回滚掉该事务。

三、隔离性(Isolation):并发事务互不干扰

3.1 原理概述

隔离性要求事务之间相互隔离,防止并发操作导致的数据不一致。PostgreSQL通过多版本并发控制(MVCC)锁机制实现不同隔离级别(如READ COMMITTEDREPEATABLE READSERIALIZABLE)。

3.2 源码实现

关键文件:src/backend/access/heap/heapam.c
核心函数:heap_lock_tuple()MultiXactId

示例SQL:

-- 事务A:读取数据
BEGIN;
SELECT * FROM accounts WHERE id = 1;
-- 事务B:更新数据
BEGIN;
UPDATE accounts SET balance = 500 WHERE id = 1;
COMMIT;
-- 事务A:再次读取数据(隔离级别分别为READ COMMITTED)
SELECT * FROM accounts WHERE id = 1;
COMMIT;

调试过程:

READ COMMITTED 隔离级别
  1. 事务A:读取数据
  2. 事务B:更新数据

HeapTupleSatisfiesUpdate()函数是具体实现MVCC的重要逻辑,返回TM_OK,表示可以正常update操作,无需获取锁。

  1. 事务B 提交完数据。

  2. 事务A 查看最新数据

四、持久性(Durability):提交后永不丢失

4.1 原理概述

持久性要求事务提交后,修改永久保存。PostgreSQL通过WAL日志刷盘fsync机制确保数据不会因系统崩溃而丢失。

4.2 源码实现

关键文件:src/backend/access/transam/xlog.c
核心函数:XLogFlush()fdatasync

示例SQL:

-- 更新数据并提交
BEGIN;
UPDATE accounts SET balance = 1000 WHERE id = 1;
COMMIT;

调试过程:

  1. 执行sql

2.提交事务的过程中,会将wal日志写入系统,保证事务持久性。


五、总结

通过PostgreSQL源码的深入分析,我们发现ACID特性的实现并非“魔法”,而是通过精心设计的机制(如WAL日志、MVCC、锁管理)和严格的代码逻辑保障的。以下是关键点总结:

  1. 原子性:依赖WAL日志和回滚机制,确保事务的“全有或全无”。
  2. 一致性:通过约束检查和触发器维护数据合法性。
  3. 隔离性:利用MVCC和锁机制实现不同隔离级别的并发控制。
  4. 持久性:通过WAL日志刷盘和fsync确保数据永久保存。

理解这些底层原理不仅能帮助开发者更好地使用数据库,还能在调优和故障排查时提供有力支持。下次当你执行一个简单的COMMIT时,不妨想象它背后复杂的代码逻辑和严谨的设计哲学。