首页 > 数据库 > 数据库开发技术 > mysql的事务与实现原理
事务:由一组 SQL 语句组成的逻辑处理单元,事务具有以下四个属性,通常简称为事务的 ACID属性。
原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全部执行,要么全部不执行。
一致性(Consistent):事务必须使数据库从一个一致性状态变到另一个一致性状态。例如,一次转账的事务操作,要求A账户转出的钱,等于B账户转入的钱,实际上,一致性,是具体业务场景的约束
隔离性(Lsolation):数据库系统提供一定的隔离机制,保障事务在不受外部并发操作影响的“独立”环境执行。事务相互独立,互不干扰
持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能保持。
当两个或多个事务选择同一行,然后基于最初选定的值更新该行值,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题,最后的更新覆盖来其他事务所做的更新
一个事务正在对一条记录做修改,在这个事务完成并提交前,这个条记录的数据就处于不一致的状态;这时另外一个事务也来读取同一条记录,如果不加控制,第二个事务读取来这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。
总结:事务A读取到来事务B已经修改但尚未提交的数据,还在这个数据基础上做来操作。此时,如果事务B回滚,事务A读取的数据无效,不符合一致性要求。
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生来改变、或某些记录已经被删除了,这种现象就叫做“不可重复读”。
总结:事务A读取到了事务B已经提交的修改数据,不符合隔离性。
幻读
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
总结:事务A读取到了事务B提交的新增数据,不符合隔离性。
原子性,持久性和一致性主要是通过redo log、undo log、Force Log at Commit和Double Write机制来完成的。
redo log重做日志用于在崩溃时恢复数据
在事务提交之后,先去预写日志,保证持久化的落盘
undo log 用于对事务回滚时进行撤销,也会用于隔离性的多版本控制
Force Log at Commit 机制保证事务提交后redo log日志都已经持久化(由innodb_flush_log_at_trx_commit 控制)
Double Write 机制用来提高数据库的可靠性,用来解决脏页落盘时部分写失效问题

内存数据落盘的整体思路与分析
buffer pool存储数据页与索引页,事务提交后先写redo log Buffer,根据innodb_flush_log_at_trx_commit参数定时写入磁盘

innodb_flush_log_at_trx_commit = 0 就是每秒调用 flush + fsync ,定时器自己维护。
innodb_flush_log_at_trx_commit = 1 就是实时调用 flush + fsync 没法批处理,性能很低。
innodb_flush_log_at_trx_commit = 2 就是实时flush ,定时 fsync 交给OS维护定时器。
当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘。数据库只需对Checkpoint后的重做日志进行恢复,这样就大大缩短了恢复的时间。
当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行 Checkpoint,将脏页也就是页的新版本刷回磁盘。
当重做日志出现不可用时,因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让 其无限增大的。重做日志可以被重用的部分是指这些重做日志已经不再需要,当数据库发生宕机 时,数据库恢复操作不需要这部分的重做日志,因此这部分就可以被覆盖重用。如果重做日志还需 要使用,那么必须强制Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。
如果说Insert Buffer给InnoDB存储引擎带来了性能上的提升,那么Double Write带给InnoDB存储 引擎的是数据页的可靠性。

Double Write由两个部分组成,一部分是内存中的Double Write buffer,大小为2MB,另外一部分是物理磁盘上的共享表空间连续的128页,大小也为2MB。
在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是通过memcpy函数将脏页先复制到内存中的 double write buffer区域,之后通过double write buffer再分两次,每次1MB顺序地写入共享表 空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免操作系统缓冲写带来的问题。在完成 double write页的写入后,再讲double wirite buffer中的页写入各个表空间文件中。
如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间 中的double write中找到该页的一个副本,将其复制到表空间文件中,再应用重做日志。
“脏读”、“不可重复读”、“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决
事务有隔离性(Isolation),并发事务间相互独立,互不干扰;几个要点:
基本目标:不同线程,发起并发事务,要实现不同线程操作数据时,不互相干扰;
相互干扰:脏读、不可重复读、幻读;
一个事务中对数据所做的修改,即使没有提交,这个修改对其他的事务仍是可见的,这种情况下就容易出现脏读,影响了数据的完整性
SELECT的时候无法重复读,即同一个事务中两次执行同样的查询语句,若在第一次与第二次查询之间时间段,其他事务又刚好修改了其查询的数据且提交了,则两次读到的数据不一致。
- 这种级别可以避免“脏数据”
- 这种隔离级别会导致“不可重复读取”
SELECT的时候可以重复读,即同一个事务中两次执行同样的查询语句,得到的数据始终都是一致的。实现的原理是,在一个事务对数据行执行读取或写入操作时锁定了这些数据行。
但是这种方式又引发了幻想读的问题。因为只能锁定读取或写入的行,不能阻止另一个事务插入数据,后期执行同样的查询会产生更多的结果。
- MySQL默认级别
与可重复读的唯一区别是,默认把普通的SELECT语句改成SELECT …. LOCK IN SHARE MODE。即为查询语句涉及到的数据加上共享琐,阻塞其他事务修改真实数据。
serializable模式中,事务被强制为依次执行。这是SQL标准建议的默认行为。

数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。
同时,不同应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读” 并不敏感,可能更关系数据并发访问的能力。
SQL规范定义了以上四种隔离级别,但是并没有给出如何实现四种隔离级别,因此不同数据库的实现方式和使用方式也并不相同。而SQL隔离级别的标准是依据基于锁的实现方式来制定的
传统的隔离级别是基于锁实现的,我们先来了解一下锁
传统的锁有两种:
共享锁(Shared Locks):简称S锁,事务对一条记录进行读操作时,需要先获取该记录的共享锁。
排他锁(Exclusive Locks):简称X锁,事务对一条记录进行写操作时,需要先获取该记录的排他锁。
需要注意的是,加了共享锁的记录,其他事务也可以获得该记录的共享锁,但是无法获取该记录的排他锁,即S锁和S锁是兼容的,S锁和X锁是不兼容的;而加了排他锁的记录,其他事务既无法获取该记录的共享锁也无法获取排他锁,即X锁和X锁也是不兼容的。
事务对一条记录进行读操作时,需要先获取该记录的S锁,但有时事务在读取记录时需要阻止其他事务访问该记录,这时就需要获取该记录的X锁
读取时对记录加S锁:
SELECT ... LOCK IN SHARE MODE;
如果事务执行了该语句,则会在读取的记录上加S锁,这样就允许其他事务也能获取到该记录的S锁;而如果其他事务需要获取该记录的X锁,那么就需要等待当前事务提交后释放掉S锁。
读取时对记录加X锁:
SELECT ... FOR UPDATE;
如果事务执行了该语句,则会在读取的记录上加X锁,这样其他事务想要说去该记录的S锁或X锁,那么需要等待当前事务提交后释放掉X锁。
对于锁的粒度而言,锁又可以分为两种:
行锁:只锁住某一行记录,其他行的记录不受影响。
表锁:锁住整个表,所有对于该表的操作都会受影响。
最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,InnoDB是在undolog中实现的,通过undolog可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。
多版本并发控制(MVCC),来实现 MySQL 上的多事务并发访问时,隔离级别控制;
数据版本:并发事务执行时,同一行数据有多个版本
事务版本:每个事务都有一个事务版本
通过 MVCC 实现一种效果:
同一时刻,同一张表,多个并发事务,看到的数据是不同的

根据行为的不同,undo log分为两种:insert undo log和update undo log
是在 insert 操作中产生的 undo log
因为 insert 操作的记录只对事务本身可见,对于其它事务此记录是不可见的,所以 insert undo log 可以在事务提交后直接删除而不需要进行 purge 操作。
update undo log
是 update 或 delete 操作中产生的 undo log
因为会对已经存在的记录产生影响,为了提供 MVCC机制,因此 update undo log 不能在事务提交时 就进行删除,而是将事务提交时放到入 history list 上,等待 purge 线程进行最后的删除操作。
为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undo log的并发写入和持久化。回滚段实际上是一种 Undo 文件组织方式。
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL唯一键时都不会包含row_id列):
trx_id:每次对某条聚簇索引记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。
roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
比方表t现在只包含一条记录:
CREATE TABLE t (id INT PRIMARY KEY,c VARCHAR(100)Engine=InnoDB CHARSET=utf8;SELECT * FROM t;+----+--------+id | c |+----+--------+1 | alex |+----+--------+1 row in set (0.01 sec)
假设插入该记录的事务id为80,那么此刻该条记录的示意图如下所示:

后面两个id分别为100、200的事务对这条记录进行UPDATE操作,操作流程如下:

每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:

对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id
对于使用
READ UNCOMMITTED隔离级别的事务来说,直接读取记录的最新版本就好了,对于使用SERIALIZABLE隔离级别的事务来说,使用加锁的方式来访问记录
使用READ COMMITTED和REPEATABLE READ隔离级别的事务,就需要用到我们上边所说的版本链了
核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的
所以设计InnoDB的设计者提出了一个ReadView的概念,这个ReadView中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids。这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
如果被访问版本的trx_id属性值小于m_ids列表中最小的事务id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。
如果被访问版本的trx_id属性值大于m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。
如果被访问版本的trx_id属性值在m_ids列表中最大的事务id和最小事务id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
tip:
在MySQL中,READ COMMITTED和REPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同
1,READ COMMITTED --- 每次读取数据前都生成一个ReadView
2,REPEATABLE READ ---在第一次读取数据时生成一个ReadView
MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复这个ReadView就好了。
原文作者:五分钟学SRE;如有侵权,请联系管理员删除。