上篇博客说了 MVCC
解决了 MySQL 在可重复的隔离情况下幻读的问题,这篇博客主要探讨下,在修改的时候,如何解决幻读的问题。
MySQL 在控制并发的时候,同样采用了锁的机制。从读写上面分,有读写和写锁
,从结构上分,有行锁和表锁
.行锁又分为行锁、间隙锁和 Next Key
读锁和写锁 读锁 :共享锁 ,S 锁
写锁:排它锁 ,X 锁
select :不加锁,加锁后,也可以使用 select
查询数据
怎么加锁 select ...lock in share mode
加读锁
select ...for updare ,update、 delete
都是加写锁
其中,读读共享, 读写互斥 写写互斥
insert
是不会加入写锁,因为 MySQL 在读的时候有 MVCC 来控制读取,无需直接添加写锁。但是会加一个隐式锁
,防止当前事务提交前,数据被其他事务访问。
隐式锁:
一个事务插入一条记录后,还未提交,这条记录会保存本次事务 id ,而其他的事务如果想来的对这个记录加时会发现事务 id 不对应,这个隐式锁会转换成显示的 写锁(X 锁)。
行锁和表锁 行锁:
Record Lock
:单个行记录上的锁,只锁定记录本身
Gap Lock
:间隙锁,锁定一个范围,但不包括记录本身。 目的是为了防止同一个事物的两次当前读,出现幻读的情况
Next-Key Lock
:1+2,锁定一个范围,并锁定记录本身。目的:解决幻读
我们还是采用 读已提交和可重复读
来测试锁的情况。
读已提交 主键索引写锁 1 2 3 / / 事务 Abegin ;select * from tbl_user where id= 1 for update ;
1 2 3 4 5 / / 事务 B begin ;select * from tbl_user where id= 1 for update ;/ / 不可以查询select * from tbl_user where id= 2 for update ; / / 可以查询
B 事务中非相同 ID 的数据能够正常的查询。说明在主键 ID 情况下,使用的是行锁,只简单的锁住了一条数据。
唯一索引和主键索引效果一致。
普通索引 1 2 3 / / 事务 Abegin ;select * from tbl_user where user_name= 'user1' for update ;
1 2 3 4 / / 事务 B begin ;select * from tbl_user where user_name= 'user1' for update ;/ / 不可以查询select * from tbl_user where user_name= 'user2' for update ; / / 可以查询
使用普通索引,也会把所有的查询出来的数据加锁
普通索引插入数据 1 2 3 begin ;mysql> select * from tbl_user where user_name = 'user1' for update ;
1 2 begin; mysql> insert into tbl_user (user_name,user_code,user_age) values ('user1','u0007',45);//正常插入
普通索引插入已经锁定的数据是能够正常插入,事务提交后,A 事务中也能够正常的查询数据,这个时候 A 事务就产生了幻读 。
全表扫描 1 2 begin ;select * from tbl_user where user_age = 10 for update ;
1 2 3 4 5 6 7 begin ;select * from tbl_user where user_name= 'user1' for update ; / / 不可以查询select * from tbl_user where id= 1 for update ; / / 不可以查询select * from tbl_user where user_age= 15 for update ; / / 不可以查询select * from tbl_user where user_age= 10 for update ; / / 不可以查询select * from tbl_user where id= 2 for update ; / / 可以查询select * from tbl_user where user_name= 'user2' for update ; / / 可以查询
通过上面的分析,全表扫描的情况下,也是只会对查询出来的数据加锁,但是如果再次使用加锁的字段再去查询,都会被阻塞。 (此处还是一个疑问,知道的朋友可以指点下)
可重复读的隔离级别 主键索引和唯一索引 结果与 读已提交
结果相同
普通索引查询 结果与 读已提交
结果相同
普通索引插入 1 2 begin ;select * from tbl_user where user_name= 'user1' for update ;
1 2 3 4 5 6 7 begin ;insert into tbl_user (user_name,user_code,user_age) values ('user1' ,'u0006' ,40 ) / / 插入 user1 无法插入。insert into tbl_user (user_name,user_code,user_age) values ('user11' ,'u000100' ,100 );/ / 插入 user1 无法插入。insert into tbl_user (user_name,user_code,user_age) values ('user10' ,'u000100' ,100 );/ / 插入 user1 无法插入。insert into tbl_user (user_name,user_code,user_age) values ('user6' ,'u0006' ,40 ) / / 能够成功插入insert into tbl_user (user_name,user_code,user_age) values ('user2' ,'u00020' ,100 );/ / 能够成功插入
在插入同样的数据的时候,MySQL 会阻塞当前插入,这样就能够给防止幻读的产生。
对于 user10
, user11
无法插入,是因为 MySQL 对数据添加了写锁的同时,又对数据添加了间隙锁,当然这个间隙锁 是有范围的。当插入 user2
,user6
的时候又可以插入了。
全表扫描 1 2 begin; select * from tbl_user where user_age =10 for update;
1 2 3 4 5 begin ;select * from tbl_user where user_name= 'user1' for update ;/ / 阻塞select * from tbl_user where user_name= 'user2' for update ;/ / 阻塞select * from tbl_user where user_age = 10 for update ;/ / 阻塞select * from tbl_user where user_age = 15 for update ;/ / 阻塞
全表扫描的情况下,所有的插入都会被阻塞,这个时候会锁住全部数据和所有的间隙。这种情况在生产环境中,一定要避免,否则会阻塞所有的更改操作。
总上对比结果如下:
隔离级别
主键索引
唯一索引
普通索引
全表扫描
读已提交
行锁
行锁
行锁
行锁
可重复读
行锁
行锁
间隙锁+行锁
锁全部数据和间隙
通过上面的分析,MySQL 防止幻读的产生总有两个策略,MVCC
解决读取的幻读,间隙锁和行锁(Next-Key Lock)来保证修改的时候产生的幻读。