事务的基本概念

MySQL中,事务的支持是在引擎层的,然而MySQL原生的MyISAM引擎并不支持事务,因此逐渐被支持事务的InnoDB引擎所取代。
在MySQL中,事务的概念是指对数据库的一组操作是原子的,要么全部成功,要么全部失败。

一、事务的基本要素(特性)(ACID)


  1. 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
  2. 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
  3. 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
  4. 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

二、事务的并发问题


  1. 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
  2. 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
  3. 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

    小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

三、MySQL事务隔离级别


事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)
  • mysql默认的事务隔离级别为repeatable-read

    事务不同隔离级别的实现方式

    读未提交:事务隔离性的最低级别,该级别下对数据没有做任何的控制;
    读提交:事务中的每条SQL语句开始执行时候都会创建一个视图,一个事务中会有多个视图的创建,每次创建的时候都会从数据库中获取最新的数据;
    可重复读:会在事务开始的时候创建一个视图,整个事务的执行期间都以这个视图为准,因此能够保证对数据的操作未提交之前对其他事务不可见,其他事务对数据的操作对当前事务也不可见;
    串行化:是直接加锁,其他事务得等锁释放后才能开始。

    多版本并发控制(MVCC)

    mvcc机制是mysql解决事务问题一项重要机制,通过这个机制,mysql解决了关于事务的问题:脏写、脏读、重复读的问题,但是默认的不可重复读的情况下还是会出现幻读的问题。
    在mysql InnoDB中,只有READ COMMITEREPEATABLE READ 二个隔离级别下面,MVCC才会工作。READ UNCOMMITED 读取最新版本的数据不需求MVCC进行多版本并发的控制,而SERIALIZABLE会对所有读取操作加锁,同样不需求MVCC。mysql InnoDB 存储引擎中 MVCC 机制简单的来说是通过隐式列 + undo log + read view 实现的。
    我们知道MySQL的默认隔离级别是RR,即可重复读,也就意味着:
    一个事务开始之前,所有还没有提交的事务,它都不可见!
    那如果一条数据(初始值是1)同时有两个事务来操作它,A事务将该条数据+1,B事务将该条数据+2,那么在RR的隔离级别下,A事务会得到数据结果为2?B事务会得到数据结果为3?如果是这样,那么就是MySQL的bug了,实际上我们想得到的结果是4。
    所以我们得想清楚一个事情:
    在指定的一个时间段内,一条数据被多个事务执行,如何保证数据的正确一致性?
    任意一个事务回滚的时候,所操作的数据都能够正确的回归到事务开始时候的状态吗?
    这就是MVCC要做的事情,当然远不止于这些!

    ◆ update语句的执行流程

    比如这样一条更新语句,其中id是主键:

    1. update table_test set num = num+1 where id = 1;

    它的MySQL内部执行流程如下:
    001.png

  • MySQL执行器先找InnoDB引擎读取id=1这一行的数据,InnoDB引擎直接用树查找主键id=1那条数据,如果数据所在页直接在内存中,那么直接返回,否则先从磁盘读取到缓存中再返回;

  • 执行器获得数据后,执行num = num + 1,再调用InnoDB引擎接口写入新的数据;
  • InnoDB引擎将新的数据更新到内存中,再将这个更新操作记录到redo log中,此时redo log日志处于prepare状态,并通知执行器已就绪,随时可以提交事务;
  • MySQL执行器写入binlog日志并持久化到磁盘,并调用InnoDB引擎的事务提交接口,InnoDB引擎将刚刚写入的redo log日志的状态改为commit状态,至此,事务完成。

从上面的流程我们可以看到redo log的写入分为两步:prepare、commit,这就是所谓的两阶段提交
而且两阶段提交一定是成功的写入了两个日志文件:redo log & binlog,只有这样事务才能提交,数据才能满足一致性原则

◆ redo log

概念:重做日志用来实现事务持久性,主要有两部分文件组成,重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者是在磁盘中。
作用:确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启 mysql 服务的时候,根据 redo log 进行重做,从而达到事务的持久性这一特性。
内容:物理格式的日志,记录的是物理数据页面的修改的信息,其 redo log 是顺序写入 redo log file 的物理文件中去的。
redo log是InnoDB引擎特有的日志模块,记录的是:“在某个数据页上做了什么修改”。
当MySQL执行一条更新语句的时候,InnoDB引擎会把记录先写到redo log文件中,并更新内存。此时这条更新操作就算完成了,但是并没有将数据的更新持久化的磁盘,InnoDB引擎会在一个合适的时机将数据的更新操作持久化到此盘。
这就是MySQL的WAL(Write-Ahead Logging)技术!
即先写日志,再写磁盘。
InnoDB引擎里面redo log写日志的具体实现:
指定一块固定大小的磁盘空间,例如4G,并分成4个文件,从头开始写,写到末尾再回到开头继续循环写,再次从头开始写之前,需要将即将覆盖的文件内容更新到数据文件中,然后再擦出这块内容,腾出空间写入新的redo log,如此往复。
002.png

write pos:redo log写入的位置;
check point:检查点,文件擦除的位置,当write pos追赶上check point时候,此时不能写入redo log,需要先将日志文件的部分内容更新到数据文件,并擦除这批日志文件,推进check point。
MySQL中有一个参数
innodb_flush_log_at_trx_commit,默认值是1,代表着每一次的redo log都持久化到磁盘。
这样就可以保证即使数据库发生异常宕机,重启后也可以恢复之前的数据记录,这个能力有一个专有的名词:crash-safe

◆ undo log

概念:回滚日志,用来记录数据被修改前的信息。正好跟前面的重做日志进行相反操作。undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。
作用:保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读;
内容:逻辑格式的日志,在执行 undo 的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于 redo log 的。
重做日志保证了事务的持久性,保证能够在宕机后恢复事务的数据,那么另外一种情况就是事务在需要回滚的时候怎么办?这时候就是undo_log的作用了,它保证了事务的一致性。
对于undo_log来说,简单理解就是做了逆向操作。
比如insert一条数据,就对应生成delete,update语句则生成相反的更新语句,这样做到将数据修改回之前的状态。

◆ binlog

概念:binlog是MySQL server层的日志,也叫归档日志。记录了所有的DDL和DML语句(除查询语句外),以事件形式记录,是事务安全型。
作用:用于复制,在主从复制中,从库利用主库上的 binlog 进行重播,实现主从同步。用于数据库的基于时间点的还原。
内容:逻辑格式的日志,可以简单认为就是执行过的事务中的 sql 语句。但又不完全是 sql 语句这么简单,而是包括了执行的 sql 语句(增删改)反向的信息,也就意味着 delete 对应着反向的 insert;update 对应着 update 执行前后的版本的信息;insert 对应着 delete 和 insert 本身的信息。
binlog相对于redo log,属于逻辑日志,记录的是这个语句的原始逻辑。binlog日志是持续追加写入的,不存在被覆盖一说。
binlog有两种模式,statement 格式的话是记sql语句, row格式会记录行的内容,记两条,更新前和更新后。
binlog日志也有一个参数sync_binlog,默认值也是1,表示每次事务的 binlog 都持久化到磁盘。这样可以保证MySQL异常重启之后binlog日志不丢失。
主从同步也是依赖于binlog日志,所以sync_binlog一定要设置为1,否则主从同步延迟是一个问题。
003.jpg
分为三步:

  1. 1. prepare阶段
  2. 1. binlog
  3. 1. 事务commit

当数据库在第二步崩溃时候,重启后,发现redo log没有commit且binlog没有写入,那么回滚事务。
数据同步、备份恢复的时候,没有binlog日志,数据是一致的。
当在第三步崩溃的时候,重启后发现虽然事务没有提交,但是binlog已经写入,redo log处于prepare状态,那么自动提交事务。
数据同步、备份恢复的时候,有binlog日志,数据也是一致的。
004.jpg
640 (3).webp

◆ 两阶段提交的重要性

1、数据库发生crash后重启的数据恢复;
2、数据库误操作后的数据恢复;
3、主从数据库的同步(全量+增量),redis主从同步也是这个思路哦~
4、两阶段提交是一个思想,不仅仅应用于MySQL数据库,日常分布式系统开发,为了保证数据的一致性,两阶段提交也是经常使用的一种思想。
那么问题来了:
两阶段提交是如何保证数据的一致性的?

基本语法

-- 使用set语句来改变自动提交模式
SET autocommit = 0;   /*关闭*/
SET autocommit = 1;   /*开启*/

-- 注意:
--- 1.MySQL中默认是自动提交
--- 2.使用事务时应先关闭自动提交

-- 开始一个事务,标记事务的起始点
START TRANSACTION  

-- 提交一个事务给数据库
COMMIT

-- 将事务回滚,数据回到本次事务的初始状态
ROLLBACK

-- 还原MySQL数据库的自动提交
SET autocommit =1;

-- 保存点
SAVEPOINT 保存点名称 -- 设置一个事务保存点
ROLLBACK TO SAVEPOINT 保存点名称 -- 回滚到保存点
RELEASE SAVEPOINT 保存点名称 -- 删除保存点

补充


  1. 事务隔离级别为读提交时,写数据只会锁住相应的行
  2. 事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
  3. 事务隔离级别为串行化时,读写数据都会锁住整张表
  4. 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。