一、文件
配置文件
用于存放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 文件(表名.ibd)。共享表空间使用 .ibdata 文件,所有表共同使用一个(或多个,自行配置, indata001 002这 种).ibdata 文件。 ibdata1 文件:系统表空间数据文件,存储表元数据、Undo日志等 。
ib_logfifile0、ib_logfifile1 文件:Redo log 日志文件。 二、运行机制
1、连接机制
建立连接(Connectors&Connection Pool),通过客户端/服务器通信协议与MySQL建立连接(TCP连 接,有握手和挥手的概念)。MySQL 客户端与服务端的通信方式是 “ 半双工 ”。对于每一个 MySQL 的连 接,时刻都有一个线程状态来标识这个连接正在做什么。
通讯机制:
全双工:能同时发送和接收数据,例如平时打电话。对应到网络就是客户端和服务端能同时发收,就 是任意一方都能随意收发,两个方向都是开放的。
半双工:指的某一时刻,要么发送数据,要么接收数据,不能同时。例如早期对讲机。mysql就是这 个模式,客户端发完服务端收到才能返回,不能既收又发
单工:只能发送数据或只能接收数据。例如单行道,区分任务,一方只能收,一方只能发。
线程状态:
每一个连接都是一个线程状态,所以我们可以通过监控这个线程状态来分析哪些连接在干吗?只能看到 当前的。
show processlist; //查看用户正在运行的线程信息,root用户能查看所有线程,其他用户只能看自己 的。
下面解释一下这些参数的意思:
id:线程ID,可以使用kill xx;就是你当前这个连接的线程id。 user:启动这个线程的用户,你用什么用户启动的,正轨的mysql使用要设置用户和分组的。 Host:发送请求的客户端的IP和端口号,注意是客户端。 db:当前命令在哪个库执行,没连库就是null Command:该线程正在执行的操作命令 Create DB:正在创建库操作 Drop DB:正在删除库操作 Execute:正在执行一个PreparedStatement Close Stmt:正在关闭一个PreparedStatement Query:正在执行一个语句 Sleep:正在等待客户端发送语句 Quit:正在退出 Shutdown:正在关闭服务器 Time:表示该线程处于当前状态的时间,单位是秒,注意是距离当前你执行show processlist的时间。最 新的就是0 State:线程状态 Updating:正在搜索匹配记录,进行修改 Sleeping:正在等待客户端发送新请求 Starting:正在执行请求处理 Checking table:正在检查数据表 Closing table : 正在将表中数据刷新到磁盘中 Locked:被其他查询锁住了记录 Sending Data:正在处理Select查询,同时将结果发送给客户端
查询缓存(Cache&Buffffer) 这是MySQL的一个可优化查询的地方,如果开启了查询缓存且在查询缓存过程中查询到完全相同(参数 也必须完全一致)的SQL语句,则将查询结果直接返回给客户端;如果没有开启查询
缓存或者没有查询到完全相同的 SQL 语句则会由解析器进行语法语义解析,并生成“解析树”。然后走具 体查询。
解析器(Parser) 将客户端发送的SQL进行语法解析,生成"解析树"。预处理器根据一些MySQL规则进一步检查“解析树”是 否合法,例如这里将检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义,后 生成新的终的“解析树”。
查询优化器(Optimizer) 根据“解析树”生成优的执行计划。MySQL使用很多优化策略生成优的执行计划,可以分为两类:静 态优化(编译时优化)、动态优化(运行时优化)。
查询执行引擎
查询执行引擎负责执行 SQL 语句,此时查询执行引擎会根据 SQL 语句中表的存储引擎类型,以及对应 的API接口与底层存储引擎缓存或者物理文件的交互,得到查询结果并返回给客户端。若开启用查询缓 存,这时会将SQL 语句和结果完整地保存到查询缓存(Cache&Buffffer)中,以后若有相同的 SQL 语 句执行则直接返回结果。
如果开启了查询缓存,先将查询结果做缓存操作。默认是关闭的。这玩意不好,8.0直接废除。 返回结果过多,mysq底层是采用增量模式返回。就是不会一次全部返回,会分批次,增量返回。
Info:一般记录线程执行的语句,默认显示前100个字符。想查看完整的使用命令:show full processlist;
缓存Select查询的结果和SQL语句 执行Select查询时,先查询缓存,判断是否存在可用的记录集,要求是否完全相同(包括参数值),这样才会 匹配缓存数据命中。 即使开启查询缓存,以下SQL也不能缓存 查询语句使用SQL_NO_CACHE 查询的结果大于query_cache_limit设置,指的是一个缓存最大的大小 查询中有一些不确定的参数,比如now() show variables like '%query_cache%'; //查看查询缓存是否启用,空间大小,限制等 show status like 'Qcache%'; //查看更详细的缓存参数,可用缓存空间,缓存块,缓存多少等
等价变换策略 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)
三、存储引擎 1、Innodb存储结构
从MySQL 5.5版本开始默认使用InnoDB作为引擎,它擅长处理事务,具有自动崩溃恢复的特性,在日常 开发中使用非常广泛。下面是官方的InnoDB引擎架构图,主要分为内存结构和磁盘结构两大部分。
1.1、InnoDB内存结构
内存结构主要包括Buffffer Pool、Change Buffffer、Adaptive Hash Index和Log Buffffer四大组件。
Buffer Pool:缓冲池,简称BP。BP以Page页为单位,默认大小16K,BP的底层采用链表数据结构管理 Page。在InnoDB访问表记录和索引时会在内存中的Page页中缓存,以后使用可以减少磁盘IO操作,提升效 率。 Page管理机制 Page页根据状态可以分为三种类型: free page : 空闲page,未被使用,还没有存数据呢 clean page:被使用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 list里面有页要淘汰的时候,说明不够用了,需要淘汰, 就会触发刷页操作,把脏页刷进磁盘,腾出空的页,这时候要是操作了dirty page的部分那么管理 他的flush list里面的也会刷盘,腾出空闲位置,保持这三个链表的同步,不能这里的脏页没了,被处理 了,你那里面还在。 改进型LRU算法维护 普通LRU:末尾淘汰法,新数据从链表头部加入,释放空间时从末尾淘汰
改性LRU:链表分为new和old两个部分,加入元素时并不是从表头插入,而是从中间midpoint位 置(new的尾部old的头部)插入,如果数据被访问,那么page就会向 new列表头部移动,如果数据 没有被访问,会逐步向old尾部移动,等待淘汰。每当有新的page数据读取到buffer pool时,InnoDb引擎 会判断是否有空闲页,是否足 够,如果有就将free page从free list列表删除,放入到LRU列表 中。没有空闲页,就会根据LRU算法淘汰LRU链表末尾的页(就是old部分的尾部开始淘汰),将内存 空间释放分配给新的页。 Buffer Pool配置参数,配置的好,就能提高命中,不用去磁盘,但是你不能过大,不然会挤压其他部 分的资源空间。 show variables like '%innodb_page_size%'; //查看page页大小 show variables like '%innodb_old%'; //查看lru list中old列表参数 show variables like '%innodb_buffer%'; //查看buffer pool参数 建议:将innodb_buffer_pool_size设置为总内存大小的60%-80%, innodb_buffer_pool_instances可以设置为多个,这样可以避免缓存争夺。块的话大一点就能 多放几个页,这个调不调好像没多大用。 --------------------------------------------------------------------------------------------------------------------------Change Buffer:写缓冲区,简称CB。在进行DML(增删改)操作时,如果BP没有其相应的Page数据,并不会 立刻将磁盘页加载到缓冲池,而是在CB记录缓冲变更(就是把你的变动操作记录在这里),等未来数据被读取 时,再将数据合并恢复到BP中。5.5版本之前叫做insert buffer,就是只对插入操作做缓冲,5.5之后才对 所有的DML语句做缓冲。而且你看上图能看出来其实他是属于Buffer poll的一部分,他使用的就是BP的空 间。
ChangeBuffer占用BufferPool空间,默认占25%,最大允许占50%,可以根据读写业务量来进行调整。通 过调整参数innodb_change_buffer_max_size; 当更新一条记录时,该记录在BufferPool存在,直接在BufferPool修改,一次内存操作。如果该记录在 BufferPool不存在(没有命中),会直接在ChangeBuffer进行一次内存操作,不用再去磁盘查询数据,避 免一次磁盘IO。当下次查询这条修改的记录时,会先进性磁盘读取,然后再从ChangeBuffer中读取信息合并 完成修改,最终载入BufferPool中。后续BufferPool里面的数据会和磁盘再做同步。
写缓冲区,仅适用于非唯一普通索引页(主键和唯一性索引都不行),为什么? 如果在索引设置唯一性,在进行修改时,InnoDB必须要做唯一性校验,那么怎么做唯一性校验呢,就是去磁盘 读取数据去做,因此必须查询磁盘,做一次IO操作,所以索性就不用这个change buffer了,反正你怎么地 都要磁盘io。会直接将记录查询到BufferPool中,然后在缓冲池修改,不会在ChangeBuffer操作。
-----------------------------------------------------------------------------------------------------------------------------Adaptive Hash Index:自适应哈希索引,用于优化对BP数据的查询。InnoDB存储引擎会监控对表索引的 查找,如果mysql观察到建立哈希索引可以带来速度的提升,则建立哈希索引,所以称之为自适应。InnoDB存 储引擎会自动根据访问的频率和模式来为某些page页建立哈希索引。以后查询的时候根据hash的O(1)的时间 复杂度有优势。用的是BP的空间。
-----------------------------------------------------------------------------------------------------------------------------Log Buffer:日志缓冲区,用来保存要写入磁盘上log文件(Redo/Undo)的数据,这些日志记录不是立即 会保存到日志文件中,而是先记录在日志缓冲区中,日志缓冲区的内容定期刷新到磁盘log文件中。日志缓冲 区满时会自动将其刷新到磁盘,当然了没满也会定时刷,当遇到BLOB或多行更新的大事务操作时,适当增加日 志缓冲区大小可以节省磁盘I/O,把多次操作统一到缓冲区一起刷入,不用每次都刷入,减少磁盘IO次数。当 然也不能一味的设置过大,不然你重启恢复也慢。一般就是默认16M,当然你看实际情况。 LogBuffer主要是用于记录InnoDB引擎日志,在DML操作时会产生Redo和Undo日志。 可以通过SHOW VARIABLES LIKE '%innodb_log%'查看log_buffer的一些信息。 LogBuffer空间满了,会自动写入磁盘。可以通过将innodb_log_buffer_size参数调大,减少磁盘IO频 率,没满也有写入策略,下面看。 innodb_flush_log_at_trx_commit参数控制日志刷新行为,默认为1 0:每隔1秒写日志文件和刷盘操作(写日志文件LogBuffer-->OS cache,再由系统操作刷盘 OScache-->磁盘文件),最多丢失1秒数据 1:事务提交,立刻写日志文件和刷盘,数据不丢失,但是会频繁IO操作,安全性好,但是性能上...
命令操作
使用SHOW ENGINE INNODB STATUS;可以看到innodb里面的一些配置信息。输出结果有一部分是 buffer pool的显示,我们来看看。
Buffer poll是缓存池。
里面有两块地方是change buffer和自适应哈希索引。change buffer先不说。 自适应哈希索引是根据你访问缓存中的命中次数,为缓存中的数据页建立索引映射的,类型是hash类型 的,hash的速度是1,所以就很快的就能找到缓存中的页进一步提高速度。key是页,值就是地址指针映 射。
查看Buffer Poll中的数据配置数据
2:事务提交,立刻写日志文件进os cache但是不是立即刷盘,每隔1秒钟进行刷盘操作,相当于上面两 个的折中,一般可以设置为这个 这里注意一个东西,就是我们说的日志刷盘一般指的是把日志数据从日志缓冲区刷盘到OS cache这个区 域,然后具体再由操作系统把数据从os cache刷盘到实际的磁盘文件中。但是你要是一旦进了系统缓冲,那 么即便你Mysql宕机了数据也不丢,因为数据已经脱离mysql了。 但是上面的BUffer poll的刷盘,则会穿过os cache(上面的图有体现),直接到达磁盘文件中。这 是他们的不同之处,注意一下,至于为什么这么设计,我理解是日志没什么命中一说,就是往进写,所以经常 要进磁盘,所以就要多经过一次缓冲区,减少io之类的。
BUFFER POOL AND MEMORY // BP ---------------------Total large memory allocated 8585216 Dictionary memory allocated 1156667 Buffer pool size 512 这里是bp的大小是512,表示的是512个页,也就是bp大小有512个页之大 Free buffers 254 这里是free list的大小是254个页 Database pages 256 这里是lru list的大小是256个页 你会发现Free buffers 和 Database pages加起来小于Buffer pool size,这是因为这个bp里面的空间可能会分配给自适应索引, 所以有时候会小于。 Old database pages 0 这里我们说lru list是有new 和 old两个热冷分区,这里就是old部分, 现在我表里没数据,就是空的 Modified db pages 0 这里就是脏页数量,就是dirty page的个数 Pending reads 0 Pending writes: LRU 0, flush list 0, single page 0 Pages made young 0, not young 0 0.00 youngs/s, 0.00 non-youngs/s Pages read 431, created 57, written 253 0.00 reads/s, 0.00 creates/s, 0.00 writes/s No buffer pool page gets since the last printout Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s LRU len: 256, unzip_LRU len: 0 I/O sum[0]:cur[0], unzip sum[0]:cur[0]
show variables like '%innodb_buffer%';
来解释一下这几个配置:
chunk_size:就是块大小。
instance:是实例个数 pool_size:就是bp的整个缓冲区大小, 是这么个关系,pool里面可以设置多个实例个数,避免你就一个实例的时候,访问缓存的时候出现争 抢,多个实例就是可以减缓这种争抢。让各个客户端去不同的实例去查找。
每个实例里面又有多个块,每个块里面就有多个页,是这么个关系。
所以实例个数 * 块大小就是pool的大小。
pool-size默认是8388608,但是你可以调大,一般大可以调为总内存的百分之六十到八十左右,当然了 看环境,你内存不够就别作死。这个大了,实例个数也可以多几个,减少争抢。
看一个这个命令:查看lru list中old列表参数
查看old列表参数,上面的pct就是占百分比,是37,就是old区占37.然后下面的时间是1000,就是1 秒。他的意思是,一旦数据页被访问了,那么old区的数据被访问就应该移动去new区么,他不是立刻移 动的,而是要一秒后,这个就是这个配置。是为了不立刻移动影响操作。缓和一秒
查看log_buffer的信息
show variables like '%innodb_old%';
SHOW VARIABLES LIKE '%innodb_log%'
解释一下上面的参数:
innodb_log_buffer_size:就是日志缓冲区的总大小 innodb_log_file_size:单个日志文件的大小 innodb_log_files_in_group:每组日志文件的个数,可以看出来,日志文件是按组划分的,默认每组两 个
上面参数都能根据实际情况调整。来优化数据库。 1.2、InnoDB磁盘结构
InnoDB磁盘主要包含Tablespaces,InnoDB Data Dictionary,Doublewrite Buffffer、Redo Log和 Undo Logs。 1.2.1、表空间(Tablespaces)
系统表空间(The System Tablespace) 包含InnoDB数据字典,Doublewrite Buffffer,Change Buffffer,Undo Logs的存储区域。系统表空间 也默认包含任何用户在系统表空间创建的表数据和索引数据。系统表空间是一个共享的表空间因为它是 被多个表共享的。该空间的数据文件通过参数innodb_data_fifile_path控制,默认值是 ibdata1:12M:autoextend(文件名为ibdata1、12MB、自动扩展)。可以用SHOW VARIABLES LIKE去查 看。
独立表空间(File-Per-Table Tablespaces)
默认开启,独立表空间是一个单表表空间,以表为单位,该表创建于自己的数据文件中,而非创建于系 统表空间中。当innodb_fifile_per_table选项开启时,表将被创建于表空间中。否则,innodb将被创建 于系统表空间中。每个表文件表空间由一个.ibd数据文件代表,该文件默认被创建于数据库目录中。表 空间的表文件支持动态(dynamic)和压缩(commpressed)行格式。5.7默认是开启的独立表空间, 所以官方其实是推荐这个的。
通用表空间(General Tablespaces)
通用表空间为通过create tablespace语法创建的共享表空间。通用表空间可以创建于mysql数据目录外 的其他表空间,其可以容纳多张表,且其支持所有的行格式。其文件也是ibd文件。具体理解可以去看上 面那张整体结构图。
# 用于存储表结构和数据。表空间又分为系统表空间、独立表空间、通用表空间、临时表空间、Undo表空间 等多种类型;
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服务器正常关闭或异常终止时,临时表空间将被移除, 每次启动时会被重新创建。
frm文件和上面的这些文件有所重叠。 1.2.2、数据字典(InnoDB Data Dictionary)
InnoDB数据字典由内部系统表组成,这些表包含用于查找表、索引和表字段等对象的元数据。元数据物 理上位于InnoDB系统表空间中。由于历史原因,数据字典元数据在一定程度上与InnoDB表元数据文件 (.frm文件)中存储的信息重叠。 1.2.3、双写缓冲区(Doublewrite Buffffer)
位于系统表空间,是一个存储区域。在BufffferPage的page页刷新到磁盘真正的位置前,会先将数据存 在Doublewrite 缓冲区。如果在page页写入过程中出现操作系统、存储子系统或mysqld进程崩溃, InnoDB可以在崩溃恢复期间从Doublewrite 缓冲区中找到页面的一个好备份(这个已经在系统里面了, mysql宕机不影响,但是系统崩了,那就。。。而且往这里面写也快一些,我们可以看到mysql设置了大 量的多级缓冲)。在大多数情况下,默认情况下启用双写缓冲区,要禁用Doublewrite 缓冲区,可以将 innodb_doublewrite设置为0。如果使用Doublewrite 缓冲区时建议将innodb_flflush_method设置为 O_DIRECT,可以提高销量表,缓冲区可以合并多次io操作为一次。
1.2.4、重做日志(Redo Log)
重做日志是一种基于磁盘的数据结构,用于在崩溃恢复期间更正不完整事务写入的数据。MySQL以循环 方式写入重做日志文件,记录InnoDB中所有对Buffffer Pool修改的日志。当出现实例故障(像断电), 导致数据未能更新到数据文件,则数据库重启时须redo,重新把数据更新到数据文件。读写事务在执行 的过程中,都会不断的产生redo log。默认情况下,重做日志在磁盘上由两个名为ib_logfifile0和 ib_logfifile1的文件物理表示。 1.2.5、撤销日志(Undo Logs)
撤消日志是在事务开始之前保存的被修改数据的备份,用于例外情况时回滚事务。撤消日志属于逻辑日 志,根据每行记录进行记录。撤消日志存在于系统表空间、撤消表空间和临时表空间中。
MySQL的innodb_flush_method这个参数控制着innodb数据文件及redo log的打开、刷写模式。有三个 值:fdatasync(默认),O_DSYNC,O_DIRECT。设置O_DIRECT表示数据文件写入操作会通知操作系统不要 缓存数据,也不要用预读,直接从InnodbBuffer写到磁盘文件。 默认的fdatasync意思是先写入操作系统缓存,然后再调用fsync()函数去异步刷数据文件与redo log的缓 存信息。
1.3、新版本结构演变
上图是8.0了,开始是5.7的,你可以对比看看,确实有的地方不一样了。 1.3.1、MySQL 5.7 版本
将 Undo日志表空间从共享表空间 ibdata 文件中分离出来,可以在安装 MySQL 时由用户自行指定文件 大小和数量。
增加了 temporary 临时表空间,里面存储着临时表或临时查询结果集的数据。 Buffffer Pool 大小可以动态修改,无需重启数据库实例。 1.3.2、MySQL 8.0 版本
将InnoDB表的数据字典和Undo都从共享表空间ibdata中彻底分离出来了,以前需要ibdata中数据字典 与独立表空间ibd文件中数据字典一致才行,8.0版本就不需要了。 temporary 临时表空间也可以配置多个物理文件,而且均为 InnoDB 存储引擎并能创建索引,这样加快 了处理的速度。
用户可以像 Oracle 数据库那样设置一些表空间,每个表空间对应多个物理文件,每个表空间可以给多个 表使用,但一个表只能存储在一个表空间中。
将Doublewrite Buffffer从共享表空间ibdata中也分离出来了,也就有了双写的缓冲区的文件了。 2、InnoDB线程模型
mysql有一些后台线程,是用来维护缓冲数据中的更新和刷盘操作。
2.1、IO Thread
在InnoDB中使用了大量的AIO(Async IO 异步io读写)来做读写处理,这样可以极大提高数据库的性 能。在InnoDB1.0版本之前共有4个IO Thread,分别是write,read,insert buffffer和log thread,后 来版本将read thread和write thread分别增大到了4个,一共有10个了。
可以通过SHOW ENGINE INNODB STATUS;语句去查看这些个数信息。 read thread : 负责读取操作,将数据从磁盘加载到缓存page页。一共有4个读线程 write thread:负责写操作,将缓存脏页刷新到磁盘。4个 log thread:负责将日志缓冲区内容刷新到磁盘。1个 insert buffffer thread :负责将写缓冲内容刷新到磁盘,就是你磁盘和change buffer合并之后, 由这个线程刷入磁盘。1个 2.2、Purge Thread
事务提交之后,其使用的undo日志将不再需要,不需要用来做回滚了,因此需要Purge Thread回收已 经分配的undo页,减少空间占用。 show variables like '%innodb_purge_threads%';
2.3、Page Cleaner Thread
作用是将脏数据刷新到磁盘,脏数据刷盘后相应的redo log也就可以覆盖,就能清除这些redo log之类 的一些释放,即可以同步数据,又能达到redo log循环使用的目的。其内部是会调用write thread线程处 理操作的。
show variables like '%innodb_page_cleaners%';
2.4、Master Thread
Master thread是InnoDB的主线程,负责调度其他各线程,优先级高。作用是将缓冲池中的数据异步 刷新到磁盘 ,保证数据的一致性。包含:脏页的刷新(page cleaner thread)、undo页回收(purge thread)、redo日志刷新(log thread)、合并写缓冲等。内部有两个主处理,分别是每隔1秒和10秒 处理。这两个主处理都是存在的,一起作用。
因为都在主线程里面做会效率低,就成了redis,所以分出多个线程。当然了你启用这些线程才能做,要 是都没启用,那就还是在主线程做。
每1秒的操作: 刷新日志缓冲区,刷到磁盘 合并写缓冲区数据,根据IO读写压力来决定是否操作,不是真的每秒都做,每秒判断一次是 否操作 刷新脏页数据到磁盘,根据脏页比例达到75%才操作(innodb_max_dirty_pages_pct, innodb_io_capacity) 这个75%可以通过show variables like '%innodb_max_dirty_pages_pct%';查看,也可 以修改。
当然了还有一个是一次刷多少页的配置;
每10秒的操作: 刷新脏页数据到磁盘 合并写缓冲区数据 刷新日志缓冲区 删除无用的undo页,这个无用的一次是删除300个page页show variables like '%innodb_purge_batch_size%';
3、InnoDB数据文件 3.1、InnoDB文件存储结构
InnoDB数据文件存储结构: 分为一个ibd(默认是独立表空间)数据文件-->Segment(段)-->Extent(区)-->Page(页)-->Row (行)
可以这么说;每个表有自己的独立表空间,对应就是一个表名.ibd文件 每个表空间内部有多个segment段,有什么数据段,索引段,事务回滚段
每个段又由多个区Extent组成 每个区有多个page组成,具体就是64个连续的page页组成一个区 每个page就是有多个数据行记录Row组成,每个行有各个部分组成,什么事务id,指针,然后就是属性 值(具体的数据)
Tablesapce 表空间,用于存储多个ibd数据文件,用于存储表的记录和索引。一个文件包含多个段。 Segment 段,用于管理多个Extent,分为数据段(Leaf node segment)叶子上是数据、索引段 (Non-leaf nodesegment)、回滚段(Rollback segment)。一个表至少会有两个 segment,一个管理数据,一个管理索引。每多创建一个索引,会多两个segment(肯定会多 一个数据段和索引段,叶子和非叶子)。 Extent 区,一个区固定包含64个连续的页,大小为1M(每个页是16K)。当表空间不足,需要分配新的 页资源,不会一页一页分,直接分配一个区。 Page 页,用于存储多个Row行记录,大小为16K。包含很多种页类型,比如数据页,undo页,系 统页,事务数据页,大的BLOB对象页。 Row 行,包含了记录的字段值,事务ID(Trx id)、滚动指针(Roll pointer)和事务有关,后面 说、字段指针(Fieldpointers)便于获取字段值等信息。
Page是文件基本的单位,无论何种类型的page,都是由page header,page trailer和pagebody组 成。如下图所示,
3.2、InnoDB文件存储格式
通过 SHOW TABLE STATUS 命令
一般情况下,如果row_format为REDUNDANT、COMPACT,文件格式为Antelope;如果row_format 为DYNAMIC和COMPRESSED,文件格式为Barracuda。
通过 information_schema 查看指定表的文件格式
select * from information_schema.innodb_sys_tables;
3.3、File文件格式(File-Format)
在早期的InnoDB版本中,文件格式只有一种,随着InnoDB引擎的发展,出现了新文件格式,用于支持 新的功能。目前InnoDB只支持两种文件格式:Antelope 和 Barracuda。 Antelope: 先前未命名的,原始的InnoDB文件格式,它支持两种行格式:COMPACT和 REDUNDANT,MySQL 5.6及其以前版本默认格式为Antelope。 Barracuda: 新的文件格式。它支持InnoDB的所有行格式,包括新的行格式:COMPRESSED和 DYNAMIC。
通过innodb_fifile_format 配置参数可以设置InnoDB文件格式,之前默认值为Antelope,5.7版本开始 改为Barracuda。 3.4、Row行格式(Row_format)
表的行格式决定了它的行是如何物理存储的,这反过来又会影响查询和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_prefifix参数 控制。 COMPRESSED 行格式 COMPRESSED行格式提供与DYNAMIC行格式相同的存储特性和功能,但增加了对表和索引数 据压缩的支持。该字段被称为页外列。
在创建表和索引时,文件格式都被用于每个InnoDB表数据文件(其名称与*.ibd匹配)。修改文件格式 的方法是重新创建表及其索引,简单方法是对要修改的每个表使用以下命令:
3.5、Undo Log 3.5.1、 Undo Log介绍
Undo:意为撤销或取消,以撤销操作为目的,返回指定某个状态的操作。 Undo Log:数据库事务开始之前,会将要修改的记录存放到 Undo 日志里,当事务回滚时或者数据库崩 溃时,可以利用 Undo 日志,撤销未提交事务对数据库产生的影响。
Undo Log产生和销毁:Undo Log在事务开始前产生;事务在提交时,即便数据确认提交了,也并不会 立刻删除undo log,因为可能涉及到MVCC的操作还会用,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存储。
第一个是文件目录:
第二个是
第三个是每个undolog的文件大小,默认是128K
第四个是文件位于的表空间号,0表示系统表空间。你要是改为大于0的数字,比如2,表示就使用undo tablespace,而且undolog文件的个数为2,undo_001,undo_002 3.5.2、Undo Log作用
实现事务的原子性 Undo Log 是为了实现事务的原子性而出现的产物。事务处理过程中,如果出现了错误或者用 户执行了 ROLLBACK 语句,MySQL 可以利用 Undo Log 中的备份将数据恢复到事务开始之前 的状态。 实现多版本并发控制(MVCC) Undo Log 在 MySQL InnoDB 存储引擎中用来实现多版本并发控制。事务未提交之前,Undo Log保存了未提交之前的版本数据,Undo Log 中的数据可作为数据旧版本快照供其他并发事 务进行快照读。
ALTER TABLE 表名 ROW_FORMAT=格式类型;
show variables like '%innodb_undo%';
事务A手动开启事务,执行更新操作,首先会把更新命中的数据备份到 Undo Buffffer 中。 事务B手动开 启事务,执行查询操作,会读取 Undo 日志数据返回,进行快照读
3.6、Redo Log和Binlog
Redo Log和Binlog是MySQL日志系统中非常重要的两种机制,也有很多相似之处,下面介绍下两者细节 和区别。 3.6.1、 Redo Log日志
Redo Log介绍 Redo:顾名思义就是重做。以恢复操作为目的,在数据库发生意外时重现操作。你可以理解为,操作失 败了,重新来一次,就用这玩意。
Redo Log:指事务中修改的任何数据,将新的数据备份存储的位置(Redo Log),被称为重做日 志。顺序写磁盘的。
Redo Log 的生成和释放:随着事务操作的执行,就会生成Redo Log,在事务提交时会将产生Redo Log 写入Log Buffffer,并不是随着事务的提交就立刻写入磁盘文件。等事务操作的脏页写入到磁盘之后, Redo Log 的使命也就完成了,Redo Log占用的空间就可以重用(被覆盖写入)。
# 这里第一次出现mvcc,我解释一下,重点理解,当然后面还会讲,这里先打一下基础 首先两个事务AB, 1、A事务在开启前先把id=1的数据备份到undo buffer中。存一个相反的操作数据,这个还会异步持久化到 Undo Log中,具体看服务操作。 2、A事务在BP或者CP中操作user表,修改id为1的age列。当然后面的持久化和加载都是后台线程做,这里只 说流程,不细分析了。 3、此时事务B开启做了id =1的数据的快照读,但是因为A此时还没提交事务呢,所以你要是从A正操作的那块 的缓冲区(BP)里面读,就读到了当前数据了,读到了没提交的数据,取到的是被改过的了,这就不是快照读 了。所以此时事务B可以去undo buffer中去拿到前面A备份的。这里没有还可以去Undo Log中拿。这就能拿 到之前的了。就不会读到事务A没提交的了。你看是不是像解决了那种脏读问题呢?这个后面事务的时候说。 4、要是你事务A执行了rollback 回滚操作,还能通过这个undo log回滚。
# 那么他为什么被提出呢? 我们前面知道,mysql写操作,是操作的buffer poll(当然也有cp的操作,这里就拿bp举例子)。bp写完 了不是立即就写回磁盘的。 那么问题来了,此时要是突然宕机,你的写操作不就丢了么,因为你提交了事务之后就写入了bp,此时丢了那 就麻了。客户端认为我都提交了事务,为啥还能丢呢? 于是引出redo log,我们在每次修改完bp的数据之后,同时把操作写入undo log然后提交事务(二阶段), 此时才算提交完成,要是此时宕机了,在恢复服务的时候就能通过这个文件记录的来恢复期间还没写入磁盘的 数据。 那你可能问了,为啥不写入磁盘之后再算提交呢?这样不就不用redo log了吗。这个存疑,后面看看TODO。
Redo Log工作原理 Redo Log 是为了实现事务的持久性而出现的产物。防止在发生故障的时间点,尚有脏页未写入表的 IBD 文件中,在重启 MySQL 服务的时候,根据 Redo Log 进行重做,从而达到事务的未入磁盘数据进行持 久化这一特性。
Redo Log写入机制 Redo Log 文件内容是以顺序循环的方式写入文件,写满时则回溯到第一个文件,进行覆盖写。这个可以 结合45讲那里一起理解。
# 这里解释三个问题: 1、为什么不直接写进ibd磁盘,而是先走了redo buffer(下面我就写为rb),然后持久化到redolog磁盘 文件,恢复的时候用redolog呢?你是不是脱了裤子放屁。 答:你要是直接写入ibd磁盘文件,那么你每次提交事务都得io一次磁盘,还得是操作线程做,这样很慢。所 以他先是写入到rb中,写内存缓冲肯定是要快的,等到攒够数了,一次写入redolog。 2、当数据写入redolog的时候,就安全了,宕机恢复就能用redolog恢复到user.ibd文件了。要是你还在 rb中,此时宕机,数据会丢。谁告诉你mysql不丢数据了? 3、为什么要写进redolog呢?而不是从rb中直接进ibd磁盘? 答:假如我们一次操作了两条数据id=1 id=999两条数据,你要是直接写进ibd磁盘,可能这两个数据不在一 个页,不连续。你就要找到两个数据的位置一起改了,这样就不是所谓的顺序io了,需要磁头转动去找到对应 位置去修改,很耗时间。但是你要是写入redolog不需要找数据位置,就顺序写进去就好了。等宕机恢复的时 候再根据这个redolog写入ibd就行了,当然此时还是可能随机io,不过宕机可能性低,重启恢复时间久一点 也还好,总比比平时操作久强的多。
如图所示:
write pos 是当前记录的位置,一边写一边后移,写到后一个文件末尾后就回到 0 号文件开头;
checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件; write pos 和 checkpoint 之间还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示写满,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint推进一 下。
Redo Log相关配置参数
每个InnoDB存储引擎至少有1个重做日志文件组(group),每个文件组至少有2个重做日志文件,默认 为ib_logfifile0和ib_logfifile1。可以通过下面一组参数控制Redo Log存储:
Redo Buffffer 持久化到 Redo Log 的策略,可通过 Innodb_flflush_log_at_trx_commit 设置:
0:每秒提交 Redo buffffer ->OS cache -> flflush cache to disk,可能丢失一秒内的事务数据。由后台 Master线程每隔 1秒执行一次操作。 1(默认值):每次事务提交执行 Redo Buffffer -> OS cache -> flflush cache to disk,安全,性能 差的方式。
2:每次事务提交执行 Redo Buffffer -> OS cache,然后由后台Master线程再每隔1秒执行OScache -> flflush cache to disk 的操作。
一般建议选择取值2,因为 MySQL 挂了数据没有损失(因为数据已经进OS CACHE了,这个是服务器的空 间了,不是Mysql的),整个服务器挂了才会损失1秒的事务提交数据。但是你一旦写进系统缓存,系统就 立即启动异步线程同步,丢数据很难。
show variables like '%innodb_log%';
3.6.2、Binlog日志
Binlog记录模式
Redo Log 是属于InnoDB引擎所特有的日志,而MySQL Server也有自己的日志,即 Binarylog(二进制 日志),简称Binlog。Binlog是记录所有数据库表结构变更以及表数据修改的二进制日志,不会记录 SELECT和SHOW这类操作。Binlog日志是以事件形式记录,还包含语句所执行的消耗时间。binlog默认 是关闭的,会消耗系统资源的百分之一,所以默认关闭,开启Binlog日志有以下两个重要的使用场 景。(就是顺序把你的sql写进去了,可以参考redis的AOF机制.) 主从复制:在主库中开启Binlog功能,这样主库就可以把Binlog传递给从库,从库拿到Binlog后实 现数据恢复达到主从数据一致性。 数据恢复:通过mysqlbinlog工具来恢复数据,后面会介绍。
Binlog文件名默认为“主机名_binlog-序列号”格式,例如oak_binlog-000001,也可以在在开启Binlog的 时候在配置文件中指定名称。文件记录模式有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()等函数。因为你在主库的 时间,同步到从库去执行,时间必然不一致了。还有就是id可能不一样,要是你是随机id。或者自 增,万一有一个不一致id就不一致了,这就是上面说的主从不一致的情况。 MIXED(mixed-based replication, MBR):以上两种模式的混合使用,一般会使用STATEMENT 模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlog(比如上面说的那 种now()时间函数),MySQL会根据执行的SQL语句选择写入模式。 Binlog文件结构,(可以参考青蛙哥的公众号)
MySQL的binlog文件中记录的是对数据库的各种修改操作,用来表示修改操作的数据结构是Logevent。 不同的修改操作对应的不同的log event。比较常用的log event有:Query event、Row event、Xid event等。binlog文件的内容就是各种Log event的集合。 Binlog文件中Log event结构如下图所示:
Binlog写入机制
根据记录模式和操作触发event事件封装生成log event(事件触发执行机制) 将事务执行过程中产生log event写入缓冲区,每个事务线程都有一个自己的独立缓冲区Log Event 保存在一个binlog_cache_mngr数据结构中,在该结构中有两个缓冲区,一个是stmt_cache,用于 存放不支持事务的信息;另一个是trx_cache,用于存放支持事务的信息。有的引擎不支持事务就 去不支持的,innodb就去trx_cache。 事务在提交阶段会将产生的log event写入到外部binlog文件中。 不同事务以串行方式(控制并发了)将log event写入binlog文件中,所以一个事务包含的log event信 息在binlog文件中是连续的,中间不会插入其他事务的log event。
Binlog文件操作 Binlog状态查看
开启Binlog功能
需要修改my.cnf或my.ini配置文件,在[mysqld]下面增加log_bin=mysql_bin_log,重启MySQL服 务。
show variables like 'log_bin'; 会有一个sqllogbin的配置,这个开启是记录你使用Binlog恢复数据的时候的执行操作,这个其实不应 该记,你恢复数据的操作记啥,造成冗余,还没用。所以这个建议关闭。
mysql> set global log_bin=mysqllogbin; ERROR 1238 (HY000): Variable 'log_bin' is a read only variable
使用show binlog events命令
使用mysqlbinlog 命令,这个是Mysql服务端的,你得进去mysql服务里面去执行。
使用 binlog 恢复数据
mysqldump:定期全部备份数据库数据。mysqlbinlog可以做增量备份和恢复操作。 删除Binlog文件
可以通过设置expire_logs_days参数来启动自动清理功能。默认值为0表示没启用。设置为1表示超 出1天binlog文件会自动删除掉。 Redo Log和Binlog区别,这个建议结合45讲理解 Redo Log是属于InnoDB引擎功能,Binlog是属于MySQL Server自带功能,并且是以二进制文件记 录。 Redo Log属于物理日志,记录该数据页更新状态内容,Binlog是逻辑日志,记录更新过程。 Redo Log日志是循环写,日志空间大小是固定,Binlog是追加写入,写完一个写下一个,不会覆盖 使用。 Redo Log作为服务宕机后事务数据的自动恢复,binlog可以作为主从复制和数据恢复。binlog没有 事务这一说,没有状态,他不知道哪些数据是提交了,哪些数据是还没提交这个。
[][] [Mysql事务数据恢复][https://zhuanlan.zhihu.com/p/142491549] 四、MySql索引原理
#log-bin=ON #log-bin-basename=mysqlbinlog binlog-format=ROW log-bin=mysqlbinlog
show binary logs; //等价于show master logs; 查看有哪些binglog日志。 show master status;// 查看当前正用的那个binlog show binlog events;// 查看目前为止有哪些事件 show binlog events in 'mysqlbinlog.000001';// 查看具体的哪个binlog文件
mysqlbinlog "文件名" mysqlbinlog "文件名" > "test.sql"
//按指定时间恢复 ,可以先去Binlog文件里面看看执行语句的时间。要是大体知道,可以不看,直接 指定范围内的恢复。 mysqlbinlog --start-datetime="2020-04-25 18:00:00" --stop- datetime="202004-26 00:00:00" mysqlbinlog.000002 | mysql -uroot -p1234 //按事件位置号恢复 mysqlbinlog --start-position=154 --stop-position=957 mysqlbinlog.000002 | mysql -uroot -p1234
purge binary logs to 'mysqlbinlog.000001'; //删除指定文件 purge binary logs before '2020-04-28 00:00:00'; //删除指定时间之前的文件 reset master; //清除所有文件
1、索引类型
索引可以提升查询速度,会影响where查询,以及order by排序。MySQL索引类型如下: 从索引存储结构划分:B Tree索引、Hash索引、FULLTEXT全文索引、R Tree索引(多维度B tree。 不常用) 从应用层次划分:普通索引、唯一索引、主键索引、复合索引 从索引键值类型划分:主键索引、辅助索引(二级索引) 从数据存储和索引键值逻辑关系划分:聚集索引(聚簇索引)数据和索引在一起,也就是主键索 引、非聚集索引(非聚簇索引),数据和索引不在一起,也就是普通的二级索引。 1.1、普通索引
这是基本的索引类型,基于普通字段建立的索引,没有任何限制。
创建普通索引的方法如下:
CREATE INDEX <索引的名字> ON tablename (字段名); ALTER TABLE tablename ADD INDEX [索引的名字] (字段名);//修改表的时候创建 CREATE TABLE tablename ( [...], INDEX [索引的名字] (字段名) );//建表的时候创建
查看索引的方法如下:
1.2、唯一索引
与"普通索引"类似,不同的就是:索引字段的值必须唯一,但允许有空值(不允许的就是主键了) 。在创 建或修改表时追加唯一约束,就会自动创建对应的唯一索引。多个为空的时候不冲突唯一性。
创建唯一索引的方法如下:
CREATE UNIQUE INDEX <索引的名字> ON tablename (字段名); ALTER TABLE tablename ADD UNIQUE INDEX [索引的名字] (字段名); CREATE TABLE tablename ( [...], UNIQUE [索引的名字] (字段名) ; 1.3、主键索引
它是一种特殊的唯一索引,不允许有空值。在创建或修改表时追加主键约束即可,每个表只能有一个主 键。
创建主键索引的方法如下:
CREATE TABLE tablename ( [...], PRIMARY KEY (字段名) ); ALTER TABLE tablename ADD PRIMARY KEY (字段名); 1.4、复合索引
单一索引是指索引列为一列的情况,即新建索引的语句只实施在一列上;用户可以在多个列上建立索 引,这种索引叫做组复合索引(组合索引)。复合索引可以代替多个单一索引,相比多个单一索引复合 索引所需的开销更小。
索引同时有两个概念叫做窄索引和宽索引,窄索引是指索引列为1-2列的索引,宽索引也就是索引列超过 2列的索引,设计索引的一个重要原则就是能用窄索引不用宽索引,因为窄索引往往比组合索引更有效。
创建复合索引的方法如下:
CREATE INDEX <索引的名字> ON tablename (字段名1,字段名2...);
SHOW INDEX FROM 表名;
ALTER TABLE tablename ADD INDEX [索引的名字] (字段名1,字段名2...); CREATE TABLE tablename ( [...], INDEX [索引的名字] (字段名1,字段名2...) );
复合索引使用注意事项:
何时使用复合索引,要根据where条件建索引,注意不要过多使用索引,过多使用会对更新操作效 率有很大影响。 如果表已经建立了(col1,col2),就没有必要再单独建立(col1);如果现在有(col1)索引,如果查 询需要col1和col2条件,可以建立(col1,col2)复合索引,对于查询有一定提高。至于原理下面会 说。 1.5、全文索引(TODO)
查询操作在数据量比较少时,可以使用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 关键字,比如
全文索引使用注意事项:
全文索引必须在字符串、文本字段上建立。在别的类型上没效果,不会生成索引。 全文索引字段值必须在小字符和大字符之间的才会有效。(innodb:3-84;myisam:4-84) 全文索引字段值要进行切词处理,按syntax字符进行切割,例如b+aaa,切分成b和aaa,要是你没 有+切符,你的查询就不会模糊。 全文索引匹配查询,默认使用的是等值匹配,例如a匹配a,不会匹配ab,ac。如果想匹配可以在布 尔模式下搜索a*
2、索引原理
MySQL官方对索引定义:是存储引擎用于快速查找记录的一种数据结构。因为需要额外开辟空间和数据 维护工作。
索引是物理数据页存储,在数据文件中(InnoDB,ibd文件),利用数据页(page)存储。innodb的 理念就是索引即数据,数据即索引。你要调大页大小是可以存更多的索引的每个页。 索引可以加快检索速度,但是同时也会降低增删改操作速度,索引维护需要代价,因为你插入数据 要重整索引树。
索引涉及的理论知识:二分查找法、Hash和B+Tree。 2.1、二分查找法
二分查找法也叫作折半查找法,它是在有序数组中查找指定数据的搜索算法。它的优点是等值查询、范 围查询性能优秀,缺点是更新数据、新增数据、删除数据维护成本高(因为要维护有序)。
select * from user where match(name) against('aaa');
select * from user where match(name) against('a*' in boolean mode);
举个例子,下面的有序数组有17 个值,查找的目标值是7,过程如下: 1、第一次查找
2、第二次查找
3、第三次查找
首先定位left和right两个指针 计算(left+right)/2 判断除2后索引位置值与目标值的大小比对 索引位置值大于目标值就-1,right移动;如果小于目标值就+1,left移动
4、第四次查找
2.2、Hash结构
Hash底层实现是由Hash表来实现的,是根据键值 <key,value> 存储数据的结构。非常适合根据key查找 value值,也就是单个key查询,或者说等值查询。其结构如下所示:
从上面结构可以看出,Hash索引可以方便的提供等值查询,但是对于范围查询就需要全表扫描了,因为 哈希是根据hash函数映射的,所以没有顺序,无法范围查询只能全表扫。 Hash索引在MySQL 中Hash结构主要应用在Memory原生的Hash索引 、InnoDB 自适应哈希索引。
InnoDB提供的自适应哈希索引功能强大,接下来重点描述下InnoDB 自适应哈希索引。 InnoDB自适应哈希索引是为了提升查询效率,InnoDB存储引擎会监控表上各个索引页的查询,当 InnoDB注意到某些索引值访问非常频繁时,会在内存中基于B+Tree索引再创建一个哈希索引,使得内 存中的 B+Tree 索引具备哈希索引的功能,即能够快速定值访问频繁访问的索引页。
InnoDB自适应哈希索引:在使用Hash索引访问时,一次性查找就能定位数据,等值查询效率要优于 B+Tree。 自适应哈希索引的建立使得InnoDB存储引擎能自动根据索引页访问的频率和模式自动地为某些热点页建 立哈希索引来加速访问。另外InnoDB自适应哈希索引的功能,用户只能选择开启或关闭功能,无法进行 人工干涉。
2.3、B+Tree结构
MySQL数据库索引采用的是B+Tree结构,在B-Tree结构上做了优化改造。 B-Tree结构
索引值和data数据分布在整棵树结构中,叶子和非叶子都存值 每个节点可以存放多个索引值及对应的data数据
树节点中的多个索引值从左到右升序排列
B树的搜索:从根节点开始,对节点内的索引值序列采用二分法查找,如果命中就结束查找。没有命中 会进入子节点重复查找过程,直到所对应的的节点指针为空,或已经是叶子节点了才结束。
B+Tree结构 非叶子节点不存储data数据,只存储索引值(理解为就是主键值,要是二级索引就是你索引的列值,参考 青蛙哥的书),这样便于存储更多的索引值 叶子节点包含了所有的索引值和data数据
叶子节点用双向指针连接(下图没标明),提高区间的访问性能
show engine innodb status \G; 查看当前状态,里面有hash索引使用前后的性能表现 show variables like '%innodb_adaptive%';
相比B树,B+树进行范围查找时,只需要查找定位两个节点的索引值,然后利用叶子节点的指针进行遍 历即可。而B树需要遍历范围内所有的节点和数据,显然B+Tree效率高。 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数据表的索引文件和数据文件是分开的,被称为非聚簇索引结构。
不管是主键索引还是辅助索引数据和索引都是分开的,你在索引查完都要去数据文件去取数据。 3、索引分析与优化 3.1、 EXPLAIN
MySQL 提供了一个 EXPLAIN 命令,它可以对 SELECT 语句进行分析,并输出 SELECT 执行的详细信 息,供开发人员有针对性的优化。例如:
EXPLAIN 命令的输出内容大致如下:\G表示纵向显示。
下面就对这几个属性一一分析意思,看懂意思才能知道怎么分析执行计划。 3.1.1、select_type
表示查询的类型。常用的值如下:
例子
EXPLAIN SELECT * from user WHERE id < 3;
SIMPLE:表示查询语句不包含子查询或union,就是一个简单的查询。
PRIMARY:表示此查询是最外层的查询,就是要是有嵌套,子查询那种,最外层的就是这个。
UNION:表示此查询是UNION的第二个或后续的查询,嵌套或者联合的时候内层的。
DEPENDENT UNION:UNION中的第二个或后续的查询语句,使用到了外面查询结果。
UNION RESULT:UNION的结果,联合的结果。
SUBQUERY:SELECT子查询语句就属于这个类型。
DEPENDENT SUBQUERY:SELECT子查询语句依赖外层查询的结果。
最常见的查询类型是SIMPLE,表示我们的查询没有子查询也没用到UNION查询。
而且前面的id就是执行顺序,id值小的先执行,大的后执行,相等的谁在前面谁先执行。这就是一个sql 的执行过程,mysql后面就这么执行。注意这个只是Mysql的执行计划,不一定就真的这么搞,只是作为 参考。
3.1.2、type(参考青蛙哥)
表示存储引擎查询数据时采用的方式。比较重要的一个属性,通过它可以判断出查询是全表扫描还是基 于索引的部分扫描。常用属性值如下,从上至下效率依次增强。
index 现在我的user表只有id是主键有索引,其他字段没有索引。
这个我们直接查user查询条件就是username,我们看到他会去扫全表,找到这个username = lwq的数 据,你可能会问,找到lwq就返回不就行了,为啥要走完全表呢?因为username这不是唯一索引,不知 道有几个,所以要扫完。结合扫描区间来看。
我们条件和上面还是一致,只不过加了一个order by id。id是索引字段(主键),我们按照索引去排序了, 他就是先按索引排序,在排序的索引树上面找username=lwq的,因为name还不是唯一,所以还要遍历 索引全表,只不过此时是有序的。其实就是走索引了,但是走的是全部索引树。在索引树上找 username。All也是在索引树上找username,但是不是基于索引的前提下的。
ALL:表示全表扫描,性能最差。这种一般就要优化了。
index:表示基于索引的全表扫描,先扫描索引再扫描全表数据。可能要问了,你这个弄了半天还是扫全表, 而且还多了一个扫索引,你怎么就比上面的ALL效率高了,因为上面那个ALL,是直接全表扫,要是碰上排序操 作,你这个最后还得排序,无序扫再排序就很伤。而这里index先按照索引扫,索引是有序的,最后按照有序 的索引去找全表数据,排序方面能优化不少。
range:表示使用索引范围查询。使用>、>=、<、<=、in等等,在索引上使用范围缩小了扫描区间,少扫描 了一些行数。
ref:表示使用非唯一索引(普通索引)进行单值查询(单值查询就是只有一行,要是有多个就是ALL)。
eq_ref:一般情况下出现在多表join查询,表示前面表的每一个记录,都只能匹配后面表的一行结果,其实 就是你表设计的时候就是一对一的关系,最后连表走索引就是这样。
const:表示使用主键或唯一索引做等值查询(等值查询唯一索引出来也就是一行了),常量查询。
NULL:表示不用访问表,速度最快,就像那种select 1直接返回1,不走表。
3.1.3、possible_keys
表示查询时能够使用到的索引。注意并不一定会真正使用,显示的是索引名称。 3.1.4、key
表示查询时真正使用到的索引,显示的是使用到的索引名称。 3.1.5、rows
MySQL查询优化器会根据统计信息,估算SQL要查询到结果需要扫描多少行记录。原则上rows是越少效 率越高,可以直观的了解到SQL效率高低。 3.1.6、key_len
表示查询使用了索引的字节数量。可以判断是否全部使用了组合索引。就是看你组合索引有多长,这个 key_len有多长,要是等于,说明都用上了,小于就是没全走。 key_len的计算规则如下:
3.1.7、Extra
Extra表示很多额外的信息,各种操作会在Extra提示相关信息,常见几种如下:
# 字符串类型 字符串长度跟字符集有关:latin1=1、gbk=2、utf8=3、utf8mb4=4 char(n):n*字符集长度 varchar(n):n * 字符集长度 + 2字节(前两个字节不存东西,设计如此) # 数值类型 TINYINT:1个字节 SMALLINT:2个字节 MEDIUMINT:3个字节 INT、FLOAT:4个字节 BIGINT、DOUBLE:8个字节 # 时间类型 DATE:3个字节 TIMESTAMP:4个字节 DATETIME:8个字节 # 字段属性 NULL属性占用1个字节,如果一个字段设置了NOT NULL,则没有此项。比如你一个索引是name varchar(20),用的utf8,那就是20 * 3 + 2 = 62 但是这个name没指定not null,也就是还要占一个存null的空间,那他其实就是63。要是你的 keylen是63那就表示都用上了。
Using where:表示查询需要通过索引回表查询数据。 Using index:表示查询需要通过索引,索引就可以满足所需数据,无需回表取数据了,类似覆盖索引查询这 种。 Using filesort:表示查询出来的结果需要额外排序。要是你使用了索引排序,直接就在索引树上排就好 了,索引树本身就有序,直接按照索引树返回。要是没有索引排序,那么就要把结果集拿出来排一次,数据量 小在内存就能排了,大的话在磁盘,在磁盘就要在磁盘文件中排,这时候就涉及IO了,就性能低,因此有 Using filesort建议优化。在内存排也需要消耗内存和 cpu也需要优化。 Using temprorary:查询使用到了临时表,一般出现于去重、分组等操作,需要有临时表出来去重,分组。
3.2、回表查询
在之前介绍过,InnoDB索引有聚簇索引和辅助索引。聚簇索引的叶子节点存储行记录,InnoDB必须要 有,且只有一个。辅助索引的叶子节点存储的是主键值和索引字段值,通过辅助索引无法直接定位行记 录,通常情况下,需要扫码两遍索引树。先通过辅助索引定位主键值,然后再通过聚簇索引定位行记 录,这就叫做回表查询,它的性能比扫一遍索引树低。
总结:通过索引查询主键值,然后再去聚簇索引查询记录信息 3.3、覆盖索引
在SQL-Server官网的介绍如下:
在MySQL官网,类似的说法出现在explain查询计划优化章节,即explain的输出结果Extra字段为 Usingindex时,能够触发索引覆盖。可以理解为使用ing就是纯用了,覆盖了。这个效率是很好的。
不管是SQL-Server官网,还是MySQL官网,都表达了:只需要在一棵索引树上就能获取SQL所需的所有 列数据,无需回表,速度更快,这就叫做索引覆盖。
实现索引覆盖常见的方法就是:将被查询的字段,建立到组合索引,也就是查询的字段都在索引树 上,在二级索引上直接就能命中全部查询字段,无需去聚簇索引回表。 3.4、左前缀原则
复合索引使用时遵循左前缀原则,左前缀顾名思义,就是左优先,即查询中使用到左边的列, 那么查询就会使用到索引,如果从索引的第二列开始查找,索引将失效。
3.5、LIKE查询
面试题:MySQL在使用like模糊查询时,索引能不能起作用? 回答:MySQL在使用Like模糊查询时,索引是可以被使用的,只有把%字符写在后面才会使用到索引。
select * from user where name like '%o%'; //不起作用 select * from user where name like 'o%'; //起作用
select * from user where name like '%o'; //不起作用 在name上建立索引select * from user where name like 'o%';这个执行计划里面的extra是using index condition。这个看上去像覆盖索引,但是后面多个了condition。这里引出一个索引下推的概念。 我对 Using index condition 的理解是, er, 首先 mysql server 和 storage engine 是两个组件, server 负 责 sql的parse, 执行; storage engine 去真正的 做 数据/index的 读取/写入. 以前是这样: server 命令 storage engine 按 index 把相应的 数据 从 数据表读出, 传给server, server来按 where条件 做选择; 现 在 ICP则是在 可能的情况下, 让storage engine 根据index 做判断, 如果不符合 条件 则无须 读 数据表. 这 样 节省了disk IO. 就是不用再回去server根据条件判断了取条件了,直接就在引擎层做了。把条件下推 了,以前是在server做。 https://dev.mysql.com/doc/refman/5.6/en/index-condition-pushdown-optimization.html 3.6、NULL查询
面试题:如果MySQL表的某一列含有NULL值,那么包含该列的索引是否有效?
对MySQL来说,NULL是一个特殊的值,从概念上讲,NULL意味着“一个未知值”,它的处理方式与其他 值有些不同。比如:不能使用=,<,>这样的运算符,对NULL做算术运算的结果都是NULL,count时不 会包括NULL行等,NULL比空字符串需要更多的存储空间等。
NULL列需要增加额外空间来记录其值是否为NULL。对于MyISAM表,每一个空列额外占用一位,四舍 五入到接近的字节。
下图来自官网:
虽然MySQL可以在含有NULL的列上使用索引(单独和组合都有用),但NULL和其他数据还是有区别的, 不建议列上允许为NULL。非要用的话,好设置NOT NULL,并给一个默认值,比如0和 ‘’ 空字符串 等,如果是datetime类型,也可以设置系统当前时间或某个固定的特殊值,例如'1970-01-01 00:00:00'。
# “NULL columns require additional space in the row to record whether their values are NULL. For MyISAM tables, each NULL column takes one bit extra, rounded up to the nearest byte.”
3.7、索引与排序
MySQL查询支持fifilesort和index两种方式的排序,fifilesort是先把结果查出,然后在缓存或磁盘进行排 序操作,效率较低。使用index是指利用索引自动实现排序,不需另做排序操作,效率会比较高。 fifilesort有两种排序算法:双路排序和单路排序。
双路排序:需要两次磁盘扫描读取,终得到用户数据。第一次将排序字段读取出来,然后排序(在内存 中进行了);第二次去读取其他字段数据,后两者并在一起返回结果。
单路排序:从磁盘查询所需的所有列数据,然后在内存排序将结果返回。如果查询数据超出缓存 sort_buffffer,会导致多次磁盘读取操作(把这些数据读到磁盘文件里面去排序,分批读进去),并创建临 时表,在临时表中排序,后产生了多次IO,反而会增加负担。解决方案:少使用select *;增加 sort_buffffer_size容量和max_length_for_sort_data容量。
建议使用双路的,io次数少。 如果我们Explain分析SQL,结果中Extra属性显示Using fifilesort,表示使用了fifilesort排序方式,需要 优化。如果Extra属性显示Using index时,表示覆盖索引,也表示所有操作在索引上完成,也可以使用 index排序方式,建议大家尽可能采用覆盖索引(都在索引树上了,因为索引本身有序,所以直接利用他 的这个顺序了)。 以下几种情况,会使用index方式的排序。
1、ORDER BY 子句索引列组合满足索引左前列
2、WHERE子句+ORDER BY子句索引列组合满足索引左前列,使用了索引就可以在索引树上排序 因为age = 18的前提下,order是有序的,所以能利用索引。
以下几种情况,会使用fifilesort方式的排序。
1、对索引列同时使用了ASC和DESC
2、WHERE子句和ORDER BY子句满足左前缀,但where子句使用了范围查询(例如>、<、in等) 因为在age>10的前提下,name不一定有序,所以索引树不能用了,就得文件排序了
3、ORDER BY或者WHERE+ORDER BY索引列没有满足索引左前列
4、使用了不同的索引,MySQL每次执行只会采用一个索引,ORDER BY涉及了两个索引,没用
5、WHERE子句与ORDER BY子句,使用了不同的索引
explain select id from user order by id; //对应(id)、(id,name)索引有效
explain select id from user where age=18 order by name; //对应 (age,name)索引
explain select id from user order by age asc,name desc; //对应 (age,name)索引
explain select id from user where age>10 order by name; //对应 (age,name)索引
explain select id from user order by name; //对应(age,name)索引
explain select id from user order by name,age; //对应(name)、(age)两个索 引
6、WHERE子句或者ORDER BY子句中索引列使用了表达式,包括函数表达式
4、查询优化 4.1、慢查询定位 4.1.1、开启慢查询日志
查看 MySQL 数据库是否开启了慢查询日志和慢查询日志文件的存储位置的命令如下:
要是你的mysql没打开,就可以通过如下命令开启慢查询日志:
long_query_time:指定慢查询的阀值,单位秒。如果SQL执行时间超过阀值,就属于慢查询记录到日志 文件中。看你的系统的容忍程度。
log_queries_not_using_indexes:ON表示打开,表示会记录没有使用索引的查询SQL。前提是 slow_query_log的值为ON,否则不会奏效。 4.1.2、查看慢查询日志
1、文本方式查看 直接使用文本编辑器(vi vim)打开slow.log日志即可。
explain select id from user where name='tom' order by age; //对应 (name)、(age)索 引
explain select id from user order by abs(age); //对应(age)索引
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;
time:日志记录的时间,啥时候记录的。 User@Host:执行的用户及主机
Query_time:执行的时间 Lock_time:锁表时间
Rows_sent:发送给请求方的记录数,结果数量 Rows_examined:语句扫描的记录条数 SET timestamp:语句执行的时间点
select....:执行的具体的SQL语句 2、使用mysqldumpslow查看 MySQL 提供了一个慢查询日志分析工具mysqldumpslow,可以通过该工具分析慢查询日志内容。
在 MySQL bin目录下执行下面命令可以查看该使用格式。
运行如下命令查看慢查询日志信息:
-t 5 表示显示前五条,-s表示按照执行时间排序 除了使用mysqldumpslow工具,也可以使用第三方分析工具,比如pt-query-digest、mysqlsla等。 4.2、慢查询优化 4.2.1、索引和慢查询
1、如何判断是否为慢查询?
MySQL判断一条语句是否为慢查询语句,主要依据SQL语句的执行时间,它把当前语句的执行时间跟 long_query_time 参数做比较,如果语句的执行时间 > long_query_time,就会把这条执行语句记录到 慢查询日志里面。long_query_time 参数的默认值是 10s,该参数值可以根据自己的业务需要进行调 整。
2、如何判断是否应用了索引? SQL语句是否使用了索引,可根据SQL语句执行过程中有没有用到表的索引,可通过 explain命令分析查 看,检查结果中的 key 值,是否为NULL。
3、应用了索引是否一定快? 下面我们来看看下面语句的 explain 的结果,你觉得这条语句有用上索引吗?比如
perl mysqldumpslow.pl --help
perl mysqldumpslow.pl -t 5 -s at C:\ProgramData\MySQL\Data\OAK-slow.log
select * from user where id>0;
虽然使用了索引,但是还是从主键索引的左边的叶节点开始向右扫描整个索引树,进行了全表扫描, 此时索引就失去了意义。
而像 select * from user where id = 2; 这样的语句,才是我们平时说的使用了索引。它表示的意思是, 我们使用了索引的快速搜索功能,并且有效地减少了扫描行数。需要看扫描区间。
查询是否使用索引,只是表示一个SQL语句的执行过程;而是否为慢查询,是由它执行的时间决定的, 也就是说是否使用了索引和是否是慢查询两者之间没有必然的联系。我们在使用索引时,不要只关注是 否起作用,应该关心索引是否减少了查询扫描的数据行数,如果扫描行数减少了,效率才会得到提升。 对于一个大表,不止要创建索引,还要考虑索引过滤性,过滤性好,执行速度才会快。 4.2.2、提高索引过滤性
假如有一个5000万记录的用户表,通过sex='男'索引过滤后,还需要定位3000万,SQL执行速度也不会 很快。其实这个问题涉及到索引的过滤性,比如1万条记录利用索引过滤后定位10条、100条、1000 条,那他们过滤性是不同的。索引过滤性与索引字段、表的数据量、表设计结构都有关系。
1、下面我们看一个案例:实战操作一波。
造数据:
先手动插入四条数据,随便来四个四五条就行。
1 zhangsan 18 1 2 lisi 18 1 3 wangwu 14 0 4 zhaoliu 17 1 然后执行insert into student (name,sex,age) select name,sex,age from student;就是把当前表里的数 据查出来然后插入表中,多执行几次这样每次执行都把之前的插一次,就越来越多。主键不用指定,自 增主键。
我执行了十来次就有五万条了。
无索引裸装上:select * from student where age=18 and name like 'zhang%';
先不看结果分析一下,没索引,就是要扫完全表找到符合条件的数据。
看一下执行计划。
竖着排列吧:
表:student 字段:id,name,sex,age 造数据:insert into student (name,sex,age) select name,sex,age from student; 可以每 次把你现在的表数据再插一遍,所以你先插几条执行这个就行 SQL案例:select * from student where age=18 and name like 'zhang%';(全表扫 描)
类型是All,扫描行数就是全表行数,很自然,没索引就是这样的。
加强第一版:在age上建个索引
没走索引,因为要回表mysql选择了不用。 加强第二版:alter table student add index(age,name); //追加age,name索引
此时使用了复合索引,而且存在索引下沉。减少了扫描行数。类型是ref。
可以看到,index condition pushdown 优化的效果还是很不错的。再进一步优化,我们可以把名 字的第一个字和年龄做一个联合索引,这里可以使用 MySQL 5.7 引入的虚拟列来实现。
alter table student add index(name); //追加name索引,十万多行,建索引花了两秒
alter table student add index(age,name); //追加age,name索引
我们建立一个虚拟列first_name,这个虚拟列是不占空间的,因为他的值来源于name,我们把name的 左边五个字符,截取作为first_name并且以此来和age建立复合索引。这样你要找zhang为前缀的,因为 zhang就是五个字符,所以直接就能等值处理,不用Like了。因为可以走我们用五个字符建立的虚拟列 的复合索引了。
类型进一步被优化了。 4.2.3、慢查询原因总结
全表扫描:explain分析type属性all
全索引扫描:explain分析type属性index
索引过滤性不好:靠索引字段选型、数据量和状态、表设计
频繁的回表查询开销:尽量少用select *,使用覆盖索引 4.3、分页查询优化 4.3.1、一般性分页
一般的分页查询使用简单的 limit 子句就可以实现。limit格式如下:
第一个参数指定第一个返回记录行的偏移量,注意从0开始;表示从哪一条开始。
第二个参数指定返回记录行的大数目;表示查询几条。
如果只给定一个参数,它表示返回大的记录行数目;
思考1:如果偏移量固定,返回记录量对执行时间有什么影响?
结果:在查询记录时,返回记录量低于100条,查询时间基本没有变化,差距不大。随着查询记录量越 大,所花费的时间也会越来越多。
//为user表添加first_name虚拟列,以及联合索引(first_name,age) alter table student add first_name varchar(6) generated always as (left(name, 5)), add index(first_name, age); explain select * from student where age=18 and name like 'zhang%';
SELECT * FROM 表名 LIMIT [offset,] rows
select * from user limit 10000,1; select * from user limit 10000,10; select * from user limit 10000,100; select * from user limit 10000,1000; select * from user limit 10000,10000;
思考2:如果查询偏移量变化,返回记录数固定对执行时间有什么影响?
结果:在查询记录时,如果查询记录量相同,偏移量超过100后就开始随着偏移量增大,查询时间急剧 的增加。(这种分页查询机制,每次都会从数据库第一条记录开始扫描,越往后查询越慢,而且查询的 数据越多,也会拖慢总查询速度。不管你设置的偏移量是从一万还是五百开始的,他内部都是从第一条 开始扫,后丢弃不符合的。就这么笨。) 4.3.2、分页优化方案
第一步、利用覆盖索引优化
第二步、要是你就需要select *这种结果,那就可以利用子查询优化
原因:使用了id做主键比较(id>=),并且子查询使用了覆盖索引进行优化。 子查询使用id,减少回表,返回id,主查询再使用子查询返回的主键id走聚簇索引不用回表。 4.3.3、测试结果
1、我们要怎么查看sql的执行时间呢?你在窗口执行完sql会有一个时间跟着,但是我说句实话,那个时 间是尼玛坑爹的。好几个sql都一样的时间,就离谱。
我们这里用一个别的方式能反应实际情况的。
先查看这个开关开着没:
我的开着呢。要是有的版本没开着,可以使用命令打开。
select * from user limit 1,100; select * from user limit 10,100; select * from user limit 100,100; select * from user limit 1000,100; select * from user limit 10000,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;
SHOW VARIABLES LIKE '%profiling%';
SET PROFILING = 1;
打开之后的sql就能查看时间了,打开之前的查不到。
查看执行
我们之前执行的命令的时间杜能看到了,就是Duration属性的值。 1、查看select * from user limit 10000,100;这个的时间
时间是0.013 2、使用优化后(这里只测一下子查询优化,覆盖的不测了)
变成了0.0016,优化减少了一个数量级。
SHOW PROFILES;
五、Mysql事务和锁 1、ACID特性
在关系型数据库管理系统中,一个逻辑工作单元要成为事务,必须满足这 4 个特性,即所谓的 ACID: 原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。 1.1、原子性
原子性:事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
修改---》Buffffer Pool修改---》刷盘。可能会有下面两种情况:
事务提交了,如果此时Buffffer Pool的脏页没有刷盘,如何保证修改的数据生效? Redo 如果事务没提交,但是Buffffer Pool的脏页刷盘了,如何保证不该存在的数据撤销?Undo 每一个写事务,都会修改BufffferPool,从而产生相应的Redo/Undo日志,在Buffffer Pool 中的页被刷 到磁盘之前,这些日志信息都会先写入到日志文件中,如果 Buffffer Pool 中的脏页没有刷成功,此时数 据库挂了,那在数据库再次启动之后,可以通过 Redo 日志将其恢复出来,以保证脏页写的数据不会丢 失。如果脏页刷新成功,此时数据库挂了,就需要通过Undo来实现了。
通过redo和undo日志来保证,要么全执行,要么全部不执行(回滚回去,靠undo,他记录了相反的操作 可以回滚),而redo日志,是还没刷页的操作就在redo中,根据二阶段提交,redo中的操作可以去拿着 xid去inlog去找事务没提交的,就可以继续提交,没事务的就不管了,这种不管。 1.2、持久性
持久性:指的是一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,后续的操作或故障不 应该对其有任何影响,不会丢失。
如下图所示,一个“提交”动作触发的操作有:binlog落地、发送binlog、存储引擎提交、flflush_logs, check_point、事务提交标记等。这些都是数据库保证其数据完整性、持久性的手段。
MySQL的持久性也与WAL技术相关,redo log在系统Crash重启之类的情况时,可以修复数据,从而保 障事务的持久性。通过原子性可以保证逻辑上的持久性,通过存储引擎的数据刷盘可以保证物理上的持 久性。 1.3、隔离性
隔离性:指的是一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他的并 发事务是隔离的。
InnoDB 支持的隔离性有 4 种,隔离性从低到高分别为:读未提交、读提交、可重复读、可串行化。锁 和多版本控制(MVCC)技术就是用于保障隔离性的(后面课程详解)。 1.4、一致性
一致性:指的是事务开始之前和事务结束之后,数据库的完整性限制未被破坏。一致性包括两方面的内 容,分别是约束一致性和数据一致性。
约束一致性:创建表结构时所指定的外键、Check、唯一索引等约束,可惜在 MySQL 中不支持 Check 。 数据一致性:是一个综合性的规定,因为它是由原子性、持久性、隔离性共同保证的结果,而不是 单单依赖于某一种技术。
一致性也可以理解为数据的完整性。数据的完整性是通过原子性、隔离性、持久性来保证的,而这3个特 性又是通过 Redo/Undo 来保证的。逻辑上的一致性,包括唯一索引、外键约束、check 约束,这属于 业务逻辑范畴。
ACID 及它们之间的关系如下图所示,4个特性中有3个与 WAL 有关系,都需要通过 Redo、Undo 日志 来保证等。
WAL的全称为Write-Ahead Logging,先写日志,再写磁盘。
2、事务控制的演进
2.1、并发事务
事务并发处理可能会带来一些问题,比如:更新丢失、脏读、不可重复读、幻读等。 2.1.1、更新丢失
当两个或多个事务更新同一行记录,会产生更新丢失现象。可以分为回滚覆盖和提交覆盖。
回滚覆盖:一个事务回滚操作,把其他事务已提交的数据给覆盖了。 提交覆盖:一个事务提交操作,把其他事务已提交的数据给覆盖了。 2.1.2、脏读
一个事务读取到了另一个事务修改但未提交的数据,是改了,但是还没提交,可能长事务,改完还去做 别的了。他改的被别的事务读到了。万一人家回滚,你读到的就是什么鸡儿。 2.1.3、不可重复读
一个事务中多次读取同一行记录不一致,后面读取的跟前面读取的不一致。出现原因可能是别人提交之 前你读了一次,提交之后你又读发现变了。读到别人提交修改的数据了。 2.1.4、幻读
一个事务中多次按相同条件查询,结果不一致。后续查询的结果和面前查询结果不同,多了或少了几行 记录。你的前后中间别人插入或者删除这个范围的数据了,你读到了就你读这个范围就不一样了。
2.2、排队方案
简单的方法,就是完全顺序执行所有事务的数据库操作,不需要加锁,简单的说就是全局排队。序列 化执行所有的事务单元,数据库某个时刻只处理一个事务操作,特点是强一致性,处理性能低。你挨个 执行了,也就没有并发了,自然就互不影响,性能极地。
这种方式很蠢,不管你几个事务是不是操作同一行数据的,都去排队,所以性能极低,我们就想,我们 操作不同数据的时候没必要排队啊,都不会影响你排队锤子,所以就有了下面的排它锁方案。
2.3、排它锁方案
引入锁之后就可以支持并发处理事务,如果事务之间涉及到相同的数据项时,会使用排他锁,或叫互斥 锁,先进入的事务独占数据项以后,其他事务被阻塞,等待前面的事务释放锁。这种方案下,如果多个 事务操作(读写)的不是一行数据,那就可以并发操作,但是要是操作到一行数据了,那就加排它锁,谁先 加锁,谁先处理,后面的只能等这个事务操作结束了。
# 我们看到了上面的情况,当多个事务并发操作的时候会出现各种对其他事务影响的结果,这就导致我们最 后读取的结果不符合预期。所以mysql的设计大佬们提出了一系列解决方案,方案是一步步演进的,所以下 面就是演进的过程。
注意,在整个事务1结束之前,锁是不会被释放的,所以,事务2必须等到事务1结束之后开始。也就是 说不管事务1处理完data2之后是不是还有其他操作,这个锁是不会释放的。因为做到事务之间的隔离, 只有事务结束了才能下一个,所以就能隔离。你要是不这样,那可能中间出问题,人家还有别的操作, 万一回滚了,万一待会又回来操作这个数据,你也说不准,所以就直接让事务结束后再释放锁。
但是这样也不好,因为假如两个事务,事务1是读这行数据,事务2也是读这行数据,读操作天然并发 啊,但是你分隔开就不好了,只有写出现的时候才会出现并发问题,所以我们好区别对待一下,于是 就有了下面的方案。 2.4、读写锁方案
读和写操作:读读、写写、读写、写读。
读写锁就是进一步细化锁的颗粒度,区分读操作和写操作,让读和读之间不加锁,这样下面的两个事务 就可以同时被执行了。
读写锁,可以让读和读并行,而读和写、写和读、写和写这几种之间还是要加排他锁。
注意,在这个机制下面读读是可以并发处理同一个数据的,而且只有读写,写写会加排它锁,上图中两 个事务,每个事务中都有多个操作,事务12的读可以并发处理,事务12的读和写就要加锁。这时候两个 事务的读可以并发了。并发度进一步提升。
到这里你以为是不是没有优化空间了,但实际上还有空间可以优化,mysql的设计大佬们都是怪物。
2.5、MVVC方案
多版本控制MVCC,也就是Copy on Write的思想。MVCC除了支持读和读并行,还支持读和写、写和读 的并行,但为了保证一致性,写和写是无法并行的。
在事务1开始写操作的时候会copy一个记录的副本,其他事务读操作会读取这个记录副本,当前事务就 写这个源文件就行了,因此不会影响其他事务对此记录的读取,实现写和读并行。 2.5.1、MVVC概念
MVCC(Multi Version Concurrency Control)被称为多版本控制,是指在数据库中为了实现高并发的 数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本。 多版本控制很巧妙地将稀缺资源的独占互斥转换为并发,大大提高了数据库的吞吐量及读写性能。
如何生成的多版本?每次事务修改操作之前,都会在Undo日志中记录修改之前的数据状态和事务号,该 备份记录可以用于其他事务的读取(其他事务能读到你这个备份的数据,下面看),也可以进行必要时的数 据回滚。 2.5.2、MVVC实现原理
MVCC大的好处是读不加锁,读写不冲突。在读多写少的系统应用中,读写不冲突是非常重要的,极 大的提升系统的并发性能,这也是为什么现阶段几乎所有的关系型数据库都支持 MVCC 的原因,不过目 前MVCC只在 Read Commited 和 Repeatable Read 两种隔离级别下工作。 在 MVCC 并发控制中,读操作可以分为两类: 快照读(Snapshot Read)与当前读 (Current Read)。
快照读:读取的是记录的快照版本(有可能是历史版本),不用加锁。(select操作就是快照读) 当前读:读取的是记录的新版本,并且当前读返回的记录,都会加锁,保证其他事务不会再并发 修改这条记录。(select... for update 或lock in share mode,insert/delete/update)也就是你 一旦当前读,这条记录就被加排它锁,保证不会被修改,你读到的永远是当前新数据。
为了让大家更直观地理解 MVCC 的实现原理,举一个记录更新的案例来讲解 MVCC 中多版本的实现。
# 这里插一个知识点 可重复读隔离级别也就是(RR),是怎么实现的呢?这个主要来自于一个面试题,就是同样是快照读,RR怎么就 保证了可重复读呢,RC怎么就无了。有什么区别?当前读不说了,都是最新的加锁了,不用讨论了。 RR下,每次开启事务对这个数据都是生成一个数据快照视图,你这个事务中每次读这个数据都是当时开启的那 个视图,所以你每次读都是一个数据。 RC下,你每次读都会生成新的视图,每次都是新的,所以可能不一样,就有不可重复读这种情况出现。
假设 F1~F6 是表中字段的名字,1~6 是其对应的数据。后面三个隐含字段分别对应该行的隐含ID(每处 理一次事务就自增1)、事务号和回滚指针,隐藏的数据你平时看不到,innodb自己处理生成的如下图所 示,下图是一行数据。
具体的更新过程如下:
假如一条数据是刚 INSERT 的,DB_ROW_ID 为 1,其他两个字段为空。当事务 1 更改该行的数据值时, 会进行如下操作,如下图所示。
更新开始: 用排他锁锁定该行;记录 Redo log; 把该行修改前的值复制到 Undo log,即图中下面的行; 修改当前行的值(都改为乘以10),填写事务编号,使回滚指针指向 Undo log 中修改前的行。将来 回滚就能根据这个undolog回滚了。 接下来事务2操作,过程与事务 1 相同(再加1),此时 Undo log 中会有两行记录,并且通过回滚指针连在 一起,通过当前记录的回滚指针回溯到该行创建时的初始内容,如下图所示。
所以这样一来就能实现这种写读操作了,我解释一下。
当前读下,你改的是原来的哪行数据,你读的就可以走这个undo log里面的数据了。这样就不冲突。 但是你可能问了,那要是事务很多,那undo log会不会越来越多,后裂开。不会的,前面说过,mysql 后台线程会定期清除undo log,什么样的日志会被清除呢?就是当前环境中已经没有任何一个事务id比你 这个undo log中的事务id小的了,也就是说此时不会回滚到你了,因为你事务回滚只能回滚你开启时候 的数据,还没操作呢。你开启时候的事务id都大于undo log中的事务id了,那你也就回不去这个 undolog了,因为你只能回到开启时候,所以这种Undolog就会被清除。
MVCC已经实现了读读、读写、写读并发处理,如果想进一步解决写写冲突,可以采用下面两种方案:
乐观锁 悲观锁 3、事务隔离级别 3.1、隔离级别类型
前面提到的“更新丢失”、”脏读”、“不可重复读”和“幻读”等并发事务问题,其实都是数据库一致性问题, 为了解决这些问题,MySQL数据库是通过事务隔离级别来解决的,数据库系统提供了以下 4 种事务隔离 级别供用户选择。
读未提交
Read Uncommitted 读未提交:解决了回滚覆盖类型的更新丢失,但可能发生脏读现象,也就是可能读 取到其他会话中未提交事务修改的数据。
已提交读
Read Committed 读已提交:只能读取到其他会话中已经提交的数据,解决了脏读。但可能发生不可重 复读现象,也就是可能在一个事务中两次查询结果不一致。
可重复读
Repeatable Read 可重复读:解决了不可重复读,它确保同一事务的多个实例在并发读取数据时,会看 到同样的数据行。不过理论上会出现幻读,简单的说幻读指的的当用户读取某一范围的数据行时,另一 个事务又在该范围插入了新行,当用户在读取该范围的数据时会发现有新的幻影行。
可串行化
Serializable 串行化:所有的增删改查串行执行。它通过强制事务排序,解决相互冲突,从而解决幻度的 问题。这个级别可能导致大量的超时现象的和锁竞争,效率低下。
# 解释一下吧: 比如当前两个事务a b事务id分别是3,4.此时你这两个事务要回滚也只能3回滚到3开启时候,4回滚到4开启 时候,此时还没操作呢。 undolog中所有的那些1,2,的你都回滚不回去了,你在开启事务的时候就读到了你开启事务前面最新的数据 了。前面的都不用了,所以只需回滚到你开启时候的状态即可。
数据库的事务隔离级别越高,并发问题就越小,但是并发处理能力越差(代价)。读未提交隔离级别 低,并发问题多,但是并发处理能力好。以后使用时,可以根据系统特点来选择一个合适的隔离级别, 比如对不可重复读和幻读并不敏感,更多关心数据库并发处理能力,此时可以使用Read Commited隔离 级别。
事务隔离级别,针对Innodb引擎,支持事务的功能。像MyISAM引擎没有关系,他就没事务这个概念。 3.1.1、事务隔离级别和锁的关系
1)事务隔离级别是SQL92定制的标准,相当于事务并发控制的整体解决方案,本质上是对锁和MVCC使 用的封装,隐藏了底层细节。两个事务要是同时读就没啥,用的就是读写锁(读读就行了),读写解决为了 避免脏读就可以用mvcc。要是同时写可能就出现覆盖之类的,所以就加排他锁实现。
2)锁是数据库实现并发控制的基础,事务隔离性是采用锁来实现,不同的隔离级别对相应操作加不同的 锁,就可以防止其他事务同时对数据进行读写操作。
3)对用户来讲,首先选择使用隔离级别,当选用的隔离级别不能解决并发问题或需求时,才有必要在开 发中手动的设置锁。
MySQL默认隔离级别:可重复读 Oracle、SQLServer默认隔离级别:读已提交
一般使用时,建议采用默认隔离级别,然后存在的一些并发问题,可以通过悲观锁、乐观锁等实现处 理。 3.2、MySql隔离级别控制
MySQL默认的事务隔离级别是Repeatable Read,查看MySQL当前数据库的事务隔离级别命令如下:
或者
设置事务隔离级别可以如下命令:当然这是修改的当前会话的隔离级别也就是当前你的那个黑窗口。你 要是想全部会话都生效,可以set后面加上global
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.3、事务隔离级别测试
开两个窗口,模拟两个会话。模拟多事务。
准备一张表:
3.3.1、读未提交级别
两个会话,分别开启一个事务:
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------- Table structure for emp -- ---------------------------DROP TABLE IF EXISTS `emp`; CREATE TABLE `emp` ( `id` int(32) NOT NULL AUTO_INCREMENT, `name` varchar(32) DEFAULT NULL, `salary` int(32) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------- Records of emp -- ---------------------------INSERT INTO `emp` VALUES ('1', 'lwq', '30000'); INSERT INTO `emp` VALUES ('2', 'xrr', '3000');
我们看到事务1中开启事务查了id为1的工资是三万,事务2开启查也是三万。然后事务1中修改为 31000,但是还没提交(没commit)。事务2又读了一次,就是31000了,此时就读到了事务1没提交的数 据了,这就是脏读。
后面的TODO。 3.3.2、可重复读
网上很多都说可重复读解决了幻读,但是实际上他只是解决了大部分情况下的幻读。还是有一种情况会 出现幻读,我这里演示一下这个情况。
上图中两个事务,事务12都是设置为RR隔离级别。
分别开启事务,1中先读是2条数据,2也是2条。然后2事务插入一条,提交事务。此时1事务去读还是两 条,没有幻读。
但是1事务此时对2事务刚才插入的id=3的数据做一次修改,再去查,此时就发现了3条,在1这个事务中 前后读取的数据条数不一致了。出现幻读。
但是我们一般涉及不到这个情况,因为并发处理下面一个事务往往不知道另一个事务的数据,所以出现 不了。但是一旦满足上面的情景,此时就可能有幻读,所以要注意。 3.3.3、可串行化
这种很垃圾的,两个事务开启,只能同时读,一旦有一个事务做了写操作,其余事务啥也干不了,不管 你是不是操作的同一行数据。 4、锁机制和实战
4.1、锁分类
在 MySQL中锁有很多不同的分类。
从操作的粒度可分为表级锁、行级锁和页级锁。 表级锁:每次操作锁住整张表。锁定粒度大,发生锁冲突的概率高,并发度低。应用在 MyISAM、InnoDB、BDB 等存储引擎中。 行级锁:每次操作锁住一行数据。锁定粒度小,发生锁冲突的概率低,并发度高。应用 在InnoDB 存储引擎中。 页级锁:每次锁定相邻的一组记录,锁定粒度界于表锁和行锁之间,开销和加锁时间界于表锁 和行锁之间,并发度一般。应用在BDB 存储引擎中。
从操作的类型可分为读锁和写锁。 读锁(S锁):共享锁,针对同一份数据,多个读操作可以同时进行而不会互相影响。 写锁(X锁):排他锁,当前写操作没有完成前,它会阻断其他写锁和读锁。 IS锁、IX锁:意向读锁、意向写锁,属于表级锁,S和X主要针对行级锁。在对表记录添加S或X锁之 前,会先对表添加IS或IX锁。这个没说清楚看书补全TODO S锁:事务A对记录添加了S锁,可以对记录进行读操作,不能做修改,其他事务可以对该记录追加S 锁,但是不能追加X锁,需要追加X锁,需要等记录的S锁全部释放。 X锁:事务A对记录添加了X锁,可以对记录进行读和修改操作,其他事务不能对记录做读和修改操 作。因为读写会有一致性问题。 从操作的性能可分为乐观锁和悲观锁。 乐观锁:一般的实现方式是对记录数据版本进行比对,在数据更新提交的时候才会进行冲突检 测,如果发现冲突了(说明中间有事务操作过了),则提示错误信息。通过数据版本号处理的。 和前面那些加锁无关。 悲观锁:在对一条数据修改的时候,为了避免同时被其他人修改,在修改数据之前先锁定,再 修改的控制方式。共享锁和排他锁是悲观锁的不同实现,但都属于悲观锁范畴。 4.2、行锁原理
在InnoDB引擎中,我们可以使用行锁和表锁,其中行锁又分为共享锁和排他锁。InnoDB行锁是通过对 索引数据页上的记录加锁实现的,主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock。
RecordLock锁:锁定单个行记录的锁。是排他的(记录锁,RC、RR隔离级别都支持) GapLock锁:间隙锁,锁定索引记录间隙,确保索引记录的间隙不变。(范围锁,RR隔离级别支 持) Next-key Lock 锁:记录锁和间隙锁组合,同时锁住数据,并且锁住数据前后范围。(记录锁+范 围锁,RR隔离级别支持) 在RR隔离级别,InnoDB对于记录加锁行为都是先采用Next-Key Lock,但是当SQL操作含有唯一索引 时(其中有就降级),Innodb会对Next-Key Lock进行优化,降级为RecordLock,仅锁住索引本身而非 范围。
来对Next-key Lock 锁做个测试; 1、准备数据表t1,没有主键,我们避免降级锁,我们只看next-key lock
2、准备数据,准备一些id不连续的数据,测试间隙
3、给id上建一个普通所以,不唯一。
4、测试
看到我们执行id=5的时候的for update 查询,我们知道锁住的就是3-5 和5-7的间隙,以及id=5的一个排 它锁。
所以我插id=6的时候就阻塞了。只有你事务1提交了事务2才能插入。 而且你此时要是执行了这个查询,然后你去修改这个id=5也是不行的,因为这行上面加的行锁(排他)。
1)select ... from 语句:InnoDB引擎采用MVCC机制实现非阻塞读,所以对于普通的select语句, InnoDB不加锁,mvvc走快照读。 2)select ... from lock in share mode语句:追加了共享锁,InnoDB会使用Next-Key Lock锁进 行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。 3)select ... from for update语句:对你扫到的数据追加了排他锁,然后InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,Next-Key Lock可以降级为RecordLock锁(仅仅对查到的排 他,范围就不管了)。 4)update ... where 语句:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以 降级为RecordLock锁。 5)delete ... where 语句:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以 降级为RecordLock锁。 6)insert语句:InnoDB会在将要插入的那一行设置一个排他的RecordLock锁。
4.3、例子讲解
下面以“update t1 set name=‘XX’ where id=10”操作为例,举例子分析下 InnoDB 对不同索引的加锁行 为,以RR隔离级别为例。 Innodb加锁是基于索引操作的,所以必须针对索引情况来看。 4.3.1、主键加锁
加锁行为:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock 锁。
本例中修改的是主键操作,主键自然唯一索引,所以降级,仅在id=10的主键索引记录上加X锁。 4.3.2、唯一键加锁
加锁行为:现在唯一索引id上加X锁,先处理唯一索引树,然后在id=10的主键索引记录上加X锁,回表加 锁,够可以的。 4.3.3、非唯一键加锁,普通索引
加锁行为:对满足id=10条件的记录和主键分别加X锁,然后在(6,c)-(10,b)、(10,b)-(10,d)、(10,d)-(11,f) 范围分别加Gap Lock。注意是开区间。
4.3.4、无索引加锁
加锁行为:表里所有行和间隙都会加X锁。(当没有索引时,会导致全表锁定,因为InnoDB引擎锁机制 是基于索引实现的记录锁定,没有索引就走的是主键索引,主键就会触发全部扫描)。 4.4、悲观锁
悲观锁(Pessimistic Locking),是指在数据处理过程,将数据处于锁定状态,一般使用数据库的锁机 制实现。从广义上来讲,前面提到的行锁、表锁、读锁、写锁、共享锁、排他锁等,这些都属于悲观锁 范畴。
不加锁控制可能出现商品超卖的情况,可以用悲观锁来规避。 4.4.1、表级锁
表级锁每次操作都锁住整张表,并发度低。常用命令如下:
手动增加表锁
查看表上加过的锁
删除表锁
lock table 表名称 read|write,表名称2 read|write;
show open tables;
表级读锁:当前表追加read锁,当前连接和其他的连接都可以读操作;但是当前连接增删改操作会报 错,其他连接增删改会被阻塞。
表级写锁:当前表追加write锁,当前连接可以对表做增删改查操作,其他连接对该表所有操作都被阻塞 (包括查询)。
总结:表级读锁会阻塞写操作,但是不会阻塞读操作。而写锁则会把读和写操作都阻塞。并发比较低, 能用行级锁还是行级锁吧。 4.4.2、共享锁(行级锁-读锁)
共享锁又称为读锁,简称S锁。共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但 是只能读不能修改。使用共享锁的方法是在select ... lock in share mode,只适用查询语句。
总结:事务使用了共享锁(读锁),只能读取,不能修改,修改操作被阻塞。 4.4.3、排他锁(行级锁-写锁)
排他锁又称为写锁,简称X锁。排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁, 其他事务就不能对该行记录做其他操作,也不能获取该行的锁。
使用排他锁的方法是在SQL末尾加上for update,innodb引擎默认会在update,delete语句加上for update。行级锁的实现其实是依靠其对应的索引,所以如果操作没用到索引的查询,那么会锁住全表记 录。 4.4.4、总结
事务使用了排他锁(写锁),当前事务可以读取和修改,其他事务不能修改,也不能获取记录锁,什么 锁都不行(select... for update)。如果查询没有使用到索引,将会锁住整个表记录。 4.5、乐观锁
乐观锁是相对于悲观锁而言的,它不是数据库提供的功能,需要开发者自己去实现。在数据库操作时, 想法很乐观,认为这次的操作不会导致冲突,因此在数据库操作时并不做任何的特殊处理,即不加锁, 而是在进行事务提交时再去判断是否有冲突了。
乐观锁实现的关键点:冲突的检测。
悲观锁和乐观锁都可以解决事务写写并发,在应用中可以根据并发处理能力选择区分,比如对并发率要 求高的选择乐观锁;对于并发率要求低的可以选择悲观锁。 4.5.1、乐观锁实现原理
原理就是冲突的检测。乐观锁因为没有真的加锁,所以并发是比悲观锁要好一些的。
使用版本字段(version) 先给数据表增加一个版本(version) 字段,每操作一次,将那条记录的版本号加 1。version是用来查看被 读的记录有无变化,作用是防止记录在业务处理期间被其他事务修改。
unlock tables;
使用时间戳(Timestamp) 与使用version版本字段相似,同样需要给在数据表增加一个字段,字段类型使用timestamp时间戳。也 是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则 提交更新,否则就是版本冲突,取消操作。这时候这时间戳其实就是个版本号,因为时间戳天然自增, 不需要你去加一。 4.5.2、乐观锁案例
下面我们使用下单过程作为案例,描述下乐观锁的使用。
第一步:查询商品信息
第二步:根据商品信息生成订单
第三步:修改商品库存,修改的时候就是版本号加一,并且对比版本号,要是和自己拿到的一致就说明 中间没人改,要是发现变了,说明别人改了,你就不能改了,相当于别人加了个锁,故称之为乐观锁。
除了自己手动实现乐观锁之外,许多数据库访问框架也封装了乐观锁的实现,比如hibernate框架。 MyBatis框架大家可以使用OptimisticLocker插件来扩展。
select (quantity,version) from products where id=1;
insert into orders ... insert into items ...
update products set quantity=quantity-1,version=version+1 where id=1 and version=#{version};
4.6、死锁与解决方案
加锁不规范可能会出现死锁,导致并发使用有问题。
下面介绍几种常见的死锁现象和解决方案: 4.6.1、表级锁死锁
产生原因:
用户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的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按 照相同的顺序来锁定资源。都按照一个顺序申请资源,别我AB,你BA,很容易就死锁。或者你一次就锁 住AB,不要给别人和你竞争的机会。 4.6.2、行级锁死锁
产生原因1:
如果在事务中执行了一条没有索引条件的查询,引发全表扫描,把行级锁上升为全表记录锁定(等价于 表级锁),多个这样的事务执行后,就很容易产生死锁和阻塞,终应用系统会越来越慢,发生阻塞或 死锁。
解决方案1:
SQL语句中不要使用太复杂的关联多表的查询;使用explain“执行计划"对SQL语句进行分析,对于有全 表扫描和全表锁定的SQL语句,建立相应的索引进行优化。 产生原因2:
两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁。
解决方案2:
在同一个事务中,尽可能做到一次锁定所需要的所有资源 按照id对资源排序,然后按顺序进行处理 4.6.3、共享锁转换为排他锁
产生原因:
事务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
意思就是事务B在1,3中间申请了排他锁,但是此时事务A想去拿排他锁,此时共享锁还加着呢。所以导 致B拿不到排他,在那等着,A此时去拿排他,此时B还等着,所以A也拿不到。 注意,事务提交才会释放锁,mysql没有可重入一说。
解决方案:以上是对同一条数据并发操作出现的,所以规避同时刻并发操作即可
对于按钮等控件,点击立刻失效,不让用户重复点击,避免引发同时对同一条记录多次操作; 使用乐观锁进行控制。乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系 统性能。需要注意的是,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户更新操作不 受我们系统的控制,因此可能会造成脏数据被更新到数据库中; 4.6.4、死锁测试
1、开两个事务窗口,用emp表做测试。
2、事务A共享查询,此时事务A对id=1加上了共享锁
3、事务B去做修改,尝试加排它锁
此时事务B阻塞 4、事务A再次去对id=1的数据做修改,尝试加排它锁
发现死锁了。死锁信息会报出来,这是mysql的机制,不会一直在那卡着。算是一种优化吧。
另外那种互相锁定资源的就不测了,很简单。A锁1B锁2A去锁2,B去锁1就出现了。
下面看一下出现死锁怎么排查。
4.6.5、死锁排查
MySQL提供了几个与锁有关的参数和命令,可以辅助我们优化锁操作,减少死锁发生。 1、查看死锁日志
通过show engine innodb status\G命令查看近期死锁日志信息。 使用方法:1、查看近期死锁日志信息;2、使用explain查看下SQL执行计划,看看是不是无索引导致锁 表之类的,可以加个索引规避优化。当然上面我们这个和索引全表没关系。
下面是我用该语句分析的我们上面的死锁日志,来看一下怎么看日志。
2021-12-18 17:50:26 0x2e18 // 死锁事务 *** (1) TRANSACTION: TRANSACTION 13626, ACTIVE 3 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 14, OS thread handle 10364, query id 394 localhost ::1 root updating update emp set salary='35000' where id=1// 造成死锁的sql,可见是事务B *** (1) WAITING FOR THIS LOCK TO BE GRANTED:// 他在等待锁的释放 // 等待的这个锁是X锁,没有gap RECORD LOCKS space id 143 page no 3 n bits 72 index PRIMARY of table `div_mybatis`.`emp` trx id 13626 lock_mode X locks rec but not gap waiting // 等待释放的锁是2号堆上的5号数据 Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 80000001; asc ;; 1: len 6; hex 00000000350a; asc 5 ;; 2: len 7; hex aa0000011e0110; asc ;; 3: len 3; hex 6c7771; asc lwq;; 4: len 4; hex 80007530; asc u0;;
// 死锁的另一个事务 *** (2) TRANSACTION: TRANSACTION 13627, ACTIVE 223 sec starting index read, thread declared inside InnoDB 5000 mysql tables in use 1, locked 1 4 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 13, OS thread handle 11800, query id 395 localhost ::1 root updating update emp set salary='33000' where id=1// 造成死锁的sql,可见这个是事务A *** (2) HOLDS THE LOCK(S):// 他持有s锁 RECORD LOCKS space id 143 page no 3 n bits 72 index PRIMARY of table `div_mybatis`.`emp` trx id 13627 lock mode S locks rec but not gap// s锁不是gap锁 // 他持有的锁是2号堆上的5号数据,正好是上面事务拿的锁 Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 // 下面是他持有的s锁,加锁的行的数据,看到个lwq 0: len 4; hex 80000001; asc ;; 1: len 6; hex 00000000350a; asc 5 ;; 2: len 7; hex aa0000011e0110; asc ;; 3: len 3; hex 6c7771; asc lwq;; 4: len 4; hex 80007530; asc u0;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:// 他在等待锁的释放,可见两个都在等待锁释 放,所以死锁了 // 这个锁是个x锁,不是gap类型
2、查看锁状态变量 通过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:系统启动后到现在总共等待的次数
如果等待次数高,而且每次等待时间长,需要分析系统中为什么会有如此多的等待,然后着手定制优 化。 六、Mysql集群架构
存数据用的数据库,一旦崩了就很麻烦,所以设计数据库的时候到了一定规模就需要考虑集群架构。 1、集群架构设计 1.1、架构设计理念
在集群架构设计时,主要遵从下面三个维度:
可用性 扩展性 一致性
下面就来看看这几个维度在设计的时候需要考虑哪些点。 1.2、可用性设计
站点高可用,冗余站点 服务高可用,冗余服务 数据高可用,冗余数据
保证高可用的方法是冗余。但是数据冗余带来的问题是数据一致性问题。各个节点之间的一致性,是不 是能及时同步,同步挂了怎么办等等都需要考虑。
实现高可用的方案有以下几种架构模式:
主从模式 简单灵活,能满足多种需求。比较主流的用法,但是写操作高可用需要自行处理。 双主模式
RECORD LOCKS space id 143 page no 3 n bits 72 index PRIMARY of table `div_mybatis`.`emp` trx id 13627 lock_mode X locks rec but not gap waiting // 等待释放的是2号堆上的5号数据 Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 80000001; asc ;; 1: len 6; hex 00000000350a; asc 5 ;; 2: len 7; hex aa0000011e0110; asc ;; 3: len 3; hex 6c7771; asc lwq;; 4: len 4; hex 80007530; asc u0;; 所以就是事务A持有s锁在等事务B释放x锁,事务B在等事务A释放x锁,而且他们加锁的地方都是2号堆的5号数 据,基本吻合我们的场景,这个看死锁日志还得结合书看一下,多练习 *** WE ROLL BACK TRANSACTION (1)
互为主从,有双主双写、双主单写两种方式,建议使用双主单写,双主双写会有很多不一致问题, 都能写可能就有很多不一样的地方,这也是为啥redis不让从库写。 1.3、扩展性设计
扩展性主要围绕着读操作扩展和写操作扩展展开。
如何扩展以提高读性能 加从库 简单易操作,方案成熟。 从库过多会引发主库性能损耗。建议不要作为长期的扩充方案,应该设法用良好的设计避免持 续加从库来缓解读性能问题。 分库分表 可以分为垂直拆分和水平拆分,垂直拆分可以缓解部分压力,水平拆分理论上可以无限扩展。 如何扩展以提高写性能 分库分表 1.4、一致性设计
一致性主要考虑集群中各数据库数据同步以及同步延迟问题,延迟大了就会有性能问题。可以采用的方 案如下:
不使用从库 扩展读性能问题需要单独考虑,否则容易出现系统瓶颈。 增加访问路由层 可以先得到主从同步长时间t,在数据发生修改后的t时间内,先访问主库,等保证同步过去了 再去读同步库。 2、主从模式 2.1、适用场景
MySQL主从模式是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL 默 认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,异步更新即可,从节点可 以复制主数据库中的所有数据库,或者特定的数据库,或者特定的表。
mysql主从复制用途:
实时灾备,用于故障切换(高可用) 读写分离,提供查询服务(读扩展) 数据备份,避免影响业务(高可用)
主从部署必要条件:
从库服务器能连通主库 主库开启binlog日志(设置log-bin参数),因为复制就是用的binlog复制的 主从server-id不同,不能冲突。 2.2、实现原理
主从模式的重点就是复制。所以先来看看复制的知识。 2.2.1、主从复制
下图是主从复制的原理图。
主从复制整体分为以下三个步骤:
主库将数据库的变更操作记录到Binlog日志文件中 从库读取主库中的Binlog日志文件信息写入到从库的Relay Log中继日志中,这个读取的线程是从 库的一个线程,异步读取的,主库写入Binlog就不管了。 从库读取中继日志信息在从库中进行Replay,更新从库数据信息 在上述三个过程中,涉及了Master的BinlogDump Thread和Slave的I/O Thread、SQL Thread,它们的 作用如下:
Master服务器对数据库更改操作记录在Binlog中,BinlogDump Thread接到写入请求后,读取 Binlog信息推送给Slave的I/O Thread。 Slave的I/O Thread将读取到的Binlog信息写入到本地Relay Log中。 Slave的SQL Thread检测到Relay Log的变更请求,解析relay log中内容在从库上执行,从而得到同 步。
上述过程都是异步操作,不等待,各做各的,俗称异步复制,存在数据延迟现象。
下图是异步复制的时序图。主库是不关心从库的,都是异步的,从库自己的线程同步。
mysql主从复制存在的问题:
主库宕机后,数据可能丢失 从库只有一个SQL Thread,主库写压力大,复制很可能延时
解决方法:
半同步复制---解决数据丢失的问题 并行复制----解决从库复制延迟的问题 2.2.2、半同步复制(解决安全性)
为了提升数据安全,MySQL让Master在某一个时间点等待Slave节点的 ACK(Acknowledge character)消息,接收到ACK消息后才进行事务提交,这也是半同步复制的基础,MySQL从5.5版本开 始引入了半同步复制机制来降低数据丢失的概率。
介绍半同步复制之前先快速过一下 MySQL 事务写入碰到主从复制时的完整过程,主库事务写入分为 4个 步骤:
InnoDB Redo File Write (Prepare Write) Binlog File Flush & Sync to Binlog File InnoDB Redo File Commit(Commit Write) 前三个就是二阶段提交,在主库执行 Send Binlog to Slave,这个是发去从库,主库是不关心的。
# 在了解半同步之前,先看一下全同步,全同步就是真的同步了,直接站在异步的对立面。需要排队。 主库发去从库,你得等从库都同步commit了,返回给你主库,此时你主库才能commit。这样从库没提交之 前,主库一直不能提交,性能不好,但是安全性好。容易造成主库长事务。
当Master不需要关注Slave是否接受到Binlog Event时,即为传统的主从复制。 当Master需要在第三步等待Slave返回ACK时,即为 after-commit,半同步复制(MySQL 5.5引入)。
当Master需要在第二步等待 Slave 返回 ACK 时,即为 after-sync,增强半同步(MySQL 5.7引入)。
下图是 MySQL 官方对于半同步复制的时序图,主库等待从库写入 relay log 并返回 ACK 后才进行 Engine Commit。
这里再加强解释一下:以这个对比为准去理解。
# 5.5的半同步是你在第三步,也就是提交redo后,你就发从从库,但是得先等到至少一个从库同步完了, 返回ACK才能继续执行你的,给客户端返回提交成功。这种时候有两个问题: 1、发送从库失败了,这样就会导致你主库一直拿不到ack,导致主库的事务也失败,这样还好,起码主 库能感知到错误。你再执行一次,再推一次就行。 2、发送从库成功了,从库执行后还没来的及返回给主库ACK,此时主库挂了,在主库这里就认为事务失 败,从库其实数据已经过去了,你再起服务,客户端要用户知道挂了,那就心想再还会执行一次,从库还同 步,这样就会重复同步了,下单就重复下单了。 5.7的半同步优化了这个操作,具体看下面。
# 5.7的时候半同步是你在第二步写入Binlog之后,你就发从库,但是得等到所有从库同步完了返回ACK, 你才能继续执行你的下面的提交redo事务。这是一个对5.5的优化。 我们在主库写完binlog就去发从库,从库回来了就继续提交主库事务,这样要是你主库没接到ACK就挂 了,
半同步复制:主上已经提交了,但是日志还没来得及传到备库,这时候从库宕机了,在半同步看来,主 库其他会话看来是透明的,看到的是他提交了的数据,因为主库提交了,但是如果从库恢复了这时候切 换到slave,slave上又没有提交,没有看到这部分数据,这就矛盾了。 而增强版同步,alter_sync,日志没有传输到备库,从库要是失败了那么主库这时候也没有提交,这时 候服务挂掉了,主库其他会话看到的是未提交的数据,并且也没有传输到备库,所以数据不存在丢失一 说。只是没数据,主从还是一致的。 2.2.3、并行复制(解决延迟)
MySQL的主从复制延迟一直是受开发者为关注的问题之一,MySQL从5.6版本开始追加了并行复制功 能,目的就是为了改善复制延迟问题,并行复制称为enhanced multi-threaded slave(简称MTS)。增 强多线程从库,看上去就是在从库加了多线程。
在从库中有两个线程IO Thread和SQL Thread,都是单线程模式工作,因此有了延迟问题,我们可以采 用多线程机制来加强,减少从库复制延迟。(IO Thread多线程意义不大,主要指的是SQL Thread多线 程)因为IO线程是接收主库发过来的binlog信息,写入到relalog里面。我们的问题不在于说你接收的有 多快,人家主库那边传过来的没加强,你接收多了一堆线程,怕不是吃多了。sql线程是为了解析从库日 志,回放sql完成数据同步,所以我们可以在接收到之后,多加几个线程解析回放,这个才合理。 在MySQL的5.6、5.7、8.0版本上,都是基于上述SQL Thread多线程思想,不断优化,减少复制延迟。
Mysql5.6并行复制原理: MySQL 5.6版本也支持所谓的并行复制,但是其并行只是基于库的。如果用户的MySQL数据库中是多个 库,对于从库复制的速度的确可以有比较大的帮助。
基于库的并行复制,就是从库那边假如有三个库要同步,每个库开一个线程复制回放,多路并经。
这个路子下面,有一个调度器(Coordinator),调度器对relay log的数据调度,每一个库都有一个job任 务队列,每个库的数据进入任务队列,后由每一个worker线程去处理队列数据。
实现相对简单,使用也相对简单些。基于库的并行复制遇到单库多表使用场景就发挥不出优势了,就一 个线程,等于没优化。
另外多库下对事务并行处理的执行顺序也是个大问题,因为多个线程并行处理多个库,假如某些sql是分 布式链路调度的,涉及到分布式操作,你这个多线程回放顺序不对可能会有事务问题。比如你又得操作 需要先操作A库,产生的数据给B库用,但是这个并行在从库上先执行了B,A的数据还没有呢,这就有问 题了。
MySQL 5.7并行复制原理
MySQL 5.7是基于组提交的并行复制,MySQL 5.7才可称为真正的并行复制,这其中为主要的原因就 是slave服务器的回放与master服务器是一致的,即master服务器上是怎么并行执行的slave上就怎样进 行并行回放。不再有库的并行复制限制。
MySQL 5.7中组提交的并行复制究竟是如何实现的?
MySQL 5.7是通过对事务进行分组,当事务提交时,它们将在单个操作中写入到二进制日志中。如果多 个事务能同时提交成功,那么它们意味着没有冲突,因此可以在Slave上并行执行,所以通过在主库上的 二进制日志中添加组提交信息。
MySQL 5.7的并行复制基于一个前提,即所有已经处于prepare阶段的事务,都是可以并行提交的。这 些当然也可以在从库中并行提交,因为处理这个阶段的事务都是没有冲突的。在一个组里提交的事务, 一定不会修改同一行。涉及修改同一行记录就不是一组。这是一种新的并行复制思路,完全摆脱了原来 一直致力于为了防止冲突而做的分发算法,等待策略等复杂的而又效率底下的工作。
InnoDB事务提交采用的是两阶段提交模式。一个阶段是prepare(undo redo sql也执行了,要是有冲突 在这个阶段也能发现),另一个是commit(写入binlog,提交清除undolog,redolog做提交)。在这个过 程中,事务之间要是有冲突prepare就能发现,因为他此时就去执行了sql,要是有冲突就能知道,在这个 阶段会把没冲突的设置成一组,这一组的就能并发操作,因为不操作同行数据,没有关系。
为了兼容MySQL 5.6基于库的并行复制,5.7引入了新的变量slave-parallel-type,其可以配置的值有: DATABASE(默认值,基于库的并行复制方式)、LOGICAL_CLOCK(基于组提交的并行复制方式)。 那么如何知道事务是否在同一组中,生成的Binlog内容如何告诉Slave哪些事务是可以并行复制的?
换言之就是从库怎么知道你是一组的,也就是Binlog到了从库怎么区分哪个是一组的。 在MySQL 5.7版本中,其设计方式是将组提交的信息存放在GTID中。为了避免用户没有开启GTID功能 (gtid_mode=OFF),MySQL 5.7又引入了称之为Anonymous_Gtid(匿名的Gtid,你自己不开,mysql 自己给你个匿名的)的二进制日志event类型ANONYMOUS_GTID_LOG_EVENT。 通过mysqlbinlog工具分析binlog日志,就可以发现组提交的内部信息。
可以发现MySQL 5.7二进制日志较之原来的二进制日志内容多了last_committed和 sequence_number,last_committed表示事务提交的时候,上次事务提交的编号,如果事务具有相同 的last_committed,表示这些事务都在一组内,可以进行并行的回放。
last_committed相同的是一组,sequence_number是连续的.
我们看到上面有三组,第一组都是0,第二组都是6,第三组就一个是12,那么在并行提交的时候就可以 开六个线程复制第一组,再开六个复制第二组,后就一个线程处理第三组就行。
而且这个编号是有规律的,每一组的last_committed的号是上一组的sequence_number的后一个 值。
Mysql8.0并行复制 MySQL8.0 是基于write-set的并行复制。MySQL会有一个集合变量来存储事务修改的记录信息(主键哈 希值),所有已经提交的事务所修改的主键值经过hash后都会与那个变量的集合进行对比,来判断改行 是否与其冲突,并以此来确定依赖关系,没有冲突即可并行。这样的粒度,就到了 row级别了,此时并 行的粒度更加精细,并行的速度会更快。
就是说,有一个集合,每次修改的行的主键做哈希会进入这个集合,后面再修改进来的哈希值和现在的 做对比,因为哈希就是O(1),所以一下就能对比出来。要是发现有一样的,那就冲突,没有冲突的就能直 接并行去处理,这样多线程操作的就是行级别的数据,处理速度就更快了。
并行复制配置与调优
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。 要开启enhanced multi-threaded slave其实很简单,只需根据如下设置:
并行监复制监控
在使用了MTS后,复制的监控依旧可以通过SHOW SLAVE STATUS\G,但是MySQL 5.7在 performance_schema库中提供了很多元数据表,可以更详细的监控并行复制过程。
通过replication_applier_status_by_worker可以看到worker进程的工作情况:
后,如果MySQL 5.7要使用MTS功能,建议使用新版本,少升级到5.7.19版本,修复了很多Bug。
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
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 | +---------------------------------------------+
2.2.4、读写分离
读写分离引入时机
大多数互联网业务中,往往读多写少(28定律),这时候数据库的读会首先成为数据库的瓶颈。如果我们 已经优化了SQL,甚至也加上了缓存,但是读依旧还是瓶颈时,这时就可以选择“读写分离”架构了。
读写分离首先需要将数据库分为主从库,一个主库用于写数据,多个从库完成读数据的操作,主从库之 间通过主从复制机制进行数据的同步,如图所示。
在应用中可以在从库追加多个索引来优化查询,主库这些索引可以不加,因为写操作每次都要维护索 引,所以主库就不加了,就给从库检索加上,用于提升写效率。读写分离架构也能够消除读写锁冲突从 而提升数据库的读写性能。使用读写分离架构需要注意:主从同步延迟和读写分配机制问题
读写分离以后也能减少一些读写锁的冲突,进一步加强效率。下面是主从模式需要注意的一些问题。
主从同步延迟
使用读写分离架构时,数据库主从同步具有延迟性,虽然有了复制优化,但是还是数据一致性会有影 响,对于一些实时性要求比较高的操作,可以采用以下解决方案。
写后立刻读 在写入数据库后,某个时间段内读操作就去主库,之后读操作访问从库。 二次查询 先去从库读取数据,找不到时就去主库进行数据读取。该操作容易将读压力返还给主库,为了避免 恶意攻击,建议对数据库访问API操作进行封装,做一些业务的定制化,减少一些压力,比如下面 的加缓存,有利于安全和低耦合。 根据业务特殊处理
根据业务特点和重要程度进行调整,比如重要的,实时性要求高的业务数据读写可以放在主库。对 于次要的业务,实时性要求不高可以进行读写分离,查询时去从库查询。需要做好路由。不重要的 比如改头像这种可以延迟一点无所谓。要区分好业务重要程度。 走缓存 热数据进缓存,缓存读不到再去读库,因为缓存挡了一下,使得数据库同步有了时间,而且大量的 操作都是读,写不会很多。这样也能进一步避免。
当然,具体问题具体分析,以上只是几种思路。
读写分离落地
怎么走,怎么去主库查,怎么去从库查,哪些去主哪些去从,怎么控制写去主库读去从库。
读写路由分配机制是实现读写分离架构关键的一个环节,就是控制何时去主库写,何时去从库读。目 前较为常见的实现方案分为以下两种:
基于编程和配置实现(应用端) 程序员在代码中封装数据库的操作,代码中可以根据操作类型进行路由分配,增删改时操作主库, 查询时操作从库。这类方法也是目前生产环境下应用广泛的。优点是实现简单,因为程序在代码 中实现,不需要增加额外的硬件开支,缺点是需要开发人员来实现,运维人员无从下手,如果其中 一个数据库宕机了,就需要修改配置重启项目。比如我们自己在代码里面写个拦截器做拦截,针对 类型做路由。 基于服务器端代理实现(服务器端)
中间件代理一般介于应用服务器和数据库服务器之间,从图中可以看到,应用服务器并不直接进入 到master数据库或者slave数据库,而是进入MySQL proxy代理服务器。代理服务器接收到应用服 务器的请求后,先进行判断然后转发到后端master和slave数据库。很多中间件都是做这个, mycat,Sharding等等。 目前有很多性能不错的数据库中间件,常用的有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软件。 3、双主模式 3.1、适用场景
很多企业刚开始都是使用MySQL主从模式,一主多从、读写分离等。但是单主如果发生单点故障,从库 切换成主库还需要作改动。因此,如果是双主或者多主,就会增加MySQL入口,提升了主库的可用性。 因此随着业务的发展,数据库架构可以由主从模式演变为双主模式。双主模式是指两台服务器互为主 从,任何一台服务器数据变更,都会通过复制应用到另外一方的数据库中。
使用双主双写还是双主单写?
建议大家使用双主单写,因为双主双写存在以下问题:
ID冲突 在A主库写入,当A数据未同步到B主库时,对B主库写入,如果采用自动递增容易发生ID主键的冲 突。 可以采用MySQL自身的自动增长步长来解决,例如A的主键为1,3,5,7...,B的主键为2,4,6,8... ,但 是对数据库运维、扩展都不友好。运维万一整错了就裂开了,而且你加一台机器可能步长就不够了 还是会有冲突。 更新丢失 同一条记录在两个主库中进行更新,会发生前面覆盖后面的更新丢失。
高可用架构如下图所示,其中一个Master提供线上服务,另一个Master作为备胎供高可用切换, Master下游挂载Slave承担读请求。
# 可能有人会问,你A修改了推给B,B拿到修改一修改,binlog又变了,又推给A,这不就一直没完了。 不会的,每次修改都带着serverId,会判断谁是发送方,谁是接收方,接收到做了修改就不发了,不会没完, 是个单向的。
随着业务发展,架构会从主从模式演变为双主模式,建议用双主单写,再引入高可用组件,例如 Keepalived和MMM等工具,实现主库故障自动切换。 3.2、MMM架构
MMM(Master-Master Replication Manager for MySQL)是一套用来管理和监控双主复制,支持双主 故障切换 的第三方软件。MMM 使用Perl语言开发,虽然是双主架构,但是业务上同一时间只允许一个 节点进行写入操作。下图是基于MMM实现的双主高可用架构。
MMM故障处理机制 MMM 包含writer和reader两类角色,分别对应写节点和读节点。 当 writer节点出现故障,程序会自动移除该节点上的VIP,VIP是双主用的一个ip,是一个虚拟 ip,对外客户端暴露的,需要给客户端使用,切换给m2之后还用这个ip,客户端感觉不到变 化。 写操作切换到 Master2,并将Master2设置为writer 将所有Slave节点会指向Master2 除了管理双主节点,MMM 也会管理 Slave 节点,在出现宕机、复制延迟或复制错误,MMM 会移 除该节点的 VIP,直到节点恢复正常。 MMM监控机制 MMM 包含monitor和agent两类程序,功能如下: monitor:监控集群内数据库的状态,在出现异常时发布切换命令,一般和数据库分开部署 (避免服务器挂了,你也挂了,起不到作用,两个master也分开部署)。 agent:运行在每个 MySQL 服务器上的代理进程,monitor 命令的执行者,完成监控的探针 工作和具体服务设置,例如设置 VIP(虚拟IP)、指向新同步节点。 有点像keepalived的机制。
3.3、MHA架构
MHA(Master High Availability)是一套比较成熟的 MySQL 高可用方案,也是一款优秀的故障切换和 主从提升的高可用软件。在MySQL故障切换过程中,MHA能做到在30秒之内自动完成数据库的故障切 换操作,并且在进行故障切换的过程中,MHA能在大程度上保证数据的一致性,以达到真正意义上的 高可用。MHA还支持在线快速将Master切换到其他主机(无故障也能主动切换),通常只需0.5-2秒。 目前MHA主要支持一主多从的架构,要搭建MHA,要求一个复制集群中必须少有三台数据库服务器。
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监控节点可以监控多个集群,可以监控上百台,大量的监控 3.4、主备切换
主备切换是指将备库变为主库,主库变为备库,有可靠性优先和可用性优先两种策略。
主备延迟是由主从数据同步延迟导致的,与数据同步有关的时间点主要包括以下三个:
主备延迟问题 主库 A 执行完成一个事务,写入 binlog,我们把这个时刻记为 T1; 之后将binlog传给备库 B,我们把备库 B 接收完 binlog 的时刻记为 T2; 备库 B 执行完成这个binlog复制,我们把这个时刻记为 T3。 所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就 是 T3-T1。 在备库上执行show slave status命令,它可以返回结果信息,返回的信息中 seconds_behind_master表示当前备库延迟了多少秒。 同步延迟主要原因如下: 备库机器性能问题 机器性能差,甚至一台机器充当多个主库的备库。 分工问题 备库提供了读操作,或者执行一些后台分析处理的操作,消耗大量的CPU资源,导致那边的同 步慢了。 大事务操作 大事务耗费的时间比较长,导致主备复制时间长。比如一些大量数据的delete或大表DDL操作 都可能会引发大事务,大事务同步的内容也多,就慢。可以用一些工具之类的,别操作sql。 可靠性优先 主备切换过程一般由专门的HA高可用组件完成,但是切换过程中会存在短时间不可用(有一个小的 时间),因为在切换过程中某一时刻主库A和从库B都处于只读状态(此时切为只读,不让写了,等同 步完成保证一致性)。如下图所示:
主库由A切换到B,切换的具体流程如下: 判断从库B的Seconds_Behind_Master值,当小于某个值才继续下一步 把主库A改为只读状态(readonly=true) 等待从库B的Seconds_Behind_Master值降为 0 把从库B改为可读写状态(readonly=false) 把业务请求切换至从库B 可用性优先 不等主从同步完成, 直接把业务请求切换至从库B ,并且让 从库B可读写 ,这样几乎不存在不可 用时间,但可能会数据不一致。
如上图所示,在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、分库分表
互联网系统需要处理大量用户的请求。比如微信日活用户破10亿,海量的用户每天产生海量的数量;美 团外卖,每天都是几千万的订单,那这些系统的用户表、订单表、交易流水表等是如何处理呢?
数据量只增不减,历史数据又必须要留存,非常容易成为性能的瓶颈,而要解决这样的数据库瓶颈问 题,“读写分离”和缓存往往都不合适,目前比较普遍的方案就是使用NoSQL/NewSQL或者采用分库分 表。mysql单表上限在十亿数据,但是业内表示单表是千万级别即可,索引树大为三到五层,比较不 错。
使用分库分表时,主要有垂直拆分和水平拆分两种拆分模式,都属于物理空间的拆分。
分库分表方案:只分库、只分表、分库又分表。
垂直拆分:由于表数量多导致的单个库大。将表拆分到多个库中。
水平拆分:由于表记录多导致的单个库大。将表记录拆分到多个表中。
4.1、拆分方式 4.1.1、垂直拆分
垂直拆分又称为纵向拆分,垂直拆分是将表按库进行分离,或者修改表结构按照访问的差异将某些列拆 分出去。应用时有垂直分库和垂直分表两种方式,一般谈到的垂直拆分主要指的是垂直分库。如下图所 示,采用垂直分库,将用户表和订单表拆分到不同的数据库中。
垂直分表就是将一张表中不常用的字段拆分到另一张表中,从而保证第一张表中的字段较少,能在一个 页中就存的下了,检索的时候就不用跨页去处理了,避免出现数据库跨页存储的问题,从而提升查询效 率。
解决:一个表中字段过多,还有有些字段经常使用,有些字段不经常使用,或者还有text等字段信息。 可以考虑使用垂直分表方案。
按列进行垂直拆分,即把一条记录分开多个地方保存,每个子表的行数相同。把主键和一些列放到一个 表,然后把主键和另外的列放到另一个表中。
垂直拆分优点
拆分后业务清晰,拆分规则明确;
易于数据的维护和扩展;
可以使得行数据变小,一个数据块 (Block) 就能存放更多的数据,在查询时就会减少 I/O 次数; 可以达到大化利用 Cache 的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的 放一起;
便于实现冷热分离的数据表设计模式。
垂直拆分缺点:
主键出现冗余,现在每个表都得存主键,不然关联不到了,需要管理冗余列;
会引起表连接 JOIN 操作,可以通过在业务服务器上进行 join 来减少数据库压力,提高了系统的复杂 度;但是有的确实避免不了,可以在业务中做处理,比如一起拿回去做聚合之类的。
依然存在单表数据量过大的问题;
事务处理复杂,分表之后可能维护多个表,库的事务。 4.1.2、水平拆分
水平拆分又称为横向拆分。 相对于垂直拆分,它不再将数据根据业务逻辑分类,而是通过某个字段(或 某几个字段),根据某种规则将数据分散至多个库或表中,每个表仅包含数据的一部分,分开的每个表 都是长得一样,只是每个表存的都是完整数据的一部分,如下图所示。垂直是数据数目一样,但是可能 每个表里面的字段不一样,分开字段存了。
水平分表是将一张含有很多记录数的表水平切分,不同的记录可以分开保存,拆分成几张结构相同的 表。如果一张表中的记录数过多,那么会对数据库的读写性能产生较大的影响,虽然此时仍然能够正确 地读写,但读写的速度已经到了业务无法忍受的地步,此时就需要使用水平分表来解决这个问题。
水平拆分:解决表中记录过多问题。
垂直拆分:解决表过多或者是表字段过多问题。
水平拆分重点考虑拆分规则:例如范围(1-200存哪里,200-400存哪里)、时间(哪个月的存哪里之类的)或 Hash算法(可以用hash键做哈希映射)等。
水平拆分优点:
拆分规则设计好,join 操作基本可以数据库做;
不存在单库大数据,高并发的性能瓶颈;
切分的表的结构相同,应用层改造较少,只需要增加路由规则即可;
提高了系统的稳定性和负载能力。
水平拆分缺点:
拆分规则难以抽象;
跨库Join性能较差;
分片事务的一致性难以解决;
数据扩容的难度和维护量极大。
分库分表之后做全部查询,建议把数据灌倒es中做统一查询。 日常工作中,我们通常会同时使用两种拆分方式,垂直拆分更偏向于产品/业务/功能拆分的过程,在技 术上我们更关注水平拆分的方案。
4.2、主键策略
我在另一篇原创中写了,抽奖第九章的预热篇。 4.3、分片策略 4.3.1、分片概念
分片(Sharding)就是用来确定数据在多台存储设备上分布的技术。Shard这个词的意思是“碎片”,如果 将一个数据库当作一块大玻璃,将这块玻璃打碎,那么每一小块都称为数据库的碎片(Database Sharding)。将一个数据库打碎成多个的过程就叫做分片,分片是属于横向扩展方案。
分片:表示分配过程,是一个逻辑上概念,表示如何实现,是一个概念,很多实现的
分库分表:表示分配结果,是一个物理上概念,表示终实现的结果,是一个终的体现。可以理解为 一种分片的实现。
数据库扩展方案:
横向扩展:一个库变多个库,加机器数量,加多机器 纵向扩展:一个库还是一个库,优化机器性能,加高配CPU或内存,加大机器
在分布式存储系统中,数据需要分散存储在多台设备上,分片就是把数据库横向扩展到多个数据库服务 器上的一种有效的方式,其主要目的就是为突破单节点数据库服务器的 I/O 能力限制,解决数据库扩展 性问题。 4.3.2、分片策略
数据分片是根据指定的分片键和分片策略将数据水平拆分,拆分成多个数据片后分散到多个数据存储节 点中。分片键是用于划分和定位表的字段,一般使用ID或者时间字段。而分片策略是指分片的规则,常 用规则有以下几种。
基于范围分片
根据特定字段的范围进行拆分,比如用户ID、订单时间、产品价格等。例如:{[1 - 100] => Cluster A, [101 - 199] => Cluster B}
优点:新的数据可以落在新的存储节点上,如果集群扩容,数据无需迁移。扩容了,后面来的数据直接 就放到新节点就行了。
缺点:数据热点分布不均,数据冷热不均匀,导致节点负荷不均。比如按时间分或者id大小,你肯定 新的数据访问多,所以冷热不均匀,前面的数据基本不被访问,压力全在新节点上。
哈希取模分片
整型的Key可直接对设备数量取模,其他类型的字段可以先计算Key的哈希值(换成一个数值),然后再对 设备数量取模。假设有n台设备,编号为0 ~ n-1,通过Hash(Key) % n就可以确定数据所在的设备编号。 该模式也称为离散分片。
优点:实现简单,数据分配比较均匀,不容易出现冷热不均,负荷不均的情况。
缺点:扩容时会产生大量的数据迁移,比如从n台设备扩容到n+1,绝大部分数据需要重新分配和迁移, 除数变了,很多数据都得变。
一致性哈希分片
采用Hash取模的方式进行拆分,后期集群扩容需要迁移旧的数据。使用一致性Hash算法能够很大程度 的避免这个问题,所以很多中间件的集群分片都会采用一致性Hash算法。
一致性Hash是将数据按照特征值映射到一个首尾相接的Hash环上,同时也将节点(按照IP地址或者机 器名Hash)映射到这个环上。对于数据,从数据在环上的位置开始,顺时针找到的第一个节点即为数据 的存储节点。Hash环示意图与数据的分布如下:
一致性Hash在增加或者删除节点的时候,受到影响的数据是比较有限的,只会影响到Hash环相邻的节 点,不会发生大规模的数据迁移。
Redis食客项目总结了,可以看看。 4.4、扩容方案
当系统用户进入了高速增长期时,即便是对数据进行分库分表,但数据库的容量,还有表的数据量也总 会达到天花板。当现有数据库达到承受极限时,就需要增加新服务器节点数量进行横向扩容。
首先来思考一下,横向扩展会有什么技术难度?
数据迁移问题 分片规则改变 数据同步、时间点、数据一致性
遇到上述问题时,我们可以使用以下两种方案: 4.4.1、停机扩容
这是一种很多人初期都会使用的方案,尤其是初期只有几台数据库的时候。停机扩容的具体步骤如下: 停的可以是一部分,不要全停,当然了非得全停也没办法。
站点发布一个公告,例如:“为了为广大用户提供更好的服务,本站点将在今晚00:00-2:00之间升 级,给您带来不便抱歉"; 时间到了,停止所有对外服务; 新增n个数据库,然后写一个数据迁移程序,将原有x个库的数据导入到新的y个库中。比如分片 规则由%x变为%y; 数据迁移完成,修改数据库服务配置,原来x个库的配置升级为y个库的配置 重启服务,连接新库重新对外提供服务
回滚方案:万一数据迁移失败,需要将配置和数据回滚,改
优点:简单
缺点:
停止服务,缺乏高可用 程序员压力山大,需要在指定时间完成 如果有问题没有及时测试出来启动了服务,运行后一段时间发现问题,数据会丢失一部分,难以回 滚,因为已经跑开了,你必然会影响一部分数据。
适用场景:
小型网站 大部分游戏 对高可用要求不高的服务 4.4.2、平滑扩容
数据库扩容的过程中,如果想要持续对外提供服务,保证服务的可用性,平滑扩容方案是好的选择。 平滑扩容就是将数据库数量扩容成原来的2倍,比如:由2个数据库扩容到4个数据库,具体步骤如下: 新增2个数据库,3,4 配置双主进行数据同步(先测试、后上线,谨慎一些,重启秒级,还行),3和1做双主同步,4和2 做双主
数据同步完成之后,配置双主双写,因为你同步是有过程的,同步中间来的写操作也要全部写入给 1的也写入3和给2的也写入4,保证后是同步完成,13 24是一样的数据(同步因为有延迟,如果 时时刻刻都有写和更新操作,写操作很多会来不及同步,必然会存在不准确问题)
数据同步完成后,删除双主同步,修改数据库配置,并重启;
此时已经扩容完成,但此时的数据并没有减少,新增的数据库跟旧的数据库一样多的数据,此时还 需要写一个程序,清空数据库中多余的数据,如: User1去除 uid % 4 = 2的数据; User3去除 uid % 4 = 0的数据; User2去除 uid % 4 = 3的数据; User4去除 uid % 4 = 1的数据; 平滑扩容方案能够实现n库扩2n库的平滑扩容,增加数据库服务能力,降低单库一半的数据量。其核心 原理是:成倍扩容,避免数据迁移。
优点:
扩容期间,服务正常进行,保证高可用 相对停机扩容,时间长,项目组压力没那么大,出错率低 扩容期间遇到问题,随时解决,不怕影响线上服务 可以将每个数据库数据量减少一半
缺点:
程序复杂、配置双主同步、双主双写、检测数据同步等 后期数据库扩容,比如成千上万都来个2n开发就别参与了,运维来吧,代价比较高,可以拆库
适用场景:
大型网站 对高可用要求高的服务 七、Mysql性能优化 1、数据库系统优化 1.1、数据库的优化维度
数据库的优化维度有四个:硬件升级,系统配置,表结构设计,sql语句索引。
优化的成本和难易程度是成反比的,如上图。表结构设计和sql语句索引是程序员来控制的。所以重点在 这两项学习上,毕竟我们就是程序员。
优化选择:
优化成本:硬件升级>系统配置>表结构设计>SQL语句及索引 优化效果:硬件升级<系统配置<表结构设计<SQL语句及索引 1.2、系统配置优化 1.3、保证从内存中读取数据
Mysql会在内存中保存一定的数据,通过LRU算法来控制内存中的数据,而将不常访问的数据保存在硬盘 文件中。等你需要访问的时候就从磁盘中通过swap机制换入到内存中。 所以我们要是尽可能的扩大内存中的数据,将数据保存在内存中,从内存中读取数据,可以提高mysql 的性能。
Innodb的内存是通过innodb_buffer_pool_size来控制的,所以扩大这个参数能够全然从内存中读取数 据,大限度的降低磁盘操作。
确定Innodb_buffer_pool_size大小的方法;
mysql> SHOW GLOBAL STATUS LIKE 'innodb_buffer_pool_pages_%'; +----------------------------------+-------+ | Variable_name | Value | +----------------------------------+-------+ | Innodb_buffer_pool_pages_data | 256 | | Innodb_buffer_pool_pages_dirty | 0 | 0表示已经被用光 | Innodb_buffer_pool_pages_flushed | 549 | | Innodb_buffer_pool_pages_free | 255 | 这个就是存空闲页的,他为0就寂了 | Innodb_buffer_pool_pages_misc | 1 | | Innodb_buffer_pool_pages_total | 512 | +----------------------------------+-------+ 6 rows in set (0.00 sec)
innodb_buffer_pool_size默认是128M,理论上可以扩大到内存的3/4或4/5。上面的数字单位都是页, 每个页16KB,加起来可以测一下。看你的机器了,要是你只有mysql服务,那可以是这个比例,要是还 有别的东西部署,那就别了,别挤压别的服务的。
扩大这个参数需要修改配置文件my.cnf,比如我是1G大小内存,那就可以设置个750.改完配置文件重启 服务生效。
innodb_buffer_pool_size = 750M 如果你的Mysql是专用的mysql server可以禁用SWAP机制,记住是mysql专用才行,你要是还有别的服 务就算了。因为swap是内存不足的时候操作系统和磁盘做空间交换的机制,把内存中不用的数据换到磁 盘上,你要用的从磁盘换进来。要是你关闭了swap机制就会报内存溢出OOM。
所以你要是还有别的服务,那就还是别关了,免得影响别的服务。
1.4、数据预热
mysql中,某个数据被读取一次才会缓存在innodb_buffer_pool中。所以要是在数据库刚刚启动就大量 请求过来,没有内存缓存,数据库就GG。所以要对数据进行预热,将磁盘上的全部数据(大量数据)可以 缓存到内存中去。数据预热能提高读取速度。
对于Innodb 数据库,进行数据预热的脚本 很麻烦,建议DBA来做,需要加载很多系统表等等。
在你需要预热时候,刚刚启动数据库服务可以执行这个脚本。 1.5、降低磁盘写入次数
增大redolog buffer,减少落盘次数,Buffer满了就去落盘成为redolog,所以增大可以减少落盘次 数 innodb_buffer_pool_size 设置为0.25 * innodb_buffer_pool_size 通过查询日志,慢查询日志可以不开,bin-log可以开 生产中不开启通用查询日志,遇到性能问题再开启慢查询日志。慢日志写入少了,也能提高性能。 写redolog策略innodb_flush_log_at_trx_commit设置为0或2 如果不涉及非常高的安全性(金融系统),或者基础架构足够安全,或者事务都非常小,都能用0 或者2来减少磁盘操作。这个可以参考前面的内容。 1.6、提高磁盘读写性能
使用SSD或者内存磁盘。 2、表结构设计优化
# 查看swap cat /proc/swaps Filename Type size Used Priority /dev/sda2 partition 1048572 0 -1
# 关闭所有的交换设备和文件 swapoff -a
2.1、设计中间表
设计中间表,一般针对于统计分析功能,或者实时性不高的需求(OLTP,OLAP)。类似报表这种,你要从 好几张表中取数据,然后聚合生成结果返回。可以建立一张中间表,包含各个表返回的字段,使用存储 过程把各个表计算好存到中间表中,也可以用定时任务处理到中间表中。你要的时候直接就从中间表获 取,就能快点,报表类的不需要实时性,因为他是分析一个趋势之类的。 2.2、设计冗余字段
这个我太有感受了,减少关联的,适当冗余个name这种,不用关联id取出来再获取name。 为了减少关联查询,创建合理的冗余字段是有必要的(创建冗余字段还需要注意数据一致性问题,这个改 了对应的数据也得改,不能只改一个,冗余这种多次出现就是麻烦) 2.3、拆表
对于字段太多的大表,考虑拆表(比如一个表有100多个字段)
对于表中经常不被使用的字段或者存储数据比较多的字段可以考虑拆表,就是垂直分表。后面的分库分 表会详解。比如用户表特别大,可以把用户做个主表,然后拆出通讯表,地址表,详情表。主表可能就 很简单就一些基础信息。其余的信息分布到其他表,像什么不常用的就拆出去。 2.4、主键优化
每张表建议都要有一个主键(主键索引),而且住家类型好是Int类型,建议自增主键(分布式下可以上雪 花)。设计到页分裂的东西。存储空间小,可排序,有顺序。 2.5、字段的设计
数据库中的表越小,在它上面执行的查询也就越快。
因此,在创建表的时候,为了得到更好的性能。我们可以将表中字段的宽度设计的尽可能小。别动不动 就是varchar255.
尽量把字段设置为not null ,这样在将来执行查询的时候,数据库不用去比较Null值,因为显式声明not null就是告诉mysql不为空,不用比较了,因为Null类型需要额外去设计存储。 对于某些文本字段,例如省份,或者性别,我们可以将他们定义为ENUM类型,因为在mysql中enum类 型被当做数值数据来处理,而数值型数据被处理起来的速度要比文本类型快的多,这样我们就可以提高 数据库的性能。因为数字编解码比较简单,存储和读取也快。
所以能用数字的就用数值类型,性别就1,0 3、SQL语句及索引优化 3.1、初始化数据库
新建一个表
表中插入一千万数据,创建存储过程执行
执行该存储过程,插入一千万数据,时间太长了,我就插了两百七十万数据差不多了。
注意此时只有一个主键索引。
3.2、使用explain来查看索引使用情况
执行一个语句,无索引
耗时一秒。explain分析,发现无索引,走了全表扫描。打开慢查询日志查看发现超过了阈值的sql,那 我们拿出来这个sql使用explain来分析一下。
CREATE TABLE tbiguser ( id INT PRIMARY KEY auto_increment, nickname VARCHAR (255), loginname VARCHAR (255), age INT, sex CHAR (1), STATUS INT, address VARCHAR (255) );
CREATE PROCEDURE test_insert() BEGIN DECLARE i INT DEFAULT 1; WHILE i<=10000000 DO INSERT INTO tbiguser VALUES(null,concat('zy',1),concat('zhaoyun',i),23,'1','1','beijing'); SET i = i + 1; END WHILE; COMMIT; END;
在nickname上建立索引
居然用了11秒之久,我们建立索引还不如不建立的好,这是因为300万数据的nickname全是zy1这个 值,区分度基本没有,所以即便你走了索引其实也是一个全索引都扫,没有缩小扫描区间。而且还加上 了回表操作,自然就不如不建立索引。
在Loginname建立索引
时间邹然缩短,因为Loginname都是不一样的值,区分度极其高,直接在索引树上找到回表一次即可。 建立nickname + loginname 联合索引
存在三个索引,只用了一个,可见那个区分度不高的直接就不用了。
index类型虽然用了索引,但是还是走了全表,没减少扫描行数,建议再优化。
3.3、SQL语句中in包含的值不应过多
Mysql对于In做了对应的优化,就是把in中的数据全部存储在一个数组中,而且这个数组是排好序的,使 用二分匹配,但是如果数值较多,产生的消耗也多。 3.4、select语句务必指明字段名称
select *增加了很多不必要的消耗(cpu io 内存 网络带宽);减少了使用覆盖索引的可能性,当表结构发生 改变时候,前端也要更新,所以好直接在select后面接上你要的字段名,不要的就不加了。 3.5、当只需要一条数据的时候,使用Limit 1
limit是可以停止全表扫描的,一旦扫到立刻结束并返回。 3.6、排序字段加索引
好是数字,字符串还得一个个字符对比,排序加了索引,直接在索引树上按照顺序拿就好了,免得你 还得拿回来server层做排序。 3.7、如果限制条件中其他字段没有索引,尽量少用or
or两边的字段中,如果有一个不是索引字段,会造成该查询不走索引的情况。可以用union all来优化, 不会使有索引的一处失效。另一处没索引的不管了。 3.8、尽量用union all 代替union
union 和union all的差异主要是前者需要将结果集合并之后再进行唯一性过滤操作,这就会涉及到排 序,增加大量的cpu运算,加大资源消耗以及延迟。当然,union all的前提条件是两个结果集没有重复 数据。 3.9、不使用order by rand()
随机排序不走索引 3.10、区分in和exists、not in和not exists
区分In和exists主要是造成了驱动顺序的改变(这是性能变化的关键),如果是exists,那么以外层表为驱 动表,先被访问。如果是In先执行子查询,所以In适合于外表大而内表小的情况,exists适用于外表小而 内表大的情况。
4、mysql开发规约,
参考阿里开发规范
in后面的括号里面是会做排序的,所以in里面要是有查询语句,查询语句符合情况这个查询可能会做排 序出现文件排序in(select ...)。 5、复杂语句优化实战
union all会有临时表,两处连接成临时表返回
七、分库分表实战及中间件 前言 实战背景介绍 背景描述
刚开始我们的系统只用了单机数据库 随着用户的不断增多,考虑到系统的高可用和越来越多的用户请求,我们开始使用数据库主从架构 当用户量级和业务进一步提升后,写请求越来越多,加主库可以解决一部分,但是跟不上规模速 度,而且双主的写会有一致性问题,这时我们开始使用了分库分表,去除掉双主。 遇到的问题
用户请求量太大 单服务器TPS、内存、IO都是有上限的,需要将请求打散分布到多个服务器 单库数据量太大 单个数据库处理能力有限;单库所在服务器的磁盘空间有限;单库上的操作IO有瓶颈 单表数据量太大 查询、插入、更新操作都会变慢,在加字段、加索引、机器迁移都会产生高负载,影响服务 一、解决问题 1、垂直拆分 1.1、垂直分库
微服务架构时,业务切割得足够独立,数据也会按照业务切分,保证业务数据隔离,大大提升了数据库 的吞吐能力
微服务下对服务切割为三个部分,三个数据量大的拆分出三个库,分别放用户,简历和职位的信息。
1.2、垂直分表
表中字段太多且包含大字段的时候,在查询时对数据库的IO、内存会受到影响,同时更新数据时,产生 的binlog文件会很大,MySQL在主从同步时也会有延迟的风险。
把职位表大量字段可以拆分成两个表,比如这个职位表,描述字段比较大,但是我们一般查询列表的时 候不需要职位的描述,我们是点击详情进去才看到描述,所以可以分成两个表,把大字段放到详情表 中,避免了每次搜一个大的表。用的时候再去详情大表里面拿。
垂直分是把所有拆分完的合起来是一个完整的结构,数据条数每个拆开的表是一致的。 2、水平拆分 2.1、水平分表
针对数据量巨大的单张表(比如订单表),按照规则把一张表的数据切分到多张表里面去。但是这些表 还是在同一个库中,所以单库级别的数据库操作还是有IO瓶颈。
2.2、水平分库
将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。 水平 分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。大部分 其实是分库分表水平垂直同时存在的。
2.3、水平分库规则
不跨库、不跨表,保证同一类的数据都在同一个服务器上面。
数据在切分之前,需要考虑如何高效的进行数据获取,如果每次查询都要跨越多个节点,就需要谨慎使 用。 2.4、水平分表规则
RANGE
时间:按照年、月、日去切分。例如order_2020、order_202005、order_20200501 地域:按照省或市去切分。例如order_beijing、order_shanghai、order_chengdu 大小:从0到1000000一个表。例如1000001-2000000放一个表,每100万放一个表,会有数据倾 斜问题,旧的数据可能很难被访问到了。
HASH
用户ID取模:
然后,除了上面两种规则,不同的业务使用的切分规则是不一样,就上面提到的切分规则,举例如下:
站内信
用户维度:用户只能看到发送给自己的消息,其他用户是不可见的,这种情况下是按照用户ID hash分 库,在用户查看历史记录翻页查询时,所有的查询请求都在同一个库内.把一个人的放一个地方,不能找 一个人跨N个库,那效率就裂开了。
用户表
1、范围法:以用户ID为划分依据,将数据水平切分到两个数据库实例,如:1到1000W在一张表, 1000W到2000W在一张表,这种情况会出现单表的负载较高 2、按照用户ID HASH尽量保证用户数据均衡分到数据库中
流水表
时间维度:可以根据每天新增的流水来判断,选择按照年份分库,还是按照月份分库,每天的流水多甚 至也可以按照日期分库
订单表
在拉勾网,求职者(下面统称C端用户)投递企业(下面统称B端用户)的职位产生的记录称之为订单 表。在线上的业务场景中,C端用户看自己的投递记录,每次的投递到了哪个状态,B端用户查看自己收 到的简历,对于合适的简历会进行下一步沟通,同一个公司内的员工可以协作处理简历。
如何能同时满足C端和B端对数据查询,不进行跨库处理?
终方案:为了同时满足两端用户的业务场景,采用空间换时间,将一次的投递记录存为两份,C端的 投递记录以用户ID为分片键,B端收到的简历按照公司ID为分片键。每个库都有两份表,一个是B端一个 是C端。
如果在登录场景下,用户输入手机号和验证码进行登录,这种情况下,登录时是不是需要扫描所有分库的信 息?答案是不用,你是按ID做的hash,你拿手机号校验,你怎么定位在哪个库里面呢? 最终方案:用户信息采用ID做切分处理,同时存储用户ID和手机号的映射的关系表(新增一个关系表),关系 表采用手机号进行切分。可以通过关系表根据手机号查询到对应的ID,再定位用户信息。两层目录的意思。其 实是借助空间换取时间,你要是拿电话号码直接比较,可能就得跨好多库。 要是直接走id就直接拿id去做哈希映射。
2.5、主键选择
UUID:本地生成,不依赖数据库,缺点就是作为主键性能太差 SNOWFLAKE:百度UidGenerator、美团Leaf、基于SNOWFLAKE算法实现 2.6、数据一致性
强一致性:XA协议 终一致性:TCC、saga、Seata 2.7、数据库扩容
成倍增加数据节点,实现平滑扩容 成倍扩容以后,表中的部分数据请求已被路由到其他节点上面,可以编程清理掉 2.8、业务层改造
基于代理层方式:Mycat、Sharding-Proxy、MySQL Proxy 基于应用层方式:Sharding-jdbc 2.9、分库后面临的问题
事务问题:一次投递需要插入两条记录,且分布在不同的服务器上,数据需要保障一致性。强一致 还是终一致的方案。 跨库跨表的join问题 全局表(字典表):基础数据/配置数据,所有库都拷贝一份,这类表就不用跨库了 字段冗余:可以使用字段冗余就不用join查询了,冗余在每个表中都有一份,大量字段就算 了,少量可以。大量字段你等于没分表,大部分冗余之后各个表都特么一样了。 系统层组装:可以在业务层分别查询出来,然后组装起来,逻辑较复杂 额外的数据管理负担和数据运算压力:数据库扩容、维护成本变高 二、ShardingSphere实战
1、ShardingSphere
Apache ShardingSphere是一款开源的分布式数据库中间件组成的生态圈。它由Sharding-JDBC、 Sharding-Proxy和Sharding-Sidecar(规划中)这3款相互独立的产品组成。 他们均提供标准化的数据 分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、容器、云原生等各种多样化的 应用场景。
ShardingSphere项目状态如下:
ShardingSphere定位为关系型数据库中间件,旨在充分合理地在分布式的场景下利用关系型数据库的计 算和存储能力,而并非实现一个全新的关系型数据库。
Sharding-JDBC:被定位为轻量级Java框架,在Java的JDBC层提供的额外服务,以jar包形式使 用,jar包就是java没错了。 Sharding-Proxy:被定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本, 用于完成对异构语言的支持。 Sharding-Sidecar:被定位为Kubernetes或Mesos的云原生数据库代理,以DaemonSet的形式代 理所有对数据库的访问。
Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar三者区别如下:虽然是jdbc是任意数据库,但是 前提是关系型的,redis这种肯定不行。
[ShardingSphere安装包下载] [https://shardingsphere.apache.org/document/current/cn/downloads/]
使用Git下载工程:git cloneGitHub - apache/shardingsphere: Build criterion and ecosystem above multi-model databases
2、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。
业务应用和数据库中间加了一层jdbc的,用来做数据库治理,分库分表的路由。 2.1、Sharding-JDBC主要功能:
数据分片 分库、分表 读写分离 分片策略 分布式主键 分布式事务 标准化的事务接口 XA强一致性事务 柔性事务 数据库治理
配置动态化,集中配置,提供配置中心 编排和治理 数据脱敏 可视化链路追踪 2.2、Sharding-JDBC 内部结构:
图中黄色部分表示的是Sharding-JDBC的入口API,采用工厂方法的形式提供。 目前有 ShardingDataSourceFactory和MasterSlaveDataSourceFactory两个工厂类。 ShardingDataSourceFactory支持分库分表、读写分离操作,分库分表分片就是这个 MasterSlaveDataSourceFactory支持读写分离操作,你要是使用单独的主从结构的读写分离 就是使用这个 图中蓝色部分表示的是Sharding-JDBC的配置对象,提供灵活多变的配置方式。 ShardingRuleConfifiguration是分库分表配置的核心和入口,它可以包含多个 TableRuleConfifiguration和MasterSlaveRuleConfifiguration。 TableRuleConfifiguration封装的是表的分片配置信息,有5种配置形式对应不同的 Confifiguration类型。 MasterSlaveRuleConfifiguration封装的是读写分离配置信息。 图中红色部分表示的是内部对象,由Sharding-JDBC内部使用,应用开发者无需关注。Sharding JDBC通过ShardingRuleConfifiguration和MasterSlaveRuleConfifiguration生成真正供 ShardingDataSource和MasterSlaveDataSource使用的规则对象。ShardingDataSource和 MasterSlaveDataSource实现了DataSource接口,是JDBC的完整实现方案。对应两种操作使用, 一个就是单纯的主从读写分离的,一个就是分库分表分片两种概念。
2.3、Sharding-JDBC初始化流程:
根据配置的信息生成Confifiguration对象,不同的配置生成不同的Confifiguration对象 通过Factory会将配置信息Confifiguration对象转化为Rule对象 通过Factory会将Rule对象与DataSource对象封装 Sharding-JDBC使用DataSource进行分库分表或读写分离操作,DataSource是个上层接口,下面 有读写分离和分库分表两种实现,对应不同的封装配置,走不同的策略,这里其实是个策略模式 2.4、Sharding-JDBC 使用过程:
引入maven依赖
注意: 请将${latest.release.version}更改为实际的版本号。 规则配置 Sharding-JDBC可以通过Java,YAML,Spring命名空间和Spring Boot四种方式配置,开发者可根 据场景选择适合的配置方式。 创建DataSource 通过ShardingDataSourceFactory工厂和规则配置对象获取ShardingDataSource,然后即可通过 DataSource选择使用原生JDBC开发,或者使用JPA, MyBatis等ORM工具。
3、数据分片实战剖析 3.1、核心概念
1、表概念 1、真实表
数据库中真实存在的物理表。例如b_order0、b_order1 2、逻辑表
在分片之后,同一类表结构的名称(总成)。例如b_order。 3、数据节点 在分片之后,由数据源和数据表组成。例如ds0.b_order1,定位到哪个数据源的哪个表
4、绑定表(分片规则一致并且有关联关系) 指的是分片规则一致的关系表(主表、子表),例如b_order和b_order_item,均按照order_id分 片,则此两个表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,可以提升关联查 询效率。
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-core</artifactId> <version>${latest.release.version}</version> </dependency>
DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, props);
如果不配置绑定表关系,采用笛卡尔积关联,会生成4个SQL,每个分表都会去做关联 (00,01,11,10),保证在完整的数据中找到你符合条件的数据,这样性能不高,所以要绑定表,就能知 道哪些数据在哪些表里面,不必完整查询,只查自己符合条件要的。
如果配置绑定表关系,生成2个SQL,因为order0的详情表是Item0,你关联也只需要去0上关联。绑定 了,就知道直接关联了。因为order0的数据其实就是在Item0上,因为你用的是一致的分片规则。所以 能字节对上。
不绑定,ss不知道这个关系,为了保证完整不遗漏正确,就会分别和0,1两个Item表关联,后在完整的 结果集就是四个里面查结果。
5、广播表
在使用中,有些表没必要做分片,例如字典表、省份信息等,因为他们数据量不大,而且这种表可能 需要与海量数据的表进行关联查询。广播表会在不同的数据节点上进行存储,存储的表结构和数据完全 相同。每个节点都存一份,一般不做改动,可以设计为广播表。
要是不广播,可能跨节点那就性能低了。
2、分片算法(ShardingAlgorithm)
由于分片算法和业务实现紧密相关,因此并未提供内置分片算法,而是通过分片策略将各种场景提炼出 来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。目前提供4种分片算法。让你自 己可以根据这个算法自己扩展自己业务的算法。他只是给你提供了一个接口,你可以自己去实现他们, 自己定义实现类里面的业务方法。
精确分片算法PreciseShardingAlgorithm 用于处理使用单一键作为分片键的=与IN进行分片的场景。 范围分片算法RangeShardingAlgorithm 用于处理使用单一键作为分片键的BETWEEN AND、>、<、>=、<=进行分片的场景。 复合分片算法ComplexKeysShardingAlgorithm
b_order:b_order0、b_order1 b_order_item:b_order_item0、b_order_item1 # 需求是从order表里面查,关联item里面查出信息 b_order 和 b_order_item就是逻辑表,你写的 sql就这个,不写0,1ss会给你根据你的规则做映射拼接成真实表名。要是你连0,1都自己写了,那要他干嘛。 太废了。 select * from b_order o join b_order_item i on(o.order_id=i.order_id) where o.order_id in (10,11);
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);
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);
用于处理使用多键作为分片键进行分片的场景,多个分片键的逻辑较复杂,需要应用开发者自 行处理其中的复杂度。 Hint分片算法HintShardingAlgorithm 用于处理使用Hint行分片的场景。对于分片字段非SQL决定(不是表中字段),而由其他外置条 件决定的场景,可使用SQL Hint灵活的注入分片字段。例:内部系统,按照员工登录主键分 库,而数据库中并无此字段。SQL Hint支持通过Java API和SQL注释两种方式使用。 3、分片策略(ShardingStrategy) 分片策略包含分片键和分片算法,真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前 提供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 不分片的策略。
4、分片策略配置 对于分片策略存有数据源分片策略和表分片策略两种维度,两种策略的API完全相同。可以把分片策略作 用在表和数据源上,然后有下面两种。
数据源分片策略 用于配置数据被分配的目标数据源。 表分片策略 用于配置数据被分配的目标表,由于表存在与数据源内,所以表分片策略是依赖数据源分片策 略结果的。 3.2、流程剖析
ShardingSphere 3个产品的数据分片功能主要流程是完全一致的,如下图所示。
or会优化成union两个查询,当然是有索引的时候。ss会做这种优化。
1、SQL解析 SQL解析分为词法解析和语法解析。 先通过词法解析器将SQL拆分为一个个不可再分的单词。再使用语 法解析器对SQL进行理解,并终提炼出解析上下文。
Sharding-JDBC采用不同的解析器对SQL进行解析,解析器类型如下:不同的解析器也就是一个方言的体 现。
MySQL解析器 Oracle解析器 SQLServer解析器 PostgreSQL解析器 默认SQL解析器 2、查询优化
负责合并和优化分片条件,如OR等。or会优化成union两个查询,当然是有索引的时候。mysql底层会 做这种优化,ss也会做这种优化。 3、SQL路由
根据解析上下文匹配用户配置的分片策略,并生成路由路径。目前支持分片路由和广播路由。
4、SQL改写 将SQL改写为在真实数据库中可以正确执行的语句。SQL改写分为正确性改写和优化改写。
5、SQL执行
通过多线程执行器异步执行SQL。 6、结果归并
将多个执行结果集归并以便于通过统一的JDBC接口输出。结果归并包括流式归并、内存归并和使用装饰 者模式的追加归并这几种方式。 3.3、SQL使用规范(ss中)
我们程序员写的sql是不带后缀的,写的是逻辑名,真正执行ss还得优化解析成物理表映射执行。所以对 于程序员来说,你怎么写都行,但是你还得遵循一些规则,不然SS执行会有问题。 1、SQL使用规范
支持项 路由至单数据节点时(单数据库),目前MySQL数据库100%全兼容,其他数据库完善中。 路由至多数据节点时,全面支持DQL、DML、DDL、DCL、TCL。支持分页、去重、排序、分 组、聚合、关联查询(不支持跨库关联)。以下用为复杂的查询为例:
不支持项(路由至多数据节点) 不支持CASE WHEN、HAVING、UNION (ALL),他底下可能会吧Or优化为union ,但是你写 出来他不支持。 支持分页子查询,但其他子查询有限支持,无论嵌套多少层,只能解析至第一个包含数据表的子查 询,一旦在下层嵌套中再次找到包含数据表的子查询将直接抛出解析异常。 例如,以下子查询可以支持:
以下子查询不支持:解析异常。
简单来说,通过子查询进行非功能需求,在大部分情况下是可以支持的。比如分页、统计总数等; 而通过子查询实现业务查询当前并不能支持。 由于归并的限制,子查询中包含聚合函数目前无法支持。 不支持包含schema的SQL。因为ShardingSphere的理念是像使用一个数据源一样使用多数据源, 因此对SQL的访问都是在同一个逻辑schema之上。 当分片键处于运算表达式或函数中的SQL时,将采用全路由的形式获取结果。例如下面SQL, create_time为分片键:
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}]
基本就是全sql结构了。除了having,他不支持having。
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 = ?))
SELECT * FROM b_order WHERE to_date(create_time, 'yyyy-mm-dd') = '2020- 0505';
由于ShardingSphere只能通过SQL字面提取用于分片的值,因此当分片键处于运算表达式或函数中 时,ShardingSphere无法提前获取分片键位于数据库中的值,从而无法计算出真正的分片值。 不支持的SQL示例
2、分页查询 完全支持MySQL和Oracle的分页查询,SQLServer由于分页查询较为复杂,仅部分支持.
性能瓶颈: 查询偏移量过大的分页会导致数据库获取数据性能低下,以MySQL为例:
这句SQL会使得MySQL在无法利用索引的情况下跳过1000000条记录后,再获取10条记录,其性能 可想而知。 而在分库分表的情况下(假设分为2个库),为了保证数据的正确性,SQL会改写为:
即将偏移量前的记录全部取出,并仅获取排序后的后10条记录。这会在数据库本身就执行很慢的 情况下,进一步加剧性能瓶颈。 因为原SQL仅需要传输10条记录至客户端,而改写之后的SQL则会 传输1,000,010 * 2的记录至客户端。 ShardingSphere的优化:框架的本身底层优化 ShardingSphere进行了以下2个方面的优化。 首先,采用流式处理 + 归并排序的方式来避免内存的过量占用。在内存中结果集有序是使用 游标去查找处理的,不是真的操作数据,后返回根据游标再取数据就行 其次,ShardingSphere对仅落至单节点的查询进行进一步优化。分页就不做sql改写,就执行 原sql,优化交给mysql自己去做 分页方案优化:你自己的sql优化 由于LIMIT并不能通过索引查询数据,因此如果可以保证ID的连续性,通过ID进行分页是比较好的 解决方案:
或通过记录上次查询结果的后一条记录的ID进行下一页的查询:如果有序不连续
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(库名ds) 包含表名这种了,就不支持,他的理念是 屏蔽这些,你不能去指定路由,需要SS自己去判断 SELECT SUM(DISTINCT col1), SUM(col1) FROM tbl_name //同时使用普通聚合函数 和 DISTINCT , SUM(DISTINCT col1)不支持(太复杂的他不支持),单独用SUM(col1)是支持的 SELECT * FROM tbl_name WHERE to_date(create_time, ‘yyyy-mm-dd’) = ? //使用函数 会导致 全路由
SELECT * FROM b_order ORDER BY id LIMIT 1000000, 10
SELECT * FROM b_order ORDER BY id LIMIT 0, 1000010
SELECT * FROM b_order WHERE id > 1000000 AND id <= 1000010 ORDER BY id
SELECT * FROM b_order WHERE id > 1000000 LIMIT 10
3.4、其他功能
1、Inline行表达式 InlineShardingStrategy:采用Inline行表达式进行分片的配置。
Inline是可以简化数据节点和分片算法配置信息。主要是解决配置简化、配置一体化。
语法格式:
行表达式的使用非常直观,只需要在配置中使用${ 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
db${0..1}.b_order${1..2} 或者 db$->{0..1}.b_order$->{1..2}
用行表达式可以简化为:
分片算法配置:
行表达式内部的表达式本质上是一段Groovy代码,可以根据分片键进行计算的方式,返回相应的真实数 据源或真实表名称。
结果为:ds0、ds1、ds2... ds9
表示ds后面的后缀是0-9之间的数字。因为你做的对10取模。 2、分布式主键
ShardingSphere不仅提供了内置的分布式主键生成器,例如UUID、SNOWFLAKE,还抽离出分布式主 键生成器的接口,方便用户自行实现自定义的自增主键生成器。
内置主键生成器:如果继续沿用自增,可能分库分表会冲突 UUID:采用UUID.randomUUID()的方式产生分布式主键。 SNOWFLAKE:在分片规则配置模块可配置每个表的主键生成策略,默认使用雪花算法,生成 64bit的长整型数据。 自定义主键生成器: 自定义主键类,实现ShardingKeyGenerator接口 按SPI规范配置自定义主键类 在Apache ShardingSphere中,很多功能实现类的加载方式是通过SPI注入的方式完成的。注 意:在resources目录下新建META-INF文件夹,再新建services文件夹,然后新建文件的名字 为org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator,打开文件,复制自定义 主键类全路径到文件中保存。 自定义主键类应用配置(TODO,查资料实现一下)
4、分布式事务剖析实战 4.1、分布式事务理论
CAP(强一致性) CAP 定理,又被叫作布鲁尔定理。对于共享数据系统,多只能同时拥有CAP其中的两个,任意两 个都有其适应的场景。
db0.b_order${0..1},db1.b_order${2..4}
ds${id % 10} 或者 ds$->{id % 10}
#对应主键字段名 spring.shardingsphere.sharding.tables.t_book.key-generator.column=id #对应主键类getType返回内容 spring.shardingsphere.sharding.tables.t_book.key- generator.type=LAGOUKEY
BASE(终一致性) BASE 是指基本可用(Basically Available)、软状态( Soft State)、终一致性( Eventual Consistency)。它的核心思想是即使无法做到强一致性(CAP 就是强一致性),但应用可以采用 适合的方式达到终一致性。 BA指的是基本业务可用性,支持分区失败; S表示柔性状态,也就是允许短时间内不同步; E表示终一致性,数据终是一致的,但是实时是不一致的。 原子性和持久性必须从根本上保障,为了可用性、性能和服务降级的需要,只有降低一致性和隔离 性的要求。BASE 解决了 CAP 理论中没有考虑到的网络延迟问题,在BASE中用软状态和终一 致,保证了延迟后的一致性。 4.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接口。
AP就是应用程序,各种程序。 RM可以理解为数据库之类的,就是管理数据的,你就理解为数据库就行。RM可以有多个,分布式 事务就是多个数据库操作。比如MYSQL就得实现这个RM接口的规范才能完成XA规范。 TM就是事务管理器,用来协调和管理事务的,可以和RM交互,可以和AP做交互调用。控制全局事 务,管理事务的声明周期之类的。 XA之所以需要引入事务管理器,是因为在分布式系统中,从理论上讲两台机器理论上无法达到一致 的状态,需要引入一个单点进行协调。由全局事务管理器管理和协调的事务,可以跨越多个资源 (数据库)和进程。 事务管理器用来保证所有的事务参与者都完成了准备工作(第一阶段)。如果事务管理器收到所有参 与者都准备好的消息,就会通知所有的事务都可以提交了(第二阶段)。MySQL 在这个XA事务中 扮演的是参与者(RM)的角色,而不是事务管理器。
TCC模式(终一致性) TCC(Try-Confifirm-Cancel)的概念,早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。TCC 是服务化的两阶段 编程模型,其 Try、Confifirm、Cancel 3 个方法均由业务编码实现: Try 操作作为一阶段,负责资源的检查和预留; Confifirm 操作作为二阶段提交操作,执行真正的业务; Cancel 是预留资源的取消; TCC事务模式相对于 XA 等传统模型如下图所示:
TCC 模式相比于 XA,解决了如下几个缺点: 解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器可以变成多点,引 入集群。 同步阻塞:引入超时机制,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑 形式,粒度变小。 数据一致性,有了补偿机制之后,由业务活动管理器控制一致性。 消息队列模式(终一致性) 消息队列的方案初是由 eBay 提出,基于TCC模式,消息中间件可以基于 Kafka、RocketMQ 等 消息队列。此方案的核心是将分布式事务拆分成本地事务进行处理,将需要分布式处理的任务通过 消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或MQ中间件,再通过业务规 则人工发起重试。 下面描述下事务的处理流程:
# 解释一下,假如AP应用程序端做一个操作,操作三个数据库(RM),这个自然涉及到了分布式事务, 多个操作数据库,首先AP要开启全局事务,在TM这里注册要操作的这几个RM资源的信息,在AP端发出 提交事务的操作,但是实际上TM会在底层执行两阶段操作。后面就是符合两阶段了。在程序端就是看到 提交事务,实际底层会完成两阶段提交。
步骤1:事务主动方处理本地事务。事务主动方在本地事务中处理业务更新操作和MQ写消息 操作。 步骤 2:事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。事务主动方 主动写消息到MQ,事务消费方接收并处理MQ中的消息。 步骤 3:事务被动方通过MQ中间件,通知事务主动方事务已处理的消息,事务主动方根据反 馈结果提交或回滚事务。失败的话根据反馈结果再次发到mq通知被动方你也回滚(就是步骤 7)。 为了数据的一致性,当流程中遇到错误需要重试,容错处理规则如下: 当步骤 1 处理出错,事务回滚,相当于什么都没发生。 当步骤 2 处理出错,由于未处理的事务消息还是保存在事务发送方,可以重试或撤销本地业 务操作。 如果事务被动方消费消息异常,需要不断重试,业务处理逻辑需要保证幂等,只处理一次。 如果是事务被动方业务上的处理失败,可以通过MQ通知事务主动方进行补偿或者事务回滚。 如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知多个事务被动方回 滚。 Saga模式(终一致性) Saga这个概念源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的一篇数据库论文Sagas ,一 个Saga事务是一个有多个短时事务组成的长时的事务。 在分布式事务场景下,我们把一个Saga分 布式事务看做是一个由多个本地事务组成的事务,每个本地事务都有一个与之对应的补偿事务。在 Saga事务的执行过程中,如果某一步执行出现异常,Saga事务会被终止,同时会调用对应的补偿 事务完成相关的恢复操作,这样保证Saga相关的本地事务要么都是执行成功,要么通过补偿恢复成 为事务执行之前的状态。(自动反向补偿机制)。Saga 事务基本协议如下: 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。有序是指的执行的一系 列小事务是有序的,因为将来失败就要按照反序去回滚,无序可能前后存在操作依赖,你的回
滚影响结果。 每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。幂等可能是失败了的小事 务不是回滚而是会重试,所以多次重试要保证幂等,不能各种重复操作多次。那就裂开。 Saga是一种补偿模式,它定义了两种补偿策略:TCC有三块,其实也是一个补偿。 向前恢复(forward recovery):对应于上面第一种执行顺序,发生失败进行重试,适用于必 须要成功的场景。 向后恢复(backward recovery):对应于上面提到的第二种执行顺序,发生错误后撤销掉之 前所有成功的子事务,使得整个 Saga 的执行结果撤销。适用于可以回滚的业务。
Saga 的执行顺序有两种,如上图: 事务正常执行完成:T1, T2, T3, ..., Tn,例如:减库存(T1),创建订单(T2),支付(T3),依次有序完 成整个事务。 事务回滚:T1, T2, ..., Tj, 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。
在 Seata 中,AT时分为两个阶段的,第一阶段,就是各个阶段本地提交操作;第二阶段会根据第一阶段 的情况决定是进行全局提交还是全局回滚操作。具体的执行流程如下:
TM 开启分布式事务,负责全局事务的begin和commit/rollback(TM 向 TC 注册全局事务记 录); RM 作为参与者,负责分支事务的执行结果上报,并且通过TC的协调进行commit/rollback(RM 向 TC 汇报资源准备状态 ); RM分支事务结束,事务一阶段结束; 根据TC 汇总事务信息,由TM发起事务提交或回滚操作; TC 通知所有 RM 提交/回滚资源,事务二阶段结束; AP是应用不说了,RM是参与的数据库,数据库自己有回滚提交功能。seata是在TM这个位置实现的调 度。
PROXY还需实践,下去开始搞自动扩充。