分布式事务可能是个伪概念

板桥里人

  分布式事务顾名思义,是分布式环境下的事务,而在分布式王国里有一个著名的CAP定理,那么事务这个概念是否需要服从CAP定理呢?
在回答这个问题之前,先将事务和CAP定理两个基本概念回顾一下:

事务定义                                         

一个事务是一个只包含所有读/写操作成功的集合,也就是一系列读写操作(主要针对写操作,读操作也是为了写)要么全部成功,要么全部失败,如同一个原子操作一样,由此可见,原子性是事务的一个重要特征。事务还有其他三个特征:一致性(Consistency)、隔离性(Isolation)和耐久性(Durability),这四个特征取其英文第一个字母简称ACID。
让我们看看ACID的一致性和隔离性含义,这两者本质上是一致的,因此一致性是针对数据状态的一致,而隔离性是针对导致数据状态变化的动作行为的隔离性。
如果同时有多个事务并发同时发生,事务中操作行为必须始终保持系统数据处于一致的状态, 这句话含义其实有两个部分,操作行为的隔离性和数据状态变化的一致性。
操作行为的隔离性实际是通过并发控制实现的,让多个操作行为相互隔离,互相不影响,类似多线程编程需要使用同步锁一样。
何为数据状态一致性?因为数据库是数据状态的大本营,关系数据库需要有保证自己内部多个表之间数据变化的一致性,不能一个表数据改变,另外一个有关联的表不发生变化,除了约束一致性, 还有级联和trigger的一致性。最常见的一致性案例是银行转账,当A账户给B账户转账100元,那么必须保证A账户和B账户增减100元的一致性,不能A账户减去100元,B账户却没有新增100元,破坏了数据一致性和完整性。
事务这个概念表面上好像是针对操作行为的,但是实际上真正关注的是操作行为的结果:状态,行为是状态变化的因,行为与状态是因果关系,因为之所以需要事务,实际是关注事务中操作导致的数据状态的切换的一致性,因此,如果事务操作是并发多个,必须系统也必须如同一个事务一样操作。
操作的隔离并发性和数据状态的一致性实际是统一的,为了更形象表达这个问题,我们以Java对象为案例说明这个问题,以我的另外一篇《为什么需要领域驱动设计》中充血对象为案例:
class Book{
private String name;  //不变属性
private int state;     //可变属性 状态

    public void order() throws StateException {
if (this,state != 0 )  //只有不处于支付或出货状态才能订购
throw new StateException();
this.state = 1;   //假设1代表被订购
}

    public void payment() throws StateException {
if (this,state != 1 ) //只有在订购以后才能支付
throw new StateException();
this.state = 2;   //假设2代表支付
}

    public void delivery() throws StateException {
if (this,state != 2 ) //只有在支付以后才能发货
throw new StateException();

       this.state = 3;   //假设3代表发货
}

}
书籍book的有三个状态:被订购、支付和发货,每个状态切换都是有规则约束的,比如发货时,首先要判断当前状态是已经支付了才能发货,这个规则判断其实是保障数据一致性,否则会出现有的书籍没有支付就出货情况,财务没有收到钱,但是货却发出了。因此,这个类的三个动作order()、payment()、delivery()都是保证了Book状态切换的一致性,但是只有逻辑代码的一致性还不够,还需要保证在多个线程同时执行这三个动作时不会发生状态切换混乱,在多线程并发运行环境下,通常看上去没有问题的代码实际存在数据正确性Bug,这个问题可见我的另外一篇文章:《多线程并发编程中的初始化问题》。
看上去话题扯得有点远,实际这些都是关于隔离并发控制和数据正确一致性的问题,上述Book类如果使用Fork/Join这样并行库进行并行运行测试,肯定是无法满足正确一致性的,那么只有给这个类的三个动作order()、payment()、delivery()的方法加上同步锁,才能实现并发控制,如下:
public synchronized void payment() throws StateException {
if (this,state != 1 ) //只有在订购以后才能支付
throw new StateException();
this.state = 2;   //假设2代表支付
}
数据库内部并发控制机制也是类似这样通过加入同步锁才能保证同一时刻只有一个线程操作,当然加同步锁是性能最差的方式,数据库内部也不会像简单使用粗粒度锁,这里只是进行并发控制的原理解释时引入简单的案例,在以后篇幅中我将介绍如何通过Actor模型实现Book这样充血模型的操作方法的一致性,也就是事务性。
回到事务ACID的隔离性与一致性属性话题上,让我们深入思考:这两个特性意味着如果一系列操作没有引起任何状态切换,那么,是不是就是可重复执行,像之前操作没发生一样呢?也就是是幂等操作?如果是幂等操作我们就可能无需再关注其是否需要事务了,反正失败了重新再来一次呗,但是如果有动作引起状态变化,那么我们就需要事务机制,以确保状态的一致性,因此,这里也给出我们一个判断何时需要事务机制的条件。
平时我们的状态切换都是使用数据库表实现,而不是像上面使用内存中的一个Book对象,因此事务ACID的隔离性需要依托数据库悲观锁或乐观锁提供的串行化机制才能实现ACID的一致性,如同上面Book对象中三个方法需要同步锁保证每个方法同时只能一个线程执行一样,比如多线程同时访问payment()方法,因为遭遇同步锁,变成一个个依次执行,如同一个个单线程串起来一样。
因此,是否可以产生这样一个经验?如果数据库的写操作基本都是需要事务的,因为数据库的增删改查四个动作中,增删改三个动作肯定是会引起数据库状态变化,它们都需要事务ACID机制,数据库乐观锁或悲观锁等策略提供不同粒度的串行化,如果对数据一致性要求越高,性能必然会所牺牲。这种串行化是有成本的, Amdahl法则描述如下:它是描述序列串行执行和并发之间的关系。
“一个程序在并行计算情况下使用多个处理器所能提升的速度是由这个程序中串行执行部分的时间决定的。”
大多数数据库管理系统选择(默认情况下)是放宽一致性高标准,以达到更好的并发性。
事务ACID的最后一个属性比较容易理解,事务一旦被提交,其引起的状态必须永久保存,这样即使在断电情况下也不会影响这些状态结果。

 

CAP定理

    CAP是分布式系统中进行平衡的理论,它是由 Eric Brewer发布在2000年。

  1. Consistent一致性: 同样数据在分布式系统中所有地方都是被复制成相同。
  2. Available可用性: 所有在分布式系统活跃的节点都能够处理操作且能响应查询。
  3. Partition Tolerant分区容忍性: 在两个复制系统之间,如果发生了计划之外的网络连接问题,对于这种情况,有一套容忍性设计来保证。

 事务

一般情况下CAP理论认为你不能同时拥有上述三种,只能同时选择其中两种,这是一个实践总结,当有网络分区P的情况下,也就是分布式系统中,你不能又要有完美的高一致性C和100%的可用性A,只能在这两者中选择一个。因此在实际操作中,我们总是降低一些一致性高标准,以及降低100%的可用性要求,一致性C和可用性A都各自妥协一点,不需要那么完美,不需要那么刚性,那么最终才能带来真正好的解决方案。
但是说到容易做到难,因为我们很多人都是靠关系数据库吃饭了N多年,已经对关系数据库有很深的依赖性了,特别是Java中的JTA事务机制能够让我们跨数据库安全操作数据,几乎在平时开发中根本不需要考虑数据正确性和一致性问题,集中使用SQL语句解决业务逻辑就可以了,现在要告诉他们JTA是一种2PC(两段事务提交two-phase commit),属于CAP中选择了完美的高一致性C和100%可用性,放弃了分区容忍性,估计比较难以接受。我们看看2PC两段事务的原理:
第一阶段:事务协调器向每个服务器询问一遍,要求每个数据库都进行precommit预提交的操作以及是否可能实现正式commit提交. 如果所有数据库都同意commit ,第二段才会开始。
第二阶段:事务协调器要求每个数据库commit数据. 如果任何数据库否决commit, 那么所有数据库将被要求回滚。
在2PC的第二阶段过程中,如果事务协调器失败出现故障,所有的数据库因为本身已经进入锁状态,就需要等待恢复,这是不能容错的一个体现,同样,在协调通讯过程中,如果一个网络消息丢失,2PC协议将导致发送更多消息,因此,2PC实际是一种堵塞协议。后来又有了3PC,但是还是容易受网络分区影响。
因为2PC选择了CAP中C和A,导致网络分区容忍性下降,同时意味着分库分表能力大大下降,因为分库分表引入了更多网络通讯,但是除了上帝,没有人保证网络100%可靠啊,连接失败情况总是有的。
当初MySQL 5版本试图挑战CAP定理,结果灰溜溜收场,后来有人基于MySQL实现了自动分库分表的中间件,又试图再次挑战CAP定理,当然不是说,传统关系数据库不能进行分库分表分片,而是一旦这样做,你就要把系统纳入CAP考虑范围,你原来对事务的100%要求可能就要降低,以换得其他要求的提高,正如男女年轻人找对象,帅、有钱和忠诚三者只能取其二,如果三者都想要,那么就找个有点帅、有点钱且能够忠诚的人就可以了,但是现实中还是有那么多条件很好的大龄单身女青年,这说明降低自己的完美要求是多么难的一件事情,这反映在架构设计上,有多少架构师能够意识到自己的完美情节正在阻碍其设计呢?而他们之所以成功地从初级程序员成长到架构师,完美情节是主要动力啊。
所以,在完美主义的架构师中,真正的分布式事务其实是一个完美命题,而从CAP定理角度看,分布式事务其实可能是个伪命题,因为真正分布式系统不但只是考虑事务性,还要考虑网络容忍性,非堵塞性等等多个指标权衡,不能片面追求高事务,否则最终得不到完美的解决方案。

使用DDD聚合发现隐藏的业务规则的案例分析:数据库事务的业务实现