MySQL笔记

存储引擎

存储引擎即表的类型,决定了底层文件系统中文件的相关物理结构。

InnoDB:支持外键、事务的存储引擎

  • 5.5版本后默认的存储引擎。
  • 被设计来处理大量的短期事务。可以确保失误的完整提交和回滚。
  • 除了增加和查询外,还需要更新、删除操作,那么有限选择InnoDB
  • 除非有非常特别的原因需要使用其他的存储引擎,否则应该有限考虑InnoDB
  • 文件结构
    • 表名.frm 存储表结构(MySQL8.0后,合并在表名.ibd中)
    • 表名.ibd 存储数据和索引
  • InnoDB是为处理巨大数据量代最大性能设计,数据量大,并发大。支持行锁,粒度小。
  • 对比MyISAMInnoDB写的处理效率差一些,并且会占用更多的磁盘空间以保存数据和索引。
  • InnoDB不仅缓存索引还要缓存真实数据, 对内存要求较高 ,而且内存大小对性能有决定性的影响。

MyISAM:主要的非事务存储引擎

  • MyISAM提供了大量的特性,包括全文搜索、压缩、空间函数(GIS)等
  • 不支持事务和行级锁
  • 有一个毫无疑问的缺陷就是崩溃后无法安全恢复。
  • 优势是访问的速度快,对事务完整性没有要求或者以查询和插入为主的应用。
  • 针对数据统计有额外的常数存储。用count(*)效率高
  • 数据文件结构:
    • 表名.frm存储表结构
    • 表名.myd存储数据(mydata)
    • 表名.myi存储索引(myindex)
  • 应用场景:只读应用或以读为主的业务。

Archive:归档,用于数据存档

  • 仅支持插入查询
  • 拥有很好的压缩机制,使用zlib压缩库
  • 采用行级锁
  • 适合日志何数据采集类应用,适合存储大量的独立的作为历史记录的数据。拥有很高的插入速度。

MySQL笔记_第1张图片

Blackhole:丢弃写操作,读操作返回空

  • 没有实现任何的存储机制,丢弃所有的插入数据,不做任何保存。
  • 但是服务器会记录Blackhole表的日志,所以可以用于复制数据到备库,或者只是简单地记录到日志。

CSV:存储数据以逗号分隔

  • 可以将普通的CSV文件作为MySQL表来处理,但支持索引
  • CSV引擎可以作为一种数据交换机制
  • 每个字段都必须NOT NULL
  • 对于数据的快速导入、导出有明显优势

Memory:置于内存的表

Memory采用的逻辑介质是内存,响应速度非常快,但是重启后数据会丢失,没有持久化。比MyISAM快至少一个数量级。要求存储的数据是数据长度不变的格式。

  • 同时支持Hash索引和B+Tree索引,默认使用Hash索引,等值查找速度非常快,但是范围查找较慢
  • Memory表的大小是受限制的,基于max_rowsmax_heap_table_size参数。
  • 数据文件与索引文件分开存储。
    • 每个机遇Memory存储引擎的表实际对应一个磁盘文件,该文件名与表名相同,类型为frm,该文件中只存表结构,而数据文件都是存储在内存中。所以服务重启后,表结构还会保留但数据丢失。

使用场景

  1. 目标数据较小,而且非常频繁的访问,如果数据过大,会导致内存溢出。
  2. 临时数据并且必须立即可用。
  3. 数据丢失也没有太大关系。

Federated:访问远程表

是访问其他MySQL服务器的一个代理,尽管该引擎看起来提供了一个很好的跨服务器的灵活性,但也经常带来问题,因此默认是禁用的。

Merge:管理多个MyISAM表构成的表集合

Merge表是由多个MyISAM表合并而来的虚拟表。

NDBMySQL集群专用存储引擎

引擎对比

特点 MyISAM InnoDB Memory Merge NDB
存储限制 64TB 没有
事务 支持
锁机制 表锁 行锁 表锁 表锁 行锁
BTree 支持 支持 支持 支持 支持
Hash索引 支持 支持
全文索引 支持
聚簇索引 支持
数据缓存 支持 支持 支持
索引缓存 只缓存索引,不缓存数据 数据、索引都缓存 支持 支持 支持
数据可压缩 支持
空间使用
内存使用 中等
批量插入的速度
外键 支持

MyISAMInnoDB

  • InnoDB提供了良好的事务管理崩溃修复能力和并发控制,对于要求事务完整性的场合需要使用InnoDB,缺点是读写效率低,内存占用相对较大。支持行锁,适合高并发操作

  • 对于MyISAM,如果是小型应用,系统以读、插入为主,很少更新、删除操作,并且对失误要求没有这么高,可以选择。占用空间小,处理速度快。不支持事务。表锁,不适合高并发操作。

InnoDB行格式

数据页内部结构

默认16KB

名称 占用大小 说明
File Header 38字节 文件头,描述页的信息
Page Header 56字节 页头,页的状态信息
InfimumSupremum 26字节 最大和最小记录,这是两个虚拟的行记录
User Records 不确定 用户记录,存储行记录内容
Free Space 不确定 空闲记录,页中还没有被使用的空间
Page Directory 不确定 页目录,存储用户记录的相对位置
File Trailer 8字节 文件尾,校验页是否完整

FileHeader

描述各种页的通用信息

  • Fil_page_offset,页号,InnoDB通过页号可以唯一定位一个页

  • Fil_page_type,页的类型

    MySQL笔记_第2张图片

  • File_page_prevFil_page_next,上一页,下一页的页号,通过双向链表连接各页,实现不需要物理连接,而是逻辑连接

  • Fil_page_space_or_chksum,校验和,通过与文件尾的校验和比对,判断页在磁盘IO时是否出现异常

  • Fil_page_lsnLog Sequence Number,页面被最后修改时对应的日志序列位置

File Trailer

包含校验和LSN,为了校验页的完整性

Free Space

页内存储记录的部分的剩余空间

User Records

记录按照指定的行格式存放在这里,形成单链表

InfimumSupremum

最大记录和最小记录,heap_no分别对应01record_type分别对应23

Page Directory

用来存储每组最后一条记录的地址偏移量。这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称为slot槽,每个槽想到与指向了不同组的最后一个记录。通过二分法找到具体的组(由于页目录记录的是组中最大记录,而记录是单向链表,所以需找到上一组的槽,然后往后找记录),再到组中查找数据。

  • 第1组,包含最小记录,只有1条记录
  • 最后一组,是最大记录所在分组,有1-8条记录
  • 其余组记录数量在4-8条

每个组中最后一条记录的头部信息中,会存储改组一共有多少条记录,作为n_owned

MySQL笔记_第3张图片

Page Header

MySQL笔记_第4张图片

InnoDB行格式(记录格式)

Compact行格式

MySQL笔记_第5张图片

变长字段长度列表

在Compact行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表。这个列表与字段顺序是相反的。

NULL值列表

Compact行格式会把可以为NULL的列统一管理起来,存在一个标记为NULL值列表中。如果表中没有允许存储 NULL 的列,则 NULL值列表也不存在了。同样是与字段顺序相反,跳过not null字段。

  • 1,表示该值为NULL
  • 0,表示该值不为NULL
记录头信息

MySQL笔记_第6张图片

  • delete_mask,标记当前记录是否被删除

    如果真实删除记录,其他记录需要重新排列,导致性能消耗。

  • min_rec_mask,非叶子节点的最小记录都会添加该标记,值为1

  • record_type,这个属性表示当前记录的类型,一共有4种类型的记录

    • 0,普通记录
    • 1,非叶子节点记录
    • 2,最小记录
    • 3,最大记录
  • heap_no,表示当前记录在本页的位置,最小记录为0,最大记录为1

  • n_owned,页目录中每个组最后一条记录的头信息中会存储该组一共有多少条记录

  • next_record,表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量

    MySQL笔记_第7张图片

    • 删除记录

      MySQL笔记_第8张图片

    • 添加记录

      MySQL笔记_第9张图片

记录的真实数据

除了记录真实字段的值外,MySQL还维护了3个隐藏字段

  • row_id,行ID,唯一标识一条记录,当表结构没有定义主键时,该字段作为隐藏主键存在
  • transaction_id,事务id,当前记录版本的修改事务ID。详见MVCC
  • roll_pointer,回滚指针,记录了该记录的历史版本的指针列表。详见MVCC

行溢出

一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65533个字节,这样就可能出现一个页存放不了一条记录,这种现象称为行溢出。

CompactReduntant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中进行分页存储,然后记录的真实数据处用20个字节存储指向这些页的地址

DynamicCompressed行格式

CompressedDynamic两种记录格式对于存放在BLOB中的数据采用了完全的行溢出的方式。如图,在数据页中只存放20个字节的指针(溢出页的地址),实际的数据都存放在Off Page(溢出页)中。

Compressed行记录格式的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储。

MySQL笔记_第10张图片

Redundant行格式

MySQL笔记_第11张图片

注意Compact行格式的开头是变长字段长度列表,而Redundant行格式的开头是字段长度偏移列表,与变长字段长度列表有两处不同:

  • 少了“变长”两个字:Redundant行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表。

  • 多了“偏移”两个字:这意味着计算列值长度的方式不像Compact行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度。

索引

索引(Index)是帮助MySQL高效获取数据的数据结构 ,创建索引的目的是减少磁盘I\O次数。

索引是在存储引擎中实现的,每种存储引擎的索引不完全相同。

优缺点

优点

  • 降低数据库的IO成本。
  • 唯一索引保证了唯一性。
  • 加速表与表之间的连接,提高查询速度。
  • 减少查询分组和排序的时间,降低CPU消耗

缺点

  • 创建索引和维护索引需要耗费时间
  • 占用磁盘空间
  • 降低表更新、插入的速度

B+Tree

页是磁盘与内存交互的基本单位。B+Tree的节点是页。

根据叶子节点存储的内容,可以区分为聚簇索引二级索引

B+Tree可以分层很多层,规定叶子节点所在层为第0层,是存放用户记录的层。往上1-3层为目录页

一般情况下,用到的B+树都不会超过4层,可以假设叶子节点存放的用户记录有100条,而目录页存放的记录可以达到1000条,那么:

  • 如果B+树只有1层,最多存放100条记录
  • 如果B+树只有2层,最多存放100*1000=100000条记录
  • 如果B+树只有3层,最多存放100*1000*1000=100000000条记录
  • 如果B+树只有4层,最多存放100*1000*1000*1000=100000000000条记录

4层已经可以存放足够多的记录了。

结构

同一层页与页之间用双向链表连接,页内的记录用单向链表连接。

在非记录页节点中,存放内容为索引列+页号,记录页中,存放内容为记录值(根据聚簇索引和二级索引不同而不同)

聚簇索引

聚簇索引是基于主键(如果表结构没有声明主键,则会使用行格式中的隐藏字段row_id作为默认主键)建立的索引。

聚簇索引并不是单独的索引,而是一种数据存储方式,数据即索引,索引即数据。

特点

  1. 使用记录的主键值排序
    1. 页内的记录按照主键大小顺序排成一个单向链表
    2. 存放记录的页根据页中记录的逐渐大小排成一个双向链表
    3. 存放目录项记录的页分为不同层次,在同一个层次中的页也是按照记录中目录项记录的主键大小排序成一个双向链表
  2. 叶子节点的存储是完整的用户记录,记录中的所有列的值(包含隐藏字段)

优点

  • 速度快,因为聚簇索引将索引和数据保存在同一个B+树中,不需要回表
  • 对于主键的排序查找、范围查找速度快

缺点

  • 插入速度严重依赖于插入顺序,如果不按照逐渐递增顺序插入数据,会出现页分裂,严重影响性能,因此,对于InnoDB引擎,一般定义一个自增的ID列作为主键
  • 更新逐渐代价高
  • 二级索引访问需要二次索引查找

限制

  • 对于MySQL数据库目前只有InnoDB引擎支持聚簇索引
  • 每个表只能由一个聚簇索引,就是基于该表的主键(或隐藏字段row_id)建立的。
  • 建议使用有序递增的作为主键

二级索引

对于非主键字段建立的索引(包括联合索引)都将采用二级索引。与聚簇索引不同的是,在二级索引中,叶子节点存放的不再是完整的记录,而是建立索引的列+主键

因为存放的内容仅包含索引的列和主键,所以在使用二级索引查询数据时,常常还需要进行回表操作,即根据索引查询到主键后,再根据主键到聚簇索引中查询相关的字段。

索引覆盖

索引覆盖即二级索引已经包含了需要查询的所有字段,不需要再进行回表。

InnoDB中B+树索引的注意事项

根页面位置万年不动
  • 当某个表创建了B+树索引后,都会为这个索引创建一个根节点页面,最开始表中没有数据的时候,根节点既没有数据,也没有目录项记录。该页面在后续不断增加数据时,始终为根节点。
  • 随后插入用户记录时,首先将记录存储到根节点
  • 当根节点可用空间不足时继续插入记录,会先将根节点的所有记录复制到一个新分配的页,在对这个新页进行页分裂操作,而根节点升级为存储目录记录的页。
内节点中的目录项记录的唯一性

需要保证B+树在同一层内节点的目录记录除了页号外,是唯一的。

聚簇索引索引列为主键,是必定唯一的。

而非唯一索引的二级索引是通过记录索引列的值+主键来保证除页号外记录唯一。

一个页面至少存储2条记录

MyISAM引擎的索引方案

MyISAM默认使用B+Tree作为索引结构,本质为二级索引,MyISAM不支持聚簇索引。叶子节点存放的是数据记录的地址。

MyISAM引擎底层文件系统中,数据和索引是分开存放的(mydmyi文件)

  • MyISAM存储记录时,按照记录的插入顺序存储,不划分数据页,由于没有刻意按照主键大小排序,所以无法使用二分法查找记录
  • MyISAM会将索引存放在索引文件中,叶子节点记录的是索引列的值+数据记录地址

MyISAMInnoDB对比

MyISAM的索引方式都是“非聚簇”的,与InnoDB包含1个聚簇索引是不同的。小结两种引擎中索引的区别:

  1. InnoDB存储引擎中,我们只需要根据主键值对聚簇索引进行一次查找就能找到对应的记录,而在MyISAM中却需要进行一次回表操作,意味着MyISAM中建立的索引相当于全部都是 二级索引 。
  2. InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。
  3. InnoDB的非聚簇索引data域存储相应记录主键的值 ,而MyISAM索引记录的是地址 。换句话说,InnoDB的所有非聚簇索引都引用主键作为data域。
  4. MyISAM的回表操作是十分快速的,因为是拿着地址偏移量直接到文件中取数据的,反观InnoDB是通过获取主键之后再去聚簇索引里找记录,虽然说也不慢,但还是比不上直接用地址去访问。
  5. InnoDB要求表必须有主键(MyISAM可以没有)。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。

索引的代价

  • 空间上的代价

    每创建一个索引,都需要创建一棵B+树,数据量越大,索引就越大。

  • 时间上的代价

    在更新、插入数据时,可能需要额外的时间进行记录移位、页面分裂、页面回收等操作维护索引树。

Hash索引

Hash索引对于等值查询来说效率非常高,但是对于范围查找和排序效率非常低。

B树

一个M阶的B树(M>2)特性

  • 根节点的子节点数范围[2,M]
  • 每个中间节点包含k-1个关键字和k个子节点,子节点的个数=关键字的数量+1k取值范围[ceil(M/2),M]
  • 叶子节点包含k-1个关键字,k取值范围[ceil(M/2),M]
  • 所有叶子节点在同一层

B树与B+树区别:

  1. B+树有k个孩子就有k个关键字,而B树子节点数量=关键字数量+1
  2. B+树非叶子节点的关键字也会同时存在子节点中,而B树子节点关键字为开区间
  3. B+树非叶子节点进用于索引,不保存记录,而B树叶子节点和非叶子节点都存放数据。
  4. B+树所有关键字都会在叶子节点出现,叶子节点构成一个有序链表。

设计原则

  • 具有唯一特性的字段建议建立索引
  • 频繁作为Where条件查询的字段建立索引
  • 经常Group Byorder by的字段建立索引
  • 需要distinct字段建立索引
  • 多表连接操作的连接字段建立索引,类型必须一致,否则索引失效
  • 区分度高的字段建立索引
  • 将最频繁的列放到联合索引的左侧
  • 联合索引优于单指索引
  • 索引不宜建立太多,占用空间大,且影响查询效率(查询优化器会先判断哪个索引效率高再决定使用哪个索引)

索引失效案例

  • 计算、函数、类型转换导致索引失效
  • 联合索引中,范围条件右边的索引失效
  • 不等于索引失效
  • is not null索引失效
  • like%开头索引失效
  • or前后存在非索引的列,索引失效

索引下推

目的:减少回表次数

虽然部分条件可能导致索引失效,但联合索引中包含该字段,可以在二级索引将条件排查后,缩小范围再回表。

Explain

table

这一列显示了对应行正在访问哪个表。每一条记录对应一个单表,JOIN涉及(被)驱动表,或偶有临时表。

不论查询语句有多复杂,里面包含了多少个表,到最后也是需要对每个表进行单表访问的,所以MySQL规定,Explain语句输出的每条记录都对应着某个单表的访问,该条记录的table列代表着该表的表名。

id

在一个大查询语句中,每个select对应一个唯一的id

  • id如果相同,可以认为是一组,从上往下顺序执行
  • 在所有组中,id值越大,优先级越高,越先执行
  • id号每个号码,表示一趟独立的查询,一个sql的查询趟数越少越好

select_type

这一行显示了对应行是简单还是复杂select

simple值意味着查询不包括子查询和union。如果查询有任何复杂的子部分,那么最外层部分标记为primary,其余部分标记如下

  • subquery

    包含在select列表中的子查询中的select

  • derived

    包含在from子句的子查询的select,会递归执行并将结果放到一个临时表中,称为派生表

  • union

    union中第二个和随后的select

  • union result

    union的临时结果集

dependent意味着select依赖于外层查询中发现的数据。

名称 描述
Simple Simple select(not using UNION or subqueries)
Primary Outermost select
Union Second or later select statement in a union
Union result result of a union
Subquery First select in subquery
Dependent subquery First select in subquery, dependent on outer query
Dependent union Second or later select statement in a union, dependent on outer query
Derived Derived table
Materialized Materialized subquery
Uncacheable subquery A sub query for which the result cannot be cached and must be re-evaluated for each row of the outer query
Uncacheable union The second or later select in a union that belongs to an uncacheable subquery

partitions

匹配的分区信息

type

表示了MySQL决定使用什么方式访问表中的行。

  • system

    当表中只有一条记录且使用统计数据是精确的的存储引擎时

  • const

    根据主键或唯一索引与常量等值匹配时

  • eq_ref

    连接查询时,被驱动表通过主键或唯一索引匹配

  • ref

    普通二级索引与常数等值匹配

  • fulltext

    全文索引

  • ref_or_null

    意味着MySQL在ref初次查找的结果里进行第二次查找以找出NULL条目

  • index_merge

    合并多个索引

  • unique_subquery

  • index_subquery

  • range

    范围查找

  • index

    当可以使用索引覆盖,但仍然需要全表扫描时

  • all

    全表扫描

possible_keys

显示了查询可能使用哪些索引,这是基于查询访问的列和使用的比较操作符来判断的。这个列表是在优化过程的早期创建的,因此有些罗列出来的索引可能对于后续优化过程是没用的。

key

显示了MySQL决定采用那个索引来优化对该表的访问。如果该索引没有出现在possible_keys中,那么MySQL选用它是出于另外的原因——例如,它可能选择了一个覆盖索引,哪怕没有where子句。

key_len

显示了MySQL在索引里使用的字节数。主要针对联合索引。

ref

这一列显示了之前的表在key列记录的索引中查找值所用的列或常数。

rows

是MySQL估计为了找到所需的行而需要扫描的行数

filtered

表示针对表里符合某个条件的记录数的百分比所作出的一个悲观估算。

extra

包含一些额外信息

  • Impossible where

    where永远为false

  • using index

    使用了覆盖索引,不需要回表

  • using where

    MySQL服务器将在存储引擎检索行后再进行过滤。

范式

第一范式

确保每一个字段的值都具有原子性,是不可再次拆分的最小数据单元。

第二范式

在满足第一范式的基础上,还要满足表中每一行数据都是可唯一标识的,而且所有非主键字段都必须完全依赖主键,不能只依赖一部分。

第三范式

在满足第二范式的基础上,确保每一个非主键字段与主键字段直接关联,不能依赖其他非主键字段,非主键之间相互独立。

反范式化

遵循业务优先规则,适当增加冗余字段,提高读性能。

存在的问题

  • 存储空间变大了(增加了冗余字段)
  • 字段值修改了,相关联的冗余字段也要相应修改,否则数据不一致
  • 数据量小的情况下,不仅不能体现性能优势,还会让设计变复杂

适用场景

  • 增加冗余字段的建议
    • 不常修改
    • 查询不可或缺
  • 历史快照、历史数据需要
    • 不随着原始数据的更新而改变的历史快照

事务

事务是一组逻辑操作单元,使数据从一种状态变换到另一种状态。

ACID特性

  • 原子性(atomicity

    原子性是指事务是一个不可分隔的工作单位,要么全部成功提交,要么全部失败回滚。

  • 一致性(consistency

    一致性是指执行事务前后,数据从一个合法性状态变换到另一个合法性状态

    合法性指的是业务上的合法性而非语法上。

  • 隔离性(isolation

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

  • 持久性(durability

    持久性指一个事务一旦提交,它对数据库的修改是永久性的,接下来的其他操作不应该对其有任何影响。是通过事务日志保证的。

状态

  • 活动的(active

    事务对应的数据库操作正在执行中

  • 部分提交的(partially committed

    事务的最后一个操作在内存中已完成,但并未刷新到磁盘。

  • 失败的(failed

    处在活动的或者部分提交的事务,遇到某些错误而无法继续进行的

  • 终止的(aborted

    失败的事务在回滚操作完成后

  • 提交的(committed

    处在部分提交的事务完成刷盘后

隔离级别

数据并发问题

  • 脏写(dirty write

    事务A修改了尚未提交的事务B修改的数据

  • 脏读(dirty read

    事务A读取了已经被事务B修改但未提交的数据。

  • 不可重复读(non-repeatable read

    在同一个事务中多次读取同一个数据,由于中途被其他事务修改并提交,造成读取数据的前后不一致问题

  • 幻读(phantom

    在同一个事务中多次读取同一个数据,由于中途其他事务插入了一些行,导致数据前后不一致的问题。

SQL中隔离级别

首先,所有隔离级别都解决了脏写的问题

  • READ UNCOMMITTED,读未提交,在这个隔离级别,所有事务都可以读取到其他事务修改未提交的值。未解决脏读、不可重复读、幻读问题。
  • READ COMMITTED,读已提交,在这个隔离级别,保证所有事务只能读取到其他事务已提交的改变。避免脏读,无法避免不可重复读、幻读问题。
  • REPEATABLE READ可重复读,保证事务A在读取一条数据后,即使其他事务修改并提交该数据,事务A再去读取还是原来的内容。避免脏读、不可重复度,并且在MySQLMVCC中解决了幻读问题。
  • SERIALIZABLE在事务持续期间,禁止其他事务对该表执行插入、更新、删除操作。性能十分低下。

开发多用户、数据库驱动的应用时,最大的一个难点是:一方面要最大程度地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据。

锁是数据库系统区别与文件系统的一个关键特性。

MyISAM支持表锁,而InnoDB支持行级锁。

lock区别latch

latch一般称为闩锁,因为其要求锁定的时间必须非常短。

lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般的lock的对象仅在commitrollback后进行释放。

MySQL笔记_第12张图片

不同角度分类锁

MySQL笔记_第13张图片

从数据操作类型分

  • 共享锁:Share Lock, S Lock,允许事务读数据,多个事务同时进行而不互相阻塞。

  • 排他锁:Exclusive Lock, X Lock,允许事务删除或者更新数据。当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。

    select ... for update语句加X锁时,会把所有扫描的行锁上,因此在MySQL中使用悲观锁必须确保使用了索引,而不是全表扫描。

MySQL笔记_第14张图片

从数据操作的粒度分

表级锁

意向锁(Intention Lock

由于InnoDB引擎支持多粒度锁,这种锁允许事务在行级上的锁与表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB支持一种额外的锁方式,称为意向锁

若将上锁的对象看成一颗树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。

如果需要对页上的记录R进行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录R上X锁。

若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。比如,在对记录RX锁之前,已经有事务对表1进行了S表锁,那么表1上已经存在S锁,之后事务需要对记录R在表1上加IX,由于不兼容,所以该事务需要等待表锁操作完成。

MySQL笔记_第15张图片

InnoDB引擎支持意向锁的设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型:

  • 意向共享锁(IS Lock):事务想要获得一张表中某几行的共享锁。
  • 意向排他锁(IX Lock):事务想要获得一张表中某几行的排他锁。

由于InnoDB支持的是行级别的锁,因此意向锁不会阻塞除全表扫描以外的任何请求。兼容性如下图所示。

MySQL笔记_第16张图片

自增锁(AUTO-INC Locking

自增锁是针对插入自增长属性时的特殊的表级锁。为了提高插入的性能,锁不是在事务完成后才释放,而是在完成对自增长值插入的SQL语句后立即释放。事务必须等待前一个插入完成(不需要等待事务的完成)。

MySQL笔记_第17张图片

MySQL笔记_第18张图片

InnoDB中的行锁

记录锁(Record Lock

单个记录上的锁。记录锁分S型记录锁X型记录锁

间隙锁(Gap Lock

锁定一个范围,但不包含记录本身。

MySQLRepeateable Read隔离级别中,是可以解决幻读问题的。InnoDB提出Gap Lock来防止插入幻影记录。

通过锁定表中尚不存在的行范围,阻止其他事务在该范围插入新数据导致幻读问题出现。

临键锁(Next-Key Lock

记录锁 + 间隙锁,锁定一个范围,并且锁定记录本身。

从对待锁的态度分

从对待锁的态度可以分成乐观锁悲观锁,这两种并不是锁,而是锁的设计思想。

悲观锁

悲观锁总是假设其他事务总会修改数据,所以每次拿数据时都会上锁,这样其他事务想要数据时就会阻塞。

悲观锁通过数据库自身的锁机制来实现,保证数据操作的排他性。

乐观锁

乐观锁认为对同一数据的并发修改并不总会发生,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有更新数据,不采取数据库自身的锁机制,而是通过程序来实现。

两种锁的使用场景

  • 乐观锁适合读操作多的场景,相对来说写操作比较少。有点在于程序实现,不存在死锁问题
  • 悲观锁适合写操作多的场景,因为写操作具有排他性。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的写操作

其他锁

全局锁

全局锁就是对整个数据库加锁。当需要让整个库处于只读状态时,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(增删改)、数据定义语句(建表、修改表结构等)和更新类事务的提交语句。典型场景:全库逻辑备份。

Flush tables with read lock

死锁

产生死锁的必要条件
  • 两个或两个以上事务
  • 每个事务都已经持有锁并且申请新的锁
  • 锁资源同时只能被一个事务持有
  • 事务之间因持有锁和申请锁导致循环等待
避免死锁的方式
  • 合理设计索引,使业务SQL尽可能通过索引定位更少的行,减少锁竞争。
  • 调整业务逻辑SQL执行顺序,避免update/delete长时间持有锁的SQL在事务前面。
  • 避免大事务,尽量将大事务拆成多个小事务来处理,小事务缩短锁定资源的时间,发生锁冲突的几率也更小。
  • 在并发比较高的系统中,不要显示加锁,
  • 降低隔离级别(酌情考虑)。比如将隔离级别从Repeatable Read调整为Read Committed,可以避免很多因gap锁造成的死锁。

MVCC

多版本并发控制(Multiversion Concurrency Control),通过数据行的多个版本管理来实现数据库的并发控制。为了查询一些正在被另一个事务更新的行,并且可以看到他修改之前的值。

MVCC的实现依赖于数据行的隐藏字段、Undo LogReadView,且只在Read CommittedRepeatable Read隔离级别中有效。

隐藏字段

  • trx_id每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务ID赋值给trx_id隐藏列
  • roll_pointer每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,该隐藏字段相当于一个指针,通过它找到该记录修改前的信息。

MySQL笔记_第19张图片

Undo Log

对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,称之为版本链

ReadView

ReadView就是事务在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会生成数据库系统当前的一个快照,InnoDB为每个事务构造了一个数组,用来记录并维护系统当前活跃(启动但未提交)事务的ID。

设计思路

Read CommittedRepeatable Read隔离级别中,必须保证读到已经提交了的事务修改的记录,所以核心问题是需要判断版本链中,哪个记录是当前事务可见的。

ReadView主要包含4个比较重要的内容

  1. creator_trx_id,创建当前ReadView的事务ID。只有对记录有修改操作时才会生成并分配事务ID,否则在只读事务中,事务ID默认为0。
  2. trx_ids,表示在生成ReadView时,当前系统中活跃的事务ID。
  3. up_limit_id,活跃的事务ID中,最小的ID。
  4. low_limit_id表示生成ReadView时,系统应该分配给下一个事务的ID值,是系统中最大的事务ID。

ReadView规则

  • 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id一致,意味着当前事务正在访问自己修改的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值小于ReadView中的up_limit_id值,意味着生成该ReadView时,该版本的事务已经提交。所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于或等于ReadView中的low_limit_id值,意味着生成该ReadView时,该版本的事务尚未启动。所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在ReadViewup_limit_idlow_limit_id之间,需要判断该版本trx_id是否在trx_ids列表中
    • 存在,则表示创建ReadView时,该版本所属事务尚未提交。所以该版本不可以被当前事务访问。
    • 不存在,则表示创建ReadView时,该版本所属事务已经提交。所以该版本可以被当前事务访问。

需要注意的是,在Read Committed隔离级别下,每次读取数据前都会生成一个ReadView来确保每次读到的都是已提交的最新数据。而在Repeatable Read隔离级别中,只在事务第一次SELECT时会获取一次ReadView来避免出现不可重复读问题。

Repeatable Read隔离级别下解决幻读问题

由于Repeatable Read隔离级别中,只在事务第一次SELECT时会获取一次ReadView,当其他事务在该事务启动后插入的记录,有可能出现两种情况:

  • 插入数据的事务在当前事务创建ReadView时已启动,即事务ID在trx_ids列表中
  • 插入数据的事务在当前事务创建ReadView时还未启动,即事务ID大于low_limit_id

以上两种情况在MVCC规则中,都不允许事务访问该版本的记录。避免了幻读问题的发生。

总结

  • Read Committed隔离级别下,每次读取数据前都会生成一个ReadView来确保每次读到的都是已提交的最新数据。
  • Repeatable Read隔离级别中,只在事务第一次SELECT时会获取一次ReadView,事务中之后的查询操作都重复使用这个ReadView

通过MVCC可以解决:

  • 读写之间阻塞的问题。通过MVCC可以让读写互相不阻塞,提升并发处理能力。
  • 降低了死锁的概率。因为MVCC采用乐观锁的方式,读取数据时并不加锁,写操作也只锁定必要的行。
  • 解决快照读的问题。当我们查询数据库在某个时间点的快照时,智能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交更新的结果。

日志

慢查询日志(slow query log

默认不开启,通过配置long_query_time来定义,这些语句会被记录到慢查询日志中,结合explain进行分析。

重做日志(redo log

重做日志提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性

由存储引擎生成,记录物理级别上的修改,比如:页xx,偏移yy,写入zz

redo log的必要性

InnoDB存储引擎是以页为单位来管理存储空间的。在真正访问页面之前,需要把磁盘上的页缓存到内存的Buffer Pool中,所有变更都必须先更新缓冲池中的数据,然后缓冲池中的脏页以一定频率被刷入磁盘(checkpoint机制),通过缓冲池来优化CPU和磁盘之间的鸿沟,这样可以保证整体性能不会下降太快。

checkpoint并不是每次变更的时候就触发的,可能出现刚写完缓冲池服务器就宕机的情况,如果没有redo log这段数据就会丢失。而基于事务的持久性特征,一旦事务提交,对数据库的影响应该永久保存。

但频繁刷盘会出现的问题:

  • 修改量与刷盘工作量不成比例

    InnoDB以页为单位进行磁盘IO,仅修改一个字节的内容也需要刷新整个页(默认16kb)数据,不合适

  • 随机IO刷新较慢,涉及页面可能不相邻

InnoDB引擎的事务采用了WAL(Write-Ahead Logging)技术,这种技术的思想就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功,当发生宕机且数据未刷新到磁盘时,可以通过redo log来恢复,保证数据的持久性

好处

  • 降低了刷盘频率
  • 占用空间小,仅存储空间ID、页号、偏移量已经需要更新的值,所需的存储空间是很小的,刷盘快。

特点

  • 顺序写入磁盘的

  • 事务执行过程中,redo log不断记录

    redo logbin log的区别,redo log是存储引擎层产生的,而bin log是数据库层产生的。假设一个事务,对表做了10万行的记录插入,在这个过程中,一直不断的往redo log顺序记录,直到事务提交,才会一次写入bin log文件中。

redo的组成

  • 重做日志的缓冲(redo log buffer),保存在内存中,易丢失
  • 重做日志文件(‘redo log file’),保存在磁盘中,持久的

redo log刷盘策略

innodb_flush_log_at_trx_commit参数来控制redo log的刷盘策略

  • 0,表示每次事务提交时不刷盘,系统默认每隔1s进行一次同步,如果期间MySQL挂了,可能丢失1s内数据。
  • 1,表示每次事务提交时都进行刷盘操作,如果事务没有提交,所以日志丢了也不会有损失。
  • 2,表示每次事务提交时只会把redo log buffer内容写入文件系统缓存page cache,不进行同步。由os自己决定什么时间同步到磁盘。MySQL服务没挂,数据就持久化了,如果宕机了,可能丢失1s的数据。

回滚日志(undo log

回滚日志用来保证事务的原子性一致性

由存储引擎生成,记录逻辑操作。在事务中更新数据的前置操作中写入undo log

undo log的必要性

事务需要保证原子性,如果出现错误或需要回滚,就需要undo log来恢复到事务开始前的状态。

undo log会产生redo log,因为undo log也需要持久性的保护。

作用

  • 回滚数据

  • MVCC

    当用户读取一行记录室,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。

MySQL笔记_第20张图片

MySQL笔记_第21张图片

通用查询日志(general query log

文本文件记录用户的所有操作,包括启动、关闭MySQL服务,所有用户连接的起始时间、所有指令等。默认不开启

当数据发生异常时,查看通用日志还原操作时的具体场景,可以帮助定位问题。

错误日志(error log

MySQL中错误日志是默认开启无法被关闭的。

以文本形式记录MySQL的服务器启动、停止运行的时间,以及系统启动、运行和停止过程中的诊断信息,包括错误、警告和提示等。

通过错误日志可以查看系统的运行状态,便于及时发现故障、修复故障。如果MySQL服务出现异常,错误日志是发现问题、解决故障的首选。

二进制日志(bin log

二进制日志文件,以事件形式记录了数据库所有执行的DDLDML等数据库更新事件的语句,并保存在二进制文件中。

主要应用场景:

  • 数据恢复,数据异常丢失时,可以通过bin log文件来恢复数据。
  • 数据复制,由于日志的延续性和时效性,masterbin log传递给slaves来达到数据一致的目的。

写入机制

在事务执行的过程中,先把日志写入到binlog cache中,事务提交时,再把binlog cache写入到binlog文件中。

writefsync的时机,可以有参数sync_binlog控制

  • 0,每次提交事务都write,由系统自行判断什么时间fsync
  • 1,每次提交事务都write且执行fsync
  • N,每个事务提交都write,但累计N个事务后才fsync

MySQL笔记_第22张图片

binlogredo log对别

  • redo log物理日志,记录在某页,偏移量多少的数据做了修改,属于InnoDB引擎产生的。
  • bin log是逻辑日志,记录的语句是原始逻辑,属于MySQL Server层。

不同作用

  • redoInnoDB有了崩溃恢复的能力
  • bin log保证mysql集群结构的数据一致性

两阶段提交

redo log在事务执行过程中不断写入,而bin log仅在事务提交时写入,两者写入时间不一致,可能导致主从机数据不一致的情况。

MySQL集群中,某事务在执行中,redo log写入了数据,而bin log没有写入,导致主机数据修改了,而从机根据bin log内容同步数据,出现了数据不一致的问题。

InnoDB使用两阶段提交来解决以上问题。

在写入bin log前后都写入redo log,判断处在commit阶段且不存在对应的binlog,就回滚事务。

MySQL笔记_第23张图片

MySQL笔记_第24张图片

格式

  • statement,基于SQL语句的复制。记录修改数据的SQL语句
  • row,基于行的复制。记录哪条数据被修改了,修改成什么样。
  • mixed,混合模式复制。一般语句使用statement格式保存binlog,而使用一些函数,statement无法完成主从复制的操作时,则采用row格式保存binlog

中继日志(relay log

中继日志只在主从服务器架构中的从服务器上存在。目的是完成主从服务器的数据同步,保证了主从服务器的数据一致性。格式与bin log相同,可以借助mysqlbinlog工具进行查看。

恢复注意点

中继日志是包含从服务器名的。如果是通过重装操作系统来恢复系统服务,需要将服务器名修改为原服务器名,避免出现数据恢复异常的情况。

主从复制

作用

  • 读写分离,通过主从复制的方式实现数据同步,通过读写分离来分散数据库的压力,提高并发能力。
  • 数据备份,通过主从复制将主机上的数据复制到从机,相当于是热备份机制。
  • 高可用性,当服务器出现故障或者宕机的情况,可以切换服务器,保证服务的正常运行。

原理

Slave会从Master读取binlog来进行数据同步。

三个线程

在主从复制的过程中,由三个线程实现,一个主库线程,两个从库线程。

MySQL笔记_第25张图片

二进制日志转储线程,是主库线程。当从库线程连接的时候,主库可以将二进制日志发送给从库,当主库读取事件的时候,会在binlog上加锁,读取完成之后,再释放锁。

从库I\O线程,会连接到主库,向主库发送请求更新binlog,这时候从库的I\O线程就可以读取到主库的二进制日志转储线程发送的binlog更新部分,并且拷贝到本地的中继日志。

从库SQL线程,会读取从库中的中继日志,并且执行日志中的事件,将从库中的数据与主库保持同步。

步骤

  1. Master将写操作记录在binlog
  2. SlaveMasterbinlog events拷贝到中继日志中。
  3. Slave重做中继日志中的事件,将改变应用到自己的数据库中。MySQL复制是异步的且串行化的。

基本原则

  1. 每个Slave只有一个Master
  2. 每个Slave只能有一个唯一的服务器ID
  3. 每个Master可以有多个Slave

参考资料

MySQL数据库教程天花板,mysql安装到mysql高级,强!硬!_哔哩哔哩_bilibili

MySQL技术内幕 (豆瓣) (douban.com)

高性能MySQL(第3版) (豆瓣) (douban.com)

你可能感兴趣的:(mysql,数据库)