MySQL实战-1

 

目录

SQL查询

SQL更新

事务

索引

全局锁,表锁,行锁

细说事务隔离机制

普通索引和唯一索引

索引选择的问题

前缀索引

刷脏页的过程

表数据的存储方式

count(*)执行原理

参考


SQL查询

一条SQL查询是如何执行的

1.客户端经过连接器连上mysql,校验用户名/密码,如果连上之后长时间没操作会自动断开的
2.查询缓存,对表变化不大的可以用,表经常变动的一个update就会清空缓存反而效率变低
3.分析器,做sql语法分析语法校验,弄出一个完整的语法树
4.优化器,对选择索引的优化,如果是两个表join会判断先读哪个表
5.执行器,去存储引擎中做真正的操作

 

SQL更新

redo log
相当于WAL(Write-Ahead Logging),先写日志再写磁盘,可以保证crash后不丢数据,写的是物理page
redo log大小是固定的,4个文件来回写,如果write_pos的位置超过了check_point,则需要等待flush到磁盘

 


bin log
是Server级别,不是引擎级别的,所有的引擎都可以用
bin log记录的是逻辑改动,是append的,用来做回放而不是恢复

一个update流程
1.执行器先找引擎获取ID=2,ID是主键引起直接用树搜索找到这一行,如果在内存中直接返回否则读磁盘
2.执行器拿到后+1,再调用引起接口写入这行数据
3.引擎将新数据更更新到内存中,再将更新操作记录到redo log中,此时redo log是prepare状态
4.执行器生成这个操作的bin log,并写入磁盘
5.执行器调用引擎的提交事务接口,引擎把redo log改成commit状态
完整的流程图如下,浅色部分表示在InnoDB内部执行的,深色部分是在执行器中执行的

 

两阶段提交
bin log+全量备份数据(假设半个月),可以恢复到本个月内任意一秒的状态
找到全量备份数据,再重放binlog
如果是增加一个读库,也是类似的操作
假设不用两阶段提交,肯定是先写bin log或者先写redo log,那么
1.先写redo log再写bin log
redo log已经记录了,但bin log失败,系统崩溃后数据可以恢复,但bin log没记录,如果要增加从库就少了记录
2.先写bin log再写redo log
崩溃后数据就无法恢复,事务无效,但bin log记录后增加从库数据又有了,导致跟原库数据不一致

加上prepare后
1.先写redo log  如果这部失败了,那事务就无法恢复了,如果成功了则可以恢复
2.再写bin log,如果bin log失败,bin log和redo log用一个事务id,如果发现bin log失败了,则不会恢复redo log,这样保证了一致性
3.commit之后,redo log和bin log最终一致,无论机器crash还是加从库都没问题
对于特殊情况,比如第二步,bin log写完之后机器crash了,重启后发现虽然没有commit,但是redo log和bin log事务id一样,而且都是写入成功,那么就可以恢复,再次commit
 

 

事务

可能出现的事务问题
脏读 ditry read
不可重复读 non-repeatable read
幻读 phantom read

事务的隔离级别
读未提交 read uncommitted
读提交   read committed
可重复读 repeatable read
串行化   serializable

读未提交,一个事务还没提交时,它做的变更就能被别的事务看到
读提交,  一个事务提交之后,他做的更变才会被其他事务看到
可重复读,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的
串行化,  对于同一行记录,写会加写锁,读会加都锁,当出现读写锁冲突的时候,后访问的事务要等前面执行完才能继续执行

对于读提交,可重复读 假设有下面一段sql

mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);

假设有两个线程开启了两个事务,他们都做同样的事情

对于上图来说,四个隔离级别有四种情况
读未提交,V1的值就是2,事务B虽然没提交,但A可以看到B修改的值,因此V2,V3都是2
读提交,  V1是1,V2的值是2,事务B的更新提交后才能被A看到,V3也是2
可重复读,V1,V2都是1,V3是2,V2是1的原因遵循的是事务在执行期间前后看到的数据一致
串行化,  事务B执行"1改为2"会被锁住,直到事务A提交后,事务B才可以继续执行,从A的角度看,V1,V2都是1,V3是2

在实现上 数据库会创建一个“视图”,访问的时候以“视图”的逻辑结果为准
读未提交,直接返回记录的最新值,没有视图的概念
读提交,  这个视图是在每个SQL语句开始的时候创建
可重复读,这个视图是在事务启动的时候创建,整个事务存在期间都用这个视图
串行化,  没有视图,直接用枷锁的方式避免并行访问

通过下面方式查询当前的事务隔离界别

mysql> show variables like 'transaction_isolation';

可重复读的使用场景
假设有一个表存了每个月的月底余额,一个表存了账单明细
如果要做数据校对,判断上个月的余额和当前余额的差额,是否跟本月的账单明细一致
那么在校验过程中,即使有用户发生了一笔新的交易,也不影响校对结果

事务隔离的实现
每条记录在更新的时候都会同时记录一条回滚操作,记录上的最新值,通过回滚操作,都可以得到前一个状态的值
假设一个值从1被按顺序改成了2,3,4 那么回滚日志中就会有类似下面的记录

当前值是4,但不同时刻启动的事务会有不同的read-view
视图A,B,C里面这一纪录分别是1,2,4 同一个记录在系统中可以存在多个版本,这个就是数据库的多版本并发控制MVCC
对于read-view A要得到1,就必须将当前的值依次执行图中鄋回滚操作才能得到
当没有事务在需要这些回滚日志时会被删除,也就是系统里没有比这个回滚日志更早的read-view时
长事务意味着系统里会存在很老的事务视图,这里面就会记录很多回滚日志,导致占用大量的存储空间

事务的启动方式
1.显示的启动事务,如begin或start transaction,以及commit还有rollback
2.设置autocommit=0,这个命令会将这个现场的自动提交关闭,意味着执行一个select,这个事务就启动了,而且不会自动提交,这个事务会持续存在直到主动commit或者rollback,或者断开连接

有的客户端连接框架在连接成功后会先执行一个set autocommit=0,这导致接下来的查询都在事务中,如果是长连接,会导致以为的长事务
所以建议显示的开启 事务 set autocommit=1
页可以设置
commit work and chain
提交事务并自动启动下一个事务,这样省去了再次执行begin的开销,同时从程序开发的角度也明确的知道每个语句是否处于事务中

可以在infomation_schema库的innodb_trx 表中查询长事务,sql如下

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

 

索引

常见的索引模型
1.Hash,查询某个值很方便,冲突了就再散列或者加拉链(树化),但无法做遍历
2.有序数组,对数时间定位到某个值,支持范围查询,但增删比较麻烦
3.二叉搜索树,增加/删除都是对数时间
存储系统则改为用B+树,是多个子节点,现在LSM树,跳表也用于存储结构

InnoDB索引
mysql的的不同引擎的索引方式不同,以InnoDB为列,他包含主键索引和普通索引

主键索引也成为聚集索引clustered index
非主键索引的叶子节点内容是主键的值,也成为二级所以你secondary index
参考下面的sql,主键查询的方式,只需要搜索ID这颗B+树

select * from T where ID=500;  

如果是普通索引,需要先搜索k索引树,得到ID的值为500,再到ID索引树搜索一次,这个过程叫回表

select * from T where ID=500;

索引维护

索引的插入和删除,都需要重新维护这个B+树
自增主键的好处是追加操作,不涉及挪动其他记录,不会触发叶子节点的分裂
如果是业务之逻辑字段做主键,则很难保证插入有序,这样会出发叶子节点分裂
从存储空间角度考虑,主键长度越小,普通叶子节点就越小,普通索引占用的空间也就越小

大部分情况下用自增主键是最好的
如果某个表是类似k-v形式的,只有一个索引,并且该索引是唯一索引,那么就用这个索引做主键

 

覆盖索引
假设有下面这段sql

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');

那么这个sql

select * from T where k between 3 and 5;

他的执行过程如下
1.在k索引树上找到k=3的记录,得到ID=300
2.再到ID索引树查找ID=300对应的R3
3.在k索引树取下一值k=5,得到ID=500
4.再到ID索引树查到ID=500对应的R4
5.在k索引树取下一个值k=6,不满足条件,循环结束
这个查询过程中度了k索引树的3条记录(步骤1,3,5),回表了两次(步骤2和4)

如果写成这样

select ID from T where k between 3 and 5

这时候只需要查询ID的值,而ID的值已经在k索引树上了,因此可以直接提供查询结果不需要回表,这个查询需求较覆盖索引,使用覆盖索引是一个常见的性能优化手段

假设有个高频的查询需求,通过身份证查询名字
那么再建立一个(身份证,名字)的联合索引,可以满足这个需求,它会用到覆盖索引,不需要再回表查询整行数据,减少语句的执行时间
但这增加的冗余性,也是有代价的,所以需要综合业务情况去考虑

最左前缀
B+树可以用最左前缀这个特定来定位记录
假设联合索引是(name,age)

当查询所有名字是"张三"时候,可以快速定位到ID4这个记录,然后遍历所有结果
如果是模糊查询 like '张%' 也能利用上这个索引,查询到第一个符合条件的记录是ID3,再向后遍历

联合索引定位的原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就需要优先考虑
如果既有联合查询,又有基于a,b各自的查询,查询条件里有b是无法使用(a,b)的,只能再添加一个b索引,此时要考虑空间原则,name字段比age字段大,那么建立(name,age),(age)就比建立(age,name),(name)划算

索引下推
对于不满足联合索引中最左前缀中的部分,假设有联合索引(name,age)现在有一个需求检索出表中 名字第一个字是张,并且年龄是10岁的所有男孩,那么sql语句会这么写

mysql> select * from tuser where name like '张 %' and age=10 and ismale=1;

在mysql5.6之前只能从ID3开始一个个的回表,找到主键上的数据行再对比字段值

InnoDB内部就利用了(name,age)判断了age是否等于10,对于不等于10的记录,直接判断并跳过,所以只需要对ID4,ID5这两条记录回表取数据判断,只需要回表2次

 

全局锁,表锁,行锁

全局锁是对整个数据库实例加锁,mysql提供了一个加全局读锁的方式,命令为
Flush tables with read lock
执行后下面语句会被阻塞

  1. 数据库更新语句,增删改
  2. 数据定义语句,建表,修改表结构等
  3. 更新类事务的提交语句

全局锁的典型使用场景是,做全库逻辑备份
缺点

  1. 如果在主库上备份,在此期间业务基本上就停了
  2. 如果在从库上备份,在此期间不能执行从主库同步过来的binlog,导致主从延迟

官方自带的mysqldup使用参数 -single-transaction时,导数据之前会启动一个事务,确保拿到一致性视图,由于MMVC支持,这个过程中数据可以正常更新
但一致性读需要存储引擎支持,非InnoDB的就不行了

和 set global redonly=true 的区别

  1. 在有些系统中readonly会用来做其他逻辑,如判断一个库是主还是备
  2. 如果执行全局锁失败了,mysql会自动释放这个锁,但readonly则不会

表锁的语法是

lock tables 。。。 read/write

可以用unlock tables 主动释放锁,也可以在客户端断开连接时自动释放
如果A线程中执行

lock tables t1 read,t2 write

则其他写t1会阻塞,读写t2会被阻塞

另一类表锁是MDL 元数据锁,MDL不需要显示使用,在访问一个表的时候会被自动加上
MDL的作用是保证读写的正确性
MDL包括读和写锁
1.读锁之间不互斥,可以由多个线程同时对一张表增删改查
2.读写锁之间,写锁之间是互斥的,用来保证变更结构操作的安全性

一个注意的事项,给小表修改字段

A和B都是读锁,都可以正常执行
C是修改表结构会被阻塞
C自己阻塞了之后,所有要在表t上申请MDL读锁的请求也被会C阻塞
也就是这个表目前完全不可以读写了,如果这个表读写频繁,很快会导致数据库线程爆满
首先要解决长事务,alter table还要设置等待时间
MariaDB整合了阿里sql,支持不等待更新

ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ... 


行锁
假设有下面两个事务

A会锁住这两条记录,B执行的时候会被阻塞,等到A 提交后才可以继续执行

InnoDB事务中
行锁是在需要的时候才加上的,但并不会立即释放,而要等到事务结束后才释放,这就是两阶段锁协议
在事务中,要把可能造成锁冲突,可能影响并发读的锁尽量往后放
假设有一个买电影票的业务
1.客户A从账户余额中扣除电影票价
2.给影院B账户余额中增加票价
3.记录一个交易日志

如果用户C在买影院B的票,那么可能冲突的语句就是2了
所以改成3,1,2这样的顺序
可以最大限度的降低锁造成的并发

死锁

因为调整了语句的执行顺序,mysql中可能会出现死锁

解决办法

  1. 加入超时机制,通过参数 innodb_lock_wait_timeout 来设置
  2. 发起死锁检测,主动回滚死锁链中的某一个事务,让其他事务得以继续执行,将 innodb_deadlock_detect 设置为on,表示开启

第一个参数默认是50秒太长了,但设置的太短可能又会影响正常的读写
第二种策略默认是开启的
死锁检测是有额外负担的
每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度为O(n)的操作,如果1000个并发同时更新一行,那么就是100W这个量级了,会消耗大量的CPU,结果是CPU利用率很高但每秒却执行不了几个事务
解决办法

  1. 确保业务一定不会出现死锁,可以临时把死锁检测关掉,但这本身也是有风险的
  2. 做并发限制,要在数据库中间件层做限制,或者修改mysql代码,在进入引擎之前排队
  3. 逻辑上做修改,比如影院的账户总额等于10条记录总和,每次更新时随机选择一条更新即可,这样冲突概率变成 1/10,可以减少所锁等待个数

但这个逻辑需要考虑到可能有退票的情况,一部分行的记录可能会变成0等特殊情况
 

细说事务隔离机制

细说事务隔离机制
事务的两种启动方式
1.begin/start transaction ,一致性视图在执行第一个快照语句时创建
2.在执行 start transaction with consistent snapshot 创建一致性视图

mysql中有两个“视图”的概念
1.view,用来查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果
2.在InnoDB实现MMVC时用到的一致性读视图,用于RC(read committed)和RR(Repeatable
 Read) 两个隔离级别

InnoDB中每个事务都有唯一的事务ID,在事务开始的时候向InnoDB申请,并且是严格递增的
每行数据也有多个版本,每次事务更新时,都会生成一个新的数据版本,并把事务id赋给这个数据版本的事务id,记为row tx_id,同时旧的事务版本还会保留

undo log就是那个虚线,通过虚线可以得到
如果当前是V4,通过计算U3,U2就可以得到V2的值

对于视图的可见性问题,InnoDB定义对每个视图定义了一个数组
低水位是已经提交的事务,高水位是未提交的事务集合

对于当前的事务,一个数据版本的row trx_id 有以下几种可能
1.如果落在绿色部分,表示是已提交的事务或者当前事务自己生成的,是可见的
2.落在红色部分,这个版本是由将来还没提交的事务生成的不可见
3.落在黄色部分
  a.若在 row trx_id数组中,表示这个版本是还没提交的事务生成的不可见
  b.不在数组中,表示这个版本是已提交的事务生成的可见

假设有三个事务A B C
开始之前系统里只有一个活跃的事务ID是99
A B C 的版本号是100 101 102
三个事务开始之前 这一行的row trx_id 是90

C先更新,此时数据的版本事务id是102
B再更新,此时当前版本事务id是101
A去读,A的可见范围是[99,100],102和101对A都是不可见的,所以拿到的值还是(1,1)

可以这么理解,一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况
1.版本未提交,不可见
2.版本已提交,但在视图创建后提交的,不可见
3.版本已提交,在视图创建前提交的,可见

在用这个逻辑分析上图,对于A来说
(1,3)还没提交属于情况1,不可见
(1,2)提交了,但是在视图数组A创建后提交的,属于情况2,不可见
(1,1)是视图数组创建之前提交的,可见

对于更新逻辑

C先更新成2,B在C的基础上再更新成3
这里B不能在视图上更新,否则之前C的更新就丢失了
因此事务B的更新 set k=k+1 是在(1,2)的基础上进行的操作
这里有一条规则
更新数据都是先读后写的,而这个读,只能读当前的值,称为 当前读

如果A线程的查询不是普通的select,而是加了锁的,也会变成当前读
下面分别是一个读锁S锁,一个写锁X锁

mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;

再假设事务C更新后不是马上提交的,也就是如下图所示

虽然C' 没有马上提交,但是(1,2)这个版本已经生产了,并且是当前最新的版本
但因为C'还没提交,也就是两阶段锁协议,先更新再提交释放锁,(1,2)这个版本的写锁还没释放
事务B是当前度,必须加锁,因此就被锁住了,必须等C'提交这个锁才能继续当前读

读提交 和 可重复读
可重复读,是在事务开始时创建一致性视图,之后事务里的其他查询都共用这个一致性视图
读提交,每个语句执行前都会重新算出一个新的视图

对于前面的A B C三个事务更新情况,如果是读提交隔离级别,情况如下

事务A的查询语句视图数组是在执行这个语句的时候创建的,时序上(1,2)和(1,3)的生成时间都在创建这个视图的时刻之前
(1,3)属于情况1,还没提交,不可见
(1,2)属于情况3,提交了,可见
所以A的查询结果返回的是k=2
 

 

普通索引和唯一索引

查询过程对比
1.普通索引当满足某个条件k=5时,还要继续往下查找
2.唯一索引,满足条件后就停止检索
mysql会把一页的内容16K读入内存再检索的
普通索引除非出现在一页的最后,或者一个表中有很多相同的普通索引
正常来说普通索引就出现在一页中,很快就能读完停止,因为是内存读取索引普通和唯一差别很小


更新操作
假设要更新的目标页在内存中
1.唯一索引检查k=4,找到3-5之间的位置没有冲突插入这个值返回
2.普通索引找到3-5之间的位置,插入k=4这个记录返回
所以这两个操作性能差别很小

如果要更新的目标页不再内存中
1.唯一索引需要读取内存判断是否有冲突,再插入这个值,这里是随机I/O
2.普通索引更新记录到change buffer中,执行结束


change buffer
最初是叫insert buffer,后来支持delete和update,就改为change buffer
更新某个记录时,会先存到change buffer中,然后定期刷磁盘,写入到系统表空间中
change的内容会跟redo log一起合并,写入到磁盘中(顺序写IO)
change buffer默认是buffer pool的50%,可以通过参数调整
innodb_change_buffer_max_size

change buffer的使用场景
适合写多读少的场景,某一个页更新的越多,收益就越大
写多读少的场景,比如账单类,日志类系统
如果有一种业务,更新之后又立马要读取,就会访问这个数据页,会触发merge过程,这样随机访问I/O的次数不会减少,反而增加了change buffer的维护代价,可以选择关闭change buffer
如果业务上已经确定不会出现重复,或者没要求,尽量选择普通索引

因为主键是唯一的,所以插入操作就不能用chang buffer了
主键和数据内容一个B+树
普通索引和主键的值又是一个B+树
虽然主键的B+不能用change buffer了,但是普通索引这个B+树还是可以用change buffer的


change buffer和redo log
假设有下面的sql

mysql> insert into t(id,k) values(id1,k1),(id2,k2);

k1所在的数据页在内存中(innob buffer pool)中,k2所在的数据页不再内存中

整个操作过程如下
1.page 1 在内存中,直接更新内存
2.page 2没有在内存中,就在内存的change buffer区域,记录下“往page 2插入一行”这个信息
3.将上述两个动作记录到redo log中(上图中的3和4)
虚线是后台操作,不影响更新响应时间

假设在这之后又有一个sql
select * from t where k in (k1,k2)
如果读语句发生在更新语句不久,内存中的数据都还在,那么此时这两个操作就和系统表空间ibdata1,redo log(ib_log_fileX)无关了

读page1的时候,直接从内存返回
读page2的时候,需要把page2从磁盘读入内存中,然后应用chang buffer里面的操作日志,生成一个正确的版本并返回
可以看到直到读page 2的时候,这个数据页才会被读入内存

redo log主要节省的是随机写磁盘的I/O消耗,转换为顺序写
change buffer主要节省的是随机读磁盘的I/O消耗

 

索引选择的问题

。。。

 

前缀索引

假设给字符串增加索引,有两种方式

mysql> alter table SUser add index index1(email);
或
mysql> alter table SUser add index index2(email(6));

第一种方式建立后,整个B+树结构如下

第二种方式建立的B+树结构如下

第二种方式节省了存储空间,但如果选择的长度不合理,就会造成性能损失
比如想找到邮箱是
[email protected]的这个人
因为email(6)存储的是zhangs
所以输入zhangsan,zhangsong都满足条件
但很明显zhangsong是不对的,会多回表一次
而不用前缀方式,直接存储整合邮箱就不会有这种问题

使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本
使用前缀索引就没法用覆盖索引了,因为mysql没法判断是否出现截断

其他索引方式
1.倒序存储,然后存储后面的6位,比如身份证这种的
2.增加一个索引字段,存储hash值,查询稳定性好,有额外的计算消耗,和第二种一样不支持范围查询

 

刷脏页的过程

当内存数据页跟磁盘数据页内容不一致的时候,就称这个内存页为“脏页”,内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”

引发数据库flush的情况
1.InonoDB的redo log写满了,这时系统就会停止所有更新操作,把checkpoint往前推进,redo log留出空间可以继续写

checkpoint的位置从CP进退到CP',就需要将两个点之间的日志,对应的所有脏页都flush到磁盘上,之后图中从write pos到CP'之间就是可以再写入redo log的区域

2.系统内存不足时,当需要新的内存页,而内存不够用时,就需要淘汰一些数据页,空出内存给别的数据页使用,如果淘汰的是脏页,就要先将脏页写到磁盘

如果刷新脏页一定会写盘,就保证了每个数据页有两种状态
a.在内存里存在,内存里肯定就是正确的结果,直接返回
b.内存里没有数据,读取文件到内存

3.系统空闲的时候,主动刷新脏页到磁盘
4.MySQL重启的时候刷新脏页

第三种,第四种情况对性能不太关注,主要是第一和第二点
第一种情况会导致整个系统不能更新,从监控上看更新数会跌到0
第二种 内存不够用,要写将脏页写到磁盘,InnoDB管理内存,缓冲池中的内存页有三种状态
a.还没有使用的
b.使用了并且是干净页
c.使用了并且是脏页

当要读入的数据页没有在内存时,必须到缓冲池中申请一个数据页,用LRU淘汰一个,如果是干净页就直接释放出来复用,如果是脏页就需要先刷新到磁盘才能复用

刷脏页是常态,但出现以下两种情况会明显影响性能

  1. 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变成
  2. 日志写满,更新全部堵住,写性能跌为0

InnoDB 需要知道主机I/O的能力,这样才能全力刷磁盘
innodb_io_capacity

这个参数就是磁盘的IOPS值,可以通过fio工具来测试
下面是fio的随机读写命令

fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest 

InnoDB的刷磁盘速度会考虑两个因素

  1. 脏页的比列
  2. redo log写盘的速度

InnoDB会计算这两个值,然后取最大一个记做R,引擎按照 innodb_io_capacity定义的能力乘以 R% 来控制刷脏页到大速度
整个流程如下图

 

InnoDB 需要知道主机I/O的能力,这样才能全力刷磁盘
innodb_io_capacity

这个参数就是磁盘的IOPS值,可以通过fio工具来测试
下面是fio的随机读写命令
fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest 

InnoDB的刷磁盘速度会考虑两个因素
1.脏页的比列
2.redo log写盘的速度
InnoDB会计算这两个值,然后取最大一个记做R,引擎按照 innodb_io_capacity定义的能力乘以 R% 来控制刷脏页到大速度
整个流程如下图


InnoDB在后台刷脏页,这个过程需要写磁盘,会导致mysql性能监控的抖动
平常要多关注脏页比列,不要绕过他经常接近75%
脏页的比例是通过

innodb_buffer_poll_pages_dirty / innodb_buffer_pool_pages_total

得到的
还有一个参数,是刷新邻居页的,机械硬盘时,写磁盘的IOPS很低,所以如果有脏页了尽可能合并到一起刷新
innodb_flush_neighhbors 控制这个行为,如果当前要刷新,并且检测到邻居页也要刷新,并且邻居的邻居的邻居。。。 最后会引起一连串的刷新
到mysql 8.0这个参数默认就关闭了

 

表数据的存储方式

表的数据可以放到共享表空间里,也可以是单独的文件

innodb_file_per_table 这个参数决定
off表示放到共享表空间,on表示开启

mysql删除数据之后,不会清空数据,只是在数据加了一个删除的标志
对于下面的这个情况

删除R4后会标记为删除,那么在300-600之间再插入数据的话,可能会复用这个位置,但是磁盘大小不会缩减
如果删除的是一个数据页上的所有记录,那么整个数据页都会被复用,但是数据不会被删除
所以delete操作会导致文件出现空洞

此外插入也可能会导致数据出现空洞
假设下图,pageA已经满了,再插入数据导致分裂

再插入一个550的数据,pageA满了就不得不分裂,这样page A的末尾就留下了空间,而且可能不止一个记录位置
索引的更新,也可以理解为删除一个旧值,再插入一个新值,也可以造成空洞

表重建
使用下面语句重建表
alter table A engine=InnoDB
具体过程如下

这个过程是阻塞的,不能有更新操作
mysql 5.6之后引入了online DDL,也是新建一个临时表
但是之后对A的更新操作都放到一个日志文件中
之后再合并这个日志文件

再插入一个550的数据,pageA满了就不得不分裂,这样page A的末尾就留下了空间,而且可能不止一个记录位置
索引的更新,也可以理解为删除一个旧值,再插入一个新值,也可以造成空洞

表重建
使用下面语句重建表
alter table A engine=InnoDB
具体过程如下

这个过程是阻塞的,不能有更新操作
mysql 5.6之后引入了online DDL,也是新建一个临时表
但是之后对A的更新操作都放到一个日志文件中
之后再合并这个日志文件

alter table的完整写法包括

alter table t engine=innodb,ALGORITHM=inplace;
alter table t engine=innodb,ALGORITHM=copy;

对于truncate,alter,analyze,optimize几个的区别

  1. 从mysql的5.6开始,alter table t engine就是online DDL方式
  2. analyze table不重建表,只是对索引信息做重新统计没有改数据
  3. optime table t等于create+analyze
  4. truncate等于drop+create

 

count(*)执行原理

MyISAM 会保存当前表的总记录数,所以count(*)直接返回
InnoDB需要按行累加就很慢了

InnoDB没有像MyISAM那样保存表总行数,是因为MMVC的原因,不同的视图返回多少行是不确定的
假设有三个会话
A先启动事务并执行一次查询
B启动事务,插入一个记录,再查询总行数
C先启动一个单独插入语句,再查询总行数

对于MMVC来说,每个事务就没法判断到底是多少行了,只能一行一行取出来再判断
mysql 有一个命令
show table status
这里面有TABLE_ROWS,这个是采样的结果,误差可能达到40%-50%

所以总结下

  1. MyIASM的count(*)很快,但不支持事务
  2. show table status很快,但误差大
  3. InnoDB的count(*)很准但速度慢


自定义的总行数保存方案
用缓存保存计数,比如Redis,但Redis重启之后就不对了,需要重新做count(*)
另外即使Redis正常工作,记录的值可能还是不准
参考下面这个时序图,先插入数据,再更新

或者是先计数累加,再更新

因为这是两个系统,没有分布式事务,所以无法保证一致性
可以在mysql中再增加一个表C,专门做计数统计,然后插入和删除的时候,做事务
这样就能保证一致性了

count(id)会把每一行的id取出来返回给server,server判断不为空再累加
count(1)遍历整个表,不取值,server对返回的每一行放入1,再累加
count(字段)也是遍历整个表,判断不为null再累加
count(*)是列外,mysql对此专门做了优化

从性能上看
count(*)和count(1)差不多
count(id) 第二
count(字段)最差

 

 

参考

redo/undo log、binlog 的详解及其区别

《MySQL实战45讲》1~15讲

说说MySQL中的Redo log Undo log都在干啥

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章