MySQL解决并发事务带来的问题--锁

事务相关:MySQL事务的隔离级别与MVCC详解

一、锁

事务并发执行时可能带来的各种问题,并发事务访问相同记录的情况大致可以划分为3种:

  1. 读-读情况:即并发事务相继读取相同的记录。
  2. 写-写情况:即并发事务相继对相同的记录做出改动。
  3. 读-写或 写-读情况:也就是一个事务进行读取操作,另一个进行改动操作。

1. 脏写问题

写-写情况下会发生脏写问题,而任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行,这个排队的过程其实是通过来实现的。这个所谓的锁其实是一个内存中的结构。在事务执行前是没有锁的,也就是说一开始是没有锁结构和记录进行关联的,如图:在这里插入图片描述当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构 ,当没有的时候就会在内存中生成一个锁结构与之关联。
在这里插入图片描述
锁结构 里有很多信息,不过为了简化理解,我们现在只把两个比较重要的属性拿了出来:

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

如上图所示,当事务T1改动了这条记录后,就生成了一个锁结构与该记录关联,因为之前没有别的事务为这条记录加锁,所以is_waiting属性就是false(获取锁成功,或者加锁成功)。

若在事务T1提交之前,另一个事务T2也想对该记录做改动,那么先去看看有没有锁结构与这条记录关联,发现有一个锁结构与之关联后,然后T2也生成了一个锁结构与这条记录关联,不过锁结构的is_waiting 属性值为 true,表示当前事务需要等待(获取锁失败,或者加锁失败,或者没有成功的获取到锁)
在这里插入图片描述
在事务T1提交之后,就会把该事务生成的锁结构释放掉,然后看看还有没有别的事务在等待获取锁,发现了事务T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting属性设置为 false ,然后把该事务对应的线程唤醒,让它继续执行,此时事务T2就算获取到锁了。

2. 脏读 、 不可重复读 、 幻读问题

读-写或写-读情况可能会发生脏读 、 不可重复读 、 幻读问题。

解决上述问题有两种方案:

  • 方案①:读操作利用多版本并发控制( MVCC ),写操作进行加锁。
    MVCC就是通过生成一个ReadView,然后通过ReadView找到符合条件的记录版本。查询语句只能读到在生成ReadView之前已提交事务所做的更改,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作肯定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本并不冲突,也就是采用MVCC时, 读-写操作并不冲突。
  • 方案②:读、写操作都采用加锁的方式。
    我们的一些业务场景不允许读取记录的旧版本,而是每次都必须去读取记录的最新版本。这样在读取记录的时候也就需要对其进行加 操作,这样也就意味着读操作和写操作也像写-写操作那样排队执行。

采用MVCC方式的话,读-写操作彼此并不冲突,性能更高。

3. 一致性读

事务利用MVCC进行的读取操作称之为 一致性读,或者一致性无锁读 ,有的地方也称之为快照读。

所有普通的SELECT语句在READ COMMITTED、REPEATABLE READ隔离级别下都算是一致性读。一致性读并不会对表中的任何记录做加锁操作,其他事务可以自由的对表中的记录做改动。

4. 锁定读

a. 共享锁和独占锁

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

假如事务T1首先获取了一条记录的S锁之后,事务T2接着也要访问这条记录。如果事务T2想要再获取一个记录的S锁,那么事务T2也会获得该锁,也就意味着事务T1和T2在该记录上同时持有S锁。如果事务T2想要再获取一个记录的X锁,那么此操作会被阻塞,直到事务T1提交之后将S锁释放掉。

假如事务T1首先获取了一条记录的X锁之后,那么不管事务T2接着想获取该记录的S锁还是X锁都会被阻塞,直到事务T1提交。

b. 锁定读的语句

  • 对读取的记录加S锁 :
SELECT ... LOCK IN SHARE MODE;

如果当前事务执行了该语句,那么它会为读取到的记录加S锁,这样允许别的事务继续获取这些记录的S锁,但是不能获取这些记录的X锁。如果别的事务想要获取这些记录的X锁 ,那么它们会阻塞,直到当前事务提交之后将这些记录上的S锁释放掉。

  • 对读取的记录加 X锁 :
SELECT ... FOR UPDATE;

如果当前事务执行了该语句,那么它会为读取到的记录加X锁 ,这样既不允许别的事务获取这些记录的S锁,也不允许获取这些记录的X锁。如果别的事务想要获取这些记录的S锁或者X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的X锁释放掉。

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

一般情况下,新插入一条记录的操作并不加锁,隐式锁可以保护这条新插入的记录在本事务提交前不被别的事务访问。

二、多粒度锁

我们前边提到的锁都是针对记录的,也可以被称之为行级锁或者行锁,对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细。其实一个事务也可以在表级别进行加锁,自然就被称之为表级锁或者表锁,对一个表加锁影响整个表中的记录,我们就说这个锁的粒度比较粗。

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

举例:

  1. 学校要占用教学楼进行考试。此时不允许教学楼中有正在上自习(读)的教室,也不允许对教室进行维修(写)。所以可以在教学楼门口放置一把X锁(类似表级别的X锁)
  2. 有领导要来参观教学楼的环境。校领导考虑并不想影响同学们上自习,但是此时不能有教室处于维修状态,所以可以在教学楼门口放置一把S锁 (类似表级别的S锁)。

问题:我们在对教学楼整体上锁( 表锁 )时,怎么知道教学楼中有没有教室已经被上锁( 行锁 )了呢?
答:通过意向锁(Intention Locks)

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

回到上述例子:

  • 如果有领导要参观教学楼,也就是想在教学楼门口前放S锁(表锁)时,首先要看一下教学楼门口有没有IX锁 ,如果有,意味着有教室在维修,需要等到维修结束把IX锁撤掉后才可以在整栋教学楼上加S锁 。
  • 如果有考试要占用教学楼,也就是想在教学楼门口前放X锁(表锁)时,首先要看一下教学楼门口有没有IS锁或IX锁 ,如果有,意味着有教室在上自习或者维修,需要等到学生们上完自习以及维修结束把IS锁和IX锁 撤掉后才可以在整栋教学楼上加 X锁 。

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

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

三、InnoDB存储引擎中的锁

1. 表级锁

在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时, InnoDB存储引擎是不会为这个表添加表级别的S锁或者X锁的。

在对某个表执行一些诸如ALTER TABLE、DROP TABLE这类的DDL语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDAT的语句会发生阻塞,同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDAT语句时,在其他会话中对这个表执行DDL语句也会发生阻塞。

这个过程其实是通过在 server层使用元数据锁( Metadata Locks ,简称 MDL )东实现的,一般情况下也不会使用InnoDB存储引擎自己提供的表级别的S锁和X锁。

InnoDB存储引擎提供的表级S锁或者X锁相当鸡肋,只会在一些特殊情况下,比方说崩溃恢复过程中用到。

2*. 行级锁

行级锁,也称为记录锁,就是在记录上加的锁。

行锁的类型:

  1. LOCK_REC_NOT_GAP
    我们前边提到的记录锁就是这种类型,也就是仅仅把一条记录锁上,LOCK_REC_NOT_GAP锁是有X锁和S锁之分的。
  2. LOCK_GAP
    仅仅是为了防止插入幻影记录。 如果你对一条记录加了LOCK_GAP锁 (不论是共享LOCK_GAP锁还是独占LOCK_GAP锁 ),并不会限制其他事务对这条记录加LOCK_REC_NOT_GAP或者继续加LOCK_GAP锁。
    如图中为number值为8的记录加了gap锁,意味着不允许别的事务在number值为8的记录前边的间隙插入新记录,其实就是number列的值 (3, 8) 这个区间的新记录是不允许立即插入的。比方说有另外一个事务再想插入一条number值为4的新记录,它定位到该条新记录的下一条记录的number值为8,而这条记录上又有一个 gap锁 ,所以就会阻塞插入操作,直到拥有这个 gap锁 的事务提交了之后,number列的值在区间 (3, 8) 中的新记录才可以被插入。在这里插入图片描述
    问题:给一条记录加了gap锁只是不允许其他事务往这条记录前边的间隙插入新记录,那对于最后一条记录之后的间隙,也就是hero表中number值为20的记录之后的间隙该咋办呢?
    答: 数据页有两条伪记录:Infimum 记录,表示该页面中最小的记录。Supremum 记录,表示该页面中最大的记录。
    为了实现阻止其他事务插入number 值在 (20, +∞) 这个区间的新记录,我们可以给索引中的最后一条记录,也就是number值为20的那条记录所在页面的Supremum记录加上一个LOCK_GAP锁。
    在这里插入图片描述
  3. LOCK_ORDINARY
    既要锁住某条记录,又要阻止其他事务在该记录前边的间隙插入新记录。(包括LOCK_GAP和LOCK_REC_NOT_GAP)
  4. LOCK_INSERT_INTENTION
    一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了所谓的LOCK_GAP锁,如果有的话,插入操作需要等待。事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待。(本质上就是把插入意向锁对应锁结构的is_waiting属性为true)
  5. 隐式锁
    一个事务对新插入的记录可以不显式的加锁(生成一个锁结构),但是由于事务id的存在,相当于加了一个隐式锁。别的事务在对这条记录加S锁或者X锁时,由于隐式锁的存在,会先帮助当前事务生成一个锁结构,然后自己再生成一个锁结构后进入等待状态。

下篇:MySQL解决并发事务带来的问题–锁--锁的内存结构

猜你喜欢

转载自blog.csdn.net/myjess/article/details/115868157