专栏持续更新中… 本专栏针对的是掌握MySQL基本操作后想要对其有深入了解并且有高性能追求的读者。
第一篇文章主要是对MySQL架构的主要概括,让读者脑海中有个对MySQL大体轮廓,很多地方没有展开细说,更多细节点和高级特性将在本专栏后面的文章继续更新,敬请期待。
客户端: 并非mysql独有,大多数基于网络的工具或服务器都有类似功能,包括连接处理、身份验证、确保安全性等。
解析器与优化器: 大多数MySQL的核心功能都在这一层,包括查询解析、分析、优化、以及所有的内置函数(例如,日期、时间、数学和加密函数),所有跨存储引擎的功能也都在这一层实现:存储过程、触发器、视图等。
存储引擎: 服务器通过存储引擎API进行通信。这些API屏蔽了不同存储引擎之间的差异,使得它们对上面的查询层基本上是透明的。存储引擎层还包含几十个底层函数,用于执行诸如 “开始一个事务” 或者 “根据主键提取一行记录” 等操作。但存储引擎不会去解析SQL(InnoDB例外,他会解析外键值定义),不同存储引擎之间也不会相互通信,而只是简单地响应服务器的请求。
连接管理: 每一个客户端连接拥有一个线程,同一连接的查询只会在这单独的线程执行,线程驻留在一个内核或者CPU上。服务器维护了一个缓存区,用于存放已就绪的线程,因此不需要为每个新的连接创建或者销毁线程。( mysql5.5开始提供了线程池插件,但是不常用。一般解决方案是在访问层实现,比如阿里开源的java线程池druid. )
安全性: 当客户端(应用)连接到MySQL服务器时,服务器需要对其进行身份验证。身份验证基于用户名、发起的主机名和密码。客户端连接成功后,服务器会 继续验证该客户端是否有具体的查询的权限 (例如,是否允许客户端对world数据库中的Country表执行SELECT语句)。
在旧版本中,MySQL可以使用内部查询缓存(query cache)提高查询速度,但是在高并发情况下更容易达到瓶颈,遂在8.0版本中移除
列举几个MySQL取消查询缓存的原因:
处理并发读/写访问的系统通常由两种锁类型组成的锁系统保证安全性,共享锁(shared lock)和排他锁(exclusive lock),也叫读锁(read lock)和写锁(write lock)。
MySQL则提供了多种选择。每种MySQL存储引擎都可以实现自己的锁策略和锁粒度,MySQL8.0手册:锁优化。
下面介绍两个最基础的锁:
事务就是一组SQL语句,作为一个工作单元以原子方式进行处理。事务的一组语句,要么全部执行成功,要么全部执行失败。
事务的ACID:
原子性(atomicity) : 一个事务必须被视为一个不可分割的工作单元,整个事务中的所有操作要么全部成功,要么全部失败。
一致性(consistency): 数据库总是从一个一致性状态转换到下一个一致性状态。就像物理中的 能量守恒定律 :
能量既不会凭空产生,也不会凭空消失,它只会从一种形式转化为另一种形式,或者从一个物体转移到其它物体,而能量的总量保持不变
例:转账分为扣减发起方账户,增加收款方账户。事务能保证金钱总和不会变化。
隔离性(isolation): 一个事务所做的修改在最终提交以前,对其他事务是不可见的.(每种存储引擎实现的隔离级别都不尽相同)
ANSI SQL标准定义了4种隔离级别.
持久性(durability): 一旦提交,事务所做的修改就会被永久保存到数据库中。
ANSI SQL标准定义了4种隔离级别,下面简单地介绍一下4种隔离级别。
READ UNCOMMITTED(读未提交)
读取未提交的数据,也称为脏读(dirty read)。
这个隔离级别会导致很多问题,性能并不会比其他级别好太多,却缺乏其他级别的很多好处,在实际应用中很少使用。
READ COMMITTED(读已提交)
大多数数据库系统的默认隔离级别是READ COMMITTED(但MySQL不是)。一个事务可以看到其他事务在它开始之后提交的修改,但在该事务提交之前,其所做的任何修改对其他事务都是不可见的。这个级别仍然允许不可重复读(nonrepeatable read),这意味着同一事务中两次执行相同语句,可能会看到不同的数据结果。
如上图,该隔离条件下,在事务A未提交时,其中有两次查询可能结果不同,这种现象称之为不可重复读问题。
REPEATABLE READ(可重复读)
是MySQL默认的事务隔离级别。
解决了不可重复读问题,保证了在同一个事务中多次读取相同行数据的结果是一样的(不同数据解决方案不同,如加入行锁) .理论上同时引入了幻读的问题
如上图,单纯加入事务A设计查询表的行锁无法阻止向表内插入数据,导致同一事务内两次查询数量不同。
理论上该隔离级别无法解决幻读问题,但MySQL中 InnoDB和XtraDB存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)解决了幻读的问题。
SERIALIZABLE(可串行化)
SERIALIZABLE是最高的隔离级别。该级别通过强制事务按序执行,使不同事务之间不可能产生冲突,从而解决了前面说的幻读问题。
简单来说,SERIALIZABLE会在读取的每一行数据上都加锁(表锁),所以可能导致大量的超时和锁争用的问题。实际应用中很少用到这个隔离级别。
死锁是指两个或多个事务相互持有并请求相同资源上的锁,产生了循环依赖。当多个事务试图以不同的顺序加锁时会导致死锁。当多个事务锁定相同的资源时,也可能会发生死锁。
例:
-- 事务1
START TRANSACTION;
UPDATE StockPrice SET close = 45.50 WHERE stock_id = 4 and date = '2020-05-01';
UPDATEStockPriceSETclose=19.80WHEREstock_id=3anddate=‘2020-05-02’;
COMMIT;
-- 事务2
START TRANSACTION;
UPDATE StockPrice SET high = 20.12 WHERE stock id = 3 and date = '2020-05-02°
UPDATE StockPrice SET high = 47.20 WHERE stock_id = 4 and date = "2020-05-01';
COMMIT;
每个事务都开始执行第一个查询,在处理过程中会更新一行数据,同时在主键索引和其他唯一索引中将该行锁定。然后,每个事务将在第二个查询中尝试更新第二行数据,却发现该行已经被锁定。这两个事务将永远等待对方完成,除非有其他因素介入解除死锁。
为了解决这个问题,数据库系统实现了各种死锁检测和锁超时机制。在MySQL的InnoDB存储引擎中,检测到循环依赖后会立即返回一个错误信息。InnoDB目前处理死锁的方式是将持有最少行级排他锁的事务回滚。
锁的行为和顺序是和存储引擎相关的。同样的一系列查询语句,有些存储引擎会产生死锁,有些则不会。死锁的产生有双重原因:有些是因为真正的数据冲突,这种情况通常很难避免,但有些则完全是由于存储引擎的实现方式导致的。
对于事务型的系统,一旦发生死锁,会选择回滚其中一个事务(部分或全部)。
这里描述的事务原语将基于InnoDB引擎中的事务。
默认情况下,单个INSERT、UPDATE或DELETE语句会被隐式包装在一个事务中并在执行成功后立即提交,这称为自动提交(AUTOCOMMIT)模式。也可以禁用此模式,手动COMMIT提交事务或ROLLBACK回滚事务。
还有一些命令,当在活动的事务中发出时,会导致MySQL在事务的所有语句执行完毕前提交当前事务。这些通常是进行重大更改的DDL命令,如ALTER TABLE,LOCK TABLES。
在事务中混合使用存储引擎
假设在事务中混合使用事务表和非事务表(例如,InnoDB和MyISAM表),如果一切顺利,事务将正常工作。如果需要回滚,则无法撤销对非事务表的更改。这会使数据库处于不一致的状态,可能难以恢复,并使整个事务问题变得毫无意义。
隐式锁定和显式锁定
隐式锁: InnoDB使用两阶段锁定协议(two-phase locking protocol)。在事务执行期间,随时都可以获取锁,但锁只有在提交或回滚后才会释放,并且所有的锁会同时释放。前面描述的锁定机制都是隐式的。InnoDB会根据隔离级别自动处理锁。
显式锁: 另外,InnoDB还支持通过特定的语句进行显式锁定,这些语句不属于SQL规范,尽量避免使用:
--InnoDB引擎级别
SELECT ... FOR SHARE
SELECT ... FOR UPDATE
--服务器级别
LOCK TABLES
UNLOCK TABLES
MySQL的大多数事务型存储引擎使用的都不是简单的行级锁机制。它们会将行级锁和可以提高并发性能的多版本并发控制(MVCC)技术结合使用。不仅是MySQL,包括Oracle、PostgreSQL以及其他一些数据库系统也都使用了MVCC,但各自的实现机制不尽相同,因为MVCC如何工作没有统一的标准。
可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。根据其实现方式,不仅实现了非阻塞的读操作,写操作也只锁定必要的行。每个存储引擎实现MVCC的方式都不同。其中一些变体包括乐观并发控制和悲观并发控制
MVCC仅适用于 REPEATABLE READ 和 READ COMMITTED 隔离级别。READ UNCOMMITTED与MVCC不兼容,是因为查询不会读取适合其事务版本的行版本,而是不管怎样都读最新版本。SERIALIZABLE与MVCC也不兼容,是因为读取会锁定它们返回的每一行
MySQL提供了一种原生方式来将一个节点执行的写操作分发到其他节点,这被称为复制。为了实现高速的分发,源节点为每个副本节点都提供一个线程,该线程作为复制客户端登录,当写入发生时会被唤醒,发送新数据。
容灾恢复: 尽量将副节点放在不同的地区,多年来,MySQL中的复制变得十分复杂。全局事务标识符、多源复制、副本上的并行复制和半同步。这些将在后面的文章详细介绍。
在8.0版本中,MySQL将表的元数据重新设计为一种数据字典,包含在表的.ibd文件中。这使得表结构上的信息支持事务和原子级数据定义更改。
在操作期间,我们不再仅仅依赖information_schema来检索表定义和元数据,而是引入了字典对象缓存,这是一种基于最近最少使用(LRU)的内存缓存,包括分区定义、表定义、存储程序定义、字符集和排序信息。服务器访问表的元数据的方式的这一重大变化减少了I/O,非常高效。
特别是当前访问最活跃的那些表,在缓存中最常出现。每个表的.ibd和.frm文件被替换为已经被序列化的字典信息(.sdi)。
InnoDB是MySQL的默认事务型存储引擎,也是最重要、使用最广泛的引擎。它是为处理大量短期事务而设计的,这些事务通常是正常提交的,很少会被回滚。InnoDB的性能和自动崩溃恢复特性,使得它在非事务型存储需求中也很流行。
InnoDB是MySQL默认的通用存储引擎。默认情况下,InnoDB将数据存储在一系列的数据文件中,这些文件统被称为表空间(tablespace)。表空间本质上是一个由InnoDB自己管理的黑盒。
InnoDB内部做了很多优化。其中包括从磁盘预取数据的可预测性预读、能够自动在内存中构建哈希索引以进行快速查找的自适应哈希索引(adaptive hash index),以及用于加速插入操作的插入缓冲区。
InnoDB内部做了很多优化。其中包括从磁盘预取数据的可预测性预读、能够自动在内存中构建哈希索引以进行快速查找的自适应哈希索引(adaptive hash index),以及用于加速插入操作的插入缓冲区
从MySQL 5.6开始,InnoDB引入了在线DDL,5.7和8.0版本中进行了扩充。允许在不使用完整表锁和外部工具的情况下进行特定的表更改操作,这大大提高了MySQL InnoDB表的可操作性。
JSON文档支持JSON类型在5.7版本被首次引入InnoDB,它实现了JSON文档的自动验证,并优化了存储以允许快速读取。MySQL 8.0.7的进一步改进增加了在JSON数组上定义多值索引的能力并且添加了实用函数。
MySQL 8.0的另一个主要变化是删除了基于文件的表元数据存储,并将其转移到使用InnoDB表存储的数据字典中。这给所有类似修改表结构这样的操作带来了InnoDB的崩溃恢复事务的好处。
最后,MySQL 8.0引入了原子数据定义更改,通过创建DDL特定的Undo日志和Redo日志来实现的,InnoDB便依赖这两种日志来跟踪变更——这是InnoDB经过验证的设计,已经扩展到MySQL服务器的操作中。
在过去的几个主要版本中,MySQL主要的改进核心在于InnoDB的演进。表元数据、用户认证、身份鉴权这些内部统计信息的管理也已经调整为使用InnoDB表来实现。
InnoDB是MySQL的默认存储引擎,它几乎能覆盖每一种使用场景。后面的章节我们将重点介绍InnoDB存储引擎,包括它的特性、性能及限制。