- 2022 年数据库发展总结
- 01. 组提交
- 02. innodb_log_buffer_size
- 03. mysql表空间回收
- MySQL GTID
- MySQL limit优化
- secure-file-priv特性
- Referece
MySQL 8.0 Study 003 Tips
学习系列
- 2022 年数据库发展总结
- 组提交
- innodb_log_buffer_size
- mysql表空间回收
- MySQL GTID是什么
- secure-file-priv特性
2022 年数据库发展总结
转载:
Q1. 中国和海外数据库的差距还有多远?
Q2. 未来是 OLTP 还是 OLAP ?
Q3. 从国际上来看 HTAP 是不是未来?
Q4. MySQL DBA 和大数据从业者会有什么改变?
Q5. 现在还是不是数据方向的创业好的时机?
01. 组提交
图解MySQL 原理解析 MySQL组提交(group commit)
WAL机制 (Write Ahead Log)定义:
WAL指的是对数据文件进行修改前,必须将修改先记录日志。MySQL为了保证ACID中的一致性和持久性,使用了WAL。
Redo log的作用:
Redo log就是一种WAL的应用。当数据库忽然掉电,再重新启动时,MySQL可以通过Redo log还原数据。也就是说,每次事务提交时,不用同步刷新磁盘数据文件,只需要同步刷新Redo log就足够了。相比写数据文件时的随机IO,写Redo log时的顺序IO能够提高事务提交速度。
组提交的作用:
在没有开启binlog时
Redo log的刷盘操作将会是最终影响MySQL TPS的瓶颈所在。为了缓解这一问题,MySQL使用了组提交,将多个刷盘操作合并成一个,如果说10个事务依次排队刷盘的时间成本是10,那么将这10个事务一次性一起刷盘的时间成本则近似于1。
当开启binlog时
为了保证Redo log和binlog的数据一致性,MySQL使用了二阶段提交,由binlog作为事务的协调者。而 引入二阶段提交 使得binlog又成为了性能瓶颈,先前的Redo log 组提交 也成了摆设。为了再次缓解这一问题,MySQL增加了binlog的组提交,目的同样是将binlog的多个刷盘操作合并成一个,结合Redo log本身已经实现的 组提交,分为三个阶段(Flush 阶段、Sync 阶段、Commit 阶段)完成binlog 组提交,最大化每次刷盘的收益,弱化磁盘瓶颈,提高性能。
02. innodb_log_buffer_size
MySQL 官方文档说 innodb_log_buffer_size 是 redo log 的写缓存,设置大一点能减少写操作。
innodb_log_buffer_size 是 redo log 的写缓存,如果 buffer 不够大,就会发生多次 IO write,将缓存中的数据刷到磁盘;
innodb_log_buffer_size 越大,IO 次数越少。
一般存储上,虽然 IO write 次数少了,但 write 的数据量是差不多的,所以整体的 IO 延迟总体差不多;
在好点的存储上,如果数据量差不多, write 次数越少,延迟会越低。
这也就是为什么大家在调整这个参数后, 经常会感知不到性能的提升。
03. mysql表空间回收
delete导致的表空洞
delete 命令其实只是把记录的位置,或者数据页标记为了“可复用”,但磁盘文件的大小是不会变的。也就是说,通过 delete 命令是不能回收表空间的。这些可以复用,而没有被使用的空间,看起来就像是“空洞”。
insert导致的表空洞
如果数据是按照索引递增顺序插入的,那么索引是紧凑的。但如果数据是随机插入的,就可能造成索引的数据页分裂。
怎么清理空洞
- 重建表
试想一下,如果你现在有一个表 A,需要做空间收缩,为了把表中存在的空洞去掉,你可以怎么做呢?你可以新建一个与表 A 结构相同的表 B,然后按照主键ID 递增的顺序,把数据一行一行地从表 A 里读出来再插入到表 B 中。由于表 B 是新建的表,所以表 A 主键索引上的空洞,在表 B 中就都不存在了。显然地,表 B 的主键索引更紧凑,数据页的利用率也更高。如果我们把表 B 作为临时表,数据从表 A 导入表 B 的操作完成后,用表 B 替换 A,从效果上看,就起到了收缩表 A 空间的作用。
- alter table 方式
alter table A engine=InnoDB 命令来重建表。在 MySQL 5.5 版本之前,这个命令的执行流程跟我们前面描述的差不多,区别只是这个临时表 B 不需要你自己创建,MySQL 会自动完成转存数据、交换表名、删除旧表的操作。
注:但是以上重建表的方式,在整个 DDL 过程中,表 A 中不能有更新。也就是说,这个 DDL 不是 Online 的。
- Online DDL诞生
从5.6开始引入了online DDL,也就是说alter table A engine=InnoDB的时候,允许对表 A 做增删改操作。但是DDL会有短暂的MDL写锁,这个会很快转换为MDL读锁,因此,认为对于一个大表来说,Online DDL 最耗时的过程就是拷贝数据到临时表的过程,这个步骤的执行期间可以接受增删改操作。所以,相对于整个 DDL 过程来说,锁的时间非常短。对业务来说,就可以认为是 Online 的。
注意:对于很大的表来说,这个操作是很消耗 IO 和 CPU 资源的。因此,如果是线上服务,你要很小心地控制操作时间。如果想要比较安全的操作的话,我推荐你使用 GitHub 开源的 gh-ost 来做。
解决方案
从 MySQL 的同步原理来看,主要是 DDL 这个单独的操作会花费太久的时间,导致从库也会被卡主。那么解决这个问题的办法就很容易想到:“拆解” DDL 的操作,把一个大操作(大事务同理)拆分成多个小操作,减少单次操作的时间。
“拆解” DDL 操作一般会用到 MySQL Online DDL 的工具,比如 pt-osc,facebook-osc,oak-online-alter-table,gh-ost 等。这些工具的思路都比较类似,创建一个源表的镜像表,先执行完表结构变更,再把源表的全量数据和增量数据都同步过去,因此可以减少单个 DDL 操作引发的同步延迟,但是不能完全避免,因为大批量 insert 数据本身也是可能会产生一些延迟的,只是这部分延迟的时间会比大多数 DDL 要小很多。
MySQL 8.0 Online DDL和pt-osc、gh-ost深度对比分析
MySQL GTID
GTID作用 主从环境中主库的dump线程可以直接通过GTID定位到需要发送的binary log的位置,而不需要指定binary log的文件名和位置,因而切换极为方便。
GTID实际上是由UUID+TID (即transactionId)组成的。其中UUID(即server_uuid) 产生于auto.conf文件(cat /data/mysql/data/auto.cnf),是一个MySQL实例的唯一标识。TID代表了该实例上已经提交的事务数量,并且随着事务提交单调递增,所以GTID能够保证每个MySQL实例事务的执行(不会重复执行同一个事务,并且会补全没有执行的事务)。GTID在一组复制中,全局唯一。
对于2台主以上的结构优势异常明显,可以在数据不丢失的情况下切换新主。
通过GTID复制,这些在主从成立之前的操作也会被复制到从服务器上,引起复制失败。也就是说通过GTID复制都是从最先开始的事务日志开始,即使这些操作在复制之前执行。比如在server1上执行一些drop、delete的清理操作,接着在server2上执行change的操作,会使得server2也进行server1的清理操作。
直接使用CHANGE MASTER TO MASTER_HOST=’xxx’, MASTER_AUTO_POSITION命令就可以直接完成failover的工作。
开启GTID
MySQL 5.6 版本,在my.cnf文件中添加:
gtid_mode=on (必选) #开启gtid功能
enforce-gtid-consistency=1 (必选) #也可以将1写为on
log_bin=log-bin=mysql-bin (必选) #开启binlog二进制日志功能
log-slave-updates=1 (必选) #也可以将1写为on
MySQL 5.7或更高版本,在my.cnf文件中添加:
gtid_mode=on (必选)
enforce-gtid-consistency=1 (必选)
log_bin=mysql-bin (可选) #高可用切换,最好开启该功能
log-slave-updates=1 (可选) #高可用切换,最好打开该功能
GTID的缺点
- 不支持非事务引擎;
- 不支持create table ... select 语句复制(主库直接报错);(原理: 会生成两个sql, 一个是DDL创建表SQL, 一个是insert into 插入数据的sql; 由于DDL会导致自动提交, 所以这个sql至少需要两个GTID, 但是GTID模式下, 只能给这个sql生成一个GTID)
- 不允许一个SQL同时更新一个事务引擎表和非事务引擎表;
- 在一个复制组中,必须要求统一开启GTID或者是关闭GTID;
- 开启GTID需要重启 (mysql5.7除外);
- 开启GTID后,就不再使用原来的传统复制方式;
- 对于create temporary table 和 drop temporary table语句不支持;
- 不支持sql_slave_skip_counter;
GTID跳过事务的方法 开启GTID以后,无法使用sql_slave_skip_counter跳过事务,因为主库会把从库缺失的GTID,发送给从库,所以skip是没有用的。
为了提前发现问题,在gtid模式下,直接禁止使用set global sql_slave_skip_counter =x。
正确的做法: 通过set gtid_next= ‘aaaa’(’aaaa’为待跳过的事务),然后执行BIGIN; 接着COMMIT产生一个空事务,占据这个GTID,再START SLAVE,会发现下一条事务的GTID已经执行过,就会跳过这个事务了。如果一个GTID已经执行过,再遇到重复的GTID,从库会直接跳过,可看作GTID执行的幂等性。
因为是通过GTID来进行复制的,也需要跳过这个事务从而继续复制,这个事务可以到主上的binlog里面查看:因为不知道找哪个GTID上出错,所以也不知道如何跳过哪个GTID
1、show slave status里的信息里可以找到在执行Master里的POS:151
2、通过mysqlbinlog找到了GTID:
3、stop slave;
4、set session gtid_next='4e659069-3cd8-11e5-9a49-001c4270714e:1'
5、begin; commit;
6、SET SESSION GTID_NEXT = AUTOMATIC; #把gtid_next设置回来
7、start slave; #开启复制
1)对于跳过一个错误,找到无法执行事务的编号,比如是2a09ee6e-645d-11e7-a96c-000c2953a1cb:1-10
mysql> stop slave;
mysql> set gtid_next='2a09ee6e-645d-11e7-a96c-000c2953a1cb:1-10';
mysql> begin;
mysql> commit;
mysql> set gtid_next='AUTOMATIC';
mysql> start slave;
2)上面方法只能跳过一个事务,那么对于一批如何跳过?
在主库执行"show master status",看主库执行到了哪里,比如:2a09ee6e-645d-11e7-a96c-000c2953a1cb:1-33,那么操作如下:
mysql> stop slave;
mysql> reset master;
mysql> set global gtid_purged='2a09ee6e-645d-11e7-a96c-000c2953a1cb:1-33';
mysql> start slave;
如何升级成 GTID replication
先介绍几个重要GTID_MODE的参数:
GTID_MODE = OFF
不产生Normal_GTID,只接受来自master的ANONYMOUS_GTID
GTID_MODE = OFF_PERMISSIVE
不产生Normal_GTID,可以接受来自master的ANONYMOUS_GTID & Normal_GTID
GTID_MODE = ON_PERMISSIVE
产生Normal_GTID,可以接受来自master的ANONYMOUS_GTID & Normal_GTID
GTID_MODE = ON
产生Normal_GTID,只接受来自master的Normal_GTID
归纳总结:
1)当master产生Normal_GTID的时候,如果slave的gtid_mode(OFF)不能接受Normal_GTID,那么就会报错
2)当master产生ANONYMOUS_GTID的时候,如果slave的gtid_mode(ON)不能接受ANONYMOUS_GTID,那么就会报错
3)设置auto_position的条件: 当master的gtid_mode=ON时,slave可以为OFF_PERMISSIVE,ON_PERMISSIVE,ON。
除此之外,都不能设置auto_position = on
============================================
下面开始说下如何online 升级为GTID模式?
step 1: 每台server执行
检查错误日志,直到没有错误出现,才能进行下一步
mysql> SET @@GLOBAL.ENFORCE_GTID_CONSISTENCY = WARN;
step 2: 每台server执行
mysql> SET @@GLOBAL.ENFORCE_GTID_CONSISTENCY = ON;
step 3: 每台server执行
不用关心一组复制集群的server的执行顺序,只需要保证每个Server都执行了,才能进行下一步
mysql> SET @@GLOBAL.GTID_MODE = OFF_PERMISSIVE;
step 4: 每台server执行
不用关心一组复制集群的server的执行顺序,只需要保证每个Server都执行了,才能进行下一步
mysql> SET @@GLOBAL.GTID_MODE = ON_PERMISSIVE;
step 5: 在每台server上执行,如果ONGOING_ANONYMOUS_TRANSACTION_COUNT=0就可以
不需要一直为0,只要出现过0一次,就ok
mysql> SHOW STATUS LIKE 'ONGOING_ANONYMOUS_TRANSACTION_COUNT';
step 6: 确保所有anonymous事务传递到slave上了
#master上执行
mysql> SHOW MASTER STATUS;
#每个slave上执行
mysql> SELECT MASTER_POS_WAIT(file, position);
或者,等一段时间,只要不是大的延迟,一般都没问题
step 7: 每台Server上执行
mysql> SET @@GLOBAL.GTID_MODE = ON;
step 8: 在每台server上将my.cnf中添加好gtid配置
gtid_mode=on
enforce-gtid-consistency=1
log_bin=mysql-bin
log-slave-updates=1
step 9: 在从机上通过change master语句进行复制
mysql> STOP SLAVE;
mysql> CHANGE MASTER TO MASTER_AUTO_POSITION = 1;
mysql> START SLAVE;
MySQL limit优化
在MySQL中Limit有两种语法:
limit offset, rows
limit rows
其中offset表示偏移量,rows表示要返回的记录条数。比如我们要查出第1000到第1010行的记录,就会写成 limit 999, 10 。 一般我们在实际开发中做分页查询的时候,rows不会太大,因为rows表示每页要显示的数量(你总不可能每页要显示个几万条吧)。 而随着页数的加深,offset就会变大,查询效率随之就变慢了。 为什么当offset很大时,查询效率会变慢呢? 比如当我们用 limit 1000000, 10 的时候,MySQL会先扫描满足条件的1000010行,扔掉前面的1000000行,返回后面的10行。所以offset越大的时候,扫描的行就越多,效率也就越慢了。
Limit Offset下推
https://help.aliyun.com/document_detail/437535.html?spm=a2c4g.11186623.0.0.cebf49b1u8jzR7
secure-file-priv特性
secure-file-priv参数是用来限制LOAD DATA, SELECT … OUTFILE, and LOAD_FILE()传到哪个指定目录的。
secure_file_priv的值为null ,表示限制mysqld 不允许导入|导出
当secure_file_priv的值为/tmp/ ,表示限制mysqld 的导入|导出只能发生在/tmp/目录下
当secure_file_priv的值没有具体值时,表示不对mysqld 的导入|导出做限制
如何查看secure-file-priv参数的值:
show global variables like '%secure%';
MYSQL新特性secure_file_priv对读写文件的影响: 此开关默认为NULL,即不允许导入导出。
解决问题: windows下:
修改my.ini 在[mysqld]内加入
secure_file_priv=
linux下:
修改my.cnf 在[mysqld]内加入
secure_file_priv=
MYSQL新特性secure_file_priv对读写文件的影响
然后重启mysql,再查询secure_file_priv
类似错误:
mysql [localhost:8032] {msandbox} (test) > select concat('insert into t1(id,c1,c2) values(',id,',','\'',c1,'\'',',','\'',c2,'\'',')') into outfile '/tmp/t1.txt' from t1;
ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
mysql [localhost:8032] {msandbox} (test) > show variables like 'secure_file%';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| secure_file_priv | NULL |
+------------------+-------+
1 row in set (0.00 sec)
mysql [localhost:8032] {msandbox} (test) >
my.cnf配置之后重启
mysql [localhost:8032] {msandbox} (test) > show variables like 'secure%';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| secure_file_priv | |
+------------------+-------+
1 row in set (0.00 sec)
mysql [localhost:8032] {msandbox} (test) > select concat('insert into t1(id,c1,c2) values(',id,',','\'',c1,'\'',',','\'',c2,'\'',')') into outfile '/tmp/t1.txt' from t1;
Query OK, 2 rows affected (0.00 sec)
mysql [localhost:8032] {msandbox} (test) >
Referece
参考:
Have a good work&life! 2023/01 via LinHong