今天饲养员又来和大家一起学习MySQL的知识了。锁作为数据库区别于文件系统的关键特性,提供了保证共享资源并发访问的正确性和有效性。同时锁作为面试的老大难,其复杂程度就不用多说了。下面我们就来了解一下InnoDB有哪些锁,了解InnoDB的加锁过程。这不仅可以避免我们写出死锁的代码,也可以帮助我们从代码的角度实现加锁的优化。
MySQL 锁的分类
先来介绍一下MySQL有哪些锁,根据锁的范围来区分,可以分为:全局锁,表级锁,行级锁(InnoDB实现)。
一、全局锁
MySQL 提供了一个加全局读锁的方式:
# 加全局读锁
FLUSH TABLES WITH READ LOCK;
# 释放全局锁
UNLOCK TABLES;FTWRL 这个命令会使数据库进入只读状态,常常用来做全库备份。InnoDB引擎支持MVCC,在使用mysqldump工具执行备份的时候加上 –single-transaction 参数,可以启动一个单独的事务,获取到一个一致性视图,所以在备份的时候可以不用加全局锁。
但是针对非事务引擎,还是要加锁来保证备份的一致性。
为什么用 FTWRL 而不是设置全局只读?
你可能会问,设置只读为啥要加锁?直接执行下面的命令不是一样的嘛?
# 设置全局只读
SET GLOBAL readonly=TRUE;虽然readonly也可以使数据库进入只读状态,但是我还是建议你通过使用FTWRL。主要是以下两个原因:
- 很多时候只读状态都用来判断库是从库还是主库,所以修改只读状态影响很大。
- FTWRL在客户端断开连接后会自动释放锁,设置只读不会自动设置为false,所以在异常情况下,FTWRL具有更高的容错性。
二、表级锁
表锁
表锁可以通过以下命令进行加锁和释放,也可以在客户端断开的时候自动释放。
# 加读锁 - 允许当前线程读,限制其他线程读写
LOCK TABLE T READ;
# 加写锁 - 允许当前线程写,限制其他线程读写
LOCK TABLE T WRITE;
# 释放锁
UNLOCK TABLES;对于像MyISAM这类不支持事务的存储引擎,只支持表锁。表锁是它们最常用于处理并发的方式。通过对表加锁,保证在同一时刻,只允许一个会话对表进行更新操作。
表锁的优缺点:
- 优点:加锁在一开始就可以完成,不会发生死锁
- 缺点:并发度太低
像MyISAM这类引擎更适合用于只读、多读或单用户的场景。InnoDB引擎也支持表锁,但请避免使用LOCK TABLE语句,因为它实现了更细粒度的行锁,提供了更为精准的并发控制,性能更好。
Metadata Locking(元数据锁)
Metadata Locking作为server端实现的锁,是MySQL用来管理对数据库对象的并发访问,确保数据一致性。
说人话就是为了防止表结构变更和增删改查之间并发导致数据不一致问题,例如事务A正在更新表,事务B直接删了表的一列,那事务A肯定无法提交了。
它不仅适用于表,而且也适用于schemas、存储过程等。它不需要显式使用,在访问一个表的时候会被自动加上。
MDL 加锁规则
- 语句逐个(one by one)获取元数据锁,不是同时获取,并在获取过程中执行死锁检测
- DML(增删改查)语句按照语句中提到表的顺序获取读锁
- DDL(变更表结构)、LOCK TABLES 和其他类似的语句按名称顺序获取写锁
- 写锁请求的优先级高于读锁请求
MDL 什么时候释放?
MDL会在事务结束之后才释放,也就是说事务执行期间会一直持有MDL。
针对非事务引擎,mysql的autocommit会把每个语句都视为一个事务,所以事务结束就是每条语句执行结束。
小表加字段也会崩?
因为MDL是在事务结束后才会释放,那么在数据库具有长事务未提交的时候,执行变更表结构就需要格外小心。
典型场景:
| 会话1 | 会话2 | 会话3 |
|---|---|---|
| begin; | ||
| select * from T; | ||
| alter table T add column c; | ||
| select * from T; |
在这个例子中,会话1持有MDL读锁,会话2需要获取MDL写锁,会话3需要获取MDL读锁。由于写锁优先级高于读锁,会话2会被阻塞,等待会话1释放。但会话3的读锁请求也会被会话2阻塞,这就导致整个表都被堵住了。
解决方案:
- 在业务低峰期执行DDL操作
- 使用
ALTER TABLE ... ALGORITHM=INPLACE来减少表锁时间 - 使用
pt-online-schema-change工具
AUTO-INC Lock
自增锁是一种特殊的表级锁,当有事务向包含自增列的表中插入数据时,会获取AUTO-INC锁。
AUTO-INC 锁是一种特殊的表锁,用于在使用自增列时保证自增值的连续性。在MySQL 5.1之前,AUTO-INC锁是语句级别的,也就是说在语句执行过程中一直持有。在MySQL 5.1及之后,可以通过参数 innodb_autoinc_lock_mode 来调整锁的模式。
三、InnoDB 行级锁
InnoDB 支持行级锁,这是它能够支持高并发访问的关键。行级锁只锁定需要修改的行,而不是整张表,因此大大提高了并发性能。
行锁的类型
1. 共享锁(Shared Locks,S锁)
SELECT * FROM table_name WHERE id = 1 LOCK IN SHARE MODE;共享锁允许其他事务也获取共享锁,但阻止其他事务获取排他锁。
2. 排他锁(Exclusive Locks,X锁)
SELECT * FROM table_name WHERE id = 1 FOR UPDATE;排他锁阻止其他事务获取任何类型的锁(共享锁或排他锁)。
意向锁
InnoDB 为了支持在不同粒度上加锁,引入了意向锁的概念。意向锁是表级锁,用于指示事务打算在表的哪一行上加锁。
- 意向共享锁(IS):事务打算在表的某一行上加共享锁
- 意向排他锁(IX):事务打算在表的某一行上加排他锁
意向锁的兼容性:
| IS | IX | S | X | |
|---|---|---|---|---|
| IS | 兼容 | 兼容 | 兼容 | 冲突 |
| IX | 兼容 | 兼容 | 冲突 | 冲突 |
| S | 兼容 | 冲突 | 兼容 | 冲突 |
| X | 冲突 | 冲突 | 冲突 | 冲突 |
记录锁
记录锁锁定索引记录,而不是记录本身。即使表没有定义索引,InnoDB 也会创建一个隐藏的聚簇索引,并使用这个索引来锁定记录。
间隙锁
间隙锁锁定索引记录之间的间隙,或者锁定第一条索引记录之前或最后一条索引记录之后的间隙。间隙锁的目的是防止其他事务向间隙中插入新记录,从而导致幻读。
临键锁(Next-Key Lock)
临键锁是记录锁和间隙锁的组合,锁定一个记录以及该记录之前的间隙。临键锁是 InnoDB 默认的行锁形式,可以有效地防止幻读。
四、MVCC(多版本并发控制)
InnoDB 通过 MVCC 来实现高并发下的读写不冲突。MVCC 的核心思想是:
- 版本号:每次修改数据时,都会生成一个新的数据版本
- undo log:保存历史版本的数据
- Read View:查询时根据事务 ID 和版本号决定读取哪个版本的数据
快照读 vs 当前读
- 快照读:读取历史版本的数据,不加锁
- 当前读:读取最新版本的数据,需要加锁(
SELECT ... FOR UPDATE,SELECT ... LOCK IN SHARE MODE)
五、死锁与避免
死锁的产生
死锁是指两个或多个事务在等待对方持有的锁,导致所有事务都无法继续执行。
死锁的检测
InnoDB 有死锁检测机制,当检测到死锁时,会选择一个事务回滚,让其他事务继续执行。
死锁的避免策略
- 固定的加锁顺序:所有事务都按照相同的顺序获取锁
- 一次性获取所有锁:减少锁持有的时间
- 使用较低的隔离级别:读已提交(READ COMMITTED)级别可以减少死锁
- 设置锁等待超时:
innodb_lock_wait_timeout
总结
MySQL InnoDB 的锁机制是理解数据库并发控制的关键。通过合理使用不同类型的锁,我们可以在保证数据一致性的同时,最大化并发性能。
关键要点:
- 全局锁主要用于全库备份,InnoDB 可以使用 MVCC 避免全局锁
- 表级锁包括表锁、元数据锁和自增锁
- InnoDB 的行级锁包括记录锁、间隙锁和临键锁
- MVCC 通过版本控制实现读写不冲突
- 了解死锁产生的原因和避免策略
参考资料
- 《MySQL 技术内幕:InnoDB 存储引擎》
- 《高性能 MySQL》
- MySQL 官方文档 - InnoDB Locking