一、MySQL架构与历史

mysql最重要、最与众不同的特性是它的存储引擎架构,这种架构的设计将查询处理及其他系统任务和数据的存储/提取相分离。这种处理和存储分离的设计可以在使用时根据性能、特性,以及其他需求来选择数据存储的方式。

1.1 MySQL 逻辑结构

image

  • 最上层的服务并不是MySQL所独有的,大多数基于网络的客户端/服务器的工具或服务都有类似的架构。比如连接处理、授权认证、安全等等。
    • 当客户端连接到MySQL服务器时,服务器需要对其进行认证。认证基于用户名、原始主机信息和密码。
  • MySQL的大多数核心服务都在第二层,包括查询解析、分析、优化、缓存以及所有的内置函数(例如:日期、时间、数字和加密函数),所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。
    • MySQL会解析查询,并创建内部数据结构(解析树),然后对其进行各种优化,包括重写查询、决定表的读取顺序,以及选择合适的索引等。用户可以通过特殊的关键字提示优化器,影响它的决策过程。也可以请求优化器解释(explain)优化过程的各个因素,使用户可以指导服务器时如何进行优化决策的,并提供一个参考基准,便于用户重构查询和schema、修改相关配置,使应用尽可能高效运行。

第三层包含了存储引擎,存储引擎负责MySQL中数据的存储和提取。

1.2 并发控制

本章的目的是讨论MySQL在两个层面的并发控制:服务器层和存储引擎层

1)读写锁

共享锁(shared lock)/ 读锁(read lock)/ S锁

排它锁(exclusive lock)/ 写锁(write lock)/ X锁

锁的概念:

  • 读锁是共享的,或者说是相互不阻塞的。多个客户可以在同一时刻读取同一个资源,而互不干扰。
  • 写锁是排他的,也就是说一个写锁会阻塞其他的写锁和读锁,这是出于安全策略的考虑,只有这样,才能保证在给定的时间里,只有一个用户能执行写入,并防止其他用户正在写入同一资源

2)锁粒度

一种提高共享资源并发性的方法就是让锁定对象更有选择性。尽量只锁定需要修改的部分数据,而不是所有的资源。

问题是加锁也需要消耗资源。锁的各种操作,包括获得锁、检查锁是否已经解除、释放锁等,都会增加系统的开销。

所谓的锁策略,就是在锁的开销和数据的安全性之间寻求平衡,这种平衡当然也会影响到性能。

表锁

表锁是MySQL中最基本的锁策略,并且是开销最小的策略。

它会锁定整张表,一个用户在对表进行写操作(插入、删除、更新等)前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获得读锁,读锁之间是不相互阻塞的。另外,写锁也比读锁有更高的优先级,因此一个写锁请求可能会被插入到读锁队列的前面。

行级锁

行级锁可以最大程度地支持并发处理(同时带来最大的锁开销)

1.3 事务

案例:银行取钱(至少包括三个步骤)

  1. 检查账户余额高于200
  2. 从账户余额减去200
  3. 另一个账户余额增加200

ACID四个事务特性

  • 原子性:一个事务必须被视为一个不可分割的最小工作单元
  • 一致性:数据库总是从一个一致性的状态转换到另一个一致性的状态
  • 隔离性:一个事务所做的修改在最终提交以前,对其他事务不可见(隔离级别)
  • 持久性:一旦事务提交,则其所做的修改就会永久保存到数据库中(持久性是个有点模糊的概念,实际上持久性也分很多种不同的级别)

1)隔离级别

  • read uncommitted(未提交读)
    • 事务中的数据,即使没有提交,对其他事务也都是可见的【脏读:事务可以读取未提交的数据】
  • read committed(提交读)
    • 一个事务开始时,只能 "看见" 已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这种级别有时候也叫做不可重复读(non-repeatable read),因为两次执行同样的查询,可能会得到不一样的结果
  • repeatable read(可重复读)
    • 解决了 【脏读】的问题,该级别保证了在同一个事务中多次读取同样记录的结果时一致的。
    • 但是理论上,可重复读还是无法解决另外一个【幻读 Phantom Read】的问题
      • 所谓幻读:当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(phantom row)。InnoDB存储引擎通过多版本并发控制 MVCC(multiversion concurrency control)解决了幻读的问题
  • serializable(可串行化)
    • 通过强制事务串行执行,避免【幻读】的问题。简单来说,serializable会在读取的每一行数据上都加锁所以可能导致大量的超时和锁争用的问题。

2)死锁

:两个或者多个事务在同一资源上相互占用

案例:

start transaction;
update price set money = 46 where id = 3;
update price set money = 32 where id = 2;
commit;
start transaction;
update price set money = 24 where id = 2;
update price set money = 45 where id = 3;
commit;

为了解决这个问题,数据库系统实现了各种死锁检测和死锁超时机制。比如InnoDB存储引擎,检测到死锁的循环依赖,并立即返回一个错误。还有一种解决方式,就是当查询的时间达到锁等待超时的设定后放弃锁请求。这种方式通常来说不太好。

InnoDB目前处理死锁的方法是:将持有最少行级排它锁的事务进行回滚(这是相对比较简单的死锁回滚算法)

3)事务日志

内存-redolog-磁盘

事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志上,而不用每次都将修改的数据本身持久化到磁盘。

事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序IO,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。

事务日志持久以后,内存中被修改的数据在后台可以慢慢地刷回到磁盘。目前大多数存储引擎都是这样实现的,我们通常称之为预写式日志/写前日志(write-ahead logging)【简称 WAL 】,修改数据需要写两次磁盘。

4)MySQL中的事务

自动提交(auto commit)

SHOW VARIABLES LIKE 'autocommit';
image
MySQL默认采用自动提交模式。1或ON表示启用,0或OFF表示禁用。当 autocommit=0 时,所有查询都是在一个事务中,直到显式执行 commit提交或者 rollback回滚。

set autommit = 1;

隐式和显式锁定

InnoDB 采用的是两阶段锁定协议(two phase locking protocol)。在事务执行过程中,随时都可以执行锁定,锁只有在执行commit或者rollback时才会释放,并且所有的锁是在同一时刻被释放。

两阶段锁定协议

整个事务分为两个阶段,前一个阶段为加锁,后一个阶段为解锁。在加锁阶段,事务只能加锁,也可以解锁,直到事务释放第一个锁,就进入解锁阶段,此过程中事务只能解锁,也可以操作数据,不能再加锁

T1:Slock A…Slock B…Xlock C…Unlock B…Unlock A…Unlock C 【符合两段锁协议】

T2:Slock A…Unlock A…Slock B…Xlock C…Unlock C…Unlock B

另外,InnoDB 也支持通过特定的语句进行显式锁定,这些语句不属于SQL规范:

  • select .... lock in share mode
  • select ... for update

1.4 多版本并发控制

可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。

InnoDB 的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。

当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

id create_by create_time update_by update_time ... 创建时间(系统版本号) 过期时间 / 删除时间(系统版本号)
  • select
    • InnoDB 会根据以下两个条件检查每行记录:
      • InnoDB 只查找版本早于当前事务版本的数据行(行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的
      • 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除
    • 只有符合上述两个条件的记录,才能返回作为查询结果
  • insert
    • 为新插入的每一行保存当前系统版本号作为行版本号
  • delete
    • 为删除的每一行保存当前系统版本号作为行删除标识
  • update
    • 插入一行新纪录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识

保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。

MVCC 只在 可重复读 和 已提交读 两个隔离级别下工作。

1.5 MySQL的存储引擎

在文件系统中,MySQL将每个数据库(也可以称之为schema)保存为数据目录下的一个子目录。创建表时,MySQL会在数据库子目录下创建一个和表同名的 frm文件保存表的定义。

可以使用 show table status 命令显示表的相关信息。

image
image

  • Name:表名
  • Row_format:行的格式
  • Dynamic的行长度是可变的,一般包含可变长度的字段,如 VARCHAR 或 BLOB
  • Fixed 的行长度则是固定的,只包含固定长度的列,如 CHAR 和 INTEGER
  • Compressed的行则只在压缩表中存在
  • Rows:表中的行数。对于MyISAM和其他一些存储引擎,该值是精确的,但对于 InnoDB,该值是估计值
  • Avg_row_length:平均每行包含的字节数
  • Data_length:表数据的大小(以字节为单位)
  • Max_data_length:表数据的最大容量,该值和存储引擎有关
  • Index_length:索引的大小(以字节为单位)
  • Data_free:对于MyISAM表,表示已分配但目前没有使用的空间。这部分空间包括了之前删除的行,以后后续可以被 INSERT 利用到的空间
  • Auto_increment:下一个 AUTO_INCREMENT的值
  • Create_time:表的创建时间
  • Update_time:表数据的最后修改时间
  • Check_time:使用 CHECK TABLE 命令或者 myisamchk 工具最后一次检查表的时间
  • Checksum:如果启用,保存的是整个表的实时校验和
  • Create_options:创建表时指定的其它选项
  • Comment:该列包含了一些其他的额外信息。对于InnoDB表,保存的是 InnoDB 表空间的剩余空间信息。

1)InnoDB概览

InnoDB 采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别。其默认级别是 可重复读,并且通过 间隙锁(next-key locking) 策略防止幻读的出现,间隙锁使得 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。

间隙锁介绍

间隙锁:当我们用范围条件而不是相等条件查询时,InnoDB会给符合条件的已有数据加锁,对于符合条件范围内但并不存在的记录,叫做 间隙(gap),也会加锁

举例:假如 user 表有 101 条记录,其 user_id 分别是 1,2,3....101。查询大于100的记录

select * from user where user_id > 100;

InnoDB 不仅会对符合条件的 user_id 值为 101 的记录加锁,还会对 user_id 大于 101 的间隙加锁,防止其它事务在表的末尾增加数据

InnoDB 表是基于聚簇索引建立的。聚簇索引对主键查询有很高的性能,不过它的二级索引中必须包含主键列,所以如果主键列很大的话,其他的所有索引都会很大

2)MyISAM

把表存储在两个文件:数据文件和索引文件,分别以 .myd 和 myi 为扩展名。

posted @   LHX2018  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示