MySQL笔记_7
主备
- 主备切换流程
状态1,客户端读写都只访问节点A,B是从库,同步A的更新,保证AB数据相同
需要切换的时候,就变成状态2,客户端只访问节点B
- 状态1不会操作B,但是建议将B设置为
read only
- 防止他人误操作B
- 防止切换逻辑出现双写的问题
- 判断访问节点的角色
read only
对超级管理员无效,所以可以同步更新的线程有超级权限
-
binlog statement
statement
就是记录SQL
语言,甚至会把注释也都记录进去statement
的格式下,delete
带有limit
,就是unsafe
的,会导致主从不一致- 删除条件的两个都有索引,如果主从版本不一,可能选择的索引不一致,所以顺序不同,导致
delete
的东西不同,本质上就是主从库可能delete
的时候用到的索引不同,因为有limit
存在,比如只删一条数据,就会导致数据不一致。
-
binlog row
row
里面多了Table_map
和Delete_rows
Table_map
用于说明接下来操作哪个库哪个表Delete_rows
用于定义删除行为- 实际上,
row
虽然会占用空间,但是有助于恢复数据,比如误删,可以根据binlog
看到,直接插入即可;误增误改都可以这么处理
-
binlog mixed
- 如果有大量的删,那么
row
就会记录大量的内容,导致磁盘IO变多,占用空间变多 statement
就不会,只会有一条语句,但是在某些情况下,会导致不一致- 所以
mixed
判断,如果不会导致不一致就用statement
,如果可能会不一致,就切换到row
- 这个格式用的不多
- 如果有大量的删,那么
双M
- 建议设置
log_slave_updates=ON
,从库执行完relay log
以后也生成binlog
- A更新事务,
binlog
记录A的server id
- B接收到
binlog
,更新,并记录server id
也就是A的server id
- 再传回A,A判断
server id
等于自身,所以就不会处理日志
并发连接和并发查询
- 线程等待锁的时候,并发线程计数减一
- 因为等待锁的时候不吃cpu资源
innodb_thread_concurrency
设置最大并发线程数
检测MySQL
-
再
mysql
库中创建一个health_check
表,只放一行数据,定期执行
mysql> select * from mysql.health_check;
-
可以检测因为并发线程过多导致的数据库不可用的问题
select 1
不行,因为不涉及InnoDB
引擎,就算超出限制也会返回- 但是这样的话,如果
binlog
磁盘占满了,也会阻塞commit
事务,但是不会阻塞返回,所以这种方法无法判断是否存在磁盘满的情况
-
更新判断:放一个
timestamp
字段,表示最后一次执行检测的时间:mysql> update mysql.health_check set t_modified=now();
- 对主从库都需要执行更新检测
- 为了主备更新不冲突,再
mysql.health_check
上存入多行数据,用两个库的serverid作为主键 - 但是可能IO已经100%了,此时正好执行到
update
语句,那么就无法判断这种情况,不能及时切换 - 可以通过检测
performance_schema
里面redolog
和binlog
的写入时间判断是否存在问题,但是性能损耗10%
误删数据
delete
误删某一行,那么就用binlog
恢复即可,需要binlog=row
,binlog_row_image=FULL
- 可以将
sql_safe_updates=ON
预防,如果delete
或者update
没有写where
条件,或者where
条件没有索引,就会报错 - 如果需要删全表,则使用
truncate table
或者drop table
- 但是如果
drop
或者truncate
,无法使用binlog
恢复,只能全量备份+增量恢复 - 如果对于核心库,可以使用延迟备份,减慢
binlog
到从库执行的时间,避免被删
kill
- 两个
kill
命令,kill query + id
和kill connection + id
- 发送
kill
命令以后,session
运行状态改为THD::KILL_QUERY
,并给session
发送一个信号 kill
无效- 线程没有执行到判断线程状态的逻辑
- 终止逻辑耗时较长
- 大事务被
kill
,需要回滚 - 大事务回滚包含较大的临时文件,压力过大,耗时长
DDL
最后阶段
- 大事务被
ctrl+c
命令是服务器开启一个新的线程发送kill query
命令
全表扫描
- 如果扫描一张大表,就获取一行数据,存储到
net_buffer
中,内存大小由net_buffer_length
控制,默认16K
- 重复获取行,直到
net_buffer
写满,发送 - 发送成功清空
net_buffer
,重复 - 发送返回
EAGAIN
或者WSAEWOULDBLOCK
,表示本地网络栈写满,进入等待,直到网络栈重新可写,再继续等待
- 所以如果客户端接收的慢,事务执行时间就会变长
- 可以使用
mysql_store_result
,将收到的内容存入客户端内存,加快接收速度 - 但是如果是大查询,就会占用大量内存,就需要使用
mysql_use_result
- 可以使用
innodb_buffer_pool_size
一般设置为可用物理内存的60%-80%,线上服务的内存命中率需要达到99%,可以通过show engine innodb status
查看Buffer pool hit rate
InnoDB
淘汰数据页用的LRU
的改进- 因为如果对一个不常用的表使用全表扫描,这时候就会将别的业务正在使用的内存给顶掉,导致别的业务缓存失效
- 所以
InnoDB
按照5:3
的比例将LRU
分为了young
和old
区,
- 如图,状态1,访问P3,P3在
young
区,按照LRU
,移到链表头部,变成状态2 - 此时,要插入新的数据页,淘汰
Pm
,但是新的数据页Px插入LRU_old
处 - 处于
old
区的数据页,每次被访问都要做下面的判断:- 如果数据页存在超过1s,就移动到链表头
- 少于1s,位置不变,
innodb_old_blocks_time=1000
,表示1s - 这样扫描大表也用到了
buffer pool
,但是对于young
区没有影响
join
join
应该将小表作为驱动表,大表作为被驱动表,因为InnoDB
只会用被驱动表上的索引,对于驱动表,也就是join
前面的表,用的是全表扫描。如果驱动表是行数N,被驱动表M被驱动表上索引查询,所以如果回表,就需要在普通索引和主键索引上都查询一次,那么时间复杂度为,驱动表上查询行,所以总共是,这就是NLJ
- 这是仅限于可以使用被驱动表的索引,如果被驱动表没有索引,就会使用
BNL
- 将驱动表的数据取出放到
join_buffer
中, - 将被驱动表的每一行跟
join_buffer
中的数据对比,满足条件放到结果集中 - 总时间复杂度就是
- 这样的话,时间复杂度就是两个表的全表扫描
- 如果驱动表较大,无法放到
join_buffer
中,就会分段放,join_buffer_size
默认256k
- 就会将驱动表放一部分行,直到
join_buffer
放满为止,然后将被驱动表取出比对,再清空join_buffer
,放驱动表的下一部分数据,直到比对结束 - 假设N行驱动表分
K
段,N
越大,K
越大,所以K
表示为, - 扫描行数就是
- 判断次
- 所以应该让小表当驱动表
- 就会将驱动表放一部分行,直到
- 将驱动表的数据取出放到
- 如果
join
很慢,就把join_buffer_size
改大一些 - 尽量让
join
走NLJ
,并且警惕explain
检查字段是否出现Block Nested Loop
- 在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与
join
的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。 join
比强行拆分单表查询性能更好
Multi-Range Read(MRR)
- 如果一张表有主键id索引,普通索引a,通过a索引扫描,有时候需要回表查询主键
- 此时a虽然是在索引上顺序查询的,但是因为需要回表,所以在主键id索引上就是乱序查的。但是大都数数据都是通过主键自增顺序插入得到的,所以可以认为按照主键递增顺序查询的话对磁盘接近顺序读,性能会更好
- 所以MRR就是先根据索引a找到数据,放到
read_rnd_buffer
中,(随机读缓冲区),将缓冲区中的数据按照id递增排序,然后依次在主键中查找。大小由read_rnd_buffer_size
控制,如果buffer
放满了就会先执行排序,然后查找id返回结果,清空buffer
再进行下一步 - 如果想要使用
MRR
,set optimizer_switch="mrr_cost_based=off"
,因为优化器不会判断时使用MRR
,所以设置为off
,固定使用MRR
Batched Key Access(BKA)
-
当使用
join
的时候,驱动表一行一行到被驱动表比对,用不上MRR
-
现在将驱动表的内容拿出来,放到
join_buffer
中,原本join_buffer
在BNL
里才有暂存驱动表的数据,在NLJ
没有用,现在NLJ
也会用到 -
然后通过
join_buffer
跟被驱动表比对 -
set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
通过这条语句开启MRR
和BKA
,因为BKA
基于MRR
-
如果
join
使用BNL
,多次扫描一个冷表,语句执行超过1s,再次扫描冷表就会将数据页放到LRU头部 -
对应的情况就是冷表的数据量小于
buffer pool
的3/8
,能够完全放到old
区中。 -
如果冷表很大,业务正常访问的数据页,就没有机会进入
young
区,这是因为正常访问的数据页要进入young
区的话,需要隔1s再被访问到,但是冷表很大,old
区在1s内就淘汰了正常数据页,就会导致buffer pool
没有这个数据 -
所以大表
JOIN
不仅会影响IO,IO在join
结束也就恢复了,还会影响buffer
,需要后面慢慢恢复
BNL对系统的影响
- 多次扫描被驱动表,影响IO
join
条件需要执行次对比,大表占用非常多资源- 导致
buffer pool
热数据被淘汰,影响内存利用率
-
可以尝试给被驱动表添加索引
-
如果不适合加索引,比如次数比较少,添加索引不划算。可以建临时表,给临时表加索引,再
join
即可,优化很好 -
或者自己在业务端实现
hash join
,会比临时表优化的还好,select * from t1
驱动表t1所有数据存入hash
结构,比如set之类,select * from t2 where b>=1 and b<=2000
获取被驱动表的2000
条数据,然后一条一条在业务端跟hash
表内容比对即可。临时表需要比较次,但是hash join
只需要比较2k
次 -
临时表可以用各种引擎,但是内存表只用
memory
引擎,一般由mysql自动创建销毁 -
临时表建表语法时
create temporary table ...
-
一个临时表只能被创建它的
session
访问,其他session
不可见 -
临时表可以与普通表重名
-
如果有同名的,那么
show create
和增删改查访问临时表 -
show tables
不显示临时表 -
所以临时表不需要担心重名问题,可以用来给
join
使用,而且不需要担心数据删除问题 -
分库分表跨库查询就会使用临时表
-
创建临时表的时候,
InnoDB
创建一个frm
文件保存表结构定义,前缀是#sql{进程 id}_{线程 id}_ 序列号
5.6版本之前,会在临时文件目录下创建一个相同前缀.ibd存放数据文件,5.7版本后有专门的临时表空间存放临时数据,因为前缀不同,所以可以创建同名临时表 -
但是普通表是库名+表名得到的,同一个库创建两个同名表就会报错
-
当
session
结束的时候,就会执行drop temporary table + 表名
-
如果
binlog
不是row
,就会记录临时表的操作,不然会报错,提示临时表不存在。但是row
格式只会记录数据,所以不会有对临时表的操作,不会发现临时表不存在这种问题,就不会记录临时表
内存临时表
union
的explain
会显示using temporary
,表示使用临时表,将union
前的结果存在临时表中,然后利用主键唯一性剔除第二个查询中的相同的结果
- 如果是
union all
,就没有去重了,所以也不需要临时表了
group by
-
创建内存临时表,表上两个字段m和c,主键是m
-
扫描t1上的索引a,取id,并%10记x
- 临时表没有x的行,那么插入x,1
- 如果有x的行,那么对应的c+1
group by
处理完以后默认会对字段m按照自然顺序排序,如果不想排序提高效率,就可以直接加上order by null
tmp_table_size
是内存临时表的大小设置参数,默认16M
,超过大小会转为磁盘临时表
-
不管是内存临时表还是磁盘临时表,
group by
都需要构建一个带唯一索引的表,执行代价高 -
5.7版本支持
generated column
机制,用来实现数据关联更新alter table t1 add column z int generated always as(id % 100), add index(z);
-
这样索引z上的数据就是
group by
有序的数据了group by id %100
就可以改写为select z, count(*) as c from t1 group by z;
此时不再需要临时表,也不需要排序,直接返回即可 -
group by
语句使用SQL_BIG_RESULT
提示,可以告诉sql
数据量很大,直接使用磁盘临时表,然后优化器还会将原本的B+树直接改为数组存储 ,两次优化
select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;
-
初始化
sort_buffer
,放入整型字段m -
扫描t1.a,取id%100放入
sort_buffer
-
对
sort_buffer
中字段m排序 -
排完序就是有序数组
-
这样就不会使用临时表
InnoDB
把数据放在主键索引上,其他索引保存主键id就是索引组织表IOT
-
总是有序
-
有空洞时,插入新数据只能放在固定位置,从而保证有序
-
数据位置改变,只需要修改主键索引
-
主键索引查一次,普通索引查两次
-
变长数据
Memory
数据单独存放,索引上保存数据位置,就是堆组织表HOT
-
按照写入顺序
-
有空洞时找到空位就可以插入新数据
-
数据位置改变,需要修改所有索引
-
所有索引地位相同
-
不支持变长,定义
varchar
,也是 按照char(N)
-
内存表锁表,并且丢失会有风险,一般不会用
- 但是内存临时表不一样,因为不会被其他
session
访问,所以没有并发性问题 - 本来重启就是要删除的,所以丢失没关系
- 备库临时表也不会影响主库
- 支持
hash
索引
- 但是内存临时表不一样,因为不会被其他
-
MyISAM
主键自增值在数据文件中 -
InnoDB
主键自增值在内存中,8.0版本实现持久化- 5.7版本前没有持久化,保存在内存,重启后就找表的最大值,最大值+1作为自增值
- 如果id=10,则
AUTO_INCREMENT=11
,删除id=10,还是11,如果删除以后重启,AUTO=10 - 8.0版本变更记录在
redo log
中,重启依靠redo log
恢复之前的值
-
插入一行数据,指定为自增,判断id=0,null,或者没有指定,就把表的自增值赋值给他
-
如果id指定了值,就使用指定值;如果指定值小于自增值,则自增值不变;
- 如果指定值大于等于自增值,就修改自增值,就是
auto_increment_offset
开始,auto_increment_increment
为步长叠加,找到第一个大于指定值作为新的自增值。两个参数默认都是1
- 如果指定值大于等于自增值,就修改自增值,就是
-
如果插入数据,自增主键,但是插入数据失败,数据重复了,那么数据无法插入成功,并且主键自增值也没有改回去(唯一键冲突会导致主键自增不连续)
-
事务回滚也会导致自增主键不连续,为了性能,因为申请id很快,但是回滚id可能会导致主键冲突,没有必要
-
批量
insert ... select, replace ...select, load data
的场景下设置innodb_autoinc_lock_mode=2
可以提升并发性,并且不会出现数据一致性问题