MySQL实战9-45

1、普通索引和唯一索引,应该怎么选择?查询过程:普通索引来查找到满足条件的第一个记录  后,需要查找下一个记录,直到碰到第一个不满足条件的记录。对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。性能差距微乎其微。InnoDB 的数据是按数据页为单位来读写的。也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。在 InnoDB 中,每个数据页的大小默认是 16KB。普通索引正好在最后一条的几率很低

change buffer:当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。change buffer 用的是 buffer pool 里的内存。唯一索引要判断唯一性,需要读取数据进内存,所以用不到change buffer。将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。普通索引使用change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。所以读多写少的数据用change buffer会频繁触发 merge 过程。起了副作用。

merge 流程:从磁盘读入数据页到内存(老版本的数据页);从 change buffer 里找出这个数据页的 change buffer 记录 (可能有多个),依次应用,得到新版数据页;写 redo log。这个 redo log 包含了数据的变更和 change buffer 的变更。到这里 merge 过程就结束了。这时候,数据页和内存中 change buffer 对应的磁盘位置都还没有修改,属于脏页

redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。

PRIMARY KEY(主键索引)、UNIQUE(唯一索引,允许有空值。如果是组合索引,则列值的组合必须唯一)、INDEX(普通索引)、FULLTEXT(全文索引)

查看索引
show index from tb_wz_all;

2、 MySQL选错索引问题

选错索引肯定是在判断扫描行数的时候出问题了。

MySQL 在执行语句之前,不能精确地知道满足这个条件的记录有多少条,只能根据统计信息来估算记录数。这个统计信息就是索引的“区分度”。显然,一个索引上不同的值越多,这个索引的区分度就越好。而一个索引上不同的值的个数,我们称之为“基数”(cardinality)。也就是说,这个基数越大,索引的区分度越好。基数通过采样统计估算

1、使用 force index(a) 来让优化器强制使用索引 a;2、第二种方法就是,我们可以考虑修改语句,引导 MySQL 使用我们期望的索引。3、第三种方法是,在有些场景下,我们可以新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引。4、analyze table t 命令修正不对的统计信息。

set long_query_time=0;将慢查询日志的阈值设置为 0。

3、怎么给字符串字段加索引?

直接创建完整索引,这样可能比较占用空间;

创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引;

倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题;

创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描。

4、SQL 语句为什么某一次变“慢”了

可能就是在刷脏页(flush):当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。不论是脏页还是干净页,都在内存中。

1、redo log 写满了。这时候系统会停止所有更新操作,把 checkpoint 往前推进,redo log 留出空间可以继续写,推进的部分对应的所有脏页都 flush 到磁盘上。

2、查询语句在内存不够,需要内存的时候可能要求淘汰一个脏页。

flush的时候,整个系统就不能再接受更新了,所以要关注脏页比例,不要让它经常接近 75%。。相邻的数据页也还是脏页的话,也会被放到一起刷,硬盘时代可以减少随机IO。MySQL 8.0 中,innodb_flush_neighbors 参数的默认值已经是 0 了,表示不刷邻居,1则会刷邻居

5、表数据删掉一半,表文件大小不变?

drop table 命令回收表空间,delete 命令是不能回收表空间的,在数据文件当中只是标记为删除,如果需要做空间收缩,为了把表中存在的空洞去掉:重建表:

 MySQL 5.6 版本开始引入的 Online DDL,重建表的流程:建立一个临时文件,扫描表 A 主键的所有数据页;用数据页中表 A 的记录生成 B+ 树,存储到临时文件中;生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件(row log)中;临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件,对应的就是图中 state3 的状态;用临时文件替换表 A 的数据文件。语句: alter table t engine=InnoDB

Online DDL 最耗时的过程就是拷贝数据到临时表的过程,这个步骤的执行期间可以接受增删改操作。重建方法都会扫描原表数据和构建临时文件。对于很大的表来说,这个操作是很消耗 IO 和 CPU 资源的。因此,如果是线上服务,你要很小心地控制操作时间。如果想要比较安全的操作的话,我推荐你使用 GitHub 开源的 gh-ost 来做。


DDL 过程如果是 Online 的,就一定是 inplace 的;反过来未必,也就是说 inplace 的 DDL,有可能不是 Online 的。截止到 MySQL 8.0,添加全文索引(FULLTEXT index)和空间索引 (SPATIAL index) 就属于这种情况。

6、count(*)过程

MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高;而 InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数(MVCC机制的限制:每一行记录都要判断自己是否对这个会话可见,因此对于 count(*) 请求来说,InnoDB 只好把数据一行一行地读出依次判断,可见的行才能够用于计算“基于这个查询”的表的总行数。优化:InnoDB 是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值,普通索引树比主键索引树小很多,优化器会找到最小的那棵树来遍历)

count(*)、count(主键 id)、count(字段) 和 count(1) 等不同用法的性能,有哪些差别:

count(*)、count(主键 id) 和 count(1) 都表示返回满足条件的结果集的总行数;而 count(字段),则表示返回满足条件的数据行里面,参数“字段”不为 NULL 的总个数。

按照效率排序的话,count(字段)<count(主键 id)<count(1)≈count(*),所以我建议你,尽量使用 count(*)。

计数放在 Redis 里面,不能够保证计数和 MySQL 表里的数据精确一致的原因,是这两个不同的存储构成的系统,不支持分布式事务,无法拿到精确一致的视图。而把计数值也放在 MySQL 中,就解决了一致性视图的问题。

 

一个事务的 binlog 是有完整格式的:statement 格式的 binlog,最后会有 COMMIT;row 格式的 binlog,最后会有一个 XID event。redo log 和 binlog它们有一个共同的数据字段,叫 XID,用此字段关联起来。对于 InnoDB 引擎来说,如果 redo log 提交完成了,事务就不能回滚(如果这还允许回滚,就可能覆盖掉别的事务的更新)。而如果 redo log 直接提交,然后 binlog 写入的时候失败,InnoDB 又回滚不了,数据和 binlog 日志又不一致了,所以两段提交。

7、order by 是怎么工作的?

全字段排序:sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。外部排序一般使用归并排序算法。MySQL 将需要排序的数据分成 多 份,每一份单独排序后存在这些临时文件中。然后把这 多个有序文件再合并成一个有序的大文件。

如果 MySQL 认为排序的单行长度太大会怎么做呢?要换一个算法:rowid 排序。MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。回表多造成磁盘读,因此不会被优先选择。

覆盖索引是指,索引上的信息足够满足查询请求,不需要再回到主键索引上去取数据。

8、如何正确地显示随机消息?


order by rand() 使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法。tmp_table_size 这个配置限制了内存临时表的大小,默认值是 16M。如果临时表大小超过了 tmp_table_size,那么内存临时表就会转成磁盘临时表,order by rand()  limit 3 没有使用临时文件的算法,也就是归并排序算法,而是采用了优先队列排序算法:(取几行作为一个堆,在所有行中对堆内的数据进行冒泡,最后返回堆内数据)。

为了得到严格随机的结果,你可以用下面这个流程:取得整个表的行数,并记为 C。取得 Y = floor(C * rand())。 floor 函数在这里的作用,就是取整数部分。再用 limit Y,1 取得一行。

9、SQL语句逻辑相同,性能却差异巨大?

1、对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。:select * from tradelog where id + 1 = 10000 这个 SQL 语句就不能用id索引快速定位

2、数据类型转换,就需要走全索引扫描:类型转换相当于函数操作

3、字符编码转换:一个是 utf8,一个是 utf8mb4,做表连接查询的时候用不上关联字段的索引。字符集转换也相当于函数操作

SQL 语句 explain 一下查看索引适用情况。

10、查一行的语句,也执行这么慢?

MySQL 数据库本身就有很大的压力,导致数据库服务器 CPU 占用率很高或 ioutil(IO 利用率)很高,这种情况下所有语句的执行都有可能变慢

1、查询长时间不返回:执行一下 show processlist 命令,看看当前语句处于什么状态。

1)等 MDL 锁:show processlist 命令查看 显示Waiting for table metadata lock,这个状态表示现在有一个线程正在表 t 上请求或者持有 MDL 写锁,把 select 语句堵住了。

2)等 flush

3)等行锁:加读锁,如果这时候已经有一个事务在这行记录上持有一个写锁,我们的 select 语句就会被堵住。怎么查出是谁占着这个写锁。如果你用的是 MySQL 5.7 版本,可以通过 sys.innodb_lock_waits 表查到。
mysql> select * from t sys.innodb_lock_waits where locked_table='`test`.`t`'\G

4)另一个update事务更新太多次,undolog太多,MVCC机制,查询要找很多版本之前

11、幻读是什么

脏读(读取未提交数据)、不可重复读(前后多次读取,数据内容不一致)、幻读(前后多次读取,数据总量不一致)

即使把所有的记录都加上锁,还是阻止不了新插入的记录,因为新的事务插入数据的话,之前的事务版本修改的时候也会先读后改,得到了新的事务的插入提交,造成了幻读。

解决幻读:不仅将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间。间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。

另一种解决幻读办法:把隔离级别设置为读提交的话,就没有间隙锁了。但同时,你要解决可能出现的数据和日志不一致问题,需要把 binlog 格式设置为 row。

加锁规则,包含了两个“原则”、两个“优化”和一个“bug”。

原则 1:加锁的基本单位是 next-key lock。希望你还记得,next-key lock 是前开后闭区间。

原则 2:查找过程中访问到的对象才会加锁。

优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。

优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。

一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。(近20年的"bug",在2019.10.14发布的MySQL 8.0.18版本中被修复了)

慢查询性能问题:

索引没有设计好;SQL 语句没写好;MySQL 选错了索引。

对应解决方案:

新加索引:MySQL 5.6 版本以后,创建索引都支持 Online DDL 了,对于那种高峰期数据库已经被这个语句打挂了的情况,最高效的做法就是直接执行 alter table 语句。比较理想的是能够在备库先执行。假设你现在的服务是一主一备,主库 A、备库 B,这个方案的大致流程是这样的:在备库 B 上执行 set sql_log_bin=off,也就是不写 binlog,然后执行 alter table 语句加上索引;执行主备切换;这时候主库是 B,备库是 A。在 A 上执行 set sql_log_bin=off,然后执行 alter table 语句加上索引。这是一个“古老”的 DDL 方案。平时在做变更的时候,你应该考虑类似 gh-ost 这样的方案,更加稳妥。但是在需要紧急处理时,上面这个方案的效率是最高的。

查询重写:call query_rewrite.flush_rewrite_rules() 这个存储过程,是让插入的新规则生效。

force index

QPS 突增打到数据库问题

白名单机制、业务账号分离,下掉QPS高的账户;语句重写,返回select 1.这些都是应急方式

12、MySQL是怎么保证数据不丢的?

MySQL 的“双 1”配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog:

1)binlog 的写入机制:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。

write 和 fsync 的时机,是由参数 sync_binlog 控制的:(write,指把日志写入到文件系统的 page cache(OS catch),并没有把数据持久化到磁盘,fsync,才是将数据持久化到磁盘的操作,fsync 才占磁盘的 IOPS。)

sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;

sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;

sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。

2)redo log 的写入机制:

MySQL是工作在用户空间(User Space)的,log buffer处于用户空间的内存中,要写入到磁盘上的log file中,中间还要经过操作系统内核空间(Kernel Space)的os buffer,调用fsync()的作用就是将OS buffer中的日志刷到磁盘上的log file中。

“双1配置”会有很高的TPS,解决:组提交(group commit)机制:日志逻辑序列号(log sequence number,LSN)通过LSN实现组提交,最大LSN的持久化,组内的其他持久化都会被覆盖到。

问题 1:执行一个 update 语句以后,我再去执行 hexdump 命令直接查看 ibd 文件内容,为什么没有看到数据有改变呢?回答:这可能是因为 WAL 机制的原因。update 语句执行完成后,InnoDB 只保证写完了 redo log、内存,可能还没来得及将数据写到磁盘。

问题 2:为什么 binlog cache 是每个线程自己维护的,而 redo log buffer 是全局共用的?回答:MySQL 这么设计的主要原因是,binlog 是不能“被打断的”。一个事务的 binlog 必须连续写,因此要整个事务完成后,再一起写到文件里。而 redo log 并没有这个要求,中间有生成的日志可以写到 redo log buffer 中。redo log buffer 中的内容还能“搭便车”,其他事务提交的时候可以被一起写到磁盘中。

问题 3:事务执行期间,还没到提交阶段,如果发生 crash 的话,redo log 肯定丢了,这会不会导致主备不一致呢?回答:不会。因为这时候 binlog 也还在 binlog cache 里,没发给备库。crash 以后 redo log 和 binlog 都没有了,从业务角度看这个事务也没有提交,所以数据是一致的。

13、MySQL是怎么保证主备一致的?

主备流程图

 binlog 有两种格式,一种是 statement,一种是 row。mixed 格式的意思是,MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式。

row 格式下binlog 里会记录所有的字段信息,方便恢复数据。用 binlog 来恢复数据的标准做法是,用 mysqlbinlog 工具解析出来,然后把解析结果整个发给 MySQL 执行。

循环复制问题:双 M 结构,双 M 结构和 M-S 结构相比,节点 A 和 B 之间总是互为主备关系。这样在切换的时候就不用再修改主备关系。解决:每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。

主备延迟问题:备库消费中转日志(relay log)的速度,比主库生产 binlog 的速度要慢。原因:1)备库所在机器的性能要比主库所在的机器性能差。2)备库的压力大,备库上读太多(一主多从解决)。3)大事务,不要一次性地用 delete 语句删除太多数据、大表 DDL(建议使用 gh-ost 方案)。都是典型的大事务场景。4)备库的并行复制能力。

MySQL 高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。

备库延迟几小时?备库上 sql_thread 更新数据, 5.6 版本之前,MySQL 只支持单线程复制,由此在主库并发高、TPS 高时就会出现严重的主备延迟问题。MySQL5.6 版本,支持了并行复制,只是支持的粒度是按库并行。相比于按表和按行分发,这个策略有两个优势:1、构造 hash 值的时候很快,只需要库名,不会上百万个并行。2、不要求 binlog 的格式。因为 statement 格式的 binlog 也可以很容易拿到库名。但是,如果你的主库上的表都放在同一个 DB 里面,这个策略就没有效果了。解决:MySQL 5.7 的并行复制策略,由参数 slave-parallel-type 来控制并行复制策略:配置为 DATABASE,表示使用 MySQL 5.6 版本的按库并行策略;配置为 LOGICAL_CLOCK,表示的就是类似 MariaDB 的策略(模拟主库的并行模式)。MySQL 5.7.22 版本里,新的并行复制策略:基于 WRITESET 的并行复制:表示的是对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset。如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行。保证相同的先后顺序。

14、主库出问题了,从库怎么办?

1、基于位点的主备切换:要从主库的 master_log_name 文件的 master_log_pos 这个位置的日志继续同步。而这个位置就是我们所说的同步位点,也就是主库对应的文件名和日志偏移量。问题:位点很难精确取到

2、基于 GTID 的主备切换:GTID 的全称是 Global Transaction Identifier,也就是全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。

过期读:

在从库上会读到系统的一个过期状态:由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态。解决:强制走主库方案(看业务是否适合);sleep 方案;判断主备无延迟方案;配合 semi-sync 方案(半同步复制,启用了 semi-sync,就表示所有给客户端发送过确认的事务,都确保了备库已经收到了这个日志:从库收到 binlog 以后,发回给主库一个 ack,表示收到了;主库收到这个 ack 以后,才能给客户端返回“事务完成”的确认。问题:一主多从的回答);等主库位点方案;等 GTID 方案(trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid1;选定一个从库执行查询语句;在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);如果返回值是 0,则在这个从库执行查询语句;否则,到主库执行查询语句。将参数 session_track_gtids 设置为 OWN_GTID,然后通过 API 接口 mysql_session_track_get_first 从返回包解析出 GTID 的值即可)。

间隙锁是不互锁的

查看死锁事务信息:执行 show engine innodb status 命令得到的部分输出。 LATESTDETECTED DEADLOCK部分,就是记录的最后一次死锁信息。

15、误删数据怎么办

误删行:使用 delete 语句误删了数据行,可以用 Flashback 工具通过闪回把数据恢复回来。Flashback 恢复数据的原理,是修改 binlog 的内容,拿回原库重放。而能够使用这个方案的前提是,需要确保 binlog_format=row 和 binlog_row_image=FULL。不建议你直接在主库上执行这些操作。预防:sql_safe_updates 参数设置为 on,忘记where报错

误删库/表:使用全量备份,加增量日志的方式:取最近一次全量备份,假设这个库是一天一备,上次备份是当天 0 点;用备份恢复出一个临时库;从日志备份里面,取出凌晨 0 点之后的日志;把这些日志,除了误删除数据的语句外,全部应用到临时库。(不能指定表,应用日志的过程就只能是单线程,解决:1、用备份恢复出临时实例之后,将这个临时实例设置成线上备库的从库。2、平时建一个延时备库)预防:不给 truncate/drop 权限,平时用读库、制定操作规范

rm 删除数据:删掉了其中某一个节点的数据的话,HA 系统就会开始工作,选出一个新的主库,从而保证整个集群的正常工作。这时,你要做的就是在这个节点上把数据恢复回来,再接入整个集群。

要把这个数据恢复功能做成自动化工具,并且经常拿出来演练。

16、kill不掉语句?

MySQL 中有两个 kill 命令:

 kill query + 线程 id,表示终止这个线程中正在执行的语句;

 kill connection + 线程 id,这里 connection 可缺省,表示断开这个线程的连接

这些“kill 不掉”的情况,其实是因为发送 kill 命令的客户端,并没有强行停止目标线程的执行,而只是设置了个状态,并唤醒对应的线程。而被 kill 的线程,需要执行到判断状态的“埋点”,才会开始进入终止逻辑阶段。并且,终止逻辑本身也是需要耗费时间的。

17、查很多数据,会不会把数据库内存打爆?

1、MySQL 是“边读边发的”:取数据和发数据的流程是这样的:获取一行,写到 net_buffer 中。这块内存的大小是由参数 net_buffer_length 定义的,默认是 16k。重复获取行,直到 net_buffer 写满,调用网络接口发出去。如果发送成功,就清空 net_buffer,然后继续取下一行,并写入 net_buffer。如果发送函数返回 EAGAIN 或 WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送。

2、InnoDB 对 LRU 算法做了改进,冷数据的全表扫描,对 Buffer Pool 的影响也能做到可控:内存命中率:查询数据直接从Buffer Pool返回的比例,越高越快。InnoDB 内存管理用的是最近最少使用 (Least Recently Used, LRU) 算法,这个算法的核心就是淘汰最久未使用的数据。缺点:刷历史表的冷数据会降低内存命中率,改进:按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域:扫描过程中,需要新插入的数据页,都被放到 old 区域 ;超过指定时间没有被再次访问则淘汰。

18、join的用法

 join 语句执行过程中,驱动表是走全表扫描,而被驱动表是走树搜索。因此应该让小表(按照where等条件过滤剩下少的表是小表)来做驱动表,前提是“可以使用被驱动表的索引”。被驱动表上没有可用的索引,使用(BNL)Block Nested-Loop Join:把表 t1 的数据读入线程内存 join_buffer 中,扫描表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回。这个过程中,对表 t1 和 t2 都做了一次全表扫描(join_buffer 的大小是由参数 join_buffer_size 设定的,默认值是 256k。如果放不下表 t1 的所有数据话:分段放。)

对BNL的优化:

MRR:这个优化的主要目的是尽量使用顺序读盘:需要排序

BKA:依赖MRR,给被驱动表的 join 字段加上索引,把 BNL 算法转成 BKA 算法。

内存表不是临时表

内存表相当于写在内存的正常表,是使用 Memory 引擎的表(由于重启会丢数据,如果一个备库重启,会导致主备同步线程停止;如果主库跟这个备库是双 M 架构,还可能导致主库的内存表数据被删掉。所以不建议使用);临时表,可以使用各种引擎类型, join 查询的时候使用到了临时表不同 session 的临时表是可以重名的,临时表会自动回收,不需要额外操作。为什么临时表可以重名?:frm 文件放在临时文件目录下,文件名的后缀是.frm,前缀是“#sql{进程 id}_{线程 id}_ 序列号”

每个线程都维护了自己的临时表链表。这样每次 session 内操作表的时候,先遍历链表,检查是否有这个名字的临时表,如果有就优先操作临时表,如果没有再操作普通表。binlog_format=row,临时表有关的语句,不会记录到 binlog 里。statment/mixed 的时候,binlog 会记录临时表的操作(MySQL 在记录 binlog 的时候,会把主库执行这个语句的线程 id 写到 binlog 中。这样,在备库的应用线程就能够知道执行每个语句的主库线程 id,并利用这个线程 id 来构造临时表的 table_def_key)。

什么时候用内存临时表:union 执行流程(union all不用)、group by 执行流程(如果对 group by 语句的结果没有排序要求,要在语句后面加 order by null;尽量让 group by 过程用上表的索引,可以避免使用内存临时表和排序、如果 group by 需要统计的数据量不大,尽量只使用内存临时表;也可以通过适当调大 tmp_table_size 参数,来避免用到磁盘临时表;)

19、自增主键不连续

在 MyISAM 引擎里面,自增值是被写在数据文件上的。而在 InnoDB 中,自增值是被记录在内存的。MySQL 直到 8.0 版本,才给 InnoDB 表的自增值加上了持久化的能力,确保重启前后一个表的自增值不变。

自增 id 是递增的,但不保证是连续的:唯一键冲突导致自增主键 id 不连续。事务回滚不回收自增 id。

20、insert的锁:

insert … select 是很常见的在两个表之间拷贝数据的方法。你需要注意,在可重复读隔离级别下,这个语句会给 select 的表里扫描到的记录和间隙加读锁。而如果 insert 和 select 的对象是同一个表,则有可能会造成循环写入。这种情况下,我们需要引入用户临时表来做优化。insert 语句如果出现唯一键冲突,会在冲突的唯一值上加共享的 next-key lock(S 锁)。因此,碰到由于唯一键约束导致报错后,要尽快提交或回滚事务,避免加锁时间过长。

21、快速复制表:

理拷贝的方式速度最快,尤其对于大表拷贝来说是最快的方法。如果出现误删表的情况,用备份恢复出误删之前的临时库,然后再把临时库中的表拷贝到生产库上,是恢复数据最快的方法。但是,这种方法的使用也有一定的局限性:必须是全表拷贝,不能只拷贝部分数据;需要到服务器上拷贝数据,在用户无法登录数据库主机的场景下无法使用;由于是通过拷贝物理文件实现的,源表和目标表都是使用 InnoDB 引擎时才能使用。(一个 InnoDB 表,除了包含这两个物理文件外,还需要在数据字典中注册。直接拷贝这两个文件的话,因为数据字典中没有 db2.t 这个表,系统是不会识别和接受它们的。不过,在 MySQL 5.6 版本引入了可传输表空间(transportable tablespace) 的方法,可以通过导出 + 导入表空间的方式,实现物理拷贝表的功能。)

用 mysqldump 生成包含 INSERT 语句文件的方法,可以在 where 参数增加过滤条件,来实现只导出部分数据。这个方式的不足之一是,不能使用 join 这种比较复杂的 where 条件写法。

用 select … into outfile (导出CSV)的方法是最灵活的,支持所有的 SQL 写法。但,这个方法的缺点之一就是,每次只能导出一张表的数据,而且表结构也需要另外的语句单独备份。后两种方式都是逻辑备份方式,是可以跨引擎使用的。

22、grant

grant 语句是用来给用户赋权的,grant 语句会同时修改数据表和内存,判断权限的时候使用的是内存数据。命令完成后即时生效,接下来新创建的连接会使用新的权限。对于一个已经存在的连接,它的全局权限不受 grant 命令的影响。跟 db 权限类似,表和列权限每次 grant 的时候都会修改数据表,也会同步修改内存中的 hash 结构。因此,对这两类权限的操作,也会马上影响到已经存在的连接。。因此,规范地使用 grant 和 revoke 语句,是不需要随后加上 flush privileges 语句的。

flush privileges 语句本身会用数据表的数据重建一份内存权限数据,所以在权限数据可能存在不一致的情况下再使用。而这种不一致往往是由于直接用 DML 语句操作系统权限表导致的,所以我们尽量不要使用这类语句。

23、分区分表

分区:优点:对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁。还有,分区表可以很方便的清理历史数据。

缺点:MySQL 在第一次打开分区表的时候,需要访问所有的分区;在 server 层,认为这是同一张表,因此所有分区共用同一个 MDL 锁;在引擎层,认为这是不同的表,因此 MDL 锁之后的执行过程,会根据分区表规则,只访问必要的分区。

分区并不是越细越好,分区不要提前预留太多

24、自增id用完怎么办?

表的自增 id 达到上限后,再申请时它的值就不会改变,进而导致继续插入数据时报主键冲突的错误。(int->bigint 4变8字节、分表)

row_id :表没有指定主键,那么 InnoDB 会给你创建一个不可见的,长度为 6 个字节的 row_id。达到上限后,则会归 0 再重新递增,如果出现相同的 row_id,后写的数据会覆盖之前的数据。(定义长的主键)

Xid :server 层维护的,用来对应事务的, 只需要不在同一个 binlog 文件中出现重复值即可。虽然理论上会出现重复值,但是概率极小,可以忽略不计。

trx_id:事务 id,InnoDB 数据可见性的核心思想是:每一行数据都记录了更新它的 trx_id,当一个事务读到一行数据的时候,判断这个数据是否可见的方法,就是通过事务的一致性视图与这行数据的 trx_id 做对比。InnoDB 的 max_trx_id 递增值每次 MySQL 重启都会被保存起来,所以我们文章中提到的脏读的例子就是一个必现的 bug,好在留给我们的时间还很充裕。

thread_id 是我们使用中最常见的,而且也是处理得最好的一个自增 id 逻辑了。

 

 

 

 

 

 


版权声明:本文为qililong88原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。