MYSQL
多版本并发控制
mysql的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制。
可以认为MVCC是行级锁的一种变种,但是它很多情况下避免了加锁操作,因为开销更低。
InnoDB的MVCC,是通过在每行记录最后保存的两个隐藏的列来实现,这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间),当然存储的并不是实际的时间值,而是系统版本好。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来查询到的每行版本号进行比较。
REPEATABLE READ隔离级别下,MVCC的实现:
SELECT
- InnoDB之查找版本早于当前事务版本号的数据行,这样可以确保事务读取的行,要么是在事务开始前已经存在,要么是事务自身插入或者修改过的。
- 行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行在事务开始之前未被删除。
INSERT
- InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
DELETE
- InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
UPDATE
- InnoDB为插入一航新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除版本号。
MVCC只在REPEATABLE READ跟READ COMMITED两个隔离级别工作。其他两个隔离级别都和MVCC不兼容。因为READ UNCOMMITED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的数据的行都加锁。
存储引擎
InnoDB存储引擎
InnoDB是MYSQL的默认事务型引擎,也是最重要、使用最广泛的存储引擎。除非有非常特别的原因需要使用其他的存储引擎,否则应该优先考虑InnoDB引擎。
InnoDB采用MVCC来支持高并发,并且实现了四个标准的隔离级别。默认级别是REPEATABLE READ(可重复读),并且通过间隙锁+MVCC策略防止幻读的实现,间隙锁使得InnoDB不仅仅锁定查询设计的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。
间隙锁:当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
参考:间隙锁(Next-Key锁)
主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。
InnoDB内部做了很多优化,包括从磁盘读取数据时采用的可预测性预读,能够自动在内存中创建hash索引以加速度操作的自适应哈希索引,以及能够加速插入操作的插入缓冲区等。
MyISAM存储引擎
在mysql5.1以及之前的版本,MyISAM是默认的存储引擎。MyISAM提供了大量的特性,包括全文索引、压缩、空间函数等,但是不支持事务和行级锁,而且有一个毫无疑问的缺陷是崩溃之后无法安全恢复。
对于只读的数据、或者表比较小、可以忍受修复操作,则依然可以使用MyISAM引擎。
创建MyISAM表的时候,如果指定了DELAY_KEY_WRITE选项,在每次修改执行完成时,不会立刻将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入到磁盘。这种方式可以极大地提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。
比较
- 事务:InnoDB支持事务,MyISAM不支持事务。
- 锁粒度:InnoDB支持表级锁跟行级锁,而MyISAM只支持表级锁。
- 外键:InnoDB支持外键。
- 备份:InnoDB支持热备份,但需要工具。
- 崩溃恢复:MyISAM崩溃后发生损坏的概率比InnoDB高很多,而且恢复的速度也比较慢。
- 其他特性:MyISAM支持全文索引、压缩、空间函数等特性。
备份的类型
- 冷备(cold backup):需要关mysql服务,读写请求均不允许状态下进行;
- 温备(warm backup): 服务在线,但仅支持读请求,不允许写请求;
- 热备(hot backup):备份的同时,业务不受影响。
索引
索引(也叫做“键(key)”)是存储引擎用于快速查找记录中的一种数据结构。
B-Tree索引
大多数mysql引擎都支持这种索引。
虽然使用术语“B-Tree",但是不同的存储引擎可能使用不同的存储结构,NDB集群存储引擎内部实际用的是T-Tree,InnoDB则使用B+Tree。
B-Tree索引能够加快访问数据的速度,因为存储引擎不需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点开始搜索,因此查找速度会快很多。
B-Tree对索引列是顺序组织存储的,很适合查找范围数据。因为索引树是有序的,所以除了用户查找,还可以用来排序和分组。
可以指定多个列作为索引列,多个索引列共同组成索引键。B-Tree索引适用于全键值、键值范围或键前缀查找,其中键前缀查找只适用与根据最左前缀查找。查找一定得按照索引的最左列开始。
B-Tree索引的数据结构
B-Tree
为了描述B-Tree,首先定义一条数据记录为二元组[key,data],key作为记录的键值,对于不同数据记录,key是互不相同的,data为数据记录除key外的数据。
- 所有节点具有相同的深度,也就是说B-Tree是平衡的。
- 一个节点中的key从左到右非递减排列。
- 如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。
查找算法:首先在根节点进行二分查找,如果找到则返回对应节点的data,否则在相应区间的指针指向的节点递归进行查找。
由于插入删除新的数据记录会破坏B-Tree的性质,因此在插入删除时,需要对树进行一个分裂、合并、旋转等操作以保持 B-Tree 性质。
B+Tree
与B-Tree相比,B+Tree有以下特点:
- 每个节点的指针上限为2d而不是2d+1(d为B-Tree的度)。
- 内节点不存储data,只存储key;外节点不存储指针。
带有顺序访问指针的B+Tree
一般在数据库系统或文件系统中使用的B+Tree结构都在经典B+Tree的基础上进行了优化,增加了顺序访问指针。
这个优化的目的是为了提供区间访问的性能,例如图中如果要查询key为18到49的所有记录。
优势
红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用B-Tree作为索引结构,主要有以下两个原因:
- 更好的检索次数:平衡树检索数据的时间复杂度等于树高h,而树高大致为O(h) = O(logN),其中d为每个节点的出度。红黑树的出度为2,而B-Tree的出度一般都很大,红黑树的树高h明显比B-Tree打非常多,因此检索次数也就更多。B+Tree相比较B-Tree更合适外存索引,因为B+Tree内节点去掉了data域,因此可以拥有更大的出度,检索效率会更高。
- 利用计算机预读特性:为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,因此速度会非常快。操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,相邻的节点也能够被预先载入。
哈希索引
InnoDB引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用得非常频繁,会在B+Tree索引之上再创建一个哈希索引,这样就让B+Tree索引具有哈希索引的一些优点,比如快速的哈希查找。
哈希索引能在O(1)时间进行查找,但是失去了有序性,它具有以下限制:
- 哈希索引只包含哈希值跟行指针,而不存储字段值,所以不能使用索引中的值来I避免都去行。
- 无法用于排序与分组。
- 只支持精确查找,无法用于部分查找与范围查找。
- 当出现哈希冲突时,存储引擎必须遍历链表中的所有行指针。
空间数据索引(R-Tree)
MyISAM表支持空间索引,可以用作地理数据存储。空间索引会从所有维度来索引数据,查询时可以根据任意维度来组合查询。
必须使用Mysql的GIS相关函数如MBRONTAINS()等来维护数据。
全文索引
全文索引是一种特殊类型的索引,它查找的是文本中的关键字,而不是直接比较索引中的值。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。
全文索引一般使用倒排序索引实现,它记录着关键词到期所在文档的映射。
MyISAM存储引擎支持全文索引,InnoDB存储引擎在Mysql 5.6.4版本中也开始支持全文索引。
索引的优点
- 大大减少了服务器需要扫描的数据行数。
- 帮助服务器避免进行排序和创建临时表(B+Tree索引是有序的,可以用来Order by和group by操作)。
- 将随机I/O变为顺序I/O(B+Tree索引是有序的,也就将相邻的数据都存储到一起)。
索引优化
独立的列
在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。
例如下面这个查询无法使用actor_id列的索引:
mysql> SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
多列索引
在需要使用多个列作为条件进行查询的时候,使用多列索引比使用多个单列索引性能更好。例如下面局域中,最好把actor_id跟film_id设置为多列索引。
mysql> SELECT film_id,actor_id FROM sakila.film_actor WHERE actor_id = 5 AND film_id = 1;
索引列中的顺序
让选择性最强的索引放在前面,索引的选择性是指:不重复的索引值和记录总数的比值。最大值为1,此时每个记录都有唯一的索引与其对应。选择性越高,查询效率也越高。
例如下面显示的结果中,customer_id的选择性比staff_id更高,因此最好把customer_id列放在多列索引前面。
SELECT COUNT(DISTINCT staff_id) / COUNT(*) AS staff_id_selectivity, COUNT(DISTINCT customer_id) / COUNT(*) AS customer_id_selectivity, COUNT(*) FROM payment;
前缀索引
对于BLOB、TEXT和VARCHAR类型的列,必须使用前缀索引,值索引开始的部分字符。
对于前缀索引的长度选取需要根据索引选择性来确定。
覆盖索引
一个索引包含(覆盖)所有需要查询的字段的值,就称之为“覆盖索引”。
具有以下优点:
- 因为索引条目通常小于数据行的大小,所以若只读取索引,能大大减少数据访问量。
- 一些存储引擎(例如MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此只访问索引可以不使用系统调用(通常比较费时)。
- 对于InnoDB引擎,若辅助索引能够覆盖查询,则无需访问主索引。
聚集索引
辅助索引
查询性能优化
优化数据访问
减少请求的数据量
- 只返回必要的列:最好不要使用SELECT *语句。
- 只返回必要的列:使用WHERE语句进行查询过滤,有时候也需要使用LIMIT语句来限制返回的数据,
- 查询重复查询的数据:使用缓存可以避免在数据库中进行查询,特别要查询的数据经常被重复查询,缓存可以带来的查询性能提升将会是非常明显的。
减少服务器端的扫描行数
最有效的方式就是使用索引来覆盖查询。
重构查询方式
一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事物日志、耗尽系统资源、阻塞很多小的但很重要的查询。
DELEFT FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
rows_affected = 0 do { rows_affected = do_query( "DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000") } while rows_affected > 0
分解大的链接查询
将一个大链接查询(join)分解成对每一个表进行单表查询,然后将结果在应用程序中进行关联。
- 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。
- 减少锁竞争;
- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可扩展。
- 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
SELECT * FROM tab JOIN tag_post ON tag_post.tag_id = tag.id JOIN post ON tag_post.post_id = post.id WHERE tag.tag = 'mysql';
SELECT * FROM tag WHERE tag = 'mysql'; SELECT * FROM tag_post WHERE tag_id = 123; SELECT * FROM post WHERE post.id IN(123,456,789);
SELECT * FROM post WHERE post.id IN (SELECT post_id FROM tag_post WHERE tag_id = (SELECT tag_id FROM tag WHERE tag = 'mysql'));
分库分表
垂直分表
日常开发和设计比较常见,即为“大表拆小表”,拆分是基于关系型数据库中的列(字段)进行的。通常情况下,某个表的字段比较多,可以新建一张“扩张表”,将不经常使用或者长度较大的字段拆分出去放在“扩张表”中。
在字段很多的情况下,拆分确实更便与开发和维护,某种意义上也能避免“跨页”问题。
拆分字段的操作建议在数据库设计阶段就做好,如果是在发展过程中拆分,则需要改写之前的查询语句,会额外带来一定的成本和风险,建议谨慎。
垂直分库
垂直分库在微服务盛行的今天已经非常普及了,基本思路就是按照业务模块划分出不同的数据库,而不是像早期一样将所有的数据表都放在一个数据库中。
系统层面的服务化拆分操作,能够解决业务系统层面的耦合和性能瓶颈,有利于系统的扩展维护。而数据库层面的拆分,道理也是相同的。
数据库的链接资源比较宝贵且单机处理能力也有限,在高并发场景下,垂直分库一定程度能够突破IO、连接数及单机硬件资源的瓶颈,是大型分布式系统中优化数据库架构的重要手段。
水平分表
也称横向分表,将表中不同的数据行按照一定的规律分不到不同的数据库表中(这些表保存在同一个数据库),这样来降低单表数据量,优化查询性能。最常见就是通过主键或者时间进行Hash和取模后拆分。
水平拆表能够降低单表的数据量,一定程度上可以缓解查询性能瓶颈,但本质上还保存在同一个库中,所以库级别还是会有IO瓶颈,所以一般不采用这种做法。
水平分库分表
思想同水平分表的思路相同,唯一不同的就是将这些拆分出来的表保存在不同的数据库中,这也是很多大型互联网公司所选择的的做法。
某种意义上来讲,有些系统中使用“冷热数据分离”(将一些使用较少的历史数据迁移到其他的数据库中,而在业务功能上,通常默认只提供热点数据的查询),也是类似的实践。在高并发和海量数据的场景下,分库分表能够有效缓解单机和单库的性能瓶颈和压力、突破IO、连接数、硬件资源的瓶颈,当然投入的硬件成本也会更高。同时,这也会带来一些复杂的技术问题和挑战(例如:跨分片的复杂查询,跨分布事务等)。
更多参考:
分不分表的几种常见形式以及可能遇到的难