| MySQL 锁 | 分类 | 描述 | ||
| 按锁粒度 | 全局锁 | 对库进行上锁,锁的是整个database,由MySQL Server层实现; | ||
| 表级锁 | 锁的是表table,由 MySQL Server 层实现;MySQL MyISAM 、MEMORY 存储引擎使用表锁; | |||
| 行级锁 | 锁的是表中的某行数据,也能锁定行之间的间隙;MySQL InnoDB 存储引擎使用行锁,也支持表锁; | |||
| 页面锁 | 页面锁,比如 MySQL BDB存储引擎采用的是页面锁,也支持表锁;基本上被InnoDB存储引擎取带; | |||
| 按锁功能 | 表共享读锁 | 读锁共享,并且可以上多次读锁; | ||
| 表独占写锁 | 只要上了写锁,不能再上读 或写锁; | |||
| 按锁实现方式 | 悲观锁 | 表级锁 (MyISAM) | 表锁(手动) | 加读锁后,可以加再加读锁,不能加写锁; 加写锁后,不能再加读锁,也不能再加写锁; |
| 元数据锁(自动) | CURD(create/update/read/delete)加读锁,DDL加写锁 | |||
| 意向锁(innoDB) | MySQL内部使用,表共享读锁、表独占写锁 | |||
| 表锁特点 | 开锁小,加锁快,不会出现死锁,锁粒度大,发生锁冲突率高,并发度低,适合以检索查询为主,少量按索引更新数据的应用场景; | |||
| MyISAM引擎引擎的读写锁互斥,并且写锁优先级比读锁高,即使读锁请求先到达等待队列,写锁请求达到后, 也会插入到读锁的前面。这也是MyISAM存储引擎不适合写多读少的应用的原因,不然如果写一直有,读将会阻塞;但可能参数设置读优先或者写优先; | ||||
| 行级锁(innoDB) | 表共享读锁(手动) | 如:select ... lock in share mode | ||
| 表独占写锁(自动) | 如:DML(insert/update/delete),select ... for update | |||
| 行锁特点 | 开锁大,加锁慢,会出现死锁,锁粒度最小,发生冲突概率要低,并发度高;目前广泛应用; | |||
| 乐观锁 | 一般是由程序实现,比如根据版本号或者时间戳来实现的。 |
表锁是由MySQL Server层实现的,可能通过show status like 'table%'; 来查看表的状态,show open tables; 查看表锁情况;

Table_locks_waited,指的就是存在多少张表阻塞等待中;
MyISAM 存储引擎写锁测试
| 操作顺序 | session A | session b | 备注 |
| 1 | 1. lock table test write; 2. select * from test; 3. insert into test(name,age)values("lisi",50); | 在SessionA中锁表,查,插入均成功; | |
| 2 | 1. select * from test; | 在Seesion B中查询阻塞; | |
| 3 | 4. unlock tables; | 阻塞 | |
| 4 | sessionB中的,1步骤成功返回; |
操作过程如下

其实在写锁过程中,也不能对其它锁进行操作,类似下面读锁演示的读其它表;
MyISAM 存储引擎读锁测试
| 操作顺序 | session A | session b | 备注 |
| 1 | 1. lock table test read; 2. select * from test limit 1; | session A加读锁 | |
| 2 | 1. select * from test limit 1; | session 可以读; | |
| 3 | 3. select * from test2 limit 1; 报错; 4. update test set name='k8svip' where id = 1; 报错 | 2. select * from test2 limit 1; | 在read锁表session A中,只可读自己的锁定的表,不能读其它表,并且也不能更新锁定的读表;session B中可以任意读; |
| 4 | 3. update test set name='k8svip' where id = 1; | 阻塞中,表在sesson A中处于锁定状态,其它session 是无法正行写操作的; | |
| 5 | 5. unlock tables; | 继续阻塞 | |
| 6 | 4. 完成第3步中的更新操作; |

元数据锁
元数据锁是在MySQL 5.5 引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁,当要对表做结构变更操作的时候,加MDL写锁,MDL元数据锁不能显式的使用,它在访问一个表的时候会被自动加上。
MDL元数据锁的作用是,保证读写的正确性,你可以想象一下,如果一个查询正在遍历一个表中的数据,而此时进行表结构的修改,增加或者删除一列,查询线程拿到的结果跟表结构对不上,肯定是不行的。
读锁之间不互斥,因此你可以有多个线程同时对一张表进行查看。
读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
演示说明,由于查询语句查询完成后,就结束了,也就是说MDL读锁就被释放掉了,为了构造演示环境,可以先开启一个事务,在开启一个事务之后,如果不人为的结束一个事务,事务里面的SQL语句,比如select时,他会一直持有MDL的读锁,直到你 commit 或者 rollback 事务结束时,才释放MDL锁;
| 操作顺序 | session A | session B | 备注 |
| 1 | 1. begin; 或者 start transaction 2. select * from test2; | ||
| 2 |
| Session B 步骤1select 正常,但修改元数据时会阻塞。 | |
| 3 | 3. commit; 或者rollback | ||
| 4 | 在SessionA commit后,Session B 阻塞消失,修改成功; |

行锁
MySQL 行锁,是利用存储引擎锁住索引项来实现,由于是根据索引项来实现行锁的,那么意味着,只有通过索引条件检索的数据,InnoDB才使用行级锁,否则InnoDB将使用表锁,这里主要总结 InnoDB 存储引擎的行级锁,查看行级锁使用状态show status like 'innodb_row_lock%';

当Innodb_row_lock_waits 等待次数很高,而且每次Innodb_row_lock_time也很长的时候,就需要分析为什么会有如何多的等待,然后根据分析结果进行优化。
对于update、delete和insert 语句,InnoDB会自动给涉及数据集加排他锁(x),而对于普通的select 语句,InnoDB不会加任何锁,事务可以通过以下语句显示的给记录集添加共享锁和排他锁;
| InnoDB锁 | 分类 | 小类别 | 说明 |
| 行锁 | 按锁定范围 | 记录锁或叫行锁 | 锁定索引中的一条记录; |
| 间隙锁 | 要么锁住索引记录中间的值,要么锁住第一个索引记录前面的值或者最后一个索引记录后面的值。 | ||
| Next-Key Locks | 它是索引记录上的记录锁和在索引记录之前的间隙锁的组合;它是记录锁和间隙锁的总称。 | ||
| 按功能 | 共享读(S) | 允许一个事务去读一行,阻止其他事务获得相同数据集的排他写;一个事务读时,其它事务不能写;共享读,指的是加上select * from table_name lock in share mode; | |
| 排他写(X) | 允许获得排他写锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁(不是读)和排他写锁;排他写,指的是加上select * from table_name where ... for update; | ||
| 表锁 | 又称意向锁,MySQL内部实现,不需要用户干预 | 意向共享锁(IS) | 事务打算给数据行加行共享锁(S),事务在给一个数据行加共享锁前必须先取得该表的IS锁; |
| 意向排他锁(IX) | 事务打算给数据行加行排他锁(X),事务在给一个数据行加排他锁前必须先取得该表的IX锁; |
意向锁和行锁可以共享,意向锁的主要作用是为了全表更新数据时的性能提升,否则全表更新数据时,需要先检索该表是否某些记录上面有行锁;
| 共享锁 S | 排他锁 X | 意向共享锁 IS | 意向排他锁 IX | |
| 共享锁 S | 兼容 | 冲突 | 兼容 | 冲突 |
| 排他锁 X | 冲突 | 冲突 | 冲突 | 冲突 |
| 意向共享锁 IS | 兼容 | 冲突 | 兼容 | 兼容 |
| 意向排他锁 IX | 冲突 | 冲突 | 兼容 | 兼容 |
行读锁演示
| 操作顺序 | session A | session B | 备注 |
| 1 | 1. begin; 2. select * from test3 where id = 1 lock in share mode; | session A 中加行读锁; | |
| 2 | 1.update test3 set name = "kkvv" where id=2; 2. update test3 set name = "kkvv1" where id=1; | Session B阻塞中 1,是可以修改的;2 处于阻塞中,超过一定时长,会报错; | |
| 3 | 3. commit; | ||
| 4 | 在SessionA commit后,Session B 阻塞消失,修改成功; |
行读锁示例

行读锁升级为表锁
| 操作顺序 | session A | session B | 备注 |
| 1 | 1. begin; 2. select * from test3 where name="kkvv" lock in share mode; | session A 在加读锁,未使用索引; | |
| 2 | 1.update test3 set name="k8svipvip" where id=1; | Session B 更新其它字段也不可以,因为在session A中 where 条件中没有使用索引,此时一旦加锁,就变为了表锁,所以导致session B中,即使更新其它字段也不可以的原因。 | |
| 3 | 3. commit; | ||
| 4 | 在SessionA commit后,Session B 阻塞消失,修改成功; |
行读锁升级为表锁示例

行写锁
| 操作顺序 | session A | session B | 备注 |
| 1 | 1. begin; 2. select * from test3 where id=1 for update; | session A加行写锁,注意这里索引是主键,行记录锁; | |
| 2 | 1. select * from test3 where id=2; 2. select * from test3 where id=1; 3. select * from test3 where id=1 lock in share mode; | sessionB 中 1,2 都不会阻塞,只有在lock in share mode共享锁时,才会阻塞; | |
| 3 | 3. commit; | ||
| 4 | 在SessionA commit后,Session B 阻塞消失,修改成功; |
行写锁示例

间隙锁
间隙锁产生于 MySQL 事务隔离级别的 RR(默认隔离级别),间隙锁锁的封。遇到有一个非唯一索引,我们对表建立一个普通索引,如下所示:

在非唯一索引加行写锁产生间隙锁案例
| 操作顺序 | session A | session B | session C | 备注 |
| 1 | 1. begin; 2. select * from test3 where age = 19 for update; | session A,在非唯一索引上面加行写锁 | ||
| 2 | 1. begin; 2.insert into test3 values(21,"k8s",37,1); 3. insert into test3 values(2,"k8s",17,1); | sessionB 中 2 都不会阻塞,因为不在间隙锁内,3为阻塞,因为在间隙锁内; | ||
| 3 | 1. insert into test3 values(4,"k8s",18,1); | 不管是否开启类型Session B事务,都会阻塞; | ||
| 4 | 3. commit; | |||
| 5 | 在SessionA commit后,Session B 阻塞消失,插入成功; |

1. 我们锁定的是 id为3 age 为19行的上面间隙;即ID为 1与3之间,3与5之间,age为17与19,19与20之间,注意以上例子中锁定的是age从17到19,19到20;
2. 想要插入的数据同时满足上面17<<age<<20间隙时,是不可以插入进去的;
3. 如 session B 中 insert into test3 values(21,"k8s",37,1); 在间隙的外面,是可以插入的,不会阻塞;
4. session B 中的 insert into test3 values(2,"k8s",17,1); 在间隙内,是不可以插入的,都会阻塞,超时会失败;
5. 同理,在Session C 中,insert into test3 values(4,"k8s",18,1); 也在间隙内,也是不可以插入的,都会阻塞,超时会失败;

以上实验是非唯一索引时,加行写锁时,会产生间隙锁 ;
下面间隙锁失败

如上图中的间隙锁就不生效了,我们本意加间隙锁ID 5到8,8到13,age 20-16,16到26;由于age没有20-->16的,间隙锁不生效;
在主键索引加行写锁产生间隙锁案例
| 操作顺序 | session A | session B | session C | 备注 |
| 1 | 1. begin; 2. select * from test3 where id >2 and id < 13 for update; | session A,在主键索引上面加行写锁时,该区间内不能再插入数据; | ||
| 2 | 1. begin; 2.insert into test3 values(7,"k8s",19,1); | sessionB 中 阻塞中 | ||
| 3 | 1. insert into test3 values(10,"k8s",5,1); | session C 也是阻塞中; | ||
| 4 | 3. commit; | 3 commit; | ||
| 5 | 在SessionA commit后,Session B 阻塞消失,插入成功; |
间隙锁用例

当索引的间隙锁值不存在时怎么办?
| 操作顺序 | session A | session B | session C | 备注 |
| 1 | 1. begin; 2.select * from test3 where age=50 for update; | session A中没有这个非唯一索引时,会如何呢? | ||
| 2 | 1. begin; 2. insert into test3 values(25,"k8s",6,1); 3. insert into test3 values(26,"k8s",45,1); | age大于41的都会阻塞,小于41的都可以插入成功; 距离最近的age的数值做为间隙锁的左起点,右边无穷大 41<<age<<∞ 都会阻塞 | ||
| 3 | 1. insert into test3 values(27,"k8s",51,1); | 也会阻塞 | ||
| 4 | 3. commit; | |||
| 5 | 在SessionA commit后,Session B 阻塞消失,插入成功; | 在SessionA commit后,Session C 阻塞消失,插入成功; |
间隙锁值不存在示例

两阶段锁
传统RDBMS加锁的一个原则,就是2PL(Two-Phase Locking,二阶段锁)。锁的操作分为两个阶段,加锁和解锁阶段,并且保证加锁阶段和解锁阶段不相交。加锁阶段:只加锁,不放锁;解锁阶段:中放锁,不加锁;例如在事务操作中:

死锁
两个或多个Session 互相等待对方的资源释放后,才能释放自己的资源,造成了死锁,我等你,你等我的状况,它是因为顺序造成的死锁现象;
| 操作顺序 | session A | session B | 备注 |
| 1 | 1. begin; 3.update test3 set name="abc" where id =1; 5. update test3 set name="abc" where id =24; | session A 、B 中开启事务; session A中更新id=1,session B中更新id=24,此时都没有问题; session A中更新id=24时,就会阻塞,当session B中再去更新id=1时,MySQL自动检测出死锁状态,造成死锁的语句直接崩溃;session A执行成功; | |
| 2 | 2. begin; 4. update test3 set name="bdc" where id=24; 6. update test3 set name="bdc" where id=1; |
死锁示例

总结
详细总结了 MyISAM 存储引擎表读锁、表写锁、元数据锁是怎么回事,InnoDB的行读锁、行读锁在什么情况下升级为表锁,行写锁、间隙锁、间隙锁在什么情况下会失败、当索引的间隙锁值不存在时怎么办?行锁解决了并发问题,间隙锁和行锁如何解决了幻读问题,请查看:MySQL 事务。
您的关注是我写作的动力
文章推荐
讲讲 tcp_tw_recycle,tcp_tw_reuse
基础小知识
专辑分享
kubeadm使用外部etcd部署kubernetes v1.17.3 高可用集群
第一篇 使用 Prometheus 监控 Kubernetes 集群理论篇
参考:《高性能MySQL》





