分类:
2008-05-21 15:16:59
PostgreSQL 提供了各种各样的锁模式用于控制对表中的数据的并发访问。这些模式可以用于在 MVCC 无法给出期望的行为的时候,由用户控制的锁定的场合。同样,大多数 PostgreSQL 命令自动施加恰当的锁以保证被引用的表在命令执行的时候不会以一种不兼容的方式被删除或者修改。 (比如,在存在其它并发操作的时候,ALTER TABLE 是不能在同一个表上面安全执行的,所以它会在表上获取一个排他锁来强制获得安全环境。)
要检查当前数据库服务器里正在持有的锁的列表,我们可以使用系统视图 pg_locks 。有关监控锁管理器子系统的状态的更多信息,请参考。
下面的列表显示了可用的锁模式和它们在 PostgreSQL 里面自动使用的环境。你也可以用 LOCK 命令明确地获取这些锁。请注意所有这些锁模式都是表级锁,即使它们的名字包含单词 "row";这些锁模式的名称是历史造成的。从某种角度而言,这些名字反应了每种锁模式的典型用法 — 但是语意都是一样的。两种锁模式之间真正的区别是它们有着不同的冲突锁集合(见表 13-2)。两个事务在同一时刻不能在同一个表上持有相互冲突的锁。(不过,一个事务决不会和自身冲突。比如,它可以在一个表上请求 ACCESS EXCLUSIVE 然后稍后的时候请求 ACCESS SHARE。)非冲突锁模式可以由许多事务并发地持有。请特别注意有些锁模式是自冲突的(比如,在任意时刻,ACCESS EXCLUSIVE 模式就不能够被多个事务拥有)而其它地都不是自冲突的(比如,ACCESS SHARE 可以被多个事务持有)。
ACCESS SHARE
ROW SHARE
ROW EXCLUSIVE
SHARE UPDATE EXCLUSIVE
SHARE
SHARE ROW EXCLUSIVE
EXCLUSIVE
ACCESS EXCLUSIVE
在获取一个锁之后,通常这个锁会持续到事务的终止。但如果一个锁是在建立一个 savepoint (保存点)之后获取的,那么如果这个 savepoint (保存点)被回滚,那么锁就被立即释放。这点和 ROLLBACK 取消所有命令自 savepoint (保存点)以来的影响的原则是一致的。同样的道理对在 PL/pgSQL 例外块里持有的锁也有效:一个块内抛出的错误将释放改块内请求的锁。
请求的锁模式 | 当前的锁模式 | |||||||
ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE | |
ACCESS SHARE | X | |||||||
ROW SHARE | X | X | ||||||
ROW EXCLUSIVE | X | X | X | X | ||||
SHARE UPDATE EXCLUSIVE | X | X | X | X | X | |||
SHARE | X | X | X | X | X | |||
SHARE ROW EXCLUSIVE | X | X | X | X | X | X | ||
EXCLUSIVE | X | X | X | X | X | X | X | |
ACCESS EXCLUSIVE | X | X | X | X | X | X | X | X |
除了表级锁以外,还有行级锁,他们可以是排他的或者是共享的。特定行上的排他行级锁是在更新或删除该行的时候自动请求的。该锁一直保持到事务提交或者回滚,方式和表级锁一样。行级锁不影响对数据的查询;它们只阻塞对同一行的写入。
要在不修改某行的前提下请求在该行的一个排他行级锁,用 SELECT FOR UPDATE 选取该行。请注意一旦我们请求了特定的行级锁,那么该事务就可以多次对该行进行更新而不用担心冲突。
要在一行上请求一个共享的行级锁,用 SELECT FOR SHARE 选取该行。一个共享锁并不阻止其它事务请求同一个共享的锁。不过,其它事务不允许更新,删除,或者排他锁住一个其它事务持有共享锁的行。任何这么做的企图都将被阻塞住,等待共享锁释放。
PostgreSQL 不会在内存里保存任何关于已修改行的信息,因此对一次锁定的行数没有限制。不过,锁住一行会导致一次磁盘写;因此,象 SELECT FOR UPDATE 将修改选中的行以标记它们被锁住了,因此会导致磁盘写。
除了表级别的和行级别的锁以外,页面级别的共享/排他销也用于控制对共享缓冲池中表页面的读/写访问。这些锁在抓取或者更新一行后马上被释放。应用程序员通常不需要关心页级锁,我们在这里提到它们只是为了完整。
明确锁定的使用可能会增加死锁的可能性,死锁是是指两个(或多个)事务相互持有对方期待的锁。比如,如果事务 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 将侦测这样的条件并退出其中一个事务。
防止死锁的最好方法通常是保证所有使用一个数据库的应用都以一致的顺序在多个对象上请求锁定。在上面的例子里,如果两个事务以同样的顺序更新那些行,那么就不会发生死锁。我们也要保证在一个对象上请求的第一个锁是该对象需要的最高的锁模式。如果我们无法提前核实这些问题,那么我们可以通过在现场重新尝试因死锁而退出的事务的方法来处理。
只要没有检测到死锁条件,一个等待表级锁或者行级锁的事务将等待冲突锁的释放不确定的时间。这就意味着一个应用持有打开的事务时间太长可不是什么好事情(比如锁,等待用户输入)。
PostgreSQL 提供了一种创建具有应用定义的含义的锁的方法。这叫劝告性锁,因为系统并不强制锁的使用 --- 对其的正确使用是应用的责任。劝告性锁对于那种在 MVCC 模式下实现起来很笨拙的锁策略很有用。一旦获得,那么一个劝告性的锁就会保存到明确释放或者是直到会话结束。和标准的锁不同的是,劝告性锁并不遵守事务的语意:一个在事务过程中获取的锁,在这个事务回滚之后将仍然持有,类似的是,即使调用它的事务失败,一个接锁命令依然会生效。同一个锁可以由它的所属进程请求多次:在锁最终释放之前,每个锁请求必须对应一个解锁请求。(如果一个会话已经持有某个锁,那么随后的请求总是会成功,即使其它请求在等待这个锁也如此。)和所有 PostgreSQL 里头的锁一样,当前持有的完整的劝告性锁的列表可以在 pg_locks 系统视图中找到。
劝告性锁是在共享内存池中分配的,共享内存池的大小是由配置变量 max_locks_per_transaction 和 max_connections 定义的。我们必须小心使用,以避免这些内存耗尽,否则服务器将无法赋予任何锁。这样就意味着服务器可以赋予的劝告性锁是有上限的,通常是几万到十几万个,具体数目取决于服务器的具体配置。
劝告性锁的一个常用场合是模拟悲观的锁策略,尤其是那种常做“平面文件”的数据管理系统里。虽然在一个表里面存储的一个标志也可以实现这个任务,劝告性锁速度更快,还避免了 MVCC 的膨胀,并且在服务器对应的会话结束的时候自动清理。在某些场合下,尤其是涉及明确排序和 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 里看到。
系统提供的用于操作劝告性锁的函数在列出。