MySQL逻辑架构
图来自这里
- 第一层并不是MySQL独有的,大多数C/S架构的工具和服务都有,进行
连接处理,授权认证,安全
等 - 第二层,
服务器层
。大多数MySQL的核心功能都在这一层,包括查询解析、分析、优化、缓存以及所有内置函数,所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等 - 第三层,
存储引擎
。负责数据的存储和提取,存储引擎提供API于上层服务器,这些接口屏蔽了不同存储引擎之间的差异,用于执行:“开启事务”、“根据主键盘提取一行记录”等操作。但存储引擎不会解析SQL(InnoDB是一个例外,他会解析外键定义,因为MySQL本身没有实现这个功能)。不同存储之间也不会相互通信,而只是简单的相应上层服务器的请求。
连接管理
每个客户端连接会在服务器进程中拥有一个线程,这个连接的查询只会在这个单独的线程中执行,该线程只能轮流在某个CPU核心或者CPU中运行。服务器会负责缓存线程,不会每一新连接新创建、销毁线程。
并发控制
只要有多个查询在同一时刻修改数据,都会产生并发控制的问题,接下来从服务器层和存储引擎层讨论MySQL的并发控制。
读写锁
在处理并发读写时,可以通过实现一个由两种类型的锁组成的锁系统来实现并发控制。这两种类型的锁通常被称为共享锁
和排他锁
,也叫读锁
和写锁
。
锁
的概念如下:
- 读锁是共享的,即相互不阻塞的。多个用户在同一时刻可以读取同一个资源,而互不干扰
- 写锁是排他的,一个写锁会阻塞其他的写锁和读锁
锁粒度
只锁定需要修改的部分数据,而不是所有资源可以提高共享资源并发性。更理想的方式是,只对会修改的数据片进行精确的锁定。
任何时候,在给定的资源上,锁定的数据量越少,则系统的并发程度越高,只要相互之间不发生冲突即可。
加锁也需要消耗资源。锁的各种操作:获得锁,检查锁是否已经解除,释放锁等,都会增加系统开销。锁策略
,在锁的开销和数据的安全性之间寻求平衡。MySQl最重要的两种最重要的锁策略:
- 表锁
表锁是MySQL中最基本的锁策略,并且是开销最下的策略。他会锁定整张表。
用户进行写操作(插入、删除、更新等)之前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获得读锁,读锁之间是不相互阻塞的
写锁比读锁有更高的优先级,因此一个写锁请求可能会被插入到读锁队列的前面 - 行级锁
行级锁可以最大程度的支持并发处理,但同时也带来了最大的锁开销。行级锁只在存储引擎层实现,而MySQL服务器层没有实现,服务器层完全不了解存储引擎层的锁实现。
表锁、行锁是锁策略
读锁、写锁是锁的实现方式
事务
事务是一组原子性的SQL查询,或者说一个独立的工作单元。即:事务内的语句,要么全部执行,要么全部执行失败
- 原子性
一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。 - 一致性
事务的一致性说的是事务要保证把数据库从一个正确的状态迁移到另一个正确的状态。正确状态就是说数据库里的数据满足其约束条件。 - 隔离性
一个事务不会影响其他事务的运行,在多个事务运行的情况下,隔离性保证了并发基础,当然实际上还需要配合锁来保证并发下的数据安全。 - 持久性
事务完成后,对数据库的更改永久保存,不会因为故障丢失。
每条SQL语句都默认封装为了一个事务,自动提交,影响速度,所以为了效率可以将多个SQL命令使用begin和commit封装为一个事务
一致性中,数据库从一个正确的状态迁移到另一个正确的状态,指的是数据处于一种语义上有意义且正确的状态,
1.原子性和一致性的的侧重点不同:原子性关注状态,要么全部成功,要么全部失败,不存在部分成功的状态。而一致性关注数据的可见性,中间状态的数据对外部不可见,只有最初状态和最终状态的数据对外可见。
2.在未提交读的隔离级别下,会造成脏读,这就是因为一个事务读到了另一个事务操作内部的数据。ACID中是的一致性描述的是一个最理想的事务应该怎样的,是一个强一致性状态,如果要做到这点,需要使用排它锁把事务排成一队,即Serializable的隔离级别,这样性能就大大降低了。现实是骨感的,所以使用隔离性的不同隔离级别来破坏一致性,来获取更好的性能。
隔离级别
-
读未提交
(Read Uncommitted):所有事务都能看到其他事务未提交的执行结果,据我所知该隔离级别应该在实际应用中很少使用,而且他并没有明显的性能优势,还不能避免脏读,不可重复读,幻读这些数据丢失和错乱的情况
原理
:事务读当前数据时不加锁,在更改数据的时候,对其加行级共享锁,结束时释放。
表现
:
事务1读取某行数据时,事务2也能读取该行数据;
事务2更新该行数据时,事务1能读到更新后的版本,即使尚未被提交;
事务1对某行数据进行更新时,事务2不能更新该行数据,直到事务1结束。 -
读已提交
(Read Committed):一个事务开始后,只能看见已经结束的事务的结果,正在执行的事务无法看到,
原理
:事务对当前被读取的数据加行级共享锁(读到时加),读完释放;事务在更新数据时加行级排他锁,事物结束后释放
表现
:事务1读取某行数据,事务2也能同时读取;
事务2更改某行数据,事务1要么读取到其commit前的要么读取到其commit后的;
事务1更新某数据时,事务2不能对其进行更新,直到事务1结束 -
可重复读
(Repeatable Read):该级别保证了每行数据的一致
原理
:事务在读取某数据时,加行级共享锁(开始读时加),直到事务结束时释放;事务在更新某数据时,加行级排他锁
表现
:事务1读取某行数据时,事务2也能对该行数据进行读取,更新;
当事务2更新某行数据时,事务1读取该行数据仍然是第一次读取到的版本;
事务1更新某行数据,事务2不能对其进行更新,直到事务1结束 -
可串行化
(Serilaizable):最高隔离级别,强制事务串行执行
原理
:事务在读取数据时,加表级共享锁,结束时释放;
在更新数据时,加表级排他锁,结束时释放
表现
:事务1正在读取A表中的记录时,则事务2也能读取A表,但不能对A表做更新、新增、删除,直到事务1结束。
事务1正在更新A表中的记录时,则事务2不能读取A表的任意记录,也不能更新,直到事务1结束。
而InnoDB默认工作模式为RR(Repeatable Read)模式,默认加锁为next-key锁,这样防止了幻读的发生(不能对该行数据进行修改,或者插入记录)
幻读就是说事务1读取了数据,然后这之间事务2插入了一行或者多行的满足事务1的选择条件,导致事务1再次使用相同的选择条件读取时,得到了比第一次更多的数据(发生了幻觉)
事务日志
事务日志可以提高事务的效率。
使用事务日志,存储引擎在修改表数据时,只需要修改该数据的内存拷贝,再将这个修改行为
记录到持久在硬盘上的事务日志上,而不用每次都读写磁盘修改数据。
事务日志采用的是追加
的方式,因此写日志的操作是磁盘上一小块区域的顺序I/O,而不像随机I/O需要在磁盘多个地方移动磁头,所以采用事务日志的方式相对来说快得多。
事务日志持久后,内存中被修改的数据在后台可以慢慢回刷到磁盘。如果数据的修改已经记录到事务日志并持久化,但数据本身并没有回写到磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。
多版本并发控制
可以认为MVCC是行级锁的一个变种,但是他在很多种情况下避免了加锁操作,因此开销更低。各个数据库系统的实现机制不尽相同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。
根据事务开始的时间的不同,每个事务对同一张表,同一时刻看到数据有可能是不一样的。
实现方式
不同存储引擎的MVCC的实现方式各不一样,InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。
- 一个列保存了行的创建时间
- 一个列保存行的过期时间(或删除时间)。
当然,存储的并不是实际的时间值,而是系统版本号。没开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的系统版本号,用来和查询到的每行记录的版本号进行比较。
MVCC只在REPEATABLE READ
和READ COMMITTED
两个隔离级别下工作,因为READ UNCOMMITTED
总是读取最新的数据行,而不是符合当前事务版本的数据行,而SERIALIZABLE
则会对所有读取的行都加锁