1 课程内容

  • MySQL架构原理和存储机制
    MySQL体系结构(内存结构、磁盘结构)、SQL运行机制、存储引擎、Undo/Redo Log等等

  • MySQL索引存储机制和工作原理
    索引存储结构、索引查询原理、索引分析和优化、查询优化等

  • MySQL事务和锁工作原理
    事务隔离级别、事务并发处理、锁机制和实战等

  • MySQL集群架构及相关原理
    集群架构设计理念、主从架构、双主架构、分库分表等

  • 互联网海量数据处理实战
    ShardingSphere、MyCat中间实战操作,分库分表实战

  • MySQL第三方工具实战
    同步工具、运维工具、监控工具等

mysql 体积小,速度快,开源免费,简单易用,可以搭建高可用模式。

2 MySQL应用架构演变

2.1 单机单库

一个简单的小型网站或者应用背后的架构可以非常简单, 数据存储只需要一个MySQL Instance就能 满足数据读取和写入需求(这里忽略掉了数据备份的实例),处于这个的阶段系统,一般会把所有 的信息存到一个MySQL Instance里面

Mysql上笔记 - 图1

瓶颈:

  1. 数据量太大,超出一台服务器承受
  2. 读写操作量太大,超出一台服务器承受
  3. 一台服务器挂了,应用也会挂掉(可用性差)

2.2 主从架构

通过给Instance挂载从库解决读取的压力, 主库宕机也可以通过主从切换保障高可用。在MySQL的场景下就是通过主从结构(双主结构也属 于特殊的主从),主库抗写压力,通过从库来分担读压力,对于写少读多的应用,主从架构 完全能够胜任。

Mysql上笔记 - 图2

瓶颈

  1. 数据量太大,超出一台服务器承受
  2. 写操作太大,超出一台M服务器承受

2.3 分分库分表

通过水平拆分来解决,水平拆分和垂直拆分有 较大区别,垂直拆分拆完的结果,每一个实例都是拥有全部数据的,而水平拆分之后,任何实例都 只有全量的1/n的数据。以下图所示,将Userinfo拆分为3个Sharding,每个Sharding持有总量的 1/3数据,3个Sharding数据的总和等于一份完整数据

Mysql上笔记 - 图3

数据如何路由成为一个关键问题, 一般可以采用范围拆分,List拆分、Hash拆分等。 如何保持数据的一致性也是个难题。

2.4 云数据库

云数据库(云计算)现在是各大IT公司内部作为节约成本的一个突破口,对于数据存储的MySQL 来说,如何让其成为一个saas(Software as a Service)是关键点。MySQL作为一个saas服务, 服务提供商负责解决可配置性,可扩展性,多用户存储结构设计等这些疑难问题。

Mysql上笔记 - 图4

3 Mysql架构原理

Mysql上笔记 - 图5

MySQL Server架构自顶向下大致可以分网络连接层、服务层、存储引擎层和系统文件层

3.1 四个层次

网络连接层

客户端连接器(Client Connectors):提供与MySQL服务器建立的支持。目前几乎支持所有主流 的服务端编程技术,例如常见的 Java、C、Python、.NET等,它们通过各自API技术与MySQL建立

服务层

服务层是MySQL Server的核心,主要包含系统管理和控制工具、连接池、SQL接口、解析器、查询优化器和缓存六个部分

  1. 连接池(Connection Pool):负责存储和管理客户端与数据库的连接,一个线程负责管理一个 连接。线程可以进行复用, 可以避免线程随着连接关闭而销毁,线程的创建和销毁也很消耗资源。对写入,查询性能有很大的提升。

  2. 系统管理和控制工具(Management Services & Utilities):例如备份恢复、安全管理、集群 管理等

  3. SQL接口(SQL Interface):用于接受客户端发送的各种SQL命令,并且返回用户需要查询的结果。比如DML、DDL、存储过程、视图、触发器等。

  4. 解析器(Parser):负责将请求的SQL解析生成一个”解析树”。然后根据一些MySQL规则进一步 检查解析树是否合法。

  5. 查询优化器(Optimizer):当“解析树”通过解析器语法检查后,将交由优化器将其转化成执行计划,然后与存储引擎交互。

    1. select uid,name from user where gender=1;
    2. 选取--》投影--》联接 策略
    3. 1select先根据where语句进行选取,并不是查询出全部数据再过滤
    4. 2select查询根据uidname进行属性投影,并不是取出所有字段
    5. 3)将前面选取和投影联接起来最终生成查询结果
  1. 缓存(Cache): 缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,权限缓存,引擎缓存等。如果查询缓存有命中的查询结果,查询语句就可以直接去查询缓存中取数据。

存储引擎层(Pluggable Storage Engines)

存储引擎负责MySQL中数据的存储与提取,与底层系统文件进行交互。MySQL存储引擎是插件式的, 服务器中的查询执行引擎通过接口与存储引擎进行通信,接口屏蔽了不同存储引擎之间的差异 。现在有 很多种存储引擎,各有各的特点,最常见的是MyISAM和InnoDB。

系统文件层(File System)

该层负责将数据库的数据和日志存储在文件系统之上,并完成与存储引擎的交互,是文件的物理存储层。主要包含日志文件,数据文件,配置文件,pid 文件,socket 文件等。

  • 日志文件:
    错误日志(Error Log)
    默认开启 show variables like ‘%log_error%’;
    通用查询日志(General query log)
    记录一般查询语句 show variables like ‘%general_log%’;
    二进制日志(binary log)
    记录了对Mysql数据库执行的更改操作,并且记录了语句发生时间,执行时长;但是它不记录select,show 等不修改数据库的SQL。主要用于数据库恢复和主从复制。
    show variables like ‘%log_bin%’; 是否开启 以及记录的日志问及那
    show variables like ‘%binlog%’; 参数查看
    show binary logs; 查看日志文件
    慢查询日志(slow query log)
    记录所有执行时间超时的查询SQL,默认是10s。
    show variables like ‘%slow_query_log%’; 是否开启以及记录日志的文件
    show variables like ‘%long_query_time%’; 查看默认的超时时间

  • 配置文件
    用于存放MySQL所有的配置信息文件,比如my.cnf、my.ini等。

  • 数据文件
    db.opt 文件:记录这个库的默认使用的字符集和校验规则。
    frm 文件:存储与表相关的元数据(meta)信息,包括表结构的定义信息等,每一张表都会 有一个frm 文件。 (8版本没了,但是有一个sdi文件)
    sdi文件

    1. {"mysqld_version_id":80019,"dd_version":80017,"sdi_version":80019,"dd_object_type":"Table","dd_object":{"name":"b_area_info","mysql_version_id":80019,"created":20201117095044,"last_altered":20201117095044,"hidden":1,"options":"avg_row_length=0;key_block_size=0;keys_disabled=0;pack_record=1;stats_auto_recalc=0;stats_sample_pages=0;","columns":[{"name":"id","type":4,"is_nullable":false,"is_zerofill":false,"is_unsigned":true,"is_auto_increment":true,"is_virtual":false,"hidden":1,"ordinal_position":1,"char_length":10,"numeric_precision":10,"numeric_scale":0,"numeric_scale_null":false,"datetime_precision":0,"datetime_precision_null":1,"has_no_default":false,"default_value_null":false,"srs_id_null":true,"srs_id":0,"default_value":"AAAAAA==","default_value_utf8_null":true,"default_value_utf8":"","default_option":"","update_option":"","comment":"主键id","generation_expression":"","generation_expression_utf8":"","options":"interval_count=0;","se_private_data":"","column_key":2,"column_type_utf8":"int unsigned","elements":[],"collation_id":33,"is_explicit_collation":false},{"name":"name","type":16,"is_nullable":false,"is_zerofill":false,"is_unsigned":false,"is_auto_increment":false,"is_virtual":false,"hidden":1,"ordinal_position":2,"char_length":15,"numeric_precision":0,"numeric_scale":0,"numeric_scale_null":true,"datetime_precision":0,"datetime_precision_null":1,"has_no_default":true,"default_value_null":false,"srs_id_null":true,"srs_id":0,"default_value":"AAAAAAAAAAAAAAAAAAAAAA==","default_value_utf8_null":true,"default_value_utf8":"","default_option":"","update_option":"","comment":"区域名称","generation_expression":"","generation_expression_utf8":"","options":"interval_count=0;","se_private_data":"","column_key":1,"column_type_utf8":"varchar(5)","elements":[],"collation_id":33,"is_explicit_collation":false},{"name":"en_name","type":16,"is_nullable":false,"is_zerofill":false,"is_unsigned":false,"is_auto_increment":false,"is_virtual":false,"hidden":1,"ordinal_position":3,"char_length":45,"numeric_precision":0,"numeric_scale":0,"numeric_scale_null":true,"datetime_precision":0,"datetime_precision_null":1,"has_no_default":true,"default_value_null":false,"srs_id_null":true,"srs_id":0,"default_value":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==","default_value_utf8_null":true,"default_value_utf8":"","default_option":"","update_option":"","comment":"区域英文名","generation_expression":"","generation_expression_utf8":"","options":"interval_count=0;","se_private_data":"","column_key":1,"column_type_utf8":"varchar(15)","elements":[],"collation_id":33,"is_explicit_collation":false}],"schema_ref":"hrh_erp","se_private_id":18446744073709551615,"engine":"MyISAM","last_checked_for_upgrade_version_id":0,"comment":"区域信息","se_private_data":"","row_format":2,"partition_type":0,"partition_expression":"","partition_expression_utf8":"","default_partitioning":0,"subpartition_type":0,"subpartition_expression":"","subpartition_expression_utf8":"","default_subpartitioning":0,"indexes":[{"name":"PRIMARY","hidden":false,"is_generated":false,"ordinal_position":1,"comment":"","options":"flags=0;","se_private_data":"","type":1,"algorithm":2,"is_algorithm_explicit":false,"is_visible":true,"engine":"MyISAM","elements":[{"ordinal_position":1,"length":4,"order":2,"hidden":false,"column_opx":0}]}],"foreign_keys":[],"check_constraints":[],"partitions":[],"collation_id":33}}


MYD 文件:MyISAM 存储引擎专用,存放 MyISAM 表的数据(data),每一张表都会有一个 .MYD 文件。MyISAMData
MYI 文件:MyISAM 存储引擎专用,存放 MyISAM 表的索引相关信息,每一张 MyISAM 表对 应一个 .MYI 文件。MyISAMIndex
ibd文件和 IBDATA 文件:存放 InnoDB 的数据文件(包括索引)。InnoDB 存储引擎有两种 表空间方式:独享表空间和共享表空间。独享表空间使用 .ibd 文件来存放数据,且每一张 InnoDB 表对应一个 .ibd 文件。共享表空间使用 .ibdata 文件,所有表共同使用一个(或多 个,自行配置).ibdata 文件。
ibdata1 文件:系统表空间数据文件,存储表元数据、Undo日志等 。
ib_logfile0、ib_logfile1 文件:Redo log 日志文件。
查看数据文件路径 show variables like ‘%datadir%’;
undo_001,undo_002 undo日志文件

  • pid文件
    pid 文件是 mysqld 应用程序在 Unix/Linux 环境下的一个进程文件,和许多其他 Unix/Linux 服务 端程序一样,它存放着自己的进程 id。

  • socket文件
    socket 文件也是在 Unix/Linux 环境下才有的,用户在 Unix/Linux 环境下客户端连接可以不通过 TCP/IP 网络而直接使用 Unix Socket 来连接 MySQL。

3.2 Mysql运行机制

Mysql上笔记 - 图6

建立连接(Connectors&Connection Pool)

通过客户端/服务器通信协议与Mysql建立连接。Mysql客户端与服务端的通信方式是“半双工”。对每一个Mysql的连接,时刻都有一个线程状态来标识这个连接在做什么。

通讯机制:

  • 全双工:能同时发送和接收数据。比如打电话。
  • 半双工:指的某一时刻,要么发送数据,要么接收数据,不能同时发送和接收数据。
  • 单工:只能发送数据和接收数据。比如单行道。

线程状态:

show processlist; //查看用户正在运行的线程信息,root用户能查看所有线程,其他用户只能看自己的。

查看完整的info show full processlist;

Mysql上笔记 - 图7

id:线程ID,可以使用kill xx;

user:启动这个线程的用户

Host:发送请求的客户端的IP和端口号

db:当前命令在哪个库执行

Command:该线程正在执行的操作命令

  • Create DB:正在创建库操作
  • Drop DB:正在删除库操作
  • Execute: 正在执行一个PreparedStatement
  • Close Stmt:正在关闭一个PreparedStatement
  • Query:正在执行一个语句
  • Sleep:正在等待客户端发送语句
  • Quit:正在退出
  • shutdown:正在关闭服务器

Time:表示该线程处于当前状态的时间 单位秒

State:线程状态

  • Updating:正在搜索匹配记录,进行修改
  • Sleeping:正在等待客户端发送新请求
  • Starting:正在执行请求处理
  • Checking table:正在检查数据表
  • Closing table : 正在将表中数据刷新到磁盘中
  • Locked:被其他查询锁住了记录
  • Sending Data:正在处理Select查询,同时将结果发送给客户端

info:一般记录线程执行的语句。

查询缓存(Cache&Buffer)

这是MySQL的一个可优化查询的地方,如果开启了查询缓存且在 查询缓存过程中查询到完全相同的SQL语句,则将查询结果直接返回给客户端;如果没有开启查询 缓存或者没有查询到完全相同的 SQL 语句则会由解析器进行语法语义解析,并生成“解析树”。

  1. 缓存Select查询的结果和SQL语句
  2. 执行Select查询时,先查询缓存,判断是否存在可用的记录集,要求是否完全相同(包括参 数值),这样才会 匹配缓存数据命中。
  3. **以下情况。即使开启查询缓存,以下SQL也不能缓存**
  4. 查询语句使用SQL_NO_CACHE
  5. 查询的结果大于query_cache_limit设置 query_cache_limit 指得是空间 大小 字节 104857 Byte=1MB
  6. 查询中有一些不确定的参数,比如now()
  7. show variables like '%query_cache%'; //查看查询缓存是否启用,空间大小,限制等
  8. query_cache_type=OFF说明是关闭的。0关闭,1开启,2查询语句需要加 SQL_CACHE

Mysql上笔记 - 图8

  1. show status like 'Qcache%'; //查看更详细的缓存参数,可用缓存空间,缓存块,缓存多少等

Mysql上笔记 - 图9

解析器(Parser)

将客户端发送的SQL进行语法解析,生成”解析树”。预处理器根据一些MySQL 规则进一步检查“解析树”是否合法,例如这里将检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义,最后生成新的“解析树”。

查询优化器(Optimizer)

根据“解析树”生成最优的执行计划。MySQL使用很多优化策略生成最 优的执行计划,可以分为两类:静态优化(编译时优化)、动态优化(运行时优化)。

  • 等价变换策略
    5=5 and a>5 改成 a > 5
    a 5 and a=5
    基于联合索引,调整条件位置

  • 优化count,min,max等函数
    InnoDB引擎min函数只需要找索引最左边
    InnoDB引擎max函数只需要找索引最右边
    MyISAM 引擎count(*),不需要计算,直接返回,在没有查询条件的时候。

  • 提前终止查询
    使用limit查询,获取limit所需数据,就不在继续遍历后面数据

  • in的优化
    Mysql对in查询,会先进行排序,再采用二分法查找数据。比如where id in(2,1,3)变成in(1,2,3)

查询执行引擎

查询执行引擎负责执行 SQL 语句,此时查询执行引擎会根据 SQL 语句中表的存储引擎类型,以及对应的API接口与底层存储引擎缓存或者物理文件的交互,得到查询结果并返回给客户端。若开 启用查询缓存,这时会将SQL 语句和结果完整地保存到查询缓存(Cache&Buffer)中,以后若有 相同的 SQL 语句执行则直接返回结果。

  1. 如果开启了查询缓存,先将查询结果做缓存操作
  2. 返回结果过多,采用增量模式返回

3.3 Mysql存储引擎层

存储引擎负责MySQL中数据的存储与提取,与底层系统文件进行交互。MySQL存储引擎是插件式的, 服务器中的查询执行引擎通过接口与存储引擎进行通信,接口屏蔽了不同存储引擎之间的差异 。现在有 很多种存储引擎,各有各的特点,最常见的是MyISAM和InnoDB。

show engines; 查询支持的存储引擎。

5.5版本之前默认采用MyISAM存储引擎,从5.5开始采用InnoDB存储引擎。

Mysql上笔记 - 图10

各种引擎介绍

InnoDB:支持事务,具有提交,回滚和崩溃恢复能力,事务安全。

MyISAM:不支持事务和外键,访问速度比较快。

Memory:利用内存创建表,访问的速度非常快,默认使用Hash索引,一旦关闭,数据就会丢失。

Archive:归档类型引擎,仅支持insert和select语句。

Csv:csv文件格式进行数据存储,由于文件限制,所有的列必须强制指定not null。另外csv引擎不支持索引和分区,适合做数据交换的中间表。

BlackHole:黑洞,只进不出。进来就消失,所有插入的数都不会保存,但是一些操作是会记录在binlog中的。可以用来做为主从复制的中介,节省空间。

Federated:可以访问远端Mysql数据库中的表。一个本地表,不保存数据,访问远程表内容。

MRG_MyISAM:一组MyISAM表的组合,这些MyISAM表必须结构相同,Merge本身没有数据,对Merge操作可以对一组MyISAM表进行操作。

InnoDB和MyISAM

事务和外键

InnoDB支持事务和外键,强调数据安全和完整性,适合大量insert或update操作

MyISAM不支持事务和外键,它提供高速存储和检索,适合大量的select查询操作

锁机制

InnoDB支持行级锁,锁定指定记录。基于索引来加锁实现。

MyISAM支持表级锁,锁定整张表。影响到并发能力。

索引结构

InnoDB使用聚簇索引,索引和记录在一起存储,及缓存索引,也缓存记录。

MyISAM使用非聚簇索引,索引和记录分开。(主键索引也是和数据分开的)

Mysql上笔记 - 图11

并发处理能力

MyISAM 使用表级锁,会导致写操作并发率低,读之间并不会进行阻塞,读写阻塞。

InnoDB读写阻塞与隔离级别有关,可以采用多版本并发控制(MVCC)来支持高并发。

存储文件

InnoDB对应两个文件,一个.frm表结构文件(8版本没了),一个.ibd数据文件。一个表最大支持64TB

MyISAM表对应三个文件,一个.frm表结构文件(8版本 .sdi文件),一个MYD表数据文件,一个.MYI索引文件。

一个表最大支持256TB。

Mysql上笔记 - 图12

适用场景:

MyISAM:不需要支持事务、并发相对较低(锁机制,表级锁)、数据修改相对较少、以读为主、数据一致性要求不高。

InnoDB:需要事务支持、行级锁对高并发有很好的适应能力、数据更新较为频繁的场景、数据一致性要求高,硬件设备内存较大,可以利用InnoDB较好的缓存能力来提高内存利用率,减少磁盘IO

如何选择存储引擎?

  • 是否需要事务?InnoDB
  • 是否存在并发修改?InnoDB
  • 是否追求快速查询,且数据修改少?MyISAM
  • 在绝大多数情况下,推荐适用InnoDB

3.4 InnoDB存储结构

主要分为内存结构和磁盘结构两部分。

Mysql上笔记 - 图13

内存结构

内存结构主要包括括Buffer Pool、Change Buffer、Adaptive Hash Index和Log Buffer四大组件。

BufferPool

BufferPool主要由一个个page页组成。

缓冲池,简称BP。BP以Page页为单位,默认大小16K,BP的底层采用链表数 据结构管理Page。在InnoDB访问表记录和索引时会在Page页中缓存,以后使用可以减少磁 盘IO操作,提升效率。

show variables like ‘%innodb_page_size%’;

Page管理机制

  1. Page根据状态可以分为三种类型:
  2. free page 空闲page,未被使用
  3. clean page:被使用page,数据没有被修改过
  4. dirty page:脏页,被使用page,数据被修改过,页中数据和磁盘的数据产生了不 一致
  5. 针对上述三种page类型,InnoDB通过三种链表结构来维护和管理
  6. free list :表示空闲缓冲区,管理free page
  7. flush list:表示需要刷新到磁盘的缓冲区,管理dirty page,内部page按修改时间 排序。脏页即存在于flush 链表,也在LRU链表中,但是两种互不影响,LRU链表负 责管理page的可用性和释放,而flush链表负责管理 脏页的刷盘操作。lru某些page淘汰也会触发刷盘操作。
  8. lru list:表示正在使用的缓冲区,管理clean pagedirty page,缓冲区以 midpoint为基点,前面链表称为 new列表区,存放经常访问的数据,占63%;后面的链表称为old列表区,存放使用较少数据,占37%。
  9. 查看 innodb状态信息:show engine innodb status \G;
  10. ![](assets/image-20200824133007184.png#alt=image-20200824133007184)
  11. 有时dtabase pages+free buffers有时小于buffer pool size
  12. 因为 buffer pool里面还包含了自适应hash索引,以及 changed buffer pool

而且有几个#buffer_pool实例个数
innodb_buffer_pool_instances=4就有几个buffer_pool

  1. BUFFER POOL AND MEMORY
  2. ----------------------
  3. Total large memory allocated 1098907648
  4. Dictionary memory allocated 822720
  5. Buffer pool size 65536 #这个为个数 65536*16kB=1048576KB=1G
  6. Free buffers 60815
  7. Database pages 4700
  8. Old database pages 1810
  9. Modified db pages 0
  10. Pending reads 0
  11. Pending writes: LRU 0, flush list 0, single page 0
  12. Pages made young 1, not young 0
  13. 0.00 youngs/s, 0.00 non-youngs/s
  14. Pages read 4525, created 175, written 6278
  15. 0.00 reads/s, 0.00 creates/s, 0.00 writes/s
  16. No buffer pool page gets since the last printout
  17. Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
  18. LRU len: 4700, unzip_LRU len: 0
  19. I/O sum[0]:cur[0], unzip sum[0]:cur[0]
  20. ----------------------
  21. INDIVIDUAL BUFFER POOL INFO
  22. ----------------------
  23. ---BUFFER POOL 0
  24. Buffer pool size 16384
  25. Free buffers 15168
  26. Database pages 1210
  27. Old database pages 466
  28. Modified db pages 0
  29. Pending reads 0
  30. Pending writes: LRU 0, flush list 0, single page 0
  31. Pages made young 1, not young 0
  32. 0.00 youngs/s, 0.00 non-youngs/s
  33. Pages read 1196, created 14, written 5355
  34. 0.00 reads/s, 0.00 creates/s, 0.00 writes/s
  35. No buffer pool page gets since the last printout
  36. Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
  37. LRU len: 1210, unzip_LRU len: 0
  38. I/O sum[0]:cur[0], unzip sum[0]:cur[0]
  39. ---BUFFER POOL 1
  40. Buffer pool size 16384
  41. Free buffers 15206
  42. Database pages 1178
  43. Old database pages 453
  44. Modified db pages 0
  45. Pending reads 0
  46. Pending writes: LRU 0, flush list 0, single page 0
  47. Pages made young 0, not young 0
  48. 0.00 youngs/s, 0.00 non-youngs/s
  49. Pages read 1105, created 73, written 179
  50. 0.00 reads/s, 0.00 creates/s, 0.00 writes/s
  51. No buffer pool page gets since the last printout
  52. Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
  53. LRU len: 1178, unzip_LRU len: 0
  54. I/O sum[0]:cur[0], unzip sum[0]:cur[0]
  55. ---BUFFER POOL 2
  56. Buffer pool size 16384
  57. Free buffers 15200
  58. Database pages 1176
  59. Old database pages 453
  60. Modified db pages 0
  61. Pending reads 0
  62. Pending writes: LRU 0, flush list 0, single page 0
  63. Pages made young 0, not young 0
  64. 0.00 youngs/s, 0.00 non-youngs/s
  65. Pages read 1102, created 74, written 423
  66. 0.00 reads/s, 0.00 creates/s, 0.00 writes/s
  67. No buffer pool page gets since the last printout
  68. Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
  69. LRU len: 1176, unzip_LRU len: 0
  70. I/O sum[0]:cur[0], unzip sum[0]:cur[0]
  71. ---BUFFER POOL 3
  72. Buffer pool size 16384
  73. Free buffers 15241
  74. Database pages 1136
  75. Old database pages 438
  76. Modified db pages 0
  77. Pending reads 0
  78. Pending writes: LRU 0, flush list 0, single page 0
  79. Pages made young 0, not young 0
  80. 0.00 youngs/s, 0.00 non-youngs/s
  81. Pages read 1122, created 14, written 321
  82. 0.00 reads/s, 0.00 creates/s, 0.00 writes/s
  83. No buffer pool page gets since the last printout
  84. Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
  85. LRU len: 1136, unzip_LRU len: 0

改进型LRU算法维护

  1. 普通LRU:末尾淘汰法,新数据从链表头部加入,释放空间时从末尾淘汰
  2. 改进型LRU:链表分为newold两个部分,加入元素时并不是从表头插入,而是从中间 midpoint位置插入, 如果数据很快被访问,那么page就会向new列表头部移动,如果 数据没有被访问,会逐步向old尾部移动, 等待淘汰。 每当有新的page数据读取到buffer pool时,InnoDb引擎会判断是否有空闲页,是否足 够,如果 有就将free pagefree list列表删除,放入到LRU列表中。没有空闲页,就会 根据LRU算法淘汰LRU链表默认 的页,将内存空间释放分配给新的页。

Buffer Pool配置参数

  1. show variables like '%innodb_page_size%'; //查看page页大小 select @@innodb_pagesize; 除以1024就是多 少KByte。默认每个page页16k。

Mysql上笔记 - 图14

  1. show variables like '%innodb_old%'; //查看lrulist中old列表参数

Mysql上笔记 - 图15

  1. show variables like '%innodb_buffer%'; //查看buffer pool参数
  2. ![](assets/image-20200824134532156.png#alt=image-20200824134532156)
  3. mysql 8.0 innodb_buffer_pool_size默认为(134217728字节)128M 8192page。和5相比加了16倍。
  4. mysql 5 默认为 8M 512page

建议:将innodb_buffer_pool_size设置为总内存(计算机总内存)大小的60%-80%, innodb_buffer_pool_instances可以设置为多个,这样可以避免缓存争夺。

蓝色为整个bufferpool

白色为bufferpool分出的多个instance

绿色为instance分成多个块chunk

红色为chunk包含多个page页

我们可以对buffer pool分配大内存然后按照这种方式进行逐层配置拆分。

Mysql上笔记 - 图16

Change Buffer

可以参考:https://www.jianshu.com/p/92a674683f28

写缓冲区,简称CB。在进行DML操作时,如果BP没有其相应的Page数据, 并不会立刻将磁盘页加载到缓冲池,而是在CB记录缓冲变更,等未来数据被读取时,再将数 据合并恢复到BP中。

ChangeBuffer占用BufferPool空间,默认占25%,最大允许占50%,可以根据读写业务量来 进行调整。参数innodb_change_buffer_max_size;

show variables like ‘%innodb_change_buffer_max_size%’;

set global innodb_change_buffer_max_size=20;

当更新一条记录时,该记录在BufferPool存在,直接在BufferPool修改,一次内存操作。如果该记录在BufferPool不存在(没有命中),会直接在ChangeBuffer进行一次内存操作,不用再去磁盘查询数据,避免一次磁盘IO。当下次查询记录时,会先进行磁盘读取,然后再从 ChangeBuffer中读取信息合并,最终载入BufferPool中。

写缓冲区,仅适用于非唯一普通索引页,为什么?

如果在索引设置唯一性,在进行修改时,InnoDB必须要做唯一性校验,因此必须查询磁盘, 做一次IO操作。会直接将记录查询到BufferPool中,然后在缓冲池修改,不会在 ChangeBuffer操作。

Log Buffer

Mysql上笔记 - 图17

日志缓冲区,用来保存要写入磁盘上log文件(Redo/Undo)的数据,日志缓冲 区的内容定期刷新到磁盘log文件中。日志缓冲区满时会自动将其刷新到磁盘,当遇到BLOB 或多行更新的大事务操作时,增加日志缓冲区可以节省磁盘I/O。 LogBuffer主要是用于记录InnoDB引擎日志,在DML操作时会产生Redo和Undo日志。 LogBuffer空间满了,会自动写入磁盘。可以通过将innodb_log_buffer_size参数调大,减少 磁盘IO频率 。

innodb_flush_log_at_trx_commit参数控制日志刷新行为,默认为1,建议设为2

show variables like “%innodb_flush%”;

Mysql上笔记 - 图18

Adaptive Hash Index :自适应哈希索引,用于优化对BP数据的查询。InnoDB存储引擎会监 控对表索引的查找,如果观察到建立哈希索引可以带来速度的提升,则建立哈希索引,所以 称之为自适应。InnoDB存储引擎会自动根据访问的频率和模式来为某些页建立哈希索引。

  1. mysql> show variables like '%hash%';
  2. +----------------------------------+-------+
  3. | Variable_name | Value |
  4. +----------------------------------+-------+
  5. | innodb_adaptive_hash_index | ON |
  6. | innodb_adaptive_hash_index_parts | 8 |
  7. +----------------------------------+-------+

skip-innodb_adaptive_hash_index 可以关闭

innodb_adaptive_hash_index_parts HashTable分区,减小锁粒度,提高并发量。每个分区各自一把锁,管自己的。最大512

  1. hash自适应索引会占用innodb buffer pool
  2. 自适应hash索引只适合搜索等值的查询或者in,而对于其他查找类型,如范围查找,是不能使用的;
  3. 如: where index_col=‘xxx’;
  4. 无法进行排序;
  5. MySQL自动管理,人为无法干预;

磁盘结构

InnoDB磁盘主要包含Tablespaces,InnoDB Data Dictionary,Doublewrite Buffer、Redo Log 和Undo Logs。

系统表空间信息(System Tablespace)(多个表共享)文件名为ibdata1:

包含InnoDB数据字典,Doublewrite Buffer,Change Buffer,Undo Logs的存储区 域。系统表空间也默认包含任何用户在系统表空间创建的表数据和索引数据。系统表空 间是一个共享的表空间因为它是被多个表共享的。该空间的数据文件通过参数 innodb_data_file_path控制。

show variables like ‘%innodb_data_file_path%’;

autoextent:自动扩展。

Mysql上笔记 - 图19

  • 数据字典(InnoDB Data Dictionary)
    InnoDB数据字典由内部系统表组成,这些表包含用于查找表、索引和表字段等对象的元数 据。元数据物理上位于InnoDB系统表空间中。由于历史原因,数据字典元数据在一定程度上 与InnoDB表元数据文件(.frm文件)中存储的信息重叠。

  • 双写缓冲区(Doublewrite Buffer)
    位于系统表空间,是一个存储区域。在BufferPage的dirty page页刷新到磁盘真正的位置之前,会先将数据存在Doublewrite 缓冲区。如果在page页写入过程中出现操作系统、存储子系统或 mysqld进程崩溃,InnoDB可以在崩溃恢复期间从Doublewrite 缓冲区中找到页面的一个备份。在大多数情况下,默认情况下启用双写缓冲区,要禁用Doublewrite 缓冲区,可以将 innodb_doublewrite设置为0。使用Doublewrite 缓冲区时建议将innodb_flush_method设 置为O_DIRECT。

    1. MySQLinnodb_flush_method这个参数控制着innodb数据文件及redo log的打开、
    2. 刷写模式。有三个值:fdatasync(默认),O_DSYNCO_DIRECT。设置O_DIRECT表示
    3. 数据文件写入操作会通知操作系统不要缓存数据,也不要用预读,直接从Innodb
    4. Buffer写到磁盘文件。
    5. 默认的fdatasync意思是先写入操作系统缓存,然后再调用fsync()函数去异步刷数据文
    6. 件与redo log的缓存信息。


Mysql上笔记 - 图20

独立表空间(File-Per-Table):

每创建一个表就会生成一个独立的.ibd文件。

默认开启,独立表空间是一个单表表空间,该表创建于自己的数据文件中,而非创建于 系统表空间中。当innodb_file_per_table选项开启时,表将被创建于表空间中。否则, innodb将被创建于系统表空间中。每个表文件表空间由一个.ibd数据文件代表,该文件 默认被创建于数据库目录中。表空间的表文件支持动态(dynamic)和压缩 (commpressed)行格式。

Mysql上笔记 - 图21

show variables like ‘%innodb_file_per_table%’;

5.7以后默认是开启的。

Mysql上笔记 - 图22

通用表空间(General Tablespace):

通用表空间为通过create tablespace语法创建的共享表空间。通用表空间可以创建于 mysql数据目录外的其他表空间,其可以容纳多张表,且其支持所有的行格式。

用法:

create tablespace ts1 add datafile ‘ts1.ibd’ engine=innodb; //在data目录下直接创建

use dbname;

create table t1(id int primary key) tablespace ts1;

撤销表空间(undo tablespaces):

撤销表空间由一个或多个包含Undo日志文件组成。在MySQL 5.7版本之前Undo占用的 是System Tablespace共享区,从5.7开始将Undo从System Tablespace分离了出来。 InnoDB使用的undo表空间由innodb_undo_tablespaces配置选项控制,默认为0。参 数值为0表示使用系统表空间ibdata1;大于0表示使用undo表空间undo_001、 undo_002等。设置为n就有那个undo_xxx

show variables like ‘%innodb_undo_tablespaces%’;

Mysql上笔记 - 图23

撤销日志(Undo Logs) 撤消日志是在事务开始之前保存的被修改数据的备份,用于例外情况时回滚事务。撤消日志 属于逻辑日志,根据每行记录进行记录。撤消日志存在于系统表空间、撤消表空间和临时表空间中。

  1. mysql> show variables like '%undo%';
  2. +--------------------------+------------+
  3. | Variable_name | Value |
  4. +--------------------------+------------+
  5. | innodb_max_undo_log_size | 1073741824 | #最大1G
  6. | innodb_undo_directory | .\ |
  7. | innodb_undo_log_encrypt | OFF |
  8. | innodb_undo_log_truncate | ON |
  9. | innodb_undo_tablespaces | 2 |# 2
  10. +--------------------------+------------+

临时表空间(Temporary Tablespace):

分为session temporary tablespaces 和global temporary tablespace两种。session temporary tablespaces 存储的是用户创建的临时表和磁盘内部的临时表。global temporary tablespace储存用户临时表的回滚段(rollback segments )。mysql服务 器正常关闭或异常终止时,临时表空间将被移除,每次启动时会被重新创建。

重做日志(Redo Log) 重做日志是一种基于磁盘的数据结构,用于在崩溃恢复期间更正不完整事务写入的数据。 MySQL以循环方式写入重做日志文件,记录InnoDB中所有对Buffer Pool修改的日志。当出 现实例故障(像断电),导致数据未能更新到数据文件,则数据库重启时须redo,重新把数 据更新到数据文件。读写事务在执行的过程中,都会不断的产生redo log。默认情况下,重 做日志在磁盘上由两个名为ib_logfile0和ib_logfile1的文件物理表示。默认一个为48M,可以适当进行调整(256M,512M)。

Mysql上笔记 - 图24

新版本架构演变

Mysql上笔记 - 图25

MySQL 5.7 版本

将 Undo日志表空间从共享表空间 ibdata 文件中分离出来,可以在安装 MySQL 时由用 户自行指定文件大小和数量。

增加了 temporary 临时表空间,里面存储着临时表或临时查询结果集的数据。

Buffer Pool 大小可以动态修改,无需重启数据库实例。

MySQL 8.0 版本

将InnoDB表的数据字典和Undo都从共享表空间ibdata中彻底分离出来了,以前需要 ibdata中数据字典与独立表空间ibd文件中数据字典一致才行,8.0版本就不需要了。

temporary 临时表空间也可以配置多个物理文件,而且均为 InnoDB 存储引擎并能创建 索引,这样加快了处理的速度。

用户可以像 Oracle 数据库那样设置一些表空间,每个表空间对应多个物理文件,每个 表空间可以给多个表使用,但一个表只能存储在一个表空间中。

将Doublewrite Buffer从共享表空间ibdata中也分离出来了。

扩展 缓存Qcache与Buffer pool区别

参考博文【https://blog.51cto.com/13111614/1946212】

这两个不是一个东西:

1、Qcacche缓存的是SQL语句及对应的结果集,缓存在内存,最简单的情况是SQL一直不重复,那Qcache的命令率肯定是0;

2、buffer pool中缓存的是整张表中的数据,缓存在内存,SQL再变只要数据都在内存,那么命中率就是100%。

3,Qcache是服务层的缓存。buffer pool是innodb存储引擎带的一个缓存池,查询数据的时候,它首先会从内存中查询,如果内存中存在的话,直接返回,从而提高查询响应时间。Buffer pool是设置的越大越好,一般设置为服务器物理内存的70%。

3.5 InnoDB线程模型

Mysql上笔记 - 图26

IO Thread

在InnoDB中使用了大量的AIO(Async IO)来做读写处理,这样可以极大提高数据库的性能。在 InnoDB1.0版本之前共有4个IO Thread,分别是write,read,insert buffer和log thread,后来 版本将read thread和write thread分别增大到了4个,一共有10个了。 read thread : 负责读取操作,将数据从磁盘加载到缓存page页。4个

show engine innodb status \G;查看引擎状态信息Mysql上笔记 - 图27

read thread:负责读取操作,将数据从磁盘加载到缓存page页。4个

write thread:负责写操作,将缓存脏页刷新到磁盘。4个

log thread:将日志缓冲区内容刷新到磁盘,形成redo log。

insert buffer thread:负责将写缓冲内容刷新到磁盘。

Purge Thread

  1. 事务提交之后,其使用的undo日志将不再需要,因此需要Purge Thread回收已经分配的undo 页。
  2. show variables like '%innodb_purge_threads%';

Page Cleaner Thread

  1. 作用是将脏数据刷新到磁盘,脏数据刷盘后响应的redo log也就可以覆盖,即可以同步数据,又能达到redo log循环使用的目的。会调用write thread线程处理。
  2. show variables like '%innodb_page_cleaners%';

Master Thread

  1. Master Thread InnoDB的主线程,负责调度其他线程,优先级最高。作用是将缓冲池中的数据异步刷新到磁盘,保证数据的一致性。包含:脏页的刷新-page cleaner threadundo页回收-purge threadredo日志刷新-log thread,合并写缓冲等。内部有两个主处理,分别是每隔1s10s处理。
  2. 1s操作:
  3. 刷新日志缓冲区,刷到磁盘 log_buufer->redolog
  4. 合并写缓冲区数据,根据IO读写压力来决定是否操作
  5. 刷新脏页数据到磁盘,根据脏页比列达到75%才操作。8.090%。这个百分比是占change_buffer的大小。
  6. show variables like '%innodb_max_dirty_pages_pct%';
  7. show variables like '%innodb_io_capacity%'; innodb_io_capacity 每次刷新多少页,默认200,可以调整 最大到2000

Mysql上笔记 - 图28

  1. 10s操作:
  2. 脏页刷新到磁盘,无条件。
  3. 合并写缓冲区数据,无条件。
  4. 刷新日志缓冲区。log_bufer->redolog
  5. 删除无用的undo 页。show variables like '%innodb_purge_batch_size%'; 默认为300每次处理300页。

3.6 Innodb数据文件

Innodb文件存储结构

Mysql上笔记 - 图29

分为一个ibd数据文件—>Segment(段)—>Extent(区)—>Page(页)—>Row(行)

Tablespace:表空间,用于存储多个ibd数据文件,用来存储表的记录和索引。一个文件包含多个段。

Segment:段,用于管理多个Extent,分为数据段(Leaf node segment),索引段(Non-leaf node segment),回滚段(Rollback segment)。一个表至少有两个segment,一个管理数据,一个管理索引。每创建一个索引,会多两个segment。

Extent:区,一个区里面包含64个连续的页,大小为1M.当表空间不足时,需要分配新的页资源,不会一页一页分,直接分配一个区。16*64=1024

Page:页,用于存储多个行记录,大小为16k。包含很多种页类型,比如数据页,undo页,系统页,事务数据页,大的Blob对象页。

Row:行,包含了记录字段值,事务ID(trx id),滚动指针(Rollpointer),字段指针(Field pointers)等信息。

Page是文件最基本的单位,无论何种类型的page,都是由page header,page trailer和page body组成。

Mysql上笔记 - 图30

数据文件存储格式

查看表信息。

use dbName;

show tables;

show table status;

Mysql上笔记 - 图31

一般情况下,如果row_format为REDUNDANT、COMPACT,文件格式为Antelope;如果 row_format为DYNAMIC和COMPRESSED,文件格式为Barracuda。

通过 information_schema 查看指定表的文件格式

5.7

  1. select * from information_schema.innodb_sys_tables \G;
  2. select * from information_schema.innodb_sys_tables where name = 'lagou/r_resume' \G;

8

  1. select * from information_schema.innodb_tables \G;
  2. select * from information_schema.innodb_tables where name = 'la_gou/r_resume' \G;

文件格式 (File Format)

在早期的InnoDB版本中,文件格式只有一种,随着InnoDB引擎的发展,出现了新文件格式,用于 支持新的功能。

目前InnoDB只支持两种文件格式:Antelope 和 Barracuda。

Antelope: 先前未命名的,最原始的InnoDB文件格式,它支持两种行格式:COMPACT和 REDUNDANT,MySQL 5.6及其以前版本默认格式为Antelope。

Barracuda: 新的文件格式。它支持InnoDB的所有行格式,包括新的行格式:COMPRESSED 和 DYNAMIC。

通过innodb_file_format 配置参数可以设置InnoDB文件格式,之前默认值为Antelope,5.7版本 开始改为Barracuda。

行格式(Row Format)

表的行格式决定了它的行是如何物理存储的,这反过来又会影响查询和DML操作的性能。如果在单个page页中容纳更多行,查询和索引查找可以更快地工作,缓冲池中所需的内存更少,写入更新时所需的I/O更少。

InnoDB存储引擎支持四种行格式:REDUNDANT、COMPACT、DYNAMIC和COMPRESSED。

Mysql上笔记 - 图32

DYNAMIC和COMPRESSED新格式引入的功能有:数据压缩、增强型长列数据的页外存储和大索引 前缀。

每个表的数据分成若干页来存储,每个页中采用B树结构存储;

如果某些字段信息过长,无法存储在B树节点中,这时候会被单独分配空间,此时被称为溢出页, 该字段被称为页外列。

  • REDUNDANT 行格式
    使用REDUNDANT行格式,表会将变长列值的前768字节存储在B树节点的索引记录中,其余 的存储在溢出页上。对于大于等于786字节的固定长度字段InnoDB会转换为变长字段,以便 能够在页外存储。

  • COMPACT 行格式
    与REDUNDANT行格式相比,COMPACT行格式减少了约20%的行存储空间,但代价是增加了 某些操作的CPU使用量。如果系统负载是受缓存命中率和磁盘速度限制,那么COMPACT格式 可能更快。如果系统负载受到CPU速度的限制,那么COMPACT格式可能会慢一些。

  • DYNAMIC 行格式
    使用DYNAMIC行格式,InnoDB会将表中长可变长度的列值完全存储在页外,而索引记录只 包含指向溢出页的20字节指针。大于或等于768字节的固定长度字段编码为可变长度字段。 DYNAMIC行格式支持大索引前缀,最多可以为3072字节,可通过innodb_large_prefix参数 控制。

  • COMPRESSED 行格式
    COMPRESSED行格式提供与DYNAMIC行格式相同的存储特性和功能,但增加了对表和索引 数据压缩的支持。

相关博客【https://blog.csdn.net/peng_zhanxuan/article/details/106506022】

在创建表和索引时,文件格式都被用于每个InnoDB表数据文件(其名称与*.ibd匹配)。修改文件 格式的方法是重新创建表及其索引,最简单方法是对要修改的每个表使用以下命令:

  1. ALTER TABLE 表名 ROW_FORMAT=格式类型;

3.7 Undo Log

  1. Undo:意为撤销或取消,以撤销操作为目的,返回指定某个状态的操作。
  2. Undo Log:将要修改的记录存放到 Undo 日志里,当事务回滚时或者数据库崩溃时, 可以利用 Undo 日志,撤销未提交事务对数据库产生的影响。
  3. Undo Log产生和销毁:Undo Log在事务开始前产生;事务在提交时,并不会立刻删除undo loginnodb会将 该事务对应的undo log放入到删除列表中,后面会通过后台线程purge thread 行回收处理。Undo Log属于逻 辑日志,记录一个变化过程。例如执行一个deleteundolog会记 录一个insert;执行一个updateundolog 记录一个相反的update
  4. Undo Log存储:undo log采用段的方式管理和记录。**在innodb数据文件中包含一种rollback segment回滚段**,**内部包含1024undo log segment**。可以通过下面一组参数来控制Undo log存储。

show variables like ‘%innodb_rollback_segments%’; //查看有多少个segment 默认为128每个回滚段包含1024个undo log segment

  1. show variables like '%undo%';

Mysql上笔记 - 图33

  1. innodb_undo_tablespaces=2说明undo日志有两个undo日志文件。可以设定个数。

Mysql上笔记 - 图34

  1. undo_logrollback segment有什么关系?
  2. 作用:
  3. 实现事务的原子性:Undo Log 是为了实现事务的原子性而出现的产物。事务处理过程中,如果出现了错误或 者用户执 行了 ROLLBACK 语句,MySQL 可以利用 Undo Log 中的备份将数据恢复到事务开始之前的状态。
  4. 实现多版本并发控制(MVCC):Undo Log MySQL InnoDB 存储引擎中用来实现多版本并发控制。事务未 提交之前,Undo Log 保存了未提交之前的版本数据,**Undo Log 中的数据可作为数据旧版本快照供其他并发 事务进行快照读。**
  5. ![](assets/image-20200825120119705.png#alt=image-20200825120119705)
  6. 事务A手动开启事务,执行更新操作,首先会把更新命中的数据备份到 Undo Buffer 中。
  7. 事务B手动开启事务, 执行查询操作,会读取 Undo 日志数据返回,进行快照读

3.8 redo log 和 binlog

redo log

Redo:顾名思义就是重做。以恢复操作为目的,在数据库发生意外时重现操作。

Redo Log:指事务中修改的任何数据,将最新的数据备份存储的位置(Redo Log),被称为重做 日志。

Redo Log 的生成和释放:随着事务操作的执行,就会生成Redo Log,在事务提交时会将产生 Redo Log写入Log Buffer,并不是随着事务的提交就立刻写入磁盘文件。等事务操作的脏页写入到磁盘之后,Redo Log 的使命也就完成了,Redo Log占用的空间就可以重用(被覆盖写入)。

Redo Log工作原理 Redo Log 是为了实现事务的持久性而出现的产物。防止在发生故障的时间点,尚有脏页未写入表 的 IBD 文件中,在重启 MySQL 服务的时候,根据 Redo Log 进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。

Mysql上笔记 - 图35

其实redo buffer就是log buffer。数据库所有更新的操作都会=》log buffer=>redo log。redo buffer和change buffer是不一样的。redo 是保存一系列的操作记录,change buffer缓冲的是修改的数据,然后下次查询的时候先从磁盘查询出来和change buffer的数据合并返回。

可能有疑问,先写到内存的log buffer可以理解(不需要与磁盘有io操作,提高效率),为啥还要有写到redo log(这个也不是每次提交都都写入到redolog文件,没1s,10s或者log buffer满时提交)这种设计呢。redo log是一种顺序写入,而如果将更新操作持久化到数据文件先要查询到对应行数据然后进行更新就是一种磁盘随机操作,性能低于顺序写入。

Redo log写入机制:

Redo Log 文件内容是以顺序循环的方式写入文件,写满时则回溯到第一个文件,进行覆盖写。

Mysql上笔记 - 图36

write pos 是当前记录的位置,一边写一边后移,写到最后一个文件末尾后就回到 0 号文件开 头;

checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件;

write pos 和 checkpoint 之间还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示写满,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。

Redo Log相关配置参数:

每个InnoDB存储引擎至少有1个重做日志文件组(group),每个文件组至少有2个重做日志文 件,默认为ib_logfile0和ib_logfile1。可以通过下面一组参数控制Redo Log存储:

  1. show variables like '%innodb_log%';

Mysql上笔记 - 图37

可以看到innodb_log_files_in_group=2表示一个日志文件组有两个日志文件,我们可以进行调整。

Redo Buffer 持久化到 Redo Log 的策略,可通过 Innodb_flush_log_at_trx_commit 设置: 建议设为2

0:每秒提交 Redo buffer ->OS cache -> flush cache to disk,可能丢失一秒内的事务数 据。由后台Master线程每隔 1秒执行一次操作。

1(默认值):每次事务提交执行 Redo Buffer -> OS cache -> flush cache to disk,最安 全,性能最差的方式。

2:每次事务提交执行 Redo Buffer -> OS cache,然后由后台Master线程再每隔1秒执行OS cache -> flush cache to disk 的操作。

一般建议选择取值2,因为 MySQL 挂了数据没有损失,整个服务器挂了才会损失1秒的事务提交数据。

Mysql上笔记 - 图38

bin log日志

Binlog记录模式:

  1. Redo Log 是属于InnoDB引擎所特有的日志,而MySQL Server也有自己的日志,即 Binary log(二进制日 志),简称BinlogBinlog是记录所有数据库表结构变更以及表数据修改的二进制 日志,不会记录SELECT SHOW这类操作。**Binlog日志是以事件形式记录,还包含语句所执行的消耗时间**。开启Binlog日志有以下两个最重要的使用场景。
  • 主从复制:在主库中开启Binlog功能,这样主库就可以把Binlog传递给从库,从库拿到 Binlog后实现数据恢复达到主从数据一致性。
  • 数据恢复:通过mysqlbinlog工具来恢复数据。

Binlog文件名默认为“主机名_binlog-序列号”格式,例如oak_binlog-000001,也可以在配置文件中指定名称。文件记录模式STATEMENT、ROW和MIXED三种,具体含义如下。

  • ROW(row-based replication, RBR):日志中会记录每一行数据被修改的情况,然后在 slave端对相同的数据进行修改。
    优点:能清楚记录每一个行数据的修改细节,能完全实现主从数据同步和数据的恢复。 比如一个sql语句可以更新10条记录,那么会产生10条日志记录。
    缺点:批量操作,会产生大量的日志,尤其是alter table会让日志暴涨。

  • STATMENT(statement-based replication, SBR):每一条被修改数据的SQL都会记录到 master的Binlog中,slave在复制的时候SQL进程会解析成和原来master端执行过的相同的 SQL再次执行。简称SQL语句复制。
    优点:日志量小,减少磁盘IO,提升存储和恢复速度
    缺点:在某些情况下会导致主从数据不一致,比如last_insert_id()、now()等函数。

  • MIXED(mixed-based replication, MBR):以上两种模式的混合使用,一般会使用 STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存 binlog,MySQL会根据执行的SQL语句选择写入模式。
    binlog_format=MIXED

Binlog文件结构:

  1. MySQLbinlog文件中记录的是对数据库的各种修改操作,用来表示修改操作的数据结构是Log event。不同的 修改操作对应的不同的log event。比较常用的log event有:Query eventRow eventXid event等。binlog 文件的内容就是各种Log event的集合。
  2. log event结构:

Mysql上笔记 - 图39

bin log写入机制

  • 根据记录模式和操作触发event事件生成log event(事件触发执行机制)

  • 事务执行过程中产生log event写入缓冲区每个事务线程都有一个缓冲区
    log Event保存再一个binblog_cache_mngr数据结构中,在该结构中有两个缓冲区,一个是stmt_cache,用 于存放不支持事务的信息;另一个是trx_cache,用于存放支持事务的信息。

  • 事务在提交阶段会将产生的log event写入到外部binlog文件中
    不同事务以串行方式将log event写入binlog文件中,所以一个事务包含的log event信息在 binlog文件中是 连续的,中间不会插入其他事务的log event。

binlog文件操作

  1. mysql> show variables like '%log_bin%';
  2. +---------------------------------+--------------------------------------------------+
  3. | Variable_name | Value |
  4. +---------------------------------+--------------------------------------------------+
  5. | log_bin | ON #开启 |
  6. | log_bin_basename | D:\develop\mysql-8.0.19-winx64\data\binlog |
  7. | log_bin_index | D:\develop\mysql-8.0.19-winx64\data\binlog.index |
  8. | log_bin_trust_function_creators | OFF |
  9. | log_bin_use_v1_row_events | OFF |
  10. | sql_log_bin | ON #恢复的logbin日志是否记录,通常应改关闭 不然就冗余了 |
  11. +---------------------------------+--------------------------------------------------+
  12. 6 rows in set, 1 warning (0.01 sec)

如果是关闭的我们可以在my.cnf或者my.ini文件,[mysqld]下面增加

  1. log-bin=logbin #logbin日志前缀
  2. binlog-fomat=ROW #logbin记录模式 ROW STATMENT MIXED

查看binlog文件,内容

  1. #查看binlog有哪些日志文件
  2. show binary logs;
  3. #正在对那个binlog文件进行写入
  4. show master status;
  5. show binlog events;
  6. #查看binlog文件内容
  7. show binlog events in 'binlog.000016';

使用mysqlbinlog

  1. mysqlbinlog "文件名"
  2. mysqlbinlog "文件名" > "test.sql"

使用binlog恢复数据

  1. //按指定时间恢复
  2. mysqlbinlog --start-datetime="2020-04-25 18:00:00" --stopdatetime="2020-04-26 00:00:00" binlog.000002 | mysql -uroot -p1234
  3. //按事件位置号恢复
  4. mysqlbinlog --start-position=234 --stop-position=841 binlog.000001| mysql -uroot -proot
  1. mysql> show master status;
  2. +---------------+----------+--------------+------------------+-------------------+
  3. | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
  4. +---------------+----------+--------------+------------------+-------------------+
  5. | binlog.000001 | 762 | | | |
  6. +---------------+----------+--------------+------------------+-------------------+
  7. 1 row in set (0.00 sec)
  8. mysql> show binlog events in "binlog.000001";
  9. +---------------+------+----------------+-----------+-------------+--------------------------------------+
  10. | Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
  11. +---------------+------+----------------+-----------+-------------+--------------------------------------+
  12. | binlog.000001 | 4 | Format_desc | 1 | 124 | Server ver: 8.0.19, Binlog ver: 4 |
  13. | binlog.000001 | 124 | Previous_gtids | 1 | 155 | |
  14. | binlog.000001 | 155 | Anonymous_Gtid | 1 | 234 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
  15. | binlog.000001 | 234 | Query | 1 | 311 | BEGIN |
  16. | binlog.000001 | 311 | Table_map | 1 | 376 | table_id: 245 (la_gou.student) |
  17. | binlog.000001 | 376 | Write_rows | 1 | 430 | table_id: 245 flags: STMT_END_F |
  18. | binlog.000001 | 430 | Xid | 1 | 461 | COMMIT /* xid=2999 */ |
  19. | binlog.000001 | 461 | Anonymous_Gtid | 1 | 540 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
  20. | binlog.000001 | 540 | Query | 1 | 617 | BEGIN |
  21. | binlog.000001 | 617 | Table_map | 1 | 682 | table_id: 245 (la_gou.student) |
  22. | binlog.000001 | 682 | Write_rows | 1 | 731 | table_id: 245 flags: STMT_END_F |
  23. | binlog.000001 | 731 | Xid | 1 | 762 | COMMIT /* xid=3002 */ |
  24. | binlog.000001 | 762 | Anonymous_Gtid | 1 | 841 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
  25. | binlog.000001 | 841 | Query | 1 | 918 | BEGIN |
  26. | binlog.000001 | 918 | Table_map | 1 | 983 | table_id: 245 (la_gou.student) |
  27. | binlog.000001 | 983 | Delete_rows | 1 | 1037 | table_id: 245 flags: STMT_END_F |
  28. | binlog.000001 | 1037 | Xid | 1 | 1068 | COMMIT /* xid=3006 */ |
  29. | binlog.000001 | 1068 | Anonymous_Gtid | 1 | 1147 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
  30. | binlog.000001 | 1147 | Query | 1 | 1224 | BEGIN |
  31. | binlog.000001 | 1224 | Table_map | 1 | 1289 | table_id: 245 (la_gou.student) |
  32. | binlog.000001 | 1289 | Delete_rows | 1 | 1338 | table_id: 245 flags: STMT_END_F |
  33. | binlog.000001 | 1338 | Xid | 1 | 1369 | COMMIT /* xid=3007 */ |
  34. +---------------+------+----------------+-----------+-------------+--------------------------------------+
  35. 22 rows in set (0.01 sec)

恢复删除的数据:mysqlbinlog —start-position=234 —stop-position=841 binlog.000001| mysql -uroot -proot

mysqldump:定期全部备份数据库数。mysqlbin可以做增量备份和恢复操作。

删除binlog文件

  1. purge binary logs to 'mysqlbinlog.000001'; //删除指定文件
  2. purge binary logs before '2020-04-28 00:00:00'; //删除指定时间之前的文件
  3. reset master; //清除所有文件

设置expire_logs_days参数来启动自动清理。默认值为0表示未启用。

show variables like ‘%expire_logs_days%;

mysql 8关闭 log bin

[mysqld]配置下加 skip-log-bin

redo log和bin log的区别

redo log属于innodb引擎功能,binlog 属于mysq server自带功能。

redo log属于物理日志记录数据页更新状态内容,binlog是逻辑日志,记录更新过程。

redo log日志是循环写,日志空间大小固定,binlog是追加写入,写完一个写下一个,不会覆盖使用。

redo log作为服务器异常当即后事务数据自动恢复使用,binlog可以作为主从复制和数据恢复使用。binlog没有自动crash-safe的能力。

4 Mysql索引原理

索引会提升查询速度,会影响where查询,以及order by排序。索引类型如下:

从索引存储结构划分:BTree索引,Hash索引,FULLTEXT全文索引,RTree索引

从应用层次划分:普通索引,唯一索引,主键索引,复合索引

从索引键值类型划分:主键索引,辅助索引(二级索引)

从数据存储和索引键值逻辑关系划分:聚集索引(聚簇索引)—数据和索引放在一起,非聚集索引(非聚簇索引)—数据和索引不在一起。

4.1 索引类型

普通索引

这是最基本的索引类型,基于普通字段建立的索引,没有任何限制。

创建普通索引的方法如下:

  1. CREATE INDEX <索引的名字> ON tablename (字段名);
  2. ALTER TABLE tablename ADD INDEX [索引的名字] (字段名);
  3. CREATE TABLE tablename ( [...], INDEX [索引的名字] (字段名) );

查看索引:

  1. show index from tablename;

删除索引:

  1. drop index indexName on tablename;

Mysql上笔记 - 图40

(1)id为PK,聚集索引,叶子节点存储行记录;

(2)name为KEY,普通索引,叶子节点存储PK值,即id;

Mysql上笔记 - 图41

唯一索引

与”普通索引”类似,不同的就是:索引字段的值必须唯一,但允许有空值 。在创建或修改表时追加唯一 约束,就会自动创建对应的唯一索引。

创建方法:

  1. CREATE UNIQUE INDEX <索引的名字> ON tablename (字段名);
  2. ALTER TABLE tablename ADD UNIQUE INDEX [索引的名字] (字段名);
  3. CREATE TABLE tablename ( [...], UNIQUE [索引的名字] (字段名) ;

主键索引

它是一种特殊的唯一索引,不允许有空值。在创建或修改表时追加主键约束即可,每个表只能有一个主 键。

  1. CREATE TABLE tablename ( [...], PRIMARY KEY (字段名) );
  2. ALTER TABLE tablename ADD PRIMARY KEY (字段名);

复合索引

单一索引是指索引列为一列的情况,即新建索引的语句只实施在一列上;用户可以在多个列上建立索 引,这种索引叫做组复合索引(组合索引)。复合索引可以代替多个单一索引,相比多个单一索引复合 索引所需的开销更小。

索引同时有两个概念叫做窄索引和宽索引,窄索引是指索引列为1-2列的索引宽索引也就是索引列超 过2列的索引设计索引的一个重要原则就是能用窄索引不用宽索引,因为窄索引往往比组合索引更有 效。但是如果一个表建了很多索引,就考虑使用宽索引了。

  1. CREATE INDEX <索引的名字> ON tablename (字段名1,字段名2...);
  2. ALTER TABLE tablename ADD INDEX [索引的名字] (字段名1,字段名2...);
  3. CREATE TABLE tablename ( [...], INDEX [索引的名字] (字段名1,字段名2...) );

注意:

何时使用复合索引,要根据where条件建索引,注意不要过多使用索引,过多使用会对更新操作效 率有很大影响。

如果表已经建立了(col1,col2),就没有必要再单独建立(col1);如果现在有(col1)索引,如果查询需要col1和col2条件,可以建立(col1,col2)复合索引,对于查询有一定提高。

全文索引

查询操作在数据量比较少时,可以使用like模糊查询,但是对于大量的文本数据检索,效率很低。如果 使用全文索引,查询速度会比like快很多倍。在MySQL 5.6 以前的版本,只有MyISAM存储引擎支持全 文索引,从MySQL 5.6开始MyISAM和InnoDB存储引擎均支持。

  1. CREATE FULLTEXT INDEX <索引的名字> ON tablename (字段名);
  2. ALTER TABLE tablename ADD FULLTEXT [索引的名字] (字段名);
  3. CREATE TABLE tablename ( [...], FULLTEXT KEY [索引的名字] (字段名) ;

和常用的like模糊查询不同,全文索引有自己的语法格式,使用 match 和 against 关键字,比如:

match里面可以支持多个字段。

  1. select * from user
  2. where match(name) against('aaa');

show variables like ‘%ft%’;

Mysql上笔记 - 图42

ft_min_word_len:myisam最小单词数,

ft_max_word_len:myisam最大单词数 满足才会建立全文索引。

innodb_ft_min_token_size: 3

innodb_ft_max_token_size: 84 3~84个字符的时候才会创建全文索引。可以在配置文件进行修改重新启动。这里是token_size而不是整个word长度,通过切词后的长度也必须大于3才会建立全文索引。

  1. ft_boolean_syntax:+ -><()~*:""&|
  2. 遇到+ -><()~*:""&|等字符才会进行切词

注意事项(了解就可以,限制太多):

  • 全文索引必须在字符串、文本字段上建立。

  • 全文索引字段值必须在最小字符和最大字符之间的才会有效。(innodb:3-84;myisam:4- 84)

  • 全文索引字段值要进行切词处理,按syntax字符进行切割,例如b+aaa,切分成b和aaa

  • 全文索引匹配查询,默认使用的是等值匹配,例如a匹配a,不会匹配ab,ac。如果想匹配可以在布 尔模式下搜索a*。

    1. select * from user
    2. where match(name) against('aaa' in boolean mode);


不过可以自己定义innodb_ft_server_stopword_table,停用词表

4.2 索引原理

数据库索引是一种数据结构,它以额外的写入和存储空间为代价来提高数据库表上数据检索操作的 速度。通俗来说,索引类似于书的目录,根据其中记录的页码可以快速找到所需的内容。

MySQL 官方对索引(Index)的定义是存储引擎用于快速查找记录的一种数据结构。

索引是物理数据页,数据库页大小(Page Size 16K 16384个字节)决定了一个页可以存储多少个索引行,以及 需要多少页来存储指定大小的索引。

索引可以加快检索速度,但同时也降低索引列插入、删除、更新的速度,索引维护需要代价。

索引涉及的理论知识有二分查找法、哈希表及 B+Tree

二分查找

针对有序的数据。

优点:等值查询,范围查询性能优秀

缺点:增删改数据维护成本高。需要对原来的有序数据进行处理。

Hash 结构

Hash底层实现是由Hash表来实现的,是根据键值 存储数据的结构。非常适合根据key查找 value值,也就是单个key查询,或者说等值查询。其结构如下所示:

Mysql上笔记 - 图43

从Hash索引的结构可知非常适用于等值查询,范围查询就需要全表扫描了。

主要应用于Memory引擎原生的Hash索引,以及innodb的自适应hash索引。

InnoDB自适应哈希索引是为了提升查询效率,InnoDB存储引擎会监控表上各个索引页的查询,当 InnoDB注意到某些索引值访问非常频繁时,会在内存中基于B+Tree索引再创建一个哈希索引,使得内 存中的 B+Tree 索引具备哈希索引的功能,即能够快速定值访问频繁访问的索引页。

InnoDB自适应哈希索引:在使用Hash索引访问时,一次性查找就能定位数据(如果存在Hash碰撞可能就需要多次查询了,mysql用的是拉链法来处理hash碰撞,我们可以优先选择避免Hash冲突算法),等值查询效率要优于 B+Tree。

hash算法:FNV64 > CRC32 (大数据量下 Hash 冲突概率较大)> MD5 > SHA1

自适应哈希索引的建立使得InnoDB存储引擎能自动根据索引页访问的频率和模式自动地为某些热点页 建立哈希索引来加速访问。

另外InnoDB自适应哈希索引的功能,用户只能选择开启或关闭功能,无法进行人工干涉。

  1. show engine innodb status \G;
  2. show variables like '%innodb_adaptive%';
  3. #关闭自适应hash索引
  4. set global innodb_adaptive_hash_index=0;
  5. #开启自适应hash索引
  6. set global innodb_adaptive_hash_index=1;

Mysql上笔记 - 图44

我们可以看到 使用AHI和不适用AHI查询速率,如果使用AHI查询速率反而低于不使用AHI查询速率,可以考虑关闭了。

  1. mysql> show variables like '%innodb_adaptive%';
  2. +----------------------------------+--------+
  3. | Variable_name | Value |
  4. +----------------------------------+--------+
  5. | innodb_adaptive_flushing | ON |
  6. | innodb_adaptive_flushing_lwm | 10 |
  7. | innodb_adaptive_hash_index | ON |
  8. | innodb_adaptive_hash_index_parts | 8 |
  9. | innodb_adaptive_max_sleep_delay | 150000 |
  10. +----------------------------------+--------+
  11. 5 rows in set, 1 warning (0.00 sec)

BTree

MySQL数据库索引采用的是B+Tree结构,在B-Tree结构上做了优化改造。

B-Tree结构

  • 索引值和data数据分布在整棵树结构中
  • 每个节点可以存放多个索引值及对应的data数据 树节点中的多个索引值
  • 从左到右升序排列

    Mysql上笔记 - 图45

B树的搜索:从根节点开始,对节点内的索引值序列采用二分法查找,如果命中就结束查找。没有 命中会进入子节点重复查找过程,直到所对应的的节点指针为空,或已经是叶子节点了才结束

B+Tree结构

  • 非叶子节点不存储data数据,只存储索引值,这样便于存储更多的索引值
  • 叶子节点包含了所有的索引值和data数据
  • 叶子节点用指针连接(双向),提高区间的访问性能

Mysql上笔记 - 图46

相比B树,B+树进行范围查找时,只需要查找定位两个节点的索引值,然后利用叶子节点的指针进行遍历即可。而B树需要遍历范围内所有的节点和数据,显然B+Tree效率高。

如下图,假如有一个2层的索引B+Tree,图中每个页面都已经被随机编号(编号可以认定为页 面号)

其中页面号为 20 的页面是 B+Tree 的根页面(根页面通常是存放在内存中的),根页面存 储了 ,pageno 是指向具体叶子节点的页面号。其他页面都是叶子节点,存放了具 体的数据 。

Mysql上笔记 - 图47

通过页面号为 20 的根节点可以快速得知 Key<10 的数据在 pageno 33 的页面,key在 [10,16) 范围的数据在 pageno 56 的页面。 叶子节点存放的 ,对于真正要存放哪些数据还得取决于该 B+Tree 是聚簇索引 (Clustered Index)还是辅助索引(Secondary Index)。

B+Tree 索引能够快速访问数据,就是因为存储引擎可以不再需要通过全表扫描来获取数据,而是从索引的根结点(通常在内存中)开始进行二分查找,根节点的槽中都存放了指向子节点的指针,存储引擎根据这些指针能够快速遍历数据。

4.3 聚簇索引和辅助索引

聚簇索引和非聚簇索引:B+Tree的叶子节点存放主键索引值和行记录就属于聚簇索引;如果索引值和行记录分开存放就属于非聚簇索引。

主键索引和辅助索引:B+Tree的叶子节点存放的是主键字段值就属于主键索引;如果存放的是非主键值就属于辅助索引(二级索引)。

聚簇索引(聚集索引)

聚簇索引是一种数据存储方式,InnoDB的聚簇索引就是按照主键顺序构建 B+Tree结构。B+Tree 的叶子节点就是行记录,行记录和主键值紧凑地存储在一起。 这也意味着 InnoDB 的主键索引就是数据表本身,它按主键顺序存放了整张表的数据,占用的空间就是整个表数据量的大小。通常说 的主键索引就是聚集索引。

InnoDB的表要求必须要有聚簇索引:

  • 如果表定义了主键,则主键索引就是聚簇索引
  • 如果表没有定义主键,则第一个非空unique列作为聚簇索引
  • 否则InnoDB会从建一个隐藏的row-id作为聚簇索引

辅助索引:

InnoDB辅助索引,也叫作二级索引,是根据索引列构建 B+Tree结构。但在 B+Tree 的叶子节点中 只存了索引列和主键的信息。二级索引占用的空间会比聚簇索引小很多, 通常创建辅助索引就是 为了提升查询效率。一个表InnoDB只能创建一个聚簇索引,但可以创建多个辅助索引。

Mysql上笔记 - 图48

5 索引分析与优化

5.1 Explain

我们在执行语句前面加上 explian可以对语句进行分析:

Mysql上笔记 - 图49

id值越大越先被执行。

select_type:

  1. simple: 表示查询语句不包含子查询或union,最常见的
  2. primary: 表示此查询是最外层的查询
  3. union: 表示此查询是union的第二个或后续的查询
  4. dependent_union: union中的第二个或后续的查询语句,使用到了外面的查询结果
  5. union_result: union查询结果
  6. ![](assets/image-20200826220010456.png#alt=image-20200826220010456)
  7. subquery: select 子查询语句
  8. ![](assets/image-20200826220150694.png#alt=image-20200826220150694)
  9. dependent subquery: select子查询语句依赖外层的查询结果
  10. 有时mysql对子查询的优化会导致dependent subquery,使用子查询的时候一定要注意,使用explain跑一遍看看。
  11. ![](assets/image-20200826221039033.png#alt=image-20200826221039033)

有这样一段话:

  1. mysql 在处理子查询时,会改写子查询。
  2. 通常情况下,我们希望由内到外,先完成子查询的结果,然后再用子查询来驱动外查询的表,完成查询。
  3. 例如:
  4. select * from test where tid in(select fk_tid from sub_test where gid=10)
  5. 通常我们会感性地认为该 sql 的执行顺序是:
  6. sub_test 表中根据 gid 取得 fk_tid(2,3,4,5,6)记录,
  7. 然后再到 test 中,带入 tid=2,3,4,5,6,取得查询数据。
  8. 但是实际mysql的处理方式为:
  9. select * from test where exists (
  10. select * from sub_test where gid=10 and sub_test.fk_tid=test.tid
  11. )
  12. mysql 将会扫描 test 中所有数据,每条数据都将会传到子查询中与 sub_test 关联,子查询不会先被执行,所以如果 test 表很大的话,那么性能上将会出现问题。

type: 通过它可以判断出查询是全表扫描还是基于索引的部分扫描

  • All:表示全表扫描,性能最差,大部分说明你的sql需要优化了。

  • index:表示基于索引的全表扫描,先扫描索引再扫描全表数据。先经过索引的全表扫描是有序的。

  • range:表示使用索引范围查询,使用>,>=,<,<=,in,between等。

  • ref:表示使用非唯一索引进行查询。 select * from u_user where real_name = '王豪' real_name为二级索引
    Mysql上笔记 - 图50

  • eq_ref:一般情况下出现在多表join查询,表示前面表的每一个记录,都只能匹配后面表的一行结果,一对一关系。

  • const:表示使用主键或唯一索引做等值查询。常量查询
    select * from u_user where id = 1
    Mysql上笔记 - 图51

  • NULL:表示不用访问表,速度最快。

possible_keys: 查询时能够使用的索引,不一定会使用,显示的是索引名称。

key: 表示查询时真正使用到的索引,显示的时索引名称。

key_len: 表示查询使用了索引的字节数,可以判断是否全部使用了组合索引,或只用到索引的最左部分字段。

  1. 计算规则:
  2. 字符串:字符串长度跟字符集有关。latin=1,gbk=2,utf8=3,utf8mb4=4 char(n):n*字符集长度
  3. varchar(n):n*字符集长度+2字节
  4. 数值类型:
  5. tinyint: 1byte
  6. smallint: 2byte
  7. mediumint: 3byte
  8. int,float: 4byte
  9. bigint,double: 8byte
  10. 时间类型:
  11. date: 3byte
  12. timestamp: 4byte (2038问题,大于2038的时间不能存储)
  13. datetime: 8byte
  14. 字段属性:
  15. null 属性只用一个byte,如果一个字段设置了not null,则没有此项。

ref:

rows: mysql索引优化器会根据统计信息,估算sql查询到结果需要扫描多少行记录。原则上rows越少效率越高,可以直观的了解到sql效率高低。

filtered:

extra:

  • using where
    1、查询条件没有用到索引,直接根据条件全表扫描。server 层对数据进行过滤。
    2、通常是查询条件有用到索引,但是其他的没有用索引的需要回表查询。需要在存储引擎层数据返回给server层对数据进行过滤。
    3、如下用id查询,查询的字段position没有索引,这个应该没有回表查询吧?
    1. select id,position from u_user where id in (1,2);


Mysql上笔记 - 图52

  • using index
    表示查询需要通过索引,索引就可以满足所需数据。

  • using filesort
    表示查询出来的结果需要额外排序,数据量小在内存,大的话在磁盘,有using filesort建议优化。

  • using emprorary
    查询使用到了临时表,一般用于去重,分组操作。

  • using index condition
    索引下沉的时候会出现。直接由存储引擎层根据条件进行索引过滤。
    如果用到了复合索引、或者like查询字段有索引左匹配时会出现。

5.2 回表查询

InnoDB索引有聚簇索引和辅助索引。聚簇索引的叶子节点存储行记录,InnoDB必须要 有,且只有一个。辅助索引的叶子节点存储的是主键值和索引字段值,通过辅助索引无法直接定位行记 录,通常情况下,需要扫描两遍索引树。先通过辅助索引定位主键值,然后再通过聚簇索引定位行记录,这就叫做回表查询,它的性能比扫一遍索引树低。

总结:通过索引查询主键值,然后再去聚簇索引查询记录信息

5.3 覆盖索引

只需要在一棵索引树上就能获取SQL所需的所 有列数据,无需回表,速度更快,这就叫做索引覆盖。

实现索引覆盖最常见的方法就是:将被查询的字段,建立到组合索引。

5.4 最左前缀原则

复合索引使用时遵循最左前缀原则,最左前缀顾名思义,就是最左优先,即查询中使用到最左边的列, 那么查询就会使用到索引,如果从索引的第二列开始查找,索引将失效。

Mysql上笔记 - 图53

5.5 Like查询

MySQL在使用like模糊查询时,索引能不能起作用?

MySQL在使用Like模糊查询时,索引是可以被使用的,只有把%字符写在后面才会使用到索引。

select * from user where name like ‘%o%’; //不起作用

select * from user where name like ‘o%’; //起作用

select * from user where name like ‘%o’; //不起作用

5.6 Using index condition

索引下沉,5.6版本之后加入的新特性

含义就是存储引擎层根据索引尽可能的过滤数据,然后在返回给服务器层根据where其他条件进行过滤

show variables like ‘%optimizer_switch%’;

可以看到 index_condition_pushdown=on说明索引下沉功能是开启的

  1. set optimizer_switch='index_condition_pushdown=off'; //关闭ICP
  1. index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on,use_invisible_indexes=off,skip_scan=on,hash_join=on

下面我们来看一个列子:

有如下表设计

  1. CREATE TABLE `u_user` (
  2. `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
  3. `pwd` varchar(70) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
  4. `user_code` varchar(10) NOT NULL DEFAULT '' COMMENT '员工编号',
  5. `real_name` varchar(30) NOT NULL COMMENT '姓名',
  6. `tel_no` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '手机号',
  7. `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '邮箱',
  8. `position` varchar(20) DEFAULT NULL COMMENT '职位',
  9. `head_img` varchar(150) DEFAULT NULL COMMENT '头像地址',
  10. `depart_id` int NOT NULL DEFAULT '-1' COMMENT '部门id',
  11. `status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '状态 1正常 0禁用',
  12. `can_pub_topic` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否可以发布主题 1是 0否',
  13. `can_pub_notice` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否可以发布公告 1是 0否',
  14. `deleted` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否删除 1是 0否',
  15. `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  16. PRIMARY KEY (`id`),
  17. KEY `idx_depart_id` (`depart_id`),
  18. KEY `idx_name` (`real_name`),
  19. KEY `idx_tel_no` (`tel_no`),
  20. KEY `idx_code` (`user_code`) USING BTREE
  21. ) ENGINE=InnoDB AUTO_INCREMENT=70 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

我们有如下查询语句:

  1. select * from u_user force index(idx_name) where id > 23 and real_name ='张三';

这里我们强制使用name索引,不让其走主键索引。

假如使用了索引下沉:

首先在存储引擎根据real_name字段索引树查到满足real_name=’张三’的数据,因为辅助索引保存了主键值,所以我们拿到这个主键id再来判断id>23是否满足,然后拿到满足条件的id回表去查询数据。

Mysql上笔记 - 图54

假如没有使用所以你下沉:

首先在存储引擎根据real_name字段索引树查到满足real_name=’张三’的数据,因为辅助索引保存了主键值,

拿到满足real_name=’张三’数据行id回表去查询,将数据行返回给mysql server层,mysql server层再通过id>23条件进行过滤。

Mysql上笔记 - 图55

5.7 Null查询

如果MySQL表的某一列含有NULL值,那么包含该列的索引是否有效?

null列可以使用索引,不论是普通二级索引,还是复合所有都有效。

但是:

对MySQL来说,NULL是一个特殊的值,从概念上讲,NULL意味着“一个未知值”,它的处理方式与其他 值有些不同。比如:不能使用=,<,>这样的运算符,对NULL做算术运算的结果都是NULL,count时 不会包括NULL行等,NULL比空字符串需要更多的存储空间等

NULL列需要增加额外空间来记录其值是否为NULL。对于MyISAM表,每一个空列额外占用一位,四舍 五入到最接近的字节

虽然MySQL可以在含有NULL的列上使用索引,但NULL和其他数据还是有区别的,不建议列上允许为 NULL。最好设置NOT NULL,并给一个默认值,比如0和 ‘’ 空字符串等,如果是datetime类型,也可以 设置系统当前时间或某个固定的特殊值,例如’1970-01-01 00:00:00’。

5.8 索引与排序

MySQL查询支持filesort和index两种方式的排序,filesort是先把结果查出,然后在缓存或磁盘进行排序 操作,效率较低。使用index是指利用索引自动实现排序,不需另做排序操作,效率会比较高。

filesort有两种排序算法:双路排序和单路排序。

双路排序:需要两次磁盘扫描读取,最终得到用户数据。第一次将排序字段读取出来,然后排序;第二 次去读取其他字段数据。

单路排序:从磁盘查询所需的所有列数据,然后在内存排序将结果返回。如果查询数据超出缓存 sort_buffer,会导致多次磁盘读取操作,并创建临时表,最后产生了多次IO,反而会增加负担。

解决方 案:少使用select *;增加sort_buffer_size容量(尽量不要修改)和max_length_for_sort_data容量

  1. mysql> show variables like '%sort_buffer%';
  2. +-------------------------+---------+
  3. | Variable_name | Value |
  4. +-------------------------+---------+
  5. | innodb_sort_buffer_size | 1048576 | 1M
  6. | myisam_sort_buffer_size | 8388608 |
  7. | sort_buffer_size | 262144 |256k
  8. +-------------------------+---------+
  9. 3 rows in set, 1 warning (0.00 sec)

如果我们Explain分析SQL,结果中Extra属性显示Using filesort,表示使用了filesort排序方式,需要优化。如果Extra属性显示Using index时,表示覆盖索引,也表示所有操作在索引上完成,也可以使用 index排序方式,建议大家尽可能采用覆盖索引。

以下几种情况,会使用index方式的排序。

ORDER BY 子句索引列组合满足索引最左前列

  1. explain select id from user order by id; //对应(id)、(id,name)索引有效

WHERE子句+ORDER BY子句索引列组合满足索引最左前列

  1. explain select id from user where age=18 order by name; //对应 (age,name)复合索引

以下几种情况,会使用filesort方式的排序。

对索引列同时使用了ASC和DESC

  1. explain select id from user order by age asc,name desc; //对应(age,name)复合索引

WHERE子句和ORDER BY子句满足最左前缀,但where子句使用了范围查询(例如>、<、in 等)

  1. explain select id from user where age>10 order by name; //对应(age,name)复合索引

ORDER BY或者WHERE+ORDER BY索引列没有满足索引最左前列

  1. explain select id from user order by name; //对应(age,name)索引

使用了不同的索引,MySQL每次只采用一个索引,ORDER BY涉及了两个索引

  1. explain select id from user order by name,age; //对应(name)、(age)两个索引

WHERE子句与ORDER BY子句,使用了不同的索引

  1. explain select id from user where name='tom' order by age; //对应(name)、(age)索引

WHERE子句或者ORDER BY子句中索引列使用了表达式,包括函数表达式

  1. explain select id from user order by abs(age); //对应(age)索引

5.9 查询定位

查看慢查询日志功能状态:其实慢的增删改语句也会记录

  1. mysql> show variables like '%slow_query_log%';
  2. +---------------------+----------------------------------------------------------+
  3. | Variable_name | Value |
  4. +---------------------+----------------------------------------------------------+
  5. | slow_query_log | ON |
  6. | slow_query_log_file | D:\develop\mysql-8.0.19-winx64\data\buqingyishi-slow.log |
  7. +---------------------+----------------------------------------------------------+
  8. 2 rows in set, 1 warning (0.00 sec)

设置参数:

  1. #开启慢查询日志
  2. SET global slow_query_log = ON;
  3. #设置文件名
  4. SET global slow_query_log_file = 'mysql-slow.log';
  5. #记录没有使用索引的查询日志 必须开启slow_query_log
  6. SET global log_queries_not_using_indexes = ON;
  7. #指定慢查询的阀值,单位秒。如果SQL执行时间超过阀值,就属于慢查询记录到日志文件中
  8. SET long_query_time = 1;

文本方式查看 各属性含义:

  1. time:日志记录的时间
  2. User@Host:执行的用户及主机
  3. Query_time:执行的时间
  4. Lock_time:锁表时间
  5. Rows_sent:发送给请求方的记录数,结果数量
  6. Rows_examined:语句扫描的记录条数
  7. SET timestamp:语句执行的时间点
  8. select....:执行的具体的SQL语句

使用mysqldumpslow查看:

MySQL 提供了一个慢查询日志分析工具mysqldumpslow,可以通过该工具分析慢查询日志 内容。

需要安装perl环境。

查看帮助信息:

  1. perl mysqldumpslow.pl --help
  1. perl mysqldumpslow.pl -t 5 -s at C:\ProgramData\MySQL\Data\mysql-slow.log

也可以使用第三方分析工具,比如pt-query-digest【参考博客:https://blog.csdn.net/xiaoweite1/article/details/80299754】、 mysqlsla等。

5.10 慢查询优化

索引是为了优化查询效果,但是查询的快慢和有没有使用索引没有必然的关系。

如下:

  1. select * from u_user where id > 0;
  2. select * from u_user;

这两个其实都是会进行全表扫描的。

因此,我们在使用索引时,不要只关注是否起作用,应该关心索引是否减少了查询扫描的数据行数,如果 扫描行数减少了,效率才会得到提升。对于一个大表,不止要创建索引,还要考虑索引过滤性,过 滤性好,执行速度才会快。

有如下表结构设计

  1. CREATE TABLE `u_user` (
  2. `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
  3. `pwd` varchar(70) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
  4. `user_code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '员工编号',
  5. `real_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '姓名',
  6. `tel_no` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '手机号',
  7. `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '邮箱',
  8. `position` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '职位',
  9. `head_img` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '头像地址',
  10. `depart_id` int NOT NULL DEFAULT '-1' COMMENT '部门id',
  11. `status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '状态 1正常 0禁用',
  12. `can_pub_topic` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否可以发布主题 1是 0否',
  13. `can_pub_notice` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否可以发布公告 1是 0否',
  14. `deleted` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否删除 1是 0否',
  15. `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  16. PRIMARY KEY (`id`) USING BTREE,
  17. KEY `idx_depart_id` (`depart_id`) USING BTREE,
  18. KEY `idx_name` (`real_name`) USING BTREE,
  19. KEY `idx_tel_no` (`tel_no`) USING BTREE,
  20. KEY `idx_code` (`user_code`) USING BTREE
  21. ) ENGINE=InnoDB AUTO_INCREMENT=74 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

445 6448数据

select count() from u_user一个简单的数量统计都需要十几秒,我们肯定是不能忍受的,所以我们在查询的时候一定要加过滤条件。如果真的需要查询大量数据或者类似于这种count(),就需要缓存了。

当depart_id和real_name都是独立的索引的时候

  1. explain select * from u_user where depart_id = 3 and real_name like '王%';
  1. mysql> explain select * from u_user where depart_id = 3 and real_name like '王%' \G;
  2. *************************** 1. row ***************************
  3. id: 1
  4. select_type: SIMPLE
  5. table: u_user
  6. partitions: NULL
  7. type: ref
  8. possible_keys: idx_depart_id,idx_name
  9. key: idx_depart_id
  10. key_len: 4
  11. ref: const
  12. rows: 884452
  13. filtered: 11.19
  14. Extra: Using where
  15. 1 row in set, 1 warning (0.00 sec)

需要扫描884452行数据扫描,没有索引下沉。

我们在 depart_id和real_name创建复合索引,数据量大,创建索引也很耗时

  1. create index idx_depart_id_name on u_user(depart_id,real_name);
  1. mysql> explain select * from u_user where depart_id = 3 and real_name like '王%' \G;
  2. *************************** 1. row ***************************
  3. id: 1
  4. select_type: SIMPLE
  5. table: u_user
  6. partitions: NULL
  7. type: range
  8. possible_keys: idx_depart_id,idx_name,idx_depart_id_name
  9. key: idx_depart_id_name
  10. key_len: 126
  11. ref: NULL
  12. rows: 126590
  13. filtered: 100.00
  14. Extra: Using index condition; Using MRR
  15. 1 row in set, 1 warning (0.01 sec)

需要扫描126590行数据,使用了索引下沉。

而且 也很快了。

  1. select count(id) from u_user where depart_id = 3 and real_name like '王%';

但其实我们的数据只有65536行数据,我们还可以进行优化。 100多ms。

加入虚拟列,比如我们可以针对姓单独搜索,但是不要像有些脑残产品非要几个条件合到一起那种,再怎么做都无法优化了。

//为u_user表添加first_name虚拟列,以及联合索引(first_name,depart_id) 然后针对first_name做等值查询

  1. alter table u_user add first_name varchar(2) generated always as (left(real_name, 1)), add index(first_name, depart_id);
  1. explain select * from u_user where depart_id = 3 and first_name = '王';
  1. mysql> explain select * from u_user where depart_id = 3 and first_name = '王' \G;
  2. *************************** 1. row ***************************
  3. id: 1
  4. select_type: SIMPLE
  5. table: u_user
  6. partitions: NULL
  7. type: ref
  8. possible_keys: idx_depart_id,idx_depart_id_name,first_name
  9. key: first_name
  10. key_len: 15
  11. ref: const,const
  12. rows: 125282
  13. filtered: 100.00
  14. Extra: NULL
  15. 1 row in set, 1 warning (0.00 sec)

虽然扫描的行也是15282,但是我们的type从range变成ref了,速率更高了。

select count(id) from u_user where depart_id = 3 and first_name = ‘王’; 也变成了几十ms左右。

慢查询原因:

  1. 全表扫描:explain分析type属性all
  2. 全索引扫描:explain分析type属性index
  3. 索引过滤性不好:靠索引字段选型、数据量和状态、表设计
  4. 频繁的回表查询开销:尽量少用select *,使用覆盖索引

5.11 分页查询优化

我们可以先开启 profiling。

  1. show variables like 'profiling';
  2. set profiling=1;#不要加global

一般性分页

般的分页查询使用简单的 limit 子句就可以实现。limit格式如下:

  1. SELECT * FROM 表名 LIMIT [offset,] rows

第一个参数指定第一个返回记录行的偏移量,注意从0开始; 第二个参数指定返回记录行的最大数目; 如果只给定一个参数,它表示返回最大的记录行数目;

测试:

如果偏移量固定,返回记录量对执行时间有什么影响?

  1. select * from u_user limit 10000,1;
  2. select * from U_user limit 10000,10;
  3. select * from u_user limit 10000,100;
  4. select * from u_user limit 10000,1000;
  5. select * from u_user limit 10000,10000;

然后 show profiles;

  1. mysql> show profiles;
  2. +----------+------------+----------------------------------------+
  3. | Query_ID | Duration | Query |
  4. +----------+------------+----------------------------------------+
  5. | 1 | 0.00365075 | show variables like 'profiling' |
  6. | 2 | 0.05038300 | select * from u_user limit 10000,1 |
  7. | 3 | 0.01735725 | select * from U_user limit 10000,10 |
  8. | 4 | 0.01718300 | select * from u_user limit 10000,100 |
  9. | 5 | 0.01968875 | select * from u_user limit 10000,1000 |
  10. | 6 | 0.07443350 | select * from u_user limit 10000,10000 |
  11. +----------+------------+----------------------------------------+
  12. 6 rows in set, 1 warning (0.00 sec)

结果:在查询记录时,返回记录量低于100条,查询时间基本没有变化,差距不大。随着查询记录量越大,所花费的时间也会越来越多。

如果查询偏移量变化,返回记录数固定对执行时间有什么影响?

  1. select * from u_user limit 1,100;
  2. select * from u_user limit 10,100;
  3. select * from u_user limit 100,100;
  4. select * from u_user limit 1000,100;
  5. select * from u_user limit 10000,100;

show profiles

  1. mysql> show profiles;
  2. +----------+------------+----------------------------------------+
  3. | Query_ID | Duration | Query |
  4. +----------+------------+----------------------------------------+
  5. `````````8 | 0.00083000 | select * from user limit 1,100 |
  6. | 9 | 0.00128050 | select * from u_user limit 1,100 |
  7. | 10 | 0.00095900 | select * from u_user limit 10,100 |
  8. | 11 | 0.00111350 | select * from u_user limit 100,100 |
  9. | 12 | 0.00291275 | select * from u_user limit 1000,100 |
  10. | 13 | 0.01799550 | select * from u_user limit 10000,100

可以看到随着偏移量的增大,查询数据会变长。当偏移量大于100的时候,查询时间急剧增加。

这种分页查询机制,每次都会从数据库第一条记录开始扫描,越往后查询越慢,而 且查询的数据越多,也会拖慢总查询速度。

分页优化方案

  1. 利用覆盖索引优化:

id和real_name都是索引

  1. select * from u_user limit 10000,100;
  2. select id from u_user limit 10000,100;
  3. select id,real_name from u_user limit 1000,100;
  1. 2.使用子查询优化
  1. select * from u_user limit 10000,100;
  2. select * from u_user where id>(select id from u_user limit 10000,1) limit 100
  1. 9 | 0.00128050 | select * from u_user limit 1,100 |
  2. | 10 | 0.00095900 | select * from u_user limit 10,100 |
  3. | 11 | 0.00111350 | select * from u_user limit 100,100 |
  4. | 12 | 0.00291275 | select * from u_user limit 1000,100 |
  5. | 13 | 0.01799550 | select * from u_user limit 10000,100 |
  6. | 14 | 0.00037450 | select name from u_user limit 10000,100 |
  7. | 15 | 0.01592600 | select real_name from u_user limit 10000,100 |
  8. | 16 | 0.01553750 | select * from u_user where id>=(select id from u_user limit 10000,1) limit 100 |
  9. | 17 | 0.00408600 | select real_name from u_user where id>=(select id from u_user limit 10000,1) limit 100

可以看到查询性能提升了。

使用了id做主键比较(id>=),并且子查询使用了覆盖索引进行优化。

5.12 问题:

select real_name from u_user where id>=(select id from u_user limit 10000,1) limit 100

select id,position from u_user where id = 2; #主键索引,type=const

select id,position from u_user where id in (1,2); #主键索引,type=range,extra=using where

  1. Using where过滤元组和是否读取数据文件或索引文件没有关系

select id,position from u_user where id > 50 limit 100000,1000;#主键索引,type=range,extra=using where

select id from u_user limit 100000,100; #idx_depart_id,type=index,extra=using index

select id from u_user force index(primary) limit 100000,100; #primary,type=index,extra=using index

select id,depart_id from u_user where depart_id in (1,2,3,9,5) #idx_deaprt_id,type=range,extry=using where;using index

select id,depart_id,real_name from u_user where depart_id > 3;

6 ACID

在关系型数据库管理系统中,一个逻辑工作单元要成为事务,必须满足这 4 个特性,即所谓的 ACID: 原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。

6.1 原子性 Atomicity

原子性:事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。

修改—-》Buffer Pool修改—-》刷盘。可能会有下面两种情况:

  1. 事务提交了,如果此时Buffer Pool的脏页没有刷盘,如何保证修改的数据生效? redo
  2. 如果事务没提交,但是Buffer Pool的脏页刷盘了,如何保证不该存在的数据撤销?(会出现这种情况,刷盘是异步的,和提没提交有没有关系,commit之后应该会记录point)Undo

每一个写事务,都会修改BufferPool,从而产生相应的Redo/Undo日志,在Buffer Pool 中的页被刷到磁盘之前,这些日志信息都会先写入到日志文件中。

如果 Buffer Pool 中的脏页没有刷成功,此时数据库挂了,那在数据库再次启动之后,可以通过 Redo 日志将其恢复出来,以保证脏页写的数据不会丢 失。

如果脏页刷新成功,此时数据库挂了,就需要通过Undo来实现了。

6.2 持久性

持久性:指的是一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,后续的操作或故障不应该对其有任何影响,不会丢失。

如下图所示,一个“提交”动作触发的操作有:binlog落地、发送binlog、存储引擎提交、flush_logs, check_point、事务提交标记等。这些都是数据库保证其数据完整性、持久性的手段。

Mysql上笔记 - 图56

MySQL的持久性也与WAL技术相关,redo log在系统Crash重启之类的情况时,可以修复数据,从而保 障事务的持久性。通过原子性可以保证逻辑上的持久性,通过存储引擎的数据刷盘可以保证物理上的持 久性。

6.3 隔离性

隔离性:指的是一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他的并发事务是隔离的。

InnoDB 支持的隔离性有 4 种,隔离性从低到高分别为:读未提交、读提交、可重复读、可串行化。锁和多版本控制(MVCC)技术就是用于保障隔离性的(后面课程详解)。

6.4 一致性

一致性:指的是事务开始之前和事务结束之后,数据库的完整性限制未被破坏。一致性包括两方面的内容,分别是约束一致性和数据一致性。

约束一致性:创建表结构时所指定的外键、Check、唯一索引等约束,可惜在 MySQL 中不支持 Check 。

数据一致性:是一个综合性的规定,因为它是由原子性、持久性、隔离性共同保证的结果,而不是 单单依赖于某一种技术。

一致性也可以理解为数据的完整性。数据的完整性是通过原子性、隔离性、持久性来保证的,而这3个 特性又是通过 Redo/Undo 来保证的。逻辑上的一致性,包括唯一索引、外键约束、check 约束,这属 于业务逻辑范畴。

Mysql上笔记 - 图57

ACID 及它们之间的关系如下图所示,4个特性中有3个与 WAL 有关系,都需要通过 Redo、Undo 日志 来保证等。

WAL的全称为Write-Ahead Logging,先写日志,再写磁盘。

Mysql上笔记 - 图58

7事务控制的演进

7.1 事务并发

事务并发处理可能会带来一些问题,比如:更新丢失、脏读、不可重复读、幻读等。

  • 更新丢失 :当两个或多个事务更新同一行记录,会产生更新丢失现象。可以分为回滚覆盖和提交覆盖。
    回滚覆盖:一个事务回滚操作,把其他事务已提交的数据给覆盖了。
    提交覆盖:一个事务提交操作,把其他事务已提交的数据给覆盖了。

  • 脏读 :一个事务读取到了另一个事务修改但未提交的数据。

  • 不可重复读: 一个事务中多次读取同一行记录不一致,后面读取的跟前面读取的不一致。

  • 幻读 :一个事务中多次按相同条件查询,结果不一致。后续查询的结果和面前查询结果不同,多了或少了 几行记录。

7.2 排队

最简单的方法,就是完全顺序执行所有事务的数据库操作,不需要加锁,简单的说就是全局排队。序列化执行所有的事务单元,数据库某个时刻只处理一个事务操作,特点是强一致性,处理性能低。

注意这里说的是整个数据库。

Mysql上笔记 - 图59

7.3 排他锁

引入锁之后就可以支持并发处理事务,如果事务之间涉及到相同的数据项时,会使用排他锁,或叫互斥 锁,先进入的事务独占数据项以后,其他事务被阻塞,等待前面的事务释放锁。

在transaction1结束之前,transation2必须等待直到transaction1释放锁。

Mysql上笔记 - 图60

7.4 读写锁

读和写操作:读读、写写、读写、写读。

读写锁就是进一步细化锁的颗粒度,区分读操作和写操作,让读和读之间不加锁,这样下面的两个事务就可以同时被执行了。

读写锁,可以让读和读并行,而读和写、写和读、写和写这几种之间还是要加排他锁。

Mysql上笔记 - 图61

7.5 MVCC

多版本控制MVCC,也就是Copy on Write的思想。MVCC除了支持读和读并行,还支持读和写、写和读 的并行,但为了保证一致性,写和写是无法并行的。

在事务1开始写操作的时候会copy一个记录的副本,其他事务读操作会读取这个记录副本,因此不会影 响其他事务对此记录的读取,实现写和读并行。

Mysql上笔记 - 图62

在事务1开始写操作的时候会copy一个记录的副本,其他事务读操作会读取这个记录副本,因此不会影响其他事务对此记录的读取,实现写和读并行。

如下一端代码:

  1. @Transactional
  2. public void update(Integer id,String name) throws InterruptedException {
  3. ///第一次查询
  4. Student r = getById(id);
  5. Thread.sleep(6000);
  6. Student student = new Student();
  7. student.setName(name);
  8. student.setId(id);
  9. //更新
  10. updateById(student);
  11. Thread.sleep(50000);
  12. //第二次查询
  13. Student queryS = getById(id);
  14. System.out.println(queryS);
  15. Thread.sleep(1000);
  16. }

当第一次查询之后,当前业务阻塞,其他事务还是能够对其进行更新操作,但是这里是无法读到的别的事务更新的,当执行到更新方法之后,如果事务没有提交,其他事务就不能对其进行写操作。

MVCC(Multi Version Concurrency Control)被称为多版本控制,是指在数据库中为了实现高并发的 数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本。

多版本控制很巧妙地将稀缺资源的独占互斥转换为并发,大大提高了数据库的吞吐量及读写性能。 如何生成的多版本?

每次事务修改操作之前,都会在Undo日志中记录修改之前的数据状态和事务号, 该备份记录可以用于其他事务的读取,也可以进行必要时的数据回滚。

实现原理

MVCC最大的好处是读不加锁,读写不冲突。在读多写少的系统应用中,读写不冲突是非常重要的,极 大的提升系统的并发性能,这也是为什么现阶段几乎所有的关系型数据库都支持 MVCC 的原因,不过目 前MVCC只在 Read Commited 和 Repeatable Read 两种隔离级别下工作。

在 MVCC 并发控制中,读操作可以分为两类: 快照读(Snapshot Read)与当前读 (Current Read)。

  • 快照读:读取的是记录的快照版本(有可能是历史版本),不用加锁。(select)
  • 当前读:读取的是记录的最新版本,并且当前读返回的记录,都会加锁,保证其他事务不会再并发 修改这条记录。(select… for update 或lock in share mode,insert/delete/update会触发当前读)

如下图:

1~6 是其对应的数据。后面三个隐含字段分别对应该行的隐含ID、事务号和回滚指针

Mysql上笔记 - 图63

具体的更新过程:

假如一条数据是刚 INSERT 的,DB_ROW_ID 为 1,其他两个字段为空。当事务 1 更改该行的数据值 时,会进行如下操作

Mysql上笔记 - 图64

  1. 用排他锁锁定该行;记录 Redo log;
  2. 把该行修改前的值复制到 Undo log,即图中下面的行;
  3. 修改当前行的值,填写事务编号,使回滚指针指向 Undo log 中修改前的行。

接下来事务2操作,过程与事务 1 相同,此时 Undo log 中会有两行记录,并且通过回滚指针连在一 起,通过当前记录的回滚指针回溯到该行创建时的初始内容

Mysql上笔记 - 图65

MVCC已经实现了读读、读写、写读并发处理,如果想进一步解决写写冲突,

可以采用下面两种方案:

乐观锁 版本号控制

悲观锁

8 事务隔离级别

Mysql上笔记 - 图66

  • Read Uncommitted 读未提交 :解决了回滚覆盖类型的更新丢失,但可能发生脏读现象,也就是 可能读取到其他会话中未提交事务修改的数据。

  • Read Committed 读已提交:只能读取到其他会话中已经提交的数据,解决了脏读。但可能发生 不可重复读现象,也就是可能在一个事务中两次查询结果不一致。

  • Repeatable Read 可重复读:解决了不可重复读,它确保同一事务的多个实例在并发读取数据 时,会看到同样的数据行。不过理论上会出现幻读,简单的说幻读指的的当用户读取某一范围的数 据行时,另一个事务又在该范围插入了新行,当用户在读取该范围的数据时会发现有新的幻影行。 mysql引入了间隙锁,解决了通常情况的幻读问题,但是没有完全解决。

  • Serializable 串行化:所有的增删改查串行执行。它通过强制事务排序,解决相互冲突,从而解决 幻度的问题。这个级别可能导致大量的超时现象的和锁竞争,效率低下。读写,写读,写写是会阻塞的,读读是不会阻塞的。

数据库的事务隔离级别越高,并发问题就越小,但是并发处理能力越差(代价)。读未提交隔离级别最 低,并发问题多,但是并发处理能力好。以后使用时,可以根据系统特点来选择一个合适的隔离级别, 比如对不可重复读和幻读并不敏感,更多关心数据库并发处理能力,此时可以使用Read Commited隔 离级别。

8.1 事务隔离级别和锁的关系

1)事务隔离级别是SQL92定制的标准,相当于事务并发控制的整体解决方案,本质上是对锁和MVCC使 用的封装,隐藏了底层细节。

2)锁是数据库实现并发控制的基础,事务隔离性是采用锁来实现,对相应操作加不同的锁,就可以防止其他事务同时对数据进行读写操作。

3)对用户来讲,首先选择使用隔离级别,当选用的隔离级别不能解决并发问题或需求时,才有必要在开发中手动的设置锁。

MySQL默认隔离级别:可重复读

Oracle、SQLServer默认隔离级别:读已提交

8.2 Mysql 隔离级别控制

  1. mysql> show variables like '%isolation%';
  2. +-----------------------+-----------------+
  3. | Variable_name | Value |
  4. +-----------------------+-----------------+
  5. | transaction_isolation | REPEATABLE-READ |
  6. +-----------------------+-----------------+

当前会话设置隔离级别:如果是全局的可以加global

  1. set transaction_isolation='READ-UNCOMMITTED';
  2. set transaction_isolation='READ-COMMITTED';
  3. set transaction_isolation='REPEATABLE-READ';
  4. set transaction_isolation='SERIALIZABLE';

mysql开启事务

  1. begin;#开启事务
  2. update ...
  3. select ...
  4. insert ...
  5. delete ..
  6. commit;#提交事务

9 锁机制

9.0 锁分类

从操作的粒度可分为表级锁、行级锁和页级锁:

  • 表级锁:每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在 MyISAM、InnoDB、BDB 等存储引擎中。

  • 行级锁:每次操作锁住一行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应 用在InnoDB 存储引擎中。

  • 页级锁:每次锁定相邻的一组记录,锁定粒度界于表锁和行锁之间,开销和加锁时间界于表 锁和行锁之间,并发度一般。应用在BDB 存储引擎中。

从操作的类型可分为读锁和写锁:

  • 读锁(S锁):共享锁,针对同一份数据,多个读操作可以同时进行而不会互相影响。 事务A对记录添加了S锁,可以对记录进行读操作,不能做修改,其他事务可以对该记录追加 S锁,但是不能追加X锁,需要追加X锁,需要等记录的S锁全部释放。

  • 写锁(X锁):排他锁,当前写操作没有完成前,它会阻断其他写锁和读锁。事务A对记录添加了X锁,可以对记录进行读和修改操作,其他事务不能对记录做修改操作。
    IS锁、IX锁:意向读锁、意向写锁,属于表级锁,S和X主要针对行级锁。在对表记录添加S或X锁之 前,会先对表添加IS或IX锁。

从操作的性能可分为乐观锁和悲观锁:

  • 乐观锁:一般的实现方式是对记录数据版本进行比对,在数据更新提交的时候才会进行冲突 检测,如果发现冲突了,则提示错误信息。 这种方式其实是没有加锁的。

  • 悲观锁:在对一条数据修改的时候,为了避免同时被其他人修改,在修改数据之前先锁定, 再修改的控制方式。共享锁和排他锁是悲观锁的不同实现,但都属于悲观锁范畴。

9.1 innodb 行级锁原理

在InnoDB引擎中,我们可以使用行锁和表锁,其中行锁又分为共享锁和排他锁。InnoDB行锁是通过对索引数据页上的记录加锁实现的,主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock。

  • RecordLock锁:锁定单个行记录的锁。(记录锁,RC、RR隔离级别都支持)

  • GapLock锁:间隙锁,锁定索引记录间隙,确保索引记录的间隙不变。(范围锁,RR隔离级别支 持)

  • Next-key Lock 锁:记录锁和间隙锁组合,同时锁住数据,并且锁住数据前后范围。(记录锁+范围锁,RR隔离级别支持)

在RR隔离级别,InnoDB对于记录加锁行为都是先采用Next-Key Lock,但是当SQL操作含有唯一索引 时,Innodb会对Next-Key Lock进行优化,降级为RecordLock,仅锁住索引本身而非范围。

1)select … from 语句:InnoDB引擎采用MVCC机制实现非阻塞读,所以对于普通的select语句,InnoDB不加锁

2)select … from lock in share mode语句:追加了共享锁,InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。

3)select … from for update语句:追加了排他锁,InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。

4)update … where 语句:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。

5)delete … where 语句:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。

6)insert语句:InnoDB会在将要插入的那一行设置一个排他的RecordLock锁。

我们来测试一下间隙锁 Next-key Lock的存在:

首先创建表:id不设为主键,

  1. create table t1(id int,name varchar(20),index idx_id(id))engine=innodb charset=utf8;
  2. #然后创建几条数据
  3. insert into t1 values(1,'h');
  4. insert into t1 values(5,'m');
  5. insert into t1 values(9,'m');
  6. insert into t1 values(12,'g');

我们可以开始两个窗口进行测试,注意要使用 begin开启事务。

比如我们在一个窗口

  1. begin;
  2. select * from t1 where id=9 for update;

另一个窗口:

  1. begin;
  2. insert into t1 values(7,'l');#处于阻塞状态
  3. insert into t1 values(2,'v');#可以执行
  4. insert into t1 values(10,'b');#处于阻塞状态

说明已经锁住了 5-9,9-12之间的间隙。

以“update t1 set name=‘XX’ where id=10”操作为例,举例子分析下 InnoDB 对不同索引的加锁行 为,以RR隔离级别为例。

  1. begin;
  2. select * from t1 where id=12 for update;

大于12的也会被锁住,无法加入锁。

另一个事务:

  1. begin;
  2. insert into t1 values(149,'c');#处于阻塞状态

主键加锁: 加锁行为:仅在id=10的主键索引记录上加X锁。

Mysql上笔记 - 图67

唯一键加锁:加锁行为:先在唯一索引id上加X锁,然后在id=10的唯一索引记录上加X锁。

Mysql上笔记 - 图68

非唯一键加锁:对满足id=10条件的记录和主键分别加X锁,然后在(6,c)-(10,b)、(10,b)-(10,d)、(10,d)- (11,f)范围分别加Gap Lock。

Mysql上笔记 - 图69

6-10,10,10-11之间是插入删除数据是会被阻塞的,也不能插入删除id=6,11的数据,但是可以更新;

无索引加锁:表里所有行和间隙都会加X锁。(当没有索引时,会导致全表锁定,因为InnoDB引擎锁机制是基于索引实现的记录锁定)。

Mysql上笔记 - 图70

Mysql上笔记 - 图71

比如我

  1. begin;
  2. select * from studeng where name='sfs' for update;

先不提交事务,

在另一个窗口:

  1. begin;
  2. insert into student values(130,"哈哈",43,"34324");#会被阻塞

但是有一种情况:

初始数据:

  1. mysql> select * from t1;
  2. +------+------+
  3. | id | name |
  4. +------+------+
  5. | 1 | h |
  6. | 5 | x |
  7. | 9 | m |
  8. | 12 | g |
  9. | 15 | i |
  10. | 1500 | i |
  11. | 9 | e |
  12. | 16 | v |
  13. | 10 | v |
  14. | 9 | v |
  15. | 7 | q |
  16. | 9 | q |
  17. +------+------+
  1. begin;
  2. select * from t1 where id=9;
  3. select * from t1 where id=15 for update;

这样id=9的记录是没有加锁,并且他的范围也没加锁。

另一个窗口:

  1. begin;
  2. insert into t1 values(9,'t');#是不会被阻塞的
  3. commit;#待commit之后,上面的事务再去查询id=9的记录是查询不到这条记录的

9.2 悲观锁

悲观锁(Pessimistic Locking),是指在数据处理过程,将数据处于锁定状态,一般使用数据库的锁机 制实现。从广义上来讲,前面提到的行锁、表锁、读锁、写锁、共享锁、排他锁等,这些都属于悲观锁 范畴。

表级锁

表级锁每次操作都锁住整张表,并发度最低。常用命令如下:

手动增加表锁:

  1. lock table tableName1 read|write,tableName2 read|write;

查看表级锁:

  1. show open tables;

删除表锁:

  1. unlock tables;

表级读锁:当前表追加read锁,当前连接和其他的连接都可以读操作;但是当前连接增删改操作会报错,其他连接增删改会被阻塞。

表级写锁:当前表追加write锁,当前连接可以对表做增删改查操作,其他连接对该表所有操作都被阻塞(包括查询)。

总结:表级读锁会阻塞写操作,但是不会阻塞读操作。而写锁则会把读和写操作都阻塞。

行级锁

共享锁(行级锁-读锁) 共享锁又称为读锁,简称S锁。共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数 据,但是只能读不能修改。使用共享锁的方法是在select … lock in share mode,只适用查询语句。

总结:事务使用了共享锁(读锁),只能读取,不能修改,修改操作被阻塞。

排他锁(行级锁-写锁) 排他锁又称为写锁,简称X锁。排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能对该行记录做其他操作,也不能获取该行的锁。

使用排他锁的方法是在SQL末尾加上for update,innodb引擎默认会在update,delete语句加上 for update。行级锁的实现其实是依靠其对应的索引,所以如果操作没用到索引的查询,那么会锁住全表记录。

9.3 乐观锁

乐观锁是相对于悲观锁而言的,它不是数据库提供的功能,需要开发者自己去实现。在数据库操作时, 想法很乐观,认为这次的操作不会导致冲突,因此在数据库操作时并不做任何的特殊处理,即不加锁, 而是在进行事务提交时再去判断是否有冲突了

version版本控制。

时间戳。

9.4 死锁与解决方案

表锁死锁

产生原因:

用户A访问表A(锁住了表A),然后又访问表B;另一个用户B访问表B(锁住了表B),然后企图 访问表A;这时由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要 等用户A释放表A才能继续,这就死锁就产生了。

用户A—》A表(表锁)—》B表(表锁)

用户B—》B表(表锁)—》A表(表锁)

解决方案:

这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。仔细分 析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进行处理,尽量避免同时锁定两个 资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。

行级锁死锁

产生原因1: 如果在事务中执行了一条没有索引条件的查询,引发全表扫描,把行级锁上升为全表记录锁定(等 价于表级锁),多个这样的事务执行后,就很容易产生死锁和阻塞,最终应用系统会越来越慢,发生阻塞或死锁。

解决方案1:

SQL语句中不要使用太复杂的关联多表的查询;

使用explain“执行计划”对SQL语句进行分析,对于 有全表扫描和全表锁定的SQL语句,建立相应的索引进行优化。

产生原因2:两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁

Mysql上笔记 - 图72

解决方案2:

在同一个事务中,尽可能做到一次锁定所需要的所有资源

按照id对资源排序,然后按顺序进行处理

事务①

  1. mysql> begin; 1
  2. Query OK, 0 rows affected (0.00 sec)
  3. mysql> select * from student where id = 2 lock in share mode; 2
  4. +----+--------+------+----------+
  5. | id | name | age | phone_no |
  6. +----+--------+------+----------+
  7. | 2 | 小儿 | 34 | 234 |
  8. +----+--------+------+----------+
  9. 1 row in set (0.00 sec)
  10. mysql> update student set name="hahah" where id=13; 6
  11. ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

事务②

  1. mysql> begin; 3
  2. Query OK, 0 rows affected (0.00 sec)
  3. mysql> select * from student where id = 13 lock in share mode; 4
  4. +----+--------+------+----------+
  5. | id | name | age | phone_no |
  6. +----+--------+------+----------+
  7. | 13 | 王豪 | 25 | 3234 |
  8. +----+--------+------+----------+
  9. 1 row in set (0.00 sec)
  10. mysql> update student set name = "王豪" where id =2; 5
  11. Query OK, 1 row affected (23.62 sec)
  12. Rows matched: 1 Changed: 1 Warnings: 0

执行顺序 1,2,3,4,5,6就会出现上面的死锁

共享锁转换为排他锁

事务A 查询一条纪录,然后更新该条纪录;此时事务B 也更新该条纪录,这时事务B 的排他锁由于 事务A 有共享锁,必须等A 释放共享锁后才可以获取,只能排队等待。事务A 再执行更新操作时, 此处发生死锁,因为事务A 需要排他锁来做更新操作。但是,无法授予该锁请求,因为事务B 已经 有一个排他锁请求,并且正在等待事务A 释放其共享锁。

解决方案: 对于按钮等控件,点击立刻失效,不让用户重复点击,避免引发同时对同一条记录多次操作;

使用乐观锁进行控制。乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统性能。

需要注意的是,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中;

如下:

事务①:

  1. mysql> begin; 1
  2. Query OK, 0 rows affected (0.00 sec)
  3. mysql> select * from student where id = 2 lock in share mode; 2
  4. +----+--------+------+----------+
  5. | id | name | age | phone_no |
  6. +----+--------+------+----------+
  7. | 2 | 王豪 | 34 | 234 |
  8. +----+--------+------+----------+
  9. 1 row in set (0.00 sec)
  10. mysql> update student set name = "王豪" where id =2; 5
  11. Query OK, 0 rows affected (0.00 sec)
  12. Rows matched: 1 Changed: 0 Warnings: 0

事务②

  1. mysql> begin; 3
  2. Query OK, 0 rows affected (0.00 sec)
  3. mysql> update student set name = "王豪" where id =2; 4
  4. ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

执行顺序:1,2,3,4,5就会出现死锁。

死锁排查

MySQL提供了几个与锁有关的参数和命令,可以辅助我们优化锁操作,减少死锁发生。

查看死锁日志 通过show engine innodb status\G命令查看近期死锁日志信息。

使用方法:

1、查看近期死锁日志信息;

2、使用explain查看下SQL执行计划 查看锁状态变量

通过show status like ‘innodb_row_lock%’ 命令检查状态变量,分析系统中的行锁的争夺 情况

Innodb_row_lock_current_waits:当前正在等待锁的数量

Innodb_row_lock_time:从系统启动到现在锁定总时间 单位ms

Innodb_row_lock_time_avg: 每次等待锁的平均时间

Innodb_row_lock_time_max:从系统启动到现在等待最长的一次锁的时间

Innodb_row_lock_waits:系统启动后到现在总共等待的次数

如果等待次数高,而且每次等待时间长,需要分析系统中为什么会有如此多的等待,然后着 手定制优化。

10 集群架构设计

集群架构设计,主要遵循三个维度:

  1. 可用性:
    站点高可用,冗余站点
    服务高可用,冗余服务
    数据高可用,冗余数据,要解决数据一致性问题
    方案:

    主从模式:简单灵活,能满足多种需求,比较主流的用法,但是写操作高可用需要自行处理

双主模式:互为主从,有双主双写,双主单写两种方式,建议使用双主单写

  1. 扩展性
    如何扩展以提高读性能

    加从库:简单易操作,方案成熟。但是从库过多会引发主库性能损耗。建议不要作为长期的扩充方案,应该设法用良好的设计避免持续加从库来缓解读性能问题。

分库分表:可以分为垂直拆分和水平拆分,垂直拆分可以缓解部分压力,水平拆分理论上可以无限扩展。


如何扩展提高写性能

分库分表

  1. 一致性
    一致性主要考虑集群中各数据库数据同步以及同步延迟问题。

    不适用从库:扩展读性能问题现需要单独考虑,否则容易出现系统瓶颈

增加访问路由层:可以先得到主从同步最长时间t,在数据发生修改后的t时间内,先访问从库。

10.1 主从模式

适用场景

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

Mysql上笔记 - 图73

mysql主从复制用途:

实时灾备,用于故障切换(高可用)

读写分离,提供查询服务(读扩展)

数据备份,避免影响业务(高可用)

主从部署必要条件:

从库服务器能连通主库

主库开启binlog日志(设置log-bin参数)

主从server-id不同

实现原理

Mysql上笔记 - 图74

主从复制整体分为以下三个步骤:

  1. 主库将数据库的变更操作记录到Binlog日志文件中

  2. 从库读取主库中的Binlog日志文件信息写入到从库的Relay Log中继日志中

  3. 从库读取中继日志信息在从库中进行Replay,更新从库数据信息
    在上述三个过程中,涉及了Master的BinlogDump Thread和Slave的I/O Thread、SQL Thread,它们 的作用如下:
    Master服务器对数据库更改操作记录在Binlog中,BinlogDump Thread接到写入请求后,读取 Binlog信息推送给Slave的I/O Thread。
    Slave的I/O Thread将读取到的Binlog信息写入到本地Relay Log中。
    Slave的SQL Thread检测到Relay Log的变更请求,解析relay log中内容在从库上执行。

上述过程都是异步操作,俗称异步复制,存在数据延迟现象。

异步复制时序图:

Mysql上笔记 - 图75mysql主从复制存在的问题:

  1. 主库宕机后,数据可能丢失
  2. 从库只有一个SQL Thread,主库写压力大,复制很可能延时

解决方法:

  1. **半同步复制---解决数据丢失的问题**
  2. **并行复制----解决从库复制延迟的问题**

10.2 半同步复制

为了提升数据安全,MySQL让Master在某一个时间点等待Slave节点的 ACK(Acknowledge character)消息,接收到ACK消息后才进行事务提交,这也是半同步复制的基础,MySQL从5.5版本开 始引入了半同步复制机制来降低数据丢失的概率

介绍半同步复制之前先快速过一下 MySQL 事务写入碰到主从复制时的完整过程,主库事务写入分为 4 个步骤:

  • InnoDB Redo File Write (Prepare Write)

  • Binlog File Flush & Sync to Binlog File

  • InnoDB Redo File Commit(Commit Write)

  • Send Binlog to Slave

1,当Master不需要关注Slave是否接受到Binlog Event时,即为传统的主从复制。 这种就属于异步复制,不管从库成没成功,主库都会提交事务,会出现从库未接收到主库发送的数据,或者主库发送的数据处理时从库崩了导致的数据不一致问题。

2,当Master需要在第三步等待Slave返回ACK时,即为 after-commit,半同步复制(MySQL 5.5引入)。主库向slave发送binlog event,然后事务commit, 不依赖于从库返回的ack,然后等待从库响应ack之后,响应给客户端成功,如果没有收到从库的ack,则向客户端响应异常。会出现当从库正常接收binlog event但是响应ack的过程中异常,就会出现假响应的问题。

3,当Master需要在第二步等待 Slave 返回 ACK 时,即为 after-sync,增强半同步(MySQL 5.7引入)。主库事务的commit依赖于从库接收到binlog event数据之后响应的ack,从而解决了上面的数据一致性问题。

10.3 并行复制

MySQL的主从复制延迟一直是受开发者最为关注的问题之一,MySQL从5.6版本开始追加了并行复制功 能,目的就是为了改善复制延迟问题,并行复制称为enhanced multi-threaded slave(简称MTS)

原博客:【https://blog.csdn.net/andong154564667/article/details/82117727】

Mysql 5.6 并行复制原理

MySQL 5.6版本也支持所谓的并行复制,但是其并行只是基于库的。如果用户的MySQL数据库中是多个 库,对于从库复制的速度的确可以有比较大的帮助。

但是通常我们分库的时候,或者单体应用都是单库多表,对于这种和单线程复制没有区别。

Mysql上笔记 - 图76

Mysql 5.7 并行复制原理

MySQL 5.7是基于组提交的并行复制,MySQL 5.7才可称为真正的并行复制,这其中最为主要的原因就 是slave服务器的回放与master服务器是一致的,即master服务器上是怎么并行执行的slave上就怎样进 行并行回放。不再有库的并行复制限制。

MySQL 5.7是通过对事务进行分组,当事务提交时,它们将在单个操作中写入到二进制日志中。如果多 个事务能同时提交成功,那么它们意味着没有冲突,因此可以在Slave上并行执行,所以通过在主库上 的二进制日志中添加组提交信息。

MySQL 5.7的并行复制基于一个前提,即所有已经处于prepare阶段的事务(InnoDB事务提交采用的是两阶段提交模式。一个阶段是prepare,另一个是commit。),都是可以并行提交的。这 些当然也可以在从库中并行提交,因为处理这个阶段的事务都是没有冲突的。在一个组里提交的事务, 一定不会修改同一行。这是一种新的并行复制思路,完全摆脱了原来一直致力于为了防止冲突而做的分 发算法,等待策略等复杂的而又效率底下的工作。

为了兼容MySQL 5.6基于库的并行复制,5.7引入了新的变量slave_parallel_type,其可以配置的值有: DATABASE(默认值,基于库的并行复制方式)、LOGICAL_CLOCK(基于组提交的并行复制方式)。

那么如何知道事务是否在同一组中,生成的Binlog内容如何告诉Slave哪些事务是可以并行复制的?

在MySQL 5.7版本中,其设计方式是将组提交的信息存放在GTID中。为了避免用户没有开启GTID功能 (gtid_mode=OFF),MySQL 5.7又引入了称之为Anonymous_Gtid的二进制日志event类型 ANONYMOUS_GTID_LOG_EVENT。

通过mysqlbinlog工具分析binlog日志,就可以发现组提交的内部信息。

Mysql上笔记 - 图77

  1. #200827 17:37:02 server id 1 end_log_pos 107283 CRC32 0x7925f59b Anonymous_GTID last_committed=201 sequence_number=202 rbr_only=yes original_committed_timestamp=1598521022121961 immediate_commit_timestamp=1598521022121961 transaction_length=389

可以发现MySQL 5.7二进制日志较之原来的二进制日志内容多了last_committed和 sequence_number,last_committed表示事务提交的时候,上次事务提交的编号,如果事务具有相同 的last_committed,表示这些事务都在一组内,可以进行并行的回放。

last_committed和sequence_number代表的就是所谓的LOGICAL_CLOCK。

MySQL8.0 并行复制

[http://blog.itpub.net/31547898/viewspace-2200045/]

MySQL8.0 是基于write-set的并行复制。MySQL会有一个集合变量来存储事务修改的记录信息(主键哈 希值),所有已经提交的事务所修改的主键值经过hash后都会与那个变量的集合进行对比,来判断该行是否与其冲突,并以此来确定依赖关系,没有冲突即可并行(如果冲突就放在同一个线程执行)。这样的粒度,就到了 row级别了,此时并行的粒度更加精细,并行的速度会更快。

并行复制配置与调优

binlog_transaction_dependency_history_size:用于控制集合变量的大小

binlog_transaction_depandency_tracking:是设在主库用于.控制binlog文件中事务之间的依赖关系,即last_committed值。

commit_order即group commit,同在prepare阶段的事务,在binlog中last_committed数值一样,传到从库之后可以并行执行;
writeset,会对事务处理的行数据哈希出一个writeset值,放到一个哈希表里,如果两个事务先后提交,但是处理的行数据没有冲突,即wirteset不一样,就可以有同样的last_committed,在从库可以并行执行;
writeset_session,比writeset多了一个约束,同一个session的事务,在binlog里保留先后顺序,也就是last_committed按先后顺序递增。

  1. #binlog 记录模式
  2. binlog_format=MIXED
  3. gtid_mode=1

slave_preserve_commit_order是设在从库,控制从库并行reply时事务提交的顺序。

transaction_write_set_extraction:用于控制事务的检测算法,参数值为:OFF、 XXHASH64、MURMUR32

master_info_repository:开启MTS功能后,务必将参数master_info_repostitory设置为TABLE,这样性能可以有50%~80% 的提升。这是因为并行复制开启后对于元master.info这个文件的更新将会大幅提升,资源的竞争也会变大

slave_parallel_workers:若将slave_parallel_workers设置为0,则MySQL 5.7退化为原单线程复制,但将 slave_parallel_workers设置为1,则SQL线程功能转化为coordinator线程,但是只有1个worker 线程进行回放,也是单线程复制。然而,这两种性能却又有一些的区别,因为多了一次 coordinator线程的转发,因此slave_parallel_workers=1的性能反而比0还要差。

slave_preserve_commit_order:MySQL 5.7后的MTS可以实现更小粒度的并行复制,但需要将slave_parallel_type设置为 LOGICAL_CLOCK,但仅仅设置为LOGICAL_CLOCK也会存在问题,因为此时在slave上应用事务的顺序是无序的,和relay log中记录的事务顺序不一样,这样数据一致性是无法保证的,为了保证事务是按照relay log中记录的顺序来回放,就需要开启参数slave_preserve_commit_order。

要开启enhanced multi-threaded slave其实很简单,只需根据如下设置:

  1. slave_parallel_type=LOGICAL_CLOCK
  2. slave_parallel_workers=16
  3. slave_pending_jobs_size_max = 2147483648
  4. slave_preserve_commit_order=1
  5. master_info_repository=TABLE
  6. relay_log_info_repository=TABLE
  7. relay_log_recovery=ON

并行复制监控

在使用了MTS后,复制的监控依旧可以通过show slave status \G,但是MySQL 5.7在 performance_schema库中提供了很多元数据表,可以更详细的监控并行复制过程。

  1. mysql> use performance_schema;
  2. Database changed
  3. mysql> show tables like 'replication%';
  4. +---------------------------------------------+
  5. | Tables_in_performance_schema (replication%) |
  6. +---------------------------------------------+
  7. | replication_applier_configuration |
  8. | replication_applier_filters |
  9. | replication_applier_global_filters |
  10. | replication_applier_status |
  11. | replication_applier_status_by_coordinator |
  12. | replication_applier_status_by_worker |
  13. | replication_connection_configuration |
  14. | replication_connection_status |
  15. | replication_group_member_stats |
  16. | replication_group_members |
  17. +---------------------------------------------+
  18. 10 rows in set (0.00 sec)

通过replication_applier_status_by_worker可以看到worker进程的工作情况:如果没有开启主从复制是没有数据的

  1. mysql> select * from replication_applier_status_by_worker;
  2. +--------------+-----------+-----------+---------------+------------------------
  3. --------------------+-------------------+--------------------+------------------
  4. ----+
  5. | CHANNEL_NAME | WORKER_ID | THREAD_ID | SERVICE_STATE | LAST_SEEN_TRANSACTION
  6. | LAST_ERROR_NUMBER | LAST_ERROR_MESSAGE |
  7. LAST_ERROR_TIMESTAMP |
  8. +--------------+-----------+-----------+---------------+------------------------
  9. --------------------+-------------------+--------------------+------------------
  10. ----+
  11. | | 1 | 32 | ON | 0d8513d8-00a4-11e6-
  12. a510-f4ce46861268:96604 | 0 | | 0000-00-00
  13. 00:00:00 |
  14. | | 2 | 33 | ON | 0d8513d8-00a4-11e6-
  15. a510-f4ce46861268:97760 | 0 | | 0000-00-00
  16. 00:00:00 |
  17. +--------------+-----------+-----------+---------------+------------------------
  18. --------------------+-------------------+--------------------+------------------
  19. ----+
  20. 2 rows in set (0.00 sec)

最后,如果MySQL 5.7要使用MTS功能,建议使用新版本,最少升级到5.7.19版本,修复了很多Bug。

11 主从同步实战

11.1 主库

配置文件:

  1. [mysql]
  2. # 设置mysql客户端默认字符集
  3. default-character-set=utf8
  4. [mysqld]
  5. #设置3306端口
  6. port=3306
  7. # 设置mysql的安装目录
  8. basedir=D:\develop\mysql-8.0.19-winx64
  9. # 设置mysql数据库的数据的存放目录
  10. datadir=D:\develop\mysql-8.0.19-winx64\data
  11. # 允许最大连接数
  12. max_connections=200
  13. # 服务端使用的字符集默认为UTF8
  14. character-set-server=utf8mb4
  15. # 创建新表时将使用的默认存储引擎
  16. default-storage-engine=INNODB
  17. #关闭log-bin skip-log-bin
  18. #开启log-bin
  19. log_bin=mysql_bin
  20. #server-id
  21. server-id=1
  22. #开启异步复制
  23. sync_binlog=1
  24. #那些库不同步
  25. binlog_ignore_db=performace_schema
  26. binlog_ignore_db=sys
  27. binlog_ignore_db=infomation_schema
  28. #同步那些库
  29. binlog-do-db=la_gou
  30. #每组包含的事务数量
  31. binlog_group_commit_sync_no_delay_count=100
  32. #组提交延迟 ms
  33. binlog_group_commit_sync_delay=1000
  34. #开启semi半同步复制
  35. rpl_semi_sync_master_enabled=1
  36. #semi半同步复制超时时间
  37. rpl_semi_sync_master_timeout=1000
  38. #master 等待slave ack的个数。比如一主两从,其中一个从返回ack就可以commit了
  39. rpl_semi_sync_master_wait_for_slave_count=1
  40. #master等待时机 AFTER_COMMIT,AFTER_SYNC
  41. rpl_semi_sync_master_wait_point=AFTER_SYNC

授权:

5.7版本

  1. grant replication slave on *.* to 'root'@'%' identified by 'root';
  2. grant all privileges on *.* to 'root'@'%' identified by 'root';
  3. flush privileges;

8.0版本

Mysql8.0以前的版本可以使用grant在授权的时候隐式的创建用户,8.0以后已经不支持,所以必须先创建用户,然后再授权

  1. CREATE USER 'root'@'%' identified BY 'root';
  2. grant all privileges on *.* to 'root'@'%' with grant option;
  3. grant replication slave on *.* to 'root'@'%' with grant option;
  4. flush privileges;
  5. show master status;
  1. mysql> show master status;
  2. +--------------------+----------+--------------+-----------------------------------------+-------------------+
  3. | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
  4. +--------------------+----------+--------------+-----------------------------------------+-------------------+
  5. | mysql-login.000001 | 1107 | la_gou | performace_schema,sys,infomation_schema | |
  6. +--------------------+----------+--------------+-----------------------------------------+-------------------+
  7. 1 row in set (0.00 sec)

11.2 从库

配置文件:

  1. [mysql]
  2. # 设置mysql客户端默认字符集
  3. default-character-set=utf8
  4. [mysqld]
  5. port=3307
  6. # 允许最大连接数
  7. max_connections=100
  8. # 服务端使用的字符集默认为UTF8
  9. character-set-server=utf8mb4
  10. # 创建新表时将使用的默认存储引擎
  11. default-storage-engine=INNODB
  12. #关闭log-bin
  13. skip-log-bin
  14. # 中继日志
  15. relay_log=mysql-relay-log
  16. #只读
  17. read_only=1
  18. #semi半同步复制开启
  19. rpl_semi_sync_slave_enabled=1
  20. slave_parallel_type=LOGICAL_CLOCK
  21. slave_parallel_workers=4
  22. slave_preserve_commit_order=1
  23. slave_pending_jobs_size_max = 2147483648
  24. master_info_repository=TABLE
  25. relay_log_info_repository=TABLE
  26. relay_log_recovery=1
  27. slave_skip_errors=1

命令:

master_log_pos是通过show master status;看到的 position。

只能同步position后面的操作。

  1. change master to master_host='172.23.32.1',master_port=3306,master_user='root',master_password='root',master_log_file='mysql_bin.000001',master_log_pos=449;
  2. start slave;
  3. show slave status \G;

因为我这里slave是docker部署的。

ip不能用127.0.0.1了 需要用Default Switch的ipv4地址

  1. 以太网适配器 vEthernet (Default Switch):
  2. 连接特定的 DNS 后缀 . . . . . . . :
  3. 本地链接 IPv6 地址. . . . . . . . : fe80::b5fd:4994:df33:bab7%35
  4. IPv4 地址 . . . . . . . . . . . . : 172.23.32.1
  5. 子网掩码 . . . . . . . . . . . . : 255.255.240.0
  6. 默认网关. . . . . . . . . . . . . :

可能存在因为密码问题无法连接主库的问题:

  1. ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root';
  2. flush privileges;

如果我们要在从库导入以前的数据,我们可以使用mysqldump工具,先把主库的数据备份到文件中然后再导入到从库。

  1. mysqldump --help
  2. mysqldump --all-databases > mysql_backup_all.sql -uroot -p

然后连接从库:

  1. mysql>use dbName;
  2. mysql> source mysql_backup_all.sql;

11.3 半同步复制,并行同步

先看看是否支持动态安装插件

  1. select @@have_dynamic_loading;
  2. #查看插件
  3. show plugins;

发现没有rpl_semi_sync_master插件需要安装:

主库安装:rpl_semi_sync_master soname semisync_master

  1. mysql>install plugin rpl_semi_sync_master soname 'semisync_master.so';

windows:

  1. mysql>install plugin rpl_semi_sync_master soname 'semisync_master.dll';

然后查看一下semi_参数:

  1. mysql> show variables like '%semi%';
  2. +-------------------------------------------+------------+
  3. | Variable_name | Value |
  4. +-------------------------------------------+------------+
  5. | rpl_semi_sync_master_enabled | OFF |
  6. | rpl_semi_sync_master_timeout | 10000 |
  7. | rpl_semi_sync_master_trace_level | 32 |
  8. | rpl_semi_sync_master_wait_for_slave_count | 1 |
  9. | rpl_semi_sync_master_wait_no_slave | ON |
  10. | rpl_semi_sync_master_wait_point | AFTER_SYNC |
  11. +-------------------------------------------+------------+
  12. 6 rows in set, 1 warning (0.01 sec)

关于rpl_semi_sync_master_wait_point参数,有两个值,AFTER_COMMIT,和AFTER_SYNC

AFTER_COMMIT:

master write binlog -> slave sync binlog -> master commit -> salve ack -> master return result

  1. 会出现脏读的情况,也就是在事务提交之后,slave 确认之前,客户端A还没有获得结果返回,但是客户端B能够读取A提交的结果;
  2. 可能会出现我们上文提到的数据丢失的问题;

AFTER_SYNC:

master write binlog -> slave sync binlog -> salve ack -> master commit -> master return result

可以在配置文件进行修改,或者使用set global

  1. set global rpl_semi_sync_master_enabled=on;
  2. set global rpl_semi_sync_master_timeout=1000; #1000ms 因为10000ms 有点长

我们看到windows mysql lib/plugin下有这两个文件

Mysql上笔记 - 图78

从库安装:

  1. mysql>install plugin rpl_semi_sync_slave soname 'semisync_slave.so';

windows安装:

  1. mysql>install plugin rpl_semi_sync_slave soname 'semisync_slave.dll';
  1. mysql> show variables like '%semi%';
  2. +---------------------------------+-------+
  3. | Variable_name | Value |
  4. +---------------------------------+-------+
  5. | rpl_semi_sync_slave_enabled | OFF |
  6. | rpl_semi_sync_slave_trace_level | 32 |
  7. +---------------------------------+-------+
  8. 2 rows in set (0.01 sec)

修改参数:

  1. set global rpl_semi_sync_slave_enabled=on;
  2. stop slave;
  3. start slave;

看一下 slave_parallel_type

  1. show variables like '%slave_parallel_type%';

这里并行复制调优参考 10.3的并行复制监控

  1. slave_parallel_type=LOGICAL_CLOCK
  2. slave_parallel_workers=16
  3. slave_preserve_commit_order=1
  4. slave_pending_jobs_size_max = 2147483648
  5. master_info_repository=TABLE
  6. relay_log_info_repository=TABLE
  7. relay_log_recovery=ON

11.4 读写分离

大多数互联网业务中,往往读多写少,这时候数据库的读会首先成为数据库的瓶颈。如果我们已经优化 了SQL,但是读依旧还是瓶颈时,这时就可以选择“读写分离”架构了。

读写分离首先需要将数据库分为主从库,一个主库用于写数据,多个从库完成读数据的操作,主从库之间通过主从复制机制进行数据的同步,如图所示。

Mysql上笔记 - 图79

在应用中可以在从库追加多个索引来优化查询,主库这些索引可以不加,用于提升写效率。 视实时情况而定。

读写分离架构也能够消除读写锁冲突从而提升数据库的读写性能。使用读写分离架构需要注意:主从同步延迟和读写分配机制问题。

2.4.2 主从同步延迟

使用读写分离架构时,数据库主从同步具有延迟性,数据一致性会有影响,对于一些实时性要求比较高的操作,可以采用以下解决方案。

写后立刻读 在写入数据库后,某个时间段内读操作就去主库,之后读操作访问从库。

二次查询 先去从库读取数据,找不到时就去主库进行数据读取。该操作容易将读压力返还给主库,为了避免 恶意攻击,建议对数据库访问API操作进行封装,有利于安全和低耦合。

根据业务特殊处理 根据业务特点和重要程度进行调整,比如重要的,实时性要求高的业务数据读写可以放在主库。对于次要的业务,实时性要求不高可以进行读写分离,查询时去从库查询。

11.5 读写分离实现方案

读写路由分配机制是实现读写分离架构最关键的一个环节,就是控制何时去主库写,何时去从库读。目 前较为常见的实现方案分为以下两种:

基于编程和配置实现(应用端):

  • 程序员在代码中封装数据库的操作,代码中可以根据操作类型进行路由分配,增删改时操作主库, 查询时操作从库。这类方法也是目前生产环境下应用最广泛的。优点是实现简单,因为程序在代码 中实现,不需要增加额外的硬件开支,缺点是需要开发人员来实现,运维人员无从下手,如果其中 一个数据库宕机了,就需要修改配置重启项目。

基于服务器端代理实现(服务器端):

Mysql上笔记 - 图80

中间件代理一般介于应用服务器和数据库服务器之间,从图中可以看到,应用服务器并不直接进入 到master数据库或者slave数据库,而是进入MySQL proxy代理服务器。代理服务器接收到应用服 务器的请求后,先进行判断然后转发到后端master和slave数据库。

目前有很多性能不错的数据库中间件,常用的有MySQL Proxy、MyCat以及Shardingsphere等等。

  • MySQL Proxy:是官方提供的MySQL中间件产品可以实现负载平衡、读写分离等。

  • MyCat:MyCat是一款基于阿里开源产品Cobar而研发的,基于 Java 语言编写的开源数据库中间 件。

  • ShardingSphere:ShardingSphere是一套开源的分布式数据库中间件解决方案,它由ShardingJDBC、Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成。已经在2020 年4月16日从Apache孵化器毕业,成为Apache顶级项目。

  • Atlas:Atlas是由 Qihoo 360公司Web平台部基础架构团队开发维护的一个数据库中间件。

  • Amoeba:变形虫,该开源框架于2008年开始发布一款 Amoeba for MySQL软件。

11.6 读写分离实现

linux:

下载mysql-proxy【https://downloads.mysql.com/archives/proxy/】wget下载

解压 tar -xzvf

windows:

下载mysql-proxy【https://downloads.mysql.com/archives/proxy/】

创建配置文件mysql-proxy.cnf linux可以放在etc路径下

一定要加[mysql-proxy]

  1. [mysql-proxy]
  2. #运行当前代理服务的用户 linux环境指定
  3. #user=root
  4. #主从mysql服务器的账号密码 主从mysql共有的用户
  5. admin-username=root
  6. admin-password=root
  7. #代理服务ip:port
  8. proxy-address=127.0.0.1:4040
  9. #主库ip
  10. proxy-backend-addresses=127.0.0.1:3306
  11. #从库ip port
  12. proxy-read-only-backend-addresses=127.0.0.1:3307
  13. #使用脚本 读写分离脚本 windows将\改为/不然会保存
  14. proxy-lua-script=D:/develop/mysql-proxy/proxy-1/share/doc/mysql-proxy/rw-splitting.lua
  15. #日志
  16. log-file=mysql-proxy.log
  17. #日志级别由高到低分别有(error|warning|info|message|debug)
  18. log-level=debug
  19. #后台运行
  20. daemon=true
  21. #遇到问题尝试重启
  22. keepalive=true

proxy-backend-addresses,proxy-read-only-backend-addresses可以设置多个backend用逗号分开

  1. #主库ip
  2. proxy-backend-addresses=127.0.0.1:3306,127.0.0.1:3307
  3. #从库ip port
  4. proxy-read-only-backend-addresses=127.0.0.1:3310,127.0.0.1:3311

linux 指定权限可读可写 chmod 660 /etc/mysql-proxy.cnf

修改rw-splitting.lua最新小连接数,多少个连接就采用读写分离

min_idle_connections = 1,默认为4

Mysql上笔记 - 图81

bin目录下执行文件

linux

  1. ./mysql-proxy --defaults-file=/etc/mysql-proxy.cnf

windows

  1. mysql-proxy --defaults-file=D:\develop\mysql-proxy\proxy-1\mysql-proxy.cnf

使用mysql-front一定要把

密码加密方式改了 看是通过什么方式连接的

  1. ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';
  2. 或者
  3. ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root';

然后我们断开slave

  1. stop slave;

然后使用proxy插入一条数据

主库会插入,从库不会插入。

读取数据 会从 主库/从库读取

12 双主模式

很多企业刚开始都是使用MySQL主从模式,一主多从、读写分离等。但是单主如果发生单点故障,从库切换成主库还需要作改动。因此,如果是双主或者多主,就会增加MySQL入口,提升了主库的可用性。 因此随着业务的发展,数据库架构可以由主从模式演变为双主模式。双主模式是指两台服务器互为主从,任何一台服务器数据变更,都会通过复制应用到另外一方的数据库中。每一台服务器都有两个角色。

Mysql上笔记 - 图82

使用双主双写还是双主单写?

建议大家使用双主单写,因为双主双写存在以下问题:

  • ID冲突
    在A主库写入,当A数据未同步到B主库时,对B主库写入,如果采用自动递增容易发生ID主键的冲 突。 可以采用MySQL自身的自动增长步长来解决,例如A的主键为1,3,5,7…,B的主键为2,4,6,8… ,但 是对数据库运维、扩展都不友好。 ``` show variables like ‘%increment%’;

改变 auto_increment_increment=2;参数 每次步长为多少 auto_increment_offset=1; auto_increment_offset=2;

  1. -
  2. 更新丢失
  3. <br />同一条记录在两个主库中进行更新,会发生前面覆盖后面的更新丢失。
  4. 高可用架构如下图所示,其中一个Master提供线上服务,另一个Master作为备胎供高可用切换, Master下游挂载Slave承担读请求。
  5. ![](assets/image-20200902075203219.png#alt=image-20200902075203219)
  6. 随着业务发展,**架构会从主从模式演变为双主模式,建议用双主单写,再引入高可用组件**,**例如 KeepalivedMMM等工具,实现主库故障自动切换。**
  7. <a name="b2663133"></a>
  8. ## 11.1 主库配置
  9. 双主的关键在于 **互为主从**,就是把上面主从的配置在两个mysql实列合到一起。
  10. 两个实例都用下面的配置就可以,改一下端口号,然后change master to 就可以。
  11. 然后要注意,最开始的时候应该注释掉semi的相关配置启动,然后进入mysql安装semimasterslave插件。
  12. 然后释放配置信息重启。
  13. ```properties
  14. [mysql]
  15. # 设置mysql客户端默认字符集
  16. default-character-set=utf8
  17. [mysqld]
  18. #设置3307端口
  19. port=3307
  20. # 允许最大连接数
  21. max_connections=200
  22. # 服务端使用的字符集默认为UTF8
  23. character-set-server=utf8mb4
  24. # 创建新表时将使用的默认存储引擎
  25. default-storage-engine=INNODB
  26. #关闭log-bin skip-log-bin
  27. #开启log-bin
  28. log_bin=mysql_bin
  29. #server-id
  30. server-id=5
  31. #开启异步复制
  32. sync_binlog=1
  33. #那些库不同步
  34. binlog_ignore_db=performace_schema
  35. binlog_ignore_db=sys
  36. binlog_ignore_db=infomation_schema
  37. #同步那些库
  38. #binlog-do-db=la_gou
  39. #每组包含的事务数量
  40. binlog_group_commit_sync_no_delay_count=100
  41. #组提交延迟 ms
  42. binlog_group_commit_sync_delay=1000
  43. #开启semi半同步复制
  44. rpl_semi_sync_master_enabled=1
  45. #semi半同步复制超时时间
  46. rpl_semi_sync_master_timeout=1000
  47. rpl_semi_sync_slave_enabled=1
  48. #在一主的配置基础上添加
  49. relay_log=master1_relay_log
  50. #A -> B -> C
  51. #也就是说,A为从服务器B的主服务器,B为从服务器C的主服务器。为了能工作,B必须既为主服务器又为从服务器。你必#须用--logs-bin启动A和B以启用二进制日志,并且用--logs-slave-updates选项启动B。
  52. log_slave_updates=1
  53. #从库并行复制类型 DATABASE基于库的并行复制 LOGICAL_CLOCK基于组提交
  54. slave_parallel_type=LOGICAL_CLOCK
  55. #并行复制线程数
  56. slave_parallel_workers=4
  57. #为了保证事务是按照relay log中记录的顺序来回放
  58. slave_preserve_commit_order=1
  59. #在多线程复制时,在队列中Pending的事件所占用的最大内存,默认为16M,如果内存富余,或者延迟较大时,
  60. #可以适当调大;注意这个值要比主库的max_allowed_packet大
  61. slave_pending_jobs_size_max = 2147483648
  62. #下面两个选项设置为table可以提升性能
  63. #这是因为并行复制开启后对于元master.info这个文件的更新将会大幅提升,资源的竞争也会变大
  64. master_info_repository=TABLE
  65. #relay.info明文存储不安全,把relay.info中的信息记录在table中相对安全。
  66. #可以避免relay.info更新不及时,SLAVE 重启后导致的主从复制出错。
  67. relay_log_info_repository=TABLE
  68. #开启relay_log覆盖
  69. relay_log_recovery=1
  70. #从库复制过程中跳过异常数
  71. slave_skip_errors=1

我这里用docker 启动一个容器

  1. docker run --name mysql-master1 -v E:/learn/docker/mysql/master-1/conf:/etc/mysql/conf.d -v E:/learn/docker/mysql/master-1/logs:/logs -v E:/learn/docker/mysql/master-1/data:/var/lib/mysql -p 3307:3307 -e MYSQL_ROOT_PASSWORD=root -d mysql:8.0.21

11.2 MMM架构

MMM(Master-Master Replication Manager for MySQL)是一套用来管理和监控双主复制,支持双主故障切换 的第三方软件。MMM 使用Perl语言开发,虽然是双主架构,但是业务上同一时间只允许一 个节点进行写入操作。下图是基于MMM实现的双主高可用架构。

Mysql上笔记 - 图83

MMM故障处理机制 MMM 包含writer和reader两类角色,分别对应写节点和读节点。

  • 当 writer节点出现故障,程序会自动移除该节点上的VIP

  • 写操作切换到 Master2,并将Master2设置为writer

  • 将所有Slave节点会指向Master2

除了管理双主节点,MMM 也会管理 Slave 节点,在出现宕机、复制延迟或复制错误,MMM 会移除该节点的 VIP,直到节点恢复正常。

MMM监控机制 MMM 包含monitor和agent两类程序,功能如下:

monitor:监控集群内数据库的状态,在出现异常时发布切换命令,一般和数据库分开部署。

agent:运行在每个 MySQL 服务器上的代理进程,monitor 命令的执行者,完成监控的探针工作和具体服务设置,例如设置 VIP(虚拟IP)、指向新同步节点。

11.3 MHA架构

MHA(Master High Availability)是一套比较成熟的 MySQL 高可用方案,也是一款优秀的故障切换和 主从提升的高可用软件。在MySQL故障切换过程中,MHA能做到在30秒之内自动完成数据库的故障切换操作,并且在进行故障切换的过程中,MHA能在最大程度上保证数据的一致性,以达到真正意义上的 高可用。MHA还支持在线快速将Master切换到其他主机,通常只需0.5-2秒。

目前MHA主要支持一主多从的架构,要搭建MHA,要求一个复制集群中必须最少有三台数据库服务 器。

Mysql上笔记 - 图84

MHA由两部分组成:MHA Manager(管理节点)和MHA Node(数据节点)。

  • MHA Manager可以单独部署在一台独立的机器上管理多个master-slave集群,也可以部署在一台 slave节点上。负责检测master是否宕机、控制故障转移、检查MySQL复制状况等。

  • MHA Node运行在每台MySQL服务器上,不管是Master角色,还是Slave角色,都称为Node,是被监控管理的对象节点,负责保存和复制master的二进制日志、识别差异的中继日志事件并将其差异的事件应用于其他的slave、清除中继日志。

MHA Manager会定时探测集群中的master节点,当master出现故障时,它可以自动将最新数据的 slave提升为新的master,然后将所有其他的slave重新指向新的master,整个故障转移过程对应用程序完全透明。

MHA故障处理机制:

  • 把宕机master的binlog保存下来 这个怎么实现的?直接服务挂了怎么办。

  • 根据binlog位置点找到最新的slave

    Master发生故障的时候,有可能一部分(或者全部)的Slave未能获取到最新的binlog,造成Slave之间的binlog转发发生偏差。

  • 用最新slave的relay log修复其它slave

  • 将保存下来的binlog在最新的slave上恢复

  • 将最新的slave提升为master

  • 将其它slave重新指向新提升的master,并开启主从复制

MHA优点:

  • 自动故障转移快
  • 主库崩溃不存在数据一致性问题
  • 性能优秀,支持半同步复制和异步复制,当监控mysql状态时,仅需要每隔N秒向master发送ping包(默认3秒),所以对性能无影响。你可以理解为MHA的性能和简单的主从复制框架性能一样。
  • 一个Manager监控节点可以监控多个集群

11.4 主备切换

主备切换是指将备库变为主库,主库变为备库,有可靠性优先和可用性优先两种策略。

  • 主备延迟问题
    主备延迟是由主从数据同步延迟导致的,与数据同步有关的时间点主要包括以下三个:

    • 主库 A 执行完成一个事务,写入 binlog,我们把这个时刻记为 T1;
    • 之后将binlog传给备库 B,我们把备库 B 接收完 binlog 的时刻记为 T2;
    • 备库 B 执行完成这个binlog复制,我们把这个时刻记为 T3。
  • 备库机器性能问题 机器性能差,甚至一台机器充当多个主库的备库。
  • 分工问题 备库提供了读操作,或者执行一些后台分析处理的操作,消耗大量的CPU资源。
  • 大事务操作 大事务耗费的时间比较长,导致主备复制时间长。比如一些大量数据的delete或大表DDL操 作都可能会引发大事务。

    • 可靠性优先
      主备切换过程一般由专门的HA高可用组件完成,但是切换过程中会存在短时间不可用,因为在切 换过程中某一时刻主库A和从库B都处于只读状态。如下图所示:
      Mysql上笔记 - 图85
      主库由A切换到B,切换的具体流程如下:
  • 判断从库B的Seconds_Behind_Master值,当小于某个值才继续下一步

  • 把主库A改为只读状态(readonly=true)
  • 等待从库B的Seconds_Behind_Master值降为 0
  • 把从库B改为可读写状态(readonly=false)
  • 把业务请求切换至从库B

    • 可用性优先
      不等主从同步完成, 直接把业务请求切换至从库B ,并且让 从库B可读写 ,这样几乎不存在不可 用时间,但可能会数据不一致。
      Mysql上笔记 - 图86
      如上图所示,在A切换到B过程中,执行两个INSERT操作,过程如下:
  • 主库A执行完 INSERT c=4 ,得到 (4,4) ,然后开始执行主从切换

  • 主从之间有5S的同步延迟,从库B会先执行 INSERT c=5 ,得到 (4,5)
  • 从库B执行主库A传过来的binlog日志 INSERT c=4 ,得到 (5,4)
  • 主库A执行从库B传过来的binlog日志 INSERT c=5 ,得到 (5,5)
  • 此时主库A和从库B会有两行不一致的数据

通过上面介绍了解到,主备切换采用可用性优先策略,由于可能会导致数据不一致,所以大多数情 况下,优先选择可靠性优先策略。在满足数据可靠性的前提下,MySQL的可用性依赖于同步延时 的大小,同步延时越小,可用性就越高。

13 分库分表

mysq 单表可以存储10亿级数据,不过这时候性能就比较差了。单表尽量控制在1000万以下。

互联网系统需要处理大量用户的请求。比如微信日活用户破10亿,海量的用户每天产生海量的数量;美 团外卖,每天都是几千万的订单,那这些系统的用户表、订单表、交易流水表等是如何处理呢?

数据量只增不减,历史数据又必须要留存,非常容易成为性能的瓶颈,而要解决这样的数据库瓶颈问 题,“读写分离”和缓存往往都不合适,目前比较普遍的方案就是使用NoSQL/NewSQL或者采用分库分 表。

使用分库分表时,主要有垂直拆分和水平拆分两种拆分模式,都属于物理空间的拆分。

分库分表方案:只分库、只分表、分库又分表。

垂直拆分:由于表数量多导致的单个库大。将表拆分到多个库中。 如果一个表字段太多,也可以一个表拆为一个主表,和扩展表。

水平拆分:由于表记录多导致的单个库大。将表记录拆分到多个表中。

13.1 垂直拆分

垂直拆分又称为纵向拆分,垂直拆分是将表按库进行分离,或者修改表结构按照访问的差异将某些列拆分出去。应用时有垂直分库和垂直分表两种方式,一般谈到的垂直拆分主要指的是垂直分库。

如下图所示,采用垂直分库,将用户表和订单表拆分到不同的数据库中。

Mysql上笔记 - 图87

垂直分表就是将一张表中不常用的字段拆分到另一张表中,从而保证第一张表中的字段较少,避免出现数据库跨页存储的问题,从而提升查询效率。

一个表中字段过多,还有有些字段经常使用,有些字段不经常使用,或者还有text等字段信 息。可以考虑使用垂直分表方案。

垂直分表拆分优点:

  • 拆分优点: 拆分后业务清晰,拆分规则明确;
  • 易于数据的维护和扩展;
  • 可以使得行数据变小,一个数据页(page) 就能存放更多的数据,在查询时就会减少 I/O 次 数;
  • 可以达到最大化利用 Cache 的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起;
  • 便于实现冷热分离的数据表设计模式。

垂直分表拆分优点:

  • 主键出现冗余,需要管理冗余列;
  • 会引起表连接 JOIN 操作,可以通过在业务服务器上进行 join 来减少数据库压力,提高了系 统的复杂度;
  • 依然存在单表数据量过大的问题;
  • 事务处理复杂。

13.2 水平拆分

水平拆分又称为横向拆分。 相对于垂直拆分,它不再将数据根据业务逻辑分类,而是通过某个字 段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个表仅包含数据的一部分,如 下图所示

Mysql上笔记 - 图88

水平分表是将一张含有很多记录数的表水平切分,不同的记录可以分开保存,拆分成几张结构相同 的表。如果一张表中的记录数过多,那么会对数据库的读写性能产生较大的影响,虽然此时仍然能 够正确地读写,但读写的速度已经到了业务无法忍受的地步,此时就需要使用水平分表来解决这个 问题。

水平拆分:解决表中记录过多问题。

垂直拆分:解决表过多或者是表字段过多问题。

水平拆分重点考虑拆分规则:例如范围、时间、或hash算法。

水平拆分优点:

  • 拆分规则设计好,join 操作基本可以数据库做;
  • 不存在单库大数据,高并发的性能瓶颈;
  • 切分的表的结构相同,应用层改造较少,只需要增加路由规则即可;
  • 提高了系统的稳定性和负载能力。

水平拆分缺点:

拆分规则难以抽象;

跨库Join性能较差;

分片事务的一致性难以解决;

数据扩容的难度和维护量极大

我们通常会同时使用两种拆分方式,垂直拆分更偏向于产品/业务/功能拆分的过程,在技 术上我们更关注水平拆分的方案。

13.3 主键生成策略

UUID :

UUID是通用唯一识别码(Universally Unique Identifier)的缩写,长度是16个字节,被表示为 32个十六进制数字,以“ - ”分隔的五组来显示,格式为8-4-4-4-12,共36个字符,例如: 550e8400-e29b-41d4-a716-446655440000。UUID在生成时使用到了以太网卡地址、纳秒级时 间、芯片ID码和随机数等信息,目的是让分布式系统中的所有元素都能有唯一的识别信息。 使用UUID做主键,可以在本地生成,没有网络消耗,所以生成性能高。

但是UUID比较长,没有规律性,耗费存储空间。

除聚集索引以外的所有索引都称为辅助索引。在InnoDB中,二级索引中的每条记录都包含行的主 键列,以及为二级索引指定的列。InnoDB使用这个主键值来搜索聚集索引中的行。如果主键是长 的,则次索引使用更多的空间,因此主键短是有利的。

如果UUID作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,影响性能。

COMB(UUID变种):

COMB(combine)型是数据库特有的一种设计思想,可以理解为一种改进的GUID,它通过组合 GUID和系统时间,以使其在索引和检索事有更优的性能。数据库中没有COMB类型,它是Jimmy Nilsson在他的“The Cost of GUIDs as Primary Keys”一文中设计出来的。 COMB设计思路是这样的:既然UniqueIdentifier数据因毫无规律可言造成索引效率低下,影响了 系统的性能,那么我们能不能通过组合的方式,保留UniqueIdentifier的前10个字节,用后6个字 节表示GUID生成的时间(DateTime),这样我们将时间信息与UniqueIdentifier组合起来,在保 留UniqueIdentifier的唯一性的同时增加了有序性,以此来提高索引效率。解决UUID无序的问 题,性能优于UUID。但还是太长了,占用字节数大。

SNOWFLAKE:

有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成,SnowFlake解决 了这种需求。SnowFlake是Twitter开源的分布式ID生成算法,结果是一个long型的ID,long型是8 个字节,64-bit。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中 心,5个bit的机器ID),12bit作为毫秒内的流水号,最后还有一个符号位,永远是0。

SnowFlake生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID重复,并且效率 较高。经测试SnowFlake每秒能够产生26万个ID。缺点是强依赖机器时钟,如果多台机器环境时 钟没同步,或时钟回拨,会导致发号重复或者服务会处于不可用状态。因此一些互联网公司也基于上述的方案做了封装,例如百度的uidgenerator(基于SnowFlake)和美团的leaf(基于数据库和 SnowFlake)等。

数据库ID表:

比如A表分表为A1表和A2表,我们可以单独的创建一个MySQL数据库,在这个数据库中创建一张 表,这张表的ID设置为自动递增,其他地方需要全局唯一ID的时候,就先向这个这张表中模拟插 入一条记录,此时ID就会自动递增,然后我们获取刚生成的ID后再进行A1和A2表的插入。 例如,下面DISTRIBUTE_ID就是我们创建要负责ID生成的表,结构如下:

  1. CREATE TABLE DISTRIBUTE_ID (
  2. id bigint(32) NOT NULL AUTO_INCREMENT COMMENT '主键',
  3. createtime datetime DEFAULT NULL,
  4. PRIMARY KEY (id)
  5. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这里的createtime字段无实际意义,是为了随便插入一条数据以至于能够自动递增ID。

使用独立的MySQL实例生成分布式ID,虽然可行,但是性能和可靠性都不够好,因为你需要 代 码连接到数据库才能获取到ID,性能无法保障,另外mysql数据库实例挂掉了,那么就无法获取分布式ID了

Redis生成ID:

当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于 Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来 实现。

也可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis。可以初始化每台Redis 的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为:

  1. A1,6,11,16,21
  2. B2,7,12,17,22
  3. C3,8,13,18,23
  4. D4,9,14,19,24
  5. E5,10,15,20,25

但是如果redis服务的实例发生变化,就需要再去维护步长了。。

13.4 分片策略

分片(Sharding)就是用来确定数据在多台存储设备上分布的技术。Shard这个词的意思是“碎片”,如 果将一个数据库当作一块大玻璃,将这块玻璃打碎,那么每一小块都称为数据库的碎片(Database Sharding)。将一个数据库打碎成多个的过程就叫做分片,分片是属于横向扩展方案。

分片:表示分配过程,是一个逻辑上概念,表示如何实现

分库分表:表示分配结果,是一个物理上概念,表示最终实现的结果

数据库扩展方案:

横向扩展:一个库变多个库,加机器数量

纵向扩展:一个库还是一个库,优化机器性能,加高配CPU或内存

在分布式存储系统中,数据需要分散存储在多台设备上,分片就是把数据库横向扩展到多个数据库服务器上的一种有效的方式,其主要目的就是为突破单节点数据库服务器的 I/O 能力限制,解决数据库扩展 性问题。

方案:

数据分片是根据指定的分片键和分片策略将数据水平拆分,拆分成多个数据片后分散到多个数据存储节 点中。分片键是用于划分和定位表的字段,一般使用ID或者时间字段。而分片策略是指分片的规则,常 用规则有以下几种。

基于范围分片

根据特定字段的范围进行拆分,比如用户ID、订单时间、产品价格等。例如: {[1 - 100] => Cluster A, [101 - 199] => Cluster B}

  • 优点:新的数据可以落在新的存储节点上,如果集群扩容,数据无需迁移。
  • 缺点:数据热点分布不均,数据冷热不均匀,导致节点负荷不均。

哈希取模分片

整型的Key可直接对设备数量取模,其他类型的字段可以先计算Key的哈希值,然后再对设备数量取模。假设有n台设备,编号为0 ~ n-1,通过Hash(Key) % n就可以确定数据所在的设备编号。该模式也称为离散分片。

13.5 扩容方案

当系统用户进入了高速增长期时,即便是对数据进行分库分表,但数据库的容量,还有表的数据量也总 会达到天花板。当现有数据库达到承受极限时,就需要增加新服务器节点数量进行横向扩容。

首先来思考一下,横向扩展会有什么技术难度?

Mysql上笔记 - 图89

  • 数据迁移问题

  • 分片规则改变

  • 数据同步、时间点、数据一致性

停机扩容

这是一种很多人初期都会使用的方案,尤其是初期只有几台数据库的时候。、

  • 站点发布一个公告,例如:“为了为广大用户提供更好的服务,本站点将在今晚00:00-2:00之间升 级,给您带来不便抱歉”;

  • 时间到了,停止所有对外服务;

  • 新增n个数据库,然后写一个数据迁移程序,将原有x个库的数据导入到最新的y个库中。比如分片规则由%x变为%y;

  • 数据迁移完成,修改数据库服务配置,原来x个库的配置升级为y个库的配置

  • 重启服务,连接新库重新对外提供服务

如果失败,就需要回滚配置和数据。

优点:简单

缺点:

  • 停止服务,缺乏高可用;
  • 开发人员压力大,需要指定时间完成
  • 如果有问题没有及时测出来启动了服务,运行后发现问题,数据丢失一部分,难以回滚

适用场景:

  • 小型网站
  • 大部分游戏
  • 对高可用要求不高的服务

平滑扩容

数据库扩容的过程中,如果想要持续对外提供服务,保证服务的可用性,平滑扩容方案是最好的选择。 平滑扩容就是将数据库数量扩容成原来的2倍,比如:由2个数据库扩容到4个数据库,具体步骤如下

新增2个数据库 配置双主进行数据同步(先测试、后上线)

数据库重启都是秒级的

后面添加的数据库实例可以将master_log_file和master_log_pos置为主库最初数据的位置,可以将以前的数据备份过来。

Mysql上笔记 - 图90

数据同步完成之后,配置双主双写(同步因为有延迟,如果时时刻刻都有写和更新操作,会存在不准确问题)

Mysql上笔记 - 图91

数据同步完成后,删除双主同步,修改数据库配置,并重启:

Mysql上笔记 - 图92

此时已经扩容完成,但此时的数据并没有减少,新增的数据库跟旧的数据库一样多的数据,此时还 需要写一个程序,清空数据库中多余的数据,如:

User1去除 uid % 4 = 2的数据;

User3去除 uid % 4 = 0的数据;

User2去除 uid % 4 = 3的数据;

User4去除 uid % 4 = 1的数据;

平滑扩容方案能够实现n库扩2n库的平滑扩容,增加数据库服务能力,降低单库一半的数据量。其核心 原理是:成倍扩容,避免数据迁移。

优点:

  • 扩容期间,服务正常进行,保证高可用
  • 相对停机扩容,时间长,项目组压力没那么大,出错率低
  • 扩容期间遇到问题,随时解决,不怕影响线上服务
  • 可以将每个数据库数据量减少一半

缺点:

  • 程序复杂、配置双主同步、双主双写、检测数据同步等
  • 后期数据库扩容,比如成千上万,代价比较高

适用场景:

  • 大型网站

  • 对高可用要求高的服务

user1 1,3,5,7,9,11 %2 = 1

1,5,9 %4 =1

3,7,11 %4 =3

user2 2,4,6,8,10,12 %2=0

2,6,10 %4=2

4,8,12 %4=0

14 MHA配置

使用docker来搭建mha环境。

14.1 第一步 master环境配置

mysql:8.0.21

mha:0.58 .deb

ubuntu:Ubuntu 20.04.1 LTS

拉取mysql 镜像 docker pull mysql:8.0.21

首先创建三个目录 master-1 slave-1 slave-2 然后每个目录下创建conf,data,logs用来做docker容器的目录挂载

conf 目录下创建my.cnf,并配置

首先搭建主库环境,然后搭建完成后将容器打包,然后再导入为docker镜像,其他俩从库就利用这个镜像再启动容器

主库配置文件内容:

  1. [mysql]
  2. # 设置mysql客户端默认字符集
  3. default-character-set=utf8
  4. [mysqld]
  5. #设置3307端口
  6. port=3307
  7. # 允许最大连接数
  8. max_connections=200
  9. # 服务端使用的字符集默认为UTF8
  10. character-set-server=utf8mb4
  11. # 创建新表时将使用的默认存储引擎
  12. default-storage-engine=INNODB
  13. #关闭log-bin skip-log-bin
  14. #开启log-bin
  15. log_bin=mysql_bin
  16. #server-id
  17. server-id=5
  18. #开启异步复制
  19. sync_binlog=1
  20. #那些库不同步
  21. binlog_ignore_db=performace_schema
  22. binlog_ignore_db=sys
  23. binlog_ignore_db=infomation_schema
  24. #同步那些库
  25. #binlog-do-db=la_gou
  26. #每组包含的事务数量
  27. binlog_group_commit_sync_no_delay_count=100
  28. #组提交延迟 ms
  29. binlog_group_commit_sync_delay=1000
  30. #开启semi半同步复制
  31. rpl_semi_sync_master_enabled=1
  32. #semi半同步复制超时时间
  33. rpl_semi_sync_master_timeout=1000
  34. #master 等待slave ack的个数。比如一主两从,其中一个从返回ack就可以commit了
  35. rpl_semi_sync_master_wait_for_slave_count=1
  36. #master等待时机 AFTER_COMMIT,AFTER_SYNC
  37. rpl_semi_sync_master_wait_point=AFTER_SYNC

注意:关于semi的配置最开始的时候应该先注释,等容器启动起来,安装semi半同步复制之后,再释放这两个注释,然后再重启容器。

docker run

  1. docker run --name mysql-master1 -v E:/learn/docker/mysql/master-1/conf:/etc/mysql/conf.d -v E:/learn/docker/mysql/master-1/logs:/logs -v E:/learn/docker/mysql/master-1/data:/var/lib/mysql -p 3307:3307 -p 1222:22 -e MYSQL_ROOT_PASSWORD=root -d mysql:8.0.21 /entrypoint.sh mysqld

然后进入容器

  1. docker exec -it mysql-master1 bash

到容器里

然后 就开始安装各种环境所需软件了

  1. apt-get update
  2. apt-get install vim
  3. apt-get install openssh-server
  4. apt-get install wget
  5. apt-get install net-tools

然后mha node环境 下载地址 【https://github.com/yoshinorim/mha4mysql-node/releases/tag/v0.58】

在usr/local/mha目录下

  1. #mha node依赖包
  2. wget https://github.com/yoshinorim/mha4mysql-node/releases/download/v0.58/mha4mysql-node_0.58-0_all.deb

安装mha依赖包

  1. apt-get install libdbi-perl 或者 libdbd-mysql-perl
  2. dpkg -i mha4mysql-node_0.58-0_all.deb

安装完成后可以dpkg -l|grep mha查看是否安装成功

  1. root@678810adc09b:/usr/local/mha# dpkg -l|grep mha
  2. ii mha4mysql-node 0.58-0 all Master High Availability Manager and Tools for MySQL, Node Package

修改/etc/ssh/sshd_config文件

注意:后面的从库都使用这个配置,所有的从库容器都使用同一个端口,因为mha manager 通过ssh访问的都是同一个端口,如果从库不一样就没法连接

  1. PermitRootLogin yes
  2. Port 22

然后 启动ssh 服务

  1. service ssh start

进入mysql,修改mysql密码加密方式,不然mha 连接的时候没法访问,后面的从库也都需要执行

  1. mysql -u root -P 3307 -p
  2. ALTER USER 'root'@'%' IDENTIFIED BY 'root' PASSWORD EXPIRE NEVER; #修改加密规则
  3. ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root'; #更新一下用户的密码
  4. FLUSH PRIVILEGES; #刷新权限

安装semi插件

  1. install plugin rpl_semi_sync_master soname 'semisync_master.so';

然后退出mysql,修改配置文件,重启容器

将容器打包成tar文件,供后面两个从库使用

  1. docker export mysql-master1 > mysql_pack.tar

14.2 第二步 mha-manager配置

到这里之后我们 先暂停mha-node环境,来部署mha-manager

我这里也用的是docker容器来部署,首先拉一个ubuntu的镜像。

然后run

进入到容器内部

  1. docker exec -it ubuntu_test bash

首先我们看一下 root 目录下有没有.ssh目录 cd ~/.ssh,没有的话我们需要创建 密钥

  1. ssh-keygen -t rsa

然后将 密钥copy到 上一步的master1实例上

我们可以获取docker 内部ip进行ssh连接 172.17.0.2

  1. root@8eb935e8976f:/# ifconfig
  2. eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
  3. inet 172.17.0.2 netmask 255.255.0.0 broadcast 0.0.0.0
  4. inet6 fe80::42:acff:fe11:2 prefixlen 64 scopeid 0x20<link>
  5. ether 02:42:ac:11:00:02 txqueuelen 0 (Ethernet)
  6. RX packets 144413 bytes 12662455 (12.0 MiB)
  7. RX errors 0 dropped 0 overruns 0 frame 0
  8. TX packets 188692 bytes 22446088 (21.4 MiB)
  9. TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
  10. lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
  11. inet 127.0.0.1 netmask 255.0.0.0
  12. inet6 ::1 prefixlen 128 scopeid 0x10<host>
  13. loop txqueuelen 1 (Local Loopback)
  14. RX packets 0 bytes 0 (0.0 B)
  15. RX errors 0 dropped 0 overruns 0 frame 0
  16. TX packets 0 bytes 0 (0.0 B)
  17. TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
  1. ssh-copy-id -i .ssh/id_rsa.pub root@172.17.0.2

在/user/local/mha目录下

  1. wget https://github.com/yoshinorim/mha4mysql-manager/releases/download/v0.58/mha4mysql-manager_0.58-0_all.deb

按装mha依赖

  1. apt-get install libdbi-perl 或者 libdbd-mysql-perl
  2. dpkg -i mha4mysql-manager_0.58-0_all.deb

在/etc/mha目录下创建app1.cnf并配置

manager_log=/var/log/mha/app1.log
manager_workdir=/var/log/mha/app1

需要提前创建

这里我就先指定后面两个从库的ip配置

  1. [server default]
  2. manager_log=/var/log/mha/app1.log
  3. manager_workdir=/var/log/mha/app1
  4. master_binlog_dir=/var/lib/mysql
  5. user=root
  6. password=root
  7. #设置监控主库,发送ping包的时间间隔,尝试三次没有回应的时候自动进行failover
  8. #2s
  9. ping_interval=2
  10. #复制用户
  11. repl_user=root
  12. #复制用户密码
  13. repl_password=root
  14. #ssh用户
  15. ssh_user=root
  16. #ssh端口
  17. ssh_port=22
  18. [server1]
  19. #主库
  20. hostname=172.17.0.3
  21. port=3307
  22. [server2]
  23. #从库
  24. hostname=172.17.0.4
  25. port=3309
  26. [server3]
  27. #从库
  28. hostname=172.17.0.5
  29. port=3310

因为我们还没有创建两个从库,所以还没发进行check

14.3 第三步 slave配置

创建slave1,slave2两个mysq 从库容器

首先我们在

slave-1/conf和slave-2/conf目录下创建连个my.cnf的配置文件

两个配置文件差不多,我们只需要修改server-id不同就可以了

注意

1,我们首先需要将rpl_semi_sync_slave_enabled=1给注释,等容器启动之后,连接mysql安装semi插件

之后再开启然后重启容器。

2,binlog_ignore_db必须和主库一样,不然执行masterha_check_repl 的时候会报错

3,从库也需要开启bin_log,因为如果主库挂了,从库升为主库的时候,需要bin_log

  1. [mysql]
  2. # 设置mysql客户端默认字符集
  3. default-character-set=utf8
  4. [mysqld]
  5. #设置3309端口
  6. port=3309
  7. # 允许最大连接数
  8. max_connections=200
  9. # 服务端使用的字符集默认为UTF8
  10. character-set-server=utf8mb4
  11. # 创建新表时将使用的默认存储引擎
  12. default-storage-engine=INNODB
  13. #开启log-bin
  14. log_bin=mysql_bin
  15. #server-id
  16. server-id=10
  17. #开启异步复制
  18. sync_binlog=1
  19. #那些库不同步
  20. binlog_ignore_db=performace_schema
  21. binlog_ignore_db=sys
  22. binlog_ignore_db=infomation_schema
  23. #同步那些库
  24. #binlog-do-db=la_gou
  25. #每组包含的事务数量
  26. binlog_group_commit_sync_no_delay_count=100
  27. #组提交延迟 ms
  28. binlog_group_commit_sync_delay=1000
  29. #semi并行复制开启
  30. rpl_semi_sync_slave_enabled=1
  31. #从库并行复制类型 DATABASE基于库的并行复制 LOGICAL_CLOCK基于组提交
  32. slave_parallel_type=LOGICAL_CLOCK
  33. #并行复制线程数
  34. slave_parallel_workers=4
  35. #为了保证事务是按照relay log中记录的顺序来回放
  36. slave_preserve_commit_order=1
  37. #下面两个选项设置为table可以提升性能
  38. #这是因为并行复制开启后对于元master.info这个文件的更新将会大幅提升,资源的竞争也会变大
  39. master_info_repository=TABLE
  40. #relay.info明文存储不安全,把relay.info中的信息记录在table中相对安全。
  41. #可以避免relay.info更新不及时,SLAVE 重启后导致的主从复制出错。
  42. relay_log_info_repository=TABLE
  43. #开启relay_log覆盖
  44. relay_log_recovery=1
  45. #从库复制过程中跳过异常数
  46. slave_skip_errors=1

我们需要将第一步导出的tar文件导入为一个新的镜像

  1. docker import mysql_pack.tar mysql_pack:v1

然后run

注意:后面的命令 /.entrypoint.sh mysqld 必须指定,不然容器没法启动

  1. docker run --name mysql-slave1 -v E:/learn/docker/mysql/slave-1/conf:/etc/mysql/conf.d -v E:/learn/docker/mysql/slave-1/logs:/logs -v E:/learn/docker/mysql/slave-1/data:/var/lib/mysql -p 3309:3309 -p 1224:22 -e MYSQL_ROOT_PASSWORD=root -d mysql_pack:v1 /entrypoint.sh mysqld
  1. docker run --name mysql-slave2 -v E:/learn/docker/mysql/slave-2/conf:/etc/mysql/conf.d -v E:/learn/docker/mysql/slave-2/logs:/logs -v E:/learn/docker/mysql/slave-2/data:/var/lib/mysql -p 3310:3310 -p 1225:22 -e MYSQL_ROOT_PASSWORD=root -d mysql_pack:v1 /entrypoint.sh mysqld

然后我们就可以进入容器里面

  1. docker exec -it mysql-slave1 bash
  1. docker exec -it mysql-slave2 bash

启动ssh服务

  1. service ssh start

连接mysql

  1. mysql -uroot -P3309 -p
  1. ALTER USER 'root'@'%' IDENTIFIED BY 'root' PASSWORD EXPIRE NEVER; #修改加密规则
  2. ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root'; #更新一下用户的密码
  3. FLUSH PRIVILEGES; #刷新权限

安装semi插件

  1. install plugin rpl_semi_sync_slave soname 'semisync_slave.so';

退出,修改本地的配置文件,然后重启容器

然后再进入mysql,开启slave

注意:这里指定的ip为docker内部的ip

  1. change master to master_host='172.17.0.2',master_port=3307,master_user='root',master_password='root',master_log_file='mysql_bin.000010',master_log_pos=793;
  2. start slave;

14.4 第四步 mha-manager配置测试

配置mha-manager

获取master1,slave1,slave2的docker内部ip,我这里获取到的时候

172.17.0.3,172.17.0.4,172.17.0.5

我们将这几个ip指定到mha-manager配置文件app1.cnf中

  1. [server1]
  2. hostname=172.17.0.3
  3. port=3307
  4. [server2]
  5. hostname=172.17.0.4
  6. port=3309
  7. [server3]
  8. hostname=172.17.0.5
  9. port=3310

然后我们执行masterha_check_ssh

  1. root@09b45de37e38:/# masterha_check_ssh --conf=/etc/mha/app1.cnf
  2. Sun Sep 6 07:06:42 2020 - [warning] Global configuration file /etc/masterha_default.cnf not found. Skipping.
  3. Sun Sep 6 07:06:42 2020 - [info] Reading application default configuration from /etc/mha/app1.cnf..
  4. Sun Sep 6 07:06:42 2020 - [info] Reading server configuration from /etc/mha/app1.cnf..
  5. Sun Sep 6 07:06:42 2020 - [info] Starting SSH connection tests..
  6. Sun Sep 6 07:06:43 2020 - [debug]
  7. Sun Sep 6 07:06:42 2020 - [debug] Connecting via SSH from root@172.17.0.3(172.17.0.3:22) to root@172.17.0.4(172.17.0.4:22)..
  8. Sun Sep 6 07:06:42 2020 - [debug] ok.
  9. Sun Sep 6 07:06:42 2020 - [debug] Connecting via SSH from root@172.17.0.3(172.17.0.3:22) to root@172.17.0.5(172.17.0.5:22)..
  10. Warning: Permanently added '172.17.0.5' (ECDSA) to the list of known hosts.
  11. Sun Sep 6 07:06:43 2020 - [debug] ok.
  12. Sun Sep 6 07:06:44 2020 - [debug]
  13. Sun Sep 6 07:06:43 2020 - [debug] Connecting via SSH from root@172.17.0.4(172.17.0.4:22) to root@172.17.0.3(172.17.0.3:22)..
  14. Warning: Permanently added '172.17.0.3' (ECDSA) to the list of known hosts.
  15. Sun Sep 6 07:06:43 2020 - [debug] ok.
  16. Sun Sep 6 07:06:43 2020 - [debug] Connecting via SSH from root@172.17.0.4(172.17.0.4:22) to root@172.17.0.5(172.17.0.5:22)..
  17. Warning: Permanently added '172.17.0.5' (ECDSA) to the list of known hosts.
  18. Sun Sep 6 07:06:43 2020 - [debug] ok.
  19. Sun Sep 6 07:06:45 2020 - [debug]
  20. Sun Sep 6 07:06:43 2020 - [debug] Connecting via SSH from root@172.17.0.5(172.17.0.5:22) to root@172.17.0.3(172.17.0.3:22)..
  21. Warning: Permanently added '172.17.0.5' (ECDSA) to the list of known hosts.
  22. Sun Sep 6 07:06:43 2020 - [debug] ok.
  23. Sun Sep 6 07:06:43 2020 - [debug] Connecting via SSH from root@172.17.0.5(172.17.0.5:22) to root@172.17.0.4(172.17.0.4:22)..
  24. Warning: Permanently added '172.17.0.4' (ECDSA) to the list of known hosts.
  25. Sun Sep 6 07:06:44 2020 - [debug] ok.
  26. Sun Sep 6 07:06:45 2020 - [info] All SSH connection tests passed successfully.
  27. Use of uninitialized value in exit at /usr/bin/masterha_check_ssh line 44.

然后校验从库是否正常:masterha_check_repl —conf=/etc/mha/app1.cnf

  1. root@09b45de37e38:/# masterha_check_repl --conf=/etc/mha/app1.cnf
  2. Sun Sep 6 07:07:26 2020 - [warning] Global configuration file /etc/masterha_default.cnf not found. Skipping.
  3. Sun Sep 6 07:07:26 2020 - [info] Reading application default configuration from /etc/mha/app1.cnf..
  4. Sun Sep 6 07:07:26 2020 - [info] Reading server configuration from /etc/mha/app1.cnf..
  5. Sun Sep 6 07:07:26 2020 - [info] MHA::MasterMonitor version 0.58.
  6. Sun Sep 6 07:07:27 2020 - [info] GTID failover mode = 0
  7. Sun Sep 6 07:07:27 2020 - [info] Dead Servers:
  8. Sun Sep 6 07:07:27 2020 - [info] Alive Servers:
  9. Sun Sep 6 07:07:27 2020 - [info] 172.17.0.3(172.17.0.3:3307)
  10. Sun Sep 6 07:07:27 2020 - [info] 172.17.0.4(172.17.0.4:3309)
  11. Sun Sep 6 07:07:27 2020 - [info] 172.17.0.5(172.17.0.5:3310)
  12. Sun Sep 6 07:07:27 2020 - [info] Alive Slaves:
  13. Sun Sep 6 07:07:27 2020 - [info] 172.17.0.4(172.17.0.4:3309) Version=8.0.21 (oldest major version between slaves) log-bin:enabled
  14. Sun Sep 6 07:07:27 2020 - [info] Replicating from 172.17.0.3(172.17.0.3:3307)
  15. Sun Sep 6 07:07:27 2020 - [info] 172.17.0.5(172.17.0.5:3310) Version=8.0.21 (oldest major version between slaves) log-bin:enabled
  16. Sun Sep 6 07:07:27 2020 - [info] Replicating from 172.17.0.3(172.17.0.3:3307)
  17. Sun Sep 6 07:07:27 2020 - [info] Current Alive Master: 172.17.0.3(172.17.0.3:3307)
  18. Sun Sep 6 07:07:27 2020 - [info] Checking slave configurations..
  19. Sun Sep 6 07:07:27 2020 - [info] read_only=1 is not set on slave 172.17.0.4(172.17.0.4:3309).
  20. Sun Sep 6 07:07:27 2020 - [warning] relay_log_purge=0 is not set on slave 172.17.0.4(172.17.0.4:3309).
  21. Sun Sep 6 07:07:27 2020 - [info] read_only=1 is not set on slave 172.17.0.5(172.17.0.5:3310).
  22. Sun Sep 6 07:07:27 2020 - [warning] relay_log_purge=0 is not set on slave 172.17.0.5(172.17.0.5:3310).
  23. Sun Sep 6 07:07:27 2020 - [info] Checking replication filtering settings..
  24. Sun Sep 6 07:07:27 2020 - [info] binlog_do_db= , binlog_ignore_db= infomation_schema,performace_schema,sys
  25. Sun Sep 6 07:07:27 2020 - [info] Replication filtering check ok.
  26. Sun Sep 6 07:07:27 2020 - [info] GTID (with auto-pos) is not supported
  27. Sun Sep 6 07:07:27 2020 - [info] Starting SSH connection tests..
  28. Sun Sep 6 07:07:29 2020 - [info] All SSH connection tests passed successfully.
  29. Sun Sep 6 07:07:29 2020 - [info] Checking MHA Node version..
  30. Sun Sep 6 07:07:30 2020 - [info] Version check ok.
  31. Sun Sep 6 07:07:30 2020 - [info] Checking SSH publickey authentication settings on the current master..
  32. Sun Sep 6 07:07:30 2020 - [info] HealthCheck: SSH to 172.17.0.3 is reachable.
  33. Sun Sep 6 07:07:30 2020 - [info] Master MHA Node version is 0.58.
  34. Sun Sep 6 07:07:30 2020 - [info] Checking recovery script configurations on 172.17.0.3(172.17.0.3:3307)..
  35. Sun Sep 6 07:07:30 2020 - [info] Executing command: save_binary_logs --command=test --start_pos=4 --binlog_dir=/var/lib/mysql --output_file=/var/tmp/save_binary_logs_test --manager_version=0.58 --start_file=mysql_bin.000005
  36. Sun Sep 6 07:07:30 2020 - [info] Connecting to root@172.17.0.3(172.17.0.3:22)..
  37. Creating /var/tmp if not exists.. ok.
  38. Checking output directory is accessible or not..
  39. ok.
  40. Binlog found at /var/lib/mysql, up to mysql_bin.000005
  41. Sun Sep 6 07:07:31 2020 - [info] Binlog setting check done.
  42. Sun Sep 6 07:07:31 2020 - [info] Checking SSH publickey authentication and checking recovery script configurations on all alive slave servers..
  43. Sun Sep 6 07:07:31 2020 - [info] Executing command : apply_diff_relay_logs --command=test --slave_user='root' --slave_host=172.17.0.4 --slave_ip=172.17.0.4 --slave_port=3309 --workdir=/var/tmp --target_version=8.0.21 --manager_version=0.58 --relay_dir=/var/lib/mysql --current_relay_log=c5b4b95deaf9-relay-bin.000002 --slave_pass=xxx
  44. Sun Sep 6 07:07:31 2020 - [info] Connecting to root@172.17.0.4(172.17.0.4:22)..
  45. Checking slave recovery environment settings..
  46. Relay log found at /var/lib/mysql, up to c5b4b95deaf9-relay-bin.000002
  47. Temporary relay log file is /var/lib/mysql/c5b4b95deaf9-relay-bin.000002
  48. Checking if super_read_only is defined and turned on.. not present or turned off, ignoring.
  49. Testing mysql connection and privileges..
  50. mysql: [Warning] Using a password on the command line interface can be insecure.
  51. done.
  52. Testing mysqlbinlog output.. done.
  53. Cleaning up test file(s).. done.
  54. Sun Sep 6 07:07:31 2020 - [info] Executing command : apply_diff_relay_logs --command=test --slave_user='root' --slave_host=172.17.0.5 --slave_ip=172.17.0.5 --slave_port=3310 --workdir=/var/tmp --target_version=8.0.21 --manager_version=0.58 --relay_dir=/var/lib/mysql --current_relay_log=slave2_relay_log.000002 --slave_pass=xxx
  55. Sun Sep 6 07:07:31 2020 - [info] Connecting to root@172.17.0.5(172.17.0.5:22)..
  56. Checking slave recovery environment settings..
  57. Relay log found at /var/lib/mysql, up to slave2_relay_log.000002
  58. Temporary relay log file is /var/lib/mysql/slave2_relay_log.000002
  59. Checking if super_read_only is defined and turned on.. not present or turned off, ignoring.
  60. Testing mysql connection and privileges..
  61. mysql: [Warning] Using a password on the command line interface can be insecure.
  62. done.
  63. Testing mysqlbinlog output.. done.
  64. Cleaning up test file(s).. done.
  65. Sun Sep 6 07:07:32 2020 - [info] Slaves settings check done.
  66. Sun Sep 6 07:07:32 2020 - [info]
  67. 172.17.0.3(172.17.0.3:3307) (current master)
  68. +--172.17.0.4(172.17.0.4:3309)
  69. +--172.17.0.5(172.17.0.5:3310)
  70. Sun Sep 6 07:07:32 2020 - [info] Checking replication health on 172.17.0.4..
  71. Sun Sep 6 07:07:32 2020 - [info] ok.
  72. Sun Sep 6 07:07:32 2020 - [info] Checking replication health on 172.17.0.5..
  73. Sun Sep 6 07:07:32 2020 - [info] ok.
  74. Sun Sep 6 07:07:32 2020 - [warning] master_ip_failover_script is not defined.
  75. Sun Sep 6 07:07:32 2020 - [warning] shutdown_script is not defined.
  76. Sun Sep 6 07:07:32 2020 - [info] Got exit code 0 (Not master dead).
  77. MySQL Replication Health is OK.

到这里我们环境配置就结束了,可以进行测试了。

14.5 环境搭建过程中遇到的问题

其实就是上面注意的点都是根据错来的

1,semi相关的配置,依赖于semi的插件,当没有安装semi插件的时候,在配置文件中开启了semi相关的配置在启动的时候会报错

2,最开始的时候mha-manager配置文件中配置的host为无线局域网适配器的ip,而mha通过ssh连接各个mysql实列访问的都是同一个端口,这就尴尬了。然后就通过ssh访问了docker容器内部的ip可以访问,然后也可以通过mysql连接另一个容器的mysql实例,所以mha-manager配置文件就使用了容器内部的ip。不过,启动校验的时候不能不能通过ssh连接manager所在实例进行操作,必须通过docker exec方式进入容器进行操作。

3,change master指定master信息的时候mster_host也必须为容器内部的ip,不然masterha_check_repl会说没有可用的从库。

4,因为使用的是8版本的mysql,密码的加密方式插件为caching_sha2_password。导致mha无法连接而报错。

因此需要执行:

  1. ALTER USER 'root'@'%' IDENTIFIED BY 'root' PASSWORD EXPIRE NEVER; #修改加密规则
  2. ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root'; #更新一下用户的密码
  3. FLUSH PRIVILEGES; #刷新权限

或者在配置文件中在[mysqld]进行配置:

  1. # 默认使用“mysql_native_password”插件认证
  2. default_authentication_plugin=mysql_native_password

5,masterha_check_repl 报

  1. [error][/usr/share/perl5/MHA/MasterMonitor.pm, ln427] Error happened on checking configurations. Redundant argument in sprintf at /usr/share/perl5/MHA/NodeUtil.pm line 201.

异常:

需要对/usr/share/perl5/MHA/NodeUtil.pm 修改

  1. sub parse_mysql_major_version($) {
  2. my $str = shift;
  3. my $result = sprintf( '%03d%03d', $str =~ m/(\d+)/g );
  4. return $result;
  5. }

为:

  1. sub parse_mysql_major_version($) {
  2. my $str = shift;
  3. $str =~ /(\d+)\.(\d+)/;
  4. my $strmajor = "$1.$2";
  5. my $result = sprintf( '%03d%03d', $strmajor =~ m/(\d+)/g );
  6. return $result;
  7. }

上面是百度的解决办法

6,从库slave 如果没有开启read_only就需要开启bin_log日志,不然启动mha_manager的时候会报错。

7,如果mha切换成功,我们再恢复原来的一主两从的环境的时候,需要将mha运行目录下的complete日志给清除,不然再次切换的时候,可能会切换不成功。

14.6 测试语句

  1. show variables like '%semi%';
  2. show master status;
  3. drop table position;
  4. create table order.position(
  5. id int(11) unsigned primary key auto_increment,
  6. name varchar(20) ,
  7. salary varchar(10) ,
  8. city varchar(50)
  9. )engine=innodb DEFAULT CHARSET=utf8;
  10. insert into position (name,salary,city) values ('张三','7999','北京');
  11. insert into position (name,salary,city) values ("李四","9999","广州");
  12. show master status;
  13. select * from position;
  14. select * from position_detail;
  15. create table order.position_detail(
  16. id int(11) primary key auto_increment,
  17. pid int(11),
  18. description varchar(30) )engine=innodb DEFAULT CHARSET=utf8;
  19. insert into position_detail(pid,description)values (1,"张三备注");
  20. insert into position_detail(pid,description)values (2,"李四备注");

15 问题

1,redolog和脏页(dirtyPage)的区别?是从redolog刷盘还是从dirtyPage刷盘?

首先 明白一个概念,数据落盘,修改的数据到ibd文件中,而不是到redolog中。
redolog主要用来做数据恢复用。
在事务开始,进行增删改操作的时候,会往logbuffer里面记录日志,然后bufferpool的脏页里面也会记录。
当事务提交的时候(这里有三种持久化redolog的机制),会将logbuffer里面的数据持久化到redolog,redolog是顺序写入的。但是脏页里面的数据是没有落盘的,也就是没有随机写入ibd中。
我们知道后台有write thread当脏页的数据达到90%的时候才会罗盘到ibd数据文件中。

所以,为啥需要redolog做记录,因为如果脏页里面说句还没落盘,数据库挂了就可以利用redolog。

2,为啥需要redolog?

根据1的分析,写日志是顺序写,会快一些,所以用写日志的方式保证数据不丢失,然后再慢慢把数据写入到应该写入的位置(随机写)。而且redolog也不是实时的,是logbuffer->redolog

16 扩展

16.1 存储限制

https://dev.mysql.com/doc/refman/8.0/en/innodb-limits.html】

https://dev.mysql.com/doc/refman/8.0/en/column-count-limit.html】

500万或者单表容量超过 2GB就应该分库分表了。

  • MySQL表具有65,535字节的最大行大小限制,即使存储引擎能够支持更大的行也是如此。但是对于定长的类型就不是65535字节限制了,Row size too large (> 8126)而是最大不能超过8126。而对于page_size=16KB的页大小来说,一页的一半为8KB=8192字节。

  • 对于默认的16KB InnoDB页大小,最大行大小略小于8KB 。对于64KB页,最大行大小略小于16KB。如果包含可变长度列(例如:text)的InnoDB 行超过最大行大小,InnoDB选择可变长度列进行页外存储。

  • MySQL表的每行行最大限制为65,535字节,这是逻辑的限制;实际存储的时候,表的物理最大行大小略小于页面的一半。如果一行的长度少于一页的一半,则所有行都将存储在本地页面内。如果它超过一页的一半,那么将选择可变长度列用于外部页外存储,直到该行大小控制在半页之内为止。而且不同的编码类型占用字节大小是不同的,utf8每个字符最多占用3个字节,utf8mb4每个字符最多占用4个字节,而且还需要1或者2个字节存储可变长长度。 所以假入一个表只有一个字段varchar(N),使用utf8编码,那么N最大为(65535-1-2)/3。
    相关博客【https://www.cnblogs.com/chenpingzhao/p/4714411.html】

    1. 65535个字节包括所有字段的长度,变长字段的长度标识(每个变长字段额外使用1或者2个字节记录实际数据长度)、NULL标识位的累计
    2. NULL标识位,如果varchar字段定义中带有default null允许列空,则需要需要1bit来标识,每8bits的标识组成一个字段。一张表中存在Nvarchar字段,那么需要(N+7)/8 (取整)bytes存储所有的NULL标识位
    3. 如果数据表只有一个varchar字段且该字段DEFAULT NULL,那么该varchar字段的最大长度为65532个字节,
    4. 65535-2-1=65532 bytes
    5. 在物理存储上,varchar使用12个额外的字节表示实际存储的字符串长度(bytes)。如果列的最大长度小于256个字节,用一个字节表示(标识)。如果最大长度大于等于256,使用两个字节。当选择的字符集为latin1,一个字符占用一个bytevarchar(255)存储一个字符,一共使用2bytes物理空间存储数据实际数据长度和数据值。varchar(256)存储一个字符,使用2 bytes表示实际数据长度,一共需要3 bytes物理存储空间。
  • 一个表的最大索引数量(非主键索引)为64个 (A table can contain a maximum of 64 secondary indexes.)

  • 复合索引最多可以包括16个列。超过会报错:ERROR 1070 (42000): Too many key parts specified; max 16 parts allowed

  • char的最大长度为255,不管用什么编码类型。

  • MySQL has hard limit of 4096 columns per table (myisam最大columns就是4096). 但是innodb 的column限制为:A table can contain a maximum of 1017 columns. Virtual generated columns are included in this limit.

  • The index key prefix length limit is 3072 bytes for InnoDB tables that use DYNAMIC or COMPRESSED row format.

  • The maximum row size, excluding any variable-length columns that are stored off-page, is slightly less than half of a page for 4KB, 8KB, 16KB, and 32KB page sizes. For example, the maximum row size for the default innodb_page_size of 16KB is about 8000 bytes. However, for an InnoDB page size of 64KB, the maximum row size is approximately 16000 bytes. LONGBLOB and LONGTEXT columns must be less than 4GB, and the total row size, including BLOB and TEXT columns, must be less than 4GB.

    1. If a row is less than half a page long, all of it is stored locally within the page. If it exceeds half a page, variable-length columns are chosen for external off-page storage until the row fits within half a page, as described in Section 15.11.2, File Space Management”.
  • Mysql上笔记 - 图93

有个问题

一共33个char(255) 总长度为255*33=8415 255*32=8160

charset改为utf8就可以插入了。

  1. CREATE TABLE t4 (
  2. c1 CHAR(255),c2 CHAR(255),c3 CHAR(255),
  3. c4 CHAR(255),c5 CHAR(255),c6 CHAR(255),
  4. c7 CHAR(255),c8 CHAR(255),c9 CHAR(255),
  5. c10 CHAR(255),c11 CHAR(255),c12 CHAR(255),
  6. c13 CHAR(255),c14 CHAR(255),c15 CHAR(255),
  7. c16 CHAR(255),c17 CHAR(255),c18 CHAR(255),
  8. c19 CHAR(255),c20 CHAR(255),c21 CHAR(255),
  9. c22 CHAR(255),c23 CHAR(255),c24 CHAR(255),
  10. c25 CHAR(255),c26 CHAR(255),c27 CHAR(255),
  11. c28 CHAR(255),c29 CHAR(255),c30 CHAR(255),
  12. c31 CHAR(255),c32 CHAR(255),c33 CHAR(255)
  13. ) ENGINE=InnoDB ROW_FORMAT=dynamic DEFAULT CHARSET latin1;
  14. ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB may help.
  15. In current row format, BLOB prefix of 0 bytes is stored inline.