MySQL学习笔记

前言

  • 本文引用了一些优秀的文章,链接放在【八、相关文章链接】,在此表示感谢。
  • 本文为自己对MySQL知识的整理,仅供自己学习使用。本文将不定时更新。
  • 我不是知识的开拓者,只是知识的搬运工

一、基础篇

1、数据库三大范式1

  • 范式:数据库设计时遵循的一种规范。
  • 第一范式:列不可再分
  • 第二范式:一张表只描述一件事情
  • 第三范式:列与主键直接相关,而不能间接相关
  • 三大范式,都要求在满足前一范式的基础上实现的。

2、查询一条SQL的执行过程2

  • 取得链接,使用MySQL连接器
  • 查询缓存,key为SQL语句,value为查询结果,匹配上了就返回结果
  • 分析器,此阶段只是做一些SQL解析,语法校验
  • 优化器,存在多个索引时,决定使用哪个索引;存在多表联查时,决定关联顺序
  • 执行器,先判断是否有权限执行语句,没有权限返回提示,有权限打开表,根据表引擎调用接口,从数据表中获取数据,再经过排序、聚合、过滤等操作,然后返回前台。

3、SQL语句执行顺序

  1. FROM table
  2. ON 条件
  3. JOIN table2
  4. WHERE 条件
  5. GROUP BY 字段
  6. FUN(聚合函数)
  7. HAVING 条件
  8. SELECT
  9. DISTINCT 字段名
  10. ORDER BY 排序字段
  11. LIMIT 起始偏移量,行数

二、引擎篇3

1、什么是数据库引擎?

  • 是链接并操控数据库的程序,用于存储、处理和保护数据的核心服务。利用数据库引擎可控制访问权限和快速处理事务,从而满足企业中要处理大量数据的应用程序的要求。

2、MyISAM

  • 在MySQL 5.5.8之前,MyISAM是MySQL的默认数据库引擎;
  • 但是它不支持数据库事务,也不支持行锁和外键,只支持表锁,因此,在做INSERT和UPDATE时需要锁定整张表,效率比较低
  • 它内部会存储一个变量来记录表格的行数;
  • MyISAM采用B+树作为索引结构,叶子节点存放的是数据的地址,MyISAM的索引方式也被叫做"非聚集的",这么称呼是为了与InnoDB的"聚集索引"进行区分;
  • MyISAM有一个与众不同的点是,它只缓冲索引文件,而不缓冲数据文件;
  • 适用场景:
    • 做很多COUNT的计算
    • 插入不频繁,查询非常频繁
    • 没有事务

3、InnoDB

  • MySQL 5.5.8之后,就开始采用InnoDB作为数据库默认引擎;
  • 它提供了对数据库ACID的支持,并且实现了SQL的四种隔离级别,其设计目标主要面向在线事务处理(OLTP)的应用。
  • 该引擎还提供了行级锁和外键约束,支持一致性的非锁定读,它的设计目标是处理大容量数据库系统,它本身其实是基于MySQL后台的完整数据库系统,MySQL运行时InnoDB会在内存中建立缓冲池,用于缓存数据和索引。
  • 该引擎不支持FULL TEXT类型的索引,也不保存表的行数。
  • InnoDB引擎的索引结构是B+树,但InnoDB索引文件本身就是数据文件,即数据域存储的就是实际的数据,这种索引就是聚集索引。这个索引的key就是数据表的主键,因此InnoDB表数据文件本身就是主索引。因为InnoDB的数据文件本身要按照主键聚集,索引InnoDB必须要求有主键(MyISAM可以没有),如果没有显式指定,MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,MySQL自动为InnoDB表生成一个隐式字段作为主键,这个主键是Long类型的,长度为6个字节。
  • 与MyISAM不同的是,InnoDB的辅助索引数据域存储的是记录的主键的值,不是地址所以当以辅助索引查找时,会先根据索引找到主键,再根据主键索引找到实际的数据。 所以InnoDB不建议使用过长的主键,否则会导致辅助索引过大。建议使用自增列作为主键,这样B+树的每一个节点都会被顺序填满,而不会频繁的分裂调整,会有效的提升插入数据效率。
  • 适用场景:
    • 可靠性要求较高,或者要求事务
    • 表更新和查询非常频繁,并且表锁定的机会比较多
    • 大量的数据集建议选择InnoDB引擎,它支持事务处理和故障恢复,数据库的大小决定故障恢复时间的长短,InnoDB可以利用事务日志进行数据恢复,会很快。

4、MyISAM与InnoDB的区别

  • 锁级别:MyISAM是表锁,InnoDB是行锁
  • 事务支持:MyISAM不支持事务,InnoDB支持事务,并实现了SQL的四种隔离级别
  • 主键:MyISAM可以不存在主键,InnoDB必须存在主键
  • 统计行数:MyISAM每张表内都存在一个变量来存放表的行数,可以SELECT COUNT(0) FROM tab直接获取行数而不用扫描整张表,InnoDB不支持
  • 主索引:MyISAM的索引和数据是分开的,InnoDB的数据文件本身就是索引文件
  • 辅助索引:MyISAM的辅助索引和主索引没区别,InnoDB的辅助索引数据域存储的是主键的值而不是地址

三、指令篇

1、数据库层面

1.1、操作数据库

1.2、操作表

1.3、操作索引

  • 查询索引
SHOW INDEX FROM 表名;

当表中存在主键的时候,内部会自动给该列创建索引
因为主键不允许重复,因此进行插入或者修改的时候,就需要先查询,看看插入 / 修改后的结果是否已经存在,为了提高查询的速度,数据库就自动给主键这列创建了索引。
使用unique约束某一列的时候,也会为该列自动生成索引,原因同上。
设置外键约束的时候,也会自动生成一个索引。因为外键也涉及到了自动查询。

  • 创建索引
CREATE INDEX 索引名 ON 表名(字段名);

如果这个表是空的或者是数据比较少,创建索引没问题。
但如果这个表中存在很多数据,创建索引会引起很大规模的硬盘IO操作,导致数据库被卡死。
所以在设计表的时候就应该先规划好哪些列需要索引。

  • 删除索引
DROP INDEX 索引名 ON 表名;

删除索引只针对手动创建的索引, 而自动生成的索引是不能被删除的

四、事务篇4

1、什么是数据库事务?为什么要有数据库事务?

  • 事务:由一个有限的数据库操作序列构成,这些操作要么全部成功,要么全部失败,是一个不可分割的工作单位。
  • 数据库事务的存在,是为了保证数据的最终一致性。

2、数据库事务特性

  • 事务的四个典型特性,即ACID
    • Atomicity(原子性):事务作为一个整体被执行,事务内的操作要么全部成功,要么全部失败
    • Consistency(一致性):事务执行前后,数据不会被破坏,比如A给B转10块钱,不管成功与否,A和B的总金额不变
    • Isolation(隔离性):多个事务并发隔离时,事务之间不会相互干扰
    • Durability(持久性):事务提交后,事务对数据库的修改将会持久的保存到数据库中

3、事务并发会出现的问题

  • 事务并发会引起脏读 不可重复读 幻读
    • 脏读:事务A读取到事务B未提交的数据
    • 不可重复读:事务A由于事务B的提交,造成前后读取的数据不一致
    • 幻读:事务A查询一个范围内的结果集,事务B向这个范围内的数据进行插入操作,并提交,然后事务A再次做相同的查询,两次读取到的结果集不同

4、数据库四大隔离级别

  • 为了解决并发造成的脏读、幻读、不可重复读,数据库设计了四种隔离级别。分别是读未提交 读已提交 可重复读 串行化
    • 读未提交:只限制了两个数据不能同时修改,但是修改数据的时候,即使事务未提交,都可以被别的事务读取到,这个级别会存在脏读、幻读、重复读的问题
    • 读已提交:当前事务只能读取到其他事务已经提交的数据,所以这种事务解决了脏读,但还存在重复读和幻读的问题
    • 可重复读:限制了读取数据的时候,不可以进行修改,所以解决了重复读的问题,但是读取数据范围的时候,是可以插入数据的,所以还会存在幻读的问题
    • 串行化:事务最高的隔离级别,在这种级别下,所有的事务都是进行串行化顺序执行的,可以避免脏读、幻读、重复读这些并发问题。但是非常耗费数据库性能,不建议

5、数据库是如何保证事务的隔离性的呢?

  • 数据库是通过加锁来实现事务的隔离性的。
  • 加锁确实可以很好的保证隔离性。比如串行化隔离级别就是通过加锁实现的。但是频繁的加锁,导致读数据的时候,没办法修改,修改数据的时候没办法读取,大大降低了数据库性能。

6、如何解决加锁后的性能问题?

  • 使用MVCC(并发版本控制),它可以实现读取数据的时候不用加锁,可以让读取数据时同时修改数据,修改数据时同时读取数据。

7、什么是MVCC?

  • MVCC(Multi-Version Concurrency Control,多版本并发控制)。它是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
  • 通俗的讲,数据库中同时存在多个版本的数据,并不是整个数据库的多个版本,而是某一条记录的多个版本同时存在,在某个事务对其进行操作的时候,需要查看这一条记录的隐藏列事务版本id,对比事务id根据事务隔离级别去判断读取哪个版本的数据。
  • 数据库隔离级别读已提交可重复读都是基于MVCC实现的,现对于加锁的简单粗暴的方式,它用更好的方式去处理读写冲突,能有效提高数据库并发性能

8、什么是事务版本号?

  • 事务在每次开启之前,都会从数据库获取一个自增长的事务ID,可以从事务ID判断事务的执行顺序。

9、什么是隐式字段?

  • 对应InnoDB存储引擎,每一行记录都有两个隐藏字段:trx_idroll_pointer,如果表中没有主见和非空唯一键时,则还会有第三个隐藏的主键列:row_id
    • row_id:非必须。单行递增的行ID,占用6字节。
    • trx_id:必须。记录操作该数据事务的事务ID。
    • roll_pointer:必须。这个隐藏列相当于一个指针,指向回滚段的undo log。

10、什么是undo log?

  • undo log就是回滚日志,用于记录数据被修改前的信息。在表记录修改前,会把数据备份到undo log中,如果事务回滚,即可通过undo log来还原数据。
  • 它可以用于:
    • 事务回滚时,保证原子性和一致性。
    • 用于MVCC快照读

11、什么是版本链?

  • 多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针roll_pointer隐藏列,指向undo log数据地址,连成一个链表,这个链表就是版本链。

12、什么是快照读和当前读?

  • 快照读:读取的是记录数据的可见版本。不加锁,普通的select语句都是快照读:
SELECT * FROM table WHERE id > 2;
  • 当前读:读取的是记录数据的最新版本。显示加锁的都是当前读
SELECT * FROM table WHERE id > 2 FOR UPDATE;
SELECT * FROM table WHERE id > 2 LOCK IN SHARE MODE;

13、什么是Read View?

  • 它就是事务执行SQL语句时,产生的读视图。实际上在InnoDB中,每个SQL语句执行前都会得到一个Read View。
  • 它主要是用来做可见性判断,即判断当前事务可见那个版本的数据。
  • Read View有几个重要的属性:
    • m_ids:当前系统重哪些未提交的读写事务ID,它的数据结构为一个List;
    • min_limit_id:表示在生成Read View时,当前系统中活跃的读写事务中最小的事务ID,即m_ids中的最小值。
    • max_limit_id:表示生成Read View时,系统中应该分配给下一事务的ID值。
    • creator_trx_id:创建当前Read View的事务ID
  • Read View的匹配规则(trx_id表示事务ID):
    • 如果trx_id < min_limit_id:表明生成该版本的事务在生成Read View前,已经提交,所以该本本可以被当前事务访问;
    • 如果trx_id >= max_limit_id:表明生成该版本事务在生成Read View后才生成,所以该版本不可以被当前事务访问;
    • 如果min_limit_id <= trx_id < max_limit_id:需要分三种情况访问
      • 如果m_ids包含trx_id,则表示Read View生成时刻,这个事务还未提交,但是如果数据的trx_id等于creator_trx_id的话,表明数据是自己生成的,因此是可见的;
      • 如果m_ids包含trx_id,并且trx_id不等于creator_trx_id,则Read View生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不到的;
      • 如果m_ids不包含trx_id,则说明你这个事务在Read View生成之前就已经提交了,修改的结构,当前事务是能看到的。

14、查询一条记录,基于MVCC,流程是怎样的?

  • 流程:
    • 获取自己的事务ID
    • 获取Read View
    • 查询得到的数据,在Read View中进行可见性规则匹配
    • 如果不符合Read View的可见性规则,那就按照
    • 最后返回符合规则的数据
  • InnoDB实现MVCC,是通过Read View + Undo Log实现的,Undo Log保存了历史快照,Read View可见性规则帮助判断当前版本的数据是够可见

五、索引篇5

1、什么是索引?

  • 索引相当于一本书的目录,目的是为了加快数据库查询速度;
  • 索引很大,一般储存在硬盘中,具体来说,是存储在硬盘的文件中;
  • 存储原理:空间换时间;
  • 数据库没有添加索引的时候,默认是进行全文搜索;
  • 索引适用于经常查询很少修改的业务。

2、索引的优缺点

  • 优点:
    • 大大提高了查询速度;
    • 减少硬盘IO速度
  • 缺点:
    • 需要消耗额外的空间(硬盘);
    • 有可能拖慢数据库增删改的速度,因为你进行增删改的时候,不仅是要改变数据库中的数据,还要更改维护索引文件;
    • 维护索引需要消耗数据库资源。

六、锁篇6

1、什么是MySQL锁?

  • 数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。
  • 具体锁结构:
    锁集体结构

2、表级锁、行级锁、页面锁

1)什么是表级锁?

  • 表级锁,是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制的最大特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。
  • 当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也最高,致使并发度大打折扣
  • 使用表级锁的主要是MyISAM、MEMORY、CSV等一些非事务性存储引擎。

2)什么是行级锁?

  • 行级锁,最大的特点就是锁定对象的颗粒度很小,由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。
  • 虽然能够在并发处理能力上面有较大的优势,但是行级锁也因此带来不少弊端。
  • 由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁也最容易发生死锁。
  • 使用行级锁的主要是InnoDB存储引擎

3)页级锁

  • 页级锁,是MySQL中比较特别的一种锁定级别。页级锁的特点是锁定颗粒度介于行级锁和表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于两者之间。
  • 使用页级锁的主要是BerkeleyDB存储引擎。

4)总结

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;
  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

3、共享锁、排它锁

  • InnoDB实现了标准的行级锁,包含两种:共享锁(S)排它锁(X)
  • 对于共享锁而言,对当前行加共享锁,不会阻塞其他事务对同一行的读请求,但会阻塞对同一行的写请求。只有当读锁释放后,才会执行其它事务的写操作。
  • 对于排它锁而言,会阻塞其他事务对同一行的读和写操作,只有当写锁释放后,才会执行其它事务的读写操作
兼容性SX
S兼容不兼容
X不兼容不兼容
  • 简而言之,就是:
    • 读锁:会阻塞写(X),但是不会阻塞读(S);
    • 写锁:会阻塞写(X),也会阻塞读(S);
  • 对于InnoDB在RR(MySQL默认隔离级别)而言,对于、``语句,会自动给涉及数据集加排它锁(X);
  • 对于普通select语句,InnoDB不会加任何锁。如果想在select操作时加上S / X锁,需要我们手动加锁。
-- 加共享锁(S)
SELECT * FROM tab_name WHERE ... LOCK IN SHARE MODE;
-- 加排它锁(X)
SELECT * FROM tab_name WHERE ... FOR UPDATE;
  • SELECT ... IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行update或者delete操作。
  • 但是,如果当前事务也需要对该记录进行更新操作,则有可能造成死锁,对于锁定行记录后需要精心更新操作的应用,应该使用SELECT ... FOR UPDATE方式来获取排它锁。

4、加锁模式

1)记录锁(Record Locks)

  • 记录锁,对表中的记录加锁,叫做记录锁,简称行锁。例如:
SELECT * FROM `test` WHERE `id` = 1 FOR UPDATE;
  • 它会在id = 1的记录上加上记录锁,以阻止其他事务插入,更新,删除id = 1这一行。
  • 需要注意的是:
    • id列必须为唯一索引或主键列,否则上诉语句加的锁就会变成临键锁。
    • 同时查询语句必须为精准匹配(=),不能为<>like等,否则也会退化成临键锁。
  • 在通过主键索引唯一索引对数据进行UPDATE操作时,也会对改行数据加记录锁:
-- id列为主键列或唯一索引列
UPDATE SET `age` = 50 WHERE `id` = 1;
  • 记录锁是锁住数据,锁住索引记录,而不是真正的数据记录。
  • 如果要锁的列没有索引,进行全表记录加锁。
  • 记录锁也是排它锁(X),所以会阻塞其他事务对其进行插入、更新、删除。

2)间隙锁(Gap Locks)

  • 间隙锁,是InnoDB在RR(可重复读)隔离级别下为了解决幻读问题,而引入的锁机制。间隙锁是InnnoDB中行锁的一种。
  • 使用间隙锁锁住的是一个区间,而不仅仅是这个区间的一条数据。
  • 举例来说,加入emp表中只有101条记录,而empid的值分别是1、2、…、100、101,SQL如下:
SELECT * FROM emp WHERE empid > 100 FOR UPDATE;
  • 当我们用条件检索数据,并请求共享或排它锁时,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的"间隙"加锁。
  • 这时候如果你插入empid等于102的数据,如果这边事务没有提交,那你的数据就会处于等待状态,无法插入数据。

3)临键锁(Next-Key Locks)

  • Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。
  • 也可以理解为一种特殊的间隙锁。通过临键锁可以解决幻读的问题。每个数据航上的非唯一索引列(该索引里面的值允许重复)上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。
  • 需要强调的一点是,InnoDB中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
  • 假如设有如下表:id - 主键;age - 普通索引。
idnameage
1张三10
3李四24
5王五32
7赵六45
  • 该表中age列潜在的临键锁:(-∞, 10],(10, 24],(24, 32],(32, 45],(45, +∞]。
  • 在事务A中执行如下命令:
-- 根据非唯一索引列修改某条记录
UPDATE `tab_name` SET `name` = Vladimir WHERE `age` = 24;

-- 或根据非唯一索引列锁住某条记录
SELECT * FROM `tab_name` WHERE `age` = 24 FOR UPDATE;
  • 不管执行了上述SQL中的哪一句,之后如果在事务B中执行以下命令,则该命令会被阻塞:
INSERT INTO `tab_name` VALUES(100, 26, '田七');
  • 很明显,事务A在对age为24的列进行UPDATE操作的同时,也获取了(10, 32]这个区间内的临键锁。

4)记录锁、间隙锁、临键锁小结

  • InnoDB中的行锁的实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
  • 记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
  • 间隙锁存在于非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。
  • 临键锁存在于非唯一索引中,该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间。

5)意向锁

  • 意向锁也分为**意向共享锁(IS)和意向排他锁(IX)。
  • 意向共享锁(IS):事务有意向对表中的某些行加共享锁(S锁)
-- 事务要获取某些行的S锁,必须先获得表的IS锁。
SELECT `col` FROM `tab` ... LOCK IN SHARE MODE;
  • 意向排他锁(IX):事务有意向对表中的某些行加排他锁(X锁)
-- 事务要获取某些行的X锁,必须先获得表的IX锁。
SELECT `col` FROM `tab` ... FOR UPDATE;
  • 首先我们要明白四点:
    • 意向共享锁(IS)和意向排它锁(IX)都是表锁
    • 意向锁是一种不与行级锁冲突的表级锁,这一点非常重要。
    • 意向锁是InnoDB自动加的,不需用户干预。
    • 意向锁是在InnoDB下存在的内部锁,对于MyISAM而言没有意向锁之说。
  • 这里就会出现疑问,既然前面已经有了共享锁(S锁)、排它锁(X锁)。那么为什么需要引入意向锁呢?它能解决什么问题?
  • 我们可以理解为意向锁存在的目的就是为了让InnoDB中的行锁和表锁更高效的共存。
  • 为什么这么说呢,我们来举一个例子。下面有一张表,id是主键。
idname
1张三
3李四
6王五
7赵六
  • 事务A获取了某一行的排它锁,并未提交:
SELECT * FROM users WHERE id = 6 FOR UPDATE;
  • 事务B想要获取users表的表锁:
LOCK TABLES users READ;
  • 因为共享锁与排它锁互斥,所以事务B在视图对users表加共享锁的时候,必须保证:
    • 当前没有其他事务持有users表的排它锁。
    • 当前没有其他事务持有users表中任意一行的排它锁。
  • 为了检测是否满足第二个条件,事务B必须确保users表不存在任何排它锁的前提下,去检测表中的每一行是否存在排它锁。很明显这是一个效率很差的做法,但是有了意向锁之后,情况就不一样了:事务 B 只要看表上有没有意向共享锁,有则说明表中有些行被共享行锁锁住了,因此,事务 B 申请表的写锁会被阻塞。这样是不是就高效多了。
  • 这也解释就应该清楚,为什么有意向锁这个东西存在了。我们可以举个生活中的例子,再来理解下为什么需要存在意向锁。
  • 打个比方,就像有个游乐场,很多小朋友进去玩,看门大爷如果要下班锁游乐场的门(加表锁),他必须确保每个角落都要去检查一遍,确保每个小朋友都离开了(释放行锁),才可以锁门。
  • 假设锁门是件频繁发生的事情,大爷就会非常崩溃。那大爷想了一个办法,每个小朋友进入,就把自己的名字写在本子上,小朋友离开,就把自己的名字划掉,那大爷就能方便掌握有没有小朋友在游乐场里,不必每个角落都去寻找一遍。例子中的“小本子”,就是意向锁,他记录的信息并不精细,他只是提醒大爷,是否有人在屋里。
  • 这里我们再来看下 共享(S)锁、排他(X)锁、意向共享锁(IS)、意向排他锁(IX) 的兼容性。
意向共享锁(IS)意向排他锁(IX)
意向共享锁(IS)兼容兼容
意向排他锁(IX)兼容兼容
  • 可以看出 意向锁之间是互相兼容的。那你存在的意义是啥?
  • 意向锁不会为难意向锁,也不会为难 行级排他(X) / 共享(X)锁,它的存在是为难 表级排他(X) / 共享(X)锁。
兼容性ISIXSX
IS兼容兼容兼容不兼容
IX兼容兼容不兼容不兼容
S兼容不兼容兼容不兼容
X不兼容不兼容不兼容不兼容
  • 注意 这里的 排他(X) / 共享(S)锁 指的都是表锁!意向锁不会与 行级的共享 / 排他锁 互斥!
  • 意向锁与意向锁之间永远是兼容的,因为当你不论加行级的 X 锁或 S 锁,都会自动获取表级的 IX 锁或者 IS 锁。也就是你有 10 个事务,对不同的 10 行加了行级 X 锁,那么这个时候就存在 10 个 IX 锁。
  • 这 10 个 IX 存在的作用是啥呢,就是假如这个时候有个事务,想对整个表加排它 X 锁,那它不需要遍历每一行是否存在 S 或 X 锁,而是看有没有存在 意向锁,只要存在一个意向锁,那这个事务就加不了表级排它(X)锁,要等上面 10 个 IX 全部释放才行。

6)插入意向锁

  • 在讲解插入意向锁之前,先来思考一个问题?
  • 下面有张表 id 主键,age 普通索引。
idnameage
1Mr10
2Tome20
3Jon30
  • 首先事务 A 插入了一行数据,并且没有 commit:
INSERT INTO `users` SELECT 4, 'Bill', 15;
  • 随后事务 B 试图插入一行数据:
INSERT INTO users SELECT 5, 'Louis', 16;
  • 请问:
    • 事务 A 使用了什么锁?
    • 事务 B 是否会被事务 A 阻塞?
  • 插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁。
  • 该锁用以表示插入意向,当多个事务在同一区间(gap)插入位置不同的多条数据时,事务之间不需要互相等待。
  • 假设存在两条值分别为 4 和 7 的记录,两个不同的事务分别试图插入值为 5 和 6 的两条记录,每个事务在获取插入行上独占的(排他)锁前,都会获取(4,7 ] 之间的间隙锁,但是因为数据行之间并不冲突,所以两个事务之间并不会产生冲突(阻塞等待)。
  • 总结来说,插入意向锁 的特性可以分成两部分:
    • 插入意向锁是一种特殊的间隙锁—— 间隙锁可以锁定开区间内的部分记录。
    • 插入意向锁之间互不排斥,所以即使多个事务在同一区间插入多条记录,只要记录本身(主键唯一索引)不冲突,那么事务之间就不会出现冲突等待。
  • 虽然插入意向锁中含有意向锁三个字,但是它并不属于意向锁而属于间隙锁,因为意向锁是表锁,而插入意向锁是行锁。
  • 现在我们可以回答开头的问题了:
    • 使用插入意向锁与记录锁。
    • 事务 A 不会阻塞事务 B。
  • 为什么不用间隙锁?
  • 如果只是使用普通的间隙锁会怎么样呢?我们在看事务 A,其实它一共获取了 3 把锁:
    • id 为 4 的记录行的记录锁。
    • age 区间在(10,15 ] 的间隙锁。
    • age 区间在(15,20 ] 的间隙锁。
  • 最终,事务 A 插入了该行数据,并锁住了(10,20 ] 这个区间。
  • 随后事务 B 试图插入一行数据:
INSERT INTO users SELECT 5, 'Louis', 16;
  • 因为 16 位于(15,20 ] 区间内,而该区间内又存在一把间隙锁,所以事务 B 别说想申请自己的间隙锁了,它甚至不能获取该行的记录锁,自然只能乖乖的等待事务 A 结束,才能执行插入操作。
  • 很明显,这样做事务之间将会频发陷入阻塞等待,插入的并发性非常之差。这时如果我们再去回想我们刚刚讲过的插入意向锁,就不难发现它是如何优雅的解决了并发插入的问题。

6)意向锁、插入意向锁小结

  • InnoDB 在RR 的事务隔离级别下,使用插入意向锁来控制和解决并发插入
  • 插入意向锁是一种特殊的间隙锁
  • 插入意向锁在锁定区间相同但记录行本身不冲突的情况下互不排斥。

5、乐观锁、悲观锁

1)乐观锁

  • 在操作数据的时候非常乐观,认为别的线程不会同时修改数据,所以不会上锁,但是在更新的时候会判断在此期间别的线程有没有更新欧这个数据。
  • 数据库提供类似于write_condition机制,Java API开发工具包下面的原子变量类就是使用了乐观锁的CAS来实现的。
  • 适用于读多写少的情况,也就是说减少操作冲突,这样可以省去锁竞争的开销,提高系统的吞吐量。

2)悲观锁

  • 在操作数据是比较悲观,每次去拿数据的时候认为别的线程也会同时修改数据,所以每次在拿数据的时候都会上锁,这样别的线程想拿到这个数据就会阻塞知道它拿到锁。
  • 行锁、表锁、读锁、写锁,都是在操作之前先上锁,Java API中的synchronizedReentrantLock等独占锁都是悲观锁思想的实现。
  • 它适用于写多读少的情况。因为,如果还使用乐观的话,会经常出现操作冲突,这样会导致应用层会不断地Retry,反而会降低系统性能。

七、SQL优化7

1、SQL优化概括(点击可跳转相应文段)

  1. 避免使用SELECT *
  2. 用union all代替union
  3. 多用limit

2、避免使用SELECT *

  • 存在问题:
    • 很多时候,我们写SQL语句的时候,为了方便,直接写SELECT *一次性查询所有列。
      -- 反例
      SELECT * FROM `tab_name`;
      
    • 在实际业务场景中,可能我们真正需要的只有其中一两列。查了很多数据,但不使用,白白浪费数据库资源,例如:内存和CPU
    • 此外,多查询出的数据,通过网络IO传输的过程中,也增加了数据传输时间。
    • 还有一个最终要的问题:SELECT *不会走覆盖索引,会出现大量的回表操作,从而导致查询SQL的性能很低。
  • 优化方案:
    • 指定查询列,多余的列不再查询。
      -- 正例
      SELECT `id`, `name` FROM `tab_name`;
      
  • 返回SQL优化概括

3、用union all代替union

  • 存在问题:
    • UNION关键字的作用是:获取排重后的数据
    • UNION ALL关键字的作用是:获取所有数据,包括重复的数据
      -- 反例
      (SELECT * FROM `tab_name` WHERE `id` = 1)
      UNION
      (SELECT * FROM `tab_name` WHERE `id` = 2);
      
    • 排重的过程需要遍历、排序和比较,非常耗时,也更消耗CPU资源。
    • 所以如果能用UNION ALL的时候,尽量不用UNION
  • 优化方案:
    • 除非是有些特殊的场景,比如UNION ALL之后,结果集出现了重复数据,而业务场景不允许出现重复数据,这时可以使用UNION ALL
      -- 反例
      (SELECT * FROM `tab_name` WHERE `id` = 1)
      UNION ALL
      (SELECT * FROM `tab_name` WHERE `id` = 2);
      
  • 返回SQL优化概括

4、多用limit

  • 存在问题:
    • 有时候,我们需要某些数据的第一条,比如:查询某个用户下的第一个订单,想看看他第一次的首单时间。
    • 根据用户id查询订单,按下单时间的排序,先查出该用户下的所有订单,得到一个订单集合,然后再在代码中获取第一个元素的数据,即首单数据,从而也可以得到首单时间。
      -- 反例
      SELECT
      	`id`, `create_date`
      FROM
      	`order`
      WHERE
      	`user_id` = 123
      ORDER BY
      	`create_date` DESC;
      
      // 反例
      List<Order> list = orderMapper.getOrderList();
      Order order = list.get(0);
      
    • 虽然这种做法在功能上没问题,但它的效率非常低,需要先查出所有数据,再进行判断,非常浪费时间和资源。
  • 优化方案:
    • 使用关键字limit,只返回该用户下单时间最小的那条数据即可
      -- 正例
      SELECT
      	`id`, `create_date`
      FROM
      	`order`
      WHERE
      	`user_id` = 123
      ORDER BY
      	`create_date` DESC
      LIMIT 1;
      
  • 返回SQL优化概括

、小表驱动大表

八、相关文章链接


  1. 数据库三大范式是什么?(3NF详解) ↩︎

  2. MySQL 执行过程 ↩︎

  3. MySQL的几种数据库引擎介绍 ↩︎

  4. 【数据库】MVCC原理详解 ↩︎

  5. MySQL索引详解 ↩︎

  6. 【数据库】MySQL 中的锁机制 ↩︎

  7. sql优化的15个小技巧(必知五颗星),面试说出七八个就有了 ↩︎

  • 26
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值