数据库作为多用户共享的资源中心,总是存在着竞争条件,显然,加锁是最为简单的一种保证竞争条件安全性的措施。
那么,mysql 锁是如何实现的,又有哪些分类?本文将为您详细讲述。
mysql 中的锁可以按照多个维度进行分类。
按照实现和工作原理,mysql 的锁可以分为:
也就是说,无论是我们通常称的行锁还是表锁,最终实际上无外乎都是加的上面这几种锁,从而实现不同的功能。
按照锁定范围,mysql 的锁可以分为:
行级锁又可以进一步细分为:
持有同一个共享锁的多个进程可以同时进入保护空间,这就是共享锁命名的来源,因为他们可以共享被锁定的资源,他通常在读取数据前加锁,以实现多个对数据的读取进程可以相互并发执行不被阻塞,因此也常被称为“读锁”。
虽然共享锁被称为“读锁”,但实际上在可重复读级别下,innodb 通过 MVCC 机制实现了无需加锁即可以避免读写冲突,所以在可重复读的级别下,普通的读取是不加锁的,但 select … lock in share mode 会在行上加共享锁。
排它锁与共享锁不同,一旦加了排它锁,其他任何加锁请求都会被阻塞,排它锁通常用于写数据前加锁,以便让各个写操作之间保持互斥,因此也被成为“写锁”。
特殊的,select … for update 会在行上加排它锁。
意向锁分为意向共享锁和意向排它锁。
那么意向锁和普通的读写锁有什么区别呢?
考虑一个事务通过 select … lock in share mode 对某一行加了共享锁,此时另一个事务要对这一行加排它锁,我们知道,第二个事务会进入阻塞等待,但如果一个事务准备给全表加排它锁呢?显然,他需要遍历全表中的所有记录,查看每一条记录的加锁状态,才能决定是否能够加锁成功,这显然是效率很低的。
解决办法很简单,我们只需要在对某一行加锁前,将整个表标记为“某些行已经加了共享锁”的状态,那么另一个事务对于整个表的加锁操作就不需要像我们前面所说的那样去遍历每一行了。
意向锁就是我们这里说的“某些行已经加了锁”的状态标识,所有的共享锁加锁前都要对表加意向共享锁,排它锁加锁前,都要对表加意向排它锁,而意向锁之间不互斥。
有了上面我们描述的这段话,就可以得到知道意向锁的加锁时机了:
IS | IX | 行级 S | 行级 X | 表级 S | 表级 X | |
---|---|---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 兼容 | 兼容 | 互斥 |
IX | 兼容 | 兼容 | 兼容 | 兼容 | 互斥 | 互斥 |
行级 S | 兼容 | 兼容 | 兼容 | 互斥 | 兼容 | 互斥 |
行级 X | 兼容 | 兼容 | 互斥 | 互斥 | 互斥 | 互斥 |
表级 S | 兼容 | 互斥 | 兼容 | 互斥 | 兼容 | 互斥 |
表级 X | 互斥 | 互斥 | 互斥 | 互斥 | 互斥 | 互斥 |
顾名思义,全局锁就是对整个数据库实例加锁,mysql 提供了一个全局锁,命令是:
flush tables with read lock // 加锁
unlock tables // 解锁
如果其他会话对某个表加了表锁,那么另一个会话加全局锁的请求会被阻塞,如果当前会话对某个表加了表锁,或在事务中,那么加全局锁的请求会失败:
Can’t execute the given command because you have active locked tables or an active transaction
一旦全局锁命令执行成功,会关闭当前已打开的所有表,此后,该数据库实例将会变为只读,所有对数据库的 update、insert、delete、加排它锁、表结构修改等操作都会被阻塞。
当当前连接断开时,全局锁会自动解锁。
全局锁最常用的使用场景是全库备份,假设没有全局锁,我们要备份一个账户数据库。
在我们备份用户 A 的账户后,在备份用户 B 之前,发生了 A 用户向 B 用户的转账,此时我们再备份的 B 账户余额增加了,最终,我们发现总账白白多了一部分资金,这显然是不能接受的。
也许此时你会说,上一篇文章讲过,在事务中,innodb 通过 MVCC 实现了事务中的一致性视图,所以我们只要在备份前开启一个事务,只进行快照读,可以保证读取到数据的一致性。
官方逻辑备份工具 mysqldump 提供了参数 –single-transaction 来保证整个备份过程处于一个事务中,从而实现备份过程中的一致性读。
但对于不支持事务的存储引擎,例如 MyISAM,我们只能依赖全局锁来实现备份过程中的一致性读。
事实上,我们还有另一种方法来实现让全库只读:
set global readonly = true
但通常我们不会去修改这个全局变量,主要原因有:
MySQL 中有两种表级锁:
前面我们讲到,在 mysql 中,锁的实现分为共享锁和排它锁,所以表锁有两种加锁命令:
关于共享锁与排它锁的互斥关系,可以参考上文中的关系表,此处不再赘述。
表锁也同样可以通过 unlock tables 命令来解锁。
由于 innodb 支持行锁,而表锁锁定范围过大,通常是不被使用的。
MDL 锁不需要显式使用,他也同样分为共享锁和排它锁。
所有的增删改查操作都会在执行前加 MDL 共享锁,但如果是在事务中,操作执行后并不会立即释放锁,而是要等到事务执行结束(提交或回滚)后才会释放。
而对表结构的修改,即 alter table 语句,会自动加 MDL 排它锁。
上面这些规则意味着,在 alter table 语句执行时,如果已有事务在执行,他将会进入阻塞,但此后,由于这次试图加 X 锁之前加了 IX 锁,所有的增删改查、事务开启操作都会被阻塞,这将会是一个非常严重的问题。
因此,在执行 alter table 语句时,一定要检查是否此时表上有事务或慢查询在执行。
MariaDB 为 alter table 语句添加了超时参数,来解决这个问题:
ALTER TABLE tbl_name NOWAIT add column …
ALTER TABLE tbl_name WAIT N add column …
NOWAIT 实现了非阻塞式调用,如果无法获取到 MDL 排它锁,那么会立即返回失败,而不会阻塞等待。
WAIT N 则实现了最大超时 N 秒的设定,等待 N 秒后没有获取排它锁,会返回失败。
欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤。
本文介绍了 MySQL 的全局锁、表级锁以及各种锁的基本实现,但事实上,在 innodb 引擎中,我们最为常用的锁是行级锁。
行级锁也是所有的锁中相对最为复杂的,敬请期待我们下一篇文章的讲解。