目录
事务的基本概念
简单来说,就是多个sql封装成一个整体,以达到完成某个事件的目的,而且保证该操作是原子性的,这种整体称为一个事务,比如说:
有一种业务需求是张三给李四转账50元,上层看起来很简单的操作,但后台却可能需要多条sql语句:(简化版)
select money from table_name where name='张三' and money >= 50; update set money = money-50 where name ='张三'; update set money = money+50 where name ='李四';
这几条sql语句的整体就是一个事务!
具体概念:
事务就是一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体,MySQL提供一种机制,保证我们达到这样的效果,事务还规定不同的客户端要看到的数据是不相同的
事务的四大特性
A、原子性:(Atomicity)
一个事务是不可分割的最小工作单位。
执行的事务,要么全部成功,要么回滚到执行事务之前的状态。
C、一致性:(Consistency)
事务执行前后的数据,必须保持一致。即使系统故障也不会丢失。
I、隔离性:(isolation)
多个事务之间数据要相互隔离,即彼此独立和透明互不影响。事务隔离分为不同的级别,包括读未提交,读提交,可重复读和串行化
D、持久性:(Durability)
事务一旦被提交(commit / rollback ),数据的改变是永久性的。
那么它们的联系是什么呢?
给大家画了一个图
一致性:
其实是站在用户的角度来理解的,利用事务的原子性,持久性,隔离性以达到一致性的目的
提交方式
我们先设置隔离级别为READ-UNCOMMITTED
查看隔离级别:
查看事务的提交方式:(默认开启了自动提交)
我们选择关闭自动提交(off)
我们向表插入数据
我们不意外的发现确实多了 id =3 name ='kk'的一行
但是
当我们ctrl + \ 异常终止进程的时候,我们再去查询表内容,我们发现:
那么为什么会发生这样的现象? 不是说事务有持久性吗,怎么没有保存数据呢?
那是因为关闭自动提交,在没有开启事务(也就是没有输入begin) 的前提下,每一条sql语句都是一个事务,异常终止,因为原子性导致刚插入的数据丢失。
只有commit(提交)之后,数据才会永久保存(持久性)
详细谈论原子性和隔离性
原子性
我们begin开启事务并且插入一条数据
观察到确实插入进去了
ctrl + \ 终止 ,数据就没了
这里的实验过程和提交方式那部分类似,都是为了验证事务的原子性
如果是commit提交之后,再异常终止,那么数据是可以保存的
rollback(回滚):
如果进行了回滚操作,那么刚插入的数据也会被清除(虽然它没有真正意义上的被插入)
我们还可以设置保存点:
mysql> savapoint s1;
rollback + 保存点名称 表示回滚到某个保存点,类似游戏里面的存档
隔离性
隔离级别:
读未提交(READ UNCOMMITTED):插入或修改删除的数据还未commit 就已经在另一个会话里显示出了结果(不推荐不安全,几乎没有加锁),这种现象称为脏读。这就是我们上面验证原子性采用的隔离级别,仅仅是实验需要,实际不推荐。
读提交(READ COMMITTED):数据未commit就看不到,即一个会话提交了但是另一个会话还未提交而另一会话就已经看到了修改的数据,另一个会话在不同时间段看到了不同的数据,这称为不可重复读问题。
可重复读(REPEATABLE READ):数据未commit就看不到 提交后另一个端还未提交另一端就看不到修改的数据,在不同时间段看到了相同的数据,但是可能会存在幻读问题。
串行化 (SERIALIZABLE):串行化是4种事务隔离级别中隔离效果最好的,解决了脏读、可重复读、幻读的问题,但是效果最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束。但是效率最低,一般也不会无脑采用这种隔离级别。
我们来继续深究隔离性
其实每个表里都有隐藏字段:
怎么理解?
除了我们正常插入的数据外,后面还跟着一些隐藏字段:
undo log
什么是uodo log?
简单理解undo日志可以理解为一个内存空间,但是不能理解它是执行记录,它可以记录数据和sql,那我们还是画个图来理解。
假设最开始存在这样一行记录:
当我们开启一个事务(假设事务id =10) ,并对它进行更新(update) 时,系统会进行如下操作
当要对一行数据进行修改时,系统会给这行数据加锁,然后系统会把待操作的这一行记录在undo log 内,因为undo log是内存级别的,所以它也一定存在地址,所以隐藏列字段回滚指针就会写入它的地址,并且更新事务id。
这只是一组,那么一个事务可能存在多组修改操作,必然会形成一条隐藏字段的回滚指针指向undo log内 存放数据地址链式结构,这样的一种链式结构我们称为历史版本链
那么回滚其实就是用undo log里的旧数据覆盖到当前数据。
undo log 会一直变大吗?
不会!因为它里面的数据是临时数据,每当事务提交后,那么它的相关数据会被直接清空,它类似于缓冲区。所以一旦一个事务被提交,那么将无法回滚。
read view
read view 在MySQL 源码中其实就是一个类,本质是用来进行可见性判断的。即当我们某个事务执行快照读的时候,对该记录创建一个read view读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是最新的数据,也可能是该记录的undo log里面某个版本的数据。
类的内容(简化):
m_ids,当前有哪些事务正在执行,且还没有提交,这些事务的 id 就会存在这里;
min_trx_id,是指 m_ids 里最小的值;
max_trx_id,是指下一个要生成的事务 id。下一个要生成的事务 id 肯定比现在所有事务的 id 都大;
creator_trx_id,每开启一个事务都会生成一个 ReadView,而 creator_trx_id 就是这个开启的事务的 id。
那么这个read view有什么作用呢?
我们还是画个图来解释
read view的作用是让系统知道当一个事务启动时,它可以看到哪些数据,不能看到哪些数据
具体的比较策略:
1、trx_id < m_ids列表中最小的事务id
表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。
2、trx_id > m_ids列表中最大的事务id
表明生成该版本的事务在生成ReadView 后才生成,所以该版本不可以被当前事务访问。
3、m_ids列表中最小的事务id < trx_id < m_ids列表中最大的事务id
隔离级别RR和RC的区别?
正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同。
在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及ReadView,将当前系统活跃的其他事务记录起来此后在调用快照读的时候,还是使用的是同一个ReadView,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个ReadView,所以对之后的修改不可见;
即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于ReadView创建的事务所做的修改均是可见
而在RC级别下的,事务中,每次快照读都会新生成一个快照和ReadView,这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因
总之在RC隔离级别下,是每个快照读都会生成并获取最新的ReadView;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建ReadView,之后的快照读获取的都是同一个Read View。。正是RC每次快照读,都会形成ReadView,所以,RC才会有不可重复读问题。