MySQL基础架构
连接器
连接器负责跟客户端建立连接、获取权限、维持和管理连接。在完成经典的 TCP 握手后,连接器就要开始认证你的身份,这个时候用的就是你输入的用户名和密码。如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。这就意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。
相关命令:show processlist
分析器
优化器
执行器
MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示 (在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。
日志系统
redo log
当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把对应数据记录更新到数据文件。write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先进行刷盘,然后擦掉对应的redo log记录,把 checkpoint 推进一下。有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
bin log
为什么会有两份日志呢?因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
这两种日志有以下三点不同:
- redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
- redo log 是物理日志;binlog 是逻辑日志,记录的是这个语句的原始逻辑。
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
有了对这两个日志的概念性理解,我们再来看执行器和 InnoDB 引擎在执行这个简单的 update 语句(update T set c=c+1 where ID=2
)时的内部流程。
- 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
- 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
如上图,是更新一条数据的流程,红色的圈中有三个异常点。
异常1发生时,也就是写入redo log失败,这个时候redo和binlog都没有,事务未提交,无任何影响。
异常2发生时,redo log写入成功,binlog写入失败,这个时候MySQL服务器重启后,需要检查binlog是否完整包含此条redo log的更新内容(通过全局事务ID对应),发现binlog中还未包含此事务变更,则丢弃此次变更。
异常3发生时,redo log和binlog都写入成功,但提交事务时MySQL进程崩溃了,那么服务器重启后,检测到redo log和binlog都有,重新提交事务。
相关参数:innodb_flush_log_at_trx_commit
这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数建议设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失,也可以设置为2。
sync_binlog
这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数建议设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。
事务隔离
MVCC:
RC:在事务里,每次使用普通select时,会生成一个ReadView,里面有4个重要的字段:
m_ids:存储当前活跃的事务ID
min_trx_id:m_ids中最小的事务ID
max_trx_id:下一个即将分配的事务ID
creator_trx_id:生成当前ReadView的事务ID
如果待查询记录的事务ID等于creator_trx_id,则该记录可见;如果事务ID小于min_trx_id,则可见;如果事务ID大于等于max_trx_id,则不可见;除此之外,若事务ID存在于mid_s,则不可见,反之可见。
RR:只会在第一次select生成ReadView,而RC是每次select都会生成一个新的ReadView。使用“可重复读”隔离级别,事务启动时的视图可以认为是静态的,不受其他事务更新的影响。
查看当前隔离级别:show variables like 'transaction_isolation';
尽量不要使用长事务。长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库。
tip:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read);当前读总是读取已经提交完成的最新版本。
MySQL 的事务启动方式有以下几种:
- 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。
- set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
建议总是使用 set autocommit=1, 通过显式语句的方式来启动事务。
可以在 information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务:select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
索引
三星索引:
第一颗星需要取出所有等值谓词中的列,作为索引开头的最开始的列(任意顺序);
第二颗星需要将 ORDER BY 列加入索引中;
第三颗星需要将查询语句剩余的列全部加入到索引中;
最左前缀原则:
检查索引列,从左到右依次检查索引列,查看以下规则:
在where子句中,该列是否至少拥有一个等值谓词与之对应?如果有,则这个列就是匹配列。如果没有,那么这个列及其后面的索引列都是非匹配列。
谓词是否是一个范围谓词,如果是,那么剩余的索引列都是非匹配列。
对于最后一个匹配列之后的索引列,如果拥有一个足够简单的谓词与其对应,那么该列为过滤列。
锁
全局锁
全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都 select 出来存成文本。以前有一种做法,是通过 FTWRL 确保不会有其他线程对数据库做更新,然后对整个库做备份。注意,在备份过程中整个库完全处于只读状态。
MySQL自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数single-transaction
的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。
全局锁主要用在逻辑备份过程中。对于全部是 InnoDB 引擎的库,建议选择使用single-transaction参数,对应用会更友好。
表级锁
表锁
表锁的语法是 lock tables … read/write。与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象:对表加读锁后,自己也不能对其进行修改;自己和其他线程只能读取该表。 当对某个表执加上写锁后(lock table t2 write),该线程可以对这个表进行读写,其他线程对该表的读和写都受到阻塞;
元数据锁(meta data lock,MDL)
在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
元数据锁是server层的锁,表级锁,主要用于隔离DML(Data Manipulation Language,数据操纵语言,如select)和DDL(Data Definition Language,数据定义语言,如改表头新增一列)操作之间的干扰。每执行一条DML、DDL语句时都会申请MDL锁,DML操作需要MDL读锁,DDL操作需要MDL写锁(MDL加锁过程是系统自动控制,无法直接干预,读读共享,读写互斥,写写互斥)
申请MDL锁的操作会形成一个队列,队列中写锁获取优先级高于读锁。一旦出现写锁等待,不但当前操作会被阻塞,同时还会阻塞后续该表的所有操作。
事务中的 MDL 锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。
如何安全地给小表加字段?:
首先我们要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。
但考虑一下这个场景。如果你要变更的表是一个热点表,虽然数据量不大,但是上面的请求很频繁,而你不得不加个字段,你该怎么做呢?这时候 kill 可能未必管用,因为新的请求马上就来了。比较理想的机制是,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者 DBA 再通过重试命令重复这个过程。
意向锁
意向锁其实不会阻塞全表扫描之外的任何请求,它们的主要目的是为了表示是否有人请求锁定表中的某一行数据。
可以举一个例子:如果没有意向锁,当已经有人使用行锁对表中的某一行进行修改时,如果另外一个请求要对全表进行修改,那么就需要对所有的行是否被锁定进行扫描,在这种情况下,效率是非常低的;不过,在引入意向锁之后,当有人使用行锁对表中的某一行进行修改之前,会先为表添加意向互斥锁(IX),再为行记录添加互斥锁(X),在这时如果有人尝试对全表进行修改就不需要判断表中的每一行数据是否被加锁了,只需要通过等待意向互斥锁被释放就可以了。
行锁
两阶段锁
在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
死锁和死锁检测
当出现死锁以后,有两种策略:
一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout
来设置。
另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect
设置为 on,表示开启这个逻辑。
在 InnoDB 中,innodb_lock_wait_timeout 的默认值是 50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。
主动死锁检测,innodb_deadlock_detect 的默认值是 on。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的。
漫日志查询配置:
slow_query_log=on
long_query_time=1
slow_query_log_file=/path/to/log
相关问题
如何将数据库恢复到指定的某一时刻?
- 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
- 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。
定期全量备份的周期“取决于系统重要性,有的是一天一备,有的是一周一备”。那么在什么场景下,一天一备会比一周一备更有优势呢?或者说,它影响了这个数据库系统的哪个指标?
在一天一备的模式里,最坏情况下需要应用一天的 binlog。比如,你每天 0 点做一次全量备份,而要恢复出一个到昨天晚上 23 点的备份。一周一备最坏情况就要应用一周的 binlog 了。系统的对应指标就是 RTO(恢复目标时间)。当然这个是有成本的,因为更频繁全量备份需要消耗更多存储空间,所以这个 RTO 是成本换来的,就需要你根据业务重要性来评估了。
如何删除一个表里面的前 10000 行数据?
在一个连接中循环执行 20 次 delete from T limit 500
唯一索引与普通索引
查询时普通索引跟唯一索引执行上的区别: 普通索引的等值查询,会继续遍历到第一个不相等的值才会结束,而唯一索引等值查询,命中则结束(二者性能差距微乎其微)。
change buffer
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
需要说明的是,虽然名字叫作 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。
将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。
change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size
来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。
change buffer 只限于用在普通索引的场景下,而不适用于唯一索引。
对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。如果所有的更新后面,都马上伴随着对这个记录的查询,那么你应该关闭 change buffer(innodb_change_buffer_max_size
设置为0)。
InnoDB 刷脏页
根据上述算得的 F1(M) 和 F2(N) 两个值,取其中较大的值记为 R,之后引擎就可以按照 innodb_io_capacity
定义的能力乘以 R% 来控制刷脏页的速度,这个参数建议设置成磁盘的 IOPS。
磁盘的 IOPS 可以通过 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
参数 innodb_max_dirty_pages_pct
是脏页比例上限,默认值是 75%。InnoDB 会根据当前的脏页比例(假设为 M),算出一个范围在 0 到 100 之间的数字,计算这个数字的伪代码类似这样:
F1(M)
{
if M>=innodb_max_dirty_pages_pct then
return 100;
return 100*M/innodb_max_dirty_pages_pct;
}
InnoDB在刷脏页时还有一个机制:在准备刷一个脏页的时候,如果这个数据页旁边的数据页刚好是脏页,就会把这个“邻居”也带着一起刷掉;而且这个把“邻居”拖下水的逻辑还可以继续蔓延,也就是对于每个邻居数据页,如果跟它相邻的数据页也还是脏页的话,也会被放到一起刷。innodb_flush_neighbors
参数就是用来控制这个行为的,值为 1 的时候会有上述的“连坐”机制,值为 0 时表示不找邻居,自己刷自己的。如果使用的是 SSD 这类 IOPS 比较高的设备的话,建议把 innodb_flush_neighbors 的值设置成 0。因为这时候 IOPS 往往不是瓶颈,而“只刷自己”,就能更快地执行完必要的刷脏页操作,减少 SQL 语句响应时间。
什么时候会触发flush:1.redo log日志写满了; 2.有新的内存也需要读入内存而内存不够用了,这时候需要刷脏页; 3.系统空闲的时候; 4.mysql正常关闭的时候强制刷新脏页。
而第一种情况是要避免的,因为出现这种情况的时候,整个系统就不能再接受更新了,所有的更新都必须堵住。如果你从监控上看,这时候更新数会跌为 0。第二种情况其实是常态,但如果一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长,也要尽量避免;
一个内存配置为 128GB、innodb_io_capacity 设置为 20000 的大规格实例,正常会建议你将 redo log 设置成 4 个 1GB 的文件。
重建表
1:为啥删除了表的一半数据,表文文件大小没变化?
因为delete 命令其实只是把记录的位置,或者数据页标记为了“可复用”,但磁盘文件的大小是不会变的。也可以认为是一种逻辑删除,所以物理空间没有实际释放,只是标记为可复用,表文件的大小当然是不变的!
2:表的数据信息存在哪里?
表数据信息可能较小也可能巨大无比,可以存储在共享表空间里,也可以单独存储在一个以.ibd为后缀的文件里,由参数innodb_file_per_table
来控制,建议总是作为一个单独的文件来存储,这样非常容易管理,并且在不需要的时候,使用drop table命令也能直接把对应的文件删除,如果存储在共享空间之中即使表删除了空间也不会释放。
实际上不只是删除数据会造成“空洞”,如果插入数据不是按照聚集索引递增的顺序插入的,可能会造成数据页分裂,进而造成“空洞”。
也就是说,经过大量增删改的表,都是可能是存在空洞的。所以,如果能够把这些空洞去掉,就能达到收缩表空间的目的。而重建表,就可以达到这样的目的。
重建表命令:alter table A engine=InnoDB
在MySQL5.5之前,这个重建过程是离线的,也就是说在这个过程中不能有新数据写入,否则会造成数据丢失。
而在MySQL5.6开始引入了Online DDL,对这个造作流程做了优化。
COUNT(*)
按照效率排序的话,count(字段)<count(主键 id)<count(1)≈count(),所以尽量使用 count()。
oder by
MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。而按排序这个动作,可能在内存中完成,也可能需要使用外部排序,这取决于排序所需的内存和参数sort_buffer_size
。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
如果待排序行的长度超过max_length_for_sort_data
,则MySQL会采用rowid 排序。
如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
可以看出,排序是有消耗的,但如果数据在磁盘本身就是按序存储的,则不需要再排序了,见三星索引。
Next Key
主从备份
从库建议设置成只读模式。因为 readonly 设置对超级 (super) 权限用户是无效的,而用于同步更新的线程,就拥有超级权限,所以不用担心从库设置成只读模式后无法同步更新。
备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。一个事务日志同步的完整过程是这样的:
- 设置主库相关配置文件,主要是server-id和log-bin,然后重启。
- 登录主库,创建slave账号,并进行相关授权:
CREATE USER 'slave'@'%' IDENTIFIED BY '123456'; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'slave'@'%';
- 设置备库相关配置文件,主要是server-id和log-bin、relay_log,然后重启。
- 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量:
change master to master_host='172.17.0.2', master_user='slave', master_password='123456', master_port=3306, master_log_file='mysql-bin.000001', master_log_pos= 2830, master_connect_retry=30;
。 - 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接。
- 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
- 备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
- sql_thread 读取中转日志,解析出日志里的命令,并执行。
这里需要说明,后来由于多线程复制方案的引入,sql_thread 演化成为了多个线程。
==========以下是主库配置文件中的信息=============
# 配置编码为utf8
character_set_server=utf8mb4
init_connect='SET NAMES utf8mb4'
# 配置要给Slave同步的数据库
binlog-do-db=test
# 不用给Slave同步的数据库,一般是Mysql自带的数据库就不用给Slave同步了
binlog-ignore-db=mysql
binlog-ignore-db=information_schema
binlog-ignore-db=performance_schema
binlog-ignore-db=sys
# 自动清理30天前的log文件
expire_logs_days=30
# 启用二进制日志
log-bin=mysql-bin
# Master的id,这个要唯一,唯一是值,在主从中唯一
server-id=3
binlog 的三种格式:
statement、row、mixed
statement 格式的 binlog 可能会导致主备不一致,row 格式的缺点是很占空间,mixed是先判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式。
如果线上 MySQL 设置的 binlog 格式是 statement 的话,那基本上就可以认为这是一个不合理的设置,现在越来越多的场景要求把 MySQL 的 binlog 格式设置成 row。
实际生产上使用比较多的是互为主备。但互为主备有一个binlog循环复制的问题。(建议把参数log_slave_updates
设置为 on,表示备库执行 relay log 后生成 binlog)。解决方法是:
- 从节点 A 更新的事务,binlog 里面记的都是 A 的 server id;
- 传到节点 B 执行一次以后,节点 B 生成的 binlog 的 server id 也是 A 的 server id;
- 再传回给节点 A,A 判断到这个 server id 与自己的相同,就不会再处理这个日志。所以,死循环在这里就断掉了。
主备延迟
可以在备库上执行 show slave status
命令,它的返回结果里面会显示 seconds_behind_master
,用于表示当前备库延迟了多少秒。网络正常情况下,主备延迟的主要来源是备库从接收完 binlog起到执行完这个事务之间的时间差。造成主备延迟的原因可能有一下:
- 大家都把查询链接打到了备库上,导致备库cpu增高,同步binlog变慢。解决办法可以是一主多从,除了备库外,可以多接几个从库,让这些从库来分担读的压力。或者增加缓存中间件。
- 长事务也可能造成延迟,要避免长事务。
- 如果在进行大表的DDL也会造成延迟。
在官方的 5.6 版本之前,MySQL 只支持单线程复制,由此在主库并发高、TPS 高时就会出现严重的主备延迟问题。之后就是多线程复制,线程个数由参数slave_parallel_workers
决定,根据经验,把这个值设置为 8~16 之间最好(32 核物理机的情况),毕竟备库还有可能要提供读查询,不能把 CPU 都吃光了。
主备切换:
- 判断备库 B 现在的 seconds_behind_master,如果小于某个值(比如 5 秒)继续下一步,否则持续重试这一步;
- 把主库 A 改成只读状态,即把 readonly 设置为 true(
set global read_only=1;
); - 判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止;
- 把备库 B 改成可读写状态,也就是把 readonly 设置为 false;
- 把业务请求切到备库 B。
可以看到,这个切换流程中是有不可用时间的。因为在步骤 2 之后,主库 A 和备库 B 都处于 readonly 状态,也就是说这时系统处于不可写状态,直到步骤 5 完成后才能恢复。
在这个不可用状态中,比较耗费时间的是步骤 3,可能需要耗费好几秒的时间。这也是为什么需要在步骤 1 先做判断,确保 seconds_behind_master 的值足够小。试想如果一开始主备延迟就长达 30 分钟,而不先做判断直接切换的话,系统的不可用时间就会长达 30 分钟,这种情况一般业务都是不可接受的。
GTID
GTID 的全称是 Global Transaction Identifier,也就是全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。它由两部分组成,格式是:
GTID=server_uuid:transaction_id
其中:server_uuid 是一个实例第一次启动时自动生成的,是一个全局唯一的值;transaction_id是一个整数,初始值是 1,每次提交事务的时候分配给这个事务,并加 1。
在 GTID 模式下,每个提交了的事务都会跟一个 GTID 一一对应。这样,每个 MySQL 实例都维护了一个 GTID 集合,用来对应“这个实例执行过的所有事务”。
这里需要注意的一点是,在MVCC里面我们说transaction_id 就是指事务 id,事务 id 是在事务执行过程中分配的,如果这个事务回滚了,事务 id 也会递增;而这里的transation_id是在事务提交的时候才会分配。
GTID 模式的启动也很简单,我们只需要在启动一个 MySQL 实例的时候,加上参数 gtid_mode=on
和 enforce_gtid_consistency=on
就可以了。
基于 GTID 的主备切换
由于不需要找位点了,所以从库 B、C、D 只需要分别执行 change master 命令指向实例 A’即可。
读写分离
- 客户端直连方案,因为少了一层 proxy 转发,所以查询性能稍微好一点儿,并且整体架构简单,排查问题更方便。但是这种方案,由于要了解后端部署细节,所以在出现主备切换、库迁移等操作的时候,客户端都会感知到,并且需要调整数据库连接信息。你可能会觉得这样客户端也太麻烦了,信息大量冗余,架构很丑。其实也未必,一般采用这样的架构,一定会伴随一个负责管理后端的组件,比如 Zookeeper,尽量让业务端只专注于业务逻辑开发。
- 带 proxy 的架构,对客户端比较友好。客户端不需要关注后端细节,连接维护、后端信息维护等工作,都是由 proxy 完成的。但这样的话,对后端维护团队的要求会更高。而且,proxy 也需要有高可用架构。因此,带 proxy 架构的整体就相对比较复杂。
理解了这两种方案的优劣,具体选择哪个方案就取决于数据库团队提供的能力了。但目前看,趋势是往带 proxy 的架构方向发展的。
不论使用哪种架构,都可能会碰到一个问题:由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态。
解决方案:
- 强制走主库方案;
- 对于必须要拿到最新结果的请求,强制将其发到主库上。比如,在一个交易平台上,卖家发布商品以后,马上要返回主页面,看商品是否发布成功。那么,这个请求需要拿到最新的结果,就必须走主库。
- 对于可以读到旧数据的请求,才将其发到从库上。在这个交易平台上,买家来逛商铺页面,就算晚几秒看到最新发布的商品,也是可以接受的。那么,这类请求就可以走从库。
- sleep 方案;
主库更新后,读从库之前先 sleep 一下。具体的方案就是,类似于执行一条 select sleep(1) 命令。 - 判断主备无延迟方案;
第一种确保主备无延迟的方法是,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为 0 才能执行查询请求。 - 配合 semi-sync 方案;
- 等主库位点方案;
- 等 GTID 方案。
在一主一备的双 M 架构里,主备切换只需要把客户端流量切到备库;而在一主多从架构里,主备切换除了要把客户端流量切到备库外,还需要把从库接到新主库上。
如何判断一个数据库是不是出问题了?
并发连接和并发查询:并发连接和并发查询,并不是同一个概念。你在show processlist
的结果里,看到的几千个连接,指的就是并发连接。而“当前正在执行”的语句,才是我们所说的并发查询。
innodb_thread_concurrency
参数的目的是,控制 InnoDB 的并发线程上限。也就是说,一旦并发线程数达到这个值,InnoDB 在接收到新请求的时候,就会进入等待状态,直到有线程退出。在 InnoDB 中innodb_thread_concurrency这个参数的默认值是 0,表示不限制并发线程数量。但是,不限制并发线程数肯定是不行的。因为,一个机器的 CPU 核数有限,线程全冲进来,上下文切换的成本就会太高。
通常情况下,建议把innodb_thread_concurrency设置为 64~128 之间的值。
SELECT 1方法::
问题:select 1 成功返回,只能说明这个库的进程还在,并不能说明主库没问题。
更新判断方法:
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 5.6 版本以后提供的performance_schema库,就在file_summary_by_event_name表里统计了每次 IO 请求的时间。
可以通过 MAX_TIMER_WAIT 的值来判断数据库是否出问题了。比如,你可以设定阈值,单次 IO 请求时间超过 200 毫秒属于异常,然后使用类似下面这条语句作为检测逻辑:
mysql> select event_name,MAX_TIMER_WAIT FROM performance_schema.file_summary_by_event_name where event_name in ('wait/io/file/innodb/innodb_log_file','wait/io/file/sql/binlog') and MAX_TIMER_WAIT>200*1000000000;
发现异常后,取到你需要的信息,再通过下面这条语句:
mysql> truncate table performance_schema.file_summary_by_event_name;
把之前的统计信息清空。这样如果后面的监控中,再次出现这个异常,就可以加入监控累积值了。
误删数据库、表
假如有人中午 12 点误删了一个库,恢复数据的流程如下:
- 取最近一次全量备份,假设这个库是一天一备,上次备份是当天 0 点;
- 用备份恢复出一个临时库;
- 从日志备份里面,取出凌晨 0 点之后的日志;
- 把这些日志,除了12点误删除数据的语句外,全部应用到临时库。
删表规范:
- 在删除数据表之前,必须先对表做改名操作。然后,观察一段时间,确保对业务无影响以后再删除这张表。
- 改表名的时候,要求给表名加固定的后缀(比如加 _to_be_deleted),然后删除表的动作必须通过管理系统执行。并且,管理系删除表的时候,只能删除固定后缀的表。
权限
可以用show grants
命令查看账户的权限。
正常情况下,grant 命令之后,没有必要跟着执行 flush privileges 命令。