跟我学Mysql之事务篇


声明:这是我在大学毕业后进入第一家互联网工作学习的内容


事务

数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。事务的使用是数据库管理系统区别文件系统的重要特征之一。

事务拥有四个重要的特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),人们习惯称之为 ACID 特性。下面我逐一对其进行解释。

  • 原子性(Atomicity)
    事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。例如,如果一个事务需要新增 100 条记录,但是在新增了 10 条记录之后就失败了,那么数据库将回滚对这 10 条新增的记录。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。

  • 一致性(Consistency)
    指事务将数据库从一种状态转变为另一种一致的的状态。事务开始前和结束后,数据库的完整性约束没有被破坏。例如工号带有唯一属性,如果经过一个修改工号的事务后,工号变的非唯一了,则表明一致性遭到了破坏。

  • 隔离性(Isolation)
    要求每个读写事务的对象对其他事务的操作对象能互相分离,即该事务提交前对其他事务不可见。 也可以理解为多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。这指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。例如一个用户在更新自己的个人信息的同时,是不能看到系统管理员也在更新该用户的个人信息(此时更新事务还未提交)。

注:Mysql 通过锁机制来保证事务的隔离性。

  • 持久性(Durability)
    事务一旦提交,则其结果就是永久性的。即使发生宕机的故障,数据库也能将数据恢复,也就是说事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。这只是从事务本身的角度来保证,排除 RDBMS(关系型数据库管理系统,例如 Oracle、Mysql 等)本身发生的故障。

注:Mysql 使用 redo log 来保证事务的持久性。

事务的隔离级别(Transaction Isolation Levels)

事务隔离是数据库处理的基础之一。隔离是ACID中的I ;隔离级别是一种设置,用于在多个事务同时进行更改和执行查询时微调性能与结果的可靠性,一致性和可重复性之间的平衡。

Mysql/InnoDB 提供SQL标准所描述的所有四个事务隔离级别。

隔离级别脏读不能重复读幻读
未提交读(Read uncommitted)
已提交读(Read committed)不能
可重复读(Repeatable read)不能不能不能
可串行化(Serializable )不能不能不能

四个级别逐渐增强,每个级别解决一个问题。事务级别越高,性能越差,大多数情况都用rc隔离级别。

  • 未提交读(Read Uncommitted):一个事务还未提交,它所做的变更就可以被别的事务看到
  • 提交读(Read Committed):一个事务提交之后,它所做的变更才可以被别的事务看到
  • 可重复读(Repeated Read):同一事务中的一致读取将读取第一次读取时候建立的 快照,InnoDB默认级别。消除了脏读、不可重复读、幻读,保证事务一致性
  • 可串行化(Serializable):隔离级别最高
    串行化读,每次读都需要获得表级共享锁,读写间相互都会阻塞

  • 脏读: 脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
  • 不能重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
  • 幻读:当同一查询在不同时间生成不同的行集时,在事务内就会发生 所谓的幻像问题。如果a事务 SELECT执行两次,但是第二次返回的行却不是第一次返回,则该行是“ phantom”行。

MVCC

InnoDB是一个多版本的存储引擎MVCC( multi-versioned storage engine)它保留有关已更改行的旧版本的信息,以支持诸如并发和回滚之类的事务功能。此信息存储在表空间中的数据结构中,该数据结构称为 回滚段InnoDB 使用回滚段中的信息来执行事务回滚中所需的撤消操作。它还使用该信息来构建行的早期版本,以实现一致的读取。

首先理解,MVCC是InnoDB存储引擎的特性,好处在于可以并发处理事务及回滚事务。

下面举个经典的例子

session 1session 2
start transaction;
select a from test; return a = 10
update test set a = 20;
-start transaction;
-select a from test; return ?
commit;
-select a from test; return ?

我们看下上面这个数据库日常操作的例子。

session 1修改了一条记录,没有提交;与此同时,session 2 来查询这条记录,这时候返回记录应该是多少呢?

session 1 提交之后 session 2 查询出来的又应该是多少呢?

由于Mysql支持多种隔离级别,这个问题是需要看session2的事务隔离级别的,情况如下:

  • 隔离级别为 READ-UNCOMMITTED 情况下:
    无论session 1 是否commit,session 2 去查看都会看到的是修改后的结果,即 a = 20

  • 隔离级别为 READ-COMMITTED 情况下:
    session 1 在 commit 前,session 2查看到的还是 a =10 , commit之后看到的则是 a = 20

  • 隔离级别为 REPEATABLE-READ 及 SERIALIZABLE 情况下:
    无论 session 1 是否commit,session 2 去查看都会看到的是修改前的结果,即 a = 10

其实不管隔离级别,我们也抛开数据库中的ACID,我们思考一个问题:众所周知,InnoDB的数据都是存储在B+tree里面的,修改后的数据到底要不要存储在实际的B+tree叶子节点,session2是怎么做到查询出来的结果还是10,而不是20呢?

在解释上述问题之前,我们需要继续了解4个基本概念

undo log(回滚日志)

Undo log是InnoDB MVCC事务特性的重要组成部分。当我们对记录做了变更操作时就会产生undo记录,Undo记录默认被记录到系统表空间(ibdata)中。

回滚日志分为插入和更新撤消日志。插入撤消日志仅在事务回滚时才需要,并且在事务提交后可以立即将其丢弃。更新撤消日志也用于一致的读取中,但是只有在不存在为其InnoDB分配了快照的事务后,才可以将其删除行。

隐藏字段

在内部,InnoDB向数据库中存储的每一行添加三个字段。

  • 6个字节的DB_TRX_ID字段表示插入或更新该行的最后一个事务的事务标识符
  • 7个字节的DB_ROLL_PTR字段则表示指向该行回滚段的指针,回滚指针指向写入回滚段的撤消日志记录。如果行已更新,则撤消日志记录将包含在更新行之前重建行内容所必需的信息。
  • 6字节的DB_ROW_ID字段包含一个行ID,该行ID随着插入新行而单调增加。如果表里没有主键则系统默认给这个字段上主键和聚簇索引,只不过不能被外部调用。

为什么一个数据只有一个DB_TRX_ID,但是却可以回滚到以前的记录呢,其实就是因为DB_ROLL_PTR和undo log的存在。

如果你需要将某一行回滚到之前的版本则根据当前版本和 undo log 计算出来的。

假设上述例子:

select a from test; return a = 10 DB_TRX_ID=1 DB_ROLL_PTR=Null

update test set a = 20; DB_TRX_ID=2 DB_ROLL_PTR=[2]→[1]

你需要回滚到DB_TRX_ID=1时a的值,操作为:通过DB_TRX_ID=2,a=20 以及DB_ROLL_PTR=[2]→[1]和undo log[2→1]你通过一些算法计算出了 DB_TRX_ID=1时a的值=10 再将结果更新。

colums.jpg

trx_sys(事务链表)

Mysql中的事务在开始到提交这段过程中,都会被保存到一个叫trx_sys的事务链表中,事务链表中保存的都是还未提交的事务,事务一旦被提交,则会被从事务链表中摘除。

trx_sys.jpg

ReadView(一致性视图)

为了方便理解,我把ReadView看做一个数据结构,在SQL开始的时候被创建。这个数据结构中包含了3个主要的成员:ReadView[高水位, 低水位, trx_ids{}]

  • 高水位(low_limit_id) :事务链表(trx_sys)ID最大值+1
  • 低水位(up_limit_id) :事务链表(trx_sys)ID最小值
  • trx_ids:事务链表(trx_sys)中事务的id集合

readview.jpg

readview规则:

1.DB_TRX_ID <low_limit_id 则该行对于当前Read View是可见的

2.DB_TRX_ID >=up_limit_id  则该行对于当前Read View是不可见的

判断某行可不可见需要满足第一条规则,且不满足第二条规则,否则不可见

不满足read view条件时候,从undo log里面获取数据DB_TRX_ID再进行对比,直到找到一个均满足这2个条件的即可

注意,ReadView是与SQL绑定的,而并不是事务,所以即使在同一个事务中,每次SQL启动时构造的ReadView的up_trx_id和low_trx_id也都是不一样的

总结MVCC

MVCC启动步骤:

  • 1.事务启动时, 创建快照; 基于整个库
  • 2.旧数据存储在UNDO中,再通过DB_ROLL_PTR 回溯查找历史版本
    如果当插入的是一条新数据时,记录上对应的回滚段指针为NULL
    如果更新记录时,原记录将被放入到undo表空间中,并通过DB_ROLL_PTR指向该记录。
  • 3.通过read view判断行记录是否可见

mvcc.png

MVCC使得数据库读不会对数据加锁,普通的SELECT请求不会加锁,提高了数据库的并发处理能力;

借助MVCC,数据库可以实现RC,RR等隔离级别,用户可以查看当前数据的前一个或者前几个历史版本。保证了ACID中的I特性(隔离性)。

深入理解事务隔离级别RR和RC的区别

由于RR隔离级别下,在每个事务开始的时候,会将当前系统中的所有的活跃事务拷贝到一个列表中(read view)。

RC隔离级别下,在事务中的每个语句开始时,会将当前系统中的所有的活跃事务拷贝到一个列表中(read view) 。

再举个例子,从MVCC启动步骤来分析。

session 1session 2时刻
insert test(a) values (10)1
start transaction;2
select a from test; return a = 10;3
update test set a = 20;4
-start transaction;5
-select a from test; return ?6
commit;7
-select a from test; return ?8
  • 时刻1 增加一行数据并自动提交,分析这行数据、trx_sys、readview
DB_TRX_IDDB_ROLL_PTRDB_ROW_IDcolum a
1NULL110
trx_sys=[]
  • 时刻2 session 1开启事务
  • 时刻3 select a from test; return a = 10;分析这行数据、trx_sys、readview
DB_TRX_IDDB_ROLL_PTRDB_ROW_IDcolum a
1NULL110
trx_sys=[2] 

注意:查询不会改变的数据行的DB_TRX_ID,但是这条语句本身的DB_TRX_ID会递增,即为2,高低水位是以当前sql的DB_TRX_ID进行计算

readview=[low_limit_id(3),up_limit_id(2),trx_sys{2}]

根据readview规则:

DB_TRX_ID <low_limit_id则该行对于当前Read View是可见的

DB_TRX_ID >= up_limit_id 则该行对于当前Read View是可见的

如果DB_TRX_ID(1) <low_limit_id(3) 则该行对于当前Read View是可见的

满足规则,则可见

如果DB_TRX_ID(1) >= up_limit_id(2) 则该行对于当前Read View是不可见的

不满足规则,则可见

由于规则都可见,所以查询结果为DB_TRX_ID=1时的colum a=10


  • 时刻4 update test set a = 20;分析这行数据、trx_sys、readview、undo log
DB_TRX_IDDB_ROLL_PTRDB_ROW_IDcolum a
2DB_TRX_ID[1]120
trx_sys=[3]→[2]

readview=[low_limit_id(4),up_limit_id(2),trx_sys{3,2}]
  • 时刻5 session 2开启事务
  • 时刻6 select a from test;(获取快照)
  • 分析这行的隐藏列、trx_sys、readview
DB_TRX_IDDB_ROLL_PTRDB_ROW_IDcolum a
2DB_TRX_ID[1]120
trx_sys=[4]→[3]→[2]

readview=[low_limit_id(5),up_limit_id(2),trx_sys{4,3,2}] 

根据readview规则:

DB_TRX_ID <low_limit_id则该行对于当前Read View是可见的

DB_TRX_ID >= up_limit_id 则该行对于当Read View是不可见的

如果DB_TRX_ID(2) <low_limit_id(5) 则该行对于当前Read View是可见的

满足条件,则可见

如果DB_TRX_ID(2) >= up_limit_id(2) 则该行对于当前Read View是不可见的

满足条件,则不可见

结论:由于规则2满足,则该行对于当前Read View是不可见,则通过回滚指针DB_ROLL_PTR=[2]→[1]和undo log[2]→[1]计算出DB_TRX_ID=1时的行数据,则继续分析

如果DB_TRX_ID(1) <low_limit_id(5) 则该行对于当前Read View是一定可见的

满足条件,则可见

如果DB_TRX_ID(1) >= up_limit_id(2) 则该行对于当前Read View是一定不可见的

不满足条件,则可见

结论:则DB_TRX_ID[1]这行对当前read view可见

则查询结果为colum a=10


  • 时刻7 session 1 提交事务
DB_TRX_IDDB_ROLL_PTRDB_ROW_IDcolum
2DB_TRX_ID[1]120

提交事务后会把事务链表已提交的事务id去掉,则当前trx_sys为[4]

trx_sys=[4]
  • 时刻8 select a from test

注意,在此之前都所有结论都试用于rr和rc,而这里不同

在rc的隔离级别下

由于每次查询都是读取到最新的readview,所以本次查询的行数据如下

DB_TRX_IDDB_ROLL_PTRDB_ROW_IDcolum a
2DB_TRX_ID[1]120
trx_sys=[5]→[4]

readview=[low_limit_id(6),up_limit_id(4),trx_sys{5,4}]

根据readview规则:

DB_TRX_ID <low_limit_id则该行对于当前Read View是可见的

DB_TRX_ID >= up_limit_id 则该行对于当Read View是不可见的

如果DB_TRX_ID(2) <low_limit_id(6) 则该行对于当前Read View是可见的

满足条件,则可见

如果DB_TRX_ID(2) >= up_limit_id(4) 则该行对于当前Read View是不可见的

不满足条件,则可见

结论:则DB_TRX_ID[2]这行对当前read view可见

则查询结果为colum a=20


在rr的隔离级别下

由于每次查询都是读取到第一次查询创建的readview,所以本次查询直接读取的是时刻6获取到的readview,即

readview=[low_limit_id(5),up_limit_id(2),trx_sys{4,3,2}]

而通过这个readview计算出来的行数据为colum a=10

则查询结果为colum a=10

总结

可能看到此时,你会觉得有点绕,为什么rc的隔离级别和rr的隔离级别在另外一个事务提交完了之后读取的数值是不一样的呢?

其实看看最上面的我对事务隔离级别的定义就能明白

在rc的隔离级别中,开启一个事务select都会是最新的readview

在rr的隔离级别中,开启一个事务,每个select都会读取第一次查询得到的readview

然后如果对readview的规则还不太理解,我再说一句:

一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:版本未提交,不可见;版本已提交,但是是在视图创建后提交的,不可见;版本已提交,而且是在视图创建前提交的,可见。

我觉得这样讲能更深地理解隔离级别的区别,至于隔离级别的不同是因为他们的底层原理不同:这里简单讲下,rr的隔离级别比rc多了一种锁————间隙锁(Gap Locks)。

间隙锁跟MVCC一起工作。实现事务处理:

在rr隔离级别:采用Gap Locks(间隙锁) 来解决幻读问题

在rc隔离级别:采用Record锁,不会出现脏读,但是会产生"幻读"问题. 也会出现可重复读

关于Mysql锁的概念我会在后面继续深入讲解,在此处就不做过多的深究了。

最后说一句,数据多版本(MVCC)是Mysql实现高性能的一个主要的一个主要方式,通过对普通的SELECT不加锁,直接利用MVCC读取指版本的值。不同的事务访问不同版本的数据快照,从而实现不同的事务隔离级别。虽然字面上是说具有多个版本的数据快照,但这并不意味着数据库必须拷贝数据,保存多份数据文件,这样会浪费大量的存储空间。InnoDB通过事务的undolog巧妙地实现了多版本的数据快照。

参考资料

MVCC原理探究及Mysql源码实现分析

Mysql MVCC实现

关于Mysql锁的概念我会在后面继续深入讲解,在此处就不做过多的深究了。

最后说一句,数据多版本(MVCC)是Mysql实现高性能的一个主要的一个主要方式,通过对普通的SELECT不加锁,直接利用MVCC读取指版本的值。不同的事务访问不同版本的数据快照,从而实现不同的事务隔离级别。虽然字面上是说具有多个版本的数据快照,但这并不意味着数据库必须拷贝数据,保存多份数据文件,这样会浪费大量的存储空间。InnoDB通过事务的undolog巧妙地实现了多版本的数据快照。

参考资料

MVCC原理探究及Mysql源码实现分析

Mysql MVCC实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值