一、逻辑架构
- mysql主要可以分为服务器层和存储引擎层。服务层执行sql解析、连接管理等功能,引擎层主要做数据提取和存储。两层之间通过标准api交互。通过这种隔离方式,mysql可以很方便的替换引擎层的具体实现以实现不同需求(如innodb,MyISAM,Memeory等)。
- 在学习mysql时要注意区分哪些是在服务器层做的哪些是在引擎层面做的,在引擎层面的动作需要在具体的实现上看。
二、并发控制
无论什么系统对并发的控制都离不开锁,mysql也是如此。
1. 锁类型
从锁的用途而言一般可以分为两大类:读锁/写锁
-
读锁(共享锁)
- 一个对象可以有多个读锁
- 一个对象存在读锁的情况下,不能被更改
-
写锁(排斥锁)
- 一个对象只能有一个写锁
- 一个对象存在写锁的情况下,只能被锁持有事务操作且其他事务不可读
2. 锁粒度
mysql有表锁和行锁。表锁在服务器上实现,而行锁取决于具体的存储引擎,不是所有引擎都支持行锁的。
顾名思义,表锁是对整个表加锁,影响面非常大。而行锁只针对具体的行,影响范围比较小。
所以结合锁类型和锁粒度,mysql至少就有四种锁:表级读/写锁、行级读写锁。这四种锁的兼容关系如下:
| 锁 | 表级读锁 | 表级写锁 | 行级读锁 | 行级写锁 |
|---|---|---|---|---|
| 表级读锁 | Y | N | Y | N |
| 表级写锁 | N | N | N | N |
| 行级读锁 | Y | N | Y | N |
| 行级写锁 | N | N | N | N |
但是在只有这四种锁的情况下,表锁和行锁是无法共存的。设想以下场景:
-
事务A申请了表b中某行记录的行写锁
-
事务B此时需要申请表b的表写锁
-
此时对于mysql服务器需要做如下检查:
- 检查表b是否有已存在的表级写锁
- 检查表b的每一行是否有已存在的行级写锁。
对于步骤2将是一个非常耗时的操作,因为首先行锁是在引擎层实现的所以服务器必须的请求引擎层,其次一个表可能会存在多个行级锁,引擎需要遍历这些锁来判断是否可以加表锁。
那怎么解决这个问题呢?最简单的办法就是在表上打一个标记,标记此表是否存在行级锁。mysql其实也是这么实现的。
mysql在上述锁的基础上增加了两种表锁:读/写意向锁。意向锁的意思是说持有这个锁的事务在未来可能会请求对应的真实锁。意向锁的工作方式如下:
- 当事务请求一个表某行的读写行锁时,服务器会自动先增加一把对应的表级意向锁。
- 此时若有另外一个事务需要申请表锁时,服务器不需要逐行检查是否存在行级锁,只需要检查意向锁即可。
相比于其他锁,意向锁有一个特殊的地方,即读写意向锁是可以相互共存的。也就是说事务A加了意向锁后,事务B也可以加意向锁,这保证了mysql操作的并发性。其实这个特性也是必然的,如果意向锁也跟其他锁一样互斥,那所有操作直接加表锁就行了,但这样就会大幅降低并发性。
mysql除了以上说的6种锁外,还有其他的诸如间隙锁、next-key锁等其他锁
3. 死锁
多个事务以不同的顺序同时访问同样的资源,就会导致死锁。mysql会在事务执行前做死锁检查,也实现了死锁超时处理机制,但还是无法避免因为真正数据冲突导致的死锁,所以写事务时需要额外注意可能的死锁情况。
三、事务
1. 事务的含义
-
事务是原子操作,要么都成功,要么都不执行
-
事务ACID
- A:原子性(atomicity),操作的最小粒度,不能再分割
- C:一致性,一个状态到另外一个状态,不会存在中间状态,和原子性很相似。
- I:隔离性,事务提交前不可见,因为事务可能会回滚,所以事务没有提交前对其他事务应不可见。
- D:持久性, 提交了就永远有效,系统崩溃等不影响。但无法做到100%的持久性
-
-
为什么需要事务?
如银行转账场景,不能有任何中间状态,否则将出现账对不齐。所以对于一次转账就可以认为是一次事务,要么都成功要么都不执行。
-
事务是在引擎层面实现的,不是所有引擎实现都支持事务。mysql默认的innodb支持事务
2. 事务的四级隔离级别
-
事务隔离含义
事务隔离是指每个事务只能看到其自身和已经提交的事务修改的内容
-
隔离的必要性
如果不做隔离,不同事务会互相干扰。当事务需要回滚时无法准确的回滚事务产生的变更。
-
mysql的四种隔离级别
- 未提交可读(脏读)
- 提交可读(不可重复读。在同一个事务中,同样的sql查询结果可能会不同)
- 可重复读(同一个事务中,同样查询结果永远都是一致的。会有幻读问题,范围查询结果可能会出现幻行)
- 可串行(所有事务串行化)
mysql的默认事务级别是可重复读。
幻读和脏读的差别:
-
脏读,在同一个事务中同一条记录两次查询结果不一样
-
幻读,在同一个事务中进行范围查询时,两次结果集会不同
-
事务日志,通过事务日志提升事务效率。因为数据不需要实时刷新到磁盘(刷新内存数据即可),如果系统崩溃从日志中还原数据即可。虽然日志模式下,需要写两次磁盘,但是日志是顺序写速度更快,一旦日志写入后数据可以在后台慢慢写入磁盘中,不影响用户使用。
-
mysql默认采用自动提交,即未显示声明的情况下,每个sql都是一个事务
3. mysql高并发事务隔离的实现 - 多版本并发控制(mvvc)
如果仅通过锁来实现可重复度的事务隔离级别,将会导致数据库的并发度大幅下降。试想以下场景:
事务A要更新一行记录c,事务B也要读取记录c。事务A先开始执行,在其未完成时事务B也开始执行了。为了保证事务 B执行过程中记录c始终是不变的。那么理论上事务A需要对记录c加写锁。然后事务B需要对记录c加读锁,但此时还有写锁存在所以事 务B只能等待事务A执行完成才能开始执行。
mysql为了提升并发度采用了一种成为多版本并发控制的方式(这种方案可以运用到很多其他类似的场景下)。具体如下:
mysql会在每行记录中增加创建版本和删除版本字段(对用户不可见),然后在不同操作下通过比较操作版本号和创建、删除版本来避免加锁。
- select:只能看到创建版本小于当前版本且删除版本为空或者大于当前版本
- insert:将创建版本设置为当前版本
- update:clone一条记录,原记录的删除版本设置为当前版本。
- delete: 删除版本设置为当前版本
在这种方案对于上述场景,事务B不需要对记录a加任何锁,只需要按照其事务版本找到对应的记录即可。
当然mysql并不是正在的为每一个版本保存一个副本,而是通过其undo log来做实现的。
这种方法的缺点就死会增加了存储和额外的维护成本,但是与其带来的提升来说,这个成本是可接受的。
4. 事务日志-性能提升设计
mysql通过事务日志的方式来提升事务执行的速度。mysql在执行事务时,会先刷新内存中的数据,然后同步写一份事务日志到磁盘。当内存数据刷新完成且事务日志记录完成就认为事务已经完成。然后再后台慢慢的将数据写入磁盘。看起来这种方式多写了一次磁盘,为什么说会提升事务性能。因为对于数据写入磁盘这个操作而言通常情况下是随机写,而写日志是顺序写。显然顺序写会比随机写速度快很多(特别是在以前机械硬盘的情况下)。
5. 事务默认开启
在未指定情况下,mysql把每一条语句都会当做事务执行。