简介
在本实验中,你将了解 PostgreSQL 事务管理,这是确保数据完整性的关键环节。你将学习如何开始和提交事务,把一系列操作作为一个完整的工作单元处理。你还将学习如何回滚失败的事务、设置隔离级别,以及通过并发更新模拟锁。
开始并提交事务
本步骤将介绍如何在 PostgreSQL 中开始并提交事务。事务会把一系列操作作为一个完整的工作单元处理,从而确保数据完整性。如果事务中的任何操作失败,整个事务都会回滚,避免只更新一部分数据并保持数据一致性。
首先,以 postgres 用户身份连接 PostgreSQL 数据库。打开终端并运行以下命令:
sudo -u postgres psql
此时应看到 postgres=# 提示符。
接下来,创建一个名为 accounts 的表,用于演示事务。
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
name VARCHAR(50),
balance DECIMAL(10, 2)
);
向 accounts 表中插入一些初始数据。
INSERT INTO accounts (name, balance) VALUES ('Alice', 100.00);
INSERT INTO accounts (name, balance) VALUES ('Bob', 50.00);
现在,使用 BEGIN 命令开始一个事务。
BEGIN;
在事务中,从 Alice 的账户向 Bob 的账户转入 $20。
UPDATE accounts SET balance = balance - 20.00 WHERE name = 'Alice';
UPDATE accounts SET balance = balance + 20.00 WHERE name = 'Bob';
要使这些更改永久生效,请使用 COMMIT 命令提交事务。
COMMIT;
查询 accounts 表,确认事务是否成功。
SELECT * FROM accounts;
你应该能看到 Alice 的余额减少了 $20,Bob 的余额增加了 $20。

最后,退出 psql shell。
\q
回滚失败的事务
本步骤将介绍如何在 PostgreSQL 中回滚失败的事务。当一系列数据库操作中出现错误时,回滚事务至关重要,它可以确保数据库保持一致状态。
你应该仍然连接着上一步使用的 PostgreSQL 数据库。如果已经断开连接,请重新运行以下命令:
sudo -u postgres psql
开始一个新事务。
BEGIN;
在这个事务中,尝试执行一个会故意失败的操作:插入重复的主键。先查询下一个可用的 id 值。
SELECT MAX(id) FROM accounts;
假设查询结果为 2。现在,尝试插入一条 id = 1 的新账户记录,因为这个 ID 已经存在,所以插入会失败。
INSERT INTO accounts (id, name, balance) VALUES (1, 'Eve', 25.00);
运行这条命令会出现 ERROR: duplicate key value violates unique constraint "accounts_pkey" 错误。
事务中出现错误后,使用 ROLLBACK 命令回滚事务,丢弃事务中所做的更改。
ROLLBACK;
查询 accounts 表,确认 ROLLBACK 是否成功。
SELECT * FROM accounts;
你应该能看到,表中仍然只有 Alice 和 Bob 的账户,余额与步骤 1 结束时相同。失败的 INSERT 操作已成功回滚。

最后,退出 psql shell。
\q
设置并测试隔离级别
本步骤将介绍 PostgreSQL 中的事务隔离级别,以及如何设置和测试它们。隔离级别决定了并发事务之间相互隔离的程度。较高的隔离级别能更好地防止数据损坏,但可能会降低并发性。
你应该仍然连接着上一步使用的 PostgreSQL 数据库。如果已经断开连接,请重新运行以下命令:
sudo -u postgres psql
打开两个独立的终端窗口。在每个终端中,都以 postgres 用户身份连接 PostgreSQL 数据库。此时应该能看到两个 postgres=# 提示符。
终端 1:
sudo -u postgres psql
终端 2:
sudo -u postgres psql
在终端 1中开始一个事务,然后将隔离级别设为 READ COMMITTED(默认级别)。SET TRANSACTION 必须在事务中、执行第一条查询之前运行。
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
在终端 1中查询 Alice 的余额。
SELECT balance FROM accounts WHERE name = 'Alice';
记下查询结果。接着,在终端 2中开始一个事务并更新 Alice 的余额。
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
UPDATE accounts SET balance = 90.00 WHERE name = 'Alice';
COMMIT;
回到终端 1,再次查询 Alice 的余额。
SELECT balance FROM accounts WHERE name = 'Alice';
由于隔离级别是 READ COMMITTED,你会看到终端 2 已提交的更新后余额(90.00)。

现在测试 REPEATABLE READ 隔离级别。在终端 1中,先回滚当前事务,再开始一个新事务,并在读取任何数据之前设置隔离级别。
ROLLBACK;
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
在终端 1中再次查询 Alice 的余额。
SELECT balance FROM accounts WHERE name = 'Alice';
记下查询结果。接着,在终端 2中开始一个事务,再次更新 Alice 的余额。
BEGIN;
UPDATE accounts SET balance = 100.00 WHERE name = 'Alice';
COMMIT;
回到终端 1,再次查询 Alice 的余额。
SELECT balance FROM accounts WHERE name = 'Alice';
由于隔离级别是 REPEATABLE READ,你仍会看到 90.00。这是该事务中第一条查询建立的快照所记录的余额,即使终端 2 已提交了 100.00 也一样。
最后,在终端 1中提交事务。
COMMIT;
现在,在终端 1中再次查询 Alice 的余额。事务结束后,你会看到最新提交的值(100.00)。
SELECT balance FROM accounts WHERE name = 'Alice';
退出两个 psql shell。
\q
通过并发更新模拟锁
本步骤将通过 PostgreSQL 中的并发更新模拟锁。锁用于防止并发事务相互干扰,从而确保数据完整性。
你应该仍然连接着上一步使用的 PostgreSQL 数据库。如果已经断开连接,请重新运行以下命令:
sudo -u postgres psql
打开两个独立的终端窗口。在每个终端中,都以 postgres 用户身份连接 PostgreSQL 数据库。此时应该能看到两个 postgres=# 提示符。
终端 1:
sudo -u postgres psql
终端 2:
sudo -u postgres psql
在终端 1中开始一个事务并查询 Alice 的余额。关键是使用 SELECT ... FOR UPDATE 锁定该行。
BEGIN;
SELECT balance FROM accounts WHERE name = 'Alice' FOR UPDATE;
这条命令会读取 Alice 的余额并锁定对应的行。在该事务提交或回滚之前,其他事务无法修改这一行。
在终端 2中开始一个事务,并尝试更新 Alice 的余额。
BEGIN;
UPDATE accounts SET balance = balance + 10 WHERE name = 'Alice';
注意,终端 2中的命令看起来会一直卡住。这是因为它正在等待终端 1释放持有的行锁。
现在,在终端 1中提交事务。
COMMIT;
终端 1提交事务后,终端 2中的 UPDATE 命令就会继续执行。
在终端 2中提交事务。
COMMIT;
现在,在任一终端中查询 accounts 表,确认更改。
SELECT * FROM accounts;
你应该能看到,终端 1 释放行锁后,终端 2 中的事务更新了 Alice 的余额。

最后,退出两个 psql shell。
\q
这个示例展示了如何使用 SELECT ... FOR UPDATE 模拟锁,防止并发更新相互干扰。如果不使用锁,两个事务可能会读取相同的初始余额,并基于该值分别应用更新,从而导致更新丢失。
总结
在本实验中,你学习了如何管理 PostgreSQL 事务。首先,你使用 psql 连接 PostgreSQL 数据库,并创建了一个包含初始数据的示例 accounts 表。
接着,你通过示例了解了事务的用法:使用 BEGIN 命令开始事务,执行多项数据库操作(更新 Alice 和 Bob 的余额),然后使用 COMMIT 命令提交事务,使更改永久生效。这说明了事务的基本原则:将一系列操作作为一个原子单元处理。你还学习了如何回滚事务和设置隔离级别。最后,你通过并发更新模拟锁,了解如何防止数据损坏。



