-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.xml
More file actions
524 lines (524 loc) · 273 KB
/
search.xml
File metadata and controls
524 lines (524 loc) · 273 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
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[40 | 高性能队列Disruptor]]></title>
<url>%2F2019%2F07%2F06%2Fjava%2F%E9%AB%98%E6%80%A7%E8%83%BD%E9%98%9F%E5%88%97Disruptor%2F</url>
<content type="text"><![CDATA[40 | 案例分析(三):高性能队列Disruptor我们在《20 | 并发容器:都有哪些“坑”需要我们填?》介绍过 Java SDK 提供了 2 个有界队列:ArrayBlockingQueue 和 LinkedBlockingQueue,它们都是基于 ReentrantLock 实现的,在高并发场景下,锁的效率并不高,那有没有更好的替代品呢?有,今天我们就介绍一种性能更高的有界队列:Disruptor。 Disruptor 是一款高性能的有界内存队列,目前应用非常广泛,Log4j2、Spring Messaging、HBase、Storm 都用到了 Disruptor,那 Disruptor 的性能为什么这么高呢?Disruptor 项目团队曾经写过一篇论文,详细解释了其原因,可以总结为如下: 内存分配更加合理,使用 RingBuffer 数据结构,数组元素在初始化时一次性全部创建,提升缓存命中率;对象循环利用,避免频繁 GC。 能够避免伪共享,提升缓存利用率。 采用无锁算法,避免频繁加锁、解锁的性能消耗。 支持批量消费,消费者可以无锁方式消费多个消息。 其中,前三点涉及到的知识比较多,所以今天咱们重点讲解前三点,不过在详细介绍这些知识之前,我们先来聊聊 Disruptor 如何使用,好让你先对 Disruptor 有个感官的认识。 下面的代码出自官方示例,我略做了一些修改,相较而言,Disruptor 的使用比 Java SDK 提供 BlockingQueue 要复杂一些,但是总体思路还是一致的,其大致情况如下: 在 Disruptor 中,生产者生产的对象(也就是消费者消费的对象)称为 Event,使用 Disruptor 必须自定义 Event,例如示例代码的自定义 Event 是 LongEvent; 构建 Disruptor 对象除了要指定队列大小外,还需要传入一个 EventFactory,示例代码中传入的是LongEvent::new; 消费 Disruptor 中的 Event 需要通过 handleEventsWith() 方法注册一个事件处理器,发布 Event 则需要通过 publishEvent() 方法。 123456789101112131415161718192021222324252627282930313233343536373839// 自定义 Eventclass LongEvent { private long value; public void set(long value) { this.value = value; }}// 指定 RingBuffer 大小,// 必须是 2 的 N 次方int bufferSize = 1024;// 构建 DisruptorDisruptor<LongEvent> disruptor = new Disruptor<>( LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE);// 注册事件处理器disruptor.handleEventsWith( (event, sequence, endOfBatch) -> System.out.println("E: "+event));// 启动 Disruptordisruptor.start();// 获取 RingBufferRingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();// 生产 EventByteBuffer bb = ByteBuffer.allocate(8);for (long l = 0; true; l++){ bb.putLong(0, l); // 生产者生产消息 ringBuffer.publishEvent( (event, sequence, buffer) -> event.set(buffer.getLong(0)), bb); Thread.sleep(1000);} RingBuffer 如何提升性能Java SDK 中 ArrayBlockingQueue 使用数组作为底层的数据存储,而 Disruptor 是使用RingBuffer作为数据存储。RingBuffer 本质上也是数组,所以仅仅将数据存储从数组换成 RingBuffer 并不能提升性能,但是 Disruptor 在 RingBuffer 的基础上还做了很多优化,其中一项优化就是和内存分配有关的。 在介绍这项优化之前,你需要先了解一下程序的局部性原理。简单来讲,程序的局部性原理指的是在一段时间内程序的执行会限定在一个局部范围内。这里的“局部性”可以从两个方面来理解,一个是时间局部性,另一个是空间局部性。时间局部性指的是程序中的某条指令一旦被执行,不久之后这条指令很可能再次被执行;如果某条数据被访问,不久之后这条数据很可能再次被访问。而空间局部性是指某块内存一旦被访问,不久之后这块内存附近的内存也很可能被访问。 CPU 的缓存就利用了程序的局部性原理:CPU 从内存中加载数据 X 时,会将数据 X 缓存在高速缓存 Cache 中,实际上 CPU 缓存 X 的同时,还缓存了 X 周围的数据,因为根据程序具备局部性原理,X 周围的数据也很有可能被访问。从另外一个角度来看,如果程序能够很好地体现出局部性原理,也就能更好地利用 CPU 的缓存,从而提升程序的性能。Disruptor 在设计 RingBuffer 的时候就充分考虑了这个问题,下面我们就对比着 ArrayBlockingQueue 来分析一下。 首先是 ArrayBlockingQueue。生产者线程向 ArrayBlockingQueue 增加一个元素,每次增加元素 E 之前,都需要创建一个对象 E,如下图所示,ArrayBlockingQueue 内部有 6 个元素,这 6 个元素都是由生产者线程创建的,由于创建这些元素的时间基本上是离散的,所以这些元素的内存地址大概率也不是连续的。 下面我们再看看 Disruptor 是如何处理的。Disruptor 内部的 RingBuffer 也是用数组实现的,但是这个数组中的所有元素在初始化时是一次性全部创建的,所以这些元素的内存地址大概率是连续的,相关的代码如下所示。 123456for (int i=0; i<bufferSize; i++){ //entries[] 就是 RingBuffer 内部的数组 //eventFactory 就是前面示例代码中传入的 LongEvent::new entries[BUFFER_PAD + i] = eventFactory.newInstance();} Disruptor 内部 RingBuffer 的结构可以简化成下图,那问题来了,数组中所有元素内存地址连续能提升性能吗?能!为什么呢?因为消费者线程在消费的时候,是遵循空间局部性原理的,消费完第 1 个元素,很快就会消费第 2 个元素;当消费第 1 个元素 E1 的时候,CPU 会把内存中 E1 后面的数据也加载进 Cache,如果 E1 和 E2 在内存中的地址是连续的,那么 E2 也就会被加载进 Cache 中,然后当消费第 2 个元素的时候,由于 E2 已经在 Cache 中了,所以就不需要从内存中加载了,这样就能大大提升性能。 除此之外,在 Disruptor 中,生产者线程通过 publishEvent() 发布 Event 的时候,并不是创建一个新的 Event,而是通过 event.set() 方法修改 Event, 也就是说 RingBuffer 创建的 Event 是可以循环利用的,这样还能避免频繁创建、删除 Event 导致的频繁 GC 问题。 如何避免“伪共享”高效利用 Cache,能够大大提升性能,所以要努力构建能够高效利用 Cache 的内存结构。而从另外一个角度看,努力避免不能高效利用 Cache 的内存结构也同样重要。 有一种叫做“伪共享(False sharing)”的内存布局就会使 Cache 失效,那什么是“伪共享”呢? 伪共享和 CPU 内部的 Cache 有关,Cache 内部是按照缓存行(Cache Line)管理的,缓存行的大小通常是 64 个字节;CPU 从内存中加载数据 X,会同时加载 X 后面(64-size(X))个字节的数据。下面的示例代码出自 Java SDK 的 ArrayBlockingQueue,其内部维护了 4 个成员变量,分别是队列数组 items、出队索引 takeIndex、入队索引 putIndex 以及队列中的元素总数 count。 12345678/** 队列数组 */final Object[] items;/** 出队索引 */int takeIndex;/** 入队索引 */int putIndex;/** 队列中元素总数 */int count; 当 CPU 从内存中加载 takeIndex 的时候,会同时将 putIndex 以及 count 都加载进 Cache。下图是某个时刻 CPU 中 Cache 的状况,为了简化,缓存行中我们仅列出了 takeIndex 和 putIndex。 假设线程 A 运行在 CPU-1 上,执行入队操作,入队操作会修改 putIndex,而修改 putIndex 会导致其所在的所有核上的缓存行均失效;此时假设运行在 CPU-2 上的线程执行出队操作,出队操作需要读取 takeIndex,由于 takeIndex 所在的缓存行已经失效,所以 CPU-2 必须从内存中重新读取。入队操作本不会修改 takeIndex,但是由于 takeIndex 和 putIndex 共享的是一个缓存行,就导致出队操作不能很好地利用 Cache,这其实就是伪共享。简单来讲,伪共享指的是由于共享缓存行导致缓存无效的场景。 ArrayBlockingQueue 的入队和出队操作是用锁来保证互斥的,所以入队和出队不会同时发生。如果允许入队和出队同时发生,那就会导致线程 A 和线程 B 争用同一个缓存行,这样也会导致性能问题。所以为了更好地利用缓存,我们必须避免伪共享,那如何避免呢? 方案很简单,每个变量独占一个缓存行、不共享缓存行就可以了,具体技术是缓存行填充。比如想让 takeIndex 独占一个缓存行,可以在 takeIndex 的前后各填充 56 个字节,这样就一定能保证 takeIndex 独占一个缓存行。下面的示例代码出自 Disruptor,Sequence 对象中的 value 属性就能避免伪共享,因为这个属性前后都填充了 56 个字节。Disruptor 中很多对象,例如 RingBuffer、RingBuffer 内部的数组都用到了这种填充技术来避免伪共享。 1234567891011121314// 前:填充 56 字节class LhsPadding{ long p1, p2, p3, p4, p5, p6, p7;}class Value extends LhsPadding{ volatile long value;}// 后:填充 56 字节class RhsPadding extends Value{ long p9, p10, p11, p12, p13, p14, p15;}class Sequence extends RhsPadding{ // 省略实现} Disruptor 中的无锁算法ArrayBlockingQueue 是利用管程实现的,中规中矩,生产、消费操作都需要加锁,实现起来简单,但是性能并不十分理想。Disruptor 采用的是无锁算法,很复杂,但是核心无非是生产和消费两个操作。Disruptor 中最复杂的是入队操作,所以我们重点来看看入队操作是如何实现的。 对于入队操作,最关键的要求是不能覆盖没有消费的元素;对于出队操作,最关键的要求是不能读取没有写入的元素,所以 Disruptor 中也一定会维护类似出队索引和入队索引这样两个关键变量。Disruptor 中的 RingBuffer 维护了入队索引,但是并没有维护出队索引,这是因为在 Disruptor 中多个消费者可以同时消费,每个消费者都会有一个出队索引,所以 RingBuffer 的出队索引是所有消费者里面最小的那一个。 下面是 Disruptor 生产者入队操作的核心代码,看上去很复杂,其实逻辑很简单:如果没有足够的空余位置,就出让 CPU 使用权,然后重新计算;反之则用 CAS 设置入队索引。 123456789101112131415161718192021222324252627// 生产者获取 n 个写入位置do { //cursor 类似于入队索引,指的是上次生产到这里 current = cursor.get(); // 目标是在生产 n 个 next = current + n; // 减掉一个循环 long wrapPoint = next - bufferSize; // 获取上一次的最小消费位置 long cachedGatingSequence = gatingSequenceCache.get(); // 没有足够的空余位置 if (wrapPoint>cachedGatingSequence || cachedGatingSequence>current){ // 重新计算所有消费者里面的最小值位置 long gatingSequence = Util.getMinimumSequence( gatingSequences, current); // 仍然没有足够的空余位置,出让 CPU 使用权,重新执行下一循环 if (wrapPoint > gatingSequence){ LockSupport.parkNanos(1); continue; } // 从新设置上一次的最小消费位置 gatingSequenceCache.set(gatingSequence); } else if (cursor.compareAndSet(current, next)){ // 获取写入位置成功,跳出循环 break; }} while (true); 总结Disruptor 在优化并发性能方面可谓是做到了极致,优化的思路大体是两个方面,一个是利用无锁算法避免锁的争用,另外一个则是将硬件(CPU)的性能发挥到极致。尤其是后者,在 Java 领域基本上属于经典之作了。 发挥硬件的能力一般是 C 这种面向硬件的语言常干的事儿,C 语言领域经常通过调整内存布局优化内存占用,而 Java 领域则用的很少,原因在于 Java 可以智能地优化内存布局,内存布局对 Java 程序员的透明的。这种智能的优化大部分场景是很友好的,但是如果你想通过填充方式避免伪共享就必须绕过这种优化,关于这方面 Disruptor 提供了经典的实现,你可以参考。 由于伪共享问题如此重要,所以 Java 也开始重视它了,比如 Java 8 中,提供了避免伪共享的注解:@sun.misc.Contended,通过这个注解就能轻松避免伪共享(需要设置 JVM 参数 -XX:-RestrictContended)。不过避免伪共享是以牺牲内存为代价的,所以具体使用的时候还是需要仔细斟酌。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[31 | Guarded Suspension模式:等待唤醒机制的规范实现]]></title>
<url>%2F2019%2F07%2F04%2Fjava%2FGuarded%20Suspension%E6%A8%A1%E5%BC%8F%EF%BC%9A%E7%AD%89%E5%BE%85%E5%94%A4%E9%86%92%E6%9C%BA%E5%88%B6%E7%9A%84%E8%A7%84%E8%8C%83%E5%AE%9E%E7%8E%B0%2F</url>
<content type="text"><![CDATA[31 | Guarded Suspension模式:等待唤醒机制的规范实现这是一个在业务场景中很常见的消息队列使用方式: 用户通过浏览器发过来一个请求,会被转换成一个异步消息发送给 MQ,等 MQ 返回结果后,再将这个结果返回至浏览器。小灰同学的问题是:给 MQ 发送消息的线程是处理 Web 请求的线程 T1,但消费 MQ 结果的线程并不是线程 T1,那线程 T1 如何等待 MQ 的返回结果呢?为了便于你理解这个场景,我将其代码化了,示例代码如下。 123456789101112131415161718192021222324class Message{ String id; String content;}// 该方法可以发送消息void send(Message msg){ // 省略相关代码}//MQ 消息返回后会调用该方法// 该方法的执行线程不同于// 发送消息的线程void onMessage(Message msg){ // 省略相关代码}// 处理浏览器发来的请求Respond handleWebReq(){ // 创建一消息 Message msg1 = new Message("1","{...}"); // 发送消息 send(msg1); // 如何等待 MQ 返回的消息呢? String result = ...;} 看到这里,相信你一定有点似曾相识的感觉,这不就是前面我们在《15 | Lock 和 Condition(下):Dubbo 如何用管程实现异步转同步?》中曾介绍过的异步转同步问题吗?仔细分析,的确是这样,不过在那一篇文章中我们只是介绍了最终方案,让你知其然,但是并没有介绍这个方案是如何设计出来的,今天咱们再仔细聊聊这个问题,让你知其所以然,遇到类似问题也能自己设计出方案来。 Guarded Suspension 模式上面小灰遇到的问题,在现实世界里比比皆是,只是我们一不小心就忽略了。比如,项目组团建要外出聚餐,我们提前预订了一个包间,然后兴冲冲地奔过去,到那儿后大堂经理看了一眼包间,发现服务员正在收拾,就会告诉我们:“您预订的包间服务员正在收拾,请您稍等片刻。”过了一会,大堂经理发现包间已经收拾完了,于是马上带我们去包间就餐。 我们等待包间收拾完的这个过程和小灰遇到的等待 MQ 返回消息本质上是一样的,都是等待一个条件满足:就餐需要等待包间收拾完,小灰的程序里要等待 MQ 返回消息。 那我们来看看现实世界里是如何解决这类问题的呢?现实世界里大堂经理这个角色很重要,我们是否等待,完全是由他来协调的。通过类比,相信你也一定有思路了:我们的程序里,也需要这样一个大堂经理。的确是这样,那程序世界里的大堂经理该如何设计呢?其实设计方案前人早就搞定了,而且还将其总结成了一个设计模式:Guarded Suspension。所谓 Guarded Suspension,直译过来就是“保护性地暂停”。那下面我们就来看看,Guarded Suspension 模式是如何模拟大堂经理进行保护性地暂停的。 下图就是 Guarded Suspension 模式的结构图,非常简单,一个对象 GuardedObject,内部有一个成员变量——受保护的对象,以及两个成员方法——get(Predicate p)和onChanged(T obj)方法。其中,对象 GuardedObject 就是我们前面提到的大堂经理,受保护对象就是餐厅里面的包间;受保护对象的 get() 方法对应的是我们的就餐,就餐的前提条件是包间已经收拾好了,参数 p 就是用来描述这个前提条件的;受保护对象的 onChanged() 方法对应的是服务员把包间收拾好了,通过 onChanged() 方法可以 fire 一个事件,而这个事件往往能改变前提条件 p 的计算结果。下图中,左侧的绿色线程就是需要就餐的顾客,而右侧的蓝色线程就是收拾包间的服务员。 123456789101112131415161718192021222324252627282930313233343536class GuardedObject<T>{ // 受保护的对象 T obj; final Lock lock = new ReentrantLock(); final Condition done = lock.newCondition(); final int timeout=1; // 获取受保护对象 T get(Predicate<T> p) { lock.lock(); try { //MESA 管程推荐写法 while(!p.test(obj)){ done.await(timeout, TimeUnit.SECONDS); } }catch(InterruptedException e){ throw new RuntimeException(e); }finally{ lock.unlock(); } // 返回非空的受保护对象 return obj; } // 事件通知方法 void onChanged(T obj) { lock.lock(); try { this.obj = obj; done.signalAll(); } finally { lock.unlock(); } }} 扩展 Guarded Suspension 模式上面我们介绍了 Guarded Suspension 模式及其实现,这个模式能够模拟现实世界里大堂经理的角色,那现在我们再来看看这个“大堂经理”能否解决小灰同学遇到的问题。 Guarded Suspension 模式里 GuardedObject 有两个核心方法,一个是 get() 方法,一个是 onChanged() 方法。很显然,在处理 Web 请求的方法 handleWebReq() 中,可以调用 GuardedObject 的 get() 方法来实现等待;在 MQ 消息的消费方法 onMessage() 中,可以调用 GuardedObject 的 onChanged() 方法来实现唤醒。 123456789101112131415161718// 处理浏览器发来的请求Respond handleWebReq(){ // 创建一消息 Message msg1 = new Message("1","{...}"); // 发送消息 send(msg1); // 利用 GuardedObject 实现等待 GuardedObject<Message> go =new GuardObjec<>(); Message r = go.get( t->t != null);}void onMessage(Message msg){ // 如何找到匹配的 go? GuardedObject<Message> go=??? go.onChanged(msg);} 但是在实现的时候会遇到一个问题,handleWebReq() 里面创建了 GuardedObject 对象的实例 go,并调用其 get() 方等待结果,那在 onMessage() 方法中,如何才能够找到匹配的 GuardedObject 对象呢?这个过程类似服务员告诉大堂经理某某包间已经收拾好了,大堂经理如何根据包间找到就餐的人。现实世界里,大堂经理的头脑中,有包间和就餐人之间的关系图,所以服务员说完之后大堂经理立刻就能把就餐人找出来。 我们可以参考大堂经理识别就餐人的办法,来扩展一下 Guarded Suspension 模式,从而使它能够很方便地解决小灰同学的问题。在小灰的程序中,每个发送到 MQ 的消息,都有一个唯一性的属性 id,所以我们可以维护一个 MQ 消息 id 和 GuardedObject 对象实例的关系,这个关系可以类比大堂经理大脑里维护的包间和就餐人的关系。 有了这个关系,我们来看看具体如何实现。下面的示例代码是扩展 Guarded Suspension 模式的实现,扩展后的 GuardedObject 内部维护了一个 Map,其 Key 是 MQ 消息 id,而 Value 是 GuardedObject 对象实例,同时增加了静态方法 create() 和 fireEvent();create() 方法用来创建一个 GuardedObject 对象实例,并根据 key 值将其加入到 Map 中,而 fireEvent() 方法则是模拟的大堂经理根据包间找就餐人的逻辑。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253class GuardedObject<T>{ // 受保护的对象 T obj; final Lock lock = new ReentrantLock(); final Condition done = lock.newCondition(); final int timeout=2; // 保存所有 GuardedObject final static Map<Object, GuardedObject> gos=new ConcurrentHashMap<>(); // 静态方法创建 GuardedObject static <K> GuardedObject create(K key){ GuardedObject go=new GuardedObject(); gos.put(key, go); return go; } static <K, T> void fireEvent(K key, T obj){ GuardedObject go=gos.remove(key); if (go != null){ go.onChanged(obj); } } // 获取受保护对象 T get(Predicate<T> p) { lock.lock(); try { //MESA 管程推荐写法 while(!p.test(obj)){ done.await(timeout, TimeUnit.SECONDS); } }catch(InterruptedException e){ throw new RuntimeException(e); }finally{ lock.unlock(); } // 返回非空的受保护对象 return obj; } // 事件通知方法 void onChanged(T obj) { lock.lock(); try { this.obj = obj; done.signalAll(); } finally { lock.unlock(); } }} 这样利用扩展后的 GuardedObject 来解决小灰同学的问题就很简单了,具体代码如下所示。 1234567891011121314151617181920// 处理浏览器发来的请求Respond handleWebReq(){ int id= 序号生成器.get(); // 创建一消息 Message msg1 = new Message(id,"{...}"); // 创建 GuardedObject 实例 GuardedObject<Message> go= GuardedObject.create(id); // 发送消息 send(msg1); // 等待 MQ 消息 Message r = go.get( t->t != null); }void onMessage(Message msg){ // 唤醒等待的线程 GuardedObject.fireEvent( msg.id, msg);} 总结Guarded Suspension 模式本质上是一种等待唤醒机制的实现,只不过 Guarded Suspension 模式将其规范化了。规范化的好处是你无需重头思考如何实现,也无需担心实现程序的可理解性问题,同时也能避免一不小心写出个 Bug 来。但 Guarded Suspension 模式在解决实际问题的时候,往往还是需要扩展的,扩展的方式有很多,本篇文章就直接对 GuardedObject 的功能进行了增强,Dubbo 中 DefaultFuture 这个类也是采用的这种方式,你可以对比着来看,相信对 DefaultFuture 的实现原理会理解得更透彻。当然,你也可以创建新的类来实现对 Guarded Suspension 模式的扩展。 Guarded Suspension 模式也常被称作 Guarded Wait 模式、Spin Lock 模式(因为使用了 while 循环去等待),这些名字都很形象,不过它还有一个更形象的非官方名字:多线程版本的 if。单线程场景中,if 语句是不需要等待的,因为在只有一个线程的条件下,如果这个线程被阻塞,那就没有其他活动线程了,这意味着 if 判断条件的结果也不会发生变化了。但是多线程场景中,等待就变得有意义了,这种场景下,if 判断条件的结果是可能发生变化的。所以,用“多线程版本的 if”来理解这个模式会更简单。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[06 | 用“等待-通知”机制优化循环等待]]></title>
<url>%2F2019%2F06%2F21%2Fjava%2F%E7%94%A8%E2%80%9C%E7%AD%89%E5%BE%85-%E9%80%9A%E7%9F%A5%E2%80%9D%E6%9C%BA%E5%88%B6%E4%BC%98%E5%8C%96%E5%BE%AA%E7%8E%AF%E7%AD%89%E5%BE%85%2F</url>
<content type="text"><![CDATA[06 | 用“等待-通知”机制优化循环等待由上文可知:在破坏占用且等待条件的时候,如果转出账本和转入账本不满足同时在文件架上这个条件,就用死循环的方式来循环等待,核心代码如下: 123// 一次性申请转出账户和转入账户,直到成功while(!actr.apply(this, target)) ; 这种循环等待的方式极其浪费CPU,如果并发冲突大,可能要循环上万次才能取到锁。 其实在这种场景下,最好的方案应该是:如果线程要求的条件(转出账本和转入账本同在文件架上)不满足,则线程阻塞自己,进入等待状态,当线程要求的条件满足后,通知等待的线程重新执行。其中,使用线程阻塞的方式就能避免循环等待消耗CPU的问题。 下面我们看看Java语言是如何支持等待——通知机制的。 就医流程我们先看一个现实世界里面的就医流程,它有着完善的等待——通知机制,所以对比就医流程,我们可以更好地理解等待——通知机制。 就医流程基本上是这样: 患者先去挂号,然后到就诊门口分诊,等待叫号; 当叫到自己的号时,患者就可以找大夫就诊了; 就诊过程中,大夫可能会让患者去做检查,同时叫下一位患者; 当患者做完检查后,拿检测报告重新分诊,等待叫号; 当大夫再次叫到自己的号时,患者再去找大夫就诊。 这个就诊流程能保证同一时刻大夫只为一个患者服务,而且还能保证大夫和患者的效率,不会让患者在大夫诊室门口傻等。下面我们把Java的等待——通知机制和就诊流程对比起来,并关注一下上面忽视的细节: 患者到就诊室门口分诊,类似于线程要去获取互斥锁;当患者被叫到时,类似线程已经获取到锁了; 大夫让患者去做检查(缺少检测报告不能诊断病因),类似于线程要求的条件没有被满足; 患者去做检查,类似于线程进入等待状态;然后大夫叫下一个患者,这个步骤对应到程序里,本质是线程释放持有的互斥锁; 患者做完检查,类似于线程要求的条件已经满足;患者拿检测报告重新分诊,类似于线程需要重新获取互斥锁。 所以加上这些至关重要的细节,综合一下,就可以得出一个完整的等待——通知机制:线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;单线程要求的条件满足时,通知等待的线程,重新获取互斥锁。 用 synchronized 实现等待——通知机制在 Java 中,等待——通知机制可以有多种实现方式,比如内置的 synchronized 配合 wait()、notify()、notifyAll() 这三个方法就能轻松实现。 在下面这个图里,左边有一个等待队列,同一时刻,只允许一个线程进入 synchronized 保护的临界区(这个临界区可以看作大夫的诊室),当有一个线程进入临界区后,其他线程就只能进入图中左边的等待队列里等待(相当于患者分诊等待)。这个等待队列和互斥锁是一对一的关系,每个互斥锁都有自己独立的等待队列。 在并发程序中,当一个线程进入临界区后,由于某些条件不满足,需要进入等待状态,Java 对象的 wait() 方法就能够满足这种需求。如上图所示,当调用 wait() 方法后,当前线程就会被阻塞,并且进入到右边的等待队列中,这个等待队列也是互斥锁的等待队列。线程在进入等待队列的同时,会释放持有的互斥锁,线程释放锁后,其他线程就有机会获得锁,并进入临界区了。 那线程要求的条件满足时,该怎么通知这个等待的线程呢?很简单,就是 Java 对象的 notify() 和 notifyAll() 方法。当条件满足时调用 notify(),会通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经满足过。 为什么说是曾经满足过呢?因为 notify() 只能保证在通知时间点,条件是满足的。而被通知线程的执行时间点和通知的时间点基本上不会重合,所以当线程执行的时候,很可能条件已经不满足了(保不齐有其他线程插队)。这一点需要格外注意。 除此之外,还有一个需要注意的点,被通知的线程要想重新执行,仍然需要获取到互斥锁(因为曾经获取的锁在调用 wait() 时已经释放了)。 上面我们一直强调 wait()、notify()、notifyAll() 方法操作的等待队列是互斥锁的等待队列,所以如果 synchronized 锁定的是 this,那么对应的一定是 this.wait()、this.notify()、this.notifyAll();如果 synchronized 锁定的是 target,那么对应的一定是 target.wait()、target.notify()、target.notifyAll()。而且 wait()、notify()、notifyAll() 这三个方法能够被调用的前提是已经获取了相应的互斥锁,所以我们会发现 wait()、notify()、notifyAll() 都是在 synchronized{}内部被调用的。如果在 synchronized{} 外部调用,或者锁定的 this,而用 target.wait() 调用的话,JVM 会抛出一个运行时异常:java.lang.IllegalMonitorStateException。 小试牛刀:一个更好地资源分配器等待 - 通知机制的基本原理搞清楚后,我们就来看看它如何解决一次性申请转出账户和转入账户的问题吧。在这个等待 - 通知机制中,我们需要考虑以下四个要素。 互斥锁:上一篇文章我们提到 Allocator 需要是单例的,所以我们可以用 this 作为互斥锁。 线程要求的条件:转出账户和转入账户都没有被分配过。 何时等待:线程要求的条件不满足就等待。 何时通知:当有线程释放账户时就通知。 将上面几个问题考虑清楚,可以快速完成下面的代码。需要注意的是我们使用了: 123while(条件不满足) { wait();} 利用这种范式可以解决上面提到的条件曾经满足过这个问题。因为当wait() 返回时,有可能条件已经发生变化了,曾经条件满足,但是现在已经不满足了,所以要重新检验条件是否满足。范式,意味着是经典做法,所以没有特殊理由不要尝试换个写法。 123456789101112131415161718192021class Allocator { private List<Object> als; // 一次性申请所有资源 synchronized void apply(Object from, Object to){ // 经典写法 while(als.contains(from) || als.contains(to)){ try{ wait(); }catch(Exception e){ } } als.add(from); als.add(to); } // 归还资源 synchronized void free(Object from, Object to){ als.remove(from); als.remove(to); notifyAll(); }} 注意:尽量使用 notifyAll(),notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程。使用 notify() 也很有风险,它的风险在于可能导致某些线程永远不会被通知到。 课后思考很多面试都会问到,wait() 方法和 sleep() 方法都能让当前线程挂起一段时间,那它们的区别是什么? wait与sleep区别在于: wait会释放所有锁而sleep不会释放锁资源; wait只能在同步方法和同步块中使用,而sleep任何地方都可以; wait无需捕捉异常,而sleep需要; 两者相同点:都会让渡CPU执行时间,等待再次调度!]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[在 MAC 上使用 Docker 安装 PostgreSQL、Oracle、SQLServer]]></title>
<url>%2F2019%2F06%2F18%2Fdocker%2F%E5%9C%A8%20MAC%20%E4%B8%8A%E4%BD%BF%E7%94%A8%20Docker%20%E5%AE%89%E8%A3%85%20PostgreSQL%E3%80%81Oracle%E3%80%81SQLServer%2F</url>
<content type="text"><![CDATA[在 MAC 上使用 Docker 安装 PostgreSQL、Oracle、SQLServer启动 Docker 后续的都是控制台的操作 安装 PostgreSQL参考 https://www.jianshu.com/p/900345a369aa 拉取镜像 123456789101112131415161718> docker pull postgres:9.69.6: Pulling from library/postgresfc7181108d40: Pull complete81cfa12d39e9: Pull complete793d305ca761: Pull complete41e3ced3a2aa: Pull completea300bc9d5405: Pull complete3c6a5c3830ed: Pull completefb8c79b24338: Pull completefcda1144379f: Pull complete1b6bdda9a7d0: Pull complete3f44e35d537d: Pull complete9161ffee7255: Pull completeb6569cb49b8b: Pull complete21e1e65283f8: Pull complete4cbcde26ced5: Pull completeDigest: sha256:de03034bae132f5cc0672f6c1a0a3a5591a0640cad6b21f2b0d00fc0ce15ab15Status: Downloaded newer image for postgres:9.6 创建本地卷 12> docker volume create pgdatapgdata 启动容器 12345678> docker run -it --rm -v pgdata:/var/lib/postgresql/data -p 5432:5432 postgres:9.6The files belonging to this database system will be owned by user "postgres".This user must also own the server process.The database cluster will be initialized with locale "en_US.utf8".The default database encoding has accordingly been set to "UTF8".The default text search configuration will be set to "english".... 进入容器创建数据 查看该容器 ID 1> docker ps 假设该容器 ID 为 123 ,新开一个 Terminal 进入容器 1> docker exec -it 123 /bin/bash 后续就可以做创建用户,创建表相关的操作了。 使用 Navicat 连接 12345host: localhostport: 5432initial database: postgresusername: postgrespassword: 123456 安装 Oracle参考 https://blog.csdn.net/master_shifu_/article/details/80790218 拉取镜像 1docker pull oracleinanutshell/oracle-xe-11g 启动容器 1docker run -d -p 9090:8080 -p 1521:1521 oracleinanutshell/oracle-xe-11g 命令解释:将容器中的Oracle XE 管理界面的8080端口映射为本机的9090端口,将Oracle XE的1521端口映射为本机的1521端口 使用 Navicat 连接 12345host: localhostport: 1521SID : XEusername: system/syspassword: oracle 安装 SQLServer参考 https://segmentfault.com/a/1190000014232366 拉取镜像 1docker pull microsoft/mssql-server-linux 创建并运行容器 1docker run --name MSSQL_1433 -m 512m -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=YourStrong(!)Password' -p 1433:1433 -d microsoft/mssql-server-linux 替换YourStrong(!)Password成你自己的密码,这个密码需要复杂密码,要有大小写和特殊符号。 登入容器 1docker exec -it MSSQL_1433 /bin/bash 连接到sqlcmd 1/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'YourStrong(!)Password' 执行SQL语句创建数据库 12CREATE DATABASE test_dbgo 使用 Navicat 连接 12345host: localhostport: 1433initial database: masterusername: SApassword: YourStrong(!)Password]]></content>
<categories>
<category>docker</category>
</categories>
</entry>
<entry>
<title><![CDATA[35 | 两阶段终止模式:如何优雅地终止线程?]]></title>
<url>%2F2019%2F06%2F12%2Fjava%2F%E4%B8%A4%E9%98%B6%E6%AE%B5%E7%BB%88%E6%AD%A2%E6%A8%A1%E5%BC%8F%EF%BC%9A%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E5%9C%B0%E7%BB%88%E6%AD%A2%E7%BA%BF%E7%A8%8B%2F</url>
<content type="text"><![CDATA[35 | 两阶段终止模式:如何优雅地终止线程?前面两篇文章我们讲述的内容,从纯技术的角度看,都是启动多线程去执行一个异步任务。既然有启动,那又该如何终止呢?今天咱们就从技术的角度聊聊如何优雅地终止线程,正所谓有始有终。 在《09 | Java 线程(上):Java 线程的生命周期》中,我曾讲过:线程执行完或者出现异常就会进入终止状态。这样看,终止一个线程看上去很简单啊!一个线程执行完自己的任务,自己进入终止状态,这的确很简单。不过我们今天谈到的“优雅地终止线程”,不是自己终止自己,而是在一个线程 T1 中,终止线程 T2;这里所谓的“优雅”,指的是给 T2 一个机会料理后事,而不是被一剑封喉。 Java 语言的 Thread 类中曾经提供了一个 stop() 方法,用来终止线程,可是早已不建议使用了,原因是这个方法用的就是一剑封喉的做法,被终止的线程没有机会料理后事。 既然不建议使用 stop() 方法,那在 Java 领域,我们又该如何优雅地终止线程呢? 如何理解两阶段终止模式前辈们经过认真对比分析,已经总结出了一套成熟的方案,叫做两阶段终止模式。顾名思义,就是将终止过程分成两个阶段,其中第一个阶段主要是线程 T1 向线程 T2 发送终止指令,而第二阶段则是线程 T2 响应终止指令。 从这里其实就能听出来 T1 向 T2 发送终止指令后 T2 并不是立马就终止的。 那在 Java 语言里,终止指令是什么呢?这个要从 Java 线程的状态转换过程说起。我们在《09 | Java 线程(上):Java 线程的生命周期》中曾经提到过 Java 线程的状态转换图,如下图所示。 从这个图里你会发现,Java 线程进入终止状态的前提是线程进入 RUNNABLE 状态,而实际上我们想要终止一个线程时其可能处在休眠状态,也就是说,我们要想终止一个线程,首先要把线程的状态从休眠状态转换到 RUNNABLE 状态。如何做到呢?这个要靠 Java Thread 类提供的interrupt() 方法,它可以将休眠状态的线程转换到 RUNNABLE 状态。 线程转换到 RUNNABLE 状态之后,我们如何再将其终止呢?RUNNABLE 状态转换到终止状态,优雅的方式是让 Java 线程自己执行完 run() 方法,所以一般我们采用的方法是设置一个标志位,然后线程会在合适的时机检查这个标志位,如果发现符合终止条件,则自动退出 run() 方法。这个过程其实就是我们前面提到的第二阶段:响应终止指令。 综合上面这两点,我们能总结出终止指令,其实包括两方面内容:interrupt() 方法和线程终止的标志位。 理解了两阶段终止模式之后,下面我们看一个实际工作中的案例。 用两阶段终止模式终止监控操作实际工作中,有些监控系统需要动态地采集一些数据,一般都是监控系统发送采集指令给被监控系统的监控代理,监控代理接收到指令之后,从监控目标收集数据,然后回传给监控系统,详细过程如下图所示。出于对性能的考虑(有些监控项对系统性能影响很大,所以不能一直持续监控),动态采集功能一般都会有终止操作。 下面的示例代码是监控代理简化之后的实现,start() 方法会启动一个新的线程 rptThread 来执行监控数据采集和回传的功能,stop() 方法需要优雅地终止线程 rptThread,那 stop() 相关功能该如何实现呢? 12345678910111213141516171819202122232425262728293031class Proxy { boolean started = false; // 采集线程 Thread rptThread; // 启动采集功能 synchronized void start() { // 不允许同时启动多个采集线程 if (started) { return; } started = true; rptThread = new Thread(()->{ while (true) { // 省略采集、回传实现 report(); // 每隔两秒钟采集、回传一次数据 try { Thread.sleep(2000); } catch (InterruptedException e) { } } // 执行到此处说明线程马上终止 started = false; }); rptThread.start(); } // 终止采集功能 synchronized void stop(){ // 如何实现? }} 按照两阶段终止模式,我们首先需要做的就是将线程 rptThread 状态转换到 RUNNABLE,做法很简单,只需要调用 rptThread.interrupt() 就可以了。线程 rptThread 的状态转换到 RUNNABLE 之后,如何优雅地终止呢?下面的示例代码中,我们选择的标志位是线程的中断状态:Thread.currentThread().isInterrupted(),需要注意的是,我们在捕获 Thread.sleep() 的中断异常之后,通过 Thread.currentThread().interrupt() 重新设置了线程的中断状态,因为 JVM 的异常处理会清除线程的中断状态。 123456789101112131415161718192021222324252627282930313233class Proxy { boolean started = false; // 采集线程 Thread rptThread; // 启动采集功能 synchronized void start(){ // 不允许同时启动多个采集线程 if (started) { return; } started = true; rptThread = new Thread(()->{ while (!Thread.currentThread().isInterrupted()) { // 省略采集、回传实现 report(); // 每隔两秒钟采集、回传一次数据 try { Thread.sleep(2000); } catch (InterruptedException e){ // 重新设置线程中断状态 Thread.currentThread().interrupt(); } } // 执行到此处说明线程马上终止 started = false; }); rptThread.start(); } // 终止采集功能 synchronized void stop(){ rptThread.interrupt(); }} 上面的示例代码的确能够解决当前的问题,但是建议你在实际工作中谨慎使用。原因在于我们很可能在线程的 run() 方法中调用第三方类库提供的方法,而我们没有办法保证第三方类库正确处理了线程的中断异常,例如第三方类库在捕获到 Thread.sleep() 方法抛出的中断异常后,没有重新设置线程的中断状态,那么就会导致线程不能够正常终止。所以强烈建议你设置自己的线程终止标志位,例如在下面的代码中,使用 isTerminated 作为线程终止标志位,此时无论是否正确处理了线程的中断异常,都不会影响线程优雅地终止。 123456789101112131415161718192021222324252627282930313233343536373839class Proxy { // 线程终止标志位 volatile boolean terminated = false; boolean started = false; // 采集线程 Thread rptThread; // 启动采集功能 synchronized void start(){ // 不允许同时启动多个采集线程 if (started) { return; } started = true; terminated = false; rptThread = new Thread(()->{ while (!terminated) { // 省略采集、回传实现 report(); // 每隔两秒钟采集、回传一次数据 try { Thread.sleep(2000); } catch (InterruptedException e) { // 重新设置线程中断状态 Thread.currentThread().interrupt(); } } // 执行到此处说明线程马上终止 started = false; }); rptThread.start(); } // 终止采集功能 synchronized void stop(){ // 设置中断标志位 terminated = true; // 中断线程 rptThread rptThread.interrupt(); }} 如何优雅地终止线程池Java 领域用的最多的还是线程池,而不是手动地创建线程。那我们该如何优雅地终止线程池呢? 线程池提供了两个方法:shutdown()和shutdownNow()。这两个方法有什么区别呢?要了解它们的区别,就先需要了解线程池的实现原理。 我们曾经讲过,Java 线程池是生产者 - 消费者模式的一种实现,提交给线程池的任务,首先是进入一个阻塞队列中,之后线程池中的线程从阻塞队列中取出任务执行。 shutdown() 方法是一种很保守的关闭线程池的方法。线程池执行 shutdown() 后,就会拒绝接收新的任务,但是会等待线程池中正在执行的任务和已经进入阻塞队列的任务都执行完之后才最终关闭线程池。 而 shutdownNow() 方法,相对就激进一些了,线程池执行 shutdownNow() 后,会拒绝接收新的任务,同时还会中断线程池中正在执行的任务,已经进入阻塞队列的任务也被剥夺了执行的机会,不过这些被剥夺执行机会的任务会作为 shutdownNow() 方法的返回值返回。因为 shutdownNow() 方法会中断正在执行的线程,所以提交到线程池的任务,如果需要优雅地结束,就需要正确地处理线程中断。 如果提交到线程池的任务不允许取消,那就不能使用 shutdownNow() 方法终止线程池。不过,如果提交到线程池的任务允许后续以补偿的方式重新执行,也是可以使用 shutdownNow() 方法终止线程池的。《Java 并发编程实战》这本书第 7 章《取消与关闭》的“shutdownNow 的局限性”一节中,提到一种将已提交但尚未开始执行的任务以及已经取消的正在执行的任务保存起来,以便后续重新执行的方案,你可以参考一下,方案很简单,这里就不详细介绍了。 其实分析完 shutdown() 和 shutdownNow() 方法你会发现,它们实质上使用的也是两阶段终止模式,只是终止指令的范围不同而已,前者只影响阻塞队列接收任务,后者范围扩大到线程池中所有的任务。 总结两阶段终止模式是一种应用很广泛的并发设计模式,在 Java 语言中使用两阶段终止模式来优雅地终止线程,需要注意两个关键点:一个是仅检查终止标志位是不够的,因为线程的状态可能处于休眠态;另一个是仅检查线程的中断状态也是不够的,因为我们依赖的第三方类库很可能没有正确处理中断异常。 当你使用 Java 的线程池来管理线程的时候,需要依赖线程池提供的 shutdown() 和 shutdownNow() 方法来终止线程池。不过在使用时需要注意它们的应用场景,尤其是在使用 shutdownNow() 的时候,一定要谨慎。 补充当我们调用线程池的 shutdownNow 时,如果线程正在执行 getTask 方法,则会通过 for 循环进入到 if 语句,于是 getTask 返回 null,从而线程退出。不管线程池里是否有未完成的任务。 如果线程因为执行提交到线程池里的任务而处于阻塞状态,则会导致报错(如果任务里没有捕获 InterruptedException 异常),否则线程会执行完当前任务,然后通过 getTask 方法返回为null来退出。 当我们调用线程池的 shuwdown 方法时,如果线程正在执行线程池里的任务,即便任务处于阻塞状态,线程也不会被中断,而是继续执行。 如果线程池阻塞等待从队列里读取任务,则会被唤醒,但是会继续判断队列是否为空,如果不为空会继续从队列里读取任务,为空则线程退出。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[08 | 事务到底是隔离的还是不隔离的?]]></title>
<url>%2F2019%2F06%2F07%2Fmysql%2F%E4%BA%8B%E5%8A%A1%E5%88%B0%E5%BA%95%E6%98%AF%E9%9A%94%E7%A6%BB%E7%9A%84%E8%BF%98%E6%98%AF%E4%B8%8D%E9%9A%94%E7%A6%BB%E7%9A%84%2F</url>
<content type="text"><![CDATA[08 | 事务到底是隔离的还是不隔离的?我在第 3 篇文章和你讲事务隔离级别的时候提到过,如果是可重复读隔离级别,事务 T 启动的时候会创建一个视图 read-view,之后事务 T 执行期间,即使有其他事务修改了数据,事务 T 看到的仍然跟在启动时看到的一样。也就是说,一个在可重复读隔离级别下执行的事务,好像与世无争,不受外界影响。 但是,我在上一篇文章中,和你分享行锁的时候又提到,一个事务要更新一行,如果刚好有另外一个事务拥有这一行的行锁,它又不能这么超然了,会被锁住,进入等待状态。问题是,既然进入了等待状态,那么等到这个事务自己获取到行锁要更新数据的时候,它读到的值又是什么呢? 我给你举一个例子吧。下面是一个只有两行的表的初始化语句。 123456mysql> CREATE TABLE `t` ( `id` int(11) NOT NULL, `k` int(11) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;insert into t(id, k) values(1,1),(2,2); 这里,我们需要注意的是事务的启动时机。 begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。 第一种启动方式,一致性视图是在第执行第一个快照读语句时创建的;第二种启动方式,一致性视图是在执行 start transaction with consistent snapshot 时创建的。 还需要注意的是,在整个专栏里面,我们的例子中如果没有特别说明,都是默认 autocommit=1。 在这个例子中,事务 C 没有显式地使用 begin/commit,表示这个 update 语句本身就是一个事务,语句完成的时候会自动提交。事务 B 在更新了行之后查询 ; 事务 A 在一个只读事务中查询,并且时间顺序上是在事务 B 的查询之后。 这时,如果我告诉你事务 B 查到的 k 的值是 3,而事务 A 查到的 k 的值是 1,你是不是感觉有点晕呢? 确实晕。 所以,今天这篇文章,我其实就是想和你说明白这个问题,希望借由把这个疑惑解开的过程,能够帮助你对 InnoDB 的事务和锁有更进一步的理解。 在 MySQL 里,有两个“视图”的概念: 一个是 view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是 create view … ,而它的查询方法与表一样。 另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。 它没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”。 在前文中,我跟你解释过一遍 MVCC 的实现逻辑。今天为了说明查询和更新的区别,我换一个方式来说明,把 read view 拆开。你可以结合这两篇文章的说明来更深一步地理解 MVCC。 “快照”在 MVCC 里是怎么工作的?在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。 这时,你会说这看上去不太现实啊。如果一个库有 100G,那么我启动一个事务,MySQL 就要拷贝 100G 的数据出来,这个过程得多慢啊。可是,我平时的事务执行起来很快啊。 实际上,我们并不需要拷贝出这 100G 的数据。我们先来看看这个快照是怎么实现的。 InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。 而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。 也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。 如图 2 所示,就是一个记录被多个事务连续更新后的状态。 图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。 你可能会问,前面的文章不是说,语句更新会生成 undo log(回滚日志)吗?那么,undo log 在哪呢? 实际上,图 2 中的三个虚线箭头,就是 undo log;而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。 明白了多版本和 row trx_id 的概念后,我们再来想一下,InnoDB 是怎么定义那个“100G”的快照的。 按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。 因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。 当然,如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数据,它自己还是要认的。 在实现上,InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。 数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。 这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。 而数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到的。 这个视图数组把所有的 row trx_id 分成了几种不同的情况。 这样,对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能: 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的; 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的; 如果落在黄色部分,那就包括两种情况 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见; 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。 比如,对于图 2 中的数据来说,如果有一个事务,它的低水位是 18,那么当它访问这一行数据时,就会从 V4 通过 U3 计算出 V3,所以在它看来,这一行的值是 11。 你看,有了这个声明后,系统里面随后发生的更新,是不是就跟这个事务看到的内容无关了呢?因为之后的更新,生成的版本一定属于上面的 2 或者 3(a) 的情况,而对它来说,这些新的数据版本是不存在的,所以这个事务的快照,就是“静态”的了。 所以你现在知道了,InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。 接下来,我们继续看一下图 1 中的三个事务,分析下事务 A 的语句返回的结果,为什么是 k=1。 这里,我们不妨做如下假设: 事务 A 开始前,系统里面只有一个活跃事务 ID 是 99; 事务 A、B、C 的版本号分别是 100、101、102,且当前系统里只有这四个事务; 三个事务开始前,(1,1)这一行数据的 row trx_id 是 90。 这样,事务 A 的视图数组就是 [99,100], 事务 B 的视图数组是 [99,100,101], 事务 C 的视图数组是 [99,100,101,102]。 为了简化分析,我先把其他干扰语句去掉,只画出跟事务 A 查询逻辑有关的操作: 从图中可以看到,第一个有效更新是事务 C,把数据从 (1,1) 改成了 (1,2)。这时候,这个数据的最新版本的 row trx_id 是 102,而 90 这个版本已经成为了历史版本。 第二个有效更新是事务 B,把数据从 (1,2) 改成了 (1,3)。这时候,这个数据的最新版本(即 row trx_id)是 101,而 102 又成为了历史版本。 并不是说事务 Id 是按数值大小生效的。 你可能注意到了,在事务 A 查询的时候,其实事务 B 还没有提交,但是它生成的 (1,3) 这个版本已经变成当前版本了。但这个版本对事务 A 必须是不可见的,否则就变成脏读了。 好,现在事务 A 要来读数据了,它的视图数组是 [99,100]。当然了,读数据都是从当前版本读起的。所以,事务 A 查询语句的读数据流程是这样的: 找到 (1,3) 的时候,判断出 row trx_id=101,比高水位大,处于红色区域,不可见; 接着,找到上一个历史版本,一看 row trx_id=102,比高水位大,处于红色区域,不可见; 再往前找,终于找到了(1,1),它的 row trx_id=90,比低水位小,处于绿色区域,可见。 这样执行下来,虽然期间这一行数据被修改过,但是事务 A 不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读。 这个判断规则是从代码逻辑直接转译过来的,但是正如你所见,用于人肉分析可见性很麻烦。 所以,我来给你翻译一下。一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况: 版本未提交,不可见; 版本已提交,但是是在视图创建后提交的,不可见; 版本已提交,而且是在视图创建前提交的,可见。 现在,我们用这个规则来判断图 4 中的查询结果,事务 A 的查询语句的视图数组是在事务 A 启动的时候生成的,这时候: (1,3) 还没提交,属于情况 1,不可见; (1,2) 虽然提交了,但是是在视图数组创建之后提交的,属于情况 2,不可见; (1,1) 是在视图数组创建之前提交的,可见。 你看,去掉数字对比后,只用时间先后顺序来判断,分析起来是不是轻松多了。所以,后面我们就都用这个规则来分析。 更新逻辑细心的同学可能有疑问了:事务 B 的 update 语句,如果按照一致性读,好像结果不对哦? 你看图 5 中,事务 B 的视图数组是先生成的,之后事务 C 才提交,不是应该看不见 (1,2) 吗,怎么能算出 (1,3) 来? 是的,如果事务 B 在更新之前查询一次数据,这个查询返回的 k 的值确实是 1。 但是,当它要去更新数据的时候,就不能再在历史版本上更新了,否则事务 C 的更新就丢失了。因此,事务 B 此时的 set k=k+1 是在(1,2)的基础上进行的操作。 所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。 因此,在更新的时候,当前读拿到的数据是 (1,2),更新后生成了新版本的数据 (1,3),这个新版本的 row trx_id 是 101。 所以,在执行事务 B 查询语句的时候,一看自己的版本号是 101,最新数据的版本号也是 101,是自己的更新,可以直接使用,所以查询得到的 k 的值是 3。 这里我们提到了一个概念,叫作当前读。其实,除了 update 语句外,select 语句如果加锁,也是当前读。 所以,如果把事务 A 的查询语句 select * from t where id=1 修改一下,加上 lock in share mode 或 for update,也都可以读到版本号是 101 的数据,返回的 k 的值是 3。下面这两个 select 语句,就是分别加了读锁(S 锁,共享锁)和写锁(X 锁,排他锁)。 12mysql> select k from t where id=1 lock in share mode;mysql> select k from t where id=1 for update; 再往前一步,假设事务 C 不是马上提交的,而是变成了下面的事务 C’,会怎么样呢? 事务 C’的不同是,更新后并没有马上提交,在它提交前,事务 B 的更新语句先发起了。前面说过了,虽然事务 C’还没提交,但是 (1,2) 这个版本也已经生成了,并且是当前的最新版本。那么,事务 B 的更新语句会怎么处理呢? 这时候,我们在上一篇文章中提到的“两阶段锁协议”就要上场了。事务 C’没提交,也就是说 (1,2) 这个版本上的写锁还没释放。而事务 B 是当前读,必须要读最新版本,而且必须加锁,因此就被锁住了,必须等到事务 C’释放这个锁,才能继续它的当前读。 到这里,我们把一致性读、当前读和行锁就串起来了。 现在,我们再回到文章开头的问题:事务的可重复读的能力是怎么实现的? 可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。 而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是: 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图; 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。 那么,我们再看一下,在读提交隔离级别下,事务 A 和事务 B 的查询语句查到的 k,分别应该是多少呢? 这里需要说明一下,“start transaction with consistent snapshot; ”的意思是从这个语句开始,创建一个持续整个事务的一致性快照。所以,在读提交隔离级别下,这个用法就没意义了,等效于普通的 start transaction。 下面是读提交时的状态图,可以看到这两个查询语句的创建视图数组的时机发生了变化,就是图中的 read view 框。(注意:这里,我们用的还是事务 C 的逻辑直接提交,而不是事务 C’) 这时,事务 A 的查询语句的视图数组是在执行这个语句的时候创建的,时序上 (1,2)、(1,3) 的生成时间都在创建这个视图数组的时刻之前。但是,在这个时刻: (1,3) 还没提交,属于情况 1,不可见; (1,2) 提交了,属于情况 3,可见。 所以,这时候事务 A 查询语句返回的是 k=2。 显然地,事务 B 查询结果 k=3。 小结InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性。 对于可重复读,查询只承认在事务启动前就已经提交完成的数据; 对于读提交,查询只承认在语句启动前就已经提交完成的数据; 而当前读,总是读取已经提交完成的最新版本。 你也可以想一下,为什么表结构不支持“可重复读”?这是因为表结构没有对应的行数据,也没有 row trx_id,因此只能遵循当前读的逻辑。 当然,MySQL 8.0 已经可以把表结构放在 InnoDB 字典里了,也许以后会支持表结构的可重复读。]]></content>
<categories>
<category>MySQL</category>
</categories>
<tags>
<tag>事务</tag>
</tags>
</entry>
<entry>
<title><![CDATA[02 | Java内存模型:看Java如何解决可见性和有序性问题]]></title>
<url>%2F2019%2F06%2F06%2Fjava%2F%E7%9C%8BJava%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%8F%AF%E8%A7%81%E6%80%A7%E5%92%8C%E6%9C%89%E5%BA%8F%E6%80%A7%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[02 | Java内存模型:看Java如何解决可见性和有序性问题什么是 Java 内存模型?你已经知道,导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,我们程序的性能可就堪忧了。 合理的方案应该是按需禁用缓存以及编译优化。那么,如何做到“按需禁用”呢?对于并发程序,何时禁用缓存以及编译优化只有程序员知道,那所谓“按需禁用”其实就是指按照程序员的要求来禁用。所以,为了解决可见性和有序性问题,只需要提供给程序员按需禁用缓存和编译优化的方法即可。 Java 内存模型是个很复杂的规范,可以从不同的视角来解读,站在我们这些程序员的视角,本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则,这也正是本期的重点内容。 使用 volatile 的困惑volatile 关键字并不是 Java 语言的特产,古老的 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。 例如,我们声明一个 volatile 变量 volatile int x = 0,它表达的是:告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。这个语义看上去相当明确,但是在实际使用的时候却会带来困惑。 例如下面的示例代码,假设线程 A 执行 writer() 方法,按照 volatile 语义,会把变量 “v=true” 写入内存;假设线程 B 执行 reader() 方法,同样按照 volatile 语义,线程 B 会从内存中读取变量 v,如果线程 B 看到 “v == true” 时,那么线程 B 看到的变量 x 是多少呢? 直觉上看,应该是 42,那实际应该是多少呢?这个要看 Java 的版本,如果在低于 1.5 版本上运行,x 可能是 42,也有可能是 0;如果在 1.5 以上的版本上运行,x 就是等于 42。 1234567891011121314// 以下代码来源于【参考 1】class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 这里 x 会是多少呢? } }} 分析一下,为什么 1.5 以前的版本会出现 x = 0 的情况呢?我相信你一定想到了,变量 x 可能被 CPU 缓存而导致可见性问题。这个问题在 1.5 版本已经被圆满解决了。Java 内存模型在 1.5 版本对 volatile 语义进行了增强。怎么增强的呢?答案是一项 Happens-Before 规则。 Happens-Before 规则如何理解 Happens-Before 呢?如果望文生义(很多网文也都爱按字面意思翻译成“先行发生”),那就南辕北辙了,Happens-Before 并不是说前面一个操作发生在后续操作的前面,它真正要表达的是:前面一个操作的结果对后续操作是可见的。就像有心灵感应的两个人,虽然远隔千里,一个人心之所想,另一个人都看得到。Happens-Before 规则就是要保证线程之间的这种“心灵感应”。所以比较正式的说法是:Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。 Happens-Before 规则应该是 Java 内存模型里面最晦涩的内容了,和程序员相关的规则一共有如下六项,都是关于可见性的。 1. 程序的顺序性规则这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。 2. volatile 变量规则这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。 3. 传递性这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。 将规则 3 的传递性应用到我们的例子中,会发生什么呢?可以看下面这幅图: 4. 管程中锁的规则这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。 5. 线程 start() 规则这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。 线程 join() 规则这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。 被我们忽视的 final前面我们讲 volatile 为的是禁用缓存以及编译优化,我们再从另外一个方面来看,有没有办法告诉编译器优化得更好一点呢?这个可以有,就是final 关键字。 final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化。Java 编译器在 1.5 以前的版本的确优化得很努力,以至于都优化错了。 在 1.5 以后 Java 内存模型对 final 类型变量的重排进行了约束。现在只要我们提供正确构造函数没有“逸出”,就不会出问题了。 总结在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[01 | 可见性、原子性和有序性问题:并发编程Bug的源头]]></title>
<url>%2F2019%2F06%2F03%2Fjava%2F%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8BBUG%E7%9A%84%E6%BA%90%E5%A4%B4%2F</url>
<content type="text"><![CDATA[01 | 可见性、原子性和有序性问题:并发编程 BUG 的源头并发程序幕后的故事这些年,我们的 CPU、内存、I/O 设备都在不断迭代,不断朝着更快的方向努力。但是,在这个快速发展的过程中,有一个核心矛盾一直存在,就是这三者的速度差异。CPU 和内存的速度差异可以形象地描述为:CPU 是天上一天,内存是地上一年(假设 CPU 执行一条普通指令需要一天,那么 CPU 读写内存得等待一年的时间)。内存和 I/O 设备的速度差异就更大了,内存是天上一天,I/O 设备是地上十年。 程序里大部分语句都要访问内存,有些还要访问 I/O,根据木桶理论(一只水桶能装多少水取决于它最短的那块木板),程序整体的性能取决于最慢的操作——读写 I/O 设备,也就是说单方面提高 CPU 性能是无效的。 为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都做出了贡献,主要体现为: CPU 增加了缓存,以均衡与内存的速度差异; 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异; 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。 现在我们几乎所有的程序都默默地享受着这些成果,但是天下没有免费的午餐,并发程序很多诡异问题的根源也在这里。 源头之一:缓存导致的可见性问题在单核时代,所有的线程都是在一颗 CPU 上执行,CPU 缓存与内存的数据一致性容易解决。因为所有线程都是操作同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的。例如在下面的图中,线程 A 和线程 B 都是操作同一个 CPU 里面的缓存,所以线程 A 更新了变量 V 的值,那么线程 B 之后再访问变量 V,得到的一定是 V 的最新值(线程 A 写过的值)。 一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。 多核时代,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。比如下图中,线程 A 操作的是 CPU-1 上的缓存,而线程 B 操作的是 CPU-2 上的缓存,很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了。这个就属于硬件程序员给软件程序员挖的“坑”。 线程切换带来的原子性问题由于 IO 太慢,早期的操作系统就发明了多进程,即便在单核的 CPU 上我们也可以一边听着歌,一边写 Bug,这个就是多进程的功劳。 操作系统允许某个进程执行一小段时间,例如 50 毫秒,过了 50 毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个 50 毫秒称为“时间片”。 在一个时间片内,如果一个进程进行一个 IO 操作,例如读个文件,这个时候该进程可以把自己标记为“休眠状态”并出让 CPU 的使用权,待文件读进内存,操作系统会把这个休眠的进程唤醒,唤醒后的进程就有机会重新获得 CPU 的使用权了。 这里的进程在等待 IO 时之所以会释放 CPU 使用权,是为了让 CPU 在这段等待时间里可以做别的事情,这样一来 CPU 的使用率就上来了;此外,如果这时有另外一个进程也读文件,读文件的操作就会排队,磁盘驱动在完成一个进程的读操作后,发现有排队的任务,就会立即启动下一个读操作,这样 IO 的使用率也上来了。 是不是很简单的逻辑?但是,虽然看似简单,支持多进程分时复用在操作系统的发展史上却具有里程碑意义,Unix 就是因为解决了这个问题而名噪天下的。 早期的操作系统基于进程来调度 CPU,不同进程间是不共享内存空间的,所以进程要做任务切换就要切换内存映射地址,而一个进程创建的所有线程,都是共享一个内存空间的,所以线程做任务切换成本就很低了。现代的操作系统都基于更轻量的线程来调度,现在我们提到的“任务切换”都是指“线程切换”。 Java 并发程序都是基于多线程的,自然也会涉及到任务切换,也许你想不到,任务切换竟然也是并发编程里诡异 Bug 的源头之一。任务切换的时机大多数是在时间片结束的时候,我们现在基本都使用高级语言编程,高级语言里一条语句往往需要多条 CPU 指令完成,例如上面代码中的count += 1,至少需要三条 CPU 指令。 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器; 指令 2:之后,在寄存器中执行 +1 操作; 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。 操作系统做任务切换,可以发生在任何一条CPU 指令执行完,是的,是 CPU 指令,而不是高级语言里的一条语句。对于上面的三条指令来说,我们假设 count=0,如果线程 A 在指令 1 执行完后做线程切换,线程 A 和线程 B 按照下图的序列执行,那么我们会发现两个线程都执行了 count+=1 的操作,但是得到的结果不是我们期望的 2,而是 1。 我们潜意识里面觉得 count+=1 这个操作是一个不可分割的整体,就像一个原子一样,线程的切换可以发生在 count+=1 之前,也可以发生在 count+=1 之后,但就是不会发生在中间。我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方。因此,很多时候我们需要在高级语言层面保证操作的原子性。 源头之三:编译优化带来的有序性问题那并发编程里还有没有其他有违直觉容易导致诡异 Bug 的技术呢?有的,就是有序性。顾名思义,有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:“a=6;b=7;”编译器优化后可能变成“b=7;a=6;”,在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的 Bug。 在 Java 领域一个经典的案例就是利用双重检查创建单例对象,例如下面的代码:在获取实例 getInstance() 的方法中,我们首先判断 instance 是否为空,如果为空,则锁定 Singleton.class 并再次检查 instance 是否为空,如果还为空则创建 Singleton 的一个实例。 123456789101112public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; }} 假设有两个线程 A、B 同时调用 getInstance() 方法,他们会同时发现 instance == null ,于是同时对 Singleton.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假设是线程 A),另外一个线程则会处于等待状态(假设是线程 B);线程 A 会创建一个 Singleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程 B 检查 instance == null 时会发现,已经创建过 Singleton 实例了,所以线程 B 不会再创建一个 Singleton 实例。 这看上去一切都很完美,无懈可击,但实际上这个 getInstance() 方法并不完美。问题出在哪里呢?出在 new 操作上,我们以为的 new 操作应该是: 分配一块内存 M; 在内存 M 上初始化 Singleton 对象; 然后 M 的地址赋值给 instance 变量。 但是实际上优化后的执行路径却是这样的: 分配一块内存 M; 将 M 的地址赋值给 instance 变量; 最后在内存 M 上初始化 Singleton 对象。 优化后会导致什么问题呢?我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。 总结 缓存导致的可见性问题 线程切换带来的原子性问题 编译优化带来的有序性问题]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[队列同步器的实现分析]]></title>
<url>%2F2019%2F06%2F01%2Fjava%2F%E9%98%9F%E5%88%97%E5%90%8C%E6%AD%A5%E5%99%A8%2F</url>
<content type="text"><![CDATA[独占式同步状态获取流程 独占式超时获取同步状态流程]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>高并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[spark 的运行模式]]></title>
<url>%2F2019%2F05%2F16%2Fspark%2Fspark%E7%9A%84%E8%BF%90%E8%A1%8C%E6%A8%A1%E5%BC%8F%2F</url>
<content type="text"><![CDATA[Spark多种运行模式整理自:https://www.jianshu.com/p/65a3476757a5 刚接触Spark时,很希望能对它的运行方式有个直观的了解,而Spark同时支持多种运行模式,官网和书籍中对他们的区别所说不详,尤其是模式之间是否有关联、启动的JVM进程是否有区别、启动的JVM进程的作用是否都一样,等等这些都没有说明,也没有现成的资料可以查询。所以,我今天总结一下,供新手参考和学习(下述结论基于 Spark 2.1.0 版本和 Hadoop 2.7.3 版本) 测试或实验性质的本地运行模式 (单机)该模式被称为 Local[N] 模式,是用单机的多个线程来模拟Spark 分布式计算,通常用来验证开发出来的应用程序逻辑上有没有问题。 其中 N 代表可以使用 N 个线程,每个线程拥有一个core。如果不指定 N ,则默认是 1 个线程(该线程有 1 个core)。如果是 local[*],则代表 Run Spark locally with as many worker threads as logical cores on your machine. 如下: spark-submit 和 spark-submit –master local 效果是一样的,(同理:spark-shell 和 spark-shell –master local 效果是一样的)。spark-submit –master local[4] 代表会有 4 个线程(每个线程一个core)来并发执行应用程序。 那么,这些线程都运行在什么进程下呢?后面会说到,请接着往下看。 运行该模式非常简单,只需要把 Spark 的安装包解压后,改一些常用的配置即可使用,而不用启动 Spark 的 Master、Worker 守护进程(只有集群的Standalone方式时,才需要这两个角色),也不用启动 Hadoop 的各服务(除非你要用到 HDFS ),这是和其他模式的区别,要记住才能理解。 那么,这些执行任务的线程,到底是共享在什么进程中呢?我们用如下命令提交作业: 1spark-submit --class JavaWordCount --master local[10] JavaWordCount.jar file:///tmp/test.txt 可以看到,在程序执行过程中,只会生成一个 SparkSubmit 进程。 这个 SparkSubmit 进程又当爹、又当妈,既是客户提交任务的Client 进程、又是 Spark 的 driver 程序、还充当着 Spark 执行 Task 的 Executor角色。 这里有个小插曲,因为 Driver 程序在应用程序结束后就会终止,那么如何在 web 界面看到该应用程序的执行情况呢,需要如此这般: 先在 spark-env.sh 增加 SPARK_HISTORY_OPTS; 然后启动 start-history-server.sh 服务; 就可以看到启动了 HistoryServer 进程,且监听端口是 18080。 之后就可以在 web 上使用 http://hostname:18080 愉快的玩耍了。 想必你们已经清楚了第一种运行模式了吧,我们接着往下说。 2,测试或实验性质的本地伪集群运行模式(单机模拟集群)这种运行模式,和 Local[N] 很像,不同的是,它会在单机启动多个进程来模拟集群下的分布式场景,而不像 Local[N] 这种多个线程只能在一个进程下委屈求全的共享资源。通常也是用来验证开发出来的应用程序逻辑上有没有问题,或者想使用 Spark 的计算框架而没有太多资源。 用法是:提交应用程序时使用 local-cluster[x, y, z]。 参数: x 代表要生成的 executor 数 y 代表每个 executor 所拥有的 core 数 z 代表每个 executor 所拥有的 memory 数 12spark-submit --master local-cluster[2, 3, 1024]spark-shell --master local-cluster[2, 3, 1024] 上面这条命令代表会使用 2 个 executor 进程,每个进程分配 3 个 core 和 1G 的内存来运行应用程序。可以看到,在程序执行过程中,会生成如下几个进程: SparkSubmit 依然充当全能角色,又是 Client 进程,又是Driver 程序,还有点资源管理的作用。生成的两个 CoarseGrainedExecutorBackend 就是用来并发执行程序的进程。它们使用的资源如下: 运行该模式依然非常简单,只需要把 Spark 的安装包解压后,改一些常用的配置即可使用。而不用启动 Spark 的 Master、Worker 守护进程(只有集群的 standalone 方式时,才需要这两个角色),也不用启动 Hadoop 的各服务(除非你要用到HDFS),这一点倒是和 local[N] 完全一样。 Spark 自带 Cluster Manager 的 Standalone Client 模式(集群)终于说到了体现分布式计算价值的地方了!(有了前面的基础,后面的内容我会稍微说快一点,只讲本文的关注点) 和单机运行的模式不同,这里必须在执行应用程序前,先启动Spark 的 Master 和 Worker 守护进程。不用启动 Hadoop 服务,除非你用到了 HDFS 的内容。 12start-master.shstart-slave.sh -h hostname url:master 如果图省事,可以用start-all.sh一条命令即可,不过这样做,和上面的分开执行有点差别,以后讲到数据本地性如何验证时会说。 启动的进程如下:(其他非 Master 节点上只会有 Worker 进程) 这种运行模式,可以使用 Spark 的 8080 web ui 来观察资源和应用程序的执行情况了。 可以看到,当前环境下,我启动了 8 个 worker 进程,每个可使用的 core 是 2 个,内存没有限制。 言归正传,用如下命令提交应用程序 12spark-submit --master spark://wl1:7077spark-submit --master spark://wl1:7077 --deploy-mode client 代表着会在所有有 Worker 进程的节点上启动 Executor 来执行应用程序,此时产生的 JVM 进程如下:(非 master 节点,除了没有 Master、SparkSubmit,其他进程都一样) Master 进程作为 cluster manager,用来对应用程序申请的资源进行管理; SparkSubmit 作为 Client 端和运行 driver 程序; CoarseGrainedExecutorBackend 用来并发执行应用程序; 注意,Worker 进程生成几个 Executor,每个 Executor 使用几个 core,这些都可以在 spark-env.sh 里面配置,此处不再啰嗦。 Spark 自带 cluster manager 的 standalone cluster 模式(集群)这种运行模式也是需要先启动 spark 的 Master、Worker 守护进程),不用启动 Hadoop 服务,除非你用到了HDFS的内容。使用如下命令执行应用程序: 1spark-submit --master spark://wl1:6066 --deploy-mode cluster 各节点启动的进程情况如下: master 节点上的进程 提交应用程序的客户端上的进程 某 worker 节点上的进程 这种模式和上面说的第三种还是有很大差别的,包括: 客户端的 SparkSubmit 进程会在应用程序提交给集群之后就退出 Master 会在集群中选择一个 Worker 进程生成一个子进程DriverWrapper 来启动 Driver程序 而 DriverWrapper 进程会占用 Worker 进程的一个 core,所以同样的资源下配置下,会比第 3 种运行模式,少用 1 个 core 来参与计算(观察下图 executor id 7 的 core数 应用程序的结果,会在执行 driver 程序的节点(某 Worker)的 stdout 中输出,而不是打印在屏幕上 基于 YARN 的 Resource Manager 的 Client 模式(集群)现在越来越多的场景,都是 Spark 跑在 Hadoop 集群中,所以为了做到资源能够均衡调度,会使用 YARN 来做为 Spark 的 Cluster Manager,来为 Spark 的应用程序分配资源。 在执行 Spark 应用程序前,要启动 Hadoop 的各种服务。由于已经有了资源管理器,所以不需要启动 Spark 的 Master、Worker 守护进程。相关配置的修改,请自行研究。 使用如下命令执行应用程序: 12spark-submit --master yarn spark-submit --master yarn --deploy-mode client 提交应用程序后,各节点会启动相关的进程如下: 在 Resource Manager 节点上提交应用程序,会生成 SparkSubmit 进程,该进程会执行 driver 程序; RM会在集群中的某个 NodeManager 上,启动一个ExecutorLauncher 进程,来做为 ApplicationMaster; 会在多个 NodeManager 上生成 CoarseGrainedExecutorBackend 进程来并发的执行应用程序。 对应的 YARN 资源管理的单元 Container,关系如下: 为 ApplicationMaster 生成了容器 000001;为 CoarseGrainedExecutorBackend 生成了容器 000002-000003。 基于YARN的Resource Manager的Custer模式(集群)使用如下命令执行应用程序: 1spark-submit --master yarn --deploy-mode cluster 和第 5 种运行模式,区别如下: 在 Resource Manager 端提交应用程序,会生成SparkSubmit 进程,该进程只用来做 Client 端,应用程序提交给集群后,就会删除该进程; Resource Manager 在集群中的某个 NodeManager 上运行 ApplicationMaster,该 AM 同时会执行 driver 程序。紧接着,会在各 NodeManager 上运行 CoarseGrainedExecutorBackend 来并发执行应用程序。 应用程序的结果,会在执行 driver 程序的节点的 stdout 中输出,而不是打印在屏幕上。 当然,3-6 这几种运行模式,你也可以在一台单机上玩,前提是你的服务器足够牛。 总结这六种模式可以分为三组: 1、2 组都是单机模式,区别是 1 只有一个进程,承担了所有角色,2 会有两类多个进程,有独立的执行器进程; 3、4 组是集群模式,都是用 Spark 自带的 Master 和 Worker 来管理资源,区别是 3(client)的提交任务的节点承担 Driver 的角色,任务不执行完它不退出,4(cluster)中提交任务的节点在提完了之后进程会退出,Spark 在执行器节点中选择一个承担 Driver 角色; 5、6 组也是集群模式,与 3、4 的区别是使用 YARN 来管理资源,然后 5(client)、6(cluster)与 3、4 的关系类似。]]></content>
<categories>
<category>大数据</category>
</categories>
<tags>
<tag>spark</tag>
</tags>
</entry>
<entry>
<title><![CDATA[常见压缩文件及使用命令]]></title>
<url>%2F2019%2F03%2F07%2Ftools%2Fcompression%2F</url>
<content type="text"><![CDATA[gzip 压缩 gzip 将得到压缩文件.gz,同时删除文件 解压 gzip -d .gz 将得到文件,同时删除文件.gz bz2 压缩 bzip2 将得到压缩文件.bz2,同时删除文件 解压 bzip2 -d .bz2 将得到文件,同时删除文件.bz2 xz 压缩 xz 将得到压缩文件.xz,同时删除文件 解压 xz -d .xz 将得到文件,同时删除文件.xz]]></content>
<categories>
<category>开发工具</category>
</categories>
<tags>
<tag>压缩</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Mac下安装HBase单机版]]></title>
<url>%2F2019%2F01%2F22%2Fhbase%2Finstall-hbase%2F</url>
<content type="text"><![CDATA[因项目测试需要,本人试图在Mac上安装一个单机版的HBase,但就是这么一个简单的事,却折腾了好久。主要是网上的资料都是你抄我我抄你,总是遗漏一些重要信息。在一个就是HBase自己有些坑,做的还不够友好。 废话不多说,直接写步骤。 TODO: 待补充]]></content>
<categories>
<category>HBase</category>
</categories>
<tags>
<tag>分布式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[CSS学习笔记]]></title>
<url>%2F2019%2F01%2F11%2Ffront-end%2Fcss%2F</url>
<content type="text"><![CDATA[CSS基础学习CSS盒模型元素分类在CSS中,html中的标签元素大体被分为三种不同的类型:块状元素、内联元素(又叫行内元素)和内联块状元素。 常用的块状元素有: 1<div>、<p>、<h1>...<h6>、<ol>、<ul>、<dl>、<table>、<address>、<blockquote> 、<form> 常用的内联元素有: 1<a>、<span>、<br>、<i>、<em>、<strong>、<label>、<q>、<var>、<cite>、<code> 常用的内联块状元素有: 1<img>、<input> 块级元素块级元素特点: 每个块级元素都从新的一行开始,并且其后的元素也另起一行。(真霸道,一个块级元素独占一行) 元素的高度、宽度、行高以及顶和底边距都可设置。 元素宽度在不设置的情况下,是它本身父容器的100%(和父元素的宽度一致),除非设定一个宽度。 设置display:block就是将元素显示为块级元素。如下代码就是将内联元素a转换为块状元素,从而使a元素具有块状元素特点。 1a{display:block;} 内联元素内联元素特点: 和其他元素都在一行上; 元素的高度、宽度及顶部和底部边距不可设置; 元素的宽度就是它包含的文字或图片的宽度,不可改变。 在html中,<span>、<a>、<label>、<strong>和<em>就是典型的内联元素(行内元素)(inline)元素。当然块状元素也可以通过代码display:inline将元素设置为内联元素。如下代码就是将块状元素div转换为内联元素,从而使 div 元素具有内联元素特点。 123456div{ display:inline; }......<div>我要变成内联元素</div> 内联块状元素内联块状元素(inline-block)就是同时具备内联元素、块状元素的特点,代码display:inline-block就是将元素设置为内联块状元素。(css2.1新增),<img>、<input>标签就是这种内联块状标签。 inline-block 元素特点: 和其他元素都在一行上; 元素的高度、宽度、行高以及顶和底边距都可设置。 边框盒子模型的边框就是围绕着内容及补白的线,这条线你可以设置它的粗细、样式和颜色(边框三个属性)。 如下面代码为 div 来设置边框粗细为 2px、样式为实心的、颜色为红色的边框: div{ border:2px solid red;}上面是 border 代码的缩写形式,可以分开写: div{ border-width:2px; border-style:solid; border-color:red;} 注意: 1、border-style(边框样式)常见样式有: dashed(虚线)| dotted(点线)| solid(实线)。 2、border-color(边框颜色)中的颜色可设置为十六进制颜色,如: border-color:#888;//前面的井号不要忘掉。 3、border-width(边框宽度)中的宽度也可以设置为: thin | medium | thick(但不是很常用),最常还是用像素(px)。 现在有一个问题,如果有想为 p 标签单独设置下边框,而其它三边都不设置边框样式怎么办呢?css 样式中允许只为一个方向的边框设置样式: div{border-bottom:1px solid red;}同样可以使用下面代码实现其它三边(上、右、左)边框的设置: border-top:1px solid red;border-right:1px solid red;border-left:1px solid red; 宽度和高度盒模型宽度和高度和我们平常所说的物体的宽度和高度理解是不一样的,css内定义的宽(width)和高(height),指的是填充以里的内容范围。 因此一个元素实际宽度(盒子的宽度)=左边界+左边框+左填充+内容宽度+右填充+右边框+右边界。 元素的高度也是同理。 比如: css代码: 123456div{ width:200px; padding:20px; border:1px solid red; margin:10px; } html代码: 123<body> <div>文本内容</div></body> 元素的实际长度为:10px+1px+20px+200px+20px+1px+10px=262px。在chrome浏览器下可查看元素盒模型,如下图: 填充元素内容与边框之间是可以设置距离的,称之为“填充”。填充也可分为上、右、下、左(顺时针)。如下代码: div{padding:20px 10px 15px 30px;} 顺序一定不要搞混。可以分开写上面代码: 123456div{ padding-top:20px; padding-right:10px; padding-bottom:15px; padding-left:30px;} 如果上、右、下、左的填充都为10px;可以这么写 1div{padding:10px;} 如果上下填充一样为10px,左右一样为20px,可以这么写: 1div{padding:10px 20px;} 边界元素与其它元素之间的距离可以使用边界(margin)来设置。边界也是可分为上、右、下、左。如下代码: 1div{margin:20px 10px 15px 30px;} 也可以分开写: 123456div{ margin-top:20px; margin-right:10px; margin-bottom:15px; margin-left:30px;} 如果上右下左的边界都为10px;可以这么写: 1div{ margin:10px;} 如果上下边界一样为10px,左右一样为20px,可以这么写: 1div{ margin:10px 20px;} 总结一下:padding和margin的区别,padding在边框里,margin在边框外。 CSS布局模型清楚了CSS 盒模型的基本概念、 盒模型类型, 我们就可以深入探讨网页布局的基本模型了。布局模型与盒模型一样都是 CSS 最基本、 最核心的概念。但布局模型是建立在盒模型基础之上,又不同于我们常说的 CSS 布局样式或 CSS 布局模板。如果说布局模型是本,那么 CSS 布局模板就是末了,是外在的表现形式。CSS包含3种基本的布局模型,用英文概括为:Flow、Layer 和 Float。在网页中,元素有三种布局模型: 流动模型(Flow) 浮动模型 (Float) 层模型(Layer) 流动模型(Flow)先来说一说流动模型,流动(Flow)是默认的网页布局模式。也就是说网页在默认状态下的 HTML 网页元素都是根据流动模型来分布网页内容的。 流动布局模型具有2个比较典型的特征: 第一点,块状元素都会在所处的包含元素内自上而下按顺序垂直延伸分布,因为在默认状态下,块状元素的宽度都为100%。实际上,块状元素都会以行的形式占据位置; 第二点,在流动模型下,内联元素都会在所处的包含元素内从左到右水平分布显示。(内联元素可不像块状元素这么霸道独占一行) 浮动模型 (Float)块状元素这么霸道都是独占一行,如果现在我们想让两个块状元素并排显示,怎么办呢?不要着急,设置元素浮动就可以实现这一愿望。 任何元素在默认情况下是不能浮动的,但可以用 CSS 定义为浮动,如 div、p、table、img 等元素都可以被定义为浮动。如下代码可以实现两个 div 元素一行显示。 123456div{ width:200px; height:200px; border:2px red solid; float:left;} 层模型(Layer)如何让html元素在网页中精确定位,就像图像软件PhotoShop中的图层一样可以对每个图层能够精确定位操作。CSS定义了一组定位(positioning)属性来支持层布局模型。 层模型有三种形式: 绝对定位(position: absolute) 相对定位(position: relative) 固定定位(position: fixed) 绝对定位如果想为元素设置层模型中的绝对定位,需要设置position:absolute(表示绝对定位),这条语句的作用将元素从文档流中拖出来,然后使用left、right、top、bottom属性相对于其最接近的一个具有定位属性的父包含块进行绝对定位。如果不存在这样的包含块,则相对于body元素,即相对于浏览器窗口。 如下面代码可以实现div元素相对于浏览器窗口向右移动100px,向下移动50px。 12345678div{ width:200px; height:200px; border:2px red solid; position:absolute; left:100px; top:50px;} 相对定位如果想为元素设置层模型中的相对定位,需要设置position:relative(表示相对定位),它通过left、right、top、bottom属性确定元素在正常文档流中的偏移位置。相对定位完成的过程是首先按static(float)方式生成一个元素(并且元素像层一样浮动了起来),然后相对于以前的位置移动,移动的方向和幅度由left、right、top、bottom属性确定,偏移前的位置保留不动。 如下代码实现相对于以前位置向下移动50px,向右移动100px; 12345678div{ width:200px; height:200px; border:2px red solid; position:relative; left:100px; top:50px;} 什么叫做“偏移前的位置保留不动”呢?在这个div后再添加一个span标签,会发现span元素是显示在了div元素以前位置的后面。 固定定位fixed:表示固定定位,与absolute定位类型类似,但它的相对移动的坐标是视图(屏幕内的网页窗口)本身。由于视图本身是固定的,它不会随浏览器窗口的滚动条滚动而变化,除非你在屏幕中移动浏览器窗口的屏幕位置,或改变浏览器窗口的显示大小,因此固定定位的元素会始终位于浏览器窗口内视图的某个位置,不会受文档流动影响,这与background-attachment:fixed;属性功能相同。以下代码可以实现相对于浏览器视图向右移动100px,向下移动50px。并且拖动滚动条时位置固定不变。 12345678div{ width:200px; height:200px; border:2px red solid; position:fixed; left:100px; top:50px;} Relative与Absolute组合使用使用position:absolute可以实现被设置元素相对于浏览器(body)设置定位以后,大家有没有想过可不可以相对于其它元素进行定位呢?答案是肯定的,当然可以。使用position:relative来帮忙,但是必须遵守下面规范: 1、参照定位的元素必须是相对定位元素的前辈元素: 123<div id="box1"><!--参照定位的元素--> <div id="box2">相对参照元素进行定位</div><!--相对定位元素--></div> 从上面代码可以看出box1是box2的父元素(父元素当然也是前辈元素了)。 2、参照定位的元素必须加入position:relative; 12345#box1{ width:200px; height:200px; position:relative;} 3、定位元素加入position:absolute,便可以使用top、bottom、left、right来进行偏移定位了。 12345#box2{ position:absolute; top:20px; left:30px; } 这样box2就可以相对于父元素box1定位了(这里注意参照物就可以不是浏览器了,而可以自由设置了)。]]></content>
<categories>
<category>前端布局样式</category>
</categories>
<tags>
<tag>CSS3</tag>
</tags>
</entry>
<entry>
<title><![CDATA[React学习笔记]]></title>
<url>%2F2018%2F12%2F28%2Ffront-end%2Freact%2F</url>
<content type="text"><![CDATA[React 高级内容React 的数据视图更新原理 state 数据 JSX 模版 数据 + 模版 生成虚拟 DOM(虚拟 DOM 就是一个 JS 对象,用它来描述真实 DOM )(损耗了性能) 用虚拟 DOM 的结构生成真实的 DOM 来显示 state 发生变化 数据 + 模版 生成新的虚拟 DOM(极大地提升了性能) 比较原始虚拟 DOM 和新的虚拟 DOM 的区别(极大地提升了性能) 直接操作 DOM,改变上一步中被修改了的的内容 优点: 性能提升 它使得跨端应用得以实现,React Native。 虚拟 DOM 中的 Diff 算法问:什么时候会需要比对?答:数据发生变化的时候,其实就是调用 setState 方法的时候 setState 方法是异步的,可以把多次 setState 统一到一起,减少 DOM 比对的次数 虚拟 DOM 是同级比对的,如果比对有差异,下面级别的就不用比对了,直接把替换下面所有的级别 虚拟 DOM 循环的时候,要给定一个唯一的 Key(最好是唯一标识 ID 之类的,不能重复的),不要使用索引作为 Key Ref 的使用 ref 可以帮助我们获取 DOM 元素 setState 是异步函数,它接收的第二个参数是 setState 完成后的回调函数 setState(() => ({}), () => {}) React 的生命周期函数生命周期函数指在某一时刻组件会自动调用执行的函数,前面讲到的 render 函数就是一个生命周期函数。 Mounting componentwillMount 函数在组件即将被挂载到页面的时刻执行,只有第一次挂载才会执行 render 可以认为就是所谓的组件挂载函数 componentDidMount 函数在组件挂载到页面之后,自动被执行,只有第一次挂载才会执行 Updation 组件被更新之前,shouldComponentUpdate(nextProps, nextState) 函数就会被自动执行。shouldComponentUpdate(nextProps, nextState) 函数返回 true 或 false。如果返回值为 false,组件和数据就不会更新,它之后的生命周期函数都不会被执行 组件被更新之前,shouldComponentUpdate 函数之后,componentWillUpdate 函数会被自动执行 组件更新完成之后,componentDidUpdate 函数会被自动执行 一个组件从父组件接受参数,只要父组件的render函数被重新执行了,子组件的 componentWillReceiveProps 就会被执行,或者用下面的两条描述 如果子组件第一次存在于父组件中,子组件的 componentWillReceiveProps 函数不会执行 如果子组件之前存在于父组件中,子组件的 componentWillReceiveProps 函数才会执行 Unmounting 当这个组件即将被从页面中剔除的时候,componentWillUnmount 函数会被自动执行 每一个组件都有这样的生命周期函数,不是只有父组件才有。 React 生命周期函数的使用场景Component 默认内置了其他所有的生命周期函数,但唯独没有内置render这个生命周期函数 借助 shouldComponentUpdate 可以避免无谓组件render函数的运行,提高性能 1234// nextProps是新的属性值shouldComponentUpdate(nextProps, nextState) { return nextProps.content !== this.propscontent} ajax 请求,因为只请求一次,所以不放在render()函数里面执行,建议放在componentDidMount执行;放在 componentwillMount 可能会和 rn 开发有冲突。 性能优化: 周期函数:shouldComponentUpdate(nextProps, nextState),提高组件性能 作用域的修改:放在constructor里面。比如:this.handleClick.bind(this),只会执行一次,避免子组件无谓渲染 react 的底层setState,内置性能提升机制,异步函数,把多次数据改变结合一一次来做,降低虚拟 DOM 比对频率 react 底层用的是虚拟DOM的概念,同层比对,还有 key 值这样的概念,提升虚拟 DOM 比对速度 使用 Charles 实现本地数据 mockCharles 可以抓取到浏览器的请求,然后返回指定数据文件的结果。 React 中实现 CSS 过渡动画12345handleToggle() { this.setState({ show: !this.state.show //可以这样写 }) } animation 的最后一个参数填写 forwards,它能够在动画结束之后保存最后一帧 css 的样式 使用 react-transition-group 实现动画安装 react 动画库 1npm install react-transition-group --save 第一次展示到页面上的时候也要动画效果,用 appear={true} Redux 入门react只是一个轻量级的视图层框架,如果要做大型应用就要搭配视图层框架redux一起使用 redux = reducer + flux,flux升级成了redux redux组件之间的传值非常简单,redux里面要求我们把数据都放在一个公共的存储区域store里面,组件之中尽量少放数据,也就是所有数据都不放在组件自身了,都把它放到一个公用的存储空间里面,然后组件改变数据就不需要传递了,改变store里面的数据之后其它的组件会感知到里面的数据发生改变。这样的话不管组件的层次有多深,但是走的流程都是一样的,会把数据的传递简化很多。 Redux 的工作流程redux是视图层框架,把所有数据都放在store之中,每个组件都要从store里拿数据,然后每个组件也要去改store里面的数据, 举例:把这个流程理解成一个图书馆的流程react compontents: 借书的人action creators: “要借什么书”这句话(语句的表达,数据的传递)store: 图书馆管理员(没办法记住所有书籍的存储情况)reducers: 图书馆管理员的记录本(要借什么书,先查有没有,要还的书查一下放到某个位置);借书的人~我要借一本书~图书管理员听见~查阅reducers手册~去store找书~把对应的书给借书人; 创建 Redux 的 Store npm安装Redux 在store文件夹下创建index.js,import { createStore } from ‘redux’ 在store文件夹下创建reducer.js 123456const defaultState = { inputValue:'123'}export default (state = defaultState,action) => { return state;} index.js中const store = createStore(reducer); 在组件中引入,this.state = store.getState() Action 和 Reducer 的编写 react首先要改变stroe里的数据,先要派发一个action, action通过dispatch(action)方法传给store stroe 把之前的数据和action(previousState,action)传给reducer reducer是个函数,它接收了state和action以后做些处理会返回一个新的newState给到store stroe用这个新的state替换到原来的数据,stroe数据改变了以后,react组件感受到了数据变化,它会从store里面重新取数据,更新组件的内容,页面就会跟着发生变化了 Reducer 可以接收 state,但是不能修改 state。 如果常量或变量在代码里写错的时候,是会报出异常的,就可以迅速定位到问题,但是如果写一个字符串的话就不会报出异常,那样的话出了bug非常难调,所以才要进行ActionTypes的拆分。 store文件夹下创建一个actionCreators.js,把action都集中写在一个文件中,方便后期维护和自动化测试 Redux 知识点复习补充 redux三个基本原则: store必须是惟一的。 只有store能够改变自己的内容。 Reducer必须是纯函数。 纯函数:给定固定输入,就一定会有固定的输出,而且不会有任何副作用。 redux 核心 api: createStore:创建store; store.dispatch:派发action,action会传递给store。 store.getState:获取到store里面所有的数据。 store.subscribe:订阅store的改变,store改变会触发store.subscribe接受的回调函数执行。 Redux 进阶UI组件和容器组件 UI 组件负责页面的渲染(傻瓜组件) 容器组件负责页面的逻辑(聪明组件) 无状态组件当一个组件只有 render 函数时,可以用无状态组件代替。无状态组建性能比较高,没有生命周期函数。UI 组件一般可以作为无状态组件。 使用 Redux-thunk 中间件实现 ajax 数据请求redux中使用redux-thunk之后可以在action中做异步请求,action可以是一个函数,dispatch接受的action是函数的时候会自动执行这个action,action这个函数默认接受一个参数dispatch,可以用来提交action。 什么是 Redux 的中间件redux中间件是在action和store之间,对dispatch方法的封装升级。使得dispatch既可以接受对象,也可以接受函数。 如何使用 React-redux npm install react-redux –save store/index.js 引入 { createStore } from redux 引入 reducer.js const store = createStore(reducer) reducer是一个纯函数 export default (state, action) = > {} todoList 组件中引入 conncect 组件连接组件和 store index.js 根组件中从react-redux 中引入 Provider const app = ( < todoList />) todoList 组件通过 connect 组件把store和组件连接起来export default connect(mapStateToPropd,mapDispatchToProps)(TodoList) react-redux提供了Provider组件,用来绑定store,Provider内部的所有子组件都能够连接store。 Provider的子组件通过react-redux中的connect连接store,写法:connect(mapStateToProps, mapDispatchToProps)(Component)mapStateToProps:store中的数据映射到组件的props中;mapDispatchToProps:把store.dispatch方法挂载到props上;Component:Provider中的子组件本身; connect函数返回的是一个容器组件。 使用immutable库避免state被直接改变 immutable库提供一个fromJS方法,可以把一个JS对象转换为immutable(不可变)对象; 使用immutable.js之后,不能用“.”访问store中的对象,要使用get()方法; 使用immutable.js之后,修改store中的数据时,要使用set方法; immutable对象的set方法,会结合之前immutable对象的值和设置的值,返回一个全新的对象,并没有改变原始的state;]]></content>
<categories>
<category>前端 JS 框架</category>
</categories>
<tags>
<tag>React</tag>
</tags>
</entry>
<entry>
<title><![CDATA[科学上网]]></title>
<url>%2F2018%2F12%2F25%2Ftools%2Fscience-online%2F</url>
<content type="text"><![CDATA[Github访问速度慢的解决方法 手动更改 hosts,更改hosts之前,你得知道修改什么网址对应的hosts。参考上面给出的链接,我主要修改的hosts地址为:github.com 和 github.global.ssl.fastly.net。查看网站对应的IP地址的方法为访问ipaddress网站,输入网址则可查阅到对应的IP地址。ipaddress地址:https://www.ipaddress.com/ 当前日期下,我查阅到的IP对应为: 12151.101.185.194 github.global.ssl.fastly.net192.30.253.113 github.com 修改的Github对应的完整hosts为: 1234567891011121314# Github192.30.253.113 github.com151.101.184.133 assets-cdn.github.com185.199.108.153 documentcloud.github.com192.30.253.118 gist.github.com185.199.108.153 help.github.com192.30.253.120 nodeload.github.com151.101.184.133 raw.github.com18.204.240.114 status.github.com192.30.253.166 training.github.com192.30.253.112 www.github.com151.101.185.194 github.global.ssl.fastly.net151.101.184.133 avatars0.githubusercontent.com151.101.184.133 avatars1.githubusercontent.com]]></content>
<categories>
<category>开发工具</category>
</categories>
<tags>
<tag>上网</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Maven常用配置]]></title>
<url>%2F2018%2F12%2F21%2Ftools%2Fmaven%2F</url>
<content type="text"><![CDATA[升级整个项目的版本号12345mvn versions:set -DnewVersion=1.0.1-SNAPSHOT# 提交mvn versions:commit# 回滚mvn versions:revert 将所有依赖打成一个jar123456789101112131415161718192021222324252627<build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <archive> <manifest> <!--这里要替换成jar包main方法所在类 --> <mainClass>com.baidu.hugegraph.xxx</mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins></build> 拷贝依赖文件到lib123456789101112131415161718192021222324252627282930313233343536<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <phase>prepare-package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> <overWriteReleases>false</overWriteReleases> <overWriteSnapshots>false</overWriteSnapshots> <overWriteIfNewer>true</overWriteIfNewer> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>com.baidu.hugegraph.xxx</mainClass> </manifest> </archive> </configuration> </plugin> </plugins></build> 选择 profile 跑不同的单元测试1234567891011121314<profiles> <profile> <id>resource-tests</id> <properties> <test-classes>**/*ResourceTest.java</test-classes> </properties> </profile> <profile> <id>task-tests</id> <properties> <test-classes>**/*TaskTest.java</test-classes> </properties> </profile></profiles> 12345678910<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.12.2</version> <configuration> <includes> <include>${test-classes}</include> </includes> </configuration></plugin> when you run mvn clean test -Presource-tests, only the classes matching */ResourceTest.java will be tested when you run mvn clean test -Ptask-tests, only the classes matching */TaskTest.java will be tested]]></content>
<categories>
<category>开发工具</category>
</categories>
<tags>
<tag>Maven</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Shell常用操作记录]]></title>
<url>%2F2018%2F12%2F21%2Ftools%2Fshell-operation%2F</url>
<content type="text"><![CDATA[修改 Linux 系统 Shell 的编码 123export LANG="en_US.UTF-8"export LANG="zh_CN.GBK"export LANG="zh_CN.UTF-8" 在 Java 代码里面执行 Shell 命令 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859/*** 执行系统命令, 返回执行结果* @param cmd 需要执行的命令* @param dir 执行命令的子进程的工作目录, null 表示和当前主进程工作目录相同*/public static String execCmd(String cmd, File dir) { StringBuilder result = new StringBuilder(); Process process = null; BufferedReader bufrIn = null; BufferedReader bufrError = null; try { String[] commond = {"/bin/sh", "-c", cmd}; // 执行命令, 返回一个子进程对象(命令在子进程中执行) process = Runtime.getRuntime().exec(commond, null, dir); // 方法阻塞, 等待命令执行完成(成功会返回0) process.waitFor(); // 获取命令执行结果, 有两个结果: 正常的输出 和 错误的输出(PS: 子进程的输出就是主进程的输入) bufrIn = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); bufrError = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8")); // 读取输出 String line = null; while ((line = bufrIn.readLine()) != null) { result.append(line).append('\n'); } while ((line = bufrError.readLine()) != null) { result.append(line).append('\n'); } } catch (Exception e) { logger.error(e); } finally { closeStream(bufrIn); closeStream(bufrError); // 销毁子进程 if (process != null) { process.destroy(); } } // 返回执行结果 return result.toString();}private static void closeStream(Closeable stream) { if (stream != null) { try { stream.close(); } catch (Exception e) { // nothing } }} Shell 中 Map 的使用 1234567891011121314151617# 定义一个空map declare -A map=()# 定义并初始化map declare -A map=(["100"]="1" ["200"]="2")# 输出所有keyecho ${!map[@]}# 输出所有value echo ${map[@]}# 添加值map["300"]="3"# 输出key对应的值echo ${map["100"]}# 遍历输出mapfor key in ${!map[@]}do echo ${map[$key]}done 在 Mac 环境中会出错,因为 Mac OS X 的默认Bash 是 3.x 版本,不支持 map 这种数据结构,有两种解决方案: 升级bash到 4.x 以上版本 用其他方式:比如 if elif 去到达相同的结果 tcpdump的使用 1sudo tcpdump -i lo0 dst host 127.0.0.1 and port 8080 netstat的使用 1netstat -n -p tcp | grep 8080 统计当前文件夹下文件的个数 1ls -l | grep "^-" | wc -l 统计当前文件夹下目录的个数 1ls -l | grep "^d" | wc -l 统计当前文件夹下文件的个数,包括子文件夹里的 1ls -lR | grep "^-" | wc -l 统计文件夹下目录的个数,包括子文件夹里的 1ls -lR | grep "^d" | wc -l]]></content>
<categories>
<category>开发工具</category>
</categories>
<tags>
<tag>Shell</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深度剖析 HDFS]]></title>
<url>%2F2018%2F10%2F19%2Fhadoop%2F%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90%20HDFS%2F</url>
<content type="text"><![CDATA[本文整理自:https://www.cnblogs.com/Xmingzi/p/6032415.html 前言大数据底层技术的三大基石起源于Google在2006年发表的三篇论文:GFS、Map-Reduce、Bigtable,其中 GFS 和 Map-Reduce 直接支持了 Apache Hadoop 项目的诞生,Bigtable 催生了 NoSQL 这个崭新的数据库领域。 为弥补 Map-Reduce 处理框架高延时的缺陷,Google 在 2009 年后推出的Dremel 促使了实时计算系统的兴起,以此引发大数据第二波技术浪潮。一些大数据公司纷纷推出自己的大数据查询分析产品,如:Cloudera 开源了大数据查询分析引擎 Impala,Hortonworks 开源了 Stinger,Fackbook 开源了 Presto、UC Berkeley AMPLAB 实验室开发了 Spark 计算框架。 所有这些技术和产品的数据来源均基于 HDFS,而 HDFS 作为一个分布式文件存储系统,最基本的就是其读写操作。 目录 HDFS 名词解释 HDFS 架构 NameNode(NN) Secondary NameNode HDFS 写文件 HDFS 读文件 Block 持续化结构 HDFS 名次解释 Block:在 HDFS 中,每个文件都是采用的分块的方式存储,每个 Block 放在不同的 DataNode 上,每个 Block 的标识是一个三元组(block id, numBytes,generationStamp),其中 block id 是具有唯一性,具体分配是由 NameNode 节点设置,然后再由 DataNode 上建立 block 文件,同时建立对应 block meta 文件 Packet:在 DFSclient 与 DataNode 之间通信的过程中,发送和接受数据过程都是以一个 Packet 为基础的方式进行 Chunk:中文名字也可以称为块,但是为了与 Block 区分,还是称之为Chunk。在 DFSClient 与 DataNode 之间通信的过程中,由于文件采用的是基于块的方式来进行的,但是在发送数据的过程中是以 Packet 的方式来进行的,每个 Packet 包含了多个 Chunk,同时对于每个 Chunk 进行checksum 计算,生成 checksum bytes 小结 一个文件被拆成多个block持续化存储(block size 由配置文件参数决定) 思考: 修改 block size 对以前持续化的数据有何影响? 数据通讯过程中一个 block 被拆成 多个 packet 一个 packet 包含多个 chunk Packet结构与定义:Packet分为两类,一类是实际数据包,另一类是heatbeat包。一个Packet数据包的组成结构,如图所示 上图中,一个 Packet 是由 Header 和 Data 两部分组成,其中 Header 部分包含了一个 Packet 的概要属性信息,如下表所示: Data 部分是一个 Packet 的实际数据部分,主要包括一个 4 字节校验和(Checksum)与一个 Chunk 部分,Chunk 部分最大为 512 字节 在构建一个 Packet 的过程中,首先将字节流数据写入一个 buffer 缓冲区中,也就是从偏移量为 25 的位置(checksumStart)开始写 Packet 数据Chunk 的 Checksum 部分,从偏移量为533的位置(dataStart)开始写Packet数据的Chunk Data部分,直到一个Packet创建完成为止。 当写一个文件的最后一个 Block 的最后一个 Packet 时,如果一个 Packet 的大小未能达到最大长度,也就是上图对应的缓冲区中,Checksum 与 Chunk Data 之间还保留了一段未被写过的缓冲区位置,在发送这个Packet 之前,会检查 Chunksum 与 Chunk Data 之间的缓冲区是否为空白缓冲区(gap),如果有则将 Chunk Data 部分向前移动,使得 Chunk Data 1 与 Chunk Checksum N 相邻,然后才会被发送到 DataNode 节点。 HDFS 架构 HDFS 主要包含四类角色:Client、NameNode、SecondaryNameNode、DataNode HDFS Client:系统使用者,调用 HDFS API 操作文件,与 NameNode 交互获取文件元数据,与 DataNode 交互进行数据读写(注意:写数据时文件切分是由 Client 完成的); NameNode:Master 节点(也称元数据节点),是系统唯一的管理者。负责元数据的管理(名称空间和数据块映射信息),配置副本策略,处理客户端请求等; DataNode:Slave 节点(也称数据存储节点),存储实际的数据,执行数据块的读写,汇报存储信息给NameNode; SecondaryNameNode:小弟角色,分担大哥 NameNode 的工作量,是NameNode 的冷备份,合并 fsimage 和 fsedits 然后再发给 NameNode,注意:在 Hadoop 2.x 版本,当启用 HDFS HA 时,将没有这一角色。 热备份:b 是 a 的热备份,如果 a 坏掉。那么 b 马上运行代替 a 的工作。冷备份:b 是 a 的冷备份,如果 a 坏掉。那么 b 不能马上代替 a 工作。但是 b 上存储 a 的一些信息,减少 a 坏掉之后的损失 HDFS 架构原则: 元数据与数据分离:文件本身的属性(即元数据)与文件所持有的数据分离; 主/从架构:一个 HDFS 集群是由一个 NameNode 和一定数目的 DataNode 组成; 一次写入多次读取:HDFS 中的文件在任何时间只能有一个 Writer。当文件被创建,接着写入数据,最后,一旦文件被关闭,就不能再修改; 移动计算比移动数据更划算:数据运算,越靠近数据,执行运算的性能就越好(数据的本地化),由于 HDFS 数据分布在不同机器上,要让网络的消耗最低,并提高系统的吞吐量,最佳方式是将运算的执行移到离它要处理的数据更近的地方,而不是移动数据。 NameNodeNameNode 是整个文件系统的管理节点,也是 HDFS 中最复杂的一个实体,它维护着 HDFS 文件系统中最重要的两个关系: HDFS 文件系统中的文件目录树,以及文件的数据块索引,即每个文件对应的数据块列表; 数据块和数据节点的对应关系,即某一个数据块保存在那些数据节点的信息; 第一个关系即目录树、元数据和数据块的索引信息会持久化到物理存储中,具体实现是保存在命名空间的镜像 fsimage 和编辑日志 edits 中。注意:在 fsimage 中,并没有记录每一个 block 对应到哪几个 DataNode 的映射信息; 第二个关系并不会持久化存储,它是在 NameNode 启动后,每个 DataNode 对本地磁盘进行扫描,将本 DataNode 上保存的 Block 信息汇报给 NameNode。NameNode 在接收到每个 DataNode 的块信息汇报后,将接收到的块信息,以及其所在的 DataNode 信息等保存在内存中。HDFS 就是通过这种块信息汇报的方式来完成 Block -> DataNodes list 的映射表构建。 fsimage 记录了自最后一次检查点之前 HDFS 文件系统中所有目录和文件的序列化信息;edits 是元数据操作日志(记录每次保存 fsimage 之后到下次保存之间的所有 HDFS 操作)。 在 NameNode 启动时候,会先将 fsimage 中的文件系统元数据信息加载到内存,然后根据 eidts 中的记录将内存中的元数据同步至最新状态,然后将这个新版本的 FsImage 从内存中保存到本地磁盘上,然后删除旧的 EditLog,这个过程称为一个检查点 (checkpoint)。 类似于数据库中的检查点,为了避免 edits 日志过大,在 Hadoop 1.X 中,SecondaryNameNode 会按照时间阈值(比如24小时)或者 edits 大小阈值(比如1G),周期性的将 fsimage 和 edits 合并,然后将最新的 fsimage 推送给 NameNode。而在 Hadoop2.X 中,这个动作是由 Standby NameNode 来完成的。 由此可看出,这两个文件一旦损坏或丢失,将导致整个HDFS文件系统不可用。 在 Hadoop 1.X 为了保证这两种元数据文件的高可用性,一般的做法是将dfs.namenode.name.dir 设置成以逗号分隔的多个目录,这多个目录至少不要在一块磁盘上,最好放在不同的机器上,比如:挂载一个共享文件系统。 fsimage/edits 是序列化后的文件,想要查看或编辑里面的内容,可通过 HDFS 提供的 oiv\oev 命令,如下: 命令: hdfs oiv(offline image viewer),用于将 fsimage 文件的内容转储到指定文件中以便于阅读,如文本文件、XML文件,该命令需要以下参数: -i(必填参数)–inputFile 输入FSImage文件 -o(必填参数)–outputFile 输出转换后的文件,如果存在,则会覆盖 -p (可选参数) –processor 将FSImage文件转换成哪种格式:(Ls | XML | FileDistribution),默认为Ls 命令:hdfs oev(offline edits viewer),该工具只操作文件因而并不需要 Hadoop 集群处于运行状态。支持的输出格式有 binary(Hadoop使用的二进制格式)、XML(在不使用参数 p 时的默认输出格式)和 stats(输出 edits 文件的统计信息) 示例: 123hdfs oiv -i /data1/hadoop/dfs/name/current/fsimage_0000000000019372521 -o /home/hadoop/fsimage.txthdfs oev -i edits_0000000000000042778-0000000000000042779 -o edits.xml 小结 NameNode 管理着 DataNode,接收 DataNode 的注册、心跳、数据块提交等信息的上报,并且在心跳中发送数据块复制、删除、恢复等指令;同时,NameNode 还为客户端对文件系统目录树的操作和对文件数据读写、对 HDFS 系统进行管理提供支持。 NameNode 启动后会进入一个称为安全模式的特殊状态。处于安全模式的 NameNode 是不会进行数据块的复制的。NameNode 从所有的 DataNode 接收心跳信号和块状态报告。块状态报告包括了某个 DataNode 所有的数据 块列表。每个数据块都有一个指定的最小副本数。当 NameNode 检测确认某 个数据块的副本数目达到这个最小值,那么该数据块就会被认为是副本安全 (safely replicated) 的;在一定百分比(这个参数可配置)的数据块被 NameNode 检测确认是安全之后(加上一个额外的 30 秒等待时间), NameNode 将退出安全模式状态。接下来它会确定还有哪些数据块的副本没 有达到指定数目,并将这些数据块复制到其他 DataNode 上。 Secondary NameNode在 HA cluster 中又称为 standby node。 定期合并 fsimage 和 edits 日志,将 edits 日志文件大小控制在一个限度下 NameNode 响应 Secondary NameNode 请求,将 edit log 推送给 Secondary NameNode,并且自己开始重新写一个新的 edit log Secondary NameNode 收到来自 NameNode 的 fsimage 文件和 edit log Secondary NameNode 将 fsimage 加载到内存,应用 edit log , 并生成一个新的 fsimage 文件 Secondary NameNode 将新的 fsimage 推送给 NameNode NameNode 用新的 fsimage 取代旧的 fsimage ,在 fstime 文件中记下检查点发生的时间 HDFS 写文件 Client将FileA按64M分块。分成两块,block1和Block2; Client向nameNode发送写数据请求,如图蓝色虚线① NameNode节点,记录block信息。并返回可用的DataNode,如粉色虚线② Block1: host2,host1,host3 Block2: host7,host8,host4 client向DataNode发送block1;发送过程是以流式写入,流式写入过程如下:4.1 将64M的block1按64k的packet划分4.2 然后将第一个packet发送给host24.3 host2接收完后,将第一个packet发送给host1,同时client想host2发送第二个packet4.4 host1接收完第一个packet后,发送给host3,同时接收host2发来的第二个packet4.5 以此类推,如图红线实线所示,直到将block1发送完毕4.6 host2,host1,host3向NameNode,host2向Client发送通知,说“消息发送完了”。如图粉红颜色实线所示4.7 client收到host2发来的消息后,向namenode发送消息,说我写完了。这样就真完成了。如图黄色粗实线4.8 发送完block1后,再向host7,host8,host4发送block2,如图蓝色实线所示 说明 当客户端向 HDFS 文件写入数据的时候,一开始是写到本地临时文件中。假设该文件的副 本系数设置为 3 ,当本地临时文件累积到一个数据块的大小时,客户端会从 Namenode 获取一个 Datanode 列表用于存放副本。然后客户端开始向第一个 Datanode 传输数据,第一个 Datanode 一小部分一小部分 (4 KB) 地接收数据,将每一部分写入本地仓库,并同时传输该部分到列表中 第二个 Datanode 节点。第二个 Datanode 也是这样,一小部分一小部分地接收数据,写入本地 仓库,并同时传给第三个 Datanode 。最后,第三个 Datanode 接收数据并存储在本地。因此, Datanode 能流水线式地从前一个节点接收数据,并在同时转发给下一个节点,数据以流水线的方式从前一个 Datanode 复制到下一个,时序图如下: 小结 写入的过程,按hdsf默认设置,1T文件,我们需要3T的存储,3T的网络流量 在执行读或写的过程中,NameNode和DataNode通过HeartBeat进行保存通信,确定DataNode活着。如果发现DataNode死掉了,就将死掉的DataNode上的数据,放到其他节点去。读取时,要读其他节点去 挂掉一个节点,没关系,还有其他节点可以备份;甚至,挂掉某一个机架,也没关系;其他机架上,也有备份 HDFS 读文件 客户端通过调用FileSystem对象的open()方法来打开希望读取的文件,对于HDFS来说,这个对象时分布文件系统的一个实例; DistributedFileSystem通过使用RPC来调用NameNode以确定文件起始块的位置,同一Block按照重复数会返回多个位置,这些位置按照Hadoop集群拓扑结构排序,距离客户端近的排在前面 (详见第三章) 前两步会返回一个FSDataInputStream对象,该对象会被封装成DFSInputStream对象,DFSInputStream可以方便的管理datanode和namenode数据流,客户端对这个输入流调用read()方法 存储着文件起始块的DataNode地址的DFSInputStream随即连接距离最近的DataNode,通过对数据流反复调用read()方法,将数据从DataNode传输到客户端 到达块的末端时,DFSInputStream会关闭与该DataNode的连接,然后寻找下一个块的最佳DataNode,这些操作对客户端来说是透明的,客户端的角度看来只是读一个持续不断的流 一旦客户端完成读取,就对FSDataInputStream调用close()方法关闭文件读取 Block 持续化结构DataNode节点上一个Block持久化到磁盘上的物理存储结构,如下图所示: 每个Block文件(如上图中blk_1084013198文件)都对应一个meta文件(如上图中blk_1084013198_10273532.meta文件),Block文件是一个一个Chunk的二进制数据(每个Chunk的大小是512字节),而meta文件是与每一个Chunk对应的Checksum数据,是序列化形式存储]]></content>
<categories>
<category>Hadoop</category>
</categories>
<tags>
<tag>HDFS</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hadoop 分布式文件系统]]></title>
<url>%2F2018%2F10%2F16%2Fhadoop%2F3-Hadoop%E5%88%86%E5%B8%83%E5%BC%8F%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%2F</url>
<content type="text"><![CDATA[前言管理网络中跨多台计算机存储的文件系统称为分布式文件系统,该系统架构于网络之上,势必会引入网络编程的复杂性。 Hadoop 有一个称为 HDFS 的分布式文件系统,但实际上,Hadoop 是一个综合性的文件系统抽象,因此它也可以集成其他文件系统,比如本地文件系统和 Amazon S3系统。 HDFS 的设计我们先来看看 HDFS 相关的一些名次,有一些是 HDFS 不支持的: 超大文件:HDFS 善于存储超大文件,比如几百 MB、几百 GB 甚至几百 TB 和 PB; 流式数据访问:适合于一次写入多次读取的场景; 商用硬件:其实就是指普通硬件,Hadoop 能够运行在普通硬件上; 低时延:HDFS 是为高数据吞吐量应用优化的,这是以提高时间延迟为代价的; 大量小文件:namenode 将文件系统的元数据存储在内存中,而无论文件大还是小,元数据都是差不多大的,所以存储 HDFS 能存储的文件数是有限制的; 多用户写,任意修改文件:HDFS 中的文件可能只有一个 writer,而且写操作总是将数据添加在文件的末尾。它不支持具有多个写入者的操作,也不允许在文件的任意位置进行修改。 HDFS 的概念数据块与普通的文件系统一样,HDFS 文件也以块为单位存储,只不过块大得多,默认为 64MB(好像较新的版本为 128MB)。但与其他文件系统不同的是,HDFS 中小于一个块大小的文件不会占据整个块的空间。 为何 HDFS 中的块如此之大?其目的是为了最小化寻址开销,如果块设置得足够大,从磁盘传输数据的时间会明显大于定位这个块开始位置所需的时间。但是块也不能设置得太大,不然会导致 map 任务数过少,并行度不够高。 HDFS 的 fsck 指令可以显示块信息。 1hadoop fsck / -files -blocks namenode 和 datanodeHDFS 集群有两类节点以管理者-工作者模式运行,即一个 namenode 和多个 datanode。namenode 管理文件系统的命名空间,它维护者文件系统数以及整棵树内所有的文件和目录。这些信息以两个文件形式永久保存在本地磁盘上:命名空间镜像文件和编辑日志文件。namenode 中也记录着每个文件中各个块所在的数据节点信息,但它并不永久保存块的位置信息,这些信息会在系统启动时由数据节点重建。 联邦 HDFSnamenode 在内存中保存文件系统每个文件和数据块的引用关系,这意味着对于一个拥有大量文件的超大集群来说,内存将成为限制系统横向扩展的瓶颈。在 2.x 发行版中引入的联邦 HDFS 允许系统通过添加 namenode 实现扩展,其中每个 namenode 管理文件系统命名空间的一部分。比如:一个 namenode 可能管理 /user 目录下的所有文件,另一个 namenode 管理 /share 下的所有文件。 在联邦环境下,每个 namenode 维护一个命名空间卷,包括命名空间的源数据和在该命名空间下的所有文件的所有数据块的数据块池。命名空间卷之间相互独立,两两之间并不通信。 HDFS 的高可用(HA)数据高可用的前提是数据不会丢失,通过联合使用在多个文件系统中备份 namenode 的元数据和通过备用 namenode 创建监测点能防止数据丢失,但是仍然无法提供文件系统的高可用。namenode 依旧存在单点失效问题。 当 namenode 失效时,要想恢复服务,系统管理员需要启动一个拥有文件系统元数据副本的新的 namenode,并配置 datanode 和客户端以便使用这个新的 namenode。新的 namenode 直到满足以下情形才能响应服务: 将命名空间的映像导入内存中; 重做编辑日志; 接收到足够多的来自 datanode 的数据块报告并退出安全模式。 对于一个大型并拥有大量文件和数据块的集群,namenode 的冷启动需要 30 分钟甚至更长时间。 Hadoop 2.x 发行版针对上述问题在 HDFS 中增加了对高可用的支持。在这一实现中,配置了一对活动-备用 namenode。当活动 namenode 失效,备用 namenode 接会接管它的任务并开始服务于来自客户端的请求,不会有任何明显中断。实现这一目标需要在架构上做如下修改: namenode 之间需要通过高可用的共享存储实现编辑日志的共享; datanode 需要同时向两个 namenode 发送数据块处理报告; 客户端需要使用特定的机制来处理 namenode 的失效问题,这一机制应当对用户透明。 Hadoop 文件系统Hadoop 有一个抽象的文件系统概念,HDFS 只是其中的一个实现。Java 抽象类 org.apache.hadoop.fs.FileSystem定义了 Hadoop 中的一个文件系统接口,并且包含一些具体的实现,包括:Local、HDFS、HFTP、HSFTP、WebHDFS、HAR、hfs、FTP、S3等。 数据流文件读取待补充 文件写入待补充 一致模型待补充]]></content>
<categories>
<category>Hadoop</category>
</categories>
<tags>
<tag>HDFS</tag>
</tags>
</entry>
<entry>
<title><![CDATA[关于 MapReduce]]></title>
<url>%2F2018%2F10%2F15%2Fhadoop%2F2-%E5%85%B3%E4%BA%8EMapReduce%2F</url>
<content type="text"><![CDATA[使用 Hadoop 来分析数据map 和 reduceMapReduce 任务过程分为两个阶段:map 阶段和 reduce 阶段。每个阶段都以键值对作为输入和输出,其类型由程序员选择。 数据流Hadoop 在存储有输入数据(HDFS 中的数据)的节点上运行 map 任务,可以获得最佳性能,这就是所谓的“数据本地化优化”,因为这样不需要使用集群的带宽资源。但是,有时候对于一个 map 任务的输入来说,存储有某个 HDFS 数据块备份的三个节点可能正在运行其他 map 任务,此时作业调度需要在三个备份中的某个备份(数据块)寻找同个机架中空闲的机器来运行该 map 任务。仅仅在非常偶然的情况下(基本不会发生),会使用其他机架中的机器运行该 map 任务,这将导致机架与机架之间的网络传输。 这也是为什么最佳分片的大小应该与块大小相同,因为它是确保可以存储在单个节点上的最大输入块的大小。如果分片跨越两个数据块,那么对于任何一个 HDFS 节点,基本上都不可能同时存储这两个数据块,因此分片中的部分数据需要通过网络传输到 map 任务节点。与使用本地数据运行整个 map 任务相比,显然是低效的。 map 的输出一般是写入本地磁盘而非 HDFS,因为 map 的输出是中间结果,该输出需要传递给 reduce 后才产生最终输出结果,而一旦作业完成,map 的输出结果就可以删除。因此,如果把它存储在 HDFS 上,难免有些小题大做。当然如果该节点上运行的 map 任务在传送中间结果给 reduce 时出错,Hadoop 会在另一个节点上重新运行这个 map 任务以再次构建 map 中间结果。 reduce 任务并不具备数据本地化的优势,因为 reduce 的输入通常总是来自于很多 mapper 的输出。 reduce 任务的数量并非有输入数据的大小决定,而事实上是独立指定的。如果有多个 reduce 任务,每个 map 任务就会针对输出进行分区(partition),也就是为每个 reduce 任务建一个分区。分区由用户定义的 partition 函数控制,但通常用默认的 partitioner 通过哈希函数来区分就已经很高效了。 combiner 函数经过上面的介绍,我们很很容易地知道,尽量减少 map 和 reduce 之间的数据传输是有利的。Hadoop 允许我们针对 map 任务的输出指定一个 combiner 函数,该函数的输出作为 reduce 的输入。combiner 属于优化方案,不管调用 combiner 多少次,reduce 的输出结果应该都一样。 通俗地说就是能够提前合并的可以尽量通过 combiner 在 map 端就合并,比如求最值,求合这类计算,提前算和最后一起算结果都是一样的,但是注意:求平均值就不是了。 12345max(0, 20, 10, 25, 15) = 25max(max(0, 20, 10), max(25, 15)) = max(20, 25) = 25mean(0, 20, 10, 25, 15) = 14mean(mean(0, 20, 10), mean(25, 15)) = mean(10, 20) = 15]]></content>
<categories>
<category>Hadoop</category>
</categories>
<tags>
<tag>MapReduce</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Jvm内存区域]]></title>
<url>%2F2018%2F10%2F10%2Finterview%2FJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%2F</url>
<content type="text"><![CDATA[前言常见面试题基本问题 介绍下 Java 内存区域(运行时数据区) Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么) 对象的访问定位的两种方式(句柄和直接指针两种方式) 拓展问题 String 类和常量池 8 种基本类型的包装类和常量池 概述对于 Java 程序员来说,在虚拟机自动内存管理机制下,不需要像 C/C++ 程序开发程序员这样为每一个 new操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给了 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。 运行时的数据区域Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。 这些组成部分一些事线程私有的,其他的则是线程共享的。 线程私有的: 程序计数器 虚拟机栈 本地方法栈 线程共享的: 堆 方法区 直接内存 程序计数器程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 从上面的介绍中我们知道程序计数器主要有两个作用: 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。 Java 虚拟机栈与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型。 Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。 局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。 Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。 StackOverFlowError: 如果 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就会抛出 StackOverFlowError 异常,比如在递归调用时。 OutOfMemoryError: 如果 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。 Java 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。 本地方法栈和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。 堆Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在的收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代。再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。 进一步划分的目的是更好地回收内存,或者更快地分配内存。 在 JDK 1.8 中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM 的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。 推荐阅读: 《Java 8 内存模型—永久代(PermGen)和元空间(Metaspace)》:http://www.cnblogs.com/paddix/p/5309550.html 方法区方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来的。 HotSpot 虚拟机中方法区也常被称为“永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。 相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。 运行时常量池运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)。 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError 异常。 JDK 1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。 推荐阅读: 《Java 中几种常量池的区分》: https://blog.csdn.net/qq_26222859/article/details/73135660 直接内存直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError异常出现。 JDK 1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据(Netty 中有大量使用)。 本机直接内存的分配不会收到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。 HotSpot 虚拟机对象探秘TODO:未完待续]]></content>
<categories>
<category>JVM</category>
</categories>
</entry>
<entry>
<title><![CDATA[深入学习Gremlin(22):遍历终止操作]]></title>
<url>%2F2018%2F10%2F08%2Fhugegraph%2F%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Gremlin%EF%BC%8822%EF%BC%89%EF%BC%9A%E9%81%8D%E5%8E%86%E7%BB%88%E6%AD%A2%E6%93%8D%E4%BD%9C%2F</url>
<content type="text"><![CDATA[第22期 Gremlin Steps: hasNext()、next()、tryNext()、toList()、toSet()、toBulkSet()、fill()、iterate() 本系列文章的Gremlin示例均在HugeGraph图数据库上执行,环境搭建可参考准备Gremlin执行环境,本文示例均以其中的“TinkerPop关系图”为初始数据。 上一期:深入学习Gremlin(21):待添加 说明Gremlin 中有一类特殊的操作,它能够终止遍历器的“遍历”行为,使其执行并返回结果。在这里要强调的一点:原生的 Gremlin 语句通常都是用遍历器连接起来的,但其实这些连接的过程并不会执行 Gremlin 语句,只有走到了terminalStep 时才会执行。这个模式类似于 Spark 中对RDD的map和action操作。 hasNext: 判断遍历器是否含有元素(结果),返回布尔值; next: 不传参数时获取遍历器的下一个元素,也可以传入一个整数 n,则获取后面 n 个元素; tryNext: hasNext和next的结合版,返回一个Optional对象,如果有结果还需要调用get()方法才能拿到; toList: 将所有的元素放到一个List中返回; toSet: 将所有的元素放到一个Set中返回,会去除重复元素; toBulkSet: 将所有的元素放到一个能排序的List中返回,重复元素也会保留; fill: 传入一个集合对象,将所有的元素放入该集合并返回,其实toList、toSet和toBulkSet就是通过fillStep实现的; iterate: 这个 Step 在终止操作里面有点特殊,它并不完全符合终止操作的定义。它会在内部迭代完整个遍历器但是不返回结果。 那肯定有细心的同学要问了,前面我们介绍了那么多的 Step 很多都没有加terminalStep 啊,为什么也能返回结果呢?其实这是 Tinkerpop 的 Gremlin 解析引擎对遍历器对象调用了一个IteratorUtils.asList()方法,又调用了它内部的fill()方法(注意:不是上面讲到的fill()Step)。 实例讲解下面通过实例来深入理解每一个操作。 Step hasNext() 示例1: 12// 判断顶点“linary”是否包含“created”出顶点g.V('linary').out('created').hasNext() 示例2: 12// 判断顶点“linary”是否包含“knows”出顶点g.V('linary').out('knows').hasNext() Step next() 示例1: 12// 获取顶点“javeme”的“knows”出顶点集合的下一个(第1个)g.V('javeme').out('knows').next() g.V('javeme').out('knows')返回的是一个遍历器(迭代器),每次执行这句话实际上都是获取的迭代器的第一个元素,那如果想获取第二个元素该怎么写呢?很简单,执行两次next()即可,但是这里的前提条件是遍历器中确实存在多个元素。 示例2: 1234// 获取顶点“javeme”的“knows”出顶点集合的下一个(第2个)it = g.V('javeme').out('knows')it.next()it.next() 示例3: 12// 获取顶点“javeme”的“knows”出顶点集合的前两个g.V('javeme').out('knows').next(2) next()与next(n)使用中有一点小小的区别,就是当没有元素或者没有足够多的元素时,执行next()会报错,但是执行next(n)则是返回一个空集合(List)。 Step tryNext() 示例1: 12// 试图获取顶点“javeme”的“created”出顶点集合中的下一个g.V('javeme').out('created').tryNext() 这里细心的读者会发现结果与前面概述中说的有些不同。概述中说的是返回一个Optional对象,要获取Optional对象里的值是需要调用它的get()方法的,怎么这里直接就把值给返回了呢?大家先别着急,我们再看一个例子。 示例2: 12// 试图获取顶点“javeme”的“created”入顶点集合中的下一个g.V('javeme').in('created').tryNext() 这里更加令人费解,没有“created”入顶点时竟然直接报错了,其实这是HugeGraph的实现中关于Optional的序列化所致。HugeGraph序列化Optional对象时会判断该对象内的值是否存在,如果存在则取出来序列化该值,否则填入一个null。详细代码见HugeGraphSONModule.java中关于OptionalSerializer的实现。 本文的重点在于学习Gremlin语法本身,下面给出上述两个示例的预期结果: 12Optional[v[3:HugeGraph]]Optional.empty Step toList() 示例1: 12// 获取所有“person”顶点的“created”出顶点集合,放入List中,允许包含重复结果g.V().hasLabel('person').out('created').toList() 示例2: 12// 获取所有“person”顶点的“created”入顶点集合,放入List中,允许包含重复结果g.V().hasLabel('person').in('created').toList() 结果与next(n)有些类似。 Step toSet() 示例1: 12// 获取所有“person”顶点的“created”出顶点集合,放入Set中,不允许包含重复结果g.V().hasLabel('person').out('created').toSet() 相比于toList,toSet去除了重复元素。 示例2: 12// 获取所有“person”顶点的“created”入顶点集合,放入Set中,不允许包含重复结果g.V().hasLabel('person').in('created').toSet() Step toBulkSet() 示例1: 12// 获取所有“person”顶点的“created”出顶点集合,放入BulkSet中,允许包含重复结果,排序g.V().hasLabel('person').out('created').toBulkSet() 所谓的BulkSet虽然名字上带有”Set”,但还是更像一个List,对比toList的结果,它实际上是把所有元素排了个序。 Step fill() 示例1: 1234// 创建一个List,获取所有“person”顶点的“created”出顶点,并放入该List中results = []g.V().hasLabel('person').out('created').fill(results)results Step iterate() 示例1: 123// 迭代所有“person”顶点it = g.V().hasLabel('person').iterate()it.hasNext() 调用了iterate()后遍历器内部的元素就已经全部迭代过了,所以再调用hasNext()返回false。 下一期:深入学习Gremlin(23):待添加]]></content>
<categories>
<category>hugegraph</category>
</categories>
<tags>
<tag>gremlin</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入学习Gremlin(23):转换操作]]></title>
<url>%2F2018%2F10%2F08%2Fhugegraph%2F%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Gremlin%EF%BC%8823%EF%BC%89%EF%BC%9A%E8%BD%AC%E6%8D%A2%E6%93%8D%E4%BD%9C%2F</url>
<content type="text"><![CDATA[第23期 Gremlin Steps: map、flatMap() 本系列文章的Gremlin示例均在HugeGraph图数据库上执行,环境搭建可参考准备Gremlin执行环境,本文示例均以其中的“TinkerPop关系图”为初始数据。 上一期:深入学习Gremlin(22):遍历终止操作 转换操作说明 map: 可以接受一个遍历器 Step 或 Lamda 表达式,将遍历器中的元素映射(转换)成另一个类型的某个对象(一对一),以便进行下一步处理; flatMap: 可以接受一个遍历器 Step 或 Lamda 表达式,将遍历器中的元素映射(转换)成另一个类型的某个对象流或迭代器(一对多)。 实例讲解下面通过实例来深入理解每一个操作。 Step map() 示例1: 123// 获取顶点“3:HugeGraph”的入“created”顶点的“name”属性,其实可以理解为顶点对象转化成了属性值对象g.V('3:HugeGraph').in('created').map(values('name'))// g.V('3:HugeGraph').in('created').map {it.get().value('name')} 自己动手将g.V('3:HugeGraph').in('created').map {it.get().value('name')}的注视打开试试效果。 示例2: 12// 先获取顶点“3:HugeGraph”的入“created”顶点,再将每个顶点转化为出边(一条)g.V('3:HugeGraph').in('created').map(outE()) 注意:顶点“javeme”其实是有三条边的,但是这里只打印出了一条。因为mapStep是一对一的转换,要想获取所有的边可以使用flatMap。 Step flatMap() 示例1: 12// 先获取顶点“3:HugeGraph”的入“created”顶点,再将每个顶点转化为出边(多条)g.V('3:HugeGraph').in('created').flatMap(outE()) 注意:这一次就能打印出顶点“javeme”的全部三条边了。 下一期:深入学习Gremlin(24):待添加]]></content>
<categories>
<category>hugegraph</category>
</categories>
<tags>
<tag>gremlin</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入学习Gremlin(18):随机过滤与注入]]></title>
<url>%2F2018%2F09%2F30%2Fhugegraph%2F%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Gremlin%EF%BC%8818%EF%BC%89%EF%BC%9A%E9%9A%8F%E6%9C%BA%E8%BF%87%E6%BB%A4%E4%B8%8E%E6%B3%A8%E5%85%A5%2F</url>
<content type="text"><![CDATA[第18期 Gremlin Steps: sample()、coin()、constant()、inject() 本系列文章的Gremlin示例均在HugeGraph图数据库上执行,环境搭建可参考准备Gremlin执行环境,本文示例均以其中的“TinkerPop关系图”为初始数据。 上一期:深入学习Gremlin(17):待添加 随机过滤说明Gremlin支持对遍历器(traversal)上的结果进行采样或者做随机过滤。 sample: 接受一个整数值,从前一步的遍历器中采样(随机)出最多指定数目的结果; coin: 字面意思是抛硬币过滤,接受一个浮点值,该浮点值表示硬币出现正面的概率。coin Step 对前一步的遍历器中的每个元素都抛一次硬币,出现正面则可以通过,反面则被拦截。 sampleStep后能接上byStep,能以指定的属性为判断依据进行随机过滤。 注入说明Gremlin允许在遍历器中注入一些默认值或自定义值,比如在分支 Step 中给 else 路径的元素一个默认值,又或者在遍历器过程中人为地加上一些额外的元素。 constant: 通常用在choose或coalesceStep中做辅助输出,为那些不满足条件的元素提供一个默认值; inject: 能够在流(遍历器)的任何位置注入与当前遍历器同输出类型的对象,当然,也可以作为流的起始 Step 产生数据; inject只是在查询过程中添加一些额外的元素,并没有把数据真正地插入到图中 实例讲解下面通过实例来深入理解每一个操作。 Step sample() 示例1: 12// 从所有顶点的出边中随机选择2条g.V().outE().sample(2) 由于sample是随机采样,所以运行结果每次都可能不一样。另外,sample(n)表示最多采样n个,如果上一步不够n个元素自然结果是会小于n的。 示例2: 12// 从所以顶点的“name”属性中随机选取3个g.V().values('name').sample(3) 示例3: 12// 从所有的“person”中根据“age”随机选择3个g.V().hasLabel('person').sample(3).by('age') 示例4:与local联合使用做随机漫游(从某个顶点出发,随机选一条边,走到边上的邻接点;再以该点为起点,继续随机选择边,走到邻接点…) 12345// 从顶点“HugeGraph”出发做3次随机漫游g.V('3:HugeGraph') .repeat(local(bothE().sample(1).otherV())) .times(3) .path() 第1次:从“HugeGraph”沿着“Szhoney>2>>S3:HugeGraph”走到“zhoney” 第2次:从“zhoney”沿着“Sjaveme>1>>Szhoney”走到“javeme” 第3次:从“javeme”沿着“Sjaveme>1>>Slinary”走到“linary” Step coin() 示例1: 12// 每个顶点按0.5的概率过滤g.V().coin(0.5) 这一次输出了5个顶点,而总共是有12个顶点的,为什么不是输出6个呢?学过概率论的应该都知道,不解释。我又多执行了两次,输出顶点数分别是5和7。 示例2: 12// 每个顶点按0.0的概率过滤g.V().coin(0.0) 示例3: 12// 每个顶点按1.0的概率过滤g.V().coin(1.0).count() 避免输出太长,加上count。 Step constant() 示例1: 1234// 输出所有“person”类顶点的“name”属性,否则输出“inhuman”(非人类)g.V().choose(hasLabel('person'), values('name'), constant('inhuman')) 示例2: 123// 与示例1功能相同,使用“coalesce”Step 实现g.V().coalesce(hasLabel('person').values('name'), constant('inhuman')) Step inject() 示例1: 12// 给顶点“HugeGraph”的作者添加一个叫“Tom”的人g.V('3:HugeGraph').in('created').values('name').inject('Tom') 示例2: 123// 在示例1的基础上计算每个元素的长度(“name”属性值的长度)g.V('3:HugeGraph').in('created').values('name').inject('Tom') .map {it.get().length()} 可以看到,注入的元素“Tom”与原生的元素一样参与了计算 示例3: 123// 在示例2的基础上计算走过的路径g.V('3:HugeGraph').in('created').values('name').inject('Tom') .map {it.get().length()}.path() 这里又能看出注入元素“Tom”与原生的元素的区别,由于“Tom”是在获取“name”属性这一步时才注入的,所以之前的从顶点“HugeGraph”出发,获取其“created”的入顶点这两步“Tom”是没有的。 示例4: 12// 使用inject创建出两个元素(顶点的id),并使用该元素作为id获取顶点及其属性“name”inject('javeme', 'linary', 'zhoney').map {g.V(it.get()).next()}.values('name') 12// 使用inject创建出一个“person”(顶点label),并使用该元素作为label获取顶点及其属性“name”inject('person').flatMap {g.V().hasLabel(it.get())}.values('name') 下一期:深入学习Gremlin(19):待添加]]></content>
<categories>
<category>hugegraph</category>
</categories>
<tags>
<tag>gremlin</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JAVA NIO 之 Buffer]]></title>
<url>%2F2018%2F09%2F28%2Fstudy-netty%2FBuffer%E8%A7%A3%E6%9E%90%2F</url>
<content type="text"><![CDATA[原文:https://segmentfault.com/a/1190000006824155 Java NIO Buffer 当我们需要与 NIO Channel 进行交互时,我们就需要使用到 NIO Buffer,即数据从 Buffer写入到 Channel 中,并且从 Channel 中读取到 Buffer 中。 实际上,NIO Buffer 其实是一块内存区域的封装,并提供了一些操作方法让我们能够方便地进行数据的读写。 Buffer 类型有: ByteBuffer CharBuffer DoubleBuffer FloatBuffer IntBuffer LongBuffer ShortBuffer 这些 Buffer 已经覆盖了能从 IO 中传输的所有的 Java 基本数据类型。 NIO Buffer 的基本使用使用 NIO Buffer 的步骤如下: 将 Channel 中的数据读取到 Buffer 中,对于 Buffer 本身处于写模式 调用 Buffer.flip() 方法,将 NIO Buffer 转换为读模式. 从 Buffer 中读取数据 调用 Buffer.clear() 或 Buffer.compact() 方法,将 Buffer 转换为写模式 当我们将数据写入到 Buffer 中时,Buffer 会记录我们已经写了多少的数据。当我们需要从 Buffer 中读取数据时,必须调用 Buffer.flip() 将 Buffer 切换为读模式。 一旦读取了所有的 Buffer 数据,那么我们必须清理 Buffer,让其变为重新可写的,清理 Buffer 可以调用 Buffer.clear() 或 Buffer.compact()。 示例: 12345678910public class Test { public static void main(String[] args) { IntBuffer intBuffer = IntBuffer.allocate(2); intBuffer.put(12345678); intBuffer.put(2); intBuffer.flip(); System.err.println(intBuffer.get()); System.err.println(intBuffer.get()); }} 上面代码中,我们分配两个单位大小的 IntBuffer,因此它可以写入两个 int 值。我们使用 put 方法将 int 值写入,然后使用 flip 方法将 buffer 转换为读模式,然后连续使用 get 方法从 buffer 中获取这两个 int 值。 每当调用一次 get 方法读取数据时,buffer 的读指针都会向前移动一个单位长度(在这里是一个 int 长度) Buffer 属性一个 Buffer 有三个属性: capacity position limit 其中 position 和 limit 的含义与 Buffer 处于读模式或写模式有关,而 capacity 的含义与 Buffer 所处的模式无关。 Capacity 一个内存块会有一个固定的大小,即容量(capacity),我们最多写入 capacity 个单位的数据到 Buffer 中,例如一个 DoubleBuffer,其 Capacity 是 100,那么我们最多可以写入 100 个 double 数据。 Position 当从一个 Buffer 中写入数据时,我们是从 Buffer 的一个确定的位置(position)开始写入的。在最初的状态时,position 的值是 0。每当我们写入了一个单位的数据后,position 就会递增 1。 当我们从 Buffer 中读取数据时,我们也是从某个特定的位置开始读取的。当我们调用了 filp() 方法将 Buffer 从写模式转换到读模式时,position 的值会自动被设置为0。每当我们读取一个单位的数据,position 的值递增 1。 position 表示了读写操作的位置指针。 limit limit - position 表示此时还可以写入/读取多少单位的数据。例如在写模式,如果此时 limit 是 10,position 是 2,则表示已经写入了 2 个单位的数据,还可以写入 10 - 2 = 8 个单位的数据。 示例: 1234567891011121314151617public class Test { public static void main(String args[]) { IntBuffer intBuffer = IntBuffer.allocate(10); intBuffer.put(10); intBuffer.put(101); System.err.println("Write mode: "); System.err.println("\tCapacity: " + intBuffer.capacity()); System.err.println("\tPosition: " + intBuffer.position()); System.err.println("\tLimit: " + intBuffer.limit()); intBuffer.flip(); System.err.println("Read mode: "); System.err.println("\tCapacity: " + intBuffer.capacity()); System.err.println("\tPosition: " + intBuffer.position()); System.err.println("\tLimit: " + intBuffer.limit()); }} 这里我们首先写入两个 int 值,此时 capacity = 10,position = 2,limit = 10;然后我们调用 flip 转换为读模式, 此时 capacity = 10,position = 0,limit = 2。 分配 Buffer为了获取一个 Buffer 对象,我们首先需要分配内存空间。每个类型的 Buffer 都有一个 allocate() 方法,我们可以通过这个方法分配 Buffer: 1ByteBuffer buf = ByteBuffer.allocate(48); 这里我们分配了 48 * sizeof(Byte) 字节的内存空间。 1CharBuffer buf = CharBuffer.allocate(1024); 这里我们分配了大小为 1024 个字符的 Buffer,即这个 Buffer 可以存储 1024 个 Char,其大小为 1024 * 2 个字节。 Direct Buffer 和 Non-Direct Buffer 的区别Direct Buffer: 所分配的内存不在 JVM 堆上,不受 GC 的管理。(但是 Direct Buffer 的 Java 对象是由 GC 管理的,因此当发生 GC,对象被回收时,Direct Buffer 也会被释放); 因为 Direct Buffer 不在 JVM 堆上分配,因此 Direct Buffer 对应用程序的内存占用的影响就不那么明显(实际上还是占用了这么多内存,但是 JVM 不好统计到非 JVM 管理的内存) 申请和释放 Direct Buffer 的开销比较大。因此正确的使用 Direct Buffer 的方式是在初始化时申请一个 Buffer,然后不断复用此 buffer,在程序结束后才释放此 buffer。 使用 Direct Buffer 时,当进行一些底层的系统 IO 操作时,效率会比较高,因为此时 JVM 不需要拷贝 buffer 中的内存到中间临时缓冲区中。 Non-Direct Buffer: 直接在 JVM 堆上进行内存的分配,本质上是 byte[] 数组的封装。 因为 Non-Direct Buffer 在 JVM 堆中,因此当进行操作系统底层 IO 操作中时,会将此 buffer 的内存复制到中间临时缓冲区中,因此 Non-Direct Buffer 的效率较低。 Buffer 的读写写入数据到 Buffer 123// read into buffer.int bytesRead = inChannel.read(buf); buf.put(127); 从 Buffer 中读取数据 123// read from buffer into channel.int bytesWritten = inChannel.write(buf);byte aByte = buf.get(); 重置 positionBuffer.rewind() 方法可以重置 position 的值为0,因此我们可以重新读取/写入 Buffer 了。如果是读模式,则重置的是读模式的 position,如果是写模式,则重置的是写模式的 position。 示例: 1234567891011121314151617181920212223public class Test { public static void main(String[] args) { IntBuffer intBuffer = IntBuffer.allocate(2); intBuffer.put(1); intBuffer.put(2); System.err.println("position: " + intBuffer.position()); intBuffer.rewind(); System.err.println("position: " + intBuffer.position()); intBuffer.put(1); intBuffer.put(2); System.err.println("position: " + intBuffer.position()); intBuffer.flip(); System.err.println("position: " + intBuffer.position()); intBuffer.get(); intBuffer.get(); System.err.println("position: " + intBuffer.position()); intBuffer.rewind(); System.err.println("position: " + intBuffer.position()); }} rewind() 主要针对于读模式,在读模式时,读取到 limit 后,可以调用 rewind() 方法,将读 position 置为 0。 关于 mark() 和 reset()我们可以通过调用 Buffer.mark() 将当前的 position 的值保存起来,随后可以通过调用 Buffer.reset() 方法将 position 的值恢复回来。 示例: 1234567891011121314151617public class Test { public static void main(String[] args) { IntBuffer intBuffer = IntBuffer.allocate(2); intBuffer.put(1); intBuffer.put(2); intBuffer.flip(); System.err.println(intBuffer.get()); System.err.println("position: " + intBuffer.position()); intBuffer.mark(); System.err.println(intBuffer.get()); System.err.println("position: " + intBuffer.position()); intBuffer.reset(); System.err.println("position: " + intBuffer.position()); System.err.println(intBuffer.get()); }} 这里我们写入两个 int 值,然后首先读取了一个值。此时读 position 的值为 1。接着我们调用 mark() 方法将当前的 position 保存起来(在读模式,因此保存的是读的 position),然后再次读取,此时 position 就是 2 了。接着使用 reset() 恢复原来的读 position,因此读 position 又为 1 了,可以再次读取数据。 flip, rewind 和 clear 的区别flip flip 方法源码 123456public final Buffer flip() { limit = position; position = 0; mark = -1; return this;} Buffer 的读/写模式共用一个 position 和 limit 变量,当从写模式变为读模式时,原先的 写 position 就变成了读模式的 limit。 rewind rewind 方法源码 12345public final Buffer rewind() { position = 0; mark = -1; return this;} rewind,即倒带,这个方法仅仅是将 position 置为 0。 clear clear 方法源码 123456public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this;} 根据源码我们可以知道,clear 将 positin 设置为 0,将 limit 设置为 capacity。 clear 方法使用场景: 在一个已经写满数据的 buffer 中,调用 clear,可以从头读取 buffer 的数据; 为了将一个 buffer 填充满数据,可以调用 clear,然后一直写入,直到达到 limit。 示例: 123456789101112131415161718IntBuffer intBuffer = IntBuffer.allocate(2);intBuffer.flip();System.err.println("position: " + intBuffer.position());System.err.println("limit: " + intBuffer.limit());System.err.println("capacity: " + intBuffer.capacity());// 这里不能读, 因为 limit == position == 0, 没有数据.//System.err.println(intBuffer.get());intBuffer.clear();System.err.println("position: " + intBuffer.position());System.err.println("limit: " + intBuffer.limit());System.err.println("capacity: " + intBuffer.capacity());// 这里可以读取数据了, 因为 clear 后, limit == capacity == 2, position == 0,// 即使我们没有写入任何的数据到 buffer 中.System.err.println(intBuffer.get()); // 读取到0System.err.println(intBuffer.get()); // 读取到0 Buffer 的比较我们可以通过 equals() 或 compareTo() 方法比较两个 Buffer,当且仅当如下条件满足时,两个 Buffer 是相等的: 两个 Buffer 是相同类型的 两个 Buffer 的剩余的数据个数是相同的 两个 Buffer 的剩余的数据都是相同的. 通过上述条件我们可以发现,比较两个 Buffer 时,并不是 Buffer 中的每个元素都进行比较,而是比较 Buffer 中剩余的元素。]]></content>
<categories>
<category>网络通信</category>
</categories>
<tags>
<tag>Buffer</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Netty源码分析的调试环境准备]]></title>
<url>%2F2018%2F09%2F27%2Fstudy-netty%2F%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87%2F</url>
<content type="text"><![CDATA[原文:http://svip.iocoder.cn/Netty/build-debugging-environment/ 前言Netty 底层基于 JDK 的 NIO,我们为什么不直接基于 JDK 的 NIO 或者其他 NIO 框架?下面是我总结出来的原因 使用 JDK 自带的 NIO 需要了解太多的概念,编程复杂 Netty 底层 IO 模型随意切换,而这一切只需要做微小的改动 Netty 自带的拆包解包,异常检测等机制让你从 NIO 的繁重细节中脱离出来,让你只需要关心业务逻辑 Netty 解决了 JDK 的很多包括空轮训在内的 bug Netty 底层对线程,selector 做了很多细小的优化,精心设计的 reactor 线程做到非常高效的并发处理 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手 Netty 社区活跃,遇到问题随时邮件列表或者 issue Netty 已经历各大 rpc 框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大 环境依赖 JDK Git Maven IntelliJ IDEA ###]]></content>
<categories>
<category>网络通信</category>
</categories>
<tags>
<tag>Netty</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入学习Gremlin(15):分支运算]]></title>
<url>%2F2018%2F09%2F25%2Fhugegraph%2F%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Gremlin%EF%BC%8815%EF%BC%89%EF%BC%9A%E5%88%86%E6%94%AF%E8%BF%90%E7%AE%97%2F</url>
<content type="text"><![CDATA[第15期 Gremlin Steps: coalesce()、optional()、union() 本系列文章的Gremlin示例均在HugeGraph图数据库上执行,环境搭建可参考准备Gremlin执行环境,本文示例均以其中的“TinkerPop关系图”为初始数据。 上一期:深入学习Gremlin(14):待添加 分支操作说明 coalesce: 可以接受任意数量的遍历器(traversal),按顺序执行,并返回第一个能产生输出的遍历器的结果; optional: 只能接受一个遍历器(traversal),如果该遍历器能产生一个结果,则返回该结果,否则返回调用optionalStep的元素本身。当连续使用.optional()时,如果在某一步返回了调用元素本身,则后续的.optional()不会继续执行; union: 可以接受任意数量的遍历器(traversal),并能够将各个遍历器的输出合并到一起; 实例讲解下面通过实例来深入理解每一个操作。 Step coalesce() 示例1: 12345// 按优先级寻找到顶点“HugeGraph”的以下边和邻接点,找到一个就停止// 1、“implements”出边和邻接点// 2、“supports”出边和邻接点// 3、“created”入边和邻接点g.V('3:HugeGraph').coalesce(outE('implements'), outE('supports'), inE('created')).inV().path().by('name').by(label) HugeGraph这三类边都是存在的,按照优先级,返回了“implements”出边和邻接点。 示例2: 12345// 按优先级寻找到顶点“HugeGraph”的以下边和邻接点,找到一个就停止(调换了示例1中的1和2的顺序)// 1、“supports”出边和邻接点// 2、“implements”出边和邻接点// 3、“created”入边和邻接点g.V('3:HugeGraph').coalesce(outE('supports'), outE('implements'), inE('created')).inV().path().by('name').by(label) 这次由于“supports”放在了“implements”的前面,所以返回了“supports”出边和邻接点。 自己动手比较一下outE('supports'), outE('implements'), inE('created')在.coalesce()中随意调换顺序的区别。 Step optional() 示例1: 12// 查找顶点"linary"的“created”出顶点,如果没有就返回"linary"自己g.V('linary').optional(out('created')) 示例2: 12// 查找顶点"linary"的“knows”出顶点,如果没有就返回"linary"自己g.V('linary').optional(out('knows')) 示例3: 12// 查找每个“person”顶点的出“knows”顶点,如果存在,然后以出“knows”顶点为起点,继续寻找其出“created”顶点,最后打印路径g.V().hasLabel('person').optional(out('knows').optional(out('created'))).path() 结果中的后面四个顶点因为没有出“knows”顶点,所以在第一步返回了自身后就停止了。 Step union() 示例1: 12// 寻找顶点“linary”的出“created”顶点,邻接“knows”顶点,并将结果合并g.V('linary').union(out('created'), both('knows')).path() 示例2: 12// 寻找顶点“HugeGraph”的入“created”顶点(创作者),出“implements”和出“supports”顶点,并将结果合并g.V('3:HugeGraph').union(__.in('created'), out('implements'), out('supports'), out('contains')).path() 顶点“HugeGraph”没有“contains”边,所以只打印出了其作者(入“created”),它实现的框架(出“implements”)和支持的特性(出“supports”)。 下一期:深入学习Gremlin(16):待添加]]></content>
<categories>
<category>hugegraph</category>
</categories>
<tags>
<tag>gremlin</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入学习Gremlin(11):统计运算]]></title>
<url>%2F2018%2F09%2F22%2Fhugegraph%2F%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Gremlin%EF%BC%8811%EF%BC%89%EF%BC%9A%E7%BB%9F%E8%AE%A1%E8%BF%90%E7%AE%97%2F</url>
<content type="text"><![CDATA[第11期 Gremlin Steps: sum()、max()、min()、mean() 本系列文章的Gremlin示例均在HugeGraph图数据库上执行,环境搭建可参考准备Gremlin执行环境,本文示例均以其中的“TinkerPop关系图”为初始数据。 上一期:深入学习Gremlin(10):逻辑运算 统计运算说明Gremlin可以在Number类型的流(遍历器)上做简单的统计运算,包括计算总和、最大值、最小值、均值。 下面讲解实现上述功能的具体Step: sum():将流上的所有的数字求和; max():对流上的所有的数字求最大值; min():对流上的所有的数字求最小值; mean():将流上的所有的数字求均值; 这四种Step只能作用在Number类型的流上,在java里就是继承自java.lang.Number类。 实例讲解下面通过实例来深入理解每一个操作。 Step sum() 示例1: 12// 计算所有“person”的“age”的总和g.V().hasLabel('person').values('age').sum() 示例2: 12// 计算所有“person”的“created”出边数的总和g.V().hasLabel('person').map(outE('created').count()).sum() 试着输入g.V().hasLabel('person').map(outE('created').count())看看每个“person”的“created”出边数 Step max() 示例1: 12// 计算所有“person”的“age”中的最大值g.V().hasLabel('person').values('age').max() 示例2: 12// 计算所有“person”的“created”出边数的最大值g.V().hasLabel('person').map(outE('created').count()).max() Step min() 示例1: 12// 计算所有“person”的“age”中的最小值g.V().hasLabel('person').values('age').min() 示例2: 12// 计算所有“person”的“created”出边数的最小值g.V().hasLabel('person').map(outE('created').count()).min() Step mean() 示例1: 12// 计算所有“person”的“age”的均值g.V().hasLabel('person').values('age').mean() 示例2: 12// 计算所有“person”的“created”出边数的均值g.V().hasLabel('person').map(outE('created').count()).mean() 下一期:深入学习Gremlin(12):待添加]]></content>
<categories>
<category>hugegraph</category>
</categories>
<tags>
<tag>gremlin</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入学习Gremlin(10):逻辑运算]]></title>
<url>%2F2018%2F09%2F21%2Fhugegraph%2F%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Gremlin%EF%BC%8810%EF%BC%89%EF%BC%9A%E9%80%BB%E8%BE%91%E8%BF%90%E7%AE%97%2F</url>
<content type="text"><![CDATA[第10期 Gremlin Steps: is()、and()、or()、not() 本系列文章的Gremlin示例均在HugeGraph图数据库上执行,环境搭建可参考准备Gremlin执行环境,本文示例均以其中的“TinkerPop关系图”为初始数据。 上一期:深入学习Gremlin(9):条件和过滤 逻辑运算说明Gremlin支持在遍历器上加上逻辑运算进行过滤,只有满足该逻辑条件的元素才会进入下一个遍历器中。 下面讲解实现上述功能的具体Step: is():可以接受一个对象(能判断相等)或一个判断语句(如:P.gt()、P.lt()、P.inside()等),当接受的是对象时,原遍历器中的元素必须与对象相等才会保留;当接受的是判断语句时,原遍历器中的元素满足判断才会保留,其实接受一个对象相当于P.eq(); and():可以接受任意数量的遍历器(traversal),原遍历器中的元素,只有在每个新遍历器中都能生成至少一个输出的情况下才会保留,相当于过滤器组合的与条件; or():可以接受任意数量的遍历器(traversal),原遍历器中的元素,只要在全部新遍历器中能生成至少一个输出的情况下就会保留,相当于过滤器组合的或条件; not():仅能接受一个遍历器(traversal),原遍历器中的元素,在新遍历器中能生成输出时会被移除,不能生成输出时则会保留,相当于过滤器的非条件。 这四种逻辑运算Step除了像一般的Step写法以外,and()和or()还可以放在where()中以中缀符的形式出现。 实例讲解下面通过实例来深入理解每一个操作。 Step is() 示例1: 12// 筛选出顶点属性“age”等于28的属性值,与`is(P.eq(28))`等效g.V().values('age').is(28) 当没有任何一个顶点的属性“age”为28时,输出为空。 示例2: 12// 筛选出顶点属性“age”大于等于28的属性值g.V().values('age').is(gte(28)) 示例3: 12// 筛选出顶点属性“age”属于区间(27,29)的属性值g.V().values('age').is(inside(27, 29)) P.inside(a, b)是左开右开区间(a,b) 示例4: 123// 筛选出由两个或两个以上的人参与创建(“created”)的顶点// 注意:这里筛选的是顶点g.V().where(__.in('created').count().is(gt(2))).values('name') where()Step可以接受一些过滤条件,在第10期会详细介绍。 示例5: 12// 筛选出有创建者(“created”)的年龄(“age”)在20~29之间的顶点g.V().where(__.in('created').values('age').is(between(20, 29))).values('name') Step and(),逻辑与 示例1: 12// 所有包含出边“supports”的顶点的名字“name”g.V().and(outE('supports')).values('name') 示例2: 12// 所有包含出边“supports”和“implements”的顶点的名字“name”g.V().and(outE('supports'), outE('implements')).values('name') 示例3: 12// 包含边“created”并且属性“age”为28的顶点的名字“name”g.V().and(outE('created'), values('age').is(28)).values('name') 示例4:“示例3”的中缀符写法 12345// 包含边“created”并且属性“age”为28的顶点的名字“name”g.V().where(outE('created') .and() .values('age').is(28)) .values('name') Step or(),逻辑或 示例1: 12// 所有包含出边“supports”的顶点的名字“name”g.V().or(outE('supports')).values('name') 只有一个条件时,and()与or()的效果一样的。 示例2: 12// 所有包含出边“supports”或“implements”的顶点的名字“name”g.V().or(outE('supports'), outE('implements')).values('name') 注意对比与g.V().and(outE('supports'), outE('implements')).values('name')的差别 示例3: 12// 包含边“created”或属性“age”为28的顶点的名字“name”g.V().or(outE('created'), values('age').is(28)).values('name') 注意对比与g.V().and(outE('created'), values('age').is(28)).values('name')的差别 示例4:“示例3”的中缀符写法 12345// 包含边“created”或属性“age”为28的顶点的名字“name”g.V().where(outE('created') .or() .values('age').is(28)) .values('name') Step not(),逻辑非 示例1: 12// 筛选出所有不是“person”的顶点的“label”g.V().not(hasLabel('person')).label() 示例2: 12// 筛选出所有包含不少于两条(大于等于两条)“created”边的“person”的名字“name”g.V().hasLabel('person').not(out('created').count().is(lt(2))).values('name') 综合运用目标:获取所有最多只有一条“created”边并且年龄不等于28的“person”顶点 写法1: 12345// 与(含有小于等于一条“created”边,年龄不等于28)g.V().hasLabel('person') .and(outE('created').count().is(lte(1)), values("age").is(P.not(P.eq(28)))) .values('name') 写法2: 12345// 非(或(含有多于一条“created”边,年龄等于28))g.V().hasLabel('person') .not(or(out('created').count().is(gt(1)), values('age').is(28))) .values('name') 下一期:深入学习Gremlin(11):待添加]]></content>
<categories>
<category>hugegraph</category>
</categories>
<tags>
<tag>gremlin</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入学习Gremlin(7):查询结果排序]]></title>
<url>%2F2018%2F09%2F21%2Fhugegraph%2F%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Gremlin%EF%BC%887%EF%BC%89%EF%BC%9A%E6%9F%A5%E8%AF%A2%E7%BB%93%E6%9E%9C%E6%8E%92%E5%BA%8F%2F</url>
<content type="text"><![CDATA[第7期 Gremlin Steps: order()、by() 本系列文章的Gremlin示例均在HugeGraph图数据库上执行,环境搭建可参考准备Gremlin执行环境,本文示例均以其中的“TinkerPop关系图”为初始数据。 上一期:深入学习Gremlin(6):循环操作 排序说明Gremlin允许对查询的结果进行排序输出,可以指定按某个属性的升序、降序或是随机序的方式输出。排序方式可以通过单独的order()或者order().by(...)指定,而by() step又有一些变种,下面分别讲解order()和order().by(...)的用法。 1.单独使用order() Step,一般用于遍历器中的元素是属性时: order()会将结果以升序输出; order()单独使用时,必须保证遍历器(traverser)中的元素是可排序的,在 java 里就是必须实现java.lang.Comparable接口,否则会抛出异常。 2.联合使用order().by(...) Step,传入排序方式,一般用于遍历器中的元素是属性时: order().by(incr): 将结果以升序输出,这也是默认的排序方式; order().by(decr): 将结果以降序输出; order().by(shuffle): 将结果以随机序输出,每次执行结果顺序都可能不一样。 使用 order().by(…) step 但是 by() 传递的仅是一个排序方式的参数时,也必须保证遍历器(traverser)中的元素是可排序的。 3.联合使用order().by(...) Step,传入属性和排序方式,用于遍历器中的元素是顶点或边时: order().by(key): 将结果按照元素属性key的值升序排列,与order().by(key, incr)等效; order().by(key, incr): 将结果按照元素属性key的值升序排列; order().by(key, decr): 将结果按照元素属性key的值降序排列; order().by(key, shuffle): 将结果按照元素属性key的值随机序排列,每次执行结果顺序都可能不一样。 by()step不是一个真正的step,而是一个“step modulator”,与此类似的还有as()和option()step。通过by()step可以为某些step添加traversal、function、comparator等,通常的使用方式是step().by()…by(),某些step只能添加一个by(),而有一些可以添加任意数量的by()step。 实例讲解 order(),使用默认的排序(升序)输出 12// 以默认排序输出所有顶点的"name"属性值g.V().values('name').order() order().by(incr),指定以升序输出 12// 以升序输出所有顶点的"name"属性值g.V().values('name').order().by(incr) order().by(decr),指定以降序输出 12// 以降序输出所有顶点的"name"属性值g.V().values('name').order().by(decr) order().by(shuffle),指定以随机序输出 12// 以随机序输出所有顶点的"name"属性值g.V().values('name').order().by(shuffle) order().by(key),按照元素属性key的值升序(默认)排列 12// 将"person"类型的顶点按照"age"升序(默认)排列输出g.V().hasLabel('person').order().by('age') 为了使”age”属性排列显示得更清晰,我们取出顶点的”age”属性 12// 将"person"类型的顶点按照"age"升序(默认)排列,并获取"age"属性g.V().hasLabel('person').order().by('age').values('age') order().by(key, incr),按照元素属性key的值升序排列 12// 将"person"类型的顶点按照"age"升序排列,并获取"age"属性g.V().hasLabel('person').order().by('age', incr).values('age') order().by(key, desc),按照元素属性key的值降序排列 12// 将"person"类型的顶点按照"age"降序排列输出,并获取"age"属性g.V().hasLabel('person').order().by('age', decr).values('age') order().by(key, shuffle),按照元素属性key的值随机序排列 12// 将"person"类型的顶点按照"age"随机序排列输出,并获取"age"属性g.V().hasLabel('person').order().by('age', shuffle).values('age') 下一期:深入学习Gremlin(8):数据分组与去重]]></content>
<categories>
<category>hugegraph</category>
</categories>
<tags>
<tag>gremlin</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入学习Gremlin(4):图查询返回结果数限制]]></title>
<url>%2F2018%2F09%2F14%2Fhugegraph%2F%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Gremlin%EF%BC%884%EF%BC%89%EF%BC%9A%E5%9B%BE%E6%9F%A5%E8%AF%A2%E8%BF%94%E5%9B%9E%E7%BB%93%E6%9E%9C%E6%95%B0%E9%99%90%E5%88%B6%2F</url>
<content type="text"><![CDATA[第4期 Gremlin Steps: count()、range()、limit()、tail()、skip() 本系列文章的Gremlin示例均在HugeGraph图数据库上执行,环境搭建可参考准备Gremlin执行环境,本文示例均以其中的“TinkerPop关系图”为初始数据。 上一期:深入学习Gremlin(3):has条件过滤 图查询返回结果数限制说明 Gremlin能统计查询结果集中元素的个数,且允许从结果集中做范围截取。假设某个查询操作(如:g.V())的结果集包含8个元素,我们可以从这8个元素中截取指定部分。主要包括: count(): 统计查询结果集中元素的个数; range(m, n): 指定下界和上界的截取,左闭右开。比如range(2, 5)能获取第2个到第4个元素(0作为首个元素,上界为-1时表示剩余全部); limit(n): 下界固定为0,指定上界的截取,等效于range(0, n),语义是“获取前n个元素”。比如limit(3)能获取前3个元素; tail(n): 上界固定为-1,指定下界的截取,等效于range(count - n, -1),语义是“获取后n个元素”。比如tail(2)能获取最后的2个元素; skip(n): 上界固定为-1,指定下界的截取,等效于range(n, -1),语义是“跳过前n个元素,获取剩余的元素”。比如skip(6)能跳过前6个元素,获取最后2个元素。 实例讲解下面通过实例来深入理解每一个操作。 Step count():查询当前traverser中的元素的个数,元素可以是顶点、边、属性、路径等。 示例1:查询图中所有顶点的个数 1g.V().count() TinkerPop关系图的圆点就是顶点,总共12个。 示例2:查询图中类型为“人person”的顶点数 1g.V().hasLabel('person').count() TinkerPop关系图的绿点就是类型为“人person”的顶点,共7个。 示例3:查询图中所有的 “人创建created” 的边数 1g.V().hasLabel('person').outE('created').count() TinkerPop关系图的所有从绿点(person)出发并且连线上为“created”的边,共8个。 示例4:查询图中所有顶点的属性数 1g.V().properties().count() TinkerPop关系图的所有顶点的属性共47个,其中“人person”共有28个,“软件software”共有19个,大家可以自己数一数。 Step range():限定查询返回的元素的范围,上下界表示元素的偏移量,左闭右开。下界以“0”作为第一个元素,上界为“-1”时表示取到最后的元素。 示例1:不加限制地查询所有类型为“人person”的顶点 1g.V().hasLabel('person').range(0, -1) 示例2:查询类型为“人person”的顶点中的第2个到第5个 1g.V().hasLabel('person').range(2, 5) 示例3:查询类型为“人person”的顶点中的第5个到最后一个 1g.V().hasLabel('person').range(5, -1) Step limit():查询前“n”个元素,相当于range(0, n) 示例1:查询前两个顶点 1g.V().limit(2) 示例2:查询前三条边 1g.E().limit(3) Step tail():与limit()相反,它查询的是后“n”个元素,相当于range(count - n, -1) 示例1:查询后两个顶点 1g.V().tail(2) 示例2:查询后三条边 1g.E().tail(3) Step skip():跳过前“n”个元素,获取剩余的全部元素 skip()在HugeGraph中(tinkerpop 3.2.5)中尚不支持,下面给出代码以及预期的结果。 12// 跳过前5个,skip(5)等价于range(5, -1)g.V().hasLabel('person').skip(5) 下一期:深入学习Gremlin(5):查询路径path]]></content>
<categories>
<category>hugegraph</category>
</categories>
<tags>
<tag>gremlin</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入学习Gremlin(0):准备执行Gremlin的图形化环境]]></title>
<url>%2F2018%2F09%2F07%2Fhugegraph%2F%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Gremlin%EF%BC%880%EF%BC%89%EF%BC%9A%E5%87%86%E5%A4%87%E6%89%A7%E8%A1%8CGremlin%E7%9A%84%E5%9B%BE%E5%BD%A2%E5%8C%96%E7%8E%AF%E5%A2%83%2F</url>
<content type="text"><![CDATA[背景Gremlin是Apache TinkerPop框架下实现的图遍历语言,支持OLTP与OLAP,是目前图数据库领域主流的查询语言,可类比SQL语言之于关系型数据库。 HugeGraph是国内的一款开源图数据库,完全支持Gremlin语言。本文将讲述如何基于HugeGraph搭建一个执行Gremlin的图形化环境。 HugeGraph的github仓库下有很多子项目,我们这里只需要使用其中的两个:hugegraph和hugegraph-studio。 部署HugeGraphServer准备安装包方式一:源码编译打包进入hugegraph项目,克隆代码库 进入终端1$ git clone git@github.com:hugegraph/hugegraph.git 完成后会在当前目录下多出来一个hugegraph的子目录,不过这个目录里面的文件是源代码,我们需要编译打包才能生成可以运行包。 进入hugegraph目录,执行命令:12$ git checkout release-0.7$ mvn package -DskipTests 注意:一定要先切换分支,hugegraph主分支上版本已经升级到0.8.0了,但是studio似乎还没有升级,为避免踩坑我们还是使用已发布版。 经过一长串的控制台输出后,最后如果能看到BUILD SUCCESS表示打包成功。这时会在当前目录下多出来一个子目录hugegraph-0.7.4和一个压缩包hugegraph-0.7.4.tar.gz,这就是我们即将要使用可以运行的包。 本人有轻微强迫症,不喜欢源代码和二进制包放在一起,容易混淆,所以把hugegraph-0.7.4拷到上一层目录,然后删除源代码目录,这样上层目录又回归清爽了。123$ mv hugegraph-0.7.4 ../hugegraph-0.7.4$ cd ..$ rm -rf hugegraph 到这儿安装包就准备好了。不过,这样操作是需要你本地装了jdk、git和maven命令行工具的,如果你没有安装也没关系,我们还可以直接下载hugegraph官方的release包。 方法二:直接下载release包点击github代码的上面的导航releases 可以看到hugegraph目前有两个release,点击hugegraph-0.7.4.tar.gz就开始下载了。 下载完之后解压即可1$ tar -zxvf hugegraph-0.7.4.tar.gz 解压完之后能看到一个hugegraph-0.7.4目录,这个目录和用源码包打包生成的是一样的。 下面讲解如何配置参数。 配置参数虽然标题叫配置参数,但其实hugegraph的默认配置就已经能在大部分环境下直接使用了,不过还是说明一下几个重要的配置项。 进入hugegraph-0.7.4目录,修改HugeGraphServer提供服务的url (host + port)1234567891011121314$ vim conf/rest-server.properties# bind urlrestserver.url=http://127.0.0.1:8080# gremlin url to connectgremlinserver.url=http://127.0.0.1:8182# graphs list with pair NAME:CONF_PATHgraphs=[hugegraph:conf/hugegraph.properties]# authentication#auth.require_authentication=#auth.admin_token=#auth.user_tokens=[] restserver.url就是HugeGraphServer对外提供RESTful API服务的地址,host为127.0.0.1时只能在本机访问的,按需要修改其中的host和port部分即可。我这里由于studio也是准备在本地启动,8080端口也没有其他服务占用,所以不修改它。 graphs是可供连接的图名与配置项的键值对列表,hugegraph:conf/hugegraph.properties表示通过HugeGraphServer可以访问到一个名为hugegraph的图实例,该图的配置文件路径为conf/hugegraph.properties。我们可以不用去管图的配置文件,按需要修改图的名字即可。我这里仍然没有修改它。 初始化后端hugegraph启动服务之前是需要手动初始化后端的,不过大家也不要看到“手动”两个字就害怕,其实就是调一个命令的事。 1234567891011121314$ bin/init-store.shIniting HugeGraph Store...2018-09-07 16:02:12 1082 [main] [INFO ] com.baidu.hugegraph.cmd.InitStore [] - Init graph with config file: conf/hugegraph.properties2018-09-07 16:02:12 1201 [main] [INFO ] com.baidu.hugegraph.HugeGraph [] - Opening backend store 'rocksdb' for graph 'hugegraph'2018-09-07 16:02:12 1258 [main] [INFO ] com.baidu.hugegraph.backend.store.rocksdb.RocksDBStore [] - Opening RocksDB with data path: rocksdb-data/schema2018-09-07 16:02:12 1417 [main] [INFO ] com.baidu.hugegraph.backend.store.rocksdb.RocksDBStore [] - Failed to open RocksDB 'rocksdb-data/schema' with database 'hugegraph', try to init CF later2018-09-07 16:02:12 1445 [main] [INFO ] com.baidu.hugegraph.backend.store.rocksdb.RocksDBStore [] - Opening RocksDB with data path: rocksdb-data/system2018-09-07 16:02:12 1450 [main] [INFO ] com.baidu.hugegraph.backend.store.rocksdb.RocksDBStore [] - Failed to open RocksDB 'rocksdb-data/system' with database 'hugegraph', try to init CF later2018-09-07 16:02:12 1456 [main] [INFO ] com.baidu.hugegraph.backend.store.rocksdb.RocksDBStore [] - Opening RocksDB with data path: rocksdb-data/graph2018-09-07 16:02:12 1461 [main] [INFO ] com.baidu.hugegraph.backend.store.rocksdb.RocksDBStore [] - Failed to open RocksDB 'rocksdb-data/graph' with database 'hugegraph', try to init CF later2018-09-07 16:02:12 1491 [main] [INFO ] com.baidu.hugegraph.backend.store.rocksdb.RocksDBStore [] - Store initialized: schema2018-09-07 16:02:12 1511 [main] [INFO ] com.baidu.hugegraph.backend.store.rocksdb.RocksDBStore [] - Store initialized: system2018-09-07 16:02:12 1543 [main] [INFO ] com.baidu.hugegraph.backend.store.rocksdb.RocksDBStore [] - Store initialized: graph2018-09-07 16:02:13 1804 [pool-3-thread-1] [INFO ] com.baidu.hugegraph.backend.Transaction [] - Clear cache on event 'store.init' 这里可以看到,hugegraph初始化了rocksdb后端,那为什么是rocksdb而不是别的呢,其实就是上一步说的conf/hugegraph.properties中配置的。 1234567891011121314151617181920212223$ vim conf/hugegraph.properties# gremlin entrence to create graphgremlin.graph=com.baidu.hugegraph.HugeFactory# cache config#schema.cache_capacity=1048576#graph.cache_capacity=10485760#graph.cache_expire=600# schema illegal name template#schema.illegal_name_regex=\s+|~.*#vertex.default_label=vertexbackend=rocksdbserializer=binarystore=hugegraph# rocksdb backend config#rocksdb.data_path=/path/to/disk#rocksdb.wal_path=/path/to/disk... 其中backend=rocksdb就是设置后端为rocksdb的配置项。 其他的后端还包括:memory、cassandra、scylladb、hbase、mysql和palo。我们这里不用去管它,用默认的rocksdb即可。 初始化完成之后,会在当前目录下出现一个rocksdb-data的目录,这就是存放后端数据的地方,没事千万不要随意删它或移动它。 注意:初始化后端这个操作只需要在第一次启动服务前执行一次,不要每次起服务都执行。不过即使执行了也没关系,hugegraph检测到已经初始化过了会跳过。 启动服务终于到了启动服务了,同样也是一条命令 123$ bin/start-hugegraph.shStarting HugeGraphServer...Connecting to HugeGraphServer (http://127.0.0.1:8080/graphs)....OK 看到上面的OK就表示启动成功了,我们可以jps看一下进程。 12345$ jps...4101 HugeGraphServer4233 Jps... 如果还不放心,我们可以发个HTTP请求试试看。 12$ curl http://127.0.0.1:8080/graphs{"graphs":["hugegraph"]} 到这里HugeGraphServer的部署就完成了,接下来我们来部署HugeGraphStudio。 部署HugeGraphStudio步骤与部署HugeGraphServer大体类似,我们就不那么啰嗦了。 记得先返回最上层目录,避免目录嵌套在一起了。 准备安装包克隆代码库 12345678$ git clone git@github.com:hugegraph/hugegraph-studio.gitCloning into 'hugegraph-studio'...mux_client_request_session: read from master failed: Broken piperemote: Counting objects: 326, done.remote: Compressing objects: 100% (189/189), done.remote: Total 326 (delta 115), reused 324 (delta 113), pack-reused 0Receiving objects: 100% (326/326), 1.60 MiB | 350.00 KiB/s, done.Resolving deltas: 100% (115/115), done. 编译打包 studio是一个包含前端的项目,使用react.js实现,自行打包的话需要安装npm、webpack等工具。 12$ cd hugegraph-studio$ mvn package -DskipTests studio打包的时间会稍长一点。 12345678910111213...[INFO] Reactor Summary:[INFO][INFO] hugegraph-studio ................................... SUCCESS [ 0.003 s][INFO] studio-api ......................................... SUCCESS [ 4.683 s][INFO] studio-dist ........................................ SUCCESS [01:42 min][INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time: 01:47 min[INFO] Finished at: 2018-09-07T16:32:44+08:00[INFO] Final Memory: 34M/390M[INFO] ------------------------------------------------------------------------ 将打包好的目录拷到上一层,删除源码目录(纯个人喜好)。 123$ mv hugegraph-studio-0.7.0 ../$ cd ..$ rm -rf hugegraph-studio 至此,我的最上层目录就只剩下两个安装包,如下: 12$ lshugegraph-0.7.4 hugegraph-studio-0.7.0 配置参数进入hugegraph-studio-0.7.0目录,修改唯一的一个配置文件。 12345678910111213141516171819202122$ cd hugegraph-studio-0.7.0$ vim conf/hugegraph-studio.propertiesstudio.server.port=8088studio.server.host=localhostgraph.server.host=localhostgraph.server.port=8080graph.name=hugegraph# the directory name released by reactstudio.server.ui=ui# the file location of studio-api.warstudio.server.api.war=war/studio-api.war# default folder in your home directory, set to a non-empty value to overridedata.base_directory=~/.hugegraph-studioshow.limit.data=250show.limit.edge.total=1000show.limit.edge.increment=20# separator ','gremlin.limit_suffix=[.V(),.E(),.hasLabel(STR),.hasLabel(NUM),.path()] 需要修改的参数是graph.server.host=localhost、graph.server.port=8080、graph.name=hugegraph。它们与HugeGraphServer的配置文件conf/rest-server.properties中的配置项对应,其中: graph.server.host=localhost与restserver.url=http://127.0.0.1:8080的host对应; graph.server.port=8080与的restserver.url=http://127.0.0.1:8080的port对应; graph.name=hugegraph与graphs=[hugegraph:conf/hugegraph.properties]的图名对应。 因为我之前并没有修改HugeGraphServer的配置文件conf/rest-server.properties,所以这里也不需要修改HugeGraphStudio的配置文件conf/hugegraph-studio.properties。 启动服务1$ bin/hugegraph-studio.sh studio的启动默认是不会放到后台的,所以我们会在控制台上看到一大串日志,在最底下看到如下日志表示启动成功: 12信息: Starting ProtocolHandler [http-nio-127.0.0.1-8088]16:56:24.507 [main] INFO com.baidu.hugegraph.studio.HugeGraphStudio ID: TS: - HugeGraphStudio is now running on: http://localhost:8088 然后我们按照提示,在浏览器中输入http://localhost:8088,就进入了studio的界面: 图中Gremlin下的框,就是我们输入gremlin语句进而操作hugegraph的入口了,下面我们给出一个例子。 创建关系图以下内容参考CSDN博客通过Gremlin语言构建关系图并进行图分析。 在输入框中输入以下代码以创建一个“TinkerPop关系图”: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869// PropertyKeygraph.schema().propertyKey("name").asText().ifNotExist().create()graph.schema().propertyKey("age").asInt().ifNotExist().create()graph.schema().propertyKey("addr").asText().ifNotExist().create()graph.schema().propertyKey("lang").asText().ifNotExist().create()graph.schema().propertyKey("tag").asText().ifNotExist().create()graph.schema().propertyKey("weight").asFloat().ifNotExist().create()// VertexLabelgraph.schema().vertexLabel("person").properties("name", "age", "addr", "weight").useCustomizeStringId().ifNotExist().create()graph.schema().vertexLabel("software").properties("name", "lang", "tag", "weight").primaryKeys("name").ifNotExist().create()graph.schema().vertexLabel("language").properties("name", "lang", "weight").primaryKeys("name").ifNotExist().create()// EdgeLabelgraph.schema().edgeLabel("knows").sourceLabel("person").targetLabel("person").properties("weight").ifNotExist().create()graph.schema().edgeLabel("created").sourceLabel("person").targetLabel("software").properties("weight").ifNotExist().create()graph.schema().edgeLabel("contains").sourceLabel("software").targetLabel("software").properties("weight").ifNotExist().create()graph.schema().edgeLabel("define").sourceLabel("software").targetLabel("language").properties("weight").ifNotExist().create()graph.schema().edgeLabel("implements").sourceLabel("software").targetLabel("software").properties("weight").ifNotExist().create()graph.schema().edgeLabel("supports").sourceLabel("software").targetLabel("language").properties("weight").ifNotExist().create()// TinkerPopokram = graph.addVertex(T.label, "person", T.id, "okram", "name", "Marko A. Rodriguez", "age", 29, "addr", "Santa Fe, New Mexico", "weight", 1)spmallette = graph.addVertex(T.label, "person", T.id, "spmallette", "name", "Stephen Mallette", "age", 0, "addr", "", "weight", 1)tinkerpop = graph.addVertex(T.label, "software", "name", "TinkerPop", "lang", "java", "tag", "Graph computing framework", "weight", 1)tinkergraph = graph.addVertex(T.label, "software", "name", "TinkerGraph", "lang", "java", "tag", "In-memory property graph", "weight", 1)gremlin = graph.addVertex(T.label, "language", "name", "Gremlin", "lang", "groovy/python/javascript", "weight", 1)okram.addEdge("created", tinkerpop, "weight", 1)spmallette.addEdge("created", tinkerpop, "weight", 1)okram.addEdge("knows", spmallette, "weight", 1)tinkerpop.addEdge("define", gremlin, "weight", 1)tinkerpop.addEdge("contains", tinkergraph, "weight", 1)tinkergraph.addEdge("supports", gremlin, "weight", 1)// Titandalaro = graph.addVertex(T.label, "person", T.id, "dalaro", "name", "Dan LaRocque ", "age", 0, "addr", "", "weight", 1)mbroecheler = graph.addVertex(T.label, "person", T.id, "mbroecheler", "name", "Matthias Broecheler", "age", 29, "addr", "San Francisco", "weight", 1)titan = graph.addVertex(T.label, "software", "name", "Titan", "lang", "java", "tag", "Graph Database", "weight", 1)dalaro.addEdge("created", titan, "weight", 1)mbroecheler.addEdge("created", titan, "weight", 1)okram.addEdge("created", titan, "weight", 1)dalaro.addEdge("knows", mbroecheler, "weight", 1)titan.addEdge("implements", tinkerpop, "weight", 1)titan.addEdge("supports", gremlin, "weight", 1)// HugeGraphjaveme = graph.addVertex(T.label, "person", T.id, "javeme", "name", "Jermy Li", "age", 29, "addr", "Beijing", "weight", 1)zhoney = graph.addVertex(T.label, "person", T.id, "zhoney", "name", "Zhoney Zhang", "age", 29, "addr", "Beijing", "weight", 1)linary = graph.addVertex(T.label, "person", T.id, "linary", "name", "Linary Li", "age", 28, "addr", "Wuhan. Hubei", "weight", 1)hugegraph = graph.addVertex(T.label, "software", "name", "HugeGraph", "lang", "java", "tag", "Graph Database", "weight", 1)javeme.addEdge("created", hugegraph, "weight", 1)zhoney.addEdge("created", hugegraph, "weight", 1)linary.addEdge("created", hugegraph, "weight", 1)javeme.addEdge("knows", zhoney, "weight", 1)javeme.addEdge("knows", linary, "weight", 1)hugegraph.addEdge("implements", tinkerpop, "weight", 1)hugegraph.addEdge("supports", gremlin, "weight", 1) 点击右上角的三角按钮,这样就创建出了一个图。 图查询在输入框中输入: 1g.V() 就能查出上面创建的图的所有顶点和边。 至此,执行Gremlin的图形化环境就已经搭建完成,后续就可以做各种各样炫酷的gremlin查询了。]]></content>
<categories>
<category>hugegraph</category>
</categories>
<tags>
<tag>gremlin</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Netty源码剖析]]></title>
<url>%2F2018%2F09%2F06%2Fstudy-netty%2Fnetty%2F</url>
<content type="text"><![CDATA[Thread -> NioEventLoop, Netty的发送机,主要包含两种线程,一种处理连接,一种发送接收数据。 while(true) -> run() Socket -> Channel IOBytes -> ByteBuf // 就是一个供读写的数据链Logic Chain -> Pipeline Logic -> ChannelHandler Question: 1、socket在哪里创建 Netty服务端启动1、创建服务端Channel bind -> initAndRegister -> newChannel channelFactory.newChannel()通过反射创建Channel 1、newSocket通过JDK创建底层channel 2、NioServerScoektSocketChannelCOnfig(TCP参数配置) 3、AbstractNioChannel.configureBlocking(false) channelFactory 的clazz是在bootStrap的.channel(Class)传入的 2、初始化服务端Channel init 1、init() 2、setchannelOptions, ChannelAttrs 3、setChildOptions、ChildAttrs 4、config Handler 5、add ServerBootstrapAcceptor(一个特殊的Handler) 3、注册Selector 1、AbstractChannel.register(channel) channelAddedchannelRegistered 4、端口绑定 1、AbstractUnsafe.bind() 服务端启动核心路径总结 newChannel() -> init() -> register() -> doBind() NioEventLoop三个问题: 默认Netty服务端起多少个线程?何时启动 Netty是如何解决jdk空轮询bug的 Netty如何保 NioEventLoop的创建new NioEventLoopGroup() NioEventLoop的启动NioEventLoop的执行逻辑]]></content>
<categories>
<category>网络通信</category>
</categories>
<tags>
<tag>Netty,NIO</tag>
</tags>
</entry>
<entry>
<title><![CDATA[并发与高并发]]></title>
<url>%2F2018%2F08%2F30%2Fjava%2Fconcurrent%2F</url>
<content type="text"><![CDATA[并发与高并发的关注点 并发:多个线程操作相同的资源,保证程序线程安全,合理使用资源 高并发:服务能同时处理很多请求,提高程序性能 基础知识CPU多级缓存 为什么需要CPU缓存: 缓存存在的意义:时间一致性,空间一致性 缓存一致性(MESI):用于保证多个CPU缓存之间缓存共享数据的一致性(没听懂) 乱序执行优化:处理器为提高运算速度而做出违背代码原有顺序的优化 J.U.C 之 AQS 组件CountDownLatch计数器,能阻塞(await)某些线程直到 CountDownLatch 的值变为 0,其他线程负责将 CountDownLatch 减 1(countDown)。 计数器只能使用一次。 Semaphore信号量,能控制一定数量线程的并发执行。 在执行业务代码前,调用acquire()获取一个许可,执行完之后,调用release()释放一个许可。 还允许尝试获取许可,尝试获取多个许可以及它们的超时版。 CyclicBarrier允许多个线程互相等待,到达全部准备好的状态。就像赛跑时所有人都要在起跑线上准备好,然后再一起开跑。 可以重置。 synchronized可重入锁,一个线程进去过一次后还可以再进去一次。 其实 synchronized 也是可重入锁。 自从 synchronized 引入了偏向锁,自旋锁之后,性能与 synchronized 差不多了。 StampedLockStampedLock 是 Java8 引入的一种新的所机制,简单的理解,可以认为它是读写锁的一个改进版本。读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发,但是读和写之间依然是冲突的,读锁会完全阻塞写锁,它使用的依然是悲观的锁策略。如果有大量的读线程,他也有可能引起写线程的饥饿。而StampedLock 则提供了一种乐观的读策略,这种乐观策略的锁非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程。 Fork/Join 框架并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。并行流的底层其实就是ForkJoin框架的一个实现。 Fork/Join框架:在必要的情况下,将一个大任务,进行拆分(fork) 成若干个子任务(拆到不能再拆,这里就是指我们制定的拆分的临界值),再将一个个小任务的结果进行join汇总。 Fork/Join采用“工作窃取模式”,当执行新的任务时他可以将其拆分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随即线程中偷一个并把它加入自己的队列中。 就比如两个CPU上有不同的任务,这时候A已经执行完,B还有任务等待执行,这时候A就会将B队尾的任务偷过来,加入自己的队列中,对于传统的线程,ForkJoin更有效的利用的CPU资源! BlockingQueueBlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。一个线程往里边放,另外一个线程从里边取。一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。 直接提交队列:SynchronousQueue,没有容量,所以提交的任务不能保存,总是将任务交给空闲线程,如果没有空闲线程,就创建线程,一旦达到maximumPoolSize就执行拒绝策略 有界任务队列:ArrayBlockingQueue,当线程池的数量小于corePoolSize时,当有新的任务时,创建线程,达到corePoolSize后,则将任务存到ArrayBlockingQueue中,直到有界队列容量已满时,才可能会将线程数提升到corePoolSize之上。 无界队列:LinkedBlockingQueue,除非系统资源耗尽,否则不存在任务队列入队失败的情况,因此当线程数达到corePoolSize之后,就不会增加,有新的任务到来时,都会放到无界队列中。 优先任务队列:PriorityBlockingQueue是带有优先级的队列,特殊的无界队列,理论上来说不是先入先出的,是根据任务的优先级来确定执行顺序 DelayQueue:执行定时任务,将任务按延迟时间长短放入队列中,延迟时间最短的最先被执行,存放在队列头部的是延迟期满后保存时间最长的任务 LinkedTransferQueue:其实和SynchronousQueue类似,当生产者生产出产品后,当先去找是否有消费者,如果有消费者在等待资源,则直接调用transfer()方法将资源给消费者消费,而不会放入队列中。如果没有消费者等待,则当生产者调用transfer()方法时会阻塞,而调用其他的方法,如aput()则不会阻塞,会把资源放到队列中,因为put()方法只有在队列满的时候才会阻塞。适用于游戏服务器中,可以是并发时消息传递的效率更高 多线程并发最佳实践 使用本地变量 使用不可变类 最小化锁的作用域范围:S=1/(1-a + a/n) 使用线程池的 Executor,而不是直接 new Thread 执行 宁可使用同步也不要使用线程 wait 和 notify 使用 BlockingQueue 实现生产消费模式 使用并发集合而不是加了锁的同步集合 使用 Semaphore 创建有界的访问 宁可使用同步代码块,也不使用同步的方法 避免使用静态变量 高并发处理的思路及手段 扩容:水平扩容、垂直扩容 缓存:Redis、Memcache、Guava Cache等的介绍与使用 队列:kafka、RabbitMQ、RocketMQ等 应用拆分:服务化Dubbo与微服务Spring Cloud 限流:Guava RateLimiter,常用限流算法 服务降级与服务熔断:Hystrix 数据库切库、分库、分表 高可用:任务调度分布式elastic-job、主备curator的实现、监控报警机制 缓存浏览器 -> 网络转发 -> 服务 -> 数据库 其实缓存可以出现在上述的各个环节中。 特征 命中率:命中数 /(命中数 + 未命中数) 最大元素(空间) 清空策略:FIFO、LFU、LRU、过期时间、随机等 缓存命中率影响因素 业务场景和业务需求 缓存的设计(粒度和策略) 缓存容量和基础设施 缓存分类和应用场景 本地缓存:编程实现(成员变量、局部变量、静态变量)、Guava Cache 分布式缓存:MemCache、Redis Guava Cache灵感来源于ConcurrentHashMap。 MemCache客户端:采用一致性哈希算法,把某个key的操作映射到固定机器上。 Redis远程内存数据库,支持数据持久化。]]></content>
<categories>
<category>并发</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于JDK命令行工具的监控]]></title>
<url>%2F2018%2F08%2F22%2Fjava%2FJVM-0%2F</url>
<content type="text"><![CDATA[JVM 参数类型标准参数X参数 非标准化参数 -Xint:解释执行 -Xcomp:第一使用就编译成本地代码 -Xmixed:混合模式,JVM自己来决定是否编译成本地代码 XX参数 Boolean类型 格式:-XX:[+-]表示启动或禁用name属性 非Boolean类型 格式:-XX:=表示name属性的值是value 需要注意的是:-Xms和-Xmx虽然是以X打头,但是实际上是XX参数 -Xms等价于-XX:InitialHeapSize -Xmx等价于-XX:MaxHeapSize 查看JVM运行时的参数 -XX:+PrintFlagsInitial -XX:+PrintFlagsFinal -XX:+UnlockExpire =表示默认值,:=表示用户修改过后的值 1jinfo -flag MaxHeapSize {pid} jstat查看JVM统计信息类加载 -class 垃圾收集 -gc JIT编译 -compiler JVM的内存结构堆区: Young: S0 + S1 + Eden Old 非堆区(Metaspace): CCS CodeCache jamp + MAT 实战内存溢出Java里面的内存溢出是说创建的对象一直不释放 堆内存溢出 构造一个List不停地添加普通对象 -Xmx32M, -Xms32M 非堆内存溢出 构造一个List不停地添加Class对象 -XX:MetaspaceSize=32M, -XX:MaxMetaspaceSize=32M 如何导出内存映像文件 内存溢出自动导出: 使用jmap命令手动导出:jamp -demp:format=b.file=heap.hprof MAT分析内存溢出官网下载,将上一步的文件导入。 jstack实战死循环与死锁监控远程普通Java进程远程启动 Java 进程时加上如下的参数,然后通过 JvisualM 就可以通过JMX连接进行监控了。 nohup java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port={port}-Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false-Djava.net.preferIPv4Stack=true -Djava.rmi.server.hostname={ip} -jar {jar_name}.jar & Btrace(可以考虑基于Btrace实现一个监控工具)Btrace 本质上就是一个拦截器,可以给 Java 进程动态添加拦截,它不需要修改原有的 Java 代码,通过一个独立进程提供监控。 默认只能在本地运行 生产环境下可以使用,但是被修改的字节码不会被还原 1btrace pid {Btrace脚本} 拦截方法 拦截时机 拦截this、参数、返回值 其他 拦截方法]]></content>
<categories>
<category>JVM</category>
</categories>
</entry>
<entry>
<title><![CDATA[HBase学习]]></title>
<url>%2F2018%2F08%2F20%2Fhbase%2Fstudy-hbase%2F</url>
<content type="text"><![CDATA[HBase中为什么要有Column FamilyHBase本身的设计目标是支持稀疏表,而稀疏表通常会有很多列,但是每一行有值的列又比较少。如果不使用Column Family的概念,那么有两种设计方案: 1.把所有列的数据放在一个文件中(也就是传统的按行存储)。那么当我们想要访问少数几个列的数据时,需要遍历每一行,读取整个表的数据,这样子是很低效的。 2.把每个列的数据单独分开存在一个文件中(按列存储)。那么当我们想要访问少数几个列的数据时,只需要读取对应的文件,不用读取整个表的数据,读取效率很高。然而,由于稀疏表通常会有很多列,这会导致文件数量特别多,这本身会影响文件系统的效率。 而Column Family的提出就是为了在上面两种方案中做一个折中。HBase中将一个Column Family中的列存在一起,而不同Column Family的数据则分开。由于在HBase中Column Family的数量通常很小,同时HBase建议把经常一起访问的比较类似的列放在同一个Column Family中,这样就可以在访问少数几个列时,只读取尽量少的数据。]]></content>
<categories>
<category>NoSQL</category>
</categories>
<tags>
<tag>HBase</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Vue入门]]></title>
<url>%2F2018%2F08%2F19%2Ffront-end%2Fstudy-vue%2F</url>
<content type="text"><![CDATA[Vue 生命周期 动画 可以使用animate.css这个库添加很多炫酷的动画; 使用velocity.js库也可以添加动画(依靠js的钩子实现的动画); 其他注意要点 子组件的data必须是一个函数 子组件不应该修改父组件传递进来的值]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>Vue.js</tag>
</tags>
</entry>
<entry>
<title><![CDATA[内存数据库事务]]></title>
<url>%2F2018%2F08%2F06%2Fmemory-db%2F</url>
<content type="text"><![CDATA[前言 我们在使用数据库的时候,应该都体验过事务,我们对事务最直观的感受就是:一系列的操作要么全部生效,要么全部不生效,不会最后处于一种中间状态。其实这句话似乎只能体现事务的原子性,那其他几个特性呢?本文会先回顾一下事务的定义和ACID特性,再以一个具体的例子展示如何实现事务的四个特性。 事务定义所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。 特性 原子性(Atomicity):事务是一个不可再分割的工作单位,事务中的操作要么都发生,要么都不发生; 一致性(Consistency):事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。 隔离性(Isolation):多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。 持久性(Durability):事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。]]></content>
<categories>
<category>数据库</category>
</categories>
<tags>
<tag>事务</tag>
</tags>
</entry>
<entry>
<title><![CDATA[分布式存储系统]]></title>
<url>%2F2018%2F08%2F06%2Fdistributed-storage%2F</url>
<content type="text"><![CDATA[需要调研的分布式存储系统包括但不限于: 谷歌的GFS、BigTable、MegaStore和Spanner,阿里的TFS、Tair和OceanBase,Facebook的Haystack,亚马逊的Dynamo,Oracle的Mysql Sharding,PingCap 的TiDB等。]]></content>
<categories>
<category>分布式存储系统</category>
</categories>
</entry>
<entry>
<title><![CDATA[自己实现一个轻量级的任务(线程)管理器]]></title>
<url>%2F2018%2F07%2F23%2Fepoch-taskmanager%2F</url>
<content type="text"><![CDATA[功能 允许自定义任务(继承任务基类),比如实时任务,延时任务,周期任务等,实时任务和延时任务都是执行一次,周期任务会反复执行。 允许定义任务链,依次顺序执行,上游任务失败了下游任务不会执行。但是不提供下游任务失败了上游任务回滚的能力。 允许提交,暂停(非必须),继续,重做和取消任务。 允许根据多种方式查询任务,比如查询根据任务Id查询,根据条件查询等,获取任务状态,进度等信息。 允许自定义任务的回调函数(成功、取消,超时,失败的响应)。 支持任务的持久化(非必须)。 关键点1、什么时候保存任务的信息? 任务第一次提交给任务管理器后,保存。 任务成功或失败后,保存。 2、查询任务信息是每次都从后端查吗? 任务创建后即加入到内存的容器中,在任务完成前都是从内存中查询,任务成功或者失败后(保存到数据库),从内存中删除任务的信息,而后的查询是从数据库查的。 3、当TaskManager退出时,应该扫描全部的任务,将内存中所有的任务(状态应该都是未完成)的快照都保存到数据库。以便于下次继续执行任务。 类结构设计1、Task 保存任务的静态信息,包括执行体(Callable),名称,类型,描述,参数,超时时间(可不设置)。 2、TaskTracker 保存任务的动态信息,包括进度,状态,运行时间等。]]></content>
<categories>
<category>调度管理</category>
</categories>
<tags>
<tag>线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Guava Futures异步回调机制源码解析]]></title>
<url>%2F2018%2F07%2F23%2Fjava%2Fguava-future%2F</url>
<content type="text"><![CDATA[前言 最近本人在实现一个异步任务调度框架,不打算依赖于任何第三方包。在实现任务状态监听时遇到了一些困惑,于是想了解一下Guava中的ListenableFuture的实现方式。ListenableFuture实现非阻塞的方式是其提供了回调机制(机制),下面将阐述该回调机制的实现,主要对Futures的addCallback方法源码进行剖析。 Guava Futures简介Google Guava框架的 com.google.common.util.concurrent包是并发相关的包,它是对JDK自带concurrent包中Future和线程池相关类的扩展,从而衍生出一些新类,并提供了更为广泛的功能。在项目中常用的该包中类如下所示: ListenableFuture:该接口扩展了Future接口,增加了addListener方法,该方法在给定的excutor上注册一个监听器,当计算完成时会马上调用该监听器。不能够确保监听器执行的顺序,但可以在计算完成时确保马上被调用。 FutureCallback:该接口提供了OnSuccess和onFailure方法。获取异步计算的结果并回调。 MoreExecutors:该类是final类型的工具类,提供了很多静态方法。例如listeningDecorator方法初始化ListeningExecutorService方法,使用此实例submit方法即可初始化ListenableFuture对象。 ListenableFutureTask:该类是一个适配器,可以将其它Future适配成ListenableFuture。 ListeningExecutorService:该类是对ExecutorService的扩展,重写了ExecutorService类中的submit方法,并返回ListenableFuture对象。 JdkFutureAdapters:该类扩展了FutureTask类并实现ListenableFuture接口,增加了addListener方法。 Futures:该类提供和很多实用的静态方法以供使用。 Futures.addCallback方法源码剖析下面将模拟异步发送请求,并对请求结果进行(回调)监听。这里使用Spring框架提供的AsyncRestTemplate,来发送http请求,并获取一个org.springframework.util.concurrent.ListenableFuture对象,此时的对象是spring框架中的ListenableFuture对象。由于org.springframework.util.concurrent包中只提供了最基本的监听功能,没有其它额外功能,这里将其转化成Guava中的ListenableFuture,用到了JdkFutureAdapters这个适配器类。(以下源码来自guava-18.0.jar) 12345678910111213141516AsyncRestTemplate tp = new AsyncRestTemplate();org.springframework.util.concurrent.ListenableFuture<ResponseEntity<Object>> response = tp .getForEntity("http://blog.csdn.net/pistolove", Object.class);ListenableFuture<ResponseEntity<Object>> listenInPoolThread = JdkFutureAdapters.listenInPoolThread(response);Futures.addCallback(listenInPoolThread, new FutureCallback<Object>() { @Override public void onSuccess(Object result) { System.err.println(result.getClass()); System.err.printf("success", result); } @Override public void onFailure(Throwable t) { System.out.printf("failure"); }}); Futures的addCallback方法通过传入ListenableFuture和FutureCallback(一般情况FutureCallback实现为内部类)来实现回调机制。 1234//com.google.common.util.concurrent.Futurespublic static <V> void addCallback(ListenableFuture<V> future, FutureCallback<? super V> callback) { addCallback(future, callback, directExecutor());} 在addCallback方法中,我们发现多了一个 directExecutor()方法,这里的 directExecutor()方法返回的是一个枚举类型的线程池,这样做的目的是提高性能,而线程池中的execute方法实质执行的是所的传入参数Runnable 的run方法,可以把这里的线程池看作一个”架子”。 123456789101112//创建一个单实例的线程 接口需要显著的性能开销 提高性能public static Executor directExecutor() { return DirectExecutor.INSTANCE;}/** See {@link #directExecutor} for behavioral notes. */private enum DirectExecutor implements Executor { INSTANCE; @Override public void execute(Runnable command) { command.run(); }} 在具体的addCallback方法中,首先判断FutureCallback是否为空,然后创建一个线程,这个线程的run方法中会获取到一个value值,这里的value值即为http请求的结果,然后将value值传入FutureCallback的onSuccess方法,然后我们就可以在onSuccess方法中执行业务逻辑了。这个线程是如何执行的呢?继续往下看,发现调用了ListenableFuture的addListener方法,将刚才创建的线程和上一步创建的枚举线程池传入。 123456789101112131415161718192021222324252627282930313233//增加回调 public static <V> void addCallback(final ListenableFuture<V> future, final FutureCallback<? super V> callback, Executor executor) { Preconditions.checkNotNull(callback); //每一个future进来都会创建一个独立的线程运行 Runnable callbackListener = new Runnable() { @Override public void run() { final V value; try { // TODO(user): (Before Guava release), validate that this // is the thing for IE. //这里是真正阻塞的地方,直到获取到请求结果 value = getUninterruptibly(future); } catch (ExecutionException e) { callback.onFailure(e.getCause()); return; } catch (RuntimeException e) { callback.onFailure(e); return; } catch (Error e) { callback.onFailure(e); return; } //调用callback的onSuccess方法返回结果 callback.onSuccess(value); } }; //增加监听,其中executor只提供了一个架子的线程池 future.addListener(callbackListener, executor); } 在addListener方法中,将待执行的任务和枚举型线程池加入ExecutionList中,ExecutionList的本质是一个链表,将这些任务链接起来。具体可参考下方代码注释。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748@Override public void addListener(Runnable listener, Executor exec) { //将监听任务和线程池加入到执行列表中 executionList.add(listener, exec); //This allows us to only start up a thread waiting on the delegate future when the first listener is added. //When a listener is first added, we run a task that will wait for the delegate to finish, and when it is done will run the listeners. //这允许我们启动一个线程来等待future当第一个监听器被加入的时候 //当第一个监听器被加入,我们将启动一个任务等待future完成,一旦当前的future完成后将会执行监听器 //判断是否有监听器加入 if (hasListeners.compareAndSet(false, true)) { //如果当前的future完成则立即执行监听列表中的监听器,执行完成后返回 if (delegate.isDone()) { // If the delegate is already done, run the execution list // immediately on the current thread. //执行监听列表中的监听任务 executionList.execute(); return; } //如果当前的future没有完成,则启动线程池执行其中的任务,阻塞等待直到有一个future完成,然后执行监听器列表中的监听器 adapterExecutor.execute(new Runnable() { @Override public void run() { try { /* * Threads from our private pool are never interrupted. Threads * from a user-supplied executor might be, but... what can we do? * This is another reason to return a proper ListenableFuture * instead of using listenInPoolThread. */ getUninterruptibly(delegate); } catch (Error e) { throw e; } catch (Throwable e) { // ExecutionException / CancellationException / RuntimeException // The task is done, run the listeners. } //执行链表中的任务 executionList.execute(); } }); } } } 在ExecutionList的add方法中,判断是否执行完成,如果没有执行完成,则放入待执行的链表中并返回,否则调用executeListener方法执行任务,在executeListener方法中,我们发现执行的是线程池的execute方法,而execute方法实质的是调用了任务线程的run方法,这样最终会调用OnSuccess方法获取到执行结果。 12345678910111213141516171819202122232425//将任务放入ExecutionList中 public void add(Runnable runnable, Executor executor) { // Fail fast on a null. We throw NPE here because the contract of // Executor states that it throws NPE on null listener, so we propagate // that contract up into the add method as well. Preconditions.checkNotNull(runnable, "Runnable was null."); Preconditions.checkNotNull(executor, "Executor was null."); // Lock while we check state. We must maintain the lock while adding the // new pair so that another thread can't run the list out from under us. // We only add to the list if we have not yet started execution. //判断是否执行完成,如果没有执行完成,则放入待执行的链表中 synchronized (this) { if (!executed) { runnables = new RunnableExecutorPair(runnable, executor, runnables); return; } } // Execute the runnable immediately. Because of scheduling this may end up // getting called before some of the previously added runnables, but we're // OK with that. If we want to change the contract to guarantee ordering // among runnables we'd have to modify the logic here to allow it. //执行监听 executeListener(runnable, executor); } execute方法是执行任务链表中的任务,由于先加入的任务会依次排列在链表的末尾,所以需要将链表翻转。然后从链表头开始依次取出任务执行并放入枚举线程池中执行。 1234567891011121314151617181920212223242526272829303132333435363738394041//执行监听链表中的任务 public void execute() { // Lock while we update our state so the add method above will finish adding // any listeners before we start to run them. //创建临时变量保存列表,并将成员变量置空让垃圾回收 RunnableExecutorPair list; synchronized (this) { if (executed) { return; } executed = true; list = runnables; runnables = null; // allow GC to free listeners even if this stays around for a while. } // If we succeeded then list holds all the runnables we to execute. The pairs in the stack are // in the opposite order from how they were added so we need to reverse the list to fulfill our // contract. // This is somewhat annoying, but turns out to be very fast in practice. Alternatively, we // could drop the contract on the method that enforces this queue like behavior since depending // on it is likely to be a bug anyway. // N.B. All writes to the list and the next pointers must have happened before the above // synchronized block, so we can iterate the list without the lock held here. //因为先加入的监听任务会在连边的末尾,所以需要将链表翻转 RunnableExecutorPair reversedList = null; while (list != null) { RunnableExecutorPair tmp = list; list = list.next; tmp.next = reversedList; reversedList = tmp; } //从链表头中依次取出监听任务执行 while (reversedList != null) { executeListener(reversedList.runnable, reversedList.executor); reversedList = reversedList.next; } } 在上文中,可以发现每当对一个ListenerFuture增加回调时,都会创建一个线程,而这个线程的run方法中会获取一个value值,这个value值就是通过下面的getUninterruptibly方法获取到的,我们可以发现在方法中调用了while进行阻塞,一直等到future获取到结果,即发送的http请求获取到数据后才会终止并返回。可以看出,回调机制将获取结果中的阻塞分散开来,即使现在有100个线程在并发地发送http请求,那么也只是创建了100个独立的线程并行阻塞,那么运行的总时间则会是这100个线程中最长的时间,而不是100个线程的时间相加,这样就实现了异步非阻塞机制。 123456789101112131415161718//用while阻塞直到获取到结果 public static <V> V getUninterruptibly(Future<V> future) throws ExecutionException { boolean interrupted = false; try { while (true) { try { return future.get(); } catch (InterruptedException e) { interrupted = true; } } } finally { if (interrupted) { Thread.currentThread().interrupt(); } } } 这里实质上执行了线程的run方法,并进行阻塞。 12345678910111213//执行监听器,调用线程池的execute方法,这里线程池并没有提供额外的功能,只提供了执行架子,实际上执行的是监听任务runnable的run方法 //而在监听任务的run方法中,会阻塞获取请求结果,请求完成后回调,已达到异步执行的效果 private static void executeListener(Runnable runnable, Executor executor) { try { executor.execute(runnable); } catch (RuntimeException e) { // Log it and keep going, bad runnable and/or executor. Don't // punish the other runnables if we're given a bad one. We only // catch RuntimeException because we want Errors to propagate up. log.log(Level.SEVERE, "RuntimeException while executing runnable " + runnable + " with executor " + executor, e); } } 使用JdkFutureAdaoter适配Spring中的ListenableFuture达到异步调用的结果。在future.get方法中到底阻塞在什么地方呢?通过调试发现最后调用的是BasicFuture中的阻塞方法。详情见下方源码和中文注释,这里不累赘。 123456FutureAdapter: //这里的get方法会调用BasicFuture中的get方法进行阻塞,直到获取到结果 @Override public T get() throws InterruptedException, ExecutionException { return adaptInternal(this.adaptee.get()); } 12345678BasicFuture: //在这里会判断当前future是否执行完成,如果没有完成则会等待,一旦执行完成则返回结果。 public synchronized T get() throws InterruptedException, ExecutionException { while (!this.completed) { wait(); } return getResult(); } 123456789101112131415161718192021222324252627282930313233FutureAdapter: //这里通过判断状态是否success,如果success则返回成功,如果new, 则阻塞等待结果直到返回,然后改变状态。 @SuppressWarnings("unchecked") final T adaptInternal(S adapteeResult) throws ExecutionException { synchronized (this.mutex) { switch (this.state) { case SUCCESS: return (T) this.result; case FAILURE: throw (ExecutionException) this.result; case NEW: try { T adapted = adapt(adapteeResult); this.result = adapted; this.state = State.SUCCESS; return adapted; } catch (ExecutionException ex) { this.result = ex; this.state = State.FAILURE; throw ex; } catch (Throwable ex) { ExecutionException execEx = new ExecutionException(ex); this.result = execEx; this.state = State.FAILURE; throw execEx; } default: throw new IllegalStateException(); } } } 123456789101112131415//这个方法判断 public boolean completed(final T result) { synchronized(this) { if (this.completed) { return false; } this.completed = true; this.result = result; notifyAll(); } if (this.callback != null) { this.callback.completed(result); } return true; } 总结本文主要剖析了Futures.callback方法的源码,我们只需要一个ListenableFuture的实例,就可以使用该方法来实现回调机制。假设在我们的主线程中,有n个子方法要发送http请求,这时,我们可以创建n个ListenableFuture,对这n个ListenableFuture增加监听,这n个请求就是异步且非阻塞的,这样不但主线程不会阻塞,而且会大大减少总的响应时间。那Futures.callback是如何实现并发的呢?通过源码,我们发现,对于每一个ListenableFuture,都会创建一个独立的线程对其进行监听,也就是这n个ListenableFuture对应着n个独立的线程,而在每一个独立的线程中会各自调用Future.get方法阻塞。]]></content>
<categories>
<category>网络通信</category>
</categories>
<tags>
<tag>async</tag>
</tags>
</entry>
<entry>
<title><![CDATA[自己动手实现RPC框架(二)之项目结构]]></title>
<url>%2F2018%2F07%2F10%2Fepoch-rpc%2Fepoch-rpc-2%2F</url>
<content type="text"><![CDATA[前言 暂时还没法确定到底是什么样的包结构,等写完的时候再来填充这里。]]></content>
<categories>
<category>分布式服务</category>
</categories>
<tags>
<tag>rpc</tag>
</tags>
</entry>
<entry>
<title><![CDATA[自己动手实现RPC框架(一)]]></title>
<url>%2F2018%2F07%2F10%2Fepoch-rpc%2Fepoch-rpc-1%2F</url>
<content type="text"><![CDATA[前言 RPC的概念看过很多,我的理解是:调用端获取到服务(网络方法)提供者的网络地址,并把方法调用的参数通过网络传递给提供者,提供者监听并获取到参数后,调用自己的方法,再把执行结果通过网络回传给调用端。这样站在调用端的角度看,就像是调用自己的本地方法一样,只不过慢一些而已。 RPC经典的架构图如下: RPC架构中可以认为有四个角色,消费者(Consumer),提供者(Provider),注册中心(Registry)和监控中心(Monitor)。以前在同一系统里的方法调用者因为网络的存在,变成了消费者,被调的方法成为了提供者。而所谓的注册中心,其实就是为了让消费者实时的去感知提供者的存在,去告诉消费者它对应的提供者的地址。监控中心,其实在整个过程中,它并不是一定要存在,只是它可以做统计,做一些数据分析,提供整个系统的可用性,健壮性。 好了,我们先简单分析一下 Registry / Consumer / Provider / Monitor 这四个角色的定义和每个角色如何各司其职,相互协作完成这整个过程的。 下面从两个方面进行分析,一是每个角色在网络的定位,二是每个角色所要完成的职责。 Registry注册中心简述: 注册中心可以有多个,都是无状态的,每个注册中心之间信息不交互 从网络的角度来说,它都是server端,它不需要主动地连接其他的任何实例,只需要像一个地主一样等待别人来连接 消费者随机选择注册中心集群中的任何实例建立长连接,提供者与注册中心中的每一个实例都建立长连接 职责(与其说职责,还不如说代码要实现的功能): 接收服务提供者的服务注册信息,接收到信息之后,发送ACK信息给服务提供者,否则服务提供者重新发送注册信息 接收消费者的订阅信息,并把它订阅的结果返回给消费者 如果注册信息变更,会主动通知订阅变更信息的消费者,注册信息的变更包括服务提供者下线,服务被人工降级,或者服务提供者的地址变更 持久化一些服务信息,例如某些服务管理员审核过了,则该服务重新注册后则不需要再审核,再例如,某个服务负载均衡的策略被管理员设置为轮询,那么下次它在注册的时候,则就是轮询,而不是默认的负载策略 Provider提供者简述: 提供者是一个精神分裂的病人,它在网络上(可以更加明确地说是站在Netty的角度上)饰演两个角色: 它是客户端,需要去连接Registry,发送注册信息,它也需要去连接monitor端,去发送一些调用的统计信息 它也是服务端,需要作为server端等待Consumer去连接,连接成功后调用服务 职责: 将自己的信息,提供的接口信息编织成注册信息发送给registry端 能够动态去调自己的方法,可以通过反射,cglib等一些方法去调用自己提供的那些方法 提供服务降级等服务,如果当某些服务调用的失败率高于限定值的时候,可以有一个对应的mock方法,提供降级服务 限流服务,限流的方式有很多种,也有很多实现方式,最简单的就是控制调用次数,比如100w次,其实简单的就是控制单位时间的调用次数,防止业务洪流冲垮服务 统计活动,将一些调用信息统计好发送给Monitor端 Consumer消费者简述: 它也是有两个网络角色,不过并不是精神分裂,它都是作为网络的客户端存在,一它需要去连接registry去获取到订阅信息,二是它需要主动去连接provider端去调用服务 职责: 去向Registry端订阅服务,拿到registry端返回的结果,这个结果也就是provider的网络地址,先建立TCP的长连接,可能是多个地址,因为提供某个服务的可能有多个提供者 当开始系统主动调用该服务的时候,拿到刚才建立的连接的集合,根据某个方法,是随意还是轮询,获取到其中的一个连接,发送方法入参,等待响应 当注册中心发送某个服务的调用的负载策略发生变化过,发送信息给consumer,consumer需要做相应的变更 Monitor监控者简述: 这个与整个系统是没有任何直接的关系的,实现方式也是多样的,可以与上面一样建立长连接,接收每个角色统计的信息,然后展示给用户,可以使用MQ,使用消息队列,每个角色把自己统计的信息放到队列中,Monitor去消费这些信息,这样做的好处就是解耦,如果monitor宕了,不影响服务 大体的RPC的流程稍微理了一下,接下来我们就来一一去实现这些功能~]]></content>
<categories>
<category>分布式服务</category>
</categories>
<tags>
<tag>rpc</tag>
</tags>
</entry>
<entry>
<title><![CDATA[自己动手实现RPC框架]]></title>
<url>%2F2018%2F07%2F05%2Fepoch-rpc%2Fepoch-rpc-0%2F</url>
<content type="text"><![CDATA[前言 RPC的文章看了不少,但是始终感觉似懂非懂,古人说的”纸上得来终觉浅,绝知此事要躬行”还是很有道理的。所以我希望通过一个真正的项目,来加深对RPC的认识。 这应该会是一个系列文章,至少在我动笔的这一刻是有非常强烈的意愿完成它的。 主要参考CSDN博客一起写RPC框架开篇说明 2018-09-03续,果不其然,还是因为诸多事情耽搁了。 今天又看了一些RPC的文章,发现黄勇老师的轻量级分布式 RPC 框架写的非常清晰易懂。于是整理出一些关键点出来,方便自己实现。 编写RPC框架的步骤如下: 编写服务接口 编写服务接口的实现类 配置服务端 启动服务器并发布服务 实现服务注册 实现 RPC 服务器 配置客户端 实现服务发现 实现 RPC 代理 发送 RPC 请求 注册中心的职责核心的职责如下: 服务提供者向其发送它提供的服务的一些基本信息,并完成注册 服务消费者订阅服务 服务提供者下线的时候,实时通知服务消费者某个服务下线 除此之外,还有一些锦上添花但在真实的业务中必须得有的功能,比如: 服务审核,这是服务治理最最简单的操作了,因为某个服务提供者上线之后,都是需要审核的,如果不审核,可能会造成很多不必要的麻烦,有可能有些开发小新,不小心把开发环境的服务向线上服务注册,如果不审核,直接通过的话,就会造成线上接口调用线下服务的尴尬局面 负载策略的记录,比如默认是随机加权策略,如果管理者希望改成加权轮询的策略,需要通知服务消费者,访问策略的改变 手动改变某个服务的访问权重,比如某个服务默认负重是50,(最大100)的时候,但是此时这个服务实例所在的机器压力不大的时候,而其他该服务实例压力很大的时候,可以适当的增加该服务的访问权重,所以我们可以在注册中心修改它的负重,然后通知服务消费者,这样就可以动态的修改负重了 一些持久化的操作,因为注册中心是无状态的,假如某个注册中心实例重启之后,以前的一些审核信息,修改的访问策略信息就会消失,这样就会需要用户重新一一审核,这是很麻烦的,所以需要将这些信息落地,持久化到硬盘,然后每次重启注册中心实例的时候,去读取这些信息 基于ZooKeeper的注册中心节点树结构如下: 计划(暂定) 2018-07-08 架构设计,功能(细节)设计 2018-07-15 网络传输模型,序列化部分 2018-07-22 服务端框架 2018-07-29 客户端框架 2018-08-06 负载均衡 2018-08-13 服务降级 2018-08-20 测试与优化]]></content>
<categories>
<category>分布式服务</category>
</categories>
<tags>
<tag>rpc</tag>
</tags>
</entry>
<entry>
<title><![CDATA[单例模式]]></title>
<url>%2F2018%2F07%2F05%2Fjava%2Fjava-singleton%2F</url>
<content type="text"><![CDATA[前言 在GoF的23种设计模式中,单例模式是比较简单的一种。然而,有时候越是简单的东西越容易出现问题。下面就单例设计模式详细的探讨一下。 所谓单例模式,简单来说,就是在整个应用中保证类只有一个实例存在。这个类的实例只提供了一个全局变量,用处相当广泛,比如保存全局数据,实现全局性的操作等。 最简单的实现首先,能够想到的最简单的实现是,把类的构造函数写成private的,从而保证别的类不能实例化此类,然后在类中提供一个静态的实例并能够返回给使用者。这样,使用者就可以通过这个引用使用到这个类的实例了。 123456789101112public class SingletonClass { private static SingletonClass instance = new SingletonClass(); public static SingletonClass getInstance() { return instance; } private SingletonClass() { }} 外部使用者如果需要使用SingletonClass的实例,只能通过getInstance()方法,并且它的构造方法是private的,这样就保证了只能有一个对象存在。 性能优化上面的代码虽然简单,但是有一个问题—-无论这个类是否被使用,都会创建一个instance对象。如果这个创建过程很耗时,比如需要连接10000次jdbc实例连接或者10000多个模版实例,并且这个类还并不一定会被使用,那么这个创建过程就是无用的。 为了解决这个问题,我们想到了新的解决方案: 123456789101112131415public class SingletonClass { private static SingletonClass instance = null; public static SingletonClass getInstance() { if (instance == null) { instance = new SingletonClass(); } return instance; } private SingletonClass() { }} 代码的变化有一处—-把instance初始化为null,直到第一次使用的时候通过判断是否为null来创建对象。 我们来想象一下这个过程。要使用SingletonClass,调用getInstance()方法。第一次的时候发现instance是null,然后就新建一个对象,返回出去;第二次再使用的时候,因为这个instance是static的,所以已经不是null了,因此不会再创建对象,直接将其返回。 这个过程就称为lazy loaded,也就是延迟加载—-直到使用的时候才进行加载。 同步上面的代码很清楚,也很简单。然而就像那句名言:”80%的错误都是由20%代码优化引起的”。单线程下,这段代码没有什么问题,可是如果是多线程,麻烦就来了。我们来分析一下: 线程1希望使用SingletonClass,调用getInstance()方法。因为是第一次调用,1就发现instance是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换(或者被抢夺执行),线程2开始执行,它要使用SingletonClass,调用getInstance()方法,同样检测到instance是null—-注意,这是在1检测完之后切换的,也就是说1并没有来得及创建对象—-因此2开始创建。2创建完成后,cpu切换到1继续执行,因为它已经检测完了,所以1不会再检测一遍,它会直接创建对象。这样,线程1和2各自拥有一个SingletonClass的对象—-单例失败!解决的方法也很简单,那就是加锁: 12345678910111213141516public class SingletonClass { private static SingletonClass instance = null; public synchronized static SingletonClass getInstance() { if(instance == null) { instance = new SingletonClass(); } return instance; } private SingletonClass() { }} 又是性能问题上面的代码又是很清楚很简单的,然而,简单的东西往往不够理想。理想的东西往往不够简单,这就是生活。这段代码毫无疑问存在性能的问题—-synchronized修饰的同步块可是要比一般的代码段慢上几倍的!如果存在很多次getInstance()的调用,那性能问题就不得不考虑了! 让我们来分析一下,究竟是整个方法都必须加锁,还是仅仅其中某一句加锁就足够了?我们为什么要加锁呢?分析一下出现lazy loaded的那种情形的原因。原因就是检测null的操作和创建对象的操作分离了。如果这两个操作能够原子地进行,那么单例就已经保证了。于是,我们开始修改代码: 1234567891011121314151617181920public class SingletonClass { private static SingletonClass instance = null; public static SingletonClass getInstance() { if (instance == null) { synchronized (SingletonClass.class) { if (instance == null) { instance = new SingletonClass(); } } } return instance; } private SingletonClass() { }} 还有问题吗?首先判断instance是不是为null,如果为null,加锁初始化;如果不为null,直接返回instance。 这就是double-checked locking设计实现单例模式。但是还有问题。 在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。 下面来想一下,创建一个变量需要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JMM规范并没有规定。(可能重排序)那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。 线程1开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,1已经把instance指向了那块内存,只是还没有调用构造方法,因此2检测到instance不为null,于是直接把instance返回了—-问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还是毛坯房。此时,如果2在1将instance构造完成之前就是用了这个实例,程序就会出现错误了! 最终解决方案在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义—-在JDK1.5之前,volatile是个关键字,但是并没有明确的规定其用途—-被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,只要我们简单的把instance加上volatile关键字就可以了。 1234567891011121314151617181920public class SingletonClass { private volatile static SingletonClass instance = null; public static SingletonClass getInstance() { if (instance == null) { synchronized (SingletonClass.class) { if(instance == null) { instance = new SingletonClass(); } } } return instance; } private SingletonClass() { }}]]></content>
<categories>
<category>编程语言</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Git常用操作记录]]></title>
<url>%2F2018%2F07%2F05%2Ftools%2Fgit-operation%2F</url>
<content type="text"><![CDATA[获取当前分支名 1git symbolic-ref --short -q HEAD 将本地仓库和github仓库关联起来 12git remote add github git@github.com:liningrui/study-rpc.gitgit pull 再查看所有分支就可以看到github远端分支的信息了 1git branch -av 删除github远端的分支 1git push github :travis 这样就删除了travis分支 创建orphan分支,名为source 1git checkout --orphan source 注:如果不提交东西,这个分支实际上没有创建 查看某个指定文件的提交历史记录 1git log -p filePath 这样就先显示指定文件的每一次提交及修改信息(diff),但是不能显示文件改名前的修改,要注意第一次提交是不是改文件名 查看某一个分支创建的时间 1git reflog show --date=iso branch 最下面的应该就是该分支的创建时间 修改提交历史 1、找到要修改的commit id及其前一个commit id 1git rebase -i --before-commit-id 弹出来的一堆以 pick 开头的 commit id 和 commit message 的行,将第一行(也允许修改多行)的 pick 修改为 edit,然后保存退出vim,git 会在标记的 commit 停下来,然后我们可以做相应的修改,再执行 12git commit -a --amendgit rebase --continue 这时 git 会打印 rebasing(progress/total),中间很有可能会产生冲突,解决好冲突后执行 12git add filegit rebase --continue 一直往下走,遇到冲突就重复这一步,直到走完全部的提交,这样就实现了修改历史。 拉取别人的提交到本地 1git fetch github pull/493/head:batch-update]]></content>
<categories>
<category>开发工具</category>
</categories>
<tags>
<tag>Git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Mac 下安装 jekyll]]></title>
<url>%2F2018%2F07%2F04%2Fmac-install-jekyll%2F</url>
<content type="text"><![CDATA[Mac 下安装 jekyll1sudo gem install jekyll 输入密码,但还是会提示没有写权限 12ERROR: While executing gem ... (Gem::FilePermissionError) You don't have write permissions for the /usr/bin directory. 原因是 Apple在OS X El Capitan中全面启用了名为System Integrity Protection (SIP)的系统完整性保护技术。受此影响,大部分系统文件即使在root用户下也无法直接进行修改。 升级ruby(推荐) 安装RVM1curl -L get.rvm.io | bash -s stable 出现异常 12345678910111213141516171819gpg: Signature made 一 7/ 2 03:41:26 2018 CSTgpg: using RSA key 62C9E5F4DA300D94AC36166BE206C29FBF04FF17gpg: Can't check signature: No public keyWarning, RVM 1.26.0 introduces signed releases and automated check of signatures when GPG software found. Assuming you trust Michal Papis import the mpapis public key (downloading the signatures).GPG signature verification failed for '/Users/liningrui/.rvm/archives/rvm-1.29.4.tgz' - 'https://github.com/rvm/rvm/releases/download/1.29.4/1.29.4.tar.gz.asc'! Try to install GPG v2 and then fetch the public key: gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3or if it fails: command curl -sSL https://rvm.io/mpapis.asc | gpg2 --import -the key can be compared with: https://rvm.io/mpapis.asc https://keybase.io/mpapisNOTE: GPG version 2.1.17 have a bug which cause failures during fetching keys from remote server. Please downgrade or upgrade to newer version (if available) or use the second method described above. 你是因为我本地安装了gpg,但是却没有它的公钥,所以我们需要先接受公钥到本地。 1gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 然后再执行上述命令,就应该Ok了。 1234567891011121314151617gpg: Signature made 一 7/ 2 03:41:26 2018 CSTgpg: using RSA key 62C9E5F4DA300D94AC36166BE206C29FBF04FF17gpg: Good signature from "Michal Papis (RVM signing) <mpapis@gmail.com>" [unknown]gpg: aka "Michal Papis <michal.papis@toptal.com>" [unknown]gpg: aka "[jpeg image of size 5015]" [unknown]gpg: WARNING: This key is not certified with a trusted signature!gpg: There is no indication that the signature belongs to the owner.Primary key fingerprint: 409B 6B17 96C2 7546 2A17 0311 3804 BB82 D39D C0E3 Subkey fingerprint: 62C9 E5F4 DA30 0D94 AC36 166B E206 C29F BF04 FF17GPG verified '/Users/liningrui/.rvm/archives/rvm-1.29.4.tgz'Installing RVM to /Users/liningrui/.rvm/ Adding rvm PATH line to /Users/liningrui/.profile /Users/liningrui/.mkshrc /Users/liningrui/.bashrc /Users/liningrui/.zshrc. Adding rvm loading line to /Users/liningrui/.profile /Users/liningrui/.bash_profile /Users/liningrui/.zlogin.Installation of RVM in /Users/liningrui/.rvm/ is almost complete: * To start using RVM you need to run `source /Users/liningrui/.rvm/scripts/rvm` in all your open shell windows, in rare cases you need to reopen all shell windows. 它提示说要使用RVM需要将rvm添加到环境变量中。 12source /Users/liningrui/.rvm/scripts/rvmrvm -v 列出所有可用的ruby版本 1rvm list known 安装最新版本的ruby(以2.5.1为例) 1rvm install 2.5.1 安装jekyll1gem install jekyll 安装完成后,cd到项目根目录,使用以下命令即可运行jekyll环境,通过 localhost:4000 即可访问。 1jekyll serve 提示1Dependency Error: Yikes! It looks like you don't have jekyll-paginate or one of its dependencies installed. In order to use Jekyll as currently configured, you'll need to install this gem. The full error message from Ruby is: 'cannot load such file -- jekyll-paginate' If you run into trouble, you can find helpful resources at https://jekyllrb.com/help/! 安装即可 1gem install jekyll-paginate 接下来就可以开始github pages之路了~ 参考: https://www.cnblogs.com/kaiye/archive/2013/04/24/3039345.html https://blog.csdn.net/andanlan/article/details/50061775]]></content>
</entry>
<entry>
<title><![CDATA[阻塞、非阻塞、同步、异步的区别]]></title>
<url>%2F2018%2F07%2F04%2Fnetwork-io%2F</url>
<content type="text"><![CDATA[我认为同步、异步、阻塞、非阻塞,是分3个层次的: CPU层次; 线程层次; 程序员感知层次。 这几个概念之所以容易混淆,是因为没有分清楚是在哪个层次进行讨论。 CPU层次在 CPU 层次,或者说操作系统进行 IO 和任务调度的层次,现代操作系统通常使用异步非阻塞方式进行 IO(有少部分 IO 可能会使用同步非阻塞轮询),即发出 IO 请求之后,并不等待 IO 操作完成,而是继续执行下面的指令(非阻塞),IO 操作和 CPU 指令互不干扰(异步),最后通过中断的方式来通知 IO 操作完成结果。 线程层次在线程层次,或者说操作系统调度单元的层次,操作系统为了减轻程序员的思考负担,将底层的异步非阻塞的IO方式进行封装,把相关系统调用(如read,write等)以同步的方式展现出来。然而,同步阻塞的IO会使线程挂起,同步非阻塞的IO会消耗CPU资源在轮询上。为了解决这一问题,就有3种思路: 多线程(同步阻塞); IO 多路复用(select,poll,epoll)(同步非阻塞,严格地来讲,是把阻塞点改变了位置); 直接暴露出异步的 IO 接口,如 kernel-aio 和 IOCP(异步非阻塞)。 程序员感知层次在Linux中,上面提到的第2种思路用得比较广泛,也是比较理想的解决方案。然而,直接使用select之类的接口,依然比较复杂,所以各种库和框架百花齐放,都试图对IO多路复用进行封装。此时,库和框架提供的API又可以选择是以同步的方式还是异步的方式来展现。如python的asyncio库中,就通过协程,提供了同步阻塞式的API;如node.js中,就通过回调函数,提供了异步非阻塞式的API。 总结因此,我们在讨论同步、异步、阻塞、非阻塞时,必须先明确是在哪个层次进行讨论。比如node.js,我们可以说她在程序员感知层次提供了异步非阻塞的API,也可以说在Linux下,她在线程层次以同步非阻塞的epoll来实现。]]></content>
<categories>
<category>网络通信</category>
</categories>
<tags>
<tag>NIO,IO</tag>
</tags>
</entry>
<entry>
<title><![CDATA[记一次 HttpClient 连接半释放导致的问题]]></title>
<url>%2F2018%2F06%2F10%2Fhugegraph%2F%E8%AE%B0%E4%B8%80%E6%AC%A1%20HttpClient%20%E8%BF%9E%E6%8E%A5%E5%8D%8A%E9%87%8A%E6%94%BE%E5%AF%BC%E8%87%B4%E7%9A%84%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[问题描述启动 0.9.2 版本的 hugegraph-server 和 hugegraph-studio,随便执行两条查询语句,然后停止 hugegraph-server,再启动,提示 “8080 端口被占用”。 详细 问题定位Step 1最开始没仔细想,主要也是因为对 TCP 这块没啥实际经验,乍一看以为是 studio 每次执行 gremlin 建立一个连接且连接未关闭导致的问题。于是简单的在 studio每个请求处理完成后将 HugeClient 关闭掉,即 finally{ client.close() },然后测试,每个请求处理完,连接都正常关闭,然后停止 hugegraph-server再重启,果然就没问题了,看来这个问题很简单嘛。 但是这么搞总觉得太暴力了,按照书上和博客的说法,连接的建立/释放都是不小的开销,咱能不能重用他呢? Step 2在 studio 中重用 HugeClient 也很简单,简单点的话,把它定义为静态的,然后请求入口处以单例的形式获取 HugeClient 即可。我在 loader 中已经写过一个HugeClientWarpper,所以这里也很快完成。再次测试,除第一次请求创建了一个连接外,后面的请求都没有创建新的连接,看起来节约了开销。然后停止hugegraph-server 再重启,结果又提示了 “8080 端口被占用”。再查看 studio 进程的 TCP 连接使用情况,发现还是有一个处于 CLOSE_WAIT 状态的连接,并且这个连接一直不会关闭。 除了停止 hugegraph-server 会产生 CLOSE_WAIT 的连接外,让 hugegraph-server 闲置一会也会在 studio 进程中产生 CLOSE_WAIT 状态的连接,但是只要studio 再请求一次,那个 CLOSE_WAIT 状态的连接会消失,然后产生一个新的 ESTABLISHED 状态的连接。 走到这里其实会发现两个问题: 为什么即使不停止 hugegraph-server 也会产生 CLOSE_WAIT 状态的连接? 为什么 CLOSE_WAIT 状态的连接不会自己消失,而是要等到 studio 再请求一次才会消失? Step 3要解释第一个问题,得找到 TCP 连接关闭的四次握手时序图 可以看到,连接关闭中只有被动关闭的一方才会出现 CLOSE_WAIT 状态,所以肯定是 hugegraph-server 主动关闭了连接。 然后发现 RestServer 有一个 KeepAlive IdleTimeout,闲置超过此时间的连接会被 jersey 关闭。 The time in seconds to keep an inactive connection alive. 这个参数的默认值为 30 秒,我们将其修改为 60,验证确实符合预期。 这里插一句题外话,KeepAlive 除了有 IdleTimeout 参数,还有一个 Max Requests Count。 The max number of HTTP requests allowed to be processed on one keep-alive connection. 这个参数的意思是:当一个连接处理的请求数超过该值了,就将其关闭。默认为 256,我们将其修改为 5,但是经过调试发现,在处理完第 6 个请求之后才会关闭连接,而不是我们设置的 5。调试代码: 12345678910111213141516171819202122private boolean checkKeepAliveRequestsCount(final HttpContext httpContext) { if (!allowKeepAlive) { return false; } final KeepAliveContext keepAliveContext = keepAliveContextAttr.get(httpContext); final int requestsProcessed = keepAliveContext.requestsProcessed++; final int maxRequestCount = keepAlive.getMaxRequestsCount(); final boolean isKeepAlive = (maxRequestCount == -1 || keepAliveContext.requestsProcessed <= maxRequestCount); if (requestsProcessed == 0) { if (isKeepAlive) { // New keep-alive connection KeepAlive.notifyProbesConnectionAccepted(keepAlive, keepAliveContext.connection); } else { // Refused keep-alive connection KeepAlive.notifyProbesRefused(keepAlive, keepAliveContext.connection); } } return isKeepAlive;} 关键在于keepAliveContext.requestsProcessed <= maxRequestCount,当处理完第一个请求,keepAliveContext.requestsProcessed的值为1,第二个请求值为2 … 第五个请求为5,仍然是满足 <= 5 条件的,这时仍然认为这个连接是 KeepAlive 的,直到处理完第六个才不满足条件,才会进行关闭连接的操作。 我觉得这是一个 BUG,至少这个参数的说明与表现不符。但是找了半天也没找到该往 github 的哪个仓库反馈。 Step 4为什么 CLOSE_WAIT 状态的连接不会自己消失,而是要等到 studio 再请求一次才会消失? 这里得跟踪 HttpClient 的代码,我们以服务端关闭了连接,studio 的连接变成了 CLOSE_WAIT 之后作为调试起始点,在PoolingHttpClientConnectionManager和AbstractConnPool中获取连接和关闭连接的代码处加了很多断点,经过一番折腾,终于发现,在AbstractConnPool的getPoolEntryBlocking方法中找到了第二个问题的答案。下面给出关键代码: 1234567891011121314151617private E getPoolEntryBlocking() { ... while (entry == null) { ... else if (this.validateAfterInactivity > 0) { if (entry.getUpdated() + this.validateAfterInactivity <= System.currentTimeMillis()) { if (!validate(entry)) { entry.close(); } } } ... } ...} 从if (!validate(entry))一直往里跟会走到AbstractHttpClientConnection的isStale()方法,该方法会尝试从socket的InputBuffer中读一点数据,读不到就认为stale,然后validate(entry)验证失败,调用entry.close()将连接关闭。 说到这里基本上已经弄明白了上面这两个问题,但其实还有一个问题不太清楚。 服务端主动关闭连接导致客户端处于 CLOSE_WAIT 状态,但是根据四次握手的流程图,为什么服务端没有卡在 FIN-WAIT1 的状态,而是直接就消失了,就好像是正常关闭了一样。]]></content>
<categories>
<category>hugegraph</category>
</categories>
<tags>
<tag>问题分析</tag>
</tags>
</entry>
</search>