-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
454 lines (286 loc) · 179 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>JuniousY的博客</title>
<icon>https://www.gravatar.com/avatar/f7f552067d5d738d5b1802fa58ce5759</icon>
<subtitle>倾听Ghost的低语</subtitle>
<link href="https://juniousy.github.io/atom.xml" rel="self"/>
<link href="https://juniousy.github.io/"/>
<updated>2023-06-14T17:34:36.634Z</updated>
<id>https://juniousy.github.io/</id>
<author>
<name>JuniousY</name>
<email>chengjy42@gmail.com</email>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>MIT 6.824 Lab4</title>
<link href="https://juniousy.github.io/2023/05/05/2023/6.824Lab4/"/>
<id>https://juniousy.github.io/2023/05/05/2023/6.824Lab4/</id>
<published>2023-05-05T15:59:59.000Z</published>
<updated>2023-06-14T17:34:36.634Z</updated>
<content type="html"><![CDATA[<p>Lab4的任务是在Lab2的基础上实现一个可迁移可配置的多节点KV存储系统。</p><span id="more"></span><p>Lab4的实现难度很高,尤其是动态迁移数据这部分,实现与调试阶段都很痛苦,很难找出是哪一步导致没法实现线性一致性。整个设计对分布式系统的认识与设计能力都有很高的要求。这里参考了很多这篇文章的思路:<a href="https://github.com/OneSizeFitsQuorum/MIT6.824-2021/blob/master/docs/lab4.md#%E5%88%86%E7%89%87%E7%BB%93%E6%9E%84">lab4.md</a>。也参考了一下这篇文章中的一些思路:<a href="https://sworduo.github.io/2019/08/16/MIT6-824-lab4-shardKV/">MIT6.824-lab4-shardKV</a>。</p><h1 id="Lab4A"><a href="#Lab4A" class="headerlink" title="Lab4A"></a>Lab4A</h1><p>Lab4A的任务目标是在Lab2的基础上实现一个高可用配置中心shardmaster。最核心的部分是通过raft实现这四个rpc方法:</p><ul><li>Join:向不同的组中添加server,然后产生一个新的config,使shard尽可能平均分配给各个组,且移动的shard最少</li><li>Leave:移除某一组,重分配它们的shard。同样需要使shard尽可能平均</li><li>Move:指定将某一个shard移动到某一个组 </li><li>Query:返回最新的配置</li></ul><p>这四个方法对应的配置数据结构是项目提供的:</p><pre><code class="go">type Config struct { Num int // config number Shards [NShards]int // shard -> gid Groups map[int][]string // gid -> servers[]}</code></pre><p>Num表示配置的递增id,Shards表示的是某个shard被分配给哪个group,Groups表示的是某一个群组中有哪些server。这个lab里shard总数是固定的值,<code>NShards=10</code>。</p><p>实现结果为:</p><pre><code class="text"># GO111MODULE=off go testTest: Basic leave/join ... ... PassedTest: Historical queries ... ... Passed Test: Move ... ... PassedTest: Concurrent leave/join ... ... PassedTest: Minimal transfers after joins ... ... PassedTest: Minimal transfers after leaves ... ... PassedTest: Multi-group join/leave ... ... PassedTest: Concurrent multi leave/join ... ... PassedTest: Minimal transfers after multijoins ... ... PassedTest: Minimal transfers after multileaves ... ... PassedPASSok .../MIT6.824/src/shardmaster 8.896s</code></pre><p>首先实现第一步是把lab2的框架都拷过来,只是将get和put、append方法改为新的rpc方法。</p><p>第二步是设计shardmaster和op结构:</p><pre><code class="go">type ShardMaster struct { mu sync.Mutex me int rf *raft.Raft applyCh chan raft.ApplyMsg // Your data here. prevOperation map[int64]CommandResponse notifyChanMap map[int]chan CommandResponse configs []Config // indexed by config num}type Op struct { // Your data here. Servers map[int][]string // for Join GIDs []int // for Leave Shard int // for Move GID int // for Move Num int // for Query Type string ClientId int64 CommandId int}type CommandResponse struct { CommandId int Config Config}</code></pre><p>为什么op结构是这样的,需要结合这4个方法的参数来解释。实际上lab4A的核心就是实现这四个方法。下面逐一介绍</p><h3 id="Join"><a href="#Join" class="headerlink" title="Join"></a>Join</h3><pre><code class="go">type JoinArgs struct { Servers map[int][]string // new GID -> servers mappings ClientId int64 CommandId int}</code></pre><p>Join方法的参数是一个map。Join方法首先要把参数里的server加到group中去</p><pre><code class="go">//... newConfig := Config{len(sm.configs), lastConfig.Shards, deepCopy(lastConfig.Groups)} for gid, servers := range groups { // 这里的groups就是参数中的那个map if _, ok := newConfig.Groups[gid]; !ok { newServers := make([]string, len(servers)) copy(newServers, servers) newConfig.Groups[gid] = newServers } }//...</code></pre><p>然后进行shard的重分配。这部分leave方法也会用到。</p><pre><code class="go">//... s2g := Group2Shards(newConfig) log(fmt.Sprintf("%+v", s2g), 1) for { // 这个部分会找出哪个group的shard数量最多,哪个group的shard数量最少 // 然后最多的给一个shard给最少的,循环直到相差数小于等于1 source, target := GetGIDWithMaximumShards(s2g), GetGIDWithMinimumShards(s2g) log(fmt.Sprintf("source %+v s2g[source] %v target %+v ", source, s2g[source], target), 1) if source != 0 && len(s2g[source])-len(s2g[target]) <= 1 { break } s2g[target] = append(s2g[target], s2g[source][0]) s2g[source] = s2g[source][1:] } log(fmt.Sprintf("%+v", s2g), 1) var newShards [NShards]int for gid, shards := range s2g { for _, shard := range shards { newShards[shard] = gid } }//...// 这个方法的返回值是一个map,key为gid,value为每个group下的shardfunc Group2Shards(config Config) map[int][]int { s2g := make(map[int][]int) for i, gid := range config.Shards { if _, ok := s2g[gid]; ok { s2g[gid] = append(s2g[gid], i) } else { s2g[gid] = []int{i} } } for gid := range config.Groups { if _, ok := s2g[gid]; !ok { s2g[gid] = []int{} } } return s2g}</code></pre><h3 id="leave"><a href="#leave" class="headerlink" title="leave"></a>leave</h3><p>leave方法的参数是要剔除出去的group:<code>GIDs []int</code></p><p>leave方法首选要做的是将group从newConfig.Groups中剔除,然后将它们的shard都存起来(这里变量名为orphanShards)</p><pre><code class="go">//... newConfig := Config{len(sm.configs), lastConfig.Shards, deepCopy(lastConfig.Groups)} s2g := Group2Shards(newConfig) orphanShards := make([]int, 0) for _, gid := range gids { if _, ok := newConfig.Groups[gid]; ok { delete(newConfig.Groups, gid) } if shards, ok := s2g[gid]; ok { orphanShards = append(orphanShards, shards...) delete(s2g, gid) } }//...</code></pre><p>然后同样进行rebalance操作。这里每次都将待分配的shard分配给shard数最少的group</p><pre><code class="go">//... var newShards [NShards]int if len(newConfig.Groups) != 0 { for _, shard := range orphanShards { target := GetGIDWithMinimumShards(s2g) s2g[target] = append(s2g[target], shard) } for gid, shards := range s2g { for _, shard := range shards { newShards[shard] = gid } } }//...</code></pre><h3 id="move-与-query"><a href="#move-与-query" class="headerlink" title="move 与 query"></a>move 与 query</h3><p>query方法只需要返回最后一个config。<br>move方法是做一次迁移操作</p><pre><code class="go">func (sm *ShardMaster) doMove(shard int, gid int) Err { lastConfig := sm.configs[len(sm.configs)-1] newConfig := Config{len(sm.configs), lastConfig.Shards, deepCopy(lastConfig.Groups)} newConfig.Shards[shard] = gid sm.configs = append(sm.configs, newConfig) return OK}</code></pre><h1 id="Lab4B"><a href="#Lab4B" class="headerlink" title="Lab4B"></a>Lab4B</h1><p>Lab4B的系统会用到Lab4A的配置管理系统,用shardmaster来负责配置更新与分片分配。然后系统会分出多个raft组来承载所有分片的读写任务。在处理读写的过程中,系统要处理raft组的变更、节点宕机与重启、网络分区等各种情况。同样的,所有操作都要保证线性一致性。</p><p><strong>这个lab不能通过全部test,仅做思路参考。</strong></p><p>实现时,我先参考了作业要求信息的步骤,先实现一个不考虑分片变化的静态系统。这可以直接照搬lab2的代码,但是要注意的是,需要根据提供的算法来决定向哪个分片进行操作:</p><pre><code class="go">func key2shard(key string) int { shard := 0 if len(key) > 0 { shard = int(key[0]) } shard %= shardmaster.NShards return shard}</code></pre><p>每个server有一个gid,表示自己属于哪个raft组。在每次接受到Get、Put、Append请求时,如果发现该server所在的组没有这个分片(根据lab3A的config信息判断),就返回<code>ErrWrongGroup</code>。做完这些之后,可以顺利得通过第一个测试任务<code>TestStaticShards</code>。</p><pre><code class="text"># GO111MODULE=off go test -run TestStaticShards Test: static shards ... ... PassedPASSok .../MIT6.824/src/shardkv 6.189s</code></pre><p>但是如果考虑到分片迁移应该怎么办呢?首先,根据课程提示,我们至少要有一个线程来拉取最新的配置,使自身的配置得到更新。其次,我们需要有一个rpc方法,帮助我们从一个raft组传递数据给另一个raft组。当然,这些操作也都必须经过raft层的应用。</p><p>有了以上的目标之后,先设计一下相关的数据结构:</p><p>Op是提交到raft中的结构,可以有为空的字段,但需要包含各种信息。参考了其他人的设计,Op中的CommandType分为Operation、Configuration、UpdateShards这几种。Operation就是客户端读写操作,Configuration是配置更新日志,UpdateShards是分片的数据更新操作。CommandType还可以加一个DeleteShards用来实现lab challenge目标中的gc任务,这里略过。</p><pre><code class="go">type Op struct { CommandType CommandType Type string // get put append Key string Value string ClientId int64 CommandId int Config *shardmaster.Config ShardOperationResponse *ShardOperationResponse}</code></pre><p>server的结构中加了这几个:</p><pre><code class="go">//... sm *shardmaster.Clerk // 用来发送rpc lastConfig shardmaster.Config // 前一次配置 currentConfig shardmaster.Config // 最新配置 persister *raft.Persister shardDatabase map[int]*Shard // shard database prevOperation map[int64]CommandResponse notifyChanMap map[int]chan CommandResponse updating bool // 这两个与更新shard数据有关 updatedNum int//...</code></pre><p>Shard结构保存了一个分片的状态和数据:</p><pre><code class="go">type ShardStatus uint8const ( Serving ShardStatus = iota ToPull Pulling GCing)type Shard struct { KV map[string]string Status ShardStatus}func NewShard() *Shard { return &Shard{make(map[string]string), Serving}}</code></pre><p>然后在启动方法中加入定时操作的线程:</p><pre><code class="go">// 定时任务func (kv *ShardKV) monitor(action func(), timeout time.Duration) { for kv.killed() == false { if _, isLeader := kv.rf.GetState(); isLeader { action() } time.Sleep(timeout) }}// start方法://... go kv.monitor(kv.configureAction, ConfigureMonitorTimeout) go kv.monitor(kv.migrationAction, MigrationMonitorTimeout)//...</code></pre><p>配置更新操作这两个方法,一个是configureAction,用来拉取配置,一个是applyConfiguration,应用配置:</p><pre><code class="go">func (kv *ShardKV) configureAction() { canPerformNextConfig := true kv.mu.Lock() if kv.updating { canPerformNextConfig = false } currentConfigNum := kv.currentConfig.Num kv.mu.Unlock() if canPerformNextConfig { nextConfig := kv.sm.Query(currentConfigNum + 1) if nextConfig.Num == currentConfigNum+1 { //log(fmt.Sprintf("{Group %v}{Node %v} fetches latest configuration %v when currentConfigNum is %v", kv.gid, kv.me, nextConfig, currentConfigNum), 1) op := Op{CommandType: Configuration, Config: &nextConfig} kv.executeOp(op) } }}// 在raft中apply成功后调用这个方法func (kv *ShardKV) applyConfiguration(nextConfig *shardmaster.Config) CommandResponse { if nextConfig.Num == kv.currentConfig.Num+1 { //log(fmt.Sprintf("{Group %v}{Node %v} updates currentConfig from %v to %v", kv.gid, kv.me, kv.currentConfig, nextConfig), 2) kv.lastConfig = kv.currentConfig kv.currentConfig = *nextConfig return CommandResponse{Err: OK} } //log(fmt.Sprintf("{Group %v}{Node %v} rejects outdated config %v when currentConfig is %v", kv.gid, kv.me, nextConfig, kv.currentConfig), 1) return CommandResponse{Err: ErrOutDated}}</code></pre><p>迁移shard数据的方法也是类似的,一个是migrationAction,另一个是applyUpdateShards。另外还有一个GetShardsData方法为rpc调用。</p><p>migrationAction会先通过调用getToPullShardIdMap,根据前一次配置和当前配置去生成更新清单,指向哪些raft组请求哪些raft分片的信息。然后进行分片迁移操作。注意我这里通过设置updating的方式阻塞了读写操作与配置更新操作,这样不符合challenge目标,可以后续通过shard状态来优化。</p><pre><code class="go">func (kv *ShardKV) migrationAction() { kv.mu.Lock() if kv.updating { kv.mu.Unlock() return } gid2shardIDs := kv.getToPullShardIdMap() if len(gid2shardIDs) > 0 { log(fmt.Sprintf("{Group %v}{Node %v} migrationAction gid2shardIDs:%+v", kv.gid, kv.me, gid2shardIDs), 2) } else { kv.mu.Unlock() return } kv.updating = true var wg sync.WaitGroup for gid, shardIDs := range gid2shardIDs { log(fmt.Sprintf("{Group %v}{Node %v} starts a PullTask to get shards %v from group %v when config is %v", kv.gid, kv.me, shardIDs, gid, kv.currentConfig), 2) wg.Add(1) go func(servers []string, configNum int, shardIDs []int) { defer wg.Done() pullTaskRequest := ShardOperationRequest{configNum, shardIDs} for _, server := range servers { var pullTaskResponse ShardOperationResponse srv := kv.make_end(server) keepTry := true for keepTry { srv.Call("ShardKV.GetShardsData", &pullTaskRequest, &pullTaskResponse) if pullTaskResponse.Err == OK { log(fmt.Sprintf("{Group %v}{Node %v}gets a PullTaskResponse %+v and tries to commit it when currentConfigNum is %v", kv.gid, kv.me, pullTaskResponse, configNum), 2) op := Op{CommandType: UpdateShards, ShardOperationResponse: &pullTaskResponse} kv.executeOp(op) keepTry = false } else if pullTaskResponse.Err == ErrWrongLeader { keepTry = false } else { log(fmt.Sprintf("{Group %v}{Node %v}gets a PullTaskResponse %+v currentConfigNum is %v", kv.gid, kv.me, pullTaskResponse, configNum), 2) time.Sleep(time.Duration(100) * time.Millisecond) } } } }(kv.lastConfig.Groups[gid], kv.currentConfig.Num, shardIDs) } kv.mu.Unlock() wg.Wait() kv.mu.Lock() kv.updating = false kv.updatedNum = kv.currentConfig.Num kv.mu.Unlock()}func (kv *ShardKV) getToPullShardIdMap() map[int][]int { gid2ShardIds := make(map[int][]int) if kv.updatedNum == kv.currentConfig.Num { return gid2ShardIds } for shardId, oldGid := range kv.lastConfig.Shards { if oldGid == 0 { continue // init } if kv.shardDatabase[shardId].Status == ToPull { continue } newGid := kv.currentConfig.Shards[shardId] if newGid == kv.gid && newGid != oldGid { for _, gId := range kv.lastConfig.Shards { if gId == oldGid { gid2ShardIds[gId] = append(gid2ShardIds[gId], shardId) kv.shardDatabase[shardId].Status = ToPull break } } } } return gid2ShardIds}</code></pre><p>在迁移过程中会新开线程做拉取配置操作与向raft提交操作记录</p><pre><code class="go">func (kv *ShardKV) GetShardsData(request *ShardOperationRequest, response *ShardOperationResponse) { // only pull shards from leader if _, isLeader := kv.rf.GetState(); !isLeader { response.Err = ErrWrongLeader return } kv.mu.Lock() //defer log(fmt.Sprintf("{Group %v}{Node %v} processes PullTaskRequest %+v with response %+v", kv.gid, kv.me, *request, *response), 2) defer kv.mu.Unlock() if kv.currentConfig.Num < request.ConfigNum { log(fmt.Sprintf("{Group %v}{Node %v} processes PullTaskRequest %+v ErrOutDated: kv.currentConfig.Num %v, request.ConfigNum %v", kv.gid, kv.me, *request, kv.currentConfig.Num, request.ConfigNum), 2) response.Err = ErrOutDated return } response.ShardData = make(map[int]map[string]string) for _, shardId := range request.ShardIds { shard := kv.shardDatabase[shardId] response.ShardData[shardId] = shard.deepCopy() } response.PrevOperation = make(map[int64]CommandResponse) for clientID, operation := range kv.prevOperation { response.PrevOperation[clientID] = operation.deepCopy() } response.ConfigNum, response.Err = request.ConfigNum, OK}</code></pre><p>最后是应用shard迁移的数据。</p><pre><code class="go">func (kv *ShardKV) applyUpdateShards(shardsInfo *ShardOperationResponse) CommandResponse { if shardsInfo.ConfigNum == kv.currentConfig.Num { log(fmt.Sprintf("{Group %v}{Node %v} accepts shards insertion %v when currentConfig is %v", kv.gid, kv.me, shardsInfo, kv.currentConfig), 1) for shardId, shardData := range shardsInfo.ShardData { shard := kv.shardDatabase[shardId] for key, value := range shardData { shard.KV[key] = value } shard.Status = Serving } for clientId, commandResponse := range shardsInfo.PrevOperation { kv.prevOperation[clientId] = commandResponse } return CommandResponse{Err: OK} } log(fmt.Sprintf("{Node %v}{Group %v} rejects outdated shards insertion %v when currentConfig is %v", kv.me, kv.gid, shardsInfo, kv.currentConfig), 1) return CommandResponse{Err: ErrOutDated}}</code></pre><p>这样实现后,可以顺利通过测试2<code>TestJoinLeave</code>。</p><pre><code class="text"># GO111MODULE=off go test -run TestJoinLeave ... PassedPASSok .../MIT6.824/src/shardkv 6.069s</code></pre><p>但是在测试3<code>TestSnapshot</code>时遇到了问题。宕机与恢复至少需要实现takeSnapshot与restoreSnapshot这两步,在实现了之后,系统的数据问题与死锁问题频发,表现为数据不符合预期或陷入死锁。对这步的调式异常痛苦。然后我重新思考了一下这两个方法的实现:</p><pre><code class="go">// 代码块1 apply线程中发起snapshot请求的入口if isLeader && message.CommandTerm == currentTerm { ch := kv.getNotifyChan(message.CommandIndex) if ch != nil { ch <- response } if kv.needSnapshot() { kv.takeSnapshot(message.CommandIndex) }}// 代码块2 apply线程中应用snapshot的入口if message.SnapshotValid { kv.mu.Lock() kv.restoreSnapshot(message.Snapshot) kv.mu.Unlock()}func (kv *ShardKV) takeSnapshot(index int) { buffer := new(bytes.Buffer) encoder := labgob.NewEncoder(buffer) encoder.Encode(kv.shardDatabase) encoder.Encode(kv.prevOperation) encoder.Encode(kv.lastConfig) encoder.Encode(kv.currentConfig) encoder.Encode(kv.updating) encoder.Encode(kv.updatedNum) log(fmt.Sprintf("{Group %v}{Node %v} 创建快照数据:index %v shardDatabase %+v", kv.gid, kv.me, index, printShardDatabase(kv.shardDatabase)), 3) kv.rf.TakeSnapshot(index, buffer.Bytes())}// 从快照中读取数据,并且将当前的db和prevOperation替换func (kv *ShardKV) restoreSnapshot(snapshot []byte) { if snapshot == nil || len(snapshot) == 0 { return } buffer := bytes.NewBuffer(snapshot) decoder := labgob.NewDecoder(buffer) var db *map[int]*Shard var prevOperation *map[int64]CommandResponse var lastConfig *shardmaster.Config var currentConfig *shardmaster.Config var updating *bool var updatedNum *int if decoder.Decode(&db) != nil || decoder.Decode(&prevOperation) != nil || decoder.Decode(&lastConfig) != nil || decoder.Decode(&currentConfig) != nil || decoder.Decode(&updating) != nil || decoder.Decode(&updatedNum) != nil { log(fmt.Sprintf("{Group %v}{Node %v} restore snapshot failed", kv.gid, kv.me), 3) } kv.shardDatabase = *db kv.prevOperation = *prevOperation kv.lastConfig = *lastConfig kv.currentConfig = *currentConfig kv.updating = *updating kv.updatedNum = *updatedNum log(fmt.Sprintf("{Group %v}{Node %v} 恢复快照数据: shardDatabase %+v, prevOperation %+v", kv.gid, kv.me, printShardDatabase(kv.shardDatabase), kv.prevOperation), 3)}</code></pre><p>然后回过头去看<code>migrationAction</code>方法,会发现中间是不能一个锁全锁到底的,必须释放锁等待网络请求再更新。那么在锁的重新获取期间,如果发生了snapshot操作,会将更新半途的数据都存进去,再恢复时,由于无法再回到原来方法的进度中,所以恢复后的数据和迁移的数据是很有可能“打架”的。</p><p>因此作出调整,不再持久化updating和updatedNum,而且,恢复数据后,默认所有的shard都处于需要更新的阶段,以防止漏更新:</p><pre><code class="go">for _, shard := range kv.shardDatabase { shard.Status = ToPull}</code></pre><p>这样改了之后就能通过第三个test了</p><pre><code class="text"># GO111MODULE=off go test -run TestSnapshot Test: snapshots, join, and leave ...labgob warning: Decoding into a non-default variable/field Err may not work ... PassedPASSok _/home/yuic/MyProjects/golang/MIT6.824/src/shardkv 17.516s</code></pre><p>不过后面的几个test依然会遇到的问题依然有死锁等待和返回值有误的情况。其中一个问题点大致定位在updating阻塞这套方案上,这在异常网络条件下会导致两个Raft组死锁等待分片数据。</p><p>另外2个challenge也需要进行优化。如需要在迁移过程中只阻塞待更新的shard,这需要对shard的状态有更清晰的定义与逻辑实现。另外还需要一个gc方法,具体场景为,在迁移走数据shard数据后,raft组会清空已不需要的数据。</p>]]></content>
<summary type="html"><p>Lab4的任务是在Lab2的基础上实现一个可迁移可配置的多节点KV存储系统。</p></summary>
<category term="Lab" scheme="https://juniousy.github.io/categories/Lab/"/>
<category term="distributed system" scheme="https://juniousy.github.io/tags/distributed-system/"/>
</entry>
<entry>
<title>MIT 6.824 Lab3</title>
<link href="https://juniousy.github.io/2022/10/20/2022/6.824Lab3/"/>
<id>https://juniousy.github.io/2022/10/20/2022/6.824Lab3/</id>
<published>2022-10-20T15:59:59.000Z</published>
<updated>2023-06-14T16:06:11.583Z</updated>
<content type="html"><![CDATA[<p>Lab3的目标是利用Raft的机制,实现一个线性一致(Linearizability)的key-value存储结构。</p><span id="more"></span><h1 id="Lab3A"><a href="#Lab3A" class="headerlink" title="Lab3A"></a>Lab3A</h1><h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>通过测试</p><pre><code>Test: one client (3A) ...labgob warning: Decoding into a non-default variable/field Err may not work ... Passed -- 15.2 5 7307 289Test: many clients (3A) ... ... Passed -- 15.9 5 12944 1483Test: unreliable net, many clients (3A) ... ... Passed -- 16.1 5 3911 1029Test: concurrent append to same key, unreliable (3A) ... ... Passed -- 1.2 3 163 52Test: progress in majority (3A) ... ... Passed -- 1.2 5 167 2Test: no progress in minority (3A) ... ... Passed -- 1.1 5 191 3Test: completion after heal (3A) ... ... Passed -- 1.1 5 96 3Test: partitions, one client (3A) ... ... Passed -- 22.4 5 15465 175Test: partitions, many clients (3A) ... ... Passed -- 23.2 5 23550 1158Test: restarts, one client (3A) ... ... Passed -- 21.3 5 21560 289Test: restarts, many clients (3A) ... ... Passed -- 23.3 5 105143 1445Test: unreliable net, restarts, many clients (3A) ... ... Passed -- 23.6 5 5069 1145Test: restarts, partitions, many clients (3A) ... ... Passed -- 29.1 5 78246 825Test: unreliable net, restarts, partitions, many clients (3A) ... ... Passed -- 32.1 5 4443 507Test: unreliable net, restarts, partitions, many clients, linearizability checks (3A) ... ... Passed -- 28.8 7 9497 887PASS</code></pre><p>一开始看到Lab3A的时候完全不知道从何入手,仔细看了下的话,其实kv server分为三个部分:Client, Server, Raft。其中Raft是Server的底层,是分布式Server做到线性一致的基础。这其中的通信关系是:Client <=> Server, Server <=> Raft, Raft <=> Raft。有两个重要的点,一个是Server和Server之间是不通信的,必须通过Raft通信,第二个是Server和Raft是一对一的关系,只有Raft leader对应的Server才有操作的资格。另外有一个细节是,在这个lab里,每个Get请求也必须通过raft协议才能完成,这样做的目的是保证线性一致。</p><h3 id="Client"><a href="#Client" class="headerlink" title="Client"></a>Client</h3><p>首先从Cliet处着手。保证消息不重复消费最重要的一部分是Client会有自己的clientId,每次指令也有一个commandId。Client的Get和Put方法就是循环调用server的接口,如果不成功就换下一个server,直到server返回成功。</p><pre><code class="go">for { var getArgs GetArgs = GetArgs{ Key: key, ClientId: ck.clientId, CommandId: ck.commandId, } var getReply GetReply for { ok := ck.servers[ck.leaderId].Call("KVServer.Get", &getArgs, &getReply) if !ok || getReply.Err == ErrWrongLeader || getReply.Err == ErrTimeout { ck.leaderId = (ck.leaderId + 1) % (len(ck.servers)) continue } DPrintf("###client %v Get ok %#v, commandId:'%v' \n", ck.clientId, getReply, ck.commandId) ck.commandId++ return getReply.Value }}</code></pre><h3 id="Server"><a href="#Server" class="headerlink" title="Server"></a>Server</h3><p>Server端会比较复杂。Server和Client通信是用的RPC,那么Server和Raft是怎么通信的呢?答案是Server向Raft发起请求是通过Raft的Start()入口方法,然后通过Raft的applyCh这个channel来获取数据。因此在Server新建的时候,需要新开一个线程用来获取applyCh的数据。</p><p>Start方法传入的数据结构会存到Raft的日志中:</p><pre><code class="go">type Op struct { Type string // get put append Key string Value string ClientId int64 CommandId int}</code></pre><p>Server在收到Get、Put、Append的RPC请求时,要做这几件事:</p><ol><li>构造Op参数</li><li>根据ClientId和CommandId判断是不是重复请求</li><li>调用Raft的Start方法</li><li>收到Start方法结果,如果返回值中isLeader是false,就返回ErrWrongLeader给Client</li><li>生成一个channel,在applyCh数据返回后给 apply message 线程后,线程会把数据传回到这个channel。接收成功就可以返回给client结果。</li></ol><p>这个channel需要做超时处理:</p><pre><code class="go">DPrintf("leader %v 开始等待PutAppend结果: index %v, isLeader %v, args %+v", kv.me, index, isLeader, args)ch := kv.makeNotifyChan(index)kv.mu.Unlock()select {case <-time.After(ExecuteTimeout): DPrintf("leader %v PutAppend 超时: index %v, isLeader %v, args %+v", kv.me, index, isLeader, args) reply.Err = ErrTimeoutcase <-ch: reply.Err = OK DPrintf("leader %v PutAppend结果得到: index %v, isLeader %v, args %+v, reply %+v", kv.me, index, isLeader, args, reply)}go func() { kv.closeNotifyChan(index) }()</code></pre><p>apply message 线程就是循环读取applyCh传来的数据,如果是写入操作,就把操作写入server的数据库中。在本lab里,这个操作是写入到一个map里。这里有一个重要的点,是判断applyCh传来的数据是不是过时的数据。因此,我的做法是在server中维护一个结构<code>prevOperation map[int64]CommandResponse</code>,记录每个Client上一次返回结果。CommandResponse中有CommandId信息。如果applyCh传来的数据中,它的CommandId小于等于前一次返回结果的CommandId,就直接抛弃这条消息。最后server把数据传回给rpc请求:</p><pre><code class="go">ch := kv.getNotifyChan(message.CommandIndex)if ch != nil { ch <- response}</code></pre><p>这里有个细节是如果拿不到channel了,就说明rpc方法已经超时返回了,删除了这个channel。这里的处理是试了很多次之后定下来的,能通过测试,但可能还是有点疑问。因为如果这里不传消息,其实是相当于丢了一次已经记录到Raft中的消息,而如果不传,那这里因为rpc已经返回了,所以channel会永远在等待发送消息中。参考了一下其他人的实现,有的人会说这里不会阻塞,可能是实现的一些细节不同吧。</p><p>整个LAB3A做下来的感受其实是挺折磨的,最大的问题是很容易遇到死锁问题,需要写大量的log,尤其是在加锁解锁和channel通信处。下面举几个例子:</p><p>死锁一是在上面说的阻塞的情况时发生:</p><pre><code class="go">// putappendDPrintf("putAppend 0")kv.mu.Lock()DPrintf("putAppend 1")// dosomethingkv.mu.Unlock()// apply线程kv.mu.Lock()...DPrintf("apply 1")ch <- responseDPrintf("apply 2")kv.mu.Unlock()DPrintf("apply 3")</code></pre><pre><code class="text">// 输出结果apply 1putAppend 1putAppend 0</code></pre><p>由于apply处有锁,而apply阻塞等待消息,因此rpc方法处的锁无法拿到。这里的错误点在于发送消息时应该是没有锁的。要么提前解锁,要么新开一个线程发送消息。</p><p>死锁二,apply必须在是leader情况下时才能发送管道消息,不然就永远在等待。为什么呢?因为只有在leader情况下才会新建chanel。当然上面判断channel是不是为空也是一种解决方法,但更正确的做法应该是只在是leader而且term相符情况下才发送消息:</p><pre><code class="go">currentTerm, isLeader := kv.rf.GetState()if isLeader && message.CommandTerm == currentTerm { // send}</code></pre><p>死锁三是在raft中发生的,这算是之前lab2中一个没有暴露出来的bug。<br>之前在raft发送applyCh消息时,是持有raft的锁的。当apply一次性提交很多个数据时,会一直占用rf.mu。但是在Server处,会调用rf.Start()或者rf.GetState(),这两个都要求锁。于是,Server端等待rf的锁,无法处理applyCh的下一条消息,而raft持有锁,等待向applyCh中发送消息,于是引发了死锁。<br>解决方法和上面一样,在消息发送时,要么解锁,要么新开线程:</p><pre><code class="go">// 问题代码:// server层currentTerm, isLeader := kv.rf.GetState()// raft层// 加锁状态 一次有多个msg发送rf.applyCh <- msg</code></pre><pre><code class="go">// 需要改成:rf.mu.Unlock()rf.applyCh <- msgrf.mu.Lock()</code></pre><h1 id="Lab3B"><a href="#Lab3B" class="headerlink" title="Lab3B"></a>Lab3B</h1><pre><code>Test: InstallSnapshot RPC (3B) ... ... Passed -- 7.5 3 4540 63Test: snapshot size is reasonable (3B) ... ... Passed -- 41.2 3 10828 800Test: restarts, snapshots, one client (3B) ... ... Passed -- 20.8 5 17973 289Test: restarts, snapshots, many clients (3B) ... ... Passed -- 25.3 5 129392 5900Test: unreliable net, snapshots, many clients (3B) ... ... Passed -- 16.0 5 3476 848Test: unreliable net, restarts, snapshots, many clients (3B) ... ... Passed -- 23.5 5 4381 730Test: unreliable net, restarts, partitions, snapshots, many clients (3B) ...... Passed -- 32.0 5 3448 343</code></pre><p>Lab3B要做的事情很简单,就是将日志压缩为snapshop。但实际我自己做下来比lab3A要繁琐得多,也尝试了很久。而且虽然通过了所有的测试,但最后三个测试有小概率失败,也很难定位到问题。</p><p>具体来说,Lab3B的流程是,server在发现日志大小超过某个临界值之后,将自己的数据序列化存储为快照,然后传给raft。raft会删除旧的日志,保留必要的信息。</p><h3 id="Server层"><a href="#Server层" class="headerlink" title="Server层"></a>Server层</h3><p>server比较简单,但也会有坑。首先在apply线程中,在拿到raft返回数据后,判断要不要生成快照:</p><pre><code class="go">func (kv *KVServer) needSnapshot() bool { if kv.maxraftstate == -1 { return false } return kv.persister.RaftStateSize() > kv.maxraftstate}</code></pre><p>如果需要,就要讲自己的数据map、lastAppliedIndex表和prevOperation表序列化生成快照。后面两个在恢复server的时候是非常有必要的,防止多次提交。在序列化的最后,是调取一个新的Raft方法传递当前的日志index(很重要)和快照数据<code>kv.rf.TakeSnapshot(index, buffer.Bytes())</code>。</p><p>反过来也需要一个反序列化的读取方法,将这些数据应用到Server层中。触发的时间点是Raft通过applyCh将操作成功的消息传递给Server层时。一个细节是启动时候也要进行这一步。</p><h3 id="Raft层"><a href="#Raft层" class="headerlink" title="Raft层"></a>Raft层</h3><p>首先,Raft层接受Server命令的方法会进行删除日志、记录快照最后包含的Index。Raft层会新增两个字段lastIncludedIndex、lastIncludedTerm。然后leader将快照数据持久化。</p><p>然后,Raft层会多一个RPC请求,这是leader向follower发送snapshot命令的请求:</p><pre><code class="go">func (rf *Raft) InstallSnapshot(args *InstallSnapshotArgs, reply *InstallSnapshotReply) { // ... 省略判断校验逻辑 logs := make([]Log, 0) startIndex := rf.newIndex(args.LastIncludedIndex + 1) if startIndex <= len(rf.logEntries) { logs = append(logs, rf.logEntries[startIndex:]...) } rf.logEntries = logs rf.lastIncludedIndex = args.LastIncludedIndex rf.lastIncludedTerm = args.LastIncludedTerm rf.lastApplied = max(rf.lastIncludedIndex, rf.lastApplied) rf.commitIndex = max(rf.lastIncludedIndex, rf.commitIndex) // ... 省略持久化数据 // ... 省略发送消息给Server层}</code></pre><p>这个核心方法是删除日志,然后将相关index修改。</p><p>那么,什么时候leader向server发送这一消息呢?我的处理是这样的,每次leader持久化之后就对所有follower进行一次发送。然后,在发送AppendEntries处,如果nextIndex的值已经和日志不匹配了,就说明也要发送。这种情况是follower脱离集群之后再回来会发生的事:</p><pre><code class="go">prevLogIndex := rf.nextIndex[id] - 1if rf.newIndex(prevLogIndex) >= len(rf.logEntries) || rf.newIndex(prevLogIndex) < -1 { //fmt.Printf("#### rf.me %v rf.nextIndex[%v] = %v, rf.lastIncludedIndex %v\n", rf.me, id, rf.nextIndex[id], rf.lastIncludedIndex) rf.mu.Unlock() rf.sendSnapshot(id) return}</code></pre><p>这里有一个非常头疼的地方是,下标index怎么处理。日志list是新建的,原来的下标是对应不上的。有两种思路,一种是,在新建日志时,重算所有的下标并更新;另一种思路是,所有的下标保持原样,在使用到日志list上时做处理。<br>一开始,我在尝试了一下第二种思路后,选择放弃了,因为要改的地方很多,以为前一种思路更简单一点,因为只要改一处地方。但实际上试下来非常头疼,debug了很久,因为很容易出现旧下标和新下标混用的情况。具体遇到的问题如:1. Server层在get方法时,apply线程将信息发给了别的rpc接口。因为获取channel的参数是Start方法的返回值中的index,一旦index变化,就会发错数据。2. 在解决了问题一之后,Raft层的数据有概率出现错配的情况,也很难调试出原因。<br>然后我重看了一下论文,再参考了一下别人的实现,思考了一下之后我觉得只有第二种才是正常的实现,因为各种index(如applied index)必须是递增的,没有理由去改动它们。所以,必须在读取日志list时做改动:</p><pre><code class="go">func (rf *Raft) newIndex(index int) int { return index - rf.lastIncludedIndex - 1}func (rf *Raft) trueIndex(index int) int { return index + rf.lastIncludedIndex + 1}</code></pre><p>这里面头疼的点是,有些不是直接和list下标有关的方法内部参数,也需要改动,举一个例子,选举时有一个参数<code>lastLogIndex</code>,就需要算成实际的下标来使用,否则出现的情况是,一个没有数据的新follower会因为term较高而被选举为leader。</p><p>解决为下标问题,Lab3B就没有什么大的问题了。总的来说,Lab3需要谨慎规避死锁问题,需要在多线程分布式环境下打充足的日志来调试问题。功能点上可能不是很多,但是很能锻炼工程实现能力和复杂问题处理能力</p>]]></content>
<summary type="html"><p>Lab3的目标是利用Raft的机制,实现一个线性一致(Linearizability)的key-value存储结构。</p></summary>
<category term="Lab" scheme="https://juniousy.github.io/categories/Lab/"/>
<category term="distributed system" scheme="https://juniousy.github.io/tags/distributed-system/"/>
</entry>
<entry>
<title>非递归中序遍历二叉树</title>
<link href="https://juniousy.github.io/2022/09/20/2022/%E9%9D%9E%E9%80%92%E5%BD%92%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E4%BA%8C%E5%8F%89%E6%A0%91/"/>
<id>https://juniousy.github.io/2022/09/20/2022/%E9%9D%9E%E9%80%92%E5%BD%92%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E4%BA%8C%E5%8F%89%E6%A0%91/</id>
<published>2022-09-19T17:37:00.000Z</published>
<updated>2023-06-14T16:06:16.074Z</updated>
<content type="html"><![CDATA[<p>举例相关leetcode题:</p><span id="more"></span><h3 id="94-二叉树的中序遍历"><a href="#94-二叉树的中序遍历" class="headerlink" title="94. 二叉树的中序遍历"></a>94. 二叉树的中序遍历</h3><p>递归我就不说了</p><h4 id="迭代(重点是使用栈):"><a href="#迭代(重点是使用栈):" class="headerlink" title="迭代(重点是使用栈):"></a>迭代(重点是使用栈):</h4><pre><code class="java">class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<Integer>(); Deque<TreeNode> stk = new LinkedList<TreeNode>(); while (root != null || !stk.isEmpty()) { while (root != null) { stk.push(root); root = root.left; } root = stk.pop(); res.add(root.val); root = root.right; } return res; }}</code></pre><h4 id="染色法"><a href="#染色法" class="headerlink" title="染色法"></a>染色法</h4><p>参考资料看到的,也是用到了栈,不过会进行“染色”,规则为:</p><ul><li>使用颜色标记节点的状态,新节点为白色,已访问的节点为灰色。</li><li>如果遇到的节点为白色,则将其标记为灰色,然后将其右子节点、自身、左子节点依次入栈。</li><li>如果遇到的节点为灰色,则将节点的值输出。</li></ul><pre><code class="python">class Solution: def inorderTraversal(self, root: TreeNode) -> List[int]: WHITE, GRAY = 0, 1 res = [] stack = [(WHITE, root)] while stack: color, node = stack.pop() if node is None: continue if color == WHITE: stack.append((WHITE, node.right)) stack.append((GRAY, node)) stack.append((WHITE, node.left)) else: res.append(node.val) return res</code></pre><p>如要实现前序、后序遍历,只需要调整左右子节点的入栈顺序即可。</p><h4 id="Morris-中序遍历"><a href="#Morris-中序遍历" class="headerlink" title="Morris 中序遍历"></a>Morris 中序遍历</h4><p>Morris 遍历算法是另一种遍历二叉树的方法,它能将非递归的中序遍历空间复杂度降为 O(1)。</p><p>Morris 遍历算法整体步骤如下(假设当前遍历到的节点为 xx):</p><ul><li>如果 xx 无左孩子,先将 xx 的值加入答案数组,再访问 xx 的右孩子,即 x = x.\textit{right}x=x.right。</li><li>如果 xx 有左孩子,则找到 xx 左子树上最右的节点(<strong>即左子树中序遍历的最后一个节点,xx 在中序遍历中的前驱节点</strong>),我们记为predecessor。根据 predecessor 的右孩子是否为空,进行如下操作。<ul><li>如果 predecessor 的右孩子为空,则将其右孩子指向 xx,然后访问 xx 的左孩子,即 x=x.left。</li><li>如果 predecessor 的右孩子不为空,则此时其右孩子指向 xx,说明我们已经遍历完 xx 的左子树,我们将 predecessor 的右孩子置空,将 xx 的值加入答案数组,然后访问 xx 的右孩子,即 x=x.right。</li></ul></li><li>重复上述操作,直至访问完整棵树。</li></ul><pre><code class="java">class Solution { public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<Integer>(); TreeNode predecessor = null; while (root != null) { if (root.left != null) { // predecessor 节点就是当前 root 节点向左走一步,然后一直向右走至无法走为止 predecessor = root.left; while (predecessor.right != null && predecessor.right != root) { predecessor = predecessor.right; } // 让 predecessor 的右指针指向 root,继续遍历左子树 if (predecessor.right == null) { predecessor.right = root; root = root.left; } // 说明左子树已经访问完了,我们需要断开链接 else { res.add(root.val); predecessor.right = null; root = root.right; } } // 如果没有左孩子,则直接访问右孩子 else { res.add(root.val); root = root.right; } } return res; }}</code></pre><p>特点是空间复杂度为O(1)、会改变原来树的结构</p><h3 id="98-验证二叉搜索树(给你一个二叉树的根节点-root-,判断其是否是一个有效的二叉搜索树)"><a href="#98-验证二叉搜索树(给你一个二叉树的根节点-root-,判断其是否是一个有效的二叉搜索树)" class="headerlink" title="98. 验证二叉搜索树(给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树)"></a>98. 验证二叉搜索树(给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树)</h3><p>这是一道相关的题目</p><pre><code class="java">class Solution { public boolean isValidBST(TreeNode root) { Deque<TreeNode> stack = new LinkedList<>(); long preVal = Long.MIN_VALUE; TreeNode node = root; while(!stack.isEmpty() || node != null) { while(node != null) { stack.push(node); node = node.left; } node = stack.pop(); if(node.val <= preVal) { return false; } preVal = node.val; node = node.right; } return true; }}</code></pre><p>另外看到这题有一个很优雅的写法:</p><pre><code class="java">class Solution { public boolean isValidBST(TreeNode root) { return isValid(root, Long.MIN_VALUE, Long.MAX_VALUE); } private boolean isValid(TreeNode node, long min, long max) { if (node == null) { return true; } if (node.val <= min || node.val >= max){ return false; } return isValid(node.left, min, node.val) && isValid(node.right, node.val, max); }}</code></pre>]]></content>
<summary type="html"><p>举例相关leetcode题:</p></summary>
<category term="开发" scheme="https://juniousy.github.io/categories/%E5%BC%80%E5%8F%91/"/>
<category term="算法" scheme="https://juniousy.github.io/tags/%E7%AE%97%E6%B3%95/"/>
</entry>
<entry>
<title>MIT 6.824 Lab2C</title>
<link href="https://juniousy.github.io/2022/08/29/2022/6.824Lab2C/"/>
<id>https://juniousy.github.io/2022/08/29/2022/6.824Lab2C/</id>
<published>2022-08-29T15:30:30.000Z</published>
<updated>2023-06-14T16:06:07.802Z</updated>
<content type="html"><![CDATA[<p>2C的持久化本身比较简单,就是把currentTerm、voteFor、logEntries存和取就行。需要注意的点的存数据的时间点应该在这三个值有变化的时候,准确来说是这两个地方:一个是在变为follower和candidate的时候,另一个是在commit data之后。另外尤其强调是在“之后”做持久化,否则会把未提交的数据保留下来。</p><span id="more"></span><p>但是实际跑测试的时候有一个测试怎么都通不过,试了很多次,然后试了很多日志终于发现不是持久化的问题。</p><pre><code class="text">GO111MODULE=off go test -run 2C Test (2C): basic persistence ... ... Passed -- 5.6 3 100 24433 6Test (2C): more persistence ... ... Passed -- 18.0 5 988 204042 16Test (2C): partitioned leader and one follower crash, leader restarts ... ... Passed -- 2.4 3 34 8699 4Test (2C): Figure 8 ... ... Passed -- 31.4 5 560 106887 8Test (2C): unreliable agreement ... ... Passed -- 5.6 5 216 76193 246Test (2C): Figure 8 (unreliable) ...--- FAIL: TestFigure8Unreliable2C (45.35s) config.go:478: one(8275) failed to reach agreementTest (2C): churn ... ... Passed -- 16.3 5 608 284683 268Test (2C): unreliable churn ... ... Passed -- 16.2 5 932 502715 249FAILexit status 1</code></pre><p>TestFigure8Unreliable2C是在测什么呢?对于普通的Figure 8 test,测试方法会不断地断线、宕机、重连服务,而加上Unreliable这个设定会让rpc请求随机失败或者延迟。通过Figure 8 test的核心点在于不要存未commit的数据(具体图见论文Figure 8)。</p><p>而TestFigure8Unreliable2C的失败结果初看非常奇怪。在跑了无数次TestFigure8Unreliable2C后发现错误的结果大概率是这样的:5个server,4个commit结果完成一致,而另一个则完全没有commit或者只commit了前面很少的一部分,甚至可能有全量的log data但是一条都没有提交。</p><p>以这次测试结果为例:</p><pre><code class="text">Test (2C): Figure 8 (unreliable) ...*** server 1 成为 leader, currentTerm 1 ***leader 1 更新commitIndex 1, 原commitIndex 0*** server 4 成为 leader, currentTerm 3 ****** server 3 成为 leader, currentTerm 4 ****** server 3 成为 leader, currentTerm 7 ***leader 3 更新commitIndex 17, 原commitIndex 0leader 3 更新commitIndex 20, 原commitIndex 17*** server 0 成为 leader, currentTerm 10 ****** server 2 成为 leader, currentTerm 11 ****** server 1 成为 leader, currentTerm 36 ****** server 0 成为 leader, currentTerm 49 ****** server 1 成为 leader, currentTerm 51 ***leader 1 更新commitIndex 184, 原commitIndex 20*** server 4 成为 leader, currentTerm 55 ****** server 4 成为 leader, currentTerm 64 ****** server 3 成为 leader, currentTerm 77 ****** server 3 成为 leader, currentTerm 93 ***leader 3 更新commitIndex 327, 原commitIndex 184leader 3 更新commitIndex 328, 原commitIndex 327leader 3 更新commitIndex 329, 原commitIndex 328leader 3 更新commitIndex 330, 原commitIndex 329</code></pre><p>最后结果是server1-4的结果都是对的,只有server0的commit index停留在20。</p><p>为什么呢?再打日志发现,TestFigure8Unreliable2C最后会让服务全部连线上(但是rpc请求还是可能失败),此时leader不断地向原来被分割开的服务发送同样的请求,而返回也是同样的coflict term 和 conflict index。这两个值是server告诉leader,你传的数据不对,要从这个地方重新传。</p><p>看下原来leader处的处理:</p><pre><code class="go">// 有冲突的情况if reply.ConflictTerm != -1 { for i, v := range rf.logEntries { if v.Term == reply.ConflictTerm { rf.nextIndex[id] = i break } }} else { rf.nextIndex[id] = reply.ConflictIndex}</code></pre><p>从表现上看,这里一直在根据coflict term决定下一次传的index id会陷入调用上的死循环。所以在此处,更应该注意follower自己发的conflict index。于是修改后加了一条逻辑:</p><pre><code class="go">if reply.ConflictTerm != -1 { if rf.logEntries[reply.ConflictIndex].Term == reply.ConflictTerm { for i, v := range rf.logEntries { if v.Term == reply.ConflictTerm { rf.nextIndex[id] = i break } } } else { rf.nextIndex[id] = reply.ConflictIndex }} else { rf.nextIndex[id] = reply.ConflictIndex}</code></pre><p>这样做就解决了问题,最后顺利通过全部测试。</p><pre><code class="text">GO111MODULE=off go test -run 2C Test (2C): basic persistence ... ... Passed -- 4.2 3 114 29456 6Test (2C): more persistence ... ... Passed -- 22.7 5 2125 472533 19Test (2C): partitioned leader and one follower crash, leader restarts ... ... Passed -- 3.9 3 102 28521 4Test (2C): Figure 8 ... ... Passed -- 35.1 5 676 119626 7Test (2C): unreliable agreement ... ... Passed -- 3.0 5 216 75708 246Test (2C): Figure 8 (unreliable) ... ... Passed -- 37.3 5 4612 7879256 172Test (2C): churn ... ... Passed -- 16.3 5 1240 1145184 697Test (2C): unreliable churn ... ... Passed -- 16.1 5 1220 879587 526PASSok .../src/raft 138.617s</code></pre><p>最后,我自己总结出的本项目的打日志方法,最主要的是这么几个地方:选取出leader处、leader提交数据处。其他地方也需要酌情打日志,但是很可能信息太多,淹没了有用的信息。</p><p>Lab的测试期望在4分钟内跑完,最后2A8秒,2B约38秒,2C约138秒,至少在我的电脑上达到标准了。</p>]]></content>
<summary type="html"><p>2C的持久化本身比较简单,就是把currentTerm、voteFor、logEntries存和取就行。需要注意的点的存数据的时间点应该在这三个值有变化的时候,准确来说是这两个地方:一个是在变为follower和candidate的时候,另一个是在commit data之后。另外尤其强调是在“之后”做持久化,否则会把未提交的数据保留下来。</p></summary>
<category term="Lab" scheme="https://juniousy.github.io/categories/Lab/"/>
<category term="distributed system" scheme="https://juniousy.github.io/tags/distributed-system/"/>
</entry>
<entry>
<title>MIT 6.824 Lab2B</title>
<link href="https://juniousy.github.io/2022/08/09/2022/6.824Lab2B/"/>
<id>https://juniousy.github.io/2022/08/09/2022/6.824Lab2B/</id>
<published>2022-08-08T17:01:45.000Z</published>
<updated>2023-06-14T16:06:02.866Z</updated>
<content type="html"><![CDATA[<p>这部分是Raft的核心,先上通过记录。</p><span id="more"></span><pre><code class="text">GO111MODULE=off go test -run 2BTest (2B): basic agreement ... ... Passed -- 1.1 3 16 4688 3Test (2B): RPC byte count ... ... Passed -- 2.8 3 48 114908 11Test (2B): agreement despite follower disconnection ... ... Passed -- 6.7 3 124 33767 8Test (2B): no agreement if too many followers disconnect ... ... Passed -- 4.2 5 212 42984 3Test (2B): concurrent Start()s ... ... Passed -- 0.8 3 10 2890 6Test (2B): rejoin of partitioned leader ... ... Passed -- 6.9 3 172 43180 4Test (2B): leader backs up quickly over incorrect follower logs ... ... Passed -- 28.6 5 2308 1695419 103Test (2B): RPC counts aren't too high ... ... Passed -- 5.8 3 100 29488 12PASSok ../MIT6.824/src/raft 56.896s</code></pre><p>一开始进行任务拆分。实际上除了任务1和2,对其他任务是一起推进实现的,因为很多逻辑都是上下游的关系。</p><ol><li>实现Start方法,实现leader自己更新记录</li><li>server通过applyCh返回结果</li><li>leader向follower发送AppendEntries</li><li>follower接收处理AppendEntries</li><li>leader得到follower返回结果(或返回失败)时的处理</li><li>确保选举成功的candidate包含所有已提交的记录(实现election restriction)</li></ol><h2 id="实现细节"><a href="#实现细节" class="headerlink" title="实现细节"></a>实现细节</h2><h3 id="1-Start方法和Apply方法"><a href="#1-Start方法和Apply方法" class="headerlink" title="1 Start方法和Apply方法"></a>1 Start方法和Apply方法</h3><p>Start方法就是业务层向Raft层提交任务的唯一入口。Leader在这个方法里添加log <code>rf.logEntries = append(rf.logEntries, Log{rf.currentTerm, command})</code>。有个小细节是,可以在此处设置 <code>rf.matchIndex[rf.me] = index</code>,index为最后一个元素的index。这样做的好处是在后续计算能否commit记录时好处理一些。</p><p>Apply方法就是Raft层向业务层返回结果的唯一出口。</p><pre><code class="go">type ApplyMsg struct { CommandValid bool Command interface{} CommandIndex int}type Raft struct { //... applyCh chan ApplyMsg //...}func (rf *Raft) startApplyLogs() { for rf.lastApplied < rf.commitIndex { msg := ApplyMsg{} rf.lastApplied++ msg.CommandIndex = rf.lastApplied msg.Command = rf.logEntries[rf.lastApplied].Command msg.CommandValid = true //DPrintf("server %d: 开始应用log, commitIndex: %d, lastApplied: %d, msg %v ", rf.me, rf.commitIndex, rf.lastApplied, msg) rf.applyCh <- msg }}</code></pre><h3 id="2-AppendEntries"><a href="#2-AppendEntries" class="headerlink" title="2 AppendEntries"></a>2 AppendEntries</h3><p>server在接受AppendEntries时,至少要有以下校验逻辑:</p><ol><li>如果args.PrevLogIndex比自身log最后一个元素的index还要大,就返回<code>ConflictIndex=len(rf.logEntries)</code></li><li>如果args.PrevLogTerm和自身的PrevLogTerm不同,返回<code>ConflictTerm = thisPrevLogTerm</code>、<code>ConflictIndex</code>为该Term下的第一个index</li></ol><p>普通server的commitIndex更新逻辑:</p><pre><code class="go"> if args.LeaderCommit > rf.commitIndex { rf.commitIndex = int(math.Min(float64(args.LeaderCommit), float64(len(rf.logEntries)-1))) }</code></pre><p>leader在发送AppendEntries后,要对server返回结果进行处理,至少要对以下异常情况进行校验:</p><ol><li>没有收到RPC结果,则不进行后续逻辑</li><li>term小于返回值的term,则变为leader</li><li>ConflictTerm或ConflictIndex有值时,重设<code>nextIndex[id] //id为server的id</code>,返回,等待下一次发送时从冲突处发送记录</li></ol><p>最后实现Figure 2中的这条:如果存在 N,N > commitIndex,大部分matchIndex[i] >= N,log[N].term == currentTerm,那么,就设置commitIndex = N。Leader的commitIndex在此处更新。</p><h3 id="3-election-restriction"><a href="#3-election-restriction" class="headerlink" title="3 election restriction"></a>3 election restriction</h3><p>按照up-to-date的定义,follower在vote时要判断该布尔值:<code>upToDate := args.LastLogTerm < lastLogTerm || (args.LastLogTerm == lastLogTerm && args.LastLogIndex < lastLogIndex)</code><br>如果为true,说明follower不该向这个candidate投出这票,因为follower相对这个candidate更有资格成为leader。这个判断很关键,后续问题点3这个例子就是不判断时会出现的情况。</p><h2 id="实现中遇到的问题"><a href="#实现中遇到的问题" class="headerlink" title="实现中遇到的问题"></a>实现中遇到的问题</h2><h3 id="问题点1-TestBasicAgree2B"><a href="#问题点1-TestBasicAgree2B" class="headerlink" title="问题点1 - TestBasicAgree2B"></a>问题点1 - TestBasicAgree2B</h3><p>TestBasicAgree2B 偶尔会无法通过。 </p><p>多试几次后发现,原因是 leader还未发送AppendEntries就有新的candidate出现并成功选举为leader。而前leader并没有丢弃自身的log,导致不一致。</p><pre><code class="text">2022/08/06 21:46:22 @@@ Leader 2: got a new Start task, command: 1002022/08/06 21:46:22 server 1 成为 candidate, currentTerm 22022/08/06 21:46:22 === Candidate 1 开始发送 RequestVote, currentTerm 2 ===2022/08/06 21:46:22 server 2 (term 1 voteFor 2) 收到 candidiate 1 (term 2 candidateId 1) 的RequestVote2022/08/06 21:46:22 server 2 成为 follower,currentTerm 1 ==> 2, leader id 2 ==> -12022/08/06 21:46:22 server 0 (term 1 voteFor 2) 收到 candidiate 1 (term 2 candidateId 1) 的RequestVote2022/08/06 21:46:22 server 0 投票给 server 12022/08/06 21:46:22 *** server 1 成为 leader, currentTerm 2 ***2022/08/06 21:46:22 === server 0 处理 Leader 1 的 AppendEntries 成功,当前logEntries [] ===2022/08/06 21:46:22 === server 2 处理 Leader 1 的 AppendEntries 成功,当前logEntries [{1 100}] ===</code></pre><p>一方面 server 要放弃之后的内容</p><pre><code class="go">... endIndex := args.PrevLogIndex + len(args.Entries) + 1 if endIndex < len(rf.logEntries) { rf.logEntries = rf.logEntries[:endIndex] }...</code></pre><p>另一方面,根据election restriction机制,一个candidate必须包含所有已提交的entries。具体实现方式是:投票这如果发现自己的log比candidate更新,则不投票。<br>更(第四声)新(up-to-date)的含义是,如果末尾entries的term更大则更新,如果term一样,则log长度长的算更新。具体实现见上文。</p><p>在本例中,尽管实现了这一机制,但是3个server中,哪怕之前收到log的前leader2没有投票,server1 有自己一票和server0 一票,依然能当选。</p><p>再仔细想一下,这样的结果其实是正确的,因为该entry确实没有被提交。但TestBasicAgree2B是要求每次请求都成功写入的(毕竟确实没有异常出现)。究其原因,最大的问题还是出在选举时,server2明明已经选上leader了,结果server1没有收到心跳。</p><p>再检查一下代码:</p><pre><code class="go">func (rf *Raft) candidateMainFlow() { ... rf.startRequestVote() // line a 此处会成为leader time.Sleep(time.Duration(electionTimeout) * time.Millisecond) // line b 此处会等待 rf.mu.Lock() if rf.state == CANDIDATE && !rf.heartBeat { rf.convertToCandidate() } rf.mu.Unlock()}</code></pre><p>成为leader后没有立刻发送心跳,反而进行了一次等待。所以应该在上述代码中的line a 和 line b中间发送一次空的AppendEntries</p><pre><code class="go"> rf.mu.Lock() isLeader := rf.state == LEADER rf.mu.Unlock() if isLeader { rf.startAppendEntries() return }</code></pre><p>但是这样还是会出错。检查发现测试程序一但发现有leader产生之后就会写命令。所以最后引入了firstHeartBeat这个布尔值,在第一次心跳发送前,不允许接受命令,也就是Start方法处的返回结果isLeader是false。这实际是个非标准的做法,不过确实有用。</p><p>单改这个地方还不够,另一个测试如果不能在发现有leader后立刻能够插入数据也会报错,所以GetState处的结果也不能立刻返回是不是leader,最后加了一下比较tricky的解决:</p><pre><code class="go">func (rf *Raft) GetState() (int, bool) { var term int var isleader bool // Your code here (2A). rf.mu.Lock() term = rf.currentTerm isleader = rf.state == LEADER for isleader && !rf.firstHeartBeat { rf.mu.Unlock() time.Sleep(time.Duration(5) * time.Millisecond) rf.mu.Lock() isleader = rf.state == LEADER } rf.mu.Unlock() return term, isleader}</code></pre><h3 id="问题点2-TestFailNoAgree2B"><a href="#问题点2-TestFailNoAgree2B" class="headerlink" title="问题点2 - TestFailNoAgree2B"></a>问题点2 - TestFailNoAgree2B</h3><p>TestFailNoAgree2B 会在5个server中让3个server(不包含原leader)离线,然后进行操作。具体出现问题的测试代码为:</p><pre><code class="go">// 3 of 5 followers disconnect leader := cfg.checkOneLeader() cfg.disconnect((leader + 1) % servers) cfg.disconnect((leader + 2) % servers) cfg.disconnect((leader + 3) % servers) index, _, ok := cfg.rafts[leader].Start(20) if ok != true { t.Fatalf("leader rejected Start()") } if index != 2 { t.Fatalf("expected index 2, got %v", index)</code></pre><p>这时候由于半数宕机,理论上命令不会被写入。不过在我一开始的实现中,没有离线的两个server依然会写入,这样就有问题。</p><p>检查代码发现是applyIndex和commitIndex的赋值存在bug,解决后通过。</p><h3 id="问题点3-TestRejoin2B"><a href="#问题点3-TestRejoin2B" class="headerlink" title="问题点3 - TestRejoin2B"></a>问题点3 - TestRejoin2B</h3><p>TestRejoin2B 是这么操作的:</p><pre><code class="text">leader1 写入 101leader1 离线leader1 写入 102 103 104leader2 (新的leader) 写入 103leader2 离线leader1 上线leader1 写入104leader2 上线写入 105</code></pre><p>遇到的问题是leader1重新上线后无法选举出新的leader。如下面的例子,server2 和 server 0互相都不投给对方vote</p><pre><code class="text">connect(2)2022/08/07 00:41:56 @@@ Leader 2: got a new Start task, command: 1042022/08/07 00:41:56 Leader 2 发送给 server 1 AppendEntries,args: {Term:1 LeaderId:2 PrevLogIndex:1 PrevLogTerm:1 Entries:[{Term:1 Command:102} {Term:1 Command:103} {Term:1 Command:104} {Term:1 Command:104}] LeaderCommit:1}2022/08/07 00:41:56 Leader 2 发送给 server 0 AppendEntries,args: {Term:1 LeaderId:2 PrevLogIndex:1 PrevLogTerm:1 Entries:[{Term:1 Command:102} {Term:1 Command:103} {Term:1 Command:104} {Term:1 Command:104}] LeaderCommit:1}2022/08/07 00:41:56 server 2 成为 follower,currentTerm 1 ==> 2, leader id 2 ==> -12022/08/07 00:41:57 server 0 成为 candidate, currentTerm 32022/08/07 00:41:57 === Candidate 0 开始发送 RequestVote, currentTerm 3 ===2022/08/07 00:41:57 server 2 (term 2 voteFor -1) 收到 candidiate 0 (term 3 candidateId 0) 的RequestVote2022/08/07 00:41:57 server 2 成为 candidate, currentTerm 42022/08/07 00:41:57 === Candidate 2 开始发送 RequestVote, currentTerm 4 ===2022/08/07 00:41:57 server 0 (term 3 voteFor 0) 收到 candidiate 2 (term 4 candidateId 2) 的RequestVote2022/08/07 00:41:57 server 0 成为 follower,currentTerm 3 ==> 4, leader id 0 ==> -12022/08/07 00:41:57 server 2 成为 candidate, currentTerm 5</code></pre><p>这个问题是由于vote时候判断up-to-date的逻辑有问题导致的,上面已经讲过。修改后这个问题解决。</p>]]></content>
<summary type="html"><p>这部分是Raft的核心,先上通过记录。</p></summary>
<category term="Lab" scheme="https://juniousy.github.io/categories/Lab/"/>
<category term="distributed system" scheme="https://juniousy.github.io/tags/distributed-system/"/>
</entry>
<entry>
<title>MIT 6.824 Lab2A</title>
<link href="https://juniousy.github.io/2022/07/18/2022/6.824Lab2A/"/>
<id>https://juniousy.github.io/2022/07/18/2022/6.824Lab2A/</id>
<published>2022-07-17T17:18:50.000Z</published>
<updated>2023-06-14T16:05:55.330Z</updated>
<content type="html"><![CDATA[<p>光看记录的一些笔记和要点确实有点一头雾水,不知道从何下手,好在Lab分阶段进行,先从最基本的地方开始。2A的测试,一个是正常情况下选举出leader,另一个是模拟leader宕机后重新选举。完成记录如下:</p><span id="more"></span><pre><code class="text">GO111MODULE=off go test -v -run 2A2022/06/26 14:41:54 server 0 成为 follower,currentTerm 0 ==> 0, leader id 0 ==> -12022/06/26 14:41:54 server 1 成为 follower,currentTerm 0 ==> 0, leader id 0 ==> -12022/06/26 14:41:54 server 2 成为 follower,currentTerm 0 ==> 0, leader id 0 ==> -1Test (2A): initial election ...2022/06/26 14:41:55 server 2 成为 candidate, currentTerm 12022/06/26 14:41:55 === Candidate 2 开始发送 RequestVote, currentTerm 1 ===2022/06/26 14:41:55 server 0 (term 0 voteFor -1) 收到 candidiate 2 (term 1 candidateId 2) 的RequestVote2022/06/26 14:41:55 server 0 投票给 server 22022/06/26 14:41:55 server 1 (term 0 voteFor -1) 收到 candidiate 2 (term 1 candidateId 2) 的RequestVote2022/06/26 14:41:55 server 1 投票给 server 22022/06/26 14:41:55 server 2 成为 leader, currentTerm 1... ... Passed -- 3.1 3 52 12924 0--- PASS: TestInitialElection2A (3.07s)...2022/06/26 14:41:59 === Leader 1 开始发送 AppendEntries ===2022/06/26 14:41:59 === server 2 收到来自 Leader 1 的 AppendEntries ===2022/06/26 14:41:59 === server 0 收到来自 Leader 1 的 AppendEntries ===...2022/06/26 14:42:01 === Leader 1 开始发送 AppendEntries ===2022/06/26 14:42:01 server 2 成为 candidate, currentTerm 102022/06/26 14:42:01 === Candidate 2 开始发送 RequestVote, currentTerm 10 ===2022/06/26 14:42:01 server 0 (term 7 voteFor 0) 收到 candidiate 2 (term 10 candidateId 2) 的RequestVote2022/06/26 14:42:01 server 0 成为 follower,currentTerm 7 ==> 10, leader id 0 ==> -12022/06/26 14:42:01 server 0 投票给 server 22022/06/26 14:42:01 server 2 成为 leader, currentTerm 10... ... Passed -- 4.5 3 100 17870 0--- PASS: TestReElection2A (4.50s)PASS</code></pre><p>开始写Lab时,首先进行任务拆分:</p><ol><li>结构定义</li><li>新建一个实例时,新建核心循环线程</li><li>实现仅进行心跳检测的AppendEntries RPC,包括发送和接受</li><li>判断超过election timeout后,开启election</li><li>candidate成功选为leader的情况</li><li>candidate收到其他candidate当选的情况</li><li>candidate进入新一轮选举的情况</li><li>随机化的election timeouts (按照lab要求要略微大于 150-300ms 这个范围)</li><li>实现 GetState ,使其能够获取实例的状态</li></ol><h2 id="实现细节"><a href="#实现细节" class="headerlink" title="实现细节"></a>实现细节</h2><h3 id="1-结构定义"><a href="#1-结构定义" class="headerlink" title="1 结构定义"></a>1 结构定义</h3><p>实际开发中,定义了身份状态(没想到golang用这种方式定义枚举)</p><pre><code class="go">const ( LEADER int = 0 CANDIDATE = 1 FOLLOWER = 2)</code></pre><p>结构体主要见论文 Figure 2 中提到的字段。除此以外,Raft类里我还定义了如下字段。后续Lab中估计还会有新的</p><pre><code class="go">... state int electionTimeout int heartBeat bool takenVote bool voteCnt int...</code></pre><p>解释一下,state是身份状态,electionTimeout是记录的延迟时间,voteCnt是计票是用的。重点说下heartBeat和takenVote,这两个代表的是一个election timeout内有没有收到心跳,有没有发送投票。按照规则,如果election timeout到了,没有收到心跳并不代表一定转为candidate,发送投票了也是就继续保留为follower。自己实现时候一开始漏了这个点,结果就是server不断地从follower变为candidate再变为follower,永远选不出leader。</p><h3 id="2-核心流程"><a href="#2-核心流程" class="headerlink" title="2 核心流程"></a>2 核心流程</h3><p>核心流程的入口是在新建实例(Make方法)时,新起的循环线程,只要不kill都会跑下去,我把它命名为<code>mainFlow</code>。在这里,leader做的事情是发送AppendEntries,Candidate做的事情是发送RequestVote(如果到了electionTimeout还是Candidate身份,就重新发起一次election),follower做的事情是判断要不要变为Candidate。</p><pre><code class="go">func (rf *Raft) mainFlow() { for !rf.killed() { rf.mu.Lock() state := rf.state rf.mu.Unlock() switch state { case LEADER: rf.startAppendEntries() time.Sleep(time.Duration(100) * time.Millisecond) case CANDIDATE: rf.candidateMainFlow() case FOLLOWER: rf.followerMainFlow() } }}</code></pre><h3 id="3-RPC"><a href="#3-RPC" class="headerlink" title="3 RPC"></a>3 RPC</h3><p>发送AppendEntries和发送RequestVote的要点:一个是对peers要异步发送,同步发送信息是不可接受的:</p><pre><code class="go">for i := range rf.peers { go func(ii int) { // ... }(i)}</code></pre><p>在Lab2A中,AppendEntries只包含空的内容,比较好实现。不过要实现对参数和返回值中的term进行判断的逻辑</p><p>另一个要点是,我在每次加锁之后都先判断一下state。两段锁之间状态的值是有可能改变的,因此必须加以校验。</p><h3 id="4-辅助方法"><a href="#4-辅助方法" class="headerlink" title="4 辅助方法"></a>4 辅助方法</h3><p>获取election timeout的方法,在转变为follower、candidate时重新获取:</p><pre><code class="golang">func (rf *Raft) genNewElectionTimeout() { r := rand.New(rand.NewSource(time.Now().UnixNano())) const min = 200 const max = 400 electionTimeout := r.Intn(max-min) + min rf.electionTimeout = electionTimeout}</code></pre><p>最后有三个实用的方法,会在各处多次被调用:<code>convertToFollower</code>、<code>convertToCandidate</code>、<code>convertToLeader</code>。<code>convertToFollower</code>是需要参数的,需要currentTerm和voteFor,将这两个值更新,然后重新随机生成electionTimeout。<code>convertToCandidate</code>会递增currentTerm,然后给自己投票,重新随机生成electionTimeout。<code>convertToLeader</code>会初始化nextIndex和matchIndex,这两个目前Lab2A没用到,之后再细说。</p>]]></content>
<summary type="html"><p>光看记录的一些笔记和要点确实有点一头雾水,不知道从何下手,好在Lab分阶段进行,先从最基本的地方开始。2A的测试,一个是正常情况下选举出leader,另一个是模拟leader宕机后重新选举。完成记录如下:</p></summary>
<category term="Lab" scheme="https://juniousy.github.io/categories/Lab/"/>
<category term="distributed system" scheme="https://juniousy.github.io/tags/distributed-system/"/>
</entry>
<entry>
<title>MIT 6.824 Lab2 - Raft - Lab2前准备的笔记</title>
<link href="https://juniousy.github.io/2022/06/12/2022/6.824Lab2-Lab2%E5%89%8D%E5%87%86%E5%A4%87%E7%9A%84%E7%AC%94%E8%AE%B0/"/>
<id>https://juniousy.github.io/2022/06/12/2022/6.824Lab2-Lab2%E5%89%8D%E5%87%86%E5%A4%87%E7%9A%84%E7%AC%94%E8%AE%B0/</id>
<published>2022-06-12T06:58:41.090Z</published>
<updated>2023-06-14T16:05:48.606Z</updated>
<content type="html"><![CDATA[<p>做lab前先整理一下要点和课前提醒,做一个笔记记录,可跳过,主要在实现时对照着看。</p><span id="more"></span><h1 id="Raft概述"><a href="#Raft概述" class="headerlink" title="Raft概述"></a>Raft概述</h1><p>Raft是一个用来实现分布式一致的协议。Raft is a protocol for implementing distributed consensus.</p><pre><code class="text">Raft is a consensus algorithm that is designed to be easy to understand. It’s equivalent to Paxos in fault-tolerance and performance. The difference is that it’s decomposed into relatively independent subproblems, and it cleanly addresses all major pieces needed for practical systems. We hope Raft will make consensus available to a wider audience, and that this wider audience will be able to develop a variety of higher quality consensus-based systems than are available today.</code></pre><p>主要分为Lead Election和Log Replication阶段</p><h4 id="Log-Replication-阶段流程概述:"><a href="#Log-Replication-阶段流程概述:" class="headerlink" title="Log Replication 阶段流程概述:"></a>Log Replication 阶段流程概述:</h4><ul><li>为了commit the log entry,leader node首先向follower nodes复制自己</li><li>leader等待大部分node写入entry</li><li>entry 提交,leader status改变</li><li>leader 通知followers entry已经提交了</li><li>cluster 的系统状态成为一致(consensus)</li></ul><h4 id="Lead-Election-阶段流程概述:"><a href="#Lead-Election-阶段流程概述:" class="headerlink" title="Lead Election 阶段流程概述:"></a>Lead Election 阶段流程概述:</h4><ul><li>election timeout:时间结束后,follower变为candidate,发起election,发送request vote</li><li>candidate 被大部分note vote后,变为leader</li><li>leader向followers发送Append Entries,按 heartbeat timeout 间歇发送</li><li>follower也向leaderAppend Entries,作为心跳检测</li><li>同时有两个candidate,就重新进入election timeout等待,重新发起election</li></ul><h1 id="Raft精要"><a href="#Raft精要" class="headerlink" title="Raft精要"></a>Raft精要</h1><p>内容是论文中的Figure 2。</p><p><img src="/raft.png"></p><h3 id="1-State"><a href="#1-State" class="headerlink" title="1. State"></a>1. State</h3><p>关于服务器状态的实现</p><h4 id="所有服务器-持久化状态"><a href="#所有服务器-持久化状态" class="headerlink" title="所有服务器 - 持久化状态"></a>所有服务器 - 持久化状态</h4><p>在进行RPC回复前进行持久化</p><ul><li><strong>currentTerm</strong> 最近server看到的term (0开始,单调增)</li><li><strong>voteFor</strong> 当前term下被推举的candidate Id</li><li><strong>log[]</strong> log entries (记录条目)。每个entry包含状态机指令,和收到leader发出的entry时的term(起始值为1)</li></ul><h4 id="所有服务器-可变状态"><a href="#所有服务器-可变状态" class="headerlink" title="所有服务器 - 可变状态"></a>所有服务器 - 可变状态</h4><ul><li><strong>commitIndex</strong> 最近一次被提交的log entry序号(0开始,单调增)</li><li><strong>lastApplied</strong> 最近一次被应用的log entry序号(0开始,单调增)</li></ul><h4 id="leaders-可变状态"><a href="#leaders-可变状态" class="headerlink" title="leaders - 可变状态"></a>leaders - 可变状态</h4><p>选举后重新初始化</p><ul><li><strong>nextIndex[]</strong> 对每个server,最近发送的log entry序号(原index+1)</li><li><strong>matchIndex[]</strong> 对每个server,最近知晓的复制成功的log entry序号</li></ul><h3 id="2-AppendEntries-RPC"><a href="#2-AppendEntries-RPC" class="headerlink" title="2. AppendEntries RPC"></a>2. AppendEntries RPC</h3><p>功能为复制log entries和心跳检测</p><h4 id="参数"><a href="#参数" class="headerlink" title="参数"></a>参数</h4><ul><li><strong>term</strong> leader的term</li><li><strong>leaderId</strong> </li><li><strong>prevLogIndex</strong> </li><li><strong>prevLogTerm</strong></li><li><strong>entries</strong> 要存的log entries (空为心跳;可能一次传多个)</li><li><strong>leaderCommit</strong> leader的commitIndex</li></ul><h4 id="结果"><a href="#结果" class="headerlink" title="结果"></a>结果</h4><ul><li><strong>term</strong> currentTerm,leader用来更新自己</li><li><strong>success</strong> 成功时,表示follower包含符合prevLogIndex和prevLogTerm的entry</li></ul><h4 id="Receiver实现"><a href="#Receiver实现" class="headerlink" title="Receiver实现"></a>Receiver实现</h4><ol><li>如果 term < currentTerm , 返回false</li><li>如果 prevLogIndex处的entry不匹配prevLogTerm,返回false</li><li>如果现有的entry和新的冲突(index一样但是term不一样),从这个entry开始删除到最新。</li><li>插入新的entries</li><li>如果 leaderCommit > commitIndex,设置 commitIndex = min(leaderCommit, 最新entry的序号)</li></ol><h3 id="3-RequestVote-RPC"><a href="#3-RequestVote-RPC" class="headerlink" title="3. RequestVote RPC"></a>3. RequestVote RPC</h3><p>candidate收集选票时触发</p><h4 id="参数-1"><a href="#参数-1" class="headerlink" title="参数"></a>参数</h4><ul><li><strong>term</strong> candidate的term</li><li><strong>candidateId</strong> </li><li><strong>lastLogIndex</strong> candidate最后一个log entry的序号</li><li><strong>lastLogTerm</strong> candidate最后一个log entry的term</li></ul><h4 id="结果-1"><a href="#结果-1" class="headerlink" title="结果"></a>结果</h4><ul><li><strong>term</strong> 当前term</li><li><strong>voteGranted</strong> true表示投一票</li></ul><h4 id="Receiver实现-1"><a href="#Receiver实现-1" class="headerlink" title="Receiver实现"></a>Receiver实现</h4><ol><li>如果term < currentTerm 返回false</li><li>如果votedFor是空或者是candidateId,然后candidate的log和receiver的log比不滞后,就投票同意</li></ol><h3 id="4-Servers的实现规则"><a href="#4-Servers的实现规则" class="headerlink" title="4. Servers的实现规则"></a>4. Servers的实现规则</h3><h4 id="所有servers"><a href="#所有servers" class="headerlink" title="所有servers"></a>所有servers</h4><ul><li>如果 commitIndex > lastApplied:lastApplied+1,将log[lastApplied]应用到状态机中</li><li>如果RPC请求或返回包含 term T > currentTerm:将currentTerm设为T,自己转为follower</li></ul><h4 id="Followers"><a href="#Followers" class="headerlink" title="Followers"></a>Followers</h4><ul><li>向candidates和leaders的RPC回复</li><li>如果election timeout到了之后,没有收到leader的AppendEntries RPC或收到candidate的投票请求,就自己转为candidate</li></ul><h4 id="Candidates"><a href="#Candidates" class="headerlink" title="Candidates"></a>Candidates</h4><ul><li>转变成candidate,发起选举<ul><li>currentTerm + 1</li><li>投自己一票</li><li>重设election timer</li><li>向所有其他服务发送RequestVote RPC</li></ul></li><li>如果得到大部分服务的投票,成为leader</li><li>如果收到新leader的AppendEntries RPC,成为follower</li><li>如果election timeout到了,开始新选举</li></ul><h4 id="Leaders"><a href="#Leaders" class="headerlink" title="Leaders"></a>Leaders</h4><ul><li>选举一旦完成,向各服务发送新的初始化空AppendEntries RPC(心跳),空闲时也重复发送</li><li>如果收到client的命令:在本地log中新增entry,在状态机上应用entry后返回</li><li>如果最近的log序号 >= nextIndex,发送AppendExtries RPC时用nextIndex<ul><li>如果成功:为follower更新nextIndex和matchIndex</li><li>如果失败,那么原因为log不一致,降低nextIndex重试</li></ul></li><li>如果存在 N,N > commitIndex,大部分matchIndex[i] >= N,log[N].term == currentTerm,那么,就设置commitIndex = N</li></ul><h1 id="注意要点"><a href="#注意要点" class="headerlink" title="注意要点"></a>注意要点</h1><p>课程提醒的实现中需要注意的点,一个是Figure 2上的要点要逐一实现,如接受非heart beat AppendEntries 时也要进行相应的检查等;第二个是归纳了四种常见的bug。</p><h4 id="Bugs"><a href="#Bugs" class="headerlink" title="Bugs"></a>Bugs</h4><h5 id="1-live-locks-活锁"><a href="#1-live-locks-活锁" class="headerlink" title="1. live locks - 活锁"></a>1. live locks - 活锁</h5><ol><li>需要妥善处理好重设election timer。如果AppendEntries已经过时了,就不要重设计时器。开始发起选举时要重设。给其他节点投票时要重设,而不是每次收到投票请求时重设(这样有更多最近记录的节点更有可能选上)。</li><li>如果是candidate正在发起选举,但是自己的election timer到时间了,那么就应该开始新的一次选举</li><li>如果已经给出投票,然后有新的RequestVote RPC有更高的term,那应该启用这个term,然后处理RPC</li></ol><h5 id="2-Incorrect-RPC-handlers-错误的PRC处理"><a href="#2-Incorrect-RPC-handlers-错误的PRC处理" class="headerlink" title="2. Incorrect RPC handlers - 错误的PRC处理"></a>2. Incorrect RPC handlers - 错误的PRC处理</h5><ul><li>Figure 2要点中说的“返回false”,意思是立即返回</li><li>如果一个AppendEntries RPC的prevLogIndex比最近的log index要早,那该当做有这个entry但是term不匹配来处理(如返回false)</li><li>对prevLogIndex的检查处理,在leader没有送出entries时也要处理</li><li>leader的commit index大过自身commit index时,要更新为min(leaderCommit, 最新entry的序号)。如果直接改为leaderCommit,会遇到应用错误的entries的情况。</li><li>严格按照要求处理“up-to-date log”。Raft判断的两个log哪个最up-to-date,是通过比较Index和logs中最后的entries的term。如果term最新,那么有最新term的log最up-to-date。如果term一样,那么哪个log的长度(entries的数量)最长就算最up-to-date。</li></ul><h5 id="3-Failure-to-follow-The-Rules-没有正确遵守规则"><a href="#3-Failure-to-follow-The-Rules-没有正确遵守规则" class="headerlink" title="3. Failure to follow The Rules - 没有正确遵守规则"></a>3. Failure to follow The Rules - 没有正确遵守规则</h5><p>Figure 2. 外补充的要点</p><ul><li>任何时候发现commitIndex > lastApplied,应该应用一条log entry到状态机。apply要保证只有一个地方去执行。具体来说,要么有一个专门的applier,要么在apply时加锁。</li><li>检查commitIndex > lastApplied要么周期性,要么在commitIndex更新之后。</li><li>如果leader发出AppendEntries RPC被拒绝时,如果不是因为log不一致,那么应该立即退出,不更新nextIndex。</li><li>leader 不能让其他节点在过时的term中更新commitIndex。因此要判断log[N].term == currentTerm。</li><li>matchIndex和nextIndex的关系不单纯是matchIndex = nextIndex - 1。nextIndex只是一种乐观的猜测。matchIndex是安全保障,用来做判断。</li></ul><h5 id="4-Term-Confusion-term混乱"><a href="#4-Term-Confusion-term混乱" class="headerlink" title="4. Term Confusion - term混乱"></a>4. Term Confusion - term混乱</h5><p>在收到旧term的RPC返回时,只在当前term和请求时的term一致时,处理该PRC返回。</p><p>更新matchIndex正确的操作是设置为prevLogIndex + len(entries[]),这里面的参数是发起请求时的值。</p><h4 id="额外的功能"><a href="#额外的功能" class="headerlink" title="额外的功能"></a>额外的功能</h4><p>这门课除了核心功能,还要求实现log压缩(section 7),快速log回溯(第8页左上方)。</p><h5 id="log压缩"><a href="#log压缩" class="headerlink" title="log压缩"></a>log压缩</h5><p>主要看Figure 13。</p><p><img src="/raft-lc.png"></p><p>leader发送表示一个snapshot的多个chunk的方式</p><h4 id="参数-2"><a href="#参数-2" class="headerlink" title="参数"></a>参数</h4><ul><li><strong>term</strong> leader的term</li><li><strong>leaderId</strong></li><li><strong>lastIncludedIndex</strong> </li><li><strong>lastIncludedTerm</strong></li><li><strong>offset</strong> chunk在snapshot文件中的byte位置偏移量</li><li><strong>data[]</strong> snapshot chunk 的原始数据,从offset开始</li><li><strong>done</strong> true表示为最后一个chunk</li></ul><h4 id="结果-2"><a href="#结果-2" class="headerlink" title="结果"></a>结果</h4><ul><li><strong>term</strong> 当前term</li></ul><h4 id="Receiver-Implementation"><a href="#Receiver-Implementation" class="headerlink" title="Receiver Implementation"></a>Receiver Implementation</h4><ol><li>如果 term < currentTerm,立即返回</li><li>第一个chunk时新建snapshot文件</li><li>在给的offset处写入文件</li><li>如果done是false,返回并等待更多chunk</li><li>保存snapshot文件,其他snapshot文件如果有更小的index,就丢弃</li><li>如果已有的log entry和snapshot的最后一个entry有同样的index和term,保留这之后的log entries</li><li>丢弃整个log</li><li>用snapshot内容重设状态机</li></ol><p>注意事项:</p><ul><li><p>在做snapshot时,应用的状态应该对设计raft log中已经有index应用过的状态。也就是说,要么知道snapshot对应的index,要么raft在完成snapshot前不应用新的log entries。</p></li><li><p>状态和snapshot提交是分开的,所以在此两者之间的崩溃会导致问题,因为此时被snapshot覆盖的log已经丢弃了。解决办法是记录真实的Raft持久化日志第一条内容的index。</p><h5 id="快速log回溯"><a href="#快速log回溯" class="headerlink" title="快速log回溯"></a>快速log回溯</h5></li><li><p>如果follower在log中没有prevLogIndex,那么应该返回conflictIndex = len(log)和conflictTerm = None</p></li><li><p>如果follower在log中有prevLogIndex,但是term对不上,那么返回值conflictTerm = log[prevLogIndex].Term,然后找第一个entry的term等于conflictTerm,回溯到这个index</p></li><li><p>收到冲突返回时,leader应该在log中搜索conflictTerm。如果找到了,就把nextIndex设为这个term最后的index之前的那个index。(这句话没看懂,原文If it finds an entry in its log with that term, it should set nextIndex to be the one beyond the index of the last entry in that term in its log.)</p></li><li><p>接上条,如果没有找到,设置nextIndex = conflictIndex</p></li></ul><h4 id="应用Raft时的注意点"><a href="#应用Raft时的注意点" class="headerlink" title="应用Raft时的注意点"></a>应用Raft时的注意点</h4><h5 id="Applying-client-operations-应用操作"><a href="#Applying-client-operations-应用操作" class="headerlink" title="Applying client operations - 应用操作"></a>Applying client operations - 应用操作</h5><p>服务应该被设计为一个状态机。需要有一个循环去接受用户的操作,和将用户的操作按顺序应用到状态机。这个循环是唯一一处能接触到状态机的地方。</p><p>如何知道用户的请求已经完成?在用户操作时,记录当前log的index,一旦在那个index处的操作被标记为已应用,看该处的操作是不是当时的操作,是表示操作成功,否表示操作失败。</p><h5 id="Duplicate-detection-重复检测"><a href="#Duplicate-detection-重复检测" class="headerlink" title="Duplicate detection - 重复检测"></a>Duplicate detection - 重复检测</h5><p>防止应用两次:每个client有一个id,每次请求有一个单调增id。如果相同clinet的相同请求id已经处理过了,就忽略。</p><h5 id="其他问题"><a href="#其他问题" class="headerlink" title="其他问题"></a>其他问题</h5><p>重复Index,死锁</p><h1 id="附录:"><a href="#附录:" class="headerlink" title="附录:"></a>附录:</h1><ul><li><a href="http://nil.csail.mit.edu/6.824/2020/papers/raft-extended.pdf">In Search of an Understandable Consensus Algorithm<br>(Extended Version)</a></li><li><a href="https://thesquareplanet.com/blog/students-guide-to-raft/">Students’ Guide to Raft</a></li></ul>]]></content>
<summary type="html"><p>做lab前先整理一下要点和课前提醒,做一个笔记记录,可跳过,主要在实现时对照着看。</p></summary>
<category term="Lab" scheme="https://juniousy.github.io/categories/Lab/"/>
<category term="distributed system" scheme="https://juniousy.github.io/tags/distributed-system/"/>
</entry>
<entry>
<title>MIT 6.824 Lab1 - Map Reduce</title>
<link href="https://juniousy.github.io/2022/05/30/2022/6.824Lab1/"/>
<id>https://juniousy.github.io/2022/05/30/2022/6.824Lab1/</id>
<published>2022-05-30T12:00:00.000Z</published>
<updated>2023-06-14T17:26:00.798Z</updated>
<content type="html"><![CDATA[<p>课程要求是不公开自己的代码的,遵守一下规则。这里简单讲讲思路和遇到的问题</p><span id="more"></span><pre><code class="text">--- wc test: PASS*** Starting indexer test.--- indexer test: PASS*** Starting map parallelism test.--- map parallelism test: PASS*** Starting reduce parallelism test.--- reduce parallelism test: PASS*** Starting crash test.--- crash test: PASS*** PASSED ALL TESTS</code></pre><h3 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h3><p>首先思考一下“要做什么”,所以做了一下功能点的拆分</p><pre><code class="md">1. master收到任务(文件名)后负责拆分任务2. worker向master申请任务(可能是map也可能是reduce)3. worker map操作,存入中间文件4. worker reduce操作,读取中间文件,写入文件5. crash worker 的处理6. master判断任务是否已经全部完成;worker结束进程</code></pre><p>master是有状态的,设计如下:</p><pre><code class="go">type Master struct { // Your definitions here. files []string //待处理的文件 mapfTasks map[int]int //key为TaskId,value为task状态0 not start 1 in progress 2 success mapfAllSuccess bool // mapre任务是否都完成 nReduce int // reduce任务的数量 reducefTasks map[int]int // key为TaskId,value为task状态0 not start 1 in progress 2 success reducefTaskFileMap map[int][]string // 一个kv表示一个 reduceId 所需要处理所有文件 done bool // 是否完成 mu sync.Mutex // 锁}</code></pre><p>我的实现用了两个rpc:</p><pre><code class="go">func (m *Master) AskForTask(args struct{}, reply *AskForTaskReply) errorfunc (m *Master) ReportTask(args ReportTaskArgs, replay *struct{}) error </code></pre><p>方法<code>AskForTask</code>就是一个<code>worker</code>向<code>master</code>申请任务的过程,对应功能点1、2和6。</p><p>功能点1和2好理解。这主要是根据<code>mapfTasks</code>和<code>reducefTasks</code>这两个任务状态表分发,这两个表key是待处理map任务的id,value是任务状态(未开始、进行中、已完成)。</p><pre><code class="go">for taskId, status := range m.mapfTasks { if status == 0 { // 分发任务 }}</code></pre><p>为什么会有6是因为考虑到<code>worker</code>持续循环调用<code>AskForTask</code>,所以把判断任务全部完成的状态也加在这个方法里,<code>AskForTaskReply</code>会告知<code>worker</code>任务全部完成。</p><pre><code class="go">if successCnt == len(m.reducefTasks) { m.done = true}</code></pre><p>功能点3、4在<code>worker.go</code>下实现。<code>map</code>操作的核心思想是读取待处理的文件,调用<code>mapf</code>,写入<code>intermedia file</code>,通过rpc方法<code>ReportTask</code>告知<code>master</code>任务已完成。。<code>reduce</code>操作的核心思想是根据master指定的<code>intermedia filename</code>去读取中间文件,然后调用<code>reducef</code>,然后通知任务完成。<br>我这样实现的话,有个关键的点是,map在通知任务完成时,要把中间文件的filename也告诉master,因为这个是reduce任务的来源。</p><p>任务5我的做法比较简单,在每次分发任务时都新建一个线程延时等待,<code>go m.waitForMapfSuccess(taskId)</code>。如果等待时间结束,任务还未完成,就把任务从进行中改为未开始。这个做法还是比较粗糙的,不过在这个lab里能够处理。</p><pre><code class="go">func (m *Master) waitForMapfSuccess(taskId int) { time.Sleep(10 * time.Second) m.mu.Lock() defer m.mu.Unlock() if m.mapfTasks[taskId] == 1 { //log.Printf("mapf taskid %v 超时", taskId) m.mapfTasks[taskId] = 0 }}</code></pre><h3 id="问题点"><a href="#问题点" class="headerlink" title="问题点"></a>问题点</h3><h4 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h4><p>我在wsl2环境下实现,而golang版本不是<code>Go1.13</code>,而是<code>Go1.18.2</code>。这里还是不建议更改版本,不过因为module相关的一些问题,哪怕我用了go1.13也会触发同样的问题,所以最后我改了下测试文件的编译命令,直接用1.18去跑(逃</p><pre><code class="shell">...(cd .. && GO111MODULE=off go build $RACE mrmaster.go) || exit 1(cd .. && GO111MODULE=off go build $RACE mrworker.go) || exit 1(cd .. && GO111MODULE=off go build $RACE mrsequential.go) || exit 1...</code></pre><p>这个改动应该是不影响在要求的环境下的测试结果的</p><h4 id="race"><a href="#race" class="headerlink" title="race"></a>race</h4><p>加-race被提醒有地方会有问题</p><pre><code class="text">WARNING: DATA RACEWrite at 0x00c000100220 by goroutine 78: _/home/...../MIT6.824/src/mr.(*Master).AskForTask() /home/...../MIT6.824/src/mr/master.go:75 +0x885Previous read at 0x00c000100220 by main goroutine: _/home/.....g/MIT6.824/src/mr.(*Master).Done() /home/...../MIT6.824/src/mr/master.go:134 +0xef</code></pre><h4 id="parallelism测试卡死-只能手动Kill"><a href="#parallelism测试卡死-只能手动Kill" class="headerlink" title="parallelism测试卡死 只能手动Kill"></a>parallelism测试卡死 只能手动Kill</h4><p>这个问题一开始很困惑,后来发现单跑这个测试是能通过的。然后我发现这个测试会读当前文件夹。而我一开始没有手动删除intermedia file,这个test也不会删之前的intermedia file,我猜测问题点出在这里。事实证明在加入了任务完成后删除intermedia file后,就能通过了。</p><h4 id="其他小bug"><a href="#其他小bug" class="headerlink" title="其他小bug"></a>其他小bug</h4><p>比如重复问题,debug发现是没有等map全部结束就发出去了reduce任务。</p><pre><code class="text">ADLER 1ADVENTURE 12ADVENTURES 7AFTER 2AGREE 16AGREEMENT 8...ADLER 1ADVENTURE 12ADVENTURES 7AFTER 2AFTER 2AGREE 16AGREEMENT 8</code></pre><p>还有crash test超时问题,打了日志后再稍微看下代码就发现是小bug,不多赘述。</p>]]></content>
<summary type="html"><p>课程要求是不公开自己的代码的,遵守一下规则。这里简单讲讲思路和遇到的问题</p></summary>
<category term="Lab" scheme="https://juniousy.github.io/categories/Lab/"/>
<category term="distributed system" scheme="https://juniousy.github.io/tags/distributed-system/"/>
</entry>
<entry>
<title>【本科时期文章】Java 同步器核心AQS</title>
<link href="https://juniousy.github.io/2019/06/14/%E5%AD%98%E6%A1%A3/Java-%E5%90%8C%E6%AD%A5%E5%99%A8%E6%A0%B8%E5%BF%83AQS/"/>
<id>https://juniousy.github.io/2019/06/14/%E5%AD%98%E6%A1%A3/Java-%E5%90%8C%E6%AD%A5%E5%99%A8%E6%A0%B8%E5%BF%83AQS/</id>
<published>2019-06-14T03:27:12.000Z</published>
<updated>2022-05-30T09:06:50.980Z</updated>
<content type="html"><![CDATA[<p>juc(java.util.concurrent) 基于 AQS ( AbstractQueuedSynchronizer )框架构建锁机制。本文将介绍AQS是如何实现共享状态同步功能,并在此基础上如何实现同步锁机制。</p><span id="more"></span><h2 id="AbstractQueuedSynchronizer"><a href="#AbstractQueuedSynchronizer" class="headerlink" title="AbstractQueuedSynchronizer"></a>AbstractQueuedSynchronizer</h2><h3 id="CLH同步队列"><a href="#CLH同步队列" class="headerlink" title="CLH同步队列"></a>CLH同步队列</h3><p>AQS如其名所示,使用了队列。当共享资源(即多个线程竞争的资源)被某个线程占有时,其他请求该资源的线程将会阻塞,进入CLH同步队列。</p><p>队列的节点为AQS内部类Node。Node持有前驱和后继,因此队列为双向队列。有如下状态:</p><ul><li>SIGNAL 后继节点阻塞(park)或即将阻塞。当前节点完成任务后要唤醒(unpark)后继节点。</li><li>CANCELLED 节点从同步队列中取消</li><li>CONDITION 当前节点进入等待队列中</li><li>PROPAGATE 表示下一次共享式同步状态获取将会无条件传播下去</li><li>0 其他</li></ul><p>AQS通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时唤醒对同步队列中的线程。未获取到锁的线程会创建节点线程安全(compareAndSetTail)的加入队列尾部。同步队列遵循FIFO,首节点是获取同步状态成功的节点。</p><h3 id="获取锁"><a href="#获取锁" class="headerlink" title="获取锁"></a>获取锁</h3><p>未获取到锁(tryAcquire失败)的线程将创建一个节点,设置到尾节点。</p><pre><code class="java">public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();}//创建节点至尾节点private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 如果compareAndSetTail失败或者队列里没有节点 enq(node); return node;}</code></pre><p>enq是一个CAS的入队方法:</p><pre><code class="java">private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } }</code></pre><p>acquireQueued方法的作用是获取锁。</p><pre><code class="java">final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); // 获取锁成功 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } // 获取失败则阻塞 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }}</code></pre><h3 id="释放锁"><a href="#释放锁" class="headerlink" title="释放锁"></a>释放锁</h3><p>首节点的线程在释放锁时,将会唤醒后继节点。而后继节点将会在获取锁成功时将自己设置为首节点。</p><pre><code class="java">public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) // 唤醒后继节点 unparkSuccessor(h); return true; } return false;}</code></pre><h3 id="响应中断式获取锁"><a href="#响应中断式获取锁" class="headerlink" title="响应中断式获取锁"></a>响应中断式获取锁</h3><p>可响应中断式锁可调用方法lock.lockInterruptibly();而该方法其底层会调用AQS的acquireInterruptibly方法。</p><pre><code class="java">public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) doAcquireInterruptibly(arg);}private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 唯一的区别是当parkAndCheckInterrupt返回true时即线程阻塞时该线程被中断,代码抛出被中断异常。 throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); }}</code></pre><h3 id="超时等待获取锁"><a href="#超时等待获取锁" class="headerlink" title="超时等待获取锁"></a>超时等待获取锁</h3><p>通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,调用AQS的方法tryAcquireNanos()。</p><pre><code class="java">public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);}tongbuqiprivate boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L) return false; final long deadline = System.nanoTime() + nanosTimeout; final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return true; } // 计算等待时间 nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) return false; if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); if (Thread.interrupted()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }</code></pre><h3 id="共享锁的获取"><a href="#共享锁的获取" class="headerlink" title="共享锁的获取"></a>共享锁的获取</h3><p>最后看下共享锁的获取。</p><pre><code class="java">public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) //获取锁失败时调用 doAcquireShared(arg);}private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); // 当tryAcquireShared返回值>=0时取得锁 if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }}</code></pre><h3 id="队列外成员变量"><a href="#队列外成员变量" class="headerlink" title="队列外成员变量"></a>队列外成员变量</h3><p>AQ还有<code>state</code>成员变量,volatile int类型,用于同步线程之间的共享状态。当state>0时表示已经获取了锁,对于重入锁来说state值即重入数,当state = 0时表示释放了锁。具体说明见下面各同步器的实现。</p><h2 id="实现同步器"><a href="#实现同步器" class="headerlink" title="实现同步器"></a>实现同步器</h2><p>每一种同步器都通过实现<code>tryacquire</code>(包括如<code>tryAcquireShared</code>之类的方法)、<code>tryRelease</code>来实现同步功能。</p><h3 id="ReentrantLock"><a href="#ReentrantLock" class="headerlink" title="ReentrantLock"></a>ReentrantLock</h3><p>主要看获取锁的过程<br>非公平锁获取锁:</p><pre><code class="java">final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //如果当前重进入数为0,说明有机会取得锁 if (c == 0) { //抢占式获取锁 compareAndSetState是原子方法 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //如果当前线程本身就持有锁,那么叠加重进入数,并且继续获得锁 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } //以上条件都不满足,那么线程进入等待队列。 return false;}</code></pre><p>公平锁获取锁类似:</p><pre><code class="java">protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 区别之处,非抢占式 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false;}</code></pre><h3 id="Semaphore"><a href="#Semaphore" class="headerlink" title="Semaphore"></a>Semaphore</h3><p>以<code>state</code>作为信号量使用,例子:</p><pre><code class="java">final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; //剩下多少许可资源 if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; }}</code></pre><h3 id="CountDownLatch"><a href="#CountDownLatch" class="headerlink" title="CountDownLatch"></a>CountDownLatch</h3><p>以<code>state</code>作为计数器,<code>state</code>为0时等待结束:</p><pre><code class="java">public void await() throws InterruptedException { //阻塞直到state为0 sync.acquireSharedInterruptibly(1);}</code></pre><p>用同步器方法减少state</p><pre><code class="java">public void countDown() { sync.releaseShared(1);}</code></pre><pre><code class="java">protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; }}</code></pre>]]></content>
<summary type="html"><p>juc(java.util.concurrent) 基于 AQS ( AbstractQueuedSynchronizer )框架构建锁机制。本文将介绍AQS是如何实现共享状态同步功能,并在此基础上如何实现同步锁机制。</p></summary>
<category term="开发" scheme="https://juniousy.github.io/categories/%E5%BC%80%E5%8F%91/"/>
<category term="Java" scheme="https://juniousy.github.io/tags/Java/"/>
<category term="并发" scheme="https://juniousy.github.io/tags/%E5%B9%B6%E5%8F%91/"/>
</entry>
<entry>
<title>【本科时期文章】Redis的数据结构与编码</title>
<link href="https://juniousy.github.io/2019/06/05/%E5%AD%98%E6%A1%A3/Redis%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%BC%96%E7%A0%81/"/>
<id>https://juniousy.github.io/2019/06/05/%E5%AD%98%E6%A1%A3/Redis%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%BC%96%E7%A0%81/</id>
<published>2019-06-05T07:42:40.000Z</published>
<updated>2022-05-30T09:06:50.982Z</updated>
<content type="html"><![CDATA[<table><thead><tr><th align="center">类型</th><th align="center">编码方式</th><th align="center">数据结构</th></tr></thead><tbody><tr><td align="center">string</td><td align="center">raw</td><td align="center">动态字符串编码</td></tr><tr><td align="center"></td><td align="center">embstr</td><td align="center">优化内存分配的字符串编码</td></tr><tr><td align="center"></td><td align="center">int</td><td align="center">整数编码</td></tr><tr><td align="center">hash</td><td align="center">hashtable</td><td align="center">散列表编码</td></tr><tr><td align="center"></td><td align="center">ziplist</td><td align="center">压缩列表编码</td></tr><tr><td align="center">list</td><td align="center">linkedlist</td><td align="center">双向链表编码</td></tr><tr><td align="center"></td><td align="center">ziplist</td><td align="center">压缩列表编码</td></tr><tr><td align="center"></td><td align="center">quicklist</td><td align="center">3.2版本新的列表编码</td></tr><tr><td align="center">set</td><td align="center">hashtable</td><td align="center">散列表编码</td></tr><tr><td align="center"></td><td align="center">intset</td><td align="center">整数集合编码</td></tr><tr><td align="center">zset</td><td align="center">skiplist</td><td align="center">跳跃表编码</td></tr><tr><td align="center"></td><td align="center">ziplist</td><td align="center">压缩列表编码</td></tr></tbody></table><span id="more"></span><h3 id="字符串结构"><a href="#字符串结构" class="headerlink" title="字符串结构"></a>字符串结构</h3><p>Redis没有采用原生C语言的字符串类型,而是自己实现了字符串结构,内部简单动态字符串(simple dynamic string,SDS)。特点如下:</p><ul><li>O(1)时间复杂度获取字符串长度、已用长度、未用长度</li><li>可用于保存字节数组,支持安全的二进制数据存储</li><li>内部实现空间预分配机制,降低内存内存再分配次数</li><li>惰性删除机制,字符串缩减后的空间不释放,作为预分配空间保留</li></ul><p>对于string,</p><ul><li>int:8个字节的长整型</li><li>embstr:小于等于39个字节的字符串</li><li>raw:大于39个字节的字符串,即用简单动态字符串(SDS)存储</li></ul><p>embstr 编码的优化之处在于将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次,mbstr 编码的字符串对象的所有数据都保存在一块连续的内存里面,redisObject 结构(type, encoding…)和 sdshdr 结构(free, len, buf)都放在一起<br>embstr 编码的字符串对象实际上是只读的: 当我们对 embstr 编码的字符串对象执行任何修改命令时, 程序会先将对象的编码从 embstr 转换成 raw , 然后再执行修改命令; 因为这个原因, embstr 编码的字符串对象在执行修改命令之后, 总会变成一个 raw 编码的字符串对象。</p><h3 id="ziplist-压缩列表"><a href="#ziplist-压缩列表" class="headerlink" title="ziplist 压缩列表"></a>ziplist 压缩列表</h3><p>hash、list、zset中,如果所有值小于hash_max_ziplist_value (默认值为 64 ),且元素个数小于 hash_max_ziplist_entries (默认值为 512 )时使用ziplist编码。</p><p>ziplist编码的主要目的是为了节约内存,因此所有数据都是采用线性连续的内存结构。结构字段含义:</p><ol><li>zlbytes:整个压缩列表所占字节长度。int-32,长度4字节。</li><li>zltail:距离尾节点的偏移量。int-32,长度4字节。</li><li>zllen:int-16,长度2字节。</li><li>entry:具体的节点:<ol><li>prev_entry_bytes_length:记录前一个节点所占空间</li><li>encoding:标示当前节点编码和长度</li><li>contents:保存节点的值</li></ol></li><li>zlend:记录列表结尾,占一个字节</li></ol><p>从上可以看出存在双向链表结构,以O(1)时间复杂度入队和出队。而新增删除操作涉及内存重新分配和释放。</p><h3 id="hashtable"><a href="#hashtable" class="headerlink" title="hashtable"></a>hashtable</h3><p>Redis 使用的hash算法是 MurmurHash2 ,解决冲突的方式是链地址法。程序总是将新节点添加到链表的表头位置(复杂度为 O(1)), 排在其他已有节点的前面。按2的幂rehash。</p><h3 id="linkedlist"><a href="#linkedlist" class="headerlink" title="linkedlist"></a>linkedlist</h3><p><a href="http://redisbook.com/preview/adlist/implementation.html">Redis 的链表实现的特性可以总结如下</a>:</p><ul><li>双端: langfei链表节点带有 prev 和 next 指针, 获取某个节点的前置节点和后置节点的复杂度都是 O(1) 。</li><li>无环: 表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL , 对链表的访问以 NULL 为终点。</li><li>带表头指针和表尾指针: 通过 list 结构的 head 指针和 tail 指针, 程序获取链表的表头节点和表尾节点的复杂度为 O(1) 。</li><li>带链表长度计数器: 程序使用 list 结构的 len 属性来对 list 持有的链表节点进行计数, 程序获取链表中节点数量的复杂度为 O(1) 。</li><li>多态: 链表节点使用 void* 指针来保存节点值, 并且可以通过 list 结构的 dup 、 free 、 match 三个属性为节点值设置类型特定函数, 所以链表可以用于保存各种不同类型的值。</li></ul><h3 id="intset"><a href="#intset" class="headerlink" title="intset"></a>intset</h3><p>存储有序、不重复的整数集。集合只包含整数且长度不超过set-max-intset-entries</p><p>intset对写入整数进行排序,通过O(lgn)时间复杂度实现查找和去重操作。字段含义:</p><ul><li>encoding:整数表示类型,根据集合内最长整数值确定类型,整数类型划分为int-16,int-32,int-64</li><li>length:表示集合元素个数</li><li>contents:整数数组,按从小到达顺序排列</li></ul><p>尽量保证整数范围一致,防止个别大整数触发集合升级操作,产生内存浪费。</p><h3 id="skiplist"><a href="#skiplist" class="headerlink" title="skiplist"></a>skiplist</h3><p>过在每个节点中维持多个指向其他节点的指针, 从而达到快速访问节点的目的。跳跃表支持平均 O(log N) 最坏 O(N) 复杂度的节点查找, 还可以通过顺序性操作来批量处理节点。</p><h3 id="Object"><a href="#Object" class="headerlink" title="Object"></a>Object</h3><p>Redis 中的每个对象都由一个 redisObject 结构表示, 该结构中和保存数据有关的三个属性分别是 type 属性、 encoding 属性和 ptr 属性。对象的 type 属性记录了对象的类型。对象的 ptr 指针指向对象的底层实现数据结构, 而这些数据结构由对象的 encoding 属性决定。</p><p>因为 C 语言并不具备自动的内存回收功能, 所以 Redis 在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制, 通过这一机制, 程序可以通过跟踪对象的引用计数信息, 在适当的时候自动释放对象并进行内存回收。由redisObject 结构的 refcount 属性记录:</p><ul><li>在创建一个新对象时, 引用计数的值会被初始化为 1 ;</li><li>当对象被一个新程序使用时, 它的引用计数值会被增一;</li><li>当对象不再被一个程序使用时, 它的引用计数值会被减一;</li><li>当对象的引用计数值变为 0 时, 对象所占用的内存会被释放。</li></ul><p>redisObject 结构包含的最后一个属性为 lru 属性, 该属性记录了对象最后一次被命令程序访问的时间。OBJECT IDLETIME 命令可以打印出给定键的空转时长, 这一空转时长就是通过将当前时间减去键的值对象的 lru 时间计算得出的。</p>]]></content>
<summary type="html"><table>
<thead>
<tr>
<th align="center">类型</th>
<th align="center">编码方式</th>
<th align="center">数据结构</th>
</tr>
</thead>
<tbody><tr>
<td align="center">string</td>
<td align="center">raw</td>
<td align="center">动态字符串编码</td>
</tr>
<tr>
<td align="center"></td>
<td align="center">embstr</td>
<td align="center">优化内存分配的字符串编码</td>
</tr>
<tr>
<td align="center"></td>
<td align="center">int</td>
<td align="center">整数编码</td>
</tr>
<tr>
<td align="center">hash</td>
<td align="center">hashtable</td>
<td align="center">散列表编码</td>
</tr>
<tr>
<td align="center"></td>
<td align="center">ziplist</td>
<td align="center">压缩列表编码</td>
</tr>
<tr>
<td align="center">list</td>
<td align="center">linkedlist</td>
<td align="center">双向链表编码</td>
</tr>
<tr>
<td align="center"></td>
<td align="center">ziplist</td>
<td align="center">压缩列表编码</td>
</tr>
<tr>
<td align="center"></td>
<td align="center">quicklist</td>
<td align="center">3.2版本新的列表编码</td>
</tr>
<tr>
<td align="center">set</td>
<td align="center">hashtable</td>
<td align="center">散列表编码</td>
</tr>
<tr>
<td align="center"></td>
<td align="center">intset</td>
<td align="center">整数集合编码</td>
</tr>
<tr>
<td align="center">zset</td>
<td align="center">skiplist</td>
<td align="center">跳跃表编码</td>
</tr>
<tr>
<td align="center"></td>
<td align="center">ziplist</td>
<td align="center">压缩列表编码</td>
</tr>
</tbody></table></summary>
<category term="开发" scheme="https://juniousy.github.io/categories/%E5%BC%80%E5%8F%91/"/>
<category term="Redis" scheme="https://juniousy.github.io/tags/Redis/"/>
</entry>
<entry>
<title>【本科时期文章】从Redis I/O多路复用到Java NIO Selector</title>
<link href="https://juniousy.github.io/2019/06/04/%E5%AD%98%E6%A1%A3/%E4%BB%8ERedis-I-O%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8%E5%88%B0Java-NIO-Selector/"/>
<id>https://juniousy.github.io/2019/06/04/%E5%AD%98%E6%A1%A3/%E4%BB%8ERedis-I-O%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8%E5%88%B0Java-NIO-Selector/</id>
<published>2019-06-04T07:19:21.000Z</published>
<updated>2022-05-30T09:06:50.983Z</updated>
<content type="html"><![CDATA[<h3 id="Redis的I-O多路复用架构"><a href="#Redis的I-O多路复用架构" class="headerlink" title="Redis的I/O多路复用架构"></a>Redis的I/O多路复用架构</h3><p>Redis的一大特点就是单线程架构。单线程架构既避免了多线程可能产生的竞争问题,又避免了多线程的频繁上下文切换问题,是Redis高效率的保证。</p><span id="more"></span><p>对于网络I/O操作,Redis基于 Reactor 模式可以用单个线程处理多个Socket。内部实现为使用文件事件处理器(file event handler)进行网络事件处理器,这个文件事件处理器是单线程的。文件事件处理器采用<code> I/O 多路复用机制(multiplexing)</code>同时监听多个 socket。产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。操作包括应答(accept)、读取(read)、写入(write)、关闭(close)等。文件事件处理器的结构包含 4 个部分:</p><ul><li>多个 socket</li><li>I/O 多路复用程序</li><li>文件事件分派器</li><li>事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)<br>连接应答处理器会创建一个能与客户端通信的 socket01,通过这个返回结果给客户端。Redis单线程的核心就是I/O 多路复用程序。</li></ul><p>I/O多路复用(IO Multiplexing)有时也称为异步阻塞IO,是一种事件驱动的I/O模型。单个I/O操作在一般情况下往往不能直接返回,传统的阻塞 I/O 模型会阻塞直到系统内核返回数据。而在 I/O 多路复用模型中,系统调用select/poll/epoll 函数会不断的查询所监测的 socket 文件描述符,查看其中是否有 socket 准备好读写了,如果有,那么系统就会通知用户进程。</p><p>Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select 、 epoll 、 evport 和 kqueue 这些 I/O 多路复用函数库来实现的, 每个 I/O 多路复用函数库在 Redis 源码中都对应一个单独的文件。</p><p>以ae_select.c实现的封装select方法为例。<code>select</code>方法定义如下所示,检测是否可读、可写、异常,返回准备完毕的descriptors个数。</p><pre><code class="c">extern int select (int __nfds, fd_set *__restrict __readfds, fd_set *__restrict __writefds, fd_set *__restrict __exceptfds, struct timeval *__restrict __timeout);</code></pre><p>Redis封装首先通过<code>aeApiCreate</code>初始化 rfds 和 wfds,注册到aeEventLoop中去。</p><pre><code class="c">static int aeApiCreate(aeEventLoop *eventLoop) { aeApiState *state = zmalloc(sizeof(aeApiState)); if (!state) return -1; FD_ZERO(&state->rfds); FD_ZERO(&state->wfds); eventLoop->apidata = state; return 0;}</code></pre><p>而 <code>aeApiAddEvent</code> 和 <code>aeApiDelEvent</code> 会通过 FD_SET 和 FD_CLR 修改 fd_set 中对应 FD 的标志位。</p><pre><code class="c">static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) { aeApiState *state = eventLoop->apidata; if (mask & AE_READABLE) FD_SET(fd,&state->rfds); if (mask & AE_WRITABLE) FD_SET(fd,&state->wfds); return 0;}static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask) { aeApiState *state = eventLoop->apidata; if (mask & AE_READABLE) FD_CLR(fd,&state->rfds); if (mask & AE_WRITABLE) FD_CLR(fd,&state->wfds);}</code></pre><p><code>aeApiPoll</code>是实际调用 select 函数的部分,其作用就是在 I/O 多路复用函数返回时,将对应的 FD 加入 aeEventLoop 的 fired 数组中,并返回事件的个数:</p><pre><code class="c">static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) { aeApiState *state = eventLoop->apidata; int retval, j, numevents = 0; memcpy(&state->_rfds,&state->rfds,sizeof(fd_set)); memcpy(&state->_wfds,&state->wfds,sizeof(fd_set)); retval = select(eventLoop->maxfd+1, &state->_rfds,&state->_wfds,NULL,tvp); if (retval > 0) { for (j = 0; j <= eventLoop->maxfd; j++) { int mask = 0; aeFileEvent *fe = &eventLoop->events[j]; if (fe->mask == AE_NONE) continue; if (fe->mask & AE_READABLE && FD_ISSET(j,&state->_rfds)) mask |= AE_READABLE; if (fe->mask & AE_WRITABLE && FD_ISSET(j,&state->_wfds)) mask |= AE_WRITABLE; eventLoop->fired[numevents].fd = j; eventLoop->fired[numevents].mask = mask; numevents++; } } return numevents;}</code></pre><p>epoll函数的封装类似。区别在于 epoll_wait 函数返回时并不需要遍历所有的 FD 查看读写情况;在 epoll_wait 函数返回时会提供一个 epoll_event 数组,其中保存了发生的 epoll 事件(EPOLLIN、EPOLLOUT、EPOLLERR 和 EPOLLHUP)以及发生该事件的 FD。Redis封装的调用只需要将<code>epoll_event</code>数组中存储的信息加入eventLoop的 fired 数组中,将信息传递给上层模块:</p><pre><code class="c">static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) { aeApiState *state = eventLoop->apidata; int retval, numevents = 0; retval = epoll_wait(state->epfd,state->events,eventLoop->setsize, tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1); if (retval > 0) { int j; numevents = retval; for (j = 0; j < numevents; j++) { int mask = 0; struct epoll_event *e = state->events+j; if (e->events & EPOLLIN) mask |= AE_READABLE; if (e->events & EPOLLOUT) mask |= AE_WRITABLE; if (e->events & EPOLLERR) mask |= AE_WRITABLE; if (e->events & EPOLLHUP) mask |= AE_WRITABLE; eventLoop->fired[j].fd = e->data.fd; eventLoop->fired[j].mask = mask; } } return numevents;}</code></pre><p>当Socket变得可读时(客户端对Socket执行 write 操作,或者执行 close 操作), 或者有新的可应答(acceptable)Socket出现时(客户端对服务器的监听Socket执行 connect 操作),Socket产生 AE_READABLE 事件。而当Socket变得可写时(客户端对Socket执行 read 操作), Socket产生 AE_WRITABLE 事件。<br>I/O 多路复用程序允许服务器同时监听Socket的 AE_READABLE 事件和 AE_WRITABLE 事件, 如果一个Socket同时产生了这两种事件, 那么文件事件分派器会优先处理 AE_READABLE 事件, 等到 AE_READABLE 事件处理完之后, 才处理 AE_WRITABLE 事件。换句话说, 如果一个Socket又可读又可写的话, 那么服务器将先读Socket, 后写Socket。</p><h3 id="Java-NIO-Selector"><a href="#Java-NIO-Selector" class="headerlink" title="Java NIO Selector"></a>Java NIO Selector</h3><p>Java中也有I/O多路复用的方式,例子为NIO的<code>Selector</code>。<br><code>selector</code>的创建方式为调用<code>Selector</code>类的静态方法,由<code>SelectorProvider</code>提供:<code>Selector selector = Selector.open();</code></p><pre><code class="java">public static Selector open() throws IOException { return SelectorProvider.provider().openSelector();}</code></pre><p><code>SelectorProvider</code>是单例模式,Linux默认提供<code>EPollSelectorProvider</code>,即提供的Selector为<code>EPollSelectorImpl</code>。</p><pre><code class="java">public static SelectorProvider provider() { synchronized (lock) { if (provider != null) return provider; return AccessController.doPrivileged( new PrivilegedAction<SelectorProvider>() { public SelectorProvider run() { if (loadProviderFromProperty()) return provider; if (loadProviderAsService()) return provider; provider = sun.nio.ch.DefaultSelectorProvider.create(); return provider; } }); }}//...../** * Returns the default SelectorProvider. */public static SelectorProvider create() { String osname = AccessController .doPrivileged(new GetPropertyAction("os.name")); if (osname.equals("SunOS")) return createProvider("sun.nio.ch.DevPollSelectorProvider"); if (osname.equals("Linux")) return createProvider("sun.nio.ch.EPollSelectorProvider"); return new sun.nio.ch.PollSelectorProvider();}</code></pre><p>调用系统Epoll方法的地方在<code>EPollArrayWrapper</code>类的<code>poll</code>方法中,该类由<code>EPollSelectorImpl</code>持有:</p><pre><code class="java">int poll(long timeout) throws IOException { updateRegistrations(); updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd); for (int i=0; i<updated; i++) { if (getDescriptor(i) == incomingInterruptFD) { interruptedIndex = i; interrupted = true; break; } } return updated;}</code></pre><p><code>Selector</code>使用中需要绑定<code>Channel</code>。以<code>ServerSocketChannel</code>为例:</p><pre><code class="java">ServerSocketChannel serverSocket = ServerSocketChannel.open();serverSocket.bind(new InetSocketAddress("localhost", 5454));serverSocket.configureBlocking(false);serverSocket.register(selector, SelectionKey.OP_ACCEPT);</code></pre><p>注册时会调用<code>Selector</code>的回调方法<code>register</code>,生成<code>SelectionKey</code>。</p><pre><code class="java">protected final SelectionKey register(AbstractSelectableChannel ch, int ops, Object attachment){ if (!(ch instanceof SelChImpl)) throw new IllegalSelectorException(); SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this); k.attach(attachment); synchronized (publicKeys) { implRegister(k); } k.interestOps(ops); return k;}</code></pre><p>最后在使用时根据<code>SelectionKeys</code>遍历查看状态。可以通过监听的事件有:</p><ul><li>Connect – OP_CONNECT client尝试连接</li><li>Accept – OP_ACCEPT server端接受连接</li><li>Read – OP_READ server端可以开始从channel里读取</li><li>Write – OP_WRITE server端可以向channel里写</li></ul><p>使用方式类似:</p><pre><code class="java">while (true) { selector.select(); Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectedKeys.iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); if (key.isAcceptable()) { register(selector, serverSocket); } if (key.isReadable()) { answerWithEcho(buffer, key); } iter.remove(); }}</code></pre><p><code>Selector</code>的wakeup()方法主要作用是解除阻塞在Selector.select()/select(long)上的线程,立即返回,调用了本地的中断方法。可以在注册了新的channel或者事件、channel关闭,取消注册时使用,或者优先级更高的事件触发(如定时器事件),希望及时处理。</p><p>通过NIO的I/O多路复用方式可以节约线程资源,提高网络I/O效率。</p><h4 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h4><ul><li><a href="http://redisbook.com/preview/event/file_event.html">Redis 设计与实现-文件事件</a></li><li><a href="https://draveness.me/redis-io-multiplexing">Redis 和 I/O 多路复用</a></li><li><a href="https://www.baeldung.com/java-nio-selector">Introduction to the Java NIO Selector</a></li></ul>]]></content>
<summary type="html"><h3 id="Redis的I-O多路复用架构"><a href="#Redis的I-O多路复用架构" class="headerlink" title="Redis的I/O多路复用架构"></a>Redis的I/O多路复用架构</h3><p>Redis的一大特点就是单线程架构。单线程架构既避免了多线程可能产生的竞争问题,又避免了多线程的频繁上下文切换问题,是Redis高效率的保证。</p></summary>
<category term="开发" scheme="https://juniousy.github.io/categories/%E5%BC%80%E5%8F%91/"/>
<category term="Java" scheme="https://juniousy.github.io/tags/Java/"/>
<category term="Redis" scheme="https://juniousy.github.io/tags/Redis/"/>
</entry>
<entry>
<title>【本科时期文章】LeetCode 23. Merge k Sorted Lists</title>
<link href="https://juniousy.github.io/2019/05/17/%E5%AD%98%E6%A1%A3/LeetCode-23-Merge-k-Sorted-Lists/"/>
<id>https://juniousy.github.io/2019/05/17/%E5%AD%98%E6%A1%A3/LeetCode-23-Merge-k-Sorted-Lists/</id>
<published>2019-05-17T08:53:24.000Z</published>
<updated>2022-05-30T12:37:14.956Z</updated>
<content type="html"><![CDATA[<h2 id="Problem"><a href="#Problem" class="headerlink" title="Problem"></a>Problem</h2><p>Merge <em>k</em> sorted linked lists and return it as one sorted list. Analyze and describe its complexity.</p><p><strong>Example:</strong></p><pre><code>Input:[ 1->4->5, 1->3->4, 2->6]Output: 1->1->2->3->4->4->5->6</code></pre><span id="more"></span><h2 id="Solution"><a href="#Solution" class="headerlink" title="Solution"></a>Solution</h2><p>使用priority queue</p><pre><code class="java">/* * @lc app=leetcode id=23 lang=java * * [23] Merge k Sorted Lists *//** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public ListNode mergeKLists(ListNode[] lists) { if (lists == null || lists.length == 0) return null; PriorityQueue<ListNode> pQueue = new PriorityQueue<>((a, b) -> a.val - b.val); ListNode dummy = new ListNode(0); ListNode cur = dummy; for (ListNode node:lists) { if (node == null) continue; pQueue.offer(node); } while(!pQueue.isEmpty()) { cur.next = pQueue.poll(); cur = cur.next; if (cur.next != null) pQueue.offer(cur.next); } return dummy.next; }}</code></pre><p>Time complexity: O(nlogk)</p><p>Space complexity: O(k)</p>]]></content>
<summary type="html"><h2 id="Problem"><a href="#Problem" class="headerlink" title="Problem"></a>Problem</h2><p>Merge <em>k</em> sorted linked lists and return it as one sorted list. Analyze and describe its complexity.</p>
<p><strong>Example:</strong></p>
<pre><code>Input:
[
1-&gt;4-&gt;5,
1-&gt;3-&gt;4,
2-&gt;6
]
Output: 1-&gt;1-&gt;2-&gt;3-&gt;4-&gt;4-&gt;5-&gt;6
</code></pre></summary>
<category term="刷题" scheme="https://juniousy.github.io/categories/%E5%88%B7%E9%A2%98/"/>
<category term="算法" scheme="https://juniousy.github.io/tags/%E7%AE%97%E6%B3%95/"/>
</entry>
<entry>
<title>【本科时期文章】ThreadLocal</title>
<link href="https://juniousy.github.io/2019/03/24/%E5%AD%98%E6%A1%A3/ThreadLocal/"/>
<id>https://juniousy.github.io/2019/03/24/%E5%AD%98%E6%A1%A3/ThreadLocal/</id>
<published>2019-03-24T14:12:30.000Z</published>
<updated>2022-05-30T09:06:50.983Z</updated>
<content type="html"><![CDATA[<p>ThreadLocal的作用并不是解决多线程共享变量的问题,而是存储那些线程间隔离,但在不同方法间共享的变量。这是线程安全的一种无同步方案,另一种是无同步方案是幂等的可重入代码。</p><p>下面先模拟一个基本的ThreadLocal存储User id的模型,然后解析原理。</p><span id="more"></span><hr><h4 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h4><pre><code class="java">import java.util.concurrent.atomic.AtomicInteger;public class ThreadLocalTest { //工作线程 class Worker implements Runnable { ThreadLocal<Integer> userId = ThreadLocal.withInitial(() -> 1); UserRepo userRepo; Worker(UserRepo userRepo) { this.userRepo = userRepo; } @Override public void run() { for (int i = 0; i < 10; i++) { handler(); try { Thread.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } } } private void handler() { userId.set(userRepo.getUserId()); System.out.println(Thread.currentThread().getName() + " userId: " + userId.get()); } } //模拟拿自增user id class UserRepo { private AtomicInteger incrUserId = new AtomicInteger(1); private Integer getUserId() { return incrUserId.getAndIncrement(); } } private void test() { UserRepo userRepo = new UserRepo(); for (int i = 0; i < 15; i++) { new Thread(new Worker(userRepo)).start(); } } public static void main(String[] args) { ThreadLocalTest test = new ThreadLocalTest(); test.test(); } }</code></pre><p>结果如下</p><pre><code>........(上略)Thread-13 userId: 135Thread-0 userId: 136Thread-2 userId: 137Thread-1 userId: 138Thread-4 userId: 139Thread-5 userId: 140Thread-3 userId: 141Thread-6 userId: 142Thread-7 userId: 143Thread-9 userId: 144Thread-10 userId: 145Thread-11 userId: 146Thread-8 userId: 147Thread-12 userId: 149Thread-14 userId: 148Thread-13 userId: 150</code></pre><hr><h4 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h4><p>核心是ThreadLocal的内部静态类ThreadLocalMap。map的key是ThreadLocal对象,value是和ThreadLocal对象有关联的值。</p><pre><code class="java">static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }}</code></pre><p>注意内部Entry是WeakReference的,原因是出于性能考虑。由于不是强联系,所以其他正在使用ThreadLocal的线程,不会妨碍gc那些来自同一个ThreadLocal的终止后的线程的变量,简单来讲就是待gc的变量会被正确gc。</p><p>在ThreadLocalMap 的 remove 方法中,除了讲entry的引用设为null以外,还调用了一个expungeStaleEntry方法:</p><pre><code class="java">if (e.get() == key) { e.clear(); expungeStaleEntry(i); return;}</code></pre><p>其中会将所有键为 null 的 Entry 的值设置为 null,这样可以防止内存泄露,已经不再被使用且已被回收的 ThreadLocal 对象对应的Entry也会被gc清除:</p><pre><code class="java">if (k == null) { e.value = null; tab[i] = null; size--;}</code></pre><p>在同样的还有rehash, resize方法方法中,也有类似的设置value为null的操作。</p><p>在创建线程时,该线程持有threadLocals。这个引用是在ThreadLocal的createMap方法中设定的,否则为null。</p><pre><code class="java">void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);}</code></pre><p>调用ThreadLocalMap的构造方法:</p><pre><code class="java">ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY);}</code></pre><p>再返回来看ThreadLocal就很好理解了<br>get方法:</p><pre><code class="java">public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //获取当前线程的ThreadLocalMap if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); //从map中取值,key就是当前ThreadLocal对象 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue();}</code></pre><p>set方法:</p><pre><code class="java">public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //获取当前线程的ThreadLocalMap if (map != null) map.set(this, value); //向map中存值,key就是当前ThreadLocal对象 else createMap(t, value); }</code></pre><hr><h4 id="应用"><a href="#应用" class="headerlink" title="应用"></a>应用</h4><p>很常见的应用在Session中存储数据。一个Session对应一个线程,对应一组线程内方法间的共享变量,这些变量都可以由ThreadLocal存储。</p><p>参考下<a href="https://www.cnblogs.com/youzhibing/p/6690341.html">结合ThreadLocal来看spring事务源码,感受下清泉般的洗涤!</a>,可以看到在Spring事务中,也有类似ThreadLocal的操作,将数据库connection绑定到当前线程,使用的也是一个map。</p>]]></content>
<summary type="html"><p>ThreadLocal的作用并不是解决多线程共享变量的问题,而是存储那些线程间隔离,但在不同方法间共享的变量。这是线程安全的一种无同步方案,另一种是无同步方案是幂等的可重入代码。</p>
<p>下面先模拟一个基本的ThreadLocal存储User id的模型,然后解析原理。</p></summary>
<category term="开发" scheme="https://juniousy.github.io/categories/%E5%BC%80%E5%8F%91/"/>
<category term="Java" scheme="https://juniousy.github.io/tags/Java/"/>
<category term="并发" scheme="https://juniousy.github.io/tags/%E5%B9%B6%E5%8F%91/"/>
</entry>
<entry>
<title>【本科时期文章】Java Collection笔记</title>
<link href="https://juniousy.github.io/2019/03/20/%E5%AD%98%E6%A1%A3/Java-Collection%E7%AC%94%E8%AE%B0/"/>
<id>https://juniousy.github.io/2019/03/20/%E5%AD%98%E6%A1%A3/Java-Collection%E7%AC%94%E8%AE%B0/</id>
<published>2019-03-20T12:19:10.000Z</published>
<updated>2022-05-30T09:06:50.980Z</updated>
<content type="html"><![CDATA[<p>这是自己整理的一些Collection的要点笔记,比较零碎,可能可读性不是很强。有新内容时会进行补充。<br>Java Collection框架:</p><ul><li>Set , HashSet TreeSet(实现SortedSet)<ul><li>SortedSet</li></ul></li><li>List , LinkedList ArrayList</li><li>Queue, PriorityQueue</li><li>Dequeue</li><li>Map , HashMap TreeMap(实现SortedMap)<ul><li>SortedMap</li></ul></li></ul><span id="more"></span><p>基本方法 add(), remove(), contains(), isEmpty(), addAll()</p><hr><h5 id="hashmap"><a href="#hashmap" class="headerlink" title="hashmap"></a>hashmap</h5><p>线程不安全,允许存null。<br>实现:</p><ol><li><p>内部有一个静态类<code>Node<K,V></code> , 实现<code> Map.Entry<K,V></code>,是“ Basic hash bin node”(文档原文)。而<code>TreeNode</code>也是节点的实现,适用于有冲突的情况,冲突后形成的是红黑树。</p></li><li><p>计算hash值方法:高16位和低16位hashcode异或,降低hash值范围小时的冲突:</p><pre><code class="java">static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}</code></pre></li><li><p>用数组存Node,数组长度必须是2的幂</p><pre><code class="java">transient Node<K,V>[] table;</code></pre></li><li><p>缓存entrySet</p><pre><code class="java">transient Set<Map.Entry<K,V>> entrySet;</code></pre></li><li><p>取:按hash值作为数组下标去取Node。下标是<code>(tab.length - 1) & hash</code>。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。比如hash是31(11111),length是4(100),-1后是11,与运算后是3(11),就是取模。<br>如果有冲突了,则有多个Node放在一个桶里,要么顺序查找(链表),要么按TreeNode去取(红黑树)。</p><pre><code class="java">public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value;}</code></pre></li></ol><p>final Node<K,V> getNode(int hash, Object key) {<br> Node<K,V>[] tab; Node<K,V> first, e; int n; K k;<br> if ((tab = table) != null && (n = tab.length) > 0 &&<br> (first = tab[(n - 1) & hash]) != null) {<br> if (first.hash == hash && // always check first node<br> ((k = first.key) == key || (key != null && key.equals(k))))<br> return first;<br> if ((e = first.next) != null) {<br> if (first instanceof TreeNode)<br> return ((TreeNode<K,V>)first).getTreeNode(hash, key);<br> do {<br> if (e.hash == hash &&<br> ((k = e.key) == key || (key != null && key.equals(k))))<br> return e;<br> } while ((e = e.next) != null);<br> }<br> }<br> return null;<br>}</p><pre><code>6. 存,往数组的`(tab.length - 1) & hash`处放。桶里没有的话则直接放,有的话,找有没有相同的值,有的话替换。加了后如果容量达到threshold就resize();```javapublic V put(K key, V value) { return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null;}</code></pre><ol start="7"><li><strong><code>resize()</code> 方法</strong>,初始化数组或扩容。扩容时数组容量扩大到2倍然后ReHash,遍历原Entry数组,把所有的Entry重新Hash到新数组。通过<code>e.hash & (newCap - 1)</code>算出新的数组下标,原因是因为数组全是2的幂,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。然后链表和treenode重新放</li></ol><p>HashMap 在第一次 put 时初始化,类似 ArrayList 在第一次 add 时分配空间。<br>在哈希碰撞的链表长度达到TREEIFY_THRESHOLD(默认8)后,会把该链表转变成树结构</p><hr><h5 id="concurrenthashmap"><a href="#concurrenthashmap" class="headerlink" title="concurrenthashmap"></a>concurrenthashmap</h5><p>HashMap允许一个key和value为null,而ConcurrentHashMap不允许key和value为null,如果发现key或者value为null,则会抛出NPE。</p><p>和hashmap一样有Node<K,V></p><p>sizeCtl:<code>private transient volatile int sizeCtl;</code>这是一个用于同步多个线程的共享变量,如果值为负数,则说明table正在被某个线程初始化或者扩容。如果某个线程想要初始化table或者对table扩容,需要去竞争sizeCtl这个共享变量,获得变量的线程才有许可去进行接下来的操作,没能获得的线程将会一直自旋来尝试获得这个共享变量。获得sizeCtl这个变量的线程在完成工作之后再设置回来,使其他的线程可以走出自旋进行接下来的操作</p><p>查询和hashmap差不多,(hashCode & (length - 1))取下标。table数组是被volatile关键字修饰,解决了可见性问题</p><p>存要复杂一点。首先计算table下标,下标没数据就通过调用casTabAt方法插入数据。有的话,那么就给该下标处的Node(不管是链表的头还是树的根)加锁插入。 </p><p>扩容操作比较复杂。扩容操作的条件是如果table过小,并且没有被扩容,那么就需要进行扩容,需要使用transfer方法来将久的记录迁移到新的table中去。整个扩容操作分为两个部分,要用到内部类forwardNode。第一部分是构建一个nextTable,它的容量是原来的两倍,这个操作是单线程完成的。<br>第二个部分就是将原来table中的元素复制到nextTable中,这里允许多线程进行操作。</p><p>size()方法,结合baseCount和counterCells数组来得到,通过累计两者的数量即可获得当前ConcurrentHashMap中的记录总量。</p><hr><h5 id="HashSet"><a href="#HashSet" class="headerlink" title="HashSet"></a>HashSet</h5><p>用HashMap实现。(内部:<code>private transient HashMap<E,Object> map;</code>)</p><hr><h5 id="ArrayList"><a href="#ArrayList" class="headerlink" title="ArrayList"></a>ArrayList</h5><p>fail fast机制:checkForComodification()方法检查modCount,检查有无结构性的改变,变了抛<code>ConcurrentModificationException</code>。</p><p>扩容调<code>Arrays.copyOf(elementData, newCapacity);</code></p><p>内部有迭代器类 Iterator。</p><hr><h5 id="LinkedList"><a href="#LinkedList" class="headerlink" title="LinkedList"></a>LinkedList</h5><p>实现List和Deque(即可以当栈、队列、双向队列使用)</p><p>内部是一个双向链表</p><p>字段存有 size、 first Node(头节点)、last Node。通过头结点、尾节点可以很快地进行双向入队出队操作。<br>随机存储效率不如ArrayList,要遍历节点。按下标读取时,会按照size,判断是链表前半段还是后半段,根据这个从头或尾节点开始遍历。</p><p>和ArrayDeque的区别之一:LinkedList可以存null,而ArrayDeque不能存null。这点在写算法题的时候可以注意一下。</p><hr><h5 id="ArrayDeque"><a href="#ArrayDeque" class="headerlink" title="ArrayDeque"></a>ArrayDeque</h5><p>转一张表整理方法。一套接口遇到失败就会抛出异常,另一套遇到失败会返回特殊值。<br>| Queue Method | Equivalent Deque Method | 说明 |<br>| ———— | ———————– | ————————————– |<br>| <code>add(e)</code> | <code>addLast(e)</code> | 向队尾插入元素,失败则抛出异常 |<br>| <code>offer(e)</code> | <code>offerLast(e)</code> | 向队尾插入元素,失败则返回<code>false</code> |<br>| <code>remove()</code> | <code>removeFirst()</code> | 获取并删除队首元素,失败则抛出异常 |<br>| <code>poll()</code> | <code>pollFirst()</code> | 获取并删除队首元素,失败则返回<code>null</code> |<br>| <code>element()</code> | <code>getFirst()</code> | 获取但不删除队首元素,失败则抛出异常 |<br>| <code>peek()</code> | <code>peekFirst()</code> | 获取但不删除队首元素,失败则返回<code>null</code> |</p><p>内部elements数组的容量一定是2的倍数,并且不会满。存数组的head和tail下标,形成一个循环数组,当这两个下标相等时,数组为空。而在添加元素时,如果这两个下标相等,说明数组已满,将容量翻倍。扩容时重置头索引和尾索引,头索引置为0,尾索引置为原容量的值。</p><hr><h5 id="CopyOnWriteArrayList"><a href="#CopyOnWriteArrayList" class="headerlink" title="CopyOnWriteArrayList"></a>CopyOnWriteArrayList</h5><p>线程安全<br>add set之类的操作都是新建一个复制arraylist<br>适用于 读多些少, 并且数据内容变化比较少的场景</p>]]></content>
<summary type="html"><p>这是自己整理的一些Collection的要点笔记,比较零碎,可能可读性不是很强。有新内容时会进行补充。<br>Java Collection框架:</p>
<ul>
<li>Set , HashSet TreeSet(实现SortedSet)<ul>
<li>SortedSet</li>
</ul>
</li>
<li>List , LinkedList ArrayList</li>
<li>Queue, PriorityQueue</li>
<li>Dequeue</li>
<li>Map , HashMap TreeMap(实现SortedMap)<ul>
<li>SortedMap</li>
</ul>
</li>
</ul></summary>
<category term="开发" scheme="https://juniousy.github.io/categories/%E5%BC%80%E5%8F%91/"/>
<category term="Java" scheme="https://juniousy.github.io/tags/Java/"/>
</entry>
<entry>
<title>【本科时期文章】实现LRUCache</title>
<link href="https://juniousy.github.io/2019/03/11/%E5%AD%98%E6%A1%A3/%E5%AE%9E%E7%8E%B0LRUCache/"/>
<id>https://juniousy.github.io/2019/03/11/%E5%AD%98%E6%A1%A3/%E5%AE%9E%E7%8E%B0LRUCache/</id>
<published>2019-03-11T14:57:49.000Z</published>
<updated>2022-05-30T09:06:50.984Z</updated>
<content type="html"><![CDATA[<p>LRU Cache 算法是操作系统在进行内存管理时可以采用的一种页面置换算法。LRU,就是Least Recently Used的简称,这个算法叫做最近最少使用算法。除了在页面置换中可以使用这一算法,其他需要缓存的场景也可以运用这一算法。这一算法的核心目的就是依照程序的就近原则,尽可能在有限的空间内缓存最多以后会使用到的内容。另外,实现这一算法也是一道<a href="https://leetcode.com/problems/lru-cache/">LeetCode题目</a>。本文就是演示如何使用java语言实现这一算法。</p><span id="more"></span><hr><h3 id="LinkedHashMap实现"><a href="#LinkedHashMap实现" class="headerlink" title="LinkedHashMap实现"></a>LinkedHashMap实现</h3><p>LinkedHashMap是最容易的实现方式,因为它内部的实现方式很贴合这一应用,至于为什么下面会有介绍。<br>LinkedHashMap和普通的HashMap不同的地方在于,它保存了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。而LRU要求最近读取过得内容有最高的缓存优先度,也就是按照访问顺序来进行迭代。而通过重写removeEldestEntry方法可以让LinkedHashMap保留有限多的数据,删除缓存中不需要的数据。</p><pre><code class="java">//简易实现class LRUCache<K, V> { private static final float loadFactor = 0.75f; private int capacity; private final LinkedHashMap<K, V> map; public LRUCache(int capacity) { if (capacity < 0) { capacity = 0; } this.capacity = capacity; //构造函数参数分别是initialCapacity、loadFactor、accessOrder,accessOrder为true即按访问顺序迭代 map = new LinkedHashMap<K, V>(0, loadFactor, true){ @Override protected boolean removeEldestEntry(Entry eldest) { return size() > LRUCache.this.capacity; } }; } public final V get(K key) { return map.get(key); } public final void put(K key, V value) { map.put(key, value); } }</code></pre><hr><h3 id="HashMap-双向链表实现"><a href="#HashMap-双向链表实现" class="headerlink" title="HashMap + 双向链表实现"></a>HashMap + 双向链表实现</h3><p>之所以LinkedHashMap能保有这样的性质,是因为它内部的实现是依托了HashMap和双向链表,因此不用LinkedHashMap我们也能实现LRUCache算法。</p><p>基本框架</p><pre><code class="java">public class LRUCache<K, V> { private int capacity; private HashMap<K, Node<K, V>> map; private Node<K, V> head; private Node<K, V> tail; public LRUCache(int capacity) { this.capacity = capacity; map = new HashMap<>(capacity); head = new Node<>(); tail = new Node<>(); head.next = tail; tail.pre = head; } class Node<K, V> { K key; V value; Node<K, V> pre; Node<K, V> next; }}</code></pre><p>公用方法</p><pre><code class="java">private void raiseNode(Node<K, V> node) { if (node.pre == head) { return; } Node<K, V> pre = node.pre; Node<K, V> next = node.next; pre.next = next; next.pre = pre; setFirst(node);}private void setFirst(Node<K, V> node) { Node<K, V> first = head.next; head.next = node; node.pre = head; first.pre = node; node.next = first;}</code></pre><p>get方法,从map里拿Value,同时将它置为链表头</p><pre><code class="java">public V get(K key) { if (!map.containsKey(key)) { return null; } Node<K, V> node = map.get(key); raiseNode(node); return node.value;}</code></pre><p>save方法,如果缓存已满,删除链表尾的值,再添加新的值到链表头</p><pre><code class="java">public void save(K key, V value) { if (map.containsKey(key)) { updateNode(key, value); } else { insertNode(key, value); }}private void updateNode(K key, V value) { Node node = map.get(key); node.value = value; raiseNode(node);}private void insertNode(K key, V value) { if (isFull()) { removeLast(); } Node node = new Node(); node.key = key; node.value = value; setFirst(node); map.put(key, node);}private boolean isFull() { return map.size() >= capacity;}</code></pre><p>测试</p><pre><code class="java">import org.junit.Test;public class LRUCacheTest { LRUCache<Integer, Integer> cache = new LRUCache(3); @Test public void test() { cache.save(1, 7); cache.save(2, 0); cache.save(3, 1); cache.save(4, 2); assert 0 == cache.get(2); assert null == cache.get(7); cache.save(5, 3); assert 0 == cache.get(2); cache.save(6, 4); assert null == cache.get(4); }}//head -> 7 -> tail//head -> 0 -> 7 -> tail//head -> 1 -> 0 -> 7 -> tail//head -> 2 -> 1 -> 0 -> tail//head -> 3 -> 2 -> 1 -> tail//head -> 2 -> 3 -> 1 -> tail//head -> 4 -> 2 -> 3 -> tail</code></pre>]]></content>
<summary type="html"><p>LRU Cache 算法是操作系统在进行内存管理时可以采用的一种页面置换算法。LRU,就是Least Recently Used的简称,这个算法叫做最近最少使用算法。除了在页面置换中可以使用这一算法,其他需要缓存的场景也可以运用这一算法。这一算法的核心目的就是依照程序的就近原则,尽可能在有限的空间内缓存最多以后会使用到的内容。另外,实现这一算法也是一道<a href="https://leetcode.com/problems/lru-cache/">LeetCode题目</a>。本文就是演示如何使用java语言实现这一算法。</p></summary>
<category term="开发" scheme="https://juniousy.github.io/categories/%E5%BC%80%E5%8F%91/"/>
<category term="算法" scheme="https://juniousy.github.io/tags/%E7%AE%97%E6%B3%95/"/>
</entry>
<entry>
<title>【本科时期文章】生产者消费者模型的一个例子</title>
<link href="https://juniousy.github.io/2019/03/02/%E5%AD%98%E6%A1%A3/%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%80%E4%B8%AA%E5%8F%98%E5%9E%8B/"/>
<id>https://juniousy.github.io/2019/03/02/%E5%AD%98%E6%A1%A3/%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%80%E4%B8%AA%E5%8F%98%E5%9E%8B/</id>
<published>2019-03-02T14:08:53.000Z</published>
<updated>2022-05-30T09:06:50.985Z</updated>
<content type="html"><![CDATA[<p>一般的生产者消费者模型中,生产者和消费者都是尽可能快地处理任务。但在工作中,我遇到了一种情况,需要每个消费者尽可能多地解决一批任务,这样可以打包处理,降低I/O频次。<br>我当时用的方法是在消费者端给BlockingQueue加锁。后来想想这种方法多余了。<br>这篇文章一是讨论一下这种方法,作个反思,二来作为新博客的第一篇文章,起个开头。</p><span id="more"></span><hr><h4 id="模拟当时的解决方法"><a href="#模拟当时的解决方法" class="headerlink" title="模拟当时的解决方法"></a>模拟当时的解决方法</h4><p>用来解决生产者消费者问题的BlockingQueue:</p><pre><code class="java">private static final BlockingQueue<Task> taskBlockingQueue = new ArrayBlockingQueue<>(100);</code></pre><p>生产者部分没有什么区别,直接往队列里添加任务:</p><pre><code class="java">private void produce(int taskId) { try { taskBlockingQueue.put(new Task(taskId)); System.out.println(String.format("生产者%d\t添加任务%d", id, taskId)); } catch (InterruptedException e) { e.printStackTrace(); }}</code></pre><p>消费者部分在打包的过程中都对阻塞队列加锁,不允许其他消费者获取任务:</p><pre><code class="java">private static final Lock packageLock = new ReentrantLock();</code></pre><p>消费者需要在指定时间内打包,超时则退出这轮消费。</p><pre><code class="java">private void consume() { try { if (packageLock.tryLock(5, TimeUnit.MILLISECONDS)) { try { doPackage(); } finally { packageLock.unlock(); } } } catch (InterruptedException e) { e.printStackTrace(); }}private void doPackage() { long start = System.currentTimeMillis(); long end; int packageNum = 0; for (int i = 0; i < consumerPackageSize; i++) { doConsume(); packageNum++; end = System.currentTimeMillis(); if (end - start > packageTime) { break; } } end = System.currentTimeMillis(); System.out.println(String.format("消费者%d\t打包%d个\t耗时%d", id, packageNum, end - start));}private void doConsume() { Task task = null; try { task = taskBlockingQueue.poll(100, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } if (task == null) { return; } System.out.println(String.format("消费者%d\t完成任务%d", id, task.getId()));}</code></pre><p>整个完整的测试类:</p><pre><code class="java">import lombok.AllArgsConstructor;import lombok.Data;import java.util.Random;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * @author Junious * @date 2019/02/25 **/public class ProducerAndConsumerTest { private static final int producerNumber = 5; private static final int consumerNumber = 5; private static final BlockingQueue<Task> taskBlockingQueue = new ArrayBlockingQueue<>(100); private static final Lock packageLock = new ReentrantLock(); private static final int consumerPackageSize = 20; private static final int packageTime = 2000; public static void main(String[] args) { Runtime.getRuntime().addShutdownHook( new Thread(() -> System.out.println ("queue size:" + taskBlockingQueue.size())) ); ProducerAndConsumerTest test = new ProducerAndConsumerTest(); test.init(); } private void init() { //add producers for (int i = 0; i < producerNumber; i++) { Thread t = new Thread(new Producer(i)); t.start(); } //add consumers for (int i = 0; i < consumerNumber; i++) { Thread t = new Thread(new Consumer(i)); t.start(); } } @AllArgsConstructor @Data class Producer implements Runnable { private int id; @Override public void run() { Random random = new Random(); while (true) { int taskId = random.nextInt(1000) + 1; produce(taskId); try { Thread.sleep(random.nextInt(300) + 400); } catch (InterruptedException e) { e.printStackTrace(); break; } } } private void produce(int taskId) { try { taskBlockingQueue.put(new Task(taskId)); System.out.println(String.format("生产者%d\t添加任务%d", id, taskId)); } catch (InterruptedException e) { e.printStackTrace(); } } } @AllArgsConstructor @Data class Consumer implements Runnable { private int id; @Override public void run() { Random random = new Random(); while (true) { consume(); try { Thread.sleep(random.nextInt(300) + 400); } catch (InterruptedException e) { e.printStackTrace(); break; } } } private void consume() { try { if (packageLock.tryLock(5, TimeUnit.MILLISECONDS)) { try { doPackage(); } finally { packageLock.unlock(); } } } catch (InterruptedException e) { e.printStackTrace(); } } private void doPackage() { long start = System.currentTimeMillis(); long end; int packageNum = 0; for (int i = 0; i < consumerPackageSize; i++) { doConsume(); packageNum++; end = System.currentTimeMillis(); if (end - start > packageTime) { break; } } end = System.currentTimeMillis(); System.out.println(String.format("消费者%d\t打包%d个\t耗时%d", id, packageNum, end - start)); } private void doConsume() { Task task = null; try { task = taskBlockingQueue.poll(100, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } if (task == null) { return; } System.out.println(String.format("消费者%d\t完成任务%d", id, task.getId())); } }}@AllArgsConstructor@Dataclass Task { private int id;}</code></pre><p>截取一段测试结果:</p><pre><code>生产者0 添加任务545生产者2 添加任务943生产者1 添加任务359生产者3 添加任务97生产者4 添加任务705消费者2 完成任务545消费者2 完成任务359消费者2 完成任务943消费者2 完成任务97消费者2 完成任务705生产者2 添加任务32消费者2 完成任务32生产者4 添加任务488消费者2 完成任务488生产者1 添加任务691消费者2 完成任务691消费者2 完成任务3生产者3 添加任务3生产者0 添加任务815消费者2 完成任务815消费者2 完成任务290生产者1 添加任务290消费者2 完成任务408生产者2 添加任务408消费者2 完成任务873生产者3 添加任务873消费者2 打包20个 耗时1165生产者0 添加任务852消费者1 完成任务852消费者1 完成任务743生产者4 添加任务743生产者0 添加任务114消费者1 完成任务114生产者1 添加任务454消费者1 完成任务454消费者1 完成任务920生产者2 添加任务920生产者3 添加任务847消费者1 完成任务847生产者4 添加任务905消费者1 完成任务905生产者3 添加任务698消费者1 完成任务698生产者1 添加任务372消费者1 完成任务372生产者0 添加任务568消费者1 完成任务568生产者2 添加任务419消费者1 完成任务419生产者4 添加任务417消费者1 完成任务417消费者1 打包20个 耗时1295生产者3 添加任务888消费者0 完成任务888生产者1 添加任务189消费者0 完成任务189生产者2 添加任务892消费者0 完成任务892生产者4 添加任务375消费者0 完成任务375生产者0 添加任务723消费者0 完成任务723生产者3 添加任务543消费者0 完成任务543消费者0 完成任务205生产者1 添加任务205生产者2 添加任务657消费者0 完成任务657生产者0 添加任务549消费者0 完成任务549生产者4 添加任务812消费者0 完成任务812生产者3 添加任务737消费者0 完成任务737消费者0 打包20个 耗时1208生产者2 添加任务784消费者4 完成任务784生产者1 添加任务252消费者4 完成任务252生产者0 添加任务622消费者4 完成任务622生产者4 添加任务524消费者4 完成任务524生产者3 添加任务73消费者4 完成任务73生产者2 添加任务491消费者4 完成任务491生产者0 添加任务225消费者4 完成任务225生产者1 添加任务207消费者4 完成任务207生产者4 添加任务326消费者4 完成任务326生产者2 添加任务983消费者4 完成任务983生产者0 添加任务865消费者4 完成任务865生产者3 添加任务347消费者4 完成任务347消费者4 打包20个 耗时1318</code></pre><p>可以看到每次打包只有一个消费者在进行消费,其实相当于只有一个消费者线程,等于没有使用并发。<br>当消费者任务很耗时时:</p><pre><code class="java">private void doConsume() { Task task = null; try { task = taskBlockingQueue.poll(100, TimeUnit.MILLISECONDS); //模拟耗时 Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } if (task == null) { return; } System.out.println(String.format("消费者%d\t完成任务%d", id, task.getId()));}</code></pre><p>这时候在中止时可以看到队列有10到30不等的Task暂留。模拟耗时越长,暂留的越多,也就是相当于性能越差。</p><hr><h3 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h3><p>实际上不需要加锁,设定一个超时时间即可。</p><pre><code class="java">@AllArgsConstructor@Dataclass Consumer2 implements Runnable { private int id; @Override public void run() { Random random = new Random(); while (true) { try { consume(); Thread.sleep(random.nextInt(300) + 400); } catch (InterruptedException e) { e.printStackTrace(); break; } } } private void consume() throws InterruptedException { long start = System.currentTimeMillis(); ArrayList<Task> tasks = new ArrayList<>(); long end; int packageNum = 0; for (int i = 0; i < consumerPackageSize; i++) { //模拟打包任务 Task task = taskBlockingQueue.poll(packageTime, TimeUnit.MILLISECONDS); if (task == null) { continue; } Thread.sleep(200); tasks.add(task); packageNum++; end = System.currentTimeMillis(); if (end - start > packageTime) { break; } } end = System.currentTimeMillis(); System.out.println(String.format("消费者%d\t打包%d个\t耗时%d\t%s", id, packageNum, end - start, tasks)); }}</code></pre><pre><code>生产者2 添加任务539生产者0 添加任务319生产者1 添加任务655生产者4 添加任务843生产者3 添加任务788生产者0 添加任务716生产者3 添加任务176生产者4 添加任务735生产者2 添加任务7生产者1 添加任务283生产者4 添加任务466生产者3 添加任务486生产者0 添加任务649生产者2 添加任务373生产者1 添加任务158生产者0 添加任务532生产者4 添加任务914生产者1 添加任务734生产者3 添加任务571生产者2 添加任务114生产者1 添加任务340生产者3 添加任务670生产者0 添加任务482生产者2 添加任务298生产者4 添加任务598消费者0 打包5个 耗时2213 [Task(id=655), Task(id=716), Task(id=466), Task(id=532), Task(id=340)]消费者4 打包5个 耗时2280 [Task(id=319), Task(id=176), Task(id=486), Task(id=914), Task(id=670)]消费者1 打包5个 耗时2328 [Task(id=788), Task(id=735), Task(id=649), Task(id=734), Task(id=482)]消费者3 打包5个 耗时2351 [Task(id=843), Task(id=7), Task(id=373), Task(id=571), Task(id=298)]消费者2 打包5个 耗时2400 [Task(id=539), Task(id=283), Task(id=158), Task(id=114), Task(id=598)]生产者3 添加任务928生产者0 添加任务360生产者1 添加任务724生产者2 添加任务539生产者4 添加任务926生产者3 添加任务206生产者0 添加任务596生产者1 添加任务841生产者4 添加任务834生产者2 添加任务340生产者3 添加任务585生产者1 添加任务500生产者4 添加任务532生产者0 添加任务800生产者2 添加任务914生产者3 添加任务202生产者1 添加任务850生产者0 添加任务506生产者1 添加任务785生产者2 添加任务633生产者4 添加任务182生产者3 添加任务154生产者0 添加任务13生产者2 添加任务880消费者3 打包5个 耗时2199 [Task(id=724), Task(id=841), Task(id=532), Task(id=506), Task(id=13)]生产者4 添加任务214消费者0 打包5个 耗时2217 [Task(id=539), Task(id=834), Task(id=800), Task(id=785), Task(id=880)]生产者1 添加任务789生产者3 添加任务786消费者4 打包6个 耗时2583 [Task(id=928), Task(id=926), Task(id=340), Task(id=914), Task(id=633), Task(id=214)]生产者0 添加任务468生产者2 添加任务189消费者1 打包6个 耗时2597 [Task(id=360), Task(id=206), Task(id=585), Task(id=202), Task(id=182), Task(id=789)]消费者2 打包5个 耗时2465 [Task(id=596), Task(id=500), Task(id=850), Task(id=154), Task(id=786)]生产者4 添加任务812生产者0 添加任务239生产者3 添加任务671生产者1 添加任务730生产者2 添加任务124生产者4 添加任务679生产者0 添加任务320生产者2 添加任务917生产者1 添加任务986生产者3 添加任务557生产者0 添加任务415生产者4 添加任务559生产者2 添加任务880生产者1 添加任务920生产者3 添加任务502生产者0 添加任务679生产者4 添加任务823生产者2 添加任务594生产者1 添加任务336生产者3 添加任务502生产者0 添加任务453生产者4 添加任务360消费者0 打包6个 耗时2249 [Task(id=468), Task(id=239), Task(id=679), Task(id=415), Task(id=679), Task(id=453)]消费者3 打包6个 耗时2320 [Task(id=189), Task(id=671), Task(id=320), Task(id=559), Task(id=823), Task(id=360)]生产者2 添加任务823生产者3 添加任务857生产者1 添加任务395生产者0 添加任务937生产者4 添加任务817消费者2 打包4个 耗时2264 [Task(id=917), Task(id=880), Task(id=594), Task(id=823)]消费者4 打包6个 耗时2622 [Task(id=812), Task(id=730), Task(id=986), Task(id=920), Task(id=336), Task(id=857)]消费者1 打包5个 耗时2408 [Task(id=124), Task(id=557), Task(id=502), Task(id=502), Task(id=395)]</code></pre><p>这时候中止可以看到队列几乎没有Task暂留</p><p>当设置消费者消费时间为1000ms时,运行一段时间队列就满了,这时候是当增加消费者线程数即可让任务处理跟上生产者的生产速度。</p>]]></content>
<summary type="html"><p>一般的生产者消费者模型中,生产者和消费者都是尽可能快地处理任务。但在工作中,我遇到了一种情况,需要每个消费者尽可能多地解决一批任务,这样可以打包处理,降低I/O频次。<br>我当时用的方法是在消费者端给BlockingQueue加锁。后来想想这种方法多余了。<br>这篇文章一是讨论一下这种方法,作个反思,二来作为新博客的第一篇文章,起个开头。</p></summary>
<category term="开发" scheme="https://juniousy.github.io/categories/%E5%BC%80%E5%8F%91/"/>
<category term="Java" scheme="https://juniousy.github.io/tags/Java/"/>
<category term="并发" scheme="https://juniousy.github.io/tags/%E5%B9%B6%E5%8F%91/"/>
</entry>
</feed>