forked from EdisonXu/EdisonXu.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 210 KB
/
content.json
1
{"meta":{"title":"Edison Xu's Blog","subtitle":null,"description":"非专业不著名IT工匠的点点滴滴","author":"Edison Xu","url":"http://edisonxu.com"},"pages":[{"title":"404 Not Found:该页无法显示","date":"2018-02-23T02:19:15.386Z","updated":"2018-02-23T02:19:15.386Z","comments":false,"path":"/404.html","permalink":"http://edisonxu.com//404.html","excerpt":"","text":""},{"title":"徐焱飞","date":"2017-01-12T00:55:25.000Z","updated":"2018-02-23T02:19:15.417Z","comments":true,"path":"about/index.html","permalink":"http://edisonxu.com/about/index.html","excerpt":"","text":"Edison Xu生于84年,标准80后,曾就职于Wipro Shanghai, 爱立信上海研究院,普兰金融,现在创业期上海磬石信息科技有限责任公司 (好了,不要吐槽官网,抽空会重做的。欢迎技术人员加盟!)。 技术上,主要从事Java开发,了解C, Scala,写过shell和python脚本。独立开发过安卓应用,尤擅后端开发,小至SSH,大到分布式。设计过底层协议,弄过连接池,也搞过Storm流式大数据处理。钟爱各种设计模式和架构。技术涉略比较杂。也算一个小小全栈吧。 管理上,考过PMP,做过PMO的program manager,系统培训和实战了Scrum后,一发而不可收拾。一直在思考如何在实践中灵活的运用PMP和Scrum,做到二者的取长补短,相互配合。 业余爱好篮球(现因膝关节手术结束“职业生涯”),痴迷游戏(钟爱沙盒),喜欢听歌唱歌(歌喉还可以,谢绝参赛),尤爱电影,一大梦想就是实现私人影院,无奈业余时间愈发减少。欢迎拉我一起参加如上活动,如能免费,更是极好的。 目前,正在研究微服务框架的最佳实践以及event sourcing。窃以为技术破冰是最难的,而相互交流,分享知识,可以有效降低破冰难度。欢迎各位一起交流,相互学习。 送上笔者最爱的“两”句话: 时刻保持危机感,才能进步。 天赋是上帝给予的,要谦虚。名声是别人给予的,要感激。自负是自己给的,要小心。——约翰•伍登 比你优秀的人比你更努力,芽儿哟,这是最骚的"}],"posts":[{"title":"Akka入门系列(三):远程Actor","slug":"akka-remote-actor","date":"2018-10-30T01:04:22.000Z","updated":"2018-10-30T01:57:48.747Z","comments":true,"path":"2018/10/30/akka-remote-actor.html","link":"","permalink":"http://edisonxu.com/2018/10/30/akka-remote-actor.html","excerpt":"虽然Akka在单机上可以运行上百万的Actor,但出于容错、负载均衡、灰度发布、提高并行度等等原因,我们仍然需要能在多个不同的服务器上运行Actor。所以Akka提供了akka-remoting的扩展包,屏蔽底层网络传输的细节,让上层以及其简单的方式使用远程的Actor调度。 官方文档:https://doc.akka.io/docs/akka/current/remoting.html 适用场景","text":"虽然Akka在单机上可以运行上百万的Actor,但出于容错、负载均衡、灰度发布、提高并行度等等原因,我们仍然需要能在多个不同的服务器上运行Actor。所以Akka提供了akka-remoting的扩展包,屏蔽底层网络传输的细节,让上层以及其简单的方式使用远程的Actor调度。 官方文档:https://doc.akka.io/docs/akka/current/remoting.html 适用场景remoting的存在其实是为akka cluster做底层支持的,通常并不会直接去使用remoting的包。但为了了解cluster的底层原理,还是有必要看下remoting。同时,remoting被设计为Peer-to-Peer而非Client-Server,所以不适用于基于后者的系统开发,比如我们无法在一个provider为local的Actor里去查找一个remote actor发送消息,必须两者均为remote actor,才满足对等。 设计Akka的所有设计,都是考虑了分布式的:所有Actor的交互都是基于事件,所有的操作都是异步的。更多设计信息,请参考Remote设计,还是会获益良多。原文中有一句话 This effort has been undertaken to ensure that all functions are available equally when running within a single JVM or on a cluster of hundreds of machines. The key for enabling this is to go from remote to local by way of optimization instead of trying to go from local to remote by way of generalization. 后面这半句看的不是很懂,希望有理解的朋友回复交流。 基本例子Akka将remoting完全配置化了,使用时几乎只需要修改配置文件,除非自定义,否则不需要动一行代码。remoting包提供了两个功能: 查找一个已存在的远程Actor 在指定的远程路径上创建一个远程Actor 添加依赖在引入akka actor的基本依赖(请看前文)后,再加上remoting的依赖12345<dependency> <groupId>com.typesafe.akka</groupId> <artifactId>akka-remote_2.12</artifactId> <version>2.5.17</version></dependency> 配置在一个Akka项目中启用remote功能的话,最基本需要在application.conf(Akka默认的配置文件名)中启用如下配置:123456789101112akka { actor { provider = remote } remote { enabled-transports = [\"akka.remote.netty.tcp\"] netty.tcp { hostname = \"127.0.0.1\" port = 2552 } }} 基本配置包含如下四点: provider从local变成remote enabled-transports指定传输的实现 hostname 指定当前Actor底层网络监听组件所需监听的主机名,如果不指定,默认会调用InetAddress.getLocalHost().getHostAddress()来获取当前主机的IP port 指定当前Actor底层网络监听组件所需监听的端口,如果设置为0,则会生成一个随机的端口 由于要测试下本地去寻找远程actor,所以本文的代码例子中,用remote.conf作为配置文件名 注意如果在同一个主机上启动多个远程Actor,那么port一定要不同。因为远程Actor的底层会启动一个网络监控组件,该组件会去监听指定IP或域名的指定端口。如果都相同,肯定会有一个绑定失败。 查找一个远程Actor我们创建一个远程Actor,一会儿去查找它。注意,这里加载了remote.conf,但覆盖了端口为2551,目的是在本地模拟一个远端的Actor。如果觉得在本地起不好理解,就可以找一台服务器,把akka.remote.netty.tcp.hostname也覆盖掉换成服务器的IP,或者干脆另起一个配置文件。123456789101112131415161718192021222324252627282930public class ToFindRemoteActor extends AbstractActor { LoggingAdapter log = Logging.getLogger(getContext().system(), this); @Override public void preStart() throws Exception { log.info(\"ToFindRemoteActor is starting\"); } @Override public Receive createReceive() { return receiveBuilder() .match(String.class, msg->{ log.info(\"Msg received: {}\", msg); }) .build(); } public static void main(String[] args) { Config config = ConfigFactory.parseString( \"akka.remote.netty.tcp.port=\" + 2551) .withFallback(ConfigFactory.load(\"remote.conf\")); // Create an Akka system ActorSystem system = ActorSystem.create(\"sys\", config); // Create an actor system.actorOf(Props.create(ToFindRemoteActor.class), \"toFind\"); }} 启动后,可以看到控制台有日志打印出来:1234[INFO] [10/26/2018 11:54:33.684] [main] [akka.remote.Remoting] Starting remoting[INFO] [10/26/2018 11:54:34.198] [main] [akka.remote.Remoting] Remoting started; listening on addresses :[akka.tcp://[email protected]:2551][INFO] [10/26/2018 11:54:34.200] [main] [akka.remote.Remoting] Remoting now listens on addresses: [akka.tcp://[email protected]:2551][INFO] [10/26/2018 11:54:34.363] [sys-akka.actor.default-dispatcher-7] [akka://sys/user/toFind] ToFindRemoteActor is starting 这时,我们先尝试在一个本地进程里去查找这个Actor:12345678910public class Main1{ public static void main( String[] args ) { ActorSystem system = ActorSystem.create(\"main1\"); LoggingAdapter log = Logging.getLogger(system, Main2.class); ActorSelection toFind = system.actorSelection(\"akka.tcp://[email protected]:2551/user/toFind\"); toFind.tell(\"hello\", ActorRef.noSender()); }} 注意,这里我没有提供application.conf,而且也没有指定其他的配置文件!所以这里的ActorSystem起的完全是本地模式。我们运行一下,看看是远端的Actor是否会打印hello呢?1[INFO] [10/26/2018 14:02:27.661] [local-akka.actor.default-dispatcher-2] [akka://local/deadLetters] Message [java.lang.String] without sender to Actor[akka://local/deadLetters] was not delivered. [1] dead letters encountered. If this is not an expected behavior, then [Actor[akka://local/deadLetters]] may have terminated unexpectedly, This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'. 结果给出了这样的日志,说明并没有发送成功。再次验证了上面提到的Akka Remote的Peer-to-Peer设计,必须要求对等,两边都是remote! 好了,回到正轨上,我们来看看如何正确的去寻找一个远端actor并发送消息。123456789101112public class Main2 { public static void main(String[] args) { Config config = ConfigFactory.load(\"remote.conf\"); // Create an Akka system ActorSystem system = ActorSystem.create(\"main2\", config); // Find remote actor ActorSelection toFind = system.actorSelection(\"akka.tcp://[email protected]:2551/user/toFind\"); toFind.tell(\"hello\", ActorRef.noSender()); }} 这里加载了remote.conf,启用remote provider。可以在ToFindRemoteActor的控制台有如下日志:1[INFO] [10/26/2018 14:12:11.376] [sys-akka.actor.default-dispatcher-4] [akka://sys/user/toFind] Msg received: hello 说明找到且正常收到了消息。 创建一个远程的Actor在Main2里,我们相当于起了一个监听着127.0.0.1:2552的ActorSystem,那我们把Main2当作远程系统(如果觉得127.0.0.1不太好理解,可以把它打包放到其他服务器,并指定hostname为这个服务器的IP),在当前机器去尝试在Main2这个远端起一个Actor。远程Actor代码如下:123456789101112131415161718public class ToCreateRemoteActor extends AbstractActor { LoggingAdapter log = Logging.getLogger(getContext().system(), this); @Override public void preStart() throws Exception { log.info(\"ToCreateRemoteActor is starting\"); } @Override public Receive createReceive() { return receiveBuilder() .match(String.class, msg->{ log.info(\"Msg received: {}\", msg); }) .build(); }} 创建配置文件如下:12345678910111213141516akka { actor { provider = \"remote\" deployment { /toCreateActor { remote = \"akka.tcp://[email protected]:2552\" } } } remote { netty.tcp { hostname = \"127.0.0.1\" port = 2553 } }} 其中toCreateActor就是指定远端要启动的Actor的别名,在本地的ActorSystem靠这个别名去启动。注意指定provider为remote! 1234567891011public class Main3 { public static void main(String[] args) { Config config = ConfigFactory.load(\"create_remote.conf\"); // Create an Akka system ActorSystem system = ActorSystem.create(\"main3\", config); ActorRef actor = system.actorOf(Props.create(ToCreateRemoteActor.class), \"toCreateActor\"); actor.tell(\"I'm created!\", ActorRef.noSender()); }} 可以看到这里就尝试去创建一个名字叫toCreateActor的Actor,而这个名字在配置文件中定义了是远端的,Akka会自动尝试去远端创建。启动一下,看到Main3的日志:123[INFO] [10/26/2018 15:25:42.794] [main] [akka.remote.Remoting] Starting remoting[INFO] [10/26/2018 15:25:43.364] [main] [akka.remote.Remoting] Remoting started; listening on addresses :[akka.tcp://[email protected]:2553][INFO] [10/26/2018 15:25:43.365] [main] [akka.remote.Remoting] Remoting now listens on addresses: [akka.tcp://[email protected]:2553] 检查Main2的日志,会发现远程Actor创建的信息:12[INFO] [10/26/2018 15:25:43.774] [main2-akka.actor.default-dispatcher-17] [akka://main2/remote/akka.tcp/[email protected]:2553/user/toCreateActor] ToCreateRemoteActor is starting[INFO] [10/26/2018 15:25:43.775] [main2-akka.actor.default-dispatcher-16] [akka://main2/remote/akka.tcp/[email protected]:2553/user/toCreateActor] Msg received: I'm created! 到这,一个远端的Actor就被创建出来了。不过,事情就这样结束了吗?思考一个问题:查询这种远端创建的Actor,跟之前那个远端自己起来的Actor,方式一样吗?参考Main2,我们再写一个Main4来尝试查询并发送消息。那有一个问题,toCreateActor的地址到底该选哪个?按理说,应该是akka.tcp://[email protected]:2552/user/toCreateActor。带着问题,我们试试看1234567891011121314public class Main4 { public static void main(String[] args) { Config config = ConfigFactory.parseString( \"akka.remote.netty.tcp.port=\" + 0) .withFallback(ConfigFactory.load(\"remote.conf\")); // Create an Akka system ActorSystem system = ActorSystem.create(\"main4\", config); // Find remote actor ActorSelection toFind = system.actorSelection(\"akka.tcp://[email protected]:2552/user/toCreateActor\"); toFind.tell(\"I'm alive!\", ActorRef.noSender()); }} Main2中,会打印1[INFO] [10/26/2018 15:42:25.508] [main2-akka.actor.default-dispatcher-16] [akka://main2/user/toCreateActor] Message [java.lang.String] without sender to Actor[akka://main2/user/toCreateActor] was not delivered. [2] dead letters encountered. If this is not an expected behavior, then [Actor[akka://main2/user/toCreateActor]] may have terminated unexpectedly, This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'. 失败了。。。。。。仔细看,Main2里面创建出来的Actor的Path是akka://main2/remote/akka.tcp/[email protected]:2553/user/toCreateActor而远端自己起的Actor地址是:akka://sys/user/toFind所以,正确的Path应该是`akka.tcp/[email protected]:2553/user/toCreateActor`修改后测试一下,会发现Main2中打印1[INFO] [10/26/2018 15:25:58.615] [main2-akka.actor.default-dispatcher-17] [akka://main2/remote/akka.tcp/[email protected]:2553/user/toCreateActor] Msg received: I'm alive! 所以,可以得出一个看上去不是很合理的结论:虽然RemoteActor是创建在远程机器上,但如果想要查询它,还得向创建者发请求。 ArteryArtert是Akka为新版的remote包起的代号。目前是共存状态,但被标记为may change状态,仅UDP模式可以用于生产。 配置与原来的remote略有不同12345678910111213akka { actor { provider = remote } remote { artery { enabled = on transport = aeron-udp canonical.hostname = "127.0.0.1" canonical.port = 25520 } }} 与原先相比,多了一个enabled选项控制artery是否启动。相比原来的remote,Artery的变化主要集中在高吞吐、低延迟场景下提高性能上,包括用Akka Streams TCP/TLS替代了原来的Netty TCP,并新增了基于Aeron的UDP协议模式,以及对直接写java.nio.ByteBuffer的支持,大小消息分channel发等等。 其他介绍在具体使用中,还需要考虑序列化、路由、安全,而这些Akka都提供了。且看下回分解。","categories":[],"tags":[{"name":"akka","slug":"akka","permalink":"http://edisonxu.com/tags/akka/"},{"name":"actor","slug":"actor","permalink":"http://edisonxu.com/tags/actor/"},{"name":"并发","slug":"并发","permalink":"http://edisonxu.com/tags/并发/"}]},{"title":"Akka入门系列(二):Actor","slug":"akka-actor","date":"2018-10-30T01:04:02.000Z","updated":"2018-10-30T02:02:17.966Z","comments":true,"path":"2018/10/30/akka-actor.html","link":"","permalink":"http://edisonxu.com/2018/10/30/akka-actor.html","excerpt":"Actor模型由于AKka的核心是Actor,而Actor是按照Actor模型进行实现的,所以在使用Akka之前,有必要弄清楚什么是Actor模型。Actor模型最早是1973年Carl Hewitt、Peter Bishop和Richard Seiger的论文中出现的,受物理学中的广义相对论(general relativity)和量子力学(quantum mechanics)所启发,为解决并发计算的一个数学模型。 Actor模型所推崇的哲学是”一切皆是Actor“,这与面向对象编程的”一切皆是对象“类似。但不同的是,在模型中,Actor是一个运算实体,它遵循以下规则:","text":"Actor模型由于AKka的核心是Actor,而Actor是按照Actor模型进行实现的,所以在使用Akka之前,有必要弄清楚什么是Actor模型。Actor模型最早是1973年Carl Hewitt、Peter Bishop和Richard Seiger的论文中出现的,受物理学中的广义相对论(general relativity)和量子力学(quantum mechanics)所启发,为解决并发计算的一个数学模型。 Actor模型所推崇的哲学是”一切皆是Actor“,这与面向对象编程的”一切皆是对象“类似。但不同的是,在模型中,Actor是一个运算实体,它遵循以下规则: 接受外部消息,不占用调用方(消息发送者)的CPU时间片 通过消息改变自身的状态 创建有限数量的新Actor 发送有限数量的消息给其他Actor 很多语言都实现了Actor模型,而其中最出名的实现要属Erlang的。Akka的实现借鉴了不少Erlang的经验。 Actor模型的实现Akka中Actor接受外部消息是靠Mailbox,参见下图 对于Akka,它又做了一些约束: 消息是不可变的 Actor本身是无状态的 基本的Actor例子本文用Maven管理一个Java的Akka项目。当日,你可以直接从https://developer.lightbend.com/start/?group=akka下载一个官方的例子。 引入依赖1234567<dependencies> <dependency> <groupId>com.typesafe.akka</groupId> <artifactId>akka-actor_2.12</artifactId> <version>${akka-version}</version> </dependency></dependencies> 编写Actor通常情况下,我们只需要直接继承AbstractActor就足够了。123456789101112131415public class EchoActor extends AbstractActor { private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this); @Override public Receive createReceive() { return receiveBuilder() .match(String.class, s -> { log.info(\"Received String message: {}\", s); }) .matchAny(o -> log.info(\"Received unknown message\")) .build(); }} AbstractActor要求必须实现createReceive()方法,该方法返回一个Receive定义了该Actor能够处理哪些消息,以及怎么处理。这里只简单的打印一个日志。 Actor的启动Akka中,用ActorSystem来管理所有的Actor,包括其生命周期及交互。启动Actor,有两种方式 使用内置的main方法1akka.Main.main(new String[]{EchoActor.class.getName()}); 这里会自动将EchoActor创建出来。 手动创建ActorSystem12ActorSystem system = ActorSystem.create(\"app\");ActorRef echoActor = system.actorOf(Props.create(EchoActor.class), \"echoActor\"); 两种方法本质上其实是一样的,只不过第一种里面把创建ActorSystem等工作封装好了罢了。 注意:ActorSystem是一个较重的存在,一般一个应用里,只需要一个ActorSystem。在同一个ActorySystem中,Actor不能重名。 Actor的PathAkka中的Actor不能直接被new出来,而是按一棵树来管理的,每个Actor都有一个树上的path: 实际上,在我们创建自己的Actor之前,Akka已经在系统中创建了三个名字中带有guardian的Actor: / 最顶层的 root guardian。它是系统中所有Actor的父,系统停止时,它是最后一个停止的 /user guardian。这是用户自行创建的所有Actor的父。这里的user跟用户没有一毛钱关系 /system 系统guardian 在上面的例子里,我们使用的是system.actorOf来创建Actor,actorOf返回的并不是Actor自身,而是一个ActorRef,它屏蔽了Actor的具体物理地址(可能是本jvm,也可以是其他jvm或另一台机器)。通过直接打印ActorRef看到Actor的path,比如本例是/app/user/echoActor。像这种直接由system创建出来的Actor被称为顶层Actor,一般系统设计的时候,顶层Actor数量往往不会太多,大都由顶层Actor通过getContext().actorOf()派生出来其他的Actor。 Actor间的相互调用(tell, ask)在Actor模型中,Actor本身的执行是不占用被调用方(akka中的话是消息的发送者)的CPU时间片,所以,akka的Actor在相互调用时均是异步的行为。 tell 发送一个消息到目标Actor后立刻返回 ask 发送一个消息到目标Actor,并返回一个Future对象,可以通过该对象获取结果。但前提是目标Actor会有Reply才行,如果没有Reply,则抛出超时异常。 Tell: Fire-forget1target.tell(message, getSelf()); 其中第二个参数是发送者。之所以要带上这个是为了方便target处理完逻辑后,如果需要返回结果,可以也通过tell异步通知回去。 Ask: Send-And-Recieve-Future1234567Future<Object> future = Patterns.ask(echoActor, \"echo me\", 200);future.onSuccess(new OnSuccess<Object>() { @Override public void onSuccess(Object result) throws Throwable { System.out.println(result); }}, system.dispatcher()); 由于之前的EchoActor并没有返回Reply,所以这里什么都没打印。修改EchoActor如下:123456789101112@Overridepublic Receive createReceive() { return receiveBuilder() .match(String.class, s -> { log.info(\"Received String message: {}\", s); ActorRef sender = getSender(); if(!sender.isTerminated()) sender.tell(\"Receive: \"+s, getSelf()); }) .matchAny(o -> log.info(\"Received unknown message\")) .build();} 由于前面两个消息的发送者是ActorRef.noSender(),所以EchoActor中getSender()返回的是DeadLetter的ActorRef,terminated值为真。只有最后的值打印出来:Receive: echo me Actor的停止停止一个Actor有三种方法: 调用ActorSystem或getContext()的stop方法 给目标发送一个毒药消息:akka.actor.PoisonPill.getInstance() 给目标发送一个Kill消息: akka.actor.Kill.getInstance() 当使用前两种方法时,Actor的行为是: 挂起它的Mailbox,停止接受新消息 给它所有的子Actor发送stop命令,并等待所有子Actor停止 最终停止自己 由于停止Actor是一个异步的操作,在目标Actor被完全停止之前,如果要创建一个同名的Actor,则会收到InvalidActorNameException。 “kill”的方法略有不同,它会抛出一个ActorKilledException到父层去,由父层实现决定如何处理。一般来说,不应该依赖于PoisonPill和Kill去关闭Actor。推荐的方法是自定义关闭消息,交由Actor处理。 如果需要等待关闭结果,可以采用PatternsCS.gracefulStop,它会返回一个CompletionStage,可以进行到期处理:123456789101112import static akka.pattern.PatternsCS.gracefulStop;import akka.pattern.AskTimeoutException;import java.util.concurrent.CompletionStage;try { CompletionStage<Boolean> stopped = gracefulStop(actorRef, Duration.ofSeconds(5), Manager.SHUTDOWN); stopped.toCompletableFuture().get(6, TimeUnit.SECONDS); // the actor has been stopped} catch (AskTimeoutException e) { // the actor wasn't stopped within 5 seconds} 发送一个自定义的消息,如Manager.SHUTDOWN,等待关闭,如果6秒未关闭,再去处理。","categories":[],"tags":[{"name":"akka","slug":"akka","permalink":"http://edisonxu.com/tags/akka/"},{"name":"actor","slug":"actor","permalink":"http://edisonxu.com/tags/actor/"},{"name":"并发","slug":"并发","permalink":"http://edisonxu.com/tags/并发/"}]},{"title":"Akka入门系列(一):基本介绍","slug":"akka-intro","date":"2018-10-30T01:02:26.000Z","updated":"2018-10-30T01:57:35.462Z","comments":true,"path":"2018/10/30/akka-intro.html","link":"","permalink":"http://edisonxu.com/2018/10/30/akka-intro.html","excerpt":"什么是Akka,它能干什么?互联网系统的发展,大多数情况下都是业务倒逼的。发展过程不外乎以下几步: 最开始时,一个简单的MVC程序就可以,甚至是早期的J2EE也能得到很好的性能。 忽然某一天,系统压力大了,一些功能变得比较慢,这时会尝试去做代码重构优化,必要的地方开始使用进程内MQ以及线程池,开启异步及多线程。 再往后,单台也不能满足系统的吞吐了,这时就得上集群,前面一个负载均衡,后面部署多台相同的服务器,将压力均衡到若干服务器上,甚至数据库都开始做切片。 再往后,继续优化,将一些压力大的功能单独提出来,做成一个Service,对外部提供服务,可以是REST,也可以是RPC,用这种方式来提高服务器利用率,毕竟有些业务只需要IO,而有些业务需要很强的CPU,根据实现逻辑不同,需要的物理资源也不同。而这时,简单的负载均衡也无法适用了,需要功能相对复杂的网关或总线,并且各种分布式下的难点都出来了——调度、分布式容错、熔断、弹性、扩容、分布式事务、灰度发布、压力调整等等。","text":"什么是Akka,它能干什么?互联网系统的发展,大多数情况下都是业务倒逼的。发展过程不外乎以下几步: 最开始时,一个简单的MVC程序就可以,甚至是早期的J2EE也能得到很好的性能。 忽然某一天,系统压力大了,一些功能变得比较慢,这时会尝试去做代码重构优化,必要的地方开始使用进程内MQ以及线程池,开启异步及多线程。 再往后,单台也不能满足系统的吞吐了,这时就得上集群,前面一个负载均衡,后面部署多台相同的服务器,将压力均衡到若干服务器上,甚至数据库都开始做切片。 再往后,继续优化,将一些压力大的功能单独提出来,做成一个Service,对外部提供服务,可以是REST,也可以是RPC,用这种方式来提高服务器利用率,毕竟有些业务只需要IO,而有些业务需要很强的CPU,根据实现逻辑不同,需要的物理资源也不同。而这时,简单的负载均衡也无法适用了,需要功能相对复杂的网关或总线,并且各种分布式下的难点都出来了——调度、分布式容错、熔断、弹性、扩容、分布式事务、灰度发布、压力调整等等。 可以看到,如果要开发一个分布式系统,工程师要掌握的架构知识比较多,从基本的多线程到复杂的调度、容错、熔断、弹性、扩容等等复杂系统等等,任何一个环节出问题,都可能导致系统的不稳定。而Akka简化了这一切: Akka屏蔽了Java的多线程和锁,转而使用Actor模型,一般的工程师可以在不了解如何优化Java多线程编程的情况下,也能实现非常高性能的系统 Actor设计之初就天然满足了分布式,而且粒度较小,单机上可以跑上百万的Actor Akka屏蔽了分布式集群中底层的通讯机制,对于开发者来说,只要根据业务写好Actor即可 Akka直接提供了分布式下高可用、弹性、动态扩容的功能,无需再次开发 Akka由Scala编写,但同时提供了Scala和Java API。或许Akka你没有听过,但Spark、Flink这些当下流行的大数据分布式流式系统应当有所耳闻,它们的的底层,通通都在使用Akka。Scala的作者Martin Odersky,就是Akka背后的公司Lightbend(以前称为Typesafe)的创始人。Lightbend一直致力于提供基于Actor模型的分布式高性能系统,而非仅仅只有分布式框架,旗下除了Akka,还有Play(响应式Web框架)、Lagom(微服务框架)、alpkka(响应式集成中间件)。 Akka和Spring区别Akka关注在高性能上,Spring关注于工具集的整合和统一上Spring写出的代码(生产可用级),未必是高性能的(没说写不出高性能的),而Akka写出来的,基本上都是高性能。 利弊好处使用了Akka框架,你可以获得: 无锁、无同异步编程和多线程编程,低成本(编码阶段)实现高性能, 天然的并发系统和响应式系统,提供高吞吐和高并发服务 直接获得容错系统,自由定义恢复、重置或关闭等操作 简单的由单机扩展至分布式,核心业务代码几乎不用动 文档齐全 并发和并行的区别 并发是一个处理器利用时间切片处理多个任务想象电影院内的5台按摩椅,每次服务5-20分钟不等,一堆人等着坐,这个人下来那个人上。 并行是多个处理器或多核的处理器同时处理不同的任务想象有N排按摩椅 ,每一排都可以服务一堆人。Akka是天然并发系统,但是并不能默认做到所有任务并行。特此澄清一下! 坏处 上手难度高,学习路线陡峭 中文文档少 国内使用人少,遇到问题可请教或讨论的人少 纯异步,调试起来比较麻烦 第三方工具集少,添加进系统可能还需额外处理,缺少部分组件的整合的资料 使用方式 作为一个独立程序单独启动 作为一个lib集成到其他框架,如Spring里 提供的模组Akka主要提供了如下的组件(还有一些小的省略了): akka-actor_2.12 核心框架 akka-remote_2.12 底层通讯模块 akka-cluster_2.12 集群模块 akka-cluster-sharding_2.12 集群分片功能模块 akka-cluster-singleton_2.12 提供集群单例功能的模块 akka-cluster-tools_2.12 集群特殊功能模块 akka-stream_2.12 流处理及流式编程模块 akka-camel_2.12 基于Apache Camel的实现模块,与各种接口进行通信 akka-agent_2.12 处理共享变量及原子操作的模块 akka-http_2.12 用于构建基础http服务的模块(注意,akka http不是web框架!) akka-stream-kafka_2.12 kakfa的流式接口模块 akka-management_2.12 分布式集群管理模块 akka-testkit_2.12 单元测试模块 akka-slf4j_2.12 实现slf4j接口的日志模块 akka-persistence_2.12 用于保存数据、实现CQRS架构、实现EventSourcing的模块 akka-distributed-data_2.12 分布式数据保存模块,实现最终一致性","categories":[],"tags":[{"name":"akka","slug":"akka","permalink":"http://edisonxu.com/tags/akka/"},{"name":"actor","slug":"actor","permalink":"http://edisonxu.com/tags/actor/"},{"name":"并发","slug":"并发","permalink":"http://edisonxu.com/tags/并发/"},{"name":"并行","slug":"并行","permalink":"http://edisonxu.com/tags/并行/"}]},{"title":"JHipster快速开发Web应用","slug":"jhipster-quick-start","date":"2018-02-01T08:39:24.000Z","updated":"2018-02-23T02:19:15.417Z","comments":true,"path":"2018/02/01/jhipster-quick-start.html","link":"","permalink":"http://edisonxu.com/2018/02/01/jhipster-quick-start.html","excerpt":"在基于Spring的Web项目开发中,通常存在两个问题: 普通CRUD的代码基本重复,完全是体力活; Controller层和持久层之间的数据传递,存在不规范。有人喜欢直接返回JSON,有人喜欢用DTO,有人喜欢直接Entity。 那如何解决这个问题呢?自动生成呗。一群喜欢动脑筋(懒)的人,发明了JHipster。 JHipster是一个基于SpringBoot和Angular的快速Web应用和SpringCloud微服务的脚手架。本文将介绍如何利用JHipster快速开发Web应用。","text":"在基于Spring的Web项目开发中,通常存在两个问题: 普通CRUD的代码基本重复,完全是体力活; Controller层和持久层之间的数据传递,存在不规范。有人喜欢直接返回JSON,有人喜欢用DTO,有人喜欢直接Entity。 那如何解决这个问题呢?自动生成呗。一群喜欢动脑筋(懒)的人,发明了JHipster。 JHipster是一个基于SpringBoot和Angular的快速Web应用和SpringCloud微服务的脚手架。本文将介绍如何利用JHipster快速开发Web应用。 安装JHipsterJHipster支持好几种安装方式,这里选用最方便的一种方式:Yarn 1. 安装Java8;2. 安装Node.js3. 安装Yarn4. 安装JHipster: yarn global add generator-jhipster创建Web应用1. 创建项目目录2. 为一些被墙的资源添加国内源在项目目录下创建.npmrc文件,为该项目特指一些源。(当然,你也可以为Node和Yarn指定全局的源,那就可以跳过这一步)1234sass_binary_site=https://npm.taobao.org/mirrors/node-sass/phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/electron_mirror=https://npm.taobao.org/mirrors/electron/registry=https://registry.npm.taobao.org 3. 在项目目录下运行命令jhipster初始化123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384 (1/16) Which *type* of application would you like to create? (Use arrow keys)> Monolithic application (recommended for simple projects) //传统Web应用 Microservice application //微服务 Microservice gateway //微服务网关 (2/16) What is the base name of your application? (jhipster) jhipster_quick_start //输入项目名称,对应Maven的 artifactId (3/16) What is your default Java package name? (com.chimestone) com.edi //输入默认包名,对应Maven的 groupId (4/16) Do you want to use the JHipster Registry to configure, monitor and scale your application? (Use arrow keys)> No Yes //选择是否启用JHipster Registry(微服务默认开启),它可以理解为Eureka、Spring Cloud Config Server、Spring Cloud Admin的一个合体 (5/16) Which *type* of authentication would you like to use? (Use arrow keys) JWT authentication (stateless, with a token)> HTTP Session Authentication (stateful, default Spring Security mechanism) OAuth2 Authentication (stateless, with an OAuth2 server implementation) //选择认证方式,支持JWT、Session和OATUH2三种 (6/16) Which *type* of database would you like to use? (Use arrow keys)> SQL (H2, MySQL, MariaDB, PostgreSQL, Oracle, MSSQL) MongoDB Cassandra //选择数据库类型 (7/16) Which *production* database would you like to use? (Use arrow keys)> MySQL MariaDB PostgreSQL Oracle (Please follow our documentation to use the Oracle proprietary driver) Microsoft SQL Server //选择数据库 (8/16) Which *development* database would you like to use? H2 with disk-based persistence> H2 with in-memory persistence MySQL //选择开发时连接的数据库,这里选H2只是为了演示 (9/16) Do you want to use Hibernate 2nd level cache? (Use arrow keys)> Yes, with ehcache (local cache, for a single node) Yes, with HazelCast (distributed cache, for multiple nodes) [BETA] Yes, with Infinispan (hybrid cache, for multiple nodes) No //选择集成到Hibernate2级缓存 (10/16) Would you like to use Maven or Gradle for building the backend? (Use arrow keys)> Maven Gradle //选择打包工具 (11/16) Which other technologies would you like to use? ( ) Social login (Google, Facebook, Twitter) (*) Search engine using Elasticsearch >(*) WebSockets using Spring Websocket ( ) API first development using swagger-codegen ( ) [BETA] Asynchronous messages using Apache Kafka //选择其他的集成框架,这里注意要按下空格键才是启用,启用后会加上*标识。看到无脑自动集成ES是不是泪流满面? (12/16) Which *Framework* would you like to use for the client? (Use arrow keys)> Angular 4 AngularJS 1.x //选择集成的Angular的版本,Angular4采用Webpack打包自动化,而1.x采用Bower和Gulp做自动化 (13/16) Would you like to use the LibSass stylesheet preprocessor for your CSS? (y/N) y //是否启用LibSass (14/16) Would you like to enable internationalization support? (Y/n) n //是否开启国际化 (15/16) Besides JUnit and Karma, which testing frameworks would you like to use? (Press <space> to select, <a> to toggle all, <i> to inverse selection) >( ) Gatling ( ) Cucumber ( ) Protractor //选择测试框架,做压力测试的同学有福了 (16/16) Would you like to install other generators from the JHipster Marketplace? (y/N) //从JHipster市场下载一些其他集成,上下键翻动,空格选取/反选,回车结束。可以看到市场里还是有不少好东西的,像pages服务、ReactNative集成、swagger2markup让你的swagger界面更漂亮、gRPC自动CRUD代码等。 全部选择后,就开始了自动执行生成项目,喝杯水坐等。如果没有翻墙且忘了添加第二步的同学,请坐等卡住。这里有一点必须提醒,虽然JHipster选项中可以启用ES集成,但受SpringBoot对ES的集成版本限制。JHipster采用的是1.5.X的SpringBoot版本,对应的spring-data-elasticsearch是2.1.X版本,该版本最高支持ES到2.X,醉了~~~具体参见这里所以如果要使用高版本的ES,还是得用ES自己提供的REST接口,据ES的一篇文章Benchmarking REST client and transport client显示,5.0以后的ES自带的REST接口性能还是可以的。 基本姿势对于普通Web应用,JHipster在SpringBoot中默认加载了SpringMVC、SpringData、SpringJPA、SpringSecurity几个主要的Web相关的家族成员,LogStash作为日志工具,同时引入了ApacheCommons包、Swagger、HikariCP数据库连接池、Jackson等工具。基本上开发一个JavaWeb项目所需的框架都具备了,甚至还引入了Metrics做运维监控。此外,它还引入两个特殊的组件——Liquibase和MapperStruct。 Liquibase是一个帮助管理数据库变更的工具 MapperStruct用于自动生成Entity和对应DTO之间的映射关系类,在使用DTO时,千万记得要把自动生成的目录加到IDE的项目路径里! 国内搞JavaWeb的,大都喜欢使用Mybatis,可惜的是JHipster默认并不提供Mybatis的集成。但是SpringJPA现在已经封装的十分完善,常规的CRUD和分页,在JHipster下,无需写一行代码(是的,你没看错)。如果确实需要比较复杂的级联查询,JPA也提供了Specification和Sample实现,性能测试下来其实没多大区别,对付普通Web足以。如果确实不喜欢JPA,好在SpringBoot本身可以同时使用JPA和Mybatis,那么就把复杂级联用Mybatis,普通CRUD用JPA,达到最佳效果。 代码结构 EntityJHipster自动产生的项目,内置了User、Authority、PersistentToken、PersistentAuditEvent四个Entity(如果选取的还有其他组件,如OAUTH2等,会有对应的Entity自动生成)。产生的几张表均以jhi_开头。如果启用了ES,那么除了@Entity注解外,你还会看到@Document注解。这里值得一提的是,官方并不推荐修改默认的表名,而且如果要更改User的字段,官方推荐使用创建一个子类继承User类,然后在该子类中把User给Map进来,参见这里。但其实完全自己修改,然后更新数据库字段后,用Liquibase diff命令生成changelog。 ControllerJHipster自动生成的Controller暴露出的RESTful接口都是标准的RESTful API风格,国内很多程序员都不在乎这个东西,导致代码风格及其粗狂。 Repository这一块得益于SpringJPA的强大,一个JpaRepository接口足以满足大多数需求,有些懒人甚至连Controller都懒得写,给Repository接口加上@RepositoryRestResource注解直接暴露RESTful接口出去。 开始表演熟悉了代码结构后,我们开始用JHipster来做项目了。 创建JDL文件描述EntityJHipster默认提供了以下几种类型及校验关键字: 类型 校验 备注 String required, minlength, maxlength, pattern Java String类型,默认长度取决于使用的底层技术,JPA默认是255长,可以用validation rules修改到1024 Integer required, min, max Long required, min, max BigDecimal required, min, max Float required, min, max Double required, min, max Enum required Boolean required LocalDate required 对应java.time.LocalDate类 Instant required 对应java.time.Instant类,DB中映射为Timestamp ZonedDateTime required 对应java.time.ZonedDateTime类,用于需要提供TimeZone的日期 Blob required, minbytes, maxbytes 官方提供了一个在线的JDL Studio,方便撰写JDL。例子如下: 123456789101112131415161718192021//双斜杠注释会被忽略掉/** 这种注释会带到生成的代码里去 */entity Person { name String required, sex Sex}enum Sex { MALE, FEMALE}entity Country{ countryName String}relationship ManyToOne { Person{country} to Country}paginate Person with paginationpaginate Country with infinite-scroll 用jhipster import-jdl your-jdl-file.jdl导入Entity。中间会提示有conflict,因为像Cache配置、LiquidBase配置等是已存在的,可以覆盖或merge。执行完毕后,看到代码已经生成进去了。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778package com.edi.domain;import .../** * 这种注释会带到生成的代码里去 */@ApiModel(description = \"这种注释会带到生成的代码里去\")@Entity@Table(name = \"person\")@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)@Document(indexName = \"person\")public class Person implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotNull @Column(name = \"name\", nullable = false) private String name; @Enumerated(EnumType.STRING) @Column(name = \"sex\") private Sex sex; @ManyToOne private Country country; // jhipster-needle-entity-add-field - Jhipster will add fields here, do not remove public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public Person name(String name) { this.name = name; return this; } public void setName(String name) { this.name = name; } public Sex getSex() { return sex; } public Person sex(Sex sex) { this.sex = sex; return this; } public void setSex(Sex sex) { this.sex = sex; } public Country getCountry() { return country; } public Person country(Country country) { this.country = country; return this; } public void setCountry(Country country) { this.country = country; } // jhipster-needle-entity-add-getters-setters - Jhipster will add getters and setters here, do not remove ... 看到有段注释带进去了。再看下Controller1234567891011121314151617181920212223242526272829303132333435363738394041package com.edi.web.rest;import .../** * REST controller for managing Person. */@RestController@RequestMapping(\"/api\")public class PersonResource { ... /** * GET /people : get all the people. * * @param pageable the pagination information * @return the ResponseEntity with status 200 (OK) and the list of people in body */ @GetMapping(\"/people\") @Timed public ResponseEntity<List<Person>> getAllPeople(@ApiParam Pageable pageable) { log.debug(\"REST request to get a page of People\"); Page<Person> page = personRepository.findAll(pageable); HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, \"/api/people\"); return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); } /** * SEARCH /_search/people?query=:query : search for the person corresponding * to the query. * * @param query the query of the person search * @param pageable the pagination information * @return the result of the search */ @GetMapping(\"/_search/people\") @Timed public ResponseEntity<List<Person>> searchPeople(@RequestParam String query, @ApiParam Pageable pageable) { log.debug(\"REST request to search for a page of People for query {}\", query); Page<Person> page = personSearchRepository.search(queryStringQuery(query), pageable); HttpHeaders headers = PaginationUtil.generateSearchPaginationHttpHeaders(query, page, \"/api/_search/people\"); return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); }} 其他的不一一列举了,这里着重看下上面两个实现,一个是分页返回列表,一个是ES搜索。分页这里与我们常规有所不同,它是把分页信息通过PaginationUtil.generatePaginationHttpHeaders(page, "/api/people");这里生成到Header里去了,前端需要从Header里取。 自定义修改返回类型(Optional)好吧,看到上面肯定有同学要说了,我们平时分页都是返回JSON,所有数据都是返回JSON!如果非得这么做,那就只能自己做个ResponseUtil,把结果包装成如下格式 1234567891011{ \"success\": true, \"data\":{ \"content\": [{ \"name\": \"张三\", \"country\": \"中国\", \"sex\": \"MALE\" }] }, \"code\": 200} 只需增加两个新类:CommonResponse123456789101112131415161718192021222324public class CommonResponse<T> { public static final int DEFAULT_CODE = 200; private boolean success; private T data; private int code=DEFAULT_CODE; public CommonResponse() { } public CommonResponse(boolean success, T data) { this.success = success; this.data = data; } public CommonResponse(boolean success, T data, int code) { this.success = success; this.data = data; this.code = code; } ... //get & set} ResponseUtil1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253public class ResponseUtil { private static final Logger LOGGER = LoggerFactory.getLogger(ResponseUtil.class); private ResponseUtil() { } public static ResponseEntity<CommonResponse> okResponse(){ return wrapResponse(true, null, DEFAULT_CODE); } public static <T> ResponseEntity<CommonResponse> wrapResponse(int statusCode){ return wrapResponse(true, null, statusCode); } public static <T> ResponseEntity<CommonResponse> wrapResponse(T data){ return wrapResponse(true, data, DEFAULT_CODE); } public static <T> ResponseEntity<CommonResponse> wrapResponse(T data, Pageable pageable){ return wrapResponse(true, data, DEFAULT_CODE); } public static <T> ResponseEntity<CommonResponse> wrapResponse(T data, int statusCode){ return wrapResponse(true, data, statusCode); } public static <T> ResponseEntity<CommonResponse> wrapResponse(boolean successful,T data){ return wrapResponse(true, data, DEFAULT_CODE); } public static <T> ResponseEntity<CommonResponse> wrapResponse(boolean successful, int statusCode){ return wrapResponse(true, null, statusCode); } public static <T> ResponseEntity<CommonResponse> wrapResponse(boolean successful, T data, int statusCode){ return ResponseEntity.ok(new CommonResponse<>(successful, data, statusCode)); } public static <T> ResponseEntity<CommonResponse> wrapResponse(boolean successful, Optional<T> maybeResponse, HttpHeaders headers, int statusCode){ return (ResponseEntity)maybeResponse.map((response) -> { CommonResponse<T> commonResponse = new CommonResponse<>(successful, response, statusCode); return ((ResponseEntity.BodyBuilder)ResponseEntity.ok().headers(headers)).body(commonResponse); }).orElse(new ResponseEntity(new CommonResponse<>(successful, null, HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND)); } public static <T> ResponseEntity<CommonResponse> wrapOrNotFound(Optional<T> maybeResponse){ return wrapResponse(true, maybeResponse, null, DEFAULT_CODE); }} 然后修改下Controller里面的返回为如下即可123456789@GetMapping(\"/people\")@Timedpublic ResponseEntity<List<Person>> getAllPeople(@ApiParam Pageable pageable) { log.debug(\"REST request to get a page of People\"); Page<Person> page = personRepository.findAll(pageable); //HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, \"/api/people\"); //return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); return ResponseUtil.wrapResponse(page);} 打包运行先执行yarn install && bower install (Angular 1.x版本) 或 yarn install(Angular 4版本)对前端代码进行编译。然后可选: 命令行运行 ./mvnw 带LiveReload前端调试 gulp (Angular1.x版本)或yarn start(Angular 4版本) 生产编译 ./mvnw clean package -Pprod 启动后,默认在本地8080端口启动JHipster的页面,看到已经用它自己的模板实现了常规页面。我们需要做的只是自己做套Angluar页面,套用该模板下的请求处理就好了。 高级姿势Docker集成在/src/main/docker/目录下,JHipster提供了docker化所需的所有文件,所以开箱即用。例如, 启动一个mysql数据库: docker-compose -f src/main/docker/mysql.yml up -d 停止并删除该mysql数据库: docker-compose -f src/main/docker/mysql.yml down Maven将本项目打包成docker镜像: ./mvnw package -Pprod dockerfile:build 启动项目容器: docker-compose -f src/main/docker/app.yml up -d 如果需要maven打包docker镜像后推到Registry,则需要修改pom.xml,将dockerfile-maven-plugin中注释掉的一段给打开。 CI集成(留坑) 结束语正常情况下,用Jhipster快速实现普通的JavaWeb项目其实仅需三步:1.初始化项目;2.用JDL创建自己的Entity;3.导入JDL;作为一个脚手架,使用起来已经非常方便了,而且它还支持微服务项目。既然谈到脚手架,不由自主的会与JFinal等其他脚手架对比,JHipster不一定比其他脚手架轻快,但好在代码规范,Spring家族全套,回头看看,确实可以解决文初的那两个问题。","categories":[],"tags":[{"name":"SpringBoot","slug":"SpringBoot","permalink":"http://edisonxu.com/tags/SpringBoot/"},{"name":"JHipster","slug":"JHipster","permalink":"http://edisonxu.com/tags/JHipster/"},{"name":"微服务","slug":"微服务","permalink":"http://edisonxu.com/tags/微服务/"}]},{"title":"Axon入门系列(八):AxonFramework与SpringCloud的整合","slug":"axon-spring-cloud","date":"2017-04-24T07:01:51.000Z","updated":"2018-10-30T01:57:10.190Z","comments":true,"path":"2017/04/24/axon-spring-cloud.html","link":"","permalink":"http://edisonxu.com/2017/04/24/axon-spring-cloud.html","excerpt":"上一篇里,我们在利用Axon3的DistributeCommand的JGroup支持,和DistributedEvent对AMQP的支持,实现了分布式环境下的CQRS和EventSourcing。在这一篇中,我们将把Axon3与当下比较火热的微服务框架——SpringCloud进行整合,并将其微服务化。 写在前面的话AxonFramework对SpringCloud的支持,是从3.0.2才开始的,但是在3.0.2和3.0.3两个版本,均存在blocking bug,所以要想与SpringCloud完成整合,版本必须大于等于3.0.4。PS:连续跳坑,debug读代码,帮Axon找BUG,血泪换来的结论……好在社区足够活跃,作者也比较给力,连续更新。","text":"上一篇里,我们在利用Axon3的DistributeCommand的JGroup支持,和DistributedEvent对AMQP的支持,实现了分布式环境下的CQRS和EventSourcing。在这一篇中,我们将把Axon3与当下比较火热的微服务框架——SpringCloud进行整合,并将其微服务化。 写在前面的话AxonFramework对SpringCloud的支持,是从3.0.2才开始的,但是在3.0.2和3.0.3两个版本,均存在blocking bug,所以要想与SpringCloud完成整合,版本必须大于等于3.0.4。PS:连续跳坑,debug读代码,帮Axon找BUG,血泪换来的结论……好在社区足够活跃,作者也比较给力,连续更新。 设计按照微服务的概念,我们把Product和Order各自相关的功能单独抽出来各做出一个服务,即product-service和order-service。与上一篇不同,这里并没有把各自service的command端和query端单独拆成一个service,而是放在一起了。当然,你也可以自行把他们拆开,中间通过mq传递消息。具体架构如下: 前置工作首先,我们在父pom中配置好与SpringCloud集成相关的公共Maven依赖。 对SpringBoot的依赖 (这一块前面我们已经配置过了,这里可以跳过) 对SpringCloud的依赖 对具体SpringCloud组件的依赖 1234567891011121314151617181920212223242526272829303132<modules> <module>common-api</module> <module>config-service</module> <module>discovery-service</module> <module>proxy-service</module> <module>product-service</module> <module>order-service</module></modules><dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement><dependencies> <!-- Spring Cloud Features --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency></dependencies> SpringCloud组件熟悉SpringCloud的朋友,可以直接跳过本章。 Discovery Serivce使用SpringCloud中的Eureka组件,实现服务注册和发现。各个服务本身把自己注册到Eureka上,Proxy Service使用的zuul,在配置了Eureka相关信息后,会自动从Eureka中发现对应服务名及其地址,与配置文件中进行匹配,从而实现动态路由。同时Eureka提供的UI也可以很直观的对服务当前的状态进行监控。使用Eureka非常简单,引入Maven依赖1234<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId></dependency> 然后在SpringBootApplication的类申明上加上@EnableEurekaServer注解即可。对应配置文件如下:1234567891011121314151617181920# Configure this Discovery Servereureka: instance: hostname: localhost lease-expiration-duration-in-seconds: 5 lease-renewal-interval-in-seconds: 5 client: #Not a client, don't register with yourself registerWithEureka: false fetchRegistry: false healthcheck: enabled: true server: enable-self-preservation: falseendpoints: shutdown: enabled: trueserver: port: 1111 #HTTP(Tomcat) port 没什么花样,只是申明自己不是EurekaClient,而是Server。Eureka有一个自我保护机制关闭,默认打开的情况下,当注册的service”挂掉”后,Eureka短时间内并不会直接把它从列表内清除,而是保留一段时间。因为Eureka的设计者认为分布式环境中网络是不可靠的,也许因为网络的原因,Eureka Server没有收到实例的心跳,但并不说命实例就完蛋了,所以这种机制下,它仍然鼓励客户端再去尝试调用这个所谓DOWN状态的实例,如果确实调用失败了,断路器机制还可以派上用场。这里我们方便起见,直接使用server.enable-self-preservation设置为false关闭掉它。(生产别这么用) Proxy Service使用SpringCloud中的zuul组件。具体作用有: 全局网关,屏蔽内部系统和网络 请求拦截和动态路由 请求负载均衡zuul的使用配置非常简单,引入Maven依赖1234<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId></dependency> 然后在SpringBootApplication类申明上加上@EnableZuulProxy和@EnableDiscoveryClient注解即可。@EnableDiscoveryClient是把Proxy Service注册到Eureka上。对应配置文件如下:1234567891011121314151617181920212223242526272829303132333435spring: application: name: proxy-service cloud: config: discovery.enabled: true discovery.serviceId: config-service failFast: false# Discovery Server Accesseureka: client: serviceUrl: defaultZone: http://${config.host:10.1.110.21}:1111/eureka/zuul: ignoredServices: '*' routes: product_command_path: path: /product/** stripPrefix: false serviceId: product-service product_query_path: path: /products/** stripPrefix: false serviceId: product-service order-command_path: serviceId: order-service path: /order/** stripPrefix: false order_query_path: serviceId: order-service path: /orders/** stripPrefix: false spring.application.name 属性指定服务名spring.cloud.config 相关的是配置ConfigService去Eureka上找serviceId为config-service的服务eureka.client.serviceUrl.defaultZone 配置要注册的Eureka的地址ignoredServices设为,即不转发除了下面routes以外的所有请求routes.<xxx>.path 是映射xxx服务与URL地址routes.<xxx>.stripPrefix 是不使用前缀,即将http://product/ 请求直接转发到product-service。如果设置了前缀,那么合法路径则变为http:///product/* 。routes.<xxx>.serviceId 即Eureka上xxx服务所注册的服务名,zuul从Eureka上找到该服务名所对应的服务器信息,从而实现动态路由。这里为了演示zuul对不同路径映射到相同服务,我故意把command和query端的URL地址设为不同,如/product和/products。 Cloud Configs Service使用SpringCloud中的Cloud组件,实现统一文件配置。(未引入SpringCloudBus实现配置修改通知,可自行修改添加。)一样,引入Maven依赖1234<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId></dependency> 在SpringBootApplication的类声明前加上@EnableConfigServer和@EnableDiscoveryClient注解。@EnableDiscoveryClient是把Config Service注册到Eureka上。SpringCloudConfig最大的好处,可以从git读取配置,给不同环境、不同zone设置不同分支,根据profile指定分支,非常方便。在这里为了方便各位自己跑,我把Config Service配置为读取本地文件。123456789101112131415161718192021222324server: port: 1000spring: # Active reading config from local file system profiles: active: native application: name: config-service cloud: config: server: native: searchLocations: /usr/edi/spring/configsmanagement: context-path: /admineureka: client: serviceUrl: defaultZone: http://localhost:1111/eureka/ 业务服务在前一篇CQRS和Event Souring系列(八):DistributeCommand和DistributeEvent 中提到过,DistributedCommandBus不会直接调用command handler,它只是在不同JVM的commandbus之间建立一个“桥梁”,通过指定CommandRouter和CommandBusConnector进行Command的分发。axon-distributed-commandbus-springcloud包提供了SpringCloud环境下的CommandRouter和CommandBusConnector。CommandRouterSpringCloudCommandRouter是该包中CommandRouter的具体实现类,其实是调用了我们在SpringBootApplication中@EnableDiscoveryClient后注入的EurekaClient。每一个Axon的command节点在启动时,会通过DiscoveryClient把本地所有的CommandHandler变向的塞入本地服务在Eureka上的metadata信息中。当DistributedCommandBus发送command时,通过DiscoveryClient从Eureka上获取所有节点信息后,找到metadata中的CommandHandler的信息进行command匹配,分发到匹配的节点去处理command。 CommandBusConnectorSpringHttpCommandBusConnector是CommandBusConnector的具体实现类,它其实在本地起了一个地址为”/spring-command-bus-connector”的REST接口,用以接受来自其他节点的command请求。同时,它也覆写了方法CommandBusConnector中的send方法,用以发送command到经CommandRouter确认的目标地址。当然,它会先判断目标地址是否本地,如果是本地,则直接调用localCommandBus去处理了,否则,则使用RestTemplate将Command发送到远程地址。 所以,启用Axon对SpringCloud的支持,必须要有三步(引入axon-spring-boot-autoconfigure的前提下): 引入axon-distributed-commandbus-springcloud包依赖; 配置文件中axon.distributed.enabled设置为true; 在自己的配置类中提供一个名字为restTemplate的Bean,返回一个RestTemplate的对象; 注意!目前不能在RestTemplate声明时加上@LoadBalance启用Ribbon做负载均衡,因为SpringHttpCommandBusConnector在发送远程command时,会根据Eureka返回的目标Server信息自己build URI,URI中直接使用了ip/hostname,而不是service name。一旦用@LoadBalance,那么请求将被拦截生成RibbonHttpRequest,该Request在执行时会把传入的URI当做service name去与DiscoveryClient取到的所有service的service name匹配,最终会找不到目标节点,而报java.lang.IllegalStateException: No instances available for 10.1.110.21 。 这里10.1.110.21即是前面SpringHttpCommandBusConnector自己从DiscoveryClient那已经解析出来的ip。 Product Serivce核心代码与上一篇并无大区别,依然是CQRS,C端采用JPA将Event持久化到Mysql,而Q端将数据保存在MongoDB,方便查询(好吧,这仅仅是为了show一下怎么样在C、Q端使用不同的持久层而已,存Event的话,MongoDB比MySql适合的多)。这里只把不同地方中关键的列出来说一下,详细请查阅代码。pom依赖引入axon-distributed-commandbus-springcloud包依赖12345<dependency> <groupId>org.axonframework</groupId> <artifactId>axon-distributed-commandbus-springcloud</artifactId> <version>${axon.version}</version></dependency> AMQPConfiguration配置AMQP协议的mq绑定,用于把Event分发到mq中,最终由Order Service的OrderSaga去处理。Product Serivce本身不消费Order Service所产生的Event,本地的EventHandle并不会走MQ。详细配置这里就省略了,可以参见上一篇文章或者看具体代码。 CloudConfiguration这个类啥都不干,只是创建一个restTemplate的实例1234567@Configurationpublic class CloudConfiguration { @Bean public RestTemplate restTemplate(){ return new RestTemplate(); }} 启动类123456789101112@SpringBootApplication@EnableDiscoveryClient@ComponentScan(basePackages = {\"com.edi.learn\"})@EnableJpaRepositories(basePackages = {\"com.edi.learn.cloud.command\"})@EnableMongoRepositories(basePackages = {\"com.edi.learn.cloud.query\"})@EnableAutoConfiguration()public class Application { public static void main(String args[]){ SpringApplication.run(Application.class, args); }} 配置文件的修改上面已经提过了,这里就不再重复。 Order Serivce就启用SpringCloud来说,与上面没有任何区别。为了让OrderSaga能正常收到并处理来自于prodcut-service的事件,必须要进行额外配置。前一篇文章中提到的@ProcessGroup,并不适用于Saga,同时,Axon3中,目前对于Saga处理distributed event并不是很友好,3.0.4以前,Saga只能支持绑定一个EventStore,但是分布式情况下,一个service可能要监听多个queue,所以3.0.4中,支持了自定义Saga配置,即可以声明一个<saga_name>+SagaConfiguration作为Bean名,并返回SagaConfiguration类型的Bean。为了让Saga能处理来自于外部MQ的事件,我们必须提供一个orderSagaConfiguration。123456789101112131415161718192021222324@Beanpublic SpringAMQPMessageSource queueMessageSource(Serializer serializer){ return new SpringAMQPMessageSource(serializer){ @RabbitListener(queues = \"orderqueue\") @Override @Transactional public void onMessage(Message message, Channel channel) throws Exception { LOGGER.debug(\"Message received: \"+message.toString()); super.onMessage(message, channel); } };}@Beanpublic SagaConfiguration<OrderSaga> orderSagaConfiguration(Serializer serializer){ SagaConfiguration<OrderSaga> sagaConfiguration = SagaConfiguration.subscribingSagaManager(OrderSaga.class, c-> queueMessageSource(serializer)); //sagaConfiguration.registerHandlerInterceptor(c->transactionManagingInterceptor()); return sagaConfiguration;}@Beanpublic TransactionManagingInterceptor transactionManagingInterceptor(){ return new TransactionManagingInterceptor(new SpringTransactionManager(transactionManager));} 如上面代码,自行指定Saga的message source,这样来自于product-service写入mq的ProductReservedEvent等事件就能被Saga正确处理。这里要注意的是事务问题,由于我们是通过MQ的onMessage来启动具体的SagaCommandHandler,上下文中并未定义事务特性,但是由于我们引入了Spring的jpa包,axon3的auto configuration会自动启用SagaJpaRepository,也就是说,onMessage方法线程执行时,会牵扯到DB的更新,必须得给它指定一个transaction manager。这里有两种方法: 使用@Transactional 注解,让Spring自行配置; 在SagaConfiguration中注册TransactionManagingInterceptor。 另外,由于在创建订单时,只传了Product的Id,根据id去查询当前product的最新详情,需要请求Product Service的query端。这个query端我们是用spring-boot-starter-data-rest直接暴露出去的HATEOAS(Hypermedia as the Engine of Application State)风格的RESTFul接口。即是说,要做一个跨服务的REST请求,且要支持HATEOAS,那么我们就使用Feign加上spring-boot-starter-hateoas。 更新pom 12345678<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId></dependency> 在order-service中添加一个Feign Client 123456789@FeignClient(value = \"product-service\")public interface ProductService { @RequestMapping(value = \"/products\", method = RequestMethod.GET) Resources<ProductDto> getProducts(); @RequestMapping(value = \"/products/{id}\", method = RequestMethod.GET) ProductDto getProduct(@PathVariable(\"id\") String productId);} 在SpringBootApplication中启用FeignClient和HypermediaSupport 12345678910111213@SpringBootApplication@EnableDiscoveryClient@ComponentScan(basePackages = {\"com.edi.learn\"})@EnableJpaRepositories(basePackages = {\"com.edi.learn.cloud.command\"})@EnableMongoRepositories(basePackages = {\"com.edi.learn.cloud.query\"})@EnableFeignClients(basePackages = {\"com.edi.learn.cloud.common.web\"})@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)public class Application { public static void main(String args[]){ SpringApplication.run(Application.class, args); }} ProductDto都是封装属性的POJO,就不写了。这样我们就可以在代码中直接注入ProductService,并调用相应方法从product-service端取数据了。 总结至此,Axon3与SpringCloud的集成已完毕。Axon3使用SpringCloud提供的服务注册和发现机制,来进行Command的分发和处理。具体运行情况我就不写了,大家可自行修改order-service的配置,去跑多个order-service。留个悬念,由于是同一段代码和配置,mq我们使用fanout,即分发的模式,所有节点都会收到ProductReservedEvent,是否所有节点都会处理呢? 写在后面的话截止到本篇,Axon3使用的大部分功能都已经做了入门介绍,并写了例子,作为研究,算是入门了,尤其是文档中没有说明的一些关键地方,我都在文中提了出来。掉过不少坑,看了很多源码, 回头看来,我对Axon3的设计是肯定与失望并存。肯定的是Axon3的易用性与性能,尤其是DisruptorCommandBus配合CachingGenericEventSourcingRepository(采用了LMAX的Disruptor框架,可以看下一篇比较早的文章介绍,猛击这里或中文翻译版);失望的是Axon3更多的优化和针对都集中在单体应用上,对分布式和微服务的集成稍显简单,例如负载均衡的支持、容错性的支持等,目前尚未看到介绍。当然,这块现在也才刚刚起步,后续应该会变得越来越好。原期望于Axon3直接把这块做掉或者提供支持,现在看来是否我想太多,这块本就不该它做呢?欢迎加群57241527讨论。 照例,本文源码:https://github.com/EdisonXu/sbs-axon/tree/master/lesson-7","categories":[],"tags":[{"name":"eventsourcing","slug":"eventsourcing","permalink":"http://edisonxu.com/tags/eventsourcing/"},{"name":"CQRS","slug":"CQRS","permalink":"http://edisonxu.com/tags/CQRS/"},{"name":"axon","slug":"axon","permalink":"http://edisonxu.com/tags/axon/"},{"name":"DDD","slug":"DDD","permalink":"http://edisonxu.com/tags/DDD/"}]},{"title":"Axon入门系列(七):DistributeCommand和DistributeEvent","slug":"axon-distribute","date":"2017-04-01T07:01:51.000Z","updated":"2018-10-30T01:57:01.016Z","comments":true,"path":"2017/04/01/axon-distribute.html","link":"","permalink":"http://edisonxu.com/2017/04/01/axon-distribute.html","excerpt":"上一篇我们才算真正实现了一个基于Axon3的例子,本篇我们来尝试实现在分布式环境下利用Axon3做CQRS,即把CommandSide和QuerySide变成两个独立应用,分别可以启多份实例。 首先,我们回顾一下CQRS&EventSourcing模式下,整个架构的关键点,或者说最大的特点: CommandSide和QuerySide的持久层分离; 保存对Aggregate状态造成变化的Event,而不是状态本身; Aggregate的状态全局原子化操作; 适用于读大于写的场景;我们前面的例子,是在一个应用里面实现了CQRS模式,而在分布式场景下,有如下要求: CommandSide和QuerySide可以不在同一个节点(甚至不在同一个应用)下; CommandSide不同的CommandHandler、EventHandler可以不在同一个节点; 不同CommandSide对同一个Aggregate的操作应具有原子性;我们来一步步满足这三个要求。","text":"上一篇我们才算真正实现了一个基于Axon3的例子,本篇我们来尝试实现在分布式环境下利用Axon3做CQRS,即把CommandSide和QuerySide变成两个独立应用,分别可以启多份实例。 首先,我们回顾一下CQRS&EventSourcing模式下,整个架构的关键点,或者说最大的特点: CommandSide和QuerySide的持久层分离; 保存对Aggregate状态造成变化的Event,而不是状态本身; Aggregate的状态全局原子化操作; 适用于读大于写的场景;我们前面的例子,是在一个应用里面实现了CQRS模式,而在分布式场景下,有如下要求: CommandSide和QuerySide可以不在同一个节点(甚至不在同一个应用)下; CommandSide不同的CommandHandler、EventHandler可以不在同一个节点; 不同CommandSide对同一个Aggregate的操作应具有原子性;我们来一步步满足这三个要求。 拆分CommandSide和QuerySide这个其实比较好解决,直接把两者分别用两个SpringBoot来承载就好了,只需要引入一个MQ,传递从CommandSide到QuerySide的事件就好了。Axon提供了对AMQP协议的MQ的支持,我们可以直接拿来用。当然,你也可以用Kafka等其他MQ,只是需要自己实现了。具体关于Axon对AMQP的支持,在后面会详述。 实现CommandHandler的分布式调用前文中提到过,Axon提供的四种CommandBus的实现中,有一个DistributedCommandBus,DistributedCommandBus不会直接调用command handler,它只是在不同JVM的commandbus之间建立一个“桥梁”。每个JVM上的DistributedCommandBus被称为“Segment”。DistributedCommandBus要求提供两个参数: CommandRouter提供路由表,指明应当把Command发到哪里。CommandRouter的实现必须提供Routing Strategy,以此来计算Routing Key。Axon提供了两种Routing Strategy: MetaDataRoutingStrategy 使用CommandMessage中的MetaData的property来找到路由key AnnotationRoutingStrategy(默认) 使用Command中@TargetIdentifier标识的field做路由key所以,当使用DistributeCommandBus时,如果使用默认的Routing Strategy,一定要在Command中提供@TargetIdentifier CommandBusConnector管理链接,提供发送、订阅方法Axon目前提供了两种Connector的实现:JGroupsConnector和SpringCloudConnector。本文将使用JGroup,后者将放到后一篇与SpringCloud集成一文中使用。起用JGroupsConnector很简单,只需要确保如下两个依赖存在:12345678910<dependency> <groupId>org.axonframework</groupId> <artifactId>axon-spring-boot-starter-jgroups</artifactId> <version>${axon.version}</version></dependency><dependency> <groupId>org.axonframework</groupId> <artifactId>axon-spring-boot-autoconfigure</artifactId> <version>${axon.version}</version></dependency> axon-spring-boot-autoconfigure提供了自动配置,在AxonAutoConfiguration类中,可以发现有如下源码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188@ConditionalOnClass(name = {\"org.axonframework.jgroups.commandhandling.JGroupsConnector\", \"org.jgroups.JChannel\"})@EnableConfigurationProperties(JGroupsConfiguration.JGroupsProperties.class)@ConditionalOnProperty(\"axon.distributed.jgroups.enabled\")@AutoConfigureAfter(JpaConfiguration.class)@Configurationpublic static class JGroupsConfiguration { private static final Logger logger = LoggerFactory.getLogger(JGroupsConfiguration.class); @Autowired private JGroupsProperties jGroupsProperties; @ConditionalOnProperty(\"axon.distributed.jgroups.gossip.autoStart\") @Bean(destroyMethod = \"stop\") public GossipRouter gossipRouter() { Matcher matcher = Pattern.compile(\"([^[\\\\[]]*)\\\\[(\\\\d*)\\\\]\").matcher(jGroupsProperties.getGossip().getHosts()); if (matcher.find()) { GossipRouter gossipRouter = new GossipRouter(matcher.group(1), Integer.parseInt(matcher.group(2))); try { gossipRouter.start(); } catch (Exception e) { logger.warn(\"Unable to autostart start embedded Gossip server: {}\", e.getMessage()); } return gossipRouter; } else { logger.error(\"Wrong hosts pattern, cannot start embedded Gossip Router: \" + jGroupsProperties.getGossip().getHosts()); } return null; } @ConditionalOnMissingBean @Primary @Bean public DistributedCommandBus distributedCommandBus(CommandRouter router, CommandBusConnector connector) { DistributedCommandBus commandBus = new DistributedCommandBus(router, connector); commandBus.updateLoadFactor(jGroupsProperties.getLoadFactor()); return commandBus; } @ConditionalOnMissingBean({CommandRouter.class, CommandBusConnector.class}) @Bean public JGroupsConnectorFactoryBean jgroupsConnectorFactoryBean(Serializer serializer, @Qualifier(\"localSegment\") CommandBus localSegment) { System.setProperty(\"jgroups.tunnel.gossip_router_hosts\", jGroupsProperties.getGossip().getHosts()); System.setProperty(\"jgroups.bind_addr\", String.valueOf(jGroupsProperties.getBindAddr())); System.setProperty(\"jgroups.bind_port\", String.valueOf(jGroupsProperties.getBindPort())); JGroupsConnectorFactoryBean jGroupsConnectorFactoryBean = new JGroupsConnectorFactoryBean(); jGroupsConnectorFactoryBean.setClusterName(jGroupsProperties.getClusterName()); jGroupsConnectorFactoryBean.setLocalSegment(localSegment); jGroupsConnectorFactoryBean.setSerializer(serializer); jGroupsConnectorFactoryBean.setConfiguration(jGroupsProperties.getConfigurationFile()); return jGroupsConnectorFactoryBean; } @ConfigurationProperties(prefix = \"axon.distributed.jgroups\") public static class JGroupsProperties { private Gossip gossip; /** * Enables JGroups configuration for this application */ private boolean enabled = false; /** * The name of the JGroups cluster to connect to. Defaults to \"Axon\". */ private String clusterName = \"Axon\"; /** * The JGroups configuration file to use. Defaults to a TCP Gossip based configuration */ private String configurationFile = \"default_tcp_gossip.xml\"; /** * The address of the network interface to bind JGroups to. Defaults to a global IP address of this node. */ private String bindAddr = \"GLOBAL\"; /** * Sets the initial port to bind the JGroups connection to. If this port is taken, JGroups will find the * next available port. */ private String bindPort = \"7800\"; /** * Sets the loadFactor for this node to join with. The loadFactor sets the relative load this node will * receive compared to other nodes in the cluster. Defaults to 100. */ private int loadFactor = 100; public Gossip getGossip() { return gossip; } public void setGossip(Gossip gossip) { this.gossip = gossip; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public String getClusterName() { return clusterName; } public void setClusterName(String clusterName) { this.clusterName = clusterName; } public String getConfigurationFile() { return configurationFile; } public void setConfigurationFile(String configurationFile) { this.configurationFile = configurationFile; } public String getBindAddr() { return bindAddr; } public void setBindAddr(String bindAddr) { this.bindAddr = bindAddr; } public String getBindPort() { return bindPort; } public void setBindPort(String bindPort) { this.bindPort = bindPort; } public int getLoadFactor() { return loadFactor; } public void setLoadFactor(int loadFactor) { this.loadFactor = loadFactor; } public static class Gossip { /** * Whether to automatically attempt to start a Gossip Routers. The host and port of the Gossip server * are taken from the first define host in 'hosts'. */ private boolean autoStart = false; /** * Defines the hosts of the Gossip Routers to connect to, in the form of host[port],... * <p> * If autoStart is set to {@code true}, the first host and port are used as bind address and bind port * of the Gossip server to start. * <p> * Defaults to localhost[12001]. */ private String hosts = \"localhost[12001]\"; public boolean isAutoStart() { return autoStart; } public void setAutoStart(boolean autoStart) { this.autoStart = autoStart; } public String getHosts() { return hosts; } public void setHosts(String hosts) { this.hosts = hosts; } } }} 可以看到我们只需在application.properties中添加12axon.distributed.jgroups.enabled=trueaxon.distributed.jgroups.gossip.autoStart=true 就可以启用JGroupsConnector。同时也可以用前缀axon.distributed.jgroups加上JGroupsProperties里定义的各种field名来做JGroup的配置。(默认连接本地7800端口)这里值得注意的是: JGroupsConnectorFactoryBean实现的方法中,有一段System.setProperty(“jgroups.tunnel.gossiprouterhosts”, jGroupsProperties.getGossip().getHosts()); ,如果axon.distributed.jgroups.gossip.autoStart未设为true(默认false),那么getGossip()显然将会报空指针异常。 JacksonSerializer的实现中,并未去考虑Jackson对Exception的处理(objectMapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)),导致一旦在Command执行时发生异常,DistributeCommandBus也会尝试把这个Exception消息进行序列化,而Jackson默认是无法处理java.lang.Throwable类的,就会发生序列化错误org.codehaus.jackson.map.exc.UnrecognizedPropertyException: Unrecognized field “cause” (Class java.lang.Throwable), not marked as ignorable,从而导致把真正的Exception给掩埋掉了。所以,这里我就改回默认的XStreamSerializer。 默认情况下,localSegment是SimpleCommandBus,所以参考前文,可以使用sendAndWait把异常抛到最前端处理,或者用send(command, callback)传入一个callback,在callback的onFailure方法对Throwable进行处理。 实现EventHandler的分布式调用通常情况下,Event的分发我们第一时间想到的就是MQ,Axon也不例外,提供了对AMQP(Advanced Message Queuing Protocol)的支持,例如Rabbit MQ。引入如下Maven依赖:123456789101112<dependency> <groupId>org.axonframework</groupId> <artifactId>axon-amqp</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId></dependency><dependency> <groupId>org.axonframework</groupId> <artifactId>axon-spring-boot-autoconfigure</artifactId></dependency> spring-boot-starter-amqp提供具体AMQP实现的服务,axon-amqp提供具体的Event分发机制实现,axon-spring-boot-autoconfigure提供AMQP的自动配置。AxonAutoConfiguration中,关于AMQP部分的源码如下:1234567891011121314151617181920212223242526272829303132333435363738394041424344@ConditionalOnClass({SpringAMQPPublisher.class, ConnectionFactory.class})@EnableConfigurationProperties(AMQPProperties.class)@Configurationpublic static class AMQPConfiguration { @Autowired private AMQPProperties amqpProperties; @ConditionalOnMissingBean @Bean public RoutingKeyResolver routingKeyResolver() { return new PackageRoutingKeyResolver(); } @ConditionalOnMissingBean @Bean public AMQPMessageConverter amqpMessageConverter(Serializer serializer, RoutingKeyResolver routingKeyResolver) { return new DefaultAMQPMessageConverter(serializer, routingKeyResolver, amqpProperties.isDurableMessages()); } @ConditionalOnProperty(\"axon.amqp.exchange\") @Bean(initMethod = \"start\", destroyMethod = \"shutDown\") public SpringAMQPPublisher amqpBridge(EventBus eventBus, ConnectionFactory connectionFactory, AMQPMessageConverter amqpMessageConverter) { SpringAMQPPublisher publisher = new SpringAMQPPublisher(eventBus); publisher.setExchangeName(amqpProperties.getExchange()); publisher.setConnectionFactory(connectionFactory); publisher.setMessageConverter(amqpMessageConverter); switch (amqpProperties.getTransactionMode()) { case TRANSACTIONAL: publisher.setTransactional(true); break; case PUBLISHER_ACK: publisher.setWaitForPublisherAck(true); break; case NONE: break; default: throw new IllegalStateException(\"Unknown transaction mode: \" + amqpProperties.getTransactionMode()); } return publisher; }} 可以看到,只要引入了Spring关于AMQP的starter包,我们只需要在application.properties中用axon.amqp.exchange=Axon.EventBus指明AMQP的exchange名字就可以启用了,非常方便。另外就是需要给spring-boot-starter-amqp提供amqp具体实现的配置,这里我们以RabbitMq为例:123456# mqspring.rabbitmq.host=10.1.110.21spring.rabbitmq.port=5672spring.rabbitmq.username=axonspring.rabbitmq.password=axonaxon.amqp.exchange=Axon.EventBus RabbitMqServer的搭建我这里就不叙述了,网上一搜一大把。但为了方便理解,我还是简单介绍下AMQP和RabbitMq的一些关键要素。首先看一下AMQP的”生产/消费”模型图我们关注里面的三个核心概念 Exchange: 交换器,message到达broker的第一站,根据分发策略,匹配查询表中的routing key,分发消息到queue中去。 Queue:消息最终被送到这里等待consumer取走。一个message可以被同时拷贝到多个queue中。 Binding:Exchange与Queue之间的绑定关系,指定了绑定策略,即消息的分发策略。分发策略有以下四种: direct“先匹配, 再投送”. 即在绑定时设定一个routing_key, 消息的routing_key匹配时, 才会被交换器投送到绑定的队列中去. fanout把消息转发给所有绑定的队列上, 就是一个”广播”行为. topic与direct类似,只是绑定的routing_key支持匹配规则(并不是正则!),会把消息自己的routing_key与绑定的routing_key进行匹配操作,只把匹配成功的发到对应queue中。这里有个“坑”,rabbit提供的*绑定一个任意字母,#绑定0个或多个字母匹配规则中,#并不能直接使用,比如#test#就无法匹配aatest33,必须要#.test.#才起作用,匹配aa.test.33,也是醉了。所以Axon默认提供的RoutingKey生成就是根据包名来匹配…… headers不使用routing_key,而使用headers来做匹配。 那么我们来对AMQP在代码中做exchange和queue的绑定,以及对event的listen动作。AMQPConfiguration12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455@Configurationpublic class AMQPConfiguration { @Value(\"${axon.amqp.exchange}\") private String exchangeName; @Bean public Queue productQueue(){ return new Queue(\"product\", true); } @Bean public Queue orderQueue(){ return new Queue(\"order\",true); } @Bean public Exchange exchange(){ return ExchangeBuilder.topicExchange(exchangeName).durable(true).build(); } @Bean public Binding productQueueBinding() { return BindingBuilder.bind(productQueue()).to(exchange()).with(\"#.product.#\").noargs(); } @Bean public Binding orderQueueBinding() { return BindingBuilder.bind(orderQueue()).to(exchange()).with(\"#.order.#\").noargs(); } /*@Bean public SpringAMQPMessageSource productQueueMessageSource(Serializer serializer){ return new SpringAMQPMessageSource(serializer){ @RabbitListener(queues = \"product\") @Override public void onMessage(Message message, Channel channel) throws Exception { LOGGER.debug(\"Product message received: \"+message.toString()); super.onMessage(message, channel); } }; } @Bean public SpringAMQPMessageSource orderQueueMessageSource(Serializer serializer){ return new SpringAMQPMessageSource(serializer){ @RabbitListener(queues = \"order\") @Override public void onMessage(Message message, Channel channel) throws Exception { LOGGER.debug(\"Order message received: \"+message.toString()); super.onMessage(message, channel); } }; }*/} 注意,由于本例中,我并没有把Product和Order相关的Service拆分成两个应用,仍然在一个CommandSide中,所以其实我们根本用不到分布式EventHandler,local可以完成的操作,放到其他node去做,反而降低了性能。所以,我这里并没有在CommandSide的这个AMQPConfiguration中去配置监听queue。这里的队列其实是CommandSide和QuerySide之间用的。但配置和原理都是一样的,如果把Product和Order分开,ProductReservedEvent在ProductServcices所在节点扔到队列后,可按需配置绑定,让OrderService能够取到该事件,交给Saga中的EventHandler去处理。在后面与SpringCloud集成的一文中,就会这样做。QuerySide的AMQPConfiguration与上面一致,但是要打开被注释掉的部分。因为exchange和queue是自动创建的,有可能QuerySide先启动,所以必须要在QuerySide也加上exchange和queue的定义及绑定策略。@RabbitListener(queues = "product")用来指定当前AMQPMessageSource要监听哪个queue。同时,还需要修改application.properties,来绑定AMQPMessageSource和具体的EventHandler注册类12axon.eventhandling.processors.product.source=productQueueMessageSourceaxon.eventhandling.processors.order.source=orderQueueMessageSource axon.eventhandling.processors.[processors_group_name].source中,前面axon.eventhandling.processors.[processors_group_name]其实是一个ProcessingGroup,Axon提供了注解@ProcessingGroup(“[processors_group_name]”)来进行标识。所以我们需要在QuerySide的ProductEventHandler和OrderEventHandler上面增加该注解1234567@Component@ProcessingGroup(\"order\")public class OrderEventHandler{}@Component@ProcessingGroup(\"product\")public class ProductEventHandler {} 测试为方便测试,我们来增加一个对Product库存进行调整的接口,这样可以启动两个CommandSide,同时对库存进行调整,看看会不会有并发问题。同样,先定义Commmand和对应的Event:ChangeStockCommand(productId, number)IncreaseStockCommand extends ChangeStockCommandDecreaseStockCommand extends ChangeStockCommandIncreaseStockEvent(productId, number)DecreaseStockEvent(productId, number) 修改ProductAggregate,增加对应的CommandHandler和EventHandler1234567891011121314151617181920212223242526272829@Aggregatepublic class ProductAggregate { ...... @CommandHandler public void handle(IncreaseStockCommand command) { apply(new IncreaseStockEvent(command.getId(),command.getNumber())); } @CommandHandler public void handle(DecreaseStockCommand command) { if(stock>=command.getNumber()) apply(new DecreaseStockEvent(command.getId(),command.getNumber())); else throw new NoEnoughStockException(\"No enough items\"); } @EventHandler public void on(IncreaseStockEvent event){ stock = stock + event.getNumber(); LOGGER.info(\"Product {} stock increase {}, current value: {}\", id, event.getNumber(), stock); } @EventHandler public void on(DecreaseStockEvent event){ stock = stock - event.getNumber(); LOGGER.info(\"Product {} stock decrease {}, current value: {}\", id, event.getNumber(), stock); }} 最后对外增加一个REST接口:1234567891011121314151617181920212223242526272829303132333435@RestController@RequestMapping(\"/product\")public class ProductController { ...... @PutMapping(\"/{id}\") public void change(@PathVariable(value = \"id\") String id, @RequestBody(required = true) JSONObject input, HttpServletResponse response){ boolean isIncrement = input.getBooleanValue(\"incremental\"); int number = input.getIntValue(\"number\"); ChangeStockCommand command = isIncrement? new IncreaseStockCommand(id, number) : new DecreaseStockCommand(id, number); try { // multiply 100 on the price to avoid float number //commandGateway.send(command, LoggingCallback.INSTANCE); commandGateway.sendAndWait(command); response.setStatus(HttpServletResponse.SC_OK);// Set up the 201 CREATED response return; } catch (CommandExecutionException cex) { LOGGER.warn(\"Add Command FAILED with Message: {}\", cex.getMessage()); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); if (null != cex.getCause()) { LOGGER.warn(\"Caused by: {} {}\", cex.getCause().getClass().getName(), cex.getCause().getMessage()); if (cex.getCause() instanceof ConcurrencyException) { LOGGER.warn(\"Concurrent issue happens for product {}\", id); response.setStatus(HttpServletResponse.SC_CONFLICT); } } } catch (Exception e) { // should not happen LOGGER.error(\"Unexpected exception is thrown\", e); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } }} 这里我用了sendAndWait,把Exception一路抛上来在Controller捕获。你也可以用我注掉的那段send(command,callback),传入一个callback,在callback的onFailure方法去处理。同样,QuerySide要对这两个事件进行处理ProductEventHandler12 好,最后我们把CommandSide的server.port配成0(随机端口),启动两个CommandSide(假定一个端口为<first_port>,一个为<second_port>)和一个QuerySide。 POST请求到http://127.0.0.1:<first_port>/product/1?name=ttt&price=10&stock=100 创建商品; POST请求到http://127.0.0.1:<second_port>/product/1?name=ttt&price=10&stock=100 会发现报错,商品已存在; GET请求到http://127.0.0.1:8080/product/1 在QuerySide查看商品是否创建成功; PUT如下json到http://127.0.0.1:<first_port>/product/1 来增加库存; 1234{ \"incremental\":true, \"number\":10} PUT如下json到http://127.0.0.1:<second_port>/product/1 来减少库存; 1234{ \"incremental\":false, \"number\":101} 重置MongoDB的库,同时发送3、4,看看结果。其实我们如果去MongoDB的Events里面查看,数据如下: 12345678910111213141516171819202122232425262728293031323334353637> db.events.find().pretty(){ \"_id\" : ObjectId(\"58ec4ef673bc0c1c188117b9\"), \"aggregateIdentifier\" : \"1\", \"type\" : \"ProductAggregate\", \"sequenceNumber\" : NumberLong(0), \"serializedPayload\" : \"<com.edi.learn.axon.events.product.ProductCreatedEvent><id>1</id><name>ttt</name><price>1000</price><stock>100</stock></com.edi.learn.axon.events.product.ProductCreatedEvent>\", \"timestamp\" : \"2017-04-11T03:35:18.310Z\", \"payloadType\" : \"com.edi.learn.axon.events.product.ProductCreatedEvent\", \"payloadRevision\" : null, \"serializedMetaData\" : \"<meta-data><entry><string>traceId</string><string>af292c24-bde4-4ba1-a190-9743822f839c</string></entry><entry><string>correlationId</string><string>af292c24-bde4-4ba1-a190-9743822f839c</string></entry></meta-data>\", \"eventIdentifier\" : \"ed244ef3-a1fe-48fb-99b8-39ebd2444cc1\"}{ \"_id\" : ObjectId(\"58ec4f0273bc0c1c188117ba\"), \"aggregateIdentifier\" : \"1\", \"type\" : \"ProductAggregate\", \"sequenceNumber\" : NumberLong(1), \"serializedPayload\" : \"<com.edi.learn.axon.events.product.IncreaseStockEvent><id>1</id><number>10</number></com.edi.learn.axon.events.product.IncreaseStockEvent>\", \"timestamp\" : \"2017-04-11T03:35:30.728Z\", \"payloadType\" : \"com.edi.learn.axon.events.product.IncreaseStockEvent\", \"payloadRevision\" : null, \"serializedMetaData\" : \"<meta-data><entry><string>traceId</string><string>05252e0c-eb0b-4ed0-945c-0134fa94b6ba</string></entry><entry><string>correlationId</string><string>05252e0c-eb0b-4ed0-945c-0134fa94b6ba</string></entry></meta-data>\", \"eventIdentifier\" : \"f6b9786d-4abd-4407-a40b-880f88738b4b\"}{ \"_id\" : ObjectId(\"58ec4f0d73bc0c1ad83281d6\"), \"aggregateIdentifier\" : \"1\", \"type\" : \"ProductAggregate\", \"sequenceNumber\" : NumberLong(2), \"serializedPayload\" : \"<com.edi.learn.axon.events.product.DecreaseStockEvent><id>1</id><number>101</number></com.edi.learn.axon.events.product.DecreaseStockEvent>\", \"timestamp\" : \"2017-04-11T03:35:41.474Z\", \"payloadType\" : \"com.edi.learn.axon.events.product.DecreaseStockEvent\", \"payloadRevision\" : null, \"serializedMetaData\" : \"<meta-data><entry><string>traceId</string><string>cf21b4a8-dfae-4da8-a6e0-964876c101c3</string></entry><entry><string>correlationId</string><string>cf21b4a8-dfae-4da8-a6e0-964876c101c3</string></entry></meta-data>\", \"eventIdentifier\" : \"ac9db091-73fd-4830-9ddb-85fea3a13206\"} 其实可以发现sequenceNumber一值是递增的,说明Event在分布式环境中也是严格按时间排序的。这样即便是在两个不同的CommandSide节点,当我们尝试去改变Aggregate的状态时,Axon会做ES来从Repository里获取当前Aggregate的最新状态,从而实现了原子性操作。 本文完整代码:https://github.com/EdisonXu/sbs-axon/tree/master/lesson-6","categories":[],"tags":[{"name":"eventsourcing","slug":"eventsourcing","permalink":"http://edisonxu.com/tags/eventsourcing/"},{"name":"CQRS","slug":"CQRS","permalink":"http://edisonxu.com/tags/CQRS/"},{"name":"axon","slug":"axon","permalink":"http://edisonxu.com/tags/axon/"},{"name":"DDD","slug":"DDD","permalink":"http://edisonxu.com/tags/DDD/"}]},{"title":"Axon入门系列(六):Saga的使用","slug":"axon-saga","date":"2017-03-31T03:37:32.000Z","updated":"2018-10-30T01:56:51.674Z","comments":true,"path":"2017/03/31/axon-saga.html","link":"","permalink":"http://edisonxu.com/2017/03/31/axon-saga.html","excerpt":"在上一篇里面,我们正式的使用了CQRS模式完成了AXON的第一个真正的例子,但是细心的朋友会发现一个问题,创建订单时并没有检查商品库存。库存是否足够直接回导致订单状态的成功与否,在并发时可能还会出现超卖。当库存不足时还需要回滚订单,所以这里出现了复杂的跨Aggregate事务问题。Saga就是为解决这里复杂流程而生的。 SagaSaga 这个名词最早是由Hector Garcia-Molina和Kenneth Salem写的Sagas这篇论文里提出来的,但其实Saga并不是什么新事物,在我们传统的系统设计中,它有个更熟悉的名字——“ProcessManager”,只是换了个马甲,还是干同样的事——组合一组逻辑处理复杂流程。但它与我们平常理解的“ProgressManager”又有不同,它的提出,最早是是为了解决分布式系统中长时间运行事务(long-running business process)的问题,把单一的transaction按照步骤分成一组若干子transaction,通过补偿机制实现最终一致性。举个例子,在一个交易环节中有下单支付两个步骤,如果是传统方式,两个步骤在一个事务里,统一成功或回滚,然而如果支付时间很长,那么就会导致第一步,即下单这里所占用的资源被长时间锁定,可能会对系统可用性造成影响。如果用Saga来实现,那么下单是一个独立事务,下单的事务先提交,提交成功后开始支付的事务,如果支付成功,则支付的事务也提交,整个流程就算完成,但是如果支付事务执行失败,那么支付需要回滚,因为这时下单事务已经提交,则需要对下单操作进行补偿操作(可能是回滚,也可能是变成新状态)。可以看到Saga是牺牲了数据的强一致性,实现最终一致性。","text":"在上一篇里面,我们正式的使用了CQRS模式完成了AXON的第一个真正的例子,但是细心的朋友会发现一个问题,创建订单时并没有检查商品库存。库存是否足够直接回导致订单状态的成功与否,在并发时可能还会出现超卖。当库存不足时还需要回滚订单,所以这里出现了复杂的跨Aggregate事务问题。Saga就是为解决这里复杂流程而生的。 SagaSaga 这个名词最早是由Hector Garcia-Molina和Kenneth Salem写的Sagas这篇论文里提出来的,但其实Saga并不是什么新事物,在我们传统的系统设计中,它有个更熟悉的名字——“ProcessManager”,只是换了个马甲,还是干同样的事——组合一组逻辑处理复杂流程。但它与我们平常理解的“ProgressManager”又有不同,它的提出,最早是是为了解决分布式系统中长时间运行事务(long-running business process)的问题,把单一的transaction按照步骤分成一组若干子transaction,通过补偿机制实现最终一致性。举个例子,在一个交易环节中有下单支付两个步骤,如果是传统方式,两个步骤在一个事务里,统一成功或回滚,然而如果支付时间很长,那么就会导致第一步,即下单这里所占用的资源被长时间锁定,可能会对系统可用性造成影响。如果用Saga来实现,那么下单是一个独立事务,下单的事务先提交,提交成功后开始支付的事务,如果支付成功,则支付的事务也提交,整个流程就算完成,但是如果支付事务执行失败,那么支付需要回滚,因为这时下单事务已经提交,则需要对下单操作进行补偿操作(可能是回滚,也可能是变成新状态)。可以看到Saga是牺牲了数据的强一致性,实现最终一致性。 Saga的概念使得强一致性的分布式事务不再是唯一的解决方案,通过保证事务中每一步都可以一个补偿机制,在发生错误后执行补偿事务来保证系统的可用性和最终一致性。 在CQRS中,我们尽量遵从“聚合尽量设计的小,且一次修改只修改一个聚合”的原则(与OO中高内聚,低耦合的原则相同),所以当我们需要完成一个复杂流程时,就可能涉及到对多个Aggregate状态的改变,我们就可以把整个过程管理统一放到Saga来定义。 设计把我们的订单创建流程修改成以下: 创建Command和Event在上一篇例子的基础上,创建如下Command和Event-ReserveProductCommand (orderId, productId, number)-RollbackReservationCommand (orderId, productId, number)-ConfirmOrderCommand (orderId)-RollbackOrderCommand (orderId)-ProductReservedEvent (orderId, productId, number)-ProductNotEnoughEvent (orderId, productId)-OrderCancelledEvent (orderId)-OrderConfirmedEvent (orderId) 都是POJO,这里我就不放代码了。具体可以去源代码看。 创建Saga123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081@Sagapublic class OrderSaga { private static final Logger LOGGER = getLogger(OrderSaga.class); private OrderId orderIdentifier; private Map<String, OrderProduct> toReserve; private Map<String, OrderProduct> toRollback; private int toReserveNumber; private boolean needRollback; @Autowired private transient CommandGateway commandGateway; @StartSaga @SagaEventHandler(associationProperty = \"orderId\") public void handle(OrderCreatedEvent event){ this.orderIdentifier = event.getOrderId(); this.toReserve = event.getProducts(); toRollback = new HashMap<>(); toReserveNumber = toReserve.size(); event.getProducts().forEach((id,product)->{ ReserveProductCommand command = new ReserveProductCommand(orderIdentifier, id, product.getAmount()); commandGateway.send(command); }); } @SagaEventHandler(associationProperty = \"orderId\") public void handle(ProductNotEnoughEvent event){ LOGGER.info(\"No enough item to buy\"); toReserveNumber--; needRollback=true; if(toReserveNumber==0) tryFinish(); } private void tryFinish() { if(needRollback){ toReserve.forEach((id, product)->{ if(!product.isReserved()) return; toRollback.put(id, product); commandGateway.send(new RollbackReservationCommand(orderIdentifier, id, product.getAmount())); }); if(toRollback.isEmpty()) commandGateway.send(new RollbackOrderCommand(orderIdentifier)); return; } commandGateway.send(new ConfirmOrderCommand(orderIdentifier)); } @SagaEventHandler(associationProperty = \"orderId\") public void handle(ReserveCancelledEvent event){ toRollback.remove(event.getProductId()); if(toRollback.isEmpty()) commandGateway.send(new RollbackOrderCommand(event.getOrderId())); } @SagaEventHandler(associationProperty = \"id\", keyName = \"orderId\") @EndSaga public void handle(OrderCancelledEvent event) throws OrderCreateFailedException { LOGGER.info(\"Order {} is cancelled\", event.getId()); // throw exception here will not cause the onFailure() method in the command callback //throw new OrderCreateFailedException(\"Not enough product to reserve!\"); } @SagaEventHandler(associationProperty = \"orderId\") public void handle(ProductReservedEvent event){ OrderProduct reservedProduct = toReserve.get(event.getProductId()); reservedProduct.setReserved(true); toReserveNumber--; if(toReserveNumber ==0) tryFinish(); } @SagaEventHandler(associationProperty = \"id\", keyName = \"orderId\") @EndSaga public void handle(OrderConfirmedEvent event){ LOGGER.info(\"Order {} is confirmed\", event.getId()); }} Saga的启动和结束Axon中通过@Saga注解标识Saga。Saga有起点和终点,必须以@StartSaga和@EndSaga区分清楚。一个Saga的起点可能只有一个,但终点可能有好几个,对应流程的不同结果。默认情况下,只有在找不到同类型已存在的Saga instance时,才会创建一个新的Saga。但是可以通过更改@StartSaga中的forceNew为true让它每次都新建一个。只有当@EndSaga对应的方法被顺利执行,Saga才会结束,但也可以直接从Saga内部调用end()方法强制结束。 EventHandlingSaga通过@SagaEventHandler注解来标明EventHandler,与普通EventHandler基本一致,唯一的不同是,普通的EventHandler会接受所有对应的Event,而Saga的EventHandler只处理与其关联过的Event。当被注解@StartSaga的方法调用时,axon默认会根据当前@SagaEventHandler中的associationProperty去找Event中的field,然后把它的值与当前Saga进行关联,类似<saga_id,<key,value>>这种形式。一旦产生关联,该Saga在遇到同一Event时,只会处理<key,value>与已关联值完全一致的Event。例如,有两个OrderCreatedEvent,我们定义associationProperty ="orderId",两个event的orderId分别为1、2,当Saga创建时接受了orderId=1的OrderCreatedEvent后,值为2的Event它就不再处理了。也可以在Saga内直接调用associateWith(String key, String/Number value)来做这个关联。例如,123456789101112131415161718192021222324252627282930public class OrderManagementSaga {private boolean paid = false;private boolean delivered = false;@Injectprivate transient CommandGateway commandGateway; @StartSaga @SagaEventHandler(associationProperty = \"orderId\") public void handle(OrderCreatedEvent event) { // client generated identifiers ShippingId shipmentId = createShipmentId(); InvoiceId invoiceId = createInvoiceId(); // associate the Saga with these values, before sending the commands associateWith(\"shipmentId\", shipmentId); associateWith(\"invoiceId\", invoiceId); // send the commands commandGateway.send(new PrepareShippingCommand(...)); commandGateway.send(new CreateInvoiceCommand(...)); } @SagaEventHandler(associationProperty = \"shipmentId\") public void handle(ShippingArrivedEvent event) { delivered = true; if (paid) { end(); } } @SagaEventHandler(associationProperty = \"invoiceId\") public void handle(InvoicePaidEvent event) { paid = true; if (delivered) { end(); } }// ...} 有时我们可能并不想直接使用Event里field的名称作为associationProperty的值,可以使用keyName来对应field名称。Saga是靠Event驱动的,但有时command发出去了,并没有在规定时间内收到预期的Event怎么办?Saga提供了EventScheduler,通过Java内置的scheduler或Quarz,定时自动发送一个Event到这个Saga。Saga的执行是在独立的线程里,所以我们无法通过commandgateway的sendAndWait方法等到其返回值或捕获异常。 Saga Store由于Sage在处理过程中也存在中间状态,而Saga的一些业务流程可能会执行很长时间,比如好几天,那么万一系统重启Saga的状态就丢失了,所以Saga也需要能够通过ES恢复,即指定一个SagaStore。SagaStore与EventStore的使用除了名字外,基本没有任何区别,也内置了InMemory,JPA,jdbc,Mongo四种实现这里我就不多叙述了。 注意!当持久化Saga时,对于注入的资源field,如CommandGateway,一定要加上transient修饰符,这样Serializer才不会去序列化这个field。当Saga从Repository读出来的时候,会自动注入相关的资源。 只需要显示的提供一个SagaStore的配置就可以了。当启用JPA时,默认会启动JpaSagaStore。我们这里使用MongoSagaStore,修改AxonConfiguration如下:1234567891011@Configurationpublic class AxonConfiguration { ..... @Bean public SagaStore sagaStore(){ org.axonframework.mongo.eventhandling.saga.repository.MongoTemplate mongoTemplate = new org.axonframework.mongo.eventhandling.saga.repository.DefaultMongoTemplate(mongoClient(), mongoDbName, \"sagas\"); return new MongoSagaStore(mongoTemplate, axonJsonSerializer()); }} 在@StartSaga执行后,会把当前Saga插入到指定的SagaStore中,当@EndSaga执行时,axon会自动的从SagaStore中删除该Saga。 修改Handler由于ReserveProductCommand和RollbackReservationCommand是需要查找原ProductAggregate的,所以单独创建一个ProductHandlerProductHandler1234567891011121314151617181920@Componentpublic class ProductHandler { private static final Logger LOGGER = getLogger(ProductHandler.class); @Autowired private Repository<ProductAggregate> repository; @CommandHandler public void on(ReserveProductCommand command){ Aggregate<ProductAggregate> aggregate = repository.load(command.getProductId()); aggregate.execute(aggregateRoot->aggregateRoot.reserve(command.getOrderId(), command.getNumber())); } @CommandHandler public void on(RollbackReservationCommand command){ Aggregate<ProductAggregate> aggregate = repository.load(command.getProductId()); aggregate.execute(aggregateRoot->aggregateRoot.cancellReserve(command.getOrderId(), command.getNumber())); }} 修改ProductAggregate,增加对应的方法和handlerProductAggregate123456789101112131415161718192021222324252627@Aggregatepublic class ProductAggregate { public void reserve(OrderId orderId, int amount){ if(stock>=amount) { apply(new ProductReservedEvent(orderId, id, amount)); }else apply(new ProductNotEnoughEvent(orderId, id)); } public void cancellReserve(OrderId orderId, int amount){ apply(new ReserveCancelledEvent(orderId, id, stock)); } @EventHandler public void on(ProductReservedEvent event){ int oriStock = stock; stock = stock - event.getAmount(); LOGGER.info(\"Product {} stock change {} -> {}\", id, oriStock, stock); } @EventHandler public void on(ReserveCancelledEvent event){ stock +=event.getAmount(); LOGGER.info(\"Reservation rollback, product {} stock changed to {}\", id, stock); }} Order这边对应也要修改Aggregate和handlerOrderHandler1234567891011121314@Componentpublic class OrderHandler { @CommandHandler public void handle(RollbackOrderCommand command){ Aggregate<OrderAggregate> aggregate = repository.load(command.getOrderId().getIdentifier()); aggregate.execute(aggregateRoot->aggregateRoot.delete()); } @CommandHandler public void handle(ConfirmOrderCommand command){ Aggregate<OrderAggregate> aggregate = repository.load(command.getId().getIdentifier()); aggregate.execute(aggregateRoot->aggregateRoot.confirm()); }} OrderAggregate12345678910111213141516@Aggregatepublic class OrderAggregate { private String state=\"processing\"; // 增加一个属性订单状态 ...... @EventHandler public void on(OrderConfirmedEvent event){ this.state = \"confirmed\"; } @EventHandler public void on(OrderCancelledEvent event){ this.state = \"deleted\"; markDeleted(); }} 启动测试其他地方基本没有什么改动,为方便起见,我把Query端也改成MongoDB了,方法比较简单,就引入spring-boot-starter-data-mongodb包,启动类里将@EnableJpaRepositories改成@EnableMongoRepositories,然后把Queyr端的Entry类包含在Scan的范围内就好了。123456789101112131415@SpringBootApplication@ComponentScan(basePackages = {\"com.edi.learn\"})@EntityScan(basePackages = {\"com.edi.learn\", \"org.axonframework.eventsourcing.eventstore.jpa\", \"org.axonframework.eventhandling.saga.repository.jpa\", \"org.axonframework.eventhandling.tokenstore.jpa\"})@EnableMongoRepositories(basePackages = {\"com.edi.learn\"})public class Application { private static final Logger LOGGER = getLogger(Application.class); public static void main(String args[]){ SpringApplication.run(Application.class, args); }} 执行后, POST请求到http://127.0.0.1:8080/product/1?name=ttt&price=10&stock=100 创建商品; POST如下JSON到http://127.0.0.1:8080/order 来创建订单 1234567{ \"username\":\"Edison\", \"products\":[{ \"id\":1, \"number\":90 }]} 再创建一次可以看到控制台打印 123456789101109:39:10.648 [http-nio-8080-exec-1] DEBUG c.e.l.a.c.w.c.ProductController - Adding Product [1] 'ttt' 10x10009:39:10.675 [http-nio-8080-exec-1] DEBUG c.e.l.a.c.a.ProductAggregate - Product [1] ttt 1000x100 is created.09:39:10.853 [http-nio-8080-exec-1] DEBUG c.e.l.a.q.h.ProductEventHandler - repository data is updated09:39:21.640 [http-nio-8080-exec-3] DEBUG c.e.l.a.c.a.ProductAggregate - Product [1] ttt 1000x100 is created.09:39:21.681 [http-nio-8080-exec-3] INFO c.e.l.a.c.a.ProductAggregate - Product 1 stock change 100 -> 1009:39:21.823 [http-nio-8080-exec-3] INFO c.e.l.axon.command.saga.OrderSaga - Order 8706dbaf-4511-4b01-b6c5-e24bec3f10a9 is confirmed09:42:35.255 [http-nio-8080-exec-5] DEBUG c.e.l.a.c.handlers.OrderHandler - Loading product information with productId: 109:42:35.259 [http-nio-8080-exec-5] DEBUG c.e.l.a.c.a.ProductAggregate - Product [1] ttt 1000x100 is created.09:42:35.263 [http-nio-8080-exec-5] INFO c.e.l.a.c.a.ProductAggregate - Product 1 stock change 100 -> 1009:42:35.301 [http-nio-8080-exec-5] INFO c.e.l.axon.command.saga.OrderSaga - No enough item to buy09:42:35.313 [http-nio-8080-exec-5] INFO c.e.l.axon.command.saga.OrderSaga - Order 6baba5e9-1173-48a8-ab98-cd51691ba9f5 is cancelled 重启程序,再创建一次订单后发送GET请求到http://127.0.0.1:8080/orders 查询订单 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980{ \"_embedded\": { \"orders\": [ { \"username\": \"Edison\", \"payment\": 0, \"status\": \"confirmed\", \"products\": { \"1\": { \"name\": \"ttt\", \"price\": 1000, \"amount\": 90 } }, \"_links\": { \"self\": { \"href\": \"http://localhost:8080/orders/8706dbaf-4511-4b01-b6c5-e24bec3f10a9\" }, \"orderEntry\": { \"href\": \"http://localhost:8080/orders/8706dbaf-4511-4b01-b6c5-e24bec3f10a9\" } } }, { \"username\": \"Edison\", \"payment\": 0, \"status\": \"cancelled\", \"products\": { \"1\": { \"name\": \"ttt\", \"price\": 1000, \"amount\": 90 } }, \"_links\": { \"self\": { \"href\": \"http://localhost:8080/orders/6baba5e9-1173-48a8-ab98-cd51691ba9f5\" }, \"orderEntry\": { \"href\": \"http://localhost:8080/orders/6baba5e9-1173-48a8-ab98-cd51691ba9f5\" } } }, { \"username\": \"Edison\", \"payment\": 0, \"status\": \"cancelled\", \"products\": { \"1\": { \"name\": \"ttt\", \"price\": 1000, \"amount\": 90 } }, \"_links\": { \"self\": { \"href\": \"http://localhost:8080/orders/27a829af-cda1-43f4-af37-fbc597fe5f6f\" }, \"orderEntry\": { \"href\": \"http://localhost:8080/orders/27a829af-cda1-43f4-af37-fbc597fe5f6f\" } } } ] }, \"_links\": { \"self\": { \"href\": \"http://localhost:8080/orders\" }, \"profile\": { \"href\": \"http://localhost:8080/profile/orders\" } }, \"page\": { \"size\": 20, \"totalElements\": 3, \"totalPages\": 1, \"number\": 0 }} 很明显看到只有第一个订单状态为’confirmed’,其他两个都是’cancelled’。重启后,Aggregate自动回溯后,对库存的判断也是正确的。 再做个小实验,我们修改OrderSaga,强制在确认订单时让线程sleep一段时间,然后去MongoDB里查看Saga信息123456@SagaEventHandler(associationProperty = \"id\", keyName = \"orderId\")@EndSagapublic void handle(OrderConfirmedEvent event) throws InterruptedException { LOGGER.info(\"Order {} is confirmed\", event.getId()); Thread.sleep(10000);} 12345678910111213> db.sagas.find().pretty(){ "_id" : ObjectId("58df074d73bc0c10f4008eff"), "sagaType" : "com.edi.learn.axon.command.saga.OrderSaga", "sagaIdentifier" : "08a371f5-9d9a-48a7-b46e-9b8e86b8897b", "serializedSaga" : BinData(0,"e30="), "associations" : [ { "key" : "orderId", "value" : "5111a55e-1ddd-4434-aab8-635c004fc1eb" } ]} 看到我们的关联值了吧。 本文代码:https://github.com/EdisonXu/sbs-axon/tree/master/lesson-5","categories":[],"tags":[{"name":"eventsourcing","slug":"eventsourcing","permalink":"http://edisonxu.com/tags/eventsourcing/"},{"name":"CQRS","slug":"CQRS","permalink":"http://edisonxu.com/tags/CQRS/"},{"name":"axon","slug":"axon","permalink":"http://edisonxu.com/tags/axon/"},{"name":"DDD","slug":"DDD","permalink":"http://edisonxu.com/tags/DDD/"}]},{"title":"Axon入门系列(五):第一个正式Axon例子","slug":"axon-cqrs-example","date":"2017-03-30T11:45:16.000Z","updated":"2018-10-30T01:56:38.385Z","comments":true,"path":"2017/03/30/axon-cqrs-example.html","link":"","permalink":"http://edisonxu.com/2017/03/30/axon-cqrs-example.html","excerpt":"前面对Axon的基本概念和基本操作做了简介,从本章开始,我们将一步步使用AxonFramework完成一个真正CQRS&EventSourcing的例子。 设计回顾一下使用AxonFramework应用的架构","text":"前面对Axon的基本概念和基本操作做了简介,从本章开始,我们将一步步使用AxonFramework完成一个真正CQRS&EventSourcing的例子。 设计回顾一下使用AxonFramework应用的架构 Command端Repository和Query端的Database是解耦的,完全可以使用不同的持久化技术,我们来尝试用MongoDB做Command端的Repository,而MySQL做Query的数据库。 例子描述我们尝试完成一个简单的case:后台人员创建商品,用户选定若干商品后下单购买。商品定义:Product(id, name, stock, price)商品创建流程:CreateProductCommand -> new ProductAggregate instance -> ProductCreatedEvent 订单定义: Order(id, username, payment, products)订单创建流程:CreateOrderCommand -> new OrderAggregate instance -> OrderCreatedEvent创建商品时,我们只接收商品ID,去查询商品的具体信息,这样来学习如何在handler内去查询Aggregate。 Command端实现Command端实现与前面几篇文章基本一致,需要定义Aggregate、Command,然后提供配置即可。 AggregateProductAggregate12345678910111213141516171819202122232425262728293031@Aggregatepublic class ProductAggregate { private static final Logger LOGGER = getLogger(ProductAggregate.class); @AggregateIdentifier private String id; private String name; private int stock; private long price; public ProductAggregate() { } @CommandHandler public ProductAggregate(CreateProductCommand command) { apply(new ProductCreatedEvent(command.getId(),command.getName(),command.getPrice(),command.getStock())); } @EventHandler public void on(ProductCreatedEvent event){ this.id = event.getId(); this.name = event.getName(); this.price = event.getPrice(); this.stock = event.getStock(); LOGGER.debug(\"Product [{}] {} {}x{} is created.\", id,name,price,stock); } // getter and setter ......} OrderAggregate1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162@Aggregatepublic class OrderAggregate { @AggregateIdentifier private OrderId id; private String username; private double payment; @AggregateMember private Map<String, OrderProduct> products; public OrderAggregate(){} public OrderAggregate(OrderId id, String username, Map<String, OrderProduct> products) { apply(new OrderCreatedEvent(id, username, products)); } public OrderId getId() { return id; } public String getUsername() { return username; } public Map<String, OrderProduct> getProducts() { return products; } @EventHandler public void on(OrderCreatedEvent event){ this.id = event.getOrderId(); this.username = event.getUsername(); this.products = event.getProducts(); computePrice(); } private void computePrice() { products.forEach((id, product) -> { payment += product.getPrice() * product.getAmount(); }); } /** * Divided 100 here because of the transformation of accuracy * * @return */ public double getPayment() { return payment/100; } public void addProduct(OrderProduct product){ this.products.put(product.getId(), product); payment += product.getPrice() * product.getAmount(); } public void removeProduct(String productId){ OrderProduct product = this.products.remove(productId); payment = payment - product.getPrice() * product.getAmount(); }} 这里,我并没有像ProductAggregate一样,把CreateOrderCommand放到OrderAggregate的构造器中去处理,原因是在创建订单时,由于需要知道商品的单价,所以要根据商品id查询商品信息,因为涉及到了其他Aggregate操作,特地单独创建一个OrderHandler来处理。1234567891011121314151617181920212223242526272829@Componentpublic class OrderHandler { private static final Logger LOGGER = getLogger(OrderHandler.class); @Autowired private Repository<OrderAggregate> repository; @Autowired private Repository<ProductAggregate> productRepository; @Autowired private EventBus eventBus; @CommandHandler public void handle(CreateOrderCommand command) throws Exception { Map<String, OrderProduct> products = new HashMap<>(); command.getProducts().forEach((productId,number)->{ LOGGER.debug(\"Loading product information with productId: {}\",productId); Aggregate<ProductAggregate> aggregate = productRepository.load(productId); products.put(productId, new OrderProduct(productId, aggregate.invoke(productAggregate -> productAggregate.getName()), aggregate.invoke(productAggregate -> productAggregate.getPrice()), number)); }); repository.newInstance(() -> new OrderAggregate(command.getOrderId(), command.getUsername(), products)); }} 如果查看org.axonframework.commandhandling.model.Repository<T>接口的定义,会发现里面只有三个方法:1234567891011121314151617181920212223242526272829303132public interface Repository<T> { /** * Load the aggregate with the given unique identifier. No version checks are done when loading an aggregate, * meaning that concurrent access will not be checked for. * * @param aggregateIdentifier The identifier of the aggregate to load * @return The aggregate root with the given identifier. * @throws AggregateNotFoundException if aggregate with given id cannot be found */ Aggregate<T> load(String aggregateIdentifier); /** * Load the aggregate with the given unique identifier. * * @param aggregateIdentifier The identifier of the aggregate to load * @param expectedVersion The expected version of the loaded aggregate * @return The aggregate root with the given identifier. * @throws AggregateNotFoundException if aggregate with given id cannot be found */ Aggregate<T> load(String aggregateIdentifier, Long expectedVersion); /** * Creates a new managed instance for the aggregate, using the given {@code factoryMethod} * to instantiate the aggregate's root. * * @param factoryMethod The method to create the aggregate's root instance * @return an Aggregate instance describing the aggregate's state * @throws Exception when the factoryMethod throws an exception */ Aggregate<T> newInstance(Callable<T> factoryMethod) throws Exception;} 有人会疑惑了,为什么没有Delete和Update?先说update,这个Repository其实是对Aggregate的操作,EventSourcing中对Aggregate所有的变化都是通过Event来实现的,所以在调用apply(EventMessage)时,Event就已经被持久化了,EventHandler在处理该Event时,就已经实现了对Aggregate的update。而Delete没有,很简单,EventSourcing脱胎于现实概念,你见过现实生活中把一个事物真正“delete”掉吗?估计得使用高能量子炮把东西轰成原子吧。所以,只会有一个把这个Aggregate标为失效的标志,Axon中,在Aggregate内部可以直接调用markDeleted()来表示这个Aggregate被“delete”掉了,其实只是不能被load出来罢了。由于Repository默认返回的是同一类型Aggregate,所以我们取属性就没那么简单了,只能通过invoke来调用get方法。是不是觉得很麻烦?因为其实CQRS压根不推荐直接从Repository直接query Aggregate来查询,而是调用Query端。 Commandcommand的实现因为都是POJO我就不贴代码了,可以直接看源码。这里写一下基于SpringWeb的Controller类(引入spring-boot-starter-web包),以创建Product为例12345678910111213141516171819202122232425262728293031323334353637@RestController@RequestMapping(\"/product\")public class ProductController { private static final Logger LOGGER = getLogger(ProductController.class); @Autowired private CommandGateway commandGateway; @RequestMapping(value = \"/{id}\", method = RequestMethod.POST) public void create(@PathVariable(value = \"id\") String id, @RequestParam(value = \"name\", required = true) String name, @RequestParam(value = \"price\", required = true) long price, @RequestParam(value = \"stock\",required = true) int stock, HttpServletResponse response) { LOGGER.debug(\"Adding Product [{}] '{}' {}x{}\", id, name, price, stock); try { // multiply 100 on the price to avoid float number CreateProductCommand command = new CreateProductCommand(id,name,price*100,stock); commandGateway.sendAndWait(command); response.setStatus(HttpServletResponse.SC_CREATED);// Set up the 201 CREATED response return; } catch (CommandExecutionException cex) { LOGGER.warn(\"Add Command FAILED with Message: {}\", cex.getMessage()); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); if (null != cex.getCause()) { LOGGER.warn(\"Caused by: {} {}\", cex.getCause().getClass().getName(), cex.getCause().getMessage()); if (cex.getCause() instanceof ConcurrencyException) { LOGGER.warn(\"A duplicate product with the same ID [{}] already exists.\", id); response.setStatus(HttpServletResponse.SC_CONFLICT); } } } }} CommandGateway提供了四种发送Comman的方法: send(command, CommandCallback) 发送command,根据执行结果调用CommandCallback中的onSuccess或onFailure方法 sendAndWait(command) 发送完command,等待执行完成并返回结果 sendAndWait(command, timeout, TimeUnit) 这个好理解,比上面多了一个超时 send(command) 该方法返回一个CompletableFuture,不用等待command的执行,立刻返回。结果通过future获取。 Repository由于我们要使用axon-mongo,而非默认的jpa,所以必须得手动指定两个Aggregate的Repository,以其中一个为例:12345678910111213141516171819202122232425262728@Configurationpublic class ProductConfig { @Autowired private EventStore eventStore; @Bean @Scope(\"prototype\") public ProductAggregate productAggregate(){ return new ProductAggregate(); } @Bean public AggregateFactory<ProductAggregate> productAggregateAggregateFactory(){ SpringPrototypeAggregateFactory<ProductAggregate> aggregateFactory = new SpringPrototypeAggregateFactory<>(); aggregateFactory.setPrototypeBeanName(\"productAggregate\"); return aggregateFactory; } @Bean public Repository<ProductAggregate> productAggregateRepository(){ EventSourcingRepository<ProductAggregate> repository = new EventSourcingRepository<ProductAggregate>( productAggregateAggregateFactory(), eventStore ); return repository; }} 使用EventSourcingRepository,必须指定一个AggregateFactory用来反射生成Aggregate的,所以我们这里定义了Aggregate的prototype,并把它注册到AggregateFactory中去。这样在系统启动时,读取历史Event进行ES还原时,就可以真实再现Aggregate的状态。 配置前面使用MySQL作为EventStorage是不是感到不爽,那么我们通过引入axon-mongo依赖,使用MongoDB来做EventStorage。pom的修改我就不写了,着重看下相关配置先是修改application.property12345678# mongomongodb.url=10.1.110.24mongodb.port=27017# mongodb.username=# mongodb.password=mongodb.dbname=axonmongodb.events.collection.name=eventsmongodb.events.snapshot.collection.name=snapshots 通过Spring提供的@Value注解在具体的Configuration类里读取。123456789101112131415161718192021222324252627282930313233343536373839@Configurationpublic class CommandRepositoryConfiguration { @Value(\"${mongodb.url}\") private String mongoUrl; @Value(\"${mongodb.dbname}\") private String mongoDbName; @Value(\"${mongodb.events.collection.name}\") private String eventsCollectionName; @Value(\"${mongodb.events.snapshot.collection.name}\") private String snapshotCollectionName; @Bean public Serializer axonJsonSerializer() { return new JacksonSerializer(); } @Bean public EventStorageEngine eventStorageEngine(){ return new MongoEventStorageEngine( axonJsonSerializer(),null, axonMongoTemplate(), new DocumentPerEventStorageStrategy()); } @Bean(name = \"axonMongoTemplate\") public MongoTemplate axonMongoTemplate() { MongoTemplate template = new DefaultMongoTemplate(mongoClient(), mongoDbName, eventsCollectionName, snapshotCollectionName); return template; } @Bean public MongoClient mongoClient(){ MongoFactory mongoFactory = new MongoFactory(); mongoFactory.setMongoAddresses(Arrays.asList(new ServerAddress(mongoUrl))); return mongoFactory.createMongo(); }} 用Jacson做序列化器,MongoClient提供了具体连接实现,MongoTemplate指定了db名称、存放event的collection名称、存放snapshot的collection名称。(snapshot的概念以后再解释)中间一个参数是做不同版本Event间兼容的,我们先留null。EventStorageEngine指定MongoEventStorageEngine,spring-boot-autoconfigure中的AxonAutoConfiguration就会帮你把它注入到Axon的配置器中。这里指的注意的是,使用Jackson做序列化器时,对应的entity的所有需要持久化的field必须都有public getter方法,因为Jackson在反射时默认只读public修饰符的field,否则就会报com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class com.edi.learn.axon.common.domain.OrderId and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.edi.learn.axon.common.events.OrderCreatedEvent[“orderId”])错误。如果确实不想写,那么在Entity的class声明前加上@JsonAutoDetect(fieldVisibility=JsonAutoDetect.Visibility.ANY)到此,Command端的实现已基本完成(Event我没写,因为与前文类似),那么我们来看看Query端。 Query端实现AxonFramework的Query端其实并没有特别的,我们只需要实现一些EventHandler来处理Command端产生的事件,来更新Query端的数据库就行了。这里我就使用JPA的MySQL实现,spring提供了spring-boot-starter-data-rest,为JPA Repository增加了HateOas风格的REST接口,非常简单,非常方便,堪称无脑。先定义三个Entity1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677@Entitypublic class ProductEntry { @Id private String id; @Column private String name; @Column private long price; @Column private int stock; public ProductEntry() { } public ProductEntry(String id, String name, long price, int stock) { this.id = id; this.name = name; this.price = price; this.stock = stock; } // getter & setter ......}@Entitypublic class OrderEntry { @Id private String id; @Column private String username; @Column private double payment; @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn(name = \"order_id\") @MapKey(name = \"id\") private Map<String, OrderProductEntry> products; public OrderEntry() { } public OrderEntry(String id, String username, Map<String, OrderProductEntry> products) { this.id = id; this.username = username; this.payment = payment; this.products = products; } // getter & setter ......}@Entitypublic class OrderProductEntry { @Id @GeneratedValue private Long jpaId; private String id; @Column private String name; @Column private long price; @Column private int amount; public OrderProductEntry() { } public OrderProductEntry(String id, String name, long price, int amount) { this.id = id; this.name = name; this.price = price; this.amount = amount; } // getter & setter ......} 比较简单,唯一需要注意的是ProductEntry和OrderEntry之间的一对多关系。然后为它们创建两个Repository1234@RepositoryRestResource(collectionResourceRel = "orders", path = "orders")public interface OrderEntryRepository extends PagingAndSortingRepository<OrderEntry, String> {}@RepositoryRestResource(collectionResourceRel = "products", path = "products")public interface ProductEntryRepository extends PagingAndSortingRepository<ProductEntry, String> {} 是不是很简单?最后定义handler,为省篇幅,我只写一个1234567891011121314151617181920212223@Componentpublic class OrderEventHandler { private static final Logger LOGGER = getLogger(OrderEventHandler.class); @Autowired private OrderEntryRepository repository; @EventHandler public void on(OrderCreatedEvent event){ Map<String, OrderProductEntry> map = new HashMap<>(); event.getProducts().forEach((id, product)->{ map.put(id, new OrderProductEntry( product.getId(), product.getName(), product.getPrice(), product.getAmount())); }); OrderEntry order = new OrderEntry(event.getOrderId().toString(), event.getUsername(), map); repository.save(order); }} 启动类由于我们使用了axon提供的MongoEventStorageEngine,其内部也使用了JPA,所以我们在启动类还需要把Axon帮我们转Entity的一些类也加到EntityScan中去123456789101112131415@SpringBootApplication@ComponentScan(basePackages = {\"com.edi.learn\"})@EntityScan(basePackages = {\"com.edi.learn\", \"org.axonframework.eventsourcing.eventstore.jpa\", \"org.axonframework.eventhandling.saga.repository.jpa\", \"org.axonframework.eventhandling.tokenstore.jpa\"})@EnableJpaRepositories(basePackages = {\"com.edi.learn.axon.query\"})public class Application { private static final Logger LOGGER = getLogger(Application.class); public static void main(String args[]){ SpringApplication.run(Application.class, args); }} 启动后,用POST发送请求http://127.0.0.1:8080/product/1?name=ttt&price=10&stock=100 ,查询mongoDB:123456789101112131415161718> use axon> show collectionseventssnapshotssystem.indexes> db.events.find().pretty(){ "_id" : ObjectId("58dd181073bc0c0fb86d895e"), "aggregateIdentifier" : "1", "type" : "ProductAggregate", "sequenceNumber" : NumberLong(0), "serializedPayload" : "{\\"id\\":\\"1\\",\\"name\\":\\"ttt\\",\\"price\\":1000,\\"stock\\":100}", "timestamp" : "2017-03-30T14:37:04.075Z", "payloadType" : "com.edi.learn.axon.common.events.ProductCreatedEvent", "payloadRevision" : null, "serializedMetaData" : "{\\"traceId\\":\\"4a298ed4-0d53-402a-ae6b-d79cc5e193bf\\",\\"correlationId\\":\\"4a298ed4-0d53-402a-ae6b-d79cc5e193bf\\"}", "eventIdentifier" : "500f3a8f-7c02-4e8e-bb9c-7b676224ce5c"} 可以看到生成的EventMessage,与前篇文章中MySQL表里内容基本一致。再去看下MySQL库的product_entry表,有记录 id name price stock 1 ttt 1000 100 用GET请求http://localhost:8080/products 会返回当前所有product信息,加上id http://localhost:8080/products/1 就会返回刚才创建的product。 本篇对应代码:https://github.com/EdisonXu/sbs-axon/tree/master/lesson-4","categories":[],"tags":[{"name":"eventsourcing","slug":"eventsourcing","permalink":"http://edisonxu.com/tags/eventsourcing/"},{"name":"CQRS","slug":"CQRS","permalink":"http://edisonxu.com/tags/CQRS/"},{"name":"axon","slug":"axon","permalink":"http://edisonxu.com/tags/axon/"},{"name":"DDD","slug":"DDD","permalink":"http://edisonxu.com/tags/DDD/"}]},{"title":"Axon入门系列(四):Axon使用EventSourcing和AutoConfigure","slug":"axon-event-sourcing","date":"2017-03-30T09:52:23.000Z","updated":"2018-10-30T01:56:32.998Z","comments":true,"path":"2017/03/30/axon-event-sourcing.html","link":"","permalink":"http://edisonxu.com/2017/03/30/axon-event-sourcing.html","excerpt":"继上一篇集成SpringBoot后,本篇将继续完成小目标: 使用EventSourcing 使用AutoConfigure配置Axon 前一篇中看到配置Axon即便在Spring中也是比较麻烦的,好在Axon提供了spring-boot-autoconfigure,提供了Spring下的一些默认配置,极大地方便了我们的工作。启用也是非常方便的,在上一篇的基础上,我们只需要干三件事即可达成目标: 引入spring-boot-autoconfigure 删除JpaConfig类 去除BankAccount中的Entity声明","text":"继上一篇集成SpringBoot后,本篇将继续完成小目标: 使用EventSourcing 使用AutoConfigure配置Axon 前一篇中看到配置Axon即便在Spring中也是比较麻烦的,好在Axon提供了spring-boot-autoconfigure,提供了Spring下的一些默认配置,极大地方便了我们的工作。启用也是非常方便的,在上一篇的基础上,我们只需要干三件事即可达成目标: 引入spring-boot-autoconfigure 删除JpaConfig类 去除BankAccount中的Entity声明 由于提供的application.properties里关于数据库的配置信息本身就是符合SpringDatasource定义的,所以,SpringBoot在检测到该配置后自动启用JPA。spring-boot-autoconfigure中AxonAutoConfiguration类帮我们提供了最常用的CommandBus、EventBus、EventStorageEngine、Serializer、EventStore等,所以可以直接运行了。在该类中有一段1234567891011121314151617181920212223242526272829303132@ConditionalOnBean(EntityManagerFactory.class)@RegisterDefaultEntities(packages = {\"org.axonframework.eventsourcing.eventstore.jpa\", \"org.axonframework.eventhandling.tokenstore\", \"org.axonframework.eventhandling.saga.repository.jpa\"})@Configurationpublic static class JpaConfiguration { @ConditionalOnMissingBean @Bean public EventStorageEngine eventStorageEngine(EntityManagerProvider entityManagerProvider, TransactionManager transactionManager) { return new JpaEventStorageEngine(entityManagerProvider, transactionManager); } @ConditionalOnMissingBean @Bean public EntityManagerProvider entityManagerProvider() { return new ContainerManagedEntityManagerProvider(); } @ConditionalOnMissingBean @Bean public TokenStore tokenStore(Serializer serializer, EntityManagerProvider entityManagerProvider) { return new JpaTokenStore(entityManagerProvider, serializer); } @ConditionalOnMissingBean(SagaStore.class) @Bean public JpaSagaStore sagaStore(Serializer serializer, EntityManagerProvider entityManagerProvider) { return new JpaSagaStore(serializer, entityManagerProvider); }} 所以,当我们提供了JPA相关配置,以及mysql-connector后,这些Bean也会被启用,可以看到里面默认的EventStoreEngine就是JpaEventStorageEngine。执行后,我们可以看到数据库中创建了如下表 其中domain_event_entry就是用来保存对Aggregate状态造成改变的所有Event的表。如果不做特别声明,所有Event都会记录在这张表里。表内容其中,比较重要的字段有 pay_load Event的具体内容 pay_load_type Event的类型,Axon在ES(Event Sourcing)时会通过这个反射出来原来的Java class time_stamp 该Event发生的时间 aggregate_identifier event所对应Aggregate的唯一标识,在ES时,只有相同identifier的event才会一起回溯 sequence_number 同一Aggregate对应的event发生的序列号,回溯时严格按照该顺序 值得注意的是,在使用EventSourcing时,由于Aggregate本身的状态是通过ES获得的,所以所有对于Aggregate状态变化的动作一定都是放在@EventHandler里的,否则将会造成状态丢失。预告一下,基本介绍已经完毕,下一篇开始,进入复杂的实现。","categories":[],"tags":[{"name":"eventsourcing","slug":"eventsourcing","permalink":"http://edisonxu.com/tags/eventsourcing/"},{"name":"CQRS","slug":"CQRS","permalink":"http://edisonxu.com/tags/CQRS/"},{"name":"axon","slug":"axon","permalink":"http://edisonxu.com/tags/axon/"},{"name":"DDD","slug":"DDD","permalink":"http://edisonxu.com/tags/DDD/"}]},{"title":"Axon入门系列(三):Axon使用Jpa存储Aggregate状态","slug":"axon-jpa","date":"2017-03-30T07:52:23.000Z","updated":"2018-10-30T01:56:23.935Z","comments":true,"path":"2017/03/30/axon-jpa.html","link":"","permalink":"http://edisonxu.com/2017/03/30/axon-jpa.html","excerpt":"上一篇里,介绍了Axon的基本概念,并且做了一个最简单的hello例子。本篇将更进一步,完成两个小目标: 集成SpringBoot; 使用Standard Repository来存储Aggregate的最新状态。 1. 更新Maven依赖干几件事:","text":"上一篇里,介绍了Axon的基本概念,并且做了一个最简单的hello例子。本篇将更进一步,完成两个小目标: 集成SpringBoot; 使用Standard Repository来存储Aggregate的最新状态。 1. 更新Maven依赖干几件事: 集成Springboot 加入spring-boot-starter-data-jpa(Spring提供的JPA快速包,很方便) 加入my-sql-connector 加入spring-boot-starter-web包,提供web接口调用,测试用 1234567891011121314151617181920<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version></parent><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> </dependencies> 2. 提供application.properties,配置好数据库信息123456789101112# Datasource configurationspring.datasource.url=jdbc:mysql://xxx.xxx.xxx.xxx:3306/cqrsspring.datasource.driverClassName=com.mysql.jdbc.Driverspring.datasource.username=<username>spring.datasource.password=<password>spring.datasource.validation-query=SELECT 1;spring.datasource.initial-size=2spring.datasource.sql-script-encoding=UTF-8spring.jpa.database=mysqlspring.jpa.show-sql=truespring.jpa.hibernate.ddl-auto=create-drop 3. 使用Spring进行配置12345678910111213141516171819202122232425262728293031323334353637383940414243444546@Configuration@EnableAxonpublic class JpaConfig { private static final Logger LOGGER = getLogger(JpaConfig.class); @Autowired private PlatformTransactionManager transactionManager; @Bean public EventStorageEngine eventStorageEngine(){ return new InMemoryEventStorageEngine(); } @Bean public TransactionManager axonTransactionManager() { return new SpringTransactionManager(transactionManager); } @Bean public EventBus eventBus(){ return new SimpleEventBus(); } @Bean public CommandBus commandBus() { SimpleCommandBus commandBus = new SimpleCommandBus(axonTransactionManager(), NoOpMessageMonitor.INSTANCE); //commandBus.registerHandlerInterceptor(transactionManagingInterceptor()); return commandBus; } @Bean public TransactionManagingInterceptor transactionManagingInterceptor(){ return new TransactionManagingInterceptor(new SpringTransactionManager(transactionManager)); } @Bean public EntityManagerProvider entityManagerProvider() { return new ContainerManagedEntityManagerProvider(); } @Bean public Repository<BankAccount> accountRepository(){ return new GenericJpaRepository<BankAccount>(entityManagerProvider(),BankAccount.class, eventBus()); }} @EnableAxon会启用SpringAxonAutoConfigurer,后者会自动把上线文里的关键配置模块注入到Axon的config中。但这个注解未来会被替代,所以推荐使用方式为引入axon-spring-boot-autoconfigure包。下一篇文章就会介绍如何使用autoconfigure进行配置。在本例中,我们把Event保存在内存中,所以指定EventStoreEngine为InMemoryEventStorageEngine。前一篇说过,Axon会给每一个Aggregate创建一个AggregateRepositoryBean,来指定每一个Aggregate的实际Repository。这里我们直接声明BankAccount对应的Repository为一个GenericJpaRepository,来直接保存Aggregate的状态。GenericJpaRepository要求提供一个EntityManagerProvider,该Provider会提供具体的EntityManager来管理持久化。值得注意的是,CommandBus在初始化时,需要提供一个TransactionManager,如果直接调用SimpleCommandBus的无参构造器,默认是NoTransactionManager.INSTANCE。本例测试时把几个command放在一个线程里串行执行,如果不提供TransactionManager,那么最终withdraw会失败。提供TransactionManager的方式有两种: 如上例中直接构造器中指定; 注册一个TransactionManagingInterceptor; 4. 把Aggregate加上JPA的标准Entity注解1234567891011121314151617181920212223@Aggregate(repository = \"accountRepository\")@Entitypublic class BankAccount { @AggregateIdentifier private AccountId accountId; ...... @Id public String getAccountId() { return accountId.toString(); } @Column public String getAccountName() { return accountName; } @Column public BigDecimal getBalance() { return balance; }} repository = “accountRepository”指定了该Aggregate对应的Repository的Bean名字,即在JpaConfig中定义的那一个。JPA要求Entity必须有一个ID,GenericJpaRepository默认使用String作为EntityId的类型,而这里并没有直接用String,将会在存储时报java.lang.IllegalArgumentException: Provided id of the wrong type for class com.edi.learn.axon.aggregates.BankAccount. Expected: class com.edi.learn.axon.domain.AccountId, got class java.lang.String解决方法是把@Id,@Column加在getter方法上。 5. 配置controller接受请求并发送command123456789101112131415161718192021222324@RestController@RequestMapping(\"/bank\")public class BankAccountController { private static final Logger LOGGER = getLogger(BankAccountController.class); @Autowired private CommandGateway commandGateway; @Autowired private HttpServletResponse response; @RequestMapping(method = RequestMethod.POST) public void create() { LOGGER.info(\"start\"); AccountId id = new AccountId(); LOGGER.debug(\"Account id: {}\", id.toString()); commandGateway.send(new CreateAccountCommand(id, \"MyAccount\",1000)); commandGateway.send(new WithdrawMoneyCommand(id, 500)); commandGateway.send(new WithdrawMoneyCommand(id, 300)); commandGateway.send(new CreateAccountCommand(id, \"MyAccount\", 1000)); commandGateway.send(new WithdrawMoneyCommand(id, 500)); }} 我这里是为了偷懒,直接一个post请求就可以执行一堆操作。有心者可以改下,接受参数,根据参数发送command。 6. 启动类1234567@SpringBootApplication@ComponentScan(basePackages = {\"com.edi.learn\"})public class Application { public static void main(String args[]){ SpringApplication.run(Application.class, args); }} 唯一需要注意的是,如果Application类不在JpaConfig包路径的前面,JpaConfig讲不会被Spring扫描注册到上下文中,需要指定包路径。 启动后,在http://localhost:8080/bank 发送一个POST请求,就可以看到log12345617:53:47.099 [http-nio-8080-exec-1] INFO c.e.l.a.c.aggregates.BankAccount - Account 2fabef76-80bc-4dfc-8f21-4b68c5969fa5 is created with balance 100017:53:47.229 [http-nio-8080-exec-1] INFO c.e.l.a.c.aggregates.BankAccount - Withdraw 500 from account 2fabef76-80bc-4dfc-8f21-4b68c5969fa5, balance result: 50017:53:47.241 [http-nio-8080-exec-1] INFO c.e.l.a.c.aggregates.BankAccount - Withdraw 300 from account 2fabef76-80bc-4dfc-8f21-4b68c5969fa5, balance result: 20017:53:47.246 [http-nio-8080-exec-1] INFO c.e.l.a.c.aggregates.BankAccount - Account 2fabef76-80bc-4dfc-8f21-4b68c5969fa5 is created with balance 100017:53:47.253 [http-nio-8080-exec-1] WARN o.a.c.gateway.DefaultCommandGateway - Command 'com.edi.learn.axon.command.commands.CreateAccountCommand' resulted in javax.persistence.EntityExistsException(A different object with the same identifier value was already associated with the session : [com.edi.learn.axon.command.aggregates.BankAccount#2fabef76-80bc-4dfc-8f21-4b68c5969fa5])17:53:47.268 [http-nio-8080-exec-1] ERROR c.e.l.a.c.aggregates.BankAccount - Cannot withdraw more money than the balance! 可以看到故意发送的第二个CreateAccountCommand时,由于id相同,提示创建失败。进一步取钱时,因余额不足报错 。 本文源码:https://github.com/EdisonXu/sbs-axon/tree/master/lesson-2","categories":[],"tags":[{"name":"eventsourcing","slug":"eventsourcing","permalink":"http://edisonxu.com/tags/eventsourcing/"},{"name":"CQRS","slug":"CQRS","permalink":"http://edisonxu.com/tags/CQRS/"},{"name":"axon","slug":"axon","permalink":"http://edisonxu.com/tags/axon/"},{"name":"DDD","slug":"DDD","permalink":"http://edisonxu.com/tags/DDD/"}]},{"title":"Axon入门系列(二):Hello,Axon3","slug":"hello-axon","date":"2017-03-30T06:47:46.000Z","updated":"2018-10-30T01:56:14.201Z","comments":true,"path":"2017/03/30/hello-axon.html","link":"","permalink":"http://edisonxu.com/2017/03/30/hello-axon.html","excerpt":"AxonFramework是一个轻量级的CQRS框架,支持EventSourcing,本系列将开始通过例子,StepByStep学习AxonFramework。 简介AxonFramework是一个基于事件驱动的轻量级CQRS框架,既支持直接持久化Aggreaget状态,也支持采用EventSourcing,使用AxonFramework的应用架构如下","text":"AxonFramework是一个轻量级的CQRS框架,支持EventSourcing,本系列将开始通过例子,StepByStep学习AxonFramework。 简介AxonFramework是一个基于事件驱动的轻量级CQRS框架,既支持直接持久化Aggreaget状态,也支持采用EventSourcing,使用AxonFramework的应用架构如下 引入Axon非常简单,加入Maven依赖即可12345<dependency> <groupId>org.axonframework</groupId> <artifactId>axon-core</artifactId> <version>${axon.version}</version></dependency> AxonFramework的源码地址:https://github.com/AxonFramework/AxonFramework包含了如下组件; core axon的核心代码 amqp 使用AMQP协议的MQ,如rabbit等,实现Event跨JVM的分发 distributed-commandbus-jgroups 使用Jgroup实现跨JVM的Command分发 distributed-commandbus-springcloud 与SpringCloud集成,使用DiscoveryClient和RESTemplate实现跨JVM的Command分发 metrics 提供监控相关信息 mongo 实现axon与mongoDB的集成 spring-boot-autoconfigure 实现spring的autoconfigure支持,只需要提供相关Property就可以自动配置Axon spring-boot-starter-jgroups 用distributed-commandbus-jgroups加上spring autoconfigure,提供jgroup“一键”集成 spring-boot-starter 与springboot集成 spring 提供各种annotation,与spring集成 例子废话不多说,我们来用一个简单的例子来说明AxonFramework最基本的使用方法:“开一个银行账户,取钱” Aggregate显然,在这个例子中,我们要实现一个Aggregate是银行账户,定义如下123456public class BankAccount { @AggregateIdentifier private AccountId accountId; private String accountName; private BigDecimal balance;} Axon中定义一个class是Aggregate有两种方法: 在配置中直接指定,如调用.configureAggregate(BankAccount.class); 与Spring集成时,可以通过加上@Aggregate的注解标明;结合前文DDD概念中关于Aggregate的介绍,每个Aggregate都有自己独立的全局唯一的标识符,@AggregateIdentifier即是这个唯一标识的标志,例子中就是银行的AccountId。一个AggregateIdentifier必须: 实现equal和hashCode方法,因为它会被拿来与其他标识对比 实现toString方法,其结果也应该是全局唯一的 实现Serializable接口以表明可序列化 这里用Axon提供的generateIdentifier方法来创建唯一标识:12345678910111213141516171819202122232425262728293031323334353637383940public class AccountId implements Serializable { private static final long serialVersionUID = 7119961474083133148L; private final String identifier; private final int hashCode; public AccountId() { this.identifier = IdentifierFactory.getInstance().generateIdentifier(); this.hashCode = identifier.hashCode(); } public AccountId(String identifier) { Assert.notNull(identifier, ()->\"Identifier may not be null\"); this.identifier = identifier; this.hashCode = identifier.hashCode(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AccountId accountId = (AccountId) o; return identifier.equals(accountId.identifier); } @Override public int hashCode() { return hashCode; } @Override public String toString() { return identifier; }} Command在CQRS模式下,所有的“写”操作,都是发送Command来操作。Axon中Command可以是任意的POJO类,由于axon是基于事件驱动的架构,Command类处理时会被axon封装成一个CommandMessage。本例只需要实现两个Command:CreateAccountCommand123456789101112public class CreateAccountCommand { private AccountId accountId; private String accountName; private long amount; public CreateAccountCommand(AccountId accountId, String accountName, long amount) { this.accountId = accountId; this.accountName = accountName; this.amount = amount; } //getter & setter ...} WithdrawMoneyCommand1234567891011public class WithdrawMoneyCommand { @TargetAggregateIdentifier private AccountId accountId; private long amount; public WithdrawMoneyCommand(AccountId accountId, long amount) { this.accountId = accountId; this.amount = amount; } //getter & setter ...} 篇幅问题,我这里省略了getter/setter方法,但是,如果使用Jackson做序列化器,必须实现空参构造器和提供所有field的getter方法! EventEvent是系统中发生任何改变时产生的事件类,典型的event就是对Aggregate状态的修改。与Command一样,Event可以是任何POJO,axon也会把Event自动封装成EventMessage,其中如果是Aggregate发送出来的Event,会被封装成DomainEventMessage。通常来说,Event最好是可序列化的。那么对应到本例,显然有两个Event:AccountCreatedEvent123456789101112public class AccountCreatedEvent { private AccountId accountId; private String accountName; private long amount; public AccountCreatedEvent(AccountId accountId, String accountName, long amount) { this.accountId = accountId; this.accountName = accountName; this.amount = amount; } //getter & setter ...} MoneyWithdrawnEvent1234567891011public class MoneyWithdrawnEvent { private AccountId accountId; private long amount; public MoneyWithdrawnEvent(AccountId accountId, long amount) { this.accountId = accountId; this.amount = amount; } //getter & setter ...} 一样,省略了gettter/setter,注意序列化器对构造器和getter的要求。 CommandHandleraxon使用@CommandHandler注解来标明用来处理Command的方法,配置时会把这些CommandHandler统一加载管理,与其对应的Command形成KV键值对。在Aggregate实现BankAccount里面加入CommandHandler如下:123456789@CommandHandlerpublic BankAccount(CreateAccountCommand command){ apply(new AccountCreatedEvent(command.getAccountId(), command.getAccountName(), command.getAmount()));}@CommandHandlerpublic void handle(WithdrawMoneyCommand command){ apply(new MoneyWithdrawnEvent(command.getAccountId(), command.getAmount()));} 这里不做其他事,只简单的产生Event并使用提供的静态方法apply把Event发送出去。值得一提的是,这里用一个构造器来接受CreateAccountCommand,至于有什么特殊,这里卖个关子,文章最后见分晓。 EventHandler专门用来处理Event的方法,用@EventHandler标明或使用EventHandlingConfiguration去注册。在BankAccount内加入:123456789101112131415161718@EventHandlerpublic void on(AccountCreatedEvent event){ this.accountId = event.getAccountId(); this.accountName = event.getAccountName(); this.balance = new BigDecimal(event.getAmount()); LOGGER.info(\"Account {} is created with balance {}\", accountId, this.balance);}@EventHandlerpublic void on(MoneyWithdrawnEvent event){ BigDecimal result = this.balance.subtract(new BigDecimal(event.getAmount())); if(result.compareTo(BigDecimal.ZERO)<0) LOGGER.error(\"Cannot withdraw more money than the balance!\"); else { this.balance = result; LOGGER.info(\"Withdraw {} from account {}, balance result: {}\", event.getAmount(), accountId, balance); }} 配置现在基本内容都有了,只差最后一步,对axon进行配置。Axon启动最少要指定如下几个模块: CommandBus CommandBus是用来分发Command到对应CommandHandler的机制。每一个Command只会发送到一个CommandHandler去,当有多个CommandHandler去订阅一个CommandMessage时,最后一个覆盖前面所有。 Axon内置了四种CommandBus: SimpleCommandBus默认,直接在发送线程里去执行command handler,执行后保存Aggregate状态和发送事件也都在同一个线程上,适用于大多数情况。 AsynchrounousCommandBus默认使用一个CachedThreadPool来起一个新线程去处理command。CachedThreadPool线程调用时,会检查是否有可用的线程,没有则创建。闲置线程60s后自动关闭。也可以通过config指定其他的线程池来采用不同的线程调度策略。 DisruptorCommandBus适用于多线程场景。SimpleCommandBus在遇到多线程调用时,为了保证aggregate的状态,必须要加锁,这样就降低了效率。DisruptorCommandBus用了开源的并发处理框架Disruptor,用两组线程来处理多线程场景,一组用于执行command handler去更新aggregate的状态,一组用于存储和发送所产生的event到EventStore。但是DisruptorCommandBus有以下的限制: 仅支持Event Sourced Aggregates 一个Command只能改变一个Aggregate的状态。 当使用Cache的时候,一个identifier只能对应一个aggregate,即不允许两个不同类型的aggregate拥有同一个identifier 所处理的Command不能导致UnitOfWork的rollback,因为DisruptorCommandBus无法保证rollback时按照dispatch的顺序来处理。 用于更新Aggregate的command只能按照dispatch的顺序执行,无法指定顺序。DisruptorCommandBus可以使用DisruptorConfiguration来配置,它提供了一些进一步优化的参数。 DistributedCommandBus不像其他CommandBus,DistributedCommandBus并不调用任何command handler,它只是在不同JVM的commandbus之间建立一个“桥梁”。每个JVM上的DistributedCommandBus被称为“Segment”。DistributedCommandBus需要指定路由规则和具体的connector,这两个东东具体实现由distributed-commandbus-xxx模块提供。 EventBus EventBus用于把event发送到subscribe它的各个handler去。Axon提供了两种EventBus的实现,都支持订阅和跟踪: SimpleEventBus 默认的EventBus,不持久化event,一旦发送到消费者去,就会销毁。 EmbeddedEventStore 可以持久化event,以便以后replay。 Repository 即Aggregate的持久化方式。Axon内置了两种 Standard Repositories 代表是GenericJpaRepository,直接把Aggregate的最新状态存到db去。 Event Sourcing Repositories 并不直接保存Aggregate的最新状态,而是保存对Aggregate造成影响的所有Event,通过Event回溯来恢复Aggregate状态 EventStorageEngine 提供event在底层storage读写的机制,内置了若干种: InMemoryEventStorageEngine 存储到内存中 JpaEventStorageEngine 使用JPA进行存储 JdbcEventStorageEngine 使用jdbc MongoEventStorageEngine 使用Mongodb存储event Serializer 由于是事件驱动框架,序列化器必不可少。Axon内置了三种:XStreamSerializer, JavaSerializer, JacksonSerializer,默认是XStreamSerializer,使用XStream来做序列化,理论上比Java自带的序列化器要快。 1234567891011121314public class Application { private static final Logger LOGGER = getLogger(Application.class); public static void main(String args[]){ Configuration config = DefaultConfigurer.defaultConfiguration() .configureAggregate(BankAccount.class) .configureEmbeddedEventStore(c -> new InMemoryEventStorageEngine()) .buildConfiguration(); config.start(); AccountId id = new AccountId(); config.commandGateway().send(new CreateAccountCommand(id, \"MyAccount\",1000)); config.commandGateway().send(new WithdrawMoneyCommand(id, 500)); config.commandGateway().send(new WithdrawMoneyCommand(id, 500)); }} Axon提供了DefaultConfigurer来帮助我们做一些基本配置,所以我们只需要简单的做Aggregate的注册和指定一个EventStorageEngine。这里因为是测试,用了InMemoryEventStorageEngine。CommandGateway是对CommandBus的一个封装,更加方便的来发送Command。 本文完整代码https://github.com/EdisonXu/sbs-axon/tree/master/lesson-1 前面说用一个构造器来接受CreateAccountCommand,有什么特殊地方。这里涉及到一个问题,就是Aggregate在Repository的创建。Axon中,打开@Aggregate注解的定义会发现里面其实定义了一个repository。12345/** * Selects the name of the AggregateRepository bean. If left empty a new repository is created. In that case the * name of the repository will be based on the simple name of the aggregate's class. */String repository() default \"\"; Axon其实会为每一个Aggregate对应一个AggregateRepository,如果不额外指定,会使用给定的StorageEngine对应的Repository。通常情况下,如果要在Repository里面保存Aggregate,需要执行repository.newInstance(()->new BankAccount()),但如果直接提供了构造器接受command,那么axon在执行这个command,如CreateAccountCommand时,会自动帮你做一个newInstance的操作。另外,有人会说,为什么要把CommandHandler、EventHandler放到Aggregate内部,能不能放到外面单独用一个类。答案是当然可以。Axon会自动扫描带有@CommandHandler,@EventHandler的方法,加载到KV值中。并没有明确规定说这些方法一定得放在Aggregate内部或外部,不过一般应该把仅涉及当前Aggregate状态变化的,放到Aggregate内部处理,如果牵扯到其他复杂逻辑,如查询其他Aggregate做判断等,则最好是另起一个handler类。","categories":[],"tags":[{"name":"CQRS","slug":"CQRS","permalink":"http://edisonxu.com/tags/CQRS/"},{"name":"axon","slug":"axon","permalink":"http://edisonxu.com/tags/axon/"},{"name":"DDD","slug":"DDD","permalink":"http://edisonxu.com/tags/DDD/"}]},{"title":"Axon入门系列(一):CQRS基本概念","slug":"hello-cqrs","date":"2017-03-23T04:57:52.000Z","updated":"2018-10-30T01:58:14.190Z","comments":true,"path":"2017/03/23/hello-cqrs.html","link":"","permalink":"http://edisonxu.com/2017/03/23/hello-cqrs.html","excerpt":"在研究微服务的过程中,跨服务的操作处理,尤其是带有事务性需要统一commit或rollback的,是比较麻烦的。本系列记录了我在研究这一过程中的心得体会。本篇主要就以下几个问题进行介绍: 微服务中的一个大难题 DDD中的几个基本概念 什么是EventSourcing? 什么是CQRS? EventSourcing和CQRS的关系? CQRS/ES怎么解决微服务的难题? 微服务中的一个大难题微服务架构已经热了有两年了,而且目测会越来越热,除非有更高级的架构出现。相关解释和说明,网上一搜一大堆,我这里就不重复了。一句话概括:微服务将原来的N个模块,或者说服务,按照适当的边界,从单节点划分成一整个分布式系统中的若干节点上。","text":"在研究微服务的过程中,跨服务的操作处理,尤其是带有事务性需要统一commit或rollback的,是比较麻烦的。本系列记录了我在研究这一过程中的心得体会。本篇主要就以下几个问题进行介绍: 微服务中的一个大难题 DDD中的几个基本概念 什么是EventSourcing? 什么是CQRS? EventSourcing和CQRS的关系? CQRS/ES怎么解决微服务的难题? 微服务中的一个大难题微服务架构已经热了有两年了,而且目测会越来越热,除非有更高级的架构出现。相关解释和说明,网上一搜一大堆,我这里就不重复了。一句话概括:微服务将原来的N个模块,或者说服务,按照适当的边界,从单节点划分成一整个分布式系统中的若干节点上。 原来服务间的交互直接代码级调用,现在则需要通过以下几种方式调用: SOA请求 RPC调用 ED(EventDriven)事件驱动 前面两种就比较类似,都属于直接调用,好处明显,缺点是请求者必须知道被请求方的地址。现在一般会提供额外的机制,如服务注册、发现等,来提供动态地址,实现负载和动态路由。目前大多数微服务框架都走的这条路子,如当下十分火热的SpringCloud等。事件驱动的方式,把请求者与被请求者的绑定关系解耦了,但是需要额外提供一个消息队列,请求者直接把消息发送到队列,被请求者监听队列,在获取到与自己有关系的事件时进行处理。主要缺点主要有二:1) 调用链不再直观;2) 高度依赖队列本身的性能和可靠性; 但无论是哪种方式,都使得传统架构下的事务无法再起到原先的作用了。事务的作用主要有二: 统一结果,要么都成功,要么都失败 并发时保证原子性操作 在传统架构下,无论是DB还是框架所提供的事务操作,都是基于同线/进程的。在微服务所处的分布式框架下,业务操作变成跨进程、跨节点,只能自行实现,而由于节点通信状态的不确定性、节点间生命周期的不统一等,把实现分布式事务的难度提高了很多。这就是微服务中的一个大难题。 DDD中的几个基本概念在进一步深入前,必须要了解几个基本概念。这些基本概念在EventSourcing和CQRS中都会用到。 Aggregate聚合。这个词或许听起来有点陌生,用集合或者组合就好理解点。 A DDD aggregate is a cluster of domain objects that can be treated as a single unit.—— Martin Fowler 以下图为例 车、轮子、轮胎构成了一个聚合。其中车是聚合根(AggregateRoot)Aggregate有两大特征: 明确的边界 AggregateRoot 具体来说,Aggregate存在于两种形式: 一个单独的对象; 一组相互有关联的对象,其中一个作为ROOT,外部只能通过AggregateRoot对这组对象进行交互;这里Customer不能直接访问Car下面的Tire,只能通过聚合根Car来访问。 什么是EventSourcing?不保存对象的最新状态,而是保存对象产生的所有事件。通过事件回溯(Event Sourcing, ES)得到对象最新的状态 以前我们是在每次对象参与完一个业务动作后把对象的最新状态持久化保存到数据库中,也就是说我们的数据库中的数据是反映了对象的当前最新的状态。而事件溯源则相反,不是保存对象的最新状态,而是保存这个对象所经历的每个事件,所有的由对象产生的事件会按照时间先后顺序有序的存放在数据库中。当我们需要这个对象的最新状态时,只要先创建一个空的对象,然后把和改对象相关的所有事件按照发生的先后顺序从先到后全部应用一遍即可。这个过程就是事件回溯。 因为一个事件就是表示一个事实,事实是不能被磨灭或修改的,所以ES中的事件本身是不可修改的(Immutable),不会有DELETE或UPDATE操作。ES很明显先天就会有个问题——由于不停的记录Event,回溯获得对象最新状态所需花的时间会与事件的数量成正比,当数据量大了以后,获取最新状态的时间也相对的比较长。而在很多的逻辑操作中,进行“写”前一般会需要“读”来做校验,所以ES架构的系统中一般会在内存中维护一份对象的最新状态,在启动时进行”预热”,读取所有持久化的事件进行回溯。这样在读对象——也就是Aggregate的最新状态时,就不会因为慢影响性能。同时,也可以根据一些策略,把一部分的Event合集所产生的状态作为一个snapshot,下次直接从该snapshot开始回溯。既然需要读,就不可避免的遇到并发问题。EventSourcing要求对回溯的操作必须是原子性的,具体实现可参照Actor模型。 Actor ModelActorModel的核心思想是与对象的交互不会直接调用,而是通过发消息。如下图:每一个Actor都有一个Mailbox,它收到的所有的消息都会先放入Mailbox中,然后Actor内部单线程处理Mailbox中的消息。从而保证对同一个Actor的任何消息的处理,都是线性的,无并发冲突。整个系统中,有很多的Actor,每个Actor都在处理自己Mailbox中的消息,Actor之间通过发消息来通信。Akka框架就是实现Actor模型的并行开发框架。Actor作为DDD聚合根,最新状态是在内存中。Actor的状态修改是由事件驱动的,事件被持久化起来,然后通过Event Sourcing的技术,还原特定Actor的最新状态到内存。另外,还有Eventuate,两者的作者是同一人,如果对Akka和Eventuate的区别感兴趣的话,可以参照我翻译的一篇文章(译)Akka Persistence和Eventuate的对比。 什么是CQRS?CQRS 架构全称是Command Query Responsibility Segregation,即命令查询职责分离,名词本身最早应该是Greg Young提出来的,但是概念却很早就有了。本质上,CQRS也是一种读写分离的机制,架构图如下: CQRS把整个系统划分成两块: Command Side 写的一边接收外部所有的Insert、Update、Delete命令,转化为Command,每一个Command修改一个Aggregate的状态。Command Side的命令通常不需要返回数据。注意:这种“写”操作过程中,可能会涉及“读”,因为要做校验,这时可直接在这一边进行读操作,而不需要再到Query Side去。 Query Side 读的一边接受所有查询请求,直接返回数据。 由于C端与Q端的分离,两端各有一个自己的Repository,可根据不同的特性选取不同的产品,比如C端用RMDB,而Q端选用读取速度更快的NoSQL产品。 CQRS适用的场景使用了CQRS架构,由于读写之间会有延迟,就意味着系统的一致性模型为最终一致性(Eventual Consistency),所以CQRS架构一般用于读比写大很多的场景。注意:CQRS并不像SOA、EDA(EventDrivenArchitecture)属于顶级架构,它有自己的局限性,并不适合于一切场景。有些天然适合于CRUD的系统,在评估CQRS所带来的好处与坏处后,认为利大于弊再选取CQRS。所以,通常CQRS只作为一个大系统中某部分功能实现时使用。 EventSourcing和CQRS的关系从前面的介绍,应该可以发现两者其实并没有直接的关系,但是EventSourcing天然适合CQRS架构的C端的实现。CQRS/ES整合在一起的架构,优缺点如下: 优点 记录了数据变化的完整过程,便于BI分析 可以有效解决线上的数据问题,重演一遍,就可以找到问题所在 可以随时将系统还原到任何一个时间点 正确的实施后,天然适合并发场景 缺点 事件数量巨大,造成存储端成本上升 通过回溯重演获取Aggregate状态时,如果相关事件过多,需要提前“预热” 事件本身的内容被重构后,必须兼容以前的事件 事件驱动对传统思维的改变,需要适应 实施门槛高,需要成熟框架或中间件支撑 CQRS/ES怎么解决微服务的难题?我们先把实现微服务事务中的主要难点列出来,然后看用CQRS/ES是怎么一一解决的。 必须自己实现事务的统一commit和rollback;这个是无论哪一种方式,都必须面对的问题。完全逃不掉。在DDD中有一个叫Saga的概念,专门用于统理这种复杂交互业务的,CQRS/ES架构下,由于本身就是最终一致性,所以都实现了Saga,可以使用该机制来做微服务下的transaction治理。 请求幂等请求发送后,由于各种原因,未能收到正确响应,而被请求端已经正确执行了操作。如果这时重发请求,则会造成重复操作。CQRS/ES架构下通过AggregateRootId、Version、CommandId三种标识来识别相同command,目前的开源框架都实现了幂等支持。 并发单点上,CQRS/ES中按事件的先来后到严格执行,内存中Aggregate的状态由单一线程原子操作进行改变。多节点上,通过EventStore的broker机制,毫秒级将事件复制到其他节点,保证同步性,同时支持版本回退。(Eventuate) CQRS/ES如何与微服务架构结合结合的方式很简单,就是把合适的服务变成CQRS/ES架构,然后提供一个统一的分布式消息队列。每个服务自己内部用的C或Q的Storage完全可以不同,但C端的Storage尽量使用同一个,例如MongoDB、Cansandra这种本身就是HA的,以保证可用性。同时也可以避免大数据分析导数据时需要从不同的库导。目前,相对成熟的CQRS/ES可用框架有: 名称 地址 语言 文档 特点 AxonFramework http://www.axonframework.org Java 比较全,更新及时 目前作者正在开发与SpringCloud的相关集成 Akka Persistence http://akka.io/ Scala(也有.Net版) 文档全 相对成熟,性能较强 Eventuate http://eventuate.io Scala 文档较少 与Akka同源,在Akka基础上对分布式相关功能进行了增强,提供AWS上的SaaS ENode https://github.com/tangxuehua/enode C# 博客 来自微软的国人原创 Confluent https://www.confluent.io Scala 文档较少 不仅仅只是CQRS/ES,是整个一套基于kafka的高性能微服务产品,提供商业版","categories":[],"tags":[{"name":"CQRS","slug":"CQRS","permalink":"http://edisonxu.com/tags/CQRS/"},{"name":"axon","slug":"axon","permalink":"http://edisonxu.com/tags/axon/"},{"name":"event sourcing","slug":"event-sourcing","permalink":"http://edisonxu.com/tags/event-sourcing/"}]},{"title":"论精益与领域设计","slug":"lean-and-ddd","date":"2017-03-17T07:00:21.000Z","updated":"2018-10-30T01:50:54.622Z","comments":true,"path":"2017/03/17/lean-and-ddd.html","link":"","permalink":"http://edisonxu.com/2017/03/17/lean-and-ddd.html","excerpt":"开始之前你是否遇到过这种事?一个“好”工程师,非常喜欢学习和钻研新的技术知识,JEE时代学习Spring,SSH时代学习分布式,大数据时代又开始学习Hadoop,Storm,云时代开始搞docker,k8……然后成长为一名“架构师”了,在他的公司,有一个重要项目,于是乎他决意把这个项目设计成一个全分布式的系统,毕竟“时髦”嘛。每一个小的开发团队负责分布式中的一个服务。由于架构师对这些程序员缺乏必要的指导和控制,每个程序员只熟悉自己负责的一小块工作,彼此间缺乏沟通和协作,这个项目的概念完整性很快就被破坏了,他们开始自行其事,分布式服务间大量的远程调用导致了性能和可伸缩性等多方面的问题,开发、测试、运维的工作难度与日递增。慢慢的,项目的开发人员,离职的越来越多,包含最初的架构师,代码的维护越来越难,最终只能重构,将十几个分布式服务合并为三个相对独立的集中应用。最初良好的架构,到最后沦为废墟。中间一位离职的程序员,换了一家公司,涨了一点工资,开始了另一段新系统建设的“狂欢”,周而复始,随着年龄的增大,他不再能够从软件开发中享受到乐趣,开发的生涯,对他来说痛苦开始多余快乐。学习新技术,他也比不上年轻人。运气好的话,他可以转产品、管理或做业务,运气不好的话,还得继续码代码,毕竟要养家糊口。从一家公司换到另一家公司…… “如果”","text":"开始之前你是否遇到过这种事?一个“好”工程师,非常喜欢学习和钻研新的技术知识,JEE时代学习Spring,SSH时代学习分布式,大数据时代又开始学习Hadoop,Storm,云时代开始搞docker,k8……然后成长为一名“架构师”了,在他的公司,有一个重要项目,于是乎他决意把这个项目设计成一个全分布式的系统,毕竟“时髦”嘛。每一个小的开发团队负责分布式中的一个服务。由于架构师对这些程序员缺乏必要的指导和控制,每个程序员只熟悉自己负责的一小块工作,彼此间缺乏沟通和协作,这个项目的概念完整性很快就被破坏了,他们开始自行其事,分布式服务间大量的远程调用导致了性能和可伸缩性等多方面的问题,开发、测试、运维的工作难度与日递增。慢慢的,项目的开发人员,离职的越来越多,包含最初的架构师,代码的维护越来越难,最终只能重构,将十几个分布式服务合并为三个相对独立的集中应用。最初良好的架构,到最后沦为废墟。中间一位离职的程序员,换了一家公司,涨了一点工资,开始了另一段新系统建设的“狂欢”,周而复始,随着年龄的增大,他不再能够从软件开发中享受到乐趣,开发的生涯,对他来说痛苦开始多余快乐。学习新技术,他也比不上年轻人。运气好的话,他可以转产品、管理或做业务,运气不好的话,还得继续码代码,毕竟要养家糊口。从一家公司换到另一家公司…… “如果”这是一个悲剧的故事,尤其是最后那位程序员。然而这可能是国内很多软件开发人员的真实写照。那么,我们从“上帝视角”来开看待这一切,会怎么样呢?先从“如果”开始吧。 如果最初那位“架构师”不盲目赶时髦,压制住自己想通过该项目展示自己功力的想法,转而根据实际的业务需要和人力资源情况,考虑适当扩展性、可维护性后,再设计这套架构,会不会好点? 如果那位“架构师”抽出时间,找到合适的方法来指导和控制开发的程序员,保证整个系统的概念完整性,架构会不会发挥出它应有的能力和好处呢? 如果那位程序员,发现自己志不在编码,早日转行找到适合自己的岗位,生活会不会幸福很多呢?比如之前中关村离职开水果店的那位成功”程序员”(一日程序员,终生程序员) 如果那位程序员,能够及时意识到学习新知识的重要性,并以此为乐,或许,他会成为另一位最初的那位架构师 如果那位程序员和那位架构师,在开始一切之前,与领域专家进行详细的领域建模,能把握相关领域的核心知识,对他们的进一步前行,转行、进阶架构、进阶CTO,会不会带来额外的好处或便利? 结论当然,世界上不允许有如果,也没有后悔药。废话不多说,如果要在技术上继续走下去,总结起来无外乎几点: 追求精益,即提高效率,避免浪费,哪怕你当了CTO、CEO; 重视沟通,重视团队管理,而不仅仅把注意力集中在新技术和良好的架构上; 一切都要透过现象看本质,把握核心要素; 保持危机感和饥渴; 如果不想在技术上走下去,可以到这里就结束了。友情提醒:当管理可比做技术难太多了,人心比海深。 我们从现在回头看软件开发的历史,开发模式的演进,从瀑布式到极限到敏捷,到现在的DevOps,还是技术的演进,其实都是在1、2两点上进行不断的尝试和优化。最终,1、2其实都可以归于精益的思想。对于精益,我就不多叙述了,强烈推荐精益创业这本经典,虽然书本身是写给产品和创业者的,但精益的思想是普适的。而3、4两点是针对个人的,对应到软件开发,就是DDD的出现。DDD最大的作用就是为了达到第三点(当然,对第2点也好很好的体现),把业务上真正核心要解决的问题进行建模。这里我就不再详细阐述DDD的具体内容了,随便一搜一大把,下一篇我会就其中几个比较重要的概念进行解释,但是千万不要把DDD与单纯的以名词建模混为一谈,我曾经见过直接把一个业务中所有的名词抽象出来进行OOP的……囧推荐看InfoQ几年前出的Domain-Driven Design Quickly(dddquickly)或中文精简版 ,有一定基础后,再去看原作者,也就是DDD概念的创始人Eric Evans的大作 Domain-Driven Design: Tacking Complexity in the Heart of Software 细思极恐,软件开发最终也还是跟哲学扯上了。","categories":[],"tags":[{"name":"DDD","slug":"DDD","permalink":"http://edisonxu.com/tags/DDD/"},{"name":"感悟","slug":"感悟","permalink":"http://edisonxu.com/tags/感悟/"}]},{"title":"JAVA线程间通信的几种方式","slug":"java-thread-communication","date":"2017-03-02T13:10:44.000Z","updated":"2018-02-23T02:19:15.417Z","comments":true,"path":"2017/03/02/java-thread-communication.html","link":"","permalink":"http://edisonxu.com/2017/03/02/java-thread-communication.html","excerpt":"今天在群里面看到一个很有意思的面试题:“编写两个线程,一个线程打印1~25,另一个线程打印字母A~Z,打印顺序为12A34B56C……5152Z,要求使用线程间的通信。”这是一道非常好的面试题,非常能彰显被面者关于多线程的功力,一下子就勾起了我的兴趣。这里抛砖引玉,给出7种想到的解法。 1. 第一种解法,包含多种小的不同实现方式,但一个共同点就是靠一个共享变量来做控制;a. 利用最基本的synchronized、notify、wait:","text":"今天在群里面看到一个很有意思的面试题:“编写两个线程,一个线程打印1~25,另一个线程打印字母A~Z,打印顺序为12A34B56C……5152Z,要求使用线程间的通信。”这是一道非常好的面试题,非常能彰显被面者关于多线程的功力,一下子就勾起了我的兴趣。这里抛砖引玉,给出7种想到的解法。 1. 第一种解法,包含多种小的不同实现方式,但一个共同点就是靠一个共享变量来做控制;a. 利用最基本的synchronized、notify、wait: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960 public class MethodOne { private final ThreadToGo threadToGo = new ThreadToGo(); public Runnable newThreadOne() { final String[] inputArr = Helper.buildNoArr(52); return new Runnable() { private String[] arr = inputArr; public void run() { try { for (int i = 0; i < arr.length; i=i+2) { synchronized (threadToGo) { while (threadToGo.value == 2) threadToGo.wait(); Helper.print(arr[i], arr[i + 1]); threadToGo.value = 2; threadToGo.notify(); } } } catch (InterruptedException e) { System.out.println(\"Oops...\"); } } }; } public Runnable newThreadTwo() { final String[] inputArr = Helper.buildCharArr(26); return new Runnable() { private String[] arr = inputArr; public void run() { try { for (int i = 0; i < arr.length; i++) { synchronized (threadToGo) { while (threadToGo.value == 1) threadToGo.wait(); Helper.print(arr[i]); threadToGo.value = 1; threadToGo.notify(); } } } catch (InterruptedException e) { System.out.println(\"Oops...\"); } } }; } class ThreadToGo { int value = 1; } public static void main(String args[]) throws InterruptedException { MethodOne one = new MethodOne(); Helper.instance.run(one.newThreadOne()); Helper.instance.run(one.newThreadTwo()); Helper.instance.shutdown(); }} b. 利用Lock和Condition:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364public class MethodTwo { private Lock lock = new ReentrantLock(true); private Condition condition = lock.newCondition(); private final ThreadToGo threadToGo = new ThreadToGo(); public Runnable newThreadOne() { final String[] inputArr = Helper.buildNoArr(52); return new Runnable() { private String[] arr = inputArr; public void run() { for (int i = 0; i < arr.length; i=i+2) { try { lock.lock(); while(threadToGo.value == 2) condition.await(); Helper.print(arr[i], arr[i + 1]); threadToGo.value = 2; condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } }; } public Runnable newThreadTwo() { final String[] inputArr = Helper.buildCharArr(26); return new Runnable() { private String[] arr = inputArr; public void run() { for (int i = 0; i < arr.length; i++) { try { lock.lock(); while(threadToGo.value == 1) condition.await(); Helper.print(arr[i]); threadToGo.value = 1; condition.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } }; } class ThreadToGo { int value = 1; } public static void main(String args[]) throws InterruptedException { MethodTwo two = new MethodTwo(); Helper.instance.run(two.newThreadOne()); Helper.instance.run(two.newThreadTwo()); Helper.instance.shutdown(); }} c. 利用volatile: volatile修饰的变量值直接存在main memory里面,子线程对该变量的读写直接写入main memory,而不是像其它变量一样在local thread里面产生一份copy。volatile能保证所修饰的变量对于多个线程可见性,即只要被修改,其它线程读到的一定是最新的值。1234567891011121314151617181920212223242526272829303132333435363738394041424344public class MethodThree { private volatile ThreadToGo threadToGo = new ThreadToGo(); class ThreadToGo { int value = 1; } public Runnable newThreadOne() { final String[] inputArr = Helper.buildNoArr(52); return new Runnable() { private String[] arr = inputArr; public void run() { for (int i = 0; i < arr.length; i=i+2) { while(threadToGo.value==2){} Helper.print(arr[i], arr[i + 1]); threadToGo.value=2; } } }; } public Runnable newThreadTwo() { final String[] inputArr = Helper.buildCharArr(26); return new Runnable() { private String[] arr = inputArr; public void run() { for (int i = 0; i < arr.length; i++) { while(threadToGo.value==1){} Helper.print(arr[i]); threadToGo.value=1; } } }; } public static void main(String args[]) throws InterruptedException { MethodThree three = new MethodThree(); Helper.instance.run(three.newThreadOne()); Helper.instance.run(three.newThreadTwo()); Helper.instance.shutdown(); }} d. 利用AtomicInteger:12345678910111213141516171819202122232425262728293031323334353637383940public class MethodFive { private AtomicInteger threadToGo = new AtomicInteger(1); public Runnable newThreadOne() { final String[] inputArr = Helper.buildNoArr(52); return new Runnable() { private String[] arr = inputArr; public void run() { for (int i = 0; i < arr.length; i=i+2) { while(threadToGo.get()==2){} Helper.print(arr[i], arr[i + 1]); threadToGo.set(2); } } }; } public Runnable newThreadTwo() { final String[] inputArr = Helper.buildCharArr(26); return new Runnable() { private String[] arr = inputArr; public void run() { for (int i = 0; i < arr.length; i++) { while(threadToGo.get()==1){} Helper.print(arr[i]); threadToGo.set(1); } } }; } public static void main(String args[]) throws InterruptedException { MethodFive five = new MethodFive(); Helper.instance.run(five.newThreadOne()); Helper.instance.run(five.newThreadTwo()); Helper.instance.shutdown(); }} 2. 第二种解法,是利用CyclicBarrierAPI;CyclicBarrier可以实现让一组线程在全部到达Barrier时(执行await()),再一起同时执行,并且所有线程释放后,还能复用它,即为Cyclic。CyclicBarrier类提供两个构造器:12345public CyclicBarrier(int parties, Runnable barrierAction) {}public CyclicBarrier(int parties) {} 这里是利用它到达Barrier后去执行barrierAction。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465public class MethodFour{ private final CyclicBarrier barrier; private final List<String> list; public MethodFour() { list = Collections.synchronizedList(new ArrayList<String>()); barrier = new CyclicBarrier(2,newBarrierAction()); } public Runnable newThreadOne() { final String[] inputArr = Helper.buildNoArr(52); return new Runnable() { private String[] arr = inputArr; public void run() { for (int i = 0, j=0; i < arr.length; i=i+2,j++) { try { list.add(arr[i]); list.add(arr[i+1]); barrier.await(); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } } } }; } public Runnable newThreadTwo() { final String[] inputArr = Helper.buildCharArr(26); return new Runnable() { private String[] arr = inputArr; public void run() { for (int i = 0; i < arr.length; i++) { try { list.add(arr[i]); barrier.await(); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } } } }; } private Runnable newBarrierAction(){ return new Runnable() { @Override public void run() { Collections.sort(list); list.forEach(c->System.out.print(c)); list.clear(); } }; } public static void main(String args[]){ MethodFour four = new MethodFour(); Helper.instance.run(four.newThreadOne()); Helper.instance.run(four.newThreadTwo()); Helper.instance.shutdown(); }} 这里多说一点,这个API其实还是利用lock和condition,无非是多个线程去争抢CyclicBarrier的instance的lock罢了,最终barrierAction执行时,是在抢到CyclicBarrierinstance的那个线程上执行的。 3. 第三种解法,是利用PipedInputStreamAPI;这里用流在两个线程间通信,但是Java中的Stream是单向的,所以在两个线程中分别建了一个input和output。这显然是一种很搓的方式,不过也算是一种通信方式吧……-_-T,执行的时候那种速度简直。。。请不要BS我。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889public class MethodSix { private final PipedInputStream inputStream1; private final PipedOutputStream outputStream1; private final PipedInputStream inputStream2; private final PipedOutputStream outputStream2; private final byte[] MSG; public MethodSix() { inputStream1 = new PipedInputStream(); outputStream1 = new PipedOutputStream(); inputStream2 = new PipedInputStream(); outputStream2 = new PipedOutputStream(); MSG = \"Go\".getBytes(); try { inputStream1.connect(outputStream2); inputStream2.connect(outputStream1); } catch (IOException e) { e.printStackTrace(); } } public void shutdown() throws IOException { inputStream1.close(); inputStream2.close(); outputStream1.close(); outputStream2.close(); } public Runnable newThreadOne() { final String[] inputArr = Helper.buildNoArr(52); return new Runnable() { private String[] arr = inputArr; private PipedInputStream in = inputStream1; private PipedOutputStream out = outputStream1; public void run() { for (int i = 0; i < arr.length; i=i+2) { Helper.print(arr[i], arr[i + 1]); try { out.write(MSG); byte[] inArr = new byte[2]; in.read(inArr); while(true){ if(\"Go\".equals(new String(inArr))) break; } } catch (IOException e) { e.printStackTrace(); } } } }; } public Runnable newThreadTwo() { final String[] inputArr = Helper.buildCharArr(26); return new Runnable() { private String[] arr = inputArr; private PipedInputStream in = inputStream2; private PipedOutputStream out = outputStream2; public void run() { for (int i = 0; i < arr.length; i++) { try { byte[] inArr = new byte[2]; in.read(inArr); while(true){ if(\"Go\".equals(new String(inArr))) break; } Helper.print(arr[i]); out.write(MSG); } catch (IOException e) { e.printStackTrace(); } } } }; } public static void main(String args[]) throws IOException { MethodSix six = new MethodSix(); Helper.instance.run(six.newThreadOne()); Helper.instance.run(six.newThreadTwo()); Helper.instance.shutdown(); six.shutdown(); } 4. 第四种解法,是利用BlockingQueue;顺便总结下BlockingQueue的一些内容。BlockingQueue定义的常用方法如下: add(Object):把Object加到BlockingQueue里,如果BlockingQueue可以容纳,则返回true,否则抛出异常。 offer(Object):表示如果可能的话,将Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false。 put(Object):把Object加到BlockingQueue里,如果BlockingQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里有空间再继续。 poll(time):获取并删除BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null。当不传入time值时,立刻返回。 peek():立刻获取BlockingQueue里排在首位的对象,但不从队列里删除,如果队列为空,则返回null。 take():获取并删除BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的对象被加入为止。 BlockingQueue有四个具体的实现类: ArrayBlockingQueue:规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小。其所含的对象是以FIFO(先入先出)顺序排序的。 LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定。其所含的对象是以FIFO顺序排序的。 PriorityBlockingQueue:类似于LinkedBlockingQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序。 SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。 这里我用了两种玩法: 一种是共享一个queue,根据peek和poll的不同来实现; 第二种是两个queue,利用take()会自动阻塞来实现。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485public class MethodSeven { private final LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(); public Runnable newThreadOne() { final String[] inputArr = Helper.buildNoArr(52); return new Runnable() { private String[] arr = inputArr; public void run() { for (int i = 0; i < arr.length; i=i+2) { Helper.print(arr[i], arr[i + 1]); queue.offer(\"TwoToGo\"); while(!\"OneToGo\".equals(queue.peek())){} queue.poll(); } } }; } public Runnable newThreadTwo() { final String[] inputArr = Helper.buildCharArr(26); return new Runnable() { private String[] arr = inputArr; public void run() { for (int i = 0; i < arr.length; i++) { while(!\"TwoToGo\".equals(queue.peek())){} queue.poll(); Helper.print(arr[i]); queue.offer(\"OneToGo\"); } } }; } private final LinkedBlockingQueue<String> queue1 = new LinkedBlockingQueue<>(); private final LinkedBlockingQueue<String> queue2 = new LinkedBlockingQueue<>(); public Runnable newThreadThree() { final String[] inputArr = Helper.buildNoArr(52); return new Runnable() { private String[] arr = inputArr; public void run() { for (int i = 0; i < arr.length; i=i+2) { Helper.print(arr[i], arr[i + 1]); try { queue2.put(\"TwoToGo\"); queue1.take(); } catch (InterruptedException e) { e.printStackTrace(); } } } }; } public Runnable newThreadFour() { final String[] inputArr = Helper.buildCharArr(26); return new Runnable() { private String[] arr = inputArr; public void run() { for (int i = 0; i < arr.length; i++) { try { queue2.take(); Helper.print(arr[i]); queue1.put(\"OneToGo\"); } catch (InterruptedException e) { e.printStackTrace(); } } } }; } public static void main(String args[]) throws InterruptedException { MethodSeven seven = new MethodSeven(); Helper.instance.run(seven.newThreadOne()); Helper.instance.run(seven.newThreadTwo()); Thread.sleep(2000); System.out.println(\"\"); Helper.instance.run(seven.newThreadThree()); Helper.instance.run(seven.newThreadFour()); Helper.instance.shutdown(); } 本文所有代码已上传至GitHub:https://github.com/EdisonXu/POC/tree/master/concurrent-test","categories":[],"tags":[{"name":"多线程","slug":"多线程","permalink":"http://edisonxu.com/tags/多线程/"}]},{"title":"(译)Akka Persistence和Eventuate的对比","slug":"akka-persistence-eventuate-comparison","date":"2017-01-22T09:23:53.000Z","updated":"2018-02-23T02:19:15.402Z","comments":true,"path":"2017/01/22/akka-persistence-eventuate-comparison.html","link":"","permalink":"http://edisonxu.com/2017/01/22/akka-persistence-eventuate-comparison.html","excerpt":"在实现微服务架构中,遇到了分布式事务的问题。Event-sourcing和CQRS是一个比较适合微服务的解决方案。在学习过程中,遇到了这篇文章,觉得很不错,特地翻译给大家。本文翻译自:A comparison of Akka Persistence with Eventuate Akka Persistence和Eventuate都是Scala写的,基于Akka的event-sourcing和CQRS工具,以不同的方式实现分布式系统方案。关于这两个工具的详情,请参见他们自己的在线文档。 我是Akka Persistence和Eventuate的原作者,目前主要关注在Eventuate的开发实现。当然,我的意见肯定会带有偏见;) 言归正传,如果我哪里写的不对,请一定一定告之我。","text":"在实现微服务架构中,遇到了分布式事务的问题。Event-sourcing和CQRS是一个比较适合微服务的解决方案。在学习过程中,遇到了这篇文章,觉得很不错,特地翻译给大家。本文翻译自:A comparison of Akka Persistence with Eventuate Akka Persistence和Eventuate都是Scala写的,基于Akka的event-sourcing和CQRS工具,以不同的方式实现分布式系统方案。关于这两个工具的详情,请参见他们自己的在线文档。 我是Akka Persistence和Eventuate的原作者,目前主要关注在Eventuate的开发实现。当然,我的意见肯定会带有偏见;) 言归正传,如果我哪里写的不对,请一定一定告之我。 Command side在Akka Persistence中,command这边(CQRS中的C)是由PersistentActor(PA)来实现的,而Eventuate是由EventSourcedActor(EA)来实现的。他们的内部状态代表了应用的写入模型。 PA和EA根据写入模型来对新的command进行校验,如果校验成功,则生成并持久化一条/多条event,后续用于更新内部状态。当crash或正常的应用重启,内部状态可以通过重演整个event log中已持久化的event或从某一个snapshot开始重演,来恢复内部状态。PA和EA都支持发送消息到其他actor的至少送达一次机制, Akka Persistence提供了AtleastOnceDelivery来实现,而Eventuate则使用ConfirmedDelivery。 从这个角度来看,PA和EA非常相似。一个主要的区别是,PA必须是单例,而EA则是可复制和同步修改的多实例。如果Akka Persistence意外地创建和更新了两个具有相同persistenceId的PA的实例,那么底层的event log将会被污染,要么是覆盖已有事件,要么把彼此冲突的事件拼接了起来(重演结果将不再准确)。Akka Persitence的event log设计只容许一个writer,并且event log本身是不能被共享的。 在Eventtuate中,EA可以共享同一个event log。基于事先自定义的event路由规则,一个EA发出的的event可以被另一个EA消费。换而言之,EA之间通过这个共享的event log可以进行协作,例如不同类型的EA一起组成一个分布式业务流程,或者实现状态复制中多地址下相同类型的EA的重建和内部状态的更新。这里的多地址甚至可以是全局分布的(globally distributed)。多地址间的状态复制是异步的,并保证可靠性。 Event Relations在Akka Persistence中,每个PA产生的event是有序的,而不同PA产生的event之间是没有任何关联性的。即使一个PA产生的event是比另一个PA产生的event早诞生,但是Akka Persistence不会记录这种先后顺序。比如,PA1持久化了一个事件e1,然后发送了一个command给PA2,使得后者在处理该command时持久化了另一个事件e2,那么显然e1是先于e2的,但是系统本身无法通过对比e1和e2来决定他们之间的这种先后的关联性。 Eventuate额外跟踪记录了这种happened-before的关联性(潜在的因果关系)。例如,如果EA1持久化了事件e1,EA2因为消费了e1而产生了事件e2,那么e1比e2先发生的这种关联性会被记录下来。happen-before关联性由vector clocks来跟踪记录,系统可以通过对比两个event的vector timestamps来决定他们之间的关联性是先后发生的还是同时发生的。 跟踪记录event间的happened-before关联是运行多份EA relica的前提。EA在消费来自于它的replica的event时,必需要清楚它的内部状态的更新到底是先于该事件的,还是同时发生(可能产生冲突)。 如果最后一次状态更新先于准备消费的event,那么这个准备消费的event可被当作一个普通的更新来处理;但如果是同时产生的,那么该event可能具有冲突性,必须做相应处理,比如, 如果EA内部状态是CRDT,则该冲突可以被自动解决(详见Eventuate的operation-based CRDTs) 如果EA内部状态不是CRDT,Eventuate提供了进一步的方法来跟踪处理冲突,根据情况选择自动化或手动交互处理的方式。 Event logs前文提到过,Akka Persistence中,每一个PA有自己的event log。根据不同的存储后端,event log可冗余式的存于多个node上(比如为了保证高可用而采用的同步复制),也可存于本地。不管是哪种方式,Akka Persistence都要求对event log的强一致性。 比如,当一个PA挂掉后,在另外一个node上恢复时,必须要保证该PA能够按正确的顺序读取到所有之前写入的event,否则这次恢复就是不完整的,可能会导致这个PA后面会覆写已存在的event,或者把一些与evet log中已有还未读的event有冲突的新event直接拼到event log后面,进而导致状态的不一致。所以,只有支持强一致性的存储后端才能被Akka Persistence使用。 AKka Persistence的写可用性取决于底层的存储后端的写可用性。根据CAP理论,对于强一致性、分布式的存储后端,它的写可用性是有局限性的,所以,Akka Persistence的command side选择CAP中的CP。 这种限制导致Akka Persistence很难做到全局分布下应用的强一致性,并且所有event的有序性还需要实现全局统一的协调处理。Eventuate在这点上做得要更好:它只要求在一个location上保持强一致性和event的有序性。这个location可以是一个数据中心、一个(微)服务、分布式中的一个节点、单节点下的一个流程等。 单location的Eventuate应用与Akka Persistence应用具有相同的一致性模型。但是,Eventuate应用通常会有多个location。单个location所产生的event会异步地、可靠地复制到其他location。跨location的evet复制是Eventuate独有的,并且保留了因果事件的存储顺序。不同location的存储后端之间并不直接通信,所以,不同location可以使用不同的存储后端。 Eventuate中在不同location间复制的event log被称之为replicated event log,它在某一个location上的代表被称为local event log。在不同location上的EA可以通过共享同一个replicated event log来进行event交换,从而为EA状态的跨location的状态复制提供了可能。即便是跨location的网络发生了隔断(network partition),EA和它们底层的event log仍保持可写。从这个角度来说,一个多location的Eventuate应用从CAP中选择了AP。网络隔断后,在不同location的写入,可能会导致事件冲突,可用前面提到的方案去解决。 通过引入分割容忍(系统中任意信息的丢失或失败,不影响系统的继续运作)的location,event的全局完整排序将不再可能。在这种限制下的最强的部分排序是因果排序(casual ordering),例如保证happened-before关联关系的排序。Eventuate中,每一个location保证event以casual order递交给它们的本地EA(以及View,具体参见下一节)。并发event在个别location的递交顺序可能不同,但在指定的location可重复提交的。 Query sideAkka Persistence中,查询的一端(CQRS中的Q)可以用PersistentView(PV)来实现。目前一个PV仅限于消费一个PA所产生的event。这种限制在Akka的邮件群里被大量讨论过。从Akka2.4开始,一个比较好的方案是Akka Persistence Query:把多个PA产生的event通过storage plugin进行聚合,聚合结果称为Akka Streams,把Akka Streams作为PV的输入。 Eventuate中,查询的这端由EventsourcedView(EV)来实现。一个EV可以消费来自于所有共享同一个event log的EA所产生的event,即使这些EA是全局分布式的。event永远按照正确的casual order被消费。一个Eventuate应用可以只用一个replicated event log,也可以用类似以topic形式区分的多个event log。未来的一些扩展将允许EV直接消费多个event log,同时,Eventuate的Akka Stream API也在规划中。 Storage plugins从storage plugin的角度看,Akka Persistence中event主要以persistenceId来区分管理,即每个PA实例拥有自己的event log。从多个PA实例进行event聚合就要求要么在storage后端创建额外的索引,要么创建实时的event流组成图(stream composition),来服务查询。Eventuate中,从多个EA来的event都存储在同一个共享的event log中。在恢复时,没有预定义aggregateId的EA可消费该event log中的所有event,而定义过aggregateId的EA则只能作为路由目的地消费对应aggregateId的event。这就要求Eventuate的storage plugin必须维护一个独立index,以便于event通过aggregateId来重演。 Akka Persistence提供了一个用以存储日志和snapshot的公共storage plguin API,社区贡献了很多具体实现。Eventuate在未来也会定义一个公共的storage plugin API。就目前而言,可在LevelDB和Canssandra两者间任选一个作为存储后台。 ThroughputAkka Persistence中的PA和Eventuate中的EA都可以选择是否保持内部状态与event log中的同步。这关系到应用在写入一个新的event前,需要对新command和内部状态所进行的校验。为了防止被校验的是陈旧状态(stale state),新的command必须要等到当前正在运行的写操作成功结束。PA通过persist方法支持该机制(相反对应的是persistAsync),EA则使用一个stateSync的布尔变量。 同步内部状态与event log的后果是造成吞吐率的下降。由于event批量写入实现的不同,Akka Persistence中的这种内部状态同步比Eventuate所造成的的影响要更大。Akka Persistence中,event的批处理只有在使用persistAsync时,才是在PA层面的,而Eventuate在EA和storage plugin两个地方分别提供了批处理,所以对于不同的EA实例所产生的event,即使他们与内部状态要同步,也能被批量写入。 Akka Persistence和Eventuate中,单个PA和EA实例的吞吐率大致上是一样的(前提是所用的storage plugin具有可比性)。但是,Eventuate的整体吞吐率可以通过增加EA实例来得到提升,Akka Persistence就不行。这对于按照每个聚合一个PA/EA的设计,且有成千上万的活跃(可写)实例的应用,就显得特别有意义。仔细阅读Akka Persistence的代码,我认为把批处理逻辑从PA挪到独立的层面去,应该不需要很大的功夫。 ConclusionEventuate支持与Akka Persistence相同的一致性模型,但是额外支持了因果一致性,对于实现EA的高可用和分隔容忍(CAP中的AP)是必要条件。Eventuate还支持基于因果排序、去重复event流的可靠actor协作。从这些角度来看,Eventuate是Akka Persistence功能性的超集。 如果选择可用性高于一致性,冲突发现和(自动或交互式的)解决必须是首要考虑。Eventuate通过提供operation-based CRDT以及发现和解决应用状态冲突版本的工具、API,来提供支持。 对于分布式系统的弹性来说,处理错误比预防错误要显得更为重要。一个临时与其他location掉队分隔的location能继续保持运作,使得Eventuate成为离线场景的一个有意思的选择。 Eventuate现在仍是一个比较早期的项目,2014年末发布原型,2015年开源。目前是在Red Bull Media House (RBMH) Open Source Initiative下进行开发,主要用于RBMH的内部项目。 年前年后杂七杂八事情太多,导致一直没能专心做研究,进度缓慢。本篇翻译有些地方可能比较难懂,因为确实没足够时间去研究Eventuate和Akka Persistence,对于作者有些表达的不是很清晰的地方,弄得还不是很清楚,只能字面上翻过来。以后再慢慢修正。","categories":[],"tags":[{"name":"akka","slug":"akka","permalink":"http://edisonxu.com/tags/akka/"},{"name":"eventsourcing","slug":"eventsourcing","permalink":"http://edisonxu.com/tags/eventsourcing/"},{"name":"eventuate","slug":"eventuate","permalink":"http://edisonxu.com/tags/eventuate/"}]},{"title":"博客新家","slug":"newblog","date":"2017-01-12T09:42:14.000Z","updated":"2018-02-23T02:19:15.417Z","comments":true,"path":"2017/01/12/newblog.html","link":"","permalink":"http://edisonxu.com/2017/01/12/newblog.html","excerpt":"花了一天的时间,学习了Hexo,自定义了下模板,买了域名,搞了github和coding.net双站解析。终于把这个新站建好了。暂时先放在免费平台上吧。这里特别感谢程序猿DD贡献给我模板,这个Blog是高仿他的,细节部分做了些改动。以后慢慢往这里堆文字吧。","text":"花了一天的时间,学习了Hexo,自定义了下模板,买了域名,搞了github和coding.net双站解析。终于把这个新站建好了。暂时先放在免费平台上吧。这里特别感谢程序猿DD贡献给我模板,这个Blog是高仿他的,细节部分做了些改动。以后慢慢往这里堆文字吧。","categories":[],"tags":[{"name":"杂项","slug":"杂项","permalink":"http://edisonxu.com/tags/杂项/"}]}]}