MVCC ---1.Mysql事务隔离

1.ACID

1 原子性(atomicity):一个事务被事务不可分割的最小工作单元,要么全部提交,要么全部失败回滚。
2一致性(consistency):数据库总是从一致性状态到另一个一致性状态,它只包含成功事务提交的结果
3 隔离型(isolation):事务所做的修改在最终提交一起,对其他事务是不可见的
4 持久性(durability):一旦事务提交,则其所做的修改就会永久保存到数据库中

2.隔离级别对应的问题

1.SQL标准的情况下


MVCC ---1.Mysql事务隔离_第1张图片

2.这四种事务隔离级别是指定的SQL标准,InnoDB默认的隔离级别是REAPEATABLE READ,但与其他数据库不同的时,它同时使用了Next-Key-Lock锁的算法,能够避免幻读的产生,因此能够完全满足事务的隔离性要求,即达到SERIALIZABLE隔离级别。

3. MVCC

Multiversion concurency control(多版本并发控制)

并发访问(读或写)数据库时,对正在事务内处理的数据做多版本的管理。以达到用来避免写操作的堵塞,从而引发读操作的并发问题。
  
默认添加的字段
DB_TRX_ID 表示最后一个事务的ID,每开启一个新事务,事务的版本号就会递增;
DB_ROLL_PTR 指向当前记录项的undo log信息;
DB_ROW_ID 标识插入的新的数据行的id。(如果存在主键或是非NULL唯一键,该字段非必须)

MVCC的逻辑流程

其实在每个表中都会加有隐藏字段,其中这里用到的有两个:
DB_TRX_ID(数据行的版本号)、DB_ROLL_PT(删除版本号)
还有一点需要提前说的是,系统有一个全局的事务id,每开启一个事务,都有他自己对应的事务id,自增。
然后下面会以一个流程的的形式对表进行几条数据的操作,想了解的,最好不要跳过

默认的隔离级别(REPEATABLE READ)下
1.SELECT
读取创建版本小于或等于当前事务版本号,并且删除版本为空或大于当前事务版本号的记录。这样可以保证在读取之前记录是存在的。
2.INSERT
将当前事务的版本号保存至行的创建版本号
3.UPDATE
新插入一行,并以当前事务的版本号作为新行的创建版本号,同时将原记录行的删除版本号设置为当前事务版本号
4.DELETE
将当前事务的版本号保存至行的删除版本号

插入
MVCC ---1.Mysql事务隔离_第2张图片
image.png

  首先我们有一个空表teacher,表结构如图
  然后开启一个事务,插入两条数据,并提交。假设现在的全局事务id为1,
  新插入数据的 行版本号为当前事务的id,删除版本号为null。

删除
MVCC ---1.Mysql事务隔离_第3张图片
image.png

再次开启一个事务,删除id为2的数据,并提交。假定此时的全局事务id为22,
  删除的数据 的删除版本号修改为当前的事务id

更新
MVCC ---1.Mysql事务隔离_第4张图片
image.png

再次开启事务(开不开的吧,反正都会自动开启并提交),将id为1的数据的name修改为19

假定此时的事务id已经到了33

那么他会先将这条数据copy一份,修改原始数据的 删除版本号 为当前的事务id

新的数据的 行版本号 为当前的事务id,并修改对应的数据

查询
MVCC ---1.Mysql事务隔离_第5张图片

假定当前的事务id已经到了44。然后在其查询的时候是存在一些查询规则的:
如果感觉这个规则不太明白的,可以在重新过一遍上面的增删改;
如果感觉已经明白了的,可以继续向下看案例,可能会让你又不明白了。

1、查找数据行版本号等于或早于当前事务版本号的数据行
   也就是说,在我查询的时候,这条数据已经存在了,或者是在我本事务中select之前的数据修改
2、查找删除版本号要么为null,要么大于当前事务版本号的记录。
   也就是说,在我开启事务并查询的时候,这条数据还没有被删除。

MVCC案例分析

但其实呢,这样的MVCC,还是有一定的问题的。
  我们从下面的两个案例来分析一波:
  首先准备两条数据,然后两个事务并发执行

1234,分别表示两个事务中的开始和执行查询的语句,在两个案例中均未执行提交操作

MVCC ---1.Mysql事务隔离_第6张图片
案例一(1,2,3,4,2)
MVCC ---1.Mysql事务隔离_第7张图片

1、先插入就两条数据,行版本号 = 事务id=1
   2、开启一个事务,并执行查询,事务id=2时查询到了两条数据,
   3、开启一个事务,并执行修改操作(这个动作不明白的参考上面的规则),此时的事务id=3
   4、再次执行事务id=2中的查询操作,查询到的数据还是原来的数据
    1)查找数据行版本号等于或早于当前事务版本号的数据行,抛弃表格中的 第三条数据。
    2)查找删除版本号要么为null,要么大于当前事务版本号的记录。两条数据都符合,不变,得到两条数据
  通过第一个案例,我们发现,MVCC的规则,没毛病。

案例二(3,4,1,2,)

1、先插入就两条数据,行版本号 = 事务id=1
   2、开启一个事务,执行修改操作,此时的事务id=2
   3、开启一个事务,执行查询操作,此时的事务id=3
    1)查找数据行版本号等于或早于当前事务版本号的数据行,全部保留
    2)查找删除版本号要么为null,要么大于当前事务版本号的记录。抛弃第一个数据,得到后两条数据。

但,此时就有了些问题。
   因为此时咱们两个事务是并发执行的,都还没有进行提交操作,我第二个事务读到了第一个事务的操作,这显然非常不合理。

4 .undo、redo、快照读、当前读 、binlog [ 待研究 ]

先来抛一些概念,能理解的最好,不理解的可以结合下面的图来进一步吸收一下

Undo Log

undo log 是在事务开始前,在操作任何数据之前,首先将需要操作的数据备份到一个地方(Undo Log)
实际上,undo log是为了实现事务的原子性而出现的一个产物。

事务在执行的过程中,如果出现了错误或者用户执行rollback语句时,mysql可以利用undo log中备份的信息将数据恢复到事务开始之前的状态。

在innodb引擎中,undo log被借用来实现 多版本并发控制

事务未提交前,undo保存了未提交之前的版本数据,undo中的数据可作为数据旧版本快照供其他并发事务进行快照读。

Redo Log

redo log 是在事务中操作的任何数据,将最新的数据备份到一个地方(Redo Log)
  redo log的持久
  redo log的持久不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo中。具体的落盘策略可以进行配置
  redo log是为了实现事务的持久性而出现的产物
  防止在发生故障的时间点,尚有赃页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的未入磁盘数据进行持久化这一特性

快照读

快照读的 sql读取的数据就是快照版本,也就是历史版本,普通的select就是快照读innodb快照读,数据的读取将由cache(原本数据)+ undo(事务修改过的数据)两部分组成当前读当前读的 sql读取的数据是最新版本。通过锁机制来保证读取数据无法通过其他事务进行修改

UPDATE、DELETE、INSERT、SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE 都是当前读
  下面咱们来看图吸收一下上面的东西。

MVCC ---1.Mysql事务隔离_第8张图片

当我们需要对某一个表进行操作的时候,会先将teacher.ibd的数据加载到内存(cache)中。

当我们开启一个事务之后,会将当前事务涉及到的数据,备份到undo buffer中,如果需要回滚的时候,会通过buffer恢复到teacher中。

binlog 待研究...

5.视图

读已提交和可重复读都有视图概念的,读已提交获取的是最新提交的视图,可重复读在事务启动的时候就开启,保证事务内读到的数据是一样的,比如一个事务 执行了两次 select city from tb_user where id = 100 ,中间有一个新的事务执行了修改操作,对于可重复读,两次查询结果都是一样的,对于读已提交,两次结果就不一样了。

  • ReadView中包含4个主要内容:

(1) m_ids:生成当前ReadView时,当前系统中的活跃事务列表
(2)min_trx_id:生成当前ReadView时,当前当前系统中最小的活跃事务Id,即m_ids中的最小值
(3)max_trx_id:生成当前ReadView时,系统应该给下一个事务分配的id值
(4)creator_trx_id:生成当前ReadView的事务Id

注意:max_trx_id并不是m_ids中的最大值,事务ID是递增分配的。

例如: 比如m_ids中有1,2,3三个活跃事务,之后事务3提交了,那么生成新的ReadView时,m_ids中只有1,2两个事务,而max_trx_id=4

  • 一致性读(consistent read)

对于读已提交和可重复读两种隔离级别,可以借助ReadView,按照如下规则实现一致性读:
1.如果被访问版本的trx_id与creator_trx_id相同,则表示当前事务正在访问自己修改的版本,该版本数据对当前事务可见;
2.如果被访问版本的trx_id小于min_trx_id,则表示生成该ReadView时,当前版本的数据已经提交,该版本数据对当前事务可见;
3.如果被访问版本的trx_id大于max_trx_id,表示生成该ReadView时,该版本的事务还未开启,该版本的数据对当前事务不可见;
4.如果被访问版本的trx_id大于min_trx_id,小于max_trx_id,就需要判断当前版本的trx_id是否在m_ids中。
如果在,则表示当前版本还为提交,该版本数据不可用;
如果不在,则表示该版本数据已经提交,该版本数据可以被当前事务访问

6.死锁

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。死锁示例:

事务一 

start transaction;
update account set money=10 where id=1;
update account set money=20 where id=2;
commit;

-事务二
start transaction;
update account set money=10 where id=2;
update account set money=20 where id=1;
commit;

假设碰巧,事务一和事务二同时执行完第一个update语句,接着准备执行第二条update语句,
却发现记录已被对方锁定,然后2个事务都等待对方释放资源,同时持有对方需要的锁,
这样就会出现死循环。

为了避免死锁问题,数据库实现了各种死锁检测和死锁超长机制,
InnoDB处理死锁的方式是:将持有最少行级排他锁的事务进行回滚。

7.快照读+当前读

快照读:读取的是快照版本,也就是历史版本
当前读:读取的是最新版本

8.锁的分类

共享锁(S Lock)
排他锁(X Lock)
意向共享锁(Intention S Lock)
意向排他锁(Intention X Lock)

S锁和X锁都是行锁,而IS锁和IX锁都为意向锁,属于表锁。
  • 1.Shared Locks(共享锁/S锁)
    若事务T对数据对象A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

  • 2.Exclusive Locks(排它锁/X锁)
    若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。

注意:排他锁会阻止其它事务再对其锁定的数据加读或写的锁,但是不加锁的就没办法控制了。

  • 3.Record Locks(行锁 又叫记录锁)
    行锁,顾名思义,是加在索引行(对!是索引行!不是数据行!)上的锁。比如select * from user where id=1 and id=10 for update,就会在id=1和id=10的索引行上加Record Lock。

  • 4.Gap Locks(间隙锁)
    间隙锁,它会锁住两个索引之间的区域。比如select * from user where id>1 and id<10 for update,就会在id为(1,10)的索引区间上加Gap Lock。

  • 5.Next-Key Locks(间隙锁)
    也叫间隙锁,它是Record Lock + Gap Lock形成的一个闭区间锁。比如select * from user where id>=1 and id<=10 for update,就会在id为[1,10]的索引闭区间上加Next-Key Lock。
    这样组合起来就有,行级共享锁,表级共享锁,行级排它锁,表级排它锁。

  • 意向锁的存在意义就是加速完成对锁互斥关系的检测。

假设一个事务A对一个表只是添加了行锁,而另一个事务B要添加表锁,
那么事务B就必须要对表中的每一行进行检查,这样就太浪费时间了。
所以引入意向锁,事务A添加行锁的时候,系统会自动为表添加意向锁,
事务B只需要检测表的意向锁,就能决定是否满足锁条件。

值得注意的是,意向锁之间并不会产生冲突,意向锁更像是一个标志位,让事务判断自己想要手动添加的锁是否被冲突。

加锁的时机
在数据库增删改查四种操作中,insert、delete和update都是会加排它锁(Exclusive Locks)的,而select只有显式声明才会加锁:

select: 即最常用的查询,是不加任何锁的
select ... lock in share mode: 会加共享锁(Shared Locks)
select ... for update: 会加排它锁

一致性非锁定读和锁定读
  • 锁定读

在一个事务中,标准的SELECT语句是不会加锁,但是有两种情况例外。
SELECT ... LOCK IN SHARE MODE 和 SELECT ... FOR UPDATE。

SELECT ... LOCK IN SHARE MODE

给记录假设共享锁,这样一来的话,其它事务只能读不能修改,直到当前事务提交
SELECT ... FOR UPDATE
给索引记录加锁,这种情况下跟UPDATE的加锁情况是一样的

  • 一致性非锁定读

consistent read (一致性读),InnoDB用多版本来提供查询数据库在某个时间点的快照。如果隔离级别是REPEATABLE READ,那么在同一个事务中的所有一致性读都读的是事务中第一个这样的读读到的快照;如果是READ COMMITTED,那么一个事务中的每一个一致性读都会读到它自己刷新的快照版本。Consistent read(一致性读)是READ COMMITTED和REPEATABLE READ隔离级别下普通SELECT语句默认的模式。一致性读不会给它所访问的表加任何形式的锁,因此其它事务可以同时并发的修改它们。

你可能感兴趣的:(MVCC ---1.Mysql事务隔离)