如果一个事务修改了另一个未提交事务的修改过的数据,则称发生了脏写现象
。
举个例子:
假设数据库中有一个数据项为x=2;
事务A修改了x=1;此时事务A并没有提交,事务B此时也来修改数据项x=0;此时事务A,B提交
事务B修改了事务A未提交修改过的数据,所以发生了脏写的现象
如果一个事务读取了另一个事务未提交的数据,则称发生了脏读现象
如果一个事务A读取了x的值,然后事务B又修改了x的值并且提交,事务A再次读取x的值时,则会与第一次x的值不同,则称发生了不可重复读(也就是两次读的结果不一样)
。
注意:如果事务发生了update更新值,delete删除记录,则可以把这种现象叫做不可重复读
当一个事务A读取符合搜索条件的记录数为x,此时另一个事务B往符合条件里插入了一条行数据并提交,此时事务A再去读符合条件的记录数时,则跟原来的不一样,则称发生了幻读
。
注意:如果事务发生了insert插入符合条件的记录,或则是update修改了记录的键值导致插入符合记录数里,则可以把这种现象叫做幻读
SQL标准对这里的不可重复读和幻读有点描述不清,我们一般认为:
幻读强调的是在后读取到了之前没有读取到的数据,这个没有读取到的数据,可能是insert或则update插入的
不可重复读的话,则是updata和delete
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | 可能 | 可能 | 可能 |
READ COMMITTED | 不可能 | 可能 | 可能 |
REPETABLE READ | 不可能 | 不可能 | 可能 |
SERIALIZABLE | 不可能 | 不可能 | 不可能 |
oralce支持两种隔离级别:read committed和serializable
mysql支持虽然支持四种隔离级别,但是mysql的repeatable read隔离级别不允许幻读
实现MVCC关键:隐藏字段,undo log,ReadView
对于聚簇索引记录来讲,每个行格式都包含了两个必须的隐藏列
这里insert log日志在只在事务回滚时起作用,事务提交后该日志文件就被系统回收了(可能是被重用或则释放了)。
虽然insert undo日志被真正回收了,但是回滚指针的值并没有清除,如果roll_point第一个字节是1的话,表示它是一个insert undo日志
现在假设我们的数据库中发生了如下事务:
那么对应的版本链则如下图:
每次事务更新该行时,都会将事务id写入trx_id中。将旧记录写入undo 日志中,每条undo日志中也会有roll_point属性,其他字段只包含索引列和更新的列(insert的undo日志中没有该属性,因为insert没有旧版本)。这些undo日志通过roll_point形成一个版本链。
我们后面会通过该记录的版本链来控制并发事务可以访问到的记录,我们把这种机制叫做MVCC多版本并发控制
注意:这里undo日志并不是全部字段都有,只会包含一些索引列和被更新的字段
比如我们上面的class字段,undo日志里是没有记录这个字段的,因为它并没有被更新,如果该版本没有这个字段,说明它是和上一个版本字段值相同
使用uncommitted read隔离级别时,由于可以读到未提交的值,所以直接读取最新版本的值即可。使用可串行化隔离级别时,使用加锁来访问记录。
对于读已提交和不可重复读这两个隔离级别时,由于不能读取未提交事务的值,所以我们必须控制它可以读取的记录,故而提出了ReadView
的概念
ReadView主要包含以下四个重要的内容:
m_ids
:在生成readview时,当前活跃的事务id
min_trx_id
:当前活跃事务中,id最小的值
max_trx_id
:表示生成该readview时,分配给下一个事务的事务id
注意:max_trx_id并不是m_ids中的最大值,事务id是递增分配的。
- 比如,现在有id为1, 2,3这三个事务,之后id为3的事务提交了。
那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4
creat_trx_id
:创建该readview的事务id
只有执行insert,update,delete操作时,才会给它分配事务唯一id,执行select操作不会给它分配事务id,默认都为0
有了这个readview和版本链后,我们就可以根据这些判断该记录是否可见了:
如果该版本对该事务不可见,则顺着版本链往下找,直到记录可见。如果到最后都没有可见的记录,则说明该记录不可见,不能包含到结果集中
读已提交和可重复读:最大的区别在于生成readview的时机不一样
因为读已提交是不能解决不可重复读的,所以一个事务中两次读取的记录会不一样,所以每执行一个select操作都会生成一个readview。
而可重复读在一个事务中两次读取的记录必须是一样的,所以在该隔离级别下只有第一次select操作会生成一个readview,后续的select操作会复用这个readview
前面我们讲的行记录中都是包含trx_id和roll_point隐藏列的,那么二级索引是没有隐藏列的,那么我们通过二级索引查找记录时,如何判断它的可见性呢???
比如下面这个事务
begin;
select name from user where name=‘刘备’;
此时我们的查询优化器决定了要到二级索引中查找name为‘刘备’的记录,那么该如何判断该记录可见呢??可分为以下两步:
我们知道在insert操作时,事务提交后就可以释放掉了,而进行delete操作和更新键值的updata操作时,并不会立即删除记录,而是给该记录打一个删除标记,其主要就是为了给MVCC服务。那么总不可能一直不删除记录吧!!那存储空间不得爆了!!
处理这些记录的操作我们叫做purge操作
,主要的问题就是什么时候处理???
简单一句话:当系统中最早产生的readview不再访问他们了,那么就可以清理了。那么什么时候才表示肯定不再访问它们了呢?只要在生成readview时,保证某个事务已经提交,就说明该readview不会访问该事务的updata undo日志了
详见《MySQL是怎样运行的》P398