直接起飞~
什么是事务?
事务由一组SQL语句组成的逻辑处理单元。
事务特性?
原子性(Atomicity) :事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。(整体上是一个原子,不可拆分)
一致性(Consistent) :在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规 则都必须应用于事务的修改,以保持数据的完整性。(数据上来说,保证数据在事务内的准确性)
隔离性(Isolation) :数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独 立”环境执行。
这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。(事务与事务之间是分隔的,不可见的)
持久性(Durable) :事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。(所有的事务操作在提交之后都会持久化到磁盘上面)
1.并发情况下,mysql事务会出现的问题
脏写(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题。
(最后事务的更新覆盖了由其他事务所做的更新)
脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;
这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的 处理,
就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。(A事务读到了B事务修改了,但是还未提交的数据不符合隔离性)
幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数 据,
这种现象就称为“幻读”。(A事务读取到了B事务提交的新增数据,不符合隔离性)
不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,
却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。(事务内读取到的相同记录值一直在变动,不利于代码编写,不符合隔离性)
2.事务隔离级别
Mysql默认的事务隔离级别是可重复读,用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔 离级别,如果Spring设置了就用已经设置的隔离级别。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交(read uncommitted) | 可能 | 可能 | 可能 |
读已提交(read committed) | 不可能 | 可能 | 可能 |
可重复读(repeatable read) | 不可能 | 不可能 | 可能 |
串行化(serializable) | 不可能 | 不可能 | 不可能 |
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度 上“串行化”进行,这显然与“并发”是矛盾的。
我们先来看脏写:
数据库使用本地user_bd,里面有一张test表:
这个时候我们在开一个客户端:
给两个客户端开启事务:
我们假设左边的事务叫A事务,右边为B事务;后面都这么叫。
这个时候,我们B事务去更改张三的钱(扣减100):
把B事务提交了,然后A事务再去更改张三的钱(假设我们没有上帝视角,不知道其他事务已经更改了money,扣减200,改为800):
实际上数据库张三的钱被更改成了800;但是我们要的是张三的钱一共扣减300,应该为700;上面情况就是脏写情况。(可以更改的时候写成money=money-100)
2.1读未提交
设置两个客户端隔离级别为读未提交(set tx_isolation='read-uncommitted';):
开启A事务和B事务,我们在B事务对张三的钱进行更改,再用A事务去查询:
我们可以看到,A事务读取到了B事务修改,但是没有提交的数据(脏读情况)。
B事务继续更改张三的钱,然后A事务去查询张三:
张三的钱一直在变动(不可重复读情况)。
我们在看一下存在幻读的情况吗?B事务插入一条数据,然后A事务再去读取,看下是否能够读取到B事务新差人的数据:
A事务一样查到了B事务新增但是还未提交的数据(幻读情况)。
这里求证了上面表格中读已提交的对于各种并发问题下是否解决。
2.2读已提交
设置两个客户端隔离级别为读已提交(set tx_isolation='read-committed';),并且开启事务A和事务B:
我们先来看一下脏读解决了没,B事务更改张三的钱,我们看下A事务读取到的是什么值:
很显然,事务A并没有读取到B事务修改还未提交的数据,读已提交解决了脏读问题;
那么我们继续来看是否解决了不可重复读问题,这个时候我们把B事务提交了,我们再来看下A事务读取到的是什么:
A事务读取的money值在变动,显然是没有解决不可重复读的问题;
那么再看下是否解决了幻读问题呢,B事务开启,然后插入一条数据,A事务去读取:
A事务读取到了B事务新增的数据,显然读已提交并未解决幻读的问题;
2.3可重复读
设置两个客户端隔离级别为可重复读(set tx_isolation='read-committed';),并且开启事务A和事务B:
我们先来看可重复读解决脏读问题了没,B事务去修改张三的值,然后看A事务读取到的是什么:
很显然,A事务并没有读取到B事务修改但是未提交的值,可重复读解决了脏读问题;
继续来看是否解决了不可重复读问题,我们把B事务提交了,这个时候数据库张三的真实值为700,我们在来看A事务读取的是什么:
可以看到,A事务读取到的张三的值一直是900,可重复读解决了不可重复读问题;
我们继续来看幻读问题,我们在把B事务开启,然后在A事务查询是否能找到B事务提交的新增数据:
A事务读取到了B事务新增的数据,可重复读并未解决幻读问题。
问题1:可重复读情况下如何解决幻读问题?
我们可以加上一共间隙锁(锁的就是两个值之间的空隙);
我们给test表在插入一条数据:
然后开启事务A和事务B:
事务A去做一个间隙更改:
在往B事务插入数据看看,会是什么情况?
B事务直接在等待A事务释放锁了,B事务才会插入成功,解决了幻读的情况。如果A事务没有释放锁,B事务就会插入失败:
2.4串行化
设置两个客户端隔离级别为串行化(set tx_isolation='serializable';),并且开启事务A和事务B:
先来看脏读的问题,B事务先去更改张三的钱,A事务去查询:
A事务在等待B事务锁释放了,并没有查询出数据,解决了脏读问题;
继续来看不可重复读的问题,我们把B事务提交了,然后看A事务张三是多少钱:
然后B事务窗口去更改张三的钱,在看A事务读取到张三的钱是多少:
B事务在等待A事务的锁,并没有修改成功,解决了不可重复读问题;
幻读就不看了,上面例子可以看出,在串行化的隔离级别下,只要事务开启了,对数据不管是查询还是其他操作,都会对那条数据加锁,其他事务想去操作这条数据必须等待其他事务锁释放才能操作。
3.MVCC(Multi-Version Concurrency Control)多版本并发控制机制
这次开3个窗口,A,B,C事务,隔离级别为默认的可重复读,进行如下操作:
这个时候在开D窗口,开启事务D(注意,事务id在只读的情况是是不会分配给当前事务的,我们这个D窗口只用来查询):
3.1undo日志版本链与read-view机制详解
undo日志版本链:undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚 日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链。(如下图所示):
read-view(一致性视图):视图由执行查询时所有未提交事 务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成;
上面mvcc开头操作,3个事务,4925为最小的未提交事务id,4926为未提交事务,最大事务id为4929。所以上面操作的read-view为([4925,4926],4929);
undo版本链对比规则:
1. 如果 row 的 trx_id 落在( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
2.如果 row的trx_id落在(trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);
3. 如果 row 的 trx_id 落在(min_id <=trx_id<= max_id),那就包括两种情况
a. 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见,若 row 的 trx_id 就是当前 自己的事务是可见的;
b. 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。
4.对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的 trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被 删除,在查询时按照上面的规则查到对应的记录如果delete_flag标记位为true,意味着记录已被删除,则不返回数 据。
好了,开始D窗口操作:
查询到的张三数据是800;我们通过undo日志+read-view来分析:
接下来,我们对A事务进行操作:
然后更新undo版本:
再用D窗口去查询张三:
通过版本日志分析:
1:先找最新的记录,事务id为4925(落在数组中),属于第三种情况,然后在看4925并未提交,所以属于第三种的a情况,所以,最新的这条数据是不可见的;
2:再找下一条(money为700),事务id也是4925(落在数组中),跟第一种情况是一样的,不在过多赘述;
3:在找下一条(money为800),事务id为4929(不在数组中,并且==max_id),属于第三种情况,再看4929是已经提交了的事务,所以这是第三种的b情况,所以money=800是可见的;
这个时候把A事务提交了:
然后用B事务去更改张三的值:
更新undo日志版本:
我们再用D窗口去查询张三:
通过版本日志分析:
1:先找最新的记录,事务id为4926(落在数组中),属于第三种情况,然后在看4926并未提交,所以属于第三种的a情况,所以,最新的这条数据是不可见的;
2:再找下一条(money为900),事务id也是4926(落在数组中),跟第一种情况是一样的,不在过多赘述;
3:在找下一条(money为600),事务id为4925(落在数组中,并且==min_id),属于第三种情况,再看4925是已经提交了的事务,所以这是第三种的b情况,所以money=600是可见的;
问题2:可重复读隔离级别是怎么实现的?
基于多版本并发控制,mvcc实现;低层为undo版本日志+一致性试图(read-view);
那么为什么可重复读情况下一直是读取重复的值呢?
我们根据undo日志版本想一想就知道了,当前开启的事务去做查询,产生了一致性试图;在后面开启的事务都是大于当前事务id的;对于当前事务来说,是属于大于最大事务id的情况(未来的事务),属于版本判断第二种情况,所以后面的事务无论做出什么修改,我这个值一直是保持这个值。
问题3:读已提交呢?
读已提交隔离级别下,每次查询都会生成新的一致性试图,所以每次读取的都是数据库最新的数据(阅读者自行推演);