重学Mysql(1)——ACID有什么好讲的

先看宏观概念

mysql整体上可以分为server层存储引擎层,存储引擎是以插件形式整合到mysql中的,所有的存储引擎共用一个server层。mysql各个模块的功能如下图所示

image-20200327152033125

再看ACID

ACID是啥?

ACID是事务型数据库(OLTP,联机事务处理,相对于OLAP,联机分析处理,是分析型数据库)为了保证事务的正确可靠,必须具备的四个特性。包括原子性,一致性,隔离性,持久性;可以说ACID就是mysql的核心功能所在,来看一下这四个特性的含义:

  • 原子性(Atomicity):一组事务中的所有操作,要不然全部完成,要不然全部不完成
  • 一致性(Consistancy):在事务的开始和事务的结束,数据库的完整性没有被破坏,写入的数据符合数据库预设约束
  • 隔离性(Isolation):允许多个事务并发写入和读取,且保证事务交叉执行过程中的数据一致性;隔离性有四个层级,隔离效果由低到高,并发性能由高到低:读未提交(read uncommitted),读提交(read committed),可重复读(repeatable read),串行化(serializable)
  • 持久性(Durability):事务完成后,对数据的修改是永久的,可防止因机器故障等导致的数据丢失

总之,只要理解了mysql对于ACID的实现,也就理解了mysql基本的流程原理,从而帮助我们从底层世界的角度更好地分析实际问题


所以,咋实现的?

说一个前提,在mysql的架构中,ACID的实现是交给存储引擎实现的,准确地说是交给innodb实现的,因此以下对于ACID的讨论仅限于mysql在使用innodb引擎时的情况,当然,在大部分情况下,我们也应该尽量选择innodb作为存储引擎。

  1. 原子性

原子性代表的是操作的整体性,即在一个事务中的任意步骤发生了异常,都要保证恢复到事务执行之前的数据状态,如何恢复?这就需要我们的回滚机制来保证。innodb的回滚机制的实现依赖于版本链undolog。关于版本链,指mysql中所有的数据都存在多个版本,分别是各个事务对同一个数据进行操作时留下的历史数据,一个数据的多个版本在逻辑上就形成了版本链,版本链上的每一个节点都记录了当时版本的值和row trx_id,row trx_id表示是哪一个事务写入的该值。版本链的逻辑关系如图:

image-20200327122838813

要注意,这只是逻辑上的关系,物理上并不存在这么多数据,物理上只存在最新的版本的数据,其中U1,2,3便是回滚段,通过当前版本加回滚段,就可往前遍历取到想要的历史版本,回滚段的结构大致如下图:

image-20200327141658690

如上所述,版本链加上回滚段undolog,mysql的innodb便可实现回滚机制,以保证事务的原子性。


了解了知识,再来谈谈启发

这种机制会引发一个问题,那就是如果有大量的长事务存在,会导致undolog的大量堆积,占用大量存储空间。因为undolog的清除必须保证系统中没有比这个undolog更早的read-view(如上图,read-view是事务开始之初创建的一致性视图,长事务中间可能会产生大量的undolog,本事务和其他后来事务产生的undolog都需要保存,一是保证事务回滚能回到最初的版本,二是实现一致性视图,简单来说就是不看后来事务提交的数据,下面会介绍一致性视图),因此,我们在实际使用中,应该尽量避免长事务的使用,一是避免undolog的大量堆积(影响空间占用),二是减少锁的竞争(影响并发度)。

扩展小知识:
如何避免长事务——设置事务自动提交,指定语句超时时间,避免不必要的只读事务,长事务的监控和报警
如何避免锁竞争——除了避免长事务之外,在一个事务中,热点数据的操作语句尽量放在事务的最后执行


  1. 一致性

对于一致性的理解我一直都是模棱两可,官方解释与广为流传的转账例子总觉得对不上,在转账的例子中,难道不是应用层利用数据库原子性所保证的数据一致性吗,似乎跟数据库本身的一致性保证没有关系。直到看到维基百科中的一段话,才真正证明了自己的疑惑是正确的。

Consistency in database systems refers to the requirement that any given database transaction must change affected data only in allowed ways. Any data written to the database must be valid according to all defined rules, including constraints, cascades, triggers, and any combination thereof. This does not guarantee correctness of the transaction in all ways the application programmer might have wanted (that is the responsibility of application-level code) but merely that any programming errors cannot result in the violation of any defined database constraints.

数据库所保证的一致性,仅仅只保证constraints(约束),cascades rollback(级联回滚),triggers(触发器)以及它们之间的组合的正确性,而转账的例子,确实是需要在应用层去控制数据的一致性,这属于应用层的约束,是利用事务的特性去保证的。


  1. 隔离性

隔离性是理解数据库执行机制的一大重点和难点,隔离性存在的意义,是为了保证多个事务并发执行时数据的一致性,也就是说隔离性提供了mysql并发执行事务的可能,提高了mysql的并发度。隔离性四个层级:读未提交(read uncommitted),读提交(read committed),可重复读(repeatable read),串行化(serializable),并发度依次降低,安全性(正确性)依次提高。mysql默认的隔离级别是可重复读,是相对比较保守,注重正确性的选择。

那么这四种隔离级别有什么区别,生产环境下又该如何选用呢?

  • 读未提交(read uncommitted):指并发事务之间可以相互看到对方未提交的数据,显然这个隔离级别是非常危险的,看到其他事务的未提交数据很容易导致意想不到的结果,脱离我们预想的行为。这种现象也就是所谓的脏读。一般情况下生产环境下不会设置读未提交隔离级别,尽管它的并发度是最高的(加锁少了),不过凡事无绝对,在充分了解这个隔离级别行为的前提下,如果有某个场景不在意脏读等情况,那读未提交便是一个可选项了
  • 读提交(read committed):指事务只有在提交之后,被该事务修改的数据才能被其他事务看到。这个隔离级别解决了脏读的情况,但是仍然存在不可重复读幻读
    • 不可重复读:在同一个事务中两次读取同一条数据期间,其他的事务修改了该条数据并提交,导致前一个事务两次读取到的数据不一致,此为不可重复度
    • 幻读:在同一个事务执行同一个范围查询期间,其他的事务在该范围中插入了一条数据并提交,导致前一个事务两次查到的数据列不一样,此为幻读
    • 不可重复读和幻读都是指同一事务中的两次查询结果不一致的情况,区别在于不可重复读侧重于修改,而幻读侧重于新增,不可重复读关注数据本身,幻读关注数据间隙。虽然读提交存在这两种问题,但在大部分情况下都是适用的,前提是熟知该隔离级别下的事务行为。实际上,出于并发度和正确性的平衡考虑,在生产环境中建议选择读提交的隔离级别,可获得较好的并发性能
  • 可重复读(repeatable read):指在读提交的基础上解决了不可重复读和幻读的问题,进一步提升了数据一致性,但当然是以一定的并发度作为牺牲,作为mysql默认的隔离级别,在数据一致性上解决了关键问题,在并发度的优化上也竭尽了所能,对于不太熟悉mysql事务执行原理的大部分团队来说,是一个最佳的选择,毕竟数据一致性往往是大部分业务所追求的关键目标
  • 串行化(serializable):指不允许事务并发,所有事务都需要排队串行化执行,这是为了保证最强的数据一致性和当然,最差的并发度。只有在特殊场景下才会使用的隔离级别,一般情况是用不到的,总之原则还是按需使用

介绍完了四种隔离级别的定义和使用场景,再适当了解一下其在innodb的实现原理,可给我们提供更广阔的分析问题的视野(例如某些奇怪的死锁等),在这里我们主要关注读提交可重复读的实现原理,原因很简单,应用最广。

读提交和可重复读的实现主要依赖两种关键机制:版本链一致性视图,两者合起来便构成了innodb中一个著名的概念:MVCC(多版本并发控制);MVCC的意义,是为了在使mysql获得并发事务执行能力的同时尽量提高其并发度。隔离级别是表面结果,MVCC是实现方式。

版本链在介绍原子性一节已经提到过,指一个数据的多个历史版本形成的逻辑上的链条,通过undolog便可在这个逻辑链条上自由切换历史版本。版本链存在的意义,一是为了给事务回滚提供数据结构支撑,二是为事务并发提供了更好的性能(相对于锁机制,锁机制是悲观锁,版本链相当于乐观锁)。

一致性视图并不是通常的表和视图的概念,而是基于版本链和一致性视图数组构建的一个逻辑上的"视图",本质上就是通过控制事务对不同数据版本的可见性来构建针对特定事务的数据视图,以保证同一个事务数据的一致性,一致性视图应用的具体流程是这样:

  1. 事务开始执行,该事务分配一个事务id,称为transaction_id,该transaction_id是递增的,由mysql保证,在这里假设是9

  2. 根据当前活跃的事务id(还未提交的事务的id)和当前系统已经创建过的最大事务id加一(称之为高水位),构建一个一致性视图数组,假设当前活跃事务id有5和7,当前系统创建的最大事务id是10(当前事务分配事务id和创建一致性视图数组期间有可能会创建并提交新事务),则构成的一致性视图数组便是:[5,7,11]

  3. 有了一致性视图数组之后,在读取版本链形式的数据时取最新版本的数据,用最新版本数据的row trx_id和一致性视图数组相比较,来确定当前事务对于某个版本数据的可见性,从而呈现一个一致性视图。row trx_id和一致性视图数组的大小关系和可见性关系如下所示

image-20200327231604998
  • 如果row trx_id处在绿色部分,则表示该版本数据对应的事务已经提交,是可见的;如果处在红色部分,说明该版本数据是当前事务之后的事务创建的,当然是不可见的;如果处在黄色部分,有可能可见也有可能不可见,具体规则如下
  • 如果row trx_id存在于黄色部分,则说明是由未提交的事务创建的数据,是不可见的
  • 如果row trx_id不存在于黄色部分,则说明是由已提交的事务创建的,数据可见。

在这个例子中,一致性视图数组为[5,7,11],假设row trx_id=13,则该数据版本不可见,需要根据undolog找到之前版本的数据,再用其row trx_id和一致性视图数组比较,直到符合上述可见性规则为止,就是当前事务可以看见的数据版本。


理解了MVCC的原理后,再来看看读提交和可重复读的实现方式,便一目了然:

  • 读提交级别下,sql每次执行之前都会创建一个一致性视图,根据可见性规则解决了脏读的问题,需要注意的是若一个事务中存在多个sql,则每个sql都会创建一个一致性视图
  • 可重复读级别下,会创建一个跨越整个事务的一致性视图,由此解决了可重复读和幻读的问题。如果是当前读的情况(select for update等),则会通过gap锁来解决幻读(只有可重复读级别下会有gap锁)

gap锁的原理是在数据的间隙加锁,阻止了其他事务插入新的数据,innodb借此锁机制解决了当前读情况下的幻读(与当前读相对的是快照读,就是普通的select)。但是gap锁也有缺点,一是锁范围的增大影响了并发度,二是gap锁有可能会引起隐蔽性很强的死锁。gap锁不存在读锁和写锁的概念,gap锁的作用是防止其他事务在间隙中插入数据,但是不影响其他事务给其加锁。当两个事务加上同一个gap锁后,如果两个事务都需要在同一间隙中插入数据,这两者就会相互等待对方的gap锁释放,形成死锁。因此在享受gap锁带来的一致性便利的同时,也要注意随之带来的隐患


所以,原理在手,如何应用?也许可以有如下的思考

  • 对于mysql默认的可重复读隔离级别,在对数据一致性不是如此敏感的情况下,我们可以替换成读提交,以获得并发度的提升
  • 深入理解不同隔离级别下的数据行为,可以帮助我们实现正确的逻辑和分析奇怪的数据并发问题

  1. 持久性

数据可以说是科技公司最重要的资产,其可靠性的重要性不言而喻,即无论在什么情况下(断电宕机等),都要保护数据不丢失或尽可能减少数据的丢失。这一点体现在mysql上,就是数据持久性的保证。

mysql用以保证数据持久性的系统是两个日志模块:binlogredolog。binlog是属于mysql server层的日志模块,redolog属于innodb的实现,也就是说只有使用了innodb才会有redolog;先说两者的应用场景:

  • binlog用于做数据归档,也就是我们一般说的数据备份,备份保存了整个数据库某个时间点的copy,用于数据恢复。
  • redolog用于做崩溃恢复,保证数据库宕机重启等也不丢失现场数据

从两者应用场景中可清晰地看出两者的区别,通过两者配合即可使mysql的数据不丢失,值得一提的是,只有使用了innodb才能利用redolog的崩溃恢复功能,也就是说mysql本身的binlog是不具备崩溃恢复功能的,而redolog也不具备份的功能,至于为什么要这么设计(两份日志),只能说是因为历史原因吧。

binlog不能做崩溃恢复的原因是不能恢复数据页,binlog记录的是逻辑数据,即sql信息或者每一行的修改信息,无法记录某一数据页是否被更新(写binlog的时候不一定会更新磁盘数据页,因此在崩溃恢复时不确定是否要恢复某一数据页的数据),redolog则记录了数据页的更新信息,可以用作崩溃恢复。而redolog不能用作备份的原因是redolog是循环写入的,也就是说如果redolog写满之后,新数据会覆盖掉旧数据,没有全量的数据就无法做备份,而binlog是追加写,不会覆盖旧数据。


我们来看一下binlog和redolog是如何共同实现mysql数据的持久化的

假设我们执行一个sql:update table1 set c = c+1 where id = 1,如下是日志模块在该更新语句中的作用流程,绿色表示server层的操作,蓝色表示在innodb中的操作

image-20200327163929877

从该流程图中我们可以获得以下信息和思考

  • 在流程图中,innodb把新行被更新到内存,但是我们并没有看到写磁盘的操作,这如何保证数据持久化呢?如果数据库宕机重启,在内存中的更新数据岂不是就丢失了?这里就轮到redolog上场了
    • 第一个问题:这如何保证数据持久化?答案很简单,后台线程会在一定时间点把内存中的更新数据刷入到磁盘中,这种设计思想称为WAL(write-ahead logging),即先写日志(redolog),再写磁盘。这里有一个疑问,写日志不也要写磁盘吗?因为写redolog是顺序写磁盘操作,相对于更新数据的随机写磁盘来说,顺序写磁盘的速度要快得多,因此WAL可以大大提升更新数据的效率,因为只要更新数据到内存加上写入日志就完成了一次更新操作。同时,延时更新磁盘操作还可以把多次写磁盘操作合成一次进行,也大大减小了随机IO的压力,从而提升系统性能
    • 第二个问题:重启后内存的更新数据会丢失吗?答案是不会。redolog就是解决这个问题的。数据库重启后,根据redolog判断有哪些数据页是没有来得及更新的,然后把这些数据页读到内存中,再应用redolog,就恢复了原来的更新数据。
  • redolog写入了两次,一次是prepare阶段,一次是commit阶段,中间进行了binlog的写入,这种日志写入方式称为两阶段提交,目的是为了保证redolog和binlog的一致性,保证两者的一致性又是为了保证数据的一致性,为什么这么说呢?刚才提过,binlog用于备份恢复,也就是备库的数据,redolog用于崩溃恢复,也就是主库的数据,如果两者日志不一致,就会造成主备的数据不一致,因此需要通过两阶段提交来保证两份日志的一致性

最后
binlog和redolog这两个日志模块是非常重要的,不仅体现在它们是数据可靠性的保障,它们也是数据更新所依赖的重要模块,换句话说,与数据库性能和一致性也息息相关。有一些重要的参数例如:redolog的大小,binlog的格式(statement或row),日志刷盘的模式(一次事务一刷或多次事务一刷),日志缓存的大小等,都是在优化数据库时需要考虑的重要方向。所以理解日志模块的原理机制,可以帮助我们在进行数据库合理配置和优化时提供清晰的思路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值