mysql5.7的锁:乐观锁/共享锁、互斥/排他锁、意向锁、记录锁、行锁/表锁、间隙锁、临界锁、插入意向锁、自增锁、空间索引预测锁、隐式锁
从锁的模式来区分:Shared Locks共享锁(行锁)、Exclusive Locks(行锁)
意向锁区分: Intention Shared Locks意向共享锁、Intention Exclusive Locks意向排它锁
从锁的算法区分:Record Locks记录锁、Gap Locks间隙锁、Next-Key Locks临界锁
一致性读/快照读
【锁定读(Locking Reads)/LBCC】
当前读分类:select for update、select .... lock in share mode、update insert delete、串行化的级别,当前读的这种方式就称为LBCC,全称为 lock-based Concurreny Control 基于锁的并发控制。
【共享锁和独占锁】
共享锁,简称S锁,其他线程可以继续获取共享锁。
独占锁,简称X锁,只允许获取一次,不容许其他线程获取独占锁。所以像select for update就会独占线程锁死这行。
insert 一般不加锁,隐式锁
update X锁: 不更新主键的情况下,就地更新,更新主键的情况下。
X锁: 更新主键的情况下,先执行delete操作, (隐式锁:)再执行insert操作。
delete X锁
组合型 | X 独占锁 | IX 意向独占锁 | S 共享锁 | IS 意向共享锁 |
表锁 | 有 | 有 | 有 | 有 |
行锁 | 有 | -- | 有 | -- |
表锁级别的兼容性,比如X独占锁与X独占锁就不兼容:
兼容性 | X 独占锁 | IX 意向独占锁 | S 共享锁 | IS 意向共享锁 |
X 独占锁 | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
IX 意向独占锁 | 不兼容 | 兼容 | 不兼容 | 兼容 |
S 共享锁 | 不兼容 | 不兼容 | 兼容 | 兼容 |
IS 意向共享锁 | 不兼容 | 兼容 | 兼容 | 兼容 |
怎么给表加锁?
Lock table t Read; 表级别S锁
Lock table t Write; 表级别X锁
而innodb已经有行锁了,所以以上这种手动加锁没太大意义,除非崩溃时恢复数据的特殊时期。
InnoDB中的表级锁
1、表级别的S锁、X锁、IS锁、IX锁、
2、元数据锁(Metadata Locks, 简称MDL)Server
3、AUTO-INC锁,自增主键锁,解决主键自增列避免产生重复自增主键的锁
InnoDB中的行级锁
1、Record Locks(官方名称是:Lock_Rec_Not_Gap) 记录锁
2、Gap Locks(官方名称:Lock_Gap) 间隙锁,对某条记录的前后,也就是两条记录之间的间隙加锁,不对行本身加锁。
3、Next-Key Locks(官方名称:Lock_ORDINARY) 临界锁
4、Insert Intention Locks 插入意向锁
5、隐式锁
【间隙锁例子】
假设我在teacher表中为name这一列建立了一个索引idx_name:
事务1 | 事务2 |
select * from teacher order by name; -- 按照idx_name列的索引排序,查出来的数据是这样的: |
|
begin; -- 开启事务 | |
update teacher set domain = 'Spring' where name = 'James'; -- 这行id是9,此时事务尚未提交,mysql会对id=9的行前后都加间隙锁,不允许此行前后插入数据。 | |
begin; -- 开启事务 | |
insert into teacher values(22, 'Jahes', 'docker'); -- 在name='James'行的后面添加新行,此sql会阻塞住, insert失败,因为事务1的间隙锁已开启。如果长时间阻塞住(也就是事务1长时间不提交),会报错:ERROR 1205(HY000): Lock wait timeout exceeded; try restarting trasaction; | |
inset into teacher values(7, 'King', 'docker'); -- 根据idx_name索引列的B+tree排序规则,由于King值已存在,所以需要将同name值的id进行排序,此insert语句中的id 7 要排到id=15的前面去,刚好落在间隙锁范围,同样阻塞住,insert失败。 | |
insert into teacher values(70,'King','docker'); -- 此时id 70落在id=15的后面,避开了间隙锁,insert成功。 | |
【临界锁例子】
临界锁就是记录锁和间隙锁的合体,也就是不仅像记录锁一样锁住行本身,还像间隙锁一样锁住了行的前后间隙,不允许insert。
那么可重复读的事务隔离级别下,innodb引擎就是使用了临界锁进行搜索和索引扫描,来避免幻读的情况。
【插入意向锁例子】
根据间隙锁的例子,在事务2中insert数据需要别的事务进行提交或回滚才允许事务2insert的情况,那么开启一个插入意向锁,类似于排队机制。
【隐式锁例子】
一个事务可以不显式的去加锁,但是别的事务在对新加的记录准备加X或S锁的时候,首先会去检查trx_id这个隐藏列,然后进行相关的锁和事务判断(这里参考下面的锁的内存结构)。
假如A事务先insert一条新记录,然后B事务去对这个新记录进行S或X锁的时候,B事务会帮助A事务在内存中生成锁结构,B事务自己再生成一个锁结构,然后把自己置为等待状态。
隐式锁的好处是延迟锁的生成,如果一个insert语句是不需要锁的,但是如果多条sql插入相同id,那么隐式锁的存在就发挥了作用,就像并发编程里的偏向锁升级成了轻量锁。
【锁的内存结构】
每条数据记录的锁结构都包含两个信息:事务信息 = t1; 是否等待 = false; false表示该锁是当前事务创建的,当前事务对获取该锁不需要等待。
当事务1提交之后,事务1会去唤醒等待该行数据记录的其他锁,相应的其他锁结构也包括两个信息:事务信息 = 2;是否等待 = true; 依次唤醒所有锁和事务。
如果是独占锁,那么一次唤醒一个,如果是共享锁,那么一次可以唤醒多个。
同一个事务,针对同一个数据页面,同一个加锁类型,才会公用一把锁,这三者只要任何一个不同,都会生成一把新的锁,但是锁结构相同。
【锁的开启和查看】
show variables like 'innodb_status_output_locks'; -- 默认是OFF关闭状态,需要开启: set global innodb_status_output_locks = ON;
show engine innodb status\G -- 查看innodb加锁的情况就会变得非常非常详细:
如果你的sql中加锁很慢的情况,就可以用这种方法来判断事务之间是否出现了严重的锁争抢情况。
【死锁】
一般来说只要有并发和死锁的共同加持情况,都会有死锁的身影,两个或两个以上的进程在执行过程中,由于竞争资源或者通讯问题造成彼此阻塞现象,若无外力作用而无法推进解决,此时称为死锁或者死锁状态。
产生死锁的必要条件:
1)互斥。进程对于分配给他的资源是排他性使用。
2)请求和保持。我已经通过请求拿到了一个资源,但是我又提出了一个新资源的请求 ,但是更多的资源我拿不到,同时对于我自己已有的资源也不放手,所以这叫请求与保持。
3)不剥夺。对于进程已经持有的资源在使用完之前不剥夺。
4)环路等待。
死锁例子:
事务1 | 事务2 |
begin; -- 开启一个事务 | |
select * from teacher where number = 1 for update; -- 加了一个行上的X锁 | |
begin; | |
select * from teacher where number = 3 for update; -- 加了一个行上的X锁 | |
select * from teacher where number = 3 for update; -- 此时会阻塞,因为锁被事务2抢走 |
|
select * from teacher where number = 1 for update; -- 尝试加锁,但是失败:发生死锁,会报错: ERROR 1213 (40001) : Deadlock found when trying to get lock; try restarting trasaction 发生死锁之后,mysql自动介入结束当前事务 |
|
此时上面的 3 for update会执行下去了,不再阻塞,因为事务2已经结束了。 | |
commit; -- 此时rollback 或者 commit都可以结束事务。 | |
show engine innodb status\G -- 此时将会看到最近一个死锁产生的情况 |
end.