Mysql是如何解决并发问题的?(脏读、幻读、不可重复读)

以前遇到这个问题时经常回答的是事务隔离级别,和如何通过加锁解决脏读、幻读、不可重复读。最近重新整理了一下发现这里其实能说的很多。

并发其实分为

读-读

读-写

写-写

其中读-读是不会产生并发冲突的,因为没有涉及到写操作。

写-写冲突是通过加锁来解决的

读-写冲突是通过mvcc + 间隙锁来解决的

一、首先来看什么是脏读、不可重复读、幻读:

什么是脏读?

脏读又称无效数据的读出,是指在数据库访问中,事务T1将某一值修改,然后事务T2读取该值,此后T1因为某种原因撤销对该值的修改,这就导致了T2所读取到的数据是无效的。

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交(commit)到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据,依据脏数据所做的操作可能是不正确的。

什么是不可重复读?

不可重复读,是指在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。这是由于查询时系统中其他事务修改的提交而引起的。比如事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。

一种更易理解的说法是:在一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据。那么,在第一个事务的两次读数据之间。由于第二个事务的修改,那么第一个事务读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复。

什么是幻读?

幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

一般解决幻读的方法是增加范围锁RangeS,锁定检锁范围为只读,这样就避免了幻读。幻读是不可重复读的一种特殊场景:当事务没有获取范围锁的情况下执行SELECT...WHERE操作可能会发生幻读。

总结:

脏读:读到了其他事务还没有提交的数据。

不可重复读:对某数据进行读取过程中,有其他事务对数据进行了修改(UPDATE、DELETE),导致第二次读取的结果不同。

幻读:幻读是指在同一个事务中,前后两次查询相同的范围时,得到的结果不一致。事务在做范围查询过程中,有另外一个事务对范围内新增或删除了记录(INSERT、DELETE)。

然后来看每种事务隔离级别分别解决了哪些问题,底层原理是什么。

二、对于写-写冲突,mysql通过加锁的方式来解决:

读未提交(Read uncommitted),这种隔离级别下会存在幻读、不可重复读和脏读的问题。

读已提交(Read committed)可以避免脏读。

修改时加排他锁直到事务提交后才释放,读取时加共享锁,读取完释放,事务1读取数据时加上共享锁后,不允许任何事物操作该数据,只能读取,之后1如果有更新操作,那么会转换为排他锁,其他事务更无权参与进来读写,这样就防止了脏读问题。

可重复读(Repeatable reads),可以解决不可重复读。

读取数据时加共享锁,写数据时加排他锁,都是事务提交才释放锁。读取时候不允许其他事务修改该数据,不管数据在事务过程中读取多少次,数据都是一致的,避免了不可重复读问题

串行化(Serializable)可以解决幻读。

采用的是范围锁RangeS RangeS_S模式,锁定检索范围为只读,这样就避免了幻读问题。

Mysql默认可重复读原因(Repeatable reads):

MySQL在主从复制的过程中,数据的同步是通过bin log进行的,就是主服务器把数据变更记录到binlog中,然后再把bin log同步传输给从服务器,从服务器接收到binlog之后,再把其中的数据恢复到自己的数据库存储中。

MySQL的bin log主要支持三种格式,分别是statement、row以及mixed。MySQL是在5.1.5版本开始支持row的、在5.1.8版本中开始支持mixed。

statement和row最大的区别,当binlog的格式为statement时,binlog 里面记录的就是 SQL语句的原文

因为mysql早期只有statement这种binlog格式,这时如果用RC会出现问题:

以上两个事务执行之后,会在bin log中记录两条记录,因为事务2先提交,所以 insert into t1 values(10,99);会被优先记录,然后再记录 delete from t1 where b<100;

这样bin log同步到备库之后,SQL语句回放时,会先执行insert into t1 values(10,99);,再执行 delete from t1 where b<100;

这时候,数据库中的数据就会变成 EMPTYSET,即没有任何数据。这就导致主库和备库的数据不一致了。

而Repetable Read这种隔离级别,会在更新数据的时候不仅对更新的行加行级锁,还会增加GAP锁和临键锁。上面的例子,在事务2执行的时候,因为事务1增加了GAP锁和临键锁,就会导致事务2执行被卡住,需要等事务1提交或者回滚后才能继续执行。

除了设置默认的隔离级别外,MySQL还禁止在使用statement格式的binlog的情况下,使用READCOMMITTED作为事务隔离级别。

接下来是如何解决读-写冲突?

三、InnoDB如何解决脏读、不可重复读、幻读(读-写冲突)?

脏读的解决。

当事务在“读已提交”隔离级别下执行读取操作时,InnoDB获取当前最新的全局事务ID,这个ID表示在当前时刻所有已提交事务的最新状态。InnoDB会检查每个数据行的版本,如果该版本是由一个小于或等于当前事务ID的事务修改的,并且该事务已提交,则这个版本是可见的。这保证了事务只能看到在它开始之前已经提交的数据版本。

不可重复读的解决。

不可重复读指一个事务读取同一行数据两次,但是在两次读取之间另一个事务修改了该行数据,导致两次读取的结果不同。InnoDB 通过使用MVCC 来解决不可重复读的问题。在RR这种隔离级别下,当我们使用快照读进行数据读取的时候,只会在第一次读取的时候生成一个ReadView,后续的所有快照读都是用的同一个快照,所以就不会发生不可重复读的问题了。

幻读的解决。

InnoDB的RR级别中,基于MVCC+ Next-Key Locks(临键锁),是在某种程度上是可以避免幻读的发生的,但是没有办法完全避免,当一个事务中发生当前读的时候,会导致幻读的发生。

在没有写的情况下读-读并发是不会出现问题的,而写-写并发这种情况比较常用的就是通过加锁的方式实现。那么,读-写并发则可以通过MVCC的机制解决。

四、如何理解MVCC?

多版本并发控制。

在并发场景下,可能出现三种情况:

读-读并发

读-写并发

写-写并发

MVCC机制就是来解决读-写并发冲突

总结一下,在InnoDB中,MVCC就是通过Read View+Undo Log来实现的,undo log中保存了历史快照,而Read View 用来判断具体哪一个快照是可见的。

UndoLog是Mysql中比较重要的事务日志之一,UndoLog是一种用于回退的日志,在事务没提交之前,MySQL会先记录更新前的数据到undolog日志文件里面,当事务回滚时或者数据库崩溃时,可以利用 undolog来进行回退。

这里面提到的存在undolog中的"更新前的数据"就是我们前面提到的快照。

那么,一条记录在同一时刻可能有多个事务在执行,那么,undolog会有一条记录的多个快照,那么在这一时刻发生SELECT要进行快照读的时候,要读哪个快照呢? 这是就需要另外几个信息

行记录的隐式字段

数据库中的每行记录中,除了保存了我们自己定义的一些字段以外,还有些重要的隐式字段的:

db_row_id:隐藏主键,如果我们没有给这个表创建主键,那么会以这个字段来创建聚簇索引。

db_trx_id:对这条记录做了最新一次修改的事务的ID

db_roll_ptr:回滚指针,指向这条记录的上一个版本,其实他指向的就是Undo Log中的上一个版本的快照的地址。

因为每次记录变更之前都会先存储一份快照到undolog中,那么这几个隐式字段也会跟着记录一起保存在undolog中,就这样,每一个快照中都有一个db trx id字段表示了对这个记录做了最新一次修改的事务的ID,以及一个db roll ptr字段指向了上一个快照的地址。这样,就形成了一个快照链表

Read View:

在 Read View 中有几个重要的属性

trx_ids,表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。

low_limit_id,应该分配给下一个事务的id 值。

up_limit_id,未提交的事务中最小的事务 ID。

creator_trx_id,创建这个 Read View的事务 ID。

trx_ids中包含了low_limit_id和up_limit_id的信息,其实 trx_ids=[up_limit_id,low_limit_id)(但是需要注意,他并不一定连续,只是会包含up_limit_id,并且小于low_limit_id,比如他可能是5,7,8,11),并且up_limit_id就是表示最低水位,low_limit_id就是表示最高水位。

也就是说,每一次读取数据的时候(RC情况下)都会生成一个ReadView,并且在其中记录上trxids(包含了up_limit_id,low_limit_id)和creator_trx_id.

假设low_limit_id = 8,up_limit_id = 5

当trx id<up limit id,即小于5的事务,说明这些事务在生成ReadView之前就已经提交了,那么该事务的结果就是可见的。

当trx id>low limit id,即大于8的事务,说明该事务在生成 ReadView后才生成,所以该事务的结果就是不可见的。

当up_limit id<trx id<low limit id,即大于等于5,小于8,这种情况下,会再拿事务ID和Read View中的trx_ids进行逐一比较。

如果,事务ID在trx_ids列表中,如6,那么表示在当前事务开启时,这个事务还是活跃的,那么这个记录对于当前事务来说应该是不可见的。

如果,事务id不在trx_ids列表中,如7,那么表示的是在当前事务开启之前,其他事务对数据进行修改并提交了,所以,这条记录对当前事务就应该是可见的。当然这里有个例外情况,那就是这个trx_id=creator_trx_id,那么就肯定是可见的。

总结一下就是,一个事务,能看到的是在他开始之前就已经提交的事务的结果,而未提交的结果都是不可见的。

如果不可见,就需要用到undolog了

从undolog里面获取数据的历史快照,然后数据快照的事务ID再来和Read View进行可见性比较,如果找到一条快照,则返回,找不到则返回空。

把这些全部串起来才算真正理解了事务隔离级别的作用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值