PostgreSQL 教程

PostgreSQL 锁机制

锁机制(Locking mechanisms)是管理数据库系统中数据的并发访问的关键。当多个事务同时执行时,它们能确保数据的完整性和一致性。如果没有适当的并发控制,事务之间可能会相互干扰,导致数据损坏或产生不一致的结果。

本章将全面介绍 PostgreSQL 中可用的不同类型的锁,以及它们如何在多用户环境中帮助维护数据完整性。我们将探索各种锁的类型、它们的兼容性,以及它们如何影响事务的运行行为。

1. 理解 PostgreSQL 中的锁

锁是 PostgreSQL 并发控制的基础环节。它们是一种防御机制,用于防止多个事务以可能导致数据不一致的方式访问同一数据。

当一个事务获取了某个数据库对象(例如:表、行或页面)上的锁时,其他试图访问该对象的事务可能会被阻塞(排队等待),具体取决于所持有的锁类型以及正在执行的操作。

2. 锁的类型

PostgreSQL 提供了多种类型的锁,每种锁的严格程度各不相同。理解这些锁类型对于设计能够高效处理并发的应用程序来说必不可少。以下是几种核心锁类型的详细解析:

  • ACCESS SHARE(访问共享锁): 这是限制最少的锁级别。多个事务可以同时对同一张表持有 ACCESS SHARE 锁。通常在读取表数据时(例如执行 SELECT 语句期间)获取此锁。它仅与 ACCESS EXCLUSIVE 锁发生冲突。
  • ROW SHARE(行共享锁): 当使用 SELECT ... FOR UPDATE/SHARE 选取行以进行更新时,会获取此锁。它允许并发读取,但会阻止其他事务获取 EXCLUSIVEACCESS EXCLUSIVE 锁,同时也阻止其他并发的 SELECT ... FOR UPDATE/SHARE 锁定相同的行。
  • ROW EXCLUSIVE(行排他锁): 类似于 ROW SHARE,但它还会阻止其他事务对相同行获取 ROW SHAREROW EXCLUSIVE 锁。当执行更新(Update)、插入(Insert)或删除(Delete)操作时,会自动获取此锁。
  • SHARE UPDATE EXCLUSIVE(共享更新排他锁): 此锁用于防止并发的表结构更改(Schema changes)和 VACUUM FULL 操作。它比 SHARE 锁的限制要少,但会阻塞写入(WRITE)访问。
  • SHARE(共享锁): 此锁允许并发读取,但会阻止写入。通常在并发创建索引等操作期间获取。
  • SHARE ROW EXCLUSIVE(共享行排他锁): 允许并发读取,但禁止其他事务修改表。此锁模式保护表免受并发数据更改的影响,并且它是自我排他的,这意味着在同一时间只能有一个会话持有它。
  • EXCLUSIVE(排他锁): 此锁只允许一个事务访问表。通常在执行诸如 REFRESH MATERIALIZED VIEW CONCURRENTLY(并发刷新物化视图)等操作期间获取。它会阻塞其他事务的读写操作。
  • ACCESS EXCLUSIVE(访问排他锁): 这是限制最严格的锁级别。它会阻止所有其他事务以任何方式访问该表。通常在执行删除表(Drop table)或执行全表重写等操作期间获取。

3. 锁兼容性矩阵

不同锁类型之间的兼容性是理解数据库如何管理并发的关键。锁兼容性矩阵展示了哪些锁类型可以同时存在于同一个资源上。以下是一个简化的兼容性矩阵:

锁类型请求 / 当前持有锁ACCESS SHAREROW SHAREROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE
ACCESS SHAREYesYesYesYesYesYesNoNo
ROW SHAREYesYesNoYesYesNoNoNo
ROW EXCLUSIVEYesNoNoNoNoNoNoNo
SHARE UPDATE EXCLUSIVEYesYesNoNoNoNoNoNo
SHAREYesYesNoNoNoNoNoNo
SHARE ROW EXCLUSIVEYesNoNoNoNoNoNoNo
EXCLUSIVENoNoNoNoNoNoNoNo
ACCESS EXCLUSIVENoNoNoNoNoNoNoNo
  • Yes 表示这些锁类型是兼容的,可以同时在同一资源上持有。
  • No 表示这些锁类型不兼容,请求第二种锁类型的事务将被阻塞,直到第一种锁被释放。

4. 隐式锁定 vs. 显式锁定

PostgreSQL 同时使用隐式和显式锁定机制。

  • 隐式锁定 (Implicit Locking): 数据库系统会根据正在执行的 SQL 命令自动获取锁。例如,当你更新一行数据时,PostgreSQL 会自动在该行上获取一个 ROW EXCLUSIVE 锁。
  • 显式锁定 (Explicit Locking): 你可以使用 LOCK TABLE 命令手动获取锁。这使你能够更精确地控制锁定行为。
LOCK TABLE my_table IN SHARE MODE;

这条语句会在 my_table 表上获取一个 SHARE 锁,在持有该锁期间,防止其他事务修改这张表。

4.1 锁的持续时间

锁的持有时间取决于事务的隔离级别以及正在执行的操作类型。通常情况下,在事务期间获取的锁会被一直持有,直到该事务被提交 (COMMIT)回滚 (ROLLBACK) 为止。

5. 锁的实战示例

让我们通过一些实际例子来演示 PostgreSQL 中的锁是如何工作的。

5.1 示例 1:并发更新

假设我们有一个名为 accounts 的表,结构如下:

CREATE TABLE accounts (
    id SERIAL PRIMARY KEY,
    balance DECIMAL(15, 2) NOT NULL
);
INSERT INTO accounts (balance) VALUES (1000.00), (500.00);

现在,考虑有两个事务试图同时更新同一个账户。

事务 1:

BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
-- ... 其他操作 ...
COMMIT;

事务 2:

BEGIN;
UPDATE accounts SET balance = balance + 50.00 WHERE id = 1;
-- ... 其他操作 ...
COMMIT;

如果没有适当的锁机制,这两个事务可能会互相干扰,导致“丢失更新”。但是,PostgreSQL 会在被更新的行上自动获取一个 ROW EXCLUSIVE 锁。因此,如果事务 1 先开始,事务 2 将被阻塞(排队),直到事务 1 提交或回滚。这确保了最终的余额计算是绝对正确的。

5.2 示例 2:使用显式锁定

考虑这样一个场景:你需要对一张表执行一系列操作,并希望在此过程中防止任何其他事务修改该表。你可以使用显式锁定来实现:

BEGIN;
LOCK TABLE accounts IN EXCLUSIVE MODE;
-- 对 accounts 表执行一系列操作
UPDATE accounts SET balance = balance * 1.05; -- 计算利息
-- ... 其他操作 ...
COMMIT;

在这个例子中,LOCK TABLE 命令在 accounts 表上获取了一个 EXCLUSIVE(排他)锁,阻止任何其他事务对该表进行读写,直到当前事务提交或回滚。

5.3 示例 3:死锁(Deadlock)场景

当两个或多个事务无限期地被阻塞,都在等待对方释放锁时,就会发生死锁。看下面这个场景:

事务 1:

BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
UPDATE accounts SET balance = balance + 50.00 WHERE id = 2;
COMMIT;

事务 2:

BEGIN;
UPDATE accounts SET balance = balance + 50.00 WHERE id = 2;
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
COMMIT;

如果事务 1 获取了 id = 1 的行的锁,而事务 2 获取了 id = 2 的行的锁;接着,每个事务都试图去获取已经被对方锁定的那行数据的锁,死锁就诞生了。不过别太担心,PostgreSQL 会自动检测并解决死锁,它会强制回滚其中一个事务,以打破僵局。