MySQL锁

在了解MySQL锁之前,首先我们必须要明白加锁的是为了解决什么问题?

我们知道事务具有个隔离性的特性,而隔离性的实现主要就是通过锁以及MVCC机制实现的(关于MVCC机制以及隔离级别的实现可查看文章:MySQL事务详解与隔离级别的实现

MVCC是一种用来解决读写冲突的无锁并发控制,在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能,解决脏读、幻读、不可重复读等问题。当然只是读取不加锁不阻塞,写操作还是会进行加锁的,即MVCC解决的只是读-写的阻塞问题,写-写依然还是阻塞的。对于写写的并发线程问题,仍需要使用锁来保证线程安全。即MVCC机制只是为了提高并发性能,减少加锁的情况

一、锁分类

  • 基于锁的属性分:

    1. 共享锁(S锁,MyISAM叫读锁):当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。
    2. 排它锁(X锁,MyISAM叫写锁):当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取,避免了出现脏数据和脏读的问题。
  • 基于锁的粒度分:

    1. 表级锁:上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问;特点:粒度大,加锁简单,容易冲突;

    2. 页级锁:介于行级锁和表级锁中间的一种锁。表级锁速度快但冲突多,行级冲突少但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。特点:开销和加锁时间界于表锁和行锁之间,会出现死锁;

    3. 行级锁:锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问,特点是粒度小,加锁比较麻烦,但不容易冲突,并发度更高;

      1. 记录锁:行锁的一种算法,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录,加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题;

      2. 间隙锁:行锁的一种算法,该锁会锁定一个范围(左开右闭),但是不括记录本身。间隙锁只会出现在REPEATABLE_READ(重复读)的事务级别中;

      3. 临键锁:行锁的一种算法,是记录锁和间隙锁的组合,会把查询出来的记录和其左区间锁住

        例:如果一个索引有 1, 3, 5, 7 四个值,当等值查询 5 时,临键锁会对( 3,5 ]这个区间进行加锁

  • 基于加锁思想分:

    1. 乐观锁(无锁):对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁,只有到数据提交的时候才通过一种机制来验证数据是否存在冲突
    2. 悲观锁:对于数据冲突保持一种悲观态度,在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的

二、不同语句加锁的属性

  • SELECT … 语句正常情况下为快照读,不加锁;
  • SELECT … LOCK IN SHARE MODE 语句为当前读,加 S 锁;
  • SELECT … FOR UPDATE 语句为当前读,加 X 锁;
  • 常见的 DML 语句(如 INSERT、DELETE、UPDATE)为当前读,加 X 锁;
  • 常见的 DDL 语句(如 ALTER、CREATE 等)加表级锁,且这些语句为隐式提交,不能回滚。

三、隔离级别对加锁的影响

MySQL的隔离级别对加锁有影响,不同的隔离级别下,锁粒度是不一样的:

  • 读未提交:修改数据时加记录锁
  • 读提交:修改数据时加记录锁
  • 可重复读:默认加临键锁,但锁可能会发生退化
  • 序列化:读的时候也会加锁

注意以上都是指的走了索引的查询,因为不走索引的查询都是加的表锁,自然不用考虑用的是哪种行级锁算法

读未提交和读提交在写数据时,不管是否范围查询,都只加记录锁

而对于可重复读来说,分为多种情况:

  • 唯一索引等值查询(以1, 4, 7, 10为例)
    • 查询记录存在:退化成记录锁(如查询7则只锁7这一条记录)
    • 查询记录不存在:退化为间隙锁(如查询9则锁住区间(7, 10),因为9不存在,InnoDB先找到7的记录,发现不匹配继续往下找到10大于了查询记录则停止)
  • 唯一索引范围查询:等同于等值查询,存在则加记录锁,不存在则加间隙锁(但要遍历到第一个不满足的记录)
    • select xxx where id >= 4 and id < 9:这条语句要找的第一行是id=4的记录,本来要加的是(1,4]这个区间的临键锁,因为命中了所以退化为只加4的记录锁,接着不断往后查找第一个不满足的记录,直到找到id=10这条记录,因为id=9是不存在的,所以加了间隙锁(4,10)
  • 非唯一索引等值查询(以1, 4, 7, 10为例)
    • 查询记录存在:左区间加临键锁,有区间加间隙锁(如查询7,则对(4,7]和(7,10)加锁)
    • 查询记录不存在:退化为间隙锁(如查询8则锁住(7,10))
  • 非唯一索引范围查询:不会退化为记录锁或者间隙锁
    • select xxx where id >= 4 and id < 9:同样的语句,但是如果id是非唯一索引,则id=4仍然加的是临键锁,即(1,4]这个区间同样会被上锁,同样的还有(4,10]这个区间也会被加锁

四、上锁机制

  1. InnoDB采用的是两阶段锁定协议,事务执行过程中,根据需要不断的加锁,最后COMMIT或ROLLBACK的时候一次性释放所有锁
  2. 在MySQL中行级锁并不是直接锁记录,而是锁索引
  3. 索引分为主键索引和非主键索引两种,如果一条SQL语句操作了主键索引,MySQL就会锁定这条主键索引;如果操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引
  4. 如果不通过索引条件检索数据,那么InnoDB将对表中所有数据加锁,等同于表锁。因为没有了索引,查询一条记录就得扫描全表数据,要扫描全表就得锁定表

为什么要两个列都加上锁: 如果只给非主键索引上了锁,那么并发事务通过主键进行其他修改操作,那么此操作并不知道该记录已经被另一个事务操作锁定(因为操作了主键索引的SQL直接查询的是主键索引对应的树)

五、意向锁

如果需要⽤到表锁的话,如何判断表中的记录没有行锁呢,如果一行一行遍历性能则太差。因而采用意向锁来快速判断是否可以对某个表使⽤表锁。

意向锁是表级锁,共有两种:

  • 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些加共享锁(S 锁), 加共享锁前必须先取得该表的 IS 锁
    • 意向共享锁与共享锁兼容,与排它锁互斥
  • 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁
    • 意向排它锁与共享锁和排它锁互斥

这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥

意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前, InooDB 会先获取该数据行所在在数据表的对应意向锁。 意向锁之间是互相兼容的。

参考文章:

MySQL死锁系列-常见加锁场景分析 - 孙龙-程序员 - 博客园 (cnblogs.com)

MySQL 四种事务隔离级别 + 锁

MySQL 不同隔离级别,都使用了什么锁?

Q.E.D.


   七岁几胆敢预言自己,操一艘战机