MySQL基本原理:事务、隔离、锁
参考资料:
https://www.cnblogs.com/kismetv/p/10331633.html
https://www.cnblogs.com/wyc1994666/p/11367051.html
一、基础概念
1.1 事务简介
作为一个关系型数据库,MySQL支持事务(Transaction)。
事务是MySQL等关系型数据库区别于NoSQL的重要方面,是保证数据一致性的重要手段。
(1)事务是访问和更新数据库的程序执行单元;
(2)在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务;
(3)事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行;
(4)事务用来管理 insert、update、delete 语句;
1.2 逻辑架构和存储引擎
MySQL服务器逻辑架构从上往下分为三层:
(1)处理客户端连接、授权认证等;
(2)服务器层,负责查询语句的解析、优化、缓存以及内置函数的实现、存储过程等;
(3)存储引擎,负责MySQL中数据的存储和提取。**MySQL中服务器层部管理事务,事务是由存储引擎实现的。**MySQL支持事务的存储引擎由InnoDB、NDB Cluster等,其中InnoDB的使用最为广泛;其他存储引擎不支持事务,如MyISAM、Memory等;
1.3 事务要达到什么效果
(1)可靠性:数据库要保证当insert或update操作时抛异常或者数据库crash的时候需要保障数据的操作前后的一致,想要做到这个,需要指导修改前后的状态,所以由了undo log和redo log;
(2)并发处理:当多个并发请求过来,并且其中有一个请求是对数据修改操作的时候会有影响,为了避免读到脏数据,所以需要对事务之间的读写进行隔离,至于隔离到什么程度取决于业务场景,MySQL隔离级别就是 用来实现该效果的。
1.4 提交和回滚
(1)典型的MySQL事务是如下操作的:
start transaction;
…… #一条或多条sql语句
commit;
其中start transaction标识事务的开始,commit提交事务,将执行结果写入到数据库;
如果sql语句执行踹性能问题,会调用rollback回滚所有已执行成功的sql语句,也可以再事务中直接使用rollback进行回滚。
(2)自动提交:
MySQL中默认采用的是自动提交(autocommit)模式,在自动提交模式下,如果没有start transaction显式地开始事务,那么每个sql语句都会被当作一个事务执行提交操作;
如果关闭了自动提交,则所有的sql语句都在一个事务中,直到执行了commit或者rollback,该事务结束,同时开始了另一个事务,可以通过set autocommit = 0;
来关闭。
(3)特殊操作
在MySQL中,存在一些特殊命令,如果在事务中执行了这些命令,会马上强制执行commit提交事务:
如DDL语句(creat table/drop table/alter table)、lock tables语句
常用的select、insert、update、delete等不会强制提交事务。
1.5 事务四大特性:ACID特性
注意:按照严格的标准,只有同时满足ACID特性才是事务;但是在各大数据库厂商的实现中,真正满足ACID的事务少之又少。例如MySQL的NDB Cluster事务不满足持久性和隔离性;InnoDB默认事务隔离级别是可重复读,不满足隔离性;Oracle默认的事务隔离级别为READ COMMITTED,不满足隔离性……因此与其说ACID是事务必须满足的条件,不如说它们是衡量事务的四个维度。
(1)原子性(Atomicity,或称为不可分割性):
原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态;
(2)一致性(Consistency):
一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态,这表示写入的资料必须完全符合所有的预设规则;数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。
(3)隔离性(Isolation):
隔离性研究的是不同事物之间的相互影响,隔离性是指事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能相互干扰,防止多个事务并发执行时由于交叉执行而导致数据不一致;
事务隔离分为不同级别:未提交读(read uncommitted)、提交读(read committed)、可重复读(repeatable read)、串行化(serializable);
严格的隔离性对应了可串行化,但实际应用中处于性能方面考虑很少使用可串行化(银行卡业务场景下会使用,比如在从银行卡中取钱的过程中不能向该银行卡中打钱);
隔离性追求的是并发情境下事务间互不干扰,简单起见主要考虑读操作和写操作,则隔离性的讨论可以分为两个方面:
a.一个事务写操作对另一个事务写操作的影响:锁机制保证隔离性;
b.一个事务写操作对另一个事务读操作的影响:MVCC保证隔离性。
(4)持久性(Durability):
持久性指事务一旦提交,它对数据库的改变就应该是永久性的,接下来的其他操作或古战更不应该对其有任何影响。
1.6 实现事务功能的三个技术
(1)日志文件(redo log和undo log);
(2)锁技术;
(3)MVCC;
事务特性 | 实现技术 |
---|---|
原子性 | undo log |
持久性 | redo log |
隔离性 | 锁机制+MVCC |
一致性 | 原子性+持久性+隔离性 |
ACID只是个概念,事务最终目的是要保障数据的可靠性,一致性。
二、实现技术
2.1 redo log 与 undo log
MySQL的事务日志:MySQL的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等;
此外InnoDB存储引擎还提供了两种事务日志:redo log(重做日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。
1. redo log
(1)概念
redo log 叫做重做日志,用来实现事务的持久性。日志文件由两部分组成:重做日志缓冲(redo log buffer,在内存中)以及重做日志文件(redo log,在磁盘中)。当事务提交之后会把所有修改信息存到该日志中,如下示例:
假设有表tb1(id, username),现插入数据(3, ceshi)
start transaction;
select balance from bank where name = "zhangsan";
// 生成 重做日志 balance = 600
update bank set balance = balance - 400;
// 生成 重做日志 amount = 400;
update finance set amount = amount + 400;
commit;
- 第一步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝。
- 第二步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值。
- 第三步:在必要的时候,采用追加写的方式将 redo log buffer 中的内容刷新到 redo log file。
- 第四步:定期将内存中修改的数据刷新到磁盘中。
(2)作用
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为了提升性能,InnoDB提供了缓冲池(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏,是使用后台线程去做缓冲池和磁盘之间的同步)。
在这种模式下,如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘(红线部分未完成),就会导致数据的丢失,事务的持久性无法保证。
因此引入redo log来记录已成功提交事务的修改信息,并将redo log持久化到磁盘,系统重启之后再读取redo log恢复最新数据。具体地,当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
综上,redo log 是用来恢复数据的,用于保障已提交事务的持久化特性。
思考:
Q1.既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?
-
1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
-
2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。
Q2.redo log与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的写入时机相对多元。
2. undo log
(1)概念
undo log 叫做回滚日志,用于记录数据被修改前的信息。正好与重做日记记录相反,重做日志记录数据被修改后的信息。undo log 主要记录数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,在发生错误时才可以回滚。示例如下:
每次写入数据或者修改数据之前都会把修改前的信息记录到undo log。
(2)作用
undo log 记录实物修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log 的信息来回滚到没被修改前的状态。
undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。
以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
综上,undo log 是用来回滚数据的,用于保障未提交事务的原子性。
2.2 MySQL锁技术以及MVCC基础
1. MySQL锁技术
当有多个请求读取表中的数据时可以不采取操作;但是多个请求中既有读又有写(修改)请求时必须进行并发控制,否则会造成不一致。
(1)读写锁
用锁来对读写请求进行控制:
共享锁(shared lock),又称为读锁:读锁是可以共享的,多个读请求可以共享一把锁读数据,不会阻塞;
排他锁(exclusive lock),又称为写锁:写锁会排斥其他所有获取锁的请求,一直阻塞,直到写入完成释放锁。
读锁 | 写锁 | |
---|---|---|
读锁 | 可并行 | 不可并行 |
写锁 | 不可并行 | 不可并行 |
事务的隔离性就是通过读写锁实现的。
2. MVCC基础
MVCC(MultiVersion Concurrency Control)多版本并发控制。
“InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存了行的过期时间,当然存储的并不是实际的时间值,而是系统版本号。” —— 《高性能MySQL》
MVCC主要实现思想是通过数据多版本来做到读写分离,从而实现不加锁读进而做到读写并行。
MVCC在MySQL中的实现依赖的是undo log 与 read view
- undo log:undo log 中记录某行数据的多个版本的数据
- read view: 用来判断当前版本数据的可见性
三、事务的实现
3.1 原子性的实现 —— undo log
“一个事务必须被视为不可分割的最小工作单位,一个事务中的所有操作要么全部成功提交,要么全部失败回滚,对于一个事务来说不可能只执行其中的部分操作,这就是事务的原子性。” ——《高性能MySQL》
数据库通过回滚操作实现上述目的。
所谓回滚操作就是当发生错误异常或者显式的执行rollback语句时需要把数据还原到原先的模样,所以这时候就需要用到undo log来进行回滚。
1. undo log 生成
从上图可以了解到数据的变更都伴随着回滚日志的产生:
(1) 产生了被修改前数据(zhangsan,1000) 的回滚日志
(2) 产生了被修改前数据(zhangsan,0) 的回滚日志
根据上面流程可以得出如下结论:
1.每条数据变更(insert/update/delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上
2.所谓的回滚就是根据回滚日志做逆向操作,比如delete的逆向操作为insert,insert的逆向操作为delete,update的逆向为update等。
思考:为什么先写日志后写数据库?
2. 根据undo log 回滚
为了做到同时成功或者失败,当系统发生错误或者执行rollback操作时需要根据undo log 进行回滚。
回滚操作就是要还原到原来的状态,undo log记录了数据被修改前的信息以及新增和被删除的数据信息,根据undo log生成回滚语句,比如:
(1) 如果在回滚日志里有新增数据记录,则生成删除该条的语句
(2)如果在回滚日志里有删除数据记录,则生成生成该条的语句
(3)如果在回滚日志里有修改数据记录,则生成修改到原先数据的语句
3.2 持久性的实现 —— redo log
在介绍redo log的时候已经讲的很较详细了
事务一旦提交,其所作做的修改会永久保存到数据库中,此时即使系统崩溃修改的数据也不会丢失。
先了解一下MySQL的数据存储机制,MySQL的表数据是存放在磁盘上的,因此想要存取的时候都要经历磁盘IO,然而即使是使用SSD磁盘IO也是非常消耗性能的。
为此,为了提升性能InnoDB提供了缓冲池(Buffer Pool),Buffer Pool中包含了磁盘数据页的映射,可以当做缓存来使用:
读数据:会首先从缓冲池中读取,如果缓冲池中没有,则从磁盘读取在放入缓冲池;
写数据:会首先写入缓冲池,缓冲池中的数据会定期同步到磁盘中;
上面这种缓冲池的措施虽然在性能方面带来了质的飞跃,但是它也带来了新的问题,当MySQL系统宕机,断电的时候可能会丢数据!!!
因为我们的数据已经提交了,但此时是在缓冲池里头,还没来得及在磁盘持久化,所以我们急需一种机制需要存一下已提交事务的数据,为恢复数据使用。
于是 redo log就派上用场了。下面看下redo log是什么时候产生的
既然redo log也需要存储,也涉及磁盘IO为啥还用它?
(1)redo log 的存储是顺序存储,而缓存同步是随机操作。
(2)缓存同步是以数据页为单位的,每次传输的数据大小大于redo log。
3.3 隔离性的实现
隔离性是事务ACID特性里最复杂的一个。在SQL标准里定义了四种隔离级别,每一种级别都规定一个事务中的修改,哪些是事务之间可见的,哪些是不可见的。
Mysql 隔离级别有以下四种(级别由低到高):
- READ UNCOMMITED (未提交读)
- READ COMMITED (提交读)
- REPEATABLE READ (可重复读)
- SERIALIZABLE (串行化)
前面说过原子性,隔离性,持久性的目的都是为了要做到一致性,但隔离型跟其他两个有所区别:
-
原子性和持久性是为了要实现数据的可性保障靠,比如要做到宕机后的恢复,以及错误后的回滚;
-
隔离性是要管理多个并发读写请求的访问顺序。 这种顺序包括串行或者是并行;写请求不仅仅是指insert操作,又包括update操作。
从隔离性的实现可以看出这是一场数据的可靠性与性能之间的权衡。
- 可靠性性高的,并发性能低(比如 Serializable)
- 可靠性低的,并发性能高(比如 Read Uncommited)
1. READ UNCOMMITTED 未提交读
在READ UNCOMMITTED隔离级别下,事务中的修改即使还没提交,对其他事务是可见的。事务可以读取未提交的数据,造成脏读。
因为读不会加任何锁,所以写操作在读的过程中修改数据,所以会造成脏读。好处是可以提升并发处理性能,能做到读写并行。
优点:读写并行,性能高;
缺点:脏读。
2. READ COMMITTED 提交读
InnoDB在 READ COMMITTED,写数据使用排它锁,读取数据不加锁而是使用了MVCC机制。或者换句话说他采用了读写分离机制。
但是该级别会产生不可重读以及幻读问题。
不可重读:在一个事务内多次读取的结果不一样;
这跟 READ COMMITTED 级别下的MVCC机制有关系,在该隔离级别下每次 select 的时候新生成一个版本号,所以每次select的时候读的不是一个副本而是不同的副本。
在每次select之间有其他事务更新了我们读取的数据并提交了,那就出现了不可重复读
3. REPEATABLE READ 可重复读(MySQL默认隔离级别)
在一个事务内的多次读取的结果是一样的。这种级别下可以避免,脏读,不可重复读等查询问题。
MySQL 有两种机制可以达到这种隔离级别的效果,分别是采用读写锁以及MVCC。
(1)采用读写锁实现:
为什么能可重复度?只要没释放读锁,在第二次读的时候还是可以读到第一次读的数据。
优点:实现起来简单
缺点:无法做到读写并行
(2)采用MVCC实现:
为什么能可重复度?因为多次读取只生成一个版本,读到的自然是相同数据。
优点:读写并行
缺点:实现的复杂度高
在该隔离级别下仍会存在幻读的问题。
幻读与不可重复度都是多次读取数据不一致,区别在于:
-
不可重复读是针对修改update操作的;
-
幻读是针对增加insert和删除delete操作的;
因此,解决不可重复读问题只需要采用行级锁,解决幻读需要采用表级锁。
4. SERIALIZABLE 串行化
该隔离级别理解起来最简单,实现也最简单。在隔离级别下除了不会造成数据不一致问题,没其他优点。
3.4 一致性的实现
下面举个例子:zhangsan 从银行卡转400到理财账户
start transaction;
select balance from bank where name="zhangsan";
// 生成 重做日志 balance=600
update bank set balance = balance - 400;
// 生成 重做日志 amount=400
update finance set amount = amount + 400;
commit;
(1)假如执行完 update bank set balance = balance - 400;
之发生异常了,银行卡的钱也不能平白无辜的减少,而是回滚到最初状态——原子性
(2)又或者事务提交之后,缓冲池还没同步到磁盘的时候宕机了,这也是不能接受的,应该在重启的时候恢复并持久化——持久性;
(3)假如有并发事务请求的时候也应该做好事务之间的可见性问题,避免造成脏读,不可重复读,幻读等。在涉及并发的情况下往往在性能和一致性之间做平衡,做一定的取舍,所以隔离性也是对一致性的一种破坏——隔离性
3.5 总结
实现事务采取了哪些技术以及思想?
- 原子性:使用 undo log ,从而达到回滚;
- 持久性:使用 redo log,从而达到故障后恢复
- 隔离性:使用锁以及MVCC,运用的优化思想有读写分离,读读并行,读写并行
- 一致性:通过回滚,以及恢复,和在并发环境下的隔离做到一致性。