MySql并不完美,但是足够灵活,能够适应高要求的环境,例如Web类应用。同时,MySql既可以嵌入到应用程序中,也可以支持数据仓库、内容索引和部署软件、高可用的冗余系统、在线事务处理系统(OLTP)等各种应用类型。
MySql最重要、最与众不同的特性是它的存储引擎架构,这种架构的设计将查询处理及其它系统任务和数据的存储/提取相分离。这种处理和存储分离的设计可以在使用时根据性能、特性,以及其它需求来选择数据存储的方式。
1、 MySql逻辑架构
下图是MySql的逻辑架构图:
第一层的服务主要是连接处理、授权认证、安全等。
第二层架构是MySql的核心服务功能所在的层次。包括查询解析、分析、优化、缓存以及所有的内置函数(例如,日期,时间,数据和加密函数),所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。
第三次包括了存储引擎,存储引擎负责MySql中数据的存储和提取。服务器通过API与存储引擎进行通信,这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询过程透明。存储引擎API包含几十个底层函数,用于执行诸如“开始一个事务”或者“根据主键提取一行记录”等操作。
- 连接管理与安全性
每个客户端连接都会在服务器进程中拥有一个线程,这个连接的查询只会在这个单独的线程中执行,该线程只能轮流在某个CPU核心或者CPU中运行。服务器会负责缓存线程,因此不需要为每一个新建的连接创建或者销毁线程。
当客户端连接到MySql服务器时,服务器需要对其进行认证。认证基于用户名、原始主机信息和密码。如果使用了安全套接字(SSL)的方式连接,还可以使用X.509证书认证。一旦客户端连接成功,服务器会继续验证该客户端是否具有执行某个特定查询的权限(例如,是否允许客户端对world数据库的Country表执行select语句)。
- 优化与执行
MySql会解析查询,并创建内部数据结构(解析树),然后对其进行各种优化,包括重写查询、决定表的读取顺序,以及选择合适的索引等。用户可以通过特殊的关键字提示优化器,影响它的决策过程。
对于select语句,在解析查询之前,服务器会先检查查询缓存,如果能够在其中找到对应的查询,服务器就不必再执行查询解析、优化和执行的整个过程,而是直接返回查询缓存中的结果集。
2、并发控制
无论何时,只要有多个查询需要在同一个时刻修改数据,都会产生并发控制的问题。
- 读写锁
在处理并发读或者写的时候,可以通过实现一个由两种类型的锁组成的锁系统来解决问题。这两种类型的锁通常被称为共享锁(shared lock)和排他锁(exclusive lock),也叫读锁(read lock)和写锁(write lock)。
读锁是共享的,或者说是相互不阻塞的。多个客户在同一个时刻可以同时读取同一个资源,而互不干扰。写锁则是排他的,也就是说一个写锁会阻塞其它的写锁和读锁,这是出于安全策略的考虑,只有这样,才能确保在给定的时间里,只有一个用户能执行写入,并防止其它用户读取正在写入的同一资源。
在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改某一部分数据时,MySql会通过锁定防止其它用户读取同一数据。大多数时候,MySql锁的内部管理都是透明的。
- 锁粒度
一种提高共享资源并发性的方式就是让锁定对象更有选择性。尽量只锁定需要修改的部分数据,而不是所有的数据。在给定的资源上,锁定的数据量越少,则系统的并发程序越高,只要相互之间不发生冲突即可。
问题是加锁也需要消耗资源。锁的各种操作,包括获得锁、检查锁是否已经解除、释放锁等,都会增加系统的开销。如果系统花费大量的时间来管理锁,而不是存取数据,那么系统的性能可能会因此受到影响。
所谓的锁策略,就是在锁的开销和数据的安全性之间寻求平衡,这种平衡当然会影响到性能。大多数数据库系统一般都是在表上施加行级锁(row-level lock),并以各种复杂的方式来实现,以便在锁比较多的情况下尽可能地提供更好的性能。
MySql则提供了多种选择。每种MySql存储引擎都可以实现自己的锁策略和锁粒度。下面介绍两种最重要的锁策略。
(1)表锁
表锁是MySql中最基本的锁策略,并且是开销最小的策略。表锁会锁定整张表。一个用户在对表进行写操作(插入、删除、更新等)前,需要先获得写锁,这会阻塞其它用户对该表的所有读写操作。只有没有写锁时,其它读取的用户才能获得读锁,读锁之间是不相互阻塞的。
写锁比读锁有更高的优先级,因此一个写锁请求可能会被插入到读锁队列的前面。
(2)行级锁
行级锁可以最大程度地支持并发处理,同时也带来了最大的锁开销。在InnoDB和XtraDB,以及其它一些存储引擎中实现了行级锁。行级锁只在存储引擎层实现,而MySql服务器层没有实现。服务器层不了解存储引擎中的锁实现。
3、事务
事务就是一组原子性的SQL查询。该组SQL语句要么全部执行成功,要么所有语句都不会执行。
使用start transaction语句开始一个事务,然后要么使用commit提交事务将修改的数据持久保留,要么使用rollback撤销所有的修改。
start transaction;
......sql语句;
commit;
一个运行良好的事务处理系统,必须具备ACID标准。ACID表示原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。
原子性:事务中的所有操作要么全部执行成功,要么都不会执行。
一致性:数据库总是从一个一致性的状态转移到另外一个一致性的状态。
隔离性:一个事务所做的修改在最终提交之前,对其它事务是不可见的。
持久性:一旦事务提交,则其所做的修改就会永久保存到数据库中。
一个实现了ACID的数据库,相比没有实现ACID的数据库,通常会需要更强的CPU处理能力、更大的内存和更多的磁盘空间。对于一些不需要事务的查询类应用,选择一个非事务型的存储引擎,可以获得更高的性能。即使存储引擎不支持事务,也可以通过 lock tables语句为应用提供一定程度的保护。
- 隔离级别
在SQL标准中定义了四种隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统的开销也更低。
下面是四种隔离级别的介绍:
(1)Read Uncommitted(未提交读)
在Read Uncommitted级别,事务中的修改,即使没有提交,对其它事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读。这个级别会导致很多的问题,在实际应用中一般很少使用。
(2)Read Committed(提交读)或不可重复读
大多数的数据库系统的默认隔离级别是Read Committed(但MySql不是)。Read Committed满足前面提高的隔离性的定义:一个事务开始前,只能看见已经提交的事务所做的修改。也就是说,一个事务从开始直到提交之前,所做的任何修改对其它事务都是不可见的。这个级别有时候也叫做不可重复读,因为两次执行同样的查询,可能会得到不一样的结果。
(3)Repeatable Read(可重复读)
Repeatable Read解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但是理论上,Repeatable Read还是无法解决另外一个幻读(phantom Read)的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。InnoDB和XtraDB存储引擎通过多版本并发控制(MVCC)解决了幻读的问题。
可重复读是MySql的默认事务隔离级别。
(4)Serializable(可串行化)
Serializable是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题。简单来说,Serializable会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。
- 死锁
死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。多个事务同时锁定同一个资源时,也会产生死锁。
为了解决这种问题,数据库系统实现了各种死锁检测和死锁超时机制。越复杂的系统,比如InnoDB存储引擎,越能检测到死锁的循环依赖,并立即返回一个错误。
还有一种解决方式,就是当查询的时间达到锁等待超时的设定后放弃锁请求,这种方式通常来说不太好。
InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚。
死锁发生后,只有部分或者完全回滚其中一个事务,才能打破死锁。对于事务型的系统,这是无法避免的,所以应用程序在设计时必须考虑如何处理死锁。大多数情况下只需要重新执行因死锁回滚的事务即可。
- 事务日志
事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。
事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序IO,而不像随机IO需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快很多。事务日志持久化后,内存中被修改的数据在后台可以慢慢地刷回磁盘。目前大多数存储引擎都是这样实现的,称之为预写式日志,修改数据需要写两次磁盘。
如果数据的修改已经记录到事务日志并持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。
- MySql中的事务
MySql中提供了两种事务型的存储引擎:InnoDB和NDB Cluster。还有一些第三方存储引擎也支持事务。
自动提交(autocommit)
MySql默认采用自动提交模式。也就是说,如果不是显式地开始一个事务,则每个查询都被当作一个事务执行提交操作。在当前连接中,可以通过设置autocmmit变量来启用或禁用自动提交模式。
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.02 sec)
1或者ON表示启用,0或者OFF表示禁用。当Autocommit=0时,所有的查询都是在一个事务中,直到显式地执行commit提交或者rollback回滚,该事务结束,同时又开始了另一个新事务。
MySql可以通过执行set transaction isolation level命令来设置隔离级别。
在事务中混合使用存储引擎
MySql服务器层不管理事务,事务是由下层的存储引擎实现的。所以在同一个事务中,使用多种存储引擎是不可靠的。
如果在事务中混合使用了事务型和非事务型的表(比如InnoDB和MyISAM表),在正常提交的情况下不会有什么问题。
但如果该事务需要回滚,非事务型的表上的变更就无法撤销,这会导致数据库处于不一致的状态。所以为每张表选择合适的存储引擎很重要。
在非事务型的表执行事务相关的操作的时候,MySql通常不会发出提醒,也不会报错。有时候只有回滚的时候才会发出一个警告“某些非事务型的表上的变更不能被回滚”。但大多数情况下,对非事务型表的操作都不会有提示。
隐式和显式锁定
InnoDB采用的是两阶段锁定协议。在事务执行过程中,随时都可以执行锁定,锁只有在执行Commit或者Rollback的时候才会释放,并且所有的锁是在同一时刻被释放。前面描述的锁定都是隐式锁定,InnoDB会根据隔离级别在需要的时候自动加锁。
另外,InnoDB也支持通过特定的语句进行显式锁定,这些语句不属于SQL规范:
select .. lock in share mode
select ..for update
MySql也支持lock tables 和 unlock tables语句,这是在服务器层实现的,和存储引擎无关。它们有自己的用途,但并不能替代事务处理。如果应用需要用到事务,还是应该选择事务型存储引擎。
4、多版本并发控制
MySql的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC)。
可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
下面通过InnoDB的简化版行为来说明MVCC是如何工作的。
InnoDB的MVCC是通过在每行记录后保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号。每开启一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
下面看在reapeatable read隔离级别下,MVCC具体是如何操作的。
select:InnoDB会根据以下两个条件检查每行记录:
1.InnoDB只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
2.行的函数版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。
只有符合上述两个条件的记录,才能返回作为查询结果。
Insert:InnoDB为新插入的每一行记录保存当前系统版本号作为行版本号。
Delete: InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
Update: InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。
MVCC只在Repeatable read和Read committed两个隔离级别下工作。其它两个隔离级别和MVCC不兼容。因为Read uncommitted总是读取最新的数据行,而不是符合当前事务版本的数据行。而Serializable则会对所有读取的行都加锁。
5、存储引擎
在文件系统中,MySql将每个数据库保存为数据目录下的一个子目录。创建表时,MySql会在数据库子目录下创建一个和表同名的.frm文件保存表的定义。例如创建一个名为MyTable的表,MySql会在MyTable.frm文件中保存该表的定义。因为MySql使用文件系统的目录和文件来保存数据库和表的定义,大小写敏感性和具体的平台密切相关。在Windows中,大小写是不敏感的;而在类Unix中则是敏感的。不同的存储引擎保存数据和索引的方式是不同的,但表的定义则是在MySql服务层统一处理的。
- InnoDB存储引擎
InnoDB存储引擎是MySql默认的事务型引擎,也是最重要、使用最广泛的存储引擎。
InnoDB存储引擎的数据存储在表空间中,表空间是由InnoDB管理的一个黑盒子,由一系列的数据文件组成。在MySql4.1以后的版本中,InnoDB存储引擎可以为每个表的数据和索引存放在单独的文件中。
InnoDB存储引擎采用MVCC来支持高并发,并且实现了四个标准的隔离级别,其默认级别是Repeatable Read(可重复读),并且通过间隙锁策略防止幻读的出现。间隙锁使得InnoDB不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。
InnoDB表是基于聚簇索引建立的。聚簇索引对主键查询有很高的性能,不过它的二级索引中(非主键索引)中必须包含主键列,所以如果主键列很大的话,其它的所有索引都会很大。因此,若表上的索引较多的话,主键应当尽可能的小。
InnoDB存储引擎内部做了很多优化,包括从磁盘读取数据时采用的可预测性读,能够自动在内存中创建hash索引以加速读操作的自适应哈希索引,以及能够加速插入操作的插入缓冲区等。
- MyISAM存储引擎
MySql5.1 版本之前,MyISAM是默认的存储引擎。MyISAM提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,还有就是崩溃后无法安全恢复。
MyISAM会将表存储在两个文件中:数据文件和索引文件,分别以.MYD和.MYI为扩展名。MyISAM表可以包含动态或者静态行。MyISAM会根据表的定义来决定采用何种行格式。MyISAM表可以存储的行记录数,一般受限于可用的磁盘空间或者操作系统中单个文件的最大尺寸。
加锁与并发:MyISAM对整张表加锁,而不是针对行。
修复:通过check table mytable检查表的错误,如果有错误可以通过执行repair table mytable进行修复。另外,如果MySql服务器已经关闭,也可以通过myisamchk命令行工具进行检查和修复操作。
索引特性:对于MyISAM表,即使是Blob和Text等长字段,也可以基于其前500个字符创建索引。MyISAM也支持全文索引,这是一种基于分词创建的索引,可以支持复杂的查询。
延时更新索引键:创建MyISAM表的时候,如果指定了Delay_key_write选项,在每次修改执行完成时,不会立刻将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入到磁盘。
MyISAM压缩表:如果表在创建并导入数据以后,不会再进行修改操作,那么这样的表适合采用MyISAM压缩表。可以使用myisampack对MyISAM表进行压缩。压缩表是不能进行修改的。压缩包可以极大地减少磁盘空间占用,因此也可以减少磁盘IO,从而提升查询性能。压缩表也支持索引,但索引也是只读的。