4.1说说你对mysql数据库事务的了解

事务可以由一条非常简单的sql语句组成,也可以由一组复杂的sql语句组成。举个最简单的场景,A向B转账100元,这个转账操作会涉及到一系列的数据库操作。
(1)从数据库中读取A的余额;
(2)将A的余额减去100元;
(3)将修改后的A余额更新到数据库中;
(4)从数据库中读取B的余额;
(5)将B的余额增加100元;
(6)将B修改后的余额更新到数据库中。
假设在执行完第三步,服务器突然掉电,mysql宕机了的话,那么如果没有事务保证操作的原子性那扣除完A的余额,B的账户余额却没有增加的话就不合理。所以要解决这个问题,就需要保证转账业务里所有数据库的操作是不可分割的,要么全部执行成功,要么全部失败,不允许中间状态的出现。
【事务的特征】
事务是由Mysql存储引擎层来实现的,InnoDB引擎是支持事务的,但是MyIsam是不支持事务的。也正因为这样,所以大多数的Mysql引擎都是用的InnoDB.
(1)原子性:一个事务中的所有操作,要么全部执行从成功,要么全部失败,不会结束在某一个中间状态;而且事务在执行过程中如果发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
实现原理:实现原子性的关键是事务回滚时能够撤销所有已成功的sql语句。
InnoDB实现回滚依靠undo log日志,当事务对数据库进行修改操作时,InnoDB会生成对应的undo log。如果事务执行失败或者调用了rollback指令,导致事务需要回滚,就可以利用undo log中的信息将数据回滚到修改之前的样子。undo log属于逻辑日志,它记录的是sql执行的相关信息。当发生回滚时,InnoDB会根据undo log的内容做出于之前相反的工作。对于insert,回滚就会执行delete;对于delete,回滚就会执行insert;对于update,回滚时会执行相反的update再把数据改回去。
一致性:数据库的完整性不会因为事务的执行而受到破坏,比如有一个字段为手机号码,它有唯一约束,表中的手机号码的值不能重复。如果一个事务对手机号码进行修改,但是事务提交后,表中的手机号码变得非唯一了,这就破坏了事务的一致性要求,这时数据库就要撤销该事务,返回事务执行前的状态。
隔离性:多个用户并发访问数据库,数据库为每一个用户开启的事务,不能被其他事务的操作数据干扰,多个并发事务之间要相互隔离。
实现原理:锁机制或MVCC
【一个事务写操作对其他事务写操作的影响】:
隔离性要求在同一时刻只能有一个事务对数据库的单个数据记录或者是多个数据记录之间进行操作。InnoDB通过锁机制来保证这一点,事务隔离级别不同,实现也是不同的。在RU/RC的隔离级别下,是不会开启next-key-lock的。在当前事务对数据记录进行操作的时候,这部分数据是锁定的,其他事务如果也需要并发修改数据,必须等待当前事务提交或回滚后释放锁才能进行操作。
持久性:事务提交后,对数据的修改就是永久性的,即便是系统故障也不会丢失。
实现原理:redo log日志

4.2并行事务会引发什么问题?

Mysql服务端是允许多个客户端连接的,这意味着Mysql会同时处理多个事务的情况。那么在同时处理多个事务时,就可能会出现脏读、不可重复读、幻读的问题。
脏读:一个事务读到了另一个未提交事务修改过的数据,这就是发生了脏读。
不可重复读:在一个事务内,在不同时刻多次读取同一个数据,如果出现前后两次读到的数据不一致的情况,就是发生了不可重复读。
幻读:在一个事务内,多次查询某个符合查询条件的数据记录,如果出现前后两次读到的记录数量是不一样的,就是发生了幻读。

4.3事务的隔离级别有哪些?

SQL标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低。
(1)读未提交:一个事务还没提交时,它做的变更就能被其他事务看到;
(2)读提交:一个事务提交以后,它做的变更才能被其他事务看到;
(3)可重复读:同一个事务中不同时刻多次读取相同的数据前后结果一致。Mysql的InnoDB引擎默认支持的隔离级别;
(4)串行化:会对数据记录加上读写锁,在多个事务对这条数据记录进行读写操作时,如果发生了读写冲突的话,后访问的事务必须等待前一个事务执行结束,才能继续执行。
image.png

4.4InnoDB是如何实现读提交和可重复读的事务隔离级别的?

InnoDB是通过Read View来实现的,实现读提交和可重复读的区别在于创建Read View的时机不同,可以把Read View理解成一个事务快照。读提交隔离级别是在每个语句执行前都会重新生成一个Read View,而可重复读隔离级别是启动事务时生成一个Read View,然后整个事务期间都一直使用这个Read View。
【注意】,执行开始事务命令,并不意味着启动了事务。在Mysql中有两种开启事务的命令,分别是:

  1. begin/start transaction
  2. start transaction with consistent snapshot

这两种开启事务的命令,事务的启动时机是不同的;
执行了begin/start transaction命令后,并不代表事务启动了。只有在执行这个命令后,执行了增删改查操作的sql语句,才是事务真正启动的时机;
执行了start transaction with consistent snapshot命令,就会马上启动事务。

4.5Read View是如何在MVCC中工作的?

(1)Read View中四个字段;
(2)聚集索引记录中有两个跟事务有关的隐藏列;
image.png
Read View中四个重要的字段:
(1)m_ids:指的是创建Read View时,当前数据库中活跃事务的事务id列表,活跃事务指的是,启动了但是还没有提交的事务;
(2)min_trx_id:指的是创建Read View时,当前数据库活跃事务中事务id最小的事务,低水位;
(3)max_trx_id:创建Read View时当前数据库中应该分配给下一个事务的id值,也就是全局事务中最大的事务id值+1;
(4)creator_trx_id:创建该Read View的事务id。
InnoDB存储引擎的数据库表,聚集索引记录中两个隐藏列:
(1)trx_id:当一个事务对该条聚集索引记录进行改动时,就会把事务的id记录在trx_id隐藏列里;
(2)roll_pointer:每次对某条聚集索引记录进行改动时,都会把旧版本的记录写入到undo日志中,然后这个隐藏列是个指针,指向每一个旧版本记录。
在创建Read View时,我们可以将记录中的trx_id划分成这三种情况:
image.png
一个事务去访问记录的时候,除了自己的更新记录总是自己可见之外,还有这几种情况;
(1)如果提交的trx_id小于Read View中的min_trx_id值,说明这个版本的记录是在创建Read View时已经提交的事务生成的,所以该版本的记录对当前事务可见。
(2)如果记录的trx_id大于等于Read View中的max_trx_id值,说明这个版本的记录是在创建Read View后由其他启动的事务生成的,所以该版本的记录对当前事务不可见。
(3)如果记录在min_trx_id和max_trx_id之间,需要判断trx_id是否在m_ids列表中:
如果记录的trx_id在m_ids列表中,说明生成该版本记录的活跃事务依然活跃着,所以该版本的记录对当前事务不可见。
如果记录的trx_id不在m_ids列表中,说明生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
举例说明,在可重复读隔离级别下,MVCC是如何工作的。
image.png
image.png
image.png
第一次读:这条记录的trx_id是50,小于事务B Read View中的低水位,所以balance=100数据的这条数据记录对事务B可见;
第二次读:id为1的数据记录生成一条新的undo log日志以链表的方式串联起来,trx_id为51,是事务A进行修改的且执行trx_id为50的前一个版本。那这时候事务B再去读发现当前记录的trx_id在低水位和高水位之间,并且存在m_ids列表中,所以当前记录对事务B不可见。所以,事务B不能读取这个版本的记录而是沿着undo log链表往下找旧版本的记录,发现trx_id为50对事务B是可见的,所以就返回balance=100W的数据记录给B。
第三次读:虽然事务A已经提交了,但对于事务B的Read View还是没有变化的,所以事务B依然不能读取到事务A已经提交的数据记录。
举例说明,MVCC在读提交的事务隔离级别是如何工作的,还是以上述逻辑为依据。
第一次读的逻辑是上述一样。
第二次读的时候,由于事务A未提交,所以第二次的逻辑也和上述一样。因为事务A的trx_id依然在事务B的Read View的ids列表中,且低水位与高位水都保持不变。
第三次读的时候,事务B生成一个新的Read View。
image.png
因为事务A对id=1的balance已经提交,退出了活跃事务,所以在B最新生成的Read View中它不在m_ids中,且低水位为52,;所以再去查询这条数据记录的时候,发现id=1的数据记录trx_id<低水位,这条数据记录对于事务B可见,所以返回balance=200万的数据记录给事务B。

4.6Mysql是如何在可重复读事务隔离级别情况下解决幻读问题的?

幻读:在一个事务内的不同时刻多次查询符合条件的数据记录,前后时刻数据记录数量不一致,这就是发生了幻读。
问题一:幻读的问题,不是已经被可重复读级别下MVCC的实现解决了吗?为什么还需要有next-key-lock呢?
image.png
image.png
从这个实验结果来看,即使事务B中途插入了一条记录,事务A前后两次查询的结果集都是一样的,并没有出现所谓的幻读问题。之所以看不到幻读现象,是因为在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。
Mysql里除了普通查询是快照读,其他都是当前读,比如update、insert、delete,select…from update,select… in share mode。这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。因此讨论可重复读隔离级别下的幻读现象,是要建立在当前读的情况下。
InnoDB引擎为了解决可重复读隔离级别下使用当前读而造成的幻读问题,就引出了next-key-lock,就是记录锁间隙锁的组合。
加锁规则,两个原则、两个优化和一个Bug。
(1)原则1:加锁的基本单位是next-key-lock,next-key-lock是前开后闭的区间。
(2)原则2:查找过程中访问到的对象才会加锁。
(3)优化1:索引上的等值查询,给唯一索引加的锁,next-key-lock退化成行锁。
(4)优化2:索引上的等值查询,向右遍历到最后一个值不满足等值条件的时候,next-key-lock退化成间隙锁。
(5)bug:唯一索引上的范围查询会访问到不满足条件的第一个值位置。
比如上述加锁,(2,3]、(3,4]、(4,无穷)

4.7说说你对Mysql的三个日志的了解

binlog:
二进制日志文件就是binlog,它是归档日志,binlog记录了Mysql所有修改数据库的操作,然后以二进制的形式记录在日志文件中,其中还包括了每条语句执行的时间和所消耗的资源,以及相关的事务信息。binlog经常用于主从库同步,从库备份读写分离的场景。当我们开启一个事务,执行写的sql语句流程时,binlog的写入机制是。
(1)系统给binlog cache分配了一块内存,每个线程一个,参数binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
(2)事务执行过程中,先把日志写入到biglog cache中。
(3)事务提交的时候,执行器把binlog cache里的完整事务写入到操作系统的Page Cache中,并清空binlog cache。
image.png
(4)每个线程都有自己binlog cache,但是共用同一份binlog文件。

  1. write就是把日志写入到文件系统的binlog files,并没有把数据持久化到磁盘,所以速度比较快。
  2. fsync才是将数据持久化到磁盘的操作。

(5)write和fsync的时机,是由参数sync_binlog控制的:

  1. sync_binlog=0的时候,表示每次提交事务都只write,不fsync;
  2. sync_binlog=2的时候,表示每次提交都会fsync;
  3. sync_binlog=N的时候,表示每次提交事务都write,但积累N个事务后才fsync;设置为N的时候,对应的风险是如果主机发生异常重启,会丢失最近N个事务的binlog日志。

redolog:
redolog是用来实现事务的持久性的,保证事务ACID中的D。它由两部分组成:一是内存中的重做日志缓冲redo log buffer,这个是易失的;二是重做日志文件redo log file,它是持久化在磁盘中的。如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程IO成本、查找成本都很高。为了解决这个问题,mysql就设计者使用了WAL(Write-Ahead Logging)技术,它的关键点就是先写日志,再写磁盘。
InnoDB的redo log是固定大小的,比如可以配置为一组4个文件,每个文件的大小都是1GB,那么总共就可以记录4GB的操作。从头开始写,写到末尾就回到开头循环写。write position是当前记录的位置,一边写一遍后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。write position和checkpoint之间的还空着的部分,可以用来记录新的操作。如果write position追上check point这时候就不能再执行新的更新,得擦除一些记录,把checkpoint推进以下。有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失。这个能力称为crash-safe。redolog是物理日志,记录的是”在某个数据页上做了什么”,mysql的数据最终保存在数据页中,物理日志记录的是数据页的变更。
redolog的写入时机:
image.png
(1)事务在prepare状态时,就已存在redo log buffer中,物理上是在Mysql进程内存中;
(2)写到page cache里,但是没有持久化;
(3)持久化到磁盘
日志写到redo log buffer是很快的,写到page cache的速度也差不多,但是持久化到磁盘的速度就很慢了。
为了控制redo log的写入策略,InnoDB提供了innodb_flush_log_at_trx_commit参数,
(1)设置为0的时候,每次事务提交时都只是把redo log留在redo log buffer中;
(2)设置为1的时候,每次事务提交时都将redo log直接持久化到磁盘;
(3)设置为2的时候,每次事务提交时都只是把redo log写到page cache。
InnoDB有一个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘。
undolog:
undolog是用来帮助事务回滚及MVCC的功能,当事务对数据库进行修改操作时,InnoDB会生成对应的undo log。如果事务执行失败或者调用了rollback执行,导致事务需要回滚,就可以利用undo log中的信息将数据回滚到修改之前的样子。undo log属于逻辑日志,它记录的是sql执行的相关信息。当发生回滚时,InnoDB会根据undo log的内容做出于之前相反的工作。对于insert,回滚就会执行delete;对于delete,回滚就会执行insert;对于update,回滚时会执行相反的update再把数据改回去。