暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL 12.2官方手册学习笔记(显示锁定之行级锁、页级锁与死锁)

PostgreSQL数据库工作学习随笔 2021-12-13
411

    除了表级锁以外,还有行级锁,在下文列出了行级锁以及在哪些情境下PostgreSQL会自动使用它们。行级锁的完整冲突表请见表 13.3 。注意一个事务可能会在相同的行上保持冲突的锁,甚至是在不同的子事务中。但是除此之外,两个事务永远不可能在相同的行上持有冲突的锁。行级锁不影响数据查询,它们只阻塞对同一行的写入者和加锁者。


先总结一下表级锁冲突及产生锁对应的命令


行级锁模式


FOR UPDATE


    FOR UPDATE会导致由SELECT语句检索到的行被锁定,就好像它们要被更新。这可以阻止它们被其他事务锁定、修改或者删除,一直到当前事务结束。也就是说其他尝试UPDATE、DELETE、SELECT FOR UPDATE、SELECT FOR NO KEY UPDATE、SELECT FOR SHARE或者SELECT FOR KEY SHARE这些行的事务将被阻塞,直到当前事务结束。反过来,SELECT FOR UPDATE将等待已经在相同行上运行以上这些命令的并发事务,并且接着锁定并且返回被更新的行(或者没有行,因为行可能已被删除)。不过,在一个REPEATABLE READ或SERIALIZABLE事务中,如果一个要被锁定的行在事务开始后被更改,将会抛出一个错误。进一步的讨论请见第 13.4 节 。


    任何在一行上的DELETE命令也会获得FOR UPDATE锁模式,在某些列上修改值的UPDATE也会获得该锁模式。当前UPDATE情况中被考虑的列集合是那些具有能用于外键的唯一索引的列(所以部分索引和表达式索引不被考虑),但是这种要求未来有可能会改变。

session1:

session2:


FOR NO KEY UPDATE


    行为与FOR UPDATE类似,不过获得的锁较弱:这种锁将不会阻塞尝试在相同行上获得锁的SELECT FOR KEY SHARE命令。任何不获取FOR UPDATE锁的UPDATE也会获得这种锁模式。

session1:

session2:


FOR SHARE


    行为与FOR NO KEY UPDATE类似,不过它在每个检索到的行上获得一个共享锁而不是排他锁。一个共享锁会阻塞其他事务在这些行上执行UPDATE、DELETE、SELECT FOR UPDATE或者SELECT FOR NO KEY UPDATE,但是它不会阻止它们执行SELECT FOR SHARE或者SELECT FOR KEY SHARE。

session1:

session2:


FOR KEY SHARE


    行为与FOR SHARE类似,不过锁较弱:SELECT FOR UPDATE会被阻塞,但是SELECT FOR NO KEY UPDATE不会被阻塞。一个键共享锁会阻塞其他事务执行修改键值的DELETE或者UPDATE,但不会阻塞其他UPDATE,也不会阻止SELECT FOR NO KEY UPDATE、SELECT FOR SHARE或者SELECT FOR KEY SHARE。

session1:

session2:


    PostgreSQL不会在内存里保存任何关于已修改行的信息,因此对一次锁定的行数没有限制。不过,锁住一行会导致一次磁盘写,例如, SELECT FOR UPDATE将修改选中的行以标记它们被锁住,并且因此会导致磁盘写入。

表 13.3. 冲突的行级锁


页级锁


    除了表级别和行级别的锁以外,页面级别的共享/排他锁被用来控制对共享缓冲池中表页面的读/写。这些锁在行被抓取或者更新后马上被释放。应用开发者通常不需要关心页级锁,我们在这里提到它们只是为了完整。


死锁


    显式锁定的使用可能会增加死锁的可能性,死锁是指两个(或多个)事务相互持有对方想要的锁。例如,如果事务 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数据库工作学习随笔,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论