有一个多门店点餐系统,点餐就是个下订单的过程,数据库设计上是一个订单表存储了全部店铺的订单信息,则有一个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);
复制代码
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,而当中的每一次遍历都会同时锁上主键索引。优化
众所周知,使用了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 解决了脏读、重复读、幻读问题。
MVCC的特性比较好理解和分析,这里重点分析RR下的MySQL的加锁过程。 首先,这里总结了几条MySQL的加锁和优化原则:
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并不能完彻底全的禁止幻读。