关系级锁
在访问表内的数据时,通常需要对整个表加锁。
例如使用 ALTER TABLE 修改表结构时,必须等待所有表上的 SELECT、UPDATE 等语句执行完成后才能执行。
在不同的事务隔离级别下,根据操作和影响范围的不同,会加不同的锁。
锁的模式以及常见的加锁场景:
| Lock Mode | SQL Command(s) |
|---|---|
| Access Share | SELECT |
| Row Share | SELECT FOR UPDATE SKIP LOCKED |
| Row Exclusive | INSERT、UPDATE、DELETE |
| Share Update Exclusive | VACUUM、CREATE INDEX CONCURRENTLY |
| Share | CREATE INDEX |
| Share Row Exclusive | CREATE TRIGGER |
| Exclusive | REFRESH MATERIALIZED VIEW CONCURRENTLY |
| Access Exclusive | DROP、TRUNCATE、LOCK TABLE、VACUUM FULL、REFRESH MATERIALIZED VIEW |
兼容性矩阵:
| Lock Mode | AS | RS | RE | SUE | S | SRE | E | AE |
|---|---|---|---|---|---|---|---|---|
| Access Share | Y | Y | Y | Y | Y | Y | Y | N |
| Row Share | Y | Y | Y | Y | Y | Y | N | N |
| Row Exclusive | Y | Y | Y | Y | N | N | N | N |
| Share Update Exclusive | Y | Y | Y | N | N | N | N | N |
| Share | Y | Y | N | N | Y | N | N | N |
| Share Row Exclusive | Y | Y | N | N | N | N | N | N |
| Exclusive | Y | N | N | N | N | N | N | N |
| Access Exclusive | N | N | N | N | N | N | N | N |
来源:https://cc.pigsty.io/blog/dev/pg-lock/
行级锁
为了提高并发性能,PostgreSQL 允许多个 UPDATE 在同一个表上并发执行。
为了保证数据的一致性,UPDATE 等操作在访问行(元组)时会在行级上再加一次锁。
锁的模式以及常见的加锁场景:
| Lock Mode | Common Scenarios / Operations |
|---|---|
| Key Share | SELECT ... FOR KEY SHARE; Foreign key constraint enforcement (protects referenced keys from being updated/deleted). |
| Share | SELECT ... FOR SHARE; Reading a row while preventing other transactions from modifying or deleting it. |
| No Key Update | UPDATE (not modifying key columns), DELETE; Row modification/deletion, may involve non-key index updates. |
| Update | UPDATE (including key columns), DELETE, SELECT ... FOR UPDATE; Row modification/deletion, may involve key index updates. |
兼容性矩阵:
| Lock Mode | Key Share | Share | No Key Update | Update |
|---|---|---|---|---|
| Key Share | Y | Y | Y | N |
| Share | Y | Y | N | N |
| No Key Update | Y | N | N | N |
| Update | N | N | N | N |
行级锁的实现
在 PostgreSQL 中,行级锁是通过元组头部的 t_infomask、t_infomask2 字段来实现的。
PostgreSQL 规定 t_infomask 和 t_infomask2 中的特定位表示 lock_only、is_multi、keys_upd、keyshr、shr 等状态。
当需要对元组加锁时,PostgreSQL 会首先检查 t_infomask 中的状态位,如果已经有其他事务持有锁,则会等待。
如果没有,则会先将 xmax 设置为当前事务的 ID,然后将 t_infomask 中的状态位设置为对应的锁模式。
- 当元组以
Update模式加锁时,元组的lock_only位会被设置为 1。 - 当元组以
No Key Update模式加锁时,元组的lock_only和is_multi位会被设置为 1。 - 当元组以
Key Share模式加锁时,元组的lock_only、keyshr位会被设置为 1。 - 当元组以
Share模式加锁时,元组的lock_only、keyshr、shr位会被设置为 1。
根据上述规则,是否有锁以及已有锁与当前模式是否兼容的判断可以通过 CAS xmax 和 t_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 的等待队列只有一个有效的