赞
踩
InnoDB是MySQL数据库的一个存储引擎,它支持事务处理。事务处理是数据库管理系统执行过程中的一个逻辑单位,由一个或多个SQL语句组成,这些语句要么全部执行,要么全部不执行,是一个不可分割的工作单位。InnoDB通过以下方式来实现事务:
原子性(Atomicity):原子性确保事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。InnoDB使用日志(redo log)来确保事务的原子性。当事务提交时,相关的变更首先会被写入到redo log中,然后再异步地应用到实际的数据页上。如果事务在执行过程中由于某些原因(如系统崩溃)被中断,那么在系统恢复时,InnoDB可以通过重放redo log中的记录来确保事务的变更被正确地应用到数据上。
一致性(Consistency):一致性确保数据库从一个一致性状态转变到另一个一致性状态。在InnoDB中,一致性是通过数据库的各种约束(如主键约束、外键约束、唯一约束等)以及事务的隔离级别来保证的。例如,在事务执行过程中,InnoDB会检查数据是否满足这些约束,如果不满足,事务会回滚,从而确保数据的一致性。
隔离性(Isolation):隔离性确保多个并发事务执行时,一个事务的执行不应影响其他事务。InnoDB通过多版本并发控制(MVCC)来实现事务的隔离性。MVCC允许每个事务看到一个一致的数据快照,就好像它是独立运行的一样。每个事务在开始时都会获取一个版本的数据快照,并在该快照上进行操作,而不会直接修改原始数据。其他并发事务的修改对当前事务是不可见的,直到当前事务提交或回滚。
持久性(Durability):持久性确保一旦事务提交,其所做的变更就永久地保存到数据库中。在InnoDB中,持久性是通过将事务的变更写入到磁盘上来实现的。除了前面提到的redo log之外,InnoDB还有一个undo log,用于在事务回滚时撤销所做的变更。这些日志都是持久化到磁盘上的,以确保在系统崩溃或重启后,事务的状态可以被正确地恢复。
Innodb通过Buffer Pool,LogBuffer,RedoLog,Undo Log来实现事务,以一个update语句为例:
Innodb在收到一个update语句后,会先根据条件找到数据所在的页,并将该页缓存在BufferPool中。
执行update语句,修改BufferPool中的数据,也就是内存中的数据。
针对update语句生成一个RedoLog对象,并存入LogBuffer中。
针对update语句生成undolog日志,用于事务回滚。
如果事务提交,那么则把RedoLog对象进行持久化,后还有其他机制将BufferPool中所修改的数据页持久化到磁盘中。
如果事务回滚,则利用undolog日志进行回滚。
锁表会带来一系列问题,影响数据库的性能和系统的稳定性。主要是下面的四个问题: 性能问题、死锁问题、可用性问题、一致性问题。
锁表会阻止其他事务对该表的并发访问,包括读操作和写操作。锁表会导致严重的性能问题:
在高并发环境下,锁表更容易导致死锁的发生。锁表会导致严重的事务问题:
长时间的表锁定会影响数据库的可用性,使得应用程序无法及时处理用户请求。锁表带来的系统可用性问题:
在高并发写操作场景下,锁表会导致数据一致性问题。锁表带来的数据一致性问题:
从操作的粒度可分为表级锁、行级锁和页级锁。
每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。表级锁应用在MyISAM、InnoDB、BDB等存储引擎中。表锁的特点:
每次操作锁住一行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。行级锁应用在InnoDB 存储引擎中。MyISAM没有行级锁。行锁的特点:
每次锁定相邻的一组记录,锁定粒度界于表锁和行锁之间,加锁开销和加锁时间界于表锁和行锁之间,并发度一般。页级锁应用在BDB等存储引擎中。页锁的特点:
MySQL 使用页级锁的情况相对较少,因为 MySQL 默认使用的是行级锁。但在特定的情况下,MySQL 可能会使用页级锁,主要包括以下几种情况:
总的来说,MySQL 使用页级锁的情况相对较少,因为页级锁通常会导致锁的粒度过大,影响并发性能。在设计数据库和应用程序时,通常会尽量避免使用页级锁,而是优先使用行级锁或其他更细粒度的锁。
对于 MySQL 来说,每种存储引擎都可以实现自己的锁策略和锁粒度,比如 InnoDB 引擎支持行级锁和表级锁,不支持 页级锁。下面主要聚焦介绍 InnoDB 存储引擎中的两大锁:
表锁,顾名思义就是对某个表加锁。表级锁可以分为:表锁、元数据锁、意向锁三种。
一般情况是对应的存储引擎没有行级锁(例如:MyIASM),或者是对应的 SQL 语句没有匹配到索引。对于第一种情况而言,因为对应存储引擎不支持行锁,所以只能是使用更粗粒度的锁来实现,这也比较好理解。对于第二种情况而言,如果存储引擎支持行锁,但对应的 SQL 就没有使用索引,那么此时也是会全表扫描,那此时也是会使用表锁。
例如下面的语句没有指定查询列,或者指定了查询列但是并没有用到索引,那么也是会直接锁定整个表。
情况1:没有指定查询列
select * from user;
情况2:指定查询列,但是没有用到索引
select * from user where name = 'zhangsan';
上面说的索引,其实是判断是否会用行级锁的关键。
元数据,指的是我们的表结构这些元数据。元数据锁(Metadata Lock)自然是执行 DDL 表结构变更语句时,我们对表加上的一个锁了。当我们对一个表做增删改查操作的时候,会加上 MDL 读锁;当我们要对表结构做变更时,就会加 MDL 写锁。
在MySQL中,当对表进行写操作(如INSERT、UPDATE、DELETE)时,需要对相关的数据行加锁以确保数据的一致性和完整性。在某些情况下,MySQL需要锁定整个表而不是部分行,这种情况下会锁定整个表,导致其他会话不能访问表。
1.使用ALTER TABLE、TRUNCATE TABLE等语句对表进行结构性修改时,MySQL需要锁定整个表以防止其他会话对表进行操作。
2.使用LOCK TABLES语句手动锁定表时,MySQL将锁定整个表以确保其他会话不能访问它。
3.在使用MyISAM存储引擎时,当执行写操作时,MySQL会对整个表进行加锁。这是因为MyISAM使用表级锁定而不是行级锁定。
项目中最常见的锁表问题,都是由于UPDATE语句或者DELETE语句的where条件没有走索引导致的。因此我们需要在条件字段上加索引,从而将表锁变为行锁。
意向锁,本质上就是空间换时间的产物,是为了提高行锁效率的一个东西。在 InnoDB 中,我们对某条记录进行锁定时,为了提高并发度,通常都只是锁定这一行记录,而不是锁定整个表。而当我们需要为整个表加 X 锁的时候,我们就需要遍历整个表的记录,如果每条记录都没有被加锁,才可以给整个表加 X 锁。而这个遍历过程就很费时间,这时候就有了意向锁。
意向锁,其实就是标记这个表有没有被锁,如果有某条记录被锁住了,那么就必须获取该表的意向锁。所以当我们需要判断这个表的记录有没有被加锁时,直接判断意向锁就可以了,减少了遍历的时间,提高了效率,是典型的用空间换时间的做法。
在对表中的行记录加锁的时候,就会用到意向锁。InnoDB 存储引擎支持 多粒度(granular)锁定,就是说允许事务在行级上的锁和表级上的锁同时存在。那么为了实现行锁和表锁并存,InnoDB 存储引擎就设计出了 意向锁(Intention Lock) 这个东西:
Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table.
很好理解:意向锁是一个表级锁,其作用就是指明接下来的事务将会用到哪种锁。有两种意向锁:
意向锁之间是相互兼容的:
IS 锁 | IX 锁 | |
IS 锁 | 兼容 | 兼容 |
IX 锁 | 兼容 | 兼容 |
但是与表级读写锁之间大部分都是不兼容的:
X 锁 | S 锁 | |
IS 锁 | 不兼容 | 兼容 |
IX 锁 | 不兼容 | 不兼容 |
注意,这里强调一点:上表中的读写锁指的是表级锁,意向锁不会与行级的读写锁互斥。
来理解一下为什么说意向锁不会与行级的读写锁互斥。举个例子,事务 T1、事务 T2、事务 T3 分别想对某张表中的记录行 r1、r2、r3 进行修改,很普通的并发场景对吧,这三个事务之间并不会发生干扰,所以是可以正常执行的。
这三个事务都会先对这张表加意向写锁,因为意向锁之间是兼容的,所以这一步没有任何问题。
那如果意向锁和行级读写锁互斥的话,岂不是这三个事务都没法再执行下去了,对吧。
思考两个问题:
1)为什么没有意向锁的话,表锁和行锁不能共存?
2)意向锁是如何让表锁和行锁共存的?
首先来看第一个问题,假设行锁和表锁能共存,举个例子:事务 T1 锁住表中的某一行(行级写锁),事务 T2 锁住整个表(表级写锁)。
问题很明显,既然事务 T1 锁住了某一行,那么其他事务就不可能修改这一行。这与 ”事务 T2 锁住整个表就能修改表中的任意一行“ 形成了冲突。所以,没有意向锁的时候,行锁与表锁是无法共存的。
再来看第二个问题,有了意向锁之后,事务 T1 在申请行级写锁之前,MySQL 会先自动给事务 T1 申请这张表的意向排他锁,当表上有意向排他锁时其他事务申请表级写锁会被阻塞,也即事务 T2 申请这张表的写锁就会失败。
行级锁是存储引擎级别的锁,需要存储引擎支持才有效。目前 MyISAM 存储引擎不支持行级锁,而 Innodb 存储引擎则支持行级锁。而表级锁,则是 MySQL 层面就支持的锁。当增删改查匹配到索引时,Innodb 会使用行级锁。如果没有匹配不到索引,那么就会直接使用表级锁。
InnoDB引擎行锁是通过对索引数据页上的记录加锁实现的,主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock,也就是InnoDB的三种行锁模式。
(1)记录锁, 仅仅锁住索引记录的一行,在单条索引记录上加锁。
(2)record lock锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。
所以说当一条sql没有走任何索引时,那么将会在每一条聚合索引后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。
(1)区间锁, 仅仅锁住一个索引区间(开区间,不包括双端端点)。
(2)在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身。
(3)间隙锁可用于防止幻读,保证索引间的不会被插入数据
比如在 100、10000中,间隙锁的可能值有 (∞, 100),(100, 10000),(10000, ∞),
(1)record lock + gap lock, 左开右闭区间。
(2)默认情况下,innodb使用next-key locks来锁定记录。select … for update
(3)但当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。
(4)Next-Key Lock在不同的场景中会退化:
比如在 100、10000中,临键锁(Next-Key Locks)的可能有 (∞, 100],(100, 10000] , 这里的关键是左开右闭。
在MySQL中,当对表进行写操作(如INSERT、UPDATE、DELETE)时,需要对相关的数据行加锁以确保数据的一致性和完整性。在某些情况下,MySQL需要锁定整个表而不是部分行,这种情况下会锁定整个表,导致其他会话不能访问表。
1.使用ALTER TABLE、TRUNCATE TABLE等语句对表进行结构性修改时,MySQL需要锁定整个表以防止其他会话对表进行操作。
2.使用LOCK TABLES语句手动锁定表时,MySQL将锁定整个表以确保其他会话不能访问它。
3.在使用MyISAM存储引擎时,当执行写操作时,MySQL会对整个表进行加锁。这是因为MyISAM使用表级锁定而不是行级锁定。
InnoDB 采用的是两阶段锁定协议(two-phase locking protocol),意思是:对于每一个事务Transaction,分为两个阶段:
第一阶段:增长阶段(Growing Phase)
在这个阶段,事务可以获得锁定,但不能释放锁定。事务可以继续请求并获得锁定,直到它达到了其锁定点(Lock Point),也就是事务获得最后一个锁定的时间点。一旦事务进入第二阶段,它就不能再获得新的锁定。
第二阶段:缩减阶段(Shrinking Phase)
在这个阶段,事务可以释放已经持有的锁定,但不能再获取新的锁定。这个阶段的目的是确保事务不会在已经进入第二阶段后再次请求锁定。
两阶段锁定协议(two-phase locking protocol),有下面的两个特点:
InnoDB 如何加锁?从下面三个维度来分开介绍:
它比较特殊,是由 InnoDB 存储引擎自己维护的,用户无法手动操作意向锁,在为数据行加读写锁之前,InnoDB 会先获取该数据行所在在数据表的对应意向锁。
1)隐式锁定:对于常见的 DDL 语句(如 ALTER
、CREATE
等),InnoDB 会自动给相应的表加表级锁。
2)显示锁定:在执行 SQL 语句时,也可以明确显示指定对某个表进行加锁(lock table user read(write)
)。
lock table user read; # 加表级读锁
unlock tables; # 释放表级锁
1)对于常见的 DML 语句(如 UPDATE
、DELETE
和 INSERT
),InnoDB 会自动给相应的记录行加写锁。
2)默认情况下对于普通 SELECT
语句,InnoDB 不会加任何锁,但是在 Serializable 隔离级别下会加行级读锁。
上面两种是隐式锁定,InnoDB 也支持通过特定的语句进行显式锁定,不过这些语句并不属于 SQL 规范:
3)SELECT * FROM table_name WHERE ... FOR UPDATE
,加行级写锁
4)SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
,加行级读锁
另外,需要注意的是,InnoDB 存储引擎的行级锁是基于索引的,也就是说当索引失效或者说根本没有用索引的时候,行锁就会升级成表锁。比较典型的索引失效情况 “使用 or
" 。
当使用ALTER TABLE或TRUNCATE TABLE等语句对表进行结构性修改时,MySQL需要锁定整个表以防止其他会话对表进行操作。
使用LOCK TABLES语句手动锁定表时,MySQL将锁定整个表以确保其他会话不能访问它。
在使用MyISAM存储引擎时,执行写操作会导致MySQL对整个表加锁,这是因为MyISAM使用表级锁定而不是行级锁定。
事务中包含多条对同一个表进行修改的SQL语句时,该表会被锁定。这是因为MVSQL采用行锁定机制,但当两个或多个事务在同时修改一个表时,未使用的修改请求会被放入等待队列。
对一个大表进行索引操作(如新建或删除索引)时,该表会被锁定。索引操作锁定表的时间与表的大小和结构有关。
在一张表中插入大量数据的同时,尝试在同一时刻进行查询操作,会导致表被锁定。这是因为查询和插入操作都需要获取相应的锁
使用SELECT..FOR UPDATE语句进行行级锁定操作时,如果索引不可用,MySQL可能会升级为表锁。
在某些情况下,如果查询的索引选择不恰当,MySQL可能会升级为表锁,尤其是当锁住的数据量较大时
项目中最常见的锁表问题,都是由于UPDATE/DELETE时, where条件没有走索引导致的。当执行UPDATE或DELETE语句且where条件未使用索引时,可能会导致全表扫描并锁定整个表。因此我们需要在条件字段上加索引,从而将表锁变为行锁。
在索引失效的情况下,MySQL会把所有聚集索引记录和间隙都锁上,称之为锁表,或叫行锁升表锁。
在 MySQL 中,索引对于查询性能至关重要,但是有些情况下索引可能会失效,从而导致查询性能下降
SELECT * FROM table WHERE YEAR(date_column) = 2023;
SELECT * FROM table WHERE date_column BETWEEN '2023-01-01' AND '2023-12-31';
SELECT * FROM table WHERE column LIKE '%value';
SELECT * FROM table WHERE column LIKE 'value%';
WHERE b = 1 AND c = 2
会导致索引失效。WHERE a = 1 AND b = 2 AND c = 3
。SELECT * FROM table WHERE varchar_column = 123;
SELECT * FROM table WHERE varchar_column = '123';
SELECT * FROM table WHERE column1 = 1 OR column2 = 2;
SELECT * FROM table WHERE column1 = 1 UNION SELECT * FROM table WHERE column2 = 2;
SELECT * FROM table WHERE varchar_column = 123;
SELECT * FROM table WHERE varchar_column = '123';
<
, >
, BETWEEN
, LIKE
)之后的索引列会失效。WHERE a > 1 AND b = 2
会导致索引 b 失效。SELECT * FROM table WHERE column IS NULL;
FORCE INDEX
强制使用特定索引,或调整查询语句和索引设计以帮助优化器选择正确的索引。通过识别和解决这些索引失效的原因,可以显著提高 MySQL 查询的性能。
MySQL在执行结构性修改、手动锁定、写操作、事务处理、索引操作、并发操作、加锁操作、以及在特定查询条件下索引失效,都可能需要对表进行锁定。
锁表会对系统性能、数据一致性和用户体验产生负面影响,甚至可能引发严重的业务中断和系统崩溃。因此,避免锁表是数据库优化和系统设计中的重要任务。可以通过优化索引、分解大事务、合理设计表结构、使用适当的事务隔离级别、读写分离等方法来减少锁表问题,提升系统的并发性能和稳定性。在 MySQL 中避免锁表问题对于提高数据库性能和并发性至关重要。以下是一些调优策略和最佳实践,以减少或避免锁表问题:
当索引包含了查询所需的所有列时,这个索引就被称为“覆盖索引”。
ALTER TABLE ... ALGORITHM=INPLACE
),以减少对表的锁定时间。SHOW PROCESSLIST
、INFORMATION_SCHEMA.INNODB_LOCKS
、INFORMATION_SCHEMA.INNODB_LOCK_WAITS
)监控锁等待情况。slow query log
分析慢查询日志,找出可能导致锁表的查询,并进行优化。主从复制:通过主从复制实现读写分离,将读操作分散到从库上,以减轻主库的负载和锁定压力。
显式锁定:在需要时显式地使用合适的锁定策略(如 SELECT ... FOR UPDATE
),但要谨慎使用,避免不必要的长时间锁定。
在MySQL中,悲观锁依赖数据库提供的锁机制来实现。在InnoDB引擎中,使用悲观锁需要先关闭MySQL数据库的自动提交属性,然后通过select ... for update
来进行加锁。在数据库中,悲观锁的流程如下:
使用
SELECT ... FOR UPDATE
会将数据锁住,不过我们需要注意一些锁的级别。MySQL InnoDB默认使用行级锁。行级锁都是基于索引的,如果一条SQL语句未使用索引,优化器在选择时,若发现锁表可能性能更好,有可能会直接锁表。
MySQL中的乐观锁主要通过CAS(Compare and Swap)的机制来实现,通常使用版本号(version)来实现。CAS是一种乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能成功更新变量的值,而其他线程都失败。失败的线程并不会被挂起,而是被告知在此次竞争中失败,并可以再次尝试。
悲观锁
在对数据库中的数据进行修改时,为了避免同时被其他人修改,最好的方法是直接对该数据进行加锁以防止并发。这种在修改数据之前先锁定再修改的方式被称为悲观并发控制(又称“悲观锁”,Pessimistic Concurrency Control,缩写为“PCC”)。
悲观锁之所以被称为悲观,是因为这是一种对数据的修改抱有悲观态度的并发控制方式。一般来说,我们认为数据被并发修改的概率较大,因此在修改之前先加锁。悲观并发控制实际上是一种保守的策略,即“先取锁再访问”,它为数据处理的安全性提供了保证。
在效率方面,处理加锁机制会导致数据库产生额外的开销,增加了产生死锁的风险。此外,悲观锁还可能降低并行性,因为如果一个事务锁定了某行数据,其他事务就必须等待该事务完成才能处理该行数据。
乐观锁
乐观锁(Optimistic Locking)是相对悲观锁而言的。乐观锁假设数据在一般情况下不会发生冲突,因此在数据提交更新时才会实际检查数据是否冲突。如果发现冲突,则会向用户返回错误信息,让用户决定如何处理。与悲观锁相比,乐观锁在处理数据库时并不会使用数据库提供的锁机制。一般来说,乐观锁的实现方式是通过记录数据的版本信息。
乐观并发控制相信事务之间的数据竞争(data race)的概率较小,因此尽可能直接进行操作,直到提交时才对数据进行检查和锁定。这样做不会产生任何锁或死锁。
在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。
各组件的功能主要是:
管理客户端的连接,并负责权限验证。当用户通过客户端发送一条SQL查询语句时,首先,MySQL服务器会验证客户端的连接信息(如用户名、密码),建立连接。连接成功后,服务器才会SQL查询语句。MySQL也提供了最大连接数、连接超时等一系列参数设置来控制客户端连接行为。
如果一个查询请求被执行过,MySQL会把执行结果存入查询缓存,如果后面又查询了同样的语句,则会直接返回结果。但是,查询缓存会因为更新频繁失效,因为只要一个表有更新操作,那么这个表的查询缓存就会被清空。这样就显得很鸡肋,我们可以通过将参数 query_cache_type 设置成 DEMAND,来关闭查询缓存。需要指出,MySQL 8.0版本直接将查询缓存的整块功能删掉了,8.0开始彻底没有这个功能了。
SQL解析器会分析SQL语句的语法结构,做词法和语法分析,检查其是否符合SQL语法规范。如果不符合,将返回错误信息。之后构建出语法树
预处理器进一步检查SQL语句中的表名、列名是否存在,把 select * 替换为具体的列,以及用户是否有相应的访问权限。
优化器根据表的数据分布、索引等信息,选择最优的执行计划。这一步骤对于提高查询效率至关重要。优化器可能会重写查询语句,比如转换JOIN顺序,选择最合适的索引等。
查询执行引擎根据优化后的执行计划,调用存储引擎的API 执行实际的数据读取或修改操作,包括主键索引查询,索引下推等。如果查询涉及多个表,MySQL会使用相应的算法(如嵌套循环、哈希连接)进行表的连接操作。之后,查询结果被收集并格式化,然后通过网络返回给客户端。如果查询结果太大,可能会采用分批次的方式返回。
MySQL中的日志非常重要,包括实例内的事务以及实例间的主从复制均基于日志实现。MySQL 中最重要的两份日志是 redo log 与 binlog。为什么会有两份日志,原因是使用场景不同。其中:
两份日志主要有以下三点不同;
重做日志是循环写入的。这意味着数据库系统会预先分配一定数量的重做日志文件,当当前文件写满后,系统会自动切换到下一个文件继续写入。一旦所有文件都被写过一遍,系统会从第一个文件开始覆盖旧的记录。这个过程通常被称为“日志切换”或“日志循环”。
为了保证两份日志之间的逻辑一致,也就是数据与备份的一致性,引入两阶段提交(two-phase commit protocol,2PC)。由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。假设执行 update,将值从 1 改为 2:
显然,如果没有两阶段提交,无法保证数据与日志的一致性。那么,有两阶段提交时会怎么样呢?
首先,介绍下两阶段提交的过程,其中将 redo log 的提交拆分为两个步骤,包括 prepare 与 commit,期间写入 binlog。
因此,如果在两阶段提交的不同时刻,MySQL 异常重启会发生什么呢?
崩溃恢复时根据两阶段提交的进度进行处理。崩溃恢复(crash-recovery)时的完整判断逻辑为:
因此,redo log prepare 后 commit 前崩溃恢复时可能发生回滚或提交,具体与 binlog 的完整性有关。
显然,时刻 B 发生 crash 的情况对应 redo log prepare 完整,且 binlog 完整的场景,因此事务提交。这里可以提出以下两个问题:
1)如何判断 binlog 完整
2)如何根据 redo log 定位对应的 binlog
接下来分别回答这两个问题。
1)如何判断 binlog 完整
判断 binlog 的完整性有以下两种方式:
如下所示,测试显示 row 与 statement 两种 binlog 格式中事务的最后一个 event 都是 XID event。
2)如何根据 redo log 定位对应的 binlog
redo log 与 binlog 有一个共同的数据字段,称为 XID。崩溃恢复的时候,会按顺序扫描 redo log:
其中:
这里可以提出另一个问题,根据事务的持久性,到什么进度后事务将无法回滚?
理论上 MySQL 中通过 redo log 实现事务的持久性,因此 redo log 刷盘后就可以保证对数据库的修改是永久性的,即使发生崩溃也不会丢失,当然也不会回滚。不过根据事务的两阶段提交协议,binlog 写入代表事务提交,同样不可能发生回滚。
因此,事务无法回滚的关键点是事务的提交,而不是单纯的 redo log 或 binlog 的写入。在事务提交的过程中,两阶段提交机制确保了 redo log 和 binlog 的一致性,这个提交过程标志着事务从可回滚转变为不可回滚。
分布式事务是一种跨多个独立的数据库、系统或网络区域的事务处理方法。XA 事务是一种遵循 XA 规范的分布式事务,因此 XA 事务是分布式事务的一种实现。XA 事务依赖两阶段提交(2PC)协议实现分布式事务的一致性和原子性。两阶段提交是最常见的分布式事务协议,用于保证分布式事务的原子性,显然并不是 MySQL 独有的。根据 XA 规范,两阶段提交的实现过程中包括两个角色:
两阶段提交中将提交操作分为两个阶段:
MySQL 中的 XA 事务分为外部 XA 与内部 XA。其中:
其中,由于 binlog 与存储引擎是独立单元,可以将 binlog 也看作一个存储引擎,因此需要通过 XA 事务实现 binlog 与存储引擎的数据一致性和原子性,从而保证全部操作要么全部提交,要么全部回滚。
在分布式事务中,XID
作为全局事务的唯一标识符,用于跟踪和协调不同数据库实例中的事务部分。这个标识符在事务的所有参与者之间是共享的,以确保事务的一致性和完整性。因此在 XA 事务中,XID
用于在多个数据库实例之间协调事务。在 MySQL 中,XID
(Transaction Identifier)是事务的唯一标识符,用于标记事务的提交。
Binlog 中一个事务由一系列事件(event)组成,这个序列由 BEGIN 事件开始,以 XID 事件结束(对于提交的事务)。因此如果事务被回滚,不会记录 XID 事件,而是记录一个 ROLLBACK 事件。
参考 ChatGPT,XID 与 GTID 的主要区别包括:
MySQL 事务二阶段提交 与 MySQL 核心模块揭秘 | 07 期 | 二阶段提交 (1) prepare 阶段,prepare 阶段做的事情分为两类:
TRX_UNDO_ACTIVE
修改为 TRX_UNDO_PREPARED
;TRX_STATE_ACTIVE
修改为 TRX_STATE_PREPARED
,标识事务已经进入二阶段提交的 prepare 阶段;READ-UNCOMMITTED
)或读已提交(READ-COMMITTED
),InnoDB 会释放事务给记录加的共享、排他 GAP 锁;其中 undo log 非常重要,原因是:
参考文章 XA事务与两阶段提交。
Undo页面链表的第一个页面的结构见下图,其中记录了一些关于这个事务的一些属性。
其中 Undo Log Segment Header 结构见下图,其中 TRX_UNDO_STATE 字段表示事务所处的状态。
其中 Undo Log Header 结构见下图。
其中:
TRX_UNDO_XID_EXISTS
:表示有没有 XID 信息;XID信息
:表示具体的 XID 是什么。TRX_UNDO_STATE 的取值包括:
- TRX_UNDO_ACTIVE:活跃状态,也就是一个活跃的事务正在往这个段里边写入 undo log;
- TRX_UNDO_CACHED:被缓存的状态。处在该状态的 Undo 页面链表等待着之后被其他事务重用;
- TRX_UNDO_TO_FREE:对于 insert undo 链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。Undo 页面链表可以被马上清理;
- TRX_UNDO_TO_PURGE:对于 update undo 链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。Undo 页面链表不可以被马上清理,而是加入 History 链表用于 MVCC,等待 purge 线程清理;
- TRX_UNDO_PREPARED:包含处于 prepare 阶段(这个阶段是在分布式事务中会出现)的事务产生的 undo log。
commit 阶段做的事情同样分为两类:
具体 commit 阶段的实现与组提交有关。因此,在客户端执行 commit 语句或自动 commit 时,MySQL 开启内部 XA 事务,分两阶段完成 XA 事务的提交。
崩溃恢复全过程分为多个阶段,其中与事务两阶段提交有关的阶段包括:
未完成事务的状态可能是以下三种之一:
TRX_STATE_ACTIVE
,表示事务还没有进入提交阶段。TRX_STATE_PREPARED
,表示事务已经提交了,但是只完成了二阶段提交的 PREPARE 阶段,还没有完成 COMMIT 阶段。TRX_STATE_COMMITTED_IN_MEMORY
,表示事务已经完成了二阶段提交的 2 个阶段,还剩一些收尾工作没做,这种状态的事务修改的数据已经可以被其它事务看见了。其中未提交事务 TRX_STATE_ACTIVE 对应 redo log 已经刷盘的未提交事务,包括以下三种场景:
因此,为了保证事务的原子性,需要在崩溃恢复时将这些未提交事务回滚,而找到这些未提交事务依赖 undo log。
MySQL 通过事务的两阶段提交实现数据与日志的一致性。其中数据指 redo log,日志指 binlog,可以认为是两个不同的存储引擎,因此基于分布式事务的 XID 协议实现一致性。具体实现中将 redo log 的提交拆分为两个步骤,包括 prepare 与 commit,期间写入 binlog。因此,写入的不同阶段异常重启时:
具体是在崩溃恢复过程中基于两阶段提交保证事务的一致性。其中:
因此,可以将崩溃恢复过程中使用的日志的顺序理解为 redo log、undo log、binlog。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。