SOA并不能解决高并发事务

前面我指出传统SOA架构其实无法面对高并发事务,我以国内淘宝网的两个PPT为案例,分析一下,其中一篇是面向生产环境的SOA系统设计 by 程立

在三十页,使用悲观锁实现服务对资源的并发控制,这种方式最容易发生我在首贴里提到的死锁:


PPT也认为这种方式不适合热点资源,也就是高并发场合。

乐观锁如何呢?



虽然乐观锁短,但是容易产生脏数据。

该PPT虽然否定了乐观和悲观锁的解决方案,但是思路还是在锁的方向,没有可加锁的资源,那么我们人为制造一个锁,以操作实例也就是操作的对象的ID为锁标识,这其实是将整个对象在内存中锁起来,会发生多线程变单线程,一个马桶只能蹲一个人的现象,我在首贴中的画图中描述过这个办法。


当然,以上资料根据2009年淘宝网支付宝公布的文档,不代表他们现在没有更新升级。


你的SOA已经使用了EDA和CQRS吗?

SOA是以服务这个方式对外提供功能,我们很显然喜欢在Service中加上JTA等事务,比如EJB的无态Bean或Spring的@Transaction标注都是激活这样的功能,这种方式实际是一种悲观事务,容易引起死锁,特别是在高并发情况下。

我们需要重新思考的是:将事务加在服务这样处理过程上,这个思路是不是有问题?因为服务只是对外提供一个统一接口,不代表对内其就是一个统一的处理过程,是一个process,这是很多人对服务的误解之处。

新的EDA+reactor思路是:服务通过发送消息给一个聚合根实体,也就是Actors模型,聚合根实体是通过消息与外面交互,其内部状态一致性操作由其自己完成。

准确说,DDD+ CQRS +EDA+reactor + Actor才是高并发事务SOA的终极解决方案。

参考:http://www.jdon.com/45738


[该贴被banq于2013-09-17 11:38修改过]

2013-09-17 11:34 "@banq
"的内容
准确说,DDD+ CQRS +EDA+reactor + Actor才是高并发事务SOA的终极解决方案。 ...

这种架构虽然提高了吞吐量,但是只能做到最终一致性。如果要求强一致性,在遇到一次修改涉及多个聚合根的时候,就需要事务了。上次你提到的关于利用事件的方式来实现转账的方法(有一个帖子)实际上也没有做到转账的强一致性。

2013-09-18 09:09 "@tangxuehua
"的内容
在遇到一次修改涉及多个聚合根的时候,就需要事务了 ...

你可能还没有理解我之前“数据喂机器”的理论,你这段话的逻辑实际上将聚合根和事务过程并列了,或者说,认为事务不属于聚合根范畴,甚至是凌驾于聚合根之上,这其实是“数据喂机器”导致。

实际上,高一致性事务等同于逻辑一致性,是被聚合根(也就是Actors模型)管理的,属于聚合根的上下文边界的。如果你不习惯这样的思维,那么我们可以说,凡是你认为需要使用高一致性强事务的地方就是一个有界的上下文,可以用聚合来表达。

不要将聚合根实体看成没有行为守护的抽象数据类型,看成被动的数据,聚合根是有强一致性保证的富对象。
http://www.jdon.com/45736

[该贴被banq于2013-09-18 09:17修改过]

2013-09-18 09:15 "@banq
"的内容
高一致性事务等同于逻辑一致性,是被聚合根(也就是Actors模型)管理的,属于聚合根的上下文边界的 ...

意思是,只要是需要强一致性的地方都应该由聚合根来实现?那银行转账呢?你不是跨聚合根要实现强一致性吗?你上次翻译的通过设计一个转账聚合根来在代码层面实现2pc的方法,本质上也没有实现强一致性。因为当转账聚合根在第二阶段要求源账号和目标账号分别去扣钱和加钱时,这两个操作的扣钱和加钱动作并不是在一个事务内,也就是不是同时发生。详见我之前对那个帖子的回复。

2013-09-18 10:00 "@tangxuehua
"的内容
你上次翻译的通过设计一个转账聚合根来在代码层面实现2pc的方法,本质上也没有实现强一致性 ...

你说的有道理,转账是REST和DDD的这个案例:http://www.jdon.com/45622/5#23143081

我们需要分清楚业务上事务和技术上事务两个,2PC两阶段提交 还是锁事务等属于技术上的事务,使用技术事务的前提是,我们首先要发现业务中需要事务的业务操作过程,注意这里首先发现的是业务操作这个动词,这种先入为主的动词发现法不是DDD对象分析法。

回到转账这个案例,如果动词先入为主,那么将转账做成一个服务,然后加上悲观锁或2PC,这是一般人的习惯思维。

使用DDD,是将转账作为一个聚合根实体,在转账这个聚合根内部来保证转账这个业务事务的实现,那么如何实现呢?简单地办法就是用一个方法如下:

void transfer(){
a.remove(100);
b.add(100);
}

这里transfer方法没有加同步,是默认转账这个对象使用Actor这样模型,而a帐号和b帐号属于transfer这个聚合内部的对象。

我在‘REST和DDD’中翻译的老外转账案例为什么在Transfer聚合根和A B帐号之间用command和event这样消息机制实现呢?前提是A和B帐号不在transfer这个聚合边界内,这其实是一种使用消息系统这样的技术手段实现事务(保证可靠性),但是不代表不是强一致性,使用消息实现可靠性在大数据分析架构中常见,例如Kafka 可以实现消息回放已经消息commit确认,只有发出去的消息完成才能commit确认,见这个帖子:http://www.jdon.com/45698#23143267

打个比喻:
Transfer <------> A
<--------> B

Transfer与A B之间的交互通过消息系统是确保消息可靠地参与到A或B扣款这个业务动作中,也就是说:只有A扣款了,带有扣款命令的消息才从队列中删除,只有B加款了,带有加款命令的消息才从队列中删除,至于这两个动作如何保证一致性,是在Transfer聚合根的transfer方法中来保证。

可以这么说,消息系统是实现分布式事务的一个技术手段,一个环节,但不是全部,更不代表使用了消息系统就意味弱一致性。

下面来比较一下基于消息ESB的服务模型和基于消息的Actor模型:

以转账为案例,服务模型如下:


class TransferService{

public void transfer(){
sendToAService.remove(100);//发生消息给账户服务
sendToBService.add(100);//发生消息给账户服务
}

}

TransferService <---JMS---> AService

通过ESB/JMS这样消息总线,实现服务之间的分布式事务,这是SOA架构的特点。

下面看看用聚合根+Actor模型,伪代码:


class Transfer extends Actor{

private void transfer(){
sendToA.remove(100);//发生消息给账户A
sendToB.add(100);//发生消息给账户B
}

..接受消息的方法

..发送消息的方法
}

这两个比较好像没有什么不同,关键是transfer方法如何保证一致性,也就是事务用语原子性?因为这个方法里面有两条语句,如果第一条执行完毕,第二条执行错误,如何让第一条语句回滚?

那么TransferService服务模型采取加入悲观锁或乐观锁或2PC等等,TransferService如果用EJB实现无态Bean实现,缺省是JTA事务激活,如果用Spring实现,TransferService加上@Transaction标注,那么也激活了JTA事务。这种锁的方式并发性能很低。

而TransferActor实现方式,由于该类任何方法都无法被外界直接方法,因此,任何时刻其transfer方法只可能一个线程访问,没有堵塞的可能性,也不存在在单线程情况下技术出错(我们讨论的范围是多线程并发,而不是单线程下的可靠性),无需回滚,只要业务上确保逻辑准确即可,

所以,TransferService和TransferActor的区别是:前者可能会被多线程并发访问,使用同步锁解决并发,不尊重多线程本身规律,拙劣;而后者在尊重多线程基础上用信箱封装,某个时刻只可能被一个线程访问,而且不影响性能。

以上只是个人思考,不代表任何官方意见。


[该贴被banq于2013-09-18 11:54修改过]

感谢banq详细的回答哈。
actor模型确实比较好。但是我之前的问题是:如果transfer,accountA,accountB分别在自己的聚合边界内。而且transfer聚合根内部就算同时产生了CreditPreparationConfirmed,DebitPreparationConfirmed这两个事件,那按照你的意思,应该就是业务上做到了“强一致性的事务”这点我仍同;但是这两个事件会分别被AccountA,AccountB处理,这两个账号接收到command命令后做第二阶段的真正的扣款或加款。
由于AccountA,AccountB是不同的聚合根,我们衡量最后转账是否有么有成功也是去看AccountA,AccountB的余额是否正确变化了为准。按照聚合根之间“使用最终一致性”的理念,那就是实际上我们并不是去保证AccountA,AccountB的强一致性(物理上的事务一致性),是吗?

我觉得,Transfer聚合根内部同时产生的CreditPreparationConfirmed,DebitPreparationConfirmed这两个事件,只是表示Transfer确认了转入和转出这两个动作可以执行了,然后外界的两个账号聚合根响应该事件,做各自的加钱和扣钱操作;但谁也无法保证AccountA,AccountB这两个聚合根实际在做自己的加钱或扣钱动作时不会失败;因为我们在技术上是使用了通过消息队列的方式来实现异步消息通信,所以不可能保证这两个动作在一个技术上强一致性的事务内。是吗?

也就是结论是,转账这个场景,从数据上看,没有实现强一致性,而是最终一致性;而业务上,如果以Transfer聚合根同时产生了CreditPreparationConfirmed,DebitPreparationConfirmed这两个事件的角度去来判断是否是业务上的强一致性的话,那确实是做到了业务上的强一致性,因为CreditPreparationConfirmed,DebitPreparationConfirmed这两个事件对外界来说确实是一起产生的,且一定是以事务的方式被持久化的。

但问题是,我们最后难道不是以数据的变化是否为强一致性来判断整个转账场景是否是强一致性的吗?
[该贴被tangxuehua于2013-09-18 12:48修改过]

2013-09-18 12:41 "@tangxuehua
"的内容
但谁也无法保证AccountA,AccountB这两个聚合根实际在做自己的加钱或扣钱动作时不会失败;因为我们在技术上是使用了通过消息队列的方式来实现异步消息通信,所以不可能保证这两个动作在一个技术上强一致性的事务内。是吗? ...

这个问题是这样可以解释的,当账户发出PreparationConfirmed这个事件,表示它们已经准备好,准备好什么?余额可以能够扣100元?这在业务上就意味着PreparationConfirmed吗?我认为不是,如果这时又来一个要扣100元的请求,而A帐号只有150元,那么这150元如何准备两个100元的扣除呢?

显然我们对PreparationConfirmed发出后意味着什么还是理解不够精确,PreparationConfirmed发出后,意味着A帐号里的100元就被冻结了,这很符合业务用语,比如警察冻结了你的账户(诈骗电话经常这么说)。

也就是说,PreparationConfirmed发出时,A账户已经少了100元,这100元被冻结了,只剩余50元,所以,紧接着再来一个扣除100元,是不行的。

好了,下面流程是否成功就剩余技术上是否出错误,至少业务上已经逻辑严谨,一致性了。

2013-09-18 13:38 "@banq
"的内容
好了,下面流程是否成功就剩余技术上是否出错误,至少业务上已经逻辑严谨,一致性了。 ...

是的,确实,如果没有断网或IO的问题,正常情况下,一定可以安全的把这冻结的100元真正扣掉了。呵呵,这样是可以理解为业务上已经做好了。

但总有万一的时候,要是这100元由于网络问题没有扣除,那AccountA显示给用户看的余额是150还是50呢?应该是50吧?也就是说,这100元就算物理数据上没真正被扣除,给用户看的也应该算被扣除了,对吗?

那这样的话,用户知道转账没成功,但是看到的余额却是50元,是否有矛盾呢?

2013-09-18 13:45 "@tangxuehua
"的内容
用户知道转账没成功,但是看到的余额却是50元,是否有矛盾呢? ...

这可以采取类似State Checkpointing状态检查模式http://www.jdon.com/45698/5#23143273

定时对没有commited的数据进行清理,也可以在冻结金额100元时设置一个timeout,超过这个时间自动归还余额。

2013-09-18 14:20 "@banq
"的内容
这可以采取类似State Checkpointing状态检查模式http://www.jdon.com/45698/5#23143273

定时对没有commited的数据进行清理,也可以在冻结金额100元时设置一个timeout,超过这个 ...

相当于是一个流程中的补救措施了。
实际上老外的这个转账的例子,本质上也是一个流程的实现,设计一个Transaction聚合根来控制流程的走向(先通知账号准备转账,接收到准备完成的事件后,再通知他们执行真正的转账),如果设计的完善点,还应该包含各种容错补救机制,比如任何一方如果加钱或扣钱失败了,应该再回滚已经做了加钱或扣钱的那一方。

这一切还是需要稳定可靠的消息通信框架,且消息要不允许丢失且必须至少会被投递一次。

" TransferService和TransferActor的区别是:前者可能会被多线程并发访问,使用同步锁解决并发,不尊重多线程本身规律,拙劣;而后者在尊重多线程基础上用信箱封装,某个时刻只可能被一个线程访问,而且不影响性能。 "

有感于自己得出这样一个结论,趁着中秋节日假期,为Jdon框架增加command功能,也就是一个聚合根实体不但可以发出领域事件,也可以通过异步并发消息接收外部的命令,输入和输出都是异步并发,类似Actors模型。

这样在Java中,结合CQRS也可以实现可替代高并发事务的Actors模型了,见两个聚合根串在一起的案例:
https://github.com/banq/jdonframework/tree/master/src/test/java/com/jdon/sample/test/cqrs

UI --command-->aggregateRootA ---event--->component---command-->aggregateRootB

[该贴被banq于2013-09-23 15:36修改过]

2013-09-23 15:35 "@banq
"的内容
UI --command-->aggregateRootA ---event--->component---command-->aggregateRootB ...

恩,你这个聚合根交互的流程和我的框架enode很像。enode中,框架层面就约束了一个command只能影响一个聚合根,聚合根之间通过事件进行异步交互。每个聚合根从内存缓存(如redis)中取出来的都是副本,并发通过eventstore中的事件表的aggregateRootId+eventStreamVersion这两个字段来做唯一索引来控制;一旦遇到并发冲突,框架自动重试command;架构图链接:
http://images.cnitblog.com/blog/13665/201307/10101615-46e3fc4415934480932f4e77468f97a6.png
框架介绍地址:http://www.cnblogs.com/netfocus/p/3179060.html
[该贴被tangxuehua于2013-09-23 18:21修改过]

我的理解SOA在于系统治理,暴露的服务都是基本是完整业务逻辑的,基于SOA做全局事务的场景应该不是很多,完全可以通过最终一致性或者对账机制保障,大部分的事务都在一个聚合根里面得到保障,所以我的想法banq类似,将事务分成两层,聚合根里面的是强一致性,SOA层面的是最终一致性和对账。我遇到的最大的问题是,公司很多同事不认可OO,否定聚合根存在,希望SOA直接集成信息服务,直接晕倒