17

我们有一个基于网络的应用程序。应用程序中有时间限制的数据库操作(插入和更新)需要更多时间才能完成,因此这个特定的流程已更改为 Java 线程,因此它不会等待(阻塞)完成完整的数据库操作。

我的问题是,如果超过 1 个用户遇到此特定流程,我将面临 PostgreSQL 引发的以下错误:

org.postgresql.util.PSQLException: ERROR: deadlock detected
  Detail: Process 13560 waits for ShareLock on transaction 3147316424; blocked by process 13566.
Process 13566 waits for ShareLock on transaction 3147316408; blocked by process 13560.

上述错误始终在 INSERT 语句中引发。

附加信息: 1)我在此表中定义了 PRIMARY KEY。2) 此表中有 FOREIGN KEY 引用。3) 将单独的数据库连接传递给每个 Java 线程。

技术 Web 服务器:Tomcat v6.0.10 Java v1.6.0 Servlet 数据库:PostgreSQL v8.2.3 连接管理:pgpool II

4

4 回答 4

27

处理死锁的一种方法是有一个重试机制,等待一个随机间隔并尝试再次运行事务。随机间隔是必要的,这样冲突的事务就不会不断地相互碰撞,从而导致所谓的活锁——甚至更难调试。实际上,大多数复杂的应用程序在需要处理事务序列化失败时,迟早会需要这种重试机制。

当然,如果您能够确定死锁的原因,通常最好消除它,否则它回来咬您。对于几乎所有情况,即使死锁情况很少见,为了获得确定顺序的锁或获得更多粗粒度锁而付出的一点吞吐量和编码开销也是值得的,以避免偶尔的大延迟命中和突然的性能悬崖扩展并发时。

当您始终让两个 INSERT 语句死锁时,很可能是唯一索引插入顺序问题。例如,在两个 psql 命令窗口中尝试以下操作:

Thread A           | Thread B
BEGIN;             | BEGIN;
                   | INSERT uniq=1;
INSERT uniq=2;     | 
                   | INSERT uniq=2; 
                   |   block waiting for thread A to commit or rollback, to
                   |   see if this is an unique key error.
INSERT uniq=1;     |
   blocks waiting  |
   for thread B,   |
     DEADLOCK      | 
                   V    

通常,解决此问题的最佳做法是找出保护所有此类事务的父对象。大多数应用程序都有一个或两个主要实体,例如用户或帐户,它们是很好的候选对象。然后,您所需要的只是让每个事务通过 SELECT ... FOR UPDATE 获得它所触及的主要实体上的锁。或者,如果涉及多个,则每次都以相同的顺序锁定所有这些(按主键排序是一个不错的选择)。

于 2009-10-05T16:50:50.263 回答
15

PostgreSQL 在这里所做的在Explicit Locking的文档中有介绍。“死锁”部分中的示例显示了您可能在做什么。您可能没有预料到的部分是,当您更新某些内容时,它会在该行上获得一个锁定,该锁定一直持续到所涉及的事务结束。如果您有多个客户端同时更新不止一件事,那么您将不可避免地陷入死锁,除非您竭尽全力防止它们发生。

如果您有多个像 UPDATE 那样取出隐式锁的东西,您应该将整个序列包装在 BEGIN/COMMIT 事务块中,并确保您在获取锁的顺序上保持一致(即使是像 UPDATE 抓取的隐式锁)到处。如果您需要更新表 A 然后表 B 中的某些内容,并且应用程序的一部分执行 A 然后 B,而另一部分执行 B 然后 A,那么您总有一天会死锁。对同一个表的两次更新同样注定会失败,除非您可以强制执行在客户端之间可重复的两者的某种顺序。一旦您拥有要更新的记录集并始终先获取“较低”的记录集,就按主键排序是一种常见的策略。

您的 INSERT 不太可能在这里受到责备,除非您违反了 Ants 已经描述的主键,否则它们更难陷入僵局。

你不想做的是尝试在你的应用程序中重复锁定,这将变成一个巨大的可扩展性和可靠性混乱(并且可能仍然会导致数据库死锁)。如果您无法在标准数据库锁定方法的范围内解决此问题,请考虑使用建议性锁定工具或显式LOCK TABLE来强制执行您需要的内容。这将为您节省一个痛苦的编码世界,而不是尝试将所有锁推到客户端。如果您对一个表有多个更新并且无法强制执行它们发生的顺序,那么您别无选择,只能在执行它们时锁定整个表;这是唯一不会引入死锁可能性的途径。

于 2009-10-06T06:06:21.680 回答
6

死锁解释
简而言之,正在发生的事情是特定的 SQL 语句(INSERT 或其他)正在等待另一条语句释放数据库特定部分的锁,然后才能继续执行。在此锁被释放之前,第一条 SQL 语句,称为“语句 A”,将不允许自己访问这部分数据库以完成其工作(= 常规锁情况)。但是... 语句 A 还对数据库的另一部分进行了锁定,以确保没有其他用户访问数据库(用于读取或修改/删除,具体取决于锁定的类型)。现在...第二条 SQL 语句本身需要访问由语句 A 的锁标记的数据部分。那是一个死锁:两个语句将无限期地等待彼此。

补救措施...

这将需要知道这些不同线程正在运行的特定 SQL 语句,并在其中查看是否有办法:

a) 移除一些锁,或改变它们的类型。
   例如,可能整个表都被锁定,因此只有给定的行,或者
   其中一页是必要的。
b) 防止在给定时间提交多个此类查询。
   这将通过信号量/锁(又名 MUTEX)在
   多线程逻辑。

请注意,“b)”方法如果没有正确实现,可能只会将死锁情况从 SQL 内部转移到程序/线程逻辑内部。关键是只创建一个互斥锁,任何线程将首先获取这些互斥锁,该线程将要运行这些容易死锁的查询之一。

于 2009-10-05T14:38:31.280 回答
0

您的问题可能是插入命令试图锁定一个或两个索引,并且索引被锁定以用于另一个胎面。

一个常见的错误是在每个线程上以不同的顺序锁定资源。检查顺序并尝试在所有线程中以相同的顺序锁定资源。

于 2009-10-05T14:37:03.343 回答