在多线程Spring应用程序中避免MySQL死锁

时间:2015-06-18 12:10:06

标签: java mysql multithreading spring

情景很简单 我有一个包含两个表的大型MySQL数据库:

-- Table 1
id (primary key) | some other columns without constraints
-----------------+--------------------------------------
       1         |       foo
       2         |       bar
       3         |       foobar
      ...        |       ...

-- Table 2
id_src | id_trg | some other columns without constraints
-------+--------+---------------------------------------
   1   |   2    |    ...
   1   |   3    |    ...
   2   |   1    |    ...
   2   |   3    |    ...
   2   |   5    |    ...
   ...
  • 在table1上,只有id是主键。该表包含大约12M条目。
  • 在table2上id_srcid_trg都是主键,并且在table1的id上都有外键约束,并且还启用了选项DELETE ON CASCADE。该表包含大约110M条目。

好的,现在我正在做的只是创建一个我要从表1中删除的id列表,然后我正在执行一个简单的DELETE FROM table1 WHERE id IN (<the list of ids>);

后一个过程就像你猜测的那样也会从table2中删除相应的id。到目前为止一切顺利,但问题是当我在多线程环境中运行它时,我得到了很多Deadlocks

一些注释:

  • 没有其他流程同时运行,也不会(暂时)
  • 我希望这个快!我有大约24个线程(如果这确实在答案中有任何不同)
  • 我已经尝试了几乎所有的事务隔离级别(TRANSACTION_NONE除外)Java sql connection transaction isolation
  • 我认为订购/排序ID无济于事!
  • 我已经尝试了SELECT ... FOR UPDATE,但简单的DELETE最多需要30秒! (所以没有使用它):

    DELETE FROM table1 
    WHERE id IN ( 
        SELECT id FROM (
            SELECT * FROM table1 
            WHERE id='some_id' 
            FOR UPDATE) AS x);  
    

我该如何解决这个问题?

我希望得到任何帮助和提前感谢:)

修改:

  1. 使用InnoDB引擎
  2. 在一个帖子中,这个过程可能需要十几个小时甚至一整天,但我的目标是几个小时!
  3. 我已经在使用连接池管理器:java.util.concurrent
  4. 有关双嵌套SELECT的说明,请参阅MySQL can’t specify target table for update in FROM clause
  5. 要从数据库中删除的列表,总共可能包含几百万个条目,分为200个块
  6. FOR UPDATE条款是我听说它锁定了一行而不是锁定整个表
  7. 该应用程序使用Spring的batchUpdate(String sqlQuery)方法,因此会自动管理事务
  8. 所有ID都启用了索引,且ID最多为50个字符!
  9. {li} DELETE ON CASCADE id_srcid_trg(每个单独)意味着table1 id=x上的每次删除都会导致table2 id_src=x和{{}上的删除1}}
  10. 根据要求提供了一些代码:

    id_trg=x
  11. public void write(List data){ try{ Arraylist idsToDelete = getIdsToDelete(); String query = "DELETE FROM table1 WHERE id IN ("+ idsToDelete + " )"; mysqlJdbcTemplate.getJdbcTemplate().batchUpdate(query); } catch (Exception e) { LOG.error(e); } } 只是一个扩展myJdbcTemplate的抽象类。

4 个答案:

答案 0 :(得分:3)

首先,你传递id的第一个简单删除查询,如果你传递id到1000之类的限制,就不会产生问题(子表中的总行数也应该接近但不是很多像10,000等),但如果您传递的数量超过50,000或更多,则会产生锁定问题。

为避免死锁,您可以按照以下方法处理此问题(假设批量删除不属于生产系统) -

步骤1:通过选择查询获取所有ID并保留在游标中。

步骤2:现在逐个删除存储在游标中的这些ID。

注意:要检查删除获取锁定的原因,我们必须检查一些事项,例如您传递的ID数量,数据库级别设置的事务级别,my.cnf中的Mysql配置设置等等。 / p>

答案 1 :(得分:3)

删除许多(> 10000)父记录可能是危险的,每个父记录都有通过级联删除的子记录,因为您一次删除的记录最多,锁定冲突的可能性最大,导致死锁或回滚。

如果可以接受(意味着您可以与数据库建立直接的JDBC连接),则应该(此处不涉及线程):

  • 计算要删除的ID列表
  • 按批次删除它们(先验地在10到100之间)提交每100或1000条记录

由于较重的工作应该放在数据库部分,我几乎不怀疑线程在这里会有所帮助。如果您想尝试一下,我建议:

  • 一个单个线程(带有专用数据库连接)计算要删除的id列表并用它们对同步队列进行传播
  • 少量线程(4个可能是8个),每个线程都有自己的数据库连接:
    • 批量使用准备好的DELETE FROM table1 WHERE id = ?
    • 从队列中取出ID并准备批次
    • 每10或100条记录向数据库发送一批
    • 每10或100批提交

我无法想象整个过程可能需要几分钟。

经过其他一些阅读后,看起来我已经习惯了旧系统,而且我的数字非常保守。

答案 2 :(得分:0)

好的,这就是我所做的,它可能实际上并没有避免死锁,而是我唯一的选择。

此解决方案实际上是一种使用Spring 处理 MySQL死锁的方法。

抓住并重试死锁:

public void write(List data){
    try{
        Arraylist idsToDelete = getIdsToDelete();
        String query = "DELETE FROM table1 WHERE id IN ("+ idsToDelete + " )";
        try {
             mysqlJdbcTemplate.getJdbcTemplate().batchUpdate(query);
        } catch (org.springframework.dao.DeadlockLoserDataAccessException e) {
             LOG.info("Caught DEADLOCK : " + e);
             retryDeadlock(query); // Retry them!
        }
       } catch (Exception e) {
           LOG.error(e);
       }
}

public void retryDeadlock(final String[] sqlQuery) {
    RetryTemplate template = new RetryTemplate();
    TimeoutRetryPolicy policy = new TimeoutRetryPolicy();
    policy.setTimeout(30000L);
    template.setRetryPolicy(policy);
    try {
        template.execute(new RetryCallback<int[]>() {

            public int[] doWithRetry(RetryContext context) {
                LOG.info("Retrying DEADLOCK " + context);
                return mysqlJdbcTemplate.getJdbcTemplate().batchUpdate(sqlQuery);
            }
        });
    } catch (Exception e1) {
        e1.printStackTrace();
    }
}

答案 3 :(得分:0)

另一种解决方案可能是使用Spring的多个步骤机制 这样DELETE查询被拆分为3,因此通过删除阻塞列开始第一步,其他步骤分别删除其他两列。

Step1: Delete id_trg from child table;
Step2: Delete id_src from child table;
Step3: Delete id from parent table;

当然最后两个步骤可以合并为1,但在这种情况下需要两个不同的ItemsWriters!