一文读懂mysql-万字长文,肝就完了

Posted 双斜杠少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文读懂mysql-万字长文,肝就完了相关的知识,希望对你有一定的参考价值。

本文是加深了解mysql 的笔记,比较长,大家可以收藏慢慢看。

开胃:先串一下 mysql 逻辑架构

查询语句执行过程:

  • 先检查该语句是否有权限,如果没有权限,直接返回错误信息。
  • 如果有权限,在 MySQL8.0 版本以前,会先查询缓存。
  • 如果没有缓存,分析器进行词法分析,提取 sql 语句select等的关键元素。然后判断sql 语句是否有语法错误,比如关键词是否正确等等。
  • 优化器进行确定执行方案
  • 进行权限校验,如果没有权限就直接返回错误信息,如果有权限就会调用数据库引擎接口,返回执行结果。

正文开始

1.存储引擎

1.1 innodb

  • 提供了对数据库 ACID事务 的支持

  • 提供了 行级锁和外键的约束。InnoDB的行锁也是分为行级 「共享读锁(S锁)「和」排它写锁(X锁)」,原理特点和MyISAM的表级锁两种模式是一样的。

  • 设计的目标就是 处理大数据容量 的数据库系统

  • 会出现死锁,但是 InnoDB的支持的并发性能比MyISAM好行锁的粒度最小,一定的方法和措施可以解决死锁的发生,极大的发挥InnoDB的性能

  • InnoDB中引入了 间隙锁 的概念来决解出现 幻读 的问题,也引入事务的特性,通过事务的 四种隔离级别 ,来降低锁冲突,提高并发性能

  • Innodb则是聚集索引,索引段和数据段在同一个文件中的不同段,查到索引后可以直接取出数据。

1.1.1 加锁 for update

若想显式的给表加行级读锁和写锁,可以执行下面的sql语句

// 给查询sql显示添加读锁

select ... lock in share mode;

// 给查询sql显示添加写锁

select ... for update

for update ,使用非索引查询,直接就是使用的表级锁」 「sql是否执行索引还得看Mysql的执行计划,对于一些小表的操作,可能就直接使用全表扫描」

for update 总结:

  1. 执行非索引条件查询执行的是表锁。
  2. 执行索引查询是否是加行锁,还得看Mysql的执行计划是否用到索引。可以通过explain关键字来查看。
  3. 用普通键索引的查询,遇到索引值相同的,也会对其他的操作数据行的产生影响(加锁)。

1.1.2 锁竞争 (死锁)

死锁在InnoDB中才会出现死锁MyISAM是不会出现死锁,因为MyISAM支持的是表锁一次性获取了所有的锁,其它的线程只能排队等候。InnoDB默认支持行锁,获取锁是分步的,并不是一次性获取所有的锁,因此在锁竞争的时候就会出现死锁的情况。

1.1.3 解决死锁

要解决死锁问题,在程序的设计上,当发现程序有高并发的访问某一个表时,尽量对该表的执行操作串行化,或者锁升级,一次性获取所有的锁资源。

然后也可以设置参数innodb_lock_wait_timeout,超时时间,并且将参数innodb_deadlock_detect 打开,当发现死锁的时候,自动回滚其中的某一个事务。

1.1.4 间隙锁

  • 主键索引 不会增加间隙锁
  • 范围查询是会增加间隙锁
  • 使用不存在的检索条件是也会加上间隙锁

当我们使用范围条件查询而不是等值条件查询的时候,InnoDB就会给符合条件的范围索引加锁,在条件范围内的记录就叫做"间隙(GAP)"

Mysql中的不可重复是已经解决了幻读问题,它通过引入间隙锁的实现来解决幻读,通过给符合条件的间隙加锁,防止再次查询的时候出现新数据产生幻读的问题

比如在区间(1,3]U[3,5)之间加了锁,是不能够新增数据行,新增num=2和num=4会失败失败,但是在这个区间以外的数据行是没有加锁的,可以新增数据行

根据索引的有序性,普通索引是可以出现重复值,Mysql在满足where条件的情况下,给(1,3]U[3,5)区间加上了锁不允许插入num=3的数据行,这样就解决了幻读。

1.1.5 MVCC(多版本并发控制)

多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发读写,写读,写写都要阻塞
引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了 InnoDB 的并发度。

  • 实现:

在内部实现中,与 Postgres 在数据行上实现多版本不同,InnoDB 是在 undolog 中实现的,通过 undolog 可以找回数据的历史版本。找回的数据历史版本可以提供给用户读。(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在 InnoDB 内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。

一致性视图只会在 RR(REPEATABLE-READ) 与 RC(READ-COMMITTED) 下才会生成,对于 RR 来说,一致性视图会在第一个查询语句的时候生成。而对于 RC 来说,每个查询语句都会重新生成视图。

当前读与快照读

MySQL 使用 MVCC 机制,可以读取之前版本数据。这些旧版本记录不会且也无法再去修改,就像快照一样。所以我们将这种查询称为快照读

当然并不是所有查询都是快照读,select .... for update/ in share mode这类 加锁查询只会查询当前记录最新版本数据。我们将这种查询称为当前读。

1.1.6 扩展:innodb 怎么保证必有主键?

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

1.2 MyIASM引擎(原本Mysql的默认引擎)

  • 不提供事务的支持,也不支持行级锁和外键
  • 支持表级锁分为 「共享读锁」「排它写锁」。获取的读锁的线程对该数据行只能读,不能修改,其它线程也只能对该数据行加读锁。获取到写锁的线程对该数据行既能读也能写,对其他线程对该数据行的读写具有排它性。
  • 读写有序,因此支持的并发性能低下,MyISAM可以理解为串行化操作。
  • MyISAM是非聚集索引,索引和数据的存储是分开的(不同的文件),索引中最终检索到的是数据的物理地址偏移量

MyISAM中默认写优先于去操作,因此MyISAM一般不适合运用于大量读写操作的程序中。

// 显式的添加表级读锁

LOCK TABLE 表名 READ

// 显示的添加表级写锁

LOCK TABLE 表名 WRITE

// 显式的解锁(当一个事务commit的时候也会自动解锁)

unlock tables;

「当一个线程获取到表级写锁后,只能由该线程对表进行读写操作,别的线程必须等待该线程释放锁以后才能操作」

MyISAM存储引擎中,虽然读写操作是串行化的,但是它也支持并发插入,这个需要设置内部变量concurrent_insert的值

1.2.1 锁调度

MyISAM存储引擎中,「假如同时一个读请求,一个写请求过来的话,它会优先处理写请求」,因为MyISAM存储引擎中认为写请求比读请求重要。

这样就会导致,「假如大量的读写请求过来,就会导致读请求长时间的等待,或者"线程饿死",因此MyISAM不适合运用于大量读写操作的场景」,这样会导致长时间读取不到用户数据,用户体验感极差。

当然可以通过设置 low-priority-updates参数,设置请求链接的优先级,使得Mysql优先处理读请求。

1.3 MEMORY引擎

所有的数据都在内存中,数据的处理速度快,但是安全性不高

2. 索引

  • Primary Key(主键索引) Primary Key(primary key = unique + not null 列唯一)约束均为列或列集合中提供了唯一性的保证,Primary Key是拥有自动定义的Unique约束,每个表只能有一个Primary Key约束
  • Normal(普通索引) 表示普通索引,大多数情况下都可以使用
  • Unique(唯一索引) 表示唯一的,不允许重复的索引,Unique(要求列值唯一),每个表中可以有多个Unique约束
  • FullText(文本索引) 表示全文收索,在检索长文本的时候,效果最好,如果就一两行字的,普通的 INDEX 即可
  • SPATIAL(空间索引) 空间索引是对空间数据类型的字段建立的索引,MYSQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING、POLYGON。创建空间索引的列,必须将其声明为NOT NULL,空间索引只能在存储引擎为MYISAM的表中创建

索引是 数据结构+算法

  • 帮助Mysql高效获取数据的数据结构,
  • 实现了高级查找算法的数据结构
  • 索引一般以文件的形式存在磁盘上

mysql 的存储文件如下:

在MyISAM中,索引(含叶子节点)存放在单独的.myi文件中,叶子节点存放的是数据的物理地址偏移量(通过偏移量访问就是随机访问,速度很快)

innodb 存储文件如下:

  • .frm 文件:存储表结构

  • .idb 文件:index + data (存储索引和数据)
    mysql 的 IO 流程:

磁盘-内核内存-应用内存

2.1 索引结构

2.1.1 btree(B+树)

查询方式:

  • 主键索引区:PI(关联保存的时数据的地址)按主键查询,
  • 普通索引区:si(关联的id的地址,然后再到达上面的地址)。所以按主键查询,速度最快

B+tree性质:

  • 1.)n棵子tree的节点包含n个关键字,不用来保存数据而是保存数据的索引。

  • 2.)所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

  • 3.)所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。

  • 4.)B+ 树中,数据对象的插入和删除仅在叶节点上进行

  • 5.)B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点

2.1.2 二叉、红黑、B对比

  • 索引设计

.frm 文件:存储表结构

.idb 文件:index + data (存储索引和数据)

  • IO 流程:

磁盘-内核内存-应用内存

磁头活塞运动,盘片转动(寻道),每次寻道 为10ms。磁盘局部性原理,每次读取为需要的数据和相邻数据 (预读),每次预读一页(4k)mysql 中 innodb 中默认的一页等于系统的 4页 (16k)。所以mysql 为 寻道(10ms)-旋转-预读(4K*k)索引,有些数据结构存在的价值是提高检索效率 (二叉树、红黑树、b树、b+树)

  • 数据结构:

    从数组+链表进行各种演变(面向对象的理解)

    算法:逻辑上的约束

二叉树(左小右大)(二分思想)

理论性的数据结构。特定场景,会失去特性退化为一个链表

红黑树(二叉树优化):

  • 1.节点是红色或黑色,
  • 2.跟节点为黑色,
  • 3,新插入的节点默认红色
  • 4,每个红色节点的两个字节点都是黑色(不能有连续的两个红色节点),
  • 5从跟节点到叶子结点的路径黑色节点个数相同

变色,自旋

在特定场景时,索引效率与树深度有关系,树深度越深(树深log2n),IO次数也多。因为每次读取16k数据,如果红黑树的话会比较浪费空间。在大规模数据存储的时候,红黑树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况

B树:

树的高度降低,又矮又胖的二分结构b树节点下有数据,所以同样的存储大小(16k)中存储的节点多少,会受到节点下数据大小的影响。节点下数据越大,B树的度会越少。每次IO读取数据大小相同,节点变大,就增加了磁盘IO次数(磁盘IO一次读出的数据量大小是固定的,单个数据变大,每次读出的就少,IO次数增多,一次IO多耗时),而B+树除了叶子节点其它节点并不存储数据,节点小,磁盘IO次数就少

B+树

跟B树相比,只有叶子结点存放数据减小枝干结点的空间大小,可以增大树的度。innodb中页的默认大小是16KB,如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,且叶子结点间有链指针,方便数据范围查找

B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的,链表连着的。那么B+树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。

非聚簇索引叶子结点为什么不直接存储数据?

  • 增大了查询寻址的次数。
  • 存储空间,为了减少磁盘数据冗余
  • 数据一致性(为了保证数据一致性)。

为什么推荐使用整型自增的主键?

  • 整型:方便b+树的key 值比较排序。

  • 自增:方便数据再内存的顺序写入。

2.2 hash

HASH(key,value)这种方式对 范围查询支持得不是很好 。底层的数据结构是**哈希表,可以理解为用hashMap存储数据。hash 索引结构的特殊性,其 检索效率非常高,索引的检索可以一次定位,不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引

  • 键值唯一时,哈希索引明显有绝对优势
  • 无法完成范围查询检索
  • 无法利用索引完成排序,以及like,这种模糊搜索
  • 不支持多列联合索引查询
  • 如果存在 大量hash碰撞问题,索引效率极低

2.2.1 Hash本身由于其特殊性也带来了很多限制和弊端,

(1)Hash 索引仅仅能满足”=”,”IN”和”<=>”查询不能使用范围查询。

由于 Hash 索引比较的是进行 Hash 运算之后的 Hash 值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为经过相应的 Hash 算法处理之后的 Hash 值的大小关系,并不能保证和Hash运算前完全一样。

(2)Hash 索引无法被用来数据的排序操作

由于 Hash 索引中存放的是经过 Hash 计算之后的 Hash 值,而且Hash值的大小关系并不一定和 Hash 运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算;

(3)Hash 索引不能利用部分索引键查询

对于组合索引,Hash 索引在计算 Hash 值的时候是组合索引键合并后再一起计算 Hash 值,而不是单独计算 Hash 值,所以通过组合索引的前面一个或几个索引键进行查询的时候,Hash 索引也无法被利用

(4)Hash 索引在任何时候都不能避免表扫描

  • hash冲突

前面已经知道,Hash 索引是将索引键通过 Hash 运算之后,将 Hash运算结果的 Hash 值和所对应的行指针信息存放于一个 Hash 表中,由于不同索引键存在相同 Hash 值,所以即使取满足某个 Hash 键值的数据的记录条数,也无法从 Hash 索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。

(5)Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。

对于选择性比较低的索引键(hash冲突),如果创建 Hash 索引,那么将会存在大量记录指针信息存于同一个 Hash 值相关联。这样要定位某一条记录时就会非常麻烦,会浪费多次表数据的访问,而造成整体性能低下。

2.3 索引类型

  • 聚簇索引
    主键索引(聚簇索引,数据和索引放在一起)整条数据和叶子结点在一起。聚集索引是指索引和数据的逻辑排列顺序与实际物理存储顺序一致,新华字典就是典型的聚集索引,字(叶子索引)和释意(数据)靠在一起,且按一定顺序排列的。
  • 非聚簇索引
    非主键索引(非聚簇索引,数据和索引不放在一起)叶子结点存储的数据为主键索引的id,并没有存储全部数据。查询到主键id后,再去查询主键索引获取全部数据。索引单独放在一块区域,并且叶子节点存放的是数据的地址偏移量。
  • 组合索引
    把多个数据字段连起来组成B+树的key。 查询遵循最左原则,带头大哥索引(第一个索引字段)一定得命中,后续索引可以进行范围查找

MyISAM是非聚集索引,而Innodb则是聚集索引。MyISAM索引和数据的存储是分开的(不同的文件),索引中最终检索到的是数据的物理地址偏移量。而InnoDB中,索引段和数据段在同一个文件中的不同段,查到索引后可以直接取出数据。

2.4 索引使用

2.4.1 在什么情况下建立索引呢?

一般来说,在WHERE和JOIN中出现的列需要建立索引,但也不完全如此,因为MySQL只对<,<=,=,>,>=,BETWEEN,IN,以及使用 LIKE 谓词时,只有前方一致的匹配才能用到索引。

实际的情况,需要控制IN查询的范围。

  1. IN 的条件过多,会导致索引失效,走索引扫描
  2. IN 的条件过多,返回的数据会很多,可能会导致应用堆内内存溢出。

2.4.2 Mysql索引选取规则

    1. 寻找可能的(可选)索引:根据用户的WHERE条件,查看每个字段是否匹配某个索引,如果匹配,就把这个索引加入待选列表中
    1. 索引对应的扫描行数最少, 如果order by 后字段有索引,则使用order by 的字段索引
    1. 如果没找到可用索引的话,再考察查询字段,也就是Select之后Where之前的那些字段。寻找查询字段所匹配的索引,并得到一个可用索引结果集。根据最小估算扫描行数优先原则,可以得到最优的索引
    1. 如果根据最终选择的索引估算出的扫描行数占据了表的很大一部分比例那么Mysql优化器可能会放弃使用该索引,而退化为使用全表扫描。这是因为索引出来的数据量很大,需要频繁的改变文件读取指针去获取数据块,效果还不如从头到位把整个表都扫描一边,也省去了去查找索引和频繁重定向读取指针带来的开销。

2.4.3 何时会全表扫描

  • 目标数据表太小了,并且每行很短的情况
  • 查询条件字段(WHERE后)没有匹配到索引。也不是说匹配不到就一定会全表扫描,见下文默认索引选择算法
  • 查询条件中的字段与某个常量比较时(就比如where age > 8),并且使用这个常量值与对应索引筛选出的记录数占了总数的大部分。优化器认为扫这么大的数据还不如扫全表了,所以选择了扫描全表。当且仅当使用了等于号只要该列有匹配的索引,一定会命中索引。如果不是等于号则会退化到扫描全表
  • 查询语句匹配到的索引对应的基数太小,也就是索引列的取值太少(例如:男、女)所以该索引中每个索引值对应的目标记录数很大,整个过程总体的查找速度还未必有全表扫描来得快。所以选择了扫描全表。

2.4.4 默认索引选择算法

** 默认索引选择算法**——当查询语句的搜索条件没有命中任何索引时,Mysql索引优化器会考量查询语句中的目标字段(select后面,where前面的部分),目标字段除去主键外,如果恰好是某个索引(包括组合索引)对应列的子集,那么该索引也会被使用。如果满足的索引有多个,将会使用索引记录数最少的索引。这个算法在[3]中得到了旁证。

2.4.5 索引设计建议

  • 索引不是定义的越多越好,查询条件比较多的情况,避免为每个字段创建索引,只需要创建一个联合索引即可
  • 创建联合索引时,值差异化大的列放在前面,而不是那些取值种类很少的列
  • 使用 EXPLAIN 进行sql执行分析,确定索引的建立
  • Mysql在执行SQL语句时,一个表只可能使用一个索引(开启了Index Merge的情况除外)

2.5.5 sql的编写需要注意优化

1.使用limit对查询结果的记录进行限定

2.避免select *,将需要查找的字段列出来

3.使用连接(join)来代替子查询

4.拆分大的 delete或 insert语句

5.可通过开启慢查询日志来找出较慢的SQL

6.不做列运算:SELECT id WHERE age + 1 = 10,任何对列的操作都将导致表扫描,它包括数据库教程函数、计算表达式等等,查询时要尽可能将操作移至等号右边

7.sql语句尽可能简单:一条sql只能在一个cpu运算;**大语句拆小语句,减少锁时间;**一条大sql可以堵死整个库

8.OR改写成IN:OR的效率是n级别,IN的效率是log(n)级别,in的个数建议控制在200以内

9.不用函数和触发器,在应用程序实现

10.避免%xxx式,模糊查询

11.尽量减少少用 JOIN

12.使用同类型进行比较,比如用’123’和’123’比,123和123比

13.尽量避免在WHERE子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描

14.对于连续数值,使用BETWEEN不用IN:SELECT id FROM t WHERE num BETWEEN 1 AND 5

15.列表数据不要拿全表,要使用LIMIT来分页,每页数量也不要太大

3. 锁

当前节内容 可以参照 基础-多线程-线程安全-锁章节内容

在我们平时执行select语句的时候就会隐式的加读锁,执行增、删、改的操作时就会隐式的执行加写锁。

3.1 粒度划分

  • 表锁

  • 页锁

  • 行锁

3.1.1 表锁(MyISAM):

粒度最大的锁 ,开销小,加锁快,不会出现死锁

由于粒度太大,因此造成锁的冲突几率大,并发性能低。Mysql的「MyISAM储存引擎就支持表锁」(参照存储引擎MyISAM),MyISAM的表锁模式有两种:「表共享读锁**」和「独占写**锁」。

当一个线程获取到MyISAM表的读锁的时候,会阻塞其他用户对该表的写操作,但是不会阻塞其它用户对该用户的读操作。相反的,当一个线程获取到MyISAM表的写锁的时候,就会阻塞其它用户的读写操作对其它的线程具有排它性。

3.1.2 页锁

页锁的粒度是 介于行锁和表锁之间的一种锁,因为页锁是在BDB中支持的一种锁机制,也 很少没人提及和使用,所以这里制作概述,不做详解。

3.1.3 行锁(innodb)

粒度最小的锁机制,行锁的加锁开销性能大,加锁慢,并且会出现死锁,行锁的锁冲突的几率低,并发性能高

行锁是 InnoDB默认的支持的锁机制,MyISAM不支持行锁,这个也是InnoDB和MyISAM的区别之一。

行锁在使用的方式上可以划分为:「共享读锁(S锁)「和」排它写锁(X锁)」

  • 共享读锁:当一个事务对Mysql中的一条数据行加上了S锁,当前事务不能修改该行数据只能执行读操作,其他事务只能对该行数据加S锁不能加X锁。

  • 排它写锁: 若是一个事务对一行数据加了X锁,该事务能够对该行数据执行读和写操作,其它事务不能对该行数据加任何的锁,既不能读也不能写。

3.2 使用方式

3.2.1 共享锁

共享读锁,「当一个线程获取到表级读锁后,该线程只能读取数据不能修改数据,其它线程也只能加读锁,不能加写锁」

3.2.2 排它锁

独占写锁:「当一个线程获取到表级写锁后,只能由该线程对表进行读写操作,别的线程必须等待该线程释放锁以后才能操作」

3.3 思想划分

3.3.1 乐观锁

mysql 乐观锁需要程序员自己去实现的锁机制」,最常见的乐观锁实现就锁机制是 「使用版本号实现」和 "通过时间戳"实现。乐观锁最常采用的是CAS算法,会有ABA 问题、只能做一个原子操作问题和循环时间过长问题。乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升

3.3.2 悲观锁

Mysql的 「悲观锁的实现是基于Mysql自身的锁机制实现,而乐观锁需要程序员自己去实现的锁机制」,最常见的乐观锁实现就锁机制是 「使用版本号实现」

4 分布式

4.1 主从结构

4.1.1 为什么使用主从结构

  • 1、在业务复杂的系统中,让主库负责写,从库负责读,这样,即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运作。
  • 2、做数据的热备
  • 3、架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能

4.1.2 什么是mysql的主从复制?

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

原理:

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

也就是说:

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

4.1.3 主从形式

  • 一主一从

  • 主主复制

  • 一主多从

  • 多主一从

  • 联级复制

主从复制分了五个步骤进行:

  • 步骤一:主库的更新事件(update、insert、delete)被写到binlog
  • 步骤二:从库发起连接,连接到主库。
  • 步骤三:此时主库创建一个binlog dump thread,把binlog的内容发送到从库。
  • 步骤四:从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到 relay log,传送过来的信息,会记录到master.info中
  • 步骤五:slave还会创建一个SQL线程,从relay log里面读取内容,从ExecMasterLog_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db。默认情况下,已经应用过的relay 会自动被清理purge

4.2 主从延迟

4.2.1 主从同步延迟的原因

一个服务器开放N个链接给客户端来连接的,这样有会有大并发的更新操作, 但是从服务器的里面读取binlog的线程仅有一个,当某个SQL在从服务器上执行的时间稍长 或者由于某个SQL要进行锁表就会导致,主服务器的SQL大量积压,未被同步到从服务器里。这就导致了主从不一致, 也就是主从延迟。

4.2.2 主从同步延迟的解决办法

    1. 业务的持久化层的实现采用分库架构,mysql服务可平行扩展,分散压力。
    1. 选择更好的硬件设备作为slave。
    1. (一主多从)把一台从服务器当作为备份使用, 而不提供查询, 那边他的负载下来了, 执行relay log 里面的SQL效率自然就高了。
    1. (一主多从)单个库读写分离,一主多从,主写从读,分散压力。这样从库压力比主库高,保护主库。
    1. 服务的基础架构在业务和mysql之间加入memcache或者redis的cache层。降低mysql的读压力。(类似 3 )
    1. 不同业务的mysql物理上放在不同机器,分散压力。(类似 1)

mysql5.7之后使用MTS并行复制技术,永久解决复制延时问题:https://blog.csdn.net/w892824196/article/details/106937133

4.3 分区

4.3.1 什么是分区?

分区和分表相似,都是按照规则分解表。不同在于分表将大表分解为若干个独立的实体表,而分区是将数据分段划分在多个位置存放,可以是同一块磁盘也可以在不同的机器。分区后,表面上还是一张表,但数据散列到多个位置了。app读写的时候操作的还是大表名字,db自动去组织分区的数据。

mysql数据库中的数据是以文件的形势存在磁盘上的,默认放在/mysql/data下面(可以通过my.cnf中的datadir来查看),一张表主要对应着三个文件,一个是frm存放表结构的,一个是myd存放表数据的,
一个是myi存表索引的。如果一张表的数据量太大的话,那么myd,myi就会变的很大,查找数据就会变的很慢,这个时候我们可以利用mysql的分区功能,
在物理上将这一张表对应的三个文件,分割成许多个小块,这样呢,我们查找一条数据时,就不用全部查找了,只要知道这条数据在哪一块,然后在那一块找就行了。
如果表的数据太大,可能一个磁盘放不下,这个时候,我们可以把数据分配到不同的磁盘里面去

分区的二种方式

a,横向分区

什么是横向分区呢?就是横着来分区了,举例来说明一下,假如有100W条数据,分成十份,前10W条数据放到第一个分区,第二个10W条数据放到第二个分区,依此类推。也就是把表分成了十分,根用merge来分表,有点像哦。取出一条数据的时候,这条数据包含了表结构中的所有字段,也就是说横向分区,并没有改变表的结构。

b,纵向分区

什么是纵向分区呢?就是竖来分区了,举例来说明,在设计用户表的时候,开始的时候没有考虑好,而把个人的所有信息都放到了一张表里面去,这样这个表里面就会有比较大的字段,如个人简介,而这些简介呢,也许不会有好多人去看,所以等到有人要看的时候,在去查找,分表的时候,可以把这样的大字段,分开来。

4.3.2 使用分区

使用起来和不分区是一样的,看起来只有一个数据库,其实有多个分区文件,比如我们要插入一条数据,不需要指定分区,MySQL会自动帮我们处理,对应用是透明的无需修改代码

4.3.3 分区的好处是:

1.可以让单表存储更多的数据

2.分区表的数据更容易维护,可以通过清楚整个分区批量删除大量数据,也可以增加新的分区来支持新插入的数据。另外,还可以对一个独立分区进行优化、检查、修复等操作

3.部分查询能够从查询条件确定只落在少数分区上,速度会很快

4.分区表的数据还可以分布在不同的物理设备上,从而高效利用多个硬件设备

5.可以使用分区表赖避免某些特殊瓶颈,例如InnoDB单个索引的互斥访问、ext3文件系统的inode锁竞争

6.可以备份和恢复单个分区

4.3.4 分区的限制和缺点:

1.一个表最多只能有1024个分区

2.如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来

3.分区表无法使用外键约束

4.NULL值会使分区过滤无效

5.所有分区必须使用相同的存储引擎

4.3.5 分区的类型:

1.RANGE分区:基于属于一个给定连续区间的列值,把多行分配给分区

2.LIST分区:类似于按RANGE分区,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择

3.HASH分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MySQL中有效的、产生非负整数值的任何表达式

4.KEY分区:类似于按HASH分区,区别在于KEY分区只支持计算一列或多列,且MySQL服务器提供其自身的哈希函数。必须有一列或多列包含整数值

4.3.6 子分区

子分区是分区表中每个分区的再次分割,子分区既可以使用HASH希分区,也可以使用KEY分区。这 也被称为复合分区(*composite partitioning*)。

1,如果一个分区中创建了子分区,其他分区也要有子分区

2,如果创建了了分区,每个分区中的子分区数必有相同

3,同一分区内的子分区,名字不相同,不同分区内的子分区名子可以相同

4.4 分库分表中间件

4.4.1 分库分表中间件

  • sharding-jdbc(当当)

  • Mycat

  • TDDL(淘宝)

  • Oceanus(58同城数据库中间件)

  • vitess(谷歌开发的数据库中间件)

  • Atlas(Qihoo 360)

常用分表分库方法:

https://mp.weixin.qq.com/s/OM3oNqu8jIbmkdsvEhtL4g

4.4.2 分库分表可能遇到的问题

  • 事务问题:需要用分布式事务啦
  • 跨节点Join的问题:解决这一问题可以分两次查询实现
  • 跨节点的count,order by,group by以及聚合函数问题:分别在各个节点上得到结果后在应用程序端进行合并。
  • 数据迁移,容量规划,扩容等问题
  • ID问题:数据库被切分后,不能再依赖数据库自身的主键生成机制啦,最简单可以考虑UUID
  • 跨分片的排序分页问题(后台加大pagesize处理?)

4.4.3 为什么需要MyCat?

传统数据库存在着先天性的弊端,但是NoSQL数据库又无法将其替代。如果传统数据易于扩展,可切分,就可以避免单机(单库)的性能缺陷。

MyCat的目标就是:低成本地将现有的单机数据库和应用平滑迁移到“云”端解决数据存储和业务规模迅速增长情况下的数据瓶颈问题

4.4.4 MyCat是什么?

从定义和分类来看,它是一个开源的分布式数据库系统,是一个实现了MySQL协议的服务器,前端用户可以把它看作是一个数据库代理,用MySQL客户端工具和命令行访问,而其后端可以用MySQL原生协议与多个MySQL服务器通信,也可以用JDBC协议与大多数主流数据库服务器通信,其核心功能是分表分库,即将一个大表水平分割为N个小表,存储在后端MySQL服务器里或者其他数据库里

MyCat发展到目前的版本,已经不是一个单纯的MySQL代理了,它的后端可以支持MySQL、SQL Server、Oracle、DB2、PostgreSQL等主流数据库,也支持MongoDB这种新型NoSQL方式的存储,未来还会支持更多类型的存储。而在最终用户看来,无论是那种存储方式,在MyCat里,都是一个传统的数据库表,支持标准的SQL语句进行数据的操作,这样一来,对前端业务系统来说,可以大幅降低开发难度,提升开发速度

本节参考:https://mp.weixin.qq.com/s/HJYkDheKbCdANj91zRDx2g

5. 事务

事务特点, 原子性(Atomicity),一致性(Consistency),隔离型(Isolation)以及持久性(Durability)。事务是要做到可靠性以及并发处理

  • 可靠性:数据库要保证当insert或update操作时抛异常或者数据库crash的时候需要保障数据的操作前后的一致,想要做到这个,我需要知道我修改之前和修改之后的状态,所以就有了undo log和redo log
  • 并发处理:也就是说当多个并发请求过来,并且其中有一个请求是对数据修改操作的时候会有影响,为了避免读到脏数据,所以需要对事务之间的读写进行隔离,至于隔离到啥程度得看业务系统的场景了,实现这个就得用MySQL 的隔离级别。

实现事务功能的三个技术,分别是日志文件(redo log 和 undo log),锁技术以及MVCC

5.1 redo log

redo log叫做重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中

当事务提交之后会把所有修改信息都会存到该日志中。redo log是用来恢复数据的 用于保障,已提交事务的持久化特性

5.1.1 redo log 有什么作用?

mysql 为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存到Boffer Pool(缓冲池)里头,把这个当作缓存来用。然后使用后台线程去做缓冲池和磁盘之间的同步

那么问题来了,如果还没来的同步的时候宕机或断电了怎么办?还没来得及同步磁盘。这样会导致丢部分已提交事务的修改信息!

所以引入了redo log来记录已成功提交事务的修改信息并且会把redo log持久化到磁盘,系统重启之后在读取redo log恢复最新数据。

start transaction;
select balance from bank where name="zhangsan";
// 生成 重做日志 balance=600
update bank set balance = balance - 400; 
// 生成 重做日志 amount=400
update finance set amount = amount + 400;
commit;

5.2 undo log

undo log 叫做回滚日志,用于记录数据被修改前的信息。他正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。

每次写入数据或者修改数据之前都会把修改前的信息记录到 undo log

5.2.1 undo log 有什么作用?

undo log 记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log的信息来进行回滚到没被修改前的状态。

undo log是用来回滚数据的用于保障 未提交事务的原子性

5.3 MVCC

MVCC (MultiVersion Concurrency Control) 叫做多版本并发控制。

InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列, 一个保存了行的创建时间一个保存了行的过期时间,当然存储的并不是实际的时间值,而是系统版本号

MVCC在mysql中的实现依赖的是undo log与read view

  • undo log :undo log 中记录某行数据的多个版本的数据
  • read view :用来判断当前版本数据的可见性

5.4 事务用到的锁

5.4.1 读写锁

通过读写锁,可以做到读读可以并行,但是不能做到写读,写写并行 事务的隔离性就是根据读写锁来实现的!!!

共享锁(shared lock),又叫做"读锁"

读锁是可以共享的,或者说多个读请求可以共享一把锁读数据,不会造成阻塞。

排他锁(exclusive lock),又叫做"写锁"

写锁会排斥其他所有获取锁的请求,一直阻塞,直到写入完成释放锁。

5.5 事务实现

  • 原子性:事务的原子性是通过 undo log 来实现的
  • 持久性:事务的持久性性是通过 redo log 来实现的
  • 隔离性:事务的隔离性是通过 (读写锁+MVCC)来实现的
  • 一致性:通过回滚,以及恢复,和在并发环境下的隔离做到一致性

原子性,持久性,隔离性折腾半天的目的也是为了保障数据的一致性!总之,ACID只是个概念,事务最终目的是要保障数据的可靠性,一致性

5.5.1 原子性的实现

一个事务必须被视为不可分割的最小工作单位,一个事务中的所有操作要么全部成功提交,要么全部失败回滚,对于一个事务来说不可能只执行其中的部分操作,这就是事务的原子性。

所谓回滚操作就是当发生错误异常或者显式的执行rollback语句时需要把数据还原到原先的模样,所以这时候就需要用到undo log来进行回滚,接下来看一下undo log在实现事务原子性时怎么发挥作用的

生成undo log

  1. 每条数据变更(insert/update/delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上
  2. 所谓的回滚就是根据回滚日志做逆向操作,比如delete的逆向操作为insert,insert的逆向操作为delete,update的逆向为update等。

根据undo log 进行回滚

为了做到同时成功或者失败,当系统发生错误或者执行rollback操作时需要根据undo log 进行回滚。

回滚操作就是要还原到原来的状态,undo log记录了数据被修改前的信息以及新增和被删除的数据信息,根据undo log生成回滚语句,比如:

  • 如果在undo log里有新增数据记录,则生成删除该条的语句
  • 如果在undo log里有删除数据记录,则生成生成该条的语句
  • 如果在undo log里有修改数据记录,则生成修改到原先数据的语句

5.5.2 持久性的实现

事务一旦提交,其所作做的修改会永久保存到数据库中,此时即使系统崩溃修改的数据也不会丢失。

为了提升**性能(IO 瓶颈)**InnoDB提供了缓冲池(Buffer Pool),Buffer Pool中包含了磁盘数据页的映射,可以当做缓存来使用:

  • 读数据:会首先从缓冲池中读取,如果缓冲池中没有,则从磁盘读取在放入缓冲池;
  • 写数据:会首先写入缓冲池,缓冲池中的数据会定期同步到磁盘中;

事务开始之后就产生redo log,redo log的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo log文件中。

上面这种缓冲池的措施虽然在性能方面带来了质的飞跃,但是当MySQL系统宕机,断电的时候可能会丢数据!!!

因为我们的数据已经提交了,但此时是在缓冲池里头,还没来得及在磁盘持久化,所以我们急需一种机制需要存一下已提交事务的数据,为恢复数据使用。于是 redo log就派上用场了。下面看下redo log是什么时候产生的。

既然redo log也需要存储,也涉及磁盘IO为啥还用它?

  • redo log 的存储是顺序存储,而缓存同步是随机操作
  • 缓存同步是以数据页为单位的,每次传输的数据大小大于redo log

5.5.3 隔离性实现

在SQL标准里定义了四种隔离级别,每一种级别都规定一个事务中的修改,哪些是事务之间可见的,哪些是不可见的。

Mysql 隔离级别有以下四种(级别由低到高):

  • READ UNCOMMITED (读未提交)
  • READ COMMITED (读已提交)
  • REPEATABLE READ (可重复读)
  • SERIALIZABLE (串行)

脏读、幻读不可重复读

脏读 :所谓的脏读,其实就是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。

不可重复读 :不可重复读字面含义已经很明了了,比如事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。即一个事务范围内两个相同的查询却返回了不同数据。 数据变了

幻读 :小的时候数手指,第一次数十10个,第二次数是11个,怎么回事?产生幻觉了?(数据多了)

幻读也是这样子,事务A首先根据条件索引得到10条数据,然后事务B改变了数据库一条数据,导致也符合事务A当时的搜索条件,这样事务A再次搜索发现有11条数据了,就产生了幻读。

隔离性是要管理多个并发读写请求的访问顺序。这种顺序包括串行或者是并行。原子性和持久性是为了要实现数据的可性保障靠。可靠性性高的,并发性能低(比如 Serializable),可靠性低的,并发性能高(比如 Read Uncommited)

READ UNCOMMITTED

在READ UNCOMMITTED隔离级别下,事务中的修改即使还没提交,对其他事务是可见的。事务可以读取未提交的数据,造成脏读。因为读不会加任何锁,所以在读的过程中修改数据,所以会造成其他读事务的脏读。好处是可以提升并发处理性能,能做到读写并行。

  • 优点:读写并行,性能高
  • 缺点:造成脏读

READ COMMITTED

一个事务在他提交之前的所有修改,对其他事务都是不可见的。其他事务能读到已提交的修改变化。在很多场景下这种逻辑是可以接受的。

InnoDB在 READ COMMITTED,采用了读写分离机制,写数据使用排它锁,读取数据不加锁而是使用了MVCC机制。该级别会产生不可重读以及幻读问题。

为什么会产生不可重复读?

这跟 READ COMMITTED 级别下的MVCC机制有关系,在该隔离级别下每次 select的时候新生成一个版本号,所以每次select的时候读的不是一个副本而是不同的副本。

REPEATABLE READ(Mysql默认隔离级别,可重复读)

在一个事务内的多次读取的结果是一样的。这种级别下可以避免,脏读,不可重复读等查询问题。mysql 有两种机制可以达到这种隔离级别的效果,分别是采用读写锁以及MVCC

读写锁实现:可重复读,只要没释放读锁,在次读的时候还是可以读到第一次读的数据。

  • 优点:实现起来简单
  • 缺点:无法做到读写并行

采用MVCC实现: 因为多次读取只生成一个版本,读到的自然是相同数据。但是在该隔离级别下仍会存在幻读的问题

  • 优点:读写并行
  • 缺点:实现的复杂度高

SERIALIZABLE(串行)

在隔离级别下除了不会造成数据不一致问题,最严格的级别,事务串行执行,资源消耗最大;

5.5.4 一致性的实现

通过回滚,以及恢复,和在并发环境下的隔离做到一致性。

  1. 事务回滚
  2. 宕机数据恢复
  3. 假如有并发事务请求的时候也应该做好事务之间的可见性问题,避免造成脏读,不可重复读,幻读等。在涉及并发的情况下往往在性能和一致性之间做平衡,做一定的取舍,所以隔离性也是对一致性的一种破坏。

6 事务的实现原理及步骤

binlog 和 redo log:
MySQL是多存储引擎的,不管使用那种存储引擎,都会有binlog,而不一定有redo log,简单的说,binlog是MySQL Server层的,redo log是InnoDB层的。

6.1 CrashSafe(宕机安全)

CrashSafe指MySQL服务器宕机重启后,能够保证:

  • 所有已经提交的事务的数据仍然存在。
  • 所有没有提交的事务的数据自动回滚。

Innodb通过Redo Log和Undo Log可以保证以上两点为了保证严格的CrashSafe,必须要在每个事务提交的时候,将redo Log写入硬件存储。这样做会牺牲一些性能,但是可靠性最好。为了平衡两者,InnoDB提供了一个innodb_flush_log_at_trx_commit 系统变量,用户可以根据应用的需求自行

以上是关于一文读懂mysql-万字长文,肝就完了的主要内容,如果未能解决你的问题,请参考以下文章

万字长文,一文读懂Linux的常规操作(墙裂建议收藏)

史诗级计算机字符编码知识分享,万字长文,一文即懂!

一文终结MySQL(万字长文,建议收藏)

万字长文!一文彻底搞懂Java—volatile关键字

Linux疑难杂症解决方案100篇(二十)-万字长文带你读懂正则表达式(建议收藏)

[万字长文]一文带你深入了解Android Gradle