-
Notifications
You must be signed in to change notification settings - Fork 4
/
atom.xml
507 lines (275 loc) · 381 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>ykgarfield</title>
<subtitle>ykgarfield's blog</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="https://ykgarfield.github.io/"/>
<updated>2019-03-09T14:43:20.437Z</updated>
<id>https://ykgarfield.github.io/</id>
<author>
<name>[object Object]</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>第3章-微服务架构中的进程间通信</title>
<link href="https://ykgarfield.github.io/2019/02/25/%E5%BE%AE%E6%9C%8D%E5%8A%A1/Microservice-Patterns(%E8%AF%91)/%E7%AC%AC3%E7%AB%A0-%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E4%B8%AD%E7%9A%84%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1/"/>
<id>https://ykgarfield.github.io/2019/02/25/微服务/Microservice-Patterns(译)/第3章-微服务架构中的进程间通信/</id>
<published>2019-02-24T16:00:00.000Z</published>
<updated>2019-03-09T14:43:20.437Z</updated>
<content type="html"><![CDATA[<p>这章包含:</p><ul><li>应用通信模式: 远程过程调用、断路器、客户端发现、自注册、服务器端发现、第三方注册、异步消息、事务发件箱(Transactional outbox)、事务日志跟踪、轮询发布者</li><li>微服务架构中进程间通信的重要性</li><li>定义和逐步演进 API</li><li>各种进程间通信选项及其权衡</li><li>使用异步消息进行通信的服务的好处</li><li>作为数据库事务的一部分可靠地发送消息</li></ul><p>Mary 和她的团队与大多数其他开发人员一样, 对进程间通信(IPC)机制有一定的经验.FTGO 应用程序有一个由移动应用程序和浏览器端 JavaScript 使用的 REST API.它还使用各种云服务, 如 Twilio 消息服务和 Stripe 支付服务.但是在像 FTGO 这样的单体应用程序中, 模块通过语言级别的方法或函数调用彼此调用.FTGO 开发人员通常不需要考虑 IPC, 除非他们正在开发REST API 或与云服务集成的模块.</p><p>相反, 正如您在第 2 章中看到的, 微服务架构将应用程序构建为一组服务.为了处理请求, 这些服务必须经常协作.因为服务实例通常是在多台计算机上运行的进程, 所以它们必须使用 IPC 进行交互.它在微服务架构中所起的作用要比在单体应用程序中所起的作用大得多.因此, 当他们将应用程序迁移到微服务时, Mary 和其他 FTGO 开发人员将需要花费更多的时间考虑 IPC.</p><p>可供选择的 IPC 机制并不缺乏.如今, 流行的选择是 REST(JSON 格式).然而, 重要的是要记住, 没有灵丹妙药.你必须仔细考虑这些选择.本章探讨了各种 IPC 选项, 包括 REST 和消息, 并讨论了权衡.</p><p>IPC 机制的选择是一个重要的架构决策.它会影响应用程序的可用性.而且, 正如我在本章和下一章中解释的那样, IPC 甚至与事务管理交叉.我喜欢由使用异步消息彼此通信的松散耦合服务组成的体系结构.同步协议(如 REST)主要用于与其他应用程序通信.</p><p>本章首先概述了微服务架构中的进程间通信.接下来, 我将描述基于远程过程调用的 IPC, 其中 REST 是最流行的例子.我将讨论重要的主题, 包括服务发现和如何处理分区故障.在那之后, 我描述了基于异步消息的 IPC.我还讨论了在保持消息顺序、正确处理重复消息和事务性消息传递的同时扩展消费者.最后, 我介绍了自包含服务的概念, 它处理同步请求, 而不与其他服务通信, 以提高可用性.</p><h1 id="微服务架构进程间通信概览"><a href="#微服务架构进程间通信概览" class="headerlink" title="微服务架构进程间通信概览"></a>微服务架构进程间通信概览</h1><p>有许多不同的 IPC 技术可供选择.服务可以使用基于请求/响应的同步通信机制, 例如基于 HTTP 的 REST 或 gRPC.或者, 它们可以使用异步的、基于消息的通信机制, 如 AMQP 或 STOMP.还有各种不同的消息格式.服务可以使用人类可读的、基于文本的格式, 如 JSON 或 XML.或者, 它们可以使用更有效的二进制格式, 如 Avro 或 Protocol Buffers.</p><p>在详细讨论具体技术之前, 我想提出几个您应该考虑的设计问题.本节首先讨论交互方式, 交互方式是描述客户端和服务如何交互的一种独立于技术的方式.接下来, 我将讨论在微服务架构中精确定义 API 的重要性, 包括 API 优先设计的概念.在此之后, 我将讨论 API 演进的重要主题.最后, 我将讨论消息格式的不同选项, 以及它们如何确定 API 演进的易用性.让我们从交互方式开始.</p><h2 id="交互方式"><a href="#交互方式" class="headerlink" title="交互方式"></a>交互方式</h2><p>在为服务的 API 选择 IPC 机制之前, 首先考虑服务与其客户端之间的交互方式是很有用的.首先考虑交互方式将帮助您专注于需求, 避免陷入特定 IPC 技术的细节中.而且, 正如 3.4 节所述, 交互方式的选择会影响应用程序的可用性.此外, 正如您将在第 9 章和第 10 章中看到的, 它将帮助您选择适当的集成测试策略.</p><p>客户端-服务交互方式有很多种.如表 3.1 所示, 它们可以分为两个维度.第一个维度是交互是一对一还是一对多:</p><ul><li><em>一对一</em>-每个客户端请求都由一个服务处理.</li><li><em>一对多</em>-每个请求由多个服务处理.</li></ul><p>第二个维度是交互是同步还是异步:</p><ul><li><em>同步</em>-客户端期望来自服务的及时响应, 甚至可能在等待时阻塞.</li><li><em>异步</em>-客户端不会阻塞, 响应(如果有的话)也不一定会立即发送.</li></ul><p><strong>表 3.1 各种交互方式可以从两个维度来描述: 一对一 vs 一对多和同步 vs 异步</strong></p><table><thead><tr><th>-</th><th>一对一</th><th>一对多</th></tr></thead><tbody><tr><td>同步</td><td>请求/响应</td><td>-</td></tr><tr><td>异步</td><td>异步请求/响应单向通知</td><td>发布/订阅 <br> 发布/异步响应</td></tr></tbody></table><p>以下是一对一交互的不同类型:</p><ul><li><em>请求/响应</em>-服务客户端向服务发出请求并等待响应.客户端希望响应及时到达.它可能会在等待时阻塞.这是一种通常会导致服务被紧密耦合的交互方式.</li><li><em>异步请求/响应</em>-服务客户端向服务发送请求, 服务异步响应.客户机在等待时不会阻塞, 因为服务可能很长一段时间没有发送响应.</li><li><em>单向通知</em>-服务客户端向服务发送请求, 但不期望或发送任何响应.</li></ul><p>重要的是要记住同步请求/响应交互方式与 IPC 技术大多是正交的.例如, 服务可以使用与 REST 或消息的请求/响应方式与另一个服务交互.即使两个服务使用消息代理(message broker)通信, 客户端服务也可能在等待响应时被阻塞.这并不一定意味着它们是松散耦合的.在本章后面讨论服务间通信对可用性的影响时, 我将再次讨论这一点.</p><p>以下是一对多交互的不同类型:</p><ul><li><em>发布/订阅</em>-客户端发布一条通知消息, 该消息由零个或多个感兴趣的服务使用.</li><li><em>发布/异步响应</em>-客户端发布请求消息, 然后等待来自感兴趣的服务的响应, 等待一定的时间.</li></ul><p>每个服务通常会使用这些交互方式的组合.FTGO 应用程序中的许多服务都有用于操作的同步和异步的 API, 而且许多服务还发布事件.</p><p>让我们看看如何定义服务 API.</p><h2 id="定义微服务架构-API"><a href="#定义微服务架构-API" class="headerlink" title="定义微服务架构 API"></a>定义微服务架构 API</h2><p>API 或接口是软件开发的核心.应用程序由模块组成.每个模块都有一个定义模块的客户端可以调用的接口的操作集.设计良好的接口在隐藏实现的同时暴露了有用的功能.它使实现能够在不影响客户端的情况下进行更改.</p><p>在单体应用程序中, 接口通常使用 Java 接口等编程语言结构指定.Java 接口指定客户端可以调用的一组方法.实现类对客户端是隐藏的.此外, 由于 Java 是静态类型语言, 如果接口更改与客户端不兼容, 应用程序将无法编译.</p><p>API 和接口在微服务架构中同样重要.服务的 API 是服务与其客户端之间的契约.如第 2 章所述, 服务的 API 由客户端可以调用的操作和服务发布的事件组成.操作具有名称、参数和返回类型.事件具有类型和一组字段, 如 3.3 节所述, 事件被发布到消息通道.</p><p>挑战在于服务 API 不是使用简单的编程语言结构定义的.根据定义, 服务及其客户端不会一起编译.如果使用不兼容的 API 部署服务的新版本, 则不会出现编译错误.相反, 会出现运行时故障.</p><p>无论选择哪种 IPC 机制, 使用某种 <em>接口定义语言(IDL)</em> 精确定义服务的 API 都非常重要.此外, 使用 API 优先的方式定义服务也有很好的理由(更多信息请参见 <a href="http://www.programmableweb.com/news/how-to-design-great-apis-api-first-design-and-raml/how-to/2015/07/10)" target="_blank" rel="noopener">www.programmableweb.com/news/how-to-design-great-apis-api-first-design-and-raml/how-to/2015/07/10)</a>, 首先编写接口定义, 然后与客户端开发人员一起审查接口定义.只有在对 API 定义进行审查完之后, 才能实现服务.进行这种预先设计可以增加构建满足客户需求的服务的机会.</p><blockquote><p><strong>API 优先设计是必不可少的</strong> <br><br>即使在小型项目中, 我也看到过因为组件在 API 上达不成一致而出现的问题.例如, 在一个项目中, 后端 Java 开发人员和 AngularJS 前端开发人员都说他们已经完成了开发.然而, 应用程序没有工作.前端应用程序用于与后端通信的 REST 和 WebSocket API 定义得很差.结果, 两个应用程序无法通信!</p></blockquote><p>API定义的性质取决于您使用的 IPC 机制.例如, 如果使用消息, API 由消息通道、消息类型和消息格式组成.如果使用 HTTP, API 由 URL、HTTP 谓词和请求/响应格式组成.在本章的后面, 我将解释如何定义 API.</p><p>服务的 API 很少是一成不变的.它很可能会随着时间的推移而演变.让我们看看如何做到这一点, 并考虑一下您将面临的问题.</p><h2 id="演进的-API"><a href="#演进的-API" class="headerlink" title="演进的 API"></a>演进的 API</h2><p>随着新特性的添加、现有特性的更改以及(也许)旧特性的删除, API 总是会随着时间的推移而变化.在一个单体应用程序中, 更改 API 并更新所有调用者相对简单.如果您使用的是静态类型语言, 那么编译器可以通过提供编译错误列表来提供帮助.唯一的挑战可能是改变的范围.更改一个广泛使用的 API 可能需要很长时间.</p><p>在基于微服务的应用程序中, 更改服务的 API 要困难得多.服务的客户端是其他服务, 这些服务通常由其他团队开发.客户端甚至可能是组织之外的其他应用程序.通常不能强制所有客户端与服务同步升级.而且, 由于现代应用程序通常不会停机进行维护, 所以通常会对服务执行滚动升级, 因此服务的新旧版本将同时运行.</p><p>制定应对这些挑战的策略很重要.如何处理对 API 的更改取决于更改的性质.</p><p><strong>使用语义版本控制</strong><br>语义版本控制规范(<a href="http://semver.org)是版本控制" target="_blank" rel="noopener">http://semver.org)是版本控制</a> API 的有用指南.它是一组规则, 用来指定版本号是如何使用和增加的.语义版本控制最初打算用于软件包的版本控制, 但您可以将其用于分布式系统中的版本控制 API.</p><p>语义版本规范(Semvers)要求版本号包含三个部分: <code>MAJOR.MINOR.PATCH</code>.你必须增加版本号的每一部分, 如下:</p><ul><li><code>MAJOR</code>-当您对 API 进行不兼容的更改时</li><li><code>MINOR</code>-当您对 API 进行向后兼容的增强时</li><li><code>PATCH</code>-当您进行向后兼容的 bug 修复时</li></ul><p>在 API 中有几个地方可以使用版本号.如果您正在实现一个 REST API, 您可以像下面提到的那样, 使用 <code>major</code> 版本作为 URL 路径的第一个元素.或者, 如果您正在实现一个使用消息的服务, 您可以在它发布的消息中包含版本号.目标是正确地对 API 进行版本控制, 并以可控的方式对其进行改进.让我们看看如何处理 <code>minor</code> 的和 <code>major</code> 的更改。</p><p><strong>进行较小的(minor), 向后兼容的更改</strong><br>理想情况下, 您应该努力只进行向后兼容的更改.向后兼容的更改是 API 的附加更改:</p><ul><li>向请求添加可选属性</li><li>向响应添加属性</li><li>添加新的操作</li></ul><p>如果您只进行这些类型的更改, 老客户端将使用新服务, 前提是它们遵守健壮性原则(<a href="https://en.wikipedia.org/wiki/Robustness_principle)" target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Robustness_principle)</a>, 该原则声明: “在您所做的事情上要保守, 在您接受别人的事情上要自由.” 服务应该为没有的请求属性提供默认值.类似地, 客户端应该忽略任何额外的响应属性.为了做到这一点, 客户端和服务必须使用支持健壮性原则的请求和响应格式.在本节稍后部分中, 我将介绍 JSON 和 XML 等基于文本的格式通常如何使 API 更容易演进.</p><p><strong>做出重大的(major), 突破性的改变</strong><br>有时您必须对 API 进行重大的、不兼容的更改.因为您不能强制客户端立即升级, 所以服务必须在一段时间内同时支持 API 的新旧版本.如果使用基于 HTTP 的 IPC 机制(如 REST), 一种方法是将主(major)版本号嵌入 URL.例如, 版本 1 的路径以 <code>'/v1/...'</code> 为前缀, 版本 2 的路径以 <code>'/v2/...'</code> 为前缀.</p><p>另一种选择是使用 HTTP 的内容协商机制, 并在 MIME 类型中包含版本号.例如, 客户端将使用如下请求请求<code>Order</code>的 <code>1.x</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></pre></td><td class="code"><pre><span class="line">GET /orders/xyz HTTP/1.1</span><br><span class="line">Accept: application/vnd.example.resource+json; version=1</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>这个请求告诉 <code>Order</code> 服务客户端期望版本 <code>1.x</code> 的响应.</p><p>为了支持一个 API 的多个版本, 实现 API 的服务适配器将包含在新旧版本之间转换的逻辑.而且, 正如第 8 章所述, API 网关几乎肯定会使用版本化的 API.它甚至可能必须支持许多旧版本的 API.</p><p>现在我们来看看消息格式的问题, 消息格式的选择会影响 API 的演进.</p><h2 id="消息格式"><a href="#消息格式" class="headerlink" title="消息格式"></a>消息格式</h2><p>IPC 的本质是消息的交换.消息通常包含数据, 因此数据的格式是重要的设计决策.消息格式的选择会影响 IPC 的效率, API 的可用性和可扩展性.如果使用的是消息传递系统或 HTTP 等协议, 则可以选择消息格式.一些 IPC 机制-例如 gRPC, 您将简要了解它-可能规定了消息格式.无论哪种情况, 都必须使用跨语言的消息格式.即使您现在使用一种语言编写微服务, 将来也可能使用其他语言.例如, 您不应该使用 Java 序列化.</p><p>消息格式有两大类:文本和二进制.让我们看看每一个.</p><p><strong>基于文本的消息格式</strong><br>第一类是基于文本的格式, 如 JSON 和 XML.这些格式的优点是, 它们不仅是人类可读的, 而且是自我描述的.JSON 消息是命名属性的集合.类似地, XML 消息实际上是命名元素和值的集合.这种格式使消息的使用者能够选择感兴趣的值, 而忽略其他值.因此, 对消息模式的许多更改可以很容易地向后兼容.</p><p>XML 文档的结构由 XML schema 指定(<a href="http://www.w3.org/XML/Schema).随着时间的推移" target="_blank" rel="noopener">www.w3.org/XML/Schema).随着时间的推移</a>, 开发人员社区逐渐认识到 JSON 也需要类似的机制.一个流行的选择是使用 JSON schema标准(<a href="http://json-schema.org).JSON" target="_blank" rel="noopener">http://json-schema.org).JSON</a> schema 定义消息属性的名称和类型, 以及它们是可选的还是必需的.JSON模式不仅是有用的文档, 应用程序还可以使用它来验证传入的消息.</p><p>使用基于文本的消息格式的缺点是消息往往很冗长, 尤其是 XML.每个消息除了包含属性的值之外, 还包含属性的名称.另一个缺点是解析文本的开销, 尤其是在消息很大的情况下.因此, 如果效率和性能很重要, 您可能需要考虑使用二进制格式.</p><p><strong>二进制消息格式</strong><br>有几种不同的二进制格式可供选择.流行的格式包括 Protocol Buffers(<a href="https://developers.google.com/protocol-buffers/docs/overview" target="_blank" rel="noopener">https://developers.google.com/protocol-buffers/docs/overview</a>) 和 Avro (<a href="https://avro.apache.org).这两种格式都提供了用于定义消息结构的类型化" target="_blank" rel="noopener">https://avro.apache.org).这两种格式都提供了用于定义消息结构的类型化</a> IDL.然后, 使用编译器生成序列化和反序列化消息的代码.您被迫采用 API 优先的方法进行服务设计!此外, 如果您使用静态类型语言编写客户端, 编译器将检查它是否正确使用 API.</p><p>这两种二进制格式之间的一个区别是, Protocol Buffers 使用标记(tagged)字段, 而 Avro 使用者需要知道模式才能解释消息.因此, 使用 Protocol Buffers 比使用Avro 更容易处理 API 演进.这篇博文(<a href="http://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html)是对" target="_blank" rel="noopener">http://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html)是对</a> Thrift、Protocol buffer 和 Avro 的绝佳比较。</p><p>现在我们已经了解了消息格式, 让我们从远程过程调用(Remote procedure invocation, RPI)模式开始, 研究传输消息的特定 IPC 机制.</p><h1 id="使用同步远程过程调用模式进行通信"><a href="#使用同步远程过程调用模式进行通信" class="headerlink" title="使用同步远程过程调用模式进行通信"></a>使用同步远程过程调用模式进行通信</h1><p>当使用基于远程过程调用的 IPC 机制时, 客户端向服务发送请求, 服务处理请求并发回响应.一些客户端可能阻塞等待响应, 而其他客户端可能具有响应式的、非阻塞的架构.但与使用消息不同, 客户端假定响应将以一种及时的方式到达.</p><p>图 3.1 显示了 RPI 是如何工作的.客户端中的业务逻辑调用由 RPI 代理适配器类实现的代理接口.RPI 代理向服务发出请求.请求由 RPI 服务器适配器类处理, 该类通过接口调用服务的业务逻辑.然后, 它向 RPI 代理发送回复.RPI 代理将结果返回给客户端的业务逻辑.</p><blockquote><p>模式: 远程过程调用 <br><br>客户端使用同步的, 基于远程过程调用的协议, 比如 REST 调用服务(<a href="http://microservices.io/patterns/communication-style/messaging.html)" target="_blank" rel="noopener">http://microservices.io/patterns/communication-style/messaging.html)</a>.</p></blockquote><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第3章-微服务架构中的进程间通信/Figure_3.1_How_RPI_works.jpg" alt="Figure 3.1 How RPI works"></p><p>代理接口通常封装底层通信协议.有多种协议可供选择.在本节中, 我将描述 REST 和 gRPC.我将介绍如何通过正确处理分区故障来提高服务的可用性, 并解释为什么使用 RPI 的基于微服务的应用程序必须使用服务发现机制.</p><p>让我们首先看看 REST.</p><h2 id="使用-REST"><a href="#使用-REST" class="headerlink" title="使用 REST"></a>使用 REST</h2><p>现在, 流行以 RESTful 风格开发 API(<a href="https://en.wikipedia.org/wiki/Representational_state_transferr).REST" target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Representational_state_transferr).REST</a> 是一种(几乎总是)使用 HTTP 的 IPC 机制.REST 的创建者 Roy Fielding 对REST的定义如下:</p><blockquote><p><em>REST 提供了一组架构约束, 当作为一个整体应用时, 这些约束强调组件交互的可伸缩性、接口的通用性、组件的独立部署和中介组件, 以减少交互延迟、加强安全性和封装遗留系统.</em> <br><br> <a href="http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm" target="_blank" rel="noopener">www.ics.uci.edu/~fielding/pubs/dissertation/top.htm</a></p></blockquote><p>REST 中的一个关键概念是<em>资源</em>, 它通常表示单个业务对象, 如客户或产品, 或业务对象的集合.REST 使用 HTTP 谓词来操作使用 URL 引用的资源.例如, GET 请求返回资源的表示形式, 通常是 XML 文档或 JSON 对象的形式, 但也可以使用二进制等其他格式.POST 请求创建一个新资源, PUT 请求更新一个资源.例如, <code>订单服务</code> 具有用于创建<code>订单</code>的 <code>POST /orders</code> 端点和用于检索 <code>订单</code> 的 <code>GET /orders/{orderId}</code> 端点.</p><p>许多开发人员声称他们基于 HTTP 的 API 是 RESTful 的.但正如 Roy Fielding 在一篇博客文章中所描述的, 并非所有这些都是由超文本驱动的(<a href="http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven).为了理解其中的原因" target="_blank" rel="noopener">http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven).为了理解其中的原因</a>, 让我们看看 REST 成熟度模型.</p><p><strong>REST 成熟度模型</strong></p><p>Leonard Richardson(与作者无关)为 REST 定义了一个非常有用的成熟度模型(<a href="http://martinfowler.com/articles/richardsontymodel.html)" target="_blank" rel="noopener">http://martinfowler.com/articles/richardsontymodel.html)</a>, 它包含以下级别:</p><ul><li><em>级别 0</em>-0 级服务的客户端通过向其唯一的 URL 端点发出 HTTP POST 请求.每个请求指定了执行的动作, 动作的目标(比如, 业务对象), 和任何参数.</li><li><em>级别 1</em>-级别 1 的服务支持资源的理念.为了执行一个资源上的动作, 客户端发出一个 POST 请求指定要执行的动作和任何参数.</li><li><em>级别 2</em>-级别 2 的服务使用 HTTP 谓词指定动作: GET 用来获取, POST 用来创建, PUT 用来更新.请求查询参数和主体, 如果有的话, 指定了动作的参数.这使得服务可以使用 web 基础设施比如对 GET 请求进行缓存.</li><li><em>级别 3</em>-级别 3 的服务的设计基于非常有名的 HATEOAS(作为应用程序状态引擎的超文本)原则.基本思想是 GET 请求返回的资源的表示包含对该资源执行操作的链接.例如, 客户端可以使用检索订单的 GET 请求返回的表示形式中的链接来取消订单.HATEOAS 的好处包括不再需要将 url 硬织入到客户机端码(<a href="http://www.infoq.com/news/2009/04/hateoas-restful-api-advantages)" target="_blank" rel="noopener">www.infoq.com/news/2009/04/hateoas-restful-api-advantages)</a>.</li></ul><p>我鼓励您查看组织中的 REST api, 看看它们对应于哪个级别.</p><p><strong>指定 REST APIs</strong></p><p>如之前在 3.1 节中提到的, 你必须使用接口定义语言(IDL)定义你的 APIs.不像老的通信协议, 比如 CORBA 和 SOAP, REST 最初没有 IDL.幸运地是, 开发者社区重新发现了 IDL 对于 RESTful API 的价值.最流行的 REST IDL 是 Open API 规范(<a href="http://www.openapis.org)" target="_blank" rel="noopener">www.openapis.org)</a>, 它从 Swagger 开源项目中演进而来.Swagger 项目是一个工具集用于开发和文档化 REST APIs.它包含了从接口定义生成客户端桩和服务器骨架的工具.</p><p><strong>在一个请求中获取多个资源的挑战</strong></p><p>REST 资源通常面向业务对象, 比如 <code>消费者</code> 和 <code>订单</code>.因此, 在设计 REST API 时, 一个常见的问题是如何让客户端在一个请求中获取多个相关联的对象.例如, 假设REST 客户端想要获取订单和订单消费者.纯粹的 REST API 要求客户端至少发出两个请求, 一个请求用于订单, 另一个请求用于其消费者.更复杂的场景将需要更多的往返, 并遭受过度延迟.</p><p>这种 API 的问题的一种解决方案就是允许客户端获取相关的资源当它获取某个资源的时候.比如, 一个客户端可能使用 <code>GET /orders/order-id-1345?expand=consumer</code> 获取一个订单和它的消费者.查询参数指定了要随着 <code>订单</code> 一起返回的相关资源.这种方式在许多场景中都工作良好, 但是对于更复杂的场景它通常并不能满足.实现它也可能很耗时.这导致了其它 API 技术的日益流行, 如 GraphQL(<a href="http://graphql.org" target="_blank" rel="noopener">http://graphql.org</a>) 和 Netflix Falcor(<a href="http://netflix.github.io/falcor/)" target="_blank" rel="noopener">http://netflix.github.io/falcor/)</a>, 这些技术旨在支持高效的数据获取.</p><p><strong>将操作映射到 HTTP 谓词的挑战</strong></p><p>另一个常见的 REST API 设计问题是如何将在一个业务对象上执行的操作映射到 HTTP 谓词.一个 REST API 应该使用 PUT 用于更新, 但是有多种方式更新一个订单, 包括取消订单, 修改订单等等.另外, 更新可能不是幂等的, 这是使用 PUT 的一个要求.一种方式就是定义子资源用来更新资源的特定方面.比如, 订单服务, 有一个 <code>POST /orders/{orderId}/cancel</code> 端点用来取消订单, 和一个 <code>POST /orders/{orderId}/revise</code> 端点用来修改订单.另一种方式是指定一个谓词作为 URL 查询参数.遗憾的是, 这两种解决方案都不是特别 RESTful.</p><p>将操作映射到 HTTP 谓词的问题导致 REST 的替代方案, 如在 3.2.2 节中稍后讨论的 gPRC 越来越流行.但是首先让我们看看 REST 的优点和缺点.</p><p><strong>REST 的缺点和优点</strong></p><p>使用 REST 有不少好处:</p><ul><li>简单又熟悉.</li><li>你可以使用浏览器测试 HTTP API, 比如, Postman 插件, 或者使用 curl(假设使用是的 JSON 或某些其他文本格式)的命令行工具.</li><li>它直接支持请求/响应风格的通信.</li><li>当然, HTTP 对防火墙是友好的.</li><li>它不需要中间代理, 这简化了系统的架构.</li></ul><p>使用 REST 也有一些缺点:</p><ul><li>它只支持请求/响应风格的通信.</li><li>降低了可用性.因为客户端和服务直接通信而不用中间代理来缓冲消息, 在通信期间, 它们必须同时运行.</li><li>客户端必须知道服务实例的位置(URLs).如在 3.2.4 节所描述的, 这在现代应用程序中是一个非常重要的问题.客户端必须使用服务发现机制来定位服务实例.</li><li>在单个请求中获取多个资源是一个挑战.</li><li>映射多个更新操作到 HTTP 谓词有时候是有些困难的.</li></ul><p>尽管有这些缺点, REST 似乎是 API 的实际标准, 尽管有一些令人关注的替代方法.例如, GraphQL 实现了灵活、高效的数据获取.第 8 章讨论了 GraphQL 并介绍了 API 网关模式.</p><p>gRPC 是 REST 的另一种选择.让我们看下它是如何工作的.</p><p><strong>使用 gRPC</strong></p><p>正如上一节提到的, 使用 REST 的一个挑战就是由于 HTTP 只提供了有限数量的谓词, 因此设计支持多个更新操作的 REST API 并不总是那么简单.避免这个问题的一个 IPC 技术就是 gRPC(<a href="http://www.grpc.io)" target="_blank" rel="noopener">www.grpc.io)</a>, 一个用来编写跨语言客户端和服务端的框架(见 <a href="https://en.wikipedia.org/wiki/Remote_procedure_call" target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Remote_procedure_call</a> 查看更多信息), gRPC 是一种基于二进制消息的协议, 这意味着如之前在讨论二进制消息格式时所提到的, 你必须采用 API 优先的方式来进行服务设计.你可以使用 Protocol Buffer(基于 IDL)来定义你的 gRPC APIs, 这是 Google 的用来序列化结构化数据的独立于语言的机制.你可以使用 Protocol Buffer 编译器来生成客户端桩和服务端骨架.编译器可以生成多种语言的代码, 包括 Java, C#, NodeJS 和 GoLang.客户端和服务端使用 HTTP/2 以 Protocol Buffers 的格式来交换二进制消息.</p><p>一个 gRPC API 由一个或多个服务和请求/响应消息定义组成.一个<em>服务定义</em>类似于 Java 接口, 和一个强类型方法的集合.除了支持简单的请求/响应 RPC, gRPC 也支持流式 RPC.一个服务端可以回复流式的消息给客户端.或者, 客户端可以向服务器发送消息流.</p><p>gRPC 使用 Protocol Buffers 作为消息格式.如之前提到的, Protocol Buffers 是一种高效的, 紧凑的, 二进制格式.它是一种带标签的格式.Protocol Buffers 消息的每个字段都有编号并且具有类型编码.一个消息的接收者可以提起它需要的字段并跳过它不能识别的字段.因此, gRPC 允许 API 在保持向后兼容的同时不断演进.</p><p>清单 3.1 显示了<code>订单服务</code>的 gRPC API 摘录.它定义了几个方法, 包括 <code>createOrder</code>.此方法有一个 <code>CreateOrderRequest</code> 参数并返回一个 <code>CreateOrderReply</code>.</p><p><strong>清单 3.1 订单服务的 gRPC API 摘录</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><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">service OrderService {</span><br><span class="line">rpc createOrder(CreateOrderRequest) returns (CreateOrderReply) {}</span><br><span class="line">rpc cancelOrder(CancelOrderRequest) returns (CancelOrderReply) {}</span><br><span class="line">rpc reviseOrder(ReviseOrderRequest) returns (ReviseOrderReply) {}</span><br><span class="line">...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">message CreateOrderRequest {</span><br><span class="line">int64 restaurantId = 1;</span><br><span class="line">int64 consumerId = 2;</span><br><span class="line">repeated LineItem lineItems = 3;</span><br><span class="line">...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">message LineItem {</span><br><span class="line">string menuItemId = 1;</span><br><span class="line">int32 quantity = 2;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">message CreateOrderReply {</span><br><span class="line">int64 orderId = 1;</span><br><span class="line">}</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p><code>CreateOrderRequest</code> 和 <code>CreateOrderReply</code> 是定义的消息.比如, <code>CreateOrderRequest</code> 消息由一个 <code>int64</code> 类型的 <code>restaurantId</code> 字段.字段的标记值是 1.</p><p>gRPC 有几个优点:</p><ul><li>设计一个具有丰富更新操作集的 API 非常简单.</li><li>它具有高效、紧凑的 IPC 机制, 特别是在交换大型消息时.</li><li>双向流支持 RPI 和消息风格的通信.</li><li>它支持使用多种语言编写的客户端和服务之间的互操作性.</li></ul><p>gRPC 同样有几个缺点:</p><ul><li>JavaScript 客户端使用基于 gRPC 的 API 比使用基于 REST/JSON 的 API 需要更多的工作.</li><li>旧的防火墙可能不支持 HTTP/2.</li></ul><p>gRPC 是 REST 的一个很有吸引力的替代方案, 但与 REST 一样, 它也是一个同步通信机制, 因此也存在局部故障的问题.让我们看看这是什么以及如何处理它.</p><h2 id="使用断路器模式处理局部故障"><a href="#使用断路器模式处理局部故障" class="headerlink" title="使用断路器模式处理局部故障"></a>使用断路器模式处理局部故障</h2><p>在分布式系统中, 无论何时一个服务发出一个同步请求到另一个服务, 局部故障的风险一直存在.因为客户端和服务是独立的进程, 一个服务可能无法及时响应客户端的请求.服务可能宕机因为故障或者维护.或者服务可能过载, 对请求的响应速度非常慢.因为客户端是阻塞等待响应, 危险在于故障可能会级联到客户端的客户端, 从而导致停机.</p><blockquote><p><strong>模式: 断路器</strong><br>RPI 代理在连续失败的次数超过指定的阈值后, 会在超时期间立即拒绝调用.详见 <a href="http://microservices.io/patterns/reliability/circuit-breaker.html" target="_blank" rel="noopener">http://microservices.io/patterns/reliability/circuit-breaker.html</a>.</p></blockquote><p>考虑一个例子, 如图 3.2 所示的场景, <code>订单服务</code>无法响应.移动客户端发出一个 REST 请求到 API 网关(这是 API 客户端到应用程序的入口), 这将在第 8 章讨论.API 网关代理了到无法响应的 <code>订单服务</code> 的请求.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.2-An%20API%20gateway%20must%20protect%20itself%20from%20unresponsive%20services.png" alt="Figure 3.2-An API gateway must protect itself from unresponsive services"></p><p><code>OrderServiceProxy</code> 是一个简单实现是无限阻塞, 等待响应.这不仅会导致糟糕的用户体验, 而且会在许多应用程序中消耗宝贵的资源, 比如线程.最终, API 网关将耗尽资源无法处理请求.整个 API 将不可用.</p><p>设计服务以防止局部故障在整个应用程序中级联是非常重要的.解决方案分为两部分:</p><ul><li>你必须使用设计好的 RPI 代理, 比如 <code>OrderServiceProxy</code>, 来处理无法响应的远程服务.</li><li>你需要决定如何从一个失败的远程服务中恢复.</li></ul><p>首先让我们看看如何编写鲁棒的 RPC 代理.</p><p><strong>开发鲁棒的 API 代理</strong></p><p>无论何时一个服务同步调用另一个服务, 它应该使用由 Netflix 描述的方案(<a href="http://techblog.netflix.com/2012/02/fault-tolerance-in-high-volume.html)保护自身.这种方案由以下机制组成" target="_blank" rel="noopener">http://techblog.netflix.com/2012/02/fault-tolerance-in-high-volume.html)保护自身.这种方案由以下机制组成</a>:</p><ul><li><em>网络超时</em>-永远不会无限期阻塞, 并总是使用超时当等待响应的时候.使用超时保证了资源不会被无限期占用.</li><li><em>限制从客户端到服务的未完成请求的数量</em>-对客户端可以向特定服务发出的未完成请求的数量施加上限.如果限制达到, 发起额外的请求可能毫无意义, 并且这些尝试应该立即失败.</li><li><em>断路器模式</em>-追踪成功和失败的请求数量, 如果错误率超过了某些阈值, 触发断路器, 这样后面的尝试将会立即失败.大量失败的请求表明服务是不可用的, 因此发送更多的请求是无意义的.一段时间之后, 客户端应该再次尝试, 如果成功, 关闭断路器.</li></ul><p>Netflix Hystrix (<a href="https://github.com/Netflix/Hystrix" target="_blank" rel="noopener">https://github.com/Netflix/Hystrix</a>) 是一个开源的库, 实现了这些以及一些其他的模式.如果你正在使用 JVM, 你一定要使用 Hystrix 当实现 RPI 代理的时候.如果你的程序运行在非 JVM 环境中, 你应该使用类似的库.比如, 在 .NET 社区中 <code>Polly</code> 是一个流行的库(<a href="https://github.com/App-vNext/Polly)" target="_blank" rel="noopener">https://github.com/App-vNext/Polly)</a>.</p><p><strong>从无法响应的服务中恢复</strong></p><p>使用 Hystrix 类似的库只是解决方案的一部分.你还必须根据具体情况决定服务应该如何从无响应的远程服务中恢复.一种方法就是简单返回一个错误给客户端.比如, 这种方法对于图 3.2 所示的场景是有意义的, 也就是创建订单的请求失败了.对于 API 网关的唯一方法就是返回一个错误给移动客户端.</p><p>在其它场景中, 返回一个 fallback 值, 比如一个默认值或者一个缓存的响应, 这样可能有意义一点.比如, 第 7 章描述了 API 网关如何使用 API 组合模式(API composition pattern)来实现 <code>findOrder()</code> 查询操作.如图 3.3 所示, <code>GET /orders/{orderId}</code> 端点的实现调用多个服务, 包括 <code>订单服务</code>, <code>餐厅服务</code> 和 <code>配送服务</code>, 然后组合结果.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.3-The%20API%20gateway%20using%20API%20composition.png" alt="Figure 3.3-The API gateway using API composition"></p><p>很可能每个服务的数据对于客户端来说不是同等重要的.<code>订单服务</code> 的数据是重要的.如果服务不可用, API 网关应该返回一个缓存版本的数据或者一个错误.其他服务的数据不那么重要.比如, 客户端可以展示有用的信息给用户即使配送状态不可用.如果 <code>配送服务</code> 不可用, API 网关应该返回缓存版本的值或者在响应中省略它.</p><p>设计你的服务可以用来处理局部故障是至关重要的, 但这不是使用 RPI 的时候要解决的唯一问题.另外一个问题是为了让一个服务使用 RPI 调用另外一个服务, 它需需要知道服务实例的网络地址.表面上看这听起来比较简单, 但是在实践中这是一个挑战问题.你必须使用服务发现机制, 让我们看下这是如何工作的.</p><h2 id="使用服务发现"><a href="#使用服务发现" class="headerlink" title="使用服务发现"></a>使用服务发现</h2><p>假设你正在编写调用一个有 REST API 的服务的代码.为了发起一个请求, 你的代码需要知道服务实例的网络位置(IP 地址和端口).在一个运行在服务硬件上的传统应用程序中, 服务实例的网络位置通常是静态的.比如, 你的代码从一个偶尔更新的配置文件中读取网络位置.但是现在, 基于云的微服务应用程序, 通常不那么简单.如图 3.4 所示, 一个现代的应用程序更加的动态.</p><p>服务实例具有动态分配的网络地址.此外, 服务实例的集合动态变化因为自动扩容、故障和升级.因此, 你的客户端代码必须使用服务发现.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.4-Service%20instances%20have%20dynamically%20assigned%20IP%20addresses.png" alt="Figure 3.4-Service instances have dynamically assigned IP addresses"></p><p><strong>服务发现概览</strong></p><p>如你刚刚看到的, 你无法静态配置一个客户端使用的服务的 IP 地址.相反, 应用程序必须使用动态服务发现机制.服务发现从概念上来说是相当简单的: 它的主要组件是服务注册表, 也就是一个应用程序服务实例网络地址的数据库.服务发现机制更新服务注册表当服务实例启动或停止的时候.当一个客户端调用一个服务, 服务发现机制查询服务注册表来获取服务实例的可用列表, 然后将请求路由到其中一个服务实例.</p><p>有两种方式来实现服务发现:</p><ul><li>服务和它们的客户端直接使用服务注册表进行交互.</li><li>部署基础设施处理服务发现(我将在第 12 章详细讨论这一点).</li></ul><p>让我们看下每种方式.</p><p><strong>应用应用程序级别的服务发现模式</strong></p><p>对于应用程序服务和它们的客户端来说实现服务发现的一种方式就是使用注册表进行交互.图 3.5 展示了这是如何工作的.服务实例使用服务注册表注册它的网络地址.服务客户端调用一个服务的时候首先查询服务注册表来获取服务实例的列表.然后它将请求发给这些实例中的其中一个.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.5-The%20service%20registry%20keeps%20track%20of%20the%20service%20instances.png" alt="Figure 3.5-The service registry keeps track of the service instances"></p><p>这种方式的服务发现是两种模式的组合.第一种模式是自注册模式.服务实例调用服务注册的注册 API 来注册它的网络地址.它可能也提供了 <em>健康检查</em> URL, 将在第 11 章详细描述.健康检查 URL 是一个 API 端点用于服务注册周期性调用来验证服务实例的健康状态以及是否可以处理请求.服务注册可能也需要服务实例周期性地调用 “心跳(heartbeat)” API, 为了防止注册过期.</p><blockquote><p><strong>模式: 自注册</strong><br>服务实例注册它自己到服务注册表中.详见 <a href="http://microser-vices.io/patterns/self-registration.html" target="_blank" rel="noopener">http://microser-vices.io/patterns/self-registration.html</a>.</p></blockquote><p>第二个模式是客户端发现模式.当一个服务客户端想调用一个服务, 它查询服务注册表来获取服务实例的列表.为了改善性能, 客户端可以缓存服务实例.然后服务客户端使用负载均衡算法, 比如循环(round-robin)或随机, 来选择一个服务实例.接着发起一个请到选择的服务实例. </p><blockquote><p><strong>模式: 客户端发现</strong><br>服务客户端从服务注册表中获取可用服务实例的列表, 并进行负载均衡.详见 <a href="http://microservices.io/patterns/client-side-discovery.html" target="_blank" rel="noopener">http://microservices.io/patterns/client-side-discovery.html</a>.</p></blockquote><p>应用程序级别的服务发现已经被 Netflix 和 Pivotal 推广.Netflix 开发并开源了几个组件: Eureka, 一个高可用的服务发现组件, Eureka Java 客户端, 和 Ribbon, 一个支持 Eureka 客户端的复杂 HTTP 客户端.Pivotal 开发了 Spring Cloud, 一个基于 Spring 的框架, 使得使用 Netflix 组件非常容易.基于 Spring Cloud 的服务自动使用 Eureka 注册, 基于 Spring Cloud 的客户端自动使用 Eureka 用于服务发现.</p><p>应用程序级别的服务发现的一个好处是它处理了这样的场景: 当服务被部署到多个发布平台.假设, 比如你在 Kubernetes 只部署了某些服务, 将在第 12 章讨论, 剩余的服务运行在遗留的环境中.应用程序级别的服务发现使用 Eureka, 比如, 在两种环境中都可以工作, 而基于 Kubernetes 的服务发现只能在 Kubernetes 中工作.</p><p>应用程序级别的服务发现的一个缺点是你需要一个用于各种语言的服务发现库, 也可能是框架.Spring Cloud 只是帮助 Spring 开发者.如果你正在使用某些其他的 Java 框架或者非 JVM 语言, 比如 NodeJS 或 GoLang, 你必须找到某些其它的服务发现框架.应用程序级别的服务发现的另一个缺点是你负责设置和管理服务发现, 这容易让人分心.所以, 通常最好使用由部署基础设施提供的服务发现机制.</p><p><strong>应用平台提供的服务发现模式</strong></p><p>后面在第 12 章你将学习到许多现代部署平台比如 Docker 和 Kubernetes 已经内置了服务注册和服务发现机制.部署平台给每个服务提供了一个 DNS 名称, 一个虚拟 IP(VIP) 地址, 和一个解析到 VIP 地址的 DNS 名称.服务客户端发起一个请求到 DNS 名称/VIP, 部署平台自动路由请求到可用服务实例中的一个.所以, 服务注册, 服务发现, 和请求路由都由部署平台处理.图 3.6 展示了这是如何工作的.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.6-The%20platform%20is%20responsible%20for%20service%20registration,%20discovery,%20and%20request%20routing.png" alt="Figure 3.6-The platform is responsible for service registration, discovery, and request routing"></p><p>DNS 名称 <code>order-service</code>, 解析到虚拟 IP 地址 <code>10.1.3.4</code>.部署平台自动负载均衡请求到 <code>订单服务</code> 的三个实例中.</p><p>这种方式由两种模式组成:</p><ul><li><em>第三方注册模式</em>-与向服务注册中心注册自己的服务不同, 处理注册的是一个名为 <code>注册器(registrar)</code> 的第三方, 它通常是部署平台的一部分.</li><li><em>服务端发现模式</em>-与客户端查询服务注册表不同, 它发起请求到一个 DNS 名称, 解析到请求路由器, 请求路由器查询服务注册中心并负载平衡请求.</li></ul><blockquote><p><strong>模式: 第三方注册</strong><br>服务实例自动由第三方注册为服务.详见 <a href="http://microservices.io/patterns/3rd-party-registration.html" target="_blank" rel="noopener">http://microservices.io/patterns/3rd-party-registration.html</a>.</p></blockquote><blockquote><p><strong>模式: 服务端发现</strong><br>客户端发起一个请求到用于服务发现的路由器.详见 <a href="http://microservices.io/patterns/server-side-discovery.html" target="_blank" rel="noopener">http://microservices.io/patterns/server-side-discovery.html</a>.</p></blockquote><p>平台提供的服务发现的主要优点是服务发现的所有方面都由部署平台处理.服务和客户端都不包含任何服务发现代码.因此, 服务发现机制对所有服务和客户端都是可用的, 不管它们是用哪种语言或框架编写的.</p><p>平台提供的服务发现的一个缺点是它只支持使用平台部署的服务发现.比如, 在之前提到的当描述应用程序级别发现的时候, 基于 Kubernetes 的发现只能用于运行在 Kubernetes 上服务.尽管有这个限制, 我还是推荐尽可能使用由平台提供的服务发现.</p><p>目前我们已经看过了使用 REST 或 gRPC 的同步 IPC, 现在让我们看下另一种选择: 异步, 基于消息的通信.</p><h1 id="使用异步消息模式的通信"><a href="#使用异步消息模式的通信" class="headerlink" title="使用异步消息模式的通信"></a>使用异步消息模式的通信</h1><p>当使用消息的时候, 服务通过异步交换消息进行通信.基于消息的应用程序通常使用 <em>消息代理</em>, 作为服务之间的中介, 尽管另一种选择是使用无代理体系结构, 其中服务彼此直接通信.一个服务发起请求到一个服务通过发送消息.如果希望服务实例进行应答, 它将通过向客户端发送单独的消息来进行应答.因为通信是异步的, 客户端不会阻塞等待应答.相反, 客户端是在假定不会立即收到应答的情况下编写的.</p><blockquote><p><strong>模式: 消息</strong><br>客户端使用异步消息调用服务.详见 <a href="http://microservices.io/patterns/communication-style/messaging.html" target="_blank" rel="noopener">http://microservices.io/patterns/communication-style/messaging.html</a>.</p></blockquote><p>我从消息概览开始这一节.我将展示如何描述一个独立于消息技术的消息架构.接下来我比较了无代理和基于代理的架构, 以及描述了选择一个消息代理的标准.然后讨论了几个重要的主题, 包括在保持消息顺序的同时扩容消费者, 检测和废弃重复的消息, 发送和接收消息作为数据库事务的一部分.让我们从看看消息如何工作的开始.</p><h2 id="消息概览"><a href="#消息概览" class="headerlink" title="消息概览"></a>消息概览</h2><p>一个有用的消息模型在 Gregor Hohpe 和 Bobby Woolf 编写的《企业集成模式》(Addison-Wesley Professional, 2003) 一书中定义了.在这个模型中, 消息通过消息通道交换(message channels).发送者(应用程序或服务)将消息写入通道, 然后接收者(应用程序或服务)从通道中读取消息.我们先看下消息, 然后看下通道.</p><p><strong>关于消息</strong></p><p>一条消息由 header 和消息体组成(<a href="http://www.enterpriseintegrationpatterns.com/Message.html).header" target="_blank" rel="noopener">www.enterpriseintegrationpatterns.com/Message.html).header</a> 是一个 name-value 对的集合, 描述了要发送数据的元数据信息.除了消息发送者提供的 name-value 对, 消息头也包含了 name-value 对, 比如由发送者或消息的基础设施生成的唯一的消息 id, 和可选的返回地址, 指定了响应应该被写入的消息通道.消息体是要发送的数据, 文本或二进制格式.</p><p>有几种不同类型的消息:</p><ul><li><em>文档</em>-常见的只包含数据的消息.接收者决定如何解释它.命令的响应是文档消息的示例.</li><li><em>命令</em>-等同于 RPC 请求的消息.它指定了要调用的操作和参数.</li><li><em>事件</em>-表示在发送方中发生了值得注意的事情的消息.一个事件通常是一个域事件, 表示域对象, 如 <code>订单</code> 或 <code>客户</code> 的状态变更.</li></ul><p>本书中描述的微服务架构广泛地使用命令和事件.</p><p>现在让我们看下通道, 也就是服务通信的机制.</p><p><strong>关于消息通道</strong></p><p>如图 3.7 所示, 消息通过通道进行交换(<a href="http://www.enterpriseintegrationpatterns.com/MessageChannel.html).发送者的业务逻辑是调用*发送接口*" target="_blank" rel="noopener">www.enterpriseintegrationpatterns.com/MessageChannel.html).发送者的业务逻辑是调用*发送接口*</a>, 此接口封装了底层的通信机制.<em>发送接口</em>由<em>消息发送者</em>适配类实现, 它通过消息通道发送消息给接收者.<em>消息通道</em>是消息基础设施的抽象.接收者中的<em>消息处理器</em>适配类被调用用来处理消息.它调用由消费者的业务逻辑实现的<em>接收接口</em>.任意数量的发送者都可以向通道发送消息.类似地, 任意数量的接收者都可以从通道接收消息.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.7-Messages%20are%20exchanged%20over%20channels.png" alt="Figure 3.7-Messages are exchanged over channels"></p><p>有两种类型的通道: 点对点(<a href="http://www.enterpriseintegrationpatterns.com/PointToPointChannel.html" target="_blank" rel="noopener">www.enterpriseintegrationpatterns.com/PointToPointChannel.html</a>) 和发布订阅(<a href="http://www.enterpriseintegrationpatterns.com/PublishSubscribeChannel.html)" target="_blank" rel="noopener">www.enterpriseintegrationpatterns.com/PublishSubscribeChannel.html)</a>:</p><ul><li><em>点对点</em>通道向从通道读取的某个消费者发送消息.对于前面描述的一对一交互方式, 服务使用点对点通道.例如, 命令消息通常通过点对点通道发送.</li><li><em>发布订阅</em>通道发送每条消息给所有的消费者.服务使用发布订阅通道用于之前描述的一对多的交互方式.比如, 一条事件消息通过发布订阅通道发送.</li></ul><h2 id="使用消息进行交互的实现"><a href="#使用消息进行交互的实现" class="headerlink" title="使用消息进行交互的实现"></a>使用消息进行交互的实现</h2><p>消息的一个有价值的特性是足够灵活用来支持如 3.1.1 节描述的所有的交互方式.某些交互方式直接由消息实现.其它的必须在消息的基础之上实现.</p><p>让我们看下如何实现每种交互方式, 从请求/响应和异步请求/响应开始.</p><p><strong>实现请求/响应和异步请求/响应</strong></p><p>当客户端和服务使用请求/响应或异步请求/响应方式进行交互的时候, 客户端发送请求, 服务发回应答.这两种交互方式的差异是使用请求/响应方式, 客户端期望服务立即进行响应, 而异步请求/响应则没有这样的期望.消息本质上就是异步的, 所以只提供了异步请求/响应.但是一个客户端可以阻塞直到收到应答.</p><p>客户端和服务通过交换一个消息对来实现异步请求/响应的交互方式.如图 3.8 所示, 客户端发送一条命令消息给一个服务拥有的点对点消息通道, 指定了要执行的操作和参数.服务处理请求然后发送一个应答消息给客户端拥有的点对点通道, 其中包含了结果. </p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.8-Implementing%20asynchronous%20request-response.png" alt="Figure 3.8-Implementing asynchronous request-response"></p><p>客户端必须告诉服务将应答消息发送到哪里, 服务必须给请求发送应答消息.幸运地是, 解决这两个问题不是很难.客户端发送一条包含了<em>应答通道</em> header 的命令消息.服务器将包含了和<em>消息标识符</em>一样的<em>关联 id(correlation id)</em>写入到应答通道中.客户端使用<em>关联 id</em> 将应答消息与请求匹配.</p><p>因为客户端和服务使用消息通信, 所以交互方式本质上是异步的.理论上来说, 消息客户端可以阻塞直到它收到应答, 但是在实践中客户端将异步处理应答.此外, 应答通常由客户端实例的任意一个来处理.</p><p><strong>实现单向通知</strong></p><p>使用异步消息实现单向通知非常简单.客户端将消息, 通常是命令消息, 发送到服务拥有的点对点通道.服务订阅通道并处理消息.它不会发送应答.</p><p><strong>实现发布/订阅</strong></p><p>消息已经内建了支持发布/订阅方式的交互.客户端发布消息到由多个消费者读取的发布-订阅通道.如在第 4 章和第 5 章描述的, 服务使用发布/订阅来发布域事件, 它代表了域对象的变更.发布域事件的服务拥有一个发布-订阅通道, 它的名字从域对象类(domain class)衍生而来.比如, <code>订单服务(Order Service)</code> 发布 <code>订单(Order)</code> 事件到 <code>订单</code>通道(Order Channel), <code>配送服务(Delivery Service)</code> 发布 <code>配送(Delivery)</code> 事件到 <code>配送(Delivery)</code>通道(Delivery Channel).对特定域对象事件感兴趣的服务只会订阅相应的通道.</p><p><strong>实现发布/异步响应</strong></p><p>发布/异步响应交互方式是一种高级的交互方式, 它通过组合发布/订阅和请求/响应的元素实现.客户端发布指定了应答通道 header 的消息到发布/订阅通道.消费者将包含了相关联 id 的应答消息写入应答通道.客户端通过使用关联 id 来收集响应, 以将应答消息与请求匹配.</p><p>在你的应用程序中的有异步 API 的每个服务都将使用这些实现技术中的一种或多种.拥有异步 API 用来调用操作的服务将有一个消息通道用于请求.类似地, 发布事件的服务将发布它们到事件消息通道.</p><p>如在 3.1.2 节描述的, 对于一个服务来说, 编写 API 规范是很重要的.接着让我们看看编写异步 API 该怎么做.</p><h2 id="为基于消息服务的-API-创建-API-规范"><a href="#为基于消息服务的-API-创建-API-规范" class="headerlink" title="为基于消息服务的 API 创建 API 规范"></a>为基于消息服务的 API 创建 API 规范</h2><p>对于服务异步 API 的规范, 必须如图 3.9 所示, 指定消息通道的名称, 通过每个通道交换的消息字节, 和它们的格式.你也必须使用一个标准来描述消息的格式, 比如 JSON, XML 或 Protobuf.但是不像 REST 和 Open API, 没有一个被广泛采用的标准来文档化通道和消息类型.相反, 你需要编写一份非正式的文档.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.9-A%20service%20asynchronous%20API%20consists.png" alt="Figure 3.9-A service asynchronous API consists"></p><p>服务异步 API 由操作, 被调用的客户端和由服务发布的事件组成.它们以不同的方式被文档化.让我们看下每一个, 从操作开始.</p><p><strong>文档化异步操作</strong></p><p>服务的操作可以使用两种不同的交互方式被调用:</p><ul><li><em>请求/异步响应方式</em> API-这由服务命令消息通道, 类型和能被服务接受的命令消息字节的格式, 以及由服务发送的应答消息的类型和格式组成.</li><li><em>单向通知方式</em> API-这由服务命令消息通道, 能被服务接受的命令消息字节的格式组成.</li></ul><p>服务可以使用相同的请求通道同时用于异步请求/应答和单向通知.</p><p><strong>文档化发布事件</strong></p><p>服务也可以使用发布/订阅的交互方式发布事件.这种方式的 API 的规范由事件通道和被服务发布到通道的事件消息的类型和格式组成.</p><p>消息和消息的通道模型是一个很好的抽象, 也是设计服务异步 API 的一种好方法.但是为了实现服务, 你需要选择消息技术, 并确定如何使用其功能实现你的设计.让我们来看看其中包含了什么.</p><h2 id="使用消息代理"><a href="#使用消息代理" class="headerlink" title="使用消息代理"></a>使用消息代理</h2><p>基于消息的应用通常使用消息代理, 一种基础设施服务, 服务通过它进行通信.但是基于代理的架构不是消息架构的唯一方式.你也可以使用基于无代理的消息架构, 服务直接与另一个服务通信.这两种方式如图 3.10, 有不同的权衡, 但是通常基于代理的架构是更好的方式.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.10-Brokerless%20architecture%20and%20broker-based%20architecture.png" alt="Figure 3.10-Brokerless architecture and broker-based architecture"></p><p>本书关注于基于代理的架构, 但是快速查看一下无代理架构也是值得的, 因为也许你可能发现在某些常见下这种方式是有用的.</p><p><strong>无代理架构</strong></p><p>在无代理架构中, 服务可以直接交换消息.ZeroMQ(<a href="http://zeromq.org)是一个流行的无代理消息技术.它既一个规范" target="_blank" rel="noopener">http://zeromq.org)是一个流行的无代理消息技术.它既一个规范</a>, 也是一组不同语言的库.它支持多种传输, 包括 TCP, 类 UNIX 域套接字(UNIX-style domain sockets) 和组播.</p><p>无代理架构有几个优点:</p><ul><li>轻量级的网络传输和更小的延迟, 因为消息直接从发送者传输到接收者, 而不用从发送者到消息代理, 再从消息代理到接收者.</li><li>消除消息代理成为性能瓶颈或单点故障的可能性.</li><li>减少了操作复杂性, 因为不需要设置和维护消息代理.</li></ul><p>尽管这些好处看起来很吸引人, 但无代理消息也有明显的缺点:</p><ul><li>服务需要知道其他服务的地址, 所以必须使用在 3.2.4 节中描述的发现机制.</li><li>降低了可用性, 因为消息的发送者和接收者都必须是可用的, 当消息进行交换的时候.</li><li><p>实现机制, 如有保证的交付, 更具挑战性.</p><p>事实上, 其中的一些缺点, 比如降低可用性和服务发现的需求, 在使用同步, 请求/响应的时候同样会有.</p><p>因为这些限制, 大部分企业应用使用基于代理的架构.让我们看下它是如何工作的.</p></li></ul><p><strong>基于代理架构的概览</strong></p><p>消息代理是所有消息流经的中介.发送者将消息写入消息代理, 消息代理将它发送给接收者.使用消息代理的一个好处是发送者不需要知道消费者的网络地址.另一个好处是消息代理缓冲消息直到消费者能够处理它们.</p><p>有多种消息代理可供选择.流行的开源消息代理的例子包括如下:</p><ul><li>ActiveMQ (<a href="http://activemq.apache.org" target="_blank" rel="noopener">http://activemq.apache.org</a>)</li><li>RabbitMQ (<a href="https://www.rabbitmq.com" target="_blank" rel="noopener">https://www.rabbitmq.com</a>)</li><li>Apache Kafka (<a href="http://kafka.apache.org" target="_blank" rel="noopener">http://kafka.apache.org</a>)</li></ul><p>也有一些基于云的消息分为, 比如 AWS Kinesis(<a href="https://aws.amazon.com/kinesis/" target="_blank" rel="noopener">https://aws.amazon.com/kinesis/</a>) 和 AWS SQS(<a href="https://aws.amazon.com/sqs/)" target="_blank" rel="noopener">https://aws.amazon.com/sqs/)</a>.</p><p>当选择一个消息代理的时候, 有几个因素需要考虑, 比如以下:</p><ul><li><em>支持编程语言</em>-你可能应该选择一种支持多种编程语言的消息服务.</li><li><em>支持消息标准</em>-消息代理是否支持一些标准, 比如 AMQP 和 STOMP, 或者是它专有的?</li><li><em>消息顺序</em>-消息代理是否保持消息的顺序?</li><li><em>投递保证</em>-消息代理提供什么样的投递保证?</li><li><em>持久化</em>-消息是否持久化到磁盘上, 并能够在代理崩溃时存活?</li><li><em>持久性</em>-如果一个消费者重新连接到消息代理, 它是否可以接收到当它断开连接的时候发送到代理的消息.</li><li><em>扩展性</em>-消息代理如何扩容?</li><li><em>延迟</em>-端到端的延迟是多少?</li><li><em>消费者竞争</em>-消息代理是否支持消费者竞争?</li></ul><p>每种代理有不同的权衡.比如, 一个低延迟的代理可能不会保持顺序, 不保证消息送达, 并可能只在内存中存储消息.保证送到和可靠存储消息到磁盘上的消息代理可能有较高的延迟.哪一种代理最好依赖于你的应用程序需求.甚至可能应用程序的不同部分有不同的消息需求.</p><p>不过, 消息顺序和可扩展性可能很重要.现在让我们看下如何使用消息代理实现消息通道.</p><p><strong>使用消息代理实现消息通道</strong></p><p>每个消息代理以不同的方式实现消息通道概念.如表 3.2 所示, JMS 消息代理, 比如 ActiveMQ 有队列和主题(topics).基于 AMQP 的消息代理比如 RabbitMQ 有交换机和队列.Apache Kafka 有主题, AWS Kinesis 有流(streams), AWS SQS 有队列.此外, 由些消息代理提供了比本章描述的消息和通道抽象更灵活的消息.</p><p><strong>表 3.2 每个消息代理以不同的方式实现消息通道概念</strong></p><table><thead><tr><th>消息代理</th><th>点对点通道</th><th>发布-订阅通道</th></tr></thead><tbody><tr><td>JMS</td><td>队列</td><td>主题</td></tr><tr><td>Apache Kafka</td><td>主题</td><td>主题</td></tr><tr><td>基于 AMQP 的代理, 比如 RabbitMQ</td><td>交换机 + 队列</td><td>广播交换机(Fanout exchange) 和配个消费者一个队列</td></tr><tr><td>AWS Kinesis</td><td>流</td><td>流</td></tr><tr><td>AWS SQS</td><td>队列</td><td>-</td></tr></tbody></table><p>在这里描述的几乎所有的消息代理都支持点对点和发布-订阅两种通道.其中一个例外是 AWS SQS, 它只支持点对点通道.</p><p>现在让我们看看基于代理消息的缺点和优点.</p><p><strong>基于代理消息的缺点和优点</strong></p><p>使用代理的消息有很多好处:</p><ul><li><em>松耦合</em>-客户端通过简单地发送消息到对应的通道发出请求.客户端完全不关心服务实例.它不需要使用发现机制来寻找服务实例的地址.</li><li><em>消息缓冲</em>-消息代理缓冲消息直到它们可以被处理.使用同步请求/响应协议, 比如 HTTP, 客户端和服务两者在交换期间都必须是可用的.而使用消息, 消息将排队等待, 直到它们可以被消费者处理.这意味着, 比如, 在线商品可以接受消费者的订单, 即使订单履行(order-fulfillment)系统缓慢或不可用.消息将简单地排队等待直到它们可以被处理.</li><li><em>灵活的通信方式</em>-消息支持之前描述的所有交互方式.</li><li><em>显示的进程间通信</em>-基于 RPC 的机制尝试将调用远程的服务当做是调用本地服务.但是由于物理定律和局部故障的可能性, 实际上它们是完全不同的.消息使得这些差异非常明显, 所以开发者不会被引入一种错误的安全感.</li></ul><p>使用消息也有一些缺点:</p><ul><li><em>潜在的性能瓶颈</em>-消息代理的一个风险可能是性能瓶颈.幸运地是, 许多现代化的消息代理被设计成高度可扩展的.</li><li><em>潜在的单点故障</em>-消息代理必须具有高可用性, 否则将影响系统的可靠性.幸运的是, 大多数现代代理被设计成高度可用的.</li><li><em>额外的操作复杂性</em>-消息系统是另一个系统组件, 它必须被安装、配置和操作.</li></ul><p>让我们看看你可能面对的一些设计问题.</p><h2 id="竞争的接收者和消息排序"><a href="#竞争的接收者和消息排序" class="headerlink" title="竞争的接收者和消息排序"></a>竞争的接收者和消息排序</h2><p>有一个挑战就是如何扩展消息接收者而同时保持消息顺序.这是一个常见的需求, 有多个服务实例为了并发处理消息.进一步来说, 即使单个服务也可能使用现成来并发地处理多条消息.使用多线程, 服务实例并发处理消息将增加应用程序的吞吐量.但是并发处理消息的调整是保证每条消息都按顺序处理一次.</p><p>比如, 假设有三个服务实例从相同的点对点通道读取, 并且一个发送者顺序发布 <code>消息创建(Order Created)</code>、<code>消息更新(Order Updated)</code> 和 <code>订单取消(Order Cancelled)</code> 事件消息.简单的消息实现可以将每个消息并发地传递到不同的接收方.因为网络问题或垃圾收集造成的延迟, 消息可能被无序处理, 这可能导致奇怪的行为.理论上说, 服务实例可以在另一个服务处理 <code>消息创建(Order Created)</code> 消息之前处理 <code>订单取消(Order Cancelled)</code> 消息.</p><p>一个被现代化消息代理, 比如 Apache Kafka 和 AWS Kinesis, 使用的通用解决方案, 就是使用<em>分片(分区)</em>的通道.图 3.11 展示了这是如何工作的, 这个方案由三部分:</p><ul><li>分配通道由两个或更多的分片组成, 每一个都像一个通道.</li><li>发送者在消息头中指定一个分片 key, 通常是一个任意的字符串或字节序列.消息代理使用分片 key 来分配消息到特定的分片/分区.比如, 它可以通过计算分片 key 的哈希值, 对分片数量取模, 来选择分片.</li><li>消息代理组将接收者的多个实例分组在一起, 然后将它们作为逻辑上的接收者.比如, Apache Kafka, 使用<em>消费者组(consumer group)</em>的术语.消息代理将每个分片分配给单个接收者.当接收者启动和停止的时候重新分片.</li></ul><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.11-Using%20a%20sharded%20message%20channel.png" alt="Figure 3.11-Using a sharded message channel"></p><p>在这个例子中, 每个订单事件消息有一个 orderId 座位它的分片 key.一个特定订单的每个事件被发布到相同的分片, 被单个消息费读取.因此, 可以保证消息被顺序处理.</p><h2 id="处理重复消息"><a href="#处理重复消息" class="headerlink" title="处理重复消息"></a>处理重复消息</h2><p>使用消息时必须解决的另一个挑战是处理重复的消息.在理想情况下, 消息代理应该只交付每条消息一次, 但是确保消息只传递一次通常成本太高.相反, 大多数消息代理承诺交付消息 <code>至少一次(at least)</code>.</p><p>当系统正常工作时, 保证只交付一次的消息代理将只交付每条消息一次.但是客户端、网络或消息代理的故障可能导致消息被交付多次.假设客户端在处理消息并更新其数据库之后崩溃-但在确认消息之前崩溃.当它重启的时候消息代理将再次交付未确认的消息到那个客户端, 或者到另一个客户端的副本.</p><p>理想情况下, 你应该使用保持顺序的消息代理当重新交付消息的时候.假设客户端处理一个 <code>订单创建(Order Created)</code> 的事件, 然后为相同的订单处理一个 <code>订单取消(Order Cancelled)</code> 的事件, 但是不知道为什么 <code>订单创建(Order Created)</code> 事件没有被确认.消息代理应该重新交付 <code>订单创建(Order Created)</code> 和 <code>订单取消(Order Cancelled)</code> 的事件.如果它只重新交付 <code>订单创建(Order Created)</code>, 客户端可能会取消订单.</p><p>有两种不同的方式来处理重复消息:</p><ul><li>编写幂等消息处理程序.</li><li>追踪消息和丢弃重复消息.</li></ul><p>让我们看下每种方式.</p><p><strong>编写幂等消息处理程序</strong></p><p>如果处理消息的应用程序逻辑是<em>幂等</em>的, 那么重复的消息将没有什么影响.如果使用相同的输入值调用应用程序逻辑多次而没有额外的影响那么它就是幂等的.比如, 取消一个已经取消的订单是一个幂等操作.使用客户端提供的 ID创建订单也是这样.如果消息代理再重新交付消息的时候保留了顺序, 那么幂等的消息处理程序可以被安全地执行多次.</p><p>不幸地是, 应用程序逻辑通常不是幂等的.或者你可能使用了重新交付消息的时候不保留顺序的消代理.重复或者无序的消息可能引起 bugs.为了解决这个问题, 你必须编写追踪消息并丢弃重复消息的消息处理程序.</p><p><strong>追踪消息和丢弃重复消息</strong></p><p>比如, 考虑一个授权客户信用卡的消息处理程序.每笔订单必须授权一次.这个应用程序的逻辑的例子每次调用的时候有不同的影响.如果重复消息引起消息处理程序执行了这个逻辑多次, 应用程序的行为将不正确.这种应用程序的消息处理程序逻辑必须是幂等的通过检测和丢弃重复的消息.</p><p>一个简单的方案是消息消费者使用 <code>消息 id</code> 追踪它已经处理过的消息并丢弃重复的.比如, 它可以存储它消费过的每条消息的 <code>消息 id</code> 到数据库表中.图 3.12 展示了如何使用一张专用表.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.12-Process%20duplicate%20messages.png" alt="Figure 3.12-Process duplicate messages"></p><p>当消费者处理一条消息, 它记录 <code>消息 id</code> 到数据库表中作为创建和更新业务实体的一部分.在这个例子中, 消费者插入一条包含 <code>消息 id</code> 的记录到 <code>PROCESSED_MESSAGES</code> 表中.如果一条消息重复了, <code>INSERT</code> 将失败并且消费者可以丢弃消息.</p><p>另一种消息处理程序方式是记录 <code>消息 ids</code> 到应用程序表中而不是一张专用表.这种方式特别有用当使用有限制事务模型的 NoSQL 数据库, 因此它不支持作为数据库事务的一部分更新两张表.第 7 章展示了这种方式的一个例子.</p><h2 id="事务消息"><a href="#事务消息" class="headerlink" title="事务消息"></a>事务消息</h2><p>服务通常需要将消息发布为更新数据库的事务的一部分.比如, 在本书中, 你可以看到每当创建或更新业务实体时发布域事件的服务示例.数据的更新和消息的发送必须在一个事务中进行.另外, 比如在发送消息之前, 一个服务可能更新数据库然后崩溃了.如果服务不支持原子性地执行者两个操作, 故障可能会让系统处于不一致的状态.</p><p>传统的方式是使用跨数据库和消息代理的分布式事务.但是如你将在第 4 章看到的, 分布式事务不是现代应用程序的一个好的选择.此外, 许多现代消息代理比如 Apache Kafka 不支持分布式事务.</p><p>因此, 应用程序必须使用不同的机制来可靠地发布消息.让我们看看这是如何工作的.</p><p><strong>使用数据库表作为消息队列</strong></p><p>让我们假设你的应用程序使用关系型数据库.一种简单方法是应用事务性发件箱(Transactional outbox)模式可靠发布消息.这种模式使用数据库表作为临时的消息队列.如图 3.13 所示, 发送消息的服务有一个 <code>OUTBOX</code> 数据库表.作为创建、更新和删除业务对象的数据库事务的一部分, 服务通过插入它们到 <code>OUTBOX</code> 表来发送消息.原子性将得到保证因为这是本地的 ACID 事务.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.13-A%20service%20reliably%20publishes%20a%20message.png" alt="Figure 3.13-A service reliably publishes a message"></p><p><code>OUTBOX</code> 表作为一个临时的消息队列.MessageRelay 是一个读取 <code>OUTBOX</code> 表的组件, 并发布消息到消息代理.</p><blockquote><p><strong>模式: 事务收件箱</strong></p><p>通过保存一个事件或消息到 <code>OUTBOX</code> 数据库表中, 将其作为数据库事务的一部分发布.想见 <a href="http://microservices.io/patterns/data/transactional-out-box.html" target="_blank" rel="noopener">http://microservices.io/patterns/data/transactional-out-box.html</a>.</p></blockquote><p>你可以使用某些 NoSQL 数据库来实施类似的方案.作为记录存储在数据库中的每个业务实体都有一个属性, 该属性是需要发布的消息列表.当一个事务更新数据库中的一个实体, 它将消息追加到那个列表中.这是原子的因为它使用单个数据操作完成.然而, 挑战在于有效地找到具有事件的业务实体并发布它们.</p><p>有两种不同的方式来把消息从数据库移动到消息代理中.我们将看下每一种方式.</p><p><strong>通过使用轮询发布器模式(POLLING PUBLISHER PATTERN)来发布事件</strong></p><p>如果应用程序使用关系型数据库, 发布消息的一种非常简单的方式就是将消息插入到 <code>OUTBOX</code> 表中, 然后 <code>MessageRelay</code> 会轮询表中未发布的消息.它周期性地查询表:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> * <span class="keyword">FROM</span> OUTBOX ORDERED <span class="keyword">BY</span> ... <span class="keyword">ASC</span></span><br></pre></td></tr></table></figure><p>接着, <code>MessageRelay</code> 发布这些消息到消息代理, 并将一条消息发送到其目标消息通道.最后, 它将这些消息从 <code>OUTBOX</code> 表中删除.</p><figure class="highlight sql"><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"><span class="keyword">BEGIN</span></span><br><span class="line"> <span class="keyword">DELETE</span> <span class="keyword">FROM</span> OUTBOX <span class="keyword">WHERE</span> <span class="keyword">ID</span> <span class="keyword">in</span> (....)</span><br><span class="line"><span class="keyword">COMMIT</span></span><br></pre></td></tr></table></figure><blockquote><p>模式: 长轮询发布器</p><p>通过轮询数据中的 outbox 表来发布消息.详见 <a href="http://microservices.io/patterns/data/polling-publisher.html" target="_blank" rel="noopener">http://microservices.io/patterns/data/polling-publisher.html</a>.</p></blockquote><p>轮询数据库是一种简单的方法, 在低规模下工作得相当好.缺点是频繁轮询数据库可能会很昂贵.此外, 是否可以将此方法用于 NoSQL 数据库取决于其查询能力.这是因为应用程序必须查询业务实体, 而不是查询 <code>OUTBOX</code> 表, 这可能有效, 也可能无效.由于这些缺点和限制, 使用跟踪数据库事务日志的更复杂和性能更好的方法通常更好, 在某些情况下, 这是必要的.</p><p><strong>通过应用事务日志追踪(TRANSACTION LOG TAILING)模式来发布事件</strong></p><p>一个复杂的解决方案是 <code>MessageRelay</code> 跟踪数据库事务日志(也称为 commit log).应用程序的每次提交更新都作为数据事务日志的一条记录.事务日志挖掘器可以读取事务日志, 并将每个更改作为消息发布到消息代理.图 3.14 展示了这种方式是如何工作的.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.14-Service%20publishing%20events%20by%20applying%20the%20transaction%20log%20tailing%20pattern.png" alt="Figure 3.14-Service publishing events by applying the transaction log tailing pattern"></p><p><code>事务日志挖掘器(Transaction Log Miner)</code> 读取事务日志条目.它将与插入的消息对应的每个相关日志条目转换为消息, 并将该消息发布到消息代理.这种方式可以用来发布写入到 RDBMS 中的 <code>OUTBOX</code> 表的消息或添加到 NoSQL 数据库记录的消息.</p><blockquote><p>模式: 事务日志追踪(Transaction log tailing)</p><p>通过跟踪事务日志发布对数据库所做的更改.详见 <a href="http://microservices.io/patterns/data/transaction-log-tailing.html" target="_blank" rel="noopener">http://microservices.io/patterns/data/transaction-log-tailing.html</a>.</p></blockquote><p>以下是一些使用这种方式的例子:</p><ul><li><em>Debezium</em>(<a href="http://debezium.io/)-一个发布数据库变更到" target="_blank" rel="noopener">http://debezium.io/)-一个发布数据库变更到</a> Apache Kafka 消息代理的开源项目</li><li><em>LinkedIn Databus</em>(<a href="https://github.com/linkedin/databus)-一个挖掘" target="_blank" rel="noopener">https://github.com/linkedin/databus)-一个挖掘</a> Oracle 事务日志并并将更改作为事件发布的开源项目.LinkedIn 使用 Databus 将各种衍生数据存储与记录系统同步.</li><li><em>DynamoDB streams</em>(<a href="http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html)-DynamoDB" target="_blank" rel="noopener">http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html)-DynamoDB</a> streams 包含在过去 24 小时内对 DynamoDB 表中的项所做的更改(创建、更新和删除)的时间顺序.应用程序可以从流中读取这些更改, 例如, 将它们作为事件发布.</li><li><em>Eventuate Tram</em>(<a href="https://github.com/eventuate-tram/eventuate-tram-core)-作者自己的开源事务消息库" target="_blank" rel="noopener">https://github.com/eventuate-tram/eventuate-tram-core)-作者自己的开源事务消息库</a>, 它使用 MySQL binlog 协议、Postgres WAL 或轮询来读取对 <code>OUTBOX</code> 表所做的更改并将其发布到 Apache Kafka.</li></ul><p>尽管这种方法很模糊, 但它的效果非常好.挑战在于实现它需要一些开发工作.例如, 你可以编写调用特定于数据库 API 的底层代码.或者, 你可以使用开源框架, 如Debezium, 它发布应用程序对 MySQL、Postgres、MongoDB 的变更到 Apache Kafka.使用 Debezium 的缺点是, 它的重点是捕获数据库级别的更改, 用于发送和接收消息的 API 超出了它的范围.这就是为什么我创建了 Eventuate Tram 框架, 它提供消息 API 以及事务跟踪和轮询.</p><h2 id="用于消息的库和框架"><a href="#用于消息的库和框架" class="headerlink" title="用于消息的库和框架"></a>用于消息的库和框架</h2><p>服务需要使用库来发送和接收消息.一种方式是使用消息代理客户端库, 虽然直接使用这样的库有几个问题:</p><ul><li>客户端库将发布消息到消息代理 API 的业务逻辑耦合在一起.</li><li>消息代理客户端库通常是低层次的, 需要编写很多行代码来发送或接收消息.作为一个开发者, 你不希望重复编写样板代码.同样, 作为本书的作者我不希望示例代码与低层次的重复代码混杂在一起.</li><li>客户端库通常只提供发送和接收消息的基本机制, 不支持高级别的交互方式.</li></ul><p>一种更好的方式是使用高级别的库或框架, 它们隐藏了低级别的细节, 并且支持高级别的交互方式.为简单起见, 本书的示例使用了我的 Eventuate Tram 框架.它有简单, 易于理解的 API, 隐藏了使用消息代理的复杂性.除了发送和接收消息的 API, Eventuate Tram 也支持高级别的交互方式, 比如异步请求/响应和域事件发布.</p><blockquote><p><strong>什么!? 为什么用 Eventuate 框架?</strong></p><p>本书的代码示例使用了我开发的开源 Eventuate 框架, 它可以用于事务消息, 事件溯源(event sourcing) 和 sagas.我选择使用我的框架, 是因为与依赖注入和 Spring 框架不同,对于微服务架构所需的许多特性, 目前还没有得到广泛采用的框架.不使用 Eventuate Tram 框架, 许多示例将需要直接使用低级别的消息 APIs, 使它们更加复杂, 模糊了重要的概念.或者他们会使用一个没有被广泛采用的框架, 这也会招致批评.相反, 示例使用了 Eventuate Tram 框架, 它有简单、易于理解的 API, 隐藏了实现细节.你可以在你的应用程序中使用这些框架.或者, 你也可以学习 Eventuate Tram 框架, 并自己重新实现这些概念.</p></blockquote><p>Eventuate Tram 也实现了两个重要的机制:</p><ul><li><em>事务消息</em>-它将消息作为数据库事务的一部分进行发布.</li><li><em>重复消息检测</em>-Eventuate Tram 消息消费者检测和丢弃重复消息, 这对于确保消费者只处理一次消息是至关重要的, 如 3.3.6 节描述的.</li></ul><p>让我们看下 Eventuate Tram APIs.</p><p><strong>基础的消息</strong></p><p>基础的消息 API 有两个 Java 接口组成: <code>MessageProducer</code> 和 <code>MessageConsumer</code>.生产者服务使用 <code>MessageProducer</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></pre></td><td class="code"><pre><span class="line">MessageProducer messageProducer = ...;</span><br><span class="line">String channel = ...;</span><br><span class="line">String payload = ...;</span><br><span class="line">messageProducer.send(destination, MessageBuilder.withPayload(payload).build())</span><br></pre></td></tr></table></figure><p>消费者方服务使用 <code>MessageConsumer</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></pre></td><td class="code"><pre><span class="line">MessageConsumer messageConsumer;</span><br><span class="line">messageConsumer.subscribe(subscriberId, Collections.singleton(destination), </span><br><span class="line">message -> { ... })</span><br></pre></td></tr></table></figure><p><code>MessageProducer</code> 和 <code>MessageConsumer</code> 是用于异步请求/响应和域事件发布的高级 APIs 的基础.</p><p><strong>域事件发布</strong></p><p>Eventuate Tram 有 APIs 用于发布和消费域事件.第 5 章解释了域事件是聚合(业务对象)事件, 在创建、更新或删除时发出.服务使用 <code>DomainEventPublisher</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></pre></td><td class="code"><pre><span class="line">DomainEventPublisher domainEventPublisher;</span><br><span class="line"></span><br><span class="line">String accountId = ...;</span><br><span class="line"></span><br><span class="line">DomainEvent domainEvent = <span class="keyword">new</span> AccountDebited(...);</span><br><span class="line"></span><br><span class="line">domainEventPublisher.publish(<span class="string">"Account"</span>, accountId, Collections.singletonList(domainEvent));</span><br></pre></td></tr></table></figure><p>服务使用 <code>DomainEventDispatcher</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></pre></td><td class="code"><pre><span class="line">DomainEventHandlers domainEventHandlers = DomainEventHandlersBuilder</span><br><span class="line">.forAggregateType("Order")</span><br><span class="line">.onEvent(AccountDebited.class, domainEvent -> { ... })</span><br><span class="line">.build();</span><br><span class="line"></span><br><span class="line">new DomainEventDispatcher("eventDispatcherId", domainEventHandlers, messageConsumer);</span><br></pre></td></tr></table></figure><p>事件不是 Eventuate Tram 支持的唯一的高级别消息模式.它也支持基于命令/应答的消息.</p><p><strong>基于命令/应答的消息</strong></p><p>客户端可以使用 <code>CommandProducer</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></pre></td><td class="code"><pre><span class="line">CommandProducer commandProducer = ...;</span><br><span class="line"></span><br><span class="line">Map<String, String> extraMessageHeaders = Collections.emptyMap();</span><br><span class="line"></span><br><span class="line">String commandId = commandProducer.send(<span class="string">"CustomerCommandChannel"</span>, </span><br><span class="line"><span class="keyword">new</span> DoSomethingCommand(), </span><br><span class="line"><span class="string">"ReplyToChannel"</span>, </span><br><span class="line">extraMessageHeaders);</span><br></pre></td></tr></table></figure><p>服务使用 <code>CommandDispatcher</code> 类来消费命令消息.<code>CommandDispatcher</code> 使用 <code>MessageConsumer</code> 接口来订阅指定的事件.它派发每条命令消息到对应的 handler 方法.下面是一个示例:</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></pre></td><td class="code"><pre><span class="line">CommandHandlers commandHandlers = CommandHandlersBuilder</span><br><span class="line">.fromChannel(commandChannel)</span><br><span class="line">.onMessage(DoSomethingCommand.class, (command) -> { ... ; <span class="keyword">return</span> withSuccess(); })</span><br><span class="line">.build();</span><br><span class="line"></span><br><span class="line">CommandDispatcher dispatcher = <span class="keyword">new</span> CommandDispatcher(<span class="string">"subscribeId"</span>, </span><br><span class="line">commandHandlers, messageConsumer, messageProducer);</span><br></pre></td></tr></table></figure><p>在本书中, 您将看到使用这些 API 发送和接收消息的代码示例.</p><p>如你所见, Eventuate Tram 框架为 Java 应用程序实现了事务消息.它提供了低级别的 API 用来事务性的发送和接收消息.它也提供了高级别的 APIs 用来发布、消费域事件和处理命令.</p><p>然我们看一个使用异步消息来提高可用性的服务设计方式.</p><h1 id="使用异步消息提高可用性"><a href="#使用异步消息提高可用性" class="headerlink" title="使用异步消息提高可用性"></a>使用异步消息提高可用性</h1><p>如你所见, 不同的 IPC 机制有不同的权衡.其中一个特殊的权衡是你选择的 IPC 机制是如何影响可用性的.在这节, 你将学习到, 作为请求处理的一部分, 与其他服务的同步通信会降低应用程序的可用性.因此, 无论何时你应该使用异步消息来设计你的服务.</p><p>让我们首先看下同步通信的问题, 以及它是如何影响可用性的.</p><h2 id="同步通信降低可用性"><a href="#同步通信降低可用性" class="headerlink" title="同步通信降低可用性"></a>同步通信降低可用性</h2><p>REST 是一种非常流行的 IPC 机制.你可能想将其用于服务间通信.不过 REST 的问题是它是一个同步协议: HTTP 客户端必须等待服务发送响应.无论何时服务使用同步通信, 应用程序的可用性都将降低.</p><p>为了了解为什么, 考虑图 3.15 的场景.<code>订单服务</code> 有一个 REST API 用于创建一个 <code>订单</code>.它调用 <code>消费者服务</code> 和 <code>餐厅服务</code> 来验证 <code>订单</code>.这两个服务都有 REST APIs.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.15-Synchronous%20communication%20reduces%20availability.png" alt="Figure 3.15-Synchronous communication reduces availability"></p><p>创建订单的步骤如下:</p><ul><li>客户端发出 HTTP <code>POST /orders</code> 请求到 <code>订单服务</code>.</li><li><code>订单服务</code> 通过发出 HTTP <code>GET /consumers/id</code> 请求到 <code>消费者服务</code> 来查询消费者信息.</li><li><code>订单服务</code> 通过发出 HTTP <code>GET /restaurant/id</code> 请求到 <code>餐厅服务</code> 来查餐厅信息.</li><li><code>订单接收服务(Order Taking)</code> 使用消费者和餐厅信息来验证请求.</li><li><code>订单接收服务</code> 创建订单.</li><li><code>订单接收服务</code> 发送 HTTP 响应到客户端.</li></ul><p>因为服务使用 HTTP, 所以它们对于 FTFO 应用程序来说必须同时可用来处理 <code>创建订单(CreateOrder)</code> 请求.如果三个服务中的其中一个宕机, FTGO 应用程序将不能创建订单.从数学上讲, 系统操作的可用性是该操作调用的服务的可用性的产物.如果 <code>订单服务</code> 和它调用的两个服务是 99.5 可用, 那么整体的可用性是 99.5% 3 = 98.5%, 这要少得多.参与处理请求的每个额外服务进一步降低了可用性.</p><p>这个问题不是特定于基于 REST 的通信.当服务只能在接收到来自另一个服务的响应后才对其客户端进行响应时, 可用性就会降低.即使服务通过异步消息使用请求/响应交互方式进行通信, 也存在此问题.比如, <code>订单服务</code> 的可用性将降低如果它通过消息代理发送消息到 <code>消费者服务</code> 然后等待响应.</p><p>如果你想最大化可用性, 那你必须最小化同步通信的数量.我们来看看怎么做.</p><h2 id="消除同步交互"><a href="#消除同步交互" class="headerlink" title="消除同步交互"></a>消除同步交互</h2><p>在处理同步请求时, 有几种不同的方法可以减少与其他服务的同步通信.一种解决方案是通过定义只有异步 API 的服务来完全避免这个问题.但这并不总是可能的.例如, 公共 API 通常是 RESTful 的.因此, 服务有时需要具有同步 API.</p><p>幸运地是, 有几种方式处理同步请求而不用发出同步请求.让我们谈谈选择.</p><p><strong>使用异步交互方式</strong></p><p>理想情况下, 所有交互都应该使用本章前面描述的异步交互方式来完成.比如, 假设 FTGO 应用程序的客户端使用异步请求/异步响应交互方式来创建订单.客户端通过发送请求消息给 <code>订单服务</code> 来创建订单.这个服务然后与其他服务异步交换消息, 并最终发送应答消息给客户端.图 3.16 展示了这个设计.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.16-The%20FTGO%20application%20has%20higher%20availability%20if%20its%20services%20communicate%20using%20asynchronous%20messaging.png" alt="Figure 3.16-The FTGO application has higher availability if its services communicate using asynchronous messaging"></p><p>客户端和服务通过消息通道发送消息进行异步通信.在这种交互中, 没有任何参与者在等待响应时被阻塞.</p><p>这样的架构将具有极强的弹性, 因为消息代理缓冲消息直到它们可以被消费.然而, 问题是服务通常具有使用 REST 等同步协议的外部 API, 因此它必须立即响应请求.</p><p>如果服务有同步 API, 一种提高可用性的方式是复制数据.让我们看看这是如何工作的.</p><p><strong>复制数据</strong></p><p>在请求处理期间最小化同步请求的一种方法是复制数据.服务在处理请求时维护所需数据的副本.它通过订阅拥有数据的服务发布的事件来更新数据副本.例如,<code>订单</code> 可以维护 <code>消费者服务</code> 和 <code>餐厅服务</code> 拥有的数据的副本.这将使 <code>订单服务</code> 能够处理创建订单的请求, 而不必与这些服务交互.图 3.17 显示了设计.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.17-Order%20Service%20is%20self-contained%20because%20it%20has%20replicas%20of%20the%20consumer%20and%20restaurant%20data.png" alt="Figure 3.17-Order Service is self-contained because it has replicas of the consumer and restaurant data"></p><p><code>消费者服务</code> 和 <code>餐厅服务</code> 在数据发生变化时发布事件.<code>订单服务</code> 订阅这些事件并更新其副本.</p><p>在某些情况下, 复制数据是一种有用的方法.例如, 第 5 章描述了 <code>订单服务</code> 如何复制 <code>餐厅服务</code> 中的数据, 以便验证和给菜单项定价.复制的一个缺点是有时需要复制大量数据, 这是低效的.例如, <code>订单服务</code> 维护 <code>消费者服务</code> 拥有的数据的副本可能不实际, 因为有大量的使用者.复制的另一个缺点是它不能解决服务如何更新其他服务拥有的数据的问题.</p><p>解决这个问题的一种方法是服务延迟与其他服务的交互, 直到它响应其客户端.接下来我们来看看它是如何工作的.</p><p><strong>响应返回后完成处理</strong></p><p>对于服务处理请求而言在请求处理期间消除异步通信的另一种方式如下所述:</p><ul><li>只使用本地可用的数据验证请求.</li><li>更新它的数据库, 包括插入消息到 <code>OUTBOX</code> 表中.</li><li>返回响应到它的客户端.</li></ul><p>当处理请求时, 服务不用与其它服务进行同步交互.相反, 它异步发送消息给其他服务.这种方式保证了服务是松耦合的.如你将在下一章所见, 这个通常使用 <code>saga</code> 实现.</p><p>比如, 如果 <code>订单服务</code> 使用这种方式, 它创建一个订单, 状态为 <code>PENDING</code>, 然后通过与其他服务异步交互消息来验证订单.图 3.18 展示了当 <code>createOrder()</code> 操作被调用的时候发生了什么.事件的顺序如下:</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/microservices/books/Microservice-Patterns/第3章-微服务架构中的进程间通信/Figure%203.18-Order%20Service%20creates%20an%20order%20without%20invoking%20any%20other%20service.png" alt="Figure 3.18-Order Service creates an order without invoking any other service"></p><ul><li><code>订单服务</code> 创建订单, 状态为 <code>PENDING</code></li><li><code>订单服务</code> 返回响应给它的客户端, 包含了订单 ID.</li><li><code>订单服务</code> 发送 <code>ValidateConsumerInfo</code> 消息到 <code>消费者服务</code>.</li><li><code>订单服务</code> 发送 <code>ValidateOrderDetails</code> 消息到 <code>餐厅服务</code>.</li><li><code>消费者服务</code> 接收到 <code>ValidateConsumerInfo</code> 消息, 验证消费者是否可以下单, 并发送 <code>ConsumerValidated</code> 消息给 <code>订单服务</code>.</li><li><code>餐厅服务</code> 接收到 <code>ValidateOrderDetails</code> 消息, 验证菜单项, 餐厅是否可以配送到订单的地址, 并发送 <code>OrderDetailsValidated</code> 消息到 <code>订单服务</code>.</li><li><code>订单服务</code> 接收到 <code>ConsumerValidated</code> 和 <code>OrderDetailsValidated</code> 消息, 并更改订单的状态为 <code>VALIDATED</code>.</li><li>…</li></ul><p><code>订单服务</code> 可以按任意顺序接收 <code>ConsumerValidated</code> 和 <code>OrderDetailsValidated</code> 消息.它通过更改订单状态来跟踪它首先接收的消息.如果它先接收到了 <code>ConsumerValidated</code>, 它更改订单的状态为 <code>CONSUMER_VALIDATED</code>, 然而如果它首先接收到了 <code>OrderDetailsValidated</code> 消息, 它改变订单的状态为 <code>ORDER_DETAILS_VALIDATED</code>.<code>订单服务</code> 更改订单的状态为 <code>VALIDATED</code> 当它接收到其他消息.</p><p>在订单被验证后, <code>订单服务</code> 完成订单创建过程的其余部分, 将在下一章讨论.这种方式的好处是, 比如, 即使 <code>消费者服务</code> 宕机了,<code></code>订单服务<code>仍然可以创建订单并向它的客户端响应.最终,</code>消费者` 服务将会恢复, 并处理所有队列中的消息, 订单随后会被验证.</p><p>服务在完全处理请求之前进行响应的一个缺点是, 这会使客户端更加复杂.比如, <code>订单服务</code> 在返回响应时, 对新创建的订单的状态做出最小程度的保证.它创建订单并立即返回在验证订单和授权消费者的信用卡之前.因此, 为了让客户端知道订单是否创建成功, 它必须定期轮询或订单服务必须向它发送通知消息.尽管听起来很复杂, 但在许多情况下这是首选的方法-特别是因为它还解决了我在下一章讨论的分布式事务管理问题.在第 4 章和第 5 章, 我将描述 <code>订单服务</code> 使用这种方式.</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>微服务架构是分布式架构, 所以进程间通信扮演了关键角色.</li><li>仔细管理服务 API 的演变是非常重要的.向后兼容的更改是最容易实现的, 因为它们不会影响客户端.如果您对服务的 API 进行了重大更改, 通常需要同时支持新旧版本直到它的客户端已经升级.</li><li>有多种 IPC 技术, 每种有不同的权衡.一个关键的设计决策是选择同步远程过程调用模式还是异步消息模式.基于同步远程过程调用的协议, 比如 REST 是最容易使用的.但是, 为了提高可用性, 服务最好使用异步消息进行通信.</li><li>为了防止系统间的级联故障, 使用同步协议的服务客户端必须被设计成可以处理局部故障, 当被调用的服务出现故障或表现出高延迟的时候.特别是, 它必须使用超时当发出请求的时候, 限制未完成请求的数量, 并使用断路器模式来避免调用故障服务.</li><li>使用同步协议的架构必须包含服务发现机制为了让客户端知道服务实例的网络地址.最简单的方式是使用部署平台实现的服务发现机制: 客户端发现和第三方的注册模式.另外一种方式是在应用层实现服务发现: 客户端发现和自注册模式.这需要更多的工作, 但是它确实处理了服务在多个部署平台上运行的场景.</li><li>设计基于消息的架构的一种好的方式是使用消息和通道模型, 它抽象了底层消息系统的细节.然后, 你可以将该设计映射到特定的消息基础设施, 该基础设施通常基于消息代理.</li><li>使用消息时的一个关键挑战是原子更新数据库和发布消息.一个好的解决方案是使用事务性发件箱(OUTBOX)模式, 首先将消息作为数据库事务的一部分写入数据库.然后, 使用轮询发布者模式或事务日志跟踪模式从数据库检索消息, 并将其发布到消息代理.</li></ul>]]></content>
<summary type="html">
<p>这章包含:</p>
<ul>
<li>应用通信模式: 远程过程调用、断路器、客户端发现、自注册、服务器端发现、第三方注册、异步消息、事务发件箱(Transactional outbox)、事务日志跟踪、轮询发布者</li>
<li>微服务架构中进程间通信的重要性</li>
</summary>
<category term="微服务" scheme="https://ykgarfield.github.io/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
<category term="Microservice-Patterns(译)" scheme="https://ykgarfield.github.io/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/Microservice-Patterns-%E8%AF%91/"/>
<category term="微服务" scheme="https://ykgarfield.github.io/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
<category term="微服模式" scheme="https://ykgarfield.github.io/tags/%E5%BE%AE%E6%9C%8D%E6%A8%A1%E5%BC%8F/"/>
</entry>
<entry>
<title>vscode github 图床插件 markdown-pic2github 改造</title>
<link href="https://ykgarfield.github.io/2019/02/14/%E5%B7%A5%E5%85%B7/vscode/vscode%20github%20%E5%9B%BE%E5%BA%8A%E6%8F%92%E4%BB%B6%20markdown-pic2github%20%E6%94%B9%E9%80%A0/"/>
<id>https://ykgarfield.github.io/2019/02/14/工具/vscode/vscode github 图床插件 markdown-pic2github 改造/</id>
<published>2019-02-13T16:00:00.000Z</published>
<updated>2019-02-14T06:05:22.846Z</updated>
<content type="html"><![CDATA[<h2 id="支持图床服务的编辑器"><a href="#支持图床服务的编辑器" class="headerlink" title="支持图床服务的编辑器"></a>支持图床服务的编辑器</h2><p>目前市场上的图床服务还是比较多的, 比如 <code>github</code>、<code>imgur</code>、<code>sm.ms</code>、<code>七牛云存储</code>、<code>upyun存储</code>、<code>阿里oss云存储</code>、<code>腾讯oss云存储</code>、<code>weibo</code> 等.选择哪种图床服务取决于个人喜好, 重要的是这些图床服务得有一个比较好的类似图形化的工具, 方便使用者.这就得看使用的 markdown 编辑器是否支持这样的功能.个人使用的是 <code>github</code> 图床服务.</p><h3 id="小书匠"><a href="#小书匠" class="headerlink" title="小书匠"></a>小书匠</h3><p>目前在这方便做的比较好的是 <code>小书匠</code>.提供了多种图床服务, 支持一些自定义的命名规则.功能按钮就能直接上传图片到指定的图床服务, 并返回对应的图片链接.不过 <code>小书匠</code> 还是有一些不尽人意的地方, 所以最终没有使用.</p><h3 id="GitNode"><a href="#GitNode" class="headerlink" title="GitNode"></a>GitNode</h3><p>这是一款最近才出来的编辑器, 也提供了一些图床服务(以插件的方式提供), 不过目前做的比较简单, 另外扩展 API 还不完善, 持续观望中.</p><h3 id="vscode"><a href="#vscode" class="headerlink" title="vscode"></a>vscode</h3><p>vscode 是一款扩展能力很强的编辑器, 可通过编写插件来进行扩展.目前也是支持了一些图传的服务.而支持 github 图床的, 目前只找到了 <code>markdown-pic2github</code> 这一个.下面会分析这款插件有哪些方面的不足, 以及如何进行改进.</p><p>插件地址: <a href="https://marketplace.visualstudio.com/items?itemName=quareia.markdown-pic2github" target="_blank" rel="noopener">https://marketplace.visualstudio.com/items?itemName=quareia.markdown-pic2github</a><br>github 地址: <a href="https://github.com/Quareia/vscode-markdown-pic2github" target="_blank" rel="noopener">https://github.com/Quareia/vscode-markdown-pic2github</a></p><h2 id="markdown-pic2github-的不足以及改造点"><a href="#markdown-pic2github-的不足以及改造点" class="headerlink" title="markdown-pic2github 的不足以及改造点"></a>markdown-pic2github 的不足以及改造点</h2><p>首先先分析下此插件有哪些方便的不足, 以及如何进行改造来满足自定义的需求.</p><ul><li>windows 环境下无法使用, 主要有几下几个原因导致:<ul><li>不支持 <code>cp</code> 命令, 复制文件.需要使用 <code>copy</code> 或者 <code>xcopy</code> 命令复制文件</li><li>进入系统盘外的磁盘目录, 只通过 <code>cd</code> 命令还不够.比如, 进入 <code>E:\git</code> 目录, 使用 <code>cmd</code> 需要两条命令(<code>E: && cd E:\git</code>)才能进入, </li></ul></li><li>没有对图片 url 进行转义, 这会导致如果 url 有特殊字符, 那么会导致返回的图片 url 有问题.</li><li>没有足够好的目录归档支持, 比如有一个 <code>hello.md</code> 文档, 文档里有多张图片, 那么这些图片最好能够归档到 <code>hello</code> 这一个目录下, 便于进行管理.对于这一点, 自己使用了如下的方式进行管理.比如在某个 git 服务(github、gitee 等)上创建了一个 <code>blog</code> 仓库用于存放自己的笔记信息(可能是私有的, 毕竟不是所有的笔记文档都想公开).并在 github 上建立了一个 public 仓库来存放图片相关的文件, 比如叫 <code>blog-files</code>.下面我们创建了一个 <code>hello.md</code> 的文档, 在 <code>blog</code> 仓库的位置如下:</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></pre></td><td class="code"><pre><span class="line">blog</span><br><span class="line"> |Java</span><br><span class="line"> | 基础知识</span><br><span class="line"> | hello.md</span><br><span class="line"> | JavaScript</span><br></pre></td></tr></table></figure><p>此文档上传了两张图片(<code>Figure-1 hello.png</code>、<code>Figure-2 world.png</code>), 那么我希望在 <code>blog-files</code> 仓库这张图片的存放位置可以如下:</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></pre></td><td class="code"><pre><span class="line">blog-files</span><br><span class="line"> |images</span><br><span class="line"> |Java</span><br><span class="line"> |基础知识</span><br><span class="line"> |hello</span><br><span class="line"> |Figure-1 hello.png</span><br><span class="line"> |Figure-2 world.png</span><br></pre></td></tr></table></figure><h2 id="markdown-pic2github-改造"><a href="#markdown-pic2github-改造" class="headerlink" title="markdown-pic2github 改造"></a>markdown-pic2github 改造</h2><p>找到此插件的目录, 对 <code>extension.js</code> 的源码进行修改.虽然不熟悉 js, nodejs 等相关技术, 不过依葫芦画瓢, 先试试再说, 核心代码如下:</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><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 class="comment">// 解构</span></span><br><span class="line"><span class="keyword">let</span> {fsPath} = result[<span class="number">0</span>]</span><br><span class="line"><span class="keyword">let</span> imgName = fsPath.split(path.sep).pop()</span><br><span class="line"><span class="keyword">let</span> cmdStr = <span class="string">'cp '</span> + fsPath + <span class="string">' '</span> + localFolder + <span class="string">' && '</span> + </span><br><span class="line"><span class="string">'cd '</span> + localFolder + <span class="string">' && '</span> +</span><br><span class="line"><span class="string">'git add '</span> + imgName + <span class="string">' && '</span> +</span><br><span class="line"><span class="string">'git commit -m '</span> + doc.fileName.split(path.sep).pop() + <span class="string">' && '</span> +</span><br><span class="line"><span class="string">'git push'</span></span><br><span class="line"><span class="built_in">console</span>.log(cmdStr)</span><br><span class="line">exec(cmdStr, <span class="function"><span class="keyword">function</span>(<span class="params">err, stdout, stderr</span>) </span>{</span><br><span class="line"> <span class="keyword">if</span> (err) {</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'wrong:'</span> + stderr)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">let</span> data = stdout</span><br><span class="line"> <span class="built_in">console</span>.log(data)</span><br><span class="line"> }</span><br><span class="line">})</span><br><span class="line"><span class="keyword">let</span> imgUrl = <span class="string">'![](https://raw.githubusercontent.com/'</span> + remoteRepo +<span class="string">'/master/'</span>+ imgName + <span class="string">')'</span></span><br><span class="line">editor.edit(<span class="function"><span class="params">textEditorEdit</span> =></span> {</span><br><span class="line"> textEditorEdit.insert(editor.selection.active, imgUrl)</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>从上面可以看到其实不是很难嘛, 都是一些比较简单的语法还有 API 的使用.另外可以看到其实插件最终是拼接了一条命令字符串, 然后应该是让操作系统执行.最终将图片的地址拼接出来插入到文档中.按照上面的要求, 做了如下的几点修改:</p><ul><li>命令字符串中使用的是 <code>cp</code> 命令, 这在 windows 上是没法使用的, 所以我们需要改成 <code>xcopy</code> 命令, 为了防止图片有空格, 最好在在命令中的图片名称前后加上 <code>"</code>.另外为了覆盖当前已存在的文件, 在此命令最后加上 <code>/y</code> 参数. </li><li><code>doc.fileName</code>(<code>vscode.window.activeTextEditor.document.fileName</code>) 取得是当前编辑文档的全路径.通过这个全路径我们可以知道是在哪个磁盘目录, 由此可以进入到 git 的根目录.根据这个根目录和文档的全路径, 可以得到要创建的图片的目录.比如, 当前编辑的 <code>hello.md</code> 文档位于 <code>E:\git\blog\Java\基础知识\hello.md</code>, 则在 <code>E:\git\blog-files\images</code> 目录下创建 <code>Java\基础知识\hello</code> 目录. </li><li>创建目录使用 <code>mkdir</code> 命令, 如果创建的目录存在的话会报错, 所以我们需要先进行判断, 不存在则创建, 所以需要使用命令: <code>if not exists Java\基础知识\hello mdir Java\基础知识\hello</code>, 另外, 因为后面还要拼接 git 相关的命令, 所以讲这条命令放在 <code>()</code> 中, 防止发生短路与, 导致后面的命令不执行.</li></ul><p>经过上面的改造, 我们可以得出类似于下面的命令:</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">E: && cd E:\git\blog-files\images && (if not exists Java\基础知识\hello mdir Java\基础知识\hello) && xcopy "E:\temp\images\Figure-1 hello.png" "E:\git\blog-files\images\Java\基础知识\hello\Figure-1 hello.png" /y && git add "images\Java\基础知识\hello\Figure-1 hello.png" && git commit -m "hello.md" && git push</span><br></pre></td></tr></table></figure><p>通过上面一系列命令的组合, 我们就完成了我们想要的整个功能.其实改造的成本并不大, 而且没太多的难度.详细代码可见 <a href="https://github.com/ykgarfield/vscode-markdown-pic2github" target="_blank" rel="noopener">https://github.com/ykgarfield/vscode-markdown-pic2github</a>, 如果有自己的特殊要求, 完全可以自行改造.</p>]]></content>
<summary type="html">
<h2 id="支持图床服务的编辑器"><a href="#支持图床服务的编辑器" class="headerlink" title="支持图床服务的编辑器"></a>支持图床服务的编辑器</h2><p>目前市场上的图床服务还是比较多的, 比如 <code>github</cod
</summary>
<category term="工具" scheme="https://ykgarfield.github.io/categories/%E5%B7%A5%E5%85%B7/"/>
<category term="vscode" scheme="https://ykgarfield.github.io/categories/%E5%B7%A5%E5%85%B7/vscode/"/>
<category term="vscode" scheme="https://ykgarfield.github.io/tags/vscode/"/>
<category term="github 图床" scheme="https://ykgarfield.github.io/tags/github-%E5%9B%BE%E5%BA%8A/"/>
</entry>
<entry>
<title>从 fesacr 源码中总结出的 Java 代码编写注意事项</title>
<link href="https://ykgarfield.github.io/2019/01/20/Java/%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83/%E4%BB%8E%20fesacr%20%E6%BA%90%E7%A0%81%E4%B8%AD%E6%80%BB%E7%BB%93%E5%87%BA%E7%9A%84%20Java%20%E4%BB%A3%E7%A0%81%E7%BC%96%E5%86%99%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/"/>
<id>https://ykgarfield.github.io/2019/01/20/Java/代码规范/从 fesacr 源码中总结出的 Java 代码编写注意事项/</id>
<published>2019-01-19T16:00:00.000Z</published>
<updated>2019-01-21T09:01:39.546Z</updated>
<content type="html"><![CDATA[<p>最近在看 <a href="https://github.com/alibaba/fescar" target="_blank" rel="noopener">fesacr</a> 的源码, 从中也看到有一些代码书写不规范或是有误的地方.本文总结了自己看源码过程中目前发现的一些问题, 都已在 github 上提了 issue 或 pr.</p><h2 id="字符串格式化"><a href="#字符串格式化" class="headerlink" title="字符串格式化"></a>字符串格式化</h2><p>在 Java API 中一般有两种方式对字符串进行格式化:</p><ul><li><code>String.format()</code></li><li><code>MessageFormat.format()</code></li></ul><p>以上两种方法的主要差异其实就是要格式化的字符串中的占位符, <code>String.format()</code> 使用类似于 <code>%s</code>(表示字符)这样的占位符, <code>MessageFormat.format()</code> 使用 <code>{0}</code>(<code>{参数索引}</code>).</p><p>而我们在项目开发中, 都会使用日志框架, 在打印日志的时候, 也会使用占位符进行格式化字符串, 占位符一般为 <code>{}</code>, 所以在项目开发过程中可能会搞错这几种方式, 搞混了这几种占位符.所以需要格外的注意.</p><p>对于 <code>String.format()</code> 而言, IDEA 其实已经提供了较好的验证支持, </p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/从_fesacr_源码中总结出的_Java_代码编写注意事项/String.format占位符错误警告.jpg" alt="String.format()占位符错误警告"></p><p>相关 issue 可见: <a href="https://github.com/alibaba/fescar/issues/122" target="_blank" rel="noopener">https://github.com/alibaba/fescar/issues/122</a></p><h2 id="不要使用-File-rename-对文件重命名"><a href="#不要使用-File-rename-对文件重命名" class="headerlink" title="不要使用 File.rename() 对文件重命名"></a>不要使用 File.rename() 对文件重命名</h2><p>如果我们想对文件进行重命名, 首先想到的就是使用 <code>File.rename()</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Renames the file denoted by this abstract pathname.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <p> Many aspects of the behavior of this method are inherently</span></span><br><span class="line"><span class="comment"> * platform-dependent: The rename operation might not be able to move a</span></span><br><span class="line"><span class="comment"> * file from one filesystem to another, it might not be atomic, and it</span></span><br><span class="line"><span class="comment"> * might not succeed if a file with the destination abstract pathname</span></span><br><span class="line"><span class="comment"> * already exists. The return value should always be checked to make sure</span></span><br><span class="line"><span class="comment"> * that the rename operation was successful.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <p> Note that the {<span class="doctag">@link</span> java.nio.file.Files} class defines the {<span class="doctag">@link</span></span></span><br><span class="line"><span class="comment"> * java.nio.file.Files#move move} method to move or rename a file in a</span></span><br><span class="line"><span class="comment"> * platform independent manner.</span></span><br><span class="line"><span class="comment"> */</span></span><br></pre></td></tr></table></figure><p>在几年前, 在用 Jetty 的时候, 也是遇到过这个问题.后来排查原因, 就是因为 <code>File.rename()</code> 引起的.所以最好不要使用此方法重命名.可以使用 <code>Files.move()</code> 或者是 apache <code>commons-io</code> 包中的方法.</p><p>相关 issue 可见: <a href="https://github.com/alibaba/fescar/issues/92" target="_blank" rel="noopener">https://github.com/alibaba/fescar/issues/92</a></p><h2 id="关闭-io-流的时候避免冗余的-close-方法调用"><a href="#关闭-io-流的时候避免冗余的-close-方法调用" class="headerlink" title="关闭 io 流的时候避免冗余的 close() 方法调用"></a>关闭 io 流的时候避免冗余的 close() 方法调用</h2><p>有如下一个获取文件 channel 的例子:</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">File file = <span class="keyword">new</span> File(...);</span><br><span class="line">RandomAccessFile raf = <span class="keyword">new</span> RandomAccessFile(file, <span class="string">"rw"</span>);</span><br><span class="line"><span class="comment">// 获取 channel</span></span><br><span class="line">FileChannel fileChannel = raf.getChannel();</span><br><span class="line"><span class="comment">// 写数据</span></span><br><span class="line">fileChannel.write(...);</span><br></pre></td></tr></table></figure><p>一般我们可以通过上述的方式写入数据到文件中, 在最后关闭的时候, 大部分时候可能是为了保险起见, 会使用如下的代码关闭相关的资源:</p><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">fileChannel.close();</span><br><span class="line">raf.close();</span><br></pre></td></tr></table></figure><p>但是如下仔细查看 <code>RandomAccessFile.close()</code> 方法的源码, 就会发现, 其实这个方法的内部会自动调用 <code>fileChannel.close()</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">close</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>{</span><br><span class="line"><span class="keyword">synchronized</span> (closeLock) {</span><br><span class="line"><span class="keyword">if</span> (closed) {</span><br><span class="line"><span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line">closed = <span class="keyword">true</span>;</span><br><span class="line">}</span><br><span class="line"> <span class="comment">// 自动关闭 channel</span></span><br><span class="line"><span class="keyword">if</span> (channel != <span class="keyword">null</span>) {</span><br><span class="line">channel.close();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">fd.closeAll(<span class="keyword">new</span> Closeable() {</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">close</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>{</span><br><span class="line"> close0();</span><br><span class="line"> }</span><br><span class="line">});</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>从上面可以看出, 关闭文件相关资源的时候, 只需要调用 <code>RandomAccessFile.close()</code>, 而不需要再调用一次 <code>fileChannel.close()</code>.</p><p>相关 issue 可见: <a href="https://github.com/alibaba/fescar/issues/127" target="_blank" rel="noopener">https://github.com/alibaba/fescar/issues/127</a></p><h2 id="Netty-服务端设置-SO-KEEPALIVE-是没有用的"><a href="#Netty-服务端设置-SO-KEEPALIVE-是没有用的" class="headerlink" title="Netty 服务端设置 SO_KEEPALIVE 是没有用的"></a>Netty 服务端设置 SO_KEEPALIVE 是没有用的</h2><p>在用 Netty 进行网络编程的时候, 通过会设置 <code>SO_KEEPALIVE</code> 选项为 <code>true</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.serverBootstrap.group(<span class="keyword">this</span>.eventLoopGroupBoss, <span class="keyword">this</span>.eventLoopGroupWorker)</span><br><span class="line">.channel(NioServerSocketChannel.class)</span><br><span class="line">.option(ChannelOption.SO_BACKLOG, nettyServerConfig.getSoBackLogSize())</span><br><span class="line">.option(ChannelOption.SO_REUSEADDR, <span class="keyword">true</span>)</span><br><span class="line"><span class="comment">// 设置 SO_KEEPALIVE 选项</span></span><br><span class="line">.option(ChannelOption.SO_KEEPALIVE, <span class="keyword">true</span>)</span><br></pre></td></tr></table></figure><p>初看上面的代码感觉是没有任何问题的, 有可能项目跑起来的时候也注意不到有什么问题.看如果真的细看启动日志就会发现, 使用上面的代码启动后, 会打印如下的警告日志:</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">WARN io.netty.bootstrap.ServerBootstrap - Unknown channel option 'SO_KEEPALIVE' for channel ...</span><br></pre></td></tr></table></figure><p>这个问题的解决方案也很简单, 应该使用 <code>childOption()</code> 方法.这一点比较难以察觉的, 有时候需要观察下启动日志.</p><p>相关 issue 可见: <a href="https://github.com/alibaba/fescar/issues/150" target="_blank" rel="noopener">https://github.com/alibaba/fescar/issues/150</a></p>]]></content>
<summary type="html">
<p>最近在看 <a href="https://github.com/alibaba/fescar" target="_blank" rel="noopener">fesacr</a> 的源码, 从中也看到有一些代码书写不规范或是有误的地方.本文总结了自己看源码过程中目前发现的
</summary>
<category term="Java" scheme="https://ykgarfield.github.io/categories/Java/"/>
<category term="代码规范" scheme="https://ykgarfield.github.io/categories/Java/%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83/"/>
<category term="fescar" scheme="https://ykgarfield.github.io/tags/fescar/"/>
<category term="代码规范" scheme="https://ykgarfield.github.io/tags/%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83/"/>
</entry>
<entry>
<title>第2章-分解策略</title>
<link href="https://ykgarfield.github.io/2019/01/15/%E5%BE%AE%E6%9C%8D%E5%8A%A1/Microservice-Patterns(%E8%AF%91)/%E7%AC%AC2%E7%AB%A0-%E5%88%86%E8%A7%A3%E7%AD%96%E7%95%A5/"/>
<id>https://ykgarfield.github.io/2019/01/15/微服务/Microservice-Patterns(译)/第2章-分解策略/</id>
<published>2019-01-14T16:00:00.000Z</published>
<updated>2019-01-21T09:17:02.642Z</updated>
<content type="html"><![CDATA[<p>这章包含:</p><ul><li>理解软件架构及其重要性</li><li>通过应用按业务能力分解和按子域分解的分解模式, 将应用程序分解为服务</li><li>用来自领域驱动设计(DDD)的有界上下文(bounded context)概念来理清数据并使分解更容易</li></ul><p>有时候你必须小心你的愿望.经过激烈的游说, Mary 最终说服了业务部门, 迁移到微服务架构是正确的.Mary 感到既兴奋又有些害怕, 她和她的架构师们开了一个长达一上午的会, 讨论从哪里开始.在讨论期间, 很明显, 微服务架构模式语言的某些方面, 如部署和服务发现, 是新的和不熟悉的, 但是很简单.微服务架构的核心挑战是将应用程序分解为服务.因此, 架构的第一个也是最重要的方面是服务的定义.当他们站在白板旁时, FTGO 团队想知道该怎么做!</p><p>在本章中, 您将学习如何为应用程序定义微服务架构.我将描述将应用程序分解为服务的策略.您将了解到服务是围绕业务关注点组织的, 而不是围绕技术关注点组织的.我还展示了如何使用来自领域驱动设计(DDD)的思想来消除 god 类, god 类是贯穿整个应用程序使用的类, 它们会导致妨碍分解的复杂依赖关系.</p><p>本章首先从软件架构概念的角度定义微服务架构.然后, 我将描述从应用程序的需求开始定义微服务架构的过程.我将讨论将应用程序分解为服务集合的策略、遇到的障碍以及如何克服它们.让我们从审查软件架构的概念开始.</p><h1 id="微服务体系结构究竟是什么"><a href="#微服务体系结构究竟是什么" class="headerlink" title="微服务体系结构究竟是什么?"></a>微服务体系结构究竟是什么?</h1><p>第 1 章描述了微服务架构的关键思想是功能分解.您不是开发一个大型应用程序, 而是将应用程序构造为一组服务.一方面, 将微服务架构描述为一种功能分解是有用的.但另一方面, 它留下了几个未解的问题, 包括微服务架构如何与概念更加广泛的软件架构相关联? 什么是服务? 服务的大小有多重要?</p><p>为了回答这些问题, 我们需要后退一步, 看看<em>软件架构</em>是什么意思.软件应用程序的架构是它的高层结构, 它由组成部分和这些部分之间的依赖关系组成.正如您将在本节中看到的, 应用程序的架构是多维的, 因此有多种方法来描述它.架构之所以重要, 是因为它决定了应用程序的软件质量属性或 <em>ilities</em>(可参考 <a href="https://zh.wikipedia.org/wiki/非功能性需求).传统上" target="_blank" rel="noopener">https://zh.wikipedia.org/wiki/非功能性需求).传统上</a>, 架构的目标是可伸缩性、可靠性和安全性.但是今天,架构还支持快速和安全的软件交付, 这一点很重要.您将了解到, 微服务架构是一种架构风格, 它使应用程序具有很高的可维护性、可测试性和可部署性.</p><p>我将通过描述软件架构的概念及其重要性来开始这一节.接下来, 我将讨论架构风格的概念.然后, 我将微服务架构定义为一种特殊的架构风格让我们从软件架构的概念开始.</p><h2 id="什么是软件架构-它为什么重要"><a href="#什么是软件架构-它为什么重要" class="headerlink" title="什么是软件架构? 它为什么重要?"></a>什么是软件架构? 它为什么重要?</h2><p>架构显然很重要.至少有两个会议专门讨论这个主题: O ‘Reilly 软件架构会议(<a href="https://conferences.oreilly.com/softw-architecture)和" target="_blank" rel="noopener">https://conferences.oreilly.com/softw-architecture)和</a> SATURN 会议(<a href="https://resources.sei.cmu.edu/news-events/events/saturn/).许多开发人员都有成为架构师的目标.但是什么是架构" target="_blank" rel="noopener">https://resources.sei.cmu.edu/news-events/events/saturn/).许多开发人员都有成为架构师的目标.但是什么是架构</a>? 它为什么重要?</p><p>为了回答这个问题, 我首先定义了术语软件架构的含义.然后, 我将讨论应用程序的架构是多维的以及如何使用视图或蓝图的集合进行最佳描述.然后我描述了软件架构的重要性, 因为它对应用程序的软件质量属性有影响.</p><p><strong>软件架构的定义</strong><br>软件架构有许多定义.例如, 请参阅 <a href="https://en.wikiquote.org/wiki/Software_architecture" target="_blank" rel="noopener">https://en.wikiquote.org/wiki/Software_architecture</a> 来阅读其中一些.我最喜欢的定义来自 Len Bass 和他在软件工程研究所(<a href="http://www.sei.cmu.edu)的同事" target="_blank" rel="noopener">www.sei.cmu.edu)的同事</a>, 他们在将软件架构作为一门学科建立起来方面发挥了关键作用.他们对软件架构的定义如下:</p><blockquote><p>计算系统的软件架构是对系统进行推理所需要的一组结构, 它包括软件元素、它们之间的关系以及两者的属性.<br> — Bass 等人编写的软件架构文档.</p></blockquote><p>这显然是一个相当抽象的定义.但它的本质是, 应用程序的架构是将其分解为部分(元素)和这些部分之间的关系(关系).分解之所以重要有几个原因:</p><ul><li>它促进了劳动和知识的分工.它使具有专业知识的多个人员(或多个团队)能够在一个应用程序上高效地一起工作.</li><li>它定义了软件元素如何交互.</li></ul><p>分解成各个部分以及这些部分之间的关系决定了应用程序的性能.</p><p><strong>软件架构的4+1视图模型</strong></p><p>更具体地说, 可以从多个角度查看应用程序的架构, 就像可以从结构、管道、电气和其他角度查看建筑物的体系结构一样.Phillip Krutchen 写了一篇描述软件架构的 4+1 视图模型的经典论文, “架构蓝图-软件架构的 ‘4+1’ 视图模型”(<a href="http://www.cs.ubc.ca/~gregor/teaching/papers/4+1view-architecture.pdf).图" target="_blank" rel="noopener">www.cs.ubc.ca/~gregor/teaching/papers/4+1view-architecture.pdf).图</a> 2.1 所示的 4+1 模型定义了软件架构的四个不同视图.它们都描述了架构的一个特定方面, 并由一组特定的软件元素和它们之间的关系组成.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.1_The_4+1_view_model_defines_four_different_views_of_a_software_architecture.jpg" alt="Figure 2.1 The 4+1 view model defines four different views of a software architecture"></p><p>每个视图的目的如下:</p><ul><li><em>逻辑视图</em>-由开发人员创建的软件元素.在面向对象语言中, 这些元素是类和包.它们之间的关系是类和包之间的关系, 包括继承、关联和依赖.</li><li><em>实现视图</em>-构建系统的输出.该视图由模块和组件组成, 模块表示打包的代码, 组件是由一个或多个模块组成的可执行或可部署单元.在 Java 中, 模块是 JAR 文件,组件通常是 WAR 文件或可执行 JAR 文件.它们之间的关系包括模块之间的依赖关系和组件与模块之间的组合关系.</li><li><em>进程视图</em>-运行时的组件.每个元素都是一个进程, 进程之间的关系表示进程间通信.</li><li><em>部署视图</em>-如何将进程映射到机器上.该视图中的元素由(物理或虚拟)机器和进程组成.机器之间的关系代表了网络.这个视图还描述了进程和机器之间的关系.</li></ul><p>除了这四个视图之外, 还有场景-4+1 模型中的 +1-动画视图.每个场景描述特定视图中的各个架构组件如何协作以处理请求.例如, 逻辑视图中的一个场景显示了类如何协作.类似地, 进程视图中的场景显示了进程如何协作.</p><p>4+1 视图模型是描述应用程序架构的一种很好的方法.每个视图描述架构的一个重要方面, 场景说明视图的元素如何协作.现在让我们看看为什么架构是重要的.</p><p><strong>为什么架构是重要的?</strong></p><p>应用程序有两类需求.第一类包括功能需求, 它们定义了应用程序必须做什么.它们通常以用例或用户故事的形式出现.架构与功能需求的关系非常小.您可以用几乎任何架构实现功能需求, 甚至是一个大泥球.</p><p>架构非常重要, 因为它使应用程序能够满足第二类需求: 其服务质量需求.这些也被称为质量属性, 即所谓的质量.服务质量要求定义了运行时质量, 如可伸缩性和可靠性.它们还定义了开发周期的质量, 包括可维护性、可测试性和可部署性.您为应用程序选择的架构决定了它如何很好地满足这些质量要求.</p><h2 id="架构风格概览"><a href="#架构风格概览" class="headerlink" title="架构风格概览"></a>架构风格概览</h2><p>在现实世界中, 建筑物的建筑往往遵循一种特定的风格, 如维多利亚时代、美国工匠或装饰艺术.每一种风格都是一套限制建筑特色和建筑材料的设计决策.架构风格的概念也适用于软件.David Garlan 和 Mary Shaw(软件架构介绍, 1994 年 1 月, <a href="https://www.cs.cmu.edu/afs/cs/project/able/ftp/intro_softarch/intro_softarch.pdf)是软件架构学科的先驱" target="_blank" rel="noopener">https://www.cs.cmu.edu/afs/cs/project/able/ftp/intro_softarch/intro_softarch.pdf)是软件架构学科的先驱</a>, 他们定义了如下的架构风格:</p><blockquote><p>架构风格根据结构组织的模式定义了一系列此类系统.更具体地说, 架构风格决定了在该风格的实例中可以使用的组件和连接器(connectors)的词汇表, 以及关于如何组合它们的一组约束.</p></blockquote><p>特定的架构风格提供了元素(组件)和关系(连接器)的有限调色板, 您可以从中定义应用程序架构的视图.应用程序通常使用架构风格的组合.例如, 在本节后面, 我将描述单体架构是如何将实现视图构造为单个(可执行/可部署)组件的架构风格.微服务架构将应用程序构造为一组散耦合的服务.</p><p><strong>分层的架构风格</strong></p><p>架构风格的经典例子是分层架构.分层架构将软件元素组织成层.每个层都有一组定义良好的职责.分层架构还限制了层之间的依赖关系.一个层只能依赖于它下面的层(如果严格的分层)或者它下面的任何层.</p><p>您可以将分层架构应用于前面讨论的四个视图中的任何一个.流行的三层架构是应用于逻辑视图的分层架构.它将应用程序的类组织成以下层:</p><ul><li><em>表示层</em>-包含实现用户界面或外部 API 的代码</li><li><em>业务逻辑层</em>-包含了业务逻辑</li><li><em>持久层</em>-实现了与数据库交互的逻辑</li></ul><p>分层架构是架构风格的一个很好的例子, 但是它也有一些明显的缺点:</p><ul><li><em>单一表示层</em>-它并不表示应用程序可能被多个系统调用这一事实.</li><li><em>单一持久层</em>-它并不表示应用程序可能与多个数据库交互这一事实.</li><li><em>根据持久层定义业务逻辑层</em>-从理论上讲, 这种依赖性阻止您在没有数据库的情况下测试业务逻辑.</li></ul><p>而且, 分层架构错误地表示了设计良好的应用程序中的依赖关系.业务逻辑通常定义定义数据访问方法的接口或接口 repository.持久层定义实现 repository 接口的 DAO 类.换句话说, 依赖关系与分层体系结构所描述的相反.</p><p>让我们来看一个克服这些缺点的替代架构:六边形架构.</p><p><strong>六边形架构风格</strong></p><p><em>六角形架构</em>是分层架构之外的另一种选择.如图 2.2 所示, 六边形架构风格以一种将业务逻辑置于中心的方式组织逻辑视图.与表示层不同, 应用程序有一个或多个<em>入站适配器(inbound adapters)</em>, 通过调用业务逻辑处理来自外部的请求.类似地, 应用程序没有数据持久层, 而是有一个或多个<em>出站适配器(outbound adapters)</em>, 这些适配器由业务逻辑调用并调用外部应用程序.此架构的一个关键特征和优点是业务逻辑不依赖于适配器.相反, 他们依赖它.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.2_An_example_of_a_hexagonal_architecture.jpg" alt="Figure 2.2 An example of a hexagonal architecture"></p><p>业务逻辑有一个或多个接口.<em>端口(port)</em> 定义了一组操作, 以及业务逻辑如何与外部操作交互.例如, 在 Java 中, 端口通常是 Java 接口。有两种端口: 入站端口和出站端口.入站端口是业务逻辑公开的 API, 它允许外部应用程序调用它.入站端口的一个例子是服务接口, 它定义了服务的公共方法.出站端口是业务逻辑调用外部系统的方式.输出端口的一个例子是存储库接口, 它定义了一组数据访问操作.</p><p>围绕业务逻辑的是适配器.与端口一样, 适配器有两种类型: 入站适配器和出站适配器.入站适配器通过调用入站端口处理来自外部的请求.入站适配器的一个例子是 Spring MVC 控制器, 它实现了一组 REST 端点或一组 web 页面.另一个示例是订阅消息的 message broker 客户机.多个入站适配器可以调用相同的入站端口.</p><p>出站适配器实现出站端口, 并通过调用外部应用程序或服务处理来自业务逻辑的请求.出站适配器的一个例子是实现访问数据库操作的<em>数据访问对象</em>(DAO)类.另一个例子是调用远程服务的代理类.出站适配器也可以发布事件.</p><p>六边形架构风格的一个重要优点是, 它将业务逻辑与适配器中的表示逻辑和数据访问逻辑解耦.业务逻辑不依赖于表示逻辑或数据访问逻辑.由于这种解耦, 独立测试业务逻辑要容易得多.另一个好处是, 它更准确地反映了现代应用程序的架构.业务逻辑可以通过多个适配器调用, 每个适配器实现一个特定的 API 或 UI.业务逻辑还可以调用多个适配器, 每个适配器调用不同的外部系统.六边形架构是描述微服务架构中每个服务的架构的一种很好的方法.</p><p>分层结构和六角形架构都是架构风格的例子.它们都定义了架构的构建块, 并对它们之间的关系施加了约束.六角形架构和分层架构以三层架构的形式组织逻辑视图.现在让我们将微服务架构定义为组织实现视图的架构风格.</p><h2 id="微服务架构是一种架构风格"><a href="#微服务架构是一种架构风格" class="headerlink" title="微服务架构是一种架构风格"></a>微服务架构是一种架构风格</h2><p>我已经讨论了 4+ 1视图模型和架构风格, 因此现在可以定义单体和微服务架构.它们都是架构风格.单体架构是一种架构风格, 它将实现视图结构为单个组件:单个可执行文件或 WAR 文件.这个定义与其他视图无关.例如, 一个单体应用程序可以拥有一个逻辑视图, 该视图按照六角形架构的线条组织.</p><blockquote><p>模式: 单体架构<br>将应用程序构建为单个可执行/可部署组件.见 <a href="http://microservices.io/patterns/monolithic.html" target="_blank" rel="noopener">http://microservices.io/patterns/monolithic.html</a>.</p></blockquote><p>微服务架构也是一种架构风格.它将实现视图构建为一组多个组件: 可执行文件或 WAR 文件.组件是服务, 连接器是使这些服务能够协作的通信协议.每个服务都有自己的逻辑视图架构, 通常是六边形的架构.图 2.3 显示了 FTGO 应用程序可能的微服务架构.该架构中的服务与业务功能相对应, 例如订单管理和餐厅管理.</p><blockquote><p>模式: 微服务架构<br>将应用程序构造为松耦合的、独立部署的服务集合, 见 <a href="http://microservices.io/patterns/microservices.html" target="_blank" rel="noopener">http://microservices.io/patterns/microservices.html</a>.</p></blockquote><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.3_A_possible_microservice_architecture_for_the_FTGO_application.jpg" alt="Figure 2.3 A possible microservice architecture for the FTGO application"></p><p>在本章的后面, 我将描述什么是<em>业务能力</em>.服务之间的连接器是使用诸如 REST API 和异步消息传递等进程间通信机制实现的.第 3 章更详细地讨论了进程间通信.</p><p>微服务架构强加的一个关键约束是服务是松耦合的.因此, 对于服务如何协作有一些限制.为了解释这些限制, 我将尝试定义术语<em>服务</em>, 描述松散耦合意味着什么, 并告诉您为什么这很重要.</p><p><strong>什么是服务?</strong></p><p><em>服务</em>是一个独立的、可独立部署的软件组件, 它实现了一些有用的功能.图 2.4 显示了服务的外部视图, 在本例中是 <code>Order Service</code>.服务具有一个 API, 该 API 为其客户端提供对其功能的访问.有两种类型的操作: 命令和查询.API 由命令、查询和事件组成.命令, 比如 <code>createOrder()</code> 执行操作并更新数据.查询, 比如 <code>findOrderById()</code> 检索数据.服务还发布由其客户端消费的事件, 比如 <code>OrderCreated</code>.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.4_shows_the_external_view_of_a_service.jpg" alt="Figure 2.4 shows the external view of a service"></p><p>服务的 API 封装了它的内部实现.与单体不同, 开发人员不能编写绕过 API 的代码.因此, 微服务体系结构强制应用程序的模块化.</p><p>微服务架构中的每个服务都有自己的架构, 可能还有技术栈.但是典型的服务具有六角形架构.它的 API 是由与服务的业务逻辑交互的适配器实现的.操作适配器调用业务逻辑, 事件适配器发布业务逻辑发出的事件.</p><p><strong>什么是松耦合</strong></p><p>微服务架构的一个重要特征是服务是松耦合的(<a href="https://en.wikipedia.org/wiki/Loose_coupling)" target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Loose_coupling)</a>, 与服务的所有交互都是通过 API 进行的, API 封装了服务的实现细节.这使得服务的实现可以在不影响其客户端的情况下进行更改.松耦合服务是改进应用程序开发时间周期(包括可维护性和可测试性)的关键.它们更容易理解、更改和测试.</p><p>对服务进行松耦合并仅通过 API 进行协作的要求禁止服务通过数据库进行通信.您必须将服务的持久数据视为类的字段并保持它们的私有.保持数据私有使开发人员能够更改其服务的数据库模式, 而不必花费时间与处理其他服务的开发人员进行协调.不共享数据库表还可以改进运行时隔离.例如, 它确保一个服务不能持有阻塞另一个服务的数据库锁.但是, 稍后您将了解不共享数据库的一个缺点是, 维护数据一致性和跨服务查询更加复杂.</p><p><strong>共享库的角色</strong></p><p>开发人员通常将功能打包在一个库(模块)中, 以便多个应用程序可以重用它, 而无需重复代码.毕竟, 如果没有 Maven 或 npm 存储库, 我们今天会怎样呢? 您可能还想在微服务架构中使用共享库.从表面上看, 这似乎是减少服务中的代码重复的好方法.但是您需要确保不会意外地在服务之间引入耦合.</p><p>例如, 假设多个服务需要更新 <code>Order</code> 业务对象.一种方法是将该功能打包为多个服务使用的库.一方面, 使用库可以消除代码重复.另一方面, 考虑当需求影响 <code>Order</code> 业务对象的方式发生变化时会发生什么.您需要同时重新构建和部署这些服务.更好的方法是将可能更改的功能(如订单管理)实现为服务.</p><p>您应该努力将库用于不太可能更改的功能.例如, 在一个典型的应用程序中, 每个服务实现一个通用的 <code>Money</code> 类是没有意义的.相反, 您应该创建服务使用的库.</p><p><strong>服务规模通常不重要</strong></p><p><em>微服务</em>这个词的一个问题是, 你听到的第一件事就是<em>微小</em>.这意味着服务应该非常小.其他基于大小的术语, 如迷你型服务(miniservice)或 nanoservice 也是如此.实际上, 大小并不是一个有用的度量标准.</p><p>更好的目标是将设计良好的服务定义为能够由一个小团队开发的服务, 该团队的开发周期最短, 与其他团队的协作也最少.从理论上讲, 一个团队可能只负责一个服务, 所以服务绝不是<em>微小</em>的.相反, 如果一个服务需要一个大的团队或者需要很长的时间来测试, 那么将团队和服务分开可能是有意义的.或者, 如果您经常因为对其他服务的更改而需要更改某个服务, 或者如果它触发了其他服务中的更改, 则说明它不是松耦合的.您甚至可能构建一个分布式的单体.</p><p>微服务架构将应用程序构建为一组小的、松耦合的服务.因此, 它改进了开发时间属性-可维护性、可测试性、可部署性等等-并使组织能够更快地开发更好的软件.它还改进了应用程序的可伸缩性, 尽管这不是主要目标.要为应用程序开发微服务架构, 需要标识服务并确定它们如何协作.我们来看看怎么做.</p><h1 id="定义应用程序微服务架构"><a href="#定义应用程序微服务架构" class="headerlink" title="定义应用程序微服务架构"></a>定义应用程序微服务架构</h1><p>我们应该如何定义微服务架构? 与任何软件开发工作一样, 起点是编写需求, 希望是领域专家, 也许是现有的应用程序.与许多软件开发一样, 定义架构是一门艺术, 而不是科学.本节描述一个简单的三步流程, 如图 2.5 所示, 用于定义应用程序的架构.但是, 重要的是要记住, 这不是一个你可以机械地遵循的过程.它很可能是迭代的, 并涉及很多创造力.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.5_A_three-step_process_for_defining_an_application’s_microservice_architecture.jpg" alt="Figure 2.5 A three-step process for defining an application’s microservice architecture"></p><p>应用程序用于处理请求, 因此定义其架构的第一步是将应用程序的需求提取到关键请求中.但是, 我没有使用 REST 或消息传递等特定 IPC 技术来描述请求, 而是使用了更抽象的系统操作(system operation)概念.系统操作是应用程序必须处理的请求的抽象.它可以是更新数据的命令, 也可以是检索数据的查询.每个命令的行为都是根据抽象领域模型定义的, 抽象领域模型也是从需求派生出来的.系统操作成为说明服务如何协作的架构场景.</p><p>流程中的第二步是确定服务的分解.有几种策略可供选择.一种策略起源于业务架构的规程, 它定义与业务功能相对应的服务.另一种策略是围绕领域驱动的设计子领域组织服务.最终的结果是围绕业务概念而不是技术概念组织的服务.</p><p>定义应用程序架构的第三步是确定每个服务的 API.为此, 要将第一步中标识的每个系统操作分配给服务.服务可以完全独立地实现操作.或者, 它可能需要与其他服务协作.在这种情况下, 您将确定服务如何协作, 这通常需要服务来支持其他操作.您还需要决定我在第3 章中描述的实现每个服务的 API 的IPC 机制.</p><p>分解有几个障碍.首先是网络延迟.您可能会发现, 由于服务之间的往返(round-trips)太多, 特定的分解是不切实际的.分解的另一个障碍是服务之间的同步通信降低了可用性.您可能需要使用第 3 章中描述的自包含(self-contained)服务的概念.第三个障碍是跨服务维护数据一致性的需求.您通常需要使用在第 4 章中讨论的 sagas.分解的第四个也是最后一个障碍是所谓的 god 类, 它们在整个应用程序中使用.幸运的是, 您可以使用领域驱动设计的概念来消除 god 类.</p><p>本节首先描述如何标识应用程序的操作.之后, 我们将研究将应用程序分解为服务的策略和指导方针, 以及分解的障碍和如何解决它们.最后, 我将描述如何定义每个服务的 API.</p><h2 id="识别系统操作-system-operations"><a href="#识别系统操作-system-operations" class="headerlink" title="识别系统操作(system operations)"></a>识别系统操作(system operations)</h2><p>定义应用程序架构的第一步是定义系统操作.起点是应用程序的需求, 包括用户故事及其关联的用户场景(注意, 这些场景与架构场景不同).使用图 2.6 所示的两步流程标识和定义系统操作.这个过程的灵感来自 Craig Larman 的《应用 UML 和模式》(Prentice Hall, 2004)一书中介绍的面向对象设计过程(参见<a href="http://www.craiglarman.com/wiki/index.php?title=Book_Applying_UML_and_Patterns).第一步创建高层域模型" target="_blank" rel="noopener">http://www.craiglarman.com/wiki/index.php?title=Book_Applying_UML_and_Patterns).第一步创建高层域模型</a>, 该模型由提供用于描述系统操作的词汇表的关键类组成.第二步识别系统操作, 并根据域模型描述每个操作的行为.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.6_System_operations_are_derived_from_the_application’s_requirements_using_a_two-step_process.jpg" alt="Figure 2.6 System operations are derived from the application’s requirements using a two-step process"></p><p>领域模型主要来源于用户描述的名词, 系统操作主要来源于动词.您还可以使用一种称为事件风暴(Event Stroming)的技术来定义域模型, 我在第 5 章中讨论了这种技术.每个系统操作的行为是根据它对一个或多个域对象的影响以及它们之间的关系来描述的.系统操作可以创建、更新或删除域对象, 也可以创建或销毁它们之间的关系.</p><p>让我们来看看如何定义一个高级域模型.之后, 我将根据域模型定义系统操作.</p><p><strong>创建高级域模型</strong></p><p>定义系统操作过程的第一步是为应用程序勾画一个高级域模型.请注意, 这个域模型比最终要实现的要简单得多.应用程序甚至不会有单个域模型, 因为您很快就会了解到, 每个服务都有自己的域模型.尽管进行了极大的简化, 但是高层领域模型在这个阶段仍然很有用, 因为它定义了描述系统操作行为的词汇表.</p><p>领域模型是使用标准技术创建的, 例如分析故事和场景中的名词并与领域专家交流.考虑一下, 例如, <code>下订单(Place Order)</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></pre></td><td class="code"><pre><span class="line">给定一个消费者</span><br><span class="line">和一个餐厅</span><br><span class="line">以及该餐厅可提供的送货地址/时间</span><br><span class="line">以及满足餐厅最低订购量的总订购量</span><br><span class="line">当顾客为餐馆下订单时</span><br><span class="line">那么消费者的信用卡就被授权了</span><br><span class="line">并且订单是在 PENDING_ACCEPTANCE 状态下创建的</span><br><span class="line">订单与消费者关联</span><br><span class="line">订单与餐厅关联</span><br></pre></td></tr></table></figure><p>此用户场景中的名词暗示存在各种类, 包括 <code>Consumer</code>、<code>Order</code>、<code>Restaurant</code> 和 <code>CreditCard</code>.</p><p>类似地,<code>接受订单(Accept Order)</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></pre></td><td class="code"><pre><span class="line">给定处于 PENDING_ACCEPTANCE 状态的订单和可用于配送订单的快递员</span><br><span class="line">当餐馆接受订单时承诺在特定时间内准备好</span><br><span class="line">然后将订单的状态更改为 ACCEPTED</span><br><span class="line">订单的 promiseByTime 被更新为 promised 时间</span><br><span class="line">并且快递员被指定来配送订单</span><br></pre></td></tr></table></figure><p>这个场景表明存在 <code>Courier</code> 和 <code>Delivery</code> 类.经过几次迭代分析后的最终结果将是一个域模型, 该模型毫无疑问地由这些类和其他类, 如 <code>MenuItem</code> 和 <code>Address</code> 组成.图2.7 是显示关键类的类图.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.7_The_key_classes_in_the_FTGO_domain_model.jpg" alt="Figure 2.7 The key classes in the FTGO domain model"></p><p>每个类的职责如下:</p><ul><li><code>Consumer</code>-下单的消费者</li><li><code>Order</code>-由消费者下的订单.它描述了订单并追踪它的状态.</li><li><code>OrderLineItem</code>-订单中的一行条目.</li><li><code>DeliveryInfo</code>-配送订单的时间和地点.</li><li><code>Restaurant</code>-为顾客准备送货单的餐馆.</li><li><code>MenuItem</code>-餐厅菜单上的一个项目.</li><li><code>Courier</code>-向消费者配送订单的快递员.它跟踪快递员的可用性和他们当前的位置.</li><li><code>Address</code>-消费者或餐馆的地址.</li><li><code>Location</code>-快递员的纬度和经度.</li></ul><p>类图, 如图 2.7 所示, 说明了应用程序架构的一个方面.但如果没有动画场景, 它也不过是一张漂亮的图片.下一步是定义与架构场景相对应的系统操作.</p><p><strong>定义系统操作</strong><br>定义了高层域模型之后, 下一步是确定应用程序必须处理的请求.UI 的细节超出了本书的范围, 但是您可以想象, 在每个用户场景中, UI 都将请求后端业务逻辑来检索和更新数据.FTGO 主要是一个 web 应用程序, 这意味着大多数请求都是基于 HTTP 的, 但也有可能一些客户端会使用消息传递.因此, 与其使用特定的协议, 不如使用更抽象的系统操作概念来表示请求.</p><p>有两种类型的系统操作:</p><ul><li><code>命令(Commands)</code>-创建、更新和删除数据的系统操作</li><li><code>查询(Queries)</code>-读取(查询)数据的系统操作</li></ul><p>最终, 这些系统操作将与 REST、RPC 或消息传递端点相对应, 但目前抽象地考虑它们是有用的.让我们首先识别一些命令.</p><p>识别系统命令的一个很好的起点是分析用户故事和场景中的动词.例如, 考虑 <code>Place Order</code> 的故事.显然, 系统必须提供一个 <code>Create Order</code> 的操作.许多其他故事单独映射到系统命令.表 2.1 列出了一些关键的系统命令.</p><p><strong>表 2.1 FTGO 应用程序的关键系统命令</strong></p><table><thead><tr><th>参与者</th><th>故事</th><th>命令</th><th>描述</th></tr></thead><tbody><tr><td>消费者</td><td>创建订单</td><td><code>createOrder()</code></td><td>创建一个订单</td></tr><tr><td>餐厅</td><td>接受订单</td><td><code>acceptOrder()</code></td><td>表示餐厅已接受订单, 并承诺在指定时间内准备好</td></tr><tr><td>餐厅</td><td>准备就绪的订单</td><td><code>noteOrderReadyForPickup()</code></td><td>表明订单已经准备就绪</td></tr><tr><td>快递员</td><td>更新位置</td><td><code>noteUpdatedLocation()</code></td><td>更新快递员的当前位置</td></tr><tr><td>快递员</td><td>送货上门</td><td><code>noteDeliveryPickedUp()</code></td><td>表示快递员在将订单送货上门</td></tr><tr><td>快递员</td><td>配送完成</td><td><code>noteDeliveryDelivered()</code></td><td>表示快递员已送达订单</td></tr></tbody></table><p>命令具有一个规范, 该规范根据域模型类定义其参数、返回值和行为.行为规范由调用操作时必须为 true 的前置条件和调用操作后为 true 的后置条件组成.例如, 下面是 <code>createOrder()</code> 系统操作的规范:</p><table><thead><tr><th>系统操作</th><th>规范</th></tr></thead><tbody><tr><td>操作</td><td><code>createOrder</code>(consumer id, payment method, delivery address, delivery time, restaurant id, order line items)</td></tr><tr><td>返回</td><td><code>orderId</code>, …</td></tr><tr><td>前置条件</td><td>1. 消费者存在并且可以下单 <br> 2. 行项目和餐厅的菜单项对应 <br> 3. 配送地址和时间在餐厅的服务范围内</td></tr><tr><td>后置条件</td><td>1. 消费者的信用卡被授权支付订单总额 <br> 2. 订单被创建, 状态为 <code>PENDING_ACCEPTANCE</code></td></tr></tbody></table><p>这些先决条件反映了前面描述的 <code>Place Order</code> 用户场景中的 <code>给定(given)</code> 项.后置条件反映了场景中的 <code>然后(then)</code>.当调用系统操作时, 它将验证前置条件, 并执行使后置条件为 true 所需的操作.</p><p>下面是 <code>acceptOrder()</code> 系统操作的规范:</p><table><thead><tr><th>系统操作</th><th>规范</th></tr></thead><tbody><tr><td>操作</td><td><code>acceptOrder(restaurantId, orderId, readyByTime)</code></td></tr><tr><td>返回</td><td>-</td></tr><tr><td>前置条件</td><td>1. <code>订单状态(order.status)</code> 是 <code>等待接受(PENDING_ACCEPTANCE)</code>. <br> 2. 快递员可以配送订单.</td></tr><tr><td>后置条件</td><td>1. <code>订单状态(order.status)</code>变为 <code>接受了(ACCEPTED)</code>. <br> 2. <code>订单准备时间(order.readyByTime)</code>变为 <code>准备时间(readyByTime)</code>. <br> 3. 快递员被分配去配送订单.</td></tr></tbody></table><p>它的前置和后置条件反映了前面的用户场景.</p><p>大多数与架构相关的系统操作是命令.不过, 有时检索数据的查询也很重要.</p><p>除了实现命令之外, 应用程序还必须实现查询.查询为 UI 提供了用户决策所需的信息.在这个阶段, 我们并没有为 FTGO 应用程序设计一个特定的 UI, 但是考虑一下, 例如, 当消费者下订单时的流程:</p><ol><li>用户输入送货地址和时间.</li><li>系统展示可配送的餐馆.</li><li>用户选择餐馆.</li><li>系统展示菜单.</li><li>用户选择商品并付款.</li><li>系统创建订单.</li></ol><p>此用户场景建议以下查询:</p><ul><li><code>findAvailableRestaurants(deliveryAddress, deliveryTime)</code>-检索能够在指定时间送达指定地址的餐厅.</li><li><code>findRestaurantMenu(id)</code>-检索包含菜单项的餐厅信息.</li></ul><p>在这两个查询中, <code>findAvailableRestaurants()</code> 可能是架构上最重要的查询.这是一个涉及地理搜索(geosearch)的复杂查询.查询的地理搜索组件包括查找所有的点-餐馆-在一个位置附近-送货地址.它还会过滤掉那些需要准备和取餐时关门的餐厅.此外, 性能非常关键, 因为只要消费者想要下订单, 就会执行此查询.</p><p>高层领域模型和系统操作捕获应用程序所做的事情.它们有助于驱动应用程序架构的定义.每个系统操作的行为都用域模型来描述的.每个重要的系统操作代表一个架构上重要的场景, 这是架构描述的一部分.</p><p>定义了系统操作之后, 下一步是识别应用程序的服务.正如前面提到的, 没有一个机械化的过程可以遵循.然而, 您可以使用各种分解策略.每一种方法都从不同的角度解决问题, 并使用自己的术语.但是对于所有的策略, 最终的结果是相同的: 由服务组成的架构主要围绕业务而不是技术概念进行组织.</p><p>让我们看看第一个策略, 它定义了与业务功能相对应的服务.</p><h2 id="通过应用按业务能力分解模式定义服务"><a href="#通过应用按业务能力分解模式定义服务" class="headerlink" title="通过应用按业务能力分解模式定义服务"></a>通过应用按业务能力分解模式定义服务</h2><p>创建微服务架构的一种策略是按业务能力分解.业务架构建模中的概念, 业务能力是业务为产生价值所做的事情.给定业务的功能集取决于业务类型.例如, 保险公司的功能通常包括承保、索赔管理、计费、合规等等.在线商店的功能包括订单管理、库存管理、配送等.</p><blockquote><p><strong>模式: 按照业务功能分解</strong><br>根据业务能力定义服务.见 <a href="http://microservices.io/patterns/decomposition/decompose-by-business-capability.html" target="_blank" rel="noopener">http://microservices.io/patterns/decomposition/decompose-by-business-capability.html</a></p></blockquote><p><strong>业务功能定义了组织的工作</strong></p><p>组织的业务能力捕捉组织的业务是什么.它们通常是稳定的, 而不是组织如何管理业务, 组织的业务随时间而变化, 有时变化很大.随着越来越多的人使用技术实现许多业务流程的自动化, 这一点在如今尤其明显.例如, 不久前你把支票交给出纳员存入银行.后来, 用 ATM 存入支票成为可能.如今, 你可以用智能手机方便地存入大多数支票.如您所见, 存款支票业务功能一直保持稳定, 但其实现方式已发生了巨大变化.</p><p><strong>识别业务功能</strong></p><p>通过分析组织的目的、结构和业务流程, 确定组织的业务功能.每个业务功能都可以看作是服务, 除了它是面向业务的, 而不是面向技术的.它的规范由各种组件组成, 包括输入、输出和服务级别协议.例如, 保险承保能力的输入是消费者的申请, 输出包括批准和价格.</p><p>业务能力通常关注于特定的业务对象.例如, 索赔业务对象是索赔管理功能的焦点.功能通常可以分解为子功能, 例如, 索赔管理功能有几个子功能, 包括索赔信息管理、索赔审查和索赔支付管理.</p><p>不难想象, FTGO 的业务能力包括:</p><ul><li>供应商管理<ul><li><em>快递员管理</em>-管理快递员信息</li><li><em>餐厅信息管理</em>-管理餐厅的菜单和其他信息, 包括位置和营业时间.</li></ul></li><li>消费者管理-管理消费者的信息</li><li>订单接收和履行<ul><li><em>订单管理</em>-允许消费者创建和管理订单</li><li><em>餐厅订单管理</em>-管理餐厅订单的准备工作</li><li><em>物流</em></li><li><em>快递可用性管理</em>-管理快递员对送货单的实时可用性</li><li><em>配送管理</em>-给消费者配送订单</li></ul></li><li>会计核算<ul><li><em>消费者会计核算</em>-管理消费者的帐单</li><li><em>餐厅会计核算</em>-管理参订的支付信息</li><li><em>快递员会计核算</em>-管理给快递员的支付信息</li></ul></li></ul><p>顶级功能包括供应商管理、消费者管理、订单接收和履行以及会计核算.可能还会有许多其他顶级功能, 包括与营销相关的功能.大多数顶级功能被分解为子功能.例如, 接受订单和履行被分解为五个子功能.</p><p>有趣的是, 这种能力层次结构有三种与餐厅相关的能力: 餐厅信息管理、餐厅订单管理和餐厅会计核算.这是因为它们代表了餐厅运营的三个非常不同的方面.</p><p>下面我们将看下如何用业务功能来定义服务.</p><p><strong>从业务功能到服务</strong></p><p>一旦确定了业务功能, 就可以为每个功能或相关功能组定义服务.图 2.8 显示了 FTGO 应用程序从功能到服务的映射.一些顶级功能, 例如会计核算功能, 被映射到服务.在其他情况下, 子功能映射到服务.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.8_Mapping_FTGO_business_capabilities_to_services.jpg" alt="Figure 2.8 Mapping FTGO business capabilities to services"></p><p>决定将哪个级别的功能层次结构映射到服务, 因为这在某种程度上是主观的.我对这种特定映射的理由如下:</p><ul><li>我将供应商管理的子功能映射到两个服务, 因为餐厅和快递员是非常不同类型的供应商.</li><li>我将接受订单和履行的功能映射到三个服务, 每个服务负责流程的不同阶段.我将快递员可用性管理和配送管理功能结合在一起, 并将它们映射到单个服务, 因为它们是紧密交织在一起的.</li><li>我将会计核算功能映射到它自己的服务, 因为不同类型的会计核算看起来很相似.</li></ul><p>稍后, 将(餐馆和快递员的)支付与(消费者的)账单分开可能是有意义的.</p><p>围绕功能组织服务的一个关键优点是, 由于它们是稳定的, 因此产生的架构也相对稳定.架构的各个组件可能随着业务方面的变化而演进, 但是架构仍然保持不变.</p><p>尽管如此, 重要的是要记住图 2.8 所示的服务仅仅是定义架构的第一次尝试.随着我们对应用领域的了解越来越多, 它们可能会随着时间而发展.特别是, 架构定义过程中的一个重要步骤是研究服务如何在每个关键架构服务中协作.例如, 您可能会发现由于进程间通信过多, 导致关节式分解是低效的, 您必须组合服务.相反, 服务的复杂性可能会增长到值得将其拆分为多个服务的程度.更重要的是, 在 2.2.5 节中, 我描述了分解的几个障碍, 它们可能会导致您重新考虑您的决策.</p><p>让我们看看另一种分解基于领域驱动设计的应用程序的方法.</p><h2 id="通过应用子域模式分解来定义服务"><a href="#通过应用子域模式分解来定义服务" class="headerlink" title="通过应用子域模式分解来定义服务"></a>通过应用子域模式分解来定义服务</h2><p>DDD, 正如 Eric Evans(Addison-Wesley Professional, 2003)在他的优秀著作《领域驱动设计》中描述的那样, 是一种构建以面向对象领域模型开发为中心的复杂软件应用程序的方法.<em>域模式(domain mode)</em> 以可用于解决该域内问题的形式捕获关于该域的知识.它定义了团队使用的词汇表, DDD 称之为 <em>通用语言(Ubiquitous Language)</em>.领域模型紧密地反映在应用程序的设计和实现中.在应用微服务架构时, DDD 有两个非常有用的概念: 子域(subdomains)和有界上下文(bounded contexts).</p><blockquote><p><strong>模式: 通过子域分解</strong><br>根据 DDD 子域定义服务, 详见 <a href="http://microservices.io/patterns/decomposition/decompose-by-subdomain.html" target="_blank" rel="noopener">http://microservices.io/patterns/decomposition/decompose-by-subdomain.html</a>.</p></blockquote><p>DDD 与传统的企业建模方法有很大的不同, 后者为整个企业创建一个单独的模型.例如, 在这种模型中, 每个业务实体(如客户、订单等)都有一个单独的定义.这种建模的问题在于, 让组织的不同部分同意一个模型是一项艰巨的任务.此外, 它还意味着从组织的给定部分的角度来看, 模型对于他们的需求过于复杂.此外, 领域模型可能令人混淆, 因为组织的不同部分可能对不同的概念使用相同的术语, 也可能对相同的概念使用不同的术语.DDD 通过定义多个领域模型来避免这些问题, 每个模型都具有显式的作用域.</p><p>DDD 为每个子域定义了一个单独的领域模型.子域是<em>域</em>的一部分, DDD 术语表示应用程序的问题空间.子领域的识别使用与识别业务功能相同的方式: 分析业务并识别不同的专业领域.最终的结果很可能是类似于业务功能的子域.FTGO 中的子域示例包括订单接收、订单管理、厨房管理、配送和财务.正如您所看到的, 这些子域与前面描述的业务功能非常相似.</p><p>DDD 将域模型的范围称为<em>有界上下文(bounded context)</em>.有界上下文包括实现模型的代码工件.在使用微服务架构时, 每个有界上下文都是一个服务或一组服务.我们可以通过应用 DDD 并为每个子域定义服务来创建微服务架构.图 2.9 显示了子域如何映射到服务, 每个子域都有自己的域模型.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.9_From_subdomains_to_services.jpg" alt="Figure 2.9 From subdomains to services"></p><p>DDD 和微服务架构几乎完全一致.子域和有界上下文的 DDD 概念很好地映射到微服务架构中的服务.此外, 微服务架构拥有服务的自治团队的概念完全符合 DDD 的概念, 即每个领域模型由单个团队拥有和开发.更好的是, 正如我在本节后面所描述的, 具有自己的域模型的子域的概念是消除 god 类从而使分解更容易的一种很好的方法.</p><p>按子域分解和按业务能力分解是定义应用程序微服务架构的两种主要模式.然而, 分解有一些有用的指导原则, 它们的根源在于面向对象的设计.让我们来看一看.</p><h2 id="分解指导原则"><a href="#分解指导原则" class="headerlink" title="分解指导原则"></a>分解指导原则</h2><p>到目前为止, 我们已经了解了定义微服务架构的主要方法.在应用微服务架构模式时, 我们还可以采用和使用面向对象设计的一些原则.这些原则由 Robert C. Martin 创建的, 并在他的经典著作《使用 Booch 方法设计面向对象的 C++ 应用程序》(Prentice Hall, 1995)中进行了描述.第一个原则是单一职责原则(SRP), 用于定义类的职责.第二个原则是共同封闭原则(CCP), 用于将类组织到包中.让我们看看这些原则, 并看看如何将它们应用于微服务架构.</p><p><strong>单一职责原则</strong><br>软件架构和设计的主要目标之一是确定每个软件元素的职责.单一职责原则如下:</p><blockquote><p>一个类应该只有一个更改的原因.<br><br> Robert C. Martin</p></blockquote><p>类所具有的每个职责都是该类更改的潜在原因.如果一个类有多个独立更改的职责, 那么这个类就不会是稳定的.通过遵循SRP, 您可以定义每个类都有一个职责, 因此也就有了进行更改的一个原因.</p><p>我们可以在定义微服务体系结构时应用 SRP, 并创建小型、内聚的服务, 每个服务都有一个职责.这将减少服务的规模并增加其稳定性.新的 FTGO 架构是 SRP 运行的一个例子.将食物送到消费者手中的每一个方面-订单接收、订单准备和配送-都是独立服务的责任.</p><p><strong>共同封闭原则</strong><br>另一个有用的原则是共同封闭原则:</p><p>另一个有用的原则是共同封闭原则:</p><blockquote><p>包中的类应该针对相同类型的更改聚集在一起.影响包的更改会影响包中的所有类.<br><br> Robert C. Martin </p></blockquote><p>其思想是, 如果两个类因为相同的基本原因而同步改变, 那么它们属于同一个包.例如, 这些类可能实现特定业务规则的不同方面.它的目标是, 当业务规则发生更改时, 开发人员只需要更改少量包中的代码(理想情况下只需要更改一个包).坚持 CCP 可以显著提高应用程序的可维护性.</p><p>我们可以在创建微服务架构和包组件时应用 CCP, 这些组件出于相同的原因更改为相同的服务.这样做将最小化在某些需求发生更改时需要更改和部署的服务数量.理想情况下, 更改只会影响单个团队和单个服务.CCP 是分布式单体反模式的解毒剂.</p><p>SRP 和 CCP 是 Bob Martin 提出的 11 条原则中的 2 条.它们在开发微服务架构时特别有用.其余九条原则用于设计类和包.有关 SRP、CCP 和其他 OOD 原则的更多信息, 请参见 Bob Martin 网站上的文章 “面向对象设计的原则”(<a href="http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod)" target="_blank" rel="noopener">http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod)</a>.</p><p>按业务能力和按子域分解以及 SRP 和 CCP 是将应用程序分解为服务的良好技术.为了应用它们并成功地开发微服务架构, 您必须解决一些事务管理和进程间通信问题.</p><h2 id="将应用程序分解为服务的障碍"><a href="#将应用程序分解为服务的障碍" class="headerlink" title="将应用程序分解为服务的障碍"></a>将应用程序分解为服务的障碍</h2><p>从表面上看, 通过定义与业务功能或子域相对应的服务来创建微服务架构的策略看起来很简单.然而, 你可能会遇到一些障碍:</p><ul><li>网络延迟</li><li>同步通信降低了可用性</li><li>维护跨服务数据一致性</li><li>获得数据的一致视图</li><li>阻止分解的 God 类</li></ul><p>让我们看看每个障碍, 从网络延迟开始.</p><p><strong>网络延迟</strong><br><em>网络延迟</em>是分布式系统中一直存在的问题.您可能会发现, 对服务的特定分解会导致两个服务之间的大量往返(round-trips).有时, 您可以通过实现一个批处理 API 来在一次往返中获取多个对象, 从而将延迟降低到可接受的程度.但在其他情况下, 解决方案是组合服务, 用语言级别的方法或函数调用替换昂贵的 IPC(进程间通信).</p><p><strong>同步通信降低了可用性</strong><br>另一个问题是如何以不降低可用性的方式实现服务间通信.例如, 实现 <code>createOrder()</code> 操作最简单的方法是让 <code>订单服务</code> 使用 REST 同步调用其他服务, 使用像 REST 这样的协议的缺点是它降低了 <code>订单服务</code> 的可用性.如果其他服务不可用, 它将无法创建订单.有时这是一个值得权衡的问题, 但是在第 3 章中, 您将了解到使用异步消息传递(消除紧密耦合并提高可用性)通常是更好的选择.</p><p><strong>维护跨服务数据一致性</strong><br>另一个挑战是维护跨服务数据一致性.一些系统操作需要更新多个服务中的数据.例如, 当餐馆接收订单时, <code>厨房服务(Kitchen Service)</code>和<code>配送服务(Delivery Service)</code>都必须进行更新.<code>厨房服务</code>改变了票据的状态.<code>配送服务</code>安排了订单的配送.这两个更新都必须以原子方式完成.</p><p>传统的解决方案是使用基于提交的两阶段分布式事务管理机制.但是正如您将在第 4 章中看到的, 对于现代应用程序来说, 这不是一个好的选择, 您必须使用一种非常不同的事务管理方法, 这就是 saga.<code>saga</code> 是使用消息传递进行协调的一系列本地事务.Saga 比传统的 ACID 事务更复杂, 但在许多情况下都能很好地工作.Saga 的一个限制是它们最终是一致的.如果您需要以原子方式更新某些数据, 那么它必须驻留在单个服务中, 这可能成为分解的障碍.</p><p><strong>获得数据的一致视图</strong><br>分解的另一个障碍是无法跨多个数据库获得真正一致的数据视图.在单体应用程序中, ACID 事务的属性保证查询将返回数据库的一致视图.相反, 在微服务架构中, 即使每个服务的数据库是一致的, 您也无法获得数据的全局一致视图.如果您需要一些数据的一致视图, 那么它必须驻留在单个服务中, 这可能妨碍分解.幸运的是, 在实践中这很少是一个问题.</p><p><strong>阻止分解的 God 类</strong><br>分解的另一个障碍是所谓的 God 类的存在.God 类是整个应用程序中使用的臃肿类(<a href="http://wiki.c2.com/?GodClass).God" target="_blank" rel="noopener">http://wiki.c2.com/?GodClass).God</a> 类通常为应用程序的许多不同方面实现业务逻辑.它通常有大量字段映射到具有许多列的数据库表.大多数应用程序至少具有这些类中的一个, 每个类代表一个对该领域至关重要的概念: 银行帐户、电子商务订单、保险策略等等.因为 God 类将应用程序的许多不同方面的状态和行为捆绑在一起, 所以要将使用它的任何业务逻辑拆分为服务, 它是不可逾越的障碍.</p><p><code>Order</code> 类是 FTGO 应用程序中 God 类的一个很好的例子.这并不奇怪-毕竟, FTGO 的目的是向客户发送食品订单.系统的很多方面都涉及到订单.如果 FTGO 应用程序只有一个领域模型, 那么 <code>Order</code> 类将是一个非常大的类.它将具有与应用程序的许多不同部分相对应的状态和行为.图 2.10 显示了将使用传统建模技术创建的该类的结构.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.10_The_Order_god_class_is_bloated_with_numerous_responsibilities..jpg" alt="Figure 2.10 The Order god class is bloated with numerous responsibilities."></p><p>如您所见, <code>Order</code> 类具有与订单处理、餐厅订单管理、配送和支付相对应的字段和方法.这个类还有一个复杂的状态模型, 因为一个模型必须描述来自应用程序不同部分的状态转换.在当前形式下, 该类使得将代码分割为服务变得极其困难.</p><p>一种解决方案是将 <code>Order</code> 类打包到一个库中, 并创建一个中央订单数据库.处理订单的所有服务都使用这个库并访问数据库.这种方法的问题在于, 它违反了微服务架构的关键原则之一, 并导致了不受欢迎的紧密耦合.例如, 对 <code>Order</code> schema 的任何更改都需要团队同步更新代码.</p><p>另一种解决方案是将 <code>订单</code> 数据库封装在 <code>订单服务</code> 中, 其他服务调用该订单服务来检索和更新订单.这种设计的问题是订单服务将是一个数据服务, 其贫血领域模型包含很少或没有业务逻辑.这两个选项都不吸引人, 但幸运的是, DDD 提供了一个解决方案.</p><p>一种更好的方法是应用 DDD, 并将每个服务作为一个单独的子域.并使用其自己的领域模型.这意味着 FTGO 应用程序中与订单相关的每个服务都有自己的领域模型和 <code>Order</code> 类的版本.多领域模型好处的一个很好的例子是配送服务.它对订单的视图, 如图 2.11 所示, 非常简单: 取货地址、取货时间、送货地址和送货时间.而且, 配送服务使用更合适的配送名称, 而不是将其称为订单.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.11_The_Delivery_Service_domain_model.jpg" alt="Figure 2.11 The Delivery Service domain model"></p><p>配送服务对订单的任何其他属性都不感兴趣.</p><p><code>厨房服务(Kitchen Service)</code>也有一个更简单的订单视图.它的 <code>订单</code> 版本称为票据(Ticket).如图 2.12 所示, 票据只是由一个状态、<code>requestedDeliveryTime</code>、一个 <code>prepareByTime</code> 和一个告诉餐馆准备内容的行项目列表组成.它不关心消费者、支付、配送等等.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.12_The_Kitchen_Service_domain_model.jpg" alt="Figure 2.12 The Kitchen Service domain model"></p><p><code>订单</code> 服务具有订单最复杂的视图, 如图 2.13 所示.尽管它有很多字段和方法, 但仍然比原来的版本简单得多.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第2章-分解策略/Figure_2.13_The_Order_Service_domain_model.jpg" alt="Figure 2.13 The Order Service domain model"></p><p>每个领域模型中的 <code>Order</code> 类代表同一个 <code>Order</code> 业务实体的不同方面.FTGO 应用程序必须在不同服务中的这些不同对象之间保持一致性.例如, 一旦订单服务授权了消费者的信用卡, 它就必须在厨房服务中触发票据的创建.类似地, 如果餐厅通过厨房服务拒绝订单, 则必须在订单服务服务中取消订单, 并将客户记入账单服务中.在第 4 章中, 您将学习如何使用前面提到的事件驱动机制 saga 来维护服务之间的一致性.</p><p>在创建技术挑战的同时, 拥有多个领域模型也会影响用户体验的实现.应用程序必须在用户体验(即它自己的域模型)和每个服务的域模型之间进行转换.例如, 在 FTGO 应用程序中, 显示给消费者的订单状态来自存储在多个服务中的订单信息.这种转换通常由 API 网关处理, 在第 8 章中讨论.尽管存在这些挑战, 但是在定义微服务体系结构时, 确定和消除 god 类是非常重要的.</p><p>现在我们来看看如何定义服务 API.</p><h2 id="定义服务-API"><a href="#定义服务-API" class="headerlink" title="定义服务 API"></a>定义服务 API</h2><p>到目前为止,我们有一个系统操作列表和一个潜在服务列表.下一步是定义每个服务的 API: 其操作和事件.服务 API 操作存在的原因有两个: 一些操作与系统操作相对应.它们由外部客户端调用, 也可能由其他服务调用.其他操作的存在是为了支持服务之间的协作.这些操作仅由其他服务调用.</p><p>服务发布事件主要是为了使其能够与其他服务协作.第 4 章描述了如何使用事件来实现 sagas, 从而维护跨服务的数据一致性.第 7 章讨论了如何使用事件更新支持高效查询的 CQRS 视图.应用程序还可以使用事件通知外部客户端.例如, 它可以使用 WebSockets 向浏览器传递事件.</p><p>定义服务 API 的起点是将每个系统操作映射到服务.然后, 我们决定服务是否需要与其他人协作来实现系统操作.如果需要协作, 那么我们将确定这些其他服务必须提供哪些 API 来支持协作.让我们首先看看如何将系统操作分配给服务.</p><p><strong>将系统操作分配给服务</strong><br>第一步是决定哪个服务是请求的初始入口点.许多系统操作整齐地映射到服务, 但有时映射不太明显.例如, 可以考虑<code>noteUpdatedLocation()</code> 操作, 该操作更新快递位置.一方面, 因为它与快递有关, 所以这个操作应该分配给<code>快递服务(Courier service)</code>.另一方面, <code>配送服务(Delivery Service)</code>需要快递位置.在这种情况下, 将操作分配给需要操作提供的信息的服务是一个更好的选择.在其他情况下, 将操作分配给具有处理操作所需信息的服务可能是有意义的.</p><p>表 2.2 显示了 FTGO 应用程序中的哪些服务负责哪些操作.</p><p><strong>将系统操作映射到 FTGO 应用程序中的服务</strong></p><table><thead><tr><th>服务</th><th>操作</th></tr></thead><tbody><tr><td>消费者服务</td><td>createConsumer()</td></tr><tr><td>订单服务</td><td>createOrder()</td></tr><tr><td>餐厅服务</td><td>findAvailableRestaurants()</td></tr><tr><td>厨房服务</td><td>acceptOrder() <br> noteOrderReadyForPickup()</td></tr><tr><td>配送服务</td><td>noteUpdatedLocation() <br> noteDeliveryPickedUp() <br> noteDeliveryDelivered()</td></tr></tbody></table><p>将操作分配给服务之后, 下一步是决定服务如何协作以处理每个系统操作.</p><p><strong>确定支持服务之间协作所需的 API</strong><br>有些系统操作完全由一个服务处理.例如, 在 FTGO 应用程序中, <code>消费者服务</code>完全独立地处理 <code>createConsumer()</code> 操作.但是其他系统操作跨越多个服务.例如, 处理其中一个请求所需的数据可能分散在多个服务中.例如, 为了实现 <code>createOrder()</code> 操作, <code>订单服务</code>必须调用以下服务, 以验证其前置条件并使后置条件变为真:</p><ul><li><code>消费者服务</code>-验证消费者是否能够下订单并获得其付款信息.</li><li><code>餐厅服务</code>-确认订单行项目, 确认送货地址/时间在餐厅服务区域内, 验证是否满足订单最低要求, 获取订单行项目价格.</li><li><code>厨房服务</code>-创建<code>票据(Ticket)</code>.</li><li><code>会计服务</code>-授权消费者的信用卡。</li></ul><p>类似地, 为了实现 <code>acceptOrder()</code> 系统操作, <code>厨房服务</code>必须调用<code>配送服务</code>来安排快递员配送订单.表 2.3 显示了服务、修改后的 API 和协作者.为了完全定义服务 API, 您需要分析每个系统操作并确定需要什么协作.</p><p><strong>表 2.3 服务及其修订的 API 和协作者</strong></p><table><thead><tr><th>服务</th><th>操作</th><th>协作者</th></tr></thead><tbody><tr><td>消费者服务</td><td>verifyConsumerDetails()</td><td>-</td></tr><tr><td>订单服务</td><td>createOrder()</td><td>消费服务 verifyConsumerDetails() <br> 餐厅服务 verifyOrderDetails() <br> 厨房服务 createTicket() <br> 会计服务 authorizeCard()</td></tr><tr><td>餐厅服务</td><td>findAvailableRestaurants() <br> verifyOrderDetails()</td><td>- </td></tr><tr><td>厨房服务</td><td>createTicket() <br> acceptOrder() <br> noteOrderReadyForPickup()</td><td>配送服务 scheduleDelivery()</td></tr><tr><td>配送服务</td><td>scheduleDelivery() <br> noteUpdatedLocation() <br> noteDeliveryPickedUp() <br> noteDeliveryDelivered()</td><td>-</td></tr><tr><td>会计服务</td><td>authorizeCard()</td><td>-</td></tr></tbody></table><p>到目前为止, 我们已经标识了每个服务实现的服务和操作.但重要的是要记住, 我们所描绘的架构是非常抽象的.我们没有选择任何特定的 IPC 技术.此外, 尽管术语<code>operation</code> 暗示了某种基于同步请求/响应的 IPC 机制, 但是您将看到异步消息传递扮演了重要角色.在本书中, 我描述了影响这些服务如何协作的架构和设计概念.</p><p>第 3 章描述了特定的 IPC 技术, 包括同步通信机制(如 REST)和使用消息代理的异步消息传递.我将讨论同步通信如何影响可用性,并介绍自包含服务的概念, 该概念不同步调用其他服务.实现自包含服务的一种方法是使用第 7 章介绍的 CQRS 模式.例如, <code>订单服务</code>可以维护<code>餐厅服务</code>拥有的数据的副本, 以便消除同步调用<code>餐厅服务</code>来验证订单的需要.它通过订阅<code>餐厅服务</code>发布的事件来保持副本的最新更新.</p><p>第 4 章介绍了 saga 的概念, 以及如何使用异步消息传递来协调参与该 saga 的服务.除了可靠地更新分散在多个服务中的数据之外, saga 也是实现自包含服务的一种方法.例如, 我描述了如何使用 saga 实现 <code>createOrder()</code> 操作, 该操作使用异步消息传递调用服务, 如消费者服务、厨房服务和会计服务.</p><p>第 8 章描述了 API 网关的概念, 它向外部客户端公开 API.API 网关可以使用 API 组合模式(见第 7 章)实现查询操作, 而不是简单地将其路由到服务.API 网关中的逻辑通过调用多个服务并组合结果来收集查询所需的数据.在这种情况下, 系统操作被分配给 API 网关, 而不是服务.服务需要实现 API 网关所需的查询操作.</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>架构决定了应用程序的灵活性, 包括可维护性、可测试性和可部署性, 它们直接影响开发速度.</li><li>微服务架构是一种架构风格, 它使应用程序具有很高的可维护性、可测试性和可部署性.</li><li>微服务架构中的服务是围绕业务关注点(业务功能或子领域)组织的, 而不是围绕技术关注点.</li><li>有两种模式用于分解:<ul><li>业务能力分解, 业务能力分解起源于业务架构</li><li>基于领域驱动设计的概念, 按子域分解</li></ul></li><li>通过应用 DDD 并为每个服务定义一个单独的领域模型, 您可以消除 god 类, 这些类会导致防止分解的复杂依赖关系.</li></ul>]]></content>
<summary type="html">
<p>这章包含:</p>
<ul>
<li>理解软件架构及其重要性</li>
<li>通过应用按业务能力分解和按子域分解的分解模式, 将应用程序分解为服务</li>
<li>用来自领域驱动设计(DDD)的有界上下文(bounded context)概念来理清数据并使分解更容易</
</summary>
<category term="微服务" scheme="https://ykgarfield.github.io/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
<category term="Microservice-Patterns(译)" scheme="https://ykgarfield.github.io/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/Microservice-Patterns-%E8%AF%91/"/>
<category term="微服务" scheme="https://ykgarfield.github.io/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
<category term="微服务模式" scheme="https://ykgarfield.github.io/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A8%A1%E5%BC%8F/"/>
</entry>
<entry>
<title>第1章-逃离单体噩梦</title>
<link href="https://ykgarfield.github.io/2019/01/05/%E5%BE%AE%E6%9C%8D%E5%8A%A1/Microservice-Patterns(%E8%AF%91)/%E7%AC%AC1%E7%AB%A0-%E9%80%83%E7%A6%BB%E5%8D%95%E4%BD%93%E5%99%A9%E6%A2%A6/"/>
<id>https://ykgarfield.github.io/2019/01/05/微服务/Microservice-Patterns(译)/第1章-逃离单体噩梦/</id>
<published>2019-01-04T16:00:00.000Z</published>
<updated>2019-02-13T14:43:34.193Z</updated>
<content type="html"><![CDATA[<p>本章包含:</p><ul><li>单体噩梦(monolithic hell)的症状以及如何通过采用微服务架构来逃离它</li><li>微服务架构的本质特征及其优缺点</li><li>微服务是如何支持 DevOps 风格的大型复杂应用程序的开发</li><li>微服务架构模式语言以及为什么要使用它</li></ul><p>那只是周一的午餐时间, 但是 Food to Go(FTGO) 公司的首席技术官 Mary 已经感到沮丧了.她的一天开始得很好.在上一周的一次优秀会议上, 她与其他软件架构师和开发人员一起学习了最新的软件开发技术, 包括持续部署和微服务架构.Mary 还和她以前在北卡罗莱纳A&T州(North Carolina A&T State)的计算机科学同学见面,分享了技术领导战争的故事.这次会议让她感到自己被赋予了权力, 并渴望改进 FTGO 开发软件的方式.</p><p>不幸的是, 这种感觉很快就消失了.她刚回到办公室的第一天早上, 又一次痛苦地与高级工程师和商务人士会面.他们花了两个小时讨论为什么开发团队会错过另一个关键的发布日期.可悲的是, 这种会议在过去几年里越来越普遍.尽管采用了敏捷, 开发的速度却在减慢, 几乎不可能实现业务目标.更糟糕的是, 似乎没有一个简单的解决方案.</p><p> 会议让 Mary 意识到 FTGO 正遭受着 <em>单体噩梦(monolithic hell)</em> 的折磨, 解决之道就是采用微服务架构.但是在会议上描述的微服务架构和相关的最先进的软件开发实践感觉像是一个难以实现的梦想.Mary 不清楚她如何在改善 FTGO 软件开发方式的同时扑灭今天的大火.</p><p>幸运的是, 正如你将在这本书中学到的, 有一个方法.但首先, 让我们看看 FTGO 面临的问题以及它们是如何实现的.</p><h1 id="缓慢地走向单体噩梦"><a href="#缓慢地走向单体噩梦" class="headerlink" title="缓慢地走向单体噩梦"></a>缓慢地走向单体噩梦</h1><p>自 2005 年末推出以来, FTGO 的发展突飞猛进.如今, 它是美国领先的在线食品递送公司之一.该公司甚至计划向海外扩张, 尽管由于在实现必要功能方面的拖延, 这些计划处于危险之中.</p><p>FTGO 应用程序的核心非常简单.消费者使用 FTGO 网站或移动应用程序在当地餐馆下单.FTGO 协调一个运送订单的快递网络.它还负责支付快递员和餐厅的费用.餐厅使用FTGO 网站来编辑菜单和管理订单.该应用程序使用各种 web 服务. 包括用于支付的 Stripe, 用于消息传递的 Twilio 和用于电子邮件的 Amazon Simple Email 服务(SES).</p><p>与许多其他老化的企业应用程序一样, FTGO 应用程序是一个整体, 由单个 Java Web 应用程序存档(WAR)文件组成.多年来, 它已经成为一个庞大而复杂的应用程序.尽管FTGO 开发团队尽了最大的努力, 但它已经成为一个泥球模型的例子(<a href="http://www.laputan.org/mud/" target="_blank" rel="noopener">www.laputan.org/mud/</a>).用这种模式的作者 Foote 和 Yoder 的话来说, 这是一种 “结构松散、杂乱无章、草率、管道胶带和捞砂线、意大利面条式代码丛林”.软件交付的速度已经放缓.更糟糕的是, FTGO 应用程序是使用一些过时的框架编写.FTGO 应用程序显示了单体噩梦的所有症状.</p><p>下一节描述 FTGO 应用程序的架构.然后讨论了为什么单体架构一开始工作得很好.我们将讨论 FTGO 应用程序的架构是如何不适应其发展的, 以及这是如何导致单体噩梦的.</p><h2 id="FTGO-应用程序的架构"><a href="#FTGO-应用程序的架构" class="headerlink" title="FTGO 应用程序的架构"></a>FTGO 应用程序的架构</h2><p>FTGO 是一个典型的企业 Java 应用程序.图 1.1 显示了它的架构.FTGO 应用程序有一个六边形架构, 这种架构风格将在第 2 章中详细描述.在六边形架构中, 应用程序的核心由业务逻辑组成.围绕业务逻辑的是实现 UI 并与外部系统集成的各种适配器.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/%E7%AC%AC1%E7%AB%A0-%E9%80%83%E7%A6%BB%E5%8D%95%E4%BD%93%E5%99%A9%E6%A2%A6/Figure-1.1_The_FTGO_application_has_a_hexagonal_architecture.png" alt="Figure-1.1_The_FTGO_application_has_a_hexagonal_architecture"></p><p>业务逻辑由模块组成, 每个模块都是域对象的集合.这些模块的示例包括<code>订单管理</code>、<code>交付管理</code>、<code>账单</code>和<code>支付</code>.有几个适配器与外部系统交互.一些是<em>入站(inbound)</em>适配器, 它们通过调用业务逻辑(包括 <code>REST API</code> 和 <code>Web UI</code> 适配器)来处理请求.其他的是<em>出站(outbound)</em>适配器, 它使业务逻辑能够访问 MySQL 数据库并调用 Twilio 和 Stripe 等云服务.</p><p>尽管有一个逻辑模块化的架构, 但是 FTGO 应用程序被打包成一个 WAR 文件.该应用程序是广泛使用的<em>单体</em>风格软件架构的一个示例, 这种风格将系统构造为单个可执行或可部署组件.如果 FTGO 应用程序是用 Go 语言(GoLang)编写的, 那么它就是一个可执行文件.应用程序的 Ruby 或 NodeJS 版本将是源代码的单个目录层次结构.单体架构本身并不坏.FTGO 开发人员在为他们的应用程序选择单体架构时做出了一个很好的决定.</p><h2 id="单体架构的好处"><a href="#单体架构的好处" class="headerlink" title="单体架构的好处"></a>单体架构的好处</h2><p>在 FTGO 的早期, 当应用程序相对较小时, 应用程序的单体架构有很多好处:</p><ul><li><em>开发简单</em>-IDEs 和其他的开发工具关注于构建单体应用.</li><li><em>易于对应用程序进行彻底更改</em>-你以更改代码和数据库 schema、构建和部署.</li><li><em>直接进行测试</em>-开发人员编写端到端测试来启动应用程序, 调用 REST API 并使用 Selenium 测试 UI.</li><li><em>直接进行部署</em>—开发人员只需将 WAR 文件复制到安装了 Tomcat 的服务器.</li><li><em>容易扩展</em>-FTGO 在负载均衡器后面运行应用程序的多个实例.</li></ul><p>然而, 随着时间的推移, 开发、测试、部署和扩展变得越来越困难.让我们看看为什么.</p><h2 id="生活在单体噩梦中"><a href="#生活在单体噩梦中" class="headerlink" title="生活在单体噩梦中"></a>生活在单体噩梦中</h2><p>不幸的是, 正如 FTGO 开发人员所发现的, 单体架构有一个巨大的限制.成功的应用程序,如 FTGO 应用程序, 有一种在单体架构上不断扩展的习惯.每一次冲刺, FTGO 开发团队都会实现更多的功能, 这使得代码库越来越大.此外, 随着公司越来越成功, 开发团队的规模也在稳步增长.这不仅提高了代码库的增长率, 还增加了管理开销.</p><p>如图 1.2 所示, 曾经小巧、简单的 FTGO 应用程序多年来已经成长为一个庞然大物.同样, 小型的开发团队现在已经变成了多个 Scrum 团队, 每个团队都在一个特定的功能领域工作.由于它的架构已经过时, FTGO 处于一个单体应用的状态.发展是缓慢而痛苦的.敏捷开发和部署是不可能的.让我们看看为什么会这样.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/%E7%AC%AC1%E7%AB%A0-%E9%80%83%E7%A6%BB%E5%8D%95%E4%BD%93%E5%99%A9%E6%A2%A6/Figure-1.2_A_case_of_monolithic_hell.png" alt="Figure-1.2 A case of monolithic hell"></p><p><strong>复杂性威胁着开发人员</strong></p><p>FTGO 应用程序的一个主要问题是它太复杂了.它太大了, 任何开发人员都无法完全理解.因此, 修复 bug 和正确实现新特性变得非常困难和耗时.最后期限错过了.</p><p>更糟糕的是, 这种压倒性的复杂性往往是螺旋式下降.如果代码库难以理解, 开发人员就不会正确地进行更改.每一次更改都会使代码基逐渐变得更复杂, 更难理解.图 1.1 前面所示的整洁的模块化架构并不反映现实.FTGO 正逐渐变成一个可怕的、难以理解的、巨大的泥球.</p><p>Mary 记得最近参加了一个会议, 她遇到了一个开发人员, 他正在编写一个工具来分析数以百万计的代码行(lines-of-code, LOC)应用程序中数千个 jar 之间的依赖关系.当时, 这个工具似乎是 FTGO 可以使用的.现在她不那么肯定了.Mary 认为更好的方法是迁移到更适合复杂应用程序的架构: 微服务.</p><p><strong>开发缓慢</strong></p><p>FTGO 开发人员发现, 除了必须克服难以克服的复杂性之外, 日常的开发任务也很缓慢.大型应用程序会重载并减慢开发人员的 IDE 速度.构建 FTGO 应用程序需要很长时间.而且, 由于它太大, 应用程序需要很长时间才能启动.因此, 编辑构建-运行-测试循环需要很长时间, 这会严重影响生产率.</p><p><strong>从提交到部署的路径是漫长而艰巨的</strong></p><p>FTGO 应用程序的另一个问题是, 将更改部署到生产环境是一个漫长而痛苦的过程.团队通常每个月部署一次产品更新, 通常是在周五或周六晚上.Mary 一直认为, 软件即服务(SaaS)应用程序的最先进技术是<em>持续部署</em>:每天在工作时间多次将更改部署到生产环境中.显然, 到 2011 年为止, Amazon.com 每 11.6 秒就部署一次更改到生产环境中, 而不会对用户产生任何影响!对于 FTGO 开发者来说, 每月更新一次以上的产品似乎是一个遥远的梦想.而采用连续部署几乎是不可能的.</p><p>FTGO 部分采用了敏捷.工程团队分为小队, 使用两周的冲刺.不幸的是, 从代码完成到在生产环境中运行的过程是漫长而艰辛的.许多开发人员都提交到相同的代码库, 其中一个问题是构建常常处于不可发布的状态.当 FTGO 开发人员试图通过使用特性分支来解决这个问题时.他们的尝试导致了冗长而痛苦的合并.因此, 一旦一个团队完成了冲刺, 随之而来的就是很长一段时间的测试和代码稳定.</p><p>将更改引入到生产中需要很长时间的另一个原因是测试需要很长时间.由于代码库非常复杂, 而且还不清楚更改的影响, 开发人员和持续集成(CI)服务器必须运行整个测试套件.系统的一些部分甚至需要人工测试.诊断和修复测试失败的原因也需要一段时间.因此, 完成一个测试周期需要几天的时间.</p><p><strong>扩展困难</strong></p><p>FTGO 团队在扩展其应用程序方面也存在问题.这是因为不同的应用程序模块有相互冲突的资源需求.例如, 餐馆数据存储在一个大的内存数据库中, 该数据库理想地部署在具有大量内存的服务器上.相反, 图片处理模块是 CPU 密集型的, 最好部署在具有大量 CPU 的服务器上.因为这些模块是同一个应用程序的一部分, 所以 FTGO 必须在服务器配置上妥协.</p><p><strong>交付一个可靠的单体是具有挑战性</strong></p><p>FTGO 应用程序的另一个问题是缺乏可靠性.因此, 经常出现生产中断.它不可靠的一个原因是, 由于应用程序很大, 彻底测试应用程序是很困难的.这种可测试性的缺乏意味着 bug 会进入生产环境.更糟糕的是, 应用程序缺乏<em>故障隔离</em>, 因为所有模块都在同一个进程中运行.每隔一段时间, 一个模块中的一个 bug—例如内存泄漏—就会使得应用程序的所有实例逐个崩溃.FTGO 开发人员不喜欢在半夜被传呼由于生产中断.商人更不喜欢失去收入和信任.</p><p><strong>受困于日益过时的技术栈中</strong></p><p>FTGO 团队体验到的单体噩梦的最后一个方面是, 架构迫使他们使用一个正在变得越来越过时的技术栈.单一架构使得采用新的框架和语言变得困难.重写整个单体应用程序, 使用一种可能更好的新技术, 是非常昂贵和危险的.因此, 开发人员被在项目开始时所做的技术选择卡住了.通常, 他们必须维护使用日益过时的技术栈编写的应用程序.</p><p>Spring 框架在向后兼容的同时继续发展, 因此理论上 FTGO 可以进行升级.不幸的是, FTGO 应用程序使用的框架版本与新版本的 Spring 不兼容.开发团队从来没有时间升级这些框架.因此, 应用程序的主要部分使用越来越过时的框架编写.更重要的是, FTGO 开发人员希望试验像 GoLang 和 NodeJS 这样的非 JVM 语言.遗憾的是, 对于单体应用程序来说, 这是不可能的.</p><h1 id="为什么此书和你相关"><a href="#为什么此书和你相关" class="headerlink" title="为什么此书和你相关"></a>为什么此书和你相关</h1><p>您很可能是开发人员、架构师、CTO 或工程副总裁.您负责的应用程序已经不再适用于单体架构.就像 FTGO 的 Mary 一样, 您在和软件交付做斗争, 想要知道如何逃离单体应用的噩梦.或者, 您可能担心您的组织正在走向单体噩梦, 您想知道如何在为时已晚之前改变方向.如果您需要逃离或避免单体噩梦, 这本书适合你.</p><p>这本书花了很多时间来解释微服务架构的概念.我的目标是让你理解这些概念, 不管你使用什么技术.你所需要的只是熟悉企业应用程序架构和设计的基础知识.特别是, 你需要知道以下几点:</p><ul><li>三层架构</li><li>Web 应用程序设计</li><li>如何使用面向对象设计来开发业务逻辑</li><li>如何使用 RDBMS: SQL 和 ACIS 事务</li><li>如何使用消息代理(message broker)和 REST API 进行进程间通信</li><li>安全性,包括认证和授权</li></ul><p>本书中的代码示例使用 Java 和 Spring 框架编写.这意味着为了充分利用示例, 您还需要熟悉 Spring 框架.</p><h1 id="你会从书中学到什么"><a href="#你会从书中学到什么" class="headerlink" title="你会从书中学到什么"></a>你会从书中学到什么</h1><p>当你读完这本书的时候, 你会明白下面的内容:</p><ul><li>微服务架构的本质特征, 它的优点和缺点, 以及何时使用</li><li>分布式数据管理模式</li><li>有效的微服务测试策略</li><li>微服务的部署选项</li><li>将单体应用程序重构为微服务架构的策略</li></ul><p>你也可以做到以下几点:</p><ul><li>使用微服务架构模式架构一个应用程序</li><li>开发服务的业务逻辑</li><li>使用 sagas 维护跨服务的数据一致性</li><li>实现跨服务的查询</li><li>有效地测试微服务</li><li>开发安全,可配置和可观察的生产就绪服务</li><li>将现有的单体应用重构为微服务</li></ul><h1 id="微服务架构的救援"><a href="#微服务架构的救援" class="headerlink" title="微服务架构的救援"></a>微服务架构的救援</h1><p>Mary 得出结论, FTGO 必须迁移到微服务架构.</p><p>有趣的是, 软件架构与功能需求几乎没有什么关系.您可以使用任何架构实现一组用例(应用程序的功能需求).事实上, 对于成功的应用程序(如 FTGO 应用程序)来说, 称为泥球是很常见的.</p><p>然而, 架构很重要, 因为它影响所谓的<em>服务质量(quality of service )</em>需求, 也称为<em>非功能性需求(nonfunctional requirements)</em>、<em>质量属性( quality attributes)</em>或<em>质量(ilities)</em>.随着 FTGO 应用程序的发展, 各种质量属性都受到了影响, 最明显的是那些影响软件交付速度的属性:可维护性、可扩展性和可测试性.</p><p>一方面, 一个纪律严明的团队可以放慢其走向单体噩梦的步伐.团队成员可以努力维护他们的应用程序的模块化.他们可以编写全面的自动化测试.另一方面, 他们不能避免在单个单体应用程序上工作的大型团队的问题.它们也无法解决日益过时的技术栈问题.一个团队能做的最好的事情就是推迟不可避免的事情.为了逃离单体应用的噩梦, 他们必须迁移到一个新的架构: 微服务架构.</p><p>如今, 越来越多的人认为, 如果您正在构建一个大型的、复杂的应用程序, 您应该考虑使用微服务架构.但究竟什么是<em>微服务</em>?不幸的是, 这个名字起不到什么作用, 因为它过分强调了大小.微服务架构有许多定义.有些人太过拘泥于字面意思, 声称服务应该是很小的——例如, 100 行代码.另一些人则认为一项服务只需要两周的开发时间.Adrian Cockcroft 以前在 Netflix 工作, 将微服务架构定义为由具有有限上下文的松散耦合元素组成的面向服务的架构.这个定义还不错, 但有点复杂.我们看看能不能做得更好. </p><h2 id="规模立方-Scale-cube-和微服务"><a href="#规模立方-Scale-cube-和微服务" class="headerlink" title="规模立方(Scale cube)和微服务"></a>规模立方(Scale cube)和微服务</h2><p>我对微服务架构的定义受到 Martin Abbott 和 Michael Fisher 的优秀著作《可伸缩性的艺术》(Addison-Wesley, 2015)的启发.本书描述了一个有用的三维可伸缩性模型: <em>规模立方(scale cube)</em>, 如图 1.3 所示.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure-1.3_The_scale_cube.png" alt="Figure-1.3 The scale cube"></p><p>这种模型定义了三种方式来伸缩应用程序: X, Y 和 Z.</p><p><strong>X 轴扩展负载平衡跨越多个相同的实例的请求</strong></p><p>X 轴缩放是扩展单体应用程序的一种常见方法.图 1.4 显示了X 轴缩放的工作原理.在负载均衡器后面运行应用程序的多个实例.负载均衡器在应用程序的 N 个相同实例之间分发请求.这是提高应用程序容量和可用性的好方法.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure-1.4_X-axis_scaling.png" alt="Figure-1.4 X-axis scaling"></p><p><strong>Z 轴缩放基于请求的属性路由请求</strong></p><p>Z 轴扩展同样运行单体应用程序的多个实例, 但与 X 轴扩展不同的是, 每个实例只负责数据的一个子集.图 1.5 显示了Z 轴伸缩的工作原理.位于实例前面的路由器使用请求属性将其路由到适当的实例.例如, 应用程序可以使用 <code>userId</code> 路由请求.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure-1.5_Y-axis_scaling.png" alt="Figure-1.5 Y-axis scaling"></p><p>在本例中, 每个应用程序实例负责一个用户子集.路由器使用请求授权(Authorization)头指制定 <code>userId</code> 选择应用程序的 N 个相同实例中的一个.Z 轴伸缩是扩展应用程序以处理不断增加的事务和数据量的好方法.</p><p><strong>Y 轴伸缩功能将应用程序分解为服务</strong></p><p>X 轴和 Z 轴伸缩提高了应用程序的容量和可用性.但是这两种方法都不能解决增加开发和应用程序复杂性的问题.要解决这些问题, 您需要应用 Y 轴伸缩或功能分解.图 1.6 显示了 Y 轴伸缩的工作方式: 通过将单个应用程序分割成一组服务.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure-1.6_Y-axis_scaling.png" alt="Figure-1.6 Y-axis scaling"></p><p>一个服务是一个微型应用程序, 它实现了狭义的功能, 比如订单管理、客户管理等等.服务使用 X 轴缩放来扩展, 尽管有些服务也可以使用 Z 扩展.例如, 订单服务由一组负载均衡的服务实例组成.</p><p>微服务架构(microservices)的高层定义是一种架构风格, 在功能上将应用程序分解为一组服务.注意, 这个定义没有说明大小.相反, 重要的是每个服务都有一组集中的、有凝聚力的职责.稍后我将在书中讨论这是什么意思.</p><p>现在让我们看看微服务架构是一种怎样的模块化形式.</p><h2 id="微服务作为一种模块化的形式"><a href="#微服务作为一种模块化的形式" class="headerlink" title="微服务作为一种模块化的形式"></a>微服务作为一种模块化的形式</h2><p>在开发大型、复杂的应用程序时, 模块化是必不可少的.像 FTGO 这样的现代应用程序太大, 个人无法开发.它也很复杂, 没发被一个人理解.应用程序必须分解成由不同的人开发和理解的模块.在单体应用程序中, 模块是使用编程语言结构(如 Java 包)和构造构件(如 Java JAR 文件)的组合来定义的.然而, 正如 FTGO 开发人员所发现的, 这种方法在实践中往往不能很好地工作.长寿命的单体应用程序通常会退化为巨大的泥球.</p><p>微服务架构使用服务作为模块化的单元.服务有一个 API, 这是难以违反的不可跨越的边界.不能像使用 Java 包那样绕过 API 访问内部类.因此, 随着时间的推移, 保持应用程序的模块化要容易得多.使用服务作为构建块还有其他好处, 包括能够独立部署和扩展它们.</p><h2 id="每个服务有自己的数据库"><a href="#每个服务有自己的数据库" class="headerlink" title="每个服务有自己的数据库"></a>每个服务有自己的数据库</h2><p>微服务架构的一个关键特征是服务是松散耦合的, 并且仅通过 API 进行通信.实现松耦合的一种方法是每个服务都有自己的数据存储.例如, 在在线商店中, <code>订单服务(Order Service)</code>有一个包含 <code>ORDERS</code> 表的数据库, 而 <code>客户服务(Customer Service)</code>有一个包含 <code>CUSTOMERS</code> 表的数据库.在开发时, 开发人员可以更改服务的schema, 而不必与从事其他服务的开发人员协调.在运行时, 服务彼此隔离-例如, 一个服务永远不会被阻塞, 因为另一个服务持有数据库锁.</p><blockquote><p><strong>别担心: 松散耦合不会让 Larry Ellison(拉里·埃里森)更富有</strong><br>要求每个服务都有自己的数据库并不意味着它有自己的数据库服务器.例如, 您不必在 Oracle RDBMS 许可证上多花 10 倍的钱.第2章深入探讨了这个主题.</p></blockquote><h2 id="FTGO-微服务架构"><a href="#FTGO-微服务架构" class="headerlink" title="FTGO 微服务架构"></a>FTGO 微服务架构</h2><p>现在我们已经定义了微服务架构并描述了它的一些基本特性, 接下来让我们看看如何将其应用到 FTGO 应用程序中.</p><p>本书的剩余部分将深入讨论 FTGO 应用程序的微服务架构.但首先让我们快速看看将 Y 轴缩放应用到这个应用程序意味着什么.如果我们对 FTGO 应用程序应用 Y 轴分解, 就会得到如图 1.7 所示的架构.分解后的应用程序由许多前端和后端服务组成.我们还将应用 X 轴, 可能还有 Z 轴缩放, 以便在运行时每个服务都有多个实例.</p><p>前端服务包括 API 网关和餐馆 Web UI.API 网关扮演着门面的角色, 在第 8 章中有详细描述, 它提供了由消费者和快递人员的移动应用程序使用的 REST API.餐馆 Web UI 实现了餐馆用来管理菜单和处理订单的 Web 界面.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure-1.7_Some_of_the_services_of_the_microservice_architecture.png" alt="Figure-1.7 Some of the services of the microservice architecture"></p><p>许多服务与本章前面描述的模块相对应.不同的是, 每个服务及其 API 都有非常明确的定义.每一个都可以独立开发、测试、部署和扩展.而且, 这种架构在保持模块化方面做得很好.开发人员不能绕过服务的 API 来访问其内部组件.第 13 章描述了如何将现有的单体应用程序转换为微服务.</p><h2 id="微服务架构与-SOA-的比较"><a href="#微服务架构与-SOA-的比较" class="headerlink" title="微服务架构与 SOA 的比较"></a>微服务架构与 SOA 的比较</h2><p>一些批评微服务架构的人声称它并不是什么新东西-它是面向服务的架构(SOA).从高层次来讲, 它们有一些相似之处.SOA 和微服务架构都是将系统构造为一组服务的架构风格.但是, 如表 1.1 所示, 一旦深入研究, 就会发现显著的差异.</p><p><strong>表 1-1 SOA 和微服务对比</strong></p><table><thead><tr><th>–</th><th>SOA</th><th>微服务</th></tr></thead><tbody><tr><td>内部服务通信</td><td>智能管道(Smart pipes), 如企业服务总线(ESB), 使用重量级协议, 如 SOAP 和其他 WS* 标准</td><td>哑管道(Dumb pipes), 如消息代理, 或直接服务对服务通信, 使用轻量级协议, 如 REST 或 gRPC</td></tr><tr><td>数据</td><td>全局数据模型与共享数据库</td><td>每个服务都有数据模型和数据库</td></tr><tr><td>典型的服务</td><td>大型单体应用</td><td>小型服务</td></tr></tbody></table><p>SOA 和微服务架构通常使用不同的技术栈.SOA 应用程序通常使用重量级技术, 如 SOAP 和其他 <code>WS*</code> 标准.他们通常使用 ESB, 一个包含业务和消息处理逻辑的<em>智能管道(Smart pipes)</em>来集成服务.使用微服务架构构建的应用程序倾向于使用轻量级的开源技术.服务通过<em>哑管道(Dumb pipes)</em>进行通信, 比如消息代理或 REST 或 gRPC 之类的轻量级协议.</p><p>SOA 和微服务架构在处理数据的方式上也有所不同.SOA 应用程序通常具有全局数据模型并共享数据库.相反, 正如前面提到的, 在微服务架构中, 每个服务都有自己的数据库.此外, 如将在第 2 章所述的, 通常认为每个服务都有自己的领域模型.</p><p>SOA 和微服务架构之间的另一个关键区别是服务的大小.SOA 通常用于集成大型、复杂、独立的应用程序.尽管微服务架构中的服务并不总是很小, 但它们几乎总是要小得多.因此, SOA 应用程序通常由一些大型服务组成, 而基于微服务的应用程序通常由几十个或数百个较小的服务组成.</p><h1 id="微服务架构的优点和缺点"><a href="#微服务架构的优点和缺点" class="headerlink" title="微服务架构的优点和缺点"></a>微服务架构的优点和缺点</h1><p>让我们先想想优点, 然后再看看缺点.</p><h2 id="微服务架构的优点"><a href="#微服务架构的优点" class="headerlink" title="微服务架构的优点"></a>微服务架构的优点</h2><p>微服务架构有以下的优点:</p><ul><li>它支持大型复杂应用程序的持续交付和部署.</li><li>服务很小, 很容易维护.</li><li>服务独立部署.</li><li>服务独立扩展.</li><li>微服务架构使团队能够自治.</li><li>它允许轻松地试验和采用新技术.</li><li>它具有更好的故障隔离.</li></ul><p>让我们看下每一种优点.</p><p><strong>它支持大型复杂应用程序的持续交付和部署</strong></p><p>微服务架构最重要的优点是它支持大型复杂应用程序的持续交付和部署.正如 1.7 节后面所述的, 持续交付/部署是 DevOps 的一部分, DevOps 是一组用于快速、频繁和可靠交付软件的实践.高性能的 DevOps 组织通常将更改部署到生产环境中而很少出现生产问题.</p><p>有三种方式使微服务架构能够持续交付/部署:</p><ul><li><em>它具有持续交付/部署所需的可测试性</em>-自动化测试是持续交付/部署的关键实践.由于微服务架构中的每个服务都相对较小, 因此自动化测试更容易编写, 执行速度也更快.因此, 应用程序的 bug 会更少.</li><li><em>它具有持续交付/部署所需的可部署性</em>-每个服务都可以独立于其他服务部署.如果负责服务的开发人员需要部署该服务的本地更改, 他们不需要与其他开发人员协调.他们可以部署他们的变更.因此, 更容易将更改频繁地部署到生产环境中.</li><li><em>它使开发团队能够自治和解耦</em>-您可以将工程组织构建为小型(例如, 两个批萨)团队的集合.每个团队单独负责一个或多个相关服务的开发和部署.如图 1.8 所示, 每个团队可以独立于所有其他团队开发、部署和扩展其服务.因此, 开发速度大大提高.</li></ul><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure-1.8_The_microservices-based_FTGO_application_consists_of_a_set_of_loosely_coupled_services.png" alt="Figure-1.8 The microservices-based FTGO application consists of a set of loosely coupled services"></p><p>持续交付和部署的能力有几个商业利益:</p><ul><li>它缩短了进入市场的时间, 从而使业务能够迅速对客户的反馈做出反应.</li><li>它使企业能够提供当今客户所期望的可靠服务.</li><li>员工满意度更高, 因为更多的时间花在交付有价值的功能上, 而不是去救火.</li></ul><p>因此,微服务架构已经成为任何依赖软件技术的业务的关键所在.</p><p><strong>服务很小, 很容易维护</strong></p><p>微服务架构的另一个好处是每个服务都相对较小.对于开发人员来说, 代码更容易理解.小型的代码库不会降低 IDE 的速度, 从而提高开发人员的工作效率.而且, 每个服务的启动速度通常都比大型整体服务快得多, 这也使得开发人员更有效率, 并加快了部署速度.</p><p><strong>服务独立扩展</strong></p><p>微服务加过中的每个服务都可以使用 X 轴克隆和 Z 轴分区独立于其他服务进行伸缩.此外, 每个服务都可以部署在最适合其资源需求的硬件上.这与使用单体架构时非常不同, 在单体架构中, 具有完全不同的资源需求的组件(例如, CPU 密集型组件和内存密集型组件)必须一起部署.</p><p><strong>更好的故障隔离</strong></p><p>微服务架构具有更好的故障隔离.例如, 一个服务中的内存泄漏只影响该服务.其他服务将继续正常处理请求.相比之下, 单体架构中一个行为不好的组件将导致整个系统的崩溃.</p><p><strong>轻松地试验和采用新技术</strong></p><p>最后但并非最不重要的一点是, 微服务架构消除了对技术栈的任何长期承诺.原则上, 在开发新服务时, 开发人员可以自由选择最适合该服务的语言和框架.在许多组织中, 限制选择是有意义的, 但关键是你不受过去决定的约束.</p><p>此外, 因为服务很小, 所以使用更好的语言和技术重写它们变得很实用.如果一项新技术的试验失败了, 你可以放弃这项工作, 而不用让整个项目冒风险.这与使用单体架构时非常不同, 在单体架构中, 最初的技术选择严重限制了将来使用不同语言和框架的能力.</p><h2 id="微服务架构的缺点"><a href="#微服务架构的缺点" class="headerlink" title="微服务架构的缺点"></a>微服务架构的缺点</h2><p>当然, 没有什么技术是银弹, 而且微服务架构有许多明显的缺陷和问题.事实上, 这本书的大部分内容都是关于如何解决这些缺点和问题的.当你读到这些挑战时, 不要担心.在这本书的后面, 我描述了解决这些问题的方法.</p><p>下面是微服务架构的主要缺陷和问题:</p><ul><li>寻找合适的服务集是具有挑战性的</li><li>分布式系统非常复杂, 这使得开发、测试和部署变得非常困难</li><li>部署跨多个服务的特性需要仔细的协调</li><li>决定何时采用微服务架构是很困难的</li></ul><p>让我们依次看下每一个.</p><p><strong>寻找合适的服务集是具有挑战性的</strong></p><p>使用微服务架构的一个挑战是, 没有一个具体的、定义良好的算法来将系统分解为服务.与许多软件开发一样, 这是一门艺术.更糟糕的是, 如果不正确地分解一个系统, 您将构建一个<em>分布式单体系统(distributed monolith)</em>, 这个系统由必须部署在一起的耦合服务组成.分布式单体架构既有单体架构的缺点, 也有微服务架构的缺点.</p><p><strong>分布式系统是复杂的</strong></p><p>使用微服架构的另一个问题是, 开发人员必须处理创建分布式系统的额外复杂性.服务必须使用进程间通信机制.这比简单的方法调用更复杂.此外, 服务必须被设计为能够处理分区故障, 并能处理远程服务不可用或具有高延迟.</p><p>实现跨多个服务的用例需要使用不熟悉的技术.每个服务都有自己的数据库, 这使得实现跨服务的事务和查询成为一项挑战.如第 4 章所述, 基于微服务的应用程序必须使用众所周知的 <em>sagas</em> 来维护服务之间的数据一致性.第 7 章解释了基于微服务的应用程序不能使用简单查询从多个服务检索数据.相反, 它必须使用 API 组合或 CQRS 视图来实现查询.</p><p>IDE 和其他开发工具关注于构建单体应用程序, 而不为开发分布式应用程序提供显式支持.编写涉及多个服务的自动化测试是一项挑战.这些都是特定于微服务架构的问题.因此, 为了成功地使用微服务, 组织的开发人员必须具有复杂的软件开发和交付技能.</p><p>微服务架构还引入了重大的操作复杂性.更多的移动部件-不同类型服务的多个实例-必须在生产中进行管理.要成功部署微服务, 您需要高度自动化.您必须使用以下技术:</p><ul><li>自动部署工具, 比如 Netflix Spinnaker</li><li>一个现成的 PaaS, 比如 Pivotal Cloud Foundry 或 Red Hat OpenShift</li><li>Docker 编排平台, 如 Docker Swarm 或 Kubernetes</li></ul><p>我在第 12 章中更详细地描述部署选项.</p><p><strong>部署跨越多个服务的特性需要仔细的协调</strong></p><p>使用微服务架构的另一个挑战是, 部署跨多个服务的特性需要在不同的开发团队之间进行仔细的协调.您必须创建一个部署计划, 根据服务之间的依赖关系对服务部署进行排序.这与单体架构非常不同, 在单体架构中, 您可以轻松地将更新部署到多个组件上.</p><p><strong>决定何时采用微服务架构是很困难的</strong></p><p>使用微服务架构的另一个问题是决定在应用程序生命周期的哪个点应该使用这个架构.在开发应用程序的第一个版本时, 通常不会遇到架构所要解决的问题.此外, 使用复杂的分布式架构会减慢开发速度.对于初创企业来说, 这可能是一个很大的难题, 其中最大的问题通常是如何快速发展业务模型和相应的应用程序.使用微服务架构使快速迭代变得更加困难.几乎可以肯定的是, 创业应该从单一应用程序开始.</p><p>但是, 稍后, 当问题是如何处理复杂性时, 从功能上将应用程序分解为一组微服务是有意义的.您可能会发现, 由于复杂的依赖关系, 重构很困难.第13章讨论了将单个应用程序重构为微服务的策略.</p><p>正如您所看到的, 微服务架构提供了许多好处, 但也有一些明显的缺点.由于这些问题, 采用微服务架构不应该轻率地进行.但对于复杂的应用程序, 比如面向消费者的web 应用程序或 SaaS 应用程序, 这通常是正确的选择.著名的网站如 eBay(<a href="http://www.slideshare.net/randyshoup/ebay-architecturestriking" target="_blank" rel="noopener">www.slideshare.net/randyshoup/ebay-architecturestriking</a> -balance-between site-stability-features-speed-performance-cost)、Amazon.com、Groupon 和 Gilt 都已经从一个单一的架构发展成为一个微服务架构.</p><p>在使用微服务架构时, 您必须解决许多设计和架构问题.更重要的是, 许多问题都有多种解决方案, 每一个都有不同的权衡.没有一个完美的解决方案.为了帮助指导您的决策, 我创建了微服务架构模式语言.在本书的其余部分中, 当我向您介绍微服务架构时, 我引用了这种模式语言.让我们看看什么是模式语言.以及它为什么有用.</p><h1 id="微服务架构语言"><a href="#微服务架构语言" class="headerlink" title="微服务架构语言"></a>微服务架构语言</h1><p>架构和设计都是关于决策的.您需要决定是单体架构还是微服务架构最适合您的应用程序.在做这些决定时, 你要考虑很多权衡取舍.如果选择了微服务架构, 则需要解决许多问题.</p><p>描述各种架构和设计选项并改进决策制定的一个好方法是使用模式语言.首先让我们了解为什么需要模式和模式语言, 然后我们将介绍微服务架构模式语言.</p><h2 id="微服务架构不是银弹"><a href="#微服务架构不是银弹" class="headerlink" title="微服务架构不是银弹"></a>微服务架构不是银弹</h2><p>早在 1986 年,<em>《人月神话》</em>(Addison-Wesley Professional, 1995 年)的作者 Fred Brooks 说, 在软件工程中, 没有灵丹妙药.这意味着没有任何技术或技巧可以让你的生产力提高十倍.然而几十年后, 开发人员仍然在激烈地争论他们最喜欢的银弹, 他们坚信他们最喜欢的技术将大大提高他们的生产力.</p><p> 很多论据都遵循了 <em>suck/rock 二分法</em>(<a href="http://nealford.com/memeagora/2009/08/05/suck-rock-dichotomy.html)" target="_blank" rel="noopener">http://nealford.com/memeagora/2009/08/05/suck-rock-dichotomy.html)</a>, Neal Ford 创造的一个术语, 它描述了软件世界中的所有东西要么是 suck 的, 要么是 rock 的, 没有中间立场.这些论点都有这样的结构: 如果你做 X,那么小狗狗就会死去,因此你必须做 Y.例如, 同步与反应式编程、面向对象与函数式、Java 与 JavaScript、REST 与消息传递.当然, 现实更加微妙.每一种技术都有其倡导者经常忽视的缺点和局限性.因此, 采用的技术通常遵循 <em>Gartner 技术成熟度曲线</em>(<a href="https://en.wikipedia.org/wiki/Hype_cycle)" target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Hype_cycle)</a>, 其中新兴技术经历五个阶段, 包括过<em>高期望的峰值</em>(它很狂热-rock), 随后是<em>理想幻灭的低谷</em>(它很糟糕-suck), 并以<em>实质生产的高峰期</em>结束(我们现在明白了权衡和何时使用它).</p><blockquote><p>Gartner 技术成熟度曲线中文可参考 <a href="https://baike.baidu.com/item/%E6%8A%80%E6%9C%AF%E6%88%90%E7%86%9F%E5%BA%A6%E6%9B%B2%E7%BA%BF/2551258" target="_blank" rel="noopener">技术成熟度曲线</a></p></blockquote><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/技术成熟度曲线.jpg" alt="技术成熟度曲线"><br>(图片来自 <a href="http://huaban.com/pins/111734468/" target="_blank" rel="noopener">http://huaban.com/pins/111734468/</a>)</p><p>微服务也不能幸免于银弹现象.这个架构是否适合您的应用程序取决于许多因素.因此, 建议始终使用微服务架构是不好的建议, 但建议永远不要使用它同样是不好的建议.和许多事情一样, 这要视情况而定.</p><p>这些关于技术的两极分化和炒作的根本原因是, 人类主要是由他们的情感驱动.乔纳森海特(Jonathan Haidt)在他的优秀著作<em>《正义的心灵: 为什么好人会被政治和宗教分裂》</em>(Vintage 出版社, 2013 年)中,用大象和骑手的比喻来描述人类的心灵是如何运作的.大象代表人类大脑的情感部分.它做了大部分的决定.骑手代表大脑的理性部分.它有时会影响大象, 但主要是为大象的决定提供理由.</p><p>我们-软件开发社区-需要克服我们的情感天性, 找到一种更好的方式来讨论和应用技术.讨论和描述技术的一个好方法是使用模式格式(pattern format), 因为它是客观的.在以模式格式描述技术时, 您必须描述其缺点.让我们看看模式格式.</p><h2 id="模式和模式语言"><a href="#模式和模式语言" class="headerlink" title="模式和模式语言"></a>模式和模式语言</h2><p>模式是对在特定上下文中发生的问题的可重用解决方案.这个想法起源于现实世界的架构, 并被证明在软件架构和设计中是有用的.模式的概念是由现实世界的建筑师 Christopher Alexander 提出.他还创建了模式语言的概念, 这是解决特定领域问题的相关模式的集合.他的书《模式语言:城镇、建筑物、建筑》(牛津大学出版社, 1977 年)描述了一种建筑模式语言, 由 253 种模式组成.这些模式从解决方案到高层次的问题, 比如城市的位置(“取水”), 到低层次的问题, 比如如何设计房间(“每个房间的两面都有灯光”).这些模式中的每一种都通过将范围从城市到窗口的物理对象排列来解决问题.</p><p>Christopher Alexander 的著作启发了软件社区采用模式和模式语言的概念.由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著的《设计模式:可重用面向对象软件的元素》(Addison-Wesley Professional, 1994)是面向对象设计模式的集合.这本书在软件开发人员中普及了模式.自上世纪 90 年代中期以来, 软件开发人员记录了大量的软件模式.<em>软件模式</em>通过定义一组协作软件元素来解决软件架构或设计问题.</p><p>例如, 假设您正在构建一个必须支持各种透支策略的银行应用程序.每项政策都对账户余额和透支账户收取的费用进行了限制.您可以使用策略模式来解决这个问题, 策略模式是经典设计模式书籍中的一个著名模式.该策略模式定义的解决方案由三个部分组成:</p><ul><li>一个策略接口 <code>Overdraft</code> 封装了透支的算法</li><li>一个或多个具体的策略类, 每个特定上下文对应一个</li><li><code>Account</code> 类使用这个算法</li></ul><p>策略模式是面向对象的设计模式, 因此解决方案的元素是类.在本节后面, 我将描述高级设计模式, 其中解决方案由协作服务组成.</p><p>模式之所以有价值, 一个原因是模式必须描述它所应用的上下文.解决方案是特定于特定上下文的, 在其他上下文中可能不能很好地工作, 这种想法是对通常讨论的技术的改进.例如, 对于用户较少的应用程序来说, 以 Netflix 的规模解决这个问题的解决方案可能不是最好的方法.</p><p>然而, 模式的价值远远超过了要求您考虑问题的上下文.它迫使您描述解决方案的其他重要但经常被忽略的方面.一个常用的模式结构包括三个特别有价值的部分:</p><ul><li>约束(forces)</li><li>产生的背景</li><li>相关的模式</li></ul><p>让我们从力开始, 看看每一个.</p><p><strong>约束:解决疑难时必须解决的问题</strong></p><p>模式的约束部分描述了在给定上下文中解决问题时必须解决的约束问题.约束可能会发生冲突, 因此不可能解决所有的冲突.哪种约束更重要取决于环境.你必须优先解决一些问题.例如, 代码必须易于理解并具有良好的性能.用响应式编写的代码比同步代码有更好的性能, 但通常更难于理解.明确列出这些约束是有用的, 因为它清楚地表明哪些问题需要解决.</p><p><strong>产生的背景: 应用模式的后果</strong></p><p>模式产生的背景部分描述了应用模式的后果.它包括三个部分:</p><ul><li><em>优点</em>-模式的优点, 包括已经解决的约束</li><li><em>缺点</em>-模式的缺点, 包括未解决的约束</li><li><em>问题</em>-应用模式引入的新问题</li></ul><p>产生的背景提供了解决方案的更完整和更少偏见的视图, 从而能够做出更好的设计决策.</p><p><strong>相关模式:五种不同类型的关系</strong></p><p>模式的相关模式部分描述了模式和其他模式之间的关系.模式之间有五种关系:</p><ul><li><em>前身(Predecessor)</em>-前身模式是一种激发对这种模式需求的模式.例如, 除了单体架构模式之外, 微服务架构模式是模式语言中其他模式的前身.</li><li><em>后继者(Successor)</em>-解决这个模式引入的问题的模式.例如, 如果应用微服务架构模式, 则必须应用许多后继模式, 包括服务发现模式和断路器模式.</li><li><em>替代模式</em>-为这种模式提供另一种解决方案的模式.例如, 单体架构模式和微服务架构模式是构建应用程序的可替代方法.你选择其中一个.</li><li><em>泛化(Generalization)</em>-一个问题的通用解决方案.例如, 在第 12 章中, 您将了解单主机单服务模式(Single service per host pattern)的不同实现.<br><em>专门化(Specialization)</em>-特定模式的专门化形式.例如, 在第 12 章中, 您将了解将服务作为容器模式部署是单主机单服务的专门化.</li></ul><p>此外, 您可以将处理特定问题区域中的问题的模式组织成组.相关模式的明确描述为如何有效地解决特定问题提供了有价值的指导.图 1.9 显示了如何以可视方式表示模式之间的关系.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure-1.9_The_visual_representation_of_different_types_of_relationships_between_the_patterns.png" alt="Figure-1.9 The visual representation of different types of relationships between the patterns"></p><p>图 1.9 所示的模式之间不同类型的关系表示如下:</p><ul><li>体现了前身-后继者模式的关系</li><li>模式是同一问题的替代解决方案</li><li>表明一个模式是另一个模式的专门化</li><li>适用于特定问题领域的模式</li></ul><p>通过这些关系联系起来的模式集合有时形成所谓的模式语言.模式语言中的模式一起工作以解决特定领域中的问题.特别是, 我已经创建了微服务架构模式语言.它是微服务相关的软件架构设计模式的集合.让我们来看看这种模式语言.</p><h2 id="微服务架构模式概览"><a href="#微服务架构模式概览" class="headerlink" title="微服务架构模式概览"></a>微服务架构模式概览</h2><p>微服务架构模式语言是一组模式, 帮助您使用微服务架构构建应用程序.图 1.10 显示了模式语言的高级结构.模式语言首先帮助您决定是否使用微服务架构.它描述了单体架构和微服务架构, 以及它们的优缺点.然后, 如果微服务架构非常适合您的应用程序, 那么模式语言可以通过解决各种架构和设计问题来帮助您有效地使用它.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure-1.10_A_high-level_view_of_the_Microservice_architecture_pattern_language.png" alt="Figure-1.10 A high-level view of the Microservice architecture pattern language"></p><p>模式语言由几组模式组成.在图 1.10 的左边是应用程序架构模式组、单体架构模式和微服务架构模式.这些是我们在本章中讨论过的模式.模式语言的其余部分由一组模式组成, 这些模式是使用微服务架构模式引入的问题的解决方案.</p><p>模式分为三层:</p><ul><li><em>基础架构模式</em>-它们解决了开发之外的大部分基础设施问题.</li><li><em>应用程序基础架构模式</em>-这些都是影响开发的基础设施问题.</li><li><em>应用程序模式</em>-解决了开发者面对的问题.</li></ul><p>这些模式是根据它们解决的问题分组在一起的.让我们来看看主要的模式组.</p><p><strong>将应用程序分解为服务的模式</strong></p><p>决定如何将系统分解为一组服务在很大程度上是一门艺术, 但是有许多策略可以提供帮助.图 1.11 中显示的两个分解模式是您可以用来定义应用程序架构的不同策略.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure_1.11_There_are_two_decomposition_patterns_Decompose_by_business_capability.jpg" alt="Figure 1.11 There are two decomposition patterns Decompose by business capability"></p><p>第 2 章详细描述了这些模式.</p><p><strong>通信模式</strong></p><p>使用微服务架构构建的应用程序是一个分布式系统.因此, 进程间通信(IPC)是微服务架构的重要组成部分.关于您的服务如何彼此通信以及如何与外部世界通信, 您必须作出各种架构和设计决策.图 1.12 为通信模式, 分为五组:</p><ul><li><em>通信方式</em>-应该使用什么样的 IPC 机制?</li><li><em>发现(Discovery)</em>-服务的客户端如何确定服务实例的 IP 地址, 以便比如发出 HTTP 请求?</li><li><em>可靠性</em>-您如何确保服务之间的通信是可靠的, 即使服务不可用?</li><li><em>事务性消息</em>-如何将消息的发送和事件的发布与更新业务数据的数据库事务集成起来?</li><li><em>外部 API</em>-应用程序的客户端如何与服务通信?</li></ul><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure_1.12_The_five_groups_of_communication_patterns.jpg" alt="Figure 1.12 The five groups of communication patterns"></p><p>第 3 章介绍了前四组模式:通信方式、发现、可靠性和事务消息.第 8 章介绍了外部 API 模式.</p><p><strong>用于实现事务管理的数据一致性模式</strong></p><p>如前所述, 为了确保松耦合, 每个服务都有自己的数据库.不幸的是, 每个服务都有一个数据库会带来一些重要的问题.我在第 4 章中描述了使用分布式事务(2PC)的传统方法对于现代应用程序来说不是一个可行的选择.相反, 应用程序需要使用 Saga 模式来维护数据一致性.图 1.13 显示了与数据相关的模式.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure_1.13_use_the_Saga_pattern_to_maintain_data_consistency_across_services.jpg" alt="Figure 1.13 use the Saga pattern to maintain data consistency across services"></p><p> 第 4、5、6 章详细地描述了这些模式.</p><p> <strong>用于在微服务架构中查询数据的模式</strong></p><p>每个服务使用数据库的另一个问题是, 一些查询需要 join 多个服务拥有的数据.服务的数据只能通过其 API 访问, 因此不能对其数据库使用分布式查询.图 1.14 显示了一些用于实现查询的模式.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure_1.14_use_one_of_the_querying_patterns_to_retrieve_data_scattered_across_multiple_services.jpg" alt="Figure 1.14 use one of the querying patterns to retrieve data scattered across multiple services"></p><p>有时您可以使用 API 组合模式, 它调用一个或多个服务的 API 并聚合结果.其他时候, 您必须使用命令查询责任隔离(CQRS)模式, 该模式维护一个或多个更容易查询的数据副本.第 7 章介绍了实现查询的不同方法.</p><p><strong>服务部署模式</strong></p><p>部署单个应用程序并不总是容易的, 但是从某种意义上说, 部署单个应用程序非常简单.您必须在负载均衡器后面运行应用程序的多个实例.</p><p>相比之下, 部署基于微服务的应用程序要复杂得多.可能有数十或数百个服务是用各种语言和框架编写的.还有许多活动部件需要管理.图 1.15 显示了部署模式.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure_1.15_Several_patterns_for_deploying_microservices.jpg" alt="Figure 1.15 Several patterns for deploying microservices"></p><p>传统的, 通常是手工的, 以特定于语言的打包格式部署应用程序的方式, 例如 WAR 文件, 不能扩展到支持微服务架构.您需要高度自动化的部署基础设施.理想情况下, 您应该使用一个部署平台, 为开发人员提供一个简单的 UI(命令行或 GUI)来部署和管理他们的服务.部署平台通常基于虚拟机、容器或无服务器技术.第 12 章介绍了不同的部署选项.</p><p><strong>可观察性模式提供了对应用程序行为的洞察</strong></p><p>操作应用程序的一个关键部分是了解其运行时行为, 并对失败请求和高延迟等问题进行故障排除.虽然理解和排除单个应用程序的故障并不总是容易的, 但是它有助于以简单、直接的方式处理请求.每个传入的请求都被负载均衡到一个特定的应用程序实例, 该实例对数据库进行一些调用并返回一个响应.例如, 如果需要了解如何处理某个特定请求, 可以查看处理该请求的应用程序实例的日志文件.</p><p>相比之下, 理解和诊断微服务架构中的问题要复杂得多.在最终将响应返回到客户端之前, 请求可以在多个服务之间跳转.因此, 没有一个日志文件可以检索.同样, 由于存在多个疑点, 延迟问题更难以诊断.</p><p>您可以使用以下模式来设计可观察的服务:</p><ul><li><em>健康检查 API</em>-暴露返回服务健康状态的端点.</li><li><em>日志聚合</em>-日志服务活动, 并将日志写入中央日志服务器, 该服务器提供搜索和警报功能.</li><li><em>分布式跟踪</em>-为每个外部请求分配一个唯一的 ID, 并在它们在服务之间流动时跟踪请求.</li><li><em>异常跟踪</em>-报告异常跟踪服务的异常, 该服务删除重复的异常, 提醒开发人员, 并跟踪每个异常的解决方案.</li><li><em>应用程序度量</em>—维护度量, 例如计数器和度量, 并将它们暴露给度量服务器.</li><li><em>审计日志</em>-记录用户行为.</li></ul><p>第 11 章详细描述了这些模式.</p><p><strong>用于服务自动化测试的模式</strong></p><p>微服务架构使单个服务更容易测试, 因为它们比单体应用程序小得多.但是, 与此同时, 重要的是测试不同的服务在一起工作, 同时避免使用复杂、缓慢和脆弱的端到端测试, 这些测试将多个服务一起测试.以下是通过隔离测试服务简化测试的模式:</p><ul><li><em>消费者驱动的契约测试(Consumer-driven contract test)</em>-验证服务是否满足其客户端的期望.</li><li><em>消费者端契约测试(Consumer-side contract test)</em>-验证服务的客户端可以与服务通信.</li><li><em>服务组件测试</em>-隔离测试服务.</li></ul><p>第 9 章和第 10 章将详细地描述这些测试模式.</p><p><strong>处理横切关注点的模式</strong></p><p>在微服务架构中, 每个服务都必须实现许多关注点, 包括可观察性模式和发现模式.它还必须实现外部化配置模式, 该模式在运行时向服务提供诸如数据库凭据之类的配置参数.在开发新服务时, 从头开始重新实现这些关注点将花费大量时间.更好的方法是应用微服务机箱模式(Microservice Chassis pattern), 并在处理这些问题的框架之上构建服务.第 11 章更详细地描述了这些模式.</p><p><strong>安全模式</strong></p><p>在微服务架构中, 用户通常通过 API 网关进行身份验证.然后, 它必须将关于用户的信息(如标识和角色)传递给它调用的服务.一种常见的解决方案是应用访问令牌(token)模式.API 网关将访问 token, 例如 JWT(JSON Web Token)传递给服务, 服务可以验证令牌并获取关于用户的信息.第 11 章更详细地讨论了访问 token 模式.</p><p>毫不奇怪, 微服务架构模式语言中的模式关注于解决架构和设计问题.为了成功地开发软件, 您当然需要正确的架构, 但这不是唯一需要考虑的问题.您还必须考虑流程和组织.</p><h1 id="超越微观服务-流程与组织"><a href="#超越微观服务-流程与组织" class="headerlink" title="超越微观服务: 流程与组织"></a>超越微观服务: 流程与组织</h1><p>对于大型、复杂的应用程序, 微服务架构通常是最佳选择.但是除了拥有正确的架构之外, 成功的软件开发还需要您拥有组织、开发和交付流程.图 1.16 显示了流程、组织和架构之间的关系.</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/第1章-逃离单体噩梦/Figure_1.16_the_relationships_between_process_and_organization_and_architecture.jpg" alt="Figure 1.16 the relationships between process and organization and architecture"></p><p>我已经描述了微服务架构.让我们看看组织和流程.</p><h2 id="软件开发和交付组织"><a href="#软件开发和交付组织" class="headerlink" title="软件开发和交付组织"></a>软件开发和交付组织</h2><p>成功必然意味着工程团队将成长.一方面, 这是一件好事, 因为更多的开发人员可以完成更多的工作.大型团队的问题是, 正如 Fred Brooks 在《人月神话》中所写的, 规模为 N 的团队的沟通开销为 O(N ^ 2).如果团队太大, 由于沟通开销, 它将变得低效.举个例子, 想象一下, 每天和 20 个人一起做一次站立会议.</p><p>解决方案是将一个大型单一团队重构为一个团队的团队.每个小组人数不多, 不超过 8-12 人.它有一个明确定义的面向业务的任务: 开发并可能操作一个或多个服务, 这些服务实现了一个特性或业务功能.团队是跨功能的, 可以开发、测试和部署其服务, 而不必经常与其他团队进行通信或协调.</p><blockquote><p>反向康威操纵(reverse Conway maneuver)<br>为了在使用微服务架构时有效地交付软件, 您需要考虑 Conway 定律(<a href="https://en.wikipedia.org/wiki/Conway%27s_law)" target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Conway%27s_law)</a>, 声明如下:<br>设计系统的组织…被限制生产这些组织的通信结构的副本的设计.<br>–Melvin Conway<br>换句话说, 应用程序的架构反映了开发它的组织的结构.因此, 很重要的一点是反向应用 Conway 法则(<a href="http://www.thoughtworks.com/radar/techniques/inverse-conway-maneuver)" target="_blank" rel="noopener">www.thoughtworks.com/radar/techniques/inverse-conway-maneuver)</a>, 并设计您的组织, 使其结构反映您的微服务架构.通过这样做, 您可以确保开发团队与服务一样松耦合.</p></blockquote><p>多个团队的速度明显高于单个大团队的速度.正如前面 1.5.1 节所述, 微服务架构在使团队能够自治方面扮演着关键角色.每个团队可以在不与其他团队协调的情况下开发、部署和扩展其服务.此外, 当服务不符合 SLA 时, 很清楚应该联系谁.</p><p>更重要的是, 开发组织的可伸缩性更强.您可以通过添加团队来发展组织.如果一个团队变得太大, 您可以将其与其关联的服务或服务分开.因为团队是松耦合的, 所以可以避免大型团队的通信开销.因此, 您可以在不影响生产力的情况下增加人员.</p><h2 id="软件开发和交付流程"><a href="#软件开发和交付流程" class="headerlink" title="软件开发和交付流程"></a>软件开发和交付流程</h2><p>使用带有瀑布式开发流程的微服务架构就像驾驶一辆马拉的法拉利-您浪费了使用微服务的大部分好处.如果您希望使用微服务架构开发应用程序, 那么采用敏捷开发和部署实践, 如 Scrum 或 Kanban(看板)是非常重要的.更好的是, 您应该实践持续交付/部署, 这是 DevOps 的一部分.</p><p>Jez Humble(<a href="https://continuousdelivery.com/)对持续交付的定义如下" target="_blank" rel="noopener">https://continuousdelivery.com/)对持续交付的定义如下</a>:</p><blockquote><p><em>持续交付是将所有类型的更改, 包括新特性、配置更改、bug 修复和试验, 安全且快速地以可持续的方式交付到生产或用户手中的能力.</em></p></blockquote><p>持续交付的一个关键特征是软件总是可发布的.它依赖于高度自动化, 包括自动化测试.持续部署在自动将可发布代码部署到生产环境中的实践中, 进一步实现了持续交付.执行连续部署的高性能组织每天将多次部署到生产环境中, 生产中断的情况要少得多, 并且能够从发生的任何中断中快速恢复(<a href="https://puppet.com/resources/whitepaper/state-of-devops-report).正如前面" target="_blank" rel="noopener">https://puppet.com/resources/whitepaper/state-of-devops-report).正如前面</a> 1.5.1 节所述, 微服务架构直接支持持续交付/部署.</p><blockquote><p><strong>快速行动而不破坏事物</strong><br>持续交付/部署(以及更一般的 DevOps)的目标是快速而可靠地交付软件.评估软件开发的四个有用指标如下:</p><ul><li><em>部署频率</em>-软件部署到生产环境中的频率</li><li><em>交付周期</em>-从开发人员签入变更到部署变更的时间</li><li><em>平均恢复时间</em>-从生产问题中恢复的时间</li><li><em>更改故障率</em>-</li></ul><p>在传统的组织中, 部署频率较低, 交付时间较长.压力重重的开发人员和操作人员通常会熬夜, 在维护窗口期间解决最后一分钟出现的问题.相反, DevOps 组织频繁地发布软件, 通常是每天多次, 生产问题要少得多.例如, 亚马逊(Amazon)在 2014 年每 11.6 秒将变更部署到生产中(<a href="http://www.youtube.com/watch?v=dxk8b9rSKOo)" target="_blank" rel="noopener">http://www.youtube.com/watch?v=dxk8b9rSKOo)</a>, 而 Netflix 的一个软件组件交付时间为 16 分钟(<a href="https://medium.com/netflix-techblog/how-we-build-code-at-netflix-c5d9bd727f15)" target="_blank" rel="noopener">https://medium.com/netflix-techblog/how-we-build-code-at-netflix-c5d9bd727f15)</a>.</p></blockquote><p><strong>采用微服务的人性化方面</strong></p><p>采用微服务架构将改变您的架构、组织和开发流程.但最终, 它改变了人们的工作环境, 正如前面提到的, 这些人是情绪化的生物.如果被忽视, 他们的情绪可能会让微服务的采用变得崎岖不平.Mary 和其他 FTGO 领导人将努力改变 FTGO 开发软件的方式.</p><p>威廉和苏珊•布里吉斯合著的畅销书《过渡管理》(Da Capo life Books, 2017,<a href="https://wmbridges.com/books)介绍了过渡的概念" target="_blank" rel="noopener">https://wmbridges.com/books)介绍了过渡的概念</a>, 即人们如何在情绪上对变化做出反应的过程.它描述了一个三阶段转换模型:</p><ol><li><p><em>结束, 失败, 和放手</em>-当人们面对迫使他们走出舒适区的变化时, 情绪波动和抗拒的时期.他们经常为失去了旧的做事方式而悲伤.例如, 当人们重新组织成跨职能的团队时, 他们会想念以前的队友.类似地, 拥有全局数据模型的数据建模组将受到每个服务拥有自己的数据模型的想法的威胁.</p></li><li><p><em>中立区</em>-介于新旧做事方式之间的中间阶段, 人们经常感到困惑.他们经常努力学习新的做事方法.</p></li><li><p><em>新的开始</em>-最后一个阶段, 人们热情地接受新的做事方式, 并开始体验其中的好处.</p></li></ol><p>本书描述了如何最好地管理产品化的每个阶段, 并增加成功实现变更的可能性.FTGO 无疑正遭受着 “单体的噩梦”, 需要迁移到微服务架构.它还必须更改其组织和开发流程.然而, FTGO 要想成功实现这一点, 就必须考虑到转型模式, 并考虑人们的情绪.</p><p>在下一章中, 您将了解软件架构的目标以及如何将应用程序分解为服务.</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ul><li>单体架构模式将应用程序构建为单个可部署单元.</li><li>微服架构模式将系统分解为一组独立部署的服务, 每个服务都有自己的数据库.</li><li>对于简单的应用程序, 单体架构是一个不错的选择, 但是对于大型复杂的应用程序, 微服务架构通常是一个更好的选择.</li><li>微服务架构通过允许小型、自治的团队并行工作来加速软件开发的速度.</li><li>微服务架构并不是万能的-它有很多明显的缺点, 包括复杂性.</li><li>微服务架构模式语言是帮助您使用微服务架构构建应用程序的模式集合.它帮助您决定是否使用微服务架构, 如果选择微服务架构, 模式语言将帮助您有效地应用它.</li><li>您需要的不仅仅是微服务架构来加速软件交付.成功的软件开发还需要 DevOps 和小型的、自治的团队.</li><li>不要忘记采用微服务的人性化一面.为了成功地过渡到微服务架构, 您需要考虑员工的情绪.</li></ul>]]></content>
<summary type="html">
<p>本章包含:</p>
<ul>
<li>单体噩梦(monolithic hell)的症状以及如何通过采用微服务架构来逃离它</li>
<li>微服务架构的本质特征及其优缺点</li>
<li>微服务是如何支持 DevOps 风格的大型复杂应用程序的开发</li>
<li>微服
</summary>
<category term="微服务" scheme="https://ykgarfield.github.io/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
<category term="Microservice-Patterns(译)" scheme="https://ykgarfield.github.io/categories/%E5%BE%AE%E6%9C%8D%E5%8A%A1/Microservice-Patterns-%E8%AF%91/"/>
<category term="微服务" scheme="https://ykgarfield.github.io/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
<category term="微服务模式" scheme="https://ykgarfield.github.io/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A8%A1%E5%BC%8F/"/>
</entry>
<entry>
<title>Swagger2Markup bug-生成的 html 文档有问题当 ApiModel value 包含斜线</title>
<link href="https://ykgarfield.github.io/2019/01/01/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/swagger/Swagger2Markup%20bug-%E7%94%9F%E6%88%90%E7%9A%84%20html%20%E6%96%87%E6%A1%A3%E6%9C%89%E9%97%AE%E9%A2%98%E5%BD%93%20ApiModel%20value%20%E5%8C%85%E5%90%AB%E6%96%9C%E7%BA%BF/"/>
<id>https://ykgarfield.github.io/2019/01/01/文档工具/swagger/Swagger2Markup bug-生成的 html 文档有问题当 ApiModel value 包含斜线/</id>
<published>2018-12-31T16:00:00.000Z</published>
<updated>2019-01-02T01:58:57.071Z</updated>
<content type="html"><![CDATA[<h1 id="错误描述"><a href="#错误描述" class="headerlink" title="错误描述"></a>错误描述</h1><p>此问题的由来是因为某天看用 <code>asciidoctor-maven-plugin</code> 生成的 swagger html 文档时, 发现有个链接没法跳转到指定的 header.如下图所示:</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/Swagger2Markup_bug-生成的_html_文档有问题当_ApiModel_value_包含斜线/link-can-not-jump.jpg" alt="link-can-not-jump"></p><p>经过排查发现是因为 <code>ApiModel</code> 的 <code>value</code> 包含 <code>/</code> 时会出现问题.以上面的问题为例, 写了一个简单的示例, 如下:</p><p><code>Controller</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@RequestMapping</span>(value= <span class="string">"..."</span>, method = RequestMethod.POST)</span><br><span class="line"><span class="meta">@ApiOperation</span>(value=<span class="string">"..."</span>, notes=<span class="string">"..."</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> BaseResponse <span class="title">createOrder</span><span class="params">(@Valid @RequestBody Order request, BindingResult bindingResult)</span> </span>{</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>Order</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 带有斜线 /</span></span><br><span class="line"><span class="meta">@ApiModel</span>(<span class="string">"order/create_order"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Order</span> </span>{</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="错误排查"><a href="#错误排查" class="headerlink" title="错误排查"></a>错误排查</h1><p>首先查看生成的 html 文件:</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></pre></td><td class="code"><pre><span class="line">// 跳转链接的 ID 是 _create_order</span><br><span class="line"><p><a href="#_create_order">create_order</a></p></span><br><span class="line"></span><br><span class="line">// ID 是 _order_create_order, 而不是 _create_order</span><br><span class="line"><h3 id="_order_create_order">order/create_order</h3></span><br></pre></td></tr></table></figure><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/Swagger2Markup_bug-生成的_html_文档有问题当_ApiModel_value_包含斜线/html-link-can-not-jump-to-header.jpg" alt="link-can-not-jump-to-header"></p><p>进一步排查生成的 <code>.adoc</code> 文件.在 <code>paths.adoc</code>、<code>definitions.adoc</code> 文件中搜索 <code>create_order</code> 相关的信息.</p><p><code>paths.adoc</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></pre></td><td class="code"><pre><span class="line">==== Parameters</span><br><span class="line"></span><br><span class="line">[options="header", cols=".^2a,.^3a,.^9a,.^4a"]</span><br><span class="line">|===</span><br><span class="line">|Type|Name|Description|Schema</span><br><span class="line">|**Body**|**request** +</span><br><span class="line">__required__|request|<<_create_order,create_order>></span><br><span class="line">|===</span><br></pre></td></tr></table></figure><p><code>definitions.adoc</code> 内容如下:</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">[[_order_create_order]]</span><br><span class="line">=== order/create_order</span><br></pre></td></tr></table></figure><p>很明显, 这两个 ID 是不一样的.就导致了生成的 html 文档也有问题.</p><p>这会导致如下两个问题:</p><ul><li>链接无法跳转到指定的 header</li><li>请求/响应示例无法生成</li></ul><h1 id="错误原因分析"><a href="#错误原因分析" class="headerlink" title="错误原因分析"></a>错误原因分析</h1><p>针对上面的两个问题, 分别做下错误原因分析:</p><ul><li><p>链接无法跳转到指定的 header: <code>GenericRef</code> 类有两个属性, <code>ref</code> 和 <code>simpleRef</code>.以上面的例子为例(<code>order/create_order</code>), <code>ref</code> 是 <code>#/definitions/order/create_order</code>, <code>simpleRef</code> 是 <code>create_order</code>.<code>simpleRef</code> 不包含 <code>order</code> 前缀.所以在生成链接 id 的时候, 可能是一个使用了 <code>simpleRef</code>, 另外一个使用了 <code>ref</code>, 所以应该统一使用其中一个.</p></li><li><p>请求/响应示例无法生成: <code>Swagger#getDefinitions()</code> 方法返回的是 <code>Map<String, Model></code>, 这个 map 里面包含了 <code>/order/create_order</code>, 但是不包含 <code>create_order</code>, 所以当生成示例的时候, 可能是使用的 <code>simpleRef</code> 从 map 中获取 <code>Model</code> 对象(此时肯定获取不到).如果使用 <code>ref</code>(去除 <code>#/definitions/</code> 前缀)可以从 map 中获取 <code>Model</code> 对象, 所以应该先使用 <code>simpleRef</code>, 如果获取不到 <code>Model</code>, 再使用 <code>ref</code>.</p></li></ul>]]></content>
<summary type="html">
<h1 id="错误描述"><a href="#错误描述" class="headerlink" title="错误描述"></a>错误描述</h1><p>此问题的由来是因为某天看用 <code>asciidoctor-maven-plugin</code> 生成的 swagge
</summary>
<category term="文档工具" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/"/>
<category term="swagger" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/swagger/"/>
<category term="swagger" scheme="https://ykgarfield.github.io/tags/swagger/"/>
<category term="swagger bug" scheme="https://ykgarfield.github.io/tags/swagger-bug/"/>
</entry>
<entry>
<title>xwiki 文档迁移到 confluence-(0) 概述</title>
<link href="https://ykgarfield.github.io/2018/12/25/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-(0)%20%E6%A6%82%E8%BF%B0/"/>
<id>https://ykgarfield.github.io/2018/12/25/文档工具/confluence/xwiki 文档迁移到 confluence-(0) 概述/</id>
<published>2018-12-24T16:00:00.000Z</published>
<updated>2020-01-03T01:17:32.711Z</updated>
<content type="html"><![CDATA[<p>本系列《xwiki 文档迁移到 confluence》主要记录了将 xwiki 文档同步迁移到 confluence 的过程.打算分为以下几个小节进行编写:</p><ul><li>迁移方案<br>本节描述了 xwiki 文档迁移到 confluence 的方案.目前并没有现成解决方案, 那么该怎么做迁移, 本节描述了迁移过程中所涉及到的问题以及使用什么样的方案进行迁移, 能够让迁移方案既毕竟简单, 又能节省成本. </li><li>遇到的坑及注意事项<br>在迁移过程中免不了遇到一些问题, 本节就描述了迁移过程中遇到的坑(主要是 xwiki 相关的), 以及要注意的一些事项.</li><li>如何将上传的 pdf 显示在 confluence 页面中<br>上传 pdf 附件到 confluence 中很简单, 但是如何在页面中直接查看 pdf 文件, 而不是到附件中去查看上传的 pdf 文件, 本节描述了这个过程, 让上传的 pdf 可以直接在页面中进行查看.</li><li>Jackson 如何动态生成 key<br>本节其实和此系列关系不大, 不过也是在这个过程中所遇到的问题, 所以也作为此系列的一节.我们知道 jackson 在序列化的时候可以根据字段名称或者注解来生成 key, 不过这些方式都是静态的.如何动态的生成 key, 本节提供了一种简单的解决方案可以达到此目的.</li></ul>]]></content>
<summary type="html">
<p>本系列《xwiki 文档迁移到 confluence》主要记录了将 xwiki 文档同步迁移到 confluence 的过程.打算分为以下几个小节进行编写:</p>
<ul>
<li>迁移方案<br>本节描述了 xwiki 文档迁移到 confluence 的方案.目前并没
</summary>
<category term="文档工具" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/tags/confluence/"/>
<category term="xwiki" scheme="https://ykgarfield.github.io/tags/xwiki/"/>
</entry>
<entry>
<title>Swagger bug-当 http 请求方法是 get 时 Operation.getConsumes() 不应该返回 application/json</title>
<link href="https://ykgarfield.github.io/2018/12/25/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/swagger/Swagger-bug-%E5%BD%93-http-%E8%AF%B7%E6%B1%82%E6%96%B9%E6%B3%95%E6%98%AF-get-%E6%97%B6-Operation.getConsumes()-%E4%B8%8D%E5%BA%94%E8%AF%A5%E8%BF%94%E5%9B%9E-application-json/"/>
<id>https://ykgarfield.github.io/2018/12/25/文档工具/swagger/Swagger-bug-当-http-请求方法是-get-时-Operation.getConsumes()-不应该返回-application-json/</id>
<published>2018-12-24T16:00:00.000Z</published>
<updated>2018-12-28T05:32:17.466Z</updated>
<content type="html"><![CDATA[<h1 id="问题由来"><a href="#问题由来" class="headerlink" title="问题由来"></a>问题由来</h1><p>相关的 issue 可见: <a href="https://github.com/springfox/springfox/issues/2815" target="_blank" rel="noopener">https://github.com/springfox/springfox/issues/2815</a></p><p>最近在用 swagger lib 的时候发现一个问题, 就是当 HTTP 请求方法是 <code>GET</code> 时候, 返回的 <code>consume</code> 是 <code>application/json</code>, 感觉有些不对劲, 于是乎就做了如下的实验:</p><p>比如有如下一个方法:</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">@Controller</span></span><br><span class="line"><span class="meta">@Api</span>(value =<span class="string">"SwaggerTestService"</span>, tags = <span class="string">"SwaggerTest"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SwaggerTestController</span> </span>{</span><br><span class="line"> <span class="meta">@RequestMapping</span>(value= <span class="string">"/SwaggerTest/getMethodTest"</span>,method = RequestMethod.GET)</span><br><span class="line"> <span class="meta">@ApiOperation</span>(value=<span class="string">"(SwaggerTestService) getMethodTest"</span>, notes=<span class="string">"(SwaggerTestService) getMethodTest"</span>)</span><br><span class="line"> <span class="function"><span class="keyword">public</span> BaseResponse <span class="title">getMethodTest</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当访问 <code>/v2/api-docs</code> 的时候返回的信息如下:</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></pre></td><td class="code"><pre><span class="line">"/SwaggerTest/getMethodTest": {</span><br><span class="line">"get": {</span><br><span class="line">"tags": [</span><br><span class="line"><span class="string">"SwaggerTest"</span></span><br><span class="line">],</span><br><span class="line">"summary": "(SwaggerTestService) getMethodTest",</span><br><span class="line">"description": "(SwaggerTestService) getMethodTest",</span><br><span class="line">"consumes": [</span><br><span class="line"><span class="string">"application/json"</span></span><br><span class="line">],</span><br><span class="line">"produces": [</span><br><span class="line"><span class="string">"*/*"</span></span><br><span class="line">]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以发现这里 <code>consumes</code> 返回的是 <code>application/json</code>, 稍微思考下, 会发现其实是不太对的.其实这就意味着 GET 请求的 <code>Content-Type</code> 是 <code>application/json</code>, 但是 HTTP GET 请求怎么会是 <code>application/json</code> 呢? 显然是不对的.相关的 GET 请求是否需要 <code>Content-Type</code> 讨论可见: <a href="https://stackoverflow.com/questions/5661596/do-i-need-a-content-type-for-http-get-requests" target="_blank" rel="noopener">https://stackoverflow.com/questions/5661596/do-i-need-a-content-type-for-http-get-requests</a>, 从中我们可以看出, 只有 <code>POST</code> 和 <code>PUT</code> 需要请求的 <code>Content-Type</code>.</p><p>因为是使用 swagger lib 相关 API 的时候发现的问题, 所以进一步发现是因为 <code>io.swagger.models.Operation#getConsumes()</code> 的返回值有问题.所以在我们使用此 API 的时候, 需要注意, 如果是 <code>GET</code> 请求, 那么是可以忽略此方法的返回值的.</p>]]></content>
<summary type="html">
<h1 id="问题由来"><a href="#问题由来" class="headerlink" title="问题由来"></a>问题由来</h1><p>相关的 issue 可见: <a href="https://github.com/springfox/springfox/
</summary>
<category term="文档工具" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/"/>
<category term="swagger" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/swagger/"/>
<category term="swagger" scheme="https://ykgarfield.github.io/tags/swagger/"/>
<category term="swagger bug" scheme="https://ykgarfield.github.io/tags/swagger-bug/"/>
</entry>
<entry>
<title>xwiki 文档迁移到 confluence-(2) 如何将上传的 pdf 显示在 confluence 页面中</title>
<link href="https://ykgarfield.github.io/2018/12/25/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-(2)%20%E5%A6%82%E4%BD%95%E5%B0%86%E4%B8%8A%E4%BC%A0%E7%9A%84%20pdf%20%E6%98%BE%E7%A4%BA%E5%9C%A8%20confluence%20%E9%A1%B5%E9%9D%A2%E4%B8%AD/"/>
<id>https://ykgarfield.github.io/2018/12/25/文档工具/confluence/xwiki 文档迁移到 confluence-(2) 如何将上传的 pdf 显示在 confluence 页面中/</id>
<published>2018-12-24T16:00:00.000Z</published>
<updated>2020-01-03T02:06:52.197Z</updated>
<content type="html"><![CDATA[<p>这是整个迁移过程中毕竟关键的一步, 也是为了实现友好的查看文档的关键步骤.前文中已经知道了怎么直接在页面中显示 PDF.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-2%E5%A6%82%E4%BD%95%E5%B0%86%E4%B8%8A%E4%BC%A0%E7%9A%84%20pdf%20%E6%98%BE%E7%A4%BA%E5%9C%A8%20confluence%20%E9%A1%B5%E9%9D%A2%E4%B8%AD/1.png" alt="1"></p><p>接下来就通过程序来实现.这个过程还是稍微有点复杂的, 所以也是作为一个单独的小节来写.</p><p>首选还是得通过抓包来总结出整个过程中都经历了哪些请求:</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">上传文件 -> 插入附件 -> 更新</span><br></pre></td></tr></table></figure><p>下面就以上面的流程来进行描述.虽然下面的流程看似也不复杂, 不过需要一步步的进行摸索, 其中也可能遇到一些问题, 需要不断的反复进行抓包实验.</p><h2 id="上传文件"><a href="#上传文件" class="headerlink" title="上传文件"></a>上传文件</h2><p>上传文件这一步我们不要通过页面中的请求来做, 这个稍微复杂了一点.confluence 提供了 restful api 可以将附件上传到文档中.</p><p>具体可见: <a href="https://docs.atlassian.com/ConfluenceServer/rest/6.12.2/#content/{id}/child/attachment-createAttachments" target="_blank" rel="noopener">https://docs.atlassian.com/ConfluenceServer/rest/6.12.2/#content/{id}/child/attachment-createAttachments</a></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">上传附件: /rest/api/content/{id}/child/attachment</span><br></pre></td></tr></table></figure><h2 id="插入附件"><a href="#插入附件" class="headerlink" title="插入附件"></a>插入附件</h2><p>插入附件需要进行如下的请求:</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">/rest/tinymce/1/macro/placeholder</span><br></pre></td></tr></table></figure><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-2%E5%A6%82%E4%BD%95%E5%B0%86%E4%B8%8A%E4%BC%A0%E7%9A%84%20pdf%20%E6%98%BE%E7%A4%BA%E5%9C%A8%20confluence%20%E9%A1%B5%E9%9D%A2%E4%B8%AD/2.png" alt="2"></p><p>请求的 JSON 格式稍微解释下:</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">{</span><br><span class="line">// wiki page 的 id</span><br><span class="line">"contentId": "2818258",</span><br><span class="line">"macro": {</span><br><span class="line">// 固定值</span><br><span class="line">"name": "view-file",</span><br><span class="line">"params": {</span><br><span class="line">// 文件名称</span><br><span class="line">"name": "测试文档1.pdf",</span><br><span class="line">// 可以传固定值</span><br><span class="line">"height": "250"</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>返回的内容是一串字符串, 这个字符的值我们需要保存下, 作为后续请求的信息.<br>不过我们要注意上面返回的字符串中 <code>src</code> 后面的内容:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-2%E5%A6%82%E4%BD%95%E5%B0%86%E4%B8%8A%E4%BC%A0%E7%9A%84%20pdf%20%E6%98%BE%E7%A4%BA%E5%9C%A8%20confluence%20%E9%A1%B5%E9%9D%A2%E4%B8%AD/3.png" alt="3"></p><p>可以看到, 返回的是类似于 <code>/plugins/servlet/view-file-macro/placeholder?type=PDF+Document</code> 的信息.不过也可能会返回如下的信息:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-2%E5%A6%82%E4%BD%95%E5%B0%86%E4%B8%8A%E4%BC%A0%E7%9A%84%20pdf%20%E6%98%BE%E7%A4%BA%E5%9C%A8%20confluence%20%E9%A1%B5%E9%9D%A2%E4%B8%AD/4.png" alt="4"></p><p>这次返回的是类似于 <code>/rest/documentConversion/latest/conversion/thumbnail/4849682/1?attachmentId=</code> 的请求.</p><p>是不是有点疑惑, 确实是让人感觉很奇怪.怎么会出现这样的现象, 上面的情形是通过如下的操作得以复现的:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-2%E5%A6%82%E4%BD%95%E5%B0%86%E4%B8%8A%E4%BC%A0%E7%9A%84%20pdf%20%E6%98%BE%E7%A4%BA%E5%9C%A8%20confluence%20%E9%A1%B5%E9%9D%A2%E4%B8%AD/5.png" alt="5"></p><p>准确的来说, 后续请求是要发送类似于 <code>/rest/documentConversion/latest/conversion/thumbnail/4849682/1?attachmentId=</code> 这样的信息而不是 <code>/plugins/servlet/view-file-macro/placeholder?type=PDF+Document</code> 这样的信息.现在先结束关于这个问题的探讨, 后续再接着探索.</p><p>插入附件请求之后还没结束, 没法直接进行更新请求.这也是在实验过程中遇到的一个问题.还需要等待文档转换完成.</p><h3 id="文档转换结果查询"><a href="#文档转换结果查询" class="headerlink" title="文档转换结果查询"></a>文档转换结果查询</h3><p>文档转换结果查询需要调用下面的请求:</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">查询文档转换结果: /rest/documentConversion/latest/conversion/thumbnail/results</span><br></pre></td></tr></table></figure><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-2%E5%A6%82%E4%BD%95%E5%B0%86%E4%B8%8A%E4%BC%A0%E7%9A%84%20pdf%20%E6%98%BE%E7%A4%BA%E5%9C%A8%20confluence%20%E9%A1%B5%E9%9D%A2%E4%B8%AD/6.png" alt="6"></p><p>上面的文档转换请求只有两次, 但是实际情况中并一定是 2 次, 我们需要进行多次查询, 如果是 202, 就休眠一会再继续查询, 直到返回码是 200 为止.</p><h3 id="发送-drafts-请求"><a href="#发送-drafts-请求" class="headerlink" title="发送 drafts 请求"></a>发送 drafts 请求</h3><p><code>drafts</code> 是草稿的意思, 意思就是我们得接着再发起一个草稿(drafts)的请求.</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">/rest/tinymce/1/drafts</span><br></pre></td></tr></table></figure><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-2%E5%A6%82%E4%BD%95%E5%B0%86%E4%B8%8A%E4%BC%A0%E7%9A%84%20pdf%20%E6%98%BE%E7%A4%BA%E5%9C%A8%20confluence%20%E9%A1%B5%E9%9D%A2%E4%B8%AD/7.png" alt="7"></p><p>注意上面的 <code>content</code> 内容, 如果是类似于 <code>/plugins/servlet/view-file-macro</code> 的, 我们需要转换为 <code>/rest/documentConversion</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></pre></td><td class="code"><pre><span class="line"><img class="editor-inline-macro" height="250" src="/plugins/servlet/view-file-macro/placeholder?type=PDF+Document&amp;name=%E6%B5%8B%E8%AF%95xxx.pdf&amp;attachmentId=2490399&amp;version=1&amp;mimeType=application%2Fpdf&amp;height=250&amp;thumbnailStatus=202" data-macro-name="view-file" data-macro-parameters="height=250|name=测试xxx.pdf" data-macro-schema-version="1"></span><br><span class="line"></span><br><span class="line"><img class="editor-inline-macro" height="250" src="/rest/documentConversion/latest/conversion/thumbnail/2490399/1?attachmentId=2490399&amp;version=1&amp;mimeType=application%2Fpdf&amp;height=250&amp;thumbnailStatus=200" data-macro-name="view-file" data-macro-parameters="height=250|name=测试xxx.pdf" data-macro-schema-version="1"></span><br></pre></td></tr></table></figure><h2 id="更新-drafts"><a href="#更新-drafts" class="headerlink" title="更新 drafts"></a>更新 drafts</h2><p>上面的操作都完成之后, 接下来要做的就是最后的确认更新操作:</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">/rest/api/content/{id}?status=draft</span><br></pre></td></tr></table></figure><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-2%E5%A6%82%E4%BD%95%E5%B0%86%E4%B8%8A%E4%BC%A0%E7%9A%84%20pdf%20%E6%98%BE%E7%A4%BA%E5%9C%A8%20confluence%20%E9%A1%B5%E9%9D%A2%E4%B8%AD/8.png" alt="8"></p><p>到这里为止, 我们展示了上传附件到 confluence, 以及如何将 PDF 文件显示在页面中.</p><p>整个过程稍微复杂了一点, 不过也不是很难.有些请求并没有对应的文档说明, 需要我们自己探索, 做实验进行验证.</p><p>最终用一张图示总结下所涉及的过程:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-2%E5%A6%82%E4%BD%95%E5%B0%86%E4%B8%8A%E4%BC%A0%E7%9A%84%20pdf%20%E6%98%BE%E7%A4%BA%E5%9C%A8%20confluence%20%E9%A1%B5%E9%9D%A2%E4%B8%AD/9.png" alt="9"></p>]]></content>
<summary type="html">
<p>这是整个迁移过程中毕竟关键的一步, 也是为了实现友好的查看文档的关键步骤.前文中已经知道了怎么直接在页面中显示 PDF.</p>
<p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master
</summary>
<category term="文档工具" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/tags/confluence/"/>
<category term="xwiki" scheme="https://ykgarfield.github.io/tags/xwiki/"/>
</entry>
<entry>
<title>xwiki 文档迁移到 confluence-(3) 遇到的坑及注意事项</title>
<link href="https://ykgarfield.github.io/2018/12/25/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-(3)%20%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91%E5%8F%8A%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/"/>
<id>https://ykgarfield.github.io/2018/12/25/文档工具/confluence/xwiki 文档迁移到 confluence-(3) 遇到的坑及注意事项/</id>
<published>2018-12-24T16:00:00.000Z</published>
<updated>2020-01-03T02:07:03.813Z</updated>
<content type="html"><![CDATA[<p>本文总结了 xwiki 文档迁移到 confluence 时遇到的坑以及要注意的一些情况.</p><h2 id="不靠谱的-xwiki-restful-api"><a href="#不靠谱的-xwiki-restful-api" class="headerlink" title="不靠谱的 xwiki restful api"></a>不靠谱的 xwiki restful api</h2><p>wiki 提供了一套 rest api, 可以用来对 wiki 文档进行一系列的操作.在这里主要用到了访问 wiki 文档信息、访问该文档的子文档新等 api.</p><p>一开始并没有发现问题, 因为觉得可以用来访问到文档信息即可.但是在后来的测试过程中遇到了几个问题:</p><ul><li>获取的子文档不全</li><li>获取到了子文档的子文档</li><li>获取了当前的文档</li><li>获取到了平级的文档</li></ul><p>对于以上的几个问题就不得不吐槽下了, 实在让人很糟心.既然上面 restful api 不好用, 那我们该怎么获取子文档呢? </p><p>转念一想, 既然 xwiki 页面上能够正常获取到文档, 而且导航菜单是很经典的树形菜单.一般树形菜单都是通过 ajax 请求获取到 json 格式的数据, 然后在前台展示.那就看看 xwiki 是不是也是这样的?</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-3%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91%E5%8F%8A%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/1.png" alt="1"></p><p>果然有几个 ajax 请求, 返回的是 json 格式的数据.单独打开每个链接发现果然如之前所想的一样.</p><p>从这个链接中我们得到了可以获得到子文档的请求:</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></pre></td><td class="code"><pre><span class="line"># {0} 部分是类似 path1/path2 的路径</span><br><span class="line"># {1} 部分是类似 path.path2 的路径</span><br><span class="line">/xwiki/bin/get/{0}/WebHome?outputSyntax=plain&sheet=XWiki.DocumentTree&root=document:xwiki:{1}.WebHome&data=children&id=%23</span><br></pre></td></tr></table></figure><p>访问某个文档的请求:</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">/xwiki/bin/get/{0}/WebHome?outputSyntax=plain&sheet=XWiki.DocumentTree&showAttachments=false&showTranslations=false&root=document:xwiki:{1}.WebHome&showRoot=true&data=children&id=%23</span><br></pre></td></tr></table></figure><h2 id="xwiki-路径与标题不匹配问题"><a href="#xwiki-路径与标题不匹配问题" class="headerlink" title="xwiki 路径与标题不匹配问题"></a>xwiki 路径与标题不匹配问题</h2><p>这样情况产生的原因是创建文档的时候两次的标题不一致导致的.比如以下的情况就导致了路径和标题不一样.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-3%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91%E5%8F%8A%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/2.png" alt="2"></p><p>对于要在 confluence 中创建同名的文档来说, 需要处理好这种问题, 组好是取标题的名称而不是路径的名称.</p><h2 id="xwiki-获取子文档需要处理分页的情况"><a href="#xwiki-获取子文档需要处理分页的情况" class="headerlink" title="xwiki 获取子文档需要处理分页的情况"></a>xwiki 获取子文档需要处理分页的情况</h2><p>在某个文档下面有多个子文档的情况下, 如果子文档的数量较多, 那么可能会产生类似于分页的情况.也就是说某个文档的子文档下面会出现如下的情况:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-3%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91%E5%8F%8A%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/3.png" alt="3"></p><p>这个时候就需要我们能够处理好这种情况, 查看返回的数据, 可以发现最后一条记录和其他的记录是不一样的.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-3%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91%E5%8F%8A%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/4.png" alt="4"></p><h2 id="xwiki-获取文档信息与从父文档获取的该文档信息可能不一致"><a href="#xwiki-获取文档信息与从父文档获取的该文档信息可能不一致" class="headerlink" title="xwiki 获取文档信息与从父文档获取的该文档信息可能不一致"></a>xwiki 获取文档信息与从父文档获取的该文档信息可能不一致</h2><p>这样情况是比如有一个文档名称为 <code>test</code>, 其父目录为 <code>DBA</code>, 那么从获取文档的接口获取 <code>test</code> 信息和从 <code>DBA</code> 获取其子文档新得到的 <code>test</code> 文档信息, 两者的信息有可能不一致.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-3%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91%E5%8F%8A%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/5.png" alt="5"></p><p>所以从上面的几种情况中可以得到一个比较完整的没什么问题的获取子文档的接口:</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></pre></td><td class="code"><pre><span class="line"># {0} 部分是类似 path1/path2 的路径</span><br><span class="line"># {1} 部分是类似 path.path2 的路径</span><br><span class="line"># {2} 部分是 offset 的值, 第一次查询传 0, 后面查询的时候根据上一次查询的结果传递此值</span><br><span class="line"># showAttachments=false 可以不把附件作为子文档对待</span><br><span class="line">/xwiki/bin/get/{0}/WebHome?outputSyntax=plain&sheet=XWiki.DocumentTree&showAttachments=false&showTranslations=false&exclusions=document:xwiki:Sandbox.WebHome&exclusions=document:xwiki:Help.WebHome&exclusions=document:xwiki:Menu.WebHome&exclusions=document:xwiki:XWiki.WebHome&data=children&id=document:xwiki:{1}.WebHome&offset={2}</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">
<p>本文总结了 xwiki 文档迁移到 confluence 时遇到的坑以及要注意的一些情况.</p>
<h2 id="不靠谱的-xwiki-restful-api"><a href="#不靠谱的-xwiki-restful-api" class="headerlink" ti
</summary>
<category term="文档工具" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/tags/confluence/"/>
<category term="xwiki" scheme="https://ykgarfield.github.io/tags/xwiki/"/>
</entry>
<entry>
<title>xwiki 文档迁移到 confluence-(4) Jackson 如何动态生成 key</title>
<link href="https://ykgarfield.github.io/2018/12/25/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-(4)%20Jackson%20%E5%A6%82%E4%BD%95%E5%8A%A8%E6%80%81%E7%94%9F%E6%88%90%20key/"/>
<id>https://ykgarfield.github.io/2018/12/25/文档工具/confluence/xwiki 文档迁移到 confluence-(4) Jackson 如何动态生成 key/</id>
<published>2018-12-24T16:00:00.000Z</published>
<updated>2020-01-03T02:07:32.404Z</updated>
<content type="html"><![CDATA[<h1 id="问题的由来"><a href="#问题的由来" class="headerlink" title="问题的由来"></a>问题的由来</h1><p>在讨论这个话题之前, 先回顾下之前第二节-如何将上传的 pdf 显示在 confluence 页面中的最后一个请求, 更新 drafts.看下其请求数据:</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><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><br><span class="line"> "status":"current",</span><br><span class="line"> "title":"测试文档",</span><br><span class="line"> "space":{</span><br><span class="line"> "key":"SPC"</span><br><span class="line"> },</span><br><span class="line"> "body":{</span><br><span class="line"> "editor":{</span><br><span class="line"> "value":"<p><img class="editor-inline-macro" height="250" src="http://localhost:8090/rest/documentConversion/latest/conversion/thumbnail/4849682/1?attachmentId=4849682&version=1&mimeType=application%2Fpdf&height=250&thumbnailStatus=200" data-macro-name="view-file" data-macro-parameters="height=250|name=测试xxx.pdf" data-macro-schema-version="1" /></p>",</span><br><span class="line"> "representation":"editor",</span><br><span class="line"> "content":{</span><br><span class="line"> "id":"2818258"</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> "id":"2818258",</span><br><span class="line"> "type":"page",</span><br><span class="line"> "version":{</span><br><span class="line"> "number":18,</span><br><span class="line"> "message":"",</span><br><span class="line"> "minorEdit":false,</span><br><span class="line"> "syncRev":"0.AjEmNKvqWOSCexKgWizfyQ0.3"</span><br><span class="line"> },</span><br><span class="line"> "ancestors":[</span><br><span class="line"> {</span><br><span class="line"> "id":"65584",</span><br><span class="line"> "type":"page"</span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>需要关注是 <code>body</code> 部分.其中有一个 <code>editor</code> 节点.因为之前在导入 confluence 相关 jar 的时候, 发现有一个类 <code>ContentRepresentation</code>:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-4Jackson%20%E5%A6%82%E4%BD%95%E5%8A%A8%E6%80%81%E7%94%9F%E6%88%90%20key/1.png" alt="1"></p><p>可以看到里面定义了一系列的动作, <code>raw</code>、<code>storage</code>、<code>editor</code> 等.所以再看看上面的请求, 觉得这个 <code>body.editor</code> 中的 <code>editor</code> 应该不是一个静态的值, 而是动态的, 也就是说如果是 <code>storage</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><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><br><span class="line"> "status":"current",</span><br><span class="line"> "title":"测试文档",</span><br><span class="line"> "space":{</span><br><span class="line"> "key":"SPC"</span><br><span class="line"> },</span><br><span class="line"> "body":{</span><br><span class="line"> "storage":{</span><br><span class="line"> "value":"...",</span><br><span class="line"> "representation":"storage",</span><br><span class="line"> "content":{</span><br><span class="line"> "id":"2818258"</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下图进行了下对比:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-4Jackson%20%E5%A6%82%E4%BD%95%E5%8A%A8%E6%80%81%E7%94%9F%E6%88%90%20key/2.png" alt="2"></p><p>那么究竟是不是这样的, 我也没有进行过进一步的考究.不过我想, 不管是不是这样的, 假如遇到这种情况的话, 我们该怎么做.如何把对象序列化为这样的 JSON 格式.当然可以简单粗暴地将就将字段直接声明为 <code>storage</code> 活着 <code>editor</code>, 是一种办法, 就是不够好.接下来就看看如何使用 Jackson 实现这样的情况:</p><h1 id="Jackson-动态生成-key"><a href="#Jackson-动态生成-key" class="headerlink" title="Jackson 动态生成 key"></a>Jackson 动态生成 key</h1><h2 id="自定义注解"><a href="#自定义注解" class="headerlink" title="自定义注解"></a>自定义注解</h2><p>首选我们自定义一个注解, 就是说如果 Jackson 在解析的时候如果遇到这样的注解, 使用我们自己的逻辑进行处理.</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></pre></td><td class="code"><pre><span class="line">@Documented</span><br><span class="line">@Retention(RetentionPolicy.RUNTIME)</span><br><span class="line">@Target({ElementType.FIELD})</span><br><span class="line">public @interface RepresentationType {</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在需要进行动态生成 key 的字段上加上此注解:</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></pre></td><td class="code"><pre><span class="line">@Data</span><br><span class="line">public class WikiContentBody {</span><br><span class="line"> /**</span><br><span class="line"> * 根据不同的 Representation 生成对应的 key</span><br><span class="line"> */</span><br><span class="line"> @RepresentationType</span><br><span class="line"> private WikiContentRepresentation contentRepresentation;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="实现自定义的-JacksonAnnotationIntrospector"><a href="#实现自定义的-JacksonAnnotationIntrospector" class="headerlink" title="实现自定义的 JacksonAnnotationIntrospector"></a>实现自定义的 JacksonAnnotationIntrospector</h2><p>这一步需要继承 <code>JacksonAnnotationIntrospector</code> 类, 并覆盖 <code>isAnnotationBundle()</code>、<code>findNameForSerialization()</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><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><br><span class="line"> * 动态替换 WikiContentBody#contentRepresentation 在序列化过程中的名称.</span><br><span class="line"> */</span><br><span class="line">public class RepresentationTypeSerializer extends JacksonAnnotationIntrospector implements Versioned {</span><br><span class="line"> @Override</span><br><span class="line"> public boolean isAnnotationBundle(Annotation ann) {</span><br><span class="line"> return false;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> @Override</span><br><span class="line"> public PropertyName findNameForSerialization(Annotated a) {</span><br><span class="line"> return null;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="将-JacksonAnnotationIntrospector-设置到-Jackson-序列化过程中"><a href="#将-JacksonAnnotationIntrospector-设置到-Jackson-序列化过程中" class="headerlink" title="将 JacksonAnnotationIntrospector 设置到 Jackson 序列化过程中"></a>将 JacksonAnnotationIntrospector 设置到 Jackson 序列化过程中</h2><p>要让 Jackson 使用我们自定义的 <code>RepresentationTypeSerializer</code> 实现, 简单的将其设置到 <code>ObjectMapper</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></pre></td><td class="code"><pre><span class="line">ObjectMapper objectMapper = new ObjectMapper();</span><br><span class="line">AnnotationIntrospector representationTypeSerializer = new RepresentationTypeSerializer();</span><br><span class="line">// 使用我们自定义的 AnnotationIntrospector</span><br><span class="line">objectMapper.setAnnotationIntrospector(representationTypeSerializer);</span><br></pre></td></tr></table></figure><h2 id="RepresentationTypeSerializer-的具体实现"><a href="#RepresentationTypeSerializer-的具体实现" class="headerlink" title="RepresentationTypeSerializer 的具体实现"></a>RepresentationTypeSerializer 的具体实现</h2><p>上面我们只是稍微简单的说明了下 <code>RepresentationTypeSerializer</code> 需要覆盖哪些方法, 并没有给出具体的实现.首先是 <code>isAnnotationBundle()</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></pre></td><td class="code"><pre><span class="line">@Override</span><br><span class="line">public boolean isAnnotationBundle(Annotation ann) {</span><br><span class="line"> Class<?> cls = ann.annotationType();</span><br><span class="line"> // 是否可以处理 RepresentationType 注解</span><br><span class="line"> return RepresentationType.class == cls || super.isAnnotationBundle(ann);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>接下来是 <code>findNameForSerialization()</code> 方法.先看下此方法的签名:</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">public PropertyName findNameForSerialization(Annotated a) {</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>方法中只有一个参数 <code>Annotated</code>, 那我们如何知道怎么进行替换呢? 替换成什么值呢? <code>Annotated</code> 似乎并没有提供什么有用的方法, 没法取到 <code>WikiContentBody</code> 中的 <code>contentRepresentation</code> 字段值.</p><h2 id="利用-ThreadLocal-取得动态动态-key"><a href="#利用-ThreadLocal-取得动态动态-key" class="headerlink" title="利用 ThreadLocal 取得动态动态 key"></a>利用 ThreadLocal 取得动态动态 key</h2><p>刚刚问题就卡在了 <code>findNameForSerialization(Annotated a)</code> 方法没有办法取得动态的 key.只要解决了这个问题, 那么一切都可以顺利进行了.</p><p>这个时候就得搬出 <code>ThreadLocal</code>.也就是说在序列化之前将要动态生成的 key 放到 <code>ThreadLocal</code> 里.看下具体的实现.</p><p>首先是 <code>ThreadLocal</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><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">public class RepresentationTypeThreadLocal {</span><br><span class="line"> private static ThreadLocal<ContentRepresentation> CR_THREAD_LOCAL = new ThreadLocal<>();</span><br><span class="line"></span><br><span class="line"> public static void set(ContentRepresentation representation) {</span><br><span class="line"> CR_THREAD_LOCAL.set(representation);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> public static ContentRepresentation get() {</span><br><span class="line"> return CR_THREAD_LOCAL.get();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> public static void remove() {</span><br><span class="line"> CR_THREAD_LOCAL.remove();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>接下来是 <code>findNameForSerialization()</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><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">@Override</span><br><span class="line">public PropertyName findNameForSerialization(Annotated a) {</span><br><span class="line">RepresentationType type = a.getAnnotation(RepresentationType.class);</span><br><span class="line">if (type != null) {</span><br><span class="line">String name = getPropertyName();</span><br><span class="line">if (name != null) {</span><br><span class="line">return PropertyName.construct(name);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">return super.findNameForSerialization(a);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> private String getPropertyName() {</span><br><span class="line"> // 利用 ThreadLocal 取得动态 key</span><br><span class="line">ContentRepresentation representation = RepresentationTypeThreadLocal.get();</span><br><span class="line">if (representation != null) {</span><br><span class="line">return representation.getRepresentation();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在序列化之前首先先调用类似于如下的代码:</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">RepresentationTypeThreadLocal.set(ContentRepresentation.EDITOR);</span><br></pre></td></tr></table></figure><p>测试打印结果如下:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-4Jackson%20%E5%A6%82%E4%BD%95%E5%8A%A8%E6%80%81%E7%94%9F%E6%88%90%20key/3.png" alt="3"></p>]]></content>
<summary type="html">
<h1 id="问题的由来"><a href="#问题的由来" class="headerlink" title="问题的由来"></a>问题的由来</h1><p>在讨论这个话题之前, 先回顾下之前第二节-如何将上传的 pdf 显示在 confluence 页面中的最后一个请求,
</summary>
<category term="文档工具" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/tags/confluence/"/>
<category term="xwiki" scheme="https://ykgarfield.github.io/tags/xwiki/"/>
</entry>
<entry>
<title>xwiki 文档迁移到 confluence-1 迁移方案</title>
<link href="https://ykgarfield.github.io/2018/12/25/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-(1)%20%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88/"/>
<id>https://ykgarfield.github.io/2018/12/25/文档工具/confluence/xwiki 文档迁移到 confluence-(1) 迁移方案/</id>
<published>2018-12-24T16:00:00.000Z</published>
<updated>2020-01-03T02:02:49.348Z</updated>
<content type="html"><![CDATA[<h1 id="同步方案的探索"><a href="#同步方案的探索" class="headerlink" title="同步方案的探索"></a>同步方案的探索</h1><p>目前 xwiki 文档迁移到 confluence 还没有什么比较成熟或者现成的解决方案.google 搜索 <code>xwiki migrate confluence</code>, 有相关的文章,也没有提供好的解决方案.在 confluence 官网中也可以发现官方也不提供解决方案.毕竟太复杂(<a href="https://migrations.atlassian.net/wiki/spaces/UWC/overview).首先" target="_blank" rel="noopener">https://migrations.atlassian.net/wiki/spaces/UWC/overview).首先</a> xwiki 和 confluence 两者使用的编辑器是不一样的, 最终各自保存的数据也包含了自己的语法格式.要将 xwiki 的语法格式转换为 confluence, 是很复杂的事情.要把两种平台编辑器的语法格式都摸清楚, 然后再进行转换, 想想都觉得太复杂了.下图是通过 xwiki restful api 获取到一段文档的内容:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-1%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88/1.png" alt="1"></p><p>可以看到红色框中是 xwiki 自己的语法格式.类似的语法格式还有很多, 我们想都搞清楚, 确实不是一件简单的事情.既然这种最原始的方式是行不通的, 那么该咋办?</p><p>于是点到 xwiki 文档中进行进一步的探索.发现可以导出文档, 并且是 PDF 的.这无疑给我们带来一点希望.但是发现 confluence 是没有办法导入 PDF 文件的, 只能导入 doc 文件.不过可以上传附件 PDF 到 confluence 中.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-1%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88/2.png" alt="2"></p><p>不过把 PDF 作为附件上传到 confluence 文档中是不是不太友好, 每次查看文档的时候还得去查看附件, 再查看 PDF 文件.虽然流程也不复杂, 不过还是稍微显得有点麻烦.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-1%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88/3.png" alt="3"></p><p>那么有什么方式可以做到直接在页面中显示附件吗? 可以按照如下的步骤:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-1%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88/4.png" alt="4"></p><p>最终的效果如下图:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-1%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88/5.png" alt="5"></p><p>点击 PDF 文件就可以直接查看其内容.这样的话就简单多了.但是如何使用程序来实现整个流程, 这部分内容留到了第 2 节-如何将上传的 pdf 显示在 confluence 页面中.</p><p>到这里, 其实我们差不多已经摸索出了一种简单可行的迁移方案.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-1%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88/6.png" alt="6"></p><p>只不过以上的流程我们都是通过程序来实现.而不是人工操作.</p><p>不过到这里还是有个疑问, confuence 中不是可以导入 doc 吗? 我们不能将 PDF 转换为 doc 吗?</p><p>是可以转换的, 不过由于这里使用的 Java 来实现.Java 相关的库中似乎没有可以实现将 PDF 转为 word 的库.尝试了几个, 效果都不怎么好, 或者比较繁琐, 最终也是放弃了.不过肯定有其它相关的编程语言有类似的库可以做到这点.要不然网上也不会有一堆的 PDF 转 word 的网站.一搜一大堆:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-1%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88/7.png" alt="7"></p><p>这样我们不就可以调用这些网站的服务来把 PDF 转为 word.这确实是一种好的方式.而且 confluence 导入 doc 之后, 可以直接在页面中显示其内容, 是最友好的一种方式.导入 doc 的效果如下图:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-1%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88/8.png" alt="8"></p><p>不过吧, 毕竟依赖了第三方, 而且假如有一些机密信息, 经过这种网站进行转换, 似乎是不太好的, 让人也不放心.</p><p>那么我们只能退而求其次, 通过上传 PDF 到 confluence 的方式来完成 xwiki 文档迁移到 confluence.</p><p>不过上面也给我们提供了另外一种思路, 假如可以做到将 PDF 转 doc 有毕竟完美的方式, 那么我们也可以使用这种方案(当然不要依赖于第三方服务).</p><h1 id="同步方案的其它问题"><a href="#同步方案的其它问题" class="headerlink" title="同步方案的其它问题"></a>同步方案的其它问题</h1><h2 id="如何知道-xwiki-文档变更了"><a href="#如何知道-xwiki-文档变更了" class="headerlink" title="如何知道 xwiki 文档变更了"></a>如何知道 xwiki 文档变更了</h2><p>上面我们大体上已经知道了怎么做迁移, 不过还有一些问题没有解决.比如 xwiki 的文档更新了, 怎么知道更新了?</p><p>可以从 xwiki 中的记录获取其版本信息.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-1%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88/9.png" alt="9"></p><p>这里的版本信息可以通过 xwiki 的 restful API 来获取.</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">/xwiki/rest/wikis/xwiki/spaces/{0}/pages/WebHome/history?media=json</span><br></pre></td></tr></table></figure><p>当然了既然我们想通过版本号来判断 xwiki 是否变更了, 那我们就得把这个数据存储下来, 这样才能再下次同步的数据进行比较.</p><h2 id="如何做同步数据的存储"><a href="#如何做同步数据的存储" class="headerlink" title="如何做同步数据的存储"></a>如何做同步数据的存储</h2><p>同步记录数据的存储, 为了简单起见, 我们可以用 h2 做本地持久化, 这样也就不需要还得单独弄个 MySQL 数据库, 避免了运维和 DBA 的介入的麻烦.</p><p>为了方便以后对表进行 DDL 操作, 我们通过 JPA 读取 <code>Entity</code>, 自动生成 DDL 语句, 并且配置如果有更新的话就进行更新.</p><p><strong>Entity 配置示意</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><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">@Data</span><br><span class="line">@Entity</span><br><span class="line">@Table(name = "xwiki_sync_wiki",</span><br><span class="line"> // 指定索引</span><br><span class="line"> indexes = {</span><br><span class="line"> @Index(name = "idx_wiki_id", columnList = "wiki_id", unique = true),</span><br><span class="line"> ...</span><br><span class="line"> })</span><br><span class="line">public class XwikiSyncXwikiEntity {</span><br><span class="line"> @Id</span><br><span class="line"> @Column(name = "id", nullable = false)</span><br><span class="line"> @GeneratedValue(strategy = GenerationType.AUTO)</span><br><span class="line"> private Integer id;</span><br><span class="line"></span><br><span class="line"> /**</span><br><span class="line"> * wiki 的 page id</span><br><span class="line"> */</span><br><span class="line"> @Column(name = "wiki_id", nullable = false)</span><br><span class="line"> private String wikiId;</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>application.yml</code> 中的 <code>datasource</code> 和 <code>jpa</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></pre></td><td class="code"><pre><span class="line">spring:</span><br><span class="line"> datasource:</span><br><span class="line"> # 将 h2 的数据持久化到本地</span><br><span class="line"> url: jdbc:h2:file:E:\\tmp\\h2\\document;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1</span><br><span class="line"> ...</span><br><span class="line"> jpa:</span><br><span class="line"> hibernate:</span><br><span class="line"> # 配置为 update</span><br><span class="line"> ddl-auto: update</span><br></pre></td></tr></table></figure><p>那么接下来我们就按照上面的思路使用程序来一步步的实现上面的方案.首先要 解决的一个问题, 也是我们主要关系的问题, 如何将 PDF 直接显示在 confluence 页面中.</p><h1 id="同步方案流程"><a href="#同步方案流程" class="headerlink" title="同步方案流程"></a>同步方案流程</h1><p>下面以一张图示来描述同步方案的整个流程:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-1%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88/10.png" alt="10"></p><h1 id="题外话-通过第三方网站服务将-PDF-转为-doc"><a href="#题外话-通过第三方网站服务将-PDF-转为-doc" class="headerlink" title="题外话: 通过第三方网站服务将 PDF 转为 doc"></a>题外话: 通过第三方网站服务将 PDF 转为 doc</h1><p>在摸索方案的过程中, 也是尝试过一个网站的服务将 PDF 转为 doc, 不过此网站并没有提供现成的 resful api, 不过我们可以通过 debug 和抓包的方式来观察网站是如何将 PDF 转为 doc 的.</p><p>这里以 <a href="https://pdf2doc.com/zh/" target="_blank" rel="noopener">https://pdf2doc.com/zh/</a> 为例.通过抓包发现将 PDF 转为 doc 就经历了下面的几个请求.还是比较简单的, 不过就是看着这个 URL 有点奇怪.不过我们可以将他们当做是随机生成的一些字符串.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/xwiki%20%E6%96%87%E6%A1%A3%E8%BF%81%E7%A7%BB%E5%88%B0%20confluence-1%E8%BF%81%E7%A7%BB%E6%96%B9%E6%A1%88/11.png" alt="11"></p><p>将以上的几个请求稍微总结下可以得出 PDF 转 doc 的请求流程:</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></pre></td><td class="code"><pre><span class="line">上传文件 -> 等待处理完成 -> 下载文件</span><br><span class="line"></span><br><span class="line">上传: https://pdf2doc.com/upload/dxn8aixisl27ifd3</span><br><span class="line">转换: https://pdf2doc.com/zh/convert/dxn8aixisl27ifd3/o_1crr4a2gi10f81ai9at2n1a1mtla?rnd=0.8705434575071989</span><br><span class="line">查询状态: https://pdf2doc.com/zh/status/dxn8aixisl27ifd3/o_1crr4a2gi10f81ai9at2n1a1mtla?rnd=0.05654307332374309</span><br><span class="line">查询状态: https://pdf2doc.com/zh/status/dxn8aixisl27ifd3/o_1crr4a2gi10f81ai9at2n1a1mtla?rnd=0.9731785120585317</span><br><span class="line">下载: https://pdf2doc.com/zh/download/dxn8aixisl27ifd3/o_1crr4a2gi10f81ai9at2n1a1mtla/test.doc?rnd=0.781468423890326</span><br></pre></td></tr></table></figure><p>上面请求过程中的 <code>dxn8aixisl27ifd3</code> 可以将其当做是文件的标识(随机生成的一些字符串).</p><p>有兴趣的可以试一下, 也是非常的简单.</p>]]></content>
<summary type="html">
<h1 id="同步方案的探索"><a href="#同步方案的探索" class="headerlink" title="同步方案的探索"></a>同步方案的探索</h1><p>目前 xwiki 文档迁移到 confluence 还没有什么比较成熟或者现成的解决方案.googl
</summary>
<category term="文档工具" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/tags/confluence/"/>
<category term="xwiki" scheme="https://ykgarfield.github.io/tags/xwiki/"/>
</entry>
<entry>
<title>confluence-如何通过程序在 confluence 中创建 markdown 文档</title>
<link href="https://ykgarfield.github.io/2018/12/23/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/confluence-%E5%A6%82%E4%BD%95%E9%80%9A%E8%BF%87%E7%A8%8B%E5%BA%8F%E5%9C%A8-confluence-%E4%B8%AD%E5%88%9B%E5%BB%BA-markdown-%E6%96%87%E6%A1%A3/"/>
<id>https://ykgarfield.github.io/2018/12/23/文档工具/confluence/confluence-如何通过程序在-confluence-中创建-markdown-文档/</id>
<published>2018-12-22T16:00:00.000Z</published>
<updated>2018-12-28T07:59:27.701Z</updated>
<content type="html"><![CDATA[<p>在 confluence 中创建 markdown 文档, 首先需要安装相关的 markdown 插件.既有免费的, 也有付费的.经过试用付费的 markdown 插件, 和免费的对比一下, 发现大部分 markdown 相关的功能都基本一致.付费的提供了一些额外的特性, 比如: 允许使用 html 标签、可选择渲染表格的样式等, 另外就是提供了一个很方便的功能就是可以直接将 markdown 文件上传为附件, 并用此插件插入附件 markdown 文档.而且渲染出来的表格样式比免费的要美观很多.免费的 markdown 插件渲染出来的表格样式实在有点不忍直视, 比较丑陋.</p><p>另外, 无论是免费的还是付费的 markdown 插件都是不支持生成目录语法.大多数平台和 markdown 编辑器可以通过扩展的 <code>[toc]</code> 语法来实现生成目录.在 confluence 中需要插入目录宏, 而且还是可以定义展示的层级, 这个特性还是比较有用的.</p><p>不过为了节约成本, 我们当然还是优先选择免费的.通过人工操作, 写入 markdown 文档是很简单的, 但是怎么通过程序自动的在 confluence 中创建 markdown 文档呢? 目前也没有现成的 API 可以实现.不过通过人工操作并进行抓包, 我们发现可以利用以下几个 API 来实现此功能.这里将插入目录宏(Table of Contents)的操作一起加上.</p><ul><li>插入目录宏: 发起 <code>/rest/tinymce/1/macro/placeholder</code> 请求.</li><li>目录宏 draft: 发起 <code>/rest/tinymce/1/drafts</code> 请求.</li><li>插入 markdown 宏: 还是发起 <code>/rest/tinymce/1/macro/placeholder</code> 请求.</li><li>更新文档(draft status): 发起 <code>/rest/api/content/{id}?status=draft</code> 请求</li></ul><p>下面具体的来说明上面的几个步骤.</p><h2 id="插入目录宏"><a href="#插入目录宏" class="headerlink" title="插入目录宏"></a>插入目录宏</h2><p>发起 <code>/rest/tinymce/1/macro/placeholder</code> 请求.请求的示例如下:</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></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"contentId"</span>:<span class="string">"6488066"</span>,</span><br><span class="line"> <span class="attr">"macro"</span>:{</span><br><span class="line"> <span class="attr">"name"</span>:<span class="string">"toc"</span>,</span><br><span class="line"> <span class="attr">"params"</span>:{</span><br><span class="line"> <span class="attr">"maxLevel"</span>:<span class="string">"2"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"body"</span>:<span class="string">""</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p> 可以看到, <code>contentId</code> 也就是 page 的 ID, <code>name</code> 固定为 <code>toc</code>, <code>maxLevel</code> 指定了目录展示的最大层次.这里只会展示两级.</p><p>响应示例:</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"><img class="editor-inline-macro" src="/plugins/servlet/confluence/placeholder/macro?definition=e3RvYzptYXhMZXZlbD0yfQ&amp;locale=zh_CN&amp;version=2" data-macro-name="toc" data-macro-parameters="maxLevel=2" data-macro-schema-version="1"></span><br></pre></td></tr></table></figure><p>这里返回的信息需要保存下面, 后面还会使用到.</p><h2 id="目录宏-draft"><a href="#目录宏-draft" class="headerlink" title="目录宏 draft"></a>目录宏 draft</h2><p>发起 <code>/rest/tinymce/1/drafts</code> 请求.请求示例如下:</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">{</span><br><span class="line"> <span class="attr">"draftId"</span>:<span class="string">"6488067"</span>,</span><br><span class="line"> <span class="attr">"pageId"</span>:<span class="string">"6488066"</span>,</span><br><span class="line"> <span class="attr">"parentPageId"</span>:<span class="string">"5898241"</span>,</span><br><span class="line"> <span class="attr">"type"</span>:<span class="string">"page"</span>,</span><br><span class="line"> <span class="attr">"title"</span>:<span class="string">"markdown测试"</span>,</span><br><span class="line"> <span class="attr">"spaceKey"</span>:<span class="string">"SPC"</span>,</span><br><span class="line"> <span class="attr">"content"</span>:<span class="string">"<p><img class="</span>editor-inline-macro<span class="string">" src="</span>http://localhost:<span class="number">8090</span>/plugins/servlet/confluence/placeholder/macro?definition=e3RvYzptYXhMZXZlbD0yfQ&locale=zh_CN&version=<span class="number">2</span><span class="string">" data-macro-name="</span>toc<span class="string">" data-macro-parameters="</span>maxLevel=<span class="number">2</span><span class="string">" data-macro-schema-version="</span><span class="number">1</span><span class="string">" /></p><p><br /></p>"</span>,</span><br><span class="line"> <span class="attr">"syncRev"</span>:<span class="string">"0.izAFz9oUJgl5fEP4WzBEKKI.1"</span>,</span><br><span class="line"> <span class="attr">"pageVersion"</span>:<span class="number">1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到之前请求返回的数据在这步操作中需要使用到.</p><h2 id="插入-markdown-宏"><a href="#插入-markdown-宏" class="headerlink" title="插入 markdown 宏"></a>插入 markdown 宏</h2><p>和插入目录宏类似.只是请求参数稍有变化:</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></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"contentId"</span>:<span class="string">"6488066"</span>,</span><br><span class="line"> <span class="attr">"macro"</span>:{</span><br><span class="line"> <span class="attr">"name"</span>:<span class="string">"markdown"</span>,</span><br><span class="line"> <span class="attr">"params"</span>:{</span><br><span class="line"></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"body"</span>:<span class="string">""</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到很简单, 就是 <code>name</code> 指定为 <code>markdown</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"><table class="wysiwyg-macro" data-macro-name="markdown" data-macro-schema-version="1" style="background-image: url(/plugins/servlet/confluence/placeholder/macro-heading?definition=e21hcmtkb3dufQ&amp;locale=zh_CN&amp;version=2); background-repeat: no-repeat;" data-macro-body-type="PLAIN_TEXT"><tr><td class="wysiwyg-macro-body"><pre /></td></tr></table></span><br></pre></td></tr></table></figure><p>这里返回的内容也需要保存下来, 后面的操作需要使用到.</p><h2 id="更新文档-draft-status"><a href="#更新文档-draft-status" class="headerlink" title="更新文档(draft status)"></a>更新文档(draft status)</h2><p>这是最后一步, 发起 <code>/rest/api/content/{id}?status=draft</code> 请求.请求示例如下:</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></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"status"</span>:<span class="string">"current"</span>,</span><br><span class="line"> <span class="attr">"title"</span>:<span class="string">"markdown测试"</span>,</span><br><span class="line"> <span class="attr">"space"</span>:{</span><br><span class="line"> <span class="attr">"key"</span>:<span class="string">"SPC"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"body"</span>:{</span><br><span class="line"> <span class="attr">"editor"</span>:{</span><br><span class="line"> <span class="attr">"value"</span>:<span class="string">"<p><img class="</span>editor-inline-macro<span class="string">" src="</span>http://localhost:<span class="number">8090</span>/plugins/servlet/confluence/placeholder/macro?definition=e3RvYzptYXhMZXZlbD0yfQ&locale=zh_CN&version=<span class="number">2</span><span class="string">" data-macro-name="</span>toc<span class="string">" data-macro-parameters="</span>maxLevel=<span class="number">2</span><span class="string">" data-macro-schema-version="</span><span class="number">1</span><span class="string">" data-macro-id="</span><span class="number">50</span>c89389<span class="number">-2</span>ea5<span class="number">-43</span>cf-a582-d5b2ed3d4796<span class="string">" /></p><table class="</span>wysiwyg-macro<span class="string">" style="</span>background-image: url('http://localhost:<span class="number">8090</span>/plugins/servlet/confluence/placeholder/macro-heading?definition=e21hcmtkb3dufQ&locale=zh_CN&version=<span class="number">2</span>'); background-repeat: no-repeat;<span class="string">" data-macro-name="</span>markdown<span class="string">" data-macro-schema-version="</span><span class="number">1</span><span class="string">" data-macro-body-type="</span>PLAIN_TEXT<span class="string">"><tbody><tr><td class="</span>wysiwyg-macro-body<span class="string">"><pre># 概要<br /><br />本文档描述了...</pre></td></tr></tbody></table><p><br /></p><p><br /></p><p><br /></p>"</span>,</span><br><span class="line"> <span class="attr">"representation"</span>:<span class="string">"editor"</span>,</span><br><span class="line"> <span class="attr">"content"</span>:{</span><br><span class="line"> <span class="attr">"id"</span>:<span class="string">"6488066"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"id"</span>:<span class="string">"6488066"</span>,</span><br><span class="line"> <span class="attr">"type"</span>:<span class="string">"page"</span>,</span><br><span class="line"> <span class="attr">"version"</span>:{</span><br><span class="line"> <span class="attr">"number"</span>:<span class="number">5</span>,</span><br><span class="line"> <span class="attr">"message"</span>:<span class="string">""</span>,</span><br><span class="line"> <span class="attr">"minorEdit"</span>:<span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"syncRev"</span>:<span class="string">"0.yM8srPzow93w6esMsDjIuXE.2"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="attr">"ancestors"</span>:[</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">"id"</span>:<span class="string">"5898241"</span>,</span><br><span class="line"> <span class="attr">"type"</span>:<span class="string">"page"</span></span><br><span class="line"> }</span><br><span class="line"> ]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以发现请求中的 <code>editor</code> 中的数据, 是把第一步和第三步返回的数据拼接在了一起, 并且在第三步返回的数据中插入了我们实际要写入的 mardown 的内容.</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"><table class="..." style="..." data-macro-body-type="..."><tr><td class="...">在这里插入实际的 markdown 内容<pre /></td></tr></table></span><br></pre></td></tr></table></figure><p>通过以上的 4 个步骤我们就可以完成通过程序自动化地在 confluence 中创建 markdown 文档.</p>]]></content>
<summary type="html">
<p>在 confluence 中创建 markdown 文档, 首先需要安装相关的 markdown 插件.既有免费的, 也有付费的.经过试用付费的 markdown 插件, 和免费的对比一下, 发现大部分 markdown 相关的功能都基本一致.付费的提供了一些额外的特性,
</summary>
<category term="文档工具" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/tags/confluence/"/>
<category term="confluence markdown" scheme="https://ykgarfield.github.io/tags/confluence-markdown/"/>
</entry>
<entry>
<title>confluence-记一次 confluence markdown 表格样式不美观发生的事</title>
<link href="https://ykgarfield.github.io/2018/12/23/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/confluence-%E8%AE%B0%E4%B8%80%E6%AC%A1-confluence-markdown-%E8%A1%A8%E6%A0%BC%E6%A0%B7%E5%BC%8F%E4%B8%8D%E7%BE%8E%E8%A7%82%E5%8F%91%E7%94%9F%E7%9A%84%E4%BA%8B/"/>
<id>https://ykgarfield.github.io/2018/12/23/文档工具/confluence/confluence-记一次-confluence-markdown-表格样式不美观发生的事/</id>
<published>2018-12-22T16:00:00.000Z</published>
<updated>2019-01-01T14:55:22.553Z</updated>
<content type="html"><![CDATA[<p>最近在用 confluence markdown 插件(免费)的时候, 发现生成的表格样式实在不太美观, 甚至有点丑陋:</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/记一次_confluence_markdown_表格样式丑陋发生的事/markdown-table-style-ugly.jpg" alt="markdown-table-style-ugly"></p><p>再看看 confluence 原生表格的样式做个对比:</p><p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images/记一次_confluence_markdown_表格样式丑陋发生的事/markdown-native-table-style-diff.jpg" alt="markdown-native-table-style-diff"></p><p>不知道之前用 markdown 插件写作的人是怎么能忍受这种表格样式的.</p><p>于是乎就去评论了一下, 没想到的是很快就得到了回复.详情可见: <a href="https://marketplace.atlassian.com/apps/1211438/markdown-macro-for-confluence?hosting=server&tab=reviews" target="_blank" rel="noopener">https://marketplace.atlassian.com/apps/1211438/markdown-macro-for-confluence?hosting=server&tab=reviews</a></p><p>接下来就继续与作者进行沟通, 不过并没有抱太大期望.意外不到的是, 作者提供了一个新版本的插件 jar 包给我, 让我试试看, 有些意外和惊喜.</p><p>后面的沟通都是通过 <a href="https://atlasauthority.atlassian.net/servicedesk/customer/portal/1/SUPPORT-111" target="_blank" rel="noopener">https://atlasauthority.atlassian.net/servicedesk/customer/portal/1/SUPPORT-111</a></p><p>因为一开始并不太熟悉怎么进行升级, 所以一开始升级的时候出了问题.在 <code>SUPPORT-111</code> 也有详细描述, 后来重新思考, 通过 <code>Atlassian Marketplace -> 管理插件 -> 上传插件</code> 的方式进行更新.更新完成之后, 表格的样式终于有了变化, 使用了 confluence 本地的样式.</p><p>此插件也于 2018.12.24 发布了新版本.</p><p>回顾整件事, 其实也是一个无心之举, 只是想去吐槽下.没想到作者也比较热心, 很快就对这个问题进行了修改.再此, 还是很感谢作者把免费插件做的这么好.</p><p>所以再使用 confluence 插件的时候, 如果觉得有做的不好的地方, 其实也是可以和作者进行沟通的, 说不定提出的建议就被采纳了.^_^</p>]]></content>
<summary type="html">
<p>最近在用 confluence markdown 插件(免费)的时候, 发现生成的表格样式实在不太美观, 甚至有点丑陋:</p>
<p><img src="https://www.github.com/ykgarfield/my-blog/raw/master/images
</summary>
<category term="文档工具" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/categories/%E6%96%87%E6%A1%A3%E5%B7%A5%E5%85%B7/confluence/"/>
<category term="confluence" scheme="https://ykgarfield.github.io/tags/confluence/"/>
<category term="confluence markdown" scheme="https://ykgarfield.github.io/tags/confluence-markdown/"/>
</entry>
<entry>
<title>虚拟网卡导致 apollo 启动太慢的问题</title>
<link href="https://ykgarfield.github.io/2018/06/26/Java/%E6%80%A7%E8%83%BD/%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E5%AF%BC%E8%87%B4%20apollo%20%E5%90%AF%E5%8A%A8%E5%A4%AA%E6%85%A2%E7%9A%84%E9%97%AE%E9%A2%98/"/>
<id>https://ykgarfield.github.io/2018/06/26/Java/性能/虚拟网卡导致 apollo 启动太慢的问题/</id>
<published>2018-06-25T16:00:00.000Z</published>
<updated>2020-01-03T02:21:20.146Z</updated>
<content type="html"><![CDATA[<p>项目中集成了 apollo 之后, 在本地启动项目速度很慢, 比没有集成 apollo 的时候慢了 30s~40s.</p><p>这里做了一个简单的例子, 主要是一个 SpringBoot 的启动类和一个 apollo 的配置类, 配置类很简单:</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><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">@Configuration</span><br><span class="line">@EnableApolloConfig</span><br><span class="line">public class HttpClientConfig {</span><br><span class="line"> @Value("${http.url}")</span><br><span class="line"> private String url;</span><br><span class="line"></span><br><span class="line"> @Value("${http.connectionTimeout}")</span><br><span class="line"> private String connectionTimeout;</span><br><span class="line"></span><br><span class="line"> @Value("${http.readTimeout}")</span><br><span class="line"> private String readTimeout;</span><br><span class="line"></span><br><span class="line"> @Value("${http.username}")</span><br><span class="line"> private String username;</span><br><span class="line"></span><br><span class="line"> @Value("${http.password}")</span><br><span class="line"> private String password;</span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>启动项目:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/Java/%E6%80%A7%E8%83%BD/%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E5%AF%BC%E8%87%B4%20apollo%20%E5%90%AF%E5%8A%A8%E5%A4%AA%E6%85%A2%E7%9A%84%E9%97%AE%E9%A2%98/1.png" alt="1"></p><p>可以看到一个很简单的只是集成了 apollo 的项目启动就花费了 39s 左右.到底是什么导致的呢? </p><p>可以使用 <code>jvisualvm</code> 进行观察:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/Java/%E6%80%A7%E8%83%BD/%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E5%AF%BC%E8%87%B4%20apollo%20%E5%90%AF%E5%8A%A8%E5%A4%AA%E6%85%A2%E7%9A%84%E9%97%AE%E9%A2%98/2.png" alt="2"></p><p>可以看到大部分的时间都消耗在了 <code>NetworkInterfaceManager#findValidateIp()</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><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">public InetAddress findValidateIp(List<InetAddress> addresses) {</span><br><span class="line"> InetAddress local = null;</span><br><span class="line"> int maxWeight = -1;</span><br><span class="line"> for (InetAddress address : addresses) {</span><br><span class="line"> if (address instanceof Inet4Address) {</span><br><span class="line"> // has host name</span><br><span class="line"> // TODO fix performance issue when calling getHostName</span><br><span class="line"> if (!Objects.equals(address.getHostName(), address.getHostAddress())) {</span><br><span class="line"> weight += 1;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> return local;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>此方法要做的事情就是寻找有效的 IP, 另外可以看到此方法的 <code>if (!Objects.equals(address.getHostName(), address.getHostAddress()))</code> 片段也注释了 <code>TODO fix performance issue when calling getHostName</code>.</p><p>不过 <code>jvisualvm</code> 并没有明确的指示出是 <code>InetAddress#getHostName()</code> 方法耗时而导致 <code>findValidateIp()</code> 方法的耗时.换成 <code>jProfiler</code> 再重新观察一下:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/Java/%E6%80%A7%E8%83%BD/%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E5%AF%BC%E8%87%B4%20apollo%20%E5%90%AF%E5%8A%A8%E5%A4%AA%E6%85%A2%E7%9A%84%E9%97%AE%E9%A2%98/3.png" alt="3"></p><p>此时就可以很明确的看出是 <code>InetAddress#getHostName()</code> 耗时而导致的问题.</p><p>关于 <code>InetAddress#getHostName()</code> 执行慢的问题可搜索下相关的话题.先看看有什么办法可以先 解决这个问题.</p><p>到这里我们可以想到的是有什么办法可以规避此方法的调用, 看下此方法的调用处, 也就是 <code>NetworkInterfaceManager#load()</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><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">private void load() {</span><br><span class="line"> // 从环境变量或系统属性获取 ip</span><br><span class="line"> String ip = getProperty("host.ip");</span><br><span class="line"></span><br><span class="line"> if (ip != null) {</span><br><span class="line"> try {</span><br><span class="line"> m_local = InetAddress.getByName(ip);</span><br><span class="line"> return;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> try {</span><br><span class="line">// 调用 findValidateIp()</span><br><span class="line">local = findValidateIp(addresses);</span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>从这里可以看到看出如果 <code>getProperty("host.ip")</code> 可以取到 ip 值, 那么 <code>findValidateIp()</code> 方法就不会被调用, 这样项目启动是不是就可以很快了? 在项目启动类加上 vm args: <code>-Dhost.ip=IP地址</code>, 重新启动项目:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/Java/%E6%80%A7%E8%83%BD/%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E5%AF%BC%E8%87%B4%20apollo%20%E5%90%AF%E5%8A%A8%E5%A4%AA%E6%85%A2%E7%9A%84%E9%97%AE%E9%A2%98/4.png" alt="4"></p><p>此时可以看到项目启动非常快.</p><p>现在再思考下, 虽然 <code>InetAddress#getHostName()</code> 执行耗时, 但是也不至于那么慢, 如果真的这样, 那么岂不是只要用了这个方法的项目启动都会很慢.</p><p>再回头看看 <code>NetworkInterfaceManager#load()</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><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></pre></td><td class="code"><pre><span class="line">try {</span><br><span class="line"> // 获取所有的网卡</span><br><span class="line">Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();</span><br><span class="line">List<NetworkInterface> nis = interfaces == null ? Collections.<NetworkInterface>emptyList() : Collections.list(interfaces);</span><br><span class="line">List<InetAddress> addresses = new ArrayList<InetAddress>();</span><br><span class="line">InetAddress local = null;</span><br><span class="line"></span><br><span class="line">try {</span><br><span class="line">for (NetworkInterface ni : nis) {</span><br><span class="line"> if (ni.isUp() && !ni.isLoopback()) {</span><br><span class="line">addresses.addAll(Collections.list(ni.getInetAddresses()));</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">// 传入地址列表</span><br><span class="line">local = findValidateIp(addresses);</span><br><span class="line">} catch (Exception e) {</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>findValidateIp()</code> 方法接收的是一个地址列表, 在本机调试, 在使用有线网的情况下 <code>addresses</code> 参数的个数是 10(无线网是 12).为什么会这么多, 因为本机安装了虚拟机, 多出了几块虚拟网卡, 查看网络连接:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/Java/%E6%80%A7%E8%83%BD/%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E5%AF%BC%E8%87%B4%20apollo%20%E5%90%AF%E5%8A%A8%E5%A4%AA%E6%85%A2%E7%9A%84%E9%97%AE%E9%A2%98/5.png" alt="5"></p><p>此时可以想到是不是因为是虚拟网卡而导致的问题? 另外 <code>NetworkInterfaceManager#load()</code> 方法中有 <code>ni.isUp()</code> 的判断, 此方法会判断网卡是否启用并运行.将除了以太网和 WLAN 之外的网络都禁用, 此时再重新运行项目, 发现项目启动速度就很快了:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/Java/%E6%80%A7%E8%83%BD/%E8%99%9A%E6%8B%9F%E7%BD%91%E5%8D%A1%E5%AF%BC%E8%87%B4%20apollo%20%E5%90%AF%E5%8A%A8%E5%A4%AA%E6%85%A2%E7%9A%84%E9%97%AE%E9%A2%98/6.png" alt="6"></p><p>从这个例子中可以看到, 如果项目集成 apollo 之后在本地测试启动速度很慢, 那么可以看下本地是否安装了虚拟机, 或者可以通过传入 vm args <code>-Dhost.ip=IP地址</code> 的方式来解决项目启动慢的问题.</p>]]></content>
<summary type="html">
<p>项目中集成了 apollo 之后, 在本地启动项目速度很慢, 比没有集成 apollo 的时候慢了 30s~40s.</p>
<p>这里做了一个简单的例子, 主要是一个 SpringBoot 的启动类和一个 apollo 的配置类, 配置类很简单:</p>
<figure
</summary>
<category term="Java" scheme="https://ykgarfield.github.io/categories/Java/"/>
<category term="性能" scheme="https://ykgarfield.github.io/categories/Java/%E6%80%A7%E8%83%BD/"/>
<category term="性能" scheme="https://ykgarfield.github.io/tags/%E6%80%A7%E8%83%BD/"/>
<category term="apollo" scheme="https://ykgarfield.github.io/tags/apollo/"/>
</entry>
<entry>
<title>Yaml 配置集合元素过多导致的 SpringBoot 启动过慢</title>
<link href="https://ykgarfield.github.io/2018/06/04/Java/%E6%80%A7%E8%83%BD/Yaml%20%E9%85%8D%E7%BD%AE%E9%9B%86%E5%90%88%E5%85%83%E7%B4%A0%E8%BF%87%E5%A4%9A%E5%AF%BC%E8%87%B4%E7%9A%84%20SpringBoot%20%E5%90%AF%E5%8A%A8%E8%BF%87%E6%85%A2/"/>
<id>https://ykgarfield.github.io/2018/06/04/Java/性能/Yaml 配置集合元素过多导致的 SpringBoot 启动过慢/</id>
<published>2018-06-03T16:00:00.000Z</published>
<updated>2020-01-03T02:21:13.977Z</updated>
<content type="html"><![CDATA[<p>在需求开发的过程中, 有次启动项目, 发现启动太慢.下面通过简化, 创建一个简单的 demo 程序来观察此现象.</p><p>主程序很简单:</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">@SpringBootApplication</span><br><span class="line">public class Application {</span><br><span class="line"> private static final Logger LOG = LoggerFactory.getLogger(Application.class);</span><br><span class="line"></span><br><span class="line"> public static void main(String[] args) {</span><br><span class="line"> SpringApplication.run(Application.class, args);</span><br><span class="line"></span><br><span class="line"> LOG.info("App start");</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>两个配置文件, 一个配置了列表, 一个配置了 map.</p><p>列表文件如下所示:</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></pre></td><td class="code"><pre><span class="line"># 配置列表, 大概有 1万 1 千多个元素.</span><br><span class="line">zip:</span><br><span class="line"> - 641004</span><br><span class="line"> - 641006</span><br><span class="line"> - 400601</span><br><span class="line"> ...</span><br></pre></td></tr></table></figure><p>map 配置如下所示:</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></pre></td><td class="code"><pre><span class="line"># 配置 map, 大概有 4万 2 千多个元素</span><br><span class="line">codeMap:</span><br><span class="line"> 00501: 1</span><br><span class="line"> 00544: 1</span><br><span class="line"> 00601: 7</span><br><span class="line"> 00602: 7</span><br><span class="line"> ...</span><br></pre></td></tr></table></figure><p>两个对应的配置类如下.</p><p>列表配置:</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></pre></td><td class="code"><pre><span class="line">@ConfigurationProperties(locations = "classpath:zip.yaml")</span><br><span class="line">@Component</span><br><span class="line">public class Zip {</span><br><span class="line"> private List<String> zip = new ArrayList<>(11790);</span><br><span class="line"></span><br><span class="line"> public List<String> getZip() {</span><br><span class="line"> return zip;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> public void setZip(List<String> zip) {</span><br><span class="line"> this.zip = zip;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>map 类配置:</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></pre></td><td class="code"><pre><span class="line">@ConfigurationProperties(locations = "classpath:codemap.yml")</span><br><span class="line">@Component</span><br><span class="line">public class CodeMap {</span><br><span class="line"> private Map<String, Integer> codemap = Maps.newHashMapWithExpectedSize(42358);</span><br><span class="line"></span><br><span class="line"> public Map<String, Integer> getCodemap() {</span><br><span class="line"> return codemap;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> public void setCodemap(Map<String, Integer> codemap) {</span><br><span class="line"> this.codemap = codemap;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>启动应用, 可以看到启动用了 56 秒, 这个时间是比较长的.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/Java/%E6%80%A7%E8%83%BD/Yaml%20%E9%85%8D%E7%BD%AE%E9%9B%86%E5%90%88%E5%85%83%E7%B4%A0%E8%BF%87%E5%A4%9A%E5%AF%BC%E8%87%B4%E7%9A%84%20SpringBoot%20%E5%90%AF%E5%8A%A8%E8%BF%87%E6%85%A2/1.png" alt="1"></p><p>其实这里很容易猜测出来可能是由于配置的问题导致的, 毕竟应用非常简单, 也就两个两个配置而.但是在实际项目开发中, 功能很多, 不一定这么容易就能够猜测是因为这个原因导致的, 下面通过 <code>jvisualvm</code> 来观察是什么原因导致的.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/Java/%E6%80%A7%E8%83%BD/Yaml%20%E9%85%8D%E7%BD%AE%E9%9B%86%E5%90%88%E5%85%83%E7%B4%A0%E8%BF%87%E5%A4%9A%E5%AF%BC%E8%87%B4%E7%9A%84%20SpringBoot%20%E5%90%AF%E5%8A%A8%E8%BF%87%E6%85%A2/2.png" alt="2"></p><p>从上图中隐约的看出可能是元素排序导致的问题, 不是很清晰.那么再换 <code>JProfiler</code> 试试看, 如下图:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/Java/%E6%80%A7%E8%83%BD/Yaml%20%E9%85%8D%E7%BD%AE%E9%9B%86%E5%90%88%E5%85%83%E7%B4%A0%E8%BF%87%E5%A4%9A%E5%AF%BC%E8%87%B4%E7%9A%84%20SpringBoot%20%E5%90%AF%E5%8A%A8%E8%BF%87%E6%85%A2/3.png" alt="3"></p><p>此时就毕竟清晰了, 可以看到解析 yaml 比较耗时.不过需要保留下怀疑态度, 真的是解析 yaml 过慢导致的问题吗? 这里先保留这个问题, 先看看如何解决这个问题.</p><p>解决方法也很简单, 大不了就自己解析.</p><p>不使用 yaml 文件, 就使用普通的文本, 每个元素就是一行.</p><p>配置类可改成如下所以, 这里以配置类表为例, 配置 map 与此类似:</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><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">@PostConstruct</span><br><span class="line">public void init() {</span><br><span class="line"> LOG.info("read zip2.txt");</span><br><span class="line"></span><br><span class="line"> Resource resource = new ClassPathResource("zip2.txt");</span><br><span class="line"> // 注意: 使用流的方式读取</span><br><span class="line"> try (BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream(), "UTF-8"), 8192)) {</span><br><span class="line"> String line;</span><br><span class="line"> while ((line = br.readLine()) != null) {</span><br><span class="line"> zip.add(line);</span><br><span class="line"> }</span><br><span class="line"> } catch (IOException e) {</span><br><span class="line"> LOG.error("read error: ", e);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>此时启动启用, 可以看到, 启动速度很快:</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/Java/%E6%80%A7%E8%83%BD/Yaml%20%E9%85%8D%E7%BD%AE%E9%9B%86%E5%90%88%E5%85%83%E7%B4%A0%E8%BF%87%E5%A4%9A%E5%AF%BC%E8%87%B4%E7%9A%84%20SpringBoot%20%E5%90%AF%E5%8A%A8%E8%BF%87%E6%85%A2/4.png" alt="4"></p><p>从这个示例中可以看到, 对于 yaml 中配置列表和 map 元素, 如果元素过多的话, 此时就不适合使用 Spring Boot 中的自动配置了, 会导致应用启动过慢.那么这个时候还不如自己去解析, 可以节省很多时间.</p><p>现在再回到上面的问题, 真的是如 <code>jprfiler</code> 分析的那样是因为解析 yaml 解析配置文件过慢吗? 可以写个测试类:</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></pre></td><td class="code"><pre><span class="line">// codemap.yml、zip.yaml</span><br><span class="line">Resource resource = new ClassPathResource("codemap.yml");</span><br><span class="line">Yaml yaml = new Yaml();</span><br><span class="line">Iterable<Object> all = yaml.loadAll(resource.getInputStream());</span><br><span class="line">for (Object obj: all) {</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>运行上面的代码, 可以看到是很快的.</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">time: 79ms -- codemap.yml 解析时间</span><br><span class="line">time: 1ms -- zip.yaml 解析时间</span><br></pre></td></tr></table></figure><p>这里可以看出 <code>jprofiler</code> 给出的结果不准确.此时再重新审视下 <code>jvisualvm</code> 给出的结果, <code>RelaxedDataBinder#sortPropertyNames()</code> 方法耗时导致的项目启动速度慢, 在此方法上打上断点, 然后以 debug 模式启动项目, 可以看到当解析完 <code>codemap.yml</code> 文件之后, 运行到这个方法, 会导致 debug 几乎暂停, 运行了很长时间.</p><p><img src="https://raw.githubusercontent.com/ykgarfield/my-blog/master/images/source/_posts/Java/%E6%80%A7%E8%83%BD/Yaml%20%E9%85%8D%E7%BD%AE%E9%9B%86%E5%90%88%E5%85%83%E7%B4%A0%E8%BF%87%E5%A4%9A%E5%AF%BC%E8%87%B4%E7%9A%84%20SpringBoot%20%E5%90%AF%E5%8A%A8%E8%BF%87%E6%85%A2/5.png" alt="5"></p><p>具体看下 <code>RelaxedDataBinder#sortPropertyNames()</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><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">private void sortPropertyNames(List<String> names) {</span><br><span class="line">for (String name : new ArrayList<String>(names)) {</span><br><span class="line"> // LinkedList#indexOf()</span><br><span class="line">int propertyIndex = names.indexOf(name);</span><br><span class="line">BeanPath path = new BeanPath(name);</span><br><span class="line">for (String prefix : path.prefixes()) {</span><br><span class="line"> // LinkedList#indexOf()</span><br><span class="line">int prefixIndex = names.indexOf(prefix);</span><br><span class="line">if (prefixIndex >= propertyIndex) {</span><br><span class="line">// The child property has a parent in the list in the wrong order</span><br><span class="line">names.remove(name);</span><br><span class="line">names.add(prefixIndex, name);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>BeanPath</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><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">BeanPath(String path) {</span><br><span class="line">this.nodes = splitPath(path);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">private List<PathNode> splitPath(String path) {</span><br><span class="line">List<PathNode> nodes = new ArrayList<PathNode>();</span><br><span class="line">String current = extractIndexedPaths(path, nodes);</span><br><span class="line">for (String name : StringUtils.delimitedListToStringArray(current, ".")) {</span><br><span class="line">if (StringUtils.hasText(name)) {</span><br><span class="line">nodes.add(new PropertyNode(name));</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">return nodes;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">private String extractIndexedPaths(String path, List<PathNode> nodes) {</span><br><span class="line">int startRef = path.indexOf("[");</span><br><span class="line">String current = path;</span><br><span class="line">while (startRef >= 0) {</span><br><span class="line">if (startRef > 0) {</span><br><span class="line">nodes.addAll(splitPath(current.substring(0, startRef)));</span><br><span class="line">}</span><br><span class="line">int endRef = current.indexOf("]", startRef);</span><br><span class="line">if (endRef > 0) {</span><br><span class="line">String sub = current.substring(startRef + 1, endRef);</span><br><span class="line">if (sub.matches("[0-9]+")) {</span><br><span class="line">nodes.add(new ArrayIndexNode(sub));</span><br><span class="line">}</span><br><span class="line">else {</span><br><span class="line">nodes.add(new MapIndexNode(sub));</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">current = current.substring(endRef + 1);</span><br><span class="line">startRef = current.indexOf("[");</span><br><span class="line">}</span><br><span class="line">return current;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>从上面的代码中可以看到, 这里对 <code>List</code>(类型是 <code>LinkedList</code>) 元素循环进行 <code>indexOf()</code>、<code>remove()</code> 和 <code>add(index, element)</code> 操作, 另外就是在循环内实例化 <code>BeanPath</code> 对象, 查看 <code>BeanPath</code> 的构造函数, 所做的操作存在大量的字符串操作.不过 <code>remove()</code> 和 <code>add(index, element)</code> 的执行是有条件的.通过 debug 发现, 这两个方法并没有被执行到, 那么也就剩下 <code>indexOf()</code> 方法(实际执行的是 <code>LinkedList#indexOf()</code> 方法)和 <code>BeanPath</code> 的构造函数会被执行到, 而 <code>LinkedList</code> 是基于链表的, 查找某个元素的下标对于 <code>LinkedList</code> 来说是比较慢的, 再加上 <code>BeanPath</code> 构造函数内所做的字符串的查找、截取等操作(元素较少的情况下可能还觉察不出来, 元素一旦比较多就会导致程序执行耗费过多的时间), 这些操作加在一起导致了 <code>RelaxedDataBinder#sortPropertyNames()</code> 方法执行比较耗时.</p><p>到这里也就搞清楚了问题的原因, 也知道了该如何解决这个问题, 在配置元素过多的情况下, 如果项目启动过慢, 那么需要自己去解析配置文件而不是让 SpringBoot 帮我们解析.</p>]]></content>
<summary type="html">
<p>在需求开发的过程中, 有次启动项目, 发现启动太慢.下面通过简化, 创建一个简单的 demo 程序来观察此现象.</p>
<p>主程序很简单:</p>
<figure class="highlight plain"><table><tr><td class="gutter"
</summary>
<category term="Java" scheme="https://ykgarfield.github.io/categories/Java/"/>
<category term="性能" scheme="https://ykgarfield.github.io/categories/Java/%E6%80%A7%E8%83%BD/"/>
<category term="性能" scheme="https://ykgarfield.github.io/tags/%E6%80%A7%E8%83%BD/"/>
<category term="SpringBoot" scheme="https://ykgarfield.github.io/tags/SpringBoot/"/>
<category term="Yaml" scheme="https://ykgarfield.github.io/tags/Yaml/"/>
</entry>
</feed>