MySQL锁机制
1、前言
Mysql为了解决并发、数据安全的问题,使用了锁机制。可以按照锁的粒度把数据库锁分为表级锁和行级锁。
表级锁:Mysql中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单 ,资源消耗也比较少,加锁快,不会出现死锁 。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。
行级锁:Mysql中锁中粒度最小的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 InnoDB支持的行级锁,包括如下几种算法。
-
Record Lock: 对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。理解成单个行记录上的锁。
-
Gap Lock: 对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
-
Next-key Lock: 锁定索引项本身和索引范围。锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。即Record Lock和Gap Lock的结合。可解决数据库幻读问题。Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。例如有一个索引有10,11,13和20这4个值,那么该索引可能被Next-Key Locking的区间为:
虽然使用行级索具有粒度小、并发度高等特点,但是表级锁有时候也是非常必要的:
-
事务更新大表中的大部分数据直接使用表级锁效率更高;
-
事务比较复杂,使用行级索很可能引起死锁导致回滚。
2、锁分类
无论是表级锁和行级锁可以进一步划分为共享锁(S)和排他锁(X)。
共享锁(s):共享锁(Share Locks,简记为S)又被称为读锁,其他用户可以并发读取数据,但任何事务都不能获取数据上的排他锁,直到已释放所有共享锁。
共享锁(S锁)又称为读锁,若事务T对数据对象A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
排他锁(X):排它锁((Exclusive lock,简记为X锁))又称为写锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。
两者之间的区别:
-
共享锁(S锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获取共享锁的事务只能读数据,不能修改数据。
-
排他锁(X锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获取排他锁的事务既能读数据,又能修改数据。
S 锁之间不冲突,X 锁则为独占锁,所以 X 之间会冲突, X 和 S 也会冲突。
InnoDB 的表锁很鸡肋:
-
LOCK TABLES yes READ 是对 yes 这个表上 S 锁。
-
LOCK TABLES yes WRITE 是对 yes 这个表上 X 锁。
但是基本上没用。平日的 update 、select 要用也是用行锁了,不可能用粒度粗的表锁。唯一能想到用上表锁的就是 DDL 语句了,比如 ALTER TABLE 的时候,应该锁定整个表,防止查询和修改。但是这个 server 已经提供了一个叫 MDL 的东西,即 Metadata Locks(元数据锁),所以已经用 MDL 来阻塞了,表锁也就排不上用场了。真要用表锁,估计也就是数据恢复的时候,手动锁表还原数据了。
但是如果真要到用表锁的时候,那表锁和行锁之间不是会冲突的吗?如果表里面已经加了行锁怎么办?得一条记录一条记录遍历过去找行锁吗?这确实是一种实现方式,但是性能太差了,假设数据库里有上千万的数据,这加个表锁得找死。所以有了个叫意向锁(Intention Locks)的东西。
-
IS(Intention Shared Lock),共享意向锁
-
IX(Intention Exclusive Lock),独占意向锁。
3、InnoDB中的锁
3.1、意向锁
InnoDB 支持多种粒度的锁,也就是行锁和表锁。为了支持多粒度锁定,InnoDB 存储引擎引入了意向锁(Intention Lock)。
那什么是意向锁呢?我们在这里可以举一个例子:如果没有意向锁,当已经有人使用行锁对表中的某一行进行修改时,如果另外一个请求要对全表进行修改,那么就需要对所有的行是否被锁定进行扫描,在这种情况下,效率是非常低的;不过,在引入意向锁之后,当有人使用行锁对表中的某一行进行修改之前,会先为表添加意向排他锁(IX),再为行记录添加排他锁(X),在这时如果有人尝试对全表进行修改就不需要判断表中的每一行数据是否被加锁了,只需要通过等待意向互斥锁被释放就可以了。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。
与上一节中提到的两种锁的种类相似的是,意向锁也分为两种:
意向共享锁(IS):事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁。
意向互斥锁(IX):事务想要在获得表中某些记录的排他锁,需要在表上先加意向排他锁。
注意:
这里的意向锁是表级锁,表示的是一种意向,仅仅表示事务正在读或写某一行记录,在真正加行锁时才会判断是否冲突。意向锁是InnoDB自动加的,不需要用户干预。意向锁其实不会阻塞全表扫描之外的任何请求,它们的主要目的是为了表示是否有人请求锁定表中的某一行数据。
3.2、自增长锁AUTO-INC Locks
自增长锁是一种特殊的表锁机制,提升并发插入性能。对于这个锁有几个特点:
-
在sql执行完就释放锁,并不是事务执行完。
-
对于Insert...select大数据量插入会影响插入性能,因为会阻塞另外一个事务执行。
-
自增算法可以配置
4、InnoDB锁算法
InnoDB 有几类行锁:
4.1、记录锁(Record-Lock)
记录锁是锁住记录的,这里要说明的是这里锁住的是索引记录,而不是真正的数据记录。
-
如果锁的是非主键索引,会在自己的索引上面加锁之后然后再去主键上面加锁锁住.
-
如果没有表上没有索引(包括没有主键),则会使用隐藏的主键索引进行加锁。
-
如果要锁的列没有索引,则会进行全表记录加锁。
前面也提到,普通的 select 语句是不会对记录加锁的,如果要在查询时对记录加行锁,可以使用下面这两个方式:
//对读取的记录加共享锁 select ... lock in share mode; //对读取的记录加独占锁 select ... for update;
上面这两条语句必须再一个事务中,当事务提交了,锁就会被释放,因此在使用这两条语句的时候,要加上 begin、start transaction 或者 set autocommit = 0。
4.2、间隙锁gap lock(解决幻读)
间隙锁顾名思义锁间隙,不锁记录。锁间隙的意思就是锁定某一个范围,间隙锁又叫gap锁,其不会阻塞其他的gap锁,但是会阻塞插入间隙锁,这也是用来防止幻读的关键。
比如此时有 1、3、5、10 这四条记录,之前的文章分析过,数据页中还有两条虚拟的记录,分别是 Infimum 和 Supremum。
可以看到,记录之前都有间隙,那间隙锁呢,锁的就是这个间隙!比如我把 3 和 5 之间的间隙锁了,此时要插入 id = 4 的记录,就会被这个间隙锁给阻塞了,这样就避免了幻读的产生!也就实现了锁定未插入的记录的需求!
还有个 Next-Key Locks 就是记录锁+间隙锁,像上面间隙锁的举例,只能锁定(3,5) 这个区间,而 Next-Key Locks 是一个前开后闭的区间(3,5],这样能防止查询 id=5 的这个幻读。
问:那间隙锁之间会不会冲突?
不会,间隙锁的唯一目的就是防止其他事务插入数据到间隙中 ,所以即使两个间隙锁要锁住相同的间隙也没有关系,因为它们的目的是一致的,所以不冲突。
那间隙锁可以显式禁用吗?
可以的。间隙锁是在事务隔离级别为可重复读的时候生效的,如果将事务隔离级别更改为READ COMMITTED,就会禁用了,此时,间隙锁对于搜索和索引扫描是禁用的,仅用于外键约束检查和重复键检查。
说到间隙锁,那你知道什么是插入意向锁吗?
插入意向锁,即 Insert Intention Locks,它也是一类间隙锁,但是它不是锁定间隙,而是等待某个间隙。比如上面举例的 id = 4 的那个事务 C ,由于被间隙锁给阻塞了,所以事务 C 会生成一个插入意向锁,表明等待这个间隙锁的释放。并且插入意向锁之间不会阻塞,因为它们的目的也是只等待这个间隙被释放,所以插入意向锁之间没有冲突。
所以这个插入意向锁其实没什么用的?
我:确实,它的目的不在于锁定资源防止别人访问,我个人觉得更像是为了遵循 MySQL 的锁代码实现而为之。
锁其实就是内存里面的一个结构,每个事务为某个记录或者间隙上锁就是创建一个锁对象来争抢资源。
如果某个事务没有抢到资源,那也会生成一个锁对象,只是状态是等待的,而当拥有资源的事务释放锁之后,就会寻找正在等待当前资源的锁结构,然后选一个让它获得资源并唤醒对应的事务使之得以执行。
4.3、next-key锁
这个锁本质是记录锁加上gap锁。在RR隔离级别下(InnoDB默认),Innodb对于行的扫描锁定都是使用此算法,但是如果查询扫描中有唯一索引会退化成只使用记录锁。为什么呢? 因为唯一索引能确定行数,而其他索引不能确定行数,有可能在其他事务中会再次添加这个索引的数据会造成幻读。
5、锁带来的问题
通过锁定机制可以实现事务隔离性要求,使得事务可以并发的工作。锁提高了并发,但是却会带来潜在的问题。
5.1、 脏读
在不同的事务下,当前事务可以读到另外事务未提交的数据。另外我们需要注意的是默认的MySQL隔离级别是REPEATABLE READ是不会发生脏读的,脏读发生的条件是需要事务的隔离级别为READ UNCOMMITTED,所以如果出现脏读,可能就是这种隔离级别导致的。
5.2、 不可重复读
指在一个事务内多次读取同一集合的数据,但是多次读到的数据是不一样的,这就违反了数据库事务的一致性的原则。但是,这跟脏读还是有区别的,脏读的数据是没有提交的,但是不可重复读的数据是已经提交的数据。
从上面的例子可以看出,在A的一次会话中,由于会话B插入了数据,导致两次查询的结果不一致,所以就出现了不可重复读的问题。
我们需要注意的是不可重复读读取的数据是已经提交的数据,事务的隔离级别为READ COMMITTED,这种问题我们是可以接受的。
如果我们需要避免不可重复读的问题的发生,那么我们可以使用Next-Key Lock算法(设置事务的隔离级别为READ REPEATABLE)来避免,在MySQL中,不可重复读问题就是幻读问题。
5.3、幻读(Phantom read)
幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。注意幻读仅专指“新插入的行”,中途通过 update 更新数据而出现同一个事务前后两次查询的「结果集合」不一样,这种不算幻读。
在可重复读隔离级别下,普通的查询是快照读(read view),是不会看到别的事务插入的数据的。
-
可重复读隔离级是由 MVCC(多版本并发控制)实现的,实现的方式是启动事务后,在执行第一个查询语句后,会创建一个视图,然后后续的查询语句都用这个视图,「快照读」读的就是这个视图的数据,视图你可以理解为版本数据,这样就使得每次查询的数据都是一样的。
MySQL 里除了普通查询是快照读,其他都是当前读,比如update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。假设要 update 一个记录,另一个事务已经 delete 这条记录并且 提交事务了,这样不是会产生冲突吗,所以 update 的时候肯定要知道最新的数据。
另外,select ... for update 这种查询语句是当前读,每次执行的时候都是读取最新的数据。
因此,要讨论「可重复读」隔离级别的幻读现象,是要建立在「当前读」的情况下。Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了 next-key 锁,就是记录锁和间隙锁的组合。间隙锁,锁的就是两个值之间的空隙,以防止其他事务在这个空隙间插入新的数据,从而避免幻读现象。
注意:next-key lock 锁的是索引,而不是数据本身,所以如果 update 语句的 where 条件没有用到索引列,那么就会全表扫描,在一行行扫描的过程中,不仅给行加上了行锁,还给行两边的空隙也加上了间隙锁,相当于锁住整个表,然后直到事务结束才会释放锁。所以在线上千万不要执行没有带索引条件的 update 语句,不然会造成业务停滞。
5.4、丢失更新
指的是一个事务的更新操作会被另外一个事务的更新操作所覆盖,从而导致数据的不一致。在当前数据库的任何隔离级别下都不会导致丢失更新问题,要出现这个问题,在多用户计算机系统环境下有可能出现这种问题。
如何避免丢失更新的问题呢,我们只需要让事务的操作变成串行化,不要并行执行就可以。
我们一般使用SELECT ... FOR UPDATE语句,给操作加上一个排他X锁。
5.5、for update
for update是一种行级锁,又叫排它锁,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行.如果其它用户想更新该表中的数据行,则也必须对该表施加行级锁。即使多个用户对一个表均使用了共享更新,但也不允许两个事务同时对一个表进行更新,真正对表进行更新时,是以独占方式锁表,一直到提交或复原该事务为止。行锁永远是独占方式锁。
只有当出现如下之一的条件,才会释放共享更新锁:
1、执行提交(COMMIT)语句
2、退出数据库(LOG OFF)
3、程序停止运行
由于InnoDB预设是Row-Level Lock,所以只有「明确」的指定主键,MySQL才会执行Row lock (只锁住被选取的资料例) ,否则MySQL将会执行Table Lock (将整个资料表单给锁住)。