万事皆由不是空穴来风,任何技术的底层都有一套严谨的架构。
前景
说到事务,我们都知道“回滚”这个概念,在一个方法上加上@Transactional 一旦程序遇到异常就会自动回滚,数据库的数据原封不动,到底是怎么实现回滚机制?其中 isolation隔离级别又是干什么?让我们一起从Mysql底层架构开始谈起。
一、Mysql架构分析
首先看下逻辑架构图,
image.png
初学Mysql这个架构图放在眼前有点懵比了吧,每个模块我们分析来看:
1、Connectors 连接器,指的是不同语言中与SQL的交互
2、Management Serveices & Utilities:系统管理和控制工具
3、Connection Pool: 连接池
1)管理缓冲用户连接,线程处理等需要缓存的需求。
2)负责监听对 MySQL Server 的各种请求,接收连接请求,转发所有连接请求到线程管理模块。每一个连接上 MySQL Server 的客户端请求都会被分配(或创建)一个连接线程为其单独服务。
3)而连接线程的主要工作就是负责 MySQL Server 与客户端的通信,接受客户端的命令请求,传递 Server 端的结果信息等。线程管理模块则负责管理维护这些连接线程。包括线程的创建,线程的 cache 等
4、SQL Interface: SQL接口
接受用户的SQL命令,并且返回用户需要查询的结果。比如select from就是调用SQL Interface
5.Parser: 解析器
SQL命令传递到解析器的时候会被解析器验证和解析。
主要功能
a . 将SQL语句进行语义和语法的分析,分解成数据结构,然后按照不同的操作类型进行分类,然后做出针对性的转发到后续步骤,以后SQL语句的传递和处理就是基于这个结构的。
b. 如果在分解过程中遇到错误,那么就说明这个sql语句是不合理的。
6、Optimizer: 查询优化器
SQL语句在查询之前会使用查询优化器对查询进行优化。explain语句查看的SQL语句执行计划,就是由查询优化器生成的。
7、Cache和Buffer:查询缓存
他的主要功能是将客户端提交给MySQL的 select请求的返回结果集 cache 到内存中,与该 query 的一个 hash 值 做一个对应。该 Query 所取数据的基表发生任何数据的变化之后, MySQL 会自动使该 query 的Cache 失效。在读写比例非常高的应用系统中, Query Cache 对性能的提高是非常显著的。当然它对内存的消耗也是非常大的。如果查询缓存有命中的查询结果,查询语句就可以直接去查询缓存中取数据。这个缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,key缓存,权限缓存等
8、 Pluggable Storage Engines:存储引擎
与其他数据库例如Oracle 和SQL Server等数据库中只有一种存储引擎不同的是,MySQL有一个被称为“Pluggable Storage Engine Architecture”(可插拔的存储引擎架构)的特性,也就意味着MySQL数据库提供了多种存储引擎。
而且存储引擎是针对表的,用户可以根据不同的需求为数据表选择不同的存储引擎,用户也可以根据自己的需要编写自己的存储引擎。也就是说,同一数据库不同的表可以选择不同的存储引擎。
简单架构流程图:
二、为什么需要事务
- 分析update driver_info set driver_status = 2 where driver_id = 10001;这条语句执行会有什么问题吗?
先理解几个概念:
1、数据库数据存放的文件称为data file
2、日志文件称为log file
数据库数据是有缓存的,如果没有缓存,每次都写或者读物理disk,那性能就太低下了:
3、数据库数据的缓存称为data buffer
4、日志(redo)缓存称为log buffer
既然数据库数据有缓存,就很难保证缓存数据(脏数据)与磁盘数据的一致性。如下图
image.png
1到2的过程中一旦发生服务宕机或断电,缓存数据会丢失,缓存与磁盘数据造成不一致问题。
在并发环境下,如果不做有效控制,会出现什么情况:
image.png
两个客户端在操作同一个块数据的时候,从上图中可以看出如果没有处理好磁盘与缓存数据一致性的问题,那么读到缓存的数据就会变成脏数据!缓存存在的意义是为了提供更好的查询性能,如果连数据一致性都无法保证那么毫无意义!
事务,最终的目的是为了保证数据的一致性
三、事务特性ACID
- 原子性(Atomicity):事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。
- 一致性(Consistency):事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态。一致性状态是指:
1.系统的状态满足数据的完整性约束(主码,参照完整性,check约束等)
2.系统的状态反应数据库本应描述的现实世界的真实状态,比如转账前后两个账户的金额总和应该保持不变。 - 隔离性(Isolation):并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。
- 持久性(Durability):事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。
这里我们需要单独把隔离性拿出来说下:
1、事务的隔离级别分为四种:
image.png
简单叙述
- 脏读:事务1读到事务2未提交的数据。
- 不可重复读:事务1第一次读的数据和第二次读的数据不一致,因为事务2改变数据并提交了。
- 幻读:事务1第一次读的数据行数和第二次读的数据行数不一致,因为事务2新增数据并提交了。
事务的隔离级别是用利用锁来控制的,Mysql默认事务隔离级别是可重复读(RR),但是利用MVCC控制版本来解决幻读问题。
2、锁
MySQL的行级锁,是由存储引擎来实现的,这里我们主要讲解InnoDB的行级锁。
InnoDB的行级锁,按照锁定范围来说,分为三种:
-
记录锁(Record Locks):锁定索引中一条记录。
-
间隙锁(Gap Locks):要么锁住索引记录中间的值,要么锁住第一个索引记录前面的值或者最后一个索引记录后面的值。
-
Next-Key Locks:是索引记录上的记录锁和在索引记录之前的间隙锁的组合。
InnoDB的行级锁,按照功能来说,分为两种:
- 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
- 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁,事务可以通过以下语句显示给记录集加共享锁或排他锁。
手动添加共享锁(S):
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
手动添加排他锁(x):
SELECT * FROM table_name WHERE ... FOR UPDATE
InnoDB也实现了表级锁,也就是意向锁,意向锁是mysql内部使用的,不需要用户干预。
- 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
-
意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
意向锁和行锁可以共存,意向锁的主要作用是为了【全表更新数据】时的性能提升。否则在全表更新数据时,需要先检索该范是否某些记录上面有行锁。
image.png
InnoDB行锁是通过给索引上的索引项加锁来实现的,因此InnoDB这种行锁实现特点意味着:只有通过索引条件检索的数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
3、MVCC和一致性非锁定读
数据库的并发控制机制有很多,最为常见的就是锁机制(事务开始时DQL加读锁结束时释放读锁、同理给DML加写锁)。锁机制一般会给竞争资源加锁,阻塞读或者写操作来解决事务之间的竞争条件,最终保证事务的可串行化。
而MVCC则引入了另外一种并发控制,它让读写操作互不阻塞,每一个写操作都会创建一个新版本的数据,读操作会从有限多个版本的数据中挑选一个最合适的结果直接返回,由此解决了事务的竞争条件。
image.png
上图直观地展现了InnoDB一致性非锁定读的机制。之所以称其为非锁定读,是因为不需要等待行上排他锁的释放。快照数据是指该行的之前版本的数据,每行记录可能有多个版本,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control, MVCC)。InnoDB是通过undo log来实现MVCC。
在事务隔离级别READ COMMITTED和REPEATABLE READ下,InnoDB默认使用一致性非锁定读。然而,对于快照数据的定义却不同。在READ COMMITTED事务隔离级别下,一致性非锁定读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,则读取事务开始时的行数据版本。
MVCC使得数据库读不会对数据加锁,普通的SELECT请求不会加锁,提高了数据库的并发处理能力。借助MVCC,数据库可以实现READ COMMITTED,REPEATABLE READ等隔离级别,用户可以查看当前数据的前一个或者前几个历史版本,保证了ACID中的I特性(隔离性)
四、理解几个问题
- 怎么实现回滚?
简单理解,如果有个迷宫你想要通过它,但是你也不知道能不能通过,当你走到半途中发现自己无法再走下去的时候你要返回去,那你该怎么返回呢,是不是应该在进入的途中走一步就在墙上标记“回去的方向”
(undo log) - 在事务提交入库的过程中尚有脏页未写入磁盘,这时服务发生故障时该怎么办?(redo log)
- 多个事务存在的情况,怎么管理好事务间的隔离关系?
(通过锁和MVCC多版本控制来实现,其中MVCC可以解决RR隔离级别下 幻读的问题)
五、引入事务日志
事务日志是在存储引擎层面的,像bin log是mysql server层面的日志,不能混淆。
1、回滚日志undo log
作用: 保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读
内容: 逻辑格式的日志,在执行undo的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于redo log的。
什么时候产生: 事务开始之前,将当前是的版本生成undo log,undo 也会产生 redo 来保证undo log的可靠性
什么时候释放: 当事务提交之后,undo log并不能立马被删除,而是放入待清理的链表,由purge线程判断是否由其他事务在使用undo段中表的上一个事务之前的版本信息,决定是否可以清理undo log的日志空间
2、重做日志(redo log)
作用: 确保事务的持久性。 防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
内容: 物理格式的日志,记录的是事务开始执行修改后的数据,其redo log是顺序写入redo log file的物理文件中去的。
什么时候产生: 事务开始之后就产生redo log,redo log的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo log文件中。
什么时候释放: 当对应事务的脏页写入到磁盘之后,redo log的使命也就完成了,重做日志占用的空间就可以重用(被覆盖)。
参考资料:https://blog.csdn.net/u010002184/article/details/88526708
网友评论