2.4.2 并发控制

1.数据库锁

事务分为几种类型:读事务,写事务以及读写混合事务。相应地,锁也分为两种类型:读锁以及写锁,允许对同一个元素加多个读锁,但只允许加一个写锁,且写事务将阻塞读事务。这里的元素可以是一行,也可以是一个数据块甚至一个表格。事务如果只操作一行,可以对该行加相应的读锁或者写锁;如果操作多行,需要锁住整个行范围。

表2-4中T1和T2两个事务操作不同行,初始时A=B=25,T1将A加100,T2将B乘以2,由于T1和T2操作不同行,两个事务没有锁冲突,可以并行执行而不会破坏系统的一致性。

2.4.2 并发控制 - 图1

表2-5中T1扫描从A到C的所有行,将它们的结果相加后更新A,初始时A=C=25,假设在T1执行过程中T2插入一行B,那么,事务T1和T2无法做到可串行化。为了保证数据库一致性,T1执行范围扫描时需要锁住从A到C这个范围的所有更新,T2插入B时,由于整个范围被锁住,T2获取锁失败而等待T1先执行完成。

2.4.2 并发控制 - 图2

多个事务并发执行可能引入死锁。表2-6中T1读取A,然后将A的值加100后更新B,T2读取B,然后将B的值乘以2更新A,初始时A=B=25。T1持有A的读锁,需要获取B的写锁,而T2持有B的读锁,需要A的写锁。T1和T2这两个事务循环依赖,任何一个事务都无法顺利完成。

2.4.2 并发控制 - 图3

解决死锁的思路主要有两种:第一种思路是为每个事务设置一个超时时间,超时后自动回滚,表2-6中如果T1或T2二者之中的某个事务回滚,则另外一个事务可以成功执行。第二种思路是死锁检测。死锁出现的原因在于事务之间互相依赖,T1依赖T2,T2又依赖T1,依赖关系构成一个环路。检测到死锁后可以通过回滚其中某些事务来消除循环依赖。

2.写时复制

互联网业务中读事务占的比例往往远远超过写事务,很多应用的读写比例达到6:1,甚至10:1。写时复制(Copy-On-Write,COW)读操作不用加锁,极大地提高了读取性能。

图2-10中写时复制B+树执行写操作的步骤如下。

2.4.2 并发控制 - 图4

图 2-10 写时复制的B+树

1)拷贝:将从叶子到根节点路径上的所有节点拷贝出来。

2)修改:对拷贝的节点执行修改。

3)提交:原子地切换根节点的指针,使之指向新的根节点。

如果读操作发生在第3步提交之前,那么,将读取老节点的数据,否则将读取新节点,读操作不需要加锁保护。写时复制技术涉及引用计数,对每个节点维护一个引用计数,表示被多少节点引用,如果引用计数变为0,说明没有节点引用,可以被垃圾回收。

写时复制技术原理简单,问题是每次写操作都需要拷贝从叶子到根节点路径上的所有节点,写操作成本高,另外,多个写操作之间是互斥的,同一时刻只允许一个写操作。

3.多版本并发控制

除了写时复制技术,多版本并发控制,即MVCC(Multi-Version Concurrency Control),也能够实现读事务不加锁。MVCC对每行数据维护多个版本,无论事务的执行时间有多长,MVCC总是能够提供与事务开始时刻相一致的数据。

以MySQL InnoDB存储引擎为例,InnoDB对每一行维护了两个隐含的列,其中一列存储行被修改的“时间”,另外一列存储行被删除的“时间”,注意,InnoDB存储的并不是绝对时间,而是与时间对应的数据库系统的版本号,每当一个事务开始时,InnoDB都会给这个事务分配一个递增的版本号,所以版本号也可以被认为是事务号。对于每一行查询语句,InnoDB都会把这个查询语句的版本号同这个查询语句遇到的行的版本号进行对比,然后结合不同的事务隔离级别,来决定是否返回改行。

下面分别以SELECT、DELETE、INSERT、UPDATE语句来说明。

(1)SELECT

对于SELECT语句,只有同时满足了下面两个条件的行,才能被返回:

a)行的修改版本号小于等于该事务号。

b)行的删除版本号要么没有被定义,要么大于事务的版本号。

如果行的修改或者删除版本号大于事务号,说明行是被该事务后面启动的事务修改或者删除的。在可重复读取隔离级别下,后开始的事务对数据的影响不应该被先开始的事务看见,所以应该忽略后开始的事务的更新或者删除操作。

(2)INSERT

对新插入的行,行的修改版本号更新为该事务的事务号。

(3)DELETE

对于删除,InnoDB直接把该行的删除版本号设置为当前的事务号,相当于标记为删除,而不是物理删除。

(4)UPDATE

在更新行的时候,InnoDB会把原来的行复制一份,并把当前的事务号作为该行的修改版本号。

MVCC读取数据的时候不用加锁,每个查询都通过版本检查,只获得自己需要的数据版本,从而大大提高了系统的并发度。当然,为了实现多版本,必须对每行存储额外的多个版本的数据。另外,MVCC存储引擎还必须定期删除不再需要的版本,及时回收空间。