文章目录
- 事务机制
- 基本介绍
- 事务管理
- 基本操作
- 提交方式
- 事务 ID
- 隔离级别
- 四种级别
- 加锁分析
- 原子特性
- 实现方式
- 实现原理undo log
- 隔离特性
- 实现方式MVCC
- 实现原理
- 隐藏字段
- undo log
- Read View
- RC RR
- 持久特性
- 实现方式redo log
- 一致特性
- 面试题
- MySQL的ACID特性分别是怎么实现的?
- 谈谈MySQL的事务隔离级别
- MySQL的事务隔离级别是怎么实现的?
- 如何解决幻读问题?
- MySQL事务如何回滚?
事务机制
基本介绍
事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 SQL 语句,这些语句要么都执行,要么都不执行,作为一个关系型数据库,MySQL 支持事务。
单元中的每条 SQL 语句都相互依赖,形成一个整体
- 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态
- 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行
事务的四大特征:ACID
- 原子性 (atomicity)
- 一致性 (consistency)
- 隔离性 (isolaction)
- 持久性 (durability)
事务的几种状态:
- 活动的(active):事务对应的数据库操作正在执行中
- 部分提交的(partially committed):事务的最后一个操作执行完,但是内存还没刷新至磁盘
- 失败的(failed):当事务处于活动状态或部分提交状态时,如果数据库遇到了错误或刷脏失败,或者用户主动停止当前的事务
- 中止的(aborted):失败状态的事务回滚完成后的状态
- 提交的(committed):当处于部分提交状态的事务刷脏成功,就处于提交状态
事务管理
基本操作
事务管理的三个步骤
- 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败
- 执行 SQL 语句:执行具体的一条或多条 SQL 语句
- 结束事务(提交|回滚)
- 提交:没出现问题,数据进行更新
- 回滚:出现问题,数据恢复到开启事务时的状态
事务操作:
- 显式开启事务
START TRANSACTION [READ ONLY|READ WRITE|WITH CONSISTENT SNAPSHOT]; #可以跟一个或多个状态,最后的是一致性读
BEGIN [WORK];
说明:不填状态默认是读写事务
- 回滚事务,用来手动中止事务ROLLBACK;
- 提交事务,显示执行是手动提交,MySQL 默认为自动提交COMMIT;
- 保存点:在事务的执行过程中设置的还原点,调用 ROLLBACK 时可以指定回滚到哪个点
SAVEPOINT point_name; #设置保存点
RELEASE point_name #删除保存点
ROLLBACK [WORK] TO [SAVEPOINT] point_name #回滚至某个保存点,不填默认回滚到事务执行之前的状态
- 操作演示
-- 开启事务
START TRANSACTION;
-- 张三给李四转账500元
-- 1.张三账户-500
UPDATE account SET money=money-500 WHERE NAME='张三';
-- 2.李四账户+500
UPDATE account SET money=money+500 WHERE NAME='李四';
-- 回滚事务(出现问题)
ROLLBACK;
-- 提交事务(没出现问题)
COMMIT;
提交方式
提交方式的相关语法:
- 查看事务提交方式
SELECT @@AUTOCOMMIT; -- 会话,1 代表自动提交 0 代表手动提交
SELECT @@GLOBAL.AUTOCOMMIT; -- 系统
- 修改事务提交方式
SET @@AUTOCOMMIT=数字; -- 系统
SET AUTOCOMMIT=数字; -- 会话
工作原理:
- 自动提交:如果没有 START TRANSACTION 显式地开始一个事务,那么每条 SQL 语句都会被当做一个事务执行提交操作;显式开启事务后,会在本次事务结束(提交或回滚)前暂时关闭自动提交
- 手动提交:不需要显式的开启事务,所有的 SQL 语句都在一个事务中,直到执行了提交或回滚,然后进入下一个事务
- 隐式提交:存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务
- DDL 语句 (CREATE/DROP/ALTER)、LOCK TABLES 语句、LOAD DATA 导入数据语句、主从复制语句等
- 当一个事务还没提交或回滚,显式的开启一个事务会隐式的提交上一个事务
事务 ID
事务在执行过程中对某个表执行了增删改操作或者创建表,就会为当前事务分配一个独一无二的事务 ID(对临时表并不会分配 ID),如果当前事务没有被分配 ID,默认是 0
说明:只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改,读写事务可以对数据表执行增删改查操作
事务 ID 本质上就是一个数字,服务器在内存中维护一个全局变量:
- 每当需要为某个事务分配 ID,就会把全局变量的值赋值给事务 ID,然后变量自增 1
- 每当变量值为 256 的倍数时,就将该变量的值刷新到系统表空间的 Max Trx ID 属性中,该属性占 8 字节
- 系统再次启动后,会读取表空间的 Max Trx ID 属性到内存,加上 256 后赋值给全局变量,因为关机时的事务 ID 可能并不是 256 的倍数,会比 Max Trx ID 大,所以需要加上 256 保持事务 ID 是一个递增的数字
聚簇索引的行记录除了完整的数据,还会自动添加 trx_id(事务ID)、roll_pointer(回滚点) 隐藏列,如果表中没有主键并且没有非空唯一索引,也会添加一个 row_id 的隐藏列作为聚簇索引
隔离级别
四种级别
事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,不同的事务之间不该互相影响,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。
隔离级别分类:
隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 |
---|---|---|---|
Read Uncommitted | 读未提交 | 脏读、不可重复读、幻读 | |
Read Committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server |
Repeatable Read | 可重复读 | 幻读 | MySQL |
Serializable | 可串行化 | 无 |
一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差
- 脏写 (Dirty Write):当两个或多个事务选择同一行,最初的事务修改的值被后面事务修改的值覆盖,所有的隔离级别都可以避免脏写(又叫丢失更新),因为有行锁
- 脏读 (Dirty Reads):在一个事务处理过程中读取了另一个未提交的事务中修改过的数据
- 不可重复读 (Non-Repeatable Reads):在一个事务处理过程中读取了另一个事务中修改并已提交的数据。
- 可重复读:在同一个事物中以及相同查询条件下,读到的数据都是一样的。
- 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,数据条目发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入。
隔离级别操作语法:
- 查询数据库隔离级别
SELECT @@TX_ISOLATION; -- 会话
SELECT @@GLOBAL.TX_ISOLATION; -- 系统
- 修改数据库隔离级别SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串;
加锁分析
InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎
- Read Uncommitted 级别,任何操作都不会加锁
- Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的加锁操作不能省略。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在 Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象(锁表),这种情况同样适用于 RR
- Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,加了读锁后其他事务就无法修改数据,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解
- Serializable 级别,读加共享锁,写加排他锁,读写互斥,使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差
- 串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁
- 可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现
原子特性
实现方式
原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态
InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志)
- redo log 用于保证事务持久性
- undo log 用于保证事务原子性和隔离性
undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本
当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容做与之前相反的操作:
- 对于每个 insert,回滚时会执行 delete
- 对于每个 delete,回滚时会执行 insert
- 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去
实现原理undo log
undo log 是采用段的方式来记录,Rollback Segement 称为回滚段,本质上就是一个类型是 Rollback Segement Header 的页面
每个回滚段中有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号,每个链表对应一个叫 undo log segment 的段
- 在以前老版本,只支持 1 个 Rollback Segement,只能记录 1024 个 undo log segment
- MySQL5.5 开始支持 128 个 Rollback Segement,支持 128*1024 个 undo 操作
工作流程:
- 事务开始:当一个事务开始时,数据库管理系统会为该事务分配一个唯一的事务标识符(Transaction ID)。
- 写入undolog:在事务执行期间,所有对数据库的修改操作都会生成相应的undolog记录。这些修改操作可以是更新、插入或删除记录等。undolog记录会包含修改前的旧数据(undo前图像)和修改后的新数据(undo后图像)。
- 写入磁盘:undolog记录会先被写入磁盘中的临时文件,以确保数据的持久性。
- 事务回滚:如果事务需要回滚(例如由于失败或用户请求),数据库管理系统会使用undolog记录中的undo前图像将事务的修改操作恢复到原始状态。
- 持久化:在事务回滚完成后,undolog记录会被写入磁盘的持久存储介质,以确保持久性和可恢复性。
隔离特性
隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰
- 严格的隔离性,对应了事务隔离级别中的 serializable,实际应用中对性能考虑很少使用可串行化
- 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响
隔离性让并发情形下的事务之间互不干扰:
- 一个事务的写操作对另一个事务的写操作(写写):锁机制保证隔离性
- 一个事务的写操作对另一个事务的读操作(读写):MVCC 保证隔离性
锁机制:事务在修改数据之前,需要先获得相应的锁,获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁
实现方式MVCC
MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来解决读写冲突的无锁并发控制,可以在发生读写请求冲突时不用加锁解决,这个读是指的快照读(也叫一致性读或一致性无锁读),而不是当前读:
- 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据
- 当前读:又叫加锁读,读取数据库记录是当前最新的版本(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读
数据库并发场景:
- 读-读:不存在任何问题,也不需要并发控制
- 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
- 写-写:有线程安全问题,可能会存在脏写(丢失更新)问题
MVCC 的优点:
- 在并发读写数据库时,做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了并发读写的性能
- 可以解决脏读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题(写锁会解决)
提高读写和写写的并发性能:
- MVCC + 悲观锁:MVCC 解决读写冲突,悲观锁解决写写冲突
- MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突
实现原理
实现原理主要是隐藏字段,undo日志,**Read View **来实现的
隐藏字段
InnoDB 存储引擎,数据库中的聚簇索引每行数据,除了自定义的字段,还有数据库隐式定义的字段:
- DB_TRX_ID:最近修改事务 ID,记录创建该数据或最后一次修改该数据的事务 ID
- DB_ROLL_PTR:回滚指针,指向记录对应的 undo log 日志,undo log 中又指向上一个旧版本的 undo log
- DB_ROW_ID:隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引
undo log
undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,要根据 undo log 逆推出以往事务的数据
undo log 的作用:
- 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复
- 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据
undo log 主要分为两种:
- insert undo log:事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃
- update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在当前读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除
每次对数据库记录进行改动,都会产生的新版本的 undo log,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为版本链,版本链的头节点就是当前的最新的 undo log,链尾就是最早的旧 undo log
说明:因为 DELETE 删除记录,都是移动到垃圾链表中,不是真正的删除,所以才可以通过版本链访问原始数据
注意:undo 是逻辑日志,这里只是直观的展示出来
工作流程:
- 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24
- 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁
- 以此类推
Read View
Read View 是事务进行读数据操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据
注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据
工作流程:将版本链的头节点的事务 ID(最新数据事务 ID,大概率不是当前线程)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比进行可见性分析,不可见就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足可见性的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录
Read View 几个属性:
- m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中)
- min_trx_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合)
- max_trx_id:生成 Read View 时当前系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务)
- creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据
creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交)
- db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以此数据对 creator 是可见的
- db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见(因为比已提交的最大事务 ID 小的并不一定已经提交,所以应该判断是否在活跃事务列表)
- db_trx_id >= max_trx_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见
- min_trx_id<= db_trx_id < max_trx_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中
- 在列表中,说明该版本对应的事务正在运行,数据不能显示(不能读到未提交的数据)
- 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(可以读到已经提交的数据)
实例演示
表 user 数据
id name age
1 张三 18
Transaction 20:
START TRANSACTION; -- 开启事务
UPDATE user SET name = '李四' WHERE id = 1;
UPDATE user SET name = '王五' WHERE id = 1;
Transaction 60:
START TRANSACTION; -- 开启事务
-- 操作表的其他数据
ID 为 0 的事务创建 Read View:
- m_ids:20、60
- min_trx_id:20
- max_trx_id:61
- creator_trx_id:0
只有红框部分才复合条件,所以只有张三对应的版本的数据可以被看到
RC RR
Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 SELECT 在 RC 和 RR 隔离级别使用 MVCC 读取记录
RR、RC 生成时机:
- RC 隔离级别下,每次读取数据前都会生成最新的 Read View(当前读)
- RR 隔离级别下,在第一次数据读取时才会创建 Read View(快照读)
RC、RR 级别下的 InnoDB 快照读区别
- RC 级别下,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因
- RR 级别下,某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个 Read View,所以一个事务的查询结果每次都是相同的RR 级别下,通过 START TRANSACTION WITH CONSISTENT SNAPSHOT 开启事务,会在执行该语句后立刻生成一个 Read View,不是在执行第一条 SELECT 语句时生成(所以说 START TRANSACTION 并不是事务的起点,执行第一条语句才算起点)
解决幻读问题:
- 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是并不能完全避免幻读场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1 去 UPDATE 该行会发现更新成功,并且把这条新记录的 trx_id 变为当前的事务 id,所以对当前事务就是可见的。因为 Read View 并不能阻止事务去更新数据,更新数据都是先读后写并且是当前读,读取到的是最新版本的数据。
- 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题
持久特性
实现方式redo log
持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。
Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入了 redo log 日志.
redo log重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中, 用于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。
如果没有redolog,可能会存在什么问题的?
在InnoDB引擎中的内存结构中,主要的内存区域就是缓冲池,在缓冲池中缓存了很多的数据页。 当我们在一个事务中,执行多个增删改的操作时,InnoDB引擎会先操作缓冲池中的数据,如果缓冲区没有对应的数据,会通过后台线程将磁盘中的数据加载出来,存放在缓冲区中,然后将缓冲池中的数据修改,修改后的数据页我们称为脏页。 而脏页则会在一定的时机,通过后台线程刷新到磁盘中,从而保证缓冲区与磁盘的数据一致。 而缓冲区的脏页数据并不是实时刷新的,而是一段时间之后将缓冲区的数据刷新到磁盘中,假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却没有持久化下来,这就出现问题了,没有保证事务的持久性。
通过redolog如何解决这个问题。
有了redolog之后,当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化,记录在redo
log buffer中。在事务提交时,会将redo log buffer中的数据刷新到redo log磁盘文件中。 过一段时间之后,如果刷新缓冲区的脏页到磁盘时,发生错误,此时就可以借助于redo log进行数据恢复,这样就保证了事务的持久性。 而如果脏页成功刷新到磁盘 或 或者涉及到的数据已经落盘,此 时redo log就没有作用了,就可以删除了,所以存在的两个redolog文件是循环写的。
**那为什么每一次提交事务,要刷新redo log 到磁盘中呢,而不是直接将buffer pool中的脏页刷新 **
**到磁盘呢 ? **
因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。 而redo log在 往磁盘文件中写入数据,由于是日志文件,所以都是顺序写的。顺序写的效率,要远大于随机写。 这 种先写日志的方式,称之为 WAL(Write-Ahead Logging)
缓冲池的刷脏策略:
- redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把对应的更新持久化到磁盘中
- Buffer Pool 内存不足,需要淘汰部分数据页(LRU 链表尾部),如果淘汰的是脏页,就要先将脏页写到磁盘(要避免大事务)
- 系统空闲时,后台线程会自动进行刷脏(Flush 链表部分已经详解)
- MySQL 正常关闭时,会把内存的脏页都刷新到磁盘上
一致特性
一致性是指事务执行前后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。
数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)
实现一致性的措施:
- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致
面试题
MySQL的ACID特性分别是怎么实现的?
原子性实现原理:
实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB实现回滚靠的是 undo log,当事务对数据库进行修改时,InnoDB会生成对应的undo log。如果事务执行失败或调用rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作。对于insert,回滚时会执行delete。对于delete,回滚时会执行insert。对于update,回滚时则会执行相反的update,把数据改回去。
持久性实现原理:
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会 很低。为此,InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲。当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有, 则从磁盘读取后放入Buffer Pool。当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。
Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
于是,redo log被引入来解决这个问题。当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作。当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入。而redo log中只包含真正需要写入的部分,无效IO大大减少。
隔离性实现原理:
隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们主要考虑最简单的读操作和写操作(加锁 读等特殊读操作会特殊说明),那么隔离性的探讨,主要可以分为两个方面。
第一方面,(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性。
隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB通过锁机制来保证这一点。锁机制的 基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁。获得锁之后,事务便可以修改数 据。该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回 滚后释放锁。
按照粒度,锁可以分为表锁、行锁以及其他位于二者之间的锁。表锁在操作数据时会锁定整张表,并发性能较差。行锁则只锁定需要操作的数据,并发性能好。但是由于加锁本身需要消耗资源,因此在锁定数据较多情况下使用表锁可以节省大量资源。MySQL中不同的存储引擎支持的锁是不一样的,例如MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行 锁。
第二方面,(一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性。
InnoDB默认的隔离级别是RR(REPEATABLE READ),RR解决脏读、不可重复读、幻读等问题,使用的是MVCC。MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议。它最大的优点是读不加锁,因此读写不冲突,并发性能好。InnoDB实现MVCC,多个版本的数据可以共存,主要基于以下技术及数据结构:
1. 隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id、指向undo log的指针等。
2. 基于undo log的版本链:每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会指向更早版本的undo log,从而形成一条版本链。
3. ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本。但是具体要恢复到哪个版 本,则需要根据ReadView来确定。所谓ReadView,是指事务(记做事务A)在某一时刻给整个事 务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比 较,从而判断数据对该ReadView是否可见,即对事务A是否可见。
一致性实现原理:
可以说,一致性是事务追求的最终目标。前面提到的原子性、持久性和隔离性,都是为了保证数据库状 态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。实现一致性的措 施包括:
保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证。
数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等。
应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据 库实现的多么完美,也无法保证状态的一致。
谈谈MySQL的事务隔离级别
SQL 标准定义了四种隔离级别,这四种隔离级别分别是:
读未提交(READ UNCOMMITTED); 读提交(READ COMMITTED);
可重复读(REPEATABLE READ); 串行化(SERIALIZABLE)。
事务隔离是为了解决脏读、不可重复读、幻读问题,下表展示了4 种隔离级别对这三个问题的解决程度:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | 可能 | 可能 | 可能 |
READ COMMITTED | 不可能 | 可能 | 可能 |
REPEATABLE READ | 不可能 | 不可能 | 可能 |
SERIALIZABLE | 不可能 | 不可能 | 不可能 |
上述4种隔离级别MySQL都支持,并且InnoDB存储引擎默认的支持隔离级别是REPEATABLE READ,但是与标准SQL不同的是,InnoDB存储引擎在REPEATABLE READ事务隔离级别下,使用Next-Key Lock 的锁算法,因此避免了幻读的产生。所以,InnoDB存储引擎在默认的事务隔离级别下已经能完全保证 事务的隔离性要求,即达到SQL标准的SERIALIZABLE隔离级别。
扩展阅读
并发情况下,读操作可能存在的三类问题:
- 脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。
- 不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重 复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事 务已提交的数据。
- 幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻 读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。
MySQL的事务隔离级别是怎么实现的?
InnoDB支持四种隔离级别,每种级别解决掉的问题如下表:
脏读 | 不可重复读幻读 | 幻读 | |
---|---|---|---|
READ UNCOMMITTED | Y | Y | Y |
READ COMMITTED | N | Y | Y |
REPEATABLE READ(默认) | N | N | N |
SERIALIZABLE | N | N | N |
这四种隔离级别的实现机制如下
- READ UNCOMMITTED & READ COMMITTED:
通过Record Lock算法实现了行锁,但READ UNCOMMITTED允许读取未提交数据,所以存在脏读问题。而READ COMMITTED允许读取提交数据,所以不存在脏读问题,但存在不可重复读问题。
- REPEATABLE READ:
使用Next-Key Lock算法实现了行锁,并且不允许读取已提交的数据,所以解决了不可重复读的问题。另外,该算法包含了间隙锁,会锁定一个范围,因此也解决了幻读的问题。
- SERIALIZABLE:
对每个SELECT语句后自动加上LOCK IN SHARE MODE,即为每个读取操作加一个共享锁。因此在这个事务隔离级别下,读占用了锁,对一致性的非锁定读不再予以支持。
如何解决幻读问题?
MySQL的InnoDB引擎,在默认的REPEATABLE READ的隔离级别下,实现了可重复读,同时也解决了幻读问题。它使用Next-Key Lock算法实现了行锁,并且不允许读取已提交的数据,所以解决了不可重复读的问题。另外,该算法包含了间隙锁,会锁定一个范围,因此也解决了幻读的问题。
MySQL事务如何回滚?
在MySQL默认的配置下,事务都是自动提交和回滚的。当显示地开启一个事务时,可以使用ROLLBACK 语句进行回滚。该语句有两种用法:
- ROLLBACK:要使用这个语句的最简形式,只需发出ROLLBACK。同样地,也可以写为ROLLBACK WORK,但是二者几乎是等价的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。
- ROLLBACK TO [SAVEPOINT] identifier :这个语句与SAVEPOINT命令一起使用。可以把事务回滚到标记点,而不回滚在此标记点之前的任何工作。