mysql的架构

138 阅读14分钟

为什么要选择mysql?

能够支撑大量的用户数据,满足大部分的业务场景使用,性能优越,很少出现宕机的情况.

经过大量的基准测试,性能比较稳定,灵活,可插拔的架构设计.

市面上比较流行,社区活跃,用户使用群体庞大,遇到问题解决方案比较多.

服务器的架构设计

image.png

三层架构

最上层的 连接层, 负责连接处理,授权认证 等操作 .

第二层是mysql比较核心的服务层,大多数核心的功能都在这一层,如查询解析,分析,优化,缓存和所有的内置函数.所有跨存储引擎的操作都在这一层完成,如存储过程,视图,解析器等.

最后是存储引擎层,mysql提供了很多可供选择的存储引擎,如常用的innodb,myisam,mysql5.5之后默认使用的存储引擎为innodb.可拔插式的设计,使我们可以根据业务场景来选择合适的存储引擎,尽管innodb已经能够满足大部分场景.存储引擎层不会进行语法解析(innodb引擎除外,innodb需要进行外键分析),他只负责数据的提取与存储,只与服务层进行交互,不同的存储引擎之间也不会进行通信.服务层通过api与存引擎进行通信.这些接口屏蔽了不同存储引擎之间的差异.

优化与执行

Mysql 从接到一个执行计划开始,会先解析语句,创建内部解析结构(解析树),然后对计划进行各种优化,包括重写查询,决定表的读取顺序,以及选择合适的索引等.具体的解析优化进程可以通过

explain + 执行计划查看 ; show profiles 查询具体执行计划的耗时,以及io cpu 消耗; show staus like 查询服务器会话状态.

查询优化是在服务层完成的,并不关心使用的是什么存储引擎,但存储引擎对于优化查询是有影响的.优化器会请求存储引擎提供容量或某个操作的开销,以及表数据的统计信息等.

对于select语句.服务器会在解析查询之前,先检查查询缓存(query cache),如果能在缓存中找到对应查询,服务器就会直接将缓存中的结果集返回,不必再执行查询解析,优化和执行的过程.

#查看mysql是否开启了查询缓存 
SHOW VARIABLES LIKE 'have_query_cache'; 

#查看查询缓存的大小限制 
SHOW VARIABLES LIKE 'query_cache_size'; 

#查看查询缓存的状态变量 
SHOW STATUS LIKE 'Qcache%';

并发控制

无论何,只要涉及到多个查询需要在同一时刻修改同一份数据,就都会发生并发控制的问题.

mysql在服务层 和 引擎层进行并发控制.

首先,根据加锁颗粒度,mysql里的锁大致分为全局锁,表锁,行锁。

1.全局锁(FTWRL)

#加锁命令 
flush table with readlock; 

#释放全局锁 
unlock tables;

全局锁的特点:全局锁会让整个数据库(所有表)处于只读状态,使用这个命令后,数据库表的增删改查(DML),表结构的更改(DDL),更新类事务的提交都会被堵塞。

全局锁的作用:全局锁会让数据库处于只可读的状态,这种状态会使数据库处于一个低效率的状态,几乎所有的业务都不会只存在读操作,这时全局锁看上去很鸡肋。

那么全局锁主要是应用于什么场景呢?

在以前,全局锁的主要作用就是:做全库的逻辑备份。如果在主库上备份,那么在备份期间都不能执行更新操作;如果在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,会导致主从延迟。

在做全库逻辑备份的是否,如果不加锁,在备份期间还会涌入新的数据。会造成备份的库里面的表不是一个逻辑时间点。

2.表级锁

MySQL中的表级别锁分为两种:

  • 表锁
  • 元数据锁(MetaData Lock)

2.1 表锁

#加锁命令(读锁/写锁) 
lock table tableName read/write; 

#释放锁 
unlock table tableName;

表锁的特点:对整个表加锁,锁的力度比较低。当没有出现更细粒度的锁时,表锁是常用的处理并发问题的方式,对于InnoDB这种支持行锁的引擎,一般不适用于表锁,因为表锁的影响效率还是很大;

对某个表加锁,不仅会影响其他线程对该表的操作,也会影响当前线程对这张表的操作。

对一个表加了读锁后,自己也不能对其进行修改;自己和其他线程只能读取该表;

对一个表加了写锁后,该线程可以对这个表进行读写,其他线程对该表的读和写都会受到阻塞。

2.2 MDL锁

MDL锁的特点:MDL是在MySQL5.5之后引入的,MDL不需要显示的使用,在访问一个表的时候会自动加上,它的作用是保证读写的正确性;

对一个表进行增删改查的时候,加MDL读锁,当对表结构做更改操作的时候,加MDL写锁;

  • 读锁之间不互斥,所以多个线程可以同时对一个表增删改查;
  • 读写锁之间,写锁之间互斥的,如果有多个线程要同时给一个表加字段,其中一个要等待另一个执行完成才能开始执行;
  • 事务中的MDL锁,在语句执行时开始申请,但是语句结束后并不会马上释放,要等到这个事务完成提交后才能释放;

MDL锁的坑:给一个小表加字段

事务1:查询表数据(读锁),但未释放。

事务2:查询数据(读锁),可以正常查询,读锁之间共享。

事务3:修改表结构,添加字段(写锁),事务1读锁未释放,阻塞,读写锁互斥。

事务4:查询表数据(读锁),事务3写锁未释放,阻塞,读写锁互斥。

这是会发现,事务3之后的所有语句都会阻塞,直到事务3释放写锁。

所以要避免长事务,事务一直不提交,就会一直占用着MDL锁。

在MySQL5.6开始支持online ddl ,这是什么意思呢,下面看online的步骤:

  • 拿MDL写锁
  • 降级成MDL读锁
  • 真正做DDL
  • 升级成MDL写锁
  • 释放MDL锁

3.行锁

行锁是在引擎层由各个引擎自己实现的,有的引擎并不支持行锁,如MyISAM,不能使用行锁就意味着:

  • 并发控制只能使用表锁,对于这种引擎的表,同一张表任意时刻只能有一个线程在执行更新操作,这严重影响了并发度;
  • InnoDB是支持行锁的,这也是MyISAM被InnoDB代替的主要原因;

3.1行锁的特点

首先需要注意:InnoDB的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则都会从行锁升级为表锁(REAPEATED-READ级别下,单读讲锁时会具体聊聊);

在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务提交了才会释放

3.2 死锁

死锁并不陌生,数据库中也会有死锁的存在,即多个事务相互持有锁。

image.png

  1. 事务1 开启事务 执行 UPDATE sss SET age = age + 1 WHERE id = 2; 顺利执行成功

  2. 事务2 开启事务 执行UPDATE sss SET age = age + 1 WHERE id = 1; 顺利执行成功

  3. 事务1 执行 UPDATE sss SET age = age + 1 WHERE id = 1; 语句,发现行锁被事务2持有,会尝试获取锁,进入阻塞状态

  4. 事务2 执行 UPDATE sss SET age = age + 1 WHERE id = 2; 语句,发现锁被事务 1 持有,相互持有,发生了死锁

    会抛出 Deadlock found when trying to get lock; try restarting transaction 的错误并会滚当前事务。
    
  5. 由于事务 2 会滚,事务1 获取到了锁,顺利执行成功

提出一个疑问,mysql是如何侦测到发生了死锁呢?

1.处理死锁策略

  • 1.直接进入等待,直到超时,这个超时时间可以通过参数 innodb_lock_wait_timeout 来进行设置,如下:(InnoDB中查看这个参数默认是50秒)

image.png

  • 2.发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(InnoDB: 持有最少行级排他锁的事务回滚),让其他事务得以执行;将参数innodb_deadlock_detect设置为on,就代表开启(InnoDB默认开启死锁检测)

image.png

两种方案的区别:

  • 第一种需要等待50s,放在生产环境来说这是不现实的,如果这个值设置的太短又会造成误判;
  • 一般采用第二种,也是InnoDB默认的处理死锁策略。但这种方式也有弊端,毕竟检测死锁检测需要消耗资源。

2.死锁检测

上面讲了,死锁检测是数据库校验死锁的一种策略,检测所付出的代价就是:

  • 每当一个事务被锁住的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,即死锁

进行死锁检测的条件:

  • 需要对当前事务加锁,并且访问的行已经被其他线程锁住时,才会进行死锁检测

事务

事务的特性

事务就是一组原子性的SQL查询,或者说一个独立的工作单元。

事务内的语句,要么全部执行成功,要么全部执行失败。

事务的四大特性:

  • 原子性(atomicity) :作为不可分割的工作单元,要么全部成功,要么全部失败。
  • 一致性(consistency) :事务总是从另一个状态到达另一个状态。
  • 隔离性(isolation) :多个事务之间的操作相互隔离,互不影响。
  • 持久性(durability) :事务一旦提交,就会永久保存到数据库中。

事务的隔离等级

mysql有四个隔离等级:

  • 读未提交 存在的并发问题: 脏读,幻读,不可重复读
  • 读已提交 存在的并发问题: 幻读,不可重复读
  • 可重复读 存在的并发问题: 幻读(这个我们之后会详细讲)
  • 串行化 存在的并发问题: 无,但效率低下

mysql的默认隔离等级是 可重复读。(阿里购买的rds默认等级是READ- COMMITTED)

*事务中存在的并发问题:

脏读:一个事务读取到了另外一个事务未提交的数据。

不可重复读:事务A查询了一条数据后,事务B对这个数据进行了update操作并提交了事务,这时事务A又查询了该条数据,发现与之前读取到的不一致。update操作

幻读:如:事务A 查询 id ≤ 5 的数据行,查询出了四条数据;这时,事务B新增了id = 5的数据并提交;此时事务A又查询 id ≤ 5 的数据行,发现查询出了5条数据(删除亦然)。delete,insert操作;InnoDB存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)解决了幻读的问题。

事务日志

事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。事务日志持久化后,内存中被修改的数据在后台可以慢慢地刷回到磁盘。目前大多数存储引擎都是这样实现的,我们通常称之为预写式日志,修改数据需要写两次磁盘。

事务隔离性是由锁来实现的,原子性 一致性 和持久性 是通过InnoDB提供的两种事务日志redo log 和 undo log 来完成的。redo log 称为重做日志,用来保证事务的持久性。undo log 是回滚日志,用来保持事务的一致性。redo log 和 undo log 的作用都可以视作为是一种恢复操作,但undo log 不是 redo log的逆向过程。

多版本并发控制MVCC(Multiversion Concurrency Control)

Mysql的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,一般都同时实现了多版本并发控制(MVCC)。不仅是MySQL,包括Oracle,PostgreSQL等其他数据库也都实现了MVCC,但各自的实现机制不尽相同,因为MVCC没有一个统一的实现标准。

可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。

MVCC的实现,是通过保存数据在某个节点的快照来实现的。(快照读- READ VIEW,也就是普通的select 操作)。也就是,不管需要执行多长的时间,每个事务看到的数据都是一样的。根据事务的开始时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。

不同的存储引擎实现MVCC的方式也是不同的。典型的有乐观(optimistic)并发控制和悲观(pessimistic)并发控制。

在这里我们主要讲下InnoDB的实现方式:

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

每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的数据的版本号做对比。下面看一下在REPEATABLE- READ隔离等级下,MVCC具体是如何操作的:

SELECT

InnoDB会根据以下两个条件检查每行的记录:

  • InnoDB只会查找版本早于当前事务版本的数据行(也就是说,行的系统版本号要小于等于当前事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前就已经存在的,要么是事务自身插入和修改的。
  • 行的删除版本要么为定义,要么大于当前事务的系统版本号。

只有符合上述两个条件的记录,才能返回作为查询结果。

INSERT

InnoDB为新插入的每一行保存当前版本号作为行创建版本号。

DELETE

InnoDB为删除的每一行保存当前版本号到删除系统版本号标识中。

UPDATE

InnoDB会新插入一条行,保存当前的版本号作为行创建版本号,然后把原来的行的删除版本号置为当前版本号。

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