概述
一、后台线程
(1)Master Thread
负责刷新内存池中的数据,保证缓冲池中的内存缓存是最近的数据。该线程具备最高的优先级,会根据数据库的运行状态在loop、background loop、flush loop、suspend loop之间切换。
- loop的主要是进行每秒钟的日志buffer刷盘,合并插入buffer,刷新脏页等,以及每十秒钟进行一次删除无用的undo页。
- background loop主要是在没有用户进行活动的时候删除无用的undo页、合并插入buffer等。
(2)IO Thread
由于InnDB使用了大量的AIO技术来处理IO请求,IO线程主要用来处理这些AIO的回调。在Linux下,IO Thread不能调整,只能是读线程、写线程、插入缓冲线程和日志线程。
(3)Purge Thread
事务提交后回收已经分配的undo日志,离散的读取undo页面,可以更好的利用磁盘的随机读写性能。
(4)Page Cleaner Thread
将之前版本中脏页的刷新放到单独的线程,来减轻Master Thread的负担。
二、InnoDB内存布局
InnoDB是基于磁盘的存储引擎,按照页的方式进行管理。页从缓冲池刷新会磁盘的操作并不是在每次发生页更新的时候触发,而是通过CheckPoint机制刷回到磁盘。
32位操作系统的限制,在32位系统下最多将该值设置为3GB,打开操作系统的PAE的选项获得32位操作系统的最大64GB内存支持。
InnoDB允许多个缓冲池实例,每个页根据Hash值平均分配到不同的缓冲池实例。数据库的缓冲池是通过LRU算法进行管理的,缓冲池页大小默认是16KB。
(1)缓冲池
由innodb_buffer_pool_size
参数控制,占用最大的内存,用于存放各种缓冲数据,因为InnoDB的工作方式是将数据库文件按页(16KB)读取到缓冲池,然后按照LRU算法来将数据保留在缓冲池的缓冲数据中。修改文件首先修改缓冲池的数据,然后将脏页刷新到磁盘。
(2)重做日志缓冲池
由innodb_log_buffer_size
参数控制,用来存储重做日志。InnoDB存储引擎首先将redo日志信息先放入到这个缓冲区,然后按照一定频率将其刷新到redo日志。redo日志缓冲一般不需要设置的很大,因为一般情况下每秒会将redo日志刷到日志文件。默认redo缓冲区大小是8MB。三种进行redo日志刷盘的时机:
- master thread的每秒钟刷盘
- 每个事务的提交
- redo日志缓冲区小于剩余空间的1/2
(3)额外的内存池
InnoDB对内存管理是通过Heap的方式进行的,在对一些数据结构本身的内存进行分配的时候,需要从额外的内存池中进行申请,当该区域的内存不够的时候会从缓冲池中进行申请。
三、InnoDB关键特性
(1)插入buffer
插入buffer和数据页相同,都是物理页组成部分。InnoDB中主键是唯一标识符,插入聚焦索引一般是顺序的,不需要随机的磁盘读写。针对非聚焦索引的插入更新操作并不是每次直接插入到索引页,而是先判断插入的非聚焦索引是否在缓冲池中,如果在就直接插入,否则就先插入到插入buffer中,欺骗数据库已经将索引插入到叶子节点。然后再按照一定频率将其插入到实际的叶子节点。插入buffer的使用满足一下两点:
- 索引是辅助索引
- 索引不是唯一的
(2)两次写
主要目标是实现应用redo日志时提高数据页的可靠性,redo日志是对页的物理操作,在应用redo日志的时候,需要创建的副本,当应用失败的时候,使用副本还原,这就是两次写机制。两次写分为两部分:
- 内存中的double write buffer,大小为2MB
- 磁盘上共享表空间中连续128个页,即两个区,大小为2MB
首先memcpy
函数将脏页复制到内存中的两次写缓冲中,然后通过每次1MB的写入共享表空间页的物理磁盘上,然后马上调用fsync
函数同步磁盘。有些文件系统本事就提供了部分写失败的方法机制,例如ZFS。
(3)自适应Hash索引
B+树的查找次数取决于B+树的高度,生产环境下一般在3-4层,因此需要3-4此查询。自适应Hash索引AHI通过缓冲池的B+树构造而来的,因此建立的速度很快,而且不需要对整张表构建Hash索引。InnoDB存储引擎会自动根据访问频率和模式自动为某些热点页建立AHI。建立AHI的要求是对这个页的连续访问模式必须是一样的,并且以该模式访问了100次,页通过该模式访问了N次,其中N=页中记录/16。
AHI数据数据库自优化的一种方式。
(4)异步IO
AIO的另一个优势是将多个IO合并为一个IO操作。InnoDB支持内核级别的AIO操作,成为Native AIO,但是需要libaio库的支持,Mac OS X没有提供InnoDB的AIO操作。启动Native AIO后,恢复速度提高75%。
(5)刷新临近页
当刷新一个脏页的时候,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日志看成大管家,保证所有数据的完整性。
- undo日志:undo日志基本是逻辑日志,其记录时间点为修改缓冲中的页面之前。先于redo日志,这样redo日志可以记录undo页面的变换,防止undo日志页因为宕机有部分没有刷新到ibd文件回滚段。次级索引记录的修改不记录undo日志。需要注意的是binlog日志的记录点是在读取到commit之后写入的。
五、检查点
当前的事务数据库系统普遍采用了Write Ahead Log策略,也就是当事务提交的时候,先写入redo日志,在进行页修改。Check Point技术的目的是:
- 缩短数据库的恢复时间,宕机后只需要对检查点后的redo日志进行恢复即可。
- 缓冲池不够用的时候根据LRU强制将脏页刷新至磁盘。
- redo日志不可用时刷新脏页,redo日志可以被重新覆盖利用的时候,就不需要刷新脏页,当redo全部用完后会强制执行CheckPoint刷新脏页数据。
对于InnoDB引擎,redo日志,数据页和CheckPoint通过LSN标记版本,LSN是8字节的数字。CheckPoint分为两种:
- Sharp CheckPoint:发生在数据库关闭的时候,会将所有的脏页刷回至磁盘。
- Fuzzy CheckPoint:用于刷新脏页。
对于Master Thread差不多以每秒或者每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回到磁盘,这个过程是异步的。Async/Sync Flush CheckPoint是为了保证重做日志的循环可使用性。这部分刷新操作由单独的Page Cleaner Thread实现,因此不会阻塞用户查询线程。
六、LRU
为什么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列表,如果没有就其拆分成为不同大小的页进行存储。
- Flush列表:在LRU列表中被修改的页叫做脏页,缓冲池和磁盘上的数据产生了不一致。这时数据库通过CheckPoint机制将脏页刷新会磁盘,而Flush列表中的页就是脏页列表。脏页既在LRU列表中也在Flush列表中。LRU用来管理页的可用性,Flush用来管理页的刷盘。
七、存储引擎对比
MySQL是一个单进程多线程架构的数据库。一个数据库实例在操作系统上表现为一个进程。在MySQL中存储引擎是基于表的。MySQL具有独有的插件式体系结构。
(1)InnoDB
-
InnoDB存储引擎支持事务,主要面向OLTP应用,特点是行锁,支持外键,支持非锁定读,默认读取操作不会产生锁。
-
InnoDB是MySQL默认的存储引擎。InnoDB存储引擎的表单存放到一个独立的ibd文件中,InnoDB存储殷勤支持裸设备(原始空间,没有经过格式化的分区)建立表空间。
-
InnoDB使用多版本MVCC并发控制获得高性能,实现了SQL的4中隔离级别,默认是repeatable级别。使用next-key-locking策略避免幻读。
- 对于表中数据的存储,InnoDB存储引擎采用聚集的方法,按照主键的顺序存放,如果没有定义主键,InnoDB存储引擎会为每一行生成一个6字节的ROWID作为主键。
(2)MyISAM
-
不支持事务,表锁设计,支持全文索引,主要面向OLAP应用。
-
缓冲池只缓存索引文件,数据文件由操作系统本身完成。
-
存储引擎有MYI和MYD组成,用于存放索引和数据。
- 可以通过myisampack工具使用赫夫曼编码静态算法进行数据压缩,压缩后表是只读的。
(3)其他存储引擎
-
NDB存储引擎:一种集群存储引擎,类似于Oracle的RAC集群,数据全部存储到内存。
-
Memory存储引擎:也叫做Heap存储引擎,数据全部存放内存,默认使用Hash索引,仅支持表锁,并发性能差,不支持Text和BLOB类型。存储varchar按照char定长方式,因此会浪费内存。一般会使用该引擎作为查询中间结果集。
-
Archive存储引擎:仅支持select和insert操作。支持索引,使用zlib算法将数据行进行压缩后存储,压缩比一般可达1:10。适合存储归档数据,事务不安全的,使用行锁实现高并发插入操作。
-
Federated存储引擎:不存放任何数据,指向远程的一台MySQL数据库服务器。类似于SQL Server的链接服务器或者Oracle的透明网关。
- Maria存储引擎:取代原有的MyISAM存储引擎。支持缓存数据和索引,使用行锁设计,提供MVCC,支持事务和非安全事务,更好的BLOB字符类型的处理性能。
八、锁机制
(1)概述
锁机制用户管理对共享资源的并发访问。lock的对象是事务,用来锁定的是数据库中的对象,lock是存在死锁机制的。加锁的基本单位是 next-key lock 。
-
InnoDB存储引擎会在行级别上对表数据上锁。
-
对于MyISAM引擎,其锁是表锁设计。
-
对于MS SQL Server 2005之前的版本都是页锁。
页锁容易实现,然而对于热点数据页的并发访问问题依然无法解决。到2005版本之后,MS SQL Server开始支持乐观并发和悲观并发,在乐观并发情况下开始支持行级锁。在这种情况下,行锁会升级到表锁。
InnoDB存储引擎锁的实现和Oracle数据库非常类似。
(2)latch
latch是一种轻量级的锁,因为其要求锁定的时间必须非常短。在InnoDB存储引擎中,latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保障并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。
(3)InnoDB中的锁
InnoDB存储引擎实现了如下的两种标准的行级锁:
- S共享锁:允许事务读一行数据。
- X排它锁:允许事务删除或更新一行数据。
意向锁:因为读取并没有改变行的数据,这种情况叫做锁兼容。此外,InnoDB支持多粒度锁定,这种锁定允许事务在行级别上的锁和表级别上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式就是I意向锁。意向锁是将锁定的对象分为多层,意向锁意味着事务希望在更细的粒度上进行加锁。如果上锁的对象看成一颗树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁。那么首先需要对粗粒度的对象上锁。
- IS意向共享锁:事务想要获得一张表中的某几行的共享锁。
- IX意向排它锁:事务想要获得一张表中的某几行的排它锁。
由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除去全表扫描以外的任何请求。
(4)一致性非锁定读取,MVCC
是指InnoDB存储引擎通过多版本的方式来读取当前执行时间数据库中的数据。如果读取的行正在执行DELETE和UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反的,InnoDB存储引擎会去读取行的一个快照数据。非锁定读机制极大的提高了数据库的并发性,这时默认的读取方式,即使读取不会占用和等待表上的锁。并不是每个事务隔离级别下都是采用非锁定的一致性读取。一个行记录可能有不止一个快照数据,这样的技术叫做MVCC,多版本并发控制。
(5)自增与锁
对每个含有自增长值的表都有一个自增长计数器,当对含有自增长的计数器的表进行插入操作,这个计数器会被初始化,这个实现方式是通过AUTO-INC locking锁机制实现的,它是一种表锁,为了提高插入性能,锁并不是在一个事务执行完成后释放的,而是在完成时对自增长值插入的SQL语句后立即释放的。
对于带有自增长值的列的并发插入性能较差,事务必须等待前一个插入操作执行完成。InnoDB存储引擎提供了一个参数innodb_automic_lock_mode
来控制自增长的模式。该参数的默认值是1,表示使用互斥量对内部存在的计数器进行累加操作。如果是0,表示AUTO-INC locking的方式进行,如果是2表示对于所有的插入操作都使用互斥量的方式进行。
MyISAM存储引擎是表锁设计,因此自增长不用考虑并发插入的问题。
(6)锁的三种算法
-
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语句可能会返回之前不存在的值。
(7)脏读、不可重复读、丢失更新
- 脏读:所谓脏数据是指事务对缓冲池中的行记录的修改,并且还没有提交。对于脏页的读取是非常正常的。
- 脏页是因为数据库实例内存与磁盘的异步造成的。
- 脏数据是指未提交的数据,如果读取到了脏数据,即一个事务可以读取到另外一个事务中未提交的数据,则显然违反了数据库的隔离性。
- 脏读一般发生在隔离级别是Read uncommitted下。
- 不可重复读:一个事务多次读取同一集合,这个事务还没有结束的时候,另外一个事务也访问该同一个数据集合,并做了一些DML操作,那么第一个事务两次读取到的数据可能不是一样的。不可重复读和脏读的区别是脏读是读到未提交的数据,而不可重复读却是读取的已经提交的数据,但是其违反了数据库事务一致性的要求。在InnoDB中通过Next-Key Locking避免不可重复读的问题。
- 丢失更新:是个事务操作会被另一个事务的更新操作覆盖,从而导致数据不一致。
(8)死锁、锁升级
-
死锁:在两个及其以上的事务在执行过程中,因为争夺资源而造成的一种互相等待的现象。
在InnoDB存储引擎中,参数
innodb_lock_wait_timeout
用来设置超时的时间。当前数据库普遍采用wait-for-graph等待图的方式来进行死锁检测。等待图要求保存锁的信息链表和事务的等待链表。等待图机制采用深度优先的算法来实现,在InnoDB1.2之前的版本中都采用递归的方式实现。- 系统中事务的数量越多发生死锁概率就越大。
- 每个事务操作的数量越多,发生死锁的概率越大。
- 操作数据的集合越小则发生死锁的概率越大。
- 锁升级:是指当前锁的粒度降低。由一句单独的SQL语句在同一个对象上持有的锁的数量超过了阈值,默认这个阈值为5000,值得注意的是,如果是不同的对象,则不会发生锁升级。或者锁资源占用的内存超过了激活内存的40%就会发生锁升级。
(9)SQL中的4个隔离级别
- read uncommitted:事务中的修改,即使没有提交,对其他事务也都是可见的
- read committed:一个事务开始时,只能看到已经提交的事务所做的修改
- repeatable read:解决了脏读问题。该级别保证了在同一个事务中多次读取同样的记录的结果是一致的。可重复读隔离级别还是无法解决另一个幻读 (PhantomRead)的问题。可重复读(REPEATABLE READ)是Mysql 默认的事务隔离级别,其中InnoDB主要通过使用MVVC获得高并发,使用一种被称为next-key-locking的策略来避免幻读。
- serializable:通过强制事务串行,避免了前面说的幻读问题
九、索引
(1)B+ Tree的原理
- 为啥索引常用 B+ 树作为底层的数据结构
- 除了 B+ 树索引,你还知道什么索引
- 为啥推荐自增 id 作为主键,自建主键不行吗
- 什么是页分裂,页合并
- 怎么根据索引查找行记录
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+树有以下特点:
- 每个节点中子节点的个数不能超过 N,也不能小于 N/2(不然会造成页分裂或页合并)
- 根节点的子节点个数可以不超过 m/2,这是一个例外
- m 叉树只存储索引,并不真正存储数据,只有最后一行的叶子节点存储行数据。
- 通过链表将叶子节点串联在一起,这样可以方便按区间查找
AVL树:
-
平衡因子(Balance Factor):某结点的左右子树的高度差。
- 每个结点的平衡因子只可能是 -1、0、1(如果绝对值超过 1,则认为是失衡)
- 每个结点的左右子树高度差不超过 1
- 搜索、插入、删除的时间复杂度是 O(logn)
B树:
-
B 树(Balanced Tree)是一种平衡的多路搜索树,多用于文件系统、数据库的实现。
- 多代结点合并,可以获得一个超级结点,且 n 代合并的超级结点,最多拥有 (2^n) 个子结点 (至少是 (2^n) 阶 B 树)
RB树:
为了保证平衡,红黑树必须满足以下性质:
- 每个结点是要么是红色或黑色
- 根结点必须是黑色
- 叶结点(外部结点、空结点)是黑色
- 红色结点不能连续(也就是,红色结点的孩子和父亲都是黑色)
-
对于每个结点,从该点至 nil(树尾端,Java 中为 null 的结点)的任何路径都包含所相同个数的黑色结点
- 红黑树与 4 阶 B 树(2-3-4树)具有等价性
- 黑色结点与红色子结点融合在一起,形成 1 个 B 树结点
- 红黑树的黑色结点个数与 4 阶 B 树的结点总个数相等
基本平衡操作:
- 左旋指的是以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。当前结点是右子树,且父结点是红色,叔父结点是黑色,对它的父结点左旋。
- 右旋指的是以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。当前结点是左子树,且父结点是红色,叔父结点是黑色,那么:
- 把父结点变为黑色
- 把祖父结点变为红色
- 对祖父结点右旋
- 变色:如果当前结点的父结点和叔父结点是红色,那么:
- 把父结点和叔父结点变为黑色
- 把祖父结点变为红色
- 把指针定义到祖父结点
(2)InnoDB中的索引
-
B+树索引
大多数MySQL的存储引擎都是用的该索引类型。不需要全表扫描,只需要对数进行搜索。B+数是有序的,利于排序和分组。可以指定多个列作为索引列,多个索引共同组成Key。
适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。
InnoDB 的 B+Tree 索引分为主索引和辅助索引。
- 主索引的data域保存完整的数据行,这种索引方式叫做聚簇索引。由于无法将表数据存储在多个不同位置,因此一个表只能有一个聚簇索引。
- 辅助索引的叶子节点的 data 域记录着主键的值,因此在使用辅助索引进行查找时,需要先查找到主键值,然后再到主索引中进行查找。也叫非聚簇索引。
-
哈希索引
能以 O(1) 时间进行查找,但是失去了有序性,因此无法用于排序与分组,只支持精确查找,无法用于部分查找和范围查找。
InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
-
全文索引
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。查找条件使用
MATCH AGAINST
,而不是普通的 WHERE。全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。 -
空间数据索引
MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。必须使用 GIS 相关的函数来维护数据。
(3) 索引优化
- 索引列在查询条件中独立。在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。
- 多列索引。在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。
- 顺序性。让选择性最强的索引列放在前面。索引的选择性是指不重复的索引值个数和记录总数的比值。
- 前缀索引:对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。
- 覆盖索引:索引包含所有需要查询的字段的值。
- 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用。
- 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。
十、数据类型
(1)整型
TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间。INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。
(2)浮点数
FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。
FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。
(3)字符串
主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。
VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。
在进行存储和检索时,会保留 VARCHAR 末尾的空格,而会删除 CHAR 末尾的空格。
(4)日期时间
MySQL 提供了两种相似的日期时间类型:DATETIME 和 TIMESTAMP
-
DATETIME
- 能够保存从 1000 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。
- 它与时区无关。
- 默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。
-
时间戳
- 和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年到 2038 年。
- 它和时区有关。
- MySQL 提供了
FROM_UNIXTIME()
函数把 UNIX 时间戳转换为日期,并提供了UNIX_TIMESTAMP()
函数把日期转换为 UNIX 时间戳。 -
默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。
- 应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。
十一、灾备
-
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使用写时复制的技术来创建快照,当创建一个快照的时候,仅仅复制原始卷中的元数据,并不会进行物理操作。快照创建以后,原始卷上的写操作会跟踪原始卷块的改变,将需要改变的数据在改变之前复制到快照预留的空间中。如果要读取的是已经修改过的数据,则将读取保存在快照中的原始卷上改变之前的数据。
- MySQL的常见基准测试工具有:sysbench和mysql-tpcc。
十二、面试题
一条SQL语句是如何执行的?
- 连接器:负责连接和健全,完成经典的TCP握手之后就是健全处理,MySQL临时使用的内存全部在连接对象里。MySQL承载的连接对象太多时就会杀死部分连接,导致OOM重启。解决方案就是定时关闭长连接或者执行大SQL之后进行连接重置,重新初始化资源。
- 查询缓存:当进行查询的时候,会检查一下MySQL中缓存的键值对缓存数据。
- 分析器:进行语法和词法分析。
- 优化器:决定会用什么索引和多表连接的顺序。
- 执行器:检查执行权限。
一条SQL语句是如何更新的?
-
首先写redo日志,然后修改内存,此时数据变成脏数据,等待一段时间刷盘。
- 由于MySQL最开始只有MyISAM引擎,该引擎没有安全崩溃的机制,因此另一家公司使用插件的形式引入了InnoDB,InnoDB通过redo日志实现了安全崩溃。redo日志是物理日志并且循环可写。
如何降低对于热点行更新导致的性能下降?
问题的根源在于大量的死锁检测与CPU负载过高。
-
一种就是关闭死锁检测,但是关闭死锁检测可能出现大量的超时,是有损业务的。
- 另一种就是控制并发度,请求到达存储引擎前进行排队处理。如果既没有数据库中间件团队又不想有损业务,那么还有一种就是分割记录,比如将总金额平均分成10份,每次随机选择一个更新。
普通索引和唯一索引如何选择?
-
innodb读取数据的时候是按照页进行的,innodb的页大小默认是16kb。
-
因此按照页来读取的方式在查找的过程中是在内存中完成的。但是对于更新操作,当进行更新时,如果数据不在内存中,那么将会把修改写入到change buffer中,除去访问数据页会发生change buffer的merge操作,系统后台线程也会定期刷新change buffer,并且在关机的时候也会merge。
-
由于唯一索引必须读取数据页进行唯一性约束判断,因此不需要change buffer。
- redo日志主要降低磁盘的随机写的IO消耗,change buffer主要降低磁盘的随机读的IO消耗。
MySQL为什么有时候选错索引会导致很慢?
-
由于MySQL的优化器主要负责挑选合适的索引,优化器会根据扫描行数目、是否使用临时表、是否排序进行综合判断。对于上述操作MySQL能做到的只是估算。
-
一个索引上不同的值的个数叫做索引的区分度,也叫做基数。MySQL通过统计采样的方式获取基数。采样的时候会随机选取N个页,同级这些页面上不同的值,得到一个平均值,然后乘上这个索引的页面个数,就会得到基数。
- 当选择索引异常的时候可以通过force index强制选择一个索引,也可以考虑修改SQL,引导MySQL选择正确的索引,还可以新建一个更加合适的索引或者删除误用的索引。
如何为字符串添加索引?
-
直接创建索引,这样会比较占用空间。
-
创建前缀索引,节省空间,但是会增加查询扫描次数,并且不能使用覆盖索引。
- 倒序索引,再创建前缀索引,用于绕过字符串本身前缀区分度不够的问题。使用reverse()函数。
- 创建Hash字段索引,查询性能稳定,有额外的计算和存储消耗时候,不支持范围扫描。
MySQL为什么会有抖动现象?
-
InnoDB在后台刷新脏页占用了内存和IO资源,查询语句可能正好需要一个脏页或者更新语句的IO资源被脏页刷新程序占用,就会造成业务端感知MySQL的抖动。应该合理设置innodb_io_capacity的值,脏页比例不要超过75%。
- 也就是说平时的查询和更新都是在内存中的,当出现抖动的时候一般是在刷脏页。一个查询需要的脏页次数太多会明显导致查询时间过长。日志写满之后更新全部堵住,写性能全部变成0。
为什么表中的数据删除一半,表文件大小不会改变?
-
MySQL的数据分为表结构定义和数据,在MySQL8.0之前会将表结构的定义存放到frm文件中,之后存放到系统数据表中。
- 表的数据既可以存放到共享表空间中,也可以存放到单独的文件中,这个行为由
innodb_file_per_table
控制。在MySQL5.6之后默认是开启的。 - MySQL中的记录的复用只限于符合范围条件的复用,而页的复用可以复用到任何位置。当执行delete的时候只是会将记录的位置标记为可复用的状态,并不会删除磁盘数据。经过大量增删改的数据可能存在很多空洞。
- 可以创建一个完全相同的表结构,然后将原始表中的数据按照索引顺序插入到新表中,然后再删除旧表,这个过程叫做重建表。
- 可以使用
alter table 表名 engine=InnoDB
实现,中间用到的临时表InnoDB会自动创建,交换表名,删除旧表。 - MySQL5.6之后支持Online DDL,过程是新建一个临时表,扫描旧表的所有页,生成一个B+树,存储到临时文件。生成临时文件的过程中将操作记录写入到raw日志文件中,临时文件生成之后将修改应用到新表上,然后删除旧表。
为什么count(x)
会慢?
- 对于MyISAM存储引擎,会把表的总行数存储到磁盘,因此读取出来就可以了,但是InnoDB在执行count(x)的时候会一行一行的读取数据然后累计。
- 但是当MyISAM表中添加过滤条件的时候还是需要一行行读取。
- InnoDB是索引组织表,在执行count(x)的时候,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值,所以普通索引树要小很多。因此对于count(x)操作主键索引树和普通索引树的结果是一样的,MySQL会自动选择数据量最小的那颗树执行count(x)。
- 在InnoDB中,采用了MVCC,因此即使两次相同的count(x)也可能会导致结果不同。因此在生产环境中只能自己使用缓存系统计数。
如何知道MySQL的binlog是完整的?
-
对于statement格式的binlog,最后会有一个COMMIT
- 对于raw格式的binlog,最后会有一个XID event
处于prepare阶段的redo日志加上完整的binlog重启后就可以恢复数据,MySQL为什么这样设计?
-
与数据和备份的一致性有关,如果在某一时刻binlog写完以后MySQL发生崩溃,这时候binlog已经写入,之后就会被从库使用。所以在主库也要提交这个事务。
- 而redo日志记录了还没有刷盘的脏数据。
数据最终的刷盘,是从redo日志刷盘还是从buffer pool刷盘?
redo日志并没有记录数据页的完整数据,所以并没有能力去更新磁盘上的数据。
- 如果实例是正常运行的状态,数据页被修改之后,脏页数据是从内存写入到磁盘。
- 如果是异常崩溃,InnoDB如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,那么就将它读取到内存,然后让redo日志更新其内容,更新完成后就会变成脏页,由内存刷盘。
redo log buffer是什么?是先修改内存还是先写redo日志?
-
当一个事务向表中插入多条记录的时候,每插入一条都会写在redo log buffer中
- 当执行commit的时候,会将redo log buffer刷新到redo日志文件(ib_logfileX)中。
order by是如何执行的?
order by用于排序操作,InnoDB中排序分为全字段排序和rowid排序。
-
在全字段排序中,MySQL会为每一个线程分配一块空间用于排序,当数据量太大的时候就会利用磁盘临时文件辅助排序。MySQL将需要排序的数据分成多份,每一份单独排序之后将存储在临时文件中的排序结果合并为一个大文件。全字段排序的性能取决于返回的字段数量。
- 不会把所有字段都放入sort_buffer。所以在sort buffer中进行排序之后还得回表查询。
为什么有时候相同的SQL操作却性能不同?
对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。
为什么有时候只查询一行数据也会很慢?
如果结果集长时间没有返回可能是在等待MDL锁,也有可能是等待Flush或者行锁。
为什么binlog cache是每个线程自己维护的,而redo log buffer是全局公用的?
-
一个事务的binlog是连续的,因此要整个事务完成之后一起写到文件里
- 而redo日志并没有这个要求,中间有日志可以写到redo log buffer中,其他事务提交的时候可以被一起写到磁盘中
事务执行期间如果发生崩溃,主备库是否会发生数据不一致?
如果这个时候binlog也在binlog cache中,redo log也在redo log buffer中,那么崩溃之后数据是一致的。
MySQL如何保障主备数据一致性的?
-
由于statement格式的SQL可能会导致数据不一致,而ROW格式的数据很占用空间。
- MySQL使用了折中的方案mixed。MySQL主动判断指定的SQL语句是否会发生数据不一致。
MySQL双Master架构如何解决循环复制问题?
-
规定两个数据库的server id必须不同,如果相同,那么不能是互为主备关系。一个备库接收到binlog并在重放的过程中生成与原binlog相同的server id。
- 每个库收到自己从自己主库发过来的日志先判断server id,如果与自己的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。
为什么还有kill不掉的语句?
MySQL中的kill有kill query+线程ID和kill connection+线程ID。与Linux相同,MySQL中kill的意思是发出停止信号而不是立即停止。
MySQL中为什么临时表可以重名?
-
内存表,指的是使用 Memory 引擎的表,建表语法是
create table … engine=memory
。这种表的数据都保存在内存里,系统重启的时候会被清空,但是表结构还在。 -
而临时表,可以使用各种引擎类型 。如果是使用 InnoDB 引擎或者 MyISAM 引擎的临时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用 Memory 引擎。
-
不同 session 的临时表是可以重名的,如果有多个 session 同时执行 join 优化,不需要担心表名重复导致建表失败的问题。由于不用担心线程之间的重名冲突,临时表经常会被用在复杂查询的优化过程中。
-
其中,分库分表系统的跨库查询就是一个典型的使用场景。 frm 文件放在临时文件目录下,文件名的后缀是 .frm ,前缀是
#sql{ 进程 id}_{ 线程 id}_序列号
。你可以使用select @@tmpdir
命令,来显示实例的临时文件目录。 - 而关于表中数据的存放方式,在不同的 MySQL 版本中有着不同的处理方式在 5.6 以及之前的版本里, MySQL 会在临时文件目录下创建一个相同前缀、以
.ibd
为后缀的文件,用来存放数据文件,而从 5.7 版本开始, MySQL 引入了一个临时文件表空间,专门用来存放临时文件的数据。因此,我们就不需要再创建 ibd 文件了。
自增主键为什么不是连续的?
-
MyISAM 引擎的自增值保存在数据文件中。
-
InnoDB 引擎的自增值,其实是保存在了内存里,并且到了 MySQL 8.0 版本后,才有了 “ 自增值持久化 ” 的能力。
-
MySQL5.7之前每次重启之后,都会去找到自增值的最大值。然后加一作为当前的自增值。
- MySQL8.0版本中,将自增值记录到了redo日志中,重启之后仍然依靠redo日志恢复重启之前的值。
如何快速的复制一张表?
- mysqldump
- 导出CSV
- 物理拷贝
十三、集群
(1)单Master+多Slave
在实际应用场景中,MySQL复制90%以上都是一个Master复制到一个或者多个Slave的架构模式,主要用于读压力比较大的应用的数据库端廉价扩展解决方案。因为只要Master和Slave的压力不是太大(尤其是Slave端压力)的话,异步复制的延时一般都很少很少。尤其是自从Slave端的复制方式改成两个线程处理之后,更是减小了Slave端的延时问题。
当slave增加到一定数量时,slave对master的负载以及网络带宽都会成为一个严重的问题
这种结构虽然简单,但是,它却非常灵活,足够满足大多数应用需求。一些建议:
(1) 不同的slave扮演不同的作用(例如使用不同的索引,或者不同的存储引擎);
(2) 用一个slave作为备用master,只进行复制;
(3) 用一个远程的slave,用于灾难恢复;
(2)主动模式的Master+Master
这样搭建复制环境之后,难道不会造成两台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结合起来,可以建立强大的高性能的数据库平台。但是,可以通过其它一些方式来模拟这种多主服务器的复制。
(3)主动被动模式的Master+Master
这是master-master结构变化而来的,它避免了M-M的缺点,实际上,这是一种具有容错和高可用性的系统。它的不同点在于其中一个服务只能进行只读操作。
(4)级联复制的Master+Slave+Slave
在有些应用场景中,可能读写压力差别比较大,读压力特别的大,一个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也会更多,同样可能造成延时较长的风险。
而如果我们通过分拆集群的方式来解决的话,可能就会要好很多了,分拆集群也需要更复杂的技术和更复杂的应用系统架构。
(5)带Slave的双主模式
这种结构的优点就是提供了冗余。在地理上分布的复制结构,它不存在单一节点故障问题,而且还可以将读密集型的请求放到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,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容;
- Master接收到来自Slave的IO进程的请求后,通过负责复制的IO进程根据请求信息读取制定日志指定位置之后的日志信息,返回给Slave 的IO进程。返回信息中除了日志所包含的信息之外,还包括本次返回的信息已经到Master端的bin-log文件的名称以及bin-log的位置;
-
Slave的IO进程接收到信息后,将接收到的日志内容依次添加到Slave端的relay-log文件的最末端,并将读取到的Master端的 bin-log的文件名和位置记录到master-info文件中,以便在下一次读取的时候能够清楚的告诉Master“我需要从某个bin-log的哪个位置开始往后的日志内容,请发给我”;
- Slave的Sql进程检测到relay-log中新增加了内容后,会马上解析relay-log的内容成为在Master端真实执行时候的那些可执行的内容,然后回放。
mysql replication单表或多表复制时需注意的几个问题:
-
主库和从库的数据库名必须相同;
-
主库和从库的复制可以精确到表,但是在需要更改主库或从库的数据结构时需要立刻重启slave;
-
不能在mysql配置文件里直接写入master的配置信息,需要用change master命令来完成;
-
指定replicate_do_db必须在my.cnf里配置,不能用change master命令来完成;
- 如果不及时清理,日积月累二进制日志文件可能会把磁盘空间占满,可以在配置文件里加上expire_logs_days=7,只保留最近7天的日志,建议当slave不再使用时,通过reset slave来取消relaylog;
(6)MMM集群模式
MMM(Master-Master replication managerfor Mysql,Mysql主主复制管理器)是一套灵活的脚本程序,基于perl实现,用来对mysql replication进行监控和故障迁移,并能管理mysql Master-Master复制的配置(同一时间只有一个节点是可写的)。
- mmm_mond:监控进程,负责所有的监控工作,决定和处理所有节点角色活动。此脚本需要在监管机上运行
- mmm_agentd:运行在每个mysql服务器上的代理进程,完成监控的探针工作和执行简单的远端服务设置。此脚本需要在被监管机上运行
- mmm_control:一个简单的脚本,提供管理mmm_mond进程的命令
mysql-mmm的监管端会提供多个虚拟IP(VIP),包括一个可写VIP,多个可读VIP,通过监管的管理,这些IP会绑定在可用mysql之上,当某一台mysql宕机时,监管会将VIP迁移至其他mysql。
在整个监管过程中,需要在mysql中添加相关授权用户,以便让mysql可以支持监理机的维护。授权的用户包括一个mmm_monitor用户和一个mmm_agent用户,如果想使用mmm的备份工具则还要添加一个mmm_tools用户。
- 优点:高可用性,扩展性好,出现故障自动切换(3秒内自动切换),对于主主同步,在同一时间只提供一台数据库写操作,保证的数据的一致性。多个Slave读的负载均衡。
- 缺点:Monitor节点是单点,可以结合Keepalived实现高可用。无法完全保证数据的一致性(在db1宕机过程中,一旦db2落后于db1,这时发生切换,db2变成了可写状态,数据的一致性就无法保证)。无论何时,只有一个数据库可写;db1宕机后,write VIP会指向db2,当db1恢复后,db1不会自动变成可写主,需要手动move_role 或者db2宕机。
MMM基于MySQL Replication做的扩展架构,主要用来监控mysql主主复制并做失败转移。其原理是将真实数据库节点的IP(RIP)映射为虚拟IP(VIP)集,在这个虚拟的IP集中,有一个专用于write的IP,多个用于read的IP,这个用于Write的VIP映射着。
数据库集群中的两台master的真实IP(RIP),以此来实现Failover的切换,其他read的VIP可以用来均衡读。
(7)MHA模式
MHA(Master High Availability)由日本DeNA公司youshimaton(现就职于Facebook公司)开发,是一套优秀的作为MySQL高可用性环境下故障切换和主从提升的高可用软件。在MySQL故障切换过程中,MHA能做到在0~30秒之内自动完成数据库的故障切换操作,并且在进行故障切换的过程中,MHA能在最大程度上保证数据的一致性,以达到真正意义上的高可用。某种意义上来说MHA架构是MMM架构的升级版,但是又缺少了vip的功能,一般会配合keepalived使用补完vip的功能。
优点:
- 自动监控Master故障转移、故障后节点之间的数据同步
- 不会有性能损耗,适用于任何存储引擎
- 具备自动数据补偿能力,在主库异常崩溃时能够最大程度的保证数据的一致性
- 可实现同城应用级别双活
- 最大程度上保证数据的一致性
缺点:
- MHA架构实现读写分离,最佳实践是在应用开发设计时提前规划读写分离事宜,在使用时设置两个连接池,即读连接池与写连接池,也可以选择折中方案即引入SQL Proxy。但无论如何都需要改动代码;
- 关于读负载均衡可以使用F5、LVS、HAPROXY或者SQL Proxy等工具,只要能实现负载均衡、故障检查及备升级为主后的读写剥离功能即可,建议使用LVS;
- MHA Manager Node 主要负责主库在crash时将bin log完整同步到slave库、监控主备库的状态及切换。
最大的问题做读写分离时需要改动代码,与开发耦合太高,不利于当前部署以及后期改造。
(8)MyCAT架构
基于阿里开源的Cobar产品而研发,Cobar的稳定性、可靠性、优秀的架构和性能以及众多成熟的使用案例使得MYCAT一开始就拥有一个很好的起点。一个彻底开源的,面向企业应用开发的大数据库集群。
支持的功能列表:
- 支持事务、ACID、可以替代MySQL的加强版数据库
- 一个可以视为MySQL集群的企业级数据库,用来替代昂贵的Oracle集群
- 一个融合内存缓存技术、NoSQL技术、HDFS大数据的新型SQL Server
- 结合传统数据库和新型分布式数据仓库的新一代企业级数据库产品
- 一个新颖的数据库中间件产品
- MYCAT监控:
- 支持对Mycat、Mysql性能监控
- 支持对Mycat的JVM内存提供监控服务
- 支持对线程的监控
- 支持对操作系统的CPU、内存、磁盘、网络的监控
优点:
- Mycat作为主数据库中间件,是与代码弱关联的,所以代码是不用修改的,使用Mycat后,连接数据库是不变的,默认端口是8066。连接方式和普通数据库一样。
- 自带监控,提供较全面的监控服务
- 可实现数据库的读写分离,在后端的主从复制数据库集群中,通过MYCAT配置,将前台的写操作路由到主数据库中,将读操作路由到从数据库上。
- MYCAT可以实现读写分离下的读操作负载均衡,将大量的读操作均衡到不同的从库上,主要出现在一主多从情形下。
- MYCAT可实现数据库的高可用,在数据库主节点可用的情况下,配置一台可写从节点,这两个节点都配置在MYCAT中,当主节点宕机时,MyCAT会自动将写操作路由到备用节点上,但并不支持在切换之后的继续主从同步。
- 当读写分离已经不能满足持续增加的访问量时,MYCAT可实现数据库的垂直拆分,将所有的数据库表按照模块划分,不同类型的表拆分到不同的数据库服务器。
- 随着业务量的增长,垂直拆分之后如果又出现了数据库性能问题,则需要进行水平切分,这就是俗称的分库分表。将数据量很大的表数据切分到不同的服务器库中,表结构是一样的,而使用MYCAT实现水平切分,对前端应用是完全透明的,不用调整前台逻辑。
缺点:
-
TPS性能低下
- 最近版本维护越来越少了
Mycat也并不是配置以后,就能完全解决分表分库和读写分离问题。Mycat配合数据库本身的复制功能,可以解决读写分离的问题,但是针对分表分库的问题,不是完美的解决。或者说,至今为止,业界没有完美的解决方案。
分表分库写入能完美解决,但是,不能完美解决主要是联表查询的问题,Mycat支持两个表联表的查询,多余两个表的查询不支持。 其实,很多数据库中间件关于分表分库后查询的问题,都是需要自己实现的,而且节本都不支持联表查询,Mycat已经算做地非常先进了。分表分库的后联表查询问题,需要通过合理数据库设计来避免。这是一套比较好的数据库集群方案,值得考虑。
(9)InnoDB Cluster
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单点问题好需要新增组件,如没有其他更好的方案暂考虑该方案。
(10)Galera Cluster for MySQL
Galera Cluster是由Codership开发的MySQL多主集群,包含在MariaDB中,同时支持Percona xtradb、MySQL,是一个易于使用的高可用解决方案,在数据完整性、可扩展性及高性能方面都有可接受的表现。图1所示为一个三节点Galera 集群,三个MySQL实例是对等的,互为主从,这被称为多主(multi-master)架构。当客户端读写数据时,可连接任一MySQL实例。对于读操作,从每个节点读取到的数据都是相同的。对于写操作,当数据写入某一节点后,集群会将其同步到其它节点。这种架构不共享任何数据,是一种高冗余架构。
Galera集群具有以下特点:
- 多主架构:真正的多主多活群集,可随时对任何节点进行读写。
- 同步复制:集群不同节点之间数据同步,某节点崩溃时没有数据丢失。
- 数据一致:所有节点保持相同状态,节点之间无数据分歧。
- 并行复制:重放支持多线程并行执行以获得更好的性能。
- 故障转移:故障节点本身对集群的影响非常小,某节点出现问题时无需切换操作,因此不需要使用VIP,也不会中断服务。
- 自动克隆:新增节点会自动拉取在线节点的数据,最终集群所有节点数据一致,而不需要手动备份恢复。
- 应用透明:提供透明的客户端访问,不需要对应用程序进行更改。
Galera集群复制要求数据库系统支持事务,因此仅支持MySQL的Innodb存储引擎,并且多主模式下只能使用可重复读隔离级别。
主从同步:
不同于MySQL原生的主从异步复制,Galera采用的是多主同步复制。
异步复制中,主库将数据更新传播给从库后立即提交事务,而不论从库是否成功读取或重放数据变化。这种情况下,在主库事务提交后的短时间内,主从库数据并不一致。同步复制时,主库的单个更新事务需要在所有从库上同步更新。换句话说,当主库提交事务时,集群中所有节点的数据保持一致。
相对于异步复制,同步复制的优点主要体现在以下几方面:
- 数据一致:同步复制保证了整个集群的数据一致性,无论何时在任何节点执行相同的select查询,结果都一样。
- 高可用性:由于所有节点数据一致,单个节点崩溃不需要执行复杂耗时的故障切换,也不会造成丢失数据或停止服务。
- 性能改进:同步复制允许在集群中的所有节点上并行执行事务,从而提高读写性能。
当然,同步复制的缺点也显而易见,这主要源于其实现方式。同步复制协议通常使用两阶段提交或分布式锁协调不同节点的操作。假设集群有n个节点,每秒处理o个操作,每个操作中包含t个事务,则每秒将在网络中产生n * o * t
条消息。这意味着随着节点数量的增加,事务冲突和死锁的概率将呈指数级增加。这也是MySQL缺省使用异步复制的主要原因。
为解决传统同步复制的问题,现已提出多种数据库同步复制的替代方法。除理论外,一些原型实现也显示出了很大的希望,如以下重要改进:
- 组通信(Group Communication):定义了数据库节点间的通信模式,保证复制数据的一致性。
- 写集(Write-sets):将多个并发数据库写操作更新的数据,绑定到单个写集消息中,提高节点并行性。MySQL中用WriteSet对象来记录每行记录,从源码来看WriteSet就是每条记录hash后的值(必须开启ROW格式的二进制日志)。
- 数据库状态机:数据库站点本地处理只读事务。更新事务首先在本地的“影子拷贝(shallow copies)”上执行,然后作为读集广播到其它数据库站点进行验证并提交。
- 事务重排序:此操作在数据库提交事务并将其广播到其它站点之前重新排序事务,增加成功通过验证的事务数。
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集群使用以下方式处理状态更改:
- 一个节点的数据库中发生状态更改
- wsrep钩子将更改转换为写集
- dlopen函数连接wsrep钩子与Galera复制插件
- Galera复制插件处理写集验证,并将更改复制到集群中的其它节点
-
Galera复制插件:实现写集复制功能的核心模块
- 组通信插件:Galera集群的组通信系统(Group Communication System,GCS),如GComm
全局事务ID(global transaction id,GTID)
在MySQL社区中,GTID的概念并不新鲜,MySQL中的GTID由Master生成,是用于标记唯一事务并通过ID定位binlog位置的一种手段,从而有效解决了级联复制等场景中的各种问题。
对Galera Cluster而言,复制不基于binlog,而是通过Galera复制插件来保障。Galera的GTID同样也标记事务唯一性,wsrep api使用GTID识别状态更改。
GTID由两部分组成:
- 状态UUID:表示当前状态的唯一ID,可以简单认为是集群的一个唯一标识符。
- 顺序号:一个64位有符号整数,表示事务在Galera Cluster所有节点中的序号。
Galera复制插件
Galera复制插件实现wsrep api,作为wsrep provider运行。Galera复制插件由以下组件构成:
- 验证层:该层准备写集,并检测本机事务,以及从其它节点同步来的事务是否可以提交。
- 复制层:该层的工作包含组通信和并行复制两方面。组通信负责与其它节点同步写集,并为事务分配全局唯一的GTID。并行复制实现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命令时,在实际提交之前,对数据库所做的更改都将被收集到一个写集中,写集中包含事务信息和所更改行的主键。然后,数据库将此写集发送到所有其它节点。节点用写集中的主键与当前节点中未完成事务的所有写集(不仅包括当前节点其它事务产生的写集,还包括其它节点传送过来的写集)的主键相比较,确定节点是否可以提交事务。同时满足以下三个条件则验证失败(存在冲突):
- 两个事务来源于不同节点
- 两个事务包含相同的主键
- 老事务对新事务不可见,即老事务未提交完成。新老事务的划定依赖于全局事务总序,即GTID
验证失败后,节点将删除写集,集群将回滚原始事务。对于所有的节点都是如此,每个节点单独进行验证。因为所有节点都以相同的顺序接收事务,它们对事务的结果都会做出相同的决定,要么全成功,要么都失败。成功后自然就提交了,所有的节点又会重新达到数据一致的状态。节点之间不交换“是否冲突”的信息,各个节点独立异步处理事务。由此可见,Galera本身的数据也不是严格同步的,很明显在每个节点上的验证是异步的,这也就是前面提到的“虚拟同步”。
最后,启动事务的节点可以通知客户端应用程序是否提交了事务。
状态转移
当一个新节点加入集群时,数据将从集群复制到这个节点,这是一个全自动的过程,Galera将此称为状态转移。前面介绍Galera架构时曾提到,wsrep api将集群中的数据改变视为状态改变,因此这里将数据同步称作状态转移也就不足为怪了。Galera集群中有两种状态转移方法:
- 状态快照传输(State Snapshot Transfers,SST),也就是通常所说的全量数据同步。
- 增量状态转移(Incremental State Transfers,IST),指增量数据同步。
当有新节点加入时,集群会选择出一个捐献者(Donor)节点为新节点提供数据,这点与MySQL组复制类似。
状态快照传输:
- 新节点加入集群时会启动状态快照传输(SST),将其数据与集群同步。Galera支持rsync、rsync_-wan、xtrabackup、mysqldump四种状态快照传输方法,由系统变量wsrep_sst_method指定,缺省为rsync。
- rsync、rsync_-wan、xtrabackup三种方法是物理备份,将数据文件直接从捐献者服务器复制到新节点服务器,并在传输后初始化接收服务器,其中xtrabackup方式可实现捐赠者无阻塞数据同步。这些方法比mysqldump快很多。
- mysqldump方法是逻辑备份,要求用户手动初始化接收服务器,并在传输之前准备好接受连接。这是一种阻塞方法,在传输期间,捐赠节点变为只读。mysqldump是状态快照传输最慢的方法,不建议在生产环境使用。
增量状态转移:
增量状态转移(IST)只向新节点发送它所缺失的事务。使用IST需要满足两个先决条件:
- 新加入节点的状态UUID与集群中的节点一致
- 新加入节点所缺失的写集在捐助者的写集缓存中存在。这点很好理解,类比MySQL的binlog,如果所需的binlog文件缺失,是无法做增量备份恢复的
满足这些条件时,捐助节点单独传输缺失的事务,并按顺序重放它们,直到新节点赶上集群。例如,假设集群中有一个节点落后于集群。
集群上的捐助节点从加入节点接收状态转移请求。它检查自身写集缓存中的序列号197223。如果该序号在写集缓存中不可用,则会启动SST。否则捐助节点将从197223到201913的提交事务发送到新加入节点。增量状态传输的优点是可以显著加快节点合并到集群的速度。另外,这个过程对捐赠者来说是非阻塞的。
增量状态传输最重要的参数是捐助节点上的gcache.size,它控制分配多少系统内存用于缓存写集。可用空间越大,可以存储的写集越多。可以存储的写集越多,通过增量状态传输可以弥合的事务间隙就越大。另一方面,如果写集缓存远大于数据库大小,则增量状态传输开始时的效率低于发送状态快照。
写集缓存(gcache)
Galera群集将写集存储在一个称为gcache的特殊缓存中。gcache使用三种类型的存储:
- 永久内存存储(Permanent In-Memory Store):写集使用操作系统的默认内存分配器进行分配,永久存储于物理内存中。gcache.keep_pages_size参数指定保留的内存页总大小,缺省值为0。由于硬件的限制,默认情况下是禁用的。
- 永久环缓冲区文件(Permanent Ring-Buffer File):写集在缓存初始化期间预分配到磁盘,生成一个内存映射文件,用作写集存储。文件目录和文件名分别由gcache.dir和gcache.name参数指定。文件大小由gcache.size参数指定,缺省值为128MB。
- 按需页存储(On-Demand Page Store):根据需要在运行时将写集分配给内存映射页文件。大小由gcache.page_size参数指定,缺省值为128M,可随写集自动变大。页面存储的大小受可用磁盘空间的限制。默认情况下,Galera会在不使用时删除页面文件,用户可以设置要保留的页面文件总大小(gcache.size)。当所有其它存储被禁用时,磁盘上至少保留一个页面的文件。
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系统变量查看节点的当前状态。
- 节点启动并建立到主组件( Primary Component,PC)的连接。由于网络问题群集可能被拆分为多个部分,为避免数据差异或脑裂,此时只能有一部分可以修改数据,这部分称为主组件。
- 当节点成功执行状态传输请求时,它将开始缓存写集。
- 节点接收状态快照传输(SST)。它将拥有所有集群数据,并开始应用缓存的写集。
- 节点完成对群集的追赶。节点将mysql状态变量wsrep_ready设置为值1,现在允许该节点处理事务。
- 节点接收状态传输请求,成为捐赠者。节点缓存它无法应用的所有写集。
- 节点完成对新加入节点的状态传输。
节点状态和流控:
Galera集群根据节点状态实现多种形式的流控以保证数据一致性。有四种主要流控类型:
- 无流控(No Flow Control):当节点处于Open或Primary状态时,此流控类型生效。此时节点还不被视为集群的一部分,不允许这些节点复制、应用或缓存任何写集。
- 写集缓存(Write-set Caching):当节点处于Joiner和Donor状态时,此流控类型生效。节点在此状态下不能应用任何写集,必须缓存它们以备以后使用。
- 赶上(Catching Up):此流控类型在节点处于Joined状态时生效。处于此状态的节点可以应用写集。这里的流控确保节点最终能够追赶上集群。由于应用写集通常比处理事务快几倍,处于这种状态的节点几乎不会影响集群性能。
- 集群同步(Cluster Sync):此流控类型在节点处于Synced状态时生效。当节点进入此状态时,流控将尝试将接收队列保持最小。
单节点故障与恢复:
当一个节点因为硬件、软件、网络等诸多原因与集群失去联系时,都被概括为节点故障。从集群的角度看,主组件看不到出问题的节点,它将会认为该节点失败。从故障节点本身的角度来看,假设它没有崩溃,那么唯一的迹象是它失去了与主组件的连接。可以通过轮询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),以及切分后对数据的定位、整合。数据切分就是将数据分散存储到多个数据库中,使得单一数据库中的数据量变小,通过扩充主机的数量缓解单一数据库的性能问题,从而达到提升数据库操作性能的目的。
数据切分根据其切分类型,可以分为两种方式:垂直(纵向)切分和水平(横向)切分。
(1)垂直切分
垂直分库就是根据业务耦合性,将关联度低的不同表存储在不同的数据库。做法与大系统拆分为多个小系统类似,按业务分类进行独立划分。与"微服务治理"的做法相似,每个微服务使用单独的一个数据库。
垂直分表是基于数据库中的"列"进行,某个表字段较多,可以新建一张扩展表,将不经常用或字段长度较大的字段拆分出去到扩展表中。在字段很多的情况下(例如一个大表有100多个字段),通过"大表拆小表",更便于开发与维护,也能避免跨页问题,MySQL底层是通过数据页存储的,一条记录占用空间过大会导致跨页,造成额外的性能开销。另外数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率更高,减少了磁盘IO,从而提升了数据库性能。
优点:
- 解决业务系统层面的耦合,业务清晰
- 与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等
- 高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈
缺点:
- 部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度
- 分布式事务处理复杂
- 依然存在单表数据量过大的问题(需要水平切分)
(2)水平切分
当一个应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平切分了。
水平切分分为库内分表和分库分表,是根据表内数据内在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。
库内分表只解决了单一表数据量过大的问题,但没有将表分布到不同机器的库上,因此对于减轻MySQL数据库的压力来说,帮助不是很大,大家还是竞争同一个物理机的CPU、内存、网络IO,最好通过分库分表来解决。
优点:
- 不存在单库数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力
- 应用端改造较小,不需要拆分业务模块
缺点:
- 跨分片的事务一致性难以保证
- 跨库的join关联查询性能较差
- 数据多次扩展难度和维护量极大
水平切分后同一张表会出现在多个数据库/表中,每个库/表的内容不同。几种典型的数据分片规则为:
根据数值范围:
按照时间区间或ID区间来切分。例如:按日期将不同月甚至是日的数据分散到不同的库中;将userId为1~9999的记录分到第一个库,10000~20000的分到第二个库,以此类推。某种意义上,某些系统中使用的"冷热数据分离",将一些使用较少的历史数据迁移到其他库中,业务功能上只提供热点数据的查询,也是类似的实践。
优点:
- 单表大小可控
- 天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移
- 使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题。
缺点:
- 热点数据成为性能瓶颈。连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询
根据数值取模:
一般采用hash取模mod的切分方式,例如:将 Customer 表根据 cusno 字段切分到4个库中,余数为0的放到第一个库,余数为1的放到第二个库,以此类推。这样同一个用户的数据会分散到同一个库中,如果查询条件带有cusno字段,则可明确定位到相应库去查询。
优点:
- 数据分片相对比较均匀,不容易出现热点和并发访问的瓶颈
缺点:
- 后期分片集群扩容时,需要迁移旧的数据(使用一致性hash算法能较好的避免这个问题)
- 容易面临跨分片查询的复杂问题。比如上例中,如果频繁用到的查询条件中不带cusno时,将会导致无法定位数据库,从而需要同时向4个库发起查询,再在内存中合并数据,取最小集返回给应用,分库反而成为拖累。
(3)分库分表带来的问题
事务一致性问题
当更新内容同时分布在不同库中,不可避免会带来跨库事务问题。跨分片事务也是分布式事务,没有简单的方案,一般可使用"XA协议"和"两阶段提交"处理。
分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间。导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平扩展的枷锁。
对于那些性能要求很高,但对一致性要求不高的系统,往往不苛求系统的实时一致性,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误后立即回滚的方式不同,事务补偿是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等等。事务补偿还要结合业务系统来考虑。
跨节点关联查询 join 问题
切分之前,系统中很多列表和详情页所需的数据可以通过sql join来完成。而切分之后,数据可能分布在不同的节点上,此时join带来的问题就比较麻烦了,考虑到性能,尽量避免使用join查询。解决这个问题的一些方法
- 全局表,也可看做是"数据字典表",就是系统中所有模块都可能依赖的一些表,为了避免跨库join查询,可以将这类表在每个数据库中都保存一份。这些数据通常很少会进行修改,所以也不担心一致性的问题。
- 字段冗余,这种方法适用场景也有限,比较适用于依赖字段比较少的情况。而冗余字段的数据一致性也较难保证,就像上面订单表的例子,买家修改了userName后,是否需要在历史订单中同步更新呢?这也要结合实际业务场景进行考虑。一种典型的反范式设计,利用空间换时间,为了性能而避免join查询。
- 数据组装,在系统层面,分两次查询,第一次查询的结果集中找出关联数据id,然后根据id发起第二次请求得到关联数据。最后将获得到的数据进行字段拼装。
- ER分片,关系型数据库中,如果可以先确定表之间的关联关系,并将那些存在关联关系的表记录存放在同一个分片上,那么就能较好的避免跨分片join问题。在1:1或1:n的情况下,通常按照主表的ID主键切分。
跨节点分页、排序、函数问题
跨节点多库进行查询时,会出现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号段,用完之后再去数据库获取,可以大大减轻数据库的压力。
- Snowflake分布式自增ID算法,Twitter的snowflake算法解决了分布式系统生成全局ID的需求,生成64位的Long型数字。强依赖机器时钟,如果时钟回拨,则可能导致生成ID重复。毫秒数在高位,生成的ID整体上按时间趋势递增;不依赖第三方系统,稳定性和效率较高,理论上QPS约为409.6w/s(1000*2^12),并且整个分布式系统内不会产生ID碰撞;可根据自身业务灵活分配bit位。
数据迁移、扩容问题
当业务高速发展,面临性能和存储的瓶颈时,才会考虑分片设计,此时就不可避免的需要考虑历史数据迁移的问题。一般做法是先读出历史数据,然后按指定的分片规则再将数据写入到各个分片节点中。此外还需要根据当前的数据量和QPS,以及业务发展的速度,进行容量规划,推算出大概需要多少分片(一般建议单个分片上的单表数据量不超过1000W)。
如果采用数值范围分片,只需要添加节点就可以进行扩容了,不需要对分片数据迁移。如果采用的是数值取模分片,则考虑后期的扩容问题就相对比较麻烦。
(4)分页查询
分页拉取数据的需求:
- 微信消息过多时,拉取第N页消息
- 京东下单过多时,拉取第N页订单
- 浏览58同城,查看第N页帖子
这些业务场景对应的消息表,订单表,帖子表分页拉取需求有这样一些特点:
- 有一个业务主键id, 例如msg_id, order_id, tiezi_id
- 分页排序是按照非业务主键id来排序的,业务中经常按照时间time来排序order by
在数据量不大时,可以通过在排序字段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代替
十六、大表优化方案
(1)字段
- 尽量使用 TINYINT、 SMALLINT、 MEDIUM_INT 作为整数类型而非 INT,如果非负则加上 UNSIGNED
- VARCHAR 的长度只分配真正需要的空间
- 使用枚举或整数代替字符串类型
- 尽量使用 TIMESTAMP 而非 DATETIME
- 单表不要有太多字段,建议在 20 以内
- 避免使用 NULL 字段,很难查询优化且占用额外索引空间
- 用整型来存 IP
(2)索引
- 索引并不是越多越好,要根据查询有针对性的创建,考虑在 WHERE 和 ORDER BY
- 命令上涉及的列建立索引,可根据 EXPLAIN 来查看是否用了索引还是全表扫描
- 应尽量避免在 WHERE 子句中对字段进行 NULL 值判断,否则将导致引擎放弃使用索引而进行全表扫描
- 值分布很稀少的字段不适合建索引,例如"性别"这种只有两三个值的字段
- 字符字段只建前缀索引
- 字符字段最好不要做主键
- 不用外键,由程序保证约束
- 尽量不用 UNIQUE,由程序保证约束
- 使用多列索引时主意顺序和查询条件保持一致,同时删除不必要的单列索引
(3)查询SQL
- 可通过开启慢查询日志来找出较慢的 SQL
- 不做列运算:SELECT id WHERE age+1=10,任何对列的操作都将导致表扫描,它包括数据库教程函数、计算表达式等等,查询时要尽可能将操作移至等号右边
- sql 语句尽可能简单:一条 sql 只能在一个 cpu 运算;大语句拆小语句,减少锁时间;一条大sql 可以堵死整个库
- 不用 SELECT *
- OR 改写成 IN:OR 的效率是 n 级别, IN 的效率是 log(n) 级别,IN 的个数建议控制在 200 以内
- 不用函数和触发器,在应用程序实现
- 避免 %xxx 式查询
- 少用 JOIN
- 使用同类型进行比较,比如用 '123' 和 '123' 比, 123 和 123 比
- 尽量避免在 WHERE 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描
- 对于连续数值,使用 BETWEEN 不用 IN:SELECT id FROM t WHERE num BETWEEN 1 AND 5
- 列表数据不要拿全表,要使用 LIMIT 来分页,每页数量也不要太大
(4)引擎
目前广泛使用的是 MyISAM 和 InnoDB 两种引擎:
MyISAM:
MyISAM 引擎是 MySQL 5.1 及之前版本的默认引擎,它的特点是:
- 不支持行锁,读取时对需要读到的所有表加锁,写入时则对表加排它锁
- 不支持事务
- 不支持外键
- 不支持崩溃后的安全恢复
- 在表有读取查询的同时,支持往表中插入新纪录
- 支持 BLOB 和 TEXT 的前 500 个字符索引,支持全文索引
- 支持延迟更新索引,极大提升写入性能
- 对于不会进行修改的表,支持压缩表,极大减少磁盘空间占用
InnoDB:
InnoDB 在 MySQL 5.5 后成为默认索引,它的特点是:
- 支持行锁,采用 MVCC 来支持高并发
- 支持事务
- 支持外键
- 支持崩溃后的安全恢复
- 不支持全文索引(5.6.4之后版本逐渐开始支持)
总体来讲,MyISAM 适合 SELECT 密集型的表,而 InnoDB 适合 INSERT 和 UPDATE 密集型的表
(5)系统调优参数
可以使用下面几个工具来做基准测试:
- sysbench:一个模块化,跨平台以及多线程的性能测试工具
- iibench-mysql:基于 Java 的 MySQL/Percona/MariaDB 索引进行插入性能测试工具
- tpcc-mysql:Percona 开发的 TPC-C 测试工具
具体的调优参数内容较多,具体可参考官方文档,这里介绍一些比较重要的参数:
- backlog:backlog 值指出在 MySQL 暂时停止回答新请求之前的短时间内多少个请求可以被存在堆栈中。也就是说,如果 MySql 的连接数据达到 maxconnections 时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即 backlog,如果等待连接的数量超过back_log,将不被授予连接资源。可以从默认的 50 升至 500
- wait_timeout:数据库连接闲置时间,闲置连接会占用内存资源。可以从默认的 8 小时减到半小时
- maxuserconnection:最大连接数,默认为 0 无上限,最好设一个合理上限
- thread_concurrency:并发线程数,设为 CPU 核数的两倍
- skipnameresolve:禁止对外部连接进行 DNS 解析,消除 DNS 解析时间,但需要所有远程主机用 IP 访问
- keybuffersize:索引块的缓存大小,增加会提升索引处理速度,对 MyISAM 表性能影响最大。对于内存 4G 左右,可设为 256M 或 384M,通过查询
show status like'key_read%'
,保证 key_reads / key_read_requests 在 0.1% 以下最好 - innodbbufferpool_size:缓存数据块和索引块,对 InnoDB 表性能影响最大。通过查询
show status like'Innodb_buffer_pool_read%'
,保证(Innodb_buffer_pool_read_requests – Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests
越高越好 - innodbadditionalmempoolsize:InnoDB 存储引擎用来存放数据字典信息以及一些内部数据结构的内存空间大小,当数据库对象非常多的时候,适当调整该参数的大小以确保所有数据都能存放在内存中提高访问效率,当过小的时候,MySQL 会记录 Warning 信息到数据库的错误日志中,这时就需要该调整这个参数大小
- innodblogbuffer_size:InnoDB 存储引擎的事务日志所使用的缓冲区,一般来说不建议超过
32MB - querycachesize:缓存 MySQL 中的 ResultSet,也就是一条 SQL 语句执行的结果集,所以仅仅只能针对 select 语句。当某个表的数据有任何任何变化,都会导致所有引用了该表的select 语句在 Query Cache 中的缓存数据失效。所以,当我们的数据变化非常频繁的情况下,使用 Query Cache 可能会得不偿失。根据命中率
(Qcache_hits / (Qcache_hits + Qcache_inserts) * 100))
进行调整,一般不建议太大,256MB 可能已经差不多了,大型的配置型静态数据可适当调大. 可以通过命令show status like'Qcache_%'
查看目前系统 Query Catch 使用大小 - readbuffersize:MySql 读入缓冲区大小。对表进行顺序扫描的请求将分配一个读入缓冲区,MySql 会为它分配一段内存缓冲区。如果对表的顺序扫描请求非常频繁,可以通过增加该变量值以及内存缓冲区大小提高其性能
- sortbuffersize:MySql 执行排序使用的缓冲大小。如果想要增加 ORDER BY 的速度,首先看是否可以让 MySQL 使用索引而不是额外的排序阶段。如果不能,可以尝试增加 sortbuffersize 变量的大小
- readrndbuffer_size:MySql 的随机读缓冲区大小。当按任意顺序读取行时(例如,按照排序顺序),将分配一个随机读缓存区。进行排序查询时,MySql 会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,如果需要排序大量数据,可适当调高该值。但 MySql 会为每个客户连接发放该缓冲空间,所以应尽量适当设置该值,以避免内存开销过大
- record_buffer:每个进行一个顺序扫描的线程为其扫描的每张表分配这个大小的一个缓冲区。如果你做很多顺序扫描,可能想要增加该值
- threadcachesize:保存当前没有与连接关联但是准备为后面新的连接服务的线程,可以快速响应连接的线程请求而无需创建新的
- tablecache:类似于 threadcache_size,但用来缓存表文件,对 InnoDB 效果不大,主要用于 MyISAM
(6)表分区
分区的好处是:
- 可以让单表存储更多的数据
- 分区表的数据更容易维护,可以通过清楚整个分区批量删除大量数据,也可以增加新的分区来支持新插入的数据。另外,还可以对一个独立分区进行优化、检查、修复等操作
- 部分查询能够从查询条件确定只落在少数分区上,速度会很快
- 分区表的数据还可以分布在不同的物理设备上,从而高效利用多个硬件设备
- 可以使用分区表赖避免某些特殊瓶颈,例如 InnoDB 单个索引的互斥访问、ext3 文件系统的
inode 锁竞争 - 可以备份和恢复单个分区
分区的限制和缺点:
- 一个表最多只能有 1024 个分区
- 如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来
- 分区表无法使用外键约束
- NULL 值会使分区过滤无效
- 所有分区必须使用相同的存储引擎
分区的类型:
- RANGE 分区:基于属于一个给定连续区间的列值,把多行分配给分区
- LIST 分区:类似于按 RANGE 分区,区别在于 LIST 分区是基于列值匹配一个离散值集合中的某个值来进行选择
- HASH 分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含 MySQL 中有效的、产生非负整数值的任何表达式
- KEY 分区:类似于按 HASH 分区,区别在于 KEY 分区只支持计算一列或多列,且 MySQL 服务器提供其自身的哈希函数。必须有一列或多列包含整数值
(7)分库分表中间件选型
十七、查询优化
(1)COUNT函数
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(expr) ,返回SELECT语句检索的行中expr的值不为NULL的数量。结果是一个BIGINT值。
- 如果查询结果没有命中任何记录,则返回0
- 但是,值得注意的是,
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(*)
慢。
最后
以上就是谦让云朵为你收集整理的MySQL高手进阶指南一、后台线程二、InnoDB内存布局三、InnoDB关键特性四、日志五、检查点六、LRU七、存储引擎对比八、锁机制九、索引十、数据类型十一、灾备十二、面试题十三、集群十四、分库分表十五、连接查询十六、大表优化方案十七、查询优化的全部内容,希望文章能够帮你解决MySQL高手进阶指南一、后台线程二、InnoDB内存布局三、InnoDB关键特性四、日志五、检查点六、LRU七、存储引擎对比八、锁机制九、索引十、数据类型十一、灾备十二、面试题十三、集群十四、分库分表十五、连接查询十六、大表优化方案十七、查询优化所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复