我们都知道,Mysql Innodb引擎的默认事务隔离级别是RR可重复读,也就是在同一个事务中,多次读取相同的数据结果相同。而其底层就是通过:“排它锁+MVCC”来实现的。
话不多说,我们来看看下面的这个问题:
我们可以看到,上面的事务A在更新数据之前,数据已经被事务B所修改,但是事务A最终提交的时候,将事务B的提交覆盖掉了,导致了事务B的更新丢失。
用我们之前学过的MVCC来理解一下为什么这个问题会发生:
首先由于事务A开启的时候,事务B还没提交,此时A事务进行了一次select操作,导致生成ReadView。根据这张图的原理:
B事务处于trx_ids的事务中,所以A事务无法看到B事务的数据处理过程,即B事务对数据的操作,对A事务不可见。这也正是可重复读的实现的原理。
而恰恰是因为A事务看不到B事务对数据的更新,而A事务本地无论如何都是读到该数据的1000元可重复读的版本,导致A事务后面的更新操作直接在1000元这个版本上对数据添加100,将B事务的更新完全覆盖。
那么如何解决上面的这个问题呢?
我们来看看上面问题的本质,其实就是在事务B操作数据之前,我们就调用了select,导致生成了ReadView,而我们又拿着这个select的数据去做后面的处理,最终导致了B数据的丢失。
看到这里可能有人会想,那等B事务的操作commit了之后再去做A事务的select操作不就不会有这个问题了吗?说的确实没错,按这么操作,B事务确实处在A事务可见区内,最终不会导致B事务的更新丢失。可是在并发情况下,你怎么知道什么时候会突然有个B事务来更新呢?
于是我们最好的做法就是在A事务的查询时,添加一个锁,mysql中的语句如下:
select * from demo where id = 1 for update;
这样在A操作事务前,B事务就无法获取到锁,也就无法进行更新操作了。从而避免了更新丢失。
其实上面的更新丢失情况,术语叫做:第二类更新丢失问题。而解决办法也有多种角度,如上面的加悲观锁方式,或者乐观锁方式(类似CAS)也可以处理此类问题。
不过强哥还是比较喜欢上面悲观锁的方式,方便且简单有效,其他的解决办法有兴趣的同学可以去网上找找我就不在这里说明了。
相信大家还有一个疑问,既然有第二类更新丢失问题,那么肯定也有第一类吧,没错,第一类更新丢失又叫回滚丢失:
不过,各种数据库的各种隔离级别都不允许此类问题的产生,所以就不在这里赘述了。至于数据库是如何避免这种问题的发生,强哥也找了好多资料,可是还是没有获取到有用的原理说明,大多类似如下的解释:
所以,在此也就不胡乱猜测,如果小伙伴们有知道的也可以留言一起讨论下哦~