高级 #database#transaction#acid

ACID 特性

ACID 是数据库事务的四大特性——原子性(要么全做要么全不做)、一致性(数据总是正确)、隔离性(并发事务互不干扰)、持久性(提交了就永久保存)

💸 一个转账的场景——把 ACID 四个字讲透

你在学校食堂用微信支付买了一份 15 元的午饭。你的账户扣了 15 元,食堂账户收了 15 元。整个过程不到一秒。

但如果这个过程被拆开看,会发生什么?

-- 转账 15 元
BEGIN TRANSACTION;
    UPDATE accounts SET balance = balance - 15 WHERE name = '你';
    -- ⚡ 假设执行到这行时断电了
    UPDATE accounts SET balance = balance + 15 WHERE name = '食堂';
COMMIT;

你想想——如果扣钱成功但加钱没做,会发生什么?你的 15 元凭空消失了。

这就是事务(Transaction)要防止的事情。ACID 就是事务的四大保障——让这种悲剧不会发生。

🏦 贯穿案例:微信转账 100 元

下面我们会用”张三给李四微信转账 100 元”这个场景来逐一解释 A、C、I、D 四个特性。


A —— 原子性(Atomicity)

原子性(Atomicity):事务中的所有操作要么全部成功,要么全部回滚。不存在”做了一半”的状态。

BEGIN TRANSACTION;
    UPDATE accounts SET balance = balance - 100 WHERE name = '张三';
    UPDATE accounts SET balance = balance + 100 WHERE name = '李四';
COMMIT;

原子性保证:

  • ✅ 两条 UPDATE 都成功 = 转账完成
  • ✅ 任何一条 UPDATE 失败 = 两条都不生效(张三的钱退回来)
  • ❌ 不可能出现”张三扣了 100 但李四没收到”

实现方式:Undo Log(回滚日志)

数据库在执行每条 UPDATE 之前,先把”改前值”记到 Undo Log 里

Undo Log 中记录的原始值:
事务 T1: 张三的余额 1000 → 准备改成 900
事务 T1: 李四的余额 2000 → 准备改成 2100

如果中途出错或执行 ROLLBACK,数据库就根据 Undo Log 把数据恢复成原来的样子。

🎪 类比:舞台魔术的预备方案

魔术师准备把一个助手从箱子 A”变”到箱子 B(就像转账,从 A 扣钱到 B 加钱)。

如果变到一半(比如 A 的暗门打开了,但 B 的机关卡住了),魔术师必须能恢复到最初状态——让助手回到 A。这就是”回滚”。

原子性保证:要么大变活人成功完成,要么像没发生过一样。没有”人卡在中间”的选项


C —— 一致性(Consistency)

一致性(Consistency):事务执行前后,数据必须满足所有的约束和规则——数据永远是”合法的”。

一致性不仅仅是数据库的事,更是应用层的事

-- 一致性约束示例
CREATE TABLE accounts (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    balance DECIMAL(10,2) CHECK (balance >= 0)  -- 余额不能为负
);

BEGIN TRANSACTION;
    UPDATE accounts SET balance = balance - 100 WHERE name = '张三';
    -- 如果张三只有 50 元,这条会把余额改成 -50 → 违反 CHECK 约束
    -- 事务会被回滚
    UPDATE accounts SET balance = balance + 100 WHERE name = '李四';
COMMIT;

一致性的不同类型

类型维护者例子
主键唯一性数据库自动保证不能插入两个学号相同的学生
外键约束数据库自动保证成绩表中的学号必须在学生表中存在
CHECK 约束数据库自动保证年龄必须大于 0
业务规则应用程序保证”转账后总金额不变”——数据库不知道这条规则

“转账后总金额不变”这个规则怎么保证?

这不是数据库能自动检查的——数据库只看到”张三余额 -100,李四余额 +100”,它不知道这两个操作应该”抵消”。这个逻辑需要你在应用层实现:要么在代码中保证加减一致,要么用存储过程封装转账逻辑。

📒 类比:宿舍记账本

你和室友合用一个记账本记录生活费。一致性就像记完账后,总收入 - 总支出 = 总余额——必须始终成立。

如果某天有人记账时少写了一笔(比如只记了”买奶茶 -15”,但忘了记”取现金 +100”),账本就不一致了——余额对不上。

数据库的约束(主键、外键、CHECK)就像记账本的一些铁律——比如”不能有两条一模一样的记录”。但”总收入 = 总支出 + 余额”这条规则,需要记账人自己保证。


I —— 隔离性(Isolation)

隔离性(Isolation):并发执行的事务之间互不干扰——每个事务都感觉自己是”唯一在运行”的。

没有隔离性会怎样?

-- 事务 A:张三的账户总额(余额 + 理财)
BEGIN;
    SELECT balance FROM accounts WHERE name = '张三';  -- 1000
    -- ⚡ 事务 B 此时转了 500 进来
    SELECT investment FROM accounts WHERE name = '张三'; -- 2000
    -- 结果:总额 = 1000 + 2000 = 3000 ❌(少算了 B 转的 500)
COMMIT;

如果事务 A 读到了”一半”的数据(事务 B 的部分改动),结果就是错误的。

隔离性的实现

隔离性通过两种机制实现:

  1. 锁(Lock)——事务 A 写数据时,事务 B 不能同时写同一行(写互斥)
  2. MVCC(Multi-Version Concurrency Control,多版本并发控制)——事务 A 读数据时,看到的是数据的一个”快照”,不会因为事务 B 在写而被阻塞

MVCC 的原理:每个事务看到的是数据在它开始的时刻的版本——即使其他事务在修改数据,读操作看到的始终是”自己的版本”。

📚 类比:图书馆占座和借书

锁就像占座:你在图书馆用书包占了一个座位(加锁),别人就不能坐这个座位了。你走了之后(释放锁)别人才能坐。

MVCC 就像借书系统中的”借阅记录”:很多同学同时借书(并发读写),每个人看的是自己借书证上的记录(自己的快照),互不影响。即使有同学还了一本书,你看到的结果还是”那本书没还”——直到你刷新(开始新事务)。

隔离性允许”同时学习”,但不允许”抢同一本书导致混乱”。

隔离性有不同的隔离级别(从宽松到严格),默认的隔离级别不一定是最严格的——因为越严格=并发性能越差。具体区别在下一节 事务隔离级别 中详细讲。


D —— 持久性(Durability)

持久性(Durability):一旦事务提交(COMMIT),数据修改就是永久保存的——即使后续系统崩溃、断电,数据也不会丢失。

BEGIN TRANSACTION;
    UPDATE accounts SET balance = balance - 100 WHERE name = '张三';
    UPDATE accounts SET balance = balance + 100 WHERE name = '李四';
COMMIT;  -- 在这一刻,结果必须永久保存

持久性 ≠ 数据已经在磁盘上

更准确地说:持久性保证的是**“即使现在断电,重启后数据也能恢复”**。

实现方式:WAL(Write-Ahead Logging,预写日志)+ Redo Log(重做日志)

  1. 事务提交时,先写 Redo Log(记录”把张三余额改成 900”这条操作)
  2. Redo Log 写成功 = 事务提交成功(哪怕真正的数据还没写到磁盘)
  3. 系统崩溃后重启,数据库扫描 Redo Log,把所有已提交但没写盘的操作重做一遍

类比:写论文时突然断电

你在 Word 里写了 3000 字的论文,点击了”保存”——结果下一秒断电了。重新开机后,你发现论文还在!

为什么?因为 Word 的”保存”操作其实做了两件事:

  1. 先把修改写入一个隐藏的”恢复文件”(相当于 Redo Log)
  2. 恢复文件写入成功 = 保存成功
  3. 主文件可能还没写——但没关系,下次打开 Word 时,它会用恢复文件还原内容

这就是持久性的本质:日志写成功就保证数据不丢,主文件可以慢慢写。


🔗 ACID 四者的关系

ACID 不是四个独立的概念——它们紧密配合:

原子性                    +    隔离性
(所有操作要么全做         (并发事务互不干扰)
 要么全不做)
         ↓                          ↓
一致性(数据始终正确)←──────────────┘

持久性(提交就不丢)
  • 原子性 + 隔离性 → 保证一致性
  • 一致性 + 持久性 → 保证数据的可靠性

📝 小结

特性一句话实现方式
Atomicity(原子性)全做或全不做Undo Log(回滚日志)
Consistency(一致性)数据始终合法约束 + 应用逻辑
Isolation(隔离性)互不干扰锁 + MVCC
Durability(持久性)提交即永久Redo Log + WAL

🎯 思考题:假设一个事务包含 5 条 SQL,在第 3 条执行时磁盘满了。这时候数据库应该怎么做?数据库已经做了的前两条 SQL 怎么处理?

为什么先学这个? ACID 是事务的基础。但”隔离性”有不同的”力度”——太强了性能差,太弱了数据可能出错。下一步看看事务隔离级别——如何在这两者之间做权衡。