Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离

文章目录

    • 一、Mysql索引
      • 1.1 一条SQL查询语句是如何执行的(说法1)
        • 1.1.1 连接器
        • 1.1.2 查询缓存
        • 1.1.3 分析器
        • 1.1.4 优化器
        • 1.1.5 执行器
      • 1.2 一条SQL查询语句是如何执行的(说法2)
        • 1.2.1 跟数据库建立连接
        • 1.2.2 查询缓存
        • 1.2.3 语法解析和预处理
        • 1.2.4 查询优化
        • 1.2.5 存储引擎
        • 1.2.6 执行引擎,返回结果
      • 1.3 MySQL 体系结构
      • 1.4 一条更新 SQL 是如何执行的?
        • 1.4.1 缓冲池(Buffer Pool)
        • 1.4.2 InnoDB 内存结构和磁盘结构
        • 1.4.3 后台线程
      • 1.5 索引文件的数据结构
        • 1.5.1 InnoDB 逻辑存储结构
        • 1.5.2 哈希表
        • 1.5.3 B树
        • 1.5.4 B+树
      • 1.6 MySQL 数据存储文件
        • 1.6.1 MyISAM
        • 1.6.2 InnoDB
        • 1.6.3 聚集索引
      • 1.7 索引种类
      • 1.8 存储引擎分类
      • 1.9 索引的使用
        • 1.9.1 联合索引最左匹配
        • 1.9.2 覆盖索引
        • 1.9.3 索引条件下推(ICP)
        • 1.9.4 索引的创建
        • 1.9.5 什么时候用不到索引?
      • 1.10 索引维护
    • 二、Mysql日志
      • 2.1 Redo日志和Undo日志
      • 2.2 binlog
      • 2.3 数据更新的流程
    • 三、锁
      • 3.1 MySQL锁的基本介绍
        • 3.1.1 表级锁
        • 3.1.2 行锁
        • 3.1.3 共享锁(S锁,读锁)
        • 3.1.4 排它锁(X锁,写锁)
        • 3.1.5 意向共享锁(IS)和意向排它锁(IX)
        • 3.1.6 乐观锁
        • 3.1.7 悲观锁
        • 3.1.8 间隙锁、记录锁、临键锁
        • 3.1.9 死锁
      • 3.2 MyISAM表锁
      • 3.3 InnoDB锁
      • 3.4 总结
    • 四、事务
      • 4.1 事务的四大特性(ACID)
      • 4.2 事务的四大隔离级别
      • 4.3 隔离级别造成大三大问题
      • 4.4 为什么 MySQL 的 InnDB 引擎能解决幻读问题
    • 五、主从复制
      • 5.1 mysql复制原理
      • 5.2 mysql主从形式
      • 5.3 mysql主从同步延时分析
      • 5.4 主从复制示例
        • 5.4.1 配置主库
        • 5.4.2 配置从库
    • 六、读写分离

一、Mysql索引

1.1 一条SQL查询语句是如何执行的(说法1)

 如图:
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第1张图片
 大体来说,MySQL可以分为Server层和存储引擎层两部分。
Server层包括连接器、查询缓存、分析器、优化器、执行器等,提供了Mysql Server 数据库所有逻辑功能,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图、函数等。
存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。
 存储引擎是MySQL中具体与文件打交道的子系统,MySQL区别于其他数据库的最重要特点是其插件式的表存储引擎,其根据文件访问层抽象接口来定制一种文件访问的机制(该机制叫存储引擎)。物理文件包括:redolog、undolog、binlog、errorlog、querylog、slowlog、data、index等。也就是说,你执行create table建表的时候,如果不指定引擎类型,默认使用的就是InnoDB。不过,你也可以通过指定存储引擎的类型来选择别的引擎。

1.1.1 连接器

 连接器负责跟客户端建立连接、获取权限、维持和管理连接。连接命令一般是这么写的:

mysql -h$ip -P$port -u$user -p

 连接命令中的mysql是客户端工具,用来跟服务端建立连接。在完成经典的TCP握手后,连接器就要开始认证你的身份,这个时候用的就是你输入的用户名和密码。
 如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。这就意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。
 连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在show processlist命令中看到它:
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第2张图片
 客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数wait_timeout控制的,默认值是8小时。

 数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。建立连接的过程通常是比较复杂的,所以建议尽量使用长连接。

 全部使用长连接后,你可能会发现,有些时候MySQL占用内存涨得特别快,这是因为MySQL在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是MySQL异常重启了。两种两种方案:

1、定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
2、如果你用的是MySQL 5.7或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

1.1.2 查询缓存

 连接建立完成后,你就可以执行select语句了。执行逻辑就会来到第二步:查询缓存。MySQL拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以key-value对的形式,被直接缓存在内存中。key是查询的语句,value是查询的结果。如果你的查询能够直接在这个缓存中找到key,那么这个value就会被直接返回给客户端。如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。
 但是,查询缓存往往弊大于利。查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此,对于更新压力大的数据库来说,查询缓存的命中率会非常低。可以将参数query_cache_type设置成DEMAND,这样对于默认的SQL语句都不使用查询缓存。对于确定要使用查询缓存的语句,可以用SQL_CACHE显式指定:

mysql> select SQL_CACHE * from T where ID=10

 需要注意的是,MySQL 8.0版本直接将查询缓存的整块功能删掉了。

1.1.3 分析器

 如果没有命中查询缓存,就要开始真正执行语句了。分析器先会做“词法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个SQL语句是否满足MySQL语法。语法分析之后是语义解析,即检查表名、列名等是否存在,是否正确。

1.1.4 优化器

 优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(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。

 两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。

1.1.5 执行器

 开始执行的时候,要先判断一下你对这个表T有没有执行查询的权限,如果没有,就会返回没有权限的错误。如果命中查询缓存,会在查询缓存放回结果的时候,做权限验证。查询也会在优化器之前调用precheck验证权限)。示例:

mysql> select * from T where ID=10

 如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。比如我们这个例子中的表T中,ID字段没有索引,那么执行器的执行流程是这样的:

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

 对于有索引的表,执行的逻辑也差不多。第一次调用的是“取满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。

1.2 一条SQL查询语句是如何执行的(说法2)

Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第3张图片

1.2.1 跟数据库建立连接

 MySQL 必须要运行一个服务,监听默认的 3306 端口。
 开发系统跟第三方对接的时候,必须要弄清楚的有两件事。

1、通信协议,比如我们是用 HTTP 还是 WebService 还是 TCP?
2、消息格式,比如我们用 XML 格式,还是 JSON 格式,还是定长格式?报文头长度多少,包含什么内容,每个字段的详细含义。

 MySQL 是支持多种通信协议的,可以使用同步/异步的方式,支持长连接/短连接。使用异步方式的话,服务端带来巨大的压力(一个连接就会创建一个线程,线程间切换会占用大量 CPU 资源);另外异步通信还带来了编码的复杂度,所以一般不建议使用。如果要异步,必须使用连接池,排队从连接池获取连接而不是创建新连接。
 MySQL 既支持短连接,也支持长连接。短连接就是操作完毕以后,马上 close 掉。长连接可以保持打开,减少服务端创建和释放连接的消耗,后面的程序访问的时候还可以使用这个连接。一般我们会在连接池中使用长连接。
 连接时长默认为8小时。
 查看 MySQL 当前有多少个连接命令:

show global status like 'Thread%';

Threads_cached:缓存中的线程连接数。
Threads_connected:当前打开的连接数。
Threads_created:为处理连接创建的线程数。
Threads_running:非睡眠状态的连接数,通常指并发连接数。

 可以用"show processlist"查看连接状态,一些常见的状态:
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第4张图片
 MySQL 服务允许的最大连接数是多少呢?在 5.7 版本中默认是 151 个,最大可以设置成 16384(2^14)。查看命令是:

show variables like 'max_connections';

 MySQL 支持哪些通信协议呢?第一种是 Unix Socket,默认是使用该协议,如果指定-h 参数,就会用第二种方式,TCP/IP 协议:

mysql -h192.168.8.211 -uroot -p123456

 编程语言的连接模块都是用TCP协议连接到MySQL服务器的。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第5张图片
 MySQL 使用了半双工的通信方式,所以客户端发送 SQL 语句给服务端的时候,(在一次连接里面)数据是不能分成小块发送的,不管你的 SQL 语句有多大,都是一次性发送。另一方面,对于服务端来说,也是一次性发送所有的数据,不能因为你已经取到了想要的数据就中断操作,这个时候会对网络和内存产生大量消耗。
 所以,一定要在程序里面避免不带 limit 的这种操作,比如一次把所有满足条件的数据全部查出来,一定要先 count 一下。如果数据量的话,可以分批查询。

1.2.2 查询缓存

 MySQL 的缓存默认是关闭的。为什么 MySQL 不推荐使用它自带的缓存呢?主要是因为 MySQL 自带的缓存的应用场景有限,第一个是它要求 SQL 语句必须一模一样,中间多一个空格,字母大小写不同都被认为是不同的的 SQL。第二个是表里面任何一条数据发生变化的时候,这张表所有缓存都会失效,所以对于有大量数据更新的应用,也不适合。

1.2.3 语法解析和预处理

 这一步主要做的事情是对语句基于 SQL 语法进行词法和语法分析和语义的解析。词法分析就是把一个完整的 SQL 语句打碎成一个个的单词。接下来就是语法分析,语法分析会对 SQL 做一些语法检查,比如单引号有没有闭合,然后根据 MySQL 定义的语法规则,根据 SQL 语句生成一个数据结构。这个数据结构我们把它叫做解析树(select_lex):
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第6张图片
 在解析的时候报错,解析 SQL 的环节里面有个预处理器。它会检查生成的解析树,解决解析器无法解析的语义。比如,它会检查表和列名是否存在,检查名字和别名,保证没有歧义。预处理之后得到一个新的解析树。

1.2.4 查询优化

 优化器最终会把解析树变成一个查询执行计划,查询执行计划是一个数据结构。MySQL 提供了一个执行计划的工具。我们在 SQL 语句前面加上 EXPLAIN,就可以看到执行计划的信息。

注意 Explain 的结果也不一定最终执行的方式。

1.2.5 存储引擎

 数据库的表在存储数据的同时,还要组织数据的存储结构,这个存储结构就是由我们的存储引擎决定的,所以我们也可以把存储引擎叫做表类型。在 MySQL 里面,支持多种存储引擎,他们是可以替换的,所以叫做插件式的存储引擎。
 MyISAM 和 InnoDB 是我们用得最多的两个存储引擎,在 MySQL 5.5 版本之前,默认的存储引擎是 MyISAM,5.5 版本之后默认的存储引擎改成了 InnoDB。

  • 1、MyISAM( 3 个文件)
     应用范围比较小。表级锁定限制了读/写的性能,因此在 Web 和数据仓库配置中,它通常用于只读或以读为主的工作。
     特点:

1.支持表级别的锁(插入和更新会锁表)。不支持事务。
2.拥有较高的插入(insert)和查询(select)速度。
3.存储了表的行数(count 速度更快)。

  • 2、InnoDB( 2 个文件)
     InnoDB 是一个事务安全(与 ACID 兼容)的 MySQL存储引擎,它具有提交、回滚和崩溃恢复功能来保护用户数据。InnoDB 行级锁(不升级为更粗粒度的锁)和 Oracle 风格的一致非锁读提高了多用户并发性和性能。InnoDB 将用户数据存储在聚集索引中,以减少基于主键的常见查询的 I/O。为了保持数据完整性,InnoDB 还支持外键引用完整性约束。
     特点:

1.支持事务,支持外键,因此数据的完整性、一致性更高。
2.支持行级别的锁和表级别的锁。
3.支持读写并发,写不阻塞读(MVCC)。
4.特殊的索引存放方式,可以减少 IO,提升查询效率。

 如何选择存储引擎?

如果对数据一致性要求比较高,需要事务支持,可以选择 InnoDB。
如果数据查询多更新少,对查询性能要求比较高,可以选择 MyISAM。
如果需要一个用于查询的临时表,可以选择 Memory。

1.2.6 执行引擎,返回结果

1.3 MySQL 体系结构

Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第7张图片

  1. Connector:用来支持各种语言和 SQL 的交互,比如 PHP,Python,Java 的JDBC;
  2. Management Serveices & Utilities:系统管理和控制工具,包括备份恢复、MySQL 复制、集群等等;
  3. Connection Pool:连接池,管理需要缓冲的资源,包括用户密码权限线程等等;
  4. SQL Interface:用来接收用户的 SQL 命令,返回用户需要的查询结果
  5. Parser:用来解析 SQL 语句;
  6. Optimizer:查询优化器;
  7. Cache and Buffer:查询缓存,除了行记录的缓存之外,还有表缓存,Key 缓存,权限缓存等等;
  8. Pluggable Storage Engines:插件式存储引擎,它提供 API 给服务层使用,跟具体的文件打交道。
  • 架构分层
     可以把 MySQL 分成三层,跟客户端对接的连接层,真正执行操作的服务层,和跟硬件打交道的存储引擎层:
    Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第8张图片
  • 1、连接层
     客户端要连接到 MySQL 服务器 3306 端口,必须要跟服务端建立连接,那么管理所有的连接,验证客户端的身份和权限,这些功能就在连接层完成。
  • 2、服务层
     查询缓存的判断、根据 SQL 调用相应的接口,对我们的 SQL 语句进行词法和语法的解析(比如关键字怎么识别,别名怎么识别,语法有没有错误等等)。然后就是优化器,MySQL 底层会根据一定的规则对我们的 SQL 语句进行优化,最后再交给执行器去执行。
  • 3、存储引擎
     存储引擎就是我们的数据真正存放的地方,在 MySQL 里面支持不同的存储引擎。

1.4 一条更新 SQL 是如何执行的?

 更新流程和查询流程,基本流程也是一致的,也就是说,它也要经过解析器、优化器的处理,最后交给执行器。区别就在于拿到符合条件的数据之后的操作。

1.4.1 缓冲池(Buffer Pool)

 InnnoDB 的数据都是放在磁盘上的,InnoDB 操作数据有一个最小的逻辑单位,叫做页(索引页和数据页)。对于数据的操作,不是每次都直接操作磁盘,因为磁盘的速度太慢了。InnoDB 使用了一种缓冲池的技术,也就是把磁盘读到的页放到一块内存区域里面。这个内存区域就叫 Buffer Pool。
 下一次读取相同的页,先判断是不是在缓冲池里。下一次读取相同的页,先判断是不是在缓冲池里面,如果是,就直接读取,不用再次访问磁盘。
 修改数据的时候,先修改缓冲池里面的页。内存的数据页和磁盘数据不一致的时候,我们把它叫做脏页。InnoDB 里面有专门的后台线程把 Buffer Pool 的数据写入到磁盘,每隔一段时间就一次性地把多个修改写入磁盘,这个动作就叫做刷脏。

1.4.2 InnoDB 内存结构和磁盘结构

Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第9张图片
内存结构
 Buffer Pool 主要分为 3 个部分:Buffer Pool、Change Buffer、Adaptive Hash Index,另外还有一个(redo)log buffer。

  • 1、Buffer Pool
     Buffer Pool 缓存的是页面信息,包括数据页、索引页。
     Buffer Pool 默认大小是 128M(134217728 字节),可以调整。
     内存的缓冲池写满了怎么办?InnoDB 用 LRU算法来管理缓冲池(链表实现,不是传统的 LRU,分成了 young 和 old),经过淘汰的数据就是热点数据。内存缓冲区对于提升读写性能有很大的作用。
  • 2、Change Buffer(写缓冲)
     如果这个数据页不是唯一索引,不存在数据重复的情况,也就不需要从磁盘加载索引页判断数据是不是重复(唯一性检查)。这种情况下可以先把修改记录在内存的缓冲池中,从而提升更新语句(Insert、Delete、Update)的执行速度。这一块区域就是 Change Buffer。5.5 之前叫 Insert Buffer 插入缓冲,现在也能支持 delete 和 update。
     把 Change Buffer 记录到数据页的操作叫做 merge。什么时候发生 merge?有几种情况:在访问这个数据页的时候,或者通过后台线程、或者数据库 shut down、redo log 写满时触发。如果数据库大部分索引都是非唯一索引,并且业务是写多读少,不会在写数据后立刻读取,就可以使用 Change Buffer(写缓冲)。
  • 3、Adaptive Hash Index
  • 4、( redo) Log Buffer
     InnoDB 把所有对页面的修改操作专门写入一个日志文件,并且在数据库启动时从这个文件进行恢复操作(实现 crash-safe)——用它来实现事务的持久性。
    Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第10张图片
     这个文件就是磁盘的 redo log(叫做重做日志),对应于/var/lib/mysql/目录下的ib_logfile0 和 ib_logfile1,每个 48M。这 种 日 志 和 磁 盘 配 合 的 整 个 过 程 , 其 实 就 是 MySQL 里 的 WAL 技 术(Write-Ahead Logging),它的关键点就是先写日志,再写磁盘。

同样是写磁盘,为什么不直接写到 db file 里面去?为什么先写日志再写磁盘?

 磁盘的最小组成单元是扇区,通常是 512 个字节。操作系统和内存打交道,最小的单位是页 Page。操作系统和磁盘打交道,读写磁盘,最小的单位是块 Block。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第11张图片
 如果我们所需要的数据是随机分散在不同页的不同扇区中,那么找到相应的数据需要等到磁臂旋转到指定的页,然后盘片寻找到对应的扇区,才能找到我们所需要的一块数据,一次进行此过程直到找完所有数据,这个就是随机 IO,读取数据速度较慢。假设我们已经找到了第一块数据,并且其他所需的数据就在这一块数据后边,那么就不需要重新寻址,可以依次拿到我们所需的数据,这个就叫顺序 IO。
 刷盘是随机 I/O,而记录日志是顺序 I/O,顺序 I/O 效率更高。因此先把修改写入日志,可以延迟刷盘时机,进而提升系统吞吐。当然 redo log 也不是每一次都直接写入磁盘,在 Buffer Pool 里面有一块内存区域(Log Buffer)专门用来保存即将要写入日志文件的数据,默认 16M,它一样可以节省磁盘 IO。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第12张图片
 redo log 的内容主要是用于崩溃恢复。磁盘的数据文件,数据来自 buffer pool。redo log 写入磁盘,不是写入数据文件。

Log Buffer 什么时候写入 log file?

 在我们写入数据到磁盘的时候,操作系统本身是有缓存的。flush 就是把操作系统缓冲区写入到磁盘。log buffer 写入磁盘的时机,由一个参数(innodb_flush_log_at_trx_commit)控制,默认是 1。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第13张图片
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第14张图片
 redo log特点:

1、redo log 是 InnoDB 存储引擎实现的,并不是所有存储引擎都有。
2、不是记录数据页更新之后的状态,而是记录这个页做了什么改动,属于物理日志。
3、redo log 的大小是固定的,前面的内容会被覆盖。

Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第15张图片
 check point 是当前要覆盖的位置。如果 write pos 跟 check point 重叠,说明 redo log 已经写满,这时候需要同步 redo log 到磁盘中。
 MySQL 的内存结构,总结一下,分为:Buffer pool、change buffer、Adaptive Hash Index、 log buffer。
磁盘结构
 磁盘结构里面主要是各种各样的表空间,叫做 Table space。表空间可以看做是 InnoDB 存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。InnoDB 的表空间分为 5 大类。

  • 1、系统表空间(system tablespace)
     在默认情况下 InnoDB 存储引擎有一个共享表空间(对应文件/var/lib/mysql/ibdata1),也叫系统表空间。InnoDB 系统表空间包含 InnoDB 数据字典和双写缓冲区,Change Buffer 和 Undo Logs。

 数据字典:由内部系统表组成,存储表和索引的元数据(定义信息)。
 双写缓冲(InnoDB 的一大特性):InnoDB 的页和操作系统的页大小不一致,InnoDB 页大小一般为 16K,操作系统页大小为 4K,InnoDB 的页写入到磁盘时,一个页需要分 4 次写。

Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第16张图片
 如果存储引擎正在写入页的数据到磁盘时发生了宕机,可能出现页只写了一部分的情况,比如只写了 4K,就宕机了,这种情况叫做部分写失效(partial page write),可能会导致数据丢失。有个问题,如果这个页本身已经损坏了,用它来做崩溃恢复是没有意义的。所以在对于应用 redo log 之前,需要一个页的副本。如果出现了写入失效,就用页的副本来还原这个页,然后再应用 redo log。
 这个页的副本就是 double write,InnoDB 的双写技术。通过它实现了数据页的可靠性。跟 redo log 一样,double write 由两部分组成,一部分是内存的 double write,一个部分是磁盘上的 double write。因为 double write 是顺序写入的,不会带来很大的开销。在默认情况下,所有的表共享一个系统表空间,这个文件会越来越大,而且它的空间不会收缩。

  • 2、独占表空间(file-per-table tablespaces)
     可以让每张表独占一个表空间。这个开关通过 innodb_file_per_table 设置,默认开启。
     其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲(Double write buffer)等还是存放在原来的共享表空间内。
  • 3、通用表空间(general tablespaces)
     通用表空间也是一种共享的表空间,可以创建一个通用的表空间,用来存储不同数据库的表,数据路径和文件可以自定义。
  • 4、临时表空间(temporary tablespaces)
     存储临时表的数据,包括用户创建的临时表,和磁盘的内部临时表。对应数据目录下的 ibtmp1 文件。当数据服务器正常关闭时,该表空间被删除,下次重新产生。
  • 5、undo log tablespace
     undo log(撤销日志或回滚日志)记录了事务发生之前的数据状态(不包括 select)。如果修改数据时出现异常,可以用 undo log 来实现回滚操作(保持原子性)。在执行 undo 的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,属于逻辑格式的日志。
     redo Log 和 undo Log 与事务密切相关,统称为事务日志。undo Log 的数据默认在系统表空间 ibdata1 文件中,因为共享表空间不会自动收缩,也可以单独创建一个 undo 表空间。
     一个更新操作的流程,这是一个简化的过程(name 原值是 javaHuang):
update user set name = 'penyuyan' where id=1;

1、事务开始,从内存或磁盘取到这条数据,返回给 Server 的执行器;
2、执行器修改这一行数据的值为 penyuyan;
3、记录 name=qingshan 到 undo log;
4、记录 name=penyuyan 到 redo log;
5、调用存储引擎接口,在内存(Buffer Pool)中修改 name=penyuyan;
6、事务提交。

1.4.3 后台线程

 后台线程的主要作用是负责刷新内存池中的数据和把修改的数据页刷新到磁盘。后台线程分为:master thread,IO thread,purge thread,page cleaner thread。

 master thread 负责刷新缓存数据到磁盘并协调调度其它后台进程。
 IO thread 分为 insert buffer、log、read、write 进程。分别用来处理 insert buffer、重做日志、读写请求的 IO 回调。
 purge thread 用来回收 undo 页。
 page cleaner thread 用来刷新脏页。
 除了 InnoDB 架构中的日志文件,MySQL 的 Server 层也有一个日志文件,叫做binlog,它可以被所有的存储引擎使用。

  • Binlog
     binlog 以事件的形式记录了所有的 DDL 和 DML 语句(因为它记录的是操作而不是数据值,属于逻辑日志),可以用来做主从复制和数据恢复。跟 redo log 不一样,它的文件内容是可以追加的,没有固定大小限制。
     在开启了 binlog 功能的情况下,我们可以把 binlog 导出成 SQL 语句,把所有的操作重放一遍,来实现数据的恢复。binlog 的另一个功能就是用来实现主从复制,它的原理就是从服务器读取主服务器的 binlog,然后执行一遍。
     一条更新语句(update teacher set name=‘盆鱼宴’ where id=1)的执行过程:

1、先查询到这条数据,如果有缓存,也会用到缓存。
2、把 name 改成盆鱼宴,然后调用引擎的 API 接口,写入这一行数据到内存,同时记录 redo log。这时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,可以随时提交。
3、执行器收到通知后记录 binlog,然后调用存储引擎接口,设置 redo log为 commit状态。
4、更新完成。

 重点:

1、先记录到内存,再写日志文件。
2、记录 redo log 分为两个阶段。
3、存储引擎和 Server 记录不同的日志。
4、先记录 redo,再记录 binlog。

1.5 索引文件的数据结构

  • 索引的文件存储形式与存储引擎有关
     数据库索引,是数据库管理系统(DBMS)中一个排序的数据结构,以协助快速查询、更新数据库表中数据。
    Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第17张图片
     存储引擎主要有: 1. MyIsam , 2. InnoDB, 3. Memory, 4. Archive, 5. Federated 。

  索引文件的结构:hash、二叉树、B树、B+树。

1.5.1 InnoDB 逻辑存储结构

 MySQL 的存储结构分为 5 级:表空间、段、簇、页、行。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第18张图片

  • 1、表空间(Table Space)
     表空间可以看做是 InnoDB 存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。分为:系统表空间、独占表空间、通用表空间、临时表空间、Undo 表空间。
  • 2、段(Segment)
     表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等,段是一个逻辑的概念。一个 ibd 文件(独立表空间文件)里面会由很多个段组成。创建一个索引会创建两个段,一个是索引段:leaf node segment,一个是数据段:non-leaf node segment。
     索引段管理非叶子节点的数据。数据段管理叶子节点的数据。也就是说,一个表的段数,就是索引的个数乘以 2。
  • 3、簇(Extent)
     一个段(Segment)又由很多的簇(也可以叫区)组成,每个区的大小是 1MB(64个连续的页)。
     每一个段至少会有一个簇,一个段所管理的空间大小是无限的,可以一直扩展下去,但是扩展的最小单位就是簇。
  • 4、页(Page)
     为了高效管理物理空间,对簇进一步细分,就得到了页。簇是由连续的页(Page)组成的空间,一个簇中有 64 个连续的页。(1MB/16KB=64)。这些页面在物理上和逻辑上都是连续的。
     InnoDB 也有页的概念(也可以称为块),每个页默认 16KB。页是 InnoDB 存储引擎磁盘管理的最小单位,通过 innodb_page_size 设置。

一个表空间最多拥有 2^32 个页,默认情况下一个页的大小为 16KB,也就是说一个表空间最多存储 64TB 的数据。

 操作系统和内存打交道,最小的单位是页 Page。文件系统的内存页通常是 4K。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第19张图片

  • 5、行(Row)
     InnoDB 存储引擎是面向行的(row-oriented),也就是说数据的存放按行进行存放。

1.5.2 哈希表

 本质上是个数组,往里面存元素时,先通过哈希函数求得一个int值,再想该int值转换为数组中对应的位置,然后将元素插入。哈希表的数组中其实都是存的k-v键值对,key是真正存放的数据,value一般是某个固定值。
 哈希表可以完成索引的存储,每次在添加索引的时候需要计算指定列的哈希值,取模运算后计算出下标,将元素插入下标位置。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第20张图片
 哈希索引的特点:

1、它的时间复杂度是 O(1),查询速度比较快。因为哈希索引里面的数据不是按顺序存储的,所以不能用于排序。
2、我们在查询数据的时候要根据键值计算哈希码,所以它只能支持等值查询(= IN),不支持范围查询(> < >= <= between and)。

 如果字段重复值很多的时候,会出现大量的哈希冲突(采用拉链法解决),效率会降低。
 哈希表在使用的时候,需要将全部的数据加载到内存,比较耗费内存空间,不是很合适。

  • InnoDB 内部使用哈希索引来实现自适应哈希索引特性
     InnoDB 只支持显式创建 B+Tree 索引,对于一些热点数据页,InnoDB 会自动建立自适应 Hash 索引,也就是在 B+Tree 索引基础上建立 Hash 索引,这个过程对于客户端是不可控制的,隐式的。

1.5.3 B树

 平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。当插入数据次数过多时,旋转次数较多,影响性能
 红黑树是基于AVL树的一个升级,损失了部分查询性能,来提升插入的性能,在红黑树中最低子树和最高子树之差小于2倍即可,在插入的时候,不需要进行N多次的旋转操作,而且还加入了变色的性能,来满足插入和查询性能的平衡。
 索引的数据,是放在硬盘上的。当我们用树的结构来存储索引的时候,访问一个节点就要跟磁盘之间发生一次 IO。InnoDB 操作磁盘的最小的单位是一页,大小是 16K。那么,一个树的节点就是 16K 的大小。
 如果我们一个节点只存一个键值+数据+引用,例如整形的字段,可能只用了十几个或者几十个字节,它远远达不到 16K 的容量,所以访问一个树节点,进行一次 IO 的时候,浪费了大量的空间。所以如果每个节点存储的数据太少,从索引中找到我们需要的数据,就要访问更多的节点,意味着跟磁盘交互次数就会过多。如果是机械硬盘时代,每次从磁盘读取数据需要 10ms 左右的寻址时间,交互次数越多,消耗的时间就越多。

 解决方案是什么呢?

 第一个就是让每个节点存储更多的数据。第二个,节点上的关键字的数量越多,我们的指针数也越多,也就是意味着可以有更多的分叉(我们把它叫做“路数”)。因为分叉数越多,树的深度就会减少(根节点是 0)。

 前面的结论就是:二叉树以及N多的变种都不能支撑索引,原因是树的深度过深,导致IO次数变多,影响数据读取的效率。所以此时需要用多叉树(B树)来实现索引。

  • B树示例1:
    Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第21张图片
     B树的特点:

1、所有键值分布在整棵树中
2、所有有可能在非叶子节点结束在关键字全集内做一次查找,性能逼近二分查找
3、每个节点最多拥有m个子树
4、根节点至少有2个子树
5、分支节点至少拥有m/2颗子树(除根节点也叶子节点之外都是分支节点)
6、所有叶子节点都在同一层、每个节点最多可以拥有m-1个key,并且以升序排列。

 在上面的例子中,假设每个节点占用一个磁盘块,一个节点上有两个升序排列的关键字和是哪个指向子树根节点的指针,指针存储的是子节点所在磁盘块的两个地址。两个关键词划分成的三个范围域对应的是三个指针指向的子树的数据的范围域。以根节点为例,关键字为16和34,P1指针指向的子树的数据范围为小于16,P2指针指向的子树的数据范围为16-34,P3指针指向的子树的数据范围为大于34。
 查找关键字过程:

1、根据根节点找到磁盘块1,读入内存(磁盘IO第一次)
2、比较关键字28在区间(16,34),找到磁盘块1的指针P2
3、根据P2指针找到磁盘块3,读入内存(磁盘IO第二次)
4、比较关键字28在区间(15,31),找到磁盘块3的指针P2
5、根据P2指针找到磁盘块8,读入内存(磁盘IO第三次)
6、在磁盘块8中的关键字列表中找到关键字28

 B树的缺点:

1、每个节点都有key和data,而每个页存储空间是有限的,如果data比较大的话会导致每个节点存储的key数量变小
2、当存储的数据量很大的时候会导致深度较大,增大查询时磁盘IO次数,进而影响查询性能

  • B树示例2:
     跟 AVL 树一样,B 树在枝节点和叶子节点存储键值、数据地址、节点引用。
     B 树的一个特点:分叉数(路数)永远比关键字数多 1。比如这棵树,每个节点存储两个关键字,那么就会有三个指针指向三个子节点:
    Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第22张图片

1.5.4 B+树

  • 1、B+树示例1:
     InnoDB 底层存储结构为B+树, B树的每个节点对应innodb的一个page, page大小是固定的,一般设为 16k。其中非叶子节点只有键值,叶子节点包含完成数据。
     B+树是在B树的基础上做的一种优化,变化如下:

1、B+树每个节点可以包含更多的节点,这样做的原因有两个:一是为了降低树的高度,二是将数据范围变成多个区间,区间越多,数据检索越快
2、非叶子节点存储key,叶子节点存储key和数据
3、叶子节点两两指针相互连接(符合磁盘的预读特性),顺序查询性能更高。

Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第23张图片
 在B+树上有两个头指针,一个指向根节点,一个另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+树进行两种查找:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进随机查找。

 Innodb,B+树叶子节点直接放置数据的示例:
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第24张图片
 Innodb是通过B+树结构对主键创建索引,然后叶子节点中存储记录,如果没有主键,那么会选择唯一键,如果没有唯一键,那么会生成一个6位的row_id来作为主键。
 如果创建索引的键是其他字段,那么在叶子节点中存储的是该记录的主键,然后再通过主键索引来找到对应的记录,叫做回表。
 MyISAM,B+树和Inondb相比,差异之处就在于索引和表数据是分开存储的,看个例子:
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第25张图片
 InnoDB索引适用场景:

1)经常更新的表,适合处理多重并发的更新请求。
2)支持事务。
3)可以从灾难中恢复(通过 bin-log 日志等)。
4)外键约束。只有他支持外键。
5)支持自动增加列属性 auto_increment。

  • 2、B+树示例2:
     B+树的存储结构:
    Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第26张图片
     MySQL 中的 B+Tree 有几个特点:
  1. 它的关键字的数量是跟路数相等的;
  2. B+Tree 的根节点和枝节点中都不会存储数据,只有叶子节点才存储数据。搜索到关键字不会直接返回,会到最后一层的叶子节点。比如我们搜索 id=28,虽然在第一层直接命中了,但是全部的数据在叶子节点上面,所以我还要继续往下搜索,一直到叶子节点。

在 InnoDB 中 B+ 树深度一般为 1-3 层,它就能满足千万级的数据存储。

  1. B+Tree 的每个叶子节点增加了一个指向相邻叶子节点的指针,它的最后一个数据会指向下一个叶子节点的第一个数据,形成了一个有序链表的结构。
  2. 它是根据左闭右开的区间 [ )来检索数据。

 InnoDB 中的 B+Tree 的特点:

1)它是 B Tree 的变种,B Tree 能解决的问题,它都能解决。B Tree 解决的两大问题是什么?(每个节点存储更多关键字;路数更多)
2)扫库、扫表能力更强(如果我们要对表进行全表扫描,只需要遍历叶子节点就可以了,不需要遍历整棵 B+Tree 拿到所有的数据)
3) B+Tree 的磁盘读写能力相对于 B Tree 来说更强(根节点和枝节点不保存数据区,所以一个节点可以保存更多的关键字,一次磁盘加载的关键字更多)
4)排序能力更强(因为叶子节点上有下一个数据区的指针,数据形成了链表)
5)效率更加稳定(B+Tree 永远是在叶子节点拿到数据,所以 IO 次数是稳定的)

1.6 MySQL 数据存储文件

 每张 InnoDB 的表有两个文件(.frm 和.ibd),MyISAM 的表有三个文件(.frm、.MYD、.MYI)。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第27张图片

 frm 是 MySQL 里面表结构定义的文件,不管你建表的时候选用任何一个存储引擎都会生成。

1.6.1 MyISAM

 在 MyISAM 里面,另外有两个文件:一个是.MYD 文件,D 代表 Data,是 MyISAM 的数据文件,存放数据记录。一个是.MYI 文件,I 代表 Index,是 MyISAM 的索引文件,存放索引。
 MyISAM 的 B+Tree 里面,叶子节点存储的是数据文件对应的磁盘地址。所以从索引文件.MYI 中找到键值后,会到数据文件.MYD 中获取相应的数据记录。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第28张图片

1.6.2 InnoDB

 在 InnoDB 里面,它是以主键为索引来组织数据的存储的,所以索引文件和数据文件是同一个文件,都在.ibd 文件里面。在 InnoDB 的主键索引的叶子节点上,它直接存储了我们的数据。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第29张图片

1.6.3 聚集索引

 聚集索引:索引键值的逻辑顺序跟表数据行的物理存储顺序是一致的。(比如字典的目录是按拼音排序的,内容也是按拼音排序的,按拼音排序的这种目录就叫聚集索引)。
 在 InnoDB 里面,它组织数据的方式叫做叫做(聚集)索引组织表,主键索引是聚集索引,非主键都是非聚集索引。
 InnoDB 中,主键索引和辅助索引是有一个主次之分的。辅助索引存储的是辅助索引和主键值。如果使用辅助索引查询,会根据主键值在主键索引中查询,最终取得数据。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第30张图片
如果一张表没有主键怎么办?

1、如果我们定义了主键(PRIMARY KEY),那么 InnoDB 会选择主键作为聚集索引。
2、如果没有显式定义主键,则 InnoDB 会选择第一个不包含有 NULL 值的唯一索引作为主键索引。
3、如果也没有这样的唯一索引,则 InnoDB 会选择内置 6 字节长的 ROWID 作为隐藏的聚集索引,它会随着行记录的写入而主键递增。

1.7 索引种类

 在 InnoDB 里面,索引类型有三种,普通索引、唯一索引(主键索引是特殊的唯一索引)、全文索引。
 mysql索引的五种类型:主键索引、唯一索引、普通索引和全文索引、组合索引。
 通过给字段添加索引,可以提高数据的读取速度,提高项目的并发能力和抗压能力。

 普通索引:仅加速查询
 唯一索引:加速查询 + 列值唯一(可以有null)
 主键索引:加速查询 + 列值唯一(不可以有null)+ 表中只有一个
 组合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
 全文索引:对文本的内容进行分词,进行搜索

  1. 主键索引
     主键是一种唯一性索引,但它必须指定为PRIMARY KEY,每个表只能有一个主键。主键一般情况可以设置为自增,主键自增时,插入数据时都是在最后追加,不会在表中间插入数据,索引方便维护。
  2. 唯一索引
     索引列的所有值都只能出现一次,即必须唯一,值可以为空。
  3. 普通索引
     基本的索引类型,值可以为空,没有唯一性的限制。(覆盖索引,简单来说select查询的数据列只用从索引中就能够取得,不必从数据表中读取,换句话说查询列要被所使用的索引覆盖
  4. 全文索引
     全文索引的索引类型为FULLTEXT。全文索引可以在varchar、char、text类型的列上创建。
     MyISAM支持,Innodb在5.6之后支持。

用 like + % 就可以实现模糊匹配了,为什么还要全文索引?like + % 在文本比较少时是合适的,但是对于大量的文本数据检索,是不可想象的。全文索引在大量的数据面前,能比 like + % 快 N 倍,速度不是一个数量级,但是全文索引可能存在精度问题。

  1. 组合索引
    多列值组成一个索引,专门用于组合搜索
     如果对多列进行索引(组合索引),列的顺序非常重要,MySQL仅能对索引最左边的前缀进行有效的查找。例如:
     假设存在组合索引(c1,c2),查询语句select * from t1 where c1=1 and c2=2能够使用该索引。查询语句select * from t1 where c1=1也能够使用该索引。但是,查询语句select * from t1 where c2=2不能够使用该索引,因为没有组合索引的引导列,即要想使用c2列进行查找,必需出现c1等于某值。
     这就是最左匹配原则。简单来说,在两个列上的组合索引,有个前后顺序(c1,c2),在查询c1时可以使用该组合索引,在同时查询c1、c2时也可以使用该索引,但只查询c2时不能使用该索引。

1.8 存储引擎分类

  两种存储引擎的简单对比:
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第31张图片

1.9 索引的使用

 列的离散度:count(distinct(column_name)) : count(*),列的全部不同值和所有数据行的比例。数据行数相同的情况下,分子越大,列的离散度就越高。

 简单来说,如果列的重复值越多,离散度就越低,重复值越少,离散度就越高。

建立索引,要使用离散度(选择度)更高的字段,否则B+Tree 里面的重复值太多,MySQL 的优化器发现走索引跟使用全表扫描差不了多少的时候,就算建了索引,也不一定会走索引。

1.9.1 联合索引最左匹配

 比如在 user 表上面,给 name 和 phone 建立了一个联合索引:

ALTER TABLE user_innodb DROP INDEX comidx_name_phone;
ALTER TABLE user_innodb add INDEX comidx_name_phone (name,phone);

Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第32张图片
 联合索引在 B+Tree 中是复合的数据结构,它是按照从左到右的顺序来建立搜索树的(name 在左边,phone 在右边)。
 name 是有序的,phone 是无序的。当 name 相等的时候,phone 才是有序的。这个时候我们使用where name='青山' and phone = '136xx '去查询数据的时候,B+Tree 会优先比较 name 来确定下一步应该搜索的方向,往左还是往右。如果 name相同的时候再比较 phone。但是如果查询条件没有 name,就不知道第一步应该查哪个节点,因为建立搜索树的时候 name 是第一个比较因子,所以用不到索引。
 仍以上面的表为例:

  1. 使用两个字段,可以用到联合索引:
EXPLAIN SELECT * FROM user_innodb WHERE name= '权亮' AND phone = '15204661800';
  1. 使用左边的 name 字段,可以用到联合索引:
EXPLAIN SELECT * FROM user_innodb WHERE name= '权亮'
  1. 使用右边的 phone 字段,无法使用索引,全表扫描:
EXPLAIN SELECT * FROM user_innodb WHERE phone = '15204661800'

1.9.2 覆盖索引

 非主键索引,我们先通过索引找到主键索引的键值,再通过主键值查出索引里面没有的数据,它比基于主键索引的查询多扫描了一棵索引树,这个过程就叫回表
 例如:select * from user_innodb where name = 'qingshan';
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第33张图片
 在辅助索引里面,不管是单列索引还是联合索引,如果 select 的数据列只用从索引中就能够取得,不必从数据区中读取,这时候使用的索引就叫做覆盖索引,这样就避免了回表。

1.9.3 索引条件下推(ICP)

 索引条件下推(Index Condition Pushdown),简称ICP。MySQL5.6新添加,用于优化数据的查询。
 索引条件下推优化(Index Condition Pushdown (ICP) )是MySQL5.6添加的,用于优化数据查询。不使用索引条件下推优化时存储引擎通过索引检索到数据,然后返回给MySQL服务器,服务器然后判断数据是否符合条件。
当使用索引条件下推优化时,如果存在某些被索引的列的判断条件时,MySQL服务器将这一部分判断条件传递给存储引擎,然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器。索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少MySQL服务器从存储引擎接收数据的次数。
 开启 ICP:

set optimizer_switch='index_condition_pushdown=on';

1.9.4 索引的创建

  1. 在用于 where 判断 order 排序和 join 的(on)字段上创建索引
  2. 索引的个数不要过多。
     浪费空间,更新变慢。
  3. 区分度低的字段,例如性别,不要建索引。
     离散度太低,导致扫描行数过多。
  4. 频繁更新的值,不要作为主键或者索引。
     页分裂
  5. 组合索引把散列性高(区分度高)的值放在前面。
  6. 创建复合索引,而不是修改单列索引。

1.9.5 什么时候用不到索引?

  1. 索引列上使用函数(replace\SUBSTR\CONCAT\sum count avg)、表达式、计算(+ - * /)
  2. 字符串不加引号,出现隐式转换
  3. like 条件中前面带%
     过滤的开销太大,所以无法使用索引。这个时候可以用全文索引。
  4. 负向查询
     NOT LIKE 不能;

 一个 SQL 语句是否使用索引,跟数据库版本、数据量、数据选择度都有关系。其实,用不用索引,最终都是优化器说了算。
 优化器是基于 cost 开销,它不是基于规则,也不是基于语义。怎么样开销小就怎么来。

1.10 索引维护

 索引在插入新的值的时候,为了维护索引的有序性,必须要维护,在维护索引的时候需要需要分以下集中情况:

  1. 如果插入一个比较大的值,直接插入即可,几乎没有成本
  2. 如果插入的是中间的某一个值,需要逻辑上移动后续的元素,空出位置
  3. 如果需要插入的数据页满了,就需要单独申请一个新的数据页,然后移动部分数据过去,叫做页分裂,此时性能会受影响同时空间的使用率也会降低。

尽量使用自增主键作为索引

二、Mysql日志

2.1 Redo日志和Undo日志

这两种日志是存储引擎层面的日志

  • 1、Redo日志
    Redo日志,innodb存储引擎的日志文件
     当发生数据修改的时候,innodb引擎会先将记录写到redo log中,并更新内存,此时更新就算是完成了,同时innodb引擎会在合适的时机将记录操作到磁盘中。
      Redolog是固定大小的,是循环写的过程。
     有了redolog之后,innodb就可以保证即使数据库发生异常重启,之前的记录也不会丢失,叫做crash-safe。
     Redo保证了持久性(隔离性是通过锁来实现)。
    Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第34张图片
  • 2、Undo日志
    Undo Log是为了实现事务的原子性,在MySQL数据库InnoDB存储引擎中,还用Undo Log来实现多版本并发控制。
     在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为Undo Log)。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。

 注意:undo log是逻辑日志,可以理解为:

当delete一条记录时,undo log中会记录一条对应的insert记录
当insert一条记录时,undo log中会记录一条对应的delete记录
当update一条记录时,它记录一条对应相反的update记录

2.2 binlog

binlog是Server层面的日志,主要做mysql功能层面的事情。
 binlog与redo日志的区别:

  1. redo是innodb独有的,binlog是所有引擎都可以使用的
  2. redo是物理日志,记录的是在某个数据页上做了什么修改,binlog是逻辑日志,记录的是这个语句的原始逻辑
  3. redo是循环写的,空间会用完,binlog是可以追加写的,不会覆盖之前的日志信息

 Binlog中会记录所有的逻辑,并且采用追加写的方式。一般在企业中数据库会有备份系统(用于应付数据丢失等情况),可以定期执行备份,备份的周期可以自己设置。
 恢复数据的过程:

  1. 找到最近一次的全量备份数据
  2. 从备份的时间点开始,将备份的binlog取出来,重放到要恢复的那个时刻

2.3 数据更新的流程

Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第35张图片
 执行流程:

  1. 执行器先从引擎中找到数据,如果在内存中直接返回,如果不在内存中,查询后返回;
  2. 执行器拿到数据之后会先修改数据,然后调用引擎接口重新写入数据;
  3. 引擎将数据更新到内存,同时写数据到redo中,此时处于prepare阶段,并通知执行器执行完成,随时可以操作;
  4. 执行器生成这个操作的binlog;
  5. 执行器调用引擎的事务提交接口,引擎把刚刚写完的redo改成commit状态,更新完成。

 Redo log为什么两阶段提交?

  • 1、先写 redo log 后写 binlog
     假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
  • 2、先写 binlog 后写 redo log
     如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。

 可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。

三、锁

3.1 MySQL锁的基本介绍

锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的 计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一 个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。
 相对其他数据库而言,MySQL的锁机制比较简单,其最 显著的特点是不同的存储引擎支持不同的锁机制。比如:
  1. MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);
  2. InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。

3.1.1 表级锁

 表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。
 该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。表锁被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁。
 MyISAM只支持表锁,因此性能相对Innodb来说相对降低,而Innodb支持表锁和行锁。

 开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

3.1.2 行锁

 行锁的是mysql锁中粒度最小的一种锁,因为锁的粒度很小,所以发生资源争抢的概率也最小,并发性能最大,但是也会造成死锁,每次加锁和释放锁的开销也会变大。
 主要是Innodb使用行锁,Innodb也是mysql在5.5.5版本之后默认使用的存储引擎。

 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

 从上述特点可见,很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适。
 仅从锁的角度 来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有 并发查询的应用,如一些在线事务处理(OLTP)系统。

 OLTP是传统的关系型数据库的主要应用,主要是基本的、日常的事务处理,例如银行交易。OLAP是数据仓库系统的主要应用,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果。

 OLTP 系统强调数据库内存效率,强调内存各种指标的命令率,强调绑定变量,强调并发操作;OLAP 系统则强调数据分析,强调SQL执行市场,强调磁盘I/O,强调分区等。

3.1.3 共享锁(S锁,读锁)

 共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

示例:若事务A对数据对象1加上S锁,则事务A可以读数据对象1但不能修改,其他事务只能再对数据对象1加S锁,而不能加X锁,直到事务A释放数据对象1上的S锁。这保证了其他事务可以读数据对象1,但在事务A释放数据对象1上的S锁之前不能对数据对象1做任何修改。

3.1.4 排它锁(X锁,写锁)

 排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁。

示例:若事务A对数据对象1加上X锁,事务A可以读数据对象1也可以修改数据对象1,其他事务不能再对数据对象1加任何锁,直到事务A释放数据对象1上的锁。这保证了其他事务在事务A释放数据对象1上的锁之前不能再读取和修改数据对象1。

3.1.5 意向共享锁(IS)和意向排它锁(IX)

 意向共享锁(IS):事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁。
 意向互斥锁(IX):事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁。
 意向共享锁和意向排它锁总称为意向锁。意向锁的出现是为了支持Innodb支持多粒度锁。
 意向锁是表级别锁。

当我们需要给一个加表锁的时候,我们需要根据意向锁去判断表中有没有数据行被锁定,以确定是否能加成功。如果意向锁是行锁,那么我们就得遍历表中所有数据行来判断。如果意向锁是表锁,则我们直接判断一次就知道表中是否有数据行被锁定了。所以说将意向锁设置成表级别的锁的性能比行锁高的多。

 有了意向锁之后,前面例子中的事务A在申请行锁(写锁)之前,数据库会自动先给事务A申请表的意向排他锁。当事务B去申请表的写锁时就会失败,因为表上有意向排他锁之后事务B申请表的写锁时会被阻塞。
 意向锁的作用就是:当一个事务在需要获取资源的锁定时,如果该资源已经被排他锁占用,则数据库会自动给该事务申请一个该表的意向锁。如果自己需要一个共享锁定,就申请一个意向共享锁。如果需要的是某行(或者某些行)的排他锁定,则申请一个意向排他锁。

3.1.6 乐观锁

 乐观锁不是数据库自带的,需要我们自己去实现。示例:

  1. SELECT data AS old_data, version AS old_version FROM …;
  2. 根据获取的数据进行业务操作,得到new_data和new_version
  3. UPDATE SET data = new_data, version = new_version WHERE version = old_version
    if (updated row > 0) {
     // 乐观锁获取成功,操作完成
    } else {
     // 乐观锁获取失败,回滚并重试
    }

 乐观锁的优点:乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统整体性能表现。
 乐观锁的缺点:乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。
 总结:读用乐观锁,写用悲观锁

3.1.7 悲观锁

 实现悲观锁时,必须先使用set autocommit=0;关闭mysql的autoCommit属性,因为查询出数据之后就要将该数据锁定。
 悲观锁的使用示例:

关闭自动提交后,我们需要手动开启事务。
//1.开始事务
begin; 或者 start transaction;
//2.查询出商品信息,然后通过for update锁定数据防止其他事务修改
select status from t_goods where id=1 for update;
//3.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//4.修改商品status为2
update t_goods set status=2;
//5.提交事务
commit; --执行完毕,提交事务

 在第2步我们将数据查询出来后直接加上排它锁(X)锁,防止别的事务来修改事务1,直到我们commit后,才释放了排它锁。
 悲观锁的优点:保证了数据处理时的安全性。
 悲观锁的缺点:加锁造成了开销增加,并且增加了死锁的机会。降低了并发性。

接下来三种锁(间隙锁、记录锁、临键锁)都是innodb的行锁,前面我们说过行锁是基于索引实现的,一旦加锁操作没有操作在索引上,就会退化成表锁。

3.1.8 间隙锁、记录锁、临键锁

 间隙锁,作用于非唯一索引上,主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。
 如果把事务的隔离级别降级为读提交(Read Committed, RC),间隙锁则会自动失效。
 记录锁,它封锁索引记录,作用于唯一索引上。
 临键锁,作用于非唯一索引上,是记录锁与间隙锁的组合。

3.1.9 死锁

 死锁是指两个或两个以上事务在执行过程中因争抢锁资源而造成的互相等待的现象。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第36张图片
 如何解决死锁?

1.等待事务超时,主动回滚。
2.进行死锁检查,主动回滚某条事务,让别的事务能继续走下去。

3.2 MyISAM表锁

 MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)表独占写锁(Table Write Lock)
 对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM表的读操作与写操作之间,以及写操作之间是串行的。
 假设有以下数据:

CREATE TABLE `mylock` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `NAME` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

INSERT INTO `mylock` (`id`, `NAME`) VALUES ('1', 'a');
INSERT INTO `mylock` (`id`, `NAME`) VALUES ('2', 'b');
INSERT INTO `mylock` (`id`, `NAME`) VALUES ('3', 'c');
INSERT INTO `mylock` (`id`, `NAME`) VALUES ('4', 'd');
  • 1、写锁阻塞读
     ​当一个线程获得对一个表的写锁之后,只有持有锁的线程可以对表进行更新操作。其他线程的读写操作都会等待,直到锁释放为止。
     ​在命令行可以这样给表加上写锁:lock table mylock write。加上该写锁之后,该session可以正常操作,别的session对该表的查询会阻塞。只有释放该锁:unlock tables后,别的session查询才能正常进行。
  • 2、读锁阻塞写
     ​一个session给表加读锁,这个session可以锁定表中的记录,但更新和访问其他表都会提示错误,同时,另一个session可以查询表中的记录,但更新就会出现锁等待。
     ​当session1给表加上读锁:lock table mylock read后,session1可以查询该表记录,session1不能查询没有锁定的表,session1插入或者更新表会提示错误。
     ​session2可以查询该表记录,可以查询或者更新未锁定的表,插入数据会等待获得锁。当session1释放锁unlock tables后,session2获得锁,更新成功。

 ​MyISAM在执行查询语句之前,会自动给涉及的所有表加读锁,在执行更新操作前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此用户一般不需要使用命令来显式加锁

  • 3、MyISAM的并发插入问题
     ​MyISAM表的读和写是串行的,这是就总体而言的,在一定条件下,MyISAM也支持查询和插入操作的并发执行。
     ​假如session1可以通过lock table mylock read local获取表的read local锁定,session1不能对表进行更新或者插入操作,session1不能查询没有锁定的表,session1不能访问其他session插入的记录。此时session2可以查询该表的记录,session2可以进行插入操作,但是更新会阻塞,。
     ​然后session1通过unlock tables释放锁资源,session1可以查看session2插入的记录,session2获取锁,更新操作完成。

 ​可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定争夺。

mysql> show status like 'table%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Table_locks_immediate | 352   |
| Table_locks_waited    | 2     |
+-----------------------+-------+

 ​如果Table_locks_waited的值比较高,则说明存在着较严重的表级锁争用情况。

3.3 InnoDB锁

 ​事务是由一组SQL语句组成的逻辑处理单元,事务具有4属性,通常称为事务的ACID属性:原子性、一致性、隔离性、持久性。
 相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多用户的并发操作,但与此同时,会带来一些问题:

  • 1、脏读
      一个事务正在对一条记录做修改,在这个事务并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”的数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做“脏读” 。
  • 2、不可重复读
     一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了,这种现象叫做“不可重复读”。
  • 3、幻读
     一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读” 。

 上述出现的问题都是数据库读一致性的问题,可以通过事务的隔离机制来进行保证。
 数据库的事务隔离越严格,并发副作用就越小,但付出的代价也就越大,因为事务隔离本质上就是使事务在一定程度上串行化,需要根据具体的业务需求来决定使用哪种隔离级别。

|                  | 脏读 | 不可重复读 | 幻读 |
| :--------------: | :--: | :--------: | :--: |
| read uncommitted ||||
|  read committed  |      |||
| repeatable read  |      |            ||
|   serializable   |      |            |      |

 可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况:

mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0     |
| Innodb_row_lock_time          | 18702 |
| Innodb_row_lock_time_avg      | 18702 |
| Innodb_row_lock_time_max      | 18702 |
| Innodb_row_lock_waits         | 1     |
+-------------------------------+-------+

 如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高。

InnoDB的行锁模式及加锁方法

  • 1、共享锁
     又称读锁。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
  • 2、排他锁
     又称写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。

 mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型
 如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。
 所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。

InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,Oracle是通过在数据块中对相应数据行加锁来实现的。
 InnoDB这种行锁实现特点意味着:InnoDB只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁

  • 1、在不通过索引条件查询的时候,innodb使用的是表锁而不是行锁
     比如创建一张表:
create table tab_no_index(id int,name varchar(10)) engine=innodb;
insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');

 session1可以通过select * from tab_no_index where id = 1 for update命令,只给一行加了排他锁,但是session2在请求其他行的排他锁的时候,会出现锁等待。原因是在没有索引的情况下,innodb只能使用表锁。

  • 2、创建带索引的表进行条件查询,innodb使用的是行锁
     比如创建一张表:
create table tab_with_index(id int,name varchar(10)) engine=innodb;
alter table tab_with_index add index id(id);
insert into tab_with_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');

 此时当一个session对一个行加锁时,不影响另一个session对别的行的操作。

3.4 总结

  • 1、MyISAM的表锁总结
  1. 共享读锁之间是兼容的,但共享读锁与排他写锁之间,以及排他写锁之间是互斥的,也就是说读和写是串行的。
  2. 在一定条件下,MyISAM允许查询和插入并发执行,我们可以利用这一点来解决应用中对同一表查询和插入的锁争用问题。
  3. MyISAM默认的锁调度机制是写优先,这并不一定适合所有应用,用户可以通过设置LOW_PRIORITY_UPDATES参数,或在INSERT、UPDATE、DELETE语句中指定LOW_PRIORITY选项来调节读写锁的争用。
  4. 由于表锁的锁定粒度大,读写之间又是串行的,因此,如果更新操作较多,MyISAM表可能会出现严重的锁等待,可以考虑采用InnoDB表来减少锁冲突。
  • 2、InnoDB表总结
  1. InnoDB的行锁是基于索引实现的,如果不通过索引访问数据,InnoDB会使用表锁。
  2. 在不同的隔离级别下,InnoDB的锁机制和一致性读策略不同。

 在了解InnoDB锁特性后,用户可以通过设计和SQL调整等措施减少锁冲突和死锁,包括:

  1. 尽量使用较低的隔离级别; 精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会;
  2. 选择合理的事务大小,小事务发生锁冲突的几率也更小;
  • 给记录集显式加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;
  1. 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会;
  2. 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响; 不要申请超过实际需要的锁级别;除非必须,查询时不要显示加锁;
  3. 对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。

四、事务

 事务是数据处理的最小操作单元,是一组不可再分割的操作集合,这个操作单元里的一系列操作要么都成功,要么都失败。/事务(Transaction)是一个操作序列。这些操作要么都做,要么都不做,是一个不可分割的工作单位。
 事务最主要的目的是为了数据一致性
 可以通过set session autoCommit = on/off 来设置mysql事务是否自动开启。如果我们设置autoCommit为off的时候,需要手动开启mysql事务。

begin;start transaction;----开启事务(2选1)
通过 commit;或者 rollback;设置事务提交或者回滚;

4.1 事务的四大特性(ACID)

  • 1、原子性
     原子性是指事务是一个不可分割的工作单位,事务中的操作要么都成功(commit),要么都失败(rollback)。
  • 2、一致性
     事务前后数据的完整性必须保持一致。
     事务执行的结果必须使数据库从一个一致性状态变到另一个一致性的状态。
  • 3、隔离性
     事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
  • 4、持久性
     持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

4.2 事务的四大隔离级别

  • 1、未提交读(read uncommitted)
     事务A对数据进行修改,但未提交。此时开启事务B,在事务B中能读到事务A中对数据库进行的未提交数据的修改。
  • 2、提交读(read committed)
     事务A对数据进行修改,但还未提交。此时开启事务B,在事务B中不能读到事务A中对数据库的修改。在事务B还没有关闭时,此时事务A提交对数据库的修改。这时候,我们在事务B中,可以查到事务A中对数据库的修改。这时存在一个问题,我们在同一个事务中,对数据库查询两次,但两次的结果是不一样的。
  • 3、可重复读(repetition read)
     在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。
  • 4、串行化(serializable)
     在开启事务A时,会产生锁表,此时别的事务会等待,等事务A结束时才会开启。

 不同的数据库采用的隔离级别也会不一样。oracle采用的提交读(RC),而mysql默认的存储引擎innodb采用的是可重复读(RR)。
 在RC级别下,所有的读取都是不加锁的,只有增删改的情况会加锁。

4.3 隔离级别造成大三大问题

Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第37张图片

  • 1、脏读
     一个事务能读到另一个事务未提交的数据,称为脏读。
  • 2、不可重复读
      一个事务读取到另一个事务已经提交的数据,称为不可重复读。
    不可重复读和脏读的区别就是 读到的修改的数据是否提交
  • 3、幻读
     幻读和不可重复读都是一个事务读取到另一个事务的已经提交后的数据,区别在于不可重复读是针对于update和delete,而幻读是针对于insert。

4.4 为什么 MySQL 的 InnDB 引擎能解决幻读问题

 通过行锁算法可以知道,在读取id=1的数据时就可以给这条数据加上锁,这样就能避免在对这条数据进行其他操作(update/delete),但是insert还能继续操作,这样只能避免了不可重复读却无法避免幻读。
 可以通过临键锁来解决幻读问题。但这是采用的悲观锁机制,不可避免的降低了性能,这是mysql不能接受的,在mysql中采用了以乐观锁为基础的MVCC(多版本并发控制)来解决幻读。

  • MVVC
     在InnoDB中,给每行增加两个隐藏字段来实现MVCC,一个用来记录数据行的创建时间,另一个用来记录行的过期时间(删除时间)。
     在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。
     在RR级别下:SELECT读取创建版本小于或等于当前事务版本号,并且删除版本为空或大于当前事务版本号的记录。这样可以保证在读取之前记录是存在的。
     INSERT将当前事务的版本号保存至行的创建版本号,UPDATE新插入一行,并以当前事务的版本号作为新行的创建版本号,同时将原记录行的删除版本号设置为当前事务版本号,DELETE将当前事务的版本号保存至行的删除版本号:
    Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第38张图片
     数据查询规则(都是数据库内部控制,无需我们操作):
  1. 查找数据行版本早于当前事务版本的数据行(行的系统版本号小于或者等于当前事务版本号)这样可以确保事务读取的行要么在事务开始之前就已经存在了,要么是事务本身插入或者修改的。
  2. 查找事务删除版本号要么为null,要么大于当前事务版本号。确保取出来的行记录在事务开始前没有被删除。

五、主从复制

 将数据库扩展成主从复制模式,将读操作和写操作分离开来,多台数据库分摊请求,从而减少单库的访问压力,进而应用得到优化。

 为什么需要主从复制?

  1. 在业务复杂的系统中,有这么一个情景,有一句sql语句需要锁表,导致暂时不能使用读的服务,那么就很影响运行中的业务,使用主从复制,让主库负责写,从库负责读,这样,即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运作
  2. 做数据的热备。
  3. 架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能。

 MySQL主从复制是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点MySQL 默认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,数据的更新可以在远程连接上进行,从节点可以复制主数据库中的所有数据库或者特定的数据库,或者特定的表。

5.1 mysql复制原理

  • 1、master服务器将数据的改变记录二进制binlog日志,当master上的数据发生改变时,则将其改变写入二进制日志中;
  • 2、slave服务器会在一定时间间隔内对master二进制日志进行探测其是否发生改变,如果发生改变,则开始一个I/OThread请求master二进制事件
  • 3、同时主节点为每个I/O线程启动一个dump线程,用于向其发送二进制事件,并保存至从节点本地的中继日志中,从节点将启动SQL线程从中继日志中读取二进制日志,在本地重放,使得其数据和主节点的保持一致,最后I/OThread和SQLThread将进入睡眠状态,等待下一次被唤醒。

 也就是说:

从库会生成两个线程,一个I/O线程,一个SQL线程;
I/O线程会去请求主库的binlog,并将得到的binlog写到本地的relay-log(中继日志)文件中;
主库会生成一个log dump线程,用来给从库I/O线程传binlog;
SQL线程,会读取relay log文件中的日志,并解析成sql语句逐一执行;

 简单来说:

  1. 主服务器将数据的更新记录到二进制日志中(记录被称作二进制日志事件)-- 主库线程;
  2. 从库将主库的二进制日志复制到本地的中继日志(relay log)-- 从库 I/O 线程;
  3. 从库读取中继日志中的事件,将其重放到数据中 – 从库 SQL 线程。

Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第39张图片
 需要注意的是:

1、master将操作语句记录到binlog日志中,然后授予slave远程连接的权限(master一定要开启binlog二进制日志功能;通常为了数据安全考虑,slave也开启binlog功能)。
2、slave开启两个线程:IO线程和SQL线程。其中:IO线程负责读取master的binlog内容到中继日志relay log里;SQL线程负责从relay log日志里读出binlog内容,并更新到slave的数据库里,这样就能保证slave数据和master数据保持一致了。
3、Mysql复制至少需要两个Mysql的服务,当然Mysql服务可以分布在不同的服务器上,也可以在一台服务器上启动多个服务。
4、Mysql复制最好确保master和slave服务器上的Mysql版本相同(如果不能满足版本一致,那么要保证master主节点的版本低于slave从节点的版本)。
5、master和slave两节点间时间需同步。

 主从复制的具体步骤:

  1. 从库通过手工执行change master to 语句连接主库,提供了连接的用户一切条件(user 、password、port、ip),并且让从库知道,二进制日志的起点位置(file名 position 号); start slave。
  2. 从库的IO线程和主库的dump线程建立连接。
  3. 从库根据change master to 语句提供的file名和position号,IO线程向主库发起binlog的请求。
  4. 主库dump线程根据从库的请求,将本地binlog以events的方式发给从库IO线程。
  5. 从库IO线程接收binlog events,并存放到本地relay-log中,传送过来的信息,会记录到master.info中。
  6. 从库SQL线程应用relay-log,并且把应用过的记录到relay-log.info中,默认情况下,已经应用过的relay 会自动被清理purge。

5.2 mysql主从形式

 有以下五种形式:一主一从、主主复制、一主多从、多主一从、联级复制。
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第40张图片

Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第41张图片
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第42张图片
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第43张图片
Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第44张图片

5.3 mysql主从同步延时分析

 mysql的主从复制都是单线程的操作,主库对所有DDL和DML产生的日志写进binlog,由于binlog是顺序写,所以效率很高,slave的sql thread线程将主库的DDL和DML操作事件在slave中重放。DML和DDL的IO操作是随机的,不是顺序,所以成本要高很多,另一方面,由于sql thread也是单线程的,当主库的并发较高时,产生的DML数量超过slave的SQL thread所能处理的速度,或者当slave中有大型query语句产生了锁等待,那么延时就产生了。
 解决方案:
  1、业务的持久化层的实现采用分库架构,mysql服务可平行扩展,分散压力。
  2、单个库读写分离,一主多从,主写从读,分散压力。这样从库压力比主库高,保护主库。
  3、服务的基础架构在业务和mysql之间加入memcache或者redis的cache层。降低mysql的读压力。
  4、不同业务的mysql物理上放在不同机器,分散压力。
  5、使用比主库更好的硬件设备作为slave,mysql压力小,延迟自然会变小。
  6、使用更加强劲的硬件设备
 MySQL 5.7可以使用并行复制原理解决复制延迟问题。

5.4 主从复制示例

 两个虚拟机:ip:192.168.2.21(主) ip:192.168.2.22(从)。

5.4.1 配置主库

  • 1、准备创建一个新用户用于从库连接主库
# 创建用户 
create user 'repl'@'%' identified by 'repl'; 
 
# 授权,只授予复制和客户端访问权限 
grant replication slave,replication client on *.* to 'repl'@'%' identified by 'repl'; 
  • 2、修改配置文件
     /etc/my.cnf 在[mysqld]下添加:
log-bin         = mysql-bin 
log-bin-index   = mysql-bin.index 
binlog_format   = mixed 
server-id       = 21 
sync-binlog     = 1 
character-set-server = utf8

 保存文件并重启主库:

service mysqld restart 

 配置说明:

log-bin:设置二进制日志文件的基本名;
log-bin-index:设置二进制日志索引文件名;
binlog_format:控制二进制日志格式,进而控制了复制类型,三个可选值
-STATEMENT:语句复制
-ROW:行复制
-MIXED:混和复制,默认选项
server-id:服务器设置唯一ID,默认为1,推荐取IP最后部分;
sync-binlog:默认为0,为保证不会丢失数据,需设置为1,用于强制每次提交事务时,同步二进制日志到磁盘上。

  • 3、备份主数据库数据
     若主库可以停机,则直接拷贝所有数据库文件。若主库是在线生产库,可采用 mysqldump 备份数据,因为它对所有存储引擎均可使用。
     为了获取一个一致性的快照,需对所有表设置读锁:
flush tables with read lock; 

 获取二进制日志的坐标:

show master status;

 返回结果示例:

+------------------+----------+--------------+------------------+-------------------+ 
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | 
+------------------+----------+--------------+------------------+-------------------+ 
| mysql-bin.000001 | 120 |              | |                   | 
+------------------+----------+--------------+------------------+-------------------+ 
1 row in set (0.00 sec) 

 备份数据:

# 针对事务性引擎 
mysqldump -uroot -ptiger --all-database -e --single-transaction --flush-logs --max_allowed_packet=1048576 --net_buffer_length=16384 > /data/all_db.sql 
 
# 针对 MyISAM 引擎,或多引擎混合的数据库 
mysqldump -uroot --all-database -e -l --flush-logs --max_allowed_packet=1048576 --net_buffer_length=16384 > /data/all_db.sql 

 恢复主库的写操作:

unlock tables;

5.4.2 配置从库

  • 1、修改配置文件
     /etc/my.cnf 在[mysqld]下添加:
log-bin             = mysql-bin
binlog_format       = mixed
log-slave-updates   = 0
server-id           = 22
relay-log           = mysql-relay-bin
relay-log-index     = mysql-relay-bin.index
read-only           = 1
slave_net_timeout   = 10

 保存文件并重启从库:

service mysqld restart

 配置说明:

log-slave-updates:控制 slave 上的更新是否写入二进制日志,默认为0;若 slave 只作为从服务器,则不必启用;若 slave 作为其他服务器的 master,则需启用,启用时需和 log-bin、binlog-format 一起使用,这样 slave 从主库读取日志并重做,然后记录到自己的二进制日志中;
relay-log:设置中继日志文件基本名;
relay-log-index:设置中继日志索引文件名;
read-only:设置 slave 为只读,但具有super权限的用户仍然可写;
slave_net_timeout:设置网络超时时间,即多长时间测试一下主从是否连接,默认为3600秒,即1小时,这个值在生产环境过大,我们将其修改为10秒,即若主从中断10秒,则触发重新连接动作。

  • 2、导入备份数据
mysql -uroot -p < /data/all_db.sql
  • 3、统一二进制日志的坐标
     此处使用的是新创建的账户:
change master to
master_host='192.168.2.21',
master_user='repl',
master_password='repl',
master_port=3306,
master_log_file='mysql-bin.000001',
master_log_pos=120;
  • 4、启动主从复制
     启动从库 slave 线程:
start slave;

 查看从服务器复制功能状态:

show slave status

六、读写分离

Mysql基础知识(三)Mysql架构、索引、日志、锁、主从复制、读写分离_第45张图片
 MySQL读写分离基本原理是让master数据库处理写操作,slave数据库处理读操作。master将写操作的变更同步到各个slave节点。
 MySQL读写分离能提高系统性能的原因在于:
  1、物理服务器增加,机器处理能力提升。拿硬件换性能。
  2、主从只负责各自的读和写,极大程度缓解X锁和S锁争用。
  3、slave可以配置myiasm引擎,提升查询性能以及节约系统开销。
  4、master直接写是并发的,slave通过主库发送来的binlog恢复数据是异步。
  5、slave可以单独设置一些参数来提升其读的性能。
  6、增加冗余,提高可用性。

你可能感兴趣的:(数据库,数据库,mysql)