一致非锁定读

InnoDB

利用MVCC实现高并发,利用Next-Key Lock解决幻读

一致性非锁定读,方法:MVCC

  1. 一致性非锁定读是InnoDB存储引擎,通过多版本并发控制MVCC的方式,来读取当前执行时间数据库中的数据
  2. 如果被读的数据行被加了排他锁,在读取这行数据的时并不会等待锁释放,而是读取该行的一个快照数据。不需要等待被访问行的X锁的释放
    1. 快照数据是指修改行之前的数据版本,通过多版本并发控制MVCC实现
  3. 非锁定读的方式极大提高了数据库的并发性。在InnoDB存储引擎中,这是默认的读取方式。

快照读数据定义

快照数据是当前行数据的一个历史版本,每行记录可能有多个版本,由多版本并发控制(Multi Version Concurrency Control,MVCC)实现

在READ COMMITED和REPEATABLE READ下,对快照数据的定义不同

  1. READ COMMITTED事务隔离级别下:对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据,可以读到另一个事务已经提交了的快照数据
  2. REPEATABLE READ事务隔离级别下:对于快照数据,非一致性读总是读取事务开始前的数据

Innodb检查每行数据,确保他们符合两个标准: 1.InnoDB只查找版本早于当前事务版本的数据行(也就是数据行的版本必须小于等于事务的版本),这确保当前事务 读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行 2.行的删除操作的版本一定是未定义的或者大于当前事务的版本号。确定了当前事务开始之前,行没有被删除

当前读

  1. 读取的是记录数据的最新版本,并且当前读返回的记录都会加上行锁,保证其他事务不会再并发的修改这条记录
    1. SELECT … LOCK IN SHARE MODE
    2. FOR UPDATE

串行化隔离级别

  1. 串行化隔离级别,InnoDB对每个select 读语句自动加上共享锁,对一致性非锁定读不再支持,一般用在分布式事务
  2. InnoDB存储引擎默认的事物隔离级别REPEATABLE READ,使用Next-Key Lock,本地事务时即可避免幻读

解决幻读 间隙锁、Next-Key Lock

幻读问题是指一个事务的两次不同时间的相同查询返回了不同的的结果集

隔离级别是可重复读,且默认的 innodb_locks_unsafe_for_binlog=0

MySQL InnoDB支持三种行锁定方式:

  1. 行锁(Record Lock):实现方式:通过给索引上的索引项加锁来实现,锁直接加在索引记录上面
    1. 如果InnoDB扫描的是一个主键、或唯一索引,InnoDB只会采用行锁方式来加锁,不加间隙锁
  2. 间隙锁(Gap Lock):键值可能在条件范围内,但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁
    1. 锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间
    2. 间隙锁在InnoDB的唯一作用就是防止其它事务的插入操作,防止幻读的发生,间隙锁不分共享锁与排它锁
  3. Next-Key Lock:行锁与间隙锁组合起来用就叫做Next-Key Lock

Next-Key Lock

InnoDB工作在可重复读隔离级别下,并且以Next-Key Lock的方式对数据行进行加锁,可以有效防止幻读的发生

当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上行锁(Record Lock) a=8

再对索引记录两边的间隙加上间隙锁(Gap Lock),其它事务不能在这个间隙插入记录 5~11

间隙

select * from t where a = 8 for update;

上面索引值有1,3,5,8,11,其记录的GAP的区间如下:是一个左开右闭的空间(原因是默认主键的有序自增的特性,结合后面的例子说明)

(-∞,1],(1,3],(3,5],(5,8],(8,11],(11,+∞)

InnoDB还会对辅助索引下一个键值加上gap lock

该SQL语句锁定的范围是(5,8],下个下个键值范围是(8,11],所以插入5~11之间的值的时候都会被锁定,要求等待。即:插入5,6,7,8,9,10 会被锁住。插入非这个范围内的值都正常

repeatable read:

  1. 在repeatable read隔离级别下,mysql不仅解决了不可重复读,还通过gap锁的引入,解决了幻读的问题。
  2. 如果事务2对某些数据作了更新,事务1通过快照读不会读取到数据变化,所以repeatable read隔离级别解决了不可重复读的问题
  3. 同时由于引入了gap锁,事务2也无法在事务1采用当前读的前提下在事务1的查询条件满足范围内插入新的数据,所以记录数量不会发生变化,也就不存在幻读问题

举个例子:

表task_queue

Id           taskId

1              2

3              9

10            20

40            41

 

开启一个会话: session 1

sql> set autocommit=0;

   ##

取消自动提交

 

sql> delete from task_queue where taskId = 20;

sql> insert into task_queue values(20, 20);

 

在开启一个会话: session 2

sql> set autocommit=0;

   ##

取消自动提交

 

sql> delete from task_queue where taskId = 25;

sql> insert into task_queue values(30, 25);

 

在没有并发,或是极少并发的情况下, 这样会可能会正常执行,在Mysql中, 事务最终都是串行执行, 但是在高并发的情况下, 执行的顺序就极有可能发生改变, 变成下面这个样子:

sql> delete from task_queue where taskId = 20;

sql> delete from task_queue where taskId = 25;

sql> insert into task_queue values(20, 20);

sql> insert into task_queue values(30, 25);

 

这 个时候最后一条语句:insert into task_queue values(30, 25); 执行时就会爆出死锁错误。因为删除taskId = 20这条记录的时候,20 --  41 都被锁住了, 他们都取得了这一个数据段的共享锁, 所以在获取这个数据段的排它锁时出现死锁。

 

 

MVCC:多版本并发控制(MVCC,Multiversion Currency Control)

一般情况下,事务性储存引擎不是只使用表锁,行锁,而是结合了MVCC机制,以处理更多的并发问题。Mvcc处理高并发能力最强

2.MVCC 实现的依赖项

MVCC 在mysql 中的实现依赖的是 undo log 与 read view。

1.undo log: undo log中记录的是数据表记录行的多个版本,也就是事务执行过程中的回滚段,其实就是MVCC 中的一行原始数据的多个版本镜像数据。 2.read view: 主要用来判断当前版本数据的可见性。 通过数据表版本号实现,见已整理乐观锁的过程

在innodb中,创建一个新事务的时候,innodb会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view)

副本中保存的是系统当前不应该被本事务看到的其他事务id列表。

当用户在这个事务中要读取该行记录的时候,innodb会将该行当前的版本号与该read view进行比较

 

3.undo log

undo log用来做回滚,具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘

undo buffer与redo buffer一样,也是环形缓冲,但当缓冲满的时候,undo buffer中的内容会也会被刷新到磁盘;与redo log不同的是,磁盘上不存在单独的undo log文件,所有的undo log均存放在主ibd数据文件中(表空间),即使客户端设置了每表一个数据文件也是如此。

 

MVCC基本原理

MVCC:多版本并发控制(MVCC,Multiversion Currency Control)。一般情况下,事务性储存引擎不是只使用表锁,行加锁的处理数据,而是结合了MVCC机制,以处理更多的并发问题。Mvcc处理高并发能力最强,

但系统开销 比最大(较表锁、行级锁),这是最求高并发付出的代价。

** InnoDB实现MVCC的方法是,它存储了每一行的三个额外的隐藏字段:**

1.DB_TRX_ID:一个6byte的标识,每处理一个事务,其值自动+1 #下面提到的“创建时间”和“删除时间”记录的就是这个DB_TRX_ID的值 #如insert、update、delete操作时,删除操作用1个bit表示。 #DB_TRX_ID是最重要的一个,可以通过语句“show engine innodb status”来查找 2.DB_ROLL_PTR: 大小是7byte,指向写到rollback segment(回滚段)的一条undo log记录 (update操作的话,记录update前的ROW值) 3.DB_ROW_ID: 大小是6byte,该值随新行插入单调增加。 #当由innodb自动产生聚集索引时聚集索引(即没有主键时,因为MYSQL默认聚簇表,会自动生成一个ROWID) #包括这个DB_ROW_ID的值, #不然的话聚集索引中不包括这个值,这个用于索引当中。

DB_TRX_ID记录了行的创建的时间删除的时间在每个事件发生的时候,每行存储版本号,而不是存储事件实际发生的时间。每次事物的开始这个版本号都会增加。自记录时间开始,每个事物都会保存记录的系统版本号

依照事物的版本来检查每行的版本号。在insert操作时 “创建时间”=DB_TRX_ID,这时,“删除时间”是未定义的;在update时,复制新增行的“创建时间”=DB_TRX_ID,删除时间未定义,旧数据行“创建时间”不变,

删除时间=该事务DB_TRX_ID;delete操作,相应数据行的“创建时间”不变,删除时间=该事务的DB_ROW_ID;select操作对两者都不修改,只读相应的数据

MVCC结合隔离级别:

1.READ UNCOMMITTED ,不适用MVCC读,可以读到其他事务修改甚至未提交的 2.READ COMMITTED ,其他事务对数据库的修改,只要已经提交,其修改的结果就是可见的, 与这两个事务开始的先后顺序无关,不完全适用于MVCC读, 3.REPEATABLE READ,可重复读,完全适用MVCC,只能读取在它开始之前已经提交的事务对数据库的修改, 在它开始以后,所有其他事务对数据库的修改对它来说均不可见 4.SERIALIZABLE ,完全不适合适用MVCC,这样所有的query都会加锁,再它之后的事务都要等待

MVCC只工作在REPEATABLE READ和READ COMMITED隔离级别下

2 REPEATABLE READ 可重复读下的MVCC

##### SELECT

Innodb检查每行数据,确保他们符合两个标准: 1.InnoDB只查找版本早于当前事务版本的数据行(也就是数据行的版本必须小于等于事务的版本),这确保当前事务 读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行 2.行的删除操作的版本一定是未定义的或者大于当前事务的版本号。确定了当前事务开始之前,行没有被删除   符合了以上两点则返回查询结果。

INSERT

InnoDB为每个新增行记录当前系统版本号作为创建ID。

DELETE

InnoDB为每个删除行的记录当前系统版本号作为行的删除ID。

UPDATE

InnoDB复制了一行。这个新行的版本号使用了系统版本号。它也把系统版本号作为了删除行的版本。

3 MVCC深入

如果根据事务DB_TRX_ID去比较获取事务的话,按道理在一个事务B(在事务A后,但A还没commit)select的话 B.DB_TRX_ID>A.DB_TRX_ID则应该能返回A事务对数据的操作以及修改。那不是和前面矛盾?其实不然。

InnoDB每个事务在开始的时候,会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),然后一致性读去比较记录的tx id的时候,并不是根据当前事务的tx id,而是根据read view最早一个事务的tx id(read view->up_limit_id)来做比较的,这样就能确保在事务B之前没有提交的所有事务的变更,B事务都是看不到的。当然,这里还有个小问题要处理一下,就是当前事务自身的变更还是需要看到的。

READ-COMMITTED 与 REPEATABLE-READ区别

都依赖于read view、 undo log

read view: 主要用来判断当前版本数据的可见性。 通过数据表版本号实现,见已整理乐观锁的过程

在innodb中,创建一个新事务的时候,innodb会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),副本中保存的是系统当前不应该被本事务看到的其他事务id列表。当用户在这个事务中要读取该行记录的时候,innodb会将该行当前的版本号与该read view进行比较。

undo log

undo log是为回滚而用,把所有没有COMMIT的事务回滚到事务开始前的状态

具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘

undo buffer与redo buffer一样,也是环形缓冲,但当缓冲满的时候,undo buffer中的内容会也会被刷新到磁盘;与redo log不同的是,磁盘上不存在单独的undo log文件,所有的undo log均存放在主ibd数据文件中(表空间),即使客户端设置了每表一个数据文件也是如此。

redo log 重做日志信息 先写入 重做日志缓冲 再按一定条件顺序写入重做日志文件!

所有对页面的修改操作写入一个专门的文件,在回放日志的时候把已经COMMIT的事务重做一遍

 

事务隔离级别的影响

 tx_isolation='READ-COMMITTED': 语句级别的一致性:只要当前语句执行前已经提交的数据都是可见的。

 tx_isolation='REPEATABLE-READ'; 语句级别的一致性:只要是当前事务执行前已经提交的数据都是可见的。

针对这两张事务的隔离级别,使用相同的可见性判断逻辑是如何做到不同的可见性的呢

不同隔离级别下read view的生成原则

1. read-commited: 每次执行重新创建read_view

在每次语句执行的过程中,都关闭read_view, 重新在row_search_for_mysql函数中创建当前的一份read_view

这样就可以根据当前的全局事务链表创建read_view的事务区间,实现read committed隔离级别

2. repeatable read:执行之前创建read_view

  在repeatable read的隔离级别下,创建事务trx结构的时候,就生成了当前的global read view

  使用trx_assign_read_view函数创建,一直维持到事务结束,这样就实现了repeatable read隔离级别

正是因为read view 生成原则不同,导致在不同隔离级别()下,read committed 总是读最新一份快照数据

而repeatable read 读事务开始时的行数据版本

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值