声明:这是我在大学毕业后进入第一家互联网工作学习的内容
数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。事务的使用是数据库管理系统区别文件系统的重要特征之一。
事务拥有四个重要的特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),人们习惯称之为 ACID 特性。下面我逐一对其进行解释。
原子性(Atomicity)
事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。例如,如果一个事务需要新增 100 条记录,但是在新增了 10 条记录之后就失败了,那么数据库将回滚对这 10 条新增的记录。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
一致性(Consistency)
指事务将数据库从一种状态转变为另一种一致的的状态。事务开始前和结束后,数据库的完整性约束没有被破坏。例如工号带有唯一属性,如果经过一个修改工号的事务后,工号变的非唯一了,则表明一致性遭到了破坏。
隔离性(Isolation)
要求每个读写事务的对象对其他事务的操作对象能互相分离,即该事务提交前对其他事务不可见。 也可以理解为多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。这指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。例如一个用户在更新自己的个人信息的同时,是不能看到系统管理员也在更新该用户的个人信息(此时更新事务还未提交)。
注:Mysql 通过锁机制来保证事务的隔离性。
注:Mysql 使用 redo log 来保证事务的持久性。
事务隔离是数据库处理的基础之一。隔离是ACID中的I ;隔离级别是一种设置,用于在多个事务同时进行更改和执行查询时微调性能与结果的可靠性,一致性和可重复性之间的平衡。
Mysql/InnoDB 提供SQL标准所描述的所有四个事务隔离级别。
隔离级别 | 脏读 | 不能重复读 | 幻读 |
---|---|---|---|
未提交读(Read uncommitted) | 能 | 能 | 能 |
已提交读(Read committed) | 不能 | 能 | 能 |
可重复读(Repeatable read) | 不能 | 不能 | 不能 |
可串行化(Serializable ) | 不能 | 不能 | 不能 |
InnoDB是一个多版本的存储引擎MVCC( multi-versioned storage engine)它保留有关已更改行的旧版本的信息,以支持诸如并发和回滚之类的事务功能。此信息存储在表空间中的数据结构中,该数据结构称为 回滚段InnoDB 使用回滚段中的信息来执行事务回滚中所需的撤消操作。它还使用该信息来构建行的早期版本,以实现一致的读取。
首先理解,MVCC是InnoDB存储引擎的特性,好处在于可以并发处理事务及回滚事务。
下面举个经典的例子
session 1 | session 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是InnoDB MVCC事务特性的重要组成部分。当我们对记录做了变更操作时就会产生undo记录,Undo记录默认被记录到系统表空间(ibdata)中。
回滚日志分为插入和更新撤消日志。插入撤消日志仅在事务回滚时才需要,并且在事务提交后可以立即将其丢弃。更新撤消日志也用于一致的读取中,但是只有在不存在为其InnoDB分配了快照的事务后,才可以将其删除行。
在内部,InnoDB向数据库中存储的每一行添加三个字段。
为什么一个数据只有一个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 再将结果更新。
Mysql中的事务在开始到提交这段过程中,都会被保存到一个叫trx_sys的事务链表中,事务链表中保存的都是还未提交的事务,事务一旦被提交,则会被从事务链表中摘除。
为了方便理解,我把ReadView看做一个数据结构,在SQL开始的时候被创建。这个数据结构中包含了3个主要的成员:ReadView[高水位, 低水位, trx_ids{}]
readview规则:
1.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使得数据库读不会对数据加锁,普通的SELECT请求不会加锁,提高了数据库的并发处理能力;
借助MVCC,数据库可以实现RC,RR等隔离级别,用户可以查看当前数据的前一个或者前几个历史版本。保证了ACID中的I特性(隔离性)。
由于RR隔离级别下,在每个事务开始的时候,会将当前系统中的所有的活跃事务拷贝到一个列表中(read view)。
RC隔离级别下,在事务中的每个语句开始时,会将当前系统中的所有的活跃事务拷贝到一个列表中(read view) 。
再举个例子,从MVCC启动步骤来分析。
session 1 | session 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 |
DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID | colum a |
---|---|---|---|
1 | NULL | 1 | 10 |
trx_sys=[]
DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID | colum a |
---|---|---|---|
1 | NULL | 1 | 10 |
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 DB_TRX_ID >= up_limit_id 则该行对于当前Read View是可见的 如果DB_TRX_ID(1) 满足规则,则可见 如果DB_TRX_ID(1) >= up_limit_id(2) 则该行对于当前Read View是不可见的 不满足规则,则可见 由于规则都可见,所以查询结果为DB_TRX_ID=1时的colum a=10 根据readview规则: DB_TRX_ID DB_TRX_ID >= up_limit_id 则该行对于当Read View是不可见的 如果DB_TRX_ID(2) 满足条件,则可见 如果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) 满足条件,则可见 如果DB_TRX_ID(1) >= up_limit_id(2) 则该行对于当前Read View是一定不可见的 不满足条件,则可见 结论:则DB_TRX_ID[1]这行对当前read view可见 则查询结果为colum a=10 提交事务后会把事务链表已提交的事务id去掉,则当前trx_sys为[4] 注意,在此之前都所有结论都试用于rr和rc,而这里不同 由于每次查询都是读取到最新的readview,所以本次查询的行数据如下 根据readview规则: DB_TRX_ID DB_TRX_ID >= up_limit_id 则该行对于当Read View是不可见的 如果DB_TRX_ID(2) 满足条件,则可见 如果DB_TRX_ID(2) >= up_limit_id(4) 则该行对于当前Read View是不可见的 不满足条件,则可见 结论:则DB_TRX_ID[2]这行对当前read view可见 则查询结果为colum a=20 由于每次查询都是读取到第一次查询创建的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实现
DB_TRX_ID
DB_ROLL_PTR
DB_ROW_ID
colum a
2
DB_TRX_ID[1]
1
20
trx_sys=[3]→[2]
readview=[low_limit_id(4),up_limit_id(2),trx_sys{3,2}]
DB_TRX_ID
DB_ROLL_PTR
DB_ROW_ID
colum a
2
DB_TRX_ID[1]
1
20
trx_sys=[4]→[3]→[2]
readview=[low_limit_id(5),up_limit_id(2),trx_sys{4,3,2}]
DB_TRX_ID
DB_ROLL_PTR
DB_ROW_ID
colum
2
DB_TRX_ID[1]
1
20
trx_sys=[4]
在rc的隔离级别下
DB_TRX_ID
DB_ROLL_PTR
DB_ROW_ID
colum a
2
DB_TRX_ID[1]
1
20
trx_sys=[5]→[4]
readview=[low_limit_id(6),up_limit_id(4),trx_sys{5,4}]
在rr的隔离级别下
总结
参考资料
参考资料