MySQL :: MySQL 8.0 参考手册 :: MySQL 词汇表
注意:MySQL是不支持嵌套事务的,在已经开启了一个事务的情况下,若再开启一个事务,会隐式的提交上一个事务!
在MySQL数据库中只有使用了InnoDB存储引擎的数据库或表才支持事务!
- 事务是可以提交或回滚的原子工作单元。单条或多条SQL语句都属于是一个工作单元。
- 当事务对数据库进行多次更改时,要么在提交事务时所有更改都成功,要么在回滚事务时所有更改都撤销。
事务是由 MySQL 的存储引擎来实现的,我们常见的 InnoDB 存储引擎它是支持事务的。
不过并不是所有的存储引擎都能支持事务,比如 MySQL 原生的 MyISAM 存储引擎就不支持事务,也正是这样,所以大多数 MySQL 的引擎都是用 InnoDB。
事务看起来感觉简单,但是要实现事务必须要遵守 ACID 4 个特性!
- 由InnoDB实现的数据库事务具体统称为ACID的属性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolctaion)和持久性(Durability)。
- 一致性也可以保证在集群架构下主从是一致的。
- 持久性是去保证一致性的前提,持久性:我去做任何数据的更改都会持久化到磁盘,不会因为断电而导致数据的丢失。
- RedoLog重做日志是去保证持久性的,即持久性通过RedoLog重做日志实现,因为如果你在内存里面更改了以后没有同步到磁盘,假如发生宕机,就会导致数据丢失,如果发生数据丢失,一致性就保证不了了。
- 在XA规范下,作为分布式事务的一部分的操作(有时缩写为2PC)。
- 当多个数据库参与事务时,要么所有数据库提交更改,要么所有数据库回滚更改。
MySQL :: MySQL 8.0 Reference Manual :: 13.3.1 START TRANSACTION, COMMIT, and ROLLBACK Statements
只读事务和读写事务 & begin VS start transaction:
- begin跟start transaction它的区别在于:start transaction它可以加参数,就是我在启动的时候我可以去加一些参数,比如:
transaction_characteristic:{ WITH CONSISTENT SNAPSHOT ⇒ MVCC快照 | READ WRITE ⇒ 开启一个读写事务 | READ ONLY ⇒ 开启一个只读事务,如果开启的是只读事务,那么在这个事务里面你不能进行操作 }
- 我们可以使用read only开启只读事务,开启只读事务模式之后,事务执行期间任何 insert 或者 update语句都是不允许的!
- 如果用begin去开启事务,默认就是读写事务,并且默认它不会去生成一个快照。
-- 默认是Read Write,如果参数为Read Only,则事务中不允许对表进行更改 start transaction read write; -- 业务操作,可以有多个业务操作 select * from table where id = 1; -- 当事务用Read Only修饰时,该操作无效 update table set age = age + 1 where id = 1; commit; -- 提交该事务 rollback; -- 回滚事务
在 MySQL 有两种开启事务的命令,分别是:
- 第一种:begin/start transaction 命令;
- 第二种:start transaction with consistent snapshot 命令;
这两种开启事务的命令,事务的启动时机是不同的:
- 执行了 begin/start transaction 命令后,并不代表事务启动了,只有在执行这个命令后,执行了增删查改操作的 SQL 语句,才是事务真正启动的时机;
- 执行了 start transaction with consistent snapshot 命令,就会马上启动事务。
- 我们知道,一条或多条SQL语句,都是属于一个工作单元,那为什么我们平时写SQL语句的时候,不需要手动去写start transaction或者begin来开启事务呢?
- 这是因为在我们的MySQL里面,事务是默认自动提交的,也就是说,当执行一条DML语句时,MySQL会立即隐式的提交事务。
- -- 查询是否开启自动提交事务
- select @@autocommit; -- 1
- -- autocommit是MySQL数据库中的会话变量,用于控制每个语句是否自动提交为一个单独的事务
- -- 如果autocommit的值为1或ON,则表示启动了自动提交(默认情况下),每个DML语句都都将作为一个独立的事务自动提交
- -- 如果autocommit的值为0或OFF,则表示禁用了自动提交,需要手动使用commit或rollback命令提交或回滚事务
-
- -- 用于显示当前会话中的autocommit变量的值
- show session variables like 'autocommit'; -- ON
-
- -- 用于显示全局配置中的autocommit变量的值,全局配置是MySQL数据库的整体配置设置,将影响所有连接和会话
- show global variables like 'autocommit'; -- ON
- -- 关闭自动提交事务
- set session autocommit = 0;
- set session autocommit = OFF;
MySQL :: MySQL 8.0 Reference Manual :: 13.3.2 Statements That Cannot Be Rolled Back
- 有些语句无法回滚,一般来说,这些包括DDL - 数据定义语言(data definition language, DDL)语句,例如创建或删除数据库的语句。
- 事务是保证单条或者多条SQL语句要么同时执行成功,要么全部回滚。
- 其实,InnoDB是支持回滚点的操作的,何为回滚点,就是我可以回滚部分操作、提交部分操作。
- -- 开启事务
- start transaction;
- begin;
- -- DML操作
- update table set age = age + 1 where id = 2;
- -- 设置回滚点,如果回滚回滚点,后续内容将会被回滚
- savepoint sp;
- -- DML操作
- update table set age = age + 1 where id = 1;
- -- 回滚到回滚点,id = 1的DML语句不生效,但是不代表事务结束,事务结束还需要commit或者rollback
- rollback to sp;
- -- 提交事务
- commit;
- 事务一定是可以并行的,因为MySQL服务端是允许多个客户端连接的,意味着MySQL会出现同时处理多个事务的情况,因此我们去操作语句的时候肯定会有很多的客户端。
怎样去查看当前所在的事务信息呢?
- 除了你的业务库以外,MySQL它是有系统库的,这个系统库里面有个很重要的叫information_schema,这里面会有很多统计信息,这里面有一个表叫INNODB_TRX,这个表可以去查看我当前没有提交的所有的事务信息,包括事物的id(trx_id)、事务的执行状态(trx_state)、事务开启的时间(trx_started)、trx_weight(事务的权重)、trx_isolation_level(当前事务的隔离级别)、trx_query(事务正在执行的SQL语句)、事务是否在等待锁等等。
为什么会有trx_weight权重?
- 事务的权重反映被更改的行数和被事务锁定的行数。由于事务跟事务之间有可能会死锁,形成死锁的时候我需要去回滚,为了解决死锁,InnoDB选择权重最小的事务作为"受害者"进行回滚。
-- 查看当前在InnoDB中执行的每个事务信息 select * from information_schema.INNODB_TRX;
数据量比较大的表在做修改操作的时候可以分批执行。
如果不是一个只读的事务,那么每个事务都会有一个自增的事务ID(trx_id),即每个非只读事务都有个递增的事务ID;如果是一个只读的事务(select from),它没有事务ID。
所以,我们所有的数据操作都是由事务(ID)来完成的,每行数据都会有一个隐藏的字段,叫做事务ID:TRX_ID,代表这个数据修改的最后的事务ID。
- ACID模型是一组数据库设计原则,强调对业务数据和关键任务应用程序非常重要的可靠性方面。
- MySQL包含了一些组件,比如紧密遵循ACID模型的InnoDB存储引擎,这样在软件崩溃和硬件故障等异常情况下,数据不会被破坏,结果也不会被扭曲。
下面将讨论MySQL的特性,特别是InnoDB存储引擎如何与ACID模型的类别进行交互:
事务看起来感觉简单,但是要实现事务必须要遵守 4 个特性,分别如下:
1. Atomicity - 原子性
- ACID模型的原子性,主要涉及到的是InnoDB存储引擎的事务。
- 原子性是指事务是一个不可分割的工作单元,事务中的操作要么同时执行成功,要么全部回滚,即事务是保证单条或多条SQL语句要么同时执行成功,要么全部回滚。
- 相关的MySQL功能包括:自动提交设置、COMMIT语句、ROLLBACK语句。
- 原子性是依靠UndoLog回滚日志来实现的,每次对数据做修改、删除或插入的操作都会生成一条UndoLog日志来记录操作之前的数据状态,使用ROLLBACK的语句能够将所有执行成功的SQL语句产生的效果撤回。
原子性 ⇒ 如果让你来恢复数据的话,你会怎么来恢复?
- 我执行了一条insert语句,只需要再执行一条delete语句就可以把它恢复过去;
- 如果执行我执行了一条delete语句,我只需要再执行一条insert语句就可以把它恢复回去;
- 如果执行了一条update语句,我可以反向的执行一条update语句从而把它恢复回去。
思考:后台与之相反的这些SQL语句由谁来生成呢?如果由MySQL Server来做这件事情就太麻烦了
- 保存修改之前的记录 ⇒ 类似于快照:保存的是之前的历史记录数据,所以在undolog里面存储的是历史记录的信息。
- 回滚日志先于数据持久化到磁盘或硬盘上,回滚就是逆向操作。
- 注意:这里的原子性跟并发的原子性是有一定区别的,并发的原子性是我在执行一个操作的时候,其它线程就不能来操作相关的资源,必须等我释放这些资源后才能来操作。
2. Consistency - 一致性
- 事务完成时,必须使所有的数据都保持一致状态!
- 所谓一致性,就是保证数据的一致,也就是保证数据不要丢失,不会因为突然的断电等导致数据与我想要的数据不一致,以保护数据不受崩溃的影响。
- 相关的MySQL功能体现:DoubleWrite-Buffer(双写缓冲区,保证内存跟磁盘之间同步的数据安全)、基于RedoLog的数据恢复。
- 一致性则是由RedoLog和UndoLog共同来保证的!
- 数据一致性是我们最根本的追求!
- 我们在进行数据操作的时候,所有的数据最终都要落盘,即我们的数据是要写在磁盘里面的!
3. Isolation - 隔离性 - 各个事务之间相互独立且互不影响:锁 + MVCC
- 隔离性:数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行!
- ACID模型的隔离性主要涉及InnoDB事务,特别是应用于每个事务的隔离级别。
- 事务的隔离性是指多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其它事务的操作数据所干扰,多个并发事务之间要隔离。
- 相关的MySQL功能包括:事务的隔离级别以及InnoDB锁的底层细节。
- 隔离性通过锁机制(悲观锁)实现,当事务操作数据的时候加锁,让事务执行前后看到的数据是一致的,也就是让并行执行事务和串行执行事务产生的效果一样,另外就是利用MVCC(多版本并发控制)实现的快照读。MVCC => 乐观锁~!
4. Durability - 持久性
- ACID模型的持久性涉及MySQL软件特性与特定硬件配置的交互。
- 持久性:事务一旦提交或者回滚,它对数据库当中的数据的改变就是永久的!
- 持久性,其实就是我的数据要尽可能地同步到磁盘做持久化保存,防止数据异常丢失,一个事务一旦被提交,它对数据库中的数据的改变就是永久性的,即使接下来数据库发生故障也不会对其有影响。
- 相关的MySQL功能包括:InnoDB的DoubleWrite-Buffer - 双写缓冲区、RedoLog的同步机制设置(innodb_flush_log_at_trx_commit变量- 有0、1、2,默认值为1,设置为1才可以做持久化保存,如果设置为0跟2,可能会有数据丢失) 、binlog的同步机制(sync_binlog变量)、独立表空间或系统表空间设置(innodb_file_per_table变量,默认开启为独立表空间,如果关闭为系统表空间)。
- 持久性是通过RedoLog重做日志实现的,RedoLog记录的是对数据库的操作,MySQL先把存放在磁盘上的数据加载到内存中,在内存中做修改再刷回到磁盘,RedoLog使得再事务提交时将数据刷回磁盘。
InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?
- 持久性是通过 redo log (重做日志)来保证的;
- 原子性是通过 undo log(回滚日志) 来保证的;
- 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
- 一致性则是通过持久性+原子性+隔离性来保证;
WAL - Write Ahead Log:预写日志,顾名思义,写磁盘数据之前先写日志,为什么?
- WAL技术指的是,MySQL的写操作并不是立刻写到磁盘上的,而是先写日志,然后再合适的时间再写到磁盘上!
- 因为表数据在磁盘上面是随机分布的,因为与磁盘IO是随机读写的,而日志文件是顺序读写的,效率更高,所以先写日志,假如说异步写磁盘失败了,还可以根据日志把丢失的修改数据补回来,减少数据丢失的可能性,redo log就是预写日志的机制!
两阶段提交的问题
- 我们知道redolog是属于InnoDB的,而binlog是属于MySQL Server的,因此在MySQL的InnoDB存储引擎当中,在开启binlog的情况下,两个日志文件是有可能同时存在的,当两种日志文件同时存在时,是先写binlog呢还是先写redolog呢?’
- 其实,不管你是先写binlog还是先写redolog它都有问题。
假设我们执行一条SQL语句做update操作:
- 假设redolog写成功了,也就是在将 redo log 刷入磁盘之后,但是来得及写入binlog,系统崩了或者系统突然断电了。在MySQL重启后,可以根据redolog来进行数据恢复,但是,binlog由于没有写成功,所以它是没有记录下来这次变更的,这也就意味着,主从同步的时候,是缺少了一条SQL的,导致主从两台机器数据不一致!
- 如果是先写入binlog成功了,即在将 bin log 刷入到磁盘之后,但是没来得及写入redolog,系统崩了或MySQL突然宕机了,在MySQL重启后,崩溃恢复的时候由于redolog没有写成功,所以数据库记录的数据还是旧值,但是因为binlog已经写入成功了,所以在做主从同步的时候,就会把新值同步到从库或从节点,就导致了主从库之间的数据不一致!
- 因此才引入了两阶段提交,事务提交后,redo log 和 bin log都要持久化到磁盘,如果不引入两阶段提交的话,在 bin log 和 redo log没办法保证一致性的情况(即出现半成功的状态),就会导致主从库之间的数据不一致!这是因为 redo log 影响主库的数据,而bin log影响从库的数据,所以 redo log 和 bin log必须保持一致才能保证主从数据的一致性!
- 所以MySQL为了避免 redo log 和 bin log 这两份日志文件之间的逻辑不一致的问题,就引入了MySQL事务的两阶段提交,就是在更新的过程中,来整体的控制 redo log 和 bin log 的一致性写入的一种手段。
- 两阶段提交其实是分布式事务的一致性协议,它可以保证多个逻辑操作要不全部成功,要么全部失败,不会出现半成功的状态。
两阶段提交的过程
两阶段提交把单个事务的提交拆分成了2个阶段,就是将 redo log的写入拆分成了两个步骤,分别是 Prpare-准备阶段 和 Commit-提交阶段,中间再穿插写入 bin log。
- 注意:不要把Commit提交阶段和commit语句混淆了,commit语句执行的时候,会包含Commit提交阶段!
Prepare阶段(预提交) - 先写 redo log
- 这个阶段SQL已经成功执行并生成 redo log,同时将 redo log对应的事务状态设置为prepare。 => 注意:处于prepare状态的 redo log 是不能够直接使用的~!
BinLog持久化到磁盘(sync_binlog = 1的作用) - 再写 bin log
- binlog提交,通过write()将binlog内存日志数据写入到文件缓冲区;
- 再通过fsync()将binlog从文件缓冲区永久写入到磁盘;
Commit阶段 - 再写 redo log
- 调用执行引擎内部的提交事务接口,将 redo log 状态设置为 commit,说白了就是给之前写入的数据打上commit标识,事物两阶段提交完成。
- 只要 bin log 写磁盘成功,就算 redo log的状态还是prepare也没有关系,一样会被认为事务已经执行成功。‘
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- write()和fsync(刷盘)是与文件系统和磁盘IO相关的两个不同的操作。
- write()操作是将数据写入文件的缓冲区,也就意味着write操作完成后,并不一定立即将数据持久化导磁盘上,而是将数据暂时存储在内存中。
- fsync(刷盘)用于强制将文件的修改持久化到磁盘上,它通常与write()操作配合使用,以确保文件的修改在fsync()操作完成之后被写入磁盘。
两阶段提交是如何保证主从数据一致性的?
- 情况一:假设我在第一次写完 redo log 之后断电了,即一阶段提交之后,处于prepare状态的时候崩溃了,断电之后我在进行数据恢复或崩溃恢复的时候,我读的是 redo log,但是此时 redo log 是处于prepare状态,处于prepare状态的 redo log 是不能够直接使用的,所以我要根据相应的事务id去 bin log 里面查询,如果能查到匹配的记录,意味着这条数据是有效数据;如果在 bin log 里面没有找到与之匹配的记录,意味着这是一条无效数据,直接丢弃,回滚事务即可,这样主从是一致的,就都没有执行整个事务。
- 情况二:一阶段提交成功,写完 bin log 之后崩溃了,此时,redo log 处于prepare状态,bin log已写入,这时候检查 bin log 中的事务是否存在并且完整,如果存在且完整,则直接提交事务,更改 redo log为commit状态;如果不存在或者不完整,则回滚事务。
- 情况三:假设 redo log 处于commit状态的时候崩溃了,此时数据库重启后,先检查 bin log 中的事务是否存在并且完整,如果存在且完整,则直接提交事务;如果不存在或者不完整,则回滚事务。
- 由此可见,两阶段提交能够确保数据的一致性!
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- redo log 可以在事务没提交之前持久化到磁盘,但是 bin log 必须在事务提交之后,才可以持久化到磁盘!
- redo log 是固定大小的,redo log 是一个循环写的过程,如果写满了,同时你很多数据还没有落盘,那么就会紧急的把之前没有落盘的数据进行落盘,腾出一段儿空间来来继续写!
如何判断binlog和redolog达成一致了?
- 当MySQL写完 redo log 并将它标记为prepare状态时,会在redolog中记录一个XID,它全局唯一的标识着这个事务,接着mysql就会对应binlog并且会直接将其刷新到磁盘中,binlog记录的row格式结束的位置上也有一个XID,只要这个XID和redolog中记录的XID是一致的,MySQL就会认为binlog和redolog逻辑上是一致的!
- 事务一定是可以并行的,因为MySQL服务端是允许多个客户端连接的,意味着MySQL会出现同时处理多个事务的情况,因此我们去操作语句的时候肯定会有很多的客户端。
- 既然事务是可以并行或并发的时候,那么就一定会有数据一致性问题,因为两个事务可能同时改相关的数据,这个时候我不知道应该听谁的,所以会有数据一致性问题,有可能导致你改的数据跟你要的数据是不一致的。
如果事务并发了,会产生或导致哪些数据一致性问题?
脏读:
- 两个并发事务,一个在读,另一个在改,我能够读到你根本还没有提交的数据,但是这些数据可能是会回滚的。
- 只要是一个事务读到了其它事务修改过但还没有提交的数据。就叫做脏读。
- 因为你读到的数据是还没有提交的,所以它随时可能发生回滚操作,一旦发生回滚,以为着你读到的数据就是无效的数据或过期的数据,这种现象就被称之为脏读。
不可重复读:
- 后面的或第二次读取跟第一次读取的结果不一致,这种现象就被称为不可重复读,不一致是因为你的这个结果由其它事务进行了更改或删除,并且提交了。
- 在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,这意味着发生了不可重复读现象。
幻读
- 幻,幻影 => 幻影行,就是一行数据它突然就出现了,原来是没有的,但是它突然出现了。在开启事务之后,读到了其他事务新添加的新数据!即多次执行相同的查询却在不同时间产生了不同的结果集。
后面的读取或第二次读取跟第一次读取的结果不一致,不一致时因为你的这个结果由其它事务进行了添加,并且提交了。 例如:如果一个SELECT执行了两次,但是第二次返回的行第一次没有返回,那么这一行就是"幻影"行。 思考:为什么加update再次select后就出现了幻读????
- 首先脏读肯定是有危害的,因为你读到的数据如果发生回滚了,那么你读到的数据就是无效数据或过期数据。
- 但是不可重复读跟幻读问题,根据不同的业务场景来,大部分的业务场景来讲,我们是能够接受的,但是,它不符合我们数据库涉及原则ACID中的I - 隔离性原则,因为事务跟事务之间进行了相关影响,我们应该在让数据在一个事务中进行了读取后,在没有提交事务之前,不能被其它事务进行更改。
这三个现象的严重性排序如下:
- 接下来,我们来看一下不同的隔离级别解决了哪些问题,哪些又有数据一致性问题。
- 解决了数据一致性问题,就一定会牺牲性能。
- 事务隔离是数据库处理的基础之一。“隔离”是缩写词ACID中的I;
- 隔离级别是在多个事务同时进行更改和执行查询时,微调性能与结果的可靠性、一致性和可重复性之间的平衡的设置。简单一句话,就是多个事务并发操作相同的表数据的时候,你去保证性能还是优先保证数据的一致性。
- SQL标准提出了四种隔离界别来规避这些现象,隔离级别越高,性能效率就越低。
- InnoDB提供了SQL:1992标准描述的所有四种事务隔离级别:读未提交(Read uncommitted)、读已提交(Read committed)、可重复读(Repeatable Read)和可序列化(Serializable)。
- InnoDB的默认隔离级别是RR可重复读取(REPEATABLE READ)。
- 既然隔离级别是为了性能与一致性的一个平衡设置,所以,有些隔离级别是追求性能,有些隔离级别是为了去追求数据一致性,不同的隔离级别它们追求的东西不一样。
- 那么Read uncommitted到Serializable,越往上,数据一致性越强,但是性能越来越低!
- 不同的隔离级别会带来不一样的数据一致性问题。
- ReadUncommitted - 读未提交:就是我能读到你没有提交的数据,没有解决任何问题,可能发生脏读、不可重复读和幻读现象,但是性能最快。RU隔离级别说的简单一点儿就是一个事务可以读取其它{未提交的事务}修改的数据!
- ReadCommitted - 读已提交:RC读已提交说的是当前事务只能读到别的事务已经提交的数据!只有你提交了,我才能读。解决了脏读的问题,不可能发生脏读现象,但是可能有不可重复读和幻读问题!
- Repeatable Read - 可重复读(默认):RR可重复读它的意思是同一个事务中发出同一个select语句两次或更多次,那么产生的结果数据集总是相同的!RR解决了不可重复读的问题,可能有/发生幻读现象,但是不可能发生脏读和不可重复读。Repeatable Read在MySQL里面只有在InnoDB存储引擎当中才会有,Repeatable Read是InnoDB存储引擎中解决幻读问题的一种隔离级别,RR隔离级别在InnoDB里面也解决了幻读问题。MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了)!
- Serializable - 串行化:事务串行化一定解决了所有的问题,通过加读写锁的方式使所有操作串行化(包括增删改查),根本就不会有并发问题,所以也不会有相关数据一致性问题,但是一般不会用,因为性能很差很差。串行化会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成后,才能继续执行!
- SERIALIZABLE比REPEATABLE READ强制执行更严格的规则,它主要用于特殊的情况,例如XA事务,以及并发和死锁问题的故障诊断。
- MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生(注意是很大程度避免,并不是彻底避免),所以 MySQL 并不会使用「串行化」隔离级别来避免幻读现象的发生,因为使用「串行化」隔离级别会影响性能,所有事务串行执行。
- 在Repeatable Read隔离级别下,InnoDB使用了多版本并发控制(MVCC)机制,通过在读取数据时对事务进行加锁,保证了读取数据时的一致性,从而解决了幻读问题,其它一些不支持事务的存储引擎,如MyISAM都不支持事务,更不会有什么幻读脏读不可重复读这些并发问题,因此也更不支持什么类似RR之类的隔离级别。
- 注意:隔离级别是SQL标准,只有支持事务的存储引擎才有事务隔离级别!
按隔离水平高低排序如下:
针对不同的隔离级别,并发事务时可能发生的现象也会不同。
- InnoDB使用不同的锁策略支持这里描述的每一种事务隔离级别。
- 用户可以通过默认的可重复读取级别来确保操作的高度一致性,在关键数据上的操作符合ACID要求非常重要。或者,您可以放宽已提交读甚至未提交读的一致性规则,在批量报告等情况下,精确的一致性和可重复的结果比最小化锁定的开销更重要。
- 用户可以更改单个会话或使用SET TRANSACTION语句的所有后续连接的隔离级别。要为所有连接设置服务器的默认隔离级别。
用户可以更改或设置Session会话或者全局Global的隔离级别:
- SET [Global | Session] - 作用域 TRANSACTION
- SESSION和GLOBAL关键字用来指定修改的事务的隔离级别的范围;
- SESSION:表示修改的事务隔离界别将应用于当前session(当前CMD窗口)内的所有事务;
- GLOBAL:表示修改的事务隔离级别将应用于所有session(全局)中的所有事务,且当前已经存在的session不受影响!
- 如果省略SESSION和GLOBAL,表示修改的事务隔离级别将应用于当前Session内的下一个还未开始的事务!
-- 修改当前会话的隔离级别为读未提交 set session transaction isolation level read uncommitted; -- 修改当前会话的隔离级别为读已提交 set session transaction isolation level read committed; -- 修改当前会话的隔离级别为可重复读 set session transaction isolation level repeatable read; -- 修改当前会话的隔离级别为串行化 set session transaction isolation level serializable; -- 查询全局隔离级别 show global variables like '%isolation%'; -- 查询当前会话的隔离级别 show session variables like '%isolation%'; -- REPEATABLE-READ -- 查询当前会话自动提交是否开启 show session variables like 'autocommit';
- 对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
- 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
- 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。
MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了,详见这篇文章 (opens new window)),根据不同的查询方式,分别提出了两种解决幻读的方案:
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
- 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
- 快照读是MVCC实现的基础,而当前读是悲观锁实现的基础!
- MVCC全称是Multi-Version Concurrency Control - 多版本并发控制是指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读的功能!是指在数据库中为了实现高并发的数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本,实现对数据库的并发访问!
- MVCC它的主要作用就是在快照读的时候来决定我们读取的到底是哪一个历史记录版本!
- 多版本指的是版本链,并发指的是并发场景下多个线程对同一行数据进行读写,控制指的是用版本链控制事务读取的结果是哪个版本的数据。
- MVCC最大的好处是读不加锁,读写不冲突,在读多写少的系统应用中,读写不冲突是非常重要的,极大的提升系统的并发性能,这也是为什么现阶段几乎所有的关系型数据库都支持MVCC的原因,不过目前MVCC只在Read Commited和Repeatable Read两种隔离级别下工作。
- MVCC主要是为了提高并发的读写性能,不用加锁就能让多个事务并发读写!
MVCC实现原理
- MVCC的实现是由三部分组成的,分别是 数据库行记录中的三个隐藏或隐式字段 + undo log版本链 + ReadView!
- undo log中保存了历史快照,而ReadView用来判断具体哪一个快照是可见的~!
MVCC基本概念
当前读 & 快照读
当前读:表示读取的数据都是最新的数据,即读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁!比如加锁的select或者对数据进行增删改都会进行当前读:、select...lock in share mode(共享锁),select...for update、update、insert、delete(排它锁),这些操作全部都会触发当前读。
- 快照读:读取的数据不一定是最新的数据,就是读取的是快照数据,有可能是历史版本的数据,普通的不加锁的select,就是单纯的select不加锁的语句就是快照读。不加锁,是非阻塞读!
- 快照读是MVCC实现的基础,而当前读是悲观锁实现的基础!
- 在MySQL中,只有Read Committed和Repeatable Read这两种事务隔离级别才会使用快照读!
- Read Committed:每次select,都生成一个快照读!
- Repeatable Read:开启事务后的第一个select语句才是快照读的地方,后面执行相同的select语句都是从快照中获取数据,可能不是当前的最新数据,这样也就保证了可重复读!
- Serializable - 串行化:快照读会退化为当前读!
什么时候会有快照读的场景?
- 可重复读啊!因为不可重复读读取到的不就是最新的数据吗?
数据库的并发场景有哪些?
- 读读:不存在任何问题,不需要并发控制。
- 读写:会有数据安全问题,可能会遇到脏读、幻读、不可重复读 ⇒ MVCC重点解决的是并发读写的问题
- 写写:会有数据安全问题,可能会遇到更新丢失问题!写写并发这种情况比较常用的就是通过加锁的方式实现!
在MySQL里面,有两种不同的解决方案,这两种解决方案可以配合使用:
- 用在只读场景。
- 快照读就是使用快照信息显示基于某个时间点的查询结果~!像我们常用的普通的select语句不加锁的情况下就是快照读~!
- 快照读比较适合对于数据一致性要求不是特别高且追求极致性能的业务场景。
- 一致性读取意味着InnoDB使用多版本来为查询提供数据库在某个时间点的快照。查询会看到在该时间点之前提交的事务所做的更改,而不会看到后面或未提交的事务所做的更改。
所以关键在于快照!- Snapshot - 快照:在某个时间节点我会去记录某些数据提供给后面使用!快照即记录数据的历史版本,每行记录可能存在多个历史版本(多版本技术)。相当于就是一张照片,定格某一时刻的风景!
某个时间节点:RC和RR生成ReadView读示图(快照)的时机?
- 可以在start transaction开启事务的时候可以指定生成快照!
- 在MySQL中,只有在事务隔离级别为Read Committed(读已提交)和RepeatableRead(可重复读)下,InnoDB才会使用非锁定一致性读取或快照读。
- 对于RC和RR隔离级别的事务来说,它们是通过ReadView来实现的,它们的区别在于创建ReadView的时机不同:
- 如果隔离级别是读已提交Read Committed,则是在每次SQL语句执行前都会重新生成一个最新的ReadView,即每次select都会生成一个新的ReadView,因此每次slelect查询总是读取行记录的最新版本!这也就意味着,事务期间多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另一个事务修改了该记录,并提交了事务!
- 如果隔离级别是可重复读Repeatable Read,则是在[启动事务时] - 即第一次执行SQL语句时,第一次select查询时会生成一个ReadView,并且整个事务期间都在用这个ReadView,即后续所有的快照读都是用的同一个ReadView快照,这样就保证了在事务执行期间读到的数据都是事务启动前的数据,所以就不会发生不可重复读的问题了!只有在本事务中对数据进行更改才会更新快照~!
- 这两个隔离级别实现是通过 事务的ReadView里的字段 和 记录中的两个隐藏列 的比对,来控制并发事务访问同一个记录时的行为,这就叫MVCC(多版本并发控制)。
Snapshot快照到底去保存哪些数据呢?
- 保存的是一个ReadView的数据结构,ReadView说白了就是一个数据快照,是在某个时间节点去创建的一个Class结构。
Read View 在 MVCC 里如何工作的?
我们需要了解两个知识:
- Read View 中四个字段作用;
- 聚簇索引记录中两个跟事务有关的隐藏列;
聚簇索引记录中跟事务相关的行隐藏字段或隐藏列,即行记录的三个隐藏字段:
对于InnoDB存储引擎的数据库表,当插入数据的时候,除了我们显式定义的字段之外,MySQL会生成一些隐藏字段,对用户不可见,它的聚簇索引记录中都包含下面几个隐藏列:
- DB_ROW_ID:隐藏主键,并不是每一张表都会生成,只有当数据库表没有主键或非空(的)唯一索引时,InnoDB才将会生成该隐藏字段,并且基于该递增字段来去建立主键,大小为6字节。
- DB_TRX_ID:翻译过来就是事务ID,更新这行数据的事务ID或最新修改事务ID,记录插入这条记录或者最后一次修改该记录的事务ID的值。事务ID是自增的!
- DB_ROLL_PTR:Roll-Pointer{回滚指针},被改动前的 undo log日志指针,指向该记录上一个历史版本状态的数据,通过它可以来找到该记录修改前的信息,用于配合undo log,指向上一个版本。
MySQL中的事务ID是系统生成的,用户不可控,同时事务ID是自增的!
undo log - 回滚日志
- 回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志!
- 当insert的时候,产生的undo log日志只再回滚时需要,在事务提交后,可被立即删除!
- 而当update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会被立即删除。
为什么需要 undo log?
- 我们在执行一条"增删改"语句的时候,虽然没有输入begin开启事务和commit提交事务,但是MySQL会隐式开启事务来执行"增删改"语句,执行完会自动提交事务,这样就保证了执行完"增删改"语句后,我们可以及时在数据库表中看到"增删改"的结果了。
- 执行一条语句是否自动提交事务,是由 autocommit 参数决定的,默认开启,所以,执行一条update语句也是会使用事务的。
- 如果一个事务在执行过程中,在还没有提交事务之前,MySQL发生了崩溃,就需要依靠undo log回滚到事务之前的数据,在事务没提交之前,MySQL会记录更新前的数据到undo log回滚日志文件里面,当事务回滚时,就需要利用 undo log来进行回滚!
- undo log回滚日志,它保证了事务的ACID特性中的原子性(Atomicity)!
- undo log表示在进行insert、delete、update操作的时候生成的方便回滚的日志~!
一条记录的每一次更新操作产生的undo log格式都有一个DB_TRX_ID - 事务id 和 DB_ROLL_PTR - 回滚指针:
- 通过DB_TRX_ID可以知道该记录是被哪个事务修改的;
- 通过DB_ROLL_PTR指针可以将这些undo log串成一个链表,这个链表就被称为版本链!
undo log {多}版本链如下图所示:
- 不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链表,undo log最终会形成一个链表,链首(链表的头部)的是最新(修改)的历史数据,链尾(链表的尾部)的是最旧的历史数据。
- 在undo log的版本链当中,所记录的就是当前该记录它的所有的历史记录版本!
如上图,可以看到,undo log里面不仅存放着数据更新前的记录,还记录着DB_ROW_ID - 隐藏主键,DB_TRX_ID - 事务ID、DB_ROLL_PTR - 回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本,其中,事务ID每次递增,回滚指针第一次如果是insert语句的话(如果是第一次插入),回滚指针为NULL,事务ID为1,第二次update之后undo log的回滚指针就会指向刚刚上一条历史版本状态数据的undo log日志,以此类推,就会形成一条undo log的回滚链,方便找到该条记录的历史版本!
undo log会无限膨胀吗?
- undo log不会无限增加,后台会有一个单独的线程叫做purge,会定期进行清理!
另外,undo log 还有一个作用,通过 ReadView + undo log 实现 MVCC(多版本并发控制)。
当我们在进行查询的时候,到底应该返回哪一个历史版本记录呢?
- 这个实际上不是由undo log的版本链来控制的,具体要返回哪一个版本是由ReadView来控制的!
ReadView(读视图)机制
- ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护着系统当前活跃的事务(未提交的事务)id!
- ReadView称之为读视图,指的是事务在进行快照读操作的时候产生的读视图,ReadView中存储的并不是真实的数据,而是存储的是事务id的信息!
- ReadView是在某个事务里面去生成的! => 事务的ReadView
我们知道快照读在读取的时候它并不是读取最新的记录,它读取的有可能是历史版本的记录,而undo log日志所形成的版本链恰好记录的就是历史版本的记录,我们快照读在执行的时候,到底读取哪一个历史记录呢?
- 其实是由ReadView来决定的!因为在ReadView当中它会记录并维护系统当前系统活跃的事务id(未提交的事务id)!
ReadView结构的4个标志位或四个重要的核心字段:
- m_low_limit_id:代表创建ReadView时,当前数据库中应该给下一个事务即将要分配的事务ID(每个非只读事务都有个递增的事务ID),即系统尚未分配的下一个事务id => 预分配事务的ID,即全局事务中最大的事务id值 + 1(因为事务ID是自增的)。也称高水位线。
- m_up_limit_id: 指的是在创建ReadView时,记录当前数据库中所有没有提交的事务ID(所有存活的事务ID)里面最小的事务ID => 最小的活跃事务ID,其实就是m_ids里面的最小值。也称低水位线
- m_ids:当前活跃的事务ID集合,1是一个数组,记录的是在创建ReadView时,当前数据库中所有活跃的事务ID集合,即所有没有提交的事务ID的集合(所有存活的事务ID列表,即启动了但还没有提交的事务)。
- m_creator_trx_id:创建该ReadView的事务ID,即当前的事务ID => ReadView创建者的事务ID。
当我收集到这四个字段信息之后,它如何来判断快照读在读取时读取的是哪一个历史版本的记录呢?
- 可见性算法:类似于高水位、低水位这样的描述。
- 而快照读的时候MVCC提取数据的依据实际上就是依据于ReadView的四个核心字段!
- 我们知道行记录的隐藏字段之一 - DB_TRX_ID就代表当前undo log版本链对应的事务ID!
- 在获取历史版本数据的时候,到底获取的是哪一个历史版本,就是拿当前事务的ID和刚才ReadView当中的四个属性进行比对!
- 而在ReadView当中就规定了版本链数据的访问规则!
可见性算法(版本链数据的访问规则):来去判断一个事务去访问记录时,该版本的记录是否对当前事务可见?
- 根据可见性算法里面的判断原则我们来进行一个对比。
在创建ReadView后,我们可以将记录中的DB_TRX_ID划分成这三种情况:
- 首先比较记录中的DB_TRX_ID == m_creator_trx_id - 创建该ReadView的事务ID,此时代表可以访问该版本对应的记录。Why?原因是因为如果该条件成立,就说明数据是当前这个事务更改的,那我刚刚更新的这个记录,我肯定是可以读取出来的!
- 其次比较如果记录中的DB_TRX_ID < ReadView中的m_up_limit_id - 最小的活跃事务ID,如果小于,则表示这个版本的记录是在创建ReadView前,已经提交的事务生成的,所以该版本的记录对当前事务可见,即当前事务能看到DB_TRX_ID所在的记录!
- 接下来判断记录中的DB_TRX_ID >= ReadView中的m_low_limit_id - 预分配事务的ID,如果当前事务的ID比预分配事务的ID还要大,则代表这个版本所在的记录是在ReadView生成后才启动的事务生成的,所以该版本的记录对当前事务肯定不可见!
- m_up_limit_id <= DB_TRX_ID < m_low_limit_id,判断记录的DB_TRX_ID值是否在活跃事务列表 m_ids 中,如果在,则代表在ReadView生成时刻,这个事务还是活跃状态,还没有commit提交,所以该版本的记录对当前事务也是不可见;如果记录的DB_TRX_ID值不在活跃事务列表 m_ids 中,则说明这个事务在ReadView生成之前就已经commit提交了,所以该版本的记录对当前事务是可见的!
- 心得:在判断的时候先去看最后一次修改该记录的事务ID的值,即DB_TRX_ID!
- 可见性算法是规定好的,不会变的东西。
- 这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
- 在MySQL中,只有在事务隔离级别为Read Committed(读已提交)和Repeatable Read(可重复读)下,InnoDB才会使用快照读!
不同的隔离级别,生成ReadView的时机也不同:
- READ COMMITTED:在事务中每一次执行快照读时生成ReadView!
- REPEATABLE READ:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView!
MVCC案例引出:
案例一:很重要
- 我们都知道InnoDB默认的事务隔离级别是Repeatable Read - RR(可重复读),解决了在并发事务下不可重复读的问题,也就是我在我当前的这个事务窗口是读不到其他事务窗口update修改并已经commit提交的数据,保证了事务与事务之间的隔离性!
- 但是,通过刚才的案例,我们看到了在RR隔离级别下,关系隐式自动提交事务(set autocommit = 0)后,开启手动提交事务,并且开启了两个不同的窗口,在另一个窗口修改表中数据并提交,竟然在该窗口中select读到了另一个窗口修改并已经提交的数据,不是说在RR隔离级别下解决了不可重复读的问题吗???
来判断事务1第一次select之后能否读取到更新之后的数据?
能!
结论:DB_TRX_ID不在活跃事务列表m_ids中,则说明这个事务在ReadView生成之前就已经commit提交了,所以该版本的记录对当前事务是可见的!
- 事务1执行了一次select,会生成一个ReadView。
案例二:很重要
来判断事务1第二次select之后能否读取到更新之后的数据?
- 不能!
- 结论:DB_TRX_ID在活跃事务列表m_ids中,则代表在ReadView生成时刻,这个事务还是活跃状态,还没有commit提交,所以该版本的记录对当前事务不可见!
- 虽然事务1里面执行了两次select语句,但由于InnoDB默认隔离级别时RR,如果隔离级别是RR,则是在第一次select查询时会生成一个ReadView,并且整个事务期间都在用这个ReadView,因此虽然事务1执行了两次select语句,但是只生成一个ReadView!
- 因为可见性算法是固定死的,不会发生任何改变,唯一可能改变的就是ReadView的值!
为什么RR隔离级别可以解决不可重复读的问题?
- MVCC是MySQL底层的实现机制,这个机制是由MySQL的Server来主动来控制的,用户是不可干预的,但是作为用户是需要控制数据是否可见的,因此必须要给到用户一个选项或者配置来控制数据是否可见,那么最关键的点是在于ReadView的生成时刻,也就是说能否读取到修改之后的数据取决于ReadView是在什么时间生成的。
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同:
- RC:读已提交隔离级别是每次进行快照读操作的时候都会生成ReadView。
- RR:可重复读隔离级别只有在事务第一次进行快照读的时候会生成ReadView,之后执行快照读操作的时候都会沿用第一次生成的ReadView视图。
- RR看不到已经修改的数据,它的前提是在你修改之前你已经执行过一次快照读了,如果没有进行过快照读!如果你之前从来没有执行过快照读,那么你依然可以看到更新之后的结果或者数据!
- RR隔离级别能够解决不可重复读的最关键点也在于ReadView没有变!RR隔离级别能够解决不可重复读的根本原因就在于沿用了第一次快照读产生的ReadView!
- 这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。
- 在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的。而 select .. for update 语句就不是快照读了,而是当前读了,也就是每次读都是拿到最新版本的数据,但是它会对读到的记录加上 next-key lock 锁。
- Oracle默认隔离级别就是RC,MySQL默认隔离级别就是RR。
提示:与事务的隔离级别无关!!!
幻读产生的根本原因就在于不可重复读的问题。为了解决不可重复读的问题,所以引入了幻读问题!
- 前面两次select都是快照读,读的都是历史数据;而我们的update是当前读,读的是最新数据,所以有了幻读的问题!
- 所以幻读产生的根本原因在于当前读和快照读一起使用,也就是只有当前读和快照读一起使用的时候,才有幻读的问题!如果只有快照读,那么永远不会发生幻读的问题!