MySQL里有很是多锁的概念,常常能够听到的有:乐观锁、悲观锁、行锁、表锁、Gap锁(间隙锁)、MDL锁(元数据锁)、意向锁、读锁、写锁、共享锁、排它锁。这么锁一听就让人头大,因而去看一些博客,有些讲乐观锁、悲观锁,有些在讲读锁、写锁,因而乐观锁和悲观锁好像理解了,读锁写锁好像也理解了,可是我任然不知道怎么用,也不知道乐观锁与读锁写锁有没有什么关系?再看了不少文章后,逐渐弄懂了它们之间的关系,因而写下这篇文章来梳理思路。能力有限,不免有误,请酌情参考。html
虽然上面列举了不少锁的名词,可是这些锁其实并非在同一个维度上的,这就是我之因此含糊不清的缘由。接下来从不一样的维度来分析 MySQL 的锁。mysql
首先读锁还有一个名称叫共享锁,写锁也相应的还有个名称叫排它锁,也就是说共享锁和读锁是同一个东西,排它锁和写锁是同一个东西。读锁、写锁是系统实现层面上的锁,也是最基础的锁。读锁和写锁仍是锁的一种性质,好比行锁里,有行写锁和行读锁。MDL 锁里也有 MDL 写锁和 MDL 读锁。读锁和写锁的加锁关系以下,Y 表示能够共存,X 表示互斥。git
读锁 | 写锁 | |
---|---|---|
读锁 | Y | X |
写锁 | X | X |
从这个表格里能够知道读锁和写锁不能共存,请考虑这样一个场景,一个请求占用了读锁,这时又来了一个请求要求加写锁,可是资源已经被读锁占据,写锁阻塞。这样本没有问题,可是若是后续不断的有请求占用读锁,读锁一直没有释放,形成写锁一直等待。这样写锁就被饿死了,为了不这种状况发生,数据库作了优化,当有写锁阻塞时,后面的读锁也会阻塞,这样就避免了饿死现象的发生。后面还会再次提到这个现象。github
以前的文章已经介绍了 MySQL 的存储模型,对于 InnoDB 引擎而言,采用的是 B+ 树索引,假设须要将整个表锁住那么须要在整个 B+ 树的每一个节点上都加上锁,显然这是个很是低效的作法。所以,MySQL 提出了意向锁的概念,意向锁就是若是要在一个节点上加锁就必须在其全部的祖先节点加上意向锁。关于意向锁还有更多复杂设计,若是想了解能够查看 《数据库系统几率》 一书。算法
表锁和行锁是两种不一样加锁粒度的锁。除了表锁和行锁之外还有更大粒度的锁——全局锁。sql
全局锁: 全局锁会锁住整个数据库,MySQL 使用 flush tables with read lock 命令来加全局锁,使用 unlock tables 解锁。线程退出后锁也会自动释放。当加上全局锁之后,除了当前线程之外,其余线程的更新操做都会被阻塞,包括增删改数据表中的数据、建表、修改表结构等。全局锁的典型使用场景是全库的逻辑备份。数据库
表锁: 表锁会锁住一张表,MySQL 使用 lock tables 编程
read/write 命令给表加上读锁或写锁,经过 unlock tables 命令释放表锁。经过 lock tables t read 给表 t 加上读锁后,当前线程只能访问表 t,不能访问数据库中的其余表,对表 t 也只有读权限,不能进行修改操做。经过 lock tables t write 给表 t 加上写锁后,当前线程只能访问表 t,不能访问数据库中的其余表,对表 t 有读写权限。行锁: 行锁会锁锁住表中的某一行或者多行,MySQL 使用 lock in share mode 命令给行加读锁,用 for update 命令给行加写锁,行锁不须要显示释放,当事务被提交时,该事务中加的行锁就会被释放。经过 select k from t where k = 1 for update 命令能够锁住 k 为 1 的全部行。另外当使用 update 命令更新表数据时,会自动给命中的行加上行锁。另外 MySQL 加行锁时并非一次性把全部的行都加上锁,执行一个 update 命令以后,server 层将命令发送给 InnoDB 引擎,InnoDB 引擎找到第一条知足条件的数据,并加锁后返回给 server 层,server 层更新这条数据而后传给 InnoDB 引擎。完成这条数据的更新后,server 层再取下一条数据。segmentfault
咱们用一个例子来验证这个过程,首先执行以下命令建表并插入几行数据bash
mysql-> create table t(id int not null auto_increment, c int not null, primary key(id))ENGINE=InnoDB;
mysql-> insert into t(id, c) values (1, 1), (2, 2), (3, 3);
复制代码
事务 A | 事务 B | 事务 C |
---|---|---|
begin | ||
select * from t where id = 3 for update; | ||
update t set c = 0 where id = c; | ||
set session transaction isolation level READ UNCOMMITTED; select * from t; | ||
commit |
事务 A 执行 select * from t where id = 3 for update 将 id 等于3的行锁住,事务 B 执行 update 命令的时候被阻塞。这时候再开启事务 C,而且将事务 C 的隔离级别修改成未提交读,获得的以下表所示,发现前两行已经被更新,最后 id 为 3 的行没有更新,说明事务 B 是阻塞在这里了。
mysql> select * from t;
+----+---+
| id | c |
+----+---+
| 1 | 0 |
| 2 | 0 |
| 3 | 3 |
+----+---+
复制代码
乐观锁老是假设不会发生冲突,所以读取资源的时候不加锁,只有在更新的时候判断在整个事务期间是否有其余事务更新这个数据。若是没有其余事务更新这个数据那么本次更新成功,若是有其余事务更新本条数据,那么更新失败。
悲观锁老是假设会发生冲突,所以在读取数据时候就将数据加上锁,这样保证同时只有一个线程能更改数据。文章前面介绍的表锁、行锁等都是悲观锁。
乐观锁和悲观锁是两种不一样的加锁策略。乐观锁假设的场景是冲突少,所以适合读多写少的场景。悲观锁则正好相反,合适写多读少的场景。乐观锁无需像悲观锁那样维护锁资源,作加锁阻塞等操做,所以更加轻量化。
乐观锁的实现有两种方式:版本号和 CAS 算法
版本号
经过版本号来实现乐观锁主要有如下几个步骤:
1 给每条数据都加上一个 version 字段,表示版本号
2 开启事务后,先读取数据,并保存数据里的版本号 version1,而后作其余处理
3 最后更新的时候比较 version1 和数据库里当前的版本号是否相同。用 SQL 语句表示就是 update t set version = version + 1 where version = version1。 根据前面事务的文章咱们知道,update 操做时会进行当前读,所以即便是在可重复读的隔离级别下,也会取到到最新的版本号。若是没有其余事务更新过这条数据,那么 version 等于 version1,因而更新成功。若是有其余事务更新过这条数据,那么 version 字段的值会被增长,那么 version 不等于 version1,因而更新没有生效。
CAS 算法
CAS 是 compare and swap 的缩写,翻译为中文就是先比较而后再交换。CAS 实现的伪代码:
<< atomic >> bool cas(int* p, int old, int new) { if (*p != old) { return false } *p = new return true } 复制代码
其中,p 是要修改的变量指针,old 是修改前的旧值,new 是将要写入的新值。这段伪代码的意思就是,先比较 p 所指向的值与旧值是否相同,若是不一样说明数据已经被其余线程修改过,返回 false。若是相同则将新值赋值给 p 所指向的对象,返回 true。这整个过程是经过硬件同步原语来实现,保证整个过程是原子的。
大多数语言都实现了 CAS 函数,好比 C 语言在 GCC 实现:
bool__sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...) type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...) 复制代码
无锁编程实际上也是经过 CAS 来实现,好比无锁队列的实现。CAS 的引入也带来了 ABA 问题。关于 CAS 后面再开一篇专门的文章来总结无锁编程。
MDL 锁也是一种表级锁,MDL 锁不须要显示使用。MDL 锁是用来避免数据操做与表结构变动的冲突,试想当你执行一条查询语句时,这个时候另外一个线程在删除表中的一个字段,那么二者就发生冲突了,所以 MySQL 在5.5版本之后加上了 MDL 锁。当对一个表作增删查改时会加 MDL 读锁,当对一个表作结构变动时会加 MDL 写锁。读锁相互兼容,读锁与写锁不能兼容。
MDL 须要注意的就是避免 MDL 写锁阻塞 MDL 读锁。
事务 A | 事务 B | 事务 C | 事务 D |
---|---|---|---|
select * from t | |||
select * from t | |||
alter table t add c int | |||
select * from t |
事务 A 执行 select 后给表 t 加 MDL 读锁。事务 B 执行 select 后给表再次加上 MDL 读锁,读锁和读锁能够兼容。事务 C 执行 alter 命令时会阻塞,须要对表 t 加 MDL 写锁。事务 C 被阻塞问题并不大,可是会致使后面全部的事务都被阻塞,好比事务 D。这是为了不写锁饿死的状况发生,MySQL 对加锁所作的优化,当有写锁在等待的时候,新的读锁都须要等待。若是事务 C 长时间拿不到锁,或者事务 C 执行的时间很长都会致使数据库的操做被阻塞。
为了不这种事情发生有如下几点优化思路:
1 避免长事务。事务 A 和事务 B 若是是长事务就可能致使事务 C 阻塞在 MDL 写锁的时间比较长。
2 对于大表,修改表结构的语句能够拆分红多个小的事务,这样每次修改表结构时占用 MDL 写锁的时间会缩短。
3 给 alter 命令加等待超时时间
Gap 锁是 InnoDB 引擎为了不幻读而引入的。在 MySQL的事务一文中已经谈到,InnoDB 引擎在可重复读隔离级别下能够避免幻读。间隙锁就是锁住数据行之间的间隙,避免新的数据插入进来。只有在进行当前读的时候才会加 gap 锁。关于什么是当前读,能够看个人上一篇文章《MySQL的事务》。
一条语句会如何加锁,单纯这一句话是没法分析出来的。对加锁的分析,必须结合事务的隔离级别和索引来看,阿里数据库专家对此已经写了很是详细分析的文章,直接贴出来你们一块儿学习,MySQL 加锁处理分析
文章最后,放上我总结的 MySQL 的思惟导图,算是对 MySQL 系列文章的一个总结。
[1] 数据库系统概念(第6版)
[2] MySQL实战45讲,林晓斌
[3] 高性能MySQL(第3版)
[6] 乐观锁、悲观锁,这一篇就够了!