数据库:mysql:锁

一、锁定义

  • 锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。

这里还要区分lock与latch:

  • latch一般称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB存储引擎中,latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。
  • lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同)。此外,lock,正如在大多数数据库中一样,是有死锁机制的。

数据库:mysql:锁_第1张图片

二、锁类型

1. 行级锁

InnoDB存储引擎实现了如下两种标准的行级锁

  • 共享锁(S Lock),允许事务读一行数据。
  • 排他锁(X Lock),允许事务删除或更新一行数据。

2. 意向锁

(1) 定义

InnoDB存储引擎支持多粒度(granular)锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁

(2) 分类

InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:

  • 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁
  • 意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁

3. AUTO-INC Locking

自增长在数据库中是非常常见的一种属性,插入操作会依据这个自增长的计数器值加1赋予自增长列。这个实现方式称做AUTO-INC Locking。这种锁其实是采用一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的SQL语句后立即释放

4. 锁的兼容性

由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。故表级意向锁与行级锁的兼容性如下:
数据库:mysql:锁_第2张图片

三、一致性非锁定读和一致性锁定读

1. 一致性非锁定读

(1) 定义

一致性的非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读取行的一个快照数据。

(2) 读取原理

如下图:直观地展现了InnoDB存储引擎一致性的非锁定读。之所以称其为非锁定读,因为不需要等待访问的行上X锁的释放。快照数据是指该行的之前版本的数据,该实现是通过undo段来完成。而undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。
数据库:mysql:锁_第3张图片
这里需要注意的是,不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同

通过上图可以知道,快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。就上图所显示的,一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。

在事务隔离级别READ COMMITTED和REPEATABLE READ(InnoDB存储引擎的默认事务隔离级别)下,InnoDB存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同。在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本

(3) 优点

非锁定读机制极大地提高了数据库的并发性。在InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。

1. 一致性锁定读

(1) 定义

在某些情况下,用户需要显式地对数据库读取操作进行加锁以保证数据逻辑的一致性。

(2) 分类

InnoDB存储引擎对于SELECT语句支持两种一致性的锁定读(locking read)操作:

  • SELECT … FOR UPDATE
  • SELECT … LOCK IN SHARE MODE

SELECT … FOR UPDATE对读取的行记录加一个X锁,其他事务不能对已锁定的行加上任何锁。
SELECT…LOCK IN SHARE MODE对读取的行记录加一个S锁,其他事务可以向被锁定的行加S锁,但是如果加X锁,则会被阻塞。

注意:SELECT … FOR UPDATE,SELECT … LOCK IN SHARE MODE必须在一个事务中,当事务提交了,锁也就释放了。因此在使用上述两句SELECT锁定语句时,务必加上BEGIN,START TRANSACTION或者SET AUTOCOMMIT=0。

四、锁算法

1. 行锁的三种算法

InnoDB存储引擎有3种行锁的算法,其分别是:

  • Record Lock:单个行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
  • Next-Key Lock∶Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身

Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。
Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。

2. Next-Key Lock介绍

(1) 设计目的

为了解决幻读问题

(2) 相关设置

用户可以通过以下两种方式来显式地关闭Gap Lock:

  • 将事务的隔离级别设置为READ COMMITTED
  • 将参数innodb_locks_unsafe_for_binlog设置为1

在上述的配置下,除了外键约束和唯一性检查依然需要的Gap Lock,其余情况仅使用Record Lock进行锁定。但需要牢记的是,上述设置破坏了事务的隔离性,并且对于replication,可能会导致主从数据的不一致。此外,从性能上来看,READ COMMITTED也不会优于默认的事务隔离级别READ REPEATABLE。

(3) 举例

例如一个索引有10,11,13和20这四个值,那么该索引可能被Next-Key Locking的区间为:

(-∞,10]   (10,11]    (11,13]     (13,20]     (20,+∞)

若事务T1已经通过next-key locking锁定了如下范围:

(10,11]     (11,13]

当插入新的记录12时,则锁定的范围会变成:

(10,11]    (11,12]   (12,13]

注意:当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列中的其中一个,那么查询其实是range类型查询,而不是point类型查询,故InnoDB存储引擎依然使用Next-Key Lock进行锁定。

下面是相关例子

//表结构
CREATE TABLE `t` (
  `a` int(11) NOT NULL,
  PRIMARY KEY (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

//插入测试数据
INSERT INTO t SELECT 1;
INSERT INTO t SELECT 2;
INSERT INTO t SELECT 5;

//查询
mysql> select * from t;
+---+
| a |
+---+
| 1 |
| 2 |
| 5 |
+---+

数据库:mysql:锁_第4张图片
表t共有1、2、5三个值。在上面的例子中,在会话A中首先对a=5进行X锁定。而由于a是主键且唯一,因此锁定的仅是5这个值,而不是(2,5)这个范围,这样在会话B中插入值4而不会阻塞,可以立即插入并返回。即锁定由Next-Key Lock算法降级为了Record Lock,从而提高应用的并发性。

//表结构
CREATE TABLE `z` (
  `a` int(11) NOT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`a`),
  KEY `b` (`b`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

//插入测试数据
INSERT INTO z SELECT 1,1;
INSERT INTO z SELECT 3,1;
INSERT INTO z SELECT 5,3;
INSERT INTO z SELECT 7,6;
INSERT INTO z SELECT 10,8;

//查询
mysql> select * from z;
+----+------+
| a  | b    |
+----+------+
|  1 |    1 |
|  3 |    1 |
|  5 |    3 |
|  7 |    6 |
| 10 |    8 |
+----+------+

表z的列b是辅助索引,若在会话A中执行下面的SQL语句:

SELECT * FROM z WHERE b=3 FOR UPDATE;

很明显,这时SQL语句通过索引列b进行查询,因此其使用传统的Next-Key Locking技术加锁,并且由于有两个索引,其需要分别进行锁定。对于聚集索引,其仅对列a等于5的索引加上Record Lock。而对于辅助索引,其加上的是Next-Key Lock,锁定的范围是(1,3),特别需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock,即还有一个辅助索引范围为(3,6)的锁。因此,若在新会话B中运行下面的SQL语句,都会被阻塞:

SELECT*FROM z WHERE a=5 LOCK IN SHARE MODE;
INSERT INTO z SELECT 4,2;
INSERT INTO z SELECT 6,5;

第一个SQL语句不能执行,因为在会话A中执行的SQL语句已经对聚集索引中列a=5的值加上X锁,因此执行会被阻塞。第二个SQL语句,主键插入4,没有问题,但是插入的辅助索引值2在锁定的范围(1,3)中,因此执行同样会被阻塞。第三个SQL语句,插入的主键6没有被锁定,5也不在范围(1,3)之间。但插入的值5在另一个锁定的范围(3,6)中,故同样需要等待。而下面的SQL语句,不会被阻塞,可以立即执行:

INSERT INTO z SELECT 8,6;
INSERT INTO z SELECT 2,0;
INSERT INTO z SELECT 6,7;

在InnoDB存储引擎中,对于INSERT的操作,其会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许查询。对于上面的例子,会话A已经锁定了表z中b=3的记录,即已经锁定了(1,3)的范围,这时若在其他会话中进行如下的插入同样会导致阻塞:

INSERT INTO z SELECT 2,2;

因为在辅助索引列b上插入值为2的记录时,会监测到下一个记录3已经被索引。而将插入修改为如下的值,可以立即执行:

INSERT INTO z SELECT 2,0;

五、锁问题

通过锁定机制可以实现事务的隔离性要求,使得事务可以并发地工作。锁提高了并发,但是却会带来潜在的问题。不过好在因为事务隔离性的要求,锁只会带来三(四)种问题,如果可以防止这三(四)种情况的发生,那将不会产生并发异常。

1. 脏读

(1) 定义

脏读指的就是在不同的事务下,当前事务可以读到另外事务未提交的数据,简单来说就是可以读到脏数据。

(2) 产生条件

  • 脏读发生的条件是需要事务的隔离级别为READ UNCOMMITTED
  • 别的事务修改数据但是未提交
//设置
mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
mysql> set global tx_isolation='READ-UNCOMMITTED';

(3) 解决办法

事务的最少隔离级别为READ-COMMITTED

(4) 特例

脏读隔离看似毫无用处,但在一些比较特殊的情况下还是可以将事务的隔离级别设置为READ UNCOMMITTED。例如replication环境中的slave节点,并且在该slave上的查询并不需要特别精确的返回值。

2. 不可重复读

(1) 定义

  • 维基百科中定义:是在一个事务内两次读到的同一(行)数据是不一样的情况。
  • 在MySQL官方文档中定义:将不可重复读的问题定义为Phantom Problem,即幻读问题。即在一个事务内两次读到的数据是不一样的情况(包括update、insert和delete)
    注意:不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据,但是其违反了数据库事务一致性的要求。

(2) 产生条件

  • 需要事务的隔离级别为READ-COMMITTED
  • 别的事务对同一(行)数据执行update并且已提交
//设置
mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
mysql> set global tx_isolation='READ-COMMITTED';

(3) 解决办法

事务隔离级别最少是READ REPEATABLE,采用一致性非锁定读和Next-Key Lock算法避免不可重复读

(4) 额外说明

一般来说,不可重复读的问题是可以接受的,因为其读到的是已经提交的数据,本身并不会带来很大的问题。因此,很多数据库厂商(如Oracle、Microsoft SQL Server)将其数据库事务的默认隔离级别设置为READ COMMITTED,在这种隔离级别下允许不可重复读的现象。在MySQL官方文档中将不可重复读的问题定义为Phantom Problem,即幻读问题.

3. 幻读

(1) 定义

维基百科中定义:当在事务过程中,另一事务向正在读取的记录添加或删除新行时,将发生幻像读取(读取的数据不一样)。

(2) 产生条件

  • 事务的隔离级别为READ-COMMITTED
  • 别的事务进行insert或者delete等操作,并且已提交

(3) 解决办法

将事务的隔离级别提升至REPEATABLE-READ,采用Next-Key Lock算法避免不可重复读

3. 丢失更新

(1) 定义

简单来说其就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。
注意:理解上面的定义关键在于,mysql数据库Innodb默认的数据库隔离级别是:REPEATABLE-READ,T1 T2事务采取一致性非锁定读, 读取的是事务开始时的行数据版本.

(2) 产生条件

  • 需要事务的隔离级别为REPEATABLE-READ.
  • 采用一致性非锁定读的方式读取数据

(3) 举例

举例:
1)事务T1将行记录r更新为v1,但是事务T1并未提交。
2)与此同时,事务T2将行记录r更新为v2,事务T2未提交。
3)事务T1提交。
4)事务T2提交。
事务T2会覆盖事务T1的数据

(4) 解决办法

加上排它锁,将T1 和 T2事务串行化(也就是数据库隔离级别SERIALIZABLE所做的事)
数据库:mysql:锁_第5张图片

六、死锁

1. 定义

死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。若无外力作用,事务都将无法推进下去。

2. 分类

  • AB-BA死锁:即A等待B,B在等待A.
    数据库:mysql:锁_第6张图片
    抛出死锁异常后,Innodb存储引擎通常选择回滚undo量最小的事务.

  • 当前事务持有了待插入记录的下一个记录的X锁,但是在等待队列中存在一个S锁的请求,则可能会发生死锁。

数据库:mysql:锁_第7张图片
可以看到,会话A中已经对记录4持有了X锁,但是会话A中插入记录3时会导致死锁发生。这个问题的产生是由于会话B中请求记录4的S锁而发生等待,但之前请求的锁对于主键值记录1、2都已经成功,若在事件点5能插入记录,那么会话B在获得记录4持有的S锁后,还需要向后获得记录3的记录,这样就显得有点不合理。因此InnoDB存储引擎在这里主动选择了死锁,而回滚的是undo log记录大的事务。

3. 死锁解决方式

InnoDB存储引擎并不会回滚大部分的错误异常,但是死锁除外。发现死锁后,InnoDB存储引擎会马上回滚一个事务, 通常来说InnoDB存储引擎选择回滚undo量最小的事务

(1) 方式1:等待超时

解决死锁问题最简单的一种方法是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。

但是要设置相关选项

mysql> show variables like 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50    |
+--------------------------+-------+

mysql> show variables like 'innodb_rollback_on_timeout';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| innodb_rollback_on_timeout | OFF   |
+----------------------------+-------+

(2)方式2:wait-for graph(等待图)的方式

当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测,这是一种更为主动的死锁检测方式

wait-for graph原理:
wait-for graph要求数据库保存以下两种信息:

  • 锁的信息链表
  • 事务等待链表

通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因此资源间相互发生等待。

你可能感兴趣的:(数据库)