负责刷新内存池中的数据,保证缓冲池中的内存缓存是最近的数据。该线程具备最高的优先级,会根据数据库的运行状态在loop、background loop、flush loop、suspend loop之间切换。
由于InnDB使用了大量的AIO技术来处理IO请求,IO线程主要用来处理这些AIO的回调。在Linux下,IO Thread不能调整,只能是读线程、写线程、插入缓冲线程和日志线程。
事务提交后回收已经分配的undo日志,离散的读取undo页面,可以更好的利用磁盘的随机读写性能。
将之前版本中脏页的刷新放到单独的线程,来减轻Master Thread的负担。
InnoDB是基于磁盘的存储引擎,按照页的方式进行管理。页从缓冲池刷新会磁盘的操作并不是在每次发生页更新的时候触发,而是通过CheckPoint机制刷回到磁盘。
32位操作系统的限制,在32位系统下最多将该值设置为3GB,打开操作系统的PAE的选项获得32位操作系统的最大64GB内存支持。
InnoDB允许多个缓冲池实例,每个页根据Hash值平均分配到不同的缓冲池实例。数据库的缓冲池是通过LRU算法进行管理的,缓冲池页大小默认是16KB。
由innodb_buffer_pool_size
参数控制,占用最大的内存,用于存放各种缓冲数据,因为InnoDB的工作方式是将数据库文件按页(16KB)读取到缓冲池,然后按照LRU算法来将数据保留在缓冲池的缓冲数据中。修改文件首先修改缓冲池的数据,然后将脏页刷新到磁盘。
由innodb_log_buffer_size
参数控制,用来存储重做日志。InnoDB存储引擎首先将redo日志信息先放入到这个缓冲区,然后按照一定频率将其刷新到redo日志。redo日志缓冲一般不需要设置的很大,因为一般情况下每秒会将redo日志刷到日志文件。默认redo缓冲区大小是8MB。三种进行redo日志刷盘的时机:
InnoDB对内存管理是通过Heap的方式进行的,在对一些数据结构本身的内存进行分配的时候,需要从额外的内存池中进行申请,当该区域的内存不够的时候会从缓冲池中进行申请。
插入buffer和数据页相同,都是物理页组成部分。InnoDB中主键是唯一标识符,插入聚焦索引一般是顺序的,不需要随机的磁盘读写。针对非聚焦索引的插入更新操作并不是每次直接插入到索引页,而是先判断插入的非聚焦索引是否在缓冲池中,如果在就直接插入,否则就先插入到插入buffer中,欺骗数据库已经将索引插入到叶子节点。然后再按照一定频率将其插入到实际的叶子节点。插入buffer的使用满足一下两点:
主要目标是实现应用redo日志时提高数据页的可靠性,redo日志是对页的物理操作,在应用redo日志的时候,需要创建的副本,当应用失败的时候,使用副本还原,这就是两次写机制。两次写分为两部分:
首先memcpy
函数将脏页复制到内存中的两次写缓冲中,然后通过每次1MB的写入共享表空间页的物理磁盘上,然后马上调用fsync
函数同步磁盘。有些文件系统本事就提供了部分写失败的方法机制,例如ZFS。
B+树的查找次数取决于B+树的高度,生产环境下一般在3-4层,因此需要3-4此查询。自适应Hash索引AHI通过缓冲池的B+树构造而来的,因此建立的速度很快,而且不需要对整张表构建Hash索引。InnoDB存储引擎会自动根据访问频率和模式自动为某些热点页建立AHI。建立AHI的要求是对这个页的连续访问模式必须是一样的,并且以该模式访问了100次,页通过该模式访问了N次,其中N=页中记录/16。
AHI数据数据库自优化的一种方式。
AIO的另一个优势是将多个IO合并为一个IO操作。InnoDB支持内核级别的AIO操作,成为Native AIO,但是需要libaio库的支持,Mac OS X没有提供InnoDB的AIO操作。启动Native AIO后,恢复速度提高75%。
当刷新一个脏页的时候,InnoDB会检查当前的页所在区的所有页,如果是脏页,那么一起进行刷新,通过AIO将多个IO合并。对于SSD建议关闭该特性。
错误日志:对MySQL进程进行记录
慢日志:通过long_query_time
进行设置,默认是10秒,默认不启动慢查询日志。
MySQL允许记录每分钟未使用索引的SQL语句。慢查询默认输出到文件,还可以将其保存到数据表中,默认是使用CSV引擎。
对于大数据量下的查询效率建议使用MyISAM存储引擎。
二进制日志:主要目标是恢复、复制、审计。默认情况下并没有启动。
默认情况下,二进制日志并不是每次写的时候同步到磁盘里,但数据库发生宕机的时候,可能最后一部分数据没有写到磁盘二进制日志文件。当设置sync_binlog=1
的时候会使用同步的方式写入。当在一个事务发出提交的时候,由于sync_binlog=1
,所以当事务没有发生提交,但是bin_log
已经被写入因此可以通过innodb_support_xa=1
来解决。bin_log
支持statement
和row
格式。通常设置为row
恢复速度更快,但是文件大小会变大。
redo日志:是物理日志,其记录时间点为缓冲中的页面修改完成,但还没有刷盘的时间点(事务提交之前,prepare阶段),即redo日志一定要比数据先到硬盘,聚集索引,次级索引,undo页面修改都需要记录redo日志,即可以把redo日志看成大管家,保证所有数据的完整性。
当前的事务数据库系统普遍采用了Write Ahead Log策略,也就是当事务提交的时候,先写入redo日志,在进行页修改。Check Point技术的目的是:
对于InnoDB引擎,redo日志,数据页和CheckPoint通过LSN标记版本,LSN是8字节的数字。CheckPoint分为两种:
对于Master Thread差不多以每秒或者每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回到磁盘,这个过程是异步的。Async/Sync Flush CheckPoint是为了保证重做日志的循环可使用性。这部分刷新操作由单独的Page Cleaner Thread实现,因此不会阻塞用户查询线程。
为什么MySQL不采用朴素的LRU算法?
Free列表:LRU列表用于管理已经读取的页,当存储引擎启动的时候LRU列表是空的,这时页存储到Free列表中。当需要从缓冲池中分页的时候,首先从Free列表中查找是否有可用的空闲页,如果有则将该页在Free列表删除,放到LRU列表中。
midpoint:在MySQL中,LRU加入了midpoint,新读取的页虽然是最新访问的页,但是并不直接放到LRU的头部,而是存储到LRU列表的midpoint位置。默认配置下,midpoint在LRU列表的5/8位置上,参数可调整。
如果直接将页放到LRU列表头部,那么某些SQL操作可能会使得缓冲池中的页面被刷新出去,从而影响缓冲池的效率。
InnoDB提供参数用于配置页被读取到midpoint位置后需要等待多久才会被加入到LRU列表的Hot端。
页压缩:InnoDB支持压缩页功能,对于非16KB的页,是通过unzip_LRU列表进行管理的。unzip_LRU是如何从缓冲池中分配内存的呢?在unzip_LRU列表中对不同大小的压缩页分别进行管理,首先检查是否有相同大小的LRU列表,如果没有就其拆分成为不同大小的页进行存储。
MySQL是一个单进程多线程架构的数据库。一个数据库实例在操作系统上表现为一个进程。在MySQL中存储引擎是基于表的。MySQL具有独有的插件式体系结构。
InnoDB存储引擎支持事务,主要面向OLTP应用,特点是行锁,支持外键,支持非锁定读,默认读取操作不会产生锁。
InnoDB是MySQL默认的存储引擎。InnoDB存储引擎的表单存放到一个独立的ibd文件中,InnoDB存储殷勤支持裸设备(原始空间,没有经过格式化的分区)建立表空间。
InnoDB使用多版本MVCC并发控制获得高性能,实现了SQL的4中隔离级别,默认是repeatable级别。使用next-key-locking策略避免幻读。
不支持事务,表锁设计,支持全文索引,主要面向OLAP应用。
缓冲池只缓存索引文件,数据文件由操作系统本身完成。
存储引擎有MYI和MYD组成,用于存放索引和数据。
NDB存储引擎:一种集群存储引擎,类似于Oracle的RAC集群,数据全部存储到内存。
Memory存储引擎:也叫做Heap存储引擎,数据全部存放内存,默认使用Hash索引,仅支持表锁,并发性能差,不支持Text和BLOB类型。存储varchar按照char定长方式,因此会浪费内存。一般会使用该引擎作为查询中间结果集。
Archive存储引擎:仅支持select和insert操作。支持索引,使用zlib算法将数据行进行压缩后存储,压缩比一般可达1:10。适合存储归档数据,事务不安全的,使用行锁实现高并发插入操作。
Federated存储引擎:不存放任何数据,指向远程的一台MySQL数据库服务器。类似于SQL Server的链接服务器或者Oracle的透明网关。
锁机制用户管理对共享资源的并发访问。lock的对象是事务,用来锁定的是数据库中的对象,lock是存在死锁机制的。加锁的基本单位是 next-key lock 。
InnoDB存储引擎会在行级别上对表数据上锁。
对于MyISAM引擎,其锁是表锁设计。
对于MS SQL Server 2005之前的版本都是页锁。
页锁容易实现,然而对于热点数据页的并发访问问题依然无法解决。到2005版本之后,MS SQL Server开始支持乐观并发和悲观并发,在乐观并发情况下开始支持行级锁。在这种情况下,行锁会升级到表锁。
InnoDB存储引擎锁的实现和Oracle数据库非常类似。
latch是一种轻量级的锁,因为其要求锁定的时间必须非常短。在InnoDB存储引擎中,latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保障并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。
InnoDB存储引擎实现了如下的两种标准的行级锁:
意向锁:因为读取并没有改变行的数据,这种情况叫做锁兼容。此外,InnoDB支持多粒度锁定,这种锁定允许事务在行级别上的锁和表级别上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式就是I意向锁。意向锁是将锁定的对象分为多层,意向锁意味着事务希望在更细的粒度上进行加锁。如果上锁的对象看成一颗树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁。那么首先需要对粗粒度的对象上锁。
由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除去全表扫描以外的任何请求。
是指InnoDB存储引擎通过多版本的方式来读取当前执行时间数据库中的数据。如果读取的行正在执行DELETE和UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反的,InnoDB存储引擎会去读取行的一个快照数据。非锁定读机制极大的提高了数据库的并发性,这时默认的读取方式,即使读取不会占用和等待表上的锁。并不是每个事务隔离级别下都是采用非锁定的一致性读取。一个行记录可能有不止一个快照数据,这样的技术叫做MVCC,多版本并发控制。
对每个含有自增长值的表都有一个自增长计数器,当对含有自增长的计数器的表进行插入操作,这个计数器会被初始化,这个实现方式是通过AUTO-INC locking锁机制实现的,它是一种表锁,为了提高插入性能,锁并不是在一个事务执行完成后释放的,而是在完成时对自增长值插入的SQL语句后立即释放的。
对于带有自增长值的列的并发插入性能较差,事务必须等待前一个插入操作执行完成。InnoDB存储引擎提供了一个参数innodb_automic_lock_mode
来控制自增长的模式。该参数的默认值是1,表示使用互斥量对内部存在的计数器进行累加操作。如果是0,表示AUTO-INC locking的方式进行,如果是2表示对于所有的插入操作都使用互斥量的方式进行。
MyISAM存储引擎是表锁设计,因此自增长不用考虑并发插入的问题。
Record Lock:单行记录上的锁。总是锁住索引记录,没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键进行锁定。
Gap Lock:间隙锁,锁定一个范围,但是不包含记录本身。
Next-Key Lock:Gap Lock+Record Lock,锁定一个前开后闭的范围,并且锁定记录本身。
InnoDB存储引擎对于行的查询都是采用这种锁定算法。其设计目的是为了解决Phantom Problem,而利用这种锁定技术,锁定的不是单个值,而是一个范围,是谓词锁的一种改进。
当查询的索引含有唯一索引时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降为Record Lock,即近锁住索引本身,而不是范围。唯一索引上的范围查询会访问到不满足条件的第一个值为止。
当在索引上进行等值查询,向右遍历且最后一个值不满足条件的时候,退化为 Gap Lock。
在默认事务隔离级别(repeatable read)情况下,InnoDB采用Next-Key Locking解决Phantom Problem幻影问题。
幻影问题是指在同一个事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的值。
死锁:在两个及其以上的事务在执行过程中,因为争夺资源而造成的一种互相等待的现象。
在InnoDB存储引擎中,参数innodb_lock_wait_timeout
用来设置超时的时间。当前数据库普遍采用wait-for-graph等待图的方式来进行死锁检测。等待图要求保存锁的信息链表和事务的等待链表。等待图机制采用深度优先的算法来实现,在InnoDB1.2之前的版本中都采用递归的方式实现。
B+ 树是平衡树的一种,平衡树是一颗查找树,并且所有叶子节点位于同一层。B+树是基于B树和叶子节点的顺序访问指针实现的。在B+树中,节点的Key从左到右非递减排列。
当进行查找的时候首先在根节点进行二分查找,找到Key所在的指针,然后递归的在指针指向的节点中查找,直到找到叶子节点,然后再从叶子节点上进行查找,找出Key对应的数据。
由于对B+树的写入操作会影响平衡性,因此在写入操作之后会进行树的分裂与合并操作。
文件系统普遍采用B+树作为索引结构的原因如下:
在计算机里,无论是内存还是磁盘,操作系统都是按页的大小进行读取的(页大小通常为 4 kb),磁盘每次读取都会预读,会提前将连续的数据读入内存中,这样就避免了多次 IO,这就是计算机中有名的局部性原理,即我用到一块数据,很大可能这块数据附近的数据也会被用到,干脆一起加载,省得多次 IO 拖慢速度, 这个连续数据有多大呢,必须是操作系统页大小的整数倍,这个连续数据就是 MySQL 的页,默认值为 16 KB,也就是说对于 B+ 树的节点,最好设置成页的大小(16 KB),这样一个 B+ 树上的节点就只会有一次 IO 读。
页大小并不是越大越好,InnoDB 是通过内存中的缓存池(pool buffer)来管理从磁盘中读取的页数据的。页太大的话,很快就把这个缓存池撑满了,可能会造成页在内存与磁盘间频繁换入换出,影响性能。
页分裂与页合并:
B+ 树为了维护索引的有序性,每插入或更新一条记录的时候,会对索引进行更新。
这种由于页分裂造成的调整必然导致性能的下降,尤其是以身份证作为主键的话,由于身份证的随机性,必然造成大量的随机结点中的插入,进而造成大量的页分裂,进而造成性能的急剧下降,那如果是以自增 id 作为主键呢,由于新插入的表中生成的 id 比索引中所有的值都大,所以它要么合到已存在的节点(元素个数未满)中,要么放入新建的节点中(如下图示)所以如果是以自增 id 作为主键,就不存在页分裂的问题了。
有页分裂就必然有页合并,什么时候会发生页合并呢,当删除表记录的时候,索引也要删除,此时就有可能发生页合。
综上所述,B+树有以下特点:
AVL树:
平衡因子(Balance Factor):某结点的左右子树的高度差。
B树:
B 树(Balanced Tree)是一种平衡的多路搜索树,多用于文件系统、数据库的实现。
RB树:
为了保证平衡,红黑树必须满足以下性质:
对于每个结点,从该点至 nil(树尾端,Java 中为 null 的结点)的任何路径都包含所相同个数的黑色结点
基本平衡操作:
B+树索引
大多数MySQL的存储引擎都是用的该索引类型。不需要全表扫描,只需要对数进行搜索。B+数是有序的,利于排序和分组。可以指定多个列作为索引列,多个索引共同组成Key。
适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。
InnoDB 的 B+Tree 索引分为主索引和辅助索引。
哈希索引
能以 O(1) 时间进行查找,但是失去了有序性,因此无法用于排序与分组,只支持精确查找,无法用于部分查找和范围查找。
InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
全文索引
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。查找条件使用 MATCH AGAINST
,而不是普通的 WHERE。全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
空间数据索引
MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。必须使用 GIS 相关的函数来维护数据。
TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间。INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。
FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。
FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。
主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。
VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。
在进行存储和检索时,会保留 VARCHAR 末尾的空格,而会删除 CHAR 末尾的空格。
MySQL 提供了两种相似的日期时间类型:DATETIME 和 TIMESTAMP
DATETIME
时间戳
FROM_UNIXTIME()
函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP()
函数把日期转换为 UNIX 时间戳。默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。
Hot Backup是指数据库运行中的直接备份,这在MySQL中也叫Online Backup。日志备份主要是指对bin log的备份,通过对一个完全备份的bin log的重做来完成数据库的恢复工作。MySQL数据库复制的原理就是异步实时的将bin log重做传送并应用到数据库。
对于mysqldump工具,可以通过--single-transaction选项获得InnoDB存储引擎的一致性备份。
mysqlimport是MySQL数据库提供的一个命令行程序,本质上是LOAD DATA INFILE的命令接口。
ibbackup是InnoDB存储引擎提供的热备工具,可以同时备份MyISAM和InnoDB。
快照备份:MySQL本身不支持快照备份功能,因此需要通过文件系统支持,Solaris支持的ZFS和Linux支持的LVM可以实现。
LVM使用写时复制的技术来创建快照,当创建一个快照的时候,仅仅复制原始卷中的元数据,并不会进行物理操作。快照创建以后,原始卷上的写操作会跟踪原始卷块的改变,将需要改变的数据在改变之前复制到快照预留的空间中。如果要读取的是已经修改过的数据,则将读取保存在快照中的原始卷上改变之前的数据。
首先写redo日志,然后修改内存,此时数据变成脏数据,等待一段时间刷盘。
问题的根源在于大量的死锁检测与CPU负载过高。
一种就是关闭死锁检测,但是关闭死锁检测可能出现大量的超时,是有损业务的。
innodb读取数据的时候是按照页进行的,innodb的页大小默认是16kb。
因此按照页来读取的方式在查找的过程中是在内存中完成的。但是对于更新操作,当进行更新时,如果数据不在内存中,那么将会把修改写入到change buffer中,除去访问数据页会发生change buffer的merge操作,系统后台线程也会定期刷新change buffer,并且在关机的时候也会merge。
由于唯一索引必须读取数据页进行唯一性约束判断,因此不需要change buffer。
由于MySQL的优化器主要负责挑选合适的索引,优化器会根据扫描行数目、是否使用临时表、是否排序进行综合判断。对于上述操作MySQL能做到的只是估算。
一个索引上不同的值的个数叫做索引的区分度,也叫做基数。MySQL通过统计采样的方式获取基数。采样的时候会随机选取N个页,同级这些页面上不同的值,得到一个平均值,然后乘上这个索引的页面个数,就会得到基数。
直接创建索引,这样会比较占用空间。
创建前缀索引,节省空间,但是会增加查询扫描次数,并且不能使用覆盖索引。
InnoDB在后台刷新脏页占用了内存和IO资源,查询语句可能正好需要一个脏页或者更新语句的IO资源被脏页刷新程序占用,就会造成业务端感知MySQL的抖动。应该合理设置innodb_io_capacity的值,脏页比例不要超过75%。
MySQL的数据分为表结构定义和数据,在MySQL8.0之前会将表结构的定义存放到frm文件中,之后存放到系统数据表中。
innodb_file_per_table
控制。在MySQL5.6之后默认是开启的。alter table 表名 engine=InnoDB
实现,中间用到的临时表InnoDB会自动创建,交换表名,删除旧表。count(x)
会慢?对于statement格式的binlog,最后会有一个COMMIT
与数据和备份的一致性有关,如果在某一时刻binlog写完以后MySQL发生崩溃,这时候binlog已经写入,之后就会被从库使用。所以在主库也要提交这个事务。
redo日志并没有记录数据页的完整数据,所以并没有能力去更新磁盘上的数据。
当一个事务向表中插入多条记录的时候,每插入一条都会写在redo log buffer中
order by用于排序操作,InnoDB中排序分为全字段排序和rowid排序。
在全字段排序中,MySQL会为每一个线程分配一块空间用于排序,当数据量太大的时候就会利用磁盘临时文件辅助排序。MySQL将需要排序的数据分成多份,每一份单独排序之后将存储在临时文件中的排序结果合并为一个大文件。全字段排序的性能取决于返回的字段数量。
对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。
如果结果集长时间没有返回可能是在等待MDL锁,也有可能是等待Flush或者行锁。
一个事务的binlog是连续的,因此要整个事务完成之后一起写到文件里
如果这个时候binlog也在binlog cache中,redo log也在redo log buffer中,那么崩溃之后数据是一致的。
由于statement格式的SQL可能会导致数据不一致,而ROW格式的数据很占用空间。
规定两个数据库的server id必须不同,如果相同,那么不能是互为主备关系。一个备库接收到binlog并在重放的过程中生成与原binlog相同的server id。
(1)对于要求一定及时返回新数据的请求可以强制走主库
(2)Sleep方案,执行select sleep(1)
,等待延迟的同步,但是更好的方法是直接客户端数据进行客户端填充,不发送查询请求。
(3)判断主备无延迟方案,通过show slave status
的结果中判断seconds_behind_master
参数是否等于0
(4)配合 semi-sync 方案,半同步复制,semi-sync通过offset的方式解决了崩溃问题,但是只是针对一对一的方式是成立的。在一主多从的情况下,主库只要接收到一个ack就立即响应客户端。
(5)等主库位点方案,通过在从库执行select master_pos_wait(file, pos, timeout)
,返回一个正整数,表示应用完file和pos位置的binlog执行了多少事务。如果是返回NULL表示从库同步线程出现异常,如果超时,那么返回-1。
(6)等 GTID 方案,通过执行select wait_for_executed_gtid_set(gtid_set, 1);
导致等待,直到这个库执行的事务中包含传入的GTID才返回0,超时就返回1。
MySQL中的kill有kill query+线程ID和kill connection+线程ID。与Linux相同,MySQL中kill的意思是发出停止信号而不是立即停止。
内存表,指的是使用 Memory 引擎的表,建表语法是create table … engine=memory
。这种表的数据都保存在内存里,系统重启的时候会被清空,但是表结构还在。
而临时表,可以使用各种引擎类型 。如果是使用 InnoDB 引擎或者 MyISAM 引擎的临时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用 Memory 引擎。
不同 session 的临时表是可以重名的,如果有多个 session 同时执行 join 优化,不需要担心表名重复导致建表失败的问题。由于不用担心线程之间的重名冲突,临时表经常会被用在复杂查询的优化过程中。
其中,分库分表系统的跨库查询就是一个典型的使用场景。 frm 文件放在临时文件目录下,文件名的后缀是 .frm ,前缀是#sql{ 进程 id}_{ 线程 id}_序列号
。你可以使用select @@tmpdir
命令,来显示实例的临时文件目录。
.ibd
为后缀的文件,用来存放数据文件,而从 5.7 版本开始, MySQL 引入了一个临时文件表空间,专门用来存放临时文件的数据。因此,我们就不需要再创建 ibd 文件了。MyISAM 引擎的自增值保存在数据文件中。
InnoDB 引擎的自增值,其实是保存在了内存里,并且到了 MySQL 8.0 版本后,才有了 “ 自增值持久化 ” 的能力。
MySQL5.7之前每次重启之后,都会去找到自增值的最大值。然后加一作为当前的自增值。
在实际应用场景中,MySQL复制90%以上都是一个Master复制到一个或者多个Slave的架构模式,主要用于读压力比较大的应用的数据库端廉价扩展解决方案。因为只要Master和Slave的压力不是太大(尤其是Slave端压力)的话,异步复制的延时一般都很少很少。尤其是自从Slave端的复制方式改成两个线程处理之后,更是减小了Slave端的延时问题。
当slave增加到一定数量时,slave对master的负载以及网络带宽都会成为一个严重的问题
这种结构虽然简单,但是,它却非常灵活,足够满足大多数应用需求。一些建议:
(1) 不同的slave扮演不同的作用(例如使用不同的索引,或者不同的存储引擎);
(2) 用一个slave作为备用master,只进行复制;
(3) 用一个远程的slave,用于灾难恢复;
这样搭建复制环境之后,难道不会造成两台MySQL之间的循环复制么?实际上MySQL自己早就想到了这一点,所以在MySQL的BinaryLog中记录了当前MySQL的server-id,而且这个参数也是我们搭建MySQLReplication的时候必须明确指定,而且Master和Slave的server-id参数值比需要不一致才能使MySQLReplication搭建成功。一旦有了server-id的值之后,MySQL就很容易判断某个变更是从哪一个MySQLServer最初产生的,所以就很容易避免出现循环复制的情况。
主动的Master-Master复制有一些特殊的用处。例如,地理上分布的两个部分都需要自己的可写的数据副本。这种结构最大的问题就是更新冲突。假设一个表只有一行(一列)的数据,其值为1,如果两个服务器分别同时执行如下语句:
在第一个服务器上执行:
UPDATE tbl SET col=col + 1;
在第二个服务器上执行:
UPDATE tbl SET col=col * 2;
那么结果是多少呢?一台服务器是4,另一个服务器是3,但是,这并不会产生错误。
实际上,MySQL并不支持其它一些DBMS支持的多主服务器复制(Multimaster Replication),这是MySQL的复制功能很大的一个限制(多主服务器的难点在于解决更新冲突),但是,如果你实在有这种需求,你可以采用MySQL Cluster,以及将Cluster和Replication结合起来,可以建立强大的高性能的数据库平台。但是,可以通过其它一些方式来模拟这种多主服务器的复制。
这是master-master结构变化而来的,它避免了M-M的缺点,实际上,这是一种具有容错和高可用性的系统。它的不同点在于其中一个服务只能进行只读操作。
在有些应用场景中,可能读写压力差别比较大,读压力特别的大,一个Master可能需要上10台甚至更多的Slave才能够支撑注读的压力。这时候,Master就会比较吃力了,因为仅仅连上来的SlaveIO线程就比较多了,这样写的压力稍微大一点的时候,Master端因为复制就会消耗较多的资源,很容易造成复制的延时。
遇到这种情况如何解决呢?这时候我们就可以利用MySQL可以在Slave端记录复制所产生变更的BinaryLog信息的功能,也就是打开—log-slave-update选项。然后,通过二级(或者是更多级别)复制来减少Master端因为复制所带来的压力。也就是说,我们首先通过少数几台MySQL从Master来进行复制,这几台机器我们姑且称之为第一级Slave集群,然后其他的Slave再从第一级Slave集群来进行复制。从第一级Slave进行复制的Slave,我称之为第二级Slave集群。如果有需要,我们可以继续往下增加更多层次的复制。这样,我们很容易就控制了每一台MySQL上面所附属Slave的数量。这种架构我称之为Master-Slaves-Slaves架构
这种多层级联复制的架构,很容易就解决了Master端因为附属Slave太多而成为瓶颈的风险,当然,如果条件允许,我更倾向于建议大家通过拆分成多个Replication集群来解决
上述瓶颈问题。毕竟Slave并没有减少写的量,所有Slave实际上仍然还是应用了所有的数据变更操作,没有减少任何写IO。相反,Slave越多,整个集群的写IO总量也就会越多,我们没有非常明显的感觉,仅仅只是因为分散到了多台机器上面,所以不是很容易表现出来。
此外,增加复制的级联层次,同一个变更传到最底层的Slave所需要经过的MySQL也会更多,同样可能造成延时较长的风险。
而如果我们通过分拆集群的方式来解决的话,可能就会要好很多了,分拆集群也需要更复杂的技术和更复杂的应用系统架构。
这种结构的优点就是提供了冗余。在地理上分布的复制结构,它不存在单一节点故障问题,而且还可以将读密集型的请求放到slave上。
级联复制在一定程度上面确实解决了Master因为所附属的Slave过多而成为瓶颈的问题,但是他并不能解决人工维护和出现异常需要切换后可能存在重新搭建Replication的问题。这样就很自然的引申出了DualMaster与级联复制结合的Replication架构,我称之为Master-Master-Slaves架构
和Master-Slaves-Slaves架构相比,区别仅仅只是将第一级Slave集群换成了一台单独的Master,作为备用Master,然后再从这个备用的Master进行复制到一个Slave集群。
这种DualMaster与级联复制结合的架构,最大的好处就是既可以避免主Master的写入操作不会受到Slave集群的复制所带来的影响,同时主Master需要切换的时候也基本上不会出现重搭Replication的情况。但是,这个架构也有一个弊端,那就是备用的Master有可能成为瓶颈,因为如果后面的Slave集群比较大的话,备用Master可能会因为过多的SlaveIO线程请求而成为瓶颈。当然,该备用Master不提供任何的读服务的时候,瓶颈出现的可能性并不是特别高,如果出现瓶颈,也可以在备用Master后面再次进行级联复制,架设多层Slave集群。当然,级联复制的级别越多,Slave集群可能出现的数据延时也会更为明显,所以考虑使用多层级联复制之前,也需要评估数据延时对应用系统的影响。
主从模式原理:
分为同步复制和异步复制,实际复制架构中大部分为异步复制。 复制的基本过程如下:
Slave上面的IO进程连接上Master,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容;
Slave的IO进程接收到信息后,将接收到的日志内容依次添加到Slave端的relay-log文件的最末端,并将读取到的Master端的 bin-log的文件名和位置记录到master-info文件中,以便在下一次读取的时候能够清楚的告诉Master“我需要从某个bin-log的哪个位置开始往后的日志内容,请发给我”;
mysql replication单表或多表复制时需注意的几个问题:
主库和从库的数据库名必须相同;
主库和从库的复制可以精确到表,但是在需要更改主库或从库的数据结构时需要立刻重启slave;
不能在mysql配置文件里直接写入master的配置信息,需要用change master命令来完成;
指定replicate_do_db必须在my.cnf里配置,不能用change master命令来完成;
MMM(Master-Master replication managerfor Mysql,Mysql主主复制管理器)是一套灵活的脚本程序,基于perl实现,用来对mysql replication进行监控和故障迁移,并能管理mysql Master-Master复制的配置(同一时间只有一个节点是可写的)。
mysql-mmm的监管端会提供多个虚拟IP(VIP),包括一个可写VIP,多个可读VIP,通过监管的管理,这些IP会绑定在可用mysql之上,当某一台mysql宕机时,监管会将VIP迁移至其他mysql。
在整个监管过程中,需要在mysql中添加相关授权用户,以便让mysql可以支持监理机的维护。授权的用户包括一个mmm_monitor用户和一个mmm_agent用户,如果想使用mmm的备份工具则还要添加一个mmm_tools用户。
MMM基于MySQL Replication做的扩展架构,主要用来监控mysql主主复制并做失败转移。其原理是将真实数据库节点的IP(RIP)映射为虚拟IP(VIP)集,在这个虚拟的IP集中,有一个专用于write的IP,多个用于read的IP,这个用于Write的VIP映射着。
数据库集群中的两台master的真实IP(RIP),以此来实现Failover的切换,其他read的VIP可以用来均衡读。
MHA(Master High Availability)由日本DeNA公司youshimaton(现就职于Facebook公司)开发,是一套优秀的作为MySQL高可用性环境下故障切换和主从提升的高可用软件。在MySQL故障切换过程中,MHA能做到在0~30秒之内自动完成数据库的故障切换操作,并且在进行故障切换的过程中,MHA能在最大程度上保证数据的一致性,以达到真正意义上的高可用。某种意义上来说MHA架构是MMM架构的升级版,但是又缺少了vip的功能,一般会配合keepalived使用补完vip的功能。
优点:
缺点:
最大的问题做读写分离时需要改动代码,与开发耦合太高,不利于当前部署以及后期改造。
基于阿里开源的Cobar产品而研发,Cobar的稳定性、可靠性、优秀的架构和性能以及众多成熟的使用案例使得MYCAT一开始就拥有一个很好的起点。一个彻底开源的,面向企业应用开发的大数据库集群。
支持的功能列表:
优点:
缺点:
TPS性能低下
Mycat也并不是配置以后,就能完全解决分表分库和读写分离问题。Mycat配合数据库本身的复制功能,可以解决读写分离的问题,但是针对分表分库的问题,不是完美的解决。或者说,至今为止,业界没有完美的解决方案。
分表分库写入能完美解决,但是,不能完美解决主要是联表查询的问题,Mycat支持两个表联表的查询,多余两个表的查询不支持。 其实,很多数据库中间件关于分表分库后查询的问题,都是需要自己实现的,而且节本都不支持联表查询,Mycat已经算做地非常先进了。分表分库的后联表查询问题,需要通过合理数据库设计来避免。这是一套比较好的数据库集群方案,值得考虑。
MySQL InnoDB集群为MySQL提供了完整的高可用性解决方案。 MySQL Shell包含AdminAPI,使您可以轻松配置和管理一组至少三个MySQL服务器实例,以充当InnoDB集群。 每个MySQL服务器实例都运行MySQL Group Replication,它提供了在InnoDB集群内复制数据的机制,具有内置故障转移功能。Admin API无需在InnoDB集群中直接使用组复制。 MySQL Shell可以根据您部署的集群自动配置自身,将客户端应用程序透明地连接到服务器实例。如果服务器实例意外故障,群集将自动重新配置。在默认的单主模式下,InnoDB集群具有单个读写服务器实例 - 主要实例。多个辅助服务器实例是主要副本的副本。如果主服务器出现故障,则辅助服务器将自动升级为主服务器。MySQL路由器检测到此情况并将客户端应用程序转发到新主服务器。高级用户还可以将群集配置为多主结构。
一般采用MySQL Router、Cluster和MySQL Shell构成的Mysql InnoDB Cluster高可用方案进行搭建。InnoDB Cluster支持自动Failover、强一致性、读写分离、读库高可用、读请求负载均衡,横向扩展的特性,是比较完备的一套方案。但是部署起来复杂,想要解决Router单点问题好需要新增组件,如没有其他更好的方案暂考虑该方案。
Galera Cluster是由Codership开发的MySQL多主集群,包含在MariaDB中,同时支持Percona xtradb、MySQL,是一个易于使用的高可用解决方案,在数据完整性、可扩展性及高性能方面都有可接受的表现。图1所示为一个三节点Galera 集群,三个MySQL实例是对等的,互为主从,这被称为多主(multi-master)架构。当客户端读写数据时,可连接任一MySQL实例。对于读操作,从每个节点读取到的数据都是相同的。对于写操作,当数据写入某一节点后,集群会将其同步到其它节点。这种架构不共享任何数据,是一种高冗余架构。
Galera集群具有以下特点:
Galera集群复制要求数据库系统支持事务,因此仅支持MySQL的Innodb存储引擎,并且多主模式下只能使用可重复读隔离级别。
主从同步:
不同于MySQL原生的主从异步复制,Galera采用的是多主同步复制。
异步复制中,主库将数据更新传播给从库后立即提交事务,而不论从库是否成功读取或重放数据变化。这种情况下,在主库事务提交后的短时间内,主从库数据并不一致。同步复制时,主库的单个更新事务需要在所有从库上同步更新。换句话说,当主库提交事务时,集群中所有节点的数据保持一致。
相对于异步复制,同步复制的优点主要体现在以下几方面:
当然,同步复制的缺点也显而易见,这主要源于其实现方式。同步复制协议通常使用两阶段提交或分布式锁协调不同节点的操作。假设集群有n个节点,每秒处理o个操作,每个操作中包含t个事务,则每秒将在网络中产生n * o * t
条消息。这意味着随着节点数量的增加,事务冲突和死锁的概率将呈指数级增加。这也是MySQL缺省使用异步复制的主要原因。
为解决传统同步复制的问题,现已提出多种数据库同步复制的替代方法。除理论外,一些原型实现也显示出了很大的希望,如以下重要改进:
Galera集群就是基于这些方法构建的。可以看到Galera复制的原理与实现与MySQL组复制有很多相似之处。为了更好地理解Galera,在深入细节之前,先将它和MySQL组复制作一类比,如下表所示。
对比项 | Galera | MySQL Group Replication |
---|---|---|
组通信系统(Group Communication System) | 专有组通信系统GComm,所有节点都必须有 ACK 消息 | 基于 Paxos,只要求大多数节点有 ACK 消息 |
二进制日志(Binlog) | 不需要二进制日志,将二进制行事件写入Gcache | 需要二进制日志 |
节点配置(Node Provisioning) | 自动全量同步(State Snapshot Transfer,SST)与增量同步(Incremental State Transfer,IST) | 没有自动全量同步,使用异步复制通道 |
全局事务ID(GTID) | 使用状态UUID和递增序列号 | 依赖GTID,集群上的写操作产生GTID事件 |
分区控制(Partition Handling) | 分区节点拒绝读写,自动恢复并重新加入集群 | 分区节点可读,接受写请求但将永久挂起,需要手工重新加入集群 |
流控(Flow Control) | 当一个节点慢到一个限制值,阻止所有节点写 | 每个节点都有所有成员的统计信息,独立决定该节点写的阈值。如果有节点慢到阈值,其它节点放慢写速度。 |
DDL支持 | 总序隔离(Total Order Isolation,TOI),DDL执行期间,所有写入都将被阻止 | DDL 并不会阻塞写,仅建议在单主模式下使用(因为 DDL 并没有冲突检测) |
复制架构:
同步复制系统中的节点将通过单个事务更新副本,从而与所有其它节点同步。这意味着当事务提交时,所有节点都将具有相同的值。此过程通过组通信使用写集复制进行。
Galera集群的内部架构包含四个组件:
数据库管理系统(DBMS):在单个节点上运行的数据库服务器。Galera群集可以使用MySQL、Mariadb或Percona xtradb。
wsrep api:Galera与数据库服务器的接口,为上层提供了丰富的状态信息和回调函数。wsrep api由wsrep hooks、dlopen函数两部分组成。wsrep hooks钩子程序用于与数据库服务器引擎集成。dlopen函数使Galera插件中的复制程序对wsrep hooks可用。
wsrep api是数据库的通用复制插件接口,定义了一组应用程序回调和复制插件调用函数。wsrep api将数据库中的数据改变视为一种状态变化,当客户端修改数据库内容时,其状态将更改。wsrep api将数据库状态更改表示为一系列事务。集群中的所有节点始终具有相同状态,它们通过以相同的顺序复制和应用状态更改来相互同步。从更技术角度看,Galera集群使用以下方式处理状态更改:
Galera复制插件:实现写集复制功能的核心模块
全局事务ID(global transaction id,GTID)
在MySQL社区中,GTID的概念并不新鲜,MySQL中的GTID由Master生成,是用于标记唯一事务并通过ID定位binlog位置的一种手段,从而有效解决了级联复制等场景中的各种问题。
对Galera Cluster而言,复制不基于binlog,而是通过Galera复制插件来保障。Galera的GTID同样也标记事务唯一性,wsrep api使用GTID识别状态更改。
GTID由两部分组成:
Galera复制插件
Galera复制插件实现wsrep api,作为wsrep provider运行。Galera复制插件由以下组件构成:
组通信插件
组通信框架为各种gcomm系统提供了一个插件体系结构。Galera集群建立在专有的组通信系统层之上,实现虚拟同步。所谓虚拟同步,简单说是指一个事务在一个节点上执行成功后,保证它在其它节点也一定会被成功执行,但并不能保证实时同步。为了解决实时性问题,Galera集群实现了自己的运行时可配置的时态流控。
组通信框架还使用GTID提供来自多个源的消息总序(Total Order)。在传输层上,Galera集群是一个对称的无向图,所有节点都通过TCP相互连接。默认情况下,TCP用于消息复制和群集成员资格服务,但也可以使用udp多播在LAN中进行复制。
Galera复制工作原理
Galera复制是一种基于验证的复制,以这两篇论文为理论基础:Don’t be lazy, be consistent 和 Database State Machine Approach。基于验证的复制使用组通信和事务排序技术实现同步复制。它通过广播并发事务之间建立的全局总序来协调事务提交。简单说就是事务必须以相同的顺序应用于所有实例。事务在本节点乐观执行,然后在提交时运行一个验证过程以保证全局数据一致性。所谓乐观执行是指,事务在一个节点提交时,被认为与其它节点上的事务没有冲突,首先在本地执行,然后再发送到所有节点做冲突检测,无冲突时在所有节点提交,否则在所有节点回滚。
当客户端发出commit命令时,在实际提交之前,对数据库所做的更改都将被收集到一个写集中,写集中包含事务信息和所更改行的主键。然后,数据库将此写集发送到所有其它节点。节点用写集中的主键与当前节点中未完成事务的所有写集(不仅包括当前节点其它事务产生的写集,还包括其它节点传送过来的写集)的主键相比较,确定节点是否可以提交事务。同时满足以下三个条件则验证失败(存在冲突):
验证失败后,节点将删除写集,集群将回滚原始事务。对于所有的节点都是如此,每个节点单独进行验证。因为所有节点都以相同的顺序接收事务,它们对事务的结果都会做出相同的决定,要么全成功,要么都失败。成功后自然就提交了,所有的节点又会重新达到数据一致的状态。节点之间不交换“是否冲突”的信息,各个节点独立异步处理事务。由此可见,Galera本身的数据也不是严格同步的,很明显在每个节点上的验证是异步的,这也就是前面提到的“虚拟同步”。
最后,启动事务的节点可以通知客户端应用程序是否提交了事务。
状态转移
当一个新节点加入集群时,数据将从集群复制到这个节点,这是一个全自动的过程,Galera将此称为状态转移。前面介绍Galera架构时曾提到,wsrep api将集群中的数据改变视为状态改变,因此这里将数据同步称作状态转移也就不足为怪了。Galera集群中有两种状态转移方法:
当有新节点加入时,集群会选择出一个捐献者(Donor)节点为新节点提供数据,这点与MySQL组复制类似。
状态快照传输:
增量状态转移:
增量状态转移(IST)只向新节点发送它所缺失的事务。使用IST需要满足两个先决条件:
满足这些条件时,捐助节点单独传输缺失的事务,并按顺序重放它们,直到新节点赶上集群。例如,假设集群中有一个节点落后于集群。
集群上的捐助节点从加入节点接收状态转移请求。它检查自身写集缓存中的序列号197223。如果该序号在写集缓存中不可用,则会启动SST。否则捐助节点将从197223到201913的提交事务发送到新加入节点。增量状态传输的优点是可以显著加快节点合并到集群的速度。另外,这个过程对捐赠者来说是非阻塞的。
增量状态传输最重要的参数是捐助节点上的gcache.size,它控制分配多少系统内存用于缓存写集。可用空间越大,可以存储的写集越多。可以存储的写集越多,通过增量状态传输可以弥合的事务间隙就越大。另一方面,如果写集缓存远大于数据库大小,则增量状态传输开始时的效率低于发送状态快照。
写集缓存(gcache)
Galera群集将写集存储在一个称为gcache的特殊缓存中。gcache使用三种类型的存储:
Galera集群使用一种分配算法,尝试按上述顺序存储写集。也就是说,它首先尝试使用永久内存存储,如果没有足够的空间用于写入集,它将尝试存储到永久环缓冲区文件。除非写入集大于可用磁盘空间,否则页面存储始终成功。
注意,如果gcache.recover参数设置为yes,则在启动时将尝试恢复gcache,以便该节点可以继续向其它节点提供IST服务。如果设置为no(缺省),gcache将在启动时失效,节点将只能为SST提供服务。
流控:
Galera集群内部使用一种称为流控的反馈机制来管理复制过程。流控允许节点根据需要暂停和恢复复制,这可以有效防止任一节点在应用事务时落后其它节点太多。
从Galera集群同步复制(虚拟同步)原理可知,事务的应用和提交在各个节点上异步发生。节点从集群接收但尚未应用和提交的事务将保留在接收队列中。由于不同节点之间执行事务的速度不一样,慢节点的接收队列会越积越长。当接收队列达到一定大小时,节点触发流控,作用就是协调各个节点,保证所有节点执行事务的速度大于队列增长速度。流控的实现原理很简单:整个Galera集群中,同时只有一个节点可以广播消息,每个节点都会获得广播消息的机会(获得机会后也可以不广播)。当慢节点的接收队列超过一定长度后,它会广播一个FC_PAUSE消息,所有节点收到消息后都会暂缓广播消息,直到该慢节点的接收队列长度减小到一定长度后再恢复复制。
gcs.fc_limit
:接收队列中积压事务的数量超过该值时,流控被触发,缺省值为16。对于Master-Slave模式(只在一个节点写)的Galera集群,可以配置一个较大的值,防止主从复制延迟。对启动多写的Galera集群,较小的值比较合适,因为较大的接收队列长度意味着更多冲突。gcs.fc_factor
:当接收队列长度开始小于 gcs.fc_factor * gcs.fc_limit
时恢复复制,缺省值为1。gcs.fc_master_slave
:Galera集群是否为Master-Slave模式,缺省为no。节点状态:
一个节点在Galera集群中可能经历的节点状态有Open、Primary、Joiner、Joined、Synced、Donor。可以通过wsrep_local_state和wsrep_local_state_comment系统变量查看节点的当前状态。
节点状态和流控:
Galera集群根据节点状态实现多种形式的流控以保证数据一致性。有四种主要流控类型:
单节点故障与恢复:
当一个节点因为硬件、软件、网络等诸多原因与集群失去联系时,都被概括为节点故障。从集群的角度看,主组件看不到出问题的节点,它将会认为该节点失败。从故障节点本身的角度来看,假设它没有崩溃,那么唯一的迹象是它失去了与主组件的连接。可以通过轮询wsrep_local_state状态变量监控Galera群集节点的状态,值及其含义见上节流控中的描述。
集群检查从节点最后一次接收到数据包的时间确定该节点是否连接到集群,检查的频率由evs.inactive_check_period参数指定,缺省值为每隔0.5秒检查一次。在检查期间,如果群集发现自上次从节点接收网络数据包以来的时间大于evs.keepalive_period参数的值(缺省值为1秒),则它将开始发出心跳信号。如果集群在evs.suspect_timeout参数(缺省值为5秒)期间没有继续从节点接收到网络数据包,则该节点被声明为suspect,表示怀疑该节点已下线。一旦主组件的所有成员都将该节点视为可疑节点,它就被声明为inactive,即节点失败。如果在大于evs.inactive_timeout(缺省值为15秒)的时间内未从节点接收到消息,则无论意见是否一致,都会声明该节点失败。在所有成员同意其成员资格之前,失败节点将保持非操作状态。如果成员无法就节点的活跃性达成一致,说明网络对于集群操作来说太不稳定。
这些选项值之间的关系为:
evs.inactive_check_period <= evs.keepalive_period <= evs.suspect_timeout <= evs.inactive_timeout
需要注意,如果网络过于繁忙,以至于无法按时发送消息或心跳信号无响应,也可能被宣布为节点失败,这可以防止集群其余部分的操作被锁。如果不希望这样处理,可以增加超时参数。如果用CAP原则来衡量,Galera集群强调的是数据一致性(Consistency),这就导致了集群需要在可用性(Availability)和分区容忍性(Partition tolerance)之间进行权衡。也就是说,当使用的网络不稳定时,低evs.suspect_timeout和evs.inactive_timeout值可能会导致错误的节点故障检测结果,而这些参数的较高值可能会导致在实际节点故障的情况下更长的发现时间。
集群中的一个节点出现故障不会影响其它节点继续正常工作,单节点故障不会丢失任何数据。失败节点的恢复是自动的。当失败节点重新联机时,它会自动与其它节点同步数据,之后才允许它重新回到集群中。如果重新同步过程中状态快照传输(SST)失败,会导致接收节点不可用,因为接收节点在检测到状态传输故障时将中止。这种情况下若使用的是mysqldump方式的SST,需要手动还原。
仲裁:
除了单节点故障外,群集还可能由于网络故障而拆分为多个部分。每部分内的节点相互连接,但各部分之间的节点失去连接,这被称为网络分裂(network partitioning)。此情况下只有一部分可以继续修改数据库状态,以避免数据差异,这一部分即为主组件。正常情况下主组件就是整个集群。当发生网络分裂时,Galera集群调用一个仲裁算法选择一部分作为主组件,保证集群中只有一个主组件。
加权法定票数(Weighted Quorum)
集群中的当前节点数量定义了当前集群的大小,群集大小决定达到仲裁所需的票数。Galera集群在节点不响应并且被怀疑不再是集群的一部分时进行仲裁投票。可以使用evs.suspect_timeout参数微调此无响应的超时时间,默认为5秒。
发生网络分裂时,断开连接的两侧都有活动节点。主组件要求获得仲裁的多数票,因此具有较多存活节点的部分将成为主组件,而另一部分将进入非主状态并开始尝试与主组件连接。
仲裁要求多数,这意味着不能在双节点群集中进行自动故障转移,因为一个节点的故障会导致另一节点自动进入非主状态。而具有偶数个节点的集群则有脑裂风险。如果在网络分裂导致节点的数量正好分成两半,则两个分区都不能成为主组件,并且都进入非主状态,如图7所示。要启用Galera集群自动故障切换,至少需要使用三个节点。
裂脑(Split-Brain)
导致数据库节点彼此独立运行的集群故障称为“脑裂”。这种情况可能导致数据不一致,并且无法修复,例如当两个数据库节点独立更新同一表上的同一行时。与任何基于仲裁的系统一样,当仲裁算法无法选择主组件时,Galera集群会受到脑裂影响。
Galera设计为避免进入分裂脑状态,如果失败导致将集群分割为两个大小相等的部分,则两部分都不会成为主组件。在节点数为偶数的集群中,为把脑裂风险降到最低,可以人为分区将一部分始终划分为集群主组件。
法定票数计算
Galera群集支持加权仲裁,其中每个节点可以被分配0到255范围内的权重参与计算。当且仅当当前节点权重总和大于最后一个主组件节点权重和减去正常离开集群节点权重和的一半时,才会被选为新的主组件。消息传递时带有权重信息。缺省的节点权重为1,此时公式被转换为单纯的节点计数比较。通过设置pc.weight参数,可以在运行时更改节点权重。
关系型数据库本身比较容易成为系统瓶颈,单机存储容量、连接数、处理能力都有限。当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从库、优化索引,做很多操作时性能仍下降严重。此时就要考虑对其进行切分了,切分的目的就在于减少数据库的负担,缩短查询时间。
数据库分布式核心内容无非就是数据切分(Sharding),以及切分后对数据的定位、整合。数据切分就是将数据分散存储到多个数据库中,使得单一数据库中的数据量变小,通过扩充主机的数量缓解单一数据库的性能问题,从而达到提升数据库操作性能的目的。
数据切分根据其切分类型,可以分为两种方式:垂直(纵向)切分和水平(横向)切分。
垂直分库就是根据业务耦合性,将关联度低的不同表存储在不同的数据库。做法与大系统拆分为多个小系统类似,按业务分类进行独立划分。与"微服务治理"的做法相似,每个微服务使用单独的一个数据库。
垂直分表是基于数据库中的"列"进行,某个表字段较多,可以新建一张扩展表,将不经常用或字段长度较大的字段拆分出去到扩展表中。在字段很多的情况下(例如一个大表有100多个字段),通过"大表拆小表",更便于开发与维护,也能避免跨页问题,MySQL底层是通过数据页存储的,一条记录占用空间过大会导致跨页,造成额外的性能开销。另外数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率更高,减少了磁盘IO,从而提升了数据库性能。
优点:
缺点:
当一个应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平切分了。
水平切分分为库内分表和分库分表,是根据表内数据内在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。
库内分表只解决了单一表数据量过大的问题,但没有将表分布到不同机器的库上,因此对于减轻MySQL数据库的压力来说,帮助不是很大,大家还是竞争同一个物理机的CPU、内存、网络IO,最好通过分库分表来解决。
优点:
缺点:
水平切分后同一张表会出现在多个数据库/表中,每个库/表的内容不同。几种典型的数据分片规则为:
根据数值范围:
按照时间区间或ID区间来切分。例如:按日期将不同月甚至是日的数据分散到不同的库中;将userId为1~9999的记录分到第一个库,10000~20000的分到第二个库,以此类推。某种意义上,某些系统中使用的"冷热数据分离",将一些使用较少的历史数据迁移到其他库中,业务功能上只提供热点数据的查询,也是类似的实践。
优点:
缺点:
根据数值取模:
一般采用hash取模mod的切分方式,例如:将 Customer 表根据 cusno 字段切分到4个库中,余数为0的放到第一个库,余数为1的放到第二个库,以此类推。这样同一个用户的数据会分散到同一个库中,如果查询条件带有cusno字段,则可明确定位到相应库去查询。
优点:
缺点:
事务一致性问题
当更新内容同时分布在不同库中,不可避免会带来跨库事务问题。跨分片事务也是分布式事务,没有简单的方案,一般可使用"XA协议"和"两阶段提交"处理。
分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间。导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平扩展的枷锁。
对于那些性能要求很高,但对一致性要求不高的系统,往往不苛求系统的实时一致性,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误后立即回滚的方式不同,事务补偿是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等等。事务补偿还要结合业务系统来考虑。
跨节点关联查询 join 问题
切分之前,系统中很多列表和详情页所需的数据可以通过sql join来完成。而切分之后,数据可能分布在不同的节点上,此时join带来的问题就比较麻烦了,考虑到性能,尽量避免使用join查询。解决这个问题的一些方法
跨节点分页、排序、函数问题
跨节点多库进行查询时,会出现limit分页、order by排序等问题。分页需要按照指定字段进行排序,当排序字段就是分片字段时,通过分片规则就比较容易定位到指定的分片;当排序字段非分片字段时,就变得比较复杂了。需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。
只是取第一页的数据,对性能影响还不是很大。但是如果取得页数很大,情况则变得复杂很多,因为各分片节点中的数据可能是随机的,为了排序的准确性,需要将所有节点的前N页数据都排序好做合并,最后再进行整体的排序,这样的操作时很耗费CPU和内存资源的,所以页数越大,系统的性能也会越差。
在使用Max、Min、Sum、Count之类的函数进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回。
全局主键避重问题
在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库自生成的ID无法保证全局唯一。因此需要单独设计全局主键,以避免跨库主键重复问题。有一些常见的主键生成策略
UUID标准形式包含32个16进制数字,分为5段,形式为8-4-4-4-12的36个字符,UUID是主键是最简单的方案,本地生成,性能高,没有网络耗时。但缺点也很明显,由于UUID非常长,会占用大量的存储空间;另外,作为主键建立索引和基于索引进行查询时都会存在性能问题,在InnoDB下,UUID的无序性会引起数据位置频繁变动,导致分页。
结合数据库维护主键ID表,在数据库中建立 sequence 表,stub字段设置为唯一索引,同一stub值在sequence表中只有一条记录,可以同时为多张表生成全局ID。使用 MyISAM 存储引擎而不是 InnoDB,以获取更高的性能。MyISAM使用的是表级别的锁,对表的读写是串行的,所以不用担心在并发时两次读取同一个ID值。
当需要全局唯一的64位ID时,执行:
REPLACE INTO sequence (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
这两条语句是Connection级别的,select last_insert_id() 必须与 replace into 在同一数据库连接下才能得到刚刚插入的新ID。使用replace into代替insert into好处是避免了表行数过大,不需要另外定期清理。此方案较为简单,但缺点也明显:存在单点问题,强依赖DB,当DB异常时,整个系统都不可用。配置主从可以增加可用性,但当主库挂了,主从切换时,数据一致性在特殊情况下难以保证。另外性能瓶颈限制在单台MySQL的读写性能。
flickr团队使用的一种主键生成策略,与上面的sequence表方案类似,但更好的解决了单点和性能瓶颈的问题。
这一方案的整体思想是:建立2个以上的全局ID生成的服务器,每个服务器上只部署一个数据库,每个库有一张sequence表用于记录当前全局ID。表中ID增长的步长是库的数量,起始值依次错开,这样能将ID的生成散列到各个数据库上。
这种方案将生成ID的压力均匀分布在两台机器上。同时提供了系统容错,第一台出现了错误,可以自动切换到第二台机器上获取ID。但有以下几个缺点:系统添加机器,水平扩展时较复杂;每次获取ID都要读写一次DB,DB的压力还是很大,只能靠堆机器来提升性能。
可以基于flickr的方案继续优化,使用批量的方式降低数据库的写压力,每次获取一段区间的ID号段,用完之后再去数据库获取,可以大大减轻数据库的压力。
数据迁移、扩容问题
当业务高速发展,面临性能和存储的瓶颈时,才会考虑分片设计,此时就不可避免的需要考虑历史数据迁移的问题。一般做法是先读出历史数据,然后按指定的分片规则再将数据写入到各个分片节点中。此外还需要根据当前的数据量和QPS,以及业务发展的速度,进行容量规划,推算出大概需要多少分片(一般建议单个分片上的单表数据量不超过1000W)。
如果采用数值范围分片,只需要添加节点就可以进行扩容了,不需要对分片数据迁移。如果采用的是数值取模分片,则考虑后期的扩容问题就相对比较麻烦。
分页拉取数据的需求:
这些业务场景对应的消息表,订单表,帖子表分页拉取需求有这样一些特点:
在数据量不大时,可以通过在排序字段time上建立索引,利用SQL提供的offset/limit功能就能满足分页:
select * from t_msg order by time offset 200 limit 100
select * from t_order order by time offset 200 limit 100
select * from t_tiezi order by time offset 200 limit 100
-- 此处假设一页数据为100条,均拉取第3页数据
分库需求:
高并发大流量的互联网架构,一般通过服务层来访问数据库,随着数据量的增大,数据库需要进行水平切分,分库后将数据分布到不同的数据库实例(甚至物理机器)上,以达到降低数据量,增加实例数的扩容目的。
一旦涉及分库,逃不开“分库依据”patition key的概念,使用哪一个字段来水平切分数据库呢:大部分的业务场景,会使用业务主键id。确定了分库依据patition key后,接下来要确定的是分库算法:大部分的业务场景,会使用业务主键id取模的算法来分库,这样即能够保证每个库的数据分布是均匀的,又能够保证每个库的请求分布是均匀的,实在是简单实现负载均衡的好方法,此法在互联网架构中应用颇多。
用户库user,水平切分后变为两个库,分库依据patition key是uid,分库算法是uid取模:uid%2余0的数据会落到db0,uid%2余1的数据会落到db1。仍然是上述用户库的例子,如果业务要查询“最近注册的第3页用户”,该如何实现呢?单库上,可以:
select * from t_user order by time offset 200 limit 100
变成两个库后,分库依据是uid,排序依据是time,数据库层失去了time排序的全局视野,数据分布在两个库上,此时该怎么办呢?
如何满足“跨越多个水平切分数据库,且分库依据与排序依据为不同属性,并需要进行分页”的查询需求,实现select * from T order by time offset X limit Y
的跨库分页SQL,是本文将要讨论的技术问题。
全局视野法:
服务层通过uid取模将数据分布到两个库上去之后,每个数据库都失去了全局视野,数据按照time局部排序之后,不管哪个分库的第3页数据,都不一定是全局排序的第3页数据。
那到底哪些数据才是全局排序的第3页数据呢,暂且分三种情况讨论:
极端情况,两个库的数据完全一样
如果两个库的数据完全相同,只需要每个库offset一半,再取半页,就是最终想要的数据。
极端情况,结果数据来自一个库
也可能两个库的数据分布及其不均衡,例如db0的所有数据的time都大于db1的所有数据的time,则可能出现:一个库的第3页数据,就是全局排序后的第3页数据(如上图中粉色部分数据)。
一般情况,每个库数据各包含一部分
正常情况下,全局排序的第3页数据,每个库都会包含一部分(如上图中粉色部分数据)。
由于不清楚到底是哪种情况,所以必须每个库都返回3页数据,所得到的6页数据在服务层进行内存排序,得到数据全局视野,再取第3页数据,便能够得到想要的全局分页数据。
再总结一下这个方案的步骤:
(1)将order by time offset X limit Y
,改写成order by time offset 0 limit X+Y
(2)服务层将改写后的SQL语句发往各个分库:即例子中的各取3页数据
(3)假设共分为N个库,服务层将得到N*(X+Y)条数据:即例子中的6页数据
(4)服务层对得到的N*(X+Y)条数据进行内存排序,内存排序后再取偏移量X后的Y条记录,就是全局视野所需的一页数据
方案优点:通过服务层修改SQL语句,扩大数据召回量,能够得到全局视野,业务无损,精准返回所需数据。
方案缺点(显而易见):
(1)每个分库需要返回更多的数据,增大了网络传输量(耗网络);
(2)除了数据库按照time进行排序,服务层还需要进行二次排序,增大了服务层的计算量(耗CPU);
(3)最致命的,这个算法随着页码的增大,性能会急剧下降,这是因为SQL改写后每个分库要返回X+Y行数据:返回第3页,offset中的X=200;假如要返回第100页,offset中的X=9900,即每个分库要返回100页数据,数据量和排序量都将大增,性能平方级下降。
业务折衷法:
“全局视野法”虽然性能较差,但其业务无损,数据精准,不失为一种方案,有没有性能更优的方案呢?“任何脱离业务的架构设计都是耍流氓”,技术方案需要折衷,在技术难度较大的情况下,业务需求的折衷能够极大的简化技术方案。
业务折衷一:禁止跳页查询
在数据量很大,翻页数很多的时候,很多产品并不提供“直接跳到指定页面”的功能,而只提供“下一页”的功能,这一个小小的业务折衷,就能极大的降低技术方案的复杂度。如上图,不够跳页,那么第一次只能够查第一页:
(1)将查询order by time offset 0 limit 100
,改写成order by time where time>0 limit 100
(2)上述改写和offset 0 limit 100的效果相同,都是每个分库返回了一页数据
(3)服务层得到2页数据,内存排序,取出前100条数据,作为最终的第一页数据,这个全局的第一页数据,一般来说每个分库都包含一部分数据
咦,这个方案也需要服务器内存排序,岂不是和“全局视野法”一样么?第一页数据的拉取确实一样,但每一次“下一页”拉取的方案就不一样了。点击“下一页”时,需要拉取第二页数据,在第一页数据的基础之上,能够找到第一页数据time的最大值:time_max。
这个上一页记录的time_max,会作为第二页数据拉取的查询条件:
(1)将查询order by time offset 100 limit 100
,改写成order by time where time>$time_max limit 100
(2)这下不是返回2页数据了(“全局视野法,会改写成offset 0 limit 200”),每个分库还是返回一页数据
(3)服务层得到2页数据,内存排序,取出前100条数据,作为最终的第2页数据,这个全局的第2页数据,一般来说也是每个分库都包含一部分数据
如此往复,查询全局视野第100页数据时,不是将查询条件改写为offset 0 limit 9900+100(返回100页数据),而是改写为time>$time_max99 limit 100(仍返回一页数据),以保证数据的传输量和排序的数据量不会随着不断翻页而导致性能下降。
业务折衷二:允许数据精度损失
“全局视野法”能够返回业务无损的精确数据,在查询页数较大,例如第100页时,会有性能问题,此时业务上是否能够接受,返回的100页不是精准的数据,而允许有一些数据偏差呢?
数据库分库-数据均衡原理:
使用patition key进行分库,在数据量较大,数据分布足够随机的情况下,各分库所有非patition key属性,在各个分库上的数据分布,统计概率情况是一致的。
例如,在uid随机的情况下,使用uid取模分两库,db0和db1:
(1)性别属性,如果db0库上的男性用户占比70%,则db1上男性用户占比也应为70%
(2)年龄属性,如果db0库上18-28岁少女用户比例占比15%,则db1上少女用户比例也应为15%
(3)时间属性,如果db0库上每天10:00之前登录的用户占比为20%,则db1上应该是相同的统计规律
利用这一原理,要查询全局100页数据,offset 9900 limit 100
改写为offset 4950 limit 50
,每个分库偏移4950(一半),获取50条数据(半页),得到的数据集的并集,基本能够认为,是全局数据的offset 9900 limit 100
的数据,当然,这一页数据的精度,并不是精准的。
根据实际业务经验,用户都要查询第100页网页、帖子、邮件的数据了,这一页数据的精准性损失,业务上往往是可以接受的,但此时技术方案的复杂度便大大降低了,既不需要返回更多的数据,也不需要进行服务内存排序了。
终极武器-二次查询法
为了方便举例,假设一页只有5条数据,查询第200页的SQL语句为select * from T order by time offset 1000 limit 5;
将select * from T order by time offset 1000 limit 5
改写为select * from T order by time offset 500 limit 5
并投递给所有的分库,注意,这个offset的500,来自于全局offset的总偏移量1000,除以水平切分数据库个数2。
如果是3个分库,则可以改写为select * from T order by time offset 333 limit 5
。
找到所返回3页全部数据的最小值:三页数据中,time最小值来自第一个库,time_min=1487501123,这个过程只需要比较各个分库第一条数据,时间复杂度很低。
第一次改写的SQL语句是select * from T order by time offset 333 limit 5
,第二次要改写成一个between语句,between的起点是time_min,between的终点是原来每个分库各自返回数据的最大值:相对第一次查询,第二次查询条件放宽了,故第二次查询会返回比第一次查询结果集更多的数据。这种方法的优点是:可以精确的返回业务所需数据,每次返回的数据量都非常小,不会随着翻页增加数据的返回量。但是需要进行两次数据库查询。
最常见的 JOIN 类型:SQL INNER JOIN(简单的 JOIN)、SQL LEFT JOIN、SQL RIGHT JOIN、SQL FULL JOIN,其中前一种是内连接,后三种是外链接。
内连接是最常见的一种连接,只连接匹配的行。
LEFT JOIN返回左表的全部行和右表满足ON条件的行,如果左表的行在右表中没有匹配,那么这一行右表中对应数据用NULL代替。
RIGHT JOIN返回右表的全部行和左表满足ON条件的行,如果右表的行在左表中没有匹配,那么这一行左表中对应数据用NULL代替。
FULL JOIN 会从左表 和右表 那里返回所有的行。如果其中一个表的数据行在另一个表中没有匹配的行,那么对面的数据用NULL代替
目前广泛使用的是 MyISAM 和 InnoDB 两种引擎:
MyISAM:
MyISAM 引擎是 MySQL 5.1 及之前版本的默认引擎,它的特点是:
InnoDB:
InnoDB 在 MySQL 5.5 后成为默认索引,它的特点是:
总体来讲,MyISAM 适合 SELECT 密集型的表,而 InnoDB 适合 INSERT 和 UPDATE 密集型的表
可以使用下面几个工具来做基准测试:
具体的调优参数内容较多,具体可参考官方文档,这里介绍一些比较重要的参数:
show status like'key_read%'
,保证 key_reads / key_read_requests 在 0.1% 以下最好show status like'Innodb_buffer_pool_read%'
,保证 (Innodb_buffer_pool_read_requests – Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests
越高越好(Qcache_hits / (Qcache_hits + Qcache_inserts) * 100))
进行调整,一般不建议太大,256MB 可能已经差不多了,大型的配置型静态数据可适当调大. 可以通过命令 show status like'Qcache_%'
查看目前系统 Query Catch 使用大小分区的好处是:
分区的限制和缺点:
分区的类型:
1、COUNT有几种用法?
2、COUNT(字段名)和COUNT(*)的查询结果有什么不同?
3、COUNT(1)和COUNT(*)之间有什么不同?
4、COUNT(1)和COUNT(*)之间的效率哪个更高?
5、为什么《阿里巴巴Java开发手册》建议使用COUNT(*)
6、MySQL的MyISAM引擎对COUNT(*)做了哪些优化?
7、MySQL的InnoDB引擎对COUNT(*)做了哪些优化?
8、上面提到的MySQL对COUNT(*)做的优化,有一个关键的前提是什么?
9、SELECT COUNT(*) 的时候,加不加where条件有差别吗?
10、COUNT(*)、COUNT(1)和COUNT(字段名)的执行过程是怎样的?
COUNT函数的功能:
COUNT(*)
的统计结果中,会包含值为NULL的行数。COUNT(常量) 和 COUNT(*)
表示的是直接查询符合条件的数据库表的行数。而COUNT(列名)表示的是查询符合条件的列的值不为NULL的行数。
除了查询得到结果集有区别之外,COUNT(*)
相比COUNT(常量)
和COUNT(列名)
来讲,COUNT(*)
是SQL92定义的标准统计行数的语法,因为他是标准语法,所以MySQL数据库对他进行过很多优化。
SQL92,是数据库的一个ANSI/ISO标准。它定义了一种语言(SQL)以及数据库的行为(事务、隔离级别等)。
COUNT(*)
的优化:
MyISAM和InnoDB有很多区别,其中有一个关键的区别和我们接下来要介绍的COUNT(*)
有关,那就是MyISAM不支持事务,MyISAM中的锁是表级锁;而InnoDB支持事务,并且支持行级锁。
因为MyISAM的锁是表级锁,所以同一张表上面的操作需要串行进行,所以,MyISAM做了一个简单的优化,那就是它可以把表的总行数单独记录下来,如果从一张表中使用COUNT(*)
进行查询的时候,可以直接返回这个记录下来的数值就可以了,当然,前提是不能有where条件。
MyISAM之所以可以把表中的总行数记录下来供COUNT(*)
查询使用,那是因为MyISAM数据库是表级锁,不会有并发的数据库行数修改,所以查询得到的行数是准确的。
但是,对于InnoDB来说,就不能做这种缓存操作了,因为InnoDB支持事务,其中大部分操作都是行级锁,所以可能表的行数可能会被并发修改,那么缓存记录下来的总行数就不准确了。
在InnoDB中,使用COUNT(*)
查询行数的时候,不可避免的要进行扫表了,那么,就可以在扫表过程中下功夫来优化效率了。
从MySQL 8.0.13开始,针对InnoDB的SELECT COUNT(*) FROM tbl_name
语句,确实在扫表的过程中做了一些优化。前提是查询语句中不包含WHERE或GROUP BY等条件。
我们知道,COUNT(*)
的目的只是为了统计总行数,所以,他根本不关心自己查到的具体值,所以,他如果能够在扫表的过程中,选择一个成本较低的索引进行的话,那就可以大大节省时间。
InnoDB中索引分为聚簇索引(主键索引)和非聚簇索引(非主键索引),聚簇索引的叶子节点中保存的是整行记录,而非聚簇索引的叶子节点中保存的是该行记录的主键的值。
所以,相比之下,非聚簇索引要比聚簇索引小很多,MySQL会优先选择最小的非聚簇索引来扫表。当我们建表的时候,除了主键索引以外,创建一个非主键索引还是有必要的。
对于COUNT(1)
和COUNT(*)
,MySQL的优化是完全一样的,根本不存在谁比谁快!建议使用COUNT(*)
!因为这个是SQL92定义的标准统计行数的语法。
COUNT(字段):
最后,就是我们一直还没提到的COUNT(字段),他的查询就比较简单粗暴了,就是进行全表扫描,然后判断指定字段的值是不是为NULL,不为NULL则累加。
相比COUNT(*)
,COUNT(字段)多了一个步骤就是判断所查询的字段是否为NULL,所以他的性能要比COUNT(*)
慢。