<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>EdisonXu的技术分享</title>
  
  <subtitle>EdisonXu&#39;s Blog</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="http://edisonxu.com/"/>
  <updated>2021-07-21T13:31:21.786Z</updated>
  <id>http://edisonxu.com/</id>
  
  <author>
    <name>Edison Xu</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>SpringCloudGateway CORS方案看这篇就够了</title>
    <link href="http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html"/>
    <id>http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html</id>
    <published>2020-10-14T10:19:12.000Z</published>
    <updated>2021-07-21T13:31:21.786Z</updated>
    
    <content type="html"><![CDATA[<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>在Spring Cloud项目中，前后端分离目前很常见，在调试时，会遇到两种情况的跨域：</p><ol><li>前端页面通过不同域名或IP访问微服务的后台，例如前端人员会在本地起HttpServer 直连后台开发本地起的服务，此时，如果不加任何配置，前端页面的请求会被浏览器跨域限制拦截，所以，业务服务常常会添加如下代码设置全局跨域：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> CorsFilter <span class="title">corsFilter</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    logger.debug(<span class="string">"CORS限制打开"</span>);</span><br><span class="line">    CorsConfiguration config = <span class="keyword">new</span> CorsConfiguration();</span><br><span class="line">    # 仅在开发环境设置为*</span><br><span class="line">    config.addAllowedOrigin(<span class="string">"*"</span>);</span><br><span class="line">    config.addAllowedHeader(<span class="string">"*"</span>);</span><br><span class="line">    config.addAllowedMethod(<span class="string">"*"</span>);</span><br><span class="line">    config.setAllowCredentials(<span class="keyword">true</span>);</span><br><span class="line">    UrlBasedCorsConfigurationSource configSource = <span class="keyword">new</span> UrlBasedCorsConfigurationSource();</span><br><span class="line">    configSource.registerCorsConfiguration(<span class="string">"/**"</span>, config);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> CorsFilter(configSource);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li><p>前端页面通过不同域名或IP访问SpringCloud Gateway，例如前端人员在本地起HttpServer直连服务器的Gateway进行调试。此时，同样会遇到跨域。需要在Gateway的配置文件中增加：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line"><span class="attr">  cloud:</span></span><br><span class="line"><span class="attr">    gateway:</span></span><br><span class="line"><span class="attr">      globalcors:</span></span><br><span class="line"><span class="attr">        cors-configurations:</span></span><br><span class="line">        <span class="comment"># 仅在开发环境设置为*</span></span><br><span class="line">          <span class="string">'[/**]'</span><span class="string">:</span></span><br><span class="line"><span class="attr">            allowedOrigins:</span> <span class="string">"*"</span></span><br><span class="line"><span class="attr">            allowedHeaders:</span> <span class="string">"*"</span></span><br><span class="line"><span class="attr">            allowedMethods:</span> <span class="string">"*"</span></span><br></pre></td></tr></table></figure></li></ol><p>那么，此时直连微服务和网关的跨域问题都解决了，是不是很完美？</p><p>No~ 问题来了，前端仍然会报错：“<strong>不允许有多个’Access-Control-Allow-Origin’ CORS头</strong>”。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Access to XMLHttpRequest at &apos;http://192.168.2.137:8088/api/two&apos; from origin &apos;http://localhost:3200&apos; has been blocked by CORS policy: </span><br><span class="line">The &apos;Access-Control-Allow-Origin&apos; header contains multiple values &apos;*, http://localhost:3200&apos;, but only one is allowed.</span><br></pre></td></tr></table></figure><p>仔细查看返回的响应头，里面包含了两份Access-Control-Allow-Origin头。</p><p>我们用客户端版的PostMan做一个模拟，在请求里设置头：<code>Origin : *</code> ，查看返回结果的头:</p><blockquote><p>不能用Chrome插件版，由于浏览器的限制，插件版设置Origin的Header是无效的</p></blockquote><p><img src="/images/2020/10/20201015101503184.png" alt=""></p><p>发现问题了：</p><p><code>Vary</code> 和  <code>Access-Control-Allow-Origin</code> 两个头重复了两次，其中浏览器对后者有唯一性限制！</p><h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><ol><li>Spring Cloud Gateway是基于<code>SpringWebFlux</code>的，所有web请求首先是交给<code>DispatcherHandler</code>进行处理的，将HTTP请求交给具体注册的handler去处理。</li></ol><p>我们知道Spring Cloud Gateway进行请求转发，是在配置文件里配置路由信息，一般都是用url predicates模式，对应的就是<code>RoutePredicateHandlerMapping</code> 。所以，<code>DispatcherHandler</code>会把请求交给 <code>RoutePredicateHandlerMapping.</code> </p><p><img src="/images/2020/10/20201016093652859.png" alt=""></p><ol start="2"><li>那么，接下来看下 <code>RoutePredicateHandlerMapping.getHandler(ServerWebExchange exchange)</code> 方法，默认提供者是其父类 <code>AbstractHandlerMapping</code> ：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Mono&lt;Object&gt; <span class="title">getHandler</span><span class="params">(ServerWebExchange exchange)</span> </span>&#123;</span><br><span class="line"><span class="keyword">return</span> getHandlerInternal(exchange).map(handler -&gt; &#123;</span><br><span class="line"><span class="keyword">if</span> (logger.isDebugEnabled()) &#123;</span><br><span class="line">logger.debug(exchange.getLogPrefix() + <span class="string">"Mapped to "</span> + handler);</span><br><span class="line">&#125;</span><br><span class="line">ServerHttpRequest request = exchange.getRequest();</span><br><span class="line"><span class="comment">// 可以看到是在这一行就进行CORS判断，两个条件：</span></span><br><span class="line"><span class="comment">// 1. 是否配置了CORS，如果不配的话，默认是返回false的</span></span><br><span class="line"><span class="comment">// 2. 或者当前请求是OPTIONS请求，且头里包含ORIGIN和ACCESS_CONTROL_REQUEST_METHOD</span></span><br><span class="line"><span class="keyword">if</span> (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) &#123;</span><br><span class="line">CorsConfiguration config = (<span class="keyword">this</span>.corsConfigurationSource != <span class="keyword">null</span> ? <span class="keyword">this</span>.corsConfigurationSource.getCorsConfiguration(exchange) : <span class="keyword">null</span>);</span><br><span class="line">CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);</span><br><span class="line">config = (config != <span class="keyword">null</span> ? config.combine(handlerConfig) : handlerConfig);</span><br><span class="line"><span class="comment">//此处交给DefaultCorsProcessor去处理了</span></span><br><span class="line"><span class="keyword">if</span> (!<span class="keyword">this</span>.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) &#123;</span><br><span class="line"><span class="keyword">return</span> REQUEST_HANDLED_HANDLER;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> handler;</span><br><span class="line">&#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>注：</p><p>网上有些关于修改Gateway的CORS设定的方式，是跟前面SpringBoot一样，实现一个<code>CorsWebFilter</code>的Bean，靠写代码提供 <code>CorsConfiguration</code> ，而不是修改Gateway的配置文件。其实本质，都是将配置交给corsProcessor去处理，殊途同归。但靠配置解决永远比hard code来的优雅。</p></blockquote><p>该方法把Gateway里定义的所有的 <code>GlobalFilter</code> 加载进来，作为handler返回，但在返回前，先进行CORS校验，获取配置后，交给corsProcessor去处理，即<code>DefaultCorsProcessor</code>类</p><ol start="3"><li>看下<code>DefaultCorsProcessor</code>的process方法：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">process</span><span class="params">(@Nullable CorsConfiguration config, ServerWebExchange exchange)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    ServerHttpRequest request = exchange.getRequest();</span><br><span class="line">    ServerHttpResponse response = exchange.getResponse();</span><br><span class="line">    HttpHeaders responseHeaders = response.getHeaders();</span><br><span class="line"></span><br><span class="line">    List&lt;String&gt; varyHeaders = responseHeaders.get(HttpHeaders.VARY);</span><br><span class="line">    <span class="keyword">if</span> (varyHeaders == <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="comment">// 第一次进来时，肯定是空，所以加了一次VERY的头，包含ORIGIN, ACCESS_CONTROL_REQUEST_METHOD和ACCESS_CONTROL_REQUEST_HEADERS</span></span><br><span class="line">        responseHeaders.addAll(HttpHeaders.VARY, VARY_HEADERS);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">for</span> (String header : VARY_HEADERS) &#123;</span><br><span class="line">            <span class="keyword">if</span> (!varyHeaders.contains(header)) &#123;</span><br><span class="line">                responseHeaders.add(HttpHeaders.VARY, header);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!CorsUtils.isCorsRequest(request)) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (responseHeaders.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != <span class="keyword">null</span>) &#123;</span><br><span class="line">        logger.trace(<span class="string">"Skip: response already contains \"Access-Control-Allow-Origin\""</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">boolean</span> preFlightRequest = CorsUtils.isPreFlightRequest(request);</span><br><span class="line">    <span class="keyword">if</span> (config == <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (preFlightRequest) &#123;</span><br><span class="line">            rejectRequest(response);</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> handleInternal(exchange, config, preFlightRequest);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在这个类里进行实际的CORS校验和处理</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">boolean</span> <span class="title">handleInternal</span><span class="params">(ServerWebExchange exchange,</span></span></span><br><span class="line"><span class="function"><span class="params">                                 CorsConfiguration config, <span class="keyword">boolean</span> preFlightRequest)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    ServerHttpRequest request = exchange.getRequest();</span><br><span class="line">    ServerHttpResponse response = exchange.getResponse();</span><br><span class="line">    HttpHeaders responseHeaders = response.getHeaders();</span><br><span class="line"></span><br><span class="line">    String requestOrigin = request.getHeaders().getOrigin();</span><br><span class="line">    String allowOrigin = checkOrigin(config, requestOrigin);</span><br><span class="line">    <span class="keyword">if</span> (allowOrigin == <span class="keyword">null</span>) &#123;</span><br><span class="line">        logger.debug(<span class="string">"Reject: '"</span> + requestOrigin + <span class="string">"' origin is not allowed"</span>);</span><br><span class="line">        rejectRequest(response);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);</span><br><span class="line">    List&lt;HttpMethod&gt; allowMethods = checkMethods(config, requestMethod);</span><br><span class="line">    <span class="keyword">if</span> (allowMethods == <span class="keyword">null</span>) &#123;</span><br><span class="line">        logger.debug(<span class="string">"Reject: HTTP '"</span> + requestMethod + <span class="string">"' is not allowed"</span>);</span><br><span class="line">        rejectRequest(response);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    List&lt;String&gt; requestHeaders = getHeadersToUse(request, preFlightRequest);</span><br><span class="line">    List&lt;String&gt; allowHeaders = checkHeaders(config, requestHeaders);</span><br><span class="line">    <span class="keyword">if</span> (preFlightRequest &amp;&amp; allowHeaders == <span class="keyword">null</span>) &#123;</span><br><span class="line">        logger.debug(<span class="string">"Reject: headers '"</span> + requestHeaders + <span class="string">"' are not allowed"</span>);</span><br><span class="line">        rejectRequest(response);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//此处添加了AccessControllAllowOrigin的头</span></span><br><span class="line">    responseHeaders.setAccessControlAllowOrigin(allowOrigin);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (preFlightRequest) &#123;</span><br><span class="line">        responseHeaders.setAccessControlAllowMethods(allowMethods);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (preFlightRequest &amp;&amp; !allowHeaders.isEmpty()) &#123;</span><br><span class="line">        responseHeaders.setAccessControlAllowHeaders(allowHeaders);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!CollectionUtils.isEmpty(config.getExposedHeaders())) &#123;</span><br><span class="line">        responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (Boolean.TRUE.equals(config.getAllowCredentials())) &#123;</span><br><span class="line">        responseHeaders.setAccessControlAllowCredentials(<span class="keyword">true</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (preFlightRequest &amp;&amp; config.getMaxAge() != <span class="keyword">null</span>) &#123;</span><br><span class="line">        responseHeaders.setAccessControlMaxAge(config.getMaxAge());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，在<code>DefaultCorsProcessor</code> 中，根据我们在<code>appliation.yml</code> 中的配置，给Response添加了 <code>Vary</code> 和 <code>Access-Control-Allow-Origin</code> 的头。</p><p><img src="/images/2020/10/20201016114136290.png" alt=""></p><ol start="4"><li>再接下来就是进入各个GlobalFilter进行处理了，其中<code>NettyRoutingFilter</code> 是负责实际将请求转发给后台微服务，并获取Response的，重点看下代码中filter的处理结果的部分：</li></ol><p><img src="/images/2020/10/20201016105802651.png" alt=""></p><p>其中以下几种header会被过滤掉的：</p><p><img src="/images/2020/10/20201016110152153.png" alt=""></p><p>很明显，在图里的第3步中，如果后台服务返回的header里有 <code>Vary</code> 和 <code>Access-Control-Allow-Origin</code> ，这时由于是putAll，没有做任何去重就加进去了，必然会重复，看看DEBUG结果验证一下：</p><p><img src="/images/2020/10/20201016114929914.png" alt=""></p><p>验证了前面的发现。</p><h2 id="解决"><a href="#解决" class="headerlink" title="解决"></a>解决</h2><p>解决的方案有两种：</p><h4 id="1-利用-DedupeResponseHeader-配置："><a href="#1-利用-DedupeResponseHeader-配置：" class="headerlink" title="1. 利用 DedupeResponseHeader 配置："></a>1. 利用 <code>DedupeResponseHeader</code> 配置：</h4><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line"><span class="attr">    cloud:</span></span><br><span class="line"><span class="attr">        gateway:</span></span><br><span class="line"><span class="attr">          globalcors:</span></span><br><span class="line"><span class="attr">            cors-configurations:</span></span><br><span class="line">              <span class="string">'[/**]'</span><span class="string">:</span></span><br><span class="line"><span class="attr">                allowedOrigins:</span> <span class="string">"*"</span></span><br><span class="line"><span class="attr">                allowedHeaders:</span> <span class="string">"*"</span></span><br><span class="line"><span class="attr">                allowedMethods:</span> <span class="string">"*"</span></span><br><span class="line"><span class="attr">          default-filters:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">DedupeResponseHeader=Vary</span> <span class="string">Access-Control-Allow-Origin</span> <span class="string">Access-Control-Allow-Credentials,</span> <span class="string">RETAIN_FIRST</span></span><br></pre></td></tr></table></figure><p><code>DedupeResponseHeader</code> 加上以后会启用<code>DedupeResponseHeaderGatewayFilterFactory</code> 在其中，<code>dedupe</code>方法可以按照给定策略处理值</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">dedupe</span><span class="params">(HttpHeaders headers, String name, Strategy strategy)</span> </span>&#123;</span><br><span class="line">List&lt;String&gt; values = headers.get(name);</span><br><span class="line"><span class="keyword">if</span> (values == <span class="keyword">null</span> || values.size() &lt;= <span class="number">1</span>) &#123;</span><br><span class="line"><span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">switch</span> (strategy) &#123;</span><br><span class="line"><span class="comment">// 只保留第一个</span></span><br><span class="line"><span class="keyword">case</span> RETAIN_FIRST:</span><br><span class="line">headers.set(name, values.get(<span class="number">0</span>));</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line"><span class="comment">// 保留最后一个        </span></span><br><span class="line"><span class="keyword">case</span> RETAIN_LAST:</span><br><span class="line">headers.set(name, values.get(values.size() - <span class="number">1</span>));</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line"><span class="comment">// 去除值相同的</span></span><br><span class="line"><span class="keyword">case</span> RETAIN_UNIQUE:</span><br><span class="line">headers.put(name, values.stream().distinct().collect(Collectors.toList()));</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>如果请求中设置的Origin的值与我们自己设置的是同一个，例如生产环境设置的都是自己的域名xxx.com或者开发测试环境设置的都是*（浏览器中是无法设置Origin的值，设置了也不起作用，浏览器默认是当前访问地址），那么可以选用<code>RETAIN_UNIQUE</code>策略，去重后返回到前端。</li><li>如果请求中设置的Oringin的值与我们自己设置的不是同一个，<code>RETAIN_UNIQUE</code>策略就无法生效，比如 ”*“ 和 ”xxx.com“是两个不一样的Origin,最终还是会返回两个<code>Access-Control-Allow-Origin</code> 的头。此时，看代码里，response的header里，先加入的是我们自己配置的<code>Access-Control-Allow-Origin</code>的值，所以，我们可以将策略设置为<code>RETAIN_FIRST</code> ，只保留我们自己设置的。</li></ul><p>大多数情况下，我们想要返回的是我们自己设置的规则，所以直接使用<code>RETAIN_FIRST</code> 即可。实际上，<code>DedupeResponseHeader</code>  可以针对所有头，做重复的处理。</p><h4 id="2-手动写一个-CorsResponseHeaderFilter-的-GlobalFilter-去修改Response中的头。"><a href="#2-手动写一个-CorsResponseHeaderFilter-的-GlobalFilter-去修改Response中的头。" class="headerlink" title="2. 手动写一个 CorsResponseHeaderFilter 的 GlobalFilter 去修改Response中的头。"></a>2. 手动写一个 <code>CorsResponseHeaderFilter</code> 的 <code>GlobalFilter</code> 去修改Response中的头。</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CorsResponseHeaderFilter</span> <span class="keyword">implements</span> <span class="title">GlobalFilter</span>, <span class="title">Ordered</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger logger = LoggerFactory.getLogger(CorsResponseHeaderFilter.class);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> String ANY = <span class="string">"*"</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getOrder</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 指定此过滤器位于NettyWriteResponseFilter之后</span></span><br><span class="line">        <span class="comment">// 即待处理完响应体后接着处理响应头</span></span><br><span class="line">        <span class="keyword">return</span> NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="meta">@SuppressWarnings</span>(<span class="string">"serial"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> Mono&lt;Void&gt; <span class="title">filter</span><span class="params">(ServerWebExchange exchange, GatewayFilterChain chain)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> chain.filter(exchange).then(Mono.fromRunnable(() -&gt; &#123;</span><br><span class="line">            exchange.getResponse().getHeaders().entrySet().stream()</span><br><span class="line">                    .filter(kv -&gt; (kv.getValue() != <span class="keyword">null</span> &amp;&amp; kv.getValue().size() &gt; <span class="number">1</span>))</span><br><span class="line">                    .filter(kv -&gt; (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)</span><br><span class="line">                            || kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)</span><br><span class="line">                            || kv.getKey().equals(HttpHeaders.VARY)))</span><br><span class="line">                    .forEach(kv -&gt;</span><br><span class="line">                    &#123;</span><br><span class="line">                        <span class="comment">// Vary只需要去重即可</span></span><br><span class="line">                        <span class="keyword">if</span>(kv.getKey().equals(HttpHeaders.VARY))</span><br><span class="line">                            kv.setValue(kv.getValue().stream().distinct().collect(Collectors.toList()));</span><br><span class="line">                        <span class="keyword">else</span>&#123;</span><br><span class="line">                            List&lt;String&gt; value = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">                            <span class="keyword">if</span>(kv.getValue().contains(ANY))&#123;  <span class="comment">//如果包含*，则取*</span></span><br><span class="line">                                value.add(ANY);</span><br><span class="line">                                kv.setValue(value);</span><br><span class="line">                            &#125;<span class="keyword">else</span>&#123;</span><br><span class="line">                                value.add(kv.getValue().get(<span class="number">0</span>)); <span class="comment">// 否则默认取第一个</span></span><br><span class="line">                                kv.setValue(value);</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;);</span><br><span class="line">        &#125;));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此处有两个地方要注意：</p><ol><li>根据下图可以看到，在取得返回值后，Filter的<code>Order</code> 值越大，越先处理Response，而真正将Response返回到前端的，是 <code>NettyWriteResponseFilter</code>, 我们要想在它之前修改Response，则<code>Order</code> 的值必须比<code>NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER</code> 大。</li></ol><p><img src="/images/2020/10/spring-cloud-gateway-fliter-order.png" alt="spring-cloud-gateway-fliter-order.png" style="zoom: 50%;"></p><ol start="2"><li>修改后置filter时，网上有些文字使用的是 <code>Mono.defer</code>去做的，这种做法，会从此filter开始，重新执行一遍它后面的其他filter，一般我们会添加一些认证或鉴权的 <code>GlobalFilter</code> ，就需要在这些filter里用<code>ServerWebExchangeUtils.isAlreadyRouted(exchange)</code>  方法去判断是否重复执行，否则可能会执行二次重复操作，所以建议使用<code>fromRunnable</code> 避免这种情况。</li></ol>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;问题&quot;&gt;&lt;a href=&quot;#问题&quot; class=&quot;headerlink&quot; title=&quot;问题&quot;&gt;&lt;/a&gt;问题&lt;/h2&gt;&lt;p&gt;在Spring Cloud项目中，前后端分离目前很常见，在调试时，会遇到两种情况的跨域：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;前端页面通过不同域名或I
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="分布式" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="SpringCloud" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/SpringCloud/"/>
    
    
      <category term="SpringCloud" scheme="http://edisonxu.com/tags/SpringCloud/"/>
    
      <category term="SpringCloud Gateway" scheme="http://edisonxu.com/tags/SpringCloud-Gateway/"/>
    
  </entry>
  
  <entry>
    <title>SpringCloud下skywalking的快速入门</title>
    <link href="http://edisonxu.com/2020/10/13/skywalking-intro.html"/>
    <id>http://edisonxu.com/2020/10/13/skywalking-intro.html</id>
    <published>2020-10-13T11:30:06.000Z</published>
    <updated>2021-07-21T13:31:21.785Z</updated>
    
    <content type="html"><![CDATA[<h2 id="什么是SkyWalking"><a href="#什么是SkyWalking" class="headerlink" title="什么是SkyWalking?"></a>什么是SkyWalking?</h2><p><a href="https://skywalking.apache.org/zh/" target="_blank" rel="noopener">SkyWalking</a>  是观察性分析平台和应用性能管理系统（APM, Application Performance Management)，由个人开发者 <a href="https://github.com/wu-sheng" target="_blank" rel="noopener">吴晟</a> 于2015年开源，2017年加入Apache孵化器。</p><p>它提供分布式跟踪、服务网格遥测分析、度量聚合和可视化的功能，支持Java、.Net、PHP、NodeJs、Golang、LUA语言探针，以及Envoy+Istio构建的Service Mesh，支持<code>OpenTracing</code>规范。</p><h3 id="SkyWalking设计原则"><a href="#SkyWalking设计原则" class="headerlink" title="SkyWalking设计原则"></a>SkyWalking设计原则</h3><ul><li><p>保持可监控性</p></li><li><p>拓扑、指标和跟踪于一体</p></li><li><p>轻量级</p></li><li><p>可插拔</p></li><li><p>便于部署</p></li><li><p>与其他系统的可交互性</p></li></ul><h3 id="SkyWalking架构"><a href="#SkyWalking架构" class="headerlink" title="SkyWalking架构"></a>SkyWalking架构</h3><p>SkyWalking总体的架构示意图如下：</p><p><img src="/images/2020/09/343233.jpg" alt="img"> </p><p>SkyWalking 在逻辑上可以分为四个组件：</p><ul><li><p>Probes </p><p>收集数据并转换为SkyWalking要求的格式，发送给后台。宏观的看，Probe包含三种形式：</p><ul><li><p>语言级的原生Agent</p><p>这种<code>Agent</code>跟随目标系统一起运行，就像其代码的一部分，例如SkyWalking的Java Agent，用<code>-javaagent</code> 命令行参数去操纵（修改和注入）执行时的代码。其他方式的<code>Agent</code>，使用目标语言库所提供的hook或拦截等机制。</p></li><li><p>Service Mesh probe </p><p>Service Mesh probe从<code>sidecar</code>、<code>service mesh</code>中<code>servie</code>的<code>control panel</code>或<code>proxy</code>获取数据。</p></li><li><p>第三方工具库</p><p>直接接收外部数据，用第三方库所提供的工具，将该数据转化为SKyWalking的格式发送到后端</p></li></ul></li><li><p>Platform backend</p><p>SkyWalking的后台，又称为<code>OAP(Observability Analysis Platform)</code>，将数据进行聚合、分析以及UI的页面流程控制</p></li><li><p>Storage </p><p>SkyWalking的数据持久层，目前已支持像<code>ElasticSearch</code>、<code>MySql</code>、<code>TiDB</code>、<code>H2</code>等常见的各种持久化库</p></li><li><p>UI</p><p>用于展示数据</p></li></ul><p>  另外，SkyWalking也提供了<a href="https://github.com/apache/skywalking-cli" target="_blank" rel="noopener">CLI</a> 可以用命令行进行操作和查询。</p><h2 id="SpringCloud中SkyWalking的使用"><a href="#SpringCloud中SkyWalking的使用" class="headerlink" title="SpringCloud中SkyWalking的使用"></a>SpringCloud中SkyWalking的使用</h2><h3 id="1-配置数据库"><a href="#1-配置数据库" class="headerlink" title="1. 配置数据库"></a>1. 配置数据库</h3><p>SkyWalking默认启用了H2的数据库，在生产级，更适合的显然是ElasticSearch（当然，<code>H2</code>下我也遇到了入库时报 <code>Agent</code> 生成的<code>traceId</code> 长度超过200的错误）。</p><p>方便起见，直接用 ES的<code>Docker</code> 镜像启用ES集群，本文选用7.9.2的镜像文件。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$sudo</span> docker pull elasticsearch:7.9.2</span><br><span class="line"><span class="comment"># 此处为方便后续容器使用ES，为其创建一个网络</span></span><br><span class="line"><span class="variable">$sudo</span> docker network create esnw</span><br><span class="line"><span class="comment"># 仅启动一个单节点做演示</span></span><br><span class="line"><span class="variable">$sudo</span> docker run -d --name elasticsearch --net esnw -p 9200:9200 -p 9300:9300 -e <span class="string">"discovery.type=single-node"</span> elasticsearch:7.9.2</span><br></pre></td></tr></table></figure><blockquote><p>注： </p><p>Docker启动的ES，默认<code>cluster_name</code>是 <strong>docker-cluster</strong>, 如有需要，可以提前改好，后续需指定</p></blockquote><h3 id="2-配置并启用OAP后台及UI"><a href="#2-配置并启用OAP后台及UI" class="headerlink" title="2. 配置并启用OAP后台及UI"></a>2. 配置并启用OAP后台及UI</h3><h5 id="2-1-下载包"><a href="#2-1-下载包" class="headerlink" title="2.1 下载包"></a>2.1 下载包</h5><p>SkyWalking在下载页面 <a href="http://skywalking.apache.org/downloads/" target="_blank" rel="noopener">http://skywalking.apache.org/downloads/</a> 提供了最新版本的整合包，包含了<code>Agent</code>、 <code>OAP</code> 和 <code>UI</code> 三个组件，但是，在Dockerhub上是将<code>OAP</code>（<a href="https://hub.docker.com/r/apache/skywalking-oap-server）" target="_blank" rel="noopener">https://hub.docker.com/r/apache/skywalking-oap-server）</a> 和UI（<a href="https://hub.docker.com/r/apache/skywalking-ui）是拆分开来了的。" target="_blank" rel="noopener">https://hub.docker.com/r/apache/skywalking-ui）是拆分开来了的。</a></p><blockquote><p> 注：</p><p>由于ES6和7 API的变动，<code>OAP</code>的分别为6和7单独打了一个包，x.y.z-es6是支持ES6的，x.y.z-es7是支持ES7的，由于我们的数据库选用的是ES7，所以要下的是es7最新的包</p></blockquote><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$sudo</span> docker pull apache/skywalking-oap-server:8.1.0-es7</span><br><span class="line"><span class="variable">$sudo</span> docker pull apache/skywalking-ui:8.1.0</span><br></pre></td></tr></table></figure><h5 id="2-2-配置"><a href="#2-2-配置" class="headerlink" title="2.2 配置"></a>2.2 配置</h5><p><code>OAP</code> 的配置集中在 <em>config/application.yml</em> 中，比较重要的配置有如下几个</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">cluster:</span></span><br><span class="line">  <span class="comment"># 配置SkyWalking的集群方式，默认是单节点，支持zk、k8s、consul、etcd和nacos</span></span><br><span class="line"><span class="attr">  selector:</span> <span class="string">$&#123;SW_CLUSTER:standalone&#125;</span></span><br><span class="line"><span class="attr">core:</span></span><br><span class="line"><span class="attr">  selector:</span> <span class="string">$&#123;SW_CORE:default&#125;</span></span><br><span class="line"><span class="attr">  default:</span></span><br><span class="line">    <span class="comment"># 对外暴露Http接口的地址以及端口12800</span></span><br><span class="line"><span class="attr">    restHost:</span> <span class="string">$&#123;SW_CORE_REST_HOST:0.0.0.0&#125;</span></span><br><span class="line"><span class="attr">    restPort:</span> <span class="string">$&#123;SW_CORE_REST_PORT:12800&#125;</span></span><br><span class="line">    <span class="comment"># 对外暴露gRPC接口的地址及端口11800</span></span><br><span class="line"><span class="attr">    gRPCHost:</span> <span class="string">$&#123;SW_CORE_GRPC_HOST:0.0.0.0&#125;</span></span><br><span class="line"><span class="attr">    gRPCPort:</span> <span class="string">$&#123;SW_CORE_GRPC_PORT:11800&#125;</span></span><br><span class="line"><span class="attr">storage:</span></span><br><span class="line">  <span class="comment"># 默认的数据库是H2</span></span><br><span class="line"><span class="attr">  selector:</span> <span class="string">$&#123;SW_STORAGE:H2&#125;</span></span><br><span class="line"><span class="attr">  elasticsearch7:</span></span><br><span class="line">    <span class="comment"># 对应ES的cluser_name，即前面提到的docker-cluster</span></span><br><span class="line"><span class="attr">    nameSpace:</span> <span class="string">$&#123;SW_NAMESPACE:"docker-cluster"&#125;</span></span><br><span class="line"><span class="attr">    clusterNodes:</span> <span class="string">$&#123;SW_STORAGE_ES_CLUSTER_NODES:localhost:9200&#125;</span></span><br><span class="line"><span class="attr">    protocol:</span> <span class="string">$&#123;SW_STORAGE_ES_HTTP_PROTOCOL:"http"&#125;</span></span><br></pre></td></tr></table></figure><p>此处，我们只需要将<code>SW_STORAGE</code>的默认值从<code>H2</code> 改为<code>elasticsearch7</code> 即可，如果之前改过ES的<code>cluster_name</code>，此处要将<code>SW_NAMESPACE</code>的默认值做对应修改。</p><p>Docker的配置，也可直接通过在启动时指定变量值进行改动。</p><h5 id="2-3-启动"><a href="#2-3-启动" class="headerlink" title="2.3 启动"></a>2.3 启动</h5><p>如果是整合包，则解压后，直接进入bin目录用startup脚本即可同时启动<code>OAP</code>和<code>UI</code>服务。</p><p>Docker启动：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 启动OAP</span></span><br><span class="line"><span class="variable">$sudo</span> docker run --name oap --network esnw --restart always -d -p 11800:11800 -p12800:12800 -e SW_STORAGE=elasticsearch7 -e</span><br><span class="line"><span class="comment"># 启动UI</span></span><br><span class="line"><span class="variable">$docker</span> run --name oap-ui --network esnw --restart always -d -p 8080:8080 -e SW_OAP_ADDRESS=oap:12800 apache/skywalking-ui:8.1.0</span><br></pre></td></tr></table></figure><blockquote><p>注:</p><p>SkyWalking的UI有个坑，不支持反向代理，具体可参见 github issue <a href="https://github.com/apache/skywalking/issues/2675" target="_blank" rel="noopener">#2675</a></p></blockquote><h3 id="3-配置并启用Agent"><a href="#3-配置并启用Agent" class="headerlink" title="3. 配置并启用Agent"></a>3. 配置并启用Agent</h3><p>SkyWalking对于Java项目十分友好，默认提供的<code>Agent</code>就是Java的，使用<code>JavaAgent</code>进行字节码注入实现拦截。SkyWalking是遵循<code>OpenTracing</code> 规范的，下图是<code>OpenTracing</code> 中定义的数据结构。</p><p><img src="/images/2020/09/openTracing_base_structure.e1a47387.png" alt="img"></p><p>其中，</p><ul><li>SpanContext：负责上线文信息保持和传递</li><li>Trace: 一次调用的完整记录<ul><li>Span:  一次调用中的某个节点/步骤，类似于一层堆栈信息，Trace是由多个Span组成，Span本身也是可以嵌套自身的数据结构<ul><li>Tag: 节点/步骤中的关键信息</li><li>Log: 节点/步骤中的日志，例如抛出的异常栈</li></ul></li><li>Baggage: 主要用于运行时跨Span或跨实例的上下文传递</li></ul></li></ul><h5 id="3-1-配置"><a href="#3-1-配置" class="headerlink" title="3.1 配置"></a>3.1 配置</h5><p>从前面下载页下载的整合包里，把整个 <strong><em>agent</em></strong> 目录拷到项目文件里，或者单独放在某个位置。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">activations</span><br><span class="line">bootstrap-plugins</span><br><span class="line"># 配置文件存放目录</span><br><span class="line">config</span><br><span class="line"># agent运行时生成的日志保存在此目录</span><br><span class="line">logs</span><br><span class="line"># 可选用的插件，默认不启用</span><br><span class="line">optional-plugins</span><br><span class="line"># 可选用的reporter插件，默认不启用</span><br><span class="line">optional-reporter-plugins</span><br><span class="line"># 默认启用的插件放于此目录</span><br><span class="line">plugins</span><br><span class="line"># 拦截探针实现</span><br><span class="line">skywalking-agent.jar</span><br></pre></td></tr></table></figure><p>暂时没有需要修改的配置，就不去更改配置文件了。</p><p><strong><em>optional-plugins</em></strong> 里的插件默认是不启用的，如果要启用，需要将其移动到<strong><em>plugins</em></strong>目录。</p><p>在<strong><em>optional-plugins</em></strong>里，有几个可选插件是值得我们注意的：</p><ul><li><p>apm-spring-annotation-plugin-8.1.0.jar</p><p>该插件可追踪Spring上下文中被<code>@Bean</code> , <code>@Service</code> ，<code>@Component</code>和<code>@Repository</code> 注解的方法，该插件一旦启用，会产生大量的span，从而消耗更多的资源，所以默认是把它放到了可选插件中。</p></li><li><p>apm-spring-cloud-gateway-2.1.x-plugin-8.1.0.jar</p><p>该插件将从SpringCloudGateway产生的span与后续相同链路的span合并到一起进行追踪，如果没有这个插件，一个REST请求，如/api/hello，经过SC Gateway到达服务a时，将会在UI上展示两个跟踪记录而非一条（服务a也必须启用skywalking agent）。该插件依赖spring-webflux-5.x-plugin。但该插件应该只对Spring Cloud Gateway启用。</p></li><li><p>apm-trace-ignore-plugin-8.1.0.jar</p><p>该插件可以自定义过滤掉一些路径，不被追踪。使用时，需要把<code>/agent/optional-plugins/apm-trace-ignore-plugin/apm-trace-ignore-plugin.config</code> 拷贝到<code>/agent/config</code>目录，然后修改其内容增加想要过滤掉的路径，例如：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">trace.ignore_path=/your/path/1/**,/your/path/2/**</span><br></pre></td></tr></table></figure></li></ul><ul><li><p>apm-customize-enhance-plugin-8.1.0.jar</p><p>该插件可以通过指定增强文件来实现方法级的精细拦截及span生成的定义，而不用写代码。具体参见：<a href="https://github.com/apache/skywalking/blob/v8.1.0/docs/en/setup/service-agent/java-agent/Customize-enhance-trace.md" target="_blank" rel="noopener">这里</a></p></li></ul><p>为方便测试，我们将apm-spring-annotation-plugin-8.1.0.jar和apm-spring-cloud-gateway-2.1.x-plugin-8.1.0.jar 拷入 <strong><em>plugins</em></strong> 目录启用。</p><h5 id="3-2-启动"><a href="#3-2-启动" class="headerlink" title="3.2 启动"></a>3.2 启动</h5><p>在启动SC项目文件时，增加JVM参数：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-javaagent:&lt;absolute_path&gt;\agent\skywalking-agent.jar -Dskywalking.agent.service_name=&lt;service_name&gt; -Dskywalking.collector.backend_service=&lt;oap_address&gt;:11800</span><br></pre></td></tr></table></figure><p>其中，<code>Java Agent</code>  默认采用的gRPC调用，所以端口默认为11800。</p><p>启动时，<code>Agent</code>会根据jar包所在位置自动寻找config目录下的<code>agent.config</code>文件读取配置。</p><p>至此，SkyWalking的初步整合已经完毕，可以向SpringCloud Gateway发送一个请求到具体的微服务，等待几秒后，可在<code>UI</code> 的<strong>拓扑图</strong>页面中看到整个微服务的拓扑，在<strong>追踪</strong>页面中看到查找到刚才所发请求的整个链路调用过程以及所消耗的时间以及具体的log等。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;什么是SkyWalking&quot;&gt;&lt;a href=&quot;#什么是SkyWalking&quot; class=&quot;headerlink&quot; title=&quot;什么是SkyWalking?&quot;&gt;&lt;/a&gt;什么是SkyWalking?&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://skywalk
      
    
    </summary>
    
    
      <category term="分布式" scheme="http://edisonxu.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="链路跟踪" scheme="http://edisonxu.com/tags/%E9%93%BE%E8%B7%AF%E8%B7%9F%E8%B8%AA/"/>
    
  </entry>
  
  <entry>
    <title>Akka入门系列(八)：akka kafka Consumer</title>
    <link href="http://edisonxu.com/2018/12/04/akka-kafka-consumer.html"/>
    <id>http://edisonxu.com/2018/12/04/akka-kafka-consumer.html</id>
    <published>2018-12-04T06:47:32.000Z</published>
    <updated>2021-07-21T13:31:21.780Z</updated>
    
    <content type="html"><![CDATA[<h1 id="核心API"><a href="#核心API" class="headerlink" title="核心API"></a>核心API</h1><p>在使用Akka kafka consumer前， 先了解下几个核心API：</p><ul><li><code>ConsumerSetting</code> Consumer的配置信息；</li><li><code>ConsumerRecord</code> Kafka消息的封装类，包含消息的K、V，以及该消息所属的topic, partition, offset, timestamp等；</li><li><code>ConsumerMessage</code> 是<code>ConsumerRecord</code>的进一步充血模型，提供了自动commit以及修改offset信息的API；</li><li><code>Subscription</code> 该Consumer的订阅信息，有<code>AutoSubscription</code>和<code>ManualSubscription</code>两个子接口，分别用于自动从Topic读取Partition以及手动绑定Partition；</li></ul><p>Akka Kafka中，Consumer一般是作为流的Source，在<code>akka.kafka.javadsl.Consumer</code>中提供了常用的几种Source。主要包含两大类：</p><pre><code>1. Offset存储及读取机制独立于Kafka以外，需自行实现commit逻辑，命名为plainxxxSource；2. Offset存储及读取机制依赖于Kafka的所提供的API，通过调用Akka已封装的`ConsumerMessage`进行offset的commit，命名为committableXXXSource</code></pre><p>详情可以参见最后的目录。</p><h1 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h1><h2 id="依赖"><a href="#依赖" class="headerlink" title="依赖"></a>依赖</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.typesafe.akka<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-stream-kafka_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><p>所有的Consumer都需要传入配置类<code>ConsumerSetting</code>，需要提供如下信息：</p><ul><li>Kafka消息key和value的反序列化器</li><li>Kafka集群的地址信息</li><li>consumer的GroupId，注意：offset是按组进行commit的</li><li>Kafka Consumer的调优参数</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ConsumerSettings <span class="title">getConsumerSettings</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params">        Deserializer keyDeserializer,</span></span></span><br><span class="line"><span class="function"><span class="params">        Deserializer valDeserializer,</span></span></span><br><span class="line"><span class="function"><span class="params">        Config config,</span></span></span><br><span class="line"><span class="function"><span class="params">        String groupId)</span></span>&#123;</span><br><span class="line">    Deserializer&lt;String&gt; keySerializer = <span class="keyword">new</span> StringDeserializer();</span><br><span class="line">    Deserializer&lt;<span class="keyword">byte</span>[]&gt; valSerializer = <span class="keyword">new</span> ByteArrayDeserializer();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> ConsumerSettings.create(config, keyDeserializer, valDeserializer)</span><br><span class="line">            .withGroupId(groupId) <span class="comment">// if not defined here, config must contains "group.id"</span></span><br><span class="line">            .withProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, <span class="string">"earliest"</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="数据处理服务定义"><a href="#数据处理服务定义" class="headerlink" title="数据处理服务定义"></a>数据处理服务定义</h2><p>我们用相同的一段代码，来代表整个Flow中的数据转换过程。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">DummyBusinessLogic</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> CompletionStage&lt;Integer&gt; <span class="title">work</span><span class="params">(ConsumerRecord&lt;String, <span class="keyword">byte</span>[]&gt; record)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> CompletableFuture.supplyAsync(() -&gt; &#123;</span><br><span class="line">            System.out.println(<span class="string">"Partition["</span>+record.partition()+<span class="string">"] got:"</span>+<span class="keyword">new</span> String(record.value()));</span><br><span class="line">            <span class="keyword">return</span> record.partition();</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="Offset管理独立于Kafka以外"><a href="#Offset管理独立于Kafka以外" class="headerlink" title="Offset管理独立于Kafka以外"></a>Offset管理独立于Kafka以外</h2><p>此类API命名规则都是<code>plainXXXSource</code>，对外都emit出<code>ConsumerRecord</code>，Offset维护在外部的存储里，可以先读取再处理或提供读取的方法给API由其调用获得最新的Offset。Commit也是手动进行，但是可以通过修改<code>auto-commit</code>参数（该值默认是false），由Kafka自行进行Offset的Commit。Kafka的自动Commit是阈值和周期性的Commit，哪个先触发就直接commit，比较适合量大且允许消息重复递交的场景。<br>我们先实现一个简单的外部存储类，用以演示<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// A dummy storage to store offset externally</span></span><br><span class="line"><span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">ExternalOffsetStorage</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> Map&lt;TopicPartition, Long&gt; partitionOffsetMap = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">ExternalOffsetStorage</span><span class="params">(String topic, <span class="keyword">int</span> partitonNum)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">for</span>(<span class="keyword">int</span> i=<span class="number">0</span>;i&lt;partitonNum;i++)&#123;</span><br><span class="line">            partitionOffsetMap.put(<span class="keyword">new</span> TopicPartition(topic, i), <span class="keyword">new</span> Long(<span class="number">0</span>));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// User CompletionStage is to warn that read the offset may cost some time</span></span><br><span class="line">    <span class="comment">/*public CompletionStage&lt;Long&gt; getLatestOffset()&#123;</span></span><br><span class="line"><span class="comment">        return CompletableFuture.completedFuture(offset.get());</span></span><br><span class="line"><span class="comment">    &#125;*/</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Long <span class="title">getLatestOffset</span><span class="params">(TopicPartition partition)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> partitionOffsetMap.get(partition);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> CompletionStage&lt;Done&gt; <span class="title">commitOffset</span><span class="params">(TopicPartition partition)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> CompletableFuture.supplyAsync(() -&gt; &#123;</span><br><span class="line">            partitionOffsetMap.put(partition, getLatestOffset(partition)+<span class="number">1</span>);</span><br><span class="line">            <span class="keyword">return</span> Done.done();</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> CompletionStage&lt;Done&gt; <span class="title">commitOffset</span><span class="params">(<span class="keyword">int</span> partition)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> CompletableFuture.supplyAsync(() -&gt; &#123;</span><br><span class="line">            <span class="keyword">for</span>(TopicPartition p: partitionOffsetMap.keySet())&#123;</span><br><span class="line">                <span class="keyword">if</span>(p.partition() == partition)</span><br><span class="line">                    partitionOffsetMap.put(p, partitionOffsetMap.get(p)+<span class="number">1</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> Done.done();</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Map&lt;TopicPartition, Long&gt; <span class="title">getPartitionOffsetMap</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> partitionOffsetMap;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> CompletionStage&lt;Map&lt;TopicPartition, Object&gt;&gt; getOffsetsOnAssign(Set&lt;TopicPartition&gt; topicPartitions)&#123;</span><br><span class="line">        <span class="keyword">return</span> CompletableFuture.supplyAsync(()-&gt;&#123;</span><br><span class="line">            Map&lt;TopicPartition, Object&gt; result = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line">            topicPartitions.forEach(partition -&gt; result.put(partition, partitionOffsetMap.get(partition)));</span><br><span class="line">            <span class="keyword">return</span> result;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h3 id="不分Partition处理"><a href="#不分Partition处理" class="headerlink" title="不分Partition处理"></a>不分Partition处理</h3><p>不分Partition处理的API，是最简单的<code>Consumer.plainSource</code>,它接受两个参数：</p><ul><li><code>ConsumerSetting</code> 配置参数</li><li><code>Subscription</code> Kafka的partition信息，可以是<code>AutoSubscription</code>或<code>ManualSubscription</code>。<ul><li><code>AutoSubscription</code>用<code>Subscriptions.topics(&quot;topic&quot;)</code>来指定</li><li><code>ManualSubscription</code>则需要显式提供每一个TopicPartition及其对应的offset。如果只有一个partition，可以直接<code>Subscriptions.assignmentWithOffset(new TopicPartition(&quot;topic&quot;, /*partition: */ 0),  currentOffset)</code>。如果是多个partition，则传入一个<code>Map</code>，<code>TopicPartition</code>作为key，Offset的值为value。<br>由于commit的时机和逻辑都是自己提供的，所以比较适合去实现<code>exact-once-delivery</code>。<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Consumer.plainSource(</span><br><span class="line">            consumerSettings,</span><br><span class="line">            Subscriptions.assignmentWithOffset(offsetStorage.getPartitionOffsetMap()))</span><br><span class="line">            <span class="comment">//Subscriptions.topics(topic))</span></span><br><span class="line">            .mapAsync(partitionNum, record -&gt; logic.work(record).thenApply(partition-&gt;offsetStorage.commitOffset(partition)))</span><br><span class="line">            .to(Sink.ignore())</span><br><span class="line">            .run(materializer);</span><br></pre></td></tr></table></figure></li></ul></li></ul><h3 id="分Partition处理"><a href="#分Partition处理" class="headerlink" title="分Partition处理"></a>分Partition处理</h3><p>API中含<code>Partitioned</code>字样的，均是分Partition处理的API，即每一个Partition会对应一个新的子Source。这一类中，是<code>Consumer.plainPartitionedSource</code>和<code>Consumer.plainPartitionedManualOffsetSource</code>。<br>与plainSource不同的点是：</p><ul><li>只接受<code>AutoSubscription</code></li><li>原Source并不直接emit ConsumerRecord，而是派生出三个子Source，从它获得的是一个<code>Pair&lt;TopicPartition, Source&gt;</code>封装类，包含了为每一个partition提供了一个Source对象。</li><li><code>Consumer.plainPartitionedManualOffsetSource</code>在<code>Consumer.plainPartitionedSource</code>基础上，增加了一个函数参数，要求传入一个Function，根据提供的包含TopicPartition信息的Set，返回对应的每个Partition的Offset信息，封装在一个Map里，key是TopicPartition, value是offset值。</li></ul><p>下面例子里，用<code>flatMapMerge</code>将原Source派生的Source合并（即Pair::second返回值）后，交由同一段Flow处理。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span>(!manualAssignOffset)</span><br><span class="line">    Consumer.plainPartitionedSource(</span><br><span class="line">            consumerSettings,</span><br><span class="line">            Subscriptions.topics(topic))</span><br><span class="line">            <span class="comment">// merge ConsumerRecord from different partition Source</span></span><br><span class="line">            .flatMapMerge(partitionNum, Pair::second)</span><br><span class="line">            <span class="comment">// use same logic flow to handle ConsumerRecord</span></span><br><span class="line">            .mapAsync(partitionNum, record -&gt; logic.work(record).thenApply(partition-&gt;offsetStorage.commitOffset(partition)))</span><br><span class="line">            .to(Sink.ignore())</span><br><span class="line">            .run(materializer);</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    Consumer.plainPartitionedManualOffsetSource(</span><br><span class="line">            consumerSettings,</span><br><span class="line">            Subscriptions.topics(topic),</span><br><span class="line">            offsetStorage::getOffsetsOnAssign)</span><br><span class="line">            <span class="comment">//.mapAsync(partitionNum, logic::workWithPartitions)</span></span><br><span class="line">            .flatMapMerge(partitionNum, Pair::second)</span><br><span class="line">            .mapAsync(partitionNum, record -&gt; logic.work(record).thenApply(partition-&gt;offsetStorage.commitOffset(partition)))</span><br><span class="line">            .to(Sink.ignore())</span><br><span class="line">            .run(materializer);</span><br></pre></td></tr></table></figure></p><h2 id="Offset管理依赖Kafka"><a href="#Offset管理依赖Kafka" class="headerlink" title="Offset管理依赖Kafka"></a>Offset管理依赖Kafka</h2><p>Kafka的JAVA API，提供了将offset保存在zookeeper上的功能，在新版的Kafka，更是为了避免zookeeper的性能问题，在其内部创建一个叫<code>__consumer_offsets</code>的topic来存储offset。由<code>offsets.storage</code>参数定义。</p><h3 id="基本使用"><a href="#基本使用" class="headerlink" title="基本使用"></a>基本使用</h3><p>该API能够自由控制何时将<code>offset</code>commit到Kafka去。比较适合用于<code>at-least-once</code>递交的场景，即消息可能会被多次递交，以保证至少会有一次成功，但相应的，如果发生错误，该错误也会发生多次。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// single commit</span></span><br><span class="line">Consumer.committableSource(consumerSettings, Subscriptions.topics(topic))</span><br><span class="line">                <span class="comment">// asynchronously finish logic work and fetch the offset to commit</span></span><br><span class="line">                .mapAsync(<span class="number">1</span>, msg-&gt; logic.work(msg.record()).thenApply(partition -&gt; msg.committableOffset()))</span><br><span class="line">                <span class="comment">// commit offset</span></span><br><span class="line">                .mapAsync(<span class="number">1</span>, offset-&gt;offset.commitJavadsl())</span><br><span class="line">                .to(Sink.ignore())</span><br><span class="line">                .run(materializer);</span><br></pre></td></tr></table></figure></p><p>这里用了<code>mapAsync</code>异步并行来处理消息，并行数位设为1，保证处理消息的顺序（Kafka单个partition是保序的，但是对于同一个topic的多个partition之间是无序的）。<br>运行一下Producer，然后可以用JMX查看该Topic的offset数在上升。</p><p>该API每处理一个消息就会commit一次，这种方式相当慢。推荐的方式是用batch批量commit，用牺牲发生错误时的重复投递来换取性能。</p><h3 id="批量Commit"><a href="#批量Commit" class="headerlink" title="批量Commit"></a>批量Commit</h3><h4 id="自动批处理"><a href="#自动批处理" class="headerlink" title="自动批处理"></a>自动批处理</h4><p>Akka提供了<code>Committer.sink</code>方法来实现自动批量Commit。在使用这个sink前，需要先在配置文件中定义或者代码里直接指定以下两个参数：</p><ul><li><code>max-batch</code> 每次commit的最大消息数，即超过该数即会触发一次commit</li><li><code>max-interval</code> 两次commit之间的最大间隔</li></ul><p>这两个参数调的越大，Kafka对于commit的load越小，消耗时间越少，但相应的，如果发生错误，重新处理的消息数肯定也是对应增加的。调的越小，则commit越频繁，会带来commit性能瓶颈。这个属于Kafka批量commit的老问题了，与Akka本身是无关的，应视不同场景进行相应参数优化。<br>修改application.conf，在<code>akka.kafka.consumer</code>区块中添加<br><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">akka.kafka.consumer</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">...</span></span><br><span class="line">    <span class="comment"># Maximum number of messages in a single commit batch</span></span><br><span class="line">    <span class="string">max-batch</span> <span class="string">=</span> <span class="number">1000</span></span><br><span class="line">    <span class="comment"># Maximum interval between commits in milliseconds</span></span><br><span class="line">    <span class="string">max-interval</span> <span class="string">=</span> <span class="number">10000</span></span><br><span class="line"><span class="string">&#125;</span></span><br></pre></td></tr></table></figure></p><p>批量commit代码如下：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// batch commit</span></span><br><span class="line">Consumer.committableSource(consumerSettings, Subscriptions.topics(topic))</span><br><span class="line">        .mapAsync(<span class="number">1</span>, msg-&gt; logic.work(msg.record())</span><br><span class="line">                .&lt;ConsumerMessage.Committable&gt;thenApply(partition -&gt; msg.committableOffset())</span><br><span class="line">        )</span><br><span class="line">        .to(Committer.sink(CommitterSettings.create(config)))</span><br><span class="line">        .run(materializer);</span><br></pre></td></tr></table></figure></p><p>PS:  <code>.&lt;ConsumerMessage.Committable&gt;</code>这里是做类型强转，将msg.committableOffset返回的<code>CommittableOffset</code>转成其实现接口<code>Committable</code>。</p><h4 id="手动批处理"><a href="#手动批处理" class="headerlink" title="手动批处理"></a>手动批处理</h4><p>另一种方式，是手动的将消息用<code>Akka Stream</code>的<code>batch</code>API聚合后做批量commit。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// manual batch commit</span></span><br><span class="line">Consumer.committableSource(consumerSettings, Subscriptions.topics(topic))</span><br><span class="line">        .mapAsync(<span class="number">1</span>, msg-&gt; logic.work(msg.record())</span><br><span class="line">                .thenApply(partition -&gt; msg.committableOffset())</span><br><span class="line">        )</span><br><span class="line">        .batch(</span><br><span class="line">                <span class="number">20</span>,</span><br><span class="line">                ConsumerMessage::createCommittableOffsetBatch,</span><br><span class="line">                ConsumerMessage.CommittableOffsetBatch::updated</span><br><span class="line">        )</span><br><span class="line">        .mapAsync(<span class="number">3</span>, batch-&gt;batch.commitJavadsl())</span><br><span class="line">        .to(Sink.ignore())</span><br><span class="line">        .run(materializer);</span><br></pre></td></tr></table></figure></p><blockquote><p>注意：<br>用这种方式时，只有当下游consumer处理速度比上游的producer处理速度要慢时，batch才会触发（背压），否则会按正常commit处理。<br>测试时，需要把producer中的控制发送速率的<code>.throttle()</code>注掉，同时调高发送消息数，这样才能看到效果。</p></blockquote><h4 id="按时间聚合批处理"><a href="#按时间聚合批处理" class="headerlink" title="按时间聚合批处理"></a>按时间聚合批处理</h4><p>以上都是适合于消息速率比较高的场景，有些场景下，消息的速率非常低，可能24小时内没有任何消息抵达。此时，需考虑打开kafka的批量commit刷新参数（<code>akka.kafka.consumer.commit-refresh-interval</code>），否则在Kafka的存储中，offset会过期。同时，对于这种速率较低的topic，最好使用按时间进行聚合后进行批处理的<code>groupWithin</code>API。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// time-based aggregation</span></span><br><span class="line">Consumer.committableSource(consumerSettings, Subscriptions.topics(topic))</span><br><span class="line">        .mapAsync(<span class="number">1</span>, msg-&gt; logic.work(msg.record())</span><br><span class="line">                .thenApply(partition -&gt; msg.committableOffset())</span><br><span class="line">        )</span><br><span class="line">        .groupedWithin(<span class="number">5</span>, Duration.ofSeconds(<span class="number">60</span>))</span><br><span class="line">        .map(ConsumerMessage::createCommittableOffsetBatch)</span><br><span class="line">        .mapAsync(<span class="number">3</span>, batch-&gt;batch.commitJavadsl())</span><br><span class="line">        .to(Sink.ignore())</span><br><span class="line">        .run(materializer);</span><br></pre></td></tr></table></figure></p><p><code>groupedWithin</code>API，接受两个参数，第一个是控制多少个消息触发聚合，第二个是控制时间窗口。如果窗口内接受消息数超过第一个参数，则立刻聚合，如果未超过，到窗口时间到期时也会触发。</p><p>测试时，第一个先把Producer的最大消息数改为4，然后不要启用速率控制。看到offset并不是立刻就commit掉。然后把Producer的最大消息数改为10，每1秒发送一个，可以看到当超过5时，立刻触发commit。</p><h3 id="分Partition处理-1"><a href="#分Partition处理-1" class="headerlink" title="分Partition处理"></a>分Partition处理</h3><p>TODO: 流的聚合</p><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>有时，我们想在offset里添加自定义的metadata，此时，可以调用<code>Consumer.commitWithMetadataSource</code>API，用法还是比较简单，具体请参考官方文档。但需要注意的是，由于kafka可以周期性commit（<code>akka.kafka.consumer.commit-refresh-interval</code>参数），第一个offset可能并不会包含新的metadata信息。</p><h2 id="每个Partition一个独立的Source"><a href="#每个Partition一个独立的Source" class="headerlink" title="每个Partition一个独立的Source"></a>每个Partition一个独立的Source</h2><h4 id="at-least-once投递"><a href="#at-least-once投递" class="headerlink" title="at-least-once投递"></a>at-least-once投递</h4><h3 id="at-most-once投递"><a href="#at-most-once投递" class="headerlink" title="at-most-once投递"></a>at-most-once投递</h3><h4 id="单独commit"><a href="#单独commit" class="headerlink" title="单独commit"></a>单独commit</h4><p>Consumer.atMostOnceSource</p><h4 id="批量commit"><a href="#批量commit" class="headerlink" title="批量commit"></a>批量commit</h4><h1 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h1><h4 id="Consumer-API"><a href="#Consumer-API" class="headerlink" title="Consumer API"></a>Consumer API</h4><table><thead><tr><th>API</th><th>使用场景</th><th>参数</th><th>发射类</th></tr></thead><tbody><tr><td>plainSource</td><td>将Offset存到外部，不支持存到Kafka本身（除非开启auto-commit，用kafka自己的自动commit功能）</td><td>ConsumerSettings&lt;K,V&gt; consumerSettings, Subscription subscription</td><td>ConsumerRecord</td></tr><tr><td>plainExternalSource</td><td>将Offset存到外部，可以使用外部<code>KafkaAsyncConsumer</code>的特殊Source，一般用于预先定义好一个Consumer Actor，然后用该API去手动绑定许多topic-partitions</td><td>ActorRef consumer, ManualSubscription subscription</td><td>ConsumerRecord</td></tr><tr><td>plainPartitionedSource</td><td>将Offset存到外部，从topic自动获取partition，每个partition分别对应一个source，放到一个Pair中</td><td>ConsumerSettings&lt;K,V&gt; consumerSettings, AutoSubscription subscription</td><td>Pair[TopicPartition, Source[ConsumerRecord[K, V], NotUsed]</td></tr><tr><td>plainPartitionedManualOffsetSource</td><td>与<code>plainPartitionedSource</code>基本一致，只是允许将partition的offset存储到外部去，使用时调用传入的<code>getOffsetsOnAssign</code>方法去从外部读取offset</td><td>ConsumerSettings&lt;K,V&gt; consumerSettings, AutoSubscription subscription, Function[Set[TopicPartition], CompletionStage[Map[TopicPartition, Long]] getOffsetsOnAssign</td><td>Pair[TopicPartition, Source[ConsumerRecord[K, V], NotUsed]</td></tr><tr><td>plainPartitionedManualOffsetSource</td><td>多了一个<code>onRevoke</code>方法，用于在关闭时去处理(存储)还未commit的offset，以及做一些清扫任务</td><td>ConsumerSettings&lt;K,V&gt; consumerSettings, AutoSubscription subscription, Function[Set[TopicPartition], CompletionStage[Map[TopicPartition, Long]] getOffsetsOnAssign, Consumer[Set[TopicPartition]] onRevoke</td><td>Pair[TopicPartition, Source[ConsumerRecord[K, V], NotUsed]</td></tr><tr><td>committableSource</td><td>提供API将Offset存到kafka内部，使用时自由控制何时commit</td><td>ConsumerSettings&lt;K,V&gt; consumerSettings, Subscription subscription</td><td>CommittableMessage</td></tr><tr><td>committableExternalSource</td><td>与<code>plainExternalSource</code>一样，只是提供了可以commit到Kafka内部的API</td><td>ActorRef consumer, ManualSubscription subscription, String groupId, FiniteDuration commitTimeout</td><td>CommittableMessage</td></tr><tr><td>commitWithMetadataSource</td><td>提供API将Offset存到kafka内部，并可以将额外的信息放入offset的元数据里，比如什么时间或哪个节点commit的等</td><td>ConsumerSettings&lt;K,V&gt; consumerSettings, Subscription subscription, Function[ConsumerRecord[K, V], String] metadataFromRecord</td><td>CommittableMessage</td></tr><tr><td>committablePartitionedSource</td><td>与<code>plainPartitionedSource</code>一样，只是提供了可以commit到Kafka内部的API</td><td>ConsumerSettings&lt;K,V&gt; consumerSettings, AutoSubscription subscription</td><td>Pair[TopicPartition, Source[CommittableMessage[K, V], NotUsed]</td></tr><tr><td>commitWithMetadataPartitionedSource</td><td>与<code>plainPartitionedSource</code>一样，只是提供了可以commit到Kafka内部的API，同时允许添加额外信息到offset的元数据里</td><td>ConsumerSettings&lt;K,V&gt; consumerSettings, AutoSubscription subscription, Function[ConsumerRecord[K, V], String] metadataFromRecord</td><td>Pair[TopicPartition, Source[CommittableMessage[K, V], NotUsed]</td></tr><tr><td>atMostOnceSource</td><td>消息在发给下游逻辑处理前，先自动将offset更新commit掉，以保证至多一次投递</td><td>ConsumerSettings&lt;K,V&gt; consumerSettings, Subscription subscription</td><td>ConsumerRecord</td></tr></tbody></table>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;核心API&quot;&gt;&lt;a href=&quot;#核心API&quot; class=&quot;headerlink&quot; title=&quot;核心API&quot;&gt;&lt;/a&gt;核心API&lt;/h1&gt;&lt;p&gt;在使用Akka kafka consumer前， 先了解下几个核心API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;C
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="分布式" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Akka" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/Akka/"/>
    
    
      <category term="分布式" scheme="http://edisonxu.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="kafka" scheme="http://edisonxu.com/tags/kafka/"/>
    
      <category term="akka" scheme="http://edisonxu.com/tags/akka/"/>
    
      <category term="actor" scheme="http://edisonxu.com/tags/actor/"/>
    
      <category term="并发" scheme="http://edisonxu.com/tags/%E5%B9%B6%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>Akka入门系列(七)：akka kafka Producer</title>
    <link href="http://edisonxu.com/2018/11/30/akka-kafka-producer.html"/>
    <id>http://edisonxu.com/2018/11/30/akka-kafka-producer.html</id>
    <published>2018-11-30T00:52:25.000Z</published>
    <updated>2021-07-21T13:31:21.780Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Akka-Stream"><a href="#Akka-Stream" class="headerlink" title="Akka Stream"></a>Akka Stream</h1><p>在用Akka去对接Kafka之前，有必要先简单了解下<code>Akka Stream</code>，它是基于<a href="http://www.reactive-streams.org/" target="_blank" rel="noopener">Reactive Stream</a>(Akka是其创立成员之一)的。<code>Akka Stream</code>中，将流的拓扑处理逻辑命名为<code>Graph</code>，拓扑中每个操作命名为<code>Operator</code>。<br>它将流式处理抽象为几个核心API：</p><ul><li><code>Source</code> : <strong>有且仅有一个Output</strong>的operator，对应数据的来源，将数据反序列化后发送给下游逻辑。</li><li><code>Sink</code> : <strong>有且仅有一个Input</strong>的operator，对应最终数据的去向，常用于结果存储。</li><li><code>Flow</code> : <strong>有且仅有一个Input和Output</strong>的operator，用于定义数据的处理逻辑。</li><li><code>RunnableGraph</code> : 一个对接了<code>Source</code>和<code>Sink</code>，并且已经准备好<code>run()</code>的<code>Flow</code>。一般用<code>source.to(sink)</code>或<code>source.runWith(sink, materializer)</code>构成一个<code>RunnableGraph</code>，可通过<code>via(flow)</code>添加其他<code>flow</code>。</li><li><code>Materializer</code> : 这个SPI(Service Provider Interface)是根据前面定义的graph进行资源申请，生成相应Actor类，然后进行执行的。<a href="https://www.cnblogs.com/gabry/p/9524201.html" target="_blank" rel="noopener">这篇博文</a>从源码级对它的原理进行了阐述，写的相当透彻，就不再重复了。</li></ul><p><img src="../images/2018/12/compose_shapes.png" alt=""></p><h1 id="Akka-Kafka核心API"><a href="#Akka-Kafka核心API" class="headerlink" title="Akka Kafka核心API"></a>Akka Kafka核心API</h1>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Akka-Stream&quot;&gt;&lt;a href=&quot;#Akka-Stream&quot; class=&quot;headerlink&quot; title=&quot;Akka Stream&quot;&gt;&lt;/a&gt;Akka Stream&lt;/h1&gt;&lt;p&gt;在用Akka去对接Kafka之前，有必要先简单了解下&lt;code&gt;Ak
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="分布式" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Akka" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/Akka/"/>
    
    
      <category term="分布式" scheme="http://edisonxu.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="kafka" scheme="http://edisonxu.com/tags/kafka/"/>
    
      <category term="akka" scheme="http://edisonxu.com/tags/akka/"/>
    
      <category term="actor" scheme="http://edisonxu.com/tags/actor/"/>
    
      <category term="并发" scheme="http://edisonxu.com/tags/%E5%B9%B6%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>Akka入门系列(六)：akka cluster中的路由和负载均衡</title>
    <link href="http://edisonxu.com/2018/11/14/akka-cluster-router.html"/>
    <id>http://edisonxu.com/2018/11/14/akka-cluster-router.html</id>
    <published>2018-11-14T07:44:03.000Z</published>
    <updated>2021-07-21T13:31:21.779Z</updated>
    
    <content type="html"><![CDATA[<p>在使用路由功能之前，我们需要先了解下常规概念：</p><ul><li><code>Router</code> 路由器，消息由外部发送到路由器，再由路由器通过路由算法转发给具体的执行者，相当于消息的中转站。</li><li><code>Routee</code> 路由目标，最早处理消息的地方。</li></ul><p>在Akka中，提供了两种做路由的方式：</p><ul><li>直接使用<code>akka.routing.Router</code>类</li><li>使用内置的Router Actor</li></ul><h2 id="直接使用Router类"><a href="#直接使用Router类" class="headerlink" title="直接使用Router类"></a>直接使用Router类</h2><p>直接使用<code>akka.routing.Router</code>类的原理其实与上一章的最简单的例子是一样的，只不过akka的Router类比我们实现的更复杂、更强大。创建Router类时需提供两个参数：</p><ul><li><p>路由规则<br>akka为Router类提供了以下几种内置的路由算法类：</p><ul><li><code>akka.routing.RoundRobinRoutingLogic</code> </li><li><code>akka.routing.RandomRoutingLogic</code></li><li><code>akka.routing.SmallestMailboxRoutingLogic</code></li><li><code>akka.routing.BroadcastRoutingLogic</code></li><li><code>akka.routing.ScatterGatherFirstCompletedRoutingLogic</code></li><li><code>akka.routing.TailChoppingRoutingLogic</code></li><li><code>akka.routing.ConsistentHashingRoutingLogic</code><br>具体算法介绍请参见文章最后的表格    </li></ul></li><li><p>路由目标的序列<br>该序列支持通过调用<code>router.addRoutee</code>和<code>router.removeRoutee</code>进行动态变化，但需要注意的是，<code>akka.routing.Router</code>类时一个immutable的线程安全类，即不可改变，这里的改变其实是将原来的router内的的routee队列增加/去掉指定routee后copy一份生成一个新的Router</p><figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">removeRoutee</span></span>(routee: <span class="type">Routee</span>): <span class="type">Router</span> = copy(routees = routees.filterNot(_ == routee))</span><br></pre></td></tr></table></figure></li></ul><h4 id="依赖"><a href="#依赖" class="headerlink" title="依赖"></a>依赖</h4><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.typesafe.akka<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-cluster-tools_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h4 id="配置文件application-conf"><a href="#配置文件application-conf" class="headerlink" title="配置文件application.conf"></a>配置文件application.conf</h4><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">akka</span> <span class="string">&#123;</span></span><br><span class="line">  <span class="string">actor</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">provider</span> <span class="string">=</span> <span class="string">"cluster"</span></span><br><span class="line">  <span class="string">&#125;</span></span><br><span class="line">  <span class="string">remote</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">netty.tcp</span> <span class="string">&#123;</span></span><br><span class="line">      <span class="string">hostname</span> <span class="string">=</span> <span class="string">"127.0.0.1"</span></span><br><span class="line">      <span class="string">port</span> <span class="string">=</span> <span class="number">0</span></span><br><span class="line">    <span class="string">&#125;</span></span><br><span class="line">    <span class="string">artery</span> <span class="string">&#123;</span></span><br><span class="line">      <span class="string">enabled</span> <span class="string">=</span> <span class="string">off</span></span><br><span class="line">      <span class="string">canonical.hostname</span> <span class="string">=</span> <span class="string">"127.0.0.1"</span></span><br><span class="line">      <span class="string">canonical.port</span> <span class="string">=</span> <span class="number">0</span></span><br><span class="line">    <span class="string">&#125;</span></span><br><span class="line">  <span class="string">&#125;</span></span><br><span class="line"></span><br><span class="line">  <span class="string">cluster</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">seed-nodes</span> <span class="string">=</span> <span class="string">[</span></span><br><span class="line">      <span class="string">"akka.tcp://ClusterSystem@127.0.0.1:2551"</span></span><br><span class="line">    <span class="string">]</span></span><br><span class="line">  <span class="string">&#125;</span></span><br><span class="line"><span class="string">&#125;</span></span><br></pre></td></tr></table></figure><h4 id="实际做事的SlaveActor"><a href="#实际做事的SlaveActor" class="headerlink" title="实际做事的SlaveActor"></a>实际做事的SlaveActor</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SlaveActor</span> <span class="keyword">extends</span> <span class="title">AbstractActor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    LoggingAdapter log = Logging.getLogger(getContext().system(), <span class="keyword">this</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Receive <span class="title">createReceive</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> receiveBuilder()</span><br><span class="line">                .match(String.class, word-&gt; log.info(<span class="string">"Node &#123;&#125; receives: &#123;&#125;"</span>, getSelf().path().toSerializationFormat(), word))</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        Config config =</span><br><span class="line">                ConfigFactory.parseString(<span class="string">"akka.cluster.roles = [slave]"</span>)</span><br><span class="line">                        .withFallback(ConfigFactory.load());</span><br><span class="line"></span><br><span class="line">        ActorSystem system = ActorSystem.create(<span class="string">"ClusterSystem"</span>, config);</span><br><span class="line">        system.actorOf(Props.create(SlaveActor.class), <span class="string">"slaveActor"</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="包含路由的MasterActor"><a href="#包含路由的MasterActor" class="headerlink" title="包含路由的MasterActor"></a>包含路由的MasterActor</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MasterActor</span> <span class="keyword">extends</span> <span class="title">AbstractActor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    LoggingAdapter log = Logging.getLogger(getContext().system(), <span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Router router = <span class="keyword">new</span> Router(<span class="keyword">new</span> RoundRobinRoutingLogic(), <span class="keyword">new</span> ArrayList&lt;&gt;());</span><br><span class="line">    <span class="keyword">private</span> Cluster cluster = Cluster.get(getContext().system());</span><br><span class="line">    <span class="keyword">boolean</span> isReady = <span class="keyword">false</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> String SLAVE_PATH = <span class="string">"/user/slaveActor"</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">preStart</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        cluster.subscribe(self(), ClusterEvent.MemberEvent.class, ClusterEvent.ReachabilityEvent.class);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Receive <span class="title">createReceive</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> receiveBuilder()</span><br><span class="line">                .match(String.class, msg-&gt;&#123;</span><br><span class="line">                    log.info(<span class="string">"Master got: &#123;&#125;"</span>, msg);</span><br><span class="line">                    <span class="keyword">if</span>(!isReady)</span><br><span class="line">                        log.warning(<span class="string">"Is not ready yet!"</span>);</span><br><span class="line">                    <span class="keyword">else</span> &#123;</span><br><span class="line">                        log.info(<span class="string">"Routee size: &#123;&#125;"</span>, router.routees().length());</span><br><span class="line">                        router.route(msg, getSender());</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;)</span><br><span class="line">                .match(ClusterEvent.MemberUp.class, mUp-&gt;&#123;</span><br><span class="line">                    <span class="keyword">if</span>(mUp.member().hasRole(<span class="string">"slave"</span>)) &#123;</span><br><span class="line">                        Address address = mUp.member().address();</span><br><span class="line">                        String path = address + SLAVE_PATH;</span><br><span class="line">                        ActorSelection selection = getContext().actorSelection(path);</span><br><span class="line">                        router = router.addRoutee(selection);</span><br><span class="line">                        isReady=<span class="keyword">true</span>;</span><br><span class="line">                        log.info(<span class="string">"New routee is added!"</span>);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;)</span><br><span class="line">                .match(ClusterEvent.MemberRemoved.class, mRemoved-&gt;&#123;</span><br><span class="line">                    router = router.removeRoutee(getContext().actorSelection(mRemoved.member().address()+SLAVE_PATH));</span><br><span class="line">                    log.info(<span class="string">"Routee is removed"</span>);</span><br><span class="line">                &#125;)</span><br><span class="line">                .match(ClusterEvent.UnreachableMember.class, mRemoved-&gt; &#123;</span><br><span class="line">                    router = router.removeRoutee(getContext().actorSelection(mRemoved.member().address() + SLAVE_PATH));</span><br><span class="line">                    log.info(<span class="string">"Routee is removed"</span>);</span><br><span class="line">                &#125;)</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">int</span> port = <span class="number">2551</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Override the configuration of the port</span></span><br><span class="line">        Config config =</span><br><span class="line">                ConfigFactory.parseString(</span><br><span class="line">                        <span class="string">"akka.remote.netty.tcp.port="</span> + port + <span class="string">"\n"</span> +</span><br><span class="line">                                <span class="string">"akka.remote.artery.canonical.port="</span> + port)</span><br><span class="line">                        .withFallback(</span><br><span class="line">                                ConfigFactory.parseString(<span class="string">"akka.cluster.roles = [master]"</span>))</span><br><span class="line">                        .withFallback(ConfigFactory.load());</span><br><span class="line"></span><br><span class="line">        ActorSystem system = ActorSystem.create(<span class="string">"ClusterSystem"</span>, config);</span><br><span class="line">        ClusterHttpManagement.get(system);</span><br><span class="line">        AkkaManagement.get(system).start();</span><br><span class="line">        system.actorOf(Props.create(MasterActor.class), <span class="string">"masterActor"</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里将MasterActor监听了集群的MemberUp事件，通过判断事件中包含的role判断是否是SlaveActor加入集群。如果是，则将该SlaveActor加到<code>Router</code>中。同时，如果SlaveActor退出或变成Unreachable状态，则从<code>Router</code>中删除。</p><h4 id="向MasterActor请求的客户端"><a href="#向MasterActor请求的客户端" class="headerlink" title="向MasterActor请求的客户端"></a>向MasterActor请求的客户端</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Client</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">( String[] args )</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        Config config = ConfigFactory.load();</span><br><span class="line">        ActorSystem system = ActorSystem.create(<span class="string">"ClusterSystem"</span>, config);</span><br><span class="line">        ActorSelection toFind = system.actorSelection(<span class="string">"akka.tcp://ClusterSystem@127.0.0.1:2551/user/masterActor"</span>);</span><br><span class="line">        <span class="keyword">int</span> counter = <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">while</span>(<span class="keyword">true</span>)&#123;</span><br><span class="line">            toFind.tell(<span class="string">"hello "</span>+counter++, ActorRef.noSender());</span><br><span class="line">            System.out.println(<span class="string">"Finish telling"</span>);</span><br><span class="line">            Thread.sleep(<span class="number">2000</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>分别启动四个窗口: 一个masterActor节点，两个slaveActor节点，一个Client，可以看到两个slaveActor轮流打印Client传递进去的消息。这时，把其中一个slaveActor关闭，可以看到Client发送的所有消息将被剩下那个slaveActor打印出来。</p><h2 id="使用Router-Actor"><a href="#使用Router-Actor" class="headerlink" title="使用Router Actor"></a>使用Router Actor</h2><p>除了我们自己在Actor里调用<code>akka.routing.Router</code>类外，Akka还提供了根据配置直接生成一个内置的RouterActor。路由逻辑在remoting和cluster两个模块中都有，如果要启用remoting中的路由，则需要引入remoting的依赖，在cluster环境下并不推荐直接去用remoting中的路由，而是用cluster模块中的cluster aware router。</p><p>RouterActor有两种类型：</p><ul><li><strong>Pool</strong><br><strong><code>Router</code>自动创建<code>Routee</code>作为自己的子Actor，然后部署到远程节点上。</strong>当<code>Routee</code>被终止时，会自动从<code>Router</code>的路由表中删除，<strong>除非使用动态路由（指定resizer），否则<code>Router</code>不会重新创建新的<code>Routee</code></strong>，当所有的<code>Routee</code>都停止时，<code>Router</code>也自动停止。</li><li><strong>Group</strong><br><strong>Routee actor是在Router actor以外单独创建好了,<code>Router</code>用<code>ActoSelection</code>向指定的Actor Path发送消息</strong>，但默认并不监控<code>Routee</code>。</li></ul><p>Router actor可以通过程序配置或文件配置。如果是通过文件配置时，必须要在代码中使用<code>FromConfig</code>或<code>RemoteRouterConfig</code>(将Routee部署到远程节点去)去显式的读取相关配置，否则即便在配置文件中定义了路由相关配置，akka也不会去使用。<br>Router actor在转发消息时不会更改消息的sender，而routee actor在回复消息时，消息直接返回到原始的发送者，不再经过router actor。</p><p>无论哪种类型，有一块是相同配置：<br><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">cluster</span> <span class="string">&#123;</span></span><br><span class="line">  <span class="string">enabled</span> <span class="string">=</span> <span class="string">on</span></span><br><span class="line">  <span class="string">allow-local-routees</span> <span class="string">=</span> <span class="string">off</span></span><br><span class="line">  <span class="string">use-roles</span> <span class="string">=</span> <span class="string">[slave]</span></span><br><span class="line"><span class="string">&#125;</span></span><br></pre></td></tr></table></figure></p><p><code>enabled 是否启用cluster aware router</code><br><code>allow-local-routees 能否在本地，即router所在的节点创建和查找routee</code><br><code>use-roles 使用指定的角色来缩小routee的查找范围，如果routee的配置与这里的不同，则router是找不到该routee的。</code></p><h3 id="Pool"><a href="#Pool" class="headerlink" title="Pool"></a>Pool</h3><p>我们在上面例子的基础上，把自己new的Router换成akka内置的RouterActor。改动主要有以下几个：</p><ul><li>在配置文件中指定路由相关信息</li><li>在<code>MasterActor</code>中，读取路由配置，创建router及相关的routees</li></ul><ol><li>配置文件中actor部分增加：<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">actor</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">provider</span> <span class="string">=</span> <span class="string">"cluster"</span></span><br><span class="line">    <span class="string">deployment</span> <span class="string">&#123;</span></span><br><span class="line">      <span class="string">/masterActor/poolRouter</span> <span class="string">&#123;</span></span><br><span class="line">        <span class="string">router</span> <span class="string">=</span> <span class="string">round-robin-pool</span></span><br><span class="line">        <span class="string">nr-of-instance</span> <span class="string">=</span> <span class="number">5</span></span><br><span class="line">        <span class="string">cluster</span> <span class="string">&#123;</span></span><br><span class="line">          <span class="string">enabled</span> <span class="string">=</span> <span class="string">on</span></span><br><span class="line">          <span class="string">allow-local-routees</span> <span class="string">=</span> <span class="string">on</span></span><br><span class="line">          <span class="string">use-roles</span> <span class="string">=</span> <span class="string">[master]</span></span><br><span class="line">        <span class="string">&#125;</span></span><br><span class="line">      <span class="string">&#125;</span></span><br><span class="line">      <span class="string">default</span> <span class="string">&#123;</span></span><br><span class="line">        <span class="string">cluster</span> <span class="string">&#123;</span></span><br><span class="line">          <span class="string">max-nr-of-instances-per-node</span> <span class="string">=</span> <span class="number">5</span></span><br><span class="line">        <span class="string">&#125;</span></span><br><span class="line">      <span class="string">&#125;</span></span><br><span class="line">    <span class="string">&#125;</span></span><br><span class="line">  <span class="string">&#125;</span></span><br></pre></td></tr></table></figure></li></ol><p>由于我们的<code>Router</code>是在masterActor下创建的RouterActor，取名为<code>poolRouter</code>，所以其路径显然是<code>akka.tcp://ClusterSystem@127.0.0.1:2551/user/masterActor/poolRouter</code>，masterActor启动时读取的是这个配置文件，所以deployment部分对应的就是masterActor及其子Actor，所以这里只需要填入相对路径就好了。注意，由于Routee是由masterActor创建出来的，所以<code>use-role</code>必须是与masterActor保持一致，否则会找不到<code>Routee</code>!<br><code>- router 指定预设的路由器</code><br><code>- nr-of-instance routee的个数</code></p><blockquote><p>注意,有两个参数非常关键：</p><ul><li><code>actor.deployment.default.cluster.max-nr-of-instances-per-node</code> 它是配置Router在每个节点上部署的最大Actor数，默认是1。虽然上面我们指定了routee数目为5，但是如果只起一个节点，你会发现永远是<br>1个routee在打印结果。</li><li><code>max-total-nr-of-instances</code> 定义router所能创建的routee的总数，默认是10000。通常来说足够用了。</li></ul></blockquote><ol start="2"><li><p>修改<code>MasterActor</code>。注释掉的部分是直接使用代码而不用配置文件手动创建<code>Router</code>的，有兴趣的可以自己试下。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MasterActor</span> <span class="keyword">extends</span> <span class="title">AbstractActor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    LoggingAdapter log = Logging.getLogger(getContext().system(), <span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> ActorRef router;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">preStart</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        router = getContext().actorOf(FromConfig.getInstance().props(Props.create(SlaveActor.class)), <span class="string">"poolRouter"</span>);</span><br><span class="line">        <span class="comment">/*int totalInstances = 1000;</span></span><br><span class="line"><span class="comment">        int maxInstancePerNode = 5, routeeNumbers=5;</span></span><br><span class="line"><span class="comment">        boolean allowLocalRoutees = true;</span></span><br><span class="line"><span class="comment">        String role = "master";</span></span><br><span class="line"><span class="comment">        ClusterRouterPoolSettings settings = new ClusterRouterPoolSettings(totalInstances, maxInstancePerNode, allowLocalRoutees, role);</span></span><br><span class="line"><span class="comment">        ClusterRouterPool routerPool = new ClusterRouterPool(new RoundRobinPool(routeeNumbers), settings);</span></span><br><span class="line"><span class="comment">        router = getContext().actorOf(routerPool.props(Props.create(SlaveActor.class)), "poolRouter");*/</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Receive <span class="title">createReceive</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> receiveBuilder()</span><br><span class="line">                .match(String.class, msg-&gt;&#123;</span><br><span class="line">                    log.info(<span class="string">"Master got: &#123;&#125;"</span>, msg);</span><br><span class="line">                    router.tell(msg, getSender());</span><br><span class="line">                &#125;)</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>运行<br>其他不变，这次只需要启动<code>Client</code>和<code>MasterActor</code>，<code>SlaveActor</code>在<code>MasterActor</code>中会自动创建出来。看到日志</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">[INFO] [11/16/2018 14:19:58.361] [ClusterSystem-akka.actor.default-dispatcher-2] [akka://ClusterSystem/user/masterActor] Master got: hello</span><br><span class="line">[INFO] [11/16/2018 14:19:58.361] [ClusterSystem-akka.actor.default-dispatcher-2] [akka://ClusterSystem/user/masterActor/poolRouter/c1] Node akka://ClusterSystem/user/masterActor/poolRouter/c1#-1154482163 receives: hello</span><br><span class="line">[INFO] [11/16/2018 14:20:00.362] [ClusterSystem-akka.actor.default-dispatcher-16] [akka://ClusterSystem/user/masterActor] Master got: hello</span><br><span class="line">[INFO] [11/16/2018 14:20:00.362] [ClusterSystem-akka.actor.default-dispatcher-16] [akka://ClusterSystem/user/masterActor/poolRouter/c2] Node akka://ClusterSystem/user/masterActor/poolRouter/c2#-50692619 receives: hello</span><br><span class="line">[INFO] [11/16/2018 14:20:02.365] [ClusterSystem-akka.actor.default-dispatcher-18] [akka://ClusterSystem/user/masterActor] Master got: hello</span><br><span class="line">[INFO] [11/16/2018 14:20:02.365] [ClusterSystem-akka.actor.default-dispatcher-18] [akka://ClusterSystem/user/masterActor/poolRouter/c3] Node akka://ClusterSystem/user/masterActor/poolRouter/c3#1415650532 receives: hello</span><br><span class="line">[INFO] [11/16/2018 14:20:04.366] [ClusterSystem-akka.actor.default-dispatcher-3] [akka://ClusterSystem/user/masterActor] Master got: hello</span><br><span class="line">[INFO] [11/16/2018 14:20:04.366] [ClusterSystem-akka.actor.default-dispatcher-3] [akka://ClusterSystem/user/masterActor/poolRouter/c4] Node akka://ClusterSystem/user/masterActor/poolRouter/c4#1345851811 receives: hello</span><br><span class="line">[INFO] [11/16/2018 14:20:06.368] [ClusterSystem-akka.actor.default-dispatcher-20] [akka://ClusterSystem/user/masterActor] Master got: hello</span><br><span class="line">[INFO] [11/16/2018 14:20:06.368] [ClusterSystem-akka.actor.default-dispatcher-20] [akka://ClusterSystem/user/masterActor/poolRouter/c5] Node akka://ClusterSystem/user/masterActor/poolRouter/c5#-1384624865 receives: hello</span><br></pre></td></tr></table></figure></li></ol><p>从c1到c5轮流打印，round-robin负载均衡起作用了。</p><h3 id="Group"><a href="#Group" class="headerlink" title="Group"></a>Group</h3><p>这种方式下，<code>Routee</code>是在<code>Router</code>外被创建的，一般要求尽量在<code>Router</code>启动前启动好<code>Routee</code>，因为<code>Router</code>在启动过程中会尝试去联络<code>Routee</code>。使用时与<code>Pool</code>型的很像，区别是</p><ul><li>需要指定<code>routees.path</code> (remote方式下支持完整协议路径，比如<code>akka.tcp://ClusterSystem:2551/user/testActor</code>，<strong>但是Cluster模式下不支持，只支持相对路径</strong>)</li><li>不需要指定也没有<code>nr-of-instance</code>参数</li></ul><blockquote><p>GroupActor是根据<code>routees.path</code>所配置的相对路径，去当前cluster的每一个节点上用<code>ActorSelection</code>去查找指定role的Routee（所以use-roles中的配置一定要和slave启动时的role一致），然后直接tell消息过去。由于整个过程是异步的，就意味着GroupActor的消息发送其实根本不关心节点上对应的Routee是否包含Routee或者是否正常启动，只是简单的根据配置去转发而已。<br>不去检测是否包含Routee，是因为Akka是Peer-to-Peer的设计，天生就要求所有节点对等，在这个约定下，它会认为cluster中所有节点的代码相同，一定会包含Routee。<br>不去检测是否正常启动，这个则是由于整个通讯都是异步的。<br>但我个人认为这里还是使用熔断机制来加强的，使用起来会更加方便。</p></blockquote><ol><li>修改配置文件<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">actor</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">provider</span> <span class="string">=</span> <span class="string">"cluster"</span></span><br><span class="line">    <span class="string">deployment</span> <span class="string">&#123;</span></span><br><span class="line">      <span class="string">/masterActor/groupRouter</span> <span class="string">&#123;</span></span><br><span class="line">        <span class="string">router</span> <span class="string">=</span> <span class="string">round-robin-group</span></span><br><span class="line">        <span class="string">cluster</span> <span class="string">&#123;</span></span><br><span class="line">          <span class="string">enabled</span> <span class="string">=</span> <span class="string">on</span></span><br><span class="line">          <span class="string">allow-local-routees</span> <span class="string">=</span> <span class="string">on</span></span><br><span class="line">          <span class="string">use-roles</span> <span class="string">=</span> <span class="string">[slave]</span></span><br><span class="line">        <span class="string">&#125;</span></span><br><span class="line">      <span class="string">&#125;</span></span><br><span class="line">    <span class="string">&#125;</span></span><br><span class="line"><span class="string">&#125;</span></span><br></pre></td></tr></table></figure></li></ol><p>use-roles中role加不加引号都可以。</p><ol start="2"><li><p>修改<code>MasterActor</code>中Router的名字，与配置文件中保持一致。注释掉的部分是直接使用代码而不用配置文件手动创建Router的，有兴趣的可以自己试下。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">preStart</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    router = getContext().actorOf(FromConfig.getInstance().props(Props.create(SlaveActor.class)), <span class="string">"groupRouter"</span>);</span><br><span class="line">    <span class="comment">/*List&lt;String&gt; routeesPaths = Arrays.asList("akka/user/slaveActor");</span></span><br><span class="line"><span class="comment">    router = getContext().actorOf(new RoundRobinGroup(routeesPaths).props(), "groupRouter");*/</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>运行<br>分别在几个不同窗口启动<code>MasterActor</code>、多个<code>SlaveActor</code>后，检查集群是否稳定后，即所有节点均是UP，如果启用了<code>akka-management-cluster-http</code>，向监控地址发送查询请求，如<br>127.0.0.1:8558/cluster/members</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"selfNode"</span>: <span class="string">"akka.tcp://ClusterSystem@127.0.0.1:2551"</span>,</span><br><span class="line">    <span class="attr">"oldestPerRole"</span>: &#123;</span><br><span class="line">        <span class="attr">"master"</span>: <span class="string">"akka.tcp://ClusterSystem@127.0.0.1:2551"</span>,</span><br><span class="line">        <span class="attr">"dc-default"</span>: <span class="string">"akka.tcp://ClusterSystem@127.0.0.1:2551"</span>,</span><br><span class="line">        <span class="attr">"slave"</span>: <span class="string">"akka.tcp://ClusterSystem@127.0.0.1:4914"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">"leader"</span>: <span class="string">"akka.tcp://ClusterSystem@127.0.0.1:2551"</span>,</span><br><span class="line">    <span class="attr">"oldest"</span>: <span class="string">"akka.tcp://ClusterSystem@127.0.0.1:2551"</span>,</span><br><span class="line">    <span class="attr">"unreachable"</span>: [],</span><br><span class="line">    <span class="attr">"members"</span>: [</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="attr">"node"</span>: <span class="string">"akka.tcp://ClusterSystem@127.0.0.1:2551"</span>,</span><br><span class="line">            <span class="attr">"nodeUid"</span>: <span class="string">"-1141014070"</span>,</span><br><span class="line">            <span class="attr">"status"</span>: <span class="string">"Up"</span>,</span><br><span class="line">            <span class="attr">"roles"</span>: [</span><br><span class="line">                <span class="string">"master"</span>,</span><br><span class="line">                <span class="string">"dc-default"</span></span><br><span class="line">            ]</span><br><span class="line">        &#125;,</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="attr">"node"</span>: <span class="string">"akka.tcp://ClusterSystem@127.0.0.1:4914"</span>,</span><br><span class="line">            <span class="attr">"nodeUid"</span>: <span class="string">"344021242"</span>,</span><br><span class="line">            <span class="attr">"status"</span>: <span class="string">"Up"</span>,</span><br><span class="line">            <span class="attr">"roles"</span>: [</span><br><span class="line">                <span class="string">"slave"</span>,</span><br><span class="line">                <span class="string">"dc-default"</span></span><br><span class="line">            ]</span><br><span class="line">        &#125;,</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="attr">"node"</span>: <span class="string">"akka.tcp://ClusterSystem@127.0.0.1:4936"</span>,</span><br><span class="line">            <span class="attr">"nodeUid"</span>: <span class="string">"678163307"</span>,</span><br><span class="line">            <span class="attr">"status"</span>: <span class="string">"Up"</span>,</span><br><span class="line">            <span class="attr">"roles"</span>: [</span><br><span class="line">                <span class="string">"slave"</span>,</span><br><span class="line">                <span class="string">"dc-default"</span></span><br><span class="line">            ]</span><br><span class="line">        &#125;,</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="attr">"node"</span>: <span class="string">"akka.tcp://ClusterSystem@127.0.0.1:4957"</span>,</span><br><span class="line">            <span class="attr">"nodeUid"</span>: <span class="string">"-573369962"</span>,</span><br><span class="line">            <span class="attr">"status"</span>: <span class="string">"Up"</span>,</span><br><span class="line">            <span class="attr">"roles"</span>: [</span><br><span class="line">                <span class="string">"slave"</span>,</span><br><span class="line">                <span class="string">"dc-default"</span></span><br><span class="line">            ]</span><br><span class="line">        &#125;</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ol><p>然后，启动Client向masterActor发送消息，可以看到均匀的打印出接受的日志，round-robin负载均衡起作用了。再多起几个SlaveActor，会将消息转发到新的actor中去，这就是<strong>Group</strong>比<strong>Pool</strong>方式好的地方，可以动态变化。</p><p>此时，你可以尝试修改下配置，将slaveActor变成和masterActor一样的role，再运行后，你会发现有消息丢失，以及转发失败的日志出来。</p><blockquote><p>这是因为在上面所有的例子中，为了方便理解，都是使用一个master+若干slave的方式来演示。<br>然而Akka的设计是Peer-to-Peer的，即所有节点对等，那么，RouterActor就会理所应当地认为在相同role的节点上都存在Routee，由于并没有去检查Routee是否能工作，直接进行了消息转发，而按照上面的写法masterAcotr所在的节点上压根就没起过slaveActor，所以就造成了消息丢失。<br>将配置中<code>allow-local-routees</code>改为<code>off</code>，这时它就不会把masterActor所在节点加到负载列表中去了。但同样的，你可以去起一个空的ActorSystem，看看有什么后果。</p></blockquote><h2 id="附录："><a href="#附录：" class="headerlink" title="附录："></a>附录：</h2><p>Akka提供的路由算法：</p><table><thead><tr><th>算法</th><th>说明</th><th>配置</th><th>算法类</th></tr></thead><tbody><tr><td>RoundRobin</td><td>轮询的给路由列表中每个Routee发送消息</td><td>round-robin-pool 或 round-robin-group</td><td>akka.routing.RoundRobin</td></tr><tr><td>Random</td><td>从路由列表中随机抽取一个Routee发送消息</td><td>random-pool 或 random-group</td><td>akka.routing.Random</td></tr><tr><td>SmallestMailbox</td><td>优先选取路由表中mailbox内消息数最少的Routee发送消息</td><td>smallest-mailbox-pool</td><td>akka.routing.SmallestMailbox</td></tr><tr><td>Broadcast</td><td>以广播的形式将消息同时转发给所有的Routee</td><td>broadcast-pool 或 broadcast-group</td><td>akka.routing.Broadcast</td></tr><tr><td>ScatterGatherFirstCompleted</td><td>将消息发送给所有的Routee，并等待第一个返回的结果，将该结果返回给发送者，其他结果被忽略掉</td><td>scatter-gather-pool 或 scatter-gather-group</td><td>akka.routing.ScatterGatherFirstCompleted</td></tr><tr><td>TailChopping</td><td>先随机选一个Routee发送消息，等待一个短时间的延迟后，再随机选一个Routee发送消息，等待第一个返回的结果并将该结果发送回发送者，其他结果被忽略掉</td><td>tail-chopping-pool 或 tail-chopping-group</td><td>akka.routing.TailChopping</td></tr><tr><td>ConsistentHashing</td><td>使用<a href="http://en.wikipedia.org/wiki/Consistent_hashing" target="_blank" rel="noopener">一致性Hash算法</a>选取Routee转发消息</td><td>consistent-hashing-pool 或 consistent-hashing-group</td><td>akka.routing.ConsistentHashing</td></tr><tr><td>Balancing</td><td>所有的Routee共享同一个mailbox，它会将繁忙的Routee中的任务重新分配给空闲的Routee，不支持group和广播</td><td>balancing-pool</td><td>akka.routing.Balancing</td></tr></tbody></table><p>本章代码地址：<a href="https://github.com/EdisonXu/akka-start-demo/tree/master/cluster" target="_blank" rel="noopener">https://github.com/EdisonXu/akka-start-demo/tree/master/cluster</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;在使用路由功能之前，我们需要先了解下常规概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Router&lt;/code&gt; 路由器，消息由外部发送到路由器，再由路由器通过路由算法转发给具体的执行者，相当于消息的中转站。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Routee&lt;/code&gt; 路由目
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="分布式" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Akka" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/Akka/"/>
    
    
      <category term="分布式" scheme="http://edisonxu.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="akka" scheme="http://edisonxu.com/tags/akka/"/>
    
      <category term="actor" scheme="http://edisonxu.com/tags/actor/"/>
    
      <category term="并发" scheme="http://edisonxu.com/tags/%E5%B9%B6%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>Akka入门系列(五)：akka cluster的基本使用</title>
    <link href="http://edisonxu.com/2018/11/13/akka-cluster-usage.html"/>
    <id>http://edisonxu.com/2018/11/13/akka-cluster-usage.html</id>
    <published>2018-11-13T00:52:25.000Z</published>
    <updated>2021-07-21T13:31:21.779Z</updated>
    
    <content type="html"><![CDATA[<p>前面一个章节<a href="http://edisonxu.com/2018/11/07/akka-cluster.html">akka cluster管理</a>介绍了<code>Akka Cluster</code>的底层原理，这一章就来看看如何使用。</p><h2 id="集群后台接入"><a href="#集群后台接入" class="headerlink" title="集群后台接入"></a>集群后台接入</h2><h3 id="对外"><a href="#对外" class="headerlink" title="对外"></a>对外</h3><p>我们知道，目前集群后台的使用方式主要有以下几种：</p><ul><li>后端直接监听指定协议的网络端口，接受外部请求，处理后按指定协议打包后返回响应。常规用法比如实现RESTful的SpringMVC，使用ProtoclBuffer、thrift等做压缩协议由Netty监听等等。</li><li>由集群提供客户端API，通过客户端向集群提交请求，可同步/异步的获得结果。</li><li>消息队列，通过异步的将消息发送到消息队列，集群监听消息队列获取消息后进行处理，最终将结果反馈到其他消息队列。</li><li>监听流，比如对目录下文件的监听处理，或基于流式消息队列的监听处理等。</li></ul><p><code>Akka</code>提供了以下几个组件以满足这几种不同的调用方式：</p><ul><li><a href="https://doc.akka.io/docs/akka-http/current/?language=java" target="_blank" rel="noopener">Akka Http</a>监听HTTP端口对外响应。 <strong>注意，这不是一个我们常见类似于SpringMVC的web服务框架，更多的是一个类似于HttpClient一样进行HTTP通信的工具集，但是是基于Actor和ActorStream的</strong>。</li><li><a href="https://doc.akka.io/docs/akka/current/cluster-client.html" target="_blank" rel="noopener">Cluster Client</a> 是<code>Akka Cluster</code>提供的远程客户端，用以向集群提交请求并获得结果。</li><li><a href="https://doc.akka.io/docs/akka/current/stream/index.html" target="_blank" rel="noopener">Akka Stream</a> 提供了完整的IO流及流式处理的工具集和API。</li><li><a href="https://developer.lightbend.com/docs/alpakka/current/" target="_blank" rel="noopener">Alpakaa</a> Akka提供的整合<code>Kafka</code>的流式处理API。</li></ul><h3 id="对内"><a href="#对内" class="headerlink" title="对内"></a>对内</h3><p>集群内部调用，一般有以下几种方式：</p><ul><li>查找目标，直接调用。由前文可知，<code>Akka Cluster</code>是完全的<code>P2P</code>结构，所以集群种任何一个Actor可以随意去请求任何的其他Actor，只需要简单的指定其ActorPath即可。</li><li>发布/订阅模式。<code>akka.cluster.pubsub.DistributedPubSubMediator</code>可不使用外部MQ的情况下，直接在集群内部提供点对点或订阅功能。</li></ul><blockquote><p>由于官方的样例中已经提供了比较好的学习代码，本章就不再自己写代码去演示了。<br>官方在github例子地址：<a href="https://github.com/akka/akka-samples" target="_blank" rel="noopener">akka-samples</a><br>对于Cluster，在<a href="https://github.com/akka/akka-samples/tree/2.5/akka-sample-cluster-java" target="_blank" rel="noopener">akka-sample-cluster-java</a>提供了4个例子:</p><ul><li>simple: 主要演示cluster启动过程中节点的交互过程，对应的是我上一篇文章</li><li>transformation： 最基本的cluster应用，典型的master-worker模式</li><li>stats： 主要演示cluster中路由的应用</li><li>factorial： 主要演示cluster中负载均衡的使用</li></ul></blockquote><h2 id="最简单的例子"><a href="#最简单的例子" class="headerlink" title="最简单的例子"></a>最简单的例子</h2><p>通常来讲，使用分布式集群的应用，大概率是并发请求量大，单请求处理较为耗时，可改为并行处理提高响应速度的，而常见的就是master-slave模式，即master接受外部请求、分配任务及返回响应，而真正的处理过程是交给slave去异步做的。所以，在这种集群应用中，不同的节点会分饰不同的角色。</p><p>第一个例子：</p><ul><li>集群分为前端和后端两部分 </li><li>frontend维护了n个backend，并定期向backend发送hello[n]的消息，比如hello1,hello2</li><li>backend将字母转换为大写，返回给frontend</li></ul><p>由于比较简单，完整的代码我就不贴了，只看几个关键点</p><h4 id="前端"><a href="#前端" class="headerlink" title="前端"></a>前端</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TransformationFrontend</span> <span class="keyword">extends</span> <span class="title">AbstractActor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  List&lt;ActorRef&gt; backends = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">  <span class="keyword">int</span> jobCounter = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> Receive <span class="title">createReceive</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> receiveBuilder()</span><br><span class="line">      .match(TransformationJob.class, job -&gt; backends.isEmpty(), job -&gt; &#123;</span><br><span class="line">        sender().tell(<span class="keyword">new</span> JobFailed(<span class="string">"Service unavailable, try again later"</span>, job),</span><br><span class="line">          sender());</span><br><span class="line">      &#125;)</span><br><span class="line">      .match(TransformationJob.class, job -&gt; &#123;</span><br><span class="line">        jobCounter++;</span><br><span class="line">        backends.get(jobCounter % backends.size())</span><br><span class="line">          .forward(job, getContext());</span><br><span class="line">      &#125;)</span><br><span class="line">      .matchEquals(BACKEND_REGISTRATION, message -&gt; &#123;</span><br><span class="line">        getContext().watch(sender());</span><br><span class="line">        backends.add(sender());</span><br><span class="line">      &#125;)</span><br><span class="line">      .match(Terminated.class, terminated -&gt; &#123;</span><br><span class="line">        backends.remove(terminated.getActor());</span><br><span class="line">      &#125;)</span><br><span class="line">      .build();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>前端维护了一个backend的ActorRef的列表，在收到自定义的<code>BACKEND_REGISTRATION</code>事件后，将消息的发送者，即Backend的actor所对应的<code>ActorRef</code>放到该列表去。<br><code>TransformationJob</code>是外部提交的任务，如果列表为空时，会返回<code>JobFailed</code>，否则用简单的负载均衡方法将Job转发给对应的后端(当前已收到的job数量对后端数取余)。<br>由于只有一个Frontend，并且Actor内部有<code>mailbox</code>队列，所以这里的jobCounter不会出现并发问题。</p><h4 id="后端"><a href="#后端" class="headerlink" title="后端"></a>后端</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TransformationBackend</span> <span class="keyword">extends</span> <span class="title">AbstractActor</span> </span>&#123;</span><br><span class="line">    </span><br><span class="line">  Cluster cluster = Cluster.get(getContext().system());</span><br><span class="line">  LoggingAdapter log = Logging.getLogger(getContext().system(), <span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">//subscribe to cluster changes, MemberUp</span></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">preStart</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    cluster.subscribe(self(), MemberUp.class);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//re-subscribe when restart</span></span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">postStop</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    cluster.unsubscribe(self());</span><br><span class="line">  &#125;</span><br><span class="line">    </span><br><span class="line">  <span class="meta">@Override</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> Receive <span class="title">createReceive</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> receiveBuilder()</span><br><span class="line">      .match(TransformationJob.class, job -&gt; &#123;</span><br><span class="line">        sender().tell(<span class="keyword">new</span> TransformationResult(self().path().toSerializationFormat(), job.getText().toUpperCase()),</span><br><span class="line">          self());</span><br><span class="line">      &#125;)</span><br><span class="line">      .match(CurrentClusterState.class, state -&gt; &#123;</span><br><span class="line">        <span class="keyword">for</span> (Member member : state.getMembers()) &#123;</span><br><span class="line">          <span class="keyword">if</span> (member.status().equals(MemberStatus.up())) &#123;</span><br><span class="line">            register(member);</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;)</span><br><span class="line">      .match(MemberUp.class, mUp -&gt; &#123;</span><br><span class="line">        register(mUp.member());</span><br><span class="line">      &#125;)</span><br><span class="line">      .build();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">void</span> <span class="title">register</span><span class="params">(Member member)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (member.hasRole(<span class="string">"frontend"</span>)) &#123;</span><br><span class="line">      log.info(<span class="string">"Trying to register myself: &#123;&#125;"</span>, self().path().toSerializationFormat());</span><br><span class="line">      getContext().actorSelection(member.address() + <span class="string">"/user/frontend"</span>).tell(</span><br><span class="line">              BACKEND_REGISTRATION, self());</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>后端在<code>prestart()</code>时去监听了<code>MemberUp</code>的事件，当收到<code>MemberUp</code>时，通过简单的判断当前Member的角色是<code>frontend</code>就尝试给frontend发送注册消息，把自己的ActorRef加到frontend所维护的列表中。<br>这里为了体现是哪个后端所做的job，我加上了相关日志，在运行时可以仔细观察以下。<br>这里有个小问题不妨思考下：如果前端此时并未启动，这个<code>BACKEND_REGISTRATION</code>会怎么样呢？</p><h4 id="启动"><a href="#启动" class="headerlink" title="启动"></a>启动</h4><p>前端的启动<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// Override the configuration of the port when specified as program argument</span></span><br><span class="line">    <span class="keyword">final</span> String port = args.length &gt; <span class="number">0</span> ? args[<span class="number">0</span>] : <span class="string">"0"</span>;</span><br><span class="line">    <span class="keyword">final</span> Config config = </span><br><span class="line">      ConfigFactory.parseString(</span><br><span class="line">          <span class="string">"akka.remote.netty.tcp.port="</span> + port + <span class="string">"\n"</span> +</span><br><span class="line">          <span class="string">"akka.remote.artery.canonical.port="</span> + port)</span><br><span class="line">      .withFallback(ConfigFactory.parseString(<span class="string">"akka.cluster.roles = [frontend]"</span>))</span><br><span class="line">      .withFallback(ConfigFactory.load());</span><br><span class="line"></span><br><span class="line">    ActorSystem system = ActorSystem.create(<span class="string">"ClusterSystem"</span>, config);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">final</span> ActorRef frontend = system.actorOf(</span><br><span class="line">        Props.create(TransformationFrontend.class), <span class="string">"frontend"</span>);</span><br><span class="line">    <span class="keyword">final</span> FiniteDuration interval = Duration.create(<span class="number">2</span>, TimeUnit.SECONDS);</span><br><span class="line">    <span class="keyword">final</span> Timeout timeout = <span class="keyword">new</span> Timeout(Duration.create(<span class="number">5</span>, TimeUnit.SECONDS));</span><br><span class="line">    <span class="keyword">final</span> ExecutionContext ec = system.dispatcher();</span><br><span class="line">    <span class="keyword">final</span> AtomicInteger counter = <span class="keyword">new</span> AtomicInteger();</span><br><span class="line">    system.scheduler().schedule(interval, interval, <span class="keyword">new</span> Runnable() &#123;</span><br><span class="line">      <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        ask(frontend,</span><br><span class="line">            <span class="keyword">new</span> TransformationJob(<span class="string">"hello-"</span> + counter.incrementAndGet()),</span><br><span class="line">            timeout).onSuccess(<span class="keyword">new</span> OnSuccess&lt;Object&gt;() &#123;</span><br><span class="line">          <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onSuccess</span><span class="params">(Object result)</span> </span>&#123;</span><br><span class="line">            System.out.println(result);</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;, ec);</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">    &#125;, ec);</span><br><span class="line"></span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure></p><ul><li>通过akka.cluster.roles来给当前所起的节点进行角色指定。指定的角色信息会带到系统的<code>Member</code>类中去</li><li>通过定时器，定时给前端actor发送Job</li></ul><p>后端的启动我就不贴了，简单的读取application.conf，覆盖端口配置和角色，然后启动<code>ActorSystem</code>去创建actor。</p><p>前面的那个问题：“如果前端此时并未启动，这个<code>BACKEND_REGISTRATION</code>会怎么样呢？”<br>在官方提供的代码中，<code>TransformationApp</code>这个类整合了前后端节点的启动，但是它把后端分配为2551和2552端口，即把两个后端作为了种子节点，而前端作为了普通节点。如果是像它这样放在一起启动倒也没什么，但是如果单独一个个去运行时，就可能会出现前端在等待集群创建，而后端在memberup时，并没有找到前端Actor，导致注册失败。因为除非Cluster发生变化导致重新Gossip改变节点状态，否则MemberUp事件不会再发，这时哪怕前端启动了，其维护的后端列表依然为空。所以，一般情况下，像这种master-slave的用法，最好master就作为种子节点。</p><p>可以看到这样去实现master-slave虽然可以，但是依然存在些问题。好在<code>Akka</code>已经提供好了解决方案，就是Router和Routee，我们下章继续。</p><blockquote><p>注意！<br>如果在配置文件中是否启用artery，actor的地址会有不同，关闭artery时一定要指定协议为akk.tcp!<br>比如在配置文件中定义种子节点时：</p><ul><li>artery enabled = on<br>seed-nodes = [“<strong>akka</strong>:<a href="mailto://ClusterSystem@127.0.0.1" target="_blank" rel="noopener">//ClusterSystem@127.0.0.1</a>:2551”]</li><li>artery enabled = off<br>seed-nodes = [“<strong>akka.tcp</strong>:<a href="mailto://ClusterSystem@127.0.0.1" target="_blank" rel="noopener">//ClusterSystem@127.0.0.1</a>:2551”]<br>同时，启用artery在win10上调试还遇到一个问题，它会将logbuffer临时文件写到windows的<code>\Users\&lt;user_name&gt;\AppData\Local\Temp</code>下，正常我们在编辑器里运行调试完毕，会习惯性直接关闭，这时这些临时文件是不会被删除的。结果就是不断的启停后磁盘会被写满。<br>解决的方案有两个：<ol><li>关闭artery，使用netty tcp；</li><li>通过命令优雅的关闭<code>ActorSystem</code></li></ol></li></ul></blockquote><h2 id="集群的监控"><a href="#集群的监控" class="headerlink" title="集群的监控"></a>集群的监控</h2><p>Akka对于集群的监控方式有两种：</p><ol><li>内置的JMX</li><li>对外提供HTTP API</li></ol><p>其中内置的JMX将会慢慢被取消掉，而且在使用时还要求每个JVM启动时加上JMX的接口参数，使用不太方便，同时所提供的<a href="http://github.com/akka/akka/tree/v2.5.18/akka-cluster/jmx-client" target="_blank" rel="noopener">jmx-client</a>工具在windows下是无法正常使用的，所以不推荐使用。<br>官方推荐使用对外提供HTTP接口的<a href="https://developer.lightbend.com/docs/akka-management/current/akka-management.html" target="_blank" rel="noopener">Akka Management</a>。它是所有其他管理模块核心，其他管理模块都是基于它做的插件。相关关系如下图：<br><img src="/images/2018/11/akka-mgmt-structure.png" alt=""></p><p>我们要使用的是<code>Cluster Http Management</code>，开启比较简单：</p><ol><li>添加依赖</li></ol><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.lightbend.akka.management<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-management-cluster-http_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>0.19.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">exclusions</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.typesafe.akka<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-actor_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.typesafe.akka<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-testkit_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.typesafe.akka<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-cluster_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.typesafe.akka<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-cluster-tools_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.typesafe.akka<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-distributed-data_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.typesafe.akka<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-cluster-sharding_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.typesafe.akka<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-stream_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">exclusions</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>这里之所以把其他的akka包都exclude掉，是因为我使用的akka包是2.5.17，而这个管理包依赖目前对应的这些包版本还是2.5.15，没有更新过来，不过不影响使用。</p><ol start="2"><li>配置文件中配置监听地址和端口(可选)<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">management</span> <span class="string">&#123;</span></span><br><span class="line">  <span class="string">http</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">hostname</span> <span class="string">=</span> <span class="string">"127.0.0.1"</span></span><br><span class="line">    <span class="string">port</span> <span class="string">=</span> <span class="string">"8558"</span></span><br><span class="line">    <span class="string">bind-hostname</span> <span class="string">=</span> <span class="string">"0.0.0.0"</span></span><br><span class="line">    <span class="string">bind-port</span> <span class="string">=</span> <span class="string">"8558"</span></span><br><span class="line">  <span class="string">&#125;</span></span><br><span class="line"><span class="string">&#125;</span></span><br></pre></td></tr></table></figure></li></ol><p>这一步是可选的，如果不配置，默认监听的是调用<code>InetAddress.getLocalHost.getHostAddress</code>这段代码返回的IP，未必是127.0.0.1，一般是当前电脑的对外IP。如果使用docker，那么就必须要显式指定一下了。<br>默认端口是8558.</p><ol start="3"><li>开启监控<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ClusterHttpManagement httpManagement = ClusterHttpManagement.get(system);</span><br><span class="line">AkkaManagement.get(system).start();</span><br></pre></td></tr></table></figure></li></ol><p>启动后，我们就可以从<code>http://127.0.0.1:8558</code>这个地址获取集群信息并发送集群指令了。</p><table><thead><tr><th>Path</th><th>HTTP method</th><th>Required form fields</th><th>Description</th></tr></thead><tbody><tr><td><code>/cluster/members/</code></td><td>GET</td><td>None</td><td>Returns the status of the Cluster in JSON format.</td></tr><tr><td><code>/cluster/members/</code></td><td>POST</td><td>address: <code>{address}</code></td><td>Executes join operation in cluster for the provided <code>{address}</code>.</td></tr><tr><td><code>/cluster/members/{address}</code></td><td>GET</td><td>None</td><td>Returns the status of <code>{address}</code> in the Cluster in JSON format.</td></tr><tr><td><code>/cluster/members/{address}</code></td><td>DELETE</td><td>None</td><td>Executes leave operation in cluster for provided <code>{address}</code>.</td></tr><tr><td><code>/cluster/members/{address}</code></td><td>PUT</td><td>operation: Down</td><td>Executes down operation in cluster for provided <code>{address}</code>.</td></tr><tr><td><code>/cluster/members/{address}</code></td><td>PUT</td><td>operation: Leave</td><td>Executes leave operation in cluster for provided <code>{address}</code>.</td></tr><tr><td><code>/cluster/shards/{name}</code></td><td>GET</td><td>None</td><td>Returns shard info for the shard region with the provided <code>{name}</code></td></tr></tbody></table><p>更详细使用请参考<a href="https://developer.lightbend.com/docs/akka-management/current/cluster-http-management.html" target="_blank" rel="noopener">官方文档</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;前面一个章节&lt;a href=&quot;http://edisonxu.com/2018/11/07/akka-cluster.html&quot;&gt;akka cluster管理&lt;/a&gt;介绍了&lt;code&gt;Akka Cluster&lt;/code&gt;的底层原理，这一章就来看看如何使用。&lt;/p&gt;
&lt;h2
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="分布式" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Akka" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/Akka/"/>
    
    
      <category term="分布式" scheme="http://edisonxu.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="akka" scheme="http://edisonxu.com/tags/akka/"/>
    
      <category term="actor" scheme="http://edisonxu.com/tags/actor/"/>
    
      <category term="并发" scheme="http://edisonxu.com/tags/%E5%B9%B6%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>Akka入门系列(四)：akka cluster原理</title>
    <link href="http://edisonxu.com/2018/11/07/akka-cluster.html"/>
    <id>http://edisonxu.com/2018/11/07/akka-cluster.html</id>
    <published>2018-11-07T08:42:38.000Z</published>
    <updated>2021-07-21T13:31:21.780Z</updated>
    
    <content type="html"><![CDATA[<p>在前面<a href="http://edisonxu.com/2018/10/30/akka-remote-actor.html">remote actor</a>一章提到过，<code>akka remoting</code>是<code>Peer-to-Peer</code>的，所以基于<code>remote</code>功能的<code>cluster</code>是一个去中心化的分布式集群。</p><p><code>Akka Cluster</code>将多个JVM连接整合在一起，实现消息地址的透明化和统一化使用管理，集成一体化的消息驱动系统。最终目的是将一个大型程序分割成若干子程序，部署到很多JVM上去实现程序的分布式并行运算（单机也可以起很多节点构成集群）。更重要的是, <code>Akka Cluster</code>集群构建与Actor编程没有直接的联系，集群构建是在ActorSystem层面上，实现了Actor消息地址的透明化，无需考虑目标运行环节是否分布式，可以按照正常的Actor编程模式进行开发。</p><p>我们知道，分布式集群是由若干节点组成的，那么节点的发现及状态管理是分布式系统一个比较重要的任务。<code>Akka Cluster</code>中将节点的生命周期划分为：</p><p><img src="/images/2018/10/member-states.png" alt=""></p><ul><li><strong>joining</strong> - 当尝试加入集群时的初始状态</li><li><strong>up</strong> - 加入集群后的正常状态</li><li><strong>leaving / exiting </strong> - 节点退出集群时的中间状态</li><li><strong>down</strong> - 集群无法感知某节点后，将其标记为down</li><li><strong>removed</strong> - 从集群中被删除，以后也无法再加入集群</li></ul><p>其实当参数<code>akka.cluster.allow-weakly-up-members</code>启用时(默认是启用的)，还有个<code>weakly up</code>，它是用于集群出现分裂时，集群无法收敛，则leader无法将状态置为up的临时状态。这个后面再解释。<br>图中还有两个特殊的名词：</p><ul><li><strong>fd*</strong> - 这个表示akka的错误检测机制<code>Faiulre Detector</code>被触发后，将节点标记为unreachable</li><li><strong>unreachable*</strong> - <code>unreachable</code>不是一个真正的节点状态，更多的像是一个flag，用来描述集群无法与该节点进行通讯。当错误检测机制侦测到这个节点又能正常通讯时，会移除这个flag。</li></ul><p>市面上大多数产品的分布式管理一般用的是注册中心机制，例如zk、consul或etcd。其实是节点把自己的信息注册到所使用的注册中心里，而master通过接受注册中心的通知得知新节点信息。显然本质上是一种master/slave的架构。这种架构有两个问题：</p><ol><li><p>master节点一般是单一的，一旦挂了影响就比较大（所以很多master都采用了HA机制），也就是所谓的系统单点故障；</p></li><li><p>通常节点的地址发现是要走master去获取的，当系统并发大时，master节点就可能成为性能瓶颈，即单点性能瓶颈。</p></li></ol><p><code>Akka</code>可能就是考虑这两点，采用了P2P的模式，这样任何一个节点都可以作为”master”，任何的节点都可以用来寻找其他节点地址。那它是怎么做到的呢？答案是<a href="http://en.wikipedia.org/wiki/Gossip_protocol" target="_blank" rel="noopener">Gossip</a>协议和<code>CRDT</code>。</p><h2 id="Akka-Gossip"><a href="#Akka-Gossip" class="headerlink" title="Akka Gossip"></a>Akka Gossip</h2><h3 id="基本介绍"><a href="#基本介绍" class="headerlink" title="基本介绍"></a>基本介绍</h3><h4 id="Gossip协议"><a href="#Gossip协议" class="headerlink" title="Gossip协议"></a>Gossip协议</h4><p><code>Gossip</code>协议简单来说，就是病毒式的将信息扩散到整个集群，无法确定何时完成完全扩散，但最终是会到达完全扩散状态的（最终一致性），即收敛。具体介绍可以参考我转载的一片文章——<a href="http://edisonxu.com/2018/11/02/gossip.html">P2P 网络核心技术：Gossip 协议</a>，这里就不再重复叙述，着重介绍下<code>Akka</code>是怎么使用<code>Gossip</code>的。</p><h4 id="CRDT"><a href="#CRDT" class="headerlink" title="CRDT"></a>CRDT</h4><p>P2P的分布式系统中，理论上每个节点都能处理外部的请求，以及向其他节点发送请求。而系统中存在的共享变量，可能在同一时间会被两个不同节点的请求用到，即并发安全问题。一般解决方案是队列或自旋，后者本质上还是一种变相的队列。排队就牵扯到两个问题：</p><ol><li>“谁先来的” </li></ol><p>很多人下意识会觉得用时间戳就可以了嘛，但在分布式集群中，每个节点如果是一台单独的服务器，那么每个节点的时间戳未必相同（比如未开启Ntp）。</p><ol start="2"><li>“同时来的怎么办”</li></ol><p>就像git，能merge就merge，不能merge就解决冲突。</p><p><code>CRDT</code>就是用于解决解决分布式事件的先后顺序及merge问题的数据结构的简称，即<code>Conflict-Free Replicated Data Types</code>的缩写，它的作用是保证最终一致性，出处参阅<a href="http://hal.upmc.fr/docs/00/55/55/88/PDF/techreport.pdf" target="_blank" rel="noopener">这份论文</a>。白话文 <a href="http://liyu1981.github.io/what-is-CRDT/" target="_blank" rel="noopener">谈谈CRDT</a> 和<a href="https://lfwen.site/2018/06/09/crdt-counter/" target="_blank" rel="noopener">CRDT介绍</a>这两篇文章讲的通俗易懂，多的就不再重复了。<br><code>Akka</code>中节点的状态就是一个特殊的<code>CRDT</code>，使用向量时钟<code>Vector Clock</code>实现方案，关于向量时钟<code>Vector Clock</code>可以参见我转发的这篇文章<a href="http://edisonxu.com/clocks">Vector Clock/Version Clock</a>。 </p><p><code>Akka</code>的gossip协议发送的具体内容如下：<br><figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">final</span> <span class="keyword">case</span> <span class="class"><span class="keyword">class</span> <span class="title">Gossip</span>(<span class="params"></span></span></span><br><span class="line"><span class="class"><span class="params">  members:    immutable.<span class="type">SortedSet</span>[<span class="type">Member</span>], // sorted set of members with their status, sorted by address</span></span></span><br><span class="line"><span class="class"><span class="params">  overview:   <span class="type">GossipOverview</span>                       = <span class="type">GossipOverview</span>(</span>),</span></span><br><span class="line"><span class="class">  <span class="title">version</span></span>:    <span class="type">VectorClock</span>                          = <span class="type">VectorClock</span>(), <span class="comment">// vector clock version</span></span><br><span class="line">  tombstones: <span class="type">Map</span>[<span class="type">UniqueAddress</span>, <span class="type">Gossip</span>.<span class="type">Timestamp</span>] = <span class="type">Map</span>.empty</span><br><span class="line">)</span><br><span class="line"><span class="keyword">final</span> <span class="keyword">case</span> <span class="class"><span class="keyword">class</span> <span class="title">GossipOverview</span>(<span class="params"></span></span></span><br><span class="line"><span class="class"><span class="params">  seen:         <span class="type">Set</span>[<span class="type">UniqueAddress</span>] = <span class="type">Set</span>.empty,</span></span></span><br><span class="line"><span class="class"><span class="params">  reachability: <span class="type">Reachability</span>       = <span class="type">Reachability</span>.empty</span></span></span><br><span class="line"><span class="class"><span class="params"></span>)</span></span><br><span class="line"><span class="class"><span class="title">class</span> <span class="title">Reachability</span> <span class="title">private</span> (<span class="params"></span></span></span><br><span class="line"><span class="class"><span class="params">  val records:  immutable.<span class="type">IndexedSeq</span>[<span class="type">Reachability</span>.<span class="type">Record</span>],</span></span></span><br><span class="line"><span class="class"><span class="params">  val versions: <span class="type">Map</span>[<span class="type">UniqueAddress</span>, <span class="type">Long</span>]</span></span></span><br><span class="line"><span class="class"><span class="params"></span>)</span></span><br></pre></td></tr></table></figure></p><ul><li><strong>members</strong> 存放该节点知道的其他节点</li><li><strong>seen</strong> 已经收到本次gossip的节点们，每个节点当接受到一个新的gossip消息时，会把自己放到seen里面，作为响应返回给发送者</li><li><strong>reachability</strong> 这个由错误检测机制<code>Faiulre Detector</code>的心跳模块来维护，用来判断节点是否存活。正常情况下records应该是空的，当有节点处于Unreachable时，才会有记录加到records里。</li><li><strong>version</strong> 向量时钟，用于冲突检测和处理</li></ul><h4 id="种子节点-SeedNode"><a href="#种子节点-SeedNode" class="headerlink" title="种子节点 SeedNode"></a>种子节点 SeedNode</h4><p><code>SeendNode</code>一般是提前配置好的一组节点。它用于接受其他节点（可以是种子节点）的加入集群的请求。不同节点，在<code>Akka Cluster</code>中启动时会有不同的逻辑：</p><ul><li>如果是种子节点，并且是排序后的种子节点数组中<strong>排第一</strong>的，它会在一个规定的时间内(默认5秒)去尝试加入已存在的集群，即发送<code>InitJoin</code>消息到其他种子节点。如果未能成功加入，则自己将<strong>创建一个新的Cluster</strong>。</li><li>如果是种子节点，但并不是数组中排第一的，则会向其他种子节点发送<code>InitJoin</code>消息，如果失败将不断重试，直到能成功加入<strong>第一个返回响应的已加入集群的种子节点</strong>对应的Cluster。</li><li>如果是普通节点，则会向其他种子节点发送<code>InitJoin</code>消息，如果失败将不断重试，直到能成功加入<strong>第一个返回响应的已加入集群的种子节点</strong>对应的Cluster。</li></ul><p>这里有一点值得注意，为什么是加入第一个返回响应的种子节点所在的集群？这个问题后面再解释。</p><h2 id="过程详解"><a href="#过程详解" class="headerlink" title="过程详解"></a>过程详解</h2><p>下面用一个简单的场景来解释整个交互过程，假定我们有两个节点n1和n2，其中n1是种子节点。我们让n2先启动。<br><img src="/images/2018/10/gossip-process.png" alt=""></p><p>上图中的T0、T1表示时间轴，但只是为了方便将步骤拆解，便于理解。其中T4和T5并没有必然的时间前后关系，这里只是假定T4在前，步骤基本是类似的，T5在前也只是稍有不同。</p><p>#T0、T1时刻只是为了表明n2在启动时，如果没有种子节点响应，则会一直等待重试</p><p>#T2时刻种子节点自己新建一个集群，由于新集群只有它自己，members和seen是一样的，所以把自己作为集群的<code>leader</code>。</p><h4 id="leader"><a href="#leader" class="headerlink" title="leader"></a>leader</h4><ul><li>Gossip协议中没有<code>leader</code>选举过程</li><li><code>leader</code>只是一个角色，任何节点均可以是<code>leader</code></li><li><code>leader</code>的确定非常简单：<strong>集群收敛后，当前members队列按IP进行排序，排第一位置的节点就是整个集群的<code>leader</code></strong></li><li><code>leader</code>并非一直不变，如果集群有新节点加入或某节点退出，导致发生Gossip过程，收敛后都会重新确定<code>leader</code></li><li><code>leader</code>的职责是更新节点在集群中的状态以及将集群的成员移入或移出集群</li></ul><blockquote><p>注意，这里有个地方容易被误解：“n1和n2构成一个集群，不是在T5才收敛吗？怎么在T2就确定<code>leader</code>了？”<br>其实当第一个种子节点新建cluster时，由于只有它一个，即seen和members里内容一样，它判断当前集群已收敛，就把自己当作leader了。所以才有了T2_2和T4。</p></blockquote><p>#T3时刻是n1响应n2的InitJoin请求，具体交互过程如下：<br><img src="/images/2018/10/gossip-interact.png" alt=""></p><p>#T3_0种子节点收到n2的<code>Join</code>消息后，会做两件事：</p><ol><li>更新当前Gossip的向量时钟；</li><li>清空当前Gossip的seen队列，然后把自己加进去。（后续发起Gossip交互时，会优先选择那些没在seen队列中的成员）</li></ol><p>#T4时刻因为作为fd能正常与n2进行心跳，n1作为leader就被通知将n2提升为Up状态</p><p>#T5时刻是一个<code>CRDT</code>的对比过程，对比两个Goissp的<code>version</code>，即<code>VectorClock</code>，比较的结果有三种：</p><ul><li><strong>Same</strong>: 相同，则进行seen队列合并就可以了</li><li><strong>Before</strong>: 本地新，则向对端发送本地的Gossip，本地不变</li><li><strong>After</strong>: 对端新，则更新本地的Gossip。如果对端的Gossip的seen里没有包含本地，则将自己添加到seen里发送给对端，以减少一次两者间的Gossip交互。</li></ul><p>#T5时刻最后集群达到了收敛</p><h4 id="Gossip-收敛"><a href="#Gossip-收敛" class="headerlink" title="Gossip 收敛"></a>Gossip 收敛</h4><p>从上面的图里可以看到节点初始化时会把自己加入到members里，回传回去，同时，节点在收到新的Gossip时，会把自己加入到seen里面。那么，在一开始，members和seen中的节点数是不同的。<br>当Gossip传递的消息被整个集群都消化掉的时候，可以称作当前集群的Gossip收敛。靠以下条件判断Gossip收敛：</p><ul><li>集群中不存在<code>unreachable</code>的节点，或者<code>unreachable</code>的节点应该均处于<code>down</code>或<code>exiting</code>状态</li><li>正常节点均处于<code>up</code>或<code>leaving</code>状态，且members里的节点都在seen里，即集群中所有的节点都收到过该Gossip</li></ul><h2 id="代码演示"><a href="#代码演示" class="headerlink" title="代码演示"></a>代码演示</h2><p>说了那么多文字，<code>Akka Cluster</code>提供了监控ClusterEvent的方法，我们可以用代码来校验下上面的知识。<br>添加依赖<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.typesafe.akka<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-cluster_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.5.17<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>首先编写<code>application.conf</code>配置文件<br><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">akka</span> <span class="string">&#123;</span></span><br><span class="line">  <span class="string">actor</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">provider</span> <span class="string">=</span> <span class="string">"cluster"</span></span><br><span class="line">  <span class="string">&#125;</span></span><br><span class="line">  <span class="string">remote</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">netty.tcp</span> <span class="string">&#123;</span></span><br><span class="line">      <span class="string">hostname</span> <span class="string">=</span> <span class="string">"127.0.0.1"</span></span><br><span class="line">      <span class="string">port</span> <span class="string">=</span> <span class="number">0</span></span><br><span class="line">    <span class="string">&#125;</span></span><br><span class="line">    <span class="string">artery</span> <span class="string">&#123;</span></span><br><span class="line">      <span class="string">enabled</span> <span class="string">=</span> <span class="string">on</span></span><br><span class="line">      <span class="string">canonical.hostname</span> <span class="string">=</span> <span class="string">"127.0.0.1"</span></span><br><span class="line">      <span class="string">canonical.port</span> <span class="string">=</span> <span class="number">0</span></span><br><span class="line">    <span class="string">&#125;</span></span><br><span class="line">  <span class="string">&#125;</span></span><br><span class="line"></span><br><span class="line">  <span class="string">cluster</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">seed-nodes</span> <span class="string">=</span> <span class="string">[</span></span><br><span class="line">      <span class="string">"akka://ClusterSystem@127.0.0.1:2552"</span><span class="string">,</span></span><br><span class="line">      <span class="string">"akka://ClusterSystem@127.0.0.1:2551"</span></span><br><span class="line">    <span class="string">]</span></span><br><span class="line">  <span class="string">&#125;</span></span><br><span class="line"><span class="string">&#125;</span></span><br></pre></td></tr></table></figure></p><p>然后，编写Actor<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SimpleClusterListener</span> <span class="keyword">extends</span> <span class="title">AbstractActor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    LoggingAdapter log = Logging.getLogger(getContext().system(), <span class="keyword">this</span>);</span><br><span class="line">    Cluster cluster = Cluster.get(getContext().system());</span><br><span class="line"></span><br><span class="line">    <span class="comment">//subscribe to cluster changes</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">preStart</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        cluster.subscribe(self(), ClusterEvent.initialStateAsEvents(), ClusterEvent.MemberEvent.class, ClusterEvent.UnreachableMember.class);</span><br><span class="line">        log.info(<span class="string">"I'm about to start! Code: &#123;&#125; "</span>, getSelf().hashCode());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">postStop</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        cluster.unsubscribe(self());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Receive <span class="title">createReceive</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> receiveBuilder()</span><br><span class="line">                .match(ClusterEvent.MemberUp.class, mUp-&gt;log.info(<span class="string">"Member is Up: &#123;&#125;"</span>, mUp.member()))</span><br><span class="line">                .match(ClusterEvent.UnreachableMember.class, mUnreachable-&gt;log.info(<span class="string">"Member detected as unreachable: &#123;&#125;"</span>, mUnreachable.member()))</span><br><span class="line">                .match(ClusterEvent.MemberRemoved.class, mRemoved-&gt;log.info(<span class="string">"Member is Removed: &#123;&#125;"</span>, mRemoved.member()))</span><br><span class="line">                .match(ClusterEvent.LeaderChanged.class, msg-&gt;log.info(<span class="string">"Leader is changed: &#123;&#125;"</span>, msg.getLeader()))</span><br><span class="line">                .match(ClusterEvent.RoleLeaderChanged.class, msg-&gt;log.info(<span class="string">"RoleLeader is changed: &#123;&#125;"</span>, msg.getLeader()))</span><br><span class="line">                .match(ClusterEvent.MemberEvent.class, event-&gt;&#123;&#125;) <span class="comment">//ignore</span></span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>最后是启动类<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">App</span> </span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">( String[] args )</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(args.length==<span class="number">0</span>)</span><br><span class="line">            startup(<span class="keyword">new</span> String[] &#123;<span class="string">"2551"</span>, <span class="string">"2552"</span>, <span class="string">"0"</span>&#125;);</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            startup(args);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">startup</span><span class="params">(String[] ports)</span></span>&#123;</span><br><span class="line">        ExecutorService pool = Executors.newFixedThreadPool(ports.length);</span><br><span class="line">        <span class="keyword">for</span>(String port : ports)&#123;</span><br><span class="line">            pool.submit(()-&gt;&#123;</span><br><span class="line">            <span class="comment">// Using input port to start multiple instances</span></span><br><span class="line">                Config config = ConfigFactory.parseString(</span><br><span class="line">                        <span class="string">"akka.remote.netty.tcp.port="</span> + port + <span class="string">"\n"</span> +</span><br><span class="line">                                <span class="string">"akka.remote.artery.canonical.port="</span> + port)</span><br><span class="line">                        .withFallback(ConfigFactory.load());</span><br><span class="line"></span><br><span class="line">                <span class="comment">// Create an Akka system</span></span><br><span class="line">                ActorSystem system = ActorSystem.create(<span class="string">"ClusterSystem"</span>, config);</span><br><span class="line"></span><br><span class="line">                <span class="comment">// Create an</span></span><br><span class="line">                system.actorOf(Props.create(SimpleClusterListener.class), <span class="string">"ClusterListener"</span>);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>这里设置了2552和2551两个种子节点，及一个随机端口启动的普通节点。<strong>故意在配置中把2552放到2551前面去。</strong><br>带参数2551作为端口启动程序，命名为Node1，启动后，可以看到它会不断尝试连接提供的种子节点中排第一的2552<br><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[WARN] [11/07/2018 17:15:13.823] [ClusterSystem-akka.actor.default-dispatcher-5] [akka://ClusterSystem@127.0.0.1:2551/system/cluster/core/daemon/joinSeedNodeProcess-1] Couldn't join seed nodes after [2] attempts, will try again. seed-nodes=[akka://ClusterSystem@127.0.0.1:2552]</span><br><span class="line">[WARN] [11/07/2018 17:15:18.835] [ClusterSystem-akka.actor.default-dispatcher-10] [akka://ClusterSystem@127.0.0.1:2551/system/cluster/core/daemon/joinSeedNodeProcess-1] Couldn't join seed nodes after [3] attempts, will try again. seed-nodes=[akka://ClusterSystem@127.0.0.1:2552]</span><br></pre></td></tr></table></figure></p><p>这时带参数2552启动程序，命名为Node2，命令行会打印<br><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">[INFO] [11/07/2018 17:18:21.285] [ClusterSystem-akka.actor.default-dispatcher-10] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka://ClusterSystem@127.0.0.1:2552] - Node [akka://ClusterSystem@127.0.0.1:2552] is JOINING itself (with roles [dc-default]) and forming new cluster</span><br><span class="line">[INFO] [11/07/2018 17:18:21.288] [ClusterSystem-akka.actor.default-dispatcher-10] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka://ClusterSystem@127.0.0.1:2552] - Cluster Node [akka://ClusterSystem@127.0.0.1:2552] dc [default] is the new leader</span><br><span class="line">[INFO] [11/07/2018 17:18:21.300] [ClusterSystem-akka.actor.default-dispatcher-10] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka://ClusterSystem@127.0.0.1:2552] - Leader is moving node [akka://ClusterSystem@127.0.0.1:2552] to [Up]</span><br><span class="line">[INFO] [11/07/2018 17:18:21.308] [ClusterSystem-akka.actor.default-dispatcher-11] [akka://ClusterSystem/user/ClusterListener] Member is Up: Member(address = akka://ClusterSystem@127.0.0.1:2552, status = Up)</span><br><span class="line">[INFO] [11/07/2018 17:18:23.323] [ClusterSystem-akka.actor.default-dispatcher-10] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka://ClusterSystem@127.0.0.1:2552] - Received InitJoin message from [Actor[akka://ClusterSystem@127.0.0.1:2551/system/cluster/core/daemon/joinSeedNodeProcess-1#1739420295]] to [akka://ClusterSystem@127.0.0.1:2552]</span><br><span class="line">[INFO] [11/07/2018 17:18:23.323] [ClusterSystem-akka.actor.default-dispatcher-10] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka://ClusterSystem@127.0.0.1:2552] - Sending InitJoinAck message from node [akka://ClusterSystem@127.0.0.1:2552] to [Actor[akka://ClusterSystem@127.0.0.1:2551/system/cluster/core/daemon/joinSeedNodeProcess-1#1739420295]] (version [2.5.17])</span><br><span class="line">[INFO] [11/07/2018 17:18:23.549] [ClusterSystem-akka.actor.default-dispatcher-10] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka://ClusterSystem@127.0.0.1:2552] - Node [akka://ClusterSystem@127.0.0.1:2551] is JOINING, roles [dc-default]</span><br><span class="line">[INFO] [11/07/2018 17:18:24.236] [ClusterSystem-akka.actor.default-dispatcher-4] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka://ClusterSystem@127.0.0.1:2552] - Leader is moving node [akka://ClusterSystem@127.0.0.1:2551] to [Up]</span><br><span class="line">[INFO] [11/07/2018 17:18:24.237] [ClusterSystem-akka.actor.default-dispatcher-12] [akka://ClusterSystem/user/ClusterListener] Member is Up: Member(address = akka://ClusterSystem@127.0.0.1:2551, status = Up)</span><br><span class="line">[INFO] [11/07/2018 17:18:25.235] [ClusterSystem-akka.actor.default-dispatcher-11] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka://ClusterSystem@127.0.0.1:2552] - Cluster Node [akka://ClusterSystem@127.0.0.1:2552] dc [default] is no longer the leader</span><br></pre></td></tr></table></figure></p><p><code>Node [akka://ClusterSystem@127.0.0.1:2552] is JOINING itself (with roles [dc-default]) and forming new cluster</code> 说明作为排第一的种子节点，它创建了集群并把自己加了进去。<br><code>Cluster Node [akka://ClusterSystem@127.0.0.1:2552] dc [default] is the new leader</code> 说明2552变成了<code>leader</code>。<br><code>Node [akka://ClusterSystem@127.0.0.1:2551] is JOINING, roles [dc-default]</code> 2551在尝试加入集群<br><code>Leader is moving node [akka://ClusterSystem@127.0.0.1:2551] to [Up]</code> 2551成功加入了集群，状态变为Up<br><code>Cluster Node [akka://ClusterSystem@127.0.0.1:2552] dc [default] is no longer the leader</code> 集群变化导致新一轮Goissp收敛后，<code>leader</code>重新选取，2551的IP比2552小，被选为新的<code>leader</code>。<br>可以从Node1的命令行看到证据：<br><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[INFO] [11/07/2018 17:18:25.755] [ClusterSystem-akka.actor.default-dispatcher-9] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka://ClusterSystem@127.0.0.1:2551] - Cluster Node [akka://ClusterSystem@127.0.0.1:2551] dc [default] is the new leader</span><br></pre></td></tr></table></figure></p><p>我们再起一个参数为2900的，命名为Node3，等到正常启动，三个Node状态都为Up。</p><ul><li>2552是集群的创建者</li><li>2551是集群的<code>leader</code></li></ul><p>此时，我们把2552重启，会看到2551的命令行中出现<code>Leader is removing unreachable node [akka://ClusterSystem@127.0.0.1:2552]</code>，等2552完全启动时，可以看到<code>Welcome from [akka://ClusterSystem@127.0.0.1:2551]</code>说明2552向2551发送的加入集群的消息，2551给它发送了<code>Welcome</code>消息。2552不再自己创建新的集群。有兴趣的可以在关闭2552的情况下重启node3.</p><h2 id="进阶"><a href="#进阶" class="headerlink" title="进阶"></a>进阶</h2><p>其实本来这部分应该放在上面，但是一上来讲理论非常不好消化，至少我个人是如此。所以，我宁愿把好理解的交互步骤放前面，把一些知识点穿插在里面，最后再把无法放进去的干巴巴的理论放最后。</p><h3 id="Akka对于Gossip的优化"><a href="#Akka对于Gossip的优化" class="headerlink" title="Akka对于Gossip的优化"></a>Akka对于Gossip的优化</h3><ul><li>如果gossiper(gossip的发送者)和recipient(goissp的接收者)拥有相同版本的Gossip(recipient已包含在seen列表里，并且version也与gossiper的完全一致)，这时Gossip的状态不用再发回给gossiper，减少交互。</li><li>akka使用的是push-pull类型gossip的变种，它每次发送的是一个digest值，而非真正的value，recipient收到后先比较版本，只有当它的版本较低时，才会去向gossiper请求真正的值。</li><li>默认情况下，集群每1秒进行一次gossip，但如果seen里的节点数少于整个集群1/2，则集群每秒钟会进行3轮gossip，以加速收敛。</li><li>在未收敛时，gossiper在选择目标节点时是随机的但带有偏向性(biased gossip)。gossiper会选择在当前版本下不在seen里的节点去交换gossip，并且选择的比例系数较高(经验值400个节点下配置为0.8)。该系数会随着轮数增加而减少，以防止单节点同时间收到过多的gossip请求。recipient对于gossip请求也是放到mailbox里的，在mailbox队列较长时，会移除较早的请求。</li><li>当收敛后，目标节点的选择就完全是随机的了，而且只发送非常小的gossip状态的消息。一旦集群发生变化，就会回到上一条所述的带有偏向性的<code>biased gossip</code>。</li></ul><h3 id="Failure-Detector机制"><a href="#Failure-Detector机制" class="headerlink" title="Failure Detector机制"></a>Failure Detector机制</h3><ul><li>职责是定期检查集群中节点是否可用</li><li>是<a href="https://pdfs.semanticscholar.org/11ae/4c0c0d0c36dc177c1fff5eb84fa49aa3e1a8.pdf" target="_blank" rel="noopener">The Phi Accrual Failure Detector</a>的实现，是一种解耦了观察与行为的增量式错误检测器。它不会简单的判断节点是否可用，而是通过收集各种数据计算出<code>phi</code>值，通过与设定好的<code>threshold</code>进行对比，判断是否出现错误。</li><li>每个节点会根据集群节点的hash有序环确定临近的几个节点进行监控（默认是5个），方便跨机房进行监控，保证集群节点的全覆盖。目标节点每1秒向这些节点发送心跳。</li><li>只要有一个monitor认为某节点是<code>unreachable</code>状态，那么该节点就会被集群认为是<code>unreachable</code></li><li>被标记为<code>unreachable</code>的节点，只有在所有的monitor都认为它是<code>reachable</code>时，它才会被重新认为是<code>reachable</code>，leader会重新改变它的状态</li></ul><h3 id="网络分区与集群分区"><a href="#网络分区与集群分区" class="headerlink" title="网络分区与集群分区"></a>网络分区与集群分区</h3><p>当网络出现异常，比如一个跨两地机房的集群，机房间的网络断了。这时：</p><ul><li>原创建集群的种子节点所在的集群，会重新发起<code>biased gossip</code>，直至收敛，确认新的<code>leader</code>，被隔开的那部分节点会被认为是<code>unreachable</code>而最终被踢掉</li><li>被隔开的那部分节点，会重新发起<code>biased gossip</code>，其中排序在最前面的种子节点会创建一个新的集群，并产生新的<code>leader</code>。原集群中的那部分失联节点会被认为是<code>unreachable</code>而最终被从新集群踢掉</li><li>两个集群最终都恢复正常能对外提供服务，即原来的一个集群在无人干涉的情况下，分裂成了两个集群</li><li>当网络恢复后，两个集群会重新发起<code>biased gossip</code>，尝试融合，恢复成一个大集群。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>由此可见，从设计上来说，<code>Akka Cluster</code>是完全去中心化，无单点故障和单点性能瓶颈的，具有天然的分布式容错性和可扩容性。</p><blockquote><p>本文参考：<br><a href="https://doc.akka.io/docs/akka/current/common/cluster.html#gossip" target="_blank" rel="noopener">Akka Cluster Specification</a><br><a href="http://netcomm.iteye.com/blog/2080440" target="_blank" rel="noopener">Akka的Cluster源码分析</a><br><a href="https://blog.csdn.net/hohojiang/article/details/52444302" target="_blank" rel="noopener">Akka Cluster原理及实战</a><br><a href="https://gist.github.com/jboner/7692270" target="_blank" rel="noopener">Akka Cluster Implementation Notes</a><br><a href="https://www.cnblogs.com/gabry/p/9403260.html" target="_blank" rel="noopener">Akka 源码分析-Cluseter-ActorSystem</a></p></blockquote>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;在前面&lt;a href=&quot;http://edisonxu.com/2018/10/30/akka-remote-actor.html&quot;&gt;remote actor&lt;/a&gt;一章提到过，&lt;code&gt;akka remoting&lt;/code&gt;是&lt;code&gt;Peer-to-Peer&lt;/c
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="分布式" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Akka" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/Akka/"/>
    
    
      <category term="分布式" scheme="http://edisonxu.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="akka" scheme="http://edisonxu.com/tags/akka/"/>
    
      <category term="actor" scheme="http://edisonxu.com/tags/actor/"/>
    
      <category term="并发" scheme="http://edisonxu.com/tags/%E5%B9%B6%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>P2P 网络核心技术：Gossip 协议</title>
    <link href="http://edisonxu.com/2018/11/02/gossip.html"/>
    <id>http://edisonxu.com/2018/11/02/gossip.html</id>
    <published>2018-11-02T03:55:08.000Z</published>
    <updated>2021-07-21T13:31:21.783Z</updated>
    
    <content type="html"><![CDATA[<h3 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h3><p><code>Gossip protocol</code> 也叫 <code>Epidemic Protocol</code> （流行病协议），实际上它还有很多别名，比如：“流言算法”、“疫情传播算法”等。</p><p>这个协议的作用就像其名字表示的意思一样，非常容易理解，它的方式其实在我们日常生活中也很常见，比如电脑病毒的传播，森林大火，细胞扩散等等。</p><p><code>Gossip protocol</code> 最早是在 1987 年发表在 ACM 上的论文 <code>《Epidemic Algorithms for Replicated Database Maintenance》</code>中被提出。主要用在分布式数据库系统中各个副本节点同步数据之用，这种场景的一个最大特点就是组成的网络的节点都是对等节点，是非结构化网络，这区别与之前介绍的用于结构化网络中的 <code>DHT</code> 算法 <code>Kadmelia</code>。</p><p>我们知道，很多知名的 P2P 网络或区块链项目，比如 <code>IPFS</code>，<code>Ethereum</code> 等，都使用了 <code>Kadmelia</code> 算法，而大名鼎鼎的 Bitcoin 则是使用了 <code>Gossip</code> 协议来传播交易和区块信息。</p><p>实际上，只要仔细分析一下场景就知道，<code>Ethereum</code> 使用 <code>DHT</code> 算法并不是很合理，因为它使用节点保存整个链数据，不像 <code>IPFS</code> 那样分片保存数据，因此 <code>Ethereum</code> 真正适合的协议应该像 Bitcoin 那样，是 <code>Gossip</code> 协议。</p><h3 id="这里先简单介绍一下-Gossip-协议的执行过程："><a href="#这里先简单介绍一下-Gossip-协议的执行过程：" class="headerlink" title="这里先简单介绍一下 Gossip 协议的执行过程："></a>这里先简单介绍一下 Gossip 协议的执行过程：</h3><p><code>Gossip</code> 过程是由种子节点发起，当一个种子节点有状态需要更新到网络中的其他节点时，它会随机的选择周围几个节点散播消息，收到消息的节点也会重复该过程，直至最终网络中所有的节点都收到了消息。这个过程可能需要一定的时间，由于不能保证某个时刻所有节点都收到消息，但是理论上最终所有节点都会收到消息，因此它是一个最终一致性协议。</p><h4 id="Gossip-演示"><a href="#Gossip-演示" class="headerlink" title="Gossip 演示"></a>Gossip 演示</h4><p>现在，我们通过一个具体的实例来深入体会一下 Gossip 传播的完整过程</p><p><strong>为了表述清楚，我们先做一些前提设定</strong></p><p>1、Gossip 是周期性的散播消息，把周期限定为 1 秒</p><p>2、被感染节点随机选择 k 个邻接节点（fan-out）散播消息，这里把 fan-out 设置为 3，每次最多往 3 个节点散播。</p><p>3、每次散播消息都选择尚未发送过的节点进行散播</p><p>4、收到消息的节点不再往发送节点散播，比如 A -&gt; B，那么 B 进行散播的时候，不再发给 A。</p><p>这里一共有 16 个节点，节点 1 为初始被感染节点，通过 Gossip 过程，最终所有节点都被感染：</p><p> <img src="/images/2018/10/gossip.gif" alt=""></p><h3 id="Gossip-的特点（优势）"><a href="#Gossip-的特点（优势）" class="headerlink" title="Gossip 的特点（优势）"></a>Gossip 的特点（优势）</h3><p><strong>1）扩展性</strong></p><p>网络可以允许节点的任意增加和减少，新增加的节点的状态最终会与其他节点一致。</p><p><strong>2）容错</strong></p><p>网络中任何节点的宕机和重启都不会影响 Gossip 消息的传播，Gossip 协议具有天然的分布式系统容错特性。</p><p><strong>3）去中心化</strong></p><p>Gossip 协议不要求任何中心节点，所有节点都可以是对等的，任何一个节点无需知道整个网络状况，只要网络是连通的，任意一个节点就可以把消息散播到全网。</p><p><strong>4）一致性收敛</strong></p><p>Gossip 协议中的消息会以一传十、十传百一样的指数级速度在网络中快速传播，因此系统状态的不一致可以在很快的时间内收敛到一致。消息传播速度达到了 logN。</p><p><strong>5）简单</strong></p><p>Gossip 协议的过程极其简单，实现起来几乎没有太多复杂性。</p><p>Márk Jelasity 在它的 《Gossip》一书中对其进行了归纳：</p><p><img src="/images/2018/10/gossip_code.jpg" alt=""></p><h3 id="Gossip-的缺陷"><a href="#Gossip-的缺陷" class="headerlink" title="Gossip 的缺陷"></a>Gossip 的缺陷</h3><p>分布式网络中，没有一种完美的解决方案，Gossip 协议跟其他协议一样，也有一些不可避免的缺陷，主要是两个：</p><p><strong>1）消息的延迟</strong></p><p>由于 Gossip 协议中，节点只会随机向少数几个节点发送消息，消息最终是通过多个轮次的散播而到达全网的，因此使用 Gossip 协议会造成不可避免的消息延迟。不适合用在对实时性要求较高的场景下。</p><p><strong>2）消息冗余</strong></p><p>Gossip 协议规定，节点会定期随机选择周围节点发送消息，而收到消息的节点也会重复该步骤，因此就不可避免的存在消息重复发送给同一节点的情况，造成了消息的冗余，同时也增加了收到消息的节点的处理压力。而且，由于是定期发送，因此，即使收到了消息的节点还会反复收到重复消息，加重了消息的冗余。</p><h3 id="Gossip-类型"><a href="#Gossip-类型" class="headerlink" title="Gossip 类型"></a>Gossip 类型</h3><p>Gossip 有两种类型：</p><ul><li>Anti-Entropy（反熵）：以固定的概率传播所有的数据</li><li>Rumor-Mongering（谣言传播）：仅传播新到达的数据</li></ul><p>Anti-Entropy 是 SI model，节点只有两种状态，Suspective 和 Infective，叫做 simple epidemics。</p><p>Rumor-Mongering 是 SIR model，节点有三种状态，Suspective，Infective 和 Removed，叫做 complex epidemics。</p><p>其实，Anti-entropy 反熵是一个很奇怪的名词，之所以定义成这样，Jelasity 进行了解释，因为 entropy 是指混乱程度（disorder），而在这种模式下可以消除不同节点中数据的 disorder，因此 Anti-entropy 就是 anti-disorder。换句话说，它可以提高系统中节点之间的 similarity。</p><p>在 SI model 下，一个节点会把所有的数据都跟其他节点共享，以便消除节点之间数据的任何不一致，它可以保证最终、完全的一致。</p><p>由于在 SI model 下消息会不断反复的交换，因此消息数量是非常庞大的，无限制的（unbounded），这对一个系统来说是一个巨大的开销。</p><p>但是在 Rumor Mongering（SIR Model） 模型下，消息可以发送得更频繁，因为消息只包含最新 update，体积更小。而且，一个 Rumor 消息在某个时间点之后会被标记为 removed，并且不再被传播，因此，SIR model 下，系统有一定的概率会不一致。</p><p>而由于，SIR Model 下某个时间点之后消息不再传播，因此消息是有限的，系统开销小。</p><h3 id="Gossip-中的通信模式"><a href="#Gossip-中的通信模式" class="headerlink" title="Gossip 中的通信模式"></a>Gossip 中的通信模式</h3><p>在 Gossip 协议下，网络中两个节点之间有三种通信方式:</p><ul><li>Push: 节点 A 将数据 (key,value,version) 及对应的版本号推送给 B 节点，B 节点更新 A 中比自己新的数据</li><li>Pull：A 仅将数据 key, version 推送给 B，B 将本地比 A 新的数据（Key, value, version）推送给 A，A 更新本地</li><li>Push/Pull：与 Pull 类似，只是多了一步，A 再将本地比 B 新的数据推送给 B，B 则更新本地</li></ul><p>如果把两个节点数据同步一次定义为一个周期，则在一个周期内，Push 需通信 1 次，Pull 需 2 次，Push/Pull 则需 3 次。虽然消息数增加了，但从效果上来讲，Push/Pull 最好，理论上一个周期内可以使两个节点完全一致。直观上，Push/Pull 的收敛速度也是最快的。</p><h3 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h3><p>对于一个节点数为 N 的网络来说，假设每个 Gossip 周期，新感染的节点都能再感染至少一个新节点，那么 Gossip 协议退化成一个二叉树查找，经过 LogN 个周期之后，感染全网，时间开销是 O(LogN)。由于每个周期，每个节点都会至少发出一次消息，因此，消息复杂度（消息数量 = N * N）是 O(N^2) 。注意，这是 Gossip 理论上最优的收敛速度，但是在实际情况中，最优的收敛速度是很难达到的。</p><p>假设某个节点在第 i 个周期被感染的概率为 pi，第 i+1 个周期被感染的概率为 pi+1 ，</p><p>1）则 Pull 的方式:</p><p><img src="/images/2018/10/gossip1.jpg" alt=""></p><p>2）Push 方式：</p><p><img src="/images/2018/10/gossip2.jpg" alt=""></p><p>显然 Pull 的收敛速度大于 Push ，而每个节点在每个周期被感染的概率都是固定的 p (0&lt;p&lt;1)，因此 Gossip 算法是基于 p 的平方收敛，也称为概率收敛，这在众多的一致性算法中是非常独特的。</p><p>全文完！</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h3&gt;&lt;p&gt;&lt;code&gt;Gossip protocol&lt;/code&gt; 也叫 &lt;code&gt;Epidemic Protocol&lt;/code&gt; （流行病协议）
      
    
    </summary>
    
      <category term="架构" scheme="http://edisonxu.com/categories/%E6%9E%B6%E6%9E%84/"/>
    
      <category term="分布式" scheme="http://edisonxu.com/categories/%E6%9E%B6%E6%9E%84/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="基础知识" scheme="http://edisonxu.com/categories/%E6%9E%B6%E6%9E%84/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
    
    
      <category term="分布式" scheme="http://edisonxu.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="event sourcing" scheme="http://edisonxu.com/tags/event-sourcing/"/>
    
  </entry>
  
  <entry>
    <title>Vector Clock/Version Clock</title>
    <link href="http://edisonxu.com/2018/11/02/clocks.html"/>
    <id>http://edisonxu.com/2018/11/02/clocks.html</id>
    <published>2018-11-02T00:54:04.000Z</published>
    <updated>2021-07-21T13:31:21.783Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>研究Akka过程中，发现Akka的node间是<code>Peer-to-Peer</code>的，就意味着任何一个节点都能处理来自外部请求，那就存在一个一致性的问题，即如果集群中两个节点间的通讯断了，但是又都能对外提供服务，这时各自在接受请求造成的状态变化应该如何解决？Akka采用的是<code>Vector Clocks</code>来解决一致性的。这个算法常见于分布式存储上，对于EBA也很常见，所以如果要搞EventSourcing，这个知识可能会被用到。在研究<code>Vector Clocks</code>时发现两篇好文，本文是其中之一，讲的非常白话清晰，算是先讲算法基础概念，后一篇是直接讲算法实现了。另一篇讲简单实现的我就不转了，有兴趣的直接猛击<a href="https://www.cnblogs.com/foxmailed/archive/2012/01/11/2319854.html" target="_blank" rel="noopener">这里</a>跳转。</p></blockquote><h3 id="physical-clock"><a href="#physical-clock" class="headerlink" title="physical clock"></a>physical clock</h3><p>机器上的物理时钟，不同的机器在同一个时间点取到的<code>physical clock</code>不一样，之间会存在一定的误差，NTP可以用来控制这个误差，机器之间的时钟误差可以控制在几十ms以内。两个事件a和b，a在机器M1上<code>physical clock</code>为12点5分0秒6ms发生，b在机器M2上<code>physical clock</code>为12点5分0秒7ms发生，这并不代表a发生在b之前，因为两个机器上取到的<code>physical clock</code>和真实时间(这个时间就是国际标准时间UTC，可以通过原子钟，internet，卫星获得)之间都有误差。比如机器M1的<code>physical clock</code>比真实时间慢10ms，那么事件a实际上是在真实时间12点5分0秒16ms发生的，机器M2的<code>physical clock</code>比真实事件慢5ms，那么事件b的实际上是在真实时间12点5分0秒12ms发生的，显然，事件a发生在事件b之后。</p><h3 id="Lamport’s-Logical-Clock"><a href="#Lamport’s-Logical-Clock" class="headerlink" title="Lamport’s Logical Clock"></a>Lamport’s Logical Clock</h3><p>单机系统容易给发生的所有事件定义一个全局顺序(total order)，但是分布式系统没有全局时钟，很难给所有事件定义一个全局顺序。所以，Lamport定义了一种偏序关系，<code>happens-before</code>，记作 -&gt;</p><p>a-&gt;b意味着所有的进程都agree事件a发生在事件b之前。</p><p>在三种情况下，可以很容易的得到这个关系：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">1. 如果事件a和事件b是同一个进程中的并且事件a发生在事件b前面，那么a-&gt;b</span><br><span class="line"></span><br><span class="line">2. 如果进程A发送一条消息m给进程B，a代表进程A发送消息m的事件，b代表进程B接收消息m的事件，那么a-&gt;b(由于消息的传递需要时间)</span><br><span class="line"></span><br><span class="line">3. -&gt;满足传递性，如果a-&gt;b AND b-&gt;c =&gt; a-&gt;c</span><br></pre></td></tr></table></figure></p><p><code>Lamport&#39;s Logical Clock</code>算法如下：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">1. 每个机器本地维护一个logical clock LCi</span><br><span class="line"></span><br><span class="line">2. 每个机器本地每发生一个事件设置LCi = LCi + 1，并且把结果作为这个事件的logical clock。</span><br><span class="line"></span><br><span class="line">3. 当机器i给机器j发送消息m时，把LCi存在消息里。</span><br><span class="line"></span><br><span class="line">4. 当机器j收到消息m时候，LCj = max(LCj, m timestamp)+1，结果值作为收到消息m这个事件的时间戳。</span><br></pre></td></tr></table></figure></p><p>这个算法能够保证a-&gt;b，那么a事件的logical clock比b事件的logical clock小。反过来，通过只比较两个事件的logical clock不能得到a和b的先后。</p><h3 id="Vector-Clock"><a href="#Vector-Clock" class="headerlink" title="Vector Clock"></a>Vector Clock</h3><p>每个机器维护一个向量VC，也就是<code>Vector Clock</code>，这个向量VC有如下属性：</p><ul><li><p>VCi[i] 是到目前为止机器i上发生的事件的个数</p></li><li><p>VCi[k] 是机器i知道的机器k发生的事件的个数(即机器i对机器j的知识)</p></li></ul><p>每个机器都有一个向量(<code>Vector</code>)，每个向量中的元素都是一个<code>logical clock</code>，所以取名为<code>Vector Clock</code>。</p><p>通过如下算法更新<code>Vector Clock</code><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">1. 机器i本地发生一个事件时将VCi[i]加1</span><br><span class="line">2. 机器i给机器j发送消息m时，将整个VCi存在消息内</span><br><span class="line">3. 机器j收到消息m时，VCj[k]=max(VCj[k],VCi[k]),同时，VCj[j]+1</span><br></pre></td></tr></table></figure></p><p>可以看出，<code>Vector Clock</code>是一种maintain因果关系(<code>causality</code>)的一种手段，<code>Vector Clock</code>在机器之间传递达到给对方传递自己已有的关于其他机器知识的目的。</p><h3 id="Dynamo为什么需要Vector-Clock-实际上是Version-Clock"><a href="#Dynamo为什么需要Vector-Clock-实际上是Version-Clock" class="headerlink" title="Dynamo为什么需要Vector Clock(实际上是Version Clock)"></a>Dynamo为什么需要Vector Clock(实际上是Version Clock)</h3><p>Dynamo是一个分布式Key/Value存储系统，这个Value可以是一行，包含多个列， 为了容错，每个Key/Value保存多副本，通常在不同的机器上，一般是3，后面以3为例。对外是一个最终一致性系统，即客户端A写入一个值返回成功后，在一定的时间内另外一个客户端可能读不到最新的值。通常，成功写入两个副本成功即返回给客户端成功，同时请求会异步的同步到第三个副本。然而，高可用是Dynamo的主要设计目标之一，即使在出现网络分区或者机器宕机时依然可读可写。</p><p>假设Key K有三个副本k1,k2,k3分别在M1，M2，M3上。</p><p>正常情况<br>M1处理写请求，M1将请求发往M2，M3，只要有一个返回，即返回客户端成功。</p><p>网络分区<br>如果M1和M2/M3之间网络都不通，k1被更新(持续高可用，依然给客户端返回成功)，随后，其他节点(集群中任意一个节点都可以接客户端请求，并且将请求路由到正确的节点上)路由了写请求给M2(假设其他节点和M1/M3之间网络不通)，k2被更新。这时，k1和k2数据不一样，最后网络恢复，三个副本进行同步时，应该保留哪个版本？如果只保留k2，即采用last write win机制，那么同步后，第一个客户端会发现它写的数据丢了。</p><p>这个时候就需要Vector Clock，更确切的说是Version Clock。</p><p>为了处理这种场景，Dynamo使用Version Clock来捕获同一个Object的不同版本之间的causality。每个Object的每个版本会有一个相关联的Version Clock, 形如[(serverA,counter),(serverB,counter),…], 通过检查同一个Object不同版本的Version Clock，可以决定是否可以完全丢弃一个版本，仅保留另外一个版本，还是需要将两个版本进行merge。如果Object的版本A的VCA包含的每项(server, counter)在版本B的VCB中都有对应项，并且counter小于等于版本B中对应项的counter(记作VCB descends VCA)，那么这个Object的版本A可以被丢弃，否则需要对两个版本进行merge。</p><p>回到刚才的例子，k1被更新，<code>Version Clock</code>(注:此处假设k1/k2/k3三个副本之前一模一样，那么就可以省略之前的<code>Version Clock</code>)为[(M1,1)],k2被更新,<code>Version Clock</code>为[(M2,1)]，随后k1/k2网络通了，他们通过比较两个<code>Version Clock</code>发现两个<code>Version Clock</code>存在冲突，不是descends的关系，那么就两个版本都保留，当客户端来读Key K的时候，两个版本的数据和对应的VC都返回给客户端，由客户端进行冲突合并，客户端进行冲突合并后写入Key K的时候，带着合并后的VC[(M1, 1), [M2, 1]]发到M1/M2，覆盖服务器版本，冲突解决。</p><p>可以看出，<code>Vector Clock</code>最初是为了给分布式系统的事件定序发明的，本质上是一种捕获<code>causality</code>的手段，只是他们捕获的是事件的关系。而<code>Version Clock</code>是捕获同一个数据的不同版本之间的<code>causality</code>.</p><p>Riak这个系统也使用了<code>Vector Clock</code>来做冲突合并，对<code>Vector Clock</code>的用法可谓比较深入，具体可以看最后两篇参考资料。</p><h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><p><a href="http://s3.amazonaws.com/AllThingsDistributed/sosp/amazon-dynamo-sosp2007.pdf" target="_blank" rel="noopener">Dynamo</a></p><p><a href="http://haslab.uminho.pt/tome/files/dvvset-dais.pdf" target="_blank" rel="noopener">Scalable and Accurate Causality Tracking for Eventually Consistent Stores</a></p><p><a href="https://haslab.wordpress.com/2011/07/08/version-vectors-are-not-vector-clocks/" target="_blank" rel="noopener">version-vectors-are-not-vector-clocks</a></p><p><a href="http://www.bailis.org/blog/causality-is-expensive-and-what-to-do-about-it/" target="_blank" rel="noopener">Causality Is Expensive</a></p><p><a href="http://basho.com/posts/technical/vector-clocks-revisited/" target="_blank" rel="noopener">Vector Clocks Revisited</a></p><p><a href="http://basho.com/posts/technical/vector-clocks-revisited-part-2-dotted-version-vectors/" target="_blank" rel="noopener">vector-clocks-revisited-part-2-dotted-version-vectors</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;研究Akka过程中，发现Akka的node间是&lt;code&gt;Peer-to-Peer&lt;/code&gt;的，就意味着任何一个节点都能处理来自外部请求，那就存在一个一致性的问题，即如果集群中两个节点间的通讯断了，但是又都能对外提供服务，这时各自在接受请求造成
      
    
    </summary>
    
      <category term="架构" scheme="http://edisonxu.com/categories/%E6%9E%B6%E6%9E%84/"/>
    
      <category term="分布式" scheme="http://edisonxu.com/categories/%E6%9E%B6%E6%9E%84/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="基础知识" scheme="http://edisonxu.com/categories/%E6%9E%B6%E6%9E%84/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"/>
    
    
      <category term="分布式" scheme="http://edisonxu.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="event sourcing" scheme="http://edisonxu.com/tags/event-sourcing/"/>
    
  </entry>
  
  <entry>
    <title>Node.js的__dirname，__filename，process.cwd()，./的一些坑</title>
    <link href="http://edisonxu.com/2018/11/01/node-path.html"/>
    <id>http://edisonxu.com/2018/11/01/node-path.html</id>
    <published>2018-11-01T00:53:36.000Z</published>
    <updated>2021-07-21T13:31:21.785Z</updated>
    
    <content type="html"><![CDATA[<h3 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h3><p>原文收录在我的 GitHub博客 (<a href="https://github.com/jawil/blog" target="_blank" rel="noopener">https://github.com/jawil/blog</a>) ，喜欢的可以关注最新动态，大家一起多交流学习，共同进步，以学习者的身份写博客，记录点滴。</p><p>最近在学习<code>Node.js</code>里面的<code>fs</code>模块，遇到了一个比较诡异的现象，踩到了坑，就是读取当前目录下的一个文件，死活读取不到，由于之前对于<code>Node.js</code>里面的<code>path</code>模块也不太熟悉，也没系统研究过，所以今天就踩了这个坑，记录踩坑的过程，防止以后踩坑和大家也踩坑。</p><p>说一下当时的情形：<br><a href="https://camo.githubusercontent.com/af6075c29e4098cf8acc29f01fde39a18255d4d0/687474703a2f2f6f70656f6b6634756b2e626b742e636c6f7564646e2e636f6d2f515132303137303531302d3138313433372e706e67" target="_blank" rel="noopener"><img src="https://camo.githubusercontent.com/af6075c29e4098cf8acc29f01fde39a18255d4d0/687474703a2f2f6f70656f6b6634756b2e626b742e636c6f7564646e2e636f6d2f515132303137303531302d3138313433372e706e67" alt="QQ20170510-181437"></a></p><p>我纳闷的很半天，我明明就是读取当前目录下的<code>1.findLargest.js</code>，为什么提示找不到这个文件，运行了几遍，死活找不到<code>1.findLargest.js</code>这个文件。</p><p>后来才发现是因为运行这个文件不是从当前目录运行了，从图中可以看出，当前的目录是<code>/Users/jawil/Desktop/nodejs/demo/ES6-lottery/syntax/nodejs</code>，而我运行这个脚本的目录是<code>/Users/jawil/Desktop/nodejs/demo/ES6-lottery</code>；这就是问题的所在了。不过为什么运行脚本的位置也会影响这个路径呢，且往下看。</p><h3 id="探索"><a href="#探索" class="headerlink" title="探索"></a>探索</h3><p>计算机不会欺骗人，一切按照规则执行，说找不到这个文件，那肯定就是真的找不到，至于为什么找不到，那就是因为我们理解有偏差，我最初理解的’./‘是当前执行<code>js</code>文件所在的文件夹的绝对路径，然后<code>Node.js</code>的理解却不是这样的，我们慢慢往下看。</p><p><code>Node.js</code>中的文件路径大概有 <code>__dirname</code>, <code>__filename</code>, <code>process.cwd()</code>, <code>./</code> 或者 <code>../</code>，前三个都是绝对路径，为了便于比较，<code>./</code> 和 <code>../</code> 我们通过 <code>path.resolve(&#39;./&#39;)</code>来转换为绝对路径。</p><p>简单说一下这几个路径的意思:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">__dirname：    获得当前执行文件所在目录的完整目录名</span><br><span class="line">__filename：   获得当前执行文件的带有完整绝对路径的文件名</span><br><span class="line">process.cwd()：获得当前执行node命令时候的文件夹目录名 </span><br><span class="line">./：           文件所在目录</span><br></pre></td></tr></table></figure></p><p>先看一看我电脑当前的目录结构：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">syntax/</span><br><span class="line">    -nodejs/</span><br><span class="line">        -1.findLargest.js</span><br><span class="line">        -2.path.js</span><br><span class="line">        -3.fs.js</span><br><span class="line">    -regs</span><br><span class="line">        -regx.js</span><br><span class="line">        -test.txt</span><br></pre></td></tr></table></figure></p><p>在 path.js 里面我们写这些代码，看看输出是什么：<br><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">'path'</span>)</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">'__dirname：'</span>, __dirname)</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">'__filename：'</span>, __filename)</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">'process.cwd()：'</span>, process.cwd())</span><br><span class="line"><span class="built_in">console</span>.log(<span class="string">'./：'</span>, path.resolve(<span class="string">'./'</span>))</span><br></pre></td></tr></table></figure></p><p>在当前目录下也就是<code>nodejs</code>目录运行 <code>node path.js</code>，我们看看输出结果：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">__dirname：     /Users/jawil/Desktop/nodejs/demo/ES6-lottery/syntax/nodejs</span><br><span class="line">__filename：    /Users/jawil/Desktop/nodejs/demo/ES6-lottery/syntax/nodejs/2.path.js</span><br><span class="line">process.cwd()： /Users/jawil/Desktop/nodejs/demo/ES6-lottery/syntax/nodejs</span><br><span class="line">./：            /Users/jawil/Desktop/nodejs/demo/ES6-lottery/syntax/nodejs</span><br></pre></td></tr></table></figure></p><p><a href="https://camo.githubusercontent.com/a78b391d9a2dde7843e824bfb72179d23f8f6c86/687474703a2f2f6f70656f6b6634756b2e626b742e636c6f7564646e2e636f6d2f515132303137303531302d3138333731322e706e67" target="_blank" rel="noopener"><img src="https://camo.githubusercontent.com/a78b391d9a2dde7843e824bfb72179d23f8f6c86/687474703a2f2f6f70656f6b6634756b2e626b742e636c6f7564646e2e636f6d2f515132303137303531302d3138333731322e706e67" alt="QQ20170510-183712"></a></p><p>然后在 <code>项目根目录ES6-lottery</code> 目录下运行 <code>node syntax/nodejs/2.path.js</code>，我们再来看看输出结果：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">__dirname：     /Users/jawil/Desktop/nodejs/demo/ES6-lottery/syntax/nodejs</span><br><span class="line">__filename：    /Users/jawil/Desktop/nodejs/demo/ES6-lottery/syntax/nodejs/2.path.js</span><br><span class="line">process.cwd()： /Users/jawil/Desktop/nodejs/demo/ES6-lottery</span><br><span class="line">./：            /Users/jawil/Desktop/nodejs/demo/ES6-lottery</span><br></pre></td></tr></table></figure></p><p><a href="https://camo.githubusercontent.com/206c3e0472c59f9b8c04b5af82fccd8916bf4528/687474703a2f2f6f70656f6b6634756b2e626b742e636c6f7564646e2e636f6d2f515132303137303531302d3138343934332e706e67" target="_blank" rel="noopener"><img src="https://camo.githubusercontent.com/206c3e0472c59f9b8c04b5af82fccd8916bf4528/687474703a2f2f6f70656f6b6634756b2e626b742e636c6f7564646e2e636f6d2f515132303137303531302d3138343934332e706e67" alt="QQ20170510-184943"></a></p><p>答案显而易见？我们可以通过上面的例子对比，暂时得出表面的结论：</p><blockquote><ul><li>__dirname: 总是返回被执行的 js 所在文件夹的绝对路径</li><li>__filename: 总是返回被执行的 js 的绝对路径</li><li>process.cwd(): 总是返回运行 node 命令时所在的文件夹的绝对路径</li><li>./: 跟 process.cwd() 一样，返回 node 命令时所在的文件夹的绝对路径</li></ul></blockquote><p>但是，我们再来看看这个例子，我们在上面的例子加几句代码，然后：</p><p>我们在<code>1.findLargest.js</code>先加这句代码</p><p>exports.A = 1;</p><p>再来在刚才报错的<code>3.fs.js</code>里面加这两句代码看看：</p><p>const test = require(‘./1.findLargest.js’);</p><p>console.log(test)</p><p>运行<code>node syntax/nodejs/3.fs.js</code>，最后看看结果：</p><p><a href="https://camo.githubusercontent.com/862776452b5c9c097777d1b0cf8e385e01d98d29/687474703a2f2f6f70656f6b6634756b2e626b742e636c6f7564646e2e636f6d2f515132303137303531302d3138353534392e706e67" target="_blank" rel="noopener"><img src="https://camo.githubusercontent.com/862776452b5c9c097777d1b0cf8e385e01d98d29/687474703a2f2f6f70656f6b6634756b2e626b742e636c6f7564646e2e636f6d2f515132303137303531302d3138353534392e706e67" alt="QQ20170510-185549"></a></p><h3 id="再次疑惑"><a href="#再次疑惑" class="headerlink" title="再次疑惑"></a>再次疑惑</h3><p>为什么都是读取<code>./1.findLargest.js</code>文件，一样的路径，为什么<code>require</code>能获取到，而<code>readFile</code>读取不到呢？</p><p>于是查了不少资料，看到了一些关于<code>require</code>引入模块的机制，从中学到了不少，也明白了为什么是这样。</p><p>我们先了解一下<code>require()</code> 的基本用法：</p><p>下面的内容来自<a href="http://www.ruanyifeng.com/blog/2015/05/require.html" target="_blank" rel="noopener">require() 源码解读</a>,由阮一峰翻译自《Node使用手册》。</p><p><img src="/images/2018/10/node-path.png" alt=""></p><p>我们从第（2）小条的a部分可以看出：</p><pre><code>（2）如果 X 以 &quot;./&quot; 或者 &quot;/&quot; 或者 &quot;../&quot; 开头 　　a. 根据 X 所在的父模块，确定 X 的绝对路径。　　b. 将 X 当成文件，依次查找下面文件，只要其中有一个存在，就返回该文件，不再继续执行。</code></pre><p><code>const test = require(&#39;./1.findLargest.js&#39;)</code>按照上面规则翻译一遍就是:</p><ol><li><p>根据<code>1.findLargest.js</code>所在的父模块，确定<code>1.findLargest.js</code>的绝对路径为<code>/Users/jawil/Desktop/nodejs/demo/ES6-lottery/syntax/nodejs</code>，关于其中的寻找细节这里不做探讨。</p></li><li><p>先把<code>1.findLargest.js</code>当成文件，依次查找当前目录下的<code>1.findLargest.js</code>，找到了，就返回该文件，不再继续执行。</p></li></ol><p>根据<code>require</code>的基本规则，对于上面出现的情形也就不足为奇了，更多<code>require</code>的机制和源码解读，请移步：<br><strong><a href="http://www.ruanyifeng.com/blog/2015/05/require.html" target="_blank" rel="noopener">require() 源码解读</a></strong>。</p><p><strong>那么关于 <code>./</code> 正确的结论是：</strong><br>在 <code>require()</code> 中使用是跟 <code>__dirname</code> 的效果相同，不会因为启动脚本的目录不一样而改变，在其他情况下跟 <code>process.cwd()</code> 效果相同，是相对于启动脚本所在目录的路径。</p><h3 id="总结："><a href="#总结：" class="headerlink" title="总结："></a>总结：</h3><blockquote><ul><li>__dirname： 获得当前执行文件所在目录的完整目录名</li><li>__filename： 获得当前执行文件的带有完整绝对路径的文件名</li><li>process.cwd()：获得当前执行node命令时候的文件夹目录名</li><li>./： 不使用require时候，./与process.cwd()一样，使用require时候，与__dirname一样</li></ul></blockquote><p>只有在 <code>require()</code> 时才使用相对路径<code>(./, ../)</code> 的写法，其他地方一律使用绝对路径，如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 当前目录下</span></span><br><span class="line"> path.dirname(__filename) + <span class="string">'/path.js'</span>; </span><br><span class="line"><span class="comment">// 相邻目录下</span></span><br><span class="line"> path.resolve(__dirname, <span class="string">'../regx/regx.js'</span>);</span><br></pre></td></tr></table></figure><p>最后看看改过之后的结果，不会报错找不到文件了，不管在哪里执行这个脚本文件，都不会出错了，防止以后踩坑。</p><p><a href="https://camo.githubusercontent.com/98dc5f0fd87a4fa1a387566124e5afa086d29b15/687474703a2f2f6f70656f6b6634756b2e626b742e636c6f7564646e2e636f6d2f515132303137303531302d3139333630342e706e67" target="_blank" rel="noopener"><img src="https://camo.githubusercontent.com/98dc5f0fd87a4fa1a387566124e5afa086d29b15/687474703a2f2f6f70656f6b6634756b2e626b742e636c6f7564646e2e636f6d2f515132303137303531302d3139333630342e706e67" alt="QQ20170510-193604"></a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;起因&quot;&gt;&lt;a href=&quot;#起因&quot; class=&quot;headerlink&quot; title=&quot;起因&quot;&gt;&lt;/a&gt;起因&lt;/h3&gt;&lt;p&gt;原文收录在我的 GitHub博客 (&lt;a href=&quot;https://github.com/jawil/blog&quot; target=&quot;_bla
      
    
    </summary>
    
      <category term="js" scheme="http://edisonxu.com/categories/js/"/>
    
      <category term="nodejs" scheme="http://edisonxu.com/categories/js/nodejs/"/>
    
    
      <category term="nodejs" scheme="http://edisonxu.com/tags/nodejs/"/>
    
      <category term="js" scheme="http://edisonxu.com/tags/js/"/>
    
  </entry>
  
  <entry>
    <title>自动初始化Gitalk评论</title>
    <link href="http://edisonxu.com/2018/10/31/gitalk-auto-init.html"/>
    <id>http://edisonxu.com/2018/10/31/gitalk-auto-init.html</id>
    <published>2018-10-31T09:52:58.000Z</published>
    <updated>2021-07-21T13:31:21.783Z</updated>
    
    <content type="html"><![CDATA[<p>最近重新将博客整理了一下，之前忙的都没有时间取打理。其中一下是用<a href="https://github.com/gitalk/gitalk/blob/master/readme-cn.md" target="_blank" rel="noopener">Gitalk</a>更换原来的评论系统。<br><code>Gitalk</code>是利用了<a href="https://developer.github.com/v3/" target="_blank" rel="noopener">GithubAPI</a>，将网站的评论转写到<code>Github</code>上指定仓库的<code>Issues</code>里，相当于做了一个代理。风格做的简约漂亮，但每种不足的是每次发表了新博文后，需要自己用管理员账号登陆下评论系统，否则就会：<br><img src="/images/2018/10/not-init.png" alt=""></p><p>所以就萌生了写个<code>nodejs</code>代码去代替人工初始化的想法。</p><h3 id="Gitalk安装"><a href="#Gitalk安装" class="headerlink" title="Gitalk安装"></a>Gitalk安装</h3><p>总体来说，使用还是比较简单的，具体步骤请参阅官网，这里就不再赘述了。重点讲下配置。<br>首先，是Github上申请OAuth App的配置<br><img src="/images/2018/10/github-oauth-app.png" alt=""></p><p>然后，是Gitalk的配置<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> gitalk = <span class="keyword">new</span> Gitalk(&#123;</span><br><span class="line">  clientID: <span class="string">'xxxxxxxxxxxxxx'</span>,</span><br><span class="line">  clientSecret: <span class="string">'xxxxxxxxxxxxxxxx'</span>,</span><br><span class="line">  repo: <span class="string">'GitHub repo'</span>,</span><br><span class="line">  owner: <span class="string">'GitHub repo owner'</span>,</span><br><span class="line">  admin: [<span class="string">'GitHub repo owner and collaborators, only these guys can initialize github issues'</span>],</span><br><span class="line">  id: md5(<span class="built_in">window</span>.location.pathname),      <span class="comment">// Ensure uniqueness and length less than 50</span></span><br><span class="line">  distractionFreeMode: <span class="literal">false</span>  <span class="comment">// Facebook-like distraction free mode</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure></p><p>这里，重点有三个地方：</p><ul><li><code>owenr</code>是Github上的ID。有些人(比如我)，登陆时习惯用邮箱，放这里就不行了。一定得是自己的ID!</li><li><code>repo</code>，要填的是用来存储评论的Issues所对应的仓库的名字，记住后面不要有<code>.git</code>！</li><li>配置中的<code>id</code>，是唯一表示当前评论页面的主键。<code>Gitalk</code>用它作为Github Issue的<code>Label</code>，而Github对<code>Label</code>的长度有限制，最多50个字符，所以这里比较好的解决方案是对<code>pathname</code>做MD5，以保证长度和唯一性。<br>需要引入md5的js，可以使用<a href="https://github.com/blueimp/JavaScript-MD5/blob/master/js/md5.min.js" target="_blank" rel="noopener">JavaScript-MD5</a>.</li></ul><h3 id="申请Personal-Access-Token"><a href="#申请Personal-Access-Token" class="headerlink" title="申请Personal Access Token"></a>申请Personal Access Token</h3><p>GithubAPI对一些接口有调用限制，具体可以查看<a href="https://developer.github.com/v3/#rate-limiting" target="_blank" rel="noopener">Rate limiting</a>的解释。对于不同的认证方式，调用限制不同。<br><br>Gitalk使用的是Github的OAuth认证，请求时必须要有<code>clientID</code>和<code>clientSecret</code>，这种方式每小时的<code>Rate limiting</code>是<strong>60</strong>。如果只是发表评论，肯定是够了。<br>而如果要批量创建所有文章对应的Issue来作为这些文章的评论存储，可能就未必够了。<br><br>好在Github提供了另一种认证方式——Personal access token，这种方式每小时的限制高达<strong>5000</strong>次。所以，第一步就是申请这个token。</p><p>从Github的<a href="https://github.com/settings/tokens" target="_blank" rel="noopener">Personal access token</a>页面，点击<a href="https://github.com/settings/tokens/new" target="_blank" rel="noopener">Generate new token</a>。<br><img src="/images/2018/10/pat-apply.png" alt=""></p><p>创建完成后，获得一个Token。</p><h3 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h3><p>我用NodeJs写了个简单的批量刷新工具，是基于网站的sitemap的。如果你的网站没有sitemap，可以自行修改。</p><h3 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">npm install sitemapper -S</span><br><span class="line">npm install cheerio -S</span><br></pre></td></tr></table></figure><h3 id="NodeJS文件"><a href="#NodeJS文件" class="headerlink" title="NodeJS文件"></a>NodeJS文件</h3><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> request = <span class="built_in">require</span>(<span class="string">'request'</span>);</span><br><span class="line"><span class="keyword">var</span> Sitemapper = <span class="built_in">require</span>(<span class="string">'sitemapper'</span>);</span><br><span class="line"><span class="keyword">var</span> cheerio = <span class="built_in">require</span>(<span class="string">'cheerio'</span>);</span><br><span class="line"><span class="keyword">var</span> crypto = <span class="built_in">require</span>(<span class="string">'crypto'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 配置信息</span></span><br><span class="line"><span class="keyword">const</span> username = <span class="string">"EdisonXu"</span> <span class="comment">//github账号，对应Gitalk配置中的owner</span></span><br><span class="line"><span class="keyword">const</span> repo_name = <span class="string">"repo"</span> <span class="comment">//用于存储Issue的仓库名，对应Gitalk配置中的repo</span></span><br><span class="line"><span class="keyword">const</span> token = <span class="string">"xxxxxxxxxxxxxxxxxxxxxxxxxxx"</span>   <span class="comment">//前面申请的personal access token</span></span><br><span class="line"><span class="keyword">const</span> sitemap_url = <span class="string">"http://edisonxu.com/sitemap.xml"</span>  <span class="comment">// 自己站点的sitemap地址</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> base_url = <span class="string">"https://api.github.com/repos/"</span>+ username + <span class="string">"/"</span> + repo_name + <span class="string">"/issues"</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> sitemap = <span class="keyword">new</span> Sitemapper();</span><br><span class="line"></span><br><span class="line">sitemap.fetch(sitemap_url)</span><br><span class="line">    .then(<span class="function"><span class="keyword">function</span> (<span class="params">sites</span>) </span>&#123;</span><br><span class="line">        sites.sites.forEach(<span class="function"><span class="keyword">function</span> (<span class="params">site, index</span>) </span>&#123;</span><br><span class="line">            <span class="keyword">if</span> (site.endsWith(<span class="string">'404.html'</span>)) &#123;</span><br><span class="line">                <span class="built_in">console</span>.log(<span class="string">'跳过404'</span>)</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            &#125;</span><br><span class="line">            request(&#123;</span><br><span class="line">                url: site,</span><br><span class="line">                headers: &#123;</span><br><span class="line">                    <span class="string">'Content-Type'</span>: <span class="string">'application/json;charset=UTF-8'</span></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;, <span class="function"><span class="keyword">function</span> (<span class="params">err, resp, bd</span>) </span>&#123;</span><br><span class="line">                <span class="keyword">if</span> (err || resp.statusCode != <span class="number">200</span>)</span><br><span class="line">                    <span class="keyword">return</span></span><br><span class="line">                <span class="keyword">const</span> $ = cheerio.load(bd);</span><br><span class="line">                <span class="keyword">var</span> title = $(<span class="string">'title'</span>).text();</span><br><span class="line">                <span class="keyword">var</span> desc = site + <span class="string">"\n\n"</span> + $(<span class="string">"meta[name='description']"</span>).attr(<span class="string">"content"</span>);</span><br><span class="line">                <span class="keyword">var</span> path = site.split(<span class="string">".com"</span>)[<span class="number">1</span>]</span><br><span class="line">                <span class="keyword">var</span> md5 = crypto.createHash(<span class="string">'md5'</span>);</span><br><span class="line">                <span class="keyword">var</span> label = md5.update(path).digest(<span class="string">'hex'</span>)</span><br><span class="line">                <span class="keyword">var</span> options = &#123;</span><br><span class="line">                    headers: &#123;</span><br><span class="line">                        <span class="string">'Authorization'</span>: <span class="string">'token '</span>+token,</span><br><span class="line">                        <span class="string">'User-Agent'</span>: <span class="string">'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36'</span>,</span><br><span class="line">                        <span class="string">'Accept'</span>: <span class="string">'application/json'</span></span><br><span class="line">                    &#125;,</span><br><span class="line">                    url: base_url+ <span class="string">"?labels="</span>+<span class="string">"Gitalk,"</span> + label,</span><br><span class="line">                    method: <span class="string">'GET'</span></span><br><span class="line">                &#125;</span><br><span class="line">                <span class="comment">// 检查issue是否被初始化过</span></span><br><span class="line">                request(options, <span class="function"><span class="keyword">function</span> (<span class="params">error, response, body</span>) </span>&#123;</span><br><span class="line">                    <span class="keyword">if</span> (error || response.statusCode != <span class="number">200</span>) &#123;</span><br><span class="line">                        <span class="built_in">console</span>.log(<span class="string">'检查['</span>+site+<span class="string">']对应评论异常'</span>)</span><br><span class="line">                        <span class="keyword">return</span></span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="keyword">var</span> jbody = <span class="built_in">JSON</span>.parse(body)</span><br><span class="line">                    <span class="keyword">if</span>(jbody.length&gt;<span class="number">0</span>)</span><br><span class="line">                        <span class="keyword">return</span></span><br><span class="line">                    <span class="comment">//创建issue</span></span><br><span class="line">                    <span class="keyword">var</span> request_body = &#123;<span class="string">"title"</span>: title, <span class="string">"labels"</span>: [<span class="string">"Gitalk"</span>, label], <span class="string">"body"</span>: desc&#125;</span><br><span class="line">                    <span class="comment">//console.log("创建内容： "+JSON.stringify(request_body));</span></span><br><span class="line">                    <span class="keyword">var</span> create_options = &#123;</span><br><span class="line">                        headers: &#123;</span><br><span class="line">                        <span class="string">'Authorization'</span>: <span class="string">'token '</span>+token,</span><br><span class="line">                        <span class="string">'User-Agent'</span>: <span class="string">'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36'</span>,</span><br><span class="line">                        <span class="string">'Accept'</span>: <span class="string">'application/json'</span>,</span><br><span class="line">                        <span class="string">'Content-Type'</span>: <span class="string">'application/json;charset=UTF-8'</span></span><br><span class="line">                        &#125;,</span><br><span class="line">                        url: base_url,</span><br><span class="line">                        body: <span class="built_in">JSON</span>.stringify(request_body),</span><br><span class="line">                        method: <span class="string">'POST'</span></span><br><span class="line">                    &#125;</span><br><span class="line">                    request(create_options, <span class="function"><span class="keyword">function</span>(<span class="params">error, response, body</span>)</span>&#123;</span><br><span class="line">                        <span class="keyword">if</span> (!error &amp;&amp; response.statusCode == <span class="number">201</span>) </span><br><span class="line">                            <span class="built_in">console</span>.log(<span class="string">"地址: ["</span>+site+<span class="string">"] Gitalk初始化成功"</span>)</span><br><span class="line">                    &#125;)</span><br><span class="line">                &#125;);</span><br><span class="line">            &#125;);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;)</span><br><span class="line">    .catch(<span class="function"><span class="keyword">function</span> (<span class="params">err</span>) </span>&#123;</span><br><span class="line">        <span class="built_in">console</span>.log(err);</span><br><span class="line">    &#125;);</span><br></pre></td></tr></table></figure><blockquote><p>注意<br>代码里默认将<code>path</code>做了MD5化，如果你的Gitalk配置里没有使用MD5后的值作为ID，这里要相应修改！</p></blockquote><h3 id="Gulp调用"><a href="#Gulp调用" class="headerlink" title="Gulp调用"></a>Gulp调用</h3><p>我的博客是Hexo+Gulp，所以我把Gitalk初始化也做到了Gulp里，这样每次写完文章压缩、发布、初始化评论一条龙。用法比较简单：<br><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> $ = <span class="built_in">require</span>(<span class="string">'gulp-load-plugins'</span>)();</span><br><span class="line">gulp.task(<span class="string">'init-gitalk'</span>, [<span class="string">'deploy'</span>], $.shell.task(<span class="string">'node init_gitalk.js'</span>));</span><br></pre></td></tr></table></figure></p><p>最后将<code>init-gitalk</code>的task自己加到你的gulp任务序列去就好了。<br>由于我博客的配置文件里已经有了username和sitemap地址，另外gitalk配置和上门代码的配置也有部分重复，所以我又添加了读取配置的功能，有需要的自行挑选。</p><h3 id="添加依赖"><a href="#添加依赖" class="headerlink" title="添加依赖"></a>添加依赖</h3><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install js-yaml -S</span><br></pre></td></tr></table></figure><h3 id="配置文件中增加"><a href="#配置文件中增加" class="headerlink" title="配置文件中增加"></a>配置文件中增加</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">gitalk:</span></span><br><span class="line"><span class="attr">  githubId:</span> <span class="string">xxx</span></span><br><span class="line"><span class="attr">  repo:</span> <span class="string">xxxxxxxxxx</span></span><br><span class="line"><span class="attr">  owner:</span> <span class="string">xxxxxxxxxxxxx</span></span><br><span class="line"><span class="attr">  token:</span> <span class="string">xxxxxxxxxxxxxx</span></span><br></pre></td></tr></table></figure><h3 id="修改脚本"><a href="#修改脚本" class="headerlink" title="修改脚本"></a>修改脚本</h3><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> yml = <span class="built_in">require</span>(<span class="string">'js-yaml'</span>);</span><br><span class="line"><span class="keyword">var</span> fs = <span class="built_in">require</span>(<span class="string">'fs'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">//读取配置文件</span></span><br><span class="line"><span class="keyword">var</span> file = process.cwd()+<span class="string">"\\_config.yml"</span>;</span><br><span class="line"><span class="keyword">var</span> config = yml.safeLoad(fs.readFileSync(file, <span class="string">'utf8'</span>))</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> username = config.gitalk.githubId</span><br><span class="line"><span class="keyword">const</span> repo_name = config.gitalk.repo</span><br><span class="line"><span class="keyword">const</span> token = config.gitalk.token</span><br><span class="line"><span class="keyword">const</span> sitemap_url = config.url+<span class="string">"/"</span>+config.sitemap.path</span><br></pre></td></tr></table></figure><p>至此，Gitalk自动初始化就完成了，再也不要手动初始化了。<code>NodeJS</code>非我之长，所以写不出”信达雅”，只保证能用，起个抛砖引玉的效果。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;最近重新将博客整理了一下，之前忙的都没有时间取打理。其中一下是用&lt;a href=&quot;https://github.com/gitalk/gitalk/blob/master/readme-cn.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Gitalk
      
    
    </summary>
    
      <category term="博客" scheme="http://edisonxu.com/categories/%E5%8D%9A%E5%AE%A2/"/>
    
    
      <category term="Gitalk" scheme="http://edisonxu.com/tags/Gitalk/"/>
    
      <category term="blog" scheme="http://edisonxu.com/tags/blog/"/>
    
      <category term="nodejs" scheme="http://edisonxu.com/tags/nodejs/"/>
    
      <category term="gulp" scheme="http://edisonxu.com/tags/gulp/"/>
    
  </entry>
  
  <entry>
    <title>Akka入门系列(三)：remote Actor</title>
    <link href="http://edisonxu.com/2018/10/30/akka-remote-actor.html"/>
    <id>http://edisonxu.com/2018/10/30/akka-remote-actor.html</id>
    <published>2018-10-30T01:04:22.000Z</published>
    <updated>2021-07-21T13:31:21.781Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>虽然<code>Akka</code>在单机上可以运行上百万的<code>Actor</code>，但出于容错、负载均衡、灰度发布、提高并行度等等原因，我们仍然需要能在多个不同的服务器上运行<code>Actor</code>。所以Akka提供了<code>akka-remoting</code>的扩展包，屏蔽底层网络传输的细节，让上层以及其简单的方式使用远程的<code>Actor</code>调度。<br>官方文档：<a href="https://doc.akka.io/docs/akka/current/remoting.html" target="_blank" rel="noopener">https://doc.akka.io/docs/akka/current/remoting.html</a></p></blockquote><h2 id="适用场景"><a href="#适用场景" class="headerlink" title="适用场景"></a>适用场景</h2><p><code>remoting</code>的存在其实是为<code>akka cluster</code>做底层支持的，通常并不会直接去使用remoting的包。但为了了解<code>cluster</code>的底层原理，还是有必要看下<code>remoting</code>。<br>同时，<code>remoting</code>被设计为<code>Peer-to-Peer</code>而非<code>Client-Server</code>，所以不适用于基于后者的系统开发，比如我们无法在一个provider为local的Actor里去查找一个<code>remote actor</code>发送消息，必须两者均为<code>remote actor</code>，才满足对等。</p><h2 id="设计"><a href="#设计" class="headerlink" title="设计"></a>设计</h2><p><code>Akka</code>的所有设计，都是考虑了分布式的：所有<code>Actor</code>的交互都是基于事件，所有的操作都是异步的。<br>更多设计信息，请参考<a href="https://doc.akka.io/docs/akka/current/general/remoting.html#location-transparency" target="_blank" rel="noopener">Remote设计</a>，还是会获益良多。<br>原文中有一句话</p><blockquote><p>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. <strong><em>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</em>.</strong> </p></blockquote><p>后面这半句看的不是很懂，希望有理解的朋友回复交流。</p><h2 id="基本例子"><a href="#基本例子" class="headerlink" title="基本例子"></a>基本例子</h2><p><code>Akka</code>将<code>remoting</code>完全配置化了，使用时几乎只需要修改配置文件，除非自定义，否则不需要动一行代码。<br><code>remoting</code>包提供了两个功能：</p><ul><li>查找一个已存在的远程Actor</li><li>在指定的远程路径上创建一个远程Actor</li></ul><h3 id="添加依赖"><a href="#添加依赖" class="headerlink" title="添加依赖"></a>添加依赖</h3><p>在引入akka actor的基本依赖(请看前文)后，再加上remoting的依赖<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.typesafe.akka<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-remote_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.5.17<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></p><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><p>在一个<code>Akka</code>项目中启用<code>remote</code>功能的话，最基本需要在<code>application.conf</code>（Akka默认的配置文件名）中启用如下配置:<br><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">akka</span> <span class="string">&#123;</span></span><br><span class="line">  <span class="string">actor</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">provider</span> <span class="string">=</span> <span class="string">remote</span></span><br><span class="line">  <span class="string">&#125;</span></span><br><span class="line">  <span class="string">remote</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">enabled-transports</span> <span class="string">=</span> <span class="string">["akka.remote.netty.tcp"]</span></span><br><span class="line">    <span class="string">netty.tcp</span> <span class="string">&#123;</span></span><br><span class="line">      <span class="string">hostname</span> <span class="string">=</span> <span class="string">"127.0.0.1"</span></span><br><span class="line">      <span class="string">port</span> <span class="string">=</span> <span class="number">2552</span></span><br><span class="line">    <span class="string">&#125;</span></span><br><span class="line"> <span class="string">&#125;</span></span><br><span class="line"><span class="string">&#125;</span></span><br></pre></td></tr></table></figure></p><p>基本配置包含如下四点：</p><ul><li><code>provider</code>从<code>local</code>变成<code>remote</code></li><li><code>enabled-transports</code>指定传输的实现</li><li><code>hostname</code> 指定当前<code>Actor</code>底层网络监听组件所需监听的主机名，如果不指定，默认会调用InetAddress.getLocalHost().getHostAddress()来获取当前主机的IP</li><li><code>port</code> 指定当前<code>Actor</code>底层网络监听组件所需监听的端口，<strong>如果设置为0，则会生成一个随机的端口</strong></li></ul><p><strong>由于要测试下本地去寻找远程actor，所以本文的代码例子中，用<code>remote.conf</code>作为配置文件名</strong></p><blockquote><p>注意<br>如果在同一个主机上启动多个远程Actor，那么<code>port</code>一定要不同。因为远程Actor的底层会启动一个网络监控组件，该组件会去监听指定IP或域名的指定端口。如果都相同，肯定会有一个绑定失败。</p></blockquote><h3 id="查找一个远程Actor"><a href="#查找一个远程Actor" class="headerlink" title="查找一个远程Actor"></a>查找一个远程Actor</h3><p>我们创建一个远程Actor，一会儿去查找它。注意，这里加载了remote.conf，但覆盖了端口为2551，目的是在本地模拟一个远端的Actor。如果觉得在本地起不好理解，就可以找一台服务器，把<code>akka.remote.netty.tcp.hostname</code>也覆盖掉换成服务器的IP，或者干脆另起一个配置文件。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ToFindRemoteActor</span> <span class="keyword">extends</span> <span class="title">AbstractActor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    LoggingAdapter log = Logging.getLogger(getContext().system(), <span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">preStart</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        log.info(<span class="string">"ToFindRemoteActor is starting"</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Receive <span class="title">createReceive</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> receiveBuilder()</span><br><span class="line">                .match(String.class, msg-&gt;&#123;</span><br><span class="line">                    log.info(<span class="string">"Msg received: &#123;&#125;"</span>, msg);</span><br><span class="line">                &#125;)</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        Config config = ConfigFactory.parseString(</span><br><span class="line">                <span class="string">"akka.remote.netty.tcp.port="</span> + <span class="number">2551</span>)</span><br><span class="line">                .withFallback(ConfigFactory.load(<span class="string">"remote.conf"</span>));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Create an Akka system</span></span><br><span class="line">        ActorSystem system = ActorSystem.create(<span class="string">"sys"</span>, config);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Create an actor</span></span><br><span class="line">        system.actorOf(Props.create(ToFindRemoteActor.class), <span class="string">"toFind"</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>启动后，可以看到控制台有日志打印出来：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">[INFO] [10/26/2018 11:54:33.684] [main] [akka.remote.Remoting] Starting remoting</span><br><span class="line">[INFO] [10/26/2018 11:54:34.198] [main] [akka.remote.Remoting] Remoting started; listening on addresses :[akka.tcp://sys@127.0.0.1:2551]</span><br><span class="line">[INFO] [10/26/2018 11:54:34.200] [main] [akka.remote.Remoting] Remoting now listens on addresses: [akka.tcp://sys@127.0.0.1:2551]</span><br><span class="line">[INFO] [10/26/2018 11:54:34.363] [sys-akka.actor.default-dispatcher-7] [akka://sys/user/toFind] ToFindRemoteActor is starting</span><br></pre></td></tr></table></figure></p><p>这时，我们先尝试在一个本地进程里去查找这个Actor：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main1</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">( String[] args )</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        ActorSystem system = ActorSystem.create(<span class="string">"main1"</span>);</span><br><span class="line">        LoggingAdapter log = Logging.getLogger(system, Main2.class);</span><br><span class="line">        ActorSelection toFind = system.actorSelection(<span class="string">"akka.tcp://sys@127.0.0.1:2551/user/toFind"</span>);</span><br><span class="line">        toFind.tell(<span class="string">"hello"</span>, ActorRef.noSender());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>注意，这里我没有提供application.conf，而且也没有指定其他的配置文件！所以这里的ActorSystem起的完全是本地模式。我们运行一下，看看是远端的Actor是否会打印hello呢？<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[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 &apos;akka.log-dead-letters&apos; and &apos;akka.log-dead-letters-during-shutdown&apos;.</span><br></pre></td></tr></table></figure></p><p>结果给出了这样的日志，说明并没有发送成功。再次验证了上面提到的Akka Remote的<code>Peer-to-Peer</code>设计，必须要求对等，两边都是<code>remote</code>！</p><p>好了，回到正轨上，我们来看看如何正确的去寻找一个远端actor并发送消息。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main2</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        Config config = ConfigFactory.load(<span class="string">"remote.conf"</span>);</span><br><span class="line">        <span class="comment">// Create an Akka system</span></span><br><span class="line">        ActorSystem system = ActorSystem.create(<span class="string">"main2"</span>, config);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Find remote actor</span></span><br><span class="line">        ActorSelection toFind = system.actorSelection(<span class="string">"akka.tcp://sys@127.0.0.1:2551/user/toFind"</span>);</span><br><span class="line">        toFind.tell(<span class="string">"hello"</span>, ActorRef.noSender());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>这里加载了remote.conf，启用remote provider。可以在ToFindRemoteActor的控制台有如下日志:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[INFO] [10/26/2018 14:12:11.376] [sys-akka.actor.default-dispatcher-4] [akka://sys/user/toFind] Msg received: hello</span><br></pre></td></tr></table></figure></p><p>说明找到且正常收到了消息。</p><h3 id="创建一个远程的Actor"><a href="#创建一个远程的Actor" class="headerlink" title="创建一个远程的Actor"></a>创建一个远程的Actor</h3><p>在Main2里，我们相当于起了一个监听着<code>127.0.0.1：2552</code>的<code>ActorSystem</code>，那我们把Main2当作远程系统(如果觉得127.0.0.1不太好理解，可以把它打包放到其他服务器，并指定hostname为这个服务器的IP)，在当前机器去尝试在Main2这个远端起一个Actor。<br>远程Actor代码如下：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ToCreateRemoteActor</span> <span class="keyword">extends</span> <span class="title">AbstractActor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    LoggingAdapter log = Logging.getLogger(getContext().system(), <span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">preStart</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        log.info(<span class="string">"ToCreateRemoteActor is starting"</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Receive <span class="title">createReceive</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> receiveBuilder()</span><br><span class="line">                .match(String.class, msg-&gt;&#123;</span><br><span class="line">                    log.info(<span class="string">"Msg received: &#123;&#125;"</span>, msg);</span><br><span class="line">                &#125;)</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>创建配置文件如下：<br><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">akka</span> <span class="string">&#123;</span></span><br><span class="line">  <span class="string">actor</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">provider</span> <span class="string">=</span> <span class="string">"remote"</span></span><br><span class="line">    <span class="string">deployment</span> <span class="string">&#123;</span></span><br><span class="line">      <span class="string">/toCreateActor</span> <span class="string">&#123;</span></span><br><span class="line">        <span class="string">remote</span> <span class="string">=</span> <span class="string">"akka.tcp://main2@127.0.0.1:2552"</span></span><br><span class="line">      <span class="string">&#125;</span></span><br><span class="line">    <span class="string">&#125;</span></span><br><span class="line">  <span class="string">&#125;</span></span><br><span class="line">  <span class="string">remote</span> <span class="string">&#123;</span></span><br><span class="line">    <span class="string">netty.tcp</span> <span class="string">&#123;</span></span><br><span class="line">      <span class="string">hostname</span> <span class="string">=</span> <span class="string">"127.0.0.1"</span></span><br><span class="line">      <span class="string">port</span> <span class="string">=</span> <span class="number">2553</span></span><br><span class="line">    <span class="string">&#125;</span></span><br><span class="line">  <span class="string">&#125;</span></span><br><span class="line"><span class="string">&#125;</span></span><br></pre></td></tr></table></figure></p><p>其中<code>toCreateActor</code>就是指定远端要启动的Actor的别名，在本地的ActorSystem靠这个别名去启动。注意指定provider为remote！</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main3</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        Config config = ConfigFactory.load(<span class="string">"create_remote.conf"</span>);</span><br><span class="line">        <span class="comment">// Create an Akka system</span></span><br><span class="line">        ActorSystem system = ActorSystem.create(<span class="string">"main3"</span>, config);</span><br><span class="line">        ActorRef actor = system.actorOf(Props.create(ToCreateRemoteActor.class), <span class="string">"toCreateActor"</span>);</span><br><span class="line">        actor.tell(<span class="string">"I'm created!"</span>, ActorRef.noSender());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到这里就尝试去创建一个名字叫<code>toCreateActor</code>的Actor，而这个名字在配置文件中定义了是远端的，Akka会自动尝试去远端创建。<br>启动一下，看到Main3的日志：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[INFO] [10/26/2018 15:25:42.794] [main] [akka.remote.Remoting] Starting remoting</span><br><span class="line">[INFO] [10/26/2018 15:25:43.364] [main] [akka.remote.Remoting] Remoting started; listening on addresses :[akka.tcp://main3@127.0.0.1:2553]</span><br><span class="line">[INFO] [10/26/2018 15:25:43.365] [main] [akka.remote.Remoting] Remoting now listens on addresses: [akka.tcp://main3@127.0.0.1:2553]</span><br></pre></td></tr></table></figure></p><p>检查Main2的日志，会发现远程Actor创建的信息：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[INFO] [10/26/2018 15:25:43.774] [main2-akka.actor.default-dispatcher-17] [akka://main2/remote/akka.tcp/main3@127.0.0.1:2553/user/toCreateActor] ToCreateRemoteActor is starting</span><br><span class="line">[INFO] [10/26/2018 15:25:43.775] [main2-akka.actor.default-dispatcher-16] [akka://main2/remote/akka.tcp/main3@127.0.0.1:2553/user/toCreateActor] Msg received: I&apos;m created!</span><br></pre></td></tr></table></figure></p><p>到这，一个远端的Actor就被创建出来了。<br>不过，事情就这样结束了吗？思考一个问题：<strong><em>查询这种远端创建的Actor，跟之前那个远端自己起来的Actor，方式一样吗？</em></strong><br>参考Main2，我们再写一个Main4来尝试查询并发送消息。那有一个问题，toCreateActor的地址到底该选哪个？按理说，应该是<code>akka.tcp://main2@127.0.0.1:2552/user/toCreateActor</code>。带着问题，我们试试看<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main4</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        Config config = ConfigFactory.parseString(</span><br><span class="line">                <span class="string">"akka.remote.netty.tcp.port="</span> + <span class="number">0</span>)</span><br><span class="line">                .withFallback(ConfigFactory.load(<span class="string">"remote.conf"</span>));</span><br><span class="line">        <span class="comment">// Create an Akka system</span></span><br><span class="line">        ActorSystem system = ActorSystem.create(<span class="string">"main4"</span>, config);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Find remote actor</span></span><br><span class="line">        ActorSelection toFind = system.actorSelection(<span class="string">"akka.tcp://main2@127.0.0.1:2552/user/toCreateActor"</span>);</span><br><span class="line">        toFind.tell(<span class="string">"I'm alive!"</span>, ActorRef.noSender());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>Main2中，会打印<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[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 &apos;akka.log-dead-letters&apos; and &apos;akka.log-dead-letters-during-shutdown&apos;.</span><br></pre></td></tr></table></figure></p><p>失败了。。。。。。<br>仔细看，Main2里面创建出来的Actor的Path是<br><code>akka://main2/remote/akka.tcp/main3@127.0.0.1:2553/user/toCreateActor</code><br>而远端自己起的Actor地址是：<br><code>akka://sys/user/toFind</code><br>所以，正确的Path应该是<a href="mailto:`akka.tcp/main3@127.0.0.1" target="_blank" rel="noopener">`akka.tcp/main3@127.0.0.1</a>:2553/user/toCreateActor`<br>修改后测试一下，会发现Main2中打印<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[INFO] [10/26/2018 15:25:58.615] [main2-akka.actor.default-dispatcher-17] [akka://main2/remote/akka.tcp/main3@127.0.0.1:2553/user/toCreateActor] Msg received: I&apos;m alive!</span><br></pre></td></tr></table></figure></p><p>所以，可以得出一个看上去不是很合理的结论：<br>虽然RemoteActor是创建在远程机器上，但如果想要查询它，还得向创建者发请求。</p><h2 id="Artery"><a href="#Artery" class="headerlink" title="Artery"></a>Artery</h2><p>Artert是Akka为新版的remote包起的代号。目前是共存状态，但被标记为<a href="https://doc.akka.io/docs/akka/current/common/may-change.html" target="_blank" rel="noopener">may change</a>状态，仅UDP模式可以用于生产。</p><p>配置与原来的remote略有不同<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">akka &#123;</span><br><span class="line">  actor &#123;</span><br><span class="line">    provider = remote</span><br><span class="line">  &#125;</span><br><span class="line">  remote &#123;</span><br><span class="line">    artery &#123;</span><br><span class="line">      enabled = on</span><br><span class="line">      transport = aeron-udp</span><br><span class="line">      canonical.hostname = &quot;127.0.0.1&quot;</span><br><span class="line">      canonical.port = 25520</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><blockquote><p>注意： 一旦启用了artery，所有Actor的地址协议就不用再带上tcp了，比如<code>akka.tcp://127.0.0.1:2551/user/testActor</code>就变成<code>akka://127.0.0.1:2551/user/testActor</code>！</p></blockquote><p>与原先相比，多了一个enabled选项控制artery是否启动。<br>相比原来的remote，Artery的变化主要集中在高吞吐、低延迟场景下提高性能上，包括用Akka Streams TCP/TLS替代了原来的Netty TCP，并新增了基于<a href="https://github.com/real-logic/Aeron" target="_blank" rel="noopener">Aeron</a>的UDP协议模式，以及对直接写<code>java.nio.ByteBuffer</code>的支持，大小消息分channel发等等。</p><h2 id="其他介绍"><a href="#其他介绍" class="headerlink" title="其他介绍"></a>其他介绍</h2><p>在具体使用中，还需要考虑序列化、路由、安全，而这些Akka都提供了。且看下回分解。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;虽然&lt;code&gt;Akka&lt;/code&gt;在单机上可以运行上百万的&lt;code&gt;Actor&lt;/code&gt;，但出于容错、负载均衡、灰度发布、提高并行度等等原因，我们仍然需要能在多个不同的服务器上运行&lt;code&gt;Actor&lt;/code&gt;。所以Akka提供了&lt;
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="分布式" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Akka" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/Akka/"/>
    
    
      <category term="akka" scheme="http://edisonxu.com/tags/akka/"/>
    
      <category term="actor" scheme="http://edisonxu.com/tags/actor/"/>
    
      <category term="并发" scheme="http://edisonxu.com/tags/%E5%B9%B6%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>Akka入门系列(二)：Actor</title>
    <link href="http://edisonxu.com/2018/10/30/akka-actor.html"/>
    <id>http://edisonxu.com/2018/10/30/akka-actor.html</id>
    <published>2018-10-30T01:04:02.000Z</published>
    <updated>2021-07-21T13:31:21.778Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Actor模型"><a href="#Actor模型" class="headerlink" title="Actor模型"></a>Actor模型</h2><p>由于<code>AKka</code>的核心是<code>Actor</code>，而<code>Actor</code>是按照<code>Actor模型</code>进行实现的，所以在使用<code>Akka</code>之前，有必要弄清楚什么是<code>Actor模型</code>。<br><code>Actor模型</code>最早是1973年Carl Hewitt、Peter Bishop和Richard Seiger的论文中出现的，受物理学中的广义相对论(<a href="https://en.wikipedia.org/wiki/General_relativity" target="_blank" rel="noopener">general relativity</a>)和量子力学(<a href="https://en.wikipedia.org/wiki/Quantum_mechanics" target="_blank" rel="noopener">quantum mechanics</a>)所启发，为解决并发计算的一个数学模型。</p><p><code>Actor模型</code>所推崇的哲学是”<strong>一切皆是Actor</strong>“，这与面向对象编程的”<strong>一切皆是对象</strong>“类似。<br>但不同的是，在模型中，<code>Actor</code>是一个运算实体，它遵循以下规则：</p><ul><li>接受外部消息，不占用调用方（消息发送者）的CPU时间片</li><li>通过消息改变自身的状态</li><li>创建有限数量的新<code>Actor</code></li><li>发送有限数量的消息给其他<code>Actor</code></li></ul><p>很多语言都实现了<code>Actor模型</code>，而其中最出名的实现要属<code>Erlang</code>的。<code>Akka</code>的实现借鉴了不少<code>Erlang</code>的经验。</p><h2 id="Actor模型的实现"><a href="#Actor模型的实现" class="headerlink" title="Actor模型的实现"></a>Actor模型的实现</h2><p><code>Akka</code>中<code>Actor</code>接受外部消息是靠<code>Mailbox</code>，参见下图<br><img src="/images/2018/10/actor-model.png" alt=""></p><p>对于<code>Akka</code>，它又做了一些约束：</p><ul><li>消息是不可变的</li><li>Actor本身是无状态的</li></ul><h2 id="基本的Actor例子"><a href="#基本的Actor例子" class="headerlink" title="基本的Actor例子"></a>基本的Actor例子</h2><p>本文用Maven管理一个Java的Akka项目。当日，你可以直接从<a href="https://developer.lightbend.com/start/?group=akka下载一个官方的例子。" target="_blank" rel="noopener">https://developer.lightbend.com/start/?group=akka下载一个官方的例子。</a></p><h3 id="引入依赖"><a href="#引入依赖" class="headerlink" title="引入依赖"></a>引入依赖</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.typesafe.akka<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>akka-actor_2.12<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;akka-version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="编写Actor"><a href="#编写Actor" class="headerlink" title="编写Actor"></a>编写Actor</h3><p>通常情况下，我们只需要直接继承<code>AbstractActor</code>就足够了。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">EchoActor</span> <span class="keyword">extends</span> <span class="title">AbstractActor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> LoggingAdapter log = Logging.getLogger(getContext().getSystem(), <span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Receive <span class="title">createReceive</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> receiveBuilder()</span><br><span class="line">                .match(String.class, s -&gt; &#123;</span><br><span class="line">                    log.info(<span class="string">"Received String message: &#123;&#125;"</span>, s);</span><br><span class="line">                &#125;)</span><br><span class="line">                .matchAny(o -&gt; log.info(<span class="string">"Received unknown message"</span>))</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p><code>AbstractActor</code>要求必须实现<code>createReceive()</code>方法，该方法返回一个<code>Receive</code>定义了该<code>Actor</code>能够处理哪些消息，以及怎么处理。这里只简单的打印一个日志。</p><h3 id="Actor的启动"><a href="#Actor的启动" class="headerlink" title="Actor的启动"></a>Actor的启动</h3><p><code>Akka</code>中，用<code>ActorSystem</code>来管理所有的<code>Actor</code>，包括其生命周期及交互。<br>启动Actor，有两种方式</p><ol><li>使用内置的main方法<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">akka.Main.main(<span class="keyword">new</span> String[]&#123;EchoActor.class.getName()&#125;);</span><br></pre></td></tr></table></figure></li></ol><p>这里会自动将EchoActor创建出来。</p><ol start="2"><li>手动创建<code>ActorSystem</code><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ActorSystem system = ActorSystem.create(<span class="string">"app"</span>);</span><br><span class="line">ActorRef echoActor = system.actorOf(Props.create(EchoActor.class), <span class="string">"echoActor"</span>);</span><br></pre></td></tr></table></figure></li></ol><p>两种方法本质上其实是一样的，只不过第一种里面把创建<code>ActorSystem</code>等工作封装好了罢了。</p><blockquote><p>注意：<br><code>ActorSystem</code>是一个较重的存在，一般一个应用里，只需要一个<code>ActorSystem</code>。<br>在同一个<code>ActorySystem</code>中，Actor不能重名。</p></blockquote><h3 id="Actor的Path"><a href="#Actor的Path" class="headerlink" title="Actor的Path"></a>Actor的Path</h3><p><code>Akka</code>中的<code>Actor</code>不能直接被new出来，而是按一棵树来管理的，每个<code>Actor</code>都有一个树上的<code>path</code>：<br><img src="/images/2018/10/actor_top_tree.png" alt="Akka Actor Hierarchy"></p><p>实际上，在我们创建自己的<code>Actor</code>之前，<code>Akka</code>已经在系统中创建了三个名字中带有<code>guardian</code>的<code>Actor</code>：</p><ul><li><code>/</code> 最顶层的 <code>root guardian</code>。它是系统中所有<code>Actor</code>的父，系统停止时，它是最后一个停止的</li><li><code>/user</code> <code>guardian</code>。这是用户自行创建的所有<code>Actor</code>的父。这里的user跟用户没有一毛钱关系</li><li><code>/system</code> 系统<code>guardian</code></li></ul><p>在上面的例子里，我们使用的是<code>system.actorOf</code>来创建<code>Actor</code>，<code>actorOf</code>返回的并不是<code>Actor</code>自身，而是一个<code>ActorRef</code>，它屏蔽了<code>Actor</code>的具体物理地址(可能是本jvm，也可以是其他jvm或另一台机器)。通过直接打印<code>ActorRef</code>看到<code>Actor</code>的<code>path</code>，比如本例是<code>/app/user/echoActor</code>。<br>像这种直接由system创建出来的<code>Actor</code>被称为顶层<code>Actor</code>，一般系统设计的时候，顶层<code>Actor</code>数量往往不会太多，大都由顶层<code>Actor</code>通过<code>getContext().actorOf()</code>派生出来其他的<code>Actor</code>。</p><h3 id="Actor间的相互调用-tell-ask"><a href="#Actor间的相互调用-tell-ask" class="headerlink" title="Actor间的相互调用(tell, ask)"></a>Actor间的相互调用(tell, ask)</h3><p>在<code>Actor模型</code>中，<code>Actor</code>本身的执行是不占用被调用方(<code>akka</code>中的话是消息的发送者)的CPU时间片，所以，<code>akka</code>的<code>Actor</code>在相互调用时均是异步的行为。</p><ul><li>tell 发送一个消息到目标<code>Actor</code>后立刻返回</li><li>ask 发送一个消息到目标<code>Actor</code>，并返回一个<code>Future</code>对象，可以通过该对象获取结果。但前提是目标<code>Actor</code>会有Reply才行，如果没有Reply，则抛出超时异常。</li></ul><h4 id="Tell：-Fire-forget"><a href="#Tell：-Fire-forget" class="headerlink" title="Tell： Fire-forget"></a>Tell： Fire-forget</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">target.tell(message, getSelf());</span><br></pre></td></tr></table></figure><p>其中第二个参数是发送者。之所以要带上这个是为了方便target处理完逻辑后，如果需要返回结果，可以也通过<code>tell</code>异步通知回去。</p><h4 id="Ask-Send-And-Recieve-Future"><a href="#Ask-Send-And-Recieve-Future" class="headerlink" title="Ask: Send-And-Recieve-Future"></a>Ask: Send-And-Recieve-Future</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Future&lt;Object&gt; future = Patterns.ask(echoActor, <span class="string">"echo me"</span>, <span class="number">200</span>);</span><br><span class="line">future.onSuccess(<span class="keyword">new</span> OnSuccess&lt;Object&gt;() &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onSuccess</span><span class="params">(Object result)</span> <span class="keyword">throws</span> Throwable </span>&#123;</span><br><span class="line">        System.out.println(result);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;, system.dispatcher());</span><br></pre></td></tr></table></figure><p>由于之前的<code>EchoActor</code>并没有返回Reply，所以这里什么都没打印。<br>修改<code>EchoActor</code>如下：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Receive <span class="title">createReceive</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> receiveBuilder()</span><br><span class="line">            .match(String.class, s -&gt; &#123;</span><br><span class="line">                log.info(<span class="string">"Received String message: &#123;&#125;"</span>, s);</span><br><span class="line">                ActorRef sender = getSender();</span><br><span class="line">                <span class="keyword">if</span>(!sender.isTerminated())</span><br><span class="line">                    sender.tell(<span class="string">"Receive: "</span>+s, getSelf());</span><br><span class="line">            &#125;)</span><br><span class="line">            .matchAny(o -&gt; log.info(<span class="string">"Received unknown message"</span>))</span><br><span class="line">            .build();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>由于前面两个消息的发送者是<code>ActorRef.noSender()</code>，所以<code>EchoActor</code>中<code>getSender()</code>返回的是<code>DeadLetter</code>的<code>ActorRef</code>，terminated值为真。只有最后的值打印出来：<code>Receive: echo me</code></p><h3 id="Actor的停止"><a href="#Actor的停止" class="headerlink" title="Actor的停止"></a>Actor的停止</h3><p>停止一个<code>Actor</code>有三种方法：</p><ul><li>调用<code>ActorSystem</code>或<code>getContext()</code>的stop方法</li><li>给目标发送一个毒药消息：<code>akka.actor.PoisonPill.getInstance()</code></li><li>给目标发送一个Kill消息： <code>akka.actor.Kill.getInstance()</code></li></ul><p>当使用前两种方法时，<code>Actor</code>的行为是：</p><ol><li>挂起它的<code>Mailbox</code>，停止接受新消息</li><li>给它所有的子<code>Actor</code>发送stop命令，并等待所有子<code>Actor</code>停止</li><li>最终停止自己</li></ol><blockquote><p>由于停止<code>Actor</code>是一个异步的操作，在目标<code>Actor</code>被完全停止之前，如果要创建一个同名的<code>Actor</code>，则会收到<code>InvalidActorNameException</code>。</p></blockquote><p>“kill”的方法略有不同，它会抛出一个<code>ActorKilledException</code>到父层去，由父层实现决定如何处理。<br>一般来说，不应该依赖于<code>PoisonPill</code>和<code>Kill</code>去关闭<code>Actor</code>。推荐的方法是自定义关闭消息，交由<code>Actor</code>处理。</p><p>如果需要等待关闭结果，可以采用<code>PatternsCS.gracefulStop</code>，它会返回一个<code>CompletionStage</code>，可以进行到期处理：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">static</span> akka.pattern.PatternsCS.gracefulStop;</span><br><span class="line"><span class="keyword">import</span> akka.pattern.AskTimeoutException;</span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.CompletionStage;</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">  CompletionStage&lt;Boolean&gt; stopped =</span><br><span class="line">    gracefulStop(actorRef, Duration.ofSeconds(<span class="number">5</span>), Manager.SHUTDOWN);</span><br><span class="line">  stopped.toCompletableFuture().get(<span class="number">6</span>, TimeUnit.SECONDS);</span><br><span class="line">  <span class="comment">// the actor has been stopped</span></span><br><span class="line">&#125; <span class="keyword">catch</span> (AskTimeoutException e) &#123;</span><br><span class="line">  <span class="comment">// the actor wasn't stopped within 5 seconds</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>发送一个自定义的消息，如<code>Manager.SHUTDOWN</code>，等待关闭，如果6秒未关闭，再去处理。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;Actor模型&quot;&gt;&lt;a href=&quot;#Actor模型&quot; class=&quot;headerlink&quot; title=&quot;Actor模型&quot;&gt;&lt;/a&gt;Actor模型&lt;/h2&gt;&lt;p&gt;由于&lt;code&gt;AKka&lt;/code&gt;的核心是&lt;code&gt;Actor&lt;/code&gt;，而&lt;code&gt;A
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="分布式" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Akka" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/Akka/"/>
    
    
      <category term="akka" scheme="http://edisonxu.com/tags/akka/"/>
    
      <category term="actor" scheme="http://edisonxu.com/tags/actor/"/>
    
      <category term="并发" scheme="http://edisonxu.com/tags/%E5%B9%B6%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>Akka入门系列(一)：基本介绍</title>
    <link href="http://edisonxu.com/2018/10/30/akka-intro.html"/>
    <id>http://edisonxu.com/2018/10/30/akka-intro.html</id>
    <published>2018-10-30T01:02:26.000Z</published>
    <updated>2021-07-21T13:31:21.780Z</updated>
    
    <content type="html"><![CDATA[<h2 id="什么是Akka，它能干什么？"><a href="#什么是Akka，它能干什么？" class="headerlink" title="什么是Akka，它能干什么？"></a>什么是Akka，它能干什么？</h2><p>互联网系统的发展，大多数情况下都是业务倒逼的。发展过程不外乎以下几步：</p><ol><li>最开始时，一个简单的MVC程序就可以，甚至是早期的J2EE也能得到很好的性能。</li><li>忽然某一天，系统压力大了，一些功能变得比较慢，这时会尝试去做代码重构优化，必要的地方开始使用进程内MQ以及线程池，开启异步及<code>多线程</code>。</li><li>再往后，单台也不能满足系统的吞吐了，这时就得上集群，前面一个<code>负载均衡</code>，后面部署多台相同的服务器，将压力均衡到若干服务器上，甚至数据库都开始做切片。</li><li>再往后，继续优化，将一些压力大的功能单独提出来，做成一个<code>Service</code>，对外部提供服务，可以是REST，也可以是RPC，用这种方式来提高服务器利用率，毕竟有些业务只需要IO，而有些业务需要很强的CPU，根据实现逻辑不同，需要的物理资源也不同。而这时，简单的负载均衡也无法适用了，需要功能相对复杂的<code>网关</code>或<code>总线</code>，并且各种分布式下的难点都出来了——<code>调度</code>、分布式<code>容错</code>、<code>熔断</code>、<code>弹性</code>、<code>扩容</code>、分布式事务、灰度发布、<code>压力调整</code>等等。</li></ol><p>可以看到，如果要开发一个分布式系统，工程师要掌握的架构知识比较多，从基本的多线程到复杂的调度、容错、熔断、弹性、扩容等等复杂系统等等，任何一个环节出问题，都可能导致系统的不稳定。而<code>Akka</code>简化了这一切：</p><ul><li><code>Akka</code>屏蔽了Java的多线程和锁，转而使用<code>Actor</code>模型，一般的工程师可以在不了解如何优化Java多线程编程的情况下，也能实现非常高性能的系统</li><li><code>Actor</code>设计之初就天然满足了分布式，而且粒度较小，单机上可以跑上百万的<code>Actor</code></li><li><code>Akka</code>屏蔽了分布式集群中底层的通讯机制，对于开发者来说，只要根据业务写好<code>Actor</code>即可</li><li><code>Akka</code>直接提供了分布式下高可用、弹性、动态扩容的功能，无需再次开发</li></ul><p><code>Akka</code>由<code>Scala</code>编写，但同时提供了<code>Scala</code>和<code>Java</code> API。或许<code>Akka</code>你没有听过，但<code>Spark</code>、<code>Flink</code>这些当下流行的大数据分布式流式系统应当有所耳闻，它们的的底层，通通都在使用<code>Akka</code>。<code>Scala</code>的作者<code>Martin Odersky</code>，就是<code>Akka</code>背后的公司<a href="https://www.lightbend.com" target="_blank" rel="noopener"><code>Lightbend</code></a>（以前称为Typesafe）的创始人。<code>Lightbend</code>一直致力于提供基于Actor模型的分布式高性能<strong>系统</strong>，而非仅仅只有分布式框架，旗下除了Akka，还有<code>Play</code>(响应式Web框架)、<code>Lagom</code>(微服务框架)、<code>alpkka</code>(响应式集成中间件)。</p><blockquote><p><code>Akka</code>和<code>Spring</code>区别<br><code>Akka</code>关注在高性能上，<code>Spring</code>关注于工具集的整合和统一上<br><code>Spring</code>写出的代码(生产可用级)，未必是高性能的(没说写不出高性能的)，而<code>Akka</code>写出来的，基本上都是高性能。</p></blockquote><h2 id="利弊"><a href="#利弊" class="headerlink" title="利弊"></a>利弊</h2><h3 id="好处"><a href="#好处" class="headerlink" title="好处"></a>好处</h3><p>使用了<code>Akka</code>框架，你可以获得：</p><ul><li>无锁、无同异步编程和多线程编程，低成本(编码阶段)实现高性能，</li><li>天然的并发系统和响应式系统，提供高吞吐和高并发服务</li><li>直接获得容错系统，自由定义恢复、重置或关闭等操作</li><li>简单的由单机扩展至分布式，核心业务代码几乎不用动</li><li>文档齐全</li></ul><blockquote><p>并发和并行的区别</p><ul><li>并发是一个处理器利用时间切片处理多个任务<br>想象电影院内的5台按摩椅，每次服务5-20分钟不等，一堆人等着坐，这个人下来那个人上。</li><li>并行是多个处理器或多核的处理器同时处理不同的任务<br>想象有N排按摩椅 ，每一排都可以服务一堆人。<br>Akka是天然并发系统，但是并不能默认做到所有任务并行。特此澄清一下!</li></ul></blockquote><h3 id="坏处"><a href="#坏处" class="headerlink" title="坏处"></a>坏处</h3><ul><li>上手难度高，学习路线陡峭</li><li>中文文档少</li><li>国内使用人少，遇到问题可请教或讨论的人少</li><li>纯异步，调试起来比较麻烦</li><li>第三方工具集少，添加进系统可能还需额外处理，缺少部分组件的整合的资料</li></ul><h2 id="使用方式"><a href="#使用方式" class="headerlink" title="使用方式"></a>使用方式</h2><ul><li>作为一个独立程序单独启动</li><li>作为一个lib集成到其他框架，如Spring里</li></ul><h2 id="提供的模组"><a href="#提供的模组" class="headerlink" title="提供的模组"></a>提供的模组</h2><p><code>Akka</code>主要提供了如下的组件（还有一些小的省略了）：</p><ul><li>akka-actor_2.12 核心框架</li><li>akka-remote_2.12 底层通讯模块</li><li>akka-cluster_2.12 集群模块</li><li>akka-cluster-sharding_2.12 集群分片功能模块</li><li>akka-cluster-singleton_2.12 提供集群单例功能的模块</li><li>akka-cluster-tools_2.12 集群特殊功能模块</li><li>akka-stream_2.12 流处理及流式编程模块</li><li>akka-camel_2.12 基于<a href="http://camel.apache.org/" target="_blank" rel="noopener">Apache Camel</a>的实现模块，与各种接口进行通信</li><li>akka-agent_2.12 处理共享变量及原子操作的模块</li><li>akka-http_2.12 用于构建基础http服务的模块(注意，akka http不是web框架！)</li><li>akka-stream-kafka_2.12 kakfa的流式接口模块</li><li>akka-management_2.12 分布式集群管理模块</li><li>akka-testkit_2.12 单元测试模块</li><li>akka-slf4j_2.12 实现slf4j接口的日志模块</li><li>akka-persistence_2.12 用于保存数据、实现CQRS架构、实现EventSourcing的模块</li><li>akka-distributed-data_2.12 分布式数据保存模块，实现最终一致性</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;什么是Akka，它能干什么？&quot;&gt;&lt;a href=&quot;#什么是Akka，它能干什么？&quot; class=&quot;headerlink&quot; title=&quot;什么是Akka，它能干什么？&quot;&gt;&lt;/a&gt;什么是Akka，它能干什么？&lt;/h2&gt;&lt;p&gt;互联网系统的发展，大多数情况下都是业务倒逼的
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="分布式" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    
      <category term="Akka" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/%E5%88%86%E5%B8%83%E5%BC%8F/Akka/"/>
    
    
      <category term="akka" scheme="http://edisonxu.com/tags/akka/"/>
    
      <category term="actor" scheme="http://edisonxu.com/tags/actor/"/>
    
      <category term="并发" scheme="http://edisonxu.com/tags/%E5%B9%B6%E5%8F%91/"/>
    
      <category term="并行" scheme="http://edisonxu.com/tags/%E5%B9%B6%E8%A1%8C/"/>
    
  </entry>
  
  <entry>
    <title>JHipster快速开发Web应用</title>
    <link href="http://edisonxu.com/2018/02/01/jhipster-quick-start.html"/>
    <id>http://edisonxu.com/2018/02/01/jhipster-quick-start.html</id>
    <published>2018-02-01T08:39:24.000Z</published>
    <updated>2021-07-21T13:31:21.785Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>在基于Spring的Web项目开发中，通常存在两个问题：</p><ol><li>普通CRUD的代码基本重复，完全是体力活；</li><li>Controller层和持久层之间的数据传递，存在不规范。有人喜欢直接返回JSON，有人喜欢用DTO，有人喜欢直接Entity。</li></ol></blockquote><p>那如何解决这个问题呢？自动生成呗。一群喜欢动脑筋（懒）的人，发明了JHipster。<img src="/images/2018/02/logo-jhipster.svg" style="display: inline-block;" width="65" height="65/"></p><blockquote><p><a href="http://www.jhipster.tech" target="_blank" rel="noopener">JHipster</a>是一个基于SpringBoot和Angular的快速Web应用和SpringCloud微服务的脚手架。本文将介绍如何利用JHipster快速开发Web应用。</p></blockquote><h2 id="安装JHipster"><a href="#安装JHipster" class="headerlink" title="安装JHipster"></a>安装JHipster</h2><p>JHipster支持好几种安装方式，这里选用最方便的一种方式：Yarn</p><h5 id="1-安装Java8；"><a href="#1-安装Java8；" class="headerlink" title="1. 安装Java8；"></a>1. 安装<a href="http://www.oracle.com/technetwork/java/javase/downloads/index.html" target="_blank" rel="noopener">Java8</a>；</h5><h5 id="2-安装Node-js"><a href="#2-安装Node-js" class="headerlink" title="2. 安装Node.js"></a>2. 安装<a href="http://nodejs.org/" target="_blank" rel="noopener">Node.js</a></h5><h5 id="3-安装Yarn"><a href="#3-安装Yarn" class="headerlink" title="3. 安装Yarn"></a>3. 安装<a href="https://yarnpkg.com/en/docs/install" target="_blank" rel="noopener">Yarn</a></h5><h5 id="4-安装JHipster：-yarn-global-add-generator-jhipster"><a href="#4-安装JHipster：-yarn-global-add-generator-jhipster" class="headerlink" title="4. 安装JHipster： yarn global add generator-jhipster"></a>4. 安装JHipster： <code>yarn global add generator-jhipster</code></h5><h2 id="创建Web应用"><a href="#创建Web应用" class="headerlink" title="创建Web应用"></a>创建Web应用</h2><h5 id="1-创建项目目录"><a href="#1-创建项目目录" class="headerlink" title="1. 创建项目目录"></a>1. 创建项目目录</h5><h5 id="2-为一些被墙的资源添加国内源"><a href="#2-为一些被墙的资源添加国内源" class="headerlink" title="2. 为一些被墙的资源添加国内源"></a>2. 为一些被墙的资源添加国内源</h5><p>在项目目录下创建<code>.npmrc</code>文件，为该项目特指一些源。（当然，你也可以为Node和Yarn指定全局的源，那就可以跳过这一步）<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">sass_binary_site=https://npm.taobao.org/mirrors/node-sass/</span><br><span class="line">phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/</span><br><span class="line">electron_mirror=https://npm.taobao.org/mirrors/electron/</span><br><span class="line">registry=https://registry.npm.taobao.org</span><br></pre></td></tr></table></figure></p><h5 id="3-在项目目录下运行命令jhipster初始化"><a href="#3-在项目目录下运行命令jhipster初始化" class="headerlink" title="3. 在项目目录下运行命令jhipster初始化"></a>3. 在项目目录下运行命令<code>jhipster</code>初始化</h5><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br></pre></td><td class="code"><pre><span class="line">  <span class="string">(1/16)</span> <span class="string">Which</span> <span class="string">*type*</span> <span class="string">of</span> <span class="string">application</span> <span class="string">would</span> <span class="string">you</span> <span class="string">like</span> <span class="string">to</span> <span class="string">create?</span> <span class="string">(Use</span> <span class="string">arrow</span> <span class="string">keys)</span></span><br><span class="line"><span class="string">&gt; Monolithic application (recommended for simple projects) </span></span><br><span class="line"><span class="string">  //传统Web应用</span></span><br><span class="line"><span class="string">  Microservice application</span></span><br><span class="line"><span class="string">  //微服务</span></span><br><span class="line"><span class="string">  Microservice gateway</span></span><br><span class="line"><span class="string">  //微服务网关</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (2/16) What is the base name of your application? (jhipster) jhipster_quick_start</span></span><br><span class="line"><span class="string">  //输入项目名称，对应Maven的 artifactId</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (3/16) What is your default Java package name? (com.chimestone) com.edi</span></span><br><span class="line"><span class="string">  //输入默认包名，对应Maven的 groupId</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (4/16) Do you want to use the JHipster Registry to configure, monitor and scale your application? (Use arrow keys)</span></span><br><span class="line"><span class="string">&gt; No</span></span><br><span class="line"><span class="string">  Yes</span></span><br><span class="line"><span class="string">  //选择是否启用JHipster Registry（微服务默认开启），它可以理解为Eureka、Spring Cloud Config Server、Spring Cloud Admin的一个合体</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (5/16) Which *type* of authentication would you like to use? (Use arrow keys)</span></span><br><span class="line"><span class="string">  JWT authentication (stateless, with a token)</span></span><br><span class="line"><span class="string">&gt; HTTP Session Authentication (stateful, default Spring Security mechanism)</span></span><br><span class="line"><span class="string">  OAuth2 Authentication (stateless, with an OAuth2 server implementation)</span></span><br><span class="line"><span class="string">  //选择认证方式，支持JWT、Session和OATUH2三种</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (6/16) Which *type* of database would you like to use? (Use arrow keys)</span></span><br><span class="line"><span class="string">&gt; SQL (H2, MySQL, MariaDB, PostgreSQL, Oracle, MSSQL)</span></span><br><span class="line"><span class="string">  MongoDB</span></span><br><span class="line"><span class="string">  Cassandra</span></span><br><span class="line"><span class="string">  //选择数据库类型</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (7/16) Which *production* database would you like to use? (Use arrow keys)</span></span><br><span class="line"><span class="string">&gt; MySQL</span></span><br><span class="line"><span class="string">  MariaDB</span></span><br><span class="line"><span class="string">  PostgreSQL</span></span><br><span class="line"><span class="string">  Oracle (Please follow our documentation to use the Oracle proprietary driver)</span></span><br><span class="line"><span class="string">  Microsoft SQL Server</span></span><br><span class="line"><span class="string">  //选择数据库</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (8/16) Which *development* database would you like to use?</span></span><br><span class="line"><span class="string">  H2 with disk-based persistence</span></span><br><span class="line"><span class="string">&gt; H2 with in-memory persistence</span></span><br><span class="line"><span class="string">  MySQL</span></span><br><span class="line"><span class="string">  //选择开发时连接的数据库，这里选H2只是为了演示</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (9/16) Do you want to use Hibernate 2nd level cache? (Use arrow keys)</span></span><br><span class="line"><span class="string">&gt; Yes, with ehcache (local cache, for a single node)</span></span><br><span class="line"><span class="string">  Yes, with HazelCast (distributed cache, for multiple nodes)</span></span><br><span class="line"><span class="string">  [BETA] Yes, with Infinispan (hybrid cache, for multiple nodes)</span></span><br><span class="line"><span class="string">  No</span></span><br><span class="line"><span class="string">  //选择集成到Hibernate2级缓存</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (10/16) Would you like to use Maven or Gradle for building the backend? (Use arrow keys)</span></span><br><span class="line"><span class="string">&gt; Maven</span></span><br><span class="line"><span class="string">  Gradle</span></span><br><span class="line"><span class="string">  //选择打包工具</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (11/16) Which other technologies would you like to use?</span></span><br><span class="line"><span class="string">  ( ) Social login (Google, Facebook, Twitter)</span></span><br><span class="line"><span class="string">  (*) Search engine using Elasticsearch</span></span><br><span class="line"><span class="string"> &gt;(*) WebSockets using Spring Websocket</span></span><br><span class="line"><span class="string">  ( ) API first development using swagger-codegen</span></span><br><span class="line"><span class="string">  ( ) [BETA] Asynchronous messages using Apache Kafka</span></span><br><span class="line"><span class="string">  //选择其他的集成框架，这里注意要按下空格键才是启用，启用后会加上*标识。看到无脑自动集成ES是不是泪流满面？</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (12/16) Which *Framework* would you like to use for the client? (Use arrow keys)</span></span><br><span class="line"><span class="string">&gt; Angular 4</span></span><br><span class="line"><span class="string">  AngularJS 1.x</span></span><br><span class="line"><span class="string">  //选择集成的Angular的版本，Angular4采用Webpack打包自动化，而1.x采用Bower和Gulp做自动化</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (13/16) Would you like to use the LibSass stylesheet preprocessor for your CSS? (y/N) y</span></span><br><span class="line"><span class="string">  //是否启用LibSass</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (14/16) Would you like to enable internationalization support? (Y/n) n</span></span><br><span class="line"><span class="string">  //是否开启国际化</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (15/16) Besides JUnit and Karma, which testing frameworks would you like to use? (Press &lt;space&gt; to select, &lt;a&gt; to toggle all, &lt;i&gt; to inverse selection)</span></span><br><span class="line"><span class="string"> &gt;( ) Gatling</span></span><br><span class="line"><span class="string">  ( ) Cucumber</span></span><br><span class="line"><span class="string">  ( ) Protractor</span></span><br><span class="line"><span class="string">  //选择测试框架，做压力测试的同学有福了</span></span><br><span class="line"><span class="string">  </span></span><br><span class="line"><span class="string">  (16/16) Would you like to install other generators from the JHipster Marketplace? (y/N)</span></span><br><span class="line"><span class="string">  //从JHipster市场下载一些其他集成，上下键翻动，空格选取/反选，回车结束。可以看到市场里还是有不少好东西的，像pages服务、ReactNative集成、swagger2markup让你的swagger界面更漂亮、gRPC自动CRUD代码等。</span></span><br></pre></td></tr></table></figure><p>全部选择后，就开始了自动执行生成项目，喝杯水坐等。<strong>如果没有翻墙且忘了添加第二步的同学，请坐等卡住。</strong><br>这里有一点必须提醒，虽然JHipster选项中可以启用ES集成，<strong>但受SpringBoot对ES的集成版本限制</strong>。<br>JHipster采用的是1.5.X的SpringBoot版本，对应的spring-data-elasticsearch是2.1.X版本，该版本最高支持ES到2.X，醉了~~~具体参见<a href="https://github.com/spring-projects/spring-data-elasticsearch" target="_blank" rel="noopener">这里</a><br>所以如果要使用高版本的ES，还是得用ES自己提供的REST接口，据ES的一篇文章<a href="https://www.elastic.co/blog/benchmarking-rest-client-transport-client" target="_blank" rel="noopener">Benchmarking REST client and transport client</a>显示，5.0以后的ES自带的REST接口性能还是可以的。</p><h2 id="基本姿势"><a href="#基本姿势" class="headerlink" title="基本姿势"></a>基本姿势</h2><p>对于普通Web应用，JHipster在SpringBoot中默认加载了<code>SpringMVC</code>、<code>SpringData</code>、<code>SpringJPA</code>、<code>SpringSecurity</code>几个主要的Web相关的家族成员，LogStash作为日志工具，同时引入了<code>ApacheCommons</code>包、<code>Swagger</code>、<code>HikariCP</code>数据库连接池、<code>Jackson</code>等工具。基本上开发一个JavaWeb项目所需的框架都具备了，甚至还引入了<code>Metrics</code>做运维监控。<br>此外，它还引入两个特殊的组件——<code>Liquibase</code>和<code>MapperStruct</code>。</p><ul><li><a href="liquibase.org">Liquibase</a>是一个帮助管理数据库变更的工具</li><li><a href="http://mapstruct.org/" target="_blank" rel="noopener">MapperStruct</a>用于自动生成Entity和对应DTO之间的映射关系类，<strong>在使用DTO时，千万记得要把自动生成的目录加到IDE的项目路径里</strong>！</li></ul><p>国内搞JavaWeb的，大都喜欢使用<code>Mybatis</code>，可惜的是JHipster默认并不提供<code>Mybatis</code>的集成。但是<code>SpringJPA</code>现在已经封装的十分完善，常规的CRUD和分页，在JHipster下，无需写一行代码（是的，你没看错）。<br>如果确实需要比较复杂的级联查询，JPA也提供了Specification和Sample实现，性能测试下来其实没多大区别，对付普通Web足以。<br>如果确实不喜欢JPA，好在SpringBoot本身可以同时使用JPA和Mybatis，那么就把复杂级联用Mybatis，普通CRUD用JPA，达到最佳效果。</p><h5 id="代码结构"><a href="#代码结构" class="headerlink" title="代码结构"></a>代码结构</h5><ul><li><p><strong>Entity</strong><br>JHipster自动产生的项目，内置了<code>User</code>、<code>Authority</code>、<code>PersistentToken</code>、<code>PersistentAuditEvent</code>四个Entity（如果选取的还有其他组件，如OAUTH2等，会有对应的Entity自动生成）。产生的几张表均以<code>jhi_</code>开头。如果启用了ES，那么除了<code>@Entity</code>注解外，你还会看到<code>@Document</code>注解。<br>这里值得一提的是，官方并不推荐修改默认的表名，而且如果要更改User的字段，官方推荐使用创建一个子类继承User类，然后在该子类中把User给Map进来，参见<a href="http://www.jhipster.tech/tips/022_tip_registering_user_with_additional_information.html" target="_blank" rel="noopener">这里</a>。但其实完全自己修改，然后更新数据库字段后，用Liquibase diff命令生成changelog。</p></li><li><p><strong>Controller</strong><br>JHipster自动生成的Controller暴露出的RESTful接口都是<a href="http://www.ruanyifeng.com/blog/2014/05/restful_api.html" target="_blank" rel="noopener">标准的RESTful API风格</a>，国内很多程序员都不在乎这个东西，导致代码风格及其粗狂。</p></li><li><p><strong>Repository</strong><br>这一块得益于SpringJPA的强大，一个JpaRepository接口足以满足大多数需求，有些懒人甚至连Controller都懒得写，给Repository接口加上<code>@RepositoryRestResource</code>注解直接暴露RESTful接口出去。</p></li></ul><h5 id="开始表演"><a href="#开始表演" class="headerlink" title="开始表演"></a>开始表演</h5><p>熟悉了代码结构后，我们开始用JHipster来做项目了。</p><ol><li>创建JDL文件描述Entity<br>JHipster默认提供了以下几种类型及校验关键字：</li></ol><table><thead><tr><th>类型</th><th style="text-align:right">校验</th><th style="text-align:right">备注</th></tr></thead><tbody><tr><td>String</td><td style="text-align:right">required, minlength, maxlength, pattern</td><td style="text-align:right">Java String类型，默认长度取决于使用的底层技术，JPA默认是255长，可以用validation rules修改到1024</td></tr><tr><td>Integer</td><td style="text-align:right">required, min, max</td><td style="text-align:right"></td></tr><tr><td>Long</td><td style="text-align:right">required, min, max</td><td style="text-align:right"></td></tr><tr><td>BigDecimal</td><td style="text-align:right">required, min, max</td><td style="text-align:right"></td></tr><tr><td>Float</td><td style="text-align:right">required, min, max</td><td style="text-align:right"></td></tr><tr><td>Double</td><td style="text-align:right">required, min, max</td><td style="text-align:right"></td></tr><tr><td>Enum</td><td style="text-align:right">required</td><td style="text-align:right"></td></tr><tr><td>Boolean</td><td style="text-align:right">required</td><td style="text-align:right"></td></tr><tr><td>LocalDate</td><td style="text-align:right">required</td><td style="text-align:right">对应<code>java.time.LocalDate</code>类</td></tr><tr><td>Instant</td><td style="text-align:right">required</td><td style="text-align:right">对应<code>java.time.Instant</code>类，DB中映射为<code>Timestamp</code></td></tr><tr><td>ZonedDateTime</td><td style="text-align:right">required</td><td style="text-align:right">对应<code>java.time.ZonedDateTime</code>类，用于需要提供TimeZone的日期</td></tr><tr><td>Blob</td><td style="text-align:right">required, minbytes, maxbytes</td><td style="text-align:right"></td></tr></tbody></table><p>官方提供了一个在线的<a href="https://start.jhipster.tech/jdl-studio/" target="_blank" rel="noopener">JDL Studio</a>，方便撰写JDL。<br>例子如下：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">//双斜杠注释会被忽略掉</span><br><span class="line">/** 这种注释会带到生成的代码里去 */</span><br><span class="line">entity Person &#123;</span><br><span class="line">    name String required,</span><br><span class="line">    sex Sex</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">enum Sex &#123;</span><br><span class="line">    MALE, FEMALE</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">entity Country&#123;</span><br><span class="line">    countryName String</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">relationship ManyToOne &#123;</span><br><span class="line">    Person&#123;country&#125; to Country</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">paginate Person with pagination</span><br><span class="line">paginate Country with infinite-scroll</span><br></pre></td></tr></table></figure><ol start="2"><li>用<code>jhipster import-jdl your-jdl-file.jdl</code>导入Entity。<br>中间会提示有<code>conflict</code>，因为像Cache配置、LiquidBase配置等是已存在的，可以覆盖或merge。<br>执行完毕后，看到代码已经生成进去了。</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.edi.domain;</span><br><span class="line"><span class="keyword">import</span> ...</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 这种注释会带到生成的代码里去</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@ApiModel</span>(description = <span class="string">"这种注释会带到生成的代码里去"</span>)</span><br><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@Table</span>(name = <span class="string">"person"</span>)</span><br><span class="line"><span class="meta">@Cache</span>(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)</span><br><span class="line"><span class="meta">@Document</span>(indexName = <span class="string">"person"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Person</span> <span class="keyword">implements</span> <span class="title">Serializable</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">long</span> serialVersionUID = <span class="number">1L</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Id</span></span><br><span class="line">    <span class="meta">@GeneratedValue</span>(strategy = GenerationType.IDENTITY)</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@NotNull</span></span><br><span class="line">    <span class="meta">@Column</span>(name = <span class="string">"name"</span>, nullable = <span class="keyword">false</span>)</span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Enumerated</span>(EnumType.STRING)</span><br><span class="line">    <span class="meta">@Column</span>(name = <span class="string">"sex"</span>)</span><br><span class="line">    <span class="keyword">private</span> Sex sex;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ManyToOne</span></span><br><span class="line">    <span class="keyword">private</span> Country country;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// jhipster-needle-entity-add-field - Jhipster will add fields here, do not remove</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Long <span class="title">getId</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> id;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setId</span><span class="params">(Long id)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.id = id;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">getName</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> name;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Person <span class="title">name</span><span class="params">(String name)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.name = name;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setName</span><span class="params">(String name)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.name = name;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Sex <span class="title">getSex</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> sex;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Person <span class="title">sex</span><span class="params">(Sex sex)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.sex = sex;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setSex</span><span class="params">(Sex sex)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.sex = sex;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Country <span class="title">getCountry</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> country;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Person <span class="title">country</span><span class="params">(Country country)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.country = country;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">this</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setCountry</span><span class="params">(Country country)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.country = country;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// jhipster-needle-entity-add-getters-setters - Jhipster will add getters and setters here, do not remove</span></span><br><span class="line">    ...</span><br></pre></td></tr></table></figure><p>看到有段注释带进去了。<br>再看下Controller<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.edi.web.rest;</span><br><span class="line"><span class="keyword">import</span> ...</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * REST controller for managing Person.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping</span>(<span class="string">"/api"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">PersonResource</span> </span>&#123;</span><br><span class="line"> ... </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * GET  /people : get all the people.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> pageable the pagination information</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> the ResponseEntity with status 200 (OK) and the list of people in body</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping</span>(<span class="string">"/people"</span>)</span><br><span class="line">    <span class="meta">@Timed</span></span><br><span class="line">    <span class="keyword">public</span> ResponseEntity&lt;List&lt;Person&gt;&gt; getAllPeople(<span class="meta">@ApiParam</span> Pageable pageable) &#123;</span><br><span class="line">        log.debug(<span class="string">"REST request to get a page of People"</span>);</span><br><span class="line">        Page&lt;Person&gt; page = personRepository.findAll(pageable);</span><br><span class="line">        HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, <span class="string">"/api/people"</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> ResponseEntity&lt;&gt;(page.getContent(), headers, HttpStatus.OK);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * SEARCH  /_search/people?query=:query : search for the person corresponding</span></span><br><span class="line"><span class="comment">     * to the query.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> query the query of the person search</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> pageable the pagination information</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> the result of the search</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping</span>(<span class="string">"/_search/people"</span>)</span><br><span class="line">    <span class="meta">@Timed</span></span><br><span class="line">    <span class="keyword">public</span> ResponseEntity&lt;List&lt;Person&gt;&gt; searchPeople(<span class="meta">@RequestParam</span> String query, <span class="meta">@ApiParam</span> Pageable pageable) &#123;</span><br><span class="line">        log.debug(<span class="string">"REST request to search for a page of People for query &#123;&#125;"</span>, query);</span><br><span class="line">        Page&lt;Person&gt; page = personSearchRepository.search(queryStringQuery(query), pageable);</span><br><span class="line">        HttpHeaders headers = PaginationUtil.generateSearchPaginationHttpHeaders(query, page, <span class="string">"/api/_search/people"</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> ResponseEntity&lt;&gt;(page.getContent(), headers, HttpStatus.OK);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>其他的不一一列举了，这里着重看下上面两个实现，一个是分页返回列表，一个是ES搜索。<br>分页这里与我们常规有所不同，它是把分页信息通过<code>PaginationUtil.generatePaginationHttpHeaders(page, &quot;/api/people&quot;);</code>这里生成到<code>Header</code>里去了，前端需要从Header里取。</p><h5 id="自定义修改返回类型-Optional"><a href="#自定义修改返回类型-Optional" class="headerlink" title="自定义修改返回类型(Optional)"></a>自定义修改返回类型(Optional)</h5><p>好吧，看到上面肯定有同学要说了，我们平时分页都是返回JSON，所有数据都是返回JSON！<br>如果非得这么做，那就只能自己做个ResponseUtil，把结果包装成如下格式</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"success"</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">"data"</span>:&#123;</span><br><span class="line">        <span class="attr">"content"</span>: [&#123;</span><br><span class="line">            <span class="attr">"name"</span>: <span class="string">"张三"</span>,</span><br><span class="line">            <span class="attr">"country"</span>: <span class="string">"中国"</span>,</span><br><span class="line">            <span class="attr">"sex"</span>: <span class="string">"MALE"</span></span><br><span class="line">        &#125;]</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">"code"</span>: <span class="number">200</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>只需增加两个新类：<br><code>CommonResponse</code><br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CommonResponse</span>&lt;<span class="title">T</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> DEFAULT_CODE = <span class="number">200</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">boolean</span> success;</span><br><span class="line">    <span class="keyword">private</span> T data;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> code=DEFAULT_CODE;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">CommonResponse</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">CommonResponse</span><span class="params">(<span class="keyword">boolean</span> success, T data)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.success = success;</span><br><span class="line">        <span class="keyword">this</span>.data = data;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">CommonResponse</span><span class="params">(<span class="keyword">boolean</span> success, T data, <span class="keyword">int</span> code)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.success = success;</span><br><span class="line">        <span class="keyword">this</span>.data = data;</span><br><span class="line">        <span class="keyword">this</span>.code = code;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    ... <span class="comment">//get &amp; set</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p><code>ResponseUtil</code><br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ResponseUtil</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger LOGGER = LoggerFactory.getLogger(ResponseUtil.class);</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="title">ResponseUtil</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ResponseEntity&lt;CommonResponse&gt; <span class="title">okResponse</span><span class="params">()</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> wrapResponse(<span class="keyword">true</span>, <span class="keyword">null</span>, DEFAULT_CODE);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">ResponseEntity&lt;CommonResponse&gt; <span class="title">wrapResponse</span><span class="params">(<span class="keyword">int</span> statusCode)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> wrapResponse(<span class="keyword">true</span>, <span class="keyword">null</span>, statusCode);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">ResponseEntity&lt;CommonResponse&gt; <span class="title">wrapResponse</span><span class="params">(T data)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> wrapResponse(<span class="keyword">true</span>, data, DEFAULT_CODE);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">ResponseEntity&lt;CommonResponse&gt; <span class="title">wrapResponse</span><span class="params">(T data, Pageable pageable)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> wrapResponse(<span class="keyword">true</span>, data, DEFAULT_CODE);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">ResponseEntity&lt;CommonResponse&gt; <span class="title">wrapResponse</span><span class="params">(T data, <span class="keyword">int</span> statusCode)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> wrapResponse(<span class="keyword">true</span>, data, statusCode);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">ResponseEntity&lt;CommonResponse&gt; <span class="title">wrapResponse</span><span class="params">(<span class="keyword">boolean</span> successful,T data)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> wrapResponse(<span class="keyword">true</span>, data, DEFAULT_CODE);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">ResponseEntity&lt;CommonResponse&gt; <span class="title">wrapResponse</span><span class="params">(<span class="keyword">boolean</span> successful, <span class="keyword">int</span> statusCode)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> wrapResponse(<span class="keyword">true</span>, <span class="keyword">null</span>, statusCode);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">ResponseEntity&lt;CommonResponse&gt; <span class="title">wrapResponse</span><span class="params">(<span class="keyword">boolean</span> successful, T data, <span class="keyword">int</span> statusCode)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> ResponseEntity.ok(<span class="keyword">new</span> CommonResponse&lt;&gt;(successful, data, statusCode));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">ResponseEntity&lt;CommonResponse&gt; <span class="title">wrapResponse</span><span class="params">(<span class="keyword">boolean</span> successful,</span></span></span><br><span class="line"><span class="function"><span class="params">                                                                  Optional&lt;T&gt; maybeResponse,</span></span></span><br><span class="line"><span class="function"><span class="params">                                                                  HttpHeaders headers,</span></span></span><br><span class="line"><span class="function"><span class="params">                                                                  <span class="keyword">int</span> statusCode)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> (ResponseEntity)maybeResponse.map((response) -&gt; &#123;</span><br><span class="line">            CommonResponse&lt;T&gt; commonResponse = <span class="keyword">new</span> CommonResponse&lt;&gt;(successful, response, statusCode);</span><br><span class="line">            <span class="keyword">return</span> ((ResponseEntity.BodyBuilder)ResponseEntity.ok().headers(headers)).body(commonResponse);</span><br><span class="line">        &#125;).orElse(<span class="keyword">new</span> ResponseEntity(<span class="keyword">new</span> CommonResponse&lt;&gt;(successful, <span class="keyword">null</span>, HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">ResponseEntity&lt;CommonResponse&gt; <span class="title">wrapOrNotFound</span><span class="params">(Optional&lt;T&gt; maybeResponse)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> wrapResponse(<span class="keyword">true</span>, maybeResponse, <span class="keyword">null</span>, DEFAULT_CODE);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>然后修改下Controller里面的返回为如下即可<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping</span>(<span class="string">"/people"</span>)</span><br><span class="line"><span class="meta">@Timed</span></span><br><span class="line"><span class="keyword">public</span> ResponseEntity&lt;List&lt;Person&gt;&gt; getAllPeople(<span class="meta">@ApiParam</span> Pageable pageable) &#123;</span><br><span class="line">    log.debug(<span class="string">"REST request to get a page of People"</span>);</span><br><span class="line">    Page&lt;Person&gt; page = personRepository.findAll(pageable);</span><br><span class="line">    <span class="comment">//HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/people");</span></span><br><span class="line">    <span class="comment">//return new ResponseEntity&lt;&gt;(page.getContent(), headers, HttpStatus.OK);</span></span><br><span class="line">    <span class="keyword">return</span> ResponseUtil.wrapResponse(page);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h5 id="打包运行"><a href="#打包运行" class="headerlink" title="打包运行"></a>打包运行</h5><p>先执行<code>yarn install &amp;&amp; bower install</code> (Angular 1.x版本) 或 <code>yarn install</code>(Angular 4版本)对前端代码进行编译。<br>然后可选：</p><ul><li>命令行运行 ./mvnw </li><li>带LiveReload前端调试 gulp (Angular1.x版本)或yarn start(Angular 4版本)</li><li>生产编译 ./mvnw clean package -Pprod</li></ul><p>启动后，默认在本地8080端口启动JHipster的页面，看到已经用它自己的模板实现了常规页面。我们需要做的只是自己做套Angluar页面，套用该模板下的请求处理就好了。</p><h2 id="高级姿势"><a href="#高级姿势" class="headerlink" title="高级姿势"></a>高级姿势</h2><h5 id="Docker集成"><a href="#Docker集成" class="headerlink" title="Docker集成"></a>Docker集成</h5><p>在/src/main/docker/目录下，JHipster提供了docker化所需的所有文件，所以开箱即用。例如，</p><ul><li>启动一个mysql数据库： <code>docker-compose -f src/main/docker/mysql.yml up -d</code></li><li>停止并删除该mysql数据库： <code>docker-compose -f src/main/docker/mysql.yml down</code></li><li>Maven将本项目打包成docker镜像： <code>./mvnw package -Pprod dockerfile:build</code></li><li>启动项目容器： <code>docker-compose -f src/main/docker/app.yml up -d</code></li></ul><p>如果需要maven打包docker镜像后推到Registry，则需要修改pom.xml，将<code>dockerfile-maven-plugin</code>中注释掉的一段给打开。</p><h5 id="CI集成"><a href="#CI集成" class="headerlink" title="CI集成"></a>CI集成</h5><p>(留坑)</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>正常情况下，用<code>Jhipster</code>快速实现普通的JavaWeb项目其实仅需三步：1.初始化项目；2.用JDL创建自己的Entity；3.导入JDL；<br>作为一个脚手架，使用起来已经非常方便了，而且它还支持微服务项目。<br>既然谈到脚手架，不由自主的会与JFinal等其他脚手架对比，JHipster不一定比其他脚手架轻快，但好在代码规范，Spring家族全套，回头看看，确实可以解决文初的那两个问题。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;在基于Spring的Web项目开发中，通常存在两个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;普通CRUD的代码基本重复，完全是体力活；&lt;/li&gt;
&lt;li&gt;Controller层和持久层之间的数据传递，存在不规范。有人喜欢直接返回JSON，有人喜欢用DTO，
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="脚手架" scheme="http://edisonxu.com/categories/Java/%E8%84%9A%E6%89%8B%E6%9E%B6/"/>
    
    
      <category term="SpringBoot" scheme="http://edisonxu.com/tags/SpringBoot/"/>
    
      <category term="JHipster" scheme="http://edisonxu.com/tags/JHipster/"/>
    
      <category term="微服务" scheme="http://edisonxu.com/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
  </entry>
  
  <entry>
    <title>Axon入门系列(八)：AxonFramework与SpringCloud的整合</title>
    <link href="http://edisonxu.com/2017/04/24/axon-spring-cloud.html"/>
    <id>http://edisonxu.com/2017/04/24/axon-spring-cloud.html</id>
    <published>2017-04-24T07:01:51.000Z</published>
    <updated>2021-07-21T13:31:21.782Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>上一篇里，我们在利用Axon3的DistributeCommand的JGroup支持，和DistributedEvent对AMQP的支持，实现了分布式环境下的CQRS和EventSourcing。<br>在这一篇中，我们将把Axon3与当下比较火热的微服务框架——SpringCloud进行整合，并将其微服务化。</p></blockquote><h6 id="写在前面的话"><a href="#写在前面的话" class="headerlink" title="写在前面的话"></a>写在前面的话</h6><p>AxonFramework对SpringCloud的支持，是从3.0.2才开始的，但是在3.0.2和3.0.3两个版本，均存在blocking bug，<strong>所以要想与SpringCloud完成整合，版本必须大于等于3.0.4</strong>。<br>PS：连续跳坑，debug读代码，帮Axon找BUG，血泪换来的结论……好在社区足够活跃，作者也比较给力，连续更新。</p><h2 id="设计"><a href="#设计" class="headerlink" title="设计"></a>设计</h2><p>按照微服务的概念，我们把Product和Order各自相关的功能单独抽出来各做出一个服务，即product-service和order-service。与上一篇不同，这里并没有把各自service的command端和query端单独拆成一个service，而是放在一起了。当然，你也可以自行把他们拆开，中间通过mq传递消息。<br>具体架构如下：<br><img src="/images/2017/04/lesson7_archi.png" alt=""></p><h2 id="前置工作"><a href="#前置工作" class="headerlink" title="前置工作"></a>前置工作</h2><p>首先，我们在父pom中配置好与SpringCloud集成相关的公共Maven依赖。</p><ul><li>对SpringBoot的依赖 (这一块前面我们已经配置过了，这里可以跳过)</li><li>对SpringCloud的依赖</li><li>对具体SpringCloud组件的依赖</li></ul><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">modules</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">module</span>&gt;</span>common-api<span class="tag">&lt;/<span class="name">module</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">module</span>&gt;</span>config-service<span class="tag">&lt;/<span class="name">module</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">module</span>&gt;</span>discovery-service<span class="tag">&lt;/<span class="name">module</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">module</span>&gt;</span>proxy-service<span class="tag">&lt;/<span class="name">module</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">module</span>&gt;</span>product-service<span class="tag">&lt;/<span class="name">module</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">module</span>&gt;</span>order-service<span class="tag">&lt;/<span class="name">module</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">modules</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">dependencyManagement</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-dependencies<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>Camden.SR6<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencyManagement</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- Spring Cloud Features --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-eureka<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-config<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="SpringCloud组件"><a href="#SpringCloud组件" class="headerlink" title="SpringCloud组件"></a>SpringCloud组件</h2><p>熟悉SpringCloud的朋友，可以直接跳过本章。</p><h3 id="Discovery-Serivce"><a href="#Discovery-Serivce" class="headerlink" title="Discovery Serivce"></a>Discovery Serivce</h3><p>使用SpringCloud中的Eureka组件，实现服务注册和发现。各个服务本身把自己注册到Eureka上，Proxy Service使用的zuul，在配置了Eureka相关信息后，会自动从Eureka中发现对应服务名及其地址，与配置文件中进行匹配，从而实现动态路由。<br>同时Eureka提供的UI也可以很直观的对服务当前的状态进行监控。<br>使用Eureka非常简单，引入Maven依赖<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-eureka-server<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>然后在SpringBootApplication的类申明上加上<code>@EnableEurekaServer</code>注解即可。<br>对应配置文件如下：<br><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Configure this Discovery Server</span></span><br><span class="line"><span class="attr">eureka:</span></span><br><span class="line"><span class="attr">  instance:</span></span><br><span class="line"><span class="attr">    hostname:</span> <span class="string">localhost</span></span><br><span class="line"><span class="attr">    lease-expiration-duration-in-seconds:</span> <span class="number">5</span></span><br><span class="line"><span class="attr">    lease-renewal-interval-in-seconds:</span> <span class="number">5</span></span><br><span class="line"><span class="attr">  client:</span> <span class="comment">#Not a client, don't register with yourself</span></span><br><span class="line"><span class="attr">    registerWithEureka:</span> <span class="literal">false</span></span><br><span class="line"><span class="attr">    fetchRegistry:</span> <span class="literal">false</span></span><br><span class="line"><span class="attr">    healthcheck:</span></span><br><span class="line"><span class="attr">      enabled:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">  server:</span></span><br><span class="line"><span class="attr">      enable-self-preservation:</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line"><span class="attr">endpoints:</span></span><br><span class="line"><span class="attr"> shutdown:</span></span><br><span class="line"><span class="attr">  enabled:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">server:</span></span><br><span class="line"><span class="attr">  port:</span> <span class="number">1111</span> <span class="comment">#HTTP(Tomcat) port</span></span><br></pre></td></tr></table></figure></p><p>没什么花样，只是申明自己不是EurekaClient，而是Server。<br>Eureka有一个自我保护机制关闭，默认打开的情况下，当注册的service”挂掉”后，Eureka短时间内并不会直接把它从列表内清除，而是保留一段时间。因为Eureka的设计者认为分布式环境中网络是不可靠的，也许因为网络的原因，Eureka Server没有收到实例的心跳，但并不说命实例就完蛋了，所以这种机制下，它仍然鼓励客户端再去尝试调用这个所谓DOWN状态的实例，如果确实调用失败了，断路器机制还可以派上用场。这里我们方便起见，直接使用server.enable-self-preservation设置为false关闭掉它。（生产别这么用）</p><h3 id="Proxy-Service"><a href="#Proxy-Service" class="headerlink" title="Proxy Service"></a>Proxy Service</h3><p>使用SpringCloud中的zuul组件。具体作用有：</p><ul><li>全局网关，屏蔽内部系统和网络</li><li>请求拦截和动态路由</li><li>请求负载均衡<br>zuul的使用配置非常简单，引入Maven依赖<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-zuul<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></li></ul><p>然后在SpringBootApplication类申明上加上<code>@EnableZuulProxy</code>和<code>@EnableDiscoveryClient</code>注解即可。<code>@EnableDiscoveryClient</code>是把Proxy Service注册到Eureka上。<br>对应配置文件如下：<br><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line"><span class="attr">  application:</span></span><br><span class="line"><span class="attr">    name:</span> <span class="string">proxy-service</span></span><br><span class="line"></span><br><span class="line"><span class="attr">  cloud:</span></span><br><span class="line"><span class="attr">    config:</span></span><br><span class="line">      <span class="string">discovery.enabled:</span> <span class="literal">true</span></span><br><span class="line">      <span class="string">discovery.serviceId:</span> <span class="string">config-service</span></span><br><span class="line"><span class="attr">      failFast:</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Discovery Server Access</span></span><br><span class="line"><span class="attr">eureka:</span></span><br><span class="line"><span class="attr">  client:</span></span><br><span class="line"><span class="attr">    serviceUrl:</span></span><br><span class="line"><span class="attr">      defaultZone:</span> <span class="attr">http://$&#123;config.host:10.1.110.21&#125;:1111/eureka/</span></span><br><span class="line"></span><br><span class="line"><span class="attr">zuul:</span></span><br><span class="line"><span class="attr">  ignoredServices:</span> <span class="string">'*'</span></span><br><span class="line"><span class="attr">  routes:</span></span><br><span class="line"><span class="attr">    product_command_path:</span></span><br><span class="line"><span class="attr">      path:</span> <span class="string">/product/**</span></span><br><span class="line"><span class="attr">      stripPrefix:</span> <span class="literal">false</span></span><br><span class="line"><span class="attr">      serviceId:</span> <span class="string">product-service</span></span><br><span class="line"><span class="attr">    product_query_path:</span></span><br><span class="line"><span class="attr">      path:</span> <span class="string">/products/**</span></span><br><span class="line"><span class="attr">      stripPrefix:</span> <span class="literal">false</span></span><br><span class="line"><span class="attr">      serviceId:</span> <span class="string">product-service</span></span><br><span class="line"><span class="attr">    order-command_path:</span></span><br><span class="line"><span class="attr">      serviceId:</span> <span class="string">order-service</span></span><br><span class="line"><span class="attr">      path:</span> <span class="string">/order/**</span></span><br><span class="line"><span class="attr">      stripPrefix:</span> <span class="literal">false</span></span><br><span class="line"><span class="attr">    order_query_path:</span></span><br><span class="line"><span class="attr">      serviceId:</span> <span class="string">order-service</span></span><br><span class="line"><span class="attr">      path:</span> <span class="string">/orders/**</span></span><br><span class="line"><span class="attr">      stripPrefix:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure></p><p><code>spring.application.name</code> 属性指定服务名<br><code>spring.cloud.config</code> 相关的是配置ConfigService去Eureka上找serviceId为<code>config-service</code>的服务<br><code>eureka.client.serviceUrl.defaultZone</code> 配置要注册的Eureka的地址<br><code>ignoredServices</code>设为<em>，即不转发除了下面<code>routes</code>以外的所有请求<br><code>routes.&lt;xxx&gt;.path</code> 是映射xxx服务与URL地址<br><code>routes.&lt;xxx&gt;.stripPrefix</code> 是不使用前缀，即将<a href="http://product/" target="_blank" rel="noopener">http://product/</a></em> 请求直接转发到product-service。如果设置了前缀，那么合法路径则变为http://<prefix>/product/* 。<br><code>routes.&lt;xxx&gt;.serviceId</code> 即Eureka上xxx服务所注册的服务名，zuul从Eureka上找到该服务名所对应的服务器信息，从而实现动态路由。<br>这里为了演示zuul对不同路径映射到相同服务，我故意把command和query端的URL地址设为不同，如/product和/products。</prefix></p><h3 id="Cloud-Configs-Service"><a href="#Cloud-Configs-Service" class="headerlink" title="Cloud Configs Service"></a>Cloud Configs Service</h3><p>使用SpringCloud中的Cloud组件，实现统一文件配置。（未引入SpringCloudBus实现配置修改通知，可自行修改添加。）<br>一样，引入Maven依赖<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-config-server<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>在SpringBootApplication的类声明前加上<code>@EnableConfigServer</code>和<code>@EnableDiscoveryClient</code>注解。<code>@EnableDiscoveryClient</code>是把Config Service注册到Eureka上。<br>SpringCloudConfig最大的好处，可以从git读取配置，给不同环境、不同zone设置不同分支，根据profile指定分支，非常方便。<br>在这里为了方便各位自己跑，我把Config Service配置为读取本地文件。<br><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server:</span></span><br><span class="line"><span class="attr">  port:</span> <span class="number">1000</span></span><br><span class="line"></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="comment"># Active reading config from local file system</span></span><br><span class="line"><span class="attr">  profiles:</span></span><br><span class="line"><span class="attr">    active:</span> <span class="string">native</span></span><br><span class="line"></span><br><span class="line"><span class="attr">  application:</span></span><br><span class="line"><span class="attr">    name:</span> <span class="string">config-service</span></span><br><span class="line"></span><br><span class="line"><span class="attr">  cloud:</span></span><br><span class="line"><span class="attr">    config:</span></span><br><span class="line"><span class="attr">      server:</span></span><br><span class="line"><span class="attr">        native:</span></span><br><span class="line"><span class="attr">          searchLocations:</span> <span class="string">/usr/edi/spring/configs</span></span><br><span class="line"></span><br><span class="line"><span class="attr">management:</span></span><br><span class="line"><span class="attr">  context-path:</span> <span class="string">/admin</span></span><br><span class="line"></span><br><span class="line"><span class="attr">eureka:</span></span><br><span class="line"><span class="attr">  client:</span></span><br><span class="line"><span class="attr">    serviceUrl:</span></span><br><span class="line"><span class="attr">      defaultZone:</span> <span class="attr">http://localhost:1111/eureka/</span></span><br></pre></td></tr></table></figure></p><h2 id="业务服务"><a href="#业务服务" class="headerlink" title="业务服务"></a>业务服务</h2><p>在前一篇<a href="http://edisonxu.com/2017/04/01/axon-distribute.html">CQRS和Event Souring系列（八）：DistributeCommand和DistributeEvent</a> 中提到过，DistributedCommandBus不会直接调用command handler，它只是在不同JVM的commandbus之间建立一个“桥梁”，通过指定<code>CommandRouter</code>和<code>CommandBusConnector</code>进行Command的分发。<code>axon-distributed-commandbus-springcloud</code>包提供了SpringCloud环境下的<code>CommandRouter</code>和<code>CommandBusConnector</code>。<br><strong><em>CommandRouter</em></strong><br><code>SpringCloudCommandRouter</code>是该包中<code>CommandRouter</code>的具体实现类，其实是调用了我们在SpringBootApplication中<code>@EnableDiscoveryClient</code>后注入的EurekaClient。<br>每一个Axon的command节点在启动时，会通过DiscoveryClient把本地所有的CommandHandler变向的塞入本地服务在Eureka上的metadata信息中。当DistributedCommandBus发送command时，通过DiscoveryClient从Eureka上获取所有节点信息后，找到metadata中的CommandHandler的信息进行command匹配，分发到匹配的节点去处理command。</p><p><strong><em>CommandBusConnector</em></strong><br><code>SpringHttpCommandBusConnector</code>是<code>CommandBusConnector</code>的具体实现类，它其实在本地起了一个地址为”/spring-command-bus-connector”的REST接口，用以接受来自其他节点的command请求。<br>同时，它也覆写了方法<code>CommandBusConnector</code>中的send方法，用以发送command到经<code>CommandRouter</code>确认的目标地址。当然，它会先判断目标地址是否本地，如果是本地，则直接调用localCommandBus去处理了，否则，则使用RestTemplate将Command发送到远程地址。</p><p>所以，启用Axon对SpringCloud的支持，必须要有三步（引入<code>axon-spring-boot-autoconfigure</code>的前提下）：</p><ol><li>引入<code>axon-distributed-commandbus-springcloud</code>包依赖；</li><li>配置文件中<code>axon.distributed.enabled</code>设置为true;</li><li>在自己的配置类中提供一个名字为<code>restTemplate</code>的Bean，返回一个RestTemplate的对象；</li></ol><p><strong>注意！</strong><br>目前不能在RestTemplate声明时加上@LoadBalance启用Ribbon做负载均衡，因为<code>SpringHttpCommandBusConnector</code>在发送远程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即是前面<code>SpringHttpCommandBusConnector</code>自己从DiscoveryClient那已经解析出来的ip。</p><h3 id="Product-Serivce"><a href="#Product-Serivce" class="headerlink" title="Product Serivce"></a>Product Serivce</h3><p>核心代码与上一篇并无大区别，依然是CQRS，C端采用JPA将Event持久化到Mysql，而Q端将数据保存在MongoDB，方便查询（好吧，这仅仅是为了show一下怎么样在C、Q端使用不同的持久层而已，存Event的话，MongoDB比MySql适合的多）。这里只把不同地方中关键的列出来说一下，详细请查阅代码。<br><em><strong>pom依赖</strong></em><br>引入<code>axon-distributed-commandbus-springcloud</code>包依赖<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.axonframework<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>axon-distributed-commandbus-springcloud<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;axon.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p><code>AMQPConfiguration</code><br>配置AMQP协议的mq绑定，用于把Event分发到mq中，最终由Order Service的OrderSaga去处理。Product Serivce本身不消费Order Service所产生的Event，本地的EventHandle并不会走MQ。详细配置这里就省略了，可以参见上一篇文章或者看具体代码。</p><p><code>CloudConfiguration</code><br>这个类啥都不干，只是创建一个restTemplate的实例<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CloudConfiguration</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> RestTemplate <span class="title">restTemplate</span><span class="params">()</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> RestTemplate();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>启动类<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="meta">@EnableDiscoveryClient</span></span><br><span class="line"><span class="meta">@ComponentScan</span>(basePackages = &#123;<span class="string">"com.edi.learn"</span>&#125;)</span><br><span class="line"><span class="meta">@EnableJpaRepositories</span>(basePackages = &#123;<span class="string">"com.edi.learn.cloud.command"</span>&#125;)</span><br><span class="line"><span class="meta">@EnableMongoRepositories</span>(basePackages = &#123;<span class="string">"com.edi.learn.cloud.query"</span>&#125;)</span><br><span class="line"><span class="meta">@EnableAutoConfiguration</span>()</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Application</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String args[])</span></span>&#123;</span><br><span class="line">        SpringApplication.run(Application.class, args);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>配置文件的修改上面已经提过了，这里就不再重复。</p><h3 id="Order-Serivce"><a href="#Order-Serivce" class="headerlink" title="Order Serivce"></a>Order Serivce</h3><p>就启用SpringCloud来说，与上面没有任何区别。为了让OrderSaga能正常收到并处理来自于prodcut-service的事件，必须要进行额外配置。前一篇文章中提到的<code>@ProcessGroup</code>，并不适用于Saga，同时，Axon3中，目前对于Saga处理distributed event并不是很友好，3.0.4以前，Saga只能支持绑定一个EventStore，但是分布式情况下，一个service可能要监听多个queue，所以3.0.4中，支持了自定义Saga配置，即可以声明一个&lt;saga_name&gt;+SagaConfiguration作为Bean名，并返回SagaConfiguration类型的Bean。为了让Saga能处理来自于外部MQ的事件，我们必须提供一个orderSagaConfiguration。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> SpringAMQPMessageSource <span class="title">queueMessageSource</span><span class="params">(Serializer serializer)</span></span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> SpringAMQPMessageSource(serializer)&#123;</span><br><span class="line">        <span class="meta">@RabbitListener</span>(queues = <span class="string">"orderqueue"</span>)</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="meta">@Transactional</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onMessage</span><span class="params">(Message message, Channel channel)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">            LOGGER.debug(<span class="string">"Message received: "</span>+message.toString());</span><br><span class="line">            <span class="keyword">super</span>.onMessage(message, channel);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> SagaConfiguration&lt;OrderSaga&gt; <span class="title">orderSagaConfiguration</span><span class="params">(Serializer serializer)</span></span>&#123;</span><br><span class="line">    SagaConfiguration&lt;OrderSaga&gt; sagaConfiguration = SagaConfiguration.subscribingSagaManager(OrderSaga.class, c-&gt; queueMessageSource(serializer));</span><br><span class="line">    <span class="comment">//sagaConfiguration.registerHandlerInterceptor(c-&gt;transactionManagingInterceptor());</span></span><br><span class="line">    <span class="keyword">return</span> sagaConfiguration;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> TransactionManagingInterceptor <span class="title">transactionManagingInterceptor</span><span class="params">()</span></span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> TransactionManagingInterceptor(<span class="keyword">new</span> SpringTransactionManager(transactionManager));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>如上面代码，自行指定Saga的message source，这样来自于product-service写入mq的ProductReservedEvent等事件就能被Saga正确处理。<br>这里要注意的是事务问题，由于我们是通过MQ的onMessage来启动具体的SagaCommandHandler，上下文中并未定义事务特性，但是由于我们引入了Spring的jpa包，axon3的auto configuration会自动启用SagaJpaRepository，也就是说，onMessage方法线程执行时，会牵扯到DB的更新，必须得给它指定一个transaction manager。这里有两种方法：</p><ol><li>使用@Transactional 注解，让Spring自行配置；</li><li>在SagaConfiguration中注册TransactionManagingInterceptor。</li></ol><p>另外，由于在创建订单时，只传了Product的Id，根据id去查询当前product的最新详情，需要请求Product Service的query端。这个query端我们是用<code>spring-boot-starter-data-rest</code>直接暴露出去的<a href="https://en.wikipedia.org/wiki/HATEOAS" target="_blank" rel="noopener">HATEOAS</a>(Hypermedia as the Engine of Application State)风格的RESTFul接口。即是说，要做一个跨服务的REST请求，且要支持HATEOAS，那么我们就使用<a href="https://github.com/OpenFeign/feign" target="_blank" rel="noopener">Feign</a>加上<code>spring-boot-starter-hateoas</code>。</p><ol><li><p>更新pom</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-feign<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-hateoas<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></li><li><p>在order-service中添加一个Feign Client</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@FeignClient</span>(value = <span class="string">"product-service"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">ProductService</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@RequestMapping</span>(value = <span class="string">"/products"</span>, method = RequestMethod.GET)</span><br><span class="line">    <span class="function">Resources&lt;ProductDto&gt; <span class="title">getProducts</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@RequestMapping</span>(value = <span class="string">"/products/&#123;id&#125;"</span>, method = RequestMethod.GET)</span><br><span class="line">    <span class="function">ProductDto <span class="title">getProduct</span><span class="params">(@PathVariable(<span class="string">"id"</span>)</span> String productId)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>在SpringBootApplication中启用FeignClient和HypermediaSupport</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="meta">@EnableDiscoveryClient</span></span><br><span class="line"><span class="meta">@ComponentScan</span>(basePackages = &#123;<span class="string">"com.edi.learn"</span>&#125;)</span><br><span class="line"><span class="meta">@EnableJpaRepositories</span>(basePackages = &#123;<span class="string">"com.edi.learn.cloud.command"</span>&#125;)</span><br><span class="line"><span class="meta">@EnableMongoRepositories</span>(basePackages = &#123;<span class="string">"com.edi.learn.cloud.query"</span>&#125;)</span><br><span class="line"><span class="meta">@EnableFeignClients</span>(basePackages = &#123;<span class="string">"com.edi.learn.cloud.common.web"</span>&#125;)</span><br><span class="line"><span class="meta">@EnableHypermediaSupport</span>(type = EnableHypermediaSupport.HypermediaType.HAL)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Application</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String args[])</span></span>&#123;</span><br><span class="line">        SpringApplication.run(Application.class, args);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ol><p>ProductDto都是封装属性的POJO，就不写了。这样我们就可以在代码中直接注入ProductService，并调用相应方法从product-service端取数据了。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>至此，Axon3与SpringCloud的集成已完毕。Axon3使用SpringCloud提供的服务注册和发现机制，来进行Command的分发和处理。具体运行情况我就不写了，大家可自行修改order-service的配置，去跑多个order-service。留个悬念，由于是同一段代码和配置，mq我们使用fanout，即分发的模式，所有节点都会收到ProductReservedEvent，是否所有节点都会处理呢？</p><h6 id="写在后面的话"><a href="#写在后面的话" class="headerlink" title="写在后面的话"></a>写在后面的话</h6><p>截止到本篇，Axon3使用的大部分功能都已经做了入门介绍，并写了例子，作为研究，算是入门了,尤其是文档中没有说明的一些关键地方，我都在文中提了出来。掉过不少坑，看了很多源码， 回头看来，我对Axon3的设计是肯定与失望并存。<br>肯定的是Axon3的易用性与性能，尤其是DisruptorCommandBus配合CachingGenericEventSourcingRepository（采用了LMAX的<a href="https://lmax-exchange.github.io/disruptor/" target="_blank" rel="noopener">Disruptor框架</a>，可以看下一篇比较早的文章介绍，猛击<a href="http://blog.trifork.com/2011/07/20/processing-1m-tps-with-axon-framework-and-the-disruptor/" target="_blank" rel="noopener">这里</a>或<a href="http://ifeve.com/axon/" target="_blank" rel="noopener">中文翻译版</a>）;<br>失望的是Axon3更多的优化和针对都集中在单体应用上，对分布式和微服务的集成稍显简单，例如负载均衡的支持、容错性的支持等，目前尚未看到介绍。<br>当然，这块现在也才刚刚起步，后续应该会变得越来越好。原期望于Axon3直接把这块做掉或者提供支持，现在看来是否我想太多，这块本就不该它做呢？欢迎加群57241527讨论。</p><p>照例，本文源码：<a href="https://github.com/EdisonXu/sbs-axon/tree/master/lesson-7" target="_blank" rel="noopener">https://github.com/EdisonXu/sbs-axon/tree/master/lesson-7</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;上一篇里，我们在利用Axon3的DistributeCommand的JGroup支持，和DistributedEvent对AMQP的支持，实现了分布式环境下的CQRS和EventSourcing。&lt;br&gt;在这一篇中，我们将把Axon3与当下比较火热
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="CQRS" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/CQRS/"/>
    
      <category term="Axon" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/CQRS/Axon/"/>
    
    
      <category term="event sourcing" scheme="http://edisonxu.com/tags/event-sourcing/"/>
    
      <category term="CQRS" scheme="http://edisonxu.com/tags/CQRS/"/>
    
      <category term="axon" scheme="http://edisonxu.com/tags/axon/"/>
    
      <category term="DDD" scheme="http://edisonxu.com/tags/DDD/"/>
    
  </entry>
  
  <entry>
    <title>Axon入门系列(七)：DistributeCommand和DistributeEvent</title>
    <link href="http://edisonxu.com/2017/04/01/axon-distribute.html"/>
    <id>http://edisonxu.com/2017/04/01/axon-distribute.html</id>
    <published>2017-04-01T07:01:51.000Z</published>
    <updated>2021-07-21T13:31:21.781Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>上一篇我们才算真正实现了一个基于Axon3的例子，本篇我们来尝试实现在分布式环境下利用Axon3做CQRS，即把CommandSide和QuerySide变成两个独立应用，分别可以启多份实例。</p></blockquote><p>首先，我们回顾一下CQRS&amp;EventSourcing模式下，整个架构的关键点，或者说最大的特点：</p><ul><li>CommandSide和QuerySide的持久层分离；</li><li>保存对Aggregate状态造成变化的Event，而不是状态本身；</li><li>Aggregate的状态全局原子化操作；</li><li>适用于读大于写的场景；<br>我们前面的例子，是在一个应用里面实现了CQRS模式，而在分布式场景下，有如下要求：</li><li>CommandSide和QuerySide可以不在同一个节点(甚至不在同一个应用)下；</li><li>CommandSide不同的CommandHandler、EventHandler可以不在同一个节点；</li><li>不同CommandSide对同一个Aggregate的操作应具有原子性；<br>我们来一步步满足这三个要求。</li></ul><h2 id="拆分CommandSide和QuerySide"><a href="#拆分CommandSide和QuerySide" class="headerlink" title="拆分CommandSide和QuerySide"></a>拆分CommandSide和QuerySide</h2><p>这个其实比较好解决，直接把两者分别用两个SpringBoot来承载就好了，只需要引入一个MQ，传递从CommandSide到QuerySide的事件就好了。<br>Axon提供了对AMQP协议的MQ的支持，我们可以直接拿来用。当然，你也可以用Kafka等其他MQ，只是需要自己实现了。<br>具体关于Axon对AMQP的支持，在后面会详述。</p><h2 id="实现CommandHandler的分布式调用"><a href="#实现CommandHandler的分布式调用" class="headerlink" title="实现CommandHandler的分布式调用"></a>实现CommandHandler的分布式调用</h2><p>前文中提到过，Axon提供的四种CommandBus的实现中，有一个<code>DistributedCommandBus</code>，<code>DistributedCommandBus</code>不会直接调用command handler，它只是在不同JVM的commandbus之间建立一个“桥梁”。每个JVM上的<code>DistributedCommandBus</code>被称为“Segment”。<br><img src="/images/2017/03/distributed-command-bus.png" alt=""><br><code>DistributedCommandBus</code>要求提供两个参数:</p><ol><li><code>CommandRouter</code>提供路由表，指明应当把Command发到哪里。<code>CommandRouter</code>的实现必须提供Routing Strategy，以此来计算Routing Key。Axon提供了两种Routing Strategy：<ul><li>MetaDataRoutingStrategy 使用CommandMessage中的MetaData的property来找到路由key</li><li>AnnotationRoutingStrategy（默认） 使用Command中@TargetIdentifier标识的field做路由key<br><strong>所以，当使用<code>DistributeCommandBus</code>时，如果使用默认的Routing Strategy，一定要在Command中提供@TargetIdentifier</strong></li></ul></li><li><code>CommandBusConnector</code>管理链接，提供发送、订阅方法<br>Axon目前提供了两种Connector的实现：JGroupsConnector和SpringCloudConnector。本文将使用JGroup，后者将放到后一篇与SpringCloud集成一文中使用。<br>起用JGroupsConnector很简单，只需要确保如下两个依赖存在：<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.axonframework<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>axon-spring-boot-starter-jgroups<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;axon.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.axonframework<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>axon-spring-boot-autoconfigure<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;axon.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></li></ol><p>axon-spring-boot-autoconfigure提供了自动配置，在<code>AxonAutoConfiguration</code>类中，可以发现有如下源码<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@ConditionalOnClass</span>(name = &#123;<span class="string">"org.axonframework.jgroups.commandhandling.JGroupsConnector"</span>, <span class="string">"org.jgroups.JChannel"</span>&#125;)</span><br><span class="line"><span class="meta">@EnableConfigurationProperties</span>(JGroupsConfiguration.JGroupsProperties.class)</span><br><span class="line"><span class="meta">@ConditionalOnProperty</span>(<span class="string">"axon.distributed.jgroups.enabled"</span>)</span><br><span class="line"><span class="meta">@AutoConfigureAfter</span>(JpaConfiguration.class)</span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">JGroupsConfiguration</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger logger = LoggerFactory.getLogger(JGroupsConfiguration.class);</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> JGroupsProperties jGroupsProperties;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ConditionalOnProperty</span>(<span class="string">"axon.distributed.jgroups.gossip.autoStart"</span>)</span><br><span class="line">    <span class="meta">@Bean</span>(destroyMethod = <span class="string">"stop"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> GossipRouter <span class="title">gossipRouter</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        Matcher matcher =</span><br><span class="line">                Pattern.compile(<span class="string">"([^[\\[]]*)\\[(\\d*)\\]"</span>).matcher(jGroupsProperties.getGossip().getHosts());</span><br><span class="line">        <span class="keyword">if</span> (matcher.find()) &#123;</span><br><span class="line"></span><br><span class="line">            GossipRouter gossipRouter = <span class="keyword">new</span> GossipRouter(matcher.group(<span class="number">1</span>), Integer.parseInt(matcher.group(<span class="number">2</span>)));</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                gossipRouter.start();</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                logger.warn(<span class="string">"Unable to autostart start embedded Gossip server: &#123;&#125;"</span>, e.getMessage());</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> gossipRouter;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            logger.error(<span class="string">"Wrong hosts pattern, cannot start embedded Gossip Router: "</span> +</span><br><span class="line">                                 jGroupsProperties.getGossip().getHosts());</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ConditionalOnMissingBean</span></span><br><span class="line">    <span class="meta">@Primary</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> DistributedCommandBus <span class="title">distributedCommandBus</span><span class="params">(CommandRouter router, CommandBusConnector connector)</span> </span>&#123;</span><br><span class="line">        DistributedCommandBus commandBus = <span class="keyword">new</span> DistributedCommandBus(router, connector);</span><br><span class="line">        commandBus.updateLoadFactor(jGroupsProperties.getLoadFactor());</span><br><span class="line">        <span class="keyword">return</span> commandBus;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ConditionalOnMissingBean</span>(&#123;CommandRouter.class, CommandBusConnector.class&#125;)</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> JGroupsConnectorFactoryBean <span class="title">jgroupsConnectorFactoryBean</span><span class="params">(Serializer serializer,</span></span></span><br><span class="line"><span class="function"><span class="params">                                                                   @Qualifier(<span class="string">"localSegment"</span>)</span> CommandBus</span></span><br><span class="line"><span class="function">                                                                           localSegment) </span>&#123;</span><br><span class="line"></span><br><span class="line">        System.setProperty(<span class="string">"jgroups.tunnel.gossip_router_hosts"</span>, jGroupsProperties.getGossip().getHosts());</span><br><span class="line">        System.setProperty(<span class="string">"jgroups.bind_addr"</span>, String.valueOf(jGroupsProperties.getBindAddr()));</span><br><span class="line">        System.setProperty(<span class="string">"jgroups.bind_port"</span>, String.valueOf(jGroupsProperties.getBindPort()));</span><br><span class="line"></span><br><span class="line">        JGroupsConnectorFactoryBean jGroupsConnectorFactoryBean = <span class="keyword">new</span> JGroupsConnectorFactoryBean();</span><br><span class="line">        jGroupsConnectorFactoryBean.setClusterName(jGroupsProperties.getClusterName());</span><br><span class="line">        jGroupsConnectorFactoryBean.setLocalSegment(localSegment);</span><br><span class="line">        jGroupsConnectorFactoryBean.setSerializer(serializer);</span><br><span class="line">        jGroupsConnectorFactoryBean.setConfiguration(jGroupsProperties.getConfigurationFile());</span><br><span class="line">        <span class="keyword">return</span> jGroupsConnectorFactoryBean;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ConfigurationProperties</span>(prefix = <span class="string">"axon.distributed.jgroups"</span>)</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">JGroupsProperties</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">private</span> Gossip gossip;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * Enables JGroups configuration for this application</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">boolean</span> enabled = <span class="keyword">false</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * The name of the JGroups cluster to connect to. Defaults to "Axon".</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> String clusterName = <span class="string">"Axon"</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * The JGroups configuration file to use. Defaults to a TCP Gossip based configuration</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> String configurationFile = <span class="string">"default_tcp_gossip.xml"</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * The address of the network interface to bind JGroups to. Defaults to a global IP address of this node.</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> String bindAddr = <span class="string">"GLOBAL"</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * Sets the initial port to bind the JGroups connection to. If this port is taken, JGroups will find the</span></span><br><span class="line"><span class="comment">         * next available port.</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> String bindPort = <span class="string">"7800"</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * Sets the loadFactor for this node to join with. The loadFactor sets the relative load this node will</span></span><br><span class="line"><span class="comment">         * receive compared to other nodes in the cluster. Defaults to 100.</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">int</span> loadFactor = <span class="number">100</span>;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> Gossip <span class="title">getGossip</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> gossip;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setGossip</span><span class="params">(Gossip gossip)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">this</span>.gossip = gossip;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">isEnabled</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> enabled;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setEnabled</span><span class="params">(<span class="keyword">boolean</span> enabled)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">this</span>.enabled = enabled;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> String <span class="title">getClusterName</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> clusterName;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setClusterName</span><span class="params">(String clusterName)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">this</span>.clusterName = clusterName;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> String <span class="title">getConfigurationFile</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> configurationFile;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setConfigurationFile</span><span class="params">(String configurationFile)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">this</span>.configurationFile = configurationFile;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> String <span class="title">getBindAddr</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> bindAddr;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setBindAddr</span><span class="params">(String bindAddr)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">this</span>.bindAddr = bindAddr;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> String <span class="title">getBindPort</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> bindPort;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setBindPort</span><span class="params">(String bindPort)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">this</span>.bindPort = bindPort;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getLoadFactor</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> loadFactor;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setLoadFactor</span><span class="params">(<span class="keyword">int</span> loadFactor)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">this</span>.loadFactor = loadFactor;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">Gossip</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">            <span class="comment">/**</span></span><br><span class="line"><span class="comment">             * Whether to automatically attempt to start a Gossip Routers. The host and port of the Gossip server</span></span><br><span class="line"><span class="comment">             * are taken from the first define host in 'hosts'.</span></span><br><span class="line"><span class="comment">             */</span></span><br><span class="line">            <span class="keyword">private</span> <span class="keyword">boolean</span> autoStart = <span class="keyword">false</span>;</span><br><span class="line"></span><br><span class="line">            <span class="comment">/**</span></span><br><span class="line"><span class="comment">             * Defines the hosts of the Gossip Routers to connect to, in the form of host[port],...</span></span><br><span class="line"><span class="comment">             * &lt;p&gt;</span></span><br><span class="line"><span class="comment">             * If autoStart is set to &#123;<span class="doctag">@code</span> true&#125;, the first host and port are used as bind address and bind port</span></span><br><span class="line"><span class="comment">             * of the Gossip server to start.</span></span><br><span class="line"><span class="comment">             * &lt;p&gt;</span></span><br><span class="line"><span class="comment">             * Defaults to localhost[12001].</span></span><br><span class="line"><span class="comment">             */</span></span><br><span class="line">            <span class="keyword">private</span> String hosts = <span class="string">"localhost[12001]"</span>;</span><br><span class="line"></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">isAutoStart</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> autoStart;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setAutoStart</span><span class="params">(<span class="keyword">boolean</span> autoStart)</span> </span>&#123;</span><br><span class="line">                <span class="keyword">this</span>.autoStart = autoStart;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="function"><span class="keyword">public</span> String <span class="title">getHosts</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                <span class="keyword">return</span> hosts;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setHosts</span><span class="params">(String hosts)</span> </span>&#123;</span><br><span class="line">                <span class="keyword">this</span>.hosts = hosts;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>可以看到我们只需在application.properties中添加<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">axon.distributed.jgroups.enabled=true</span><br><span class="line">axon.distributed.jgroups.gossip.autoStart=true</span><br></pre></td></tr></table></figure></p><p>就可以启用JGroupsConnector。同时也可以用前缀axon.distributed.jgroups加上<code>JGroupsProperties</code>里定义的各种field名来做JGroup的配置。（默认连接本地7800端口）<br>这里值得注意的是：</p><ol><li><code>JGroupsConnectorFactoryBean</code>实现的方法中，有一段<em>System.setProperty(“jgroups.tunnel.gossiprouterhosts”, jGroupsProperties.getGossip().getHosts());</em> ，如果axon.distributed.jgroups.gossip.autoStart未设为true(默认false)，那么getGossip()显然将会报空指针异常。</li><li><code>JacksonSerializer</code>的实现中，并未去考虑Jackson对Exception的处理(<strong>objectMapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)</strong>)，导致一旦在Command执行时发生异常，DistributeCommandBus也会尝试把这个Exception消息进行序列化，而Jackson默认是无法处理java.lang.Throwable类的，就会发生序列化错误<em>org.codehaus.jackson.map.exc.UnrecognizedPropertyException: Unrecognized field “cause” (Class java.lang.Throwable), not marked as ignorable</em>，从而导致把真正的Exception给掩埋掉了。所以，这里我就改回默认的XStreamSerializer。</li><li>默认情况下，<code>localSegment</code>是SimpleCommandBus，所以参考前文，可以使用sendAndWait把异常抛到最前端处理，或者用send(command, callback)传入一个callback，在callback的onFailure方法对Throwable进行处理。</li></ol><h2 id="实现EventHandler的分布式调用"><a href="#实现EventHandler的分布式调用" class="headerlink" title="实现EventHandler的分布式调用"></a>实现EventHandler的分布式调用</h2><p>通常情况下，Event的分发我们第一时间想到的就是MQ，Axon也不例外，提供了对<a href="https://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol" target="_blank" rel="noopener">AMQP</a>(Advanced Message Queuing Protocol)的支持，例如Rabbit MQ。<br>引入如下Maven依赖：<br><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.axonframework<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>axon-amqp<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-amqp<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.axonframework<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>axon-spring-boot-autoconfigure<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></p><p>spring-boot-starter-amqp提供具体AMQP实现的服务，axon-amqp提供具体的Event分发机制实现，axon-spring-boot-autoconfigure提供AMQP的自动配置。<br>AxonAutoConfiguration中，关于AMQP部分的源码如下：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@ConditionalOnClass</span>(&#123;SpringAMQPPublisher.class, ConnectionFactory.class&#125;)</span><br><span class="line"><span class="meta">@EnableConfigurationProperties</span>(AMQPProperties.class)</span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">AMQPConfiguration</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> AMQPProperties amqpProperties;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ConditionalOnMissingBean</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> RoutingKeyResolver <span class="title">routingKeyResolver</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> PackageRoutingKeyResolver();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ConditionalOnMissingBean</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> AMQPMessageConverter <span class="title">amqpMessageConverter</span><span class="params">(Serializer serializer, RoutingKeyResolver routingKeyResolver)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> DefaultAMQPMessageConverter(serializer, routingKeyResolver, amqpProperties.isDurableMessages());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ConditionalOnProperty</span>(<span class="string">"axon.amqp.exchange"</span>)</span><br><span class="line">    <span class="meta">@Bean</span>(initMethod = <span class="string">"start"</span>, destroyMethod = <span class="string">"shutDown"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> SpringAMQPPublisher <span class="title">amqpBridge</span><span class="params">(EventBus eventBus, ConnectionFactory connectionFactory,</span></span></span><br><span class="line"><span class="function"><span class="params">                                          AMQPMessageConverter amqpMessageConverter)</span> </span>&#123;</span><br><span class="line">        SpringAMQPPublisher publisher = <span class="keyword">new</span> SpringAMQPPublisher(eventBus);</span><br><span class="line">        publisher.setExchangeName(amqpProperties.getExchange());</span><br><span class="line">        publisher.setConnectionFactory(connectionFactory);</span><br><span class="line">        publisher.setMessageConverter(amqpMessageConverter);</span><br><span class="line">        <span class="keyword">switch</span> (amqpProperties.getTransactionMode()) &#123;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">case</span> TRANSACTIONAL:</span><br><span class="line">                publisher.setTransactional(<span class="keyword">true</span>);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> PUBLISHER_ACK:</span><br><span class="line">                publisher.setWaitForPublisherAck(<span class="keyword">true</span>);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> NONE:</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> IllegalStateException(<span class="string">"Unknown transaction mode: "</span> + amqpProperties.getTransactionMode());</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> publisher;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>可以看到，只要引入了Spring关于AMQP的starter包，我们只需要在application.properties中用<code>axon.amqp.exchange=Axon.EventBus</code>指明AMQP的exchange名字就可以启用了，非常方便。<br>另外就是需要给spring-boot-starter-amqp提供amqp具体实现的配置，这里我们以<a href="https://www.rabbitmq.com/" target="_blank" rel="noopener">RabbitMq</a>为例：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># mq</span><br><span class="line">spring.rabbitmq.host=10.1.110.21</span><br><span class="line">spring.rabbitmq.port=5672</span><br><span class="line">spring.rabbitmq.username=axon</span><br><span class="line">spring.rabbitmq.password=axon</span><br><span class="line">axon.amqp.exchange=Axon.EventBus</span><br></pre></td></tr></table></figure></p><p>RabbitMqServer的搭建我这里就不叙述了，网上一搜一大把。但为了方便理解，我还是简单介绍下AMQP和RabbitMq的一些关键要素。<br>首先看一下AMQP的”生产/消费”模型图<br><img src="/images/2017/04/producer_consumer.png" alt=""><br>我们关注里面的三个核心概念</p><ul><li><code>Exchange</code>: 交换器，message到达broker的第一站，根据分发策略，匹配查询表中的routing key，分发消息到queue中去。</li><li><code>Queue</code>：消息最终被送到这里等待consumer取走。一个message可以被同时拷贝到多个queue中。</li><li><code>Binding</code>：Exchange与Queue之间的绑定关系，指定了绑定策略，即消息的分发策略。<br>分发策略有以下四种：</li><li><code>direct</code><br><img src="/images/2017/04/direct_exchange.png" alt=""><br>“先匹配, 再投送”. 即在绑定时设定一个<code>routing_key</code>, 消息的<code>routing_key</code>匹配时, 才会被交换器投送到绑定的队列中去.</li><li><code>fanout</code><br><img src="/images/2017/04/fanout_exchange.png" alt=""><br>把消息转发给所有绑定的队列上, 就是一个”广播”行为.</li><li><code>topic</code><br><img src="/images/2017/04/topic_exchange.png" alt=""><br>与direct类似，只是绑定的<code>routing_key</code>支持匹配规则（并不是正则！），会把消息自己的<code>routing_key</code>与绑定的<code>routing_key</code>进行匹配操作，只把匹配成功的发到对应queue中。<br>这里有个“坑”，rabbit提供的*绑定一个任意字母，#绑定0个或多个字母匹配规则中，#并不能直接使用，比如#test#就无法匹配aatest33，必须要#.test.#才起作用，匹配aa.test.33，也是醉了。所以Axon默认提供的RoutingKey生成就是根据包名来匹配……</li><li><code>headers</code><br>不使用<code>routing_key</code>，而使用<code>headers</code>来做匹配。</li></ul><p>那么我们来对AMQP在代码中做exchange和queue的绑定，以及对event的listen动作。<br><code>AMQPConfiguration</code><br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AMQPConfiguration</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Value</span>(<span class="string">"$&#123;axon.amqp.exchange&#125;"</span>)</span><br><span class="line">  <span class="keyword">private</span> String exchangeName;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Bean</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> Queue <span class="title">productQueue</span><span class="params">()</span></span>&#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> Queue(<span class="string">"product"</span>, <span class="keyword">true</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Bean</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> Queue <span class="title">orderQueue</span><span class="params">()</span></span>&#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> Queue(<span class="string">"order"</span>,<span class="keyword">true</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Bean</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> Exchange <span class="title">exchange</span><span class="params">()</span></span>&#123;</span><br><span class="line">      <span class="keyword">return</span> ExchangeBuilder.topicExchange(exchangeName).durable(<span class="keyword">true</span>).build();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Bean</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> Binding <span class="title">productQueueBinding</span><span class="params">()</span> </span>&#123;</span><br><span class="line">      <span class="keyword">return</span> BindingBuilder.bind(productQueue()).to(exchange()).with(<span class="string">"#.product.#"</span>).noargs();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Bean</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> Binding <span class="title">orderQueueBinding</span><span class="params">()</span> </span>&#123;</span><br><span class="line">      <span class="keyword">return</span> BindingBuilder.bind(orderQueue()).to(exchange()).with(<span class="string">"#.order.#"</span>).noargs();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/*@Bean</span></span><br><span class="line"><span class="comment">  public SpringAMQPMessageSource productQueueMessageSource(Serializer serializer)&#123;</span></span><br><span class="line"><span class="comment">      return new SpringAMQPMessageSource(serializer)&#123;</span></span><br><span class="line"><span class="comment">          @RabbitListener(queues = "product")</span></span><br><span class="line"><span class="comment">          @Override</span></span><br><span class="line"><span class="comment">          public void onMessage(Message message, Channel channel) throws Exception &#123;</span></span><br><span class="line"><span class="comment">              LOGGER.debug("Product message received: "+message.toString());</span></span><br><span class="line"><span class="comment">              super.onMessage(message, channel);</span></span><br><span class="line"><span class="comment">          &#125;</span></span><br><span class="line"><span class="comment">      &#125;;</span></span><br><span class="line"><span class="comment">  &#125;</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">  @Bean</span></span><br><span class="line"><span class="comment">  public SpringAMQPMessageSource orderQueueMessageSource(Serializer serializer)&#123;</span></span><br><span class="line"><span class="comment">      return new SpringAMQPMessageSource(serializer)&#123;</span></span><br><span class="line"><span class="comment">          @RabbitListener(queues = "order")</span></span><br><span class="line"><span class="comment">          @Override</span></span><br><span class="line"><span class="comment">          public void onMessage(Message message, Channel channel) throws Exception &#123;</span></span><br><span class="line"><span class="comment">              LOGGER.debug("Order message received: "+message.toString());</span></span><br><span class="line"><span class="comment">              super.onMessage(message, channel);</span></span><br><span class="line"><span class="comment">          &#125;</span></span><br><span class="line"><span class="comment">      &#125;;</span></span><br><span class="line"><span class="comment">  &#125;*/</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>注意，由于本例中，我并没有把Product和Order相关的Service拆分成两个应用，仍然在一个CommandSide中，所以其实我们根本用不到分布式EventHandler，local可以完成的操作，放到其他node去做，反而降低了性能。<br>所以，我这里并没有在CommandSide的这个<code>AMQPConfiguration</code>中去配置监听queue。这里的队列其实是CommandSide和QuerySide之间用的。<br>但配置和原理都是一样的，如果把Product和Order分开，<code>ProductReservedEvent</code>在ProductServcices所在节点扔到队列后，可按需配置绑定，让OrderService能够取到该事件，交给Saga中的EventHandler去处理。<br>在后面与SpringCloud集成的一文中，就会这样做。<br>QuerySide的<code>AMQPConfiguration</code>与上面一致，但是要打开被注释掉的部分。因为exchange和queue是自动创建的，有可能QuerySide先启动，所以必须要在QuerySide也加上exchange和queue的定义及绑定策略。<br><code>@RabbitListener(queues = &quot;product&quot;)</code>用来指定当前AMQPMessageSource要监听哪个queue。<br>同时，还需要修改application.properties，来绑定AMQPMessageSource和具体的EventHandler注册类<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">axon.eventhandling.processors.product.source=productQueueMessageSource</span><br><span class="line">axon.eventhandling.processors.order.source=orderQueueMessageSource</span><br></pre></td></tr></table></figure></p><p>axon.eventhandling.processors.[processors_group_name].source中，前面axon.eventhandling.processors.[processors_group_name]其实是一个ProcessingGroup，Axon提供了注解@ProcessingGroup(“[processors_group_name]”)来进行标识。<br>所以我们需要在QuerySide的<code>ProductEventHandler</code>和<code>OrderEventHandler</code>上面增加该注解<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@ProcessingGroup</span>(<span class="string">"order"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderEventHandler</span></span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@ProcessingGroup</span>(<span class="string">"product"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ProductEventHandler</span> </span>&#123;&#125;</span><br></pre></td></tr></table></figure></p><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p>为方便测试，我们来增加一个对Product库存进行调整的接口，这样可以启动两个CommandSide，同时对库存进行调整，看看会不会有并发问题。<br>同样，先定义Commmand和对应的Event：<br>ChangeStockCommand(productId, number)<br>IncreaseStockCommand extends ChangeStockCommand<br>DecreaseStockCommand extends ChangeStockCommand<br>IncreaseStockEvent(productId, number)<br>DecreaseStockEvent(productId, number)</p><p>修改<code>ProductAggregate</code>，增加对应的CommandHandler和EventHandler<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Aggregate</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ProductAggregate</span> </span>&#123;</span><br><span class="line">  ......</span><br><span class="line"></span><br><span class="line">  <span class="meta">@CommandHandler</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(IncreaseStockCommand command)</span> </span>&#123;</span><br><span class="line">      apply(<span class="keyword">new</span> IncreaseStockEvent(command.getId(),command.getNumber()));</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@CommandHandler</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(DecreaseStockCommand command)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span>(stock&gt;=command.getNumber())</span><br><span class="line">          apply(<span class="keyword">new</span> DecreaseStockEvent(command.getId(),command.getNumber()));</span><br><span class="line">      <span class="keyword">else</span></span><br><span class="line">          <span class="keyword">throw</span> <span class="keyword">new</span> NoEnoughStockException(<span class="string">"No enough items"</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@EventHandler</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">on</span><span class="params">(IncreaseStockEvent event)</span></span>&#123;</span><br><span class="line">      stock = stock + event.getNumber();</span><br><span class="line">      LOGGER.info(<span class="string">"Product &#123;&#125; stock increase &#123;&#125;, current value: &#123;&#125;"</span>, id, event.getNumber(), stock);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@EventHandler</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">on</span><span class="params">(DecreaseStockEvent event)</span></span>&#123;</span><br><span class="line">      stock = stock - event.getNumber();</span><br><span class="line">      LOGGER.info(<span class="string">"Product &#123;&#125; stock decrease &#123;&#125;, current value: &#123;&#125;"</span>, id, event.getNumber(), stock);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>最后对外增加一个REST接口：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping</span>(<span class="string">"/product"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ProductController</span> </span>&#123;</span><br><span class="line">  ......</span><br><span class="line">  <span class="meta">@PutMapping</span>(<span class="string">"/&#123;id&#125;"</span>)</span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">change</span><span class="params">(@PathVariable(value = <span class="string">"id"</span>)</span> String id,</span></span><br><span class="line"><span class="function">                     @<span class="title">RequestBody</span><span class="params">(required = <span class="keyword">true</span>)</span> JSONObject input,</span></span><br><span class="line"><span class="function">                     HttpServletResponse response)</span>&#123;</span><br><span class="line">      <span class="keyword">boolean</span> isIncrement = input.getBooleanValue(<span class="string">"incremental"</span>);</span><br><span class="line">      <span class="keyword">int</span> number = input.getIntValue(<span class="string">"number"</span>);</span><br><span class="line">      ChangeStockCommand command = isIncrement? <span class="keyword">new</span> IncreaseStockCommand(id, number) : <span class="keyword">new</span> DecreaseStockCommand(id, number);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">          <span class="comment">// multiply 100 on the price to avoid float number</span></span><br><span class="line">          <span class="comment">//commandGateway.send(command, LoggingCallback.INSTANCE);</span></span><br><span class="line">          commandGateway.sendAndWait(command);</span><br><span class="line">          response.setStatus(HttpServletResponse.SC_OK);<span class="comment">// Set up the 201 CREATED response</span></span><br><span class="line">          <span class="keyword">return</span>;</span><br><span class="line">      &#125; <span class="keyword">catch</span> (CommandExecutionException cex) &#123;</span><br><span class="line">          LOGGER.warn(<span class="string">"Add Command FAILED with Message: &#123;&#125;"</span>, cex.getMessage());</span><br><span class="line">          response.setStatus(HttpServletResponse.SC_BAD_REQUEST);</span><br><span class="line">          <span class="keyword">if</span> (<span class="keyword">null</span> != cex.getCause()) &#123;</span><br><span class="line">              LOGGER.warn(<span class="string">"Caused by: &#123;&#125; &#123;&#125;"</span>, cex.getCause().getClass().getName(), cex.getCause().getMessage());</span><br><span class="line">              <span class="keyword">if</span> (cex.getCause() <span class="keyword">instanceof</span> ConcurrencyException) &#123;</span><br><span class="line">                  LOGGER.warn(<span class="string">"Concurrent issue happens for product &#123;&#125;"</span>, id);</span><br><span class="line">                  response.setStatus(HttpServletResponse.SC_CONFLICT);</span><br><span class="line">              &#125;</span><br><span class="line">          &#125;</span><br><span class="line">      &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">          <span class="comment">// should not happen</span></span><br><span class="line">          LOGGER.error(<span class="string">"Unexpected exception is thrown"</span>, e);</span><br><span class="line">          response.setStatus(HttpServletResponse.SC_BAD_REQUEST);</span><br><span class="line">      &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>这里我用了sendAndWait，把Exception一路抛上来在Controller捕获。你也可以用我注掉的那段send(command,callback)，传入一个callback，在callback的onFailure方法去处理。<br>同样，QuerySide要对这两个事件进行处理<br><code>ProductEventHandler</code><br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure></p><p>好，最后我们把CommandSide的server.port配成0（随机端口），启动两个CommandSide(假定一个端口为&lt;first_port&gt;，一个为&lt;second_port&gt;)和一个QuerySide。</p><ol><li>POST请求到<a href="http://127.0.0.1" target="_blank" rel="noopener">http://127.0.0.1</a>:&lt;first_port&gt;/product/1?name=ttt&amp;price=10&amp;stock=100 创建商品；</li><li>POST请求到<a href="http://127.0.0.1" target="_blank" rel="noopener">http://127.0.0.1</a>:&lt;second_port&gt;/product/1?name=ttt&amp;price=10&amp;stock=100 会发现报错，商品已存在；</li><li>GET请求到<a href="http://127.0.0.1:8080/product/1" target="_blank" rel="noopener">http://127.0.0.1:8080/product/1</a> 在QuerySide查看商品是否创建成功；</li><li><p>PUT如下json到<a href="http://127.0.0.1" target="_blank" rel="noopener">http://127.0.0.1</a>:&lt;first_port&gt;/product/1 来增加库存；</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line"><span class="attr">"incremental"</span>:<span class="literal">true</span>,</span><br><span class="line"><span class="attr">"number"</span>:<span class="number">10</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>PUT如下json到<a href="http://127.0.0.1" target="_blank" rel="noopener">http://127.0.0.1</a>:&lt;second_port&gt;/product/1 来减少库存；</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line"><span class="attr">"incremental"</span>:<span class="literal">false</span>,</span><br><span class="line"><span class="attr">"number"</span>:<span class="number">101</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>重置MongoDB的库，同时发送3、4，看看结果。<br>其实我们如果去MongoDB的Events里面查看，数据如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line">&gt; db.events.find().pretty()</span><br><span class="line">&#123;</span><br><span class="line">        <span class="attr">"_id"</span> : ObjectId(<span class="string">"58ec4ef673bc0c1c188117b9"</span>),</span><br><span class="line">        <span class="attr">"aggregateIdentifier"</span> : <span class="string">"1"</span>,</span><br><span class="line">        <span class="attr">"type"</span> : <span class="string">"ProductAggregate"</span>,</span><br><span class="line">        <span class="attr">"sequenceNumber"</span> : NumberLong(<span class="number">0</span>),</span><br><span class="line">        <span class="attr">"serializedPayload"</span> : <span class="string">"&lt;com.edi.learn.axon.events.product.ProductCreatedEvent&gt;&lt;id&gt;1&lt;/id&gt;&lt;name&gt;ttt&lt;/name&gt;&lt;price&gt;1000&lt;/price&gt;&lt;stock&gt;100&lt;/stock&gt;&lt;/com.edi.learn.axon.events.product.ProductCreatedEvent&gt;"</span>,</span><br><span class="line">        <span class="attr">"timestamp"</span> : <span class="string">"2017-04-11T03:35:18.310Z"</span>,</span><br><span class="line">        <span class="attr">"payloadType"</span> : <span class="string">"com.edi.learn.axon.events.product.ProductCreatedEvent"</span>,</span><br><span class="line">        <span class="attr">"payloadRevision"</span> : <span class="literal">null</span>,</span><br><span class="line">        <span class="attr">"serializedMetaData"</span> : <span class="string">"&lt;meta-data&gt;&lt;entry&gt;&lt;string&gt;traceId&lt;/string&gt;&lt;string&gt;af292c24-bde4-4ba1-a190-9743822f839c&lt;/string&gt;&lt;/entry&gt;&lt;entry&gt;&lt;string&gt;correlationId&lt;/string&gt;&lt;string&gt;af292c24-bde4-4ba1-a190-9743822f839c&lt;/string&gt;&lt;/entry&gt;&lt;/meta-data&gt;"</span>,</span><br><span class="line">        <span class="attr">"eventIdentifier"</span> : <span class="string">"ed244ef3-a1fe-48fb-99b8-39ebd2444cc1"</span></span><br><span class="line">&#125;</span><br><span class="line">&#123;</span><br><span class="line">        <span class="attr">"_id"</span> : ObjectId(<span class="string">"58ec4f0273bc0c1c188117ba"</span>),</span><br><span class="line">        <span class="attr">"aggregateIdentifier"</span> : <span class="string">"1"</span>,</span><br><span class="line">        <span class="attr">"type"</span> : <span class="string">"ProductAggregate"</span>,</span><br><span class="line">        <span class="attr">"sequenceNumber"</span> : NumberLong(<span class="number">1</span>),</span><br><span class="line">        <span class="attr">"serializedPayload"</span> : <span class="string">"&lt;com.edi.learn.axon.events.product.IncreaseStockEvent&gt;&lt;id&gt;1&lt;/id&gt;&lt;number&gt;10&lt;/number&gt;&lt;/com.edi.learn.axon.events.product.IncreaseStockEvent&gt;"</span>,</span><br><span class="line">        <span class="attr">"timestamp"</span> : <span class="string">"2017-04-11T03:35:30.728Z"</span>,</span><br><span class="line">        <span class="attr">"payloadType"</span> : <span class="string">"com.edi.learn.axon.events.product.IncreaseStockEvent"</span>,</span><br><span class="line">        <span class="attr">"payloadRevision"</span> : <span class="literal">null</span>,</span><br><span class="line">        <span class="attr">"serializedMetaData"</span> : <span class="string">"&lt;meta-data&gt;&lt;entry&gt;&lt;string&gt;traceId&lt;/string&gt;&lt;string&gt;05252e0c-eb0b-4ed0-945c-0134fa94b6ba&lt;/string&gt;&lt;/entry&gt;&lt;entry&gt;&lt;string&gt;correlationId&lt;/string&gt;&lt;string&gt;05252e0c-eb0b-4ed0-945c-0134fa94b6ba&lt;/string&gt;&lt;/entry&gt;&lt;/meta-data&gt;"</span>,</span><br><span class="line">        <span class="attr">"eventIdentifier"</span> : <span class="string">"f6b9786d-4abd-4407-a40b-880f88738b4b"</span></span><br><span class="line">&#125;</span><br><span class="line">&#123;</span><br><span class="line">        <span class="attr">"_id"</span> : ObjectId(<span class="string">"58ec4f0d73bc0c1ad83281d6"</span>),</span><br><span class="line">        <span class="attr">"aggregateIdentifier"</span> : <span class="string">"1"</span>,</span><br><span class="line">        <span class="attr">"type"</span> : <span class="string">"ProductAggregate"</span>,</span><br><span class="line">        <span class="attr">"sequenceNumber"</span> : NumberLong(<span class="number">2</span>),</span><br><span class="line">        <span class="attr">"serializedPayload"</span> : <span class="string">"&lt;com.edi.learn.axon.events.product.DecreaseStockEvent&gt;&lt;id&gt;1&lt;/id&gt;&lt;number&gt;101&lt;/number&gt;&lt;/com.edi.learn.axon.events.product.DecreaseStockEvent&gt;"</span>,</span><br><span class="line">        <span class="attr">"timestamp"</span> : <span class="string">"2017-04-11T03:35:41.474Z"</span>,</span><br><span class="line">        <span class="attr">"payloadType"</span> : <span class="string">"com.edi.learn.axon.events.product.DecreaseStockEvent"</span>,</span><br><span class="line">        <span class="attr">"payloadRevision"</span> : <span class="literal">null</span>,</span><br><span class="line">        <span class="attr">"serializedMetaData"</span> : <span class="string">"&lt;meta-data&gt;&lt;entry&gt;&lt;string&gt;traceId&lt;/string&gt;&lt;string&gt;cf21b4a8-dfae-4da8-a6e0-964876c101c3&lt;/string&gt;&lt;/entry&gt;&lt;entry&gt;&lt;string&gt;correlationId&lt;/string&gt;&lt;string&gt;cf21b4a8-dfae-4da8-a6e0-964876c101c3&lt;/string&gt;&lt;/entry&gt;&lt;/meta-data&gt;"</span>,</span><br><span class="line">        <span class="attr">"eventIdentifier"</span> : <span class="string">"ac9db091-73fd-4830-9ddb-85fea3a13206"</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ol><p>其实可以发现<code>sequenceNumber</code>一值是递增的，说明Event在分布式环境中也是严格按时间排序的。这样即便是在两个不同的CommandSide节点，当我们尝试去改变Aggregate的状态时，Axon会做ES来从Repository里获取当前Aggregate的最新状态，从而实现了原子性操作。</p><p>本文完整代码：<a href="https://github.com/EdisonXu/sbs-axon/tree/master/lesson-6" target="_blank" rel="noopener">https://github.com/EdisonXu/sbs-axon/tree/master/lesson-6</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;上一篇我们才算真正实现了一个基于Axon3的例子，本篇我们来尝试实现在分布式环境下利用Axon3做CQRS，即把CommandSide和QuerySide变成两个独立应用，分别可以启多份实例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先，我们回
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="CQRS" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/CQRS/"/>
    
      <category term="Axon" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/CQRS/Axon/"/>
    
    
      <category term="event sourcing" scheme="http://edisonxu.com/tags/event-sourcing/"/>
    
      <category term="CQRS" scheme="http://edisonxu.com/tags/CQRS/"/>
    
      <category term="axon" scheme="http://edisonxu.com/tags/axon/"/>
    
      <category term="DDD" scheme="http://edisonxu.com/tags/DDD/"/>
    
  </entry>
  
  <entry>
    <title>Axon入门系列(六)：Saga的使用</title>
    <link href="http://edisonxu.com/2017/03/31/axon-saga.html"/>
    <id>http://edisonxu.com/2017/03/31/axon-saga.html</id>
    <published>2017-03-31T03:37:32.000Z</published>
    <updated>2021-07-21T13:31:21.782Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>在上一篇里面，我们正式的使用了CQRS模式完成了AXON的第一个真正的例子，但是细心的朋友会发现一个问题，创建订单时并没有检查商品库存。<br>库存是否足够直接回导致订单状态的成功与否，在并发时可能还会出现超卖。当库存不足时还需要回滚订单，所以这里出现了复杂的跨Aggregate事务问题。<br>Saga就是为解决这里复杂流程而生的。</p></blockquote><h2 id="Saga"><a href="#Saga" class="headerlink" title="Saga"></a><strong>Saga</strong></h2><p><strong>Saga</strong> 这个名词最早是由Hector Garcia-Molina和Kenneth Salem写的<a href="http://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf" target="_blank" rel="noopener">Sagas</a>这篇论文里提出来的，但其实Saga并不是什么新事物，在我们传统的系统设计中，它有个更熟悉的名字——“ProcessManager”，只是换了个马甲，还是干同样的事——组合一组逻辑处理复杂流程。<br>但它与我们平常理解的“ProgressManager”又有不同，它的提出，最早是是为了解决分布式系统中长时间运行事务(long-running business process)的问题，把单一的transaction按照步骤分成一组若干子transaction，通过补偿机制实现最终一致性。<br>举个例子，在一个交易环节中有下单支付两个步骤，如果是传统方式，两个步骤在一个事务里，统一成功或回滚，然而如果支付时间很长，那么就会导致第一步，即下单这里所占用的资源被长时间锁定，可能会对系统可用性造成影响。如果用Saga来实现，那么下单是一个独立事务，下单的事务先提交，提交成功后开始支付的事务，如果支付成功，则支付的事务也提交，整个流程就算完成，但是如果支付事务执行失败，那么支付需要回滚，因为这时下单事务已经提交，则需要对下单操作进行补偿操作（可能是回滚，也可能是变成新状态）。<br>可以看到Saga是牺牲了数据的强一致性，实现最终一致性。</p><blockquote><p>Saga的概念使得强一致性的分布式事务不再是唯一的解决方案，通过保证事务中每一步都可以一个补偿机制，在发生错误后执行补偿事务来保证系统的可用性和最终一致性。</p></blockquote><p>在CQRS中，我们尽量遵从“聚合尽量设计的小，且一次修改只修改一个聚合”的原则（与OO中高内聚，低耦合的原则相同），所以当我们需要完成一个复杂流程时，就可能涉及到对多个Aggregate状态的改变，我们就可以把整个过程管理统一放到Saga来定义。</p><h2 id="设计"><a href="#设计" class="headerlink" title="设计"></a>设计</h2><p>把我们的订单创建流程修改成以下：<br><img src="/images/2017/03/flowchart.png" alt=""></p><h2 id="创建Command和Event"><a href="#创建Command和Event" class="headerlink" title="创建Command和Event"></a>创建Command和Event</h2><p>在上一篇例子的基础上，创建如下Command和Event<br>-ReserveProductCommand (orderId, productId, number)<br>-RollbackReservationCommand (orderId, productId, number)<br>-ConfirmOrderCommand (orderId)<br>-RollbackOrderCommand (orderId)<br>-ProductReservedEvent (orderId, productId, number)<br>-ProductNotEnoughEvent (orderId, productId)<br>-OrderCancelledEvent (orderId)<br>-OrderConfirmedEvent (orderId)</p><p>都是POJO，这里我就不放代码了。具体可以去源代码看。</p><h2 id="创建Saga"><a href="#创建Saga" class="headerlink" title="创建Saga"></a>创建Saga</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Saga</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderSaga</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger LOGGER = getLogger(OrderSaga.class);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> OrderId orderIdentifier;</span><br><span class="line">    <span class="keyword">private</span> Map&lt;String, OrderProduct&gt; toReserve;</span><br><span class="line">    <span class="keyword">private</span> Map&lt;String, OrderProduct&gt; toRollback;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> toReserveNumber;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">boolean</span> needRollback;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">transient</span> CommandGateway commandGateway;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@StartSaga</span></span><br><span class="line">    <span class="meta">@SagaEventHandler</span>(associationProperty = <span class="string">"orderId"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(OrderCreatedEvent event)</span></span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.orderIdentifier = event.getOrderId();</span><br><span class="line">        <span class="keyword">this</span>.toReserve = event.getProducts();</span><br><span class="line">        toRollback = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line">        toReserveNumber = toReserve.size();</span><br><span class="line">        event.getProducts().forEach((id,product)-&gt;&#123;</span><br><span class="line">            ReserveProductCommand command = <span class="keyword">new</span> ReserveProductCommand(orderIdentifier, id, product.getAmount());</span><br><span class="line">            commandGateway.send(command);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@SagaEventHandler</span>(associationProperty = <span class="string">"orderId"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(ProductNotEnoughEvent event)</span></span>&#123;</span><br><span class="line">        LOGGER.info(<span class="string">"No enough item to buy"</span>);</span><br><span class="line">        toReserveNumber--;</span><br><span class="line">        needRollback=<span class="keyword">true</span>;</span><br><span class="line">        <span class="keyword">if</span>(toReserveNumber==<span class="number">0</span>)</span><br><span class="line">            tryFinish();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">tryFinish</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(needRollback)&#123;</span><br><span class="line">            toReserve.forEach((id, product)-&gt;&#123;</span><br><span class="line">                <span class="keyword">if</span>(!product.isReserved())</span><br><span class="line">                    <span class="keyword">return</span>;</span><br><span class="line">                toRollback.put(id, product);</span><br><span class="line">                commandGateway.send(<span class="keyword">new</span> RollbackReservationCommand(orderIdentifier, id, product.getAmount()));</span><br><span class="line">            &#125;);</span><br><span class="line">            <span class="keyword">if</span>(toRollback.isEmpty())</span><br><span class="line">                commandGateway.send(<span class="keyword">new</span> RollbackOrderCommand(orderIdentifier));</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        commandGateway.send(<span class="keyword">new</span> ConfirmOrderCommand(orderIdentifier));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@SagaEventHandler</span>(associationProperty = <span class="string">"orderId"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(ReserveCancelledEvent event)</span></span>&#123;</span><br><span class="line">        toRollback.remove(event.getProductId());</span><br><span class="line">        <span class="keyword">if</span>(toRollback.isEmpty())</span><br><span class="line">            commandGateway.send(<span class="keyword">new</span> RollbackOrderCommand(event.getOrderId()));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@SagaEventHandler</span>(associationProperty = <span class="string">"id"</span>, keyName = <span class="string">"orderId"</span>)</span><br><span class="line">    <span class="meta">@EndSaga</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(OrderCancelledEvent event)</span> <span class="keyword">throws</span> OrderCreateFailedException </span>&#123;</span><br><span class="line">        LOGGER.info(<span class="string">"Order &#123;&#125; is cancelled"</span>, event.getId());</span><br><span class="line">        <span class="comment">// throw exception here will not cause the onFailure() method in the command callback</span></span><br><span class="line">        <span class="comment">//throw new OrderCreateFailedException("Not enough product to reserve!");</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@SagaEventHandler</span>(associationProperty = <span class="string">"orderId"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(ProductReservedEvent event)</span></span>&#123;</span><br><span class="line">        OrderProduct reservedProduct = toReserve.get(event.getProductId());</span><br><span class="line">        reservedProduct.setReserved(<span class="keyword">true</span>);</span><br><span class="line">        toReserveNumber--;</span><br><span class="line">        <span class="keyword">if</span>(toReserveNumber ==<span class="number">0</span>)</span><br><span class="line">            tryFinish();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@SagaEventHandler</span>(associationProperty = <span class="string">"id"</span>, keyName = <span class="string">"orderId"</span>)</span><br><span class="line">    <span class="meta">@EndSaga</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(OrderConfirmedEvent event)</span></span>&#123;</span><br><span class="line">        LOGGER.info(<span class="string">"Order &#123;&#125; is confirmed"</span>, event.getId());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Saga的启动和结束"><a href="#Saga的启动和结束" class="headerlink" title="Saga的启动和结束"></a>Saga的启动和结束</h3><p>Axon中通过<code>@Saga</code>注解标识Saga。Saga有起点和终点，必须以<code>@StartSaga</code>和<code>@EndSaga</code>区分清楚。一个Saga的起点可能只有一个，但终点可能有好几个，对应流程的不同结果。<br>默认情况下，只有在找不到同类型已存在的Saga instance时，才会创建一个新的Saga。但是可以通过更改<code>@StartSaga</code>中的<code>forceNew</code>为true让它每次都新建一个。<br>只有当<code>@EndSaga</code>对应的方法被顺利执行，Saga才会结束，但也可以直接从Saga内部调用<code>end()</code>方法强制结束。</p><h3 id="EventHandling"><a href="#EventHandling" class="headerlink" title="EventHandling"></a>EventHandling</h3><p>Saga通过<code>@SagaEventHandler</code>注解来标明EventHandler，与普通EventHandler基本一致，唯一的不同是，普通的EventHandler会接受所有对应的Event，而Saga的EventHandler只处理与其关联过的Event。<br>当被注解<code>@StartSaga</code>的方法调用时，axon默认会根据当前<code>@SagaEventHandler</code>中的<code>associationProperty</code>去找Event中的field，然后把它的值与当前Saga进行关联，类似<code>&lt;saga_id,&lt;key,value&gt;&gt;</code>这种形式。<br>一旦产生关联，该Saga在遇到同一Event时，只会处理<code>&lt;key,value&gt;</code>与已关联值完全一致的Event。例如，有两个<code>OrderCreatedEvent</code>，我们定义<code>associationProperty =&quot;orderId&quot;</code>，两个event的orderId分别为1、2，当Saga创建时接受了orderId=1的<code>OrderCreatedEvent</code>后，值为2的Event它就不再处理了。<br>也可以在Saga内直接调用<code>associateWith(String key, String/Number value)</code>来做这个关联。例如，<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderManagementSaga</span> </span>&#123;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">boolean</span> paid = <span class="keyword">false</span>;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">boolean</span> delivered = <span class="keyword">false</span>;</span><br><span class="line"><span class="meta">@Inject</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">transient</span> CommandGateway commandGateway;</span><br><span class="line">  <span class="meta">@StartSaga</span></span><br><span class="line">  <span class="meta">@SagaEventHandler</span>(associationProperty = <span class="string">"orderId"</span>)</span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(OrderCreatedEvent event)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// client generated identifiers</span></span><br><span class="line">    ShippingId shipmentId = createShipmentId();</span><br><span class="line">    InvoiceId invoiceId = createInvoiceId();</span><br><span class="line">    <span class="comment">// associate the Saga with these values, before sending the commands</span></span><br><span class="line">    associateWith(<span class="string">"shipmentId"</span>, shipmentId);</span><br><span class="line">    associateWith(<span class="string">"invoiceId"</span>, invoiceId);</span><br><span class="line">    <span class="comment">// send the commands</span></span><br><span class="line">    commandGateway.send(<span class="keyword">new</span> PrepareShippingCommand(...));</span><br><span class="line">    commandGateway.send(<span class="keyword">new</span> CreateInvoiceCommand(...));</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="meta">@SagaEventHandler</span>(associationProperty = <span class="string">"shipmentId"</span>)</span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(ShippingArrivedEvent event)</span> </span>&#123;</span><br><span class="line">    delivered = <span class="keyword">true</span>;</span><br><span class="line">    <span class="keyword">if</span> (paid) &#123; end(); &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="meta">@SagaEventHandler</span>(associationProperty = <span class="string">"invoiceId"</span>)</span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(InvoicePaidEvent event)</span> </span>&#123;</span><br><span class="line">    paid = <span class="keyword">true</span>;</span><br><span class="line">    <span class="keyword">if</span> (delivered) &#123; end(); &#125;</span><br><span class="line">  &#125;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>有时我们可能并不想直接使用Event里field的名称作为<code>associationProperty</code>的值，可以使用keyName来对应field名称。<br>Saga是靠Event驱动的，但有时command发出去了，并没有在规定时间内收到预期的Event怎么办？Saga提供了<code>EventScheduler</code>，通过Java内置的scheduler或Quarz，定时自动发送一个Event到这个Saga。<br>Saga的执行是在独立的线程里，所以我们无法通过commandgateway的sendAndWait方法等到其返回值或捕获异常。</p><h3 id="Saga-Store"><a href="#Saga-Store" class="headerlink" title="Saga Store"></a>Saga Store</h3><p>由于Sage在处理过程中也存在中间状态，而Saga的一些业务流程可能会执行很长时间，比如好几天，那么万一系统重启Saga的状态就丢失了，所以Saga也需要能够通过ES恢复，即指定一个<code>SagaStore</code>。<br><code>SagaStore</code>与<code>EventStore</code>的使用除了名字外，基本没有任何区别，也内置了InMemory,JPA,jdbc,Mongo四种实现这里我就不多叙述了。</p><blockquote><p>注意！当持久化Saga时，对于注入的资源field，如CommandGateway，一定要加上<code>transient</code>修饰符，这样Serializer才不会去序列化这个field。当Saga从Repository读出来的时候，会自动注入相关的资源。</p></blockquote><p>只需要显示的提供一个<code>SagaStore</code>的配置就可以了。当启用JPA时，默认会启动<code>JpaSagaStore</code>。我们这里使用<code>MongoSagaStore</code>，修改<code>AxonConfiguration</code>如下：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AxonConfiguration</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  .....</span><br><span class="line">  <span class="meta">@Bean</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> SagaStore <span class="title">sagaStore</span><span class="params">()</span></span>&#123;</span><br><span class="line">      org.axonframework.mongo.eventhandling.saga.repository.MongoTemplate mongoTemplate =</span><br><span class="line">              <span class="keyword">new</span> org.axonframework.mongo.eventhandling.saga.repository.DefaultMongoTemplate(mongoClient(), mongoDbName, <span class="string">"sagas"</span>);</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">new</span> MongoSagaStore(mongoTemplate, axonJsonSerializer());</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p><strong>在@StartSaga执行后，会把当前Saga插入到指定的SagaStore中，当@EndSaga执行时，axon会自动的从SagaStore中删除该Saga。</strong></p><h2 id="修改Handler"><a href="#修改Handler" class="headerlink" title="修改Handler"></a>修改Handler</h2><p>由于<code>ReserveProductCommand</code>和<code>RollbackReservationCommand</code>是需要查找原ProductAggregate的，所以单独创建一个<code>ProductHandler</code><br><code>ProductHandler</code><br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ProductHandler</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger LOGGER = getLogger(ProductHandler.class);</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> Repository&lt;ProductAggregate&gt; repository;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@CommandHandler</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">on</span><span class="params">(ReserveProductCommand command)</span></span>&#123;</span><br><span class="line">        Aggregate&lt;ProductAggregate&gt; aggregate = repository.load(command.getProductId());</span><br><span class="line">        aggregate.execute(aggregateRoot-&gt;aggregateRoot.reserve(command.getOrderId(), command.getNumber()));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@CommandHandler</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">on</span><span class="params">(RollbackReservationCommand command)</span></span>&#123;</span><br><span class="line">        Aggregate&lt;ProductAggregate&gt; aggregate = repository.load(command.getProductId());</span><br><span class="line">        aggregate.execute(aggregateRoot-&gt;aggregateRoot.cancellReserve(command.getOrderId(), command.getNumber()));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>修改ProductAggregate，增加对应的方法和handler<br><code>ProductAggregate</code><br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Aggregate</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ProductAggregate</span> </span>&#123;</span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">reserve</span><span class="params">(OrderId orderId, <span class="keyword">int</span> amount)</span></span>&#123;</span><br><span class="line">    <span class="keyword">if</span>(stock&gt;=amount) &#123;</span><br><span class="line">        apply(<span class="keyword">new</span> ProductReservedEvent(orderId, id, amount));</span><br><span class="line"></span><br><span class="line">    &#125;<span class="keyword">else</span></span><br><span class="line">        apply(<span class="keyword">new</span> ProductNotEnoughEvent(orderId, id));</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">cancellReserve</span><span class="params">(OrderId orderId, <span class="keyword">int</span> amount)</span></span>&#123;</span><br><span class="line">      apply(<span class="keyword">new</span> ReserveCancelledEvent(orderId, id, stock));</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@EventHandler</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">on</span><span class="params">(ProductReservedEvent event)</span></span>&#123;</span><br><span class="line">      <span class="keyword">int</span> oriStock = stock;</span><br><span class="line">      stock = stock - event.getAmount();</span><br><span class="line">      LOGGER.info(<span class="string">"Product &#123;&#125; stock change &#123;&#125; -&gt; &#123;&#125;"</span>, id, oriStock, stock);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@EventHandler</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">on</span><span class="params">(ReserveCancelledEvent event)</span></span>&#123;</span><br><span class="line">      stock +=event.getAmount();</span><br><span class="line">      LOGGER.info(<span class="string">"Reservation rollback, product &#123;&#125; stock changed to &#123;&#125;"</span>, id, stock);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>Order这边对应也要修改Aggregate和handler<br><code>OrderHandler</code><br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderHandler</span> </span>&#123;</span><br><span class="line">  <span class="meta">@CommandHandler</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(RollbackOrderCommand command)</span></span>&#123;</span><br><span class="line">      Aggregate&lt;OrderAggregate&gt; aggregate = repository.load(command.getOrderId().getIdentifier());</span><br><span class="line">      aggregate.execute(aggregateRoot-&gt;aggregateRoot.delete());</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@CommandHandler</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(ConfirmOrderCommand command)</span></span>&#123;</span><br><span class="line">      Aggregate&lt;OrderAggregate&gt; aggregate = repository.load(command.getId().getIdentifier());</span><br><span class="line">      aggregate.execute(aggregateRoot-&gt;aggregateRoot.confirm());</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p><code>OrderAggregate</code><br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Aggregate</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderAggregate</span> </span>&#123;</span><br><span class="line">  <span class="keyword">private</span> String state=<span class="string">"processing"</span>; <span class="comment">// 增加一个属性订单状态</span></span><br><span class="line">  ......</span><br><span class="line"></span><br><span class="line">  <span class="meta">@EventHandler</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">on</span><span class="params">(OrderConfirmedEvent event)</span></span>&#123;</span><br><span class="line">      <span class="keyword">this</span>.state = <span class="string">"confirmed"</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@EventHandler</span></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">on</span><span class="params">(OrderCancelledEvent event)</span></span>&#123;</span><br><span class="line">      <span class="keyword">this</span>.state = <span class="string">"deleted"</span>;</span><br><span class="line">      markDeleted();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="启动测试"><a href="#启动测试" class="headerlink" title="启动测试"></a>启动测试</h2><p>其他地方基本没有什么改动，为方便起见，我把Query端也改成MongoDB了，方法比较简单，就引入<code>spring-boot-starter-data-mongodb</code>包，启动类里将<code>@EnableJpaRepositories</code>改成<code>@EnableMongoRepositories</code>，然后把Queyr端的Entry类包含在Scan的范围内就好了。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="meta">@ComponentScan</span>(basePackages = &#123;<span class="string">"com.edi.learn"</span>&#125;)</span><br><span class="line"><span class="meta">@EntityScan</span>(basePackages = &#123;<span class="string">"com.edi.learn"</span>,</span><br><span class="line">        <span class="string">"org.axonframework.eventsourcing.eventstore.jpa"</span>,</span><br><span class="line">        <span class="string">"org.axonframework.eventhandling.saga.repository.jpa"</span>,</span><br><span class="line">        <span class="string">"org.axonframework.eventhandling.tokenstore.jpa"</span>&#125;)</span><br><span class="line"><span class="meta">@EnableMongoRepositories</span>(basePackages = &#123;<span class="string">"com.edi.learn"</span>&#125;)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Application</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger LOGGER = getLogger(Application.class);</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String args[])</span></span>&#123;</span><br><span class="line">        SpringApplication.run(Application.class, args);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>执行后，</p><ol><li>POST请求到<a href="http://127.0.0.1:8080/product/1?name=ttt&amp;price=10&amp;stock=100" target="_blank" rel="noopener">http://127.0.0.1:8080/product/1?name=ttt&amp;price=10&amp;stock=100</a> 创建商品；</li><li><p>POST如下JSON到<a href="http://127.0.0.1:8080/order" target="_blank" rel="noopener">http://127.0.0.1:8080/order</a> 来创建订单</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line"><span class="attr">"username"</span>:<span class="string">"Edison"</span>,</span><br><span class="line"><span class="attr">"products"</span>:[&#123;</span><br><span class="line"><span class="attr">"id"</span>:<span class="number">1</span>,</span><br><span class="line"><span class="attr">"number"</span>:<span class="number">90</span></span><br><span class="line">&#125;]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>再创建一次<br>可以看到控制台打印</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">09:39:10.648 [http-nio-8080-exec-1] DEBUG c.e.l.a.c.w.c.ProductController - Adding Product [1] &apos;ttt&apos; 10x100</span><br><span class="line">09:39:10.675 [http-nio-8080-exec-1] DEBUG c.e.l.a.c.a.ProductAggregate - Product [1] ttt 1000x100 is created.</span><br><span class="line">09:39:10.853 [http-nio-8080-exec-1] DEBUG c.e.l.a.q.h.ProductEventHandler - repository data is updated</span><br><span class="line">09:39:21.640 [http-nio-8080-exec-3] DEBUG c.e.l.a.c.a.ProductAggregate - Product [1] ttt 1000x100 is created.</span><br><span class="line">09:39:21.681 [http-nio-8080-exec-3] INFO  c.e.l.a.c.a.ProductAggregate - Product 1 stock change 100 -&gt; 10</span><br><span class="line">09:39:21.823 [http-nio-8080-exec-3] INFO  c.e.l.axon.command.saga.OrderSaga - Order 8706dbaf-4511-4b01-b6c5-e24bec3f10a9 is confirmed</span><br><span class="line">09:42:35.255 [http-nio-8080-exec-5] DEBUG c.e.l.a.c.handlers.OrderHandler - Loading product information with productId: 1</span><br><span class="line">09:42:35.259 [http-nio-8080-exec-5] DEBUG c.e.l.a.c.a.ProductAggregate - Product [1] ttt 1000x100 is created.</span><br><span class="line">09:42:35.263 [http-nio-8080-exec-5] INFO  c.e.l.a.c.a.ProductAggregate - Product 1 stock change 100 -&gt; 10</span><br><span class="line">09:42:35.301 [http-nio-8080-exec-5] INFO  c.e.l.axon.command.saga.OrderSaga - No enough item to buy</span><br><span class="line">09:42:35.313 [http-nio-8080-exec-5] INFO  c.e.l.axon.command.saga.OrderSaga - Order 6baba5e9-1173-48a8-ab98-cd51691ba9f5 is cancelled</span><br></pre></td></tr></table></figure></li><li><p>重启程序，再创建一次订单后发送GET请求到<a href="http://127.0.0.1:8080/orders" target="_blank" rel="noopener">http://127.0.0.1:8080/orders</a> 查询订单</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"_embedded"</span>: &#123;</span><br><span class="line">    <span class="attr">"orders"</span>: [</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">"username"</span>: <span class="string">"Edison"</span>,</span><br><span class="line">        <span class="attr">"payment"</span>: <span class="number">0</span>,</span><br><span class="line">        <span class="attr">"status"</span>: <span class="string">"confirmed"</span>,</span><br><span class="line">        <span class="attr">"products"</span>: &#123;</span><br><span class="line">          <span class="attr">"1"</span>: &#123;</span><br><span class="line">            <span class="attr">"name"</span>: <span class="string">"ttt"</span>,</span><br><span class="line">            <span class="attr">"price"</span>: <span class="number">1000</span>,</span><br><span class="line">            <span class="attr">"amount"</span>: <span class="number">90</span></span><br><span class="line">          &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">"_links"</span>: &#123;</span><br><span class="line">          <span class="attr">"self"</span>: &#123;</span><br><span class="line">            <span class="attr">"href"</span>: <span class="string">"http://localhost:8080/orders/8706dbaf-4511-4b01-b6c5-e24bec3f10a9"</span></span><br><span class="line">          &#125;,</span><br><span class="line">          <span class="attr">"orderEntry"</span>: &#123;</span><br><span class="line">            <span class="attr">"href"</span>: <span class="string">"http://localhost:8080/orders/8706dbaf-4511-4b01-b6c5-e24bec3f10a9"</span></span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">"username"</span>: <span class="string">"Edison"</span>,</span><br><span class="line">        <span class="attr">"payment"</span>: <span class="number">0</span>,</span><br><span class="line">        <span class="attr">"status"</span>: <span class="string">"cancelled"</span>,</span><br><span class="line">        <span class="attr">"products"</span>: &#123;</span><br><span class="line">          <span class="attr">"1"</span>: &#123;</span><br><span class="line">            <span class="attr">"name"</span>: <span class="string">"ttt"</span>,</span><br><span class="line">            <span class="attr">"price"</span>: <span class="number">1000</span>,</span><br><span class="line">            <span class="attr">"amount"</span>: <span class="number">90</span></span><br><span class="line">          &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">"_links"</span>: &#123;</span><br><span class="line">          <span class="attr">"self"</span>: &#123;</span><br><span class="line">            <span class="attr">"href"</span>: <span class="string">"http://localhost:8080/orders/6baba5e9-1173-48a8-ab98-cd51691ba9f5"</span></span><br><span class="line">          &#125;,</span><br><span class="line">          <span class="attr">"orderEntry"</span>: &#123;</span><br><span class="line">            <span class="attr">"href"</span>: <span class="string">"http://localhost:8080/orders/6baba5e9-1173-48a8-ab98-cd51691ba9f5"</span></span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">"username"</span>: <span class="string">"Edison"</span>,</span><br><span class="line">        <span class="attr">"payment"</span>: <span class="number">0</span>,</span><br><span class="line">        <span class="attr">"status"</span>: <span class="string">"cancelled"</span>,</span><br><span class="line">        <span class="attr">"products"</span>: &#123;</span><br><span class="line">          <span class="attr">"1"</span>: &#123;</span><br><span class="line">            <span class="attr">"name"</span>: <span class="string">"ttt"</span>,</span><br><span class="line">            <span class="attr">"price"</span>: <span class="number">1000</span>,</span><br><span class="line">            <span class="attr">"amount"</span>: <span class="number">90</span></span><br><span class="line">          &#125;</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="attr">"_links"</span>: &#123;</span><br><span class="line">          <span class="attr">"self"</span>: &#123;</span><br><span class="line">            <span class="attr">"href"</span>: <span class="string">"http://localhost:8080/orders/27a829af-cda1-43f4-af37-fbc597fe5f6f"</span></span><br><span class="line">          &#125;,</span><br><span class="line">          <span class="attr">"orderEntry"</span>: &#123;</span><br><span class="line">            <span class="attr">"href"</span>: <span class="string">"http://localhost:8080/orders/27a829af-cda1-43f4-af37-fbc597fe5f6f"</span></span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    ]</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"_links"</span>: &#123;</span><br><span class="line">    <span class="attr">"self"</span>: &#123;</span><br><span class="line">      <span class="attr">"href"</span>: <span class="string">"http://localhost:8080/orders"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">"profile"</span>: &#123;</span><br><span class="line">      <span class="attr">"href"</span>: <span class="string">"http://localhost:8080/profile/orders"</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"page"</span>: &#123;</span><br><span class="line">    <span class="attr">"size"</span>: <span class="number">20</span>,</span><br><span class="line">    <span class="attr">"totalElements"</span>: <span class="number">3</span>,</span><br><span class="line">    <span class="attr">"totalPages"</span>: <span class="number">1</span>,</span><br><span class="line">    <span class="attr">"number"</span>: <span class="number">0</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ol><p>很明显看到只有第一个订单状态为’confirmed’，其他两个都是’cancelled’。重启后，Aggregate自动回溯后，对库存的判断也是正确的。</p><ol start="5"><li>再做个小实验，我们修改<code>OrderSaga</code>，强制在确认订单时让线程sleep一段时间，然后去MongoDB里查看Saga信息<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SagaEventHandler</span>(associationProperty = <span class="string">"id"</span>, keyName = <span class="string">"orderId"</span>)</span><br><span class="line"><span class="meta">@EndSaga</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(OrderConfirmedEvent event)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">    LOGGER.info(<span class="string">"Order &#123;&#125; is confirmed"</span>, event.getId());</span><br><span class="line">    Thread.sleep(<span class="number">10000</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&gt; db.sagas.find().pretty()</span><br><span class="line">&#123;</span><br><span class="line">        &quot;_id&quot; : ObjectId(&quot;58df074d73bc0c10f4008eff&quot;),</span><br><span class="line">        &quot;sagaType&quot; : &quot;com.edi.learn.axon.command.saga.OrderSaga&quot;,</span><br><span class="line">        &quot;sagaIdentifier&quot; : &quot;08a371f5-9d9a-48a7-b46e-9b8e86b8897b&quot;,</span><br><span class="line">        &quot;serializedSaga&quot; : BinData(0,&quot;e30=&quot;),</span><br><span class="line">        &quot;associations&quot; : [</span><br><span class="line">                &#123;</span><br><span class="line">                        &quot;key&quot; : &quot;orderId&quot;,</span><br><span class="line">                        &quot;value&quot; : &quot;5111a55e-1ddd-4434-aab8-635c004fc1eb&quot;</span><br><span class="line">                &#125;</span><br><span class="line">        ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>看到我们的关联值了吧。</p><p>本文代码：<a href="https://github.com/EdisonXu/sbs-axon/tree/master/lesson-5" target="_blank" rel="noopener">https://github.com/EdisonXu/sbs-axon/tree/master/lesson-5</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;在上一篇里面，我们正式的使用了CQRS模式完成了AXON的第一个真正的例子，但是细心的朋友会发现一个问题，创建订单时并没有检查商品库存。&lt;br&gt;库存是否足够直接回导致订单状态的成功与否，在并发时可能还会出现超卖。当库存不足时还需要回滚订单，所以这里
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="CQRS" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/CQRS/"/>
    
      <category term="Axon" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/CQRS/Axon/"/>
    
    
      <category term="event sourcing" scheme="http://edisonxu.com/tags/event-sourcing/"/>
    
      <category term="CQRS" scheme="http://edisonxu.com/tags/CQRS/"/>
    
      <category term="axon" scheme="http://edisonxu.com/tags/axon/"/>
    
      <category term="DDD" scheme="http://edisonxu.com/tags/DDD/"/>
    
  </entry>
  
  <entry>
    <title>Axon入门系列(五)：第一个正式Axon例子</title>
    <link href="http://edisonxu.com/2017/03/30/axon-cqrs-example.html"/>
    <id>http://edisonxu.com/2017/03/30/axon-cqrs-example.html</id>
    <published>2017-03-30T11:45:16.000Z</published>
    <updated>2021-07-21T13:31:21.781Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>前面对Axon的基本概念和基本操作做了简介，从本章开始，我们将一步步使用AxonFramework完成一个真正CQRS&amp;EventSourcing的例子。</p></blockquote><h2 id="设计"><a href="#设计" class="headerlink" title="设计"></a>设计</h2><p>回顾一下使用AxonFramework应用的架构<br><img src="/images/2017/03/detailed-architecture-overview.png" alt=""></p><p>Command端Repository和Query端的Database是解耦的，完全可以使用不同的持久化技术，我们来尝试用MongoDB做Command端的Repository，而MySQL做Query的数据库。</p><h3 id="例子描述"><a href="#例子描述" class="headerlink" title="例子描述"></a>例子描述</h3><p>我们尝试完成一个简单的case：后台人员创建商品，用户选定若干商品后下单购买。<br>商品定义：Product(id, name, stock, price)<br>商品创建流程：<br><code>CreateProductCommand</code> -&gt; new <code>ProductAggregate</code> instance -&gt; <code>ProductCreatedEvent</code></p><p>订单定义： Order(id, username, payment, products)<br>订单创建流程：<br><code>CreateOrderCommand</code> -&gt; new <code>OrderAggregate</code> instance -&gt; <code>OrderCreatedEvent</code><br>创建商品时，我们只接收商品ID，去查询商品的具体信息，这样来学习如何在handler内去查询Aggregate。</p><h2 id="Command端实现"><a href="#Command端实现" class="headerlink" title="Command端实现"></a>Command端实现</h2><p>Command端实现与前面几篇文章基本一致，需要定义Aggregate、Command，然后提供配置即可。</p><h3 id="Aggregate"><a href="#Aggregate" class="headerlink" title="Aggregate"></a>Aggregate</h3><p><code>ProductAggregate</code><br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Aggregate</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ProductAggregate</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger LOGGER = getLogger(ProductAggregate.class);</span><br><span class="line"></span><br><span class="line">    <span class="meta">@AggregateIdentifier</span></span><br><span class="line">    <span class="keyword">private</span> String id;</span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> stock;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">long</span> price;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">ProductAggregate</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@CommandHandler</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">ProductAggregate</span><span class="params">(CreateProductCommand command)</span> </span>&#123;</span><br><span class="line">        apply(<span class="keyword">new</span> ProductCreatedEvent(command.getId(),command.getName(),command.getPrice(),command.getStock()));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@EventHandler</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">on</span><span class="params">(ProductCreatedEvent event)</span></span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.id = event.getId();</span><br><span class="line">        <span class="keyword">this</span>.name = event.getName();</span><br><span class="line">        <span class="keyword">this</span>.price = event.getPrice();</span><br><span class="line">        <span class="keyword">this</span>.stock = event.getStock();</span><br><span class="line">        LOGGER.debug(<span class="string">"Product [&#123;&#125;] &#123;&#125; &#123;&#125;x&#123;&#125; is created."</span>, id,name,price,stock);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// getter and setter</span></span><br><span class="line">    ......</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p><code>OrderAggregate</code><br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Aggregate</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderAggregate</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@AggregateIdentifier</span></span><br><span class="line">    <span class="keyword">private</span> OrderId id;</span><br><span class="line">    <span class="keyword">private</span> String username;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">double</span> payment;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@AggregateMember</span></span><br><span class="line">    <span class="keyword">private</span> Map&lt;String, OrderProduct&gt; products;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">OrderAggregate</span><span class="params">()</span></span>&#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">OrderAggregate</span><span class="params">(OrderId id, String username, Map&lt;String, OrderProduct&gt; products)</span> </span>&#123;</span><br><span class="line">        apply(<span class="keyword">new</span> OrderCreatedEvent(id, username, products));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> OrderId <span class="title">getId</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> id;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">getUsername</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> username;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Map&lt;String, OrderProduct&gt; <span class="title">getProducts</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> products;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@EventHandler</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">on</span><span class="params">(OrderCreatedEvent event)</span></span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.id = event.getOrderId();</span><br><span class="line">        <span class="keyword">this</span>.username = event.getUsername();</span><br><span class="line">        <span class="keyword">this</span>.products = event.getProducts();</span><br><span class="line">        computePrice();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">computePrice</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        products.forEach((id, product) -&gt; &#123;</span><br><span class="line">            payment += product.getPrice() * product.getAmount();</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Divided 100 here because of the transformation of accuracy</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">double</span> <span class="title">getPayment</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> payment/<span class="number">100</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addProduct</span><span class="params">(OrderProduct product)</span></span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.products.put(product.getId(), product);</span><br><span class="line">        payment += product.getPrice() * product.getAmount();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">removeProduct</span><span class="params">(String productId)</span></span>&#123;</span><br><span class="line">        OrderProduct product = <span class="keyword">this</span>.products.remove(productId);</span><br><span class="line">        payment = payment - product.getPrice() * product.getAmount();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>这里，我并没有像ProductAggregate一样，把CreateOrderCommand放到OrderAggregate的构造器中去处理，原因是在创建订单时，由于需要知道商品的单价，所以要根据商品id查询商品信息，因为涉及到了其他Aggregate操作，特地单独创建一个OrderHandler来处理。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderHandler</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger LOGGER = getLogger(OrderHandler.class);</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> Repository&lt;OrderAggregate&gt; repository;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> Repository&lt;ProductAggregate&gt; productRepository;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> EventBus eventBus;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@CommandHandler</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(CreateOrderCommand command)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        Map&lt;String, OrderProduct&gt; products = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line">        command.getProducts().forEach((productId,number)-&gt;&#123;</span><br><span class="line">            LOGGER.debug(<span class="string">"Loading product information with productId: &#123;&#125;"</span>,productId);</span><br><span class="line">            Aggregate&lt;ProductAggregate&gt; aggregate = productRepository.load(productId);</span><br><span class="line">            products.put(productId,</span><br><span class="line">                    <span class="keyword">new</span> OrderProduct(productId,</span><br><span class="line">                            aggregate.invoke(productAggregate -&gt; productAggregate.getName()),</span><br><span class="line">                            aggregate.invoke(productAggregate -&gt; productAggregate.getPrice()),</span><br><span class="line">                            number));</span><br><span class="line">        &#125;);</span><br><span class="line">        repository.newInstance(() -&gt; <span class="keyword">new</span> OrderAggregate(command.getOrderId(), command.getUsername(), products));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>如果查看<code>org.axonframework.commandhandling.model.Repository&lt;T&gt;</code>接口的定义，会发现里面只有三个方法：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">Repository</span>&lt;<span class="title">T</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Load the aggregate with the given unique identifier. No version checks are done when loading an aggregate,</span></span><br><span class="line"><span class="comment">     * meaning that concurrent access will not be checked for.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> aggregateIdentifier The identifier of the aggregate to load</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> The aggregate root with the given identifier.</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> AggregateNotFoundException if aggregate with given id cannot be found</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">Aggregate&lt;T&gt; <span class="title">load</span><span class="params">(String aggregateIdentifier)</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Load the aggregate with the given unique identifier.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> aggregateIdentifier The identifier of the aggregate to load</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> expectedVersion     The expected version of the loaded aggregate</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> The aggregate root with the given identifier.</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> AggregateNotFoundException if aggregate with given id cannot be found</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">Aggregate&lt;T&gt; <span class="title">load</span><span class="params">(String aggregateIdentifier, Long expectedVersion)</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Creates a new managed instance for the aggregate, using the given &#123;<span class="doctag">@code</span> factoryMethod&#125;</span></span><br><span class="line"><span class="comment">     * to instantiate the aggregate's root.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> factoryMethod The method to create the aggregate's root instance</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> an Aggregate instance describing the aggregate's state</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> Exception when the factoryMethod throws an exception</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function">Aggregate&lt;T&gt; <span class="title">newInstance</span><span class="params">(Callable&lt;T&gt; factoryMethod)</span> <span class="keyword">throws</span> Exception</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>有人会疑惑了，为什么没有Delete和Update？<br>先说update，这个Repository其实是对Aggregate的操作，EventSourcing中对Aggregate所有的变化都是通过Event来实现的，所以在调用apply(EventMessage)时，Event就已经被持久化了，<code>EventHandler</code>在处理该Event时，就已经实现了对Aggregate的update。<br>而Delete没有，很简单，EventSourcing脱胎于现实概念，你见过现实生活中把一个事物真正“delete”掉吗？估计得使用高能量子炮把东西轰成原子吧。<br>所以，只会有一个把这个Aggregate标为失效的标志，Axon中，在Aggregate内部可以直接调用markDeleted()来表示这个Aggregate被“delete”掉了，其实只是不能被load出来罢了。<br>由于Repository默认返回的是同一类型Aggregate<t>，所以我们取属性就没那么简单了，只能通过invoke来调用get方法。是不是觉得很麻烦？因为其实CQRS压根不推荐直接从Repository直接query Aggregate来查询，而是调用Query端。</t></p><h3 id="Command"><a href="#Command" class="headerlink" title="Command"></a>Command</h3><p>command的实现因为都是POJO我就不贴代码了，可以直接看源码。<br>这里写一下基于SpringWeb的Controller类（引入<code>spring-boot-starter-web</code>包），以创建Product为例<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping</span>(<span class="string">"/product"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ProductController</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger LOGGER = getLogger(ProductController.class);</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> CommandGateway commandGateway;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@RequestMapping</span>(value = <span class="string">"/&#123;id&#125;"</span>, method = RequestMethod.POST)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">create</span><span class="params">(@PathVariable(value = <span class="string">"id"</span>)</span> String id,</span></span><br><span class="line"><span class="function">                       @<span class="title">RequestParam</span><span class="params">(value = <span class="string">"name"</span>, required = <span class="keyword">true</span>)</span> String name,</span></span><br><span class="line"><span class="function">                       @<span class="title">RequestParam</span><span class="params">(value = <span class="string">"price"</span>, required = <span class="keyword">true</span>)</span> <span class="keyword">long</span> price,</span></span><br><span class="line"><span class="function">                       @<span class="title">RequestParam</span><span class="params">(value = <span class="string">"stock"</span>,required = <span class="keyword">true</span>)</span> <span class="keyword">int</span> stock,</span></span><br><span class="line"><span class="function">                       HttpServletResponse response) </span>&#123;</span><br><span class="line"></span><br><span class="line">        LOGGER.debug(<span class="string">"Adding Product [&#123;&#125;] '&#123;&#125;' &#123;&#125;x&#123;&#125;"</span>, id, name, price, stock);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// multiply 100 on the price to avoid float number</span></span><br><span class="line">            CreateProductCommand command = <span class="keyword">new</span> CreateProductCommand(id,name,price*<span class="number">100</span>,stock);</span><br><span class="line">            commandGateway.sendAndWait(command);</span><br><span class="line">            response.setStatus(HttpServletResponse.SC_CREATED);<span class="comment">// Set up the 201 CREATED response</span></span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (CommandExecutionException cex) &#123;</span><br><span class="line">            LOGGER.warn(<span class="string">"Add Command FAILED with Message: &#123;&#125;"</span>, cex.getMessage());</span><br><span class="line">            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);</span><br><span class="line">            <span class="keyword">if</span> (<span class="keyword">null</span> != cex.getCause()) &#123;</span><br><span class="line">                LOGGER.warn(<span class="string">"Caused by: &#123;&#125; &#123;&#125;"</span>, cex.getCause().getClass().getName(), cex.getCause().getMessage());</span><br><span class="line">                <span class="keyword">if</span> (cex.getCause() <span class="keyword">instanceof</span> ConcurrencyException) &#123;</span><br><span class="line">                    LOGGER.warn(<span class="string">"A duplicate product with the same ID [&#123;&#125;] already exists."</span>, id);</span><br><span class="line">                    response.setStatus(HttpServletResponse.SC_CONFLICT);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p><code>CommandGateway</code>提供了四种发送Comman的方法：</p><ul><li>send(command, CommandCallback)  发送command，根据执行结果调用<code>CommandCallback</code>中的<code>onSuccess</code>或<code>onFailure</code>方法</li><li>sendAndWait(command) 发送完command，等待执行完成并返回结果</li><li>sendAndWait(command, timeout, TimeUnit) 这个好理解，比上面多了一个超时</li><li>send(command) 该方法返回一个<code>CompletableFuture</code>，不用等待command的执行，立刻返回。结果通过future获取。</li></ul><h3 id="Repository"><a href="#Repository" class="headerlink" title="Repository"></a>Repository</h3><p>由于我们要使用<code>axon-mongo</code>，而非默认的jpa，所以必须得手动指定两个Aggregate的Repository，以其中一个为例：<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ProductConfig</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> EventStore eventStore;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="meta">@Scope</span>(<span class="string">"prototype"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> ProductAggregate <span class="title">productAggregate</span><span class="params">()</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> ProductAggregate();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> AggregateFactory&lt;ProductAggregate&gt; <span class="title">productAggregateAggregateFactory</span><span class="params">()</span></span>&#123;</span><br><span class="line">        SpringPrototypeAggregateFactory&lt;ProductAggregate&gt; aggregateFactory = <span class="keyword">new</span> SpringPrototypeAggregateFactory&lt;&gt;();</span><br><span class="line">        aggregateFactory.setPrototypeBeanName(<span class="string">"productAggregate"</span>);</span><br><span class="line">        <span class="keyword">return</span> aggregateFactory;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Repository&lt;ProductAggregate&gt; <span class="title">productAggregateRepository</span><span class="params">()</span></span>&#123;</span><br><span class="line">        EventSourcingRepository&lt;ProductAggregate&gt; repository = <span class="keyword">new</span> EventSourcingRepository&lt;ProductAggregate&gt;(</span><br><span class="line">                productAggregateAggregateFactory(),</span><br><span class="line">                eventStore</span><br><span class="line">        );</span><br><span class="line">        <span class="keyword">return</span> repository;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>使用EventSourcingRepository，必须指定一个AggregateFactory用来反射生成Aggregate的，所以我们这里定义了Aggregate的prototype，并把它注册到AggregateFactory中去。<br>这样在系统启动时，读取历史Event进行ES还原时，就可以真实再现Aggregate的状态。</p><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><p>前面使用MySQL作为EventStorage是不是感到不爽，那么我们通过引入<code>axon-mongo</code>依赖，使用MongoDB来做EventStorage。<br>pom的修改我就不写了，着重看下相关配置<br>先是修改application.property<br><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># mongo</span></span><br><span class="line"><span class="string">mongodb.url=10.1.110.24</span></span><br><span class="line"><span class="string">mongodb.port=27017</span></span><br><span class="line"><span class="comment"># mongodb.username=</span></span><br><span class="line"><span class="comment"># mongodb.password=</span></span><br><span class="line"><span class="string">mongodb.dbname=axon</span></span><br><span class="line"><span class="string">mongodb.events.collection.name=events</span></span><br><span class="line"><span class="string">mongodb.events.snapshot.collection.name=snapshots</span></span><br></pre></td></tr></table></figure></p><p>通过Spring提供的@Value注解在具体的Configuration类里读取。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CommandRepositoryConfiguration</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value</span>(<span class="string">"$&#123;mongodb.url&#125;"</span>)</span><br><span class="line">    <span class="keyword">private</span> String mongoUrl;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value</span>(<span class="string">"$&#123;mongodb.dbname&#125;"</span>)</span><br><span class="line">    <span class="keyword">private</span> String mongoDbName;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value</span>(<span class="string">"$&#123;mongodb.events.collection.name&#125;"</span>)</span><br><span class="line">    <span class="keyword">private</span> String eventsCollectionName;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value</span>(<span class="string">"$&#123;mongodb.events.snapshot.collection.name&#125;"</span>)</span><br><span class="line">    <span class="keyword">private</span> String snapshotCollectionName;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Serializer <span class="title">axonJsonSerializer</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> JacksonSerializer();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> EventStorageEngine <span class="title">eventStorageEngine</span><span class="params">()</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> MongoEventStorageEngine(</span><br><span class="line">                axonJsonSerializer(),<span class="keyword">null</span>, axonMongoTemplate(), <span class="keyword">new</span> DocumentPerEventStorageStrategy());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span>(name = <span class="string">"axonMongoTemplate"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> MongoTemplate <span class="title">axonMongoTemplate</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        MongoTemplate template = <span class="keyword">new</span> DefaultMongoTemplate(mongoClient(), mongoDbName, eventsCollectionName, snapshotCollectionName);</span><br><span class="line">        <span class="keyword">return</span> template;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> MongoClient <span class="title">mongoClient</span><span class="params">()</span></span>&#123;</span><br><span class="line">        MongoFactory mongoFactory = <span class="keyword">new</span> MongoFactory();</span><br><span class="line">        mongoFactory.setMongoAddresses(Arrays.asList(<span class="keyword">new</span> ServerAddress(mongoUrl)));</span><br><span class="line">        <span class="keyword">return</span> mongoFactory.createMongo();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>用Jacson做序列化器，MongoClient提供了具体连接实现，MongoTemplate指定了db名称、存放event的collection名称、存放snapshot的collection名称。（snapshot的概念以后再解释）<br>中间一个参数是做不同版本Event间兼容的，我们先留null。<br><code>EventStorageEngine</code>指定<code>MongoEventStorageEngine</code>，<code>spring-boot-autoconfigure</code>中的<code>AxonAutoConfiguration</code>就会帮你把它注入到Axon的配置器中。<br>这里指的注意的是，<strong>使用Jackson做序列化器时，对应的entity的所有需要持久化的field必须都有public getter方法</strong>，因为Jackson在反射时默认只读public修饰符的field，否则就会报<br>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”])<br>错误。如果确实不想写，那么在Entity的class声明前加上<code>@JsonAutoDetect(fieldVisibility=JsonAutoDetect.Visibility.ANY)</code><br>到此，Command端的实现已基本完成（Event我没写，因为与前文类似），那么我们来看看Query端。</p><h2 id="Query端实现"><a href="#Query端实现" class="headerlink" title="Query端实现"></a>Query端实现</h2><p>AxonFramework的Query端其实并没有特别的，我们只需要实现一些<code>EventHandler</code>来处理Command端产生的事件，来更新Query端的数据库就行了。<br>这里我就使用JPA的MySQL实现，spring提供了<code>spring-boot-starter-data-rest</code>，为JPA Repository增加了HateOas风格的REST接口，非常简单，非常方便，堪称无脑。<br>先定义三个Entity<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ProductEntry</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="meta">@Id</span></span><br><span class="line">  <span class="keyword">private</span> String id;</span><br><span class="line">  <span class="meta">@Column</span></span><br><span class="line">  <span class="keyword">private</span> String name;</span><br><span class="line">  <span class="meta">@Column</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">long</span> price;</span><br><span class="line">  <span class="meta">@Column</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">int</span> stock;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="title">ProductEntry</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="title">ProductEntry</span><span class="params">(String id, String name, <span class="keyword">long</span> price, <span class="keyword">int</span> stock)</span> </span>&#123;</span><br><span class="line">      <span class="keyword">this</span>.id = id;</span><br><span class="line">      <span class="keyword">this</span>.name = name;</span><br><span class="line">      <span class="keyword">this</span>.price = price;</span><br><span class="line">      <span class="keyword">this</span>.stock = stock;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// getter &amp; setter</span></span><br><span class="line">  ......</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderEntry</span> </span>&#123;</span><br><span class="line">  <span class="meta">@Id</span></span><br><span class="line">  <span class="keyword">private</span> String id;</span><br><span class="line">  <span class="meta">@Column</span></span><br><span class="line">  <span class="keyword">private</span> String username;</span><br><span class="line">  <span class="meta">@Column</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">double</span> payment;</span><br><span class="line">  <span class="meta">@OneToMany</span>(fetch = FetchType.EAGER, cascade = CascadeType.ALL)</span><br><span class="line">  <span class="meta">@JoinColumn</span>(name = <span class="string">"order_id"</span>)</span><br><span class="line">  <span class="meta">@MapKey</span>(name = <span class="string">"id"</span>)</span><br><span class="line">  <span class="keyword">private</span> Map&lt;String, OrderProductEntry&gt; products;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="title">OrderEntry</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="title">OrderEntry</span><span class="params">(String id, String username, Map&lt;String, OrderProductEntry&gt; products)</span> </span>&#123;</span><br><span class="line">      <span class="keyword">this</span>.id = id;</span><br><span class="line">      <span class="keyword">this</span>.username = username;</span><br><span class="line">      <span class="keyword">this</span>.payment = payment;</span><br><span class="line">      <span class="keyword">this</span>.products = products;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// getter &amp; setter</span></span><br><span class="line">  ......</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderProductEntry</span> </span>&#123;</span><br><span class="line">  <span class="meta">@Id</span></span><br><span class="line">  <span class="meta">@GeneratedValue</span></span><br><span class="line">  <span class="keyword">private</span> Long jpaId;</span><br><span class="line">  <span class="keyword">private</span> String id;</span><br><span class="line">  <span class="meta">@Column</span></span><br><span class="line">  <span class="keyword">private</span> String name;</span><br><span class="line">  <span class="meta">@Column</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">long</span> price;</span><br><span class="line">  <span class="meta">@Column</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">int</span> amount;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="title">OrderProductEntry</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="title">OrderProductEntry</span><span class="params">(String id, String name, <span class="keyword">long</span> price, <span class="keyword">int</span> amount)</span> </span>&#123;</span><br><span class="line">      <span class="keyword">this</span>.id = id;</span><br><span class="line">      <span class="keyword">this</span>.name = name;</span><br><span class="line">      <span class="keyword">this</span>.price = price;</span><br><span class="line">      <span class="keyword">this</span>.amount = amount;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// getter &amp; setter</span></span><br><span class="line">  ......</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>比较简单，唯一需要注意的是ProductEntry和OrderEntry之间的一对多关系。<br>然后为它们创建两个Repository<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">@RepositoryRestResource(collectionResourceRel = &quot;orders&quot;, path = &quot;orders&quot;)</span><br><span class="line">public interface OrderEntryRepository extends PagingAndSortingRepository&lt;OrderEntry, String&gt; &#123;&#125;</span><br><span class="line">@RepositoryRestResource(collectionResourceRel = &quot;products&quot;, path = &quot;products&quot;)</span><br><span class="line">public interface ProductEntryRepository extends PagingAndSortingRepository&lt;ProductEntry, String&gt; &#123;&#125;</span><br></pre></td></tr></table></figure></p><p>是不是很简单？最后定义handler，为省篇幅，我只写一个<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderEventHandler</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger LOGGER = getLogger(OrderEventHandler.class);</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> OrderEntryRepository repository;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@EventHandler</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">on</span><span class="params">(OrderCreatedEvent event)</span></span>&#123;</span><br><span class="line">        Map&lt;String, OrderProductEntry&gt; map = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line">        event.getProducts().forEach((id, product)-&gt;&#123;</span><br><span class="line">            map.put(id,</span><br><span class="line">                    <span class="keyword">new</span> OrderProductEntry(</span><br><span class="line">                            product.getId(),</span><br><span class="line">                            product.getName(),</span><br><span class="line">                            product.getPrice(),</span><br><span class="line">                            product.getAmount()));</span><br><span class="line">        &#125;);</span><br><span class="line">        OrderEntry order = <span class="keyword">new</span> OrderEntry(event.getOrderId().toString(), event.getUsername(), map);</span><br><span class="line">        repository.save(order);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="启动类"><a href="#启动类" class="headerlink" title="启动类"></a>启动类</h2><p>由于我们使用了axon提供的<code>MongoEventStorageEngine</code>，其内部也使用了JPA，所以我们在启动类还需要把Axon帮我们转Entity的一些类也加到EntityScan中去<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="meta">@ComponentScan</span>(basePackages = &#123;<span class="string">"com.edi.learn"</span>&#125;)</span><br><span class="line"><span class="meta">@EntityScan</span>(basePackages = &#123;<span class="string">"com.edi.learn"</span>,</span><br><span class="line">        <span class="string">"org.axonframework.eventsourcing.eventstore.jpa"</span>,</span><br><span class="line">        <span class="string">"org.axonframework.eventhandling.saga.repository.jpa"</span>,</span><br><span class="line">        <span class="string">"org.axonframework.eventhandling.tokenstore.jpa"</span>&#125;)</span><br><span class="line"><span class="meta">@EnableJpaRepositories</span>(basePackages = &#123;<span class="string">"com.edi.learn.axon.query"</span>&#125;)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Application</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger LOGGER = getLogger(Application.class);</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String args[])</span></span>&#123;</span><br><span class="line">        SpringApplication.run(Application.class, args);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>启动后，用POST发送请求<a href="http://127.0.0.1:8080/product/1?name=ttt&amp;price=10&amp;stock=100" target="_blank" rel="noopener">http://127.0.0.1:8080/product/1?name=ttt&amp;price=10&amp;stock=100</a> ，查询mongoDB：<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">&gt; use axon</span><br><span class="line">&gt; show collections</span><br><span class="line">events</span><br><span class="line">snapshots</span><br><span class="line">system.indexes</span><br><span class="line">&gt; db.events.find().pretty()</span><br><span class="line">&#123;</span><br><span class="line">        &quot;_id&quot; : ObjectId(&quot;58dd181073bc0c0fb86d895e&quot;),</span><br><span class="line">        &quot;aggregateIdentifier&quot; : &quot;1&quot;,</span><br><span class="line">        &quot;type&quot; : &quot;ProductAggregate&quot;,</span><br><span class="line">        &quot;sequenceNumber&quot; : NumberLong(0),</span><br><span class="line">        &quot;serializedPayload&quot; : &quot;&#123;\&quot;id\&quot;:\&quot;1\&quot;,\&quot;name\&quot;:\&quot;ttt\&quot;,\&quot;price\&quot;:1000,\&quot;stock\&quot;:100&#125;&quot;,</span><br><span class="line">        &quot;timestamp&quot; : &quot;2017-03-30T14:37:04.075Z&quot;,</span><br><span class="line">        &quot;payloadType&quot; : &quot;com.edi.learn.axon.common.events.ProductCreatedEvent&quot;,</span><br><span class="line">        &quot;payloadRevision&quot; : null,</span><br><span class="line">        &quot;serializedMetaData&quot; : &quot;&#123;\&quot;traceId\&quot;:\&quot;4a298ed4-0d53-402a-ae6b-d79cc5e193bf\&quot;,\&quot;correlationId\&quot;:\&quot;4a298ed4-0d53-402a-ae6b-d79cc5e193bf\&quot;&#125;&quot;,</span><br><span class="line">        &quot;eventIdentifier&quot; : &quot;500f3a8f-7c02-4e8e-bb9c-7b676224ce5c&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>可以看到生成的EventMessage，与前篇文章中MySQL表里内容基本一致。<br>再去看下MySQL库的product_entry表，有记录</p><table><thead><tr><th>id</th><th>name</th><th>price</th><th>stock</th></tr></thead><tbody><tr><td>1</td><td>ttt</td><td>1000</td><td>100</td></tr></tbody></table><p>用GET请求<a href="http://localhost:8080/products" target="_blank" rel="noopener">http://localhost:8080/products</a> 会返回当前所有product信息，加上id <a href="http://localhost:8080/products/1" target="_blank" rel="noopener">http://localhost:8080/products/1</a> 就会返回刚才创建的product。</p><p>本篇对应代码：<a href="https://github.com/EdisonXu/sbs-axon/tree/master/lesson-4" target="_blank" rel="noopener">https://github.com/EdisonXu/sbs-axon/tree/master/lesson-4</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;前面对Axon的基本概念和基本操作做了简介，从本章开始，我们将一步步使用AxonFramework完成一个真正CQRS&amp;amp;EventSourcing的例子。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;设计&quot;&gt;&lt;a href=&quot;#设计
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="CQRS" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/CQRS/"/>
    
      <category term="Axon" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/CQRS/Axon/"/>
    
    
      <category term="event sourcing" scheme="http://edisonxu.com/tags/event-sourcing/"/>
    
      <category term="CQRS" scheme="http://edisonxu.com/tags/CQRS/"/>
    
      <category term="axon" scheme="http://edisonxu.com/tags/axon/"/>
    
      <category term="DDD" scheme="http://edisonxu.com/tags/DDD/"/>
    
  </entry>
  
  <entry>
    <title>Axon入门系列(四)：Axon使用EventSourcing和AutoConfigure</title>
    <link href="http://edisonxu.com/2017/03/30/axon-event-sourcing.html"/>
    <id>http://edisonxu.com/2017/03/30/axon-event-sourcing.html</id>
    <published>2017-03-30T09:52:23.000Z</published>
    <updated>2021-07-21T13:31:21.781Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>继上一篇集成SpringBoot后，本篇将继续完成小目标：</p><ol><li>使用EventSourcing</li><li>使用AutoConfigure配置Axon</li></ol></blockquote><p>前一篇中看到配置Axon即便在Spring中也是比较麻烦的，好在Axon提供了<code>spring-boot-autoconfigure</code>，提供了Spring下的一些默认配置，极大地方便了我们的工作。<br>启用也是非常方便的，在上一篇的基础上，我们只需要干三件事即可达成目标：</p><ol><li>引入<code>spring-boot-autoconfigure</code></li><li>删除JpaConfig类</li><li>去除<code>BankAccount</code>中的Entity声明</li></ol><p>由于提供的application.properties里关于数据库的配置信息本身就是符合SpringDatasource定义的，所以，SpringBoot在检测到该配置后自动启用JPA。<br><code>spring-boot-autoconfigure</code>中<code>AxonAutoConfiguration</code>类帮我们提供了最常用的<code>CommandBus</code>、<code>EventBus</code>、<code>EventStorageEngine</code>、<code>Serializer</code>、<code>EventStore</code>等，所以可以直接运行了。<br>在该类中有一段<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@ConditionalOnBean</span>(EntityManagerFactory.class)</span><br><span class="line"><span class="meta">@RegisterDefaultEntities</span>(packages = &#123;<span class="string">"org.axonframework.eventsourcing.eventstore.jpa"</span>,</span><br><span class="line">        <span class="string">"org.axonframework.eventhandling.tokenstore"</span>,</span><br><span class="line">        <span class="string">"org.axonframework.eventhandling.saga.repository.jpa"</span>&#125;)</span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">JpaConfiguration</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ConditionalOnMissingBean</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> EventStorageEngine <span class="title">eventStorageEngine</span><span class="params">(EntityManagerProvider entityManagerProvider,</span></span></span><br><span class="line"><span class="function"><span class="params">                                                 TransactionManager transactionManager)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> JpaEventStorageEngine(entityManagerProvider, transactionManager);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ConditionalOnMissingBean</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> EntityManagerProvider <span class="title">entityManagerProvider</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> ContainerManagedEntityManagerProvider();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ConditionalOnMissingBean</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> TokenStore <span class="title">tokenStore</span><span class="params">(Serializer serializer, EntityManagerProvider entityManagerProvider)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> JpaTokenStore(entityManagerProvider, serializer);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ConditionalOnMissingBean</span>(SagaStore.class)</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> JpaSagaStore <span class="title">sagaStore</span><span class="params">(Serializer serializer, EntityManagerProvider entityManagerProvider)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> JpaSagaStore(serializer, entityManagerProvider);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>所以，当我们提供了JPA相关配置，以及mysql-connector后，这些Bean也会被启用，可以看到里面默认的<code>EventStoreEngine</code>就是<code>JpaEventStorageEngine</code>。<br>执行后，我们可以看到数据库中创建了如下表<br><img src="/images/2017/03/db.png" alt=""></p><p>其中<code>domain_event_entry</code>就是用来保存对Aggregate状态造成改变的所有Event的表。如果不做特别声明，所有Event都会记录在这张表里。<br>表内容<br><img src="/images/2017/03/domainevents.png" alt=""><br>其中，比较重要的字段有</p><ul><li>pay_load Event的具体内容</li><li>pay_load_type Event的类型，Axon在ES(Event Sourcing)时会通过这个反射出来原来的Java class</li><li>time_stamp 该Event发生的时间</li><li>aggregate_identifier event所对应Aggregate的唯一标识，在ES时，只有相同identifier的event才会一起回溯</li><li>sequence_number 同一Aggregate对应的event发生的序列号，回溯时严格按照该顺序</li></ul><p>值得注意的是，在使用EventSourcing时，由于Aggregate本身的状态是通过ES获得的，所以所有对于Aggregate状态变化的动作一定都是放在<code>@EventHandler</code>里的，否则将会造成状态丢失。<br>预告一下，基本介绍已经完毕，下一篇开始，进入复杂的实现。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;blockquote&gt;
&lt;p&gt;继上一篇集成SpringBoot后，本篇将继续完成小目标：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用EventSourcing&lt;/li&gt;
&lt;li&gt;使用AutoConfigure配置Axon&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;前一篇中看到
      
    
    </summary>
    
      <category term="Java" scheme="http://edisonxu.com/categories/Java/"/>
    
      <category term="框架" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/"/>
    
      <category term="CQRS" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/CQRS/"/>
    
      <category term="Axon" scheme="http://edisonxu.com/categories/Java/%E6%A1%86%E6%9E%B6/CQRS/Axon/"/>
    
    
      <category term="event sourcing" scheme="http://edisonxu.com/tags/event-sourcing/"/>
    
      <category term="CQRS" scheme="http://edisonxu.com/tags/CQRS/"/>
    
      <category term="axon" scheme="http://edisonxu.com/tags/axon/"/>
    
      <category term="DDD" scheme="http://edisonxu.com/tags/DDD/"/>
    
  </entry>
  
</feed>
