读锁:S锁
写锁:X锁
S锁 | X锁 | |
---|---|---|
S锁 | 读读共享 | 读写互斥 |
X锁 | 读写互斥 | 写写互斥 |
读操作:
写操作:
对于DELETE操作,先对要删除记录加X锁,再执行删除操作
对于INSERT操作,会加一个隐式锁来保证该待插入记录再提交前不会被其他事务访问到
隐式锁:某事务插入记录时,还未提交,记录里会存有该事务id,当其他事务来查询该记录时发现事务id不是自己的id,即其他事务查询不到该未提交的数据,请参考mysql-事务,基于版本链+ReadView的多版本并发控制的MVCC模型
对于UPDATE操作,如果修改前后没有导致记录存储空间变化,则会先加X锁,再修改数据,否则,会现加X锁,删除记录,插入新记录
行锁从概念上看有行读锁和行写锁,从粒度上看有单个行记录锁,间隙锁,范围锁,这几点会在下一小节锁的算法提及
表的S,X锁
对表加读锁:LOCK TABLES t1 READ
对表加写锁:LOCK TABLES t1 WRITE
解锁:UNLOCK TABLES(如何解锁特定表?知道的评论告诉我)
一般不对表进行加锁,因为InnoDB引擎的优点之一就是行锁的设计,粒度小,性能更高
IS锁,IX锁
行锁 | 表锁 | 阻塞与否 |
---|---|---|
X | X | 阻塞 |
X | S | 不阻塞 |
S | X | 阻塞 |
S | S | 不阻塞 |
无锁 | X或S | 不阻塞 |
从表格可以看出,对表加S锁都会成功,当存在行记录锁(S或X),对表加X锁都会失败
反过来,当存在表锁是执行for update和lock in share mode 会发生什么呢?
表锁 | 行锁 | 阻塞与否 |
---|---|---|
X | X | 阻塞 |
X | S | 阻塞 |
S | X | 阻塞 |
S | S | 不阻塞 |
解释一下第一行数据:事务1加表写锁 LOCK TABLES t1 WRITE,事务2select… for update会一直阻塞
AUTO-INC锁
系统变量 show variables like ‘innodb_autoinc_lock_mode’
innodb_autoinc_lock_mode值为0:采用AUTO-INC锁。
innodb_autoinc_lock_mode值为2:采用轻量级锁。
当innodb_autoinc_lock_mode值为1:当插入记录数不确定是采用AUTO-INC锁,当插入记录数确
定时采用轻量级锁。
附加几点知识点
在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的S锁或者X锁
在对某个表执行ALTER TABLE、DROP TABLE这些DDL语句时,其他事务对这个表执行SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞,或者,某个事务对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,其他事务对这个表执行DDL语句也会发生阻塞。这个过程是通过使用的元数据锁(英文名:MetadataLocks,简称MDL)来实现的,并不是使用的表级别的S锁和X锁。
下面我们举很多例子来说明这三种情况:
隔离级别是可重复读,user表如下:
id为自增主键,name是唯一索引,age是普通索引
例1:使用主键精确查询
事务1:
begin
select * from user where id=1 for update
事务2:
select * from user where id = 1 for update 阻塞
select * from user where id = 3 for update 不阻塞
ps: 记得执行完事务2去事务1commit一下
例2:使用主键范围查找
事务1:
begin
select * from user where id <7 for update
事务2:
select * from user where id = 1或3或5 for update 阻塞
select * from user where id = 2或4或6 for update 不阻塞,返回空记录
insert user (id,name,sex,age) values(2或4或6,'sd','男',43) 阻塞,这里阻塞得仔细想一想,如果不阻塞的话,插入成功,
事务1再去查找id<7的记录就会出现幻读,所以说innodb可重复读解决了幻读问题,如果是已提交读隔离级别这里则不会阻塞
例3:使用唯一索引 like查找
事务1:
begin
select * from user where name like 'a%' for update
事务2:
select * from user where id = 1或3 for update 阻塞
select * from user where id = 2或4或6 for update 不阻塞,返回空记录
select * from user where id = 5或7或9或11 for update 不阻塞,返回记录
insert user (id,name,sex,age) values(6,'aa','男',43)阻塞
insert user (id,name,sex,age) values(2,'ba','男',43)不阻塞
说明插入的时候只能插入不符合name like 'a%'的数据
例4:使用唯一索引 ,但是索引失效
事务1:
begin
select * from user where name > 'b' for update
此时索引失效,转为全表扫描
事务2:
select * from user where id=1或3或5或7或9或11 for update 阻塞
执行任何插入语句也插入不了
例5:使用普通索引
事务1:
begin
select * from user where age = 30 for update
事务2:
select * from user where id = 3 for update 阻塞
select * from user where id = 除了3 for update 不阻塞
insert user (id,name,sex,age) values(2,'ab','男',20-40) 阻塞 (包括20,30,40)
这里就是范围锁了,为age=30的两端间隙20-30,30-40设置间隙锁,加上三条记录锁就是范围锁,
其实这里我想不太通为什么要设置范围锁,其实我觉得只要设置不允许插入age=30的数据就好了啊,
但是我实际测试出来就是设置了范围锁,知道的可以告诉一下我
有如下场景:用户A给B和C转账,假设A有1000元,给B转100,给C转200
第一步:事务1查出A余额1000
第二步:事务2查出A余额1000
第三步:事务1给B转100元,此时账户A是900元,B是100元
第四步:事务2给C转200元,此时账户A是800元,C是200元
结果我们发现A+B+C的余额是800+100+200=1100,造成这种结果的原因是第三步的更新A账户减100这次更新丢失了,被第四步覆盖掉
很容易发现其实第一步第二步不可以这样直接就查出来余额,我们希望步骤是1,3——2,4
解决方案:
悲观锁:第一步查询余额时select balance from table for update,先占一把写锁,这样直到事务1释放之前事务2都查不到余额
乐观锁:使用一个数据库字段version,初始设置为1,我们再来考虑上面四步
第一步事务1查出A余额1000,version=1
第二步事务2查出A余额1000,version=1
第三步:事务1给B转100元,此时账户A是900元,B是100元,修改数据库version=2
第四步:事务2给C转200元,但是此时他发现我的version=1数据库却是2,此次转账失败
最终A余额900,B 100元,C 0元
例子:
事务1
begin
1 select * from user where id = 1 for update
3 select * from user where id = 3 for update
commit
事务2
begin
2 select * from user where id = 1 for update
4 select * from user where id = 3 for update
commit
四条select语句执行顺序按照前面的数字,越小的越先执行
避免死锁