侧边栏壁纸
博主头像
DBA的笔记本博主等级

think in coding

  • 累计撰写 21 篇文章
  • 累计创建 20 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Postgresql MVCC -2 锁

张彤
2024-08-05 / 0 评论 / 0 点赞 / 35 阅读 / 148259 字

Postgresql MVCC -2 锁

了解完postgresql中的事务隔离级别,剩下的就是锁机制了。

显式锁 Explicit Locking

PostgreSQL提供了多种锁模式来控制对表中数据的并发访问。在MVCC未提供所需行为的情况下,这些模式可用于应用程序控制的锁定。此外,大多数PostgreSQL命令会自动获取适当模式的锁,以确保在命令执行时不会以不兼容的方式删除或修改引用的表。 (例如, TRUNCATE不能安全地与同一表上的其他操作同时执行,因此它会在表上获取ACCESS EXCLUSIVE锁来强制执行。)

Table-Level Locks

  1. ACCESS SHARE (AccessShareLock)

    SELECT命令在引用的表上获取此模式的锁。一般来说,任何只读取表而不修改表的查询都会获取这种锁模式。

  2. ROW SHARE (RowShareLock)

    SELECT命令在指定了FOR UPDATEFOR NO KEY UPDATEFOR SHAREFOR KEY SHARE选项之一的所有表上获取此模式的锁(除了在没有引用的任何其他表上的ACCESS SHARE锁之外)任何显式的FOR ...锁定选项)。

    • 注意,虽然名字中含有row,但是这类锁不是还是表锁,不是行锁

    1. 关于select for 锁定子句

      1. FOR UPDATE

        • 锁定选定的行以进行更新。这些行不能被其他事务修改,直到当前事务完成。

        • 示例:

          BEGIN;
          SELECT * FROM employees WHERE department_id = 10 FOR UPDATE;
          -- 这里,你可以对结果集中的行进行更新操作
          UPDATE employees SET salary = salary * 1.1 WHERE department_id = 10;
          COMMIT;
      2. FOR NO KEY UPDATE

        • 锁定选定的行以进行更新,但不包括可能更改唯一键值的更新。这种锁比 FOR UPDATE 更弱,可以与 FOR SHAREFOR KEY SHARE 兼容。

        • 示例:

          BEGIN;
          SELECT * FROM employees WHERE department_id = 10 FOR NO KEY UPDATE;
          -- 这里,你可以对结果集中的行进行不改变唯一键的更新操作
          UPDATE employees SET salary = salary + 100 WHERE department_id = 10;
          COMMIT;
      3. FOR SHARE

        • 锁定选定的行以防止其他事务进行更新操作,但允许共享读取。其他事务可以读取这些行,但不能对其进行更新。

        • 示例:

          BEGIN;
          SELECT * FROM employees WHERE department_id = 10 FOR SHARE;
          -- 这里,你可以对结果集中的行进行读取操作,但不能更新它们
          SELECT salary FROM employees WHERE department_id = 10;
          COMMIT;
      4. FOR KEY SHARE

        • 锁定选定的行以防止其他事务进行更改唯一键的操作。与 FOR SHARE 类似,但允许对非唯一键的列进行更新。

        • 示例:

          BEGIN;
          SELECT * FROM employees WHERE department_id = 10 FOR KEY SHARE;
          -- 这里,你可以对结果集中的行进行读取操作,并且可以更新非唯一键列
          UPDATE employees SET salary = salary + 50 WHERE department_id = 10;
          COMMIT;

  3. ROW EXCLUSIVE (RowExclusiveLock)

    命令UPDATEDELETEINSERTMERGE在目标表上获取此锁模式(除了在任何其他引用表上的ACCESS SHARE锁之外)。一般来说,任何修改表中数据的命令都会获取此锁定模式。

    • 同样的,即使名称中带有row,但是是表锁不是行锁

  4. SHARE UPDATE EXCLUSIVE (ShareUpdateExclusiveLock)

    VACUUM (不带FULL )、 ANALYZECREATE INDEX CONCURRENTLYCREATE STATISTICSCOMMENT ONREINDEX CONCURRENTLY以及某些ALTER INDEXALTER TABLE变体获取

  5. SHARE (ShareLock)

    通过CREATE INDEX获取(后面不带CONCURRENTLY )。

  6. SHARE ROW EXCLUSIVE (ShareRowExclusiveLock)

    CREATE TRIGGER和某些形式的ALTER TABLE获取。

    此模式可保护表免受并发数据更改的影响,并且是自排他性的,因此一次只有一个会话可以保存该表。

  7. EXCLUSIVE (ExclusiveLock)

    REFRESH MATERIALIZED VIEW CONCURRENTLY获取

  8. ACCESS EXCLUSIVE (AccessExclusiveLock)

    通过DROP TABLETRUNCATEREINDEXCLUSTERVACUUM FULLREFRESH MATERIALIZED VIEW (不带CONCURRENTLY )命令获取.许多形式的ALTER INDEXALTER TABLE也在该级别获取锁。这也是未显式指定模式的LOCK TABLE语句的默认锁定模式。

    与所有模式的锁冲突

一旦获得,锁通常会一直保持到事务结束。但是,如果在建立保存点后获取了锁,则在回滚到该保存点时该锁会立即释放。这与ROLLBACK取消保存点以来命令的所有效果的原理是一致的。这同样适用于在PL/pgSQL异常块中获取的锁:从块中转义的错误会释放在其中获取的锁,例如:

CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    salary NUMERIC NOT NULL
);
​
BEGIN;
​
-- 创建一个保存点
SAVEPOINT my_savepoint;
​
-- 获取一个锁,例如对表 employees 的 FOR UPDATE 锁
SELECT * FROM employees WHERE id = 1 FOR UPDATE;
​
-- 在此点,行 id=1 已被锁定
-- 你可以执行其他操作,例如更新其他表或行
-- 但我们选择回滚到保存点
ROLLBACK TO SAVEPOINT my_savepoint;
​
-- 现在,锁已被释放,你可以执行其他操作
-- 例如,另一个事务现在可以获取行 id=1 的锁
SELECT * FROM employees WHERE id = 1; -- 这将不会阻塞
​
COMMIT;
​

锁定模式冲突表

ACCESS SHARE

ROW SHARE

ROW EXCL.

SHARE UPDATE EXCL.

SHARE

SHARE ROW EXCL.

EXCL.

ACCESS EXCL.

ACCESS SHARE

X

ROW SHARE

X

X

ROW EXCL.

X

X

X

X

SHARE UPDATE EXCL.

X

X

X

X

X

SHARE

X

X

X

X

X

SHARE ROW EXCL.

X

X

X

X

X

X

EXCL.

X

X

X

X

X

X

X

ACCESS EXCL.

X

X

X

X

X

X

X

X

Row-Level Locks

除了表级锁之外,还有行级锁,下面列出了PostgreSQL自动使用它们的上下文。

请注意,即使在不同的子事务中,事务也可以在同一行上持有冲突的锁;但除此之外,两个事务永远不能在同一行上持有冲突的锁。

行级锁不影响数据查询;他们只阻止同一行的writer和locker

  • wirter:它们只会阻塞试图对同一行进行写入(更新、删除、插入)

  • locker:试图获取锁的其他事务

行级锁在事务结束或保存点回滚期间释放,就像表级锁一样。

  1. FOR UPDATE

    FOR UPDATE会导致SELECT语句检索到的行被锁定,就像要进行更新一样。这可以防止它们被其他事务锁定、修改或删除,直到当前事务结束。也就是说,尝试对这些行进行UPDATEDELETESELECT FOR UPDATESELECT FOR NO KEY UPDATESELECT FOR SHARESELECT FOR KEY SHARE的其他事务将被阻塞,直到当前事务结束;相反, SELECT FOR UPDATE将等待在同一行上运行任何这些命令的并发事务,然后锁定并返回更新的行(如果该行被删除,则不返回任何行)。然而,在REPEATABLE READSERIALIZABLE事务中,如果自事务启动以来要锁定的行已更改,则会引发错误。

    FOR UPDATE锁模式也可由行上的任何DELETE以及修改某些列的值的UPDATE获取。目前,针对UPDATE情况考虑的列集是那些具有可在外键中使用的唯一索引的列(因此不考虑部分索引和表达式索引),但这在将来可能会改变。

  2. FOR NO KEY UPDATE

    行为与FOR UPDATE类似,只是获取的锁较弱:此锁不会阻止尝试获取同一行上的锁的SELECT FOR KEY SHARE命令。任何未获取FOR UPDATE锁的UPDATE也会获取此锁定模式。

  3. FOR SHARE

    行为与FOR NO KEY UPDATE类似,不同之处在于它在每个检索的行上获取共享锁而不是独占锁。共享锁会阻止其他事务对这些行执行UPDATEDELETESELECT FOR UPDATESELECT FOR NO KEY UPDATE ,但不会阻止它们执行SELECT FOR SHARESELECT FOR KEY SHARE

  4. FOR KEY SHARE

    行为与FOR SHARE类似,只是锁较弱: SELECT FOR UPDATE被阻止,但SELECT FOR NO KEY UPDATE不会被阻止。键共享锁会阻止其他事务执行DELETE或任何更改键值的UPDATE ,但不会阻止其他UPDATE ,也不会阻止SELECT FOR NO KEY UPDATESELECT FOR SHARESELECT FOR KEY SHARE

PostgreSQL不会记住内存中有关已修改行的任何信息,因此一次锁定的行数没有限制。然而,锁定行可能会导致磁盘写入,例如, SELECT FOR UPDATE修改选定的行以将其标记为锁定,因此将导致磁盘写入。

行级锁冲突

FOR KEY SHARE

FOR SHARE

FOR NO KEY UPDATE

FOR UPDATE

FOR KEY SHARE

X

FOR SHARE

X

X

FOR NO KEY UPDATE

X

X

X

FOR UPDATE

X

X

X

X

Page-Level Locks

除了表锁和行锁之外,页级共享/独占锁还用于控制对共享缓冲池中表页的读/写访问。这些锁在获取或更新行后立即释放。

Deadlocks

使用显式锁定会增加死锁的可能性,其中两个(或多个)事务各自持有对方想要的锁。例如,如果事务 1 获取了表 A 的排它锁,然后尝试获取表 B 的排它锁,而事务 2 已经排它锁定了表 B,现在想要获取表 A 的排它锁,那么两者都无法继续。 PostgreSQL自动检测死锁情况,并通过中止所涉及的事务之一来解决死锁情况,从而允许其他事务完成。 (具体哪个事务将被中止很难预测,不应依赖。)

请注意,行级锁也可能导致死锁(因此,即使不使用显式锁定,死锁也可能发生)。考虑两个并发事务修改表的情况。第一个事务执行:

UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 11111;

这将获取具有指定帐号的行的行级锁。然后,第二个事务执行:

UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 22222;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 11111;

第一个UPDATE语句成功获取指定行的行级锁,因此它成功更新该行。但是,第二个UPDATE语句发现它尝试更新的行已被锁定,因此它等待获取锁的事务完成。事务二现在正在等待事务一完成,然后才能继续执行。现在,事务一执行:

UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 22222;

事务一尝试获取指定行上的行级锁,但它不能:事务二已经持有这样的锁。因此它等待事务二完成。因此,事务一在事务二上被阻塞,事务二在事务一上被阻塞:出现死锁情况。 PostgreSQL将检测到这种情况并中止其中一个事务。

防止死锁的最佳方法通常是通过确保所有使用数据库的应用程序以一致的顺序获取多个对象上的锁来避免死锁。在上面的示例中,如果两个事务都以相同的顺序更新行,则不会发生死锁。还应该确保事务中对象上获取的第一个锁是该对象所需的最严格的模式。如果无法提前验证这一点,则可以通过重试因死锁而中止的事务来即时处理死锁。

只要没有检测到死锁情况,寻求表级锁或行级锁的事务就会无限期地等待冲突锁被释放。这意味着应用程序长时间保持事务打开(例如,在等待用户输入时)是一个坏主意。

Advisory Locks

PostgreSQL 提供了一种创建具有应用程序定义含义的锁的方法。这些锁被称为建议锁(advisory locks),因为系统不强制使用它们——正确使用这些锁由应用程序决定。对于不适合 MVCC(多版本并发控制)模型的锁定策略,建议锁非常有用。例如,建议锁常用于模拟“平面文件”数据管理系统中典型的悲观锁定策略。虽然可以使用存储在表中的标志来实现相同的目的,但建议锁更快,避免了表膨胀,并且会在会话结束时由服务器自动清理。

在 PostgreSQL 中获取建议锁有两种方式:会话级别或事务级别。一旦在会话级别获取,建议锁将一直持有,直到明确释放或会话结束。与标准锁请求不同,会话级别的建议锁请求不遵守事务语义:在事务期间获取的锁,即使事务稍后回滚,锁仍然会保持;同样,即使调用事务稍后失败,解锁操作也是有效的。一个进程可以多次获取同一个建议锁;对于每个完成的锁请求,必须有一个对应的解锁请求才能真正释放锁。另一方面,事务级别的锁请求行为更像常规锁请求:它们会在事务结束时自动释放,并且没有显式的解锁操作。对于短期使用建议锁,这种行为通常比会话级别的行为更方便。会话级别和事务级别的建议锁请求会以预期的方式互相阻塞。如果一个会话已经持有某个建议锁,它的额外请求将总是成功,即使其他会话在等待该锁;这一点无论是现有的锁持有和新请求是在会话级别还是事务级别都适用。

和 PostgreSQL 中的所有锁一样,当前任何会话持有的所有建议锁的完整列表可以在 pg_locks 系统视图中找到。

建议锁和常规锁都存储在一个共享内存池中,其大小由配置变量 max_locks_per_transactionmax_connections 定义。必须注意不要耗尽此内存,否则服务器将无法授予任何锁。这对服务器可以授予的建议锁数量施加了上限,通常根据服务器的配置情况,在几万到几十万之间。

在某些使用建议锁的方法中,尤其是在涉及显式排序和 LIMIT 子句的查询中,必须注意控制获取的锁,因为 SQL 表达式的求值顺序会影响锁的获取。例如:

SELECT pg_advisory_lock(id) FROM foo WHERE id = 12345; -- ok
SELECT pg_advisory_lock(id) FROM foo WHERE id > 12345 LIMIT 100; -- danger!
SELECT pg_advisory_lock(q.id) FROM
(
  SELECT id FROM foo WHERE id > 12345 LIMIT 100
) q; -- ok

在上面的查询中,第二种形式是危险的,因为在执行锁定函数之前,无法保证 LIMIT 子句先应用。

这可能导致获取一些应用程序未预期的锁,从而无法释放这些锁(直到会话结束)。从应用程序的角度来看,这些锁将是悬空的,尽管仍然可以在 pg_locks 中查看。


0

评论区