我在 Mysql 5.1 数据库中有一个 InnoDB 表,它有大约 2700 万行。该表有三个未索引mediumint unsigned
的列,我希望能够定期全局重置为“0”。例如:
update myTable set countA = 0;
这个非常简单的更新查询遇到了 InnoDB 的行级锁定问题。锁定太多行后,更新查询失败,并出现有据可查的错误:
ERROR 1206 (HY000): The total number of locks exceeds the lock table size
问题在于,对于如此大的表,单个行锁的数量已经超过了为存储锁分配的空间。
我找到了一些关于如何处理这个问题的建议:
锁定整个表以关闭行锁定
这似乎是最好、最干净的解决方案,而且我对在这些不频繁的操作期间将这个特定的表锁定几分钟没有任何问题。问题是,给定的解决方案实际上对我不起作用。也许它是用来与旧版本的 Mysql 一起工作的东西?
增加锁缓冲区的大小
通过增加 Mysql 变量的值innodb_buffer_pool_size
,我们可以为行锁创造更多的空间。我对这个解决方案感到非常不舒服,因为即使我可以分配足够的空间,随着表的增长,我也会为失败做好准备。此外,这似乎是一个糟糕的设置,需要创建千兆字节的可以说是不必要的锁。
索引受影响的列(见注释)
如果我们正在对适当索引支持的单个列进行批量更新,那么 InnoDB 可以避免锁定所有行。通过使用索引,它可以只锁定受影响的行。我实际上试过了,但发现管理这三个索引会使我的增量更新慢很多。由于我将有数以千万计的更新查询为需要重置计数的每个实例调整这三个计数,我不想牺牲增量更新的效率。
分批更新列
源文档将其描述为一种变通方法,但我发现它在某种程度上非常有效:
update myTable set countA = 0 where countA != 0 limit 500000;
通过重复执行此操作,直到受影响的行数小于指定的limit
,所有行都会得到更新。这个解决方案在特别大的表上对我来说失败了,因为 Mysql 必须进一步寻找匹配的行,因此可以在单次迭代中更新的行数急剧下降。到 1,000 行更新对于一次执行来说太多了,我仍然有数百万个非零值要更新。
那么我还剩下什么可能性呢?
- 停止使用 InnoDB:这需要对我当前的流程进行一些其他重组,但我会考虑这样做。
- 将计数列移出主表:如果我有一个 CountA 表,那么我可以通过使用重置计数,
delete from CountA
并且可以通过对主表的内部连接来检索计数。这会减慢我对单个计数的更新速度,因为在有条件地更新或在 CountA 表中插入一行之前,我必须从主表中获取 id。不是很好,但我会考虑。 - 其他既是干净的解决方案,又是可以预期与我的桌子一起合理增长的解决方案?
更新:在接受响应的帮助下,我现在有了一个批处理实现,它可以在大约五分钟内完成工作。尽管我更希望不需要批处理,但在出现更直接的解决方案之前似乎是这样。如果它有助于下一个人偶然发现这个问题,这是我相关的 Java JDBC 代码。(也建议阅读从已接受答案链接的博客文章。)
int batchsize = 10_000;
PreparedStatement pstmt = connection.prepareStatement
("UPDATE tableName SET countA = 0, countB = 0, countC = 0 "
+ "WHERE id BETWEEN ? AND ?");
for (int left = 0; left < maxId; left += batchsize) {
pstmt.setInt(1, left + 1);
pstmt.setInt(2, left + batchsize);
pstmt.executeUpdate();
}
pstmt.close();