InnoDB锁类型主要包括读锁(共享锁)、写锁(排它锁)、意向锁和MDL锁,从锁粒度上来说,又包括全局锁,表锁和行锁。

1、全局锁

全局锁就是对整个数据库实例加锁

MySQL 提供了一个加全局读锁的方法,命令是Flush tables with read lock(FTWRL),会让整个库处于只读状态。


全局锁的典型使用场景是,做全库逻辑备份:
通过 FTWRL 确保不会有其他线程对数据库做更新,然后对整个库做备份

FTWRL的问题:

  • 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆;
  • 如果在从库上备份,那么备份期间从库不能执行主库同步过来的 binlog,会导致主从延迟

对于使用事务引擎的库一般不用FTWRL而是使用一致性读(事务利用MVCC进行的读取操作,通过ReadView忽略生成ReadView后对数据的更新操作)


为什么不使用 set global readonly=true 的方式设置全库只读?

  • 在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库
  • 在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。

    2、InnoDB表级别锁

2.1、共享锁(S锁)和独占锁(X锁)

给表加S锁:

  • 别的事务可以获取该表的S锁,但是不可以获取该表的X锁
  • 别的事务可以获取表中某些记录的S锁,但是不可以获取表中某些记录的X锁

给表加X锁:

  • 别的事务不可以获取该表的S锁和X锁
  • 别的事务不可以获取该表中某些记录的S锁和X锁

**LOCK TABLES t READ**:InnoDB引擎对表t加表级别的S锁
**LOCK TABLES t WRITE**:InnoDB引擎对表t加表级别的X锁

2.2、意向共享锁(IS锁)和意向独占锁(IX锁)

当对使用InnoDB存储引擎的表的某条记录加S锁时,需要先在表级别加一个IS锁
当对使用InnoDB存储引擎的表的某条记录加X锁时,需要先在表级别加一个IX锁

IS锁和IX锁的使命只是为了后续加表级别的S锁和X锁时,判断表中是否有已经被加锁的记录,以避免使用遍历的方式查看表中有没有上锁记录

2.3、MDL锁

MDL 不需要显式使用,在访问一个表的时候会被自动加上,MDL 的作用是,保证读写的正确性(,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上)。

在 MySQL 5.5 版本中引入了 MDL,其加锁规则为:
当对一个表做增删改查操作的时候,加 MDL 读锁
当要对表做结构变更操作的时候,加 MDL 写锁

MDL作用是防止DDL和DML并发的冲突

在对一个表执行DDL语句时,其它事务对这个表的增删改查操作会发生阻塞
在一个事务对表进行增删改查时,执行DDL语句发生阻塞
多个线程进行DDL操作时,需要串行执行


对线上的热点表加字段MDL锁带来的问题

image.png

session A 先启动,这时候会对表 t 加一个 MDL 读锁,由于 session B 需要的也是 MDL 读锁,因此可以正常执行,之后 session C 会被 blocked,是因为 session A 的 MDL 读锁还没有释放,而 session C 需要 MDL 写锁,因此只能被阻塞。
如果只有 session C 自己被阻塞还没什么关系,但是之后所有要在表 t 上新申请 MDL 读锁的请求也会被 session C 阻塞。所有对表的增删改查操作都需要先申请 MDL 读锁,等于这个表现在完全不可读写了。如果某个表上的查询语句频繁,而且客户端有重试机制,也就是说超时后会再起一个新 session 再请求的话,这个库的线程很快就会爆满。


思考如何安全的给一个线上的热点表加字段?

1、解决长事务问题
事务不提交,就会一直占着 MDL 锁。如果DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务

2、在 alter table 语句里面设定等待时间
如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃,之后开发人员或者 DBA 再通过重试命令重复这个过程。

  1. ALTER TABLE tbl_name NOWAIT add column ...
  2. ALTER TABLE tbl_name WAIT N add column ...

3、InnoDB行级别锁

3.1、单个行记录锁 Record Lock

单个行记录锁 Record Lock是有读锁和写锁之分的:

  • 共享锁(Shared Lock):简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁
  • 独占锁(Exclusive Lock):也称排它锁,简称X锁。在事务需要修改一条记录时,需要先获取记录的X锁。

兼容关系

兼容性 X锁 S锁
X锁 不兼容 不兼容
S锁 不兼容 兼容

假设事务T1获取了一个记录的S锁,如果事务T2想要再获取该记录的S锁,那么事务T2也会获得该锁,如果事务T2想要再获取该记录的X锁,那么此操作会被阻塞,直到事务T1提交之后将S锁释放为止。
假设事务T1获取了一个记录的X锁,之后无论事务T2想要再获取该记录的S锁或者X锁,都会被阻塞。


对读取的记录加S锁:
SELECT ... LOCK IN SHARE MODE
对读取的记录加X锁:
SELECT ... FOR UPDATE


S锁的应用:
写-写情况下会发生脏写现象,这是任何一种隔离级别都不允许发生的现象

在多个未提交事务相继对同一条记录进行修改操作时,通过为该记录加锁,让它们排队执行。而锁本质是一个内存中的结构,当一个事务想对某条记录进行改动时,首先会查看内存中有没有与这条记录相关的锁结构,如果没有,就会在内存中生成一个锁结构与之关联。

点击查看【processon】

获取锁成功:在内存中生成了对应的锁结构,而且锁的is_waiting属性为false,表示事务可以继续执行操作
获取锁失败:在内存中生成了对应的锁结构,但是锁的is_waiting属性为true,表示事务可以等待

3.2、间隙锁Gap Lock

Mysql在可重复读级别下MVCC可以很大程度的解决幻读现象,但是不能完全解决

3.2.1、为什么可重复读级别下MVCC不能完全解决幻读?

表字段及数据情况: person,两个字段 id,name ,闲存在一条记录(1,“张三”)

发生时间 事务A 事务B
T1 BEGIN;
T2 SELECT count(*) from person where id < 5; BEGIN;
T3
INSERT person (id,name) VALUES (6,’李四’);
T4 COMMIT;
T5 update person set name = ‘李四’ where id= 2;
T6 SELECT count(*) from person where id < 5;
T7 COMMIT;

在RR隔离级别下,事务A在第一次执行select时生产了一个ReadView,之后事务B向person表插入一条记录并提交。然后事务A对事务B刚插入数据进行修改,那么这条记录的隐藏列trx_id变为了事务A的事务id,之后事务A再使用select语句查询就可以查询到这条记录了。因为这个特殊现象的存在,我们也可以认为MVCC并不能完全禁止幻读。

给一条记录加间隙锁是不允许其它事务向这条记录前面的间隙插入新纪录

3.3、Next-Key_Lock

next-key是记录锁和间隙锁的结合体,既能锁定该记录,又能防止其它事务将新记录插入到被保护记录前面的间隙中