MySQL深刻学习总结

MySQL简介

  • 关于MySQL发音的官方答案:
    The official way to pronounce “MySQL” is “My Ess Que Ell” (not “my sequel”), but we do not mind if you pronounce it as “my sequel” or in some other localized way.

        MySQL 能够分为 Server 层和存储引擎层两部分。mysql

        Server层包括链接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及全部的内置函数(如日期、时间、数学和加密函数等),全部跨存储引擎的功能都在这一层实现,好比存储过程、触发器、视图等。算法

        存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。如今最经常使用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认存储引擎。create table 语句中使用 engine=memory,来指定使用内存引擎建立表。sql

        如今最经常使用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认存储引擎。create table 语句中使用 engine=memory, 来指定使用内存引擎建立表。数据库

查询语句执行过程

clipboard.png

链接器

        第一步,链接器链接到数据库,链接器负责跟客户端创建链接、获取权限、维持和管理链接。后端

链接命令通常是这么写的:mysql -h$ip -P$port -u$user -p$password

帐号密码错误会报错:Access denied for user数组

        链接完成后,若是没有后续的动做,这个链接就处于空闲状态,能够在show processlist命令中看到它。文本中这个图是show processlist的结果,其中的Command列显示为"Sleep"的这一行,就表示如今系统里面有一个空闲链接。缓存

clipboard.png

        客户端若是太长时间没动静,链接器就会自动将它断开。这个时间是由参数wait timeout控制的,默认值是8小时。安全

断开后再执行sql会报错:Lost connection to MySQL server during query

        创建链接的过程一般是比较复杂的,因此建议在使用中要尽可能减小创建链接的动做,也就是尽可能使用长链接。性能优化

        可是 MySQL 在执行过程当中临时使用的内存是管理在链接对象里面的。这些资源会在链接断开的时候才释放。因此若是长链接累积下来,可能致使内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启。网络

怎么解决这个问题呢?能够考虑如下两种方案。

  1. 按期断开长链接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开链接,以后要查询再重连。
  2. 若是用的是MySQL 5.7或更新版本,能够在每次执行一个比较大的操做后,经过执行mysql reset connection来从新初始化链接资源。这个过程不须要重连和从新作权限验证可是会将链接恢复到刚刚建立完时的状态。

查询缓存

        第二步,查询语句会先查询缓存,以前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。

        可是查询缓存利大于弊,由于查询缓存的失效很是频繁,只要有对一个表的更新,这个表上全部的查询缓存都会被清空。

        除非是静态配置表才适合用查询缓存。能够将参数 query_cache_type 设置成DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。SQL_CACHE 显式指定使用查询缓存。

select SQL_CACHE * from T where ID=10;

        可是,MySQL 8.0版本完全删除了查询缓存功能。

分析器

        第三步,分析语句,先是词法分析,找出select,表名,列名等关键字;而后是语法分析,判断语法是否正确。表名列名不对的sql,会在语法分析时报错。

语法错误:ERROR 1064 (42000): You have an error in your SQL syntax;

优化器

        第四步,决定使用哪一个索引,join的时候决定各个表的链接顺序。

执行器

        第五步,先判断对当前表是否有权限(若是命中查询缓存,会在返回结果时验证权限)。

ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'

        如:select * from T where ID=10; 执行过程

  1. 调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是否是 10,若是不是则跳过,若是是则将这行存在结果集中;
  2. 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
  3. 执行器将上述遍历过程当中全部知足条件的行组成的记录集做为结果集返回给客户端。

        慢查询日志中有一行 rows_examined 字段,表示这个语句执行过程当中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。可是引擎扫描行数跟 rows_examined 并非彻底相同的。

查询的数据如何返回

  • 对一个200G的大表作全表扫描,而内存只有16G,会不会把数据库主机的内存用光了?

    实际上,MySQL不是取到所有数据再返回客户端。取数据和发数据的流程是这样的:

    1. 获取一行,写到 net_buffer 中。这块内存的大小是由参数 net_buffer_length 定义的,默认是 16k。
    2. 重复获取行,直到 net_buffer 写满,调用网络接口发出去。
    3. 若是发送成功,就清空 net_buffer,而后继续取下一行,并写入 net_buffer。
    4. 若是发送函数返回 EAGAIN 或 WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈从新可写,再继续发送。
  • MySQL 客户端发送请求后,接收服务端返回结果的方式有两种:

    1. 一种是本地缓存,也就是在本地开一片内存,先把结果存起来。若是用 API 开发,对应的就是 mysql_store_result 方法。
    2. 另外一种是不缓存,读一个处理一个。若是用 API 开发,对应的就是 mysql_use_result 方法。

      MySQL 客户端默认采用第一种方式,而若是加上–quick 参数,就会使用第二种不缓存的方式。

      采用不缓存的方式时,若是本地处理得慢,就会致使服务端发送结果被阻塞,所以会让服务端变慢。

        MySQL 是“边读边发的”。这就意味着,若是客户端接收得慢,会致使 MySQL 服务端因为结果发不出去,这个事务的执行时间变长。

        对于正常的线上业务来讲,若是一个查询的返回结果不会不少的话,都建议使用 mysql_store_result 这个接口,直接把查询结果保存到本地内存。

更新语句执行过程

        更新语句一样会走链接器,查询缓存(清空该表缓存),分析器,优化器这一套流程,与查询流程不同的是,更新流程还涉及两个重要的日志模块,redo log(重作日志)和 binlog(归档日志)。

重作日志:redo log

        若是每一次的更新操做都须要写进磁盘,而后磁盘也要找到对应的那条记录,而后再更新,整个过程 IO 成本、查找成本都很高。

        MySQL采用了WAL技术,全称是 Write-Ahead Logging,的关键点就是先写日志,再写磁盘。

        具体来讲,当有一条记录须要更新的时候,InnoDB 引擎就会先把记录写到 redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操做记录更新到磁盘里面,而这个更新每每是在系统比较空闲的时候作。

        可是若是 InnoDB 的 redo log 写满了。这时候系统会中止全部更新操做,把 checkpoint 往前推动(对应的全部脏页都 flush 到磁盘上),redo log 留出空间能够继续写。

        一旦一个查询请求须要在执行过程当中先 flush 掉一个脏页时,这个查询就可能要比平时慢了。因为刷脏页的逻辑会占用 IO 资源并可能影响到了更新语句,要尽可能避免这种状况,就要合理地设置 innodb_io_capacity 的值,而且平时要多关注脏页比例,不要让它常常接近 75%。脏页比例是经过 Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total 获得的,具体的命令参考下面代码:

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;

在 InnoDB 中,innodb_flush_neighbors 参数就是用来控制这个行为的,值为 1 的时候会有“连坐”机制,值为 0 时表示不找邻居,本身刷本身的。固态硬盘建议设置为0。

        InnoDB 的 redo log 是能够配置的固定大小,好比能够配置为一组 4 个文件,每一个文件的大小是 1GB,总共就能够记录 4GB 的操做。从头开始写,写到末尾就又回到开头循环写,以下面这个图所示。若是redo log 设置的过小,磁盘压力很小,可是数据库出现间歇性的性能下跌。
clipboard.png

        write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是日后推移而且循环的,擦除记录前要把记录更新到数据文件。

        write pos 和 checkpoint 之间的是还空着的部分,能够用来记录新的操做。若是 write pos 追上 checkpoint,这时候就得停下来先擦掉一些记录,把 checkpoint 推动一下。

        有了 redo log,InnoDB 就能够保证即便数据库发生异常重启,以前提交的记录都不会丢失,这个能力称为crash-safe。

        redo log buffer :插入数据的过程当中,生成的日志都得先保存起来,但又不能在还没 commit 的时候就直接写到 redo log 文件里。因此,redo log buffer 就是一块内存,用来先存 redo 日志的。也就是说,在执行第一个 insert 的时候,数据的内存被修改了,在执行 commit 的时候 redo log buffer 才写入了日志。

为了控制 redo log 的写入策略,innodb_flush_log_at_trx_commit 参数,它有三种可能取值:

  1. 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;
  2. 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;
  3. 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。

        InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,而后调用 fsync 持久化到磁盘。也就是说,一个没有提交的事务的 redo log,也是可能已经持久化到磁盘的。

还有两种场景也会把没有提交的redo log 写到硬盘。

  1. redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动写盘。注意,因为这个事务并无提交,因此这个写盘动做只是 write,而没有调用 fsync,也就是只留在了文件系统的 page cache。
  2. 并行的事务提交的时候,顺带将这个事务的 redo log buffer 持久化到磁盘。假设一个事务 A 执行到一半,另外一个事务B提交,事务B要把 redo log buffer 里的日志所有持久化到磁盘。

归档日志:binlog

redo log 是 InnoDB 引擎特有的日志,而 Server 层也有本身的日志,称为 binlog(归档日志)。

binlog 的三种格式对比:
        statement:记录到 binlog 里的是语句原文,最后会有 COMMIT;可能会致使主备不一致,由于limit 、等sql 执行时可能主备优化器选择的索引不同,排序也不同。now()执行的结果也不同。
        row :记录了操做的事件每一条数据的变化状况,最后会有一个 XID event。缺点是太占空间。
        mixed:同时使用两种格式,由数据库判断具体某条sql使用哪一种格式。可是有选择错误的状况。

这两种日志有如下三点不一样。

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,全部引擎均可以使用。
  2. redo log 是物理日志,记录的是“在某个数据页上作了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,好比“给 ID=2 这一行的 c 字段加 1 ”。
  3. redo log 是循环写的,空间固定会用完;binlog 是能够追加写入的。追加写”是指 binlog 文件写到必定大小后会切换到下一个,并不会覆盖之前的日志。

redo log 和 binlog 是怎么关联起来的?
        它们有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log:

  • 若是碰到既有 prepare、又有 commit 的 redo log,就直接提交;
  • 若是碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。

        处于 prepare 阶段的 redo log 加上完整 binlog,重启也能恢复,由于 binlog 完整了,那么从库就同步过去了,为了保证主从一致,有完整的 binlog 就算成功。

        事务执行过程当中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。

write 和 fsync 的时机,是由参数 sync_binlog 控制的:

  1. sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;
  2. sync_binlog=1 的时候,表示每次提交务都会执行 fsync;
  3. sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
比较常见的是将其设置为 100~1000 中的某个数值。对应的风险是:若是主机发生异常重启,会丢失最近 N 个事务的 binlog 日志。

更新语句执行过程

好比:update T set c=c+1 where ID=2;

  1. 执行器先找引擎取 ID=2 这一行。若是 ID=2 这一行所在的数据页原本就在内存中,就直接返回给执行器;不然,须要先从磁盘读入内存,而后再返回。
  2. 执行器拿到引擎给的行数据,把这个值加上 1,获得新的一行数据,再调用引擎接口写入这行新数据。
  3. 引擎将这行新数据更新到内存中,同时将这个更新操做记录到 redo log 里面,此时 redo log 处于 prepare 状态。而后告知执行器执行完成了,随时能够提交事务。
  4. 执行器生成这个操做的 binlog,并把 binlog 写入磁盘。
  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改为提交(commit)状态,更新完成。

        这里给出这个 update 语句的执行流程图,图中浅色框表示是在 InnoDB 内部执行的,深色框表示是在执行器中执行的。其实就是把redo log 和binlog 作两阶段提交,为了让两份日志之间的逻辑一致。
clipboard.png

备份恢复

保存必定时间的binlog,同时系统会按期作整库备份。

当须要恢复到指定的某一秒时,

  1. 首先,找到最近的一次全量备份,若是运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库
  2. 而后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到指定的那个时刻。

        redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数建议设置成 1,这样能够保证 MySQL 异常重启以后数据不丢失。

        binlog用于备份恢复和从库同步。sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数也建议设置成 1,这样能够保证 MySQL 异常重启以后 binlog 不丢失。

主备同步

  1. 在备库 B 上经过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪一个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
  2. 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库创建链接。
  3. 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
  4. 备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
  5. sql_thread 读取中转日志,解析出日志里的命令,并执行。

clipboard.png
        一主一备结构,须要注意主备切换,备库设置只读,避免切换bug形成双写不一致问题(设置 readonly 对超级用户是无效的,同步更新的线程有超级权限,因此还能写入同步数据)。

        双主结构,要避免循环更新问题,由于MySQL 在 binlog 中记录了这个命令第一次执行时所在实例的 server id。因此能够规定两个库的 server id 必须不一样,每一个库在收到从本身的主库发过来的日志后,先判断 server id,若是跟本身的相同,表示这个日志是本身生成的,就直接丢弃这个日志。

主备延迟

        能够在备库上执行 show slave status 命令,它的返回结果里面会显示 seconds_behind_master,用于表示当前备库延迟了多少秒。每一个事务的 binlog 里面都有一个时间字段,用于记录主库上写入的时间; 备库取出当前正在执行的事务的时间字段的值,计算它与当前系统时间的差值,获得 seconds_behind_master。

主备延迟最直接的表现是,备库消费中转日志(relay log)的速度,比主库生产 binlog 的速度要慢。

主备延迟的来源

  1. 有些部署条件下,备库所在机器的性能要比主库所在的机器性能差。
  2. 考虑到主备切换,主备机器通常都同样了,可是还可能备库读的压力太大,

    一主多从,或者经过binlog输出到外部系统(好比Hadoop),让外部系统提供部分统计查询能力。
  3. 大事务,若是事务执行十分钟,那就会致使主从延迟十分钟。

主备复制策略

        在官方的 5.6 版本以前,MySQL 只支持单线程复制,由此在主库并发高、TPS 高时就会出现严重的主备延迟问题。

        并行复制策略有按表并行分发策略,按行并行分发策略,可是按行分发在决定线程分发的时候,须要消耗更多的计算资源。这两个方案其实都有一些约束条件:

  1. 要可以从 binlog 里面解析出表名、主键值和惟一索引的值。也就是说,主库的 binlog 格式必须是 row;
  2. 表必须有主键;
  3. 不能有外键。表上若是有外键,级联更新的行不会记录在 binlog 中,这样冲突检测就不许确。

        官方 MySQL5.6 版本,支持了并行复制,只是支持的粒度是按库并行。相比于按表和按行分发,这个策略有两个优点:

  1. 构造 hash 值的时候很快,只须要库名;并且一个实例上 DB 数也不会不少,不会出现须要构造 100 万个项这种状况。
  2. 不要求 binlog 的格式。由于 statement 格式的 binlog 也能够很容易拿到库名。

        MariaDB 的并行复制策略,伪模拟主库并发度,主库 redo log 组提交 (group commit) 优化,同一组提交会记录commit_id,备库把同一个commit_id分发到多个worker执行。

官方的 MySQL5.7 版本,由参数 slave-parallel-type 来控制并行复制策略:

  1. 配置为 DATABASE,表示使用 MySQL 5.6 版本的按库并行策略;
  2. 配置为 LOGICAL_CLOCK,表示的就是相似 MariaDB 的策略。不过,MySQL 5.7 这个策略,针对并行度作了优化。

        MySQL 5.7.22 版本里,MySQL 增长了一个新的并行复制策略,基于 WRITESET 的并行复制。对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset。若是两个事务没有操做相同的行,也就是说它们的 writeset 没有交集,就能够并行。

读写分离

读写分离有两种方案:

  1. 客户端直连方案,由于少了一层 proxy 转发,因此查询性能稍微好一点儿,而且总体架构简单,排查问题更方便。可是这种方案,因为要了解后端部署细节,因此在出现主备切换、库迁移等做的时候,客户端都会感知到,而且须要调整数据库链接信息。 可能会以为这样客户端也太麻烦了,信息大量冗余,架构很丑。其实也未必,通常采用这样的架构,必定会伴随一个负责管理后端的组件,好比 Zookeeper,尽可能让业务端只专一于业务逻辑开发。
  2. 带 proxy 的架构,对客户端比较友好。客户端不须要关注后端细节,链接维护、后端信息维护等工做,都是由 proxy 完成的。但这样的话,对后端维护团队的要求会更高。并且,proxy 也须要有高可用架构。所以,带 proxy 架构的总体就相对比较复杂。

主从延迟的状况下怎么办?

  1. 强制走主库方案;对于必需要拿到最新结果的请求,强制将其发到主库上。
  2. sleep 方案;主库更新后,读从库以前先 sleep 一下。由于大多数状况下主备延迟在 1 秒以内。
  3. 判断主备无延迟方案; 每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0。若是还不等于 0 ,那就必须等到这个参数变为 0 才能执行查询请求。
  4. 配合 semi-sync 方案;半同步复制:

    1. 事务提交的时候,主库把 binlog 发给从库;
    2. 从库收到 binlog 之后,发回给主库一个 ack,表示收到了;
    3. 主库收到这个 ack 之后,才能给客户端客户端返回“事务完成”的确认。
  5. 等主库位点方案;
  6. 等 GTID 方案。

隔离级别

数据库特性

ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、、隔离性、持久性)。

        当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。

  • 脏读:指的是一个事务的读操做读到了另外一个未提交的事务修改的值。
  • 不可重复读:指的是一个事务读了同一个值两次,可是两次的值不一样,由于中间另外一个事务修改了这个值。
  • 幻读:仍然指的是一个事务中读了两次,结果不一样,可是与不可重复读不一样的是,这里不一样是由于别的事物作了插入操做,而是读的条件是一个范围的条件,这样第二次会多读到一条数据。

    不可重复读重点在于update和delete,而幻读的重点在于insert。

幻读问题——间隙锁

        即便把全部的记录都加上锁,仍是阻止不了新插入的记录,也就是说行锁解决不了幻读问题,行锁只能锁住行,可是新插入记录这个动做,要更新的是记录之间的“间隙”。所以,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。

        当执行 select * from t where d=5 for update 的时候,就不止是给数据库中已有的 6 个记录加上了行锁,还同时加了 7 个间隙锁。这样就确保了没法再插入新的记录。

        间隙锁和行锁合称 next-key lock,每一个 next-key lock 是前开后闭区间。也就是说,表 t 初始化之后,若是用 select * from t for update 要把整个表全部记录锁起来,就造成了 7 个 next-key lock,分别是(-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。

        间隙锁和 next-key lock 的引入,解决了幻读的问题,但同时也带来了一些“困扰”。间隙锁的引入,可能会致使一样的语句锁住更大的范围,这实际上是影响了并发度的。

隔离级别

        SQL 标准的事务隔离级别包括:读未提交read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。隔离级别越高,效率越低。

  • 读未提交是指,一个事务还没提交时,它作的变动就能被别的事务看到。
  • 读提交是指,一个事务提交以后,它作的变动才会被其余事务看到。
  • 可重复读是指,一个事务执行过程当中看到的数据,老是跟这个事务在启动时看到的数据是一致的。固然在可重复读隔离级别下,未提交变动对其余事务也是不可见的。
  • 串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读” 会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

在实现上,数据库里面会建立一个视图,访问的时候以视图的逻辑结果为准。

  1. “可重复读”隔离级别下:这个视图是在事务启动时建立的,整个事务存在期间都用这个视图。
  2. “读提交”隔离级别下:这个视图是在每一个 SQL 语句开始执行的时候建立的。
  3. “读未提交”隔离级别下:直接返回记录上的最新值,没有视图概念
  4. “串行化”隔离级别下:直接用加锁的方式来避免并行访问。

在 MySQL 里,有两个“视图”的概念:

  • 一个是 view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。建立视图的语法是 create view … ,而它的查询方法与表同样。
  • 另外一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。

MySQL 默认隔离级别是可重复读,Oracle 默认隔离级别是“读提交”。

        将启动参数 transaction-isolation 的值设置成 READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ 、SERIALIZABLE。

        能够用 show variables 来查看当前的值。

事务隔离的实现——undo log

        每条记录在更新的时候都会同时记录一条回滚操做。同一条记录在系统中能够存在多个版本,这就是数据库的(MVCC)。

        MVCC的全称是“多版本并发控制”。为了查询一些正在被另外一个事务更新的行,而且能够看到它们被更新以前的值,不用等待另外一个事务释放锁。

        InnoDB会给数据库中的每一行增长三个字段,它们分别是DB_TRX_ID(事务版本号)、DB_ROLL_PTR(建立时间)、DB_ROW_ID(惟一id)。

        InnoDB 里面每一个事务有一个惟一的事务 ID,叫做 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。

        InnoDB 利用了“全部数据都有多个版本”的这个特性,实现了“秒级建立快照”的能力。

        B+Tree叶结点上,始终存储的是最新的数据(多是还未提交的数据)。而旧版本数据,经过UNDO记录存储在回滚段(Rollback Segment)里。每一条记录都会维护一个ROW HEADER元信息,存储有建立这条记录的事务ID,一个指向UNDO记录的指针。经过最新记录和UNDO信息,能够还原出旧版本的记录。

假设一个值从 1 被按顺序改为了 二、三、4,在回滚日志里面就会有相似下面的记录。
clipboard.png
        当前值是 4,可是在查询这条记录的时候,不一样时刻启动的事务会有不一样的 read-view。同一条记录在系统中能够存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要获得 1,就必须将当前值依次执行图中全部的回滚操做获得。这些回滚信息记录在undo log 里。

        当系统里没有比这个回滚日志更早的 read-view 的时候会删除老的undo log。

避免长事务

        尽可能不要使用长事务,长事务意味着系统里面会存在很老的事务视图。会有很大的undo log日志占用空间。并且长事务还会占据锁资源,也可能拖垮整个库。

        能够在 information_schema 库的innodb_trx 这个表中查询长事务,好比下面这个语句,用于查找持续时间超过 60s 的事务。能够监控这个表,设置长事务阈值报警或者直接kill。

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

        能够经过 SET MAX_EXECUTION_TIME 命令来控制每一个语句执行的最长时间,避免单个语句意外执行太长时间。

        确认是否有没必要要的只读事务。

        若是使用的是 MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2或更大的值)。若是真的出现大事务致使undo log过大,这样设置后清理起来更方便。

索引

常见索引模型

        Hash表 + 链表,查询新增都很快,可是只适用于只有等值查询的场景,不能区间查询, Memcached 及其余一些 NoSQL 引擎在用。

        有序数组,等值查询和范围查询场景中的性能就都很是优秀,二分查找O(log(N)),可是更新的效率很低,因此只适用于静态存储引擎。

        平衡二叉树,更新和查询都比较快。
        还有跳跃表,LSM树等。

B+ 树

        为了让一个查询尽可能少地读磁盘,就须要使用多叉树。MySQL采用的是B+树,因为索引不止存在内存中,还要写到磁盘上。二叉树的树高过高,100万数据,就有20层,在机械硬盘时代,从磁盘随机读一个数据块须要 10 ms 左右的寻址时间。就要花费200ms的寻址时间,就太慢了。MySQL B+树 的一层节点数量在1200左右,只须要1-3次磁盘IO就能够了,由于InnoDB存储引擎的最小储存单元页(Page),一个页的大小是16K。通常来讲主键id为bigint类型,长度8字节,指针6字节,那么16284/14 = 1170。因此一次IO最多读取1170个节点。

        相对于B树,B+树把全部的数据都放在了叶子节点上,这样虽然每次都须要查询叶子节点,但也不过两三层,若是干节点也放数据,那干节点就变大了,一次就读取不了1200节点了,层高会变大不少。

        而且MySQL把B+树的全部叶子节点的数据用指针连起来了,这样作区间查询是很是快的。

主键索引和非主键索引

        主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。

        非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。

        查询语句,若是走主键索引,会直接获得数据,若是走非主键索引,查到主键后,还须要回主键索引再查一次数据。这个过程称为回表。(覆盖索引不须要回表)
clipboard.png

        分为聚簇索引和非聚簇索引的缘由:更新数据的时候,因为数据的地址变了,须要更改索引,可是因为数据只跟主键索引绑定,索引只须要更新聚簇索引,固然还有被更新列涉及到的索引也要更新。若是全部全部都跟数据绑定,虽然省掉了回表的过程,可是每次更新,须要更新全部的索引,得不偿失。

索引维护

        B+ 树为了维护索引有序性,在插入新值的时候须要作必要的维护。

        好比按顺序插入1-499,501-1000,索引都在一页,再插入一个500,根据 B+ 树的算法,这时候须要申请一个新的数据页,而后挪动部分数据(501到1000的数据)过去。这个过程称为页分裂。在这种状况下,性能天然会受影响。

        除了影响性能外,页分裂操做还影响数据页的利用率。本来放在一个页的数据,如今分到两个页中,总体空间利用率下降大约 50%。

        固然有分裂就有合并。当相邻两个页因为删除了数据,利用率很低以后,会将数据页作合并。合并的过程,能够认为是分裂过程的逆过程。

        因此通常建表规范都要求用自增主键,避免页分裂,固然也有特殊状况,使用别的字段当作主键。

        而且索引可能由于删除,或者页分裂等缘由,致使数据页有空洞,重建索引的过程会建立一个新的索引,把数据按顺序插入,这样页面的利用率最高,也就是索引更紧凑、更省空间。

alter table T drop index k;
alter table T add index(k);

        可是不能重建主键索引,不管是删除主键仍是建立主键,都会将整个表重建。可使用 alter table T engine=InnoDB 重建表。

覆盖索引

        若是执行的语句是 select ID from T where k between 3 and 5,这时只须要查 ID 的值,而 ID 的值已经在 k 索引树上了,所以能够直接提供查询结果,不须要回表。也就是说,在这个查询里面,索引 k 已经“覆盖了”查询需求,称为覆盖索引。

        因为覆盖索引能够减小树的搜索次数,显著提高查询性能,因此使用覆盖索引是一个经常使用的性能优化手段。

        若是有根据身份证号查询市民信息的需求,只要在身份证号字段上创建索引就够了。若是如今有一个高频请求,要根据市民的身份证号查询他的姓名,再创建一个(身份证号、姓名)的联合索引就是覆盖索引,省去了回表环节。

最左前缀原则

        若是为每一种查询都设计一个索引,索引是否是太多了。

        B+ 树这种索引结构,能够利用索引的“最左前缀”,来定位记录。

        为了直观地说明这个概念,用(name,age)这个联合索引来分析。
clipboard.png
        能够看到,索引项是按照索引定义里面出现的字段顺序排序的。

        当逻辑需求是查到全部名字是“张三”的人时,能够快速定位到 ID4,而后向后遍历获得全部须要的结果。

        若是要查的是全部名字第一个字是“张”的人,SQL 语句的条件是"where name like ‘张 %’"。这时,也可以用上这个索引,查找到第一个符合条件的记录是 ID3,而后向后遍历,直到不知足条件为止。

        能够看到,不仅是索引的所有定义,只要知足最左前缀,就能够利用索引来加速检索。这个最左前缀能够是联合索引的最左 N 个字段,也能够是字符串索引的最左 M 个字符。

前缀索引

        使用前缀索引,定义好长度,就能够作到既节省空间,又不用额外增长太多的查询成本。

        在创建索引时关注的是区分度,区分度越高越好。由于区分度越高,意味着重复的键值越少。所以,能够经过统计索引上有多少个不一样的值来判断要使用多长的前缀。

        可使用下面这个语句,算出这个列上有多少个不一样的值:

select count(distinct email) as L from SUser;

        使用前缀索引就用不上覆盖索引对查询性能的优化了,这是在选择是否使用前缀索引时须要考虑的一个因素。

        那么对于身份证号,一共 18 位,其中前 6 位是地址码,因此同一个县的人的身份证号前 6 位通常会是相同的。该怎么存储,怎么设计索引呢?

    1. 第一种方式是使用倒序存储。身份证号的最后 6 位没有地址码这样的重复逻辑。

      select field_list from t where id_card = reverse('input_id_card_string');

      select field_list from t where id_card = reverse('input_id_card_string');

    1. 第二种方式是使用 hash 字段。在表上再建立一个整数字段,来保存身份证的校验码,同时在这个字段上建立索引。

      alter table t add id_card_crc int unsigned, add index(id_card_crc);

      而后每次插入新记录的时候,都同时用 crc32() 这个函数获得校验码填到这个新字段。因为校验码可能存在冲突,因此查询语句 where 部分要判断 id_card 的值是否精确相同。

      select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'

    索引下推

            最左前缀的时候,那些不符合最左前缀的部分,会怎么样呢?

            若是如今有一个需求:检索出表中“名字第一个字是张,并且年龄是 10 岁的全部男孩”。那么,SQL 语句是这么写的:

    mysql> select * from tuser where name like '张 %' and age=10 and ismale=1;

            这个语句在搜索索引树的时候,只能用 “张”,找到第一个知足条件的记录 ID3。

            而后须要判断其余条件是否知足。

            在 MySQL 5.6 以前,只能从 ID3 开始一个个回表。到主键索引上找出数据行,再对比字段值。

            而 MySQL 5.6 引入的索引下推优化(index condition pushdown),能够在索引遍历过程当中,对索引中包含的字段先作判断,直接过滤掉不知足条件的记录,减小回表次数。
    clipboard.png

    change buffer

            当须要更新一个数据页时,若是数据页在内存中就直接更新,而若是这个数据页尚未在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操做缓存在 change buffer 中,这样就不须要从磁盘中读入这个数据页了。在下次查询须要访问这个数据页的时候,将数据页读入内存,而后执行 change buffer 中与这个页有关的操做。经过这种方式就能保证这个数据逻辑的正确性。虽然是只更新内存,可是在事务提交的时候,把 change buffer 的操做也记录到 redo log 里了,因此崩溃恢复的时候,change buffer 也能找回来。

            须要说明的是,虽然名字叫做 change buffer,实际上它是能够持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。

            将 change buffer 中的操做应用到原数据页,获得最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会按期 merge。在数据库正常关闭(shutdown)的过程当中,也会执行 merge 操做。

    ​        显然,若是可以将更新操做先记录在 change buffer,减小读磁盘,语句的执行速度会获得明显的提高。并且,数据读入内存是须要占用 buffer pool 的,因此这种方式可以避免占用内存,提升内存利用率。

            惟一索引的更新就不能使用 change buffer,实际上也只有普通索引可使用。

            change buffer 用的是 buffer pool 里的内存,所以不能无限增大。change buffer 的大小,能够经过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。

    ​        若是要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的。

    第一种状况是,这个记录要更新的目标页在内存中。这时,InnoDB 的处理流程以下:

    • 对于惟一索引来讲,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;
    • 对于普通索引来讲,找到 3 和 5 之间的位置,插入这个值,语句执行结束。

      这个判断只会耗费微小的 CPU 时间。不是重点

    第二种状况是,这个记录要更新的目标页不在内存中。这时,InnoDB 的处理流程以下:

    • 对于惟一索引来讲,须要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
    • 对于普通索引来讲,则是将更新记录在 change buffer,语句执行就结束了。

            将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操做之一。change buffer 由于减小了随机磁盘访问,因此对更新性能的提高是会很明显的。

            change buffer 适用于写多读少的业务,好比帐单类、日志类的系统。由于会记录不少change buffer(写的时候) 才会merge(读的时候)

            反过来,读多写少的业务,几乎每次把更新记录在change buffer 后,就会当即出发merge,这样随机访问 IO 的次数不会减小,反而增长了change buffer 的维护代价。

            因此,对于身份证号这类字段,若是业务已经保证不会写入重复数据,不须要数据库作约束,加普通索引比加主键索引要好,若是全部的更新后面,都立刻伴随着对这个记录的查询,那么应该关闭 change buffer。而在其余状况下,change buffer 都能提高更新性能。

            在实际使用中,能够发现,普通索引和 change buffer 的配合使用,对于数据量大的表的更新优化仍是很明显的,特别是在使用机械硬盘时。

    change buffer 和 redo log 对比

    insert into t(id,k) values(id1,k1),(id2,k2);

    这条更新语句作了以下操做:

    1. Page 在内存中,直接更新内存;
    2. Page 没有在内存中,就在内存的 change buffer 区域,记录下“要往 Page 插入一行”这个信。
    3. 将上述两个动做记入 redo log 中。

    后续的更新操做

    1. Page 在内存中,会直接从内存返回。
    2. Page 不在内容中,须要把 Page 从磁盘读入内存中,而后应用 change buffer 里面的操做日志,生成一个正确的版本并返回结果。

            因此,若是要简单地对比这两个机制在提高更新性能上的收益的话,redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。

    优化器如何选择索引

            优化器结合是否扫描行数、是否使用临时表、是否排序等因素进行综合判断。

            MySQL 在真正开始执行语句以前,并不能精确地知道知足条件的记录有多少条,而只能根据统计信息来估算记录数。

            这个统计信息就是索引的“区分度”。显然,一个索引上不一样的值越多,这个索引的区分度就越好。而一个索引上不一样的值的个数,称之为“基数”(cardinality)。也就是说,这个基数越大,索引的区分度越好。

            可使用 show index 方法,看到一个索引的基数。
    clipboard.png

            MySQL 采样统计的方法得到基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不一样值,获得一个平均值,而后乘以这个索引的页面数,就获得了这个索引的基数。当变动的数据行数超过 1/M 的时候,会自动触发从新作一次索引统计。analyze table t 命令,能够用来从新统计索引信息。

    在 MySQL 中,有两种存储索引统计的方式,能够经过设置参数 innodb_stats_persistent 的值来选择:

    • 设置为 on 的时候,表示统计信息会持久化存储。这时,默认的 N 是 20,M 是 10。
    • 设置为 off 的时候,表示统计信息只存储在内存中。这时,默认的 N 是 8,M 是 16。

    其实索引统计只是一个输入,对于一个具体的语句来讲,优化器还要判断,执行这个语句自己要扫描多少行。

    clipboard.png
    rows 这个字段表示的是预计扫描行数。

            少数状况下优化器会选错索引,第一种方法能够采用 force index 强行选择一个索引。

            但其实使用 force index 最主要的问题仍是变动的及时性。由于选错索引的状况仍是比较少出现的,因此开发的时候一般不会先写上 force index。而是等到线上出现问题的时候,才会再去修改 SQL 语句、加上 force index。可是修改以后还要测试和发布,对于生产系统来讲,这个过程不够敏捷。

            因此,数据库的问题最好仍是在数据库内部来解决。既然优化器放弃了使用索引 a,说明 a 还不够合适,因此第二种方法就是,能够考虑修改语句,引导 MySQL 使用指望的索引。好比,在这个例子里,显然把“order by b limit 1” 改为 “order by b,a limit 1” ,语义的逻辑是相同的。

            以前优化器选择使用索引 b,是由于它认为使用索引 b 能够避免排序(b 自己是索引,已是有序的了,若是选择索引 b 的话,不须要再作排序,只须要遍历),因此即便扫描行数多,也断定为代价更小。

            如今 order by b,a 这种写法,要求按照 b,a 排序,就意味着使用这两个索引都须要排序。所以,扫描行数成了影响决策的主要条件,因而此时优化器选了只须要扫描 1000 行的索引 a。

            固然,这种修改并非通用的优化手段,可能修改语义这件事儿不太好,能够用 limit 100 让优化器意识到,使用 b 索引代价是很高的。实际上是根据数据特征诱导了一下优化器,也不具有通用性。

    select from (select from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 100)alias limit 1;

            第三种方法是:在有些场景下,能够新建一个更合适的索引,来提供给优化器作选择,或删掉误用的索引。

            对索引字段作函数操做,可能会破坏索引值的有序性,所以优化器就决定放弃走树搜索功能。

    1. 条件字段函数操做

      select count(*) from tradelog where month(t_modified)=7;

      同理 where id+1=1000 也不会用索引,改为 where id =1000 - 1 会用索引。

    2. 隐式类型转换

      select * from tradelog where tradeid=110717; (tradeid 是varchar)

      等同于 select * from tradelog where CAST(tradid AS signed int) = 110717;

    3. 隐式字符编码转换

      select * from trade_detail where tradeid=$L2.tradeid.value;

      $L2.tradeid.value 的字符集是 utf8mb4。字符集 utf8mb4 是 utf8 的超集,因此当这两个类型的字符串在作比较的时候,MySQL 内部的操做是,先把 utf8 字符串转成 utf8mb4 字符集,再作比较。

      至关于 select * from trade_detail where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value;

    全局锁和表锁

    全局锁

            顾名思义,全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当须要让整个库处于只读状态的时候,可使用可使用这个命令,以后其余线程的如下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。

            全局锁的典型使用场景是,作全库逻辑备份。也就是把整库每一个表都 select 出来存成文本。

            经过 FTWRL 确保不会有其余线程对数据库作更新,而后对整个库作备份。在备份过程当中整个库彻底处于只读状,这是很危险的。可是不加锁,备份的数据会有不一致的问题。

            能够拿到一个一致性视图来备份,官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction 的时候,导数据以前就会启动一个事务,来确保拿到一致性视图。而因为 MVCC 的支持,这个过程当中数据是能够正常更新的。

            那为何还须要FTWRL呢,由于一致性读是好,但前提是引擎要支持这个隔离级别。对于 MyISAM 这种不支持事务的引擎,就须要使用 FTWRL 命令了。

            既然要全库只读,为何不使用 set global readonly=true 的方式呢?确实 readonly 方式也可让全库进入只读状态,但仍是建议用 FTWRL 方式,主要有两个缘由:

    • 在有些系统中,readonly 的值会被用来作其余逻辑,好比用来判断一个库是主库仍是备库。所以,修改 global 变量的方式影响面更大,不建议使用。
    • 在异常处理机制上有差别。若是执行 FTWRL 命令以后因为客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到能够正常更新的状态。而将整个库设置为 readonly 以后,若是客户端发生异常,则数据库就会一直保持 readonly 状态,这样会致使整个库长时间处于不可写状态,风险较高。

    表级锁

            MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。

            表锁的语法是 lock tables … read/write。与 FTWRL 相似,能够用 unlock tables 主动释放锁,也能够在客户端断开的时候自动释放。须要注意,lock tables 语法除了会限制别的线程的读写外,也限定了本线程接下来的操做对象。

            对于 InnoDB 这种支持行锁的引擎,通常不使用 lock tables 命令来控制并发,毕竟锁住整个表的影响面仍是太大。

            另外一类表级的锁是 MDL(metadata lock)。MDL 不须要显式使用,在访问一个表的时候会被自动加上。MDL 的做用是,保证读写的正确性。能够想象一下,若是一个查询正在遍历一个表中的数据,而执行期间另外一个线程对这个表结构作变动,删了一列,那么查询线程拿到的结果跟表结构对不上,确定是不行的。

    ​        所以,在 MySQL 5.5 版本中引入了 MDL,当对一个表作增删改查操做的时候,加 MDL 读锁;当要对表作结构变动操做的时候,加 MDL 写锁。

    • 读锁之间不互斥,所以能够有多个线程同时对一张表增删改查。
    • 读写锁之间、写锁之间是互斥的,用来保证变动表结构操做的安全性。

    安全的给表增长字段

    ​        有几个请求在读写表,会加上MDL读锁,而后修改表字段的请求会被blocked,请求MDL写锁,这个时候,后面的所有读写请求都会被MDL写锁 blocked,若是查询语句频繁,并且客户端有重试机制,也就是说超时后会再起一个新 session 再请求的话,这个库的线程很快就会爆满。

    那么如何安全的给表加字段呢?

            首先要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,能够查到当前执行中的事务。若是要作 DDL 变动的表恰好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。

            其次,在 alter table 语句里面设定等待时间,若是在这个指定的等待时间里面可以拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。以后开发人员或者 DBA 再经过重试命令重复这个过程。

    ALTER TABLE tbl_name NOWAIT add column ...
    ALTER TABLE tbl_name WAIT N add column ...

    行锁

            MyISAM 引擎就不支持行锁。不支持行锁意味着并发控制只能使用表锁,对于这种引擎的表,同一张表上任什么时候刻只能有一个更新在执行,这就会影响到业务并发度。InnoDB 是支持行锁的,这也是 MyISAM 被 InnoDB 替代的重要缘由之一。

            在 InnoDB 事务中,行锁是在须要的时候才加上的,但并非不须要了就马上释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。

    ​        若是事务中须要锁多个行,要把最可能形成锁冲突、最可能影响并发度的锁尽可能日后放。

    死锁

            当并发系统中不一样线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会致使这几个线程都进入无限等待的状态,称为死锁。这里用数据库中的行锁举个例子。
    clipboard.png
            这时候,事务 A 在等待事务 B 释放 id=2 的行锁,而事务 B 在等待事务 A 释放 id=1 的行锁。 事务 A 和事务 B 在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁之后,有两种策略:

    • 一种策略是,直接进入等待,直到超时。这个超时时间能够经过参数 innodb_lock_wait_timeout 来设置。

      设置时间长,等待时间太长;设置时间短,有的长事务,不是死锁的也会结束。
    • 另外一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其余事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。

      每一个新来的被堵住的线程,都要判断会不会因为本身的加入致使了死锁,这是一个时间复杂度是 O(n) 的操做。会耗费大量的CPU资源。

    慢SQL问题排查

    使用 show processlist 命令查看 Waiting for table metadata lock 的示意图。
    clipboard.png
    这个状态表示的是,如今有一个线程正在表 t 上请求或者持有 MDL 写锁,把 select 语句堵住了。

            经过查询 sys.schema_table_lock_waits 这张表,就能够直接找出形成阻塞的 process id,把这个链接用 kill 命令断开便可。

    经过 sys.innodb_lock_waits 查行锁

    select * from t sys.innodb_lock_waits where locked_table= 'test'.'t'G
    clipboard.png
    这个信息很全,4 号线程是形成堵塞的罪魁祸首。而干掉这个罪魁祸首的方式,就是 KILL QUERY 4 或 KILL 4。实际上,这里 KILL 4 才有效。

    其余

    count(*) 语句分析

            MyISAM 引擎把一个表的总行数存在了磁盘上,所以执行 count(*) 的时候会直接返回这个数,效率很高;

            InnoDB 引擎就麻烦了,执行 count(*) 的时候,须要把数据一行一行地从引擎里面读出来,而后累积计数。由于多版本并发控制(MVCC)的缘由,InnoDB 表“应该返回多少行”也是不肯定的。

            count() 是一个聚合函数,对于返回的结果集,一行行地判断,若是 count 函数的参数不是 NULL,累计值就加 1,不然不加。最后返回累计值。
            因此,count(*)、count(主键 id) 和 count(1) 都表示返回知足条件的结果集的总行数;而 count(字段),则表示返回知足条件的数据行里面,参数“字段”不为 NULL 的总个数。

            按照效率排序的话,count(字段) < count(主键id) < count(1) < count(*),因此建议,尽可能使用count(*)。

    order by 语句分析

    ​        MySQL 会给每一个线程分配一块内存用于快速排序,称为 sort_buffer

    ​        explain 结果里的 Extra 这个字段中的“Using filesort”表示的就是须要排序。

    ​        sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。若是要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但若是排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。

            创建联合索引,甚至覆盖索引,能够避免排序过程。

    join 语句分析

    ​        直接使用 join 语句,MySQL 优化器可能会选择表 t1 或 t2 做为驱动表,改用 straight_join 让 MySQL 使用固定的链接方式执行查询,这样优化器只会按照指定的方式去 join。

    select * from t1 straight_join t2 on (t1.a=t2.a);

    clipboard.png
            在这条语句里,被驱动表 t2 的字段 a 上有索引,join 过程用上了这个索引,所以效率是很高的。称之为“Index Nested-Loop Join”,简称 NLJ。

    ​        若是被驱动表 t2 的字段 a 上没有索引,那每次到 t2 去匹配的时候,就要作一次全表扫描。这个效率很低。这个算法叫作“Simple Nested-Loop Join”的算法,简称 BNL。

    ​        因此在判断要不要使用 join 语句时,就是看 explain 结果里面,Extra 字段里面有没有出现“Block Nested Loop”字样。

            在决定哪一个表作驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成以后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该做为驱动表。

            Multi-Range Read 优化,这个优化的主要目的是尽可能使用顺序读盘。由于大多数的数据都是按照主键递增顺序插入获得的,因此能够认为,若是按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,可以提高读性能。

    select * from t1 where a>=1 and a<=100;

    clipboard.png
            Batched Key Access(BKA) 算法。这个 BKA 算法,其实就是对 NLJ 算法的优化。

            NLJ 算法执行的逻辑是:从驱动表 t1,一行行地取出 a 的值,再到被驱动表 t2 去作 join。也就是说,对于表 t2 来讲,每次都是匹配一个值。这时,MRR 的优点就用不上了。

    ​        既然如此,就把表 t1 的数据取出来一部分,先放到一个临时内存。这个临时内存就是 join_buffer。

    自增主键

    clipboard.png
            表定义里面出现了一个 AUTO_INCREMENT=2,表示下一次插入数据时,若是须要自动生成自增值,会生成 id=2。

    实际上,表的结构定义存放在后缀名为.frm 的文件中,可是并不会保存自增值。

    • MyISAM 引擎的自增值保存在数据文件中。
    • InnoDB 引擎的自增值,实际上是保存在了内存里,MySQL 8.0 版本后,才有了“自增值持久化”的能力。

      • MySQL 5.7 及以前的版本,自增值保存在内存里,并无持久化。每次重启后,第一次打开表的时候,都会去找自增值的最大值 max(id),而后将 max(id)+1 做为这个表当前的自增值。
      • MySQL 8.0 版本,将自增值的变动记录在了 redo log 中,重启的时候依靠 redo log 恢复重启以前的值。

    自增值修改机制

    • 若是插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段;
    • 若是插入数据时 id 字段指定了具体的值 X ,就直接使用语句里指定的值 Y。

      • 若是 X < Y,那么这个表的自增值不变;
      • 若是 X≥Y,就须要把当前自增值修改成新的自增值。

            新的自增值生成算法是:从 auto_increment_offset 开始,以 auto_increment_increment 为步长,持续叠加,直到找到第一个大于 X 的值,做为新的自增值。

    自增值的修改时机

    1. 执行器调用 InnoDB 引擎接口写入一行,传入的这一行的值(0,1,1);
    2. InnoDB 发现用户没有指定自增 id 的值,获取表 t 当前的自增值 2;
    3. 将传入的行的值改为 (2,1,1);
    4. 将表的自增值改为 3;
    5. 继续执行插入数据操做,因为已经存在 c=1 的记录,因此报 Duplicate key error,语句返回。

            因此,sql执行报错了,自增值已经改变了,惟一键冲突是致使自增主键 id 不连续的第一种缘由。一样地,事务回滚也会产生相似的现象,这就是第二种缘由。

            批量插入的时候,因为系统预先不知道要申请多少个自增 id,因此就先申请一个,而后两个,而后四个,直到够用。这是主键 id 出现自增 id 不连续的第三种缘由。

    自增id用完怎么办

    一、主键id

            再申请下一个 id 时,获得的值保持不变。因此到最大值以后,再申请id,因为id不变,因此插入会报主键冲突,若是数据量比较大,主键id应该用 bigint unsigned。默认是无符号整型 (unsigned int) ,4 个字节232-1(4294967295)。

    二、系统row_id

            若是建立的 InnoDB 表没有指定主键,那么 InnoDB 会建立一个不可见的,长度为 6 个字节的 row_id。InnoDB 维护了一个全局的 dict_sys.row_id 值,全部无主键的 InnoDB 表,每插入一行数据,都把当前的 dict_sys.row_id 值做为要插入数据的 row_id,而后把 dict_sys.row_id 的值加 1。

            实际上,在代码实现时 row_id 是一个长度为 8 字节的无符号长整型 (bigint unsigned)。可是,InnoDB 在设计时,给 row_id 留的只是 6 个节的长度,这样写到数据表中时只放了最后 6 个字节,因此 row_id 能写到数据表中的值,就有两个特征:

            248-1到 264 之间,row_id 会是0,264 以后会从0开始。

            在 InnoDB 逻辑里,申请到 row_id=N 后,就将这行数据写入表中;若是表中已经存在 row_id=N 的行,新写入的行就会覆盖原有的行。

            覆盖数据,就意味着数据丢失,影响的是数据可靠性;报主键冲突,是插入失败,影响的是可用性。而通常状况下,可靠性优先于可用性。

    三、Xid

            redo log 和 binlog 相配合的时候,提到了有一个共同的字段叫做 Xid。它在 MySQL 中是用来对应事务的。

            MySQL 内部维护了一个全局变量 global_query_id,每次执行语句的时候将它赋值给 Query_id,而后给这个变量加 1。若是当前语句是这个事务执行的第一条语句,那么 MySQL 还会同时把 Query_id 赋值给这个事务的 Xid。

            而 global_query_id 是一个纯内存变量,重启以后就清零了。因此就知道了,在同一个数据库实例中,不一样事务的 Xid 也是有可能相同的。

            可是 MySQL 重启以后会从新生成新的 binlog 文件,这就保证了,同一个 binlog 文件里,Xid 必定是唯一的。

            可是 global_query_id 定义的长度是 8 个字节,这个自增值的上限是 264-1。理论上也是可能重复的。

    四、trx_id

            Xid 是由 server 层维护的。InnoDB 内部使用 Xid,就是为了可以在 InnoDB 事务和 server 之间作关联。可是,InnoDB 本身的 trx_id,是另外维护的。

            InnoDB 内部维护了一个 max_trx_id 全局变量,每次须要申请一个新的 trx_id 时,就得到 max_trx_id 的当前值,而后并将 max_trx_id 加 1。

            InnoDB 数据可见性的核心思想是:每一行数据都记录了更新它的 trx_id,当一个事务读到一行数据的时候,判断这个数据是否可见的方法,就是经过事务的一致性视图与这行数据的 trx_id 作对比。

            对于正在执行的事务,能够从 information_schema.innodb_trx 表中看到事务的 trx_id。

    ​ update 和 delete 语句除了事务自己,还涉及到标记删除旧数据,也就是要把数据放到 purge 队列里等待后续物理删除,这个操做也会把 max_trx_id+1, 所以在一个事务中至少加 2;

    ​ InnoDB 的后台操做,好比表的索引信息统计这类操做,也是会启动内部事务的,所以你可能看到,trx_id 值并非按照加 1 递增的。

            只读事务会分配一个特殊的,比较大的id,把当前事务的 trx 变量的指针地址转成整数,再加上 248,使用这个算法,就能够保证如下两点:

    1. 由于同一个只读事务在执行期间,它的指针地址是不会变的,因此不管是在 innodb_trx 仍是在 innodb_locks 表里,同一个只读事务查出来的 trx_id 就会是同样的。
    2. 若是有并行的多个只读事务,每一个事务的 trx 变量的指针地址确定不一样。这样,不一样的并发只读事务,查出来的 trx_id 就是不一样的。

            加上248是为了保证只读事务显示的 trx_id 值比较大,正常状况下就会区别于读写事务的 id。理论状况下也可能只读事务与读写事务相等,可是没有影响。

            max_trx_id 会持久化存储,重启也不会重置为 0,那么从理论上讲,只要一个 MySQL 服务跑得足够久,就可能出现 max_trx_id 达到 248-1 的上限,而后从 0 开始的状况。当达到这个状态后,MySQL 就会持续出现一个脏读的 bug。由于后续的trx_id确定比末尾那些trx_id大,能看到这些数据。

    五、thread_id

            系统保存了一个全局变量 thread_id_counter,每新建一个链接,就将 thread_id_counter 赋值给这个新链接的线程变量。定义的大小是 4 个字节,所以达到 232-1 后,它就会重置为 0,而后继续增长。可是,在 show processlist 里不会看到两个相同的 thread_id。由于 MySQL 设计了一个惟一数组的逻辑,给新线程分配 thread_id 的时候,逻辑代码是这样的:

    do {
            new_id= thread_id_counter++;
    } while (!thread_ids.insert_unique(new_id).second);

    误删数据怎么办

    1. delete 语句误删数据行:Flashback工具过闪回把数据恢复回来。 原理是修改 binlog 的内容,拿回原库重放。而可以使用这个方案的前提是,须要确保 binlog_format=row 和 binlog_row_image=FULL。

      如何预防:把 sql_safe_updates 参数设置为 on。,delete 或者 update 语句必须有where条件,不然执行会报错。
    1. 误删库 / 表:全量备份,加增量日志,在应用日志的时候,须要跳过 12 点误操做的那个语句的 binlog:

      1. 若是原实例没有使用 GTID 模式,只能在应用到包含 12 点的 binlog 文件的时候,先用–stop-position 参数执行到误操做以前的日志,而后再用–start-position 从误操做以后的日志继续执行;
      2. 若是实例使用了 GTID 模式,就方便多了。假设误操做命令的 GTID 是 gtid1,那么只须要执行 set gtid_next=gtid1;begin;commit; 先把这个 GTID 加到临时实例的 GTID 集合,以后按顺序执行 binlog 的时候,就会自动跳过误操做的语句。

        如何加速恢复:使用 mysqlbinlog 命令时,加上一个–database 参数,用来指定误删表所在的库。
        在 start slave 以前,先经过执行 change replication filter replicate_do_table = (tbl_name) 命令,就可让临时库只同步误操做的表;

            延迟复制备库,通常的主备复制结构存在的问题是,若是主库上有个表被误删了,这个命令很快也会被发给全部从库,进而致使全部从库的数据表也都一块儿被误删了。延迟复制的备库是一种特殊的备库,经过 CHANGE MASTER TO MASTER_DELAY = N 命令,能够指定这个备库持续保持跟主库有 N 秒的延迟。

            好比把 N 设置为 3600,这就表明了若是主库上有数据被误删了,而且在 1 小时内发现了这个误操做命令,这个命令就尚未在这个延迟复制的备库执行。这时候到这个备库上执行 stop slave,再经过以前介绍的方法,跳过误操做命令,就能够恢复出须要的数据。

    预防误删库 / 表的方法,制定操做规范。这样作的目的,是避免写错要删除的表名。

    1. 在删除数据表以前,必须先对表作更名操做。而后,观察一段时间,确保对业务无影响之后再删除这张表。
    2. 改表名的时候,要求给表名加固定的后缀(好比加_to_be_deleted),而后删除表的动做必须经过管理系统执行。而且,管理系删除表的时候,只能删除固定后缀的表。

    删除数据,为什么表文件大小不变

            delete 命令其实只是把记录的位置,或者数据页标记为了“可复用”,但磁盘文件的大小是不会变的。也就是说,经过 delete 命令是不能回收表空间的。这些能够复用,而没有被使用的空间,看起来就像是“空洞”。

            实际上,不止是删除数据会形成空洞,插入数据也会。若是数据是随机插入的,就可能形成索引的数据页分裂。更新索引上的值,能够理解为删除一个旧的值,再插入一个新值。不难理解,这也是会形成空洞的。

            也就是说,通过大量增删改的表,都是多是存在空洞的。因此,若是可以把这些空洞去掉,就能达到收缩表空间的目的。而重建表,就能够达到这样的目的。

            使用 alter table A engine=InnoDB 命令来重建表。MySQL 会自动完成转存数据、交换表名、删除旧表的操做。

            重建表的时候,InnoDB 不会把整张表占满,每一个页留了 1/16 给后续的更新用。也就是说,其实重建表以后不是“最”紧凑的。

    怎么复制一张表

    一、mysqldump 方法

    使用 mysqldump 命令将数据导出成一组 INSERT 语句。你可使用下面的命令:

    mysqldump -h$host -P$port -u$user --add-locks=0 --no-create-info --single-transaction --set-gtid-purged=OFF db1 t --where="a>900" --result-file=/client_tmp/t.sql

    而后能够经过下面这条命令,将这些 INSERT 语句放到 db2 库里去执行。

    mysql -h127.0.0.1 -P13000 -uroot db2 -e "source /client_tmp/t.sql"

    二、导出 CSV 文件

    直接将结果导出成.csv 文件。MySQL 提供了下面的语法,用来将查询结果导出到服务端本地目录:

    select * from db1.t where a>900 into outfile '/server_tmp/t.csv';

    而后用下面的 load data 命令将数据导入到目标表 db2.t 中。

    load data infile '/server_tmp/t.csv' into table db2.t;

    三、物理拷贝方法

    直接拷贝文件是不行的,须要在数据字典中注册。

    MySQL 5.6 版本引入了可传输表空间(transportable tablespace) 的方法,,能够经过导出 + 导入表空间的方式,实现物理拷贝表的功能。

    相关文章
    相关标签/搜索