目录
1. 什么是 MVCC
2. MVCC 是否彻底解决了事物的隔离性
3. MySQL 中如何实现共享锁和排他锁
4. MySQL 中如何实现悲观锁和乐观锁
MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种多版本并发控制机制,它通过给每一个操作加上一个版本号,来解决它的隔离性问题!(主要是用来解决幻读问题)
它使用了快照读的方式,将历史版本的结果集保存到缓存中(ReadView),那么后续需要查询结果集的时候,就不会进行实时的查询(当前读),而是从历史版本中找到与当前查询操作所对应的版本号匹配的结果集。(版本号是全局共享的)
MVCC 虽然主要用来解决幻读问题的,它也是可以解决不可重复读问题的,并且 MVCC 是 MySQL 内置的一个机制,先来看看 MVCC 是如何解决不可重复读问题的:
数据库中原数据:
客户端 1 执行如下操作:
// 1.开启事物 A [版本 1]
begin;
// 2.查询 user 表中 id 为 3 的用户身份信息 [版本 1]
select * from user where id = 3;
// 3.等事物 B 执行修改操作后,再次查询 id 为 3 的用户 [取缓存中的版本 1]
select * from user where id = 3;
客户端 2 执行如下操作:
// 开启事物 [版本 1]
begin;
// 等事物 A 执行完第一次查询操作时,将 id 为 3 的用户名改为王老五 [版本 2]
update user set name = '王老五' where id= 3;
// 提交事物
commit;
// 此时可以再开一个客户端 3,验证一下数据库中是否已经是最新数据
MVCC 的核心思想:
MVCC 将每个事物的读和写操作进行解耦,通过保存数据的历史版本来实现并发控制。每个事物在开始的时候会创建一个读视图(ReadView),用于确定在事物开始时可见的数据版本。在 MVCC 中,当一个事物执行写操作时,会生成一个新的数据版本,并将旧版本的数据保存在回滚日志(Undo Log)中。这样其他事物在读取数据时,仍然可以访问到旧版本的数据,从而避免了幻读问题和不可重复读问题。
(历史版本的数据就类似于对之前查询出来的结果集拍张照,在当前事物还没执行完,还需要查询时,直接从缓存中拿那个版本的照片)
在面试中如果被问到这个问题了,那么回答没有彻底解决。虽然 RR 隔离级别中通过 MVCC 解决大部分幻读问题,但是依然存在幻读问题。
请看示例,原数据:
客户端 1 执行如下操作:
// 事物 A
begin;
select * from user where id > 0 and id < 9;
// 等待事物 B 执行完新增操作后,使用下面两种方式进行查询
select * from user where id > 0 and id < 9; [快照读]
select * from user where id > 0 and id < 9 for update; [当前读]
客户端 2 执行如下操作:
begin;
// 事物 B 执行新增操作
insert into user(id,name,age,address) values(4,'老六',66,'广州');
commit;
最终执行结果:
【解决方案】既然 MVCC 也不能解决幻读问题,那么该怎么解决幻读问题 ??
MySQL 中如何加锁,使用 for update (既是当前读,也是加锁),示例:
select * from user where id > 0 and id < 9 for update; // 排他锁(写锁)
当事物 A 在执行时,进行锁表操作,那么事物 B 开启事物后,想要执行新增操作,但是事物 A 已经执行锁表操作了,所以事物 B 尝试获取锁,发现获取不到,一段时间后就放弃了,所以显示获取锁超时的错误(默认超时时间为 50s)。
共享锁也称为读锁,它允许多个事物同时获取同一数据的共享锁,用于读数据。
语法: select... lock in share mode
select * from user wehre id = 1 lock in share mode;
排他锁也称为写锁,它只允许一个事物获取排他锁,用于修改数据。
语法: select .... for update
select * from user where id = 1 for update;
1. 悲观锁
悲观锁就是通过加上 for update 进行锁表的操作实现的
2. 乐观锁
MySQL 没有内置乐观锁的实现,需要在业务代码中通过手动指定版本号来实现。