一,关于事务和一致性的理解

事务的产生,其实是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题.可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的.而不是伴随着数据库系统天生就有的。
ACID里的AID都是数据库的特征,也就是依赖数据库的具体实现.而唯独这个C,实际上它依赖于应用层,也就是依赖于开发者.这里的一致性是指系统从一个正确的状态,迁移到另一个正确的状态.什么叫正确的状态呢?就是当前的状态满足预定的约束就叫做正确的状态.而事务具备ACID里C的特性是说通过事务的AID来保证我们的一致性.
一致性就是:应用系统从一个正确的状态到另一个正确的状态.而ACID就是说事务能够通过AID来保证这个C的过程.C是目的,AID都是手段

事务的四个属性:ACID
1.原子性(Automic):一个动作要么做完,要么不做。

2.一致性(Consistency):保证数据处于一致性的状态,我理解就是保证数据有意义的。

3.隔离性(Isolation):多个事务并行的结果,应该和多个事务串行的结果一致。

4.持久性(Duration):一个事务一旦成功提交,对数据改变是永久性的。

这个四个属性中,最重要的是一致性,也就是说其他的三个属性都是为了保证一致性而存在。

二,那么,AID是如何保证的呢

在使用Mysql的时候,有两种常用的存储引擎:MyISAM和InnoDB

InnoDB的性能不如MyISAM高,因为InnoDB提供事务支持以及外部键等高级数据库功能

事务有什么用?

银行转账,A转给B账户100元,需要保证A账户减少100元的同时B账户增加100元,如果A账户减少100元之后,系统crash,B账户并未增加100元,这样是没有数据保证一致性的。所以,如果两个动作是同一个事务,那么此时A操作需要回滚,保证数据一致性。
如何回滚?
原子性如何保证?

这个就需要说到Mysql的log,Mysql有许多种类的log,例如:二进制日志(binlog),错误日志,慢查询日志等等。这里需要引入的是undo log。这是Mysql的Write-Ahead-Logging机制。

例如:数据库中A=1,现在需要update A=3

那么整个步骤如下:

  1.  
    1.事务开始
  2.   
  3.  
    2.记录A=1到undo log
  4.   
  5.  
    3.修改A=3
  6.   
  7.  
    4.将undo log写入磁盘
  8.   
  9.  
    5.将A=3数据写入磁盘
  10.   
  11.  
    6.事务提交

这个就是undo log工作流程,也就是在数据库断电或者crash的时候,在进行恢复的时候,把undo log里面的数据写回到数据库,这样就让数据回滚了。这样实现了事务的原子性,同时保证了数据的一致性。

但是,这样每个操作都会进行磁盘IO的写入,频繁的磁盘IO对性能是很大的降低。

引入redo log实现持久性,这个时候就在考虑如果只需要将日志写入磁盘,将数据缓存在内存中,一定时间后再进行更新。

例如:数据库中A=1,B=2,需要update A=3,B=4

  1.  
    1.事务开始
  2.   
  3.  
    2.记录A=1到undo log
  4.   
  5.  
    3.修改A=3
  6.   
  7.  
    4.记录A=3到redo log
  8.   
  9.  
    5.记录B=2到undo log
  10.   
  11.  
    6.修改B=4
  12.   
  13.  
    7.记录B=4到redo log
  14.   
  15.  
    8.将redo log顺序写入磁盘
  16.   
  17.  
    9.事务提交

整个过程中,数据修改都是在内存中,极大提升磁盘IO速度,而且将redo log提前写入磁盘。

如果整个事务执行的过程系统崩溃或者断电了,在系统重启的时候,恢复机制会将redo log中已提交的事务重做,保证事务的持久性;而undo log中未提交的事务进行回滚,保证事务的原子性。

Mysql通过预写式日志,保证了原子性和持久性。那么在多个事务并行的情况下,是否还能保证数据的一致性?如果A事务能够访问B事务正在提交的数据,然后B事务又做出了回滚,这样是不是就让数据乱套了。所以事务并行的情况下,这样是不够的。

隔离性:引入锁机制来保证。那么是什么情况下不锁,什么情况下锁呢?还有读写分离需要实现吗?这就需要去说说四种级别的隔离性。

1.未提交读:A事务可以读取B事务正在修改的数据,但是会出现B事务如果回滚,这样数据前后不一致,会造成脏读的现象。

2.已提交读:A事务只能读取B事务已提交的数据。B事务修改数据,A事务进行读取,数据未改,当B事务提交数据后,A事务读取数据不一致。这是幻读现象,因为同一个事务,我读取两次相同的数据返回的是不同的,这样并未保证一致性。

3.可重复读:利用MVCC并发版本控制,B事务修改数据,A事务进行读取,数据未改,当B事务提交后,A事务读取数据,这个时候会返回A数据之前读的版本。

4.序列化:简单粗暴,读锁和写锁都是排它锁,不管读操作还是写操作都是会对数据上锁。这样粗暴造成性能下降很多。

大多数数据库默认使用已提交读的隔离级别,Mysql中InnoDB默认使用可重复读的隔离级别。

总结

Mysql的InnoDB保证事务性,最重要是保证数据的一致性。

通过预写式日志,undo log保证原子性,redo log保证持久性,设置隔离级别,保证并发事务进行的时候,保证数据一致性。

一、MySQL事务模型ACID

MySQL是一个多引擎数据库,其中InnoDB支持数据库事务,也是最常用的引擎。下边就介绍InnoDB的事务模型

MySQL官方文档对事务是这么描述的“事务是可以提交或回滚的原子工作单元。当事务对数据库进行多个更改时,要么提交事务时所有更改都成功,要么回滚事务时撤消所有更改。”

“ACID模型是一组数据库设计原则,强调业务数据和关键应用程序的可靠性很重要。MySQL包含与ACID模型紧密结合的innodb存储引擎组件,确保数据不会被破坏,结果不会被软件崩溃和硬件故障等异常情况所篡改。当您依赖ACID的特性,就不再需要重新发明一致性检查和崩溃恢复机制。”

ACID模型按照字母拆解分为四大特性

A : atomicity 原子性。原子性是我们对事务最直观的理解:事务就是一系列的操作,要么全部都执行,要么全部都不执行。

C : consistency 一致性。数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。例如对银行转帐事务,不管事务成功还是失败,应该保证事务结束后ACCOUNTS表中Tom和Jack的存款和不变。

I : isolation 隔离性。在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。

D : durability 持久性。只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

二、InnoDB存储引擎架构

下边这张图是InnoDB的架构,包括两大部分,内存结构(In-Memory Structures)和磁盘上的结构(On-Disk Structures)。

在这张图中,尤其要关注“Redo Log”和“Undo Tablespaces”这两个区域,它们跟事务息息相关。 

mysql 状态 Mysql状态一致性_mysql 状态

内存结构(In-Memory Structures)更多的目的是在提高性能,因此本文不会过多关注。如果感兴趣,可以访问MySQL的官方网站www.mysql.com

“Undo Tablespaces”包含Undo Log(撤消日志),Undo Log是撤消日志记录的集合,其中包含如何撤消事务对聚集索引记录的最新更改的信息。Undo Log存在于撤消日志段中,这些日志段包含在回滚段中。

MySQL事务的四个特性中ACD三个特性是通过Redo Log(重做日志)和Undo Log 实现的,而 I(隔离性)是通过Lock(锁)来实现。

三、普及个概念MVCC

MVCC,Multi-Version Concurrency Control,多版本并发控制。这项技术使得InnoDB的事务隔离级别下执行一致性读操作有了保证,换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值。这是一个可以用来增强并发性的强大技术,查询不用等待另一个事务释放锁。这项技术广泛应用于数据库,例如Oracle,PostgreSQL。当然也有一些数据库产品以及mysql的其它存储引擎不支持它。

看一看MVCC机制的示意图,图下边会给出文字解释 

mysql 状态 Mysql状态一致性_数据_02

图中底部横轴是时间,纵向的箭头用来标记增、删、改、查发生的时刻。尤其注意时间轴上方两条色块,代表数据的两个版本V1、V2。为了醒目,我把V1、V2用红色方框圈了起来(多版本的体现)。从左向右解读这张图

1、T1事务插入数据a=3,然后提交,生成了数据对应的V1版本

2、T2事务开始读取a数据,读取会持续一段时间,由于开始读取的时刻,只有V1版本,所以最终T2读到a=3

3、T2读取过程中,T3对数据a进行修改,a=4,生成a数据的V2版本,但此时并未提交,因此生效的是V1版本数据。

4、T3修改提交之前,T4读取a数据,由于此时V1版本数据生效,因此,T4读到a=3

5、T3提交a=4的修改,V1版本数据失效,V2生效。a的值变为4

6、T5读取a的值,读到V2版本,a=4

至此,MVCC的概念就搞明白了,那么MySQL是怎么实现的呢?

四、InnoDB多版本的实现

1、三个隐藏字段

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

(1)DB_TRX_ID字段,6字节。表示插入或更新行的最后一个事务的事务标识符。此外,删除在内部被视为更新,其中行中的特殊位被设置为将其标记为已删除。

(2)DB_ROLL_PTR字段,7字节,叫做回滚指针(roll pointer)。回滚指针指向写入回滚段的撤消日志(Undo Log)。如果行已更新,则撤消日志包含重建更新前该行内容所需的信息。

(3)DB_ROW_ID字段,6字节。包含一个随着新行插入而单调增加的行ID,如果innodb自动生成聚集索引,则该索引包含行ID值。否则,DB_ROW_ID列不会出现在任何索引中。

2、多版本产生过程

以新增一条记录并对该记录进行2次修改来说明具体实现 

mysql 状态 Mysql状态一致性_数据库_03

这条记录有3个隐含字段(前面已经介绍),分别应对行的ID、事务号和回滚指针。

当插入的是一条新数据时,记录上对应的回滚段指针为NULL 

mysql 状态 Mysql状态一致性_数据库_04

这个过程做了以下几件事

  • 用排他锁锁定该行
  • 把该行修改前的值拷贝到Undo Log中
  • 修改当前行的值,填写事务编号,使回滚指针指向Undo Log中的修改前的行
  • 记录Redo Log,包括Undo Log中的变化 

mysql 状态 Mysql状态一致性_mysql 状态_05

多次更新后,回滚指针会把不同版本的记录串在一起。在InnoDB中存在purge线程,它会查询那些比现在最老的活动事务还早的Undo Log,并删除它们,从而保证Undo Log文件不至于无限增长。

3、提交与回滚

当事务正常提交时,InnoDB只需要更改事务状态为commit即可,不需要做其他额外的工作

回滚(rollback)需要根据当前回滚指针从Undo Log中找出事务修改前的版本,并恢复。如果事务影响的行非常多,回滚则可能会很慢,根据经验值没提交的事务行数在1000~10000之间,InnoDB效率还是非常高的(唐成-数据库多版本实现内幕)。

commit效率高,rollback代价大

4、可见性

事务隔离是数据库处理的基础之一,隔离是缩写ACID中的I。隔离级别是当多个事务同时进行更改和执行查询时,微调性能、可靠性、一致性和结果再现性之间的平衡的设置。

InnoDB提供SQL1992标准定义的四个隔离级别,READ UNCOMMITTED(未提交读), READ COMMITTED(已提交读), REPEATABLE READ(可重复读), and SERIALIZABLE(可串行化)。默认的是REPEATABLE READ

每种隔离级别具体的意义可以百度查到,实现原理深入进去比较复杂。注意到每条数据隐藏的事务ID字段DB_TRX_ID有时序性,理论上可以根据一些策略,借助这个字段来实现与隔离级别相关的功能。事实上InnoDB也是这么做的。当然这个功能还涉及很多锁的问题,这里不再展开。