当前位置:网站首页>MySQL事务及实现原理全面总结,再也不用担心面试
MySQL事务及实现原理全面总结,再也不用担心面试
2022-07-06 09:21:00 【李孛欢】
一. 事务transanction的四个基本要素
简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败,它具有以下四个基本要素。
ACID:原子性(Atomicity)、
一致性(Correspondence)、
隔离性(Isolation)、
持久性(Durability)
1、原子性:事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。如果事务在执行过程中发生错误,事务会被回滚(Rollback)到事务开始前的状态;
2、一致性:事务开始前和结束后,数据库的完整性约束没有被破坏。比如A向B转账,不可能A被扣了钱,B却没收到转账。要保证最后现实逻辑与数据库操作后的数据一致
3、隔离性:事务的隔离性意味着并发的事务之间是相互隔离的。即一个事务的内部操作及正在操作的数据必须封锁起来,不被企图进行修改的其他事务看到。
4、持久性:事务完成后,事务对数据库的所有更新将被保存起来,不会回滚。
一致性是事务的最终目的,原子性、隔离性、持久性都是为了实现一致性!!事务的执行流程如下图所示:下文会具体解释相关日志和该执行流程
二. 原子性
在了解原子性实现原理之前,我们先要了解一下MySQL的WAL技术,WAL全称为Write-Ahead Logging,预写日志系统。其主要是指MySQL在执行写操作的时候并不是立刻更新到磁盘上,而是先记录在日志中,并更新内存,之后在系统空闲的时候将其更新到磁盘中。日志主要分为undo log、redo log、binlog。
原子性的实现主要依靠的是Undo log日志。原子性的体现主要是在sql在执行过程中发生错误而发生回滚上。回滚是要回到执行前的一个状态,那么怎么回到执行前的状态呢?我们是不是就得将执行前的状态记录下来。Undo log 就是实现这个功能的一个日志。
这个回滚日志Undo log,记录的东西非常简单:
- 比如如果在缓存页里执行了一个insert语句,那么此时undo log必须记录了插入数据的主键ID,回滚的时候就可以从缓存页里把这条数据给删除了;
- 如果在缓存页里执行了一个delete语句,那么undo log必须记录下来被删除的数据,回滚的时候就得重新插入一条数据
- 如果在缓存页里执行了一个update语句,那么起码要把更新之前的那个值记录下来,回滚的时候重新update一下,把之前更新的旧值更新回去。
- 如果在缓存页里执行了一个select语句,因为没有改变buffer pool,因此不需要任何undo log。
Redo log用来记录某数据块被修改后的值,可以用来恢复未写入 data file 的已成功事务更新的数据;Undo log是用来记录数据更新前的值,保证数据更新失败能够回滚。
三. 持久性
我们需要先来了解下InnoDB是怎么来读写数据的。我们知道数据库的数据都是存放在磁盘中的,但是磁盘I/O的成本是很大的,如果每次读写数据都要访问磁盘,数据库的效率就会非常低。为了解决这个问题,InnoDB提供了 Buffer Pool 作为访问数据库数据的缓冲。
Buffer Pool 是位于内存的,包含了磁盘中部分数据页的映射。当需要读取数据时,InnoDB会首先尝试从Buffer Pool中读取,读取不到的话就会从磁盘读取后放入Buffer Pool;当写入数据时,会先写入Buffer Pool的页面,并把这样的页面标记为dirty,并放到专门的flush list上,这些修改的数据页会在后续某个时刻被刷新到磁盘中(这一过程称为刷脏,由其他后台线程负责) 。
通过前面的介绍,我们知道InnoDB使用 Buffer Pool 来提高读写的性能。但是 Buffer Pool 是在内存的,是易失性的,如果一个事务提交了事务后,MySQL突然宕机,且此时Buffer Pool中修改的数据还没有刷新到磁盘中的话,就会导致数据的丢失,事务的持久性就无法保证。为了解决这个问题,InnoDB引入了 redo log来实现数据修改的持久化。根据我们在上面所介绍的WAL机制,先写日志,再写磁盘,有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个 能力称为crash-safe。
redo log是由两部分组成的:一是内存中的重做日志缓冲(redo log buffer);二是用来持久化的重做日志文件(redo log file)。我们的数据最开始是在内存之中的,当我们提交事务的时候,redo log会有三种提交方式,来把内存的数据写到磁盘当中,这三种方式可以设置的
- 从用户空间写到日志空间里去,然后每秒钟从日志空间写到操作系统内存中,然后调用fsync()写到磁盘当中(不会丢失数据,效率较低)
- 从内存空间写到操作系统内存,然后调用fsync()直接写到磁盘当中(效率最高,会丢失一些数据)
- 从内存空间写到操作系统内存,然后每秒调用fsync()直接写到磁盘当中(不会丢失数据,效率较低)
事务提交的时候,写入redo log 相比于直接刷脏的好处主要有三点:
- 刷脏是随机I/O,但写redo log 是顺序I/O,顺序I/O比随机I/O快。
- 刷脏是以数据页(Page)为单位的,即使一个Page只有一点点修改也要整页写入;而redo log中只包含真正被修改的部分,数据量非常小,无效IO大大减少。
- 刷脏的时候可能要刷很多页的数据,无法保证原子性(例如只写了一部分数据就失败了),而redo log buffer 向 redo log file 写log block,是按512个字节,也就是一个扇区的大小进行写入,扇区是写入的最小单位,因此可以保证写入是必定成功的。
四. 隔离性
最后我们来看面试被问的最多的隔离性。当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non- repeatable read)、幻读(phantomread)的问题,为了解决这些问题,就有了“隔离级别”的概念。
- 脏读:如果一个事务A对数据进行了更改,但是还没有提交,而另一个事务B就可以读到事务A尚未提交的更新结果。这样,当事务A进行回滚时,那么事务B开始读到的数据就是一笔脏数据。
- 不可重复读:同一个事务对同一个数据进行读取操作,读取到的结果不同。例如,事务B在事务A的更新操作前读到的数据,跟在事务A提交此更新操作后读到的数据,可能不同。
- 幻读:就是一个事务读到另一个事务新增加并提交的数据(insert)。在同一个事务中,对于同一组数据读取到的结果不一致。比如,事务A 新增了一条记录,事务B 在 事务A 提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录。幻读出现的原因就是由于事务并发新增记录而导致的。
不可重复读和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于删除或新增!!
下面我们来看为了解决这些问题出现的隔离级别。首先要知道,隔离得越严实,效率就会越低。因此很多时候,我们都要 在二者之间寻找一个平衡点。SQL标准的事务隔离级别包括:读未提交(read uncommitted)、 读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。 具体解释如下:
- 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
- 读已提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一 致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
- 串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突 的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
数据库的隔离性就是通过加锁和MVCC来实现的。从上面可以看到,可重复读的隔离级别会出现幻读的问题,而MySQL的默认隔离级别是可重复读,并且解决了幻读的问题。简单来说,MySQL的默认隔离级别解决了脏读、幻读、不可重复读的问题。我们先来看数据库并发场景有哪些
数据库并发场景有三种,分别为:
- 读-读:不存在任何问题,也不需要并发控制
- 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
- 写-写:有线程安全问题,可能存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
写-写操作的线程安全是通过加锁来实现的,具体可以看我之前总结的一篇文章:MySQL锁总结(全面简洁 + 图文详解)_李孛欢的博客-CSDN博客 。但是加锁的操作会严重影响数据库的性能和并发量,因此出现了MVCC---多版本并发控制。MVCC是一种用来解决读-写冲突的无锁并发控制,MVCC在数据库中的实现,就是为了解决读(快照读)写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。MVCC可以为数据库解决以下问题:
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
- 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
下面我们来看看MVCC的具体实现原理,参考:阿里P7要求这么低吗?老哥给你讲清楚什么是MySQL的MVCC_哔哩哔哩_bilibili
什么是当前读和快照读?
- 当前读
像select lock in share mode(共享锁),select for update;update,insert,delete(排他锁)这些操作都是一种当前读。就是它读取的是记录的最新版本,读取时要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
- 快照读
像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的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产生一个聚簇索引
如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本。
如上所示,undo log和回滚指针将最新记录和历史记录连接起来形成了一个版本链。那么我读取数据的时候到底选择哪个版本呢? 这就要用read view来决定了。
read view 由下面这四部分组成。所谓活跃的事务ID就是还没有commit的事务id。那么readview是如何判断的呢?如下图所示:
这里的trx_id是每一个版本的事务id,如前面的图所示,张三的trx_id为1,李四的为2。
- 第一种情况说明,当前这条记录是我自己创建的,当然可以读取。
- 第二种情况就是trx_id小于最小的活跃事务id,min_trx_id,那就说明trx_id是已经commit了,那么该条数据可以被访问。
- 第三种情况如果trx_id > max_trx_id,读取不到,因为已经超出了版本链。
- 第四种情况,如果trx_id在min和max之间,就要看trx_id是不是在m_ids中,如果在,就不可访问,因为还没有commit。
了解了MVCC的原理,我们来看看RC和RR是怎么通过MVCC实现的
RC、RR级别下的InnoDB快照读有什么不同?
正是Read View生成的时机不同,从而造成RC、RR级别下的快照读的结果不同。
- 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
- 即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
- 而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因
总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View,这样也解决了快照读的幻读问题。当前读的幻读问题是通过间隙锁解决的。
边栏推荐
- Questions and answers of "signal and system" in the first semester of the 22nd academic year of Xi'an University of Electronic Science and technology
- 最新坦克大战2022-全程开发笔记-1
- Tyut Taiyuan University of technology 2022 introduction to software engineering summary
- Arduino+ water level sensor +led display + buzzer alarm
- 5月27日杂谈
- String class
- [中国近代史] 第九章测验
- 4.二分查找
- 3. C language uses algebraic cofactor to calculate determinant
- C language to achieve mine sweeping game (full version)
猜你喜欢
MPLS experiment
Questions and answers of "basic experiment" in the first semester of the 22nd academic year of Xi'an University of Electronic Science and technology
凡人修仙学指针-1
4.二分查找
西安电子科技大学22学年上学期《基础实验》试题及答案
3.猜数字游戏
TYUT太原理工大学2022数据库大题之概念模型设计
TYUT太原理工大学2022数据库大题之分解关系模式
Differences and application scenarios between MySQL index clock B-tree, b+tree and hash indexes
Decomposition relation model of the 2022 database of tyut Taiyuan University of Technology
随机推荐
Change vs theme and set background picture
8.C语言——位操作符与位移操作符
View UI Plus 发布 1.1.0 版本,支持 SSR、支持 Nuxt、增加 TS 声明文件
凡人修仙学指针-2
View UI plus released version 1.3.0, adding space and $imagepreview components
Atomic and nonatomic
Smart classroom solution and mobile teaching concept description
西安电子科技大学22学年上学期《基础实验》试题及答案
Relational algebra of tyut Taiyuan University of technology 2022 database
Common method signatures and meanings of Iterable, collection and list
Counter attack of flour dregs: redis series 52 questions, 30000 words + 80 pictures in detail.
5月27日杂谈
20220211-CTF-MISC-006-pure_ Color (use of stegsolve tool) -007 Aesop_ Secret (AES decryption)
Set container
2.C语言初阶练习题(2)
3. C language uses algebraic cofactor to calculate determinant
TYUT太原理工大学2022数据库大题之概念模型设计
Inheritance and polymorphism (I)
IPv6 experiment
西安电子科技大学22学年上学期《信号与系统》试题及答案