MySQL INNODB锁分析心得

场景重现

有一个多门店点餐系统,点餐就是个下订单的过程,数据库设计上是一个订单表存储了全部店铺的订单信息,则有一个order表,而且每条记录都会关联business_idmysql

DDL:sql

CREATE TABLE `o` (
  `id` int(11) NOT NULL,
  `business_id` int(11) DEFAULT NULL,
  `status` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
复制代码

插入数据:数据库

INSERT INTO test_innodb.`order` (id,business_id,status) VALUES 
(0,0,0)
,(5,5,5)
,(10,10,10)
,(11,10,15)
,(20,20,20)
,(25,25,25);
复制代码
  • 更新店铺id为10的订单状态为3

UPDATE o SET status = 3 WHERE business_id = 10;bash

session A session B
begin;
UPDATE o SET status = 3 WHERE business_id = 10;
begin;
INSERT INTO o VALUES(30, 10, 3) (BLOCKED)

查询当前INNODB_LOCKS的状况session

MySQL [test_innodb]> SELECT * FROM information_schema.INNODB_LOCKS;
+-------------------+-------------+-----------+-----------+-----------------------+------------+------------+-----------+----------+------------------------+
| lock_id           | lock_trx_id | lock_mode | lock_type | lock_table            | lock_index | lock_space | lock_page | lock_rec | lock_data              |
+-------------------+-------------+-----------+-----------+-----------------------+------------+------------+-----------+----------+------------------------+
| 41043937:3410:3:1 | 41043937    | X         | RECORD    | `test_innodb`.`order` | PRIMARY    |       3410 |         3 |        1 | supremum pseudo-record |
| 41043889:3410:3:1 | 41043889    | X         | RECORD    | `test_innodb`.`order` | PRIMARY    |       3410 |         3 |        1 | supremum pseudo-record |
+-------------------+-------------+-----------+-----------+-----------------------+------------+------------+-----------+----------+------------------------+
复制代码

能够看到当前lockdata中显示了supremum,实际上因为business_id不是索引,Mysql须要从根结点开始遍历全部主键索引,过滤找到匹配的值,因此全部记录包括 [30, supremum+] 的主键索引都被锁住了。数据库设计

此时给business_id加上一个普通索引,重放上面的session状况,再次查询INNODB_LOCKS的状况性能

MySQL [test_innodb]> SELECT * FROM information_schema.INNODB_LOCKS;
+-------------------+-------------+-----------+-----------+-----------------------+-----------------------+------------+-----------+----------+-----------+
| lock_id           | lock_trx_id | lock_mode | lock_type | lock_table            | lock_index            | lock_space | lock_page | lock_rec | lock_data |
+-------------------+-------------+-----------+-----------+-----------------------+-----------------------+------------+-----------+----------+-----------+
| 41042271:3410:4:6 | 41042271    | X,GAP     | RECORD    | `test_innodb`.`order` | order_business_id_IDX |       3410 |         4 |        6 | 20, 20    |
| 41034805:3410:4:6 | 41034805    | X,GAP     | RECORD    | `test_innodb`.`order` | order_business_id_IDX |       3410 |         4 |        6 | 20, 20    |
+-------------------+-------------+-----------+-----------+-----------------------+-----------------------+------------+-----------+----------+-----------+
复制代码

lock_type列中出现了新的字眼GAP,这是为了不新数据插入产生的一种锁,根据目前SQL执行的状况,在普通索引business_id (5, 20)会被锁,主键索引id (5,20) 也会被加上锁。这是因为Mysql 先经过普通索引business_id 符合搜索值为10的索引,这个过程左边第一个不符合的条件为5,右边第一个不符合的条件为20,而当中的每一次遍历都会同时锁上主键索引。优化

理论分析

索引基础知识

  1. 经过非主键索引查询时,Mysql先找到非主键索引,再找到主键索引,再找到记录(B+树)
  2. 非惟一索引搜索时,会向左向右找到第一个不符合条件的索引(二分查找)
  3. Mysql Innodb的锁做用是索引
  4. 一个SQL中若是有屡次加锁操做,他们并非在一个指令内完成的

Next-Key Lock

众所周知,使用了INNODB引擎后天然就会有行锁的概念,而行锁又分为S锁和X锁,分别表明了读锁和写锁,他们之间有以下关系:ui

S X
S 共享 冲突
X 冲突 冲突

SHARE MODE、FOR UPDATE 、UPDATE、DELETE、INSERT 都会产生不一样的锁类型,具体能够参考MySQL的官方文档,这里咱们暂时只须要了解是行锁便可spa

再有,大部分场景下咱们都会沿用Mysql的默认事务隔离级别:Repeatable read,简称RR(可重复读),这个机制自己能够避免脏读重复读的问题,不过没法解决幻读问题,除非将隔离级别上升到Serializable(可串行化),可是这样性能损耗太大。

MySQL为了更好地解决幻读问题,在INNODB引擎里的可重复读隔离级别下加入了Gap Lock(间隙锁)的机制。对于间隙锁能够先大体理解为,MySQL为了不新的数据插入形成幻读,加大了行锁的粒度,锁住了某个索引的数据区间

因此 MVCC(RR的特性) + Gap Lock + Row Lock 解决了脏读、重复读、幻读问题。

  • 从而,这里就产生了 Next-Key Lock 的定义: Gap Lock + Row Lock,这是一个前开后闭的区间。

MVCC的特性比较好理解和分析,这里重点分析RR下的MySQL的加锁过程。 首先,这里总结了几条MySQL的加锁和优化原则:

  • 原则 1:加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间。
  • 原则 2:查找过程当中访问到的对象才会加锁。
  • 优化 1:索引上的等值查询,给惟一索引加锁的时候,next-key lock 退化为行锁。
  • 优化 2:索引上的等值查询,向右遍历时且最后一个值不知足等值条件的时候,next-key lock 退化为间隙锁。
  • 一个 bug:惟一索引上的范围查询会访问到不知足条件的第一个值为止。

案例分析

  • 等值查询间隙锁
  • 非惟一索引等值锁
  • 主键索引范围锁
  • 非惟一索引范围锁
  • 惟一索引范围锁
  • 死锁的例子

附录

MVCC + Next-Key Lock 没有办法解决下面这种例外状况:

# 事务T1,REPEATABLE READ隔离级别下
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM hero WHERE number = 30;
Empty set (0.01 sec)

# 此时事务T2执行了:INSERT INTO hero VALUES(30, 'g关羽', '魏'); 并提交

mysql> UPDATE hero SET country = '蜀' WHERE number = 30;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM hero WHERE number = 30;
+--------+---------+---------+
| number | name    | country |
+--------+---------+---------+
|     30 | g关羽   | 蜀      |
+--------+---------+---------+
1 row in set (0.01 sec)
复制代码

在REPEATABLE READ隔离级别下,T1第一次执行普通的SELECT语句时生成了一个ReadView,以后T2向hero表中新插入了一条记录便提交了,ReadView并不能阻止T1执行UPDATE或者DELETE语句来对改动这个新插入的记录(由于T2已经提交,改动该记录并不会形成阻塞),可是这样一来这条新记录的trx_id隐藏列就变成了T1的事务id,以后T1中再使用普通的SELECT语句去查询这条记录时就能够看到这条记录了,也就把这条记录返回给客户端了。由于这个特殊现象的存在,你也能够认为InnoDB中的MVCC并不能完彻底全的禁止幻读。

参考:mp.weixin.qq.com/s/wSlNZcQka…

相关文章
相关标签/搜索