说实话,这方面内容和我的兴趣毫无任何关系,本人天性就讨厌这些相对而言理论化的东西,特别是在如此的复杂环境之下更是让人头疼。不过明天就要考试了,也许是我历史上的倒数第二次考试,没有办法,潜下心来啃啃这本传说中的经典书籍Jim Gray的Transaction Processing: Concepts and Techniques.
这里主要谈一谈Isolation。
在ACID中,Isolation就是里面的那个I,其实本来这个词应该有更多更好的理解方法,比如书上说从解决问题的角度来讲,可以考虑其为Concurrency Control,从技术的角度上来说,就是一个locking的机制。Isolation的本来含义是希望在有很多很多不同的线程进行调用时,确保每一个线程的执行都相当于它是一个人在单独进行,没有受到其它线程的干扰,得到其应该得到的值。
在这里,我们可以将所有对数据库的操作分为两大类型,读和写。对于读而言,它的操作仅仅只是获取值,而不会更改值,而写操作则是要更新数据的状态,带来副作用。一般而言,对于很多线程同时进行读操作,是不会产生任何问题的。但是只要其中嵌入了一个小小的写,那么一切就变得复杂起来了。不同的顺序的操作会带来很多不同的问题。我们主要将其归纳为以下三类问题:(从这个翻译可以看出中文显得有多尴尬了吧:))
1)遗失更新(Lost Update):T1读-》T2写-》T1写
对于某个操作,T1首先读取,然后根据读到的值,进行操作之后,把结果写回到这个值中去。但是在T1读完这个值后,T2对其进行了操作更新,也就是说T1这个时候的计算的依据已经不是最新的数据了,T2写的这个值就直接等于遗失掉了。举个通俗的例子,比如我给你的银行账户的值翻倍,你有100块可以变成200块。但是在我读了100以后,有个人给你打了50块,你的账户应该是150了,但是我翻倍之后直接吧200写了进去。那个50块的操作就没影了。也就是所谓的遗失更新。
2)脏读取(Dirty Read):T1写-》T2读-》T1写
脏读取更加好理解,T1对数据更新是需要一个过程的,我们假设其中有两部分。结果在第一个和第二个之间的时候,T2对数据进行了读操作。显然其读出来的值不是最新的值,是一个脏数据,这个数据是马上需要被替换掉的。还是用账户的例子,账户里本来有100块,我要给A汇出50,然后B给我打进60,所以最终我应该是110。但是由于刚汇出A,就进行了读取,结果读到的账户的值就只有50了。显然是个错误的脏的读取。
3)不可重复读(Unrepeatable Read):T1读-》T2写-》T1读
不可重读的意思不言自明就是两次读取的值不相同。T1在一次操作中需要连续读取两次数据,结果在读取之中的时候,T2进来对数据进行了更新,于是T1读取的数值就不一致了。
注意以上所有的T1和T2都是指的单一一个事务,如果是多个事务的话,就不存在这些问题了。
根据上面的这些概念,我们可以按照数据库是否能够处理以上这些问题将其分类:
一般来说分为四个级别:
级别3:所有的并行化处理都被串行化了,于是任何的以上问题都不会出现。
级别2:没有遗失更新和脏读取的问题,但是可能会存在不可重复读的问题。
级别1:没有遗失更新的问题,但是可能会存在脏读取和不可重复读的问题。
级别0:可能会出现任何一种问题,但是如果另外一个事务的级别在1或者1以上,则该事务不会去读取其脏数据。
在理清楚了Isolation的问题之后,我们来看一下Lock的问题。最常见的Lock有两种,一种是共享锁(SLOCK),一种是专有锁(XLOCK)。在持有共享锁的时候,其他的事务可以对这个数据可以读不能写,也就是说可以给它附加共享锁但是不能加上专有锁。而如果一个事务持有某个数据的专有锁,则任何其他事务都不能对这个数据附加任何锁。
在这之前,有个简单的概念可以讲一下。我们可以吧对数据库的一系列操作视为这些操作的序列:BEGIN,COMMIT,ROLLBACK.READ,WRITE,XLOCK,SLOCK,UNLOCK。对于任何一个操作序列,我们可以将其简化为只含有后面五个操作的序列:
比如BEGIN->SLOCK A->XLOCK B->READ A->WRITE B->COMMIT
我们可以吧BEGIN和COMMIT去掉,然后将COMMIT改换为所有对LOCK操作的解锁,即:
SLOCK A->XLOCK B->READ A->WRITE B->UNLOCK A->UNLOCK B
在比如BEGIN->SLOCK A->READ B->XLOCK B->WRITE B->ROLLBACK
对于所有的ROLLBACK操作,我们需要将序列中的WRITE操作再次进行,相当于滚回原状态,然后再释放锁,即:
SLOCK A->READ B->XLOCK B->WRITE B->WRITE B->UNLOCK A->UNLOCK B
但是在这些操作过程中,我们还可能会碰到另外一个问题,所谓的虚读(Phantoms)。
这个问题会发生在上锁的时候,我们对于一个学生数据库中的所有男生进一个UPDATE操作,但是这个时候又转学来了一个学生,于是无论如何上锁,这多出来的一个男生始终无法受到约束。在这种情况下,我们需要使用谓词锁(Predicate Lock)。在使用谓词锁的时候,任何插入或者删除数据的事务,只要满足这个谓词,就无法进行操作。从某个层面上,我们可以看到这个锁对数据进行了进一步的细化。
但是谓词锁也存在几个问题,最主要的一个问题就是计算量的巨大,因为每次在操作之前,都需要对所有的谓词进行比较判断。这样即严重的影响了数据库的效率。
另外一个可以解决的办法和谓词锁很相似,被称之为粒度锁(Granularity of Locks)。这个概念实际上是在谓词锁的基础之上,在操作之前,首先定义好了所有的谓词结构,将其排放成一个树状。这样任何的操作都可以很迅速地找到自己的位置。比如一个数据库,我们将其分为三层,分别是地点,文件和记录。每一层都有父子关系:
最上一层是数据库,所有的数据都属于这一层,因此这一层的PREDICATE永远是TRUE。
下面一层是地理位置,分为中国,澳洲,美国。那么分别满足这个谓词的可以在这层上是TRUE。
接下来是文件层,每一个地理位置都有不同的文件层。比如在中国有档案文件,户口文件。
然后最下面一层是记录,比如户口文件中有张三的记录,有李四的记录。
于是我们在上锁的时候,只需要按照层次进行划分即可。比如我现在需要操作的是所有的中国的档案文件,那么谓词就是地点=“中国” AND 文件=“档案”,如果我需要的是李四的户口,那么谓词就是地点=“中国” AND 文件=“户口” AND 记录=”李四”。
通过对粒度的控制,我们可以随意的掌握锁的大小来更好的调整资源。但是在这个状况下,我们又产生了一个新的问题,比如我锁定了某个儿子,但是如果其父亲要求更改其值怎么办?于是我们定义一种新的锁,称之为意向锁(Intent Lock)。当我们需要锁定一个儿子的时候,我们需要将其之上的祖先全部附加意向锁。比如我们需要给中国的户口文件附上共享锁,在这之前我们需要对整个数据库,地点=“中国”这两个谓词附加上意向锁,最后才对户口文件附加共享锁。而当一个数据被赋予意向锁之后,它不能在被附加任何的共享锁或者专有锁,当然其他的事务也可以继续对其附加意向锁。
紧接着,我们又可以看到另外一个问题。如果一个事务需要对儿子附加共享锁,而另一个事务需要对父亲附加共享锁,其实这两个操作是没有冲突的,但是因为意向锁的原因,第二个事务必须等待第一个事务操作结束才能执行。于是我们通过将意向锁继续细化的办法来解决这个问题。
意向锁被分为三类分别是:
意向专有锁(IXLOCK:可能对其儿子附加共享锁或者专有锁
意向共享锁(ISLOCK):可能对其儿子附加共享锁
共享意向专有锁(SIXLOCK):自己本身是共享锁,但是希望对儿子附加专有锁
我们同时还介绍另外一种锁,称之为更新锁(UPDATE LOCK)。更新所的目的在于避免死锁。更多的关于死锁的内容下面再讨论。
将所有的这些锁进行一个图标的总结:
|
|
None
|
S
|
X
|
IS
|
IX
|
SIX
|
|
S
|
+
|
+
|
-
|
+
|
-
|
-
|
|
X
|
+
|
-
|
-
|
-
|
-
|
-
|
|
IS
|
+
|
+
|
-
|
+
|
+
|
+
|
|
IX
|
+
|
-
|
-
|
+
|
+
|
-
|
|
SIX
|
+
|
-
|
-
|
+
|
-
|
-
|