🚀 Database
1、数据库基础
1.1 事务的概念和特性
1.2 锁
1.3 锁协议
1.4 事务日志
1.5 MVCC实现原理
1.6 基础知识
1.6.1 三范式
1.6.2 多表连接方式
1.6.3 存储过程
1.6.4 TRUNCATE和DROP的区别
1.6.5 触发器
1.6.6 视图
2、MySQL
2.1 索引
2.2 索引组织表
2.3 InnoDB和MyISAM的区别
2.4 Checkpoint技术
2.5 宕机恢复原理
2.6 数据库优化
2.7 分库分表
2.8 一致性哈希算法
2.9 主从复制
3、Redis
3.1 概述
3.1.1 为什么Redis单线程还这么快
3.1.2 Redis数据类型
3.1.3 持久化机制
3.1.4 过期机制和内存淘汰策略
3.2 线程模型
3.3 分布式问题
3.3.1 Redis实现分布式锁
3.4 缓存异常
3.4.1 缓存击穿、缓存雪崩
3.5 高可用
3.5.1 主从复制
3.5.2 哨兵模式
3.5.3 集群模式
-
+
tourist
register
Sign in
锁
首先,对 MySQL 锁进行划分: 1. 按照锁的**粒度**划分:**表锁**(Table Level Lock)**、行锁**(Row Level Lock)。 2. 按照锁的**使用方式**划分:**共享锁**(Share Lock)**、排他锁**(悲观锁的一种实现)(eXclusive Lock)。 3. 按照锁的**思想**划分:**悲观锁**(Pessimistic Concurrency Control, PCC)**、乐观锁**(Optimistic Concurrency Control, PCC)。 4. **InnoDB**的几种**意向锁**:**意向共享锁**(Intensive Shared Lock, IS)、**意向排他锁**(Intensive Exclusive Lock)。 5. **InnoDB**的几种**行级锁**:**记录锁**(Recoord Lock)、**间隙锁**(Gap Lock)、**临键锁**(Next-key Lock)。 ## 表锁和行锁 ### 表锁 #### 含义 1. 表级锁是 MySQL 中粒度最大的一种锁,表示当前的操作**对整张表加锁**。 2. 表锁响应的是**非索引字段**,即**全表扫描**,全表扫描时**锁定整张表**。 3. 表级锁有两种模式:**表共享锁**(Table Read Lock)**、表独占写锁**(Table Write Lock)**:** 1. **表共享锁:** 不会阻塞其他用户对同一张表的读请求,但会**阻塞对同一张表的写请求**。 2. **表独占写锁:**会**阻塞**其他用户**对同一张表的读写请求**,当一个线程获得对一个表的写锁之后,只有持有锁的线程可以对表进行更新操作,其他线程的读、写操作都会等待,直到锁被释放为止。 #### 如何加表锁 1. 给表显示加锁,一般是为了一定程度模拟事务操作,实现对**某一时间点多个表的一致性读取**。 2. 例如,有一个订单表 `orders`,其中记录有订单的总金额 `total`,同时还有一个订单明细表 `order_detail`,其中记录有订单每一产品的金额小计 `subtotal`,假设我们需要检查这两个表的金额合计是否相等,可能就需要执行如下两条 SQL: ```sql select sum(total) from orders; select sum(subtotal) from order_detail; ``` 3. 这时,如果不给这两个表加锁,就可能产生错误的结果,因为第一条语句在执行的过程中,`order_detail` 表可能已经发生了改变,因此,正确的方法应该是: ```sql lock tables order read local, order_detail read local; select sum(total) from orders; select sum(subtotal) from order_detail; unlock tables; ``` 4. 针对上面的叙述,有以下需要说明的地方: 1. 上面的例子在`lock tables` 时加了`local` 选项,其作用就是在满足表并发插入的条件下,允许其他用户在表尾插入记录(InnoDB 中`read local` 和`read` 等价)。 2. 在用`lock tables` 给表显式加表锁时,必须**同时取得所有涉及表的锁**,也就是说,再执行`lock tables` 后,**只能访问显式加锁的这些表,不能访问未加锁的表**,这也正是**表锁不会出现死锁**的原因。同时,如果加的是读锁,那么只能执行查询操作,而不能执行更新操作。 #### 优缺点 ##### 优点 1. **开销小,加锁快**。 2. **不会出现死锁**。 ##### 缺点 1. **锁定力度大**,发生**冲突的概率最高**,**并发度最低**。 ### 行锁 #### 含义 1. 读取操作行级锁是 MySQL 中锁定粒度最细的一种锁,表示只**对当前操作的行进行加锁**。 2. MySQL 的**行锁是基于索引加载的**,所以**行锁是要加在索引响应的行上**,即**命中索引**。 3. 行级锁按照使用方式分为**共享锁**(Share Lock)和**排他锁**(sXclusive Lock),具体可参照[共享锁和排他锁]()。 #### 加锁原理 ##### 单行数据的加锁原理 1. 假如有如下两条 SQL: ```sql update user set age = 10 where id = 49; update user set age = 10 where name = 'Tom'; ``` 2. 第一条 SQL**使用主键索引来查询**,则只**需要在** `id=49` 这个**主键索引上加上写锁**。 3. 第二条 SQL 则**使用辅助索引来查询**,首先**在 `name=Tom` 这个索引上加写锁**,然后**由于使用**InnoDB 的**辅助索引**,还**需要根据主键索引进行查询**,所以还**需要在 `id=49` 这个主键索引上加写锁**。 4. 也就是说**主键索引需要加一把锁**,使用**辅助索引需要在辅助索引和主键索引上各加一把锁**。 #### 优缺点 ##### 优点 1. 能**大大减少数据库操作的冲突**。 ##### 缺点 1. **加锁粒度最小**,但是**加锁的开销也最大**,**有可能出现死锁的情况**。 ## 共享锁和排他锁 ### 共享锁 #### 含义 1. 共享锁又称**读锁**,是**读取操作创建的锁**。 2. **其他用户可以并发读取数据**,**但任何事务都不能对数据进行修改**,**直到已释放所有共享锁**。 3. 如果**事务 T 对数据 A 加上共享锁后**,则**其他事务只能对 A 再加共享锁**,**不能加排他锁**。 4. **获取共享锁的事务只能读数据**,**不能修改数据**。 #### 用法 1. 可以通过**在查询语句后面加上 `LOCK IN SHARE MODE` 来使用共享锁**。 2. 此时 MySQL 会**对查询结果中的每行都加共享锁**,**当没有其他线程对查询结果集中的任何一行使用排他锁时**,**可以成功申请共享锁**,**否则会被阻塞**。 3. **其他线程也可以读取使用了共享锁的表**,而且**这些线程读取的是同一个版本的数据**。 ### 排他锁 #### 含义 1. 排他锁又称**写锁**。 2. 如果**事务 T 对数据 A 加上排他锁后**,则**其他事务不能再对 A 加任何类型的锁**。 3. **获取排他锁的事务既能读数据**,**又能修改数据**。 #### 用法 1. 可以通过**在查询语句后面添加 `FOR UPDATE` 来使用排他锁**,此时 MySQL 会**对查询结果中的每行都加排他锁**,**当没有其他线程对查询结果集中的任何一行使用排他锁时**,**可以成功申请排他锁**,**否则会被阻塞**。 ## 乐观锁和悲观锁 ### 悲观锁 #### 含义 1. 悲观锁指的是**对数据被外界**(包括本系统当前的其它事务,以及来自外部系统的事务处理)**修改持保守态度**,**认为数据随时可能会修改**,因此,**每次去取数据的时候都会给他上锁**,**防止其他事务读取或修改数据**。 2. **悲观锁的实现,往往依靠数据库提供的锁机制**(也**只有数据库底层提供的锁机制才能真正保证数据访问的排他性**,**否则**,**即使在本系统中实现了加锁机制**,**也无法保证外部系统不会修改数据**)。 #### 实现方式 1. 在**对任意记录修改前**,先**尝试为该记录加上排他锁**。 2. 如果**加锁失败**,说明**该记录正在被修改**,那么**当前查询可能要等待或者抛出异常**,具体的响应方式由开发者根据实际需要决定。 3. 如果**成功加锁**,那么就可以**对记录作修改**,**事务成功后就可以解锁了**。 4. 期间如果**有其它对该记录做修改或加排他锁的操作**,都**会等待我们解锁或者抛出异常**。 #### 优缺点 ##### 优点 1. 悲观锁采取的是保守策略,**先加锁,成功了才访问数据**,这保证了**数据获取和修改都是有序进行的**,因此适合在**写多读少**的环境中使用,可以**有效地保证数据的安全性**。 ##### 缺点 1. 由于需要加锁,可能会面临**锁冲突**甚至**死锁**的问题。 2. 加锁和释放锁会**增加系统的额外开销**,**降低系统的效率**,同时也会**降低系统的并行性**。 #### 示例 > 1. 下面是以 MySQL 的 InnoDB 引擎为例。 > > 2. 要使用悲观锁,我们必须关闭 MySQL 数据库的自动提交属性,因为 MySQL 默认使用`autocommit` 模式,也就是说,当我们执行一个更新操作后,MySQL 会立即将结果进行提交,可以使用下面的命令设置 MySQL 为非`autocommit` 模式。 > > ```sql > set autocommit = 0; > ``` 商品 `goods` 表中有一个字段 `status`,`status` 为 1 代表商品未被下单,`status` 为 2 代表商品已被下单,那么对某个商品下单时必须确保该商品 `status` 为 1,假设该商品的 `id` 为 1。 ![](https://notebook.ricear.com/media/202105/2021-05-26_163941.png) 1. **不使用锁时:** 1. 第一步操作中,查询出来的商品`status` 为 1,但是当我们执行第三步`update` 操作的时候,在高并发情况下有可能出现其他人先一步对商品下单,把`goods` 中`id` 为 1 的`status` 修改为 2 了,但是我们并不知道数据已经被修改了,这样就可能造成同一个商品被下单 2 次。 2. **使用悲观锁时:** 1. 与普通查询不一样,这里使用了`select ... for update` 的方式,这样就**通过数据库实现了悲观锁**。 2. 此时在`goods` 表中,`id`**为 1 的那条数据就被锁定了**,直到我们**修改完毕后再解锁**,在这个过程中,因为`goods` 被锁定了,就**不会出现有第三者来对其进行修改**了。 3. 此时如果有其他的事务针对这条数据进行`select ... for update`,那么这个事务就会处于阻塞状态,等待第一个事务的提交,只有当第一个事务提交数据之后,第二个事务才能正常执行。 4. 但是在这个过程中,**正常的 `select` 是不会受影响的**。 5. 执行`select ... for update` 时,锁有三种级别,分别是**无锁、行级锁、表级锁**: 1. **无锁: 明确指定主键/索引**,但**查无数据**。 2. **行级锁: 明确指定主键/索引**,并且**有此数据**。 3. **表级锁:** 1. **未指定主键/索引**。 2. **主键/索引不明确**。 ![](https://notebook.ricear.com/media/202105/3_1622021457.png) ### 乐观锁 #### 含义 1. 相对悲观锁而言,乐观锁假设认为**数据一般情况下不会造成冲突**,所以在**数据进行提交更新**的时候,才会正式**对数据的冲突与否进行检测**,如果发现冲突了,则返回错误的信息,让用户决定如何去做。 2. 相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制,一般的实现方式是**记录数据的版本**。 > 数据版本是什么? > > 1. 数据版本就是为数据增加的一个**版本标识**。 > 2. 当**读取数据**的时候,将**版本标识的值一同读出**,数据**每更新一次**,同时**对版本标识进行更新**。 > 3. 当我们提交更新的时候,判断数据库表对应记录的**当前版本信息**与**第一次取出来的版本标识进行比对**,如果数据库表当前版本号与第一次取出来的**版本标识值相等**,则**予以更新**,**否则认为是过期数据**。 #### 优缺点 ##### 优点 1. 乐观并发控制相信事务之间的数据竞争的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以**不会产生任何锁和死锁**。 2. 乐观并发控制实际上**没有加锁**,所以**没有额外开销**,**适合读多写少的场景**,能**极大提高数据库的性能**。 ##### 缺点 1. 如果**两个事务都读取了数据库的某一行**,经过**同时修改以后写回数据库**,这时就遇到了问题,因为两个事务都会修改数据库的数据,这时**数据就会发生错乱**。 #### 示例 1. `t_goods` 表中的数据如下: ![](https://notebook.ricear.com/media/202106/2021-06-10_112419.png) 2. 首先用户 A 和用户 B 同时将臭豆腐(`id=2`)的数据查出来,此时二者读到的臭豆腐对应的 `id` 为 2, `num` 为 1, `version` 为 0: ```sql select * from t_goods where name = '臭豆腐'; ``` 3. 然后用户 A 先买,将 `id=1` 和 `version=0` 作为条件进行更新(将臭豆腐的数量减 1,并且将版本加 1): ```sql update t_goods set num = {num} - 1, version = {version} + 1 where id = {id} and version = {version}; ``` 4. 接着用户 B 开始购买,按照相同的条件进行更新。 5. B 更新完成后,发现更新的数据行数为 0,说明已经有人改动过数据,此时就应该提示用户 B 重新查看最新数据来进行购买。 ## 意向锁 ### 为什么要引入意向锁 1. 由于**表锁和行锁虽然锁定范围不同**,**但是会相互冲突**,所以,**当我们要加表锁时**,势必**要先遍历该表的所有记录**,**判断是否加有排他锁**。 2. 这种遍历检查的方式显然是一种**低效的方式**,因此 MySQL**引入了意向锁**,来**检测表锁和行锁的冲突**。 ### 特点 1. **意向锁是表级锁**,可以分为**意向共享锁**(IS 锁)和**意向排他锁**(IX 锁)。 2. 当**事务要在记录上加共享锁或者排他锁**时,要**首先在表上加上意向锁**,这样判断表中是否有记录加锁就很简单了,只要看一下**表上是否有意向锁**就行了。 3. **意向锁之间是不会产生冲突**的,他**只会阻塞表级共享锁或表级排他锁**。 4. 同时,**意向锁也不会和行锁冲突**,**行锁只会和行锁冲突**。 5. 意向锁是**InnoDB 自动加的**,**不需要用户干预**。 ### 意向锁的兼容互斥性 1. 意向锁之间是互相兼容的。 | | 意向共享锁(IS) | 意向排他锁(IX) | | ---------------- | ---------------- | --------------- | | 意向共享锁(IS) | 兼容 | 兼容 | | 意向排他锁(IX) | 兼容 | 兼容 | 2. 意向锁会和表级共享锁/排他锁互斥。 | | 意向共享锁(IS) | 意向排他锁(IX) | | ---------- | ---------------- | ---------------- | | 表级共享锁 | 兼容 | 互斥 | | 表级排他锁 | 互斥 | 互斥 | ### 示例 1. 假设有一张 `users` 表,事务 A 获取了其中**某一行的排他锁**,尚**未提交**: ```sql SELECT * FROM users WHERE id = 6 FOR UPDATE; ``` 2. 此时 `users` 表存在两把锁,分别是 `users` 表上的**意向排他锁**和 `id` 为 6 的数据行上的**排他锁**。 3. 事务 B 想要获取 `users`**表的共享锁**: ```sql LOCK TABLES users READ; ``` 4. 此时事务 B 检测**事务 A 持有 `users` 表的意向排他锁**,就可以得知**事务 A 必然持有该表中某些数据行的排他锁**,那么**事务 B 对 `users` 表的加锁请求就会被排斥**(阻塞),而**无需去检测表中的每一行数据是否存在排他锁**。 5. 最后事务 C 也想获取 `users` 表中**某一行的排他锁**: ```sql SELECT * FROM users WHERE id = 5 FOR UPDATE; ``` 6. 此时事务 C 会**申请 `users` 表的意向排他锁**,虽然事务 C**检测到了事务 A 持有 `users` 表的意向排他锁**,但是因为**意向锁之间不互斥**,所以事务 C**获取到了 `users` 表的意向排他锁**,因为 **`id` 为 5 的数据行上不存在任何排他锁**,最终事务 C**成功获取到了该数据行的排他锁**。 ## 行级锁 ### 记录锁 1. **记录锁是最简单的行锁**。 2. 当SQL语句**无法使用索引时**,会进行**全表扫描**,这个时候MySQL会**给整张表的所有数据行加记录锁**,再右MySQL Server层进行过滤,如果发现**不满足 `WHERE`条件**,会**释放对应记录的锁**,这样做,保证了**最后只会持有满足条件记录上的锁**,但是**每条记录的加锁操作还是不能省略的**。 3. 所以,**更新操作必须要根据索引进行操作**,没有索引时,不仅会消耗大量的锁资源,增加数据库的开销,还会极大的降低了数据库的并发性能。 ### 间隙锁 #### 含义 1. 当我们使用**范围条件**而不是相等条件**检索数据**,**并请求共享或排他锁时**,InnoDB会**给符合条件的已有数据记录的索引项加锁**。 2. 对于**键值在条件范围内但并不存在的记录**,InnoDB也**会对这个间隙加锁**,这种锁机制就是所谓的**间隙锁**。 3. 间隙锁是**索引记录中的间隔**,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。 4. 间隙锁在InnoDB的唯一作用就是**防止其他事务的插入操作**,以此来达到**防止幻读**的发生。 #### 如何禁止间隙锁 1. 要禁止间隙锁,主要有两种方法: 1. 把隔离级别降为**读已提交**。 2. 开启参数 `innodb_locks_unsafe_for_binlog`,这个值默认值为 `OFF`,即启用间隙锁,因为此参数是只读模式,如果想要禁用间隙锁,需要修改 `my.cnf`(Windows是 `my.ini`),然后重新启动才行。 ![](https://notebook.ricear.com/media/202106/1_1624519033.png) ```yml # 在 my.cnf 里面的[mysqld]添加 [mysqld] innodb_locks_unsafe_for_binlog = 1 ``` #### 间隙锁定的区域 1. 根据检索条件**向左寻找最靠近检索条件的记录值$A$**,作为**左区间**,**向右寻找最靠近检索条件的记录值$B$**,作为**右区间**,即**锁定的间隙为$(A,B)$**。 #### 间隙锁的分类 下面场景的测试环境为**MySQL 5.7**,存储引擎为**InnoDB**,默认的隔离级别为**可重复读**(Repeatable Read)。 ##### 唯一索引的间隙锁 ###### 总结 1. 对于指定查询**某一条记录**的加锁语句,如果**记录存在**,则会产生**记录锁**,如果该**记录不存在**,则会产生**间隙锁**。 2. 对于查找**某一范围**内的查询语句,会产生**间隙锁**。 ###### 示例 1. 假如有如下示例表: ```sql CREATE TABLE `my_gap` ( `id` int(1) NOT NULL AUTO_INCREMENT, `name` varchar(8) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `my_gap` VALUES ('1', '张三'); INSERT INTO `my_gap` VALUES ('5', '李四'); INSERT INTO `my_gap` VALUES ('7', '王五'); INSERT INTO `my_gap` VALUES ('11', '赵六'); ``` 2. 该表会产生的间隙如下: ```txt 1. (-infinity, 1] 2. (1, 5] 3. (5, 7] 4. (7, 11] 5. (11, +infinity] ``` 3. **只是用记录锁**,**不会产生间隙锁**: 1. ```sql /* 开启事务1 */ BEGIN; /* 查询 id = 5 的数据并加记录锁 */ SELECT * FROM `my_gap` WHERE `id` = 5 FOR UPDATE; /* 延迟30秒执行,防止锁释放 */ SELECT SLEEP(30); # 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句 /* 事务2插入一条 name = '杰伦' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (4, '杰伦'); # 正常执行 /* 事务3插入一条 name = '学友' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (8, '学友'); # 正常执行 /* 提交事务1,释放事务1的锁 */ COMMIT; ``` 2. 上面的案例中,由于**主键是唯一索引**,而且**只使用一个索引查询**,并且**只锁定了一条记录**,所以**只会对 `id=5`的数据加上记录锁**,而**不会产生间隙锁**。 4. **产生间隙锁**: 1. 恢复初始化的4条记录,继续在 `id`唯一索引列上做如下测试: ![](https://notebook.ricear.com/media/202106/2021-06-24_155324.png) 2. 然后执行以下SQL: ```sql /* 开启事务1 */ BEGIN; /* 查询 id 在 7 - 11 范围的数据并加记录锁 */ SELECT * FROM `my_gap` WHERE `id` BETWEEN 5 AND 7 FOR UPDATE; /* 延迟30秒执行,防止锁释放 */ SELECT SLEEP(30); # 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句 ``` /* 事务2插入一条 id = 3,name = '思聪3' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (3, '思聪3'); # 正常执行 /* 事务3插入一条 id = 4,name = '思聪4' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (4, '思聪4'); # 正常执行 /* 事务4插入一条 id = 6,name = '思聪6' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (6, '思聪6'); # 阻塞 /* 事务5插入一条 id = 8, name = '思聪8' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (8, '思聪8'); # 阻塞 /* 事务6插入一条 id = 9, name = '思聪9' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (9, '思聪9'); # 阻塞 /* 事务7插入一条 id = 11, name = '思聪11' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (11, '思聪11'); # 阻塞 /* 事务8插入一条 id = 12, name = '思聪12' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (12, '思聪12'); # 正常执行 /* 提交事务1,释放事务1的锁 */ COMMIT; ``` 3. 从上面可以看到,$(5,7]$、$(7,11]$这两个区间都不可插入数据,其他区间都可以正常插入数据,所以可以得出结论:**当我们给$(5,7]$这两个区间加锁的时候,会锁住$(5,7]$、$(7,11]$这两个区间**。 4. 如果上面的查询SQL改为: ```sql SELECT * FROM `my_gap` WHERE `id` > 5 AND id < 7 FOR UPDATE; ``` 此时,产生的间隙锁会锁住$(5,7)$这个区间。 5. 恢复初始化的4条记录,我们再来测试如果锁住不存在的数据时,会如何。 6. 然后执行以下SQL: ```sql /* 开启事务1 */ BEGIN; /* 查询 id = 3 这一条不存在的数据并加记录锁 */ SELECT * FROM `my_gap` WHERE `id` = 3 FOR UPDATE; /* 延迟30秒执行,防止锁释放 */ SELECT SLEEP(30); # 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句 /* 事务2插入一条 id = 3,name = '小张' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (2, '小张'); # 阻塞 /* 事务3插入一条 id = 4,name = '小白' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (4, '小白'); # 阻塞 /* 事务4插入一条 id = 6,name = '小东' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (6, '小东'); # 正常执行 /* 事务5插入一条 id = 8, name = '大罗' 的数据 */ INSERT INTO `my_gap` (`id`, `name`) VALUES (8, '大罗'); # 正常执行 /* 提交事务1,释放事务1的锁 */ COMMIT; ``` 7. 从上面可以看出,**指定查询某一条记录**时,**如果这条记录不存在**,**会产生间隙锁**。 ##### 普通索引的间隙锁 ###### 总结 1. 在**普通索引列**上,**不管是何种查询**,**只要加锁**,**都会产生间隙锁**,这跟唯一索引不一样。 2. 在**普通索引跟唯一索引中**,**数据间隙的分析**,**数据行是优先根据普通索引排序**,然后**再根据唯一索引排序**。 ###### 示例 1. 假如有如下示例表,其中 `id`是主键,同时在 `number`上,建立了一个普通索引: ```sql # 注意:number 不是唯一值 CREATE TABLE `my_gap1` ( `id` int(1) NOT NULL AUTO_INCREMENT, `number` int(1) NOT NULL COMMENT '数字', PRIMARY KEY (`id`), KEY `number` (`number`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; INSERT INTO `my_gap1` VALUES (1, 1); INSERT INTO `my_gap1` VALUES (5, 3); INSERT INTO `my_gap1` VALUES (7, 8); INSERT INTO `my_gap1` VALUES (11, 12); ``` 2. `my_gap1`表中 `number`索引存在如下隐藏间隙: ```txt 1. (-infinity, 1] 2. (1, 3] 3. (3, 8] 4. (8, 12] 5. (12, +infinity] ``` 3. **测试1**: 1. 执行以下SQL: ```sql /* 开启事务1 */ BEGIN; /* 查询 number = 3 的数据并加记录锁 */ SELECT * FROM `my_gap1` WHERE `number` = 3 FOR UPDATE; /* 延迟30秒执行,防止锁释放 */ SELECT SLEEP(30); # 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句 /* 事务2插入一条 number = 0 的数据 */ INSERT INTO `my_gap1` (`number`) VALUES (0); # 正常执行 /* 事务3插入一条 number = 1 的数据 */ INSERT INTO `my_gap1` (`number`) VALUES (1); # 被阻塞 /* 事务4插入一条 number = 2 的数据 */ INSERT INTO `my_gap1` (`number`) VALUES (2); # 被阻塞 /* 事务5插入一条 number = 4 的数据 */ INSERT INTO `my_gap1` (`number`) VALUES (4); # 被阻塞 /* 事务6插入一条 number = 8 的数据 */ INSERT INTO `my_gap1` (`number`) VALUES (8); # 正常执行 /* 事务7插入一条 number = 9 的数据 */ INSERT INTO `my_gap1` (`number`) VALUES (9); # 正常执行 /* 事务8插入一条 number = 10 的数据 */ INSERT INTO `my_gap1` (`number`) VALUES (10); # 正常执行 /* 提交事务1 */ COMMIT; ``` 2. 因为**查询的 `number`的值为3**,而且 **`number`为普通索引**,因此**会产生间隙锁**,间隙锁的**区间为$(1,8)$**,因此 `number`**在这个区间的插入SQL都会执行成功**,**不在这个区间的插入SQL都会执行失败**。 4. **测试2**: 1. 首先将数据还原为初始化的形式,然后执行如下SQL: ```sql /* 开启事务1 */ BEGIN; /* 查询 number = 3 的数据并加记录锁 */ SELECT * FROM `my_gap1` WHERE `number` = 3 FOR UPDATE; /* 延迟30秒执行,防止锁释放 */ SELECT SLEEP(30); /* 事务1插入一条 id = 2, number = 1 的数据 */ INSERT INTO `my_gap1` (`id`, `number`) VALUES (2, 1); # 阻塞 /* 事务2插入一条 id = 3, number = 2 的数据 */ INSERT INTO `my_gap1` (`id`, `number`) VALUES (3, 2); # 阻塞 /* 事务3插入一条 id = 6, number = 8 的数据 */ INSERT INTO `my_gap1` (`id`, `number`) VALUES (6, 8); # 阻塞 /* 事务4插入一条 id = 8, number = 8 的数据 */ INSERT INTO `my_gap1` (`id`, `number`) VALUES (8, 8); # 正常执行 /* 事务5插入一条 id = 9, number = 9 的数据 */ INSERT INTO `my_gap1` (`id`, `number`) VALUES (9, 9); # 正常执行 /* 事务6插入一条 id = 10, number = 12 的数据 */ INSERT INTO `my_gap1` (`id`, `number`) VALUES (10, 12); # 正常执行 /* 事务7修改 id = 11, number = 12 的数据 */ UPDATE `my_gap1` SET `number` = 5 WHERE `id` = 11 AND `number` = 12; # 阻塞 /* 提交事务1 */ COMMIT; ``` 2. 查看表中的数据: ![](https://notebook.ricear.com/media/202106/2021-06-24_163621.png) 3. 这里有一个奇怪的现象: 1. 事务3 添加 id = 6,number = 8 的数据,阻塞了。 2. 事务4 添加 id = 8,number = 8 的数据,正常执行了。 3. 事务7 将 id = 11,number = 12 的数据修改为 id = 11, number = 5 的操作,给阻塞了。 首先我们可以看一下下面这张图: ![image.png](https://notebook.ricear.com/media/202106/2021-06-24_163848.png) 从图中可以看出,当 `number`相同时,会根据主键 `id`来排序: 1. 事务 3 添加的 id = 6,number = 8,这条数据是在 (3,8) 的区间里边,所以会阻塞。 2. 事务 4 添加的 id = 8,number = 8,这条数据实在 (8,12) 区间里边,所以不会阻塞。 3. 事务 7 的修改语句相当于 在 (3,8) 的区间里边插入一条数据,所以也被阻塞了。 ### 临键锁 1. **临键锁是记录锁和间隙锁的组合**,他的**锁范围既包含索引记录**,**又包含索引区间**。 2. InnoDB默认使用**可重复读**(Repeatable Read)**隔离级别**,同时在**索引查询时使用临键锁来避免幻读的产生**。 ## 参考文献 1. [深入理解 MySQL 锁类型和加锁原理](https://juejin.cn/post/6878884451162521613)。 2. [mysql 悲观锁详解](https://juejin.cn/post/6844903437579059214)。 3. [一文读懂数据库中的乐观锁和悲观锁和 MVCC](https://learnku.com/articles/47517)。 4. [合理的使用 MySQL 乐观锁与悲观锁](https://zhuanlan.zhihu.com/p/139007138)。 5. [mysql 悲观锁和乐观优缺点_乐观锁、悲观锁和 MVCC 各是什么?各自优缺点是什么?...](https://blog.csdn.net/weixin_39947314/article/details/113720708) 6. [悲观锁与乐观锁的实现(详情图解)](https://juejin.cn/post/6937494982945144863)。 7. [深入理解数据库行锁与表锁](https://zhuanlan.zhihu.com/p/52678870)。 8. [MySQL 当中的各种锁(中级篇)](https://learnku.com/articles/42715)。 9. [MySQL 中的锁(表锁、行锁)](https://www.cnblogs.com/chenqionghe/p/4845693.html)。 10. [13.3.6 LOCK TABLES and UNLOCK TABLES Statements](https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html)。 11. [详解 MySql InnoDB 中意向锁的作用](https://juejin.cn/post/6844903666332368909)。 12. [mysql-行锁 + 间隙锁(next-key lock)](https://quguang.wang/post/mysql-next-key-lock)。 13. [mysql-行锁+间隙锁(next-key lock)](https://quguang.wang/post/mysql-next-key-lock)。 14. [In-depth understanding of mysql--gap locks, Next-Key Locks](https://www.programmersought.com/article/99765878120)。 15. [14.7.4 Phantom Rows](https://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html)。
ricear
July 31, 2022, 3:56 p.m.
©
BY-NC-ND(4.0)
转发文档
Collection documents
Last
Next
手机扫码
Copy link
手机扫一扫转发分享
Copy link
Markdown文件
share
link
type
password
Update password