ACID ,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),只要你接触数据库总是逃不掉的。现在,我问你一个问题,如果现在数据库不提供 ACID 保证,你如何在应用程序层面实现 ACID 呢?

首先,这个问题本质上与问 ACID 的原理是一样的,只不过更偏向于顶层。我们接触数据库一般都是从顶层开始,而不是从底层开始,没有人一开始是通过读源码学习数据库的使用的。而数据库的设计,也不是一开始就看到了 ACID 的。向下的路径与向上的路径存在某种隐秘的重合。因此,讨论这个问题,有助于我们更好的理解 ACID 。

我们关于数据库事务的需求是,有 N 条语句在一个事务内执行,它们要么都成功,要么都失败。显然,第一个问题是,我们如何保证失败之后进行回滚呢?唯一办法就是记录下操作,然后模拟反操作。事实上 git 就是这么做的。数据库需要回滚的操作有三种, insert 、 update 和 delete , insert 和 delete 是相对的,但 update 不是,因为数据改变原有条件对应的数据也发生了变化,不可逆向回去,因此只能记录下被更新的数据和更新前的数据,或者说取个 diff 。由此我们获得关于如何实现事务的初步想法。

但这是远远不够的。目前,我们还有一系列问题存在。比如,现在我们除了事务,还有很多语句也在执行。如果我们的事务都直接对代码进行修改,这无疑会造成一些混乱。因为事务是否成功只有在事务完成之后才能确定,一旦有逻辑依赖于这种成功性,我们就必须确保,我们的数据不能直接在数据上进行修改。解决办法就是在主干数据上进行分支,我们通过记录主干与分支的差异,从而达成主干与分支数据的隔离。这里需要考虑的问题有很多,比如不同事务间隔离的问题。由于事务间是可以并行的,所以事务与事务间的可见性也是一个麻烦的问题。我们可以进行一个抽象来思考这个问题。即使单个语句不是事务,我们也可以把它考虑成是某个可见性上的事务。于是,所有执行期间的可见性问题都将转换为事务间的隔离性问题。虽然同一时间内有众多事务事务执行,但是纵向地看,事务间对其他事务的可见性要求是有限和可分的,按读写可分为四类,宽松读宽松写,宽松读严格写,严格读宽松写,严格读严格写。这样的可见性划分足以满足我们的需求。

但问题还没有解决。如果分支数据是叉开的,那么是否意味着数据要存两份呢?因为仅仅只有操作并不能满足事务在其中的速度。我们并不完全存两份,我们存各个数据操作后的 diff ,并借助指针进行偏移。在事务成功时,我们能够快速地应用上差异——显然,我们需要用到锁。在事务失败时,我们也能快速溯源进行回溯。

当然,以上只是我们的粗略方案,实际数据库设计更为复杂。