PostgreSQL 教程

PostgreSQL 数据库事务

事务(Transactions)是数据库管理的基础,用于确保数据的一致性和可靠性。

本章将深入探讨事务的核心概念,重点学习如何在 PostgreSQL 中使用 BEGIN 和 COMMIT 命令来开启和结束事务。

1. 回顾事务的 ACID 特性

作为上一模块的简要回顾,事务保证了数据库操作以可靠和可预测的方式进行,并严格遵循 ACID 特性:

  • 原子性 (Atomicity):事务是一个不可分割的工作单元;其中的所有操作要么全部成功,要么全部失败。
  • 一致性 (Consistency):事务将数据库从一个有效的状态转换到另一个有效的状态。它通过遵守预定义的规则和约束来维护数据完整性。
  • 隔离性 (Isolation):事务之间是相互隔离的,这可以防止相互干扰,并确保并发(同时进行)的事务不会破坏数据。
  • 持久性 (Durability):一旦事务被提交,它所做的更改就是永久性的,即使发生系统故障也能保存下来。

2. 使用 BEGIN 命令开启事务

在 PostgreSQL 中,BEGIN 命令用于初始化(开启)一个事务。它标志着一系列应当被视为单个逻辑工作单元的操作的开始。在发出 COMMIT(提交)或 ROLLBACK(回滚)命令之前,对数据库所做的所有更改都属于这个事务的一部分,并且不会被永久保存。

2.1 语法

基本语法非常直观:

BEGIN;

可选地,你可以在事务开始时指定隔离级别 (isolation level)访问模式 (access mode)。我们将在后面的章节中详细讨论隔离级别,但现在你只需要知道,它决定了并发事务之间相互隔离的程度。访问模式则定义了该事务是只读的(read-only)还是读写的(read-write)。

BEGIN ISOLATION LEVEL level READ WRITE; -- 或者 READ ONLY (只读)

这里的 level(级别)可以是 SERIALIZABLE(可串行化)、REPEATABLE READ(可重复读)或 READ COMMITTED(读已提交)。如果没有指定 ISOLATION LEVEL 和/或 READ WRITE,数据库将使用默认的隔离级别和读写模式。

2.2 示例

让我们考虑一个简化的银行业务场景。我们有一个名为 accounts 的表,包含 account_id(账户 ID)和 balance(余额)两列。我们想要从账户 1 转账 100 美元到账户 2。

-- 开启一个新事务
BEGIN;

-- 从账户 1 扣除 100 美元
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;

-- 向账户 2 增加 100 美元
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;

-- 提交事务以保存更改
COMMIT;

在这个例子中,BEGIN 命令开启了事务。随后的 UPDATE 语句修改了 accounts 表。然而,这些更改此时还不是永久的。只有当执行了 COMMIT 命令时,它们才会被永久保存。例如,如果账户 1 的余额不足,我们将使用 ROLLBACK 命令(将在下一章讨论)来撤销这些更改。

2.3 隐式事务

在某些 PostgreSQL 客户端工具和配置中,事务可能会被隐式(自动)开启。例如,PostgreSQL 的交互式终端 psql 通常默认运行在“自动提交 (autocommit)”模式下。在自动提交模式下,每条单独的 SQL 语句都会被视为一个独立的事务,并在成功执行后自动提交。

你可以使用命令 SET AUTOCOMMIT OFF; 来禁用自动提交。当自动提交被关闭时,你必须显式地使用 BEGINCOMMIT(或 ROLLBACK)来管理事务。

3. 使用 COMMIT 命令提交事务

COMMIT 命令用于最终完成一个事务,使数据库中的所有更改成为永久性的。一旦事务被提交,这些更改就保证具备了持久性(Durability),这意味着它们能够在系统崩溃或其他故障中存活下来。

3.1 语法

提交事务的语法同样很简单:

COMMIT;

3.2 示例

继续使用刚才的银行业务场景,在执行完转账操作后,我们使用 COMMIT 来保存更改:

-- 开启一个新事务
BEGIN;

-- 从账户 1 扣除 100 美元
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;

-- 向账户 2 增加 100 美元
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;

-- 提交事务以保存更改
COMMIT;

如果 COMMIT 命令执行成功,这两个 UPDATE 操作就会被永久应用到 accounts 表中。

3.3 COMMIT 的重要性

如果没有 COMMIT 命令,在事务内部所做的任何更改都只对当前会话可见,并且不会被永久保存。这种机制确保了如果事务的任何部分失败,整个事务都可以被回滚,从而维护了数据的一致性。

4. 实战案例:电商订单处理

设想一个电商应用程序,客户在其中下达了一个订单。以下操作需要在单个事务中完成:

  1. 更新 products(商品)表,减少订购商品的库存数量。
  2. orders(订单)表中创建一条包含订单详情的新记录。
  3. order_items(订单明细)表中为订单中的每一件商品创建记录。
  4. 更新 customer_accounts(客户账户)表以反映支付扣款。
BEGIN;

-- 更新商品库存数量
UPDATE products SET quantity = quantity - 2 WHERE product_id = 101;
UPDATE products SET quantity = quantity - 1 WHERE product_id = 102;

-- 插入订单详情
INSERT INTO orders (customer_id, order_date, total_amount) VALUES (123, NOW(), 199.98);

-- 获取刚生成的订单的订单 ID
-- 注意:此示例为了演示使用了简化方法。在真实场景中,你会使用特定数据库的函数(例如 PostgreSQL 中的 RETURNING 子句)来获取新生成的订单 ID
SELECT currval(pg_get_serial_sequence('orders', 'order_id')) INTO order_id;

-- 插入订单明细
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (order_id, 101, 2, 79.99);
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (order_id, 102, 1, 39.99);

-- 更新客户账户余额
UPDATE customer_accounts SET balance = balance - 199.98 WHERE customer_id = 123;

COMMIT;

如果这些操作中有任何一个失败(例如,库存不足、支付失败),则应该使用 ROLLBACK 回滚整个事务,以防止数据出现不一致。

5. 假设场景:大学选课系统

想象一个大学的选课系统,学生在其中注册课程。一个选课事务将包含以下步骤:

  1. 检查学生是否满足该课程的先修条件。
  2. 验证该课程是否还有剩余座位。
  3. 更新学生的记录,将该课程添加进去。
  4. 更新课程的记录,反映新的报名人数。
BEGIN;

-- 检查先修条件 (简化版)
SELECT has_prerequisites FROM students WHERE student_id = 'S123';

-- 检查剩余座位 (简化版)
SELECT seats_available FROM courses WHERE course_id = 'CS101';

-- 更新学生记录
INSERT INTO student_courses (student_id, course_id) VALUES ('S123', 'CS101');

-- 更新课程记录
UPDATE courses SET seats_available = seats_available - 1 WHERE course_id = 'CS101';

COMMIT;

如果这些检查中的任何一个失败(例如,学生不满足先修条件,或者课程已满),事务就应该被回滚,以维护选课数据的完整性。