mysql——锁

mysql——锁

InnoDB锁

读锁与写锁

读锁:S锁
写锁:X锁

S锁 X锁
S锁 读读共享 读写互斥
X锁 读写互斥 写写互斥

读操作:

  • 普通的select操作,InnoDB不会加任何锁
  • select… lock in share mode,将查到的数据加一把S锁,不允许其他事务对这些数据加X锁
  • selct… for update,将查到的数据加一把X锁,不允许其他事务对这些数据加X锁或S锁
    可以开启两个事务去测试这些理论

写操作:

  • 对于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锁

  • IS锁:意向共享锁,当事务准备在行记录上加一把S锁的时候,会先对表加一把IS锁
  • IX锁:意向排他锁,当事务准备在行记录上加一把X锁的时候,会先对表加一把IX锁
    IS锁和IX锁的提出仅仅是为了在对表加S锁,X锁的时候可以快速判断表中是否有记录加了锁,而不用遍历表中所有记录看看有没有锁记录
    提出问题如下:对表加S,X锁的时候为何要看看是否有记录加了锁?
    我的猜想:如果表中有记录有锁,可能对表加X锁就阻塞导致加不上表写锁
    验证:事务1:select… for update 但不提交, 事务二尝试去LOCK TABLES t1 WRITE发现一直阻塞直到事务1提交
    结论如下:
    我解释一下表格第一行数据,其他类似:
    事务1对某行记录加写锁但不提交,事务2对表加写锁会发生阻塞,直到事务1提交
行锁 表锁 阻塞与否
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锁

  • 1:在执行插入就生成一个表级别的AUTO-INC锁,为每个待插入记录分配AUTO-INCREMENT的值,然后执行INSERT操作,最后释放AUTO-INC锁
  • 2:在为每个待插入记录分配AUTO-INCREMENT的值的时候获取一把轻量级锁,获得递增值后酒吧轻量级锁释放,再执行INSERT

系统变量 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锁。

锁的算法

  • Record Lock:单个行记录上的锁
  • Gap Lock:间隙锁,锁定范围,不包括记录本身
  • Next-key Lock:Record Lock+Gap Lock,锁定范围,包括记录本身

下面我们举很多例子来说明这三种情况:
隔离级别是可重复读,user表如下:
id为自增主键,name是唯一索引,age是普通索引
在这里插入图片描述

例1:使用主键精确查询

事务1begin
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:使用主键范围查找

事务1begin
select * from user where id <7 for update
事务2:
select * from user where id = 135 for update 阻塞
select * from user where id = 246 for update 不阻塞,返回空记录
insert  user (id,name,sex,age) values(246,'sd','男',43) 阻塞,这里阻塞得仔细想一想,如果不阻塞的话,插入成功,
事务1再去查找id<7的记录就会出现幻读,所以说innodb可重复读解决了幻读问题,如果是已提交读隔离级别这里则不会阻塞

例3:使用唯一索引 like查找

事务1begin
select * from user where name like 'a%' for update
事务2:
select * from user where id = 13 for update 阻塞
select * from user where id = 246 for update 不阻塞,返回空记录
select * from user where id = 57911 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:使用唯一索引 ,但是索引失效

事务1begin
select * from user where name > 'b' for update
此时索引失效,转为全表扫描
事务2select * from user where id=1357911 for update 阻塞
执行任何插入语句也插入不了

例5:使用普通索引

事务1begin
select * from user where age = 30 for update

事务2select * 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) 阻塞 (包括203040)
这里就是范围锁了,为age=30的两端间隙20-3030-40设置间隙锁,加上三条记录锁就是范围锁,
其实这里我想不太通为什么要设置范围锁,其实我觉得只要设置不允许插入age=30的数据就好了啊,
但是我实际测试出来就是设置了范围锁,知道的可以告诉一下我

锁的问题

丢失更新(ABA)

有如下场景:用户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语句执行顺序按照前面的数字,越小的越先执行

避免死锁

  • 以固定的顺序访问表和行
  • 大事务拆小,大事务更容易产生死锁
  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率
  • 降低隔离级别(下下签)
  • 为表添加合理的索引
发布了164 篇原创文章 · 获赞 81 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/LiuRenyou/article/details/105460770