先来了解一下存储引擎,因为不同存储引擎索引和锁的实现是不同的。
MySQL存储引擎其实就是对于数据库文件的一种存取机制,如何实现存储数据,如何为存储的数据建立索引以及如何更新,查询数据等技术实现的方法。因为他是开源的所以就出现了各种各样存储数据的方式因此就出现了很多种储存引擎,例如MyISAM,InnoDB、Memory等等。虽然存储引擎很多,不然常用的就两个分别是:MyISAM和InnoDB。默认MySQL的存储引擎为InnoDB。
varchar
类型的字段时,就会自动使用静态格式。使用静态格式的表的性能很高,因为在维护和访问数据时需要的开销很低。不过占用的空间很大varchar
,那么就会自动使用动态格式,虽然使用动态格式占用的空间比较小,但是性能会变低,而且更新、删除时会产生碎片,因此需要定期执行optimize table
清理碎片my-z[ei]m
、mai-ai-sam
索引是一种数据结构,通过索引提升查询速度,,加速表之间的连接等等。类似于书的目录,通过目录,我们可以很快找到对应的内容。不过使用索引后索引也要占用一部分空间。并且当我们对数据进行增删改的时候也需要对索引进行维护。所以索引适用于经常查询但是改动不频繁的数据。
索引类型根据功能划分可以分为:
根据列划分可以分为
根据物理存储方式可以分为:
索引的数据结构有很多中,例如Hash、B-Tree、B+Tree 。在MySQL的InnoDB中索引默认使用的数据结构为B+Tree
B+Tree由B-Tree优化而来,而B-Tree由平衡二叉树优化而来,而Hash通常不会用来当做索引
Hash是以键值对进行存储的。Hash当做索引存储时,Key可以存储索引列,Value可以存储行记录或者行磁盘地址。Hash表在等值查询时效率很高,时间复杂度为O(1);但是不支持范围快速查找,范围查找时还是只能通过扫描全表方式。并且哈希索引不支持组合索引的最左匹配规则。如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题
平衡二叉树是二叉树的优化版本,在普通二叉树中每个节点最多有2个分叉,左子树和右子树数据顺序为左小右大。但是普通二叉树却很容易出现子树节点向一边倾斜的问题。为了处理这个问题就出现出现了平衡二叉树,平衡二叉树在插入删除数据时通过左旋/右旋操作保持二叉树的平衡,这样不会出现一边子树很高、一边子树很矮的情况了。使用平衡二叉树可以很好的兼容二分查找法。不过由于在平衡二叉树中每个节点只能存储一个元素,所以每个节点都相当于与一次IO操作,如果表中的数据越来越多那么树的高度就会越来越高。从而导致io的次数增加查询性能变差
B-Tree是平衡二叉树的优化版本,由于在平衡二叉树中IO太频繁了,所以B-Tree主要的目的就是减少IO的次数,降低树的高度。所以在B-Tree中,每个节点会储存多个元素以及元素的子节点的地址并尽可能多的存储数据,这样二叉树就变成了多叉树。通过增加树杈降低树的高度,这样IO的次数就减少了。在B-Tree中每个节点中的元素会包含键和数据,并且元素之间会根据键值排序。这样在查找的过程中可以直接读取节点数据到内存中,然后再内存中对节点中的元素进行比较,虽然比较次数没有减少,但是IO次数却减少了很多。所以使用B-Tree构建索引可以很好的提升查询的效率。不过B-Tree并不支持快速范围查询,在范围查询的情况下需要多次从根节点进行查找,而且由于每个节点中都存储数据,所以当表的字段越多那么每条记录所占的空间就会越大,这样也会导致树变高,从而增加IO次数
B+Tree是B-Tree的优化版本。B-Tree和B+Tree的区别是,在B-Tree中所有节点都会存储数据,而在B+Tree中只有子叶节点才会存储数据,并且最底层的子叶节点之间会形成一个双向链表。由于在B+Tree中只有子叶节点才会存储数据,所以非子叶节点可以存储更多的键从而拥有更多的子叶节点,因此B+Tree的树会更矮并且基本不会被表中的列的数量影响。由于子叶节点之间会形成一个双向链表并且只有子叶节点会存储所有的数据,所以他可以很好的支持范围查询,也就是在范围查询时当我们通过B+Tree查找到范围的最大值或最下值之后,我们只需要根据子叶节点的链表向前或先后进行遍历查找就可以了,这样就不需要从根节点遍历了。
MyISAM和InnoDB的索引实现是不同的,并且他们的存储方式也不同
MyISAM的索引都是非聚簇索引,索引和数据文件是分开存储的,.MYD
为数据文件 .MYI
为索引文件。默认使用B+Tree作为索引结构,在索引中叶子节点中存储的键为索引列的值,数据为索引所在行的磁盘地址。他的主键索引和其他的索引实现也基本相同,只是不同索引的约束不同而已。
在InnoDB中会存在一个聚簇索引和多个非聚簇索引(也可以称为辅助索引或二级索引)。通常聚簇索引就是主键索引,当一个表没有创建主键索引时,InnoDB会自动创建一个ROWID隐式字段来构建聚簇索引。
在InnoDB中索引和数据文件是存储在一起的,聚簇索引的子叶节点中会存储对应的数据,而其他的非聚簇索引中子叶节点存储的是数据的主键值。所以非聚簇索引访问数据时需要二次查找,也就是需要先根据索引查找到对应的主键值,然后再通过主键值到聚簇索引中查找数据(通过主键值到聚簇索引中查找称为回表查询)
索引分为普通的单列索引和组合索引(也可以称为联合索引),单列索引就是把单个字段的值作为索引的键,而组合索引就是把多个字段的值的组合作为索引的键。
在组合索引中会使用最左前缀匹配原则,例如创建组合索引a,b,c
,在查找时他首先会根据a字段进行查找,如果a字段相同在根据b字段进行查找,如果a和b都相同才根据c字段进行查找。也就是说创建组合索引(a,b,c)
相对于创建了(a)、(a,b)、(a,b,c)
三个索引。所以组合索引遵循最左前缀匹配原则。
根据最左匹配原则会出现下面情况,如果表中有主键索引i
,组合索引为a,b,c
,单列索引d
,非索引字段e
a
字段(不受编写时的顺序影响)那么就会使用组合索引,例如a=1
、b=1 and a=2
、a=1 and c=3
等等a
字段,且查询字段中包含除了主键或组合索引以外的字段,那么就不会使用组合索引。
select * from table where b=3 and c=4
或select a,d,e from table where b=3 and c=4
a
字段,不过查询字段中只有主键或组合索引字段,那么是可以使用组合索引的,并且会使用覆盖索引的优化
select i,a,b from table where b=3 and c=4
a=任何值
覆盖索引并不是一种索引,而是一种优化手段。因为在使用非聚簇索引进行查询的时候,我们只可以获取主键值,之后还需要通过主键值进行回表查询最后获取数据。如果当我们只需要获取索引列的数据,那么通过非聚簇索引查询后就可以拿到结果了,这时就没必要在进行回表查询。这种情况就是覆盖索引。通过覆盖索引避免回表操作能够有效提高查询效率。
为了更有效的使用覆盖索引,在创建索引的时候我们可以根据情况创建更多的组合索引。因为组合索引中可以包含很多字段,如果我们需要频繁的查询某些字段,那么我们可以通过这些字段创建组合索引,那么在查询这些字段的时候就可以使用覆盖索引,避免回表操作,从而提高效率。并且使用组合索引相比于使用一个个的单列索引更节省空间。
创建组合索引后相当于创建了多个索引,所以我们使用的时候并不一定要每次都使用到所有的组合索引列,只需要使用组合索引的时候符合最左匹配原则就可以了。所以通常情况下优先使用组合索引
查看表中有那些索引
show index from table_name;
查看某个查询是否使用索引:
创建索引应该优先创建组合索引,因为组合所以相比于一个个创建索引占用的空间更小,并且更有利于使用覆盖索引优化。其中应该创建索引的情况包括
%
开头or
操作!=
、is null
、is not null
操作时索引会失效,主键索引没影响。数据库中锁可以分为很多类型
其中这些锁之间的兼容关系为,其中第一行为已有锁,第一列为要加的锁:
注意这里的共享锁和排他锁包括行级和表级的共享锁、排他锁
共享锁(S) | 排他锁(X) | 意向共享锁(IS) | 意向排他锁(IX) | |
---|---|---|---|---|
共享锁(S) | 兼容 | 兼容 | ||
排他锁(X) | ||||
意向共享锁(IS) | 兼容 | 兼容 | 兼容 | |
意向排他锁(IX) | 兼容 | 兼容 |
简单概括就是
在mysql中行锁是基于索引实现的。并且mysql中行锁还可以进一步细分为:记录锁(Record Lock)、间隙锁(Gap Lock)、 Next-Key锁、插入意向锁
1,5,9
。我们的操作需要匹配2
,这时候肯定匹配不到所以就会将2所在的区域的两个前后两个索引间的范围锁起来也就是1-5
这个区域。使用间隙锁可以防止其他事务在这个范围内插入或修改记录,保证记录不会变,从而不会出现幻读现象。并且间隙锁只在可重复读(Repeatable Read 简称 RR)的事务隔离级别下才会使用,MySql默认的事务隔离级别就是可重复读
1,5,9
。我们匹配10,那么就会锁住9-正无穷
的区域。同理如果匹配0,那么就会锁住负无穷-1
的区域1,5,9
,我们的操作需要匹配5
,那么他会将1-5,5,5-9
的区域锁起来,其中1-5和5-9为间隙锁而5为记录锁,所以他是记录锁和间隙锁的组合。这样做仍然是为了解决幻读的问题,因为字段值为5的数据可能有多条,为了防止在新增一条字段值为5的数据就只能将前后两个索引间的范围锁住。并且Next-Key锁只在可重复读(Repeatable Read 简称 RR)的事务隔离级别下才会使用这些锁的兼容关系为,其中第一行为已有锁,第一列为要加的锁:
记录锁 | 间隙锁 | Next-Key锁 | 插入意向锁 | |
---|---|---|---|---|
记录锁 | 兼容 | 兼容 | ||
间隙锁 | 兼容 | 兼容 | 兼容 | 兼容 |
Next-Key锁 | 兼容 | 兼容 | ||
插入意向锁 | 兼容 | 兼容 |
简单概括就是
一个操作是否需要加锁以及加什么锁,主要根据以下几个条件判断,分别是:
数据库的存储引擎
sql语句
SELECT ...
语句正常情况下为快照读,不加锁;SELECT ... LOCK IN SHARE MODE
语句为当前读,加共享行锁;SELECT ... FOR UPDATE
语句为当前读,加排他行锁;数据库的隔离级别,需要加锁进行操作的情况下Mysql的隔离级别和正常的隔离级别有些不同
where从句中的条件是否使用索引以及索引的类型
测试锁的时候只需要启用两个连接(例如在SQLyog中启动两次,注意在SQLyog中一次连接启动两个窗口是不行的),然后为了不让他自动提交事务我们可以在sql语句前后添加begin
和commit
,这样只要不执行commit事务就不会提交。
注意请不要搞混了,我们平常执行的查询语句是快照读哦,是不需要加锁的,所以平常写sql的时候不需要关心锁的问题。但如果是SELECT ... LOCK IN SHARE MODE
、SELECT ... FOR UPDATE
和增删改操作则需要进行加锁
mvcc称为多版本并发控制,在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
在mysql中读操作分为两种
SELECT ... LOCK IN SHARE MODE
或SELECT ... FOR UPDATE
这种语句进行数据读取时采用的就是当前读。他读取的是记录的最新版本。并且读取时还要保证其他并发事务不能修改当前记录,因此会对读取的记录进行加锁。SELECT...
语句进行读取操作时,使用的就是快照读,当然前提是事务隔离级别不为串行化,在串行化中都是当前读,所以串行化对数据的并发操作性能影响是很大的。快照读是基于多版本并发控制实现的,多版本并发控制指的就是维护一个数据的多个版本,使得读写操作没有冲突。这样读取数据就不需要加锁了,并且由于快照读其实读取的是数据的某个版本所以还可以解决脏读,幻读,不可重复读等问题,但不能解决更新丢失问题,因为读取的版本可能不是最新版本MVCC在mysql中的实现主要通过隐式字段,undo日志以及Read View来实现
在不同事务隔离级别的影响下,快照读的结果是不同的
上面说的锁基本都是InnoDB的锁,因为MyISAM是不支持事务的。不支持事务意味着无法回滚数据,并且会出现事务安全中的一系列问题。由于一份数据只允许同时由一个进程操作,所以MyISAM的表也有锁,不过MyISAM只支持表锁,在执行增删改的时候默认会加表级排他锁,而查时会加表级共享锁。但是操作完成后就会立即释放锁,相当于每个操作都是一个事务,而在InnoDB中一个事务里面可以进行多次操作。在MyISAM中如果要测试锁得手动加锁才能测试或在查询时通过sleep测试,因为没有事务所以不能使用begin和commit
测试
例如:
进程1:
-- 给test_myisam表加读锁,如果要加写锁可以将READ改为WRITE
LOCK TABLE test_myisam READ ;
SELECT * FROM test_myisam ;
--释放当前进程的所有锁
UNLOCK TABLES ;
-- 或通过sleep测试,注意sleep得在查询字段之后
SELECT *,SLEEP(10) FROM test_myisam ;
进程2:
-- 由于增删改会加写锁所以会阻塞,不过查询是可以的
UPDATE test_myisam SET age=30 WHERE id=1;
MyISAM在加共享锁之后,理论上是不允许同时插入数据的,但是mysql有个系统变量concurrent_insert
用于控制其插入行为(只允许插入,不允许更新等),可以通过select @@concurrent_insert;
查询,他的值包括
NEVER (or 0)
时,不允许并发插入AUTO(or 1)
时,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。这也是MySQL 的默认设置
optimize table table_name[,table_name]
来收回空洞ALWAYS (or 2)
时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。在查询时如果需要支持并发插入,那么加锁时得添加READ LOCAL
,而不是READ
。例如LOCK TABLE test_myisam READ LOCAL;
由于读写操作分别加读锁和写锁,所以如果同时执行读写操作那么他们会以串行的方式进行。并且在MyISAM存储引擎中,写锁的优先级会高于读锁的优先级。也就是当有读请求和写请求时,写请求会优先获得锁(即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前)。这样当有大量的写请求就会导致查询操作很难获取到锁从而可能一直在阻塞中。这也正是有大量修改操作的表不适合用MyISAM存储引擎的原因。
不过我们可以设置MyISAM的调度行为:
low-priority-updates
,使MyISAM引擎默认给予读请求优先的权利。SET LOW_PRIORITY_UPDATES=1
,使该连接发出的更新请求优先级降低。LOW_PRIORITY
属性,降低该语句的优先级。例如:UPDATE [LOW_PRIORITY] tabel_name SET col1=expr1,col2=expr2,...
max_write_lock_count
设置一个合适的值,当一个表的读锁达到这个值后,MySQL便暂时将写请求的优先级降低,给读进程一定获得锁的机会,例如SET max_write_lock_count=100
如果一张表有一亿条数据那么优化可以通过这几个方面进行优化
当数据量很大的时候,如果创建了索引那么查询的时候查询的效率取决于查询数据的多少,如果查询的数据很少那么也是很快的,如果查询的数据多,哪自然就很慢,所以如果数据很多我们查询的时候通常都需要指定一个范围,例如查询时必须指定一个时间范围。当然当数据量很大那么索引也会很大,而插入的时候由于需要创建索引所以插入时会很慢
当查询的数据很多,分页的起点会影响查询的效率,例如
select * from table_name limit 0,10
-- 耗时0.003秒
select * from table_name limit 1000000,10
-- 耗时7.28秒
这种情况我们可以对分页进行改写
select * from table_name
where id >= (select id from table_name limit 1000000, 1)
limit 0,10
-- 耗时0.365秒
改写后子查询是直接获取主键数据所以会很快(使用了主键索引并且会使用覆盖索引优化),而外部查询是根据主键筛选数据,筛选后数据量很小,所以分页就很快了
MySQL中InnoDB类型的表会生成一个.idb
用于存储表数据,和一个.frm
用于存储表定义。而MyISAM类型的表会生成.myd
文件用于存储数据,.myi
用于存储索引和.frm
用于存储表定义。当表中的数据越来越多,那么存储数据的文件就会越来越大。这个时候我们可以通过分区partition
,将数据存储到多个分区文件中,其实就是将数据文件分为多个文件。
使用分区的好处是,在分区之后如果我们通过分区字段进行数据筛选那么只需要筛选部分分区就可以了这样可以加快筛选速度,同时在涉及sum()和count()
这类聚合函数的查询时,可以在每个分区上面并行处理,最终只需要汇总所有分区得到的结果。而且还可以让表存储更多的数据。
不过如果在创建分区的时候表中有主键,那么分区字段中必须包含主键,所以想要使用分区筛选就必须包含主键。如果数据不是特别多使用分区和使用索引的性能基本一致。并且分区并不能替代分表分库,如果想快速提高性能还是得分表分库
分区可以支持多种类型:
通过主从复制可以实现读写分离,对请求进行负载、对数据进行备份提高可用性等等,并且主从复制是MySQL自带的功能,直接配置就行
主从复制是基于binlog
(二进制日志)实现的。开启binlog后数据库他会将所有的操作都记录到binlog中。并且开启主从复制后会额外开启三个线程,分别是:主节点的IO线程以及从节点的IO线程和SQL线程。
relaylog
(中继日志)中。当然也会将binlog的文件名和位置记录到master-info文件中以便知道后续从什么地方开始同步主从复制方式可以通过binlog_format
来进行设置,例如:binlog_format=mixed
GTID复制模式是MySQL5.6新增的。在原来基于二进制日志的复制中,从库需要告知主库要从日志的哪个偏移位置开始进行同步,如果指定错误会造成数据的遗漏,从而造成数据的不一致。并且当发生故障,需要主从切换,那么需要找到binlog的偏移位置也就是position,然后将主节点指向新的主节点,相对来说比较麻烦。而通过GTID复制模式就不用再找binlog的position了,他会基于事务自动找到同步的位置
GTID的工作原理为:
当然GTID也有缺点,例如:
create table….select
语句复制
create temporary table
和 drop temporary table
语句不支持启用主从复制之后我们可以让主库用于写操作,从库用于读操作,这样就可以使用读写分离了。在我们程序中使用读写分离,我们可以使用应用层的实现方案,使用应用层的实现方案我们在程序中直接导入相关依赖就可以了,例如sharding-jdbc
就支持读写分离,当然也可以使用代理层的实现方案,使用代理层的实现方案后我们连接数据库直接连接代理就可以了,例如Mycat
和sharding-proxy
。
由于主从复制中主节点会将所有的操作写入binlog,然后binlog更新的内容会被同步到从节点的relaylog中,最后从节点在从relaylog中读取对应的数据进行操作从而实现同步。所以延迟是一定会存在的。无法做到实时的强一致。我们能做的是减少延迟的时间。如果非要强一致可以直接读主库并添加缓存
导致延迟的情况可能包括:
查看主从复制是否出现延迟可以通过show slave status
命令输出的Seconds_Behind_Master
参数的值来判断
解决办法
关系型数据库本身比较容易成为系统瓶颈,单机存储容量、连接数、处理能力都有限。当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从库、优化索引,做很多操作时性能仍下降严重。此时就要考虑对其进行切分了,切分的目的就是为了解决由于数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成若干数据库 ,将数据大表拆分成若干数据表,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。
数据切分主要可以分为两种,分别是:分表和分库。而在进行分表和分库的时候我们可以根据需求进行水平拆分或垂直拆分。
使用优先级:
在使用的时候应该优先考虑垂直分表,这个我们在设计表结构的时候就可以做到。然后是水平分表。水平分表后如果是由于服务器导致的瓶颈,我们可以考虑进行垂直分库,当然在垂直分库后每个库中可以进行垂直分表(一般不会在进行垂直分表,在第一次垂直分表后应该就不能在继续分了)和水平分表的操作。如果垂直分库后,在库中某张表的数据非常大而且查询非常的频繁,以至于在单个数据库中进行水平分表后还是会因为服务器的性能出现了瓶颈,那么我们就可以对这个库中的表进行水平分库,将表的数据拆分到其他的库中,其实相当于将水平分表后的表放到其他库中,当然一般不会有怎么夸张的表和数据量。
分库分表的实现大致可以分为两种,分别是:
sharding-JDBC
(新版叫ShardingSphere-JDBC
)就是应用层的实现Mycat
、sharding-proxy
就是代理层的实现其实在应用层面我们也可以自己实现简单的分表和分库,例如我们可以通过微服务并且每个服务连接自己的数据库这样就实现了分库,而分表我们可以自己实现存储过程来进行插入和查询,从而将数据分散在不同的表中。当然如果目前已有的实现满足需求那也就没必要自己去实现了
在进行分库分表后会引入一些新的问题,其中包括:
分库分表后自增主键重复问题,解决方法是:
跨库事务问题:当更新内容同时分布在不同库中,就会出现跨库事务问题
很多分库分表的实现不支持不同库的表进行关联操作。所以在分库的时候应该尽可能的将有关联的表放在同一个库中,或弄一些全局表每个库都存一份。当然也可以在应用层查询两次然后再把数据进行拼装
当然还有很多限制,不同分库分表的实现有不同的限制,例如某些操作符不支持(sharding-JDBC中不支持case when、having、union
等等)