【问题标题】:Understanding MySQL InnoDB locks了解 MySQL InnoDB 锁
【发布时间】:2021-10-15 07:37:58
【问题描述】:

我对 MySQL InnoDB 锁的工作原理感到困惑。

为了更好地理解它,我进行了以下实验。我使用 Spring boot 和 JPA 在下表中插入/更新记录(尽管这个问题与 Spring boot 或 JPA 无关)

表名: test

Column name Type Constraint
id Bigint PK AI
name Varchar
val Int
br_id Int

Innodb 锁等待超时: 50 秒

存储库: ITestRepository.java

@Modifying
@Query(value = "update test set val=val+1 where val>:val and br_id=:brId", nativeQuery = true) // Just a random update query
public void updateVal(Integer val, Integer brId);

服务: TestService.java

@org.springframework.transaction.annotation.Transactional
public void saveMany(final int brId) { // brId is always passed as 1
    for (int i = 0; i < 200; i++) {
        this.testRepository.updateVal(i, brId); // ======> LINE: 1
        this.testRepository.save(new TestEntity(String.valueOf(i), i, brId)); // ======> LINE: 2
        try {
            Thread.sleep(2000);
        } catch (final InterruptedException e) {
            e.printStackTrace();
        }
    }
}

@org.springframework.transaction.annotation.Transactional
public void saveOne(final int val, final int brId) { // brId is always passed as 2
    this.testRepository.updateVal(val, brId); // ======> LINE: 3
    this.testRepository.save(new TestEntity(String.valueOf(val), val, brId)); // ======> LINE: 4
}
  • 注意 1: 在执行以下任何情况之前,表格总是被截断。
  • 注意 2: 上面的update 查询是故意不会更新任何内容的。

案例1: 只触发一次saveMany方法

在这种情况下,一切正常,事务完成后插入 200 行,没有任何错误。

案例2: 先触发saveMany,然后触发saveOne一次

在这种情况下,如果我们触发 saveOne,则在执行 saveMany 时,saveOne 将在 50 秒后失败并出现 Lock wait timeout 异常,并且一旦循环结束,saveMany 将成功完成。

案例 3: 评论 LINE: 1 & LINE: 3 或评论 LINE: 2 & LINE: 4 并首先触发 saveMany,然后 saveOne 一次

在这种情况下,如果我们触发saveOne,则会在执行saveMany 时,一切都会正常工作,两种方法都会成功完成,没有任何异常。

案例4: 先评论LINE: 1并触发saveMany,然后saveOne一次

在这种情况下,如果我们触发 saveOne,则在执行 saveMany 时,saveOne 将在 LINE: 3 上失败,lock wait timeout 更新操作异常。

案例5: 先评论LINE: 2并触发saveMany,然后再触发saveOne一次

在这种情况下,如果我们触发 saveOne,则在执行 saveMany 时,saveOne 将在 LINE: 4 上失败,lock wait timeout 插入操作异常。

根据以上案例,我的推导如下:

  1. 并行 insertsupdates 不锁定表
  2. 并行insertupdate会锁表(先触发的操作会先获取锁)

我无法理解上述两个推导是如何工作的。我的意思是从 MySQL 文档中,他们声明整个表在 InnoDB 中永远不会被锁定,并且在执行 insertsupdates 时只有行被锁定。正如您在上述情况中看到的,br_id 对于这两种方法总是不同的,因此updates 正在不同的行集上执行,那么为什么会引发lock wait timeout 异常?此外,并行 insertsupdates 不会导致任何问题,如何以及为什么?

编辑 1:

如果br_id没有被索引,那么它的工作方式和上面的例子一样,但是如果br_id被索引,那么Deadlock found when trying to get lock; try restarting transaction在并行执行saveOne方法时会立即抛出异常saveMany方法。

【问题讨论】:

  • 是否 val 和/或 br_id 已编入索引?
  • @Shadow 不,不是。我已经在表格中提到了所有可用的约束。
  • Innodb 锁定它需要扫描的记录。如果没有可用的索引,那么它可能会锁定表中的所有记录。
  • 所以你的意思是如果我索引br_id,它不会锁定表?让我试一试。
  • 但是,为什么并行 insertsupdates 没有引起任何问题?

标签: mysql transactions locking innodb


【解决方案1】:
update test
    set val = ...
    where val > ...
      and br_id = ...

由于相关列上没有索引,所有行都被锁定。

最好有

INDEX(br_id)

INDEX(br_id, val)

后者更适合定位要修改的行,但缺点是需要更新索引的 BTree。

请注意,有些“锁”是“间隙锁”。这涉及锁定索引值之间的差距。这有时会引起意外。 (我不知道这是否与此更新有关。)

查看锁的一个工具是SHOW ENGINE INNODB STATUS;(由于那里的信息是短暂的,它可能无法显示您需要的信息。)

BEGIN;
UPDATE ...
((spend lots of time before COMMIT))
COMMIT;

这将延迟或死锁任何需要相同行锁的竞争连接。

几乎总是 InnoDB 可以看到为了获得必要的锁而简单地停止(最多 innodb_lock_wait_timeout 秒)。

InnoDB 可以总是发现死锁,并ROLLBACK 一个事务,同时让另一个事务完成。

有几种情况(尤其是“间隙锁定”,其中 InnoDB 在可能没有死锁时保守地声明死锁。(我不知道这是不是你的情况。)

【讨论】:

  • 感谢您的回答,瑞克。正如我所提到的,INDEX(br_id) 导致死锁(不知道如何以及为什么)。此外,update 查询的构建方式不会更新表中的任何内容。并行updates 或并行inserts 也不会引起任何问题。为什么以及如何?
  • @JigneshM.Khatri - 你运行autocommit 的值是多少?你有明确的BEGIN...COMMIT 吗? Begin 和 Commit 之间需要多长时间?向我们展示 Update 语句的 SQL。提供SHOW CREATE TABLE
  • 从我的问题来看,saveMany 方法将在几秒钟后一次提交所有 200 个 insertupdate 操作。 saveMany 中的每个 insert-update 操作之间有 2 秒的故意延迟。正如我所说,update 的 SQL 已经被提及。见ITestRepository.java
  • 有一百个框架掩盖了 SQL。我可以为 SQL 提供建议,但我无法学习所有数百个框架。如果您提供生成的 SQL,我可以提供进一步帮助。否则,您必须等待知道两个您正在使用的框架 MySQL 详细信息的人给您答案。建议你加一个[tag]来帮助得到这样的人。
  • 要实际查看当前使用的锁,请查看表 performance_schema.data_locks。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多