MySQL 事务是如何工作的? MySQL 在哪里存储字段的临时值?

How do MySQL Transactions work under the hood? Where does MySQL store the temporary value of a field?

我知道 MySQL 事务可以让您一次执行多个 inserts/updates,其中全部或 none 都会成功,我知道如何使用它们。

我不确定 MySQL 如何在很长一段时间内保存所有数据,以及这可能会对性能产生什么影响。

在另一个事务提交之前选择

我有一个 table 的 100 个人,名字叫“John”,我在交易中循环更新每个名字为“Jane”,每次更新需要 1 秒,这意味着完成交易需要 100 秒.

如果我在那 100 秒内从另一个进程创建 select,结果将是“John”而不是“Jane”。如果我在事务提交后创建 select 它将 return "Jane".

一切都很好,我对此作品并不感到困惑。

在交易中选择

这是更令人困惑的一点。

我有一个 table 有 100 个名为“John”的人,我开始了一个交易,我在其中循环遍历 select 每一行。每个 select 查询需要 1 秒,所以这需要 100 秒。

50 秒后,另一个进程(不在事务内)将每一行更新为“Jane”。

在我的第一个流程中,在交易中,即使在对“Jane”的更新完成后,我仍然会收到“John”作为结果。

要明确时间是这样的:

它是如何工作的

所以现在我可以同时执行 SELECT name FROM names WHERE id = 31 并且有一个 return “John” 和一个 return “Jane” 取决于我是否在交易中,或者交易开始的时间。

MySQL 必须以某种方式将此字段的值存储两次。

需要复印吗?

我不认为它需要数据库或 table 的副本,因为当您开始交易时它不知道您要接触什么 table。直到事务开始 10 分钟后,您才可以触摸 table,但无论其他进程在此期间进行了多少修改,数据仍然是 10 分钟前的。

我还试验过数据库和 tables,它们的大小为 GB,需要几分钟才能转储,不可能制作完整副本。

在什么地方临时搁置?

也许它暂时将字段的值保存在某处等待事务完成?

然后在执行 select.

时需要检查是否有未决值

因此 SELECT name FROM names WHERE id = 31 相当于:

// John
if (pending_value('names', 'name', 31) {
   // Jane
   name = get_pending_value('names', 'name', 31);
} else {
   // John
   name = get_db_value('name', 'name', 31);
}

这显然是非常愚蠢的伪代码,但它本质上是在说“是否有待处理的更新?如果有,请改用它”

这大概会保存在某个地方的内存中?或者也许是一个文件?还是系统数据库之一?

它如何影响性能

如果我的 names table 有 10 亿行,而我们执行相同的查询,那么 MySQL 会同时知道 10 亿行包含值“John”,而那 10 亿行rows 的值为“Jane”。这肯定会影响性能。

但是受影响的是事务内的查询还是事务外的查询?

例如

  1. 进程 1 = Begin transaction
  2. 进程 2 = UPDATE names SET name = "Jane"
  3. 进程 1 = SELECT name FROM names WHERE id = 31 //约翰
  4. 进程 2 = SELECT name FROM names WHERE id = 31 //简

步骤 (3) 或步骤 (4) 中的查询对性能有影响还是两者都有?

一些线索:

  • 阅读“MVCC”——多版本并发控制
  • tentatively-changed 行保留到 COMMITROLLBACK。 (请参阅文档中的“历史列表”。)这是 row-by-row,而不是整个 table 或数据库。它不会“将行锁升级为 table 锁”。
  • 每个 table 的每一行都有一个 transaction_id。每个新的 t运行saction 都有一个新的、更高的 id。
  • 那个 xaction id,连同“t运行saction 隔离模式”,决定了你的 t运行saction 可以“看到”每一行的哪个副本。所以,是的,可以有多个“行” WHERE id = 31.
  • 行被锁定,而不是 tables。在您的某些示例中,t运行sactions 运行 有一段时间,然后偶然发现了 'same' 行。
  • 在某些情况下,行之间的“间隙”是锁定的。 (我在你的例子中没有注意到这一点。)
  • 整个 table 仅针对 DDL(删除、更改等)锁定,而非 DML(Select、更新等)
  • 当发生冲突时,可能会出现“死锁”。这是每个 t运行saction 都在等待另一个释放锁的时候。一个 t运行saction 自动回滚。
  • 当发生冲突时,可能会发生“锁等待”。这是带锁的 t运行saction 最终会放手,让等待的 t运行saction 继续。
  • 当发生冲突并发生“锁定等待”时,innodb_lock_wait_timeout 控制放弃之前的时间。
  • 每个语句都在一个 t运行saction 中。当 autocommit=ON 时,每个语句都是对自身的 t运行 动作。 (您的最后一个示例缺少 BEGIN,在这种情况下进程 2 有 2 个单独的 t运行sactions。)

在您的第一个示例中,read_uncommitted 的隔离模式会让您看到另一个 t运行saction 发生的变化。这是一种很少使用的模式。其他模式在 COMMITted 之前不会让您看到更改,如果是 ROLLBACK'd 则永远不会看到更改。 (是的,每个更改的行都有一个副本。)

repeatable_read 模式(和其他模式)有效地限制您只能看到 transaction_id 或更早的行。因此,即使在 12:00:31,您仍然会看到“John”。

一般建议:

  • 不要编写运行时间超过几秒的 t运行action
  • 记得在适当的地方使用 SELECT ... FOR UPDATE——这会在 SELECT 中的行上添加更强的锁,以防它们在 t运行 操作中被更新或删除.
  • 实际情况下最好有一个INSERT加100行;那将是 100 single-row INSERTs 的 10 倍。 (与 UPDATEDELETE 类似。)
  • 使用SHOW ENGINE=InnoDB STATUS;(我发现它在处理死锁方面很有用,但对其他用途来说很神秘。)