November 7th, 2013

数据库是一种十分复杂的软件,它为用户提供了可靠的数据存储与查询机制,本文将来探讨一下其中的“可靠存储”这个问题。数据库的可靠存储特性主要体现在如下几个方面:

  1. 当出现硬件错误时,保证数据的一致性
  2. 保证一系列的操作是原子的,要么成功要么失败
  3. 当数据备份多处时,保证不同数据备份的一致性

单库事务控制,预写日志

数据一致性经常也被称为“事务一致性”,也就是保证一系列的操作,要么都成功要么都失败。例如有下面的关系数据表,存储了用户的账户信息:

账户名称 账户类型 账户余额
扎迪 支票 800
扎迪 存款 300
皮德罗 支票 150

现在希望将扎迪的支票账户中的200元转移到扎迪存款账户里,最终结果希望是

账户名称 账户类型 账户余额
扎迪 支票 600
扎迪 存款 500
皮德罗 支票 150

那么,我们需要将这个任务分两步来执行:

  1. 先将扎迪的支票账户更新为600
  2. 再将扎迪的存款账户更新为500

但是,如果这两个步骤不能保证,要么同时成功要么同时失败的话,将出现很严重的后果。比如:当完成第一步后,突然断电了,扎迪会发现他的支票账户少了200元,但是存款账户仍然是300,凭空少了200元!尽管说,像断电这样的都是小概率事件,但是在上面这个例子中是绝对不被允许的。

在数据库中为了解决这个问题,提出了事务的概念,程序员可以将两个步骤包装在一个事务中提交给数据库,数据库能够保证“事务一致性”。这样神奇的效果是如何实现的呢?其实原理很简单,就是所谓的“预写日志”,我们来看下具体的过程

程序员在程序中依次将下面的指令发送给数据库,以完成更新

  1. 开始事务
  2. 将扎迪的支票账户余额变为600
  3. 将扎迪的存款账户余额变成500
  4. 结束事务

这些指令被数据库程序接收后,将在转化为如下操作:

  1. 开始事务
  2. 将扎迪的支票账户余额从800变为600
  3. 将扎迪的存款账户余额从300变成500
  4. 结束事务

数据库并不是立刻执行事务中的操作,而是将这些操作依次写入“预写日志”,预写日志是保存在磁盘上的。随后,数据库程序将执行“预写日志”中的内容,将更新操作体现在数据文件中。在正常的情况下,当完成“结束事务”操作后,删除上面的日志,并告知程序执行成功,这样,正常的操作就完成了。(有些数据库会在日志删除前再次写入“归档日志”中,以便可以通过归档日志恢复整个库)

这里有一个需要注意的问题,程序在将事务操作发送给数据库的过程中,数据库也可能崩溃,也就是说“预写日志”可能记录不完整。这样,当数据库从崩溃中重启后可能发生的两种情况:

  • 数据库崩溃重启后,查看预写日志,发现一个事务,并且该事务的最后一个操作是“结束事务”。那么此时可以推断:这个事务有可能执行成功了,但日志没来得及删除;也有可能这个事务没有执行完,甚至还没有执行。不过无论如何,数据库将进行“前滚”恢复,将事务中剩下操作执行一遍。但是问题是,怎么才能知道究竟执行到哪一步了呢?事实上,这一点根本不重要,因为哪怕重新回放整个预写日志中的内容也仍然可以达到一致性。对于这种无论执行多少次都不会影响最终结果的特性称为“幂等”。
  • 数据库崩溃重启后,查看预写日志,发现一个事务,并且该事务的最后一个操作不是“结束事务”。那么可以推断:可能是程序指令在发送到数据库的过程中,数据库崩溃了,而且事务一定没有正确执行。这个时候由于无法确定,这个事务后面时候还有其他的操作没有接收,所以数据库只能执行“回滚”恢复,将已经在日志中的操作反过来恢复。读者可能已经注意到了,数据库在记入预写日志的时候,不仅记录的新值,同时还记录的原值,这样就能将数据回退到初始状态。 这里实际上解释了两个概念:前滚和回滚,这是数据库事务控制中很重要的概念,读者可以自行查询其他资料以便了解更多的细节。

跨库事务控制,二阶段提交

在很多数据库系统中,不仅支持单库的事务一致性,还支持多个库的事务一致性,虽然不同的数据库产品在实现这个功能的时候,所使用的细节技术不同,但是大致都是采用了”二阶段提交“这个方法,下面我们来看下它是如何工作的。

一般来说,多个库中在一次事务中需要有一个事务发起者,称为”主库”,其他的库称为“从库”。假设需要执行一个在所有库的某个表中插入一行数据的事务。

  • 第一阶段,主库锁定表,并将事务写入自己的预写日志;主库将事务发给从库,从库也各自锁定自己的表,并把事务写入预写日志,完成后返回告诉主库一阶段完成
  • 第二阶段,主库开始执行自己的事务,并通知从库提交事务。如果在这个过程中没有任何错误,那么操作将在多个库中完成;如果发生错误,比如从库锁表失败,或者从库没有响应,或者从库磁盘满…主库将通知所有参与事务的从库回滚该事务,并且回滚主库的事务。回滚就是根据预写日志的内容回滚事务的操作,可见预写日志的重要性。 下图分别展示了两种过程

成功的二阶段提交,A为主库,B、C为从库

失败的二阶段提交,A为主库,B、C为从库

总结

可见,“预写日志”对于事务前滚或回滚来说是非常重要的,这个精巧的设计保证了数据库的“一致性”,进而保证了“可靠存储”。而“二阶段提交”也是一个十分精巧而简单的方法,在数据库的“主从复制”、“跨库事务”等方面都有应用。

不过事实上,上面的讨论都是基于单用户的,但数据库还需要解决多用户并发问题,这是通过“锁”的机制来实现的,锁也同样重要,不过本文不再阐述。


1块2块也是钱,小额赞助