1.背景
https://blog.csdn.net/qq_24516549/article/details/89374174
之前介绍了不加以mvcc的经典情况,下面介绍一下会出现的问题:
Read Committed(读已提交)(这里介绍的是不用mvcc的情况,与真实情况有出入)
事务对当前被读取的数据加 行级S锁,一旦读完该行,立即释放该锁;
事务在修改数据的时候对数据增加行级X排他锁,在事务结束时释放。
Repeatable Read(可重复读)(这里介绍的是不用mvcc的情况,与真实情况有较大出入)
事务对当前被读取的数据加 行级S锁,在事务结束时释放。
事务在修改数据的时候对数据增加行级X排他锁,在事务结束时释放。
问题在于,如果修改数据,就会加上X锁,那么别的读请求都会被阻塞,这个有点太傻了,非常浪费性能;而且其实这种操作并无什么意义,事务是一个原子性的操作,对于事务以外的查询请求来说,事务对数据的修改是在结束的一瞬间完成的(一般不会使用未提交读事务等级),也就是说在A事务提交或回滚前,我别的查询应该只需要看到A事务操作前的数据就可以了;在A事务提交或回滚后,我别的查询才能看到A修改后的最新结果(也就是在没有回滚时A事务对数据的修改结果)
2.解决方案
可以看出,其实重点在于为了保持事务的各种特性,在上一节有提到我们需要加锁,加锁会阻塞读的操作,那么数据库为我们提供一种叫做mvcc的解决方案,对于mysql来说可以应用于Read committed和Repeatable read两种等级
3.MVCC
Multi-Version Concurrency Control 多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存。
如果有人从数据库中读数据的同时,有另外的人写入数据,有可能读数据的人会看到『半写』或者不一致的数据。有很多种方法来解决这个问题,叫做并发控制方法。最简单的方法,通过加锁,让所有的读者等待写者工作完成,但是这样效率会很差。MVCC 使用了一种不同的手段,每个连接到数据库的读者,在某个瞬间看到的是数据库的一个快照,写者写操作造成的变化在写操作完成之前(或者数据库事务提交之前)对于其他的读者来说是不可见的。
当一个 MVCC 数据库需要更一个一条数据记录的时候,它不会直接用新数据覆盖旧数据,而是将旧数据标记为过时(obsolete)并在别处增加新版本的数据。这样就会有存储多个版本的数据,但是只有一个是最新的。这种方式允许读者读取在他读之前已经存在的数据,即使这些在读的过程中半路被别人修改、删除了,也对先前正在读的用户没有影响。这种多版本的方式避免了填充删除操作在内存和磁盘存储结构造成的空洞的开销,但是需要系统周期性整理(sweep through)以真实删除老的、过时的数据。对于面向文档的数据库(Document-oriented database,也即半结构化数据库)来说,这种方式允许系统将整个文档写到磁盘的一块连续区域上,当需要更新的时候,直接重写一个版本,而不是对文档的某些比特位、分片切除,或者维护一个链式的、非连续的数据库结构。
MVCC 提供了时点(point in time)一致性视图。MVCC 并发控制下的读事务一般使用时间戳或者事务 ID去标记当前读的数据库的状态(版本),读取这个版本的数据。读、写事务相互隔离,不需要加锁。读写并存的时候,写操作会根据目前数据库的状态,创建一个新版本,并发的读则依旧访问旧版本的数据。
简单的说就是对于一行数据可能有多个版本,事务在读取时根据当前事务id和时间戳来读合适版本的快照数据,在修改时则会创建一个新版本
有了MVCC,Read Committed和Repeatable Read就的实现就很直观了:
对于Read Committed,每次读取时,总是取最新的,被提交的那个版本的数据记录。
对于Repeatable Read,每次读取时,总是取created_by_txn_id小于等于当前事务ID的那些数据记录。在这个范围内,如果某一数据多个版本都存在,则取最新的。
即使有了mvcc,有时候我们还是需要一些额外手段来保证业务逻辑的正确
比如在实现一个简单的计数器,从数据库中查出数据加1后update回去
在Repeatable Read隔离级别下,我们无法读取别的事务已提交的数据,如果是按照以下逻辑运行,可能会导致错误
事务A读取数据为1
事务A设置数据为1+1=2
事务B读取数据为1
事务B设置数据为1+1=2
事务B执行update x = 2并提交
事务A执行update x = 2并提交
可以看出来,计数器使用了两次然而实际上mysql中的数据
x只加了1
在Read Committed隔离级别下,即使我们能够读取别的事务已经提交的数据,但是我们无法保证在select与update之中x是否被修改,那么我的修改可能会覆盖别的事务的修改,影响整个事务的逻辑
对于这种情况,我有两种解决方案:
- select时手动加锁
读取数据时使用select for update,这样在获得锁的事务提交前,别的事务无法读取该行数据 - update时加乐观锁
更新数据时使用
update x = 2 where x=1;
这样,在x已被修改时update会返回0,即可做相应回滚或响应