从本源来理解比较容易理解,如果只是描述概念和定义,容易让人云里雾里找不到方向.正好这两天在浏览mysql的文档,我可以简单在这里总结一下,帮助其他还没有理解的朋友,如果有错误也麻烦帮忙指正.
先讲一点背景知识:
首先明确一点,数据库的命令的执行者的封装基本抽象是Transaction,语句的执行都会有对应的Transaction对象,并且都会有对应的id来标识不同的Transaction.Transaction Id按照不同的时序来分配, 通俗点说,时间上来执行的早的transaction id小一点, 晚的transaction id大一点.不是说只有我们手动去start transaction这样才会创建transaction.我们执行一个简单的autocommit类型的语句,比如insert或者update语句,都会对应生成一个新的事务.
所以,所有SQL语句执行皆有其对应的transaction id, transaction id的大小可以表示SQL语句执行发生的早晚.
Mysql提供了多版本读取能力.什么叫多版本?就是说表中一份存储的数据行会有多个版本,这个版本对应的version就是transaction id.当行数据被某个transaction变更时, 这个行会有个隐含的hidden column中记录了这个更新者的transaction id.旧的数据在被覆盖的同时,会存储到一个undolog的文件中,这个文件中就是保存着每一行的版本数据链表,沿着这个链表走我们可以走过这一行数据之前的版本变更.
(这个undolog数据量不需要那么大,比如一个行数据的更新者transaction id小于所有server中活跃的transaction ids,那么这个行数据的undolog中的数据就可以删除掉,因为不会有transaction来去查询这个行记录的undolog)
一,现在来说什么是可重复读:
当开始一个新的transaction,这个transaction会分配一个新的transactionid,此时在这个transaction内部如果我们执行一条普通的select语句,根据select语句的condition,我们会找到一些满足条件的行记录,先不着急返回:
1.如果此时是mysql默认的repeatable read隔离模式
每个匹配的行记录,我们从前面说的hidden column中找到对应的最近一次执行更新操作的操作者transaction id,我们将这个id(也就是版本),和我们当前的transaction id进行比较,如果大于执行语句所属的transaction的id,那么就需要去undolog文件中去寻找旧的版本,一直找到小于当前transaction id的版本的行记录,这个就作为快照数据返回.其他的行记录都是这样来处理的.
在repeatable read隔离模式中,所有的普通select语句(consistent read)(非select ... for update的语句)都会进行这样的比对过程来返回数据.也就是说,即使在我们当前这个事务执行过程中,有其它事务执行插入了新的数据能够满足我们的查询条件,但因为这个数据的版本(transaction id)大于我们当前事务的id,我们是查询不到这个数据的.
这也就是成为可重复查的原因,不管多少次查询,每次读取的都是同样的版本的数据,也就是字面的意思.
2.如果此时是read committed隔离模式
对于普通的查询select语句不再有上面这个版本的限制,每次查询,只需要返回满足条件的最新的行记录即可.所以此时就不是可重复读,也就是说我们可以看到别的事务提交的数据(所以名字叫read committed嘛)
二, 然后来说什么是幻读:
幻读的字面意思很简单,就是在事务内两次查询语句(注意是select ...for...update语句,不是前面的普通查询语句<consistent read>),读取到了不同的数据.
这个从情理上来看是很正常的时情,首先因为数据库是一个支持大量并发任务的服务,那么我们在事务执行的过程中,新的数据插入并发生变化也是很正常的事情,所以这不是BUG.
注意,这里说到了并发, 在编程语言里面我们知道,出现了并发问题的一个最最常见的情况就是,check and do这样的操作,这样不是并发操作安全的,因为check和do的过程中可能会插入其它的操作,所以当我们进行do操作的时候,先前的条件可能已经不满足了.
那么在编程语言中我们是怎么解决问题的呢?
也就是加锁,对我们关注的数据(面向对象语言中的对象)进行加锁操作,其实也就是独占,我这个任务在拿着这个锁的时候,别的人都靠边站(都阻塞在那里),我这个任务做完了,其它任务才可以去做.
在数据库中也是同样的问题,我们的select..where..condition..for update也是先圈定一个感兴趣的数据(满足condition的行数据),然后进行update操作,在这个事务进行过程中面临的也是并发的问题.
那么参考编程语言的做法,我们的方式也是独占,独占的目标是什么呢?
只是满足条件的行数据吗?
当然不止,比如condition: a>5 and a<10,目前只有一条记录a=7,如果我们只锁这个记录可以吗?不可以,因为其它事务可能还会做新的插入操作,比如插入一条a=9的记录,假如我们的业务逻辑是,如果当前a在5-10的区间中只有一条记录,我们就可以删除这个记录(这个业务逻辑很奇怪,先假设是这样),在上一次查询的时候是只有一条行记录,我们认为满足了条件,现在我们就要删除这个记录,却发现5-10中有了2条记录,那么此时的删除操作就是一个误操作....
数据库解决这个问题的办法就是,对这个区间都加锁,不仅仅是已有的行记录,空的那些a的值[8,9]都会加上锁.也就是gap lock,这个时候我们当前的事务就达到了对这个区间独占的目的,其它的事务在我们处理过程中就无法插入新的数据了: ).
当然, 是否独占,加不加区间的锁,这些mysql给我们了自由选择的权力,在READ COMMITED下,就不会加上区间锁,只会锁住已有的记录,所以此时如果有其它事务插入新的数据,当然也可以成功.如果在REAPEATABLE-READ下,就是会执行上面说的独占操作,其它想在这个区间插入数据的事务就得等在那里了(阻塞住了).
讲到这里,我觉得幻读和可重复读的关系应该理清了,
1.repeatable read模式下,我们执行select...for..update独占了满足条件的记录,阻断了其它事务的变更,不会出现幻读的情况.
在read committed模式下,不独占,会有并发问题,会出现查到新的数据的问题.
阻断幻读的目的往往在于我们有check-and-do这样的业务逻辑需求,来实现更进一步严格的原子性需求.
2.对于可重复读,是数据库多版本的一种福利,帮助我们实现在事务中能够实现锁定时间点读取快照的目的.
最后说一个区间锁的场景,
比如我们需要业务场景中经常会有类似这样一个需求,当表中存在满足某个条件的一行数据,我们就不做.如果不存在我们就插入一条记录.此时,我们就可以利用上面说的解决幻读的办法,使用repeatable模式,因为它会锁定对应的条件,在我们select...for..update过程中,可以保证不会有其它事务插入.这样我们的表中就不会出现因为并发问题,导致无法实现唯一性的问题了.
作者:扣鼎之歌
链接:https://www.zhihu.com/question/38507762/answer/968486962
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。