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

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

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

    一致性读(Consistent Reads)

    事务利用MVCC进行的读取操作称之为一致性读,或者一致性无锁读,有的地方也称之为快照读。所有普通的SELECT语句(plain SELECT)在READ COMMITTED、REPEATABLE READ隔离级别下都算是一致性读,比方说:
    SELECT FROM t; SELECT FROM t1 INNER JOIN t2 ON t1.col1 = t2.col2
    一致性读并不会对表中的任何记录做加锁操作,其他事务可以自由的对表中的记录做改动。

    锁定读(Locking Reads)

    锁分了个类:

  • 共享锁,英文名:Shared Locks,简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁。

  • 独占锁,也常称排他锁,英文名:Exclusive Locks,简称X锁。在事务要改动一条记录时,需要先获取该记录的X锁。
兼容性 X S
X 不兼容 不兼容
S 不兼容 兼容

对读取的记录加S锁:

SELECT … LOCK IN SHARE MODE;

对读取的记录加X锁:

SELECT … FOR UPDATE;

写操作

平常所用到的写操作无非是DELETE、UPDATE、INSERT这三种:

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

    多粒度锁

    意向锁(英文名:Intention Locks):

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

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

    MySQL中的行锁和表锁

    对于MyISAM、MEMORY、MERGE这些存储引擎来说,它们只支持表级锁,而且这些引擎并不支持事务,所以使用这些存储引擎的锁一般都是针对当前会话来说的

    InnoDB存储引擎中的锁

    InnoDB中的表级锁

    表级别的S锁、X锁
    但是DDL语句时,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行DDL语句也会发生阻塞。这个过程其实是通过在server层使用一种称之为元数据锁(英文名:Metadata Locks,简称MDL)东东来实现的,一般情况下也不会使用InnoDB存储引擎自己提供的表级别的S锁和X锁。
    表级别的IS锁、IX锁
    表级别的AUTO-INC锁
    在使用MySQL过程中,我们可以为表的某个列添加AUTO_INCREMENT属性,之后在插入记录时,可以不指定该列的值,系统会自动为它赋上递增的值

系统实现这种自动给AUTO_INCREMENT修饰的列递增赋值的原理主要是两个:

  • 采用AUTO-INC锁,也就是在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。这样一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。

如果我们的插入语句在执行前不可以确定具体要插入多少条记录(无法预计即将插入记录的数量),比方说使用INSERT … SELECT、REPLACE … SELECT或者LOAD DATA这种插入语句,一般是使用AUTO-INC锁为AUTO_INCREMENT修饰的列生成对应的值。

  • 采用一个轻量级的锁,在为插入语句生成AUTO_INCREMENT修饰的列的值时获取一下这个轻量级锁,然后生成本次插入语句需要用到的AUTO_INCREMENT列的值之后,就把该轻量级锁释放掉,并不需要等到整个插入语句执行完才释放锁。

    InnoDB中的行级锁

    1. mysql> SELECT * FROM hero;
    2. +--------+------------+---------+
    3. | number | name | country |
    4. +--------+------------+---------+
    5. | 1 | l刘备 | |
    6. | 3 | z诸葛亮 | |
    7. | 8 | c曹操 | |
    8. | 15 | x荀彧 | |
    9. | 20 | s孙权 | |
    10. +--------+------------+---------+
    11. 5 rows in set (0.01 sec)
    image.png
    Record Locks:
    比方说我们把number值为8的那条记录加一个正经记录锁的示意图如下:
    image.png
    正经记录锁是有S锁和X锁之分
    Gap Locks:
    比方说我们把number值为8的那条记录加一个gap锁的示意图如下:
    锁住(3,8)
    image.png
    gap锁的提出仅仅是为了防止插入幻影记录而提出的,虽然有共享gap锁和独占gap锁这样的说法,但是它们起到的作用都是相同的

这时候应该想起我们在前边唠叨数据页时介绍的两条伪记录了:

  • Infimum记录,表示该页面中最小的记录。
  • Supremum记录,表示该页面中最大的记录。

image.png

Next-Key Locks:

有时候我们既想锁住某条记录,又想阻止其他事务在该记录前边的间隙插入新记录

image.png

Insert Intention Locks

我们说一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了所谓的gap锁(next-key锁也包含gap锁,后边就不强调了),如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。但是设计InnoDB的大叔规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待。设计InnoDB的大叔就把这种类型的锁命名为Insert Intention Locks,官方的类型名称为:LOCK_INSERT_INTENTION,我们也可以称为插入意向锁。
image.png
image.png

隐式锁

一个事务在执行INSERT操作时,如果即将插入的间隙已经被其他事务加了gap锁,那么本次INSERT操作会阻塞,并且当前事务会在该间隙上加一个插入意向锁,否则一般情况下INSERT操作是不加锁的。

一个事务对新插入的记录可以不显式的加锁(生成一个锁结构),但是由于事务id这个牛逼的东东的存在,相当于加了一个隐式锁。别的事务在对这条记录加S锁或者X锁时,由于隐式锁的存在,会先帮助当前事务生成一个锁结构,然后自己再生成一个锁结构后进入等待状态。

InnoDB锁的内存结构

image.png