youyichannel

志于道,据于德,依于仁,游于艺!

0%

重拾MySQL —— 锁

《MySQL是怎样运行的 —— 从跟上理解MySQL》—— 第二十二章

一、解决并发事务带来问题的两种基本方式

并发事务访问相同记录的情况大致可以划分为3种:

  • 读-读情况:即并发事务相继读取相同的记录。
  • 写-写情况:即并发事务相继对相同的记录做出改动。 => 发生脏写问题
  • 读-写写-读情况:也就是一个事务进行读取操作,另一个进行改动操作。 => 发生脏读不可重复读幻读的问题。

1.1 写 - 写情况

在这种情况下会发生脏写的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行,这个排队的过程其实是通过来实现的。这个所谓的其实是一个内存中的结构,在事务执行前本来是没有锁的,也就是说一开始是没有锁结构和记录进行关联的。当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构,当没有的时候就会在内存中生成一个锁结构与之关联。比方说事务T1要对这条记录做改动,就需要生成一个锁结构与之关联:

其实在锁结构里有很多信息,两个比较重要的属性:

  • trx信息:代表这个锁结构是哪个事务生成的。
  • is_waiting:代表当前事务是否在等待。

当事务T1改动了这条记录后,就生成了一个锁结构与该记录关联,因为之前没有别的事务为这条记录加锁,所以is_waiting属性就是false,这个场景就称之为获取锁成功,或者加锁成功,然后就可以继续执行操作了。

在事务T1提交之前,另一个事务T2也想对该记录做改动,那么先去看看有没有锁结构与这条记录关联,发现有一个锁结构与之关联后,然后也生成了一个锁结构与这条记录关联,不过锁结构is_waiting属性值为true,表示当前事务需要等待,这个场景就称为获取锁失败,或者加锁失败,或者没有成功的获取到锁,

在事务T1提交之后,就会把该事务生成的锁结构释放掉,然后看看还有没有别的事务在等待获取锁,发现了事务T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting属性设置为false,然后把该事务对应的线程唤醒,让它继续执行,此时事务T2就算获取到锁了。

1.2 读 - 写 或者 写 - 读情况

这种情况下可能发生脏读不可重复读幻读的问题。

SQL标准规定不同隔离级别下可能发生的问题不一样:

  • READ UNCOMMITTED隔离级别下,脏读不可重复读幻读都可能发生。
  • READ COMMITTED隔离级别下,不可重复读幻读可能发生,脏读不可以发生。
  • REPEATABLE READ隔离级别下,幻读可能发生,脏读不可重复读不可以发生。
  • SERIALIZABLE隔离级别下,上述问题都不可以发生。

不过各个数据库厂商对SQL标准的支持都可能不一样,与SQL标准不同的一点就是,MySQLREPEATABLE READ隔离级别实际上就基本解决了幻读问题。

怎么解决脏读不可重复读幻读这些问题呢?其实有两种可选的解决方案:

  • 方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁
  • 方案二:读、写操作都采用加锁的方式。

采用MVCC方式的话,读-写操作彼此并不冲突,性能更高,采用加锁方式的话,读-写操作彼此需要排队执行,影响性能。但是也要看实际的场景。

1.3 一致性读

事务利用MVCC进行的读取操作称之为一致性读,或者一致性无锁读,有的地方也称之为快照读。所有普通的SELECT语句(plain SELECT)在READ COMMITTEDREPEATABLE READ隔离级别下都算是一致性读

一致性读并不会对表中的任何记录做加锁操作,其他事务可以自由的对表中的记录做改动。

1.4 锁定读

1.4.1 共享锁和独占锁

在使用加锁的方式解决问题时,由于既要允许读-读情况不受影响,又要使写-写读-写写-读情况中的操作相互阻塞,MySQL给锁分了个类:

  • 共享锁Shared Locks,简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁(不严谨)。
  • 独占锁,也常称排他锁Exclusive Locks,简称X锁。在事务要改动一条记录时,需要先获取该记录的X锁
兼容性 X S
X 不兼容 不兼容
S 不兼容 兼容

S锁S锁是兼容的,S锁X锁是不兼容的,X锁X锁也是不兼容的。

1.4.2 锁定读

  • 对读取的记录加S锁

    SELECT ... LOCK IN SHARE MODE;

    如果当前事务执行了该语句,那么它会为读取到的记录加S锁,这样允许别的事务继续获取这些记录的S锁(比方说别的事务也使用SELECT ... LOCK IN SHARE MODE语句来读取这些记录),但是不能获取这些记录的X锁

  • 对读取的记录加X锁

    SELECT ... FOR UPDATE;

    如果当前事务执行了该语句,那么它会为读取到的记录加X锁,这样既不允许别的事务获取这些记录的S锁(比方说别的事务使用SELECT ... LOCK IN SHARE MODE语句来读取这些记录),也不允许获取这些记录的X锁

1.5 写操作

平常所用到的写操作无非是DELETEUPDATEINSERT这三种:

  • DELETE:对一条记录做DELETE操作的过程其实是先在B+树中定位到这条记录的位置,然后获取一下这条记录的X锁,然后再执行delete mark操作。可以把这个定位待删除记录在B+树中位置的过程看成是一个获取X锁锁定读
  • UPDATE,在对一条记录做UPDATE操作时分为三种情况:
    • 如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化,则先在B+树中定位到这条记录的位置,然后再获取一下记录的X锁,最后在原记录的位置进行修改操作。可以把这个定位待修改记录在B+树中位置的过程看成是一个获取X锁锁定读
    • 如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化,则先在B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+树中位置的过程看成是一个获取X锁锁定读,新插入的记录由INSERT操作提供的隐式锁进行保护。
    • 如果修改了该记录的键值,则相当于在原记录上做DELETE操作之后再来一次INSERT操作,加锁操作就需要按照DELETEINSERT的规则进行了。
  • INSERT:一般情况下,新插入一条记录的操作并不加锁,InnoDB通过隐式锁来保护这条新插入的记录在本事务提交前不被别的事务访问。

二、多粒度锁

细粒度锁:行锁

粗粒度锁:表锁

给表加的锁也可以分为共享锁S锁)和独占锁X锁):

  • 给表加S锁,如果一个事务给表加了S锁
    • 别的事务可以继续获得该表的S锁
    • 别的事务可以继续获得该表中的某些记录的S锁
    • 别的事务不可以继续获得该表的X锁
    • 别的事务不可以继续获得该表中的某些记录的X锁
  • 给表加X锁:如果一个事务给表加了X锁(意味着该事务要独占这个表):
    • 别的事务不可以继续获得该表的S锁
    • 别的事务不可以继续获得该表中的某些记录的S锁
    • 别的事务不可以继续获得该表的X锁
    • 别的事务不可以继续获得该表中的某些记录的X锁

那么在上表锁的时候,需要知道有没有行锁,如何知道?InnoDB使用了意向锁来解决问题:

  • 意向共享锁Intention Shared Lock,简称IS锁。当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁
  • 意向独占锁Intention Exclusive Lock,简称IX锁。当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁

IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录,也就是说其实IS锁和IX锁是兼容的,IX锁和IX锁是兼容的。

兼容性 X IX S IS
X 不兼容 不兼容 不兼容 不兼容
IX 不兼容 兼容 不兼容 兼容
S 不兼容 不兼容 兼容 兼容
IS 不兼容 兼容 兼容 兼容

三、InnoDB是如何加锁的

详情见书

[完整版:Innodb到底是怎么加锁的 (qq.com)](