【MySQL】事务(重点)
目录
一、什么是事务:
二、事务的前置知识了解
引擎是否支持事务
事务的提交方式
事务操作的前置准备:
三、事务回滚:
四、事务崩溃:
原子性:
持久性:
五、自动提交和手动提交:
六、事务的隔离性:
怎么理解:
隔离级别示例:
读未提交:
读提交:
可重复读:
串行化:
七、拓展知识:
数据库的并发场景:
三个隐藏字段:
undo日志:
MVCC多版本控制:
周边问题:
read view:
RC VS RR:
一、什么是事务:
事务是由多条DML的SQL语句构成的,这些语句在逻辑上是相关联的,比如进行转账操作,就需要有多条SQL语句:查询账户,在此账户上减去指定金额,在别的账户上加上指定金额等等,这些操作是由多条语句构成的,但是这多条操作必须是原子的,也就是说要么转账失败,要么转账成功,不能只进行一般(在我这账户里面扣了,但是在别人的账户那里却没有加上)
这样一个在用户角度看起来是原子性的操作在MySQL中就是一个事务,要把事务看做一个整体
在MySQL中,会有多个事务同时执行,而每个事务都有许多SQL语句,那么在MySQL中就会有很多SQL被同时执行,所以事务不仅仅只是由SQL语句的集合,其还要满足一下属性:
事务的四大属性:
- 原子性:这个在之前的学习中了解过了,就是只有执行前和执行后,没有正在执行,也就是一个事务只有其中的SQL语句全部完成和其中的SQL语句还未开始执行这两个情况,不会在中间的某个环节结束
- 一致性:这个指的是,事务在执行前和执行后,用户数据的正确性没有被破坏,也就是说在执行前后数据的变化是可预期的,比如说在转账这个事务前后,我肯定能预期我的账户会减少,别人的账户会增多我减少的部分,在技术层面上,保证好原子性,隔离性,持久性就能够保证一致性
- 隔离性:MySQL中允许多个事务同时执行数据,隔离性就是保证事务之间互不影响,隔离性又分许多等级包括读未提交( Read uncommitted )读提交( read committed )可重复读( repeatable read)和串行化( Serializable ),两个事务同时执行,两个事务又是具有原子性的,这时当两个事务并发执行的时候要体现出事务之间的隔离性
- 持久性:当事务执行结束后,对数据的修改是永久的,即便后面系统崩溃也不会丢失数据
以上四大属性也被叫做ACID
二、事务的前置知识了解
引擎是否支持事务
在MySQL中,MyISAM引擎是不支持事务的,InnoDB引擎是支持的,我们默认也是InnoDB引擎
事务的提交方式
查看事务的提交方式:
关于事务有两种提交方式,分别是手动提交和自动提交,可以在命令行中用如下SQL进行查看自动提交是否打开:
show variables like 'autocommit';
如下,就证明自动提交是打开的,如果value那一列是OFF的话就证明自动提交是关闭的,此时也就是手动提交
修改事务的提交方式:
用set设置autocommit来进行事务的自动提交的打开或者关闭
0就是OFF,1就是ON
事务操作的前置准备:
在接下来进行事务操作的时候,我们首先将隔离级别修改为读未提交( Read uncommitted )这是最低的隔离级别,这样在后面的操作中能够方便查看,在了解完其他性质后,我们再来了解事务的隔离级别:
首先查看当前的隔离级别:
select @@transaction_isolation;
如上是可重复读的隔离级别,我将其修改为读未提交( Read uncommitted )
set global transaction isolation level read uncommitted;
但是在修改后进行查看发现没有改变,这是因为当前查看的是局部的隔离级别
如上修改是修改的全局的,如果想查看全局的需要如下SQL:
隔离这部分接下来会详细讲解的,可以在后面详细了解
我们重新登录一下MySQL就会把局部的也修改了
接下来准备一张测试表
这是一张员工表,其中包含了员工ID,员工姓名,员工工资
mysql> create table if not exists account(-> id int primary key,-> name varchar(50) not null default '',-> blance decimal(10, 2) not null default 0.0-> );
三、事务回滚:
首先就是启动一个事务,这里有两种方法,begin和start transaction
等到后面commit为止这就是MySQL中的一个事务,在这期间的SQL语句是要看做一个整体的
接着准备两个终端,一个进行插入操作,并设置保存点,另一个进行查看操作
设置保存点的方法:
savepoint 名称;
如上,这是首先对emp员工表进行插入,每插入一条数据就设置一个保存点,当三条语句全部插入后再右边可以进行查看
这个时候,如果我后悔插入关羽这条消息了,就可以使用事务回滚,如下SQL语句进行回滚到s2
rollback to 名称;
此时在进行查看就会发现关羽这条消息不见了
也可以回滚到s1让张飞也不见
接着我们在进行插入
如果想一次性回滚到最开始可以使用rollback,这样就在右侧看不到任何记录了
接着我们在进行插入数据,这个时候直接commit提交数据,这个时候尽管在进行回滚在右边仍然能看到数据,此时左边的事务就是完成了,其修改就是永久了的,这就是事务的持久性,就不能进行事务回滚了
综上所述:
- 使用begin/start transaction启动一个事务
- 使用commi提交一个事务
- 使用savepoint设置一个保存点
- 使用rollback to 回到指定一个保存点
- 使用rollback回滚到最开始
四、事务崩溃:
原子性:
首先在员工表中是存在如下信息的,接着启动一个事务进行测试:
如上,首先在左侧事务进行插入一个数据,在右侧是能够看到的,此时是处于读未提交的隔离性中,但是在左侧的MySQL崩溃后,没有及时的进行commit提交,这个时候会回滚到当前事务的最开始,此时在右侧进行查询就会查询不到新插入的语句,这也保证了事务的原子性
持久性:
那么如果在事务崩溃前进行commit提交了呢?
如上,在MySQL崩溃前进行commit提交后,尽管MySQL崩溃了,但是依然能够看到上一个事务进行插入的数据,这正是因为事务的持久性,如果事务提交后,其修改的数据是永久的
五、自动提交和手动提交:
这里的自动提交和begin的启动事务是无关的,事务必须使用commit命令进行手动提交,数据才会被持久化
那么自动提交和手动提交有什么区别呢?
首先查看当前事务的提交方式:
show variables like'autocommit';
这是自动提交的,并且此时有着如下的一张表:
接着在左侧进行删除表中的数据,然后将左侧的客户端崩溃,发现该员工表中的对应数据被删除了
接着仅仅将提交方式改为手动提交,其余动作不变
set autocommit=0;
接着在左侧进行删除表中的数据,然后将左侧的客户端崩溃,发现该员工表中的对应数据并没有被删除
综上所述:
全局变量autocommit影响的是单SQL语句,在InnoDB中,一条SQL语句会被封装成一个事务,这个事务就需要根据autocommit的大小来进行自动提交或者手动提交
六、事务的隔离性:
怎么理解:
MySQL在启动后,可能会被多个客户(进程或者线程)进行访问,都是以事务为单位进行访问的,一个事务又是由多条SQL语句组成的,那么这些SQL语句在执行的时候可能会访问同一张表,那么MySQL为了保证在执行的过程中一个事务不被其他事务所干扰,于是就有了事务的隔离性,与隔离级别
隔离级别有以下四种:
READ UNCOMMITTED(读未提交):读到了别人未提交的数据,在这个级别下,事务中的修改,即使没有提交,对其他事务也是可见的,这个级别会导致脏读,即一个事务可以读取到另一个事务未提交的数据
READ COMMITTED(提交读):读别人提交了的数据,在这个级别下,一个事务只能看见已经提交事务所做的修改,这可以防止脏读,但是仍然可能发生不可重复读,不可重复读是指在同一个事务中,多次读取同一数据集合时,由于其他事务的介入,导致前后两次读取结果不一致
REPEATABLE READ(可重复读):无论别的事务是否提交修改后的数据,只有当前事务也提交后才能够看到之前别的事务所修改的数据,这是MySQL的默认事务隔离级别。在这个级别下,保证在同一个事务中多次读取同样记录的结果是一致的,因此解决了不可重复读的问题。但是,仍可以发生幻读,即当某个事务在处理某个范围内的记录时,另一个事务插入了新的记录,导致原本的查询结果集发生了变化
SERIALIZABLE(可串行化):这是最高的隔离级别,所有CURD必须按照到来的顺序一个一个执行,在SERIALIZABLE级别下,事务的执行会通过锁定参与事务的所有数据来实现,从而完全避免脏读,不可重复读以及幻读的问题,这意味着事务的执行会像顺序执行一样,一个接一个地串行化执行,从而保证了数据的一致性,但是,这种级别的性能开销最大,因为它需要对所有相关的数据行加锁
隔离级别示例:
隔离级别在MySQL中的表现有两种,分别是全局隔离级别和会话隔离级别
查看全局隔离级别:
查询全局事务隔离级别,即MySQL服务启动后所有新会话的默认隔离级别
select @@global.transaction_isolation;
查看会话隔离级别:
select @@transaction_isolation;
// 或者
select @@session.transaction_isolation;
查询当前会话的事务隔离级别,若会话未主动修改过隔离级别,则继承全局值
设置隔离级别:
set [session | global] transaction isolation level {read uncommitted | read committed | repeatable read | serializable}
如上,其中session表示设置会话隔离级别,global表示设置全局隔离级别
后面的{ }中,在四个中选一个作为设置后的隔离级别
注意:
- 设置会话的隔离级别只会影响当前会话,新起的会话依旧采用全局隔离级
- 设置全局隔离级别会影响后续的新会话,但当前会话的隔离级别没有发生变化,如果要让当前会话的隔离级别也改变,则需要重启MySQL
读未提交:
如下两个终端中,均是读未提交,并且有如下数据的员工表
那么在左右两边开启事务,并且先进行插入一条数据,并没有进行提交,在右边是能够看到的,这就是读到了别的事务未提交的数据,这种情况叫做脏读,是不合理的
并且此时rollback回滚后发现所读到的数据也回滚了
读提交:
先将隔离级别修改为读提交
接着做和上一个步骤一样的,观察结果:
如上,当进行插入后,发现在右边并未查到,但是像下面那样,将左边的事务提交后,发现此时就能够看到了
这就是读别人提交后的数据,这就解决了脏读的问题,但是又出现了不可重复读的问题,也就是右边在一直查询,这样会在某个查询前后出现查询不一致问题,这就导致了右边的事务不可重复读
在实际应用中,这个不可重复读会存在问题的,比如在一个公司中发年终奖前后,如果一个员工在统计工资区间中,另一个员工突然又经过老板同意给公司的某个员工加上薪资,此时就可能会存在同一个员工在奖品区间中出现两次,这是不合理的
所以这个不可重复读,在实际应用中会存在问题的
可重复读:
首先将隔离级别修改为可重复度
接着启动事务,然后在左边进行表的修改,当执行完后在右边再次进行查看,发现右边没有被修改
接着将左边的事务进行提交,此时在进行查看发现右边的表依然没有被修改
最后将右边的事务也提交了,此时在进行查询就能够看得见了
像上边这种,在一个事务中,一直进行查询读,尽管别的事务对同一张表后进行修改,此时依然是看不到这个修改的,直到当前事务也被提交后才能进行看到,也就是说,在可重复读的条件下,在一个事务中进行查询的结果总是一致的
这种隔离级别是MySQL默认的,也是最常使用的
一般的数据库在可重复读隔离级别下,update数据是满足可重复读的,但insert数据会存在幻读问题,因为隔离性是通过对数据加锁完成的,而新插入的数据原本是不存在的,因此一般的加锁无法屏蔽这类问题,但是在MySQL中是解决了这类问题的
幻读:一个事务在执行过程中,相同的select查询得到了新的数据,这种现象叫做幻读
串行化:
首先依然是将隔离级别修改为串行化:
然后启动事务,然后在两边都进行查操作,发现都能够进行查看
但是此时如果有一方进行修改表的操作,会被阻塞
如果长时间没有响应就会报错,如果在阻塞中,另一个事务提交了,此时就不会阻塞了
但是两边查询的结果不一样,直到左边的事务也提交
对于串行化,其效率非常低,在开发中几乎不会使用
对于隔离级别:隔离级别越严格,安全性越高,但数据库的并发性能也就越低,在选择隔离级别时往往需要在两者之间找一个平衡点
七、拓展知识:
数据库的并发场景:
- 读-读并发:不存在任何问题,不需要并发控制
- 读-写并发:有线程安全问题,可能会存在事务隔离性问题,可能遇到脏读 幻读 不可重复读
- 写-写并发:有线程安全问题,可能存在更新丢失问题,第一类更新丢失,第二类更新丢失
这里最值得讨论的是读-写并发,读-写并发是数据库当中最高频的场景,在解决读-写并发时不仅需要考虑线程安全问题,还需要考虑并发的性能问题
接下来要了解事务的隔离是怎么做到的,RC和RR的底层区别在哪?事务回滚又是怎么做到的,接下来需要知道些前置知识:
当一个事务启动的时候,MySQL会为其设置事务ID,每个事务都要有自己的事务ID,可以根据事务的大小,来决定事务到来的先后顺序
mysqld可能会面临处理多个事务的情况,事务也有自己的生命周期,mysqld要对事务进行管理,先描述,再组织,所以事务在mysqld中一定是对应的一个或者一套结构体对象
三个隐藏字段:
MySQL在给我们建表的时候,除了我们在SQL语句中自主创建的字段,还会有三个默认隐藏的字段的
DB_TRX_ID :6 byte,表示最后一次对这个表进行操作的事务ID
DB_ROLL_PTR : 7 byte,在MySQL中,进行修改数据并不是简简单单的修改就行了,还需要将这个数据进行保存,这样在进行事务回滚的时候能够找到历史数据,这就是指向上一个版本的指针
DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引
undo日志:
这是回滚日志,用于对已经执行的操作进行回滚,MySQL会为undo日志开辟对应的缓冲区,用于存储日志相关的信息,必要时会将缓冲区中的数据刷新到磁盘
关于上图,操作系统之上是应用层,然后在应用层上的MySQL中有一个buffer pool空间,在这个空间里面就有我们的undo日志
事实上,undo log就是MySQL中的一段缓冲区,用来记录MySQL中事务回滚的操作的
MVCC多版本控制:
首先我们有如下一串数据
这串数据我们创建之后,就是在B+树中的叶子结点中的,我们又知道这个叶子节点是被加载到buffer pool中的,此时后来的一个事务到来,将该数据的age修改为18
这个时候不是简单的修改,
1、要先对访问这条记录进行加锁,
2、将这个原本的记录复制一份到undo log中,
3、将表中的数据进行修改,将age改为18,
4、此时尽管是只修改了一个字段,但是后面隐藏的字段中修改该记录的事务ID也要被修改为新到来的事务ID,这里假设为11,并且回滚指针也不在指向空,要指向undo log中被复制的原本数据
5、最后提交事务,释放锁即可
同样的道理,又来了一个事务,将name改为李四
这样,这些历史数据就像链表一样串起来,就有了一个版本链,当进行事务回滚的时候,需要回滚到哪个版本,就能够通过这个版本链找到对应的版本了,直接找到版本后覆盖当前数据即可,所谓的创建保存点就可以理解为给某些版本做标记,方便查找回滚
这种多版本控制就叫做MVCC多版本控制,并且在undo log中的一个个的历史版本就称为一个个的快照
周边问题:
那么上述都是修改才有的版本链,那么Delete和INSERT呢?
Delete的数据也会形成版本链的,在MySQL中Delete删除不是真的删了,本质是通过修改数据中的某个比特位将其置为删除,此时就相当于将这行数据进行删除了,如果回滚就是将表示存在还是不存在的比特位修改即可,这样是能够形成版本链的
INSERT操作本身不会形成版本链,但它为后续的UPDATE或DELETE操作提供了初始版本,版本链的形成必须依赖对同一行的多次修改,因此,INSERT是版本链的起点,但非版本链本身
当前读与快照读
当前读:读取最新的记录,就是当前读,增删改,都叫做当前读,select也可能当前读
快照读:读取历史版本,就叫做快照读
对数据进行CURD的时候的加锁问题
当对表进行CURD的时候,此时有两种情况,一个是对当前数据进行修改,一个是对历史数据进行修改,当对当前数据进行修改的时候,可能是CURD中的任意一种情况,此时就需要进行加锁保护,但是对历史数据修改的时候只可能是select,为快照读,就不需要加锁,毕竟历史版本不会被修改,也就是可能并发执行,提高效率
undo log中的版本链何时才会被清除?
unod log存在的意义是进行回滚操作和进行快照读,所以当事务提交后,且所有依赖该版本的事务已完成此时才会将undo log中的版本链清除
read view:
当我们某个事务执行快照读的时候,会对该记录创建一个Read View读视图,记录并维护系统当前活跃事务的ID,read view是一个类,与事务的关系就类似于进程地址空间和PCB,根据这个Read View来判断,当前事务能够看到该记录的哪个版本的数据
Read View是一个数据结构,记录当前事务执行一致性读时的可见性上下文,它决定了哪些数据版本对当前事务是可见的,从而避免脏读,不可重复读等问题
Read View主要由下述字段组成
字段名 | 含义说明 |
---|---|
creator_trx_id | 创建该Read View的事务ID(即当前事务ID) |
up_limit_id | 最小活跃事务ID(即当前系统中最小的未提交事务ID) |
low_limit_id | 当前系统最大事务ID + 1(即系统中尚未分配的事务ID) |
m_ids | 当前系统中所有活跃事务(未提交事务)的ID列表 |
m_low_limit_no | 最小的Undo Log编号(用于InnoDB的Purge机制) |
注意:
Read View是事务可见性的一个类,不是事务创建出来,就会有Read View,而是当这个已经存在的事务首次进行快照读的时候,MySQL形成的Read View
事务ID是依次增大的
当进行一次快照读的时候,此时可以将事务ID分为三种情况
事务ID小于up_limit_id,up_limit_id已经是当前系统中最小的未提交事务ID,如果事务ID比它还要小就证明此刻生成Read View时,该事务一定是提交了的
事务ID大于low_limit_id,low_limit_id是系统中尚未分配的事务ID,所以当事务ID大于low_limit_id时,该事务是生成Read View时还未启动的事务ID
事务ID在这两者之间,这些事务就需要通过判断m_ids来进行判断是否提交,注意:这里的快照事务ID不一定是连续的,毕竟事务的时间长短是不一样的,有可能提前启动的事务,结束得晚,后启动的事务结束得早或晚都有可能
一个事务在进行读操作时,只应该看到自己或已经提交的事务所作的修改,因此我们可以根据Read View来判断当前事务能否看到另一个事务所作的修改
RC VS RR:
关于RC和RR,他们本质区别就是:
RC在每一次查询的时候都会生成一次Read View,每次select都是快照读,因此每次快照读时都能读取到被提交了的最新的数据
RR只有在第一次select的时候才会生成一次Read View,之后的select都用这个Read View,因此当前事务看不到第一次快照读之后其他事务所作的修改
RR级别下快照读只会创建一次Read View,所以RR级别是可重复读的,而RC级别下每次快照读都会创建新的Read View,所以RC级别是不可重复读的
接下来通过两个实验理解理解:
首先将事务隔离级别设置成可重复度
在第一个实验中,我们先让二者都进行快照读,然后在左边进行修改提交,再在右边进行查看
如上,无论怎么进行查看都是看不见修改的,这也符合可重复读的特性
在第二个实验中,我们先在左边进行修改提交,再在右边进行查看
这时发现右边能够查看到左边的修改了
为什么呢?
实验一中,在最开始就进行了快照读,此时左右都属于活跃事务,都是在中间的,那么无论左边怎么修改,右边的都看不到
实验二中,左边先进行修改后提交,右边在进行快照读,此时左边的事务ID就小于最小活跃事务ID,右边事务中快照读生成的Read View中的m_ids就一定没有左边的事务ID,此时左边的事务ID就比右边事务的up_limit_id还要小,或者是在up_limit_id和low_limit_id之间但是没在m_ids里面,无论那种情况都能够被右边的事务所看到
那么RC呢?
RC和RR的本质区别就在于RC在每次进行快照读的时候,都是生成新的快照了,那么low_limit_id就是当前系统下最大的事务ID,所以就能保证只要其他事务commit了, 其他事务就不是活跃事务了, 那么就只剩下了在up_limit_id和low_limit_id之间但是没在m_ids里面这种情况,也就一定能看到了别的事务的修改了