深入理解mysql的锁和mvcc

基础

1 锁讲解:面试必备-行锁、表锁 - 乐观锁、悲观锁的区别和联系

2 mvcc机制讲解(如何实现各种隔离级别):数据库基础(四)Innodb MVCC实现原理

刚读完上面两篇内容,可能会有很多疑惑的地方,下面是我自己整理出来的一些疑惑点:

1 rr级别如何防止幻读

“RR” 是 “Repeatable Read”(可重复读)的缩写,它是数据库的四种标准隔离级别之一,其余三种是 “Read Uncommitted”(读未提交),“Read Committed”(读已提交)和 “Serializable”(串行化)。不同的隔离级别提供了不同的并发控制,可以在 “数据一致性” 和 “并发性能” 之间进行权衡。

可重复读(Repeatable Read,RR)隔离级别保证了在一个事务中多次读取同一数据的结果是一致的,也就是说,该级别的事务不会看到其他事务对该数据的修改。这可以防止 “不可重复读” 的问题,即一个事务在读取同一行数据的两个不同时间点上看到了不同的数据。

然而,即使在 RR 隔离级别下,也可能会出现 “幻读”(Phantom Read)问题。幻读是指在一个事务处理过程中,第一次和最后一次查询的结果不一致。这种现象并不是由于读取的记录的值被其他事务更改引起的,而是由于其他事务插入或删除了一些记录,导致我们看到了“幻影”记录。

防止幻读的最主要的方法是使用最高级别的隔离级别——串行化(Serializable)。在串行化级别下,事务是完全串行执行的,自然就不存在幻读问题。但是这种级别的隔离在并发性能上有很大的损失。

在实践中,有一些数据库系统(如 MySQL 的 InnoDB 存储引擎)会通过 “Next-Key Locking” 来在 RR 级别下防止幻读。Next-Key Locking 是一种锁定策略,它不仅锁定了查询所访问的索引记录,还锁定了索引中的一个“间隙”(Gap Locking),这样就可以防止其他事务在该“间隙”中插入新的记录,从而避免了幻读的产生。

注意:Next-Key Locking 策略可能导致锁冲突增多,可能降低并发性能。在实际应用中,需要根据实际情况权衡使用。

2 next-lock(临键锁)和gap-lock(间隙锁)

两者的区别

Next-Key Locking 和 Gap Locking 都是数据库管理系统(例如 MySQL 的 InnoDB 引擎)在可重复读(Repeatable Read,RR)隔离级别下防止幻读的技术。它们都涉及到了对数据的一种“范围锁定”,即不仅锁定实际访问到的记录,还锁定记录之间的“间隙”。两者的主要区别在于锁定的精细度。

  1. Next-Key Locking:Next-Key Locking 是 InnoDB 默认的行锁定方式,它是一种组合锁定,包括了行锁(record lock)和间隙锁(gap lock)。具体来说,如果一个事务 T1 访问了一个索引记录 R,那么 Next-Key Locking 会对 R 的“键值-间隙”和 R 本身进行锁定。这样,不仅阻止了其他事务修改 R,也阻止了其他事务在 R 的键值-间隙内插入新的记录。Next-Key Locking 可以有效地防止幻读。

  2. Gap Locking:Gap Locking 只锁定索引记录之间的间隙,而不锁定记录本身。例如,如果一个事务 T1 访问了索引记录 R1 和 R2,那么 Gap Locking 会锁定 R1 和 R2 之间的间隙,但并不锁定 R1 和 R2 本身。这意味着其他事务可以修改 R1 和 R2 的内容,但不能在 R1 和 R2 之间插入新的记录。Gap Locking 主要用于实现读取的一致性和防止幻读,但它不防止不可重复读。

Next-Key Locking 更加保守,它提供了更强的数据一致性保证,但可能会降低并发性能。而 Gap Locking 则更加宽松,它允许对已有记录的并发修改,但仍然可以防止幻读。在实际应用中,应根据具体需求选择合适的锁定策略。

利用临键锁可以完美解决幻读的问题,面试官问了一下真的是完美的吗?

总结:安全上完美,性能上有缺陷

"完美"可能是一个过于绝对的表述,因为即使使用了临键锁,也不能解决所有的并发问题,而且它会带来其他的开销。

临键锁是InnoDB在可重复读隔离级别下用于防止幻读的一种锁,它可以防止在某个范围内插入新的记录,这确实可以防止幻读的发生。然而,这并不能解决所有的并发问题。例如,临键锁不能防止"写偏斜"问题,即当两个事务都读取相同的数据并试图修改它们时可能发生的问题。

此外,临键锁会增加锁的开销,可能导致性能降低。临键锁需要锁定一定范围的记录,而不仅仅是单个记录,这可能会阻止其他并发的事务访问这个范围内的记录,即使这些事务并不直接与被锁定的记录冲突。

所以,虽然临键锁可以有效地解决幻读问题,但是否能称之为"完美",需要考虑到它对并发性和性能可能带来的影响。

可重复读是由行锁来保证的吗

可重复读(Repeatable Read,RR)的保证并不仅仅依赖于行锁。在实际的数据库系统中,RR 隔离级别通常需要结合使用多种技术来实现,包括行锁,间隙锁(Gap Lock),多版本并发控制(MVCC,Multi-Version Concurrency Control)等。

行锁可以防止同一行数据被其他事务并发修改,但行锁并不能防止“幻读”(Phantom Read)问题。幻读是指在一个事务内,两次查询的结果不一致,而这种不一致并不是由于查询的记录本身被修改,而是由于新的记录的插入或旧的记录的删除。

为了解决幻读问题,许多数据库系统会使用间隙锁(Gap Lock)。间隙锁可以锁定两个索引记录之间的“间隙”,阻止其他事务在这个间隙中插入新的记录。

而多版本并发控制(MVCC)则是另一种重要的技术,它可以允许多个读事务同时访问同一行数据,而不会互相阻塞。在 MVCC 中,每个写操作都会创建一个数据行的新版本,读操作则会访问到一个适当的旧版本。这样,每个事务都可以在一致的快照中运行,就像它是唯一在运行一样。

所以,可重复读隔离级别通常需要行锁、间隙锁、MVCC 等多种技术共同实现。

来自MVCC链接的一个问题

原文: MVCC通过保存数据的历史版本,根据比较数据的版本号来决定数据的是否显示,在不需要加读锁的情况就能达到事务的隔离效果,最终可以在读取数据的时候可以同时进行修改,修改数据时候可以同时读取,极大的提升了事务的并发性能。

问题:当写时数据进行读取,读取时进行修改,不会产生并发问题嘛?比如同时有两个请求打进来,一个读一个写,但是写先执行,读后执行,这个读操作到底是应该在当前版本的数据上进行操作呢还是在写操作结束后进行?

答: 你对多版本并发控制(MVCC,Multi-Version Concurrency Control)的理解是正确的。MVCC 通过保存数据的历史版本,并在读取数据时选择适当的版本,可以让读取操作和修改操作同时进行,从而提高事务的并发性能。

对于你提到的问题,“当写时数据进行读取,读取时进行修改,不会产生并发问题吗?”,实际上在 MVCC 模型中是有相应的规则和策略来处理这种情况的。

在 MVCC 中,每个事务在开始时都会获得一个唯一的事务ID(也被称为版本号)。当一个事务想要读取一个数据项时,它会看到的是版本号不大于它自己事务ID的最新版本。当一个事务想要修改一个数据项时,它会创建一个新的版本,其版本号是这个事务的事务ID。

因此,对于你的问题,假设有两个事务T1(写)和T2(读),其中T1的事务ID小于T2的事务ID。如果T1先执行,它会创建一个新的数据版本,版本号为T1的事务ID;然后当T2执行时,它会看到的是版本号不大于T2事务ID的最新版本,也就是T1创建的那个版本。

这样,即使读取操作和修改操作并发进行,也不会出现数据一致性问题,因为每个事务都是在一致的数据快照中运行的。在这种模式下,读操作不会阻塞写操作,写操作也不会阻塞读操作,从而实现了高并发性能。

但请注意,虽然 MVCC 可以提高并发性能,但它并不能解决所有的并发问题。例如,它不能解决两个事务同时修改同一数据项的问题。这种情况通常需要使用锁或其他并发控制机制来处理。

事务T1是对自己持有的版本数据做更改嘛?这种情况下,T2读取的到数据是不是有可能是更改过后的?

在MVCC(Multi-Version Concurrency Control)的机制中,事务在进行写操作(例如更新一个数据项)时,会创建该数据项的一个新版本(其实该新版本还是放在数据行中,原来的版本位于undo log行,参考mvcc链接2.4的demo),而不是直接修改原始数据。新版本的版本号等于该事务的事务ID。原始数据的版本会被保留,以便其他还在使用旧版本数据的事务能够继续访问。

假设有两个事务T1和T2,T1是一个写事务,T2是一个读事务,它们的事务ID分别是ID1和ID2,并且ID1 < ID2。当T1想要修改一个数据项时,它会创建一个新的数据版本,版本号为ID1。当T2想要读取这个数据项时,它会看到的是版本号不大于ID2的最新版本。因为ID1 < ID2,所以T2可以看到T1创建的新版本。

但是,具体T2能看到哪个版本的数据,也取决于T1和T2的开始时间以及MVCC的实现策略。例如,如果T2开始时T1还没有提交,那么T2可能只能看到T1修改之前的数据版本;如果T2开始时T1已经提交,那么T2可能就能看到T1修改后的数据版本。

需要注意的是,MVCC能确保的是事务的一致性视图,也就是在一个事务中,不管数据是否被其他事务修改,它看到的数据总是一致的。对于并发的读写操作,是否会有冲突,要看具体的并发控制策略和隔离级别。在某些情况下,可能需要使用锁或其他同步机制来防止并发冲突。

如何理解“MVCC能确保的是事务的一致性视图,也就是在一个事务中,不管数据是否被其他事务修改,它看到的数据总是一致的”

“MVCC能确保的是事务的一致性视图,也就是在一个事务中,不管数据是否被其他事务修改,它看到的数据总是一致的”,这句话的意思是:在多版本并发控制(MVCC)的机制下,每一个事务看到的都是一致的、不变的数据快照,这个快照取决于该事务开始的时间点。这就是所谓的"一致性视图"。

这是如何实现的呢?当事务开始时,它会获得一个唯一的事务ID,这个ID也代表了事务开始的时间点。当这个事务想要读取一个数据项时,它会看到的是版本号不大于它自己事务ID的最新版本。也就是说,这个事务只能看到它开始时存在的数据版本,不能看到在它开始之后其他事务创建的新版本。即使在该事务执行过程中,其他事务对数据进行了修改,这个事务看到的数据也不会改变。

例如,假设事务T1在时间点1开始,此时它读取到的数据项A的版本为1。在T1执行过程中,另一个事务T2在时间点2修改了数据项A,创建了版本2。然而,即使数据项A的实际版本已经变为2,T1仍然能看到的是版本1的数据,因为版本1是在T1开始时存在的版本。因此,无论数据如何变化,T1看到的数据总是一致的。

这种特性使得在MVCC机制下,读操作不需要阻塞写操作,写操作也不需要阻塞读操作,可以提高并发性能。同时,由于每个事务都在一致的数据快照中运行,所以也能保证事务的一致性。

在正常使用锁机制的情况下,写操作T1和读操作T2,如果T1先执行,那么T2就只能阻塞等待T1执行完毕后读取到更新后的数据呢?

在使用锁机制的情况下,如果一个事务(T1)获取了对某个数据项的写锁,那么其他所有试图读取或者写入这个数据项的事务(比如 T2)都会被阻塞,直到 T1 释放了这个锁。所以,在这种情况下,T2 的确会等待 T1 执行完毕并释放锁之后,才能读取到更新后的数据。

这种阻塞等待的情况可能会导致并发性能下降,特别是在高并发的系统中。为了解决这个问题,一种方法是使用非阻塞的并发控制机制,比如 MVCC(Multi-Version Concurrency Control)。在 MVCC 中,读操作不会阻塞写操作,写操作也不会阻塞读操作,因此可以提高并发性能。

然而,虽然 MVCC 可以提高并发性能,但它也有一些限制和开销,比如需要维护多个版本的数据,需要处理版本冲突等问题。因此,在实际应用中,应该根据具体的需求和环境,选择合适的并发控制策略。

如果写操作T1中间执行完事务,但是T2在T1结束后又读取了一次该数据,此时读取的是提交后的数据嘛还是提交前的呢?

在多版本并发控制(MVCC)的模型中,事务开始时获取一个事务ID,该ID同时也是一个时间戳,代表了事务开始的时间点。这个事务对数据的所有读取操作,都只会看到其开始时间(即事务ID)之前的数据版本。

如果写事务T1在执行过程中提交了,那么它对数据的修改将会生成一个新的数据版本。这个新的版本的版本号将会等于T1的事务ID。

对于读事务T2来说,如果它在T1提交之前就已经开始了,那么即使T1提交了新的数据版本,T2依然只能看到它开始时那一刻的数据版本,也就是说,它看到的仍然是T1提交前的数据。

但是,如果T2是在T1提交之后才开始的,那么T2在读取数据时,会看到T1提交的新版本数据,因为这个版本的数据是在T2开始之前就已经存在的。

所以,T2读取的是提交后的数据还是提交前的数据,取决于T2的开始时间是在T1提交之前还是之后。这就是MVCC模型能够提供一致性视图的原理。

关于read view的问题

材料:

2.5 Read view 匹配条件:

1、数据事务ID <up_limit_id 则显示

如果数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。

2、数据事务ID>=low_limit_id 则不显示

如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view
创建之后才产生的,所以数据不予显示。

3、up_limit_id <=数据事务ID<low_limit_id 则与活跃事务集合trx_ids里匹配

如果数据的事务ID大于最小的活跃事务ID,同时又小于等于系统最大的事务ID,这种情况就说明这个数据有可能是在当前事务开始的时候还没有提交的。

所以这时候我们需要把数据的事务ID与当前read view 中的活跃事务集合trx_ids 匹配:

情况1: 如果事务ID不存在于trx_ids 集合(则说明read view产生的时候事务已经commit了),这种情况数据则可以显示。

情况2: 如果事务ID存在trx_ids则说明read
view产生的时候数据还没有提交,但是如果数据的事务ID等于creator_trx_id
,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。

情况3: 如果事务ID既存在trx_ids而且又不等于creator_trx_id那就说明read
view产生的时候数据还没有提交,又不是自己生成的,所以这种情况下此数据不能显示。

4、不满足read view条件时候,从undo log里面获取数据

当数据的事务ID不满足read view条件时候,从undo log里面获取数据的历史版本,然后数据历史版本事务号回头再来和read
view 条件匹配 ,直到找到一条满足条件的历史数据,或者找不到则返回空结果;

如何理解这里的“数据事务ID”,是指当前的读事务id嘛,为什么事务号不是最大的呢,事务id不应该是自增的嘛?

在MySQL的InnoDB引擎中,事务采用了多版本并发控制(MVCC)机制。在这个机制下,每个数据行都有一个关联的事务ID,代表了这个数据行版本的创建者。当一个事务修改数据行时,它会创建一个新版本的数据行,而新版本的事务ID就是该事务的ID。因此,"数据事务ID"指的是创建这个数据行版本的事务ID。

对于你的第一个问题,"数据事务ID"并不是当前读事务的ID,而是创建当前版本数据行的事务的ID。InnoDB引擎确实使用了一个自增的事务ID来标识事务,但是你需要理解的是,每个事务在它开始时获取的ID,是在那个时间点的最大值,后来开始的事务获取的ID将会更大。所以,如果一个数据行的事务ID(即创建它的事务的ID)小于read view中的最小活跃事务ID,那么我们可以确定,这个数据行是在当前事务开始之前就已经创建了的,因此可以被当前事务看到。

这里的“该数据”是指表中正在存储的当前最新版本的记录嘛?

对于你的第二个问题,"该数据"是指当前读事务试图访问的数据行的某个版本。这个版本可能并不是数据行的当前最新版本,因为在MVCC中,读事务可能会看到旧版本的数据行,具体取决于读事务的开始时间和read view的内容。这就是MVCC能够提供一致性视图的方式。

在下面这种情况下,可能数据事务ID是一个提前执行完的写操作,对不对

数据事务ID>=low_limit_id 则不显示

如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view
创建之后才产生的,所以数据不予显示。

在你提到的这种情况下,"数据事务ID"可能确实是由一个已经提交的写事务创建的。如果数据行的事务ID大于read view的low_limit_id,说明这个数据行的版本是在当前读事务开始后才被创建的。因此,即使这个写事务已经提交,这个数据版本仍然不会被当前的读事务看到,因为在MVCC中,每个事务只能看到它开始时刻及之前的数据版本。

这时read view中的活跃事务链表都是读事务嘛,有没有写事务呢?

Read view中记录的活跃事务列表包括了在该读事务开始时刻,所有还没有提交的事务,不论这些事务是读事务还是写事务。如果是写事务,那么它可能在之后修改数据,创建新的数据版本,但这些新版本的数据不会被当前的读事务看到。如果是读事务,那么它不会修改数据,但是为了防止出现幻读(phantom read)问题,需要将其包含在活跃事务列表中

为什么将读事务放入read view活跃事务链表中就能防止幻读问题呢?

幻读是一种并发控制问题,当一个事务在读取一些行,然后另一个事务插入了一些新的行,再回头读取时,原事务发现有新的“幻影”行出现。如果将读事务放入read view的活跃事务链表中,那么其他并发的读事务就无法看到该写事务未提交的变更,因此在一致性视图中它不会看到新的“幻影”行,从而防止了幻读问题。这是因为mvcc的规范决定的:一个事务只能看到版本号比当前事务id小于等于的版本数据

为什么更新行的事务id还可以存在于活跃事务链表中,既然都更新了行,不应该代表已经提交了吗?

首先这里的"更新了行"只是指将旧版本的数据挪到了undo log日志中,并且新版本的数据就占据了当前的数据行,似乎是更新了,但是还没有提交,所以read view时本身也可能在活跃事务链表中。

MVCC的缺点相关

什么是write skew?

写偏是一个更复杂的现象(并发写的问题),它涉及到两个或多个事务同时读取相同的数据,然后基于读取的数据修改它们。这可能导致数据库处于不一致的状态。例如,两个事务都读取了同一个数据,然后都基于读取的数据进行了修改,可能会导致一些冲突和数据的不一致。要解决写偏,可能需要采取更高级的并发控制机制,例如使用乐观锁(Optimistic Locking)或悲观锁(Pessimistic Locking)

举个例子:
如果有两个座位,且两个用户(事务A和事务B)各自预定一个座位,那么座位数应该会变为0,当A和B同时读取剩余座位数(2个),然后都决定订一个座位,每个人都认为预订后应剩下一个座位。但是当两个事务都提交的时候,由于他们都是从2开始减去了一个座位,结果会是1个座位,而不是0。这就是所谓的write skew问题。

Read Skew 呢?

Read Skew(读偏斜): 在同一个事务中,多次读取同一数据项返回的结果不同。也就是上面Read-Write Skew中提到的“不可重复读”问题。

MVCC有什么缺陷?

mvcc并不能解决多个写事务的并发问题(不能解决write skew问题),只能用于解决读-写事务之间的并发问题。

为什么mvcc并不能解决多个读写复合事务之间或者写和写事务之间的并发问题

事务的类别

只读、只写、读写复合事务,一般mvcc只能解决只读和只写事务,或者只读和只读事务之间的并发问题,但是解决不了复合事务间或者只写和只写事务之间的并发问题

对于同时包含读和写操作的事务,MVCC同样能够很好的处理读操作。但是,对于写操作,如果写操作依赖于事务中的读操作,那么就可能出现问题。这就是所谓的"读-写偏斜"(read-write skew)

如何解决mvcc的write-skew问题或者写-写的事务问题?

1 让事务串行化执行(串行化隔离级别),但是并发度很低
2 数据库中使用带版本号的乐观锁,这样每次都只会有一个事务执行成功
3 可以使用分布式锁,让每次只放行一个事务去数据库中修改这个数据行或者相关业务
4 悲观锁:悲观锁假设并发事务会导致数据不一致,因此在修改数据前先上锁,防止其他事务并发修改。在我们的银行账户例子中,可以在检查余额和修改余额之间对账户进行加锁。这样,只有当一个事务完成后,另一个事务才能继续,从而避免了write skew问题。但是,悲观锁可能导致并发性能下降。

MVCC的应用场景

mvcc和其他的锁一样都只能保证事务的隔离性,并不能保证业务上存在依赖的事务的安全对吗?

是的,MVCC只能保证事务的隔离性,即并发执行的事务不会互相干扰。但是,它不能保证业务上存在的依赖关系。这需要在业务逻辑层面进行处理。例如,在秒杀场景中,即使使用了MVCC,也可能需要使用乐观锁或悲观锁等技术来保证商品不会超卖。

快照读和当前读,在秒杀场景中,应用哪个

在秒杀场景中,我们通常需要保证库存的正确性,因此需要实时地知道当前的库存状态,也就是我们需要"当前读"。快照读虽然能提供较高的并发性能,但是由于它是基于某个时间点的数据快照,可能无法反映出实时的库存状态,因此可能不适合秒杀这种对实时性要求较高的场景。

快照读取的可能是旧数据,存在安全问题吗?

快照读取的数据可能是旧的,这在一些场景下可能会带来问题。以秒杀为例,如果我们基于快照读取的库存数据来进行下单操作,那么可能会出现超卖的情况。比如说,商品的实际库存只有1,但是在多个事务的快照读中,它们都看到库存为1,于是都进行了下单操作,结果导致实际卖出了多件商品,这就是一种安全问题。

但是,这并不是说快照读没有用处。在一些对数据实时性要求不那么高,但对并发性能有较高要求的场景下,快照读可以提供较好的性能。例如,我们要统计用户的购买历史,在这种情况下,数据的实时性就不那么重要,而快照读可以提供更好的并发性能。

关于使用快照读还是当前读,mysql数据库可以设置的吗,粒度精确到表呢还是库呢

在MySQL中,你可以在每个事务的基础上选择使用快照读(也称为一致性读)或者当前读。这是通过设置事务的隔离级别来实现的。例如,如果你选择"READ COMMITTED"隔离级别,那么每次查询都会读取最新的数据(当前读)。而如果选择"REPEATABLE READ"隔离级别,那么在事务开始时就会创建一个数据快照,之后的所有查询都会基于这个快照来读取数据(快照读)。这个设置是针对每个事务的,不能精确到表或库。

猜你喜欢

转载自blog.csdn.net/yxg520s/article/details/131817634