在讲MySQL的事务之前,我们需要明白什么是事务,为什么会用到事务。
事务:是指一组操作要么同时成功,要么同时失败,失败后数据内容恢复初始状态,成功后数据内容持久化。
为什么用到事务呢? 在项目研发中,基于对对象的抽象会产生不同的对象实体,这些实体映射到底层数据库表的时候会有多张表存在。一组业务操作可能需要对这多张表同时处理,为了保障这多张表数据同时成功或者同时失败,就需要借助数据库事务来保障。
1. MySQL事务
1.1 事务的特性
- 原子性:指单个事务本身涉及到的数据库操作,要么全部成功,要么全部失败,不存在完成事务中一部分操作的可能;
- 隔离性:指多个事务之间,没有相互影响干扰。
- 一致性:指事务执行前后的状态要一致,没有脏数据。
- 持久性:数据库一旦完成事务的提交之后,那么这个事务的状态就会持久在数据库中。
1.2 事务的隔离级别
MySQL标准定义了4类隔离级别,用来限定事务内外的哪些改变是可见的,哪些是不可见的。隔离级别由低到高:Read Uncommitted < Read Committed < Repeatable Read < Serializable。对应隔离级别产生的差异有:
=========================================================================
隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
=========================================================================
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能
=========================================================================
脏读:当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个实物访问了这个数据,并且使用了这个数据。
不可重复读:在一个事务内,对一个数据进行了多次的读取;这个事务还没有结束的时候,另一个事务对数据进行了修改,导致第一个事务前后读取到的数据不一致,产生不可重复读。
幻读:第一个事务对表中的数据进行了修改,这种修改涉及到表的全部数据;第二个事务也修改了表中的数据,这种修改主要是向表中插入一条记录,导致操作第一个事务的用户发现表中还有没有修改的数据行,像是幻觉一样。
–查看全局事务隔离级别
SELECT @@global.tx_isolation;
— 查看session隔离级别
SELECT @@session.tx_isolation;
— 查看当前事务隔离级别
SELECT @@tx_isolation;
//设置read uncommitted级别: set session transaction isolation level read uncommitted;
//设置read committed级别: set session transaction isolation level read committed;//设置repeatable read级别: set session transaction isolation level repeatable read;
//设置serializable级别: set session transaction isolation level serializable;
2. 事务的分类
- 扁平事务:最简单也是实际使用最频繁的一种事务,由 begin 开始,commit work 或者 rollback 结束,期间的操作都是原子性操作,要么执行,要么回滚
-
带有保存点的扁平事务:
- 除了支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态
- 保存点用来通知系统应该记住事务当前的状态,一旦事务过程中发生错误,事务能回到保存点当时的状态
-
链事务
- 可视为带有保存点的扁平事务的变种,当发生系统崩溃时,所有保存点都将消失,因为保存点是易失的而非持久的
- 当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行
- 嵌套事务:由一个顶层事务控制着各个层次的事务,顶层事务之下嵌套的事务称为子事务,其控制着每个局部的变换
- 分布式事务:通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在的位置访问网络中的不同节点
3. 事务实现机制
3.1 基于锁的事务隔离
数据库的锁是为了解决事务的隔离性问题,为了让事务之间相互不影响,每个事务进行操作的时候都会对数据加上一把特有的锁,防止其他事务同时操作数据。数据库里有的锁有很多种,为了方面理解,对锁进行了一个分类,分别如下
- 基于锁的属性分类:共享锁、排他锁。
- 基于锁的粒度分类:表锁、行锁、记录锁、间隙锁。
- 基于锁的状态分类:意向共享锁、意向排它锁。
3.1.1 锁的属性分类
共享锁(Share Lock)
共享锁又称读锁,简称S锁;当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。
排他锁(Exclusive Lock)
排他锁又称写锁,简称X锁;当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。
3.1.2 锁的力度分类
表锁
引擎 MyISAM, 直接锁定整张表,在锁定期间,其它进程无法对该表进行写操作;如果是读锁,则其它进程读也不允许。特点: 粒度大,加锁简单,容易冲突;
页锁
引擎 BDB。页级锁介于表级锁和行级锁之间,一次锁定相邻的一组记录
行锁
引擎 INNODB, 锁住的是表的某一行或多行记录,其它进程还是可以对同一个表中的其它记录进行操作。 特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高;
记录锁(Record Lock)
记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录。
间隙锁(Gap Lock)
间隙锁属于行锁中的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成一个区间,遵循左开右闭原则。
间隙锁作用
:防止幻读问题。
临键锁(Next-Key Lock)
临键锁也属于行锁的一种,是INNODB的行锁默认算法,它是记录锁和间隙锁的组合,加锁的时候会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住。
临键锁的作用:
临键锁避免了在范围查询时出现脏读、重复读、幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入。
3.1.3 状态锁
为了允许表级锁和行级锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
- 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
- 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
共享锁(S) |
排他锁(X) |
意向共享锁(IS) |
意向排他锁(IX) |
|
共享锁(S) |
兼容 |
冲突 |
兼容 |
冲突 |
排他锁(X) |
冲突 |
冲突 |
冲突 |
冲突 |
意向共享锁(IS) |
兼容 |
冲突 |
兼容 |
兼容 |
意向排他锁(IX) |
冲突 |
冲突 |
兼容 |
兼容 |
注意:意向锁是InnoDB自动加的,不需要用户干预;对于insert、update、delete,InnoDB会自动给涉及的数据加排他锁(X)。
3.1.2 MVCC
MVCC:多版本并发控制(MVCC,Multiversion Currency Control),实现了非阻塞的读操作(解决了幻读、不可重复读的问题),但保存了两个额外的系统版本号,占用额外的存储空间。实现原理:innodb存储的最基本row中包含一些额外的存储信息 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT。
- DATA_TRX_ID,6字节, 标记了最新更新这条行记录的transaction id,每处理一个事务,其值自动+1
- DATA_ROLL_PTR:7字节,指向当前记录项的rollback segment的undo log记录,找之前版本的数据就是通过这个指针
- DB_ROW_ID:6字节,当由innodb自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值,否则聚集索引中不包括这个值.,这个用于索引当中
- DELETE BIT:位用于标识该记录是否被删除,这里是标志出来的删除。真正意义的删除是在commit的时候
在默认隔离级别RR下,MVCC的工作方式:
-
Select:
- InnoDB只查找版本早于当前事务版本的数据行,保证事务读取的行, 要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
- 行的删除版本定义,要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。
- INSERT:InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
- DELETE:InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
- UPDATE:InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
MVCC在mysql中的实现依赖的是undo log与read view
- undo log :undo log 中记录某行数据的多个版本的数据。
- read view :用来判断当前版本数据的可见性
3.2 redo log 和undo log, binlog
3.2.1 redo log
redo log叫做重做日志,是InnoDB存储引擎层的日志,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。redo log日志的大小是固定的,即记录满了以后就从头循环写。
redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。
为了确保每次日志都能写入到事务日志文件中,在每次将log buffer中的日志写入日志文件的过程中都会调用一次操作系统的fsync操作(即fsync()系统调用)。MySQL支持用户自定义在commit时如何将log buffer中的日志刷log file中。这种控制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有3种值:0、1、2,默认为1。但注意,这个变量只是控制commit动作是否刷新log buffer到磁盘。
- 当设置为1的时候,事务每次提交都会将log buffer中的日志写入os buffer并调用fsync()刷到log file on disk中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
- 当设置为0的时候,事务提交时不会将log buffer中日志写入到os buffer,而是每秒写入os buffer并调用fsync()写入到log file on disk中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
- 当设置为2的时候,每次提交都仅写入到os buffer,然后是每秒调用fsync()将os buffer中的日志写入到log file on disk。
3.2.2 undo log
undo log 叫做回滚日志,记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log的信息来进行回滚到没被修改前的状态。
undo log有两个作用:提供回滚和多个行版本控制(MVCC)。undo log和redo log记录物理日志不一样,它是逻辑日志。当执行rollback时,可以从undo log中的逻辑记录读取到相应的内容并进行回滚。
undo log是采用段(segment)
的方式来记录的,每个undo
操作在记录的时候占用一个undo log segment
。
另外,
undo log
也会产生redo log
,因为undo log
也要实现持久性保护。
undo log 格式:
- insert undo log:insert 操作产生的 undo log ,在事务提交后就删除
- update undo log:delete 和 update 操作产生的 undo log,该 undo log 可能需要提供 MVCC 机制,因此不能在事务提交时就删除
3.2.3 binlog 二进制日志
mysql-binlog是MySQL数据库的二进制日志,用于记录用户对数据库操作的SQL语句((除了数据查询语句)信息。binlog的格式也有三种:STATEMENT、ROW、MIXED 。
3.2.3 binlog 和事务日志的先后顺序
二进制日志是MySQL的上层日志,先于存储引擎的事务日志被写入。在MySQL5.6以前,当事务提交(即发出commit指令)后,MySQL接收到该信号进入commit prepare阶段;进入prepare阶段后,立即写内存中的二进制日志,写完内存中的二进制日志后就相当于确定了commit操作;然后开始写内存中的事务日志;最后将二进制日志和事务日志刷盘,它们如何刷盘,分别由变量 sync_binlog 和 innodb_flush_log_at_trx_commit 控制。
但因为要保证二进制日志和事务日志的一致性,在提交后的prepare阶段会启用一个
prepare_commit_mutex
锁来保证它们的顺序性和一致性。但这样会导致开启二进制日志后group commmit失效,特别是在主从复制结构中,几乎都会开启二进制日志。在MySQL5.6中进行了改进。提交事务时,在存储引擎层的上一层结构中会将事务按序放入一个队列,队列中的第一个事务称为leader,其他事务称为follower,leader控制着follower的行为。虽然顺序还是一样先刷二进制,再刷事务日志,但是机制完全改变了:删除了原来的prepare_commit_mutex行为,也能保证即使开启了二进制日志,group commit也是有效的。
MySQL5.6中分为3个步骤:
flush阶段、sync阶段、commit阶段。
- flush阶段:向内存中写入每个事务的二进制日志。
- sync阶段:将内存中的二进制日志刷盘。若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的刷盘操作。这在MySQL5.6中称为BLGC(binary log group commit)。
- commit阶段:leader根据顺序调用存储引擎层事务的提交,由于innodb本就支持group commit,所以解决了因为锁 prepare_commit_mutex 而导致的group commit失效问题。
3.2.4 group commit
对于InnoDB来说,事务提交的两个阶段
- 修改内存中事务对应的信息,并且将日志写入重做日志缓冲
- 调用 fsync 将确保日志都从重做日志缓冲写入磁盘
若事务为非只读事务,则每次事务提交时需要进行一次 fsync 操作,以此保证重做日志都已经写入磁盘,为了提高磁盘 fsync 的效率,当前数据库提供了 group commit 的功能,即一次 fsync 可以刷新确保多个事务日志被写入文件
3.3 事务实现
3.3.1 undo log实现原子性
每条数据变更(insert/update/delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上。
3.3.2 redo log实现持久性
事务开始之后就产生redo log,redo log的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo log文件中。
3.3.3 隔离级别实现
通过MySQL的锁和MVCC实现事务隔离级别;
3.3.4 一致性实现
通过回滚,以及恢复,和在并发环境下的隔离做到一致性。
参考文献: