1.1. MySQL逻辑架构
- 连接/线程处理,这一层属于客户端交互部分,通常,这里还会进行授权认证,安全等功能。
- 查询缓存,解析器,优化器,属于服务层,MySQL大多数核心功能都在此,包括查询解析,分析,优化,缓存,以及所有的内置函数,所有的跨存储引擎都是在这一步实现的。
- 存储引擎实现真实的存储提取数据的操作。服务层通过API与存储引擎通信,不同存储引擎之间无法通信。
1.1.1. 连接管理和安全性
每个与服务器通信的客户端,服务器都会为其创建一个线程去处理客户端请求(就像ServerSocket和Socket一样),不过为了性能,会缓存线程,这样同一个客户端再次连接时可以直接使用。
连接之后,会进行鉴权操作,判断这个客户端能不能操作某个库。
1.1.2. 优化与执行
MySQL会解析查询请求,并对其进行各种优化,创建解析树。对于select语句,在解析查询之前,会先查看查询缓存有没有这一项,如果有的话,就直接返回,否则继续。
1.2. 并发控制
在此,仅讨论两个层面的并发控制:服务层与存储引擎层。
1.2.1. 读写锁
一般来说,解决并发控制有一个不错的方案:读写锁。读锁(共享锁)和写锁(排他锁),不说了,这个我知道。
大多数时候,MySQL锁的内部管理都是透明的。
1.2.2. 锁粒度
想要提高共享资源的并发性,那么便要减少锁的范围,一般来说,锁粒度越小,并发性就越高。但是加锁解锁也会消耗资源,过细的锁粒度会加剧这个负担,于是好的锁策略就是在锁开销和系统并发性之间取得一个良好的平衡。
MySQL对存储引擎如何实现锁并没有什么限制,现在来介绍MySQL中两种最重要的锁策略。
- 表锁。会锁定整张表,它也是最基本的锁策略。写锁的优先级高于读锁,在等待队列中,写锁可能会插队,跑到读锁队列的前面。尽管存储引擎可以有自己的实现,但是某些行为,MySQL会忽略存储引擎的锁,转而使用自己的机制,比如alter table时就会启用表锁。
- 行锁。行锁可以最大程度的支持并发处理,同样会加剧锁开销。行锁只有存储引擎实现,MySQL服务层没有这种实现。
1.3. 事务
事务指的是一组操作,它包含多个SQL语句。一个事务一旦开始,从头到尾,要么所有操作成功且更新数据;要么失败,数据不变。而不存在事务部分操作成功,部分失败的可能。
事务具有ACID特性。
- 原子性(Atomicity),一个事务是最小的不可分割的工作单元。它要么全部成功,要么全部失败。
- 一致性(Consistency),数据库总是从一个状态转移到另一个状态,而不存在部分状态更新。
- 隔离性(Isolation),通常来说,一个事务所做的修改,在最终提交发生之前,对其他事务是不可见的。
- 持久性(Durability),一旦一个事务被提交,它所做的修改就会被永久保存,而不论数据库崩溃与否。
事务可以做到这么多安全性,是基于占用一定系统资源作为前提的,所以如果需求不需要事务的话,可以选择非事务型数据库。
1.3.1. 隔离级别
隔离性其实比所定义的要复杂,在SQL标准中,定义了四种隔离级别,较低的级别可以支持更高的并发,系统开销也更低。
- 未提交读(Read Uncommitted),在这个级别中,事务的修改即使没有提交,对于其他事务也是可见的。事务读取未提交的数据,称为“脏读”。
- 提交读(Read Committed),在这个级别中,事务的修改只有提交才是可见的。但是如果在事务提交之前和提交之后都进行了读取的话,可能读取到不一致,所以这个级别又称“不可重复读”。
- 可重复读(Repeatable Read),解决了脏读问题,但是又引入了幻读问题,就是在事务读取某一范围的数据时,数据发生了改变。可以通过多版本并发控制解决。
- 可串行化(Serializable),强制保证事务的串行执行,对每行数据加锁,这样会导致大量的锁。
| 隔离级别 | 脏读可能性 | 不可重复读可能性 | 幻读可能性 | 加锁读 |
|---|---|---|---|---|
| READ UNCOMMITED | YES | YES | YES | NO |
| READ COMMITED | NO | YES | YES | NO |
| REPEATABLE READ | NO | NO | YES | NO |
| SERIALIZABLE | NO | NO | NO | YES |
关于幻读和不可重复读,可以看这篇。
不可重复读和幻读到底有什么区别呢?
- 不可重复读是前后多次读,期间有更新操作,读取了其他事务更改的数据,针对update操作
解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
- 幻读是前后多次读,期间有增加删除操作,读取了其他事务新增或删除的数据,针对insert和delete操作
解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
这时候再理解事务隔离级别就简单多了呢。
来看一张图:
1.3.2. 死锁
当发生死锁时,把持有最少行级排他锁的事务进行回滚。
1.3.3. MySQL中的事务
MySQL默认采用自动提交的方式处理事务。即,每次查询都会触发一次事务提交。
InnoDB使用的是两阶段锁定协议,在事务执行期间随时都可以执行锁定,只有在commit或rollback时才会释放锁,并且所有的锁都是在同一时刻释放;InnoDB会根据隔离级别在需要的时候自动加锁。这些都是隐式解锁,InnoDB还支持显示加锁,通过在SQL语句中实现这一功能。
1.4. 多版本并发控制
MySQL的大多数事务型存储引擎都不是简单的实现了行级锁,而是出于性能考虑,采用了称为MVCC(多版本并发控制)的解决方案。
可以认为MVCC是行级锁的一种变种,在很多情况下都可以避免加锁操作,但是它也同样实现了非阻塞的读和阻塞的写,写操作仅锁定必要的行。
MVCC技术的实现,本质上是通过保存快照来实现的。不同存储引擎的MVCC实现有所不同,但是主要有两种:乐观并发控制和悲观并发控制。下面来看看InnoDB的简略实现。
InnoDB的MVCC是通过在每行之后保存两个隐藏的列来实现的。一个是行的创建时间,一个是行的过期时间。这里可以使用系统版本号(不是OS的版本,而是数据库系统)来实现记录。每开始一个事务,系统版本号都会自动递增,事务开始时刻的系统版本号作为事务版本号。
- select:只寻找创建版本号早于事务版本号的行且删除版本未定义或晚于事务版本。(创建版本<=事务版本<删除版本)
- insert:把当前系统版本号作为行创建版本号。
- delete:把当前系统版本号作为行删除版本号。
- update:插入一行新纪录(并不是直接更新),同时使用当前系统版本号作为创建版本号,使用当前系统版本号作为原来的行的删除版本号。
MVCC只在可重复读和提交读工作(第二第三个)。