接上一篇:MySQL这一章就够了(一)

redo log

redo log通常是重做日志(物理日志),记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。

redo log不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo 中。具体 的落盘策略可以进行配置 。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。RedoLog是为了实现事务的持久性而出现的产物。

redo log日志存在的原因:

redolog是用来做崩溃恢复使用的,这种崩溃恢复不需要我们人为的参与,MySQL自己内部自己实现了这种崩溃恢复的功能,我们只管享受这种功能给我们带来的服务即可,这种服务给我们的感受就是:MySQL数据库异常宕机的时候,重启服务之后,数据库中之前提交的记录都不会丢失数据仍然可以正常恢复,不管这种提交的记录是否已经更新到具体的表所对应的磁盘page也中。

那么MySQL内部在实现崩溃恢复的功能时,到底是如何实现的呢? 举例来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到redo log里面(redolog日志是循环追加写的,属于顺序IO,记录的速度在某些程度上可以和内存相媲美),并更新内存,这个时候更新就算完成了。MySQL在崩溃恢复的时候,会从记下来的redolog中找到已经提交的更改内容,所以不会担心MySQL异常重启后,数据的丢失。

InnoDB 引擎会在适当的时候,将这redolog中记录的操作更新到表中数据对应的page页所在的物理磁盘上,而这个更新往往是在MySQL服务比较空闲的时候去刷新到磁盘,此时才是真正的把更新的数据内容刷新到对应的page页中。而这个更新redolog日志中的内容到真正的表数据对应的page页的刷盘操作通常比费时的,需要从磁盘中找到对应的page页,还涉及到分页或合并的各种操作,属于随机IO写入性能太差,所以MySQL在执行更新操作的时候,并没有直接去更新真正的数据页中的内容,而只是更新了缓存和记录了redolog日志。先写日志,再写磁盘,这就是很多软件在提高写的性能的时候所使用的WAL(write ahead logging)预写日志的功能。

MySQL的这种崩溃恢复的功能,这就是我们经常所说的crash-safe,而实现这个crash-safe功能的主要组件就是redolog。因为只有innodb存储引擎才有这个特有的日志,所有只有innodb才支持这种崩溃恢复数据不丢失的特性,这也是为什么innodb存储引擎替代myiasm存储引擎成为主流默认的存储引擎的原因之一。

在最早的时候MySQL里并没有InnoDB引擎。MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,binlog日志只能用于归档。而InnoDB是另一个公司以插件形式引入MySQL的,既然只依靠binlog是没有crash-safe能力的,所以InnoDB使用另外一套日志系统,也就是redo log来实现crash-safe能力。这就是为什么会有relaylog日志产生的原因。

redo log和二进制日志的区别

redo log不是二进制日志。虽然二进制日志中也记录了innodb表的很多操作,**也能实现重做的功能,**但是它们之间有很大区别。

  1. 二进制日志是在存储引擎的上层产生的,不管是什么存储引擎,对数据库进行了修改都会产生二进制日志。而redo log是innodb层产生的,只记录该存储引擎中表的修改。并且二进制日志先于redo log被记录。具体的见后文group commit小结。
  2. 二进制日志记录操作的方法是逻辑性的语句。即便它是基于行格式的记录方式,其本质也还是逻辑的SQL设置,如该行记录的每列的值是多少。而redo log是在物理格式上的日志,它记录的是数据库中每个页的修改。
  3. 二进制日志只在每次事务提交的时候一次性写入缓存中的日志"文件"(对于非事务表的操作,则是每次执行语句成功后就直接写入)。而redo log在数据准备修改前写入缓存中的redo log中,然后才对缓存中的数据执行修改操作;而且保证在发出事务提交指令时,先向缓存中的redo log写入日志,写入完成后才执行提交动作。
  4. 因为二进制日志只在提交的时候一次性写入,所以二进制日志中的记录方式和提交顺序有关,且一次提交对应一次记录。而redo log中是记录的物理页的修改,redo log文件中同一个事务可能多次记录,最后一个提交的事务记录会覆盖所有未提交的事务记录。例如事务T1,可能在redo log中记录了 T1-1,T1-2,T1-3,T1* 共4个操作,其中 T1* 表示最后提交时的日志记录,所以对应的数据页最终状态是 T1* 对应的操作结果。而且redo log是并发写入的,不同事务之间的不同版本的记录会穿插写入到redo log文件中,例如可能redo log的记录方式如下: T1-1,T1-2,T2-1,T2-2,T2*,T1-3,T1* 。
  5. 事务日志记录的是物理页的情况,它具有幂等性,因此记录日志的方式极其简练。幂等性的意思是多次操作前后状态是一样的,例如新插入一行后又删除该行,前后状态没有变化。而二进制日志记录的是所有影响数据的操作,记录的内容较多。例如插入一行记录一次,删除该行又记录一次。

redo log的基本概念

redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。

在概念上,innodb通过***force log at commit***机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化。

为了确保每次日志都能写入到事务日志文件中,在每次将log buffer中的日志写入日志文件的过程中都会调用一次操作系统的fsync操作(即fsync()系统调用)。因为MariaDB/MySQL是工作在用户空间的,MariaDB/MySQL的log buffer处于用户空间的内存中。要写入到磁盘上的log file中(redo:ib_logfileN文件,undo:share tablespace或.ibd文件),中间还要经过操作系统内核空间的os buffer,调用fsync()的作用就是将OS buffer中的日志刷到磁盘上的log file中。

也就是说,从redo log buffer写日志到磁盘的redo log file中,过程如下:

在此处需要注意一点,一般所说的log file并不是磁盘上的物理日志文件,而是操作系统缓存中的log file,官方手册上的意思也是如此(例如:With a value of 2, the contents of the InnoDB log buffer are written to the log file after each transaction commit and the log file is flushed to disk approximately once per second)。但说实话,这不太好理解,既然都称为file了,应该已经属于物理文件了。所以在本文后续内容中都以os buffer或者file system buffer来表示官方手册中所说的Log file,然后log file则表示磁盘上的物理日志文件,即log file on disk。

另外,之所以要经过一层os buffer,是因为open日志文件的时候,open没有使用O_DIRECT标志位,该标志位意味着绕过操作系统层的os buffer,IO直写到底层存储设备。不使用该标志位意味着将日志进行缓冲,缓冲到了一定容量,或者显式fsync()才会将缓冲中的刷到存储设备。使用该标志位意味着每次都要发起系统调用。比如写abcde,不使用o_direct将只发起一次系统调用,使用o_object将发起5次系统调用。

MySQL支持用户自定义在commit时如何将log buffer中的日志刷log file中。这种控制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有3种值:0、1、2,默认为1。但注意,这个变量只是控制commit动作是否刷新log buffer到磁盘。

  • 当设置为1的时候,事务每次提交都会将log buffer中的日志写入os buffer并调用fsync()刷到log file on disk中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
  • 当设置为0的时候,事务提交时不会将log buffer中日志写入到os buffer,而是每秒写入os buffer并调用fsync()写入到log file on disk中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
  • 当设置为2的时候,每次提交都仅写入到os buffer,然后是每秒调用fsync()将os buffer中的日志写入到log file on disk。

注意,有一个变量 innodb_flush_log_at_timeout 的值为1秒,该变量表示的是刷日志的频率,很多人误以为是控制 innodb_flush_log_at_trx_commit 值为0和2时的1秒频率,实际上并非如此。测试时将频率设置为5和设置为1,当 innodb_flush_log_at_trx_commit 设置为0和2的时候性能基本都是不变的。关于这个频率是控制什么的,在后面的"刷日志到磁盘的规则"中会说。

在主从复制结构中,要保证事务的持久性和一致性,需要对日志相关变量设置为如下:

  • 如果启用了二进制日志,则设置sync_binlog=1,即每提交一次事务同步写到磁盘中。
  • 总是设置innodb_flush_log_at_trx_commit=1,即每提交一次事务都写到磁盘中。

上述两项变量的设置保证了:每次提交事务都写入二进制日志和事务日志,并在提交时将它们刷新到磁盘中。

选择刷日志的时间会严重影响数据修改时的性能,特别是刷到磁盘的过程。下例就测试了 innodb_flush_log_at_trx_commit 分别为0、1、2时的差距。

#创建测试表
drop table if exists test_flush_log;
create table test_flush_log(id int,name char(50))engine=innodb;#创建插入指定行数的记录到测试表中的存储过程
drop procedure if exists proc;
delimiter $$
create procedure proc(i int)
begindeclare s int default 1;declare c char(50) default repeat('a',50);while s<=i dostart transaction;insert into test_flush_log values(null,c);commit;set s=s+1;end while;
end$$
delimiter ;当前环境下, innodb_flush_log_at_trx_commit 的值为1,即每次提交都刷日志到磁盘。测试此时插入10W条记录的时间。
mysql> call proc(100000);
Query OK, 0 rows affected (15.48 sec)
结果是15.48秒。再测试值为2的时候,即每次提交都刷新到os buffer,但每秒才刷入磁盘中。
mysql> set @@global.innodb_flush_log_at_trx_commit=2;
mysql> truncate test_flush_log;
mysql> call proc(100000);
Query OK, 0 rows affected (3.41 sec)
结果插入时间大减,只需3.41秒。最后测试值为0的时候,即每秒才刷到os buffer和磁盘。
mysql> set @@global.innodb_flush_log_at_trx_commit=0;
mysql> truncate test_flush_log;
mysql> call proc(100000);
Query OK, 0 rows affected (2.10 sec)
结果只有2.10秒。

最后可以发现,其实值为2和0的时候,它们的差距并不太大,但2却比0要安全的多。它们都是每秒从os buffer刷到磁盘,它们之间的时间差体现在log buffer刷到os buffer上。因为将log buffer中的日志刷新到os buffer只是内存数据的转移,并没有太大的开销,所以每次提交和每秒刷入差距并不大。可以测试插入更多的数据来比较,以下是插入100W行数据的情况。从结果可见,值为2和0的时候差距并不大,但值为1的性能却差太多。

尽管设置为0和2可以大幅度提升插入性能,但是在故障的时候可能会丢失1秒钟数据,这1秒钟很可能有大量的数据,从上面的测试结果看,100W条记录也只消耗了20多秒,1秒钟大约有4W-5W条数据,尽管上述插入的数据简单,但却说明了数据丢失的大量性。更好的插入数据的做法是将值设置为1,然后修改存储过程,将每次循环都提交修改为只提交一次,这样既能保证数据的一致性,也能提升性能,修改如下:

drop procedure if exists proc;
delimiter $$
create procedure proc(i int)
begindeclare s int default 1;declare c char(50) default repeat('a',50);start transaction;while s<=i DOinsert into test_flush_log values(null,c);set s=s+1;end while;commit;
end$$
delimiter ;测试值为1时的情况。
mysql> set @@global.innodb_flush_log_at_trx_commit=1;
mysql> truncate test_flush_log;
mysql> call proc(1000000);
Query OK, 0 rows affected (11.26 sec)

日志块(log block)

innodb存储引擎中,redo log以块为单位进行存储的,每个块占512字节,这称为redo log block。所以不管是log buffer中还是os buffer中以及redo log file on disk中,都是这样以512字节的块存储的。

每个redo log block由3部分组成:日志块头、日志块尾和日志主体。其中日志块头占用12字节,日志块尾占用8字节,所以每个redo log block的日志主体部分只有512-12-8=492字节。

因为redo log记录的是数据页的变化,当一个数据页产生的变化需要使用超过492字节()的redo log来记录,那么就会使用多个redo log block来记录该数据页的变化。

日志块头包含4部分:

  • log_block_hdr_no:(4字节)该日志块在redo log buffer中的位置ID。
  • log_block_hdr_data_len:(2字节)该log block中已记录的log大小。写满该log block时为0x200,表示512字节。
  • log_block_first_rec_group:(2字节)该log block中第一个log的开始偏移位置。
  • lock_block_checkpoint_no:(4字节)写入检查点信息的位置。

关于log block块头的第三部分 log_block_first_rec_group ,因为有时候一个数据页产生的日志量超出了一个日志块,这是需要用多个日志块来记录该页的相关日志。例如,某一数据页产生了552字节的日志量,那么需要占用两个日志块,第一个日志块占用492字节,第二个日志块需要占用60个字节,那么对于第二个日志块来说,它的第一个log的开始位置就是73字节(60+12)。如果该部分的值和 log_block_hdr_data_len 相等,则说明该log block中没有新开始的日志块,即表示该日志块用来延续前一个日志块。

日志尾只有一个部分: log_block_trl_no ,该值和块头的 log_block_hdr_no 相等。

上面所说的是一个日志块的内容,在redo log buffer或者redo log file on disk中,由很多log block组成。如下图:

log group和redo log file

log group表示的是redo log group,一个组内由多个大小完全相同的redo log file组成。组内redo log file的数量由变量 innodb_log_files_group 决定,默认值为2,即两个redo log file。这个组是一个逻辑的概念,并没有真正的文件来表示这是一个组,但是可以通过变量 innodb_log_group_home_dir 来定义组的目录,redo log file都放在这个目录下,默认是在datadir下。

mysql> show global variables like "innodb_log%";
+-----------------------------+----------+
| Variable_name               | Value    |
+-----------------------------+----------+
| innodb_log_buffer_size      | 8388608  |
| innodb_log_compressed_pages | ON       |
| innodb_log_file_size        | 50331648 |
| innodb_log_files_in_group   | 2        |
| innodb_log_group_home_dir   | ./       |
+-----------------------------+----------+[root@xuexi data]# ll /mydata/data/ib*
-rw-rw---- 1 mysql mysql 79691776 Mar 30 23:12 /mydata/data/ibdata1
-rw-rw---- 1 mysql mysql 50331648 Mar 30 23:12 /mydata/data/ib_logfile0
-rw-rw---- 1 mysql mysql 50331648 Mar 30 23:12 /mydata/data/ib_logfile1

可以看到在默认的数据目录下,有两个ib_logfile开头的文件,它们就是log group中的redo log file,而且它们的大小完全一致且等于变量 innodb_log_file_size 定义的值。第一个文件ibdata1是在没有开启 innodb_file_per_table 时的共享表空间文件,对应于开启 innodb_file_per_table 时的.ibd文件。

在innodb将log buffer中的redo log block刷到这些log file中时,会以追加写入的方式循环轮训写入。即先在第一个log file(即ib_logfile0)的尾部追加写,直到满了之后向第二个log file(即ib_logfile1)写。当第二个log file满了会清空一部分第一个log file继续写入。

由于是将log buffer中的日志刷到log file,所以在log file中记录日志的方式也是log block的方式。

在每个组的第一个redo log file中,前2KB记录4个特定的部分,从2KB之后才开始记录log block。除了第一个redo log file中会记录,log group中的其他log file不会记录这2KB,但是却会腾出这2KB的空间。如下:

redo log file的大小对innodb的性能影响非常大,设置的太大,恢复的时候就会时间较长,设置的太小,就会导致在写redo log的时候循环切换redo log file。

redo log的格式:

因为innodb存储引擎存储数据的单元是页(和SQL Server中一样),所以redo log也是基于页的格式来记录的。默认情况下,innodb的页大小是16KB(由 innodb_page_size 变量控制),一个页内可以存放非常多的log block(每个512字节),而log block中记录的又是数据页的变化。

其中log block中492字节的部分是log body,该log body的格式分为4部分:

  • redo_log_type:占用1个字节,表示redo log的日志类型。
  • space:表示表空间的ID,采用压缩的方式后,占用的空间可能小于4字节。
  • page_no:表示页的偏移量,同样是压缩过的。
  • redo_log_body表示每个重做日志的数据部分,恢复时会调用相应的函数进行解析。例如insert语句和delete语句写入redo log的内容是不一样的。

如下图,分别是insert和delete大致的记录方式。

日志刷盘的规则

log buffer中未刷到磁盘的日志称为脏日志(dirty log)。

在上面的说过,默认情况下事务每次提交的时候都会刷事务日志到磁盘中,这是因为变量 innodb_flush_log_at_trx_commit 的值为1。但是innodb不仅仅只会在有commit动作后才会刷日志到磁盘,这只是innodb存储引擎刷日志的规则之一。

刷日志到磁盘有以下几种规则:

1.发出commit动作时。已经说明过,commit发出后是否刷日志由变量 innodb_flush_log_at_trx_commit 控制。

2.每秒刷一次。这个刷日志的频率由变量 innodb_flush_log_at_timeout 值决定,默认是1秒。要注意,这个刷日志频率和commit动作无关。

3.当log buffer中已经使用的内存超过一半时。

4.当有checkpoint时,checkpoint在一定程度上代表了刷到磁盘时日志所处的LSN位置。

数据页刷盘的规则及checkpoint:

内存中(buffer pool)未刷到磁盘的数据称为脏数据(dirty data)。由于数据和日志都以页的形式存在,所以脏页表示脏数据和脏日志。

上一节介绍了日志是何时刷到磁盘的,不仅仅是日志需要刷盘,脏数据页也一样需要刷盘。

在innodb中,数据刷盘的规则只有一个:checkpoint。 但是触发checkpoint的情况却有几种。不管怎样,checkpoint触发后,会将buffer中脏数据页和脏日志页都刷到磁盘。

innodb存储引擎中checkpoint分为两种:

  • sharp checkpoint:在重用redo log文件(例如切换日志文件)的时候,将所有已记录到redo log中对应的脏数据刷到磁盘。
  • fuzzy checkpoint:一次只刷一小部分的日志到磁盘,而非将所有脏日志刷盘。有以下几种情况会触发该检查点:
    • master thread checkpoint:由master线程控制,每秒或每10秒刷入一定比例的脏页到磁盘。
    • flush_lru_list checkpoint:从MySQL5.6开始可通过 innodb_page_cleaners 变量指定专门负责脏页刷盘的page cleaner线程的个数,该线程的目的是为了保证lru列表有可用的空闲页。
    • async/sync flush checkpoint:同步刷盘还是异步刷盘。例如还有非常多的脏页没刷到磁盘(非常多是多少,有比例控制),这时候会选择同步刷到磁盘,但这很少出现;如果脏页不是很多,可以选择异步刷到磁盘,如果脏页很少,可以暂时不刷脏页到磁盘
    • dirty page too much checkpoint:脏页太多时强制触发检查点,目的是为了保证缓存有足够的空闲空间。too much的比例由变量 innodb_max_dirty_pages_pct 控制,MySQL 5.6默认的值为75,即当脏页占缓冲池的百分之75后,就强制刷一部分脏页到磁盘。

由于刷脏页需要一定的时间来完成,所以记录检查点的位置是在每次刷盘结束之后才在redo log中标记的。

MySQL停止时是否将脏数据和脏日志刷入磁盘,由变量innodb_fast_shutdown={ 0|1|2 }控制,默认值为1,即停止时只做一部分purge,忽略大多数flush操作(但至少会刷日志),在下次启动的时候再flush剩余的内容,实现fast shutdown。

LSN超详细分析

LSN称为日志的逻辑序列号(log sequence number),在innodb存储引擎中,lsn占用8个字节。LSN的值会随着日志的写入而逐渐增大。

根据LSN,可以获取到几个有用的信息:

1.数据页的版本信息。

2.写入的日志总量,通过LSN开始号码和结束号码可以计算出写入的日志量。

3.可知道检查点的位置。

实际上还可以获得很多隐式的信息。

LSN不仅存在于redo log中,还存在于数据页中,在每个数据页的头部,有一个fil_page_lsn记录了当前页最终的LSN值是多少。通过数据页中的LSN值和redo log中的LSN值比较,如果页中的LSN值小于redo log中LSN值,则表示数据丢失了一部分,这时候可以通过redo log的记录来恢复到redo log中记录的LSN值时的状态。

redo log的lsn信息可以通过 show engine innodb status 来查看。
MySQL 5.5版本的show结果中只有3条记录,没有pages flushed up to。mysql> show engine innodb stauts
---
LOG
---
Log sequence number 2225502463
Log flushed up to   2225502463
Pages flushed up to 2225502463
Last checkpoint at  2225502463
0 pending log writes, 0 pending chkp writes
3201299 log i/o's done, 0.00 log i/o's/second

其中:

  • log sequence number就是当前的redo log(in buffer)中的lsn;
  • log flushed up to是刷到redo log file on disk中的lsn;
  • pages flushed up to是已经刷到磁盘数据页上的LSN;
  • last checkpoint at是上一次检查点所在位置的LSN。

innodb从执行修改语句开始:

(1).首先修改内存中的数据页,并在数据页中记录LSN,暂且称之为data_in_buffer_lsn;

(2).并且在修改数据页的同时(几乎是同时)向redo log in buffer中写入redo log,并记录下对应的LSN,暂且称之为redo_log_in_buffer_lsn;

(3).写完buffer中的日志后,当触发了日志刷盘的几种规则时,会向redo log file on disk刷入重做日志,并在该文件中记下对应的LSN,暂且称之为redo_log_on_disk_lsn;

(4).数据页不可能永远只停留在内存中,在某些情况下,会触发checkpoint来将内存中的脏页(数据脏页和日志脏页)刷到磁盘,所以会在本次checkpoint脏页刷盘结束时,在redo log中记录checkpoint的LSN位置,暂且称之为checkpoint_lsn。

(5).要记录checkpoint所在位置很快,只需简单的设置一个标志即可,但是刷数据页并不一定很快,例如这一次checkpoint要刷入的数据页非常多。也就是说要刷入所有的数据页需要一定的时间来完成,中途刷入的每个数据页都会记下当前页所在的LSN,暂且称之为data_page_on_disk_lsn。

详细说明如下图:

上图中,从上到下的横线分别代表:时间轴、buffer中数据页中记录的LSN(data_in_buffer_lsn)、磁盘中数据页中记录的LSN(data_page_on_disk_lsn)、buffer中重做日志记录的LSN(redo_log_in_buffer_lsn)、磁盘中重做日志文件中记录的LSN(redo_log_on_disk_lsn)以及检查点记录的LSN(checkpoint_lsn)。

假设在最初时(12:0:00)所有的日志页和数据页都完成了刷盘,也记录好了检查点的LSN,这时它们的LSN都是完全一致的。

假设此时开启了一个事务,并立刻执行了一个update操作,执行完成后,buffer中的数据页和redo log都记录好了更新后的LSN值,假设为110。这时候如果执行 show engine innodb status 查看各LSN的值,即图中①处的位置状态,结果会是:

log sequence number(110) > log flushed up to(100) = pages flushed up to = last checkpoint at

之后又执行了一个delete语句,LSN增长到150。等到12:00:01时,触发redo log刷盘的规则(其中有一个规则是 innodb_flush_log_at_timeout 控制的默认日志刷盘频率为1秒),这时redo log file on disk中的LSN会更新到和redo log in buffer的LSN一样,所以都等于150,这时 show engine innodb status ,即图中②的位置,结果将会是:

log sequence number(150) = log flushed up to > pages flushed up to(100) = last checkpoint at

再之后,执行了一个update语句,缓存中的LSN将增长到300,即图中③的位置。

假设随后检查点出现,即图中④的位置,正如前面所说,检查点会触发数据页和日志页刷盘,但需要一定的时间来完成,所以在数据页刷盘还未完成时,检查点的LSN还是上一次检查点的LSN,但此时磁盘上数据页和日志页的LSN已经增长了,即:

log sequence number > log flushed up to 和 pages flushed up to > last checkpoint at

但是log flushed up to和pages flushed up to的大小无法确定,因为日志刷盘可能快于数据刷盘,也可能等于,还可能是慢于。但是checkpoint机制有保护数据刷盘速度是慢于日志刷盘的:当数据刷盘速度超过日志刷盘时,将会暂时停止数据刷盘,等待日志刷盘进度超过数据刷盘。

等到数据页和日志页刷盘完毕,即到了位置⑤的时候,所有的LSN都等于300。

随着时间的推移到了12:00:02,即图中位置⑥,又触发了日志刷盘的规则,但此时buffer中的日志LSN和磁盘中的日志LSN是一致的,所以不执行日志刷盘,即此时 show engine innodb status 时各种lsn都相等。

随后执行了一个insert语句,假设buffer中的LSN增长到了800,即图中位置⑦。此时各种LSN的大小和位置①时一样。

随后执行了提交动作,即位置⑧。默认情况下,提交动作会触发日志刷盘,但不会触发数据刷盘,所以 show engine innodb status 的结果是:

log sequence number = log flushed up to > pages flushed up to = last checkpoint at

最后随着时间的推移,检查点再次出现,即图中位置⑨。但是这次检查点不会触发日志刷盘,因为日志的LSN在检查点出现之前已经同步了。假设这次数据刷盘速度极快,快到一瞬间内完成而无法捕捉到状态的变化,这时 show engine innodb status 的结果将是各种LSN相等。

Innodb的恢复行为

在启动innodb的时候,不管上次是正常关闭还是异常关闭,总是会进行恢复操作。

因为redo log记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志(如二进制日志)要快很多。而且,innodb自身也做了一定程度的优化,让恢复速度变得更快。

重启innodb时,checkpoint表示已经完整刷到磁盘上data page上的LSN,因此恢复时仅需要恢复从checkpoint开始的日志部分。例如,当数据库在上一次checkpoint的LSN为10000时宕机,且事务是已经提交过的状态。启动数据库时会检查磁盘中数据页的LSN,如果数据页的LSN小于日志中的LSN,则会从检查点开始恢复。

还有一种情况,在宕机前正处于checkpoint的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度。这时候一宕机,数据页中记录的LSN就会大于日志页中的LSN,在重启的恢复过程中会检查到这一情况,这时超出日志进度的部分将不会重做,因为这本身就表示已经做过的事情,无需再重做。

另外,事务日志具有幂等性,所以多次操作得到同一结果的行为在日志中只记录一次。而二进制日志不具有幂等性,多次操作会全部记录下来,在恢复的时候会多次执行二进制日志中的记录,速度就慢得多。例如,某记录中id初始值为2,通过update将值设置为了3,后来又设置成了2,在事务日志中记录的将是无变化的页,根本无需恢复;而二进制会记录下两次update操作,恢复时也将执行这两次update操作,速度比事务日志恢复更慢。

和redo log有关的几个变量

  • innodb_flush_log_at_trx_commit={0|1|2} # 指定何时将事务日志刷到磁盘,默认为1。

    • 0表示每秒将"log buffer"同步到"os buffer"且从"os buffer"刷到磁盘日志文件中。
    • 1表示每事务提交都将"log buffer"同步到"os buffer"且从"os buffer"刷到磁盘日志文件中。
    • 2表示每事务提交都将"log buffer"同步到"os buffer"但每秒才从"os buffer"刷到磁盘日志文件中。
  • innodb_log_buffer_size:# log buffer的大小,默认8M
  • innodb_log_file_size:#事务日志的大小,默认5M
  • innodb_log_files_group =2:# 事务日志组中的事务日志文件个数,默认2个
  • innodb_log_group_home_dir =./:# 事务日志组路径,当前目录表示数据目录
  • innodb_mirrored_log_groups =1:# 指定事务日志组的镜像组个数,但镜像功能好像是强制关闭的,所以只有一个log group。在MySQL5.7中该变量已经移除。

Redolog知识补充

指定Redo log 记录在{datadir}/ib_logfile1&ib_logfile2 可通过innodb_log_group_home_dir 配置指定 目录存储

一旦事务成功提交且数据持久化落盘之后,此时Redo log中的对应事务数据记录就失去了意义,所以Redo log的写入是日志文件循环写入的。

指定Redo log日志文件组中的数量 innodb_log_files_in_group 默认为2指定Redo log每一个日志文件最大存储量innodb_log_file_size 默认48指定Redo log在cache/buffer中的buffer池大小innodb_log_buffer_size 默认16Redo buffer 持久化Redo log的策略, Innodb_flush_log_at_trx_commit:取值 0 每秒提交 Redo buffer --> Redo log OS cache -->flush cache to disk[可能丢失一秒的事务数据]取值 1 默认值,每次事务提交执行Redo buffer --> Redo log OS cache -->flush cache to dis[最安全,性能最差的方式]取值 2 每次事务提交执行Redo buffer --> Redo log OS cache 再每一秒执行 ->flush cache tdisk操作

undo log

undo回滚日志用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。*

undo log用来回滚行记录到某个版本。事务未提交之前,Undo保存了未提交之前的版本数据,Undo中的数据可作为数据旧版本快照供其他并发事务进行快照读。是为了实现事务的原子性而出现的产物,在Mysql innodb存储引擎中用来实现多版本并发控制。

undo log基本概念

undo log有两个作用:提供回滚和多个行版本控制(MVCC)。

在数据修改的时候,不仅记录了redo,还记录了相对应的undo,如果因为某些原因导致事务失败或回滚了,可以借助该undo进行回滚。

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。

当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。

undo log是采用段(segment)的方式来记录的,
每个undo操作在记录的时候占用一个undo log segment。
另外,undo log也会产生redo log,因为undo log也要实现持久性保护。

Undo log是InnoDB MVCC事务特性的重要组成部分。当我们对记录做了变更操作时就会产生undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可以使用独立的Undo 表空间。
在Innodb当中,INSERT操作在事务提交前只对当前事务可见,Undo log在事务提交后即会被删除,因为新插入的数据没有历史版本,所以无需维护Undo log。而对于UPDATE、DELETE,责需要维护多版本信息。 在InnoDB当中,UPDATE和DELETE操作产生的Undo log都属于同一类型:update_undo。(update可以视为insert新数据到原位置,delete旧数据,undo log暂时保留旧数据)。

文件结构

回滚段的管理,也有个入口位置专门存储回滚段的管理信息,就是第6个页面(page5).这个页面专门用来存储事务相关信息的。主要包括:

Macro bytes Desc
TRX_SYS 38 每个数据页都会保留的文件头字段
TRX_SYS_TRX_ID_STORE 8 持久化的最大事务ID,这个值不是实时写入的,而是256次递增写一次
TRX_SYS_FSEG_HEADER 10 指向用来管理事务系统的segment所在的位置
TRX_SYS_RSEGS 128 * 8 用于存储128个回滚段位置,包括space id及page no。每个回滚段包含一个文件segment(trx_rseg_header_create)

在5.7版本中,回滚段既可以在ibdata中,也可以在独立undo表空间,或者ibtmp临时表空间中,一个可能的分布如下图所示。图片来自taobao.mysql

上图展示了基本的Undo回滚段布局结构,其中:
rseg0预留在系统表空间ibdata中;
rseg 1~rseg 32这32个回滚段存放于临时表的系统表空间中;
rseg33~ 则根据配置存放到独立undo表空间中
(如果没有打开独立Undo表空间,则存放于ibdata中)

关键结构体

InnoDB最多可以创建128个回滚段,而每个回滚段(也就是上面TRX_SYS_RSEGS数组中元素)需要单独的Page来维护其拥有的undo slot,Page类型为FIL_PAGE_TYPE_SYS。描述如下:

Macro bytes Desc
TRX_RSEG 38 保留的Page头
TRX_RSEG_MAX_SIZE 4 回滚段允许使用的最大Page数,当前值为ULINT_MAX
TRX_RSEG_HISTORY_SIZE 4 在history list上的undo page数,这些page需要由purge线程来进行清理和回收
TRX_RSEG_HISTORY FLST_BASE_NODE_SIZE(16) history list的base node
TRX_RSEG_FSEG_HEADER (FSEG_HEADER_SIZE)10 指向当前管理当前回滚段的inode entry
TRX_RSEG_UNDO_SLOTS 1024 * 4 undo slot数组,共1024个slot,值为FIL_NULL表示未被占用,否则记录占用该slot的第一个undo page

回滚段头页的创建参阅函数 trx_rseg_header_create 源码在innobase/trx/trx0rseg.cc

根据上面的信息,可以整理出所有回滚段组织架构。如下所示

INNODB支持的回滚段总共有128*1024=131072个,TRX_RSEG_UNDO_SLOTS数组的元素每一个元素对应一个页面,这个页面对应一个段,页面号就段首页的页面号。在每一个事务开始的时候,都会分配一个rseg,就是从长度128的数组中,根据最近使用情况,找一个邻近位置的rseg,再这个事务的生命周期内,被分配的rseg就会被这个事务所使用。

在事务要存储回滚记录的时候,就会从1024个slot中,根据类型(插入还是更新)找到空闲的槽作为自己的undo段。如果已经申请过同类型的槽,则直接使用。否则就需要新创建一个段。并将段首号写入到这个rseg对应的空闲槽中。这样结构就与事务具体结合起来了。当然找不到空闲位置,就报异常了。

所有回滚段都记录在trx_sys->rseg_array,数组大小为128,分别对应不同的回滚段;
rseg_array数组类型为trx_rseg_t,用于维护回滚段相关信息;
每个回滚段对象trx_rseg_t还要管理undo log信息,对应结构体为trx_undo_t,使用多个链表来维护trx_undo_t信息;
事务开启时,会专门给他指定一个回滚段,以后该事务用到的undo log页,就从该回滚段上分配;
事务提交后,需要purge的回滚段会被放到purge队列上(purge_sys->purge_queue)。
trx_rseg_t 源码在innobase/include/trx0rseg.h

/** The rollback segment memory object */
struct trx_rseg_t {/*--------------------------------------------------------*//** rollback segment id == the index of its slot in the trxsystem file copy */ulint                id;/** mutex protecting the fields in this struct except id,space,page_nowhich are constant */RsegMutex         mutex;/** space where the rollback segment header is placed */ulint             space;/** page number of the rollback segment header */ulint                page_no;/** page size of the relevant tablespace */page_size_t          page_size;/** maximum allowed size in pages */ulint             max_size;/** current size in pages */ulint              curr_size;/*--------------------------------------------------------*//* Fields for update undo logs *//** List of update undo logs */UT_LIST_BASE_NODE_T(trx_undo_t)   update_undo_list;/** List of update undo log segments cached for fast reuse */UT_LIST_BASE_NODE_T(trx_undo_t)   update_undo_cached;/*--------------------------------------------------------*//* Fields for insert undo logs *//** List of insert undo logs */UT_LIST_BASE_NODE_T(trx_undo_t) insert_undo_list;/** List of insert undo log segments cached for fast reuse */UT_LIST_BASE_NODE_T(trx_undo_t) insert_undo_cached;/*--------------------------------------------------------*//** Page number of the last not yet purged log header in the historylist; FIL_NULL if all list purged */ulint               last_page_no;/** Byte offset of the last not yet purged log header */ulint              last_offset;/** Transaction number of the last not yet purged log */trx_id_t            last_trx_no;/** TRUE if the last not yet purged log needs purging */ibool               last_del_marks;/** Reference counter to track rseg allocated transactions. */ulint              trx_ref_count;/** If true, then skip allocating this rseg as it reside inUNDO-tablespace marked for truncate. */bool                skip_allocation;
};

分配回滚段

当开启一个读写事务时(或者从只读事务转换为读写事务),我们需要预先为事务分配一个回滚段:对于只读事务,如果产生对临时表的写入,则需要为其分配回滚段,使用临时表回滚段(第1~32号回滚段),函数入口:trx_assign_rseg(源码在/innobase/trx/trx0trx.cc) -->trx_assign_rseg_low(/innobase/trx/trx0trx.cc)–>get_next_noredo_rseg(/innobase/trx/trx0trx.cc)。

在MySQL5.7中事务默认以只读事务开启,当随后判定为读写事务时,
则转换成读写模式,并为其分配事务ID和回滚段,
调用函数:trx_set_rw_mode(innobase/trx/trx0trx.cc) -->
trx_assign_rseg_low -->
get_next_redo_rseg

源码如下:

/******************************************************************//**
Get next redo rollback segment. (Segment are assigned in round-robin fashion).
@return assigned rollback segment instance */
static
trx_rseg_t*
get_next_redo_rseg(
/*===============*/ulong max_undo_logs,  /*!< in: maximum number of UNDO logs to use */ulint  n_tablespaces)  /*!< in: number of rollback tablespaces */
{trx_rseg_t*    rseg;static ulint   redo_rseg_slot = 0;ulint       slot = 0;slot = redo_rseg_slot++;slot = slot % max_undo_logs;/* Skip slots alloted to non-redo also ensure even distributionin selecting next redo slots.For example: If we don't do even distribution then for any value ofslot between 1 - 32 ... 33rd slots will be alloted creatingskewed distribution. */if (trx_sys_is_noredo_rseg_slot(slot)) {if (max_undo_logs > srv_tmp_undo_logs) {slot %= (max_undo_logs - srv_tmp_undo_logs);if (trx_sys_is_noredo_rseg_slot(slot)) {slot += srv_tmp_undo_logs;}} else {slot = 0;}}#ifdef UNIV_DEBUGulint start_scan_slot = slot;bool    look_for_rollover = false;
#endif /* UNIV_DEBUG */bool allocated = false;while (!allocated) {for (;;) {rseg = trx_sys->rseg_array[slot];#ifdef UNIV_DEBUG/* Ensure that we are not revisiting the sameslot that we have already inspected. */if (look_for_rollover) {ut_ad(start_scan_slot != slot);}look_for_rollover = true;
#endif /* UNIV_DEBUG */slot = (slot + 1) % max_undo_logs;/* Skip slots allocated for noredo rsegs */while (trx_sys_is_noredo_rseg_slot(slot)) {slot = (slot + 1) % max_undo_logs;}if (rseg == NULL) {continue;} else if (rseg->space == srv_sys_space.space_id()&& n_tablespaces > 0&& trx_sys->rseg_array[slot] != NULL&& trx_sys->rseg_array[slot]->space!= srv_sys_space.space_id()) {/** If undo-tablespace is configured, skiprseg from system-tablespace and try to useundo-tablespace rseg unless it is not possibledue to lower limit of undo-logs. */continue;} else if (rseg->skip_allocation) {/** This rseg resides in the tablespace thathas been marked for truncate so avoid using thisrseg. Also, this is possible only if there areat-least 2 UNDO tablespaces active and 2 redorsegs active (other than default system boundrseg-0). */ut_ad(n_tablespaces > 1);ut_ad(max_undo_logs>= (1 + srv_tmp_undo_logs + 2));continue;}break;}/* By now we have only selected the rseg but not marked itallocated. By marking it allocated we are ensuring that it willnever be selected for UNDO truncate purge. */mutex_enter(&rseg->mutex);if (!rseg->skip_allocation) {rseg->trx_ref_count++;allocated = true;}mutex_exit(&rseg->mutex);}ut_ad(rseg->trx_ref_count > 0);ut_ad(!trx_sys_is_noredo_rseg_slot(rseg->id));return(rseg);
}
  • 采用round-robin的轮询方式来赋予回滚段给事务,如果回滚段被标记为skip_allocation(这个undo tablespace太大了,purge线程需要对其进行truncate操作),则跳到下一个;

  • 选择一个回滚段给事务后,会将该回滚段的rseg->trx_ref_count递增,这样该回滚段所在的undo tablespace文件就不可以被truncate掉;

  • 临时表回滚段被赋予trx->rsegs->m_noredo,普通读写操作的回滚段被赋予trx->rsegs->m_redo;如果事务在只读阶段使用到临时表,随后转换成读写事务,那么会为该事务分配两个回滚段。

只读事务与读写事务的区别在于他们随后会不会记录redo log(undo也是需要redo来保护的)。

undo生命周期

在进行分配的时候,MySQL会从第一个回滚段开始轮询所有的回滚段,寻找当前不会被purge线程truncate掉的回滚段,以后该事务用到的undo page都会从这个undo段来分配。

然后在undo slot当中记录自己的事务ID,将该回滚段的count增加,来标识该回滚段中仍记录着未提交数据,防止被purge线程truncate掉。

最后,如果是临时表回滚段,则不记录redo,如果是普通读写操作,则会记录redo。

另外,如果一个事物,在只读阶段使用了临时表回滚段,之后又转变成了读写事物,那么两个回滚段都会被使用。

事物提交之后,需要purge的undo段都会放到purge队列上

使用undo段

当产生数据变更时,我们需要使用Undo log记录下变更前的数据以维护多版本信息。insert 和 delete/update 分开记录undo,因此需要从回滚段单独分配Undo slot。函数入口:trx_undo_report_row_operation 源码在innobase/trx/trx0rec.cc

源码很长,不贴了。主要流程如下:

  • 判断当前变更的是否是临时表,如果是临时表,则采用临时表回滚段来分配,否则采用普通的回滚段;

  • 临时表操作记录undo时不写redo log;

  • 操作类型为TRX_UNDO_INSERT_OP,且未分配insert undo slot时,调用函数trx_undo_assign_undo进行分配;

  • 操作类型为TRX_UNDO_MODIFY_OP,且未分配Update undo slot时,调用函数trx_undo_assign_undo进行分配。

我们来看看函数trx_undo_assign_undo的流程,源码在innobase/trx/trx0undo.cc:

1.首先总是从cahced list上分配trx_undo_t (函数trx_undo_reuse_cached(innobase/trx/trx0undo.cc 这个函数是用来给事务分配slot的),当满足某些条件时,事务提交时会将其拥有的trx_undo_t放到cached list上,这样新的事务可以重用这些undo 对象,而无需去扫描回滚段,寻找可用的slot,在后面的事务提交一节会介绍到);1、对于INSERT,从trx_rseg_t::insert_undo_cached上获取,并修改头部重用信息(trx_undo_insert_header_reuse)及预留XID空间(trx_undo_header_add_space_for_xid)2、对于DELETE/UPDATE,从trx_rseg_t::update_undo_cached上获取, 并在undo log hdr page上创建新的Undo log header(trx_undo_header_create),及预留XID存储空间(trx_undo_header_add_space_for_xid)3、获取到trx_undo_t对象后,会从cached list上移除掉。并初始化trx_undo_t相关信息(trx_undo_mem_init_for_reuse),将trx_undo_t::state设置为TRX_UNDO_ACTIVE2.如果没有cache的trx_undo_t,则需要从回滚段上分配一个空闲的undo slot(trx_undo_create),并创建对应的undo页,进行初始化;
一个回滚段可以支持1024个事务并发,如果不幸回滚段都用完了(通常这几乎不会发生),会返回错误DB_TOO_MANY_CONCURRENT_TRXS。
每一个Undo log segment实际上对应一个独立的段,段头的起始位置在UNDO 头page的TRX_UNDO_SEG_HDR+TRX_UNDO_FSEG_HEADER偏移位置(见下图)3.已分配给事务的trx_undo_t会加入到链表trx_rseg_t::insert_undo_list或者trx_rseg_t::update_undo_list上;4.如果是数据词典操作(DDL)产生的undo,主要是表级别操作,例如创建或删除表,还需要记录操作的table id到undo log header中(TRX_UNDO_TABLE_ID),同时将TRX_UNDO_DICT_TRANS设置为TRUE。(trx_undo_mark_as_dict_operation)。

总的来说,undo header page主要包括如下信息(图片来自taobao.mysql):

如何写入undo日志

入口函数:trx_undo_report_row_operation 源码:/innobase/trx/trx0rec.cc
当分配了一个undo slot,同时初始化完可用的空闲区域后,就可以向其中写入undo记录了。写入的page no取自undo->last_page_no,初始情况下和hdr_page_no相同。
对于INSERT_UNDO,调用函数trx_undo_page_report_insert进行插入,记录格式大致如下图所示:

对于UPDATE_UNDO,调用函数 trx_undo_page_report_modify(源码innobase/trx/trx0rec.cc) 进行插入,UPDATE UNDO的记录格式大概如下图 :

在写入的过程中,可能出现单页面空间不足的情况,导致写入失败,我们需要将刚刚写入的区域清空重置(trx_undo_erase_page_end),同时申请一个新的page(trx_undo_add_page) 加入到undo log段上,同时将undo->last_page_no指向新分配的page,然后重试。

完成Undo log写入后,构建新的回滚段指针并返回
(trx_undo_build_roll_ptr),
回滚段指针包括undo log所在的回滚段id、日志所在的page no、
以及page内的偏移量,需要记录到聚集索引记录中。

事务操作:

事务Prepare(准备)阶段:

入口函数:trx_prepare_low 源码:/innobase/trx/trx0trx.cc

当事务完成需要提交时,为了和BINLOG做XA,InnoDB的commit被划分成了两个阶段:prepare阶段和commit阶段,本小节主要讨论下prepare阶段undo相关的逻辑。
为了在崩溃重启时知道事务状态,需要将事务设置为Prepare,MySQL 5.7对临时表undo和普通表undo分别做了处理,前者在写undo日志时总是不需要记录redo,后者则需要记录。
分别设置insert undo 和 update undo的状态为prepare,调用函数trx_undo_set_state_at_prepare,过程也比较简单,找到undo log slot对应的头页面(trx_undo_t::hdr_page_no),将页面段头的TRX_UNDO_STATE设置为TRX_UNDO_PREPARED,同时修改其他对应字段,
如下图所示(对于外部显式XA所产生的XID,这里不做讨论):

Tips:InnoDB层的XID是如何获取的呢? 当Innodb的参数innodb_support_xa打开时,在执行事务的第一条SQL时,就会去注册XA,根据第一条SQL的query id拼凑XID数据,然后存储在事务对象中。参考函数trans_register_ha

事务Commit:

当事务commit时,需要将事务状态设置为COMMIT状态,这里同样通过Undo来实现的。

入口函数:trx_commit_low(/innobase/trx/trx0trx.cc)–>trx_write_serialisation_history(innobase/trx/trx0trx.cc)

在该函数中,需要将该事务包含的Undo都设置为完成状态,先设置insert undo,再设置update undo(trx_undo_set_state_at_finish),完成状态包含三种:

  • 如果当前的undo log只占一个page,且占用的header page大小使用不足其3/4时(TRX_UNDO_PAGE_REUSE_LIMIT),则状态设置为TRX_UNDO_CACHED,该undo对象会随后加入到undo cache list上;

  • 如果是Insert_undo(undo类型为TRX_UNDO_INSERT),则状态设置为TRX_UNDO_TO_FREE;

  • 如果不满足a和b,则表明该undo可能需要Purge线程去执行清理操作,状态设置为TRX_UNDO_TO_PURGE。

在确认状态信息后,写入undo header page的TRX_UNDO_STATE中。

如果当前事务包含update undo,并且undo所在回滚段不在purge队列时,还需要将当前undo所在的回滚段(及当前最大的事务号)加入Purge线程的Purge队列(purge_sys->purge_queue)中(参考函数trx_serialisation_number_get)。

对于undate undo需要调用trx_undo_update_cleanup进行清理操作,清理的过程包括:

  1. 将undo log加入到history list上,调用trx_purge_add_update_undo_to_history:

    • 如果该undo log不满足cache的条件(状态为TRX_UNDO_CACHED,如上述),则将其占用的slot设置为FIL_NULL,意为slot空闲,同时更新回滚段头的TRX_RSEG_HISTORY_SIZE值,将当前undo占用的page数累加上去;

    • 将当前undo加入到回滚段的TRX_RSEG_HISTORY链表上,作为链表头节点,节点指针为UNDO头的TRX_UNDO_HISTORY_NODE;

    • 更新trx_sys->rseg_history_len(也就是show engine innodb status看到的history list),如果只有普通的update_undo,则加1,如果还有临时表的update_undo,则加2,然后唤醒purge线程;

    • 将当前事务的trx_t::no写入undo头的TRX_UNDO_TRX_NO段;

    • 如果不是delete-mark操作,将undo头的TRX_UNDO_DEL_MARKS更新为false;

    • 如果undo所在回滚段的rseg->last_page_no为FIL_NULL,表示该回滚段的旧的清理已经完成,进行如下赋值,记录这个回滚段上第一个需要purge的undo记录信息:

      rseg->last_page_no = undo->hdr_page_no;rseg->last_offset = undo->hdr_offset;rseg->last_trx_no = trx->no;rseg->last_del_marks = undo->del_marks;
    
  2. 如果undo需要cache,将undo对象放到回滚段的update_undo_cached链表上;否则释放undo对象(trx_undo_mem_free)。

    注意上面只清理了update_undo,insert_undo直到事务释放记录锁、从读写事务链表清除、以及关闭read view后才进行,调用函数trx_undo_insert_cleanup:

    • 如果Undo状态为TRX_UNDO_CACHED,则加入到回滚段的insert_undo_cached链表上;
    • 否则,将该undo所占的segment及其所占用的回滚段的slot全部释放掉(trx_undo_seg_free),修改当前回滚段的大小(rseg->curr_size),并释放undo对象所占的内存(trx_undo_mem_free),和Update_undo不同,insert_undo并未放到History list上。

事务完成提交后,需要将其使用的回滚段引用计数rseg->trx_ref_count减1;

事务回滚:

如果事务因为异常或者被显式的回滚了,那么所有数据变更都要改回去。这里就要借助回滚日志中的数据来进行恢复了。

入口函数为:row_undo_step(源码/innobase/row/row0undo.cc) --> row_undo(/innobase/row/row0undo.cc)

操作也比较简单,析取老版本记录,做逆向操作即可:对于标记删除的记录清理标记删除标记;对于in-place更新,将数据回滚到最老版本;对于插入操作,直接删除聚集索引和二级索引记录(row_undo_ins)。

具体的操作中,先回滚二级索引记录(row_undo_mod_del_mark_sec、row_undo_mod_upd_exist_sec、row_undo_mod_upd_del_sec),再回滚聚集索引记录(row_undo_mod_clust)。这里不展开描述,可以参阅对应的函数。

多版本控制

InnoDB的多版本使用undo来构建,这很好理解,undo记录中包含了记录更改前的镜像,如果更改数据的事务未提交,对于隔离级别大于等于read commit的事务而言,它不应该看到已修改的数据,而是应该给它返回老版本的数据。

入口函数: row_vers_build_for_consistent_read(源码在/innobase/row/row0vers.cc)

由于在修改聚集索引记录时,总是存储了回滚段指针和事务id,可以通过该指针找到对应的undo 记录,通过事务Id来判断记录的可见性。当旧版本记录中的事务id对当前事务而言是不可见时,则继续向前构建,直到找到一个可见的记录或者到达版本链尾部。(关于事务可见性及read view,可以参阅我们之前的月报)

Tips 1:构建老版本记录(trx_undo_prev_version_build)需要持有page latch,因此如果Undo链太长的话,其他请求该page的线程可能等待时间过长导致crash,最典型的就是备库备份场景:

当备库使用innodb表存储复制位点信息时(relay_log_info_repository=TABLE),逻辑备份显式开启一个read view并且执行了长时间的备份时,这中间都无法对slave_relay_log_info表做purge操作,导致版本链极其长;当开始备份slave_relay_log_info表时,就需要去花很长的时间构建老版本;复制线程由于需要更新slave_relay_log_info表,因此会陷入等待Page latch的场景,最终有可能导致信号量等待超时,实例自杀。 (bug#74003)

Tips 2:在构建老版本的过程中,总是需要创建heap来存储旧版本记录,实际上这个heap是可以重用的,无需总是重复构建(bug#69812)

Tips 3:如果回滚段类型是INSERT,就完全没有必要去看Undo日志了,因为一个未提交事务的新插入记录,对其他事务而言总是不可见的。

Tips 4: 对于聚集索引我们知道其记录中存有修改该记录的事务id,我们可以直接判断是否需要构建老版本(lock_clust_rec_cons_read_sees),但对于二级索引记录,并未存储事务id,而是每次更新记录时,同时更新记录所在的page上的事务id(PAGE_MAX_TRX_ID),如果该事务id对当前事务是可见的,那么就无需去构建老版本了,否则就需要去回表查询对应的聚集索引记录,然后判断可见性(lock_sec_rec_cons_read_sees)。

Purge清理操作

从上面的分析我们可以知道:update_undo产生的日志会放到history list中,当这些旧版本无人访问时,需要进行清理操作;另外页内标记删除的操作也需要从物理上清理掉。后台Purge线程负责这些工作。

入口函数:srv_do_purge --> trx_purge

  1. 确认可见性

    在开始尝试purge前,purge线程会先克隆一个最老的活跃视图(trx_sys->mvcc->clone_oldest_view),所有在readview开启之前提交的事务所做的事务变更都是可以清理的。

  2. 获取需要purge的undo记录(trx_purge_attach_undo_recs

    从history list上读取多个Undo记录,并分配到多个purge线程的工作队列上((purge_node_t*) thr->child->undo_recs),默认一次最多取300个undo记录,可通过参数innodb_purge_batch_size参数调整。

  3. Purge工作线程

    当完成任务的分发后,各个工作线程(包括协调线程)开始进行purge操作 入口函数: row_purge_step -> row_purge -> row_purge_record_func

    主要包括两种:一种是记录直接被标记删除了,这时候需要物理清理所有的聚集索引和二级索引记录(row_purge_record_func);另一种是聚集索引in-place更新了,但二级索引上的记录顺序可能发生变化,而二级索引的更新总是标记删除 + 插入,因此需要根据回滚段记录去检查二级索引记录序是否发生变化,并执行清理操作(row_purge_upd_exist_or_extern)。

  4. 清理history list

    从前面的分析我们知道,insert undo在事务提交后,Undo segment 就释放了。而update undo则加入了history list,为了将这些文件空间回收重用,需要对其进行truncate操作;默认每处理128轮Purge循环后,Purge协调线程需要执行一次purge history List操作。

    入口函数:trx_purge_truncate --> trx_purge_truncate_history

    从回滚段的HISTORY 文件链表上开始遍历释放Undo log segment,由于history 链表是按照trx no有序的,因此遍历truncate直到完全清除,或者遇到一个还未purge的undo log(trx no比当前purge到的位置更大)时才停止。

崩溃恢复

当实例从崩溃中恢复时,需要将活跃的事务从undo中提取出来,对于ACTIVE状态的事务直接回滚,对于Prepare状态的事务,如果该事务对应的binlog已经记录,则提交,否则回滚事务。

实现的流程也比较简单,首先先做redo (recv_recovery_from_checkpoint_start),undo是受redo 保护的,因此可以从redo中恢复(临时表undo除外,临时表undo是不记录redo的)。

在redo日志应用完成后,初始化完成数据词典子系统(dict_boot),随后开始初始化事务子系统(trx_sys_init_at_db_start),undo 段的初始化即在这一步完成。

在初始化undo段时(trx_sys_init_at_db_start -> trx_rseg_array_init -> ... -> trx_undo_lists_init),会根据每个回滚段page中的slot是否被使用来恢复对应的undo log,读取其状态信息和类型等信息,创建内存结构,并存放到每个回滚段的undo list上。

当初始化完成undo内存对象后,就要据此来恢复崩溃前的事务链表了(trx_lists_init_at_db_start),根据每个回滚段的insert_undo_list来恢复插入操作的事务(trx_resurrect_insert),根据update_undo_list来恢复更新事务(tex_resurrect_update),如果既存在插入又存在更新,则只恢复一个事务对象。另外除了恢复事务对象外,还要恢复表锁及读写事务链表,从而恢复到崩溃之前的事务场景。

当从Undo恢复崩溃前活跃的事务对象后,会去开启一个后台线程来做事务回滚和清理操作(recv_recovery_rollback_active -> trx_rollback_or_clean_all_recovered),对于处于ACTIVE状态的事务直接回滚,对于既不ACTIVE也非PREPARE状态的事务,直接则认为其是提交的,直接释放事务对象。但完成这一步后,理论上事务链表上只存在PREPARE状态的事务。

随后很快我们进入XA Recover阶段,MySQL使用内部XA,即通过Binlog和InnoDB做XA恢复。在初始化完成引擎后,Server层会开始扫描最后一个Binlog文件,搜集其中记录的XID(MYSQL_BIN_LOG::recover),然后和InnoDB层的事务XID做对比。如果XID已经存在于binlog中了,对应的事务需要提交;否则需要回滚事务。

Tips:为何只需要扫描最后一个binlog文件就可以了? 因为在每次rotate到一个新的binlog文件之前,总是要保证前一个binlog文件中对应的事务都提交并且sync redo到磁盘了,也就是说,前一个binlog文件中的事务在崩溃恢复时肯定是出于提交状态的。

undo log的存储方式

innodb存储引擎对undo的管理采用段的方式。rollback segment称为回滚段,每个回滚段中有1024个undo log segment。

在以前老版本,只支持1个rollback segment,这样就只能记录1024个undo log segment。后来MySQL5.5可以支持128个rollback segment,即支持128*1024个undo操作,还可以通过变量 innodb_undo_logs (5.6版本以前该变量是 innodb_rollback_segments )自定义多少个rollback segment,默认值为128。

undo log默认存放在共享表空间中。

[root@xuexi data]# ll /mydata/data/ib*
-rw-rw---- 1 mysql mysql 79691776 Mar 31 01:42 /mydata/data/ibdata1
-rw-rw---- 1 mysql mysql 50331648 Mar 31 01:42 /mydata/data/ib_logfile0
-rw-rw---- 1 mysql mysql 50331648 Mar 31 01:42 /mydata/data/ib_logfile1

如果开启了 innodb_file_per_table ,将放在每个表的.ibd文件中。

在MySQL5.6中,undo的存放位置还可以通过变量 innodb_undo_directory 来自定义存放目录,默认值为"."表示datadir。

默认rollback segment全部写在一个文件中,但可以通过设置变量 innodb_undo_tablespaces 平均分配到多少个文件中。该变量默认值为0,即全部写入一个表空间文件。该变量为静态变量,只能在数据库示例停止状态下修改,如写入配置文件或启动时带上对应参数。但是innodb存储引擎在启动过程中提示,不建议修改为非0的值,如下:

2017-03-31 13:16:00 7f665bfab720 InnoDB: Expected to open 3 undo tablespaces but was able
2017-03-31 13:16:00 7f665bfab720 InnoDB: to find only 0 undo tablespaces.
2017-03-31 13:16:00 7f665bfab720 InnoDB: Set the innodb_undo_tablespaces parameter to the
2017-03-31 13:16:00 7f665bfab720 InnoDB: correct value and retry. Suggested value is 0

和undo log相关的变量

undo相关的变量在MySQL5.6中已经变得很少。如下:它们的意义在上文中已经解释了。mysql> show variables like "%undo%";
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_undo_directory   | .     |
| innodb_undo_logs        | 128   |
| innodb_undo_tablespaces | 0     |
+-------------------------+-------+

delete/update操作的内部机制

当事务提交的时候,innodb不会立即删除undo log,因为后续还可能会用到undo log,如隔离级别为repeatable read时,事务读取的都是开启事务时的最新提交行版本,只要该事务不结束,该行版本就不能删除,即undo log不能删除。

但是在事务提交的时候,会将该事务对应的undo log放入到删除列表中,未来通过purge来删除。并且提交事务时,还会判断undo log分配的页是否可以重用,如果可以重用,则会分配给后面来的事务,避免为每个独立的事务分配独立的undo log页而浪费存储空间和性能。

通过undo log记录delete和update操作的结果发现:(insert操作无需分析,就是插入行而已)

  • delete操作实际上不会直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的。
  • update分为两种情况:update的列是否是主键列。
    • 如果不是主键列,在undo log中直接反向记录是如何update的。即update是直接进行的。
    • 如果是主键列,update分两部执行:先删除该行,再插入一行目标行。

查询日志(log):

通用查询日志

默认关闭,记录查询的sql语句,如果开启会降低mysql的整体性能

通用查询日志可以存放到一个文本文件或者表中,所有连接和语句被记录到该日志文件或表,缺省未开启该日志。
通过–log[=file_name]或-l [file_name]选项启动它。如果没有给定file_name的值, 默认名是host_name.log。
mysqld按照它接收的顺序记录语句到查询日志。这可能与执行的顺序不同。
不同于更新日志和二进制日志,它们在查询执行后,但是任何一个锁释放之前记录日志。
查询日志包含所有语句,而二进制日志不包含只查询数据的语句。
服务器重新启动和日志刷新不会产生新的一般查询日志文件。

全局查询日志

永远不要在生产环境开启这个功能

#开启
general_log=1;#记录日志文件的路径
general_log_file=/path/logfile;#输出格式
log_output=FILE;#命令的方式:
set general_log=1;
set global log_output='TABLE';以后编写的sql语句,会记录到mysql库里的general_log表,可以用下面的命令查看:
select * from mysql.general_log;


系统变量

log_output=[none|file|table|file,table] #通用查询日志输出格式
general_log=[on|off] #是否启用通用查询日志
general_log_file[=filename] #通用查询日志位置及名字

通日志的备份

在Linux或Unix中,你可以通过下面的命令重新命名文件
并创建一个新文件:
shell> mv hostname.log hostname-old.log
shell> mysqladmin flush-logs
shell> cp hostname-old.log to-backup-directory
shell> rm hostname-old.log
在Windows中,服务器打开日志文件期间不能重新命名日志文件。必须先停止服务器然后重新命名日志文件。然后重启服务器来创建新日志文件。

演示通用查询日志的使用

a、启用通用查询日志
--演示环境
root@localhost[(none)]> show variables like '%version%';
+-------------------------+------------------------------+
| Variable_name           | Value                        |
+-------------------------+------------------------------+
| innodb_version          | 5.5.39                       |
| protocol_version        | 10                           |
| slave_type_conversions  |                              |
| version                 | 5.5.39-log                   |
| version_comment         | MySQL Community Server (GPL) |
| version_compile_machine | x86_64                       |
| version_compile_os      | Linux                        |
+-------------------------+------------------------------+--查看系统变量
root@localhost[(none)]> show variables like '%general%';
+------------------+----------------------------+
| Variable_name    | Value                      |
+------------------+----------------------------+
| general_log      | OFF                        |
| general_log_file | /var/lib/mysql/suse11b.log |
+------------------+----------------------------+--查看当前的通用日志,显示无日志文件
root@localhost[(none)]> system ls /var/lib/mysql/suse11b.log
ls: cannot access /var/lib/mysql/suse11b.log: No such file or directory
--设置变量general_log以开启通用查询日志
root@localhost[(none)]> set @@global.general_log=1;
Query OK, 0 rows affected (0.00 sec)--再次查看通用日志文件已存在
root@localhost[(none)]> system ls /var/lib/mysql/suse11b.log
/var/lib/mysql/suse11b.log
root@localhost[(none)]> select * from tempdb.tb1;  --执行查询
+------+------+
| id   | val  |
+------+------+
|    1 | jack |
+------+------+--查看通用日志文件内容
root@localhost[(none)]> system more /var/lib/mysql/suse11b.log
/usr/sbin/mysqld, Version: 5.5.39-log (MySQL Community Server (GPL)). started with:
Tcp port: 3306  Unix socket: /var/lib/mysql/mysql.sock
Time                 Id Command    Argument
141003 16:18:12     4 Query     show variables like '%general%'
141003 16:18:55     4 Query     select * from tempdb.tb1b、更改通用查询日志位置
root@localhost[(none)]> exit
Bye
suse11b:~ # service mysql stop
Shutting down MySQL...                                          done
suse11b:~ # mysqld --general_log_file=/tmp/suse11b.log --user=mysql &
[1] 47009
suse11b:~ # ps -ef|grep mysql|grep -v grep
mysql    47009 44514  1 16:22 pts/0    00:00:00 mysqld --general_log_file=/tmp/suse11b.log --user=mysql
root     47053 44514  0 16:22 pts/0    00:00:00 grep mysql
suse11b:~ # mysql
root@localhost[(none)]> system ls /tmp/suse11b.log
ls: cannot access /tmp/suse11b.log: No such file or directory
root@localhost[(none)]> show variables like '%gener%';
+------------------+------------------+
| Variable_name    | Value            |
+------------------+------------------+
| general_log      | OFF              |
| general_log_file | /tmp/suse11b.log |
+------------------+------------------+root@localhost[(none)]> set global general_log=on;
Query OK, 0 rows affected (0.01 sec)--此时从系统变量看出,通用日志已经到/tmp目录下
root@localhost[(none)]> show variables like '%gener%';
+------------------+------------------+
| Variable_name    | Value            |
+------------------+------------------+
| general_log      | ON               |
| general_log_file | /tmp/suse11b.log |
+------------------+------------------+--发布查询
root@localhost[(none)]> select count(*) from tempdb.tb1;
+----------+
| count(*) |
+----------+
|        1 |
+----------+--查看通用日志文件内容
root@localhost[(none)]> system more /tmp/suse11b.log
mysqld, Version: 5.5.39-log (MySQL Community Server (GPL)). started with:
Tcp port: 3306  Unix socket: /var/lib/mysql/mysql.sock
Time                 Id Command    Argument
141003 16:30:03     1 Query     show variables like '%gener%'
141003 16:30:09     1 Query     select count(*) from tempdb.tb1c、通用查询日志输出方式
--可以输出为文件,表以及不输出,即TABLE,FILE,NONE
--系统变量log_output
root@localhost[(none)]> show variables like 'log_output';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_output    | FILE  |
+---------------+-------+--下面修改为输出为表方式
root@localhost[(none)]> set global log_output='TABLE';
Query OK, 0 rows affected (0.00 sec)root@localhost[(none)]> show variables like 'log_output';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_output    | TABLE |
+---------------+-------+--发布查询
root@localhost[(none)]> select * from tempdb.tb1;
+------+------+
| id   | val  |
+------+------+
|    1 | jack |
+------+------+--Author: Leshami
--Blog  : http://blog.csdn.net/leshamiroot@localhost[(none)]> system more /tmp/suse11b.log
mysqld, Version: 5.5.39-log (MySQL Community Server (GPL)). started with:
Tcp port: 3306  Unix socket: /var/lib/mysql/mysql.sock
Time                 Id Command    Argument
141003 16:30:03     1 Query     show variables like '%gener%'
141003 16:30:09     1 Query     select count(*) from tempdb.tb1
141003 16:31:00     1 Query     show variables like 'log_output'
141003 17:00:48     1 Query     set global log_output='TABLE'  #通用查询日志输出到文件仅仅记录到全局变量的修改--mysql.general_log记录了通用查询日志的信息
root@localhost[(none)]> desc mysql.general_log;
+--------------+------------------+------+-----+-------------------+-----------------------------+
| Field        | Type             | Null | Key | Default           | Extra                       |
+--------------+------------------+------+-----+-------------------+-----------------------------+
| event_time   | timestamp        | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
| user_host    | mediumtext       | NO   |     | NULL              |                             |
| thread_id    | int(11)          | NO   |     | NULL              |                             |
| server_id    | int(10) unsigned | NO   |     | NULL              |                             |
| command_type | varchar(64)      | NO   |     | NULL              |                             |
| argument     | mediumtext       | NO   |     | NULL              |                             |
+--------------+------------------+------+-----+-------------------+-----------------------------+--从通用查询日志表里查看通用查询日志的内容
root@localhost[(none)]> select thread_id,command_type,argument from mysql.general_log;
+-----------+--------------+---------------------------------------------------------------+
| thread_id | command_type | argument                                                      |
+-----------+--------------+---------------------------------------------------------------+
|         1 | Query        | show variables like 'log_output'                              |
|         1 | Query        | select * from tempdb.tb1                                      |
|         1 | Query        | desc mysql.general_log                                        |
|         1 | Query        | select thread_id,command_type,argument from mysql.general_log |
+-----------+--------------+---------------------------------------------------------------+root@localhost[(none)]> show variables like 'log_output';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_output    | TABLE |
+---------------+-------+--使用FILE,TABLE 2者混合输出通用日志
root@localhost[(none)]> set global log_output='file,table';
Query OK, 0 rows affected (0.00 sec)root@localhost[(none)]> select @@global.log_output;
+---------------------+
| @@global.log_output |
+---------------------+
| FILE,TABLE          |
+---------------------+root@localhost[(none)]> insert into tempdb.tb1 values(2,'robinson');
Query OK, 1 row affected (0.06 sec)root@localhost[(none)]> commit;
Query OK, 0 rows affected (0.01 sec)--验证结果,表和文件里边存在通用的日志记录
root@localhost[(none)]> system tail /tmp/suse11b.log|grep robinson
141003 17:41:54     2 Query     insert into tempdb.tb1 values(2,'robinson')
root@localhost[(none)]> select thread_id,command_type,argument from mysql.general_log-> where argument like '%robinson%';
+-----------+--------------+------------------------------------------------------------------------+
| thread_id | command_type | argument                                                               |
+-----------+--------------+------------------------------------------------------------------------+
|         2 | Query        | insert into tempdb.tb1 values(2,'robinson')                            |
|         2 | Query        | select thread_id,command_type,argument from mysql.general_log          |
|           |              |    where argument like ''robinson''                                    |
+-----------+--------------+------------------------------------------------------------------------+d、关闭通用查询日志
--可以通过设置系统变量general_log来关闭通用查询日志,此时日志输出设置为FILE,TABLE
root@localhost[(none)]> show variables like 'log_output';
+---------------+------------+
| Variable_name | Value      |
+---------------+------------+
| log_output    | FILE,TABLE |
+---------------+------------+root@localhost[(none)]> set global general_log=off;
Query OK, 0 rows affected (0.01 sec)root@localhost[(none)]> show variables like '%gener%';
+------------------+------------------+
| Variable_name    | Value            |
+------------------+------------------+
| general_log      | OFF              |
| general_log_file | /tmp/suse11b.log |
+------------------+------------------+root@localhost[(none)]> delete from tempdb.tb1 where id=2;
Query OK, 1 row affected (0.12 sec)root@localhost[(none)]> commit;
Query OK, 0 rows affected (0.00 sec)root@localhost[(none)]> system tail -n 1 /tmp/suse11b.log
141003 17:45:13     2 Query     set global general_log=offroot@localhost[(none)]> select thread_id,command_type,argument from mysql.general_log-> where argument like '%delete%';
Empty set (0.00 sec)--从上面的演示可知,尽管我们设置了log_output为FILE,TABLE,但general_log为OFF,通用日志无任何记录产生root@localhost[(none)]> set global log_output=none;
Query OK, 0 rows affected (0.00 sec)root@localhost[(none)]> set global general_log=1;
Query OK, 0 rows affected (0.00 sec)root@localhost[(none)]> truncate table tempdb.tb1;
Query OK, 0 rows affected (0.01 sec)root@localhost[(none)]> system tail -n 1 /tmp/suse11b.log
Time                 Id Command    Argument--通过上面的演示,在log_output=none,general_log=on的清下下无任何通用日志输出。

慢查询日志:

#慢查询日志是什么:
MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中相应时间超过阙值的语句,
具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。
long_query_time的默认值为10S,意思 是运行10秒以上的语句会被抓取出来。
由它来查看哪些SQL抄错了我们的最大忍耐时间值,比如一条SQL执行超过5秒钟,
我们就算慢SQL,希望能收集超过5秒的SQL,结合之前explain进行全面分析。默认:show variables like '%slow_query_log%';开启:set global slow_query_log=1;

默认情况下,MySQL数据库没有开启慢查询日志 ,需要我们手动来设置这个参数。当然,如果不是调优需要的话一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件。

慢查询日志操作

#查看当前多少秒算慢
show variables like 'long_query_time%';#设置慢的阙值
set global long_query_time=3;#设置以后没变化?
需要重新连接或新开一个会话才能看到修改值
show variables like 'long_query_time%';
show global variables like 'long_query_time';#查看慢查询SQL的个数
show global status like 'slow_queries';
结果却显示为大于0的数字
才是真实的慢查询数。show global status like 'slow_query';
结果显示为empty
表示没有这个名字的status





慢查询扩展

八、索引

索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。举例说明索引:如果把数据库中的某一张看成一本书,那么索引就像是书的目录,可以通过目录快速查找书中指定内容的位置,对于数据库表来说,可以通过索引快速查找表中的数据。

索引的目的在于提高查询效率,可以类比字典,如果要查"MySQL"这个单词,我们肯定需要定位到M字母,然后从上往下找到y字母,再找到剩下的SQL。

MySQL官方对索引的定义为:

索引(Index)是帮助MySQL高效获取数据的数据结构。

可以得到索引的本质: **索引是数据结构 **。

可以理解为:排好序的快速查找数据结构

一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。我们平常所说的索引,如果没有特别指明,都是指B数(多路搜索树,并不一定是二叉的)结构组织的索引。聚集索引,次要索引,覆盖索引,复合索引,前缀索引,唯一索引默认都是使用B+树索引,统称索引。当然,除了B+树这种类型的索引(index)之外,还有哈希索引(hash)。




在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。

下图就是一种可能的索引方式示例:


左边是数据表,一共有两例七条记录,最左边的是数据记录的物理地址

为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在一定的复杂度内获取到相应数据,从而快速的检索出复合条件的记录。

主键索引是一级索引,其他所有的索引都是二级索引 (辅助索引)

主键索引和辅助索引:

五大索引类型:

普通索引、复合索引、唯一索引、主键索引、 全文索引

① 普通索引(单列索引)

普通索引(单列索引):单列索引是最基本的索引,它没有任何限制。(1)创建索引
CREATE INDEX index_name ON table_name(col_name);
create [unique] index idxname on tablename(col_name);(2)修改表结构的方式添加索引
ALTER TABLE table_name ADD INDEX index_name(col_name);(3)创建表的时候同时创建索引
CREATE TABLE `news` (`id` int(11) NOT NULL AUTO_INCREMENT ,`title` varchar(255)  NOT NULL ,`content` varchar(255)  NULL ,`time` varchar(20) NULL DEFAULT NULL ,PRIMARY KEY (`id`),INDEX index_name (title(255))
)(4)删除索引
drop INDEX index_name ON table_name;
drop index index_name
或者
alter table `表名` drop index 索引名;

② 复合索引(组合索引)

复合索引:复合索引是在多个字段上创建的索引。复合索引遵守“最左前缀”原则,即在查询条件中使用了复合索引的第一个字段,索引才会被使用。因此,在复合索引中索引列的顺序至关重要。

(1)创建一个复合索引
create index index_name on table_name(col_name1,col_name2,...);(2)修改表结构的方式添加索引
alter table table_name add index index_name(col_name,col_name2,...);

③ 唯一索引

唯一索引:唯一索引和普通索引类似,主要的区别在于,唯一索引限制列的值必须唯一,但允许存在空值(只允许存在一条空值)。如果在已经有数据的表上添加唯一性索引的话:如果添加索引的列的值存在两个或者两个以上的空值,则不能创建唯一性索引会失败。(一般在创建表的时候,要对自动设置唯一性索引,需要在字段上加上 not null)
如果添加索引的列的值存在两个或者两个以上的null值,还是可以创建唯一性索引,只是后面创建的数据不能再插入null值 ,并且严格意义上此列并不是唯一的,因为存在多个null值。
对于多个字段创建唯一索引规定列值的组合必须唯一。
比如:在order表创建orderId字段和 productId字段 的唯一性索引,那么这两列的组合值必须唯一!“空值” 和”NULL”的概念:
1:空值是不占用空间的 .
2: MySQL中的NULL其实是占用空间的.长度验证:注意空值的之间是没有空格的。> select length(''),length(null),length(' ');
+------------+--------------+-------------+
| length('') | length(null) | length(' ') |
+------------+--------------+-------------+
|          0 |         NULL |           1 |
+------------+--------------+-------------+(1)创建唯一索引
# 创建单个索引
CREATE UNIQUE INDEX index_name ON table_name(col_name);# 创建多个索引
CREATE UNIQUE INDEX index_name on table_name(col_name,...);(2)修改表结构
# 单个
ALTER TABLE table_name ADD UNIQUE index index_name(col_name);
# 多个
ALTER TABLE table_name ADD UNIQUE index index_name(col_name,...);(3)创建表的时候直接指定索引
CREATE TABLE `news` (`id` int(11) NOT NULL AUTO_INCREMENT ,`title` varchar(255)  NOT NULL ,`content` varchar(255)  NULL ,`time` varchar(20) NULL DEFAULT NULL ,PRIMARY KEY (`id`),UNIQUE index_name_unique(title)
)

④ 主键索引

主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引:(1)主键索引(创建表时添加)
CREATE TABLE `news` (`id` int(11) NOT NULL AUTO_INCREMENT ,`title` varchar(255)  NOT NULL ,`content` varchar(255)  NULL ,`time` varchar(20) NULL DEFAULT NULL ,PRIMARY KEY (`id`)
)(2)主键索引(创建表后添加)
alter table tbl_name add primary key(col_name);CREATE TABLE `order` (`orderId` varchar(36) NOT NULL,`productId` varchar(36)  NOT NULL ,`time` varchar(20) NULL DEFAULT NULL
)alter table `order` add primary key(`orderId`);

⑤ 全文索引

在一般情况下,模糊查询都是通过 like 的方式进行查询。
但是,对于海量数据,这并不是一个好办法,在 like “value%” 可以使用索引,但是对于 like “%value%” 这样的方式,执行全表查询,这在数据量小的表,不存在性能问题,但是对于海量数据,全表扫描是非常可怕的事情,所以 like 进行模糊匹配性能很差。这种情况下,需要考虑使用全文搜索的方式进行优化。全文搜索在 MySQL 中是一个 FULLTEXT 类型索引。FULLTEXT 索引在 MySQL 5.6 版本之后支持 InnoDB,而之前的版本只支持 MyISAM 表。全文索引主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的where语句的参数匹配。fulltext索引配合match against操作使用,而不是一般的where语句加like。目前只有char、varchar,text 列上可以创建全文索引。小技巧:
在数据量较大时候,先将数据放入一个没有全局索引的表中,然后再用CREATE index创建fulltext索引,要比先为一张表建立fulltext然后再将数据写入的速度快很多。(1)创建表的适合添加全文索引CREATE TABLE `news` (`id` int(11) NOT NULL AUTO_INCREMENT ,`title` varchar(255)  NOT NULL ,`content` text  NOT NULL ,`time` varchar(20) NULL DEFAULT NULL ,PRIMARY KEY (`id`),FULLTEXT (content)
)(2)修改表结构添加全文索引ALTER TABLE table_name ADD FULLTEXT index_fulltext_content(col_name)1
2
(3)直接创建索引CREATE FULLTEXT INDEX index_fulltext_content ON table_name(col_name)
1
注意: 默认 MySQL 不支持中文全文检索!MySQL 全文搜索只是一个临时方案,对于全文搜索场景,更专业的做法是使用全文搜索引擎,例如 ElasticSearch 或 Solr。

一般性建议:

  1. 对于单键索引,尽量选择针对当前query过滤性更好的索引
  2. 在选择组合索引的时候,当前Query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
  3. 在选择组合索引的时候,尽量选择可以能够包含当前query中的where子句中更多字段的索引
  4. 尽可能通过分析统计信息和调整query的写法来达到选择合适索引的目的

聚集性索引:

实际上,可以把索引理解为一种特殊的目录。微软的SQL SERVER提供了两种索引:聚集索引(clustered index,也称聚类索引、簇集索引)和非聚集索引(nonclustered index,也称非聚类索引、非簇集索引)。下面,我们举例来说明一下聚集索引和非聚集索引的区别:

其实,我们的汉语字典的正文本身就是一个聚集索引。比如,我们要查“安”字,因为“安”的拼音是“an”,而按照拼音排序汉字的字典是以英文字母“a”开头并以“z”结尾的,那么“安”字就自然地排在字典的前部。如果您翻完了所有以“a”开头的部分仍然找不到这个字,那么就说明您的字典中没有这个字。也就是说,字典的正文部分本身就是一个目录,您不需要再去查其他目录来找到您需要找的内容。 我们把这种正文内容本身就是一种按照一定规则排列的目录称为“聚集索引”。
每个表只能有一个聚集索引,因为目录只能按照一种方法进行排序。

一张表至少有一个索引,就是聚集索引。

  1. 有主键 主键就是聚集索引
  2. 带not null的字段,该字段就是聚集索引
  3. rowid隐藏的字段,它是聚集索引、

聚簇索引:

聚簇索引就是聚集索引。

聚簇索引是一种数据存储方式,将索引与数据存储在同一个叶子节点中。聚簇索引由搜索引擎负责实现。 (如图1)

图一: 图二:

聚簇索引优点:
1、把相关数据保存在一起,因为mysql数据库读取数据是按照页读取的,当读取某一个用户数据时,相邻的数据也会加载到内存中。根据用户读取一个id的数据时,相邻数据被读取的可能性会非常高,这种按页加载就减少了IO操作
2、数据访问更快
3、使用覆盖索引扫描的查询可以直接使用叶节点中的主键值

聚簇索引缺点:

1、聚簇索引最大限度提高了IO密集型应用性能,但是当数据都在内存中时,聚簇索引优势就没有了
2、插入速度严重依赖插入顺序。如果不是按照顺序插入,可能导致数据的移动设置页分裂,从而影响性能
3、更新聚簇索引的代价非常高,因为会强制INNODB将每个给跟新的行移动到新的位置上去
4、聚簇索引插入新列或者更新聚簇索引的时候可能导致页分裂
5、聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏或者由于页分裂导致数据存储不连续
6、二级索引需要的存储空间更大,因为二级索引中包含了主键列,同时二级索引需要两次查询才能查询到行数据。(如图2)

非聚集索引:

如果遇到不认识的字,不知道它的发音,这时候,需要去根据“偏旁部首”查到您要找的字,然后根据这个字后的页码直接翻到某页来找到您要找的字。但您结合“部首目录”和“检字表”而查到的字的排序并不是真正的正文的排序方法,比如您查“张”字,我们可以看到在查部首之后的检字表中“张”的页码是672页,检字表中“张”的上面是“驰”字,但页码却是63页,“张”的下面是“弩”字,页面是390页。很显然,这些字并不是真正的分别位于“张”字的上下方,现在您看到的连续的“驰、张、弩”三字实际上就是他们在非聚集索引中的排序,是字典正文中的字在非聚集索引中的映射。我们可以通过这种方式来找到您所需要的字,但它需要两个过程,先找到目录中的结果,然后再翻到您所需要的页码。 我们把这种目录纯粹是目录,正文纯粹是正文的排序方式称为“非聚集索引”。

区别及优缺点:
区别:

  • 聚集索引一个表只能有一个,而非聚集索引一个表可以存在多个

  • 聚集索引存储记录是物理上连续存在,而非聚集索引是逻辑上的连续,物理存储并不连续

  • 聚集索引:物理存储按照索引排序;聚集索引是一种索引组织形式,索引的键值逻辑顺序决定了表数据行的物理存储顺序。

  • 非聚集索引:物理存储不按照索引排序;非聚集索引则就是普通索引了,仅仅只是对数据列创建相应的索引,不影响整个表的物理存储顺序。

  • 索引是通过二叉树的数据结构来描述的,我们可以这么理解聚簇索引:索引的叶节点就是数据节点。而非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。

优势与缺点:

聚集索引插入数据时速度要慢(时间花费在“物理存储的排序”上,也就是首先要找到位置然后插入),查询数据比非聚集数据的速度快。

需要搞清楚的几个问题:
第一:聚集索引的约束是唯一性,是否要求字段也是唯一的呢?

分析:如果认为是的朋友,可能是受系统默认设置的影响,一般我们指定一个表的主键,如果这个表之前没有聚集索引,同时建立主键时候没有强制指定使用非聚集索引,SQL会默认在此字段上创建一个聚集索引,而主键都是唯一的,所以理所当然的认为创建聚集索引的字段也需要唯一。
  
结论:聚集索引可以创建在任何一列你想创建的字段上,这是从理论上讲,实际情况并不能随便指定,否则在性能上会是恶梦。

第二:为什么聚集索引可以创建在任何一列上,如果此表没有主键约束,即有可能存在重复行数据呢?
  
粗一看,这还真是和聚集索引的约束相背,但实际情况真可以创建聚集索引。

分析其原因是:如果未使用 UNIQUE 属性创建聚集索引,数据库引擎将向表自动添加一个四字节 uniqueifier 列。必要时,数据库引擎 将向行自动添加一个 uniqueifier 值,使每个键唯一。此列和列值供内部使用,用户不能查看或访问。

第三:是不是聚集索引就一定要比非聚集索引性能优呢?
  
如果想查询学分在60-90之间的学生的学分以及姓名,在学分上创建聚集索引是否是最优的呢?

答:否。既然只输出两列,我们可以在学分以及学生姓名上创建联合非聚集索引,此时的索引就形成了覆盖索引,即索引所存储的内容就是最终输出的数据,这种索引在比以学分为聚集索引做查询性能更好。

第四:在数据库中通过什么描述聚集索引与非聚集索引的?
  
索引是通过二叉树的形式进行描述的,我们可以这样区分聚集与非聚集索引的区别:聚集索引的叶节点就是最终的数据节点,而非聚集索引的叶节仍然是索引节点,但它有一个指向最终数据的指针。

第五:在主键是创建聚集索引的表在数据插入上为什么比主键上创建非聚集索引表速度要慢?

有了上面第四点的认识,我们分析这个问题就有把握了,在有主键的表中插入数据行,由于有主键唯一性的约束,所以需要保证插入的数据没有重复。我们来比较下主键为聚集索引和非聚集索引的查找情况:聚集索引由于索引叶节点就是数据页,所以如果想检查主键的唯一性,需要遍历所有数据节点才行,但非聚集索引不同,由于非聚集索引上已经包含了主键值,所以查找主键唯一性,只需要遍历所有的索引页就行(索引的存储空间比实际数据要少),这比遍历所有数据行减少了不少IO消耗。这就是为什么主键上创建非聚集索引比主键上创建聚集索引在插入数据时要快的真正原因。

何时使用聚集索引或非聚集索引:

动作描述 使用聚集索引 使用非聚集索引
列经常被分组排序
返回某范围内的数据 不应
一个或极少不同值 不应 不应
小数目的不同值 不应
大数目的不同值 不应
频繁更新的列 不应
外键列
主键列
频繁修改索引列 不应

稠密索引 :

在密集索引中,数据库中的每个搜索键值都有一个索引记录。这样可以加快搜索速度,但需要更多空间来存储索引记录本身。索引记录包含搜索键值和指向磁盘上实际记录的指针。

稠密索引:每个索引键值都对应有一个索引项

稠密索引能够比稀疏索引更快的定位一条记录。但是,稀疏索引相比于稠密索引的优点是:它所占空间更小,且插入和删除时的维护开销也小。

稠密索引是指在线性索引的过程中将数据集中的每个记录对应一个索引项。索引项按关键码有序,索引表是有序表。当索引文件可以在内存中容纳时,将索引文件驻留内存,可用高效查找方法如二分查找实现索引表上的查找,在找到索引项后,可根据其中存放的指向记录的位置信息,可快速找到要找的记录。但若主文件记录数较大,由于是稠密索引,故索引表也会很大,甚至无法存储在内存中,可能就需要反复去访问磁盘,查找性能大大下降。

稠密索引不适合在主文件中进行插入或删除一条记录的运算,因为一旦在文件中插入或删除一条记录,就必然要引起记录的移动。为了使稠密索引文件按关键码有序而且是顺序存储的,索引就必须更新。

稀疏索引 :

又叫分块索引。在稀疏索引中,不会为每个搜索关键字创建索引记录。此处的索引记录包含搜索键和指向磁盘上数据的实际指针。要搜索记录,我们首先按索引记录进行操作,然后到达数据的实际位置。如果我们要寻找的数据不是我们通过遵循索引直接到达的位置,那么系统将开始顺序搜索,直到找到所需的数据为止。

稀疏索引:相对于稠密索引,稀疏索引只为某些搜索码值建立索引记录;在搜索时,找到其最大的搜索码值小于或等于所查找记录的搜索码值的索引项,然后从该记录开始向后顺序查询直到找到为止。

注意理解:稠密索引是因为索引项和数据集的记录个数相同,所以空间代价很大。

如何减少索引项的个数呢?

我们可以对数据集进行分块,使其分块有序,然后再对每一块建立一个索引项(类似于图书馆的分块)。

分块有序是把数据集的记录分成了若干块,并且这些块需要满足两个条件:

(1)块内无序

  • 每一块内的记录不要求有序

(2)块间有序

  • 比如要求第二块记录的关键字均要大于第一块中所有记录的关键字,第三块要大于第二块。

  • 只有块间有序才有可能在查找时带来效率。

  • 对于分块有序的数据集,将每块对应一个索引项,这种索引方法叫做分块索引。

分块索引的索引项结构分为三个数据项:

a: 最大关键码–存储每一块中的最大关键字。

b: 块长–存储每一块中记录的个数以便于循环时使用。

c: 块首地址–用于指向块首数据元素的指针,便于开始对这一块中记录进行遍历

在分块索引表中查找,可以分为两步:

a: 在分块索引表中查找要查的关键字所在块。

由于分块索引表是块间有序的,因此很容易利用折半插值等算法得到结果。

b:根据块首指针找到相应的块,并在块中顺序查找关键码。

因为块中可以是无序的,因此只能顺序查找。

优缺点:

  • 稠密索引比稀疏索引 更快的定位一条记录。 但是占用的空间比较大 。
  • 稀疏索引所占空间小,并且插入和删除时所需维护的开销也小。 但是在查询的精确率上还是相对于稠密索引还是比较慢的,因为不需要顺序查找,还有回表。

覆盖索引:

使用的索引和创建的索引,字段和字段顺序全匹配。

索引使用的误区:

1、主键就是聚集索引–错误想法的

这种想法是极端错误的,是对聚集索引的一种浪费。虽然默认是在主键上建立聚集索引的。

通常,我们会在每个表中都建立一个ID列,以区分每条数据,并且这个ID列是自动增大的,步长一般为1。如果我们将这个列设为主键,mysql会将此列默认为聚集索引。这样做有好处,就是可以让您的数据在数据库中按照ID进行物理排序,但这样做意义不大。

显而易见,聚集索引的优势是很明显的,而每个表中只能有一个聚集索引的规则,这使得聚集索引变得更加珍贵。

从我们前面谈到的聚集索引的定义我们可以看出,使用聚集索引的最大好处就是能够根据查询要求,迅速缩小查询范围,避免全表扫描。在实际应用中,因为 ID号是自动生成的,我们并不知道每条记录的ID号,所以我们很难在实践中用ID号来进行查询。这就使让ID号这个主键作为聚集索引成为一种资源浪费。其次,让每个ID号都不同的字段作为聚集索引也不符合“大数目的不同值情况下不应建立聚合索引”规则;当然,这种情况只是针对用户经常修改记录内容,特别是索引项的时候会负作用,但对于查询速度并没有影响。

如在办公自动化系统中,无论是系统首页显示的需要用户签收的文件、会议还是用户进行文件查询等任何情况下进行数据查询都离不开字段的是“日期”还有用户本身的“用户名”。

通常,办公自动化的首页会显示每个用户尚未签收的文件或会议。虽然我们的where语句可以仅仅限制当前用户尚未签收的情况,但如果您的系统已建立了很长时间,并且数据量很大,那么,每次每个用户打开首页的时候都进行一次全表扫描,这样做意义是不大的,绝大多数的用户1个月前的文件都已经浏览过了,这样做只能徒增数据库的开销而已。事实上,我们完全可以让用户打开系统首页时,数据库仅仅查询这个用户近3个月来未阅览的文件,通过“日期”这个字段来限制表扫描,提高查询速度。如果您的办公自动化系统已经建立的2年,那么您的首页显示速度理论上将是原来速度8倍,甚至更快。

在这里之所以提到“理论上”三字,是因为如果您的聚集索引还是盲目地建在ID这个主键上时,您的查询速度是没有这么高的,即使您在“日期”这个字段上建立的索引(非聚合索引)。下面我们就来看一下在1000万条数据量的情况下各种查询的速度表现(3个月内的数据为25万条):

1).仅在主键上建立聚集索引,并且不划分时间段:
Select gid,fariqi,neibuyonghu,title from tgongwen
用时:128470毫秒(即:128秒)
2).在主键上建立聚集索引,在fariq上建立非聚集索引:
select gid,fariqi,neibuyonghu,title from Tgongwen
where fariqi> dateadd(day,-90,getdate())
用时:53763毫秒(54秒)
3).将聚合索引建立在日期列(fariqi)上:
select gid,fariqi,neibuyonghu,title from Tgongwen
where fariqi> dateadd(day,-90,getdate())
用时:2423毫秒(2秒)

虽然每条语句提取出来的都是25万条数据,各种情况的差异却是巨大的,特别是将聚集索引建立在日期列时的差异。事实上,如果您的数据库真的有1000 万容量的话,把主键建立在ID列上,就像以上的第1、2种情况,在网页上的表现就是超时,根本就无法显示。这也是摒弃ID列作为聚集索引的一个最重要的因素。得出以上速度的方法是:在各个select语句前加:

declare @d datetime
set @d=getdate()
并在select语句后加:select [语句执行花费时间(毫秒)]=datediff(ms,@d,getdate())

2、只要建立索引就能显著提高查询速度–错误想法的

事实上,我们可以发现上面的例子中,第2、3条语句完全相同,且建立索引的字段也相同;不同的仅是前者在fariqi字段上建立的是非聚合索引,后者在此字段上建立的是聚合索引,但查询速度却有着天壤之别。所以,并非是在任何字段上简单地建立索引就能提高查询速度。

从建表的语句中,我们可以看到这个有着1000万数据的表中fariqi字段有5003个不同记录。在此字段上建立聚合索引是再合适不过了。在现实中,我们每天都会发几个文件,这几个文件的发文日期就相同,这完全符合建立聚集索引要求的:“既不能绝大多数都相同,又不能只有极少数相同”的规则。由此看来,我们建立“适当”的聚合索引对于我们提高查询速度是非常重要的。

3、把所有需要提高查询速度的字段都加进聚集索引,以提高查询速度–错误想法的

上面已经谈到:在进行数据查询时都离不开字段的是“日期”还有用户本身的“用户名”。既然这两个字段都是如此的重要,我们可以把他们合并起来,建立一个复合索引(compound index)。

很多人认为只要把任何字段加进聚集索引,就能提高查询速度,也有人感到迷惑:如果把复合的聚集索引字段分开查询,那么查询速度会减慢吗?带着这个问题,我们来看一下以下的查询速度(结果集都是25万条数据):(日期列fariqi首先排在复合聚集索引的起始列,用户名neibuyonghu排在后列):

1).select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi>’‘2004-5-5’’
查询速度:2513毫秒
2).select gid,fariqi,neibuyonghu,title from Tgongwen
where fariqi>’‘2004-5-5’’ and neibuyonghu=’‘办公室’’
查询速度:2516毫秒
3).select gid,fariqi,neibuyonghu,title from Tgongwen where neibuyonghu=’‘办公室’’
查询速度:60280毫秒

从以上试验中,我们可以看到如果仅用聚集索引的起始列作为查询条件和同时用到复合聚集索引的全部列的查询速度是几乎一样的,甚至比用上全部的复合索引列还要略快(在查询结果集数目一样的情况下);而如果仅用复合聚集索引的非起始列作为查询条件的话,这个索引是不起任何作用的。当然,语句1、2的查询速度一样是因为查询的条目数一样,如果复合索引的所有列都用上,而且查询结果少的话,这样就会形成“索引覆盖”,因而性能可以达到最优。同时,请记住:无论您是否经常使用聚合索引的其他列,但其前导列一定要是使用最频繁的列。

4、用聚合索引比用不是聚合索引的主键速度快

下面是实例语句:(都是提取25万条数据)
select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi=’‘2004-9-16’’
使用时间:3326毫秒
select gid,fariqi,neibuyonghu,reader,title from Tgongwen where gid<=250000
使用时间:4470毫秒
这里,用聚合索引比用不是聚合索引的主键速度快了近1/4。

5、用聚合索引比用一般的主键作order by时速度快,特别是在小数据量情况下

select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by fariqi
用时:12936
select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by gid
用时:18843
这里,用聚合索引比用一般的主键作order by时,速度快了3/10。事实上,如果数据量很小的话,用聚集索引作为排序列要比使用非聚集索引速度快得明显的多;而数据量如果很大的话,如10万以上,则二者的速度差别不明显。

6、使用聚合索引内的时间段,搜索时间会按数据占整个数据表的百分比成比例减少,而无论聚合索引使用了多少个:

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>’‘2004-1-1’’
用时:6343毫秒(提取100万条)
select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>’‘2004-6-6’’
用时:3170毫秒(提取50万条)
select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi=’‘2004-9-16’’
用时:3326毫秒(和上句的结果一模一样。如果采集的数量一样,那么用大于号和等于号是一样的)
select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>’‘2004-1-1’’ and fariqi<’‘2004-6-6’’
用时:3280毫秒

7、日期列不会因为有分秒的输入而减慢查询速度

下面的例子中,共有100万条数据,2004年1月1日以后的数据有50万条,但只有两个不同的日期,日期精确到日;之前有数据50万条,有5000个不同的日期,日期精确到秒。
select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>’‘2004-1-1’’ order by fariqi
用时:6390毫秒
select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi<’‘2004-1-1’’ order by fariqi
用时:6453毫秒

索引的原理:

索引如何快速定位:

索引一般以文件形式存在磁盘中(也可以存于内存中),存储的索引的原理大致概括为以空间换时间,数据库在未添加索引的时候进行查询默认的是进行全量搜索,也就是进行全局扫描,有多少条数据就要进行多少次查询,然后找到相匹配的数据就把他放到结果集中,直到全表扫描完。而建立索引之后,会将建立索引的KEY值放在一个n叉树上(BTree),就是B+树的节点上。因为B+树的特点就是适合在磁盘等直接存储设备上组织动态查找表,每次以索引进行条件查询时,会去树上根据key值直接进行搜索。




索引优点:

① 建立索引的列可以保证行的唯一性,生成唯一的rowId

② 建立索引可以有效缩短数据的检索时间

③ 建立索引可以加快表与表之间的连接

④ 为用来排序或者是分组的字段添加索引可以加快分组和排序顺序

⑤类似大学图书馆建书目索引,提高数据检索的效率,降低数据库的IO成本。

⑥通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗。

索引缺点:

① 创建索引和维护索引需要时间成本,这个成本随着数据量的增加而加大

② 创建索引和维护索引需要空间成本,每一条索引都要占据数据库的物理存储空间,数据量越大,占用空间也越大(数据表占据的是数据库的数据空间)

③ 会降低表的增删改的效率,因为每次增删改索引需要进行动态维护,导致时间变长。增强改慢是因为,修改数据的时候还需要改变索引。

④索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立最优秀的索引,或优化查询。

MySQL索引结构:

Hash索引、full-text全文索引、R-Tree索引

索引失效的原因:

解决like通配符使用两边%,索引失效问题:

解决方法:使用覆盖索引





















使用索引的建议:

对于单键索引,尽量选择针对当前query过滤性更好的索引。

在选择组合索引的时候,当前Query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。

在选择组合索引的时候,尽量选择可以能够包含当前query中的where字句中更多字段的索引。

尽可能通过分析统计信息和调整query的写法来达到选择合适索引的目的。

关于时间复杂度:

索引的查询和删除

#查看:
show indexes from `表名`;
#或
show keys from `表名`;#删除
alter table `表名` drop index 索引名;#有四种方式来添加数据库的索引:
alter table tbl_name add primary key (column_list);该语句添加一个主键,这意味着索引值必须是唯一的,且不能为null
alter table tbl_name add unique index_name(column_list);这条语句创建索引的值必须是唯一的(除了null外,null可能会出现多次)
alter table tbl_name add index index_name(column_list);添加普通索引,索引值可出现多次
alter table tbl_name add fulltext index_name(column_list);该语句指定了索引为fulltext,用于全文索引。

查看索引使用情况

show status like ‘Handler_read%’;

handler_read_key:这个值越高越好,越高表示使用索引查询到的次数
handler_read_rnd_next:这个值越高,说明查询低效

哪些情况需要创建索引:

  1. 主键自动建立唯一索引
  2. 频繁作为查询条件的字段应该创建索引
  3. 查询总与其他表关联的字段,外键关系建立索引‘
  4. 频繁更新的字段不适合创建索引(因为每次更新不单单是更新了记录,还会更新索引)
  5. Where条件里用不到的字段不创建索引
  6. 单键/组合索引的选择问题,who?(在高并发下倾向创建组合索引)
  7. 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
  8. 查询中统计或者分组字段

哪些情况不适合创建索引:

  1. 表记录太少
  2. 经常增删改的表
  3. 数据重复且分布平均的表字段,因此应该只为最经常查询和最经常排序的数据列建立索引。
    如果某个数据列包含许多重复的内容,为它建立索引就没有太大的实际效果。
  4. 过滤性不好的不适合建索引。

如果一个表有10万行记录,有一个字段A只有T和F两种值,且每个值的分布概率大约为50%,那么对这种表A字段建索引一般不会提高数据库的查询速度。

索引的选择性是指索引列中不同值得数据与表中记录数的比。如果一个表中有2000条记录,表索引列有1980个不同的值,那么这个索引的选择性就是1980/2000=0.99。一个索引的选择性越接近1,那么这个索引的效率就越高。

MySQL Query Optimizer

MySQL中有专门负责优化select语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的Query提供他认为最优的执行计划(他认为最优的数据检索方式,但不见得是DBA认为是最优的,这部分最耗费时间)。

当客户端向MySQL请求一个Query,命令解析器模块完成请求分类,区别出是select并转发给MySQL Query Optimizer时,MySQL Query Optimizer 首先会对整条Query进行优化,处理掉一些常量表达式的预算,直接换算成常量值。并对Query中的查询条件进行简化和转换,如去掉一些无用或显而易见的条件、结构调整等。然后分析Query中的Hint信息(如果有),看显示Hint信息是否可以完全确定该Query的执行计划。如果没有Hint或Hint信息还不足以完全确定执行计划,则会读取所涉及对象的统计信息,根据Query进行写相应的计算分析,然后再得出最后的执行计划。

MySQL常见瓶颈:

CPU:CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候、

IO:磁盘I/O瓶颈发生在装入数据远大于内存容量的时候。

服务器硬件的性能瓶颈:top、free、iostat和vmstat来查看系统的性能状态。

MySQL索引实现

在MySQL中,索引属于存储引擎级别的概念,不同存储引擎对索引的实现方式是不同的,本文主要讨论MyISAM和InnoDB两个存储引擎的索引实现方式。

MyISAM索引实现

MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。下图是MyISAM索引的原理图:


这里设表一共有三列,假设我们以Col1为主键,则上图是一个MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示:

同样也是一颗B+Tree,data域保存数据记录的地址。因此,MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。

MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分。

InnoDB索引实现

虽然InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM截然不同。

第一个重大区别是InnoDB的数据文件本身就是索引文件。从上文知道,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。

上图是InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。

第二个与MyISAM索引的不同是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助索引都引用主键作为data域。例如,图11为定义在Col3上的一个辅助索引:

这里以英文字符的ASCII码作为比较准则。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。

了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。

索引使用策略及优化

MySQL的优化主要分为结构优化(Scheme optimization)和
查询优化(Query optimization)。

结构优化:

(Scheme optimization)

查询优化:

(Query optimization)

覆盖索引、、

左连接索引建在右表,右连接索引建在左表

模糊查两边%问题,使用覆盖索引解决,字段和顺序一致,

九、Explain用法:

Explain简介:

使用explain关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈。

explain+SQL语句

explain返回的结果项很多,主要关注三种,分别是type、key、rows。其中key表明的是这次查找中所用到的索引,rows是指这次查找数据所扫描的行数(这里可以先这样理解,但实际上是内循环的次数)。而type则是意味着类型。

Explain能干嘛:

表的读取顺序

数据读取操作的操作类型

哪些索引可以使用

哪些索引可以被实际使用

表之间的引用

每张表有多少行被优化器查询

Explain包含的信息:

ID:

select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序

三种情况:

1、id相同,执行顺序由上至下

2、id不同,如果是子查询,id的序号会递增,id的值越大优先级越高,越先被执行

3、id相同不同,同时存在

//获取SalesOrgCode的长度
//根据SalesOrgCode的长度给Userlevel赋值



select_type:

主要是用于区别普通查询、联合查询、子查询等的复杂查询

table:

显示这一行的数据时关于哪个表的

type:

显示的是访问的类型,





示例:



possible_keys:

显示可能应用在这张表中的索引,一个或多个。

查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。

key:

实际使用的索引,如果为null,则没有使用索引。

查询中若使用了覆盖索引,则该索引仅出现在key列表中。

key_len:

表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度,在不损失精确性的情况下,长度越短越好。

key_len显示的值为索引字段的最大可能长度, 并非实际使用长度 ,即key_len是根据表定义计算而得,不是通过表内检错出的。

key_len字段能够帮你检查是否充分的利用上了索引



ref:

显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值。

rows:

根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数

filtered

这个字段表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例,注意是百分比,不是具体行。

返回结果的行占需要读到的行(rows列的值)的百分比,就是百分比越高,说明需要查询到数据越准确,百分比越小,说明查询到的数据量大,而结果集很少。

Extra:

包含不适合在其他列中显示,但十分重要的额外信息

Using filesort:说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取,

mysql中无法利用索引完成的排序操作称为"文件排序"。

Using temporary:使了用临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序order by和分组查询group by。

Using index:标识相应的select操作中使用了覆盖索引(Covering Index),避免访问了表的数据行,效率不错!

如果同时出现using where,表明索引被用来执行索引键值的查找;如果没有同时出现using where,表明索引用来读取数据而非执行查找动作。

Using where:表明使用了where过滤

Using join buffer:使用了连接缓存

impossible where:where子句的值总是false,不能用来获取任何元祖

select tables optimized away:在没有group by子句的情况下,基于索引优化min/max操作或者对于MyISAM存储引擎优化count(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。

distinct:优化distinct操作,在找到第一匹配的元组后即停止找同样值的动作

案例










案例:







Explain分析:

  1. 观察,跑一天,看看生产的慢SQL情况
  2. 开启慢查询日志并捕获,设置阙值,比如超过5秒钟的就是慢SQL,并将它抓取出来。
  3. explain+慢SQL分析
  4. show profile查询SQL在MySQL服务器里面的执行细节和声明周期。
  5. SQL数据库服务器的参数调优,运维经理 or DBA的工作。

方式一优于方式二,永远小表驱动大表

//方式一:优
for(int i=5;...){for(int j=1000;...){}
}
//方式二
for(int i=1000;...){for(int j=5;...){}
}类似于嵌套循环Nested Loop

Explain总结

explain就像一面镜子,有事没事写完SQL记得explain一下。同时type的几种类型几乎都是语句索引之上的,因此需要对索引有个深入的了解,而且explain的结果可以指导我们什么时候加索引,什么时候不加索引,从而让我们更好的使用索引。

使用 explain 查看 索引是否生效!

1、explain select * from students;

2、explain extended select * from students;

id:SELECT识别符。这是SELECT的查询序列号。
select_type:SELECT类型,可以为以下任何一种:SIMPLE:简单SELECT(不使用UNION或子查询)PRIMARY:最外面的SELECTUNION:UNION中的第二个或后面的SELECT语句DEPENDENT UNION:UNION中的第二个或后面的SELECT语句,取决于外面的查询UNION RESULT:UNION 的结果SUBQUER:子查询中的第一个SELECTDEPENDENT SUBQUERY:子查询中的第一个SELECT,取决于外面的查询DERIVED:导出表的SELECT(FROM子句的子查询)
table:输出的行所引用的表
type:联接类型。下面给出各种联接类型,按照从最佳类型到最坏类型进行排序:system:表仅有一行(=系统表)。这是const联接类型的一个特例。const:表最多有一个匹配行,它将在查询开始时被读取。因为仅有一行,在这行的列值可被优化器剩余部分认为是常数。      const表很快,因为它们只读取一次!eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。这可能是最好的联接类型,除了const类型。ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取。ref_or_null:该联接类型如同ref,但是添加了MySQL可以专门搜索包含NULL值的行。index_merge:该联接类型表示使用了索引合并优化方法。unique_subquery:该类型替换了下面形式的IN子查询的ref: value IN (SELECT primary_key FROM             single_table WHERE some_expr) unique_subquery是一个索引查找函数,可以完全替换子查询,效率更        高。index_subquery:该联接类型类似于unique_subquery。可以替换IN子查询,但只适合下列形式的子查询中的非唯     一索引: value IN (SELECT key_column FROM single_table WHERE some_expr)。range:只检索给定范围的行,使用一个索引来选择行。index:该联接类型与ALL相同,除了只有索引树被扫描。这通常比ALL快,因为索引文件通常比数据文件小。ALL:对于每个来自于先前的表的行组合,进行完整的表扫描。
possible_keys:指出MySQL能使用哪个索引在该表中找到行。
key:显示MySQL实际决定使用的键(索引)。如果没有选择索引,键是NULL。
key_len:显示MySQL决定使用的键长度。如果键是NULL,则长度为NULL。
ref:显示使用哪个列或常数与key一起从表中选择行。
rows:显示MySQL认为它执行查询时必须检查的行数。多行之间的数据相乘可以估算要处理的行数。
filtered:显示了通过条件过滤出的行数的百分比估计值。
Extra:该列包含MySQL解决查询的详细信息。Distinct:MySQL发现第1个匹配行后,停止为当前的行组合搜索更多的行。Not exists:MySQL能够对查询进行LEFT JOIN优化,发现1个匹配LEFT JOIN标准的行后,不再为前面的的行组合在         该表内检查更多的行。range checked for each record (index map: #):MySQL没有发现好的可以使用的索引,但发现如果来自前      面的表的列值已知,可能部分索引可以使用。Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。Using index:从只使用索引树中的信息而不需要进一步搜索读取实际的行来检索表中的列信息。Using temporary:为了解决查询,MySQL需要创建一个临时表来容纳结果。Using where:WHERE 子句用于限制哪一个行匹配下一个表或发送到客户。Using sort_union(...), Using union(...), Using intersect(...):这些函数说明如何为                 index_merge联接类型合并索引扫描。Using index for group-by:类似于访问表的Using index方式,Using index for group-by表示MySQL发          现了一个索引,可以用来查 询GROUP BY或DISTINCT查询的所有列,而不要额外搜索硬盘访问实际的表。

十、SQL优化:

SQL语句慢的原因:

1、查询语句写的烂

2、索引失效

3、关联查询太多join(设计缺陷或不得已的需求)

4、服务器调优及各个参数设置(缓冲、线程数等)



对于MySQL层优化我一般遵从五个原则:

1、减少数据访问: 设置合理的字段类型,启用压缩,通过索引访问等减少磁盘IO
2、返回更少的数据: 只返回需要的字段和数据分页处理 减少磁盘io及网络io
3、减少交互次数: 批量DML操作,函数存储等减少数据连接次数
4、减少服务器CPU开销: 尽量减少数据库排序操作以及全表查询,减少cpu 内存占用
5、利用更多资源: 使用表分区,可以增加并行操作,更大限度利用cpu资源

总结到SQL优化中,就三点:

  • 最大化利用索引;
  • 尽可能避免全表扫描;
  • 减少无效数据的查询;

分析SQL:





1、避免索引失效场景

索引失效–存在索引但不使用索引:happy:

  • 尽量避免在字段开头模糊查询,会导致数据库引擎放弃索引进行全表扫描。

    SELECT * FROM t WHERE username LIKE '%陈%'优化方式:尽量在字段后面使用模糊查询。如下:
    SELECT * FROM t WHERE username LIKE '陈%'如果需求是要在前面使用模糊查询,
    1、使用MySQL内置函数INSTR(str,substr) 来匹配,作用类似于java中的indexOf(),查询字符串出现的角标位   置。
    2、使用FullText全文索引,用match against 检索
    3、数据量较大的情况,建议引用ElasticSearch、solr,亿级数据量检索速度秒级
    4、当表数据量较少(几千条儿那种),别整花里胡哨的,直接用like '%xx%'。
    
  • 尽量避免使用in 和not in,会导致引擎走全表扫描。

    SELECT * FROM t WHERE id IN (2,3)优化方式:如果是连续数值,可以用between代替。
    SELECT * FROM t WHERE id BETWEEN 2 AND 3如果是子查询,可以用exists代替.
    -- 不走索引
    select * from A where A.id in (select id from B);
    -- 走索引
    select * from A where exists (select * from B where B.id = A.id);
    
  • 尽量避免使用 or,会导致数据库引擎放弃索引进行全表扫描。

    SELECT * FROM t WHERE id = 1 OR id = 3优化方式:可以用union代替or。如下:
    SELECT * FROM t WHERE id = 1UNION
    SELECT * FROM t WHERE id = 3
    
  • 尽量避免进行null值的判断,会导致数据库引擎放弃索引进行全表扫描。

    SELECT * FROM t WHERE score IS NULL优化方式:可以给字段添加默认值0,对0值进行判断。如下:
    SELECT * FROM t WHERE score = 0
    
  • 尽量避免在where条件中等号的左侧进行表达式、函数操作,会导致数据库引擎放弃索引进行全表扫描。

    可以将表达式、函数操作移动到等号右侧。如下:-- 全表扫描
    SELECT * FROM T WHERE score/10 = 9
    -- 走索引
    SELECT * FROM T WHERE score = 10*9
    
  • 当数据量大时,避免使用where 1=1的条件。通常为了方便拼装查询条件,我们会默认使用该条件,数据库引擎会放弃索引进行全表扫描。

    SELECT username, age, sex FROM T WHERE 1=1优化方式:用代码拼装sql时进行判断,没 where 条件就去掉 where,有where条件就加 and。
    
  • 查询条件不能用 <> 或者 !=

    使用索引列作为条件进行查询时需要避免使用<>或者!=等判断条件。如确实业务需要,使用到不等于符号,需要在重新评估索引建立,避免在此字段上建立索引,改由查询条件中其他索引字段代替。
    
  • where条件仅包含复合索引非前置列

    如下:复合(联合)索引包含key_part1,key_part2,key_part3三列,但SQL语句没有包含索引前置列"key_part1",按照MySQL联合索引的最左匹配原则,不会走联合索引。select col1 from table where key_part2=1 and key_part3=2
    
  • 隐式类型转换造成不使用索引

    如下SQL语句由于索引对列类型为varchar,但给定的值为数值,涉及隐式类型转换,造成不能正确走索引。 select col1 from table where col_varchar=123;
    
  • order by 条件要与where中条件一致,否则order by不会利用索引进行排序

    -- 不走age索引
    SELECT * FROM t order by age;-- 走age索引
    SELECT * FROM t where age > 0 order by age;对于上面的语句,数据库的处理顺序是:
    第一步:根据where条件和统计信息生成执行计划,得到数据。
    第二步:将得到的数据排序。当执行处理数据(order by)时,数据库会先查看第一步的执行计划,看order by 的  字段是否在执行计划中利用了索引。如果是,则可以利用索引顺序而直接取得已经排好序的数据。如果不是,则重新进行排序操作。
    第三步:返回排序后的数据。
    当order by 中的字段出现在where条件中时,才会利用索引而不再二次排序,更准确的说,order by 中的字段在执行计划中利用了索引时,不用排序操作。这个结论不仅对order by有效,对其他需要排序的操作也有效。比如group by 、union 、distinct等。
    
  • 正确使用hint优化语句

    MySQL中可以使用hint指定优化器在执行时选择或忽略特定的索引。一般而言,处于版本变更带来的表结构索引变化,更建议避免使用hint,而是通过Analyze table多收集统计信息。但在特定场合下,指定hint可以排除其他索引干扰而指定更优的执行计划。USE INDEX 在你查询语句中表名的后面,添加 USE INDEX 来提供希望 MySQL 去参考的索引列表,就可以让 MySQL 不再考虑其他可用的索引。例子: SELECT col1 FROM table USE INDEX (mod_time, name)...
    IGNORE INDEX 如果只是单纯的想让 MySQL 忽略一个或者多个索引,可以使用 IGNORE INDEX 作为 Hint。例子: SELECT col1 FROM table IGNORE INDEX (priority) ...
    FORCE INDEX 为强制 MySQL 使用一个特定的索引,可在查询中使用FORCE INDEX 作为Hint。例子: SELECT col1 FROM table FORCE INDEX (mod_time) ...
    在查询的时候,数据库系统会自动分析查询语句,并选择一个最合适的索引。但是很多时候,数据库系统的查询优化器并不一定总是能使用最优索引。如果我们知道如何选择索引,可以使用FORCE INDEX强制查询使用指定的索引。例如:
    SELECT * FROM students FORCE INDEX (idx_class_id) WHERE class_id = 1 ORDER BY id DESC;
    
  • 避免在索引列上使用内置函数

    反例:
    EXPLAIN
    SELECT * FROM student
    WHERE DATE_ADD(birthday,INTERVAL 7 DAY) >=NOW();正例:
    EXPLAIN
    SELECT * FROM student
    WHERE  birthday >= DATE_ADD(NOW(),INTERVAL 7 DAY);理由:
    1、使用索引列上内置函数
    2、索引失效:
    

2、查询语句优化

  1. 避免在where中对字段进行表达式操作

    反例:
    EXPLAIN
    SELECT * FROM student WHERE id+1-1=+1正例:
    EXPLAIN
    SELECT * FROM student WHERE id=+1-1+1EXPLAIN
    SELECT * FROM student WHERE id=1理由:
    SQL解析时,如果字段相关的是表达式就进行全表扫描。
    
  2. 避免在where子句中使用!=或<>操作符

    应尽量避免在where子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描。记住实现业务优先,实在没办法,就只能使用,并不是不能使用。如果不能使用,SQL也就无需支持了。反例:
    EXPLAIN
    SELECT * FROM student WHERE salary!=3000EXPLAIN
    SELECT * FROM student WHERE salary<>3000理由:
    使用!=和<>很可能会让索引失效
    
  3. 去重distinct过滤字段要少

    #索引失效
    EXPLAIN
    SELECT DISTINCT * FROM student#索引生效
    EXPLAIN
    SELECT DISTINCT id,NAME FROM studentEXPLAIN
    SELECT DISTINCT NAME FROM student理由:
    带distinct的语句占用cpu时间高于不带distinct的语句。因为当查询很多字段时,如果使用distinct,数据库引擎就会对数据进行比较,过滤掉重复数据,然而这个比较、过滤的过程会占用系统资源,如cpu时间
    
  4. where中使用默认值代替null

    环境准备:#修改表,增加age字段,类型int,非空,默认值0
    ALTER TABLE student ADD age INT NOT NULL DEFAULT 0;#修改表,增加age字段的索引,名称为idx_age
    ALTER TABLE student ADD INDEX idx_age (age);反例:
    EXPLAIN
    SELECT * FROM student WHERE age IS NOT NULL正例:
    EXPLAIN
    SELECT * FROM student WHERE age>0理由:
    1、并不是说使用了is null 或者 is not null 就会不走索引了,这个跟mysql版本以及查询成本都有关
    2、如果mysql优化器发现,走索引比不走索引成本还要高,就会放弃索引,这些条件 !=,<>,is null,is not null经常被认为让索引失效,其实是因为一般情况下,查询的成本高,优化器自动放弃索引的
    3、如果把null值,换成默认值,很多时候让走索引成为可能,同时,表达意思也相对清晰一点
    
  5. 避免出现select

    首先,select * 操作在任何类型数据库中都不是一个好的SQL编写习惯。使用select * 取出全部列,会让优化器无法完成索引覆盖扫描这类优化,会影响优化器对执行计划的选择,也会增加网络带宽消耗,更会带来额外的I/O,内存和CPU消耗。建议提出业务实际需要的列数,将指定列名以取代select *。
    
  6. 避免出现不确定结果的函数

    特定针对主从复制这类业务场景。由于原理上从库复制的是主库执行的语句,
    使用如now()、rand()、sysdate()、current_user()等不确定结果的函数很容易导致主库与从库相应的数据不一致。另外不确定值的函数,产生的SQL语句无法利用query cache。
    
  7. 多表关联查询时,小表在前,大表在后。

    在MySQL中,执行 from 后的表关联查询是从左往右执行的(Oracle相反),
    第一张表会涉及到全表扫描,所以将小表放在前面,先扫小表,
    扫描快效率较高,在扫描后面的大表,
    或许只扫描大表的前100行就符合返回条件并return了。例如:
    表1有50条数据,表2有30亿条数据;
    如果全表扫描表2,你品,那就先去吃个饭再说吧是吧。
    
  8. 使用表的别名

    当在SQL语句中连接多个表时,请使用表的别名并把别名前缀于每个列名上。
    这样就可以减少解析的时间并减少哪些友列名歧义引起的语法错误。
    
  9. 用where字句替换having字句

    避免使用HAVING字句,因为HAVING只会在检索出所有记录之后才对结果集进行过滤,
    而where则是在聚合前刷选记录,如果能通过where字句限制记录的数目,
    那就能减少这方面的开销。HAVING中的条件一般用于聚合函数的过滤,除此之外,应该将条件写在where字句中。
    where和having的区别:where后面不能使用组函数
    
  10. 调整Where字句中的连接顺序

    MySQL采用从左往右,自上而下的顺序解析where子句。
    根据这个原理,应将过滤数据多的条件往前放,最快速度缩小结果集。
    
  11. 提高group by语句的效率

    可以在执行到该语句前,把不需要的记录过滤掉反例:先分组,再过滤
    select job,avg(salary) from employee
    group by job
    having job ='president' or job = 'managent';正例:先过滤,后分组
    select job,avg(salary) from employee
    where job ='president' or job = 'managent'
    group by job;默认情况下,MySQL 会对GROUP BY分组的所有值进行排序,如 “GROUP BY col1,col2,....;” 查询的方法如同在查询中指定 “ORDER BY col1,col2,...;” 如果显式包括一个包含相同的列的 ORDER BY子句,MySQL 可以毫不减速地对它进行优化,尽管仍然进行排序。因此,如果查询包括 GROUP BY 但你并不想对分组的值进行排序,你可以指定 ORDER BY NULL禁止排序。例如:
    SELECT col1, col2, COUNT(*) FROM table GROUP BY col1, col2 ORDER BY NULL ;#group by关键字优化(和order by的优化有类似)
    1.group by实质是先排序后进行分组,遵照索引建的最佳左前缀。
    2.当无法使用索引列时,增大max_length_for_sort_data参数的设置+增大sort_buffer_size参数的设置。
    3.where高于having,能写在where限定的条件就不要去having限定了。
    
  12. 复合索引最左特性

    创建复合索引,也就是多个字段
    ALTER TABLE student ADD INDEX idx_name_salary (NAME,salary)满足复合索引的左侧顺序,哪怕只是部分,复合索引生效
    EXPLAIN
    SELECT * FROM student WHERE NAME='name1'没有出现左边的字段,则不满足最左特性,索引失效
    EXPLAIN
    SELECT * FROM student WHERE salary=3000复合索引全使用,按左侧顺序出现 name,salary,索引生效
    EXPLAIN
    SELECT * FROM student WHERE NAME='陈子枢' AND salary=3000虽然违背了最左特性,但MYSQL执行SQL时会进行优化,底层进行颠倒优化
    EXPLAIN
    SELECT * FROM student WHERE salary=3000 AND NAME='name1'理由:
    1、复合索引也称为联合索引
    2、当我们创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则
    3、联合索引不满足最左原则,索引一般会失效,但是这个还跟Mysql优化器有关的
    
  13. inner join 、left join、right join,优先使用inner join

    三种连接如果结果相同,优先使用inner join,如果使用left join左边表尽量小inner join 内连接,只保留两张表中完全匹配的结果集
    left join会返回左表所有的行,即使在右表中没有匹配的记录
    right join会返回右表所有的行,即使在左表中没有匹配的记录理由:
    1、如果inner join是等值连接,返回的行数比较少,所以性能相对会好一点
    2、同理,使用了左连接,左边表数据结果尽量小,条件尽量放到左边处理,意味着返回的行数可能比较少。这是mysql     优化原则,就是小表驱动大表,小的数据集驱动大的数据集,从而让性能更优
    
  14. 优化join语句

    MySQL中可以通过子查询来使用 SELECT 语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另一个查询中。使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死,并且写起来也很容易。但是,有些情况下,子查询可以被更有效率的连接(JOIN)..替代。例子:假设要将所有没有订单记录的用户取出来,可以用下面这个查询完成:
    SELECT col1 FROM customerinfo WHERE CustomerID NOT in (SELECT CustomerID FROM salesinfo )如果使用连接(JOIN).. 来完成这个查询工作,速度将会有所提升。尤其是当 salesinfo表中对 CustomerID 建有索引的话,性能将会更好,查询如下:
    SELECT col1 FROM customerinfo LEFT JOIN salesinfoON customerinfo.CustomerID=salesinfo.CustomerID WHERE salesinfo.CustomerID IS NULL连接(JOIN).. 之所以更有效率一些,是因为 MySQL 不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作。
    
  15. 尽量使用union all替代union

    反例:
    SELECT * FROM student
    UNION
    SELECT * FROM student正例:
    SELECT * FROM student
    UNION ALL
    SELECT * FROM student理由:
    union和union all的区别是,union会自动去掉多个结果集合中的重复结果,而union all则将所有的结果全部显示出来,不管是不是重复
    union:对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序
    union在进行表链接后会筛选掉重复的记录,所以在表链接后会对所产生的结果集进行排序运算,删除重复的记录再返回结果。实际大部分应用中是不会产生重复的记录,最常见的是过程表与历史表UNION
    
  16. 适当使用commit

    适当使用commit可以释放事务占用的资源而减少消耗,commit后能释放的资源如下:事务占用的undo数据块;
    事务在redo log中记录的数据块;
    释放事务施加的,减少锁争用影响性能。特别是在需要使用delete删除大量数据的时候,必须分解删除量并定期commit。
  17. 避免重复查询更新的数据

    针对业务中经常出现的更新行同时又希望获得改行信息的需求,MySQL并不支持PostgreSQL那样的UPDATE RETURNING语法,在MySQL中可以通过变量实现。例如,更新一行记录的时间戳,同时希望查询当前记录中存放的时间戳是什么,简单方法实现:
    Update t1 set time=now() where col1=1;
    Select time from t1 where id =1; 使用变量,可以重写为以下方式:
    Update t1 set time=now () where col1=1 and @now: = now ();
    Select @now; 前后二者都需要两次网络来回,但使用变量避免了再次访问数据表,特别是当t1表数据量较大时,后者比前者快很多。
    
  18. 对于复杂的查询,可以使用中间临时表 暂存数据;

  19. 拆分复杂SQL为多个小SQL,避免大事务

    1、简单的SQL容易使用到MySQL的QUERY CACHE;
    2、减少锁表时间特别是使用MyISAM存储引擎的表;
    3、可以使用多核CPU。
    
  20. 使用合理的分页方式以提高分页效率

    使用合理的分页方式以提高分页效率 针对展现等分页需求,合适的分页方式能够提高分页的效率。#案例1:
    select * from t where thread_id = 10000 and deleted = 0 order by gmt_create asc limit 0, 15;上述例子通过一次性根据过滤条件取出所有字段进行排序返回。数据访问开销=索引IO+索引全部记录结果对应的表数据IO。因此,该种写法越翻到后面执行效率越差,时间越长,尤其表数据量很大的时候。适用场景:当中间结果集很小(10000行以下)或者查询条件复杂(指涉及多个不同查询字段或者多表连接)时适用。#案例2:
    select t.* from (select id from t where thread_id = 10000 and deleted = 0order by gmt_create asc limit 0, 15) a, t where a.id = t.id;
    上述例子必须满足t表主键是id列,且有覆盖索引secondary key:(thread_id, deleted, gmt_create)。通过先根据过滤条件利用覆盖索引取出主键id进行排序,再进行join操作取出其他字段。数据访问开销=索引IO+索引分页后结果(例子中是15行)对应的表数据IO。因此,该写法每次翻页消耗的资源和时间都基本相同,就像翻第一页一样。适用场景:当查询和排序字段(即where子句和order by子句涉及的字段)有对应覆盖索引时,且中间结果集很大的情况时适用。
    
  21. row_number() over()分组排序功能。

    查询数据量大的表 会造成查询缓慢。主要的原因是扫描行数过多。
    这个时候可以通过程序,分段分页进行查询,循环遍历,将结果合并处理进行展示。要查询100000到100050的数据,如下:
    SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY ID ASC) AS rowid,* FROM infoTab)t WHERE t.rowid > 100000 AND t.rowid <= 100050#row_number() over()分组排序功能:
    在使用 row_number() over()函数时候,over()里头的分组以及排序的执行晚于 where 、group by、  order by 的执行。#一次排序:对查询结果进行排序(无分组)
    select id,name,age,salary,row_number()over(order by salary desc) rn
    from TEST_ROW_NUMBER_OVER t#进一步排序:根据id分组排序
    select id,name,age,salary,row_number()over(partition by id order by salary desc) rank
    from TEST_ROW_NUMBER_OVER t#再一次排序:找出每一组中序号为一的数据
    select * from(select id,name,age,salary,row_number()over(partition by id order by salary desc) rank
    from TEST_ROW_NUMBER_OVER t)
    where rank <2#排序找出年龄在13岁到16岁数据,按salary排序
    select id,name,age,salary,row_number()over(order by salary desc)  rank
    from TEST_ROW_NUMBER_OVER t where age between '13' and '16'1.使用row_number()函数进行编号,如
    select email,customerID, ROW_NUMBER() over(order by psd) as rows from QT_Customer
    原理:先按psd进行排序,排序完后,给每条数据进行编号。2.在订单中按价格的升序进行排序,并给每条记录进行排序代码如下:
    select DID,customerID,totalPrice,ROW_NUMBER() over(order by totalPrice) as rows from OP_Order3.统计出每一个各户的所有订单并按每一个客户下的订单的金额 升序排序,同时给每一个客户的订单进行编号。这样就知道每个客户下几单了:
    select ROW_NUMBER() over(partition by customerID  order by totalPrice)as rows,customerID,totalPrice, DID from OP_Order4.统计每一个客户最近下的订单是第几次下的订单:
    with tabs as
    (
    select ROW_NUMBER() over(partition by customerID  order by totalPrice)as rows,customerID,totalPrice, DID from OP_Order  )
    select MAX(rows) as '下单次数',customerID from tabs
    group by customerID 5.统计每一个客户所有的订单中购买的金额最小,而且并统计改订单中,客户是第几次购买的:
    思路:利用临时表来执行这一操作。
    1.先按客户进行分组,然后按客户的下单的时间进行排序,并进行编号。
    2.然后利用子查询查找出每一个客户购买时的最小价格。
    3.根据查找出每一个客户的最小价格来查找相应的记录。with tabs as  (  select ROW_NUMBER() over(partition by customerID  order by insDT)
    as rows,customerID,totalPrice, DID from OP_Order  )  select * from tabs  where totalPrice in   (  select MIN(totalPrice)from tabs group by customerID  ) 6.筛选出客户第一次下的订单。
    思路。利用rows=1来查询客户第一次下的订单记录。with tabs as  (  select ROW_NUMBER() over(partition by customerID  order by insDT) as rows,* from OP_Order  )  select * from tabs where rows = 1 select * from OP_Order 7.注意:在使用over等开窗函数时,over里头的分组及排序的执行晚于“where,group by,order by”的执行。select   ROW_NUMBER() over(partition by customerID  order by insDT) as rows,  customerID,totalPrice, DID  from OP_Order where insDT>'2011-07-22
    

22.Order By优化:

#MySQL支持两种方式的排序,FileSort和Index。
Index:效率高,指MySQL扫描索引本身完成排序
FileSort:效率低,#Order By满足两种情况会使用Index方式排序:
1.order by语句使用索引最左前列
2.使用Where子句与Order By子句条件列满足索引最左前列。#filesort有两种方法:双路排序和单路排序
1.双路排序:MySQL4.1之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据。读取行指针和order by列,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出。
从磁盘取排序字段,在buffer进行排序,再从磁盘取其他字段。
取一批数据,要对磁盘进行了两次扫描,众所周知,I\O是很耗时的,所以在mysql4.1之后,出现了第二种改进的算法,就是单路排序。
2.单路排序:从磁盘读取查询需要的所有列,按照Order by列在buffer对它们进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间,因为它把每一行都保存在内存中了。#总结
由于单路是后出的,总体而言好过双路。
但是单路排序存在一定的问题:在sort_buffer中,方法B比方法A更多占用很多空间,因为方法B是把所有字段都取出,所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完序再去取sort_buffer容量大小,再排......从而多次I/O。
本来想省一次I/O操作,反而导致了大量的I/O操作,反而得不偿失。


案例:

3、增删改 DML 语句优化

  1. 大批量插入数据

    如果同时执行大量的插入,建议使用多个值的INSERT语句(方法二)。这比使用分开INSERT语句快(方法一),一般情况下批量插入效率有几倍的差别。方法一:
    insert into T values(1,2);
    insert into T values(1,3);
    insert into T values(1,4);方法二:
    Insert into T values(1,2),(1,3),(1,4); 选择后一种方法的原因有三。
    1、减少SQL语句解析的操作,MySQL没有类似Oracle的share pool,采用方法二,只需要解析一次就能进行数据的插入操作;
    2、在特定场景可以减少对DB连接次数
    3、SQL语句较短,可以减少网络传输的IO。
    
  2. 批量删除优化

    避免同时修改或删除过多数据,因为会造成cpu利用率过高,会造成锁表操作,从而影响别人对数据库的访问。反例:
    #一次删除10万或者100万+?
    delete from student where id <100000;#采用单一循环操作,效率低,时间漫长
    for(User user:list){delete from student;
    }正例:
    #分批进行删除,如每次500
    for(){
    delete student where id<500;delete student where id>=500 and id<1000;理由:
    一次性删除太多数据,可能造成锁表,会有lock wait timeout exceed的错误,所以建议分批操作
    
  3. 伪删除设计

    商品状态(state):1-上架、2-下架、3-删除理由:
    1、这里的删除只是一个标识,并没有从数据库表中真正删除,可以作为历史记录备查
    2、同时,一个大型系统中,表关系是非常复杂的,如电商系统中,商品作废了,但如果直接删除商品,其它商品详情,  物流信息中可能都有其引用。
    3、通过where state=1或者where state=2过滤掉数据,这样伪删除的数据用户就看不到了,从而不影响用户的使     用
    4、操作速度快,特别数据量很大情况下
    
  4. 查询优先还是更新(insert、update、delete)优先

  5. MySQL 还允许改变语句调度的优先级,它可以使来自多个客户端的查询更好地协作,这样单个客户端就不会由于锁定而等待很长时间。改变优先级还可以确保特定类型的查询被处理得更快。我们首先应该确定应用的类型,判断应用是以查询为主还是以更新为主的,是确保查询效率还是确保更新的效率,决定是查询优先还是更新优先。下面我们提到的改变调度策略的方法主要是针对只存在表锁的存储引擎,比如 MyISAM 、MEMROY、MERGE,对于Innodb 存储引擎,语句的执行是由获得行锁的顺序决定的。MySQL 的默认的调度策略可用总结如下

    1)写入操作优先于读取操作。
    2)对某张数据表的写入操作某一时刻只能发生一次,写入请求按照它们到达的次序来处理。
    3)对某张数据表的多个读取操作可以同时地进行。MySQL 提供了几个语句调节符,允许你修改它的调度策略:
    ·LOW_PRIORITY关键字应用于DELETE、INSERT、LOAD DATA、REPLACE和UPDATE;
    ·HIGH_PRIORITY关键字应用于SELECT和INSERT语句;
    ·DELAYED关键字应用于INSERT和REPLACE语句。如果写入操作是一个 LOW_PRIORITY(低优先级)请求,那么系统就不会认为它的优先级高于读取操作。在这种情况下,如果写入者在等待的时候,第二个读取者到达了,那么就允许第二个读取者插到写入者之前。只有在没有其它的读取者的时候,才允许写入者开始操作。这种调度修改可能存在 LOW_PRIORITY写入操作永远被阻塞的情况。SELECT 查询的HIGH_PRIORITY(高优先级)关键字也类似。
    它允许SELECT 插入正在等待的写入操作之前,即使在正常情况下写入操作的优先级更高。
    另外一种影响是,高优先级的 SELECT 在正常的 SELECT 语句之前执行,
    因为这些语句会被写入操作阻塞。
    如果希望所有支持LOW_PRIORITY 选项的语句都默认地按照低优先级来处理,
    那么 请使用--low-priority-updates 选项来启动服务器。
    通过使用 INSERTHIGH_PRIORITY 来把 INSERT 语句提高到正常的写入优先级,
    可以消除该选项对单个INSERT语句的影响。
    
  6. 使用truncate代替delete

    当删除全表中记录时,使用delete语句的操作会被记录到undo块中,
    删除记录也记录binlog,当确认需要删除全表时,
    会产生很大量的binlog并占用大量的undo数据块,此时既没有很好的效率也占用了大量的资源。使用truncate替代,不会记录可恢复的信息,数据不能被恢复。
    也因此使用truncate操作有其极少的资源占用与极快的时间。
    另外,使用truncate可以回收表的水位,使自增字段值归零。
    

4、建表优化

  1. 在表中建立索引,优先考虑where、order by使用到的字段。

  2. 尽量使用数字型字段(如性别,男:1 女:2)

    若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,
    并会增加存储开销。
    这是因为引擎在处理查询和连接时会 逐个比较字符串中每一个字符,
    而对于数字型而言只需要比较一次就够了。
    
  3. 用varchar/nvarchar 代替 char/nchar

    尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,
    可以节省存储空间,其次对于查询来说,
    在一个相对较小的字段内搜索效率显然要高些。
    不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,
    空间就固定了, 不管是否插入值(NULL也包含在内),
    都是占用 100个字符的空间的,如果是varchar这样的变长字段, null 不占用空间。
    

十一、存储过程

含义:一组预先编译好的SQL语句的集合。理解成批处理语句

  1. 提高代码的重用性
  2. 简化操作
  3. 减少了编译次数并且减少了和数据库服务器的连接次数,提高了效率
#创建存储过程:
create procedure 存储过程名(参数列表)
begin存储过程体(一组合法的SQL语句)
end#参数列表包含3部分:
参数模式 参数名 参数类型#参数模式:
in、out、inout
in:该参数可以作为输入,也就是该参数需要调用方传入值
out:该参数可以作为输出,也就是该参数可以作为返回值
inout:该参数既可以作为输入又可以作为输出,也就是该参数既需要传入值,又可以返回值。#调用:
call 存储过程名(实参列表)
举例:
调用in模式的参数:call sp1('值');
调用out模式的参数:set @name; call sp1(@name);
调用inout模式的参数:set @name=值; call sp1(@name); select @name;#查看:
show create procedure 存储过程名;#删除:
drop procedure 存储过程名;#调用语法
call 存储过程名(实参列表);#案例:
#插入到admin表中五条记录
delimiter $
create procedure myp1()
begininsert into admin(username,password)values('john1','0000'),('john2','0001'),('john3','0002'),('john4','0003'),('john5','0004');
end $
#调用
call myp1()$#创建带in模式参数的存储过程
#根据女生名,查询对应的男生信息
create procedure myp2(in beautyName varchar(20))
beginselect bo.* from boys bo right join beauty b on bo.id=b.boyfriend_idwhere b.name=beautyName;
end $
#调用
call myp2('柳岩')$#创建存储过程实现,用户是否登录成功
create procedure myp3(in username varchar(20),in password varhcar(20))
begindeclare result varchar(20) default'';#声明并初始化select count(*) into result#赋值from adminwhere admin.username=username and admin.password=password;select result;#使用select if(result>0,'成功','失败');#使用
end $
#调用
call myp3('张飞','8888')$#创建带out模式的存储过程
#根据女神名,返回对应的男神名
create procedure myp5(in beautyName varchar(20),out boyName varchar(20))
beginselect bo.boyName from boys bo inner join beauty b om bo.id=b.boyfriend_idwhere b.name=beautyName;
end $#调用
call myp5('小昭',@bName)$
select @bName$#根据女神名,返回对应的男神名和男神魅力值
create procedure myp6(in beautyName varchar(20),out boyName varchar(20),out userCP int)
beginselect bo.boyName,bo.userCP into boyName,userCP from boys bo inner join beauty b on bo.id=b.boyfriend_id where b.name=beautyName;
end $#调用
call myp6('小昭',@bName,@usercp)$#创建带inout模式参数的存储过程
#传入a和b两个值,最终a和b都翻倍并返回
create procedure myp8(inout a int,inout b int)
beginset a=a*2;set b=b*2;
end $
#调用
set @m=10$
set @n=20$
call myp8(@m,@n)$
select @m,@n$#创建存储过程实现传入用户名和密码,插入到admin表中
create procedure test_pro1(in username varchar(20),in loginPwd varchar(20))
begininsert into admin(admin.username,password)valuse(username,loginpwd);
end $#创建存储过程实现传入女神编号,返回女神名称和女神电话
create procedure test_pro2(in id int,out name varchar(20),out phone varchar(20))
beginselect b.name,b.phone into name,phone from beauty b where b.id=id;
end $#创建存储过程或函数实现传入两个女神生日,返回大小
create procedure test_pro3(in birth1 datetime,in birth2 datetime,out tesult int)
beginselect datediff(birth1,birth2) into result;
end $#创建存储过程或函数实现传入一个日期,格式化成xx年xx月xx日并返回
create procedure test_pro4(in mydate datetime,out strDate varchar(50))
beginselect date_format(mydate,'%yy年%m月%d日')into strDate;
end $
#调用:
call test_pro4(now(),@str)$
select @str $#创建存储过程或函数实现传入女神名称,返回:女神 and 男神 格式的字符串。如传入:小昭,返回:小昭and张无忌
create procedure test_pro5(in beautyName varchar(20),out str varchar(50))
beginselect concat(beautyName,'and',boyName)into strfrom boys boright join beauty b on b.boyfriend_id=bo.idwhere b.name=beautyName;
end $
#调用
call test_pro5('小昭',@str)$
select @str $#创建存储过程或函数,根据传入的条目数和起始索引,查询beauty表的记录
create procedure test_pro6(in startIndex int,in size int)
beginselect * from beauty limit startIndex,size;
end $
#调用
call test_pro6(3,5)$#删除存储过程
#语法:drop procedure 存储过程名
drop procedure p1;
drop procedure p2,p2,p3;#这样不行#查看存储过程的信息
desc mype;#这样不行
show create procedure myp2;

注意:

  1. 参数模式:in、out、inout,其中in可以省略

  2. 如果存储过程体仅仅只有一句话,begin end可以省略

  3. 存储过程体的每一条sql语句都需要用分号结尾

  4. 存储过程的结尾可以使用delimiter重新设置

    #语法:
    delimiter 结束标记
    delimiter $
    

系统变量

说明:变量是由系统提供的,不是用户定义,属于服务器层面。

系统变量分为:全局变量、会话变量

语法

#查看系统变量
show global|【session】variables; #查看满足条件的部分系统变量
show global|【session】variables like '%char%';#查看指定的系统变量的值
select  @@global|【session】.系统变量名;#为某个系统变量赋值
方式一:
set global|【session】系统变量名 = 值;
方式二:
set @@global|【session】.系统变量名=值;#注意:
如果是全局级别,则需要加global。如果是会话级别,则需要加session。如果不写,则默认session。

为系统变量赋值

#方式一:
set  【global|session】变量名=值;如果没有明显声明global还是session,则默认是session。#方式二:
set  @@global.变量名=值;
set  @@变量名=值;
全局变量

服务器层面上的,必须拥有super权限才能为系统变量赋值,作用域为整个服务器。

作用域:服务器每次启动将为所有的全局变量赋初始值,针对于所有的会话(连接)有效,但不能跨重启。

#查看所有的全局变量
show global variables;#查看部分的全局变量
show global variables like '%char%;'#查看指定的全局变量的值
select @@global.autocommit;
select @@tx_isolation;#为某个指定的全局变量赋值
set @@global.autocommit=0;
会话变量

服务器为每一个连接的客户端都提供了系统变量,

作用域:为当前的连接(会话)有效。

#查看所有的会话变量
show variables;
show session variables;#查看部分的会话变量
show variables like '%char%';
show session variables like '%char%';#查看指定的某个会话变量
select @@tx_isolation;
select @@session.tx_isolation;#为某个会话变量赋值
方式一:
set @@session.tx_isolation='read-uncommitted';
方式二:
set session tx_isolation='read-committed';

自定义变量

说明:变量是用户自定义的,不是由系统的

使用步骤:

  • 声明
  • 赋值
  • 使用(查看、比较、运算等)
用户变量

作用域:针对于当前连接(会话)生效,同于会话变量的作用域

位置:begin end里面,也可以放在外面

#赋值的操作符:
=或:=#声明并初始化
set @用户变量名=值;或
set @用户变量名:=值;或
select @用户变量名:=值;#赋值(更新用户变量的值)
#方式一:通过set或select
set @用户变量名=值;或
set @用户变量名:=值;或
select @用户变量名:=值;
#方式二:通过select into
select 字段 into 变量名 from 表;#使用(查看用户变量的值)
select @用户变量名;#案例:
set @name='john';
set @name=100;
set @count=1;
select count(*) into @count from employees;#声明两个变量并赋初始值,求和,并打印
set @m=1;
set @n=2;
set @sum=@m+@n;
select @sum;
局部变量

作用域:仅仅在定义它的begin end中有效,应用在begin end中的第一句话

位置:只能放在begin end中,而且只能放在第一句

#声明
declare 变量名 类型;
declare 变量名 类型 default 值;#赋值或更新
#方式一:通过set或select
set 局部变量名=值;或
set 局部变量名:=值;或
select @局部变量名;=值;
#方式二:通过select into
select 字段 into 局部变量名 from 表;#使用
select 局部变量名;#案例:
#声明两个变量并赋初始值,求和,并打印
declare m int default 1;
declare n int default 2;
declare sum int;
set sum=m+n;
select sum;

用户变量VS局部变量:

作用域 定义和使用的位置 语法
用户变量 当前会话 会话中的任何地方 必须加@符号,不用加限定类型
局部变量 begin end中 只能在begin end中,且为第一句话 一般不用加@符号,需要限定类型

函数

说明:类似于java中的方法,将一组完成特定功能的逻辑语句包装起来,对外暴露名字。

含义:一组预先编译好的SQL语句的集合,理解成批处理语句

  1. 提高代码的重用性
  2. 简化操作
  3. 减少了编译次数并且减少了和数据库服务器的连接次数,提高了效率。

函数和存储过程的区别:

存储过程:可以有0个返回,也可以有多个返回。适合做批量插入、批量更新。

函数:有且仅有1个返回。适合做处理数据后返回一个结果。

#创建语法
create function 函数名(参数列表) returns 返回类型
begin函数体
end
return 值;
注意:函数体中肯定需要有return语句#注意:
1.参数列表包含两部分:参数名、参数类型。
2.函数体:肯定会有return语句,如果没有会报错(如果return语句没有放在函数体的最后也不报错,但不建议)
3.函数体仅有一句话,则可以省略begin end
4.使用delimiter语句设置结束标记#调用语法
select 函数名(参数列表)#查看函数
show create function 函数名#删除函数
drop function 函数名;#案例
#无参有返回
#返回公司的员工个数
create function myf1() returns int
begindeclare c int dafult 0;#定义变量select count(*) into c#赋值from employees;return c;
end $
#调用
selec myf1()$#有参有返回
#根据员工名,返回它的工资
create function myf2(empName varcahr(20)) returns double
beginset @set=0;#定义用户变量select salary into @sal #赋值from employeeswhere last_name = empName;return @sal;
end $
#调用
select myf2('k_ing') $#根据部门名,返回该部门的平均工资
create function myf3(deptName varchar(20)) returns double
begindeclare sal double;select avg(salary) into salfrom employees ejoin departments d on e.department_id=d.department_idwhere d.department_name=deptName;return sal;
end $
#调用
select myf3('IT')$#创建函数、实现传入两个float,返回两者之和
create function test_fun1(num1 float,num2 float) returns float
begindeclare sum float default 0;set sum = num1+num2;return sum;
end $
#调用
select test_fun1(1,2)$

流程控制结构

说明:

  • 顺序结构:程序从上往下依次执行

  • 分支结构:程序按条件进行选择执行,或从两条或多条路径中选择一条执行。

  • 循环结构:程序满足一定条件下,重复执行一组代码。

分支结构:

if函数

#功能
实现简单双分支#语法:
if(表达式1,表达式2,表达式3)#执行顺序
如果表达式1成立,则if函数返回表达式2的值,否则返回表达式3的值#应用位置:
可以作为表达式放在任何位置

case结构

#功能:
实现多分支#情况1:类似于java中的switch语句,一般用于实现的等值判断。
#语法一:
case 变量|表达式|字段
when 要判断的值 then 返回的值1或语句1;
when 要判断的值 then 返回的值2或语句2;
..
else 要返回的值n或语句n;
end【case】;#情况2:类似于java中的多重if语句,一般用于实现区间判断
#语法二:
case
when 要判断的条件1 then 返回的值1或语句1;
when 要判断的条件2 then 返回的值2或语句2;
..
else 要返回的值n或语句n;
end【case】#特点:
1.可以作为表达式,嵌套在其他语句中使用,可以放在任何地方,begin end中或begin end的外面可以作为独立的语句去使用,只能放在begin end中
2.如果when中的值满足或条件成立,则执行对应的then后面的语句,并且结束case如果都不满足,则执行else中的语句或值
3.else可以省略,如果else省略了,并且所有when条件都不满足,则返回null#应用位置:
作为表达式,嵌套在其他语句中使用,可以放在任何地方,begin end中或begin end的外面
作为独立的语句使用,只能放在begin end中#示例1
CASEWHEN m.SetFlg = '1' THEN'是' ELSE '否' END AS 'setFlg',
#示例2
CASEt1.DisOrSales WHEN '0' THENt2.DiscountsNameEn ELSE t3.SalesPackageNameEn END AS SalesPackageNameEn,
#示例3
t1.DisOrSales WHEN '0' THENt2.DiscountsNameZh ELSE t3.SalesPackageNameZh END AS SalesPackageNameZh,
#创建存储过程,根据传入的成绩,来显示等级,比如传入的成绩:90-100显示A,80-90显示B,60-80显示C,否则显示D。
create procedure test_case(in score int)
begincasewhere score>=90 and score<=100 then select 'A';where score>=80 then select 'B';where score>=60 then select 'c';else select 'D';end case;
end $
#调用
call test_case(95)$

if结构

#功能
实现多重分支#语法
if 条件1 then 语法
elseif 条件2 then 语法2;
...
【else 语句 n】;
end if;#应用位置:
只能放在begin end#案例
#创建存储过程,根据传入的成绩,来显示等级,比如传入的成绩:90-100显示A,80-90显示B,60-80显示C,否则显示D。
create procedure test_if(score int)returns char
beginif score>=90 and score<=100 then return 'A';elseif score>=80 then return 'B';elseif score>=60 then return 'c';else return 'D';end if;
end $
#调用
call test_case(95)$

循环结构

whiel、loop、repeat

位置:只能放在begin end中

特点*都能实现循环结构

对比:这三种循环都可以省略名称,但如果循环中添加了循环控制语句(leave或iterate)则必须添加名称。

loop 一般用于实现简单的死循环。
while 先判断后执行
repeat 先执行后判断,无条件至少执行一次
名称 语法 特点 位置
while 先判断后执行 begin end中
repeat 先执行后判断 begin end中
loop 没有条件的死循环 begin end中
#while语法
Label:while loop_condition do
loop_list
end while label;#repeat语法
Label:repeat
loop_list
Until end_condition
end repeat label;#loop语法
Label:loop
loop_list
end loop label;

循环控制语句:

leave:类似于break,用于跳出所在的循环
iterate:类似于continue,用于结束本次循环,继续下一次。#案例:
#添加leave语句
#批量插入,根据次数插入到admin表中多条记录,如果次数>20则停止
create procedure pro_while1(in insertCount int)
begindeclare i int default 1;a:where i<=insertCount doinsert into admin(username,password)values(concat('Rose',i),'666');if i>20 then leven a;end if;set i=i+1;end while a;
end $
#调用
call pro_while1(100)$#添加iterate语句
#批量插入,根据次数插入到admin表中多条记录,只插入偶数次
create procedure test_while1(in insertCount int)
begindeclare i int default 0;a:while i<=insertCount doset i=i+1;if mod(i,2)!=0 then iterate a;end if;insert into admin(username,password)values(count('xiaohua',i),'0000');end while a;
end $
#调用
call test_while1(100)$

While

#语法:
【标签:】while 循环条件 do循环体
end while【标签】;  #案例
#没有添加循环控制语句
#批量插入,根据次数插入到admin表中多条记录
create procedure pro_while1(in insertCount int)
begindeclare i int default 1;where i<=insertCount doinsert into admin(username,password)values(concat('Rose',i),'666');set i=i+1;end while;
end $
#调用
call pro_while1(100)$#向该表中插入指定个数的随机字符串
create procedure test_randstr_insert(in insertCount int)
begindeclare i int default 1;#定义一个循环变量i,表示插入次数declare str varchar(26) default 'abcdefghijklmnopqrstuvwxyz';declare startIndex int default 1;#代表起始索引declare len int default 1;#代表截取的字符的长度while i<=insertCount doset len = floor(rand()*(20-startIndex+1)+1);#产生一个随机的整数,代表截取长度,1-(26-startIndex+1)set startIndex = floor(rand()*26+1);#产生一个随机的整数,代表起始索引1-26insert into stringcontent(content) values(substr(str,startIndex,len));set i=i+1;#循环变量更新end while;
end $

Loop

#语法:
【标签:】loop循环体;
end loop【标签】;可以用来模拟简单的死循环

Repeat

#语法:
【标签:】repeat循环体
until 结束循环的条件
end repeat【标签】;

十二、MySql集群

主从复制:

slave会从master读取binlog来进行数据同步,

主从都配置在mysqld节点下,都是小写

三步骤:

变记录到二进制日志(binary log)。这些记录过程叫做二进制日志事件,binary log events

slave将master的binary log events拷贝到它的中继日志(relay log)

重做中继日志中的时间,将改变应用到自己的数据库中,MySQL复制是异步的且串行化的

复制的基本原则:

每个slave只有一个master

每个slave只能有一个唯一的服务器ID

每个master可以有多个salve

复制的最大问题:

延迟












开始操作:

Mysql数据库没有增量备份的机制,当数据量太大的时候备份是一个很大的问题。还好mysql数据库提供了一种主从备份的机制,其实就是把主数据库的所有的数据同时写到备份的数据库中。实现mysql数据库的热备份。

要想实现双机的热备,首先要了解主从数据库服务器的版本的需求。要实现热备mysql的版本都高于3.2。还有一个基本的原则就是作为从数据库的数据版本可以高于主服务器数据库的版本,但是不可以低于主服务器的数据库版本。

当然要实现mysql双机热备,除了mysql本身自带的REPLICATION功能可以实现外,也可以用Heartbeat这个开源软件来实现。不过本文主要还是讲如何用mysql自带的REPLICATION来实现mysql双机热备的功能。

1. 准备服务器

由于Mysql不同版本之间的(二进制日志)binlog格式可能会不太一样,因此最好的搭配组合是主(Master)服务器的Mysql版本和从(Slave)服务器版本相同或者更低,主服务器的版本肯定不能高于从服务器版本。

本次我用于测试的两台服务器版本都是Mysql-5.5.17。

2. Mysql 建立主-从服务器双机热备配置步骤

2.1环境描述

A服务器(主服务器Master):59.151.15.36

B服务器(从服务器Slave):218.206.70.146

主从服务器的Mysql版本皆为5.5.17

Linux环境下

将主服务器需要同步的数据库内容进行备份一份,上传到从服务器上,保证始初时两服务器中数据库内容一致。

不过这里说明下,由于我是利用Mysql在安装后就有的数据库test进行测试的,所以两台服务器里面是没有建立表的,只不分别在test里面建立了同样的一张空表tb_mobile;

Sql语句如下:

mysql> create table tb_mobile( mobile VARCHAR(20) comment’手机号码’, time timestamp DEFAULT now() comment’时间’ );

2.2 主服务器Master配置

2.2.1 创建同步用户

进入mysql操作界面,在主服务器上为从服务器建立一个连接帐户,该帐户必须授予REPLICATION SLAVE权限。因为从mysql版本3.2以后就可以通过REPLICATION对其进行双机热备的功能操作。

操作指令如下:

mysql> grant replication slave on . to ‘replicate’@‘218.206.70.146’ identified by ‘123456’;

mysql> flush privileges;

创建好同步连接帐户后,我们可以通过在从服务器(Slave)上用replicat帐户对主服务器(Master)数据库进行访问下,看下是否能连接成功。

在从服务器(Slave)上输入如下指令:

[root@YD146 ~]# mysql -h59.151.15.36 -ureplicate -p123456

如果出现下面的结果,则表示能登录成功,说明可以对这两台服务器进行双机热备进行操作。

2.2.2 修改mysql配置文件

如果上面的准备工作做好,那边我们就可以进行对mysql配置文件进行修改了,首先找到mysql配置所有在目录,一般在安装好mysql服务后,都会将配置文件复制一一份出来放到/ect目录下面,并且配置文件命名为:my.cnf。即配置文件准确目录为/etc/my.cnf

(Linux下用rpm包安装的MySQL是不会安装/etc/my.cnf文件的,

至于为什么没有这个文件而MySQL却也能正常启动和作用,在点有两个说法,

第一种说法,my.cnf只是MySQL启动时的一个参数文件,可以没有它,这时MySQL会用内置的默认参数启动,

第二种说法,MySQL在启动时自动使用/usr/share/mysql目录下的my-medium.cnf文件,这种说法仅限于rpm包安装的MySQL,

解决方法,只需要复制一个/usr/share/mysql目录下的my-medium.cnf文件到/etc目录,并改名为my.cnf即可。)

找到配置文件my.cnf打开后,在[mysqld]下修改即可:

[mysqld]

server-id = 1        //唯一id

log-bin=mysql-bin //其中这两行是本来就有的,可以不用动,添加下面两行即可.指定日志文件

binlog-do-db = test     //记录日志的数据库

binlog-ignore-db = mysql //不记录日志的数据库

2.2.3 重启mysql服务

修改完配置文件后,保存后,重启一下mysql服务,如果成功则没问题。

2.2.4 查看主服务器状态

进入mysql服务后,可通过指令查看Master状态,输入如下指令:

注意看里面的参数,特别前面两个File和Position,在从服务器(Slave)配置主从关系会有用到的。

注:这里使用了锁表,目的是为了产生环境中不让进新的数据,好让从服务器定位同步位置,初次同步完成后,记得解锁。

2.3 从服务器Slave配置

2.3.1修改配置文件

因为这里面是以主-从方式实现mysql双机热备的,所以在从服务器就不用在建立同步帐户了,直接打开配置文件my.cnf进行修改即可,道理还是同修改主服务器上的一样,只不过需要修改的参数不一样而已。如下:

[mysqld]

server-id = 2

log-bin=mysql-bin

replicate-do-db = test

replicate-ignore-db = mysql,information_schema,performance_schema

2.3.2重启mysql服务

修改完配置文件后,保存后,重启一下mysql服务,如果成功则没问题。


2.3.3用change mster 语句指定同步位置

这步是最关键的一步了,在进入mysql操作界面后,输入如下指令:

mysql>stop slave; //先停步slave服务线程,这个是很重要的,如果不这样做会造成以下操作不成功。

mysql>change master to

>master_host=‘59.151.15.36’,master_user=‘replicate’,master_password=‘123456’,

> master_log_file=’ mysql-bin.000016 ',master_log_pos=107;

注:master_log_file, master_log_pos由主服务器(Master)查出的状态值中确定。也就是刚刚叫注意的。master_log_file对应File, master_log_pos对应Position。Mysql 5.x以上版本已经不支持在配置文件中指定主服务器相关选项。

遇到的问题,如果按上面步骤之后还出现如下情况:


则要重新设置slave。指令如下

mysql>stop slave;

mysql>reset slave;

之后停止slave线程重新开始。成功后,则可以开启slave线程了。

mysql>start slave;

2.3.4查看从服务器(Slave)状态

用如下指令进行查看

mysql> show slave status\G;

查看下面两项值均为Yes,即表示设置从服务器成功。

Slave_IO_Running: Yes

Slave_SQL_Running: Yes

2.4 测试同步

之前开始已经说过了在数据库test只有一个表tb_mobile没有数据,我们可以先查看下两服务器的数据库是否有数据:

Master:59.151.15.36


Slave:218.206.70.146


好了,现在可以在Master服务器中插入数据看下是否能同步。

Master:59.151.15.36

Slave:218.206.70.146


可以从上面两个截图上看出,在Master服务器上进行插入的数据在Slave服务器可以查到,这就表示双机热备配置成功了。

3. Mysql 建立主-主服务器双机热备配置步骤

服务器还是用回现在这两台服务器

3.1创建同步用户

同时在主从服务器建立一个连接帐户,该帐户必须授予REPLIATION SLAVE权限。这里因为服务器A和服务器B互为主从,所以都要分别建立一个同步用户。

服务器A:

mysql> grant replication slave on . to ‘replicate’@‘218.206.70.146’ identified by ‘123456’;

mysql> flush privileges;

服务器B:

mysql> grant replication slave on . to ‘replicate’@‘59.151.15.36’ identified by ‘123456’;

mysql> flush privileges;

3.2修改配置文件my.cnf

服务器A

[mysqld]

​ server-id = 1

log-bin=mysql-bin

binlog-do-db = test

​ binlog-ignore-db = mysql

#主-主形式需要多添加的部分

​ log-slave-updates

​ sync_binlog = 1

​ auto_increment_offset = 1

​ auto_increment_increment = 2

​ replicate-do-db = test

​ replicate-ignore-db = mysql,information_schema

服务器B:

[mysqld]

server-id = 2

log-bin=mysql-bin

​ replicate-do-db = test

​ replicate-ignore-db = mysql,information_schema,performance_schema

#主-主形式需要多添加的部分

​ binlog-do-db = test

​ binlog-ignore-db = mysql

​ log-slave-updates

​ sync_binlog = 1

​ auto_increment_offset = 2

​ auto_increment_increment = 2

3.3分别重启A服务器和B服务器上的mysql服务

重启服务器方式和上面的一样,这里就不做讲解了**。**

3.4分别查A服务器和B服务器作为主服务器的状态

服务器A:


服务器B:


3.5分别在A服务器和B服务器上用change master to 指定同步位置

服务器A:

mysql>change master to

>master_host=‘218.206.70.146’,master_user=‘replicate’,master_password=‘123456’,

> master_log_file=’ mysql-bin.000011 ',master_log_pos=497;

服务器B:

mysql>change master to

>master_host=‘59.151.15.36’,master_user=‘replicate’,master_password=‘123456’,

> master_log_file=’ mysql-bin.000016 ',master_log_pos=107;

3.6 分别在A和B服务器上重启从服务线程

mysql>start slave;

3.7 分别在A和B服务器上查看从服务器状态

mysql>show slave status\G;

查看下面两项值均为Yes,即表示设置从服务器成功。

Slave_IO_Running: Yes

Slave_SQL_Running: Yes

3.8 测试主-主同步例子

测试服务器A:

在服务器A上插入一条语句如下图所示:


之后在服务器B上查看是否同步如下图所示:


测试服务器B:

在服务器B上插入一条语句如下图所示:


然后在从服务器A上查看是否有同步数据如下图所示:


最后从结果可以看出主-主形式的双机热备是能成功实现的。

4. 配置参数说明

Server-id

ID值唯一的标识了复制群集中的主从服务器,因此它们必须各不相同。Master_id必须为1到232-1之间的一个正整数值,slave_id值必须为2到232-1之间的一个正整数值。

Log-bin

表示打开binlog,打开该选项才可以通过I/O写到Slave的relay-log,也是可以进行replication的前提。

Binlog-do-db

表示需要记录二进制日志的数据库。如果有多个数据可以用逗号分隔,或者使用多个binlog-do-dg选项。

Binglog-ingore-db

表示不需要记录二进制日志的数据库,如果有多个数据库可用逗号分隔,或者使用多binglog-ignore-db选项。

Replicate-do-db

表示需要同步的数据库,如果有多个数据可用逗号分隔,或者使用多个replicate-do-db选项。

Replicate-ignore-db

表示不需要同步的数据库,如果有多个数据库可用逗号分隔,或者使用多个replicate-ignore-db选项。

Master-connect-retry

master-connect-retry=n表示从服务器与主服务器的连接没有成功,则等待n秒(s)后再进行管理方式(默认设置是60s)。如果从服务器存在mater.info文件,它将忽略些选项。

Log-slave-updates

配置从库上的更新操作是否写入二进制文件,如果这台从库,还要做其他从库的主库,那么就需要打这个参数,以便从库的从库能够进行日志同步。

Slave-skip-errors

在复制过程,由于各种原因导致binglo中的sql出错,默认情况下,从库会停止复制,要用户介入。可以设置slave-skip-errors来定义错误号,如果复制过程中遇到的错误是定义的错误号,便可以路过。如果从库是用来做备份,设置这个参数会存在数据不一致,不要使用。如果是分担主库的查询压力,可以考虑。

slave-skip-errors=[err_code1,err_code2,...|all|ddl_exist_errors]
Command-Line Format --slave-skip-errors=name
Option-File Format slave-skip-errors
System Variable Name slave_skip_errors
Variable Scope Global
Dynamic Variable No
Permitted Values
Type string
Default OFF
Valid Values OFF
[list of error codes]
all
ddl_exist_errors

MySQL 5.6 as well as MySQL Cluster NDB 7.3 support an additional shorthand value
ddl_exist_errors, which is equivalent to the error code list 1007,1008,1050,1051,
1054,1060,1061,1068,1094,1146.
Examples:

–slave-skip-errors=1062,1053
–slave-skip-errors=all
–slave-skip-errors=ddl_exist_errors

Sync_binlog=1 Or N

Sync_binlog的默认值是0,这种模式下,MySQL不会同步到磁盘中去。这样的话,Mysql依赖操作系统来刷新二进制日志binary log,就像操作系统刷新其他文件的机制一样。因此如果操作系统或机器(不仅仅是Mysql服务器)崩溃,有可能binlog中最后的语句丢失了。要想防止这种情况,可以使用sync_binlog全局变量,使binlog在每N次binlog写入后与硬盘同步。当sync_binlog变量设置为1是最安全的,因为在crash崩溃的情况下,你的二进制日志binary log只有可能丢失最多一个语句或者一个事务。但是,这也是最慢的一种方式(除非磁盘有使用带蓄电池后备电源的缓存cache,使得同步到磁盘的操作非常快)。

即使sync_binlog设置为1,出现崩溃时,也有可能表内容和binlog内容之间存在不一致性。如果使用InnoDB表,Mysql服务器处理COMMIT语句,它将整个事务写入binlog并将事务提交到InnoDB中。如果在两次操作之间出现崩溃,重启时,事务被InnoDB回滚,但仍然存在binlog中。可以用-innodb-safe-binlog选项来增加InnoDB表内容和binlog之间的一致性。(注释:在Mysql 5.1版本中不需要-innodb-safe-binlog;由于引入了XA事务支持,该选项作废了),该选项可以提供更大程度的安全,使每个事务的binlog(sync_binlog=1)和(默认情况为真)InnoDB日志与硬盘同步,该选项的效果是崩溃后重启时,在滚回事务后,Mysql服务器从binlog剪切回滚的InnoDB事务。这样可以确保binlog反馈InnoDB表的确切数据等,并使从服务器保持与主服务器保持同步(不接收回滚的语句)。

Auto_increment_offset和Auto_increment_increment

Auto_increment_increment和auto_increment_offset用于主-主服务器(master-to-master)复制,并可以用来控制AUTO_INCREMENT列的操作。两个变量均可以设置为全局或局部变量,并且假定每个值都可以为1到65,535之间的整数值。将其中一个变量设置为0会使该变量为1。

这两个变量影响AUTO_INCREMENT列的方式:auto_increment_increment控制列中的值的增量值,auto_increment_offset确定AUTO_INCREMENT列值的起点。

如果auto_increment_offset的值大于auto_increment_increment的值,则auto_increment_offset的值被忽略。例如:表内已有一些数据,就会用现在已有的最大自增值做为初始值。

如何解决MySQL主从同步错误的SQL

解决: stop slave;
#表示跳过一步错误,后面的数字可变
set global sql_slave_skip_counter =1;start slave;
之后再用:mysql> show slave status\G
查看: Slave_IO_Running: YesSlave_SQL_Running: Yes ok,现在主从同步状态正常了。

一主一从常见配置:

mysql版本一直且后台以服务运行

主从都配置在【mysqld】节点下,都是小写

主机修改my.ini配置文件

从机修改my.cnf文件

配置完成后,主机+从机都重启后台mysql服务

主机从机都关闭防火墙

在windows主机上建立账户并授权slave

在Linux从机上配置需要复制的主机

主机新建库、新建表、insert记录,从机复制

如何 停止从服务复制功能:stop slave;

主机修改my.ini配置文件


在Windows主机上建立账户并授权slave

1、grant replication slave on *.* to 'zhangsan'@'从机器数据库IP' identified by '123456'2、#刷新该表
flush privileges;3、#查询master的状态:
show master status;
记录下File和Position的值4、执行完此步骤后不要再操作主服务器MySQL,防止主服务器状态值变化

在Linux从机上配置需要复制的主机

change master to master_host='主机IP',
master_user='zhangsan',
master_password='123456',
master_log_file='file名字',
master_log_pos=Position数字;2、#启动从服务器复制功能
start slave;
#停止从服务复制功能
stop slave;3、#下面两个参都是yes,说明主从配置成功
show slave status\G
Slave_IO_Runnion:Yes
Slave_SQL_Runniog:Yes

热备:

Mysql数据库没有增量备份的机制,当数据量太大的时候备份是一个很大的问题。还好mysql数据库提供了一种主从备份的机制,其实就是把主数据库的所有的数据同时写到备份的数据库中。实现mysql数据库的热备份。

要想实现双机的热备,首先要了解主从数据库服务器的版本的需求。要实现热备mysql的版本都高于3.2。还有一个基本的原则就是作为从数据库的数据版本可以高于主服务器数据库的版本,但是不可以低于主服务器的数据库版本。

当然要实现mysql双机热备,除了mysql本身自带的REPLICATION功能可以实现外,也可以用Heartbeat这个开源软件来实现。不过本文主要还是讲如何用mysql自带的REPLICATION来实现mysql双机热备的功能。

使用KeepAlived实现高可用的MYSQL_HA集群环境中,MYSQL为(Master/Master)主/主同步复制关系,保证MYSQL服务器数据的一致性,用KeepAlived提供虚拟IP,通过KeepAlived来进行故障监控,实现Mysql故障时自动切换。

布署环境拓朴如下:

Mysql VIP :192.168.187.61

Master1:192.168.187.129

Master:192.168.187.132

OS 环境:Cent OS 5.9

Mysql版本:Mysql5.5.31

1.安装mysql

因为CentOS的Mysql还是停留在5.0.19,而我们做Mysql之间的同步复制,Mysql版本至少要在Mysql5.1以上,所以要对其进行升级安装。

>>使用 yum安装, yum 可以帮你解决依赖于冲突

>>开启mysql服务

>>刚安装密码为空,设置root密码

>>更改mysql配置文件

>>登陆Mysql

2.Mysql主/主配置

2.1 设置配置文件

Mysql是通过日志进行同步复制的,先建立日志文件

在两台要进行备份的mysql服务器上的my.cnf文件进行配置如下(将下面的配置分别加入相关服务器的my.cnf):

Master1(192.168.187.129) Master(192.168.187.132)
#主标服务标识号,必需唯一server-id = 1#因为MYSQL是基于二进制的日志来做同步的,每个日志文件大小为 1Glog-bin=/var/log/mysql/mysql-bin.log#要同步的库名binlog-do-db = test#不记录日志的库,即不需要同步的库binlog-ignore-db=mysql#用从属服务器上的日志功能log-slave-updates#经过1日志写操作就把日志文件写入硬盘一次(对日志信息进行一次同步)。n=1是最安全的做法,但效率最低。默认设置是n=0。sync_binlog=1# auto_increment,控制自增列AUTO_INCREMENT的行为用于MASTER-MASTER之间的复制,防止出现重复值,auto_increment_increment=n有多少台服务器,n就设置为多少,auto_increment_offset=1设置步长,这里设置为1,这样Master的auto_increment字段产生的数值是:1, 3, 5, 7, …等奇数IDauto_increment_offset=1auto_increment_increment=2#进行镜像处理的数据库replicate-do-db = test#不进行镜像处理的数据库replicate-ignore-db= mysql #主标服务标识号,必需唯一server-id = 2#因为MYSQL是基于二进制的日志来做同步的,每个日志文件大小为 1Glog-bin=/var/log/mysql/mysql-bin.log#要同步的库名binlog-do-db = test#不记录日志的库,即不需要同步的库binlog-ignore-db=mysql#用从属服务器上的日志功能log-slave-updates#经过1日志写操作就把日志文件写入硬盘一次(对日志信息进行一次同步)。n=1是最安全的做法,但效率最低。默认设置是n=0。sync_binlog=1# auto_increment,控制自增列AUTO_INCREMENT的行为用于MASTER-MASTER之间的复制,防止出现重复值,auto_increment_increment=n有多少台服务器,n就设置为多少,auto_increment_offset=2设置步长,这里设置为2,这样Master的auto_increment字段产生的数值是:2, 4, 6, 8, …等奇数IDauto_increment_offset=2auto_increment_increment=2#进行镜像处理的数据库replicate-do-db = test#不进行镜像处理的数据库replicate-ignore-db= mysql

2.2查看配置情况

按上面的配置将两台服务器配置好以后,重新启动mysql服务,用showmaster status查看一下两台服务器的Master配置情况,可以看出已经配置成功,如下:

NO1:Master1(192.168.187.129)的情况

NO2:Master(192.168.187.132)的情况

2.3建立权限帐户,实现同步

a.创建账户并授予REPLICATION SLAVE权限

因为进行双向复制,两边服务器都需要建立一个用于复制的的用户。两边可以复用上面的语句,用户名和密码可以自行进行修改。

b.同步设置

Master1(192.168.187.129)上操作如下:

Master2(192.168.187.132)上面操作如下:

c.测试情况:

Step1:建一个测试表Test,两个字段,id与name字段,id字段为自增,两个服务器上面都是同样的结构,如下图:

Step2:我在Master1(192.168.187.129)表上执行一个insert语句,并进行查询,如下图:

Step3:在Master2(192.168.187.132)中查询,可以发现数据已经同步过来了,如下图:

3KeepAlived安装配置

3.1 KeepAlived的安装方法

可参照“高可用的负载均衡配置方法(Haproxy+KeepAlived)”5.1 中KeepAlived的安装方法

3.2将keepalived加入服务

可参照“高可用的负载均衡配置方法(Haproxy+KeepAlived)”5.2 中将keepalived加入服务

3.3 KeepAlived的配置

安装好以后,对其进行配置如下:

有两台机器(MASTER1)所在的192.168.187.129与(Master2)192.168.187.132,用(VIP)192.168.187.61做虚拟IP。

在两台服各器中的/etc/keepalived文件夹中的keepalived.conf下进行配置:

Master1的设置

192.168.187.129

global_defs {

router_id Mysql_HA  #当前节点名

}

vrrp_instance VI_1{

state BACKUP #两台配置节点均为BACKUP

interface eth0 #绑定虚拟IP的网络接口

virtual_router_id 51 #VRRP组名,两个节点的设置必须一样,以指明各个节点属于同一VRRP组

priority 100 #节点的优先级,另一台优先级改低一点

acvert_int 1 #组播信息发送间隔,两个节点设置必须一样

nopreempt #不抢占,只在优先级高的机器上设置即可,优先级低的机器不设置

authentication{ #设置验证信息,两个节点必须一致

auth_type PASS

auth_pass 1111

}

Virtual_ipaddress{ #指定虚拟IP,两个节点设置必须一样

192.168.187.61

}

}

virtual_server 192.168.187.61 3306 { #linux虚拟服务器(LVS)配置

delay_loop 2 #每个2秒检查一次real_server状态

lb_algo wrr #LVS调度算法,rr|wrr|lc|wlc|lblc|sh|dh

lb_kind DR #LVS集群模式 ,NAT|DR|TUN

persistence_timeout 60 #会话保持时间

protocol TCP #使用的协议是TCP还是UDP

real_server 192.168.187.129 3306 {

weight 3 #权重

notify_down /usr/local/bin/mysql.sh #检测到服务down后执行的脚本

TCP_CHECK {

connect_timeout 10 #连接超时时间

nb_get_retry 3 #重连次数

delay_before_retry 3 #重连间隔时间

connect_port 3306 #健康检查端口

}

}

Master2的设置

192.168.187.132

global_defs {

router_id Mysql_HA  #当前节点名

}

vrrp_instance VI_1{

state BACKUP #两台配置节点均为BACKUP

interface eth0 #绑定虚拟IP的网络接口

virtual_router_id 51 #VRRP组名,两个节点的设置必须一样,以指明各个节点属于同一VRRP组

priority 90 #节点的优先级,另一台优先级改低一点

acvert_int 1 #组播信息发送间隔,两个节点设置必须一样

authentication{ #设置验证信息,两个节点必须一致

auth_type PASS

auth_pass 1111

}

Virtual_ipaddress{ #指定虚拟IP,两个节点设置必须一样

192.168.187.61

}

}

virtual_server 192.168.187.61 3306 { #linux虚拟服务器(LVS)配置

delay_loop 2 #每个2秒检查一次real_server状态

lb_algo wrr #LVS调度算法,rr|wrr|lc|wlc|lblc|sh|dh

lb_kind DR #LVS集群模式 ,NAT|DR|TUN

persistence_timeout 60 #会话保持时间

protocol TCP #使用的协议是TCP还是UDP

real_server 192.168.187.132 3306 {

weight 3 #权重

notify_down /usr/local/bin/mysql.sh #检测到服务down后执行的脚本

TCP_CHECK {

connect_timeout 10 #连接超时时间

nb_get_retry 3 #重连次数

delay_before_retry 3 #重连间隔时间

connect_port 3306 #健康检查端口

}

}

脚本/usr/local/bin/mysql.sh

3.4 KeepAlived测试

可参照“高可用的负载均衡配置方法(Haproxy+KeepAlived)”5.4 中KeepAlived测试

4.Mysql测试

Step1:打开三个服务器进行查看,刚开始三个都为空

Step2:在VIP(192.168.187.61)服务器中插入一条数据

Step3:再查看三个服务器中的数据都已经同步过来了

当关掉做为主机的192.168.187.129做为宕机处理,同样也不会出问题,虚拟IP由192.168.187.129漂移

到192.168.187.132上面。

5.安装时出现的问题及处理方法

NO1: Slave将无法链接到 Master情况

错误:Slave将无法链接到 Master

原因:bind-address默认是127.0.0.1你必须更改它

解决办法:修改my.cnf,加上如下图红框所示的配置!

NO2: mysql error 1129 错误

错误:mysql 1129错误!如下图:

原因:是因为mysql将ip连接阻塞了。

解决办法:登录到mysql数据库服务器端,使用命令:

主从热备:

自己先预先在两台机器上安装mysql。

配置主服务器(开启二进制日志文件)
编辑主master服务器配置文件/etc/my.cnf
在[mysqld]节点下加入两句话
server-id=1 #MySQL服务ID唯一的
log-bin=mysql-bin #启用二进制日志;

重启服务:service mysql restart

登录mysql:mysql –uroot -proot
查看主节点的二进制文件(名称),position的值,为从节点挂接主节点准备数据;

mysql>flush tables with read lock; #数据库锁表,不让写数据;这步骤可不做,为了在配置同步时查询到的二进制文件(名称),position的值准确不会被更改
对于当前环境的mysql无需使用lock命令,因为没有人操作,但是生产环境中必须这样做

mysql>show master status; #查看MASTER状态(这两个值File和Position)其中的file就是二进制文件,position记录当前操作sql的步骤数(注意一条sql包含多步,所以不是sql语句的条数)

mysql>unlock tables; #从启动好后,记得要解除锁定
主数据库到此配置完毕

配置从服务器
修改/etc/my.cnf增加一行
server-id=2 #MySQL服务ID唯一的

log-bin=mysql-bin #启用二进制日志;

重启服务

service mysql restart
通过mysql命令配置同步日志的指向:(类似于redis的slaveof挂接主从)

mysql>change master to master_host='192.168.1.25', master_port=3306,
master_user='root',master_password='root',
master_log_file='mysql-bin.000001',
master_log_pos=120;
master_host 主服务器的IP地址
master_port 主服务器的PORT端口
master_log_file 和主服务器show master status中的File字段值相同
master_log_pos 和主服务器show master status中的Position字段值相同
mysql>start slave; #stop slave;停止服务,出错时先停止,再重新配置,这里启动从服务
mysql>show slave status\G; #查看SLAVE状态,\G结果纵向显示。必须大写
service mysql restart #重启服务

注意:如果出错,可以看后面的错误信息。观察Slave_SQL_Running_State字段,它会记录详细的错误信息

此处配置为一台主一台从,如果需要互为主从需要在主服务器上执行一遍从服务的内容即可
测试同步状态,如果一台主一台从只需在主操作,从查看即可,如果互为主从,需要在双方各自操作,在另外一方查看刚才的操作是否同步过来。
案例1:在主中创建表格,插入数据
观察从
案例2:将从节点中插入数据,然后在主里继续添加数据
观察主,从状态(如果为一主一从,此处会破坏主从结构,互为主从不影响)

案例3:对第二个案例的数据在主中进行变更,
观察从
由于第二步操作主从结构失效

这是发现没法同步,调用show slave status 发现已经报错

sql线程已经不工作了

id为3的重复,在从中有数据了

重新挂接(不能够轻易的在单机热备的从节点中操作写)
错误数据必须清除或者调整正确否则继续主从失效
查看主节点中的二进制文件名称 pos
停止从节点的从状态
stop slave

show master status;

在从节点中把查询出来的最新数据放到命令里挂接主节点

启动从节点的slave
start slave

但是这个时候发现id为3的对应b1字段的值没有改

所以mysql虽然支持主从关系但是并没有维护读写分离的状态

双机热备解决方案:

一、概念

1、热备份和备份的区别

热备份指的是:High Available(HA)即高可用,而备份指的是Backup,数据备份的一种。这是两种不同的概念,应对的产品也是两种功能上完全不同的产品。热备份主要保障业务的连续性,实现的方法是故障点的转移。而备份,主要目的是为了防止数据丢失,而做的一份拷贝,所以备份强调的是数据恢复而不是应用的故障转移。

2、什么是双机热备?

双机热备从广义上讲,就是对于重要的服务,使用两台服务器,互相备份,共同执行同一服务。当一台服务器出现故障时,可以由另一台服务器承担服务任务,从而在不需要人工干预的情况下,自动保证系统能持续提供服务。

从狭义上讲,双机热备就是使用互为备份的两台服务器共同执行同一服务,其中一台主机为工作机(Primary Server),另一台主机为备份主机(Standby Server)。在系统正常情况下,工作机为应用系统提供服务,备份机监视工作机的运行情况(一般是通过心跳诊断,工作机同时也在检测备份机是否正常),当工作机出现异常,不能支持应用系统运营时,备份机主动接管工作机的工作,继续支持关键应用服务,保证系统不间断的运行。双机热备针对的是IT核心服务器、存储、网络路由交换的故障的高可用性解决方案。

二、环境描述

1、master

系统:windows 7

数据库:mysql5.5

ip:192.168.0.123

2、slave

系统:windows 7

数据库:mysql5.5

ip:192.168.0.105

(注:主服务器的版本不能高于从服务器版本 ,两台服务器须处于同一局域网)

三、主从热备实现

1、账户准备

①在master服务器上为从服务器建立一个连接帐户,该帐户必须授予REPLICATION SLAVE权限。进入mysql操作界面,输入以下SQL:

grant replication slave on *.* to 'replicate'@'192.168.0.105' identified by '123456';
flush privileges;

操作如图:

②验证连接账户

在从服务器(slave)上用replicat帐户对主服务器(master)数据库进行访问,看是否可以连接成功。

在从服务器打开命令提示符,输入以下命令:

mysql -h192.168.0.123 -ureplicate -p123456

如果出现下面的结果,则表示能登录成功,说明可以对这两台服务器进行双机热备进行操作。

2、master配置

①修改mysql配置文件。找到my.ini配置文件打开后,在[mysqld]下修改即可:

[mysqld]

server-id = 123 #主ID,与从ID不能相同

log-bin=mysql-bin # 设定生成log文件名

binlog-do-db = test_db #设置同步数据库名

replicate-do-db=test_db # 从服务器同步数据库名

binlog-ignore-db = mysql #避免同步mysql用户配置

②重启mysql服务

打开命令提示符,输入以下两条命令完成重启:

net stop mysql
net start mysql

③查看master服务器状态

show master status;

④锁表

目的是为了产生环境中不让进新的数据,好让从服务器定位同步位置,初次同步完成后,记得解锁

flush tables with read lock;

步骤③④操作如图:

3、slave配置

①修改my.ini配置文件

log-bin=mysql-bin #设定生成log文件名
server-id=105 # 从ID,与主ID不能相同
binlog-do-db=test_db #设置同步数据库名
binlog-ignore-db=mysql #避免同步mysql用户配置
replicate-do-db=test_db # 从服务器同步数据库名
replicate-ignore-db = mysql,information_schema,performance_schema

②重启mysql服务

③用change mster 语句指定同步位置

进入mysql操作界面后,输入如下指令:

stop slave;
reset slave;
change master to master_host='192.168.0.123',
master_user='replicate',
master_password='123456',
master_log_file='mysql-bin.000124',
master_log_pos=107;
start slave;

注:这里的master_log_file、master_log_pos必须和前面show master status查询结果保持一致

操作如图:

4、解锁master表

unlock tables;

至此,主从热备实现完成,可进行测试操作。

MyCat

单机数据库瓶颈:

1.磁盘空间上限

2.服务器性能上限

3.单点故障

数据库性能瓶颈 :

1.单表性能瓶颈

2.单库性能瓶颈

3.读写性能瓶颈

解决方案1:读写分离

1.MySQL读写分离能提高系统性能的原因在于:

2.物理服务器增加,机器处理能力提升。拿硬件换性能。

3.主从只负责各自的读和写,极大程度缓解X锁和S锁争用。

4.slave可以配置myisam引擎,提升查询性能以及节约系统开销。

5.master直接写是并发的,slave通过主库发送来的binlog恢复数据是异步。

6.slave可以单独设置一些参数来提升其读的性能。

7.增加冗余,提高可用性。

解决方案2:分库分表

在我们的业务(web应用)中,关系型数据库本身比较容易成为系统性能瓶颈,单机存储容量、连接数、处理能力等都很有限,数据库本身的“有状态性”导致了它并不像Web和应用服务器那么容易扩展。那么在我们的业务中,是否真的有必要进行分库分表,就可以从上面几个条件来考虑。

1.单机储存容量。您的数据量是否在单机储存中碰到瓶颈。

2.连接数、处理能力。在我们的用户量达到一定程度时,特定时间的并发量又成了一个大问题,在一个高并发的网站中秒级数十万的并发量都是很正常的。在普通的单机数据库中秒级千次的操作问题都很大。

3.所以在我们进行分库分表之前我们最好考虑一下,我们的数据量是不是够大,并发量是不是够大。

分库分表之垂直分表:

垂直分表在日常开发和设计中比较常见,通俗的说法叫做“大表拆小表”,拆分是基于关系型数据库中的“列”(字段)进行的。通常情况,某个表中的字段比较多,可以新建立一张“扩展表”,将不经常使用或者长度较大的字段拆分出去放到“扩展表”中,如下图所示:

分库分表之垂直分库:

垂直分库在“微服务”盛行的今天已经非常普及了。基本的思路就是按照业务模块来划分出不同的数据库,而不是像早期一样将所有的数据表都放到同一个数据库中。如下图:

分库分表之水平切分:

水平切分是一个非常好的思路,将用户按一定规则(按id哈希)分组,并把该组用户的数据存储到一个数据库分片中,即一个sharding,这样随着用户数量的增加,只要简单地配置一台服务器即可,原理图如下:

分库分表之水平分表

水平分表也称为横向分表,比较容易理解,就是将表中不同的数据行按照一定规律分布到不同的数据库表中(这些表保存在同一个数据库中),这样来降低单表数据量,优化查询性能。最常见的方式就是通过主键或者时间等字段进行Hash和取模后拆分。如下图所示:

分库分表之水平分库分表

水平分库分表与上面讲到的水平分表的思想相同,唯一不同的就是将这些拆分出来的表保存在不同的数据库中。这也是很多大型互联网公司所选择的做法。如下图:

分库分表之数据库中间件:

2013年阿里的Cobar在社区使用过程中发现存在一些比较严重的问题,及其使用限制,经过Mycat发起人第一次改良,第一代改良版——Mycat诞生。 Mycat开源以后,一些Cobar的用户参与了Mycat的开发,最终Mycat发展成为一个由众多软件公司的实力派架构师和资深开发人员维护的社区型开源软件。

1.一个新颖的数据库中间件产品

2.一个彻底开源的,面向企业应用开发的大数据库集群

3.支持事务、ACID、可以替代MySQL的加强版数据库

4.一个可以视为MySQL集群的企业级数据库,用来替代昂贵的Oracle集群

5.一个融合内存缓存技术、NoSQL技术、HDFS大数据的新型SQL Server

6.结合传统数据库和新型分布式数据仓库的新一代企业级数据库产品

环境准备-mysql集群

通过docker准备mysql集群

拉取mysql镜像 docker pull mysql:5.7

启动多个mysql容器

docker run -itd -p 3301:3306 -e MYSQL_ROOT_PASSWORD=123456 docker.io/mysql:5.7

docker run -itd -p 3302:3306 -e MYSQL_ROOT_PASSWORD=123456 docker.io/mysql:5.7

docker run -itd -p 3303:3306 -e MYSQL_ROOT_PASSWORD=123456 docker.io/mysql:5.7

在多个mysql节点上创建多个database

mysql3301:CREATE DATABASE db1 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci

mysql3302:CREATE DATABASE db2 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci

mysql3303:CREATE DATABASE db3 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci

环境准备-mycat安装

官网 http://www.mycat.io/

下载链接 http://dl.mycat.io/1.6.6.1/

安装

环境准备-mycat系统设置

Server.xml

用户配置

端口配置

环境准备-mycat名词解释

Schema标签

schema 标签用于定义 MyCat 实例中的逻辑库,MyCat 可以有多个逻辑库,每个逻辑库都有自己的相关配置。可以使用 schema 标签来划分这些不同的逻辑库。

dataNode标签

dataNode 标签定义了 MyCat 中的数据节点,也就是我们通常说所的数据分片。一个 dataNode 标签就是一个独立的数据分片。

dataHost标签

该标签定义了具体的数据库实例、读写分离配置和心跳语句。

环境准备-mycat数据库配置:

Datahost

DataNode

Schema

Mycat结构图

Mycat分库分表-全局表:

一个真实的业务系统中,往往存在大量的类似字典表的表格,它们与业务表之间可能有关系,这种关系,可 以理解为“标签”,而不应理解为通常的“主从关系”,这些表基本上很少变动,可以根据主键 ID 进行缓存; 在分片的情况下,当业务表因为规模而进行分片以后,业务表与这些附属的字典表之间的关联,就成了比较 棘手的问题,考虑到字典表具有以下几个特性:

1.变动不频繁

2.数据量总体变化不大

3.数据规模不大,很少有超过数十万条记录。

鉴于此,MyCAT 定义了一种特殊的表,称之为“全局表”,全局表具有以下特性:

全局表的插入、更新操作会实时在所有节点上执行,保持各个分片的数据一致性

全局表的查询操作,只从一个节点获取

全局表可以跟任何一个表进行 JOIN 操作 将字典表或者符合字典表特性的一些表定义为全局表,则从另外一个方面,很好的解决了数据 JOIN 的难题。

Mycat分库分表-取模分片

实现方式:切分规则根据配置中输入的数值n。此种分片规则将数据分成n份(通常dn节点也为n),从而将数据均匀的分布于各节点上。

优点:这种策略可以很好的分散数据库写的压力。比较适合于单点查询的情景

缺点:不方便扩展;出现了范围查询,就需要MyCAT去合并结果,当数据量偏高的时候,这种跨库查询+合并结果消耗的时间有可能会增加很多,尤其是还出现了order by的时候

分库分表:

数据过多才会进行分库分表。

常用的框架原理

1.原理

1、框架模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
2、mysql master收到dump请求,开始推送binary log给slave(也就是canal)
3、框架解析binary log对象(原始为byte流)


2.框架

mysql-binlog-connector-java(https://github.com/shyiko/mysql-binlog-connector-java)

目前开源的 CDC (change data capture)工具,如 Zendesk maxwell、Redhat debezium、LinkedIn Databus 等都底层依赖 mysql-binlog-connector-java 或者其前身 open-replicator

不需要独立部署稳定性不是很好,时间久了会出现connetion lost(连接丢失)的情况。

canal (https://github.com/alibaba/canal)

需要独立部署canal server服务canal 已在阿里云推出商业化版本 数据传输服务DTS, 开通即用,
免去部署维护的昂贵使用成本。DTS针对阿里云RDS、
DRDS等产品进行了适配,解决了Binlog日志回收,主备切换、
VPC网络切换等场景下的订阅高可用问题。
同时,针对RDS进行了针对性的性能优化。

HA机制设计

canal的ha分为两部分,canal server和canal client分别有对应的ha(主备模式)实现canal server: 为了减少对mysql dump的请求,不同server上的instance要求同一时间只能有一个处于running,其他的处于standby状态.
canal client: 为了保证有序性,一份instance同一时间只能由一个canal client进行get/ack/rollback操作,否则客户端接收无法保证有序

canal 1.1.1版本之后, 默认支持将canal server接收到的binlog数据直接投递到MQ, 目前默认支持的MQ系统有:

Kafka,RocketMQ。1.1.3版本下修复了投递MQ模式,canal server HA在切换后不生效。

十四、JDBC

在java中,数据库存取技术可分为如下几类:

  • JDBC直接访问数据库
  • JDO技术(Java Data Object)
  • 第三方O/R工具,如Hibernate、Mybatis等、

JDBC是java访问数据库的基石,JDO、Hibernate等只是更好的封装了JDBC.

JDBC的好处:

  1. 程序员不用记多套API,减轻了开发压力

  2. 提高代码的维护性

什么是JDBC

JDBC(Java Data Connectivity),是一个 独立于特定数据库管理系统(DBMS)、通用的SQL数据库存取和操作的公共接口 (一组API),定义了用来访问数据库的标准Java类库,使用这个类库可以以一种标准的方法、方便地访问数据库资源。

JDBC是Java和数据库的连接技术,sun公司推出的一套java应用程序访问数据库的技术规范。

规范:抽象类或接口

JDBC为访问不同的数据库库提供了一种统一的途径,为开发者屏蔽了一些细节问题。

JDBC的目标是使Java程序员使用JDBC可以连接任何提供了JDBC驱动程序的数据库系统,这样就使得程序员无需对特定的数据库系统的特点有过多的了解,从而大大简化和加快了开发过程稿。

如果没有JDBC,那么Java程序访问数据库时时这样的:

JDBC使用:

前提:准备mysql的驱动包,加载到项目中
复制msql-connector-java-5.1.37-bin.jar到项目的根目录下或libs目录下,然后右击build path-add to build path1、加载驱动
2、获取连接
3、执行增删改查操作☆
4、关闭连接类的加载时机:1》new对象2》加载子类3》调用类中的静态成员4》通过反射使用new对象的方式加载类的不足:1、属于编译器加载,如果编译期间该类不存在,则直接报编译错误,也就是依赖性太强。2、导致Driver对象创建了两遍,效率较低。 public class testConnection1{public static void main(String[]args) thorw SQLException{//1加载驱动DriverManager.registerDrever(new Driver());//2获取连接Connection connection =   DriverManager.getConnection("jdbc://mysql://localhost:3306/girls","root","root");\System.out.println("连接成功");//3执行增删改查//3-1编写SQL语句String sql ="delete from beauty where id=9";//3-2获取执行SQL语句的命令对象Statement statement = connection.createStatement();//3-3使用命令对象指向SQL语句int update = statement.executeUpdate(sql);//3-4处理执行结果System.out.println(update>0?"success":"failure");//4.关闭连接statement.close();connection.close();}
}

采用反射的方式加载类:1、属于运行期加载,大大降低了类的依赖性。2、Driver对象仅仅创建了1遍,效率较高。public class testConnection2{public static void main(String[]args) thorw SQLException{Properties info = new Properties();info.load(new FileInputStream("scr\\jdbc.properties"));String user = info.getProperty("user");String password = info.getProperty("password");String driver = info.getProperty("dirver");String url = info.getProperty("url");//1加载驱动Class.forName(driver);//2获取连接Connection connection =    DriverManager.getConnection(url,user,password);System.out.println("连接成功");//3执行增删改查String sql ="delete from beauty where id=9";//获取执行SQL语句的命令对象Statement statement = connection.createStatement();//执行sql语句//int update = statement.executeUpdate(sql); //执行增删改查,返回受影响的行数//boolean execute = statement.execute(Sql); //执行任何sql语句ResultSet set = statement.executeQuery(sql); //执行查询语句,返回一个结果集//boolean flag = set.next();while(set.next()){int id = set.getInt(1);String name = set.getString(2);String sex = set.getString(3);Date date = set.getDate(4);System.out/println(id+"\t"+name+"\t"+sex+"\t"+date);}//4.关闭连接statement.close();statement.close();connection.close();}
}






使用PreparedStatement:

使用PreparedStatement的好处:

  1. 不在使用+拼接sql语句,减少语法错误,语义性强
  2. 将模板sql(固定的部分)和参数部分进行了分离,提高维护性
  3. 有效的解决了sql注入问题。
  4. 大大减少了编译次数,效率较高

    案例:

JDBC的相关API



PreparedStatement和Statement的区别:

JDBC Utils

此类是封装JDBC连接的工具类

功能:

  • 获取连接
  • 释放资源
//获取连接
public static Connection getConnection() throws Exception{  Properties info = new Properties();info.load(new FileInputStream("src\\jdbc.properties"));String user = info.getProperty("user");String password = info.getProperty("password");String driver = info.getProperty("dirver");String url = info.getProperty("url");//1加载驱动Class.forName(driver);       //2获取连接Connection connection =  DriverManager.getConnection(url,user,password);return connection;
}//功能:释放资源
public static void close(ResultSet set,Statement statemetn,Connection connection)thorws Exception{if(set!=null){set.close();}if(statement!=null){statement.close();}if(connection!=null){connection.close();}}

使用JDBC Utils

JDBC操作事务:

使用步骤:

1、开启事务:取消事务自动提交的功能

2、编写组成事务的一组sql语句

3、结束事务


批处理




Blob类型数据的读写:


数据库连接池





使用德鲁伊连接池:

开源框架的使用步骤:

  1. 导jar包
  2. 看帮助
  3. 调用方法,去使用





    QueryRunner类:

update(connection,sql,params):执行任何增删改语句

query(connection,sql,ResultSetHandler,params):执行任何查询语句

ResultSetHandler接口:

BeanHandler:将结果集的第一行,封装成对象,并返回new BeanHandler(XX.class)

BeanListHandler:将结果集中的所有航,封装成对象的集合,并返回new BeanListHandler(XX.class)

ScalarHandler:将结果集中的第一行第一列,以Object形式返回new ScalarHandler()

封装通用的增删改查:

public static int update(Stirng sql,Object...params){try{//1.连接Connect connect = JDBCUtilsByDruid.getConnection();//2.执行sql语句PreparedStatement statement = connection.prepareStatement(sql);for(int i=0;i<params.length;i++){statement.setObject(i+1,params[i]);}int update = statement.executeUpdate();return update;}catch(Exception e){e.printStackTrace();}
}

十五、缓冲池

MySQL修改的时候先修改缓冲池,然后在修改数据库。如果缓冲池中没有 就先修改数据库,然后添加到缓冲池中,

简介:

Innodb维护了一个缓存区域叫做Buffer Pool,用来缓存数据和索引在内存中。Buffer Pool可以用来加速数据的读写,如果Buffer Pool越大,那么Mysql就越像一个内存数据库,所以了解Buffer Pool的配置可以提高Buffer Pool的性能。

应用系统分层架构,为了加速数据访问,会把最常访问的数据,放在缓存(cache)里,避免每次都去访问数据库。

操作系统,会有缓冲池(buffer pool)机制,避免每次访问磁盘,以加速数据的访问。

MySQL作为一个存储系统,同样具有缓冲池(buffer pool)机制,以避免每次查询数据都进行磁盘IO。

InnoDB的缓冲池缓存什么?有什么用?

缓存表数据与索引数据,把磁盘上的数据加载到缓冲池,避免每次访问都进行磁盘IO,起到加速访问的作用。

为啥不把所有数据都放到缓冲池里

凡事都具备两面性,抛开数据易失性不说,访问快速的反面是存储容量小:

(1)缓存访问快,但容量小,数据库存储了200G数据,缓存容量可能只有64G;

(2)内存访问快,但容量小,买一台笔记本磁盘有2T,内存可能只有16G;

因此,只能把“最热”的数据放到“最近”的地方,以“最大限度”的降低磁盘访问。

如何管理与淘汰缓冲池,使得性能最大化呢?

在介绍具体细节之前,先介绍下“预读”的概念。

什么是预读?

磁盘读写,并不是按需读取,而是按页读取,一次至少读一页数据(一般是4K),如果未来要读取的数据就在页中,就能够省去后续的磁盘IO,提高效率。

预读为什么有效?

数据访问,通常都遵循“集中读写”的原则,使用一些数据,大概率会使用附近的数据,这就是所谓的“局部性原理”,它表明提前加载是有效的,确实能够减少磁盘IO。

按页(4K)读取,和InnoDB的缓冲池设计有啥关系?

(1)磁盘访问按页读取能够提高性能,所以缓冲池一般也是按页缓存数据;

(2)预读机制启示了我们,能把一些“可能要访问”的页提前加入缓冲池,避免未来的磁盘IO操作

InnoDB是以什么算法,来管理这些缓冲页呢?

最容易想到的,就是LRU(Least recently used)。

画外音:memcache,OS都会用LRU来进行页置换管理,但MySQL的玩法并不一样。

传统的LRU是如何进行缓冲页管理?

最常见的玩法是,把入缓冲池的页放到LRU的头部,作为最近访问的元素,从而最晚被淘汰。这里又分两种情况:

(1)页已经在缓冲池里,那就只做“移至”LRU头部的动作,而没有页被淘汰;

(2)页不在缓冲池里,除了做“放入”LRU头部的动作,还要做“淘汰”LRU尾部页的动作;

如上图,假如管理缓冲池的LRU长度为10,缓冲了页号为1,3,5…,40,7的页。

假如,接下来要访问的数据在页号为4的页中:

1)页号为4的页,本来就在缓冲池里;

(2)把页号为4的页,放到LRU的头部即可,没有页被淘汰;

画外音:为了减少数据移动,LRU一般用链表实现。

假如,再接下来要访问的数据在页号为50的页中:

(1)页号为50的页,原来不在缓冲池里;

(2)把页号为50的页,放到LRU头部,同时淘汰尾部页号为7的页;

传统的LRU缓冲池算法十分直观,OS,memcache等很多软件都在用,MySQL为啥这么矫情,不能直接用呢?

这里有两个问题:

(1)预读失效;

(2)缓冲池污染;

什么是预读失效?

由于预读(Read-Ahead),提前把页放入了缓冲池,但最终MySQL并没有从页中读取数据,称为预读失效。

如何对预读失效进行优化?

要优化预读失效,思路是:

(1)让预读失败的页,停留在缓冲池LRU里的时间尽可能短;

(2)让真正被读取的页,才挪到缓冲池LRU的头部;

以保证,真正被读取的热数据留在缓冲池里的时间尽可能长。

具体方法是:

(1)将LRU分为两个部分:

  • 新生代(new sublist)
  • 老生代(old sublist)

(2)新老生代收尾相连,即:新生代的尾(tail)连接着老生代的头(head);

(3)新页(例如被预读的页)加入缓冲池时,只加入到老生代头部:

  • 如果数据真正被读取(预读成功),才会加入到新生代的头部
  • 如果数据没有被读取,则会比新生代里的“热数据页”更早被淘汰出缓冲池

举个例子,整个缓冲池LRU如上图:

(1)整个LRU长度是10;

(2)前70%是新生代;

(3)后30%是老生代;

(4)新老生代首尾相连;

假如有一个页号为50的新页被预读加入缓冲池:

(1)50只会从老生代头部插入,老生代尾部(也是整体尾部)的页会被淘汰掉;

(2)假设50这一页不会被真正读取,即预读失败,它将比新生代的数据更早淘汰出缓冲池;

假如50这一页立刻被读取到,例如SQL访问了页内的行row数据:

(1)它会被立刻加入到新生代的头部;

(2)新生代的页会被挤到老生代,此时并不会有页面被真正淘汰;

改进版缓冲池LRU能够很好的解决“预读失败”的问题。

画外音:但也不要因噎废食,因为害怕预读失败而取消预读策略,大部分情况下,局部性原理是成立的,预读是有效的。

新老生代改进版LRU仍然解决不了缓冲池污染的问题。

什么是MySQL缓冲池污染?

当某一个SQL语句,要批量扫描大量数据时,可能导致把缓冲池的所有页都替换出去,导致大量热数据被换出,MySQL性能急剧下降,这种情况叫缓冲池污染。

例如,有一个数据量较大的用户表,当执行:

select * from user where name like “%shenjian%”;

虽然结果集可能只有少量数据,但这类like不能命中索引,必须全表扫描,就需要访问大量的页:

(1)把页加到缓冲池(插入老生代头部);

(2)从页里读出相关的row(插入新生代头部);

(3)row里的name字段和字符串shenjian进行比较,如果符合条件,加入到结果集中;

(4)…直到扫描完所有页中的所有row…

如此一来,所有的数据页都会被加载到新生代的头部,但只会访问一次,真正的热数据被大量换出。

怎么这类扫码大量数据导致的缓冲池污染问题呢?

MySQL缓冲池加入了一个“老生代停留时间窗口”的机制:

(1)假设T=老生代停留时间窗口;

(2)插入老生代头部的页,即使立刻被访问,并不会立刻放入新生代头部;

(3)只有满足“被访问”并且“在老生代停留时间”大于T,才会被放入新生代头部;

继续举例,假如批量数据扫描,有51,52,53,54,55等五个页面将要依次被访问。

如果没有“老生代停留时间窗口”的策略,这些批量被访问的页面,会换出大量热数据。

加入“老生代停留时间窗口”策略后,短时间内被大量加载的页,并不会立刻插入新生代头部,而是优先淘汰那些,短期内仅仅访问了一次的页。

而只有在老生代呆的时间足够久,停留时间大于T,才会被插入新生代头部。

上述原理,对应InnoDB里哪些参数?

有三个比较重要的参数。

参数:innodb_buffer_pool_size

介绍:配置缓冲池的大小,在内存允许的情况下,DBA往往会建议调大这个参数,越多数据和索引放到内存里,数据库的性能会越好。

参数:innodb_old_blocks_pct

介绍:老生代占整个LRU链长度的比例,默认是37,即整个LRU中新生代与老生代长度比例是63:37。

画外音:如果把这个参数设为100,就退化为普通LRU了。

参数:innodb_old_blocks_time

介绍:老生代停留时间窗口,单位是毫秒,默认是1000,即同时满足“被访问”与“在老生代停留时间超过1秒”两个条件,才会被插入到新生代头部。

总结

(1)缓冲池(buffer pool)是一种常见的降低磁盘访问的机制;

(2)缓冲池通常以页(page)为单位缓存数据;

(3)缓冲池的常见管理算法是LRU,memcache,OS,InnoDB都使用了这种算法;

(4)InnoDB对普通LRU进行了优化:

  • 将缓冲池分为老生代和新生代,入缓冲池的页,优先进入老生代,页被访问,才进入新生代,以解决预读失效的问题
  • 页被访问,且在老生代停留时间超过配置阈值的,才进入新生代,以解决批量数据访问,大量热数据淘汰的问题

Buffer Pool在数据库里的地位

1、回顾一下Buffer Pool是个什么东西?

数据库中的Buffer Pool是个什么东西?其实他是一个非常关键的组件,数据库中的数据实际上最终都是要存放在磁盘文件上的,如下图所示。

但是我们在对数据库执行增删改操作的时候,不可能直接更新磁盘上的数据的,因为如果你对磁盘进行随机读写操作,那速度是相当的慢,随便一个大磁盘文件的随机读写操作,可能都要几百毫秒。如果要是那么搞的话,可能你的数据库每秒也就只能处理几百个请求了! 在对数据库执行增删改操作的时候,实际上主要都是针对内存里的Buffer Pool中的数据进行的,也就是实际上主要是对数据库的内存里的数据结构进行了增删改,如下图所示。

其实每个人都担心一个事,就是你在数据库的内存里执行了一堆增删改的操作,内存数据是更新了,但是这个时候如果数据库突然崩溃了,那么内存里更新好的数据不是都没了吗? MySQL就怕这个问题,所以引入了一个redo log机制,你在对内存里的数据进行增删改的时候,他同时会把增删改对应的日志写入redo log中,如下图。

万一你的数据库突然崩溃了,没关系,只要从redo log日志文件里读取出来你之前做过哪些增删改操作,瞬间就可以重新把这些增删改操作在你的内存里执行一遍,这就可以恢复出来你之前做过哪些增删改操作了。 当然对于数据更新的过程,他是有一套严密的步骤的,还涉及到undo log、binlog、提交事务、buffer pool脏数据刷回磁盘,等等。

2、Buffer Pool的一句话总结

Buffer Pool是数据库中我们第一个必须要搞清楚的核心组件,因为增删改操作首先就是针对这个内存中的Buffer Pool里的数据执行的,同时配合了后续的redo log、刷磁盘等机制和操作。

所以Buffer Pool就是数据库的一个内存组件,里面缓存了磁盘上的真实数据,然后我们的系统对数据库执行的增删改操作,其实主要就是对这个内存数据结构中的缓存数据执行的。

Buffer Pool这个内存数据结构到底长个什么样子?

1、如何配置你的Buffer Pool的大小?

我们应该如何配置你的Buffer Pool到底有多大呢? 因为Buffer Pool本质其实就是数据库的一个内存组件,你可以理解为他就是一片内存数据结构,所以这个内存数据结构肯定是有一定的大小的,不可能是无限大的。 这个Buffer Pool默认情况下是128MB,还是有一点偏小了,我们实际生产环境下完全可以对Buffer Pool进行调整。比如我们的数据库如果是16核32G的机器,那么你就可以给Buffer Pool分配个2GB的内存,使用下面的配置就可以了。 [server] innodb_buffer_pool_size = 2147483648 如果你不知道数据库的配置文件在哪里以及如何修改其中的配置,那建议可以先在网上搜索一些MySQL入门的资料去看看,其实这都是最基础和简单的。 我们先来看一下下面的图,里面就画了数据库中的Buffer Pool内存组件

2、数据页:MySQL中抽象出来的数据单位

假设现在我们的数据库中一定有一片内存区域是Buffer Pool了,那么我们的数据是如何放在Buffer Pool中的?

我们都知道数据库的核心数据模型就是 表+字段+行 的概念,所以大家觉得我们的数据是一行一行的放在Buffer Pool里面的吗? 这就明显不是了,实际上MySQL对数据抽象出来了一个数据页的概念,他是把很多行数据放在了一个数据页里,也就是说我们的磁盘文件中就是会有很多的数据页,每一页数据里放了很多行数据,如下图所示。

所以实际上假设我们要更新一行数据,此时数据库会找到这行数据所在的数据页,然后从磁盘文件里把这行数据所在的数据页直接给加载到Buffer Pool里去。 也就是说,Buffer Pool中存放的是一个一个的数据页,如下图。

3、磁盘上的数据页和Buffer Pool中的缓存页是如何对应起来的?

实际上默认情况下,磁盘中存放的数据页的大小是16KB,也就是说,一页数据包含了16KB的内容。 而Buffer Pool中存放的一个一个的数据页,我们通常叫做缓存页,因为毕竟Buffer Pool是一个缓冲池,里面的数据都是从磁盘缓存到内存去的。 而Buffer Pool中默认情况下,一个缓存页的大小和磁盘上的一个数据页的大小是一一对应起来的,都是16KB。 我们看下图,我给图中的Buffer Pool标注出来了他的内存大小,假设他是128MB吧,然后数据页的大小是16KB。

4、缓存页对应的描述信息是什么?

对于每个缓存页,他实际上都会有一个描述信息,这个描述信息大体可以认为是用来描述这个缓存页的。 比如包含如下的一些东西:这个数据页所属的表空间、数据页的编号、这个缓存页在Buffer Pool中的地址以及别的一些杂七杂八的东西。 每个缓存页都会对应一个描述信息,这个描述信息本身也是一块数据,在Buffer Pool中,每个缓存页的描述数据放在最前面,然后各个缓存页放在后面。所以此时我们看下面的图,Buffer Pool实际看起来大概长这个样子 。

而且这里我们要注意一点,Buffer Pool中的描述数据大概相当于缓存页大小的5%左右,也就是每个描述数据大概是800个字节左右的大小,然后假设你设置的buffer pool大小是128MB,实际上Buffer Pool真正的最终大小会超出一些,可能有个130多MB的样子,因为他里面还要存放每个缓存页的描述数据。

思考

对于Buffer Pool而言,他里面会存放很多的缓存页以及对应的描述数据,那么假设Buffer Pool里的内存都用尽了,已经没有足够的剩余内存来存放缓存页和描述数据了,此时Buffer Pool里就一点内存都没有了吗?还是说Buffer Pool里会残留一些内存碎片呢? 如果你觉得Buffer Pool里会有内存碎片的话,那么你觉得应该怎么做才能尽可能减少Buffer Pool里的内存碎片呢?

在生产环境中,如何基于机器配置来合理设置Buffer Pool?

1、生产环境中应该给buffer pool设置多少内存?

今天这篇文章我们接着上一次讲解的Buffer Pool的一些内存划分的原理,来给大家最后总结一下,在生产环境中到底应该如何设置Buffer Pool的大小呢。 首先考虑第一个问题,我们现在数据库部署在一台机器上,这台机器可能有个8G、16G、32G、64G、128G的内存大小,那么此时buffer pool应该设置多大呢? 有的人可能会想,假设我有32G内存,那么给buffer pool设置个30GB得了,这样的话,MySQL大量的crud操作都是基于内存来执行的,性能那是绝对高! 这么想就大错特错了,虽然你的机器有32GB的内存,但是你的操作系统内核就要用掉起码几个GB的内存!你的机器上可能还有别的东西在运行!你的数据库里除了buffer pool是不是还有别的内存数据结构! 所以上面那种想法是绝对不可取的! 如果你胡乱设置一个特别大的内存给buffer,会导致你的mysql启动失败的,他启动的时候就发现操作系统的内存根本不够用! 所以通常来说,我们建议一个比较合理的、健康的比例,是给buffer pool设置你的机器内存的50%~60%左右 比如你有32GB的机器,那么给buffer设置个20GB的内存,剩下的留给OS和其他人来用,这样比较合理一些。 假设你的机器是128GB的内存,那么buffer pool可以设置个80GB左右,大概就是这样的一个规则。

2、buffer pool总大小=(chunk大小 * buffer pool数量)的2倍数

接着确定了buffer pool的总大小之后,就得考虑一下设置多少个buffer pool,以及chunk的大小。 此时有一个很关键的公式:buffer pool总大小 = (chunk大小 * buffer pool数量) 的倍数 比如默认的chunk大小是128MB,那么此时如果你的机器的内存是32GB,你打算给buffer pool总大小在20GB左右,此时你的buffer pool的数量应该是多少个呢?

假设你的buffer pool的数量是16个,这是没问题的,那么此时chunk大小 * buffer pool的数量 = 16 * 128MB = 2048MB,然后buffer pool总大小如果是20GB,此时buffer pool总大小就是2048MB的10倍,这就符合规则了。 当然,此时你可以设置多一些buffer pool数量,比如设置32个buffer pool,那么此时buffer pool总大小(20GB)就是(chunk大小128MB * 32个buffer pool)的5倍,也是可以的。 那么此时你的buffer pool大小就是20GB,然后buffer pool数量是32个,每个buffer pool的大小是640MB,然后每个buffer pool包含5个128MB的chunk,算下来就是这么一个结果了。

3、一点总结

数据库在生产环境运行时,必须根据机器的内存设置合理的buffer pool的大小,然后设置buffer pool的数量,这样可以尽可能的保证你的数据库的高性能和高并发能力。 在线上运行时,buffer pool是有多个的,每个buffer pool里多个chunk但是共用一套链表数据结构,然后执 行crud的时候,就会不停的加载磁盘上的数据页到缓存页里来,然后会查询和更新缓存页里的数据,同时维护一系列的链表结构。 然后后台线程定时根据lru链表和flush链表,去把一批缓存页刷入磁盘释放掉这些缓存页,同时更新free链表。 如果执行crud的时候发现缓存页都满了,没法加载自己需要的数据页进缓存,此时就会把lru链表冷数据区域的缓存页刷入磁盘,然后加载自己需要的数据页进来。 整个buffer pool的结构设计以及工作原理,就是上面我们总结的这套东西了,大家只要理解了这个,首先你对MySQL执行crud的时候,是如何在内存里查询和更新数据的,你就彻底明白了。

接着我们后面继续探索undo log、redo log、事务机制、事务隔离、锁机制,这些东西,一点点就把MySQL他的数据更新、事务、锁这些原理,全部搞清楚了,同时中间再配合穿插一些生产经验、实战案例。

4、buffer pool的配置

innodb_buffer_pool_size:缓存区域的大小。
innodb_buffer_pool_chunk_size:当增加或减少innodb_buffer_pool_size时,操作以块(chunk)形式执行。块大小由innodb_buffer_pool_chunk_size配置选项定义,默认值128M。
innodb_buffer_pool_instances:当buffer pool比较大的时候(超过1G),innodb会把buffer pool划分成几个instances,这样可以提高读写操作的并发,减少竞争。读写page都使用hash函数分配给一个instances。
当增加或者减少buffer pool大小的时候,实际上是操作的chunk。buffer pool的大小必须是innodb_buffer_pool_chunk_sizeinnodb_buffer_pool_instances,如果配置的innodb_buffer_pool_size不是innodb_buffer_pool_chunk_sizeinnodb_buffer_pool_instances的倍数,buffer pool的大小会自动调整为innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances的倍数,自动调整的值不少于指定的值。
如果指定的buffer大小是9G,instances的个数是16,chunk默认的大小是128M,那么buffer会自动调整为10G。具体的配置可以参考mysql官网的介绍mysql reference

SHOW ENGINE INNODB STATUS

当你的数据库启动之后,你随时可以通过上述命令,去查看当前innodb里的一些具体情况,执行SHOW ENGINEINNODB STATUS就可以了。此时你可能会看到如下一系列的东西:

Total memory allocated xxxx;
Dictionary memory allocated xxx
Buffer pool size xxxx
Free buffers xxx
Database pages xxx
Old database pages xxxx
Modified db pages xx
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young xxxx, not young xxx
xx youngs/s, xx non-youngs/s
Pages read xxxx, created xxx, written xxx
xx reads/s, xx creates/s, 1xx writes/s
Buffer pool hit rate xxx / 1000, young-making rate xxx / 1000 not xx / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: xxxx, unzip_LRU len: xxx
I/O sum[xxx]:cur[xx], unzip sum[16xx:cur[0]

下面解释一下这里的东西,主要讲解这里跟buffer pool相关的一些东西。

  1. Total memory allocated,这就是说buffer pool最终的总大小是多少
  2. Buffer pool size,这就是说buffer pool一共能容纳多少个缓存页
  3. Free buffers,这就是说free链表中一共有多少个空闲的缓存页是可用的
  4. Database pages和Old database pages,就是说lru链表中一共有多少个缓存页,以及冷数据区域里的缓存页数量
  5. Modified db pages,这就是flush链表中的缓存页数量
  6. Pending reads和Pending writes,等待从磁盘上加载进缓存页的数量,还有就是即将从lru链表中刷入磁盘的数量、即将从flush链表中刷入磁盘的数量
  7. Pages made young和not young,这就是说已经lru冷数据区域里访问之后转移到热数据区域的缓存页的数 量,以及在lru冷数据区域里1s内被访问了没进入热数据区域的缓存页的数量
  8. youngs/s和not youngs/s,这就是说每秒从冷数据区域进入热数据区域的缓存页的数量,以及每秒在冷数据区域里被访问了但是不能进入热数据区域的缓存页的数量
  9. Pages read xxxx, created xxx, written xxx,xx reads/s, xx creates/s, 1xx writes/s,这里就是说已经读取、创建和写入了多少个缓存页,以及每秒钟读取、创建和写入的缓存页数量
  10. Buffer pool hit rate xxx / 1000,这就是说每1000次访问,有多少次是直接命中了buffer pool里的缓存的
  11. young-making rate xxx / 1000 not xx / 1000,每1000次访问,有多少次访问让缓存页从冷数据区域移动到了热数据区域,以及没移动的缓存页数量
  12. LRU len:这就是lru链表里的缓存页的数量
  13. I/O sum:最近50s读取磁盘页的总数
  14. I/O cur:现在正在读取磁盘页的数量

LRU算法

为了管理这些数据,innodb使用了一些链表。
lru链表:用来存储内存中的缓存数据。
free链表:用来存放所有的空闲页,每次需要数据页存储数据时,就首先检测free中有没有空闲的页来分配。
flush链表:在内存中被修改但还没有刷新到磁盘的数据页列表,就是所谓的脏页列表,内存中的数据跟对应的磁盘上的数据不一致,属于该列表的页面同样存在于lru列表中,但反之未必。

防止缓存污染

思考一个问题,如果msyql做一次全表扫描,那么全表扫面的数据就会放到buffer pool中,而且全表扫描的数据大部分都是用不到的,那么之前的热点数据也就被冲掉了。所以innodb有一些策略来防止缓存污染。
在Buffer Pool中,存储数据的最小单位是页,默认是16K,使用LRU算法的变体来进行页数据的淘汰和置换。Buffer Pool把LRU链表分为两个部分,一个部分叫做头部链表用来存储热点数据,一个部分叫做尾部链表,用来存储即将淘汰的数据。头部链表和尾部链表有一个分界点,默认是3/8,就是有3/8的空间用来存储old页面。在innodb中有个参数是innodb_old_blocks_pct,默认是37大概就是3/8,通过配置这个参数可以选择头部链表和尾部链表占用的空间比例,innodb_old_blocks_pct的可配置的范围是从5-95,值越大说明尾部链表占用的空间越大,也就越接近LRU算法。当mysql从磁盘往缓存区存数据的时候,都会先把数据存储在尾部链表,这样一来,即使有全表扫描,那么全表扫描的数据也只能进入尾部链表中,不会影响头部链表的数据。
在innodb中还有一个参数也是用来防止缓存污染的,就是innodb_old_blocks_time。这个参数的默认值是1000ms,意思是,在把数据读入尾部链表的1000ms之内,再次访问相同的数据,这个数据页不会进入到头部链表。这个值越大,那么数据进入头部链表的机会就越少,那么数据被淘汰的概率就越大。

预读操作

预读是mysql提高性能的一个重要的特性。预读是指,在获取一个页面的数据时,在不久的时间里面也会用到存储数据页面的后面的页面(page)或者块(extend)。在mysql中预读有两种。

Linear线性预读

线性预读的单位是extend,一个extend中有64个page。线性预读的一个重要参数是innodb_read_ahead_threshold,是指在连续访问多少个页面之后,把下一个extend读入到buffer pool中,不过预读是一个异步的操作。当然这个参数不能超过64,因为一个extend最多只有64个页面。
例如,innodb_read_ahead_threshold = 56,就是指在连续访问了一个extend的56个页面之后把下一个extend读入到buffer pool中。在添加此参数之前,InnoDB仅计算当它在当前范围的最后一页中读取时是否为整个下一个范围发出异步预取请求。

Random随机预读

随机预读方式则是表示当同一个extent中的一些page在buffer pool中发现时,Innodb会将该extent中的剩余page一并读到buffer pool中。由于随机预读方式给innodb code带来了一些不必要的复杂性,同时在性能也存在不稳定性,在5.5中已经将这种预读方式废弃,默认是OFF。若要启用此功能,即将配置变量设置innodb_random_read_ahead为ON。

数据页访问流程

当访问的页面在缓存池在命中的话,直接返回该页。为了避免扫描LRU,innodb为每个instances维护了一个page hash,通过space id和page no可以直接找到对应的page。一般情况下,当我饿你需要读入一个Page时,首先根据space id和page no找到对应的instances,然后再查询page hash,如果page hash中没有,则需要从磁盘中读取。
如果没有命中,则需要把页面从磁盘加载到缓存池中,因此需要在缓存池中找到一个空闲的内存块来缓存这个页面。
如果空闲内存被使用完,也就是free链表上没有内存块了。则需要在生产一个空闲的内存块。
首先去LRU列表中找可以替换的内存页面,查找的方向是从列表的尾部开始找,如果找到可以替换的页面,将其从LRU列表中摘除,加入空闲列表,然后再去空闲列表中找空闲的内存块。第一查找最多值扫描100个页面,循环进行到第二次时,会扫描整个LRU列表。
如果在LRU列表中没有找到可以替换的页,则进行单页刷新,将脏页刷新到磁盘之后,然后将释放的内存块加入到空闲列表。然后再去空闲列表中取。为什么只做单页刷新呢?因为这个函数的目的是获取空闲内存页,进行脏页刷新是不得已而为之,所以只会进行一个页面的刷新,目的是为了尽快的获取空闲内存块。
通过数据页访问机制,可以知道其中当无空闲页时产生空闲页就成为一个必须要做的事情了。如果需要刷新脏页来产生空闲页面或者需要扫描整个LRU列表来产生空闲页面的时候,查找空闲内存块的时间就会延长,这个是一个bad case,是我们希望尽量避免的。因此,innodb buffer pool中存在大量可以替换的页面,或者free列表中一直存在着空闲内存块,对快速获取到空闲内存块起决定性的作用。

缓存池刷新策略

InnoDB会在后台执行某些任务,包括从缓冲池刷新脏页(那些已更改但尚未写入数据库文件的页)。

当启用innodb_max_dirty_pages_pct_lwm(默认值0)参数时,表示启用了脏页面预刷新行为,以控制脏页面占比。也是为了防止脏页占有率超过innodb_max_dirty_pages_pct(默认值75%)的设定值。默认禁用“预刷新”行为。如果当脏页的占有率达到了innodb_max_dirty_pages_pct的设定值的时候,InnoDB就会强制刷新buffer pool pages。另外当free列表小于innodb_lru_scan_depth值时也会触发刷新机制,innodb_lru_scan_depth控制LRU列表中可用页的数量,该值默认为1024。

后台刷新的动作由后台刷新协调线程触发,该线程的所有工作内容均由buf_flush_page_cleaner_coordinator函数完成,我们后面简称它为协调函数。接下来,来看后台刷新协调函数的主体流程。

调用page_cleaner_flush_pages_recommendation建议函数,对每个缓冲池实例生成脏页刷新数量的建议。在执行刷新之前,会用建议函数生成每个buffer pool需要刷新多少个脏页的建议。

生成刷新建议之后,通过设置事件的方式,向刷新线程(Page Cleaner线程)发出刷新请求。后台刷新线程在收到请求刷新的事件后,会执行pc_flush_slot函数对某个缓存池进行刷新,刷新的过程首先是对lru列表进行刷新,执行的函数为buf_flush_LRU_list,完成LRU列表的刷新之后,就会根据建议函数生成的建议对脏页列表进行刷新,执行的函数为buf_flush_do_batch。

后台刷新的协调线程会作为刷新调度总负责人的角色,它会确保每个buffer pool都已经开始执行刷新。如果哪个buffer pool的刷新请求还没有被处理,则由刷新协调线程亲自刷新,且直到所有的buffer pool instance都已开始/进行了刷新,才退出这个while循环。

当所有的buffer pool instance的刷新请求都已经开始处理之后,协调函数(或协调线程)就等待所有buffer pool instance的刷新的完成,等待函数为pc_wait_finished。如果这次刷新的总耗时超过4000ms,下次循环之前,会在数据库的错误日志记录相关的超时信息。它期望每秒钟对buffer pool进行一次刷新调度。如果相邻两次刷新调度的间隔超过4000ms ,也就是4秒钟,MySQL的错误日志中会记录相关信息,意思就是“本来预计1000ms的循环花费了超过4000ms的时间。

前面我们反复讲到,每个buffer pool需要刷新多少页面是由建议函数生成的,它在做刷新建议的时候,具体考虑了哪些因素?现在我们来详细解析。

在讲这段内容之前,我们先来了解两个参数:innodb_io_capacity与innodb_io_capacity_max,这两个参数大部分朋友都不陌生,设置这个参数的目的,是告诉MySQL数据库,它所在服务器的磁盘的随机IO能力。MySQL数据库目前还没有去自己评估服务器磁盘IO能力的功能,所以磁盘io能力大小由这个参数提供,以便让数据库知道磁盘的实际IO能力。这个参数将直接影响建议刷新的页面的数量。

建议函数它会计算当前的脏页刷新平均速度(也就是一秒钟刷新了多少脏页)以及重做日志的生成平均速度。但这个函数并不是每次被调用时,都计算一次平均速度。它是多久计算一次的呢?这个是由数据库参数innodb_flushing_avg_loops来决定的,默认是30,当这个函数被调用了30次之后或者经过30秒之后,重新计算一次平均值。我们暂且简单理解为30秒钟。计算规则是当前的平均速度加上最近30秒钟期间的平均速度再除以2得出新的平均速度。两个平均值相加再平均,得出新的平均值。这样的平均值能明显的体现出最近30秒的速度的变化。

十六、MVCC

简介

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。在小编看来,他无非就是乐观锁的一种实现方式。在Java编程中,如果把乐观锁看成一个接口,MVCC便是这个接口的一个实现类而已。

特点

1.MVCC其实广泛应用于数据库技术,像Oracle,PostgreSQL等也引入了该技术,即适用范围广

2.MVCC并没有简单的使用数据库的行锁,而是使用了行级锁,row_level_lock,而非InnoDB中的innodb_row_lock.

基本原理

MVCC的实现,通过保存数据在某个时间点的快照来实现的。这意味着一个事务无论运行多长时间,在同一个事务里能够看到数据一致的视图。根据事务开始的时间不同,同时也意味着在同一个时刻不同事务看到的相同表里的数据可能是不同的。

在并发读写数据库时,读操作可能会不一致的数据(脏读)。为了避免这种情况,需要实现数据库的并发访问控制,最简单的方式就是加锁访问。由于,加锁会将读写操作串行化,所以不会出现不一致的状态。但是,读操作会被写操作阻塞,大幅降低读性能。在java concurrent包中,有copyonwrite系列的类,专门用于优化读远大于写的情况。而其优化的手段就是,在进行写操作时,将数据copy一份,不会影响原有数据,然后进行修改,修改完成后原子替换掉旧的数据,而读操作只会读取原有数据。通过这种方式实现写操作不会阻塞读操作,从而优化读效率。而写操作之间是要互斥的,并且每次写操作都会有一次copy,所以只适合读大于写的情况。

MVCC的原理与copyonwrite类似,全称是Multi-Version Concurrent Control,即多版本并发控制。在MVCC协议下,每个读操作会看到一个一致性的snapshot,并且可以实现非阻塞的读。MVCC允许数据具有多个版本,这个版本可以是时间戳或者是全局递增的事务ID,在同一个时间点,不同的事务看到的数据是不同的。

MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。所以我们先来看看这个三个point的概念

和乐观锁悲观锁的区别:

维基百科: 多版本并发控制(Multiversion concurrency control, MCC 或 MVCC),是数据库管理系统常用的一种并发控制,也用于程序设计语言实现事务内存。

乐观并发控制和悲观并发控制都是通过延迟或者终止相应的事务来解决事务之间的竞争条件来保证事务的可串行化;虽然前面的两种并发控制机制确实能够从根本上解决并发事务的可串行化的问题,但是其实都是在解决写冲突的问题,两者区别在于对写冲突的乐观程度不同(悲观锁也能解决读写冲突问题,但是性能就一般了)。而在实际使用过程中,数据库读请求是写请求的很多倍,我们如果能解决读写并发的问题的话,就能更大地提高数据库的读性能,而这就是多版本并发控制所能做到的事情。

与悲观并发控制和乐观并发控制不同的是,MVCC是为了解决读写锁造成的多个、长时间的读操作饿死写操作问题,也就是解决读写冲突的问题。MVCC 可以与前两者中的任意一种机制结合使用,以提高数据库的读性能。

数据库的悲观锁基于提升并发性能的考虑,一般都同时实现了多版本并发控制。不仅是MySQL,包括Oracle、PostgreSQL等其他数据库系统也都实现了MVCC,但各自的实现机制不尽相同,因为MVCC没有一个统一的实现标准。

总的来说,MVCC的出现就是数据库不满用悲观锁去解决读-写冲突问题,因性能不高而提出的解决方案。

适用场景

一、悲观锁

  1. 用来解决读-写冲突和写-写冲突的的加锁并发控制
  2. 适用于写多读少,写冲突严重的情况,因为悲观锁是在读取数据的时候就加锁的,读多的场景会需要频繁的加锁和很多的的等待时间,而在写冲突严重的情况下使用悲观锁可以保证数据的一致性
  3. 数据一致性要求高
  4. 可以解决脏读,幻读,不可重复读,第一类更新丢失,第二类更新丢失的问题

二、乐观锁

  1. 解决写-写冲突的无锁并发控制
  2. 适用于读多写少,因为如果出现大量的写操作,写冲突的可能性就会增大,业务层需要不断重试,这会大大降低系统性能
  3. 数据一致性要求不高,但要求非常高的响应速度
  4. 无法解决脏读,幻读,不可重复读,但是可以解决更新丢失问题

三、MVCC

  1. 解决读-写冲突的无锁并发控制
  2. 与上面两者结合,提升它们的读性能
  3. 可以解决脏读,幻读,不可重复读等事务问题,更新丢失问题除外

基本特征

  • 每行数据都存在一个版本,每次数据更新时都更新该版本。
  • 修改时Copy出当前版本随意修改,各个事务之间无干扰。
  • 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)

InnoDB存储引擎MVCC的实现策略

在每一行数据中额外保存两个隐藏的列:当前行创建时的版本号和删除时的版本号(可能为空,其实还有一列称为回滚指针,用于事务回滚,不在本文范畴)。这里的版本号并不是实际的时间值,而是系统版本号。每开始新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询每行记录的版本号进行比较。

每个事务又有自己的版本号,这样事务内执行CRUD操作时,就通过版本号的比较来达到数据版本控制的目的。

MVCC实现方式.

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存了这个行的创建时间,一个保存的是行的删除时间。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的ID),没开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的ID.下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的.

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。每个事务读到的数据项都是一个历史快照,被称为快照读,不同于当前读的是快照读读到的数据可能不是最新的,但是快照隔离能使得在整个事务看到的数据都是它启动时的数据状态。而写操作不覆盖已有数据项,而是创建一个新的版本,直至所在事务提交时才变为可见。

什么是当前读和快照读?

在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的当前读和快照读?

  • 当前读
    像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
  • 快照读
    像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

说白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现

当前读,快照读和MVCC的关系

  • 准确的说,MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。仅仅是一个理想概念
  • 而在MySQL中,实现这么一个MVCC理想概念,我们就需要MySQL提供具体的功能去实现它,而快照读就是MySQL为我们实现MVCC理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现
  • 要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC模型在MySQL中的具体实现则是由 3个隐式字段,undo日志 ,Read View 等去完成的,具体可以看下面的MVCC实现原理

MVCC下InnoDB的增删查改是怎么work的

1.插入数据(insert):记录的版本号即当前事务的版本号

执行一条数据语句:insert into testmvcc values(1,“test”);

假设事务id为1,那么插入后的数据行如下:

2、在更新操作的时候,采用的是先标记旧的那行记录为已删除,并且删除版本号是事务版本号,然后插入一行新的记录的方式。

比如,针对上面那行记录,事务Id为2 要把name字段更新

update table set name= ‘new_value’ where id=1;

3、删除操作的时候,就把事务版本号作为删除版本号。比如

delete from table where id=1;

4、查询操作:

从上面的描述可以看到,在查询时要符合以下两个条件的记录才能被事务查询出来:

  1. 删除版本号未指定或者大于当前事务版本号,即查询事务开启后确保读取的行未被删除。(即上述事务id为2的事务查询时,依然能读取到事务id为3所删除的数据行)

  2. 创建版本号 小于或者等于 当前事务版本号 ,就是说记录创建是在当前事务中(等于的情况)或者在当前事务启动之前的其他事物进行的insert。

(即事务id为2的事务只能读取到create version<=2的已提交的事务的数据集)

补充:

1.MVCC手段只适用于Msyql隔离级别中的读已提交(Read committed)和可重复读(Repeatable Read).

2.Read uncimmitted由于存在脏读,即能读到未提交事务的数据行,所以不适用MVCC.

原因是MVCC的创建版本和删除版本只要在事务提交后才会产生。

3.串行化由于是会对所涉及到的表加锁,并非行锁,自然也就不存在行的版本控制问题。

4.通过以上总结,可知,MVCC主要作用于事务性的,有行锁控制的数据库模型。

MVCC能解决什么问题

数据库并发场景有三种,分别为:
  • 读-读:不存在任何问题,也不需要并发控制
  • 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  • 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
MVCC带来的好处是?

多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
  • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

MVCC 使大多数读操作都可以不用加锁,这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。

小结一下

总之,MVCC就是因为大牛们,不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案,所以在数据库中,因为有了MVCC,所以我们可以形成两个组合:

  • MVCC + 悲观锁
    MVCC解决读写冲突,悲观锁解决写写冲突

  • MVCC + 乐观锁
    MVCC解决读写冲突,乐观锁解决写写冲突
    这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题

隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

  • DB_TRX_ID
    6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
  • DB_ROLL_PTR
    7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
  • DB_ROW_ID
    6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
  • 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了

如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本

undo日志

undo log主要分为两种:

  • insert undo log
    代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  • update undo log
    事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

purge

  • 从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
  • 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

对MVCC有帮助的实质是update undo log ,undo log实际上就是存在rollback segment中旧记录链,它的执行流程如下:

一、 比如一个有个事务插入persion表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL

二、 现在来了一个事务1对该记录的name做出了修改,改为Tom

  • 在事务1修改该行(记录)数据时,数据库会先对该行加排他锁
  • 然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本
  • 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它
  • 事务提交后,释放锁

三、 又来了个事务2修改person表的同一个记录,将age修改为30岁

  • 在事务2修改该行数据时,数据库也先为该行加锁
  • 然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面
  • 修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录
  • 事务提交,释放锁

从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录(当然就像之前说的该undo log的节点可能是会purge线程清除掉,向图中的第一条insert undo log,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里)

Read View(读视图)

什么是Read View?

什么是Read View,说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

所以我们知道 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本

那么这个判断条件是什么呢?

如上,它是一段MySQL判断可见性的一段源码,即changes_visible方法(不完全哈,但能看出大致逻辑),该方法展示了我们拿DB_TRX_ID去跟Read View某些属性进行怎么样的比较

在展示之前,我先简化一下Read View,我们可以把Read View简单的理解成有三个全局属性

trx_list(名字我随便取的)一个数值列表,用来维护Read View生成时刻系统正活跃的事务IDup_limit_id记录trx_list列表中事务ID最小的IDlow_limit_idReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1

首先比较DB_TRX_ID < up_limit_id, 如果小于,则当前事务能看到DB_TRX_ID 所在的记录,如果大于等于进入下一个判断

接下来判断 DB_TRX_ID 大于等于 low_limit_id , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断

判断DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在,则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在Read View生成之前就已经Commit了,你修改的结果,我当前事务是能看见的

整体流程

我们在了解了隐式字段,undo log, 以及Read View的概念之后,就可以来看看MVCC实现的整体流程是怎么样了

整体的流程是怎么样的呢?我们可以模拟一下

  • 当事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View读视图,假设当前事务ID为2,此时还有事务1和事务3在活跃中,事务4在事务2快照读前一刻提交更新了,所以Read View记录了系统当前活跃事务1,3的ID,维护在一个列表上,假设我们称为trx_list

Read View不仅仅会通过一个列表trx_list来维护事务2执行快照读那刻系统正活跃的事务ID,还会有两个属性up_limit_id(记录trx_list列表中事务ID最小的ID),low_limit_id(记录trx_list列表中事务ID最大的ID,也有人说快照读那刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的最大值+1,我更倾向于后者;所以在这里例子中up_limit_id就是1,low_limit_id就是4 + 1 = 5,trx_list集合的值是1,3,Read View如下图

我们的例子中,只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务,所以当前该行当前数据的undo log如下图所示;我们的事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID去跟up_limit_id,low_limit_id和活跃事务ID列表(trx_list)进行比较,判断当前事务2能看到该记录的版本是哪个。

所以先拿该记录DB_TRX_ID字段记录的事务ID 4去跟Read View的的up_limit_id比较,看4是否小于up_limit_id(1),所以不符合条件,继续判断 4 是否大于等于 low_limit_id(5),也不符合条件,最后判断4是否处于trx_list中的活跃事务, 最后发现事务ID为4的事务不在当前活跃事务列表中, 符合可见性条件,所以事务4修改后提交的最新结果对事务2快照读时是可见的,所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本

  • 也正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同

MVCC相关问题

RR是如何在RC级的基础上解决不可重复读的?

当前读和快照读在RR级别下的区别:
表1:

表2:

而在表2这里的顺序中,事务B在事务A提交后的快照读和当前读都是实时的新数据400,这是为什么呢?

  • 这里与上表的唯一区别仅仅是表1的事务B在事务A修改金额前快照读过一次金额数据,而表2的事务B在事务A修改金额前没有进行过快照读。

所以我们知道事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读的地方非常关键,它有决定该事务后续快照读结果的能力

我们这里测试的是更新,同时删除和更新也是一样的,如果事务B的快照读是在事务A操作之后进行的,事务B的快照读也是能读取到最新的数据的

RC,RR级别下的InnoDB快照读有什么不同?

正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同

  • 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
  • 即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
  • 而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因

总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。

MVCC举例:

create table yang(
id int primary key auto_increment,
name varchar(20));

假设系统的版本号从1开始.

INSERT

InnoDB为新插入的每一行保存当前系统版本号作为版本号.
第一个事务ID为1;

start transaction;
insert into yang values(NULL,'yang') ;
insert into yang values(NULL,'long');
insert into yang values(NULL,'fei');
commit;

对应在数据中的表如下(后面两列是隐藏列,我们通过查询语句并看不到)

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined

SELECT

InnoDB会根据以下两个条件检查每行记录:
a.InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的.
b.行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除.
只有a,b同时满足的记录,才能返回作为查询结果.

DELETE

InnoDB会为删除的每一行保存当前系统的版本号(事务的ID)作为删除标识.
看下面的具体例子分析:
第二个事务,ID为2;

start transaction;
select * from yang;  //(1)
select * from yang;  //(2)
commit;

假设1

假设在执行这个事务ID为2的过程中,刚执行到(1),这时,有另一个事务ID为3往这个表里插入了一条数据;
第三个事务ID为3;

start transaction;
insert into yang values(NULL,'tian');
commit;

这时表中的数据如下:

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined
4 tian 3 undefined

然后接着执行事务2中的(2),由于id=4的数据的创建时间(事务ID为3),执行当前事务的ID为2,而InnoDB只会查找事务ID小于等于当前事务ID的数据行,所以id=4的数据行并不会在执行事务2中的(2)被检索出来,在事务2中的两条select 语句检索出来的数据都只会下表:

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined

假设2

假设在执行这个事务ID为2的过程中,刚执行到(1),假设事务执行完事务3后,接着又执行了事务4;
第四个事务:

start   transaction;
delete from yang where id=1;
commit;

此时数据库中的表如下:

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 4
2 long 1 undefined
3 fei 1 undefined
4 tian 3 undefined

接着执行事务ID为2的事务(2),根据SELECT 检索条件可以知道,它会检索创建时间(创建事务的ID)小于当前事务ID的行和删除时间(删除事务的ID)大于当前事务的行,而id=4的行上面已经说过,而id=1的行由于删除时间(删除事务的ID)大于当前事务的ID,所以事务2的(2)select * from yang也会把id=1的数据检索出来.所以,事务2中的两条select 语句检索出来的数据都如下:

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 4
2 long 1 undefined
3 fei 1 undefined

UPDATE

InnoDB执行UPDATE,实际上是新插入了一行记录,并保存其创建时间为当前事务的ID,同时保存当前事务ID到要UPDATE的行的删除时间.

假设3

假设在执行完事务2的(1)后又执行,其它用户执行了事务3,4,这时,又有一个用户对这张表执行了UPDATE操作:
第5个事务:

start  transaction;
update yang set name='Long' where id=2;
commit;

根据update的更新原则:会生成新的一行,并在原来要修改的列的删除时间列上添加本事务ID,得到表如下:

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 4
2 long 1 5
3 fei 1 undefined
4 tian 3 undefined
2 Long 5 undefined

继续执行事务2的(2),根据select 语句的检索条件,得到下表:

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 4
2 long 1 5
3 fei 1 undefined

还是和事务2中(1)select 得到相同的结果.

###2:

MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能。

InnoDB在每行数据都增加两个隐藏字段,一个记录创建的版本号,一个记录删除的版本号。

* SELECT:
当隔离级别是REPEATABLE READ时select操作,InnoDB必须每行数据来保证它符合两个条件:
1、InnoDB必须找到一个行的版本,它至少要和事务的版本一样老(也即它的版本号不大于事务的版本号)。这保证了不管是事务开始之前,或者事务创建时,或者修改了这行数据的时候,这行数据是存在的。
2、这行数据的删除版本必须是未定义的或者比事务版本要大。这可以保证在事务开始之前这行数据没有被删除。
符合这两个条件的行可能会被当作查询结果而返回。
* INSERT:
InnoDB为这个新行记录当前的系统版本号。
* DELETE:
InnoDB将当前的系统版本号设置为这一行的删除ID。
* UPDATE:
InnoDB会写一个这行数据的新拷贝,这个拷贝的版本为当前的系统版本号。它同时也会将这个版本号写到旧行的删除版本里。
这种额外的记录所带来的结果就是对于大多数查询来说根本就不需要获得一个锁。他们只是简单地以最快的速度来读取数据,确保只选择符合条件的行。这个方案的缺点在于存储引擎必须为每一行存储更多的数据,
做更多的检查工作,处理更多的善后操作。
MVCC只工作在REPEATABLE READ和READ COMMITED隔离级别下。READ UNCOMMITED不是MVCC兼容的,因为查询不能找到适合他们事务版本的行版本;它们每次都只能读到最新的版本。
SERIABLABLE也不与MVCC兼容,因为读操作会锁定他们返回的每一行数据。

MVCC总结:

切记:第一个SELECT执行的时候,当前事务取到了系统版本号n(并不是begin的时候就生成版本号,而是执行事务内第一个语句时生成),系统版本号自增为n+1。此后,其他事务的更新操作能取到的系统版本号最小为n+1,所以当前事务再次SELECT将看不见它们的更新

客观上,我们认为他就是乐观锁的一整实现方式,就是每行都有版本号,保存时根据版本号决定是否成功。

但由于Mysql的写操作会加排他锁(前文有讲),如果锁定了还算不算是MVCC?

了解乐观锁的小伙伴们,都知道其主要依靠版本控制,即消除锁定,二者相互矛盾,so从某种意义上来说,Mysql的MVCC并非真正的MVCC,他只是借用MVCC的名号实现了读的非阻塞而已。

十七、B+树

索引在 MySQL 数据库中分三类:

  • B+ 树索引
  • Hash 索引
  • 全文索引

我们今天要介绍的是工作开发中最常接触到的 InnoDB 存储引擎中的 B+ 树索引。要介绍 B+ 树索引,就不得不提二叉查找树,平衡二叉树和 B 树这三种数据结构。B+ 树就是从他们仨演化来的。

二叉查找树

首先,让我们先看一张图:

从图中可以看到,我们为 user 表(用户信息表)建立了一个二叉查找树的索引。

图中的圆为二叉查找树的节点,节点中存储了键(key)和数据(data)。键对应 user 表中的 id,数据对应 user 表中的行数据。

二叉查找树的特点就是任何节点的左子节点的键值都小于当前节点的键值,右子节点的键值都大于当前节点的键值。顶端的节点我们称为根节点,没有子节点的节点我们称之为叶节点。

如果我们需要查找 id=12 的用户信息,利用我们创建的二叉查找树索引,查找流程如下:

  • 将根节点作为当前节点,把 12 与当前节点的键值 10 比较,12 大于 10,接下来我们把当前节点>的右子节点作为当前节点。
  • 继续把 12 和当前节点的键值 13 比较,发现 12 小于 13,把当前节点的左子节点作为当前节点。
  • 把 12 和当前节点的键值 12 对比,12 等于 12,满足条件,我们从当前节点中取出 data,即 id=12,name=xm。

利用二叉查找树我们只需要 3 次即可找到匹配的数据。如果在表中一条条的查找的话,我们需要 6 次才能找到。

平衡二叉树

上面我们讲解了利用二叉查找树可以快速的找到数据。但是,如果上面的二叉查找树是这样的构造:

这个时候可以看到我们的二叉查找树变成了一个链表。如果我们需要查找 id=17 的用户信息,我们需要查找 7 次,也就相当于全表扫描了。

导致这个现象的原因其实是二叉查找树变得不平衡了,也就是高度太高了,从而导致查找效率的不稳定。

为了解决这个问题,我们需要保证二叉查找树一直保持平衡,就需要用到平衡二叉树了。

平衡二叉树又称 AVL 树,在满足二叉查找树特性的基础上,要求每个节点的左右子树的高度差不能超过 1。

下面是平衡二叉树和非平衡二叉树的对比:

由平衡二叉树的构造我们可以发现第一张图中的二叉树其实就是一棵平衡二叉树。

平衡二叉树保证了树的构造是平衡的,当我们插入或删除数据导致不满足平衡二叉树不平衡时,平衡二叉树会进行调整树上的节点来保持平衡。具体的调整方式这里就不介绍了。

平衡二叉树相比于二叉查找树来说,查找效率更稳定,总体的查找速度也更快。

B 树

因为内存的易失性。一般情况下,我们都会选择将 user 表中的数据和索引存储在磁盘这种外围设备中。

但是和内存相比,从磁盘中读取数据的速度会慢上百倍千倍甚至万倍,所以,我们应当尽量减少从磁盘中读取数据的次数。

另外,从磁盘中读取数据时,都是按照磁盘块来读取的,并不是一条一条的读。

如果我们能把尽量多的数据放进磁盘块中,那一次磁盘读取操作就会读取更多数据,那我们查找数据的时间也会大幅度降低。

如果我们用树这种数据结构作为索引的数据结构,那我们每查找一次数据就需要从磁盘中读取一个节点,也就是我们说的一个磁盘块。

我们都知道平衡二叉树可是每个节点只存储一个键值和数据的。那说明什么?说明每个磁盘块仅仅存储一个键值和数据!那如果我们要存储海量的数据呢?

可以想象到二叉树的节点将会非常多,高度也会极其高,我们查找数据时也会进行很多次磁盘 IO,我们查找数据的效率将会极低!

为了解决平衡二叉树的这个弊端,我们应该寻找一种单个节点可以存储多个键值和数据的平衡树。也就是我们接下来要说的 B 树。

B 树(Balance Tree)即为平衡树的意思,下图即是一棵 B 树:

图中的 p 节点为指向子节点的指针,二叉查找树和平衡二叉树其实也有,因为图的美观性,被省略了。

图中的每个节点称为页,页就是我们上面说的磁盘块,在 MySQL 中数据读取的基本单位都是页,所以我们这里叫做页更符合 MySQL 中索引的底层数据结构。

从上图可以看出,B 树相对于平衡二叉树,每个节点存储了更多的键值(key)和数据(data),并且每个节点拥有更多的子节点,子节点的个数一般称为阶,上述图中的 B 树为 3 阶 B 树,高度也会很低。

基于这个特性,B 树查找数据读取磁盘的次数将会很少,数据的查找效率也会比平衡二叉树高很多。

假如我们要查找 id=28 的用户信息,那么我们在上图 B 树中查找的流程如下:

  • 先找到根节点也就是页 1,判断 28 在键值 17 和 35 之间,那么我们根据页 1 中的指针 p2 找到页 3。
  • 将 28 和页 3 中的键值相比较,28 在 26 和 30 之间,我们根据页 3 中的指针 p2 找到页 8。
  • 将 28 和页 8 中的键值相比较,发现有匹配的键值 28,键值 28 对应的用户信息为(28,bv)。

B+ 树

B+ 树是对 B 树的进一步优化。让我们先来看下 B+ 树的结构图:

根据上图我们来看下 B+ 树和 B 树有什么不同:

①B+ 树非叶子节点上是不存储数据的,仅存储键值,而 B 树节点中不仅存储键值,也会存储数据。

之所以这么做是因为在数据库中页的大小是固定的,InnoDB 中页的默认大小是 16KB。

如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数又会再次减少,数据查询的效率也会更快。

另外,B+ 树的阶数是等于键值的数量的,如果我们的 B+ 树一个节点可以存储 1000 个键值,那么 3 层 B+ 树可以存储 1000×1000×1000=10 亿个数据。

一般根节点是常驻内存的,所以一般我们查找 10 亿数据,只需要 2 次磁盘 IO。

②因为 B+ 树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。

那么 B+ 树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。而 B 树因为数据分散在各个节点,要实现这一点是很不容易的。

有心的读者可能还发现上图 B+ 树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。

其实上面的 B 树我们也可以对各个节点加上链表。这些不是它们之前的区别,是因为在 MySQL 的 InnoDB 存储引擎中,索引就是这样存储的。

也就是说上图中的 B+ 树索引就是 InnoDB 中 B+ 树索引真正的实现方式,准确的说应该是聚集索引(聚集索引和非聚集索引下面会讲到)。

通过上图可以看到,在 InnoDB 中,我们通过数据页之间通过双向链表连接以及叶子节点中数据之间通过单向链表连接的方式可以找到表中所有的数据。

MyISAM 中的 B+ 树索引实现与 InnoDB 中的略有不同。在 MyISAM 中,B+ 树索引的叶子节点并不存储数据,而是存储数据的文件地址。

B+树操作:

B+树和二叉树、平衡二叉树一样,都是经典的数据结构。B+树由B树和索引顺序访问方法(ISAM,是不是很熟悉?对,这也是MyISAM引擎最初参考的数据结构)演化而来,但是在实际使用过程中几乎已经没有使用B树的情况了。

B+树的定义十分复杂,因此只简要地介绍B+树:B+树是为磁盘或其他直接存取辅助设备而设计的一种平衡查找树,在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶节点中,各叶节点指针进行连接。

我们先来看一个B+树,其高度为2,每页可存放4条记录,扇出(fan out)为5。

可以看出,所有记录都在叶节点中,并且是顺序存放的,如果我们从最左边的叶节点开始顺序遍历,可以得到所有键值的顺序排序:5、10、15、20、25、30、50、55、60、65、75、80、85、90。

B+树的插入操作

B+树的插入必须保证插入后叶节点中的记录依然排序,同时需要考虑插入B+树的三种情况,每种情况都可能会导致不同的插入算法,如表5-1所示。

我们用实例来分析B+树的插入,我们插入28这个键值,发现当前Leaf Page和Index Page都没有满,我们直接插入就可以了。

这次我们再插入一条70这个键值,这时原先的Leaf Page已经满了,但是Index Page还没有满,符合表5-1的第二种情况,这时插入Leaf Page后的情况为50、55、60、65、70。我们根据中间的值60拆分叶节点。

因为图片显示的关系,这次我没有能在各叶节点加上双向链表指针。最后我们来插入记录95,这时符合表5-1讨论的第三种情况,即Leaf Page和Index Page都满了,这时需要做两次拆分。

可以看到,不管怎么变化,B+树总是会保持平衡。但是为了保持平衡,对于新插入的键值可能需要做大量的拆分页(split)操作,而B+树主要用于磁盘,因此页的拆分意味着磁盘的操作,应该在可能的情况下尽量减少页的拆分。因此,B+树提供了旋转(rotation)的功能。

旋转发生在Leaf Page已经满了、但是其左右兄弟节点没有满的情况下。这时B+树并不会急于去做拆分页的操作,而是将记录移到所在页的兄弟节点上。通常情况下,左兄弟被首先检查用来做旋转操作,这时我们插入键值70,其实B+树并不会急于去拆分叶节点,而是做旋转,50,55,55旋转。

可以看到,采用旋转操作使B+树减少了一次页的拆分操作,而这时B+树的高度依然还是2。

B+树的删除操作

B+树使用填充因子(fill factor)来控制树的删除变化,50%是填充因子可设的最小值。B+树的删除操作同样必须保证删除后叶节点中的记录依然排序,同插入一样,B+树的删除操作同样需要考虑如表5-2所示的三种情况,与插入不同的是,删除根据填充因子的变化来衡量。

首先,删除键值为70的这条记录,该记录符合表5-2讨论的第一种情况,删除后。

接着我们删除键值为25的记录,这也是表5-2讨论的第一种情况,但是该值还是Index Page中的值,因此在删除Leaf Page中25的值后,还应将25的右兄弟节点的28更新到Page Index中,最后可得到图。

最后我们来看删除键值为60的情况,删除Leaf Page中键值为60的记录后,填充因子小于50%,这时需要做合并操作,同样,在删除Index Page中相关记录后需要做Index Page的合并操作,最后得到图。

聚集索引 VS 非聚集索引

在上节介绍 B+ 树索引的时候,我们提到了图中的索引其实是聚集索引的实现方式。

那什么是聚集索引呢?在 MySQL 中,B+ 树索引按照存储方式的不同分为聚集索引和非聚集索引。

这里我们着重介绍 InnoDB 中的聚集索引和非聚集索引:

①聚集索引(聚簇索引):以 InnoDB 作为存储引擎的表,表中的数据都会有一个主键,即使你不创建主键,系统也会帮你创建一个隐式的主键。

这是因为 InnoDB 是把数据存放在 B+ 树中的,而 B+ 树的键值就是主键,在 B+ 树的叶子节点中,存储了表中所有的数据。

这种以主键作为 B+ 树索引的键值而构建的 B+ 树索引,我们称之为聚集索引。

②非聚集索引(非聚簇索引):以主键以外的列值作为键值构建的 B+ 树索引,我们称之为非聚集索引。

非聚集索引与聚集索引的区别在于非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键,想要查找数据我们还需要根据主键再去聚集索引中进行查找,这个再根据聚集索引查找数据的过程,我们称为回表。

明白了聚集索引和非聚集索引的定义,我们应该明白这样一句话:数据即索引,索引即数据。

聚集索引和非聚集索引查找数据

前面我们讲解 B+ 树索引的时候并没有去说怎么在 B+ 树中进行数据的查找,主要就是因为还没有引出聚集索引和非聚集索引的概念。

下面我们通过讲解如何通过聚集索引以及非聚集索引查找数据表中数据的方式介绍一下 B+ 树索引查找数据方法。

聚集索引查找数据

还是这张 B+ 树索引图,现在我们应该知道这就是聚集索引,表中的数据存储在其中。

现在假设我们要查找 id>=18 并且 id<40 的用户数据。对应的 sql 语句为:

select * from user where id>=18 and id <40

其中 id 为主键,具体的查找过程如下:

①一般根节点都是常驻内存的,也就是说页 1 已经在内存中了,此时不需要到磁盘中读取数据,直接从内存中读取即可。

从内存中读取到页 1,要查找这个 id>=18 and id <40 或者范围值,我们首先需要找到 id=18 的键值。

从页 1 中我们可以找到键值 18,此时我们需要根据指针 p2,定位到页 3。

②要从页 3 中查找数据,我们就需要拿着 p2 指针去磁盘中进行读取页 3。

从磁盘中读取页 3 后将页 3 放入内存中,然后进行查找,我们可以找到键值 18,然后再拿到页 3 中的指针 p1,定位到页 8。

③同样的页 8 页不在内存中,我们需要再去磁盘中将页 8 读取到内存中。

将页 8 读取到内存中后。因为页中的数据是链表进行连接的,而且键值是按照顺序存放的,此时可以根据二分查找法定位到键值 18。

此时因为已经到数据页了,此时我们已经找到一条满足条件的数据了,就是键值 18 对应的数据。

因为是范围查找,而且此时所有的数据又都存在叶子节点,并且是有序排列的,那么我们就可以对页 8 中的键值依次进行遍历查找并匹配满足条件的数据。

我们可以一直找到键值为 22 的数据,然后页 8 中就没有数据了,此时我们需要拿着页 8 中的 p 指针去读取页 9 中的数据。

④因为页 9 不在内存中,就又会加载页 9 到内存中,并通过和页 8 中一样的方式进行数据的查找,直到将页 12 加载到内存中,发现 41 大于 40,此时不满足条件。那么查找到此终止。

最终我们找到满足条件的所有数据,总共 12 条记录:

(18,kl), (19,kl), (22,hj), (24,io), (25,vg) , (29,jk), (31,jk) , (33,rt) , (34,ty) , (35,yu) , (37,rt) , (39,rt) 。

下面看下具体的查找流程图

利用非聚集索引查找数据

读者看到这张图的时候可能会蒙,这是啥东西啊?怎么都是数字。如果有这种感觉,请仔细看下图中红字的解释。

什么?还看不懂?那我再来解释下吧。首先,这个非聚集索引表示的是用户幸运数字的索引(为什么是幸运数字?一时兴起想起来的:-)),此时表结构是这样的。

在叶子节点中,不再存储所有的数据了,存储的是键值和主键。对于叶子节点中的 x-y,比如 1-1。左边的 1 表示的是索引的键值,右边的 1 表示的是主键值。

如果我们要找到幸运数字为 33 的用户信息,对应的 sql 语句为:

select * from user where luckNum=33

查找的流程跟聚集索引一样,这里就不详细介绍了。我们最终会找到主键值 47,找到主键后我们需要再到聚集索引中查找具体对应的数据信息,此时又回到了聚集索引的查找流程。

下面看下具体的查找流程图:

在 MyISAM 中,聚集索引和非聚集索引的叶子节点都会存储数据的文件地址。

B+树特征:

一个m阶的B树具有如下几个特征:

1.根结点至少有两个子女。

2.每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m

3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m

4.所有的叶子结点都位于同一层。

5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

一个m阶的B+树具有如下几个特征:

1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。

2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

节点之间含有重复元素,而且叶子节点用指针连在一起。

首先,每一个父节点的元素都出现在子节点的中,是子节点的最大(或最小)元素。

B-树中的卫星数据(Satellite Information):

B+树中的卫星数据(Satellite Information):

需要补充的是,在数据库的聚集索引(Clustered Index)中,叶子节点直接包含卫星数据。在非聚集索引(NonClustered Index)中,叶子节点带有指向卫星数据的指针。

第一次磁盘IO:

第二次磁盘IO:

第三次磁盘IO:

B-树的范围查找过程

自顶向下,查找到范围的下限(3):

中序遍历到元素6:

中序遍历到元素8:

中序遍历到元素9:

中序遍历到元素11,遍历结束:

B+树的范围查找过程

自顶向下,查找到范围的下限(3):

通过链表指针,遍历到元素6, 8:

通过链表指针,遍历到元素9, 11,遍历结束:

B+树的特征:

1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。

2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

B+树的优势:

1.单一节点存储更多的元素,使得查询的IO次数更少。

2.所有查询都要查找到叶子节点,查询性能稳定。

3.所有叶子节点形成有序链表,便于范围查询。

B+树总结:

本篇文章从二叉查找树,详细说明了为什么 MySQL 用 B+ 树作为数据的索引,以及在 InnoDB 中数据库如何通过 B+ 树索引来存储数据以及查找数据。

我们一定要记住这句话:数据即索引,索引即数据。
最后给大家推荐一个网站,用于模拟数据结构,我觉得满分。

https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

MySQL全文总结:

呕心沥血大半年 整理出本一文,其中 有我自己的读书笔记、也有摘抄网上的博客,集百家之长。MySQL是我学习的第一站但绝不是最后一站。能看到这里的兄弟都很了不起,想必不是平凡之辈!希望我们共同进步 共同成长,搏一个无悔人生。

学习不是一朝一夕的事 学习是一辈子的事,什么都想学 什么都学一点,这种想法和做法是不对的。必须一定要制定计划 按周期按计划的学习,2021年,是我踏入程序员之路的浅浅几年,在公司日常摸鱼 养逼晒蛋,不知前路几何,也不知是哪个深夜 还是哪个昏沉的梦中,突然恍然浑身一激灵。突然觉悟,你才几何岁月就这般慵度懒散 这成何体统,你的未来一眼就能看到头,你70岁 80岁的时候依旧一成不变,还是这般混天撩日。想到此处 浑身浑身的不自在,父母之力 已无可指望,自己不努力 何来铮铮光辉岁月?何不痛定思痛,苦下心思干上他个十几二十年,拼一个辉煌的人生呢!这一路,行路难 多歧路才会今安在,这一路你需要披荆斩棘 熬过无数无人问津 无人知道的苦苦寂寞深夜,需要抓破头皮铁石心肠的让自己静下心来去学习,去进步,方能成就自己。所谓一朝红日出 依旧与天齐,他日若遂凌云志 敢笑黄巢不丈夫!

MySQL这一章就够了(二)相关推荐

  1. MySQL优化-一篇文章就够了(转发加收藏吧)

    关注我,一个仍存梦想的屌丝程序员,每天为你分享高质量编程博客. 回复 "代金券"  免费获取腾讯云和阿里云代金券 前言 说起MySQL的查询优化,相信大家收藏了一堆:不能使用SEL ...

  2. MySQL这一章就够了(一)

    前言:呕心沥血5个月淦出本文,整理所有MySQL知识.我愿称之为地表最强MySQL. MySql笔记 MySQL是关系型数据库,基于SQL查询的开源跨平台数据库管理系统.它最初是由瑞典MySQL AB ...

  3. mysql第三章关系模型_一个MySQL关系模型只有三个关系(二维表)组成。_学小易找答案...

    [判断题]DELETE语句功能是对表中所有记录或满足条件的记录进行批量删除. [填空题]The computer's entire ____ was on a single board. [单选题]下 ...

  4. mysql第五章项目二_Todo List:Node+Express 搭建服务端毗邻Mysql – 第五章(第1节)

    点击右上方红色按钮关注"web秀",让你真正秀起来 前言 万丈高楼平地起,我们的Todo List项目也是越来越结实了.Todo List的前面4章内容都是在为Client端开发, ...

  5. Kali Linux 网络扫描秘籍 第三章 端口扫描(二)

    第三章 端口扫描(二) 作者:Justin Hutchens 译者:飞龙 协议:CC BY-NC-SA 4.0 3.6 Scapy 隐秘扫描 执行 TCP 端口扫描的一种方式就是执行一部分.目标端口上 ...

  6. mysql第五章事务_mysql 第五章 备份恢复

    mysql 第五章 备份恢复 一.备份策略***** 1.每周一次全备,每天一次增量备 2.每天检查备份是否成功 3.每季度进行备份恢复演练 4.设置 -master-data=2 (记录备份的GTI ...

  7. Kali Linux 网络扫描秘籍 第四章 指纹识别(二)

    第四章 指纹识别(二) 作者:Justin Hutchens 译者:飞龙 协议:CC BY-NC-SA 4.0 4.6 Nmap 服务识别 虽然特征抓取是非常有利的信息来源,服务特征中的版本发现越来越 ...

  8. 全面认识MOS管,一篇文章就够了

    基础知识中 MOS 部分迟迟未整理,实际分享的电路中大部分常用电路都用到了MOS管, 今天势必要来一篇文章,彻底掌握mos管! ...更新:为什么常在MOS管GS并联电阻? ...更新:为什么要在MO ...

  9. 全面认识二极管,一篇文章就够了

    电子设计基础元器件 二极管,小小二极管,大大用途. ... 矜辰所致 目录 前言 一.二极管基础知识 1.1 什么是二极管 1.2 二极管的组成 1.3 二极管的原理 二.二极管特性 2.1 伏安特性 ...

最新文章

  1. ESXi6.5环境搭建(一:VMware Workstations 12 Pro 环境的安装及配置)
  2. Nat. Mach. Intel. | IBM RXN: 深度学习在化学反应分类上大放异彩
  3. iOS JPG图片文件压缩中的坑
  4. 开源一个上架App Store的相机App
  5. 函数式编程语言python-函数式编程
  6. Git/码云上多人协作,创建分支,上传修改
  7. python 并发编程目录
  8. android 简单获取实时天气数据_绘图本身很简单但是获取数据很难
  9. 禁止“挖矿”!谷歌杀了所有的 Chrome 扩展应用
  10. php上传文件+(中文转换为拼音的类)+ 输入字符提示信息+根据头部信息给出相对应得二维码扫描...
  11. Axure8.0如何汉化?
  12. 华硕触控板无法在Win11中使用的解决办法
  13. VHDL三段式状态机
  14. PHP表格数据Excel转图片输出通用方法,列可自动拓展
  15. 屏幕取词翻译软件:Lingoes 灵格斯词霸
  16. 自己的域名申请和google账户申请
  17. 杭州城市交通拥堵综合治理实践
  18. ZooKeeper官方Java例子解读
  19. Android studio虚拟器突然黑屏解决办法
  20. Flink 异常 - 9.The heartbeat of TaskManager with id container timed out 分析与 Heartbeat 简介

热门文章

  1. windows用sshfs远程挂载目录linux
  2. DELL升级bios方式
  3. jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry for key PRIMARY异常解决办法
  4. java10 var关键字浅析
  5. 数据可视化练习-用powerBI生成自动播放的动态排行榜
  6. 将位图转换为矢量图形:
  7. Keil C51中断编程中寄存器组的选用与保护
  8. java 实现跳表(skiplist)及论文解读
  9. Python-读取PDF文件显示在窗口上-Pdf阅读器
  10. Proxychains: HTTP 代理