BDB 事务隔离等级

Posted by zhangxiaojian on August 20, 2014

事务的隔离性是数据库很重要的一个方面。它保证事务之间对相同数据库操作的正确性。常常用在多线程程序中,而保证隔离性又需要对资源的竞争,因此应对不同等级的要求,也对隔离性分为不同的等级。等级越低,对数据的保护性越差,但是对资源的竞争越小。需要对各种等级有所了解,才能在使用中找到平衡。

Degree1:Read Uncommitted

1.描述

隔离等级最低的一种,保证一个事务不会重写 另一个事务写过但是尚未commit 的数据。也就是write操作会加写锁,之后其它事务的write操作就会阻塞,一直等待前一个事务完成操作,将锁释放。但是read操作绝不会阻塞,如果事务read操作之前有其它事务的write操作,就会读到write操作修改过但是尚未提交的数据。也就是read操作不需要等写锁释放。如果事务read操作之后有其它事务的write操作,write操作同样不会阻塞,也就是说read操作不会加写锁直到事务commit。来看一个示意图:

2

3

上面的图是一个正常的执行流程,Txn1使用正常的写操作,假设把数据从1改写为2,之后Txn2读刚刚写过数据页,那么读到的数据将是2.但是下面的图的情况也不可避免,假设在Txn2读到数据2之后,Txn1因为某种情况异常,事务回滚。从1修改到2的操作并没有写入数据库,数据库中从未出现过2这个数据,但是确读到了!读取了脏数据!我们为低等级的隔离付出了代价。但是它不会有锁竞争。在多线程程序中不需要等待。吞吐量很大。

2.使用配置:

  • 在打开数据库的时候,提供DB_READ_UNCOMMITTED标记,让数据库支持这种操作。

  • 在创建事务,打开游标,和直接读操作中提供DB_READ_UNCOMMITTED标记。

3.使用情况和完整例子:

用在需要高吞吐,相应速度快的程序中,但是需要保证事务绝对不会abort。使用单线程模拟多线程的一个完整例子

Degree2: Read Committed

1.描述

拥有Degree1的保护,不会重复写数据。并且保证不会出现读到尚未commit的数据,也就是说如果write操作在前,read操作需要阻塞等待写锁的释放,才能读到数据。如下图:

4

如果图中的read操作在前,write操作在后,不会出现阻塞,因为read操作不会一直加读锁直到事务commit。这种隔离等级下,会出现少量的锁竞争,但保证了读到的数据都在数据库中存在。

但是并不完美,上段所述的write操作是直接调用Db的put函数(在这里有描述)。如果write操作使用游标来实现,如上图右下角所示,对page1,page2和page3都进行了写操作,最终游标停留在page3上。再调到另一个线程执行Txn2的read操作,此时read操作读page1和page2上面的数据是不会阻塞的,因为Degree2等级只保证游标所在的page在事务commit前不会被读到。如果游标离开了,那么在page上的写锁也会随之释放。问题就又回到了与Degree1相同的场景,会读到未committed的数据!

2.使用配置

  • 创建事务的时候指定DB_READ_COMMITTED标记。

  • 打开游标的时候指定DB_READ_COMMITTED标记。

3.使用情况和完整例子

使用游标向着一个方向对数据库进行读写,不会回头去访问已经读写过的数据,这时可以使用Read committed。因为游标走过的page都不会再被访问,那么留着锁不放反而会是一种浪费。

Queue类型数据库的一个完整例子  &&  Recno 数据库的一个完整例子

分开两种数据库,是因为Queue数据库提供record级别的锁,而Recno是page级别的锁。(注:一个record就是一个key-value值,而一个page上通常会包含多个record)

Degree4:Default

1.描述

首先介绍默认等级隔离,再介绍Degree3,其实在官方文档中也只列出了以上三种。第四种当作介绍来说的。

数据库默认的隔离等级是最高的,上述Read Committed中说到游标读写完成之后就会释放锁,导致可能读到脏数据,默认隔离等级游标离开锁也不会释放,它能够保证两个事务之间完全不会互相影响。一个事务在操作,另一个就只能等待(两个都read操作没问题,因为都是加读锁)。

这也就导致系统因锁竞争吞吐量下降很大。并发度是最低的。

2.使用配置

默认的,不配置=配置

3.使用情况和完整例子

用在对并发度要求不高的情况。完整例子

Degree3: Snapshot

1.描述

Degree2因为游标离开释放锁,导致可能读到脏数据,Degree4因为游标离开并不释放锁,导致直到事务提交才会把锁释放,并发性很差。snapshot(快照)隔离就是两者中的一个折中解决方案。如图:

5

平常见的最多的“快照”就是百度快照,因为百度抓取的页面可能不是最新的,抓取时的一个版本。甚至有些源页面已经不存在了,还可以通过百度快照找到历史页面。这其中核心思想相同,就是数据冗余。如上图所示,假设read操作在前,write操作在后,读的时候用游标读,并且配置snapshot隔离。那么在读的时候就会复制一份page到内存中,并释放锁。接下来的写操作可以是默认的隔离等级,它可以直接对page1的源页面进行write操作,不需要阻塞等待。 Degree1和Degree2的不足之处都是可能会读到数据库中从未存在过的脏数据。看看snapshot怎么处理:假设write操作在前,将它配置为snapshot隔离,写之前的数据为1,写之后修改为2。在写之前同样会生成一份copy,数据此时都是1,write操作会作用于copy的page中,将1改为2。然后另一线程进行read操作,它读到的将是1,这样就解决了读脏数据的问题。

snapshot隔离显然不会有读写的竞争,因此并发性非常好。但是要付出空间上的代价。

2.付出的代价

每对一个page操作就会生成一份copy,BDB缓存空间会因此快速的减少。如果缓存空间不足以存储冗余的page,就会将page作为临时文件存储在磁盘上。不必要的磁盘IO就会发生,这会大大降低系统的运行速度,甚至不如default隔离等级。

系统提供DbEnv::log_archive()方法用来估计需要的缓存大小,大约是剩余日志文件大小的两倍。这个方法获得的是文件的路径和文件名,要获得文件大小进行判断。方法详细讲解 点这里

除了多余的空间,snapshot还需要更多的产生更多的事务支持。系统默认支持20个活动的事务。因为page的冗余,可能在每个page上的事务就会不止一个,而且到冗余的page全部提交之后才会commit事务,这就导致默认的20个可能不够用。 we need more.

3.使用配置

  • 当打开环境或者数据库的时候提供DB_MULTIVERSION标记参数。使数据库支持“多版本”

  • 当打开事务或者游标的时候提供DB_TXN_SNAPSHOT标记参数。(注不是DB_SNAPSHOT参数,这个坑了我好久)

4.使用情况和完整例子

  • 有一个比较大的缓存空间

  • 需要重复的读

  • 数据库中的部分数据会频繁的被多个事务更改

  • 系统中读写竞争降低了吞吐量,或是线程大部分是只读的,但是对锁分配器的竞争降低了吞吐

完整例子点这里

总结

文中参考了官方文档的部分内容,加上自己写代码验证形成此文。理解是一个方面,到能够通过正确降低隔离等级提高并发性又是另外一个方面。需要更多的使用和深入思考。