Mysql - InnoDB引擎对事务ACID的实现原理分析

目录

事务的一些命令

事务的一些基础知识

Mysql中的MVCC、Undolog、视图、当前读的关系

Mysql的事务实现原理

1、原子性

2、一致性

3、持久性

4、隔离性


    在开始分析Mysql的InndoDB怎么实现事务的四个特性,应对不同的隔离级别之前,需要先1、知道一些事务的命令  2、事务的一些基础知识 3、什么是当前读,快照读 4、Mysql中的MVCC与视图和Undo Log的关系等:

事务的一些命令

    Mysql只有InnoDB引擎支持事务,开始分析前先看看事务的相关命令,这些都是下面实验数据会用到的。事务默认的自动提交的,在my.cnf(Windows中是my.ini)配置中设置: autocommit = 1 (或者ON),其他事务的相关命令如下:

开启事务: begin 或 start transaction; // 只是开启事务,当需要的时候才创建视图

                   start transaction with consistent snapshot;  // 开启事务,并且马上创建视图

提交事务:commit;

回滚事务:rockback;

设置当前会话事务隔离级别:set session transaction isolation level 【read uncommitted / read committed / repeatable read / serializable】

设置全局的事务隔离级别:set global transaction isolation level 【read uncommitted / read committed / repeatable read / serializable】

查询数据库的事务隔离级别:SELECT @@tx_isolation

 

事务的一些基础知识

    事务并不是Mysql或者InnoDB特有,是事务模型的抽象,事务需要满足原子性、一致性、持久性、隔离性,而隔离性分为四个级别:读未提交、读已提交、可重复读、串行化。

原子性:事务开始后所有操作要不都执行要不都不执行,是一个不可分割的整体,不能停留在中间状态,中间状态对外不可见。

一致性:事务开始和结束后,数据库的完整性没有破坏,比如:A向B转账,不能出现A扣钱了,而B没有收到钱。

持久性:事务完成后所有的操作都必须持久化,即哪怕重启后不能丢失数据(内存重启会丢失数据)。

隔离性:所有的事务之间互不干扰,A事务先执行就不能读取到B事务的任何操作。

    事务的要保证完全的隔离性,就必须对所有的操作按照表锁进行互斥,但是数据安全性(正确性)是满足了但是并发性能过低,于是出现了事务隔离级别,提高性能的同时入侵了一致性。这就需要根据自己的业务能容忍的事务隔离级别所带来的问题,Mysql的可重复读隔离级别解决了不可重复读的问题,并解决了部分幻读的问题(当前读获取的数据会有幻读的问题,快照读没有问题)。下面是每个事务隔离级别对应可能出现的问题:

脏读:事务A读取到了事务B中间的数据,而之后事务B发生了回滚,那么事务A读取到的就是脏数据,就发生了脏读。脏读只可能发生在读未提交的隔离级别,但是基本所有的应用对不会选择该事务隔离级别,要不问题出现了都不知道从哪里找。

不可重复读:事务A执行过程中部对一个条数据发生了多次查询操作,中间(时间)事务B对数据发生过修改操作,导致事务A前后读取到数据不一致。 不可重复读针对的是【修改】操作,只需要事务期间将对应的数据行锁住即可解决。

        

幻读:事务A在执行过程中查询多次 select count(*) from table where id > 8,在此期间事务B新增了一条id为12的数据,导致事务A多次拿到的count值变多了一条,称为幻读。删除操作也是一样,只是后面拿到的次数少了一条。删除操作类似上面解决不可重复读只需要将行锁加上互斥操作即可,但是新增操作记录还不存,没有办法加锁(行锁是作用在索引B+树上的记录锁)。幻读针对的是【新增、删除】操作

        

 

Mysql中的MVCC、Undolog、视图、当前读的关系

    MVCC与Read View、当前读和快照读、Undo Log的关系这里稍微梳理一下,方便下面事务实现原理的分析。MVCC(多版本并发控制)中会增加三个字段:DB_RTX_ID指向每条数据的新增、修改(删除也被视为特殊的更新);DB_ROLL_PTR指向回滚段的Undo log记录,如下图;DB_ROW_ID指向行主键,InnoDB聚簇索引(即B+树的主键,因为我们在创建表的时候可以不使用默认自增主键)。这里需要再梳理一下,表有一个或者多个表空间组成(有一种叫撤销表空间),MVCC多版本的行记录与表的关系为:表 》表空间 》段 》区 》页 》行(行里有这三个字段),如下图(自己画的不是很好,也可以结合标准图看):

 

    梳理完上面的关系,需要梳理MVCC的多版本有什么作用,除了根据回滚字段找到回滚段中的Undo log进行撤销,还有什么作用,先看一张图:

MVCC的视图(Read View):首先视图只在read committed、repeatable read事务隔离级别下才存在,在开启事务后当需要时会创建视图,即设定一个版本号,由于其他所有数据都是多版本同时存在,那么视图版本创建时就相当于拥有了所有行的静态快照。当前我们也可以要求在创建事务时立即创建快照版本,即上面的命令:start transaction with consistent snapshot。

再梳理当前读和快照读。当前读:总是读取数据的最新版本,如上图中的值 4,比如:select ... for update、update、delete时需要先执行查询也是当前读。历史读:就是根据MVCC视图的版本,查询时总是根据当前值,通过Undo log记录一直查到属于自己的版本数据,如上图中,视图1在需要查询数据时,总是根据当前值一直往后翻Redo Log一直找到自己的版本。并且只有当没有视图指向自己时,undo log才能被删除。

 

Mysql的事务实现原理

1、原子性

    Mysql事务的原子性依赖事务的回滚机制实现,当事务完成需要提交(commit动作)时,则依赖Mysql的双写(double Write)机制提交事务,否则rollback机制依赖MVCC底层的版本行数据的回滚字段(指向了回滚表空间的回滚段)。

2、一致性

    一致性也依赖上面的Mysql双写机制,崩溃恢复。并且一致性依赖隔离事务隔离级别,不一致的问题就是上面的隔离级别与可能出现的问题(脏读、幻读、不可重复读)。崩溃恢复在下面(crash)持久性时一并说明。

3、持久性

    持久性Mysql官方说明比较多:1、InnoDB双写缓存区  2、innodb_flush_log_at_trx_commit(事务提交落盘策略,性能与丢数据风险的权衡) 3、sync_binlog(同步binlog的策略) 4、innodb_file_per_table(表的表空间存储在单独文件还是系统表空间)5、是否支持fsync(),数据备份等【更多是DBA需要关注的可能风险点】。

    持久性基于binlog + redolog的二阶段提交方式实现,也反证明一致性的实现。如下图当执行一条 update语句时,是执行器调用对应底层的InnoDB引擎的接口, Binlog和redolog是一定要保证一致的,不论在何时发生崩溃,否则 binlog执行数据同步(主从复制)后,节点间的数据就无法保证一致性。

    整个流程主要点说明:

    1、update需要先执行一次当前读操作,获取多版本的最新数据

    2、执行器第一次调用InnoDB接口,事务处理prepare阶段,写入了redo log。

crash:此时事务不完整,binlog还没有写,不会有数据不一致的问题。

    3、执行器自己写binlog

crash:重启完成后发现事务是未提交的,可以根据事务撤回binlog。

    4、执行器调用InnoDB接口,提交事务。

 

4、隔离性

    串性化【表锁】事务隔离级别类似MyISAM引擎一样,直接获取表锁,互斥排队执行操作。所以不存在脏度、幻读、不可重复读的问题,但是性能也最低。

    读未提交【什么都没有】的事务隔离级别实现非常简单,没有Mysql视图没有锁,那么每次读取数据时都是当前读(获取的都是最新值),最可怕的就是当前读时另一个线程创建了数据,但是后面事务进行了回滚就获取到了脏数据。幻读和不可重读读的问题也都存在。

    读已提交【创建的多个视图】只是在需要的时候开启一个视图,比如上图中事务A执行了三次读取操作,就会在读取前创建了三个视图,根据MVCC的多本就对应到了具体的Undo Log上,如上图的往回查找过程。读已提交事务隔离级别根据视图Read View总是会查询到版本创建时的数据,不是当前读,就不会读到别人没有提交的数据。或者说视图创建时,维护了当时的事务状态【已提交的,已创建的、未提交的】。

    不可重复读【维护同一个视图 + 行锁 + 临建锁 + 间隙锁】隔离作为Mysql的默认事务隔离级别,实现时会在开启事务后,需要时开启视图(创建一个版本号)并且直到事务提交都会维护同一个版本号,然后再根据MVCC所有行数据的多版本对应到Undo Log中。

1、可重复读隔离级别怎么解决不可重复读?

    不可重复读针对的就是update语句,而update时本身行已经存在(行中的DB_ROW_ID已经存在),此时只需要行锁将对应的行锁住即可。

2、可重复读隔离级别解决了大部分的幻读问题,怎么理解?

视图本身只对快照读有用,如上图中的普通select语句,如果此时执行的是当前读操作(select ...for update,delete,update)此时就会有问题,这种情况下还是需要将整个表锁住才行,即串行化的表锁。也就是说解决不了当前读的幻读问题。那是怎么解决的快照读的幻读问题?

视图总会找到对应的MVCC版本数据,幻读针对的是新增和删除操作,特别是新增操作此时数据行都还不存在,依靠行锁是完全不行的,此时需要锁住行与行之间的间隙。如果InnoDB认为细粒度锁的代价太大时,也会直接退化为表锁。这里需要理解下面的锁概念(更多锁的信息可以参考:Mysql - 争取一篇讲清楚Mysql中的锁(全局锁、表级锁、行级锁)及加锁规则、死锁问题):

行锁是锁在聚簇索引上面的,即B+树的主键索引;

间隙锁是一个前开后开的空间;

临建锁是一个前开后闭的空间,可以理解 临建锁是行锁和间隙锁的集合。

而加锁的规则为:

1、加锁的基本规则是next lock【临键锁,前开后闭】; 查询的过程中查到的对象才会加锁;

2、索引上的等值查询,给唯一索引加锁时,next lock退化为行锁;索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为gap lock;

3、唯一索引上的范围查询,会查询到不满足条件的第一个值为止
 

即总结为:Mysql的Inndo引擎,可重复读(Repeatable Read)默认事务隔离级别,使用行锁解决了不可重复读的问题;基于维护同一个视图(Read View),再配合 行锁(Record Lock) + 临建锁(Next-key Lock)+ 间隙锁(Gap Lock)的加锁规则,解决了快照读的幻读问题;没有解决当前读的幻读问题。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值