下面我给出的是 MySQL 的基本架构示意图,从中你能够清楚地看到 SQL 语句在 MySQL 的各个功能模块中的执行过程。html
从图中能够看出,不一样的存储引擎公用一个server层。mysql
链接命令算法
mysql -h$ip -P$port -u$user -p
sql
链接保持时间shell
链接完成以后,若是没有后续的动做,这个链接就会处于空闲状态,能够在show processlist
命令中看到。数据库
客户端若是太长时间没有动静,链接器会主动将其断连。这个参数是wait_timeout控制的,默认时间是8小时。数组
断开以后再发送请求的话,就会收到一个错误提醒:Lost connection to MySQL server during query。须要客户端从新链接,而后再发送请求。缓存
长链接占用内存问题bash
创建链接以后,进行select语句查询,就会进入查询缓存阶段。每次的查询会把结果放入缓存,下次有相同的查询会直接在缓存中获取便可,无需进入存储引擎进行查询。架构
可是,查询缓存这个事情是弊大于利的,因此不会建议使用。mysql8.0以后直接将该模块删除了。
为啥有弊呢?由于每次存在update操做的状况下,就会把整个缓存都清除掉,致使缓存命中率特别低。
在8.0以前的版本,能够直接将query_cache_type设置为DEMAND,这样的话,查询语句默认不使用缓存。在须要使用查询缓存的地方能够显示调用。mysql> select SQL_CACHE * from T where ID=10;
分析器先作“词法分析”,根据语法规则,判断输入的sql语句是否知足mysql语法规则。
优化器是在表中存在多个索引的状况下,决定用哪一个索引;或者在一个语句有多表关联(join)的时候,决定各个表的链接顺序。
MySQL经过分析器知道了你要作什么,经过优化器知道了该怎么作,因而就进入了执行器阶段,开始执行语句。
与查询流程不同的是,更新流程还涉及两个重要的日志模块,它们正是咱们今天要讨论的主角:redo log(重作日志)和 binlog(归档日志)。
若是每一次的更新操做都须要写进磁盘,而后磁盘也要找到对应的那条记录,而后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MySQL 的设计者就用了相似酒店掌柜粉板的思路来提高更新效率。
而粉板和帐本配合的整个过程,其实就是 MySQL 里常常说到的 WAL 技术,WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写帐本。
具体来讲,当有一条记录须要更新的时候,InnoDB 引擎就会先把记录写到 redo log(按我理解这个其实仍是要写文件,只不过是顺序写,效率高)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操做记录更新到磁盘里面,而这个更新每每是在系统比较空闲的时候作。
InnoDB 的 redo log 是固定大小的,好比能够配置为一组 4 个文件,每一个文件的大小是 1GB,那么这块“粉板”总共就能够记录 4GB 的操做。从头开始写,写到末尾就又回到开头循环写,以下面这个图所示。
这种机制保证了即便数据库异常宕机,以前的数据也不会丢失,这个能力成为crash-safe。
redo log是innodb引擎特有的日志,而引擎层上面的server层(若是忘了mysql的架构,记得看上一篇的图示)也有本身的日志,成为binlog(归档日志)。
为啥会有两份日志嘛?
由于最开始 MySQL 里并无 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,可是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另外一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,因此 InnoDB 使用另一套日志系统——也就是 redo log 来实现 crash-safe 能力。
这里面涉及到了两阶段提交。 MySQL经过两阶段提交过程来完成事务的一致性的,也即redo log和binlog的一致性的,理论上是先写redo log,再写binlog,两个日志都提交成功(刷入磁盘),事务才算真正的完成。
若是你的 DBA 承诺说半个月内能够恢复,那么备份系统中必定会保存最近半个月的全部 binlog,同时系统会按期作整库备份。这里的“按期”取决于系统的重要性,能够是一天一备,也能够是一周一备。
当须要恢复到指定的某一秒时,好比某天下午两点发现中午十二点有一次误删表,须要找回数据,那你能够这么作:
事务就是要保证数据库一组操做,要么所有成功,要么所有失败。在mysql中,事务是引擎层支实现的,MyISAM不支持事务,InnoDB支持事务。这也是MyISAM被InnoDB取代的重要缘由之一。
在实现上,数据库会建立一个视图,访问的时候以视图的逻辑结果为准。
在“可重复读”级别下,这个视图是在事务启动的时候建立的,整个事务存在期间都用这个视图。
在“读提交”级别下,这个视图是在每一个SQL语句开始执行的时候建立的。
在“读未提交”级别下,直接返回记录上的最新值。
在“串行化”级别下,直接用加锁的方式来避免并行访问。
Oracle的默认级别是“读提交”。
Mysql的默认级别是“可重复读”。
查看当前数据库默认的事务隔离级别:
show variables like 'transaction_isolation';
复制代码
每条update语句执行的同时,都会同时记录一条回滚日志。记录上的最新值,经过回滚操做,均可以获得前一个状态的值。
假设一个值从1按顺序改为了2/3/4,在回滚日志里会有相似下面的记录。
当前该值为4,不一样时刻启动的事务会有不一样的read-view。同一条记录在系统中能够存在多个版本,就是数据库的多版本并发控制(MVCC)。对于read-view A,要获得1,就必须将当前值依次执行图中全部的回滚操做获得。
当没有事务再须要用到这些回滚日志的时候,相应的回滚日志会被删除。
能够在 information_schema 库的 innodb_trx 这个表中查询长事务,好比下面这个语句,用于查找持续时间超过 60s 的事务。
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
复制代码
从应用开发端和数据库端两个方面来看问题。
应用开发端
数据库端
监控information_schema.Innodb_trx表,设置长事务阈值,超过阈值就告警或kill。
mysql -N -uroot -p'密码' -e "select now(),(UNIX_TIMESTAMP(now()) - UNIX_TIMESTAMP(a.trx_started)) diff_sec,b.id,b.user,b.host,b.db,d.SQL_TEXT from information_schema.innodb_trx a inner join
information_schema.PROCESSLIST b
on a.TRX_MYSQL_THREAD_ID=b.id and b.command = 'Sleep'
inner join performance_schema.threads c ON b.id = c.PROCESSLIST_ID
inner join performance_schema.events_statements_current d ON d.THREAD_ID = c.THREAD_ID;" | while read A B C D E F G H
do
#echo $C
if [ "$C" -gt 5 ]
then
echo date "+%Y-%m-%d %H:%M:%S"
echo "processid[$D] $E@$F in db[$G] hold transaction time $C SQL:$H"
fi
done >> /tmp/longtransaction.txt
复制代码
使用pt-kill工具,kill长时间的异常事务。参考:www.cnblogs.com/bjx2020/p/9…
在业务开发测试阶段,输出全部的general_log,分析日志行为,提前发现。
mysql>set global general_log_file='/tmp/general.log'; #设置路径
mysql>set global general_log=on; # 开启general log模式
复制代码
索引就是为了提升数据查询的效率,就像书的目录同样。
在InnoDB中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。
在InnoDB中,索引使用B+树的索引模式。B+ 树可以很好地配合磁盘的读写特性,减小单次查询的磁盘访问次数。
左边是主键索引,右边是普通索引。主键索引的叶子节点存的是整行数据,普通索引的叶子节点存的是主键的值。
也就是说,基于非主键索引的查询须要多扫描一棵索引树。所以,咱们在应用中应该尽可能使用主键查询。
B+ 树为了维护索引有序性,在插入新值的时候须要作必要的维护。以上面这个图为例,若是插入新的行 ID 值为 700,则只须要在 R5 的记录后面插入一个新记录。若是新插入的 ID 值为 400,就相对麻烦了,须要逻辑上挪动后面的数据,空出位置。
而更糟的状况是,若是 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候须要申请一个新的数据页,而后挪动部分数据过去。这个过程称为页分裂。在这种状况下,性能天然会受影响。
因此使用自增主键的重要性就体如今这里了。每次插入一条新记录,都是追加操做,都不涉及到挪动其余记录,也不会触发叶子节点的分裂。
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
以下所示的表:
mysql> create table T (
ID int primary key,
k int NOT NULL DEFAULT 0,
s varchar(16) NOT NULL DEFAULT '',
index k(k))
engine=InnoDB;
insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');
复制代码
以下所示的查询,须要执行几回树的操做,会扫描多少行:
select * from T where k between 3 and 5
复制代码
执行流程以下:
在这个例子中,因为查询结果所须要的数据只在主键索引上有,因此不得不回表。那么,有没有可能通过索引优化,避免回表过程呢?
若是执行的语句是select ID from T where k between 3 and 5
,这时只须要查询ID的值,而ID的值自己已经在k索引树上,不须要回表。
也就是说,在这个查询里面,索引k已经覆盖了咱们的查询需求,咱们称为覆盖索引。
又有一个问题:获取字段和检索字段都不是主键,怎么使用覆盖索引。
答案:创建两个字段的联合索引便可。
这边有一个疑问,若是为每一种查询都设计一个索引,索引会不会太多了。这个时候能够借助最左前缀原则来复用索引。
当已经有(a,b)索引以后,就不须要单独的a索引了。
根据加锁范围,MySQL里面的锁大体能够分为全局锁、表级锁和行级锁。
顾名思义,全局锁就是对整个数据库加锁。
加锁方式
Flush tables with read lock
,使得整个数据库处于只读状态。任何更新类的语句所有阻塞。
使用场景
**作全库逻辑备份。**也就是把整库每一个表都select出来存成文本。
风险
具体案例
为何不使用set global readonly=true的方式?
MySQL中表级锁有两种:表锁和元数据锁MDL(meta data lock)
表锁
表锁的语法是 lock tables … read/write。
举个例子, 若是在某个线程 A 中执行 lock tables t1 read, t2 write; 这个语句,则其余线程写 t一、读写 t2 的语句都会被阻塞。同时,线程 A 在执行 unlock tables 以前,也只能执行读 t一、读写 t2 的操做。连写 t1 都不容许,天然也不能访问其余表。
InnoDB支持行锁,因此通常不会用到表锁。
MDL
InnoDB支持行锁,MyISAM不支持行锁,这个也是被InnoDB替代的缘由之一。
事务 B 的 update 语句会被阻塞,直到事务 A 执行 commit 以后,事务 B 才能继续执行。
在 InnoDB 事务中,行锁是在须要的时候才加上的,但并不是不须要了就马上释放, 而是要等到事务结束时才释放。
建议:
若是你的事务中须要锁多个行,要把最可能形成锁冲突、最可能影响并发度的锁尽可能日后放。
死锁:
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会致使这几个线程都进入无限等待的状态。
解决方案: 一、经过参数 innodb_lock_wait_timeout 根据实际业务场景来设置超时时间,InnoDB引擎默认值是50s。 二、发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其余事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑(默认是开启状态)。
如何解决热点行更新致使的性能问题? 一、若是你能确保这个业务必定不会出现死锁,能够临时把死锁检测关闭掉。通常不建议采用 二、控制并发度,对应相同行的更新,在进入引擎以前排队。这样在InnoDB内部就不会有大量的死锁检测工做了。 三、将热更新的行数据拆分红逻辑上的多行来减小锁冲突,可是业务复杂度可能会大大提升。
innodb支持RC和RR隔离级别实现是用的一致性视图(consistent read view)
事务在启动时会拍一个快照,这个快照是基于整个库的. 基于整个库的意思就是说一个事务内,整个库的修改对于该事务都是不可见的(对于快照读的状况) 若是在事务内select t表,另外的事务执行了DDL t表,根据发生时间,要嘛锁住要嘛报错(参考第六章)
事务是如何实现的MVCC呢?
因为当前读都是先读后写,只能读当前的值,因此为当前读.会更新事务内的up_limit_id为该事务的transaction id
概念
当须要更新一个数据页,若是数据页在内存中就直接更新,若是不在内存中,在不影响数据一致性的前提下,InnoDB会将这些更新操做缓存在change buffer中。下次查询须要访问这个数据页的时候,将数据页读入内存,而后执行change buffer中的与这个页有关的操做。
优点
将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操做之一。 change buffer 由于减小了随机磁盘访问,因此对更新性能的提高很明显。
merge
将change buffer中的操做应用到原数据页上,获得最新结果的过程,称为merge 访问这个数据页会触发merge,系统有后台线程按期merge,在数据库正常关闭的过程当中,也会执行merge
持久化
change buffer是能够持久化的数据。在内存中有拷贝,也会被写入到磁盘上,顺序写,没有性能问题。
大小配置
change buffer用的是buffer pool里的内存,change buffer的大小,能够经过参数innodb_change_buffer_max_size来动态设置。这个参数设置为50的时候,表示change buffer的大小最多只能占用buffer pool的50%。
使用场景
redo log是WAL机制的实现方式,同change buffer的目的相同吧,都是为了减小随机读写。
例子:经过一个场景来描述二者的区别
mysql> insert into t(id,k) values(id1,k1),(id2,k2);
其中k是索引。
当前的状态是k1所在的数据页在内存(InnoDB buffer pool)中;k2所在的数据页不在内存中。
insert过程
从这边能够看出,在change buffer的参与下,执行的insert语句成本很低。(固然update和delete也是同样的)
select过程mysql> select * from t where k in (k1,k2)
对于查询过程来讲:
对于更新过程来讲:
惟一索引的更新不能使用change buffer
选择索引是优化器的工做,而优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。
在数据库里,扫描行数是影响执行代价的因素之一,扫描行数越少,意味着访问磁盘数据的次数越少,消耗的cpu资源越少。
mysql选错索引的状况下,确定就是判断扫描行数的时候出现了问题。
mysql在真正开始执行语句以前,并不能精确的知道知足这个条件的记录有多少条,而只能根据统计信息来估算记录数。
这个统计信息就是索引的“区分度”。显然,一个索引上不一样的值越多,这个索引的区分度就越好。而一个索引上不一样的值的个数,咱们称之为“基数”(cardinality)。也就是说,这个基数越大,索引的区分度越好。
show index from t
就能够看到每一个索引的基数(cardinality)。
mysql使用的是采样统计的方法。
采样统计的时候,InnoDB 默认会选择 N 个数据页,统计这些页面上的不一样值,获得一个平均值,而后乘以这个索引的页面数,就获得了这个索引的基数。
analyze tabel t
analyze table t
从新统计。force index
强行选择一个正确的索引。字符串通常占用空间较大,**在对空间比较敏感的系统作操做的时候,而且不会存在范围查询的时候。**能够考虑如下方案