mysql锁相关(1)

172 阅读11分钟

前言

本文基于mysql InnoDB引擎进行介绍。锁和事务是不同的概念,事务一般指要做的或所做的事情,具备ACID的一个程序执行单元。锁是协调多个进程或线程并发访问某一个资源的机制。事务的隔离性通过锁机制来实现。

Image.png

锁粒度

表锁

表级锁为表级别的锁定,会锁定整张表,是Mysql中最大颗粒度的锁定机制。表级锁最大的特点就是实现逻辑简单,带来的系统负面影响小,获取锁和释放锁的速度很快。但是因为锁住整张表,带来的资源争抢概率很大,导致并发度大打折扣。一般alter table之类的语句使用表级锁。

行锁

行级锁的锁粒度在Mysql中是最小的,只针对当前行进行加锁,所以发生资源争抢的概率也是最小。行级锁能够给与尽可能大的并发处理能力,但是也有不小弊端。由于锁定资源的颗粒度小,所以每次获取锁和释放锁需要做的事情很多,带来的开销自然也比较大。而且行锁容易发生死锁。
随着锁定资源颗粒度的减小,锁定相同数据量的数据所需要消耗的内存数量也越来越多,实现算法也会越来越复杂。不过,随着锁定资源颗粒度的减小,请求遇到锁等待的可能性也会随之降低,整体并发度也会随之提升。之后会重点介绍行锁

页锁

页锁比较特殊,介乎于行级锁和表级锁之间。所以获取锁定所需要的资源开销,以及所能提供的并发处理能力同样也是介于上面二者之间。同理,也会发生死锁,主要用于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.For example, SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;

这句话说明行锁一定是作用在索引上的。

gap锁(间隙锁)

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. For example, SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;

间隙锁一定是开区间的,例如(10,20)

It is also worth noting here that conflicting locks can be held on a gap by different transactions. For example, transaction A can hold a shared gap lock (gap S-lock) on a gap while transaction B holds an exclusive gap lock (gap X-lock) on the same gap. The reason conflicting gap locks are allowed is that if a record is purged from an index, the gap locks held on the record by different transactions must be merged.
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.

间隙锁是不互斥的,两个事务可以共同持有。间隙锁的本质是用于阻止其他事务在该间隙插入新纪录,而自身事务是允许在该间隙插入数据的

There are also other effects of using the READ COMMITTED isolation level. Record locks for nonmatching rows are released after MySQL has evaluated the WHERE condition. For UPDATE statements, InnoDB does a “semi-consistent” read, such that it returns the latest committed version to MySQL so that MySQL can determine whether the row matches the WHERE condition of the UPDATE.

在RC隔离级别下,只有行锁,不会有间隙锁。

next-key锁(临键锁)

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.

临键锁是行锁+间隙锁,并且是左开右闭。

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 (see Section 15.7.4, "Phantom Rows").

InnoDB用临键锁解决幻读问题

共享锁S/排他锁X

InnoDB implements standard row-level locking where there are two types of locks, shared (S) locks and exclusive (X) locks.
A shared (S) lock permits the transaction that holds the lock to read a row.
An exclusive (X) lock permits the transaction that holds the lock to update or delete a row.

共享锁和排他锁都是行锁。与间隙锁没有关系

shared lockA kind of lock that allows other transactions to read the locked object, and to also acquire other shared locks on it, but not to write to it. The opposite of exclusive lock.

A kind of lock that prevents any other transaction from locking the same row. Depending on the transaction isolation level, this kind of lock might block other transactions from writing to the same row, or might also block other transactions from reading the same row. The default InnoDB isolation level, REPEATABLE READ, enables higher concurrency by allowing transactions to read rows that have exclusive locks, a technique known as consistent read.

共享锁是允许一个事务并发读取某行记录所需要持有的锁,例如select ... in share model.
排他锁是允许一个事务并发更新或者删除某一个记录所需要持有的锁,例如select ... for update

意向共享锁IS/意向排他锁IX

InnoDB supports multiple granularity locking which permits coexistence of row locks and table locks. For example, a statement such as LOCK TABLES ... WRITE takes an exclusive lock (an X lock) on the specified table. To make locking at multiple granularity levels practical, InnoDB uses intention locks. 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. There are two types of intention locks:
- An intention shared lock (IS) indicates that a transaction intends to set a shared lock on individual rows in a table
- An intention exclusive lock (IX) indicates that a transaction intends to set an exclusive lock on individual rows in a table.

这段话说明意向共享锁/意向排他锁属于表锁,且取得意向共享锁/意向排他锁是取得共享锁/排他锁的前置条件。共享锁/排他锁与意向共享锁/意向排他锁的兼容性关系:

XIXSIS
X互斥互斥互斥互斥
IX互斥兼容互斥兼容
S互斥互斥兼容兼容
IS互斥兼容兼容兼容

重点关注的是IX锁和IX锁是相互兼容的,会导致死锁的前置条件

插入意向锁IIX

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.

插入意向锁只用于并发插入操作,如果不是插入到同一个间隙同一个位置则不需要等待。插入意向锁锁住的是一个点,两个事务不能同一时间拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(不在该区间则没事)。

自增锁

An AUTO-INC lock is a special table-level lock taken by transactions inserting into tables with AUTO_INCREMENT columns. In the simplest case, if one transaction is inserting values into the table, any other transactions must wait to do their own inserts into that table, so that rows inserted by the first transaction receive consecutive primary key values.

自增锁是一种特殊的表级锁,主要用于插入自增字段,也就是常见的自增主键id。详细的可以看innodb自增处理

锁机制

悲观锁

对数据被外界修改保持保守态度,比较悲观的并发控制方式,总是假设最坏的情况,每次读取数据的时候都默认外界会修改数据,因此需要进行加锁操作,往往依靠数据库提供的锁机制。
悲观并发控制实际上是一锁二检查三更新,为数据处理提供安全保证。但是效率方面,加锁机制会增加额外开销,而且会产生死锁的机会,降低并行度。
select...for update是mysql常见的实现悲观锁的方式,但是必须确定走了索引,否则将会锁住整个表的数据

乐观锁

乐观锁相对于悲观锁而言,一般认为数据不会造成冲突。所以在数据进行提交更新的时候,才会对数据冲突与否进行检测
常见的方式,增加版本号字段version,每次更新操作verison+1。先获取当前版本号,然后update... where... and version=当前版本号。如果有其他事务修改了当前记录,那么数据库记录中版本号和当前当前版本号不一致,并不会更新,也就能知道获取到的记录非最新。

多版本并发MVCC

首先明确多MVCC主要是为了解决读-写冲突。事务开启的时候,会从数据库获取一个自增长的事务id,可以从事务id判断事务的执行先后顺序,即事务版本号。

对于InnoDB引擎,每行记录都会有两个隐藏列,trx_id、roll_point,如果没有聚簇索引的话会自动生成隐藏主键roll_id

隐藏列
列名是否必须描述
row_id单调递增行ID,不是必须,占6个字节,与MVCC无关
trx_id记录操作该行数据事务的事务ID
roll_point回滚指针,指向当前记录行undo log信息

记录操作指的是insert、update、delete。delete可以认为是一个update操作

ReadView

当执行SQL语句查询的时候会产生一致性识视图,即ReadView,并且ReadView保存了当前事务开启时所有活跃的事务列表,也可以理解为:ReadView保存了不应该让这个事务看到的其他事物ID列表。
ReadView几个重要属性

  • trx_ids: 当前系统中那些活跃(未提交)的读写事务ID, 它数据结构为一个List。(重点注意:这里的trx_ids中的活跃事务,不包括当前事务自己和已提交的事务,这点非常重要)
  • low_limit_id: 目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。
  • up_limit_id: 活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id。
  • creator_trx_id: 表示生成该 ReadView 的事务的事务id

那么如何判断该记录是否可见呢,具体规则如下

  • 如果被访问版本的 事务ID = creator_trx_id,那么表示当前事务访问的是被自己修改过的记录,那么该版本对当前事务可见
  • 如果被访问版本的 事务ID < up_limit_id,那么表示生成该版本的事务在当前事务生成的ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的事务ID > low_limit_id,那么表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问
  • 如果被访问版本的事务ID 在 up_limit_id和low_limit_id之间,那么需要判断是否在trx_ids列表里面。如果在,则说明被访问版本的事务还是活跃的,不可以被访问到。如果不在,则说明创建该ReadView时候生成被访问版本的事务已经提交,所以可以被访问到。

上面说过在select才会生成ReadView。但是不同隔离级别是有区别的,在RC下,每个select都会生成最新的ReadView,在RR下,当前事务中第一个select才会创建ReadView。所以

MVCC原理分析

如何查询到一条记录呢

1、获取当前事务ID,即trx_id。事务开启的时候获取,即begin的时候
2、获取到ReadView,select的时候获取
3、数据库表中如果查询到数据,那就到ReadView中的事务版本号进行比较,规则见ReadView
4、如果不符合ReadView可见性规则,那么到UndoLog中的历史快照中找符合规则的数据

综上,MVCC通过ReadView+undoLog的方式实现的。ReadView判断当前版本是否可见,UndoLog提供历史快照。

MVCC如何实现读已提交和可重读呢

MVCC示例 (2).png

兼容性

共享锁

参考上面

排它锁

参考上面

示例分析

CREATE TABLE `test` (
  `id` int(20) NOT NULL,
  `name` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
mysql> SELECT * FROM test;
+----+------+
| id | name |
+----+------+
|  1 | 1    |
|  5 | 5    |
| 10 | 10   |
| 15 | 15   |
| 20 | 20   |
| 25 | 25   |
+----+------+
6 rows in set (0.00 sec)

场景一

Image [2].png

在场景一中,因为IX锁是表锁IX锁之间是兼容的,因而事务一和事务二都能同时获取到IX锁间隙锁。另外,需要说明的是,因为我们的隔离级别是RR,且在请求X锁的时候,查询的对应记录都不存在,因而返回的都是间隙锁。接着事务一请求插入意向锁,这时发现事务二已经获取了一个区间间隙锁,而且事务一请求的插入点在事务二的间隙锁区间内,因而只能等待事务二释放间隙锁。这个时候事务二也请求插入意向锁,该插入点同样位于事务一已经获取的间隙锁的区间内,因而也不能获取成功,不过这个时候,MySQL已经检查到了死锁,于是事务二被回滚,事务一提交成功。

场景二

Image [3].png

两个间隙锁没有交集,而各自获取的插入意向锁也不是同一个点,因而都能执行成功。