type
status
date
slug
summary
tags
category
icon
password
架构设计
简介
MySQL 是最流行的关系型数据库软件之一,由于其体积小、速度快、开源免费、简单易用、维护成本 低等,在集群架构中易于扩展、高可用,因此深受开发者和企业的欢迎。
Oracle和MySQL是世界市场占比最高的两种数据库。
IOE:IBM的服务器, Oracle数据库,EMC存储设备。都是有钱的公司产品采购,例如银行、电信、石 油、证券等大企业。
Oracle :垄断,有钱的大企业采用,互联网企业之外使用第一。
MySQL :互联网高速发展,互联网企业使用第一。
架构演进
本节主要介绍网站在不同的并发访问量级和数据量级下, MySQL应用架构的演变过程。
用户请求–》 应用层 –》服务层 –》存储层
架构V1.0 - 单机单库
一个简单的小型网站或者应用背后的架构可以非常简单, 数据存储只需要一个MySQL Instance就能 满足数据读取和写入需求(这里忽略掉了数据备份的实例),处于这个的阶段系统,一般会把所有 的信息存到一个MySQL Instance里面。
V1.0 瓶颈
- 数据量太大,超出一台服务器承受
- 读写操作量太大,超出一台服务器承受
- 一台服务器挂了,应用也会挂掉(可用性差)
架构V2.0 - 主从架构
V2.0架构主要解决架构V1.0下的高可用和读扩展问题,通过给Instance挂载从库解决读取的压力, 主库宕机也可以通过主从切换保障高可用。在MySQL的场景下就是通过主从结构(双主结构也属 于特殊的主从),主库抗写压力,通过从库来分担读压力,对于写少读多的应用, V2.0主从架构 完全能够胜任。
V2.0瓶颈
- 数据量太大,超出一台服务器承受
- 写操作太大,超出一台M服务器承受
架构V3.0 - 分库分表
对于V1.0和V2.0遇到写入瓶颈和存储瓶颈时,可以通过水平拆分来解决,水平拆分和垂直拆分有 较大区别,垂直拆分拆完的结果,每一个实例都是拥有全部数据的,而水平拆分之后,任何实例都 只有全量的1/n的数据。以下图所示,将Userinfo拆分为3个Sharding ,每个Sharding持有总量的 1/3数据, 3个Sharding数据的总和等于一份完整数据
数据如何路由成为一个关键问题, 一般可以采用范围拆分, List拆分、 Hash拆分等。 如何保持数据的一致性也是个难题。
架构V4.0 - 云数据库
云数据库(云计算)现在是各大IT公司内部作为节约成本的一个突破口,对于数据存储的MySQL 来说,如何让其成为一个saas ( Software as a Service )是关键点。 MySQL作为一个saas服务, 服务提供商负责解决可配置性,可扩展性,多用户存储结构设计等这些疑难问题。
体系架构
MySQL Server架构自顶向下大致可以分网络连接层、服务层、存储引擎层和系统文件层。
一、网络连接层
客户端连接器(Client Connectors):提供与MySQL服务器建立的支持。目前几乎支持所有主流 的服务端编程技术,例如常见的 Java、C、 Python、.NET等,它们通过各自API技术与MySQL建立连接。
二、服务层( MySQL Server )
服务层是MySQL Server的核心,主要包含系统管理和控制工具、连接池、 SQL接口、解析器、查询优 化器和缓存六个部分。
连接池( Connection Pool ) :负责存储和管理客户端与数据库的连接,一个线程负责管理一个 连接。
系统管理和控制工具( Management Services & Utilities ) :例如备份恢复、安全管理、集群 管理等
- SQL接口( SQL Interface ) :用于接受客户端发送的各种SQL命令,并且返回用户需要查询的结 果。比如DML、 DDL、存储过程、视图、触发器等。
- 解析器( Parser ) :负责将请求的SQL解析生成一个“解析树”。然后根据一些MySQL规则进一步 检查解析树是否合法。
- 查询优化器(Optimizer) :当“解析树”通过解析器语法检查后,将交由优化器将其转化成执行计 划,然后与存储引擎交互。
select uid,name from user where gender=1;
选取–》投影–》联接 策略
1 ) select先根据where语句进行选取,并不是查询出全部数据再过滤
2 ) select查询根据uid和name进行属性投影,并不是取出所有字段
3 )将前面选取和投影联接起来最终生成查询结果
缓存(Cache&Buffer ) : 缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,权限缓存,引擎缓存等。如果查询缓存有命中的查询结果,查询语句就可以直接去查询缓存中取数据。
三、存储引擎层( Pluggable Storage Engines)
存储引擎负责MySQL中数据的存储与提取,与底层系统文件进行交互。 MySQL存储引擎是插件式的, 服务器中的查询执行引擎通过接口与存储引擎进行通信,接口屏蔽了不同存储引擎之间的差异 。现在有 很多种存储引擎,各有各的特点,最常见的是MyISAM和InnoDB。
四、系统文件层( File System )
该层负责将数据库的数据和日志存储在文件系统之上,并完成与存储引擎的交互,是文件的物理存储 层。主要包含日志文件,数据文件,配置文件, pid 文件, socket 文件等。
- 日志文件
- 错误日志( Error log)
- 通用查询日志( General query log )
- 二进制日志( binary log ) 记录了对MySQL数据库执行的更改操作,并且记录了语句的发生时间、执行时长;但是它不 记录select、show等不修改数据库的SQL。主要用于数据库恢复和主从复制。
默认开启,
show variables like '%log_error%'
记录一般查询语句, show variables like ‘%general%’;
- 慢查询日志(Slow query log )
记录所有执行时间超时的查询SQL ,默认是10秒。
- 配置文件
用于存放MySQL所有的配置信息文件,比如my.cnf、 my.ini等。
- 数据文件
- db.opt 文件:记录这个库的默认使用的字符集和校验规则。
- frm 文件:存储与表相关的元数据( meta )信息,包括表结构的定义信息等,每一张表都会有一个frm 文件。
- MYD 文件: MyISAM 存储引擎专用,存放 MyISAM 表的数据( data) ,每一张表都会有一个.MYD 文件。
- MYI 文件: MyISAM 存储引擎专用,存放 MyISAM 表的索引相关信息,每一张 MyISAM 表对应一个.MYI 文件。
- ibd文件和 IBDATA 文件:存放 InnoDB 的数据文件(包括索引)。InnoDB 存储引擎有两种表空间方式:独享表空间和共享表空间。独享表空间使用 .ibd 文件来存放数据,且每一张 InnoDB 表对应一个 .ibd 文件。共享表空间使用 .ibdata 文件,所有表共同使用一个(或多 个,自行配置) .ibdata 文件。
- ibdata1 文件:系统表空间数据文件,存储表元数据、 Undo日志等 。
- ib_logfile0、ib_logfile1 文件: Redo log 日志文件。
- pid 文件
pid 文件是 mysqld 应用程序在 Unix/Linux 环境下的一个进程文件,和许多其他 Unix/Linux 服务 端程序一样,它存放着自己的进程 id。
- socket 文件
socket 文件也是在 Unix/Linux 环境下才有的,用户在 Unix/Linux 环境下客户端连接可以不通过 TCP/IP 网络而直接使用 Unix Socket 来连接 MySQL。
运行机制
①建立连接( Connectors&Connection Pool ),通过客户端/服务器通信协议与MySQL建立连
接。 MySQL 客户端与服务端的通信方式是 “ 半双工 ”。对于每一个 MySQL 的连接,时刻都有一个 线程状态来标识这个连接正在做什么。
通讯机制:
- 全双工:能同时发送和接收数据,例如平时打电话。
- 半双工:指的某一时刻,要么发送数据,要么接收数据,不能同时。例如早期对讲机
- 单工:只能发送数据或只能接收数据。例如单行道,HTTP
线程状态:
show processlist; //查看用户正在运行的线程信息, root用户能查看所有线程,其他用户只能看自己的
- id :线程ID ,可以使用kill xx;
- user :启动这个线程的用户
- Host :发送请求的客户端的IP和端口号
- db :当前命令在哪个库执行
- Command :该线程正在执行的操作命令
- Create DB :正在创建库操作
- Drop DB :正在删除库操作
- Execute :正在执行一个PreparedStatement
- Close Stmt :正在关闭一个PreparedStatement
- Query :正在执行一个语句
- Sleep :正在等待客户端发送语句
- Quit :正在退出
- Shutdown :正在关闭服务器
- Time :表示该线程处于当前状态的时间,单位是秒
- State :线程状态
- Updating :正在搜索匹配记录,进行修改
- Sleeping :正在等待客户端发送新请求
- Starting :正在执行请求处理
- Checking table :正在检查数据表
- Closing table : 正在将表中数据刷新到磁盘中
- Locked :被其他查询锁住了记录
- Sending Data :正在处理Select查询,同时将结果发送给客户端
- Info :一般记录线程执行的语句,默认显示前100个字符。想查看完整的使用show full processlist;
②查询缓存( Cache&Buffer),这是MySQL的一个可优化查询的地方,如果开启了查询缓存且在查询缓存过程中查询到完全相同的SQL语句,则将查询结果直接返回给客户端;如果没有开启查询 缓存或者没有查询到完全相同的 SQL 语句则会由解析器进行语法语义解析,并生成“解析树”。
- 缓存Select查询的结果和SQL语句
- 执行Select查询时,先查询缓存,判断是否存在可用的记录集,要求是否完全相同(包括参数值),这样才会匹配缓存数据命中。
- 即使开启查询缓存,以下SQL也不能缓存
- 查询语句使用SQL_NO_CACHE
- 查询的结果大于query_cache_limit设置
- 查询中有一些不确定的参数,比如now()
- show variables like ‘%query_cache%’; //查看查询缓存是否启用,空间大小,限制等
- show status like ‘Qcache%’; //查看更详细的缓存参数,可用缓存空间,缓存块,缓存多少等
进阶:
大多数情况下我会建议你不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。
1 缓存需要语句完全相等,包括参数。
2 只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此,缓存命中率不高。只有在表更新频率不高,查询语句完全一致的情况下,可以手动开启缓存,其他一律关闭。 注意:mysql8之后,取消了缓存功能。
③解析器( Parser)将客户端发送的SQL进行语法解析,生成“解析树”。预处理器根据一些MySQL规则进一步检查“解析树”是否合法,例如这里将检查数据表和数据列是否存在,还会解析名字和别 名,看看它们是否有歧义,最后生成新的“解析树”。
如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,比如下面这个语句 select 少打了开头的字母“s”。
④查询优化器( Optimizer)根据“解析树”生成最优的执行计划。 MySQL使用很多优化策略生成最 优的执行计划,可以分为两类:静态优化(编译时优化)、动态优化(运行时优化)。
- 等价变换策略
- 5=5 and a>5 改成 a > 5
- a < b and a=5 改成b>5 and a=5
- 基于联合索引,调整条件位置等
- 优化count、 min、 max等函数
- InnoDB引擎min函数只需要找索引最左边
- InnoDB引擎max函数只需要找索引最右边
- MyISAM引擎count(*) ,不需要计算,直接返回
- 提前终止查询
- 使用了limit查询,获取limit所需的数据,就不在继续遍历后面数据 limit 100 ,10
- in的优化
- MySQL对in查询,会先进行排序,再采用二分法查找数据。
⑤查询执行引擎负责执行 SQL 语句,此时查询执行引擎会根据 SQL 语句中表的存储引擎类型,以及对应的API接口与底层存储引擎缓存或者物理文件的交互,得到查询结果并返回给客户端。若开 启用查询缓存,这时会将SQL 语句和结果完整地保存到查询缓存( Cache&Buffer)中,以后若有 相同的 SQL 语句执行则直接返回结果。
- 如果开启了查询缓存,先将查询结果做缓存操作
- 返回结果过多,采用增量模式返回
存储引擎
各个引擎对比
存储引擎在MySQL的体系架构中位于第三层,负责MySQL中的数据的存储和提取,是与文件打交道的 子系统,它是根据MySQL提供的文件访问层抽象接口定制的一种文件访问机制,这种机制就叫作存储引 擎。
使用show engines命令,就可以查看当前数据库支持的引擎信息。
在5.5版本之前默认采用MyISAM存储引擎,从5.5开始采用InnoDB存储引擎。
InnoDB :支持事务,具有提交,回滚和崩溃恢复能力,事务安全
MyISAM :不支持事务和外键,访问速度快
Memory :利用内存创建表,访问速度非常快,因为数据在内存,而且默认使用Hash索引,但是 一旦关闭,数据就会丢失
Archive :归档类型引擎,仅能支持insert和select语句
Csv :以CSV文件进行数据存储,由于文件限制,所有列必须强制指定not null ,另外CSV引擎也不
支持索引和分区,适合做数据交换的中间表
BlackHole: 黑洞,只进不出,进来消失,所有插入数据都不会保存
Federated :可以访问远端MySQL数据库中的表。一个本地表,不保存数据,访问远程表内容。 MRG_MyISAM :一组MyISAM表的组合,这些MyISAM表必须结构相同, Merge表本身没有数据, 对Merge操作可以对一组MyISAM表进行操作。
3.1 InnoDB和MyISAM对比
InnoDB和MyISAM是使用MySQL时最常用的两种引擎类型,我们重点来看下两者区别。
- 事务和外键
InnoDB支持事务和外键,具有安全性和完整性,适合大量insert或update操作
MyISAM不支持事务和外键,它提供高速存储和检索,适合大量的select查询操作 锁机制
InnoDB支持行级锁,锁定指定记录。基于索引来加锁实现。
MyISAM支持表级锁,锁定整张表。
- 索引结构
InnoDB使用聚集索引(聚簇索引),索引和记录在一起存储,既缓存索引,也缓存记录。
MyISAM使用非聚集索引(非聚簇索引),索引和记录分开。
- 并发处理能力
MyISAM使用表锁,会导致写操作并发率低,读之间并不阻塞,读写阻塞。
InnoDB读写阻塞可以与隔离级别有关,可以采用多版本并发控制( MVCC)来支持高并发(读写不加锁),行锁(悲观锁)
- 存储文件
InnoDB表对应两个文件,一个.frm表结构文件,一个.ibd数据文件。 InnoDB表最大支持64TB;
MyISAM表对应三个文件,一个.frm表结构文件,一个MYD表数据文件,一个.MYI索引文件。从 MySQL5.0开始默认限制是256TB。
- 适用场景
MyISAM
- 不需要事务支持(不支持)
- 并发相对较低(锁定机制问题)
- 数据修改相对较少,以读为主
- 数据一致性要求不高
InnoDB
- 需要事务支持(具有较好的事务特性)
- 行级锁定对高并发有很好的适应能力
- 数据更新较为频繁的场景
- 数据一致性要求较高
- 硬件设备内存较大,可以利用InnoDB较好的缓存能力来提高内存利用率,减少磁盘IO
总结:
两种引擎该如何选择?
- 是否需要事务?有, InnoDB
- 是否存在并发修改?有, InnoDB
- 是否追求快速查询,且数据修改少?可以备选 MyISAM。其实可以更多的选择其他组件MongoDB
- 在绝大多数情况下,推荐使用InnoDB
扩展资料:各个存储引擎特性对比
InnoDB存储结构
从MySQL 5.5版本开始默认使用InnoDB作为引擎,它擅长处理事务,具有自动崩溃恢复的特性,在日 常开发中使用非常广泛。
下面是官方的InnoDB引擎架构图,主要分为内存结构和磁盘结构两大部分。
一、InnoDB内存结构
内存结构主要包括Buffer Pool、Change Buffer、Adaptive Hash Index和Log Buffer四大组件。
- Buffer Pool :缓冲池,简称BP。 BP以Page页为单位,默认大小16K,BP的底层采用链表数
据结构管理Page。在InnoDB访问表记录和索引时会在Page页中缓存,以后使用可以减少磁 盘IO操作,提升效率。
Page管理机制
Page根据状态可以分为三种类型:
free page : 空闲page ,未被使用
clean page :被使用page ,数据没有被修改过
dirty page :脏页,被使用page ,数据被修改过,页中数据和磁盘的数据产生了不 一致
针对上述三种page类型, InnoDB通过三种链表结构来维护和管理
- free list :表示空闲缓冲区,管理free page
- flush list :表示需要刷新到磁盘的缓冲区,管理dirty page ,内部page按修改时间
排序。脏页即存在于flush链表,也在LRU链表中,但是两种互不影响, LRU链表负 责管理page的可用性和释放,而flush链表负责管理脏页的刷盘操作。
- lru list :表示正在使用的缓冲区,管理clean page和dirty page ,缓冲区以
midpoint为基点,前面链表称为new列表区,存放经常访问的数据,占63% ;后 面的链表称为old列表区,存放使用较少数据,占37%。
- 改进型LRU算法维护
普通LRU :末尾淘汰法,新数据从链表头部加入,释放空间时从末尾淘汰
改性LRU :链表分为new和old两个部分,加入元素时并不是从表头插入,而是从中间 midpoint位置插入,如果数据很快被访问,那么page就会向new列表头部移动,如果 数据没有被访问,会逐步向old尾部移动,等待淘汰。
每当有新的page数据读取到buffer pool时, InnoDb引擎会判断是否有空闲页,是否足够,如果有就将free page从free list列表删除,放入到LRU列表中。没有空闲页,就会 根据LRU算法淘汰LRU链表默认的页,将内存空间释放分配给新的页。
- Buffer Pool配置参数
建议:将innodb_buffer_pool_size设置为总内存大小的60%-80% , innodb_buffer_pool_instances可以设置为多个,这样可以避免缓存争夺。
- Change Buffer :写缓冲区,简称CB。在进行DML操作时,如果BP没有其相应的Page数据,
并不会立刻将磁盘页加载到缓冲池,而是在CB记录缓冲变更,等未来数据被读取时,再将数 据合并恢复到BP中。
ChangeBuffer占用BufferPool空间,默认占25% ,最大允许占50% ,可以根据读写业务量来 进行调整。参数innodb_change_buffer_max_size;
当更新一条记录时,该记录在BufferPool存在,直接在BufferPool修改,一次内存操作。如 果该记录在BufferPool不存在(没有命中),会直接在ChangeBuffer进行一次内存操作,不 用再去磁盘查询数据,避免一次磁盘IO。当下次查询记录时,会先进性磁盘读取,然后再从 ChangeBuffer中读取信息合并,最终载入BufferPool中。
写缓冲区,仅适用于非唯一普通索引页,为什么?
如果在索引设置唯一性,在进行修改时, InnoDB必须要做唯一性校验,因此必须查询磁盘, 做一次IO操作。会直接将记录查询到BufferPool中,然后在缓冲池修改,不会在ChangeBuffer操作。
- Adaptive Hash Index :自适应哈希索引,用于优化对BP数据的查询。 InnoDB存储引擎会监
控对表索引的查找,如果观察到建立哈希索引可以带来速度的提升,则建立哈希索引,所以 称之为自适应。 InnoDB存储引擎会自动根据访问的频率和模式来为某些页建立哈希索引。
- Log Buffer :日志缓冲区,用来保存要写入磁盘上log文件( Redo/Undo )的数据,日志缓冲区的内容定期刷新到磁盘log文件中。日志缓冲区满时会自动将其刷新到磁盘,当遇到BLOB 或多行更新的大事务操作时,增加日志缓冲区可以节省磁盘I/O。
LogBuffer主要是用于记录InnoDB引擎日志,在DML操作时会产生Redo和Undo日志。 LogBuffer空间满了,会自动写入磁盘。可以通过将innodb_log_buffer_size参数调大,减少磁盘IO频率,将随机IO变成顺序IO
innodb_flush_log_at_trx_commit参数控制日志刷新行为,默认为1
0 :每隔1秒写日志文件和刷盘操作(写日志文件LogBuffer–>OS cache ,刷盘OScache–>磁盘文件),最多丢失1秒数据
1 :事务提交,立刻写日志文件和刷盘,数据不丢失,但是会频繁IO操作
2 :事务提交,立刻写日志文件,每隔1秒钟进行刷盘操作
随机io为什么慢?
磁头寻道+磁盘旋转
磁盘原理:https://zhuanlan.zhihu.com/p/89505052
二、 InnoDB磁盘结构
InnoDB磁盘主要包含Tablespaces,InnoDB Data Dictionary,Doublewrite Buffer、 Redo Log 和Undo Logs。
- 表空间( Tablespaces ):用于存储表结构和数据。表空间又分为系统表空间、独立表空间、
通用表空间、临时表空间、 Undo表空间等多种类型;
- 系统表空间( The System Tablespace )
包含InnoDB数据字典, Doublewrite Buffer,Change Buffer ,Undo Logs的存储区 域。系统表空间也默认包含任何用户在系统表空间创建的表数据和索引数据。系统表空 间是一个共享的表空间因为它是被多个表共享的。该空间的数据文件通过参数 innodb_data_file_path控制,默认值是ibdata1:12M:autoextend(文件名为ibdata1、 12MB、自动扩展)。
- 独立表空间( File-Per-Table Tablespaces )
默认开启,独立表空间是一个单表表空间,该表创建于自己的数据文件中,而非创建于 系统表空间中。当innodb_file_per_table选项开启时,表将被创建于表空间中。否则, innodb将被创建于系统表空间中。每个表文件表空间由一个.ibd数据文件代表,该文件 默认被创建于数据库目录中。表空间的表文件支持动态(dynamic)和压缩( commpressed )行格式。
- 通用表空间( General Tablespaces )
通用表空间为通过create tablespace语法创建的共享表空间。通用表空间可以创建于 mysql数据目录外的其他表空间,其可以容纳多张表,且其支持所有的行格式。
- 撤销表空间( Undo Tablespaces )
撤销表空间由一个或多个包含Undo日志文件组成。在MySQL 5.7版本之前Undo占用的 是System Tablespace共享区,从5.7开始将Undo从System Tablespace分离了出来。 InnoDB使用的undo表空间由innodb_undo_tablespaces配置选项控制,默认为0。参 数值为0表示使用系统表空间ibdata1;大于0表示使用undo表空间undo_001、undo_002等。
- 临时表空间(Temporary Tablespaces )
分为session temporary tablespaces 和global temporary tablespace两种。 session temporary tablespaces 存储的是用户创建的临时表和磁盘内部的临时表。 global temporary tablespace储存用户临时表的回滚段( rollback segments )。 mysql服务 器正常关闭或异常终止时,临时表空间将被移除,每次启动时会被重新创建。
- 数据字典( InnoDB Data Dictionary)
InnoDB数据字典由内部系统表组成,这些表包含用于查找表、索引和表字段等对象的元数据。元数据物理上位于InnoDB系统表空间中。由于历史原因,数据字典元数据在一定程度上 与InnoDB表元数据文件( .frm文件)中存储的信息重叠。
- 双写缓冲区( Doublewrite Buffer )
位于系统表空间,是一个存储区域。在BufferPage的page页刷新到磁盘真正的位置前,会先 将数据存在Doublewrite 缓冲区。如果在page页写入过程中出现操作系统、存储子系统或 mysqld进程崩溃, InnoDB可以在崩溃恢复期间从Doublewrite 缓冲区中找到页面的一个好备份。在大多数情况下,默认情况下启用双写缓冲区,要禁用Doublewrite 缓冲区,可以将 innodb_doublewrite设置为0。使用Doublewrite 缓冲区时建议将innodb_flush_method设 置为O_DIRECT。
MySQL的innodb_flush_method这个参数控制着innodb数据文件及redo log的打开、 刷写模式。
有三个值: fdatasync(默认),O_DSYNC,O_DIRECT。
设置O_DIRECT表示 数据文件写入操作会通知操作系统不要缓存数据,也不要用预读,直接从Innodb Buffer写到磁盘文件。
默认的fdatasync意思是先写入操作系统缓存,然后再调用fsync()函数去异步刷数据文 件与redo log的缓存信息。
- 重做日志( Redo Log)
重做日志是一种基于磁盘的数据结构,用于在崩溃恢复期间更正不完整事务写入的数据。 MySQL以循环方式写入重做日志文件,记录InnoDB中所有对Buffer Pool修改的日志。当出 现实例故障(像断电),导致数据未能更新到数据文件,则数据库重启时须redo ,重新把数 据更新到数据文件。读写事务在执行的过程中,都会不断的产生redo log。默认情况下,重做日志在磁盘上由两个名为ib_logfile0和ib_logfile1的文件物理表示。
- 撤销日志( Undo Logs )
撤消日志是在事务开始之前保存的被修改数据的备份,用于例外情况时回滚事务。撤消日志 属于逻辑日志,根据每行记录进行记录。撤消日志存在于系统表空间、撤消表空间和临时表空间中。
InnoDB线程模型
- IO Thread
在InnoDB中使用了大量的AIO ( Async IO )来做读写处理,这样可以极大提高数据库的性能。在 InnoDB1.0版本之前共有4个IO Thread ,分别是write ,read,insert buffer和log thread ,后来版本将read thread和write thread分别增大到了4个,一共有10个了。 1. readthread : 负责读取操作,将数据从磁盘加载到缓存page页。 4个
- write thread :负责写操作,将缓存脏页刷新到磁盘。 4个
- log thread :负责将日志缓冲区内容刷新到磁盘。 1个
- insert buffer thread :负责将写缓冲内容刷新到磁盘。 1个
- Purge Thread
事务提交之后,其使用的undo日志将不再需要,因此需要Purge Thread回收已经分配的undo 页。
show variables like ‘%innodb_purge_threads%’;
- Page Cleaner Thread
作用是将脏数据刷新到磁盘,脏数据刷盘后相应的redo log也就可以覆盖,即可以同步数据,又能达到redo log循环使用的目的。会调用write thread线程处理。
show variables like ‘%innodb_page_cleaners%’;
- Master Thread
Master thread是InnoDB的主线程,负责调度其他各线程,优先级最高。作用是将缓冲池中的数 据异步刷新到磁盘 ,保证数据的一致性。包含:脏页的刷新( page cleaner thread )、 undo页回收( purgethread )、 redo日志刷新( log thread )、合并写缓冲等。
内部有两个主处理,分别是每隔1秒和10秒处理。
每1秒的操作:
- 刷新日志缓冲区,刷到磁盘
- 合并写缓冲区数据,根据IO读写压力来决定是否操作
- 刷新脏页数据到磁盘,根据脏页比例达到75%才操作( innodb_max_dirty_pages_pct,innodb_io_capacity)
每10秒的操作:
- 刷新脏页数据到磁盘
- 合并写缓冲区数据
- 刷新日志缓冲区
- 删除无用的undo页
InnoDB数据文件
一、 InnoDB文件存储结构
InnoDB数据文件存储结构:
分为一个ibd数据文件–>Segment (段) –>Extent (区) –>Page (页) –>Row (行)
- Tablesapce
表空间,用于存储多个ibd数据文件,用于存储表的记录和索引。一个文件包含多个段。
- Segment
段,用于管理多个Extent ,分为数据段( Leaf node segment )、索引段( Non-leaf node segment)、回滚段( Rollback segment)。一个表至少会有两个segment ,一个管理数据,一个管理索引。每多创建一个索引,会多两个segment。
- Extent
区,一个区固定包含64个连续的页,大小为1M。当表空间不足,需要分配新的页资源,不会 一页一页分,直接分配一个区。
- Page
页,用于存储多个Row行记录,大小为16K。包含很多种页类型,比如数据页, undo页,系统页,事务数据页,大的BLOB对象页。
- Row
行,包含了记录的字段值,事务ID ( Trx id )、滚动指针( Roll pointer)、字段指针( Field pointers )等信息。
Page是文件最基本的单位,无论何种类型的page ,都是由page header ,page trailer和page body组成。如下图所示,
二、 InnoDB文件存储格式
- 通过 SHOW TABLE STATUS 命令
一般情况下,如果row_format为REDUNDANT、COMPACT ,文件格式为Antelope ;如果 row_format为DYNAMIC和COMPRESSED ,文件格式为Barracuda。
- 通过 information_schema 查看指定表的文件格式
select * from information_schema.innodb_sys_tables;
- 三、 File文件格式( File-Format )
在早期的InnoDB版本中,文件格式只有一种,随着InnoDB引擎的发展,出现了新文件格式,用于 支持新的功能。目前InnoDB只支持两种文件格式: Antelope 和 Barracuda。
- Antelope: 先前未命名的,最原始的InnoDB文件格式,它支持两种行格式: COMPACT和
REDUNDANT,MySQL 5.6及其以前版本默认格式为Antelope。
- Barracuda: 新的文件格式。它支持InnoDB的所有行格式,包括新的行格式: COMPRESSED
和 DYNAMIC。
通过innodb_file_format 配置参数可以设置InnoDB文件格式,之前默认值为Antelope,5.7版本 开始改为Barracuda。
- 四、 Row行格式( 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_prefix参数控制。
- COMPRESSED 行格式
COMPRESSED行格式提供与DYNAMIC行格式相同的存储特性和功能,但增加了对表和索引 数据压缩的支持。
在创建表和索引时,文件格式都被用于每个InnoDB表数据文件(其名称与*.ibd匹配)。修改文件 格式的方法是重新创建表及其索引,最简单方法是对要修改的每个表使用以下命令:
ALTER TABLE 表名 ROW_FORMAT=格式类型 ;
Undo Log(重点)
一、Undo Log介绍
Undo :意为撤销或取消,以撤销操作为目的,返回指定某个状态的操作。
Undo Log :数据库事务开始之前,会将要修改的记录存放到 Undo 日志里,当事务回滚时或者数据库崩溃时,可以利用 Undo 日志,撤销未提交事务对数据库产生的影响。
Undo Log产生和销毁: Undo Log在事务开始前产生;事务在提交时,并不会立刻删除undo log,innodb会将该事务对应的undo log放入到删除列表中,后面会通过后台线程purge thread进 行回收处理。 Undo Log属于逻辑日志,记录一个变化过程。例如执行一个delete,undolog会记 录一个insert ;执行一个update ,undolog会记录一个相反的update。
Undo Log存储: undo log采用段的方式管理和记录。在innodb数据文件中包含一种rollback segment回滚段,内部包含1024个undo log segment。可以通过下面一组参数来控制Undo log存储。
show variables like '%innodb_undo%';
二、Undo Log作用
- 实现事务的原子性
Undo Log 是为了实现事务的原子性而出现的产物。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句, MySQL 可以利用 Undo Log 中的备份将数据恢复到事务开始之前的状态。
- 实现多版本并发控制( MVCC )
Undo Log 在 MySQL InnoDB 存储引擎中用来实现多版本并发控制。事务未提交之前, Undo Log 保存了未提交之前的版本数据, Undo Log 中的数据可作为数据旧版本快照供其他并发事务进行快照读。
事务A手动开启事务,执行更新操作,首先会把更新命中的数据备份到 Undo Buffer 中。
事务B手动开启事务,执行查询操作,会读取 Undo 日志数据返回,进行快照读
RedoLog(重点)
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
Redo Log和Binlog是MySQL日志系统中非常重要的两种机制,也有很多相似之处,下面介绍下两者细节和区别。
一、Redo Log日志
- Redo Log介绍
Redo :顾名思义就是重做。以恢复操作为目的,在数据库发生意外时重现操作。
Redo Log :指事务中修改的任何数据,将最新的数据备份存储的位置( Redo Log),被称为重做日志。
Redo Log 的生成和释放:随着事务操作的执行,就会生成Redo Log ,在事务提交时会将产生 Redo Log写入Log Buffer,并不是随着事务的提交就立刻写入磁盘文件。等事务操作的脏页写入到磁盘之后, Redo Log 的使命也就完成了, Redo Log占用的空间就可以重用(被覆盖写入)。
- Redo Log工作原理
Redo Log 是为了实现事务的持久性而出现的产物。防止在发生故障的时间点,尚有脏页未写入表 的 IBD 文件中,在重启 MySQL 服务的时候,根据 Redo Log 进行重做,从而达到事务的未入磁盘 数据进行持久化这一特性。
Redo Log写入机制
Redo Log 文件内容是以顺序循环的方式写入文件,写满时则回溯到第一个文件,进行覆盖写。
如图所示:
- write pos 是当前记录的位置,一边写一边后移,写到最后一个文件末尾后就回到 0 号文件开头;
- checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件;
write pos 和 checkpoint 之间还空着的部分,可以用来记录新的操作,其他的空间是写入的redo log内容。如果 write pos 追上 checkpoint ,表示写满,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
- Redo Log相关配置参数
每个InnoDB存储引擎至少有1个重做日志文件组(group),每个文件组至少有2个重做日志文 件,默认为ib_logfile0和ib_logfile1。可以通过下面一组参数控制Redo Log存储:
Redo Buffer 持久化到 Redo Log 的策略,可通过 Innodb_flush_log_at_trx_commit 设置:
0 :每秒提交 Redo buffer ->OS cache -> flush cache to disk ,可能丢失一秒内的事务数据。由后台Master线程每隔 1秒执行一次操作。
1 :(默认值):每次事务提交执行 Redo Buffer -> OS cache -> flush cache to disk ,最安全,性能最差的方式。
2 :每次事务提交执行 Redo Buffer -> OS cache ,然后由后台Master线程再每隔1秒执行OS cache -> flush cache to disk 的操作。
一般建议选择取值2 ,因为 MySQL 挂了数据没有损失,整个服务器挂了才会损失1秒的事务提交数 据。
BinLog(重点)
- Binlog记录模式
Redo Log 是属于InnoDB引擎所特有的日志,而MySQL Server也有自己的日志,即 Binary log (二进制日志),简称Binlog。 Binlog是记录所有数据库表结构变更以及表数据修改的二进制日志,不会记录SELECT和SHOW这类操作。 Binlog日志是以事件形式记录,还包含语句所执行的消耗时间。开启Binlog日志有以下两个最重要的使用场景。
- 主从复制:在主库中开启Binlog功能,这样主库就可以把Binlog传递给从库,从库拿到Binlog后实现数据恢复达到主从数据一致性。
- 数据恢复:通过mysqlbinlog工具来恢复数据。
Binlog文件名默认为“主机名_binlog-序列号”格式,例如oak_binlog-000001 ,也可以在配置文件 中指定名称。文件记录模式有STATEMENT、 ROW和MIXED三种,具体含义如下。
- ROW( row-based replication, RBR):日志中会记录每一行数据被修改的情况,然后在slave端对相同的数据进行修改。
优点:能清楚记录每一个行数据的修改细节,能完全实现主从数据同步和数据的恢复。
缺点:批量操作,会产生大量的日志,尤其是alter table会让日暴涨。
- STATMENT ( statement-based replication, SBR):每一条被修改数据的SQL都会记录到master的Binlog中, slave在复制的时候SQL进程会解析成和原来master端执行过的相同的 SQL再次执行。简称SQL语句复制。
优点:日志量小,减少磁盘IO ,提升存储和恢复速度
缺点:在某些情况下会导致主从数据不一致,比如last_insert_id()、random等函数。
- MIXED ( mixed-based replication, MBR):以上两种模式的混合使用,一般会使用STATEMENT模式保存binlog ,对于STATEMENT模式无法复制的操作使用ROW模式保存 binlog,MySQL会根据执行的SQL语句选择写入模式。比如使用随机数函数这种statement形式无法保持幂等的就必须使用row格式存储
- Binlog文件结构
MySQL的binlog文件中记录的是对数据库的各种修改操作,用来表示修改操作的数据结构是Log event。不同的修改操作对应的不同的log event。比较常用的log event有: Query event、 Row event、Xid event等。 binlog文件的内容就是各种Log event的集合。
Binlog文件中Log event结构如下图所示:
- Binlog写入机制
- 根据记录模式和操作触发event事件生成logevent (事件触发执行机制)
- 将事务执行过程中产生log event写入缓冲区,每个事务线程都有一个缓冲区
Log Event保存在一个binlog_cache_mngr数据结构中,在该结构中有两个缓冲区,一个是 stmt_cache ,用于存放不支持事务的信息;另一个是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 binlog events命令
- 使用mysqlbinlog 命令
- 使用 binlog 恢复数据
mysqldump :定期(每周/每天)全部备份数据库数据。 mysqlbinlog可以做增量备份和恢复操作。恢复数据比较久远或者需要全库备份的时候需要使用全量+增量备份的形式。
- 删除Binlog文件
可以通过设置expire_logs_days参数来启动自动清理功能。默认值为0表示没启用。设置为1表示超 出1天binlog文件会自动删除掉。
- Redo Log和Binlog区别
- Redo Log是属于InnoDB引擎功能, Binlog是属于MySQL Server自带功能,并且是以二进制文件记录。
- Redo Log属于物理日志,记录该数据页更新状态内容, Binlog是逻辑日志,记录更新过程。
- Redo Log日志是循环写,日志空间大小是固定, Binlog是追加写入,写完一个写下一个,不会覆盖使用。
- Redo Log作为服务器异常宕机后事务数据自动恢复使用, Binlog可以作为主从复制和数据恢复使用。 Binlog没有自动crash-safe能力。
更新语句的执行流程
准备一个测试表
一个简单的update语句
与查询流程相同都会经过下图的步骤
与查询流程不一样的是,更新流程还涉及两个重要的日志模块,redo log(重做日志)和 binlog(归档日志)
执行流程:
- 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
- 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
图中浅色框表示是在 InnoDB 内部执行的,深色框表示是在执行器中执行的。
为什么最后三步需要用到“两阶段提交”?
保证binlog和redo log的一致性
宕机恢复的时候的大致步骤如下 Step1. 按顺序扫描redolog,如果redolog中的事务既有prepare标识,又有commit标识,就直接提交(复制redolog disk中的数据页到磁盘数据页) Step2 .如果redolog事务只有prepare标识,没有commit标识,则说明当前事务在commit阶段crash了,binlog中当前事务是否完整未可知,此时拿着redolog中当前事务的XID(redolog和binlog中事务落盘的标识),去查看binlog中是否存在此XID a. 如果binlog中有当前事务的XID,则提交事务(复制redolog disk中的数据页到磁盘数据页) b. 如果binlog中没有当前事务的XID,则回滚事务(使用undolog来删除redolog中的对应事务)
索引(重中之重点)
索引的出现其实就是为了提高数据查询的效率,就像书的目录一样
1. 索引类型
索引可以提升查询速度,会影响where查询,以及order by排序。
MySQL索引类型如下:
从索引存储结构划分: B Tree索引、 Hash索引、 FULLTEXT全文索引、 R 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…);
ALTER TABLE tablename ADD INDEX [索引的名字] (字段名1 ,字段名2…);
CREATE TABLE tablename ( […], INDEX [索引的名字] (字段名1 ,字段名2…) );
复合索引使用注意事项:
复合索引字段是有顺序的,在查询使用时要按照索引字段的顺序使用。
例如select * from user where name=xx and age=xx ,匹配(name,age)组合索引,不匹配(age)。
何时使用复合索引,要根据where条件建索引,注意不要过多使用索引,过多使用会对更新操作效 率有很大影响。
如果表已经建立了(col1,col2) ,就没有必要再单独建立( col1 );如果现在有(col1)索引,如果查 询需要col1和col2条件,可以建立(col1,col2)复合索引,对于查询有一定提高。
1.5 全文索引
查询操作在数据量比较少时,可以使用like模糊查询,但是对于大量的文本数据检索,效率很低。如果 使用全文索引,查询速度会比like快很多倍。在MySQL 5.6 以前的版本,只有MyISAM存储引擎支持全文索引,从MySQL 5.6开始MyISAM和InnoDB存储引擎均支持。
创建全文索引的方法如下:
CREATE FULLTEXT INDEX <索引的名字> ON tablename (字段名);
ALTER TABLE tablename ADD FULLTEXT [索引的名字] (字段名);
CREATE TABLE tablename ( […], FULLTEXT KEY [索引的名字] (字段名) ;
和常用的like模糊查询不同,全文索引有自己的语法格式,使用 match 和 against 关键字,比如
select * from userwhere match(name) against('aaa');
全文索引使用注意事项:
- 全文索引必须在字符串、文本字段上建立。
- 全文索引字段值必须在最小字符和最大字符之间的才会有效。( innodb:3-84 ;myisam:4-84 )
- 全文索引字段值要进行切词处理,按syntax字符进行切割,例如b+aaa ,切分成b和aaa
- 全文索引匹配查询,默认使用的是等值匹配,例如a匹配a ,不会匹配ab,ac。如果想匹配可以在布尔模式下搜索a*
select * from userwhere match(name) against(’a*’ in boolean mode);
2. 索引原理
MySQL官方对索引定义:是存储引擎用于快速查找记录的一种数据结构。需要额外开辟空间和数据维护 工作。
索引是物理数据页存储,在数据文件中( InnoDB ,ibd文件),利用数据页(page)存储。 索引可以加快检索速度,但是同时也会降低增删改操作速度,索引维护需要代价。
索引涉及的理论知识:二分查找法、 Hash和B+Tree。
2.1 二分查找法
二分查找法也叫作折半查找法,它是在有序数组中查找指定数据的搜索算法。它的优点是等值查询、范 围查询性能优秀,缺点是更新数据、新增数据、删除数据维护成本高。
首先定位left和right两个指针计算(left+right)/2
判断除2后索引位置值与目标值的大小比对
索引位置值大于目标值就-1 ,right移动;如果小于目标值就+1 ,left移动
限制:有序有界有下标
举个例子,下面的有序数组有17 个值,查找的目标值是7 ,过程如下:
第一次查找
第二次查找
第三次查找
第四次查找
2.2 Hash结构
Hash底层实现是由Hash表来实现的,是根据键值 <key,value> 存储数据的结构。非常适合根据key查找 value值,也就是单个key查询,或者说等值查询。其结构如下所示:
从上面结构可以看出, Hash索引可以方便的提供等值查询,但是对于范围查询就需要全表扫描了。 Hash索引在MySQL 中Hash结构主要应用在Memory原生的Hash索引 、 InnoDB 自适应哈希索引。
InnoDB提供的自适应哈希索引功能强大,接下来重点描述下InnoDB 自适应哈希索引。
InnoDB自适应哈希索引是为了提升查询效率, InnoDB存储引擎会监控表上各个索引页的查询,当 InnoDB注意到某些索引值访问非常频繁时,会在内存中基于B+Tree索引再创建一个哈希索引,使得内 存中的 B+Tree 索引具备哈希索引的功能,即能够快速定值访问频繁访问的索引页。
InnoDB自适应哈希索引:在使用Hash索引访问时,一次性查找就能定位数据,等值查询效率要优于 B+Tree。
自适应哈希索引的建立使得InnoDB存储引擎能自动根据索引页访问的频率和模式自动地为某些热点页 建立哈希索引来加速访问。另外InnoDB自适应哈希索引的功能,用户只能选择开启或关闭功能,无法 进行人工干涉。
show engine innodb status
show variables like ‘%innodb_adaptive%’;
2.3 B+Tree结构(重点)
MySQL数据库索引采用的是B+Tree结构,在B-Tree结构上做了优化改造。
- B-Tree结构
- 索引值和data数据分布在整棵树结构中
- 每个节点可以存放多个索引值及对应的data数据
- 树节点中的多个索引值从左到右升序排列
B树的搜索:从根节点开始,对节点内的索引值序列采用二分法查找,如果命中就结束查找。没有 命中会进入子节点重复查找过程,直到所对应的的节点指针为空,或已经是叶子节点了才结束。
- B+Tree结构
- 非叶子节点不存储data数据,只存储索引值,这样便于存储更多的索引值
- 叶子节点包含了所有的索引值和data数据
- 叶子节点用指针连接,提高区间的访问性能
相比B树, B+树进行范围查找时,只需要查找定位两个节点的索引值,然后利用叶子节点的指针进 行遍历即可。而B树需要遍历范围内所有的节点和数据,显然B+Tree效率高。
为什么数据库存储使用b+树 而不是二叉树?
因为二叉树树高过高,每次查询都需要访问过多节点,即访问数据块过多造成磁盘IO过多 影响性能。 b+树的一次随机io能拿出更多的数据。
N叉数的N是多少?
以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200。MySql默认一个page为16K,一个整数(bigint)字段索引的长度为 8B,另外每个索引还跟着6B的指向其子树的指针;所以16K/14B ≈ 1170。
这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。其实树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。
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 SELECT * from user WHERE id < 3;
EXPLAIN 命令的输出内容大致如下:
- select_type
表示查询的类型。常用的值如下: * SIMPLE : 表示查询语句不包含子查询或union
- PRIMARY :表示此查询是最外层的查询
- UNION :表示此查询是UNION的第二个或后续的查询
- DEPENDENT UNION :UNION中的第二个或后续的查询语句,使用了外面查询结果
- UNION RESULT :UNION的结果
- SUBQUERY:SELECT子查询语句
- DEPENDENT SUBQUERY:SELECT子查询语句依赖外层查询的结果。
最常见的查询类型是SIMPLE ,表示我们的查询没有子查询也没用到UNION查询。
- type
表示存储引擎查询数据时采用的方式。比较重要的一个属性,通过它可以判断出查询是全表扫描还 是基于索引的部分扫描。常用属性值如下,从上至下效率依次增强。
- ALL :表示全表扫描,性能最差。
- index :表示基于索引的全表扫描,先扫描索引再扫描全表数据。
- range :表示使用索引范围查询。使用>、>=、<、<=、in等等。
- ref :表示使用非唯一索引进行单值查询。
- eq_ref :一般情况下出现在多表join查询,表示前面表的每一个记录,都只能匹配后面表的一行结果。
- const :表示使用主键或唯一索引做等值查询,常量查询。
- NULL :表示不用访问表,速度最快。
- possible_keys
表示查询时能够使用到的索引。注意并不一定会真正使用,显示的是索引名称。
- key
表示查询时真正使用到的索引,显示的是索引名称。
- rows
MySQL查询优化器会根据统计信息,估算SQL要查询到结果需要扫描多少行记录。原则上rows是 越少效率越高,可以直观的了解到SQL效率高低。
- key_len
表示查询使用了索引的字节数量。可以判断是否全部使用了组合索引。
key_len的计算规则如下:
- 字符串类型
字符串长度跟字符集有关: 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 ,则没有此项。
- Extra
Extra表示很多额外的信息,各种操作会在Extra提示相关信息,常见几种如下:
- Using where
索引(age,name) select salary,age name from t;进行回表
表示查询需要通过索引回表查询数据。
- Using index
索引(age,name) select age name from t;可以使用覆盖索引
表示查询需要通过索引,索引就可以满足所需数据。
- Using filesort
表示查询出来的结果需要额外排序,数据量小在内存,大的话在磁盘,因此有Using filesort 建议优化。
- Using temprorary
查询使用到了临时表,一般出现于去重、分组等操作。
3.2 回表查询
在之前介绍过, InnoDB索引有聚簇索引和辅助索引。聚簇索引的叶子节点存储行记录, InnoDB必须要 有,且只有一个。辅助索引的叶子节点存储的是主键值和索引字段值,通过辅助索引无法直接定位行记 录,通常情况下,需要扫码两遍索引树。先通过辅助索引定位主键值,然后再通过聚簇索引定位行记录,这就叫做回表查询 ,它的性能比扫一遍索引树低。
总结:通过索引查询主键值,然后再去聚簇索引查询记录信息
3.3 覆盖索引
在SQL-Server官网的介绍如下:
在MySQL官网,类似的说法出现在explain查询计划优化章节,即explain的输出结果Extra字段为Using index时,能够触发索引覆盖。
不管是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’; //不起作用
3.6 NULL查询
面试题:如果MySQL表的某一列含有NULL值,那么包含该列的索引是否有效?
对MySQL来说, NULL是一个特殊的值,从概念上讲, NULL意味着“一个未知值” ,它的处理方式与其他 值有些不同。比如:不能使用=,<,>这样的运算符,对NULL做算术运算的结果都是NULL,count时 不会包括NULL行等, NULL比空字符串需要更多的存储空间等。
“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.”
NULL列需要增加额外空间来记录其值是否为NULL。对于MyISAM表,每一个空列额外占用一位,四舍 五入到最接近的字节。
虽然MySQL可以在含有NULL的列上使用索引,但NULL和其他数据还是有区别的,不建议列上允许为 NULL。最好设置NOT NULL ,并给一个默认值,比如0和 ‘’ 空字符串等,如果是datetime类型,也可以 设置系统当前时间或某个固定的特殊值,例如’1970-01-01 00:00:00’。
3.7 索引与排序
MySQL查询支持filesort和index两种方式的排序, filesort是先把结果查出,然后在缓存或磁盘进行排序 操作,效率较低。使用index是指利用索引自动实现排序,不需另做排序操作,效率会比较高。
filesort有两种排序算法:双路排序和单路排序。
双路排序:需要两次磁盘扫描读取,最终得到用户数据。第一次将排序字段读取出来,然后排序;第二次去读取其他字段数据。
单路排序:从磁盘查询所需的所有列数据,然后在内存排序将结果返回。如果查询数据超出缓存sort_buffer ,会导致多次磁盘读取操作,并创建临时表,最后产生了多次IO ,反而会增加负担。解决方案:少使用select * ;增加sort_buffer_size容量和max_length_for_sort_data容量。
如果我们Explain分析SQL ,结果中Extra属性显示Using filesort ,表示使用了filesort排序方式,需要优化。如果Extra属性显示Using index时,表示覆盖索引,也表示所有操作在索引上完成,也可以使用 index排序方式,建议大家尽可能采用覆盖索引。
以下几种情况,会使用index方式的排序。
- ORDER BY 子句索引列组合满足索引最左前列
explain select id from user order by id; //对应(id)、 (id,name)索引有效
- WHERE子句+ORDER BY子句索引列组合满足索引最左前列
explain select id from user where age=18 order by name; //对应(age,name)索引
以下几种情况,会使用filesort方式的排序。
- 对索引列同时使用了ASC和DESC
explain select id from user order by age asc,name desc; //对应(age,name)索引
- WHERE子句和ORDER BY子句满足最左前缀,但where子句使用了范围查询(例如>、<、in等)
explain select id from user where age>10 order by name; //对应(age,name)索引
- ORDER BY或者WHERE+ORDER BY索引列没有满足索引最左前列
explain select id from user order by name; //对应(age,name)索引
- 使用了不同的索引, MySQL每次只采用一个索引, ORDER BY涉及了两个索引
explain select id from user order by name,age; //对应(name)、 (age)两个索引
- WHERE子句与ORDER BY子句,使用了不同的索引
explain select id from user where name=‘tom’ order by age; //对应 (name)、 (age)索引
- WHERE子句或者ORDER BY子句中索引列使用了表达式,包括函数表达式
explain select id from user order by abs(age); //对应(age)索引
TODO—
3.8 主键选择
推荐小的自增数字。 因为有序,占用空间小。
B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。
以上面这个图为例,如果插入新的行 ID 值为 700,则只需要在 R5 的记录后面插入一个新记录。
如果新插入的 ID 值为 400,就相对麻烦了,需要逻辑上挪动后面的数据,空出位置。而更糟的情况是,如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂
除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。
当然有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。
4.查询优化
4.1 慢查询定位
- 开启慢查询日志
查看 MySQL 数据库是否开启了慢查询日志和慢查询日志文件的存储位置的命令如下:
通过如下命令开启慢查询日志:
long_query_time :指定慢查询的阀值,单位秒。如果SQL执行时间超过阀值,就属于慢查询
记录到日志文件中。
log_queries_not_using_indexes :表示会记录没有使用索引的查询SQL。前提是slow_query_log 的值为ON ,否则不会奏效。
- 查看慢查询日志
- 文本方式查看
直接使用文本编辑器打开slow.log日志即可。
time :日志记录的时间
User@Host :执行的用户及主机
Query_time :执行的时间
Lock_time :锁表时间
Rows_sent :发送给请求方的记录数,结果数量
Rows_examined :语句扫描的记录条数
SET timestamp :语句执行的时间点
select…. :执行的具体的SQL语句
- 使用mysqldumpslow查看
4.2 慢查询优化
- 索引和慢查询
- 如何判断是否为慢查询?
MySQL判断一条语句是否为慢查询语句,主要依据SQL语句的执行时间,它把当前语句的执 行时间跟 long_query_time 参数做比较,如果语句的执行时间 > long_query_time ,就会把 这条执行语句记录到慢查询日志里面。 long_query_time 参数的默认值是 10s ,该参数值可 以根据自己的业务需要进行调整。
- 如何判断是否应用了索引?
SQL语句是否使用了索引,可根据SQL语句执行过程中有没有用到表的索引,可通过 explain 命令分析查看,检查结果中的 key 值,是否为NULL。
- 应用了索引是否一定快?
下面我们来看看下面语句的 explain 的结果,你觉得这条语句有用上索引吗?比如
select * from user where id>0;
虽然使用了索引,但是还是从主键索引的最左边的叶节点开始向右扫描整个索引树,进行了 全表扫描,此时索引就失去了意义。
而像
select * from user where id = 2;
这样的语句,才是我们平时说的使用了索引。它表示 的意思是,我们使用了索引的快速搜索功能,并且有效地减少了扫描行数。查询是否使用索引,只是表示一个SQL语句的执行过程;而是否为慢查询,是由它执行的时间决定 的,也就是说是否使用了索引和是否是慢查询两者之间没有必然的联系。
我们在使用索引时,不要只关注是否起作用,应该关心索引是否减少了查询扫描的数据行数,如果 扫描行数减少了,效率才会得到提升。对于一个大表,不止要创建索引,还要考虑索引过滤性,过 滤性好,执行速度才会快。
- 提高索引过滤性
假如有一个5000万记录的用户表,通过sex=’男’索引过滤后,还需要定位3000万, SQL执行速度也 不会很快。其实这个问题涉及到索引的过滤性,比如1万条记录利用索引过滤后定位10条、100条、1000条,那他们过滤性是不同的。索引过滤性与索引字段、表的数据量、表设计结构都有关系。
- 下面我们看一个案例:
表: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 '张%'; (全表扫描)
- 优化1
alter table student add index(name); //追加name索引
- 优化2
alter table student add index(age,name); //追加age,name索引
- 优化3
- 慢查询原因总结
- 全表扫描: explain分析type属性all
- 全索引扫描: explain分析type属性index
- 索引过滤性不好:靠索引字段选型、数据量和状态、表设计
- 频繁的回表查询开销:尽量少用select * ,使用覆盖索引
4.3 分页查询优化
一般性分页
般的分页查询使用简单的 limit 子句就可以实现。 limit格式如下:
SELECT * FROM 表名 LIMIT [offset,] rows
- 第一个参数指定第一个返回记录行的偏移量,注意从0开始;
- 第二个参数指定返回记录行的最大数目;
- 如果只给定一个参数,它表示返回最大的记录行数目;
思考1 :如果偏移量固定,返回记录量对执行时间有什么影响?
结果:在查询记录时,返回记录量低于100条,查询时间基本没有变化,差距不大。随着查询记录 量越大,所花费的时间也会越来越多。
思考2 :如果查询偏移量变化,返回记录数固定对执行时间有什么影响?
结果:在查询记录时,如果查询记录量相同,偏移量超过100后就开始随着偏移量增大,查询时间 急剧的增加。(这种分页查询机制,每次都会从数据库第一条记录开始扫描,越往后查询越慢,而 且查询的数据越多,也会拖慢总查询速度。)
分页优化方案
第一步:利用覆盖索引优化
select * from user limit 10000,100;
select id from user limit 10000,100;
第二步:利用子查询优化
select * from user limit 10000,100;
select * from user where id>= (select id from user limit 10000,1) limit 100;
原因:使用了id做主键比较(id>=) ,并且子查询使用了覆盖索引进行优化。
5.最佳实践
5.1 阿里开发规范
https://github.com/alibaba/p3c/tree/master/p3c-gitbook/MySQL%E6%95%B0%E6%8D%AE%E5%BA%93
- 【强制】业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。 说明:不要以为唯一索引影响了insert速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
- 【强制】超过三个表禁止join。需要join的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引。 说明:即使双表join也要注意表索引、SQL性能。
- 【强制】在varchar字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。 说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为20的索引,区分度会高达90%以上,可以使用count(distinct left(列名, 索引长度))/count(*)的区分度来确定。
- 【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。 说明:索引文件具有B-Tree的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
- 【推荐】如果有order by的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现file_sort的情况,影响查询性能。 正例:where a=? and b=? order by c; 索引:a_b_c 反例:索引中有范围查找,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b; 索引a_b无法排序。
- 【推荐】利用覆盖索引来进行查询操作,避免回表。 说明:如果一本书需要知道第11章是什么标题,会翻开第11章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。 正例:能够建立索引的种类分为主键索引、唯一索引、普通索引三种,而覆盖索引只是一种查询的一种效果,用explain的结果,extra列会出现:using index。
- 【推荐】利用延迟关联或者子查询优化超多分页场景。 说明:MySQL并不是跳过offset行,而是取offset+N行,然后返回放弃前offset行,返回N行,那当offset特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写。 正例:先快速定位需要获取的id段,然后再关联:
- 【推荐】 SQL性能优化的目标:至少要达到 range 级别,要求是ref级别,如果可以是consts最好。 说明: 1)consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。 2)ref 指的是使用普通的索引(normal index)。 3)range 对索引进行范围检索。 反例:explain表的结果,type=index,索引物理文件全扫描,速度非常慢,这个index级别比较range还低,与全表扫描是小巫见大巫。
- 【推荐】建组合索引的时候,区分度最高的在最左边。 正例:如果where a=? and b=? ,a列的几乎接近于唯一值,那么只需要单建idx_a索引即可。 说明:存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where a>? and b=? 那么即使a的区分度更高,也必须把b放在索引的最前列。
- 【推荐】防止因字段类型不同造成的隐式转换,导致索引失效。
- 【参考】创建索引时避免有如下极端误解: 1)宁滥勿缺。认为一个查询就需要建一个索引。 2)宁缺勿滥。认为索引会消耗空间、严重拖慢更新和新增速度。 3)抵制惟一索引。认为业务的惟一性一律需要在应用层通过“先查后插”方式解决。
5.2 普通索引和唯一索引的选择
读性能:
对于普通索引来说,查找到满足条件的第一个记录 ,需要查找下一个记录,直到碰到第一个不满足条件的记录。
对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
InnoDB 的数据是按数据页为单位来读写的。也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。在 InnoDB 中,每个数据页的大小默认是 16KB。所以唯一索引读的性能优势微乎其微。
写性能:
对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。
比如,要插入(k,name) (4,400) 这个记录,就要先判断现在表中是否已经存在 k=4 的记录,而这必须要将数据页读入内存才能判断。
如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。
因此,唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。
如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的。
第一种情况是,这个记录要更新的目标页在内存中。这时,InnoDB 的处理流程如下:
- 对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;
- 对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。
这样看来,普通索引和唯一索引对更新语句性能影响的差别,只是一个判断,只会耗费微小的 CPU 时间。但这不是我们关注的重点。
第二种情况是,这个记录要更新的目标页不在内存中。这时,InnoDB 的处理流程如下:
- 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
- 对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。将写磁盘操作变成写内存操作
将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。
结论:
在实际使用中,普通索引和 change buffer 的配合使用,对于数据量大的表的更新优化还是很明显的。
这两类索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。所以尽量选择普通索引。
问题:
5.2说保证唯一要不使用唯一索引,5.1说要使用。如何选择?
性能和逻辑复杂性的权衡。
在需要唯一键的场景下,我倾向于使用唯一索引。性能问题可能永远不会出现,但是代码复杂性带来的维护成本是看得见摸得着的。
锁(重点)
1. 锁分类
在 MySQL中锁有很多不同的分类。
1.1 从操作的粒度可分为表级锁、行级锁和页级锁。
- 表级锁:每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、 InnoDB、 BDB 等存储引擎中。
- 行级锁:每次操作锁住一行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB 存储引擎中。
- 页级锁:每次锁定相邻的一组记录,锁定粒度界于表锁和行锁之间,开销和加锁时间界于表锁和行锁之间,并发度一般。应用在BDB 存储引擎中。
1.2 从操作的类型可分为读锁和写锁。
- 读锁( S锁):共享锁,针对同一份数据,多个读操作可以同时进行而不会互相影响。
- 写锁(X锁):排他锁,当前写操作没有完成前,它会阻断其他写锁和读锁。
IS锁、 IX锁:意向读锁、意向写锁,属于表级锁, S和X主要针对行级锁。在对表记录添加S或X锁之 前,会先对表添加IS或IX锁。
S锁:事务A对记录添加了S锁,可以对记录进行读操作,不能做修改,其他事务可以对该记录追加 S锁,但是不能追加X锁,需要追加X锁,需要等记录的S锁全部释放。
X锁:事务A对记录添加了X锁,可以对记录进行读和修改操作,其他事务不能对记录做读和修改操 作。
1.3 从操作的性能可分为乐观锁和悲观锁。
- 乐观锁:一般的实现方式是对记录数据版本进行比对,在数据更新提交的时候才会进行冲突检测,如果发现冲突了,则提示错误信息。
- 悲观锁:在对一条数据修改的时候,为了避免同时被其他人修改,在修改数据之前先锁定,再修改的控制方式。共享锁和排他锁是悲观锁的不同实现,但都属于悲观锁范畴。
2. 行锁原理
在InnoDB引擎中,我们可以使用行锁和表锁,其中行锁又分为共享锁和排他锁。 ****InnoDB行锁是通过对索引数据页上的记录加锁实现的 ,主要实现算法有 3 种: Record Lock、Gap Lock 和 Next-key Lock。
RecordLock锁:锁定单个行记录的锁。(记录锁, RC、 RR隔离级别都支持)
GapLock锁:间隙锁,锁定索引记录间隙,确保索引记录的间隙不变。(范围锁, RR隔离级别才支持) 前开后开区间(10,20)
Next-key Lock 锁:记录锁和间隙锁组合,同时锁住数据,并且锁住数据前后范围。(记录锁+范围锁, RR隔离级别支持) 前开后闭区间(10,20]
在RR隔离级别, InnoDB对于记录加锁行为都是先采用Next-Key Lock ,但是当SQL操作含有唯一索引时, Innodb会对Next-Key Lock进行优化,降级为RecordLock ,仅锁住索引本身而非范围。
1 ) select … from 语句: InnoDB引擎采用MVCC机制实现非阻塞读,所以对于普通的select语句, InnoDB不加锁
2 ) select … from lock in share mode语句:追加了共享锁, InnoDB会使用Next-Key Lock锁进行处 理,如果扫描发现唯一索引,可以降级为RecordLock锁。
3 ) select … from for update语句:追加了排他锁, InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
4 ) update … where 语句: InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
5 ) delete … where 语句: InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
6 ) insert语句: InnoDB会在将要插入的那一行设置一个排他的RecordLock锁。
下面以“update t1 set name=‘XX’ where id=10”操作为例,举例子分析下 InnoDB 对不同索引的加锁行 为,以RR隔离级别为例。
- 主键加锁
加锁行为:仅在id=10的主键索引记录上加X锁。
- 唯一键加锁
加锁行为:现在唯一索引id上加X锁,然后在id=10的主键索引记录上加X锁。
- 非唯一键加锁
加锁行为:对满足id=10条件的记录和主键分别加X锁,然后在(6,c)-(10,b)、(10,b)-(10,d)、(10,d)- (11,f)范围分别加Gap Lock。
- 无索引加锁
加锁行为:表里所有行和间隙都会加X锁。(当没有索引时,会导致全表锁定,因为InnoDB引擎 锁机制是基于索引实现的记录锁定)。
3. 悲观锁
悲观锁( Pessimistic Locking),是指在数据处理过程,将数据处于锁定状态,一般使用数据库的锁机 制实现。从广义上来讲,前面提到的行锁、表锁、读锁、写锁、共享锁、排他锁等,这些都属于悲观锁范畴。
- 表级锁
表级锁每次操作都锁住整张表,并发度最低。常用命令如下:
手动增加表锁
查看表上加过的锁
删除表锁
表级读锁:当前表追加read锁,当前连接和其他的连接都可以读操作;但是当前连接增删改操作 会报错,其他连接增删改会被阻塞。
表级写锁:当前表追加write锁,当前连接可以对表做增删改查操作,其他连接对该表所有操作都 被阻塞(包括查询)。
总结:表级读锁会阻塞写操作,但是不会阻塞读操作。而写锁则会把读和写操作都阻塞。
- 共享锁(行级锁-读锁)
共享锁又称为读锁,简称S锁。共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数 据,但是只能读不能修改。使用共享锁的方法是在select … lock in share mode ,只适用查询语 句。
总结:事务使用了共享锁(读锁),只能读取,不能修改,修改操作被阻塞。
- 排他锁(行级锁-写锁)
排他锁又称为写锁,简称X锁。排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排 他锁,其他事务就不能对该行记录做其他操作,也不能获取该行的锁。
使用排他锁的方法是在SQL末尾加上for update ,innodb引擎默认会在update,delete语句加上 for update。行级锁的实现其实是依靠其对应的索引,所以如果操作没用到索引的查询,那么会锁 住全表记录。
总结:事务使用了排他锁(写锁),当前事务可以读取和修改,其他事务不能修改,也不能获取记录 锁( select… for update )。如果查询没有使用到索引,将会锁住整个表记录。
4. 乐观锁
乐观锁是相对于悲观锁而言的,它不是数据库提供的功能,需要开发者自己去实现。在数据库操作时, 想法很乐观,认为这次的操作不会导致冲突,因此在数据库操作时并不做任何的特殊处理,即不加锁,
而是在进行事务提交时再去判断是否有冲突了。
乐观锁实现的关键点:冲突的检测。
悲观锁和乐观锁都可以解决事务写写并发,在应用中可以根据并发处理能力选择区分,比如对并发率要 求高的选择乐观锁;对于并发率要求低的可以选择悲观锁。
- 乐观锁实现原理
- 使用版本字段( version )
先给数据表增加一个版本(version) 字段,每操作一次,将那条记录的版本号加 1。version 是用来查看被读的记录有无变化,作用是防止记录在业务处理期间被其他事务修改。
- 使用时间戳(Timestamp )
与使用version版本字段相似,同样需要给在数据表增加一个字段,字段类型使用timestamp 时间戳。也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳 进行对比,如果一致则提交更新,否则就是版本冲突,取消操作。
- 乐观锁案例
下面我们使用下单过程作为案例,描述下乐观锁的使用。
- 第一步:查询商品信息
select (quantity,version) from products where id=1;
- 第二部:根据商品信息生成订单
insert into orders …
insert into items …
- 第三部:修改商品库存
update products set quantity=quantity-1,version=version+1where id=1 and version=#{version};
除了自己手动实现乐观锁之外,许多数据库访问框架也封装了乐观锁的实现,比如hibernate框架。 MyBatis框架大家可以使用OptimisticLocker插件来扩展。
Mybatis-plus 使用注解直接实现
5. 死锁与解决方案
下面介绍几种常见的死锁现象和解决方案:
一、表锁死锁
产生原因:
用户A访问表A (锁住了表A),然后又访问表B ;另一个用户B访问表B (锁住了表B ),然后企图 访问表A ;这时用户A由于用户B已经锁住表B ,它必须等待用户B释放表B才能继续,同样用户B要 等用户A释放表A才能继续,这就死锁就产生了。
用户A–》A表(表锁) -10s-》 B表(表锁)
用户B–》 B表(表锁) -10s-》A表(表锁)
解决方案:
这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。仔细分 析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进行处理,尽量避免同时锁定两个资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。
二、行级锁死锁
产生原因1:
如果在事务中执行了一条没有索引条件的查询,引发全表扫描,把行级锁上升为全表记录锁定(等 价于表级锁),多个这样的事务执行后,就很容易产生死锁和阻塞,最终应用系统会越来越慢,发生阻塞或死锁。
解决方案1:
SQL语句中不要使用太复杂的关联多表的查询;使用explain“执行计划”对SQL语句进行分析,对于 有全表扫描和全表锁定的SQL语句,建立相应的索引进行优化。
产生原因2:
两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁。
解决方案2:
- 在同一个事务中,尽可能做到一次锁定所需要的所有资源
- 按照id对资源排序,然后按顺序进行处理
三、共享锁转换为排他锁
产生原因:
事务A 查询一条纪录,然后更新该条纪录;此时事务B 也更新该条纪录,这时事务B 的排他锁由于 事务A 有共享锁,必须等A 释放共享锁后才可以获取,只能排队等待。事务A 再执行更新操作时, 此处发生死锁,因为事务A 需要排他锁来做更新操作。但是,无法授予该锁请求,因为事务B 已经 有一个排他锁请求,并且正在等待事务A 释放其共享锁。
事务A: select * from dept where deptno=1 lock in share mode; //共享锁,1
update dept set dname=‘java’ where deptno=1;//排他锁,3
事务B: update dept set dname=‘Java’ where deptno=1;//由于1有共享锁,没法获取排他锁,需等待, 2
解决方案:
- 对于按钮等控件,点击立刻失效,不让用户重复点击,避免引发同时对同一条记录多次操作;
- 使用乐观锁进行控制。乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统性能。需要注意的是,由于乐观锁机制是在我们的系统中实现,来自外部系统的用 户更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中;
四、死锁排查
MySQL提供了几个与锁有关的参数和命令,可以辅助我们优化锁操作,减少死锁发生。
- 查看死锁日志
通过show engine innodb **status命令查看近期死锁日志信息。
使用方法:
1、查看近期死锁日志信息;
2、使用explain查看下SQL执行计划
- 查看锁状态变量
通过show status like’innodb_row_lock%’命令检查状态变量,分析系统中的行锁的争夺 情况
Innodb_row_lock_current_waits :当前正在等待锁的数量
Innodb_row_lock_time :从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg: 每次等待锁的平均时间
Innodb_row_lock_time_max :从系统启动到现在等待最长的一次锁的时间
如果等待次数高,而且每次等待时间长,需要分析系统中为什么会有如此多的等待,然后着 手定制优化。
6.进阶
- 锁粒度
在 InnoDB 事务中,行锁是在需要的时候才加上的(执行到哪一行就拿哪一行的锁),但并不是不需要了就立刻释放,而是要等到事务结束时(commit)才释放。这个就是两阶段锁协议。
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
- update加锁原则
原则 1:加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间(0,5])。
原则 2:查找过程中访问到的对象才会加锁。
优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
优化 2:索引上的等值查询,非唯一索引向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。(Mysql8.0.18以后修复)
案例表sql:
等值查询间隙锁
1、根据原则 1,加锁单位是 next-key lock,session A 加锁范围就是 (5,10];
2、同时根据优化 2,这是一个等值查询 (id=7),而 id=10 不满足查询条件,next-key lock 退化成间隙锁,因此最终加锁的范围是 (5,10)。
所以插入id为8会被等待锁释放,更新id为10会成功。
结论
不要在不存在的索引上更新,会把行锁变成间隙锁,增加锁粒度。
非唯一索引等值锁
1、根据原则 1,加锁单位是 next-key lock,因此会给 (0,5]加上 next-key lock。(因为命中(0,5]next-key lock左开右闭的’5’)
2、要注意 c 是普通索引,因此仅访问 c=5 这一条记录是不能马上停下来的,需要向右遍历,查到 c=10 才放弃。根据原则 2,访问到的都要加锁, 因此要给 (5,10]加 next-key lock。
3、但是同时这个符合优化 2:等值判断,向右遍历,最后一个值不满足 c=5 这个等值条件,因此退化成间隙锁 (5,10)。所以当前在索引c上的间隙锁为(0,5],(5,10)
4、根据原则 2 ,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么 session B 的 update 语句可以执行完成。
但是sessionC被sessionA的在c列上的(5,10)间隙锁锁住导致等待。
结论
lock in share mode 只锁覆盖索引
for update 系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。
主键索引范围锁
下面两条语句的语义是一样的,但是加锁的范围不同。
1、开始执行的时候,要找到第一个 id=10 的行,因此本该是 next-key lock(5,10]。 根据优化 1, 主键 id 上的等值条件,退化成行锁,只加了 id=10 这一行的行锁。
2、范围查找就往后继续找,找到 id=15 这一行停下来,因此需要加 next-key lock(10,15]。
3、mysql8以后唯一索引的不会范围查找了所以会退化成间隙锁(10,15),SessionC不会blocked
结论
有的时候升级版本能解决很多问题(性能 特性…),否则做了很多优化都是重复造轮子。
非唯一索引范围锁
在第一次用 c=10 定位记录的时候,索引 c 上加了 (5,10]这个 next-key lock 后,由于索引 c 是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,
因此最终 sesion A 加的锁是,索引 c 上的 (5,10] 和 (10,15] 这两个 next-key lock。锁范围 (5,15]
非唯一索引上存在“等值”的例子
虽然有两个 c=10,但是它们的主键值 id 是不同的(分别是 10 和 30),因此这两个 c=10 的记录之间,也是有间隙的。
delete from t where c=10
sql语句的加锁如下图。这个蓝色区域左右两边都是虚线,表示开区间,即 (c=5,id=5) 和 (c=15,id=15) 这两行上都没有锁。limit 语句加锁
delete 语句明确加了 limit 2 的限制,因此在遍历到 (c=10, id=30) 这一行之后,满足条件的语句已经有两条,循环就结束了。因此,索引 c 上的加锁范围就变成了从(c=5,id=5) 到(c=10,id=30) 这个前开后闭区间,
跟上个案例相比少了(10-30,15-15)的间隙锁
结论
在删除数据的时候尽量加 limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。
间隙锁造成死锁
1、session A 启动事务后执行查询语句加 lock in share mode,在索引 c 上加了 next-key lock(5,10] 和间隙锁 (10,15);
2、session B 的 update 语句也要在索引 c 上加 next-key lock(5,10] ,进入锁等待;
3、然后 session A 要再插入 (8,8,8) 这一行,被session B 的间隙锁锁住。由于出现了死锁,InnoDB 让 session B 回滚。
session B 的 next-key lock 不是还没申请成功吗?
其实是这样的,session B 的“加 next-key lock(5,10]”操作,实际上分成了两步,先是加 (5,10) 的间隙锁,加锁成功;
然后加 c=10 的行锁,这时候才被锁住的。
就算分成了两步,为什么session B加(5,10)就能成功呢?session A不是加了(5, 10]的锁吗?
间隙锁和间隙锁之间并不冲突,间隙锁和insert到这个间隙的语句才会冲突,因此session B加间隙锁(5, 10)是可以成功的,但是如果往(5, 10)里面插入的话会被阻塞。 但是如果直接加next-key lock(5, 10],那么肯定是会被阻塞的,因此这个例子确实说明,加锁的步骤是分两步的,先是间隙锁,后是行锁。而且只要理解了间隙锁和行锁之间冲突的原则是不一样的,也就很容易理解这两个锁并不是一起加的了。
事务(重点)
1. ACID
在关系型数据库管理系统中,一个逻辑工作单元要成为事务,必须满足这 4 个特性,即所谓的 ACID: 原子性(Atomicity)、一致性( Consistency)、隔离性( Isolation )和持久性( Durability)。
1.1 原子性
原子性:事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
修改—》 Buffer Pool修改—》刷盘。可能会有下面两种情况:
- 事务提交了,如果此时Buffer Pool的脏页没有刷盘,这时候宕机了,如何保证修改的数据生效? Redo
- 如果事务没提交,但是Buffer Pool的脏页刷盘了,如何保证不该存在的数据撤销? Undo
每一个写事务,都会修改BufferPool ,从而产生相应的Redo/Undo日志,在Buffer Pool 中的页被刷到 磁盘之前,这些日志信息都会先写入到日志文件中,如果 Buffer Pool 中的脏页没有刷成功,此时数据 库挂了,那在数据库再次启动之后,可以通过 Redo 日志将其恢复出来,以保证脏页写的数据不会丢失。如果脏页刷新成功,此时数据库挂了,就需要通过Undo来实现了。
1.2 持久性
持久性:指的是一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,后续的操作或故障不 应该对其有任何影响,不会丢失。
如下图所示,一个“提交”动作触发的操作有: binlog落地、发送binlog、存储引擎提交、 flush_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.3 排队
最简单的方法,就是完全顺序执行所有事务的数据库操作,不需要加锁,简单的说就是全局排队。序列 化执行所有的事务单元,数据库某个时刻只处理一个事务操作,特点是强一致性,处理性能低。
2.2 排他锁
引入锁之后就可以支持并发处理事务,如果事务之间涉及到相同的数据项时,会使用排他锁,或叫互斥锁,先进入的事务独占数据项以后,其他事务被阻塞,等待前面的事务释放锁。
注意,在整个事务1结束之前,锁是不会被释放的,所以,事务2必须等到事务1结束之后开始。
2.3 读写锁
读和写操作:读读、写写、读写、写读。
读写锁就是进一步细化锁的颗粒度,区分读操作和写操作,让读和读之间不加锁,这样下面的两个事务 就可以同时被执行了。
读写锁,可以让读和读并行,而读和写、写和读、写和写这几种之间还是要加排他锁。
3.隔离级别
3.1 隔离级别类型
前面提到的“更新丢失”、”脏读”、“不可重复读”和“幻读”等并发事务问题,其实都是数据库一致性问题, 为了解决这些问题, MySQL数据库是通过事务隔离级别来解决的,数据库系统提供了以下 4 种事务隔 离级别供用户选择。
- 读未提交
Read Uncommitted 读未提交:解决了回滚覆盖类型的更新丢失,但可能发生脏读现象,也就是 可能读取到其他会话中未提交事务修改的数据。
- 已提交读
Read Committed 读已提交:只能读取到其他会话中已经提交的数据,解决了脏读。但可能发生 不可重复读现象,也就是可能在一个事务中两次查询结果不一致。
- 可重复度
Repeatable Read 可重复读:解决了不可重复读,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上会出现幻读,简单的说幻读指的的当用户读取某一范围的数 据行时,另一个事务又在该范围插入了新行,当用户在读取该范围的数据时会发现有新的幻影行。
- 可串行化
Serializable 串行化:所有的增删改查串行执行。它通过强制事务排序,解决相互冲突,从而解决 幻度的问题。这个级别可能导致大量的超时现象的和锁竞争,效率低下。
数据库的事务隔离级别越高,并发问题就越小,但是并发处理能力越差(代价)。读未提交隔离级别最 低,并发问题多,但是并发处理能力好。以后使用时,可以根据系统特点来选择一个合适的隔离级别, 比如对不可重复读和幻读并不敏感,更多关心数据库并发处理能力,此时可以使用Read Commited隔 离级别。
事务隔离级别,针对Innodb引擎,支持事务的功能。像MyISAM引擎没有关系。
事务隔离级别和锁的关系
1 )事务隔离级别是SQL92定制的标准,相当于事务并发控制的整体解决方案,本质上是对锁和MVCC使 用的封装,隐藏了底层细节。
2)锁是数据库实现并发控制的基础,事务隔离性是采用锁来实现,对相应操作加不同的锁,就可以防 止其他事务同时对数据进行读写操作。
3 )对用户来讲,首先选择使用隔离级别,当选用的隔离级别不能解决并发问题或需求时,才有必要在 开发中手动的设置锁。
MySQL默认隔离级别:可重复读
Oracle、SQLServer默认隔离级别:读已提交
一般使用时,建议采用默认隔离级别,然后存在的一些并发问题,可以通过悲观锁、乐观锁等实现处 理。
3.2 MySQL隔离级别控制
MySQL默认的事务隔离级别是Repeatable Read ,查看MySQL当前数据库的事务隔离级别命令如下:
设置事务隔离级别可以如下命令:
4.MVCC(重点)
简介
什么是 MVCC ?
MVCC(Multi-Version Concurrency Control)即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。MVCC使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能。
MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
什么是当前读和快照读?
在学习 MVCC 多版本并发控制之前,我们必须先了解一下,什么是 MySQL InnoDB 下的当前读和快照读?
- 当前读 像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读。
为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
- 快照读 像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的实现是基于多版本并发控制,而有可能是之前的历史版本。
MVCC 就是为了实现读-写冲突不加锁,而这个读指的就是 快照读 , 而非当前读。
当前读实际上是一种加锁的操作,是悲观锁的实现
当前读,快照读和MVCC的关系
- MVCC 多版本并发控制是 「维持一个数据的多个版本,使得读写操作没有冲突」 的概念,只是一个抽象概念,并非实现
- 因为 MVCC 只是一个抽象概念,要实现这么一个概念,MySQL 就需要提供具体的功能去实现它,「快照读就是 MySQL 实现 MVCC 理想模型的其中一个非阻塞读功能」。而相对而言,当前读就是悲观锁的具体功能实现
- 要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC 模型在 MySQL 中的具体实现则是由 3 个隐式字段,undo 日志 ,Read View 等去完成的,具体可以看下面的 MVCC 实现原理
MVCC 能解决什么问题,好处是?
数据库并发场景有三种,分别为:
- 读-读:不存在任何问题,也不需要并发控制
- 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
- 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
MVCC 带来的好处是? 多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
- 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
如何使用MVCC解决并发问题? 有了 MVCC,所以我们可以形成两个组合:
- MVCC + 悲观锁 MVCC解决读写冲突,悲观锁解决写写冲突
- MVCC + 乐观锁 MVCC 解决读写冲突,乐观锁解决写写冲突
这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题
MVCC 的实现原理
MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。所以我们先来看看这个三个 point 的概念
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID等字段
- DB_TRX_ID 6 byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID
- DB_ROLL_PTR 7 byte,回滚指针,指向这条记录的上一个版本的undo log
- DB_ROW_ID 6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引
undo日志
undo log 主要分为两种:
- insert undo log 代表事务在 insert 新记录时产生的 undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
- update undo log 事务在进行 update 或 delete 时产生的 undo log ; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除
对 MVCC 有帮助的实质是 update undo log ,undo log 实际上就是存在 rollback segment 中旧记录链,
执行流程:
一、 比如一个有个事务插入 persion 表插入了一条新记录
记录如下, name 为 Jerry , age 为 24 岁, 隐式主键 是 1, 事务 ID 和 回滚指针 ,我们假设为 NULL
二、 现在来了一个 事务 1 对该记录的 name 做出了修改,改为 Tom
- 在事务 1修改该行(记录)数据时,数据库会先对该行加排他锁
- 然后把该行数据拷贝到 undo log 中,作为旧记录,既在 undo log 中有当前行的拷贝副本
- 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务 ID 为当前事务 1的 ID, 我们默认从 1 开始,之后递增,回滚指针指向拷贝到 undo log 的副本记录,既表示我的上一个版本就是它
- 事务提交后,释放锁
三、 又来了个事务 2 修改person 表 的同一个记录,将age 修改为 30 岁
- 在事务2修改该行数据时,数据库也先为该行加锁
- 然后把该行数据拷贝到 undo log 中,作为旧记录,发现该行记录已经有 undo log 了,那么最新的旧数据作为链表的表头,插在该行记录的 undo log 最前面
- 修改该行 age 为 30 岁,并且修改隐藏字段的事务 ID 为当前事务 2的 ID, 那就是 2 ,回滚指针指向刚刚拷贝到 undo log 的副本记录
- 事务提交,释放锁
不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线链表,undo log 的链首就是最新的旧记录,链尾就是最早的旧记录
Read View 读视图
什么是 Read View?
Read View 就是事务进行快照读操作的时候生产的读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID
我们可以把 Read View 简单的理解成有三个全局属性
- 活跃事务列表
- 一个数值列表
- 用于维护 Read View 生成时刻系统 正活跃的事务 ID 列表
- up_limit_id
- lower water mark
- 是 trx_list 列表中事务 ID 最小的 ID
- low_limit_id
- hight water mark
- ReadView 生成时刻系统尚未分配的下一个事务 ID ,也就是 目前已出现过的事务 ID 的最大值 + 1
- 为什么是 low_limit ? 因为它也是系统此刻可分配的事务 ID 的最小值
这两个 ID 其实就可以从当前执行的事务的视角,将所有的事务分为三个部分
- 小于低水位的部分一定是当前事务开始前就提交了的部分
- 大于等于高水位的则一定是还未提交的事务,我们一定不可见
- 处于中间的部分就要分类讨论了:
- 如果在视图数组中,说明当前事务开始时,这些事务仍在活跃,所以应该是不可见的;
- 如果不在数组中,说明在仍活跃着的事务范围内,但其中有一些事务虽然不是开始最早的,但是结束的却比活跃数组中的事务早,以至于当前事务开始时,这些事务已经结束,所以就应该是可见的。
可见事务id总结
要么比低水位更早,要么比高水位的 id 小但是不能出现在活跃事物数组中。
整体流程
流程模拟
- 当事务 2对某行数据执行了快照读,此时还有事务1和事务3在活跃中,事务 4在事务 2快照读前一刻提交更新了。
事务 1 | 事务 2 | 事务 3 | 事务 4 |
事务开始 | 事务开始 | 事务开始 | 事务开始 |
… | … | … | 修改且已提交 |
进行中 | 快照读 | 进行中 | ㅤ |
… | … | … | ㅤ |
- up_limit_id 就是1,low_limit_id 就是 4 + 1 = 5,活跃事务列表值是 1, 3,Read View 如下图
- 我们的例子中,只有事务 4 修改过该行记录,并在事务 2 执行快照读前,就提交了事务。所以当前DB_TRX_ID 为4
- 拿该记录 DB_TRX_ID 字段记录的事务 ID 4 去跟 Read View 的 up_limit_id 比较
- DB_TRX_ID(4)大于 up_limit_id(1),所以不符合可见条件
- 继续判断 DB_TRX_ID(4) 是否大于等于 low_limit_id(5),不符和不可见条件
- 最后判断 DB_TRX_ID(4) 是否处于 trx_list 中的活跃事务, 最后发现事务 ID 为 4 的事务不在当前活跃事务列表中, 符合可见性条件
- 如果上述三点判断后不符合可见性,则去undolog链中找上一条记录的DB_TRX_ID,重复123步查找
所以事务 4修改后提交的最新结果对事务 2 快照读时是可见的,获取字段的值为’A’
MVCC 相关问题
RR 是如何在 RC 级的基础上解决不可重复读的?
Read View 生成时机的不同,从而造成 RC , RR 级别下快照读的结果的不同
当前读和快照读在 RR 级别下的区别:
表1:
事务A | 事务B |
开启事务 | 开启事务 |
快照读(无影响)查询金额为500 | 快照读查询金额为500 |
更新金额为400 | ㅤ |
提交事务 | ㅤ |
ㅤ | select 快照读金额为500 |
ㅤ | select lock in share mode当前读金额为400 |
在上表的顺序下,事务 B 的在事务 A 提交修改后的快照读是旧版本数据,而当前读是实时新数据 400
表2:
事务A | 事务B |
开启事务 | 开启事务 |
快照读(无影响)查询金额为500 | ㅤ |
更新金额为400 | ㅤ |
提交事务 | ㅤ |
ㅤ | select 快照读金额为400 |
ㅤ | select lock in share mode当前读金额为400 |
而在表 2这里的顺序中,事务 B 在事务 A 提交后的快照读和当前读都是实时的新数据 400,这是为什么呢?
表2的第一次快照读在事务A更新金额为400而且commit之后发生
RC , RR 级别下的 InnoDB 快照读有什么不同?
正是 Read View 生成时机的不同,从而造成 RC , RR 级别下快照读的结果的不同
在 RC 隔离级别下,是每个快照读都会生成并获取最新的 Read View;
而在 RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View, 之后的快照读获取的都是同一个 Read View。
MySQL优化
数据库优化维度有四个:
硬件升级、系统配置、表结构设计、 SQL语句及索引。
优化选择:
优化成本:硬件升级>系统配置>表结构设计>SQL语句及索引。
优化效果:硬件升级<系统配置<表结构设计<SQL语句及索引。
大部分问题都是开发者没有充分利用数据库性能造成的
背景知识
order by 是如何运行的?
Extra 这个字段中的“Using filesort”表示的就是需要排序,MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。
全字段排序执行流程:
1、初始化 sort_buffer,确定放入 name、city、age 这三个字段;
2、从索引 city 找到第一个满足 city=’杭州’条件的主键 id,也就是图中的 ID_X;
3、到主键 id 索引取出整行,取 name、city、age 三个字段的值,存入 sort_buffer 中;
4、从索引 city 取下一个记录的主键 id;
5、重复步骤 3、4 直到 city 的值不满足查询条件为止,对应的主键 id 也就是图中的 ID_Y;
6、对 sort_buffer 中的数据按照字段 name 做快速排序;按照排序结果取前 1000 行返回给客户端。
排序可能在内存中完成,也可能需要使用外部排序,这取决于排序所需的内存和参数 sort_buffer_size。
sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
MySQL 认为排序的单行长度太大会怎么做呢?使用rowid排序
rowid排序流程:
1、初始化 sort_buffer,确定放入两个字段,即 name 和 id;
2、从索引 city 找到第一个满足 city=’杭州’条件的主键 id,也就是图中的 ID_X;
3、到主键 id 索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中;
4、从索引 city 取下一个记录的主键 id;
5、重复步骤 3、4 直到不满足 city=’杭州’条件为止,也就是图中的 ID_Y;
6、对 sort_buffer 中的数据按照字段 name 进行排序;
7、遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。
全字段排序 VS rowid 排序
1、如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。
2、如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
这也就体现了 MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。对于 InnoDB 表来说,rowid 排序会要求回表两次造成磁盘读,因此不会被优先选择。
根因是因为需要排序的字段无序造成额外排序
使用索引增加有序性
alter table t add index city_user(city, name);
执行流程:
1、从索引 (city,name) 找到第一个满足 city=’杭州’条件的主键 id;
2、到主键 id 索引取出整行,取 name、city、age 三个字段的值,作为结果集的一部分直接返回;
3、从索引 (city,name) 取下一个记录主键 id;重复步骤 2、3,直到查到第 1000 条记录,或者是不满足 city=’杭州’条件时循环结束。
能否再优化呢?
使用覆盖索引去掉回表扫描
alter table t add index city_user_age(city, name, age);
union是如何运行的?
使用了临时表 (Using temporary)。
执行流程:
1、创建一个内存临时表,这个临时表只有一个整型字段 f,并且 f 是主键字段。
2、执行第一个子查询,得到 1000 这个值,并存入临时表中。
3、执行第二个子查询:拿到第一行 id=1000,试图插入临时表中。但由于 1000 这个值已经存在于临时表了,违反了唯一性约束,所以插入失败,然后继续执行;
取到第二行 id=999,插入临时表成功。
4、从临时表中按行取出数据,返回结果,并删除临时表,结果中包含两行数据分别是 1000 和 999。
临时表是因为union去重使用的。如果使用union all就代表不用去重,那就不用临时表。
group by是如何运行的?
执行流程:
1、创建内存临时表,表里有两个字段 m 和 c,主键是 m;
2、扫描表 t1 的索引 a,依次取出叶子节点上的 id 值,计算 id%10 的结果,记为 x;
- 如果临时表中没有主键为 x 的行,就插入一个记录 (x,1);
- 如果表中有主键为 x 的行,就将 x 这一行的 c 值加 1;
3、遍历完成后,再根据字段 m 做排序,得到结果集返回给客户端。(mysql 8之后去掉了对结果做排序)
group by优化:
使用索引
通过索引保证顺序性
如果可以确保输入的数据是有序的,那么计算 group by 的时候,就只需要从左到右,顺序扫描,依次累加。
过程:
当碰到第一个 1 的时候,由于有顺序证明所有的0已经遍历过了,已经知道累积了 X 个 0,结果集里的第一行就是 (0,X);
当碰到第一个 2 的时候,由于有顺序证明所有的1已经遍历过了,已经知道累积了 Y 个 1,结果集里的第二行就是 (1,Y);
按照这个逻辑执行的话,扫描到整个输入的数据结束,就可以拿到 group by 的结果,不需要临时表,也不需要再额外排序。
通过5.7 genertate column机制实现数据动态关联
替换成olap引擎
group by的多用在数据分析场景,使用列数据库性能更好。比如Hbase和现在用的比较多的Clickhouse或者Doris
join 是如何运行的?
Index Nested-Loop Join(NLJ) 可以使用上索引的情况
执行流程:
1、从表 t1 中读入一行数据 R;
2、从数据行 R 中,取出 a 字段到表 t2 里去查找;
3、取出表 t2 中满足条件的行,跟 R 组成一行,作为结果集的一部分;
4、重复执行步骤 1 到 3,直到表 t1 的末尾循环结束。
先遍历t1,从t1的每一行读取a,使用t2的索引a去t2表中去找到符合条件的记录。
对驱动表 t1 做了全表扫描,这个过程需要扫描 100 行;
而对于每一行 R,根据 a 字段去表 t2 查找,走的是索引上的树搜索过程。由于我们构造的数据都是一一对应的,因此每次的搜索过程都只扫描一行,也是总共扫描 100 行;
所以,整个执行流程,总扫描行数是 200。
假设被驱动表的行数是 M,驱动表的行数为N。每次在被驱动表查一行数据,要先搜索索引 a,再搜索主键索引(回表)。每次搜索一棵树近似复杂度是以 2 为底的 M 的对数,记为 log2M,所以在被驱动表上查一行的时间复杂度是 2*log2M。
时间复杂度为N+N*2*log2M。
结论:
1、驱动表是走全表扫描,而被驱动表是走树搜索。所以使用小表当驱动表。
2、使用NLJ比业务系统多次查询mysql效率高,减少了io开销
Simple Nested-Loop Join 暴力叉乘
使用非索引join 扫描行数为两表数据量乘积。100*1000=10W。
时间空间复杂度都为O(M*N)
Mysql没有采用这种方式,采用了相对优化的BNJ。
Block Nested-Loop Join(BNJ)
被驱动表没有索引的执行流程:
1、把表 t1 的数据读入线程内存 join_buffer 中,由于我们这个语句中写的是 select *,因此是把整个表 t1 放入了内存;
2、扫描表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回。
扫描的行数为M+N 100+1000=1100行。内存判断的次数为10*1000=10W次
扫描行数是 N+M; 这就是跟Simple Nested-Loop Join的区别
内存判断 N*M 次。
t1表太大join_buffer放不下怎么办?
会把驱动表切成多份加载。但是会增加被驱动表的扫描次数
扫描行数是 N+λNM;
内存判断 N*M 次。
count(*)的实现
MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,时间复杂度O(1),效率很高;
而 InnoDB 引擎就麻烦了,它执行 count() 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。
InnoDB 是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。所以,普通索引树比主键索引树小很多。
对于 count(*) 这样的操作,遍历哪个索引树得到的结果逻辑上都是一样的。
因此,MySQL 优化器会找到最小的那棵树来遍历。在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。
如何优化count?
我们把这个计数直接放到数据库里单独的一张计数表保存count值。
利用事务的特性高效准确的提供的计数
不同count的区别
count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加。最后返回累计值。
至于分析性能差别的时候,你可以记住这么几个原则:
1、server 层要什么就给什么;
2、InnoDB 只给必要的值;
- *3、现在的优化器只优化了 count(*) 的语义为“取行数”,其他“显而易见”的优化并没有做。**
- 对于 count(主键 id) 来说,InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。
- 对于 count(1) 来说,InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。单看这两个用法的差别的话,你能对比出来,count(1) 执行得要比 count(主键 id) 快。因为从引擎返回 id 会涉及到解析数据行,以及拷贝字段值的操作。
- 对于 count(字段) 来说:如果这个“字段”是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个“字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。也就是前面的第一条原则,server 层要什么字段,InnoDB 就返回什么字段。
- 但是 count(*) 是例外,并不会把全部字段取出来,而是专门做了优化,不取值。count(*) 肯定不是 null,按行累加。
结论:
按照效率排序的话,count(字段)<count(主键 id)<count(1)≈count(*)
所以尽量使用 count(*)
1. 系统参数优化
1.1 保证从内存中读取数据
MySQL会在内存中保存一定的数据,通过LRU算法将不常访问的数据保存在硬盘文件中。
尽可能的扩大内存中的数据量,将数据保存在内存中,从内存中读取数据,可以提升MySQL性能。 扩大innodb_buffer_pool_size ,能够全然从内存中读取数据。最大限度降低磁盘操作。
innodb_buffer_pool_size默认为128M,理论上可以扩大到内存的3/4或4/5。
修改 my.cnf
innodb_buffer_pool_size = 750M
如果是专用的MySQL Server可以禁用SWAP
1.2 降低磁盘写入次数
增大redolog,减少落盘次数,把随机io变成顺序io
innodb_log_file_size 设置为 0.25 * innodb_buffer_pool_size
通用查询日志、慢查询日志可以不开, bin-log开
生产中不开通用查询日志,遇到性能问题开慢查询日志
写redolog策略 innodb_flush_log_at_trx_commit设置为0或2
如果不涉及非常高的安全性 (金融系统),或者基础架构足够安全,或者事务都非常小,都能够用 0 或者 2 来减少磁盘操作。
1.3 提高磁盘读写性能
使用SSD或者内存磁盘
2.表结构优化
2.1 设计中间表
设计中间表,一般针对于统计分析功能,或者实时性不高的需求(OLTP、OLAP)
2.2 设计冗余字段
为减少关联查询,创建合理的冗余字段 (创建冗余字段还需要注意数据一致性问题)
2.3 拆表
对于字段太多的大表,考虑拆表 (比如一个表有100多个字段)
对于表中经常不被使用的字段或者存储数据比较多的字段,考虑拆表
2.4 主键优化
每张表建议都要有一个主键(主键索引),而且主键类型最好是int类型,建议自增主键(不考虑分布 式系统的情况下 雪花算法)。
2.5 字段的设计
数据库中的表越小,在它上面执行的查询也就会越快。
因此,在创建表的时候,为了获得更好的性能,我们可以将表中字段的宽度设得尽可能小。 尽量把字段设置为NOTNULL,这样在将来执行查询的时候,数据库不用去比较NULL值。
对于某些文本字段,例如“省份”或者“性别”,我们可以将它们定义为ENUM类型。因为在MySQL中, ENUM类型被当作数值型数据来处理,而数值型数据被处理起来的速度要比文本类型快得多。这样,我 们又可以提高数据库的性能。
能用数字的用数值类型
3.sql语句优化
3.1 EXPLAIN查看索引使用情况
掌握explain是sql优化的必备技能
3.2 SQL语句中IN包含的值不应过多
MySQL对于IN做了相应的优化,即将IN中的常量全部存储在一个数组里面,而且这个数组是排好序 的。但是如果数值较多,产生的消耗也是比较大的。
3.3 SELECT语句务必指明字段名称
SELECT * 增加很多不必要的消耗(CPU、 IO、内存、网络带宽);减少了使用覆盖索引的可能性;
当表结构发生改变时,前端也需要更新。所以要求直接在select后面接上字段名。
3.4 当只需要一条数据的时候,使用limit 1
limit 是可以停止全表扫描的
3.5 排序字段加索引
使用索引的有序性,避免filesort二次排序。
3.6 如果限制条件中其他字段没有索引,尽量少用or
or两边的字段中,如果有一个不是索引字段,会造成该查询不走索引的情况。
3.7 尽量用union all代替union
union是使用临时表去做并集的。临时表是因为union去重使用的。如果使用union all就代表不用去重,那就不用临时表。
3.8 不使用ORDER BY RAND()
ORDER BY RAND() 不走索引。使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法。
在业务服务中使用计算x=random(总行数),然后limit x,1取出随机数
3.9 区分in和exists、 not in和not exists
区分in和exists主要是造成了驱动顺序的改变(这是性能变化的关键),如果是exists,那么以外层表为驱动表,先被访问,如果是IN,那么先执行子查询。所以IN适合于外表大而内表小的情况; EXISTS适合 于外表小而内表大的情况。
关于not in和not exists,推荐使用not exists ,不仅仅是效率问题, not in可能存在逻辑问题。如何高 效的写出一个替代not exists的SQL语句?
原SQL语句:
select colname … from A表 where a.id not in (select b.id from B表)
高效的SQL语句:
select colname … from A表 Left join B表 on where a.id = b.id where b.id is null
3.10 使用合理的分页方式以提高分页的效率
分页使用 limit m,n 尽量让m 小
3.11 分段查询
一些用户选择页面中,可能一些用户选择的范围过大,造成查询缓慢。主要的原因是扫描行数过多。这 个时候可以通过程序,分段进行查询,循环遍历,将结果合并处理进行展示。
3.12 不建议使用%前缀模糊查询
例如LIKE“%name”或者LIKE“%name%”,这种查询会导致索引失效而进行全表扫描。但是可以使用LIKE “name%”。
那么如何解决这个问题呢,答案:使用全文索引或ES全文检索
3.13 避免在where子句中对字段进行表达式操作
select user_id,user_project from user_base where age*2=36;
中对字段就行了算术运算,这会造成引擎放弃使用索引,建议改成:
select user_id,user_project from user_base where age=36/2;
3.14 避免隐式类型转换
where子句中出现column字段的类型和传入的参数类型不一致的时候发生的类型转换,建议先确定 where中的参数类型。 where age=‘18’
3.15 对于联合索引来说,要遵守最左前缀法则
举列来说索引含有字段id、 name、school,可以直接用id字段,也可以id、 name这样的顺序,但是 name;school都无法使用这个索引。所以在创建联合索引的时候一定要注意索引字段顺序,常用的查询 字段放在最前面。
3.16 必要时可以使用force index来强制查询走某个索引
有的时候MySQL优化器采取它认为合适的索引来检索SQL语句,但是可能它所采用的索引并不是我们想 要的。这时就可以采用forceindex来强制优化器使用我们制定的索引。
3.17 注意范围查询语句
对于联合索引来说,如果存在范围查询,比如between、>、<等条件时,会造成后面的索引字段失效。
3.18 使用JOIN优化
LEFT JOIN A表为驱动表, INNER JOIN MySQL会自动找出那个数据少的表作用驱动表, RIGHT JOIN B 表为驱动表。
1、使用小表为驱动表,大表为被驱动表,被驱动表关联字段增加索引。
2、在应用程序中对被驱动表设置缓存。
3.19 group by的优化
1、尽量让 group by 过程用上表的索引,确认方法是 explain 结果里没有 Using temporary 和 Using filesort;
2、如果 group by 需要统计的数据量不大,尽量只使用内存临时表;也可以通过适当调大 tmp_table_size 参数,来避免用到磁盘临时表;
3.20 count的优化
1、count(字段)<count(主键 id)<count(1)≈count(*),所以我建议你,尽量使用 count(*)。
2、使用额外的数据库表存储count,可以保证高效准确读取count信息。
4.最佳实践
阿里巴巴开发规范:
https://github.com/alibaba/p3c/tree/master/p3c-gitbook/MySQL%E6%95%B0%E6%8D%AE%E5%BA%93
Mysql集群架构
1. 集群架构设计
1.1 架构设计理念
在集群架构设计时,主要遵从下面三个维度:
- 可用性
- 扩展性
- 一致性
1.2 可用性设计
- 站点高可用,冗余站点
- 服务高可用,冗余服务
- 数据高可用,冗余数据
保证高可用的方法是冗余。但是数据冗余带来的问题是数据一致性问题。
实现高可用的方案有以下几种架构模式:
- 主从模式
简单灵活,能满足多种需求。比较主流的用法,但是写操作高可用需要自行处理。
- 双主模式
互为主从,有双主双写、双主单写两种方式,建议使用双主单写
1.3 扩展性设计
扩展性主要围绕着读操作扩展和写操作扩展展开。
如何扩展以提高读性能
- 加从库
简单易操作,方案成熟。
从库过多会引发主库性能损耗。建议不要作为长期的扩充方案,应该设法用良好的设计避免 持续加从库来缓解读性能问题。
- 分库分表
可以分为垂直拆分和水平拆分,垂直拆分可以缓解部分压力,水平拆分理论上可以无限扩展。
- 如何扩展以提高写性能
- 分库分表
1.4 一致性设计
一致性主要考虑集群中各数据库数据同步以及同步延迟问题。可以采用的方案如下:
- 不使用从库
扩展读性能问题需要单独考虑,否则容易出现系统瓶颈。
- 增加访问路由层
可以先得到主从同步最长时间t ,在数据发生修改后的t时间内,先访问主库。
2. 主从模式
2.1 适用场景
MySQL主从模式是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。 MySQL 默 认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,从节点可以复制主数据库 中的所有数据库,或者特定的数据库,或者特定的表。
mysql主从复制用途:
实时灾备,用于故障切换(高可用)
读写分离,提供查询服务(读扩展)
数据备份,避免影响业务(高可用)
主从部署必要条件:
从库服务器能连通主库
主库开启binlog日志(设置log-bin参数)
主从server-id不同
2.2 实现原理
2.2.1 主从复制
下图是主从复制的原理图。
主从复制整体分为以下三个步骤:
主库将数据库的变更操作记录到Binlog日志文件中
从库读取主库中的Binlog日志文件信息写入到从库的Relay Log中继日志中
- 从库读取中继日志信息在从库中进行Replay,更新从库数据信息
在上述三个过程中,涉及了Master的BinlogDump Thread和Slave的I/O Thread、SQL Thread ,它们 的作用如下:
Master服务器对数据库更改操作记录在Binlog中, BinlogDump Thread接到写入请求后,读取 Binlog信息推送给Slave的I/O Thread。
Slave的I/O Thread将读取到的Binlog信息写入到本地Relay Log中。
Slave的SQL Thread检测到Relay Log的变更请求,解析relay log中内容在从库上执行。
上述过程都是异步操作,俗称异步复制,存在数据延迟现象。
下图是异步复制的时序图。
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
当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。
2.3 并行复制
MySQL的主从复制延迟一直是受开发者最为关注的问题之一, MySQL从5.6版本开始追加了并行复制功 能,目的就是为了改善复制延迟问题,并行复制称为enhanced multi-threaded slave (简称MTS )。
在从库中有两个线程IO Thread和SQL Thread ,都是单线程模式工作,因此有了延迟问题,我们可以采 用多线程机制来加强,减少从库复制延迟。( IO Thread多线程意义不大,主要指的是SQL Thread多线 程)
在MySQL的5.6、5.7、8.0版本上,都是基于上述SQL Thread多线程思想,不断优化,减少复制延迟。
并行复制的标准
coordinator 在分发的时候,需要满足以下这两个基本要求:
- 不能造成更新覆盖。这就要求更新同一行的两个事务,必须被分发到同一个 worker 中。
- 同一个事务不能被拆开,必须放到同一个 worker 中。
2.3.1 MySQL 5.6并行复制原理
MySQL 5.6版本也支持所谓的并行复制,只是支持的粒度是按库并行。如果用户的MySQL数据库中是多个 库,对于从库复制的速度的确可以有比较大的帮助。
基于库的并行复制,实现相对简单,使用也相对简单些。基于库的并行复制遇到单库多表使用场景就发 挥不出优势了,另外对事务并行处理的执行顺序也是个大问题。
2.3.2 MySQL 5.7并行复制原理
MySQL 5.7是基于组提交的并行复制, MySQL 5.7才可称为真正的并行复制,这其中最为主要的原因就 是slave服务器的回放与master服务器是一致的,即master服务器上是怎么并行执行的slave上就怎样进 行并行回放。不再有库的并行复制限制。
MySQL 5.7中组提交的并行复制究竟是如何实现的?
组提交(group commit)。MySQL 5.7是通过对事务进行分组,当事务提交时,它们将在单个操作中写入到二进制日志中。如果多个事务能同时提交成功,那么它们意味着没有冲突,因此可以在Slave上并行执行,所以通过在主库上的二进制日志中添加组提交信息。
MySQL 5.7的并行复制基于一个前提,即所有已经处于prepare阶段的事务,都是可以并行提交的。这些当然也可以在从库中并行提交,因为处理这个阶段的事务都是没有冲突的。在一个组里提交的事务, 一定不会修改同一行。这是一种新的并行复制思路,完全摆脱了原来一直致力于为了防止冲突而做的分发算法,等待策略等复杂的而又效率底下的工作。
InnoDB事务提交采用的是两阶段提交模式。一个阶段是prepare ,另一个是commit。
为了兼容MySQL 5.6基于库的并行复制, 5.7引入了新的变量slave-parallel-type ,
其可以配置的值有: DATABASE (默认值,基于库的并行复制方式)、 LOGICAL_CLOCK (基于组提交的并行复制方式)。
通过mysqlbinlog工具分析binlog日志,就可以发现组提交的内部信息。
主库在写binlog的时候会给这些binlog里面记commit_id和sequence_no,来说明事务之间在主库上并行prepare的状态;
备库是通过解析binlog拿到 commit_id 和 sequence_no,来决定要怎么并发的。
可以发现MySQL 5.7二进制日志较之原来的二进制日志内容多了last_committed和 sequence_number ,last_committed表示事务提交的时候,上次事务提交的编号,如果事务具有相同 的last_committed ,表示这些事务都在一组内,可以进行并行的回放。
2.3.3 MySQL8.0 并行复制
MySQL8.0 是基于write-set的并行复制。
writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容(event 里的行数据),节省了很多计算量
新增了一个参数
binlog-transaction-dependency-tracking
,用来控制是否启用这个新策略。这个参数的可选值有以下三种。- COMMIT_ORDER,表示的就是前面介绍的,根据同时进入 prepare 和 commit 来判断是否可以并行的策略。
- WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset。如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行。
这个 hash 值是通过“库名 + 表名 + 索引名 + 值”计算出来的。如果一个表上除了有主键索引外,还有其他唯一索引,那么对于每个唯一索引,insert 语句对应的 writeset 就要多增加一个 hash 值。
- WRITESET_SESSION,是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。
2.4 读写分离
2.4.1 读写分离引入时机
大多数互联网业务中,往往读多写少,这时候数据库的读会首先成为数据库的瓶颈。如果我们已经优化 了SQL ,但是读依旧还是瓶颈时,这时就可以选择“读写分离”架构了。
读写分离首先需要将数据库分为主从库,一个主库用于写数据,多个从库完成读数据的操作,主从库之 间通过主从复制机制进行数据的同步,如图所示。
在应用中可以在从库追加多个索引来优化查询,主库这些索引可以不加,用于提升写效率。
读写分离架构也能够消除读写锁冲突从而提升数据库的读写性能。使用读写分离架构需要注意: 主从同
步延迟和读写分配机制问题
- ***2.4.2** 主从同步延迟**
使用读写分离架构时,由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态。对于一些实时性要求比较高的操作,可以采用以下解决方案。
- 强制走主库
对于必须要拿到最新结果的请求强制读写都在主库上,否则可以读从写主。比如广告中给广告主扣费后还有检查是否有余额对广告进行关停,就必须强制走主库。
- 使用半同步复制
semi-sync plugin 做了这样的设计:
事务提交的时候,主库把 binlog 发给从库;
从库收到 binlog 以后,发回给主库一个 ack,表示收到了;
主库收到这个 ack 以后,才能给客户端返回“事务完成”的确认。
- 写后读主库
在写入数据库后,某个时间段内读操作就去主库,之后读操作访问从库。
- 判断主备无延迟
show slave status
- 对比位点确保主备无延迟
- 对比 GTID 集合确保主备无延迟
先判断
seconds_behind_master
是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为 0 才能执行查询请求2.4.3 读写分离落地
读写路由分配机制是实现读写分离架构最关键的一个环节,就是控制何时去主库写,何时去从库读。目前较为常见的实现方案分为以下两种:
- 基于编程和配置实现(应用端)
程序员在代码中封装数据库的操作,代码中可以根据操作类型进行路由分配,增删改时操作主库, 查询时操作从库。这类方法也是目前生产环境下应用最广泛的。
优点是实现简单,因为程序在代码中实现,不需要增加额外的硬件开支,
缺点是需要开发人员来实现,运维人员无从下手,如果其中
一个数据库宕机了,就需要修改配置重启项目。
- 基于服务器端代理实现(服务器端)
中间件代理一般介于应用服务器和数据库服务器之间,从图中可以看到,应用服务器并不直接进入 到master数据库或者slave数据库,而是进入MySQL proxy代理服务器。代理服务器接收到应用服 务器的请求后,先进行判断然后转发到后端master和slave数据库。
目前有很多性能不错的数据库中间件,常用的有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顶级项目。
最佳实践
- 小米服务端proxy读写分离
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承担读请求。
随着业务发展,架构会从主从模式演变为双主模式,建议用双主单写,再引入高可用组件,例如 Keepalived和MMM等工具,实现主库故障自动切换。
3.2 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.3主备切换策略
主备切换是指将备库变为主库,主库变为备库,有可靠性优先和可用性优先两种策略。
- 主备延迟问题
主备延迟是由主从数据同步延迟导致的,与数据同步有关的时间点主要包括以下三个:
- 主库 A 执行完成一个事务,写入 binlog ,我们把这个时刻记为 T1;
- 之后将binlog传给备库 B ,我们把备库 B 接收完 binlog 的时刻记为 T2;
- 备库 B 执行完成这个binlog复制,我们把这个时刻记为 T3。
所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就 是 T3-T1。
在备库上执行show slave status命令,它可以返回结果信息, seconds_behind_master表示当前 备库延迟了多少秒。
同步延迟主要原因如下:
- 部署的时候从库机器性能比主库差。主从对称部署。
- 从库的压力大。当只有一主一从的时候一般主库负责写从库负责读。这时候从库除了要处理binlog的同步还要处理读请求,从而造成延迟。可以部署多个从库分担读压力。
- 大事务。因为主库上必须等事务执行完成才会写入 binlog,再传给备库。所以,如果一个主库上的语句执行 10 分钟,那这个事务很可能就会导致从库延迟 10 分钟。避免大事务,业务上拆分成多个小事务
- 可靠性优先
主备切换过程一般由专门的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的可用性依赖于同步延时的大小,同步延时越小,可用性就越高。
docker搭建mha
https://github.com/prontera/docker-mysql-mha
4. 分库分表
互联网系统需要处理大量用户的请求。比如微信日活用户破10亿,海量的用户每天产生海量的数量;美团外卖,每天都是几千万的订单,那这些系统的用户表、订单表、交易流水表等是如何处理呢?
数据量只增不减,历史数据又必须要留存,非常容易成为性能的瓶颈,而要解决这样的数据库瓶颈问 题, “读写分离”和缓存往往都不合适,目前比较普遍的方案就是使用NoSQL/NewSQL或者采用分库分 表。
使用分库分表时,主要有垂直拆分和水平拆分两种拆分模式,都属于物理空间的拆分。 分库分表方案:只分库、只分表、分库又分表。
垂直拆分:由于表数量多导致的单个库大。将表拆分到多个库中。
水平拆分:由于表记录多导致的单个库大。将表记录拆分到多个表中。
4.1 拆分方式
- 垂直拆分
垂直拆分又称为纵向拆分,垂直拆分是将表按库进行分离,或者修改表结构按照访问的差异将某些 列拆分出去。应用时有垂直分库和垂直分表两种方式,一般谈到的垂直拆分主要指的是垂直分库。
如下图所示,采用垂直分库,将用户表和订单表拆分到不同的数据库中。
垂直分表就是将一张表中不常用的字段拆分到另一张表中,从而保证第一张表中的字段较少,避免 出现数据库跨页存储的问题,从而提升查询效率。
解决:一个表中字段过多,还有有些字段经常使用,有些字段不经常使用,或者还有text等字段信 息。可以考虑使用垂直分表方案。
按列进行垂直拆分,即把一条记录分开多个地方保存,每个子表的行数相同。把主键和一些列放到 一个表,然后把主键和另外的列放到另一个表中。
垂直拆分优点:
- 拆分后业务清晰,拆分规则明确;
- 易于数据的维护和扩展;
- 可以使得行数据变小,一个数据块 (Block) 就能存放更多的数据,在查询时就会减少 I/O 次数;
- 可以达到最大化利用 Cache 的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起;
- 便于实现冷热分离的数据表设计模式。
垂直拆分缺点:
- 主键出现冗余,需要管理冗余列;
- 会引起表连接 JOIN 操作,可以通过在业务服务器上进行 join 来减少数据库压力,提高了系统的复杂度;
- 依然存在单表数据量过大的问题;
- 事务处理复杂。
- 水平拆分
水平拆分又称为横向拆分。 相对于垂直拆分,它不再将数据根据业务逻辑分类,而是通过某个字 段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个表仅包含数据的一部分,如 下图所示。
水平分表是将一张含有很多记录数的表水平切分,不同的记录可以分开保存,拆分成几张结构相同 的表。如果一张表中的记录数过多,那么会对数据库的读写性能产生较大的影响,虽然此时仍然能 够正确地读写,但读写的速度已经到了业务无法忍受的地步,此时就需要使用水平分表来解决这个 问题。
水平拆分:解决表中记录过多问题。
垂直拆分:解决表过多或者是表字段过多问题。
水平拆分重点考虑拆分规则:例如范围、时间或Hash算法等。
水平拆分优点:
- 分片规则设计好,join 操作基本可以数据库做;
- 不存在单库大数据,高并发的性能瓶颈;
- 切分的表的结构相同,应用层改造较少,只需要增加路由规则即可;
- 提高了系统的稳定性和负载能力。
水平拆分缺点:
- 拆分规则难以抽象;
- 跨库Join性能较差;
- 分片事务的一致性难以解决
- 数据扩容的难度和维护量极大。
日常工作中,我们通常会同时使用两种拆分方式,垂直拆分更偏向于产品/业务/功能拆分的过程,在技术上我们更关注水平拆分的方案。
4.2 主键策略
在很多中小项目中,我们往往直接使用数据库自增特性来生成主键ID ,这样确实比较简单。而在分库分 表的环境中,数据分布在不同的数据表中,不能再借助数据库自增长特性直接生成,否则会造成不同数 据表主键重复。下面介绍几种ID生成算法。
- UUID
UUID是通用唯一识别码( Universally Unique Identifier)的缩写,长度是16个字节,被表示为 32个十六进制数字,以“ - ”分隔的五组来显示,格式为8-4-4-4-12 ,共36个字符,例如:550e8400-e29b-41d4-a716-446655440000。 UUID在生成时使用到了以太网卡地址、纳秒级时 间、芯片ID码和随机数等信息,目的是让分布式系统中的所有元素都能有唯一的识别信息。
优点:
使用UUID做主键,可以在本地生成,没有网络消耗,所以生成性能高。
缺点:
但是UUID比较长,耗费存储空间。
无序,插入的时候会造成mysql的页分裂。
All indexes other than the clustered index are known as secondary indexes. In InnoDB, each record in a secondary index contains the primary key columns for the row, as well as the columns specified for the secondary index. InnoDB uses this primary key value to search for the row in the clustered index. If the primary key is long, the secondary indexes use more space, so it is advantageous to have a short primary key.
除聚集索引以外的所有索引都称为辅助索引。在InnoDB中,二级索引中的每条记录都包含行的主 键列,以及为二级索引指定的列。 InnoDB使用这个主键值来搜索聚集索引中的行。如果主键是长 的,则次索引使用更多的空间,因此主键短是有利的。
如果UUID作为数据库主键,在InnoDB引擎下, UUID的无序性可能会引起数据位置频繁变动,影响性能。
- SNOWFLAKE
有些时候我们希望能使用一种简单一些的ID ,并且希望ID能够按照时间有序生成, SnowFlake解决 了这种需求。 SnowFlake是Twitter开源的分布式ID生成算法,结果是一个long型的ID ,long型是8 个字节, 64-bit。其核心思想是:使用41bit作为毫秒数, 10bit作为机器的ID ( 5个bit是数据中心, 5个bit的机器ID ), 12bit作为毫秒内的流水号,最后还有一个符号位,永远是0。如下图所示:
SnowFlake生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID重复,并且效率 较高。经测试SnowFlake每秒能够产生26万个ID。
缺点是
强依赖机器时钟,如果多台机器环境时 钟没同步,或时钟回拨,会导致发号重复或者服务会处于不可用状态。
因此一些互联网公司也基于 上述的方案做了封装,例如百度的uidgenerator (基于SnowFlake)和美团的leaf (基于数据库和 SnowFlake )等。
- 数据库ID表
比如A表分表为A1表和A2表,我们可以单独的创建一个MySQL数据库,在这个数据库中创建一张 表,这张表的ID设置为自动递增,其他地方需要全局唯一ID的时候,就先向这个这张表中模拟插 入一条记录,此时ID就会自动递增,然后我们获取刚生成的ID后再进行A1和A2表的插入。
例如,下面DISTRIBUTE_ID就是我们创建要负责ID生成的表,结构如下:
当分布式集群环境中哪个应用需要获取一个全局唯一的分布式ID的时候,就可以使用代码连接这 个数据库实例,执行如下SQL语句即可。
注意:
- 这里的createtime字段无实际意义,是为了随便插入一条数据以至于能够自动递增ID。
- 使用独立的MySQL实例生成分布式ID ,虽然可行,但是性能和可靠性都不够好,因为你需要代码连接到数据库才能获取到ID ,性能无法保障,另外mysql数据库实例挂掉了,那么就无法获取分布式ID了。
- Redis生成ID
当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于 Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来 实现。
也可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis。可以初始化每台Redis 的值分别是1,2,3,4,5 ,然后步长都是5。各个Redis生成的ID为:
A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25
如何选择主键?
推荐Redis和雪花算法
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}
优点:新的数据可以落在新的存储节点上,如果集群扩容,数据无需迁移。
缺点:数据热点分布不均,数据冷热不均匀,导致节点负荷不均。
- 哈希取模分片
整型的Key可直接对设备数量取模,其他类型的字段可以先计算Key的哈希值,然后再对设备数量 取模。假设有n台设备,编号为0 ~ n-1 ,通过Hash(Key)% n就可以确定数据所在的设备编号。该 模式也称为离散分片。
优点:实现简单,数据分配比较均匀,不容易出现冷热不均,负荷不均的情况。
缺点:扩容时会产生大量的数据迁移,比如从n台设备扩容到n+1 ,绝大部分数据需要重新分配和 迁移。
- 一致性哈希分片
如何理解一致性哈希?
扩容前后一致
采用Hash取模的方式进行拆分,后期集群扩容需要迁移旧的数据。使用一致性Hash算法能够很大 程度的避免这个问题,所以很多中间件的集群分片都会采用一致性Hash算法。
一致性Hash是将数据按照特征值映射到一个首尾相接的Hash环上,同时也将节点(按照IP地址或 者机器名Hash )映射到这个环上。对于数据,从数据在环上的位置开始,顺时针找到的第一个节 点即为数据的存储节点。 Hash环示意图与数据的分布如下:
一致性Hash在增加或者删除节点的时候,受到影响的数据是比较有限的,只会影响到Hash环相邻的节 点,不会发生大规模的数据迁移。
4.4 扩容方案
当系统用户进入了高速增长期时,即便是对数据进行分库分表,但数据库的容量,还有表的数据量也总 会达到天花板。当现有数据库达到承受极限时,就需要增加新服务器节点数量进行横向扩容。
首先来思考一下,横向扩展会有什么技术难度?
数据迁移问题
分片规则改变
数据同步、时间点、数据一致性
遇到上述问题时,我们可以使用以下两种方案:
4.4.1 停机扩容
这是一种很多人初期都会使用的方案,尤其是初期只有几台数据库的时候。停机扩容的具体步骤如下:
站点发布一个公告,例如: “为了为广大用户提供更好的服务,本站点将在今晚00:00-2:00之间升 级,给您带来不便抱歉”;
时间到了,停止所有对外服务;
新增n个数据库,然后写一个数据迁移程序,将原有x个库的数据导入到最新的y个库中。比如分片 规则由%x变为%y;
数据迁移完成,修改数据库服务配置,原来x个库的配置升级为y个库的配置
重启服务,连接新库重新对外提供服务
回滚方案:万一数据迁移失败,需要将配置和数据回滚,改天再挂公告。
优点:简单
缺点:停止服务,缺乏高可用
程序员压力山大,需要在指定时间完成
如果有问题没有及时测试出来启动了服务,运行后发现问题,数据会丢失一部分,难以回滚。
适用场景:
小型网站
大部分游戏
对高可用要求不高的服务
4.4.2 平滑扩容
数据库扩容的过程中,如果想要持续对外提供服务,保证服务的可用性,平滑扩容方案是最好的选择。 平滑扩容就是将数据库数量扩容成原来的2倍,比如:由2个数据库扩容到4个数据库,具体步骤如下:
新增2个数据库
配置双主进行数据同步(先测试、后上线)
数据同步完成之后,配置双主双写(同步因为有延迟,如果时时刻刻都有写和更新操作,会存在不 准确问题)
数据同步完成后,删除双主同步,修改数据库配置,并重启;
此时已经扩容完成,但此时的数据并没有减少,新增的数据库跟旧的数据库一样多的数据,此时还 需要写一个程序,清空数据库中多余的数据,如:
User1去除 uid % 4 = 2的数据;
User3去除 uid % 4 = 0的数据;
User2去除 uid % 4 = 3的数据;
User4去除 uid % 4 = 1的数据;
平滑扩容方案能够实现n库扩2n库的平滑扩容,增加数据库服务能力,降低单库一半的数据量。其核心 原理是:成倍扩容,避免数据迁移。
优点:
扩容期间,服务正常进行,保证高可用
相对停机扩容,时间长,项目组压力没那么大,出错率低
扩容期间遇到问题,随时解决,不怕影响线上服务
可以将每个数据库数据量减少一半
缺点:
程序复杂、配置双主同步、双主双写、检测数据同步等
后期数据库扩容,比如成千上万,代价比较高
适用场景:
大型网站
对高可用要求高的服务
分库分表
ShardingSphere
Apache ShardingSphere 是一款开源的分布式数据库生态项目,由 JDBC 和 Proxy 两款产品组成。 其核心采用可插拔架构,通过组件扩展功能。 对上以数据库协议及 SQL 方式提供诸多增强功能,包括数据分片、访问路由、数据安全等;对下原生支持 MySQL、PostgreSQL、SQL Server、Oracle 等多种数据存储引擎。 Apache ShardingSphere 项目理念,是提供数据库增强计算服务平台,进而围绕其上构建生态。 充分利用现有数据库的计算与存储能力,通过插件化方式增强其核心能力,为企业解决在数字化转型中面临的诸多使用难点,为加速数字化应用赋能。
官方文档:https://shardingsphere.apache.org/document/current/cn/overview/
官方代码示例:https://github.com/apache/shardingsphere/tree/master/examples
- 分布式事务:https://github.com/apache/shardingsphere/tree/master/examples/shardingsphere-jdbc-example/single-feature-example/transaction-example
- 分片:https://github.com/apache/shardingsphere/tree/master/examples/shardingsphere-jdbc-example/single-feature-example/sharding-example
- 读写分离: https://github.com/apache/shardingsphere/tree/master/examples/shardingsphere-jdbc-example/single-feature-example/readwrite-splitting-example
分布式事务
理论
XA (强一致性)
XA是由X/Open组织提出的分布式事务的规范,是基于两阶段提交协议。 XA规范主要定义了全局 事务管理器( TM )和局部资源管理器( RM )之间的接口。目前主流的关系型数据库产品都是实现 了XA接口。
XA之所以需要引入事务管理器,是因为在分布式系统中,从理论上讲两台机器理论上无法达到一 致的状态,需要引入一个单点进行协调。由全局事务管理器管理和协调的事务,可以跨越多个资源 (数据库)和进程。
事务管理器用来保证所有的事务参与者都完成了准备工作(第一阶段)。如果事务管理器收到所有参 与者都准备好的消息,就会通知所有的事务都可以提交了(第二阶段)。 MySQL 在这个XA事务中 扮演的是参与者的角色,而不是事务管理器。
TCC模式(最终一致性)
TCC ( Try-Confirm-Cancel )的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。 TCC 是服务化的两阶段 编程模型,其 Try、Confirm、Cancel 3 个方法均由业务编码实现:
TCC 模式需要⽤户根据⾃⼰的业务场景实现 Try 、Confirm 和 Cancel 三个操作;事务发起⽅在⼀阶段 执⾏ Try ⽅式,在⼆阶段提交执⾏ Confirm ⽅法,⼆阶段回滚执⾏ Cancel ⽅法。
TCC 三个⽅法描述:
- Try:资源的检测和预留;
- Confirm:执⾏的业务操作提交;要求 Try 成功 Confirm ⼀定要能成功;
- Cancel:预留资源释放。
业务模型分 2 阶段设计:
⽤户接⼊ TCC ,最重要的是考虑如何将⾃⼰的业务模型拆成两阶段来实现。
以“扣钱”场景为例,在接⼊ TCC 前,对 A 账户的扣钱,只需⼀条更新账户余额的 SQL 便能完成
update account set balanece= balanece-30 where id =1;
但是在接⼊ TCC 之后,⽤户就需要考虑如何将原来⼀步就能完成的扣钱操作,拆成两阶段,实现成三个 ⽅法,并且保证⼀阶段 Try 成功的话 ⼆阶段 Confirm ⼀定能成功。
Try ⽅法作为⼀阶段准备⽅法,需要做资源的检查和预留。在扣钱场景下, Try 要做的事情是就是 检查账户余额是否充⾜,预留转账资⾦,预留的⽅式就是冻结 A 账户的 转账资⾦。 Try ⽅法执⾏之后, 账号 A 余额虽然还是 100,但是其中 30 元已经被冻结了,不能被其他事务使⽤。
⼆阶段 Confirm ⽅法执⾏真正的扣钱操作。 Confirm 会使⽤ Try 阶段冻结的资⾦,执⾏账号扣款。 Confirm ⽅法执⾏之后,账号 A 在⼀阶段中冻结的 30 元已经被扣除,账号 A 余额变成 70 元 。
如果⼆阶段是回滚的话,就需要在 Cancel ⽅法内释放⼀阶段 Try 冻结的 30 元,使账号 A 的回到 初始状态, 100 元全部可⽤。
优点:
- 使用最终一致性,避免了XA的强阻塞
缺点:
- 对业务代码侵入性很强。业务代码的改造量很大
消息队列模式(最终一致性)
消息队列的方案最初是由 eBay 提出,基于TCC模式,消息中间件可以基于 Kafka、 RocketMQ 等 消息队列。此方案的核心是将分布式事务拆分成本地事务进行处理,将需要分布式处理的任务通过 消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或MQ中间件,再通过业务规 则人工发起重试。
下面描述下事务的处理流程:
- 步骤1 :事务主动方处理本地事务。
事务主动方在本地事务中处理业务更新操作和MQ写消息操作。
- 步骤 2 :事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。
事务主动方主动写消息到MQ ,事务消费方接收并处理MQ中的消息。
- 步骤 3 :事务被动方通过MQ中间件,通知事务主动方事务已处理的消息,事务主动方根据反馈结果提交或回滚事务。
为了数据的一致性,当流程中遇到错误需要重试,容错处理规则如下:
- 当步骤 1 处理出错,事务回滚,相当于什么都没发生。
- 当步骤 2 处理出错,由于未处理的事务消息还是保存在事务发送方,可以重试或撤销本地业务操作。
- 如果事务被动方消费消息异常,需要不断重试,业务处理逻辑需要保证幂等。
- 如果是事务被动方业务上的处理失败,可以通过MQ通知事务主动方进行补偿或者事务回滚。
- 如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。
Saga模式(最终一致性)
Saga这个概念源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的一篇数据库论文Sagas ,一 个Saga事务是一个有多个短时事务组成的长时的事务。 在分布式事务场景下,我们把一个Saga分 布式事务看做是一个由多个本地事务组成的事务,每个本地事务都有一个与之对应的补偿事务。在 Saga事务的执行过程中,如果某一步执行出现异常, Saga事务会被终止,同时会调用对应的补偿
事务完成相关的恢复操作,这样保证Saga相关的本地事务要么都是执行成功,要么通过补偿恢复 成为事务执行之前的状态。(自动反向补偿机制)。
Saga 事务基本协议如下:
- 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction)Ti 组成。
每个 Ti 都有对应的幂等补偿动作 Ci ,补偿动作用于撤销 Ti 造成的结果。
Saga是一种补偿模式,它定义了两种补偿策略:
- 向前恢复( forward recovery):对应于上面第一种执行顺序,发生失败进行重试,适用于必须要成功的场景。
- 向后恢复( backward recovery):对应于上面提到的第二种执行顺序,发生错误后撤销掉之前所 有成功的子事务,使得整个 Saga 的执行结果撤销。
Saga 的执行顺序有两种,如上图:
事务正常执行完成: T1, T2, T3, …, Tn ,
例如:减库存(T1) ,创建订单(T2) ,支付(T3) ,依次有序完 成整个事务。
事务回滚: T1, T2, …, Tj, Cj,…, C2, C1 ,其中 0 < j < n ,
例如:减库存(T1) ,创建订单(T2) ,支付(T3) ,支付失败,支付回滚(C3) ,订单回滚(C2) ,恢复库存(C1)。
实战
Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
https://seata.io/zh-cn/docs/user/quickstart.html
- AT模式(推荐)
官方at模式文档:https://seata.io/zh-cn/docs/dev/mode/at-mode.html
AT 模式是⼀种⽆侵⼊的分布式事务解决⽅案。在 AT 模式下,⽤户只需关注⾃⼰的“业务 SQL”,⽤户的 “业务 SQL” 作为⼀阶段, Seata 框架会⾃动⽣成事务的⼆阶段提交和回滚操作。
那么如何做到对业务的⽆侵⼊的呢?
- ⼀阶段
在⼀阶段, Seata 会拦截“业务 SQL”,⾸先解析 SQL 语义,找到“业务 SQL”要更新的业务数 据,在业务数据被更新前,将其保存成“before image”,然后执⾏“业务 SQL”更新业务数据,在业 务数据更新之后,再将其保存成“after image”,最后⽣成⾏锁。以上操作全部在⼀个数据库事务 内完成,这样保证了⼀阶段操作的原⼦性。
- ⼆阶段
- 提交
⼆阶段如果是提交的话,因为“业务 SQL”在⼀阶段已经提交⾄数据库, 所以 Seata 框架只需将⼀阶段保存的快照数据和⾏锁删掉,完成数据清理即可。
- 回滚
⼆阶段如果是回滚的话, Seata 就需要回滚⼀阶段已经执⾏的“业务 SQL”,还原业务数 据。回滚⽅式便是⽤“before image”还原业务数据;但在还原前要⾸先要校验脏写,对⽐“数据库当前业务数据”和 “after image”,如果两份数据完全⼀致就说明没有脏写,可以还原业 务数据,如果不⼀致就说明有脏写,出现脏写就需要转⼈⼯处理。
AT 模式的⼀阶段、⼆阶段提交和回滚均由 Seata 框架⾃动⽣成,⽤户只需编写“业务 SQL”,便能轻松接⼊分布式事务, AT 模式是⼀种对业务⽆任何侵⼊的分布式事务解决⽅案
运维工具
Yearning
https://github.com/cookieY/Yearning
SQL审核平台
canal
https://github.com/alibaba/canal
canal [kə’næl],译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费
早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。
基于日志增量订阅和消费的业务包括
- 数据库镜像
- 数据库实时备份
- 索引构建和实时维护(拆分异构索引、倒排索引等)
- 业务 cache 刷新
- 带业务逻辑的增量数据处理(广告由关闭变成开启-> consumer里面把当前广告加入索引)
datax
https://github.com/alibaba/DataX
概述
DataX 是一个异构数据源离线同步工具,致力于实现包括关系型数据库(MySQL、Oracle等)、HDFS、Hive、ODPS、HBase、FTP等各种异构数据源之间稳定高效的数据同步功能。
- 设计理念
为了解决异构数据源同步问题,DataX将复杂的网状的同步链路变成了星型数据链路,DataX作为中间传输载体负责连接各种数据源。当需要接入一个新的数据源的时候,只需要将此数据源对接到DataX,便能跟已有的数据源做到无缝数据同步。
- 当前使用现状
DataX在阿里巴巴集团内被广泛使用,承担了所有大数据的离线同步业务,并已持续稳定运行了6年之久。目前每天完成同步8w多道作业,每日传输数据量超过300TB。
架构设计
DataX本身作为离线数据同步框架,采用Framework + plugin架构构建。将数据源读取和写入抽象成为Reader/Writer插件,纳入到整个同步框架中。
- Reader:Reader为数据采集模块,负责采集数据源的数据,将数据发送给Framework。
- Writer: Writer为数据写入模块,负责不断向Framework取数据,并将数据写入到目的端。
- Framework:Framework用于连接reader和writer,作为两者的数据传输通道,并处理缓冲,流控,并发,数据转换等核心技术问题。
最佳实践
Mysql安装测试
- ChatGPT模拟
Prompt如下:
- docker安装
https://hub.docker.com/_/mysql
- 作者:鹤涵
- 链接:https://www.hehanwang.com/article/mysql
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。