mysql中的MVCC

学任何东西都是循序渐进的过程

之前看了这方面的东西,那时候很心急,因此很多东西不能完全理解。今天再次拿出来看

首先学习MVCC我们先要去学习redo log 和undo log这些日志是干嘛的

undo log:

存储了数据先前的版本

我们的mysql事务有一特性称为原子性,undo log就完成了这一特性。它是这样实现的:首先我们将数据读取到内存中,然后在内存中修改数据,在修改任何数据之前,我们都需要备份要修改的数据当前值,然后进行修改,这样就算事务出错,我们可以根据undo log回滚

举例:

假设有A、B两个数据,值分别为1,2。
  A.事务开始.
  B.记录A=1到undo log.
  C.修改A=3.
  D.记录B=2到undo log.
  E.修改B=4.
  F.将undo log写到磁盘。
  G.将数据写到磁盘。
  H.事务提交

我们来模拟一下事务崩溃的过程

A-F:事务崩溃,那么数据未写到磁盘,那么就算事务崩溃不会影响到正常数据

G-H:事务崩溃,数据可能只写了一半或者写完没提交崩溃,这时我们可以根据已经写入磁盘的undo log进行数据回滚

缺点:我们发现每次事务都需要两次io操作,这会很消耗时间,我们假设将数据在内存中缓存一段时间,那么将会很好的解决这个问题,但是新的问题又出现了,就是事务的持久化如何实现?如果我们没有将undo log 和 数据写入磁盘,那么事务提交后崩溃,成功提交的修改操作将会付之东流,因此我们引入了新的日志redo log

redo log:

redolog是用来消除大量io并且能够持久化的辅助日志。它写入的是提交的新数据的备份。举个例子:

  假设有A、B两个数据,值分别为1,2.
  A.事务开始.
  B.记录A=1到undo log.
  C.修改A=3.
  D.记录A=3到redo log.
  E.记录B=2到undo log.
  F.修改B=4.
  G.记录B=4到redo log.
  H.将redo log写入磁盘。
  I.事务提交
我们发现只进行了一次io操作,当提交后系统崩溃了,我们可以根据redolog来恢复修改了的数据,这样将io限制到了一次。提高了不少效率。

那么接下来讲今天的主角MVCC(多版本控制工具):

    为什么需要它:

    我们知道innodb不仅实现了行锁,还实现了意向锁,这样可以减少很多锁的竞争。但是对于当代并发量很大的要求下,如果每次都进行了锁定某行数据,就算是行锁也会等待许多时间

  因此mysql实现了快照读,就是通过保留某一刻的时间点,只能读到之前已提交的事务进行的操作。之后此事务的操作都是通过那一刻的时间点的数据进行操作,别的事务无法干扰。可重复读和读提交内容这两种隔离级别都是依靠此技术实现的

我们要具体说说什么是快照读:

上面所说的时间点并不是真正的时间点,而是一个事务的ID。我们通过所有事务的id来判断建立的快照读该读取哪些数据

我们来说说判断的规则:

参考:http://mysql.taobao.org/monthly/2017/10/01/  淘宝数据库内核月报 - 2017 / 10

    

事务快照是用来存储数据库的事务运行情况。一个事务快照的创建过程可以概括为:

  • 查看当前所有的未提交并活跃的事务,存储在数组中
  • 选取未提交并活跃的事务中最小的XID,记录在快照的xmin中
  • 选取所有已提交事务中最大的XID,加1后记录在xmax中
  • 根据不同的情况,赋值不同的satisfies,创建不同的事务快照

其中根据xmin和xmax的定义,事务和快照的可见性可以概括为:

  • 当事务ID小于xmin的事务表示已经被提交,其涉及的修改对当前快照可见
  • 事务ID大于或等于xmax的事务表示正在执行,其所做的修改对当前快照不可见
  • 事务ID处在 [xmin, xmax)区间的事务, 需要结合活跃事务列表与事务提交日志CLOG,判断其所作的修改对当前快照是否可见,即SnapshotData中的satisfies。

      虽然用的是别的数据库,但是不影响我们的理解,原理是一样的。讲解:

选取未提交并活跃的事务中最小的XID,记录在快照的xmin中——当事务ID小于xmin的事务表示已经被提交,其涉及的修改对当前快照可见  :           这两条是对应的,也就说如果事务id都小于了当前活跃的最小事务,也就是说这些事务必然已经提交了

选取所有已提交事务中最大的XID,加1后记录在xmax中——事务ID大于或等于xmax的事务表示正在执行,其所做的修改对当前快照不可见:            这两条是对应的,如果事务的id都大于了当前最大提交事务, 那么他必须是提交过的事物

事务ID处在 [xmin, xmax)区间的事务, 需要结合活跃事务列表与事务提交日志CLOG,判断其所作的修改对当前快照是否可见,即SnapshotData中的satisfies。:   这条得举个例子:假设事务1,2,3都开启,事务3提交了,1,2未提交,那么此时xmax是4,而xmin是1,2就处于这中间,用上面两种情况无法解决,所以有了这种解决方法,它会有专门的判断方法,这里我们有深究,想了解的同学可以自己深究

这样我们就通过了事务id可以读取到真正的需要的数据

特别要说下:开启事务后第一个select语句才是快照读的地方,不是一开启事务就快照读

rr隔离级别:整个事务只使用第一次select或者自己的本身修改的版本的快照读

rc隔离级别:每次select都生成一个快照读。

那么我们如何得到事务想要的版本呢?

 

我们看一看innodb中的数据结构就能很好的理解了

 InnoDB中数据格式是这样的:

  • 6字节的事务ID(DB_TRX_ID)字段: 用来标识最近一次对本行记录做修改(insert|update)的事务的标识符, 即最后一次修改(insert|update)本行记录的事务id。
    至于delete操作,在innodb看来也不过是一次update操作,更新行中的一个特殊位将行表示为deleted, 并非真正删除
  • 7字节的回滚指针(DB_ROLL_PTR)字段: 指写入回滚段(rollback segment)的 undo log record (撤销日志记录记录)。
    如果一行记录被更新, 则 undo log record 包含 '重建该行记录被更新之前内容' 所必须的信息。
  • 6字节的DB_ROW_ID字段: 包含一个随着新行插入而单调递增的行ID, 当由innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。
    结合聚簇索引的相关知识点, 我的理解是, 如果我们的表中没有主键或合适的唯一索引, 也就是无法生成聚簇索引的时候, InnoDB会帮我们自动生成聚集索引, 但聚簇索引会使用DB_ROW_ID的值来作为主键; 如果我们有自己的主键或者合适的唯一索引, 那么聚簇索引中也就不会包含 DB_ROW_ID 了 。

          

也就是说我们之前说的快照读就是建立在DB_TRX_ID,这个字段存储了事务的id。

在《高性能mysql》中说道每行记录后面保存两个隐藏的列来实现的。一个保存了行的创建时间,一个保存了行的过期时间,根据这个两个隐藏的列来实现。然而我在mysql官方文档中发现并不是这样的。

翻译如下

"在内部,InnoDB为数据库中存储的每一行添加三个字段。6字节DB_TRX_ID字段表示插入或更新该行的最后一个事务的事务标识符。此外,删除在内部被视为更新,其中行中的特殊位被设置为将其标记为已删除。每行还包含一个DB_ROLL_PTR称为滚动指针的7字节 字段。滚动指针指向写入回滚段的撤消日志记录。如果更新了行,则撤消日志记录包含在更新行之前重建行内容所需的信息。6字节DB_ROW_ID字段包含在插入新行时单调增加的行ID。如果 InnoDB自动生成聚簇索引,索引包含行ID值。否则,该 DB_ROW_ID列不会出现在任何索引中。"

也就是说书中两个隐藏的列其实就是DB_TRX_ID字段,它表示了插入或者更新该行的最后一个事务标识。然而删除在内部被视为更新。在这个字段中有一个特殊位用来标记是否为已经删除了。我们还是看官网的吧。书中有些容易误导人感觉。

接下来的问题是如何找到相应数据,这时我们之前说到的undolog就出现了:在我们每次进行数据操作时,undo都存储了原先的数据,我们就可以通过 这个undo log找到原先的数据,这不就成了先前的版本了?所以叫做多版本控制,也就是我们想要快照读的数据。

如何去找到呢?我们的innodb引擎中的数据行中还有个字段没说DB_ROLL_PTR:这个字段其实是一个指针指向了这行数据被更改前的数据行,我们就可以通过这个指针一直找到相应的数据版本。实际如图:

我们每次对某行进行操作就会产生一个原来的版本,照着这个版本链我们可以找到想要的快照数据。

这样就完成了版本控制。

大致过程就是当我们的事物需要读的时候,会跟着版本链找到适合自己的版本显示。修改时将数据拷贝到内存中修改。

还有个问题就是我们要经常提交事物,如果一直不提交,事务链会很长,占用空间。

 

刚才说了mvcc减去了很多加锁的操作,但是只是读的时候不需要加锁,但是写的时候我们还是要加锁的。看下面的当前读的结束就明白了

当前读:为什么需要这种读呢?

假设你要update一个记录,另一个事务已经delete这条数据并且commit了,这样不是会产生冲突吗,所以你update的时候肯定要知道最新的信息啊。所以当前读都会得到最近的信息并且都会锁定相应的数据,因为我们需要修改这样的数据。

我自己做了个小实验:

打开两个事务,打开后首先都select一下,事务A删除某行,此时事务A读不到删除的数据了,然而B能读到,当我们在事务B修改被A删除了的一行时,此时mysql命令行不动了,等了很久会跳出lock wait exceeded;(锁等待时间超时)

此时我们重来一遍,在B等待锁的时候,A提交,此时B显示提交成功,但是0 rows affected ,说明A更改时会持有锁,直到事务提交,当事务持有锁的时候,其他事务可以照常读,但是不能去修改了。

有哪些当前读?

  • select ... lock in share mode
  • select ... for update
  • insert
  • update
  • delete
  • 这些操作都会上锁,避免出现安全问题

那么快照读就是通过mvcc和undolog实现的。

当前读的实现方式:间隙所和记录锁(行锁)

 

间隙锁:就是锁定那些范围空间内的数据,假设锁定id>3的数据,但是id只有3,4,5,那么4,5和后面的数字都会被锁定,像6,7.。。。,为什么要这样?因为如果我们不锁定没有的数据,我们加入了新的数据id=6,就会出现幻读,因此间隙锁很好的避免了幻读

 

 

部分借鉴:https://segmentfault.com/inbox/1430000016677641

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值