-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.xml
More file actions
458 lines (458 loc) · 257 KB
/
search.xml
File metadata and controls
458 lines (458 loc) · 257 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
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[浅谈ProtoBuf编码]]></title>
<url>%2F2019%2F11%2F14%2F%E6%B5%85%E8%B0%88ProtoBuf%E7%BC%96%E7%A0%81%2F</url>
<content type="text"><![CDATA[编码结构TLV 格式是我们比较熟悉的编码格式。 所谓的 TLV 即 Tag - Length - Value。Tag 作为该字段的唯一标识,Length 代表 Value 数据域的长度,最后的 Value 并是数据本身。 ProtoBuf 编码采用类似的结构,但是实际上又有较大区别,其编码结构可见下图: 我们来一步步解析上图所表达的编码结构。 首先,每一个 message 进行编码,其结果由一个个字段组成,每个字段可划分为 Tag - [Length] - Value ,如下图所示: 特别注意这里的 [Length] 是可选的,含义是针对不同类型的数据编码结构可能会变成 Tag - Value 的形式,如果变成这样的形式,没有了 Length 我们该如何确定 Value 的边界?答案就是 Varint 编码,在后面将详细介绍。 继续深入 Tag ,Tag 由 field_number 和 wire_type 两个部分组成: field_number: message 定义字段时指定的字段编号 wire_type: ProtoBuf 编码类型,根据这个类型选择不同的 Value 编码方案。 整个 Tag 采用 Varints 编码方案进行编码,Varints 编码会在后面详细介绍。 Tag 结构如下图所示: 3 bit 的 wire_type 最多可以表达 8 种编码类型,目前 ProtoBuf 已经定义了 6 种,如下图所示: 第一列即是对应的类型编号,第二列为面向最终编码的编码类型,第三列是面向开发者的 message 字段的类型。 注意其中的 Start group 和 End group 两种类型已被遗弃。 另外要特别注意一点,虽然 wire_type 代表编码类型,但是 Varint 这个编码类型里针对 sint32、sint64 又会有一些特别编码(ZigTag 编码)处理,相当于 Varint 这个编码类型里又存在两种不同编码。 重新来看完整的编码结构图: 现在我们可以理解一个 message 编码将由一个个的 field 组成,每个 field 根据类型将有如下两种格式: Tag - Length - Value:编码类型表中 Type = 2 即 Length-delimited 编码类型将使用这种结构, Tag - Value:编码类型表中 Varint、64-bit、32-bit 使用这种结构。 可以思考一下为什么这么设计编码方案?可以看完下面各种编码详细的介绍再来细细品味这个问题。 其中 Tag 由字段编号 field_number 和 编码类型 wire_type 组成, Tag 整体采用 Varints 编码。 现在来模拟一下,我们接收到了一串序列化的二进制数据,我们先读一个 Varints 编码块,进行 Varints 解码,读取最后 3 bit 得到 wire_type(由此可知是后面的 Value 采用的哪种编码),随后获取到 field_number (由此可知是哪一个字段)。依据 wire_type 来正确读取后面的 Value。接着继续读取下一个字段 field。 Varints 编码上一节中多次提到 Varints 编码,现在我们来正式介绍这种编码方案。 总结的讲,Varints 编码的规则主要为以下三点: 在每个字节开头的 bit 设置了 msb(most significant bit ),标识是否需要继续读取下一个字节 存储数字对应的二进制补码 补码的低位排在前面 为什么低位排在前面?这里主要是为编码实现(移位操作)做的一个小优化。可以尝试写个二进制移位进行编码解码的小例子来体会这一点。 来看一个最为简单的例子: 1234int32 val = 1; // 设置一个 int32 的字段的值 val = 1; 这时编码的结果如下原码:0000 ... 0000 0001 // 1 的原码表示补码:0000 ... 0000 0001 // 1 的补码表示Varints 编码:0#000 0001(0x01) // 1 的 Varints 编码,其中第一个字节的 msb = 0 编码过程数字 1 对应补码 0000 ... 0000 0001(规则 2),从末端开始取每 7 位一组并且反转排序(规则 3),因为 0000 ... 0000 0001 除了第一个取出的 7 位组(即原数列的后 7 位),剩下的均为 0。所以只需取第一个 7 位组,无需再取下一个 7 bit,那么第一个 7 位组的 msb = 0。最终得到 0 | 000 0001(0x01) 。 解码过程我们再做一遍解码过程,加深理解。 编码结果为 0#000 0001(0x01) 。首先,每个字节的第一个 bit 为 msb 位,msb = 1 表示需要再读一个字节(还未结束),msb = 0 表示无需再读字节(读取到此为止)。 在上面的例子中,数字 1 的 Varints 编码中 msb = 0,所以只需要读完第一个字节无需再读。去掉 msb 之后,剩下的 000 0001 就是补码的逆序,但是这里只有一个字节,所以无需反转,直接解释补码 000 0001,还原即为数字 1。 注意:这里编码数字 1,Varints 只使用了 1 个字节。而正常情况下 int32 将使用 4 个字节存储数字 1。 仔细品味上述的 Varints 编码,我们可以发现 Varints 的本质实际上是每个字节都牺牲一个 bit 位(msb),来表示是否已经结束(是否还需要读取下一个字节),msb 实际上就起到了 Length 的作用,正因为有了 msb(Length),所以我们可以摆脱原来那种无论数字大小都必须分配四个字节的窘境。通过 Varints 我们可以让小的数字用更少的字节表示。从而提高了空间利用和效率。 这里为什么强调牺牲?因为每个字节都拿出一个 bit 做 msb,而原先这个 bit 是可直接用来表示 Value 的,现在每个字节都少了一个 bit 位即只有 7 位能真正用来表达 Value。那就意味这 4 个字节能表达的最大数字为 2^28,而不再是 2^32 了。 这意味着什么?意味着当数字大于 2^28 时,采用 Varints 编码将导致分配 5 个字节,而原先明明只需要 4 个字节,此时 Varints 编码的效率不仅不是提高反而是下降。 但这并不影响 Varints 在实际应用时的高效,因为事实证明,在大多数情况下,小于 2^28 的数字比大于 2^28 的数字出现的更为频繁。 到目前为止,好像一切都很完美。但是当前的 Varints 编码却存在着明显缺陷。我们的例子好像只给出了正数,我们来看一下负数的 Varints 编码情况。 1234int32 val = -1原码:1000 ... 0001 // 注意这里是 8 个字节补码:1111 ... 1111 // 注意这里是 8 个字节Varints 编码:1#1111111 ... 0#000 0001 (FF FF FF FF FF FF FF FF FF 01) 注意,因为负数必须在最高位(符号位)置 1,这一点意味着无论如何,负数都必须占用所有字节,所以它的补码总是占满 8 个字节。你没法像正数那样去掉多余的高位(都是 0)。再加上 msb,最终 Varints 编码的结果将固定在 10 个字节。 为什么是十个字节? int32 不应该是 4 个字节吗?这里是 ProtoBuf 基于兼容性的考虑(比如开发者将 int64 的字段改成 int32 后应当不影响旧程序),而将 int32 扩展成 int64 的八个字节。 为什么之前讲正数的时候没有这种扩展? 请仔细品味 Varints 编码,正数的前提下 int32 和 int64 天然兼容! 所以目前的情况是我们定义了一个 int32 类型的变量,如果将变量值设置为负数,那么直接采用 Varints 编码的话,其编码结果将总是占用十个字节,这显然不是我们希望得到的结果。如何解决? ZigZag 编码在上一节中我们提到了 Varints 编码对负数编码效率低的问题。 为解决这个问题,ProtoBuf 为我们提供了 sint32、sint64 两种类型,当你在使用这两种类型定义字段时,ProtoBuf 将使用 ZigZag 编码,而 ZigZag 编码将解决负数编码效率低的问题。 ZigZag 的原理和概念比我们想象的简单易懂,一句话就可概括介绍 ZigZag 编码:ZigZag 编码:有符号整数映射到无符号整数,然后再使用 Varints 编码 。 对于 ZigZag 编码的思维不难理解,既然负数的 Varints 编码效率很低,那么就将负数映射到正数,然后对映射后的正数进行 Varints 编码。解码时,解出正数之后再按映射关系映射回原来的负数。 例如我们设置 int32 val = -2。映射得到 3,那么对数字 3 进行 Varints 编码,将结果存储或发送出去。接收方接到数据后进行 Varints 解码,得到数字 3,再将 3 映射回 -2。 这里的“映射”是以移位实现的,并非存储映射表。 Varint 类型介绍了 Varints 编码和 ZigZag 编码之后,我们就可以继续深入分析每个类型的编码。 在第一节中我们提到了 wire_type 目前已定义 6 种,其中两种已被遗弃(Start group 和 End group),只剩下四种类型: Varint、64-bit、Length-delimited、32-bit。 接下来我们就来一个个详细分析,彻底搞明白 ProtoBuf 针对每种类型的编码策略。 注意,我们在之前已经强调过,与其它三种类型不同,Varint 类型里不止一种编码策略。 除了 int32、int64 等类型的 Varints 编码,还有 sint32、sint64 类型的 ZigZag 编码。 当我们使用 int32、int64、uint32、uint64、bool、enum 声明字段类型时,其字段值将使用之前介绍的 Varints 编码。 其中 bool 的本质为 0 和 1,enum 本质为整数常量。 在结合本文开头介绍的编码结构: Tag - [Length] - Value,这里的 Value 采用 Varints 编码,因此不需要 Length,则编码结构为 Tag - Value,其中 Tag 和 Value 均采用 Vartins 编码。 int32、int64、uint32、uint64来看一个最简单的 int32 的小例子: 123456syntax = "proto3";// message 定义message Example1 { int32 int32Val = 1;} 在程序中设置字段值为 1,其编码结果为: 12345// 设置字段值 为 1Example1 example1;example1.set_int32val(1);// 编码结果tag-(Varints)0#0001 000 + value-(Varints)0#000 0001 = 0x08 0x01 在程序中设置字段值为 666,其编码结果为: 12345// 设置字段值 为 666Example1 example1;example1.set_int32val(666);// 编码结果tag-(Varints)00001 000 + value-(Varints)1#0011010 0#000 0101 = 0x08 0x9a 0x05 在程序中设置字段值为 -1,其编码结果为: 12345// 设置字段值 为 1Example1 example1;example1.set_int32val(-1);// 编码结果tag-(Varints)00001 000 + value-(Varints)1#1111111 ... 0#000 0001 = 0x08 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x01 int64、uint32、uint64 与 int32 同理 boolbool 的例子: 123456syntax = "proto3";// message 定义message Example1 { bool boolVal = 1;} 在程序中设置字段值为 true,其编码结果为: 123456// 设置字段值 为 trueExample1 example1;example1.set_boolval(true);// 编码结果tag-(Varints)00001 000 + value-(Varints)0#000 0001 = 08 01 在程序中设置字段值为 true,其编码结果为: 123456// 设置字段值 为 falseExample1 example1;example1.set_boolval(false);// 编码结果空 这里有个有意思的现象,当 boolVal = false 时,其编码结果为空,为什么? 这里是 ProtoBuf 为了提高效率做的又一个小技巧:规定一个默认值机制,当读出来的字段为空的时候就设置字段的值为默认值。而 bool 类型的默认值为 false。也就是说将 false 编码然后传递(消耗一个字节),不如直接不输出任何编码结果(空),终端解析时发现该字段为空,它会按照规定设置其值为默认值(也就是 false)。如此,可进一步节省空间提高效率。 enumenum 的例子: 123456789101112131415syntax = "proto3";// message 定义message Example1 { enum COLOR { YELLOW = 0; RED = 1; BLACK = 1; WHITE = 1; BLUE = 1; } // 枚举常量必须在 32 位整型值的范围 // 使用 Varints 编码,对负数不够高效,因此不推荐在枚举中使用负数 COLOR colorVal = 1;} 在程序中设置字段值为 Example1_COLOR_BLUE,其编码结果为: 123456// 设置字段值 为 Example1_COLOR_BLUEExample1 example1;example1.set_colorval(Example1_COLOR_BLUE);// 编码结果tag-(Varints)00001 000 + value-(Varints)0#000 0100 = 08 04 sint32、sint64sint32、sint64 将采用 ZigZag 编码。编码结构依然为 Tag - Value,只不过在编码和解码的过程中多出一个映射的过程,映射后依然采用 Varints 编码。来看 sint32 的例子: 123456syntax = "proto3";// message 定义message Example1 { sint32 sint32Val = 1;} 在程序中设置字段值为 -1,其编码结果为: 123456// 设置字段值 为 -1Example1 example1;example1.set_colorval(-1);// 编码结果,1 映射回 -1 tag-(Varints)00001 000 + value-(Varints)0#000 0001 = 08 01 在程序中设置字段值为 -2,其编码结果为: 123456// 设置字段值 为 -2Example1 example1;example1.set_colorval(-2);// 编码结果,3 映射回 -2编码结果:tag-(Varints)00001 000 + value-(Varints)0#000 0011 = 08 03 sint64 与 sint32 同理。 int、uint 和 sint: 之所以同时出现了这三种类型,是因为历史和代码迭代的结果。ProtoBuf 最初只有 int 类型,由于 int 类型不适合负数(负数编码效率低),所以提供了 sint。因为 sint 的一部分正数其实是表达的负数,所以其正数范围有所减小,所以在一些全是正式场景下需要提供 uint 类型。 64-bit 和 32-bit 类型64-bit 和 32-bit 比较简单,与 Varints 一样其编码结构为 Tag-Value,不同的是不管数字大小,64-bit 存储 8 字节,32-bit 存储 4 字节。读取时同理,64-bit 直接读取 8 字节,32-bit 直接读取 4 字节。 为什么需要 64-bit 和 32-bit?之前已经分析过了 Varints 编码在一定范围内是有高效的,超过某一个数字占用字节反而更多,效率更低。如果现在有场景是存在大量的大数字,那么使用 Varints 就不要合适的,此时使用 64-bit 和 32-bit 更为合适。具体的,如果数值比 2^56 大的话,64-bit 这个类型比 uint64 高效,如果数值比 2^28 大的话,32-bit 这个类型比 uint32 高效。 fixed64、sfixed64、double来看例子: 12345678// message 定义syntax = "proto3";message Example1 { fixed64 fixed64Val = 1; sfixed64 sfixed64Val = 2; double doubleVal = 3;} 在程序中分别设置字段值 1、-1、1.2,其编码结果为: 123456789// 设置字段值 为 -2example1.set_fixed64val(1)example1.set_sfixed64val(-1)example1.set_doubleval(1.2)// 编码结果,总是 8 个字节09 # 01 00 00 00 00 00 00 0011 # FF FF FF FF FF FF FF FF (没有 ZigZag 编码)19 # 33 33 33 33 33 33 F3 3F fixed32、sfixed32、float与 64-bit 同理。 Length-delimited 类型string、bytes、EmbeddedMessage、repeated终于遇到了体现编码结构图中 [Length] 意义的类型了。Length-delimited 类型的编码结构为 Tag - Length - Value 。 这种编码方式很好理解,来看例子: 1234567891011121314syntax = "proto3";// message 定义message Example1 { string stringVal = 1; bytes bytesVal = 2; message EmbeddedMessage { int32 int32Val = 1; string stringVal = 2; } EmbeddedMessage embeddedExample1 = 3; repeated int32 repeatedInt32Val = 4; repeated string repeatedStringVal = 5;} 设置相应的值: 1234567891011121314Example1 example1;example1.set_stringval("hello,world");example1.set_bytesval("are you ok?");Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage();embeddedExample2->set_int32val(1);embeddedExample2->set_stringval("embeddedInfo");example1.set_allocated_embeddedexample1(embeddedExample2);example1.add_repeatedint32val(2);example1.add_repeatedint32val(3);example1.add_repeatedstringval("repeated1");example1.add_repeatedstringval("repeated2"); 最终编码的结果为: 123450A 0B 68 65 6C 6C 6F 2C 77 6F 72 6C 64 12 0B 61 72 65 20 79 6F 75 20 6F 6B 3F 1A 10 08 01 12 0C 65 6D 62 65 64 64 65 64 49 6E 66 6F 22 02 02 03[ proto3 默认 packed = true](编码结果打包处理,见下一小节的介绍)2A 09 72 65 70 65 61 74 65 64 31 2A 09 72 65 70 65 61 74 65 64 32(repeated string 为啥不进行默认 packed ?) 读者可对照上面介绍过的编码来理解这段相对复杂的编码结果。(为降低难度,已按字段分行,即第一个字段的编码结果对应第一行,第二个字段对应第二行…) 补充 packed 编码在 proto2 中为我们提供了可选的设置 [packed = true],而这一可选项在 proto3 中已成默认设置。 packed 目前只能用于 primitive 类型。 packed = true 主要使让 ProtoBuf 为我们把 repeated primitive 的编码结果打包,从而进一步压缩空间,进一步提高效率、速度。这里打包的含义其实就是:原先的 repeated 字段的编码结构为 Tag-Length-Value-Tag-Length-Value-Tag-Length-Value…,因为这些 Tag 都是相同的(同一字段),因此可以将这些字段的 Value 打包,即将编码结构变为 Tag-Length-Value-Value-Value… 上一节例子中 repeatedInt32Val 字段的编码结果为:22 | 02 02 03 22 即 00100010 -> wire_type = 2(Length-delimited), field_number = 4(repeatedInt32Val 字段),02 字节长度为 2,则读取两个字节,之后按照 Varints 解码出数字 2 和 3。]]></content>
<categories>
<category>编码</category>
</categories>
<tags>
<tag>编码</tag>
</tags>
</entry>
<entry>
<title><![CDATA[InnoDB中的AUTO_INCREMENT处理]]></title>
<url>%2F2019%2F10%2F24%2FInnoDB%E4%B8%AD%E7%9A%84AUTO-INCREMENT%E5%A4%84%E7%90%86%2F</url>
<content type="text"><![CDATA[网站和app并发性能取决于访问链路的每个环节,包括前端、流量路由、后台业务逻辑代码、中间件和数据库等。大部分环节都可以通过横向扩展来提高并发性能,数据库作为链路的末端要保持数据一致性等特点不像其他环节容易横向扩展,所以数据库性能尤为重要,特别是插入性能。 数据库设计通常会用一列与业务无关的自增长id作为主键(互联网业务数据库设计一般不会完全遵循数据库范式,如果有其他列值是随时间递增,也可以用该列做主键),提高写入效率和方便数据复制。而自增长id生成模式影响着数据插入性能。 InnoDB提供了一种可配置的锁定机制,可以显着提高SQL语句的可伸缩性和性能,从而为具有AUTO_INCREMENT列的表添加行 。要在InnoDB表使用AUTO_INCREMENT机制, AUTO_INCREMENT必须将列定义为索引的一部分,以便可以在表上执行等效的索引查找SELECT MAX(ai_col)以获取最大列值。通常,这是通过使列成为某些表索引的第一列来实现的。 本节介绍AUTO_INCREMENT用于生成自动增量值的锁定模式的行为 ,以及每种锁定模式如何影响复制。自增量锁定模式是在启动时使用 innodb_autoinc_lock_mode 参数配置。 以下术语用于描述 innodb_autoinc_lock_mode 设置: “INSERT-like” 语句:在表中生成新的行中的所有语句,包括 INSERT, INSERT … SELECT,REPLACE, REPLACE … SELECT和LOAD DATA。包括“简单插入”,“批量插入”和“混合模式 ”插入。 “简单插入”:可以预先确定要插入的行数的语句(最初处理语句时)。这包括没有嵌套子查询的单行和多行INSERT以及REPLACE语句,但不包括INSERT … ON DUPLICATE KEY UPDATE。 “批量插入”:预先不知道要插入的行数(以及所需的自动增量值的数量)的语句。这包括 INSERT … SELECT, REPLACE … SELECT和LOAD DATA声明,但不是简单的 INSERT。在处理每一行时,InnoDB为AUTO_INCREMENT列分配一个新值。 “混合模式插入”:指定一些(但不是全部)新增行的自动增量值的“简单插入”语句。举个例子 INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d'); 其中c1是 ti表的AUTO_INCREMENT表。 另一种类型的“混合模式插入”是INSERT … ON DUPLICATE KEY UPDATE,在最坏的情况下实际上是INSERT 后跟了一个UPDATE操作,其中AUTO_INCREMENT列在UPDATE阶段期间可能使用或不使用分配值 。innodb_autoinc_lock_mode 配置参数有三种可能的设置 。对于“传统”,“连续”或 “交错”锁定模式,设置分别为0,1或2。从MySQL 8.0开始,交错锁定模式(innodb_autoinc_lock_mode=2)是默认设置。在MySQL 8.0之前,连续锁定模式是默认值(innodb_autoinc_lock_mode=1)。 MySQL 8.0中的交错锁定模式的默认设置反映了从基于语句的复制到基于行的复制的更改,作为默认复制类型。基于语句的复制需要连续的自动增量锁定模式,以确保为给定的SQL语句序列以可预测和可重复的顺序分配自动增量值,而基于行的复制对SQL语句的执行顺序不敏感。 innodb_autoinc_lock_mode = 0 (“传统”锁定模式)传统的锁定模式提供了与在MySQL 5.1中引入innodb_autoinc_lock_mode配置参数之前相同的行为 。由于语义可能存在差异,传统的锁定模式选项用于向后兼容,性能测试以及解决“混合模式插入”问题。 在此锁定模式下,所有“INSERT-like”语句都会获得一个特殊的表级AUTO-INC 锁,以便插入带有 AUTO_INCREMENT列的表中。此锁通常保持在语句的末尾(而不是事务的结尾),以确保为给定的INSERT语句序列以可预测和可重复的顺序分配自动增量值,并确保自动增量值由任何给定的声明分配是连续的。 对于基于语句的复制,这意味着在从服务器上复制SQL语句时,自动增量列使用的值与主服务器上的值相同。执行多个INSERT语句的结果是确定性的,并且从服务器再现与主服务器上相同的数据。如果多个INSERT语句生成的自动递增值是交错的,则两个并发INSERT语句的结果将是不确定的,并且无法使用基于语句的复制可靠地传播到从服务器。 为清楚起见,请考虑使用此表的示例: 12345CREATE TABLE t1 (c1 INT(11) NOT NULL AUTO_INCREMENT,c2 VARCHAR(10) DEFAULT NULL,PRIMARY KEY (c1)) ENGINE=InnoDB; 假设有两个事务正在运行,每个事务都将行插入带有AUTO_INCREMENT列的表中 。一个事务使用INSERT … SELECT插入1000行的语句,另一个事务使用 插入一行的简单 INSERT语句: 123Tx1: INSERT INTO t1 (c2) SELECT 1000 rows from another table ... Tx2: INSERT INTO t1 (c2) VALUES ('xxx'); InnoDB事先无法判断从Tx1 SELECT中的INSERT语句中检索了多少行 ,并且随着语句的进行,它会一次分配一个自动增量值。使用保持在语句末尾的表级锁定,一次只能执行一个 INSERT引用表t1的语句,并且不会交错生成不同语句的自动增量数。由Tx1 INSERT … SELECT语句生成的自动递增值是连续的,并且用于Tx2中的INSERT 语句的(单个)自动递增值要小于或大于用于Tx1的语句,具体取决于首先执行的语句。 只要SQL语句在从二进制日志重放时(在使用基于语句的复制时或在恢复方案中)以相同的顺序执行,结果与首次运行Tx1和Tx2时的结果相同。因此,表级锁定一直持续到语句结束,使用自动增量的INSERT语句可以安全地用于基于语句的复制。但是,当多个事务同时执行insert语句时,这些表级锁限制了并发性和可伸缩性。 在前面的示例中,如果没有表级锁定,则用于Tx2的INSERT中的自动增量列的值取决于语句执行的时间。如果Tx2的INSERT在Tx1的INSERT运行时执行(而不是在启动之前或完成之后),则由两个INSERT语句分配的特定自动增量值是不确定的,并且可能因运行而异。 在连续锁定模式下,InnoDB可以避免将表级AUTO-INC锁定用于预先知道行数的“简单插入”语句,并且仍然保留基于语句的复制的确定性执行和安全性。 如果不使用二进制日志作为恢复或复制的一部分重放SQL语句,则 交错 锁定模式可用于消除表级AUTO-INC锁的所有使用,从而 实现更高的并发性和性能,但代价是允许自动存在空白 – 由语句分配的增量编号,可能具有由并发执行的语句交错分配的编号。 innodb_autoinc_lock_mode = 1 (“连续”锁定模式)在此模式下,“批量插入”使用特殊的 AUTO-INC表级锁定并保持它直到语句结束。这适用于所有 INSERT … SELECT, REPLACE … SELECT和LOAD DATA语句。只有一个持有AUTO-INC锁的语句可以一次执行。如果批量插入操作的源表与目标表不同,则AUTO-INC在从源表中选择的第一行上执行共享锁之后,将对目标表执行锁定。如果批量插入操作的源和目标是同一个表,则AUTO-INC 在对所有选定的行执行共享锁定后执行锁定。 “简单插入”(预先知道要插入的行数)通过在互斥锁(轻量级锁定)的控制下获得所需数量的自动增量值在分配过程的持续时间内保持,来避免表级锁AUTO-INC锁定直到语句完成。除非另一个事务持有AUTO-INC锁,否则不使用表级AUTO-INC锁 。如果另一个事务持有AUTO-INC锁,则“简单插入”等待AUTO-INC锁定,就像它是“批量插入”。 此锁定模式确保在存在未提前知道行数的INSERT语句(以及在语句进行时指定自动增量编号)的情况下,由任何“INSERT-like” 语句指定的所有自动增量值都是连续,并且操作对于基于语句的复制是安全的。 简而言之,这种锁定模式显着提高了可伸缩性,同时可以安全地使用基于语句的复制。此外,与“传统” 锁定模式一样,由任何给定语句分配的自动递增数字是连续的。对于任何使用自动增量的语句,与“传统”模式相比,语义没有变化,但有一个重要的例外。 “混合模式插入”的例外情况是,用户为多行“简单插入”中的某些行(但不是所有行)提供AUTO_INCREMENT列的显式值。 对于此类插入,InnoDB会分配比要插入的行数更多的自动增量值。 但是,自动分配的所有值都是连续生成(因此高于)最近执行的先前语句生成的自动增量值。 “超额”号码丢失了。 innodb_autoinc_lock_mode = 2 (“交错”锁定模式)在此锁定模式下, 没有“INSERT-like”语句使用表级AUTO-INC锁 ,并且多个语句可以同时执行。 这是最快且最具扩展性的锁定模式,但在从二进制日志重放SQL语句时使用基于语句的复制或恢复方案时,这是不安全的。 在这种锁定模式下,自动增量值保证是唯一的,并且在所有同时执行的“INSERT-like”语句中单调递增。 但是,因为多个语句可以同时生成数字(即,数字的分配在语句之间交错),所以为任何给定语句插入的行生成的值可能不是连续的。 如果执行的唯一语句是“简单插入”,其中要插入的行数是提前知道的,则除了“混合模式插入”之外,单个语句生成的数字没有间断 。但是,当执行“批量插入”时,任何给定语句分配的自动增量值可能存在间断。 总结MySQL 插入语句分三类:简单插入(Simple inserts)、批量插入(Bulk inserts)和混合插入(Mixed-mode inserts),这三种插入在innodb_autoinc_lock_mode取不同值时,生成的自增长值和插入性能会不一样。]]></content>
<categories>
<category>数据库</category>
</categories>
<tags>
<tag>数据库</tag>
<tag>InnoDB</tag>
</tags>
</entry>
<entry>
<title><![CDATA[浅谈ZGC]]></title>
<url>%2F2019%2F10%2F21%2F%E6%B5%85%E8%B0%88ZGC%2F</url>
<content type="text"><![CDATA[ZGC is a new garbage collector recently open-sourced by Oracle for the OpenJDK. It was mainly written by Per Liden. ZGC is similar to Shenandoah or Azul’s C4 that focus on reducing pause-times while still compacting the heap. Although I won’t give a full introduction here, “compacting the heap” just means moving the still-alive objects to the start (or some other region) of the heap. This helps to reduce fragmentation but usually this also means that the whole application (that includes all of its threads) needs to be halted while the GC does its magic, this is usually referred to as stopping the world. Only when the GC is finished, the application can be resumed. In GC literature the application is often called mutator, since from the GC’s point of view the application mutates the heap. Depending on the size of the heap such a pause could take several seconds, which could be quite problematic for interactive applications. There are several ways to reduce pause times: The GC can employ multiple threads while compacting (parallel compaction). Compaction work can also be split across multiple pauses (incremental compaction). Compact the heap concurrently to the running application without stopping it (or just for a short time) (concurrent compaction). No compaction of the heap at all (an approach taken by e.g. Go’s GC). ZGC uses concurrent compaction to keep pauses to a minimum, this is certainly not obvious to implement so I want to describe how this works. Why is this complicated? You need to copy an object to another memory address, at the same time another thread could read from or write into the old object. If copying succeeded there might still be arbitrary many references somewhere in the heap to the old object address that need to be updated to the new address. I should also mention that although concurrent compaction seems to be the best solution to reduce pause time of the alternatives given above, there are definitely some tradeoffs involved. So if you don’t care about pause times, you might be better off using a GC that focuses on throughput instead. GC barriersThe key to understanding how ZGC does concurrent compaction is the load barrier (often called read barrier in GC literature). Although I have an own section about ZGC’s load-barrier, I want to give a short overview since not all readers might be familiar with them. If a GC has load-barriers, the GC needs to do some additional action when reading a reference from the heap. Basically in Java this happens every time you see some code like obj.field. A GC could also need a write/store-barrier for operations like obj.field = value. Both operations are special since they read from or write into the heap. The names are a bit confusing, but GC barriers are different from memory barriers used in CPUs or compilers. Both reading and writing in the heap is extremely common, so both GC-barriers need to be super efficient. That means just a few assembly instructions in the common case. Read barriers are an order of magnitude more likely than write-barriers (although this can certainly vary depending on the application), so read-barriers are even more performance-sensitive. Generational GC’s for example usually get by with just a write barrier, no read barrier needed. ZGC needs a read barrier but no write barrier. For concurrent compaction I haven’t seen a solution without read barriers. Another factor to consider: Even if a GC needs some type of barrier, they might “only” be required when reading or writing references in the heap. Reading or writing primitives like int or double might not require the barrier. Reference coloringThe key to understanding ZGC is reference coloring. ZGC stores additional metadata in heap references. On x64 a reference is 64-bit wide (ZGC doesn’t support compressed oops or class pointers at the moment), but today’s hardware actually limits a reference to 48-bit for virtual memory addresses. Although to be exact only 47-bit, since bit 47 determines the value of bits 48-63 (for our purpose those bits are always 0). ZGC reserves the first 42-bits for the actual address of the object (referenced to as offset in the source code). 42-bit addresses give you a theoretical heap limitation of 4TB in ZGC. The remaining bits are used for these flags: finalizable, remapped, marked1 and marked0 (one bit is reserved for future use). There is a really nice ASCII drawing in ZGC’s source that shows all these bits: 12345678910111213141516 6 4 4 4 4 4 0 3 7 6 5 2 1 0+-------------------+-+----+-----------------------------------------------+|00000000 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 11111111|+-------------------+-+----+-----------------------------------------------+| | | || | | * 41-0 Object Offset (42-bits, 4TB address space)| | || | * 45-42 Metadata Bits (4-bits) 0001 = Marked0| | 0010 = Marked1| | 0100 = Remapped| | 1000 = Finalizable| || * 46-46 Unused (1-bit, always zero)|* 63-47 Fixed (17-bits, always zero) Having metadata information in heap references does make dereferencing more expensive, since the address needs to be masked to get the real address (without metainformation). ZGC employs a nice trick to avoid this: When reading from memory exactly one bit of marked0, marked1 or remapped is set. When allocating a page at offset x, ZGC maps the same page to 3 different address: for marked0: (0b0001 << 42) | x for marked1: (0b0010 << 42) | x for remapped: (0b0100 << 42) | x ZGC therefore just reserves 16TB of address space (but not actually uses all of this memory) starting at address 4TB. Here is another nice drawing from ZGC’s source: 123456789+--------------------------------+ 0x0000140000000000 (20TB)| Remapped View |+--------------------------------+ 0x0000100000000000 (16TB)| (Reserved, but unused) |+--------------------------------+ 0x00000c0000000000 (12TB)| Marked1 View |+--------------------------------+ 0x0000080000000000 (8TB)| Marked0 View |+--------------------------------+ 0x0000040000000000 (4TB) At any point of time only one of these 3 views is in use. So for debugging the unused views can be unmapped to better verify correctness. Pages & Physical & Virtual MemoryShenandoah separates the heap into a large number of equally-sized regions. An object usually does not span multiple regions, except for large objects that do not fit into a single region. Those large objects need to be allocated in multiple contiguous regions. I quite like this approach because it is so simple. ZGC is quite similar to Shenandoah in this regard. In ZGC’s parlance regions are called pages. The major difference to Shenandoah: Pages in ZGC can have different sizes (but always a multiple of 2MB on x64). There are 3 different page types in ZGC: small (2MB size), medium (32MB size) and large (some multiple of 2MB). Small objects (up to 256KB size) are allocated in small pages, medium-sized objects (up to 4MB) are allocated in medium pages. Objects larger than 4MB are allocated in large pages. Large pages can only store exactly one object, in constrast to small or medium pages. Somewhat confusingly large pages can actually be smaller than medium pages (e.g. for a large object with a size of 6MB). Another nice property of ZGC is, that it also differentiates between physical and virtual memory. The idea behind this is that there usually is plenty of virtual memory available (always 4TB in ZGC) while physical memory is more scarce. Physical memory can be expanded up to the maximum heap size (set with -Xmx for the JVM), so this tends to be much less than the 4 TB of virtual memory. Allocating a page of a certain size in ZGC means allocating both physical and virtual memory. With ZGC the physical memory doesn’t need to be contiguous - only the virtual memory space. So why is this actually a nice property? Allocating a contiguous range of virtual memory should be easy, since we usually have more than enough of it. But it is quite easy to imagine a situation where we have 3 free pages with size 2MB somewhere in the physical memory, but we need 6MB of contiguous memory for a large object allocation. There is enough free physical memory but unfortunately this memory is non-contiguous. ZGC is able to map this non-contiguous physical pages to a single contiguous virtual memory space. If this wasn’t possible, we would have run out of memory. On Linux the physical memory is basically an anonymous file that is only stored in RAM (and not on disk), ZGC uses memfd_create to create it. The file can then be extended with ftruncate, ZGC is allowed to extend the physical memory (= the anonymous file) up to the maximum heap size. Physical memory is then mmaped into the virtual address space. Marking & Relocating objectsA collection is split into two major phases: marking & relocating. (Actually there are more than those two phases but see the source for more details). A GC cycle starts with the marking phase, which marks all reachable objects. At the end of this phase we know which objects are still alive and which are garbage. ZGC stores this information in the so called live map for each page. A live map is a bitmap that stores whether the object at the given index is strongly-reachable and/or final-reachable (for objects with a finalize-method). During the marking-phase the load-barrier in application-threads pushes unmarked references into a thread-local marking buffer. As soon as this buffer is full, the GC threads can take ownership of this buffer and recursively traverse all reachable objects from this buffer. Marking in an application thread just pushes the reference into a buffer, the GC threads are responsible for walking the object graph and updating the live map. After marking ZGC needs to relocate all live objects in the relocation set. The relocation set is a set of pages, that were chosen to be evacuated based on some criteria after marking (e.g. those page with the most amount of garbage). An object is either relocated by a GC thread or an application thread (again through the load-barrier). ZGC allocates a forwarding table for each page in the relocation set. The forwarding table is basically a hash map that stores the address an object has been relocated to (if the object has already been relocated). The advantage with ZGC’s approach is that we only need to allocate space for the forwarding pointer for pages in the relocation set. Shenandoah in comparison stores the forwarding pointer in the object itself for each and every object, which has some memory overhead. The GC threads walk over the live objects in the relocation set and relocate all those objects that haven’t been relocated yet. It could even happen that an application thread and a GC thread try to relocate the same object at the same time, in this case the first thread to relocate the object wins. ZGC uses an atomic CAS-operation to determine a winner. While not marking the load-barrier relocates or remaps all references loaded from the heap. That ensure that every new reference the mutator sees, already points to the newest copy of an object. Remapping an object means looking up the new object address in the forwarding table. The relocation phase is finished as soon as the GC threads are finished walking the relocation set. Although that means all objects have been relocated, there will generally still be references into the relocation set, that need to be remapped to their new addresses. These reference will then be healed by trapping load-barriers or if this doesn’t happen soon enough by the next marking cycle. That means marking also needs to inspect the forward table to remap (but not relocate - all objects are guaranteed to be relocated) objects to their new addresses. This also explains why there are two marking bits (marked0 and marked1) in an object reference. The marking phase alternates between the marked0 and marked1 bit. After the relocation phase there may still be references that haven’t been remapped and thus have still the bit from the last marking cycle set. If the new marking phase would use the same marking bit, the load-barrier would detect this reference as already marked. Load-BarrierZGC needs a so called load-barrier (also referred to as read-barrier) when reading a reference from the heap. We need to insert this load-barrier each time the Java program accesses a field of object type, e.g. obj.field. Accessing fields of some other primitive type do not need a barrier, e.g. obj.anInt or obj.anDouble. ZGC doesn’t need store/write-barriers for obj.field = someValue. Depending on the stage the GC is currently in (stored in the global variable ZGlobalPhase), the barrier either marks the object or relocates it if the reference isn’t already marked or remapped. The global variables ZAddressGoodMask and ZAddressBadMask store the mask that determines if a reference is already considered good (that means already marked or remapped/relocated) or if there is still some action necessary. These variables are only changed at the start of marking- and relocation-phase and both at the same time. This table from ZGC’s source gives a nice overview in which state these masks can be: 12345 GoodMask BadMask WeakGoodMask WeakBadMask --------------------------------------------------------------Marked0 001 110 101 010Marked1 010 101 110 001Remapped 100 011 100 011 Assembly code for the barrier can be seen in the MacroAssembler for x64, I will only show some pseudo assembly code for this barrier: 12345mov rax, [r10 + some_field_offset]test rax, [address of ZAddressBadMask]jnz load_barrier_mark_or_relocate# otherwise reference in rax is considered good The first assembly instruction reads a reference from the heap: r10 stores the object reference and some_field_offset is some constant field offset. The loaded reference is stored in the rax register. This reference is then tested (this is just an bitwise-and) against the current bad mask. Synchronization isn’t necessary here since ZAddressBadMask only gets updated when the world is stopped. If the result is non-zero, we need to execute the barrier. The barrier needs to either mark or relocate the object depending on which GC phase we are currently in. After this action it needs to update the reference stored in r10 + some_field_offset with the good reference. This is necessary such that subsequent loads from this field return a good reference. Since we might need to update the reference-address, we need to use two registers r10 and rax for the loaded reference and the objects address. The good reference also needs to be stored into register rax, such that execution can continue just as when we would have loaded a good reference. Since every single reference needs to be marked or relocated, throughput is likely to decrease right after starting a marking- or relocation-phase. This should get better quite fast when most references are healed. Stop-the-World PausesZGC doesn’t get rid of stop-the-world pauses completely. The collector needs pauses when starting marking, ending marking and starting relocation. But this pauses are usually quite short - only a few milliseconds. When starting marking ZGC traverses all thread stacks to mark the applications root set. The root set is the set of object references from where traversing the object graph starts. It usually consists of local and global variables, but also other internal VM structures (e.g. JNI handles). Another pause is required when ending the marking phase. In this pause the GC needs to empty and traverse all thread-local marking buffers. Since the GC could discover a large unmarked sub-graph this could take longer. ZGC tries to avoid this by stopping the end of marking phase after 1 millisecond. It returns into the concurrent marking phase until the whole graph is traversed, then the end of marking phase can be started again. Starting relocation phase pauses the application again. This phase is quite similar to starting marking, with the difference that this phase relocates the objects in the root set. ConclusionI hope I could give a short introduction into ZGC. I certainly couldn’t describe every detail about this GC in a single blog post. If you need more information, ZGC is open-source, so it is possible to study the whole implementation.]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[短网址生成系统]]></title>
<url>%2F2019%2F10%2F08%2F%E7%9F%AD%E7%BD%91%E5%9D%80%E7%94%9F%E6%88%90%E7%B3%BB%E7%BB%9F%2F</url>
<content type="text"><![CDATA[概述过长的网址不利于传播,特别是对于微博和 Twitter 等有发文长度限制的网站,短网址生成系统(TinyURL)可以将一个网址变短。在浏览器中输入短网址之后,TinyURL 会将该短网址转换成原始网址并进行重定向。 最烂的回答实现一个算法,将长地址转成短地址。实现长和短一一对应。然后再实现它的逆运算,将短地址还能换算回长地址。 这个回答看起来挺完美的,但是稍微有点计算机或者信息论常识的人就能发现,这个算法就跟永动机一样,是永远不可能找到的。即使我们定义短地址是100位。那么它的变化是62的100次方。62=10数字+26大写字母+26小写字母。无论这个数多么大,他也不可能大过世界上可能存在的长地址。所以实现一一对应,本身就是不可能的。 再换一个说法来反驳,如果真有这么一个算法和逆运算,那么基本上现在的压缩软件都可以歇菜了,而世界上所有的信息,都可以压缩到100个字符。这可能吗? 另一个很烂的回答和上面一样,也找一个算法,把长地址转成短地址,但是不存在逆运算。我们需要把短对长的关系存到DB中,在通过短查长时,需要查DB。怎么说呢,没有改变本质,如果真有这么一个算法,那必然是会出现碰撞的,也就是多个长地址转成了同一个短地址。因为我们无法预知会输入什么样的长地址到这个系统中,所以不可能实现这样一个绝对不碰撞的hash函数。 比较烂的回答那我们用一个hash算法,我承认它会碰撞,碰撞后我再在后面加1,2,3不就行了。 ok,这样的话,当通过这个hash算法算出来之后,可能我们会需要做btree式的大于小于或者like查找到能知道现在应该在后面加1,2,或3,这个也可能由于输入的长地址集的不确定性。导致生成短地址时间的不确定性。同样烂的回答还有随机生成一个短地址,去查找是否用过,用过就再随机,如此往复,直到随机到一个没用过的短地址。 正确的回答正确的原理就是通过发号策略,给每一个过来的长地址,发一个号即可,小型系统直接用mysql的自增索引就搞定了。如果是大型应用,可以考虑各种分布式key-value系统做发号器。不停的自增就行了。第一个使用这个服务的人得到的短地址是 http://xx.xx/0 第二个是 http://xx.xx/1 第11个是 http://xx.xx/a 第依次往后,相当于实现了一个62进制的自增字段即可。 下面是几个子问题 1、 62进制如何用数据库或者KV存储来做?其实我们并不需要在存储中用62进制,用10进制就好了。比如第10000个长地址,我们给它的短地址对应的编号是9999,我们通过存储自增拿到9999后,再做一个10进制到62进制的转换,转成62进制数即可。这个10~62进制转换,你完全都可以自己实现。 2、 如何保证同一个长地址,每次转出来都是一样的短地址上面的发号原理中,是不判断长地址是否已经转过的。也就是说用拿着百度首页地址来转,我给一个 http://xx.xx/abc 过一段时间你再来转,我还会给你一个 http://xx.xx/xyz。这看起来挺不好的,但是不好在哪里呢?不好在不是一一对应,而一长对多短。这与我们完美主义的基因不符合,那么除此以外还有什么不对的地方? 有人说它浪费空间,这是对的。同一个长地址,产生多条短地址记录,这明显是浪费空间的。那么我们如何避免空间浪费,有人非常迅速的回答我,建立一个长对短的KV存储即可。嗯,听起来有理,但是,这个KV存储本身就是浪费大量空间。所以我们是在用空间换空间,而且貌似是在用大空间换小空间。真的划算吗?这个问题要考虑一下。当然,也不是没有办法解决,我们做不到真正的一一对应,那么打个折扣是不是可以搞定?这个问题的答案太多种,各有各招,我这就不说了。 3、如何保证发号器的大并发高可用上面设计看起来有一个单点,那就是发号器。如果做成分布式的,那么多节点要保持同步加1,多点同时写入,这个嘛,以CAP理论看,是不可能真正做到的。其实这个问题的解决非常简单,我们可以退一步考虑,我们是否可以实现两个发号器,一个发单号,一个发双号,这样就变单点为多点了?依次类推,我们可以实现1000个逻辑发号器,分别发尾号为0到999的号。每发一个号,每个发号器加1000,而不是加1。这些发号器独立工作,互不干扰即可。而且在实现上,也可以先是逻辑的,真的压力变大了,再拆分成独立的物理机器单元。1000个节点,估计对人类来说应该够用了。如果你真的还想更多,理论上也是可以的。 4、具体存储如何选择这个问题就不展开说了,各有各道,主要考察一下对存储的理解。对缓存原理的理解,和对市面上DB、Cache系统可用性,并发能力,一致性等方面的理解。 5. 跳转用301还是302301是永久重定向,302是临时重定向。短地址一经生成就不会变化,所以用301是符合http语义的。同时对服务器压力也会有一定减少。 但是如果使用了301,我们就无法统计到短地址被点击的次数了。而这个点击次数是一个非常有意思的大数据分析数据源。能够分析出的东西非常非常多。所以选择302虽然会增加服务器压力,但是我想是一个更好的选择。]]></content>
<categories>
<category>算法</category>
</categories>
<tags>
<tag>算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[微信红包的架构设计简介]]></title>
<url>%2F2019%2F09%2F04%2F%E5%BE%AE%E4%BF%A1%E7%BA%A2%E5%8C%85%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E7%AE%80%E4%BB%8B%2F</url>
<content type="text"><![CDATA[背景有某个朋友在朋友圈咨询微信红包的架构。 概况2014年微信红包使用数据库硬抗整个流量,2015年使用cache抗流量。 问题微信的金额什么时候算?微信金额是拆的时候实时算出来,不是预先分配的,采用的是纯内存计算,不需要预算空间存储。 采取实时计算金额的考虑:预算需要占存储,实时效率很高,预算才效率低。 实时性:为什么明明抢到红包,点开后发现没有?2014年的红包一点开就知道金额,分两次操作,先抢到金额,然后再转账。 2015年的红包的拆和抢是分离的,需要点两次,因此会出现抢到红包了,但点开后告知红包已经被领完的状况。进入到第一个页面不代表抢到,只表示当时红包还有。 分配:红包里的金额怎么算?为什么出现各个红包金额相差很大?随机,额度在0.01和剩余平均值*2之间。 例如:发100块钱,总共10个红包,那么平均值是10块钱一个,那么发出来的红包的额度在0.01元~20元之间波动。 当前面3个红包总共被领了40块钱时,剩下60块钱,总共7个红包,那么这7个红包的额度在:0.01~(60/7*2)=17.14之间。 注意:这里的算法是每被抢一个后,剩下的会再次执行上面的这样的算法。 这样算下去,会超过最开始的全部金额,因此到了最后面如果不够这么算,那么会采取如下算法:保证剩余用户能拿到最低1分钱即可。 如果前面的人手气不好,那么后面的余额越多,红包额度也就越多,因此实际概率一样的。 红包的设计微信从财付通拉取金额数据过来,生成个数/红包类型/金额放到redis集群里,app端将红包ID的请求放入请求队列中,如果发现超过红包的个数,直接返回。根据红包的裸祭处理成功得到令牌请求,则由财付通进行一致性调用,通过像比特币一样,两边保存交易记录,交易后交给第三方服务审计,如果交易过程中出现不一致就强制回归。 并发性处理:红包如何计算被抢完?cache会抵抗无效请求,将无效的请求过滤掉,实际进入到后台的量不大。cache记录红包个数,原子操作进行个数递减,到0表示被抢光。财付通按照20万笔每秒入账准备,但实际还不到8万每秒。 如何保持8w每秒的写入?多主sharding,水平扩展机器。 数据容量多少?一个红包只占一条记录,有效期只有几天,因此不需要太多空间。 轮询红包分配,压力大不?抢到红包的人数和红包都在一条cache记录上,没有太大的查询压力。 一个红包一个队列?没有队列,一个红包一条数据,数据上有一个计数器字段。 有没有从数据上证明每个红包的概率是不是均等?不是绝对均等,就是一个简单的拍脑袋算法。 拍脑袋算法,会不会出现两个最佳?答:会出现金额一样的,但是手气最佳只有一个,先抢到的那个最佳。 每领一个红包就更新数据么?答:每抢到一个红包,就cas更新剩余金额和红包个数。 红包如何入库入账?数据库会累加已经领取的个数与金额,插入一条领取记录。入账则是后台异步操作。 入帐出错怎么办?比如红包个数没了,但余额还有?答:最后会有一个take all操作。另外还有一个对账来保障。]]></content>
<categories>
<category>算法</category>
</categories>
<tags>
<tag>算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于Redis的分布式锁的安全性]]></title>
<url>%2F2019%2F08%2F22%2F%E5%9F%BA%E4%BA%8ERedis%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E7%9A%84%E5%AE%89%E5%85%A8%E6%80%A7%2F</url>
<content type="text"><![CDATA[实际上,关于Redis分布式锁的安全性问题,在分布式系统专家Martin Kleppmann和Redis的作者antirez之间就发生过一场争论。由于对这个问题一直以来比较关注,这场争论的大概过程是这样的:为了规范各家对基于Redis的分布式锁的实现,Redis的作者提出了一个更安全的实现,叫做Redlock。有一天,Martin Kleppmann写了一篇blog,分析了Redlock在安全性上存在的一些问题。然后Redis的作者立即写了一篇blog来反驳Martin的分析。但Martin表示仍然坚持原来的观点。随后,这个问题在Twitter和Hacker News上引发了激烈的讨论,很多分布式系统的专家都参与其中。 对于那些对分布式系统感兴趣的人来说,这个事件非常值得关注。不管你是刚接触分布式系统的新手,还是有着多年分布式开发经验的老手,读完这些分析和评论之后,大概都会有所收获。要知道,亲手实现过Redis Cluster这样一个复杂系统的antirez,足以算得上分布式领域的一名专家了。但对于由分布式锁引发的一系列问题的分析中,不同的专家却能得出迥异的结论,从中我们可以窥见分布式系统相关的问题具有何等的复杂性。实际上,在分布式系统的设计中经常发生的事情是:许多想法初看起来毫无破绽,而一旦详加考量,却发现不是那么天衣无缝。 Redlock算法就像本文开头所讲的,借助Redis来实现一个分布式锁(Distributed Lock)的做法,已经有很多人尝试过。人们构建这样的分布式锁的目的,是为了对一些共享资源进行互斥访问。 但是,这些实现虽然思路大体相近,但实现细节上各不相同,它们能提供的安全性和可用性也不尽相同。所以,Redis的作者antirez给出了一个更好的实现,称为Redlock,算是Redis官方对于实现分布式锁的指导规范。Redlock的算法描述就放在Redis的官网上。在Redlock之前,很多人对于分布式锁的实现都是基于单个Redis节点的。而Redlock是基于多个Redis节点(都是Master)的一种实现。为了能理解Redlock,我们首先需要把简单的基于单Redis节点的算法描述清楚,因为它是Redlock的基础。 基于单Redis节点的分布式锁首先,Redis客户端为了获取锁,向Redis节点发送如下命令: SET resource_name my_random_value NX PX 30000 上面的命令如果执行成功,则客户端成功获取到了锁,接下来就可以访问共享资源了;而如果上面的命令执行失败,则说明获取锁失败。 注意,在上面的SET命令中: my_random_value是由客户端生成的一个随机字符串,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。 NX表示只有当resource_name对应的key值不存在的时候才能SET成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。 PX 30000表示这个锁有一个30秒的自动过期时间。当然,这里30秒只是一个例子,客户端可以选择合适的过期时间。 最后,当客户端完成了对共享资源的操作之后,执行下面的Redis Lua脚本来释放锁: 123456if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1])else return 0end 这段Lua脚本在执行的时候要把前面的my_random_value作为ARGV[1]的值传进去,把resource_name作为KEYS[1]的值传进去。 至此,基于单Redis节点的分布式锁的算法就描述完了。这里面有好几个问题需要重点分析一下。 首先第一个问题,这个锁必须要设置一个过期时间。否则的话,当一个客户端获取锁成功之后,假如它崩溃了,或者由于发生了网络分割(network partition)导致它再也无法和Redis节点通信了,那么它就会一直持有这个锁,而其它客户端永远无法获得锁了。antirez在后面的分析中也特别强调了这一点,而且把这个过期时间称为锁的有效时间(lock validity time)。获得锁的客户端必须在这个时间之内完成对共享资源的访问。 第二个问题,第一步获取锁的操作,网上不少文章把它实现成了两个Redis命令: 12SETNX resource_name my_random_valueEXPIRE resource_name 30 虽然这两个命令和前面算法描述中的一个SET命令执行效果相同,但却不是原子的。如果客户端在执行完SETNX后崩溃了,那么就没有机会执行EXPIRE了,导致它一直持有这个锁。 第三个问题,也是antirez指出的,设置一个随机字符串my_random_value是很有必要的,它保证了一个客户端释放的锁必须是自己持有的那个锁。假如获取锁时SET的不是一个随机字符串,而是一个固定值,那么可能会发生下面的执行序列: 客户端1获取锁成功。 客户端1在某个操作上阻塞了很长时间。 过期时间到了,锁自动释放了。 客户端2获取到了对应同一个资源的锁。 客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。 之后,客户端2在访问共享资源的时候,就没有锁为它提供保护了。 第四个问题,释放锁的操作必须使用Lua脚本来实现。释放锁其实包含三步操作:’GET’、判断和’DEL’,用Lua脚本来实现能保证这三步的原子性。否则,如果把这三步操作放到客户端逻辑中去执行的话,就有可能发生与前面第三个问题类似的执行序列: 客户端1获取锁成功。 客户端1访问共享资源。 客户端1为了释放锁,先执行’GET’操作获取随机字符串的值。 客户端1判断随机字符串的值,与预期的值相等。 客户端1由于某个原因阻塞住了很长时间。 过期时间到了,锁自动释放了。 客户端2获取到了对应同一个资源的锁。 客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。 实际上,在上述第三个问题和第四个问题的分析中,如果不是客户端阻塞住了,而是出现了大的网络延迟,也有可能导致类似的执行序列发生。 前面的四个问题,只要实现分布式锁的时候加以注意,就都能够被正确处理。但除此之外,antirez还指出了一个问题,是由failover引起的,却是基于单Redis节点的分布式锁无法解决的。正是这个问题催生了Redlock的出现。 这个问题是这样的。假如Redis节点宕机了,那么所有客户端就都无法获得锁了,服务变得不可用。为了提高可用性,我们可以给这个Redis节点挂一个Slave,当Master节点不可用的时候,系统自动切到Slave上(failover)。但由于Redis的主从复制(replication)是异步的,这可能导致在failover过程中丧失锁的安全性。考虑下面的执行序列 客户端1从Master获取了锁。 Master宕机了,存储锁的key还没有来得及同步到Slave上。 Slave升级为Master。 客户端2从新的Master获取到了对应同一个资源的锁。 于是,客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。针对这个问题,antirez设计了Redlock算法,我们接下来会讨论。 其它疑问 1前面这个算法中出现的锁的有效时间(lock validity time),设置成多少合适呢?如果设置太短的话,锁就有可能在客户端完成对于共享资源的访问之前过期,从而失去保护;如果设置太长的话,一旦某个持有锁的客户端释放锁失败,那么就会导致所有其它客户端都无法获取锁,从而长时间内无法正常工作。看来真是个两难的问题。 而且,在前面对于随机字符串my_random_value的分析中,antirez也在文章中承认的确应该考虑客户端长期阻塞导致锁过期的情况。如果真的发生了这种情况,那么共享资源是不是已经失去了保护呢?antirez重新设计的Redlock是否能解决这些问题呢? 分布式锁Redlock由于前面介绍的基于单Redis节点的分布式锁在failover的时候会产生解决不了的安全性问题,因此antirez提出了新的分布式锁的算法Redlock,它基于N个完全独立的Redis节点(通常情况下N可以设置成5)。 运行Redlock算法的客户端依次执行下面各个步骤,来完成获取锁的操作: 获取当前时间(毫秒数)。 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。 由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作,因此理论上它的可用性更高。我们前面讨论的单Redis节点的分布式锁在failover的时候锁失效的问题,在Redlock中不存在了,但如果有节点发生崩溃重启,还是会对锁的安全性有影响的。具体的影响程度跟Redis对数据的持久化程度有关。 假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列: 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。 节点C重启后,客户端2锁住了C, D, E,获取锁成功。 这样,客户端1和客户端2同时获得了锁(针对同一资源)。 在默认情况下,Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync),因此最坏情况下可能丢失1秒的数据。为了尽可能不丢数据,Redis允许设置成每次修改数据都进行fsync,但这会降低性能。当然,即使执行了fsync也仍然有可能丢失数据(这取决于系统而不是Redis的实现)。所以,上面分析的由于节点重启引发的锁失效问题,总是有可能出现的。为了应对这一问题,antirez又提出了 延迟重启(delayed restarts) 的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。 关于Redlock还有一点细节值得拿出来分析一下:在最后释放锁的时候,antirez在算法描述中特别强调,客户端应该向所有Redis节点发起释放锁的操作。也就是说,即使当时向某个节点获取锁没有成功,在释放锁的时候也不应该漏掉这个节点。这是为什么呢?设想这样一种情况,客户端发给某个Redis节点的获取锁的请求成功到达了该Redis节点,这个节点也成功执行了SET操作,但是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。因此,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。实际上,这种情况在异步通信模型中是有可能发生的:客户端向服务器通信是正常的,但反方向却是有问题的。 其它疑问 2前面在讨论单Redis节点的分布式锁的时候,最后我们提出了一个疑问,如果客户端长期阻塞导致锁过期,那么它接下来访问共享资源就不安全了(没有了锁的保护)。这个问题在Redlock中是否有所改善呢?显然,这样的问题在Redlock中是依然存在的。 另外,在算法第4步成功获取了锁之后,如果由于获取锁的过程消耗了较长时间,重新计算出来的剩余的锁有效时间很短了,那么我们还来得及去完成共享资源访问吗?如果我们认为太短,是不是应该立即进行锁的释放操作?那到底多短才算呢?又是一个选择难题。 Martin的分析Martin Kleppmann在2016-02-08这一天发表了一篇blog,名字叫 How to do distributed locking Martin在这篇文章中谈及了分布式系统的很多基础性的问题(特别是分布式计算的异步模型),对分布式系统的从业者来说非常值得一读。这篇文章大体可以分为两大部分: 前半部分,与Redlock无关。Martin指出,即使我们拥有一个完美实现的分布式锁(带自动过期功能),在没有共享资源参与进来提供某种fencing机制的前提下,我们仍然不可能获得足够的安全性。 后半部分,是对Redlock本身的批评。Martin指出,由于Redlock本质上是建立在一个同步模型之上,对系统的记时假设(timing assumption)有很强的要求,因此本身的安全性是不够的。 首先我们讨论一下前半部分的关键点。Martin给出了下面这样一份时序图: 在上面的时序图中,假设锁服务本身是没有问题的,它总是能保证任一时刻最多只有一个客户端获得锁。上图中出现的lease这个词可以暂且认为就等同于一个带有自动过期功能的锁。客户端1在获得锁之后发生了很长时间的GC pause,在此期间,它获得的锁过期了,而客户端2获得了锁。当客户端1从GC pause中恢复过来的时候,它不知道自己持有的锁已经过期了,它依然向共享资源(上图中是一个存储服务)发起了写数据请求,而这时锁实际上被客户端2持有,因此两个客户端的写请求就有可能冲突(锁的互斥作用失效了)。 初看上去,有人可能会说,既然客户端1从GC pause中恢复过来以后不知道自己持有的锁已经过期了,那么它可以在访问共享资源之前先判断一下锁是否过期。但仔细想想,这丝毫也没有帮助。因为GC pause可能发生在任意时刻,也许恰好在判断完之后。 也有人会说,如果客户端使用没有GC的语言来实现,是不是就没有这个问题呢?Martin指出,系统环境太复杂,仍然有很多原因导致进程的pause,比如虚存造成的缺页故障(page fault),再比如CPU资源的竞争。即使不考虑进程pause的情况,网络延迟也仍然会造成类似的结果。 总结起来就是说,即使锁服务本身是没有问题的,而仅仅是客户端有长时间的pause或网络延迟,仍然会造成两个客户端同时访问共享资源的冲突情况发生。而这种情况其实就是我们在前面已经提出来的“客户端长期阻塞导致锁过期”的那个疑问。 那怎么解决这个问题呢?Martin给出了一种方法,称为fencing token。fencing token是一个单调递增的数字,当客户端成功获取锁的时候它随同锁一起返回给客户端。而客户端访问共享资源的时候带着这个fencing token,这样提供共享资源的服务就能根据它进行检查,拒绝掉延迟到来的访问请求(避免了冲突)。如下图: 在上图中,客户端1先获取到的锁,因此有一个较小的fencing token,等于33,而客户端2后获取到的锁,有一个较大的fencing token,等于34。客户端1从GC pause中恢复过来之后,依然是向存储服务发送访问请求,但是带了fencing token = 33。存储服务发现它之前已经处理过34的请求,所以会拒绝掉这次33的请求。这样就避免了冲突。 现在我们再讨论一下Martin的文章的后半部分。 Martin在文中构造了一些事件序列,能够让Redlock失效(两个客户端同时持有锁)。为了说明Redlock对系统记时(timing)的过分依赖,他首先给出了下面的一个例子(还是假设有5个Redis节点A, B, C, D, E): 客户端1从Redis节点A, B, C成功获取了锁(多数节点)。由于网络问题,与D和E通信失败。 节点C上的时钟发生了向前跳跃,导致它上面维护的锁快速过期。 客户端2从Redis节点C, D, E成功获取了同一个资源的锁(多数节点)。 客户端1和客户端2现在都认为自己持有了锁。 上面这种情况之所以有可能发生,本质上是因为Redlock的安全性(safety property)对系统的时钟有比较强的依赖,一旦系统的时钟变得不准确,算法的安全性也就保证不了了。Martin在这里其实是要指出分布式算法研究中的一些基础性问题,或者说一些常识问题,即好的分布式算法应该基于异步模型(asynchronous model),算法的安全性不应该依赖于任何记时假设(timing assumption)。在异步模型中:进程可能pause任意长的时间,消息可能在网络中延迟任意长的时间,甚至丢失,系统时钟也可能以任意方式出错。一个好的分布式算法,这些因素不应该影响它的安全性(safety property),只可能影响到它的活性(liveness property),也就是说,即使在非常极端的情况下(比如系统时钟严重错误),算法顶多是不能在有限的时间内给出结果而已,而不应该给出错误的结果。这样的算法在现实中是存在的,像比较著名的Paxos,或Raft。但显然按这个标准的话,Redlock的安全性级别是达不到的。 随后,Martin觉得前面这个时钟跳跃的例子还不够,又给出了一个由客户端GC pause引发Redlock失效的例子。如下: 客户端1向Redis节点A, B, C, D, E发起锁请求。 各个Redis节点已经把请求结果返回给了客户端1,但客户端1在收到请求结果之前进入了长时间的GC pause。 在所有的Redis节点上,锁过期了。 客户端2在A, B, C, D, E上获取到了锁。 客户端1从GC pause从恢复,收到了前面第2步来自各个Redis节点的请求结果。客户端1认为自己成功获取到了锁。 客户端1和客户端2现在都认为自己持有了锁。 Martin给出的这个例子其实有点小问题。在Redlock算法中,客户端在完成向各个Redis节点的获取锁的请求之后,会计算这个过程消耗的时间,然后检查是不是超过了锁的有效时间(lock validity time)。也就是上面的例子中第5步,客户端1从GC pause中恢复过来以后,它会通过这个检查发现锁已经过期了,不会再认为自己成功获取到锁了。随后antirez在他的反驳文章中就指出来了这个问题,但Martin认为这个细节对Redlock整体的安全性没有本质的影响。 抛开这个细节,我们可以分析一下Martin举这个例子的意图在哪。初看起来,这个例子跟文章前半部分分析通用的分布式锁时给出的GC pause的时序图是基本一样的,只不过那里的GC pause发生在客户端1获得了锁之后,而这里的GC pause发生在客户端1获得锁之前。但两个例子的侧重点不太一样。Martin构造这里的这个例子,是为了强调在一个分布式的异步环境下,长时间的GC pause或消息延迟(上面这个例子中,把GC pause换成Redis节点和客户端1之间的消息延迟,逻辑不变),会让客户端获得一个已经过期的锁。从客户端1的角度看,Redlock的安全性被打破了,因为客户端1收到锁的时候,这个锁已经失效了,而Redlock同时还把这个锁分配给了客户端2。换句话说,Redis服务器在把锁分发给客户端的途中,锁就过期了,但又没有有效的机制让客户端明确知道这个问题。而在之前的那个例子中,客户端1收到锁的时候锁还是有效的,锁服务本身的安全性可以认为没有被打破,后面虽然也出了问题,但问题是出在客户端1和共享资源服务器之间的交互上。 在Martin的这篇文章中,还有一个很有见地的观点,就是对锁的用途的区分。他把锁的用途分为两种: 为了效率(efficiency),协调各个客户端避免做重复的工作。即使锁偶尔失效了,只是可能把某些操作多做一遍而已,不会产生其它的不良后果。比如重复发送了一封同样的email。 为了正确性(correctness)。在任何情况下都不允许锁失效的情况发生,因为一旦发生,就可能意味着数据不一致(inconsistency),数据丢失,文件损坏,或者其它严重的问题。 最后,Martin得出了如下的结论: 如果是为了效率(efficiency)而使用分布式锁,允许锁的偶尔失效,那么使用单Redis节点的锁方案就足够了,简单而且效率高。Redlock则是个过重的实现(heavyweight)。 如果是为了正确性(correctness)在很严肃的场合使用分布式锁,那么不要使用Redlock。它不是建立在异步模型上的一个足够强的算法,它对于系统模型的假设中包含很多危险的成分(对于timing)。而且,它没有一个机制能够提供fencing token。那应该使用什么技术呢?Martin认为,应该考虑类似Zookeeper的方案,或者支持事务的数据库。 Martin对Redlock算法的形容是: neither fish nor fowl (非驴非马) 其它疑问 3 Martin提出的fencing token的方案,需要对提供共享资源的服务进行修改,这在现实中可行吗? 根据Martin的说法,看起来,如果资源服务器实现了fencing token,它在分布式锁失效的情况下也仍然能保持资源的互斥访问。这是不是意味着分布式锁根本没有存在的意义了? 资源服务器需要检查fencing token的大小,如果提供资源访问的服务也是包含多个节点的(分布式的),那么这里怎么检查才能保证fencing token在多个节点上是递增的呢? Martin对于fencing token的举例中,两个fencing token到达资源服务器的顺序颠倒了(小的fencing token后到了),这时资源服务器检查出了这一问题。如果客户端1和客户端2都发生了GC pause,两个 fencing token都延迟了,它们几乎同时达到了资源服务器,但保持了顺序,那么资源服务器是不是就检查不出问题了?这时对于资源的访问是不是就发生冲突了? 分布式锁+fencing的方案是绝对正确的吗?能证明吗? antirez的反驳Martin在发表了那篇分析分布式锁的blog (How to do distributed locking)之后,该文章在Twitter和Hacker News上引发了广泛的讨论。但人们更想听到的是Redlock的作者antirez对此会发表什么样的看法。 Martin的那篇文章是在 2016-02-08 这一天发表的,但据Martin说,他在公开发表文章的一星期之前就把草稿发给了antirez进行review,而且他们之间通过email进行了讨论。不知道Martin有没有意料到,antirez对于此事的反应很快,就在Martin的文章发表出来的第二天,antirez就在他的博客上贴出了他对于此事的反驳文章,名字叫 Is Redlock safe?。 这是高手之间的过招。antirez这篇文章也条例非常清晰,并且中间涉及到大量的细节。antirez认为,Martin的文章对于Redlock的批评可以概括为两个方面(与Martin文章的前后两部分对应): 带有自动过期功能的分布式锁,必须提供某种fencing机制来保证对共享资源的真正的互斥保护。Redlock提供不了这样一种机制。 Redlock构建在一个不够安全的系统模型之上。它对于系统的记时假设(timing assumption)有比较强的要求,而这些要求在现实的系统中是无法保证的。 antirez对这两方面分别进行了反驳。 首先,关于fencing机制。antirez对于Martin的这种论证方式提出了质疑:既然在锁失效的情况下已经存在一种fencing机制能继续保持资源的互斥访问了,那为什么还要使用一个分布式锁并且还要求它提供那么强的安全性保证呢?即使退一步讲,Redlock虽然提供不了Martin所讲的递增的fencing token,但利用Redlock产生的随机字符串( my_random_value )可以达到同样的效果。这个随机字符串虽然不是递增的,但却是唯一的,可以称之为unique token。antirez举了个例子,比如,你可以用它来实现“Check and Set”操作。 然后,antirez的反驳就集中在第二个方面上:关于算法在记时(timing)方面的模型假设。在我们前面分析Martin的文章时也提到过,Martin认为Redlock会失效的情况主要有三种: 时钟发生跳跃。 长时间的GC pause。 长时间的网络延迟。 antirez肯定意识到了这三种情况对Redlock最致命的其实是第一点:时钟发生跳跃。这种情况一旦发生,Redlock是没法正常工作的。而对于后两种情况来说,Redlock在当初设计的时候已经考虑到了,对它们引起的后果有一定的免疫力。所以,antirez接下来集中精力来说明通过恰当的运维,完全可以避免时钟发生大的跳动,而Redlock对于时钟的要求在现实系统中是完全可以满足的。 Martin在提到时钟跳跃的时候,举了两个可能造成时钟跳跃的具体例子: 系统管理员手动修改了时钟。 从NTP服务收到了一个大的时钟更新事件。 antirez反驳说: 手动修改时钟这种人为原因,不要那么做就是了。否则的话,如果有人手动修改Raft协议的持久化日志,那么就算是Raft协议它也没法正常工作了。 使用一个不会进行“跳跃”式调整系统时钟的ntpd程序(可能是通过恰当的配置),对于时钟的修改通过多次微小的调整来完成。 而Redlock对时钟的要求,并不需要完全精确,它只需要时钟差不多精确就可以了。比如,要记时5秒,但可能实际记了4.5秒,然后又记了5.5秒,有一定的误差。不过只要误差不超过一定范围,这对Redlock不会产生影响。antirez认为呢,像这样对时钟精度并不是很高的要求,在实际环境中是完全合理的。 好了,到此为止,如果你相信antirez这里关于时钟的论断,那么接下来antirez的分析就基本上顺理成章了。 关于Martin提到的能使Redlock失效的后两种情况,Martin在分析的时候恰好犯了一个错误(在本文上半部分已经提到过)。在Martin给出的那个由客户端GC pause引发Redlock失效的例子中,这个GC pause引发的后果相当于在锁服务器和客户端之间发生了长时间的消息延迟。Redlock对于这个情况是能处理的。回想一下Redlock算法的具体过程,它使用起来的过程大体可以分成5步: 获取当前时间。 完成 获取锁 的整个过程(与N个Redis节点交互)。 再次获取当前时间。 把两个时间相减,计算 获取锁 的过程是否消耗了太长时间,导致锁已经过期了。如果没过期,客户端持有锁去访问共享资源。 在Martin举的例子中,GC pause或网络延迟,实际发生在上述第1步和第3步之间。而不管在第1步和第3步之间由于什么原因(进程停顿或网络延迟等)导致了大的延迟出现,在第4步都能被检查出来,不会让客户端拿到一个它认为有效而实际却已经过期的锁。当然,这个检查依赖系统时钟没有大的跳跃。这也就是为什么antirez在前面要对时钟条件进行辩护的原因。 有人会说,在第3步之后,仍然可能会发生延迟啊。没错,antirez承认这一点,他对此有一段很有意思的论证,原话如下: The delay can only happen after steps 3, resulting into the lock to be considered ok while actually expired, that is, we are back at the first problem Martin identified of distributed locks where the client fails to stop working to the shared resource before the lock validity expires. Let me tell again how this problem is common with all the distributed locks implementations , and how the token as a solution is both unrealistic and can be used with Redlock as well.译文:延迟只能发生在第3步之后,这导致锁被认为是有效的而实际上已经过期了,也就是说,我们回到了Martin指出的第一个问题上,客户端没能够在锁的有效性过期之前完成与共享资源的交互。让我再次申明一下,这个问题对于 所有的分布式锁的实现 是普遍存在的,而且基于token的这种解决方案是不切实际的,但也能和Redlock一起用。 这里antirez所说的“Martin指出的第一个问题”具体是什么呢?在本文上半部分我们提到过,Martin的文章分为两大部分,其中前半部分与Redlock没有直接关系,而是指出了任何一种带自动过期功能的分布式锁在没有提供fencing机制的前提下都有可能失效。这里antirez所说的就是指的Martin的文章的前半部分。换句话说,对于大延迟给Redlock带来的影响,恰好与Martin在文章的前半部分针对所有的分布式锁所做的分析是一致的,而这种影响不单单针对Redlock。Redlock的实现已经保证了它是和其它任何分布式锁的安全性是一样的。当然,与其它“更完美”的分布式锁相比,Redlock似乎提供不了Martin提出的那种递增的token,但antirez在前面已经分析过了,关于token的这种论证方式本身就是“不切实际”的,或者退一步讲,Redlock能提供的unique token也能够提供完全一样的效果。 以上就是antirez在blog中所说的主要内容。有一些点值得我们注意一下: antirez是同意大的系统时钟跳跃会造成Redlock失效的。在这一点上,他与Martin的观点的不同在于,他认为在实际系统中是可以避免大的时钟跳跃的。当然,这取决于基础设施和运维方式。 antirez在设计Redlock的时候,是充分考虑了网络延迟和程序停顿所带来的影响的。但是,对于客户端和资源服务器之间的延迟(即发生在算法第3步之后的延迟),antirez是承认所有的分布式锁的实现,包括Redlock,是没有什么好办法来应对的。 讨论进行到这,Martin和antirez之间谁对谁错其实并不是那么重要了。只要我们能够对Redlock(或者其它分布式锁)所能提供的安全性的程度有充分的了解,那么我们就能做出自己的选择了。 基于ZooKeeper的分布式锁更安全吗?很多人(也包括Martin在内)都认为,如果你想构建一个更安全的分布式锁,那么应该使用ZooKeeper,而不是Redis。那么,为了对比的目的,让我们先暂时脱离开本文的题目,讨论一下基于ZooKeeper的分布式锁能提供绝对的安全吗?它需要fencing token机制的保护吗? 我们不得不提一下分布式专家所写的一篇blog,题目叫 Note on fencing and distributed locks Flavio Junqueira是ZooKeeper的作者之一,他的这篇blog就写在Martin和antirez发生争论的那几天。他在文中给出了一个基于ZooKeeper构建分布式锁的描述(当然这不是唯一的方式): 客户端尝试创建一个znode节点,比如 /lock 。那么第一个客户端就创建成功了,相当于拿到了锁;而其它的客户端会创建失败(znode已存在),获取锁失败。 持有锁的客户端访问共享资源完成后,将znode删掉,这样其它客户端接下来就能来获取锁了。 znode应该被创建成ephemeral的。这是znode的一个特性,它保证如果创建znode的那个客户端崩溃了,那么相应的znode会被自动删除。这保证了锁一定会被释放。 看起来这个锁相当完美,没有Redlock过期时间的问题,而且能在需要的时候让锁自动释放。但仔细考察的话,并不尽然。 ZooKeeper是怎么检测出某个客户端已经崩溃了呢?实际上,每个客户端都与ZooKeeper的某台服务器维护着一个Session,这个Session依赖定期的心跳(heartbeat)来维持。如果ZooKeeper长时间收不到客户端的心跳(这个时间称为Sesion的过期时间),那么它就认为Session过期了,通过这个Session所创建的所有的ephemeral类型的znode节点都会被自动删除。 设想如下的执行序列: 客户端1创建了znode节点 /lock ,获得了锁。 客户端1进入了长时间的GC pause。 客户端1连接到ZooKeeper的Session过期了。znode节点 /lock 被自动删除。 客户端2创建了znode节点 /lock ,从而获得了锁。 客户端1从GC pause中恢复过来,它仍然认为自己持有锁。 最后,客户端1和客户端2都认为自己持有了锁,冲突了。这与之前Martin在文章中描述的由于GC pause导致的分布式锁失效的情况类似。 看起来,用ZooKeeper实现的分布式锁也不一定就是安全的。该有的问题它还是有。但是,ZooKeeper作为一个专门为分布式应用提供方案的框架,它提供了一些非常好的特性,是Redis之类的方案所没有的。像前面提到的ephemeral类型的znode自动删除的功能就是一个例子。 还有一个很有用的特性是ZooKeeper的watch机制。这个机制可以这样来使用,比如当客户端试图创建 /lock 的时候,发现它已经存在了,这时候创建失败,但客户端不一定就此对外宣告获取锁失败。客户端可以进入一种等待状态,等待当 /lock 节点被删除的时候,ZooKeeper通过watch机制通知它,这样它就可以继续完成创建操作(获取锁)。这可以让分布式锁在客户端用起来就像一个本地的锁一样:加锁失败就阻塞住,直到获取到锁为止。这样的特性Redlock就无法实现。 小结一下,基于ZooKeeper的锁和基于Redis的锁相比在实现特性上有两个不同: 在正常情况下,客户端可以持有锁任意长的时间,这可以确保它做完所有需要的资源访问操作之后再释放锁。这避免了基于Redis的锁对于有效时间(lock validity time)到底设置多长的两难问题。实际上,基于ZooKeeper的锁是依靠Session(心跳)来维持锁的持有状态的,而Redis不支持Sesion。 基于ZooKeeper的锁支持在获取锁失败之后等待锁重新释放的事件。这让客户端对锁的使用更加灵活。 顺便提一下,如上所述的基于ZooKeeper的分布式锁的实现,并不是最优的。它会引发“herd effect”(羊群效应),降低获取锁的性能。这里是一个更好的实现 。 我们重新回到Flavio Junqueira对于fencing token的分析。Flavio Junqueira指出,fencing token机制本质上是要求客户端在每次访问一个共享资源的时候,在执行任何操作之前,先对资源进行某种形式的“标记”(mark)操作,这个“标记”能保证持有旧的锁的客户端请求(如果延迟到达了)无法操作资源。这种标记操作可以是很多形式,fencing token是其中比较典型的一个。 随后Flavio Junqueira提到用递增的epoch number(相当于Martin的fencing token)来保护共享资源。而对于分布式的资源,为了方便讨论,假设分布式资源是一个小型的多备份的数据存储(a small replicated data store),执行写操作的时候需要向所有节点上写数据。最简单的做标记的方式,就是在对资源进行任何操作之前,先把epoch number标记到各个资源节点上去。这样,各个节点就保证了旧的(也就是小的)epoch number无法操作数据。 当然,这里再展开讨论下去可能就涉及到了这个数据存储服务的实现细节了。比如在实际系统中,可能为了容错,只要上面讲的标记和写入操作在多数节点上完成就算成功完成了(Flavio Junqueira并没有展开去讲)。在这里我们能看到的,最重要的,是这种标记操作如何起作用的方式。这有点类似于Paxos协议(Paxos协议要求每个proposal对应一个递增的数字,执行accept请求之前先执行prepare请求)。antirez提出的random token的方式显然不符合Flavio Junqueira对于“标记”操作的定义,因为它无法区分新的token和旧的token。只有递增的数字才能确保最终收敛到最新的操作结果上。 在这个分布式数据存储服务(共享资源)的例子中,客户端在标记完成之后执行写入操作的时候,存储服务的节点需要判断epoch number是不是最新,然后确定能不能执行写入操作。如果按照上一节我们的分析思路,这里的epoch判断和接下来的写入操作,是不是在一个原子操作里呢?根据Flavio Junqueira的相关描述,我们相信,应该是原子的。那么既然资源本身可以提供原子互斥操作了,那么分布式锁还有存在的意义吗?应该说有。客户端可以利用分布式锁有效地避免冲突,等待写入机会,这对于包含多个节点的分布式资源尤其有用(当然,是出于效率的原因)。 关于时钟在Martin与antirez的这场争论中,冲突最为严重的就是对于系统时钟的假设是不是合理的问题。Martin认为系统时钟难免会发生跳跃(这与分布式算法的异步模型相符),而antirez认为在实际中系统时钟可以保证不发生大的跳跃。 Martin对于这一分歧发表了如下看法: So, fundamentally, this discussion boils down to whether it is reasonable to make timing assumptions for ensuring safety properties. I say no, Salvatore says yes — but that’s ok. Engineering discussions rarely have one right answer.译文: 从根本上来说,这场讨论最后归结到了一个问题上:为了确保安全性而做出的记时假设到底是否合理。我认为不合理,而antirez认为合理 —— 但是这也没关系。工程问题的讨论很少只有一个正确答案。 那么,在实际系统中,时钟到底是否可信呢?对此,有人专门写了一篇文章,TIL: clock skew exists,总结了很多跟时钟偏移有关的实际资料,并进行了分析。文章最后得出的结论是:clock skew is real (时钟偏移在现实中是存在的) 。 总结最后,我相信,这个讨论还远没有结束。分布式锁(Distributed Locks)和相应的fencing方案,可以作为一个长期的课题,随着我们对分布式系统的认识逐渐增加,可以再来慢慢地思考它。思考它更深层的本质,以及它在理论上的证明。]]></content>
<categories>
<category>分布式</category>
</categories>
<tags>
<tag>分布式</tag>
<tag>Redis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[蓄水池采样]]></title>
<url>%2F2019%2F08%2F15%2F%E8%93%84%E6%B0%B4%E6%B1%A0%E9%87%87%E6%A0%B7%2F</url>
<content type="text"><![CDATA[问题描述给定一个无限的数据流,要求随机取出 k 个数。也就是说当数据流有 N 个数据时,不论 N 为多少,每个数被取出的概率都为 k/N 算法 先取出前 k 个数; 从第 k+1 开始,以 k/i 的概率取出这个数,并随机替换掉之前已经取出的 k 个数中的一个。 123456Init a reservoir with the size kfor i = k + 1 to N M = random(1, i); if (M < k) SWAP the Mth value and ith valueend for 证明使用归纳法进行证明,其中 i 表示当前到来的数据编号。 当 i==k+1 时,该数以 k/(k+1) 被取出。当该数被取出时,需要替换前 k 个数中的某个数,被替换的概率为 k/(k+1) · 1/k = 1/(k+1),这个数被保留的概率即为 1 - 被替换的概率 = 1 - 1/(k+1) = k/(k+1)。 假设 i==p 时符合条件,即前 p 个数都以 k/p的概率被取出。当 i==p+1 时,该数被取出概率为 k/(p+1)。对于前 p 个数,该数被取出要符合两个条件:之前被取出、没有被第 p+1 的数替代。没有被第 p+1 的数替代的概率 = 1 - 被第 p+1 的数替换。因此总概率为 k/p ·(1- k/(p+1)·1/k) = k/ (p+1) 。 综上所述,得证。]]></content>
</entry>
<entry>
<title><![CDATA[分布式事务实现方案]]></title>
<url>%2F2019%2F08%2F07%2F%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E5%AE%9E%E7%8E%B0%E6%96%B9%E6%A1%88%2F</url>
<content type="text"><![CDATA[分布式事务的实现主要有以下 5 种方案。 XA 方案所谓的 XA 方案,即:两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。 这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。如果要玩儿,那么基于 Spring + JTA 就可以搞定,自己随便搜个 demo 看看就知道了。 这个方案,我们很少用,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。我可以给大家介绍一下, 现在微服务,一个大的系统分成几十个甚至几百个服务。一般来说,每个服务只能操作自己对应的一个数据库。 如果你要操作别的服务对应的库,不允许直连别的服务的库,违反微服务架构的规范,你随便交叉胡乱访问,几百个服务的话,全体乱套,这样的一套服务是没法管理的,没法治理的,可能会出现数据被别人改错,自己的库被别人写挂等情况。 如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许交叉访问别人的数据库。 TCC 方案TCC 的全称是:Try、Confirm、Cancel。 Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留。 Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作。 Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)。 这种方案说实话几乎很少人使用,一般来说跟钱相关的,跟钱打交道的,支付、交易相关的场景,会用 TCC,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。而且最好是你的各个业务执行的时间都比较短。但是说实话,自己手写回滚逻辑,或者是补偿逻辑,实在太恶心了,那个业务代码是很难维护的。 本地消息表本地消息表。 A 系统在自己本地一个事务里操作同时,插入一条数据到消息表; 接着 A 系统将这个消息发送到 MQ 中去; B 系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息; B 系统执行成功之后,就会更新自己本地消息表的状态以及 A 系统消息表的状态; 如果 B 系统处理失败了,那么就不会更新消息表状态,那么此时 A 系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到 MQ 中去,让 B 再次处理; 这个方案保证了最终一致性,哪怕 B 事务失败了,但是 A 会不断重发消息,直到 B 那边成功为止。 这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务,如果是高并发场景咋办呢?咋扩展呢?所以一般确实很少用。 可靠消息最终一致性方案可靠消息最终一致性方案。 这个的意思,就是干脆不要用本地的消息表了,直接基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。 A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作别执行了; 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消息,如果失败就告诉 mq 回滚消息; 如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务; mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。 这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。 最大努力通知方案最大努力通知方案。 系统 A 本地事务执行完之后,发送个消息到 MQ; 这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口; 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。]]></content>
<categories>
<category>分布式</category>
</categories>
<tags>
<tag>分布式</tag>
<tag>事务</tag>
</tags>
</entry>
<entry>
<title><![CDATA[分布式ID生成算法-SnowFlake]]></title>
<url>%2F2019%2F07%2F05%2F%E5%88%86%E5%B8%83%E5%BC%8FID%E7%94%9F%E6%88%90%E7%AE%97%E6%B3%95-SnowFlake%2F</url>
<content type="text"><![CDATA[概述分布式id生成算法的有很多种,Twitter的SnowFlake就是其中经典的一种。SnowFlake算法生成id的结果是一个64bit大小的整数,它的结构如下图 结构 1位,不用。二进制中最高位为1的都是负数,但是我们生成的id一般都使用整数,所以这个最高位固定是0 41位,用来记录时间戳(毫秒)。 41位可以表示 2^(41)-1 个数字, 如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 $2^{41}-1$,减1是因为可表示的数值范围是从0开始算的,而不是1。 也就是说41位可以表示 2^(41)-1 个毫秒的值,转化成单位年则是 (2^(41)-1) / (1000 60 60 24 365) = 69年 10位,用来记录工作机器id。 可以部署在 2^(10) = 1024 个节点,包括5位 datacenterId 和5位 workerId 5位(bit)可以表示的最大正整数是 2^(5)-1 = 31,即可以用0、1、2、3、….31这32个数字,来表示不同的 datecenterId 或 workerId 12位,序列号,用来记录同毫秒内产生的不同id。 12位(bit)可以表示的最大正整数是 2^(12)-1 = 4095,即可以用0、1、2、3、….4095这4096个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号 由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的。 SnowFlake可以保证: 所有生成的id按时间趋势递增 整个分布式系统内不会产生重复id(因为有datacenterId和workerId来做区分)]]></content>
<categories>
<category>分布式</category>
</categories>
<tags>
<tag>算法</tag>
<tag>分布式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[浅谈ZAB协议]]></title>
<url>%2F2019%2F06%2F28%2F%E6%B5%85%E8%B0%88ZAB%E5%8D%8F%E8%AE%AE%2F</url>
<content type="text"><![CDATA[众所周知,ZooKeeper 是一个开源的分布式协调服务,很多分布式的应用都是基于 ZooKeeper 来实现分布式锁、服务管理、通知订阅等功能。 那么 ZooKeeper 自身是如何在分布式环境下实现数据的一致性的呢? 答案就是ZooKeeper Atomic Broadcast(ZAB,zookeeper原子消息广播协议)。 谈ZAB之前我们先聊一聊 2PC。 2PC两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。 运行过程 准备阶段:协调者询问参与者事务是否执行成功,参与者发回事务执行结果。 提交阶段:如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。 需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。 问题 同步阻塞:所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。 单点问题:协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待,无法完成其它操作。 数据不一致:在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。 太过保守:任意一个节点失败就会导致整个事务失败,没有完善的容错机制。 ZAB现在再来看看ZAB是怎么解决上面的问题的。在我看来,ZAB就是 2PC变种 + 崩溃恢复,其中 崩溃恢复是解决协调者崩溃后发生的问题。 消息广播ZAB协议与二阶段提交协议不同的是,ZAB协议在二阶段提交过程中,移除了中断逻辑。 ZAB协议在有过半的Follower服务器已经反馈Ack之后就开始提交Proposal了,而不需要等待集群中所有Follower服务器都反馈响应。 关于ZAB在Leader出现单点宕机如果保证事务提交,保证数据一致性,则引入崩溃恢复模式来解决这个问题。 ZAB的消息广播协议是基于具有FIFO(先进先出)特性的TCP协议来进行网络通信,保证消息广播过程中消息的接收与发送的顺序性。 在整个消息广播过程中,Leader服务器会为每一个事务请求处理步骤: Leader服务器会为事务请求生成一个全局的的递增事务ID(即ZXID),保证每个消息的因果关系的顺序。 Leader服务器会为该事务生成对应的Proposal,进行广播。 Leader服务器会为每一个Follower服务器都各自分配一个单独的队列,让后将需要广播的事务Proposal依次放入这些队列中去,并根据FIFO策略进行消息发送。 每一个Follower服务器在接收到这个事务Proposal之后,首先以日志形式写入本地磁盘,并且成功写入后反馈给Leader服务器一个Ack响应 当Leader服务器接收超过半数的Follower的Ack响应,Leader自身也会完成对事务的提交。同时就会广播一个Commit消息给所有的Follower服务器以通知进行事务提交。每一个Follower服务器在接收到Commit消息后,也会完成对事务的提交。 崩溃恢复确保当Leader出现单点问题,在新选举出Leader后,保证数据一致性。 保证一致性的关键在于下面两点: ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交。 ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务。 对于1来说,只需要通过选举找到当前集群中持有事务ID最大的那台机器,其他的服务器同步这台机器即可。 对于2来说, Zab 通过巧妙的设计 zxid 来实现这一目的。一个 zxid 是64位,高 32 是纪元(epoch)编号,每经过一次 leader 选举产生一个新的 leader,新 leader 会将 epoch 号 +1。低 32 位是消息计数器,每接收到一条消息这个值 +1,新 leader 选举后这个值重置为 0。这样设计的好处是旧的 leader 挂了后重启,它不会立即被重新选举为 leader,因为此时它的 zxid 肯定小于当前的新 leader。当旧的 leader 作为 follower 接入新的 leader 后,新的 leader 会让它将所有的拥有旧的 epoch 号的未被 COMMIT 的 proposal 清除。]]></content>
<categories>
<category>分布式</category>
</categories>
<tags>
<tag>分布式</tag>
<tag>事务</tag>
<tag>ZooKeeper</tag>
</tags>
</entry>
<entry>
<title><![CDATA[clrpc-简单的RPC工具]]></title>
<url>%2F2019%2F06%2F17%2Fclrpc-%E7%AE%80%E5%8D%95%E7%9A%84RPC%E5%B7%A5%E5%85%B7%2F</url>
<content type="text"><![CDATA[这是一个基于 Java 、 由 Netty 负责传输 、默认使用 Protostuff 负责编解码的简单的RPC(远程过程调用)工具。 服务提供者将服务发布注册到 注册中心 ZooKeeper 上后,服务消费者请求 注册中心 ZooKeeper 查找订阅服务后与服务提供者通信调用服务( 支持 同步服务 和 异步服务 )。 Setup当前阶段均为 SNAPSHOT 版本,暂时不提供依赖配置。 你可以使用命令 git clone git@github.com:CongLinDev/clrpc.git 克隆到本地进行使用。 UsageDefine Service And Implement it12345678910111213141516171819// define a service named 'HelloService'@conglin.clrpc.service.annotation.Service(name = "HelloService")interface HelloService { String hello(String arg); String hi(String arg);}// implements interface HelloServiceclass HelloServiceImpl implements HelloService { @Override public String hello(String arg) { return "Hello " + arg; } @Override public String hi(String arg) { return "Hi " + arg; }} Service Provider1234567// 创建服务提供者RpcProviderBootstrap bootstrap = new RpcProviderBootstrap();// 发布服务并开启服务bootstrap.publish(new HelloServiceImpl()) .hookStop() // 注册关闭钩子,用于优雅关闭服务提供者 .start(); Service Consumer123456789101112131415161718192021222324// 创建服务消费者RpcConsumerBootstrap bootstrap = new RpcConsumerBootstrap();// 开启服务消费者bootstrap.start();// 提前刷新需要订阅的服务bootstrap.refresh(HelloService.class);//使用同步服务HelloService syncService = bootstrap.subscribe(HelloService.class);String result = syncService.hello("I am consumer!"); // 一直阻塞,直到返回结果// 使用异步服务HelloService asyncService = bootstrap.subscribeAsync(HelloService.class);String fakeResult = asyncService.hello("I am consumer!"); // 直接返回默认值RpcFuture future = AsyncObjectProxy.lastFuture(); // 获取该线程最新一次操作的产生的future对象future.addCallback(new Callback(){ // 使用回调处理结果 @Override public void success(Object res) {} @Override public void fail(Exception e) {}});// 关闭服务消费者bootstrap.stop(); Service Consumer (With Transaction)123456789101112131415161718192021// 创建服务消费者RpcConsumerBootstrap bootstrap = new RpcConsumerBootstrap();// 开启服务消费者bootstrap.start();// 提前刷新需要订阅的服务bootstrap.refresh(HelloService.class);TransactionProxy proxy = bootstrap.subscribeTransaction();HelloService service = proxy.subscribeAsync(HelloService.class);proxy.begin(); // 事务开启service.hello("first request"); // 异步发送第一条请求RpcFuture f1 = AsyncObjectProxy.lastFuture(); // 获取第一条请求产生的future对象service.hi("second request"); // 异步发送第二条请求RpcFuture f2 = AsyncObjectProxy.lastFuture(); // 获取第二条请求产生的future对象RpcFuture future = proxy.commit(); // 事务提交 返回事务 Future// 关闭服务消费者bootstrap.stop(); Service Monitor12345678// 由监视器工厂创建监视器RpcMonitorBootstrap bootstrap = new ConsoleRpcMonitorBootstrap();// 设置监视器的配置以及你需要监视的服务// 并开启服务监视器bootstrap.monitor(HelloService.class) .hookStop() // 注册关闭钩子,用于优雅关闭服务监视器 .start(); Architecture Config默认配置文件名为 clrpc-config。 默认配置文件模板。 Config File配置文件位置默认在项目 resources 目录下,默认格式为 json ,默认文件为 clrpc-config.json。 Config Items Field Type Null Default Remark zookeeper.provider.address String YES 127.0.0.1:2181 服务注册地址 zookeeper.provider.root-path String YES /clrpc 服务注册根节点 zookeeper.provider.session-timeout Integer YES 5000 超时时间,单位为毫秒 zookeeper.consumer.address String YES 127.0.0.1:2181 服务搜索地址 zookeeper.consumer.root-path String YES /clrpc 服务搜索根节点 zookeeper.consumer.session-timeout Integer YES 5000 超时时间,单位为毫秒 zookeeper.monitor.address String YES 127.0.0.1:2181 服务监视地址 zookeeper.monitor.root-path String YES /clrpc 服务监视根节点 zookeeper.monitor.session-timeout Integer YES 5000 超时时间,单位为毫秒 zookeeper.atomicity.address String YES 127.0.0.1:2181 原子服务地址 zookeeper.atomicity.root-path String YES /clrpc 原子服务根节点 zookeeper.atomicity.session-timeout Integer YES 5000 超时时间,单位为毫秒 meta.provider.* Map<String, Object> YES Empty Map 服务提供者通用元信息,发布至注册中心 meta.consumer.* Map<String, Object> YES Empty Map 服务消费者通用元信息,发布至注册中心 provider.port Integer YES 0 服务提供者端口号 provider.thread.boss Integer YES 1 服务提供者的bossGroup线程数 provider.thread.worker Integer YES 4 服务提供者的workerGroup线程数 provider.channel.handler-factory String YES null 实现ChannelHandlerFactory,可自定义添加处理器 consumer.wait-time Long YES 5000 无服务提供者时等待重试时间,单位为毫秒 consumer.thread.worker Integer YES 4 服务使用者的workerGroup线程数 consumer.retry.check-period Long YES 3000 重试机制执行周期 consumer.retry.initial-threshold Long YES 3000 初始重试时间门槛 consumer.fallback.max-retry Integer TES -1 Fallback 机制允许重试最大的次数(负数代表不开启,0代表不重试) provider.channel.handler-factory String YES null 实现ChannelHandlerFactory,可自定义添加处理器 service.thread-pool.core-size Integer YES 5 业务线程池核心线程数 service.thread-pool.max-size Integer YES 10 业务线程池最大线程数 service.thread-pool.keep-alive Integer YES 1000 当线程数大于核心时,多余空闲线程在终止之前等待新任务的最长时间 service.thread-pool.queue Integer YES 10 业务线程池队列数 About customized meta infomation在一个进程中,针对不同的服务可以使用不同的元信息。 例如服务提供者提供了 AService 和 BService,那么发布的元信息在配置文件中分别对应于 meta.provider.AService 和 meta.provider.BService 指向的具体元信息;若具体元信息不存在,则发布 meta.provider.* 对应的通用元信息;若通用元信息不存在,则发布空信息。 About customized channel handlerClick me. Test使用 默认配置文件 进行本机模拟RPC测试。 OS:Manjaro 19.0.2 Kyria Kernel: x86_64 Linux 5.4.24-1-MANJARO CPU:Intel Core i5-6300HQ @ 4x 2.30GHz RAM: 11873 MB JDK: openjdk-13.0.2 Synchronous Test (without cache)在同步测试中,尽量了排除业务逻辑占用时间的干扰。 服务端 客户端 Conclusion: 本机基础上,且只有一台服务器的情况下,1000次的同步请求大约在 500毫秒 内完成。 本机基础上,且只有一台服务器的情况下,10000次的同步请求大约在 2000毫秒 内完成。 本机基础上,且只有一台服务器的情况下,100000次的同步请求大约在 10000毫秒 内完成。 Asynchronous Test (without cache)在异步测试中,尽量了排除业务逻辑占用时间的干扰。 服务端 客户端 Conclusion: 本机基础上,且只有一台服务器的情况下,1000次的异步请求大约在 680毫秒 内完成。(请求调用完成后每500毫秒检查一次) 本机基础上,且只有一台服务器的情况下,10000次的异步请求大约在 1400毫秒 内完成。(请求调用完成后每500毫秒检查一次) 本机基础上,且只有一台服务器的情况下,100000次的异步请求大约在 3000毫秒 内完成。(请求调用完成后每500毫秒检查一次) Extensionclrpc 利用了 Netty 的 ChannelPipeline 作为处理消息的责任链,并提供消息处理扩展点。 使用者实现接口 conglin.clrpc.service.handler.factory.ChannelHandlerFactory,并声明在配置文件中,即可完成对消息处理的扩展。 在创建 conglin.clrpc.service.handler.factory.ChannelHandlerFactory 对象时,会向构造方法其中传入一个参数,其参数类型如下: Role Type Remark Provider conglin.clrpc.service.context.ProviderContext 上下文 Consumer conglin.clrpc.service.context.ConsumerContext 上下文 LicenseApache 2.0]]></content>
<categories>
<category>RPC</category>
</categories>
<tags>
<tag>Java</tag>
<tag>RPC</tag>
<tag>分布式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Morris Traversal方法遍历二叉树]]></title>
<url>%2F2019%2F06%2F15%2FMorris-Traversal%E6%96%B9%E6%B3%95%E9%81%8D%E5%8E%86%E4%BA%8C%E5%8F%89%E6%A0%91%2F</url>
<content type="text"><![CDATA[通常,实现二叉树的前序(preorder)、中序(inorder)、后序(postorder)遍历有两个常用的方法:一是递归(recursive),二是使用栈实现的迭代版本(stack+iterative)。这两种方法都是O(n)或者O(logn)的空间复杂度(递归本身占用stack空间或者用户自定义的stack),所以不满足要求。(用这两种方法实现的中序遍历实现可以参考这里。) Morris Traversal方法可以做到这两点,与前两种方法的不同在于该方法只需要O(1)空间,而且同样可以在O(n)时间内完成。 要使用O(1)空间进行遍历,最大的难点在于,遍历到子节点的时候怎样重新返回到父节点(假设节点中没有指向父节点的p指针),由于不能用栈作为辅助空间。为了解决这个问题,Morris方法用到了线索二叉树(threaded binary tree)的概念。在Morris方法中不需要为每个节点额外分配指针指向其前驱(predecessor)和后继节点(successor),只需要利用叶子节点中的左右空指针指向某种顺序遍历下的前驱节点或后继节点就可以了。 中序遍历步骤: 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。 重复以上1、2直到当前节点为空。 下图为每一步迭代的结果(从左至右,从上到下),cur代表当前节点,深色节点表示该节点已输出。 前序遍历前序遍历与中序遍历相似,代码上只有一行不同,不同就在于输出的顺序。 步骤: 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。 重复以上1、2直到当前节点为空。 图示: 下图为每一步迭代的结果(从左至右,从上到下),cur代表当前节点,深色节点表示该节点已输出。 后序遍历后续遍历稍显复杂,需要建立一个临时节点dump,令其左孩子是root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。 步骤: 当前节点设置为临时节点dump。 如果当前节点的左孩子为空,则将其右孩子作为当前节点。 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右孩子。 重复以上1、2直到当前节点为空。 图示:]]></content>
<categories>
<category>算法</category>
</categories>
<tags>
<tag>算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java内存模型的小总结]]></title>
<url>%2F2019%2F06%2F02%2FJava%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E7%9A%84%E5%B0%8F%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[JMM规定了线程的工作内存和主内存的交互关系,以及线程之间的可见性和程序的执行顺序。一方面,要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能地放松。JMM对程序员屏蔽了CPU以及OS内存的使用问题,能够使程序在不同的CPU和OS内存上都能够达到预期的效果。 Java采用内存共享的模式来实现线程之间的通信。编译器和处理器可以对程序进行重排序优化处理,但是需要遵守一些规则,不能随意重排序。 原子性:一个操作或者多个操作要么全部执行要么全部不执行; 可见性:当多个线程同时访问一个共享变量时,如果其中某个线程更改了该共享变量,其他线程应该可以立刻看到这个改变; 有序性:程序的执行要按照代码的先后顺序执行; 在并发编程模式中,势必会遇到上面三个概念,JMM对原子性并没有提供确切的解决方案,但是JMM解决了可见性和有序性,至于原子性则需要通过锁来解决了。 如果一个操作A的操作结果需要对操作B可见,那么我们就认为操作A和操作B之间存在happens-before关系,即A happens-before B。 happens-before原则是JMM中非常重要的一个原则,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以解决在并发环境下两个操作之间是否存在冲突的所有问题。JMM规定,两个操作存在happens-before关系并不一定要A操作先于B操作执行,只要A操作的结果对B操作可见即可。 在程序运行过程中,为了执行的效率,编译器和处理器是可以对程序进行一定的重排序,但是他们必须要满足两个条件:1 执行的结果保持不变,2 存在数据依赖的不能重排序。重排序是引起多线程不安全的一个重要因素。 同时顺序一致性是一个比较理想化的参考模型,它为我们提供了强大而又有力的内存可见性保证,它主要有两个特征: 一个线程中的所有操作必须按照程序的顺序来执行; 所有线程都只能看到一个单一的操作执行顺序,在顺序一致性模型中,每个操作都必须原则执行且立刻对所有线程可见。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[浅谈Class.forName()和classLoader.loadClass()的区别]]></title>
<url>%2F2019%2F05%2F17%2F%E6%B5%85%E8%B0%88Class-forName-%E5%92%8CclassLoader-loadClass-%E7%9A%84%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[在理解这两个区别前,需要弄清楚java类的加载机制。 类加载机制 加载。加载过程完成以下三件事: 通过类的完全限定名称获取定义该类的二进制字节流。 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。 验证。确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 准备。类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值(一般为 0 或 false 或 null),使用的是方法区的内存。实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。特别的,类静态常量直接设置值。 解析。将常量池的符号引用替换为直接引用的过程。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。 初始化。初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。 区别二者均是调用了 ClassLoad 的 loadClass(String name, boolean resolve) 方法。 不同的是 参数 resolve 取值不同。 Class.forName(String name) 取 true ,loadClass(String name) 取 false。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879/** * Loads the class with the specified <a href="#binary-name">binary name</a>. The * default implementation of this method searches for classes in the * following order: * * <ol> * * <li><p> Invoke {@link #findLoadedClass(String)} to check if the class * has already been loaded. </p></li> * * <li><p> Invoke the {@link #loadClass(String) loadClass} method * on the parent class loader. If the parent is {@code null} the class * loader built into the virtual machine is used, instead. </p></li> * * <li><p> Invoke the {@link #findClass(String)} method to find the * class. </p></li> * * </ol> * * <p> If the class was found using the above steps, and the * {@code resolve} flag is true, this method will then invoke the {@link * #resolveClass(Class)} method on the resulting {@code Class} object. * * <p> Subclasses of {@code ClassLoader} are encouraged to override {@link * #findClass(String)}, rather than this method. </p> * * <p> Unless overridden, this method synchronizes on the result of * {@link #getClassLoadingLock getClassLoadingLock} method * during the entire class loading process. * * @param name * The <a href="#binary-name">binary name</a> of the class * * @param resolve * If {@code true} then resolve the class * * @return The resulting {@code Class} object * * @throws ClassNotFoundException * If the class could not be found */ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats PerfCounter.getParentDelegationTime().addTime(t1 - t0); PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; }} resolve默认是false不链接,不进行链接意味着不进行上述2、3、4步骤,那么静态块和静态对象就不会得到执行。 测试1234567891011121314151617181920212223242526public class Test{ public static void main(String[] args) { try { System.out.println("1-----------------------"); ClassLoader.getSystemClassLoader().loadClass("X"); System.out.println("2-----------------------"); ClassLoader.getSystemClassLoader().loadClass("X"); System.out.println("3-----------------------"); ClassLoader.getSystemClassLoader().loadClass("X"); System.out.println("4-----------------------"); Class.forName("X"); System.out.println("5-----------------------"); Class.forName("X"); System.out.println("6-----------------------"); Class.forName("X"); } catch (ClassNotFoundException e) { e.printStackTrace(); } }}class X{ static{ System.out.println("hello"); }} 输出为: 12345671-----------------------2-----------------------3-----------------------4-----------------------hello5-----------------------6-----------------------]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[0-1背包问题]]></title>
<url>%2F2019%2F05%2F05%2F0-1%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[问题0-1背包是在 n 件物品取出若干件放在空间为 w 的背包里,每件物品的重量为 w1,w2至wn,与之相对应的价值为p1,p2至pn。0-1背包是背包问题中最简单的问题。0-1背包的约束条件是给定几种物品,每种物品有且只有一个,并且有权值和重量两个属性。在0-1背包问题中,因为每种物品只有一个,对于每个物品只需要考虑选与不选两种情况。如果不选择将其放入背包中,则不需要处理。如果选择将其放入背包中,由于不清楚之前放入的物品占据了多大的空间,需要枚举将这个物品放入背包后可能占据背包空间的所有情况。 解析0-1背包问题是很明显的动态规划问题。 算法设物品重量为 weights[] ,其相对应的价值 values[]。 共有 n 件物品。最大承重量为 w。 详细见解法1注释。 解法11234567891011121314151617public void bag(int[] weights, int[] values, int n, int w){ // dp[i][j] 代表前 i 件放入容量为 j的背包中的最大价值 int[][] dp = new int[n + 1][w + 1]; for(int i = 1; i <= n; i++){ for(int j = 0; j <= w; j++){ if(j >= weights[i]){ // 如果当前容量可以放入第 i 件物品 // 根据最大价值原则,决定是否放入当前这件物品 dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]); }else{ // 放不下的话就不放 dp[i][j] = dp[i - 1][j]; } } } return dp[n][w];} 解法2解法1的空间占用较大,经过分析,对于解法1的 dp[i][j] 我们只需要知道左上角的值 dp[i - 1][j - weights[i]] 即可。所以使用一维数组即可。 12345678910111213public void bag(int[] weights, int[] values, int n, int w){ int[] dp = new int[w + 1]; for(int i = 1; i <= n; i++){ for(int j = w; j >= w; j--){ if(j >= weights[i]){ // 如果当前容量可以放入第 i 件物品 // 根据最大价值原则,决定是否放入当前这件物品 dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]); } } } return dp[w];}]]></content>
<categories>
<category>算法</category>
</categories>
<tags>
<tag>算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[缓存与数据库的双写一致性问题]]></title>
<url>%2F2019%2F04%2F27%2F%E7%BC%93%E5%AD%98%E4%B8%8E%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E5%8F%8C%E5%86%99%E4%B8%80%E8%87%B4%E6%80%A7%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题。 一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。 串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。 Cache Aside Pattern最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。 更新的时候,先更新数据库,然后再删除缓存。 为什么是删除缓存,而不是更新缓存? 原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。 比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。 另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到? 举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。 其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。 最初级的缓存不一致问题及解决方案问题:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。 解决思路:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。 比较复杂的数据不一致问题分析数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。这样的话,数据库和缓存中的数据不一样了… 为什么上亿流量高并发场景下,缓存会出现这个问题? 只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。 解决方案如下: 更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。 一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。 这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。 待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。 如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。 高并发的场景下,该解决方案要注意的问题:读请求长时阻塞 由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。 该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。 另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作。如果一个内存队列里居然会挤压 100 个商品的库存修改操作,每隔库存修改操作要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到数据,这个时候就导致读请求的长时阻塞。 一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会 hang 多少时间,如果读请求在 200ms 返回,如果你计算过后,哪怕是最繁忙的时候,积压 10 个更新操作,最多等待 200ms,那还可以的。 如果一个内存队列中可能积压的更新操作特别多,那么就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。 一般来说,数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。像这种针对读高并发、读缓存架构的项目,一般来说写请求是非常少的,每秒的 QPS 能到几百就不错了。]]></content>
<categories>
<category>缓存</category>
</categories>
<tags>
<tag>数据库</tag>
<tag>缓存</tag>
</tags>
</entry>
<entry>
<title><![CDATA[浅谈ThreadLocal]]></title>
<url>%2F2019%2F04%2F20%2F%E6%B5%85%E8%B0%88ThreadLocal%2F</url>
<content type="text"><![CDATA[ThreadLocalThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。 数据结构 每个Thread线程内部都有一个Map(ThreadLocalMap); Map里面存储线程本地对象(key)和线程的变量副本(value); 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。 所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。 在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。 Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。 Hash冲突怎么解决和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。 ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。 ThreadLocalMap的问题由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。 如何避免泄漏既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。 如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。 总结 每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。 ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。 适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[黑匣子问题]]></title>
<url>%2F2019%2F04%2F12%2F%E9%BB%91%E5%8C%A3%E5%AD%90%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[问题有一个黑匣子,黑匣子里有一个关于 x 的多项式 p(x) 。我们不知道它有多少项,但已知所有的系数都是正整数。每一次,你可以给黑匣子输入一个整数,黑匣子将返回把这个整数代入多项式后的值。那么,最少需要多少次, 我们可以得到这个多项式每项的系数呢? 解答答案是两次。 设 P(x)=An*x^n+An-1*x^(n-1)+...+A1*x^1+A0 第一次,输入 1 ,于是便得到整个多项式的所有系数之和。记作 S 。 第二次,输入 S + 1 ,于是黑匣子返回的是 An*(S+1)^n+An-1*(S+1)^(n-1)+...+A1*(S+1)^1+A0 我们要得到 An, An-1, An-2…A1, A0 即将第二次产生的结果转为 S+1 进制,每一位上的结果即为所得。(第一次得到S是确保 对于任意i, 有Ai <= S ) 其实最大系数不超过N的多项式,本来就是N进制的本质。]]></content>
<categories>
<category>算法</category>
</categories>
<tags>
<tag>算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Http缓存优先级问题]]></title>
<url>%2F2019%2F04%2F05%2FHttp%E7%BC%93%E5%AD%98%E4%BC%98%E5%85%88%E7%BA%A7%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[HTTP缓存分为强缓存和对比缓存(也叫协商缓存)。 强缓存强缓存:只要请求了一次,在有效时间内,不会再请求服务器(请求都不会发起),直接从浏览器本地缓存中获取资源。 主要字段有 expires:date(过期日期) cache-control: max-age=time(秒数,多久之后过期) | no-cache|no-store 如果expires和cache-control同时存在,cache-control会覆盖expires。 这里建议两个都写,cache-control是 Http 1.1 的头字段,expires是 Http 1.0 的头字段,都写的话兼容会好点。 对比缓存对比缓存:无论是否变化,是否过期都会发起请求,如果内容没过期,直接返回304,从浏览器缓存中拉取文件,否则直接返回更新后的内容。 主要字段有 服务器端返回字段 Etag: xxxx (一般为md5值) 其对应客户端匹配字段为, If-None-Match: w/xxx(xxx的值和上面的etag的xxx一样的话则返回304,否则返回200以及修改后的资源) 服务器端返回字段:Last-Modifieddate(日期),对应客户端匹配字段If-Modified-Since:date(如果服务器date小于等于客户端请求date则返回304,否则返回200以及修改后的资源) 优先级同时存在各种缓存头时,各缓存头优先级及生效情况为: 强缓存和对比缓存同时存在,如果强缓存还在生效期则强制缓存覆盖对比缓存,对比缓存不生效;如果强缓存不在有效期,对比缓存生效。即:强缓存优先级 > 对比缓存优先级 强缓存expires和cache-control同时存在时,则cache-control会覆盖expires,expires无论有没有过期,都无效。 即:cache-control优先级 > expires优先级。 对比缓存Etag和Last-Modified同时存在时,则Etag会覆盖Last-Modified,Last-Modified不会生效。即:ETag优先级 > Last-Modified优先级。]]></content>
<categories>
<category>Http</category>
</categories>
<tags>
<tag>Http</tag>
</tags>
</entry>
<entry>
<title><![CDATA[飞机座位问题]]></title>
<url>%2F2019%2F03%2F25%2F%E9%A3%9E%E6%9C%BA%E5%BA%A7%E4%BD%8D%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[问题一架飞机上有一百个座位,编号是从1到100。现在编号为1到100的乘客依次坐上飞机。编号为1的乘客比较皮,上了飞机之后是随机(等概率地)坐座位的。编号为2的乘客上了飞机之后,他先看有没有人坐在2号位上,如果有,那他就在剩下的位子里随机(等概率地)挑选一个,如果没有人坐,他就坐在2号位上。3号也是一样,如果前面有人已经坐了3号位了,他就在剩下的位子上随便挑一个做,反之则坐自己位子。以此类推,最后问题是,第100个人坐在第100号位子上的概率应该是多少? 解答凭感觉来看最后一个上飞机的人的位子可能被前99个人中的任何一个人占有,所以做到自己的位子的概率非常低。 但实际上,换个角度来看,我们就能很简单的算出第100个人坐在第100号位子上的概率应该是多少。 这个问题其实相当于1号坐在了i号位置上,i号乘客上车之后把一号赶走;一号继续去随机坐位置。即等价于2至99号座位都是固定的,一号只能在1与100两个座位里选一个。即答案是 1/2 。]]></content>
</entry>
<entry>
<title><![CDATA[百囚徒挑战]]></title>
<url>%2F2019%2F03%2F10%2F%E7%99%BE%E5%9B%9A%E5%BE%92%E6%8C%91%E6%88%98%2F</url>
<content type="text"><![CDATA[问题 监狱决定给关押的100名囚徒一次特赦的机会,条件是囚徒通过一项挑战。所有囚徒被编号为1-100,对应他们编号的100个号码牌被打乱顺序放在了100个抽屉里。每个囚徒需要从所有抽屉里打开至多半数(50个),并从中找出对应自己编号的号码牌。如果找到了则该名囚徒的任务成功。所有囚徒会依次单独进入挑战室完成任务,并且从第一个囚徒进入挑战室开始,直到所有囚徒结束挑战为止囚徒之间任何形式的交流都是禁止的。当一名囚徒完成任务后,挑战室会被恢复为他进入之前的样子。在这100名囚徒中,任意一名囚徒的失败都会导致整个挑战失败,只有当所有囚徒全部成功完成任务时,他们才会统一得到特赦的机会。最后,在开始挑战之前,监狱给了所有囚徒一个月时间商量对策。那么,囚徒究竟有多大的几率得到释放? 一眼看上去囚徒获胜的概率小的可怜,因为假如每个人的任务都是一次独立实验,那么他完成任务的概率只有 1/2 。再加上基数 100 人 即为 (1/2)^100。 但是只要囚徒采取了正确的策略,那他们获胜的概率很大。 解答不妨假设抽屉里的号码牌是随机放置的(否则,囚徒可以自己在脑内打乱所有抽屉的位置以达到同样的效果),之后囚徒首先为抽屉编号,例如从左上到右下依次编号。而每个囚徒的策略,就是首先打开与自己编号相同的抽屉,从中取出号码牌,并打开号码牌所对应的抽屉。之后,重复此过程,直到找到自己的号码牌,或者50个抽屉的机会用完。 例如,29号囚徒首先打开了29号抽屉,里面放着51号的号码牌,于是他打开51号抽屉,里面放着18号的号码牌,于是他打开18号的抽屉,里面放着29号的号码牌,他完成了任务。(只是随便举例) 为了计算成功概率,首先对这个游戏进行化简。将抽屉与号码牌的对应关系视为一个映射 f(29)=51 f(51)=18 f(18)=29那么从任意一个数出发,不停地迭代计算,最终总能回到这个数。通过这种方法,1-100 的数字被分割为了一些“圆环”,而每个圆环的长度不一,比如 f(3)=3 这种圆环长度就是1,意味着3号抽屉里装着3号号码牌; f(29)=51 f(51)=18 f(18)=29 这种圆环长度是3;这时,我们发现,所有囚徒能够通过挑战,当且仅当所有圆环的长度不超过50 ,此时显然每个囚徒都能在50次以内找到自己的号码牌,反之如果有一个圆环长度超过50,那么这个圆环上的所有人都会失败。 接下来就是计算了。比起计算“所有圆环的长度不超过50”的概率,“有一个圆环长度超过50”的概率更容易计算。因为“有一个圆环的长度是51”和“有一个圆环的长度是52”之类的事件是彼此互斥的(圆环的长度总和是100),所以总概率就是它们的和。 而对于 m>= 51只需先选出 m 个元素,将它们构成一个环,之后再将剩下的元素随机打乱即可唯一地得到一种分布。具体地说,所有形成长度为 m 环的映射种类为 100!/m 。全排列个数为 m! ,因此这个概率等于 P(m) = 1/m 。 综上,所有圆环长度不超过50的概率等于 1-(1/51)-(1/52)-...-(1/100) 其值大约是 0.312,这个概率就是囚徒被释放的概率。]]></content>
<categories>
<category>概率</category>
</categories>
<tags>
<tag>概率</tag>
</tags>
</entry>
<entry>
<title><![CDATA[从1到n整数中1出现的次数]]></title>
<url>%2F2019%2F03%2F04%2F%E4%BB%8E1%E5%88%B0n%E6%95%B4%E6%95%B0%E4%B8%AD1%E5%87%BA%E7%8E%B0%E7%9A%84%E6%AC%A1%E6%95%B0%2F</url>
<content type="text"><![CDATA[题目输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。例如输入12,从1到12这些整数中包含1的数字有1,10,11和12,1一共出现了5次。 思路设N = abcde ,其中abcde分别为十进制中各位上的数字。 如果要计算百位上1出现的次数,它要受到3方面的影响:百位上的数字,百位以下(低位)的数字,百位以上(高位)的数字。 如果百位上数字为0,百位上可能出现1的次数由更高位决定。比如:12013,则可以知道百位出现1的情况可能是:100~199,1100~1199,2100~2199,,…,11100~11199,一共1200个。可以看出是由更高位数字(12)决定,并且等于更高位数字(12)乘以 当前位数(100)。 如果百位上数字为1,百位上可能出现1的次数不仅受更高位影响还受低位影响。比如:12113,则可以知道百位受高位影响出现的情况是:100~199,1100~1199,2100~2199,,….,11100~11199,一共1200个。和上面情况一样,并且等于更高位数字(12)乘以 当前位数(100)。但同时它还受低位影响,百位出现1的情况是:12100~12113,一共114个,等于低位数字(113)+1。 如果百位上数字大于1(2~9),则百位上出现1的情况仅由更高位决定,比如12213,则百位出现1的情况是:100~199,1100~1199,2100~2199,…,11100~11199,12100~12199,一共有1300个,并且等于更高位数字+1(12+1)乘以当前位数(100)。 代码12345678910111213141516171819202122232425public class Solution { public int countDigitOne(int n) { int count = 0;//1的个数 int i = 1;//当前位 int current = 0,after = 0,before = 0; while((n/i)!= 0){ current = (n/i)%10; //高位数字 before = n/(i*10); //当前位数字 after = n-(n/i)*i; //低位数字 //如果为0,出现1的次数由高位决定,等于高位数字 * 当前位数 if (current == 0) count += before*i; //如果为1,出现1的次数由高位和低位决定,高位*当前位+低位+1 else if(current == 1) count += before * i + after + 1; //如果大于1,出现1的次数由高位决定,//(高位数字+1)* 当前位数 else{ count += (before + 1) * i; } //前移一位 i = i*10; } return count; }} 简洁版 123456public static int countDigitOne(int n) { int ones = 0; for (long m = 1; m <= n; m *= 10) ones += (n/m + 8) / 10 * m + (n/m % 10 == 1 ? n%m + 1 : 0); return ones;}]]></content>
<categories>
<category>算法</category>
</categories>
<tags>
<tag>算法</tag>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[简单博客系统(Web)]]></title>
<url>%2F2019%2F03%2F03%2F%E7%AE%80%E5%8D%95%E5%8D%9A%E5%AE%A2%E7%B3%BB%E7%BB%9F-Web%2F</url>
<content type="text"><![CDATA[Serendipity简介Serendipity (意为发现美好)是一个简单的微型博客项目,由 Spring Boot(Maven) 构建,其中使用了 Spring MVC Spring Security Spring Data Jpa , 数据库使用了 MySQL。 项目功能包含: 用户注册 用户登陆 发布信息 删除信息 注:该项目中,用户称为 Serendipper 信息称为 Serendimsg 。 源码源码已经托管在Github,戳我 查看源码。 详细介绍实体用户(Serendipper)在包含了一些必要属性的前提下,使用 @OneToMany 关联到 信息(Serendimsg)。 信息(Serendimsg)在包含了一些必要属性的前提下,使用 @CreatedBy 关联到 用户(Serendipper),以及 @CreatedDate 确定发送时间。 Controller调用 Service 提供的接口实现功能。 由于时间紧迫,没有编写 RESTful 风格的 Controller。而是使用传统的 Thymeleaf 模板引擎进行渲染。 Service接口 SerendipperService 和 SerendimsgService 为 Controller 层服务。 SerendipperServiceImpl 和 SerendimsgServiceImpl 实现了接口。 DAO接口 SerendipperRepository 和 SerendimsgRepository 继承 JpaRepository 为 Service 层服务。 因此不用特意实现这两个接口,只需要根据 Spring Data 的规定编写接口即可。 SecuritySecurity 使用Spring Security 控制。具体设置位于类 conglin.serendipity.config.WebSecurityConfig 中。 下载1git clone git@github.com:CongLinDev/Serendipity.git]]></content>
<categories>
<category>Spring</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>Spring Boot</tag>
</tags>
</entry>
<entry>
<title><![CDATA[字符串匹配的三个算法]]></title>
<url>%2F2019%2F02%2F02%2F%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8D%E7%9A%84%E4%B8%89%E4%B8%AA%E7%AE%97%E6%B3%95%2F</url>
<content type="text"><![CDATA[字符串匹配的意思是给一个字符串集合,和另一个字符串集合,看这两个集合交集是多少。 若是都只有一个字符串,那么就看其中一个是否包含另外一个; 若是父串集合(比较长的,被当做模板)的有多个,子串(拿去匹配的)只有一个,就是问这个子串是否存在于父串之中; 若是子串父串集合都有多个,那么就是问交集了。 KMP算法KMP算法是用来处理一对一的匹配的。 朴素的匹配算法,或者说暴力匹配法,就是将两个字符串从头比到尾,若是有一个不同,那么从下一位再开始比。这样太慢了。所以KMP算法的思想是,对匹配串本身先做一个处理,得到一个next数组。这个数组是做什么用的呢?next [j] = k,代表j之前的字符串中有最大长度为k 的相同前缀后缀。记录这个有什么用呢?对于ABCDABC这个串,如果我们匹配ABCDABTBCDABC这个长串,当匹配到第7个字符T的时候就不匹配了,我们就不用直接移到B开始再比一次,而是直接移到第5位来比较,岂不美哉?所以求出了next数组,KMP就完成了一大半。next数组也可以说是开始比较的位数。 计算next数组的方法是对于长度为n的匹配串,从0到n-1位依次求出前缀后缀最大匹配长度。 比如ABCDABD这个串: 如何去求next数组呢?k是匹配下标。这里没有从最后一位开始和第一位开始分别比较前缀后缀,而是利用了next[i-1]的结果。 123456789101112131415161718192021222324252627282930void getnext()//获取next数组{ int i,n,k; n=strlen(ptr); memset(next,0,sizeof(next)); k=0; for(i=1;i<n;i++) { while(k>0 && ptr[k]!=ptr[i]) k=next[k]; if(ptr[k]==ptr[i]) k++; next[i+1]=k; //next表示的是匹配长度 }} 这里是按照《算法导论》的代码来写的。算法导论算法循环是从1到n而不是从0到n-1,所以在下面匹配的时候需要j=next[j+1]。 1234567891011121314151617181920212223242526272829303132int kmp(char *a,char *b)//匹配ab两串,a为父串{ int i=0,j=0; int len1=strlen(a); int len2=strlen(b); getnext(); while(i<len1&&j<len2) { if(j==0||a[i]==b[j]) { i++;j++; } else j=next[j+1];//到前一个匹配点 } if(j>=len2) return i-j; else return -1;} 这里next数组的作用就显现出来了。最后返回的是i-j,也就是说,是从i位置前面的第j位开始的,也就是上面说的,next数组也可以说是开始比较的位数。也就是说,在父串的i位比的时候已经是在比子串的第j位了。 一个完整的代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100#include <iostream>#include <cstring>#include <cstdio>using namespace std;const int N=100;char str[100],ptr[100];//父串str和子串ptrint next[100];string ans;void getnext()//获取next数组{ int i,n,k; n=strlen(ptr); memset(next,0,sizeof(next)); k=0; for(i=1;i<n;i++) { while(k>0 && ptr[k]!=ptr[i]) k=next[k]; if(ptr[k]==ptr[i]) k++; next[i+1]=k; //next表示的是匹配长度 }}int kmp(char *a,char *b)//匹配ab两串,a为父串{ int i=0,j=0; int len1=strlen(a); int len2=strlen(b); getnext(); while(i<len1&&j<len2) { if(j==0||a[i]==b[j]) { i++;j++; } else j=next[j+1];//到前一个匹配点 } if(j>=len2) return i-j; else return -1;}int main(){ while( scanf( "%s%s", str, ptr ) ) { int ans = kmp(str,ptr); if(ans>=0) printf( "%d\n", kmp( str,ptr )); else printf("Not find\n"); } return 0;} 字典树算法上面的KMP是一对一匹配的时候常用的算法。而字典树则是一对多的时候匹配常用算法。其含义是,把一系列的模板串放到一个树里面,然后每个节点存的是它自己的字符,从根节点开始往下遍历就可以得到一个个单词了。 我这里写的代码稍微和上面有一点区别,我的节点tnode里面没有存它本身的字符,而是存一个孩子数组。所以当数据量很大的时候还是需要做一些变通的,不可直接套用此代码。若是想以每个节点为一个node,那么要注意根节点是空的。 树的节点tnode,这里的next[i]存的是子节点指针。sum=0表示这个点不是重点。为n>0表示有n个单词以此为终点。 123456789101112131415161718struct tnode{ int sum;//用来判断是否是终点的 tnode* next[26]; tnode(){ for(int i =0;i<26;i++) next[i]=NULL; sum=0; }}; 插入函数: 假设字典树已经有了aer,现在插入abc,首先看a,不为空,那么直接跳到a节点里,看b,为空,那么新建,跳到b里,新建c,跳出。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152tnode* newnode(){ tnode *p = new tnode; for(int i =0;i<26;i++) p->next[i]=NULL; p->sum=0; return p;}//插入函数void Insert(char *s){ tnode *p = root; for(int i = 0 ; s[i] ; i++) { int x = s[i] - 'a'; if(p->next[x]==NULL) { tnode *nn=newnode(); for(int j=0;j<26;j++) nn->next[j] = NULL; nn->sum = 0; p->next[x]=nn; } p = p->next[x]; } p->sum++;//这个单词终止啦} 字符串比较:就是一个个字符去比呗…时间复杂度O(m),m是匹配串长度。 1234567891011121314151617181920212223242526272829303132bool Compare(char *ch){ tnode *p = root; int len = strlen(ch); for(int i = 0; i < len; i++) { int x = ch[i] - 'a'; p = p->next[x]; if(p==NULL) return false; if(i==len-1 && p->sum>0 ){ return true; } } return false;} 给个完整的代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186#include<queue>#include<set>#include<cstdio>#include <iostream>#include<algorithm>#include<cstring>#include<cmath>using namespace std;/* trie字典树*/struct tnode{ int sum;//用来判断是否是终点的 tnode* next[26]; tnode(){ for(int i =0;i<26;i++) next[i]=NULL; sum=0; }};tnode *root; tnode* newnode(){ tnode *p = new tnode; for(int i =0;i<26;i++) p->next[i]=NULL; p->sum=0; return p;}//插入函数void Insert(char *s){ tnode *p = root; for(int i = 0 ; s[i] ; i++) { int x = s[i] - 'a'; if(p->next[x]==NULL) { tnode *nn=newnode(); for(int j=0;j<26;j++) nn->next[j] = NULL; nn->sum = 0; p->next[x]=nn; } p = p->next[x]; } p->sum++;//这个单词终止啦}//匹配函数bool Compare(char *ch){ tnode *p = root; int len = strlen(ch); for(int i = 0; i < len; i++) { int x = ch[i] - 'a'; p = p->next[x]; if(p==NULL) return false; if(i==len-1 && p->sum>0 ){ return true; } } return false;}void DELETE(tnode * &top){ if(top==NULL) return; for(int i =0;i<26;i++) DELETE(top->next[i]); delete top;}int main(){ int n,m; cin>>n; char s[20]; root = newnode(); for(int i =0;i<n;i++){ scanf("%s",s); Insert(s); } cin>>m; for(int i =0;i<m;i++){ scanf("%s",s); if(Compare(s)) cout<<"YES"<<endl; else cout<<"NO"<<endl; } DELETE(root); return 0;} AC自动机字典树是一对多的匹配,那么AC自动机就是多对多的匹配了。意思是:给一个字典,再给一个m长的文本,问这个文本里出现了字典里的哪些字。 这个问题可以用n个单词的n次KMP算法来做(效率为O(n*m*单词平均长度)),也可以用1个字典树去匹配文本串的每个字母位置来做(效率为O(m*每次字典树遍历的平均深度))。上面两种解法效率都不高,如果用AC自动机来解决的话,效率将为线性O(m)时间复杂度。 AC自动机也运用了一点KMP算法的思想。简述为字典树+KMP也未为不可。 首先讲一下acnode的结构: 与字典树相比,就多了个fail指针对吧,这个就相当于KMP算法里的next数组。只不过它存的是失配后跳转的位置,而不是跳转之后再向前跳了多少罢了。 12345678910111213141516171819202122struct acnode{ int sum; acnode* next[26]; acnode* fail; acnode(){ for(int i =0;i<26;i++) next[i]=NULL; fail= NULL; sum=0; }}; 插入什么的我就不说了,记得把fail置为空即可。 这里说一下fail指针的获取。fail指针是通过BFS来求的。 看这么一张图 图中数字我们不用管它,绿色代表是终点,虚线就是fail指针了。我们可以看到91 E节点的fail指针是指向76 E 的,也就是说执行到这里如果无法继续匹配就会跳到76 E那个节点继续往后匹配。我们可以看到它们前面都是H,也就是说fail指针指向的是父节点相同的同值节点(根节点视为与任何节点相同)。我们要算的是在一个长文本里面有多少个出现的单词,这个fail指针就是为了快速匹配而诞生的。若文本里出现了HISHERS,我们首先匹配了HIS,有通过fail指针跳到85 S从而匹配SHE,再匹配HERS。fail指针跳到哪里就代表这一点之前的内容已经被匹配了。这样就避免了再从头重复判断的过程。 在函数里,当前节点的fail指针也会去更新此节点的孩子的fail指针,因为父节点相同啊,而且因为它是此节点的fail指针,这两个节点的父节点也相同啊~所以一路相同过来,就保证fail指向的位置前缀是相同的。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162void getfail(){ queue<acnode*> q; for(int i = 0 ; i < 26 ; i ++ ) { if(root->next[i]!=NULL){ root->next[i]->fail = root; q.push(root->next[i]); } } while(!q.empty()){ acnode* tem = q.front(); q.pop(); for(int i = 0;i<26;i++){ if(tem->next[i]!=NULL) { acnode *p; p = tem->fail; while(p!=NULL){ if(p->next[i]!=NULL){ tem->next[i]->fail = p->next[i]; break; } p=p->fail; } if(p==NULL) tem->next[i]->fail = root; q.push(tem->next[i]); } } }} 给个完整的代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280#include<queue>#include<set>#include<cstdio>#include <iostream>#include<algorithm>#include<cstring>#include<cmath>using namespace std;/* ac自动机*/struct acnode{ int sum; acnode* next[26]; acnode* fail; acnode(){ for(int i =0;i<26;i++) next[i]=NULL; fail= NULL; sum=0; }};acnode *root;int cnt;acnode* newnode(){ acnode *p = new acnode; for(int i =0;i<26;i++) p->next[i]=NULL; p->fail = NULL; p->sum=0; return p;}//插入函数void Insert(char *s){ acnode *p = root; for(int i = 0; s[i]; i++) { int x = s[i] - 'a'; if(p->next[x]==NULL) { acnode *nn=newnode(); for(int j=0;j<26;j++) nn->next[j] = NULL; nn->sum = 0; nn->fail = NULL; p->next[x]=nn; } p = p->next[x]; } p->sum++;}//获取fail指针,在插入结束之后使用void getfail(){ queue<acnode*> q; for(int i = 0 ; i < 26 ; i ++ ) { if(root->next[i]!=NULL){ root->next[i]->fail = root; q.push(root->next[i]); } } while(!q.empty()){ acnode* tem = q.front(); q.pop(); for(int i = 0;i<26;i++){ if(tem->next[i]!=NULL) { acnode *p; if(tem == root){ tem->next[i]->fail = root; } else { p = tem->fail; while(p!=NULL){ if(p->next[i]!=NULL){ tem->next[i]->fail = p->next[i]; break; } p=p->fail; } if(p==NULL) tem->next[i]->fail = root; } q.push(tem->next[i]); } } }}//匹配函数void ac_automation(char *ch){ acnode *p = root; int len = strlen(ch); for(int i = 0; i < len; i++) { int x = ch[i] - 'a'; while(p->next[x]==NULL && p != root)//没匹配到,那么就找fail指针。 p = p->fail; p = p->next[x]; if(!p) p = root; acnode *temp = p; while(temp != root) { if(temp->sum >= 0) /* 在这里已经匹配成功了,执行想执行的操作即可,怎么改看题目需求+ */ { cnt += temp->sum; temp->sum = -1; } else break; temp = temp->fail; } }}int main(){ cnt = 0; int n; cin>>n; char c[101]; root = newnode(); for(int i = 0 ;i < n;i++){ scanf("%s",c); Insert(c); } getfail(); int m ; cin>> m; for(int i = 0;i<m;i++){ scanf("%s",c); ac_automation(c); } cout<<cnt<<endl; return 0;}]]></content>
<categories>
<category>算法</category>
</categories>
<tags>
<tag>C++</tag>
<tag>算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[飞机大战游戏设计]]></title>
<url>%2F2019%2F01%2F01%2F%E9%A3%9E%E6%9C%BA%E5%A4%A7%E6%88%98%E6%B8%B8%E6%88%8F%E8%AE%BE%E8%AE%A1%2F</url>
<content type="text"><![CDATA[本项目基于 Python3 进行开发,使用了 pygame 模块。 开发这个项目的目的不是为了做游戏,而是熟悉 设计模式 。 源代码源码已经托管在Github,戳我 查看源码。你也可以通过使用命令 git clone git@github.com:CongLinDev/AirplaneWarGame.git 直接下载源码查看。 类图类图如下: 设计模式该项目用到了多种设计模式,但由于Python是动态语言,所以一些可以用到的设计模式没有涉及。 静态工厂方法对于 Plane 和 Bullet 的创建均是使用了静态工厂创建。 生成器游戏中的 Level 实质上是一个生成器,根据 Level 的不同,生成的 EnemyPlane 数量不同,其组装的 EnemyPlaneGroup 也不同。 桥接游戏中的 Listener 起到了桥接的作用,监听按下不同 Button 对象。 享元工厂这里设计的享元的作用是一次加载所需的资源如图片、音乐等。不同常规的是,这里的享元工厂并没有工厂,笔者把它设计成 单件模式 的效果。 单件模式这里的享元使用Python的type方法进行创建其元类,只需要使用一个语句 __metaclass__ = metaflyweight 即可将类变成单件。函数 metaflyweight 如下: 123456789101112131415#纯函数式使用元类#type(类名, 父类的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))metaflyweight = lambda name, parents, attrs: type( name, parents, dict(attrs.items() + [ ('__instances', dict()), ('__new__', classmethod( lambda cls, *args, **kargs: cls.__instances.setdefault(#setdefault() 键如果不在字典中,会更新字典 tuple(args), super(type(cls), cls).__new__(*args, **kargs)) ) ) ]) ) 命令模式游戏中玩家飞机的子弹由玩家飞机通过一个 Command 的子类进行通知发射,其作用是为了使发射子弹与飞机移动进行 时间 上的解耦,有利于以后的扩展。 Demo]]></content>
<tags>
<tag>Python</tag>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[抓取简单的Pcap文件并读出信息]]></title>
<url>%2F2018%2F12%2F15%2F%E6%8A%93%E5%8F%96%E7%AE%80%E5%8D%95%E7%9A%84Pcap%E6%96%87%E4%BB%B6%E5%B9%B6%E8%AF%BB%E5%87%BA%E4%BF%A1%E6%81%AF%2F</url>
<content type="text"><![CDATA[以下开发均基于Linux平台,以Ubuntu为例进行讲解,具体源码移步这里。 抓取Pcap文件笔者使用libpcap库(libpcap是unix/linux平台下的网络数据包捕获函数包)进行抓包。 安装libpcap库 Libpcap下载。 解压下载的压缩包 tar -zxvf filename.tar.gz (filename是下载的文件名) 配置生成makefile文件。进入解压的文件夹,执行 ./configure。这里可能会提示缺少flex,使用sudo apt-get install flex即可。 执行make。这里可能会提示缺少yacc,使用sudo apt-get install yacc即可。 执行sudo make install。 使用库函数抓包 pcap_lookupdev():函数用于查找网络设备,返回可被 pcap_open_live() 函数调用的网络设备名指针。 pcap_lookupnet():函数获得指定网络设备的网络号和掩码。 pcap_open_live(): 函数用于打开网络设备,并且返回用于捕获网络数据包的数据包捕获描述字。对于此网络设备的操作都要基于此网络设备描述字。 pcap_compile(): 函数用于将用户制定的过滤策略编译到过滤程序中。 pcap_setfilter():函数用于设置过滤器。 pcap_loop():函数 pcap_dispatch() 函数用于捕获数据包,捕获后还可以进行处理,此外 pcap_next() 和 pcap_next_ex() 两个函数也可以用来捕获数据包。 pcap_close():函数用于关闭网络设备,释放资源。 pcap_dump_open用于打开保存的文件 pcap_dump用于输出数据到文件。 分析Pcap文件这里给出用于读取Pcap文件的结构体。读者可以从中看出Pcap文件的结构。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213/* Wireshark File Formate +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | PCAP File Header | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | PCAP Package Header | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Ethernet frame header | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | IP Header | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SCTP Package | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*/typedef unsigned char _1Byte;typedef unsigned short _2Byte;typedef unsigned int _4Byte;#define PCAP_FILE_HEADER_SIZE 24 //24个字节#define PACKET_HEADER_SIZE 16 //16个字节#define MAC_HEADER_SIZE 14 //14个字节#define IP_HEADER_SIZE 20 //20个字节#define ICMP_HEADER_SIZE 8 //8个字节#define TCP_HEADER_SIZE 20 //20个字节#define UDP_HEADER_SIZE 8 //8个字节/* PCAP File Header 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Magic Number(0xA1B2C3D4) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Magjor Version(0x02) | Minor Version(0x04) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Time Zone(0) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Time Stamp Accuracy(0) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Snapshot Length(0xFFFF) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Link Layer Type(0x01) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*/struct PcapFileHeader{ _4Byte magic; //4Byte:标记文件开始,并用来识别文件自己和字节顺序。 _2Byte majorVersion; //2Byte: 当前文件主要的版本号,一般为 0x0200 _2Byte minorVersion; //2Byte: 当前文件次要的版本号,一般为 0x0400 _4Byte timezone; //4Byte:当地的标准时间,如果用的是GMT则全零 _4Byte sigFlags; //4Byte:时间戳的精度 _4Byte snapLen; //4Byte:最大的存储长度 _4Byte linkType; //4Byte:链路类型};/* Packet Header 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Seconds | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Microseconds | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | CapLen | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Len | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*/struct PacketHeader{ _4Byte seconds; //4Byte 秒计时,被捕获时间的高位,单位是seconds _4Byte microseconds; //4Byte 微秒计时,被捕获时间的低位,单位是microseconds _4Byte capLen; //4Byte 当前数据区的长度,即抓取到的数据帧长度,不包括Packet Header本身的长度,单位是 Byte _4Byte len; //4Byte 离线数据长度:网络中实际数据帧的长度,一般不大于caplen,多数情况下和Caplen数值相等};/* IP Header 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Version| IHL |Type of Service| Total Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Identification |Flags| Fragment Offset | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Time to Live | Protocol | Header Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Destination Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*/struct IPHeader{ /*----------第一行-------------------*/ union//共一个字节 { _1Byte version;//版本号 _1Byte headerLength;//包头长度,指明IPv4协议包头长度的字节数包含多少个32位 }; _1Byte serviceType;//区分服务 _2Byte totalLength;//总长度 /*----------第二行-------------------*/ _2Byte identification;//标识 union { _2Byte flags;//标志,当封包在传输过程中进行最佳组合时使用的3个bit的识别记号 _2Byte fragmentOffset;//片偏移 }; /*----------第三行-------------------*/ _1Byte timeToLive;//生存时间 _1Byte protocol;//协议 _2Byte headerChecksum;//首部检验和 /*----------第四行-------------------*/ _4Byte sourceAddress;//源地址 /*----------第五行-------------------*/ _4Byte destinationAddress;//目标地址};//MAC帧信息struct MACHeader{ _1Byte destinationAddress[6]; _1Byte sourceAddress[6]; _2Byte type;};/* ICMPHeader 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | type | code | Header Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Identification | serial | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*/struct ICMPHeader { _1Byte type; //类型 _1Byte code; //代码 _2Byte headerChecksum;//首部检验和 _2Byte identification;//标识 _2Byte serial;//序列号};/* TCP Header 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source port | Destination port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Serial | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Acknowledgement Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |DataOffset|Reserve|u|a|p|r|s|f| Window | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Checksum | Urgent Pointer | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*/struct TCPHeader{ _2Byte sourcePort;//源端口 _2Byte destinationPort;//目的端口 _4Byte serial;//序号 _4Byte acknowledgementNumber;//确认号 union{//共2个字节 _2Byte dataOffset;//数据偏移 _2Byte reserve;//保留 _2Byte urg;//紧急 _2Byte ack;//确认 _2Byte psh;//推送 _2Byte rst;//复位 _2Byte syn;//同步 _2Byte fin;//终止 }; _2Byte window;//窗口 _2Byte checksum;//检验和 _2Byte urgentpointer;//紧急指针};struct UDPHeader{ _2Byte sourcePort;//源端口 _2Byte destinationPort;//目的端口 _2Byte length;//长度 _2Byte checksum;//检验和}; 分析的过程即是个读取文件的过程,在此不再赘述。]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>计算机网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Linux--实现简单的ls功能]]></title>
<url>%2F2018%2F11%2F18%2FLinux-%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95%E7%9A%84ls%E5%8A%9F%E8%83%BD%2F</url>
<content type="text"><![CDATA[利用Linux C实现简单的ls功能,其中包括: -a 显示所有文件及目录 (ls内定将文件名或目录名称开头为”.”的视为隐藏档,不会列出)。 -l 除文件名称外,亦将文件型态、权限、拥有者、文件大小等资讯详细列出。 -R 若目录下有文件,则以下之文件亦皆递归依序列出。 -d 显示目录名称而非其内容。 -i 显示文件和目录的inode编号。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159//ls_command.cpp#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <string.h>#include <limits.h>#include <sys/types.h>#include <sys/stat.h>#include <dirent.h>#include <stdbool.h>#define PARAM_TYPE_NUM 6//参数种类#define DESTINATION_PARAM 0#define a_PARAM 1#define d_PARAM 2#define i_PARAM 3#define l_PARAM 4#define R_PARAM 5void handle_params(int argc, char* argv[], bool params[], char* optstring);void handle_ls_command(bool params[], char* argv[]);void printDir(char *dir, int depth, bool params[]);void handle_params(int argc, char* argv[], bool params[], char* optstring){ int opt; while((opt = getopt(argc, argv, optstring)) != -1){ switch(opt){ case 'a': params[a_PARAM] = true; break; case 'd': params[d_PARAM] = true; break; case 'i': params[i_PARAM] = true; break; case 'l': params[l_PARAM] = true; break; case 'R': params[R_PARAM] = true; break; case '?': printf("ls: unknown option: %c\n", optopt); optind--; break; default: break; } } if(optind < argc){ params[DESTINATION_PARAM] = true; }}void handle_ls_command(bool params[], char* argv[]){ if(params[DESTINATION_PARAM] != true){ printDir(".", 0, params); }else{ printDir(argv[optind], 0, params); }}void printDir(char *dir, int depth, bool params[]){ DIR *destination_directory = opendir(dir); if(destination_directory == NULL){//如果打开失败,给出提示,并退出。 printf("ls: 打开文件夹 %s 失败。\n", dir); // exit(-1); return; } struct dirent *entry; struct stat statbuf; chdir(dir);//更换工作路径 while((entry = readdir(destination_directory)) != NULL){ lstat(entry->d_name, &statbuf); //-d 显示目录名称而非其内容 if(params[d_PARAM] == true){ printf("%*s%s/ \n", depth, " ", dir); //-i 显示文件和目录的索引节点号 if(params[i_PARAM] == true){ printf("%*s%ld\n", depth, " inode:", statbuf.st_ino); } //-l 除文件名称外,亦将文件型态、权限、拥有者、文件大小等资讯详细列出 if(params[l_PARAM] == true){ printf("%*s%d\t", depth, " 权限:", statbuf.st_mode);//权限 printf("%*s%d\t", depth, " 拥有者:", statbuf.st_uid);//拥有者 printf("%*s%d\t", depth, " 组ID:", statbuf.st_gid);//组名 printf("%*s%ld 字节\n", depth, " 文件大小:", statbuf.st_size);//文件大小 } exit(0); } if(S_ISDIR(statbuf.st_mode)){//如果是目录 if(params[a_PARAM] == false && (strcmp(entry->d_name, ".")==0 || strcmp(entry->d_name, "..")==0) ){ //不显示 . 和 .. 文件夹 continue; } printf("%*s%s/ \n", depth, " ", entry->d_name); //-i 显示文件和目录的索引节点号 if(params[i_PARAM] == true){ printf("%*s%ld\n", depth, " inode:", statbuf.st_ino); } //-l 除文件名称外,亦将文件型态、权限、拥有者、文件大小等资讯详细列出 if(params[l_PARAM] == true){ printf("%*s%d\t", depth, " 权限:", statbuf.st_mode);//权限 printf("%*s%d\t", depth, " 拥有者:", statbuf.st_uid);//拥有者 printf("%*s%d\t", depth, " 组ID:", statbuf.st_gid);//组名 printf("%*s%ld 字节\n", depth, " 文件大小:", statbuf.st_size);//文件大小 } //-R 若目录下有文件,则以下之文件亦皆递归依序列出 //若没有,直接跳出第一次循环即可 if(params[R_PARAM] == true){ printDir(entry->d_name, depth + 4, params); } }else{//一般文件 //如果没有 -a 则忽略以.开头的文件 if(params[a_PARAM] == false && (entry->d_name[0] == '.')){ continue; } printf("%*s%s \n", depth, " ", entry->d_name);//只列出名字 //-l 除文件名称外,亦将文件型态、权限、拥有者、文件大小等资讯详细列出 if(params[l_PARAM] == true){ printf("%*s%d\t", depth, " 权限:", statbuf.st_mode);//权限 printf("%*s%d\t", depth, " 拥有者:", statbuf.st_uid);//拥有者 printf("%*s%d\t", depth, " 组ID:", statbuf.st_gid);//组名 printf("%*s%ld 字节\n", depth, " 文件大小:", statbuf.st_size);//文件大小 } //-i 显示文件和目录的索引节点号 if(params[i_PARAM] == true){ printf("%*s%ld\n", depth, " inode:", statbuf.st_ino); } } } chdir(".."); closedir(destination_directory);}int main(int argc, char* argv[]){ bool params[PARAM_TYPE_NUM] = {false};//首先全部设为false handle_params(argc, argv, params, "alRdi::"); handle_ls_command(params, argv); exit(0);}]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
<tag>C语言</tag>
</tags>
</entry>
<entry>
<title><![CDATA[图形学--3D图形斜投影]]></title>
<url>%2F2018%2F10%2F28%2F%E5%9B%BE%E5%BD%A2%E5%AD%A6-3D%E5%9B%BE%E5%BD%A2%E6%96%9C%E6%8A%95%E5%BD%B1%2F</url>
<content type="text"><![CDATA[平行投影分为正投影和斜投影。当投影线与投影面不垂直,也就是说,投影线与投影面相倾斜时,所得到的物体的投影叫做斜投影。 利用矩阵变换同样可以得到3D图形几何变换,我将图形学–图形几何变换一文中的矩阵类进行修改,再加上重写了一个CPoint3D类,就可以简单实现3D图形的 斜等测轴测投影图 和 斜二测轴测投影图 。 使用时,直接调用函数 CPoint3D::cavalier_projection 和 CPoint3D::cabinet_projection 即可。 读者若有其他变换需求,只需模仿这两种变换,将变换矩阵的值进行修改即可。CPoint3D.h12345678910111213141516171819202122232425#pragma onceclass CPoint3D{public: CPoint3D(); ~CPoint3D(); CPoint3D(int x, int y, int z);private: int x; int y; int z; public: void setX(int value); int getX(); void setY(int value); int getY(); void setZ(int value); int getZ(); static void CPoint3DToCPoint(CPoint3D points_3d[], CPoint points[], int point_number); static void cavalier_projection(CPoint3D points_3d[], CPoint points[], int point_number);//斜等测轴测投影图 static void cabinet_projection(CPoint3D points_3d[], CPoint points[], int point_number); //斜二测轴测投影图}; CPoint3D.cpp123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081#include "stdafx.h"#include "CPoint3D.h"#include "MatrixTransformation3D.h"CPoint3D::CPoint3D(){ this->x = 0; this->y = 0; this->z = 0;}CPoint3D::~CPoint3D(){}CPoint3D::CPoint3D(int x, int y, int z){ this->x = x; this->y = y; this->z = z;}void CPoint3D::setX(int value){ this->x = value;}int CPoint3D::getX(){ return x;}void CPoint3D::setY(int value){ this->y = value;}int CPoint3D::getY(){ return y;}void CPoint3D::setZ(int value){ this->z = value;}int CPoint3D::getZ(){ return z;}void CPoint3D::CPoint3DToCPoint(CPoint3D points_3d[], CPoint points[], int point_number){ for(int i = 0; i < point_number; i++) { points[i].x = points_3d[i].getX(); points[i].y = points_3d[i].getY(); }}//斜等测轴测投影图void CPoint3D::cavalier_projection(CPoint3D points_3d[], CPoint points[], int point_number){ const double PI = 3.1415926535; MatrixTransformation3D matrix_transformation_3d(points_3d, point_number); matrix_transformation_3d.oblique_projection(PI / 4, PI / 4); //alpha = PI / 4, beta = PI / 4 matrix_transformation_3d.matrixTo3DPoint(points_3d, point_number); CPoint3DToCPoint(points_3d, points, point_number);}//斜二测轴测投影图void CPoint3D::cabinet_projection(CPoint3D points_3d[], CPoint points[], int point_number){ const double PI = 3.1415926535; MatrixTransformation3D matrix_transformation_3d(points_3d, point_number); matrix_transformation_3d.oblique_projection(63.4 * PI / 180, PI / 4); //alpha = 63.4 * PI / 180, beta = PI / 4 matrix_transformation_3d.matrixTo3DPoint(points_3d, point_number); CPoint3DToCPoint(points_3d, points, point_number);} MatrixTransformation3D.h1234567891011121314151617181920212223242526272829303132333435#pragma once#include "CPoint3D.h"class MatrixTransformation3D{public: MatrixTransformation3D(); ~MatrixTransformation3D();private: int row;//矩阵行数 int column;//矩阵列数 double* pointMatrix;//点矩阵 double transforMatrix[4][4];//变换矩阵public: MatrixTransformation3D(CPoint3D points[], int pointNumber);private: bool setMatrixElement(int row, int column, double value); bool setMatrixElement(int row, int column, double value, double matrix[]); double getMatrixElement(int row, int column); double getMatrixElement(int row, int column, double matrix[]); bool setTransforMatrixElement(int row, int column, double value); void matrixMultiplication(); int round(double value);public: CPoint3D* MatrixTransformation3D::matrixTo3DPoint(CPoint3D points[], int point_number);//矩阵转为点数组 void MatrixTransformation3D::oblique_projection(double alpha, double beta);//斜投影}; MatrixTransformation3D.cpp123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134#include "stdafx.h"#include "MatrixTransformation3D.h"MatrixTransformation3D::MatrixTransformation3D(){ row = 0; column = 0; pointMatrix = NULL;}MatrixTransformation3D::MatrixTransformation3D(CPoint3D points[], int pointNumber){ row = pointNumber; column = 4; pointMatrix = new double[row * column]; for (int i = 1; i <= row; i++) { setMatrixElement(i, 1, points[i - 1].getX()); setMatrixElement(i, 2, points[i - 1].getY()); setMatrixElement(i, 3, points[i - 1].getZ()); setMatrixElement(i, 4, 1); }}MatrixTransformation3D::~MatrixTransformation3D(){ delete[] pointMatrix;}bool MatrixTransformation3D::setMatrixElement(int row, int column, double value){ return setMatrixElement(row, column, value, pointMatrix);}bool MatrixTransformation3D::setMatrixElement(int row, int column, double value, double matrix[]){ int realIndex = (row - 1) * this->column + column - 1; if (realIndex < this->row * this->column && realIndex >= 0) { matrix[realIndex] = value; return true; } return false;}double MatrixTransformation3D::getMatrixElement(int row, int column){ return getMatrixElement(row, column, pointMatrix);}double MatrixTransformation3D::getMatrixElement(int row, int column, double matrix[]){ int realIndex = (row - 1) * this->column + column - 1; if (realIndex < this->row * this->column && realIndex >= 0) { return matrix[realIndex]; } return 0;}bool MatrixTransformation3D::setTransforMatrixElement(int row, int column, double value){ if (row <= this->row && row > 0 && column <= this->column && column > 0) { transforMatrix[row - 1][column - 1] = value; return true; } return false;}void MatrixTransformation3D::matrixMultiplication(){ double* result = new double[row * column]; double tempResult; for (int m = 0; m < row; m++) { for (int s = 0; s < column; s++) { tempResult = 0.0; //变量使用前初始化,否则结果具有不确定性 for (int n = 0; n < column; n++) { tempResult += getMatrixElement(m + 1, n + 1) * transforMatrix[n][s]; } setMatrixElement(m + 1, s + 1, tempResult, result); } } double* tempPointer = this->pointMatrix; this->pointMatrix = result; delete[] tempPointer;}int MatrixTransformation3D::round(double value){ return (int)(value + 0.5);}CPoint3D* MatrixTransformation3D::matrixTo3DPoint(CPoint3D points[], int point_number){ for (int i = 1; i <= point_number; i++) { points[i - 1].setX(round(getMatrixElement(i, 1))); points[i - 1].setY(round(getMatrixElement(i, 2))); points[i - 1].setZ(round(getMatrixElement(i, 3))); } return points;}//斜投影void MatrixTransformation3D::oblique_projection(double alpha, double beta){ setTransforMatrixElement(1, 1, 1); setTransforMatrixElement(1, 2, 0); setTransforMatrixElement(1, 3, 0); setTransforMatrixElement(1, 4, 0); setTransforMatrixElement(2, 1, 0); setTransforMatrixElement(2, 2, 1); setTransforMatrixElement(2, 3, 0); setTransforMatrixElement(2, 4, 0); setTransforMatrixElement(3, 1, -cos(beta) / tan(alpha)); setTransforMatrixElement(3, 2, -sin(beta) / tan(alpha)); setTransforMatrixElement(3, 3, 0); setTransforMatrixElement(3, 4, 0); setTransforMatrixElement(4, 1, 0); setTransforMatrixElement(4, 2, 0); setTransforMatrixElement(4, 3, 0); setTransforMatrixElement(4, 4, 1); matrixMultiplication();}]]></content>
<categories>
<category>图形学</category>
</categories>
<tags>
<tag>C++</tag>
<tag>图形学</tag>
<tag>几何变换</tag>
</tags>
</entry>
<entry>
<title><![CDATA[图形学--图形几何变换]]></title>
<url>%2F2018%2F10%2F21%2F%E5%9B%BE%E5%BD%A2%E5%AD%A6-%E5%9B%BE%E5%BD%A2%E5%87%A0%E4%BD%95%E5%8F%98%E6%8D%A2%2F</url>
<content type="text"><![CDATA[二维图形基本几何变换是指相对于坐标原点和坐标轴进行的几何变换,包括平移(Translate)、比例(Scale)、旋转(Rotate)、反射(Reflect)和错切(shear)5种变换。物体变换物体变换是通过变换物体上每一个顶点实现的,因此以点的二维基本几何变换为例讲解二维图形基本几何变换矩阵。 下面给出C++实现二维图形基本几何变换的具体代码。算法中只包含平移和旋转两种变换。使用时,直接调用函数translation 和 whirling 即可。读者若有其他变换需求,只需模仿这两种变换,将变换矩阵的值进行修改即可。 MatrixTransformation.h123456789101112131415161718192021222324252627282930313233class MatrixTransformation{public: MatrixTransformation(); ~MatrixTransformation(); private: int row;//矩阵行数 int column;//矩阵列数 double* pointMatrix;//点矩阵 double transforMatrix[3][3];//变换矩阵public: MatrixTransformation(CPoint points[], int pointNumber);private: bool setMatrixElement(int row, int column, double value); bool setMatrixElement(int row, int column, double value, double matrix[]); double getMatrixElement(int row, int column); double getMatrixElement(int row, int column, double matrix[]); bool setTransforMatrixElement(int row, int column, double value); void matrixMultiplication();public: CPoint* matrixToPoint(CPoint points[], int point_number);//矩阵转为点数组 void translation(int offset_x, int offset_y);//平移 void whirling(double angle);//旋转 void whirling(int offset_x, int offset_y, double angle);//绕点(offset_x, offset_y)旋转}; MatrixTransformation.cpp123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136MatrixTransformation::MatrixTransformation(){ row = 0; column = 0; pointMatrix = NULL;}MatrixTransformation::MatrixTransformation(CPoint points[], int pointNumber){ row = pointNumber; column = 3; pointMatrix = new double[row * column]; for(int i = 1; i <= row; i++) { setMatrixElement(i, 1, points[i - 1].x); setMatrixElement(i, 2, points[i - 1].y); setMatrixElement(i, 3, 1); }}MatrixTransformation::~MatrixTransformation(){ delete[] pointMatrix;}bool MatrixTransformation::setMatrixElement(int row, int column, double value){ return setMatrixElement(row, column, value, pointMatrix);}bool MatrixTransformation::setMatrixElement(int row, int column, double value, double matrix[]){ int realIndex = (row - 1) * this->column + column - 1; if (realIndex < this->row * this->column && realIndex >= 0) { matrix[realIndex] = value; return true; } return false;}double MatrixTransformation::getMatrixElement(int row, int column){ return getMatrixElement(row, column, pointMatrix);}double MatrixTransformation::getMatrixElement(int row, int column, double matrix[]){ int realIndex = (row - 1) * this->column + column - 1; if (realIndex < this->row * this->column && realIndex >= 0) { return matrix[realIndex]; } return 0;}bool MatrixTransformation::setTransforMatrixElement(int row, int column, double value){ if (row <= 3 && row > 0 && column <= 3 && column > 0) { transforMatrix[row - 1][column - 1] = value; return true; } return false;}void MatrixTransformation::matrixMultiplication(){ double* result = new double[row * column]; int tempResult; for (int m = 0; m < row; m++) { for (int s = 0; s < column; s++) { tempResult = 0; //变量使用前初始化,否则结果具有不确定性 for (int n = 0; n < column; n++) { tempResult += getMatrixElement(m + 1, n + 1) * transforMatrix[n][s]; } setMatrixElement(m + 1, s + 1, tempResult, result); } } double* tempPointer = this->pointMatrix; this->pointMatrix = result; delete[] tempPointer;}CPoint* MatrixTransformation::matrixToPoint(CPoint points[], int point_number){ for(int i = 1; i <= point_number; i++) { points[i - 1].x = getMatrixElement(i, 1); points[i - 1].y = getMatrixElement(i, 2); } return points;}void MatrixTransformation::translation(int offset_x, int offset_y){ setTransforMatrixElement(1, 1, 1); setTransforMatrixElement(1, 2, 0); setTransforMatrixElement(1, 3, 0); setTransforMatrixElement(2, 1, 0); setTransforMatrixElement(2, 2, 1); setTransforMatrixElement(2, 3, 0); setTransforMatrixElement(3, 1, offset_x); setTransforMatrixElement(3, 2, offset_y); setTransforMatrixElement(3, 3, 1); matrixMultiplication();}void MatrixTransformation::whirling(double angle){ setTransforMatrixElement(1, 1, cos(angle)); setTransforMatrixElement(1, 2, sin(angle)); setTransforMatrixElement(1, 3, 0); setTransforMatrixElement(2, 1, -sin(angle)); setTransforMatrixElement(2, 2, cos(angle)); setTransforMatrixElement(2, 3, 0); setTransforMatrixElement(3, 1, 0); setTransforMatrixElement(3, 2, 0); setTransforMatrixElement(3, 3, 1); matrixMultiplication();}void MatrixTransformation::whirling(int offset_x, int offset_y, double angle){ translation(-offset_x, -offset_y); whirling(angle); translation(offset_x, offset_y);}]]></content>
<tags>
<tag>C++</tag>
<tag>算法</tag>
<tag>图形学</tag>
<tag>几何变换</tag>
</tags>
</entry>
<entry>
<title><![CDATA[图形学--扫描线种子填充算法]]></title>
<url>%2F2018%2F10%2F15%2F%E5%9B%BE%E5%BD%A2%E5%AD%A6-%E6%89%AB%E6%8F%8F%E7%BA%BF%E7%A7%8D%E5%AD%90%E5%A1%AB%E5%85%85%E7%AE%97%E6%B3%95%2F</url>
<content type="text"><![CDATA[算法描述:种子填充算法原理和程序都很简单, 但由于多次递归, 费时、费内存, 效率不高。为了减少递归次数, 提高效率可以采用扫描线种子填充算法。 算法的基本过程如下: 当给定种子点 (x, y) 时, 首先填充种子点所在扫描线上的位于给定区域的一个区段, 然后确定与这一区段相连通的上、下两条扫描线上位于给定区域内的区段, 并依次保存下来。反复这个过程, 直到填充结束。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182/** * begin_point 是起始点 * boundaryColor 是边界色 * fillColor 是填充色 * */void scanLineSeedFillingAlgorithm(CPoint begin_point, COLORREF boundaryColor, COLORREF fillColor, CDC* pDC){ std::stack<CPoint> stack;//使用STL自带的栈容器 stack.push(begin_point);//将起点先入栈 CPoint* currentPoint = NULL;//当前操作的点 int x_left = 0, x_right = 0; while (!stack.empty())//栈不空时进行循环 { currentPoint = &stack.top();//访问栈顶元素 stack.pop();//删除栈顶元素 pDC->SetPixelV(*currentPoint, fillColor);//填充颜色 x_left = fillThisLine(currentPoint->x - 1, currentPoint->y, boundaryColor, fillColor, false, pDC) + 1; x_right = fillThisLine(currentPoint->x + 1, currentPoint->y, boundaryColor, fillColor, true, pDC) - 1; int tempY = currentPoint->y; searchNewLineSeed(&stack, x_left, x_right, tempY + 1, boundaryColor, fillColor, pDC); searchNewLineSeed(&stack, x_left, x_right, tempY - 1, boundaryColor, fillColor, pDC); }}/** * x 是该点横坐标 * y 是该点纵坐标 * boundaryColor 是边界色 * fillColor 是填充色 * sign是填充标志,当sign是false时,向左扫描;当sign是true时,向右扫描。 * */int fillThisLine(int x, int y, COLORREF boundaryColor, COLORREF fillColor, BOOL sign, CDC* pDC){ COLORREF currentColor = pDC->GetPixel(x, y); if (sign == false)//当sign是false时,向左扫描。 { while (currentColor != boundaryColor)//当当前颜色不是边界线颜色时进行循环,否则跳出循环。 { pDC->SetPixelV(x, y, fillColor);//向左填充颜色 currentColor = pDC->GetPixel(--x, y);//获取左侧颜色 } } else//当sign是true时,向右扫描。 { while (currentColor != boundaryColor)//当当前颜色不是边界线颜色时进行循环,否则跳出循环。 { pDC->SetPixelV(x, y, fillColor);//向右填充颜色 currentColor = pDC->GetPixel(++x, y);//获取右侧颜色 } } return x;}/** * stack是栈指针 * x_left 是左边界 * x_right 是右边界 * y 是当前扫描线 * boundaryColor 是边界色 * fillColor 是填充色 */ void searchNewLineSeed(std::stack<CPoint> *stack, int x_left, int x_right, int y, COLORREF boundaryColor, COLORREF fillColor, CDC* pDC){ BOOL findNewSeed = false; for(; x_left <= x_right; x_left++) { if(pDC->GetPixel(x_left, y) != boundaryColor && pDC->GetPixel(x_left, y) != fillColor) { findNewSeed = true; break; } } if(findNewSeed) { stack->push(CPoint(x_left, y)); }}]]></content>
<tags>
<tag>C++</tag>
<tag>算法</tag>
<tag>图形学</tag>
<tag>填充算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[图形学--区域填充算法]]></title>
<url>%2F2018%2F10%2F15%2F%E5%9B%BE%E5%BD%A2%E5%AD%A6-%E5%8C%BA%E5%9F%9F%E5%A1%AB%E5%85%85%E7%AE%97%E6%B3%95%2F</url>
<content type="text"><![CDATA[算法描述:区域填充是指从区域内的某一个象素点(种子点)开始,由内向外将填充色扩展到整个区域内的过程。区域是指已经表示成点阵形式的填充图形,它是相互连通的一组像素的集合。 区域填充算法(边界填充算法和泛填充算法)是根据区域内的一个已知象素点(种子点)出发,找到区域内其他象素点的过程,所以把这一类算法也成为种子填充算法。 下面给出八连通区域填充算法(四连通区域填充算法类似)123456789101112131415161718192021222324252627282930313233343536373839404142/** * begin_point 是起始点 * boundaryColor 是边界色 * fillColor 是填充色 * */ void eightAdjacentPointFillingAlgorithm(CPoint begin_point, COLORREF boundaryColor, COLORREF fillColor, CDC* pDC){ std::stack<CPoint> stack;//使用STL自带的栈容器 stack.push(begin_point);//将起点先入栈 CPoint* currentPoint = NULL;//当前操作的点 while(!stack.empty())//栈不空时进行循环 { currentPoint = &stack.top();//访问栈顶元素 stack.pop();//删除栈顶元素 pDC->SetPixelV(*currentPoint, fillColor);//填充颜色 isFilling(&stack, currentPoint->x - 1, currentPoint->y, boundaryColor, fillColor, pDC);//左侧点 isFilling(&stack, currentPoint->x - 1, currentPoint->y - 1, boundaryColor, fillColor, pDC);//左上点 isFilling(&stack, currentPoint->x, currentPoint->y - 1, boundaryColor, fillColor, pDC);//上侧点 isFilling(&stack, currentPoint->x + 1, currentPoint->y + 1, boundaryColor, fillColor, pDC);//右上点 isFilling(&stack, currentPoint->x + 1, currentPoint->y, boundaryColor, fillColor, pDC);//右侧点 isFilling(&stack, currentPoint->x + 1, currentPoint->y - 1, boundaryColor, fillColor, pDC);//右下点 isFilling(&stack, currentPoint->x, currentPoint->y - 1, boundaryColor, fillColor, pDC);//下侧点 isFilling(&stack, currentPoint->x - 1, currentPoint->y - 1, boundaryColor, fillColor, pDC);//左下点 }}/** * stack 是栈指针 * boundaryColor 是边界色 * fillColor 是填充色 */ void isFilling(std::stack<CPoint> *stack, int x, int y, COLORREF boundaryColor, COLORREF fillColor, CDC* pDC){ COLORREF currentColor = pDC->GetPixel(x, y);;//获得当前操作点颜色 if (currentColor != boundaryColor && currentColor != fillColor) { stack->push(CPoint(x, y)); }}]]></content>
<tags>
<tag>C++</tag>
<tag>算法</tag>
<tag>图形学</tag>
<tag>填充算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[图形学--边缘填充算法]]></title>
<url>%2F2018%2F10%2F15%2F%E5%9B%BE%E5%BD%A2%E5%AD%A6-%E8%BE%B9%E7%BC%98%E5%A1%AB%E5%85%85%E7%AE%97%E6%B3%95%2F</url>
<content type="text"><![CDATA[算法描述:边缘填充算法是先求出多边形的每条边与扫描线的交点,然后将交点右侧的所有像素颜色全部取为补色(或反色)。按任意顺序处理完多边形的所有边后,就完成了多边形的填充任务。 边缘填充算法利用了图像处理中的求“补”或求“反”的概念,对于黑白图像,求补就是把RGB(255,255,255)(白色)的像素置为RGB(0,0,0)(黑色),反之亦然;对于彩色图像,求补就是将背景色置为填充色,反之亦然。求补的一条基本性质是一个像素求补两次就恢复为原色。如果多边形内部的像素被求补偶数次,保持原色,如果被求补奇数次,显示填充色。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374/** * points 是点数组 * point_num 是点的个数 * foregroundColor 是前景色 * backgroundColor 是背景色 * fence 是栅栏 * */void edgeFillingAlgorithm(CPoint points[], int point_num, COLORREF foregroundColor, COLORREF backgroundColor, int fence, CDC *pDC){ int yMin, yMax;//边的最小y和最大y //x,y为当前点x、y坐标,reciprocalOfSlope为斜率分之一,1/k double x, reciprocalOfSlope; int y; for (int i = 0; i < point_num; i++) { int j = (i + 1) % point_num;//j为相对于i的下一个点 if (points[i].y - points[j].y != 0) { reciprocalOfSlope = (points[i].x - points[j].x) * 1.0 / (points[i].y - points[j].y); //TODO:以下处理,得到每条边的y的最大值和最小值 if (points[i].y < points[j].y)//斜率不为0时 { yMin = points[i].y; yMax = points[j].y; x = points[i].x; } else { yMin = points[j].y; yMax = points[i].y; x = points[j].x; } for (y = yMin; y < yMax; y++)//沿每条扫描线处理 { //对每条扫描线与边的交点的右侧像素循环,其中max_X是包围圈的右边界 if(x < fence) { for (int tempX = Round(x); tempX < fence; tempX++) { if (pDC->GetPixel(tempX, y) == foregroundColor) { pDC->SetPixelV(tempX, y, backgroundColor); } else { pDC->SetPixelV(tempX, y, foregroundColor); } } } else { for (int tempX = fence; tempX < x; tempX++) { if (pDC->GetPixel(tempX, y) == foregroundColor) { pDC->SetPixelV(tempX, y, backgroundColor); } else { pDC->SetPixelV(tempX, y, foregroundColor); } } } x = x + reciprocalOfSlope; } } else //斜率为0时 { } }}]]></content>
<tags>
<tag>C++</tag>
<tag>算法</tag>
<tag>图形学</tag>
<tag>填充算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[图形学--有效边表填充算法]]></title>
<url>%2F2018%2F10%2F15%2F%E5%9B%BE%E5%BD%A2%E5%AD%A6-%E6%9C%89%E6%95%88%E8%BE%B9%E5%A1%AB%E5%85%85%E7%AE%97%E6%B3%95%2F</url>
<content type="text"><![CDATA[算法描述:有效边表填充算法通过维护边表和有效边表,避开了扫描线与多边形所有边求交的复杂运算。 填充原理是按照扫描线从小到大的移动顺序,计算当前扫描线与有效边的交点,然后把这些交点按x值递增的顺序进行排序、配对,以确定填充区间,最后用指定颜色填充区间内的所有像素,即完成填充工作。有效边表填充算法已成为目前最为有效的多边形填充算法之一。 Edge.h12345678910111213141516#pragma onceclass Edge{public: Edge(); ~Edge(); Edge(double x, int yMax, double reciprocalOfSlope, Edge* pNext); void setEdge(double x, int yMax, double reciprocalOfSlope, Edge* pNext); void reUseEdge();public: double x; int yMax; double reciprocalOfSlope;//斜率分之一,1/k Edge* pNext;}; Edge.cpp1234567891011121314151617181920212223242526272829303132333435363738#include "stdafx.h"#include "Edge.h"Edge::Edge(){ x = 0.0; yMax = 0; reciprocalOfSlope = 0.0; pNext = NULL;}Edge::~Edge(){}Edge::Edge(double x, int yMax, double reciprocalOfSlope, Edge* pNext){ this->x = x; this->yMax = yMax; this->reciprocalOfSlope = reciprocalOfSlope; this->pNext = pNext;}void Edge::setEdge(double x, int yMax, double reciprocalOfSlope, Edge* pNext){ this->x = x; this->yMax = yMax; this->reciprocalOfSlope = reciprocalOfSlope; this->pNext = pNext;}void Edge::reUseEdge(){ this->x = this->x + this->reciprocalOfSlope; this->pNext = NULL;} Bucket.h12345678910111213141516171819#pragma once#include "Edge.h"class Bucket{public: Bucket(); ~Bucket(); Bucket(int scanLine, Edge* pEdge, Bucket* pNext); static Bucket* creatBucket(CPoint points[], int points_num); static Bucket* creatEdgeTable(CPoint points[], Edge edges[], int points_num, Bucket* headBucket); static void addEdge(Bucket* currentBucket, Edge* edge);public: int scanLine; Edge* pEdge; Bucket* pNext;}; Bucket.cpp123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116#include "stdafx.h"#include "Bucket.h"Bucket::Bucket(){ scanLine = 0; pEdge = NULL; pNext = NULL;}Bucket::~Bucket(){}Bucket::Bucket(int scanLine, Edge* pEdge, Bucket* pNext){ this->scanLine = scanLine; this->pEdge = pEdge; this->pNext = pNext;}Bucket* Bucket::creatBucket(CPoint points[], int points_num){ if (points_num == 0) { return NULL; } int scanMin = points[0].y, scanMax = points[0].y;//确定扫描线的最小值和最大值。初始值使用第一个点的y值。 for (int i = 1; i < points_num; i++) { if (points[i].y < scanMin) { scanMin = points[i].y;//扫描线的最小值 } if (points[i].y > scanMax) { scanMax = points[i].y;//扫描线的最大值 } } //TODO: 以下是创建桶的代码,此时桶上不加入边 Bucket *headBucket = new Bucket(scanMin, NULL, NULL);//建立桶的头结点 Bucket *currentBucket = headBucket; for (int i = scanMin + 1; i <= scanMax; i++)//建立桶的其它结点 { currentBucket->pNext = new Bucket(i, NULL, NULL);//新建一个桶结点 currentBucket = currentBucket->pNext;//使currentBucket指向新建的桶结点 } return headBucket;}Bucket* Bucket::creatEdgeTable(CPoint points[], Edge edges[], int points_num, Bucket* headBucket){ Bucket *currentBucket = NULL; Edge *currentEdge = NULL; for (int i = 0; i < points_num; i++)//访问每个顶点 { int j = (i + 1) % points_num;//边的第二个顶点,points[i]和points[j]构成一条边 currentBucket = headBucket;//从桶链表的头结点开始循环 if (points[j].y > points[i].y)//若终点比起点高 { while (currentBucket->scanLine != points[i].y)//在桶内寻找该边的yMin { currentBucket = currentBucket->pNext;//移到下一个桶结点 }//跳出循环时,找到对应的桶,即为currentBucket edges[i].setEdge(points[i].x, points[j].y, (points[j].x - points[i].x) * 1.0 / (points[j].y - points[i].y), NULL);//将该边记入数组edges中 addEdge(currentBucket, &edges[i]);//将该边加入桶中 } if (points[j].y < points[i].y)//终点比起点低 { while (currentBucket->scanLine != points[j].y)//在桶内寻找该边的yMin { currentBucket = currentBucket->pNext;//移到下一个桶结点 }//跳出循环时,找到对应的桶,即为currentBucket edges[i].setEdge(points[j].x, points[i].y, (points[i].x - points[j].x) * 1.0 / (points[i].y - points[j].y), NULL);//将该边记入数组edges中 addEdge(currentBucket, &edges[i]);//将该边加入桶中 } } return headBucket;}//按照x的大小顺序将边加入桶中void Bucket::addEdge(Bucket* currentBucket, Edge* edge){ if (currentBucket->pEdge == NULL)//若当前桶结点上没有链接边结点 { currentBucket->pEdge = edge;//第一个边结点直接连接到对应的桶中 } else if(edge->x < currentBucket->pEdge->x)//比首结点小时 { Edge *tempEdge = currentBucket->pEdge; currentBucket->pEdge = edge; edge->pNext = tempEdge; } else //如果当前边已连有边结点 { Edge *currentEdge = currentBucket->pEdge; while (currentEdge->pNext) { if(currentEdge->pNext->x >= edge->x && currentEdge->x < edge->x)//currentEdge的x比该边x小 且currentEdge下一个边比比该边x大时插入 { Edge* tempEdge = currentEdge->pNext; currentEdge->pNext = edge; edge->pNext = tempEdge; return; } currentEdge = currentEdge->pNext; } //跳出循环时,说明edge->x比最后一个结点都大,所以放在最后 currentEdge->pNext = edge; edge->pNext = NULL; }} main.cpp1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556/** * headBucket 是桶表头节点 * colorref 是填充颜色 */ void effectiveEdgeTableFillingAlgorithm(Bucket* headBucket ,COLORREF colorref, CDC *pDC){ Edge *currentEdge = NULL, *tempEdge = NULL; Bucket* currentBucket = headBucket;//当前操作的桶,从第一个开始 Bucket* tempBucket = NULL; //访问所有桶结点 while(currentBucket->pNext) //for(currentBucket = headBucket; currentBucket != NULL; currentBucket = currentBucket->pNext) { currentEdge = currentBucket->pEdge;//首先currentEdge指向当前扫描线的第一个边 //TODO: 以下是当前桶的绘图的代码。 bool in = true; //设置一个bool变量in,初始值为真,用于判断当前选择像素点是否在图形内部 tempEdge = currentEdge; while(tempEdge->pNext) { if (in)//若在内部则绘图 { for (double x = tempEdge->x; x < tempEdge->pNext->x; x++) { pDC->SetPixelV(x, currentBucket->scanLine, colorref); //Sleep(1); } } in = !in;//in取反 tempEdge = tempEdge->pNext; } //TODO:以下是处理下个桶的代码。即将当前桶的边处理,得出下个桶的有效边,并插入下个桶中,同时释放当前桶的资源。 //遍历该桶 while(currentEdge) { if(currentEdge->yMax > currentBucket->scanLine + 1) //若满足加入条件,则将该结点进行修改并按顺序插入下一个桶 { tempEdge = currentEdge; currentEdge = currentEdge->pNext; tempEdge->reUseEdge();//修改信息 Bucket::addEdge(currentBucket->pNext, tempEdge);//插入下个桶 } else//否则释放该边的资源 { tempEdge = currentEdge; currentEdge = currentEdge->pNext; //if(tempEdge != NULL){ delete tempEdge; } } } tempBucket = currentBucket; currentBucket = currentBucket->pNext; delete tempBucket; }}]]></content>
<categories>
<category>图形学</category>
</categories>
<tags>
<tag>算法</tag>
<tag>图形学</tag>
<tag>填充算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[大类分流软件设计]]></title>
<url>%2F2018%2F06%2F30%2F%E5%A4%A7%E7%B1%BB%E5%88%86%E6%B5%81%E8%BD%AF%E4%BB%B6%E8%AE%BE%E8%AE%A1%2F</url>
<content type="text"><![CDATA[大类分流基本思想是将6个文件模拟为一个按绩点顺序存放的队列,按排行和志愿顺序对队首元素分配专业。 将队首元素按志愿顺序映射为一个数组。对队首每个元素,如果当前志愿的专业人数达到人数限制,则判断下一个志愿。如果所有志愿人数都已满,则放入另一个未处理学生的队列中,等一轮分配完再处理。下面是具体算法。 已知n个学生,每个学生最多5个志愿。已知6个专业的人数限制。 将6个专业对应的文件读入6个存放学生信息的队列中; 设计数器 count.每个队首的绩点排名等于count弹出,弹出的元素根据志愿顺序映射到长度为6的数组对应的位置,count加上弹出的学生的人数;绩点排行小于count说明已分配过。则直接弹出 根据数组顺序来分配专业。如果当前志愿人数未满,写入结果数组,对应专业当前人数+1;人数已满则判断下一个志愿;所有志愿人数都满则排入未处理学生的队列。 最后处理未处理学生的队列,将他们分配到当前专业人数未满的专业即可。 需要注意注意的是第二部分,有可能有绩点排行相同的情况,所做的处理是把弹出的数组处理为一个链表数组。排名相同的学生按专业顺序插入到对应数组对应位置的链表中去。处理时仍然按次序处理即可。 这个算法的关键是注意到6个文件本身是排好序的,所以并没有将所有文件合并为一个有序序列,而是通过处理来模拟达到队列的效果。 处理的关键是每次“只处理6个文件的一行中绩点最高的学生的分配”,提高效率的关键是将绩点最高的学生的信息按志愿顺序映射存储再分配,因为是按志愿顺序处理。这样排名靠前的学生处理效率将大大提高,排名靠后的学生如果是根据自身情况选择合适的志愿,处理效率也会提高。 这儿是测试得到的结果 项目已经托管到 GitHub,戳我 。]]></content>
<categories>
<category>数据结构</category>
</categories>
<tags>
<tag>Java</tag>
<tag>数据结构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Markdown文本转为Html]]></title>
<url>%2F2018%2F06%2F17%2FMarkdown%E6%96%87%E6%9C%AC%E8%BD%AC%E4%B8%BAHtml%2F</url>
<content type="text"><![CDATA[利用Java语言,使用正则表达式借助编译原理的知识,制作出一个简易的将Markdown文本转为Html的程序。 网页中不方便代码展示及维护,故托管到Github,请移步 这里。 已经支持中文!~由于没有进行编码和解码处理,目前暂时只支持英文的转化。~ 支持的操作: 标题 分割线 有序列表和无序列表 斜体 加粗 删除线 标记 图片链接 网址链接和邮箱链接 不支持的操作: 引用片段 代码片段 表格 其他罕见功能 (持续更新)]]></content>
<categories>
<category>编译技术</category>
</categories>
<tags>
<tag>编译原理</tag>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[C++实现简单计算器]]></title>
<url>%2F2018%2F05%2F12%2FC-%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95%E8%AE%A1%E7%AE%97%E5%99%A8%2F</url>
<content type="text"><![CDATA[利用编译原理中递归下降的 语法分析 和 语义分析 ,使用 C++ 实现简单计算器。 文法如下: EXP -> TERM {AlphaOP TERM} TERM -> FACTOR {BetaOP FACTOR} FACTOR -> (EXP) | number AlphaOP -> + | - BetaOP -> * | \ 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124#include <iostream>#include <string>#include <sstream>using namespace std;/* * EXP -> TERM {AlphaOP TERM} * TERM -> FACTOR {BetaOP FACTOR} * FACTOR -> (EXP) | number * AlphaOP -> + | - * BetaOP -> * | \ */string inputExp;string::iterator ite;void handleError();void matchChar(char token);double matchDigit();double factor();double term();double expression();//错误处理void handleError() { cerr << "Error expression ! " + inputExp <<endl; exit(-1);}//匹配字符void matchChar(char token) { if (*ite == token) { ite++; } else { handleError(); }}//匹配数字double matchDigit() { string tempString = ""; while (isdigit(*ite)) { tempString += *ite; ite++; } istringstream iss(tempString); double temp; iss >> temp; return temp;}double factor() { double temp; if (isdigit(*ite)) { temp = matchDigit(); } else if (*ite == '(') { matchChar('('); //处理负数 if(*ite == '-'){ ite++; temp = matchDigit() * (-1); } else{ temp = expression(); } matchChar(')'); } else { handleError(); } return temp;}double term() { double temp = factor(); while (*ite == '*' || *ite == '/') { if (*ite == '*') { matchChar('*'); temp = temp * factor(); } else { matchChar('/'); temp = temp / factor(); } } return temp;}double expression() { double temp = term(); while (*ite == '+' || *ite == '-') { if (*ite == '+') { matchChar('+'); temp = temp + factor(); } else { matchChar('-'); temp = temp - factor(); } } return temp;}int main() { double result = 0; cout << "请输入表达式:"; cin >> inputExp; //加上空格防止迭代器溢出 inputExp += " "; ite = inputExp.begin(); while (*ite != inputExp.back()) { result = expression(); } cout << result << endl; system("pause"); return 0;}]]></content>
<categories>
<category>编译技术</category>
</categories>
<tags>
<tag>编译原理</tag>
<tag>C++</tag>
</tags>
</entry>
<entry>
<title><![CDATA[OperateFile-LAN-Java]]></title>
<url>%2F2018%2F04%2F21%2FOperateFile-LAN-Java%2F</url>
<content type="text">< 。 (持续更新) 预计于2018年6月份交付。 已交付。]]></content>
<categories>
<category>分布式</category>
</categories>
<tags>
<tag>Java</tag>
<tag>RMI</tag>
</tags>
</entry>
<entry>
<title><![CDATA[链表实现简单列车查询系统]]></title>
<url>%2F2018%2F03%2F31%2F%E9%93%BE%E8%A1%A8%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95%E5%88%97%E8%BD%A6%E6%9F%A5%E8%AF%A2%E7%B3%BB%E7%BB%9F%2F</url>
<content type="text"><![CDATA[利用C++链表知识实现简单的列车查询系统。 设计目的综合运用链表知识解决实际问题的能力。 设计内容设计火车售票处的计算机系统,可以为客户提供下列各项服务: 查询列车信息:根据旅客提出的起始站和终点站名,或者列车车次,输出下列信息:列车车次、发车时刻、到达时刻、运行时间,以及每个途经站点的站名、到达时间、发车时间、运行里程等信息; 录入列车信息; 修改列车信息; 删除列车信息; 浏览所有列车信息; 其它必要功能。 设计要求 要求采用链表方式存储所有列车车次基本信息(如车次名称等),对于其中的每个列车车次,也采用链表方式存储各个途经站点信息; 能够支持查询、修改、增加、删除等信息; 如有时间,建议提供保存和打开功能,用户可以把所有信息保存到硬盘文件上,也可以从硬盘文件上读取信息; 界面友好。 代码实现station.h123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869#pragma once#include <string>#include <sstream>using std::ostream;using std::string;using std::stringstream;//string类型的时间转换为int型分钟inline int timeToMinute(string aTime){ string::iterator ite = aTime.begin(); int hour, minute; while(ite != aTime.end()){ if(*ite == ':'){ stringstream hourStream(string(aTime.begin(), ite)); hourStream >> hour; stringstream minuteStream(string(ite + 1, aTime.end())); minuteStream >> minute; return (hour * 60 + minute); } ite++; } return 0;}class Station{ public: //有参构造函数 Station(string _stationName = "未命名站点", string _arrivalTime = "", string _leaveTime = ""){ stationName = _stationName; arrivalTime = _arrivalTime; leaveTime = _leaveTime; pNextStation = NULL; } //复制构造函数 Station(Station &aStation){ this->stationName = aStation.stationName; this->arrivalTime = aStation.arrivalTime; this->leaveTime = aStation.leaveTime; this->pNextStation = aStation.pNextStation; } //析构函数 ~Station(){} //重载输出运算符 friend ostream & operator<<(ostream &os, const Station &aStation){ os << aStation.stationName << '\t' << aStation.arrivalTime << '\t' << aStation.leaveTime; return os; } //计算此站出发到下站抵达经历的时间 int calTime(){ if(this->pNextStation == NULL){ return 0; } else { int duringTime = timeToMinute(this->pNextStation->arrivalTime) - timeToMinute(this->leaveTime); return (duringTime > 0) ? duringTime : duringTime + 24 * 60; } } string stationName; //站名 string arrivalTime; //到达时间 string leaveTime; //出发时间 Station* pNextStation; //下一个站点}; train.h12345678910111213141516171819202122232425262728293031323334353637383940414243#pragma once#include "station.h"#include <vector>using std::vector;class Train{ public: //构造函数 Train(string _trainName = "未命名列车"){ trainName = _trainName; stationNum = 0; headStation = NULL; pNextTrain = NULL; } //析构函数 ~Train(); //添加站点 void insertStation(int locate, string _stationName = "未命名站点", string _arrivalTime = "未定义时间", string _leaveTime = "未定义时间"); //键盘添加站点 void keyboardInsertStation(); //删除站点 void deleteStation(); //改变站点信息 void changeStation(); //根据站点名称查询站点信息 void searchStationForName(); //打印该列车经过所有站点信息 void showAllStation(); //求列车经过站点个数 int getLength(); //重载运算符 friend ostream & operator<<(ostream &os, const Train &aTrain); //更新运行里程和时间 void updateInfo(); string trainName; //列车车次 int stationNum; //站点个数 Station* headStation; //头站点 Train* pNextTrain; //下一列列车 //vector <int> mileag; //运行里程 vector <int> time; //运行时间}; train.cpp123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211#include "train.h"#include <iostream>using std::cout;using std::endl;using std::cin;//更新运行里程和时间void Train::updateInfo(){ Station *myStation = this->headStation; //清空容器 this->time.clear(); while(myStation){ //插入时间 this->time.push_back(myStation->calTime()); myStation = myStation->pNextStation; }}//重载运算符ostream & operator<<(ostream &os, const Train &aTrain) { Station *p = aTrain.headStation; while(p){ os << aTrain.trainName << '\t' << *p << '\t' << p->calTime() <<endl; p = p->pNextStation; } return os;}//求列车站点长度int Train::getLength(){ return this->stationNum;}//添加站点void Train::insertStation(int locate, string _stationName, string _arrivalTime, string _leaveTime){ if (locate > this->stationNum + 1 || locate < 1) { cout << "ERROR:超出行程表范围!" << endl; } else if (this->headStation != NULL && locate > 1) { //new一个新站 Station* aStation = new Station(_stationName, _arrivalTime, _leaveTime); //将新站加入列车行程表中 Station* p = this->headStation; while (locate - 2) { p = p->pNextStation; locate--; } aStation->pNextStation = p->pNextStation; p->pNextStation = aStation; this->stationNum++; this->updateInfo(); cout << "加入新站成功!" << endl; } else if (this->headStation != NULL && locate == 1){ //new一个新站 Station* aStation = new Station(_stationName, _arrivalTime, _leaveTime); //将新站加入列车行程表中 Station* p = this->headStation; this->headStation = aStation; aStation->pNextStation = p; this->stationNum++; this->updateInfo(); cout << "加入新站成功!" << endl; } else { //new一个新站 Station* aStation = new Station(_stationName, _arrivalTime, _leaveTime); //将新站加入列车行程表中 this->headStation = aStation; this->stationNum++; this->updateInfo(); cout << "加入新站成功!" << endl; }}//键盘增加站点void Train::keyboardInsertStation(){ int locate; cout << "请输入插入的位置:"; cin >> locate; string _stationName, _arrivalTime, _leaveTime; cout << "请分别输入 站点名称 到达时间 出发时间 :" << endl; cin >> _stationName >> _arrivalTime >> _leaveTime; this->insertStation(locate, _stationName, _arrivalTime, _leaveTime);}//删除站点void Train::deleteStation(){ int locate; cout << "请输入插入的位置:"; cin >> locate; if(locate > this->stationNum || locate < 1){ cout << "ERROR:超出行程表范围!" << endl; }else if(this->headStation != NULL && locate > 1){ Station* p = this->headStation; while(locate - 2){ //到达第i-1个结点,p->pNextStation为欲删除的结点的指针 p = p->pNextStation; locate--; } Station* q = p->pNextStation; p->pNextStation = p->pNextStation->pNextStation; delete q; this->stationNum--; this->updateInfo(); cout << "删除站点完成!" << endl; } else if(this->headStation != NULL && locate == 1){ Station* p = this->headStation; this->headStation = p->pNextStation; delete p; this->stationNum--; this->updateInfo(); cout << "删除站点完成!" << endl; } else { cout << "列车站台为空!" << endl; }}//析构函数Train::~Train(){ if(this->headStation == NULL){ } else{ Station* p = this->headStation; Station* q; while (p){ q = p; p = p->pNextStation; delete q; } }}//改变站点信息void Train::changeStation(){ int locate; cout << "请输入需要改变站台所在的位置:"; cin >> locate; if(locate > this->stationNum || locate < 1){ cout << "ERROR:超出行程表范围!" << endl; return; }else{ Station* p = this->headStation; while(locate - 1){ p = p->pNextStation; locate--; } cout << *p << endl; cout << "1. 站点名称\n2. 列车到达时间\n3. 列车出发时间\n"; cout << "站点已找到,请选择改变的信息:"; int choose; string changeValue; cin >> choose; switch(choose){ case 1: cout << "请输入新的站名:"; cin >> changeValue; p->stationName = changeValue; cout << "修改完成!" << endl; break; case 2: cout << "请输入新的到达时间:"; cin >> changeValue; p->arrivalTime = changeValue; cout << "修改完成!" << endl; this->updateInfo(); break; case 3: cout << "请输入新的出发时间:"; cin >> changeValue; p->leaveTime = changeValue; cout << "修改完成!" << endl; this->updateInfo(); break; default: cout << "未更改数据,请输入正确的数字!" << endl; } }}//根据站点名称查询站点信息void Train::searchStationForName() { string _stationName; cout << "请输入要查询的站名:" ; cin >> _stationName; Station* p = this->headStation; if (p == NULL) { cout << "行程表为空!" << endl; } int count = 1; //计数器 while (p) { if (p->stationName == _stationName) { cout << "该站点为行程表第" << count << "个站点\n站点名称为:" << p->stationName << "\n列车到达时间为:" << p->arrivalTime << "\n列车出发时间为:" << p->leaveTime << endl; } count++; p = p->pNextStation; } cout << "未找到该站点的信息!" << endl;}//打印该列车所有站点void Train::showAllStation() { cout << this->trainName << " 的行程表为:" << endl; cout << "列车信息 站点名称 到站时间 出发时间 到达下站所需时间(min)" << endl; cout << *this << endl;} schedule.h12345678910111213141516171819202122232425262728293031323334353637383940414243#pragma once#include "train.h"class Schedule{ public: //构造函数 Schedule(string _scheduleName = "未命名时刻表"){ scheduleName = _scheduleName; trainNum = 0; headTrain = NULL; } //析构函数 ~Schedule(); //添加列车 Train* insertTrain(int locate, string _trainName = "未命名列车"); //键盘添加站点 void keyboardInsertTrain(); //文本添加站点 void txtInsertTrain(string filename); //文本输出站点 void scheduleToTxt(string filename); //删除列车 void deleteTrain(); //改变列车站点信息 void changeTrainStation(); //改变列车名称 void changeTrainName(); //根据列车名称查询站点信息 Train* searchTrainForName(); //打印所有列车经过所有站点信息 void showAllTrain(); //求列车个数 int getLength(); //操作菜单 void operateTheSchedule(); //重载运算符 friend ostream & operator<<(ostream &os, const Schedule &aSchedule); private: string scheduleName; //列车车次 int trainNum; //站点个数 Train* headTrain; //头站点}; schedule.cpp123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334#include "schedule.h"#include <iostream>#include <fstream>using std::cout;using std::endl;using std::cin;using std::ifstream;using std::ofstream;//重载运算符ostream & operator<<(ostream &os, const Schedule &aSchedule) { Train *p = aSchedule.headTrain; while (p) { os << *p; p = p->pNextTrain; } return os;}//求列车长度int Schedule::getLength(){ return this->trainNum;}//添加列车Train* Schedule::insertTrain(int locate, string _trainName){ if (locate > this->trainNum + 1 || locate < 1) { cout << "ERROR:超出行程表范围!" << endl; return NULL; } else if (this->headTrain != NULL && locate > 1) { //new一个新列车 Train* aTrain = new Train(_trainName); //将新列车加入列车行程表中 Train* p = this->headTrain; while (locate - 2) { p = p->pNextTrain; locate--; } aTrain->pNextTrain = p->pNextTrain; p->pNextTrain = aTrain; this->trainNum++; cout << "加入新列车成功!" << endl; return aTrain; } else if(this->headTrain != NULL && locate == 1){ //new一个新列车 Train* aTrain = new Train(_trainName); //将新列车加入列车行程表中 Train* p = this->headTrain; this->headTrain = aTrain; aTrain->pNextTrain = p; this->trainNum++; cout << "加入新列车成功!" << endl; return aTrain; } else { //new一个新列车 Train* aTrain = new Train(_trainName); //将新站加入列车行程表中 this->headTrain = aTrain; this->trainNum++; cout << "加入新列车成功!" << endl; return aTrain; }}//键盘增加列车void Schedule::keyboardInsertTrain(){ int locate; cout << "请输入插入的位置:"; cin >> locate; string _trainName; cout << "请输入列车名称:" ; cin >> _trainName; this->insertTrain(locate, _trainName);}//文本添加站点void Schedule::txtInsertTrain(string filename){ ifstream infile; infile.open(filename + ".txt"); if(!infile){ cout << "ERROR:打开 " << filename << ".txt 文件失败" << endl; return; } //_lastTrainName用于优化查找速度,但不可用于多次文本输入内存 string _trainName, _stationName, _arrivalTime, _leaveTime, _lastTrainName; if(this->headTrain == NULL){ _lastTrainName = ""; }else{ _lastTrainName = this->headTrain->trainName; } while(infile >> _trainName>> _stationName>>_arrivalTime>>_leaveTime){ if(_trainName == _lastTrainName){ Train *p = this->headTrain; while (p) { if (p->trainName == _trainName) { p->insertStation(p->stationNum+1, _stationName, _arrivalTime, _leaveTime); break; } p = p->pNextTrain; } } else { Train *p = this->insertTrain(this->trainNum+1, _trainName); p->insertStation(p->stationNum+1, _stationName, _arrivalTime, _leaveTime); _lastTrainName = _trainName; } } infile.close();}//文本输出信息void Schedule::scheduleToTxt(string filename){ ofstream outfile; outfile.open(filename + ".txt"); if (!outfile) { cout << "ERROR:打开 " << filename << ".txt 文件失败" << endl; return; } outfile << *this; outfile.close();}//删除站点void Schedule::deleteTrain(){ int locate; cout << "请输入删除的位置:"; cin >> locate; if(locate > this->trainNum || locate < 1){ cout << "ERROR:超出行程表范围!" << endl; }else if(this->trainNum != NULL && locate > 1){ Train* p = this->headTrain; while(locate - 2){ //到达第i-1个结点,p->pNextTrain为欲删除的结点的指针 p = p->pNextTrain; locate--; } Train* q = p->pNextTrain; p->pNextTrain = p->pNextTrain->pNextTrain; //删除列车的站点 while(q->stationNum){ Station* delSta; delSta = q->headStation; q->headStation = q->headStation->pNextStation; delete delSta; q->stationNum--; } //删除列车 delete q; this->trainNum--; cout << "删除列车完成!" << endl; } else if (this->trainNum != NULL && locate == 1){ Train* p = this->headTrain; this->headTrain = p->pNextTrain; //删除列车的站点 while (p->stationNum) { Station* delSta; delSta = p->headStation; p->headStation = p->headStation->pNextStation; delete delSta; p->stationNum--; } //删除列车 delete p; this->trainNum--; cout << "删除列车完成!" << endl; } else{ cout << "行程表为空!" << endl; }}//析构函数Schedule::~Schedule(){ if (this->headTrain == NULL) { }else { Train* p = this->headTrain; Train* q; while (p) { q = p; p = p->pNextTrain; delete q; } }}//改变列车信息void Schedule::changeTrainStation(){ Train* p = this->searchTrainForName(); if(p != NULL){ while (1) { cout << " ******************************************************" << endl; cout << " * 列车查询系统子菜单 *" << endl; cout << " * *" << endl; cout << " * 您现在正在操作 " << p->trainName << " 列车 *" << endl; cout << " * 请输入对应功能的数字完成操作 *" << endl; cout << " * 1.添加站点信息 *" << endl; cout << " * 2.删除站点信息 *" << endl; cout << " * 3.修改站点信息 *" << endl; cout << " * 0.退出子菜单 *" << endl; cout << " * *" << endl; cout << " * 列车 "<< p->trainName << " 现有站台 "<<p->getLength() <<" 个 *" << endl; cout << " ******************************************************" << endl; int choice; cout << "请输入您的选项:"; cin >> choice; switch (choice) { case 1: p->keyboardInsertStation(); break; case 2: p->deleteStation(); break; case 3: p->changeStation(); break; case 0: return; default: cout << "输入选项非法!请重新输入!" << endl; break; } } }}//查询列车具体信息Train* Schedule::searchTrainForName() { string _trainName; cout << "请输入要查询的列车名:" ; cin >> _trainName; Train* p = this->headTrain; if (p == NULL) { cout << "行程表为空!" << endl; return p; } int count = 1; //计数器 while (p) { if (p->trainName == _trainName) { cout << *p << endl; return p; } count++; p = p->pNextTrain; } cout << "未找到该列车的信息!" << endl; return p;}//改变列车名称void Schedule::changeTrainName(){ Train* p = this->searchTrainForName(); if (p != NULL) { cout << "列车已找到,请输入新的列车名:"; string _trainName; cin >> _trainName; p->trainName = _trainName; cout << "修改成功!" << endl; }}//打印所有列车所有站点void Schedule::showAllTrain() { cout << this->scheduleName << " 的行程表为:" << endl; cout << "列车信息\t站点名称\t到站时间\t出发时间" << endl; cout << *this;}//操作菜单void Schedule::operateTheSchedule() { while (true) { int choice; cout << " ******************************************************" << endl; cout << " * 列车查询系统 *" << endl; cout << " * *" << endl; cout << " * 请输入对应功能的数字完成操作 *" << endl; cout << " * 1.键盘添加列车信息 *" << endl; cout << " * 2.文本添加列车信息 *" << endl; cout << " * 3.删除列车信息 *" << endl; cout << " * 4.修改列车名称信息 *" << endl; cout << " * 5.修改列车站点具体信息 *" << endl; cout << " * 6.输出行程表信息 *" << endl; cout << " * 0.退出系统 *" << endl; cout << " * *" << endl; cout << " * 目前共有" << this->getLength() << "辆列车 *" << endl; cout << " ******************************************************" << endl; cout << "请输入您的选项:"; cin >> choice; switch (choice) { case 1: this->keyboardInsertTrain(); break; case 2: { cout << "请输入要打开的文件名:"; string filename; cin >> filename; this->txtInsertTrain(filename); break; } case 3: this->deleteTrain(); break; case 4: this->changeTrainName(); break; case 5: this->changeTrainStation(); break; case 6: this->showAllTrain(); break; case 0: { cout << "您需要保存该行程表吗?(Y/N)\t"; char c; cin >> c; if (c == 'Y' || c == 'y') { cout << "请输入文件名:"; string filename; cin >> filename; this->scheduleToTxt(filename); return; } return; } default: cout << "输入选项非法!请重新输入!" << endl; break; } }} main.cpp12345678#include "schedule.h"int main(){ Schedule mySchedule("我的行程表"); mySchedule.operateTheSchedule(); system("pause"); return 0;}]]></content>
<categories>
<category>数据结构</category>
</categories>
<tags>
<tag>C++</tag>
<tag>数据结构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[C++文件在Html的高亮处理]]></title>
<url>%2F2018%2F03%2F17%2FC-%E6%96%87%E4%BB%B6%E5%9C%A8Html%E7%9A%84%E9%AB%98%E4%BA%AE%E5%A4%84%E7%90%86%2F</url>
<content type="text"><![CDATA[编译原理最基础的问题便是词法分析器了。下面是我使用C++语言利用迭代器实现的将C++源码转为HTML文件,并实现HighLight。 (长期更新,直至成熟)(较为成熟,不再更新) 版本记录 V-0.2.1 (20180326) 添加对制表符(\t)的支持。 V-0.2.0 (20180325) 优化搜索算法。 V-0.1.4 (20180324) 支持显示代码行数。 V-0.1.3 (20180323) 添加了对多行注释的高亮显示。 V-0.1.2 (20180322) 添加了对单行注释(//)的高亮显示。 V-0.1.1 (20180318) 完善了部分功能,添加了对部分字符(如<、>、&、”、)的支持以及头文件行的高亮显示。 V-0.1.0 (20180317) 只实现了大致功能,对字符(如<、>等)处理还未完善,其次准确的说,这是按照词读入,并不是字母,当然利用迭代器同理可以实现。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125#include <iostream>#include <fstream>#include <string>#include <vector>#include <algorithm>#include <iomanip>using namespace std;//63个保留字const int RESERVEWORDNUM = 63;const string reserveWord[RESERVEWORDNUM] = { "asm", "auto", "bool", "break", "catch", "case", "char", "class", "const", "const_cast", "continue", "default", "delete", "do", "double", "dynamic_cast", "else", "enum", "explicit", "export", "extern", "false", "float", "for", "friend", "goto", "if", "inline", "int", "long", "mutable", "namespace", "new", "operator", "private", "protected", "public", "register", "register_cast", "return", "short", "signed", "sizeof", "static", "static_cast", "struct", "switch", "template", "this", "throw", "true", "try", "typedef", "typeid", "typename", "union", "unsigned", "using", "virtual", "void", "wchar_t", "volatile", "while"};//动态数组vector<string> reserveWordVector(reserveWord, reserveWord + RESERVEWORDNUM);//检测是否是保留字bool isReserveWord(string &word){ vector<string>::iterator itV; itV = find(reserveWordVector.begin(), reserveWordVector.end(), word); if ( itV != reserveWordVector.end() ){ return true ; } else { return false ; }}void translate(string filename){ ifstream infile; //输入流对象 infile.open(filename + ".cpp"); ofstream outfile; //输出流对象 outfile.open(filename + ".html"); int line = 0; //记录cpp文件行数 if (!infile && !outfile){ cout << "ERROR:打开文件失败!" << endl; return ; } string aLine; //存放cpp文件一行代码 string::iterator lineIte; //定义行迭代器 //读取一行代码 while (getline(infile, aLine)) { line++; //更新行数 aLine += " "; //每行后添加空白防止迭代器溢出 string aHtmlLine = ""; //html的一行 lineIte = aLine.begin(); //初始化行迭代器指向行首位置 //对该行进行分割,对分割出的字进行检测 while (lineIte != aLine.end()){ string word = ""; //存放一个单词 //行迭代器遇到空格或换行符号时停止并截取单词 while (*lineIte != ' ' && *lineIte != aLine.back()){ //针对html语法,对字符串的处理 if (*lineIte == '<') { word += "&lt;"; } else if (*lineIte == '>') { word += "&gt;"; } else if (*lineIte == '&') { word += "&amp;"; } else if (*lineIte == '\"') { word += "&quot;"; } else if (*lineIte == '\t') { aHtmlLine += " &nbsp; &nbsp; &nbsp; &nbsp; "; } //处理缩进问题 else { word += *lineIte; } lineIte++; } //检测单行注释 if (*word.begin() == '/' && *(word.begin() + 1) == '/') { word = "<font color=\"yellow\"><b>" + word + "</b></font>"; //注释为黄色 } //检测多行注释 if (*word.begin() == '/' && *(word.begin() + 1) == '*') { word = "<font color=\"yellow\"><b>" + word ; //注释为黄色 } if (*word.rbegin() == '/' && *(word.rbegin() + 1) == '*') { word = word + "</b></font>"; } //检测是否是保留字 if (isReserveWord(word)) { aHtmlLine += "<font color=\"blue\"><b>" + word + "</b></font>"; //加粗保留字并设置为蓝色 } else { aHtmlLine += word; //其他字不做处理 } if (*lineIte == ' ') { lineIte++; aHtmlLine += " "; } } //检测是否是头文件行 if (*aLine.begin() == '#') { aHtmlLine = "<font color=\"green\"><b>" + aHtmlLine + "</b></font>"; //头文件行变绿色 } outfile << "<font color=\"black\"><b>" << setw(4) << setfill('0') << line << "</b></font>&sdot; &nbsp; &nbsp; &nbsp; &nbsp;" + aHtmlLine + "<br/>" << endl; //写入.html文件 } infile.close(); //关闭文件流 outfile.close(); //关闭文件流 cout << "转换成功!" << endl;}int main(){ cout << "请输入您要转换的cpp文件:"; string cppFileName; cin >> cppFileName; translate(cppFileName); system("pause"); return 0;}]]></content>
<categories>
<category>编译技术</category>
</categories>
<tags>
<tag>编译原理</tag>
<tag>C++</tag>
</tags>
</entry>
<entry>
<title><![CDATA[扔鸡蛋问题]]></title>
<url>%2F2018%2F02%2F28%2F%E6%89%94%E9%B8%A1%E8%9B%8B%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[Google有一道扔鸡蛋的面试题目: You work in a 100 floor building and you get 2 identical eggs.You need to figure out the highest floor an egg can be dropped without breaking.The question is how many throws you need to make.Find an algorithm that is minimizing number of throws in the worst-case scenario. 这道题的意思大致如下: 情景假设1、你在一个 100层高的大楼里; 2、你有 2个一模一样的鸡蛋; 任务1、弄清楚: 鸡蛋最高可以从几层楼扔下去而不会被摔坏; 2、弄清楚你需要扔几次; 3、 提出一个算法,找出在最坏情况下,扔出鸡蛋而不把鸡蛋摔坏的最少次数; 假设首先,在解题之前,我们要做好几个简单的假设: 2个鸡蛋的脆弱程度是一样的。 如果鸡蛋从N楼扔下来没有碎,那么它从小于N楼扔下来,也不会碎 如果鸡蛋从N楼扔下来碎了,那么它从大于N楼扔下来,也一定会碎 一颗扔出去但没有碎的鸡蛋,可以继续被用于试验。 碎了的鸡蛋将无法再继续试验。 有了这些假设之后,我们就可以解题了。 其实解决这道问题的方法有很多,在此列举一些: 最简单解法最简单的一个方法,就是 将鸡蛋从第一层开始,逐层扔出,看它在哪一层会摔碎。 这个方法虽然可靠,但它可能需要进行很多次尝试。比如,在最差的情况下,它需要尝试的次数是100次。 需要注意的一点是,当你只有一颗鸡蛋时,这个算法是唯一可靠的方法。 所以一旦你打碎了第一颗鸡蛋,手里只剩下最后一颗的时候,你就要开始运用这个算法。 最直观解法这个方法重点,是要 利用好第一颗鸡蛋,最大效率地把100层高楼划分成N个更小数目的区间。 一个比较直观和流行的答案是, 将鸡蛋从【要检查的楼层* 1/N】层开始扔下去,逐层检查。 例如,当N=3时,我们就将第一颗鸡蛋从100*1/3 ≈ 33层楼扔出: ► 如果它破损了,我们就接着用第二颗鸡蛋检验32层楼及以下。 ► 如果它没破损,我们就继续将同一颗鸡蛋从33 + (67 * 1/3) = 55层楼扔出,如果它破了,我们就用第二颗鸡蛋,检验34层 - 55层 …… 当N = 3时,最坏的情况是max(33, 24, …) = 33层。 按照这个思路,再通过dynamic programming,我们就可以找到一个完美的N,来最优化鸡蛋的投掷次数了。 这个解法在面试中还是很有"价值"的,毕竟它能向面试官展现求职者的编程思维。但它还不是最佳解法。 最优解在理解最佳解法之前,我们需要理解以下这个equilibrium(均衡状态): 这个均衡状态计算的是在最坏情境下,所需的扔鸡蛋次数。这里,F(n)指的是楼层,是我们扔第一个鸡蛋后的下一层。 假如我们引入以下变量: 那么刚刚的equilibrium就变成这样: 当这个函数里的所有参数都相等时,就是我们的最优解。 那么我们如何做到呢? 从末尾开始看,最后的D(n)将会变成1,因为最终我们将会到达一个点 —— 就是只剩下一层楼可以扔第一颗鸡蛋。 因此,D(n-1)应该等于2。 我们接着会发现,第一颗鸡蛋最终应该是从第99层楼扔出,之前是从99-2=97层,再往前则是97-3=94层,90, 85, 79, 72, 64, 55, 45, 34, 22,然后是第9层。 这就是一个最优解! 这样一来,我们就得出了答案: 在最坏的情况下,我们需要的扔鸡蛋的最少次数,是14次 (最小的差别在于13,但是我们还需要在第9层额外扔一次)。 检查现在就到了检验我们的解法是否正确的时候了。我们可以编写一个简单的Kotlin程序来检验答案。首先,我们需要解释一下,如何在某些决策中,计算扔鸡蛋次数。当有2层或更少的楼层时,我们需要按照剩余的楼层数,来决定扔鸡蛋的次数。 否则,我们应该调用以下均衡函数: 我们在这里使用了bestMaxThrows函数。这是一个假设的函数,它会返回一个投掷次数的数值,并假设接下来的一系列决策是完美的。 我们是这样定义它的: 同样,我们把"计算下一层最优解"的任务,交给了bestNextStep 函数。这个函数很好的为我们指明了下一步的方向。我们可以这样简单地定义它:当只有二层或更少的楼层待检验时,我们会从第一层扔出鸡蛋。否则,我们需要检查所有备选项,然后找到最优解。 下面是具体执行步骤: 需要注意的是,这个方程用了maxThrows函数,因此会涉及到recurrence (循环)。 但这并不成问题,因为当bestNextStep调用maxThrows时,它始终会使用比floorLeft更小的值调用它(因为nextFloor总是大于0)。 在我们使用它之前,我们需要添加一些缓冲,从而加速计算: 首先,我们可以检查它是否返回与我们计算结果相同的结果: 结果是14 —— 这个结果看上去还不错,我们接着检查之后的几个步骤: Result9, 22, 34, 45, 55, 64, 72, 79, 85, 90, 94, 97, 99, 100, 跟我们计算的结果是一致的! Bigger picture以上分享的这个算法,其实还可以解决许多其他类似的问题。 比如,我们可以把原题的提问改改,改成计算最随机情况下的扔鸡蛋次数。我们还可以看看,当建筑物的高度有变化时,得出的结果是否也会跟着变化。 下图很好地回答了以上问题:]]></content>
<categories>
<category>算法</category>
</categories>
<tags>
<tag>算法</tag>
<tag>Google</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hello World!]]></title>
<url>%2F2018%2F02%2F26%2Fhello-world%2F</url>
<content type="text"><![CDATA[2018年2月26日,基于Hexo框架并托管在GitHub服务器上,我建立了第一个自己的博客网站 https://conglindev.github.io/。 虽然功能不完善,但是具有了博客的基本的功能。以后我会尽量完善和美化这个网站的。 Hello world!]]></content>
</entry>
</search>