写记录加锁,读基于快照读,并且事务中每个语句有独立的快照,确保读到最新的事务提交,解决了脏读的问题,但不解决可重复读问题,当然也无法避免幻读,ReadSkew&WriteSkew等问题。
3.REPEATABLE READ
提到REPEATABLE READ隔离级别,不得不提到SNAPSHOT,一般主流数据库里面都不提SNAPSHOT隔离级别,但是实际实现的时候又都是基于SNAPSHOT来做的,但这里又有一些细微的区别。对于MySQL(InnoDB)而言,读的时候仍然是快照读,相对于READ-COMMITED隔离级别,是一个事务一个快照,确保可重复读,也不存在幻读问题;但是写的时候,采用的当前读,也就是更新的时候,不再考虑快照,而是基于最新的版本来更新,这样就可能会造成LostUpdate问题。当然,解决办法也很简单,事务内的读也采用当前读,这样也就避免了LostUpdate问题。这里举个例子:假设t是一张库存表,pk='iphone'是主键,卖出一部iphone就减去一个库存,count=count-1;假设有两种写法
case1:
begin:
select var = count from t where pk = 'iphone';
var = var - 1;
update count = var from t where pk = 'iphone';
commit;
case2:
begin:
update count = count - 1 from t where pk = 'iphone';
commit;
对于case1,就会发生LostUpdate,试想下如果两个同类型的事务并发,快照读读到的是old count,就可能出现覆盖写的问题,导致库存少减了。
对于case2,则不会有LostUpdate问题,update场景下,读都是当前读,在RR隔离级别下,会加写锁,确保能读到最新的count。
对于MySQL(RocksDB)而言,读一样是基于同一个快照;写的时候,仍然是基于快照读(这个与RocksDB的LSM存储结构有关,只能基于一个快照去读取多版本数据),那么要更新记录时候,会判断记录中的版本是否比事务的快照版本新(ValidateSnapshot),如果是,说明在事务获取快照后,有其它事务执行了更新操作,这个时候事务会回滚,也就不会发生LostUpdate问题。PG也是采用类似的机制,与MySQL(InnoDB)的本质区别在于,写的时候,是基于快照读去写,而还是基于当前读去写。最终的效果是,MySQL(InnoDB)在RR隔离级别下,也会存在LostUpdate问题,同时因为快照读和当前读混用(select, select ... for update),实际上严格来说,也就没有解决幻读和可重复读的问题。Oracle没有实现RR隔离级别,只提供RC和SERIALIZABLE隔离级别。无论是MySQL(InnoDB,RocksDB),PG都没有解决WriteSkew问题。
4.SERIALIZABLE
最严格的隔离级别,自然是没有“异常”的,我们前面也说到,为了提供系统的并发度,才选择通过降低数据库的隔离级别,但必需要容忍部分“异常”。串行化解决了脏读/写,丢失更新,幻读,不可重复读,以及ReadSkew&WriteSkew等问题。MySQL(Innodb)通过将所有所有读都变为当前读,并结合(GAP,Next-Key,InsertIntention)lock来实现串行化隔离,PG则是事务提交时,根据readset和writeset检查是否与其它事务之间有读写依赖成环,最终确定事务能否提交。MySQL(Rocksdb)只支持RC和RR,不支持串行化隔离级别。下图来源于论文,整理了不同隔离级别对应的异常。
总结
本文结合论文和主流的数据库系统讨论了数据库的隔离级别。一般来说,生产环境中设置ReadCommit的居多,文章中也提到了,在读提交隔离级别下,会存在有不可重复读,幻读以及Read/Write Skew等问题。说明,生产环境是可以“容忍”这些“异常”的。当然,这不能说明隔离级别不重要,如果某些业务场景,不能容忍“异常”,就比如我文章中提到的减库存的例子,如果业务代码写法不正确,就可能导致问题。总之,我们需要在系统的并发度和隔离级别做一个权衡,确保业务正确的前提下,得到最好的性能。
Linux公社的RSS地址:https://www.linuxidc.com/rssFeed.aspx