+
+
基础架构:一条SQL查询语句是如何执行的
+
+
MySQL可以分为Server层和存储引擎层两部分
+
Server层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
+
而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎
+
连接器
+
连接器负责跟客户端建立连接、获取权限、维持和管理连接。建立连接后,权限修改不会影响已经存在的连接的权限
+
长连接:连接成功后,如果客户度持续有请求,一直使用同一个连接
+短连接:每次执行很少的几次查询就断开连接,下次查询再重新建立
+
建立连接比较耗时,尽量使用长连接,但是全部使用长连接会导致OOM,因为MySQL在执行过程中临时使用的内存是管理在连接对象里面,连接不断开内存不会释放
+
解决方案:
+
+- 定期断开长连接
+- 执行mysql_reset_connection重新初始化连接资源
+
+
查询缓存
+
执行过的语句及其结果可能会以key-value对的形式,直接缓存在内存中
+
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上的所有查询缓存都会被清空,因此不要使用查询缓存,MySQL 8.0删掉了此功能
+
分析器
+
分析器先做“词法分析”,识别出SQL语句中的字符串分别是什么,例如,识别“select”是查询语句,“T”识别成表名T
+
然后做“语法分析”,判断输入的SQL语句是否满足MySQL语法,如果语句不对,会收到“You have an error in your SQL syntax”的错误提醒
+
优化器
+
优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序
+
例如下面的语句执行两个表的join:
+
mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
+
+
+- 既可以先从表t1里面取出c=10的记录的ID值,再根据ID值关联到表t2,再判断t2里面d的值是否等于20。
+- 也可以先从表t2里面取出d=20的记录的ID值,再根据ID值关联到t1,再判断t1里面c的值是否等于10。
+
+
这两种执行方法的结果一样但效率不同,优化器决定使用哪个方法
+
执行器
+
+- 判断有没有表的执行权限
+- 根据表的引擎定义调用引擎提供的接口,例如“取满足条件的第一行”,“满足条件的下一行”,数据库的慢查询日志rows_examined字段表示语句在执行过程中扫描了多少行,引擎扫描行数跟rows_examined并不是完全相同的
+
+
日志系统:一条SQL更新语句是如何执行的
+
一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。更新语句还涉及 redo log(重做日志)和 binlog(归档日志)
+
redo log
+
WAL(Write-Ahead Logging):更新记录时,InnoDB引擎会先把记录写到redo log里面并更新内存,然后在适当的时候将操作记录更新到磁盘里面
+
InnoDB的redo log是固定大小和循环写的,write pos是当前记录的位置,checkpoint是当前要擦除的位置,擦除记录前要把记录更新到数据文件
+
redo log保证即使数据库异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe
+
binlog
+
redo log是InnoDB引擎特有的日志,Server层特有的引擎是binlog(归档日志)
+
两者有三点不同
+
+- redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用
+- redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1”
+- redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志
+
+
执行器和InnoDB引擎在执行update语句时的内部流程
+
+- 执行器先找引擎取ID=2这一行。ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
+- 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
+- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
+- 执行器生成这个操作的binlog,并把binlog写入磁盘。
+- 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。
+
+
下图的浅色框表示在InnoDB内部执行,深色框表示在执行器中执行
+
+
两阶段提交
+
redo log的写入分为两步骤:prepare和commit,也就是”两阶段提交“,目的是为了让两份的日志之间的逻辑一致
+
当数据库需要恢复到指定的某一秒时,可以先在临时库上这样做:
+
+- 找到最近的一次全量备份
+- 从备份的时间点开始,将备份的binlog依次取出来重放到指定时间
+
+
如果redo log不是两阶段提交
+
+- 先写redo log后写binlog。假设在redo log写完,binlog还没写完,MySQL异常重启,数据可以恢复,但是binlog没有记录这个语句。之后用binlog恢复临时库时会缺少更新
+- 先写binlog后写redo log。假设binlog写完,redo log还没写,MySQL异常重启之后,这个事务无效,数据没有恢复。但是binlog里面已经有这个语句,所以之后用binlog恢复临时库会多一个事务
+
+
innodb_flush_log_at_trx_commit这个参数设置成1的时候,表示每次事务的redo log都直接持久化到磁盘。这个参数建议设置成1,这样可以保证MySQL异常重启之后数据不丢失。
+
sync_binlog这个参数设置成1的时候,表示每次事务的binlog都持久化到磁盘。建议设置成1,这样可以保证MySQL异常重启之后binlog不丢失。
+
如果redo log处于prepare状态且binlog写入完成,MySQL异常重启会commit掉这个事务
+
事务隔离
+
事务保证一组数据库操作要么全部成功,要么全部失败
+
隔离性与隔离级别
+
ACID中的I指的是隔离性(Isolation)
+
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。
+
事务隔离级别包括:
+
+- 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到
+- 读提交:一个事务提交之后,它做的变更才会被其他事务看到
+- 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其它事务也是不可见的
+- 串行化:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成才能继续执行
+
+
数据库使用视图来实现隔离级别。在“可重复读”隔离级别下,视图是在事务开启时创建的。在“读提交”隔离级别下,视图是在每个SQL语句开始执行的时候创建的。“读未提交”直接返回记录的最新值,没有视图概念。“串行化”直接用加锁的方式
+
事务隔离的实现
+
这里展开说明“可重复读”
+
在MySQL中,每条记录在更新时都会同时记录一条回滚操作。假设一个值从1按顺序改成了2、3、4,在回滚日志里有类似下面的记录
+
+
当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。视图A、B、C对应的值分别是1、2、4,同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。对于视图A,要得到1,就必须将当前值依次执行图中所有的回滚操作。即使现在有另一个事务正在将4改成5,这个事务跟视图A、B、C对应的事务是不会冲突的。
+
系统会将没有比回滚日志更早的read-view时删掉这个回滚日志。因此尽量不要使用长事务,系统里面会存在很老的事务视图
+
事务的启动方式
+
MySQL的事务启动方式有如下几种:
+
+- 显式启动事务,begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback
+- 隐式启动事务,一条SQL语句会自动开启一个事务。需要设置autocommit = 1 才会自动提交
+
+
set autocommit=0,会将这个线程的自动提交关掉。事务持续存在直到你主动执行commit 或 rollback语句,或者断开连接
+
建议使用set autocommit=1,并显示启动事务
+
在autocommit为1的情况下,用begin显式启动的事务,如果执行commit则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行begin语句的开销
+
可以在information_schema库的innodb_trx这个表中查询长事务,比如下面这个语句,用于查找持续时间超过60s的事务。
+
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
+
+
如何避免长事务对业务的影响?
+
从应用端来看
+
+- 通过MySQL的general_log确保autocommit=1
+- 包含多个select语句的只读事务,没有一致性要求就拆分
+- 通过SET MAX_EXECUTION_TIME控制每个语句的最长执行时间
+
+
从数据库端来看
+
+- 监控 information_schema.Innodb_trx表,设置长事务阈值,超过就kill
+- 推荐使用Percona的pt-kill
+- 业务测试阶段就输出所有general_log,分析日志提前发现问题
+- innodb_undo_tablespaces设置成2或更大的值
+
+
深入浅出索引(上)
+
索引的出现是为了提高数据查询的效率
+
索引的常见模型
+
哈希表,只适用于只有等值查询的场景,不适用于范围查询
+
有序数组在等值查询和范围查询场景中都非常优秀,但更新数据需要挪动数组元素,成本太高。只适用于静态存储引擎(数据不再变化)
+
平衡二叉查找树的时间复杂度是O(log(N)),但是算上每次访问节点的磁盘IO开销,查询非常慢。为了减少磁盘IO次数,出现了N叉树
+
InnoDB的索引模型
+
根据叶子节点内容,索引类型分为主键索引和非主键索引
+
主键索引(聚簇索引):叶子节点存的是整行数据
+普通索引(二级索引):叶子结点存的是主键的值
+
+
基于主键索引和普通索引的查询的区别:
+
+- 如果语句是select * from T where ID=500,即主键查询方式,则只需要搜索ID这棵B+树;
+- 如果语句是select * from T where k=5,即普通索引查询方式,则需要先搜索k索引树,得到ID的值为500,再到ID索引树搜索一次。这个过程称为回表。
+
+
索引维护
+
在插入新记录时,B+树为了维护有序性会进行页分裂和页合并
+
自增主键 VS 业务字段主键
+
性能上:自增主键按序插入,不会触发叶子节点的分裂,而业务字段做主键往往不是有序插入,导致页分裂和页合并,性能差
+存储空间上:主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。业务字段主键是身份证号(20字节)不如自增主键(4字节或8字节)
+
业务字段做主键的场景是:
+
+- 只有一个索引
+- 该索引必须是唯一索引
+
+
这就是典型的KV场景,直接将这个字段设置为主键
+
深入浅出索引(下)
+
覆盖索引
+
覆盖索引:索引的叶子节点可以直接提供查询结果,不需要回表
+
可以为高频请求建立联合索引起到覆盖索引的作用
+
最左前缀原则
+
索引项是按照索引定义里面出现的字段的顺序排序的。满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左N个字段,也可以是字符串索引的最左M个字符
+
索引内的字段顺序评估标准:
+
+- 复用能力,如果该顺序的联合索引能少维护一个索引,那么该顺序优先使用
+- 空间,如果必须维护联合索引和单独索引,那么给小字段单独索引,联合索引的顺序是(大字段,小字段)
+
+
索引下推
+
在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足的条件的记录,减少回表次数(联合索引在按最左匹配时碰到范围查询停止,索引下推可以对后面的索引字段做条件判断后再返回结果集)
+
全局锁和表锁
+
根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类
+
全局锁
+
全局锁是对整个数据库实例加锁,让整个库处于只读状态
+
Flush tables with read lock
+
+
全局锁的典型使用场景是不支持MVCC的引擎(MyISAM)的全库逻辑备份,如果所有表的引擎支持MVCC,可以在备份时开启事务确保拿到一致性视图(mysqldump加上参数-single-transaction)
+
让全库只读,另外一种方式是set global readonly = true
,但仍然建议使用FTWRL,因为:
+
+- readonly的值可能会用来做其它逻辑,比如判断是主库还是备库
+- FTWRL在客户端发生异常断开时,MySQL会自动释放全局锁,而readonly会一直保持
+
+
表级锁
+
表级锁有两种:表锁,元数据锁(meta data lock,MDL)
+
表锁
+
语法:lock tables … read/write
+
表锁会限制其它线程的读写,也会限定本线程的操作对象
+
例如,线程A执行lock tables t1 read, t2 write;
,其它线程写t1、读写t2都会被阻塞,线程A只能执行读t1、读写t2,不能访问其它表
+
如果支持行锁,一般不使用表锁
+
元数据锁
+
MDL不需要显示使用,在访问表时会被自动加上,事务提交才释放,作用是保证读写的正确性
+
当对表做增删改查操作时,加MDL读锁;当对表结构变更时,加MDL写锁
+
+- 读锁之间不互斥,因此可以有多个线程同时对一张表增删查改
+- 读写锁之间、写锁之间是互斥的。因此如果有两个线程要同时给一个表加字段,其中一个要等另一执行完再执行
+
+
给表加字段的方式:
+
+- kill掉长事务,事务不提交会一直占着MDL
+- 在alter table语句设置等待时间,如果在等待时间内能拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句。后面重试这个过程
+
+
行锁
+
行锁是针对表中行记录的锁
+
两阶段锁
+
两阶段锁协议:在InnoDB事务中,行锁是在需要的时候加上的,但并不是不需要了就立刻释放,而是要等到事务结束才释放
+
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放
+
死锁和死锁检测
+
死锁:并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源,导致这几个线程无限等待
+
死锁出现后有两种策略:
+
+- 设置等待时间,修改innodb_lock_wait_timeout
+- 发起死锁检测,主动回滚死锁链条中的某一个事务,让其他事务可以继续执行。innodb_deadlock_detect设置为on表示开启
+
+
第一种策略,等待时间太长,业务的用户接受不了,等待时间太短会出现误伤。所以一般用死锁检测
+
死锁检测有性能问题,解决思路有几种:
+
+- 如果能确保业务一定不会出现死锁,可以临时把死锁检测关掉。这种方法存在业务有损的风险,业务逻辑碰到死锁会回滚重试,但是没有死锁检测会超时导致业务有损
+- 控制并发程度。数据库Server层实现,对相同行的更新,在进入引擎之前排队
+- 将一行改成逻辑上的多行。例如账户余额等于10行之和,扣钱时随机扣一行,这种方案需要根据业务逻辑做详细设计
+
+
详解事务隔离
+
假如有如下表和事务A、B、C
+
mysql> CREATE TABLE `t` (
+ `id` int(11) NOT NULL,
+ `k` int(11) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
+insert into t(id, k) values(1,1),(2,2);
+
+
+
"快照"在MVCC里是怎么工作的
+
快照不是整个数据库的拷贝。
+
InnoDB里每个事务都有一个唯一的transaction id,是事务开始时申请的,严格递增的。每行数据有多个版本,每次事务更新数据时,都会生成一个新的数据版本,并把transaction id赋给这个数据版本的事务id,记为row trx_id。某个版本的数据可以通过当前版本和undo log计算出来
+
在实现上,InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的是启动了但还没提交
+
数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位
+
+
对于当前事务的启动瞬间来说,一个数据版本的row trx_id,有以下几种可能:
+
+- 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
+- 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
+- 如果落在黄色部分,那就包括两种情况
+a. 若 row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;
+b. 若 row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。
+
+
InnoDB利用了“所有数据都有多个版本”的特性,实现了“秒级创建快照”的能力
+
可以用时间顺序来理解版本的可见性。
+
一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:
+
+- 版本未提交,不可见;
+- 版本已提交,但是是在视图创建后提交的,不可见
+- 版本已提交,而且是在视图创建前提交的,可见
+
+
更新逻辑
+
更新数据都是先读后写,读是当前值,称为“当前读”(current read)。所以事务B是在(1,2)上进行修改
+
select如果加锁,也是当前读,不加就是一致读,下面两个select语句分别加了读锁(S锁,共享锁)和写锁(X锁,排它锁)。行锁包括共享锁和排它锁
+
mysql> select k from t where id=1 lock in share mode;
+mysql> select k from t where id=1 for update;
+
+
假设事务C变成了事务C’
+
+
事务C’还没提交,但是生成了最新版本(1,2),根据“两阶段协议”,(1,2)这个版本上的写锁还没释放,事务B的更新是当前读,需要加锁,所以被阻塞
+
可重复读的核心就是一致性读(consistent read);而事务更新数据时只能用当前读,如果当前的记录的行锁被其他事务占用的话,就进入锁等待。
+
读提交的逻辑和可重复读的逻辑类似,主要区别是:
+
+- 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
+- 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
+
+
普通索引和唯一索引之间的选择
+
普通索引VS唯一索引:两者类似,区别是唯一索引的列值不能重复,允许一个为空
+
下面从这两种索引对查询语句和更新语句的性能来分析,前提是业务保证记录的唯一性,如果业务不能保证唯一性又有唯一需求,就必须用唯一索引
+
查询过程
+
普通索引:查找到满足条件的第一个记录后,需要查找下一个记录,直到碰到第一个不满足条件的记录
+
唯一索引:由于唯一性,查找到满足条件的第一个记录后就停止
+
由于InnoDB的数据是按数据页为单位来读写,所以两者性能差距微乎其微
+
更新过程
+
change buffer:当需要更新一个数据页时,如果数据页在内存中就直接更新,如果不在,在不影响数据一致性的前提下,InnoDB会将这些更新操作缓存在change buffer中。在下次查询需要访问这个数据页时,读入内存,然后执行change buffer中与这个页有关的操作,这个过程称为merge
+
唯一索引的更新不能用change buffer,因为需要先将数据页读入内存判断操作是否违反唯一性约束
+
假如现在有个插入新记录的操作,如果要更新的目标页在内存中,普通索引和唯一索引性能差距不大。如果目标页不在内存中,对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值;对于普通索引来说,将更新记录在change buffer,此时普通索引的性能好(主键索引的数据页是一定要加载进内存做更新操作,普通索引的数据页不用进内存)
+
change buffer的使用场景
+
因为merge的时候是真正做数据更新的时候,在merge之前,change buffer记录的变更越多,收益越大
+
对于写多读少的业务,change buffer的效果最好,比如账单类、日志类的系统
+
索引的选择和实践
+
尽量使用普通索引
+
如果更新完马上查询,就关闭change buffer。否则开着能提升更新性能
+
change buffer 和 redo log
+
redo log 主要节省的是随机写磁盘的IO消耗(转成顺序写),而change buffer主要节省的则是随机读磁盘的IO消耗。
+
MySQL为什么有时候会选错索引
+
+
session B 先删除了所有数据然后调用idata存储过程插入了10万行数据。
+
然后session B 执行三条SQL:
+
// 将慢查询日志的阈值设置为0,表示这个线程接下来的语句都会被记录慢查询日志中
+set long_query_time=0;
+select * from t where a between 10000 and 20000;
+select * from t force index(a) where a between 10000 and 20000;
+
+
+
Q1走了全表扫描,Q2使用了正确的索引
+
优化器的逻辑
+
选择索引是优化器的工作,目的是寻找最优方案执行语句,判断标准包括扫描行数、是否使用临时表、是否排序等因素
+
上面查询语句没有涉及临时表和排序,说明扫描行数判断错误了
+
MySQL是怎么判断扫描行数的
+
MySQL在真正开始执行语句之前,并不能精确知道有多少行,而只能用统计信息估算。这个统计信息就是索引的“区分度”,索引上不同的值称为“基数”,基数越大,区分度越好。基数由采样统计得出。
+
如果统计信息不对导致行数和实际情况差距较大,可以使用analyze table 表名
来重新统计索引信息
+
索引选择异常和处理
+
由于索引统计信息不准确导致的问题,可以用analyze table来解决,其它优化器误判的解决方法如下:
+
+- 使用force index强行选择索引。缺点是变更不及时,开发通常不写force index,当生产环境出现问题,再修改需要重新测试和发布
+- 修改语句,引导MySQL使用我们期望的索引。缺点是需要根据数据特征进行修改,不具备通用性
+- 新建更合适的索引或删掉误用的索引。缺点是找到更合适的索引比较困难
+
+
怎么给字符串字段加索引
+
可以给字符串字段建立一个普通索引,也可以给字符串前缀建立普通索引。使用前缀索引,定义好长度,就可以既节省空间,又不用额外增加太多查询成本
+
可以通过统计索引上有多少个不同的值来判断使用多长的前缀,不同值越多,区分度越高,查询性能越好
+
首先算出这列有多少不同值
+
mysql> select count(distinct email) as L from SUser;
+
+
然后选取不同长度的前缀来看这个值
+
mysql> select
+ count(distinct left(email,4))as L4,
+ count(distinct left(email,5))as L5,
+ count(distinct left(email,6))as L6,
+ count(distinct left(email,7))as L7,
+from SUser;
+
+
前缀索引对覆盖索引的影响
+
前缀索引可能会增加扫描行数,导致影响性能,还可能导致用不上覆盖索引对查询的优化。
+
前缀索引的叶子节点只包含主键,如果查询字段不仅仅有主键,那必须回表。而用完整字符串做索引,如果查询字段只有主键和索引字段,那就不用回表
+
其它方式
+
对于邮箱来说,前缀索引的效果不错。
+但是对于身份证来说,可能需要长度12以上的前缀索引,才能满足区分度要求,但是这样又太占空间了
+
有一些占用空间更小但是查询效率相同的方法:
+
+- 倒序存储身份证号,建立长度为6的前缀索引,身份证后6位可以提供足够的区分度
+- 加个身份证的整型hash字段,给这个字段加索引
+
+
这两种方法的相同点是都不支持范围查询,区别在于:
+
+- 倒序存储不占额外空间
+- 倒序每次写和读都需要额外调用一次reverse函数,hash字段需要额外调用一次crc32函数,reverse稍优
+- hash字段的查询性能更稳定一些
+
+
为什么MySQL为“抖”一下
+
“抖”:SQL语句偶尔会执行特别慢,且随机出现,持续时间很短
+
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
+
平时执行很快的更新操作,其实就是在写内存和日志,“抖”可能是在刷脏页(flush),情况有以下几种:
+
+- redo log满了,系统会停止所有更新操作,把checkpoint往前推进,原位置和新位置之间的所有脏页都flush到磁盘上。尽量避免这种情况,会阻塞更新操作
+- 系统内存不足,淘汰脏页。尽量避免一个查询要淘汰的脏页太多
+- 系统空闲
+- 正常关闭
+
+
InnoDB刷脏页的控制策略
+
使用fio测试磁盘的IOPS,并把innodb_io_capacity设置成这个值,告诉InnoDB全力刷盘可以刷多快
+
fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest
+
+
不能总是全力刷盘,InnoDB刷盘速度还要参考内存脏页比例和redo log写盘速度
+
+
脏页比例不要经常接近75%,查看命令如下:
+
mysql> 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;
+
+
还有个策略是刷盘的“连坐”机制:脏页的邻居如果是脏页会一起被刷盘。这种策略对机械硬盘有大幅度性能提升,但是SSD的IOPS已不是瓶颈,推荐innodb_flush_neighbors设置成0,只刷自己
+
为什么表数据删掉一半,表文件大小不变
+
InnoDB表包含两部分:表结构定义和数据。MySQL 8.0 之前,表结构是存在以.frm为后缀的文件,MySQL 8.0 允许表结构定义放在系统数据表中
+
参数innodb_file_per_table
+
设置成OFF表示,表的数据放在系统共享表空间,也就是跟数据字典放在一起;
+设置成ON表示,每个InnoDB表数据存储在一个以.ibd为后缀的文件中
+
从MySQL 5.6.6开始默认就是ON
+
数据删除流程
+
InnoDB里的数据都是用B+数的结构组织的。
+
+
记录的复用:删除R4记录时,InnoDB会把记录标记为删除,插入ID在300到600之间的记录时可能会复用这个位置,磁盘文件大小不会缩小
+
数据页的复用:InnoDB的数据是按页存储的。如果将page A上所有记录删除以后,page A会被标记为可复用,这时候插入ID=50的记录需要使用新页时,page A会被复用。因此,delete整个表会把所有数据页都标记为可复用,但是磁盘文件不会变小
+
可以复用,而没被使用的空间,看起来就像是“空洞”,不只是删除数据会造成空洞,随机插入数据会引发索引的数据页分裂,导致空洞。更新索引上的值,可以理解为删除旧值和插入新值,也会造成空洞。解决空洞的方法是重建表
+
重建表
+
可以使用alter table A engine=InnoDB命令来重建表。MySQL 5.6是离线重建,重建期间更新会丢失。
+
+MySQL 5.6 引入了Online DDL,重建表的流程:
+
+- 建立一个临时文件,扫描表A主键的所有数据页
+- 用数据页中表A的记录生成B+数,存储到临时文件中
+- 生成临时文件的过程中,将所有对A的操作记录在一个日志文件(row log)中,对应图中state2状态
+- 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表A相同的数据文件,对应图中state3状态
+- 用临时文件替换表A的数据文件
+
+
alter语句在启动时需要获取MDL写锁,这个写锁在真正拷贝数据之前就退化成读锁了,目的是实现Online,MDL读锁不会阻塞记录的增删改操作(DML)
+
推荐使用gh-ost做大表的重建
+
Online 和 inplace
+
inplace是指整个DDL过程在 InnoDB 内部完成,对于 Server层来说,没有把数据挪动到临时表,这是一个“原地”操作,这就是inplace名称由来
+
和inplace对应的是copy,也就是前面离线重建
+
DDL过程如果是 Online 的,就一定是inplace的;反过来未必,全文索引和空间索引是 inplace 的,但不是 Online 的
+
optimize table、analyze table和alter table三种方式重建表的区别:
+
+- 从MySQL 5.6开始,alter table t engine=InnoDB(也就是recreate)默认就是上面引入Online DDL后的重建过程
+- analyze table t 不是重建表,只是对表的索引信息做重新统计,没有修改数据,这个过程中加了MDL读锁
+- optimize table t 等于recreate+analyze
+
+
count(*)慢该怎么办
+
count(*)的实现方式
+
InnoDB count(*)会遍历全表,优化器会找到最小的索引数进行计数,结果准确但有性能问题。show table status命令显示的行数是采样估算的,不准确
+
用缓存系统保存计数
+
可以用Redis来保存记录数,但是会出现逻辑上不精确的问题。根本原因是这两个不同的存储构成的系统,不支持分布式事务,无法拿到精确一致的视图
+
+这种情况是Redis的计数不精确
+
+这种情况是查询结果不精确
+
在数据库保存计数
+
将计数放在数据库里单独的一张计数表中,可以利用事务解决计数不精确的问题
+
+
在会话B读操作期间,会话A还没提交事务,因此B没有看到计数值加1的操作,因此计数值和“最近100条记录”的结果在逻辑上是一致的
+
不同的count用法
+
count(*)、count(主键id)和count(1) 都表示返回满足条件的结果集的总行数。count(字段),则表示返回满足条件的数据行里面,参数“字段”不为NULL的总个数
+
对于count(主键id)来说,InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为空的,就按行累加。
+
对于count(1)来说,InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。
+
count(1)执行得要比count(主键id)快。因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。
+
对于count(字段)来说:
+
如果这个“字段”是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加;
+
如果这个“字段”定义允许为null,那么执行的时候,判断到有可能是null,还要把值取出来再判断一下,不是null才累加。
+
count(*) 是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*)肯定不是null,按行累加。
+
结论是:按照效率排序的话,
+count(字段) < count(主键id) < count(1) ≈ count(*),所以尽量使用count(*)。
+
orderby是怎么工作的
+
假设有SQL语句
+
select city,name,age from t where city='杭州' order by name limit 1000;
+
+
全字段排序
+
+
如果要排序的数据量小于sort_buffer_size,排序就在内存中完成,否则外部排序(归并)
+
rowid 排序
+
max_length_for_sort_data 是MySQL中专门控制用于排序的行数据的长度的参数,超过这个值就不会全字段排序,用rowid排序
+
+
全字段排序 VS rowid排序
+
如果内存够就用全字段排序,rowid排序回表多造成磁盘读,性能较差
+
并不是所有的order by语句都要排序的,如果建索引时就是有序的就不排
+
创建一个city和name的联合索引,查询过程如下:
+
+
还可以创建city、name和age的联合索引,这样就不用回表了
+
+
如何正确地显示随机消息
+
10000行记录如何随机选择3个
+
内存临时表
+
用order by rand()来实现这个逻辑
+
mysql> select word from words order by rand() limit 3;
+
+
R:随机数,W:单词,pos:rowid,对于有主键的表,rowid就是主键ID,没有主键就由系统生成
+
原表->内存临时表:扫描10000行
+内存临时表->sort_buffer:扫描10000行
+内存临时表->结果集:访问3行
+
+
order by rand()使用了内存临时表,内存临时表排序的时候使用了rowid排序方法
+
磁盘临时表
+
当内存临时表大小超过了tmp_table_size时,如果使用归并排序,内存临时表会转为磁盘临时表,如果使用优先队列排序(排序+limit操作),且维护的堆大小不超过sort_buffer_size,则不会转为磁盘临时表
+
随机排序方法
+
+- 取得整个表的行数,记为C
+- 取得 Y = floor(C * rand())
+- 再用 limit Y,1 取得一行
+
+
取多个随机行就重复多次这个算法
+
mysql> select count(*) into @C from t;
+set @Y1 = floor(@C * rand());
+set @Y2 = floor(@C * rand());
+set @Y3 = floor(@C * rand());
+select * from t limit @Y1,1; //在应用代码里面取Y1、Y2、Y3值,拼出SQL后执行
+select * from t limit @Y2,1;
+select * from t limit @Y3,1;
+
+
或者优化一下,Y1,Y2,Y3从小到大排序,这样扫描的行数就是Y3
+
id1 = select * from t limit @Y1,1;
+id2 = select * from t where id > id1 limit @Y2-@Y1,1;
+id3 = select * from t where id > id2 limit @Y3
+
+
为什么逻辑相同的SQL语句性能差异巨大
+
+- 对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器决定放弃走树搜索功能,但不是放弃索引,优化器可以选择遍历索引
+- 隐式类型转换可能会触发上面的规则1
+- 隐式字符编码转换也可能触发上面的规则1
+
+
为什么只查一行的语句也执行这么慢
+
查询长时间不返回
+
+- 等MDL锁。通过查询sys.schema_table_lock_waits,可以找出造成阻塞的process id,把这个连接用kill杀掉
+- 等flush。可能情况是有一个flush tables命令被别的语句堵住了,然后它又堵住了查询语句,可以用show processlist 查出并杀掉阻塞flush的连接
+- 等行锁。通过查询sys.innodb_lock_waits 杀掉对应连接
+
+
查询慢
+
+- 查询字段没有索引,走了全表扫描
+- 事务隔离级别为可重复读,当前事务看不到别的事务的修改,但是别的事务执行了多次修改,当前事务在查询时要根据undo log查询到应该看到的值
+
+
幻读
+
幻读:一个事务在前后两次查询同一个范围时,后一次查询看到了前一次查询没有看到的行
+
+- 在可重复读隔离级别下,普通的查询时快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现
+- 幻读专指“新插入的行”
+
+
幻读的问题
+
+
解决幻读
+
间隙锁:锁住两行之间的间隙
+
在行扫描过程中,不仅给行加行锁,还给行间的间隙上锁
+
跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。
+
间隙锁和行锁合称next-key lock,左开右闭
+
间隙锁的引入,可能会导致同样的语句锁住更大的范围,影响并发度。
+
间隙锁只在可重复读隔离级别下才会生效
+
为什么只改一行的语句,锁这么多
+
加锁规则(可重复读隔离级别):
+
+- 原则1:加锁的基本单位是next-key lock
+- 原则2:查找过程中访问到的对象才会加锁
+- 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁
+- 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁
+- 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止
+
+
假设有如下SQL语句
+
CREATE TABLE `t` (
+ `id` int(11) NOT NULL,
+ `c` int(11) DEFAULT NULL,
+ `d` int(11) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `c` (`c`)
+) ENGINE=InnoDB;
+
+insert into t values(0,0,0),(5,5,5),
+(10,10,10),(15,15,15),(20,20,20),(25,25,25);
+
+
案例一:等值查询间隙锁
+
+
由于表t中没有id=7的记录
+
+- 根据原则1,加锁单位是next-key lock,session A加锁范围就是(5,10];
+- 同时根据优化2,这是一个等值查询(id=7),而id=10不满足查询条件,next-key lock退化成间隙锁,因此最终加锁的范围是(5,10)。
+
+
所以,session B要往这个间隙里面插入id=8的记录会被锁住,但是session C修改id=10这行是可以的。
+
案例二:非唯一索引等值锁
+
+
这里session A要给索引c上c=5的这一行加上读锁。
+
+- 根据原则1,加锁单位是next-key lock,因此会给(0,5]加上next-key lock
+- 要注意c是普通索引,因此仅访问c=5这一条记录是不能马上停下来的,需要向右遍历,查到c=10才放弃。根据原则2,访问到的都要加锁,因此要给(5,10]加next-key lock。
+- 但是同时这个符合优化2:等值判断,向右遍历,最后一个值不满足c=5这个等值条件,因此退化成间隙锁(5,10)
+- 根据原则2 ,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么session B的update语句可以执行完成。
+
+
但session C要插入一个(7,7,7)的记录,就会被session A的间隙锁(5,10)锁住。
+
需要注意,在这个例子中,lock in share mode只锁覆盖索引,但是如果是for update就不一样了。 执行 for update时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。
+
这个例子说明,锁是加在索引上的;同时,它给我们的指导是,如果你要用lock in share mode来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段。比如,将session A的查询语句改成select d from t where c=5 lock in share mode
+
案例三:主键索引范围锁
+
mysql> select * from t where id=10 for update;
+mysql> select * from t where id>=10 and id<11 for update;
+
+
这两条语句在逻辑上是等价的,但是加锁规则不一样
+
+- 开始执行的时候,要找到第一个id=10的行,因此本该是next-key lock(5,10]。 根据优化1, 主键id上的等值条件,退化成行锁,只加了id=10这一行的行锁
+- 范围查找就往后继续找,找到id=15这一行停下来,因此需要加next-key lock(10,15]
+
+
所以,session A这时候锁的范围就是主键索引上,行锁id=10和next-key lock(10,15]
+
需要注意一点,首次session A定位查找id=10的行的时候,是当做等值查询来判断的,而向右扫描到id=15的时候,用的是范围查询判断
+
案例四:非唯一索引范围锁
+
+
这次session A用字段c来判断,加锁规则跟主键索引范围锁的唯一不同是:在第一次用c=10定位记录的时候,索引c上加了(5,10]这个next-key lock后,由于索引c是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终sesion A加的锁是,索引c上的(5,10] 和(10,15] 这两个next-key lock
+
案例五:唯一索引范围锁bug
+
+
session A是一个范围查询,按照原则1的话,应该是索引id上只加(10,15]这个next-key lock,并且因为id是唯一键,所以循环判断到id=15这一行就应该停止了。
+
但是实现上,InnoDB会往前扫描到第一个不满足条件的行为止,也就是id=20。而且由于这是个范围扫描,因此索引id上的(15,20]这个next-key lock也会被锁上。
+
案例六:非唯一索引上存在“等值”的例子
+
现在插入一条新记录
+
mysql> insert into t values(30,10,30);
+
+
+
delete语句加锁的逻辑和 select … for update是类似的,session A在遍历的时候,先访问第一个c=10的记录。同样地,根据原则1,这里加的是(c=5,id=5)到(c=10,id=10)这个next-key lock。
+
然后,session A向右查找,直到碰到(c=15,id=15)这一行,循环才结束。根据优化2,这是一个等值查询,向右查找到了不满足条件的行,所以会退化成(c=10,id=10) 到 (c=15,id=15)的间隙锁。
+
也就是说,这个delete语句在索引c上的加锁范围,就是下图中蓝色区域覆盖的部分。蓝色区域左右两边都是虚线,表示开区间,即(c=5,id=5)和(c=15,id=15)这两行上都没有锁。
+
+
案例七:limit 语句加锁
+
+
session A的delete语句加了 limit 2。你知道表t里c=10的记录其实只有两条,因此加不加limit 2,删除的效果都是一样的,但是加锁的效果却不同。可以看到,session B的insert语句执行通过了,跟案例六的结果不同。这是因为,案例七里的delete语句明确加了limit 2的限制,因此在遍历到(c=10, id=30)这一行之后,满足条件的语句已经有两条,循环就结束了。
+
因此,索引c上的加锁范围就变成了从(c=5,id=5)到(c=10,id=30)这个前开后闭区间,如下图所示:
+
+
这个例子对我们实践的指导意义就是,在删除数据的时候尽量加limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。
+
案例八:一个死锁的例子
+
本案例目的是说明:next-key lock 实际上是间隙锁和行锁加起来的结果
+
+
+- session A 启动事务后执行查询语句加lock in share mode,在索引c上加了next-key lock(5,10] 和间隙锁(10,15);
+- session B 的update语句也要在索引c上加next-key lock(5,10] ,进入锁等待;
+- 然后session A要再插入(8,8,8)这一行,被session B的间隙锁锁住。由于出现了死锁,InnoDB让session B回滚
+
+
你可能会问,session B的next-key lock不是还没申请成功吗?
+
其实是这样的,session B的“加next-key lock(5,10] ”操作,实际上分成了两步,先是加(5,10)的间隙锁,加锁成功;然后加c=10的行锁,这时候才被锁住的。
+
也就是说,我们在分析加锁规则的时候可以用next-key lock来分析。但是要知道,具体执行的时候,是要分成间隙锁和行锁两段来执行的。
+
“饮鸩止渴”提高性能的方法
+
短连接风暴
+
短连接模式就是连接到数据库后,执行很少的SQL语句就断开,下次需要的时候再重连,在业务高峰期,会出现连接数暴涨的情况
+
两种有损业务的解决方法:
+
+- 处理掉占着连接但是不工作的线程。优先断开事务外空闲太久的连接
+- 减少连接过程的损耗。关闭权限验证
+
+
慢查询性能问题
+
引发慢查询的情况有三种:
+
+- 索引没有设计好。最高效的解决方法是直接alter table建索引
+- SQL语句没有写好,导致没用上索引。解决方法是使用query_rewrite重写SQL语句
+- MySQL选错了索引。应急方案是给语句加上force index或者使用query_rewrite重写语句加上force index
+
+
出现情况最多的是前两种,通过下面过程可以预先发现和避免
+
+- 上线前,在测试环境,把慢查询日志(slow log)打开,并且把long_query_time设置成0,确保每个语句都会被记录入慢查询日志
+- 在测试表里插入模拟线上的数据,做一遍回归测试
+- 观察慢查询日志里每类语句的输出,特别留意Rows_examined字段是否与预期一致
+
+
QPS突增问题
+
+- 业务bug导致。可以把这个功能的SQL从白名单去掉
+- 如果新功能使用的是单独的数据库用户,可以用管理员账号把这个用户删掉,然后断开连接
+- 用query_rewrite把压力最大的SQL语句直接重写成"select 1"返回
+
+
方法3存在两个副作用:
+
+- 如果别的功能也用到了这个SQL语句就会误伤
+- 该语句可能是业务逻辑的一部分,导致业务逻辑一起失败
+
+
方法3是优先级最低的方法。方法1和2依赖于规范的运维体系:虚拟化、白名单机制、业务账号分离
+
MySQL是怎么保证数据不丢的
+
只要redo log和binlog保证持久化到磁盘,就能确保MySQL异常重启后,数据可以恢复
+
binlog的写入机制
+
binlog的写入逻辑:事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中
+
一个事务的binlog是不能被拆开的,因此不论这个事务多大,也要确保一次性写入
+
系统给binlog cache分配了一片内存,每个线程一个,参数binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘
+
事务提交的时候,执行器把binlog cache里的完整事务写入到binlog中,并清空binlog cache
+
+
+- 图中的write,指的是把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快
+- 图中的fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为fsync才占磁盘的IOPS
+
+
write和fsync的时机由参数sync_binlog控制:
+
+- sync_binlog=0,表示每次提交事务都只write,不fsync
+- sync_binlog=1,表示每次提交事务都会fsync
+- sync_binlog=N(N>1),表示每次提交事务都write,但累积N个事务后才fsync
+
+
sync_binlog设置成N可以改善IO瓶颈场景的性能,但对应的风险是:如果主机发生异常重启,会丢失最近N个事务的binlog
+
redo log的写入机制
+
事务执行过程中,生成的redo log要先写到redo log buffer,但不是每次生成后都要直接持久化到磁盘,因为事务没提交,日志丢了也不会有损失。
+但是也有可能事务没有提交,redo log buffer 中的部分日志持久化到了磁盘。下图是redo log的三种状态
+
+
日志写到redo log buffer是很快的,write到page cache也快,但是持久化到磁盘就很慢。
+
InnoDB提供了innodb_flush_log_at_trx_commit参数来控制redo log的写入策略:
+
+- 设置为0表示每次事务提交时都只是把redo log 留在redo log buffer中
+- 设置为1表示每次事务提交时都将redo log直接持久化到磁盘
+- 设置为2表示每次事务提交时都只是把redo log写到page cache
+
+
InnoDB有个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘
+
注意,事务执行过程中的redo log也是直接写在redo log buffer中的,这些redo log也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的redo log,也可能已经持久化到磁盘
+
除了后台线程每秒一次的轮询操作外,还有两个场景会让没提交的事务的redo log写入到磁盘
+
+- redo log buffer占用的空间即将达到 innodb_log_buffer_size一半的时候,后台线程会主动write到page cache
+- 并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘
+
+
两阶段提交的过程,时序上redo log先prepare,再写binlog,最后再把redo log commit
+
如果innodb_flush_log_at_trx_commit设置为1,那么redo log在prepare阶段就要持久化一次,因为有一个崩溃恢复逻辑是prepare的redo log + 完整的binlog
+
每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB就认为redo log在commit的时候就不需要fsync了,只会write到文件系统的page cache中就够了
+
通常我们说MySQL的“双1”配置,指的就是sync_binlog和innodb_flush_log_at_trx_commit都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare 阶段),一次是binlog
+
redo log组提交
+
日志逻辑序列号LSN:LSN单调递增,用来对应redo log的一个个写入点。每次写入长度为length的redo log,LSN的值就会加上length
+
如下图所示,是三个并发事务在prepare阶段,都写完redo log buffer,持久化到磁盘的过程中
+
+
从图中可以看到,
+
+- trx1是第一个到达的,会被选为这组的leader
+- 等trx1要开始写盘的时候,这个组里面已经有3个事务了,这时候LSN也变成了160
+- trx1去写盘的时候,带的就是LSN=160,因此等trx1返回时,所有LSN小于等于160的redo log,都已经被持久化到磁盘
+- 这时候trx2和trx3就可以直接返回了
+
+
所以,一次组提交里面,组员越多,节约磁盘IOPS的效果越好。但如果只有单线程压测,那就只能老老实实地一个事务对应一次持久化操作了。
+
在并发更新场景下,第一个事务写完redo log buffer以后,接下来这个fsync越晚调用,组员可能越多,节约IOPS的效果就越好。
+
MySQL为了让组提交的效果更好,细化了两阶段提及的顺序,让redo log的fsync往后拖
+
+
上图的顺序说明binlog也可以组提交,但是通常情况下步骤3会执行得很快,所以能集合到一起持久化的binlog比较少。可以通过设置binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count来提升binlog组提交的效果
+
性能瓶颈在IO的提升方法
+
+- 设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
+- 将sync_binlog 设置为大于1的值(比较常见是100~1000)。这样做的风险是,主机掉电时会丢binlog日志。
+- 将innodb_flush_log_at_trx_commit设置为2。这样做的风险是,主机掉电的时候会丢数据。
+
+
MySQL是怎么保证主备一致的
+
MySQL的主备一致依赖于binlog
+
MySQL主备的基本原理
+
主备切换流程
+
+
客户端的读写是直接访问主库,备库同步主库的更新,与主库保持一致。虽然备库不会被客户端访问,但仍推荐设置成只读模式,因为:
+
+- 有时候一些运营类的查询语句会放到备库上去查,设置为只读可以防止误操作
+- 防止切换逻辑有bug,比如切换过程中出现双写,造成主备不一致
+- 可以用readonly状态来判断节点的角色
+
+
备库的只读对超级权限用户是无效的,用于同步更新的线程拥有超级权限
+
同步流程
+
主库的更新语句同步到备库的完成流程图如下
+
+
备库B跟主库A之间维持了一个长连接。主库A内部有一个线程,专门用于服务备库B的这个长连接。一个事务日志同步的完整过程如下:
+
+- 在备库B上通过change master命令,设置主库A的IP、端口、用户名、密码,以及要从哪个位置开始请求binlog,这个位置包含文件名和日志偏移量
+- 在备库B上执行start slave命令,这时候备库会启动两个线程,就是图中的io_thread和sql_thread。其中io_thread负责与主库建立连接。
+- 主库A校验完用户名、密码后,开始按照备库B传过来的位置,从本地读取binlog,发给B
+- 备库B拿到binlog后,写到本地文件,称为中转日志(relay log)
+- sql_thread读取中转日志,解析出日志里的命令,并执行
+
+
binlog的三种格式对比
+
binlog有三种格式,statement、row以及前两种格式的混合mixed
+
假设有如下表:
+
mysql> CREATE TABLE `t` (
+ `id` int(11) NOT NULL,
+ `a` int(11) DEFAULT NULL,
+ `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `a` (`a`),
+ KEY `t_modified`(`t_modified`)
+) ENGINE=InnoDB;
+
+insert into t values(1,1,'2018-11-13');
+insert into t values(2,2,'2018-11-12');
+insert into t values(3,3,'2018-11-11');
+insert into t values(4,4,'2018-11-10');
+insert into t values(5,5,'2018-11-09');
+
+
statement格式就是SQL语句原文
+
+
下图是该语句执行效果
+
+
statement格式下,delete 带 limit,很可能出现主备数据不一致的情况,比如上面的例子:
+
+- 如果delete语句使用的是索引a,那么会根据索引a找到第一个满足条件的行,也就是说删除的是a=4这一行
+- 但如果使用的是索引t_modified,那么删除的就是 t_modified='2018-11-09’ 也就是a=5这一行。
+
+
row格式binlog如下
+
+
row格式的binlog把SQL语句替换成了两个event:Table_map和Delete_rows
+
+- Table_map event,用于说明接下来要操作的表是test库的表t;
+- Delete_rows event,用于定义删除的行为。
+
+
借助mysqlbinlog工具查看详细的binlog
+
+
当binlog_format使用row格式的时候,binlog里面记录了真实删除行的主键id,这样binlog传到备库去的时候,就肯定会删除id=4的行,不会有主备删除不同行的问题。
+
mixed格式吸收了statement和row格式的优点,占用空间小,避免了数据不一致
+
但是现在binlog设置成row的场景更多,理由有很多,其中之一是恢复数据。
+
如果执行的是delete语句,row格式的binlog也会把被删掉的行的整行信息保存起来。所以,如果你在执行完一条delete语句以后,发现删错数据了,可以直接把binlog中记录的delete语句转成insert,把被错删的数据插入回去就可以恢复了
+
如果你是执行错了insert语句呢?那就更直接了。row格式下,insert语句的binlog里会记录所有的字段信息,这些信息可以用来精确定位刚刚被插入的那一行。这时,你直接把insert语句转成delete语句,删除掉这被误插入的一行数据就可以了。
+
如果执行的是update语句的话,binlog里面会记录修改前整行的数据和修改后的整行数据。所以,如果你误执行了update语句的话,只需要把这个event前后的两行信息对调一下,再去数据库里面执行,就能恢复这个更新操作了
+
循环复制问题
+
binlog的特性确保了主备一致性。实际生产上使用比较多的是双M结构
+
+
双M结构中,节点A和B之间总是互为主备关系,在切换的时候就不用再修改主备关系
+
循环复制指的是A节点更新完,把binlog发给B,B更新完又生成binlog发给了A,解决循环复制的方法如下:
+
+- 规定两个库的server id必须不同,如果相同,则它们之间不能设定为主备关系
+- 一个备库接到binlog并在重放的过程中,生成与原binlog的server id相同的新的binlog
+- 每个库在收到从自己的主库发过来的日志后,先判断server id,如果跟自己的相同,表示这个日志是自己生成的,直接丢弃
+
+
因此,双M结构的日志执行流会变成这样:
+
+- 从节点A更新的事务,binlog里面记得都是A的server id
+- 传到节点B执行一次以后,节点B生成的binlog的server id也是A的server id
+- 再传回给节点A,A判断到这个server id与自己的相同,就不会再处理这个日志。所以,死循环这里就断掉了
+
+
MySQL是怎么保证高可用的
+
正常情况下,只要主库执行更新生成的所有binlog,都可以传到备库并被正确地执行,备库就能达到跟主库一致的状态,这就是最终一致性。但是MySQL要提供高可用,只有最终一致性是不够的
+
主备延迟
+
与数据同步有关的时间点主要包括以下三个:
+
T1:主库A执行完成一个事务,写入binlog
+T2:备库B接收完这个binlog
+T3:备库B执行完成这个事务
+
主备延迟:同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是T3-T1
+
seconds_behind_master表示备库延迟了多少秒
+
网络正常情况下,主备延迟的主要因素是T3-T2,直接表现是备库消费中转日志(relay log)的速度比主库生产binlog的速度慢
+
主备延迟的来源
+
+- 备库的机器性能差。解决方法是对称部署
+- 备库的压力大。有些统计查询语句只在备库上跑,导致备库压力大,解决方法是一主多从分担读的压力或者把binlog输送到Hadoop来提供统计查询能力
+- 大事务。比如一次性用delete删除太多数据或者大表DDL
+- 备库的并行复制能力
+
+
由于主备延迟的存在,所以在主备切换的时候,有不同的策略
+
可靠性优先策略
+
在双M结构下,主备切换流程如下:
+
+- 判断备库B现在的seconds_behind_master,如果小于某个值(比如5秒)继续下一步,否则持续重试这一步;
+- 把主库A改成只读状态,即把readonly设置为true;
+- 判断备库B的seconds_behind_master的值,直到这个值变成0为止;
+- 把备库B改成可读写状态,也就是把readonly 设置为false;
+- 把业务请求切到备库B。
+
+
+
步骤2直到步骤5,主库A和备库B都处于readonly状态,系统不可用(不可写)
+
可用性优先策略
+
把上面策略里的步骤4和5放到最开始执行,代价是可能出现数据不一致的情况
+
一般情况下,可靠性优于可用性。在满足数据可靠性的前提下,MySQL高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。
+
备库为什么会延迟好几个小时
+
当备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就有可能成了小时级别
+
MySQL 5.6之前,备库应用日志更新数据只能使用单线程,在主库并发高、TPS高时会出现严重的主备延迟问题
+
按表分发策略
+
按表分发事务的基本思路是,如果两个事务更新不同的表,它们就可以并行
+
+
worker线程维护一张执行队列里的事务涉及的表,key是“库名.表名”,value表示队列中有多少个事务修改这个表
+
事务在分发的时候,和所有worker的冲突关系有3种:
+
+- 如果跟所有worker都不冲突,coordinator线程就会把这个事务分配给最空闲的woker;
+- 如果跟多于一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突关系的worker只剩下1个;
+- 如果只跟一个worker冲突,coordinator线程就会把这个事务分配给这个存在冲突关系的worker。
+
+
按表分发方案在多个表负载均衡的场景效果很好。但是碰到热点表会退化成单线程复制
+
按行分发策略
+
要解决热点表的并行复制问题,就需要一个按行并行复制方案。核心思路是:如果两个事务没有更新相同的行,它们在备库上可以并行执行。这个模式要求binlog格式必须是row
+
判断事务和worker是否冲突,用的规则不是“修改同一个表”,而是“修改同一行”。worker维护的hash表的key是“库名+表名+唯一索引的名字+唯一索引的值”
+
按行分发策略比按表分发策略需要消耗更多的计算资源,这两种方案都有一样的约束条件:
+
+- 要能够从binlog里面解析出表名、主键值和唯一索引的值。也就是说,主库的binlog格式必须是row;
+- 表必须有主键;
+- 不能有外键。表上如果有外键,级联更新的行不会记录在binlog中,这样冲突检测就不准确。
+
+
MySQL 5.6版本的并行复制策略
+
官方MySQL 5.6版本支持的并行复制的力度是按库并行。hash表的key是数据库名
+
相比于按表和按行分发,有两个优势:
+
+- 构造hash值的时候很快,只需要库名;而且一个实例上DB数也不会很多,不会出现需要构造100万个项这种情况
+- 不要求binlog的格式。因为statement格式的binlog也可以很容易拿到库名
+
+
MariaDB的并行复制策略
+
MariaDB的并行复制策略利用了redo log组提交优化的特性:
+
+- 能够在同一组里提交的事务,一定不会修改同一行
+- 主库上可以并行执行的事务,备库上也一定可以并行执行
+
+
这个策略的目标是“模拟主库的并行模式”,但它没有实现“真正的模拟主库并发度”这个目标。在主库上,一组事务在commit的时候,下一组事务是同时处于“执行中”状态的
+
MySQL 5.7的并行复制策略
+
由参数slave-parallel-type来控制并行复制策略:
+
+- 配置为DATABASE,表示使用MySQL 5.6版本的按库并行策略;
+- 配置为 LOGICAL_CLOCK,表示的就是优化过的类似MariaDB的策略
+
+
该优化的策略的思想是:
+
+- 同时处于prepare状态的事务,在备库执行时是可以并行的;
+- 处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的。
+
+
MySQL 5.7.22的并行复制策略
+
新增了一个并行复制策略,基于WRITESET的并行复制。
+
新增参数binlog-transaction-dependency-tracking,用来控制是否启用这个新策略:
+
+- COMMIT_ORDER,表示的就是前面介绍的,根据同时进入prepare和commit来判断是否可以并行的策略。
+- WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的hash值,组成集合writeset。如果两个事务没有操作相同的行,也就是说它们的writeset没有交集,就可以并行
+- WRITESET_SESSION,是在WRITESET的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。
+
+
该策略类似按行分发,但是有很大优势:
+
+- writeset是在主库生成后直接写入到binlog里面的,这样在备库执行的时候,不需要解析binlog内容(event里的行数据),节省了很多计算量;
+- 不需要把整个事务的binlog都扫一遍才能决定分发到哪个worker,更省内存;
+- 由于备库的分发策略不依赖于binlog内容,所以binlog是statement格式也是可以的。
+
+
该方案对于“表上没主键”和“外键约束”的场景,也会暂时退化为单线程模型。
+
主库出问题了,从库怎么办
+
大多数互联网应用场景都是读多写少,要解决读性能问题,就要涉及:一主多从
+
+
图中,虚线箭头表示的是主备关系,也就是A和A’互为主备, 从库B、C、D指向的是主库A。一主多从的设置,一般用于读写分离,主库负责所有的写入和一部分读,其他的读请求则由从库分担。
+
下面讨论,在一主多从架构下,主库故障后的主备切换问题
+
+
相比于一主一备的切换流程,一主多从结构在切换完成后,A’会成为新的主库,从库B、C、D也要改接到A’。正是由于多了从库B、C、D重新指向的这个过程,所以主备切换的复杂性也相应增加了
+
基于位点的主备切换
+
节点B设置成节点A’ 的从库的时候,需要执行change master命令,必须设置主库的日志文件名和偏移量。A和A’的位点是不同的,从库B切换时需要先经过“找同步位点”这个逻辑
+
同步位点很难精确取到
+
取同步位点的方法如下:
+
+- 等待新主库A’ 把中转日志(relay log)全部同步完成
+- 在A’ 上执行show master status命令,得到当前A’ 上最新的 File 和 Position
+- 取原主库A故障的时刻T
+- 用mysqlbinlog工具解析A’的File,得到T时刻的位点
+
+
mysqlbinlog File
+
+
+
图中,end_log_pos后面的值“123”,表示的就是A’这个实例,在T时刻写入新的binlog的位置,可以把这个值作为$master_log_pos ,用在节点B的change master命令里
+
这个值并不精确,从库B的同步线程可能会出错,解决方法如下:
+
+- 通过sql_slave_skip_counter跳过出错事务
+- 设置slave_skip_errors,跳过指定错误,通常设置成1032,1062,对应的错误是删除数据找不到行,插入数据唯一键冲突
+
+
GTID
+
前两种方式操作复杂,容易出错,MySQL 5.6 引入了GITD。
+
GTID全称是Global Transaction Identifier,也就是全局事务ID,是一个事务在提交的时候生成的,是这个事务的唯一标识,格式是:
+
GTID=server_uuid:gno
+
+
+- server_uuid是一个实例第一次启动时自动生成的,是一个全局唯一的值
+- gno是一个整数,初始值是1,每次提交事务的时候分配给这个事务,并加1
+
+
GTID有两种生成方式:
+
+-
+
如果gtid_next=automatic,代表使用默认值。这时,MySQL就会把server_uuid:gno分配给这个事务。
+a. 记录binlog的时候,先记录一行 SET @@SESSION.GTID_NEXT=‘server_uuid:gno’;
+b. 把这个GTID加入本实例的GTID集合
+
+-
+
如果gtid_next是一个指定的GTID的值,比如通过set gtid_next='current_gtid’指定为current_gtid,那么就有两种可能:
+a. 如果current_gtid已经存在于实例的GTID集合中,接下来执行的这个事务会直接被系统忽略;
+b. 如果current_gtid没有存在于实例的GTID集合中,就将这个current_gtid分配给接下来要执行的事务,也就是说系统不需要给这个事务生成新的GTID,因此gno也不用加1
+
+
+
每个MySQL实例都维护了一个GTID集合,用来对应“这个实例执行过的所有事务”
+
当从库需要跳过某个事务时,在主库上查出GTID,在从库上提交空事务,把这个GTID加入到从库的GTID集合中
+
set gtid_next='aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10';
+begin;
+commit;
+set gtid_next=automatic;
+start slave;
+
+
基于GTID的主备切换
+
切换命令指定master_auto_position=1表示这个主备关系使用的是GTID协议,不需要指定主库日志文件和偏移量
+
我们把A’ 的GTID集合记为set_a,实例B的GTID集合记为set_b,切换流程如下:
+
+- 实例B指定主库A’,基于主备协议建立连接。
+- 实例B把set_b发给主库A’
+- 实例A’算出set_a与set_b的差集,也就是所有存在于set_a,但是不存在于set_b的GITD的集合,判断A’本地是否包含了这个差集需要的所有binlog事务。
+a. 如果不包含,表示A’已经把实例B需要的binlog给删掉了,直接返回错误;
+b. 如果确认全部包含,A’从自己的binlog文件里面,找出第一个不在set_b的事务,发给B;
+- 之后就从这个事务开始,往后读文件,按顺序取binlog发给B去执行
+
+
GTID和在线DDL
+
假设,这两个互为主备关系的库还是实例X和实例Y,且当前主库是X,并且都打开了GTID模式。这时的主备切换流程可以变成下面这样:
+
+-
+
在实例X上执行stop slave。
+
+-
+
在实例Y上执行DDL语句。注意,这里并不需要关闭binlog。
+
+-
+
执行完成后,查出这个DDL语句对应的GTID,并记为 server_uuid_of_Y:gno。
+
+-
+
到实例X上执行以下语句序列:
+set GTID_NEXT="server_uuid_of_Y:gno";
+begin;
+commit;
+set gtid_next=automatic;
+start slave;
+
+这样做的目的在于,既可以让实例Y的更新有binlog记录,同时也可以确保不会在实例X上执行这条更新。
+
+-
+
接下来,执行完主备切换,然后照着上述流程再执行一遍即可
+
+
+
读写分离有哪些坑
+
由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态,这种现象称为“过期读”
+
过期读处理方案包括:
+
+- 强制走主库方案
+- sleep方案
+- 判断主备无延迟方案
+- 配合semi-sync方案
+- 等主库位点方案
+- 等GTID方案
+
+
强制走主库方案
+
该方案将查询请求分为两类:
+
+- 对于必须要拿到最新结果的请求,强制将其发到主库上。比如,在一个交易平台上,卖家发布商品以后,马上要返回主页面,看商品是否发布成功。那么,这个请求需要拿到最新的结果,就必须走主库
+- 对于可以读到旧数据的请求,才将其发到从库上。在这个交易平台上,买家来逛商铺页面,就算晚几秒看到最新发布的商品,也是可以接受的。那么,这类请求就可以走从库
+
+
这个方案用的最多,但是问题在于存在“所有查询都不能是过期读”的需求,比如金融类业务,那就必须放弃读写分离,所有读写压力都在主库
+
下面讨论的是:可以支持读写分离的场景下,有哪些解决过期读的方案
+
Sleep方案
+
主库更新后,读从库之前先sleep一下。这个方案假设,大多数情况下主备延迟在1s之内
+
该方案可以解决类似Ajax场景下的过期读问题。例如卖家发布商品,直接将卖家输入的内容作为新商品显示出来,并不查从库。等待卖家刷新页面,相当于sleep了一段时间,解决了过期读问题
+
该方案存在的问题是不精确:
+
+- 如果查询请求本来0.5s就可以在从库上拿到正确结果,也会等到1s
+- 如果延迟超过1s,还是会出现过期读
+
+
判断主备无延迟方案
+
有三种方法:
+
+- 每次从库执行查询请求前,先判断seconds_behind_master是否已经等于0。如果还不等于0 ,那就必须等到这个参数变为0才能执行查询请求。
+- 对比位点。如果Master_Log_File和Relay_Master_Log_File、Read_Master_Log_Pos和Exec_Master_Log_Pos这两组值完全相同表示主备无延迟
+- 对比GITD。Retrieved_Gtid_Set和Executed_Gtid_Set相同表示是主备无延迟
+
+
该方案比Sleep更准确,方法2和3比1准确,但是不能说精确。因为存在客户端已经收到提交确认,而备库还没收到日志的状态,因此备库认为主备无延迟,从而发生过期读
+
配合semi-sync
+
为解决上面的问题,引入semi-sync replication:
+
semi-sync做了这样的设计:
+
+- 事务提交的时候,主库把binlog发给从库
+- 从库收到binlog以后,发回给主库一个ack
+- 主库收到ack以后,才能给客户端返回“事务完成”的确认
+
+
开启semi-sync,就表示所有给客户端发送过确认的事务,都确保备库已经收到了这个日志
+
semi-sync+判断主备无延迟方案存在两个问题:
+
+- 一主多从情况下,因为主库只要收到一个从库的ack就给客户端返回确认,其它未响应ack的从库可能会发生过期读问题
+- 在业务高峰期,主库的位点或者GITD集合更新很快,这种情况下,可能出现从库一直存在主备延迟导致客户端查询一直等待
+
+
等主库位点方案
+
该方案解决了前面两个问题
+
命令:
+
select master_pos_wait(file, pos[, timeout]);
+
+
这条命令的逻辑如下:
+
+- 它是在从库执行的
+- 参数file和pos指的是主库上的文件名和位置
+- timeout可选,设置为正整数N表示这个函数最多等待N秒
+
+
为了解决前面两个问题,流程如下:
+
+- trx1事务更新完成后,马上执行show master status得到当前主库执行到的File和Position;
+- 选定一个从库执行查询语句;
+- 在从库上执行select master_pos_wait(File, Position, 1);
+- 如果返回值是>=0的整数,则在这个从库执行查询语句;
+- 否则,到主库执行查询语句。
+
+
+
GTID方案
+
等GTID也可以解决前面两个问题
+
流程如下:
+
+- trx1事务更新完成后,从返回包直接获取这个事务的GTID,记为gtid1;
+- 选定一个从库执行查询语句;
+- 在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);
+- 如果返回值是0,则在这个从库执行查询语句;
+- 否则,到主库执行查询语句。
+
+
+
如何判断一个数据库是不是出问题了
+
select 1 判断
+
select 1 成功返回只能说明数据库进程还在,不能说明没问题
+
并发连接:通过show precesslist查询连接数,连接数可以远大于并发查询数量
+并发查询:“当前正在执行”的语句的数量
+
线程进入锁等待后,并发线程的计数会减一,即进入锁等待的线程不吃CPU
+
假如设置并发线程数是3,下面的情况是A、B、C在并发查询,D先select 1不占并发线程数所以能正常返回,但实际上已经不能正常查询了
+
+
查表判断
+
为了能够检测InnoDB并发线程数过多导致的系统不可用情况,我们需要找一个访问InnoDB的场景。一般的做法是,在系统库(mysql库)里创建一个表,比如命名为health_check,里面只放一行数据,然后定期执行:
+
mysql> select * from mysql.health_check;
+
+
这种方法在磁盘空间满了就无效。因为更新事务要写binlog,而一旦binlog所在磁盘满了,那么所有更新语句都会堵住,但是系统仍然可以读数据
+
更新判断
+
我们把查询换成更新来作为监控语句。常见做法是放一个timestamp字段表示最后一次检测时间,这条更新语句类似于:
+
mysql> update mysql.health_check set t_modified=now();
+
+
主库和备库用同样的更新语句可能会出现行冲突,导致主备同步停止,所以mysql.health_check表不能只有一行数据
+
mysql> CREATE TABLE `health_check` (
+ `id` int(11) NOT NULL,
+ `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
+
+
+insert into mysql.health_check(id, t_modified) values (@@server_id, now()) on duplicate key update t_modified=now();
+
+
MySQL规定主备的server_id必须不同,这样主备各自的检测命令就不会冲突
+
更新判断存在的问题是“判定慢”。因为更新语句在IO负载已经100%时仍然可能在超时前完成。检测系统看到update命令没有超时,就认为“系统正常”,但实际上正常SQL语句已经很慢了
+
内部统计
+
前面几种方法都是外部检测,外部检测都需要定时轮询,所以系统可能已经出问题了,但是却需要等到下一个检测发起执行语句的时候,才有可能发现问题,导致主备切换慢
+
针对磁盘利用率问题,MySQL 5.6 在file_summary_by_event_name表里统计了每次IO请求的时间,可以设置阈值作为检测逻辑
+
误删数据怎么办?
+
误删分为以下几类:
+
+- 使用delete误删数据行
+- 使用drop table或者truncate table误删数据表
+- 使用drop database误删数据库
+- 使用rm误删整个MySQL实例
+
+
误删行
+
可以使用Flashback恢复,原理是修改binlog的内容,拿回原库重放。使用这个方案的前提是确保binlog_format=row 和 binlog_row_image=FULL
+
建议在备库上执行,再恢复回主库
+
误删库/表
+
这种情况要求线上有定期的全量备份,并且实时备份binlog
+
假如有人中午12点误删了一个库,恢复数据的流程如下:
+
+- 取最近一次全量备份,假设这个库是一天一备,上次备份是当天0点;
+- 用备份恢复出一个临时库;
+- 从日志备份里面,取出凌晨0点之后的日志;
+- 把这些日志,除了误删除数据的语句外,全部应用到临时库
+
+
如果临时库有多个数据库,在使用mysqlbinlog时可以加上-database指定误删表所在库,加速数据恢复
+
在应用日志的时候,需要跳过12点误操作的那个语句的binlog:
+
+- 如果原实例没有使用GTID模式,只能在应用到包含12点的binlog文件的时候,先用–stop-position参数执行到误操作之前的日志,然后再用–start-position从误操作之后的日志继续执行;
+- 如果实例使用了GTID模式,就方便多了。假设误操作命令的GTID是gtid1,那么只需要执行set gtid_next=gtid1;begin;commit; 先把这个GTID加到临时实例的GTID集合,之后按顺序执行binlog的时候,就会自动跳过误操作的语句
+
+
即使这样,使用mysqlbinlog方法恢复数据仍然不快,因为:
+
+- mysqlbinlog并不能指定只解析一个表的日志
+- 用mysqlbinlog解析出日志应用,应用日志的过程就只能是单线程
+
+
一种加速方法是,在用备份恢复出临时实例之后,将这个临时实例设置成线上备库的从库,这样:
+
+- 在start slave之前,先通过执行
+change replication filter replicate_do_table = (tbl_name) 命令,就可以让临时库只同步误操作的表;
+- 这样做也可以用上并行复制技术,来加速整个数据恢复过程。
+
+
延迟复制备库
+
上面的方案存在“恢复时间不可控问题”,比如一周一备份,第6天误操作,那就需要恢复6天的日志,这个恢复时间可能按天计算
+
一般的主备复制结构存在的问题是,如果主库上有个表被误删了,这个命令很快也会被发给所有从库,进而导致所有从库的数据表也都一起被误删了。
+
延迟复制的备库是一种特殊的备库,通过 CHANGE MASTER TO MASTER_DELAY = N命令,可以指定这个备库持续保持跟主库有N秒的延迟。
+
比如你把N设置为3600,这就代表了如果主库上有数据被误删了,并且在1小时内发现了这个误操作命令,这个命令就还没有在这个延迟复制的备库执行。这时候到这个备库上执行stop slave,再通过之前介绍的方法,跳过误操作命令,就可以恢复出需要的数据。
+
这样的话,你就随时可以得到一个,只需要最多再追1小时,就可以恢复出数据的临时实例,也就缩短了整个数据恢复需要的时间
+
预防误删库/表的方法
+
+-
+
账号分离,避免写错命令
+
+- 只给业务开发同学DML权限,而不给truncate/drop权限。而如果业务开发人员有DDL需求的话,也可以通过开发管理系统得到支持
+- 即使是DBA团队成员,日常也都规定只使用只读账号,必要的时候才使用有更新权限的账号
+
+
+-
+
指定操作规范,避免写错要删除的表名
+
+- 删除数据表之前,必须先对表做改名操作。然后,观察一段时间,确保对业务无影响以后再删除这张表。
+- 改表名的时候,要求给表名加固定的后缀(比如加_to_be_deleted),然后删除表的动作必须通过管理系统执行。并且,管理系删除表的时候,只能删除固定后缀的表。
+
+
+
+
rm删除数据
+
对于有高可用机制的MySQL集群,最不怕rm。只要整个集群没被删掉,HA系统会选出新主库,保证整个集群正常工作。因此备库尽量跨机房、跨城市
+
为什么还有kill不掉的语句
+
MySQL有两个kill命令:
+
+- kill query+线程id,表示终止这个线程正在执行的语句
+- kill connection+线程id,connection可缺省,表示断开这个线程的连接,如果有语句正在执行,先停止语句
+
+
收到kill后,线程做什么
+
kill并不是马上停止,而是告诉线程,这条语句已经不需要继续执行了,可以开始“执行停止的逻辑了”
+
处理kill query命令的线程做了两件事:
+
+- 把目标线程的运行状态改成THD::KILL_QUERY(将变量killed赋值为THD::KILL_QUERY);
+- 给目标线程发一个信号,通知目标线程处理THD::KILL_QUERY状态。如果目标线程处于等待状态,必须是一个可以被唤醒的等待,否则不会执行到判断线程状态的“埋点”
+
+
处理kill connection命令的线程做了两件事:
+
+- 把目标线程状态设置为KILL_CONNECTION
+- 关闭目标线程的网络连接
+
+
kill无效的两类情况:
+
+- 线程没有执行到判断线程状态的逻辑。这种情况有innodb_thread_concurrency 不够用,IO压力过大
+- 终止逻辑耗时较长。这种情况有kill超大事务、回滚大查询、kill最后阶段的DDL命令
+
+
处于Killed状态的线程,你可以通过影响系统环境来让状态尽早结束。比如并发度不够导致线程没有执行到判断线程状态的逻辑,就增大innodb_thread_concurrency。除此之外,做不了什么,只能等流程自己结束
+
大查询会不会打爆内存
+
主机内存小于表的大小,全表扫描不会用光主机内存,否则逻辑备份早就挂了
+
全表扫描对server层的影响
+
假设对200G的表 db1.t 全表扫描,需要保留结果到客户端,会使用类似命令:
+
mysql -h$host -P$port -u$user -p$pwd -e "select * from db1.t" > $target_file
+
+
服务端不保存完整的查询结果集,取数据和发数据的流程是这样的:
+
+- 获取一行,写到net_buffer中
+- 重复获取行,直到net_buffer写满,调用网络接口发出去
+- 如果发送成功,就清空net_buffer,然后继续取下一行,并写入net_buffer
+- 如果发送函数返回EAGAIN或WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送
+
+
+
从这个流程可以看出:
+
+- 一个查询在发送过程中,占用的MySQL内部的内存最大就是net_buffer_length这么大,并不会达到200G;
+- socket send buffer 也不可能达到200G(默认定义/proc/sys/net/core/wmem_default),如果socket send buffer被写满,就会暂停读数据的流程。
+
+
全表扫描对InnoDB层的影响
+
数据页在Buffer Pool(BP)中管理,BP可以起到加速查询的作用,作用效果依赖于一个重要指标:内存命中率
+
BP的大小由参数 innodb_buffer_pool_size 确定,一般设置成可用物理内存的60%~80%
+
如果BP满了,要从磁盘读入一个数据页,就要淘汰一个旧数据页,InnoDB内存管理用的是改进后的最近最少使用(LRU)算法
+
+
上图head指向刚刚被访问过的数据页
+
基本的LRU算法在遇到全表扫描历史数据表时,会出现内存命中率急剧下降,磁盘压力增加,SQL响应变慢的情况
+
+
InnoDB按照 5:3 将LRU链表分成young区和old区,LRU_old指向old区域第一个位置,即整个链表的5/8处
+
改进后的LRU算法如下:
+
+- 访问young区域的数据页,和之前的算法一样,移动到链表头
+- 访问不在链表中的数据页,淘汰tail指向的最后一页,在LRU_old处插入新数据页
+- 访问old区域的数据页,若这个数据页在LRU链表中存在时间超过1s,就移动到链表头部,否则不动,1s由参数innodb_old_blocks_time控制
+
+
这个策略在扫描大表时不会对young区域造成影响,保证BP响应正常业务的查询命中率
+
可不可以使用join
+
先创建两个DDL一样的表
+
CREATE TABLE `t2` (
+ `id` int(11) NOT NULL,
+ `a` int(11) DEFAULT NULL,
+ `b` int(11) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `a` (`a`)
+) ENGINE=InnoDB;
+
+create table t1 like t2;
+insert into t1 (select * from t2 where id<=100)
+
+
Index Nested-Loop Join
+
有如下语句:
+
select * from t1 straight_join t2 on (t1.a=t2.a);
+
+
straight_join让MySQL使用固定的连接方式执行查询,这里t1是驱动表,t2是被驱动表
+
+
这个语句的执行流程如下:
+
+- 从表t1中读入一行数据R
+- 从数据行R中,取出a字段到表t2里去查
+- 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分
+- 重复执行步骤1到3,直到表t1的末尾循环结束
+
+
在形式上,这个过程和我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为“Index Nested-Loop Join”,简称NLJ
+
+
在流程里:
+
+- 对驱动表t1做了全表扫描,这个过程需要扫描100行
+- 对于每一行R,根据a字段去表t2查找,走的是树搜索过程。由于我们构造的数据是一一对应的,因此每次搜索都只扫描一行,也就是总共扫描100行
+- 所以,整个执行流程,总扫描行数是200
+
+
如果不用join,上面的连接需求,用单表查询实现的话,扫描行数一样,但是交互次数多,而且客户端要自己拼接SQL语句和结果,因此不如直接join
+
假设驱动表行数是N。被驱动表行数是M,被驱动表查一行数据要先走索引a,再走主键索引,因此时间复杂度是2∗log2M。驱动表要扫描N行,然后每行都要去被驱动表上匹配,所以整个执行过程复杂度是 N+N∗2∗log2M。显然N影响更大,因此让小表做驱动表
+
Simple Nested-Loop Join
+
现在语句改成如下:
+
select * from t1 straight_join t2 on (t1.a=t2.b);
+
+
由于t2的字段b没有索引,每次到t2去匹配都要做全表扫描,因此这个查询要扫描100*1000=10万行。
+
Block Nested-Loop Join
+
当被驱动表上没有可用索引,MySQL使用的算法流程如下:
+
+- 把表t1的数据读入线程内存join_buffer中,由于我们这个语句中写的是select *,因此是把整个表t1放入了内存;
+- 扫描表t2,把表t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回。
+
+
+
该算法和Simple Nested-Loop Join算法扫描的行数一样多,但该算法是内存操作,速度更快。碰到大表不能放入join_buffer的情况就分多次放
+
总结一下:
+
第一个问题:能不能使用join语句?
+
+- 如果可以使用Index Nested-Loop Join算法,也就是说可以用上被驱动表上的索引,其实是没问题的;
+- 如果使用Block Nested-Loop Join算法,扫描行数就会过多。尤其是在大表上的join操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种join尽量不要用
+
+
所以在判断要不要使用join语句时,就是看explain结果里面,Extra字段里面有没有出现“Block Nested Loop”字样
+
第二个问题:如果要使用join,应该选择大表做驱动表还是选择小表做驱动表?
+
总是使用小表做驱动表。更准确地说,在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表
+
join语句怎么优化
+
创建两个表t1、t2(id int primary key, a int, b int, index(a))。给表t1插入1000行数据,每一行a=1001-id,即字段a是逆序的。给表t2插入100万行数据
+
Multi-Range Read优化
+
现在有SQL语句:
+
select * from t1 where a>=1 and a<=100;
+
+
MRR优化的设计思路是:大多数的数据都是按照主键递增顺序插入的,所以按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。使用MRR的语句的执行流程如下:
+
+- 根据索引a,定位到满足条件的记录,将id值放入read_rnd_buffer中;
+- 将read_rnd_buffer中的id进行递增排序
+- 排序后的id数组,依次到主键id索引中查记录,并作为结果返回。
+
+
+
MRR能够提升性能的核心在于,这条查询语句在索引a上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键id。这样通过排序以后,再去主键索引查数据,才能体现出“顺序性”的优势
+
Batched Key Access
+
MySQL 5.6 引入Batched Key Acess(BKA)算法,这个算法是对NLJ算法的优化
+
NLJ算法执行的逻辑是:从驱动表t1,一行行地取出a的值,再到被驱动表t2去做join。也就是说,对于表t2来说,每次都是匹配一个值。这时,MRR的优势就用不上了
+
优化思路就是,从表t1里一次性多拿出些行,一起传给表t2。取出的数据先放到join_buffer
+
+
BNL算法的性能问题
+
+- 可能会多次扫描被驱动表,占用磁盘IO资源;
+- 判断join条件需要执行M*N次对比(M、N分别是两张表的行数),如果是大表就会占用非常多的CPU资源;
+- 可能会导致Buffer Pool的热数据被淘汰,影响内存命中率。
+
+
如果explain命令发现优化器使用BNL算法。我们就需要优化,常见做法是,给被驱动表的join字段加上索引,把BNL算法转成BKA算法
+
BNL转BKA
+
select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000;
+
+
在索引创建资源开销大情况下,可以考虑使用临时表:
+
+- 把表t2中满足条件的数据放在临时表tmp_t中;
+- 为了让join使用BKA算法,给临时表tmp_t的字段b加上索引;
+- 让表t1和tmp_t做join操作
+
+
对应的SQL语句:
+
create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
+insert into temp_t select * from t2 where b>=1 and b<=2000;
+select * from t1 join temp_t on (t1.b=temp_t.b);
+
+
扩展-hash join
+
BNL的问题是join_buffer里面维护的是一个无序数组,如果是一个hash表,可以大幅减少判断次数。可以在业务端实现这个优化:
+
+- select * from t1;取得表t1的全部1000行数据,在业务端存入一个hash结构
+- select * from t2 where b>=1 and b<=2000; 获取表t2中满足条件的2000行数据。
+- 把这2000行数据,一行一行地取到业务端,到hash结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行
+
+
为什么临时表可以重名
+
内存表和临时表的区别:
+
+- 内存表,指的是使用Memory引擎的表,建表语法是create table … engine=memory。这种表的数据都保存在内存里,系统重启的时候会被清空,但是表结构还在。除了这两个特性看上去比较“奇怪”外,从其他的特征上看,它就是一个正常的表
+- 临时表,可以使用各种引擎类型 。如果是使用InnoDB引擎或者MyISAM引擎的临时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用Memory引擎
+
+
临时表的特性
+
+
临时表在使用上有以下几个特点:
+
+- 建表语法是create temporary table …。
+- 一个临时表只能被创建它的session访问,对其他线程不可见。所以,图中session A创建的临时表t,对于session B就是不可见的。
+- 临时表可以与普通表同名
+- session A内有同名的临时表和普通表的时候,show create语句,以及增删改查语句访问的是临时表
+- show tables命令不显示临时表
+
+
临时表的应用
+
由于不用担心线程之间的重名冲突,临时表经常会被用在复杂查询的优化过程中。其中,分库分表系统的跨库查询就是一个典型的使用场景。
+
一般分库分表的场景,就是要把一个逻辑上的大表分散到不同的数据库实例上。比如。将一个大表ht,按照字段f,拆分成1024个分表,然后分布到32个数据库实例上。如下图所示:
+
+
分区key的选择是以“减少跨库和跨表查询”为依据的。如果大部分的语句都会包含f的等值条件,那么就要用f做分区键
+
比如:
+
select v from ht where f=N;
+
+
可以通过分表规则(比如,N%1024)来确认需要的数据被放在了哪个分表上
+
但是,如果这个表上还有另外一个索引k,并且查询语句是这样的:
+
select v from ht where k >= M order by t_modified desc limit 100;
+
+
由于查询条件里面没有用到分区字段f,只能到所有的分区中去查找满足条件的所有行,然后统一做order by 的操作。这种情况有两种思路:
+
+- 在proxy层的进程代码中实现排序。优势是快,缺点是工作量大,proxy端压力大
+- 把分库数据汇总到一个表中,再在汇总上操作。如下图所示
+
+
+
为什么临时表可以重名?
+
create temporary table temp_t(id int primary key)engine=innodb;
+
+
执行该语句,MySQL会创建一个frm文件保存表结构定义。该文件放在临时文件目录下,文件名的后缀是.frm,前缀是“#sql{进程id}_{线程id}_序列号”
+
+
除了文件名不同,内存里面也有一套机制区别不同的表,每个表都对应一个table_def_key
+
+- 一个普通表的table_def_key的值是由“库名+表名”得到的,所以如果你要在同一个库下创建两个同名的普通表,创建第二个表的过程中就会发现table_def_key已经存在了。
+- 而对于临时表,table_def_key在“库名+表名”基础上,又加入了“server_id+thread_id”
+
+
临时表和主备复制
+
如果当前的binlog_format=row,那么跟临时表有关的语句,就不会记录到binlog里
+
如果binlog_format=statment/mixed,创建临时表的语句会传到备库,由备库的同步线程执行。因为主库的线程退出时会自动删除临时表,但是备库同步线程是持续运行的,所以还需要在主库上再写一个DROP TEMPORARY TABLE传给备库执行
+
主库上不同线程创建同名的临时表是没关系的,但是传到备库怎么处理?
+
+
MySQL在记录binlog的时候,会把主库执行这个语句的线程id写到binlog中。这样,在备库的应用线程就能够知道执行每个语句的主库线程id,并利用这个线程id来构造临时表的table_def_key:
+
+- session A的临时表t1,在备库的table_def_key就是:库名+t1+“M的serverid”+“session A的thread_id”;
+- session B的临时表t1,在备库的table_def_key就是 :库名+t1+“M的serverid”+“session B的thread_id”
+
+
为什么会使用内部临时表
+
union 执行流程
+
假设有表t1:
+
create table t1(id int primary key, a int, b int, index(a));
+delimiter ;;
+create procedure idata()
+begin
+ declare i int;
+
+ set i=1;
+ while(i<=1000)do
+ insert into t1 values(i, i, i);
+ set i=i+1;
+ end while;
+end;;
+delimiter ;
+call idata();
+
+
然后执行:
+
(select 1000 as f) union (select id from t1 order by id desc limit 2);
+
+
这个语句的执行流程是这样的:
+
+-
+
创建一个内存临时表,这个临时表只有一个整型字段f,并且f是主键字段
+
+-
+
执行第一个子查询,得到1000这个值,并存入临时表中
+
+-
+
执行第二个子查询:
+
+- 拿到第一行id=1000,试图插入临时表中。但由于1000这个值已经存在于临时表了,违反了唯一性约束,所以插入失败,然后继续执行;
+- 取到第二行id=999,插入临时表成功
+
+
+-
+
从临时表中按行取出数据,返回结果,并删除临时表,结果中包含两行数据分别是1000和999
+
+
+
如果使用union all,就没有去重,执行的时候是依次执行子查询,得到的结果直接作为结果集的一部分,不需要临时表
+
group by 执行流程
+
select id%10 as m, count(*) as c from t1 group by m;
+
+
这个语句的执行流程如下:
+
+-
+
创建内存临时表,表里有两个字段m和c,主键是m
+
+-
+
扫描表t1的索引a,依次取出叶子节点上的id值,计算id%10的结果,记为x;
+
+- 如果临时表中没有主键为x的行,就插入一个记录(x,1);
+- 如果表中有主键为x的行,就将x这一行的c值加1
+
+
+-
+
遍历完成后,再根据字段m做排序,得到结果集返回给客户端
+
+
+
如果不需要排序,在语句末尾加上order by null
+
当内存临时表大小达到上限时,会转成磁盘临时表,磁盘临时表默认使用的引擎是InnoDB
+
group by 优化方法 --索引
+
新增一列,给这列加索引
+
alter table t1 add column z int generated always as(id % 100), add index(z);
+
+
对这列group by:
+
select z, count(*) as c from t1 group by z;
+
+
group by 优化方法 --直接排序
+
碰到不能加索引的场景就得老老实实做排序
+
在group by语句中加入SQL_BIG_RESULT这个提示(hint),就可以告诉优化器:这个语句涉及的数据量很大,请直接用磁盘临时表
+
select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;
+
+
这个语句的执行流程如下:
+
+- 初始化sort_buffer,确定放入一个整型字段,记为m
+- 扫描表t1的索引a,依次取出里面的id值, 将 id%100的值存入sort_buffer中
+- 扫描完成后,对sort_buffer的字段m做排序(如果sort_buffer内存不够用,就会利用磁盘临时文件辅助排序)
+- 排序完成后,就得到了一个有序数组。顺序扫描一遍就可以得到结果
+
+
基于上面的union、union all和group by语句的执行过程的分析,我们来回答文章开头的问题:MySQL什么时候会使用内部临时表?
+
+- 如果语句执行过程可以一边读数据,一边直接得到结果,是不需要额外内存的,否则就需要额外的内存,来保存中间结果
+- join_buffer是无序数组,sort_buffer是有序数组,临时表是二维表结构
+- 如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。比如我们的例子中,union需要用到唯一索引约束, group by还需要用到另外一个字段来存累积计数。
+
+
都说InnoDB好,那还要不要使用Memory引擎
+
内存表的数据组织结构
+
假设有两张表t1,t2,t1使用Memory引擎,t2使用InnoDB引擎
+
create table t1(id int primary key, c int) engine=Memory;
+create table t2(id int primary key, c int) engine=innodb;
+insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
+insert into t2 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
+
+
然后,分别执行select *from t1
和select* from t2
。t2表的(0,0)出现在第一行,t1表出现在最后一行
+
这是因为InnoDB引擎的数据就存在主键索引上,而主键索引是有序存储的,在执行select *的时候,就会按照叶子节点从左到右扫描,所以得到的结果里,0就出现在第一行
+
而Memory引擎的数据和索引是分开的。主键索引存的是每个数据的位置。执行select *走的是全表扫描数据数组
+
+
InnoDB和Memory引擎的数据组织方式是不同的:
+
+- InnoDB引擎把数据放在主键索引上,其他索引上保存的是主键id。这种方式,我们称之为索引组织表
+- Memory引擎采用的是把数据单独存放,索引上保存数据位置的数据组织形式,我们称之为堆组织表
+
+
两个引擎的一些典型不同:
+
+- InnoDB表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的
+- 当数据文件有空洞的时候,InnoDB表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值;
+- 数据位置发生变化的时候,InnoDB表只需要修改主键索引,而内存表需要修改所有索引
+- InnoDB表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的
+- InnoDB支持变长数据类型,不同记录的长度可能不同;内存表不支持Blob 和 Text字段,并且即使定义了varchar(N),实际也当作char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同。
+
+
hash索引和B-Tree索引
+
内存表的范围查询不能走主键索引,但是可以加一个B-Tree索引,B-Tree索引类似于InnoDB的B+树索引
+
alter table t1 add index a_btree_index using btree (id);
+
+
+
+
不建议在生产环境使用内存表,原因有两方面:
+
+- 锁粒度问题。内存表不支持行锁,只支持表锁
+- 数据持久化问题
+
+
自增主键为什么不是连续的
+
自增主键可以让主键索引尽量保持递增顺序插入,避免页分裂,因此索引更紧凑,但自增主键不能保证连续递增
+
自增值保存在哪?
+
InnoDB的自增值保存在内存中。每次重启MySQL都会计算max(id)+1作为自增值。8.0版本,重启的时候依靠redo log恢复自增值
+
自增值修改机制
+
假设,某次插入的值是X,当前的自增值是Y
+
+- 如果X < Y,那么自增值不变
+- 如果X >= Y,将当前自增值修改为新的自增值 Z = auto_increment_offset+k*auto_increment_increment。Z > X,auto_increment_offset是自增初始值,auto_increment_increment是自增步长,k是自然数
+
+
自增值的修改时机
+
自增值在真正执行插入数据的操作之前修改。如果因为唯一键冲突导致插入失败会出现id不连续,事务回滚也是类似现象
+
自增锁的优化
+
自增id锁并不是一个事务锁,而是每次申请完就马上释放,以便允许别的事务再申请。建议innodb_autoinc_lock_mode设置成2,即前面的策略,同时binlog_format=row,避免insert … select造成主备数据不一致
+
insert语句的锁为什么这么多
+
insert … select 语句
+
在可重复读隔离级别下,binlog_format=statement时,执行 insert … select 语句会对select表的需要访问的资源加锁。加锁是为了避免主备不一致
+
insert 循环写入
+
如果把select表的结果insert到select表中,会对select表全表扫描,创建一个临时表,再将select结果insert回表。这么做的原因是:这类一边遍历数据,一边更新数据的情况,如果读出来的数据直接写回原表,就可能在遍历过程中,读到刚刚插入的记录,新插入的记录如果参与计算逻辑,就跟语义不符
+
优化方法是:手动创建内存临时表,先 insert临时表select目标表,再 insert目标表select临时表,这样就不会对目标表全表扫描
+
insert 唯一键冲突
+
+
在session A执行rollback语句回滚的时候,session C几乎同时发现死锁并返回
+
这个死锁产生的逻辑是这样的:
+
+- 在T1时刻,启动session A,并执行insert语句,此时在索引c的c=5上加了记录锁。注意,这个索引是唯一索引,因此退化为记录锁
+- 在T2时刻,session B要执行相同的insert语句,发现了唯一键冲突,加上读锁;同样地,session C也在索引c上,c=5这一个记录上,加了读锁(共享next-key lock)
+- T3时刻,session A回滚。这时候,session B和session C都试图继续执行插入操作,都要加上写锁(排它next-key lock)。两个session都要等待对方的行锁,所以就出现了死锁
+
+
+
insert into … on duplicate key update
+
这个语义的逻辑是,插入一行数据,如果碰到唯一键约束,就执行后面的更新语句。它给唯一索引加排它的next-key lock(写锁)
+
怎么最快地复制一张表
+
如果可以控制对原表的扫描行数和加锁范围很小的话,可以直接用insert … select。否则先将数据写到外部文件,再写回目标表,方法有三种:
+
+-
+
物理拷贝的方式速度最快,尤其对于大表拷贝来说是最快的方法。如果出现误删表的情况,用备份恢复出误删之前的临时库,然后再把临时库中的表拷贝到生产库上,是恢复数据最快的方法。但是,这种方法的使用也有一定的局限性:
+
+- 必须是全表拷贝,不能只拷贝部分数据;
+- 需要到服务器上拷贝数据,在用户无法登录数据库主机的场景下无法使用;
+- 由于是通过拷贝物理文件实现的,源表和目标表都是使用InnoDB引擎时才能使用。
+
+
+-
+
用mysqldump生成包含INSERT语句文件的方法,可以在where参数增加过滤条件,来实现只导出部分数据。这个方式的不足之一是,不能使用join这种比较复杂的where条件写法
+
+-
+
用select … into outfile的方法是最灵活的,支持所有的SQL写法。但,这个方法的缺点之一就是,每次只能导出一张表的数据,而且表结构也需要另外的语句单独备份
+
+
+
grant之后要跟着flushprivileges吗
+
grant语句会同时修改数据表和内存,判断权限的时候使用的是内存数据。因此,规范地使用grant和revoke语句,是不需要随后加上flush privileges语句的。
+
flush privileges语句本身会用数据表的数据重建一份内存权限数据,所以在权限数据可能存在不一致的情况下再使用。而这种不一致往往是由于直接用DML语句操作系统权限表导致的,所以我们尽量不要使用这类语句。
+
要不要使用分区表
+
相对于用户分表:
+
优势:对业务透明,使用分区表的业务代码更简洁,且可以很方便的清理历史数据
+劣势:第一次访问的时候需要访问所有分区;共用MDL锁
+
+
+
+