MySQL笔记_5
日志
MySQL
中日志分为四类: 错误日志、二进制日志、查询日志、慢查询日志
错误日志
- 记录了当
mysqld
启动和停止时,以及服务器在运行过程中发生的任何严重错误时的相关信息。服务器出现故障无法使用就看这个日志 - 默认开启,存放在
/var/log/
中,日志文件名为mysqld.log
- 查看错误日志位置:
show variables like '%log_error%';
二进制日志
binlog
是server层日志
binlog
记录了所有的DDL, DML
语句,不包含查询语句binlog
默认开启,查看二进制日志位置:show variables like '%log_bin%';
- 最开始
MySQL
只有MyISAM
引擎,只有binlog
用于归档,没有crash-safe
的能力,后面InnoDB
设计了另一套日志系统,利用redo log
来实现crash-safe
的能力
作用
- 灾难时数据恢复
MySQL
主从复制
日志格式
- 查看二进制的日志格式:
show variables like '%binlog_format%';
日志格式 | 含义 |
---|---|
STATEMENT | 基于SQL 语句的日志记录,记录SQL 语句,对数据进行修改的SQL 都会记录在日志文件中 |
ROW | 基于行的日志记录,记录的是每一行的数据变更(默认) |
MIXED | 混合了STATEMENT与ROW两种格式,默认采用STATEMENT,某些情况下会自动切换为ROW记录 |
- 如果要修改二进制文件的日志格式,就去修改
/etc/my.cnf
,添加binlog_format=STATMENT
后,使用systemctl restart mysqld
重新启动服务即可
查看二进制日志
mysqlbinlog [参数] binlog文件名
参数:
-d
指定数据库名称,只列出指定数据库的相关操作-o
忽略日志中前n行命令-v
将行事件(数据变更)重构为SQL
语句-vv
将行事件(数据变更)重构为SQL
语句,并输出注释信息
删除二进制日志
配置二进制日志的过期时间,到了过期时间会自动删除: show variables like '%binlog_expire_logs_seconds%';
指令 | 含义 |
---|---|
reset master | 删除全部的binlog 日志,删除之后日志编号从binlog.000001 重新开始 |
purge master logs to ‘binlog.******’ | 删除******之前的所有日志 |
purge master logs before ‘yyyy-mm-dd hh24:mm:ss’ | 删除日志为’yyyy-mm-dd hh24:mm:ss’之前产生的所有日志 |
更新语句流程
- 执行器调用引擎找到
ID=2
的一行,如果这一行的数据页本身就在内存中,那么就直接返回给执行器,否则读入内存后再返回 - 执行器拿到引擎返回的数据,加1,调用引擎写入新数据
- 引擎将数据更新到内存中,同时更新
redo log
,此时redo log
处于prepare
状态,告知执行器写入完成,可以提交事务。 - 执行器生成
binlog
,并将binlog
写入磁盘 - 执行器调用引擎的提交接口,把
redo log
状态改为commit
状态,完成更新
这里的
prepare
和commit
就是两阶段提交,为了保证两个日志逻辑一致,所以只有在binlog
写入完成以后,redo log
才会从prepare
变为commit
binlog
记录的是数据操作的原始逻辑操作,也就是sql语句,所以回放的时候使用的是binlog
- 某时刻有一次删表操作,需要找到最近的一次全量备份,从备份的时间点开始,回放
binlog
,重放到误删表的时刻即可
如果不是两阶段提交, 例如更新ID=2的记录中c字段原始 = 0, 更新后 + 1
-
先写
redo log
,后写binlog
redo log
写完了,binlog
没有写完就宕机了,恢复后系统依然可以得到c=1,但是binlog
没有写完,所以binlog
没有这个语句,备份日志就有语句丢失,如果需要使用binlog
恢复恢复临时库,就少了一次更新,导致最终临时库中c=0, 与原始库不同
-
先写
binlog
,后写redo log
binlog
写完了,但是redo log
还没有写,崩溃恢复以后事务无效,所以c=0,但是binlog
已经将c=1的更新语句记录下来了,所以使用binlog
进行恢复的时候就会得到c=1,与原始库不同
- 如果面临数据库的扩容问题的时候,就需要全量备份 + binlog,如果
binlog
和redo log
记录的状态不一致,就会导致主从不一致问题
查询日志
- 查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询语句。
- 默认未开启,使用
show variables like '%general%';
查看
按照如下方式修改/etc/my.cnf
可以打开查询日志
1 | general_log=1 |
systemctl restart mysqld
# 重启mysql服务
慢查询日志
- 记录所有执行时间超过参数
long_query_time
,并且扫描记录数不小于min_examined_row_limit
的所有SQL
语句的日志,默认未开启。 long_query_time
默认10秒,最小为0,默认文件名为localhost-slow.log
- 慢查询默认不会记录管理语句,也不会记录不使用索引进行的查询语句。使用
log_slow_admin_statements
和log_queries_not_using_indexes
更改此行为,即1
2
3
4# 记录执行较慢的管理语句
log_slow_admin_statements=1
# 记录执行较慢的未使用索引的语句
log_queries_not_using_indexes=1
redo log
redo log
是引擎层日志
Writing-Ahead Logging(WAL)
先写日志,再写磁盘,避免每次更新一条数据就要写入磁盘,导致大量的IO消耗- 当一条记录需要更新,
InnoDB
就先写入到redo log
中,并更新内存,就算更新完成。然后在适当的时候再将记录更新到磁盘中。因为写入日志是顺序写入的,比写入磁盘这种随机写入要快的多。 redo log
的大小是固定的,可以配置为一组4个文件,每个文件大小是1GB
,从头开始写,写到末尾会重新从头写入
writepos
是当前写入的位置,边写边后移。checkpoint
是当前擦除的位置,边擦边后移,擦除时将当前记录更新到数据文件中。writepos
和checkpoint
就是可以写入的位置,如果二者相等,表明此时不能写入新的数据了,需要先擦除一些记录,才能继续写入。InnoDB
通过redo log
保证数据库重启以后依然可以实现数据恢复,提交的记录都不会丢失,就是crash-safe
redo log
VS binlog
redo log
是InnoDB
引擎特有的,binlog
属于server
层,所有引擎都可以使用redo log
是物理日志,记录在某个数据页上修改了什么;binlog
是逻辑日志,记录原始逻辑,比如对ID = 2
这行的c字段+1redo log
循环写,空间固定,会消耗完;binlog
追加写入,一定大小后切换到下一个,不会覆盖以前的日志
redo log
刷脏页
- 内存数据页与磁盘数据页不同,内存数据页就是脏页,内存写入磁盘后,就是干净页
- 更新写入
redo log
,没有写入磁盘,就是脏页MySQL
空闲,就会flush
到磁盘上MySQL
正常关闭,会flush
到磁盘redo log
写满了,没有办法接受行的请求,所有更新暂停,flush
脏页- 内存不够用了,要先
flush
一部分- 读取数据的时候,需要申请行的页,那么如果没有内存空间了,就需要淘汰干净页。如果没有干净页,就需要
flush
脏页以后变成干净页再淘汰
- 读取数据的时候,需要申请行的页,那么如果没有内存空间了,就需要淘汰干净页。如果没有干净页,就需要
控制刷脏页
- 配置
innodb_io_capacity
参数,设置为磁盘的IOPS
,告诉InnoDB
磁盘能力,可以利用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
- 设置这个参数,可以控制全力刷脏页。如果设置的较低,就会导致刷脏页速度变慢
- 控制全力刷脏页速度的占比,有两个因素,脏页再内存中占比和
redo log
写盘的速度-
配置参数
innodb_max_dirty_pages_pct
,默认75%
,InnoDB
会计算当前脏页的占比M
-
每次日志记录都有编号,跟
checkpoint
对应序号的差输入函数计算,得到N
,N
越大,计算结果越大。最后N
和M
取最大值,然后取百分号乘上全力刷脏页的速度,并以这个速度进行flush
-
脏页的比例通过
Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total
计算的得到
select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty';select VARIABLE_VALUE into @b from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total';select @a/@b;
-
刷新脏页的时候,如果旁边也是脏页,就会继续刷新旁边的页
innodb_flush_neighbors = 1
就是连坐刷新innodb_flush_neighbors = 0
表示只刷自己
-
机械硬盘的话,连坐刷新有意义;但如果是SSD,建议设置为0,8.0版本后默认为0
-
总结
redo log
用于恢复主机故障时没有更新的数据;binlog
用于备份- 恢复数据时:
- 如果
redo log
的状态为commit
,表明binlog
也是成功的,就可以直接恢复数据; - 如果
redo log
状态是prepare
,则需要查询binlog
对应的事务是否成功,来决定是需要回滚还是执行
- 如果
- 因此,
redo log
保证了crash-safe
的能力,innodb_flush_log_at_trx_commit
参数表示redo log
持久化方式innodb_flush_log_at_trx_commit = 1
表示每次事务的redo log
都持久化到磁盘上,可以保证每次异常重启以后数据都不会丢失;sync_binlog = 1
表示每次事务的binlog
都会持久化到磁盘- 所以这两个参数都建议设置为1
- 两个参数有
0 1 2
三个值可选,- 0表示每秒将
log buffer
同步到os buffer
,并刷新到磁盘; - 1表示每个事务都刷新到磁盘;
- 2表示每个事务提交都将
log buffer
同步到os buffer
中,但是每秒刷新到磁盘
- 0表示每秒将
45讲对于日志的叙述
redo log
在prepare
状态,写入binlog
之前,崩溃了,恢复后因为redo log
没有提交,binlog
没有写,事务回滚,并且由于没有写binlog
,所以从库无影响redo log
没有commit
, 写入binlog
之后,- 如果
redo log
事务完整,也就是已经有了commit
标识(但是还没有commit
),就直接提交 - 如果
redo log
只有prepare
,判断binlog
是否存在并完整- 如果
binlog
存在并完整,则提交事务 - 否则回滚事务
binlog
判断完整性:statement
格式binlog
的最后会有COMMIT
row
格式binlog
最后会有XID EVENT
- 5.6版本之后引入了
binlog-checksum
验证binlog
完整性
- 如果
所以只要
binlog
完整,就可以通过redo log
恢复数据,因为binlog
完整的话,从库就会拿去用,就需要主库也有同样的数据- 如果
- 如果没有两阶段提交,那么
redo log
写好了,就无法回滚,此时如果binlog
写入失败,也无法回滚,导致不一致 - 只有
binlog
无法支持崩溃恢复,因为binlog
是WAL
的,如果先写了,但是事务没有commit
,此时崩溃,就会不一致 - 如果只有
redo log
,可以崩溃恢复,也不需要两阶段提交。但是binlog
可以归档,而且redo log
循环写,历史无法保留;并且高可用,依赖监听binlog
redo log
可以直接设置为4个文件,每个1GB。innodb_log_file_size
指定每个redo log
大小,默认48M
innodb_log_files_in_group
指定 个数,默认2
redo log
没有记录页面的完整数据,所以不能更新磁盘,数据最终写入都依赖内存数据页的脏页
有两张表,like
和friend
,任何一方单方面关注的时候,只会添加在like
表中,如果互相关注,需要在friend
表中添加。如果两个人同时点击关注对方,就有可能出现两个单方面关注,friend
表为空。
- like
表添加一个字段relationship
- 为1表示userid
关注likeid
- 为2表示likeid
关注userid
- 为3表示互相关注
- 设定userid < likeid
- 插入数据比较两个用户id(A B)大小
- 如果
A < B
1
2
3
4
5
6
7
8
9
10begin; /*启动事务*/
insert into `like`(user_id, liker_id, relation_ship) values(A, B, 1) on duplicate key update relation_ship=relation_ship | 1;
select relation_ship from `like` where user_id=A and liker_id=B;
/*
代码中判断返回的 relation_ship,
如果是1,事务结束,执行 commit
如果是3,则执行下面这两个语句:
*/
insert ignore into friend(friend_1_id, friend_2_id) values(A,B);
commit; - 如果
A > B
1
2
3
4
5
6
7
8
9
10begin; /*启动事务*/
insert into `like`(user_id, liker_id, relation_ship) values(B, A, 2) on duplicate key update relation_ship=relation_ship | 2;
select relation_ship from `like` where user_id=B and liker_id=A;
/*
代码中判断返回的 relation_ship,
如果是2,事务结束,执行 commit
如果是3,则执行下面这两个语句:
*/
insert ignore into friend(friend_1_id, friend_2_id) values(B,A);
commit; insert on duplicate
强制占住一行,select relationship
在行锁保护下进行|
按位或 和insert ignore
避免重复调用
- 如果
主从复制
- 主从复制就是将主数据库的
DDL, DML
语句通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(重做),从而使主从库数据同步 MySQL
支持一台主库同时向多台从库进行复制,从库也可以作为其他服务器的主库,实现链状复制
优点
- 主库出现问题,快速切换从库提供服务(高可用)
- 实现读写分离,降低主库访问压力
- 在从库备份,避免备份期间影响主库服务
原理
主数据库执行DML, DDL
语句后,会写入binlog
日志中。此时从数据库的IOthread
会读取主数据库的binlog
,写入自己的中继日志Relay log
中,然后从数据库的SQLthread
会读取Relay log
,并重做其中的DML, DDL
语句,实现主从复制
主从复制分三步:
- master在事务提交时,把数据变更记录在二进制日志文件
binlog
中 - 从库读取主库的
binlog
,写入到从库的中继日志Relay Log
中 - slave重做中继日志的事件,改变自己的数据
主库配置
- 修改
/etc/my.cnf
1
2
3
4
5
6
7
8
9# MySQL服务ID,保证集群环境中唯一,取值范围为(1 ~ 2^32-1),默认为1
server-id=1
# 是否只读,1表示只读,0表示读写
read-only=0
# 忽略的数据,指不需要同步的数据库
# binlog-ignore-db=mysql
# 指定同步的数据库
# binlog-do-db=db01
# 不指定这两个设置,则表示所有数据库都需要同步 - 重启
MySQL
服务:systemctl restart mysqld
- 登录
MySQL
,创建远程连接账号,授予主从复制权限1
2
3
4-- 创建用户,并设置密码,可以在任意主机连接mysql服务
create user '用户名'@'%' identified with mysql_native_password by '密码';
-- 为用户分配主从复制权限
grant replication slave on *.* to '用户名'@'%'; - 查看二进制日志坐标:
show master status;
字段含义:
file: 从哪个日志文件开始推送
position: 从日志文件的哪个位置开始推送日志
binlog_ignore_db: 指定不需要同步的数据库
从库配置
- 修改
/etc/my.cnf
1
2
3
4# MySQL服务ID,保证集群环境中唯一,取值范围为(1 ~ 2^32-1),和主库不一样即可
server-id=2
# 是否只读,1表示只读,0表示读写
read-only=1- 从库的
read-only=1
表示从库只需要查询即可 - 但是这个只读只是针对普通用户权限,对超级管理员仍然是可写
- 如果需要禁止超级管理员的写权限,则需要在
/etc/my.cnf
中再指定一个super-read-only=1
参数
- 从库的
- 重启
MySQL
服务:systemctl restart mysqld
- 登录从库的
MySQL
,设置主库配置1
2
3change replication source to source_host='主机ip', source_user='用户名', source_password='密码', source_log_file='同步的日志文件', source_log_pos=日志文件开始同步的位置; --8.0.23
change master to master master_host='主机ip', master_user='用户名', master_password='密码', master_log_file='同步的日志文件', master_log_pos=日志文件开始同步的位置; --8.0.23之前 - 开启同步
1
2start replica; --8.0.22
start slave; --8.0.22之前 - 查看主从同步状态
1
2show replica status; --8.0.22
show slave status; --8.0.22之前- 文件显示比较乱,可以在后面加上\G,就会按行显示:
show replica status\G;
- 主要看两行:
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
- 只要这两行开启了,主从复制就没有问题
- 文件显示比较乱,可以在后面加上\G,就会按行显示:
分库分表
- IO瓶颈: 热点数据太多,数据库缓存不足,产生大量磁盘I/O,请求数据太多,带宽不够
- CPU瓶颈: 排序、分组、连接查询、聚合统计等
SQL
会消耗大量的CPU资源,请求数太多,CPU出现瓶颈
-
拆分维度分为垂直拆分和水平拆分,拆分粒度方面分为分库和分表
- 垂直分库: 以表为依据,将不同的业务拆分到不同数据库中
特点: 每个库表结构不一样、库中数据不同,所有库的并集才是全部的数据 - 垂直分表: 以字段为依据,根据字段属性将不同字段拆分到不同的表中
特点: 每个表结构不一样、每个表的数据不同,一般通过一列(主键/外键)关联、所有表的并集是全部数据 - 水平分库: 以字段为依据,将一个库的数据拆分到多个库中
特点: 每个库表结构一样、每个库数据不同、所有库的并集是全部的数据 - 水平分表: 以字段为依据,将一个表的数据拆分到多个表中
特点: 每个表的表结构一样、每个表的数据不同、所有表的并集是全部的数据
shardingJDBC
: 基于AOP
,在应用程序对本地执行的SQL
进行拦截,解析,改写,路由处理。只支持Java
,性能好MyCat
: 数据库分库分表的中间件,不用调整代码即可实现分库分表,支持多种语言- 使用
mysql
一样的链接命令可以登录mycat
,只有端口不一样,mycat
端口是8066
- 垂直分库: 以表为依据,将不同的业务拆分到不同数据库中
MyCat
结构
-
所有数据都存储在物理结构内
-
分片规则决定了某个数据应该插入到哪一个库的表中,一般水平分表的时候需要指定,默认是
auto-sharding-long
,其中规则为:根据id选择对应的数据库,但是如果超过了1500万,则直接报错
MyCat
配置
schema.xml
包含三组标签:schema, datanode, datahost
schema
核心属性:name
: 指定自定义的逻辑库库名checkSQLschema
: 在SQL
语句操作时指定了数据库名称,执行时是否自动去除。如果为true
,表示第一次指定一下,后面就可以不用再指定数据库名称了。如果为false
,则表明需要使用use 数据库名;
进行指定sqlMaxLimit
: 如果没有指定limit
进行查询,列表查询模式返回查询了多少条记录
schema.table
核心属性:name
: 定义逻辑表表名,在该逻辑库下唯一dataNode
: 定义逻辑表所属的dataNode
,需要与dataNode
标签中的name
对应,多个dataNode
使用逗号分割rule
: 分片规则名字,在rule.xml
中定义primaryKey
: 逻辑表对应真实表的主键type
: 逻辑表的类型,只有全局表和普通表,默认为普通表。
dataNode
核心属性:name
: 定义数据节点名称dataHost
: 数据库实例主机名称,引用自dataHost
标签中的name
属性database
: 定义分片需要的数据库
dataHost
核心属性:name
: 唯一标识,供上层标签使用maxCon/minCon
: 最大/最小连接数balance
: 负载均衡策略,取值0,1,2,3writeType
: 写操作分发方式,0表示写操作转发到第一个writeHost
,第一个挂了就转发到第二个,1表示写操作随机分发到配置的writeHost
dbDriver
: 数据库驱动,支持native, jdbc
ruler.xml
包含两类标签:tableRule、function
server.xml
主要包含两个标签:system、user
MyCat
分片
- 字典表类型的表可以设置成全局表
分片规则
- 范围分片
auto-sharding-long
- 取模
mod-long
- 枚举
sharding-by-intfile-enumstatus
- 一致性哈希
sharding-by-murmur
: 计算指定的哈希值,根据哈希值计算落在哪一个逻辑表中。相同的哈希因子计算值总是划分到相同的分区表中,不会因为分区节点的增加而改变原来数据的分区位置 - 应用指定
sharding-by-substring
: 运行阶段由应用自主决定路由到哪个分片,并直接根据字符子串(必须是数字)计算分片号 - 固定分片哈希算法
sharding-by-long-hash
: 类似于十进制取模,但是为二进制操作。比如取低10位和1111111111进行位&
运算。分片长度最大默认为1024 - 字符串哈希解析
sharding-by-stringhash
: 截取子字符串,进行哈希算法,与1023
进行位&
运算得出分片 - 按天分片
sharding-by-date
- 按月份分片
sharding-by-month
固定分片哈希算法 VS 取模
- 取模运算连续的值会分配到不同的分片,固定分片哈希算法中连续的值会分配到相同的分片,降低事务处理难度
- 固定分片哈希算法既可以均匀分配,也可以不均匀分配
- 分片字段必须是数字
MyCat
管理
- 默认有两个端口,分别是8066和9066
- 8066是数据访问端口,进行
DML
以及DDL
操作 - 9066是数据库管理端口,即
MyCat
的服务管理控制功能,用于管理MyCat
的整个集群状态
- 8066是数据访问端口,进行
命令 | 含义 |
---|---|
show @@help | 查看管理工具帮助文档 |
show @@version | 查看mycat的版本 |
reload @@config | 重新加载mycat的配置文件 |
show @@datasource | 查看mycat的数据源信息 |
show @@datanode | 查看mycat现有的分片节点信息 |
show @@threadpool | 查看mycat线程池信息 |
show @@sql | 查看执行的sql |
show @@sql.sum | 查看执行的sql统计 |
读写分离
-
主库写,从库读,配置
MyCat
的writeHost
以及readHost
-
读写分离需要修改
balance=1 or 3
,问题在于主库宕机了,就只能进行查询了(不满足高可用性)参数值 含义 0 不开启读写分离机制,所有读操作发送到当前可用的writeHost上 1 全部的readHost与备用的writeHost都参与select语句的负载均衡(主要针对双主双从) 2 所有的读写操作都随机在writeHost,readHost上分发 (没有读写分离) 3 所有的读请求随机分发到writeHost对应的readHost上执行,writeHost不负担读压力 -
为了避免主数据库挂了以后服务宕机的问题,可以使用双主双从的设置。