【问题标题】:PreparedStatement Batch Insert/Update - DeadLock IssuePreparedStatement 批量插入/更新 - 死锁问题
【发布时间】:2020-11-27 19:54:02
【问题描述】:

我正在开发一个支持多个数据库的系统。所有的插入和更新都是批量发生的,同样是在 PreparedStatement 批处理的帮助下实现的。但是,对于 PostgreSQL,有很多时候它会导致批量更新死锁。我想知道是否有办法避免这种情况。

错误:检测到死锁
详情:进程 30655 等待数据库 17148 的关系 295507848 上的 ExclusiveLock;被进程 30662 阻止。

我有重试逻辑,但它给出了:

错误:当前事务被中止,命令被忽略,直到事务块结束

我对如何处理这种情况有点困惑。

    public void InsertUpdateBatch(String auditPrefix, String _tableName, TableStructure<?> ts, 
    StringBuilder sb, String operation) throws Exception {
    
    boolean retry = true;
    boolean isInsert = "insert".equalsIgnoreCase(operation) ? true : false;
    int minTry = 0;
    int maxTries = 2;

    ThreadLocal<PreparedStatement> statement = isInsert ? pstmt : updateStmt;
    ThreadLocal<List<Object[]>> dataToProcess = isInsert ? insertBatchData : updateBatchData;
    
    while (retry) {
        try {
            long t1 = System.currentTimeMillis();
            int[] retCount = {};
            
            retCount = statement.get().executeBatch();
            
            // Clearing the batch and batch data
            statement.get().clearBatch();
            dataToProcess.get().clear();
            
            if(isInsert) {
                syncReport.addInsert(ts.getTableName(), retCount.length);
            } else {
                syncReport.addUpdate(ts.getTableName(), retCount.length);
            }
            
            this.syncReport.addDatabaseTime(t1, System.currentTimeMillis());

            retry = false;
            
        } catch (Exception e) {
            // Clearing the batch explicitly
            statement.get().clearBatch();
            
            log.log(Level.INFO, "Thread " + Thread.currentThread().getName() + ": tried the operation " + operation + " for "  + (minTry + 1) + " time(s)");

            if (++minTry == maxTries) {
                retry = false;
                minTry = 0;
                e.printStackTrace();
                commitSynchException(auditPrefix, _tableName, ts, sb, operation, isInsert, e);
            } else {
                
                trackRecordCount(e, ts, !isInsert);
                // Rebuild Batch
                rebuildBatch(ts, dataToProcess.get(), e);
                // Clearing old batch data after rebuilding the batch
                dataToProcess.get().clear();
            }
            
        }
    }
    
}

【问题讨论】:

    标签: java postgresql prepared-statement database-deadlocks


    【解决方案1】:

    重试是解决方案。但是你没有正确实现它。

    -- 按照@Mark Rotteveel 的建议进行编辑--

    您需要在连接上显式调用.abort(),然后您可以重试。您可能可以保留 PreparedStatement / Statement 对象,但如果您仍然遇到问题,请考虑关闭并重新创建这些对象。

    -- 结束编辑 ---

    您的第二个问题是缺乏 nagled 指数退避。

    计算机是可靠的。非常可靠。比瑞士手表好。

    如果两个线程执行一项工作,并且作为该工作的一部分,它们彼此死锁,并且它们都会看到这一点,中止它们的事务并重新开始,那么...

    很可能完全相同的事情会再次发生。然后再次。然后再次。然后再次。在不幸的情况下,计算机可能会如此可靠。

    解决方案是随机指数退避。确保两个线程不会以完全相同的顺序和完全相同的时间以完全相同的方式继续做事的一种方法是字面上开始抛硬币以强制降低稳定性。这听起来很愚蠢,但如果没有这个概念,互联网就不会存在(以太网的工作原理就是这样:以太网上的所有系统都会立即发送数据,然后检查线路上的尖峰,表明多方同时发送,并且结果是无法阅读的混乱。如果他们检测到这一点,他们随机等待指数退避然后再次发送。这个看似疯狂的解决方案击败了令牌环网络的裤子)。

    “指数”部分的意思是:随着重试的到来,使延迟更长(并且仍然是随机的)。

    您的最后一个错误是您总是重试,而不是仅在明智的情况下重试。

    这是一个示例指数随机退避,它可以解决您的所有问题,除了您需要重新制作(准备好的)Statement 对象并关闭旧对象的部分;你的 sn-p 并不清楚发生在哪里。

    } (catch SQLException e) { // catch SQLEx, not Ex
        String ps = e.getSQLState();
        if (ps != null && ps.length() == 5 && ps.startsWith("40")) {
            // For postgres, this means retry. It's DB specific!
            retryCount++;
            if (retryCount > 50) throw e;
            try {
                Thread.sleep((retryCount * 2) + rnd.nextInt(8 * retryCount);
                continue; // continue the retry loop.
            } catch (InterruptedException e2) {
                // Interrupted; stop retrying and just throw the exception.
                throw e;
            }
         }
         // it wasn't retry; just throw it.
         throw e;
    }
    

    或者,帮自己一个大忙,放弃所有这些工作并使用图书馆。 JDBC 被设计成对“最终用户”来说非常烦人、不一致和丑陋——这是因为 JDBC 的目标受众不是你。这是数据库供应商。这是可以想象的最低级别的胶水,具有各种奇怪的技巧,因此所有数据库供应商都可以公开他们的宠物功能。

    那些使用 JDBC 访问数据库的人应该使用构建在上面的抽象库!

    例如,JDBI 很好,并且非常好地支持重试,使用 lambdas。

    【讨论】:

    • 在 JDBC 语句中不要“属于”事务:不需要关闭语句或准备好的语句。问题是 OP 在重试之前需要回滚事务。
    • @MarkRotteveel 嗯,我的错误 - 将更新答案。
    猜你喜欢
    • 2015-03-29
    • 1970-01-01
    • 2011-11-15
    • 2012-02-16
    • 2019-01-14
    • 2016-11-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多