深入理解事务爆砍面试官

事务面试第一阶段

概述

我们的数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。
这些问题的本质都是数据库的多事务并发问题,为了解决多事务并发问题,数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制,用一整套机制来解决多事务并发问题

事务及其ACID属性

事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。
原子性(Atomicity) :事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
一致性(Consistent) :在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性。
隔离性(Isolation) :数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
持久性(Durable) :事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

并发事务处理带来的问题

更新丢失(Lost Update)或脏写

当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新。

脏读(Dirty Reads)

一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。
  一句话:事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求。

不可重读(Non-Repeatable Reads)

一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
  一句话:事务A内部的相同查询语句在不同时刻读出的结果不一致,不符合隔离性

幻读(Phantom Reads)

一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
 一句话:事务A读取到了事务B提交的新增数据,不符合隔离性

事务隔离级别

“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read)   幻读(Phantom Read)读未提交(Read uncommitted) 可能 可能 可能
读已提交(Read committed) 不可能 可能 可能
可重复读(Repeatableread) 不可能 不可能 可能
可串行化(Serializable)  不可能 不可能 不可能

数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。
同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读"和“幻读”并不敏感,可能更关心数据并发访问的能力。

如何解决脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)

脏读

事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求。
如何解决
通过提高事务隔离级别到读未提交,这样粒度不大,也可以解决脏读

不可重复读

事务A内部的相同查询语句在不同时刻读出的结果不一致,不符合隔离性
通过mvcc来解决

幻读

事务A读取到了事务B提交的新增数据,不符合隔离性
(1)在快照读读情况下,mysql通过mvcc来避免幻读。(查询)
(2)在当前读读情况下,mysql通过next-key来避免幻读。锁住某个条件下的数据不能更改。(对数据进行操作)
可重复读的隔离级别下使用了MVCC(multi-version concurrency control)机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。
所以在rr级别一般需要通过行锁来进行操作数据,查询则mysql会通过mvcc来避免

问题为什么在rr级别的情况会如果不命中索引的情况下会行锁变表锁,而rc不会

因为rc他不需要来保证数据的可重复读,所以他行锁不会升级为表锁。而rr的情况下,不可重复读是mvcc来解决了,加锁的是应该为了解决幻读的当前读情况。为什么在没有索引的情况下会从行锁升级为表锁,因为你在没有快速击中索引的情况下,就不能确保锁具体哪行的数据,所以直接升级为表锁。
主要是效率的问题,rc无需保证所以,他有的是时间来找到这一行,rr需要迅速来找到行锁。

问题mysql是如何来保证事务的四大特性?

脏读、可重复读、幻读都解决了,那么mysql是如何来保证事务的四大特性呢?
原子性:指的是一个事务中的操作要么全部成功,要么全部失败。
一致性:指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设中间sql执行过程中系统崩溃A也不会损失100块,因为事务没有提交,修改也就不会保存到数据库。
隔离性:指的是一个事务的修改在最终提交前,对其他事务是不可见的。
持久性:指的是一旦事务提交,所做的修改就会永久保存到数据库中。

总的来说,MySQL中事务的原子性是通过 undo log 来实现的,事务的持久性性是通过 redo log 来实现的,事务的隔离性是通过读写锁+MVCC来实现的。

事务的一致性通过原子性、隔离性、持久性来保证。也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。数据库必须要实现AID三大特性,才有可能实现一致性。

那么mvcc只是实现了隔离性,这是面试问事务的第二阶段来了

面试第二阶段

MVCC多版本并发控制机制

Mysql在可重复读隔离级别下如何保证事务较高的隔离性
这个隔离性就是靠MVCC(Multi-Version Concurrency Control)机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。
Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制

mvcc机制剖析

undo日志版本链与read view机制详解

undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链
在这里插入图片描述

在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。

版本链比对规则:
  1. 如果 row 的 trx_id 落在绿色部分( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
  2. 如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);
  3. 如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况
    a. 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的事务是可见的);
    b. 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。

对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag标记位为true,意味着记录已被删除,则不返回数据。

注意:begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作InnoDB表的语句,事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。

总结:

MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。

面试第三阶段

MySQL中事务的原子性是通过 undo log 来实现的,事务的持久性性是通过 redo log 来实现的,事务的隔离性是通过读写锁+MVCC来实现的。
那么undo log和redo log又是怎么来保证的呢
在事务的具体实现机制上,MySQL采用的是WAL(Write-ahead logging,预写式日志)机制来实现的。这也是是当今的主流方案。
在使用WAL的系统中,所有的修改都先被写入到日志中,然后再被应用到系统中。通常包含redo和undo两部分信息。
redo log称为重做日志,每当有操作时,在数据变更之前将操作写入redo log,这样当发生掉电之类的情况时系统可以在重启后继续操作。
undo log称为撤销日志,当一些变更执行到一半无法完成时,可以根据撤销日志恢复到变更之间的状态。

redo日志 保证持久性

redo日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后系统崩溃重启后可以把事务所做的任何修改都恢复出来。

如何快速恢复

因为一个页有16kb,如果一次恢复16kb肯定效率不高,所以把修改了哪些东西记录一下就好,比方说某个事务将系统表空间中的第100号页面中偏移量为1000处的那个字节的值1改成2我们只需要记录一下:
将第0号表空间的100号页面的偏移量为1000处的值更新为2。

优点

1、redo日志占用的空间非常小
存储表空间ID、页号、偏移量以及需要更新的值所需的存储空间是很小的。
2、redo日志是顺序写入磁盘的
在执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO。

格式

InnoDB们针对事务对数据库的不同修改场景定义了多种类型的redo日志,通用的一般包含:
type:该条redo日志的类型,redo日志设计大约有53种不同的类型日志。
space ID:表空间ID。
page number:页号。
data:该条redo日志的具体内容。

redo log写入

在无事发生时redo log也在记录着操作的数据
redo日志都放在了大小为512字节的块(block)中

服务器启动时就向操作系统申请了一大片称之为redo log buffer的连续内存空间,翻译成中文就是redo日志缓冲区,我们也可以简称为log buffer。

这片内存空间被划分成若干个连续的redo log block,我们可以通过启动参数innodb_log_buffer_size来指定log buffer的大小,该启动参数的默认值为16MB。

向log buffer中写入redo日志的过程是顺序的,也就是先往前边的block中写,当该block的空闲空间用完之后再往下一个block中写

redo log刷盘时机

那如果redo log内存空间满了,他就得将一部分内存的数据放入磁盘
一般是四个时候
如果当前写入log buffer的redo日志量已经占满了log buffer总容量的大约一半左右,就需要把这些日志刷新到磁盘上
在事务提交时可以不把修改过的Buffer Pool页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的redo日志刷新到磁盘。
后台有一个线程,大约每秒都会刷新一次log buffer中的redo日志到磁盘
正常关闭服务器时等等。
那么在线程间隙中,以及没提交的时候这时候出现问题,这里的部分数据就会丢失

redo日志文件组

MySQL的数据目录(使用SHOW VARIABLES LIKE 'datadir’查看)下默认有两个名为ib_logfile0和ib_logfile1的文件
innodb_log_group_home_dir,该参数指定了redo日志文件所在的目录,默认值就是当前的数据目录。
innodb_log_file_size,
该参数指定了每个redo日志文件的大小,默认值为48MB,
innodb_log_files_in_group,该参数指定redo日志文件的个数,默认值为2,最大值为100。

面试如何回答

先回答redo log的作用 在回答格式, redo log写入, redo log刷盘时机,日志文件组,其他参数还追问就可以打哈哈了

undo日志

我们说过事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。但是偏偏有时候事务执行到一半会出现一些情况,比如:
情况一:事务执行过程中可能遇到各种错误,比如服务器本身的错误,操作系统错误,甚至是突然断电导致的错误。
情况二:程序员可以在事务执行过程中手动输入ROLLBACK语句结束当前的事务的执行。
这两种情况都会导致事务执行到一半就结束,但是事务执行过程中可能已经修改了很多东西,为了保证事务的原子性,我们需要把东西改回原先的样子,这个过程就称之为回滚(英文名:rollback),这样就可以造成这个事务看起来什么都没做,所以符合原子性要求。

回滚提交的事务和未提交的事务

已提交的事务会继续进行从buffer pool缓存中继续刷盘,未提交的就把buffer pool的数据回滚

回滚的情况

每当我们要对一条记录做改动时(这里的改动可以指INSERT、DELETE、UPDATE),都需要把回滚时所需的东西都给记下来。比方说:
你插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉。
你删除了一条记录,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中。
你修改了一条记录,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值。
这些为了回滚而记录的这些东西称之为撤销日志,英文名为undo log/undo日志。这里需要注意的一点是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo日志。

事务id

当我们需要回滚的时候需要通过事务id来回滚

分配id的时期

一个事务可以是一个只读事务,或者是一个读写事务:
对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个事务id,否则的话是不分配事务id的。

对于读写事务来说,只有在它第一次对某个表(包括用户创建的临时表)执行增、删、改操作时才会为这个事务分配一个事务id,否则的话也是不分配事务id的。
单纯查询无需事务id

undo日志的格式

undo日志给insert、delete、update设计了不同的设计模式

insert

我们在回滚插入操作时,只需要知道这条记录的主键信息,然后根据主键信息做对应的删除操作,做删除操作时就会顺带着把所有二级索引中相应的记录也删除掉

DELETE操作对应的undo日志

因为是回滚,你删除了一条记录,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中,

update操作对应的undo日志

不更新主键的情况
在不更新主键的情况下,又可以细分为被更新的列占用的存储空间不发生变化和发生变化的情况。
有任何一个被更新的列更新前比更新后占用的存储空间大,或者更新前比更新后占用的存储空间小都不能进行就地更新

在不更新主键的情况下,如果有任何一个被更新的列更新前和更新后占用的存储空间大小不一致,那么就需要先把这条旧的记录从聚簇索引页面中删除掉,然后再根据更新后列的值创建一条新的记录插入到页面中。

更新主键的情况

将旧记录进行delete mark操作,再插入一条新纪录

版本链

在对一条记录进行delete mark操作前,需要把该记录的旧的trx_id和roll_pointer隐藏列的值都给记到对应的undo日志中来,执行完delete mark操作后,它对应的undo日志和INSERT操作对应的undo日志就串成了一个链表。这个链表就称之为版本链
这就是实现mvcc的机制,与上方结合了

面试第四个阶段

到这的话,我这种1w的人应该已经涨5k吧,嘻嘻,数据库事务应该也会把面试官干烂了

总结事务的流程(总的来说,事务流程分为事务的执行流程和事务恢复流程。)

事务执行

我们已经知道了MySQL的事务主要主要是通过 Redo Log和 Undo Log实现的。
MySQL事务执行流程如下图
https://cdn.processon.com/63c540f025c8764ea62b4fab?e=1673875201&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:APf9i2X3qRclvQZmnqi5jjtLeZE=https://cdn.processon.com/63c540f025c8764ea62b4fab?e=1673875201&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:APf9i2X3qRclvQZmnqi5jjtLeZE=
可以看出,MySQL在事务执行的过程中,会记录相应SQL语句的UndoLog 和 Redo Log,然后在内存中更新数据并形成数据脏页。接下来RedoLog会根据一定规则触发刷盘操作,Undo Log 和数据脏页则通过刷盘机制刷盘。事务提交时,会将当前事务相关的所有Redo Log刷盘,只有当前事务相关的所有Redo Log 刷盘成功,事务才算提交成功。

事务恢复

如果一切正常,则MySQL事务会按照上图中的顺序执行。如果MySQL由于某种原因崩溃或者宕机,当然进行数据的恢复或者回滚操作。
如果事务在执行第8步,即事务提交之前,MySQL 崩溃或者宕机,此时会先使用Redo Log恢复数据,然后使用Undo Log回滚数据。
如果在执行第8步之后MySQL崩溃或者宕机,此时会使用Redo Log恢复数据,大体流程如下图所示。

在这里插入图片描述
很明显,MySQL崩溃恢复后,首先会获取日志检查点信息,随后根据日志检查点信息使用Redo Log进行恢复。MySQL崩溃或者宕机时事务未提交,则接下来使用Undo Log回滚数据。如果在MySQL崩溃或者宕机时事务已经提交,则用Redo Log恢复数据即可

恢复机制

在服务器不挂的情况下,redo日志简直就是个大累赘,不仅没用,反而让性能变得更差。但是万一数据库挂了,就可以在重启时根据redo日志中的记录就可以将页面恢复到系统崩溃前的状态。
MySQL可以根据redo日志中的各种LSN值,来确定恢复的起点和终点。然后将redo日志中的数据,以哈希表的形式,将一个页面下的放到哈希表的一个槽中。之后就可以遍历哈希表,因为对同一个页面进行修改的redo日志都放在了一个槽里,所以可以一次性将一个页面修复好(避免了很多读取页面的随机IO)。并且通过各种机制,避免无谓的页面修复,比如已经刷新的页面,进而提升崩溃恢复的速度。

总结

事务需要配合BufferPool食用更舒服,上面流程弄清楚了数据库事务应该没有面试官问的倒你了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值