【问题标题】:Multithreading - Avoiding and dealing with database deadlocks多线程 - 避免和处理数据库死锁
【发布时间】:2009-12-08 11:09:38
【问题描述】:

我正在寻找一种在 Java 6 应用程序中处理数据库死锁的好策略;几个并行线程可能同时写入同一个表。如果检测到死锁,数据库 (Ingres RDMBS) 将随机终止其中一个会话。

考虑到以下要求,处理死锁情况的可接受技术是什么?

  • 应保留总经过时间 尽可能小
  • 终止会话将导致 显着(可衡量的)回滚
  • 时间线程没有办法
    相互交流,即 策略应该是自主的

到目前为止,我想出的策略大致如下:

short attempts = 0;
boolean success = false;
long delayMs = 0;

Random random = new Random();
do {
    try {
        //insert loads of records in table 'x'
        success = true;
    } catch (ConcurrencyFailureException e) {
        attempts++;
        success = false;
        delayMs = 1000*attempts+random.nextInt(1000*attempts);

        try {
                Thread.sleep(delayMs);
            } catch (InterruptedException ie) {
        }
    }
} while (!success);

可以以任何方式改进吗?例如等待固定数量(幻数)的秒数。 是否有不同的策略可以产生更好的结果?

注意:将使用几种数据库级技术来确保死锁在实践中非常罕见。此外,应用程序将尝试避免调度同时写入同一个表的线程。上述情况只是“最坏的情况”。

注意:插入记录的表组织为堆分区表,没有索引;每个线程都会在自己的分区中插入记录。

【问题讨论】:

    标签: java algorithm database-deadlocks scenarios ingres


    【解决方案1】:

    一种常用的方法是某种形式的指数回退。而不是您的1000*attempts+random 方法,使延迟成为尝试次数的指数函数。这可确保在前一两次尝试中将延迟降至最低,如果您死锁可能只是运气不好,但在以后很明显连接确实拥塞时会给您带来更大的延迟。

    当然,另一种方法是尝试安排您的数据库访问,以减少发生死锁的可能性。但是,如果不知道您的查询是做什么的(以及如何以及何时执行),就不可能说是否可以做到

    【讨论】:

    • 指数函数听起来不错 - 我会尝试模拟它!正如我在注释中提到的,应用程序设计将旨在避免死锁,包括有利地安排数据库访问。但不能保证在极端情况下会发生大量死锁。
    • 您能否为您提到的“指数退避”技术提供任何技术参考?
    • 常用于网络协议中避免拥塞。查看 wiki 文章:en.wikipedia.org/wiki/Exponential_backoff 但基本思想很简单。您只需使用某种指数函数来确定每次重试尝试的延迟。可以调整确切的细节以适合您的目的。当然,最简单的可能实现是延迟 2^n 毫秒,其中 n 是到目前为止的重试次数。但也许你认为开始时增长太慢,或者开始太低,或者增长太快。然后你只需添加一个乘数,或添加一些东西到 n
    • 基本思想很简单,前 1-3 次尝试应该很便宜并且延迟很短。很可能他们只是运气不好,您几乎可以立即重试,您将避免死锁。但是如果一直失败,就说明连接拥塞了,你必须退后重试,减少连接压力。如果线路拥塞,所有客户端都会这样做,因此拥塞会自行解决,因为所有客户端等待的时间越来越长,直到您找到平衡点。
    • 同样重要的是:为延迟添加随机抖动!否则,你可能会在几乎与其他人尝试第 i 次的同时进行第 i 次尝试,因为我会不断增加...
    【解决方案2】:

    我们就是这样做的。循环并重试事务,直到完成。

    我们没有搞乱随机延迟。

    此外,我们在 try 块内进行了提交,并在异常处理程序中进行了回滚。

    当您有多个可锁定资源和多个并发事务时,死锁是不可避免的。这是争用锁的逻辑结果。

    如果您避免争用锁(即悲观的表级锁定),那么您也倾向于防止并发。如果您可以定义不争用锁的事务,则可以避免死锁。然而,对同一张表的并发访问几乎就是死锁的定义。

    加载时,插入(特别是在 HEAP 表中)可以(通常)并行进行,而不会出现很多争用问题。如果您延迟构建索引,则在插入期间不会进行其他更新。

    因此,您可以通过删除索引、将组织更改为堆、加载多个并发进程(或线程,拥有多个进程通常更快)然后构建索引(并可能重新组织表),您也许可以避免死锁。

    在进行更新或删除时,帮助不大。

    【讨论】:

    • 在我的模拟中(20 个线程),零延迟将触发大量死锁;大的延迟实际上会显着改善整体经过的时间。 autocommit 设置为 on - 但数据库死锁也将涉及自动回滚
    • @Adrian:我们有相当复杂的错误处理,所以我们做了回滚“只是为了确定”。我们也在用 C 语言工作,所以这是一个“虚拟异常”。最后,我们有一些 C/Ingres n00b 进行编码,所以我们这样做以防它们隐藏了一些小的逻辑错误。
    • @Adrian:在我们的实际实践中,是否存在延迟并没有产生任何实际差异。一个负载很重的操作系统会引入它自己的随机延迟。 YMMV。我不是在讨论你的模拟。我告诉你我们做了什么。我们没有搞乱随机延迟。
    • @S.Lott 我们确实使用没有索引的堆表(实际上是分区堆表)
    【解决方案3】:

    如果您不需要同时访问数据库,一个简单的解决方案可能是删除它并使用任务处理队列来更新数据库,通过队列对数据库进行序列化访问。我意识到这将为您的应用程序引入一个异步元素,因此不适用于大多数用户启动的应用程序或在线 Web 应用程序,但对于批处理/离线类型的应用程序可能值得考虑(我意识到可能不是您想要的答案不过)。

    【讨论】:

    • 处理队列会大大降低应用程序的速度——不惜一切代价避免死锁,这不是一个选择。生产服务器是一个 4 处理器的 Unix 服务器,应该可以轻松处理一些并行线程
    【解决方案4】:

    对于像 Ingres 这样的数据库,您总是会遇到一些死锁,因此您必须假设任何插入、更新或删除都将失败并有一个重试策略(如您的示例中所示)。 您应该设计您的数据库,以使争用最小化并且死锁很少发生。如果您在多次重试后仍不断出现事务失败,那么这表明您必须进行一些重大的数据库重新设计(或转移到像 Oracle 这样的系统,通常可以通过适当的使用来设计应用程序以避免死锁行级锁定)。

    【讨论】:

    • 没问题,但我想补充一点,如果您遇到太多死锁/回滚/重试,序列化事务可能是一个更快的选择
    • 迁移到像 Oracle 这样的另一个 RDBMS 不是这个项目的选项。尝试了序列化,它比“正常”运行慢很多,死锁数量很少
    【解决方案5】:

    这是怎么回事?

    short attempts = 0;
    boolean success = false;
    long delayMs = 0;
    
    Random random = new Random();
    do {
    try {
         synchronized(ClassName.class) {
             //insert loads of records in table 'x'
          }
    
        success = true;
    } catch (ConcurrencyFailureException e) {
        attempts++;
        success = false;
        delayMs = 1000*attempts+random.nextInt(1000*attempts);
    
        try {
                        Thread.sleep(delayMs);
                } catch (InterruptedException ie) {
        }
      }
    } while (!success);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-11-24
      • 1970-01-01
      • 2021-08-02
      相关资源
      最近更新 更多