连接池获得外部请求
分配给工作线程
调用SQL接口
SQL解析器解析SQL语句
查询优化器生成最优执行计划
执行器根据执行计划调用存储引擎的接口
从磁盘中加载数据到缓冲池
数据区
数据页,默认16kb
数据行
内存中的一块空间,默认128M
先把数据从磁盘加载到缓冲池中
一个数据页对应一个缓存页
直接更新缓冲池的数据,变成脏页
后台IO线程刷入磁盘中
数据结构:描述数据块+缓存页
当Buffer Pool总内存比较大(大于1G),会自动划分出多个buffer pool实例,每个实例内有独立的free、frush、lru链表。而每个实例内又通过多个chunk块动态扩展内存,这多个chunk块共用三大链表。
innodb_buffer_pool_size:缓冲池总大小,一般设置为机器内存的50%~60%。
innodb_buffer_pool_instances:缓冲池实例数,高并发下很影响性能。
innodb_buffer_pool_chunk_size:chunk块大小,单位是M。
buffer pool总大小 = (chunk大小 × buffer pool数量)的2倍数。
磁盘是随机读取的,效率很低,IOPS,每秒io数。
回滚日志
更新前保存数据旧值
用于事务失败回滚
重做日志
记录更新的内容
事务提交时必须刷入磁盘中
因为更新的是缓冲池(内存)中的数据,脏页还没刷入磁盘,此时宕机了可以用redo.log进行重做
写日志是顺序写,速度和写内存相当
redo.log是InnoDB特有的日志
二进制日志或归档日志
MySQL 通用日志
主从复制时使用
事务提交时必须刷入磁盘
free链表记录空闲缓存页
flush链表记录脏页
LRU链表记录最近最少使用的缓存页,当没有空闲缓存页时,会先将这部分缓存页进行刷盘
磁盘随机读写(性能低,IOPS,响应延迟等参数,SSD固态硬盘好过机械硬盘)
磁盘顺序读写(性能高,redolog,binlog等日志,os cache机制)
脏写:回滚了别人更新的数据。
脏读:读到了别人未提交的数据。
不可重复读:事务执行期间,读到别人已提交的数据,多次读到的值不同。
幻读:事务执行期间进行范围查询,这时别人新增或删除了数据,每次查到的数据量都不一样。
前面三种问题主要针对同一条数据的修改,最后一种幻读问题针对的是表的新增或删除
原子性:参与事务的所有SQL是最小的分割单元,要么一起执行成功,要么一起执行失败。
一致性:事务执行前后数据都是正确的,不存在矛盾。
隔离性:事务之间相互隔离,不干扰。
持久性:事务提交成功后,数据变化是永久性的。
读未提交:简称RU级别,可以读到别人未提交的数据,任何一种问题都不能防止。
读已提交:简称RC级别,只能读到别人已提交的数据,可防止脏读,不能防止不可重复读、幻读,Oracle默认。
可重复读:简称RR级别,在事务执行期间多次查询,无论别人怎么修改提交,都是查到一样的值,可防止脏写、脏读、不可重复读,不能防止幻读,MySQL默认,且它和SQL标准不同,可以避免幻读(MVCC机制)。
串行化:事务没有并发执行,所有问题都可以避免。
针对同一条数据存在两种并发情况:
读&写并发:通过MVCC机制实现RC、RR等隔离等级。
读&读并发:通过互斥锁实现。
每条数据和undolog都包含两个隐藏字段:
trx_id
roll_pointer
trx_id是最近一次更新这条数据的事务id,是一个递增的序列。roll_pointer是回滚指针,指向上一条undolog。
执行一个事务期间会生成一个或多个ReadView(一次性视图),主要包含四个关键元素:
m_ids:生成当前ReadView时还活跃的没提交的事务id列表。
min_trx_id:当前m_ids列表里最小的那个。
max_trx_id:是指下一个要生成的事务 id。下一个要生成的事务 id 肯定比现在所有事务的 id 都大。
creator_trx_id:每开启一个事务都会生成一个过多个ReadView,而creator_trx_id 就是这个开启的事务的 id。
事务60开启生成ReadView时,还有一个事务70未提交,那么m_ids就是[60,70],mix_trx_id就是60,max_trx_id就是71,creator_trx_id就是60。
一个事务是否可读当前版本数据,要拿数据行中的trx_id和该事务ReadView进行比较:
当trx_id = creator_trx_id:说明是自己修改的,可见。
当trx_id < min_trx_id:说明是生成当前ReadView前别人提交的,可见。
当trx_id >= max_trx_id:说明是生成当前ReadView后又有人更新了数据,不可见。
当min_trx_id <= trx_id < max_trx_id且在m_ids列表中:说明是生成当前ReadView时,修改了数据的事务是未提交的活跃事务,不可见。
当min_trx_id <= trx_id < max_trx_id且不在在m_ids列表中:说明事务在生成ReadView之前已经已经提交了,可见。
当比较得出的结果是对当前版本的数据不可见时,会顺着undolog版本链往下,拿undolog中的trx_id和当前事务ReadView进行比较,符合以上规则就直接读取undolog中的数据,这也叫快照读。
MVCC机制就是MySQL通过undolog日志版本链+ReadView机制实现RC、RR隔离等级的一套无锁机制,极大地提升了读&写并发性能。
上面说了一个事务执行期间会生成一个或多个ReadView。
RC级别:事务执行期间每一次select都会生成一个ReadView,这样已提交的事务就不会在m_ids列表里面,但是符合min_trx_id <= trx_id <= max_trx_id,所以可见,具体参照上面「版本比较规则」第5点。
RR级别:事务执行期间无论发生多少次select都只会生成一个ReadView,所以期间有其他事务提交了,也还会在m_ids列表里,会被当前事务认为是未提交的,所以不可见,具体参照上面「版本比较规则」第4点。
解决幻读:事务执行期间突然别人新增数据进来,但是别人的事务id是大于等于当前事务ReadView中的max_trx_id的,所以不可见,具体参照上面「版本比较规则」第3点。
悲观锁:假设总会被别人先更新,因此需要先抢占互斥锁,适合写多读少的场景,MySQL中的独占锁、共享锁都是悲观锁的实现。
乐观锁:假设总不会被别人更新,因此在更新前不抢占互斥锁,而是在更新时比较是否有人更新,适合写少读多的场景,可以通过添加version等字段来实现。
独占锁:简称X锁,对谁都互斥。
共享锁:简称S锁,只有共享锁对共享锁不互斥,其他对谁都互斥。
行锁、页锁、表锁。
InnoDB支持行锁、表锁。
MyISAM只支持表锁。
脏写:属于读&读并发问题,通过锁解决。
脏读、不可重复读、幻读:属于读&写并发问题,通过MVCC机制解决。
事务1把某行数据从磁盘加载到Buffer Pool缓冲页。
检查到未被别人加锁则直接加锁(生成锁数据结构,等待状态=false,并和数据关联起来,进行更新)。
事务2检查到数据已被别人加锁,此时也会对数据加锁,但会排队等待(生成锁数据结构,等待状态=true,并和数据关联起来,线程阻塞等待)。
事务1更新完后提交,释放它自己的锁,并找到和这条数据关联的事务2的锁,修改它的等待状态=false,并唤醒事务2继续执行。
事务2执行直至提交后释放锁。
加共享锁:select * from table_name where id = xxx lock in share mode。
加独占锁:select * from table_name where id = xxx for update。
查询显式加锁即表示接下来可能进行更新,其他事务就不能更新。如果加的是共享锁,则跟其他事务查询加的共享锁是不互斥的。
正常我们在进行update table_name set column_name = xxx where id = xxx的时候,会隐式地加上独占锁,这就是行锁,独占锁都是互斥的,所以不可能发生「脏写」问题,一个事务提交了才会释放自己的独占锁,唤醒下一个事务执行。
一般不建议通过显式查询加锁,在数据库层面实现负载的业务锁机制,而是通过redis/zookeeper等分布式锁实现。
数据页与数据页之间组成双向链表。
数据页内数据行之间组成单向链表。
每个数据页都有一个页目录,记录:主键→槽位,数据就放在槽位里。
为了方便二分法查找,数据页内的数据行要按照主键id有小到大排列,且后一个数据页里的主键id值都要比前一个数据页的大,若主键id是自增的就不用挪动,若是uuid等类型的随机字符串id,则会进行比较id大小,进行数据行挪动,这就是页分裂。
假如查找条件:where name like '%张三',此时不走索引。
从第一个数据页开始加载到内存,查询出所有数据行一条条进行比对,依次循环,直至扫描完所有数据页。
索引也是以数据页的形式存储在磁盘中,叫做「索引页」。
以主键索引为例,索引页保存内容是最小主键值和页号对应关系,如:
最小id=1 → 页号=2
最小id=4 → 页号=8
最小id=1 → 索引页号=20
最小id=58 → 索引页号=28
依次类推,形成一个B+树,同一层级的索引页组成「双向链表」,和数据页的双向链表是一样的。
根据id进行查找的时候,从B+树的最顶级索引页开始,使用二分法查找,一层一层往下,根据最小id值,找出属于哪个数据页。
最后在数据页里面的页目录里面,再次使用二分法查找出属于哪个槽位,然后把数据读取出来。
一个索引就对应一棵独立的B+树,基于主键的聚簇索引的索引页里面仅包含了主键值+页号。而我们自己建立的索引不管是单列索引还是复合索引,它的索引页里面仅包含索引字段的值+主键值。
以InnoDB存储引擎的主键索引为例。
最下层的索引页实际上是直接指向了数据页的,可以说是索引页+数据页共同组成了一颗大的B+树,「叶子节点就是数据页本身」,这就是聚簇索引。
而MyISAM存储引擎,主键索引树的叶子节点并不是数据页,这也是它和InnoDB引擎的最大区别。
以name字段索引为例。
索引页里面存储的是:
最小的name值 → 索引页号
(实际上还有主键值,name值相同的情况下,比较主键值,把数据放到主键值大的数据页里面去。)
索引树的最下面一层存储的是主键id和name值。
如果是select * from table where name = '张三'的查询步骤是:先在name索引树里面往下找到id,然后再通过主键聚簇索引查出整行数据,这就叫「回表查询」。
多字段复合索引也是一样的,索引页里面存储的是复合字段的最小值+索引页号。
聚簇索引和普通索引的区别是:
- 聚簇索引最下面一层叶子节点就是数据页,存放的是完整的一行一行的数据。
- 普通索引最下面叶子节点也是页,当存放的仅仅是,索引列的值和主键值。如复合索引(x1,x2,x3)的索引树,存的就是x1、x2、x3的值,以及主键id。
若查询的字段多于复合索引树中维护的字段,那得先通过该索引查出主键值,然后拿主键值通过聚簇索引,找到整行数据,最后查出相应的字段,这个过程就叫做「回表」。
InnoDB存储引擎默认会给我们维护一套主键的索引结构,即「聚簇索引」,而且表里的数据是直接放在索引树的最下层,作为叶子节点。聚簇索引的维护过程:
表创建好之后就是一个空的数据页,这个数据页作为聚簇索引的一部分,叫做「根页」。
新增数据时直接插入根页中。
当根页满了,新增两个新的数据页,把根页中的数据移到两个新的页中,并按照主键id值大小进行排序,第二个数据页的主键值都大于第一个数据页的主键值。
根页升级为「索引页」,保存的是最小主键值+页号,并分别指向下面两个数据页。
当数据越来越多,一个索引页放不下所有索引了,就会依次按照同样的方式分裂出更多的数据页和索引页。
班级、姓名、科目建立复合索引。
索引树叶子节点上包含三个索引字段的值,以及主键id值,并按顺序排列。
排列顺序:首先按照班级字段的值来排序,如果一样则按照姓名字段来排序,如果一样再按照科目名称来排序。
父级索引页存放的是每个子页里面班级+姓名+科目组合最小的值,以及对应的主键值。
索引页内部记录组成单向链表,同级的索引页之间组成双向链表。
查询是从最左侧的索引字段开始,用二分法进行查找,即先找出班级,再找出姓名,然后再找出科目。所以where条件一定得包含最左侧的索引字段,不然不走索引。
一个表索引数量不要太多,一般设计两三个,尽量包含where、order by、group by的字段,避免影响增删改的效率以及过多占用磁盘空间。
索引应建立在基数比大的字段,避免建立在值比较少,如只有0、1两个值这样的字段,避免无法发挥二分法查找的优势。
避免在索引字段上使用函数或者计算,那将导致不会使用索引。
从「二十九、索引数据结构」中知道,索引页里面仅仅包含索引字段的值+主键值。若遇上select * from table order by xxx这种查询,即使order by的字段设计了索引,此时也有可能因为大量的回表到聚簇索引查询,而让MySQL优化直接放弃索引,干脆走全表扫描。针对这种情况要加limit、where限制。
指定查询字段代替用select *,甚至最好就是需要的字段直接能包含在索引字段里面,这边就避免了二次回表查询。
尽量针对字段类型比较小的字段设计索引,比如tinyint,这样占用磁盘空间小。如果不得不对varchar (255)长文本字段设计索引的话,可以考虑前缀索引,即只把前面20个字做索引,但这样的话order by、group by都无法使用这个字段的索引了。
主键尽量用自增序列,尽量不用uuid之类的随机序列,这样起码可以避免主键的聚簇索引频繁地页分裂。
等值匹配原则:当where条件里面的字段和复合索引的字段完全一样时,一定会用上索引,即使顺序不一样,MySQL也会自动优化成按复合索引的顺序去查询。
最左侧列匹配原则:从「三十四、复合索引原理」可知,复合索引是从最左侧的字段开始查询,所以需要保证每个where、order by、group by后期面跟的字段,都是复合索引最左侧开始的部分字段,这样索引才会有用。
最左前缀匹配原则:like 'xxx%'可以走索引,而like '%xxx'不会走索引。
范围查找匹配原则:如果where条件中第一个就是范围查询,那么只有对复合索引最左侧那个字段的范围查询才会走索引。(首先顺序要遵守第9点最左侧开始原则)
等值匹配+范围查找匹配原则:如果where条件中第一个是等值匹配,后面是一个或多个范围匹配,那只有等值匹配和第一个范围匹配可以用上索引,后面的范围匹配用不上。(首先顺序要遵守第9点最左侧开始原则)
每次提交一个SQL给到MySQL,它里面的查询优化器,都会计算出一个最优的执行计划,包括:
怎么查各个表
使用哪些索引
怎么做排序和分组
在查询语句在开头加explain命令,如:
explain select * from table where id = xxx
id:一个复杂SQL可能包含多条执行计划,这个就是执行计划的id。
select_type:查询类型。
table:查询的表名。
partitions:表分区。
type:查询方式(关键)。
possible_keys:可供选择的索引(关键)。
key:实际使用的索引名称。
rows:预估会读取多少条数据。
filtered:经过搜索条件过滤之后剩余数据的百分比。
const:主键聚簇索引,或者普通唯一索引查询。
ref:普通索引查询。
range:使用普通索引进行范围帅选。
index:查询的字段包含在复合索引列内,只需要遍历复合索引树,不需要回表查询。
all:全表扫描。
以上方式查询速度由高到低。
QPS:每秒处理多少个请求。
TPS:每秒处理多少个事务。
IOPS:每秒执行多少个随机IO读写。
吞吐量:磁盘存储每秒可以读写多少字节的数据量。
latency:往磁盘里写入一条数据的延迟。
CPU负载:CPU处理能力。
网络负载:网路带宽每秒中传输数据量。
内存负载:内存耗费。
数据库调优简单来说,其实就是控制好表中的数据量,维护好索引,一般不会有太大的问题。
调整buffer pool的总大小,实例数,chunk块大小等参数,减少因为缓存页满了而大量刷盘的次数。
建立索引。
主从复制,读写分离。
分库分表。
避免在高峰期执行批量数据处理等长事务。
强制使用索引语法,避免因查询结果为空而让MySQL进行全表扫描。
不要使用not in等方式会导致全表扫描。
定期进行历史表转存,减少主表中的数据量。
主库写,从库读。
主库增删改的时候把二进制日志binlog写入磁盘。
从库发送请求到主库拉取binlog。
从库执行binlog的操作,还原出一样的数据。
主库运行了一段时间后,后面中途要搭从库,要怎么做?——在凌晨的时候,让业务系统对外不可用,不让数据写进来,然后在主库上手动备份数据,那到从库上还原,之后再开始主从复制。
异步复制的时候,从库还没拉取到binlog日志,主库就挂了,这时候切换到从库了但数据没同步过来怎么办?——采用半同步的方式进行复制,也就是等从库成功拉取到binlog日志,并给与反馈到主库了,主库才提交事务。
MySQL传统复制方式。
GTID复制方式。
Mycat等中间件搭建。
不要超过1000万,较好是在500万以内,最佳选择是100万以内,建立好索引,问题不会太大。
一般1亿行数据,大概在1GB到几GB,几千万用户,顶多就几GB大小。
数据量有多大,参考「四十六」。
看并发压力有多高,去用户中心,虽然数据量可能有几千万,但并发压力并不高,两台服务器足够了。
按数据行数参考要分到每个表的行数有多少,参考「四十五」。
垂直拆分:不同业务的表分到不同的数据库,分担读写压力。
水平拆分,一张表中的大量数据分到不同库不同表。
将主键id对表数量进行取模运算,然后根据余数路由到不同的库表。
根据订单id进行拆分,能均匀分散落到各个表中。(若根据用户id拆分,活跃用户的订单多,僵尸用户订单少,会导致分散不均匀)
针对用户端,要查询当前用户的订单列表,可以再做一个(userId,orderId)索引映射表,然后再对这个表按用户id进行拆分,这样就会有两次查询,先根据用户id查出他的订单id列表,然后用订单id去查询补全剩下的数据。
针对运营端,要根据多个条件进行复杂检索,可以做binlog监听,把要搜索的字段同步到ES,由ES进行复杂条件检索出一批订单id后,在根据订单id查询补全剩下的数据。