一.定义和特性
事务就是一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。
要了解事务最重要的是了解他的四个特性:
- 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
- 隔离性 :数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务 并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Read uncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化 ( Serializable )
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失
在四个特性中,隔离性是最难理解的我们之后重点讲解。
为什么会出现事务 ?
事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型, 不需要我们去考虑各种各样的潜在错误和并发问题.可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的.而不是伴随着数据库系统天生就有的 。
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。
二.事务提交方式
事务的提交方式常见的有两种: 自动提交,手动提交。
查看事务提交方式:
这里表示自动提交打开,当我们在命令行每输入一行命令都会被当成一个事务提交。
用 SET 来改变 MySQL 的自动提交模式
使用set autocommit=0; 即可让自动提交关闭,事务需要手动提交。
三.事务常见操作方式
1.开启事务
start transaction或者begin。
2.创建保存点
savepoint save1;创建保存后可以之后使事务会滚到对应的保存点,就像游戏里的存档一样。
3.回滚到保存点
rollback to save1;rollback回滚到最开始。
4.提交事务
commit。
5.例子如下:
先创建一个简单表格
对事务进行操作,我们先开启事务,建立第一个保存点,再插入一条记录,建立第二个保存点,再插入事务,查看数据显示俩条记录,再不断回滚,查看记录。
上面就是手动提交事务的基本流程。
四.事务隔离级别
表中的数据可能会被多个事务并发访问,隔离级别就是允许事务受到其他事务不同程度的干扰。分别有四个隔离级别:
- 读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多 并发问题,如脏读,幻读,不可重复读等
- 读提交【Read Committed】 :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默 认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离 级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
- 可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行 中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
- 串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突, 从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,但是可能会导致超时和锁竞争 (这种隔离级别太极端,实际生产基本不使用)
光看定义比较难理解,我们结合具体的操作理解。
1.查看与设置隔离性
隔离级别在作用范围可以可以分成:全局隔离级别和当前(会话)隔离级别,一个是对全部会话生效,一个是当前会话生效,就非常类似于全局变量和局部变量。
查看隔离级别:
设置隔离级别
2.读未提交
顾名思义,就是在在该隔离条件下,不同事务可以读互相读未提交的数据
我们同时开启事务1,2。这时,在事务1中插入一条记录后不提交,直接在事务2中可以读取到该记录,这就是读未提交。这时会引发脏读,一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读。
3.读提交
顾名思义,就是在在该隔离条件下,不同事务可以读互相读已经提交的数据 。
我们同时开启事务1,2。这时,在事务1中插入一条记录后不提交,在事务2中不可以读取到该记录,当我们在事务1中commit,才能读取到记录。
此时还在当前事务2中,并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段 (依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)。也就是说读提交会造成不可重复读。
4.可重复读
这解决了上面的问题,保证同一个事务读到同一个值,这里有一个坑。
同时开启事务1,2。在事务1插入一条记录, 不提交在事务2中读取,读取不到,提交后再读取依旧读取不到。这里会有一个坑。当我们调整第3步和第4步顺序,可以在事务2中读取到插入的记录。也就是开启事务1,2后,先插入数据后立马提交,这时在事务2中进行第一次查询是能读取到俩条记录的 。 这里的原理我们后面讲解。
5.串行化
这个比较容易理解,它在每个读的数据行上面加上共享锁,通过强制事务排序,使之不可能相互冲突。如下:
事务1不提交,事务2查看就会阻塞。 每个事务必须按顺序执行。
五.多版本并发控制( MVCC )
多版本并发控制( MVCC )是一种用来解决读-写冲突的无锁并发控制,可以用来实现隔离性。
理解 MVCC 需要知道三个前提知识:
- 3个记录隐藏字段
- undo 日志
- Read View
1. 3个记录隐藏字段
- DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID
- DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)
- DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引
- 补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
每个事务都有id作为标识,第一个字段用来记录创建这条记录/最后一次修改该记录的事物ID,第二个字段指向更新前的历史版本,第三个字段表示隐藏主键,如果数据表没有主键,就会用隐藏主键充当索引(对索引不理解的可以看上篇博客哦)。
2.uodo log日志
undo log 被称为回滚日志,用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚(保证事务的原子性) 和 MVCC(多版本并发控制) 。MySQL 将来是以服务进程的方式,在内存中运行。我们之前所讲的所有机制:索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。 所以,我们这里理解undo log,简单理解成,就是 MySQL 中的一段内存缓冲区,用来保存日志数据的就行。
3.模拟mvcc
现在有一个事务10(仅仅为了好区分),对student表中记录进行修改(update):将name(张三)改成 name(李四)。
- 事务10,因为要修改,所以要先给该记录加行锁。
- 修改前,现将改行记录拷贝到undo log中,所以,undo log中就有了一行副本数据。(原理就是写时拷贝)
- 所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name,改成 '李四'。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务10 的ID, 我们默认从 10 开始,之后递增。而原始记录的回 滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的 上一个版本就是它。
- 事务10提交,释放锁。
现在又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38)。
- 事务11,因为也要修改,所以要先给该记录(李四)加行锁.
- 修改前,现将改行记录拷贝到undo log中,所以,undo log中就又有了一行副本数据。此时,新的 副本,我们采用头插方式,插入undo log。
- 现在修改原始记录中的age,改成 38。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务11 的 ID。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
- 事务11提交,释放锁。
这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。 上面的一个一个版本,我们可以称之为一个一个的快照 。
上面是以更新(`upadte`)主讲的,如果是`delete`呢?一样的,别忘了,删数据不是清空,而是设置flag 为删除即可。也可以形成版本。
如果是`insert`呢?因为`insert`是插入,也就是之前没有数据,那么`insert`也就没有历史版本。但是 一般为了回滚操作,insert的数据也是要被放入undo log中,如果当前事务commit了,那么这个undo log 的历史insert记录就可以被清空了。 总结一下,也就是我们可以理解成,`update`和`delete`可以形成版本链,`insert`暂时不考虑。
那么`select`呢? 首先,`select`不会对数据做任何修改,所以,为`select`维护多版本,没有意义。不过,此时有个问题, 就是: select读取,是读取最新的版本呢?还是读取历史版本?
select读取分为俩种:
- 当前读:读取最新的记录,就是当前读。增删改,都叫做当前读,select也有可能当前读,比如:select lock in share mode(共享锁), select for update (这个好理解,我们后面不讨论)
- 快照读:读取历史版本(一般而言),就叫做快照读。(这个我们后面重点讨论)
我们可以看到,在多个事务同时删改查的时候,都是当前读,是要加锁的。那同时有select过来,如果也要读取最新版(当前读),那么也就需要加锁,这就是串行化。 但如果是快照读,读取历史版本的话,是不受加锁限制的。也就是可以并行执行!换言之,提高了效率,即 MVCC的意义所在。
那么,是什么决定了,select是当前读,还是快照读呢?隔离级别!多个事务在执行中,CURD操作是会交织在一起的。为了保证事务的"有先有后",应该让不同的事务看到它该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题。
那么,如何保证,不同的事务,看到不同的内容呢?也就是如何如何实现隔离级别?
4.Read View
Read View就是事务进行快照读操作的时候生产的读视图 (Read View),在该事务执行的快照读的那一 刻,会生成数据库系统当前的一个快照,**记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被 分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)**Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照 读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。光看定义很抽象,我们直接讲解结构更易理解。
我们可以把Read View 看成下面的简化结构:
class readview
{
m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id; //记录m_ids列表中事务ID最小的ID(没有写错)
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的
最大值+1(也没有写错)
creator_trx_id //创建该ReadView的事务ID
}
当我们为一个事务创建视图时按按时间可以分成下面三类:
- 1.已经提交的事务, 或者就是自己这个事务,应该能看到这些事务的数据。
- 2.和创建视图的事务一起在并发执行的事务,这些事务的数据是不能看到的。
- 3.后来的事务,这些事务的数据是不能看到的
总结起来就是,之前的事务能看到,同级别和之后的不能看到。 这样也符合逻辑。
这里我门重点要注意是创建Read view 时他的活跃跃事务id并不一定是连续的。如11,12,13,14,15号事务运行,在快照读前,12,14提交了,那么m_id=11,13,15。此时12,14是在已提交的范围内。
举个例子,假设当前有条记录:
事务操作:
事务4:修改name(张三) 变成name(李四)
当事务2对某行数据执行了 快照读 ,数据库为该行数据生成一个 Read View 读视图
此时版本链是:
只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务。
我们的事务2在快照读该行记录的时候,就会自上而下拿行记录的 DB_TRX_ID 去跟 up_limit_id,low_limit_id和活跃事务ID列表(trx_list) 进行比较,判断当前事务2能看到该记录的版本。 过程如下:
- DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步(如果小于说明是已提交)
- DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步(如果大于说明是未提交)
- m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中。(不在就说明已提交,在就说明未提交)
故,事务4的更改,应该看到。 所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本。
5.RR 与 RC的本质区别
这里我们就可以解决上面一个遗留问题:
在可重复读级别下,按上面的顺序,事务2俩次读取都不能读到事务1插入的记录的,我们可能会疑惑,事务2第二次读取前,事务1不是已经提交了吗,按照read view的理解不是应该能读取到吗?读提交按照上面的顺序第二次能读到又是因为上面?
这时因为Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同。
在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来 此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见。
而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因 。
总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。 正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。
这样我们就能理解为什么在可重复读的情况下,俩次读取都不能读到插入的记录.因为事务1在第一次读取时未提交,俩次读取都是基于同一个read view,自然不行 。当3,4步的顺序调换时,俩次读取都能读到,是因为事务1在第一次读取时就提交了。
MySql事务就讲解到这里,求点赞了,啊啊啊啊啊啊啊啊啊啊啊啊。