MySQL MVCC(多版本并发控制)

3 阅读4分钟

MySQL MVCC(多版本并发控制)

这个知识点刚开始会比较抽象,但是你前面已经理解了:

  • Java锁
  • 数据库行锁
  • 事务
  • 乐观控制

所以现在会容易很多。


一、先回顾一个问题

我们之前说:

普通查询:

select *
from asset_warrant_info
where id=10001;

不会加锁。

那问题来了:

如果两个事务同时操作:

怎么办?

例如:

数据库:

订单
id=10001
status=待支付

事务A:

begin;

select *
from orders
where id=10001;

看到:

待支付

事务B:

同时:

begin;

update orders
set status='已支付'
where id=10001;

commit;

这时候事务A再次查询:

会看到什么?

如果直接读取最新数据:

第一次:

待支付

第二次:

已支付

同一个事务里面看到的数据变化了。

这叫:

不可重复读


所以MySQL需要一种机制:

让事务中的查询结果保持一致。

这就是:

MVCC


二、MVCC是什么?

全称:

Multi-Version Concurrency Control

中文:

多版本并发控制

简单理解:

MySQL不会直接覆盖旧数据。

而是:

保存多个版本。

例如:

订单:

最开始:

status=待支付

版本1。

后来:

事务B修改:

status=已支付

MySQL内部:

类似:

版本2:
status=已支付

版本1:
status=待支付

旧版本不会立即删除。


所以:

事务A:

查询的时候:

可以看到:

适合自己的那个版本。


三、MVCC解决什么问题?

核心:

读不加锁,提高并发。

如果没有MVCC:

读取:

必须:

select
  |
加锁
  |
读取

这样:

大量查询会互相等待。

有MVCC:

读取:

select
  |
找历史版本
  |
返回

不用锁。


四、这个和ConcurrentHashMap有什么相似?

你之前问:

ConcurrentHashMap为什么get不用加锁?

这里很像。

ConcurrentHashMap:

写:

需要控制。

读:

尽量无锁。

MySQL:

修改:

需要锁。

查询:

利用MVCC:

不用锁。

思想:

都是:

读多写少场景,提高并发。


五、MySQL怎么保存这些版本?

InnoDB靠两个东西:

1. 隐藏字段

每一行数据实际上有一些隐藏字段:

例如:

用户表

id
name

实际上类似:

id
name
trx_id
roll_pointer

trx_id

表示:

哪个事务修改了这条数据。

例如:

事务100修改:

name=张三

记录:

trx_id=100

roll_pointer

指向旧版本。

像链表:

当前版本

 ↓

旧版本

 ↓

更旧版本

六、undo log在这里发挥作用

之前讲:

undo log用于回滚。

但是它还有一个作用:

保存历史版本。

例如:

原数据:

余额=1000

事务A:

执行:

update account
set balance=900;

数据库:

当前:

balance=900

undo log:

保存:

balance=1000

如果其他事务需要看旧数据:

从undo log找。


七、事务隔离级别和MVCC关系

MySQL有4种隔离级别:

READ UNCOMMITTED

READ COMMITTED

REPEATABLE READ

SERIALIZABLE

我们重点看两个。


1. READ COMMITTED(读已提交)

意思:

只能看到别人已经提交的数据。

例如:

事务A:

修改:

余额=900

但是没commit。

事务B:

查询:

看不到。

等A:

commit。

B再次查询:

看到:

900。


2. REPEATABLE READ(可重复读)

MySQL默认。

特点:

同一个事务:

多次查询:

结果一致。

例如:

事务A开始:

时间点T1

查询:

余额1000

事务B修改:

余额900
commit

事务A再次查询:

仍然:

余额1000

因为:

MVCC找到旧版本。


八、那什么时候会看到最新数据?

比如:

你的事务:

begin;

select * 
from account
where id=1;

普通select:

走MVCC。

但是:

如果:

select *
from account
where id=1
for update;

这个不同。

它不是快照读。

它是:

当前读。

意思:

我要最新真实数据。

所以:

需要加锁。


九、两种读取方式

这个非常重要。

1. 快照读

普通:

select *
from table;

特点:

  • 不加锁
  • 使用MVCC
  • 读取历史版本

2. 当前读

例如:

select *
from table
for update;

或者:

update
delete
insert

特点:

  • 读取最新数据
  • 需要锁

十、结合你的审批场景

比如:

审批页面查询:

select *
from asset_warrant_info
where id=10001;

这是:

快照读。

用户查看资料:

不需要锁。


点击:

“提交审批”:

执行:

update asset_warrant_info
set status='审批中'
where id=10001
and status='待补充';

这是:

当前修改。

MySQL:

加锁。


所以一个业务流程:

可能同时用了:

查询页面
   |
   | MVCC
   |
展示数据


提交按钮
   |
   | 行锁
   |
修改状态

十一、面试怎么回答MVCC?

如果问:

MySQL如何实现高并发读取?

可以回答:

InnoDB通过MVCC实现非阻塞读。每条记录内部维护事务版本信息,并结合undo log保存历史版本。普通select通过Read View判断可见性,读取符合事务隔离级别的数据,而update、delete等写操作通过行锁保证并发修改安全。