地改变事务A正在读取的数据。
事务A获取了数据的写锁,事务B想读取对应的数据,事务B也必须等到事务A提交或中止后方可进行读取。
事务A获取了数据的写锁,事务B想写对应的数据,事务B也必须等到事务A提交或中止后方可进行写入操作。
由上面三个规则可以看出,2PL提供串行化的访问,它可以防止任何的并发问题,但是由此带来的问题也显而易见,数据库的并发能力大大降低了。
共享锁与独占锁
两阶段锁的逻辑是通过共享锁与独占锁共同来实现的:
如果事务A要读取数据,则必须先获取共享锁。数据库允许多个事务同时拥有共享锁,但如果另一个事务拥有独占锁,则其他事务要获取共享锁则必须等待。
如果事务A要写入数据,则必须先获取独占锁。任何其他事务都不能同时拥有锁,(无论是共享还是独占)因此如果对象上存在任何锁,事务A必须等待。
如果事务A先读取数据,然后写入数据。它可以将共享锁升级为独占锁。升级与直接获得独占锁相同。
在事务获得锁之后,它必须继续持有锁直到事务结束(提交或中止)。这就是“两阶段”的名称:第一阶段在获取锁时,第二阶段释放锁。
由于使用了这么多锁,所以很容易发生事务A被卡住等待事务B释放它的锁,反之亦然。这种情况称为死锁。数据库自动检测死锁之后会终止事务,然后重启事务排队。
序列化的快照隔离(SSI)
两阶段锁(2PL)由于采取了悲观的并发控制,不但容易引起死锁,且性能低下。所以接下来我们要来看看序列化的快照隔离(SSI),它提供了完整的串行化,但是只有很小的性能损失相比两阶段锁。
当我们以前讨论快照隔离中的并发写问题,是因为事务从数据库读取一些数据,检查读取结果,并决定根据它看到的结果采取一些操作。然而,在快照隔离的情况下,原始查询的结果在事务提交时可能不再是最新的,因为数据可能在此期间进行了修改。所以查询和事务中的写之间可能存在因果依赖关系。为了提供串行化隔离,数据库可以检测到这种情况,并且终止不合法的事务。
检测是否读取旧的数据
快照隔离通常采用多版本并发控制实现,当一个事务读取一个数据库的一致性快照,它忽略了新的写入。为了防止这种异常,数据库需要跟踪事务时读取时是否忽略了另一个事务的写操作,当事务要提交时,数据库检查任何已忽略的写操作。如果忽略了写操作,则必须中止事务。
为什么要等到提交时,而不是检测到读取旧数据时就立即终止事务呢?那么,如果事务如果是只读事务,则不需要中止,在事务进行读取时,数据库还不知道该事务是否稍后将执行写入操作。上文Alice与Bob请假的例子可以通过这样的方式避免并发写的问题:
检测影响先前读取的写入
如果并没有检测到读取了旧的数据,仍然有可能出现并发写入的问题。
所以当事务写入数据库时,它记录读取受影响数据的任何其他事务的索引。一旦第一个事务是成功提交,其他所有相关的索引事务必须终止。通过这样快照隔离的方式,保证了并发写入的安全性。同样是上文的例子,下图暂时了索引终止技术:
许多工程细节影响算法在实践中的工作效果。跟踪事务的读写的粒度。如果数据库非常详细地跟踪每一个事务的活动,那么它就可以精确地判断哪些事务需要中止,但是这些开销会变得很大。而不太详细的跟踪事务会更快速,但可能导致更多的事务被中止。相比与两阶段锁,可串行化隔离快照是大有好处的:一个事务不需要阻塞等待另一个事务持有的锁。
小结:
我们在本篇之中总结了数据库事务与隔离运用到的多种策略与技术,希望大家能够更好的认识事务在数据库系统之中的重要意义,并且能够为自己的开发环境运用最恰当的隔离级别。