mysql的事物(ACID)特性和实现原理

我们都知道MySQL事务的ACID,但是按照严格的标准,只有同时满足ACID特性才是事务;但是在各大数据库厂商的实现中,真正满足ACID的事务少之又少。例如MySQL的NDB Cluster事务不满足持久性和隔离性;InnoDB默认事务隔离级别是可重复读,不满足隔离性;Oracle默认的事务隔离级别为READ COMMITTED,不满足隔离性……
因此与其说ACID是事务必须满足的条件,不如说它们是衡量事务的四个维度。很多的文章也都会介绍事务ACID是什么,但是却很少有人能够介绍到事务ACID的实现原理。今天这里就来介绍一下实现原理。

首先特性有四种,分别为 原子性,隔离性,一致性,持久性。

1.原子性:

原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么全部做。如果对于一个事务来说其中的sql语句执行失败,则已经执行的语句也必须要回滚,数据库退回到事务之前的状态。
实现原理:
mysql InnoDB 提供了两种事物日志, redo log(重做日志)和 undo log(回滚日志)。redo log会保存操作的正向日志,undo log则相反,举个栗子,新增一条数据时,undo log会新增一条insert sql,而undo log则是生成一条delete where id =id 的sql 日志,而修改时,redo log记录update sql ,而undo log 则是修改内容和条件相反,简单理解 redo log 是保存要去做的事情,undo log就是用来保存撤回之前完成的事情。

下面来具体介绍对于原子性实现的关键: 当事务出现回滚时候能够撤销所有已经成功执行的sql语句。

InnoDB能够实现回滚的主要原因就是靠undo log: 当事务对数据库进行修改的时候,InnoDB会生成对应的undo log ;如果此时事务执行失败或者调动了 rollback,导致事务出现回滚情况,可以利用undo log中的信息将数据回滚到修改前的样子

undo log 属于一个逻辑日志,它用来记录的是sql执行相关的信息。对于一个insert语句在回滚时候会执行delete,相反也是如此。例如对于一个update在执行的时候,其生成的undolog 中会包含被修改的主键(以便知道修改了哪些行,修改了哪些列)以便在回滚时候能够使用这些记录的信息将数据还原到执行之前。

2.隔离性

隔离性是指:事物与事物之间的操作互不影响。事务内部的操作与其他的事务是隔离的,并发执行的各个事务之间是不能相互干扰。隔离性,对应了事务隔离级别中的 Serializable,但是在我们的实际的开发中很少使用到可串行化。
隔离性追求的是并发情形下事务之间不会相互干扰,简单起见,我们仅考虑最简单的读操作和写操。
主要分为两个方面,一个线程写对另一个线程的读,写影响,
举例:现在数据库有一条数据id=5,amount=10的数据,现有线程A,B,A线程先读取数据,后B线程更改数据amount =15,A线程再次获取数据,这时的amount还是=10,是不是很神奇,那我们看一下怎么实现的。
实现原理:主要是运用了锁机制和操作日志和隐藏数据列来实现的。

锁机制

首先来看两个事务的写操作之间的相互影响。隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB通过锁机制来保证这一点。
锁机制的基本原理可以概括为:
事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

行锁与表锁

按照粒度,分为表锁与行锁:
MyISAM 支持表锁,InnoDB 支持表锁和行级锁,默认是行级锁。
表级锁:开销小,加锁快,不会出现死锁。锁定粒度大,发送锁冲突的概率比较高,并发处理效果较低。
行级锁: 开销大,加锁慢,会出现死锁,锁定粒度较大,发生锁冲突的概率会小一点,并发处理的效果高
mysql的事物隔离级别又分为四种:RU(读未提交) RC(读已提交) RR(可重复读)可串行化读

RU:所有事务都可以看到其他未提交事务的执行结果, 即在未提交读级别,事务的修改,即使没有提交,对其他事务也是可见的,该隔离级别很少使用到,也被称为脏读。

RC:大多数的数据库默认的隔离级别是 提交读,但是对于Mysql不是 提交读:一个事务开始时,只能“看见”已经提交的事物所做的修改。换句话说一个事务从开始直到提交之前,所做的任何修改都是对其他事务不可见的。这个级别也叫作不可重复读。

RR:解决了脏读了问题,该级别保证了在同一个事务多次读取同样记录的结果是一致的。但是可重复读还是无法解决幻读的问题: 什么是幻读 指的是当某个事务在读取某个范围内的记录的时候,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时候 就会产生 幻行**。InnoDB 和XtraDB 存储引擎通过版本并发控制解决而了幻读的问题
可重复读是Mysql的事务的默认隔离级别。**
注意在 SQL标准中,RR是无法避免幻读问题,但是InnoDB实现的RR避免了幻读问题。

可串行化读:是最高的隔离级别,通过强制事务串行化执行,避免了前面所说到的幻读的问题。就是可串行化 会在读取的每一行数据上都加上锁,但是这样会导致超时和锁争用问题。

上面提到了 脏读,幻读,不可重复读,这里解释一下:
1.幻读:事物A读取一个指定条件的范围数据集,事物B在这个范围里新增一条数据,事物A再次用同样的条件查询结果集,发现多了一条事物B新增的数据,这条数据就是幻读数据

2.不可重复读:事物A读取一行记录,然后事物B修改了事物A读取的记录,事物A再次读取,结果是事物B更改后的数据,这个叫不可重复读。

3.脏读:事物A更新了一条记录,但是还未提交,事物B读取了事物A更新的数据,之后事物A触发了回滚操作,事物读取的数据就是脏读数据。

四种隔离级别解决的问题:
隔离级别 脏读 幻读 不可重复读

隔离级别脏读幻读不可重复读
读未提交 (RU)
读已提交(RC)
可重复读 (RR)
串行

MVCC(多版本并发控制)
前面讲到了 RR解决了 脏读,不可重复读,幻读等问题使用到的就是MVCC(Multi-Version Concurrency Control) 既多版本的并发控制:在同一个时刻,不同的事物读取到的数据可能是不同的(多版本)。对于MVCC来说最大的优点就是读不加锁,因此读写不冲突,并发性能好
InnoDB实现MVCC,多个版本的数据可以共存,主要是依靠数据的隐藏列(也可以称之为标记位)和undo log。其中数据的隐藏列包括了该行数据的版本号、删除时间、指向undo log的指针等等;当读取数据时,MySQL可以通过隐藏列判断是否需要回滚并找到回滚需要的undo log,从而实现MVCC。简单理解就是解决了数据读取的脏读,幻读和不可重复读。

如何解决脏读
现有数据 id = 1 amount=1,事物A修改amount = 10 (暂未提交事务),事物B读取数据amount =1,事物A提交事务。
事物B读取数据时,会发现数据已被其他事务修改,且状态为未提交。此时事务B读取最新数据后,根据数据的undo log执行回滚操作,得到事务A修改前的数据,从而避免了脏读

如何解决幻读
现有数据 id = 1 amount=1,事物A查询 id范围为1~5的数据,事物B插入了id=2的数据并且提交,事物A再次查询id为1 ~ 5的结果集,发现只有id=1的数据。

nnoDB实现的RR通过next-key lock机制避免了幻读现象
next-key lock是行锁的一种,实现相当于record lock(记录锁) + gap lock(间隙锁);其特点是不仅会锁住记录本身(record lock的功能),还会锁定一个范围(gap lock的功能)。当然,这里我们讨论的是不加锁读:此时的next-key lock并不是真的加锁,只是为读取的数据增加了标记(标记内容包括数据的版本号等);
当事务A在第一次读取0<id<5数据时,标记的不只是id=1的数据,而是将范围(0,5)进行了标记,这样当事物A再次读取 id in (0~5)数据时,便可以发现id=2的数据比之前标记的版本号更高,此时再结合undo log执行回滚操作,避免了幻读。

如何解决不可重复读
现有数据 id = 1 amount=1,事物A查询结果为id=1 amount=1,事物B修改amount=2 并且提交,事物A再次查询,amount=1;

当事务A在第一次读取数据时,会记录该数据的版本号(数据的版本号是以row为单位记录的),假设版本号为1;当事务B提交时,该行记录的版本号增加,假设版本号为2;当事务A再一次读取数据时,发现数据的版本号(2)大于第一次读取时记录的版本号(1),因此会根据undo log执行回滚操作,得到版本号为1时的数据,从而实现了可重复读

概括来说,InnoDB实现的RR,通过锁机制、数据的隐藏列、undo log和类next-key lock,实现了一定程度的隔离性,可以满足大多数场景的需要。不过需要说明的是,RR虽然避免了幻读问题,但是毕竟不是Serializable,不能保证完全的隔离。

3.一致性

一致性是指事物执行结束后,数据库的完整性没有被破坏,事务执行的前后都是合法的数据状态。数据库的完整性包括但是不限于:
实体完整性(如行的主键存在且唯一)
列完整性(如字段的类型,大小,长度符合要求),
外键约束(外键约束还存在)
用户自定义完整性(如转账前后,两个账户的和应该是不变的)

实现原理:
可以说,一致性是事物追求的最终目标,前面提到的原子性,隔离性,持久性都是为了保证数据库的一致性。此外除了数据库底层的保障,一致性的实现也需要应用层的保障。

保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致

4.持久性

事务一旦提交,他对数据库的改变就是永久的,接下来的其他操作或是故障都不应该对其造成影响。

实现的原理
前面提到了InnoDB实现了两个事务日志,首先我们来聊一下redo log 存在的背景。

InnoDB作为MySQL的存储引擎(在5.1版本以后),数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:
当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。

Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。

于是,redo log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。

既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。

redo log 与 binlog
我们知道,在MySQL中还存在binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的:

(1)作用不同:redo log是用于crash recovery的,保证MySQL宕机也不会影响持久性;binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。
(2)层次不同:redo log是InnoDB存储引擎实现的,而binlog是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持InnoDB和其他存储引擎。
(3)内容不同:redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。
(4)写入时机不同:binlog在事务提交时写入;redo log的写入时机相对多元:

前面曾提到:当事务提交时会调用fsync对redo log进行刷盘;这是默认情况下的策略,修innodb_flush_log_at_trx_commit参数可以改变该策略,但事务的持久性将无法保证。
除了事务提交时,还有其他刷盘时机:如master thread每秒刷盘一次redo log等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快。

总结:
原子性: 语句要么都执行,要么都不执行,是事务最核心的特性,事务本身来说就是以原子性历来定义的,实现主要是基于undo log(回滚日志)
持久性: 保证事务提交之后,不会因为宕机等其他的原因而导致数据的丢失,主要是基于 redo log实现
隔离性: 保证事务与事务之间的执行是相互隔离的,事务的执行不会受到其他事务的影响。InnoDB存储引擎默认的数据库隔离级别是 RR ,RR又主要是基于锁机制,数据的隐藏列,undo log类 以及 next-key lock机制
一致性: 事务追求的最终目标,一致性的实现即需要数据库层面的保障,也需要应用层面的保障。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值