[精选] 一文讲清楚 MySQL 事务隔离级别和实现原理,开发人员必备知识点
文章来自:https://juejin.im/post/6844903827611582471
点击加入:
精选文章正文
了解事务之前,先来看看数据库为什么需要有事务,假设没有事务会有什么影响?
我们来捋一捋上述银行发生的过程,简单的分三步:
A发起转账10000给B -> A银行卡减10000元 -> B银行卡增加10000元。
上述案例是第三步出现了问题,如果有事务,则不会发生案例中的事情,可以理解为事务就是这三个步骤是一根绳子上的蚂蚱,要么都成功,要么都失败。
所以数据库引入事务的主要目的是事务会把数据库会从一种一致状态转换到另一种一致状态,数据库提交工作时可以确保要么所有修改都保存,要么所有修改都不保存。
了解事务,还需要了解事务的理论依据ACID,也可以说事务的几个特性。
Atomicity(原子性):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。
Consistency(一致性):数据库总是从一个一致性状态转换到另一个一致状态。下面的银行列子会说到。
Isolation(隔离性):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。注意这里的“通常来说”,后面的事务隔离级级别会说到。
Durability(持久性):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。(持久性的安全性与刷新日志级别也存在一定关系,不同的级别对应不同的数据安全级别。)
继续用银行账户转账为例,更好地理解ACID
START TRANSACTION;
SELECT balance FROM checking WHERE customer_id = 10233276;
UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276;
COMMIT;
原子性:要么完全提交(10233276的checking余额减少200,savings 的余额增加200),要么完全回滚(两个表的余额都不发生变化)
一致性:这个例子的一致性体现在 200元不会因为数据库系统运行到第3行之后,第4行之前时崩溃而不翼而飞,因为事务还没有提交。
隔离性:允许在一个事务中的操作语句会与其他事务的语句隔离开,比如事务A运行到第3行之后,第4行之前,此时事务B去查询checking余额时,它仍然能够看到在事务A中被减去的200元(账户钱不变),因为事务A和B是彼此隔离的。在事务A提交之前,事务B观察不到数据的改变。
持久性:这个很好理解。
事务的隔离性是通过锁、MVCC等实现
事务的原子性、一致性和持久性则是通过事务日志实现
事务的隔离级别
并发事务带来了哪些问题呢?
更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 --最后的更新覆盖了由其他事务所做的更新。
例如,两个编辑人员制作了同一 文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。
最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同 一文件,则可避免此问题。
脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前, 这条记录的数据就处于不一致状态;这时, 另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"。
不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读” 。
幻读 (Phantom Reads): 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读” 。
并发事务处理带来的问题的解决办法:
更新丢失”通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。
脏读” 、 “不可重复读”和“幻读” ,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决:
一种是加锁:在读取数据前,对其加锁,阻止其他事务对数据进行修改。
另一种是数据多版本并发控制(MultiVersion Concurrency Control,简称 MVCC 或 MCC),也称为多版本数据库:不用加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本。
SQL标准定义了4类隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
四种隔离级别,按READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE顺序,隔离级别是从低到高,InnoDB默认是REPEATABLE-READ级别,此级别在其余数据库中是会引起幻读问题,InnoDB采用Next-Key Lock锁算法避免了此问题
1 READ-UNCOMMITTED 中文叫未提交读,即一个事务读到了另一个未提交事务修改过的数据,整个过程如下图:
如上图,SessionA和SessionB分别开启一个事务,SessionB中的事务先将id为1的记录的name列更新为'lisi',然后Session 中的事务再去查询这条id为1的记录,那么在未提交读的隔离级别下,查询结果由'zhangsan'变成了'lisi',
也就是说某个事务读到了另一个未提交事务修改过的记录。但是如果SessionB中的事务稍后进行了回滚,那么SessionA中的事务相当于读到了一个不存在的数据,这种现象也称为脏读。
可见READ-UNCOMMITTED是非常不安全。
2 READ COMMITTED 中文叫已提交读,或者叫不可重复读。即一个事务能读到另一个已经提交事务修改后的数据,如果其他事务均对该数据进行修改并提交,该事务也能查询到最新值。如下图:
在第4步 SessionB 修改后,如果未提交,SessionA是读不到,但SessionB一旦提交后,SessionA即可读到SessionB修改的内容。
从某种程度上已提交读是违反事务的隔离性的。
3 REPEATABLE READ 中文叫可重复读,即事务能读到另一个已经提交的事务修改过的数据,但是第一次读过某条记录后,即使后面其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据。如下图:
InnoDB默认是这种隔离级别,SessionB无论怎么修改id=1的值,SessionA读到依然是自己开启事务第一次读到的内容。
4 SERIALIZABLE 叫串行化, 上面三种隔离级别可以进行 读-读 或者 读-写、写-读三种并发操作,而SERIALIZABLE不允许读-写,写-读的并发操作。如下图:
SessionB 对 id=1 进行修改的时候,SessionA 读取id=1则需要等待 SessionB 提交事务。可以理解SessionB在更新的时候加了X锁。
Mysql中的事务实现原理
事务的实现是基于数据库的存储引擎。不同的存储引擎对事务的支持程度不一样。mysql中支持事务的存储引擎有innoDB和NDB。
innoDB是mysql默认的存储引擎,默认的隔离级别是RR(Repeatable Read),并且在RR的隔离级别下更进一步,通过多版本并发控制(MVCC,Multiversion Concurrency Control )解决不可重复读问题,加上间隙锁(也就是并发控制)解决幻读问题。因此innoDB的RR隔离级别其实实现了串行化级别的效果,而且保留了比较好的并发性能。
事务的隔离性是通过锁实现,而事务的原子性、一致性和持久性则是通过事务日志实现。说到事务日志,不得不说的就是redo和undo。
redo_log 实现持久化和原子性,而undo_log实现一致性,二种日志均可以视为一种恢复操作,redo_log是恢复提交事务修改的页操作,而undo_log是回滚行记录到特定版本。
二者记录的内容也不同,redo_log是物理日志,记录页的物理修改操作,而undo_log是逻辑日志,根据每行记录进行记录。
redo_log 重做日志
redo_log 重做日志上面已经提到实现持久化和原子性,重做日志由两部分组成,一是内存中的重做日志缓存(redo log buffer),这部分是容易丢失的。二是重做日志文件(redo log file),这部分是持久的。
知道redo_log是什么?还需要了解其更新流程以及redo log存的是什么内容和恢复机制。
1 更新流程
先来了解第一个问题,redo log的更新流程如下图,以一次Update 操作为例。
执行update操作。
先将原始数据从磁盘读取到内存,修改内存中的数据。
生成一条重做日志写入redo log buffer,记录数据被修改后的值。
当事务提交时,需要将redo log buffer中的内容刷新到redo log file。
事务提交后,也会将内存中修改数据的值写入磁盘。
为了确保每次日志都写入重做日志文件,InnoDB存储引擎会调用一次fsync操作。
2 存储格式内容
了解redo log存储格式和内容之前,先来对比一下跟binlog二进制日志由什么不同,binlog主要是主从复制和进行POINT-IN-TIME的恢复,想必大家对它不陌生。
binlog只有在事务提交的时候才会写入,且是数据库的上层产生的。redo log是Innodb引擎层产生的。
对比一二者的写入方式:
binlog是每次事务才写入,所以每个事务只会有一条日志,记录的逻辑日志,也可以说记录的就是SQL语句。
redo log是事务开始就开始写入,*T1表示事务提交。记录的是物理格式日志,即每个页的修改。
redo log默认是以block(块)的方式为单位进行存储,每个块是512个字节。不同的数据库引擎有对应的重做日志格式,Innodb的存储管理是基于页的,所以其重做日志也是基于页的。
redo log格式:
redo_log_type 重做日志类型
space 表空间的ID
page_no 页的偏移量
redo_log_body 存储内容
执行一条插入语句,重做日志大致为:
INSERT INTO user SELECT 1,2;
|
page(2,3), offset 32, value 1,2 # 主键索引
page(2,4), offset 64, value 2 # 辅助索引
可以看到重做日志存储的格式有点看不太懂,看不懂没有关系,主要是告诉大家,
重做日志存储物理格式日志,也就是基于存储页的修改。
3 恢复机制
再来了解一下 redo log的恢复机制:
上图概况了重做日志的恢复机制,先来解释一下图中出现的 LSN 是什么?
LSN(Log Sequence Number) 日志序列号,Innodb里,LSN占8个字节,且是单调递增的,代表的含义有: 重做日志写入的总量、checkpoint的位置、页的版本。
假设在LSN=10000的时候数据库出现故障,磁盘中checkpoint为10000,表示磁盘已经刷新到10000这个序列号,当前redolog的checkpoint是13000,则需要恢复10000-13000的数据。
再来想想,redo log为什么可以实现事务的原子性和持久性。
原子性,是redo log记录了事务期间操作的物理日志,事务提交之前,并没有写入磁盘,保存在内存里,如果事务失败,数据库磁盘不会有影响,回滚掉事务内存部分即可。
持久性,redo log 会在事务提交时将日志存储到磁盘redo log file,保证日志的持久性。
undo log
redo log一旦提交意味着持久化了,但是有时候需要对其进行rollback操作,那就需要undo log。
undo log是逻辑日志,只是将数据库逻辑的恢复到原来的样子。并不能将数据库物理地恢复到执行语句或者事务之前的样子。虽然所有的逻辑修改均被取消了,但是数据结构和页本身在回滚前后可能不一样了。
既然是逻辑日志,可以理解为它存储的是SQL, 在事务中使用的每一条 INSERT 都对应了一条 DELETE,每一条 UPDATE 也都对应一条相反的 UPDATE 语句。
undo log 存放在数据库内部的一个特殊段(segment)中,也叫undo段,存在于共享表空间中。
undo log实现了事务的一致性,可以通过undo log恢复到事务之前的逻辑状态,保证一致性。
undo log 还可以实现MVCC(Multi-Version Concurrency Control ,多版本并发控制),多版本并发控制其实可以通过 undo log 形成一个事务执行过程中的版本链,每一个写操作会产生一个版本,数据库发生读的并发访问时,读操作访问版本链,返回最合适的结果直接返回。从而读写操作之间没有冲突,提高了性能。
以上是本文的全部内容,希望对大家的学习有帮助,也希望大家多多支持 php自学中心 ,学习与交流少不了一个圈子,点击加技术群: