上篇博客说了 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
//事务 A begin; select * from tbl_user whereid=1forupdate;
1 2 3 4 5
//事务 B begin; select * from tbl_user whereid=1forupdate;//不可以查询 select * from tbl_user whereid=2forupdate; //可以查询
B 事务中非相同 ID 的数据能够正常的查询。说明在主键 ID 情况下,使用的是行锁,只简单的锁住了一条数据。
唯一索引和主键索引效果一致。
普通索引
1 2 3
//事务 A begin; select * from tbl_user where user_name='user1'forupdate;
1 2 3 4
//事务 B begin; select * from tbl_user where user_name='user1'forupdate;//不可以查询 select * from tbl_user where user_name='user2'forupdate; //可以查询
使用普通索引,也会把所有的查询出来的数据加锁
普通索引插入数据
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 =10forupdate;
1 2 3 4 5 6 7
begin; select * from tbl_user where user_name='user1'forupdate; //不可以查询 select * from tbl_user whereid=1forupdate; //不可以查询 select * from tbl_user where user_age=15forupdate; //不可以查询 select * from tbl_user where user_age=10forupdate; //不可以查询 select * from tbl_user whereid=2forupdate; //可以查询 select * from tbl_user where user_name='user2'forupdate; //可以查询
对于 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'forupdate;//阻塞 select * from tbl_user where user_name='user2'forupdate;//阻塞 select * from tbl_user where user_age =10forupdate;//阻塞 select * from tbl_user where user_age =15forupdate;//阻塞