MYSQL
MySQL是最流行的关系型数据库管理系统,在WEB应用方面MySQL是最好的RDBMS(Relational Database Management System:关系数据库管理系统)应用软件之一。它是C和C++语言编写的、支持多个操作系统、支持多线程、为多种编程语言提供API、优化SQL算法提高了查询速度以及提供用于管理和检查数据库的管理工具。
逻辑架构
图片来源:blog.csdn.net/fuzhongmin0…
特性
索引
Mysql的四种隔离级别www.jianshu.com/p/8d735db9c…
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。\
Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
Repeatable Read(可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:
脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
在MySQL中,实现了这四种隔离级别,分别有可能产生问题如下所示:
ACID
MySQL 作为一个关系型数据库,以最常见的 InnoDB 引擎来说,是如何保证 ACID 的。\
(Atomicity )原子性
原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。
下面说回undo log。实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB 实现回滚,靠的是undo log :当事务对数据库进行修改时,InnoDB 会生成对应的undo log ;如果事务执行失败或调用了rollback ,导致事务需要回滚,便可以利用undo log 中的信息将数据回滚到修改之前的样子。
undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。
以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
(Consistency)一致性
一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。 数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。
(Isolation)隔离性
并发访问数据库时,一个事务不被其他事务所干扰。事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的 Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。
(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
(一个事务)写操作对(另一个事务)读操作的影响:MVCC 保证隔离性
- 锁机制 首先来看两个事务的写操作之间的相互影响。隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB 通过锁机制来保证这一点。 锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
- 行锁与表锁 按照粒度,锁可以分为表锁、行锁以及其他位于二者之间的锁。表锁在操作数据时会锁定整张表,并发性能较差;行锁则只锁定需要操作的数据,并发性能好。但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。MySQL 中不同的存储引擎支持的锁是不一样的,例如 MyIsam 只支持表锁,而 InnoDB 同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。
(Durability)持久性
持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
redo log和undo log都属于InnoDB的事务日志。下面先聊一下redo log存在的背景。
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
于是,redo log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。
锁
表锁和行锁
- InnoDB支持表级和行级的粒度锁,表锁就是对整张表进行加锁,而行锁则是锁定某行、某几行数据或者行之间的间隙。
- 行锁作用在索引上,使用主键索引(聚簇索引)会加一把锁,使用二级索引则会在主键索引和对应的二级索引上都加锁。
- 当操作多行数据时,会一行一行的加锁和返回数据
表锁
加锁过程的开销小,加锁的速度快;不会出现死锁的情况;锁定的粒度大,发生锁冲突的几率大,并发度低;
- 一般在执行DDL语句时会对整个表进行加锁,比如说 ALTER TABLE 等操作;
- 如果对InnoDB的表使用行锁,被锁定字段不是主键,也没有针对它建立索引的话,那么将会锁整张表;
- 表级锁更适合于以查询为主,并发用户少,只有少量按索引条件更新数据的应用,如Web 应用。
行锁
加锁过程的开销大,加锁的速度慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;
- 最大程度的支持并发,同时也带来了最大的锁开销。
- 在 InnoDB 中,除单个 SQL 组成的事务外,锁是逐步获得的,这就决定了在 InnoDB 中发生死锁是可能的。
- 行级锁只在存储引擎层实现,而 MySQL 服务器层没有实现。 行级锁更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。
共享锁和排他锁
- 共享锁(S):加了锁的记录,所有事务都能去读取但不能修改,同时阻止其他事务获得相同数据集的排他锁;加锁方法:
select * from table_name where ... lock in share mode - 排他锁(X):允许已经获得排他锁的事务去更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁;加锁方法:
select * from table_name where ... for update
意向锁
意向锁也是表级锁,分为读意向锁(IS锁)和写意向锁(IX锁)。当事务要在记录上加上行锁时,要首先在表上加上意向锁。这样判断表中是否有记录正在加锁就很简单了,只要看下表上是否有意向锁就行了,从而就能提高效率。
意向锁之间是不会产生冲突的,它只会阻塞表级读锁或写锁。意向锁不于行级锁发生冲突。
行锁的分类
记录锁(Record Lock)
记录锁最简单的一种行锁形式,上面我们以及稍微提及过了。这里补充下的点就是:行锁是加在索引上的,如果当你的查询语句不走索引的话,那么它就会升级到表锁,最终造成效率低下,所以在写SQL语句时需要特别注意。
间隙锁(Gap Lock)
当我们使用范围条件而不是相等条件去检索,并请求锁时,InnoDB就会给符合条件的记录的索引项加上锁;而对于键值在条件范围内但并不存在(参考上面所说的空闲块)的记录,就叫做间隙,InnoDB在此时也会对间隙加锁,这种记录锁+间隙锁的机制叫Next-Key Lock。额,扯的有点快。 上面这段话表明间隙锁是可以共存的,共享间隙锁与独占间隙锁之间是没有区别的,两者之间并不冲突。其存在的目的都是防止其他事务往间隙中插入新的纪录,故而一个事务所采取的间隙锁是不会去阻止另外一个事务在同一个间隙中加锁的。
当然也不是在什么时候都会去加间隙锁的:在 RU 和 RC 两种隔离级别下,即使你使用 select in share mode 或 select for update,也无法防止幻读(读后写的场景)。因为这两种隔离级别下只会有行锁,而不会有间隙锁。而如果是 RR 隔离级别的话,就会在间隙上加上间隙锁。
临键锁(Next-key Lock)
临键锁是记录锁与与间隙锁的结合,所以临键锁与间隙锁是一个同时存在的概念。
关于临键锁与幻读,官方文档有这么一条说明: 就是说 MySQL 默认隔离级别是RR,在这种级别下,如果你使用 select in share mode 或者 select for update 语句,那么InnoDB会使用临键锁(记录锁 + 间隙锁),因而可以防止幻读;即使你的隔离级别是 RR,如果你这是使用普通的select语句,那么此时 InnoDB 引擎将是使用快照读,而不会使用任何锁,因而还是无法防止幻读。(其实普通读应该是快照读没错,但是快照读应该是不会有幻读幻读问题,mmp)。
插入意向锁(Insert Intention Lock)
插入意图锁是一种间隙锁,在行执行 INSERT 之前的插入操作设置。如果多个事务 INSERT 到同一个索引间隙之间,但没有在同一位置上插入,则不会产生任何的冲突。假设有值为4和7的索引记录,现在有两事务分别尝试插入值为 5 和 6 的记录,在获得插入行的排他锁之前,都使用插入意向锁锁住 4 和 7 之间的间隙,但两者之间并不会相互阻塞,因为这两行并不冲突。
插入意向锁只会和 间隙或者 Next-key 锁冲突,正如上面所说,间隙锁作用就是防止其他事务插入记录造成幻读,正是由于在执行 INSERT 语句时需要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操作的执行。
死锁的产生和避免
产生:两个session加锁的顺序不一致
避免:调整加锁顺序、注意gap锁