数据库死锁

18

一个经典引起数据库死锁的原因是两个事务按不同顺序插入和更新表。

例如,事务A先在表A中插入,然后在表B中插入。

而事务B则先在表B中插入,再插入表A。

这种情况总是存在数据库死锁的风险(假设您没有使用可串行化隔离级别)。

我的问题是:

  1. 您在设计中遵循何种模式以确保所有事务按相同顺序进行插入和更新。我正在阅读的一本书建议可以按表名排序语句。您是否采用了类似或不同的方法来强制执行所有插入和更新按相同顺序进行?

  2. 删除记录呢?删除操作需要从子表开始,而更新和插入需要从父表开始。您如何确保此过程不会遇到死锁?

6个回答

14
  1. 所有交易按照相同的顺序进行插入\更新。
  2. 删除:在事务外部确定要删除的记录,然后尝试在最小可能的事务中进行删除,例如通过主键或类似标识符在查找阶段查找。
  3. 通常使用小型事务。
  4. 索引和其他性能调优,既可以加快事务速度,又可以促进索引查找而不是表扫描。
  5. 避免“热表”,例如一个表用于为其他表的主键生成递增计数器。任何其他“交换机”类型的配置都有风险。
  6. 特别是如果不使用Oracle,请详细了解目标关系数据库管理系统的锁定行为(乐观/悲观,隔离级别等)。确保您不允许行锁升级为表锁,因为某些关系数据库管理系统会这样做。

7
死锁并不是什么大事。只需在失败时准备重试您的事务即可。
而且要保持它们的短暂性。由于索引的魔力,涉及非常少记录的查询的短暂事务是理想的,以最小化死锁 - 锁定的行数更少,时间更短。
您需要知道现代数据库引擎不会锁定表格;它们将锁定行;因此死锁的可能性稍微降低了一些。
您还可以通过使用MVCC和CONSISTENT READ事务隔离级别来避免锁定:一些线程将只看到过期的数据,而不是锁定。

在一定程度上,我同意。你应该编写程序以从死锁中恢复。 而且你的所有建议都很好。但同时,我认识到在获取和释放锁的方式上保持一致是很好的(这适用于所有锁)。 我想知道有哪些模式\策略可以帮助你做到这一点。 - RN.
如果您正在使用 SQL Server 2005,则使用“READ COMMITTED SNAPSHOT”。这应该会为您提供与 Oracle 相同的语义:编写者永远不会阻塞读取器。 - aquinas

3
  1. 仔细设计数据库过程,尽可能减少涉及多个表的事务。当我拥有数据库设计控制权时,从来没有出现过一种死锁情况,我不能设计出导致这种情况的条件。这并不是说它们不存在,也许在我的有限经验之外的情况下更为普遍;但我有足够的机会改进造成这些问题的设计。一个明显的策略是从一个按时间顺序编写的只写入表开始,用于插入新的完整原子事务,而且没有相互依赖,并以有序的异步过程应用其效果。

  2. 除非您完全确定所采取的数据库默认隔离级别和锁定设置存在哪些风险,并通过测试证明了这一点,否则始终使用数据库默认隔离级别和锁定设置。首先,如果可能的话重新设计您的流程。然后,强加最小的增加保护,以消除风险(并进行测试以证明其有效性)。不要为了“以防万一”而增加限制——这通常会导致意想不到的后果,有时还会导致您本来想避免的问题。

  3. 重申另一个观点,大部分您在此处以及其他网站上读到的有关更改数据库设置来处理事务风险和锁定问题的文章是误导性的和/或错误的,因为它们经常相互冲突。不幸的是,特别是对于 SQL Server,我发现没有一份文件可以清晰地解释这些问题。


避免涉及多个表的事务?这样做会失去事务的意义,因为事务的目的是将相关的更新分组在一起,以便表不会失去同步。-1。 - Seun Osewa
1
也许我表达不够清晰。当然,这就是事务存在的原因;但是,如果您可以修改数据流程,减少依赖表更改的次数,就可以降低锁定的机会,而不会导致表不同步。 - dkretz

2
我分析所有数据库操作,确定每个操作是否需要在多语句事务中,并且对于每种情况,需要什么最低隔离级别才能防止死锁......就像你所说的,如果使用"serializable"将一定会这样做。
通常,只有非常少数的数据库操作需要首先进行多语句事务,在其中,只有少数需要"serializable"隔离级别才能消除死锁。
对于那些需要进行多语句事务的操作,请在开始之前设置该事务的隔离级别,并在提交之后重置为默认值。

隔离的主要原因不是为了防止死锁,而是为了防止幻读和竞态条件。 - Walter Mitty
“幻读”和“丢失/重复读取”可以通过隔离级别Serializable来防止。 - Charles Bretana
每个隔离级别都存在是为了防止其自身特征集的数据不一致性问题。 “脏读”由“读取已提交”级别防止,“不可重复读”由“可重复读”防止,“幻读”和“丢失/重复读”由Serializable级别防止。 - Charles Bretana

2

我发现避免死锁中最好的投资之一是使用可以排序数据库更新的对象关系映射器。具体的顺序并不重要,只要每个事务按照相同的顺序进行写入(并完全相反地进行删除)即可。

这样做的原因是无论如何你的操作都会始终先访问表 A,然后是表 B,接着是表 C(可能依赖于表 B)。

只要你在存储过程或数据层的访问代码中小心谨慎,就可以实现类似的结果。唯一的问题是手动完成需要极大的仔细和耐心,而具有工作单元概念的 ORM 可以自动处理大多数情况。

更新:删除应该向前运行以验证所有内容的版本与期望的版本相同(您仍然需要记录版本号或时间戳),然后在一切验证完成后向后删除。由于这些操作都应该在一个事务中完成,因此不存在什么东西会在你操作的时候发生改变的可能性。ORM 之所以要反向操作是为了遵守键的要求,但如果您向前检查,则已经拥有了所有需要的锁。


1
我完全同意! 但是ORM解决方案如何处理删除操作呢? 如果我在事务A中更新父表中的一行,并在事务B中删除子表中的一行,ORM解决方案如何避免死锁? ORM解决方案可以帮助排序,但是在数据库中进行删除操作存在固有问题。 - RN.
更新了我的回答,包括更多关于这个案例的内容。 - Godeke
1
由于这些操作应该在一个事务中完成,因此不应该存在任何可能会在您的操作过程中发生变化的情况。除非您将事务设置为SERIALIZABLE,否则这种情况确实存在。没有其他方法可以保护您免受在“前向检查”和实际更新之间发生的更改的影响。您只是转移了问题,而不是解决了它。 - Erwin Smout
是的,这就是我的ORM中运行删除操作的方式,尽管应该更明确。我的观点更多的是使用ORM可以帮助组织数据访问中最棘手的问题之一:通过确保操作按特定顺序运行来避免不必要的冲突。一个尊重关键插入和删除顺序的顺序。(我曾经见过手动强制执行这种顺序的尝试,如果不是由工具链强制执行,似乎总会有些东西滑过去)。 - Godeke

0

只有当数据库锁定了整个表时,您的示例才会成为问题。如果您的数据库正在这样做...那就跑吧 :)


我不这么认为。即使在行级锁的情况下,只要两个事务都竞争同一行上的锁,就会发生这种情况。 - RN.
好的,我应该说它有可能发生... 两个不同的事务想要修改相同的行并以不同的顺序进行修改是非常罕见的。 - aquinas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接