事务:是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、 要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元);
因为要保证数据的一致性,如果没有事务,那么在读写,删除修改数据时,会造成数据的各种问题出现,导致数据不一致,无效数据,错误数据等等问题, 所以出现了事务,那么,以下我们来看看,
事务定义的几大特性,这是事务存在的几个作用,最终还是保证数据一致性
1、事务的四大特性
原子性:
一个事务内的操作,要么同时成功,要么同时失败
一致性:
一个事务必须使数据库从一个一致性状态变换到另一个一致性状态,所谓一致性,即,从实际的业务逻辑上来说,最终结果是对的、 是跟程序员的所期望的结果完全符合的
隔离性:
一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相 干扰。
持久性:
也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任 何影响
1.1、原子性
实现原理「undo log」
在说明原子性原理之前,首先介绍一下MySQL的事务日志。MySQL的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种事务日志:
redo log(重做日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。
下面说回undo log。实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;
如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:
对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。
以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
1.2、一致性
可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。
实现一致性的措施包括:
保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致
1.3、隔离性
实现原理「锁机制」
首先来看两个事务的写操作之间的相互影响。隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB通过锁机制来保证这一点。
锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
行锁与表锁:按照粒度,锁可以分为表锁、行锁以及其他位于二者之间的锁。表锁在操作数据时会锁定整张表,并发性能较差;行锁则只锁定需要操作的数据,并发性能好。但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。MySQL中不同的存储引擎支持的锁是不一样的,例如MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。
有多种方法可以查看InnoDB中锁的情况,例如:
select * from information_schema.innodb_locks; #锁的概况
show engine innodb status; #InnoDB整体状态,其中包括锁的情况
后面专门有一篇介绍锁的文章
1.4、持久性
实现原理「redo log」
redo log和undo log都属于InnoDB的事务日志。下面先聊一下redo log存在的背景。
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘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中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少
重点
为了保证事务操作的原子性,必须实现基于日志的REDO/UNDO机制,但原子性并不能完全保证一致性,而在事务处理的ACID中,一致性是最基本的属性, 其他的三个特性都是为了保证一致性而存在的,一致性指的是语义上面的一致而非语法,即在多并发或复杂业务场景下,我们的各种增删改查与我们预估 结果是一致的,满足我们的期望,所以是语义上的一致性,而为了保证数据的一致性,解决数据不一致的问题,衍生出了事务,进而逐步衍生隔离级别,隔离带来的问题,使用了锁,等等一系列发展,为了满足需要,解决问题保证数据一致性,所以用了事务,而并发事务不隔离就跟多并发线程未加锁一样,数据各种问题,比如数据脏读,不可重复读,幻读等等
2、隔离性的问题及级别
read uncommitted(RU):读未提交,出现的问题:脏读,并发性能最高,隔离级别最低
read committed(RC):读已提交,解决的问题:脏读,出现的问题:不可重复读
repeatable read(RR): 可重复读,解决的问题:不可重复读,出现的问题:幻读,mysql默认的事务隔离级别
serializable: 串行化,解决的问题:幻读,并发能力最低,隔离级别最高
1、读未提交
事务B可以读到事务A未提交的数据,但如果事务回滚,则读出的数据会造成脏读,即读取的数据可能不存在,属于无效数据
2、读已提交
解决了脏读问题,但多事务并发下会出现问题:事务B去读数据,此时事务A去更新数据,事务B需要对读取到的数据进行检验,所以再次进行了读取操作,事务A刚好修改了数据进行提交,事务B取的就是修改后的数据,两次数据不一致,导致结果也不一样了,这种叫不可重复读,同时降低了一点并发
3、可重复读
解决了不可重复读的问题,保证在多事务并发下,事务B如果在读取数据,那么事务A就不去修改数据了,同样,事务A在修改时,事务B也不会去查询但会出现幻读问题,在以上的案例中,将事务A的修改变成事务A进行删除或插入,对于事务B来说,就不是数据不一致,而是数量不一致了,这种就是幻读,仿佛出现了幻觉。又降低了一点并发,mysql默认的事务隔离级别
4、串行化
以上问题都没了,隔离级别最高,但是并发能力最低,隔离级别与并发能力成反比例,在处理金额相关的重要数据时可设为临时级别,一般不建议使用
结论:那我们了解了mysql的默认事务级别是可重复读(RR),前面我们说过,在mysql5.5之后采用的是InnoDB存储引擎,而InnoDB在RR的级别下,解决了幻读问题。
那下面我我们来介绍一下InnoDB是如何解决幻读问题
Innodb使用next-key locks(简单理解为X锁【排它锁】+GAP锁)解决幻读,MVCC解决的是普通读(快照读)的幻读,next-key locks解决的是当前读情况下的幻读。 在此之前我们先来了解一下,RR是如何实现的
RR级别下事务的原理,我们需要知道以下内容
了解MYSQL中的两种视图
了解RR级别下,如何实现的事务隔离
了解什么是当前读,以及当前读会造成什么问题
在mysql中,视图有两种
View,常用来查询的虚拟表,在调用时执行查询语句获取结果,创建视图:create view。
存储引擎层 InnoDB 用来实现 MVCC(Mutil-Version Concurrency Control | 多版本并发控制)时用到的一致性视图consistent read view,用于支持 RC 和 RR 隔离级别的实现。简单来说,就是定义在事务执行期间,事务内能看到什么样的数据。
事务真正启动的时机
在使用 begin 或 start transation 时,事务并没有真正开始运行,而是在执行一个对 InnoDB 表的操作时(即第一个快照读操作时),事务才真正启动,如果想要立即开始一个事务,可以用 start transaction with consistent snapshot 命令。 那么在RR级别下的事务,如果其他事务修改了数据,事务中看到的数据与启动事务时看到的数据是一致的,并不会受其他事务的影响,下面看一种特殊情况:A马上开启事务,B也马上开启事务,C修改数据,B再修改数据,然后查询数据,A查询数据,并提交事务,此时A查询出来的数据是原值,B提交事务,此时B查询出来 的数据是加上了C的修改后的数据,而并非期待的原值上B进行修改的数据,事务隔离失败?这就涉及到MVCC中快照的原理
MVCC的实现—快照
在RR级别下,事务启动时会基于整库拍个快照,用于记录下当前状态下的数据信息,如此的话,对于数据库比较大的情况,事务是否会启动的非常慢?然而并不是, 事务启动的非常快,原因在与InnoDB中的快照的实现
快照的实现
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
DB_TRX_ID:6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
DB_ROLL_PTR:7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
DB_ROW_ID:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
在InnoDB中,每个事务都有唯一的事务ID,叫做transaction id,在开启事务时,严格按照id递增的顺序向InnoDB事务系统申请,数据库中,每行数据都有多个版本,在每次开启更新事务时,都会生成一个新的数据版本,然后把此次事务生成的transaction id赋值给当前数据版本的事务ID,也就是每行数据都有自己的数据版本以及对应的transaction id事务ID,记为 row trx_id, 在每次更新时,都会生成一条回滚日志(undo log),同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说每个版本有自己的 row trx_id。而每次事务开启的数据版本物理上并不真实存在,而是通过当前事务版本和undo log计算出来的。
如所示,就是一个记录被多个事务连续更新后的状态。
图中的三个虚线箭头,就是 undo log(回滚日志);而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。
事务隔离的实现
在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值,假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录: