文章目录
- 事务(Transaction)
- 为什么会出现事务
- ACID四大属性
- 事务提交的方式
- 事务基本操作:
- 事务隔离级别(MVCC+)
- 隔离级别:
- 如何理解隔离性?
- 为何要存在隔离级别?
- 一致性
- 读读并发
- 写写并发
- 读写并发
- 三个隐藏列字段
- undo log
- 模拟MVCC版本链
- 总结
- ReadView
- 实验
- RR和RC本质区别
- 例一:实现效果是RR级别
- 例2:实验效果是RC级别
事务(Transaction)
MySQL一定会遇到并发问题,内部都自己解决了,采用的策略就是事务。因为并发问题一张票卖给两个人了就是。
在用户层的角度:你要完成什么样的业务叫做事务。
在MySQL角度:为了完成一个目的的步骤中每一个sql操作(上层理解是具有逻辑关系的)整合在一起就叫事务。
- 例如,我给别人转账,先把我的账户余额减去,再把别人的加上去,这两部合在一起叫事务。
所有的sql操作一半都会被MySQL包装成为事务,以事务的方式提交。
为什么会出现事务
本质是为了当应用程序访问数据库的时候,事务能够自己解决潜在错误和并发问题。原子性要么提交要么回滚,就不用考虑网络异常等情况。因此事务本质上是为了应用层服务的,而不是伴随数据库系统天生就有的。
InNoDB支持事务,myisam不支持事务。
ACID四大属性
- 原子性A:一个事务的所有的操作要么全部完成,要么全部不完成。如果发生错误,就会回滚到事务开始前的状态。
- 一致性C:
- 隔离性I:互相不要影响
- 持久性D:事务处理之后对数据的修改是永久的即便系统故障也不回丢失。
事务提交的方式
手动提交和自动提交,autocommit=1就是自动提交
show variables like 'autocommit';
查看事务提交方式
set autocommit =1;
设置为自动提交,=0禁止自动提交
事务基本操作:
set global transaction isolation level read uncommited;--设置默认隔离级别是读未提交
select @@tx_isolation;--查看隔离级别
start transaction;--命令行启动事务
begin;
savepoint s1;--设置回滚点
rollbackto s1;--回滚到s1这个点
rollback;--回滚到起始点,将操作都撤销
非正常操作(读未提交隔离级别):
- 实验一:未提交commit直接
ctrl+\
中止了Aborted,客户端崩溃,数据直接回滚恢复到最开始。–原子性
手动启动(start transaction/begin )一个事物的时候和MySQL中是否事务会自动提交无关。
-
实验二:commit提交之后已经被提交到数据库之后就不能再被回滚了–持久性
-
begin操作会自动更改提交方式,就叫手动提交,和系统默认的自动提交就无关了。
-
自动提交设置是给谁设置的呢?会影响谁呢?
其实我们之前的所有单条sql本质在MySQL中全部各自会被以事务的方式进行提交的,自动提交的,不需要begin什么的。select查询并不受影响,当我们insert数据之后,一旦ctrl+\异常中止,数据就会回滚,插入的单条语句的数据也会丢失。
自动提交就是给mysql中单条SQL设置的,我们的默认行为
- 一个事务已经被commit之后就没办法回滚了。
-
事务隔离级别(MVCC+)
如何理解隔离性?
数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个特征叫隔离性。
数据库中,允许事务收到不同程度的干扰,就有了隔离级别。
隔离级别:
- 读未提交(Read Uncommited):
select @@tx_isolation;--查看事务隔离属性
select @@gloabl.tx_isolation;--全局隔离级别
select @@session.tx_isolation;--只会影响本次客户端的隔离界别,再次登录就没了
读到了另一个事务还未提交的数据,对方可以回滚,错误的指导了另一个事务进行操作。相当于没有任何隔离性,也有很多的并发问题,脏读。
如下图的我插入错误的唐僧然后回滚删除了,却提前被另一端的另一个事务读走了,可能就在上层为唐僧建立了等等数据结构。
不同的隔离级别是由锁的不同的使用决定的。
select @@global.tx_isolation;
查看全局隔离级别。重启MySQL之后有效果对所有的回话都有影响。
select @@session.tx_isolation;
查看本次会话的隔离级别。session隔离级别只会影响自己,并且立马有效。
set global transaction isolation level Repeatable Read;
- 读提交(Read Commited)
set global transaction isolation level read commited;--读提交
读不到对端未提交的数据就是读提交了
问题:同一个事务内连续的读取出现了不一样的值(上帝视角是因为提交和没提交的时间点不同),造成不可重复读取,这是个问题吗?
-
你已经提交别人能读到,不应该等价于你已经提交,和你并行运行的事务也能读到。
-
不可重复读有什么问题?
当一个作为逻辑判断的事务,读取表内容,判断结果受到修改内容的影响。表被其他三个同时运行的不同的事务(读提交)设置为三个不同的内容,导致逻辑判断事务连续做出三次判断结果,导致上层逻辑错误。
-
可重复读(Repeatable Read)默认是RR级别
set global transaction isolation level repeatable read;
再怎么对数据进行修改尽管已经提交,我这个事务就是读不到修改甚至删除导致我读到的都是一样的结果。只有将我这个并行的事务结束掉,重新整一个就可以读到了,就是可重复读。
一般数据库,对插入操作无法进行屏蔽,尽管处于可重复读状态RR级别,会读到另一个事务新插入的数据,造成幻读问题。不过MySQL解决了。
-
串行化
对所有的操作全部加锁,进行串行化,只能一个个来,是事务之间的串行化。
set global transaction isolation level serializable;
即使是串行化,对于读操作是没有限制的。
两个并行读取的事务,一方事务想要修改就会卡主,只有另一个同时读取的事务发生commit之后,这一端才能够操作成功。
如何理解隔离性?
MySQL的内部机制让同时启动并发执行的各个事务,看到不同的数据修改(不包括读),就叫做隔离性。我们作为一个事务可看到不同可见性的数据,程度的不同叫做隔离级别。
为何要存在隔离级别?
隔离级别越严格,安全性越高,数据库的并发能力也就越低,只是为了安全,不需要种类繁多的隔离级别,无脑串行化就行。所以,安全+效率之间找平衡点,而这个平衡点不是MySQL决定的,采用什么方案什么隔离级别是由上层决定的,应用场景决定的。
一致性
事务执行的结果必须使得数据库从一个一致性状态变成另一个一致性状态。事务成功提交结果,数据库处于 一致性状态,事务执行中断而改未完成的事务对数据库所做的修改已经被写入数据库,此时数据库处于一种不一致额状态。
原子性持久性隔离性的目标就是为了维护一致性,一致性是由用户和MySQL共同决定的。是事务维护的最终目标。
读读并发
不存在任何问题
写写并发
有线程安全问题,存在更新丢失问题,串行化解决。
读写并发
多版本并发控制MVCC是一种解决读写冲突的无锁并发控制。
每一个事务启动时都会有一个id
,读写并发实际上只需要考虑访问同一条记录时读写操作并发的问题。
可通过事务id区分那一个事务先来的,所以肯定有先有后的再怎么并行。
三个隐藏列字段
DB_ROW_ID
6字节隐含的自增主键,innodb会自动产生一个聚簇索引。
DB_TRX_ID
6字节记录最近修改事务ID,创建这条记录修改该记录的事务ID。
DB_ROLL_PTR
7字节,回滚指针,指向这条记录的上一个被修改的版本。
flag
:标识该记录是否有效,记录被更新或删除并不代表真的删除,而是删除flflag变了 。
undo log
mysql是以服务进程的方式在内存中运行,索引,事务,隔离性等都是在内存中完成的,即在mysql内部相关换乘区中保存相关数据完成判断操作,然后再合适的时候将相关数据刷新到磁盘中。简单理解为MySQL中的一段内存缓冲区,用来保存日志数据。
模拟MVCC版本链
首先创建一张表:student
mysql> create table if not exists student(
name varchar(11) not null,
age int not null
);
mysql> insert into student (name, age) values ('张三', 21);
Query OK, 1 row affected (0.05 sec)
虽然值创建了name 和age,但是还是会维护三个隐藏字段:
如果此时来了一个id=250的事务想要修改表中的内容,那么先写时拷贝将数据先暂存在undo log缓冲区,然后做相关字段的填充,依次类推形成一个历史版本链。
上面的一个一个版本,我们可以称之为一个一个的快照。 这样,我们就有了一个基于链表记录的历史版本链 。
回滚指针就是将这个版本连打上标签就行了。所谓的回滚,无非就是用历史数据,覆盖当前数据 。
总结
delete删除操作,不是将数据清空而是将flag标志设置为删除状态,也可以形成版本放到undo_log中。
如果是insert
呢?因为insert
是插入,也就是之前没有数据,那么insert
也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中,在他之前设置一个空版本,只要便于回滚就行。
如果当前事务commit了,那么这个undo log 的历史insert记录就可以被清空了。避免只进不出导致undolog内存临时缓冲区挤满。新插入说明以前没有你这个的历史版本,也没人用历史版本,所以你删除了历史版本留下最新版本就行了。
update修改delete删除的是可能存在历史版本的,你用别人也可能正在用,所以你不能删除历史版本,直到访问的事务都退出了才可以删除。
首先,select
不会对数据做任何修改,所以,为select
维护多版本,没有意义。不过,此时有个问题,就是:select读取,是读取最新的版本呢?还是读取历史版本? update修改必须是最新版本,select查找不会修改,所以可查当前记录或者历史版本。事务即使没有提交,修改操作就是将数据修改了,已经形成了历史版本链。
修改数据时修改的都是最新的版本,RR可重复读级别就是另一个事务看到的版本和我修改的版本并不一样,看到的是老版本,一种隔离性的实现理解。隔离级别的本质:让你看到哪一个历史版本。
读取最新的记录叫当前读,读取之前的版本叫做快照读。增删改,都叫做当前读,select也有可能当前读,只是不形成版本罢了。
在多个事务同时删改查的时候,都是当前读,是要加锁的。那同时有select过来,如果也要读取最新版(当前读),那么也就需要加锁,这就是串行化。但如果是快照读,读取历史版本的话,是不受加锁限制的 ,因为能访问快照读的一定是select,已经是历史版本就说明是不可修改的了。换言之,提高了效率,即MVCC的意义所在。
- 那为什么要有隔离级别呢? 事务都是原子的。所以,无论如何,事务启动之后,ID都是被确定的了,事务总有先有后。
那么多个事务在执行中,CURD操作是会交织在一起的。那么,为了保证事务的“有先有后”,是不是应该让不同的事务看到它该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题。
- 那么,如何保证,不同的事务,看到不同的内容呢?也就是如何实现隔离级别?
ReadView
mysql会为事务管理形成的结构体,里面有一个指针指向另一个对象,就是readview。
事务进行快照读的操作时的快视图在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个历史版本的数据。 事务在存在并发执行的。
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
};
我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的 DB_TRX_ID
。
那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的 DB_TRX_ID
。
所以现在的问题就是,当前快照读,应不应该读到当前版本记录。用上下limit_id和链表中的修改版本的事务id进行对比,就能保证我的可见性。李白的诗词我看的到,我孙子写的我现在还看不到。我看到的一定是之前已经有的。当你的事务到来的时候,形成事务ID,你能看到的内部的各种数据,就要被确定下来。
实验
创建一张表,然后先后有四个事务几乎同时开始进行访问这张表。
当事务2进行快照读,数据库为他生成了一个readview读视图如下:
m_ids; //1,3
up_limit_id; //1
low_limit_id;//4+1=5,因为这个是系统指定的readview生成时刻尚未分配的下一个事物的id
creator_trx_id;//2
只有事务4修改过记录,并且是在事务2 形成快照读之前就提交了事务。
事务2在快照读该记录时会拿着事务4的DB_TRX_ID和up_limit_id,low_limit_id和活跃事务列表(trx_list)进行比较确定事务2能看到的该记录的版本。而事务4提交的记录对应的事务id DB_TRX_ID=4;
4>1&&4<5,所以事务4并不是在我形成快照前后提交的,只能是在我形成快照时提交的,m_ids中也没有4,说明事务4不在当前的活跃事务列表中。
所以,事务4应该看到,并且是事务2能读到的最新的数据版本记录,也是全局角度最新的版本。
- 以上实验是RC级别,并行的事务提交了你能看到。
RR和RC本质区别
并不是在事务begin的时候创建readview,而是在首次select之后才开始创建readview,undolog前后创建的区别导致能不能看到修改的内容也就是历史版本的在两个事物创建readview先后问题。
两个事务分别select,底层此时分别为他俩各自形成快照读。
例一:实现效果是RR级别
事务A操作 | 事务A描述 | 事务B描述 | 事务B操作 |
---|---|---|---|
begin | 开启事务 | 开启事务 | begin |
select*from stu | 快照读 | 快照读 | select*from stu |
update age=18 | 更新值age=18 | ||
commit | 提交事务 | ||
快照读B看到的是age并未更新(之前形成的readview确定了已经) | select*from stu | ||
看当前读,才看到age=18或者先把Bcommit再快照读不过是新的了 | select *from stu lock in share mode |
例2:实验效果是RC级别
事务A操作 | 事务A描述 | 事务B描述 | 事务B操作 |
---|---|---|---|
begin | 开启事务 | 开启事务 | begin |
select*from stu | 快照读 | ||
update age=28 | 更新值age=28 | ||
commit | 提交事务 | ||
select快照读age=28 | select*from stu | ||
看当前读,才看到age=28 | select *from stu lock in share mode |
结论:
- 正是事务B的Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同。
- 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见。本质就是不对压入undolog中的位置进行更新了。
- 而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因,总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;本质就是持续对Readview的位置进行更新。
- 而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。
- 正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题,因为每次看到的东西可能都不一样,或做出修改的或是没做出修改的造成不可重复读问题。