【MySQL】MySQL中的锁机制

MySQL锁机制起步

锁是计算机用以协调多个进程间并发访问同一共享资源的一种机制。MySQL中为了保证数据访问的一致性与有效性等功能,实现了锁机制,MySQL中的锁是在服务器层或者存储引擎层实现的。html

行锁与表锁

首先咱们来了解行锁与表锁的基本概念,从名字中咱们就能够了解:表锁就是对整张表进行加锁,而行锁则是锁定某行、某几行数据或者行之间的间隙。mysql

各引擎对锁的支持状况以下:linux

行锁 表锁 页锁
MyISAM
BDB
InnoDB

行锁

A record lock is a lock on an index record. Record locks always lock index records, even if a table is defined with no indexes. For such cases, InnoDB creates a hidden clustered index and uses this index for record locking.sql

上文出自MySQL的官方文档,从这里咱们能够看出行锁是做用在索引上的,哪怕你在建表的时候没有定义一个索引,InnoDB也会建立一个聚簇索引并将其做为锁做用的索引。数据库

这里仍是讲一下InnoDB中的聚簇索引。每个InnoDB表都须要一个聚簇索引,有且只有一个。若是你为该表表定义一个主键,那么MySQL将使用主键做为聚簇索引;若是你不为定义一个主键,那么MySQL将会把第一个惟一索引(并且要求NOT NULL)做为聚簇索引;若是上诉两种状况都GG,那么MySQL将自动建立一个名字为GEN_CLUST_INDEX的隐藏聚簇索引。服务器

由于是聚簇索引,因此B+树上的叶子节点都存储了数据行,那么若是如今是二级索引呢?InnoDB中的二级索引的叶节点存储的是主键值(或者说聚簇索引的值),因此经过二级索引查询数据时,还须要将对应的主键去聚簇索引中再次进行查询。网络

关于索引的问题就到这,咱们用一张直观的图来表示行锁:session

接下来以两条SQL的执行为例,讲解一下InnoDB对于单行数据的加锁原理:并发

update user set age = 10 where id = 49;
update user set age = 10 where name = 'Tom';

第一条SQL使用主键查询,只须要在 id = 49 这个主键索引上加上锁。第二条 SQL 使用二级索引来查询,那么首先在 name = Tom 这个索引上加写锁,而后因为使用 InnoDB 二级索引还需再次根据主键索引查询,因此还须要在 id = 49 这个主键索引上加锁。性能

也就是说使用主键索引须要加一把锁,使用二级索引须要在二级索引和主键索引上各加一把锁。

根据索引对单行数据进行更新的加锁原理了解了,那若是更新操做涉及多个行呢,好比下面 SQL 的执行场景。

update user set age = 10 where id > 49;

上述 SQL 的执行过程以下图所示。MySQL Server 会根据 WHERE 条件读取第一条知足条件的记录,而后 InnoDB 引擎会将第一条记录返回并加锁,接着 MySQL Server 发起更新改行记录的 UPDATE 请求,更新这条记录。一条记录操做完成,再读取下一条记录,直至没有匹配的记录为止。

表锁

上面咱们讲解行锁的时候,操做语句中的条件判断列都是有创建索引的,那么若是如今的判断列不存在索引呢?InnoDB既支持行锁,也支持表锁,当没有查询列没有索引时,InnoDB就不会去搞什么行锁了,毕竟行锁必定要有索引,因此它如今搞表锁,把整张表给锁住了。那么具体啥是表锁?还有其余什么状况下也会进行锁表呢?

表锁使用的是一次性锁技术,也就是说,在会话开始的地方使用 lock 命令将后续须要用到的表都加上锁,在表释放前,只能访问这些加锁的表,不能访问其余表,直到最后经过 unlock tables 释放全部表锁。

除了使用 unlock tables 显示释放锁以外,会话持有其余表锁时执行lock table 语句会释放会话以前持有的锁;会话持有其余表锁时执行 start transaction 或者 begin 开启事务时,也会释放以前持有的锁。

表锁由 MySQL Server 实现,行锁则是存储引擎实现,不一样的引擎实现的不一样。在 MySQL 的经常使用引擎中 InnoDB 支持行锁,而 MyISAM 则只能使用 MySQL Server 提供的表锁。

两种锁的比较

表锁:加锁过程的开销小,加锁的速度快;不会出现死锁的状况;锁定的粒度大,发生锁冲突的概率大,并发度低;

  • 通常在执行DDL语句时会对整个表进行加锁,好比说 ALTER TABLE 等操做;

  • 若是对InnoDB的表使用行锁,被锁定字段不是主键,也没有针对它创建索引的话,那么将会锁整张表;

  • 表级锁更适合于以查询为主,并发用户少,只有少许按索引条件更新数据的应用,如Web 应用。

行锁:加锁过程的开销大,加锁的速度慢;会出现死锁;锁定粒度最小,发生锁冲突的几率最低,并发度也最高;

  • 最大程度的支持并发,同时也带来了最大的锁开销。
  • 在 InnoDB 中,除单个 SQL 组成的事务外,锁是逐步得到的,这就决定了在 InnoDB 中发生死锁是可能的。
  • 行级锁只在存储引擎层实现,而 MySQL 服务器层没有实现。 行级锁更适合于有大量按索引条件并发更新少许不一样数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。

MyISAM表锁

MyISAM表级锁模式

  • 表共享读锁(Table Read Lock):不会阻塞其余线程对同一个表的读操做请求,但会阻塞其余线程的写操做请求;

  • 表独占写锁(Table Write Lock):一旦表被加上独占写锁,那么不管其余线程是读操做仍是写操做,都会被阻塞;

默认状况下,写锁比读锁具备更高的优先级;当一个锁释放后,那么它会优先相应写锁等待队列中的锁请求,而后再是读锁中等待的获取锁的请求。

This ensures that updates to a table are not “starved” even when there is heavy SELECT activity for the table. However, if there are many updates for a table, SELECT statements wait until there are no more updates.

这种设定也是MyISAM表不适合于有大量更新操做和查询操做的缘由。大量更新操做可能会形成查询操做很难以获取读锁,从而过长的阻塞。同时一些须要长时间运行的查询操做,也会使得线程“饿死”,应用中应尽可能避免出现长时间运行的查询操做(在可能的状况下能够经过使用中间表等措施对SQL语句作必定的“分解”,使每一步查询都能在较短的时间内完成,从而减小锁冲突。若是复杂查询不可避免,应尽可能安排在数据库空闲时段执行,好比一些按期统计能够安排在夜间执行。)

咱们能够经过一些设置来调节MyISAM的调度行为:

  • 经过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利;
  • 经过执行命令SET LOW_PRIORITY_UPDATES=1,使该链接发出的更新请求优先级下降;
  • 经过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,下降该语句的优先级;
  • 给系统参数max_write_lock_count设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级下降,给读进程必定得到锁的机会。

MyISAM对表加锁分析

MyISAM在执行查询语句(SELECT)前,会自动给涉及的全部表加读锁,在执行更新操做(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不须要用户干预,所以用户通常不须要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁。在自动加锁的状况下,MyISAM 老是一次得到 SQL 语句所须要的所有锁,这也正是 MyISAM 表不会出现死锁(Deadlock Free)的缘由。

MyISAM存储引擎支持并发插入,以减小给定表的读操做和写操做之间的争用:

若是MyISAM表在数据文件中没有空闲块(因为删除或更新致使的空行),则行始终插入数据文件的末尾。在这种状况下,你能够自由混合并发使用MyISAM表的 INSERT 和 SELECT 语句而不须要加锁(你能够在其余线程进行读操做的状况下,同时将行插入到MyISAM表中)。若是文件中有空闲块,则并发插入会被禁止,但当全部的空闲块从新填充有新数据时,它又会自动启用。 要控制此行为,可使用MySQL的concurrent_insert系统变量。

  • 当concurrent_insert=0时,不容许并发插入功能。

  • 当concurrent_insert=1时,容许对没有空闲块的表使用并发插入,新数据位于数据文件结尾(缺省)。

  • 当concurrent_insert=2时,无论表有没有空想快,都容许在数据文件结尾并发插入。

显式加表锁的应用

上面已经说起了表锁的加锁方式,通常表锁都是隐式加锁的,不须要咱们去主动声明,可是也有须要显式加锁的状况,这里简单作下介绍:

给MyISAM表显式加锁,通常是为了必定程度模拟事务操做,实现对某一时间点多个表的一致性读取。例如,有一个订单表orders,其中记录有订单的总金额total,同时还有一个订单明细表 order_detail,其中记录有订单每一产品的金额小计 subtotal,假设咱们须要检查这两个表的金额合计是否相等,可能就须要执行以下两条SQL:

SELECT SUM(total) FROM orders;
SELECT SUM(subtotal) FROM order_detail;

这时,若是不先给这两个表加锁,就可能产生错误的结果,由于第一条语句执行过程当中,order_detail表可能已经发生了改变。所以,正确的方法应该是:

LOCK tables orders read local,order_detail read local;
SELECT SUM(total) FROM orders;
SELECT SUM(subtotal) FROM order_detail;
Unlock tables;

查看表锁争用状况:

能够经过检查 table_locks_waited 和 table_locks_immediate 状态变量来分析系统上的表锁的争夺,若是 Table_locks_waited 的值比较高,则说明存在着较严重的表级锁争用状况:

mysql> SHOW STATUS LIKE 'Table%';
+-----------------------+---------+
| Variable_name | Value |
+-----------------------+---------+
| Table_locks_immediate | 1151552 |
| Table_locks_waited | 15324 |
+-----------------------+---------+

InnoDB行锁与表锁

InnoDB锁模式

1)InnoDB中的行锁

InnoDB实现了如下两种类型的行锁:

  • 共享锁(S):加了锁的记录,全部事务都能去读取但不能修改,同时阻止其余事务得到相同数据集的排他锁;
  • 排他锁(X):容许已经得到排他锁的事务去更新数据,阻止其余事务取得相同数据集的共享读锁和排他写锁;

2)InnoDB表锁——意向锁

因为表锁和行锁虽然锁定范围不一样,可是会相互冲突。当你要加表锁时,势必要先遍历该表的全部记录,判断是否有排他锁。这种遍历检查的方式显然是一种低效的方式,MySQL引入了意向锁,来检测表锁和行锁的冲突。

Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table。

The intention locking protocol is as follows:

  • Before a transaction can acquire a shared lock on a row in a table, it must first acquire an IS lock or stronger on the table.

  • Before a transaction can acquire an exclusive lock on a row in a table, it must first acquire an IX lock on the table.

意向锁也是表级锁,分为读意向锁(IS锁)和写意向锁(IX锁)。当事务要在记录上加上行锁时,要首先在表上加上意向锁。这样判断表中是否有记录正在加锁就很简单了,只要看下表上是否有意向锁就好了,从而就能提升效率。

意向锁之间是不会产生冲突的,它只会阻塞表级读锁或写锁。意向锁不于行级锁发生冲突。

锁模式的兼容矩阵

下面表显示了了各类锁之间的兼容状况:

X IX S IS
X
IX 兼容 兼容
S 兼容 兼容
IS 兼容 兼容 兼容

(注意上面的X与S是说表级的X锁和S锁,意向锁不和行级锁发生冲突)

若是一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;若是二者不兼容,那么该事务就须要等待锁的释放。

InnoDB的加锁方法

  • 意向锁是 InnoDB 自动加的,不须要用户干预;
  • 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及的数据集加上排他锁;
  • 对于普通的SELECT语句,InnoDB不会加任何锁;事务能够经过如下语句显示给记录集添加共享锁或排他锁:
    • 共享锁(S):select * from table_name where ... lock in share mode。此时其余 session 仍然能够查询记录,并也能够对该记录加 share mode 的共享锁。可是若是当前事务须要对该记录进行更新操做,则颇有可能形成死锁。
    • 排他锁(X):select * from table_name where ... for update。其余session能够查询记录,可是不能对该记录加共享锁或排他锁,只能等待锁释放后在加锁。

select for update

在执行这个 select 查询语句的时候,会将对应的索引访问条目加上排他锁(X锁),也就是说这个语句对应的锁就至关于update带来的效果;

使用场景:为了让确保本身查找到的数据必定是最新数据,而且查找到后的数据值容许本身来修改,此时就须要用到select for update语句;

性能分析:select for update语句至关于一个update语句。在业务繁忙的状况下,若是事务没有及时地commit或者rollback可能会形成事务长时间的等待,从而影响数据库的并发使用效率。

select lock in share mode

in share mode 子句的做用就是将查找的数据加上一个share锁,这个就是表示其余的事务只能对这些数据进行简单的 select 操做,而不能进行 DML 操做。

使用场景:为了确保本身查询的数据不会被其余事务正在修改,也就是确保本身查询到的数据是最新的数据,而且不容许其余事务来修改数据。与select for update不一样的是,本事务在查找完以后不必定能去更新数据,由于有可能其余事务也对同数据集使用了 in share mode 的方式加上了S锁;

性能分析:select lock in share mode 语句是一个给查找的数据上一个共享锁(S 锁)的功能,它容许其余的事务也对该数据上S锁,可是不可以容许对该数据进行修改。若是不及时的commit 或者rollback 也可能会形成大量的事务等待。

InnoDB的锁争用状况

能够经过检查 InnoDB_row_lock 状态变量来分析系统上的行锁的争夺状况:

mysql> show status like 'innodb_row_lock%'; 
+-------------------------------+-------+ 
| Variable_name | Value | 
+-------------------------------+-------+ 
| InnoDB_row_lock_current_waits | 0 | 
| InnoDB_row_lock_time | 0 | 
| InnoDB_row_lock_time_avg | 0 | 
| InnoDB_row_lock_time_max | 0 | 
| InnoDB_row_lock_waits | 0 | 
+-------------------------------+-------+ 
5 rows in set (0.01 sec)

行锁的类型

上面咱们根据了锁的粒度将锁分为了行锁与表锁,接下来根据使用场景的不一样,又能够将行锁进行进一步的划分:Next-Key Lock、Gap Lock、Record Lock以及插入意向GAP锁。

不一样的锁锁定的位置是不一样的,好比说记录锁只锁定对应的记录,而间隙锁锁住记录和记录之间的间隙,Next-key Lock则锁住所属记录之间的间隙。不一样的锁类型锁定的范围大体如图所示:

记录锁(Record Lock)

记录锁最简单的一种行锁形式,上面咱们以及稍微说起过了。这里补充下的点就是:行锁是加在索引上的,若是当你的查询语句不走索引的话,那么它就会升级到表锁,最终形成效率低下,因此在写SQL语句时须要特别注意。

间隙锁(Gap Lock)

A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record。

当咱们使用范围条件而不是相等条件去检索,并请求锁时,InnoDB就会给符合条件的记录的索引项加上锁;而对于键值在条件范围内但并不存在(参考上面所说的空闲块)的记录,就叫作间隙,InnoDB在此时也会对间隙加锁,这种记录锁+间隙锁的机制叫Next-Key Lock。额,扯的有点快。

从上面这句话能够代表间隙锁是所在两个存在的索引之间,是一个开区间,像最开始的那张索引图,15和18之间,是有(16,17)这个间隙存在的。

Gap locks in InnoDB are “purely inhibitive”, which means that their only purpose is to prevent other transactions from inserting to the gap. Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function.

上面这段话代表间隙锁是能够共存的,共享间隙锁与独占间隙锁之间是没有区别的,二者之间并不冲突。其存在的目的都是防止其余事务往间隙中插入新的纪录,故而一个事务所采起的间隙锁是不会去阻止另一个事务在同一个间隙中加锁的。

固然也不是在何时都会去加间隙锁的:

Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED. Under these circumstances, gap locking is disabled for searches and index scans and is used only for foreign-key constraint checking and duplicate-key checking.

这段话代表,在 RU 和 RC 两种隔离级别下,即便你使用 select in share mode 或 select for update,也没法防止幻读(读后写的场景)。由于这两种隔离级别下只会有行锁,而不会有间隙锁。而若是是 RR 隔离级别的话,就会在间隙上加上间隙锁。

临键锁(Next-key Lock)

A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.

临键锁是记录锁与与间隙锁的结合,因此临键锁与间隙锁是一个同时存在的概念,而且临键锁是个左开有闭的却好比(16, 18]。

关于临键锁与幻读,官方文档有这么一条说明:

By default, InnoDB operates in REPEATABLE READ transaction isolation level. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows.

就是说 MySQL 默认隔离级别是RR,在这种级别下,若是你使用 select in share mode 或者 select for update 语句,那么InnoDB会使用临键锁(记录锁 + 间隙锁),于是能够防止幻读;

可是我也在网上看到相关描述:即便你的隔离级别是 RR,若是你这是使用普通的select语句,那么此时 InnoDB 引擎将是使用快照读,而不会使用任何锁,于是仍是没法防止幻读。

插入意向锁(Insert Intention Lock)

An insert intention lock is a type of gap lock set by INSERT operations prior to row insertion. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.

官方文档已经解释得很清楚了,这里我作个翻译机:

插入意图锁是一种间隙锁,在行执行 INSERT 以前的插入操做设置。若是多个事务 INSERT 到同一个索引间隙之间,但没有在同一位置上插入,则不会产生任何的冲突。假设有值为4和7的索引记录,如今有两事务分别尝试插入值为 5 和 6 的记录,在得到插入行的排他锁以前,都使用插入意向锁锁住 4 和 7 之间的间隙,但二者之间并不会相互阻塞,由于这两行并不冲突。

插入意向锁只会和 间隙或者 Next-key 锁冲突,正如上面所说,间隙锁做用就是防止其余事务插入记录形成幻读,正是因为在执行 INSERT 语句时须要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操做的执行。

不一样类型锁之间的兼容

不一样类型的锁之间的兼容以下表所示:

RECORED GAP NEXT-KEY II GAP(插入意向锁)
RECORED 兼容 兼容
GAP 兼容 兼容 兼容 兼容
NEXT-KEY 兼容 兼容
II GAP 兼容 兼容

(其中行表示已有的锁,列表示意图加上的锁)

其中,第一行表示已有的锁,第一列表示要加的锁。插入意向锁较为特殊,因此咱们先对插入意向锁作个总结,以下:

  • 插入意向锁不影响其余事务加其余任何锁。也就是说,一个事务已经获取了插入意向锁,对其余事务是没有任何影响的;
  • 插入意向锁与间隙锁和 Next-key 锁冲突。也就是说,一个事务想要获取插入意向锁,若是有其余事务已经加了间隙锁或 Next-key 锁,则会阻塞。

其余类型的锁的规则较为简单:

  • 间隙锁不和其余锁(不包括插入意向锁)冲突;
  • 记录锁和记录锁冲突,Next-key 锁和 Next-key 锁冲突,记录锁和 Next-key 锁冲突;

文章写到这里吧,再写下去有点长了。上面文章中不少信息都来源网络,我只是个搬运工,倘若哪里有表述错误,请评论区留言。

参考资料:

相关文章
相关标签/搜索