MVCC的设计思想:copyonwrite思想
在并发读写数据库时,读操作可能会不一致的数据(脏读)。
为了避免这种情况,需要实现数据库的并发访问控制,最简单的方式就是加锁访问。
由于,加锁访问,不光阻塞了写,也阻塞了读,会将读写操作串行化,当然,不会出现不一致的状态。
但是,读操作会被写操作阻塞,大幅降低读性能。
copyonwrite思想
在java concurrent包中,有copyonwrite系列的类,专门用于读多写少场景。
copyonwrite的思想是:
在进行写操作时,将数据copy一份,不会影响原有数据,然后进行修改,修改完成后原子替换掉
旧的数据,而读操作只会读取原有数据。
copyonwrite的优势是:
通过这种方式实现写操作不会阻塞读操作,从而优化***读效率***。
copyonwrite的缺点是:
写操作之间是要互斥的,并且每次写操作都会有一次copy,所以,copyonwrite,更多只适合读操
作远多于写操作场景。
MVCC的原理
MVCC的原理与copyonwrite类似,全称是***Multi-Version Concurrent Control***,即多版本并发控制。
在MVCC协议下,每个读操作, 会看到一个***一致性***的snapshot 快照,并且可以实现***非阻塞***的读。
读这个snapshot 快照,不用加锁。
除了snapshot 快照版本之外, MVCC允许数据具有多个版本,版本编号可以是时间戳,或者是全局递
增的事务ID,在同一个时间点,不同的事务看到的数据是不同的。
下面是一个简单的例子, 以事务ID为版本号:
------------------------------------------------------------------------------------------> 时间轴
|-------Read(T1)-----|
|-----------Update(T2)-----------
如上图,假设有两个并发操作Read(T1)和Update(T2):
Read(T1)和Update(T2)的操作如下:
Read:read a =1(T1)
Update:a = 2 (T2)
Read:read (读操作)的版本T1表示要读取数据的版本,
在时间轴上,Read 早于Update,由于Update在Read 开始之后,所以Update提交的数据,所以对于
Read 是不可见的。
所以,Read只会读取T1版本的数据,即a = 1。读不到 a =2
Update 写操作才会更新数据的版本,读操作不会。
关于MVCC数据的一致性
而对于读操作而言,只能读到在自己之前开始的,所有已经提交的写操作,正在执行中的写操作对其是
不可见的。
由于在update操作提交之前,不能影响已有数据的一致性,所以不会改变旧的数据,
另外,update操作会被拆分成insert + delete。
update操作需要标记删除旧的数据,insert新的数据。只有update提交之后,才会影响后续的读操作
mysql的innodb引擎是如何实现MVCC的
什么是 MVCC 多版本并发控制(Multi-Version Concurrent Control)呢 ?
其实就是 innodb会为每一行添加两个字段,注意是在每一行记录的后面增加两个隐藏列:
创建版本号
删除版本号
分别表示该行创建的版本和删除的版本,填入的是事务版本号(事务的编号),
事务版本号随着事务的创建不断递增
下面是一个简单的例子:
1、在插入操作时 : 记录的创建版本号就是事务版本号(事务id )。
插入一条记录, 事务版本号(事务id )假设是1 ,那么记录如下:
id | name | create version | delete version |
---|---|---|---|
1 | test | 1 |
也就是说,创建版本号就是事务版本号(事务id )。
2、在更新操作的时候,采用的是先标记旧的那行记录为已删除,就是说,删除版本号是事务版本号,
然后插入一行新的记录的方式。
比如,针对上面那行记录,事务Id为2 要把name字段更新
update table set name= 'new_value' where id=1
id | name | create version | delete version |
---|---|---|---|
1 | test | 1 | 2 |
1 | new_value | 2 |
3、删除操作的时候,就把事务版本号作为删除版本号。比如
delete from table where id=1;
id | name | create version | delete version |
---|---|---|---|
1 | new_value | 2 | 3 |
4、查询操作:
select from table where id=1;
从上面的描述可以看到,在查询时要符合以下两个条件的记录才能被事务查询出来:
- 1)该行的创建版本号小于等于当前版本号(也就是,行的创建版本本号小于或等于事务版本号),
这样可以确保事务读取的行:
只么是在事务开始前已经存在的
要么是事务自身插入或者修改过的。 - 2)该行的删除版本号大于当前版本或者为空。
这可以确保事务读取到的行,在事务开始之前未被删除。
通过前面的 3个事务, 目前的版本记录,具体如下
|id |name |create version |delete version|
|----|---|---|--|
|1 |test |1|2|
|1|new_value|2|3|
假设执行 这个查询的 select from table where id=1; 事务编号为 4
那么 该行的创建版本号 1/2 小于等于当前版本号 4 的记录,有两条。
该行的删除版本号 2/3大于当前版本或者为空,的, 0条。
所以, select from table where id=1; 查不到数据
在repeated read的隔离级别下,MVCC具体的实现:
select:
满足以下两个条件innodb会返回该行数据:
该行的创建版本号小于等于当前版本号,保证改行在当前版本之前已经被插入。
该行的删除版本号大于当前版本或者为空。删除版本号大于当前版本意味着当前版本并未执行该行
的删除操作,是之后才删除的。
insert:
将新插入的行的创建版本号设置为当前系统的版本号。说明该行在当前版本被插入。
delete:
将要删除的行的删除版本号设置为当前系统的版本号。 说明该行在当前版本被删除。
update:
不执行原地update,而是转换成insert + delete。将旧行的删除版本号设置为当前版本号,并将新
行insert同时设置创建版本号为当前版本号。
将旧行的删除版本号设置为当前版本号,并将新行insert同时设置创建版本号为当前版本号。
其他规则
执行insert、delete和update都要将系统版本号递增。 执行select的版本号为系统版本号。
由于旧数据并不真正的删除,所以必须对这些数据进行清理,innodb会开启一个后台线程执行清
理工作,具体的规则是将删除版本号小于当前系统版本的行删除,这个过程叫做purge。
总之:通过MVCC 就保证了各个事务互不影响。
数据库并发场景有三种,分别为:
1、读读:不存在任何问题,也不需要并发控制
2、读写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读
3、写写:有线程安全问题,可能存在更新丢失问题
MVCC是一种用来解决读写冲突的无锁并发控制,目标是高并发
MVCC为事务分配单项增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读
该事务开始前的数据库的快照,所以MVCC可以为数据库解决以下问题:
1、在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数
据库并发读写的性能
2、解决脏读、幻读、不可重复读等事务隔离问题,但是不能解决更新丢失问题
从这里也可以体会到一种提高系统性能的思路,就是: 通过版本号+副本的方式,来减少锁的争用。
MVCC的实现,通过保存数据在某个时间点的快照来实现的。
这意味着一个事务无论运行多长时间,在同一个事务里能够看到数据一致的视图。
根据事务开始的时间不同,同时也意味着在同一个时刻,不同事务看到的相同表里的数据可能是不同
的。
MVCC的基本特征:
每行数据都存在一个版本,每次数据更新时,都更新该版本。
修改时Copy出当前版本随意修改,各个事务之间无干扰。
保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)
MVCC并不是MySql独有的,Oracle,PostgreSQL等都在使用。
上面是MVCC的基础流程,在不同的 数据库中,实现的方案都是不同的。
在Mysql 中,是使用 undo log 版本链+ ReadView 快照读 试图 ,实现 MVCC 机制的。