MySQL之表锁、行锁、MVCC详解

该文建议配合数据库理论之并发控制阅读。

概述

锁是计算机协调多个进程或线程并发访问某一资源的机制(避免争抢)。

在数据库中,除传统的计算资源(如 CPU、RAM、I/O 等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。

锁分类

从对数据操作的粒度分 :

  1. 表锁:操作时,会锁定整个表。
  2. 行锁:操作时,会锁定当前操作行。

从对数据操作的类型分:

  1. 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。
  2. 写锁(排它锁):当前操作没有完成之前,它会阻断其他写锁和读锁。

MySQL中的锁

相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。下表中罗列出了各存储引擎对锁的支持情况:

存储引擎 表级锁 行级锁 页面锁
MyISAM 支持 不支持 不支持
InnoDB 支持 支持 不支持
MEMORY 支持 不支持 不支持
BDB 支持 不支持 支持

MySQL这3种锁的特性可大致归纳如下 :

锁类型 特点
表级锁 偏向MyISAM 存储引擎,开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁 偏向InnoDB 存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

从上述特点可见,很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适。

仅从锁的角度来说:
+ 表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web 应用
+ 行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理(OLTP)系统。

所有的存储引擎都以自己的方式实现了锁机制,服务器层完全不了解存储引擎中的锁实现。但服务器层也会使用各种有效的表锁来实现不同的目的。

对于 MySQL 而言,事务机制更多是靠底层的存储引擎实现的,在服务器层面只有表锁。支持事务的 InnoDB 存储引擎实现了行锁。

MyISAM 表锁

偏向 MyISAM 存储引擎,开销小,加锁快,无死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

在特定的场景中,表锁也可能有良好的性能。比如,READ LOCAL 表锁支持某些类型的并发写操作;另外,写锁也比读锁有更高的优先级,因此一个写锁请求可以会被插入到读锁队列的前面。

尽管存储引擎可以管理自己的锁,服务器还是会使用各种有效的表锁来实现不同的目的。比如服务器在 ALTER TABLE 时使用表锁,而忽略存储引擎的锁机制。MyISAM 在读表前自动对表加读锁,在写表前自动对表加写锁。

SQL语法

添加表锁:

lock table table1 read/write , table2 read/write ,...

read为读锁,共享锁;write为写锁,独占锁。

显示加过锁的表:

show open tables;
  • In_use : 表当前被查询使用的次数。如果该数为零,则表是打开的,但是当前没有被使用。为1代表已加锁。
  • Name_locked:表名称是否被锁定。名称锁定用于取消表或对表进行重命名等操作。

释放表锁:

unlock tables;

总结

  1. 对 MyISAM 表的读操作(加读锁),不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
  2. 对 MyISAM 表的写操作(加写锁),会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作。

简而言之,就是读锁会阻塞写,但是不会堵塞读。而写锁则会把读和写都堵塞。这也是MyISAM不适合做写为主表的引擎的原因。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞。

InnoDB 行锁

特点:

  • 锁粒度小,并发度高;开销大,加锁慢,会出现死锁
  • 支持事务

InnoDB也实现了共享锁和排他锁两种类型的行锁。

InnoDB也支持意向锁,可以对表级别上意向锁。

对于UPDATEDELETEINSERT语句,InnoDB会自动给涉及的数据集加排他锁

对于普通SELECT语句,InnoDB不会加任何锁,使用一致性非锁定读

了解:一致性非锁定读

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

MySQL之表锁、行锁、MVCC详解

快照数据是该行的之前版本的数据,实现是通过undo段来完成(即为了回滚事务而产生的重做日志)。

通过读取不同的快照版本实现不同的事务隔离级别:

  • 读已提交:一致性非锁定读总是读取被锁定行的最新一份快照数据。产生了不可重复读的问题。
  • 重复读:一致性非锁定读总是读取事务开始时的行数据版本. 解决不可重复读的问题。

一致性非锁定读是InnoDB默认的读取方式,不会占用和等待表上的锁。

一致性锁定读

某些情况下,用户需要显式地对数据库读取操作进行加锁以保证数据逻辑的一致性。可以通过以下语句显示给记录集加共享锁或排他锁(一致性锁定读)。

共享锁(S):
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE

排他锁(X) :
SELECT * FROM table_name WHERE ... FOR UPDATE

当事务提交了,锁就释放了。

可以使用show status like 'innodb_row_lock%';查看innodb行锁的争用情况

  • Innodb_row_lock_current_waits: 当前正在等待锁定的数量
  • Innodb_row_lock_time: 从系统启动到现在锁定总时间长度

  • Innodb_row_lock_time_avg:每次等待所花平均时长

  • Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间

  • Innodb_row_lock_waits: 系统启动后到现在总共等待的次数

行锁的三种算法

record lock

InnoDB 里的行锁(record lock)是索引记录的锁。

record lock 锁住的是索引记录,即使该表上没有任何索引,那么 innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。所以说当一条 sql 没有走任何索引时,那么将会在每一条聚簇索引后面加 X 锁,这个类似于表锁,但原理上和表锁应该是完全不同的。

gap lock

当我们用范围条件,而不是使用相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据进行加锁; 对于键值在条件范围内但并不存在的记录,叫做 "间隙(GAP)",InnoDB也会对这个 "间隙" 加锁,这种锁机制就是所谓的间隙锁(gap lock),它锁定一个范围的记录,但不包括记录本身。锁加在未使用的空闲空间上,可能是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间。间隙锁针对事务隔离级别为可重复读或以上级别。

next-key lock

行锁与间隙锁组合起来用就叫做 Next-Key Lock。锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是阻止多个事务将记录插入到同一范围内,解决幻读的问题。

InnoDB 工作在可重复读隔离级别下,并且会以 Next-Key Lock 的方式对数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock 是行锁和间隙锁的组合,当 InnoDB 扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。

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

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

若事务T1已经锁定了如下范围:

(10,11](11,13]

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

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

当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-key Lock进行优化,将其降级为Record lock,即仅锁住索引本身,而不是范围。

如何理解三种算法

我这里谈谈我自己的理解:

对于唯一索引(比如主键),只要保证锁住该索引记录,就不可能出现幻读,这对应的就是Record Lock

比如对于sql查询

select * from t_user where id = 5 for update;

这种情况就只会加一个Record Lock,第一次查询数据之后锁住了该记录,此后在同一事务内一直到第二次查询不可能会出现其他事务也将其他记录插入到id = 5这个位置。

而如果不是唯一索引,第一次查询数据之后即使所住了该记录,但可能还会有记录被插到id = 5这个位置,所以此时需要范围索引,锁住索引为5这个值,对于范围查询,范围索引更重要。

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/mysql%e4%b9%8b%e8%a1%a8%e9%94%81%e3%80%81%e8%a1%8c%e9%94%81%e3%80%81mvcc%e8%af%a6%e8%a7%a3/

发表评论

电子邮件地址不会被公开。