MySQL事务及实现原理

时间:2024-11-07 07:01:00

目录

一、MySQL事务简单介绍

二、事务特性ACID介绍

原子性(Atomicity)

一致性(Consistency)

隔离性(Isolation)

持久性(Durability)

三、事务隔离级别

四、MySQL事务实现原理

(一)事务原理总述

(二)undo log 原子性分析

(三)redo log 持久性分析

(四)多版本并发控制(MVCC)隔离性分析

(五)MySQL的锁机制一致性与隔离性性分析

行锁与表锁

意向锁

扩展:意向锁、共享锁和排他锁的兼容性

行锁算法(记录锁+间隙锁+下一键锁)

死锁问题分析

扩展:避免死锁技巧

参考文章链接


一、MySQL事务简单介绍

MySQL事务是指一组操作,它们被看作一个单独的工作单元,要么全部成功,要么全部失败回滚。在MySQL中,事务可以确保数据的一致性和完整性。

事务通常由四个关键词来描述:

  1. BEGIN 或 START TRANSACTION:标志着事务的开始。
  2. COMMIT:表示事务完成,并把所有的修改持久化到数据库。
  3. ROLLBACK:表示事务的失败,并且撤销所有对数据库的修改。
  4. SAVEPOINT:可以设置事务的一个保存点,可以回滚到此处。

在MySQL中,只有使用了InnoDB存储引擎的表才支持事务。

当执行一系列的SQL语句时,如果其中有一个SQL语句执行失败,则所有SQL语句都会回滚,也就是说之前的所有SQL操作都被撤销,数据库回到之前的状态。而只有当所有SQL语句都执行成功后,它们才会被提交到数据库中,这就保证了数据的一致性。

事务在开发中常用于保证数据的完整性和一致性,例如在进行银行转账时,需要保证从一个账户扣除的金额一定会被转入到另一个账户中,如果出现了其中一个账户扣除了金额而另一个账户没有收到对应的金额的情况,那么这就是一种数据的不一致性。在这种情况下,使用事务可以保证这个问题不会发生,因为要么所有的操作都成功,要么都失败。

二、事务特性ACID介绍

原子性(Atomicity)

事务中的所有操作要么全部成功,要么全部失败回滚。如果有一个操作失败,则整个事务都应该回滚到最初状态。

例如,假设我们有一个银行转账系统。当我们从一个账户转账到另一个账户时,需要确保资金的安全和正确性。如果转账过程中任何一个步骤失败,例如金额不足或接收方账户不存在,则必须回滚到最初状态,确保事务的原子性。

一致性(Consistency)

在事务执行过程中,数据库必须始终保持一致状态。在事务执行的任何时刻,数据库必须满足一组事务的约束条件。

例如,假设我们有一个订单系统。当用户下订单时,订单总金额必须小于用户账户的余额。如果订单总金额大于用户账户的余额,则必须回滚事务,以保持一致性。

隔离性(Isolation)

事务应该在相互隔离的环境中执行,以避免并发执行时可能出现的问题。每个事务都应该以一种完全独立的方式执行,不受其他事务的影响。

例如,如果两个用户在同一时间购买同一件商品,系统必须确保两个用户看到的是正确的库存量,并且不会出现两个用户都买到同一件商品的情况。

持久性(Durability)

一旦事务成功提交,其结果就应该持久保存在数据库中,即使系统崩溃或重新启动,数据也应该仍然存在。

例如,假设我们有一个电子邮件系统。当用户发送电子邮件时,该邮件必须被保存在数据库中,即使系统在发送电子邮件后崩溃,也必须确保该邮件在系统恢复后仍然存在。

需要注意的是,不同的数据库管理系统对事务的实现方式可能会有所不同,因此在使用事务时,需要根据具体的数据库管理系统和应用场景来选择适合的实现方式。

三、事务隔离级别

在没有隔离级别的情况下,可能会发生以下情况:

  • 脏读(Dirty Read):一个事务读取了另一个事务还未提交的数据,如果这个事务回滚,那么读到的数据就是无效的,这种情况称为脏读。
  • 不可重复读(Non-repeatable Read):一个事务在执行过程中多次读取同一数据,由于其他事务对该数据进行了修改,因此这些读取操作得到的结果可能不同,这种情况称为不可重复读。
  • 幻读(Phantom Read):一个事务按照相同的查询条件两次查询,但是得到的结果集却不同。这是因为其他事务对该表进行了新增或删除操作,导致当前事务查询到的结果集不一致,这种情况称为幻读。

这些情况都是由于多个事务之间的数据相互干扰导致的,而隔离级别就是用来解决这些问题的。

事务的隔离级别规定了在一个事务内的修改哪些在事务内和事务间可见,哪些不可见。SQL标准定义了四个隔离级别,一般而言,隔离级别越高,安全性越高,但系统开销更大,并发性能也越差。

隔离级别 含义 脏读 不可重复读 幻读
读未提交(Read Uncommitted) 一个事务执行的操作,即使还未提交,也能被其他事务看到 存在 存在 存在
读已提交(Read Committed) 一个事务提交之后,其他事务才能看到该事务的修改 不存在 存在 存在
可重复读(Repeatable Read) 同一个事务内多次读取的结果一致 不存在 不存在 存在
可串行化(Serializable) 强制事务串行按顺序执行 不存在 不存在 不存在

通过如下SQL命令可以查看和修改MySQL的事务隔离级别

-- 查看全局事务隔离级别
select @@global.tx_isolation
-- 查看当前会话事务隔离级别
select @@tx_isolation
-- 修改全局事务隔离级别
set global transaction isolation level repeatable read
-- 修改当前会话事务隔离级别
set session transaction isolation level repeatable read

在实际应用中,读未提交级别在并发时会导致很多问题,性能相对于其他隔离级别提高也有限,可串行化级别强制事务串行,并发效率很低,只适合于对数据一致性要求极高的场景,这两个隔离级别都很少使用。因此在大多数数据库系统中,默认的隔离级别是RC(读已提交)或RR(可重复读)

MySQL的InnoDB默认隔离级别是RR(可重复读),但与标准SQL不同的是,InnoDB在RR(可重复读)隔离级别下,使用Next-Key锁避免了幻读问题。也就是说,InnoDB在RR隔离级别下已经能完全保证事务隔离性要求,即达到了SQL标准的Serializable隔离级别。

四、MySQL事务实现原理

(一)事务原理总述

MySQL 事务是基于 InnoDB 存储引擎实现的。MySQL 的事务原理主要包括以下几个方面:

  1. redo logInnoDB 在执行事务时,会将事务的修改操作记录在 redo log 中,以保证事务的持久性。redo log 记录了每个事务对数据所做的修改,包括修改的行、列和修改前后的值等信息。当事务提交时,会将 redo log 写入到磁盘中,以保证数据的持久性。
  2. undo logInnoDB 在执行事务时,会将事务的修改操作记录在 undo log 中,以支持事务的回滚和 MVCC 功能。undo log 记录了每个事务对数据所做的修改,包括修改的行、列和修改前的值等信息。当事务需要回滚时,会使用 undo log 中的信息将数据恢复到事务执行前的状态。
  3. MVCCInnoDB 实现了多版本并发控制(MVCC)来支持事务的隔离性。MVCC 是通过保存多个版本的同一行来实现的,每个版本都有一个唯一的时间戳,表示该版本的生命周期。在事务执行过程中,会根据当前事务的隔离级别确定可见的数据版本,以保证事务之间的隔离。同时也可以保证并发性。
  4. 锁机制InnoDB 通过实现共享锁和排它锁来保证数据的一致性和隔离性。共享锁用于读操作,可以多个事务同时持有;排它锁用于写操作,同一时间只能有一个事务持有。在事务执行过程中,会根据需要自动加锁和解锁,以保证数据的一致性、隔离性和并发性。
  5. 事务提交与回滚InnoDB 支持事务的原子性,一旦事务提交,就会将修改操作写入磁盘中,并释放所有锁。如果事务发生异常或被回滚,会将修改操作回滚,并释放所有锁。

综上所述,MySQL 事务的原理涉及多个方面,包括 redo log、undo log、MVCC、锁机制以及事务提交和回滚等。这些机制共同保证了事务的 ACID 特性,同时也保证了数据的一致性、并发性和持久性。

(二)undo log 原子性分析

undo log 是 InnoDB 存储引擎中用于实现事务回滚和 MVCC 的机制之一,可以保证事务的原子性。其原理如下:

当一个事务需要修改一行数据时,InnoDB 首先将该行数据的原始值拷贝到 undo log 中,然后执行修改操作。如果事务需要回滚,可以使用 undo log 中的原始值将数据恢复到修改前的状态。如果事务提交,则可以将 undo log 中的信息删除。

在事务执行期间,每次对数据进行修改时,InnoDB 将修改前的值保存到 undo log 中,以便在事务回滚时使用。如果事务提交,则将 undo log 中的信息删除,以保证数据的一致性。如果事务发生异常或回滚,可以使用 undo log 中的信息将数据恢复到事务开始前的状态,以保证事务的原子性。

总之,undo log 通过保存数据的原始值来保证事务的原子性,可以使得数据修改操作能够撤销和回滚,并确保数据的一致性

(三)redo log 持久性分析

redo log 是 InnoDB 存储引擎实现事务持久性的重要机制之一。在事务提交时,InnoDB 会将事务所做的修改操作记录在 redo log 中,并确保其持久化到磁盘上,从而保证数据的持久性。

具体来说,InnoDB 使用 WAL 技术(Write-Ahead Logging)来实现 redo log 的持久化。WAL 技术的基本思想是先将修改操作记录到 redo log 中,再将数据写入磁盘中。这样可以确保在出现宕机等异常情况时,可以通过 redo log 中的信息将数据恢复到事务执行前的状态,从而保证数据的一致性和持久性

在 InnoDB 中,redo log 是以固定大小的文件形式存在的。当 redo log 文件被写满后,InnoDB 会自动创建新的 redo log 文件,并将新的修改操作记录在新的文件中。旧的 redo log 文件可以在不影响数据一致性的情况下被删除,从而实现 redo log 的循环利用。

为了确保 redo log 的持久化,InnoDB 在写入 redo log 时会采用一些优化技术,例如 write-ahead logging 和 group commit。write-ahead logging 是指在修改数据之前,先将修改操作记录在 redo log 中,再将数据写入磁盘中。这样可以确保即使出现宕机等异常情况,也可以通过 redo log 中的信息将数据恢复到事务执行前的状态。而 group commit 是指将多个事务的提交操作合并到一起,一起写入 redo log 中,从而减少写入磁盘的次数,提高写入性能。

综上所述,InnoDB 通过 WAL 技术实现 redo log 的持久化,并采用 write-ahead logging 和 group commit 等优化技术来提高写入性能。这些机制共同保证了 MySQL 数据库的事务持久性,从而保证了数据的一致性和可靠性。

(四)多版本并发控制(MVCC)隔离性分析

MVCC(Multi-Version Concurrency Control)是 InnoDB 存储引擎用来实现事务隔离的一种技术。MVCC 技术通过为每个事务保存一个可见的数据版本,来实现在并发访问的情况下保证事务的隔离性。MVCC 主要涉及以下两个方面:

版本号

在 MVCC 中,每一行数据都会有多个版本号,每个版本号对应着一个事务,表示该版本是由该事务所修改的。事务在进行修改时,会为该行数据生成一个新的版本,该版本号比当前最大的版本号大1。而查询操作只能读取版本号小于等于当前事务的版本号的数据。

事务版本链

每个事务都有一个版本链,版本链是由该事务创建的所有版本所组成的链表。在该链表上,每个版本都指向前一个版本,最后一个版本指向 NULL。版本链的作用是,当事务需要回滚时,可以沿着版本链将数据恢复到事务开始的状态。

通过使用版本号和事务版本链,MVCC 实现了 InnoDB 存储引擎的多版本并发控制,同时也保证了事务的隔离性。在执行查询操作时,根据当前事务的隔离级别,InnoDB 存储引擎会选择可见的数据版本。在可重复读的隔离级别下,InnoDB 存储引擎会将当前事务的版本号作为可见的最大版本号,因此当前事务只能读取该版本号之前的数据版本,避免了脏读和不可重复读等问题。

MVCC只在RR和RC隔离级别下生效,不同的是RR级别下在事务第一个select语句开始的时候生成快照读视图,RC级别下每次select都会生成新的读视图。

需要注意的是,MVCC 技术虽然可以有效地提高并发性,但同时也会带来一些问题,如版本链过长可能导致性能问题,同时需要占用更多的存储空间来保存多个版本。因此,在使用 MVCC 技术时,需要权衡其带来的利弊,合理地设置事务隔离级别和存储空间等参数。

(五)MySQL的锁机制一致性与隔离性性分析

锁机制主要是为了保证并发事务的一致性和隔离性。在并发事务中,多个事务可能同时操作相同的数据,如果不进行锁定,就会产生数据不一致的问题。例如,两个事务同时对同一行数据进行修改,如果没有锁机制,可能会导致数据被覆盖,从而造成数据的不一致。

通过使用锁机制,可以保证每个事务在修改数据时,都能够独占相应的资源,防止其他事务对数据的并发操作,从而保证了事务的一致性。同时,锁机制也可以通过设置不同的隔离级别来保证事务之间的隔离性,避免不同事务之间的互相干扰和影响。

因此,锁机制既保证了并发事务的一致性,也保证了事务之间的隔离性。具体原理可以概括为:在事务修改数据之前,需要先获得相应的锁,获得锁之后,事务才可以修改数据,并且在整个事务期间,这部分数据都是锁定的,其他事务如果要修改数据,必须等待当前事务提交或回滚后释放锁

行锁与表锁

锁按照粒度可以分为行锁和表锁。表锁会锁定整张表,而行锁则只锁定需要操作的数据,显然行锁具有更好的并发性能。但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。

InnoDB同时支持表锁和行锁,出于性能考虑,绝大多数情况下使用的都是行锁。InnoDB实现了两种标准行级锁

  • 共享锁(S Lock):允许事务读一行数据。在select语句后面加上lock in share mode可以显式获取共享锁

  • 排他锁(X Lock):允许事务删除或更新一行数据。update、delete和insert语句会自动给涉及数据集加排他锁,select语句需要在语句后加上for update显式加排他锁

#排他锁
SELECT * FROM table_name WHERE ... FOR UPDATE;
#共享锁
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE;

意向锁

意向锁是一种特殊的表级锁,它是为了协调行级锁和表级锁而引入的。在进行行级锁定之前,InnoDB 存储引擎会先使用意向锁来协调并通知其它事务该行的锁定情况,从而提高并发性能。

意向锁分为两种类型:

  • 意向共享锁(Intention Shared Lock,IS锁):表示一个事务想要在某个数据行上加共享锁,此时会先设置该表的 IS 锁。当一个事务想要在某个数据行上加行级共享锁时,需要检查该表的 IS 锁是否存在,如果存在,则说明有其它事务想要在该表上加行级共享锁,此时需要等待其它事务释放 IS 锁后再进行加锁操作。
  • 意向排他锁(Intention Exclusive Lock,IX锁):表示一个事务想要在某个数据行上加排他锁,此时会先设置该表的 IX 锁。当一个事务想要在某个数据行上加行级排他锁时,需要检查该表的 IX 锁是否存在,如果存在,则说明有其它事务想要在该表上加行级共享锁或行级排他锁,此时需要等待其它事务释放 IX 锁后再进行加锁操作。

使用意向锁的主要目的是减少锁冲突,提高并发性能,同时保证数据的一致性。如果没有意向锁的协调机制,可能会导致不同事务之间的锁定产生冲突,从而降低并发性能。

扩展:意向锁、共享锁和排他锁的兼容性

IS IX S X
IS 兼容 兼容 兼容 不兼容
IX 兼容 兼容 不兼容 不兼容
S 兼容 不兼容 兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容

行锁算法(记录锁+间隙锁+下一键锁

在行锁中,有三种行锁算法:Record Lock、Gap Lock和Next-Key Lock。下面对这三种锁进行详细分析:

  • Record Lock(记录锁)Record Lock是在行上设置的锁,用于保证在事务中不会有其他事务对同一行进行修改。在事务中对某一行进行修改时,会对该行加上记录锁,其他事务需要对该行进行修改时,必须等待该记录锁被释放。
  • Gap Lock(间隙锁)Gap Lock是在索引记录之间设置的锁,用于防止其他事务在这些索引记录之间插入新的索引记录。在事务中对索引进行修改时,会对索引记录之间的间隙加上间隙锁,其他事务需要在这些间隙之间插入新的索引记录时,必须等待间隙锁被释放。
  • Next-Key Lock(下一键锁)Next-Key Lock是Record Lock和Gap Lock的结合体,同时锁住了索引记录和索引记录之间的间隙。在事务中对索引进行修改时,会对索引记录及其间隙加上下一键锁,其他事务需要对这些索引记录及其间隙进行修改时,必须等待下一键锁被释放。

在上述三种锁中,Record Lock用于保证行的并发访问,Gap Lock用于保证索引记录之间的并发访问,Next-Key Lock则是前两种锁的结合体,用于同时保证行和索引记录之间的并发访问。

需要注意的是,Next-Key Lock并不仅仅是Record Lock和Gap Lock的简单叠加,而是在两种锁的基础上增加了额外的约束条件。例如,Next-Key Lock会锁定当前索引记录及其间隙,并要求下一个索引记录不能被锁定。这种锁的机制可以有效地避免死锁的发生,同时保证数据的一致性和完整性

案例分析

CREATE TABLE user (
  id INT PRIMARY KEY,
  name VARCHAR(50),
  age INT
);

Record Lock行锁算法举例

当执行一个更新操作时,InnoDB 会将要更新的行加上行锁,以保证其它事务不能修改这一行,直到当前事务提交或回滚。如果其它事务要修改同一行,必须等待该行的行锁被释放。

现在执行以下两个事务:

事务 A:

BEGIN;
SELECT * FROM user WHERE id=1 FOR UPDATE;
-- do some updates
COMMIT;

事务 B:
BEGIN;
SELECT * FROM user WHERE id=1 FOR UPDATE;
-- do some updates
COMMIT;

当事务 A 执行 SELECT * FROM user WHERE id=1 FOR UPDATE; 语句时,会将 id=1 的行加上行锁。因此,当事务 B 执行 SELECT * FROM user WHERE id=1 FOR UPDATE; 时,会被阻塞,直到事务 A 释放行锁。

Gap Lock行锁算法举例

Gap Lock 的作用是锁定一个范围,但不包括记录本身。当一个事务要向一个不存在的记录插入数据时,InnoDB 会加上 Gap Lock,以确保没有其它事务插入相同的记录。同样的,如果其它事务要更新或删除这个范围内的记录,也会被阻塞。

现在执行以下两个事务:

事务 A:

BEGIN;
INSERT INTO user (id, name, age) VALUES (5, 'Tom', 25);
COMMIT;

事务 B:

BEGIN;
SELECT * FROM user WHERE id > 4 AND id < 6 FOR UPDATE;
-- do some updates
COMMIT;

当事务 A 执行 INSERT INTO user (id, name, age) VALUES (5, 'Tom', 25); 语句时,会加上 Gap Lock,锁定 id > 4 AND id < 6 这个范围。因此,当事务 B 执行 SELECT * FROM user WHERE id > 4 AND id < 6 FOR UPDATE; 时,会被阻塞,直到事务 A 释放 Gap Lock。

Next-Key Lock行锁算法举例

Next-Key Lock 不仅锁定范围,还锁定范围内的记录,以保证记录在范围内的行被锁定。Next-Key Lock 由两部分组成,一部分是 Gap Lock,一部分是 Record Lock。下面给出 Next-Key Lock 的三种典型情况:

  • 当事务 A 执行 INSERT INTO user (id, name, age) VALUES (5, 'Tom', 25); 语句时,会加上 Next-Key Lock,锁定 id = 5 这一行。因此,当事务 B 执行 SELECT * FROM user WHERE id = 5 FOR UPDATE; 时,会被阻塞,直到事务 A 释放 Next-Key Lock。
  • 当事务 A 执行 DELETE FROM user WHERE id = 5; 语句时,会加上 Next-Key Lock,锁定 id = 5 这一行。因此,当事务 B 执行 SELECT * FROM user WHERE id = 5 FOR UPDATE; 时,会被阻塞,直到事务 A 释放 Next-Key Lock。
  • 当事务 A 执行 UPDATE user SET age = 30 WHERE id = 5; 语句时,会加上 Next-Key Lock,锁定 id = 5 这一行。因此,当事务 B 执行 SELECT * FROM user WHERE id = 5 FOR UPDATE; 时,会被阻塞,直到事务 A 释放 Next-Key Lock。

Next-Key Lock 可以保证不会出现幻读的情况,因为 Next-Key Lock 不仅锁定了范围,还锁定了范围内的记录。而幻读是由于范围内有新插入的行导致的,Next-Key Lock 可以锁定这些新插入的行,从而避免了幻读的发生。

死锁问题分析

既然InnoDB对记录操作时会加锁,不可避免会出现死锁的问题。如果两个事务在执行过程中,都持有对方需要的锁,并且在等待对方释放锁,此时就发生了死锁

简单锁举例场景

假设我们有一张表user(id, name, age),id是主键,name是普通索引。索引数据如下

场景一:delete from user where id=3   走id主键索引,会直接锁主键索引上id为3的记录

场景二:delete from user where name='卢自清'   走name二级索引,会先锁住二级索引,然后再去锁聚簇索引上对应主键的记录

场景三:delete from user where age=20  不走索引,全表扫描,会对所有记录加锁

可以看到,查询条件的不同,加锁的结果也不一样。而死锁一般是事务相互等待对方的锁,最后形成环路造成的。

典型形成环路的死锁例子

操作不同表的相同记录

事务A 事务B

begin;

delete from table1 where id=2;

begin;

update table2 set msg='aaa' where id=1;

update table2 set msg='aaa' where id=1;
delete from table1 where id=2;

这个比较好理解,事务A持有表table1的id=2记录行锁,等待表table2的id=1的记录行锁,事务B持有表table2的id=1记录行锁,等待表table1的id=2记录行锁,两者互相等待,出现死锁

操作同一张表的相同记录

事务A 事务B

begin;

delete from table1 where id=2;

begin;

delete from table1 where id=1;

delete from table1 where id=1;
delete from table1 where id=2;

这个比较常见,事务在批量更新的时候,如果一个事务更新的顺序是[1,2],另一个事务更新的顺序是[2,1],就可能出现死锁

不同索引造成锁冲突

事务A 事务B

begin;

update user set name='张清风' where name='卢自清';

begin;

delete from user where  id=2;

这个就很隐晦了,事务A在执行时,除了在二级索引加锁外,还会在主键索引上加锁,在主键索引上加锁的顺序是[2,5],事务B执行时,只在主键索引上加锁,加锁顺序是[2]。[2]存在环路,有发生死锁的可能。

gap锁冲突

事务A 事务B

begin;

update user set name='张清风' where name='卢自清';

begin;

update user set name='张清风' where name='王澄泓';

insert into user values(null,'林可佳',19);
insert into user values(null,'闫澜飜',28);

事务A和事务B都持有gap锁,插入数据时都要等待对方的gap锁释放,发生死锁。

扩展:避免死锁技巧

由于死锁是个偶发性的问题,对线上造成的影响也难以预料,要求在业务层面采取措施避免死锁的发生,下面给出了几个可以避免死锁的技巧:

  • 以固定的顺序访问表和行,避免循环等待
  • 大事务更容易发生死锁,如果业务允许,将大事务拆小。
  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。
  • 降低隔离级别。如果业务允许,将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
  • 为表添加合理的索引。如果不走索引将会为表的所有行都加锁,增大了死锁的概率。

参考文章链接

  1. 阮一峰的《MySQL 教程》中的事务部分:/blog/2013/12/mysql_tutorial.html
  2. MySQL 官方文档中的事务部分:MySQL :: MySQL 8.0 Reference Manual :: 13.3.1 START TRANSACTION, COMMIT, and ROLLBACK Statements
  3. 阿里云数据库 RDS 的事务管理介绍:404错误页-阿里云帮助中心
  4. 《高性能 MySQL》一书中的事务部分:高性能MySQL(第3版) (豆瓣)
  5. MySQL 事务详解:/mysql/
  6. MySQL 事务隔离级别和锁机制详解:/lzrabbit/p/
  7. 深入浅出 MySQL 事务隔离级别:/-wenli/p/
  8. InnoDB 存储引擎下的事务管理机制:/s/kZzeCLylw5zJbgRv6oOUzA
  9. MySQL 存储引擎 InnoDB 事务原理详解:利用Rsync同步备份服务器数据-腾讯云开发者社区-腾讯云
  10. MySQL 事务深入分析:/kerrycode/p/