-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
493 lines (290 loc) · 302 KB
/
atom.xml
File metadata and controls
493 lines (290 loc) · 302 KB
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>代码星冰乐</title>
<subtitle>专注成就未来</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="https://www.hchstudio.cn/"/>
<updated>2021-07-21T11:42:18.906Z</updated>
<id>https://www.hchstudio.cn/</id>
<author>
<name>ChanghuiN</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>Kafka的日志复制机制</title>
<link href="https://www.hchstudio.cn/article/2021/7cba/"/>
<id>https://www.hchstudio.cn/article/2021/7cba/</id>
<published>2021-01-14T13:25:43.000Z</published>
<updated>2021-07-21T11:42:18.906Z</updated>
<content type="html"><![CDATA[<p>Kafka 是一个分布式的发布-订阅消息系统。它最初是在 LinkedIn 开发的,2011年7月成为一个 Apache 项目。今天,Kafka 被 LinkedIn、 Twitter 和 Square 用于日志聚合、队列、实时监控和事件处理等应用程序。在下面的文章中,我们将讨论下 Kafka 的 replication 设计。<br><a id="more"></a></p><p>replication 的目的是为了提供服务的高可用, 即使有些节点出现了失败,Producer可以继续发布消息,Consumer可以继续接收消息。</p><h2 id="保证数据一致性的方式"><a href="#保证数据一致性的方式" class="headerlink" title="保证数据一致性的方式"></a>保证数据一致性的方式</h2><p>有两种典型的方式来保证数据的强一致。这两种方式都要求指定一个leader,所有的写都是发送给leader。leader负责接收所有的写请求,并以相同的顺序将这些写传播给其他follower。</p><h3 id="多数复制"><a href="#多数复制" class="headerlink" title="多数复制"></a>多数复制</h3><p>基于多数提交的方式。leader 要等到大多数 fellower 接收到数据之后才认为数据是可提交的状态。在 leader 失败的情况下,通过多数 fellower 的协调选出新的 leader 。这种方式的算法有raft、paxos等算法, 比如Zookeeper、 Google Spanner、etcd等。这种方式在有 2n + 1个节点的情况下,最多可以容忍n个节点失败。</p><h3 id="主从复制"><a href="#主从复制" class="headerlink" title="主从复制"></a>主从复制</h3><p>基于主从复制的方式。需要等 leader 和 fellower 都写入成功才算消息接收成功, 在有n个节点的情况下,最多可以容忍n-1节点失败。</p><p>Kafka使用的是主从复制的方式来实现集群之间的日志复制。原因如下:</p><p>基于主从复制的方式可以在相同数量的副本中容忍更多故障。 也就是说,它可以容忍带有 n + 1个副本的 n 个故障,而基于多数复制的方式通常只能容忍带有2n +1个副本的n个故障。 例如,如果只有2个副本,则基于多数复制的方式不能容忍任何故障。<br>Kafka的日志复制主要考虑的是同一个数据中心机器之间的数据复制,相对来说延迟并不会成为日志复制的瓶颈。</p><h2 id="几个概念"><a href="#几个概念" class="headerlink" title="几个概念"></a>几个概念</h2><p>在 Kafka 中,消息流是由 topic 定义的,topic被划分为一个或多个partition。而复制发生在 partition 级别,每个 partition 都有有一个或多个副本。</p><p><img src="https://img.hchstudio.cn/topic_loginc_0114.png" alt="topic的逻辑关系"></p><p><img src="https://img.hchstudio.cn/topic%20%E5%AD%98%E5%82%A8%E7%9A%84%E7%89%A9%E7%90%86%E5%85%B3%E7%B3%BB.png" alt="topic的屋里存储关系"></p><p>在 Kafka 集群中,将副本均匀地分配到不同的服broker上。每个副本都在磁盘上维护一个日志。发布的消息按顺序附加到日志中,每条消息都通过日志中的单调递增offset来标识。<br>offset 是分区中的逻辑概念。给定一个offset,可以在每个分区副本中标识相同的消息。当 consumer 订阅某个主题时,它会跟踪每个分区中用于消费的偏移量,并使用它向 broker 发出读取请求。<br><img src="https://img.hchstudio.cn/kafka_replication_diagram.png" alt><br>如上图所示当 producer 将消息发布到topic的某个 partition 时,该消息首先被转发到该 partition 的leader副本,并追加到其日志中。fellower 的副本不断地从 leader 那里获取新的信息。一旦有足够多的副本接收到消息,leader 就提交消息。<br>有个问题就是说 leader 如何决定到什么程度是足够的。leader 不能总是等待所有副本的写操作完成。这样为了保证数据一致性而降低我们服务的可用性是不可行的,这是因为任何跟随者副本可以失败和领导者不能无限地等待。</p><h2 id="Kafka的ISR模型"><a href="#Kafka的ISR模型" class="headerlink" title="Kafka的ISR模型"></a>Kafka的ISR模型</h2><p>为了解决上面提出的问题,Kafka采用了一种折中的方案,引入了 ISR的概念。ISR是in-sync replicas的简写。ISR的副本保持和leader的同步,当然leader本身也在ISR中。初始状态所有的副本都处于ISR中,当一个消息发送给leader的时候,leader会等待ISR中所有的副本告诉它已经接收了这个消息,如果一个副本失败了,那么它会被移除ISR。下一条消息来的时候,leader就会将消息发送给当前的ISR中节点了。</p><p>同时,leader还维护这HW(high watermark),这是一个分区的最后一条消息的offset。HW会持续的将HW发送给fellower,broker可以将它写入到磁盘中以便将来恢复。</p><p>当一个失败的副本重启的时候,它首先恢复磁盘中记录的HW,然后将它的消息同步到HW这个offset。这是因为HW之后的消息不保证已经commit。这时它变成了一个fellower, 从HW开始从Leader中同步数据,一旦追上leader,它就可以再加入到ISR中。</p><p>kafka使用Zookeeper实现leader选举。如果leader失败,controller会从ISR选出一个新的leader。leader 选举的时候可能会有数据丢失,但是committed的消息保证不会丢失。</p><p>故障恢复,leader重新选举的表述~</p><h2 id="数据一致性与服务可用性的权衡"><a href="#数据一致性与服务可用性的权衡" class="headerlink" title="数据一致性与服务可用性的权衡"></a>数据一致性与服务可用性的权衡</h2><p>为了保证数据的一致性,Kafka提出了ISR,在同步日志到 fellower 的时候为了提高服务的可用性,fellow在将leader同步的日志写入内存后就返回给leader日志写入成功的标志。然后这些操作都是可以通过Kafka的配置来实现的。</p><h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><ul><li><a href="https://kafka.apache.org/documentation/#replication" target="_blank" rel="noopener">https://kafka.apache.org/documentation/#replication</a></li><li><a href="https://colobu.com/2017/11/02/kafka-replication/" target="_blank" rel="noopener">https://colobu.com/2017/11/02/kafka-replication/</a></li><li><a href="https://engineering.linkedin.com/kafka/intra-cluster-replication-apache-kafka" target="_blank" rel="noopener">https://engineering.linkedin.com/kafka/intra-cluster-replication-apache-kafka</a></li><li><a href="https://cwiki.apache.org/confluence/display/KAFKA/kafka+Detailed+Replication+Design+V3" target="_blank" rel="noopener">https://cwiki.apache.org/confluence/display/KAFKA/kafka+Detailed+Replication+Design+V3</a></li></ul>]]></content>
<summary type="html">
<p>Kafka 是一个分布式的发布-订阅消息系统。它最初是在 LinkedIn 开发的,2011年7月成为一个 Apache 项目。今天,Kafka 被 LinkedIn、 Twitter 和 Square 用于日志聚合、队列、实时监控和事件处理等应用程序。在下面的文章中,我们将讨论下 Kafka 的 replication 设计。<br>
</summary>
<category term="Java" scheme="https://www.hchstudio.cn/categories/Java/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Java" scheme="https://www.hchstudio.cn/tags/Java/"/>
<category term="Kafka" scheme="https://www.hchstudio.cn/tags/Kafka/"/>
</entry>
<entry>
<title>从20到21</title>
<link href="https://www.hchstudio.cn/article/2021/8e2f/"/>
<id>https://www.hchstudio.cn/article/2021/8e2f/</id>
<published>2021-01-02T13:25:43.000Z</published>
<updated>2021-07-21T11:42:18.903Z</updated>
<content type="html"><![CDATA[<p>这一年过得不容易,记个流水账吧~<br><a id="more"></a></p><h2 id="一场突如其来的瘟疫"><a href="#一场突如其来的瘟疫" class="headerlink" title="一场突如其来的瘟疫"></a>一场突如其来的瘟疫</h2><p>一场突入起来的瘟疫,开启了2020年,刚开始的时候口罩都买不起了快,一时是京城口罩贵~</p><p>因为疫情宅家的小半年,倒是学着自己做了不少道菜。</p><h2 id="两个月考下来的驾照"><a href="#两个月考下来的驾照" class="headerlink" title="两个月考下来的驾照"></a>两个月考下来的驾照</h2><p>之后北京疫情缓解了的时候,就被对象催着报了考驾照的班,我这个人做什么事的时候比较拖,多亏了她~</p><p>学驾照的时候,科目二一共八个课时,硬是换了八个教练,奈何我天赋异禀,科目二科目三都是一把过。</p><h2 id="两次远行,见识了不一样的风景"><a href="#两次远行,见识了不一样的风景" class="headerlink" title="两次远行,见识了不一样的风景"></a>两次远行,见识了不一样的风景</h2><p>一次成都,一次上海。</p><p>人生第一次坐飞机,在飞机爬坡的时候心跳真的超级快,感觉要跳出来一样。</p><h2 id="两次莫名起来的车祸"><a href="#两次莫名起来的车祸" class="headerlink" title="两次莫名起来的车祸"></a>两次莫名起来的车祸</h2><p>第一场车祸,十一的时候仗着自己刚学的驾照,租了一辆车出去浪,一个不小心在变道的时候把别人的车给刮了,万幸的是租车的时候买了保险,除了交警的罚单,倒是没有什么其他损失。在这里学到好多,就是做事不能太过于循规蹈矩。再一个就是与人交流,不要太自以为是以及轻易的轻信于他人。</p><p>第二场车祸,这次是打的滴滴,我是乘客,万幸的时候人没出事,我当时直接下车就离开了,不知道滴滴师傅后来怎么样了。</p><h2 id="数十趟北京天津之间的往来"><a href="#数十趟北京天津之间的往来" class="headerlink" title="数十趟北京天津之间的往来"></a>数十趟北京天津之间的往来</h2><p>年初的时候就开始打算天津落户的事情,在落户的事差不多稳了的时候,就开始看天津的房子了,鉴于最近两年还会在北京工作就没有看市区的二手房,主要看天津环城四区的新房,应该说主要是主要看北辰跟西青的新房。</p><p>这期间还有个插曲就是之前过于着急没看清楚房子的信息,就稀里糊涂交了定金,后来又后悔退定金的问题,细节就不多说了。总的来说天津的房地产是监管还是比较严格的,让我对这个城市有了一定的好感~</p><p>这期间北京天津之间的往返没少跑,不过离得是真的近~</p><h2 id="一套上百万的房子"><a href="#一套上百万的房子" class="headerlink" title="一套上百万的房子"></a>一套上百万的房子</h2><p>在看了大半年的房子之后,在综合多方面考虑之后,定下了在天津这座城市的第一套房,也是我人生中的第一套房,一套上百万的房子,从此沦为银行的佃农。</p><p>交首付的时候,当五十多万的存款从我银行卡上一笔划走的时候,我的心里竟然没有一丝丝的波澜,镇定的像这钱从来都不属于我一样。</p><p>在这里要感谢借钱给我,助我凑够首付的每一个人,谢谢你们~</p><h2 id="2020年其他的东西"><a href="#2020年其他的东西" class="headerlink" title="2020年其他的东西"></a>2020年其他的东西</h2><p>从业三年多,2020年从一月到十二月是我第一个完整的一年一直呆在一个公司的工作,为自己的坚持鼓掌。</p><p>关于理财,2020年跟着A股的大牛市,稍稍赚了一点,没有大富大贵的命,小赚不亏就好。</p><h2 id="总结一下吧"><a href="#总结一下吧" class="headerlink" title="总结一下吧"></a>总结一下吧</h2><p>这一年是我有限人生中过得及其快速的一年,眨眼之间,真的是眨眼之间~</p><h2 id="2021年会发生什么"><a href="#2021年会发生什么" class="headerlink" title="2021年会发生什么"></a>2021年会发生什么</h2><p>2021年会发生什么,我也不知道,至于2021年自己能做成什么事,先挖个坑,到明年的这个时候再来补吧……</p><p>不过,最明显的就是2021年突然意识到自己开始奔三了要,岁月真的是把杀猪刀~</p>]]></content>
<summary type="html">
<p>这一年过得不容易,记个流水账吧~<br>
</summary>
<category term="总结" scheme="https://www.hchstudio.cn/categories/%E6%80%BB%E7%BB%93/"/>
<category term="总结" scheme="https://www.hchstudio.cn/tags/%E6%80%BB%E7%BB%93/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
</entry>
<entry>
<title>go 并发编程</title>
<link href="https://www.hchstudio.cn/article/2020/e8fc/"/>
<id>https://www.hchstudio.cn/article/2020/e8fc/</id>
<published>2020-07-01T13:15:51.000Z</published>
<updated>2021-07-21T11:42:18.905Z</updated>
<content type="html"><![CDATA[<h2 id="基本的同步原语"><a href="#基本的同步原语" class="headerlink" title="基本的同步原语"></a>基本的同步原语</h2><p>Go 语言在 sync 包中提供了用于同步的一些基本原语,包括常见的 sync.Mutex、sync.RWMutex、sync.WaitGroup、sync.Once。</p><h3 id="Mutex"><a href="#Mutex" class="headerlink" title="Mutex"></a>Mutex</h3><p><strong>数据结构</strong></p><p>Go 语言的 sync.Mutex 由两个字段 state 和 sema 组成。其中 state 表示当前互斥锁的状态,而 sema 是用于控制锁状态的信号量。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Mutex <span class="keyword">struct</span> { </span><br><span class="line"> state <span class="keyword">int32</span> </span><br><span class="line"> sema <span class="keyword">uint32</span> </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上述两个加起来只占 8 字节空间的结构体表示了 Go 语言中的互斥锁。</p><p><strong>实现原理</strong></p><p>互斥锁的状态比较复杂,如下图所示,最低三位分别表示 mutexLocked、mutexWoken 和 mutexStarving,剩下的位置用来表示当前有多少个 Goroutine 等待互斥锁的释放:如下图</p><p><img src="http://img.hchstudio.cn/go-lock-state.png" alt></p><p>在默认情况下,互斥锁的所有状态位都是 0,int32 中的不同位分别表示了不同的状态:</p><ul><li>mutexLocked — 表示互斥锁的锁定状态;</li><li>mutexWoken — 表示从正常模式被从唤醒;</li><li>mutexStarving — 当前的互斥锁进入饥饿状态;</li><li>waitersCount — 当前互斥锁上等待的 Goroutine 个数;</li></ul><p><strong>正常模式和饥饿模式</strong><br>sync.Mutex 有两种模式 — 正常模式和饥饿模式。我们需要在这里先了解正常模式和饥饿模式都是什么,它们有什么样的关系。</p><p>在正常模式下,锁的等待者会按照先进先出的顺序获取锁。但是刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁,为了减少这种情况的出现,一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式,防止部分 Goroutine 被『饿死』。</p><p>饥饿模式是在 Go 语言 1.9 版本引入的优化,引入的目的是保证互斥锁的公平性。</p><p>在饥饿模式中,互斥锁会直接交给等待队列最前面的 Goroutine。新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待。如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会被切换回正常模式。</p><p>相比于饥饿模式,正常模式下的互斥锁能够提供更好地性能,饥饿模式的能避免 Goroutine 由于陷入等待无法获取锁而造成的高尾延时。</p><p><strong>锁的使用</strong><br><figure class="highlight go"><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">var</span> lock = sync.Mutex{}</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">test_lock</span><span class="params">()</span></span> {</span><br><span class="line">lock.Lock()</span><br><span class="line"> <span class="keyword">defer</span> lock.Unlock()</span><br><span class="line"><span class="comment">// do something </span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p><strong>注意事项</strong></p><p>1,Unlock 未加锁或者已解锁的 Mutex 会 panic</p><p>2,Mutex 不会比较当前请求的 goroutine 是否已经持有这个锁,所以可以一个 goorutine Lock ,另一个 goroutine Unlock, 但是慎用,避免死锁</p><p>3,Mutex 是非重入锁。 如果想重入,使用扩展的同步原语。</p><p>注:这里解释下重入锁与非重入锁</p><p>重入锁:顾名思义,就是指当前线程在获取锁成功后可以反复进入的锁。</p><p>不可重入锁:就是指在获取锁成功后需要释放当前锁之后才能再次获取锁。</p><p>RWMutex</p><p>读写互斥锁 sync.RWMutex 是细粒度的互斥锁,它不限制资源的并发读,但是读写、写写操作无法并行执行。适合写少读多的状态,对并发的读很适合。</p><table><thead><tr><th></th><th>读</th><th>写</th></tr></thead><tbody><tr><td>读</td><td>Y</td><td>N</td></tr><tr><td>写</td><td>N</td><td>N</td></tr></tbody></table><p>一般常见的服务对资源的读多写少的场景,因为大多数的读请求之间不会相互影响,所以我们可以对读写资源操作的进行分离,提高服务的性能。</p><p>数据结构<br><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> RWMutex <span class="keyword">struct</span> {</span><br><span class="line">w Mutex</span><br><span class="line">writerSem <span class="keyword">uint32</span></span><br><span class="line">readerSem <span class="keyword">uint32</span></span><br><span class="line">readerCount <span class="keyword">int32</span></span><br><span class="line">readerWait <span class="keyword">int32</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>在上面的代码中的五个字段的含义分别是:</p><ul><li>w — 复用互斥锁提供的能力;</li><li>writerSem 和 readerSem — 分别用于写等待读和读等待写:</li><li>readerCount 存储了当前正在执行的读操作的数量;</li><li>readerWait 表示当写操作被阻塞时等待的读操作个数; </li></ul><h3 id="写锁"><a href="#写锁" class="headerlink" title="写锁"></a>写锁</h3><p><strong>获取写锁</strong><br><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rw *RWMutex)</span> <span class="title">Lock</span><span class="params">()</span></span> {</span><br><span class="line">rw.w.Lock()</span><br><span class="line"> <span class="comment">// 阻塞后续的读操作</span></span><br><span class="line">r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders</span><br><span class="line"><span class="keyword">if</span> r != <span class="number">0</span> && atomic.AddInt32(&rw.readerWait, r) != <span class="number">0</span> {</span><br><span class="line">runtime_SemacquireMutex(&rw.writerSem, <span class="literal">false</span>, <span class="number">0</span>)</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p><strong>释放写锁</strong><br><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rw *RWMutex)</span> <span class="title">Unlock</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="comment">// 调用 atomic.AddInt32 函数将变回正数,释放读锁;</span></span><br><span class="line">r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)</span><br><span class="line"><span class="keyword">if</span> r >= rwmutexMaxReaders {</span><br><span class="line">throw(<span class="string">"sync: Unlock of unlocked RWMutex"</span>)</span><br><span class="line">}</span><br><span class="line"> <span class="comment">// 通过 for 循环触发所有由于获取读锁而陷入等待的 Goroutine:</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="keyword">int</span>(r); i++ {</span><br><span class="line">runtime_Semrelease(&rw.readerSem, <span class="literal">false</span>, <span class="number">0</span>)</span><br><span class="line">}</span><br><span class="line">rw.w.Unlock()</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h3 id="读锁"><a href="#读锁" class="headerlink" title="读锁"></a>读锁</h3><p><strong>获取读锁</strong><br><figure class="highlight go"><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="function"><span class="keyword">func</span> <span class="params">(rw *RWMutex)</span> <span class="title">RLock</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="comment">// 如果该方法返回负数 — 其他 Goroutine 获得了写锁,当前 Goroutine 就会调用 sync.runtime_SemacquireMutex 陷入休眠等待锁的释放;否则则成功获取读锁</span></span><br><span class="line"><span class="keyword">if</span> atomic.AddInt32(&rw.readerCount, <span class="number">1</span>) < <span class="number">0</span> {</span><br><span class="line">runtime_SemacquireMutex(&rw.readerSem, <span class="literal">false</span>, <span class="number">0</span>)</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p><strong>释放读锁</strong><br><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rw *RWMutex)</span> <span class="title">RUnlock</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="comment">// 如果返回值大于等于零 — 读锁直接解锁成功;</span></span><br><span class="line"> <span class="comment">// 如果返回值小于零 — 有一个正在执行的写操作,在这时会调用sync.RWMutex.rUnlockSlow 方法;</span></span><br><span class="line"><span class="keyword">if</span> r := atomic.AddInt32(&rw.readerCount, <span class="number">-1</span>); r < <span class="number">0</span> {</span><br><span class="line">rw.rUnlockSlow(r)</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rw *RWMutex)</span> <span class="title">rUnlockSlow</span><span class="params">(r <span class="keyword">int32</span>)</span></span> {</span><br><span class="line"><span class="keyword">if</span> r+<span class="number">1</span> == <span class="number">0</span> || r+<span class="number">1</span> == -rwmutexMaxReaders {</span><br><span class="line">throw(<span class="string">"sync: RUnlock of unlocked RWMutex"</span>)</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> atomic.AddInt32(&rw.readerWait, <span class="number">-1</span>) == <span class="number">0</span> {</span><br><span class="line">runtime_Semrelease(&rw.writerSem, <span class="literal">false</span>, <span class="number">1</span>)</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h3 id="WaitGroup"><a href="#WaitGroup" class="headerlink" title="WaitGroup"></a>WaitGroup</h3><p>如下图所示,WaitGroup可以将原本顺序执行的代码在多个 Goroutine 中并发执行,加快程序处理的速度。</p><p><img src="http://img.hchstudio.cn/wait-group.png" alt></p><p>sync.WaitGroup 必须在 sync.WaitGroup.Wait 方法返回之后才能被重新使用;<br>sync.WaitGroup.Done 只是对 sync.WaitGroup.Add 方法的简单封装,我们可以向 sync.WaitGroup.Add 方法传入任意负数(需要保证计数器非负)快速将计数器归零以唤醒其他等待的 Goroutine;<br>可以同时有多个 Goroutine 等待当前 sync.WaitGroup 计数器的归零,这些 Goroutine 会被同时唤醒;</p><p>waitGroup的例子:<br><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="string">"fmt"</span></span><br><span class="line"> <span class="string">"sync"</span></span><br><span class="line"> <span class="string">"time"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">worker</span><span class="params">(id <span class="keyword">int</span>, wg *sync.WaitGroup)</span></span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">defer</span> wg.Done()</span><br><span class="line"></span><br><span class="line"> fmt.Printf(<span class="string">"Worker %d starting\n"</span>, id)</span><br><span class="line"></span><br><span class="line"> time.Sleep(time.Second)</span><br><span class="line"> fmt.Printf(<span class="string">"Worker %d done\n"</span>, id)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> i := <span class="number">1</span>; i <= <span class="number">5</span>; i++ {</span><br><span class="line"> wg.Add(<span class="number">1</span>)</span><br><span class="line"> <span class="keyword">go</span> worker(i, &wg)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> wg.Wait()</span><br><span class="line">}</span><br><span class="line"><span class="string">``</span><span class="string">` </span></span><br><span class="line"><span class="string">### sync.Once</span></span><br><span class="line"><span class="string">Go 语言标准库中 sync.Once 可以保证在 Go 程序运行期间的某段代码只会执行一次。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">结构体</span></span><br><span class="line"><span class="string">`</span><span class="string">``</span> <span class="keyword">go</span></span><br><span class="line"><span class="keyword">type</span> Once <span class="keyword">struct</span> {</span><br><span class="line">done <span class="keyword">uint32</span></span><br><span class="line">m Mutex</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>sync.Once.Do 是 sync.Once 结构体对外唯一暴露的方法,该方法会接收一个入参为空的函数:</p><p>如果传入的函数已经执行过,就会直接返回;<br>如果传入的函数没有执行过,就会调用 sync.Once.doSlow 执行传入的函数:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">func (o *Once) Do(f func()) {</span><br><span class="line">if atomic.LoadUint32(&o.done) == 0 {</span><br><span class="line">o.doSlow(f)</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">func (o *Once) doSlow(f func()) {</span><br><span class="line">o.m.Lock()</span><br><span class="line">defer o.m.Unlock()</span><br><span class="line">if o.done == 0 {</span><br><span class="line">defer atomic.StoreUint32(&o.done, 1)</span><br><span class="line">f()</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>sync.Once 执行逻辑:</p><ul><li>为当前 Goroutine 获取互斥锁;</li><li>执行传入的无入参函数;</li><li>运行延迟函数调用,将成员变量 done 更新成 1;</li><li>通过成员变量 done 确保函数不会执行第二次。</li></ul><p>关于 sync.Once使用的例子:<br><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">"fmt"</span></span><br><span class="line"><span class="string">"sync"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line">o := &sync.Once{}</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">10</span>; i++ {</span><br><span class="line">o.Do(<span class="function"><span class="keyword">func</span><span class="params">()</span></span> {</span><br><span class="line">fmt.Println(<span class="string">"only once"</span>)</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><h2 id="Channel"><a href="#Channel" class="headerlink" title="Channel"></a>Channel</h2><p>Go 语言中最常见的、也是经常被人提及的设计模式就是:不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存。</p><p>在很多主流的编程语言中,多个线程传递数据的方式一般都是共享内存的方式来实现的,为了解决线程冲突的问题,我们需要限制同一时间能够读写这些变量的线程数量,这与 Go 语言鼓励的方式并不相同。</p><p>通过共享内存的方式实现多线程之间的数据传递:</p><p><img src="http://img.hchstudio.cn/thread-memory.png" alt></p><p>go中使用channel实现goroutine之间的数据共享:<br><img src="http://img.hchstudio.cn/go-channel.png" alt></p><h3 id="Channel-类型"><a href="#Channel-类型" class="headerlink" title="Channel 类型"></a>Channel 类型</h3><ul><li>无缓冲区的channel</li><li>有缓冲区的channel</li></ul><p>下图是示意图:<br><img src="http://img.hchstudio.cn/channel-type.png" alt></p><p><strong>非缓冲通道特性:</strong></p><ul><li>向此类通道发送元素值的操作会被阻塞,直到至少有一个针对该通道的接收操作开始进行为止。</li><li>从此类通道接收元素值的操作会被阻塞,直到至少有一个针对该通道的发送操作开始进行为止。</li><li>针对非缓冲通道的接收操作会在与之相应的发送操作完成之前完成。<br>总结来说就是:如果接受者没有准备好,则发送者会阻塞,反之亦然。</li></ul><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">var</span> c = <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">int</span>)</span><br><span class="line"> <span class="keyword">var</span> a <span class="keyword">string</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> {</span><br><span class="line"> a = <span class="string">"hello world"</span></span><br><span class="line"> <-c</span><br><span class="line"> }()</span><br><span class="line"></span><br><span class="line"> c <- <span class="number">0</span></span><br><span class="line"> fmt.Println(a)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="select-多路复用"><a href="#select-多路复用" class="headerlink" title="select 多路复用"></a>select 多路复用</h3><p>select 语句提供了一种处理多通道的方法。跟 switch 语句很像,但是每个分支都是一个通道:</p><ul><li>所有通道都会被监听</li><li>select 会阻塞直到某个通道读取到内容</li><li>如果多个通道都可以处理,则会以伪随机的方式处理</li><li>如果有默认分支,并且没有通道就绪,则会立即执行</li></ul><p>结合 goroutine、channel、select 的一个简单示例,将6个数字1~6发送到一个容量为3的管道中,两个 goroutine 每秒接受一次数字后打印信息:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="string">"fmt"</span></span><br><span class="line"> <span class="string">"sync"</span></span><br><span class="line"> <span class="string">"time"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"></span><br><span class="line"> ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">int</span>, <span class="number">3</span>)</span><br><span class="line"> <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line"> <span class="comment">// start 2 goroutines</span></span><br><span class="line"> <span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">2</span>; i++ {</span><br><span class="line"> wg.Add(<span class="number">1</span>)</span><br><span class="line"> <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(id <span class="keyword">int</span>)</span></span> {</span><br><span class="line"> tick := time.Tick(<span class="number">1</span> * time.Second)</span><br><span class="line"> <span class="keyword">for</span> {</span><br><span class="line"> <span class="keyword">select</span> {</span><br><span class="line"> <span class="keyword">case</span> <-tick:</span><br><span class="line"> {</span><br><span class="line"> i, ok := <-ch</span><br><span class="line"> <span class="keyword">if</span> !ok {</span><br><span class="line"> wg.Done()</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"> fmt.Println(<span class="string">"goroutine"</span>, id, <span class="string">"recv"</span>, i)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }(i)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// sender</span></span><br><span class="line"> <span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">6</span>; i++ {</span><br><span class="line"> ch <- i</span><br><span class="line"> fmt.Println(<span class="string">"send"</span>, i)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="built_in">close</span>(ch)</span><br><span class="line"> wg.Wait()</span><br><span class="line"> fmt.Println(<span class="string">"main goroutine end"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><p><a href="https://colobu.com/2018/12/18/dive-into-sync-mutex/" target="_blank" rel="noopener">https://colobu.com/2018/12/18/dive-into-sync-mutex/</a></p><p><a href="https://iswade.github.io/articles/go_concurrency/" target="_blank" rel="noopener">https://iswade.github.io/articles/go_concurrency/</a></p><p><a href="https://segmentfault.com/a/1190000016466500" target="_blank" rel="noopener">https://segmentfault.com/a/1190000016466500</a></p><p><a href="https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-channel/" target="_blank" rel="noopener">https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-channel/</a></p>]]></content>
<summary type="html">
在公司的一次简单的小分享~
</summary>
<category term="go" scheme="https://www.hchstudio.cn/categories/go/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="go" scheme="https://www.hchstudio.cn/tags/go/"/>
</entry>
<entry>
<title>【译】了解Linux CPU负载-您何时应该担心?</title>
<link href="https://www.hchstudio.cn/article/2020/ce5a/"/>
<id>https://www.hchstudio.cn/article/2020/ce5a/</id>
<published>2020-06-23T16:00:00.000Z</published>
<updated>2021-07-21T11:42:18.906Z</updated>
<content type="html"><![CDATA[<p>您可能已经熟悉Linux平均负载。 平均负载是 <code>uptime</code> 和 <code>top</code> 命令显示的三个数字-它们看起来像这样:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">load average: 0.09, 0.05, 0.01</span><br></pre></td></tr></table></figure><p>大多数人都对负载平均值的含义有所了解:三个数字代表了较长时间段内的平均值(一分钟,五分钟和十五分钟的平均值),而较低的数字更好。 较高的数字表示问题或机器过载。 但是,门槛是多少? 什么构成“好”和“坏”负载平均值? 什么时候应该关注负载平均值,什么时候应该地修复它?</p><p>首先,简要了解负载平均值的含义。 我们将从最简单的情况开始:一台带有一个单核处理器的机器。<br><img src="http://img.hchstudio.cn/server-load-overage.png" alt="cpu-load"></p><h2 id="The-traffic-analogy"><a href="#The-traffic-analogy" class="headerlink" title="The traffic analogy"></a>The traffic analogy</h2><p>单核CPU就像一条流量通道。 想象您是一名桥梁操作员…有时您的桥梁太忙了,有汽车排成一行。 您想让人们知道桥上的交通如何。 一个体面的指标是在特定时间等待多少辆汽车。 如果没有汽车在等,传入的驾驶员知道他们可以马上驶过。 如果对汽车进行了备份,则驾驶员知道他们要耽误时间。</p><p>那么,网桥运营商,您将使用哪种编号系统? 怎么样:</p><ul><li><p><strong>0.00表示桥上根本没有流量</strong>。 实际上,介于0.00和1.00之间表示没有备用轮胎,到达的汽车将继续行驶。</p></li><li><p><strong>1.00表示桥刚好处于满负荷状态。</strong> 一切都还不错,但是如果流量增加一点,事情就会变慢。</p></li><li><p><strong>超过1.00表示有备份。</strong> 多少? 那么,2.00意味着总共有两个车道的汽车价值-桥上一个车道的价值,一个车道的等待价值。 3.00表示总共有3条车道-桥上1条车道值得等待。 等等。</p></li></ul><p><img src="http://img.hchstudio.cn/understanding-load-averages.png" alt></p><p>这基本上是CPU负载。 “汽车”是指使用CPU时间(“过桥”)或排队使用CPU的进程。 Unix将其称为运行队列长度:当前正在运行的进程数与正在等待(排队)的进程数之和。</p><p>就像桥梁操作员一样,您希望您的汽车/过程永远不会等待。 因此,理想情况下,您的CPU负载应保持在1.00以下。 就像桥接运算符一样,如果您暂时获得高于1.00的峰值,您仍然可以…但是当您始终高于1.00时,您就需要担心。</p><h2 id="So-you’re-saying-the-ideal-load-is-1-00"><a href="#So-you’re-saying-the-ideal-load-is-1-00" class="headerlink" title="So you’re saying the ideal load is 1.00?"></a>So you’re saying the ideal load is 1.00?</h2><p>好吧,不完全是。 负载为1.00的问题是您没有净空。 实际上,许多系统管理员会在0.70处画一条线:</p><ul><li><p>“需要研究”的经验法则:0.70如果平均负载保持在> 0.70以上,那么应该在情况变得更糟之前进行调查。</p></li><li><p>“立即解决”的经验法则是:1.00。 如果平均负载保持在1.00以上,请查找问题并立即解决。 否则,您将在半夜醒来,这将不会很有趣。</p></li><li><p>“ Arrgh,这是WTF 3AM?” 经验法则:5.0。 如果平均负载高于5.00,则可能会遇到严重的麻烦,盒子要么挂着要么减速,这将(莫名其妙地)发生在最坏的时间,例如深夜或当您在场时 在会议上。 不要让它到达那里。</p></li></ul><h2 id="What-about-Multi-processors-My-load-says-3-00-but-things-are-running-fine"><a href="#What-about-Multi-processors-My-load-says-3-00-but-things-are-running-fine" class="headerlink" title="What about Multi-processors? My load says 3.00, but things are running fine!"></a>What about Multi-processors? My load says 3.00, but things are running fine!</h2><p>有一个四处理器系统? 3.00负载仍然很健康。</p><p>在多处理器系统上,负载是相对于可用处理器核心数量的。 在单核系统上,“ 100%利用率”标记是1.00,在双核上是2.00,在四核上是4.00,依此类推。</p><p>如果再回到桥梁类比,“ 1.00”实际上意味着“一个车道的通行价值”。 在单车道的桥上,这意味着它已被填满。 在两层桥梁上,负载为1.00表示其容量为50%时-只有一个车道已满,因此还有另一个完整车道可以填充。</p><p>与CPU相同:1.00的负载是单核机箱上的CPU利用率为100%。 在双核计算机上,负载为2.00就是100%CPU使用率。</p><h2 id="Multicore-vs-multiprocessor"><a href="#Multicore-vs-multiprocessor" class="headerlink" title="Multicore vs. multiprocessor"></a>Multicore vs. multiprocessor</h2><p>当我们讨论主题时,让我们谈谈多核与多处理器。 出于性能目的,具有单个双核处理器的计算机是否基本上等同于具有两个具有一个内核的处理器的计算机? 是。 大致。 关于缓存的数量,处理器之间的进程切换频率等,这里有很多微妙之处。尽管有这些细微之处,但为了确定CPU负载值,内核总数是重要的,无论如何 这些内核分布在许多物理处理器上。</p><p>这引出了两个新的经验法则:</p><p>-“核数=最大负载”经验法则:在多核系统上,您的负载不应超过可用核数。</p><p>-“核心就是核心”经验法则:核心在CPU上的分布方式无关紧要。 两个四核==四个双核==八个单核。 这些都是八个核心。</p><h2 id="Bringing-It-Home"><a href="#Bringing-It-Home" class="headerlink" title="Bringing It Home"></a>Bringing It Home</h2><p>让我们看一下 uptime 的平均负载输出:<br><figure class="highlight shell"><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">~ $ uptime</span><br><span class="line">23:05 up 14 days, 6:08, 7 users, load averages: 0.65 0.42 0.36</span><br></pre></td></tr></table></figure></p><p>这是在双核CPU上,因此我们有很大的余量。在负载达到并保持在1.7左右之前,我什至不会考虑它。</p><p>现在,那三个数字呢? 0.65是最后一分钟的平均值,0.42是最近五分钟的平均值,而0.36是最近15分钟的平均值。这使我们想到了一个问题:</p><p>我应该观察哪个平均值? 1、5或15分钟?</p><p>对于我们已经讨论过的数字(1.00 =立即修复,依此类推),您应该查看5或15分钟的平均值。坦白说,如果您的广告活动平均在一分钟内达到1.0以上的峰值,您还是可以的。这是15分钟平均值超过1.0并保持在该水平上的时间。 (显然,据我们了解,将这些数字调整为系统具有的处理器核心数量)。</p><p>因此,核的数量对于解释平均负载很重要……我如何知道我的系统有多少个核?</p><p>cat / proc / cpuinfo可获取系统中每个处理器的信息。注意:在OSX上不可用,但Google可以选择。要获得一个计数,请通过grep和单词计数运行它:grep’模型名称’/ proc / cpuinfo | wc -l</p><h2 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h2><p><a href="https://scoutapm.com/blog/understanding-load-averages" target="_blank" rel="noopener">原文链接</a></p><p><a href="https://en.wikipedia.org/wiki/Load_%28computing%29" target="_blank" rel="noopener">Wikipedia - A good, brief explanation of Load Average</a></p><p><a href="https://www.linuxjournal.com/article/9001" target="_blank" rel="noopener">Linux Journal - very well-written article</a></p>]]></content>
<summary type="html">
看到一篇关于 load overage 的文章,觉得很有趣,先机翻下来~
</summary>
<category term="Linux" scheme="https://www.hchstudio.cn/categories/Linux/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Java" scheme="https://www.hchstudio.cn/tags/Java/"/>
</entry>
<entry>
<title>Zookeeper 与分布式锁</title>
<link href="https://www.hchstudio.cn/article/2020/4d59/"/>
<id>https://www.hchstudio.cn/article/2020/4d59/</id>
<published>2020-06-22T13:15:51.000Z</published>
<updated>2021-07-21T11:42:18.906Z</updated>
<content type="html"><![CDATA[<p>在<a href="http://www.hchstudio.cn/article/2020/7bc5/">上篇文章中</a>讨论了基于 Redis 的单机分布式锁与集群分布式锁的方案,在数据一致性要求不是很高的情况下,Redis 实现的分布式锁可以满足我们的要求。最近在拜读了 zookeeper 的论文之后,对于 zookeeper 实现的分布式锁,也是有必要了解一下的。<br><a id="more"></a></p><h2 id="ZooKeeper-是什么?"><a href="#ZooKeeper-是什么?" class="headerlink" title="ZooKeeper 是什么?"></a>ZooKeeper 是什么?</h2><p>ZooKeeper 的论文是这样描述的:</p><blockquote><p>ZooKeeper 是一种用于协调的服务分布式应用程序。 由于 ZooKeeper 是关键基础结构的一部分,因此 ZooKeeper 旨在提供一个简单而高性能的内核,以在客户端构建更复杂的协调原语。 它在复制的集中式服务中合并了来自组消息传递,共享寄存器和分布式锁定服务的元素。 ZooKeeper 公开的接口具有共享寄存器的免等待方面,它具有事件驱动机制,类似于分布式文件系统的缓存失效,以提供简单而强大的协调服务。</p></blockquote><h2 id="ZooKeeper-实现的简单的分布式锁"><a href="#ZooKeeper-实现的简单的分布式锁" class="headerlink" title="ZooKeeper 实现的简单的分布式锁"></a>ZooKeeper 实现的简单的分布式锁</h2><p>使用 Zookeeper 实现分布式锁的最简单的方案是在 Zookeeper 中创建一个临时状态(EPHEMERAL)的锁节点,为了获取锁,客户端尝试使用 EPHEMERAL 标志创建指定的 znode(如 lock )。如果创建成功,则客户端将持有该锁。否则,客户端可以读取设置了监视标志的 znode,以便在当前领导者去世时得到通知。客户端死亡或显式删除 znode 时会释放该锁。 其他等待锁定的客户端一旦观察到 znode 被删除,就会再次尝试获取锁定。</p><h2 id="对-ZooKeeper-分布式锁的优化"><a href="#对-ZooKeeper-分布式锁的优化" class="headerlink" title="对 ZooKeeper 分布式锁的优化"></a>对 ZooKeeper 分布式锁的优化</h2><p>对于上面的方案, 细心的同学可能会发现,如果同时有 n 个客户端去获取锁资源,就会只能有一个客户端获取锁成功,那么就会有 n-1 个客户端设置对锁节点的监听,如果在 n 比较大的情况下会有客户端获取锁的时间被延长,甚至是无限延长,在某些情况下是不可接受的。</p><p>对于上面的这种极端情况,Zookeeper 官方给出了比较好的解决方案,就是将竞争的客户端进行排队,具体实现的伪代码如下所示:<br><figure class="highlight shell"><figcaption><span>script</span></figcaption><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">Lock</span><br><span class="line">1 n = create(l + “/lock-”, EPHEMERAL|SEQUENTIAL)</span><br><span class="line">2 C = getChildren(l, false)</span><br><span class="line">3 if n is lowest znode in C, exit</span><br><span class="line">4 p = znode in C ordered just before n</span><br><span class="line">5 if exists(p, true) wait for watch event</span><br><span class="line">6 goto 2</span><br><span class="line">Unlock</span><br><span class="line">1 delete(n)</span><br></pre></td></tr></table></figure></p><p>在上面的代码中,Lock的第1行中使用SEQUENTIAL标志,可相对于所有其他尝试命令客户尝试获取锁定。 如果客户端的znode在第3行的序列号最低,则客户端将持有该锁。 否则,客户端将等待删除具有锁定或将在该客户端的znode之前收到锁定的znode。 通过仅查看客户端znode之前的znode,我们仅在释放锁或放弃锁请求时才唤醒一个进程,从而避免了羊群效应。 客户端监视的znode消失后,客户端必须检查它现在是否持有该锁。 (先前的锁定请求可能已被放弃,并且具有较低序号的znode仍在等待或保持锁定。)</p><p>总而言之,这个锁的实现方案具有以下优点:</p><ul><li>1.删除一个znode只会导致一个客户端唤醒,因为每个znode都被另一个客户端监视,因此我们没有羊群效应。</li><li>2.没有轮询或超时。</li><li>3.由于我们实现了锁定的方式,因此通过浏览ZooKeeper数据可以看到锁定争用,中断锁定和调试锁定问题的数量。</li></ul><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Lock attempts to acquire the lock. It will wait to return until the lock</span></span><br><span class="line"><span class="comment">// is acquired or an error occurs. If this instance already has the lock</span></span><br><span class="line"><span class="comment">// then ErrDeadlock is returned.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(l *Lock)</span> <span class="title">Lock</span><span class="params">()</span> <span class="title">error</span></span> {</span><br><span class="line"><span class="keyword">if</span> l.lockPath != <span class="string">""</span> {</span><br><span class="line"><span class="keyword">return</span> ErrDeadlock</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">prefix := fmt.Sprintf(<span class="string">"%s/lock-"</span>, l.path)</span><br><span class="line"></span><br><span class="line">path := <span class="string">""</span></span><br><span class="line"><span class="keyword">var</span> err error</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">3</span>; i++ {</span><br><span class="line">path, err = l.c.CreateProtectedEphemeralSequential(prefix, []<span class="keyword">byte</span>{}, l.acl)</span><br><span class="line"><span class="keyword">if</span> err == ErrNoNode {</span><br><span class="line"><span class="comment">// Create parent node.</span></span><br><span class="line">parts := strings.Split(l.path, <span class="string">"/"</span>)</span><br><span class="line">pth := <span class="string">""</span></span><br><span class="line"><span class="keyword">for</span> _, p := <span class="keyword">range</span> parts[<span class="number">1</span>:] {</span><br><span class="line"><span class="keyword">var</span> exists <span class="keyword">bool</span></span><br><span class="line">pth += <span class="string">"/"</span> + p</span><br><span class="line">exists, _, err = l.c.Exists(pth)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> exists == <span class="literal">true</span> {</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">}</span><br><span class="line">_, err = l.c.Create(pth, []<span class="keyword">byte</span>{}, <span class="number">0</span>, l.acl)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> && err != ErrNodeExists {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">} <span class="keyword">else</span> <span class="keyword">if</span> err == <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">seq, err := parseSeq(path)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> {</span><br><span class="line">children, _, err := l.c.Children(l.path)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">lowestSeq := seq</span><br><span class="line">prevSeq := <span class="number">-1</span></span><br><span class="line">prevSeqPath := <span class="string">""</span></span><br><span class="line"><span class="keyword">for</span> _, p := <span class="keyword">range</span> children {</span><br><span class="line">s, err := parseSeq(p)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> s < lowestSeq {</span><br><span class="line">lowestSeq = s</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> s < seq && s > prevSeq {</span><br><span class="line">prevSeq = s</span><br><span class="line">prevSeqPath = p</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> seq == lowestSeq {</span><br><span class="line"><span class="comment">// Acquired the lock</span></span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Wait on the node next in line for the lock</span></span><br><span class="line">_, _, ch, err := l.c.GetW(l.path + <span class="string">"/"</span> + prevSeqPath)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> && err != ErrNoNode {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">} <span class="keyword">else</span> <span class="keyword">if</span> err != <span class="literal">nil</span> && err == ErrNoNode {</span><br><span class="line"><span class="comment">// try again</span></span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">ev := <-ch</span><br><span class="line"><span class="keyword">if</span> ev.Err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> ev.Err</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">l.seq = seq</span><br><span class="line">l.lockPath = path</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>就这样吧,先水一篇文章~</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://fpj.me/2016/02/10/note-on-fencing-and-distributed-locks/" target="_blank" rel="noopener">Note on fencing and distributed locks</a><br><a href="https://blog.didiyun.com/index.php/2018/11/20/zookeeper/" target="_blank" rel="noopener">基于Zookeeper的分布式锁原理及实现</a></p>]]></content>
<summary type="html">
<p>在<a href="http://www.hchstudio.cn/article/2020/7bc5/">上篇文章中</a>讨论了基于 Redis 的单机分布式锁与集群分布式锁的方案,在数据一致性要求不是很高的情况下,Redis 实现的分布式锁可以满足我们的要求。最近在拜读了 zookeeper 的论文之后,对于 zookeeper 实现的分布式锁,也是有必要了解一下的。<br>
</summary>
<category term="Go" scheme="https://www.hchstudio.cn/categories/Go/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Go" scheme="https://www.hchstudio.cn/tags/Go/"/>
</entry>
<entry>
<title>基于Redis的分布式锁到底安全吗?</title>
<link href="https://www.hchstudio.cn/article/2020/7bc5/"/>
<id>https://www.hchstudio.cn/article/2020/7bc5/</id>
<published>2020-02-10T00:35:52.000Z</published>
<updated>2021-07-21T11:42:18.907Z</updated>
<content type="html"><![CDATA[<h2 id="单机-Redis-实现的分布式锁"><a href="#单机-Redis-实现的分布式锁" class="headerlink" title="单机 Redis 实现的分布式锁"></a>单机 Redis 实现的分布式锁</h2><p>1,单机实现分布式锁的脚本(官方推荐实现)<br><figure class="highlight bash"><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">SET lock_key random_value NX PX 10000</span><br><span class="line">// <span class="keyword">do</span> sth</span><br><span class="line"><span class="built_in">eval</span> <span class="string">"if redis.call("</span>get<span class="string">",KEYS[1]) == ARGV[1] then</span></span><br><span class="line"><span class="string"> return redis.call("</span>del<span class="string">",KEYS[1])</span></span><br><span class="line"><span class="string"> else</span></span><br><span class="line"><span class="string"> return 0</span></span><br><span class="line"><span class="string"> end"</span></span><br></pre></td></tr></table></figure></p><p>2,注意事项(对释放锁的控制,以及锁超时的控制)random_value 要保证唯一,可以用 trace_id 来保证!<br>3,存在的问题,单机Redis只是依赖单台 Redis ,当依赖的 Redis 挂掉之后会造成比较大的问题!<br>4,那么部署 Redis 的主从可以保证吗?主要原因是 Redis 主节点与从节点之间的数据同步是异步的。 </p><h2 id="分布式-Redis-实现的分布式锁"><a href="#分布式-Redis-实现的分布式锁" class="headerlink" title="分布式 Redis 实现的分布式锁"></a>分布式 Redis 实现的分布式锁</h2><h3 id="Redlock-算法"><a href="#Redlock-算法" class="headerlink" title="Redlock 算法"></a>Redlock 算法</h3><p>Redlock 算法是基于 N 个完全独立的 Redis 节点(通常情况下 N 可以设置成 5)。<br>1,获取当前时间(毫秒数)。</p><p>2,按顺序依次向 N 个 Redis 节点执行获取锁的操作。这个获取操作跟基于单 Redis 节点的获取锁的过程相同。为了保证在某个 Redis 节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个 Redis 节点获取锁失败(<strong>比如该Redis节点不可用,或者该 Redis 节点上的锁已经被其它客户端持有</strong>)以后,应该立即尝试下一个 Redis 节点。</p><p>3,计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。</p><p>4,如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。</p><p>5,如果最终获取锁失败了(<strong>可能由于获取到锁的 Redis 节点个数少于 N/2+1 ,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间</strong>),那么客户端应该立即向所有 Redis 节点发起释放锁的操作(这里来保证所有的 Redis 节点都可以报以获取的锁释放掉)。</p><p>Redlock 算法实现的前提是基于不同的机器具有相同的时钟,或者误差很小可以忽略不计的假设。(<strong>这也是DDIA作者喷的一点</strong>)</p><p>存在的问题:<br>1,关于Redis的持久化的问题<br>假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:<br>1.1 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。<br>1.2 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。<br>1.3 节点C重启后,客户端2锁住了C, D, E,获取锁成功。</p><p><strong>Redis 给出的解决方案:延迟重启,既当 Redis 节点挂掉之后不要立刻重启,而要等待一个锁的过期时间之后再重启。</strong></p><p>2,如果获取锁消耗的时间过多以至于无法完成后续的操作,如何释放锁?<br>个人认为需要业务方自己拿捏一个业务操作的需要消耗的时长,</p><p>3,antirez在设计Redlock的时候,是充分考虑了网络延迟和程序停顿所带来的影响的。但是,对于客户端和资源服务器之间的延迟(即发生在算法第3步之后的延迟),antirez 是承认所有的分布式锁的实现,包括 Redlock,是没有什么好办法来应对的。</p><h3 id="DDIA-作者对于-Redlock-的观点"><a href="#DDIA-作者对于-Redlock-的观点" class="headerlink" title="DDIA 作者对于 Redlock 的观点"></a>DDIA 作者对于 Redlock 的观点</h3><p>在 Martin 的这篇文章中,他把锁的用途分为两种:</p><ul><li>为了效率(efficiency),协调各个客户端避免做重复的工作。即使锁偶尔失效了,只是可能把某些操作多做一遍而已,不会产生其它的不良后果。比如重复发送了一封同样的 email。</li><li>为了正确性(correctness)。在任何情况下都不允许锁失效的情况发生,因为一旦发生,就可能意味着数据不一致(inconsistency),数据丢失,文件损坏,或者其它严重的问题。</li></ul><p>1,带有自动过期功能的分布式锁,必须提供某种fencing机制来保证对共享资源的真正的互斥保护。Redlock 提供不了这样一种机制。<br><img src="https://img.hchstudio.cn/unsafe-lock.png" alt><br>上图展示的是:由于GC停顿造成的共享资源被多个客户端访问的问题。原因是:客户端在 GC 停顿造成锁的等待超时从而被释放,然而客户端并不知道,认为自己还是处于持有锁的状态。</p><p><img src="https://img.hchstudio.cn/fencing-tokens.png" alt><br>上图展示的是:通过使用 fencing tokens 来解决锁失效未释放的问题。Redis 指出上图的漏洞,如果客户端1和客户端2都发生了GC pause,两个fencing token都延迟了,它们几乎同时到达了资源服务器,但保持了顺序,那么资源服务器是不是就检查不出问题了?这时对于资源的访问是不是就发生冲突了?</p><p>2,Redlock 构建在一个不够安全的系统模型之上。它对于系统的记时假设(timing assumption)有比较强的要求,而这些要求在现实的系统中是无法保证的。<br>Redlock 算法对机器时钟的强依赖,Martin 认为算法的实现不应该对时序做任何假设:进程可能会暂停任意时长,数据包可能会在网络中被任意延迟,时钟可能会被任意错误,即便如此,该算法仍可以正确执行并给出正确结果。<br>关于时钟的不可靠性:Redis 作者认为 Redlock 对时钟的要求,并不需要完全精确,它只需要时钟差不多精确就可以了。</p><p>3,在 Redlock 第三步完成之后的网络延迟,也为造成 Redlock 算法的失效,但是这个问题并不是 Redlock 算法独有的</p><p>Martin得出了如下的结论:</p><ul><li>如果是为了效率(efficiency)而使用分布式锁,允许锁的偶尔失效,那么使用单 Redis 节点的锁方案就足够了,简单而且效率高。Redlock 则是个过重的实现(heavy weight)。</li><li>如果是为了正确性(correctness)在很严肃的场合使用分布式锁,那么不要使用 Redlock。它不是建立在异步模型上的一个足够强的算法,它对于系统模型的假设中包含很多危险的成分(对于 timing)。而且,它没有一个机制能够提供 fencing token。那应该使用什么技术呢?Martin认为,应该考虑类似Zookeeper的方案,或者支持事务的数据库。</li></ul><h2 id="最后的讨论"><a href="#最后的讨论" class="headerlink" title="最后的讨论"></a>最后的讨论</h2><p>在了解了 Redlock 算法的逻辑及其可能存在的问题之后,我们可以针对自己的业务场景进行选择~</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html" target="_blank" rel="noopener">How to do distributed locking</a></p><p><a href="http://antirez.com/news/101" target="_blank" rel="noopener">Is Redlock safe?</a></p><p><a href="https://redis.io/topics/distlock" target="_blank" rel="noopener">Distributed locks with Redis</a></p>]]></content>
<summary type="html">
分布式锁的实现 Redlock 算法安全吗?
</summary>
<category term="Java" scheme="https://www.hchstudio.cn/categories/Java/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
</entry>
<entry>
<title>【译】Raft 学生指南</title>
<link href="https://www.hchstudio.cn/article/2020/62e/"/>
<id>https://www.hchstudio.cn/article/2020/62e/</id>
<published>2020-02-02T13:15:51.000Z</published>
<updated>2021-07-21T11:42:18.906Z</updated>
<content type="html"><![CDATA[<p>在过去的几个月中,我一直担任MIT的 <a href="https://pdos.csail.mit.edu/6.824/" target="_blank" rel="noopener">6.824 分布式系统课程</a>的助教。 传统上,该班级有许多基于 Paxos 共识算法的实验,但是今年,我们决定转向 <a href="https://raft.github.io/" target="_blank" rel="noopener">Raft</a>。 Raft 的设计更易于理解,我们希望这种改变可以使学生的学习更轻松。</p><p>这篇文章,以及随附的<a href="https://thesquareplanet.com/blog/instructors-guide-to-raft/" target="_blank" rel="noopener">《Raft 教师指南》</a>一文,记录了我们使用 Raft 的旅程,并希望对 Raft 协议的教学者和试图更好地了解 Raft 内部原理的学生有所帮助。 如果您想对 Paxos 与 Raft 进行比较,或者需要对 Raft 进行更多的教学分析,请阅读<a href="https://thesquareplanet.com/blog/instructors-guide-to-raft/" target="_blank" rel="noopener">《Raft 教师指南》</a>。 这篇文章的底部包含 6.824 个学生常见的<a href="https://thesquareplanet.com/blog/raft-qa/" target="_blank" rel="noopener">Q&A</a>。 如果您遇到的问题未在本文的主要内容中列出,请查看<a href="https://thesquareplanet.com/blog/raft-qa/" target="_blank" rel="noopener">Q&A</a>。 这篇文章很长,但它提出的所有观点都是许多 6.824 学生遇到的实际问题,这是值得一读的。</p><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>在我们深入研究 Raft 之前,有些背景可能会有用。 6.824 曾经有一组内置于 Go 中的基于 Paxos 的实验;选择 Go 是因为它语法简单易于学习,而且非常适合编写并发的分布式应用程序。 在四个实验过程中,学生将构建了一个容错的分布式 KV 存储系统。 第一个实验让他们建立了一个基于共识的日志库,第二个实验在此基础上添加了一个键值存储,第三个实验通过多个容错的分片主节点处理配置更改,在多个容错集群之间分了键空间。 我们还有第四个实验,学生必须在磁盘完好无损的情况下处理机器的故障和恢复。 该实验可作为学生的默认最终项目使用。</p><p>今年,我们决定使用 Raft 重写所有这些实验。 前三个实验都是相同的,但是第四个实验被删除了,因为 Raft 已经内置了持久性和故障恢复功能。 本文将主要讨论我们在第一个实验中的经验,因为它是与 Raft 最直接相关的经验,尽管我还将介绍如何在 Raft 之上构建应用程序。</p><p>Raft 是什么呢?对于那些刚开始了解它的人,我们最好用 Raft 官网上的文字来描述:</p><blockquote><p>Raft 是一种共识算法,旨在使其易于理解。 在容错和性能方面与 Paxos 相当。 区别在于它被分解为相对独立的子问题,并且彻底地解决了实际系统所需的所有主要问题。 我们希望 Raft 将使共识能够为更广泛的受众所接受,并且希望这个更广泛的受众能够开发出更高质量的基于共识的系统。</p></blockquote><p><a href="http://thesecretlivesofdata.com/raft/" target="_blank" rel="noopener">像这样的</a>可视化文件很好地概述了协议的主要组成部分,并且更直观的描述了 Raft 的各个阶段。 如果您还没有阅读过 <a href="http://nil.csail.mit.edu/6.824/2018/papers/raft-extended.pdf" target="_blank" rel="noopener">Raft 论文</a>,那么在继续本文之前,您应该先阅读该文档,因为我假设您对 Raft 相当熟悉。</p><p>与所有分布式共识协议一样,细节之处非常重要。 在没有故障的稳定状态下,Raft 的行为很容易理解,并且可以直观地进行解释。 例如,从可视化中很容易看出,假设没有失败,最终将选举一名 leader ,并且最终发送给 leader 的所有操作将由 followers 按正确的顺序进行。 但是,当引入延迟的消息,网络分区和故障服务器时,每一个 if , but 和 and 都变得至关重要。 特别是,由于阅读本文时的误解或疏忽,我们反复看到许多错误。 这个问题并非 Raft 独有,而是所有提供正确性的复杂分布式系统中都存在的问题。</p><h2 id="Raft-协议的实现"><a href="#Raft-协议的实现" class="headerlink" title="Raft 协议的实现"></a>Raft 协议的实现</h2><p>Raft 的最终指南在Raft 论文的 Figure 2 中。 该图指定了 Raft 服务器之间交换的每个 RPC 的行为,给出了服务器必须维护的各种不变量,并指定了何时应执行某些操作。 在本文的其余部分中,我们将大量讨论 Figure 2。如下文描述的一样。</p><p>Figure 2 定义了每个服务器在任何状态下对于每个传入的 RPC 应该做什么,以及何时应该发生某些其他事情(例如,何时可以安全地在日志中应用条目)。 刚开始,您可能会倾向于将 Figure 2 视为非正式指南。 您只需阅读一次,然后开始编写大致遵循其说要执行的实现的代码。 这样做,您将快速启动并运行大多数正常运行的 Raft 操作。 然后问题开始浮现了。</p><p>实际上,Figure 2 是非常精确的,并且应以规范任期将其所作的每个陈述都视为必须,而不应视为应该。 例如,每当您收到 AppendEntries 或 RequestVote RPC 时,您都可以合理地重置对等方的选举计时器,因为这两者都表明其他一些服务器要么认为自己是 leader,要么正试图成为 leader。 这意味着我们不应该干涉。 但是,如果您仔细阅读 Figure 2,如 Raft 论文所示:</p><blockquote><p>如果在没有收到当前 leader 的 AppendEntries RPC 或未向候选人授予投票的情况下经过选举超时,请转换为候选人。</p></blockquote><p>事实证明,区别非常重要,因为在某些情况下,前一种实现可能导致活动性大大降低。</p><h3 id="Raft-协议实现的细节"><a href="#Raft-协议实现的细节" class="headerlink" title="Raft 协议实现的细节"></a>Raft 协议实现的细节</h3><p>为了使讨论更加具体,让我们考虑一个例子,该例子使上 6.828 课程的学生人数增加了。 Raft 论文在许多地方提到了心跳 RPC。 具体来说,leader 会每个心跳间隔至少一次向所有对等方发送一个 AppendEntries RPC,以防止他们开始新的选举。 如果领导者没有新条目要发送到特定对等方,则 AppendEntries RPC 不包含任何条目,并被视为心跳。</p><p>我们的许多学生都认为心跳在某种程度上是“特殊的”。 当对等方收到心跳时,应将其与非心跳 AppendEntries RPC 区别对待。 特别是,许多人在接收到心跳信号后便会简单地重置其选举计时器,然后返回成功,而无需执行 Figure 2 中指定的任何检查。这非常危险。 通过接受 RPC,followers 可以隐式告诉 leader,他们的日志与领导者的日志相匹配,并且包括并包括在 AppendEntries 参数中的 prevLogIndex。 收到答复后,领导者可能错误地认为某些条目已被复制到大多数服务器,然后开始提交。</p><p>许多人遇到的另一个问题(通常是在解决上述问题后立即发生)是,一旦收到心跳,他们就会在 prevLogIndex 之后截断 followers 的日志,然后附加 AppendEntries 参数中包含的所有条目。 这也是不正确的。 我们可以再次转到 Figure 2:</p><blockquote><p>如果现有条目与新条目(索引相同但任期不同)冲突,则删除现有条目及其后的所有条目。</p></blockquote><p>如果在这里至关重要。 如果 followers 具有 leader 发送的所有条目,则 followers 务必不要截断其日志。 领导者发送的条目之后的任何元素都必须保留。 这是因为我们可能会从领导者那里收到过时的 AppendEntries RPC,而截断日志意味着“收回”我们可能已经告诉领导者我们在日志中的条目。</p><h2 id="调试-Raft-协议"><a href="#调试-Raft-协议" class="headerlink" title="调试 Raft 协议"></a>调试 Raft 协议</h2><p>不可避免地,您的 Raft 实现的第一个迭代会出现错误。 第二个也是如此。 第三。 第四。 总的来说,每个错误都比前一个错误少,并且根据经验,大多数错误是由于不忠实地遵循 Figure 2 而导致的。</p><p>在调试 Raft 时,通常有四个主要的 bug 来源:死锁,错误或不完整的 RPC 处理程序,未遵循规则以及任期混乱。 死锁也是一个普遍的问题,但是通常可以通过记录所有锁和解锁并找出要使用但不释放的锁来调试死锁。 让我们依次考虑以下每个方面:</p><h3 id="未释放锁"><a href="#未释放锁" class="headerlink" title="未释放锁"></a>未释放锁</h3><p>当系统发生动态锁定时,系统中的每个节点都在做某事,但是总的来说,您的节点都处于没有进展的状态。 在 Raft 中,这很容易发生,特别是如果您不认真地遵循 Figure 2 。 一种死锁情况特别经常出现。 没有选举任何 leader ,或者一旦选举了 leader,其他节点开始选举,迫使最近当选的 leader 立即退位。</p><p>出现这种情况的原因有很多,但我们已经看到许多学生犯了一些错误:</p><ul><li><p>确保您按照 Figure 2 所述正确地重置了选举计时器。具体来说,您仅应在以下情况下重新启动选举计时器:a)从当前 leader 那里获得了 AppendEntries RPC(如果AppendEntries参数中的任期已过时,则不应重置计时器); b)您正在开始选举;c)您给另一位 followers 投票。</p><p>在不可靠的网络中,后一种情况尤为重要,在这种网络中,followers 可能拥有不同的日志。在这种情况下,您通常只会获得少数服务器,而大多数服务器都愿意投票。如果您在有人要求您投票给他们投票时重置选举计时器,则日志过时的服务器和日志较长的服务器一样有可能前进。</p><p>实际上,由于只有很少的服务器具有足够的最新日志,因此这些服务器不太可能能够以足够的和平度进行选举。如果您遵循 Figure 2 中的规则,则具有最新日志的服务器将不会因过时的服务器选举而中断,因此更有可能完成选举并成为 leader。</p></li><li><p>请按照 Figure 2 的指示进行何时开始选举。 特别要注意的是,如果您是候选人(即您当前正在进行选举),但是选举计时器触发了,则应该重新进行选举。 这对于避免由于RPC延迟或丢失而导致的系统停顿非常重要。</p></li><li><p>在处理传入的RPC之前,请确保遵循“服务器规则”中的第二条规则。 第二条规则指出:</p><blockquote><p>如果RPC请求或响应包含条件 T > currentTerm :设置 currentTerm = T,则转换为 followers </p></blockquote></li></ul><p>例如,如果您已经对当任期进行了投票,而传入的 RequestVote RPC 具有较高的任期,那么您应该首先卸任并采用其任期(从而重置votedFor),然后处理 RPC,这将导致您同意投票!</p><h3 id="不正确的RPC处理程序"><a href="#不正确的RPC处理程序" class="headerlink" title="不正确的RPC处理程序"></a>不正确的RPC处理程序</h3><p>即使 Figure 2 清楚地说明了每个 RPC 处理程序应该做什么,但仍然有些微妙的地方容易遗漏。 这是我们不断反复看到的少数几个,您在实施时应格外注意:</p><ul><li>如果某个步骤说“答复错误”,则意味着您应立即答复,而不要执行任何后续步骤。</li><li>如果您获得带有指向日志末尾的prevLogIndex的AppendEntries RPC,则应像处理该条目但该任期不匹配一样处理它(即,回复false)。</li><li>选中#2,即使领导者未发送任何条目,也应执行 AppendEntries RPC 处理程序。</li><li>AppendEntries 的最后一步(#5)中的最小值是必需的,并且需要使用最后一个新条目的索引进行计算。 仅具有在日志到达末尾时在 lastApplied 和 commitIndex 停止之间应用日志中的内容的功能还不够。 这是因为在领导者发送给您的条目之后,您的日志中可能有与领导者的日志不同的条目(所有条目都与您的日志中的条目匹配)。 由于#3要求您仅在条目冲突时才截断日志,因此不会删除这些条目,并且如果 LeaderCommit 超出了领导者发送给您的条目,则您可能会应用错误的条目。</li><li>严格按照第5.4节中的描述实施“最新日志”检查很重要。 没有作弊,只是检查长度!</li></ul><h3 id="不遵守规则"><a href="#不遵守规则" class="headerlink" title="不遵守规则"></a>不遵守规则</h3><p>尽管 Raft 论文非常明确地说明了如何实现每个 RPC 处理程序,但它也保留了许多未指定的规则和不变量的实现。 它们在 Figure 2 右侧的“服务器规则”块中列出。尽管其中一些是不言自明的,但也有一些需要非常仔细地设计应用程序,以使其不违反The Rules:</p><ul><li>在执行过程中的任何时候执行 <code>go if commitIndex> lastApplied</code>,则应应用特定的日志条目。 立即执行操作(例如,在AppendEntries RPC处理程序中)不是至关重要的,但是确保此应用程序仅由一个实体完成很重要。 具体来说,您将需要一个专用的“应用程序”,或者锁定这些应用程序,以便其他一些例程也不会检测到需要应用条目并尝试应用。</li><li>确保您定期或在更新 commitIndex 之后(即在 matchIndex 更新之后)检查 commitIndex> lastApplied。 例如,如果在将 AppendEntries 发送给对等对象的同时检查 commitIndex,则可能必须等到下一个条目追加到日志之后才能应用刚刚发送并得到确认的条目。</li><li>如果领导者发出一个 AppendEntries RPC 并被拒绝,但不是由于日志不一致(只有在我们的任期过去时才可能发生),那么您应该立即下台,而不要更新 nextIndex。 如果这样做的话,如果立即连任,您可以与 nextIndex 的重置竞争。</li><li>领导不允许将commitIndex更新到上一个任期(或就此而言,将来的任期)中的某个位置。 因此,按照规则所说,您特别需要检查 log[N].term == currentTerm。 这是因为 Raft 领导者如果不是从其当前任期开始,就无法确定该条目是否确实已提交(将来也不会更改)。 Raft 论文的 Figure 8 对此进行了说明。</li></ul><p>造成混淆的一个常见原因是 nextIndex 和 matchIndex 之间的差异。 特别是,您可能会注意到 matchIndex = nextIndex-1,而根本没有实现 matchIndex。 这不安全。 通常通常将 nextIndex 和 matchIndex 同时更新为相似的值(具体来说,nextIndex = matchIndex + 1),但两者的作用却截然不同。 nextIndex 是对领导者与给定关注者共享的前缀的猜测。 它通常是相当乐观的(我们共享一切),并且仅在负面响应时才向后移动。 例如,当刚刚选择一个领导者时,将 nextIndex 设置为日志末尾的索引索引。 在某种程度上,nextIndex 用于提高性能–您只需要将这些内容发送给该对等方即可。</p><p>matchIndex 用于安全。 这是对领导者与给定跟随者共享日志的前缀的保守度量。 matchIndex 永远不能设置为太高的值,因为这可能导致 commitIndex 移得太远。 这就是为什么将 matchIndex 初始化为-1(即我们没有前缀),并且仅在关注者肯定地确认 AppendEntries RPC 时才进行更新的原因。</p><h3 id="任期不一致"><a href="#任期不一致" class="headerlink" title="任期不一致"></a>任期不一致</h3><p>任期混淆是指服务器被来自旧任期的 RPC 混淆。通常,在接收 RPC 时这不是问题,因为 Figure 2 中的规则明确说明了您看到旧任期时应采取的措施。但是,Figure 2 通常不会讨论收到旧的 RPC 答复时应采取的措施。根据经验,到目前为止,最简单的方法是首先在答复中记录该任期(该任期可能高于您当前的任期),然后将当前任期与您在原始RPC中发送的任期进行比较。如果两者不同,请放弃答复并返回。仅当两个任期相同时,您才可以继续处理答复。您可以使用一些聪明的协议推理在此处进行进一步的优化,但是这种方法似乎很好用。不这样做会导致眼泪和绝望的漫长曲折道路。</p><p>一个相关但不完全相同的问题是,假设在您发送 RPC 的时间与收到回复之间的状态没有发生变化。一个很好的例子是在收到对 RPC 的响应时设置 matchIndex = nextIndex-1 或 matchIndex = len(log)。这是不安全的,因为自发送 RPC 以来,这两个值都可能已更新。相反,正确的做法是将 matchIndex 更新为您最初在 RPC 中发送的参数的 prevLogIndex + len(entries [])。</p><h3 id="先把优化放在一边"><a href="#先把优化放在一边" class="headerlink" title="先把优化放在一边"></a>先把优化放在一边</h3><p>Raft 包括几个有趣的可选功能。 在 6.824 中,我们要求学生实施其中两个:日志压缩(第7节)和加速日志回溯(第8页左上角)。 前者对于避免日志无限制增长是必要的,而后者对于使过时的 follower 快速更新很有用。</p><p>这些功能不是 Raft 最核心的一部分,因此在本文中没有像主要共识协议那样受到足够的重视。 日志压缩已经相当全面地介绍了(在论文图13中),但是省略了一些设计细节,如果您随便阅读它可能会错过:</p><ul><li><p>对应用程序状态进行快照时,需要确保应用程序状态与 Raft 日志中某个已知索引之后的状态相对应。 这意味着应用程序需要与 Raft 通信该快照所对应的索引,或者 Raft 需要延迟应用其他日志条目,直到快照完成为止。</p></li><li><p>本文不讨论服务器崩溃时的恢复协议,并且由于涉及快照而重新出现。特别是,如果筏状态和快照分别提交,则服务器可能在持久快照和持久更新更新的筏状态之间崩溃。这是一个问题,因为论文的图13中的步骤7指示必须删除快照覆盖的Raft日志。<br>如果在服务器恢复时读取了更新的快照,但读取了过时的日志,则可能最终应用了快照中已包含的一些日志条目。发生这种情况是因为 commitIndex 和 lastApplied 未持久保存,因此 Raft 不知道这些日志条目已被应用。解决此问题的方法是在 Raft 中引入一个持久状态,该状态记录 Raft 持久日志中第一个条目所对应的“真实”索引。然后可以将其与加载的快照的 lastIncludedIndex 进行比较,以确定要丢弃日志开头的哪些元素。</p></li></ul><p>加速日志回溯优化的规格非常少,可能是因为作者认为对于大多数部署而言,它不是必需的。 从文本中不清楚不清楚领导者应如何使用从客户端发送回的冲突索引和任期来确定要使用的 nextIndex 。 我们认为作者可能希望您遵循的协议是:</p><ul><li>If a follower does not have prevLogIndex in its log, it should return with conflictIndex = len(log) and conflictTerm = None.</li><li>If a follower does have prevLogIndex in its log, but the term does not match, it should return conflictTerm = log[prevLogIndex].Term, and then search its log for the first index whose entry has term equal to conflictTerm.</li><li>Upon receiving a conflict response, the leader should first search its log for conflictTerm. If it finds an entry in its log with that term, it should set nextIndex to be the one beyond the index of the last entry in that term in its log.</li><li>If it does not find an entry with that term, it should set nextIndex = conflictIndex.</li></ul><p>一个半途而废的解决方案是只使用冲突索引(并忽略冲突term),这简化了实现,但是领导者有时最终会向追随者发送比严格更新最新日志条目更多的日志条目。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://thesquareplanet.com/blog/students-guide-to-raft/" target="_blank" rel="noopener">Students’ Guide to Raft</a></p>]]></content>
<summary type="html">
在过去的几个月中,我一直担任MIT的 [6.824 分布式系统课程](https://pdos.csail.mit.edu/6.824/)的助教。 传统上,该班级有许多基于 Paxos 共识算法的实验,但是今年,我们决定转向 [Raft](https://raft.github.io/)。 Raft 的设计更易于理解,我们希望这种改变可以使学生的学习更轻松。
</summary>
<category term="Raft" scheme="https://www.hchstudio.cn/categories/Raft/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Raft" scheme="https://www.hchstudio.cn/tags/Raft/"/>
</entry>
<entry>
<title>ThreadPoolExecutor 的简单梳理</title>
<link href="https://www.hchstudio.cn/article/2020/c5a/"/>
<id>https://www.hchstudio.cn/article/2020/c5a/</id>
<published>2020-01-19T13:15:51.000Z</published>
<updated>2021-07-21T11:42:18.907Z</updated>
<content type="html"><![CDATA[<p>还是楼主惯用的论述三连问,先问是什么,再问为什么,最后祭除终极大杀器 <code>just do it</code> ……</p><h2 id="what"><a href="#what" class="headerlink" title="what ?"></a>what ?</h2><p>那么什么是线程池呢?总的来说,线程池是一种线程使用模式。线程的频繁创建于调度会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。</p><h2 id="why"><a href="#why" class="headerlink" title="why ?"></a>why ?</h2><p>上面说的是整个线程池的总体概念,当然 <code>Java</code> 中的线程池也是基于相同的理念设计的。在 <code>Java</code> 线程池可以提高线程复用,又可以固定最大线程使用量,防止无限制地创建线程。当程序提交一个任务需要一个线程时,会去线程池中查找是否有空闲的线程,若有,则直接使用线程池中的线程工作,若没有,会去判断当前已创建的线程数量是否超过最大线程数量,如未超过,则创建新线程,如已超过,则进行排队等待或者直接抛出异常。如下图所示<br><img src="https://img.hchstudio.cn/Thread-pool2.png" alt></p><p>总的来说,线程池就是把资源池化,预先准备好,以避免不必要额外开销。</p><h2 id="how"><a href="#how" class="headerlink" title="how ?"></a>how ?</h2><p><code>Java</code> 提供了一套 <code>Executor</code> 框架,根据常用的场景对 <code>ThreadPoolExecutor</code> 类做了简单的封装,当然这样做的话难免有些束手束脚,所以大部分情况下都是根据自己的业务需求直接调用 <code>ThreadPoolExecutor</code> 实现自己的线程池。<br><img src="https://img.hchstudio.cn/Executors.png" alt></p><p>这个框架中包括了 <code>ScheduledThreadPoolExecutor</code> 和 <code>ThreadPoolExecutor</code> 两个核心线程池。前者是用来定时执行任务,后者是用来执行被提交的任务。因为这两个线程池的原理是一样的,都是调用底层的 <code>ThreadPoolExecutor</code>,下面我们就重点看看 ThreadPoolExecutor 类是如何实现线程池的。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">ThreadPoolExecutor</span><span class="params">(<span class="keyword">int</span> corePoolSize,//线程池的核心线程数量</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">int</span> maximumPoolSize,//线程池的最大线程数</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">long</span> keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间</span></span></span><br><span class="line"><span class="function"><span class="params"> TimeUnit unit,//时间单位</span></span></span><br><span class="line"><span class="function"><span class="params"> BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列</span></span></span><br><span class="line"><span class="function"><span class="params"> ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可</span></span></span><br><span class="line"><span class="function"><span class="params"> RejectedExecutionHandler handler)</span> <span class="comment">//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务</span></span></span><br></pre></td></tr></table></figure></p><p>通过上面代码,我们发现线程池有两个线程数的设置,一个为核心线程数,一个为最大线程数。在创建完线程池之后,默认情况下,线程池中并没有任何线程,等到有任务来才创建线程去执行任务。</p><p>当创建的线程数等于 <code>corePoolSize</code> 时,提交的任务会被加入到设置的阻塞队列中。当队列满了,会创建线程执行任务,直到线程池中的数量等于 <code>maximumPoolSize</code>。当线程数量已经等于 <code>maximumPoolSize</code> 时, 新提交的任务无法加入到等待队列,也无法创建非核心线程直接执行,当我们又没有为线程池设置具体的拒绝策略时,线程池就会抛出 <code>RejectedExecutionException</code> 异常,即线程池拒绝接受当前提交的任务。</p><p>当线程池中创建的线程数量超过设置的 <code>corePoolSize</code>,在某些线程处理完任务后,如果等待 <code>keepAliveTime</code> 时间后仍然没有新的任务分配给它,那么这个线程将会被回收。线程池回收线程时,会对所谓的“核心线程”和“非核心线程”一视同仁,直到线程池中线程的数量等于设置的 <code>corePoolSize</code> 参数,回收过程才会停止。从下面代码中可以看出<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setCorePoolSize</span><span class="params">(<span class="keyword">int</span> corePoolSize)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (corePoolSize < <span class="number">0</span> || maximumPoolSize < corePoolSize)</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException();</span><br><span class="line"> <span class="keyword">int</span> delta = corePoolSize - <span class="keyword">this</span>.corePoolSize;</span><br><span class="line"> <span class="keyword">this</span>.corePoolSize = corePoolSize;</span><br><span class="line"> <span class="keyword">if</span> (workerCountOf(ctl.get()) > corePoolSize)</span><br><span class="line"> interruptIdleWorkers();</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (delta > <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">int</span> k = Math.min(delta, workQueue.size());</span><br><span class="line"> <span class="keyword">while</span> (k-- > <span class="number">0</span> && addWorker(<span class="keyword">null</span>, <span class="keyword">true</span>)) {</span><br><span class="line"> <span class="keyword">if</span> (workQueue.isEmpty())</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>到目前为止,楼主只是围绕 <code>ThreadPoolExecutor</code> 的构造方法,简单阐述了下 Java 中的线程池实现的基本逻辑,想要更深入了理解,也可以尝试在阅读完 <code>JDK</code> 的线程池实现源码之后造个轮子。</p>]]></content>
<summary type="html">
前两天在一个技术交流群里,看到群友在讨论关于 `Java` 线程池的使用,以及线程池的几个参数如何设置的问题,本着脑子会忘的原则,记录下自己对线程池的理解~
</summary>
<category term="ThreadPoolExecutor" scheme="https://www.hchstudio.cn/categories/ThreadPoolExecutor/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Java" scheme="https://www.hchstudio.cn/tags/Java/"/>
</entry>
<entry>
<title>MapReduce 的简单实现</title>
<link href="https://www.hchstudio.cn/article/2020/1ed5/"/>
<id>https://www.hchstudio.cn/article/2020/1ed5/</id>
<published>2020-01-18T13:15:51.000Z</published>
<updated>2021-07-21T11:42:18.905Z</updated>
<content type="html"><![CDATA[<p>最近有幸拜读 Google 分布式的三大论文,本着好记性不如烂笔头的原则,谈谈楼主对分布式系统开发的一点小小的心得~</p><p>相信用过 Hadoop 的同学在等待结果输出的时候会出现类似于这样的 <code>INFO : 2020-01-17 11:44:14,132 Stage-11 map = 0%, reduce = 0%</code> 的日志,它展示了 MapReduce 的执行过程,下面我们也将就 MapReduce 进行展开,阐述 MapReduce 的执行原理以及根据 Google 的论文实现了 mini 版的 MapReduce。</p><h2 id="什么是-MapReduce"><a href="#什么是-MapReduce" class="headerlink" title="什么是 MapReduce"></a>什么是 MapReduce</h2><blockquote><p>MapReduce is a programming model and an associated implementation for processing and generating large data sets. Users specify a map function that processes a key/value pair to generate a set of intermediate key/value pairs, and a reduce function that merges all intermediate values associated with the same intermediate key.</p></blockquote><p>就像 Google 的 MapReduce 论文中所说的, MapReduce 是一个编程模型,也是一个处理和生成超大数据集的算法模型的相关实现。用户首先创建一个 Map 函数处理一个基于 <code>key/value pair</code> 的数据集合,输出中间的基于 <code>key/value pair</code> 的数据集合,然后再创建一个 Reduce 函数用来合并所有的具有相同中间 key 值的中间 value 值。</p><h2 id="MapReduce-的例子"><a href="#MapReduce-的例子" class="headerlink" title="MapReduce 的例子"></a>MapReduce 的例子</h2><h3 id="Map-与-Reduce-的原理"><a href="#Map-与-Reduce-的原理" class="headerlink" title="Map 与 Reduce 的原理"></a>Map 与 Reduce 的原理</h3><p>MapReduce编程模型的原理是:利用一个输入 <code>key/value pair</code> 集合来产生一个输出的 <code>key/value pair</code> 集合。MapReduce 库的用户用两个函数表达这个计算:Map和Reduce。</p><p><strong>Map :</strong> 用户自定义的 Map 函数接受一个输入的 <code>key/value pair</code> 值,然后产生一个中间 <code>key/value pair</code> 值的集合。MapReduce 库把所有具有相同中间 key 值的中间 value 值集合在一起后传递给 Reduce 函数。</p><p><strong>Reduce :</strong> 用户自定义的 Reduce 函数接受一个中间 key 的值和相关的一个 value 值的集合。Reduce 函数合并这些 value 值,形成一个较小的 value 值的集合。一般的,每次 Reduce 函数调用只产生 0 或 1 个输出 value 值。通常我们通过一个迭代器把中间 value 值提供给 Reduce 函数,这样我们就可以处理无法全部放入内存中的大量的 value 值的集合。</p><h3 id="Map-与-Reduce-的应用例子"><a href="#Map-与-Reduce-的应用例子" class="headerlink" title="Map 与 Reduce 的应用例子"></a>Map 与 Reduce 的应用例子</h3><ul><li>计算文档中每个单词出现的次数:Map 函数处理文档,对文档中的单词进行拆分,然后输出(单词,1)。Reduce 函数把相同单词的 value 值都累加起来,产生(单词,记录总数)结果。</li><li>倒排索引:Map 函数分析每个文档输出一个(词,文档号)的列表,Reduce函数的输入是一个给定词的所有(词,文档号),排序所有的文档号,输出(词,list(文档号))。所有的输出集合形成一个简单的倒排索引,它以一种简单的算法跟踪词在文档中的位置。</li></ul><h2 id="MapReduce-执行过程图解"><a href="#MapReduce-执行过程图解" class="headerlink" title="MapReduce 执行过程图解"></a>MapReduce 执行过程图解</h2><p><img src="https://img.hchstudio.cn/map-reduce-exec.png" alt="Execution overview"><br>上图中展示了我们的 MapReduce 实现中执行的全部流程。当用户调用MapReduce函数时,将发生下面的一系列动作(下面的序号和上图中的序号一一对应):</p><ol><li>用户程序首先调用的 MapReduce 库将输入文件分成 M 个数据片度,每个数据片段的大小一般从 16MB 到 64MB (可以通过可选的参数来控制每个数据片段的大小)。然后用户程序在机群中创建大量的程序副本。</li><li>这些程序副本中的有一个特殊的程序 <code>master</code> 。副本中其它的程序都是 <code>worker</code> 程序,由 <code>master</code> 分配任务。有 M 个 Map 任务和 R 个 Reduce 任务将被分配,<code>master</code> 将一个 Map 任务或 Reduce 任务分配给一个空闲的 <code>worker</code>。</li><li>被分配了 Map 任务的 <code>worker</code> 程序读取相关的输入数据片段,从输入的数据片段中解析出 <code>key/value pair</code> ,然后把 <code>key/value pair</code> 传递给用户自定义的 Map 函数,由 Map 函数生成并输出的中间 <code>key/value pair</code>,并缓存在内存中。</li><li>缓存中的<code>key/value pair</code> 通过分区函数分成 R 个区域,之后周期性的写入到本地磁盘上。缓存的 <code>key/value pair</code> 在本地磁盘上的存储位置将被回传给 <code>master</code>,由 <code>master</code> 负责把这些存储位置再传送给 <code>Reduce worker</code> 。</li><li>当 <code>Reduce worker</code> 程序接收到 <code>master</code> 程序发来的数据存储位置信息后,使用 RPC 从 <code>Map worker</code> 所在主机的磁盘上读取这些缓存数据。当 <code>Reduce worker</code> 读取了所有的中间数据后,通过对 key 进行排序后使得具有相同 key 值的数据聚合在一起。由于许多不同的 key 值会映射到相同的 Reduce 任务上,因此必须进行排序。如果中间数据太大无法在内存中完成排序,那么就要在外部进行排序。</li><li><code>Reduce worker</code> 程序遍历排序后的中间数据,对于每一个唯一的中间 key 值,<code>Reduce worker</code> 程序将这个key值和它相关的中间 value 值的集合传递给用户自定义的 Reduce 函数。Reduce 函数的输出被追加到所属分区的输出文件。</li><li>当所有的 Map 和 Reduce 任务都完成之后,<code>master</code> 唤醒用户程序。在这个时候,在用户程序里的对 MapReduce 调用才返回。</li></ol><p>在成功完成任务之后,MapReduce 的输出存放在 R 个输出文件中(对应每个 Reduce 任务产生一个输出文件,文件名由用户指定)。一般情况下,用户不需要将这 R 个输出文件合并成一个文件,我们经常把这些文件作为另外一个 MapReduce 的输入,或者在另外一个可以处理多个分割文件的分布式应用中使用。</p><h2 id="MapReduce-程序的实现"><a href="#MapReduce-程序的实现" class="headerlink" title="MapReduce 程序的实现"></a>MapReduce 程序的实现</h2><p>MapReduce 的核心就是实现其 Map 与 Reduce 的逻辑代码,显示楼主将就在上面描述的 Map 与 Reduce 的执行过程完成对 Map 与 Reduce 的实现。</p><h3 id="实现-Map"><a href="#实现-Map" class="headerlink" title="实现 Map"></a>实现 Map</h3><p>1,下面的 doMap 函数管理一项 map 任务:它读取输入文件(inFile),为该文件的内容调用用户定义的 map 函数(mapF),然后将 mapF 的输出分区为 nReduce 中间文件。</p><p>2,每个 reduce 任务对应一个中间文件。文件名包括 map 任务编号和 reduce 任务编号。使用由reduceName 函数生成的文件名作为 reduce 任务的中间文件。在每个key mod nReduce 上调用 ihash()来选择对应的 reduce 任务。</p><p>3,mapF 是应用程序提供的 map 函数。第一个参数应该是输入文件名。第二个参数应该是整个输入文件的内容。 mapF()返回包含用于 reduce 的键/值对的切片。</p><p>4,下面程序中使用 json 格式将 mapF 处理好的数据写入文件中,为了数据处理方便,下面程序中处理好的每条数据都采用换行符进行分割。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">reduceName</span><span class="params">(jobName <span class="keyword">string</span>, mapTask <span class="keyword">int</span>, reduceTask <span class="keyword">int</span>)</span> <span class="title">string</span></span> {</span><br><span class="line"><span class="keyword">return</span> <span class="string">"mrtmp."</span> + jobName + <span class="string">"-"</span> + strconv.Itoa(mapTask) + <span class="string">"-"</span> + strconv.Itoa(reduceTask)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">doMap</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params">jobName <span class="keyword">string</span>, // MapReduce 的任务名称</span></span></span><br><span class="line"><span class="function"><span class="params">mapTask <span class="keyword">int</span>, // 当前执行的 mapTask</span></span></span><br><span class="line"><span class="function"><span class="params">inFile <span class="keyword">string</span>, // 输入的的文件</span></span></span><br><span class="line"><span class="function"><span class="params">nReduce <span class="keyword">int</span>, // reduceTask 的数量</span></span></span><br><span class="line"><span class="function"><span class="params">mapF <span class="keyword">func</span>(filename <span class="keyword">string</span>, contents <span class="keyword">string</span>)</span> []<span class="title">KeyValue</span>, // 用户自定义的 <span class="title">map</span> 函数</span></span><br><span class="line">) {</span><br><span class="line">f, err := os.Open(inFile)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">debug(<span class="string">"open file err %v"</span>, err)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">defer</span> f.Close()</span><br><span class="line"></span><br><span class="line">dat, err := ioutil.ReadAll(f)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">debug(<span class="string">"open map file err %v"</span>, err)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">res := mapF(inFile, <span class="keyword">string</span>(dat))</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, kv := <span class="keyword">range</span> res {</span><br><span class="line"></span><br><span class="line">hash := ihash(kv.Key)</span><br><span class="line">r := hash % nReduce</span><br><span class="line"><span class="comment">// mrtmp.xxx-0-0</span></span><br><span class="line">fd, err := os.OpenFile(reduceName(jobName, mapTask, r), os.O_RDWR|os.O_CREATE|os.O_APPEND, <span class="number">0644</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">debug(<span class="string">"open mrtmp.xxx file err %v"</span>, err)</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">enc := json.NewEncoder(fd)</span><br><span class="line"><span class="keyword">if</span> err := enc.Encode(&kv); err != <span class="literal">nil</span> {</span><br><span class="line">debug(<span class="string">"encode json err %v"</span>, err)</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">}</span><br><span class="line">fd.Close()</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ihash</span><span class="params">(s <span class="keyword">string</span>)</span> <span class="title">int</span></span> {</span><br><span class="line">h := fnv.New32a()</span><br><span class="line">h.Write([]<span class="keyword">byte</span>(s))</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">int</span>(h.Sum32() & <span class="number">0x7fffffff</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="实现-Reduce"><a href="#实现-Reduce" class="headerlink" title="实现 Reduce"></a>实现 Reduce</h3><p>doReduce 管理一个 reduce 任务:它读取任务的中间文件,按 key 对中间文件中的数据对进行排序,为每个 key 调用用户定义的 reduceF 函数,并将 reduceF 的输出的写入磁盘。<br><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">reduceName</span><span class="params">(jobName <span class="keyword">string</span>, mapTask <span class="keyword">int</span>, reduceTask <span class="keyword">int</span>)</span> <span class="title">string</span></span> {</span><br><span class="line"><span class="keyword">return</span> <span class="string">"mrtmp."</span> + jobName + <span class="string">"-"</span> + strconv.Itoa(mapTask) + <span class="string">"-"</span> + strconv.Itoa(reduceTask)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">doReduce</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params">jobName <span class="keyword">string</span>, // MapReduce 的任务名称</span></span></span><br><span class="line"><span class="function"><span class="params">reduceTask <span class="keyword">int</span>, // 当前运行的 reduce 任务的任务号</span></span></span><br><span class="line"><span class="function"><span class="params">outFile <span class="keyword">string</span>, // 结果输出的文件路径</span></span></span><br><span class="line"><span class="function"><span class="params">nMap <span class="keyword">int</span>, // <span class="keyword">map</span> 任务的个数</span></span></span><br><span class="line"><span class="function"><span class="params">reduceF <span class="keyword">func</span>(key <span class="keyword">string</span>, values []<span class="keyword">string</span>)</span> <span class="title">string</span>, // 用户的自定义 <span class="title">reduce</span> 函数</span></span><br><span class="line">) {</span><br><span class="line">kvMap := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="keyword">string</span>][]<span class="keyword">string</span>)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < nMap; i++ {</span><br><span class="line"><span class="function"><span class="keyword">func</span><span class="params">()</span></span> {</span><br><span class="line">inFileName := reduceName(jobName, i, reduceTask)</span><br><span class="line">inFile, err := os.Open(inFileName)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="built_in">panic</span>(<span class="string">"can't open file:"</span> + inFileName)</span><br><span class="line">}</span><br><span class="line"><span class="keyword">defer</span> inFile.Close()</span><br><span class="line"></span><br><span class="line"><span class="comment">// Read and Decoder the file</span></span><br><span class="line"><span class="keyword">var</span> kv KeyValue</span><br><span class="line"><span class="keyword">for</span> decoder := json.NewDecoder(inFile); decoder.Decode(&kv) != io.EOF; {</span><br><span class="line">kvMap[kv.Key] = <span class="built_in">append</span>(kvMap[kv.Key], kv.Value)</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 class="keyword">var</span> keys []<span class="keyword">string</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// sort by key</span></span><br><span class="line"><span class="keyword">for</span> k := <span class="keyword">range</span> kvMap {</span><br><span class="line">keys = <span class="built_in">append</span>(keys, k)</span><br><span class="line">}</span><br><span class="line">sort.Strings(keys)</span><br><span class="line"></span><br><span class="line"><span class="comment">// reduce</span></span><br><span class="line">outfd, err := os.Create(outFile)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="built_in">panic</span>(<span class="string">"can't create file:"</span> + outFile)</span><br><span class="line">}</span><br><span class="line"><span class="keyword">defer</span> outfd.Close()</span><br><span class="line">enc := json.NewEncoder(outfd)</span><br><span class="line"><span class="keyword">for</span> _, k := <span class="keyword">range</span> keys {</span><br><span class="line">reducedValue := reduceF(k, kvMap[k])</span><br><span class="line">enc.Encode(KeyValue{Key: k, Value: reducedValue})</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h3 id="对-doMap-与-doReduce-的封装"><a href="#对-doMap-与-doReduce-的封装" class="headerlink" title="对 doMap 与 doReduce 的封装"></a>对 doMap 与 doReduce 的封装</h3><p>下面的函数是对 doMap 与 doReduce 进行顺序调用,生成 MapReduce 任务的结果输出到结果文件中。<br><figure class="highlight go"><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="function"><span class="keyword">func</span> <span class="title">Sequential</span><span class="params">(jobName <span class="keyword">string</span>, files []<span class="keyword">string</span>, nreduce <span class="keyword">int</span>,</span></span></span><br><span class="line"><span class="function"><span class="params">mapF <span class="keyword">func</span>(<span class="keyword">string</span>, <span class="keyword">string</span>)</span> []<span class="title">KeyValue</span>,</span></span><br><span class="line">reduceF <span class="function"><span class="keyword">func</span><span class="params">(<span class="keyword">string</span>, []<span class="keyword">string</span>)</span> <span class="title">string</span>,</span></span><br><span class="line">) (mr *Master) {</span><br><span class="line">mr = newMaster(<span class="string">"master"</span>)</span><br><span class="line"><span class="keyword">go</span> mr.run(jobName, files, nreduce, <span class="function"><span class="keyword">func</span><span class="params">(phase jobPhase)</span></span> {</span><br><span class="line"><span class="keyword">switch</span> phase {</span><br><span class="line"><span class="keyword">case</span> mapPhase:</span><br><span class="line"><span class="keyword">for</span> i, f := <span class="keyword">range</span> mr.files {</span><br><span class="line">doMap(mr.jobName, i, f, mr.nReduce, mapF)</span><br><span class="line">}</span><br><span class="line"><span class="keyword">case</span> reducePhase:</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < mr.nReduce; i++ {</span><br><span class="line">doReduce(mr.jobName, i, mergeName(mr.jobName, i), <span class="built_in">len</span>(mr.files), reduceF)</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}, <span class="function"><span class="keyword">func</span><span class="params">()</span></span> {</span><br><span class="line">mr.stats = []<span class="keyword">int</span>{<span class="built_in">len</span>(files) + nreduce}</span><br><span class="line">})</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h2 id="使用-MapReduce-实现词频统计和倒排索引"><a href="#使用-MapReduce-实现词频统计和倒排索引" class="headerlink" title="使用 MapReduce 实现词频统计和倒排索引"></a>使用 MapReduce 实现词频统计和倒排索引</h2><p>在上面我们提到了 MapReduce 在实际应用中的例子,下面我们将对这两个例子做一下简单的实现。</p><h3 id="实现词频统计"><a href="#实现词频统计" class="headerlink" title="实现词频统计"></a>实现词频统计</h3><p>为了实现词频统计这一功能,我们使用 MapReduce 框架的思路就是实现自定义的 map 与 reduce 函数:<br>1,map:读取文档,将文档中的单词逐个提取出来,生成(单词,1)这样的键值对,然后把数据罗盘,写入到中间文件中。<br>2,reduce:读取中间文件,按照键值对进行排序,将 key 相同的数据聚合到一起,统计每个单词出现的次数,然后将结果写入到文件中落盘。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">"6.824/src/mapreduce"</span></span><br><span class="line"><span class="string">"fmt"</span></span><br><span class="line"><span class="string">"os"</span></span><br><span class="line"><span class="string">"strconv"</span></span><br><span class="line"><span class="string">"strings"</span></span><br><span class="line"><span class="string">"unicode"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">mapF</span><span class="params">(filename <span class="keyword">string</span>, contents <span class="keyword">string</span>)</span> <span class="params">(res []mapreduce.KeyValue)</span></span> {</span><br><span class="line"><span class="comment">// Your code here (Part II).</span></span><br><span class="line">f := <span class="function"><span class="keyword">func</span><span class="params">(c <span class="keyword">rune</span>)</span> <span class="title">bool</span></span> {</span><br><span class="line"><span class="keyword">return</span> !unicode.IsLetter(c)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">words := strings.FieldsFunc(contents, f)</span><br><span class="line"><span class="keyword">for</span> _, w := <span class="keyword">range</span> words {</span><br><span class="line">kv := mapreduce.KeyValue{Key: w, Value: <span class="string">"1"</span>}</span><br><span class="line">res = <span class="built_in">append</span>(res, kv)</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> res</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">reduceF</span><span class="params">(key <span class="keyword">string</span>, values []<span class="keyword">string</span>)</span> <span class="title">string</span></span> {</span><br><span class="line"><span class="comment">// Your code here (Part II).</span></span><br><span class="line">sum := <span class="number">0</span></span><br><span class="line"><span class="keyword">for</span> _, e := <span class="keyword">range</span> values {</span><br><span class="line">data, err := strconv.Atoi(e)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">fmt.Printf(<span class="string">"Reduce err %s%v\n"</span>, key, err)</span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">}</span><br><span class="line">sum += data</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> strconv.Itoa(sum)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(os.Args) < <span class="number">4</span> {</span><br><span class="line">fmt.Printf(<span class="string">"%s: see usage comments in file\n"</span>, os.Args[<span class="number">0</span>])</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"><span class="keyword">var</span> mr *mapreduce.Master</span><br><span class="line">mr = mapreduce.Sequential(<span class="string">"wcseq"</span>, os.Args[<span class="number">3</span>:], <span class="number">3</span>, mapF, reduceF)</span><br><span class="line">mr.Wait()</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="实现倒排索引"><a href="#实现倒排索引" class="headerlink" title="实现倒排索引"></a>实现倒排索引</h3><p>同样,在理解了倒排索引的基础上设计我们自己的 map 与 reduce 方法,<br>1,map:将读取文档,将文档中的单词作为 key,单词所在的文档作为 value,写入到中间文件中。<br>2,reduce:读取中间文件,按照键值对进行排序,将 key 相同的数据聚合到一起,将单词出现的文件名拼接在一起,写入到结果文件中。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">"bytes"</span></span><br><span class="line"><span class="string">"os"</span></span><br><span class="line"><span class="string">"strconv"</span></span><br><span class="line"><span class="string">"strings"</span></span><br><span class="line"><span class="string">"unicode"</span></span><br><span class="line">)</span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">"6.824/src/mapreduce"</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">mapF</span><span class="params">(document <span class="keyword">string</span>, value <span class="keyword">string</span>)</span> <span class="params">(res []mapreduce.KeyValue)</span></span> {</span><br><span class="line"><span class="comment">// Your code here (Part V).</span></span><br><span class="line">words := strings.FieldsFunc(value, <span class="function"><span class="keyword">func</span><span class="params">(c <span class="keyword">rune</span>)</span> <span class="title">bool</span></span> {</span><br><span class="line"><span class="keyword">return</span> !unicode.IsLetter(c)</span><br><span class="line">})</span><br><span class="line"><span class="keyword">for</span> _, w := <span class="keyword">range</span> words {</span><br><span class="line">res = <span class="built_in">append</span>(res, mapreduce.KeyValue{Key: w, Value: document})</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> res</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">reduceF</span><span class="params">(key <span class="keyword">string</span>, values []<span class="keyword">string</span>)</span> <span class="title">string</span></span> {</span><br><span class="line"><span class="comment">// Your code here (Part V).</span></span><br><span class="line">sum := <span class="number">0</span></span><br><span class="line"><span class="keyword">var</span> buffer bytes.Buffer</span><br><span class="line"><span class="keyword">if</span> key == <span class="string">"www"</span> {</span><br><span class="line">fmt.Println(values)</span><br><span class="line">}</span><br><span class="line">isExist := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>)</span><br><span class="line"><span class="keyword">for</span> _, e := <span class="keyword">range</span> values {</span><br><span class="line"><span class="keyword">if</span> _, ok := isExist[e]; !ok {</span><br><span class="line">buffer.WriteString(e)</span><br><span class="line">buffer.WriteString(<span class="string">","</span>)</span><br><span class="line">sum += <span class="number">1</span></span><br><span class="line">isExist[e] = e</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">iiRes := strconv.Itoa(sum) + <span class="string">" "</span> + strings.TrimRight(buffer.String(), <span class="string">","</span>)</span><br><span class="line"><span class="keyword">return</span> iiRes</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(os.Args) < <span class="number">4</span> {</span><br><span class="line">fmt.Printf(<span class="string">"%s: see usage comments in file\n"</span>, os.Args[<span class="number">0</span>])</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"><span class="keyword">var</span> mr *mapreduce.Master</span><br><span class="line">mr = mapreduce.Sequential(<span class="string">"iiseq"</span>, os.Args[<span class="number">3</span>:], <span class="number">3</span>, mapF, reduceF)</span><br><span class="line">mr.Wait()</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>本文参考 Google 的论文,实现了一个单机版的 MapReduce 框架,并实现了两个简单的 MapReduce 实例,文中的代码可以在楼主的 <a href="https://www.hchstudio.cn/">GitHub</a> 下载查看,如果你也在刷 MIT 6.824 课程,欢迎留言交流~。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://pdos.csail.mit.edu/6.824/papers/mapreduce.pdf" target="_blank" rel="noopener">MapReduce: Simplified Data Processing on Large Clusters</a></p>]]></content>
<summary type="html">
最近有幸拜读 Google 分布式的三大论文,本着好记性不如烂笔头的原则,谈谈楼主对分布式系统开发的一点小小的心得~
</summary>
<category term="MapReduce" scheme="https://www.hchstudio.cn/categories/MapReduce/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Java" scheme="https://www.hchstudio.cn/tags/Java/"/>
</entry>
<entry>
<title>使用 Map 实现策略模式</title>
<link href="https://www.hchstudio.cn/article/2020/4a72/"/>
<id>https://www.hchstudio.cn/article/2020/4a72/</id>
<published>2020-01-16T00:35:52.000Z</published>
<updated>2021-07-21T11:42:18.907Z</updated>
<content type="html"><![CDATA[<p>上篇文章在谈到优化代码的时候,有一部分涉及到了使用策略模式优化我们的代码,本篇文章将围绕策略模式谈谈自己的思考~</p><h2 id="What"><a href="#What" class="headerlink" title="What?"></a>What?</h2><p>总的来说,设计模式是对软件设计中普遍存在并且反复出现的各种问题,所提出的通用解决方案,是一系列编码经验的集合。</p><p>那么什么是策略模式呢?它是定义一个算法的系列,将其各个分装,并且使他们有交互性。策略模式使得算法在用户使用的时候能独立的改变。如下图所示<br><img src="https://img.hchstudio.cn/sequence-data.png" alt="策略模式时区图"></p><h2 id="Why"><a href="#Why" class="headerlink" title="Why ?"></a>Why ?</h2><ul><li>完成一项任务,往往可以有多种不同的方式,每一种方式称为一个策略,我们可以根据环境或者条件的不同选择不同的策略来完成该项任务。</li><li>在软件开发中也常常遇到类似的情况,实现某一个功能有多个途径,此时可以使用一种设计模式来使得系统可以灵活地选择解决途径,也能够方便地增加新的解决途径。</li><li>在软件系统中,有许多算法可以实现某一功能,如查找、排序等,一种常用的方法是硬编码(Hard Coding)在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法;当然也可以将这些查找算法封装在一个统一的方法中,通过if…else…等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,需要修改封装算法类的源代码;更换查找算法,也需要修改客户端调用代码。在这个算法类中封装了大量查找算法,该类代码将较复杂,维护较为困难。</li><li>除了提供专门的查找算法类之外,还可以在客户端程序中直接包含算法代码,这种做法更不可取,将导致客户端程序庞大而且难以维护,如果存在大量可供选择的算法时问题将变得更加严重。</li><li>为了解决这些问题,可以定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法,在这里,每一个封装算法的类我们都可以称之为策略(Strategy),为了保证这些策略的一致。</li></ul><h2 id="How"><a href="#How" class="headerlink" title="How ?"></a>How ?</h2><p>在软件编码中,实现策略模式需要我们定义各种策略类,但是在 go 中我们可以使用 map 来避免这一缺点,直接定义需要实现的策略方法即可。</p><p>一般情况下我们都是这样做,采用 <code>if/else</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"><span class="keyword">if</span> condition :</span><br><span class="line"> <span class="comment">// doSomething</span></span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> condition:</span><br><span class="line"> <span class="comment">// doOtherthing</span></span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span></span><br><span class="line"> <span class="comment">// doAnOtherThing</span></span><br><span class="line"><span class="keyword">else</span> </span><br><span class="line"> <span class="comment">// doAlotOfthing</span></span><br></pre></td></tr></table></figure><h3 id="golang实现的”策略模式“"><a href="#golang实现的”策略模式“" class="headerlink" title="golang实现的”策略模式“"></a>golang实现的”策略模式“</h3><p>策略模式的精髓是封装一组算法实现以供使用时的调度,<code>golang</code> 里面有一个很重要的语法糖就是 <code>func()</code> 方法变量,因此,在 <code>golang</code> 中实现类似策略模式的做法,不需要依赖于对象而进行,比如<br><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">"fmt"</span></span><br><span class="line">)</span><br><span class="line"><span class="keyword">var</span> Strategy <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="function"><span class="keyword">func</span><span class="params">(v ...<span class="keyword">interface</span>{})</span></span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">init</span><span class="params">()</span></span>{</span><br><span class="line"> Strategy := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="keyword">string</span>]<span class="function"><span class="keyword">func</span><span class="params">(v ...<span class="keyword">interface</span>{})</span>)</span></span><br><span class="line">Strategy[<span class="string">"add"</span>] = <span class="function"><span class="keyword">func</span><span class="params">(a <span class="keyword">int</span>,b <span class="keyword">int</span>)</span></span> {</span><br><span class="line">fmt.Println(<span class="string">"a + b"</span>)</span><br><span class="line">}</span><br><span class="line">Strategy[<span class="string">"reduce"</span>] = <span class="function"><span class="keyword">func</span><span class="params">(a <span class="keyword">int</span>,b <span class="keyword">int</span>)</span></span> {</span><br><span class="line">fmt.Println(<span class="string">"a - b"</span>)</span><br><span class="line">}</span><br><span class="line">Strategy[<span class="string">"multiply"</span>] = <span class="function"><span class="keyword">func</span><span class="params">(a <span class="keyword">int</span>,b <span class="keyword">int</span>)</span></span> {</span><br><span class="line">fmt.Println(<span class="string">"a*b"</span>)</span><br><span class="line">}</span><br><span class="line">Strategy[<span class="string">"divide"</span>] = <span class="function"><span class="keyword">func</span><span class="params">(a <span class="keyword">int</span>,b <span class="keyword">int</span>)</span></span> {</span><br><span class="line">fmt.Println(<span class="string">"a/b"</span>)</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>{</span><br><span class="line">Strategy[<span class="string">"insert"</span>]()</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><h3 id="使用-map-来实现策略模式的优点"><a href="#使用-map-来实现策略模式的优点" class="headerlink" title="使用 map 来实现策略模式的优点"></a>使用 map 来实现策略模式的优点</h3><p>策略模式的核心是封装一组算法实现特别是相似的算法实现,所以我们可以通过 map 来进行 KV 的约束,key 是客户端传进来的对应策略,用具体的算法实现 fun() 作为 value,这样无论是算法的封装还是调度都从业务场景中解耦了。</p><h3 id="使用-map-来实现策略模式的缺点"><a href="#使用-map-来实现策略模式的缺点" class="headerlink" title="使用 map 来实现策略模式的缺点"></a>使用 map 来实现策略模式的缺点</h3><p>当然,缺点就是如果需要扩展策略,就要到增加一个 Entry<K,V>,没有传统的实现方式中直接扩展一个实现了策略接口的对象那么方便,这两个还得看具体的项目取舍,一句老话,没有好坏,只有合适不合适,好的软件实现都是考虑到各种情况的折中。</p>]]></content>
<summary type="html">
上篇文章在谈到优化代码的时候,有一部分涉及到了使用策略模式优化我们的代码,本篇文章将围绕策略模式谈谈自己的思考~
</summary>
<category term="Go" scheme="https://www.hchstudio.cn/categories/Go/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
</entry>
<entry>
<title>使用 Go 优化我们的接口</title>
<link href="https://www.hchstudio.cn/article/2019/41d1/"/>
<id>https://www.hchstudio.cn/article/2019/41d1/</id>
<published>2019-12-23T00:35:52.000Z</published>
<updated>2021-07-21T11:42:18.906Z</updated>
<content type="html"><![CDATA[<p>标题起的是有点大,不过还好本片文章主要也是使用 Go 来优化 HTTP 服务的,也算打个擦边球吧~</p><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>特征数据暴增,导致获取一个城市下所有的特征的接口延时高,下面是监控上看到的接口响应耗时,最慢的时候接口响应时间能达到 5s 多。<br><img src="https://img.hchstudio.cn/cost_time.png" alt="cost-time"></p><h2 id="缓存优化方案"><a href="#缓存优化方案" class="headerlink" title="缓存优化方案"></a>缓存优化方案</h2><p>代码优化思路:</p><p>1,使用缓存</p><p>1.1为什么使用内存,而不是 Redis?</p><p>分析业务需求,当前需要存储起来的数据是ObjectId,ObjectId 是一个长度为14左右的字符串,我们假设平均下来ObjectId是长度为16的字符串,这样算下来就是每个ObjectId占用的内存大小是2个字节,当前业务需要存储的ObjectId大概是30万条,这样算下来当前业务需要存储的ObjectId要占用的内存在0.5M完全可以在内存中进行操作。相比于使用 Redis 来说没有网络开销,效率更高。</p><p>1.2,缓存初始化:当服务启动时,本地缓存初始化为空。</p><p>1.3 关于缓存版本的概念</p><p>缓存版本是离线特征生产任务更新后将数据版本更新到 fusion 中。</p><p>下面三种方案都是基于内存存储 ObjectId 数据,在内存更新的时候策略有所不同。</p><h3 id="方案一"><a href="#方案一" class="headerlink" title="方案一"></a>方案一</h3><p>2.1 缓存更新</p><p>使用主动更新缓存的方式,创建定时任务,每间隔1分钟查一次fusion的数据版本,若更新则更新缓存中的数据。</p><p>2.2 缺点</p><p>单独启动一个缓存更新线程,代码不好维护,也会有定时任务线程挂掉的情况,不易发现。还有就是需要提前把相关参数配置到代码中或者引入配置中心,维护成本较高。</p><h3 id="方案二"><a href="#方案二" class="headerlink" title="方案二"></a>方案二</h3><p>3.1 缓存更新</p><p>采用被动触发的缓存更新策略,由接口调用触发。请求进来后检测当前缓存中的数据的版本与fusion中的数据版本是否一致,若版本更新,则重新读取当前请求对应城市的所有feature数据到缓存中,并将更新后的数据返回给调用方。</p><p>3.2 缺点</p><p>由于是被动触发的是同步更新缓存的,容易造成接口调用时如果正好遇上版本更新,需要更新数据到内存中,会出现偶现的毛刺。</p><p>3.3 业务执行时序图<br><img src="https://img.hchstudio.cn/method-2.png" alt="方案二时序图"></p><h3 id="方案三(最终采用的方案)"><a href="#方案三(最终采用的方案)" class="headerlink" title="方案三(最终采用的方案)"></a>方案三(最终采用的方案)</h3><p>4.1,缓存更新</p><p>采用被动更新缓存的策略,由接口调用方触发。若当前缓存中有数据则直接返回缓存中的数据,然后检测当前缓存中的数据的版本与fusion中的数据版本是否一致,若版本更新,则重新读取当前请求对应城市的所有feature数据到缓存中,反之结束缓存更新逻辑。</p><p>4.2 业务执行时序图<br><img src="https://img.hchstudio.cn/method-3.png" alt="方案三时序图"></p><h2 id="并发优化方案"><a href="#并发优化方案" class="headerlink" title="并发优化方案"></a>并发优化方案</h2><h3 id="使用-Goroutine-来优化我们的串行逻辑"><a href="#使用-Goroutine-来优化我们的串行逻辑" class="headerlink" title="使用 Goroutine 来优化我们的串行逻辑"></a>使用 Goroutine 来优化我们的串行逻辑</h3><p>Go语言最大的特色就是从语言层面支持并发(Goroutine),Goroutine是Go中最基本的执行单元。事实上每一个Go程序至少有一个Goroutine:主Goroutine。当程序启动时,它会自动创建。</p><p>为了更好理解Goroutine,现讲一下线程和协程的概念</p><p>线程(Thread):有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。</p><p>线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程的切换一般也由操作系统调度。</p><p>协程(coroutine):又称微线程与子例程(或者称为函数)一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。</p><p>和线程类似,共享堆,不共享栈,协程的切换一般由程序员在代码中显式控制。它避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。</p><h3 id="golang-中的-map-是线程不安全的"><a href="#golang-中的-map-是线程不安全的" class="headerlink" title="golang 中的 map 是线程不安全的"></a>golang 中的 map 是线程不安全的</h3><p>很显然,我们可以用锁机制解决 Map 的并发读写问题。我们将map结构改成如下:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// M</span></span><br><span class="line"><span class="keyword">type</span> M <span class="keyword">struct</span> {</span><br><span class="line"> Map <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span></span><br><span class="line"> lock sync.RWMutex <span class="comment">// 加锁</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Set ...</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(m *M)</span> <span class="title">Set</span><span class="params">(key, value <span class="keyword">string</span>)</span></span> {</span><br><span class="line"> m.lock.Lock()</span><br><span class="line"> <span class="keyword">defer</span> m.lock.Unlock()</span><br><span class="line"> m.Map[key] = value</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Get ...</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(m *M)</span> <span class="title">Get</span><span class="params">(key <span class="keyword">string</span>)</span> <span class="title">string</span></span> {</span><br><span class="line"> m.lock.RLock()</span><br><span class="line"> <span class="keyword">defer</span> m.lock.RUnlock()</span><br><span class="line"> <span class="keyword">return</span> m.Map[key]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在上面的代码中,我们引入了锁机制操作,从而保证了map在多个goroutine中的安全。</p><h2 id="使用策略模式优化我们的逻辑"><a href="#使用策略模式优化我们的逻辑" class="headerlink" title="使用策略模式优化我们的逻辑"></a>使用策略模式优化我们的逻辑</h2><p>这块主要是因为代码中存在太多的 if/else ,故采用策略模式来优化我们的代码结构。这里先放上一篇网上找到的<a href="https://www.hchstudio.cn/article/2020/4a72/">文章</a>,之后有时间再单独出一篇相关文章吧。优化后的代码相较于之前代码量少了 50% ,更加清晰与便于维护。下面是优化的代码上线后的效果,请求耗时都在100ms以下:<br><img src="https://img.hchstudio.cn/youhua-2.png" alt="监控接口耗时"></p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>上面整体介绍了下当我们的接口耗时较长的时候的一般处理方案,当然具体问题还得具体分析,所以当出现接口反应慢的情况的时候,我们应该具体分析接口反应慢的具体原因,方可对症下药!</p>]]></content>
<summary type="html">
标题起的是有点大,不过还好本片文章主要也是使用 Go 来优化 HTTP 服务的,也算打个擦边球吧~
</summary>
<category term="Go" scheme="https://www.hchstudio.cn/categories/Go/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
</entry>
<entry>
<title>如何使用 Redis 实现分布式锁</title>
<link href="https://www.hchstudio.cn/article/2019/8064/"/>
<id>https://www.hchstudio.cn/article/2019/8064/</id>
<published>2019-12-22T13:15:51.000Z</published>
<updated>2021-07-21T11:42:18.906Z</updated>
<content type="html"><![CDATA[<p>锁是我们在设计和实现大多数系统时绕不过的话题。一旦有竞争条件出现,在没有保护的操作的前提下,可能会出现不可预知的问题。</p><p>而现代系统大多为分布式系统,这就引入了分布式锁,要求具有在分布各处的服务上保护资源的能力。</p><p>而实现分布式锁,目前大多有以下三种方式:</p><ul><li>使用数据库实现。</li><li>使用 Redis 等缓存系统实现。</li><li>使用 Zookeeper 等分布式协调系统实现。</li></ul><p>其中 Redis 简便灵活,高可用分布式,且支持持久化。本文即介绍基于 Redis 实现分布式锁。</p><h2 id="SETNX-语义"><a href="#SETNX-语义" class="headerlink" title="SETNX 语义"></a>SETNX 语义</h2><p>使用 Redis 实现分布式锁,根本原理是 SETNX 指令。其语义如下:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SETNX key value</span><br></pre></td></tr></table></figure><blockquote><p>命令执行时,如果 <code>key</code> 不存在,则设置 <code>key</code> 值为 <code>value</code>(同<code>set</code>);如果 <code>key</code> 已经存在,则不执行赋值操作。并使用不同的返回值标识。<a href="https://redis.io/commands/setnx" target="_blank" rel="noopener">命令描述文档</a></p></blockquote><p>还可以通过 SET 命令的 NX 选项使用:<br><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SET key value [expiration EX seconds|PX milliseconds] [NX|XX]</span><br></pre></td></tr></table></figure></p><blockquote><p>NX - 仅在 key 不存在时执行赋值操作。<a href="https://redis.io/commands/set" target="_blank" rel="noopener">命令描述文档</a><br>而如下文所述,通过SET的NX选项使用,可同时使用其它选项,如EX/PX设置超时时间,是更好的方式。</p></blockquote><h2 id="setnx实现分布式锁"><a href="#setnx实现分布式锁" class="headerlink" title="setnx实现分布式锁"></a>setnx实现分布式锁</h2><p>下面我们对比下几种具体实现方式。</p><h3 id="方案1:SETNX-delete"><a href="#方案1:SETNX-delete" class="headerlink" title="方案1:SETNX + delete"></a>方案1:SETNX + delete</h3><p>伪代码如下:</p><figure class="highlight bash"><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">setnx lock_a random_value</span><br><span class="line">// <span class="keyword">do</span> sth</span><br><span class="line">delete lock_a</span><br></pre></td></tr></table></figure><p>此实现方式的问题在于:一旦服务获取锁之后,因某种原因挂掉,则锁一直无法自动释放。从而导致死锁。</p><h3 id="方案2:SETNX-SETEX"><a href="#方案2:SETNX-SETEX" class="headerlink" title="方案2:SETNX + SETEX"></a>方案2:SETNX + SETEX</h3><p>伪代码如下:</p><figure class="highlight bash"><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">setnx lock_a random_value</span><br><span class="line">setex lock_a 10 random_value // 10s超时</span><br><span class="line">// <span class="keyword">do</span> sth</span><br><span class="line">delete lock_a</span><br></pre></td></tr></table></figure><p>按需设置超时时间。此方案解决了方案1死锁的问题,但同时引入了新的死锁问题:<br>如果setnx之后,setex 之前服务挂掉,会陷入死锁。<br>根本原因为 setnx/setex 分为了两个步骤,非原子操作。</p><h3 id="方案3:SET-NX-PX"><a href="#方案3:SET-NX-PX" class="headerlink" title="方案3:SET NX PX"></a>方案3:SET NX PX</h3><p>伪代码如下:</p><figure class="highlight bash"><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">SET lock_a random_value NX PX 10000 // 10s超时</span><br><span class="line">// <span class="keyword">do</span> sth</span><br><span class="line">delete lock_a</span><br></pre></td></tr></table></figure><p>此方案通过 set 的 NX/PX 选项,将加锁、设置超时两个步骤合并为一个原子操作,从而解决方案1、2的问题。(PX与EX选项的语义相同,差异仅在单位。)<br>此方案目前大多数 sdk、redis 部署方案都支持,因此是推荐使用的方式。<br>但此方案也有如下问题:</p><p>如果锁被错误的释放(如超时),或被错误的抢占,或因redis问题等导致锁丢失,无法很快的感知到。</p><h3 id="方案4:SET-key-randomvalue-NX-PX"><a href="#方案4:SET-key-randomvalue-NX-PX" class="headerlink" title="方案4:SET key randomvalue NX PX"></a>方案4:SET key randomvalue NX PX</h3><p>方案4在3的基础上,增加对 value 的检查,只解除自己加的锁。<br>类似于 CAS,不过是 compare-and-delete。<br>此方案 redis 原生命令不支持,为保证原子性,需要通过lua脚本实现:。</p><p>伪代码如下:</p><figure class="highlight bash"><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">SET lock_a random_value NX PX 10000</span><br><span class="line">// <span class="keyword">do</span> sth</span><br><span class="line"><span class="built_in">eval</span> <span class="string">"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"</span> 1 lock_a random_value</span><br></pre></td></tr></table></figure><p>此方案更严谨:即使因为某些异常导致锁被错误的抢占,也能部分保证锁的正确释放。并且在释放锁时能检测到锁是否被错误抢占、错误释放,从而进行特殊处理。</p><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><h3 id="超时时间"><a href="#超时时间" class="headerlink" title="超时时间"></a>超时时间</h3><p>从上述描述可看出,超时时间是一个比较重要的变量:</p><p>超时时间不能太短,否则在任务执行完成前就自动释放了锁,导致资源暴露在锁保护之外。<br>超时时间不能太长,否则会导致意外死锁后长时间的等待。除非人为接入处理。<br>因此建议是根据任务内容,合理衡量超时时间,将超时时间设置为任务内容的几倍即可。<br>如果实在无法确定而又要求比较严格,可以采用定期 setex/expire 更新超时时间实现。</p><h3 id="重试"><a href="#重试" class="headerlink" title="重试"></a>重试</h3><p>如果拿不到锁,建议根据任务性质、业务形式进行轮询等待。<br>等待次数需要参考任务执行时间。</p><h3 id="与redis事务的比较"><a href="#与redis事务的比较" class="headerlink" title="与redis事务的比较"></a>与redis事务的比较</h3><p>setnx 使用更为灵活方案。multi/exec 的事务实现形式更为复杂。<br>且部分redis集群方案(如codis),不支持multi/exec 事务。</p><h2 id="golang-demo"><a href="#golang-demo" class="headerlink" title="golang demo"></a>golang demo</h2><p>基于 redigo简单实例代码如下。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="string">"fmt"</span></span><br><span class="line"> <span class="string">"sync"</span></span><br><span class="line"> <span class="string">"time"</span></span><br><span class="line"></span><br><span class="line"> <span class="string">"github.com/garyburd/redigo/redis"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">getLock</span><span class="params">(redisAddr, lockKey <span class="keyword">string</span>, ex <span class="keyword">uint</span>, retry <span class="keyword">int</span>)</span> <span class="title">error</span></span> {</span><br><span class="line"> <span class="keyword">if</span> retry <= <span class="number">0</span> {</span><br><span class="line"> retry = <span class="number">10</span></span><br><span class="line"> }</span><br><span class="line"> conn, err := redis.DialTimeout(<span class="string">"tcp"</span>, redisAddr, time.Minute, time.Minute, time.Minute)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> fmt.Println(<span class="string">"conn to redis failed, err:%v"</span>, err)</span><br><span class="line"> <span class="keyword">return</span> err</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">defer</span> conn.Close()</span><br><span class="line"> ts := time.Now() <span class="comment">// as random value</span></span><br><span class="line"> <span class="keyword">for</span> i := <span class="number">1</span>; i <= retry; i++ {</span><br><span class="line"> <span class="keyword">if</span> i > <span class="number">1</span> { <span class="comment">// sleep if not first time</span></span><br><span class="line"> time.Sleep(time.Second)</span><br><span class="line"> }</span><br><span class="line"> v, err := conn.Do(<span class="string">"SET"</span>, lockKey, ts, <span class="string">"EX"</span>, retry, <span class="string">"NX"</span>)</span><br><span class="line"> <span class="keyword">if</span> err == <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">if</span> v == <span class="literal">nil</span> {</span><br><span class="line"> fmt.Println(<span class="string">"get lock failed, retry times:"</span>, i)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> fmt.Println(<span class="string">"get lock success"</span>)</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> fmt.Println(<span class="string">"get lock failed with err:"</span>, err)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> i >= retry {</span><br><span class="line"> err = fmt.Errorf(<span class="string">"get lock failed with max retry times."</span>)</span><br><span class="line"> <span class="keyword">return</span> err</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">unLock</span><span class="params">(redisAddr, lockKey <span class="keyword">string</span>)</span> <span class="title">error</span></span> {</span><br><span class="line"> conn, err := redis.DialTimeout(<span class="string">"tcp"</span>, redisAddr, time.Minute, time.Minute, time.Minute)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> fmt.Println(<span class="string">"conn to redis failed, err:%v"</span>, err)</span><br><span class="line"> <span class="keyword">return</span> err</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">defer</span> conn.Close()</span><br><span class="line"> v, err := redis.Bool(conn.Do(<span class="string">"DEL"</span>, lockKey))</span><br><span class="line"> <span class="keyword">if</span> err == <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">if</span> v {</span><br><span class="line"> fmt.Println(<span class="string">"unLock success"</span>)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> fmt.Println(<span class="string">"unLock failed"</span>)</span><br><span class="line"> <span class="keyword">return</span> fmt.Errorf(<span class="string">"unLock failed"</span>)</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> fmt.Println(<span class="string">"unLock failed, err:"</span>, err)</span><br><span class="line"> <span class="keyword">return</span> err</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line"> RedisAddr = <span class="string">"127.0.0.1:3000"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line"> key := <span class="string">"lock_demo"</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">5</span>; i++ {</span><br><span class="line"> wg.Add(<span class="number">1</span>)</span><br><span class="line"> <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(id <span class="keyword">int</span>)</span></span> {</span><br><span class="line"> <span class="keyword">defer</span> wg.Done()</span><br><span class="line"> time.Sleep(time.Second)</span><br><span class="line"> <span class="comment">// getLock</span></span><br><span class="line"> err := getLock(RedisAddr, key, <span class="number">10</span>, <span class="number">10</span>)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> fmt.Println(fmt.Sprintf(<span class="string">"worker[%d] get lock failed:%v"</span>, id, err))</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// sleep for random</span></span><br><span class="line"> <span class="keyword">for</span> j := <span class="number">0</span>; j < <span class="number">5</span>; j++ {</span><br><span class="line"> time.Sleep(time.Second)</span><br><span class="line"> fmt.Println(fmt.Sprintf(<span class="string">"worker[%d] hold lock for %ds"</span>, id, j+<span class="number">1</span>))</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// unLock</span></span><br><span class="line"> err = unLock(RedisAddr, key)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> fmt.Println(fmt.Sprintf(<span class="string">"worker[%d] unlock failed:%v"</span>, id, err))</span><br><span class="line"> }</span><br><span class="line"> fmt.Println(fmt.Sprintf(<span class="string">"worker[%d] done"</span>, id))</span><br><span class="line"> }(i)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> wg.Wait()</span><br><span class="line"> fmt.Println(<span class="string">"demo is done!"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">
锁是我们在设计和实现大多数系统时绕不过的话题。一旦有竞争条件出现,在没有保护的操作的前提下,可能会出现不可预知的问题。
</summary>
<category term="Java" scheme="https://www.hchstudio.cn/categories/Java/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Java" scheme="https://www.hchstudio.cn/tags/Java/"/>
</entry>
<entry>
<title>Kafka Consumer 的 Rebalance 机制</title>
<link href="https://www.hchstudio.cn/article/2019/db6d/"/>
<id>https://www.hchstudio.cn/article/2019/db6d/</id>
<published>2019-11-19T13:15:51.000Z</published>
<updated>2021-07-21T11:42:18.904Z</updated>
<content type="html"><![CDATA[<h2 id="Kafka-之前版本的-Consumer-Groups"><a href="#Kafka-之前版本的-Consumer-Groups" class="headerlink" title="Kafka 之前版本的 Consumer Groups"></a>Kafka 之前版本的 Consumer Groups</h2><h3 id="Consumer-Group"><a href="#Consumer-Group" class="headerlink" title="Consumer Group"></a>Consumer Group</h3><p><img src="http://img.hchstudio.cn/kafka-Consumer-Group.png" alt></p><p>如上图所示,<code>Consumer</code> 使用 <code>Consumer Group</code> 名称标记自己,并且发布到主题的每条记录都会传递到每个订阅消费者组中的一个 <code>Consumer</code> 实例。 <code>Consumer</code> 实例可以在单独的进程中或在单独的机器上。</p><p>如果所有 <code>Consumer</code> 实例都属于同一个 <code>Consumer Group</code> ,那么这些 <code>Consumer</code> 实例将平衡再负载的方式来消费 <code>Kafka</code>。</p><p>如果所有 <code>Consumer</code> 实例具有不同的 <code>Consumer Group</code>,则每条记录将广播到所有 <code>Consumer</code> 进程。</p><h3 id="Group-Coordinator"><a href="#Group-Coordinator" class="headerlink" title="Group Coordinator"></a>Group Coordinator</h3><p><code>Group Coordinator</code> 是一个服务,每个 <code>Broker</code>在启动的时候都会启动一个该服务。<code>Group Coordinator</code> 的作用是用来存储 <code>Group</code> 的相关 <code>Meta</code> 信息,并将对应 <code>Partition</code> 的 <code>Offset</code> 信息记录到 <code>Kafka</code> 内置<code>Topic(__consumer_offsets)</code> 中。<code>Kafka</code> 在 0.9 之前是基于 <code>Zookeeper</code> 来存储 <code>Partition</code> 的 <code>Offset</code> 信息 <code>(consumers/{group}/offsets/{topic}/{partition})</code>,因为 <code>Zookeeper</code> 并不适用于频繁的写操作,所以在 0.9 之后通过内置 <code>Topic</code> 的方式来记录对应 <code>Partition</code> 的 <code>Offset</code>。如下图所示:</p><p>在 <code>Kafka 0.8.2</code> 之前是这样的<br><img src="http://img.hchstudio.cn/kafka-zk.png" alt></p><p>之后是这样的:<br><img src="http://img.hchstudio.cn/kafka-coordinator.png" alt></p><p>每个 <code>Group</code> 都会选择一个 <code>Coordinator</code> 来完成自己组内各 <code>Partition</code> 的 <code>Offset</code> 信息,选择的规则如下:</p><ol><li>计算 <code>Group</code> 对应在 <code>__consumer_offsets</code> 上的 <code>Partition</code></li><li>根据对应的Partition寻找该Partition的leader所对应的Broker,该Broker上的Group Coordinator即就是该Group的Coordinator</li></ol><p>Partition计算规则:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">partition-Id(__consumer_offsets) = Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount)</span><br></pre></td></tr></table></figure></p><p>其中 <code>groupMetadataTopicPartitionCount</code> 对应 <code>offsets.topic.num.partitions</code> 参数值,默认值是 50 个分区</p><h2 id="Consumer-Rebalance-Protocol"><a href="#Consumer-Rebalance-Protocol" class="headerlink" title="Consumer Rebalance Protocol"></a>Consumer Rebalance Protocol</h2><h3 id="发生-rebalance-的时机"><a href="#发生-rebalance-的时机" class="headerlink" title="发生 rebalance 的时机"></a>发生 rebalance 的时机</h3><ol><li>组成员个数发生变化。例如有新的 <code>consumer</code> 实例加入该消费组或者离开组。</li><li>订阅的 <code>Topic</code> 个数发生变化。</li><li>订阅 <code>Topic</code> 的分区数发生变化。</li></ol><h3 id="消费者进程挂掉的情况"><a href="#消费者进程挂掉的情况" class="headerlink" title="消费者进程挂掉的情况"></a>消费者进程挂掉的情况</h3><ol><li><code>session</code> 过期</li><li><code>heartbeat</code> 过期</li></ol><p><code>Rebalance</code> 发生时,<code>Group</code> 下所有 <code>Consumer</code> 实例都会协调在一起共同参与,<code>Kafka</code> 能够保证尽量达到最公平的分配。但是 <code>Rebalance</code> 过程对 <code>Consumer Group</code> 会造成比较严重的影响。在 <code>Rebalance</code> 的过程中 <code>Consumer Group</code> 下的所有消费者实例都会停止工作,等待 <code>Rebalance</code> 过程完成。</p><h2 id="消费者的-Rebalance-协议"><a href="#消费者的-Rebalance-协议" class="headerlink" title="消费者的 Rebalance 协议"></a>消费者的 Rebalance 协议</h2><h3 id="Rebalance-发生后的执行过程"><a href="#Rebalance-发生后的执行过程" class="headerlink" title="Rebalance 发生后的执行过程"></a>Rebalance 发生后的执行过程</h3><p>1,有新的 <code>Consumer</code> 加入 <code>Consumer Group</code></p><p>2,从 <code>Consumer Group</code> 选出 <code>leader</code></p><p>3,<code>leader</code> 进行分区的分配</p><h3 id="Issues"><a href="#Issues" class="headerlink" title="Issues"></a>Issues</h3><p>Known Issue #1: Stop-the-world Rebalance<br><img src="http://img.hchstudio.cn/kafka-issue1.png" alt><br>如上图所示:之前版本的 <code>Kafka</code> 在发生 <code>Rebalance</code> 时候会释放 <code>Consumer Group</code> 的所有资源,造成比较长的 <code>Stop-the-world</code> </p><p>Known Issue #2: Back-and-forth Rebalance<br><img src="http://img.hchstudio.cn/kafka-issue2.png" alt><br>如上图所示:在发生 <code>Rebalance</code> 的时候发生的不必要的资源释放与重新分配。</p><h2 id="当前的-Rebalance-与-改进后的-ReBalance-对比"><a href="#当前的-Rebalance-与-改进后的-ReBalance-对比" class="headerlink" title="当前的 Rebalance 与 改进后的 ReBalance 对比"></a>当前的 Rebalance 与 改进后的 ReBalance 对比</h2><p><img src="https://img.hchstudio.cn/kafka-rebalance.png" alt></p><h2 id="渐进式-Rebalance-协议"><a href="#渐进式-Rebalance-协议" class="headerlink" title="渐进式 Rebalance 协议"></a>渐进式 Rebalance 协议</h2><p><img src="http://img.hchstudio.cn/kafka-cooperative.png" alt><br>如上图所示,新的渐进式 Rebalance 协议,在 Rebalance 的时候不需要当前所有的 Consumer 释放所拥有的资源,而是当需要触发 Rebalance 的时候对当前资源进行登记,然后进行渐进式的 Rebalance。<br>这样做产生的优化效果</p><ul><li>相较之前进行了更多次数的 Rebalance,但是每次 Rebalance 对资源的消耗都是比较廉价的</li><li>发生迁移的分区相较之前更少了 </li><li>Consumer 在 Rebalance 期间可以继续运行</li></ul><h2 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h2><ul><li><a href="https://www.confluent.io/blog/incremental-cooperative-rebalancing-in-kafka" target="_blank" rel="noopener">Incremental Cooperative Rebalancing in Apache Kafka: Why Stop the World When You Can Change It?</a></li><li><a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-429%3A+Kafka+Consumer+Incremental+Rebalance+Protocol" target="_blank" rel="noopener">KIP-429: Kafka Consumer Incremental Rebalance Protocol</a></li><li><a href="https://cwiki.apache.org/confluence/display/KAFKA/Incremental+Cooperative+Rebalancing%3A+Support+and+Policies" target="_blank" rel="noopener">Incremental Cooperative Rebalancing: Support and Policies</a></li></ul>]]></content>
<summary type="html">
上周参加了 Kafka Meetup 北京站的技术分享,本文简单介绍下 Kafka Consumer 的 Rebalance 机制以及其新版本中的优化策略~
</summary>
<category term="Kafka,Java" scheme="https://www.hchstudio.cn/categories/Kafka-Java/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Java" scheme="https://www.hchstudio.cn/tags/Java/"/>
</entry>
<entry>
<title>实时数据并发写入 Redis 优化</title>
<link href="https://www.hchstudio.cn/article/2019/a5e3/"/>
<id>https://www.hchstudio.cn/article/2019/a5e3/</id>
<published>2019-11-10T00:35:52.000Z</published>
<updated>2021-07-21T11:42:18.907Z</updated>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>当前架构的逻辑是将并发请求数据写入队列中,然后起一个单独的异步线程对数据进行串行处理。这种方式的好处就是不用考虑并发的问题,当然其弊端也是显而易见的~</p><h2 id="乐观锁实现数据的并发更新"><a href="#乐观锁实现数据的并发更新" class="headerlink" title="乐观锁实现数据的并发更新"></a>乐观锁实现数据的并发更新</h2><p>根据当前业务的数据更新在秒级,<code>key</code> 的碰撞率较低的情况。笔者打算采用使用 <code>CAS</code> 乐观锁方案:使用 <code>Lua</code> 脚本实现 <code>Redis</code> 对数据的原子更新,即便是在并发的情况下其性能也会上一个级别。下面是 CAS 乐观锁实现数据并发更新的流程图:<br><img src="https://img.hchstudio.cn/cas.png" alt="CAS"></p><p>根据上面的流程图设计出了 <code>Lua</code> 脚本:<br><figure class="highlight lua"><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"><span class="keyword">local</span> keys,values=KEYS,ARGV</span><br><span class="line"><span class="keyword">local</span> version = redis.call(<span class="string">'get'</span>,keys[<span class="number">1</span>]) </span><br><span class="line"><span class="keyword">if</span> values[<span class="number">1</span>] == <span class="string">''</span> <span class="keyword">and</span> version == <span class="literal">false</span></span><br><span class="line"><span class="keyword">then</span></span><br><span class="line">redis.call(<span class="string">'SET'</span>,keys[<span class="number">1</span>],<span class="string">'1'</span>)</span><br><span class="line">redis.call(<span class="string">'SET'</span>,keys[<span class="number">2</span>],values[<span class="number">2</span>])</span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> version == values[<span class="number">1</span>]</span><br><span class="line"><span class="keyword">then</span></span><br><span class="line">redis.call(<span class="string">'SET'</span>,keys[<span class="number">2</span>],values[<span class="number">2</span>])</span><br><span class="line">redis.call(<span class="string">'INCR'</span>,keys[<span class="number">1</span>])</span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span></span><br><span class="line"><span class="keyword">else</span></span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure></p><h2 id="可能存在问题及其解决方案"><a href="#可能存在问题及其解决方案" class="headerlink" title="可能存在问题及其解决方案"></a>可能存在问题及其解决方案</h2><p>1,在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。如:<br><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">10</span>; i++ {</span><br><span class="line"> isRetry := execLuaScript()</span><br><span class="line"> <span class="keyword">if</span> !isRetry {</span><br><span class="line"> <span class="keyword">break</span> </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 class="function"><span class="keyword">func</span> <span class="title">execLuaScript</span><span class="params">()</span> <span class="title">bool</span></span> {</span><br><span class="line"> ctx := context.Background()</span><br><span class="line">r := client.GetRedisKVClient(ctx)</span><br><span class="line"><span class="keyword">defer</span> r.Close()</span><br><span class="line"></span><br><span class="line">luaScript := <span class="string">`</span></span><br><span class="line"><span class="string">local keys,values=KEYS,ARGV</span></span><br><span class="line"><span class="string">local version = redis.call('get',keys[1]) </span></span><br><span class="line"><span class="string">if values[1] == '' and version == false</span></span><br><span class="line"><span class="string">then</span></span><br><span class="line"><span class="string">redis.call('SET',keys[1],'1')</span></span><br><span class="line"><span class="string">redis.call('SET',keys[2],values[2])</span></span><br><span class="line"><span class="string">return 1</span></span><br><span class="line"><span class="string">end</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">if version == values[1]</span></span><br><span class="line"><span class="string">then</span></span><br><span class="line"><span class="string">redis.call('SET',keys[2],values[2])</span></span><br><span class="line"><span class="string">redis.call('INCR',keys[1])</span></span><br><span class="line"><span class="string">return 1</span></span><br><span class="line"><span class="string">else</span></span><br><span class="line"><span class="string">return 0</span></span><br><span class="line"><span class="string">end`</span></span><br><span class="line"></span><br><span class="line">casVersion, err := r.Get(<span class="string">"test_version"</span>)</span><br><span class="line"></span><br><span class="line">kvs := <span class="built_in">make</span>([]redis.KeyAndValue, <span class="number">0</span>)</span><br><span class="line">kvs = <span class="built_in">append</span>(kvs, redis.KeyAndValue{<span class="string">"test_version"</span>, casVersion.String()})</span><br><span class="line">kvs = <span class="built_in">append</span>(kvs, redis.KeyAndValue{<span class="string">"test"</span>, <span class="string">"123123123"</span>})</span><br><span class="line">mv, err := r.Eval(luaScript, kvs...)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">log.Errorf(<span class="string">"%v"</span>, err)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">val, _ := mv.Int64()</span><br><span class="line">log.Debugf(<span class="string">">>>>>> lua 脚本运行结果 :%d"</span>, val)</span><br><span class="line"> <span class="keyword">if</span> val == <span class="number">1</span> {</span><br><span class="line"> <span class="comment">// lua 脚本执行成功,无需重试 </span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> val == <span class="number">0</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>2,<code>Lua</code> 脚本执行时只能在同一台机器上生效,因此在 <code>Redis</code> 集群在就要求相关联的 <code>key</code> 分配到相同机器。这里很多同学可能会问为什么,其实很简单,<code>Redis</code> 是单线程的,倘若 <code>Lua</code> 脚本操作的 <code>key</code> 在不同机器上执行,也就无法保证其执行的原子性了。</p><p>解决方法还是从分片技术的原理上找:<br>数据分片,就是一个 <code>hash</code> 的过程:对 <code>key</code> 做 <code>md5</code>,<code>sha1</code> 等 <code>hash</code> 算法,根据 <code>hash</code> 值分配到不同的机器上。</p><p>为了实现将key分到相同机器,就需要相同的 <code>hash</code> 值,即相同的 <code>key</code>(改变 <code>hash</code> 算法也行,但比较复杂)。但 <code>key</code> 相同是不现实的,因为 <code>key</code> 都有不同的用途。但是我们让 <code>key</code> 的一部分相同对我们业务实现来说是可以实现的。那么能不能拿 <code>key</code> 一部分来计算 <code>hash</code> 呢?答案是肯定的,</p><p>这就是 <a href="https://github.com/twitter/twemproxy/blob/master/notes/recommendation.md#hash-tags" target="_blank" rel="noopener">Hash Tag</a> 。允许用key的部分字符串来计算hash。当一个key包含 {} 的时候,就不对整个key做hash,而仅对 {} 包括的字符串做 hash。假设 hash 算法为sha1。对 user:{user1}:ids和user:{user1}:tweets ,其 hash 值都等同于 sha1(user1)。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>对于上面的优化过程,目前代码重构开发工作已经完成,但是还未正式上线,等上线之后再来补一下优化之后性能的提升情况~</p>]]></content>
<summary type="html">
对于并发请求如何在不强制加锁的情况下快速更新呢?这里有热腾腾的线上实践……
</summary>
<category term="Java" scheme="https://www.hchstudio.cn/categories/Java/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
</entry>
<entry>
<title>Redis 与 Lua 使用中的小问题</title>
<link href="https://www.hchstudio.cn/article/2019/88af/"/>
<id>https://www.hchstudio.cn/article/2019/88af/</id>
<published>2019-11-05T13:15:51.000Z</published>
<updated>2021-07-21T11:42:18.905Z</updated>
<content type="html"><![CDATA[<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>在 Redis 里执行 <code>get</code> 或 <code>hget</code> 不存在的 <code>key</code> 或 <code>field</code> 时返回值在终端显式的是 <code>(nil)</code>,类似于下面这样<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> get test_version</span><br><span class="line">(nil)</span><br></pre></td></tr></table></figure></p><p>如果在 Lua 脚本中判断获取到的值是否为空值时,就会产生比较迷惑的问题,以为判断空值的话就用 <code>nil</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">127.0.0.1:6379> get test_version</span><br><span class="line">(nil)</span><br><span class="line">127.0.0.1:6379> EVAL "local a = redis.call('get',KEYS[1]) print(a) if a == 'nil' then return 1 else return 0 end" 1 test_version test_version</span><br><span class="line">(integer) 0</span><br></pre></td></tr></table></figure><p>我们来看下执行 Lua 脚本返回结果的数据类型是什么<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> get test_version</span><br><span class="line">(nil)</span><br><span class="line">127.0.0.1:6379> EVAL "local a = redis.call('get',KEYS[1]) return type(a)" 1 test_version test_version</span><br><span class="line">"boolean"</span><br></pre></td></tr></table></figure></p><p>通过上面的脚本可以看到,当 Redis 返回的结果为 <code>(nil)</code> 时候,其真实的数据类型为 <code>boolean</code>,因此我们直接判断 <code>nil</code> 是有问题的。</p><h2 id="Redis-官方文档"><a href="#Redis-官方文档" class="headerlink" title="Redis 官方文档"></a>Redis 官方文档</h2><p>通过翻阅<a href="https://redis.io/commands/eval" target="_blank" rel="noopener">官方文档</a>,找到下面所示的一段话,</p><p><strong>Redis to Lua</strong> conversion table.</p><ul><li>Redis integer reply -> Lua number</li><li>Redis bulk reply -> Lua string</li><li>Redis multi bulk reply -> Lua table (may have other Redis data types nested)</li><li>Redis status reply -> Lua table with a single ok field containing the status</li><li>Redis error reply -> Lua table with a single err field containing the error</li><li>Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type</li></ul><p><strong>Lua to Redis</strong> conversion table.</p><ul><li>Lua number -> Redis integer reply (the number is converted into an integer)</li><li>Lua string -> Redis bulk reply</li><li>Lua table (array) -> Redis multi bulk reply (truncated to the first nil inside the Lua array if any)</li><li>Lua table with a single ok field -> Redis status reply</li><li>Lua table with a single err field -> Redis error reply</li><li>Lua boolean false -> Redis Nil bulk reply.</li></ul><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>通过<a href="https://redis.io/commands/eval" target="_blank" rel="noopener">官方文档</a>,我们知道判断 Lua 脚本返回空值使用,应该直接判断 <code>true/false</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">127.0.0.1:6379> get test_version</span><br><span class="line">(nil)</span><br><span class="line">127.0.0.1:6379> EVAL "local a = redis.call('get',KEYS[1]) if a == false then return 'empty' else return 'not empty' end" 1 test_version test_version</span><br><span class="line">"empty"</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">
在 Redis 里执行 `get` 或 `hget` 不存在的 `key` 或 `field` 时返回值在终端显式的是 `(nil)`,对于这样的问题,在 Lua 脚本中如何判空呢?
</summary>
<category term="Redis" scheme="https://www.hchstudio.cn/categories/Redis/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Redis" scheme="https://www.hchstudio.cn/tags/Redis/"/>
</entry>
<entry>
<title>Git 命令</title>
<link href="https://www.hchstudio.cn/article/2019/b1aa/"/>
<id>https://www.hchstudio.cn/article/2019/b1aa/</id>
<published>2019-10-20T13:15:51.000Z</published>
<updated>2021-07-21T11:42:18.904Z</updated>
<content type="html"><![CDATA[<p>熟练使用工具决定工作效率,Git 是工作中常见的分布式版本控制系统。本篇文章总结一些常用的命令以及原理。</p><h2 id="Git-命令"><a href="#Git-命令" class="headerlink" title="Git 命令"></a>Git 命令</h2><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><p>git config [<options>] </options></p><table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td>git config –global …</td><td>全局配置</td></tr><tr><td>git config –local …</td><td>本地项目</td></tr><tr><td>git config –global user.name/email …</td><td>配置全局名称和邮箱</td></tr><tr><td>git config –list</td><td>参看配置</td></tr></tbody></table><h3 id="日志"><a href="#日志" class="headerlink" title="日志"></a>日志</h3><p>git log [<options>] [<revision-range>] [[–] <path>…]<br>git reflog 引用日志</path></revision-range></options></p><table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td>git log –stat</td><td>简略的统计信息</td></tr><tr><td>git log -count</td><td>显示记录条数</td></tr><tr><td>git log –pretty=</td><td>可用的选项包括 oneline,short,full,fuller 和 format(后跟指定格式)</td></tr></tbody></table><h3 id="远程"><a href="#远程" class="headerlink" title="远程"></a>远程</h3><p>git remote [-v | –verbose]</p><table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td>git remote -v</td><td>查看远程仓库</td></tr><tr><td>git remote add <name> <url></url></name></td><td>添加远程仓库</td></tr><tr><td>git remote rename <old> <new></new></old></td><td>重命名远程仓库</td></tr><tr><td>git remote remove <name></name></td><td>移除远程仓库</td></tr><tr><td>git remote show <name></name></td><td>参看远程仓库</td></tr></tbody></table><h3 id="标签"><a href="#标签" class="headerlink" title="标签"></a>标签</h3><p>git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] <tagname> [<head><meta name="generator" content="Hexo 3.9.0">]<br>git tag -d <tagname>…<br>git tag -l [-n[<num>]] [–contains <commit>] [–no-contains <commit>] [–points-at <object>]<br> [–format=<format>] [–[no-]merged [<commit>]] [<pattern>…]<br>git tag -v [–format=<format>] <tagname>…</tagname></format></pattern></commit></format></object></commit></commit></num></tagname></head></tagname></file></msg></key-id></p><table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td>git tag -l</td><td>查看 tag 列表</td></tr><tr><td>git tag <name></name></td><td>创建轻量级标签</td></tr><tr><td>git tag -a <name> -m <message></message></name></td><td>创建附注标签</td></tr><tr><td>git tag -a <name> 9fceb02</name></td><td>后期给某一提交打标签</td></tr><tr><td>git show <tagname></tagname></td><td>查看 tag 信息</td></tr><tr><td>git push origin <tagname></tagname></td><td>推送到远程服务器</td></tr><tr><td>git push origin –tags</td><td>不在服务器的标签全部推送上去</td></tr><tr><td>git tag -d <tagname></tagname></td><td>删除标签</td></tr><tr><td>git push <remote> :refs/tags/<tagname></tagname></remote></td><td>从任何远程仓库中移除这个标签</td></tr></tbody></table><h3 id="分支"><a href="#分支" class="headerlink" title="分支"></a>分支</h3><p>git branch [<options>] [-r | -a] [–merged | –no-merged]<br>git branch [<options>] [-l] [-f] <branch-name> [<start-point>]<br>git branch [<options>] [-r] (-d | -D) <branch-name>…<br>git branch [<options>] (-m | -M) [<old-branch>] <new-branch><br>git branch [<options>] (-c | -C) [<old-branch>] <new-branch><br>git branch [<options>] [-r | -a] [–points-at]<br>git branch [<options>] [-r | -a] [–format]</options></options></new-branch></old-branch></options></new-branch></old-branch></options></branch-name></options></start-point></branch-name></options></options></p><table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td>git branch -a</td><td>远程和本地的分支列表</td></tr><tr><td>git branch -d</td><td>删除分支</td></tr><tr><td>git branch -D</td><td>删除分支,甚至没有合并</td></tr><tr><td>git branch -m</td><td>移动或者重命名分支</td></tr><tr><td>-vv</td><td>查看设置的所有跟踪分支</td></tr><tr><td>git push origin –delete <branchname></branchname></td><td>删除远程分支</td></tr></tbody></table><h3 id="检出"><a href="#检出" class="headerlink" title="检出"></a>检出</h3><p>git checkout [<options>] <branch><br>git checkout [<options>] [<branch>] – <file>…</file></branch></options></branch></options></p><table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td>git checkout -b <branch> origin/<branch></branch></branch></td><td>创建并检出新分支</td></tr><tr><td>git checkout – <file>…</file></td><td>恢复文件在工作区的修改</td></tr></tbody></table><h3 id="合并"><a href="#合并" class="headerlink" title="合并"></a>合并</h3><p>git merge [<options>] [<commit>…]<br>git merge –abort<br>git merge –continue</commit></options></p><table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td>git merge –abort</td><td>抛弃合并过程并且尝试重建合并前的状态</td></tr><tr><td>git merge –continue</td><td>合并冲突解决</td></tr></tbody></table><h3 id="推送"><a href="#推送" class="headerlink" title="推送"></a>推送</h3><p>git push [<options>] [<repository> [<refspec>…]]</refspec></repository></options></p><h3 id="重置"><a href="#重置" class="headerlink" title="重置"></a>重置</h3><p>git reset [–mixed | –soft | –hard | –merge | –keep] [-q] [<commit>]<br>git reset [-q] [<tree-ish>] [–] <paths>…<br>git reset –patch [<tree-ish>] [–] [<paths>…]</paths></tree-ish></paths></tree-ish></commit></p><table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td>git reset –mixed</td><td>重置已提交和缓存区域</td></tr><tr><td>git reset –soft</td><td>仅仅重置已提交</td></tr><tr><td>git reset –hard</td><td>重置已提交、缓存区域和工作目录</td></tr></tbody></table><h2 id="三棵树"><a href="#三棵树" class="headerlink" title="三棵树"></a>三棵树</h2><p>Git 的思维框架(将其作为内容管理器)管理三棵不同的树。“树” 在我们这里的实际意思是 “文件的集合”,而不是指特定的数据结构。 (在某些情况下索引看起来并不像一棵树,不过我们现在的目的是用简单的方式思考它。)</p><p>Git 作为一个系统,是以它的一般操作来管理并操纵这三棵树的:</p><table><thead><tr><th>树</th><th>用途</th></tr></thead><tbody><tr><td>HEAD</td><td>上一次提交的快照,下一次提交的父结点</td></tr><tr><td>Index</td><td>预期的下一次提交的快照</td></tr><tr><td>Working Directory</td><td>工作目录</td></tr></tbody></table><h3 id="HEAD"><a href="#HEAD" class="headerlink" title="HEAD"></a>HEAD</h3><p>HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。这表示 HEAD 将是下一次提交的父结点。 通常,理解 HEAD 的最简方式,就是将它看做 你的上一次提交 的快照。<br>参看快照命令:git cat-file -p HEAD </p><p>注:cat-file 是底层命令,它们一般用于底层工作,在日常工作中并不使用。不过它们能帮助我们了解到底发生了什么。 </p><h3 id="索引-Index"><a href="#索引-Index" class="headerlink" title="索引(Index)"></a>索引(Index)</h3><p>索引是你的 预期的下一次提交。 我们也会将这个概念引用为 Git 的“暂存区域”,这就是当你运行 git commit 时 Git 看起来的样子。 </p><p>Git 将上一次检出到工作目录中的所有文件填充到索引区,它们看起来就像最初被检出时的样子。 之后你会将其中一些文件替换为新版本,接着通过 git commit 将它们转换为树来用作新的提交。 </p><h3 id="工作目录-Working-Directory"><a href="#工作目录-Working-Directory" class="headerlink" title="工作目录(Working Directory)"></a>工作目录(Working Directory)</h3><p>最后,你就有了自己的工作目录。 另外两棵树以一种高效但并不直观的方式,将它们的内容存储在 .git 文件夹中。 工作目录会将它们解包为实际的文件以便编辑。 你可以把工作目录当做 沙盒。在你将修改提交到暂存区并记录到历史之前,可以随意更改。</p><h3 id="工作流程"><a href="#工作流程" class="headerlink" title="工作流程"></a>工作流程</h3><p>Git 主要的目的是通过操纵这三棵树来以更加连续的状态记录项目的快照。<br><img src="https://git-scm.com/book/en/v2/images/reset-workflow.png" alt="reset-workflow"><br>简单的总结如下:</p><ol><li>在工作目录编辑文件;</li><li>git add 后,Index 会保存并指向工作目录的修改;</li><li>git commit 后,会提交新的修改,HEAD 指向改新的修改。</li></ol><h2 id="命令区别"><a href="#命令区别" class="headerlink" title="命令区别"></a>命令区别</h2><h3 id="fetch、pull"><a href="#fetch、pull" class="headerlink" title="fetch、pull"></a>fetch、pull</h3><p>当 git fetch 命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容。它只会获取数据然后让你自己合并。 </p><p>然而,git pull 在大多数情况下它的含义是一个 git fetch 紧接着一个 git merge 命令。</p><h3 id="reset、checkout"><a href="#reset、checkout" class="headerlink" title="reset、checkout"></a>reset、checkout</h3><p>reset 命令会以特定的顺序重写这三棵树,在你指定以下选项时停止:</p><ul><li>移动 HEAD 分支的指向 (若指定了 –soft,则到此停止)</li><li>使索引看起来像 HEAD (若未指定 –hard,则到此停止)</li><li>使工作目录看起来像索引</li></ul><p>运行 git checkout [branch] 与运行 git reset –hard [branch] 非常相似,它会更新所有三棵树使其看起来像 [branch],不过有两点重要的区别。</p><p>首先不同于 reset –hard,checkout 对工作目录是安全的,它会通过检查来确保不会将已更改的文件弄丢。 其实它还更聪明一些。它会在工作目录中先试着简单合并一下,这样所有<em>还未修改过的</em>文件都会被更新。 而 reset –hard 则会不做检查就全面地替换所有东西。</p><p>第二个重要的区别是如何更新 HEAD。 reset 会移动 HEAD 分支的指向,而 checkout 只会移动 HEAD 自身来指向另一个分支。</p><p>下面的速查表列出了命令对树的影响。</p><table><thead><tr><th></th><th>HEAD</th><th>Index</th><th>Workdir</th><th>WD Safe?</th></tr></thead><tbody><tr><td>Commit Level</td></tr><tr><td>reset –soft [commit]</td><td>REF</td><td>NO</td><td>NO</td><td>YES</td></tr><tr><td>reset [commit]</td><td>REF</td><td>YES</td><td>NO</td><td>YES</td></tr><tr><td>reset –hard [commit]</td><td>REF</td><td>YES</td><td>YES</td><td>NO</td></tr><tr><td>checkout [commit]</td><td>HEAD</td><td>YES</td><td>YES</td><td>YES</td></tr><tr><td>File Level</td></tr><tr><td>reset (commit) [file]</td><td>NO</td><td>YES</td><td>NO</td><td>YES</td></tr><tr><td>checkout (commit) [file]</td><td>NO</td><td>YES</td><td>YES</td><td>NO</td></tr></tbody></table><h2 id="命令快照"><a href="#命令快照" class="headerlink" title="命令快照"></a>命令快照</h2><table><thead><tr><th>命令</th><th>描述</th></tr></thead><tbody><tr><td>git config</td><td>设置与配置</td></tr><tr><td>git help</td><td>帮助</td></tr><tr><td>git init</td><td>初始化</td></tr><tr><td>git clone</td><td>克隆</td></tr><tr><td>git add</td><td>将内容从工作目录添加到暂存区</td></tr><tr><td>git status</td><td>为你展示工作区及暂存区域中不同状态的文件</td></tr><tr><td>git diff</td><td>查看任意两棵树的差异</td></tr><tr><td>git difftool</td><td>可视化工具</td></tr><tr><td>git commit</td><td>提交</td></tr><tr><td>git reset</td><td>重置</td></tr><tr><td>git rm</td><td>从工作区,或者暂存区移除文件</td></tr><tr><td>git mv</td><td>在暂存区移到文件</td></tr><tr><td>git clean</td><td>从工作区中移除不想要的文件的命令</td></tr><tr><td>git branch</td><td>分支管理</td></tr><tr><td>git checkout</td><td>检出</td></tr><tr><td>git merge</td><td>合并</td></tr><tr><td>git mergetool</td><td>合并工具</td></tr><tr><td>git log</td><td>历史记录</td></tr><tr><td>git stash</td><td>临时地保存一些还没有提交的工作</td></tr><tr><td>git tag</td><td>标签</td></tr><tr><td>git fetch</td><td>从远程仓库中拉取</td></tr><tr><td>git pull</td><td>从远程仓库中拉取并合并</td></tr><tr><td>git push</td><td>推送到远程仓库</td></tr><tr><td>git remote</td><td>远程仓库记录的管理工具</td></tr><tr><td>git archive</td><td>创建项目一个指定快照的归档文件</td></tr><tr><td>git submodule</td><td>子模块</td></tr><tr><td>git show</td><td>显示一个标签或一个提交的信息</td></tr><tr><td>git shortlog</td><td>归纳 git log 的输出</td></tr><tr><td>git describe</td><td>描述</td></tr><tr><td>git bisect</td><td>二分查找</td></tr><tr><td>git blame</td><td>文件最后的修改者</td></tr><tr><td>git grep</td><td>查找任何字符串</td></tr><tr><td>git cherry-pick</td><td>获得并引入单个提交中的变更</td></tr><tr><td>git rebase</td><td>顺序合并多个提交</td></tr><tr><td>git revert</td><td>撤销或者倒转</td></tr><tr><td>git reflog</td><td>引用日志</td></tr></tbody></table>]]></content>
<summary type="html">
熟练使用工具决定工作效率,Git 是工作中常见的分布式版本控制系统。本篇文章总结一些常用的命令以及原理。
</summary>
<category term="工具" scheme="https://www.hchstudio.cn/categories/%E5%B7%A5%E5%85%B7/"/>
<category term="ChanghuiN" scheme="https://www.hchstudio.cn/tags/ChanghuiN/"/>
<category term="工具" scheme="https://www.hchstudio.cn/tags/%E5%B7%A5%E5%85%B7/"/>
</entry>
<entry>
<title>二分查找算法细节详解</title>
<link href="https://www.hchstudio.cn/article/2019/64d3/"/>
<id>https://www.hchstudio.cn/article/2019/64d3/</id>
<published>2019-09-01T00:35:52.000Z</published>
<updated>2021-07-21T11:42:18.906Z</updated>
<content type="html"><![CDATA[<h2 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h2><p>我相信对很多读者朋友来说,编写二分查找的算法代码属于玄学编程,虽然看起来很简单,就是会出错,要么会漏个等号,要么少加个 1。</p><p>不要气馁,因为二分查找其实并不简单。看看 Knuth 大佬(发明 KMP 算法的那位)怎么说的:</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">Although the basic idea of binary search is comparatively straightforward, </span><br><span class="line">the details can be surprisingly tricky...</span><br></pre></td></tr></table></figure><p>这句话可以这样理解:思路很简单,细节是魔鬼。</p><p>本文以问答的形式,探究几个最常用的二分查找场景:寻找一个数、寻找左侧边界、寻找右侧边界。第一个场景是最简单的算法形式,解决 这道题,后两个场景就是本题。</p><p>而且,我们就是要深入细节,比如不等号是否应该带等号,mid 是否应该加一等等。分析这些细节的差异以及出现这些差异的原因,保证你能灵活准确地写出正确的二分查找算法。</p><h2 id="零、二分查找框架"><a href="#零、二分查找框架" class="headerlink" title="零、二分查找框架"></a>零、二分查找框架</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">binarySearch</span><span class="params">(<span class="keyword">int</span>[] nums, <span class="keyword">int</span> target)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> left = <span class="number">0</span>, right = ...;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span>(...) {</span><br><span class="line"> <span class="keyword">int</span> mid = (right + left) / <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">if</span> (nums[mid] == target) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (nums[mid] < target) {</span><br><span class="line"> left = ...</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (nums[mid] > target) {</span><br><span class="line"> right = ...</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ...;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>分析二分查找的一个技巧是:不要出现 <code>else</code>,而是把所有情况用 <code>else if</code> 写清楚,这样可以清楚地展现所有细节。本文都会使用 <code>else if</code>,旨在讲清楚,读者理解后可自行简化。</p><p>其中 … 标记的部分,就是可能出现细节问题的地方,当你见到一个二分查找的代码时,首先注意这几个地方。后文用实例分析这些地方能有什么样的变化。</p><p>另外声明一下,计算 <code>mid</code> 时需要技巧防止溢出,即 <code>mid=left+(right-left)/2</code>。本文暂时忽略这个问题。</p><h2 id="一、寻找一个数(基本的二分搜索)"><a href="#一、寻找一个数(基本的二分搜索)" class="headerlink" title="一、寻找一个数(基本的二分搜索)"></a>一、寻找一个数(基本的二分搜索)</h2><p>这个场景是最简单的,肯能也是大家最熟悉的,即搜索一个数,如果存在,返回其索引,否则返回 <code>-1</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">binarySearch</span><span class="params">(<span class="keyword">int</span>[] nums, <span class="keyword">int</span> target)</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> left = <span class="number">0</span>; </span><br><span class="line"> <span class="keyword">int</span> right = nums.length - <span class="number">1</span>; <span class="comment">// 注意</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span>(left <= right) {</span><br><span class="line"> <span class="keyword">int</span> mid = (right + left) / <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">if</span>(nums[mid] == target)</span><br><span class="line"> <span class="keyword">return</span> mid; </span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (nums[mid] < target)</span><br><span class="line"> left = mid + <span class="number">1</span>; <span class="comment">// 注意</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (nums[mid] > target)</span><br><span class="line"> right = mid - <span class="number">1</span>; <span class="comment">// 注意</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>为什么 <code>while</code> 循环的条件中是 <code><=</code>,而不是 <code><</code> ?</li></ol><p>答:因为初始化 <code>right</code> 的赋值是 <code>nums.length-1</code>,即最后一个元素的索引,而不是 <code>nums.length</code>。</p><p>这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 <code>[left, right]</code>,后者相当于左闭右开区间 <code>[left, right)</code>,因为索引大小为 <code>nums.length</code> 是越界的。</p><p>我们这个算法中使用的是前者 <code>[left, right]</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span>(nums[mid] == target)</span><br><span class="line"> <span class="keyword">return</span> mid;</span><br></pre></td></tr></table></figure><p>但如果没找到,就需要 <code>while</code> 循环终止,然后返回 <code>-1</code>。那 <code>while</code> 循环什么时候应该终止?搜索区间为空的时候应该终止,意味着你没得找了,就等于没找到嘛。</p><p><code>while(left <= right)</code> 的终止条件是 <code>left == right + 1</code>,写成区间的形式就是 <code>[right + 1, right]</code>,或者带个具体的数字进去 <code>[3, 2]</code>,可见这时候搜索区间为空,因为没有数字既大于等于 <code>3</code> 又小于等于<br><code>2</code>的吧。所以这时候 while 循环终止是正确的,直接返回 <code>-1</code> 即可。</p><p><code>while(left < right)</code> 的终止条件是 <code>left == right</code>,写成区间的形式就是 <code>[left, right]</code>,或者带个具体的数字进去 <code>[2, 2]</code>,这时候搜索区间非空,还有一个数 <code>2</code>,但此时 <code>while</code> 循环终止了。也就是说这区间 <code>[2, 2]</code> 被漏掉了,索引<br><code>2</code> 没有被搜索,如果这时候直接返回 -1 就是错误的。</p><p>当然,如果你非要用 <code>while(left < right)</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="keyword">while</span>(left < right) {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> nums[left] == target ? left : -<span class="number">1</span>;</span><br></pre></td></tr></table></figure><ol start="2"><li>为什么 <code>left = mid + 1</code>,<code>right = mid - 1</code>?我看有的代码是 <code>right = mid</code> 或者 <code>left = mid</code> ,没有这些加加减减,到底怎么回事,怎么判断?</li></ol><p>答:这也是二分查找的一个难点,不过只要你能理解前面的内容,就能够很容易判断。</p><p>刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即 <code>[left, right]</code>。那么当我们发现索引 <code>mid</code> 不是要找的 <code>target</code> 时,如何确定下一步的搜索区间呢?</p><p>当然是 <code>[left, mid - 1]</code> 或者 <code>[mid + 1, right]</code> 对不对?因为 mid 已经搜索过,应该从搜索区间中去除。</p><ol start="3"><li>此算法有什么缺陷?</li></ol><p>答:至此,你应该已经掌握了该算法的所有细节,以及这样处理的原因。但是,这个算法存在局限性。</p><p>比如说给你有序数组 <code>nums = [1,2,2,2,3],target = 2</code>,此算法返回的索引是 <code>2</code><br>1,没错。但是如果我想得到 target 的左侧边界,即索引 1<br>2,或者我想得到 target 的右侧边界,即索引 <code>3</code><br>3,这样的话此算法是无法处理的。</p><p>这样的需求很常见。你也许会说,找到一个 <code>target</code>,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。</p><p>我们后续的算法就来讨论这两种二分查找的算法。</p><h2 id="二、寻找左侧边界的二分搜索"><a href="#二、寻找左侧边界的二分搜索" class="headerlink" title="二、寻找左侧边界的二分搜索"></a>二、寻找左侧边界的二分搜索</h2><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><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"><span class="function"><span class="keyword">int</span> <span class="title">left_bound</span><span class="params">(<span class="keyword">int</span>[] nums, <span class="keyword">int</span> target)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (nums.length == <span class="number">0</span>) <span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line"> <span class="keyword">int</span> left = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">int</span> right = nums.length; <span class="comment">// 注意</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">while</span> (left < right) { <span class="comment">// 注意</span></span><br><span class="line"> <span class="keyword">int</span> mid = (left + right) / <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">if</span> (nums[mid] == target) {</span><br><span class="line"> right = mid;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (nums[mid] < target) {</span><br><span class="line"> left = mid + <span class="number">1</span>;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (nums[mid] > target) {</span><br><span class="line"> right = mid; <span class="comment">// 注意</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> left;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>为什么 <code>while(left < right)</code> 而不是 <code><=</code> ?</li></ol><p>答:用相同的方法分析,因为 <code>right = nums.length</code> 而不是 <code>nums.length - 1</code>。因此每次循环的「搜索区间」是 <code>[left, right)</code> 左闭右开。</p><p><code>while(left < right)</code> 终止的条件是 <code>left == right</code>,此时搜索区间 <code>[left, left)</code> 为空,所以可以正确终止。</p><ol start="2"><li>为什么没有返回 -1 的操作?如果 <code>nums</code> 中不存在 <code>target</code> 这个值,怎么办?</li></ol><p>答:因为要一步一步来,先理解一下这个「左侧边界」有什么特殊含义:<br><img src="https://img.hchstudio.cn/binary-search01.png" alt="binary-search"></p><p>对于这个数组,算法会返回 <code>1</code>。这个 <code>1</code> 的含义可以这样解读:nums 中小于 <code>2</code> 的元素有 <code>1</code> 个。</p><p>比如对于有序数组 nums = [2,3,5,7], target = 1,算法会返回 <code>0</code> ,含义是:<code>nums</code> 中小于<code>1</code> 的元素有<code>0</code>个。</p><p>再比如说 <code>nums</code> 不变,<code>target = 8</code>,算法会返回 <code>4</code>,含义是:<code>nums</code> 中小于 <code>8</code> 的元素有<code>4</code> 个。</p><p>综上可以看出,函数的返回值(即 <code>left</code> 变量的值)取值区间是闭区间 <code>[0, nums.length]</code>,所以我们简单添加两行代码就能在正确的时候 <code>return -1</code>:<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span> (left < right) {</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">}</span><br><span class="line"><span class="comment">// target 比所有数都大</span></span><br><span class="line"><span class="keyword">if</span> (left == nums.length) <span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line"><span class="comment">// 类似之前算法的处理方式</span></span><br><span class="line"><span class="keyword">return</span> nums[left] == target ? left : -<span class="number">1</span>;</span><br></pre></td></tr></table></figure></p><ol start="3"><li>为什么 <code>left = mid + 1,right = mid</code> ?和之前的算法不一样?</li></ol><p>答:这个很好解释,因为我们的「搜索区间」是 <code>[left, right)</code> 左闭右开,所以当 <code>nums[mid]</code> 被检测之后,下一步的搜索区间应该去掉 mid 分割成两个区间,即 <code>[left, mid)</code> 或 <code>[mid + 1, right)</code>。</p><ol start="4"><li>为什么该算法能够搜索左侧边界?</li></ol><p>答:关键在于对于 <code>nums[mid] == target</code> 这种情况的处理:</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"><span class="keyword">if</span> (nums[mid] == target)</span><br><span class="line"> right = mid;</span><br></pre></td></tr></table></figure><p>可见,找到 <code>target</code> 时不要立即返回,而是缩小「搜索区间」的上界 <code>right</code>,在区间 <code>[left, mid)</code>中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。</p><ol start="5"><li>为什么返回 <code>left</code> 而不是 <code>right</code>?</li></ol><p>答:都是一样的,因为 <code>while</code> 终止的条件是 <code>left == right</code>。</p><h2 id="三、寻找右侧边界的二分查找"><a href="#三、寻找右侧边界的二分查找" class="headerlink" title="三、寻找右侧边界的二分查找"></a>三、寻找右侧边界的二分查找</h2><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><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">right_bound</span><span class="params">(<span class="keyword">int</span>[] nums, <span class="keyword">int</span> target)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (nums.length == <span class="number">0</span>) <span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line"> <span class="keyword">int</span> left = <span class="number">0</span>, right = nums.length;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">while</span> (left < right) {</span><br><span class="line"> <span class="keyword">int</span> mid = (left + right) / <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">if</span> (nums[mid] == target) {</span><br><span class="line"> left = mid + <span class="number">1</span>; <span class="comment">// 注意</span></span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (nums[mid] < target) {</span><br><span class="line"> left = mid + <span class="number">1</span>;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (nums[mid] > target) {</span><br><span class="line"> right = mid;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> left - <span class="number">1</span>; <span class="comment">// 注意</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>为什么这个算法能够找到右侧边界?</li></ol><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"><span class="keyword">if</span> (nums[mid] == target) {</span><br><span class="line"> left = mid + <span class="number">1</span>;</span><br></pre></td></tr></table></figure><p>当 <code>nums[mid] == target</code> 时,不要立即返回,而是增大「搜索区间」的下界 <code>left</code>,使得区间不断向右收缩,达到锁定右侧边界的目的。</p><ol start="2"><li>为什么最后返回 <code>left - 1</code> 而不像左侧边界的函数,返回 <code>left</code>?而且我觉得这里既然是搜索右侧边界,应该返回 <code>right</code> 才对。</li></ol><p>答:首先,<code>while</code> 循环的终止条件是 <code>left == right</code>,所以 <code>left</code> 和 <code>right</code> 是一样的,你非要体现右侧的特点,返回 <code>right - 1</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (nums[mid] == target) {</span><br><span class="line"> left = mid + <span class="number">1</span>;</span><br><span class="line"> <span class="comment">// 这样想: mid = left - 1</span></span><br></pre></td></tr></table></figure><p><img src="https://img.hchstudio.cn/binary-search02.png" alt></p><p>因为我们对 left 的更新必须是 <code>left = mid + 1</code>,就是说 <code>while</code> 循环结束时,<code>nums[left]</code> 一定不等于 <code>target</code> 了,而 <code>nums[left-1]</code> 可能是 <code>target</code>。</p><p>至于为什么 left 的更新必须是 <code>left = mid + 1</code>,同左侧边界搜索,就不再赘述。</p><ol start="3"><li>为什么没有返回 <code>−1</code><br><code>−1</code> 的操作?如果 <code>nums</code> 中不存在 <code>target</code> 这个值,怎么办?</li></ol><p>答:类似之前的左侧边界搜索,因为 <code>while</code> 的终止条件是 <code>left == right</code>,就是说 <code>left</code> 的取值范围是 <code>[0, nums.length]</code>,所以可以添加两行代码,正确地返回 <code>−1</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="keyword">while</span> (left < right) {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (left == <span class="number">0</span>) <span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line"><span class="keyword">return</span> nums[left-<span class="number">1</span>] == target ? (left-<span class="number">1</span>) : -<span class="number">1</span>;</span><br></pre></td></tr></table></figure><h2 id="四、最后总结"><a href="#四、最后总结" class="headerlink" title="四、最后总结"></a>四、最后总结</h2><p>来梳理一下这些细节差异的因果逻辑:</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></pre></td><td class="code"><pre><span class="line">因为我们初始化 right = nums.length - <span class="number">1</span></span><br><span class="line">所以决定了我们的「搜索区间」是 [left, right]</span><br><span class="line">所以决定了 <span class="keyword">while</span> (left <= right)</span><br><span class="line">同时也决定了 left = mid+<span class="number">1</span> 和 right = mid-<span class="number">1</span></span><br><span class="line"></span><br><span class="line">因为我们只需找到一个 target 的索引即可</span><br><span class="line">所以当 nums[mid] == target 时可以立即返回</span><br></pre></td></tr></table></figure><p>第二个,寻找左侧边界的二分查找:<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">因为我们初始化 right = nums.length</span><br><span class="line">所以决定了我们的「搜索区间」是 [left, right)</span><br><span class="line">所以决定了 <span class="keyword">while</span> (left < right)</span><br><span class="line">同时也决定了 left = mid + <span class="number">1</span> 和 right = mid</span><br><span class="line"></span><br><span class="line">因为我们需找到 target 的最左侧索引</span><br><span class="line">所以当 nums[mid] == target 时不要立即返回</span><br><span class="line">而要收紧右侧边界以锁定左侧边界</span><br></pre></td></tr></table></figure></p><p>第三个,寻找右侧边界的二分查找:<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">因为我们初始化 right = nums.length</span><br><span class="line">所以决定了我们的「搜索区间」是 [left, right)</span><br><span class="line">所以决定了 <span class="keyword">while</span> (left < right)</span><br><span class="line">同时也决定了 left = mid + <span class="number">1</span> 和 right = mid</span><br><span class="line"></span><br><span class="line">因为我们需找到 target 的最右侧索引</span><br><span class="line">所以当 nums[mid] == target 时不要立即返回</span><br><span class="line">而要收紧左侧边界以锁定右侧边界</span><br><span class="line"></span><br><span class="line">又因为收紧左侧边界时必须 left = mid + <span class="number">1</span></span><br><span class="line">所以最后无论返回 left 还是 right,必须减一</span><br><span class="line">如果以上内容你都能理解,那么恭喜你,二分查找算法的细节不过如此。</span><br></pre></td></tr></table></figure></p><p>通过本文,你学会了:</p><p>分析二分查找代码时,不要出现 <code>else</code>,全部展开成 <code>else if</code> 方便理解。</p><p>注意「搜索区间」和 while 的终止条件,如果存在漏掉的元素,记得在最后检查。</p><p>如需要搜索左右边界,只要在 <code>nums[mid] == target</code> 时做修改即可。搜索右侧时需要减一。</p><p>以后就算遇到其他的二分查找变形,运用这几点技巧,也能保证你写出正确的代码。</p><h2 id="出处"><a href="#出处" class="headerlink" title="出处"></a>出处</h2><p><a href="https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/er-fen-cha-zhao-suan-fa-xi-jie-xiang-jie-by-labula/" target="_blank" rel="noopener">出处</a></p>]]></content>
<summary type="html">
我相信对很多读者朋友来说,编写二分查找的算法代码属于玄学编程,虽然看起来很简单,就是会出错,要么会漏个等号,要么少加个 1。
</summary>
<category term="Java" scheme="https://www.hchstudio.cn/categories/Java/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Java" scheme="https://www.hchstudio.cn/tags/Java/"/>
</entry>
<entry>
<title>实现自己的 RPC 框架(二)</title>
<link href="https://www.hchstudio.cn/article/2019/ba29/"/>
<id>https://www.hchstudio.cn/article/2019/ba29/</id>
<published>2019-08-05T00:35:52.000Z</published>
<updated>2021-07-21T11:42:18.907Z</updated>
<content type="html"><![CDATA[<p>前段时间自己搞了个 RPC 的轮子,不过相对来说比较简单,最近在原来的基础上加以改造,使用 Zookeeper 实现了 provider 自动寻址以及消费者的简单负载均衡,对之前的感兴趣的请转 <a href="https://www.hchstudio.cn/article/2018/b674/">造个轮子—RPC动手实现</a>。</p><h2 id="RPC-模型"><a href="#RPC-模型" class="headerlink" title="RPC 模型"></a>RPC 模型</h2><p>在原来使用 TCP 直连的基础上实现基于 Zookeeper 的服务的注册与发现,改造后的依赖关系是这样的。</p><p><img src="https://img.hchstudio.cn/child-rpc2.png" alt="child-rpc"></p><h2 id="怎么用"><a href="#怎么用" class="headerlink" title="怎么用"></a>怎么用</h2><p>话不多说,我们来看下如何发布和引用服务。<br>服务端我们将服务的 IP 和端口号基础信息注册到 Zookeeper 上。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> wuhaifei 2019-08-02</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ZookeeperServerMainTest</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> ServerConfig serverConfig = <span class="keyword">new</span> ServerConfig();</span><br><span class="line"> serverConfig.setSerializer(AbstractSerializer.SerializeEnum.HESSIAN.serializer)</span><br><span class="line"> .setHost(<span class="string">"172.16.30.114"</span>)</span><br><span class="line"> .setPort(<span class="number">5201</span>)</span><br><span class="line"> .setRef(HelloServiceImpl<span class="class">.<span class="keyword">class</span>.<span class="title">getName</span>())</span></span><br><span class="line"><span class="class"> .<span class="title">setRegister</span>(<span class="title">true</span>)</span></span><br><span class="line"><span class="class"> .<span class="title">setInterfaceId</span>(<span class="title">HelloService</span>.<span class="title">class</span>.<span class="title">getName</span>())</span>;</span><br><span class="line"></span><br><span class="line"> RegistryConfig registryConfig = <span class="keyword">new</span> RegistryConfig().setAddress(<span class="string">"127.0.0.1:2181"</span>)</span><br><span class="line"> .setSubscribe(<span class="keyword">true</span>)</span><br><span class="line"> .setRegister(<span class="keyword">true</span>)</span><br><span class="line"> .setProtocol(RpcConstants.ZOOKEEPER);</span><br><span class="line"> ServerProxy serverProxy = <span class="keyword">new</span> ServerProxy(<span class="keyword">new</span> NettyServerAbstract())</span><br><span class="line"> .setServerConfig(serverConfig)</span><br><span class="line"> .setRegistryConfig(registryConfig);</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> serverProxy.export();</span><br><span class="line"> <span class="keyword">while</span> (<span class="keyword">true</span>){</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line"> e.printStackTrace();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>通过 Zookeeper 引用注册在其上的服务。<br><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> wuhaifei 2019-08-02</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ZookeeperClientMainTest</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> ClientConfig clientConfig = <span class="keyword">new</span> ClientConfig();</span><br><span class="line"> clientConfig.setProtocol(RpcConstants.ZOOKEEPER)</span><br><span class="line"> .setTimeoutMillis(<span class="number">100000</span>)</span><br><span class="line"> .setSerializer(AbstractSerializer.SerializeEnum.HESSIAN.serializer);</span><br><span class="line"></span><br><span class="line"> RegistryConfig registryConfig = <span class="keyword">new</span> RegistryConfig()</span><br><span class="line"> .setAddress(<span class="string">"127.0.0.1:2181"</span>)</span><br><span class="line"> .setProtocol(RpcConstants.ZOOKEEPER)</span><br><span class="line"> .setRegister(<span class="keyword">true</span>)</span><br><span class="line"> .setSubscribe(<span class="keyword">true</span>);</span><br><span class="line"> ClientProxy<HelloService> clientProxy = <span class="keyword">new</span> ClientProxy(clientConfig, <span class="keyword">new</span> NettyClientAbstract(), HelloService<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class"> .<span class="title">setRegistryConfig</span>(<span class="title">registryConfig</span>)</span>;</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < <span class="number">10</span>; i++) {</span><br><span class="line"> HelloService helloService = clientProxy.refer();</span><br><span class="line"> System.out.println(helloService.sayHi());</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>运行结果就不一一贴出了,感兴趣的小伙伴可以查看楼主传到 github 上的源码<a href="https://github.com/haifeiWu/child-rpc.git" target="_blank" rel="noopener">这是一个rpc的轮子</a>。</p><h2 id="服务的发布与订阅"><a href="#服务的发布与订阅" class="headerlink" title="服务的发布与订阅"></a>服务的发布与订阅</h2><p>楼主在原来代码的基础上添加了 Zookeeper 的注册的逻辑,原来的代码相关介绍请转 <a href="https://www.hchstudio.cn/article/2018/b674/">造个轮子—RPC动手实现</a>。</p><h3 id="服务的发布"><a href="#服务的发布" class="headerlink" title="服务的发布"></a>服务的发布</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">* 发布服务</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">export</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> Object serviceBean = Class.forName((String) serverConfig.getRef()).newInstance();</span><br><span class="line"> RpcInvokerHandler.serviceMap.put(serverConfig.getInterfaceId(), serviceBean);</span><br><span class="line"> <span class="keyword">this</span>.childServer.start(<span class="keyword">this</span>.getServerConfig());</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (serverConfig.isRegister()) {</span><br><span class="line"> <span class="comment">// 将服务注册到zookeeper</span></span><br><span class="line"> register();</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line"> <span class="comment">// 取消服务注册</span></span><br><span class="line"> unregister();</span><br><span class="line"> <span class="keyword">if</span> (e <span class="keyword">instanceof</span> ChildRpcRuntimeException) {</span><br><span class="line"> <span class="keyword">throw</span> (ChildRpcRuntimeException) e;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> ChildRpcRuntimeException(<span class="string">"Build provider proxy error!"</span>, e);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> exported = <span class="keyword">true</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 注册服务</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">register</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (serverConfig.isRegister()) {</span><br><span class="line"> Registry registry = RegistryFactory.getRegistry(<span class="keyword">this</span>.getRegistryConfig());</span><br><span class="line"> registry.init();</span><br><span class="line"> registry.start();</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> registry.register(<span class="keyword">this</span>.serverConfig);</span><br><span class="line"> } <span class="keyword">catch</span> (ChildRpcRuntimeException e) {</span><br><span class="line"> <span class="keyword">throw</span> e;</span><br><span class="line"> } <span class="keyword">catch</span> (Throwable e) {</span><br><span class="line"> String appName = serverConfig.getInterfaceId();</span><br><span class="line"> LOGGER.info(appName, <span class="string">"Catch exception when register to registry: "</span></span><br><span class="line"> + registryConfig.getId(), e);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="服务的订阅"><a href="#服务的订阅" class="headerlink" title="服务的订阅"></a>服务的订阅</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">* 服务的引用.</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> T <span class="title">refer</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span> (config.isSubscribe()) {</span><br><span class="line"> subscribe();</span><br><span class="line"> }</span><br><span class="line"> childClient.init(<span class="keyword">this</span>.clientConfig);</span><br><span class="line"> <span class="keyword">return</span> invoke();</span><br><span class="line"> } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line"> e.printStackTrace();</span><br><span class="line"> }</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><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 订阅zk的服务列表.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">subscribe</span><span class="params">()</span> </span>{</span><br><span class="line"> Registry registry = RegistryFactory.getRegistry(<span class="keyword">this</span>.getRegistryConfig());</span><br><span class="line"> registry.init();</span><br><span class="line"> registry.start();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">this</span>.clientConfig = (ClientConfig) config;</span><br><span class="line"> List<String> providerList = registry.subscribe(<span class="keyword">this</span>.clientConfig);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">null</span> == providerList) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> ChildRpcRuntimeException(<span class="string">"无可用服务供订阅!"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 使用随机算法,随机选择一个provider</span></span><br><span class="line"> <span class="keyword">int</span> index = ThreadLocalRandom.current().nextInt(providerList.size());</span><br><span class="line"> String providerInfo = providerList.get(index);</span><br><span class="line"> String[] providerArr = providerInfo.split(<span class="string">":"</span>);</span><br><span class="line"> clientConfig = (ClientConfig) <span class="keyword">this</span>.config;</span><br><span class="line"> clientConfig.setHost(providerArr[<span class="number">0</span>]);</span><br><span class="line"> clientConfig.setPort(Integer.parseInt(providerArr[<span class="number">1</span>]));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上面代码比较简单,就是在原来直连的基础上添加 zk 的操作,在发布服务的时候将 provider 的 IP 和端口号基础信息注册到 zk 上,在引用服务的时候使用随机算法从 zk 上选取可用的 provider 信息,然后进行 invoke 调用。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>RPC(Remote procedure call)底层逻辑相对来说比较简单,楼主在实现的过程中参考了其他 RPC 框架的部分代码,受益匪浅~</p>]]></content>
<summary type="html">
书接上回,前段时间自己搞了个 RPC 的轮子,不过相对来说比较简单,最近在原来的基础上加以改造,使用 Zookeeper 实现了 provider 自动寻址以及消费者的简单负载均衡。
</summary>
<category term="Java" scheme="https://www.hchstudio.cn/categories/Java/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Java" scheme="https://www.hchstudio.cn/tags/Java/"/>
</entry>
<entry>
<title>lang3 的 split 方法误用</title>
<link href="https://www.hchstudio.cn/article/2019/ade9/"/>
<id>https://www.hchstudio.cn/article/2019/ade9/</id>
<published>2019-07-26T10:10:57.000Z</published>
<updated>2021-07-21T11:42:18.906Z</updated>
<content type="html"><![CDATA[<p>apache 的 lang3 是我们开发常用到的三方工具包,然而对这个包不甚了解的话,会产生莫名其秒的 bug ,在这里做下记录。</p><h2 id="误用示例"><a href="#误用示例" class="headerlink" title="误用示例"></a>误用示例</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestDemo</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>{</span><br><span class="line"> String sendMsg = <span class="string">"{\"expiredTime\":\"20190726135831\",\"drives\":\"androidgetui\",\"msgBody\":\"{\\\"serialNumber\\\":\\\"wow22019072611349502\\\",\\\"push_key\\\":\\\"appactive#549110277\\\",\\\"title\\\":\\\"\\xe6\\x9c\\x89\\xe4\\xba\\xba@\\xe4\\xbd\\xa0\\\",\\\"message\\\":\\\"\\xe4\\xbb\\x8a\\xe5\\xa4\\xa9\\xe5\\x87\\xa0\\xe7\\x82\\xb9\\xe5\\x87\\xba\\xe5\\x8f\\x91\\xef\\xbc\\x9f\\\",\\\"link\\\":\\\"chelaile://homeTab/home?select=3\\\",\\\"open_type\\\":0,\\\"expireDays\\\":\\\"30\\\",\\\"type\\\":14}\",\"clients\":[\"13065ffa4e25c4a7c68\"]}CHELAILE_PUSH{\"cityId\":\"007\",\"gpsTime\":\"2019-07-24 21:33:06\",\"lat\":\"30.605916\",\"lng\":\"103.980439\",\"s\":\"android\",\"sourceUdid\":\"a4419b93-fb0e-43c7-98fa-5b7c18255660\",\"token\":\"13065ffa4e25c4a7c68\",\"tokenType\":\"3\",\"udid\":\"UDID2TOKEN#a4419b93-fb0e-43c7-98fa-5b7c18255660\",\"userCreateTime\":\"2018-04-20 08:13:32\",\"userLastActiveTime\":\"2019-07-24 21:33:06\",\"vc\":\"150\"}"</span>;</span><br><span class="line"> String[] dataArr = StringUtils.split(sendMsg,<span class="string">"CHELAILE_PUSH"</span>);</span><br><span class="line"> Assert.assertEquals(dataArr.length,<span class="number">2</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="分析原因"><a href="#分析原因" class="headerlink" title="分析原因"></a>分析原因</h2><p>通过分析字符串的拆分结果,发现该方法并不是将分隔符去截取字符串,而是将分隔符的每一个字符都当成分隔符去截取字符串,当我们的分隔符是一个字符的时候一般不会出现上面示例中出现的问题,如果分隔符是多个字符的时候这个问题就显现出来了。</p><h2 id="查看-StringUtils-源码"><a href="#查看-StringUtils-源码" class="headerlink" title="查看 StringUtils 源码"></a>查看 StringUtils 源码</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * <pre></span></span><br><span class="line"><span class="comment"> * StringUtils.split(null, *) = null</span></span><br><span class="line"><span class="comment"> * StringUtils.split("", *) = []</span></span><br><span class="line"><span class="comment"> * StringUtils.split("abc def", null) = ["abc", "def"]</span></span><br><span class="line"><span class="comment"> * StringUtils.split("abc def", " ") = ["abc", "def"]</span></span><br><span class="line"><span class="comment"> * StringUtils.split("abc def", " ") = ["abc", "def"]</span></span><br><span class="line"><span class="comment"> * StringUtils.split("ab:cd:ef", ":") = ["ab", "cd", "ef"]</span></span><br><span class="line"><span class="comment"> * </pre></span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> str 要解析的字符串,可能为空</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> separatorChars 用做分割字符的字符们(注意是字符串们哦!),当 separatorChars 传入的值为空的时候则用空格来做分隔符</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> String[] split(<span class="keyword">final</span> String str, <span class="keyword">final</span> String separatorChars) {</span><br><span class="line"> <span class="keyword">return</span> splitWorker(str, separatorChars, -<span class="number">1</span>, <span class="keyword">false</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Performs the logic for the {<span class="doctag">@code</span> split} and</span></span><br><span class="line"><span class="comment"> * {<span class="doctag">@code</span> splitPreserveAllTokens} methods that return a maximum array</span></span><br><span class="line"><span class="comment"> * length.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> str the String to parse, may be {<span class="doctag">@code</span> null}</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> separatorChars the separate character</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> max the maximum number of elements to include in the</span></span><br><span class="line"><span class="comment"> * array. A zero or negative value implies no limit.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> preserveAllTokens if {<span class="doctag">@code</span> true}, adjacent separators are</span></span><br><span class="line"><span class="comment"> * treated as empty token separators; if {<span class="doctag">@code</span> false}, adjacent</span></span><br><span class="line"><span class="comment"> * separators are treated as one separator.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> an array of parsed Strings, {<span class="doctag">@code</span> null} if null String input</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> String[] splitWorker(<span class="keyword">final</span> String str, <span class="keyword">final</span> String separatorChars, <span class="keyword">final</span> <span class="keyword">int</span> max, <span class="keyword">final</span> <span class="keyword">boolean</span> preserveAllTokens) {</span><br><span class="line"> <span class="comment">// Performance tuned for 2.0 (JDK1.4)</span></span><br><span class="line"> <span class="comment">// Direct code is quicker than StringTokenizer.</span></span><br><span class="line"> <span class="comment">// Also, StringTokenizer uses isSpace() not isWhitespace()</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (str == <span class="keyword">null</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 class="keyword">final</span> <span class="keyword">int</span> len = str.length();</span><br><span class="line"> <span class="keyword">if</span> (len == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> ArrayUtils.EMPTY_STRING_ARRAY;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">final</span> List<String> list = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"> <span class="keyword">int</span> sizePlus1 = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">int</span> i = <span class="number">0</span>, start = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">boolean</span> match = <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">boolean</span> lastMatch = <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">if</span> (separatorChars == <span class="keyword">null</span>) {</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 用空格作为分隔符切割字符串</span></span><br><span class="line"> <span class="keyword">while</span> (i < len) {</span><br><span class="line"> <span class="keyword">if</span> (Character.isWhitespace(str.charAt(i))) {</span><br><span class="line"> <span class="keyword">if</span> (match || preserveAllTokens) {</span><br><span class="line"> lastMatch = <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (sizePlus1++ == max) {</span><br><span class="line"> i = len;</span><br><span class="line"> lastMatch = <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> list.add(str.substring(start, i));</span><br><span class="line"> match = <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> start = ++i;</span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> }</span><br><span class="line"> lastMatch = <span class="keyword">false</span>;</span><br><span class="line"> match = <span class="keyword">true</span>;</span><br><span class="line"> i++;</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (separatorChars.length() == <span class="number">1</span>) {</span><br><span class="line"> <span class="comment">// 分隔符的字符数为 1 的时候,切割字符串的逻辑</span></span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">char</span> sep = separatorChars.charAt(<span class="number">0</span>);</span><br><span class="line"> <span class="keyword">while</span> (i < len) {</span><br><span class="line"> <span class="keyword">if</span> (str.charAt(i) == sep) {</span><br><span class="line"> <span class="keyword">if</span> (match || preserveAllTokens) {</span><br><span class="line"> lastMatch = <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (sizePlus1++ == max) {</span><br><span class="line"> i = len;</span><br><span class="line"> lastMatch = <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> list.add(str.substring(start, i));</span><br><span class="line"> match = <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> start = ++i;</span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> }</span><br><span class="line"> lastMatch = <span class="keyword">false</span>;</span><br><span class="line"> match = <span class="keyword">true</span>;</span><br><span class="line"> i++;</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 当分隔符的字符数为多个的时候,分割字符串的逻辑</span></span><br><span class="line"> <span class="comment">// 示例:分隔字符串 abc,分割字符串的分隔符可以是 a,ab,abc</span></span><br><span class="line"> <span class="keyword">while</span> (i < len) {</span><br><span class="line"> <span class="keyword">if</span> (separatorChars.indexOf(str.charAt(i)) >= <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (match || preserveAllTokens) {</span><br><span class="line"> lastMatch = <span class="keyword">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (sizePlus1++ == max) {</span><br><span class="line"> i = len;</span><br><span class="line"> lastMatch = <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> list.add(str.substring(start, i));</span><br><span class="line"> match = <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> start = ++i;</span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> }</span><br><span class="line"> lastMatch = <span class="keyword">false</span>;</span><br><span class="line"> match = <span class="keyword">true</span>;</span><br><span class="line"> i++;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (match || preserveAllTokens && lastMatch) {</span><br><span class="line"> list.add(str.substring(start, i));</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> list.toArray(<span class="keyword">new</span> String[list.size()]);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>平时只知道调用api,在使用三方包的时候,没有认真查看api文档,对于三方包的方法,使用处于想当然的状态,这里应该做好反省。</p>]]></content>
<summary type="html">
apache 的 lang3 是我们开发常用到的三方工具包,然而对这个包不甚了解的话,会产生莫名其秒的 bug ,在这里做下记录。
</summary>
<category term="Java" scheme="https://www.hchstudio.cn/categories/Java/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Java" scheme="https://www.hchstudio.cn/tags/Java/"/>
</entry>
<entry>
<title>[译] 为什么String在Java中是不可变的</title>
<link href="https://www.hchstudio.cn/article/2019/496a/"/>
<id>https://www.hchstudio.cn/article/2019/496a/</id>
<published>2019-07-08T00:35:52.000Z</published>
<updated>2021-07-21T11:42:18.906Z</updated>
<content type="html"><![CDATA[<p>String 在 Java 中是不可变的。 不可变类只是一个无法修改其实例的类。 创建实例时,将初始化实例中的所有信息,并且无法修改信息。 不可变类有许多优点。 本文总结了<a href="https://www.programcreek.com/2009/02/diagram-to-show-java-strings-immutability/" target="_blank" rel="noopener">为什么 String 设计为不可变的</a>。 这篇文章从内存,同步和数据结构的角度说明了不变性概念。</p><h2 id="1-字符串池"><a href="#1-字符串池" class="headerlink" title="1. 字符串池"></a>1. 字符串池</h2><p>字符串池(String intern pool)是方法区域中的特殊存储区域。 创建字符串并且池中已存在该字符串时,将返回现有字符串的引用,而不是创建新对象。</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></pre></td><td class="code"><pre><span class="line">String string1 = <span class="string">"abcd"</span>;</span><br><span class="line">String string2 = <span class="string">"abcd"</span>;</span><br></pre></td></tr></table></figure><p>如下图所示:</p><p><img src="https://img.hchstudio.cn/java-string-pool.jpeg" alt="字符串池"></p><p>如果字符串是可变的,则使用一个引用更改字符串将导致其他引用的错误。</p><h2 id="2-缓存的哈希码"><a href="#2-缓存的哈希码" class="headerlink" title="2. 缓存的哈希码"></a>2. 缓存的哈希码</h2><p>字符串的哈希码经常在 Java 中使用。 例如,在 HashMap 或 HashSet 中。 不可变保证哈希码总是相同的,这样它就可以缓存起来而不用担心变化。这意味着,每次使用时都不需要计算哈希码。 这更有效率。</p><p>在String类中,它具有如下代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">int</span> hash;<span class="comment">//this is used to cache hash code.</span></span><br></pre></td></tr></table></figure><h2 id="3-其他对象中的字符串"><a href="#3-其他对象中的字符串" class="headerlink" title="3. 其他对象中的字符串"></a>3. 其他对象中的字符串</h2><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></pre></td><td class="code"><pre><span class="line">HashSet<String> set = <span class="keyword">new</span> HashSet<String>();</span><br><span class="line">set.add(<span class="keyword">new</span> String(<span class="string">"a"</span>));</span><br><span class="line">set.add(<span class="keyword">new</span> String(<span class="string">"b"</span>));</span><br><span class="line">set.add(<span class="keyword">new</span> String(<span class="string">"c"</span>));</span><br><span class="line"> </span><br><span class="line"><span class="keyword">for</span>(String a: set)</span><br><span class="line">a.value = <span class="string">"a"</span>;</span><br></pre></td></tr></table></figure><p>在此示例中,如果 String 是可变的,则可以更改其值,这将违反 set 的设计(set包含非重复元素)。 当然,上面的示例仅用于演示目的,并且实际字符串类中没有值字段。</p><h2 id="4-安全"><a href="#4-安全" class="headerlink" title="4. 安全"></a>4. 安全</h2><p>String 被广泛用作许多 java 类的参数,例如 网络连接,打开文件等。字符串不是不可变的,连接或文件将被更改,这可能会导致严重的安全威胁。 该方法认为它连接到一台机器,但事实并非如此。 可变字符串也可能在 Reflection 中引起安全问题,因为参数是字符串。</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">boolean</span> <span class="title">connect</span><span class="params">(string s)</span></span>{</span><br><span class="line"> <span class="keyword">if</span> (!isSecure(s)) { </span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> SecurityException(); </span><br><span class="line">}</span><br><span class="line"> <span class="comment">//here will cause problem, if s is changed before this by using other references. </span></span><br><span class="line"> causeProblem(s);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="5-不可变保证了线程安全"><a href="#5-不可变保证了线程安全" class="headerlink" title="5. 不可变保证了线程安全"></a>5. 不可变保证了线程安全</h2><p>由于无法更改不可变对象,因此可以在多个线程之间自由共享它们。 这消除了进行同步的要求。</p><p>综上所诉,出于效率和安全原因,String 被设计为不可变的,这也是在一般情况下在一些情况下优选不可变类的原因。</p>]]></content>
<summary type="html">
String 在 Java 中是不可变的。 不可变类只是一个无法修改其实例的类。 创建实例时,将初始化实例中的所有信息,并且无法修改信息。
</summary>
<category term="Java" scheme="https://www.hchstudio.cn/categories/Java/"/>
<category term="haifeiWu" scheme="https://www.hchstudio.cn/tags/haifeiWu/"/>
<category term="Java" scheme="https://www.hchstudio.cn/tags/Java/"/>
</entry>
</feed>