一.Mysql原理

1.1 架构

截屏2023-04-13 15.29.03

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

  • 网络连接层

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

  • 服务层

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

    • 连接池(Connection Pool):负责存储和管理客户端与数据库的连接,一个线程负责管理一个连接。

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

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

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

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

      比如select uid,name from user where gender=1;执行的步骤

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

  • 存储引擎层

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

  • 系统文件层

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

    • 日志文件

      • 错误日志(Error log):默认开启,命令:show variables like ‘%log_error%’

      • 通用查询日志(General query log):记录一般查询语句,命令:show variables like ‘%general%’;

      • 二进制日志(binary log):记录了对MySQL数据库执行的更改操作,并且记录了语句的发生时间、执行时长;但是它不记

        录select、show等不修改数据库的SQL。主要用于数据库恢复和主从复制。

        • show variables like ‘%log_bin%’; //是否开启
        • show variables like ‘%binlog%’; //参数查看
        • show binary logs;//查看日志文件
      • 慢查询日志(Slow query log):记录所有执行时间超时的查询SQL,默认是10秒。

        • show variables like ‘%slow_query%’; //是否开启
        • show variables like ‘%long_query_time%’; //时长
    • 配置文件

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

    • 数据文件

      • db.opt 文件:记录这个库的默认使用的字符集和校验规则。
      • frm 文件:存储与表相关的元数据(meta)信息,包括表结构的定义信息等,每一张表都会有一个frm 文件。
      • MYD 文件:MyISAM 存储引擎专用,存放 MyISAM 表的数据(data),每一张表都会有一个.MYD 文件。
      • MYI 文件:MyISAM 存储引擎专用,存放 MyISAM 表的索引相关信息,每一张 MyISAM 表对应一个 .MYI 文件。
      • ibd文件和 IBDATA 文件:存放 InnoDB 的数据文件(包括索引)。InnoDB 存储引擎有两种表空间方式:独享表空间和共享表空间。独享表空间使用 .ibd 文件来存放数据,且每一张InnoDB 表对应一个 .ibd 文件。共享表空间使用 .ibdata 文件,所有表共同使用一个(或多个,自行配置).ibdata 文件。
      • ibdata1 文件:系统表空间数据文件,存储表元数据、Undo日志等 。
      • ib_logfile0、ib_logfile1 文件:Redo log 日志文件。
    • pid 文件

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

    • socket 文件

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

1.2 运行机制

截屏2023-04-13 15.46.39

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

  • 通讯机制:

    • 全双工:能同时发送和接收数据,例如平时打电话。
    • 半双工:指的某一时刻,要么发送数据,要么接收数据,不能同时。例如早期对讲机
    • 单工:只能发送数据或只能接收数据。例如单行道
  • 线程状态:

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

    • 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:一般记录线程执行的语句,默认显示前100个字符。想查看完整的使用show full processlist;

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

  • 缓存Select查询的结果和SQL语句

  • 执行Select查询时,先查询缓存,判断是否存在可用的记录集,要求是否完全相同(包括参数值),这样才会匹配缓存数据命中。

  • 即使开启查询缓存,以下SQL也不能缓存

    • 查询语句使用SQL_NO_CACHE
    • 查询的结果大于query_cache_limit设置
    • 查询中有一些不确定的参数,比如now()
  • show variables like ‘%query_cache%’; //查看查询缓存是否启用,空间大小,限制等

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

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

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

  • 等价变换策略

    • 5=5 and a>5 改成 a > 5
    • a < b and a=5 改成b>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)

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

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

1.3 存储引擎

存储引擎在MySQL的体系架构中位于第三层,负责MySQL中的数据的存储和提取,是与文件打交道的子系统,它是根据MySQL提供的文件访问层抽象接口定制的一种文件访问机制,这种机制就叫作存储引擎。

使用show engines命令,就可以查看当前数据库支持的引擎信息。在5.5版本之前默认采用MyISAM存储引擎,从5.5开始采用InnoDB存储引擎

存储引擎:

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

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

  • Memory:利用内存创建表,访问速度非常快,因为数据在内存,而且默认使用Hash索引,但是一旦关闭,数据就会丢失

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

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

  • BlackHole: 黑洞,只进不出,进来消失,所有插入数据都不会保存

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

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

1.3.1 InnoDB和MyISAM对比
  • 事务和外键

    InnoDB支持事务和外键,具有安全性和完整性,适合大量insert或update操作

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

  • 锁机制

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

    MyISAM支持表级锁,锁定整张表。

  • 索引结构

    InnoDB使用聚集索引(聚簇索引),索引和记录在一起存储,既缓存索引,也缓存记录。

    MyISAM使用非聚集索引(非聚簇索引),索引和记录分开。

  • 并发处理能力

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

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

  • 存储文件

    InnoDB表对应两个文件,一个.frm表结构文件,一个.ibd数据文件。InnoDB表最大支持64TB;

    MyISAM表对应三个文件,一个.frm表结构文件,一个MYD表数据文件,一个.MYI索引文件。从MySQL5.0开始默认限制是256TB。

  • 适用场景

    MyISAM

    • 不需要事务支持(不支持)
    • 并发相对较低(锁定机制问题)
    • 数据修改相对较少,以读为主
    • 数据一致性要求不高

    InnoDB

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

    两种引擎该如何选择?

    • 是否需要事务?有,InnoDB
    • 是否存在并发修改?有,InnoDB
    • 是否追求快速查询,且数据修改少?是,MyISAM

    在绝大多数情况下,推荐使用InnoDB

存储引擎对比

截屏2023-04-13 17.51.23

1.3.2 InnoDB存储结构

从MySQL 5.5版本开始默认使用InnoDB作为引擎,它擅长处理事务,具有自动崩溃恢复的特性。官方的InnoDB引擎架构图如下,主要分为内存结构和磁盘结构两大部分

截屏2023-04-13 18.06.37

1.InnoDB内存结构

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

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

    • Page管理机制

      Page根据状态可以分为三种类型:

      • free page : 空闲page,未被使用
      • clean page:被使用page,数据没有被修改过
      • dirty page:脏页,被使用page,数据被修改过,页中数据和磁盘的数据产生了不一致

      针对上述三种page类型,InnoDB通过三种链表结构来维护和管理

      • free list :表示空闲缓冲区,管理free page
      • flush list:表示需要刷新到磁盘的缓冲区,管理dirty page,内部page按修改时间排序。脏页即存在于flush链表,也在LRU链表中,但是两种互不影响,LRU链表负责管理page的可用性和释放,而flush链表负责管理脏页的刷盘操作。
      • lru list:表示正在使用的缓冲区,管理clean page和dirty page,缓冲区以midpoint为基点,前面链表称为new列表区,存放经常访问的数据,占63%;后面的链表称为old列表区,存放使用较少数据,占37%。
    • 改进型LRU算法维护

      普通LRU:末尾淘汰法,新数据从链表头部加入,释放空间时从末尾淘汰

      改性LRU:链表分为new和old两个部分,加入元素时并不是从表头插入,而是从中间midpoint位置插入,如果数据很快被访问,那么page就会向new列表头部移动,如果数据没有被访问,会逐步向old尾部移动,等待淘汰。

      每当有新的page数据读取到buffer pool时,InnoDb引擎会判断是否有空闲页,是否足够,如果有就将free page从free list列表删除,放入到LRU列表中。没有空闲页,就会根据LRU算法淘汰LRU链表默认的页,将内存空间释放分配给新的页。

    • Buffer Pool配置参数

      show variables like ‘%innodb_page_size%’; //查看page页大小

      show variables like ‘%innodb_old%’; //查看lru list中old列表参数

      show variables like ‘%innodb_buffffer%’; //查看buffer pool参数

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

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

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

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

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

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

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

  • Log Buffer:日志缓冲区,用来保存要写入磁盘上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

    • 0 :每隔1秒写日志文件和刷盘操作(写日志文件LogBuffer–>OS cache,刷盘OS cache–>磁盘文件),最多丢失1秒数据
    • 1:事务提交,立刻写日志文件和刷盘,数据不丢失,但是会频繁IO操作
    • 2:事务提交,立刻写日志文件,每隔1秒钟进行刷盘操作

2.InnoDB磁盘结构

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

  • 表空间(Tablespaces):用于存储表结构和数据。表空间又分为系统表空间、独立表空间、通用表空间、临时表空间、Undo表空间等多种类型;

    • 系统表空间(System Tablespace)

      包含InnoDB数据字典,Doublewrite Buffer,Change Buffer,Undo Logs的存储区域。系统表空间也默认包含任何用户在系统表空间创建的表数据和索引数据。系统表空间是一个共享的表空间因为它是被多个表共享的。该空间的数据文件通过参数innodb_data_file_path控制,默认值是ibdata1:12M:autoextend(文件名为ibdata1、12MB、自动扩展)。

    • 独立表空间(File-Per-Table Tablespaces)

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

    • 通用表空间(General Tablespaces)

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

      CREATE TABLESPACE ts1 ADD DATAFILE ts1.ibd Engine=InnoDB; //创建表空间ts1 
      
      CREATE TABLE t1 (c1 INT PRIMARY KEY) TABLESPACE ts1; //将表添加到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等。

    • 临时表空间(Temporary Tablespaces)

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

  • 数据字典(InnoDB Data Dictionary)

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

  • 双写缓冲区(Doublewrite Buffer)

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

    MySQL的innodb_flush_method这个参数控制着innodb数据文件及redo log的打开、刷写模式。有三个值:fdatasync(默认),O_DSYNC,O_DIRECT。设置O_DIRECT表示数据文件写入操作会通知操作系统不要缓存数据,也不要用预读,直接从Innodb Buffer写到磁盘文件。

    默认的fdatasync意思是先写入操作系统缓存,然后再调用fsync()函数去异步刷数据文件与redo log的缓存信息。

  • 重做日志(Redo Log)

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

  • 撤销日志(Undo Logs)

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

3.新版本结构演变

  • 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中也分离出来了。
1.3.3 InnoDB线程模型

截屏2023-05-04 17.50.30

  • 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个
    • write thread:负责写操作,将缓存脏页刷新到磁盘。4个
    • log thread:负责将日志缓冲区内容刷新到磁盘。1个
    • insert buffer thread :负责将写缓冲内容刷新到磁盘。1个
  • Purge Thread

    事务提交之后,其使用的undo日志将不再需要,因此需要Purge Thread回收已经分配的undo页。

    show variables like ‘%innodb_purge_threads%’;

  • Page Cleaner Thread

    作用是将脏数据刷新到磁盘,脏数据刷盘后相应的redo log也就可以覆盖,即可以同步数据,又能达到redo log循环使用的目的。会调用write thread线程处理。

    show variables like ‘%innodb_page_cleaners%’;

  • Master Thread

    Master thread是InnoDB的主线程,负责调度其他各线程,优先级最高。作用是将缓冲池中的数据异步刷新到磁盘 ,保证数据的一致性。包含:脏页的刷新(page cleaner thread)、undo页回收(purge thread)、redo日志刷新(log thread)、合并写缓冲等。内部有两个主处理,分别是每隔1秒和10秒处理。

    每1秒的操作

    • 刷新日志缓冲区,刷到磁盘

    • 合并写缓冲区数据,根据IO读写压力来决定是否操作

    • 刷新脏页数据到磁盘,根据脏页比例达到75%才操作(innodb_max_dirty_pages_pct,innodb_io_capacity)

    每10秒的操作

    • 刷新脏页数据到磁盘
    • 合并写缓冲区数据
    • 刷新日志缓冲区
    • 删除无用的undo页
1.3.4 InnoDB数据文件
1.3.4.1 InnoDB文件存储结构

截屏2023-05-04 18.02.36

Tablesapce–>ibd数据文件–>Segment(段)–>Extent(区)–>Page(页)–>Row(行)

  • Tablesapce

    表空间,用于存储多个ibd数据文件

  • ibd

    用于存储表的记录和索引。一个文件包含多个段。

  • Segment

    段,用于管理多个Extent,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollback segment)。一个表至少会有两个segment,一个管理数据,一个管理索引。每多创建一个索引,会多两个segment,分别是内节点段和叶子段,内节点段用来管理(存储)B+树非叶子(页面)的数据,叶子段用来管理(存储)B+树叶子节点的数据。

  • Extent

    区,一个区固定包含64个连续的页,大小为1M。当表空间不足,需要分配新的页资源,不会一页一页分,直接分配一个区。

  • Page

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

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

截屏2023-05-04 18.05.41

  • Row

    行,包含了记录的字段值,事务ID(Trx id)、滚动指针(Roll pointer)、字段指针(Field pointers)等信息。

1.3.4.2 InnoDB文件存储格式

File文件格式(File-Format)

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

select * from information_schema.innodb_sys_tables;

在早期的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行格式(Row_format)

通过 SHOW TABLE STATUS 命令

截屏2023-05-04 18.11.45

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

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

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

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行格式相同的存储特性和功能,但增加了对表和索引数据压缩的支持。

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

ALTER TABLE 表名 ROW_FORMAT=格式类型;
1.3.5 Undo Log
  • Undo:意为撤销或取消,以撤销操作为目的,返回指定某个状态的操作。

  • Undo Log:数据库事务开始之前,会将要修改的记录存放到 Undo 日志里,当事务回滚时或者数据库崩溃时,可以利用 Undo 日志,撤销未提交事务对数据库产生的影响。

  • Undo Log产生和销毁:Undo Log在事务开始前产生;事务在提交时,并不会立刻删除undo log,innodb会将该事务对应的undo log放入到删除列表中,后面会通过后台线程purge thread进行回收处理。Undo Log属于逻辑日志,记录一个变化过程。例如执行一个delete,undolog会记录一个insert;执行一个update,undolog会记录一个相反的update。

  • Undo Log存储:undo log采用段的方式管理和记录。在innodb数据文件中包含一种rollback segment回滚段,内部包含1024个undo log segment。可以通过下面一组参数来控制Undo log存储。

    show variables like '%innodb_undo%';
    

Undo Log作用

  • 实现事务的原子性

    Undo Log 是为了实现事务的原子性而出现的产物。事务处理过程中,如果出现了错误或者用户执行了 ROLLBACK 语句,MySQL 可以利用 Undo Log 中的备份将数据恢复到事务开始之前的状态。

  • 实现多版本并发控制(MVCC)

    Undo Log 在 MySQL InnoDB 存储引擎中用来实现多版本并发控制。事务未提交之前,Undo Log保存了未提交之前的版本数据,Undo Log 中的数据可作为数据旧版本快照供其他并发事务进行快照读

截屏2023-05-04 18.33.39

事务A手动开启事务,执行更新操作,首先会把更新命中的数据备份到 Undo Buffer 中。

事务B手动开启事务,执行查询操作,会读取 Undo 日志数据返回,进行快照读

Undo Log类型

  • insert undo log

    代表事务在insert新数据时产生的undolog ,只在事务回滚的时候需要。并且在事务提交后可以被立即丢弃

  • update undo log

    事务在进行update或者delete时产生的undolog 。不仅在事务回滚时需要,在快照读时也需要。所以不能随便删除,只有当readview中不存在这个事务的事务id时才能被删除(被purge线程统一清理)

为了实现innodbMVCC,在事务提交前更新和删除只是设置一下老记录的delete_bit ,并不是真实的删除

1.3.6 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 进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。

截屏2023-05-06 14.49.59

Redo Log写入机制

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

截屏2023-05-06 16.08.09

如图所示:

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

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

Redo Log相关配置参数

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

show variables like '%innodb_log%';

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

  • 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秒的事务提交数据

截屏2023-05-06 16.18.54

1.3.7 Binlog

Binlog记录模式

Redo Log 是属于InnoDB引擎所特有的日志,而MySQL Server也有自己的日志,即 Binary log(二进制日志),简称Binlog。Binlog是记录所有数据库表结构变更以及表数据修改的二进制日志,不会记录SELECT和SHOW这类操作。Binlog日志是以事件形式记录,还包含语句所执行的消耗时间。开启Binlog日志有以下两个最重要的使用场景。

  • 主从复制:在主库中开启Binlog功能,这样主库就可以把Binlog传递给从库,从库拿到Binlog后实现数据恢复达到主从数据一致性。
  • 数据恢复:通过mysqlbinlog工具来恢复数据。

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

  • ROW(row-based replication, RBR):日志中会记录每一行数据被修改的情况,然后在slave端对相同的数据进行修改。

    • 优点:能清楚记录每一个行数据的修改细节,能完全实现主从数据同步和数据的恢复。
    • 缺点:批量操作,会产生大量的日志,尤其是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文件结构

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

Binlog文件中Log event结构如下图所示:

截屏2023-05-06 16.31.03

Binlog写入机制

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

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

  • 事务在提交阶段会将产生的log event写入到外部binlog文件中。

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

Binlog文件操作

  • Binlog状态查看
show variables like 'log_bin';
  • 开启Binlog功能
mysql> set global log_bin=mysqllogbin; 

ERROR 1238 (HY000): Variable 'log_bin' is a read only variable

需要修改my.cnf或my.ini配置文件,在[mysqld]下面增加log_bin=mysql_bin_log,重启MySQL服务。

#log-bin=ON 

#log-bin-basename=mysqlbinlog 

binlog-format=ROW 

log-bin=mysqlbinlog
  • 使用show binlog events命令
show binary logs; //等价于show master logs; 

show master status; 

show binlog events; 

show binlog events in 'mysqlbinlog.000001';
  • 使用mysqlbinlog 命令
mysqlbinlog "文件名" 

mysqlbinlog "文件名" > "test.sql"
  • 使用 binlog 恢复数据
//按指定时间恢复 

mysqlbinlog --start-datetime="2020-04-25 18:00:00"

datetime="2020-04-26 00:00:00" mysqlbinlog.000002 | mysql -uroot -p1234 

//按事件位置号恢复 

mysqlbinlog --start-position=154 --stop-position=957 mysqlbinlog.000002 | mysql -uroot -p1234

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

  • 删除Binlog文件
purge binary logs to 'mysqlbinlog.000001'; //删除指定文件 

purge binary logs before '2020-04-28 00:00:00'; //删除指定时间之前的文件 

reset master; //清除所有文件

可以通过设置expire_logs_days参数来启动自动清理功能。默认值为0表示没启用。设置为1表示超出1天binlog文件会自动删除掉。

1.3.8 Redo Log和Binlog区别

Redo Log是属于InnoDB引擎功能,Binlog是属于MySQL Server自带功能,并且是以二进制文件记录。

Redo Log属于物理日志,记录该数据页更新状态内容,Binlog是逻辑日志,记录更新过程。

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

Redo Log作为服务器异常宕机后事务数据自动恢复使用,Binlog可以作为主从复制和数据恢复使用。Binlog没有自动crash-safe能力。

二.MySQL索引原理

2.1 索引类型

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

  • 从索引存储结构划分:B Tree索引、Hash索引、FULLTEXT全文索引、R Tree索引
  • 从应用层次划分:普通索引、唯一索引、主键索引、复合索引
  • 从索引键值类型划分:主键索引、辅助索引(二级索引)
  • 从数据存储和索引键值逻辑关系划分:聚集索引(聚簇索引)、非聚集索引(非聚簇索引)
2.1.1 普通索引

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

CREATE INDEX <索引的名字> ON tablename (字段名);

ALTER TABLE tablename ADD INDEX [索引的名字] (字段名);

CREATE TABLE tablename ( [...], INDEX [索引的名字] (字段名) );
2.1.2 唯一索引

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

CREATE UNIQUE INDEX <索引的名字> ON tablename (字段名);

ALTER TABLE tablename ADD UNIQUE INDEX [索引的名字] (字段名);

CREATE TABLE tablename ( [...], UNIQUE [索引的名字] (字段名) ;
2.1.3 主键索引

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

CREATE TABLE tablename ( [...], PRIMARY KEY (字段名) );

ALTER TABLE tablename ADD PRIMARY KEY (字段名);
2.1.4复合索引

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

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

创建组合索引的方法如下:

CREATE INDEX <索引的名字> ON tablename (字段名1,字段名2...);

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

CREATE TABLE tablename ( [...], INDEX [索引的名字] (字段名1,字段名2...) );

复合索引使用注意事项:

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

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

2.1.5全文索引

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

CREATE FULLTEXT INDEX <索引的名字> ON tablename (字段名);

ALTER TABLE tablename ADD FULLTEXT [索引的名字] (字段名);

CREATE TABLE tablename ( [...], FULLTEXT KEY [索引的名字] (字段名) ;

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

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

全文索引使用注意事项:

  • 全文索引必须在字符串、文本字段上建立。
  • 全文索引字段值必须在最小字符和最大字符之间的才会有效。(innodb:3-84;myisam:4-84)
  • 全文索引字段值要进行切词处理,按syntax字符进行切割,例如b+aaa,切分成b和aaa
  • 全文索引匹配查询,默认使用的是等值匹配,例如a匹配a,不会匹配ab,ac。如果想匹配可以在布尔模式下搜索a*
select * from user  where match(name) against('a*' in boolean mode);

2.2 索引原理

MySQL官方对索引定义:是存储引擎用于快速查找记录的一种数据结构。需要额外开辟空间和数据维护工作。

  • 索引是物理数据页存储,在数据文件中(InnoDB,ibd文件),利用数据页(page)存储。
  • 索引可以加快检索速度,但是同时也会降低增删改操作速度,索引维护需要代价。
2.2.1 二分查找法

二分查找法也叫作折半查找法,它是在有序数组中查找指定数据的搜索算法。它的优点是等值查询、范围查询性能优秀,缺点是更新数据、新增数据、删除数据维护成本高。

  • 首先定位left和right两个指针
  • 计算(left+right)/2
  • 判断除2后索引位置值与目标值的大小比对
  • 索引位置值大于目标值就-1,right移动;如果小于目标值就+1,left移动
2.2.2 Hash结构

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

截屏2023-05-06 18.20.18

从上面结构可以看出,Hash索引可以方便的提供等值查询,但是对于范围查询就需要全表扫描了。在MySQL 中Hash结构主要应用在Memory原生的Hash索引 、InnoDB 自适应哈希索引。

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

InnoDB自适应哈希索引:在使用Hash索引访问时,一次性查找就能定位数据,等值查询效率要优于B+Tree。

自适应哈希索引的建立使得InnoDB存储引擎能自动根据索引页访问的频率和模式自动地为某些热点页建立哈希索引来加速访问。另外InnoDB自适应哈希索引的功能,用户只能选择开启或关闭功能,无法进行人工干涉。

show engine innodb status; 

show variables like '%innodb_adaptive%';
2.2.3 B+Tree结构

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

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

截屏2023-05-09 10.23.31

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

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

截屏2023-05-09 10.24.36

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

2.2.4 聚簇索引和辅助索引
  • 聚簇索引和非聚簇索引:B+Tree的叶子节点存放主键索引值和行记录就属于聚簇索引;如果索引值和行记录分开存放就属于非聚簇索引。
  • 主键索引和辅助索引:B+Tree的叶子节点存放的是主键字段值就属于主键索引;如果存放的是非主键值就属于辅助索引(二级索引)。

在InnoDB引擎中,主键索引采用的就是聚簇索引结构存储。

  • 聚簇索引(聚集索引)

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

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

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

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

非聚簇索引

与InnoDB表存储不同,MyISAM数据表的索引文件和数据文件是分开的,被称为非聚簇索引结构。

截屏2023-05-09 10.25.54

2.3 索引分析与优化

EXPLAIN

https://blog.csdn.net/DingKG/article/details/103461298?spm=1001.2014.3001.5501

2.3.1 回表查询

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

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

2.3.2 覆盖索引

在SQL-Server官网的介绍如下:

截屏2023-05-10 17.48.54

在MySQL官网,类似的说法出现在explain查询计划优化章节,即explain的输出结果Extra字段为Using index时,能够触发索引覆盖

截屏2023-05-10 17.49.05

不管是SQL-Server官网,还是MySQL官网,都表达了:只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快,这就叫做索引覆盖

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

2.3.3 NULL查询

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

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

2.3.4 索引与排序

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

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

  • 双路排序:需要两次磁盘扫描读取,最终得到用户数据。第一次将排序字段读取出来,然后排序;第二次去读取其他字段数据。
  • 单路排序:从磁盘查询所需的所有列数据,然后在内存排序将结果返回。如果查询数据超出缓存sort_buffer,会导致多次磁盘读取操作,并创建临时表,最后产生了多次IO,反而会增加负担。解决方案:少使用select *;增加sort_buffer_size容量和max_length_for_sort_data容量。

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

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

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

    explain select id from user order by id; //对应(id)、(id,name)索引有效 
    
  • WHERE子句+ORDER BY子句索引列组合满足索引最左前列

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

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

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

    explain select id from user order by age asc,name desc; //对应 (age,name)索引 
    
  • WHERE子句和ORDER BY子句满足最左前缀,但where子句使用了范围查询(例如>、<、in等)

    explain select id from user where age>10 order by name; //对应 (age,name)索引 
    
  • ORDER BY或者WHERE+ORDER BY索引列没有满足索引最左前列

    explain select id from user order by name; //对应(age,name)索引 
    
  • 使用了不同的索引,MySQL每次只采用一个索引,ORDER BY涉及了两个索引

    explain select id from user order by name,age; //对应(name)、(age)两个索 引
    
  • WHERE子句与ORDER BY子句,使用了不同的索引

    explain select id from user where name='tom' order by age; //对应 (name)、(age)索引
    
  • WHERE子句或者ORDER BY子句中索引列使用了表达式,包括函数表达式

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

2.4 查询优化

2.4.1 慢查询定位
  • 开启慢查询日志

    查看 MySQL 数据库是否开启了慢查询日志和慢查询日志文件的存储位置的命令如下:

    SHOW VARIABLES LIKE 'slow_query_log%' 
    

    通过如下命令开启慢查询日志:

    SET global slow_query_log = ON; 
    
    SET global slow_query_log_file = 'OAK-slow.log'; 
    
    SET global log_queries_not_using_indexes = ON; 
    
    SET long_query_time = 10;
    
    • long_query_time:指定慢查询的阀值,单位秒。如果SQL执行时间超过阀值,就属于慢查询记录到日志文件中。

    • log_queries_not_using_indexes:表示会记录没有使用索引的查询SQL。前提是slow_query_log的值为ON,否则不会奏效。

  • 查看慢查询日志

    • 文本方式查看

      直接使用文本编辑器打开slow.log日志即可。

      截屏2023-05-10 18.43.56

      • time:日志记录的时间

      • User@Host:执行的用户及主机

      • Query_time:执行的时间

      • Lock_time:锁表时间

      • Rows_sent:发送给请求方的记录数,结果数量

      • Rows_examined:语句扫描的记录条数

      • SET timestamp:语句执行的时间点

      • select…:执行的具体的SQL语句

    • 使用mysqldumpslow查看

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

      在 MySQL bin目录下执行下面命令可以查看该使用格式。

      perl mysqldumpslow.pl --help
      

      运行如下命令查看慢查询日志信息:

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

    除了使用mysqldumpslow工具,也可以使用第三方分析工具,比如pt-query-digest、mysqlsla等。

2.4.2 慢查询优化
  • 索引和慢查询

    • 如何判断是否为慢查询?

      MySQL判断一条语句是否为慢查询语句,主要依据SQL语句的执行时间,它把当前语句的执行时间跟 long_query_time 参数做比较,如果语句的执行时间 > long_query_time,就会把这条执行语句记录到慢查询日志里面。long_query_time 参数的默认值是 10s,该参数值可以根据自己的业务需要进行调整。

    • 如何判断是否应用了索引?

      SQL语句是否使用了索引,可根据SQL语句执行过程中有没有用到表的索引,可通过 explain命令分析查看,检查结果中的 key 值,是否为NULL。

    • 应用了索引是否一定快?

      select * from user where id>0;
      

      虽然使用了索引,但是还是从主键索引的最左边的叶节点开始向右扫描整个索引树,进行了全表扫描,此时索引就失去了意义。

      而像 select * from user where id = 2; 这样的语句,才是我们平时说的使用了索引。它表示的意思是,我们使用了索引的快速搜索功能,并且有效地减少了扫描行数。

    查询是否使用索引,只是表示一个SQL语句的执行过程;而是否为慢查询,是由它执行的时间决定的,也就是说是否使用了索引和是否是慢查询两者之间没有必然的联系

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

  • 提高索引过滤性

    如果在通过索引字段筛选后还有很大的数据量那SQL执行速度也不会很快。这个涉及到索引的过滤性,比如1万条记录利用索引过滤后定位10条、100条、1000条,那他们过滤性是不同的。索引过滤性与索引字段、表的数据量、表设计结构都有关系

  • 慢查询原因总结

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

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

    SELECT * FROM 表名 LIMIT [offset,] rows
    
    • 第一个参数指定第一个返回记录行的偏移量,注意从0开始;
    • 第二个参数指定返回记录行的最大数目;
    • 如果只给定一个参数,它表示返回的最大记录行数目;

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

    在查询记录时,如果查询偏移量变化,查询记录量相同,偏移量超过100后就开始随着偏移量增大,查询时间急剧的增加。(这种分页查询机制,每次都会从数据库第一条记录开始扫描,越往后查询越慢,而且查询的数据越多,也会拖慢总查询速度。)

  • 分页优化方案

    • 利用覆盖索引优化

      select * from user limit 10000,100; 
      
      select id from user limit 10000,100; 
      
    • 利用子查询优化

      select * from user limit 10000,100; 
      
      select * from user where id>= (select id from user limit 10000,1) limit 100;
      
      //使用了id做主键比较(id>=),并且子查询使用了覆盖索引进行优化。
      

三.MySQL事务和锁

3.1 ACID

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

  • 原子性

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

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

    • 事务提交了,如果 Buffer Pool 中的脏页没有刷成功,此时数据库挂了,那在数据库再次启动之后,可以通过 Redo log将其恢复出来,以保证脏页写的数据不会丢失。
    • 如果事务没提交,如果脏页刷新成功,此时数据库挂了,就需要通过Undo log来实现了。
  • 一致性

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

  • 约束一致性:创建表结构时所指定的外键、唯一索引等约束

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

    一致性也可以理解为数据的完整性。数据的完整性是通过原子性、隔离性、持久性来保证的,而这3个特性又是通过 Redo/Undo 来保证的。

    截屏2023-05-11 11.57.10

  • 隔离性

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

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

  • 持久性

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

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

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

截屏2023-05-11 11.52.40

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

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

截屏2023-05-11 11.58.13

3.2 事务控制

3.2.1 并发事务

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

  • 更新丢失

    当两个或多个事务更新同一行记录,会产生更新丢失现象。可以分为回滚覆盖和提交覆盖。

    • 回滚覆盖:一个事务回滚操作,把其他事务已提交的数据给覆盖了。
    • 提交覆盖:一个事务提交操作,把其他事务已提交的数据给覆盖了。
  • 脏读

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

  • 不可重复读

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

  • 幻读

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

3.2.2 排队

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

截屏2023-05-12 10.38.34

3.2.3 排他锁

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

截屏2023-05-12 10.44.37

注意,在整个事务1结束之前,锁是不会被释放的,所以,事务2必须等到事务1结束之后开始

3.2.4 读写锁

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

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

截屏2023-05-12 11.10.52

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

3.2.5 MVCC

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

截屏2023-05-12 11.12.49

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

3.2.5.1 MVCC概念

MVCC(Multi Version Concurrency Control)被称为多版本控制,是指在数据库中为了实现高并发的数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本。多版本控制很巧妙地将稀缺资源的独占互斥转换为并发,大大提高了数据库的吞吐量及读写性能。

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

3.2.5.2 MVCC实现原理

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

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

  • 快照读:读取的是记录的快照版本(有可能是历史版本),不用加锁。(select)

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

举一个记录更新的案例来讲解 MVCC 中多版本的实现。假设 F1~F6 是表中字段的名字,1~6 是其对应的数据。后面三个隐含字段分别对应该行的隐含ID、事务号和回滚指针,如下图所示。

截屏2023-05-12 11.15.37

具体的更新过程如下:

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

截屏2023-05-12 11.16.02

  • 用排他锁锁定该行;记录 Redo log;

  • 把该行修改前的值复制到 Undo log;

  • 修改当前行的值,填写事务编号,使回滚指针指向 Undo log 中修改前的行。

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

截屏2023-05-12 11.16.24

MVCC已经实现了读读、读写、写读并发处理,如果想进一步解决写写冲突,可以采用下面两种方案:

  • 乐观锁

  • 悲观锁

3.3 事务隔离级别

3.3.1 隔离级别类型

更新丢失、脏读、不可重复读和幻读等并发事务问题,其实都是数据库一致性问题,为了解决这些问题,MySQL数据库是通过事务隔离级别来解决的,数据库系统提供了以下 4 种事务隔离级别供用户选择。

隔离级别 脏读 幻读 不可重复读 回滚覆盖 提交覆盖
READ UNCOMMITED(读未提交) 允许 允许 允许 不允许 允许
READ COMMITTED(读已提交) 不允许 允许 允许 不允许 允许
REPEATABLE READ(可重复读) 不允许 允许 不允许 不允许 不允许
SERIALIZABLE (串行化) 不允许 不允许 不允许 不允许 不允许
  • 读未提交

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

  • 已提交读

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

  • 可重复度

    解决了不可重复读,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上会出现幻读,简单的说幻读指的的当用户读取某一范围的数据行时,另一个事务又在该范围插入了新行,当用户在读取该范围的数据时会发现有新的幻影行。

  • 可串行化

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

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

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

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

事务隔离级别和锁的关系

  • 事务隔离级别是SQL92定制的标准,相当于事务并发控制的整体解决方案,本质上是对锁和MVCC使用的封装,隐藏了底层细节。
  • 锁是数据库实现并发控制的基础,事务隔离性是采用锁来实现,对相应操作加不同的锁,就可以防止其他事务同时对数据进行读写操作。
  • 对用户来讲,首先选择使用隔离级别,当选用的隔离级别不能解决并发问题或需求时,才有必要在开发中手动的设置锁。
3.3.2 隔离级别控制

MySQL默认的事务隔离级别是Repeatable Read,查看MySQL当前数据库的事务隔离级别命令如

show variables like 'tx_isolation';
//或
select @@tx_isolation;

设置事务隔离级别可以如下命令:

set tx_isolation='READ-UNCOMMITTED'; 

set tx_isolation='READ-COMMITTED'; 

set tx_isolation='REPEATABLE-READ'; 

set tx_isolation='SERIALIZABLE';

3.4 锁机制

3.4.1 锁分类

在 MySQL中锁有很多不同的分类。

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

  • 表级锁:每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、InnoDB、BDB 等存储引擎中。
  • 行级锁:每次操作锁住一行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB 存储引擎中。
  • 页级锁:每次锁定相邻的一组记录,锁定粒度界于表锁和行锁之间,开销和加锁时间界于表锁和行锁之间,并发度一般。应用在BDB 存储引擎中。

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

  • 读锁(S锁):共享锁(select…lock in share mode),针对同一份数据,多个读操作可以同时进行而不会互相影响。事务A对记录添加了S锁,可以对记录进行读操作,不能做修改,其他事务可以对该记录追加S锁,但是不能追加X锁,需要追加X锁,需要等记录的S锁全部释放。
  • 写锁(X锁):排他锁(select…for update),当前写操作没有完成前,它会阻断其他写锁和读锁。事务A对记录添加了X锁,可以对记录进行读和修改操作,其他事务不能对记录做读和修改操作。
  • IS锁、IX锁:意向读锁、意向写锁,属于表级锁,S和X主要针对行级锁。在对表记录添加S或X锁之前,会先对表添加IS或IX锁。

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

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

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

3.4.2 行锁原理

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

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

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

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

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

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

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

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

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

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

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

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

  • 主键加锁

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

截屏2023-05-12 15.53.59

  • 唯一键加锁

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

截屏2023-05-12 15.53.47

  • 非唯一键(普通索引)加锁

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

截屏2023-05-12 15.53.34

  • 无索引加锁

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

截屏2023-05-12 15.53.17

3.4.3 悲观锁

是指在数据处理过程,将数据处于锁定状态,一般使用数据库的锁机制实现。行锁、表锁、读锁、写锁、共享锁、排他锁等,这些都属于悲观锁范畴。

  • 表级锁

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

    • 手动增加表锁

      lock table 表名称 read|write,表名称2 read|write; 
      
    • 查看表上加过的锁

      show open tables; 
      
    • 删除表锁

      unlock tables;
      

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

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

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

  • 共享锁(行级锁-读锁)

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

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

  • 排他锁(行级锁-写锁)

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

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

    总结:事务使用了排他锁(写锁),当前事务可以读取和修改,其他事务不能修改,也不能获取记录锁(select… for update)。如果查询没有使用到索引,将会锁住整个表记录。

3.4.4 乐观锁

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

乐观锁实现的关键点:冲突的检测。

悲观锁和乐观锁都可以解决事务写写并发,在应用中可以根据并发处理能力选择区分,比如对并发率要求高的选择乐观锁;对于并发率要求低的可以选择悲观锁。

  • 乐观锁实现原理

    • 使用版本字段(version)

      先给数据表增加一个版本(version) 字段,每操作一次,将那条记录的版本号加 1。version是用来查看被读的记录有无变化,作用是防止记录在业务处理期间被其他事务修改

      截屏2023-05-12 17.40.31

    • 使用时间戳(Timestamp)

      与使用version版本字段相似,同样需要给在数据表增加一个字段,字段类型使用timestamp时间戳。也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则提交更新,否则就是版本冲突,取消操作。

3.4.5 死锁与解决方案
  • 表锁死锁

    • 产生原因:

      用户A访问表A(锁住了表A),然后又访问表B;另一个用户B访问表B(锁住了表B),然后企图访问表A;这时用户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:

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

      截屏2023-05-12 17.49.25

    • 解决方案2:

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

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

  • 共享锁转换为排他锁

    • 产生原因:

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

      事务A: select * from dept where deptno=1 lock in share mode; //共享锁,1

      update dept set dname=‘java’ where deptno=1;//排他锁,3

      事务B: update dept set dname=‘Java’ where deptno=1;//由于1有共享锁,没法获取排他锁,需等待,2

    • 解决方案:

      • 对于按钮等控件,点击立刻失效,不让用户重复点击,避免引发同时对同一条记录多次操作;
      • 使用乐观锁进行控制。乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统性能。需要注意的是,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中;
  • 死锁排查

    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:从系统启动到现在锁定总时间长度
      • Innodb_row_lock_time_avg: 每次等待锁的平均时间
      • Innodb_row_lock_time_max:从系统启动到现在等待最长的一次锁的时间
      • Innodb_row_lock_waits:系统启动后到现在总共等待的次数

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

四.集群架构

4.1 集群架构设计

4.1.1 架构设计理念

在集群架构设计时,主要遵从下面三个维度:

  • 可用性

  • 扩展性

  • 一致性

4.1.2 可用性设计
  • 站点高可用,冗余站点

  • 服务高可用,冗余服务

  • 数据高可用,冗余数据

保证高可用的方法是冗余。但是数据冗余带来的问题是数据一致性问题。

实现高可用的方案有以下几种架构模式:

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

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

4.1.3 扩展性设计

扩展性主要围绕着读操作扩展和写操作扩展展开。

  • 如何扩展以提高读性能

    • 加从库

      简单易操作,方案成熟。

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

    • 分库分表

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

  • 如何扩展以提高写性能

    分库分表

4.1.4 一致性设计

一致性主要考虑集群中各数据库数据同步以及同步延迟问题。可以采用的方案如下:

  • 不使用从库

    扩展读性能问题需要单独考虑,否则容易出现系统瓶颈。

  • 增加访问路由层

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

4.2 主从模式

MySQL主从模式是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL 默认采用异步复制方式,,从节点可以复制主数据库中的所有数据库,或者特定的数据库,或者特定的表。

截屏2023-05-15 11.31.54

  • mysql主从复制用途:

    • 实时灾备,用于故障切换(高可用)
    • 读写分离,提供查询服务(读扩展)
    • 数据备份,避免影响业务(高可用)
  • 主从部署必要条件:

    • 主从服务器能连通
    • 主库开启binlog日志(设置log-bin参数)
    • 主从server-id不同
4.2.1 实现原理
4.2.1.1 主从复制
  • 主库将数据库的变更操作记录到Binlog日志文件中

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

  • 从库读取中继日志信息在从库中进行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中内容在从库上执行。

截屏2023-05-15 18.57.23

截屏2023-05-15 19.00.02

mysql主从复制存在的问题:

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

解决方法:

  • 半同步复制

    解决数据丢失的问题

  • 并行复制

    解决从库复制延迟的问题

4.2.1.2 半同步复制

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

MySQL 事务写入碰到主从复制时事务写入分为 4个步骤:

  • InnoDB Redo File Write (Prepare Write):写Redo log
  • Binlog File Flush & Sync to Binlog File:写Binlog
  • InnoDB Redo File Commit(Commit Write):提交事务
  • Send Binlog to Slave:异步发送Binlog

当Master不需要关注Slave是否接受到Binlog Event时,即为传统的主从复制。

当Master需要在第三步等待Slave返回ACK时,即为 after-commit,半同步复制(MySQL 5.5引入)。

当Master需要在第二步等待 Slave 返回 ACK 时,即为 after-sync,增强半同步(MySQL 5.7引入)。

  • 半同步复制

    Mysql5.5开始推出半同步复制,相比异步复制,半同步复制提高了数据完整性,在master的dump线程通知slave后,增加了一个ack(消息确认)这一个步骤。即binlog先在引擎层提交,然后再等待slave反馈收到relay log,只有收到ack后master才将commit ok结果反馈给客户端。

    会出现幻读,当引擎层已经commit的时候他还需要等待slave的ACK确认,才在客户端返回commit ok,但是在写入数据后并且从库确认之前,主库的其他客户端是可以看到这一条记录的,这就造成了幻读。

  • 增强半同步半同步复制

    master将每个事务写入binlog,传递给slave刷新到磁盘(relay log)。master等待slave反馈接收到relay log的ack之后,再在引擎层提交事务并返回commit ok结果返回给客户端。即使主库crash,所有在主库上已经提交的事务都能保证已经同步到slave的relay log中,即确保已经在relay log中了。

    截屏2023-05-16 14.56.13

半同步复制通俗的讲就是主库确认从库接受到binlog后再返回事务提交的信息,早期的半同步复制实现的方式是在engine commit(对应二阶段提交中给redo打commit标记)后去等待从库的确认信息,在innodb引擎中,engine commit后事务做的修改其他会话就能读到了,这时如果其他会话读到了这个修改的事务,但是从库没有收到事务对应的binlog,主库突然crash掉发生了主备切换,再去做读操作的时候就会发现刚刚读到的这条数据读不到了,产生了幻读。因此后面修改了半同步复制的实现方式,主库在binlog写入之后,engine commit之前去进行从库接受日志的确认,若未收到确认信息,存储引擎层就不会将事务提交,这样就避免了早期半同步复制幻读的问题,这种半同步复制的实现方式就是增强半同步。

4.2.2 并行复制

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

在从库中有两个线程IO Thread和SQL Thread,都是单线程模式工作,因此有了延迟问题,我们可以采用多线程机制来加强,减少从库复制延迟。(IO Thread多线程意义不大,主要指的是SQL Thread多线程)

在MySQL的5.6、5.7、8.0版本上,都是基于上述SQL Thread多线程思想,不断优化,减少复制延迟。

  • 5.6并行复制原理

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

    基于库的并行复制,实现相对简单,使用也相对简单些。基于库的并行复制遇到单库多表使用场景就发挥不出优势了,另外对事务并行处理的执行顺序也是个大问题。

    截屏2023-05-16 15.13.59

  • 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(基于组提交的并行复制方式)。

    • 实现的方案

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

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

      截屏2023-05-19 14.55.55

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

  • 8.0并行复制原理

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

    并行复制配置与调优

    slave-parallel-type=LOGICAL_CLOCK 
    
    slave-parallel-workers=16 
    
    slave_pending_jobs_size_max = 2147483648 
    
    slave_preserve_commit_order=1 
    
    master_info_repository=TABLE 
    
    relay_log_info_repository=TABLE 
    
    relay_log_recovery=ON
    
    • binlog_transaction_dependency_history_size

      用于控制集合变量的大小。

    • binlog_transaction_depandency_tracking

      用于控制binlog文件中事务之间的依赖关系,即last_committed值。

      • COMMIT_ORDERE: 基于组提交机制
      • WRITESET: 基于写集合机制
      • WRITESET_SESSION: 基于写集合,比writeset多了一个约束,同一个session中的事务last_committed按先后顺序递增
    • 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。

  • 并行复制监控

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

    mysql> show tables like 'replication%'; 
    
    +---------------------------------------------+ 
    
    | Tables_in_performance_schema (replication%) | 
    
    +---------------------------------------------+ 
    
    | replication_applier_configuration | 
    
    | replication_applier_status | 
    
    | replication_applier_status_by_coordinator | 
    
    | replication_applier_status_by_worker | 
    
    | replication_connection_configuration | 
    
    | replication_connection_status | 
    
    | replication_group_member_stats | 
    
    | replication_group_members | 
    
    +---------------------------------------------+
    

    通过replication_applier_status_by_worker可以看到worker进程的工作情况:

    mysql> select * from replication_applier_status_by_worker; 
    
    +--------------+-----------+-----------+---------------+------------------------ 
    
    --------------------+-------------------+--------------------+------------------ 
    
    ----+ 
    
    | CHANNEL_NAME | WORKER_ID | THREAD_ID | SERVICE_STATE | LAST_SEEN_TRANSACTION 
    
    | LAST_ERROR_NUMBER | LAST_ERROR_MESSAGE | 
    
    LAST_ERROR_TIMESTAMP | 
    
    +--------------+-----------+-----------+---------------+------------------------ 
    
    --------------------+-------------------+--------------------+------------------ 
    
    ----+ 
    
    | | 1 | 32 | ON | 0d8513d8-00a4-11e6- 
    
    a510-f4ce46861268:96604 | 0 | | 0000-00-00 
    
    00:00:00 | 
    
    | | 2 | 33 | ON | 0d8513d8-00a4-11e6- 
    
    a510-f4ce46861268:97760 | 0 | | 0000-00-00 
    
    00:00:00 | 
    
    +--------------+-----------+-----------+---------------+------------------------ 
    
    --------------------+-------------------+--------------------+------------------ 
    
    ----+ 
    
    2 rows in set (0.00 sec)
    

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

4.2.3 读写分离

如果我们已经优化了SQL,但是读依旧还是瓶颈时,这时就可以选择“读写分离”架构了。

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

截屏2023-05-19 17.12.22

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

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

4.2.3.1 主从同步延迟

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

  • 写后立刻读

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

  • 二次查询

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

  • 根据业务特殊处理

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

4.2.3.2读写路由分配

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

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

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

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

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

截屏2023-05-19 17.27.20

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

  • MySQL Proxy:是官方提供的MySQL中间件产品可以实现负载平衡、读写分离等。
  • MyCat:MyCat是一款基于阿里开源产品Cobar而研发的,基于 Java 语言编写的开源数据库中间件。
  • ShardingSphere:ShardingSphere是一套开源的分布式数据库中间件解决方案,它由Sharding JDBC、Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成。已经在2020年4月16日从Apache孵化器毕业,成为Apache顶级项目。
  • Atlas:Atlas是由 Qihoo 360公司Web平台部基础架构团队开发维护的一个数据库中间件。
  • Amoeba:变形虫,该开源框架于2008年开始发布一款 Amoeba for MySQL软件。

4.3 双主模式

主从模式下如果单主发生单点故障,从库切换成主库还需要作改动。因此,如果是双主或者多主,就会增加MySQL入口,提升了主库的可用性。

双主模式是指两台服务器互为主从,任何一台服务器数据变更,都会通过复制应用到另外一方的数据库中。

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

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

  • ID冲突

    在A主库写入,当A数据未同步到B主库时,对B主库写入,如果采用自动递增容易发生ID主键的冲突。

    可以采用MySQL自身的自动增长步长来解决,例如A的主键为1,3,5,7…,B的主键为2,4,6,8… ,但是对数据库运维、扩展都不友好。

  • 更新丢失

    同一条记录在两个主库中进行更新,会发生前面覆盖后面的更新丢失。

随着业务发展,架构会从主从模式演变为双主模式,建议用双主单写,再引入高可用组件,例如Keepalived和MMM等工具,实现主库故障自动切换。

高可用架构如下图所示,其中一个Master提供线上服务,另一个Master作为备胎供高可用切换,Master下游挂载Slave承担读请求。

截屏2023-05-19 17.55.04

4.3.1 MMM架构

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

截屏2023-05-19 18.03.49

  • 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)、指向新同步节点。
4.3.2 MHA架构

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

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

截屏2023-05-19 18.13.19

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
  • 用最新slave的relay log修复其它slave
  • 将保存下来的binlog在最新的slave上恢复
  • 将最新的slave提升为master
  • 将其它slave重新指向新提升的master,并开启主从复制

MHA优点:

  • 自动故障转移快
  • 主库崩溃不存在数据一致性问题
  • 性能优秀,支持半同步复制和异步复制
  • 一个Manager监控节点可以监控多个集群
4.3.3 主备切换

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

  • 主备延迟问题

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

    • 主库 A 执行完成一个事务,写入 binlog,我们把这个时刻记为 T1;
    • 之后将binlog传给备库 B,我们把备库 B 接收完 binlog 的时刻记为 T2;
    • 备库 B 执行完成这个binlog复制,我们把这个时刻记为 T3。

    所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是 T3-T1。在备库上执行show slave status命令,它可以返回结果信息,seconds_behind_master表示当前备库延迟了多少秒。

    同步延迟主要原因如下

    • 备库机器性能问题

      机器性能差,甚至一台机器充当多个主库的备库。

    • 分工问题

      备库提供了读操作,或者执行一些后台分析处理的操作,消耗大量的CPU资源。

    • 大事务操作

      大事务耗费的时间比较长,导致主备复制时间长。比如一些大量数据的delete或大表DDL操作都可能会引发大事务。

  • 可靠性优先

    主备切换过程一般由专门的HA高可用组件完成,但是切换过程中会存在短时间不可用,因为在切换过程中某一时刻主库A和从库B都处于只读状态。如下图所示:

    截屏2023-05-19 18.28.51

    主库由A切换到B,切换的具体流程如下:

    • 判断从库B的Seconds_Behind_Master值,当小于某个值才继续下一步
    • 把主库A改为只读状态(readonly=true)
    • 等待从库B的Seconds_Behind_Master值降为 0
    • 把从库B改为可读写状态(readonly=false)
    • 把业务请求切换至从库B
  • 可用性优先

    不等主从同步完成, 直接把业务请求切换至从库B ,并且让 从库B可读写 ,这样几乎不存在不可用时间,但可能会数据不一致

    截屏2023-05-19 18.28.38

    如上图所示,在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的可用性依赖于同步延时的大小,同步延时越小,可用性就越高。

4.4 分库分表

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

4.4.1 拆分方式
  • 垂直拆分

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

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

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

    • 垂直拆分优点

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

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

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

    水平分表是将一张含有很多记录数的表水平切分,不同的记录可以分开保存,拆分成几张结构相同的表

    如果一张表中的记录数过多,那么会对数据库的读写性能产生较大的影响,虽然此时仍然能够正确地读写,但读写的速度已经到了业务无法忍受的地步,此时就需要使用水平分表来解决这个问题。

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

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

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

    • 水平拆分优点

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

      • 拆分规则难以抽象;
      • 跨库Join性能较差;
      • 分片事务的一致性难以解决;
      • 数据扩容的难度和维护量极大。

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

4.4.2 主键策略

在很多中小项目中,我们往往直接使用数据库自增特性来生成主键ID,这样确实比较简单。而在分库分表的环境中,数据分布在不同的数据表中,不能再借助数据库自增长特性直接生成,否则会造成不同数据表主键重复。下面介绍几种ID生成算法。

  • 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变种)

    既然UUID数据因毫无规律可言造成索引效率低下,影响了系统的性能,那么我们能不能通过组合的方式,保留UUID的前10个字节,用后6个字节表示UUID生成的时间(DateTime),这样我们将时间信息与UUID组合起来,在保留UUID的唯一性的同时增加了有序性,以此来提高索引效率。解决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。如下图所示:

截屏2023-05-25 16.13.03

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

  • 数据库ID表

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

    例如,下面DISTRIBUTE_ID就是我们创建要负责ID生成的表,结构如下

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

    当分布式集群环境中哪个应用需要获取一个全局唯一的分布式ID的时候,就可以使用代码连接这个数据库实例,执行如下SQL语句即可。

    insert into DISTRIBUTE_ID(createtime) values(NOW()); 
    
    select LAST_INSERT_ID();
    

    注意:

    • 这里的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为:

    A:1,6,11,16,21 
    
    B:2,7,12,17,22 
    
    C:3,8,13,18,23 
    
    D:4,9,14,19,24 
    
    E:5,10,15,20,25
    
4.4.3 分片策略

分片(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就可以确定数据所在的设备编号。该模式也称为离散分片

    优点:实现简单,数据分配比较均匀,不容易出现冷热不均,负荷不均的情况。

    缺点:扩容时会产生大量的数据迁移,比如从n台设备扩容到n+1,绝大部分数据需要重新分配和迁移。

  • 一致性哈希分片

    采用Hash取模的方式进行拆分,后期集群扩容需要迁移旧的数据。使用一致性Hash算法能够很大程度的避免这个问题,所以很多中间件的集群分片都会采用一致性Hash算法。

    一致性Hash是将数据按照特征值映射到一个首尾相接的Hash环上,同时也将节点(按照IP地址或者机器名Hash)映射到这个环上。对于数据,从数据在环上的位置开始,顺时针找到的第一个节点即为数据的存储节点。Hash环示意图与数据的分布如下:

截屏2023-05-25 17.13.07

一致性Hash在增加或者删除节点的时候,受到影响的数据是比较有限的,只会影响到Hash环相邻的节点,不会发生大规模的数据迁移。

4.4.4 扩容方案

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

横向扩展难度如下

  • 数据迁移问题

  • 分片规则改变

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

遇到上述问题时,可以使用以下两种方案:

  • 停机扩容

    • 停止所有对外服务
    • 新增n个数据库,然后写一个数据迁移程序,将原有x个库的数据导入到最新的y个库中。比如分片规则由%x变为%y;
    • 数据迁移完成,修改数据库服务配置,原来x个库的配置升级为y个库的配置
    • 重启服务,连接新库重新对外提供服务

    优点

    • 简单

    缺点

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

    适用场景

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

    数据库扩容的过程中,如果想要持续对外提供服务,保证服务的可用性,平滑扩容方案是最好的选择。

    平滑扩容就是将数据库数量扩容成原来的2倍,比如:由2个数据库扩容到4个数据库,具体步骤如下:

    • 新增2个数据库

    • 配置双主进行数据同步(先测试、后上线)

      截屏2023-05-25 18.12.59

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

      截屏2023-05-25 18.18.25

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

      截屏2023-05-25 18.21.20

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

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

    优点

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

    缺点

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

    适用场景

    • 大型网站
    • 对高可用要求高的服务

五.分库分表中间件

遇到的问题

  • 用户请求量太大

    单服务器TPS、内存、IO都是有上限的,需要将请求打散分布到多个服务器单库数据量太大

  • 单个数据库处理能力有限

    单库所在服务器的磁盘空间有限;单库上的操作IO有瓶颈

  • 单表数据量太大

    查询、插入、更新操作都会变慢,在加字段、加索引、机器迁移都会产生高负载,影响服务

解决方案

  • 垂直拆分

    • 垂直分库

      微服务架构时,业务切割得足够独立,数据也会按照业务切分,保证业务数据隔离,大大提升了数据库的吞吐能力

    • 垂直分表

      表中字段太多且包含大字段的时候,在查询时对数据库的IO、内存会受到影响,同时更新数据时,产生的binlog文件会很大,MySQL在主从同步时也会有延迟的风险

  • 水平拆分

    • 水平分表

      针对数据量巨大的单张表(比如订单表),按照规则把一张表的数据切分到多张表里面去。但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。

      • 规则

        • RANGE

          • 时间:按照年、月、日去切分。例如order_2020、order_202005、order_20200501
          • 地域:按照省或市去切分。例如order_beijing、order_shanghai、order_chengdu
          • 大小:从0到1000000一个表。例如1000001-2000000放一个表,每100万放一个表
        • HASH

          对某个字段进行hash取模。

    • 水平分库

      将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。 水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈

      水平分库时不跨库、不跨表,保证同一类的数据都在同一个服务器上面。数据在切分之前,需要考虑如何高效的进行数据获取,如果每次查询都要跨越多个节点,就需要谨慎使用。

  • 主键选择

    • UUID:本地生成,不依赖数据库,缺点就是作为主键性能太差

    • SNOWFLAKE:百度UidGenerator、美团Leaf、基于SNOWFLAKE算法实现

  • 数据一致性

    • 强一致性:XA协议

    • 最终一致性:TCC、saga、Seata

  • 数据库扩容

    • 成倍增加数据节点,实现平滑扩容

    • 成倍扩容以后,表中的部分数据请求已被路由到其他节点上面,可以清理掉

  • 业务层改造

    • 基于代理层方式:Mycat、Sharding-Proxy、MySQL Proxy

    • 基于应用层方式:Sharding-jdbc

  • 分库后面临的问题

    • 事务问题:数据分布在不同的服务器上,数据需要保障一致性。

    • 跨库跨表的join问题

      • 全局表(字典表):基础数据/配置数据,所有库都拷贝一份
      • 字段冗余:可以使用字段冗余就不用join查询了
      • 系统层组装:可以在业务层分别查询出来,然后组装起来,逻辑较复杂
    • 额外的数据管理负担和数据运算压力

      数据库扩容、维护成本变高

5.1 ShardingSphere

Apache ShardingSphere是一款开源的分布式数据库中间件组成的生态圈。它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(规划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、容器、云原生等各种多样化的应用场景。

ShardingSphere定位为关系型数据库中间件,旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库。

Sharding-JDBC:被定位为轻量级Java框架,在Java的JDBC层提供的额外服务,以jar包形式使用。

Sharding-Proxy:被定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版

本,用于完成对异构语言的支持。

Sharding-Sidecar:被定位为Kubernetes或Mesos的云原生数据库代理,以DaemonSet的形式代

理所有对数据库的访问。

  • Sharding-JDBC:被定位为轻量级Java框架,在Java的JDBC层提供的额外服务,以jar包形式使用。

  • Sharding-Proxy:被定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。

  • Sharding-Sidecar:被定位为Kubernetes或Mesos的云原生数据库代理,以DaemonSet的形式代理所有对数据库的访问。

截屏2023-05-26 14.53.50

截屏2023-05-26 15.21.55

Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar三者区别如下:

Sharding-JDBC Sharding-Proxy Sharding-Sidecar
数据库 任意 Mysql Mysql
连接消耗数
异构语言 仅Java 任意 任意
性能 损耗低 损耗略高 损耗低
无中心化
静态入口
5.1.1 Sharding-JDBC

Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架的使用。

  • 适用于任何基于Java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
  • 基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
  • 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL。

截屏2023-05-26 18.08.37

Sharding-JDBC主要功能:

  • 数据分片

    • 分库、分表
    • 读写分离
    • 分片策略
    • 分布式主键
  • 分布式事务

    • 标准化的事务接口

    • XA强一致性事务

    • 柔性事务

  • 数据库治理

    • 配置动态化
    • 编排和治理
    • 数据脱敏
    • 可视化链路追踪

Sharding-JDBC 内部结构

截屏2023-05-26 18.12.18

  • 图中黄色部分表示的是Sharding-JDBC的入口API,采用工厂方法的形式提供。 目前有ShardingDataSourceFactory和MasterSlaveDataSourceFactory两个工厂类。

    • ShardingDataSourceFactory支持分库分表、读写分离操作
    • MasterSlaveDataSourceFactory支持读写分离操作
  • 图中蓝色部分表示的是Sharding-JDBC的配置对象,提供灵活多变的配置方式。ShardingRuleConfiguration是分库分表配置的核心和入口,它可以包含多个TableRuleConfiguration和MasterSlaveRuleConfiguration。

    • TableRuleConfiguration封装的是表的分片配置信息,有5种配置形式对应不同的Configuration类型。
    • MasterSlaveRuleConfiguration封装的是读写分离配置信息。
  • 图中红色部分表示的是内部对象,由Sharding-JDBC内部使用,应用开发者无需关注。Sharding-JDBC通过ShardingRuleConfiguration和MasterSlaveRuleConfiguration生成真正供ShardingDataSource和MasterSlaveDataSource使用的规则对象。ShardingDataSource和MasterSlaveDataSource实现了DataSource接口,是JDBC的完整实现方案。

Sharding-JDBC初始化流程:

  • 根据配置的信息生成Configuration对象

  • 通过Factory会将Configuration对象转化为Rule对象

  • 通过Factory会将Rule对象与DataSource对象封装

  • Sharding-JDBC使用DataSource进行分库分表和读写分离操作

Sharding-JDBC 使用过程

  • 引入maven依赖

    <dependency> 
    	<groupId>org.apache.shardingsphere</groupId> 
    	<artifactId>sharding-jdbc-core</artifactId> 
    	<version>${latest.release.version}</version> 
    </dependency> 
    
  • 规则配置

    Sharding-JDBC可以通过Java,YAML,Spring命名空间和Spring Boot Starter四种方式配置,开发者可根据场景选择适合的配置方式。

  • 创建DataSource

    通过ShardingDataSourceFactory工厂和规则配置对象获取ShardingDataSource,然后即可通过DataSource选择使用原生JDBC开发,或者使用JPA, MyBatis等ORM工具。

    DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap,shardingRuleConfig, props);
    
5.1.2 数据分片
5.1.2.1 概念
  • 表概念

    • 真实表

      数据库中真实存在的物理表。例如b_order0、b_order1

    • 逻辑表

      在分片之后,同一类表结构的名称(总成)。例如b_order。

    • 数据节点

      在分片之后,由数据源和数据表组成。例如ds0.b_order1

    • 绑定表

      指的是分片规则一致的关系表(主表、子表),例如b_order和b_order_item,均按照order_id分片,则此两个表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,可以提升关联查询效率。

      b_order:b_order0、b_order1 
      
      b_order_item:b_order_item0、b_order_item1 
      
      select * from b_order o join b_order_item i on(o.order_id=i.order_id) 
      
      where o.order_id in (10,11);
      

      如果不配置绑定表关系,采用笛卡尔积关联,会生成4个SQL

      select * from b_order0 o join b_order_item0 i on(o.order_id=i.order_id) where o.order_id in (10,11); 
      
      select * from b_order0 o join b_order_item1 i on(o.order_id=i.order_id) where o.order_id in (10,11); 
      
      select * from b_order1 o join b_order_item0 i on(o.order_id=i.order_id) where o.order_id in (10,11); 
      
      select * from b_order1 o join b_order_item1 i on(o.order_id=i.order_id) where o.order_id in (10,11);
      

      如果配置绑定表关系,生成2个SQL

      select * from b_order0 o join b_order_item0 i on(o.order_id=i.order_id) where o.order_id in (10,11); 
      
      select * from b_order1 o join b_order_item1 i on(o.order_id=i.order_id) where o.order_id in (10,11);
      
    • 广播表

      在使用中,有些表没必要做分片,例如字典表、省份信息等,因为他们数据量不大,而且这种表可能需要与海量数据的表进行关联查询。广播表会在不同的数据节点上进行存储,存储的表结构和数据完全相同。

  • 分片算法

    由于分片算法和业务实现紧密相关,因此并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。目前提供4种分片算法。

    • 精确分片算法 PreciseShardingAlgorithm

      用于处理使用单一键作为分片键的=与IN进行分片的场景。

    • 范围分片算法RangeShardingAlgorithm

      用于处理使用单一键作为分片键的BETWEEN AND、>、<、>=、<=进行分片的场景。

    • 复合分片算法ComplexKeysShardingAlgorithm

      用于处理使用多键作为分片键进行分片的场景,多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。

    • Hint分片算法HintShardingAlgorithm

      用于处理使用Hint行分片的场景。对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段。例:内部系统,按照员工登录主键分库,而数据库中并无此字段。SQL Hint支持通Java API和SQL注释两种方式使用

  • 分片策略

    分片策略包含分片键和分片算法,真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前提供5种分片策略。

    • 标准分片策略StandardShardingStrategy

      只支持单分片键,提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。提供精确分片算法(PreciseShardingAlgorithm)和范围分片算法(RangeShardingAlgorithm)两个分片算法。PreciseShardingAlgorithm是必选的,RangeShardingAlgorithm是可选的。但是SQL中使用了范围操作,如果不配置RangeShardingAlgorithm会采用全库路由扫描,效率低。

    • 复合分片策略ComplexShardingStrategy

      支持多分片键。提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。

    • 行表达式分片策略InlineShardingStrategy

      只支持单分片键。使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发。如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0到t_user_7。

    • Hint分片策略HintShardingStrategy

      通过Hint指定分片值而非从SQL中提取分片值的方式进行分片的策略。

    • 不分片策略NoneShardingStrategy

      不分片的策略。

  • 分片策略配置

    对于分片策略存有数据源分片策略和表分片策略两种维度,两种策略的API完全相同。

    • 数据源分片策略

      用于配置数据如何被分配

    • 表分片策略

      用于配置数据被分配的目标表

5.1.2.2 流程

截屏2023-05-29 16.13.31

  • SQL解析

    SQL解析分为词法解析和语法解析。 先通过词法解析器将SQL拆分为一个个不可再分的单词。再使用语法解析器对SQL进行理解,并最终提炼出解析上下文。

    Sharding-JDBC采用不同的解析器对SQL进行解析,解析器类型如下:

    • MySQL解析器
    • Oracle解析器
    • SQLServer解析器
    • PostgreSQL解析器
    • 默认SQL解析器
  • 查询优化

    负责合并和优化分片条件,如OR等。

  • SQL路由

    根据解析上下文匹配用户配置的分片策略,并生成路由路径。目前支持分片路由和广播路由。

  • SQL改写

    将SQL改写为在真实数据库中可以正确执行的语句。SQL改写分为正确性改写和优化改写。

  • SQL执行

    通过多线程执行器异步执行SQL。

  • 结果归并

    将多个执行结果集归并以便于通过统一的JDBC接口输出。结果归并包括流式归并、内存归并和使用装饰者模式的追加归并这几种方式。

5.1.2.3 规范
  • 支持项

    路由至单数据节点时,目前MySQL数据库100%全兼容,其他数据库完善中。路由至多数据节点时,全面支持DQL、DML、DDL、DCL、TCL。支持分页、去重、排序、分组、聚合、关联查询(不支持跨库关联)。以下用最为复杂的查询为例:

    SELECT select_expr [, select_expr ...] 
    
    FROM table_reference [, table_reference ...] 
    
    [WHERE predicates] 
    
    [GROUP BY {col_name | position} [ASC | DESC], ...] 
    
    [ORDER BY {col_name | position} [ASC | DESC], ...] 
    
    [LIMIT {[offset,] row_count | row_count OFFSET offset}]
    
  • 不支持项

    路由至多数据节点时不支持CASE WHEN、HAVING、UNION (ALL)

  • 部分支持子查询

    支持分页子查询,但其他子查询有限支持,无论嵌套多少层,只能解析至第一个包含数据表的子查询,一旦在下层嵌套中再次找到包含数据表的子查询将直接抛出解析异常。例如,以下子查询可以支持:

    SELECT COUNT(*) FROM (SELECT * FROM b_order o)
    

    以下子查询不支持:

    SELECT COUNT(*) FROM (SELECT * FROM b_order o WHERE o.id IN (SELECT id FROM b_order WHERE status = ?))
    

    简单来说,通过子查询进行非功能需求,在大部分情况下是可以支持的。比如分页、统计总数等;而通过子查询实现业务查询当前并不能支持。

  • 由于归并的限制,子查询中包含聚合函数目前无法支持

  • 不支持包含schema的SQL

    因为ShardingSphere的理念是像使用一个数据源一样使用多数据源,因此对SQL的访问都是在同一个逻辑schema之上。

  • 当分片键处于运算表达式或函数中的SQL时,将采用全路由的形式获取结果。

    例如下面SQL,create_time为分片键:

    SELECT * FROM b_order WHERE to_date(create_time, 'yyyy-mm-dd') = '2020-05-05';
    

    由于ShardingSphere只能通过SQL字面提取用于分片的值,因此当分片键处于运算表达式或函数中时,ShardingSphere无法提前获取分片键位于数据库中的值,从而无法计算出真正的分片值。

  • 分页查询

    完全支持MySQL和Oracle的分页查询,SQLServer由于分页查询较为复杂,仅部分支持

    • 性能瓶颈

      查询偏移量过大的分页会导致数据库获取数据性能低下,以MySQL为例:

      SELECT * FROM b_order ORDER BY id LIMIT 1000000, 10
      

      这句SQL会使得MySQL在无法利用索引的情况下跳过1000000条记录后,再获取10条记录,其性能可想而知。 而在分库分表的情况下(假设分为2个库),为了保证数据的正确性,SQL会改写为:

      SELECT * FROM b_order ORDER BY id LIMIT 0, 1000010
      

      即将偏移量前的记录全部取出,并仅获取排序后的最后10条记录。这会在数据库本身就执行很慢的情况下,进一步加剧性能瓶颈。 因为原SQL仅需要传输10条记录至客户端,而改写之后的SQL则会传输1,000,010 * 2的记录至客户端。

    • ShardingSphere的优化

      ShardingSphere进行了以下2个方面的优化。

      • 首先,采用流式处理 + 归并排序的方式来避免内存的过量占用。

      • 其次,ShardingSphere对仅落至单节点的查询进行进一步优化。

    • 分页方案优化

      由于LIMIT并不能通过索引查询数据,因此如果可以保证ID的连续性,通过ID进行分页是比较好的解决方案:

      SELECT * FROM b_order WHERE id > 1000000 AND id <= 1000010 ORDER BY id 
      

      或通过记录上次查询结果的最后一条记录的ID进行下一页的查询:

      SELECT * FROM b_order WHERE id > 1000000 LIMIT 10
      
  • 不支持的SQL示例

    INSERT INTO tbl_name (col1, col2, …) VALUES(1+2, ?, …) //VALUES语句不支持运算 表达式
    
    INSERT INTO tbl_name (col1, col2, …) SELECT col1, col2, … FROM tbl_name WHERE col3 = ? //INSERT .. SELECT 
    
    SELECT COUNT(col1) as count_alias FROM tbl_name GROUP BY col1 HAVING count_alias > ? //HAVING 
    
    SELECT * FROM tbl_name1 UNION SELECT * FROM tbl_name2 //UNION 
    
    SELECT * FROM tbl_name1 UNION ALL SELECT * FROM tbl_name2 //UNION ALL 
    
    SELECT * FROM ds.tbl_name1 //包含schema 
    
    SELECT SUM(DISTINCT col1), SUM(col1) FROM tbl_name //同时使用普通聚合函数 和DISTINCT 
    
    SELECT * FROM tbl_name WHERE to_date(create_time, ‘yyyy-mm-dd’) = ? //会导致全路由
    
5.1.2.4 其他
  • Inline行表达式

    InlineShardingStrategy:采用Inline行表达式进行分片的配置。

    Inline是可以简化数据节点和分片算法配置信息。主要是解决配置简化、配置一体化。

    行表达式的使用非常直观,只需要在配置中使用expression或{ expression }或expression->{ expression }标识行表达式即可。例如:

    ${begin..end} 表示范围区间 
    
    ${[unit1, unit2, unit_x]} 表示枚举值
    

    行表达式中如果出现多个或{}或->{}表达式,整个表达式结果会将每个子表达式结果进行笛卡尔(积)组合。例如,以下行表达式:

    ${['online', 'offline']}_table${1..3} 
    
    $->{['online', 'offline']}_table$->{1..3}
    

    最终会解析为:

    online_table1, online_table2, online_table3, offline_table1, offline_table2, offline_table3
    
    • 数据节点配置

      对于均匀分布的数据节点,如果数据结构如下:

      db0
      
      ├── b_order2 
      
      └── b_order1 
      
      db1
      
      ├── b_order2 
      
      └── b_order1
      

      用行表达式可以简化为:

      db${0..1}.b_order${1..2} 
      
      或者
      
      db$->{0..1}.b_order$->{1..2}
      

      对于自定义的数据节点,如果数据结构如下:

      db0
      
      ├── b_order0 
      
      └── b_order1 
      
      db1
      
      ├── b_order2 
      
      ├── b_order3 
      
      └── b_order4
      

      用行表达式可以简化为:

      db0.b_order${0..1},db1.b_order${2..4}
      
    • 分片算法配置

      行表达式内部的表达式本质上是一段Groovy代码,可以根据分片键进行计算的方式,返回相应的真实数据源或真实表名称。

      ds${id % 10} 
      或者
      ds$->{id % 10} 
      

      结果为:ds0、ds1、ds2… ds9

  • 分布式主键

    ShardingSphere不仅提供了内置的分布式主键生成器,例如UUID、SNOWFLAKE,还抽离出分布式主键生成器的接口,方便用户自行实现自定义的自增主键生成器。

    内置主键生成器

    • UUID

      采用UUID.randomUUID()的方式产生分布式主键。

    • SNOWFLAKE

      在分片规则配置模块可配置每个表的主键生成策略,默认使用雪花算法,生成64bit的长整型数据

    自定义主键生成器

    • 自定义主键类,实现ShardingKeyGenerator接口

    • 按SPI规范配置自定义主键类

      在Apache ShardingSphere中,很多功能实现类的加载方式是通过SPI注入的方式完成的。

      注意:在resources目录下新建META-INF文件夹,再新建services文件夹,然后新建文件的名字为org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator,打开文件,复制自定义主键类全路径到文件中保存。

    • 自定义主键类应用配置

      #id
      spring.shardingsphere.sharding.tables.position.key-generator.column=id
      spring.shardingsphere.sharding.tables.position.key-generator.type=MYTESTID
      
5.1.3 读写分离

读写分离是通过主从的配置方式,将查询请求均匀的分散到多个数据副本,进一步的提升系统的处理能力

截屏2023-05-29 17.43.06

主从架构:读写分离,目的是高可用、读写扩展。主从库内容相同,根据SQL语义进行路由。

分库分表架构:数据分片,目的读写扩展、存储扩容。库和表内容不同,根据分片配置进行路由。

将水平分片和读写分离联合使用,能够更加有效的提升系统性能, 下图展现了将分库分表与读写分离一同使用时,应用程序与数据库集群之间的复杂拓扑关系。

截屏2023-05-29 17.46.39

读写分离虽然可以提升系统的吞吐量和可用性,但同时也带来了数据不一致的问题,包括多个主库之间的数据一致性,以及主库与从库之间的数据一致性的问题。 并且,读写分离也带来了与数据分片同样的问题,它同样会使得应用开发和运维人员对数据库的操作和运维变得更加复杂。

读写分离应用方案

在数据量不是很多的情况下,我们可以将数据库进行读写分离,以应对高并发的需求,通过水平扩展从库,来缓解查询的压力。如下:

截屏2023-05-29 17.54.33

分表+读写分离

在数据量达到500万的时候,这时数据量预估千万级别,我们可以将数据进行分表存储

截屏2023-05-29 17.55.35

分库分表+读写分离

在数据量继续扩大,这时可以考虑分库分表,将数据存储在不同数据库的不同表中

透明化读写分离所带来的影响,让使用方尽量像使用一个数据库一样使用主从数据库集群,是ShardingSphere读写分离模块的主要设计目标。

主库、从库、主从同步、负载均衡

  • 核心功能

    • 提供一主多从的读写分离配置。仅支持单主库,可以支持独立使用,也可以配合分库分表使用

    • 独立使用读写分离,支持SQL透传。不需要SQL改写流程

    • 同一线程且同一数据库连接内,能保证数据一致性。如果有写入操作,后续的读操作均从主库读取。

    • 基于Hint的强制主库路由。可以强制路由走主库查询实时数据,避免主从同步数据延迟。

  • 不支持项

    • 主库和从库的数据同步
    • 主库和从库的数据同步延迟
    • 主库双写或多写
    • 跨主库和从库之间的事务的数据不一致。建议在主从架构中,事务中的读写均用主库操作。
5.1.4 强制路由

在一些应用场景中,分片条件并不存在于SQL,而存在于外部业务逻辑。因此需要提供一种通过在外部业务代码中指定路由配置的一种方式,在ShardingSphere中叫做Hint。如果使用Hint指定了强制分片路由,那么SQL将会无视原有的分片逻辑,直接路由至指定的数据节点操作。

HintManager主要使用ThreadLocal管理分片键信息,进行hint强制路由。在代码中向HintManager添加的配置信息只能在当前线程内有效。

Hint使用场景:

  • 数据分片操作,如果分片键没有在SQL或数据表中,而是在业务逻辑代码中

  • 读写分离操作,如果强制在主库进行某些数据操作

Hint使用过程:

  • 编写分库或分表路由策略,实现HintShardingAlgorithm接口

    public class MyHintShardingAlgorithm implements HintShardingAlgorithm<Long> {
        @Override
        public Collection<String> doSharding(
                Collection<String> availableTargetNames,
                HintShardingValue<Long> shardingValue) {
            System.out.println("availableTargetNames:"+availableTargetNames);
            System.out.println("shardingValue:"+shardingValue);
            Collection<String> result = new ArrayList<>();
            for (String each : availableTargetNames){
                for (Long value : shardingValue.getValues()){
                    if(each.endsWith(String.valueOf(value % 2))){
                        result.add(each);
                    }
                }
            }
            return result;
        }
    }
    
  • 在配置文件指定分库或分表策略

    spring.shardingsphere.sharding.tables.city.database-strategy.hint.algorithm-class-name=com.edu.mysql.hint.MyHintShardingAlgorithm
    
  • 在代码执行查询前使用HintManager指定执行策略值

    @Test
        public void test1(){
            HintManager hintManager = HintManager.getInstance();
            hintManager.setDatabaseShardingValue(2L); //强制路由到ds${xx%2}
            hintManager.setMasterRouteOnly();//强制主库路由
            List<City> list = cityRepository.findAll();
            list.forEach(city->{
                System.out.println(city.getId()+" "+city.getName()+" "+city.getProvince());
            });
        }
    

    在读写分离结构中,为了避免主从同步数据延迟及时获取刚添加或更新的数据,可以采用强制路由走主库查询实时数据,使用hintManager.setMasterRouteOnly设置主库路由即可。

5.1.5 数据脱敏

数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。涉及客户安全数据或者一些商业性敏感数据,如身份证号、手机号、卡号、客户号等个人信息按照规定,都需要进行数据脱敏。

数据脱敏模块属于ShardingSphere分布式治理这一核心功能下的子功能模块。

  • 在更新操作时,它通过对用户输入的SQL进行解析,并依据用户提供的脱敏配置对SQL进行改写,从而实现对原文数据进行加密,并将密文数据存储到底层数据库。
  • 在查询数据时,它又从数据库中取出密文数据,并对其解密,最终将解密后的原始数据返回给用户。

Apache ShardingSphere自动化&透明化了数据脱敏过程,让用户无需关注数据脱敏的实现细节,像使用普通数据那样使用脱敏数据。

5.1.5.1 整体架构

ShardingSphere提供的Encrypt-JDBC和业务代码部署在一起。业务方需面向Encrypt-JDBC进行JDBC编程。

截屏2023-05-30 11.12.16

Encrypt-JDBC将用户发起的SQL进行拦截,并通过SQL语法解析器进行解析、理解SQL行为,再依据用户传入的脱敏规则,找出需要脱敏的字段和所使用的加解密器对目标字段进行加解密处理后,再与底层数据库进行交互。

5.1.5.2 脱敏规则

脱敏配置主要分为四部分:数据源配置,加密器配置,脱敏表配置以及查询属性配置,其详情如下图所示:

截屏2023-05-30 11.14.08

  • 数据源配置

    指DataSource的配置信息

  • 加密器配置

    指使用什么加密策略进行加解密。目前ShardingSphere内置了两种加解密策略:AES/MD5

  • 脱敏表配置

    指定哪个列用于存储密文数据(cipherColumn)、哪个列用于存储明文数据(plainColumn)以及用户想使用哪个列进行SQL编写(logicColumn)

  • 查询属性的配置

    当底层数据库表里同时存储了明文数据、密文数据后,该属性开关用于决定是直接查询数据库表里的明文数据进行返回,还是查询密文数据通过Encrypt-JDBC解密后返回。

5.1.5.3 脱敏流程

ShardingSphere会将逻辑列与明文列和密文列进行了列名映射

截屏2023-05-30 11.25.41

使用Encrypt-JDBC进行增删改查时,其中的处理流程和转换逻辑,如下图所示

截屏2023-05-30 11.26.05

5.1.5.4 加密策略

ShardingSphere提供了两种加密策略用于数据脱敏,该两种策略分别对应ShardingSphere的两种加解密的接口,即Encryptor和QueryAssistedEncryptor。

  • Encryptor

    该解决方案通过提供encrypt(), decrypt()两种方法对需要脱敏的数据进行加解密。在用户进行INSERT, DELETE,UPDATE时,ShardingSphere会按照用户配置,对SQL进行解析、改写、路由,并会调用encrypt()将数据加密后存储到数据库, 而在SELECT时,则调用decrypt()方法将从数据库中取出的脱敏数据进行逆向解密,最终将原始数据返回给用户。

    当前,ShardingSphere针对这种类型的脱敏解决方案提供了两种具体实现类,分别是MD5(不可逆),AES(可逆),用户只需配置即可使用这两种内置的方案。

  • QueryAssistedEncryptor

    相比较于第一种脱敏方案,该方案更为安全和复杂。它的理念是:即使是相同的数据,如两个用户的密码相同,它们在数据库里存储的脱敏数据也应当是不一样的。这种理念更有利于保护用户信息,防止撞库成功。

    它提供三种函数进行实现,分别是encrypt(), decrypt(), queryAssistedEncrypt()。在encrypt()阶段,用户通过设置某个变动种子,例如时间戳。针对原始数据+变动种子组合的内容进行加密,就能保证即使原始数据相同,也因为有变动种子的存在,致使加密后的脱敏数据是不一样的。在decrypt()可依据之前规定的加密算法,利用种子数据进行解密。queryAssistedEncrypt()用于生成辅助查询列,用于原始数据的查询过程。

    当前,ShardingSphere针对这种类型的脱敏解决方案并没有提供具体实现类,却将该理念抽象成接口,提供给用户自行实现。ShardingSphere将调用用户提供的该方案的具体实现类进行数据脱敏。

5.1.6 分布式事务

5.1.6.1 理论

  • CAP(强一致性)

    CAP 定理,又被叫作布鲁尔定理。对于共享数据系统,最多只能同时拥有CAP其中的两个,任意两个都有其适应的场景。

    描述
    C 一致性,分布式环境中,数据在多个副本之间能够保持严格的一致性。当系统执行更新后系统仍然保持一致性
    A 可用性,系统一直处于可用状态,对于每一次请求都能返回非错的响应,但是不保证数据是最新的
    P 分区容错性,除非整个网络挂了,不然在遇到网络分区故障时仍然能对外提供满足一致性和可用性的服务
  • BASE(最终一致性)

    BASE 是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。它的核心思想是即使无法做到强一致性(CAP 就是强一致性),但应用可以采用适合的方式达到最终一致性。

    • BA指的是基本业务可用性,支持分区失败;
    • S表示柔性状态,也就是允许短时间内不同步;
    • E表示最终一致性,数据最终是一致的,但是实时是不一致的。

    原子性和持久性必须从根本上保障,为了可用性、性能和服务降级的需要,只有降低一致性和隔离性的要求。BASE 解决了 CAP 理论中没有考虑到的网络延迟问题,在BASE中用软状态和最终一致,保证了延迟后的一致性。

5.1.6.2 模式
2PC模式

2PC是Two-Phase Commit缩写,即两阶段提交,就是将事务的提交过程分为两个阶段来进行处理。事务的发起者称协调者,事务的执行者称参与者。协调者统一协调参与者执行。

  • 阶段 1:准备阶段

    • 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。

    • 各参与者执行事务操作,但不提交事务,将 undo 和 redo 信息记入事务日志中。

    • 如参与者执行成功,给协调者反馈 yes;如执行失败,给协调者反馈 no。

  • 阶段 2:提交阶段

    如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息。

2PC 方案实现起来简单,实际项目中使用比较少,主要因为以下问题:

  • 性能问题

    所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。

  • 可靠性问题

    如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。

  • 数据一致性问题

    在阶段 2 中,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。

3PC模式

3PC 三阶段提交,是两阶段提交的改进版本,与两阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制。三阶段提交将两阶段的准备阶段拆分为 2 个阶段,插入了一个preCommit 阶段,解决了原先在两阶段提交中,参与者在准备之后,由于协调者或参与者发生崩溃或错误,而导致参与者无法知晓处于长时间等待的问题。如果在指定的时间内协调者没有收到参与者的消息则默认失败。

  • 阶段1:canCommit

    协调者向参与者发送 commit 请求,参与者如果可以提交就返回 yes 响应,否则返回 no 响应。

  • 阶段2:preCommit

    协调者根据阶段 1 canCommit 参与者的反应情况执行预提交事务或中断事务操作。

    • 参与者均反馈 yes:协调者向所有参与者发出 preCommit 请求,参与者收到preCommit 请求后,执行事务操作,但不提交;将 undo 和 redo 信息记入事务日志中;各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。
    • 任何一个参与者反馈 no或等待超时:协调者向所有参与者发出 abort 请求,无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。
  • 阶段3:do Commit

    该阶段进行真正的事务提交,根据阶段 2 preCommit反馈的结果完成事务提交或中断操作。

相比2PC模式,3PC模式降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时(比如网络中断等),参与者会继续提交事务。

XA(强一致性)

XA是由X/Open组织提出的分布式事务的规范,是基于两阶段提交协议。 XA规范主要定义了全局事务管理器(TM)和局部资源管理器(RM)之间的接口。目前主流的关系型数据库产品都是实现了XA接口。

截屏2023-05-30 17.25.30

XA之所以需要引入事务管理器,是因为在分布式系统中,从理论上讲两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。由全局事务管理器管理和协调的事务,可以跨越多个资源(数据库)和进程。

事务管理器用来保证所有的事务参与者都完成了准备工作(第一阶段)。如果事务管理器收到所有参与者都准备好的消息,就会通知所有的事务都可以提交了(第二阶段)。MySQL 在这个XA事务中扮演的是参与者的角色,而不是事务管理器。

TCC模式(最终一致性)

TCC(Try-Confifirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。TCC 是服务化的两阶段编程模型,其 Try、Confirm、Cancel 3 个方法均由业务编码实现:

  • Try 操作作为一阶段,负责资源的检查和预留;
  • Confirm 操作作为二阶段提交操作,执行真正的业务;
  • Cancel 是预留资源的取消;

TCC事务模式相对于 XA 等传统模型如下图所示:

截屏2023-05-30 17.41.51

TCC 模式相比于 XA,解决了如下几个缺点:

  • 解决了协调者单点

    由主业务方发起并完成这个业务活动。业务活动管理器可以变成多点,引入集群。

  • 同步阻塞

    引入超时机制,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。

  • 数据一致性

    有了补偿机制之后,由业务活动管理器控制一致性。

消息队列模式(最终一致性)

消息队列的方案最初是由 eBay 提出,基于TCC模式,消息中间件可以基于 Kafka、RocketMQ 等消息队列。此方案的核心是将分布式事务拆分成本地事务进行处理,将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或MQ中间件,再通过业务规则人工发起重试。

下面描述下事务的处理流程:

截屏2023-05-30 17.46.12

  • 步骤1:事务主动方处理本地事务。

  • 步骤 2:事务主动方主动写消息到MQ,事务消费方接收并处理MQ中的消息。

  • 步骤 3:事务被动方通过MQ中间件,通知事务主动方事务已处理的消息,事务主动方根据反馈结果提交或回滚事务。

为了数据的一致性,当流程中遇到错误需要重试,容错处理规则如下:

  • 当步骤 1 处理出错,事务回滚,相当于什么都没发生。

  • 当步骤 2 处理出错,由于未处理的事务消息还是保存在事务发送方,可以重试或撤销本地业务操作。

  • 如果事务被动方消费消息异常,需要不断重试,业务处理逻辑需要保证幂等。

  • 如果是事务被动方业务上的处理失败,可以通过MQ通知事务主动方进行补偿或者事务回滚。

  • 如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。

Saga模式(最终一致性)

Saga这个概念源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的一篇数据库论文Sagas ,一个Saga事务是一个有多个短时事务组成的长时的事务。 在分布式事务场景下,我们把一个Saga分布式事务看做是一个由多个本地事务组成的事务,每个本地事务都有一个与之对应的补偿事务。在Saga事务的执行过程中,如果某一步执行出现异常,Saga事务会被终止,同时会调用对应的补偿事务完成相关的恢复操作,这样保证Saga相关的本地事务要么都是执行成功,要么通过补偿恢复成为事务执行之前的状态。(自动反向补偿机制)。

Saga 事务基本协议如下:

  • 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
  • 每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。

Saga是一种补偿模式,它定义了两种补偿策略

  • 向前恢复(forward recovery)

    对应于上面第一种执行顺序,发生失败进行重试,适用于必须要成功的场景。

  • 向后恢复(backward recovery)

    对应于上面提到的第二种执行顺序,发生错误后撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。

截屏2023-05-30 17.58.55

  • 事务正常执行完成

    T1, T2, T3, …, Tn,例如:减库存(T1),创建订单(T2),支付(T3),依次有序完成整个事务。

  • 事务回滚:T1, T2, …, Tn, Cj,…, C2, C1,其中 0 < j <= n,例如:减库存(T1),创建订单(T2),支付(T3),支付失败,支付回滚(C3),订单回滚(C2),恢复库存(C1)。

Seata框架

Fescar开源项目,最初愿景是能像本地事务一样控制分布式事务,解决分布式环境下的难题。

Seata(Simple Extensible Autonomous Transaction Architecture)是一套一站式分布式事务解决方案,是阿里集团和蚂蚁金服联合打造的分布式事务框架。Seata目前的事务模式有AT、TCC、Saga和XA,默认是AT模式,AT本质上是2PC协议的一种实现。

Seata AT事务模型包含TM(事务管理器),RM(资源管理器),TC(事务协调器)。其中TC是一个独立的服务需要单独部署,TM和RM以jar包的方式同业务应用部署在一起,它们同TC建立长连接,在整个事务生命周期内,保持RPC通信。

  • 全局事务的发起方作为TM,全局事务的参与者作为RM
  • TM负责全局事务的begin和commit/rollback
  • RM负责分支事务的执行结果上报,并且通过TC的协调进行commit/rollback。

截屏2023-05-30 18.02.33

在 Seata 中,AT时分为两个阶段的,第一阶段,就是各个阶段本地提交操作;第二阶段会根据第一阶段的情况决定是进行全局提交还是全局回滚操作。具体的执行流程如下:

  • TM 开启分布式事务,负责全局事务的begin和commit/rollback(TM 向 TC 注册全局事务记录);
  • RM 作为参与者,负责分支事务的执行结果上报,并且通过TC的协调进行commit/rollback(RM 向 TC 汇报资源准备状态 );
  • RM分支事务结束,事务一阶段结束;
  • TM根据TC 汇总的事务信息,发起事务提交或回滚操作;
  • TC 通知所有 RM 提交/回滚资源,事务二阶段结束;
5.1.6.3 Sharding-JDBC整合XA

Java通过定义JTA接口实现了XA的模型,JTA接口里的ResourceManager需要数据库厂商提供XA的驱动实现,而TransactionManager则需要事务管理器的厂商实现,传统的事务管理器需要同应用服务器绑定,因此使用的成本很高。 而嵌入式的事务管器可以以jar包的形式提供服务,同ShardingSphere集成后,可保证分片后跨库事务强一致性。

ShardingSphere支持以下功能:

  • 支持数据分片后的跨库XA事务
  • 两阶段提交保证操作的原子性和数据的强一致性
  • 服务宕机重启后,提交/回滚中的事务可自动恢复
  • SPI机制整合主流的XA事务管理器,默认Atomikos
  • 同时支持XA和非XA的连接池
  • 提供spring-boot和namespace的接入端

ShardingSphere整合XA事务时,分离了XA事务管理和连接池管理,这样接入XA时,可以做到对业务的零侵入。截屏2023-05-30 18.22.22

  • Begin(开启XA全局事务)

    XAShardingTransactionManager会调用具体的XA事务管理器开启XA的全局事务。

  • 执行物理SQL

    ShardingSphere进行解析/优化/路由后会生成SQL操作,执行引擎为每个物理SQL创建连接的同时,物理连接所对应的XAResource也会被注册到当前XA事务中。事务管理器会在此阶段发送XAResource.start命令给数据库,数据库在收到XAResource.end命令之前的所有SQL操作,会被标记为XA事务。

    例如:

    XAResource1.start ## Enlist阶段执行 
    
    statement.execute("sql1"); ## 模拟执行一个分片SQL1 
    
    statement.execute("sql2"); ## 模拟执行一个分片SQL2 
    
    XAResource1.end ## 提交阶段执行
    

    这里sql1和sql2将会被标记为XA事务。

  • Commit/rollback(提交XA事务)

    XAShardingTransactionManager收到接入端的提交命令后,会委托实际的XA事务管理进行提交动作,这时事务管理器会收集当前线程里所有注册的XAResource,首先发送XAResource.end指令,用以标记此XA事务的边界。 接着会依次发送prepare指令,收集所有参与XAResource投票,如果所有XAResource的反馈结果都是OK,则会再次调用commit指令进行最终提交,如果有一个XAResource的反馈结果为No,则会调用rollback指令进行回滚。 在事务管理器发出提交指令后,任何XAResource产生的异常都会通过recovery日志进行重试,来保证提交阶段的操作原子性,和数据强一致性。

    例如:

    XAResource1.prepare ## ack: yes 
    
    XAResource2.prepare ## ack: yes 
    
    XAResource1.commit 
    
    XAResource2.commit 
    
    XAResource1.prepare ## ack: yes 
    
    XAResource2.prepare ## ack: no 
    
    XAResource1.rollback 
    
    XAResource2.rollback
    
5.1.6.4 Sharding-JDBC整合Sage

ShardingSphere的柔性事务已通过第三方servicecomb-saga组件实现的,通过SPI机制注入使用。

ShardingSphere是基于反向SQL技术实现的反向补偿操作,它将对数据库进行更新操作的SQL自动生成反向SQL,并交由Saga-actuator引擎执行。使用方则无需再关注如何实现补偿方法,将柔性事务管理器的应用范畴成功的定位回了事务的本源——数据库层面。ShardingSphere支持以下功能:

  • 完全支持跨库事务
  • 支持失败SQL重试及最大努力送达
  • 支持反向SQL、自动生成更新快照以及自动补偿
  • 默认使用关系型数据库进行快照及事务日志的持久化,支持使用SPI的方式加载其他类型的持久化

Saga柔性事务的实现类为SagaShardingTransactionMananger, ShardingSphere通过Hook的方式拦截逻辑SQL的解析和路由结果,这样,在分片物理SQL执行前,可以生成逆向SQL,在事务提交阶段再把SQL调用链交给Saga引擎处理

截屏2023-05-31 10.28.49

  • Init(Saga引擎初始化)

    包含Saga柔性事务的应用启动时,saga-actuator引擎会根据saga.properties的配置进行初始化的流程。

  • Begin(开启Saga全局事务)

    每次开启Saga全局事务时,将会生成本次全局事务的上下文(SagaTransactionContext),事务上下文记录了所有子事务的正向SQL和逆向SQL,作为生成事务调用链的元数据使用。

  • 执行物理SQL

    在物理SQL执行前,ShardingSphere根据SQL的类型生成逆向SQL,这里是通过Hook的方式拦截Parser的解析结果进行实现。

  • Commit/rollback(提交Saga事务)

    提交阶段会生成Saga执行引擎所需的调用链路图,commit操作产生ForwardRecovery(正向SQL补偿)任务,rollback操作产生BackwardRecovery任务(逆向SQL补偿)。

5.1.6.5 Sharding-JDBC整合Seata

分布式事务的实现目前主要分为两阶段的XA强事务和BASE柔性事务。

截屏2023-05-31 10.50.50

Seata AT事务作为BASE柔性事务的一种实现,可以无缝接入到ShardingSphere生态中。在整合Seata AT事务时,需要把TM,RM,TC的模型融入到ShardingSphere 分布式事务的SPI的生态中。在数据库资源上,Seata通过对接DataSource接口,让JDBC操作可以同TC进行RPC通信。同样,ShardingSphere也是面向DataSource接口对用户配置的物理DataSource进行了聚合,因此把物理DataSource二次包装为Seata 的DataSource后,就可以把Seata AT事务融入到ShardingSphere的分片中

截屏2023-05-31 10.51.07

  • Init(Seata引擎初始化)

    包含Seata柔性事务的应用启动时,用户配置的数据源会按seata.conf的配置,适配成Seata事务所需的DataSourceProxy,并且注册到RM中。

  • Begin(开启Seata全局事务)

    TM控制全局事务的边界,TM通过向TC发送Begin指令,获取全局事务ID,所有分支事务通过此全局事务ID,参与到全局事务中;全局事务ID的上下文存放在当前线程变量中。

  • 执行分片物理SQL

    处于Seata全局事务中的分片SQL通过RM生成undo快照,并且发送participate指令到TC,加入到全局事务中。ShardingSphere的分片物理SQL是按多线程方式执行,因此整合Seata AT事务时,需要在主线程和子线程间进行全局事务ID的上下文传递,这同服务间的上下文传递思路完全相同。

  • Commit/rollback(提交Seata事务)

    提交Seata事务时,TM会向TC发送全局事务的commit和rollback指令,TC根据全局事务ID协调所有分支事务进行commit和rollback。

5.1.7 SPI加载

在Apache ShardingSphere中,很多功能实现类的加载方式是通过SPI注入的方式完成的。 Service Provider Interface (SPI)是Java提供的一套被第三方实现或扩展的API,它可以用于实现框架扩展或组件替换。

  • SQL解析

    SQL解析的接口用于规定用于解析SQL的ANTLR语法文件。

    主要接口是SQLParserEntry,其内置实现类有MySQLParserEntry, PostgreSQLParserEntry,SQLServerParserEntry和OracleParserEntry。

  • 数据库协议

    数据库协议的接口用于Sharding-Proxy解析与适配访问数据库的协议。

    主要接口是DatabaseProtocolFrontendEngine,其内置实现类有MySQLProtocolFrontendEngine和PostgreSQLProtocolFrontendEngine。

  • 数据脱敏

    数据脱敏的接口用于规定加解密器的加密、解密、类型获取、属性设置等方式。

    主要接口有两个:Encryptor和QueryAssistedEncryptor,其中Encryptor的内置实现类有AESEncryptor和MD5Encryptor。

  • 分布式主键

    分布式主键的接口主要用于规定如何生成全局性的自增、类型获取、属性设置等。

    主要接口为ShardingKeyGenerator,其内置实现类有UUIDShardingKeyGenerator和SnowflflakeShardingKeyGenerator。

  • 分布式事务

    分布式事务的接口主要用于规定如何将分布式事务适配为本地事务接口。

    主要接口为ShardingTransactionManager,其内置实现类有XAShardingTransactionManager和SeataATShardingTransactionManager。

  • XA事务管理器

    XA事务管理器的接口主要用于规定如何将XA事务的实现者适配为统一的XA事务接口。

    主要接口为XATransactionManager,其内置实现类有AtomikosTransactionManager,NarayanaXATransactionManager和BitronixXATransactionManager。

  • 注册中心

    注册中心的接口主要用于规定注册中心初始化、存取数据、更新数据、监控等行为。

    主要接口为RegistryCenter,其内置实现类有Zookeeper。

5.1.8 编排治理

编排治理模块提供配置中心/注册中心(以及规划中的元数据中心)、配置动态化、数据库熔断禁用、调用链路等治理能力。

  • 配置中心

    配置集中化:越来越多的运行时实例,使得散落的配置难于管理,配置不同步导致的问题十分严重。将配置集中于配置中心,可以更加有效进行管理。

    配置动态化:配置修改后的分发,是配置中心可以提供的另一个重要能力。它可支持数据源、表与分片及读写分离策略的动态切换。

    • 配置中心数据结构

      配置中心在定义的命名空间的config下,以YAML格式存储,包括数据源,数据分片,读写分离、Properties配置,可通过修改节点来实现对于配置的动态管理。

      config
      
      ├──authentication # Sharding-Proxy权限配置 
      
      ├──props # 属性配置 
      
      ├──schema # Schema配置 
      
      ├ ├──sharding_db # SchemaName配置 
      
      ├ ├ ├──datasource # 数据源配置 
      
      ├ ├ ├──rule # 数据分片规则配置 
      
      ├ ├──masterslave_db # SchemaName配置 
      
      ├ ├ ├──datasource # 数据源配置 
      
      ├ ├ ├──rule # 读写分离规则
      
    • config/authentication

      password: root 
      
      username: root
      
    • config/sharding/props

      sql.show: true
      
    • config/schema/schemeName/datasource

      多个数据库连接池的集合,不同数据库连接池属性自适配(例如:DBCP,C3P0,Druid,HikariCP)

      ds_0:
      dataSourceClassName: com.zaxxer.hikari.HikariDataSource
      	properties: 
      		url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC&useSSL=false 
      		password: root 
      		username: root 
      		maxPoolSize: 50 
      		minPoolSize: 1 
      ds_1:
      dataSourceClassName: com.zaxxer.hikari.HikariDataSource
      	properties: 
      		url: jdbc:mysql://127.0.0.1:3306/test2?serverTimezone=UTC&useSSL=false 
      		password: root 
      		username: root 
      		maxPoolSize: 50 
      		minPoolSize: 1 
      
    • config/schema/sharding_db/rule

      数据分片配置,包括数据分片配置

      tables: 
      	b_order: 
      		actualDataNodes: ds_$->{0..1}.b_order_$->{0..1} 
      		databaseStrategy:
      			inline: 
      			shardingColumn: user_id 
      				algorithmExpression: ds_$->{user_id % 2} 
      	keyGenerator: 
      		column: order_id 
      	logicTable: b_order 
      		tableStrategy: 
      			inline: 
      				shardingColumn: order_id 
      				algorithmExpression: b_order_$->{order_id % 2} 
      	b_order_item: 
      		actualDataNodes: ds_$->{0..1}.b_order_item_$->{0..1} 
      		databaseStrategy: 
      			inline: 
      				shardingColumn: user_id 
      				algorithmExpression: ds_$->{user_id % 2} 
      		keyGenerator: 
      			column: order_item_id 
      		logicTable: b_order_item 
      		tableStrategy: 
      			inline: 
      				shardingColumn: order_id 
      				algorithmExpression: b_order_item_$->{order_id % 2}
      
    • config/schema/masterslave/rule读写分离独立使用时使用该配置

      name: ds_ms 
      masterDataSourceName: master 
      slaveDataSourceNames: 
      	- ds_slave0 
      	- ds_slave1 
      loadBalanceAlgorithmType: ROUND_ROBIN
      
    • 动态生效

      在注册中心上修改、删除、新增相关配置,会动态推送到生产环境并立即生效

  • 注册中心

    相对于配置中心管理配置数据,注册中心存放运行时的动态/临时状态数据,比如可用的proxy的实例,需要禁用或熔断的datasource实例。通过注册中心,可以提供熔断数据库访问程序对数据库的访问和禁用从库的访问的编排治理能力。治理仍然有大量未完成的功能(比如流控等)

    • 注册中心数据结构

      注册中心在定义的命名空间的state下,创建数据库访问对象运行节点,用于区分不同数据库访问实例。包括instances和datasources节点。

      instances 
      
      ├──your_instance_ip_a@-@your_instance_pid_x 
      
      ├──your_instance_ip_b@-@your_instance_pid_y 
      
      ├──.... 
      
      datasources 
      
      ├──ds0 
      
      ├──ds1 
      
      ├──....
      
    • state/instances

      数据库访问对象运行实例信息,子节点是当前运行实例的标识。 运行实例标识由运行服务器的IP地址和PID构成。运行实例标识均为临时节点,当实例上线时注册,下线时自动清理。注册中心监控这些节点的变化来治理运行中实例对数据库的访问等。

    • state/datasources

      可以控制读写分离,可动态添加删除以及禁用。

    • 熔断实例

      可在IP地址@-@PID节点写入DISABLED(忽略大小写)表示禁用该实例,删除DISABLED表示启用。

      Zookeeper命令如下:

      [zk: localhost:2181(CONNECTED) 0] set /your_zk_namespace/your_app_name/state/instances/your_instance_ip_a@-@your_instance_pid_x DISABLED
      
    • 禁用从库

      在读写分离场景下,可在数据源名称子节点中写入DISABLED表示禁用从库数据源,删除DISABLED或节点表示启用。

      Zookeeper命令如下

      [zk: localhost:2181(CONNECTED) 0] set /your_zk_namespace/your_app_name/state/datasources/your_slave_datasource_name DISABLED
      
  • 支持的配置中心和注册中心

    ShardingSphere在数据库治理模块使用SPI方式载入数据到配置中心/注册中心,进行实例熔断和数据库禁用。 目前,ShardingSphere内部支持Zookeeper和Etcd这种常用的配置中心/注册中心。 此外,您可以使用其他第三方配置中心/注册中心,例如Apollo、Nacos等,并通过SPI的方式注入到ShardingSphere,从而使用该配置中心/注册中心,实现数据库治理功能。

  • 应用性能监控

    APM是应用性能监控的缩写。目前APM的主要功能着眼于分布式系统的性能诊断,其主要功能包括调用链展示,应用拓扑分析等。

    ShardingSphere并不负责如何采集、存储以及展示应用性能监控的相关数据,而是将SQL解析与SQL执行这两块数据分片的最核心的相关信息发送至应用性能监控系统,并交由其处理。 换句话说,ShardingSphere仅负责产生具有价值的数据,并通过标准协议递交至相关系统。ShardingSphere可以通过两种方式对接应用性能监控系统。

    • 使用OpenTracing API发送性能追踪数据。面向OpenTracing协议的APM产品都可以和ShardingSphere自动对接,比如SkyWalking,Zipkin和Jaeger。
    • 使用SkyWalking的自动探针。 ShardingSphere团队与SkyWalking团队共同合作,在SkyWalking中实现了ShardingSphere自动探针,可以将相关的应用性能数据自动发送到SkyWalking中。
5.1.9 Sharding-Proxy

Sharding-Proxy是ShardingSphere的第二个产品,定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。 目前先提供MySQL版本,它可以使用任何兼容MySQL协议的访问客户端(如:MySQL Command Client, MySQL Workbench等操作数据,对DBA更加友好

  • 向应用程序完全透明,可直接当做MySQL使用

  • 适用于任何兼容MySQL协议的客户端

截屏2023-05-30 15.37.35

Sharding-Proxy的优势在于对异构语言的支持,以及为DBA提供可操作入口

Sharding-Proxy使用过程

  • 下载Sharding-Proxy的最新发行版;

  • 解压缩后修改conf/server.yaml和以config-前缀开头的文件,进行分片规则、读写分离规则配置

  • 引入依赖jar

    如果后端连接MySQL数据库,需要下载MySQL驱动, 解压缩后将mysql-connector-java-5.1.48.jar拷贝到${sharding-proxy}\lib目录。

    如果后端连接PostgreSQL数据库,不需要引入额外依赖。

  • Linux操作系统请运行bin/start.sh,Windows操作系统请运行bin/start.bat启动Sharding-Proxy。

    使用默认配置启动:${sharding-proxy}\bin\start.sh

    配置端口启动:${sharding-proxy}\bin\start.sh ${port}

    使用客户端工具连接。如: mysql -h 127.0.0.1 -P 3307 -u root -p root

若想使用Sharding-Proxy的数据库治理功能,则需要使用注册中心实现实例熔断和从库禁用功能。

Sharding-Proxy默认提供了Zookeeper的注册中心解决方案。只需按照配置规则进行注册中心的配置,即可使用。

注意事项

  • Sharding-Proxy 默认不支持hint,如需支持,请在conf/server.yaml中,将props的属性proxy.hint.enabled设置为true。在Sharding-Proxy中,HintShardingAlgorithm的泛型只能是String类型。

  • Sharding-Proxy默认使用3307端口,可以通过启动脚本追加参数作为启动端口号。如:bin/start.sh 3308

  • Sharding-Proxy使用conf/server.yaml配置注册中心、认证信息以及公用属性。

  • Sharding-Proxy支持多逻辑数据源,每个以"config-"做前缀命名yaml配置文件,即为一个逻辑数据源。

5.2 Mycat

Mycat 是一个实现了 MySQL 协议的 Server,前端用户可以把它看作是一个数据库代理,用 MySQL 客户端工具和命令行访问,而其后端可以用 MySQL 原生协议或JDBC 协议与多个 MySQL 服务器通信,其核心功能是分库分表和读写分离,即将一个大表水平分割为 N 个小表,存储在后端 MySQL 服务器里或者其他数据库里。

  • 对于 DBA 来说

    Mycat 就是 MySQL Server,但是Mycat 本身并不存储数据,数据是在后端的 MySQL 上存储的,因此数据可靠性以及事务等都是 MySQL 保证的。简单的说,Mycat 就是 MySQL 最佳伴侣。

  • 对于软件工程师来说

    Mycat 就是一个近似等于 MySQL 的数据库服务器,你可以用连接 MySQL 的方式去连接Mycat(除了端 口不同,默认的 Mycat 端口是 8066 而非 MySQL 的 3306,因此需要在连接字符串上增加端口信息),大多数情况下,可以用你熟悉的对象映射框架使用 Mycat,但建议对于分片表,尽量使用基础的 SQL 语句,因为这样能达到最佳性能,特别是几千万甚至几百亿条记录的情况下。

  • 对于架构师来说

    Mycat 是一个强大的数据库中间件,不仅仅可以用作读写分离、以及分表分库、容灾备份,而且可以用于多 用户应用开发、云平台基础设施、让你的架构具备很强的适应性和灵活性,借助于即将发布的 Mycat 智能优化模块,系统的数据访问瓶颈和热点一目了然,根据这些统计分析数据,可以自动或手工调整后端存储,将不同的表映射到不同存储引擎上,而整个应用的代码一行也不用改变。

5.2.1 核心概念
  • 逻辑库

    对数据进行分片处理之后,从原有的一个库,被切分为多个分片数据库,所有的分片数据库集群构成了整个完整的数据库存储。Mycat在操作时,使用逻辑库来代表这个完整的数据库集群,便于对整个集群操作。

  • 逻辑表

    既然有逻辑库,那么就会有逻辑表,分布式数据库中,对应用来说,读写数据的表就是逻辑表。

  • 分片表

    分片表,是指那些原有的很大数据的表,需要切分到多个数据库的表,这样,每个分片都有一部分数据,所 有分片构成了完整的数据。例如在 mycat 配置中的 t_node 就属于分片表,数据按照规则被分到 dn1,dn2 两个分片节点上。

    <table name="t_node" primaryKey="vid" autoIncrement="true" dataNode="dn1,dn2" rule="rule1" />
    
  • 非分片表

    一个数据库中并不是所有的表都很大,某些表是可以不用进行切分的,非分片是相对分片表来说的,就是那些不需要进行数据切分的表。如下配置中 t_node,只存在于分片节点dn1上。

    <table name="t_node" primaryKey="vid" autoIncrement="true" dataNode="dn1" />
    
  • ER表

    Mycat提出了基于 E-R 关系的数据分片策略,子表的记录与所关联的父表记录存放在同一个数据分片上,即子表依赖于父表,通过表分组(Table Group)保证数据 join 不会跨库操作。表分组(Table Group)是解决跨分片数据 join 的一种很好的思路,也是数据切分规划的重要一条规则。

  • 全局表

    一个真实的业务系统中,往往存在大量的类似字典表的表,这些表基本上很少变动,字典表具有以下几个特 性:

    • 变动不频繁;
    • 数据量总体变化不大;
    • 数据规模不大,很少有超过数十万条记录。

    对于这类的表,在分片的情况下,当业务表因为规模而进行分片以后,业务表与这些附属的字典表之间的关联,就成了比较棘手的问题,所以 Mycat 中通过数据冗余来解决这类表的 join,即所有的分片都有一份数据的拷贝,所有将字典表或者符合字典表特性的一些表定义为全局表。数据冗余是解决跨分片数据 join 的一种很好的思路,也是数据切分规划的另外一条重要规则。

  • 分片节点

    数据切分后,一个大表被分到不同的分片数据库上面,每个分片表所在的数据库就是分片节点dataNode。

  • 节点主机

    数据切分后,每个分片节点不一定都会独占一台机器,同一机器上面可以有多个分片数据库, 这样一个或多个分片节点所在的机器就是节点主机,为了规避单节点主机并发数限制, 尽量将读写压力高的分片节点均衡的放在不同的节点主机dataHost。

  • 分片规则

    前面讲了数据切分,一个大表被分成若干个分片表,就需要一定的规则rule,这样按照某种业务规则把数据分到 某个分片的规则就是分片规则,数据切分选择合适的分片规则非常重要,将极大的避免后续数据处理的难度。

5.2.2 server.xml配置

server.xml几乎保存了所有 mycat 需要的系统配置信息。

  • user标签

    这个标签主要用于定义登录 mycat 的用户和权限。比如定义了一个用户,用户名为 user、密码也为 user,可访问的 schema为edu

    <user name="user" defaultAccount="true">
    	<property name="password">user</property>
    	<property name="schemas">edu</property>
    	<!-- <property name="readOnly">true</property> --><!-- 是否只读 -->
    	<property name="defaultSchema">edu</property>
    </user>
    
  • firewall标签

    <!-- 全局SQL防火墙设置 -->
    <!--白名单可以使用通配符%或着*-->
    <!--例如<host host="127.0.0.*" user="root"/>-->
    <!--例如<host host="127.0.*" user="root"/>-->
    <!--例如<host host="127.*" user="root"/>-->
    <!--例如<host host="1*7.*" user="root"/>-->
    <!--这些配置情况下对于127.0.0.1都能以root账户登录-->
    
    <firewall>
       <whitehost>
          <host host="1*7.0.0.*" user="root"/>
       </whitehost>
       <blacklist check="true"> 
       	<property name="selelctAllow">false</property>
       	<property name="selelctIntoAllow">false</property>
       	<property name="updateAllow">false</property>
       	<property name="insertAllow">false</property>
       	<property name="deletetAllow">false</property>
       	<property name="dropAllow">false</property> 
       </blacklist>
    </firewall>
    
  • 全局序列号

    在实现分库分表的情况下,数据库自增主键已无法保证自增主键的全局唯一。为此,Mycat 提供了全局sequence,并且提供了包含本地配置和数据库配置等多种实现方式

    <system> 
    	<property name="sequnceHandlerType">0</property>
    </system>
    

    0表示使用本地文件方式;1表示使用数据库方式生成;2表示使用本地时间戳方式;3表示基于ZK与本地配置的分布式ID生成器;4表示使用zookeeper递增方式生成

    • 本地文件

      此方式 Mycat 将 sequence 配置到文件中,当使用到 sequence 中的配置后,Mycat 会更新 classpath中的 sequence_conf.properties 文件中 sequence 当前的值。

      #default global sequence 
      
      GLOBAL.HISIDS= 
      
      GLOBAL.MINID=10001 
      
      GLOBAL.MAXID=20000 
      
      GLOBAL.CURID=10000 
      
      # self define sequence 
      
      COMPANY.HISIDS= 
      
      COMPANY.MINID=1001 
      
      COMPANY.MAXID=2000 
      
      COMPANY.CURID=1000 
      
      ORDER.HISIDS= 
      
      ORDER.MINID=1001 
      
      ORDER.MAXID=2000 
      
      ORDER.CURID=1000
      
    • 数据库方式

    在数据库中建立一张表,存放 sequence 名称(name),sequence 当前值(current_value),步长(increment) 等信息。

    CREATE TABLE MYCAT_SEQUENCE
    (
        name          VARCHAR(64) NOT NULL,
        current_value BIGINT(20)  NOT NULL,
        increment     INT         NOT NULL DEFAULT 1,
        PRIMARY KEY (name)
    ) ENGINE = InnoDB; 
    
    • 本地时间戳方式

    ID为64 位二进制 ,42(毫秒)+5(机器 ID)+5(业务编码)+12(重复累加)换算成十进制为 18 位数的 long 类型,每毫秒可以并发 12 位二进制的累加。

    在 Mycat 下配置sequence_time_conf.properties文件

    WORKID=0-31 任意整数 
    DATAACENTERID=0-31 任意整数
    

    每个Mycat 配置的 WORKID、DATAACENTERID 不同,组成唯一标识,总共支持32*32=1024 种组合。

    • 分布式 ZK ID 生成器

    ZK 的连接信息统一在 myid.properties 的 zkURL 属性中配置。基于 ZK 与本地配置的分布式 ID 生成器,InstanceID可以通过ZK自动获取,也可以通过配置文件配置。在sequence_distributed_conf.properties,只要配置INSTANCEID=ZK就表示从 ZK 上获取 InstanceID。

    ID 最大为63位二进制,可以承受单机房单机器单线程 1000*(2^6)=640000 的并发。结构如下

    • current time millis(微秒时间戳 38 位,可以使用 17 年)

    • clusterId(机房或者 ZKid,通过配置文件配置,5 位)

    • instanceId(实例 ID,可以通过 ZK 或者配置文件获取,5 位)

    • threadId(线程 ID,9 位)

    • increment(自增,6 位)

    • ZK 递增方式

    ZK 的连接信息统一在 myid.properties 的 zkURL 属性中配置。需要配置sequence_conf.properties文件

    • TABLE.MINID 某线程当前区间内最小值
    • TABLE.MAXID 某线程当前区间内最大值
    • TABLE.CURID 某线程当前区间内当前值
5.2.3 schema.xml配置

schema.xml 作为 Mycat 中重要的配置文件之一,管理着 Mycat 的逻辑库、表、分片节点、主机等信息。

<schema name="edu" checkSQLschema="true" sqlMaxLimit="100" randomDataNode="dn1">
	</schema>
类型 数量 说明
name String 1 逻辑库名,对应Server.xml中配置的逻辑库名
checkSQLschema Boolean 1 是否去表库名
sqlMaxLimit Integer 1 查询返回的记录数限制limit
dataNode String n 分片节点,","间隔
randomDataNode String 一个 默认分片节点(非查询语句走这个dataNode),如果没有就会随机
  • table标签

    table标签定义了 Mycat 中的逻辑表,所有需要拆分的表都需要在这个标签中定义

    <table name="position" primaryKey="id" dataNode="dn1,dn2" rule="mod-long" autoIncrement="true">
    		</table>
    
    类型 数量 描述
    name String 1 逻辑表名
    primaryKey String 1 主键
    dataNode String 1 分片节点,","间隔
    rule String 1 分片规则
    autoIncrement Boolean 1 自增长主键
    ruleRequired Boolean 1 是否强制绑定分片规则
    type String 1 逻辑表类型,全局表、普通表
    subTables String 1 分表
    needAddLimit String 1 是否为查询SQL自动加limit限制
  • dataNode标签

    dataNode标签定义了 MyCat 中的分片节点,也就是我们通常说所的数据分片。

    	<dataNode name="dn1" dataHost="localhost1" database="test_myCat" />
    
    类型 数量 描述
    name String 1 定义数据节点的名字,这个名字需要是唯一的,我们需要在 table 标签上应用这个名字,来建立表与分片对应的关系。
    dataHost String 1 用于定义该分片属于哪个分片主机,属性值是引用 dataHost 标签上定义的 name 属性
    database String 1 用于定义该分片节点具体的库。
  • dataHost标签

    dataHost标签在 Mycat 逻辑库中也是作为最底层的标签存在,直接定义了具体的数据库实例、读写分离配置和心跳语句

    <dataHost name="localhost2" maxCon="1000" minCon="10" balance="1"
    			  writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1"  slaveThreshold="100">
    	</dataHost>
    
    类型 数量 描述
    name String 1 节点主机名
    maxCon Integer 1 最大连接数
    minCon Integer 1 最小连接数
    balance Integer 1 读操作负载均衡类型
    0:不开启读写分离机制,所有读操作都发送到当前可用的 writeHost 上
    1:全部的 readHost 与 stand by writeHost 参与 select 语句的负载均衡,简单的说, 当双主双从模式(M1->S1,M2->S2,并且 M1 与 M2 互为主备),正常情况下,M2,S1,S2 都参与 select 语句的负载均衡。
    2:所有读操作都随机的在 writeHost、readhost 上分发。
    3:所有读请求随机的分发到 wiriterHost 对应的 readhost 执行,writerHost 不负担读压力(1.4新加)
    writeType Integer 1 写操作负载均衡类型
    0:所有写操作发送到配置的第一个 writeHost,第一个挂了切到还生存的第二个writeHost,重新启动后已切换后的为准,切换记录在配置文件中:dnindex.properties .
    1:所有写操作都随机的发送到配置的 writeHost,1.5 以后废弃不推荐。
    dbType String 1 数据库类型
    dbDriver String 1 数据库驱动
    switchType String 1 主从切换类型
    -1:表示不自动切换
    1:默认值,自动切换
    2:基于 MySQL 主从同步的状态决定是否切换
    3:基于MySQL cluster集群切换机制
    slaveThreshold 1 心跳时间
  • heartbeat标签

    <heartbeat>select user()</heartbeat>
    

    这个标签内指明用于和后端数据库进行心跳检查的语句。例如,MYSQL 可以使用 select user(),Oracle 可以 使用 select 1 from dual 等。

    这个标签还有一个 connectionInitSql 属性,主要是当使用 Oracla 数据库时,需要执行的初始化 SQL 语句就 这个放到这里面来。例如:alter session set nls_date_format=‘yyyy-mm-dd hh:mi:ss’

    开启主从切换的语句必须是:show slave status或者是showstatus like ‘wsrep%’

  • writeHost和readHost标签

    writeHost和readHost标签都指定后端数据库的相关配置给 mycat,用于实例化后端连接池。唯一不同的是,writeHost 指定写实例、readHost 指定读实例。在一个 dataHost 内可以定义多个 writeHost 和 readHost。但是,如果 writeHost 指定的后端数据库宕机, 那么这个 writeHost 绑定的所有 readHost都将不可用。另一方面,由于这个 writeHost 宕机系统会自动的检测 到,并切换到备用的 writeHost上去。

    <writeHost host="hostM1" url="jdbc:mysql://xxxx:3306" user="user" password="pwd">
    		</writeHost>
    		<writeHost host="hostS1" url="jdbc:mysql://xxxx:3306" user="user" password="pwd" /writeHost>
    
    类型 数量 描述
    host String 1 主机名
    url String 1 数据库连接
    user String 1 用户名
    password String 1 密码
    weight String 1 权重
    usingDecrypt String 1 是否对密码加密,默认0
5.2.4 rule.xml配置

rule.xml用于定义Mycat的分片规则。

  • tableRule标签
<tableRule name="rule1">
	<rule>
		<columns>id</columns>
		<algorithm>func1</algorithm>
	</rule>
</tableRule>
类型 数量 描述
name String 1 路由名
columns String 1 分片的键
algorithm String 1 使用 function 标签中的 name 属性,具体路由算法。

function标签

<function name="func1" class="io.mycat.route.function.PartitionByLong">
		<property name="partitionCount">8</property>
		<property name="partitionLength">128</property>
</function>
类型 数量 描述
name String 1 算法名
class String 1 算法类路径
property String 1 算法使用的字段
5.2.5 应用
5.2.5.1 分库分表

Mycat常用分片规则如下:

  • 时间类

    按天分片、自然月分片、单月小时分片

  • 哈希类

    Hash固定分片、日期范围Hash分片、截取数字Hash求模范围分片、截取数字Hash分片、一致性Hash分片

  • 取模类

    取模分片、取模范围分片、范围求模分片

  • 其他类

    枚举分片、范围约定分片、应用指定分片、冷热数据分片

Mycat常用分片配置示例:

  • 自动分片

    <tableRule name="auto-sharding-long"> 
    
    <rule> 
    
    <columns>id</columns> 
    
    <algorithm>rang-long</algorithm> 
    
    </rule> 
    
    </tableRule> 
    
    <function name="rang-long" class="io.mycat.route.function.AutoPartitionByLong"> 
    
    <property name="mapFile">autopartition-long.txt</property> 
    
    </function>
    

    autopartition-long.txt文件内容如下:

    # range start-end ,data node index 
    
    # K=1000,M=10000. 
    
    0-500M=0 
    
    500M-1000M=1 
    
    1000M-1500M=2
    
  • 枚举分片

    把数据分类存储。

    <tableRule name="sharding-by-intfile"> 
    
    <rule> 
    
    <columns>sharding_id</columns> 
    
    <algorithm>hash-int</algorithm> 
    
    </rule> 
    
    </tableRule> 
    
    <function name="hash-int" 
    
    class="io.mycat.route.function.PartitionByFileMap"> 
    
    <property name="mapFile">partition-hash-int.txt</property> 
    
    <!-- 找不到分片时设置容错规则,把数据插入到默认分片0里面 --> 
    
    <property name="defaultNode">0</property> 
    
    </function>
    

    partition-hash-int.txt文件内容如下:

    10000=0 
    
    10010=1
    
  • 取模分片

    根据分片字段值 % 分片数

    <tableRule name="mod-long"> 
    
    <rule>
    
    <columns>id</columns> 
    
    <algorithm>mod-long</algorithm> 
    
    </rule> 
    
    </tableRule> 
    
    <function name="mod-long" class="io.mycat.route.function.PartitionByMod"> 
    
    <!--分片数 --> 
    
    <property name="count">3</property> 
    
    </function>
    
  • 冷热数据分片

    根据日期查询日志数据冷热数据分布 ,最近 n 个月的到实时交易库查询,超过 n 个月的按照 m 天分片。

    <tableRule name="sharding-by-date"> 
    
    <rule> 
    
    <columns>create_time</columns> 
    
    <algorithm>sharding-by-hotdate</algorithm> 
    
    </rule> 
    
    </tableRule>
    
    <function name="sharding-by-hotdate" 
    
    class="org.opencloudb.route.function.PartitionByHotDate"> 
    
    <!-- 定义日期格式 --> 
    
    <property name="dateFormat">yyyy-MM-dd</property> 
    
    <!-- 热库存储多少天数据 --> 
    
    <property name="sLastDay">30</property> 
    
    <!-- 超过热库期限的数据按照多少天来分片 --> 
    
    <property name="sPartionDay">30</property> 
    
    </function> 
    
  • 一致性哈希分片

    <tableRule name="sharding-by-murmur">
      <rule>
      <columns>user_id</columns>
      <algorithm>murmur</algorithm>
      </rule>
    </tableRule>
    <function name="murmur" class="io.mycat.route.function.PartitionByMurmurHash">
       <property name="seed">0</property><!-- 默认是 0-->
       <property name="count">2</property><!-- 要分片的数据库节点数量,必须指定,否则没法分片-->
       <property name="virtualBucketTimes">160</property><!-- 一个实际的数据库节点被映射为这么多虚拟
    节点,默认是 160 倍,也就是虚拟节点数是物理节点数的 160 倍-->
    <!--
    <property name="weightMapFile">weightMapFile</property>
    节点的权重,没有指定权重的节点默认是 1。以 properties 文件的格式填写,以从 0 开始到 count-1 的整数值也就是节点索引为 key,以节点权重值为值。所有权重值必须是正整数,否则以 1 代替 -->
    <!--
    <property name="bucketMapPath">/etc/mycat/bucketMapPath</property>
    用于测试时观察各物理节点与虚拟节点的分布情况,如果指定了这个属性,会把虚拟节点的 murmur hash 值与物理节点的映射按行输出到这个文件,没有默认值,如果不指定,就不会输出任何东西 -->
    </function>
    
    index bucket ratio 
    0 1001836 0.1001836 
    1 1038892 0.1038892 
    2 927886 0.0927886 
    3 972728 0.0972728 
    4 1086100 0.10861 
    5 908616 0.0908616 
    6 1024269 0.1024269 
    7 1018029 0.1018029 
    8 995581 0.0995581 
    9 1026063 0.1026063
    

    第一列是分片节点的编号,第二列是hash到每个节点的数据量,第三列是每个hash到每个节点的数据量与总数据量的比值。第三列的和是1.0,第二列的和是10000000。 如果数据量相当少,会发现一致性哈希的分布不够均匀,而只要数据量在10000以上一致性哈希的分布比率就能保持在0.1左右,数据越多分布越均匀,每个节点的数据量越接近。

5.2.5.2 读写分离

在schema.xml文件中配置Mycat读写分离。使用前需要搭建MySQL主从架构,并实现主从复制,Mycat不负责数据同步问题。

<dataHost name="localhost1" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native"> 
	<heartbeat>select user()</heartbeat> 
	<!-- can have multi write hosts --> 
	<writeHost host="M1" url="localhost:3306" user="root" password="123456"> 
		<readHost host="S1" url="localhost:3307" user="root" password="123456" weight="1" /> 
	</writeHost> 
</dataHost>

balance参数:

  • 0 : 所有读操作都发送到当前可用的writeHost

  • 1 :所有读操作都随机发送到readHost和stand by writeHost

  • 2 :所有读操作都随机发送到writeHost和readHost

  • 3 :所有读操作都随机发送到writeHost对应的readHost上,但是writeHost不负担读压力

writeType参数:

  • 0 : 所有写操作都发送到可用的writeHost

  • 1 :所有写操作都随机发送到readHost

  • 2 :所有写操作都随机发送到writeHost,readHost

另一种写法:

<dataHost name="localhost1" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native"> 
	<heartbeat>select user()</heartbeat> 
	<!-- can have multi write hosts --> 
	<writeHost host="M1" url="localhost:3306" user="root" password="123456"> </writeHost>
	<writeHost host="S1" url="localhost:3307" user="root" password="123456"></writeHost> 
</dataHost>

第一种写法当写挂了读不可用,第二种可以继续使用

事务内部的一切操作都会走写节点,所以读操作不要加事务,如果读延时较大,使用根据主从延时切换的读写分离,或者强制走写节点

5.2.5.3 强制路由

一个查询 SQL 语句以/* !mycat * /注解来确定其是走读节点还是写节点。

强制走从: 

/*!mycat:db_type=slave*/ select * from travelrecord //有效 

/*#mycat:db_type=slave*/ select * from travelrecord 

强制走写: 

/*!mycat:db_type=master*/ select * from travelrecord //有效 

/*#mycat:db_type=slave*/ select * from travelrecord 

1.6 以后Mycat除了支持db_type注解以外,还有其他注解

/*!mycat:sql=sql */ 指定真正执行的SQL 

/*!mycat:schema=schema1 */ 指定走那个schema 

/*!mycat:datanode=dn1 */ 指定sql要运行的节点 

/*!mycat:catlet=io.mycat.catlets.ShareJoin */ 通过catlet支持跨分片复杂SQL实现以及存储过程支持等
5.2.5.4 主从切换

switchType参数:

  • -1: 表示不自动切换

  • 1 :表示自动切换

  • 2 :基于MySQL主从同步状态决定是否切换

  • 3 :基于MySQL cluster集群切换机制

1.4 开始支持 MySQL 主从复制状态绑定的读写分离机制,让读更加安全可靠,配置如下:

MyCAT 心跳检查语句配置为 show slave status ,dataHost 上定义两个新属性: switchType=“2” 与slaveThreshold=“100”,此时意味着开启 MySQL 主从复制状态绑定的读写分离与切换机制,Mycat 心跳机 制通过检测 show slave status 中的 “Seconds_Behind_Master”, “Slave_IO_Running”,“Slave_SQL_Running” 三个字段来确定当前主从同步的状态以及 Seconds_Behind_Master 主从复制时延,

当 Seconds_Behind_Master > slaveThreshold 时,读写分离筛选器会过滤掉此 Slave 机器,防止读到很久之 前的旧数据,而当主节点宕机后,切换逻辑会检查 Slave 上的 Seconds_Behind_Master 是否为 0,为 0 时则 表示主从同步,可以安全切换,否则不会切换。

<dataHost name="localhost1" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="2" slaveThreshold="100"> 
	<heartbeat>show slave status </heartbeat> 
   <!-- can have multi write hosts --> 
   <writeHost host="M1" url="localhost:3306" user="root" password="123456"> </writeHost>	
   <writeHost host="S1" url="localhost:3316" user="root" 
</dataHost> 

1.4.1 开始支持 MySQL 集群模式,让读更加安全可靠,配置如下: MyCAT 心跳检查语句配置为 show status like ‘wsrep%’ ,dataHost 上定义两个新属性: switchType=“3”

此时意味着开启 MySQL 集群复制状态状态绑定的读写分离与切换机制,Mycat 心跳机制通过检测集群复制时延时,如果延时过大或者集群出现节点问题不会负载改节点。

<dataHost name="localhost1" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="3"> 
	<heartbeat>show status like 'wsrep%'</heartbeat> 
   <!-- can have multi write hosts --> 
   <writeHost host="M1" url="localhost:3306" user="root" password="123456"> </writeHost>	
   <writeHost host="S1" url="localhost:3316" user="root" 
</dataHost> 
5.2.6 事务

Mycat 目前没有出来跨分片的事务强一致性支持,单库内部可以保证事务的完整性,如果跨库事务,在执行的时候任何分片出错,可以保证所有分片回滚,但是一旦应用发起 commit 指令,无法保证所有分片都成功,考虑到某个分片挂的可能性不大所以称为弱 XA。

XA 事务使用

Mycat 从 1.6.5 版本开始支持标准 XA 分布式事务,考虑到 MySQL 5.7 之前版本XA有bug,所以推荐最佳搭配 XA 功能使用 MySQL 5.7 版本。

Mycat 实现 XA 标准分布式事务,Mycat 作为XA 事务协调者角色,即使事务过程中 Mycat 宕机挂掉,由于 Mycat 会记录事务日志,所以 Mycat 恢复后会进行事务的恢复善后处理工作。考虑到分布式事务的性能开销比较大,所以只推荐在全局表的事务以及其他一些对一致性要 求比较高的场景。

XA 操作说明

  • XA 事务需要设置手动提交

    set autocommit=0;
    
  • 使用该命令开启 XA 事务

    set xa=on; 
    
  • 执行相应的 SQL 语句部分

    insert into city(id,name,province) values(200,'chengdu','sichuan'); 
    
    update position set salary='300000' where id<5; 
    
  • 提交或回滚事务

    commit; 
    
    rollback;
    

保证Repeatable Read

mycat 有一个特性,就是开事务之后,如果不运行 update/delete/select for update 等更新类语句SQL 的话,不会将当前连接与当前 session 绑定。如下图所示:

截屏2023-06-01 16.12.14

这样做的好处是可以保证连接可以最大限度的复用,提升性能。

但是,这就会导致两次 select 中如果有其它的在提交的话,会出现两次同样的 select 不一致的现象,即不能 Repeatable Read,这会让人直连 MySQL 的人很困惑,可能会在依赖 Repeatable Read 的场景出现问题。所以做了一个开关,当 server.xml 的 system 配置了 strictTxIsolation=true 的时候,会关掉这个特性,以保证 repeatable read,加了开关 后如下图所示:

截屏2023-06-01 16.12.44

六.运维和第三方工具

6.1 Yearning

官网文档:https://guide.yearning.io/

Yearning 工具包含的主要功能如下:

  • SQL查询

    • 查询导出
    • 查询自动补全
  • SQL审核

    • 流程化工单
    • SQL语句检测
    • SQL语句执行
    • SQL回滚
    • 历史审核记录
  • 推送

    • 站内信工单通知
    • E-mail工单推送
    • 钉钉webhook机器人工单推送
  • 其他

    • todoList
    • LDAP登陆
  • 用户权限及管理

    • 拼图式细粒度权限划分

6.2 canal

官网文档:https://github.com/alibaba/canal

canal 译意为水道/管道,主要用途是基于MySQL数据库增量日志解析,提供增量数据订阅和消费。

早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。

基于日志增量订阅和消费的业务包括以下内容:

  • 数据库镜像
  • 数据库实时备份
  • 索引构建和实时维护(拆分异构索引、倒排索引等)
  • 业务 cache 刷新
  • 带业务逻辑的增量数据处理

当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x

canal的工作原理类似mysql主从同步原理:

截屏2023-06-01 17.48.48

  • canal模拟MySQL slave的交互协议,伪装自己为MySQL slave,向MySQL master发送dump协议

  • MySQL master收到dump协议请求,开始推送binary log 给canal

  • canal解析binary log对象(原始为byte流)

6.3 DataX

官网文档:https://github.com/alibaba/DataX

DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具/平台,实现包括 MySQL、Oracle、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、DRDS等各种异构数据源之间高效的数据同步功能。

截屏2023-06-01 17.52.44

设计理念

为了解决异构数据源同步问题,DataX将复杂的网状的同步链路变成了星型数据链路,DataX作为中间传输载体负责连接各种数据源。当需要接入一个新的数据源的时候,只需要将此数据源对接到DataX,便能跟已有的数据源做到无缝数据同步。

当前使用现状

DataX在阿里巴巴集团内被广泛使用,承担了所有大数据的离线同步业务,并已持续稳定运行了多年。当年每天完成同步8w多道作业,每日传输数据量超过300TB。

DataX框架设计

DataX本身作为离线数据同步框架,采用Framework + plugin架构构建。将数据源读取和写入抽象成为Reader/Writer插件,纳入到整个同步框架中。

截屏2023-06-01 17.53.48

  • Reader

    Reader为数据采集模块,负责采集数据源的数据,将数据发送给Framework。

  • Writer

    Writer为数据写入模块,负责不断向Framework取数据,并将数据写入到目的端。

  • Framework

    Framework用于连接reader和writer,作为两者的数据传输通道,并处理缓冲,流控,并发,数据转换等核心技术问题

DataX插件体系

DataX Framework提供了简单的接口与插件交互,提供简单的插件接入机制,只需要任意加上一种插件,就能无缝对接其他数据源。经过几年积累,DataX目前已经有了比较全面的插件体系,主流的RDBMS数据库、NOSQL、大数据计算系统都已经接入。DataX目前支持数据如下:

类型 数据源 Reader Writer 文档
RDBMS 关系型数据库 MySQL ✔️ ✔️ 读写
Oracle ✔️ ✔️ 读写
SQLServcer ✔️ ✔️ 读写
PostgreSQL ✔️ ✔️ 读写
DRDS ✔️ ✔️ 读写
达梦 ✔️ ✔️ 读写
通用RDBMS(支持所有关系型数据库) ✔️ ✔️ 读写
阿里云数仓数据存储 ODPS ✔️ ✔️ 读写
ADS ✔️
OSS ✔️ ✔️ 读写
OCS ✔️ ✔️ 读写
NoSQL数据存储 OTS ✔️ ✔️ 读写
Hbase0.94 ✔️ ✔️ 读写
Hbase1.1 ✔️ ✔️ 读写
MongoDB ✔️ ✔️ 读写
Hive ✔️ ✔️ 读写
无结构化数据存储 TxtFile ✔️ ✔️ 读写
FTP ✔️ ✔️ 读写
HDFS ✔️ ✔️ 读写
Elasticsearch ✔️

DataX核心架构

DataX 3.0 开源版本支持单机多线程模式完成同步作业运行,下面是一个DataX作业生命周期的时序图,从整体架构设计非常简要的说明DataX各个模块相互关系。

截屏2023-06-01 17.57.31

  • DataX完成单个数据同步的作业,我们称之为Job,DataX接受到一个Job之后,将启动一个进程来完成整个作业同步过程。DataX Job模块是单个作业的中枢管理节点,承担了数据清理、子任务切分(将单一作业计算转化为多个子Task)、TaskGroup管理等功能。
  • DataXJob启动后,会根据不同的源端切分策略,将Job切分成多个小的Task(子任务),以便于并发执行。Task便是DataX作业的最小单元,每一个Task都会负责一部分数据的同步工作。
  • 切分多个Task之后,DataX Job会调用Scheduler模块,根据配置的并发数据量,将拆分成的Task重新组合,组装成TaskGroup(任务组)。每一个TaskGroup负责以一定的并发运行完毕分配好的所有Task,默认单个任务组的并发数量为5。
  • 每一个Task都由TaskGroup负责启动,Task启动后,会固定启动Reader—>Channel—>Writer的线程来完成任务同步工作。
  • DataX作业运行起来之后, Job监控并等待多个TaskGroup模块任务完成,等待所有TaskGroup任务完成后Job成功退出。否则,异常退出,进程退出值非0

6.4 percona-toolkit

官网文档:https://www.percona.com/doc/percona-toolkit/3.0/index.html

percona-toolkit是一组高级命令行工具的集合,可以查看当前MySQL服务的摘要信息,磁盘检测,分析慢查询日志,查找重复索引,实现表同步等等。

percona-toolkit工具是 MySQL一个重要分支产品percona的,它是一组命令的集合。

工具包的下载地址:https://www.percona.com/downloads/percona-toolkit/LATEST/

安装过程很简单,先解压:

tar -zxvf percona-toolkit-3.0.3_x86_64.tar.gz 

由于是二进制的包,解压完可以直接进到percona-toolkit-3.0.3/bin目录下使用。

  • pt-query-digest

    pt-query-digest是用于分析mysql慢查询的一个工具,它可以分析binlog、General log、slowlog,也可以通过showprocesslist或者mysqldumpslow命令来进行分析。可以把分析结果输出到文件中,分析过程是先对查询语句的条件进行参数化,然后对参数化以后的查询进行分组统计,统计出各查询的执行时间、次数、占比等,可以借助分析结果找出问题进行优化。

    数据库大多数的性能问题是 slow sql 语句造成的,需要及时做相关的优化处理。使用示例如下:

    • 直接分析慢查询文件

      pt-query-digest slow_OAK.log > slow_report.log
      
    • 分析最近24小时内的查询

      pt-query-digest --since=24h slow_OAK.log > slow_report.log 
      
    • 分析只含有select语句的慢查询

      pt-query-digest --filter '$event->{fingerprint} =~ m/^select/i' slow_OAK.log> slow_report.log 
      

    查看SQL报告,总结慢语句有哪些,并可以看对应时间的消耗。分析结果如下所示:

    # Profile 
    
    # Rank Query ID Response time Calls R/Call V/M Item 
    
    # ==== ================== ============= ===== ======= ===== ============== 
    
    # 1 0x040ADBE3A1EED0A2 16.8901 87.2% 1 16.8901 0.00 CALL insert_city 
    
    # 2 0x8E44F4ED46297D4C 1.3013 6.7% 3 0.4338 0.18 INSERT SELECT testDB._city 
    
    # 3 0x12E7CAFEA3145EEF 0.7431 3.8% 1 0.7431 0.00 DELETE city 
    
    # MISC 0xMISC 0.4434 2.3% 3 0.1478 0.0 <3ITEMS> 
    
    # Query 1: 0 QPS, 0x concurrency, ID 0x040ADBE3A1EED0A2 at byte 19060 ____ 
    
    # Scores: V/M = 0.00\# Time range: all events occurred at 2020-05-08 12:12:10 
    
    # Attribute pct total min max avg 95% stddev median 
    
    # ============ === ======= ======= ======= ======= ======= ======= ======= 
    
    # Count 2 1 
    
    # Exec time 47 18s 18s 18s 18s 18s 0 18s 
    
    # Lock time 0 103us 103us 103us 103us 103us 0 103us 
    
    # Rows sent 0 0 0 0 0 0 0 0 
    
    # Rows examine 0 0 0 0 0 0 0 0 
    
    # Query size 0 21 21 21 21 21 0 21 
    
    # String: 
    
    # Databases  testDB
    
    # Hosts localhost 
    
    # Users root 
    
    # Query_time distribution 
    
    # 1us 
    
    # 10us 
    
    # 100us 
    
    # 1ms 
    
    # 10ms 
    
    # 100ms 
    
    # 1s 
    
    # 10s+ ################################################################ 
    

    可以看到报告中,列举出了一些sql语句响应时间占比情况,以及SQL语句的执行时间情况。方便我们可以很直观的观察哪些语句有问题。

  • pt-index-usage

    pt-index-usage命令能够连接到MySQL数据库服务器,读取慢查询日志,并使用EXPLAIN询问MySQL如何执行每个查询。分析完成时,它打印出一个关于查询没有使用的索引的报告。

    对于我们已有的生产环境,随着系统运行的时间越长,DML操作越来越慢,这可能和我们最初设计的索引是有关的(变慢的情况很多),项目一旦上线,很少会有人去关注索引的使用情况。某些索引是从create开始就没使用过,这无形中就给MySQL增加了维护负担,任何对该表的DML操作,都要维护这些没有被使用的索引。我们可以使用pt-index-usage工具找出哪些索引一直没有被使用,然后进行删除。

    pt-index-usage语法格式如下:

    pt-index-usage [OPTION...] [FILE...] 
    

    打印报告:

    pt-index-usage /path/to/slow_OAK.log --host localhost
    

    也可以将报告写入到一个数据库表中,存入后方便我们的查看。如果在生产服务器上使用此功能,则应该小心,它可能会增加负载。

    pt-index-usage slow.log --no-report --save-results-database percona
    
  • pt-online-schema-change

    pt-online-schema-change命令可以在线整理表结构,收集碎片,给大表添加字段和索引。避免出现锁表导致阻塞读写的操作。针对 MySQL 5.7 版本,就可以不需要使用这个命令,直接在线 online DDL 就可以了。

    pt-online-schema-change --user=root --password=root --host=localhost -- alter="ADD COLUMN city_bak VARCHAR(256)" D=lagou,t=city --execute 
    
  • pt-table-checksum

    pt-table-checksum命令可以检查主从复制一致性。pt table checksum通过在主机上执行校验和查询来执行在线复制一致性检查。如果发现任何差异,或者出现任何警告或错误,则工具的“退出状态”为非0

    该命令将连接到本地主机上的复制主机,对每个表进行校验和,并报告每个检测到的复制副本的结果:

    比较testDB库的差异情况,在主库上面执行:

    [root@node1 bin]# ./pt-table-checksum --no-check-binlog-format --nocheck- replication-filters --databases=testDB --replicate=testDB.checksums -- host=192.168.95.130 -uroot -proot 
    
    TS ERRORS DIFFS ROWS CHUNKS SKIPPED TIME TABLE 
    
    05-10T18:01:05 0 0 1 1 0 0.013 
    
    testDB.heartbeat 
    
    05-10T18:01:05 0 0 0 1 0 0.015 testDB.city 
    
    05-10T18:01:05 0 0 0 1 0 0.011 testDB.position 
    

    上述结果显示diff都为0,证明主从的lagou库没有差异情况。

    比较testDB库哪些表有差异(需要添加replicate-check-only),在主库上面执行:

    [root@node1 bin]# ./pt-table-checksum --no-check-binlog-format --nocheck-replication-filters --databases=lagou --replicate=testDB.checksums --replicate-check-only --host=192.168.95.130 -uroot -proot 
    
    Differences on node1 
    
    TABLE CHUNK CNT_DIFF CRC_DIFF CHUNK_INDEX LOWER_BOUNDARY UPPER_BOUNDARY 
    
    testDB.position_detail 1 1 1
    

    上述结果显示testDB库下面position_detail表主从数据不一致。

6.5 MySQLMTOP

官网文档:http://www.lepus.cc/

MySQLMTOP 是一个由Python+PHP开发的开源MySQL企业监控系统。该系统由Python实现多进程数据采集和告警,PHP实现Web展示和管理,优点如下:

  • MySQL服务器无需安装任何Agent,只需在监控WEB界面配置相关数据库信息
  • 启动监控进程后,即可对上百台MySQL数据库的状态、连接数、QTS、TPS、数据库流量、复制、性能慢查询等进行实时监控。
  • 可以在数据库偏离设定的正常运行阀值(如连接异常,复制异常,复制延迟) 时发送告警邮件通知到DBA进行处理。
  • 可以对历史数据归档,通过图表展示数据库近期状态,以便DBA和开发人员能对遇到的问题进行分析和诊断。

MySQLMTOP主要功能如下:

  • 实时 MySQL 状态监控和警报

    MySQLMTOP 持续监视 MySQL 的基本状态和性能信息,包括数据库连接状态,启动时间,数据库版本,总连接数,活动进程,QPS,TPS,进出MySQL数据库的流量信息。在数据库状态异常或偏离正常基准水平时发出报警邮件通知。

  • 实时 MySQL复制监控

    MySQLMTOP自动发现 MySQL 复制拓扑结构,自动监视数据库的延时和binlog信息,可以了解所有 MySQL 主服务器和从服务器的性能、可用性和运行状况。并在问题(如从服务器延迟)导致停机前向管理员提供改正建议。

    截屏2023-06-01 18.55.45

  • 远程监控云中的 MySQL

    适合于云和虚拟机的设计,能远程监视MySQL服务器不需要任何远程代理器

  • 直观管理所有 MySQL

    MySQLMTOP提供一个基于 Web 的界面,可令全面深入地了解数据库性能、可用性、关键活动等;直观地查看一台服务器、自定义的应用组或所有服务器。一组丰富的实时图形和历史图形将帮助您深入了解详细的服务器统计信息。

    截屏2023-06-01 18.55.29

  • 可视化MySQL慢查询分析

    监视实时查询性能,查看执行统计信息,筛选和定位导致性能下降的 SQL 代码。结合使用InformationSchema 可直接从 MySQL 服务器收集数据,无需额外的软件或配置。

  • 性能监控

    监视影响 MySQL 性能的主要指标。如Key_buffer_read_hits、Key_buffer_write_hits、Thread_cache_hits、Key_blocks_used_rate、Created_tmp_disk_tables_rate等信息,根据相关性能指标可以对服务器核心参数进行调整优化。

6.6 ELK

在简单应用中,直接在日志文件中 grep就可以获得自己想要的信息。但在规模较大分布式系统中,此方法效率低下,面临问题包括日志量太大如何归档、文本搜索太慢怎么办、如何多维度查询。需要集中化的日志管理,所有服务器上的日志收集汇总。常见解决思路是建立集中式日志收集系统,将所有节点上的日志统一收集,管理,访问。

一般大型系统是一个分布式部署的架构,不同的服务模块部署在不同的服务器上,问题出现时,大部分情况需要根据问题暴露的关键信息,定位到具体的服务器和服务模块。构建一套集中式日志系统,可以提高定位问题的效率。

一个完整的集中式日志系统,需要包含以下几个主要特点:

  • 收集-能够采集多种来源的日志数据
  • 传输-能够稳定的把日志数据传输到中央系统
  • 存储-如何存储日志数据
  • 分析-可以支持 UI 分析
  • 警告-能够提供错误报告,监控机制

ELK提供了一整套解决方案,并且都是开源软件,之间互相配合使用,完美衔接,高效的满足了很多场合的应用。目前主流的一种日志系统。

ELK 最早是 Elasticsearch(简称ES)、Logstash、Kibana 三款开源软件的简称,三款软件后来被同一公司收购,并加入了Xpark、Beats等组件,改名为Elastic Stack,成为现在最流行的开源日志解决方案,虽然有了新名字但大家依然喜欢叫ELK,现在所说的ELK就指的是基于这些开源软件构建的日志系统

ELK实现架构

截屏2023-06-05 18.18.09

上述架构中,各技术作用如下

  • MySQL 服务器安装 Filebeat 作为 agent 收集 slowLog

  • Filebeat 读取 MySQL 慢日志文件做简单过滤传给 Kafka 集群

  • Logstash 读取 Kafka 集群数据并按字段拆分后转成 JSON 格式存入 ES 集群

  • Kibana 读取ES集群数据展示到web页面上

慢查询日志收集展示

  • Filebeat日志收集

    filebeat读取收集slow log,处理后写入kafka。收集日志时需要解决以下问题:

    • 日志行合并
    • 获取SQL执行时间
    • 确定SQL对应的DB名
    • 确定SQL对应的主机

    Filebeat示例

    截屏2023-06-05 18.20.13

    重要参数解释:

    • input_type:指定输入的类型是log或者是stdin

    • paths:慢日志路径,支持正则,比如/data/*.log

    • exclude_lines:过滤掉#Time开头的行

    • multiline.pattern:匹配多行时指定正则表达式,这里匹配以# Time或者# User开头的行,Time行要先匹配再过滤

    • multiline.negate:定义上边pattern匹配到的行是否用于多行合并,也就是定义是不是作为日志的一部分

    • multiline.match:定义匹配的行日志作为日志元素的开始还是结束。

    • tail_files:定义是从文件开头读取日志还是结尾,这里定义为true,从现在开始收集,之前已存在的不管

    • name:设置fifilebeat的名字,如果为空则为服务器的主机名,这里我们定义为服务器IP

    • output.kafka:配置要接收日志的kafka集群地址可topic名称

  • Kafka 接收示例

    {"@timestamp":"2020-05-08T09:36:00.140Z","beat": 
    
    {"hostname":"oak","name":"10.63.144.71","version":"5.4.0"},"input_type":"log","m 
    
    essage":"# User@Host: select[select] @ [10.63.144.16] Id: 23460596\n# 
    
    Query_time: 0.155956 Lock_time: 0.000079 Rows_sent: 112 Rows_examined: 
    
    366458\nSET timestamp=1533634557;\nSELECT DISTINCT(uid) FROM common_member WHERE 
    
    hideforum=-1 AND uid != 
    
    0;","offset":1753219021,"source":"/data/slow/mysql_slow.log","type":"log"}
    
  • Logstash示例

    Logstash消费kafka消息,可以利用kafka consumer group实现集群模式消费保障单点故障不影响日志处理,grok正则处理后写入elasticsearch。

    截屏2023-06-05 18.26.51

    重要参数解释:

    • input:配置 kafka 的集群地址和 topic 名字

    • filter:过滤日志文件,主要是对 message 信息(前文 kafka 接收到的日志格式)进行拆分,拆分成一个一个易读的字段,例如User、Host、Query_time、Lock_time、timestamp等。

    • grok:MySQL版本不同,慢查询日志格式有些差异,grok可以根据不同的慢查询格式写不同的正则表达式去匹配,当有多条正则表达式存在时,logstash会从上到下依次匹配,匹配到一条后边的则不再匹配。

    • date:定义让SQL中的timestamp_mysql字段作为这条日志的时间字段,kibana上看到的时间排序的数据依赖的就是这个时间

    • output:配置ES服务器集群的地址和index,index自动按天分割

6.7 Prometheus

Prometheus于2012年由SoundCloud创建,目前已经已发展为最热门的分布式监控系统。

Prometheus完全开源的,被很多云厂商(架构)内置,在这些厂商(架构)中,可以简单部署Prometheus,用来监控整个云基础架构设施。比如DigitalOcean或Docker都使用普罗米修斯作为基础监控。

Prometheus是一个时间序列数据库,它涵盖了可以绑定的整个生态系统工具集及其功能。

Prometheus主要用于对基础设施的监控,包括服务器、数据库、VPS,几乎所有东西都可以通过Prometheus进行监控。

Prometheus主要优点如下:

  • 提供多维度数据模型和灵活的查询方式。通过将监控指标关联多个tag,来将监控数据进行任意维度的组合,并且提供简单的PromQL,还提供查询接口,可以很方便地结合等GUI组件展示数据。

  • 在不依赖外部存储的情况下,支持服务器节点的本地存储。通过Prometheus自带的时序数据库,可以完成每秒千万级的数据存储。

  • 定义了开发指标数据标准,以基于HTTP的pull方式采集时序数据。只有实现了Prometheus监控数据格式的监控数据才可以被Prometheus采集、汇总。

  • 支持通过静态文件配置和动态发现机制发现监控对象,自动完成数据采集。Prometheus目前已经支持Kubernetes、etcd、consul等多种服务发现机制,可以减少运维人员的手动配置环节。

  • 易于维护,可以通过二进制文件直接启动,并且提供容器化部署镜像。

  • 支持数据的分区采集和联邦部署,支持大规模集群监控。

生态系统

  • Alertmanager

    Prometheus通过配置文件定义规则将告警信息推送到Alertmanager。Alertmanager可以将其导出到多个端点,例如Pagerduty或Email等。

  • 数据可视化

    在Web UI中可视化时间序列数据,轻松过滤查看监控目标的信息,与Grafana、Kibana等类似。

  • 服务发现

    Prometheus可以动态发现监控目标,并根据需要自动废弃目标。这在云架构中使用动态变更地址的容器时,非常方便。

    截屏2023-06-05 18.35.21

实现原理

  • 数据存储

    Prometheus指标数据支持本地存储和远程存储。

    截屏2023-06-05 18.37.57

  • 指标数据

    Prometheus使用键-值对存储监控数据。键描述了测量值时将实际测量值存储为数字的值。Prometheus并不会存储原始信息,如日志文本,它存储的是随时间汇总的指标。

    一般来说键也就监控指标,如果想要获得有关指标的更多详细信息,Prometheus有一个标签的概念。标签旨在通过向键添加其他字段来为指标提供更详细信息。

    截屏2023-06-05 18.38.23

  • 度量类型

    Prometheus监控指标有Counter(计数器)、Gauge(仪表盘)、Histogram(直方图)和Summary(摘要)四种度量类型。

    • Counter

      计数器Counter是我们使用的最简单的度量标准形式。计数器的值只能增加或重置为0,比如,要计算服务器上的HTTP错误数或网站上的访问次数,这时候就使用计数器。

    • Gauges

      Gauges用于处理随时间减少或增加的值。比如温度、内存变化等。Gauge类型的值可以上升和下降,可以是正值或负值。

    • Histogram

      直方图Histogram是一种更复杂的度量标准类型。它为我们的指标提供了额外信息,例如观察值的总和及其数量,常用于跟踪事件发生的规模。其值在具有可配置上限的存储对象中聚合。比如,为了监控性能指标,我们希望得到在有20%的服务器请求响应时间超过300毫秒发送警告。对于涉及比例的指标就可以考虑使用直方图。

    • Summary

      摘要Summary是对直方图的扩展。除了提供观察的总和和计数之外,它们还提供滑动窗口上的分位数度量。分位数是将概率密度划分为相等概率范围的方法。直方图随时间汇总值,给出总和和计数函数,使得易于查看给定度量的变化趋势。而摘要则给出了滑动窗口上的分位数(即随时间不断变化)。

  • PromQL

    对于Prometheus数据,我们可以通过HTTP来查询,如果是复杂的数据查询,则还可以使用PromQL进行。和关系型数据库实现SQL解析一样,Prometheus实现了一套自己的数据库语言解析器,最大的区别是支持查询。

    使用Prometheus的PromQL,会处理两种向量:

    • 即时向量:表示在最近时间戳中跟踪的指标。

    • 时间范围向量:用于查看度量随时间的演变,可以使用自定义时间范围查询Prometheus。

    PromQL API公开了一组方便查询数据操作的函数。用它可以实现排序,数学函计算(如导数或指数函数),统计预测计算(如Holt Winters函数)等。

  • Instrumentation仪表化

    仪表化是Prometheus的一个重要组成部分。在从应用程序检索数据之前,必须要仪表化它们。Prometheus术语中的仪表化表示将客户端类库添加到应用程序,以便它们向Prometheus吐出指标。可以对大多数主流的编程语言进行仪表化。

    在仪表化操作时,需要创建内存对象(如仪表盘或计数器),然后选择指标公开的位置。Prometheus将从该位置获取并存储到时间序列数据库。

  • Exporters模板

    Exporter是一个采集监控数据样本的组件。除了官方实现的Exporter,还有很多第三方实现如 Redis exporter 和RabbitMQ exporter等。这些Exporters通过HTTPS/HTTP方式、TCP方式、本地文件方式或标准协议方式访问。

    常见的Exporters模版有:

    • 数据库模版

      用于MongoDB数据库,MySQL服务器的配置。

    • HTTP模版

      用于HAProxy,Apache或Nginx等Web服务器和代理的配置。

    • Unix模版

      用来使用构建的节点导出程序监视系统性能,可以实现完整的系统指标的监控。

    截屏2023-06-05 18.41.14

  • Alertmanager告警

    在处理时间序列数据库时,我们希望对数据进行处理,并对结果给出反馈,而这部分工作由告警来实现。

    告警在Grafana中非常常见,Prometheus是通过Alertmanager实现完成的告警系统。Alertmanager是一个独立的工具,可以绑定到Prometheus并运行自定义Alertmanager。告警通过配置文件定义,定义由一组指标规则组成,如果数据命中这些规则,则会触发告警并将信息发送到预定义的目标。

    Prometheus的告警,可以通过email,Slack webhooks,PagerDuty和自定义HTTP目标等

    截屏2023-06-05 18.42.13

Logo

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。

更多推荐