关系级锁

在访问表内的数据时,通常需要对整个表加锁。 例如使用 ALTER TABLE 修改表结构时,必须等待所有表上的 SELECTUPDATE 等语句执行完成后才能执行。 在不同的事务隔离级别下,根据操作和影响范围的不同,会加不同的锁。

锁的模式以及常见的加锁场景:

Lock ModeSQL Command(s)
Access ShareSELECT
Row ShareSELECT FOR UPDATE SKIP LOCKED
Row ExclusiveINSERT、UPDATE、DELETE
Share Update ExclusiveVACUUM、CREATE INDEX CONCURRENTLY
ShareCREATE INDEX
Share Row ExclusiveCREATE TRIGGER
ExclusiveREFRESH MATERIALIZED VIEW CONCURRENTLY
Access ExclusiveDROP、TRUNCATE、LOCK TABLE、VACUUM FULL、REFRESH MATERIALIZED VIEW

兼容性矩阵:

Lock ModeASRSRESUESSREEAE
Access ShareYYYYYYYN
Row ShareYYYYYYNN
Row ExclusiveYYYYNNNN
Share Update ExclusiveYYYNNNNN
ShareYYNNYNNN
Share Row ExclusiveYYNNNNNN
ExclusiveYNNNNNNN
Access ExclusiveNNNNNNNN

来源:https://cc.pigsty.io/blog/dev/pg-lock/

行级锁

为了提高并发性能,PostgreSQL 允许多个 UPDATE 在同一个表上并发执行。 为了保证数据的一致性,UPDATE 等操作在访问行(元组)时会在行级上再加一次锁。

锁的模式以及常见的加锁场景:

Lock ModeCommon Scenarios / Operations
Key ShareSELECT ... FOR KEY SHARE; Foreign key constraint enforcement (protects referenced keys from being updated/deleted).
ShareSELECT ... FOR SHARE; Reading a row while preventing other transactions from modifying or deleting it.
No Key UpdateUPDATE (not modifying key columns), DELETE; Row modification/deletion, may involve non-key index updates.
UpdateUPDATE (including key columns), DELETE, SELECT ... FOR UPDATE; Row modification/deletion, may involve key index updates.

兼容性矩阵:

Lock ModeKey ShareShareNo Key UpdateUpdate
Key ShareYYYN
ShareYYNN
No Key UpdateYNNN
UpdateNNNN

行级锁的实现

在 PostgreSQL 中,行级锁是通过元组头部的 t_infomaskt_infomask2 字段来实现的。 PostgreSQL 规定 t_infomaskt_infomask2 中的特定位表示 lock_onlyis_multikeys_updkeyshrshr 等状态。

当需要对元组加锁时,PostgreSQL 会首先检查 t_infomask 中的状态位,如果已经有其他事务持有锁,则会等待。 如果没有,则会先将 xmax 设置为当前事务的 ID,然后将 t_infomask 中的状态位设置为对应的锁模式。

  • 当元组以 Update 模式加锁时,元组的 lock_only 位会被设置为 1。
  • 当元组以 No Key Update 模式加锁时,元组的 lock_onlyis_multi 位会被设置为 1。
  • 当元组以 Key Share 模式加锁时,元组的 lock_onlykeyshr 位会被设置为 1。
  • 当元组以 Share 模式加锁时,元组的 lock_onlykeyshrshr 位会被设置为 1。

根据上述规则,是否有锁以及已有锁与当前模式是否兼容的判断可以通过 CAS xmaxt_infomask 来实现。

组事务

当有多个事务对同一行加共享锁时,xmax 已经不足以追踪锁的多个持有者。 因此 PostgreSQL 会将这些事务合并为一个组事务,在 t_infomask 中设置 is_multi 位,并在 xmax 中设置一个组 ID。 PostgreSQL 会在 pg_multixact 中维护一个组 ID 和事务 ID 的映射表,进而追踪何时释放共享锁。

组事务 ID 与常规的事务 ID 长度相同,均是 32 位,但它们是独立分发的。这意味着事务和组事务可能具有相同的 ID。PostgreSQL 会根据 is_multi 位区分 xmax 中的 ID 是事务 ID 还是组事务 ID。

与常规事务 ID 一样,组事务 ID 也会随着时间的推移而递增,需要定期冻结和清理。

公平性与等待队列

在行级锁的实现一节,可以看到行级锁只维护了哪些事务持有锁,并不会维护有哪些事务正在等待锁以及先后顺序。 因此当稳定地出现一系列加共享锁的事务时,他们会持续持有锁并导致尝试加独占锁的事务出现饥饿。

为了避免这种情况,PostgreSQL 会在加锁失败时将事务加入到一个由 pg_locks 维护的等待队列中。 实际上,PostgreSQL 的等待队列只有一个有效的