mysql复习【面试】
- 前言
- mysql复习
- 第08章 索引的创建与设计原则
- 3.索引设计原则
- 3.2哪些情况下适合创建索引
- 3.4 哪些情况不适合创建索引
- 第10章 索引优化与查询优化
- 2.索引失效案例
- 8.覆盖索引
- 9. 如何给字符串添加索引
- 10. 索引下推
- 11. 普通索引 vs 唯一索引
- 12.其他的优化策略
- 13. 淘宝数据库,主键如何设计的?
- 第11章 数据库的设计规范
- 第13章 事务基础知识
- 3.2数据的并发问题
- 3.3 SQL中的四种隔离级别
- 第14章 MySQL事务日志
- 1.redo日志
- 2.undo日志
- 第15章 锁
- 2. MySQL并发事务访问相同记录
- 3. 锁的不同角度分类
- 第16章 多版本并发控制
- 1. 什么是MVCC
- 2. 快照读与当前读
- 4. MVCC实现原理之ReadView
- 6. 总结
前言
写作于
2022-11-10 12:36:33
发布于
2022-11-20 16:11:40
mysql复习
第08章 索引的创建与设计原则
第08章 索引的创建与设计原则【2.索引及调优篇】【MySQL高级】
3.索引设计原则
3.2哪些情况下适合创建索引
1.字段的数值有唯一性限制
2.频繁作为 WHERE 查询条件的字段
3.经常 GROUP BY 和 ORDER BY 的列
4.UPDATE、DELETE 的 WHERE 条件列
5.DISTINCT 字段需要创建索引
6. 多表 JOIN 连接操作时,创建索引注意事项
首先,连接表的数量尽量不要超过 3 张,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率。
其次, 对 WHERE 条件创建索引 ,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,没有 WHERE 条件过滤是非常可怕的。
最后, 对用于连接的字段创建索引 ,并且该字段在多张表中的 类型必须一致 。比如 course_id 在student_info 表和 course 表中都为 int(11) 类型,而不能一个为 int 另一个为 varchar 类型。
7.使用列的类型小的创建索引
8. 使用字符串前缀创建索引
9. 区分度高(散列性高)的列适合作为索引
10.使用最频繁的列放到联合索引的左侧
11. 在多个字段都要创建索引的情况下,联合索引优于单值索引
3.4 哪些情况不适合创建索引
1.在where中使用不到的字段,不要设置索引
2. 数据量小的表最好不要使用索引
3. 有大量重复数据的列上不要建立索引
4. 避免对经常更新的表创建过多的索引
5. 不建议用无序的值作为索引
6. 删除不再使用或者很少使用的索引
7. 不要定义冗余或重复的索引
第10章 索引优化与查询优化
第10章 索引优化与查询优化【2.索引及调优篇】【MySQL高级】
2.索引失效案例
2.1全值匹配
2.2最佳左前缀法则
2.3主键插入顺序
2.4 计算、函数、类型转换(自动或手动)导致索引失效
2.5 类型转换导致索引失效
2.6 范围条件右边的列索引失效
2.7 不等于(!= 或者<>)索引失效
2.8 is null可以使用索引,is not null无法使用索引
2.9 like以通配符%开头索引失效
2.10 OR 前后存在非索引的列,索引失效
2.11 不同的字符集进行比较前需要进行 转换、会造成索引失效
2.12练习及一般性建议练习:
假设: index(a,b.c)
一般性建议:
对于单列索引,尽量选择针对当前query过滤性更好的索引
在选择组合索引的时候,当前query中过滤性最好的字段在索引字段顺序中,位置越靠前越好
在选择组合索引的时候,尽量选择能够包含当前query中的where了句中更多字段的索引
在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面
8.覆盖索引
覆盖索引: SQL只需要通过索引就可以返回查询所需要的数据,而不必通过二级索引查到主键之后再去查询数据
理解方式一: 索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引。
理解方式二: 非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段)。
简单说就是, 索引列+主键 包含 SELECT 到 FROM之间查询的列 。
9. 如何给字符串添加索引
前缀索引
9.1 前缀索引
MySQL是支持前缀索引的。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串
alter table teacher add index index1(email);
#或
alter table teacher add index index2(email(6))
10. 索引下推
ICP的使用条件:
① 只能用于二级索引(secondary index)
② explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者 ref_or_null
③ 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录到server端做where过滤。
④ ICP可以用于MyISAM和InnnoDB存储引擎
⑤ MySQL 5.6版本的不支持分区表的ICP功能,5.7版本的开始支持。
⑥ 当SQL使用覆盖索引时,不支持ICP优化方法
11. 普通索引 vs 唯一索引
11.3 change buffer的使用场景
普通索引和唯一索引应该怎么选择?其实,这两类索引在查询能力上是没差别的,主要考虑的是对 更新性能 的影响。所以,建议你 尽量选择普通索引 。
在实际使用中会发现, 普通索引 和 change buffer 的配合使用,对于数据量大的表的更新优化还是很明显的。
如果所有的更新后面,都马上 伴随着对这个记录的查询 ,那么你应该 关闭change buffer 。而在其他情况下,change buffer都能提升更新性能。
由于唯一索引用不上change buffer的优化机制,因此如果 业务可以接受 ,从性能角度出发建议优先考虑非唯一索引。但是如果"业务可能无法确保"的情况下,怎么处理呢?
首先, 业务正确性优先 。我们的前提是“业务代码已经保证不会写入重复数据”的情况下,讨论性能问题。如果业务不能保证,或者业务就是要求数据库来做约束,那么没得选,必须创建唯一索引。
这种情况下,本节的意义在于,如果碰上了大量插入数据慢、内存命中率低的时候,给你多提供一个排查思路。
然后,在一些“ 归档库 ”的场景,你是可以考虑使用唯一索引的。比如,线上数据只需要保留半年,然后历史数据保存在归档库。这时候,归档数据已经是确保没有唯一键冲突了。要提高归档效率,可以考虑把表里面的唯一索引改成普通索引
12.其他的优化策略
12.1 EXISTS 和 IN 的区分
12.2 COUNT(*)与COUNT(具体字段)效率
12.3 关于SELECT(*)
13. 淘宝数据库,主键如何设计的?
13.1 自增ID的问题
自增ID做主键,简单易懂,几乎所有数据库都支持自增类型,只是实现上各自有所不同而已。自增ID除了简单,其他都是缺点,总体来看存在以下几方面的问题:
- 可靠性不高
存在自增ID回溯的问题,这个问题直到最新版本的MySQL 8.0才修复 - 安全性不高
对外暴露的接口可以非常容易猜测对应的信息。比如:/User/1/这样的接口,可以非常容易猜测用户ID的值为多少,总用户数量有多少,也可以非常容易地通过接口进行数据的爬取 - 性能差
自增ID的性能较差,需要在数据库服务器端生成 - 交互多
业务还需要额外执行一次类似 last_insert_id() 的函数才能知道刚才插入的自增值,这需要多一次的网络交互。在海量并发的系统中,多1条SQL,就多一次性能上的开销 - 局部唯一性
最重要的一点,自增ID是局部唯一,只在当前数据库实例中唯一,而不是全局唯一,在任意服务器间都是唯一的。对于目前分布式系统来说,这简直就是噩梦
在淘宝的电商业务中,订单服务是一个核心业务。请问,订单表的主键淘宝是如何设计的呢?是自增ID吗?
打开淘宝,看一下订单信息:
可以发现,订单号不是自增ID!
订单号是19位的长度,且订单的最后5位都是一样的,都是08113。且订单号的前面14位部分是单调递增的。
大胆猜测,淘宝的订单ID设计应该是:
这样的设计能做到全局唯一,且对分布式系统查询极其友好
13.4 推荐的主键设计
UUID
第11章 数据库的设计规范
第11章 数据库的设计规范【2.索引及调优篇】【MySQL高级】
2.4 第一范式(1st NF)
第一范式主要是确保数据表中每个字段的值必须具有原子性,也就是说数据表中每个字段的值为不可再次拆分的最小数据单元
在设计某个字段的时候,对于字段X来说,不能把字段X拆分成字段X-1和字段X-2。事实上,任何的DBMS都会满足第一范式的要求,不会将字段进行拆分。
2.5 第二范式(2nd NF)
第二范式要求,在满足第一范式的基础上,还要满足数据表里的每一条数据记录,都是可唯一标识的。而且所有非主键字段,都必须完全依赖主键,不能只依赖主键的一部分。如果知道主键的所有属性的值,就可以检索到任何元组(行)的任何属性的任何值。(要求中的主键,其实可以拓展替换为候选键)
1NF 告诉我们字段属性需要是原子性的,而 2NF 告诉我们一张表就是一个独立的对象,一张表只表达一个意思
小结:第二范式(2NF)要求实体的属性完全依赖主关键字。如果存在不完全依赖,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与元实体之间是一对多的关系。
2.6 第三范式(3rd NF)
第三范式是在第二范式的基础上,确保数据表中的每一个非主键字段都和主键字段直接相关,也就是说,要求数据表中的所有非主键字段不能依赖于其他非主键字段。(即,不能存在非主属性A依赖于非主属性B,非主属性B依赖于主键c的情况,即存在"A一>B一>C"的决定关系)通俗地讲,该规则的意思是所有非主键属性之间不能有依赖关系,必须相互独立。
这里的主键可以拓展为候选键。
2.7小结
关于数据表的设计,有三个范式要遵循
(1) 第一范式(1NF),确保每列保持原子性
数据库的每一列都是不可分割的原子数据项,不可再分的最小数据单元,而不能是集合、数组、记录等非原子数据项
(2) 第二范式(2NF),确保每列都和主键完全依赖
尤其在复合主键的情况下,非主键部分不应该依赖于部分主键
(3)第三范式(3NF)确保每列都和主键列直接相关,而不是间接相关
第13章 事务基础知识
第13章 事务基础知识【3.事务篇】【MySQL高级】
3.2数据的并发问题
1. 脏写( Dirty Write
)
对于两个事务 Session A、Session B,如果事务Session A 修改了
另一个 未提交
事务Session B 修改过
的数据,那就意味着发生了 脏写
这里大家对事务的隔离级比较了解的话,会发现默认隔离级别下,上面SessionA中的更新语句会处于等待状态,这里只是跟大家说明一下会出现这样现象。
2. 脏读( Dirty Read
)
对于两个事务 Session A、Session B,Session A 读取
了已经被 Session B 更新
但还 没有被提交
的字段。之后若 Session B 回滚
,Session A 读取
的内容就是 临时且无效
的。
3. 不可重复读( Non-Repeatable Read
)
对于两个事务Session A、Session B,Session A 读取
了一个字段,然后 Session B 更新
了该字段。 之后Session A 再次读取
同一个字段, 值就不同
了。那就意味着发生了不可重复读。
4. 幻读( Phantom
)
对于两个事务Session A、Session B, Session A 从一个表中 读取
了一个字段, 然后 Session B 在该表中 插入
了一些新的行。 之后, 如果 Session A 再次读取
同一个表, 就会多出几行。那就
意味着发生了幻读。
注意1:
有的同学会有疑问,那如果Session B中删除了
一些符合studentno > 的记录而不是插入新记录,那SessionA之后再根据studentno > 0
的条件读取的记录变少了,这种现象算不算幻读呢?这种现象不属于幻读
,幻读强调的是一个事务按照某个相同条件多次读取
记录时,后读取时读到了之前没有读到的记录
。
注意2:
那对于先前已经读到的记录,之后又读取不到这种情况,算啥呢?这相当于对每一条记录都发生了不可重复读
的现象。幻读只是重点强调了读取到了之前读取没有获取到的记录
3.3 SQL中的四种隔离级别
上面介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题有轻重缓急之分,我们给这些问题按照严重性来排一下序:
我们愿意舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,并发问题发生的就越多。 SQL标准
中设立了4个 隔离级别
:
-
READ UNCOMMITTED
:读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。 -
READ COMMITTED
:读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在。 -
REPEATABLE READ
:可重复读,事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。这是MySQL的默认隔离级别。 -
SERIALIZABLE
:可串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。
SQL标准
中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:
YES表示没有解决
脏写
怎么没涉及到?因为脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生。
RR
其实RR(Mysql默认隔离级别)也是可以避免幻读的,通过对select操作手动加行X锁(独占锁)
(SELECT … FOR UPDATE这也正是SERIALIZABLE 隔离级别下会隐式为你做的事情),同时,即便当前记录不存在,比如id = 3是不存在的,当前事务也会获得一把记录锁(因为InnoDB的行锁锁定的是索引,故记录实体存在与否没关系,存在就加行X锁
,不存在就加间隙锁
),其他事务则无法插入此索引的记录,故杜绝了幻读。
第14章 MySQL事务日志
第14章 MySQL事务日志【3.事务篇】【MySQL高级】
事务有4种特性:原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢?
事务的隔离性由锁机制实现
而事务的原子性、一致性和持久性由事务的redo日志和undo日志来保证。
REDO LOG称为重做日志,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的特久性。
UNDO LOG称为回滚日志,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。
1.redo日志
InnoDB存储引擎是以页为单位来管理存储空间的。在真正访问页面之前,需要把在磁盘上的页缓存到内存中的Buffer Pool之后才可以访问。所有的变更都必须先更新缓冲池中的数据,然后缓冲池中的脏页会以一定的频率被刷入磁盘( checkPoint机制),通过缓冲池来优化CPU和磁盘之间的鸿沟,这样就可以保证整体的性能不会下降太快。
InnoDB引擎的事务采用了WAL技术(Write-Ahead Logging ),这种技术的思想就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功,这里的日志就是redo log。当发生宕机且数据未刷到磁盘的时候,可以通i过redo log来恢复,保证ACID中的D,
这就是redo log的作用。
体会:
Write-Ahead Log(预先日志持久化):在持久化一个数据页之前,先将内存中相应的日志页持久化。
1.5 redo log的刷盘策略
针对这种情况,InnoDB给出innodb_flush_log_at_trx_commit 参数,该参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:
设置为0 :表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日志的同步)
设置为1 :表示每次事务提交时都将进行同步,刷盘操作( 默认值 )
设置为2 :表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os自己决定什么时候同步到磁盘文件。
2.undo日志
redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中 更新数据 的 前置操作 其实是要先写入一个 undo log 。
2.2 Undo日志的作用
作用1:回滚数据
用户对undo日志可能有误解:undo用于将数据库物理地恢复到执行语句或事务之前的样子。但事实并非如此。undo是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。
这是因为在多用户并发系统中,可能会有数十、数百甚至数千个并发事务。数据库的主要任务就是协调对数据记录的并发访问。比如,一个事务在修改当前一个页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这样会影响其他事务正在进行的工作。
作用2:MVCC
undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取
补充:
purge线程两个主要作用是:清理undo页和清除page里面带有Delete_Bit标识的数据行。在InnoDB中,事务中的Delete操作实际上并不是真正的删除掉数据行,而是一种Delete Mark操作,在记录上标识Delete_Bit,而不删除记录。是一种"假删除";只是做了个标记,真正的删除工作需要后台purge线程去完成
第15章 锁
第15章 锁【3.事务篇】【MySQL高级】
2. MySQL并发事务访问相同记录
2.2 写-写情况
写-写 情况,即并发事务相继对相同的记录做出改动。
在这种情况下会发生脏写的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们 排队执行 ,这个排队的过程其实是通过 锁 来实现的。
2.4 并发问题的解决方案
怎么解决 脏读 、 不可重复读 、 幻读 这些问题呢?其实有两种可选的解决方案:
方案一:读操作利用多版本并发控制( MVCC ,下章讲解),写操作进行 加锁 。
所谓的MVCC,就是生成一个ReadView,通过ReadView找到符合条件的记录版本((历史版本由undo日志构建)。查询语句只能读到在生成ReadView之前已提交事务所做的更改,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作肯定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写操作并不冲突
普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。
在 READ COMMITTED 隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了 事务不可以读取到未提交的事务所做的更改 ,也就是避免了脏读现象;
在 REPEATABLE READ 隔离级别下,一个事务在执行过程中只有 第一次执行SELECT操作 才会生成一个ReadView,之后的SELECT操作都 复用 这个ReadView,这样也就避免了不可重复读 和幻读的问题
方案二:读、写操作都采用加锁的方式
如果我们的一些业务场景不允许读取记录的旧版本,而是每次都必须去读取记录的最新版本。比如,在银行存款的事务中,你需要先把账户的余额读出来,然后将其加上本次存款的数额,最后再写到数据库中。在将账户余额读取出来后,就不想让别的事务再访问该余额,直到本次存款事务执行完成,其他事务才可以访问账户的余额。这样在读取记录的时候就需要对其进行加锁操作,这样也就意味着读操作和写操作也像写-写操作那样排队执行。
脏读的产生是因为当前事务读取了另一个未提交事务写的一条记录,如果另一个事务在写记录的时候就给这条记录加锁,那么当前事务就无法继续读取该记录了,所以也就不会有脏读问题的产生了。
不可重复读的产生是因为当前事务先读取一条记录,另外一个事务对该记录做了改动之后并提交之后,当前事务再次读取时会获得不同的值,如果在当前事务读取记录时就给该记录加锁,那么另一个事务就无法修改该记录,自然也不会发生不可重复读了
幻读问题的产生是因为当前事务读取了一个范围的记录,然后另外的事务向该范围内插入了新记录,当前事务再次读取该范围的记录时发现了新插入的新记录。采用加锁的方式解决幻读问题就有一些麻烦,因为当前事务在第一次读取记录时幻影记录并不存在,所以读取的时候加锁就有点尴尬(因为你并不知道给谁加锁)
小结对比发现:
采用 MVCC 方式的话, 读-写 操作彼此并不冲突, 性能更高 。
采用 加锁 方式的话, 读-写 操作彼此需要 排队执行 ,影响性能。
一般情况下当然愿意采用 MVCC来解决 读-写 操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用 加锁 的方式执行。下面就讲解下MySQL中不同类别的锁。、
3. 锁的不同角度分类
锁的分类图,如下:
3.1从数据操作的类型划分:读锁、写锁
对于数据库中并发事务的读-读情况并不会引起什么问题。对于写-写、读-写或写-读这些情况可能会引起一些问题,需要使用MVCC或者加锁的方式来解决它们。在使用加锁的方式解决问题时,由于既要允许读-读情况不受影响,又要使写-写、读-写或写-读情况中的操作相互阻塞,所以MySQL实现一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为共享锁(Shared Lock,S Lock)和排他锁(Exclusive Lock,X Lock),也叫读锁(readlock)和写锁(write lock)
读锁:也称为共享锁、英文用S表示。,针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的。
写锁:也称为排他锁、英文用X表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源
需要注意的是对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上。
举例(行级读写锁)︰如果一个事务T1已经获得了某个行r的读锁,那么此时另外的一个事务T2是可以去获得这个行r的读锁的,因为读取操作并没有改变行r的数据;但是,如果某个事务T3想获得行r的写锁,则它必须等待事务T1、T2释放掉行r上的读锁才行。
总结:这里的兼容是指对同一张表或记录的锁的兼容性情况
兼容情况 | X锁 | S锁 |
X锁 | 不兼容 | 不兼容 |
S锁 | 不兼容 | 兼容 |
1.表锁(Table Lock)
① 表级别的S锁、X锁
在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的S锁
或者X锁
的。在对某个表执行一些诸如ALTER TABLE
、DROP TABLE
这类的DDL
语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行DDL语句也会发生阻塞。这个过程其实是通过在server层
使用一种称之为元数据锁
(英文名: Metadata Locks
,简称MDL
)结构来实现的。
一般情况下,不会使用InnoDB存储引擎提供的表级别的 S锁
和 X锁
。只会在一些特殊情况下,比方说 崩溃恢复
过程中用到。比如,在系统变量 autocommit=0
,innodb_table_locks = 1
时, 手动
获取InnoDB存储引擎提供的表t 的 S锁
或者 X锁
可以这么写:
-
LOCK TABLES t READ
:InnoDB存储引擎会对表 t 加表级别的 S锁
。 -
LOCK TABLES t WRITE
:InnoDB存储引擎会对表 t 加表级别的 X锁
。
不过尽量避免在使用InnoDB存储引擎的表上使用 LOCK TABLES
这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的 行锁
,关于InnoDB表级别的 S锁
和 X锁
大家了解一下就可以了
② 意向锁 (intention lock)
InnoDB 支持 多粒度锁(multiple granularity locking)
,它允许 行级锁
与 表级锁
共存,而意向锁就是其中的一种 表锁
。
1、意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。
2、意向锁是一种不与行级锁冲突表级锁
,这一点非常重要。
3、表明“某个事务正在某些行持有了锁或该事务准备去持有锁”
意向锁分为两种:
- 意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁)
- 意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁)
即:意向锁是由存储引擎 自己维护的
,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行 所在数据表的对应意向锁
。
因为共享锁与排他锁互斥,所以事务B在试图对teacher表加共享锁的时候,必须保证两个条件
(1)当前没有其他事务持有teacher表的排他锁
(2)当前没有其他事务持有teacher 表中任意一行的排他锁。
为了检测是否满足第二个条件,事务B必须在确保teacher表不存在任何排他锁的前提下,去检测表中的每一行是否存在排他锁。很明显这是一个效率很差的做法,但是有了意向锁之后,情况就不一样了。
意向锁是怎么解决这个问题的呢?首先需要知道意向锁之间的兼容互斥性,如下所示:
兼容性 | 意向共享锁(lS) | 意向排他锁(IX) |
意向共享锁(IS) | 兼容 | 兼容 |
意向排他锁(IX) | 兼容 | 兼容 |
即意向锁之间是互相兼容的,虽然意向锁和自家兄弟互相兼容,但是它会与普通的排他/共享锁互斥。
兼容性 | 意向共享锁(lS) | 意向排他锁(IX) |
共享锁(IS) | 兼容 | 互斥 |
排他锁(IX) | 互斥 | 互斥 |
注意这里的排他/共享锁指的都是表锁,意向锁不会与行级的共享/排他锁互斥。
从上面的案例可以得到如下结论:
- InnoDB支持
多粒度锁
,特定场景下,行级锁可以与表级锁共存。 - 意向锁之间互不排斥,但除了IS与S兼容外,
意向锁会与共享锁 / 排他锁互斥
。 - IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
- 意向锁在保证并发性的前提下,实现了
行锁和表锁共存
且满足事务隔离性
的要求。
③ 自增锁(AUTO-INC锁)
④ 元数据锁(MDL锁)
MySQL5.5引入了meta data lock,简称MDL锁,属于表锁范畴。MDL的作用是,保证读写的正确性。比如,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更
,增加了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的
因此,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。
读锁之间不互斥,因此可以有多个线程同时对一张表增删改查。读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性,解决了DML和DDL操作之间的一致性问题。不需要显式使用
,在访问一个表的时候会被自动加上。
2. InnoDB中的行锁
行锁(Row Lock)也称为记录锁,顾名思义,就是锁住某一行(某条记录row)。需要的注意的是,MySQL服务器层并没有实现行锁机制,行级锁只在存储引擎层实现。
优点: 锁定力度小,发生锁冲突概率低
,可以实现的并发度高
。
缺点: 对于锁的开销比较大
,加锁会比较慢,容易出现死锁
情况。
InnoDB与MylSAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。
① 记录锁(Record Locks)
记录锁也就是仅仅把一条记录锁上,官方的类型名称为: LOCK_REC_NOT_GAP
。比如把id值为8的那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为8的记录,对周围的数据没有影响
记录锁是有S锁和X锁之分的,称之为 S型记录锁
和 X型记录锁
。
- 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
- 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
② 间隙锁(Gap Locks)
MySQL
在 REPEATABLE READ
隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用 MVCC
方案解决,也可以采用 加锁
方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些 幻影记录
加上 记录锁
。InnoDB提出了一种称之为Gap Locks
的锁,官方的类型名称为: LOCK_GAP
,我们可以简称为 gap锁
。比如,把id值为8的那条记录加一个gap锁的示意图如下。
图中id值为8的记录加了gap锁,意味着 不允许别的事务在id值为8的记录前边的间隙插入新记录
,其实就是id列的值(3, 8)这个区间的新记录是不允许立即插入的。比如,有另外一个事务再想插入一条id值为4的新记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3, 8)中的新记录才可以被插入。
gap锁的提出仅仅是为了防止插入幻影记录而提出的。虽然有共享gap锁
和独占gap锁
这样的说法,但是它们起到的作用是相同的。而且如果对一条记录加了gap锁〈不论是共享gap锁还是独占gap锁),并不会限制其他事务对这条记录加记录锁或者继续加gap锁。
举例:
Session1 | Session2 |
select * from student where id = 5 lock in share mode; | |
select * from student where id = 5 for update; |
这里session 2并不会被堵住。因为表里并没有id=5这个记录,因此 session 1加的是间隙锁(3,8)。而session 2也是在这个间隙加的间隙锁。它们有共同的目标,即:保护这个间隙,不允许插入值。但,它们之间是不冲突的。
注意,给一条记录加了gap锁
只是不允许
其他事务往这条记录前边的间隙插入新记录
,那对于最后一条记录之后的间隙,也就是student 表中id值为20
的记录之后的间隙该咋办呢?也就是说给哪条记录加gap锁
才能阻止其他事务插入id值
在(20,正无穷)
这个区间的新记录呢?这时候我们在讲数据页时介绍的两条伪记录派上用场了:
- ·Infimum·记录,表示该页面中最小的记录。
-
Supremum
记录,表示该页面中最大的记录。
为了实现阻止其他事务插入id值在(20, 正无穷)这个区间的新记录,可以给索引中的最后一条记录,也就是id值为20的那条记录所在页面的Supremum记录加上一个gap锁,如图所示
③ 临键锁(Next-Key Locks)
有时候既想 锁住某条记录
,又想 阻止
其他事务在该记录前边的 间隙插入新记录
,所以InnoDB就提出了一种称之为 Next-Key Locks
的锁,官方的类型名称为: LOCK_ORDINARY
,我们也可以简称为next-key锁
。Next-Key Locks是在存储引擎 innodb
、事务级别在 可重复读
的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。
next-key锁
的本质就是一个记录锁
和一个gap锁
的合体,它既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的间隙
④ 插入意向锁(Insert Intention Locks)
我们说一个事务在 插入
一条记录时需要判断一下插入位置是不是被别的事务加了 gap锁
( next-key锁
也包含 gap锁
),如果有的话,插入操作需要等待,直到拥有gap锁
的那个事务提交。但是InnoDB 规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个 间隙
中 插入
新记录,但是现在在等待。InnoDB就把这种类型的锁命名为 Insert Intention Locks
,官方的类型名称为:
LOCK_INSERT_INTENTION
,我们称为 插入意向锁
。插入意向锁是一种 Gap锁
,不是意向锁,在insert操作时产生。
插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁
事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。
插入意向锁是在插入一条记录行前,由INSERT 操作产生的一种间隙锁
。该锁用以表示插入意向,当多个事务在同一区间(gap)插入位置不同的多条数据时,事务之间不需要互相等待。假设存在两条值分别为4和7的记录,两个不同的事务分别试图插入值为5和6的两条记录,每个事务在获取插入行上独占的(排他)锁前,都会获取(4,7)之间的间隙锁,但是因为数据行之间并不冲突
,所以两个事务之间并不会产生冲突(阻塞等待)。总结来说,插入意向锁的特性可以分成两部分:
(1)插入意向锁是一种特殊的间隙锁―—间隙锁可以锁定开区间内的部分记录。
(2)插入意向锁之间互不排斥,所以即使多个事务在同一区间插入多条记录,只要记录本身(主键、唯一索引)不冲突,那么事务之间就不会出现冲突等待
注意,虽然插入意向锁中含有意向锁三个字,但是它并不属于意向锁而属于间隙锁,因为意向锁是表锁而插入意向锁是行锁
3.4按加锁的方式划分:显式锁、隐式锁
一个事务在执行INSERT操作时,如果即将插入的间隙已经被其他事务加了gap锁,那么本次INSERT操作会阻塞,并且当前事务会在该间隙上加一个插入意向锁,否则一般情况下INSERT操作是不加锁的。
那如果一个事务首先插入了一条记录(此时并没有在内存生产与该记录关联的锁结构),然后另一个事务:
立即使用SELECT … LOCK IN SHARE MODE语句读取这条记录,也就是要获取这条记录的S锁,或者使用SELECT… FOR UPDATE语句读取这条记录,也就是要获取这条记录的X锁,怎么办?
如果允许这种情况的发生,那么可能产生脏读问题。
立即修改这条记录,也就是要获取这条记录的x锁,怎么办?
如果允许这种情况的发生,那么可能产生脏写问题。
这时候前边提过的事务id又要起作用了。把聚簇索引和二级索引中的记录分开看一下:
**情景一:**对于聚簇索引记录来说,有一个 trx_id 隐藏列,该隐藏列记录着最后改动该记录的 事务id 。那么如果在当前事务中新插入一条聚簇索引记录后,该记录的 trx_id 隐藏列代表的的就是当前事务的 事务id ,如果其他事务此时想对该记录添加 S锁 或者 X锁 时,首先会看一下该记录的trx_id 隐藏列代表的事务是否是当前的活跃事务,如果是的话,那么就帮助当前事务创建一个 X锁 (也就是为当前事务创建一个锁结构, is_waiting 属性是 false ),然后自己进入等待状态(也就是为自己也创建一个锁结构, is_waiting 属性是 true )。
**情景二:**对于二级索引记录来说,本身并没有 trx_id 隐藏列,但是在二级索引页面的 Page Header 部分有一个 PAGE_MAX_TRX_ID 属性,该属性代表对该页面做改动的最大的 事务id ,如果 PAGE_MAX_TRX_ID 属性值小于当前最小的活跃 事务id ,那么说明对该页面做修改的事务都已经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复 情景一 的做法。
即:一个事务对新插入的记录可以不显式的加锁(生成一个锁结构),但是由于事务id的存在,相当于加了一个隐式锁。别的事务在对这条记录加S锁或者X锁时,由于隐式锁的存在,会先帮助当前事务生成一个锁结构,然后自己再生成一个锁结构后进入等待状态。隐式锁是一种延迟加锁的机制,从而来减少加锁的数量。
隐式锁在实际内存对象中并不含有这个锁信息。只有当产生锁等待时,隐式锁转化为显式锁。
隐式锁的逻辑过程如下:
A. InnoDB的每条记录中都一个隐含的trx_id字段,这个字段存在于聚簇索引的B+Tree中。
B. 在操作一条记录前,首先根据记录中的trx_id检查该事务是否是活动的事务(未提交或回滚)。如果是活动的事务,首先将 隐式锁 转换为 显式锁 (就是为该事务添加一个锁)。
C. 检查是否有锁冲突,如果有冲突,创建锁,并设置为waiting状态。如果没有冲突不加锁,跳到E。
D. 等待加锁成功,被唤醒,或者超时。
E. 写数据,并将自己的trx_id写入trx_id字段。
3.5其它锁之:全局锁
全局锁就是对整个数据库实例加锁。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。全局锁的典型使用场景是:做全库逻辑备份。
全局锁的命令:
Flush tables with read lock
3.6其它锁之:死锁
1.概念
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。死锁举例如下:
一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout 来设置。
另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数innodb_deadlock_detect 设置为on ,表示开启这个逻辑。
2.产生死锁的必要条件
两个或者两个以上事务
每个事务都已经持有锁并且申请新的锁
锁资源同时只能被同一个事务持有或者不兼容
事务之间因为持有锁和申请锁导致彼此循环等待
死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。
第16章 多版本并发控制
第16章 多版本并发控制【3.事务篇】【MySQL高级】
1. 什么是MVCC
MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC是通过数据行的多个版本管理来实现数据库的并发控制。这项技术使得在InnoDB的事务隔离级别下执行一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
MVCC没有正式的标准,在不同的DBMS中 MVCC的实现方式可能是不同的,也不是普遍使用的(大家可以参考相关的DBMS文档)。这里讲解InnoDB 中 MVCC的实现机制(MySQL其它的存储引擎并不支持它)。
2. 快照读与当前读
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读,而这个读指的就是快照读,而非当前读。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
2.1 快照读
快照读又叫一致性读,读取的是快照数据。不加锁的简单的 SELECT 都属于快照读,即不加锁的非阻塞读;比如这样:
之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。
既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。
2.2 当前读
当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前读。比如:
3.2隐藏字段、Undo Log版本链
回顾一下undo日志的版本链,对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列。
trx_id : 每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列
roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
能不能在两个事务中交叉更新同一条记录呢?
不能!这不就是一个事务修改了另一个未提交事务修改过的数据,脏写。
InnoDB使用锁来保证不会有脏写情况的发生,也就是在第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新。
4. MVCC实现原理之ReadView
MVCC 的实现依赖于:隐藏字段、Undo Log、Read View
4.2设计思路
使用READ UNCONNMITTED隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。
使用SERIALIZABLE隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。
使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务,都必须保证读到 已经提交了的 事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题。
这个ReadView中主要包含4个比较重要的内容,分别如下:
-
creator_trx_id
,创建这个 Read View 的事务 ID。
说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
-
trx_ids
,表示在生成ReadView时当前系统中活跃的读写事务的 事务id列表
。 -
up_limit_id
,活跃的事务中最小的事务 ID。 -
low_limit_id
,表示生成ReadView时系统中应该分配给下一个事务的 id
值。low_limit_id 是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。
注意:low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。
4.3 ReadView的规则
有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见。
- 如果被访问版本的trx_id属性值与ReadView中的
creator_trx_id
值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。20可以访问自己 - 如果被访问版本的trx_id属性值小于ReadView中的
up_limit_id
值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。10可以访问最新 - 如果被访问版本的trx_id属性值大于或等于ReadView中的
low_limit_id
值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。(20被10阻塞) - 如果被访问版本的trx_id属性值在ReadView的
up_limit_id
和 low_limit_id
之间,那就需要判断一下trx_id属性值是不是在 trx_ids
列表中。
- 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。(10没提交,不能访问最新)*
- 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。(10提交之后,就能访问最新)
6. 总结
这里介绍了 MVCC
在 READ COMMITTD
、 REPEATABLE READ
这两种隔离级别的事务在执行快照读操作时访问记录的版本链的过程。这样使不同事务的 读-写
、 写-读
操作并发执行,从而提升系统性能
核心点在于 ReadView
的原理, READ COMMITTD
、 REPEATABLE READ
这两个隔离级别的一个很大不同就是生成ReadView的时机不同:
-
READ COMMITTD
在每一次进行普通SELECT操作前都会生成一个ReadView -
REPEATABLE READ
只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了
说明:之前说执行DELETE语句或者更新主键的UPDATE语句并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的delete mark操作,相当于只是对记录打上了一个删除标志位,这主要就是为MVCC服务的。
通过 MVCC 可以解决:
-
读写之间阻塞的问题
。通过MVCC 可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力 -
降低了死锁的概率
。这是因为MVCC采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁 定必要的行 -
解决快照读的问题
。当查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果