假设我们有两个表user
和book
。
在应用程序级别,为了确保数据完整性并避免竞争条件,我们在事务开始时锁定实体。
问题一:
假设在应用程序级别我们有两个请求同时运行:
请求 1(应用程序级别):
start trx
lock for update user with id 1
lock for update book with id 2
do changes
commit
请求 2(应用程序级别):
start trx
lock for update book with id 2
lock for update user with id 1
do changes
commit
可能会出现两个请求第一个锁都先于第二个锁申请的情况,即用户 1 的请求 1 锁和图书 2 的请求 2 锁,这将导致死锁。
我们怎样才能避免这种情况呢?我唯一的想法是在某个地方写下所有表的锁定顺序,user->book
并确保我们遵循它。
问题2:
如果我们在不同的请求中执行两个查询,以在突变之前锁定实体
- 要求1
SELECT * FROM user WHERE id IN [1,2] FOR UPDATE
- 要求2
SELECT * FROM user WHERE id IN [2,1] FOR UPDATE
这会导致僵局吗?例如,如果出于某种原因,一个查询将首先锁定用户 1,然后切换到请求 2,然后再返回锁定用户 2。
如果每个事务的第一个语句在需要第二个锁之前已经获取了第一个表中的行级锁,则问题 1可能会死锁。
其中一个事务最终将被选择回滚,从而允许另一个事务完成。
正如您所指出的,解决方案是以一致的顺序获取锁。
对于问题 2: 两个查询都应该获得
ACCESS SHARE
锁来读取表,并且在读取表而不更改数据时不应互相阻塞。锁ACCESS SHARE
与其他锁不冲突。该锁只会与
ACCESS EXCLUSIVE
我相信的锁发生冲突。(或某种形式的选择更新)它们也是一种声明。一个人可以等待另一个人完成,但没有理由陷入僵局。持有独占锁也不会等待另一个独占锁。
当两个事务希望以无法通过等待另一个事务完成来解决的顺序获取同一对象上的冲突锁时,就会发生死锁。
供参考: https://www.postgresql.org/docs/current/explicit-locking.html
您显然已经了解死锁产生的本质,简单地举一个最简单的例子,就是两个线程各自获取并持有一个锁,并且每个线程都需要获取另一个锁才能继续,但是每个线程需要获取的锁是对方已经持有的。
这种意想不到的情况本质上是由于程序员的设计缺陷造成的。数据库引擎(在运行时)检测这些缺陷导致的死锁,并通过终止其中一个线程并释放其所有锁来解决它们,但引擎不会静态分析代码来识别两个线程之间死锁的可能性并采取行动来抢占它们,除了永久中止线程及其所有未提交的工作之外,它们不会以任何智能方式处理死锁。
更广泛地说,它是并发算法之间“隔离”原则的缺陷之一,而 SQL 引擎(按照流行的理解)应该提供这种原则。
尽管如此,数据库引擎执行的死锁监控比以前更好,当整个计算机系统遇到这些并发编程缺陷之一时,它会永久挂起。
正如您已经注意到的,正确设计的一种方法是确保永远不会建议按可能导致冲突的顺序获取一系列锁。这可能涉及表的锁定顺序,但也可能涉及同一表中的行锁定顺序。例如,标准模式是按照始终从低到高帐号的顺序更新帐户余额表 - 因为如果双向执行,最终将陷入死锁,即使锁定纯粹在范围内只有一张桌子。
在实践中,由于 SQL 引擎自行决定查询计划(默认情况下),包括必要的锁定和锁定放置的顺序,并且由于不可能使用 SQL 以直接且显式的方式指定所有顺序,因此通常识别必要的命令比实际让引擎遵守它们要容易得多。
此外,虽然当涉及有限数量的表上的有限数量的并发算法时,必要的分析足够简单,但执行分析并为不平凡的应用程序的整个数据库确定适当的锁定顺序却变得非常困难。 。
另一种策略是增加锁的粒度,从而消除顺序冲突的可能性。因此,例如,不要使用行锁定(在我给出的有关更新帐户余额的示例中),而是升级为表锁定,以便只需要一个表锁来覆盖所有正在更新的行,并且线程要么拥有该唯一的锁,要么不是。
第三种策略是实现某种自定义序列化机制,该机制并不直接使多个算法在并发执行时的锁定兼容,而是简单地防止两个算法之间出现并发执行计划,否则很容易出现并发执行计划陷入僵局。
最后,可以接受并处理死锁的风险。可以将可能被中止的死锁受害者按优先级顺序放置,以确定哪个被中止以及哪个优先继续,并且可以将附加逻辑添加到算法中,以便在成为死锁受害者的情况下重试工作。死锁解决。
有时,即使在使用正确分析的锁定顺序设计的系统中,也可能有必要(出于简单的性能原因)违反该顺序,从而面临死锁的风险。
最初设计有锁定顺序的系统也可能会以可能需要更改锁定顺序的方式进行更改,但不是进行复杂的分析和返工,而这可能是改变现有顺序所必需的,而现有的顺序是围绕该系统进行的。已经编写了大型数据库应用程序,而是设计了一种算法来尽最大努力违背顺序,并简单地重试死锁。