《高性能MySQL》——架构与历史(笔记)

文章目录

  • 一、MySQL架构与历史
      • 1.1.1 连接管理与安全性
      • 1.1.2 优化与执行
    • 1.2 并发控制
      • 1.2.1 读写锁
      • 1.2.2 锁粒度(锁模式)
        • 表锁(table lock)
        • 行级锁(row lock)
    • 1.3 事务
      • 1.3.1 隔离级别
        • READ UNCOMITTED (读未提交)
        • READ COMMITTED (读提交)
        • REPEATABLE READ (可重复读)
        • SERIALIZABLE (可串行化)
      • 1.3.2 死锁
      • 1.3.3 事务日志
      • 1.3.4 MySQL中的事务
        • 自动提交(AUTOCOMMIT)
        • 在事务中混合使用存储引擎
        • 隐式和显式锁定
    • 1.4 多版本并发控制
    • 1.5 MySQL的存储引擎
      • 1.5.1 InnoDB存储引擎
        • InnoDB概览
      • 1.5.2 MyISAM 存储引擎
        • 存储
        • MyISAM特性
        • MyISAM压缩表
        • MyISAM性能
      • 1.5.3 转换表的引擎
  • 附录

一、MySQL架构与历史

客户端
连接/线程处理
查询缓存
解析器
优化器
  1. 连接/线程处理:大多数基于网络的客户端/服务器的工具或者服务都有类似的架构。比如连接处理、授权认证、安全等等。

  2. 解析器、查询缓存、优化器:大多数MySQL的核心服务功能都在这一层,包括查询解析、分析、优化、缓存以及所有的内置函数(例如,日期、时间、数学和加密函数),所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。

  3. 存储引擎:负责MySQL中数据的存储和提取。和GNU/Linux下的各种文件系统一样,每个存储引擎都有它的优势和劣势。服务器通过API与存储引擎进行通信。这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询过程透明。
    存储引擎API包含几十个底层函数,用于执行诸如“开始一个事务”或者“根据主键提取一行记录”等操作。但存储引擎不会去解析SQL,不同存储引擎之间也不会相互通信,而只是简单地响应上层服务器的请求。

例外:innodb 存储引擎会去解析SQL,解析外键定义。

1.1.1 连接管理与安全性

每个客户端连接都会在服务器进程中拥有一个线程,这个连接的查询只会在这个单独的线程中执行,该线程只能轮流在某个CPU核心或者CPU中运行。服务器会负责缓存线程,因此不需要为每一个新建的连接创建或者销毁线程生。(使用者通过数据库给的API管理连接池)

当客户端(应用)连接到MySQL服务器时,服务器需要对其进行认证。认证基于用户名、原始主机信息和密码。一旦客户端连接成功,服务器会继续验证该客户端是否具有执行某个特定查询的权限。

1.1.2 优化与执行

MySQL会解析查询,并创建内部数据结构(解析树),然后对其进行各种优化,包括重写查询、决定表的读取顺序,以及选择合适的索引等。

  • 用户可以通过特殊的关键字 提示(hint) 优化器,影响它的决策过程。
  • 也可以请求优化器 解释(explain) 优化过程的各个因素,使用户可以知道服务器是如何进行优化决策的,并提供一个参考基准,便于用户重构查询和schema、修改相关配置,使应用尽可能高效运行。

优化器并不关心表使用的是什么存储引擎,但存储引擎对于优化查询是有影响的。

例如,某些存储引擎的某种索引,可能对一些特定的查询有优化。

优化器会请求存储引擎提供容量或某个具体操作的开销信息,以及表数据的统计信息等。

对于SELECT语句,在解析查询之前,服务器会先检查查询缓存(Query Cache),如果能够在其中找到对应的查询,服务器就不必再执行查询解析、优化和执行的整个过程,而是直接返回查询缓存中的结果集。

1.2 并发控制

讨论MySQL在两个层面的并发控制:

  • 服务器层
  • 存储引擎层

以Unix系统的email box为例,典型的mbox文件格式是非常简单的。一个mbox邮箱中的所有邮件都串行在一起,彼此首尾相连。这种格式对于读取和分析邮件信息非常友好,同时投递邮件也很容易,只要在文件末尾附加新的邮件内容即可。

但如果两个进程在同一时刻对同一个邮箱投递邮件,邮箱的數据将会被破坏,两封邮件的内容会交叉地附加在邮箱文件的末尾。

设计良好的邮箱投递系统会通过 锁(lock) 来防止数据损坏。如果客户试图投递邮件,而邮箱已经被其他客户锁住,那就必须等待,直到锁释放才能进行投递。

但是它在任意一个时刻,只有一个进程可以修改邮箱的数据,这在大容量的邮箱系统中是个问题。

1.2.1 读写锁

从邮箱中读取数据没有这样的麻烦,即使同一时刻多个用户并发读取也不会有什么问题。因为读取不会修改数据,所以不会出错。

但如果某个客户正在读取邮箱,同时另外一个用户试图删除编号为25的邮件,其操作就结果是不确定的,读的客户可能会报错退出,也可能读取到不一致的邮箱数据。所以,为安全起见,即使是读取邮箱也需要特别注意。

如果把上述的邮箱当成数据库中的一张表,把邮件当成表中的一行记录,就很容易看出,同样的问题依然存在。从很多方面来说,邮箱就是一张简单的数据库表。修改数据库表中的记录,和删除或者修改邮箱中的邮件信息,十分类似。

解决这类经典问题的方法就是并发控制,其实非常简单。在处理并发读或者写时,可以通过实现一个由两种类型的锁组成的锁系统来解决问题。

这两种类型的锁通常被称为

  • 共享锁(shared lock)排他锁(exclusive lock)

也叫

  • 读锁(read lock)写锁(write lock)

锁的概念如下:

  • 读锁是共享的,或者说是相互不阻塞的。
  • 写锁则是排他的,也就是说一个写锁会阻塞其他的写锁和读锁。

在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改某一部分数据时,MySQL会通过锁定防止其他用户读取同一数据。大多数时候,MySQL锁的内部管理都是透明的。

1.2.2 锁粒度(锁模式)

一种提高共享资源并发性的方式就是让锁定对象更有选择性。理想的方式是,只对会修改的数据片进行精确的锁定。任何时候,在给定的资源上,锁定的数据量越少,则系统的并发程度越高,只要相互之间不发生冲突即可。

问题是加锁也需要消耗资源。锁的各种操作,包括获得锁、检查锁是否已经解除、释放锁等,都会增加系统的开销。如果系统花费大量的时间来管理锁,而不是存取数据,那么系统的性能可能会因此受到影响。

所谓的锁策略,就是在锁的开销和数据的安全性之间寻求平衡,这种平衡当然也会影响到性能。


大多数商业数据库系统没有提供更多的选择,一般都是在表上施加 行级锁(row-levellock) ,并以各种复杂的方式来实现,以便在锁比较多的情况下尽可能地提供更好的性能。

而MySQL则提供了多种选择。每种MySQL存储引擎都可以实现自己的锁策略和锁粒度。

在存储引擎的设计中,锁管理是个非常重要的决定。将锁粒度固定在某个级别,可以为某些特定的应用场景提供更好的性能,但同时却会失去对另外一些应用场景的良好支持。

表锁(table lock)

表锁是MySQL中最基本的锁策略,并且是开销最小的策略。

表锁非常类似于前文描述的邮箱加锁机制:它会锁定整张表。

一个用户在对表进行写操作(插入、删除、更新等)前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获得读锁,读锁之间是不相互阻塞的。

写锁也比读锁有更高的优先级,因此一个写锁请求可能会被插人到读锁队列的前面(写锁可以插入到锁队列中读锁的前面,反之读锁则不能插入到写锁的前面)。

尽管存储引擎可以管理自己的锁,MySQL本身还是会使用各种有效的表锁来实现不同的目的。例如,服务器会为诸如ALTER TABLE 之类的语句使用表锁,而忽略存储引擎的锁机制。

行级锁(row lock)

行级锁可以最大程度地支持并发处理(同时也带来了最大的锁开销)。在InnoDB和XtraDB,以及其他一些存储引擎中实现了行级锁。

行级锁只在存储引擎层实现,而MySQL服务器层没有实现。服务器层完全不了解存储引擎中的锁实现。

注意,MyISAM引擎不支持行级锁和事务

行级锁需要数据库和引擎自己去实现,也就是说不是所有数据库都有

mysql的InnoDB引擎的一些特殊排他的行级锁:记录锁、间隙锁。

  1. 记录锁:存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
  2. 间隙锁(next-key locking):存在于索引中,且需要是范围查询。用于锁住查询目标以及目标间索引记录的间隔,避免范围内其他事务插入,本事务却不知道的情况(幻读)。存在于可重复读级别。

1.3 事务

事务就是一组原子性的SQL查询,或者说一个独立的工作单元。

如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。

  • 事务内的语句,要么全部执行成功,要么全部执行失败。

银行应用是解释事务必要性的一个经典例子。

假设一个银行的数据库有两张表:

  • 支票(checking) 表和 储蓄(savings) 表。现在要从用户Jane的支票账户转移200美元到她的储蓄账户,那么需要至少三个步骤:
  1. 检查支票账户的余额高于200美元。
  2. 从支票账户余额中减去200美元。
  3. 在储蓄账户余额中增加200美元。
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;

事务有4条特性(ACID):

  • 原子性(atomicity)

一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。

  • 一致性(consistency)

数据库总是从一个一致性的状态转换到另外一个一致性的状态。

在前面的例子中,一致性确保了,即使在执行第三、四条语句之间时系统崩溃,支票账户中也不会损失200美元,因为事务最终没有提交,所以事务中所做的修改也不会保存到数据库中。

  • 隔离性(isolation)

通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。

在前面的例子中,当执行完第三条语句、第四条语句还未开始时,此时有另外一个账户汇总程序开始运行,则其看到的支票账户的余额并没有被减去200美元。

隔离级别(Isolationlevel) 的部分详细讲解。

  • 持久性(durability)

一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。

持久性是个有点模糊的概念,因为实际上持久性也分很多不同的级别

有些持久性策略能够提供非常强的安全保障,而有些则未必。而且不可能有能做到100%的持久性保证的策略。

后面章节讲述MySQL中持久性的真正含义。


事务的ACID特性可以确保银行不会弄丟你的钱。而在应用逻辑中,要实现这一点非常难,甚至可以说是不可能完成的任务。

一个兼容ACID的数据库系统,需要做很多复杂但可能用户并没有觉察到的工作,才能确保ACID的实现。

就像锁粒度的升级会增加系统开销一样,这种事务处理过程中额外的安全性,也会需要数据库系统做更多的额外工作。一个实现了ACID的数据库,相比没有实现ACID的数据库,通常会需要更强的CPU处理能力、更大的内存和更多的磁盘空间。

正如本章不断重复的,这也正是MySQL的存储引擎架构可以发挥优势的地方。

  • 用户可以根据业务是否需要事务处理,来选择合适的存储引擎。

  • 对于一些不需要事务的查询类应用,选择一个非事务型的存储引擎,可以获得更高的性能。

  • 存储引擎不支持事务,也可以通过LOCK TABLES语句为应用提供一定程 度的保护,这些选择用户都可以自主决定。

1.3.1 隔离级别

隔离性其实比想象的要复杂。

在SQL标准中定义了四种隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。

较低级别的隔离通常可以执行更高的并发,系统的开销也更低。

不同数据库实现的隔离级别不同,因此使用时需要仔细阅读手册

READ UNCOMITTED (读未提交)

在READ UNCOMMITTED 级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为 脏读(Dirty Read)

这个级别会导致很多问题,从性能上来说,READ UNCOMMITTED 不会比其他的级别好太多,但却缺乏其他级别的很多好处,除非真的有非常必要的理由,在实际应用中一般很少使用。

READ COMMITTED (读提交)

大多数数据库系统的默认隔离级别都是READ COMMITTED (但MySQL不是)。

READ COMMITTED 满足前面提到的隔离性的简单定义:

  • 一个事务开始时,只能“看见”已经提交的事务所做的修改。

换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。

这个级别有时候也叫做 不可重复读(nonrepeatable read),因为两次执行同样的查询,可能会得到不一样的结果。

REPEATABLE READ (可重复读)

REPEATABLE READ 解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但是理论上,可重复读隔离级别还是无法解决另外一个 幻读(Phantom Read) 的问题。

所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Row)。

InnoDB和XtraDB存储引擎通过 多版本并发控制(MVCC, Multiversion Concurrency Control) 解决了幻读的问题。本章稍后会做进一步的讨论。

可重复读是MySQL的默认事务隔离级别

SERIALIZABLE (可串行化)

SERIALIZABLE是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题。

简单来说,SERIALIZABLE 会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。

这里每一个select会自动加上共享锁,至于是行锁还是表锁要看数据库或数据库引擎自己的实现

实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。

《高性能MySQL》——架构与历史(笔记)_第1张图片

1.3.2 死锁

死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。

多个事务同时锁定同一个资源时,也会产生死锁。例如,设想下面两个事务同时处理StockPrice表:

START TRANSACTION;
UPDATE StockPrice SET close = 45.50 WHERE stock _id = 4 and date ='2002-05-01' ; 
UPDATE StockPrice SET close = 19.80 WHERE stock_ id = 3 and date =' 2002-05-02' ;
COMMIT;

START T.RANSACTION;
UPDATE StockPrice SET high = 20.12 WHERE stock_ id = 3 and date =' 2002-05-02' ; 
UPDATE StockPrice SET high = 47.20 WHERE stock_ id = 4 and date ='2002-05-01' ; 
COMMIT;

如果凑巧,两个事务都执行了第一条UPDATE语句,更新了一行数据,同时也锁定了该行数据,接着每个事务都尝试去执行第二条UPDATE语句,却发现该行已经被对方锁定,然后两个事务都等待对方释放锁,同时又持有对方需要的锁,则陷人死循环。

除非有外部因素介入才可能解除死锁。为了解决这种问题,数据库系统实现了各种死锁检测和死锁超时机制。

越复杂的系统,比如InnoDB存储引擎,越能检测到死锁的循环依赖,并立即返回一个错误。

这种解决方式很有效,否则死锁会导致出现非常慢的查询。还有一种解决方式,就是当查询的时间达到锁等待超时的设定后放弃锁请求,这种方式通常来说不太好。

InnoDB 目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚

锁的行为和顺序是和存储引擎相关的。以同样的顺序执行语句,有些存储引擎会产生死锁,有些则不会。

死锁的产生有双重原因:

  • 有些是因为真正的数据冲突,这种情况通常很难避免
  • 有些则完全是由于存储引擎的实现方式导致的。

死锁发生以后,只有部分或者完全回滚其中一个事务,才能打破死锁。

对于事务型的系统,这是无法避免的,所以应用程序在设计时必须考虑如何处理死锁。大多数情况下只需要重新执行因死锁回滚的事务即可。

1.3.3 事务日志

事务日志可以帮助提高事务的效率。

使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。

事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。

事务日志持久以后,内存中被修改的数据在后台可以慢慢地刷回到磁盘。目前大多数存储引擎都是这样实现的,我们通常称之为 预写式日志(Write-Ahead Logging) ,修改数据需要写两次磁盘。


如果数据的修改已经记录到事务日志并持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。具体的恢复方式则视存储引擎而定。

1.3.4 MySQL中的事务

MySQL提供了两种事务型的存储引擎:

  • InnoDB

  • NDB Cluster

另外还有一些第三方存储引擎也支持事务,比较知名的包括XtraDB和PBXT。

自动提交(AUTOCOMMIT)

MySQL默认采用 自动提交(AUTOCOMMIT) 模式。

如果不是显式地开始一个事务,则每个查询都被当作一个事务执行提交操作。

在当前连接中,可以通过设置AUTOCOMMIT变量来启用或者禁用自动提交模式:

《高性能MySQL》——架构与历史(笔记)_第2张图片

  • 1或者ON表示启用
  • 0或者0FF表示禁用

当AUTOCOMMIT=0时,所有的查询都是在一个事务中,直到显式地执行COMMIT提交或者ROLLBACK回滚,该事务结束,同时又开始了另一个新事务。

修改AUTOCOMMIT对非事务型的表,比如MyISAM或者内存表,不会有
任何影响。对这类表来说,没有COMMIT或者ROLLBACK的概念,也可以说是相当于一直处于AUTOCOMMIT启用的模式。

另外还有一些命令,在执行之前会 强制执行COMMIT提交 当前的活动事务。典型的例子,在 数据定义语言(DDL) 中,如果是会导致大量数据改变的操作,比如ALTER TABLE,就是如此。

DML(Data Manipulation Language) 数据操作语言
数据库的基本操作,SQL中处理数据等操作统称为数据操纵语言,实现了基本的“增删改查”操作。
包括的关键字有:select、update、delete、insert、merge
可以手动控制事务的开启、提交和回滚
DDL(Data Definition Language) 数据定义语言
用于定义和管理 SQL 数据库中的所有对象的语言,对数据库中的某些对象(例如,database,table)进行管理。包括的关键字有:create、alter、drop、truncate、comment、grant、revoke。
是隐性提交的,不能回滚

另外还有LOCK TABLES 等其他语句也会导致同样的结果。如果有需要,请检查对应版本的官方文档来确认所有可能导致自动提交的语句列表。

msyql官方文档

MySQL可以通过执行SET TRANSACTION ISOLATION LEVEL 命令来设置隔离级别。

新的隔离级别会在下一个事务开始的时候生效。可以在配置文件中设置整个数据库的隔离级别,也可以只改变当前会话的隔离级别:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

MySQL能够识别所有的4个ANSI隔离级别,InnoDB引擎也支持所有的隔离级别。

在事务中混合使用存储引擎

MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。

所以在同一个事务中,使用多种存储引擎是不可靠的。

如果在事务中混合使用了事务型和非事务型的表(例如InnoDB和MyISAM表),在正常提交的情况下不会有什么问题。

但如果该事务需要回滚,非事务型的表上的变更就无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。所以,为每张表选择合适的存储引擎非常重要。

在非事务型的表上执行事务相关操作的时候,MySQL通常不会发出提醒,也不会报错。

有时候只有回滚的时候才会发出一个警告:“某些非事务型的表上的变更不能被回滚”。但大多数情况下,对非事务型表的操作都不会有提示。

隐式和显式锁定

InnoDB采用的是 两阶段锁定协议(two-phase locking protocol)

在事务执行过程中,随时都可以执行锁定,锁只有在执行COMMIT或者ROLLBACK的时候才会释放,并且所有的锁是在同一时刻被释放

前面描述的锁定都是隐式锁定,InnoDB会根据隔离级别在需要的时候自动加锁

另外,InnoDB也支持通过特定的语句进行显式锁定,这些语句不属于SQL规范

SELECT ... LOCK IN SHARE MODE # 共享锁,锁行
SELECT ... FOR UPDATE # 排他锁,锁行

MySQL也支持LOCK TABLES 和UNLOCK TABLES 语句,这是在服务器层实现的,和存储引擎无关。它们有自己的用途,但并不能替代事务处理。如果应用需要用到事务,还是应该选择事务型存储引擎。

经常可以发现,应用已经将表从MyISAM转换到InnoDB,但还是显式地使用LOCK TABLES语句。这不但没有必要,还会严重影响性能,实际上InnoDB的行级锁工作得更好。


LOCK TABLES 和 事务之间相互影响的话,情况会变得非常复杂,在某些MySQL版本中甚至会产生无法预料的结果。

因此,除了事务中禁用了AUTOCOMMIT,可以使用LOCK TABLES 之外,其他任何时候都不要显式地执行LOCK TABLES, 不管使用的是什么存储引擎。

1.4 多版本并发控制

MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了 多版本并发控制(MVCC)

需要注意的是,MVCC没有一个统一的实现标准,不同数据库可能不一样。

可以认为MVCC是行级锁的一个变种,但是它在很多情况下 避免了加锁操作 ,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。

根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。

前面说到不同存储引擎的MVCC实现是不同的,典型的

  • 有乐观(optimistic) 并发控制
  • 悲观(pessimistic) 并发控制。

下面我们通过InnoDB的简化版行为来说明MVCC是如何工作的。

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。

  • 一个保存了行的创建时间
  • 一个保存行的过期时间(或删除时间)

当然存储的并不是实际的时间值,而是 系统版本号(system version number) 。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

下面看一下在REPEATABLE READ 隔离级别下,MVCC具体是如何操作的。

  • SELECT

InnoDB会根据以下两个条件检查每行记录:

  1. InnoDB只查找版本早于当前事务版本的数据行(行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开
    始前已经存在的,要么是事务自身插入或者修改过的。

  2. 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除

只有符合上述两个条件的记录,才能返回作为查询结果。

  • INSERT

InnoDB为新插人的每一行保存当前系统版本号作为行版本号。

  • DELETE

InnoDB为删除的每一行保存当前系统版本号作为行删除标识。

  • UPDATE

InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。


保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。

不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。

MVCC 只在 读提交和可重复读 两个隔离级别下工作。

其他两个隔离级别都和MVCC不兼容

  • 读未提交总是读取最新的数据行,而不是符合当前事务版本的数据行。

  • SERIALIZABLE则会对所有读取的行都加锁。

1.5 MySQL的存储引擎

在文件系统中,MySQL将每个数据库(也可以称之为schema)保存为数据目录下的一个子目录。

创建表时,MySQL会在数据库子目录下创建一个和表同名的.frm 文件保存表的定义。例如创建一个名为MyTable的表,MySQL会在MyTablefrm文件中保存该表的定义。

因为MySQL使用文件系统的目录和文件来保存数据库和表的定义,大小写敏感性和具体的平台密切相关。

  • 在Windows中,大小写是不敏感的
  • 在类Unix中,大小写是敏感的

不同的存储引擎保存数据和索引的方式是不同的,但表的定义则是在MySQL服务层统一处理的。可以使用SHOW TABLE STATUS命令 (在MySQL 5.0以后的版本中,也可以查询INFORMATION_ SCHEMA 中对应的表)显示表的相关信息。

例如,对于mysql数据库中的user表:

《高性能MySQL》——架构与历史(笔记)_第3张图片

  • Name:表名
  • Engine:表的存储引擎类型。在旧版本中,该列的名字叫Type,而不是Engine
  • Row_ format:行的格式。对于MyISAM表,可选的值为Dynamic、Fixed或者Compressed
    • Dynamic的行长度是可变的,一般包含可变长度的字段,如VARCHAR或BLOB
    • Fixed的行长度则是固定的,只包含固定长度的列,如CHAR和INTEGER
    • Compressed 的行则只在 压缩表 中存在
  • Rows:表中的行数。对于MyISAM和其他一些存储引擎,该值是精确的,但对于InnoDB该值是估计值
  • Avg_ row_ length:平均每行包含的字节数
  • Data_ Length:表数据的大小(以字节为单位)
  • Max_ data_ length:表数据的最大容量,该值和存储引擎有关
  • Index_ length:索引的大小(以字节为单位)
  • Data_ free:对于MyISAM表,表示已分配但目前没有使用的空间。这部分空间包括了之前删除的行,以及后续可以被INSERT利用到的空间
  • Auto_ increment:下一个AUTO_ INCREMENT的值
  • Create_ time:表的创建时间
  • Update_ time:表数据的最后修改时间
  • Check_ time:使用CKECKTABLE命令或者myisamchk工具最后一次检查表的时间
  • Collation:表的默认字符集和字符列排序规则
  • Checksum:如果启用,保存的是整个表的实时校验和
  • Create_ options:创建表时指定的其他选项
  • Comment:该列包含了一些其他的额外信息。
    • 对于MyISAM表,保存的是表在创建时带的注释
    • 对于InnoDB表,则保存的是InnoDB表空间的剩余空间信息
    • 如果是一个视图,则该列包含“VIEW” 的文本字样。.

1.5.1 InnoDB存储引擎

InnoDB是MySQL的默认事务型引擎,也是最重要、使用最广泛的存储引擎。

设计用来处理大量的 短期(short-lived) 事务,短期事务大部分情况是正常提交的,很少会被回滚。

InnoDB的性能和自动崩溃恢复特性,使得它在非事务型存储的需求中也很流
行。除非有非常特别的原因需要使用其他的存储引擎,否则应该 优先考虑 InnoDB引擎。

InnoDB概览

InnoDB的数据存储表空间(tablespace) 中,表空间是由InnoDB管理的一个黑盒子,由一系列的数据文件组成。

在MySQL 4.1 以后的版本中,InnoDB 可以将每个表的 数据索引 存放在 单独 的文件中。

InnoDB 也可以使用裸设备作为表空间的存储介质,但现代的文件系统使得裸设备不再是必要的选择。

InnoDB采用MVCC来支持高并发,并且实现了四个标准的隔离级别。其 默认级别是REPEATABLE READ (可重复读) ,并且通过 间隙锁(next-key locking) 策略防止幻读的出现。间隙锁使得InnoDB不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。

InnoDB表是基于聚簇索引建立的

聚簇索引一般都是主键,聚簇索引和数据放在一起,非聚簇索引则是和数据分开的。
非聚簇索引会在多叉树(B树,MySQL的B树做了一定修改,因此也有人称之为B+树)中记录索引值以及主键。当只查询索引和主键时,我们可以直接获取,称之为索引覆盖
因此在查询这两个值之外的数据的时候,会回表,也就是再通过聚簇索引查出其他数据。

InnoDB 的索引结构和MySQL的其他存储引擎有很大的不同,聚簇索引对主键查询有很高的性能。不过它的二级索引(secondary index,非主键索引)中必须包含主键列,所以如果主键列很大的话,其他的所有索引都会很大。因此,若表上的索引较多的话,主键应当尽可能的小。

InnoDB 的存储格式是平台独立的,也就是说可以将数据和索引文件从Intel 平台复制到PowerPC或者Sun SPARC平台。

InnoDB内部做了很多优化,包括从磁盘读取数据时采用的可预测性预读,能够自动在内存中创建hash索引以加速读操作的 自适应哈希索引(adaptive hash index) ,以及能够加速插入操作的 插入缓冲区(insert buffer) 等。

InnoDB的行为是非常复杂的,不容易理解。如果使用了InnoDB引擎,建议阅读官方手册中的“InnoDB 事务模型和锁”一节。

15.2.10. InnoDB事务模型和锁定

如果应用程序基于InnoDB构建,则事先了解一下InnoDB的MVCC架构带来的一些微妙和细节之处是非常有必要的。存储引擎要为所有用户甚至包括修改数据的用户维持一致性的视图, 是非常复杂的工作。

作为事务型的存储引擎,InnoDB 通过一些机制和工具支持真正的热备份,Oracle 提供的MySQL Enterprise Backup、Percona 提供的开源的XtraBackup都可以做到这一点。

MySQL的其他存储引擎不支持热备份,要获取一致性视图需要停止对所有表的写人,而在读写混合场景中,停止写人可能也意味着停止读取。

1.5.2 MyISAM 存储引擎

在MySQL 5.1及之前的版本,MyISAM是默认的存储引擎。

MyISAM 提供了大量的特性,包括全文索引、压缩、空间函数(GIS) 等,但MyISAM 不支持事务和行级锁 ,而且有一个毫无疑问的缺陷就是崩溃后无法安全恢复。

对于只读的数据,或者表比较小、可以忍受 修复(repair) 操作,则依然可以继续使用MyISAM 。

存储

MyISAM会将表存储在两个文件中:

  • 数据文件和索引文件,分别以.MYD和.MYI为扩展名。

索引和数据分开存储
可参考-InnoDB 和 MyISAM的索引区别

MyISAM表可以包含动态或者静态(长度固定)行。

MySQL会根据表的定义来决定采用何种行格式。MyISAM表可以存储的行记录数,一般受限于可用的磁盘空间,或者操作系统中单个文件的最大尺寸。

在MySQL 5.0中,MyISAM表如果是变长行,则默认配置只能处理256TB的数据,因为指向数据记录的指针长度是6个字节。而在更早的版本中,指针长度默认是4字节,所以只能处理4GB的数据。而所有的MySQL版本都支持8字节的指针。

要改变MyISAM表指针的长度(调高或者调低),可以通过修改表的MAX_ ROWS 和AVG_ ROW_LENGTH选项的值来实现,两者相乘就是表可能达到的最大大小。修改这两个参数会导致重建整个表和表的所有索引,这可能需要很长的时间才能完成。

MyISAM特性

作为MySQL最早的存储引擎之一,MyISAM有一些已经开发出来很多年的特性,可以满足用户的实际需求。

  • 加锁与并发

MyISAM对整张表加锁,而不是针对行。

读取时会对需要读到的所有表加共享锁,写入时则对表加排他锁。

但是在表有读取查询的同时,也可以往表中插入新的记录(这被称为并发插入,CONCURRENT INSERT)。.

  • 修复

对于MyISAM表,MySQL可以手工或者自动执行检查和修复操作,但这里说的修复和事务恢复以及崩溃恢复是不同的概念。

执行表的修复可能导致一些数据丢失,而且修复操作是非常慢的。

可以通过CHECK TABLE mytable 检查表的错误,如果有错误可以通过执行REPAIR TABLE mytable 进行修复。

另外,如果MySQL服务器已经关闭,也可以通过myisamchk命令行工具进行检查和修复操作。

  • 索引特性
    对于MyISAM表,即使是BL0B和TEXT等长字段,也可以基于其前500个字符创建索引。

MyISAM也支持全文索引,这是一种基于分词创建的索引,可以支持复杂的查询。

  • 延迟更新索引键(Delayed Key Write)

创建MyISAM表的时候,如果指定了DELAY_ KEY_ WRITE选项,在每次修改执行完成时,不会立刻将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区(in-memorykeybuffer),只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写人到磁盘。

这种方式可以极大地提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。

延迟更新索引键的特性,可以在全局设置,也可以为单个表设置。

MyISAM压缩表

如果表在创建并导人数据以后,不会再进行修改操作,那么这样的表或许适合采用MyISAM压缩表。

可以使用myisampack对MyISAM表进行压缩(也叫打包pack)。

压缩表是不能进行修改的(除非先将表解除压缩,修改数据,然后再次压缩)。

压缩表可以极大地减少磁盘空间占用,因此也可以减少磁盘I/O,从而提升查询性能。压缩表也支持索引,但索引也是只读的。

以现在的硬件能力,对大多数应用场景,读取压缩表数据时的解压带来的开销影响并不大,而减少I/O带来的好处则要大得多。

压缩时表中的记录是独立压缩的,所以读取单行的时候不需要去解压整个表(甚至也不解压行所在的整个页面)。

MyISAM性能

MyISAM引擎设计简单,数据以紧密格式存储,所以在某些场景下的性能很好。

MyISAM有一些服务器级别的性能扩展限制,比如对索引键缓冲区(key cache)的Mutex锁, MariaDB基于段(segment) 的索引键缓冲区机制来避免该问题。

但MyISAM最典型的性能问题还是表锁的问题,如果你发现所有的查询都长期处于“Locked” 状态,那么毫无疑问表锁就是罪魁祸首。

1.5.3 转换表的引擎

ALTER TABLE

将表从一个引擎修改为另一个引擎最简单的办法是使用ALTER TABLE 语句。

下面的语句将mytable的引擎修改为InnoDB :

ALTER TABLE mytable ENGINE = InnoDB;

上述语法可以适用任何存储引擎。

但有一些问题

  1. 需要执行很长时间

MySQL会按行将数据从原表复制到一张新的表中,在复制期间可能会消耗系统所有的I/O能力,同时原表上会加上读锁。所以,在繁忙的表上执行此操作要特别小心。

  1. 失去和原引擎相关的所有特性

例如,如果将一张InnoDB表转换为MyISAM,然后再转换回InnoDB,原InnoDB表.上所有的外键将丢失。


一个替代方案是采用接下来将讨论的导出与导入的方法,手工进行表的复制。

导出与导入

为了更好地控制转换的过程,可以使用mysqldump工具将数据导出到文件,然后修改文件中CREATE TABLE语句的存储引擎选项,注意同时修改表名,因为同一个数据库中不能存在相同的表名,即使它们使用的是不同的存储引擎。

同时要注意mysqldump默认会自动在CREATE TABLE语句前加上DROP TABLE语句,不注意这一点可能会导致数据丢失。

创建与查询(CREATE和SELECT)

该转换的技术综合了ALTER方法的高效和导入导出的安全。

不需要导出整个表的数据,而是先创建一个新的存储引擎的表,然后利用INSERT…SELECT语法来导数据:

CREATE TABLE innodb _table LIKE myisan _table;
ALTER TABLE innodb_ table ENGINE InnoDB;
INSERT INTO innodb_ _table SELECT * FROM myisam. _table;

数据量不大的话,这样做工作得很好。

如果数据量很大,则可以考虑做分批处理,针对每一段数据执行事务提交操作,以避免大事务产生过多的undo。

假设有主键字段id,重复运行以下语句(最小值x和最大值y进行相应的替换)将数据导人到新表:

START TRANSACTION;
INSERT INTO innodb _table SELECT * FROM myisam _table WHERE id BETWEEN x AND y;
CONMIT;

这样操作完成以后,新表是原表的一个全量复制,原表还在,如果需要可以删除原表。

如果有必要,可以在执行的过程中对原表加锁,以确保新表和原表的数据一致。

附录

《高性能MySQL》
Baron Schwartz, Peter Zaitsev, Vadim Tkachenko著
宁海元周振兴彭立勋翟卫祥刘辉译

你可能感兴趣的:(MySQL,mysql,架构,服务器)