MySQL 最重要,最与众不同的是它的存储引擎架构,这种架构的设计将查询处理(Query Processing)及其他系统任务(Server Task) 和数据的存储提取相分离。
1.1 mysql逻辑架构
最上层服务并不是mysql独有的,大多数基于网络的客户端/服务器的工具或者服务都有类似的架构。比如连接处理、授权认证、安全等。
第二层架构是mysql比较有意思的部分。大多数mysql的核心服务否在这一层,包括解析 、分析、优化、缓存以及所有的内置函数,所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图。
第三层包含存储引擎。存储引擎负责mysql中数据的存储和提取。不同的引擎有不同的特点。服务器与存储引擎件通过api通信。
1.1.1 连接管理与安全性
每个客户端连接都会在服务器进程中拥有一个进程。但是mysql5.5中或者更新的版本中提供了一个Api,支付线程池(thread-pooling)插件,可以使用少量的线程,支付大量的连接。所以新版本中,一个连接销毁后,他的线程,会给其他连接继续使用。
1.1.2 优化与执行
mysql 会解析查询,并创建内部数据结构(解析树),然后对其进行各种优化,包括重写查询、决定表的读取顺序、以及选择合适的索引等。用户可以通过特殊的关键字提示优化器,影响他的决策过程,也可以通过解释器(explain)优化各个因素。
优化器并不关心使用的什么存储引擎,但存储引擎对优化是有影响的。
对于select语句查询前,会先检查缓存(Query Catch),如果能够在其中找到对应的查询,服务器就不需要再执行后续过程,而是直接返回。
1.2 并发控制
只要有多个查询需要同一时间修改数据,就会产生并发控制的问题。
1.2.1 读写锁
共享锁(share lock) 和 排他锁(exclusive),又叫读锁(read lock)和写锁(write lock)。
读锁共享,多个客户可以同时读取一个资源,互不干扰。
写锁是排他的,也就是说一个写锁会阻塞其他的读锁和写锁,这是处于安全考虑。
1.2.2 粒度锁
一种提高共享资源并发性的方式就是让锁定对象更有选择性。尽量只锁定需要修改的资源而不是所有资源。
在给定的资源上,锁定的资源数量越少,则并发程度越高,只要相互之间不发生冲突即可;但是锁也会消耗资源,锁的各种操作,都会增加系统开销。
所谓锁策略,就是在锁的开销与安全之间寻求平衡,这种平衡当然会影响性能。大多数商业数据库没有提供更多的选择,一般都是在表上施加行级锁(row-level lock)。
而mysql提供了多种选择。每种mysql引擎,都可以实现自己的锁粒度与锁策略。
表锁(table lock)
表锁是mysql中最基本的锁策略,并且是开销最小的策略。一个用户对表进行写操作(增、删、改),需要获得锁,这会阻塞其他用户对该表的读写操作。
行级锁(row lock)
行级锁可以最大限度的支持并发(同时也带来最大的锁开销)。行级锁只在引擎层有实现,在mysql服务层没有实现。
1.3 事务
事务就是一组原子性的sql查询,或者说一个独立的工作单元。如果数据库引擎能够成功的对数据库应用改组查询的全部语句,那么执行改组查询;如果其中有一条语句因为崩溃或其他原因无法执行,那么所有语句都不会执行。
事务的经典例子:
银行(bank)中有两个客户(name)张三和李四
我们需要将张三的1000元存款(sal)转到李四的账户上
我们需要怎要通过sql语句来实现这个过程
update bank set sal = sal - 1000 where name = '张三';
update bank set sal = sal + 1000 where name = '李四';
但是万一出现一些错误,比如将字段名称打错了,没有检查就执行了这个两个语句,比如
update bank set sal = sal - 1000 where name = '张三';
update bank set sale = sale + 1000 where name = '李四';
我们通过查询数据库会发现 张三依然减少了1000元但是李四却没有加钱
如果可以有一种方法使得sql语句要么都执行,要么里面有一句没有执行,就全部不执行。
事务的4大特性ACID。
原子性(atomicity),一致性(consistency),隔离性(isolation),持久性(durability)
原子性(atomicity):
一个事务必须被视为一个不可分割的最小单元,要不成功全部提交,要不失败全部回滚。
一致性(consistency):
数据库总是从一个一致性的状态转换到另一个一致性的状态。银行例子中,执行前后两个状态,但是两个账户的状态必须能对应起来。
隔离性(isolation):
通常来说,一个事务所作的修改,在最终提交前,其他事务是不可见的。
持久性(durability):
一旦事务提交,则其对数据库所做的修改就会永久保持的数据库中。
事务虽然保证了安全,但同时也消耗了资源。mysql的一个优势是,可以通过选择不同的引擎,来选择是不是使用事务,来提高效率,但是实际应用中非常难,混合引擎,可能会出现 ,不可预知的错误。
1.3.1 隔离级别
有四个隔离级别:
READ UNCOMMITED(未提交读):
在这个级别,事务的修改,即使没有提交,对其他事务也是可见的。事务可以读取到未提交的数据,这也被称为“脏读”。这一级别性能相比其他级别性能提升没有很多,但是问题很多,所以应用很少。
READ COMMITED(提交读):
大多数数据库的默认级别都是READ COMMITED,但是mysql不是。这一级别满足前面说的“隔离性”,的定义:一个事务开始时,只能看见,已经提交的事务所作的修改。,这个级别也叫“不可重复读(norepeatedable read)”,因为两次执行同样的查询,可能会得到不一样的结果。
REPEATABLE READ(重复读):
READ COMMITED解决了脏读的问题。该级别保证了多次读取同一级别数据是一致的。但还是会出现幻读。
InnoDB与XtraDB引擎通过多版本并发控制(MVVC, Multiversion Concurrency Controller)解决了,幻读的问题。
本级别是mysql默认级别。
READ COMMITED与REPEATABLE READ具体可参考文章
https://blog.csdn.net/tolcf/article/details/49311035
个人对这篇文章的理解是赞同的。
SERIALIZABLE(可串行化):
本级别是最高隔离级别。它通过强制事务串行执行,解决了幻读的问题。简单来说SERIALIZABLE会在读取的每一条数据上都加锁,会造成大量的超时与锁争用问题。所以本级别也很少使用。
1.3.2 死锁
死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。
以StockPrice表上的两个事务为例:
事务1
START TRANSACTION;
UPDATE StockPrice SET close= 45.50 WHERE stock_id = 4 and date= '2002-05-01';
UPDATE StockPrice SET close= 19.80 WHERE stock_id = 3 and date= '2002-05-02';
COMMIT;
事务 2
START TRANSACTION;
UPDATE StockPrice SET high = 20.12 WHERE stock_id = 3 anddate= '2002-05-02';
UPDATE StockPrice SET high = 20.12 WHERE stock_id = 4 anddate= '2002-05-01';
COMMIT;
如果不走运的话,每个事务都可以执行完第一个语句,并在过程中锁住资源。然后每个事务都试图去执行第二行语句,当时却发现它被锁住了。两个事务将永远的等待对方完成,除非有其他原因打断死锁。
为了解决这个问题,数据库实现了各种死锁探查和超时机制。像InnoDB这样复杂的存储引擎会提示循环依赖并且立即返回错误。否则死锁将会导致查询非常缓慢。其他一些不好的做法是等待超时后放弃。当前InnoDB处理死锁的方式是回滚持有最少排他行级锁的事务。(几乎最简单的回滚的参考指标)
锁的行为是顺序是存储引擎决定的。因此,一些存储引擎可能会在特定的操作顺序下发生死锁,其他的可能没有。死锁有两种:一些是因为实际数据冲突而无法避免,一些是因为存储引擎的工作方式产生。
只有部分或者完全回滚其中的一个事务才可能打破死锁。死锁是事务系统中客观存在的事实,你的应该在设计上必须应该考虑处理死锁。一些业务系统可以从头重试事务。
1.4多版本控制mvvc
MySQL的大多数事务型存储引擎实现的其实都不是简单的行级锁。基于提升并发性能的考虑, 它们一般都同时实现了多版本并发控制(MVCC)。不仅是MySQL, 包括Oracle,PostgreSQL等其他数据库系统也都实现了MVCC, 但各自的实现机制不尽相同, 因为MVCC没有一个统一的实现标准。
可以认为MVCC是行级锁的一个变种, 但是它在很多情况下避免了加锁操作, 因此开销更低。虽然实现机制有所不同, 但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
MVCC的实现方式有多种, 典型的有乐观(optimistic)并发控制 和 悲观(pessimistic)并发控制。
MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。
简单实现:
SELECT:
当隔离级别是REPEATABLE READ时select操作,InnoDB必须每行数据来保证它符合两个条件:
1、InnoDb只查找版本号早于当前事务版本号的数据行,这样可以保证查找到的数据,要么在查找前已经存在,要么事务自身修改或查找过。
2、这行数据的删除版本必须是未定义的或者比事务版本要大。这可以保证在事务开始之前这行数据没有被删除。
符合这两个条件的行可能会被当作查询结果而返回。
INSERT:
InnoDB为删除的每一行保存当前系统版本号作为行版本号。
DELETE:
InnoDB为新插入的每一行保存当前系统版本号作为删除标识。
UPDATE:
InnoDB会写一个这行数据的新拷贝,这个拷贝的版本为当前的系统版本号。它同时也会将这个版本号写到旧行的删除版本里。
这种额外的记录所带来的结果就是对于大多数查询来说根本就不需要获得一个锁。他们只是简单地以最快的速度来读取数据,确保只选择符合条件的行。这个方案的缺点在于存储引擎必须为每一行存储更多的数据,做更多的检查工作,处理更多的善后操作。