Domain Events异步应用

Domain Events领域事件定义见这里Domain Events – 救世主,领域事件可以实现领域业务模型与技术架构之间的松耦合,达到实现类似DCI架构同样效果。

DCI是通过设定一个场景对象,让与这个场景有关的功能在场景中混合实现,注入参与者角色和参与模型;而领域事件则是通过松散的事件达到场景目标,不同场景对应不同领域事件。下面以JiveJdon实例说明领域事件应用。

在JiveJdon的Account中有一个计算该用户发帖总数字段messageCount,如下:


public class Account{

private int messageCount;

public int getMessageCount(){
return messageCount;
}

}

这个messageCount是通过查询该用户所有发帖数查询出来的,也许你会问,为什么不将用户发帖总数设置为数据表一个字段,这样用户发帖时,更新这个字段,这样只要直接查询这个字段就可以得到messageCount?

没有设立专门持久字段的原因如下:
1. 从模型设计角度看:messageCount字段归属问题,messageCount其实是一个表示Account和Message两个实体的关系关联统计字段,但是这种关联关系不属于高聚合那种组合关系的关联,不是Account和Message的必备属性,根据DDD的高聚合低关联原则,能不要的关联就不要,因此放在哪里都不合适。

2.从性能可伸缩性角度看:如果我们将messageCount放在第三方专门关联表中,那么用户增加新帖子,除了对Message对应的表操作外,还需要对这个关联表操作,而且必须是事务的,事务是反伸缩性的,性能差,如果象messageCount各种统计字段很多,跨表事务边界延长,这就造成整体性能下降。

3.当然,如果将messageCount硬是作为Account表字段,由于整个软件的业务操作都是Account操作的,是不是将其他业务统计如threadCount等等都放到Account表中呢?这会造成Account表变大,最终也会影响性能。


那么messageCount每次都通过查询Message对应表中用户所有发帖数获得,这也会导致性能差,表中数据越多,这种查询就越费CPU。

使用缓存,因为Account作为模型被缓存,那么其messageCount值将只有第一次创建Account执行查询,以后就通过缓存中Account可直接获得。

所以,根据DDD,在AccountRepository或AccountFactory实现数据表和实体Account的转换,Account中的值都是在这个类中通过查询数据表获得的。

当访问量增加时,这里又存在一个性能问题,虽然一个Account创建时,messageCount查询耗时可能觉察不出,但是如果是几十几百个Account第一次创建,总体性能损耗也是比较大的,鉴于我们对可伸缩性无尽的追求,这里还是有提升余地。

从设计角度看,由于messageCount不是Account的必备字段,因此,不是每次创建Account时都要实现messageCount
的赋值,可采取即用即查方式。所以,我们需要下面设计思路:


public class Account{

private int messageCount = -1;

public int getMessageCount(){
if(messageCount == -1)
//第一次使用时即时查询数据表
return messageCount;
}

}

怎么实现这个功能呢?使用Hibernate的懒加载?使用Lazy load需要激活Open Session In View,否则如果Session关闭了,这时客户端需要访问messageCount,就会抛lazy Exception错误,但是OSIV只能在一个请求响应范围打开,messageCount访问可能不是在这次请求中访问,有可能在后面请求或其他用户请求访问,所以,这个懒加载必须是跨Session,是整个应用级别的.

实际上,只要Account保存在缓存中,对象和它的字段能够跨整个应用级别,这时,只要在messageCount被访问即时查询数据表,就能实现我们目标,其实如此简单问题,因为考虑Hibernate等ORM框架特点反而变得复杂,这就是DDD一直反对的技术框架应该为业务设计服务,而不能成为束缚和障碍,这也是一山不容二虎的一个原因。

这带来一个问题,如何在让Account这个实体对象中直接查询数据库呢?是不是直接将AccountRepository或AccountDao注射到Account这个实体呢?由于AccountDao等属于技术架构部分,和业务Account没有关系,只不过是支撑业务运行的环境,如果将这么多计算机技术都注射到业务模型中,弄脏了业务模型,使得业务模型必须依赖特定的技术环境,这实际上就不是POJO了,POJO定义是不依赖任何技术框架或环境。

POJO是Martin Fowler提出的,为了找到解决方式,我们还是需要从他老人家方案中找到答案,模型事件以及Event模式也是他老人家肯定的,这里Account模型只需要向技术环境发出一个查询Event指令也许就可以。

那么,我们就引入一个Domain Events对象吧,以后所有与技术环境的指令交互都通过它来实现,具体实现中,由于异步Message是目前我们已知架构中最松耦合的一种方案,所以,我们将异步Message整合Domain Events实现,应该是目前我们知识水平能够想得到的最好方式之一,当然不排除以后有更好方式,目前JdonFramework 6.2已经整合了Domain Events + 异步消息机制,我们就可以直接来使用。

这样,Account的messageCount即用即查就可以使用Domain Events + 异步消息实现:


public int getMessageCount(){
if (messageCount == -1) {
if (messageCountAsyncResult == null) {
//向技术环境发出查询获得messageCount值的命令,
//这个命令是在另外新线程实现,因此结果不一定立即返回
messageCountAsyncResult =
domainEvents.computeAccountMessageCount(account.getUserIdLong());
} else {
//当客户端再次调用本方法时,可以获得查询结果,
//如果查询过程很慢,还是没有完成,会在这里堵塞等待,但概率很小
messageCount = (Integer) messageCountAsyncResult.getEventResult();
}
}
}

messageCount最后获得,需要通过两次调用getMessageCount方法,第一次是激活异步查询,第二次是获得查询结果,在B/S架构中,一般第二次查询是由浏览器再次发出请求,这浏览器服务器一来一回的时间,异步查询一般基本都已经完成,这就是充分利用B/S架构的时间差,实现高效率的并行计算。

所以,并不是一味死用同步就能提高性能,可伸缩性不一定是指单点高性能,而是指整个系统的高效率,利用系统之间传送时间差,实现并行计算也是一种架构思路。这种思考思路在实际生活中也经常会发生。

最后,关于messageCount还有一些有趣结尾,如果浏览器不再发第二次请求,那么浏览器显示Account的messageCount就是-1,我们可以做成不显示,也就看不到Account的发帖总数,如果你的业务可以容忍这个情况,比如目前这个论坛就可以容忍这种情况存在,Account的messageCount第一次查询会看不到,以后每次查询就会出现,因为Account一直在缓存内存中。

如果你的业务不能容忍,那么就在浏览器中使用AJAX再次发出对getMessageCount的二次查询,那么用户就会每次
都会看到用户的发帖总数,JiveJdon这个论坛的标签关注人数就是采取这个技术实现的。这样浏览器异步和服务器端异步完美结合在一起,整个系统向异步高可伸缩性迈进一大步。

更进一步,有了messageCount异步查询,如何更新呢?当用户发帖时,直接对内存缓存中Account更新加一就可以,这样,模型操作和数据表操作在DDD + 异步架构中完全分离了,数据表只起到存储作用(messageCount甚至没有专门的存储数据表字段),这和流行的NoSQL架构是同一个思路。

由于针对messageCount有一些专门操作,我们就不能直接在Account中实现这些操作,可以使用一个专门值对象实现。如下:


public class AccountMessageVO {

private int messageCount = -1;

private DomainMessage messageCountAsyncResult;

private Account account;

public AccountMessageVO(Account account) {
super();
this.account = account;
}

public int getMessageCount(DomainEvents domainEvents) {
if (messageCount == -1) {
if (messageCountAsyncResult == null) {
messageCountAsyncResult = domainEvents.computeAccountMessageCount(account.getUserIdLong());
} else {
messageCount = (Integer) messageCountAsyncResult.getEventResult();
}
}
return messageCount;
}

public void update(int count) {
if (messageCount != -1) {
messageCount = messageCount + count;
}

}
}

注:以上功能已经在当前论坛实现。

相关文章:
Domain Events – 救世主
CAP原理和BASE思想
开源JdonFramework 6.2全新发布

[该贴被admin于2009-11-22 08:11修改过]
[该贴被admin于2010-03-28 09:00修改过]
[该贴被admin于2010-03-28 09:01修改过]
[该贴被admin于2010-03-28 09:01修改过]


我也遇到过这个问题。解决的思路是差不多。
但是稍微有点不一样。
我在这介绍一下我这边本身设计。
DomainEvent
他只有 type:String,target:Object
两个属性

DomainEventListener
只是初步的设计了handle(de:DomainEvent):void
这样的方法

DomainContext
这个类把他看作是Observer就可以了。
是设计成了单例

在载入Account的时候,直接载入 messageCount的字段。用hibernate的foruma也可以。
之后如果这个对象存在于缓存的话。
我是用了静态内部类来处理。

public class Account{

private int messageCount;
private AccountMessageCountListener amc = new AccountMessageCountListener ();

private static class AccountMessageCountListener implements DomainEventListener{
public AccountMessageCountListener (){
DomainContext.getInstance().addDomainEventLister(this);
}

public void handle(DomainEvent de){
if(de.getType().equals("Create_new_Message")){
Message msg = (Message) de.getTarget();
Account account = msg.getAccount();
account.messageCount = account.messageCount+1;
}
}
}

public int getMessageCount(){
return messageCount;
}
}
当然AccountMessageCountListener 这个类也可以做成单例
当然更新 messageCount的时候要锁住.在这边没加锁
当然在Service层(或者Manager层)save message (这里的save插入数据库成功)
成功之后要触发 Create_new_Message的DomainEvent事件
这样就直接更新了Account的MessageCount字段了。

你觉得呢
Bang

[该贴被bosslee于2009-11-21 23:43修改过]
[该贴被bosslee于2009-11-21 23:49修改过]

不错,使用了观察者模式。

观察者模式有同步和异步,异步观察者模式就和消息生产和消费模式差不多了,Event-Listerner事件监听模式

在DDD实践中,使用事件模式主要是为解决业务模型和技术架构耦合。

这个想法是很好的,不过有些东西我还想跟banq讨论一下。
首先、采用异步消息的方式进行统计MessageCount,这个想法非常好,但是却多了一次另一次ajax的查询过程,你的主要想解决的就是多用户的等待问题,从HTTP1.0的协议发展到HTTP1.1协议,最大的改进就是将页面对象的多次请求整合在一次请求内完成,减少客户连接服务器的次数,如果在人数众多的时候,连接一次服务器就代表了什么??一台服务器只有65535个端口,能够承载的连接可以计算一下能有多少。
其次、何时进行AJAX的更新,Onload的时候发现是-1么??那么跟用户直接载入的时候查询差多少那,而且就如我上面说的,还多了一次连接??如果是查看用户详细信息的时候,那可以在用户新发帖的时候更新就可以了,这个方法就显得有些多余。
再次,我不知道你的这个是否只能依赖于BS的结构,是否只有采用AJAX进行查询的这种方式。这样其实是业务与MVC的V进行了绑定,扩展性是否真的增强,有待考察。
最后、如果异步更新的消息队列出现了问题,那么是否代表了客户端会不断的向服务器发起连接,典型的DDoS攻击形式……,你的服务器岌岌可危。

2009年11月23日 15:07 "ACoder"的内容
这个想法是很好的,不过有些东西我还想跟banq讨论一下。
务器只有65535个端口,能够承载的连接可以计算一下能有多少。
其次、何时进行AJAX的更
再次,我不知道你的这个是否只能依赖于BS的结构
最后、如果异步更新的消息队列出现了问题。

首先多谢你肯定这个idea, 你的疑问可能是大多数人的疑问:
这里的异步是指一个分布式环境下异步,这样我们就能够将业务处理分到不同服务器执行,比如messagecount可以给另外一台服务器执行,那么客户端下次可能就到那台服务器获取结果,这个非常类似互联网网站设立专门的图形服务器一样,这里还有一个分布式job服务器架构

何时进行AJAX更新,这些都是由我们定义的,因为AJAX寄生在Jsp页面。

可以不依赖BS架构,多层架构C/S更容易实现。

消息队列可以采取JMS集群,没有单点风险。这样,整个分布式架构可以如下图:

Bang
按照你的逻辑,是不是我可以理解成,系统启动时,载入messageCount和第一次调用这个函数的时候载入的区别呢。
如果是的话,他对于前者来说有什么优势呢?

2009年11月23日 22:24 "bosslee"的内容
Bang
按照你的逻辑,是不是我可以理解成,系统启动时,载入messageCount和第一次调用这个函数的时候载入的区别呢。

不是系统启动,而是当客户端访问Account的getMessageCount这个事件时。

一般传统观点是,一次请求事件,对应一次响应结果,一般都是想在一次请求把所需要的结果全部获得,就象贪心馋嘴的小孩吃糖果一样,现在就不同了,搞清楚你所要结果的主次,先获得主要的,次要的再来一趟获取。

所以,getMessageCount要两次触发调用,第一次触发调用是激活查询或计算事件,第二次触发调用是获得计算结果。

这个类似专门JOB异步模式适合那些费时的计算和查询,再往上就可以使用hadoop等云计算来对付。

但是,千里之行,始于足下,你必须首先分辨出你的业务哪些是费时费力的,而且不能将这些耗CPU计算推给数据库,然后再在架构设计上专门对待。

2009年11月21日 09:47 "banq"的内容
由于针对messageCount有一些专门操作,我们就不能直接在Account中实现这些操作,可以使用一个专门值对象实现

这里也体现了一个DDD实践中什么时候使用值对象的一个原则,当围绕这些值字段有一些专门复杂操作时,就有必要独立出来做一个值对象,最近InfoQ 2009一个讲座就是谈这个问题的:Power Use of Value Objects in DDD

他使用了电话号码为例子,因为在Action和Service中都涉及phoneNumber处理,所以,专门成立一个phoneNumber的值对象,如下:

[该贴被banq于2009-11-26 11:05修改过]


Domain Events还可以实现懒加载大数据的预加载。

比如图片,只有显示图片时才会需要图片的二进制数据,这个数据比较大,所以,一般从持久层加载图片时,只加载图片其他核心文字信息,如图片ID,名称等,当需要显示时,再加载二进制数据输出真正图片。

下面是JiveJdon的上传图片代码:


public byte[] getData() {
byte[] bytes = super.getData();
if (bytes != null)
return bytes;
preloadData();
bytes = (byte[]) imgDataAsyncResult.getEventResult();
this.setData(bytes);
return bytes;
}

//预加载
public void preloadData() {
if (imgDataAsyncResult == null && domainEvents != null)
imgDataAsyncResult = this.domainEvents.loadUploadEntity(this);
}

上面preloadData可以在控制帖子显示的Action中预先调用一下,当帖子被输出到浏览器中时,帖子再通过html语法img调用上述getData数据,显示图片。

所以,html语法显示图片本身的异步性和我们服务器端异步结合在一起,这种模式可以适合大量图片显示场合。

特点是:显示快,因为当浏览器发出html的img语法调用服务器的图片时,服务器端图片一般已经预加载完成,准备就绪,只能调用,这比img语法到达服务器端时再进行图片大数据加载更有效。

该模式不但适合图片,也适合任何大数据对象。

参考并发策略可以解决延迟
[该贴被banq于2009-12-14 10:24修改过]

“一台服务器只有65535个端口,能够承载的连接可以计算一下能有多少”

这句话让我觉得自己又新手了一下,难道我又理解错了?服务器每承载一个连接都要开一个端口吗,而不是只要绑定监听端口一个就够?如果是这样的话那以qq的tcp连接数,那得多少台机器才能承受住如此巨大的用户连接啊,反正一个机器只能服务65535个用户

更何况,就算真如你所说,现在服务器也一般是主线程监听,后台线程池对accept 的socket进行服务,根本就不用浪费掉所有的65535个端口。

不知道是我一直理解错了基础知识还是你这句话误导了人。。。。。。

非常好的模式。我们可以暂时称为“全局懒加载模式”。通过异步加载使得当前请求的数据先加载,耗时的,并且不被当前请求需要用到的数据异步的加载。同时这种全局异步懒加载的命中率非常高,高于SESSION级别的命中率。

2009年12月15日 22:35 "xmuzyu"的内容
非常好的模式。我们可以暂时称为“全局懒加载模式”。

传统意义上,懒加载和异步都是好像不被人接受的,会带来比较差的性能,高延迟性,属于边缘技术,这其实是被误导了:并发策略可以解决延迟

懒加载和异步代表的并发策略实际是一种潮流趋势,特别是作为并行计算语言Scala和erlang的新亮点:
函数式编程functional programming的特点

而最新发布JiveJdon3.9使用传统Java,基于Jdon框架6.2实现了领域模型层的懒加载和异步,可以完全克服Hibernate等ORM框架带来的Lazy懒加载错误问题。

JiveJdon3.0源码下载:http://www.jdon.com/jdonframework/download.html

呵呵。。。
[该贴被yuhonglei4025于2009-12-16 17:09修改过]

我觉得这个和架构没半点关系。

我注入Events和注入一个带异构功能的Service有什么区别呢?

我觉得BANQ在理解领域模型的时候有偏差。

既然是技术层的东西,也就是说,在领域知识内不存在计算机技术的概念,你为什么要把技术层引入到DOMAIN呢?

DOMAIN就应该只关注于领域对象之间的关系和行为就足够了,涉及到技术的,都交给其他层完成,而不是非要在DOMAIN中加上技术性的操作,你觉得别扭,这是必然的,因为在原本的业务概念中,根本不存在这中技术性的概念!

2010年01月11日 17:02 "tianhaoleng"的内容
在领域知识内不存在计算机技术的概念,你为什么要把技术层引入到DOMAIN呢

这是对的,但是因为Java语言的限制,如果采取基于Scala的Akka那种Actor Model,而一个领域模型实际就是一个Actor,就非常自然了。Actor只能通过发送异步消息和其他Actor通讯联系,这种消息发送是异步的,属于“fire-and-forget”方式。


怎样让领域模型驱动技术为其服务,不否认有更优雅的方式,权宜之举由领域模型发出事件消息不失是一种办法,而如果你将服务注射到领域模型,就不是一种事件驱动架构(当然,还要将更多技术注射到领域模型)。而Domain events相当于只注射一次,首先建立一个事件通道,以后就可以通过这个管道让Domain一次次发送事件消息来命令技术架构为其做事,相当于在两者之间建立一个管道。

个人认为这是一种模拟Scala的Actor模型做法。

[该贴被banq于2010-01-11 17:29修改过]