MySQL高级 之 事务(ACID特性 与 隔离级别)

Posted 走慢一点点

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL高级 之 事务(ACID特性 与 隔离级别)相关的知识,希望对你有一定的参考价值。

什么是事务?

事务就是DBMS当中用户程序的任何一次执行,事务是DBMS能看到的基本修改单元。

事务是指对系统进行的一组操作,为了保证系统的完整性,事务需要具有ACID特性。即原子性(atomicity),一致性(consistency),隔离性(isolation),持久性(durability)。

mysql事务实现机制

MySQL提供了两种事务型的存储引擎:InnoDB和NDB Cluster(主要用于MySQL Cluster 分布式集群环境)。另外还有一些第三方存储引擎也支持事务,比较知名的包括XtraDB和PBXT。下面以InnoDB来说明。

事务日志

MySQL会最大程度的使用缓存机制来提高数据库的访问效率,但是万一数据库发生断电,因为缓存的数据没有写入磁盘,导致缓存在内存中的数据丢失而导致数据不一致怎么办?

InnoDB主要是通过事务日志实现ACID特性,事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。

事务日志采用追加的方式,因此写日志的操作是磁盘上一小块区域的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头。所以事务日志的方式相对来说要快得多。事务日志持久以后,内存中被修改的数据在后台可以慢慢的刷回磁盘。目前大多数存储引擎是这样实现的,我们称之为预写式日志,即修改数据需要写两次磁盘。

Redo、Undo、Checkpoint

事务日志包括重做日志redo和回滚日志undo

redo记录的是已经全部完成的事务,就是执行了commit的事务,记录文件是ib_logfile0 、ib_logfile1。

Undo记录的是已部分完成并且写入硬盘的未完成的事务,默认情况下回滚日志是记录在表空间中的(共享表空间或者独享表空间)。

一般情况下,mysql在崩溃之后,重启服务,innodb通过回滚日志undo将所有已完成并写入磁盘的未完成事务进行rollback,然后将redo中的事务全部重新执行一遍即可恢复数据,但是随着redo的量增加,每次从redo的第一条开始恢复就会浪费长的时间,所以引入了checkpoint机制。

一般业务运行过程中,当业务需要对某张表的某行数据进行修改的时候,innodb会先将该数据从磁盘读取到缓存中去,然后在缓存中对这条数据进行修改,这样缓存中的数据就和磁盘的数据不一致了,这个时候缓存中的数据就称为dirty page,只有当脏页统一刷新到磁盘中才会是clean page

Checkpoint:如果在某个时间点,脏页的数据被刷新到了磁盘,系统就把这个刷新的时间点记录到redo log的结尾位置,在进行恢复数据的时候,checkpoint时间点之前的数据就不需要进行恢复了,可以缩短时间。

Innodb_log_buffer_size:redo(重做日志)缓存大小

Innodb_log_file_size:redo(重做日志)log文件大小,文件越大数据恢复的时间越长

Innodb_log_file_group: redo(重做日志)og文件数量,默认是2个 :ib_logfile0、ib_logfile1

事务日志的的机制实际上是满足的事务的原子性和持久性,即要么都成功,要么都失败。而谈到事务的一致性和隔离性,就要谈到“锁”了。

InnoDB采用的是两阶段锁定协议。在事务执行过程中,随时都可以执行锁定,锁只有在执行COMMIT或ROLLBACK时才会释放,并且所有的锁同一时刻释放。InnoDB会根据隔离级别在需要的时候自动加锁。

共享锁:由读表操作加上的锁,加锁后其他用户只能获取该表或行的共享锁,不能获取排它锁,也就是说只能读不能写

排它锁:由写表操作加上的锁,加锁后其他用户不能获取该表或行的任何锁,典型是mysql事务中

start transaction;

select * from user where userId = 1 for update;

执行完这句以后

1)当其他事务想要获取共享锁,比如事务隔离级别为SERIALIZABLE的事务,执行

select * from user;

将会被挂起,因为SERIALIZABLE的select语句需要获取共享锁

2)当其他事务执行

  select * from user where userId = 1 for update;

  update user set userAge = 100 where userId = 1; 

也会被挂起,因为for update会获取这一行数据的排它锁,需要等到前一个事务释放该排它锁才可以继续进行

锁的范围:

行锁: 对某行记录加上锁

表锁: 对整个表加上锁

这样组合起来就有,行级共享锁,表级共享锁,行级排他锁,表级排他锁

MVCC(一致性非锁定读)

MVCC 全称 Multi-Version Concurrency Control,多版本并发控制,也可称之为一致性非锁定读;它通过行的多版本控制方式来读取当前执行时间数据库中的行数据。实质上使用的是快照数据。
一致性非锁定读指的是:要读取的行被加了X锁(排它锁),这时候读取操作不会等待行上锁的释放,而是会读取行的一个快照数据。如下图所示:

需要注意的一些点:
①每行记录可能有多个版本
②在事务隔离级别READ COMMITTED (简写RC)和 REPEATABLE READ(简写RR)下,InnoDB存储引擎使用一致性非锁定读。但是对快照的定义却不相同。在RC下,一致性非锁定读总是读取被锁定行的最新一份快照数据。而在RR级别下,总是读取事务开始时的数据版本。
③innodb对每个行要存储多个版本是多么浪费存储空间呀?然而进一步了解,原来所谓的多版本只是innodb聪明地撒了个谎,多个版本是通过undo日志实现的,这里可以理解为既然undo日志包括了所有用来恢复历史版本数据的信息,那么我们只要将“不同版本”指针指向不同时间节点的undo日志即可,这样读取的时候通过对不同时间节点的undo日志进行恢复从而得到不同的版本数据。同时对于undo日志的读取是不需要加锁的,因此这极大地提高了数据库的并发性。

为什么要使用MVCC

消除锁的开销;这个较好理解,如果要保证数据的一致性,最简单的方式就是对操作数据进行加锁,但是加锁不可避免的会有锁开销。所以,如果有能避免进行加锁的方式当然是最好的。

提高并发。

MVCC与事务隔离级别

在InnoDB事务隔离级别“读已提交”与“可重复读”下 ,InnoDB存储引擎使用MVCC机制(非锁定一致性读)。在“读已提交”事务隔离级别下,对于快照数据,MVCC读总是读取被锁定行的最新的快照数据。而“可重复读”读到的总是读取事务开始时的行数据版本。

MVCC原理

对于“可重复读”读到的总是读取事务开始时的行数据版本,那MVCC机制是如何保证可重复读的?其实就是在每一行记录的后面增加两个隐藏列,记录创建事务ID(创建版本号)和删除事务ID(删除版本号),在每次执行新的事务时,都会更新并且递增唯一的事务ID。

SELECT
InnoDB会根据以下两个条件检查每行记录:
a. InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号<=事务的系统版本号),这样可 以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或修改过的。
b. 行的删除版本要么未定义,要么大于当前事务版本号。这样可以确保事务读取到的行,在事务开始之前未被 删除。
只有符合上述两个条件的记录,才能返回作为查询结果。

INSERT
InnoDB为新插入的每一行保存当前系统版本号作为行版本号。

DELETE
InnoDB为删除的每一行保存当前系统版本号作为删除标识。

UPDATE
InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行 删除标识。

保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样读操作简单而性能好,也能保证读到符合标准的行。不足之处是需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。另外,MVCC只在REPEATABLE READ和READ COMMITED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容,因为READ UNCOMMITED纵是读取到最新的数据行,而不是符合当前事务版本的数据行,而SERIALIZABLE则会对所有读取的行都加锁。

ACID特性

1. 原子性(Atomicity)

**事务是一个原子操作单元,一个事务包含多个操作,这些操作要么全部执行,要么全都不执行。**实现事务的原子性,要支持回滚操作,在某个操作失败后,回滚到事务执行之前的状态。

回滚实际上是一个比较高层抽象的概念,大多数DB在实现事务时,是在事务操作的数据快照上进行的(比如,MVCC),并不修改实际的数据,如果有错并不会提交,所以很自然的支持回滚。

而在其他支持简单事务的系统中,不会在快照上更新,而直接操作实际数据。可以先预演一边所有要执行的操作,如果失败则这些操作不会被执行,通过这种方式很简单的实现了原子性。

2. 一致性(Consistency)

数据库一致性(Database Consistency)是指事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。那么,什么又是一致性状态呢,这跟业务约束有关系,比如经典的转账事务,事务处理完毕后,不能出现一个账户钱被扣了,另一个账户的钱没有增加的情况,如果两者加起来的钱还是等于转账前的钱,那么就是一致性状态。

数据库状态如何变化?

每一次数据变更就会导致数据库的状态迁移。如果数据库的初始状态是C0,第一次事务T1的提交就会导致系统生成一个SYSTEM CHANGE NUMBER(SCN),这时数据库状态从C0转变成C1。执行第二个事务T2的时候数据库状态从C1变成C2,以此类推,执行第Tn次事务的时候数据库状态由C(n-1)变成Cn。

一致写

一致写:事务执行的数据变更只能基于上一个一致的状态,且只能体现在一个状态中。T(n)的变更结果只能基于C(n-1),C(n-2), …C(1)中的一个状态,且只能体现在C(n)状态中。也就是说,一个状态只能有一个事务变更数据,不允许有2个或者2个以上事务在一个状态中变更数据。至于具体一致写基于哪个状态,需要判断T(n)事务是否和T(n-1),T(n-2),…T(1)有依赖关系。

举个栗子:

定义100个事务T(1)…T(100)实现相同的逻辑 update table set i=i+1,i的初始值是0,那么并发执行这100个事务之后i的值是多少?可能很容易想到是100。那么怎么从一致性角度去理解呢?

数据库随机调度到T(50)执行,此时数据库状态是C(0),而其它事务都和T(50)有依赖关系,根据写一致性原理,其它事务必须等到T(50)执行完毕后数据库状态变为C(1)才可以执行。因此数据库利用锁机制阻塞其它事务的执行。直到T(50)执行完毕,数据库状态从C(0)迁移到C(1)。数据库唤醒其它事务后随机调度到T(89)执行,以此类推直到所有事务调度执行完毕,数据库状态最终变为C(100)。

一致读

一致读:事务读取数据只能从一个状态中读取,不能从2个或者2个以上状态读取。也就是T(n)只能从C(n-1),C(n-2)… C(1)中的一个状态读取数据,不能一部分数据读取自C(n-1),而另一部分数据读取自C(n-2)。

举个栗子:

还是上面的例子,假设T(1)…T(100)顺序执行,在不同的时机执行select i from table,我们看到i的值是什么?
T(1)的执行过程中。数据库状态尚未迁移,读到的i=0。T(1)执行完毕,T(2)的执行过程中,数据库状态迁移至C(1),读到的i=1。

3. 隔离性(Isolation)

数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的。隔离级别指并发事务之间互相影响的程度,根据影响程度的不同分为四种隔离级别。

四种事务隔离级别

未提交读(Read Uncommitted):一个事务可以读到另一个事务未提交的结果。所有的并发事务问题都会发生。允许脏读。

提交读(Read Committed):只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。Oracle等多数数据库默认都是该级别。

可重复读(Repeated Read):在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。InnoDB默认级别

串行读(Serializable):事务串行化执行,每次读都需要获得表级共享锁,读写相互都会阻塞,隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题。

三种并发问题

脏读:事务A修改了一个数据,但未提交,事务B读到了事务A未提交的更新结果,如果事务A提交失败,事务B读到的就是脏数据。

不可重复读:在同一个事务中,对于同一份数据读取到的结果不一致。比如,事务B在事务A提交前读到的结果,和事务A提交后读到的结果可能不同。不可重复读出现的原因就是事务并发修改记录,要避免这种情况,最简单的方法就是对要修改的记录加锁,这回导致锁竞争加剧,影响性能。另一种方法是通过MVCC可以在无锁的情况下,避免不可重复读。

幻读(虚读):在同一个事务中,同一个查询多次返回的结果不一致。事务A新增了一条记录,事务B在事务A提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录, 即在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。幻读是由于并发事务增加记录导致的,这个不能像不可重复读通过记录加锁解决,因为对于新增的记录根本无法加锁。需要将事务串行化,才能避免幻读。

幻读和不可重复读的区别是:不可重复读是读取到了别人对表中的某一条记录进行了修改,导致前后读取的数据不一致。 虚读是前后读取到表中的记录总数不一样,读取到了其它事务插入的数据。
脏读、不可重复读主要是事务B里面修改了数据
幻读主要是事务B里面新增了数据

隔离级别| 脏读(Dirty Read)| 不可重复读(NonRepeatable Read) | 幻读(Phantom Read)
----|----|----
未提交读(Read uncommitted)| 可能 | 可能 | 可能
提交读(Read committed) | 不可能 | 可能 | 可能
可重复读(Repeatable read) | 不可能 | 不可能 | 可能
串行读(Serializable ) | 不可能 | 不可能 | 不可能

《高性能mysql》一书中的说明:

修改事务隔离级别的方法

1.全局修改,修改mysql.ini配置文件

#可选参数有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE
2 [mysqld]
3 transaction-isolation = REPEATABLE-READ

2.对当前session修改,在登录mysql客户端后,执行命令

SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE

举个几个栗子

脏读

当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

session1session2

//事务1更新了数据

//默认隔离级别:可重复读(Repeatable read)下,未出现脏读

//事务1尚未提交

//修改session2的隔离级别为:未提交读(Read uncommitted),出现脏读

结论:session2在READ-UNCOMMITTED下读取到session 1中未提交事务修改的数据。

不可重复读

在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

session1session2

//开启事务1,隔离级别:已提交读(Read committed),查询结果只有一条数据

//默认隔离级别下,开启事务2,新增一条数据,并提交

//读到了事务2提交的数据,和第一次的结果不一样,出现了不可重复读
-

可重复读

session1session2

//和第一次的结果一样,REPEATABLE-READ级别可以重复读
-

//commit session1之后 再查询可以看到session2插入的数据3了
-

幻读

情况一:

第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

session1session2
-

session1 中事务第一次读取出3行,做了一次更新后,session2事务里提交的数据就出现了。可以看做是一种幻读。

情况二:

session1session2
-
-

以为表里没有数据,其实数据已经存在了,傻乎乎的提交后,才发现数据冲突了。可以看做是另一种幻读。

当隔离级别是可重复读,且禁用innodb_locks_unsafe_for_binlog的情况下,在搜索和扫描index的时候使用的next-key locks可以避免幻读

4. 持久性(Durability)

事务提交后,对数据的修改、系统的影响是永久的。即使出现系统故障也能够保持。

以上是关于MySQL高级 之 事务(ACID特性 与 隔离级别)的主要内容,如果未能解决你的问题,请参考以下文章

mysql基础---事务 事务的四大特性(ACID) 四种隔离级别

MySQL的ACID

MySQL - 深入理解 MySQL 的事务和隔离级别

MySQL的事务的基本特性和隔离级别MySQL事务的ACID靠什么保障

MySQL高级——锁与事务

MySQL_高级部分