zoukankan      html  css  js  c++  java
  • mysql 5 事务详解

    一 概念

    事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个sql语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL支持事务,本文介绍基于MySQL5.6。

    一个经典案例说明事务

    银行引用是事务的一个经典例子:假如银行有两张表,一张支票表,一张储蓄表,现在需要从Jones用户的支票账户转移200¥ 至储蓄账户,那么至少需要三步:
        1. 检查Jones的支票账户余额是否大于200¥
        2. Jones的支票账户-2003. Jones的储蓄账户+200¥
    上述三步可组成一个事务,当2、3步故障时,之前执行的操作会自动回滚,保证数据的一致性。

    二  事务的ACID特性

    ACID是衡量事务的四个特性:

    • 原子性(Atomicity,或称不可分割性):语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于undo log
    • 持久性(Durability):保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redo log
    • 隔离性(Isolation):保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)
    • 一致性(Consistency):事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障

    1 原子性

    定义

    原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。

    实现原理:undo log

        在说明原子性原理之前,首先介绍一下MySQL的事务日志。MySQL的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种事务日志redo log(重做日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。

        下面说回undo log。实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。

        undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。

         以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。

    2 持久性

    1)定义

    持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

    2). 实现原理:redo log

    redo log和undo log都属于InnoDB的事务日志。下面先聊一下redo log存在的背景。

          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进行刷盘(redo log内容刷到磁盘上)。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。

          既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:

    (1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。

    (2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。

    3). redo log与binlog

    我们知道,在MySQL中还存在binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的:

    (1)作用不同:redo log是用于crash recovery的,保证MySQL宕机也不会影响持久性;binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。

    (2)层次不同:redo log是InnoDB存储引擎实现的,而binlog是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持InnoDB和其他存储引擎。

    (3)内容不同:redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。

    (4)写入时机不同:binlog在事务提交时写入;redo log的写入时机相对多元:

    • 前面曾提到:当事务提交时会调用fsync对redo log进行刷盘;这是默认情况下的策略,修改innodb_flush_log_at_trx_commit参数可以改变该策略,但事务的持久性将无法保证。
    • 除了事务提交时,还有其他刷盘时机:如master thread每秒刷盘一次redo log等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快

    3、隔离性

    1). 定义

          与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务

    之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。

     通过锁机制来实现,同一时间锁住的资源只能被一个线程(会话)访问

    4、一致性

    1). 基本概念

          一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性

    如转账前后,两个账户余额的和应该不变,A账号给B账号转账500  A减少500  B只能增加500 不能说因为网络问题转了2次500 ,一致性就要求,A-500   B+500

    2). 实现

          可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。

    实现一致性的措施包括:

    • 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
    • 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
    • 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致

    三    事务隔离级别

           SQL标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与读问题的关系如下:

    • 读未提交:read uncommitted     可能产生脏读
    • 读已提交:read committed    不可重复读(可能读到别的事务改变后的数据)
    • 可重复读:repeatable read    mysql默认隔离级别,读当前事务的版本数据,但可能产生幻读(读到别的事务新增/删除后的数据)
    • 串行化:serializable   能解决以上所有问题,包括幻读问题 (锁表机制实现的)

    脏读:

        一个事务读取到另外一个事务未提交的数据。

    例子:A向B转账,A执行了转账语句,但A还没有提交事务,B读取数据,发现

    自己账户钱变多了!B跟A说,我已经收到钱了。A回滚事务【rollback】,等B再查看账户的钱时,发现钱并没有多...

    读取的数据被别的事务回滚了

    出现脏读的本质就是因为操作(修改)完该数据就立马释放掉锁,导致读的数据就变成了无用的或者是错误的数据

    不可重复读

          一个事务读取到另外一个事务已经提交的数据,也就是说一个事务可以看到其他事务所做的修改,例如:A查询数据库得到

    数据,B去修改数据库的数据,导致A多次查询数据库的结果都不一样【危害:A每次查询的结果都是受B的影响的,那么A查询出来的信息就没有意思了】

    虚读(幻读)

          是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。和不可重复读类似,但虚读(幻读)会读到其他事务的插入的数据,导致前后读取不

    一致,幻读的重点在于新增或者删除 (数据条数变化),不可重复读的重点是修改.

     RR解决脏读、不可重复读问题,使用的是MVCC

    四  MVCC (多版本的并发控制协议)

    RR解决脏读、不可重复读等问题,使用的是MVCC:MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议。

    下面的例子很好的体现了MVCC的特点

          在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)——在T5时刻,事务A和事务C可以读取到不同版本的数据。

     MVCC最大的优点是读不加锁,因此读写不冲突,并发性能好。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是否可见。

    上面第3条也可能很好的解释了,上图案例中为什么事务C查询到结果是200,因为在查询之前事务给整个系统打的快照里,余额已经是200了(被事务B修改为了200)

    mvcc基本特征

    • 每行数据都存在一个版本,每次数据更新时都更新该版本。
    • 修改时Copy出当前版本随意修改,各个事务之间无干扰。
    • 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)

    InnoDB存储引擎MVCC的实现策略

          在每一行数据中额外保存两个隐藏的列:当前行创建时的版本号删除时的版本号(可能为空,其实还有一列称为回滚指针,用于事务回滚,不在本文范畴)。这里的版本号并不是实际的时间值,而是系统版本号。每开始新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询每行记录的版本号进行比较。

    每个事务又有自己的版本号,这样事务内执行CRUD操作时,就通过版本号的比较来达到数据版本控制的目的。

    MVCC下InnoDB的增删查改是怎么work的

    1.插入数据(insert):记录的版本号即当前事务的版本号

    执行一条数据语句:insert into testmvcc values(1,"test");

    假设事务id为1,那么插入后的数据行如下:

    Mysql中MVCC的使用及原理详解

    2、更新数据: 采用的是先标记旧的那行记录为已删除,并且删除版本号是事务版本号,然后插入一行新的记录的方式。

    比如,针对上面那行记录,事务Id为2 要把name字段更新

    update table set name= 'new_value' where id=1;

    Mysql中MVCC的使用及原理详解

    3、删除数据: 就把事务版本号作为删除版本号。比如

    delete from table where id=1;

    Mysql中MVCC的使用及原理详解

    4、查询操作:

    从上面的描述可以看到,在查询时要符合以下两个条件的记录才能被事务查询出来:

    1) 删除版本号未指定或者大于当前事务版本号,即查询事务开启后确保读取的行未被删除。(即上述事务id为2的事务查询时,依然能读取到事务id为3所删除的数据行)

    2) 创建版本号 小于或者等于 当前事务版本号 ,就是说记录创建是在当前事务中(等于的情况)或者在当前事务启动之前的其他事物进行的insert。

    (即事务id为2的事务只能读取到create version<=2的已提交的事务的数据集)

    关于Mysql中MVCC的总结

            客观上,我们认为他就是乐观锁的一整实现方式,就是每行都有版本号,保存时根据版本号决定是否成功。

    但由于Mysql的写操作会加排他锁(前文有讲),如果锁定了还算不算是MVCC?

    了解乐观锁的小伙伴们,都知道其主要依靠版本控制,即消除锁定,二者相互矛盾,so从某种意义上来说,Mysql的MVCC并非真正的MVCC,他只是借用MVCC的名号实现了读的非阻塞而已。

  • 相关阅读:
    雪花算法 Java 版
    Java 生成有序 UUID
    Spring Boot 2 集成 Swagger
    Spring Cloud 学习 (九) Spring Security, OAuth2
    Spring Cloud 学习 (八) Spring Boot Admin
    Spring Cloud 学习 (七) Spring Cloud Sleuth
    Spring Cloud 学习 (六) Spring Cloud Config
    原创:全排列非递归算法:微软给出的算法
    原创:协同过滤之spark FP-Growth树应用示例
    转载:scala中的:++::::::
  • 原文地址:https://www.cnblogs.com/hup666/p/13377629.html
Copyright © 2011-2022 走看看