MySQL笔记_6
语句耗时
- 如果
select
长时间不返回,使用show processlist
查看当前语句处于什么状态- 比如
MDL
写锁阻塞了别的session
的读 - 可以查询是什么
session
阻塞,但是在MySQL
启动时需要设置performance_schema = ON
,这样会有10%
的性能损失
- 比如
-
sys.schema_table_lock_waits
,查询这张表可以找到导致阻塞的processid
,kill
掉即可 -
还有可能被
flush
阻塞了,一般flush
的两个语句是flush table t with read lock
锁表t
,flush tables with read lock
锁所有的表,本身刷脏页的速度很快,但是可能flush
命令被别的session
阻塞了 -
或者别的线程占用写锁,那么申请
lock in share mode
的读锁就会被阻塞。通过查找sys.innodb_lock_waits
表即可 -
如果在
RR
场景下,或者在一个事务中,使用select
,如果此时别的session
有一个update
语句,update
了一百万次,那么自己的select
语句就会特别慢,因为是一致性读,会存在非常大的undolog
需要不断查找,而如果使用select in lock share mode
,就是当前读,速度就会很快。
幻读
RR
下,普通查询是快照读,幻读只有在当前读的环境下才会出现,因为快照读不会看到别人提交的数据。并且,如果别人修改了一行,在当前读的两次查询中,第二次查询才读到,那不是幻读。幻读只有是读到别人新插入的行
-
幻读会破坏加锁的语义,因为先对d=5的行加锁,在别的
session
中insert
了一条d=5,a=1的行,并修改a=1的行将b改成2,这样的话,也就是修改了d=5的行,破坏了最初对d=5的行加锁的语义 -
幻读 + statement格式的
binlog
会导致数据不一致性,因为binlog
是在commit
的时候才会记录, -
即便给所有的行都加上锁,也无法解决幻读的问题,因为原本不存在的行,无法加锁,所以即便给所有的行都加锁,也没有办法阻止插入新的行
-
所以需要加上间隙锁,在值和值的中间加锁,避免插入新的值
-
解决幻读,需要加行锁和间隙锁(
gap lock
,开区间) -
行锁 + 间隙锁 =
next-key lock
(临键锁,左开右闭)
死锁
- 如果对不存在的一行加锁,比如id=9,不存在
select * from t where id = 9 for update
- 那么本来应该加行锁+间隙锁,但是因为id=9不存在,所以只会加间隙锁
- 本来
select for update
是两个排他锁,但是因为不存在这一行,所以两个session
都可以对id=9
这一行使用select for update
,因为都是间隙锁,所以不会冲突 - 此时
session B
判断不存在这一行,尝试添加id=9
的记录,但是因为session A
中有间隙锁,所以被阻塞 session A
也判断不存在这一行,尝试添加id=9
的记录,但是因为session B
中存在间隙锁,所以被阻塞,导致死锁
- 所以
RR
会导致死锁,因为只有RR
才有间隙锁,一般会设置为RC + row
格式的binlog
,这样可以解决死锁,并且不会出现数据日志不一致问题 - 一般在金融等,或者表备份的时候,才是
RR
加锁
- 加锁的基本单位是
next-key lock
,前开后闭 - 查找中访问的对象才会加锁
- 索引上等值查询,唯一索引的时候
next-key lock
退化为行锁 - 等值查询,最右边的值不满足等值查询的时候,退化为间隙锁
- 唯一索引上范围查询会找到不满足条件的第一个值
现在一张表,字段是id,c, d
,c
上有普通索引,id
是主键
- 内含数据
(0, 0, 0)(5, 5, 5)(10, 10, 10)(15, 15, 15)(20, 20, 20)(25, 25, 25)
举例1 - 等值查询间隙锁
sessionA | session B | session C |
---|---|---|
begin; | ||
update t set d = d + 1 where id = 7; | ||
insert into t values(8, 8, 8); | ||
update t set d = d + 1 where id = 10; |
- 对于
session A
表中没有id=7
的行,加锁单位是临键锁,前开后闭,所以范围是(5, 10]
- id=7是等值查询,找到最后边的值10,不满足等值条件,所以退化为间隙锁,加锁范围就是
(5, 10)
session B
插入id=8就会被阻塞,但是session C
插入id=10不会被锁
举例2 - 非唯一索引等值查询
sessionA | session B | session C |
---|---|---|
begin; | ||
select id from t where c = 5 lock in share mode; | ||
update t set d = d + 1 where id = 5; | ||
insert into t values(7, 7, 7); |
- 加临键锁,对
(0, 5]
- c是普通索引,所以会访问到右边第一个不满足的值为止,也就是查询到10, 所以
(5, 10]
加临键锁 - c=5是等值查询,最右边一个值10不满足等值条件,退化为间隙锁
(5, 10)
- 访问到的对象才会加锁,覆盖索引不需要访问主键,所以主键索引不需要加锁,因此
session B
可以完成,session C
插入(7, 7, 7)
会被阻塞 lock in share mode
只会锁覆盖索引,但是for update
会判断你要更新,就会给主键索引上满足条件加行锁
举例3 - 主键索引范围查询
sessionA | session B | session C |
---|---|---|
begin; | ||
select * from t where id >= 10 and id < 11 for update; | ||
insert into t values(8, 8, 8); | ||
insert into t values(13, 13, 13); | ||
update t set d = d + 1 where id = 15; |
- 先找到
id = 10
,next-key lock (5, 10]
,但是唯一索引会退化为行锁 - 但是范围查找,找到id=15停止,所以会有临键锁
(10, 15]
- 所以B插入
(8, 8, 8)
可以通过,插入(13, 13, 13)
被阻塞;C更新15也被阻塞
举例4 - 非唯一索引范围查询
sessionA | session B | session C |
---|---|---|
begin; | ||
select * from t where c >= 10 and c < 11 for update; | ||
insert into t values(8, 8, 8); | ||
update t set d = d + 1 where c = 15; |
- 查找c = 10的时候,临键锁
(5, 10]
,不是唯一索引,不会退化为行锁 session A
加上了两个临键锁(5, 10]
和(10, 15]
- B插入
(8, 8, 8)
阻塞
举例5 - 唯一索引范围查询bug
sessionA | session B | session C |
---|---|---|
begin; | ||
select * from t where id > 10 and id <= 15 for update; | ||
update t set d = d + 1 where id = 20; | ||
insert into t values(16, 16, 16); |
- A是范围查询,索引id加到
(10, 15]
临键锁,id是唯一索引,所以判断到id=15结束 - 但是会访问到第一个不满足条件的值为止,所以会找到id=20,范围查询,所以会加上
(15, 20]
的临键锁 - 因此B更新20会阻塞,C插入16也会阻塞
举例6 - 非唯一索引存在等值
比如同时有两行c=10,分别是(10, 10, 10)
,和(30, 10, 30)
- 非唯一索引上包含主键值,所以不会重复
sessionA | session B | session C |
---|---|---|
begin; | ||
delete from t where c = 10; | ||
insert into t values(12, 12, 12); | ||
update t set d = d + 1 where c = 15; |
- A遍历找到第一个c=10的记录,所以临键锁加在
(5 5, 10 10]
上 - A向右查找,碰到第一个不满足条件的行,也就是
15 15
停止,等值查询,退化为10 10
到15 15
的行锁 - 所以最后是
(5 5, 10 10]
临键锁,(10 10, 15 15)
行锁,因此B插入12会被阻止,C修改15会通过
举例7 - limit加锁
sessionA | session B |
---|---|
begin; | |
delete from t where c = 10 limit 2; | |
insert into t values(12, 12, 12); |
- 因为有
limit 2
,所以遍历到10 30
后,就已经满足两条了,循环结束 - A加锁范围就是
(5 5, 10 10]
临键锁(10 10, 10 30]
临键锁 - B插入12通过
- 所以删除的时候可以加
limit
,限制删除的条数,操作更安全。- 但是唯一索引上删除不需要,因为唯一索引本身就只加行锁
- 可能更适合普通索引,但是数据是唯一的情况
举例8 - 死锁
sessionA | session B |
---|---|
begin; | |
select id from t where c = 10 lock in share mode; | |
update t set d = d + 1 where c = 10; | |
insert into t values (8, 8, 8) |
- A在c上加了
(5, 10]
临键锁和(10, 15)
间隙锁 - B要在c上加
(5, 10]
临键锁,所以等待A释放 - A 想插入8,需要加锁,等待B释放,死锁
- 最后B回滚
- B的临键锁没有申请成功,但是B加锁
(5, 10]
先加间隙锁,加锁成功,再加c=10的行锁,阻塞,两段执行
总结
- RR才有间隙锁,并且两阶段提交,事务
commit
了,锁释放 - RC没有间隙锁,并且语句执行完就会释放不满足条件的行的行锁
- RC在外键也会有间隙锁
优化
show variables like '%connections%;查看max_connections
,也就是最大连接数,超过最大连接数的就会被阻止,最大连接数不是越大越好。
- 处理掉占用连接但是没有工作的线程
show variables like 'wait_timeout';
查看超时时间,超过时间以后就会断开连接- 可以使用
kill connection + id
,但是这样客户端不会马上知道,只有在发起下一个请求以后才会报错,就变成了MySQL
一直都没有恢复,但是再次 调用接口就可以访问了
- 减少连接消耗
- 用
–skip-grant-tables
跳过权限验证,但是风险极高,不建议使用,找回密码可以用 - 8.0版本如果使用跳过权限验证,会同时
--skip-networking
打开这个,保证数据库只有本地访问
- 用
- 性能问题导致短链接增加
- 慢查询
- 索引没设计好
- 5.6版本后可以使用
Online DDL
创建索引,alert table
即可 - 如果一主一备,在从库处理,
set sql_log_bin=off
,不写binlog
,加索引 - 主备切换
- 再在从库
set sql_log_bin=off
,加索引
- 5.6版本后可以使用
- 语句有问题
- 使用存储过程
query_rewrite
重写某一个语句
- 使用存储过程
- 选错索引
- 使用
query_rewrite
加上索引即可,或者语句加索引
- 使用
- 可以在上线前处理好,上线前将
slow log
打开,long_query_time=0
记录查询语句 - 测试表模拟线上数据,回归测试
- 留意
row_examined
数据 - 全量回归测试可以使用开源工具
pt-query-digest
- 索引没设计好
QPS
暴增- 如果是全新业务有
bug
,下掉业务,数据库端去除白名单 - 新功能是单独数据库用户,删除用户,断开现有连接
- 如果和现有业务部署在一起,查询重写功能重写语句,压力最大的
SQL
直接select 1
返回- 但是会有误伤,如果别的业务也用了这个SQL,或者这个语句还有别的用处,都会导致其他业务失败
- 如果是全新业务有
- 更新语句
- 慢查询
数据更新
-
binlog
在事务执行时写入binlog cache
,事务提交时写入binlog
,清空cache
-
binlog_cache_size
控制cache
大小,超过大小的暂时写入磁盘 -
多个线程有自己的
binlog cache
,但是共用一个binlog
-
write
是写入cache
,速度快,fsync
是写binlog
文件,sync_binlog
参数控制时机sync_binlog = 0
,每次提交只write
,不fsync
sync_binlog = 1
每次提交都会fsync
sync_binlog = N
(N > 1
),每次都write
,累计N
个事务再fsync
- IO非常大的话,可以适当调大这个值,
100 - 1000
,但是如果重启了,就会丢失N条binlog
,如果容错率很低,不能设置为1
-
redo log
三种状态,分别是redo log buffer
,write
到page cache
,没有持久化和fsync
到磁盘,innodb_flush_log_at_trx_commit
控制= 0
表示每次都只保存再buffer
中= 1
表示每次提交都持久化到磁盘= 2
表示每次提交都write
到page cache
InnoDB
后台线程每隔1s都会将redo log buffer
中的内容write
到pagecache
再fsync
到磁盘,所有的线程共用一个redo log buffer
,与binlog
不同,所以没有提交的事务的redo log
,也有可能被持久化到磁盘- 同样,如果
redo log buffer
占用innodb_log_buffer_size
即将到达一半,就会触发主动写盘,但是只write
,没有fysnc
- 如果并行事务,并且
innodb_flush_log_at_trx_commit=1
,那么别的事务提交的时候,会把自己没有提交的redo log
持久化到磁盘 - 如果=1,那么
prepare
阶段就会持久化一次,所以只需要每隔1s的刷盘,崩溃恢复的逻辑,只需要binlog
写入磁盘,redo log prepare write
了,就不需要fsync
了 - 如果
sync_binlog=1, innodb_flush_log_at_trx_commit=1
,那么事务提交前,现需要等两次刷盘,一次是redo log
的prepare
,一次是binlog
- 也就是如果
MySQL``TPS
两万,就会有四万次写磁盘,所以使用了组提交的方法减少写盘。
-
日志逻辑序列号是
LSN
,单调递增,对应redo log
的写入点,写入长度为len
,那么LSN + len
,可以避免多次重复提交redo log
- 比如三个事务同时在
redo log buffer
中,事务1写盘的时候,会带上事务23写盘,这样LSN
之前的事务都完成了写盘,只需要看LSN
后的事务即可 binlog
也是有组提交,但是效果不如redo log
,因为redo log
时间较短,所以binlog
等不到那么多
- 比如三个事务同时在
- 设置
binlog_group_commit_sync_delay
和binlog_group_commit_sync_no_delay_count
,满足一个即可,减少binlog
的写盘次数,故意等待时间,跟sync_binlog
不同,sync
是没有写盘就commit
事务,但是这两个参数是直到写盘了,才commit
事务,会增加响应时间,但是不会像sync
一样丢数据innodb_flush_log_at_trx_commit
不建议为0,这样redo log
只会在内存中,宕机了会丢数据innodb_flush_log_at_trx_commit
设置成2,性能差不多,只有在主机关机才会丢数据,如果服务宕机,重启以后数据还在
-
binlog不能中断,必须连续写,所以每个线程一个
binlog cache
,不然主从不一致;redo log
甚至可以跟着别的线程一块写,所以都是一个redo log buffer
-
所以可以
sync_binlog=1,innodb_flush_log_at_trx_commit=2
,这样redo log
如果丢失了,就通过binlog
恢复就可以 -
一般设置非双一,就是
innodb_flush_log_at_trx_commit=2
、sync_binlog=1000
- 业务高峰期,会有预案主库改写为非双1
- 从库延迟,为了让从库赶上主库,减少
binlog
生成频率,赶上了可以改回来 - 用备份恢复主库的副本,也就是使用
binlog
的时候 - 批量导入数据也可以修改,不然
binlog=row
产生内容太多
-
主从首先保证了最终一致性,只要主库正确提交,就有
binlog
,那么从库正常就会重放binlog
-
主从延迟就是从库执行完的时间点-主库执行完的时间点
-
从库上执行
show slave status
,可以看到seconds_behind_master
,表示延迟了多少 -
主从延迟最主要是从库消费
relay log
的速度比主库生成binlog
的速度更慢 -
主从延迟来源
- 从库性能比主库机器性能差(但是现在一般不会)
- 从库压力大,CPU资源占用较多,导致重放
relay log
速度变慢- 一主多从,让多个从库分担压力
binlog
输出到外部,比如Hadoop
,让外部提供查询能力
- 大事务,比如一个事务再主库上执行了十分钟,就延迟了十分钟
- 大表上
DDL
主从复制
- 5.6版本之前,支持单线程,但是会导致应用日志较慢,造成延迟
- 多线程就是拆
sql thread
为多个线程
- 比如
coordinator
就是原来的sql thread
,但是不更新了,只是负责读中转日志,并分发日志,work
线程才更新日志,worker
线程个数根据slave_parallel_workers
决定,一般32核设置8-16即可, coordinator
分发规则:- 不能更新覆盖,同一行的两个事务必须在同一个
worker
中 - 不能拆分事务,同一个事务必须在同一个
worker
中
- 不能更新覆盖,同一行的两个事务必须在同一个
- 比如
- 按表分发策略
- 如果两个事务更新不同的表,就可以分发到不同的
worker
中 - 如果事务跨表,需要将两张表一起考虑
- 每个
worker
中对应一张hash
表,保存当前worker
中正在执行的事务里面涉及的表,值是多少个事务在更新这个表 - 如果
coordinator
收到事务T,判断其中修改的表- 没有
worker
中包含这些表,那就分给最空闲的worker
- 如果跟多于一个
worker
冲突,就等待 - 如果只跟一个
worker
冲突,那就分给这个worker
- 没有
- 所以如果热点表,就变成了单线程,如果负载均衡,就很好解决问题
- 如果两个事务更新不同的表,就可以分发到不同的
- 按行分发策略
- 需要
binlog = row
- 必须有主键,不能有外键
- 但是对于大事务,耗费内存,耗费CPU资源更大
- 需要
- 5.6版本,按库分发并行
- 5.7版本
slave-parallel-type
控制并行策略,配置为DATABASE
就是按库并行,配置LOGICAL_CLOCK
- 同处于
prepare
的事务,备库可以并行 - 处于
prepare
和commit
的事务可以并行
- 同处于
一主多从基本架构
- 主库负责写入和一部分读,从库负责读
切换主库的时候需要找同步位点,5.6版本使用GTID
,全局事务ID,在事务提交时生成
两个部分,格式为server_uuid:gno
server_uuid
是实例启动产生的,全局唯一gno
是整数,初始值为1,每次提交事务的时候分配给这个事务,就加1- 启动
GTID
在启动时加上参数gtid_mode=on
和enforce_gtid_consistency=on
即可
使用基于GTID
的主备切换,就不需要指定位点,因为位点是不精确的。所以建议使用基于GTID
的主从复制
主从延迟怎么解决
-
强制走主库方案;
- 比如交易平台,卖家发布商品以后,马上返回主页面,看商品是否发布成功,这个就强制走主库,因为需要拿到最新的结果
- 如果是买家,晚几秒看到商品也可以接受,就走从库
-
sleep
方案sleep(1)
大部分同步1s内能搞定,但是不太稳妥
-
判断主备无延迟方案
show slave status中的seconds_behind_master
是否等于0- 单位是秒,如果精度不够,可以使用位点和
GTID
- 单位是秒,如果精度不够,可以使用位点和
Master_Log_File
和Read_Master_Log_Pos
表示读到主库的最新位点
Relay_Master_Log_File
和Exec_Master_Log_Pos
表示备库执行的最新位点
如果Master_Log_File=Relay_Master_Log_File&&Read_Master_Log_Pos=Exec_Master_Log_Pos
表示日志同步完成GTID
中
Auto_Position=1
表示主备使用了GTID
协议
Retrieved_Gtid_Set
是备库收到的所有日志GTID
集合
Executed_Gtid_Set
是备库已经执行完的GTID
集合
如果两个集合相同,则日志同步完成- 但是上面的方法是判断备库收到的
binlog
已经执行完了,但是实际上可能主库有binlog
,但是备库还没有收到,这时需要使用半同步复制 - 业务高峰期,主库位点和
GTID
更新很快,位点判断就会一直不成立,那么从库就一直都无法响应
-
配合
semi-sync
方案- 牺牲一定可用性,保持一致性
- 但是因为半同步只要一个从库同步就返回
ack
,实际上请求可能打到别的没有同步的从库上 - 所以半同步配合位点方案,会有问题
- 一主多从,会有过期读的问题
- 如果一直有延迟,就一直无法
select
,但是实际上不需要等待到完全没有延迟,而是只要一个事务完成以后,select
到哪个事务即可,不需要等待其他事务也同步完
-
等主库位点方案
select master_pos_wait(file, pos[, timeout]);
file pos
是主库上的文件名和位置- 从库上执行
timeout
可选,表示超时- 返回一个正整数
M
,表示命令开始执行,到应用完file
和pos
表示的binlog
位置,一共执行了多少个事务 - 如果备库挂了,那就返回
NULL
- 如果超时,返回
-1
- 如果执行时,已经执行过这个位置了,返回0
- 具体流程
- 一个事务执行完,马上在主库上执行
show master status
得到主库执行到的File
和Pos
(不需要完全精确) - 选一个从库查询
- 从库上执行
select master_pos_wait(File, Pos, 1);
- 如果返回>=0,则再这个从库上执行查询语句
- 否则就去主库查询
- 一个事务执行完,马上在主库上执行
- 所以如果不允许过期读的情况出现,那么就超时放弃,或者转到主库查询,并做限流
-
等
GTID
方案WAIT_FOR_EXECUTED_GTID_SET(gtid_set[, timeout])
,超时时间默认为0,如果为0,表示一直等待- 语句作用:等待,直到这个库执行的事务包含传入的
gtid_set
,返回0 - 超时返回1
- 语句作用:等待,直到这个库执行的事务包含传入的
- 5.7版本后,不需要主动执行
show master status
,而是更新以后返回事务的GTID
,减少一次查询session_track_gtids = OWN_GTID
- 通过API从
mysql_session_track_get_first
解析GTID
即可
- 具体流程
- 事务完成,接受到事务的
GTID,gtid1
- 选一个从库查询,执行
WAIT_FOR_EXECUTED_GTID_SET(gtid1, 1)
- 返回0就在这个从库查询
- 否则去主库查询
- 事务完成,接受到事务的