【问题标题】:Postgres 9.4 detects Deadlock when read-modify-write on single tablePostgres 9.4 在单个表上读取-修改-写入时检测到死锁
【发布时间】:2019-07-11 22:02:14
【问题描述】:

我们有一个带有简单表格的应用程序

given_entity{
    UUID id;
    TimeStamp due_time;
    TimeStamp process_time;
} 

这是一个 Spring Boot (1.2.5.RELEASE) 应用程序,它使用 spring-data-jpa.1.2.5.RELEASE 和 hibernate-4.3.10.FINAL 作为 jpa 提供者。
我们有这个应用程序的 5 个实例,每个实例都有一个调度程序,每 2 秒运行一次,并在数据库中查询截止时间为最后 2 分钟但尚未处理的行;

SELECT * FROM given_entity 
WHERE process_time is null and due_time between  now() and NOW() - INTERVAL '2 minutes' 
FOR UPDATE

要求是上表的每一行都被恰好一个应用程序实例成功处理。 然后应用程序实例处理这些行并在一个事务中更新其 process_time 字段。 这可能需要也可能不会超过 2 秒,这是调度程序间隔。 此外,我们在此表上没有任何索引,但 PK 索引。 值得注意的第二点是,这些实例可能会在该表中插入由客户端单独调用的行。

问题:在日志中我看到来自 postgresql 的这条消息(很少但会发生)

ERROR: deadlock detected
Detail: Process 10625 waits for ShareLock on transaction 25382449; blocked by process 10012.
Process 10012 waits for ShareLock on transaction 25382448; blocked by process 12238.
Process 12238 waits for AccessExclusiveLock on tuple (1371,45) of relation 19118 of database 19113; blocked by process 10625.
Hint: See server log for query details.
Where: while locking tuple (1371,45) in relation "given_entity"

问题: 这是怎么发生的? 我检查了 postgresql 锁并搜索了互联网。我没有发现任何说死锁只能在一张简单的桌子上发生的事情。 我也无法使用测试重现此错误。

【问题讨论】:

  • 你能把代码粘贴到你更新表格的地方吗? select for update 阻止其他事务的修改,应该小心。
  • 要完全避免这些问题,您可以使用 Hibernate 乐观锁定机制(除非有一些真正正当的理由来锁定您想要更新的记录)。也许这也可能有帮助:blog.2ndquadrant.com/…
  • 对于更新,我们只需更新其中的 process_time 字段。类似于 'UPDATE given_entity set process_time= now() where id = ?'
  • 对于乐观锁,我们不能使用它,因为要求是每一行都必须由一个实例完全处理。使用乐观锁,两个实例可以同时处理该行,当第二个实例得到 OptimisticLockException 时,为时已晚,该行已经被处理。我们希望保持简单,并使用 DB 行锁来同步我们应用程序的不同实例。

标签: postgresql hibernate deadlock


【解决方案1】:

进程 A 尝试锁定第 1 行,然后锁定第 2 行。同时,进程 B 尝试锁定第 2 行,然后锁定第 1 行。这就是触发死锁所需的全部内容。

问题在于行锁是以不确定的顺序获取的,因为SELECT 以不确定的顺序返回其行。避免这种情况只是确保所有进程在锁定行时就顺序达成一致,即:

SELECT * FROM given_entity 
WHERE process_time is null and due_time between  now() and NOW() - INTERVAL '2 minutes' 
ORDER BY id
FOR UPDATE

在 Postgres 9.5+ 中,您可以使用 FOR UPDATE SKIP LOCKED 简单地忽略被另一个进程锁定的任何行。

【讨论】:

【解决方案2】:

这很容易发生。

可能有几行满足条件

due_time BETWEEN now() AND now() - INTERVAL '2 minutes'

因此很容易发生SELECT ... FOR UPDATE 找到并锁定一行,然后被阻止锁定下一行。请记住——对于死锁,不需要涉及多个表,涉及多个可锁定资源就足够了。在您的情况下,这是 given_entity 表中的两个不同行。

甚至可能在您的两个SELECT ... FOR UPDATE 语句之间发生死锁。
由于您说表上只有主键索引,因此查询必须执行顺序扫描。在 PostgreSQL 中,从顺序扫描返回的行没有固定的顺序。相反,如果两个顺序扫描同时运行,第二个将“搭载”第一个,并在第一个顺序扫描的当前位置开始扫描表。

您可以通过将参数synchronize_seqscans 设置为off 来检查是否是这种情况,并查看死锁是否消失。另一种选择是在运行语句之前对表进行SHARE ROW EXCLUSIVE 锁定。

【讨论】:

  • 非常感谢。我也怀疑扫描行的不同顺序。但是由于我无法在 100 个线程同时写入和 100 个线程同时读取的单元测试中重现它,我不确定是否是这种情况。我无法解释的另一件事是为什么在这里获得“AccessExclusiveLock”?那是因为同时插入将发生并且postgresql想要重新索引PK?我不确定 'synchronize_sequence=off' 将如何影响我们的表现。您认为查询结果的搜索列上的索引可以缓解这种情况吗?
  • 至于“SHARE ROW EXCLUSIVE”,如果我有其他选择,我宁愿不去显式锁定。
  • 更重要的是,'SHARE ROW EXCLUSIVE' 与使用 'SELECT FOR UPDATE' 获取的 'ROW SHARE' 之外的其他锁冲突。我不确定它如何影响性能。
  • due_time 上的索引可能是个好主意。我也不认为锁定是一个好主意,但如果你这样做:ACCESS EXCLUSIVE LOCK 太强了,ROW SHARE 不够强,因为它不会与自身冲突。
【解决方案3】:

在您的 application.properties 中开启休眠批量更新

hibernate.batch.size=100
hibernate.order_updates=true
hibernate.order_inserts=true
hibernate.jdbc.fetch_size = 400

【讨论】:

    猜你喜欢
    • 2016-11-14
    • 2022-01-22
    • 1970-01-01
    • 2023-03-30
    • 2013-09-04
    • 1970-01-01
    • 1970-01-01
    • 2015-06-29
    • 1970-01-01
    相关资源
    最近更新 更多