1. 什么是ACID?
ACID是数据库事务的四个核心特性,是保证数据可靠性和一致性的基石。这四个字母分别代表:
- 原子性(Atomicity):事务是"不可分割的最小单位",要么完全执行(Commit),要么完全不执行(Rollback);
- 一致性(Consistency):事务执行前后,数据库始终处于"合法状态"(满足所有约束条件);
- 隔离性(Isolation):多个并发事务之间"互不干扰",每个事务都像独自在数据库中运行;
- 持久性(Durability) :事务提交后,修改会永久保存,即使系统崩溃也不会丢失。
PostgreSQL作为关系型数据库的佼佼者,通过事务日志(WAL) 、MVCC(多版本并发控制) 、锁机制等技术,完整实现了ACID特性。
2. 原子性:要么全做,要么全不做
原子性是事务的"灵魂"------它保证了复杂操作的"整体性"。比如转账业务中,"扣减A账户余额"和"增加B账户余额"必须同时成功或同时失败,不能出现"钱扣了但没到账"的情况。
2.1 事务的开始与结束
PostgreSQL中,事务通过BEGIN
(或START TRANSACTION
)开启,COMMIT
提交(确认修改),ROLLBACK
回滚(撤销所有修改):
sql
-- 开启事务
BEGIN;
-- 执行操作:扣减A账户200元
UPDATE accounts SET balance = balance - 200 WHERE id = 1;
-- 执行操作:增加B账户200元
UPDATE accounts SET balance = balance + 200 WHERE id = 2;
-- 提交事务(修改生效)
COMMIT;
如果中间任何一步出错(比如B账户不存在),PostgreSQL会自动回滚整个事务,所有修改都会被撤销。
2.2 回滚与保存点(Savepoint)
当事务需要"部分回滚"时,可以用保存点(Savepoint)。比如,在一个长事务中,你可能想尝试某个操作,失败后只回滚该操作,而保留之前的修改:
sql
BEGIN;
-- 第一步:扣减A账户200元(成功)
UPDATE accounts SET balance = balance - 200 WHERE id = 1;
-- 创建保存点sp1
SAVEPOINT sp1;
-- 第二步:尝试给C账户转200元(失败,C账户不存在)
UPDATE accounts SET balance = balance + 200 WHERE id = 3;
-- 回滚到保存点sp1(仅撤销第二步)
ROLLBACK TO sp1;
-- 调整操作:给B账户转200元(成功)
UPDATE accounts SET balance = balance + 200 WHERE id = 2;
-- 提交事务(最终A扣200,B加200)
COMMIT;
保存点的作用是将原子性"细分",但整个事务依然保持原子性------只有COMMIT
后,所有修改才会永久生效。
2. 一致性:从"合法状态"到"合法状态"
一致性是指事务执行前后,数据库必须满足所有"数据完整性约束"。这些约束包括:
- 主键约束(Primary Key):确保每行数据的唯一性;
- 外键约束(Foreign Key) :确保关联表的数据一致性(比如订单表的
user_id
必须存在于用户表); - Check约束 :确保字段值符合自定义规则(比如
balance >= 0
); - 非空约束(NOT NULL):确保字段不存空值。
2.1 约束的"强制检查"
PostgreSQL会在事务执行每个修改操作 时自动检查约束。比如,当你尝试插入一条balance=-100
的记录时:
sql
-- 尝试插入非法数据(balance为负)
INSERT INTO accounts (user_id, balance) VALUES (3, -100);
PostgreSQL会立即抛出错误:
sql
ERROR: check constraint "accounts_balance_check" violated by row
此时事务会进入"终止状态",必须通过ROLLBACK
撤销所有修改,才能继续操作。
2.2 一致性的"双重保障"
一致性是应用层+数据库层共同作用的结果:
- 应用层:在数据进入数据库前,先验证合法性(比如前端检查"转账金额不能为负");
- 数据库层:通过约束强制拦截非法数据,避免"脏数据"进入系统。
3. 隔离性:并发事务的"互不干扰"
当多个用户同时操作数据库时,隔离性保证每个事务"看不到"其他事务的中间状态。PostgreSQL通过隔离级别和**MVCC(多版本并发控制)**实现这一特性。
3.1 隔离级别的"四种境界"
PostgreSQL支持四种隔离级别(从弱到强):
隔离级别 | 脏读(Dirty Read) | 不可重复读(Non-repeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
读未提交(Read Uncommitted) | 允许 | 允许 | 允许 |
读已提交(Read Committed) | 禁止 | 允许 | 允许 |
可重复读(Repeatable Read) | 禁止 | 禁止 | 禁止 |
串行化(Serializable) | 禁止 | 禁止 | 禁止 |
注意 :PostgreSQL的默认隔离级别是读已提交(Read Committed),这是"性能与一致性的平衡选择"。
3.2 隔离级别的"实战演示"
我们用一个简单的例子,看不同隔离级别的行为差异:
场景:查询账户余额
假设accounts
表中有一条记录:id=1, balance=1000
。
(1)读已提交(默认)
sql
-- 事务A(读已提交)
BEGIN;
-- 第一次查询:得到1000
SELECT balance FROM accounts WHERE id=1;
-- 此时事务B执行:UPDATE accounts SET balance=1500 WHERE id=1; COMMIT;
-- 第二次查询:得到1500(不可重复读)
SELECT balance FROM accounts WHERE id=1;
COMMIT;
结论:读已提交级别下,每个语句都会读取"最新的已提交数据",因此会出现"不可重复读"。
(2)可重复读
sql
-- 事务A(可重复读)
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 第一次查询:得到1000(快照生成)
SELECT balance FROM accounts WHERE id=1;
-- 事务B执行UPDATE并提交
-- 第二次查询:依然得到1000(快照一致)
SELECT balance FROM accounts WHERE id=1;
COMMIT;
结论:可重复读级别下,事务会基于"开始时的快照"进行所有查询,避免了不可重复读和幻读。
(3)串行化
sql
-- 事务A(串行化)
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE id=1; -- 1000
-- 事务B尝试UPDATE accounts SET balance=1500 WHERE id=1;
-- 此时事务B会被阻塞,直到事务A提交或回滚
COMMIT;
结论:串行化级别下,事务会"串行执行",完全避免所有并发问题,但性能最低(适合对一致性要求极高的场景,比如金融交易)。
3.3 MVCC:"多版本数据"的魔法
PostgreSQL的隔离性依赖MVCC(多版本并发控制)。它的核心思想是:
- 每个修改操作都会生成"新版本数据",而不是直接覆盖旧版本;
- 事务根据"快照(Snapshot)"读取数据------快照包含事务开始时所有"已提交的数据版本";
- 旧版本数据会在"没有事务引用"时被自动清理(通过
VACUUM
)。
比如,当事务B修改balance
为1500时,PostgreSQL会:
- 保留旧版本(balance=1000);
- 生成新版本(balance=1500);
- 事务A的快照依然指向旧版本,因此看不到事务B的修改。
4. 持久性:提交后的数据"永不丢失"
持久性是指事务提交后,修改会永久保存,即使系统崩溃也不会丢失。PostgreSQL通过**WAL(Write-Ahead Logging,预写日志)**实现这一特性。
4.1 WAL的"预写原则"
WAL的核心逻辑是:所有修改必须先写入日志,再写入数据文件。具体流程如下:
- 事务执行
UPDATE
操作时,PostgreSQL先将修改内容写入WAL日志(顺序写入,速度快); - WAL日志会被同步到磁盘 (通过
fsync
调用,保证日志不丢失); - 定期将WAL中的修改"刷入"数据文件(通过
Checkpoint
机制)。
即使系统突然崩溃(比如断电),重启后PostgreSQL会:
- 读取WAL日志;
- 重放所有"已提交但未刷入数据文件"的修改;
- 回滚所有"未提交"的修改。
4.2 Checkpoint:"缩短恢复时间"的关键
Checkpoint
是PostgreSQL定期执行的"磁盘同步操作",它会:
- 将所有"脏页"(修改过但未写入数据文件的内存页)写入数据文件;
- 在WAL日志中标记一个"检查点",表示"之前的修改已安全写入磁盘"。
这样,当系统崩溃时,PostgreSQL只需要重放检查点之后的WAL日志,大大减少恢复时间。
5. 实战:用ACID保障"转账业务"
我们用一个经典的"转账场景",演示如何用ACID特性保证数据正确性:
5.1 准备工作
首先创建账户表(包含Check
约束,保证余额非负):
sql
-- 创建账户表
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
balance NUMERIC(10,2) NOT NULL CHECK (balance >= 0)
);
-- 插入测试数据:用户1有1000元,用户2有500元
INSERT INTO accounts (user_id, balance) VALUES (1, 1000.00), (2, 500.00);
5.2 转账事务
用户1给用户2转200元,事务必须保证:
- 原子性:要么"用户1扣200+用户2加200",要么都不执行;
- 一致性:转账后双方余额都非负;
- 隔离性:并发转账时,看不到对方的中间状态;
- 持久性:转账成功后,数据永久保存。
sql
-- 开启转账事务
BEGIN;
-- 步骤1:扣减用户1的余额
UPDATE accounts SET balance = balance - 200.00 WHERE user_id = 1;
-- 步骤2:增加用户2的余额
UPDATE accounts SET balance = balance + 200.00 WHERE user_id = 2;
-- 步骤3:检查是否有错误(比如用户1余额不足)
-- 如果一切正常,提交事务
COMMIT;
5.3 异常处理
如果用户1的余额不足(比如只有100元),步骤1会触发Check
约束错误:
sql
ERROR: check constraint "accounts_balance_check" violated by row
此时事务会自动终止,必须通过ROLLBACK
撤销所有修改:
sql
ROLLBACK;
6. 课后Quiz:巩固ACID知识
问题1:什么是原子性?PostgreSQL如何保证原子性?
答案 :原子性是事务要么全做要么全不做。PostgreSQL通过ROLLBACK
(回滚)和Savepoint
(保存点)实现原子性------当事务出错时,回滚所有修改;保存点允许部分回滚,但整个事务依然原子。
问题2:PostgreSQL的默认隔离级别是什么?它能避免哪些并发问题?
答案 :默认隔离级别是读已提交(Read Committed)。它能避免脏读,但不能避免不可重复读和幻读。
问题3:持久性是如何通过WAL实现的?
答案:WAL要求所有修改先写入日志(同步到磁盘),再写入数据文件。即使系统崩溃,重启后PostgreSQL会重放WAL日志中的未完成修改,保证数据不丢失。
问题4:假设你在开发一个电商系统,用户下订单时需要"扣减库存"和"创建订单",如何用事务保证原子性?
答案:将两个操作包裹在一个事务中:
sql
BEGIN;
-- 扣减库存
UPDATE products SET stock = stock - 1 WHERE id = 100;
-- 创建订单
INSERT INTO orders (user_id, product_id) VALUES (1, 100);
COMMIT;
如果任何一步出错,ROLLBACK
会撤销所有修改,避免"库存扣了但订单没创建"的情况。
7. 常见报错及解决
报错1:current transaction is aborted, commands ignored until end of transaction block
原因 :事务中某条语句出错(比如违反约束),导致事务进入"终止状态"。 解决 :执行ROLLBACK
撤销所有修改,再重新执行正确的语句。 预防:在应用层捕获错误,及时回滚事务。
报错2:could not serialize access due to read/write dependencies among transactions
原因 :使用Serializable
隔离级别时,并发事务存在"读写依赖",无法串行执行。 解决 :重试事务,或降低隔离级别到Repeatable Read
。 预防 :避免在高并发场景使用Serializable
级别。
报错3:check constraint "xxx" violated by row
原因 :修改操作违反了Check
约束(比如余额为负)。 解决 :检查数据合法性,修改后重新执行。 预防:应用层先验证数据(比如前端检查"转账金额不能为负")。
参考链接
- PostgreSQL隔离级别:www.postgresql.org/docs/17/tra...
- PostgreSQL WAL日志:www.postgresql.org/docs/17/wal...
- PostgreSQL约束:www.postgresql.org/docs/17/ddl...
余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:编程智域 前端至全栈交流与成长
,阅读完整的文章:转账不翻车、并发不干扰,PostgreSQL的ACID特性到底有啥魔法?
往期文章归档
-
PostgreSQL里的PL/pgSQL到底是啥?能让SQL从"说目标"变"讲步骤"? - cmdragon's Blog
-
PostgreSQL UPDATE语句怎么玩?从改邮箱到批量更新的避坑技巧你都会吗? - cmdragon's Blog
-
PostgreSQL 17安装总翻车?Windows/macOS/Linux避坑指南帮你搞定? - cmdragon's Blog
-
能当关系型数据库还能玩对象特性,能拆复杂查询还能自动管库存,PostgreSQL凭什么这么香? - cmdragon's Blog
-
FastAPI的死信队列处理机制:为何你的消息系统需要它? - cmdragon's Blog
免费好用的热门在线工具