mysql innodb行级锁的理解,究竟是什么解决了幻读
程序员文章站
2022-06-14 15:29:30
...
行锁
行锁是为了最大并发化所提供的一种锁,*某一行数据。我知道的mysql行锁有三种,就间隙锁使用场景,我分成了唯一索引
和非唯一索引
两种情况。记住所有的for update都是当前读并且加上行锁,跟快照读不一样,你需要明白这个问题。
- Record Lock: 记录锁,就是字面意思锁定某一行数据,值得注意的是,
只有通过索引进行检索的时候才会使用行级锁,如果不是通过索引进行检索就会升级成表锁。
- gap Lock: 间隙锁,开区间范围性加锁
- Next-key Lock: gap+record,闭区间范围性加锁
当主键作为索引的时候,主键肯定是唯一索引
为了防止混乱,我用两张不同的表进行了实验
Record Lock
查看表索引:
mysql> show index from account;
+---------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+---------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| account | 0 | PRIMARY | 1 | id | A | 4 | NULL | NULL | | BTREE | | |
+---------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
行锁:
//事务A锁住id为1的
mysql> select * from account where id='1' for update;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 3000 |
+----+------+-------+
1 row in set (0.00 sec)
//事务B获得行级写锁id为1的时候失败
mysql> select * from account where id='1' for update;
^C^C -- query aborted
//获得行级写锁ID为2成功,说明了并没有发生间隙锁
mysql> select * from account where id='2' for update;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 2 | bbb | 5000 |
+----+------+-------+
1 row in set (0.00 sec)
行锁变表锁:
// 事务A
mysql> select * from account where name='aaa' for update;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 3000 |
+----+------+-------+
1 row in set (0.00 sec)
//事务B,阻塞
mysql> select * from account where id=1 for update;
^C^C -- query aborted
mysql> select * from account where id=2 for update;
^C^C -- query aborted
mysql> select * from account for update;
^C^C -- query aborted
分析:
为了让实验更明显,这里是手动进行select for update进行写加锁,事务A通过非索引name进行搜索,此时它搜索的是整个表,所以此时不会只锁那一行,而是将整个表锁住,此时事务B进行获取写锁的时候就会被阻塞,无法获得想要的写锁。
Gap Lock间隙锁
//事务A,指明了区间1,+无穷
mysql> select * from account where id>1 for update;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 2 | bbb | 5000 |
| 3 | ccc | 5000 |
| 5 | ddd | 5000 |
| 6 | eee | 5000 |
+----+------+-------+
4 rows in set (0.00 sec)
//事务B,阻塞
mysql> select * from account where id='3' for update;
^C^C -- query aborted
next-key lock
和gap一样,只是区间不同
总结
我们可以看到,如果索引是在唯一索引上,那么它的这个间隙是不存在的,这不是真正的间隙,除了手动指明区间的时候它才会对那个区间进行*,比如>10是(10,+无穷),>=10是[10,+无穷),下面我们看看当非唯一索引的情况。
非唯一索引
表结构:
mysql> desc test2;
+-------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| value | int(11) | YES | MUL | NULL | |
+-------+---------+------+-----+---------+-------+
表索引:
mysql> show index from test2;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| test2 | 0 | PRIMARY | 1 | id | A | 3 | NULL | NULL | | BTREE | | |
| test2 | 1 | test | 1 | value | A | 3 | NULL | NULL | YES | BTREE | | |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
主键是唯一索引,新添加一个普通索引在value上。 实验数据:
mysql> select * from test2;
+----+-------+
| id | value |
+----+-------+
| 1 | 1 |
| 3 | 3 |
| 5 | 5 |
+----+-------+
next-key Lock
一般情况下
有了上面唯一索引的经验,以value=3为例,手动为value=3的那一行进行加锁,那么锁的范围就是(1,3]+[3,5),也就是(1,3)和(3,5)加上3本身。也就是这个范围内的值不能再作为value被使用.
| 1 | 1 |
xxxxxxxxxxxxxx间隙
| 3 | 3 |
xxxxxxxxxxxxxx间隙
| 5 | 5 |
//事务A进行手动加锁
select * from test where value=3 for update;
//事务B
//分别尝试对间隙进行插入
mysql> insert into test2 values(2,2);
^C^C -- query aborted
mysql> insert into test2 values(4,4);
^C^C -- query aborted
//验证开区间
mysql> update test2 set value=0 where value =1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
//验证行级锁
mysql> update test2 set value=0 where value=3;
^C^C -- query aborted
当那一行不存在的时候
情况A:
表内区间
//事务A
mysql> update test2 set value=4 where value=4;
//事务B,尝试对不存在的那一行进行插入,阻塞,其实它的*范围是(3,5),也就是说4不能再被使用
mysql> insert into test2 values(4,4);
^C^C -- query aborted
情况B:
非表内区间
//事务A
mysql> update test2 set value=0 where value=10000;
//事务B,全部阻塞,由此看出*区间是(5,+无穷)
mysql> insert into test2 values(1111111,1111111);
^C^C -- query aborted
mysql> insert into test2 values(200,200);
^C^C -- query aborted
mysql> insert into test2 values(6,6);
^C^C -- query aborted
到此,间隙锁就基本上分析清楚了
总结
- 间隙锁一般都是针对非唯一索引而言的,它会对相邻的区间进行*,被锁上的区间内的值是无法再被使用的。当那个条件不存在的时候会找表中存在的相邻数据,然后再进行加锁。
- 当主键做为条件的时候,不存在间隙锁,除非手动指定范围。
解决幻读
幻读的种类分为两种
- 当前读,这种手动带锁的读就是当前读,每次读依据非唯一索引作为条件,进行范围读取,某一个where范围内被锁住,这个范围内不允许insert,所以就不会有幻读,避免了这一个问题,即使是唯一索引的情况,手动指明范围也是锁住了的。
- 快照读,对于普通的select语句,是不需要加锁的,用的是MVCC解决的幻读,请参考上篇博客https://blog.csdn.net/qq_41376740/article/details/82314915
如有错误,还望指正,可能理解比较片面
上一篇: 软件工程基础——个人项目——数独(1)
下一篇: Unity3D移动端实现摇一摇功能