PreparedStatement 批处理 Insert/Update - 死锁问题

PreparedStatement Batch Insert/Update - DeadLock Issue

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

ERROR: deadlock detected
Detail: Process 30655 waits for ExclusiveLock on relation 295507848 of database 17148; blocked by process 30662.

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

ERROR: current transaction is aborted, commands ignored until end of transaction block

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

    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();
            }
            
        }
    }
    
}

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

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

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

-- 结束编辑 ---

你的第二个问题是缺乏烦人的指数退避。

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

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

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

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

'exponential' 部分的意思是:随着重试的进行,使延迟更长(并且仍然是随机的)。

你最后的错误是你总是重试,而不是只在合理的时候重试。

这是一个指数随机退避示例,它可以解决您的所有问题,但您需要重新创建 (Prepared)Statement 对象并关闭旧对象的部分除外;您的代码片段不清楚发生的位置。

} (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 被设计成对 'end users' 来说令人难以置信的恼人、前后矛盾和丑陋——那是因为 JDBC 的目标受众不是你。这是数据库供应商。这是可以想象到的最低级别的胶水,具有各种奇怪的技巧,因此所有数据库供应商都可以公开他们的宠物功能。

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

例如,JDBI 很棒,并且非常支持使用 lambda 进行重试。