-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
308 lines (147 loc) · 553 KB
/
search.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
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>使用 LLaMA-Factory 微调 Qwen2.5</title>
<link href="/junwei/27f857ef.html"/>
<url>/junwei/27f857ef.html</url>
<content type="html"><![CDATA[<h1 id="使用-LLaMA-Factory-微调-Qwen2-5"><a href="#使用-LLaMA-Factory-微调-Qwen2-5" class="headerlink" title="使用 LLaMA-Factory 微调 Qwen2.5"></a>使用 LLaMA-Factory 微调 Qwen2.5</h1><h2 id="实验环境"><a href="#实验环境" class="headerlink" title="实验环境"></a>实验环境</h2><p>Windows11、Git</p><h2 id="基座模型"><a href="#基座模型" class="headerlink" title="基座模型"></a>基座模型</h2><ol><li>去到<a href="https://modelscope.cn/my/overview"> 魔搭社区</a>下载一个基座模型</li></ol><p><img src="/../images/27f857ef/image-20241031145119565.png" alt="image-20241031145119565"></p><ol start="2"><li>这里选择<code>Qwen/Qwen2.5-0.5B-Instruct</code>,点进去,中间模型文件点击右边的下载模型下载到本地(下载方法自己选择)</li></ol><h2 id="LLaMA-Factory框架"><a href="#LLaMA-Factory框架" class="headerlink" title="LLaMA-Factory框架"></a>LLaMA-Factory框架</h2><h3 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h3><p>到<a href="https://github.com/hiyouga/LLaMA-Factory">LLaMA-Factory</a>的仓库下载LLaMA-Factory,中文手册<a href="https://github.com/hiyouga/LLaMA-Factory/blob/main/README_zh.md">README_zh.md</a></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> --depth 1 https://github.com/hiyouga/LLaMA-Factory.git</span><br><span class="line"><span class="built_in">cd</span> LLaMA-Factory</span><br><span class="line">pip install -e .</span><br></pre></td></tr></table></figure><blockquote><p>注意:如果是有GPU的需要自己去pytorch官网下载带CUDA的torch,否则怎么安装都是CPU版本的</p></blockquote><h3 id="准备训练数据"><a href="#准备训练数据" class="headerlink" title="准备训练数据"></a>准备训练数据</h3><ol><li><p>建立自己的数据集JSON格式文件,训练需要有固定的JSON格式</p></li><li><p>我使用的是Alpaca格式,类似于下面的JSON格式,但是还可以添加其他属性,我这里只是有简单的<code>instruction</code>、<code>input</code>、<code>output</code></p></li></ol><p><img src="/../images/27f857ef/image-20241031153551195.png" alt="image-20241031153551195"></p><blockquote><p>具体数据格式细节可看<a href="https://github.com/hiyouga/LLaMA-Factory/blob/main/data/README_zh.md">LLaMA-Factory/data/README_zh.md</a></p></blockquote><ol start="3"><li>将训练数据<code>train.json</code>放在data文件夹中</li><li>修改数据集描述文件:data文件夹中的<code>dataset_info.json</code>文件,最上面加上你的数据集让他可以识别出来</li></ol><p><img src="/../images/27f857ef/image-20241031153656923.png" alt="image-20241031153656923"></p><h3 id="启动Web-UI"><a href="#启动Web-UI" class="headerlink" title="启动Web UI"></a>启动Web UI</h3><p>找到<code>src</code>文件夹下的<code>webui.py</code>,运行即可自动打开浏览器,看到LLaMA-Factory的WebUI</p><blockquote><p>如果还缺少依赖自行安装</p></blockquote><h3 id="微调模型"><a href="#微调模型" class="headerlink" title="微调模型"></a>微调模型</h3><ol><li>先选择语言为<code>zh</code>,然后在模型名称处选择自己第一步下载的基座模型名称,再指定你模型下载的路径</li></ol><p><img src="/../images/27f857ef/image-20241031154502304.png" alt="image-20241031154502304"></p><blockquote><p>推荐使用绝对路径,因为相对路径是相对于LLaMA-Factory文件夹的</p></blockquote><ol start="2"><li>为了测试模型路径是否选择正确,可以先到Chat处进行对话,证明模型加载成功</li></ol><p><img src="/../images/27f857ef/image-20241031154642278.png" alt="image-20241031154642278"></p><ol start="3"><li>从Chat回到Train处,参数先不管(不懂),选择数据集,预览一下,可以看到数据出来证明成功</li></ol><p><img src="/../images/27f857ef/image-20241031154821509.png" alt="image-20241031154821509"></p><ol start="4"><li>到下方,预览命令->保存训练参数->载入训练参数->开始,直接按顺序点过去</li></ol><p><img src="/../images/27f857ef/image-20241031155121137.png" alt="image-20241031155121137"></p><ol start="5"><li>可以看到控制台中代码开始跑了,说明开始训练</li></ol><p><img src="/../images/27f857ef/image-20241031155240922.png" alt="image-20241031155240922"></p><ol start="6"><li>训练完毕,会出现提示和训练损失曲线</li></ol><p><img src="/../images/27f857ef/image-20241031155402155.png" alt="image-20241031155402155"></p><h3 id="测试训练的模型"><a href="#测试训练的模型" class="headerlink" title="测试训练的模型"></a>测试训练的模型</h3><ol><li><p>来到Chat处,选择检查点路径,这个检查点就相当于是我们的一个训练存档,选择刚刚训练的检查点</p></li><li><p>对话发现没有达到效果,他还叫通义千问,<strong>可能是由于我们训练的太少的问题</strong></p></li></ol><p><img src="/../images/27f857ef/image-20241031160735063.png" alt="image-20241031160735063"></p><ol start="3"><li>重新训练,<strong>降低学习率为4e-4(学慢点),然后加大训练轮数为10.0</strong>,重新加载新的检查点,这次效果达到了预期</li></ol><p><img src="/../images/27f857ef/image-20241031163909363.png" alt="image-20241031163909363"></p><blockquote><p>实测降低学习率和加大轮数可以达到好一点的效果,第一次损失曲线知道了2.9以下,修正后可以到0.5以下,不懂原理,可能是这个原因</p></blockquote><h3 id="导出训练后的模型"><a href="#导出训练后的模型" class="headerlink" title="导出训练后的模型"></a>导出训练后的模型</h3><ol><li>训练好后我们可以导出模型,选择我们想要的检查点,检查点+基座模型=我们的模型</li></ol><p><img src="/../images/27f857ef/image-20241031161738158.png" alt="image-20241031161738158"></p><ol start="2"><li>点击导出,导出后你可以发现他和我们的基座模型文件内容是一样的,因为导出本质上还是一个模型</li></ol><p><img src="/../images/27f857ef/image-20241031162143000.png" alt="image-20241031162143000"></p><ol start="2"><li>可以到Chat中测试导出的新模型,只需改变模型地址即可,无需再选择检查点,也可到达同样的效果</li></ol><p><img src="/../images/27f857ef/image-20241031162620186.png" alt="image-20241031162620186"></p><h3 id="导入Ollama运行"><a href="#导入Ollama运行" class="headerlink" title="导入Ollama运行"></a>导入Ollama运行</h3><p>我们的模型不进行模型转换的话,无法在Ollama中使用,因为Ollama不支持我们这种safetensors 格式的模型,需要将其转为GGUF格式,详见<a href="https://github.com/ollama/ollama">ollama</a>。</p><p>推荐使用<a href="https://github.com/ggerganov/llama.cpp">llama.cpp: LLM inference in C/C++</a>转换为GGUF文件,即可导入Ollama,交给大家自己摸索,他不仅仅可用转换,更大的作用是量化模型,这个具体我也没了解……</p>]]></content>
</entry>
<entry>
<title>编写优雅代码:如何用卫语句简化复杂逻辑</title>
<link href="/junwei/60db17a5.html"/>
<url>/junwei/60db17a5.html</url>
<content type="html"><![CDATA[<h1 id="什么是卫语句"><a href="#什么是卫语句" class="headerlink" title="什么是卫语句"></a>什么是卫语句</h1><p><strong>卫语句</strong>(Guard Clause)是一种编程中的设计模式,用来<strong>提前处理异常情况或不符合预期的条件</strong>,从而<strong>避免深层嵌套和复杂的条件逻辑</strong>。</p><h1 id="快速认识卫语句"><a href="#快速认识卫语句" class="headerlink" title="快速认识卫语句"></a>快速认识卫语句</h1><p>假设有一个函数,它的任务是接收用户信息并发送欢迎邮件。需要对用户信息进行多项验证,比如检查用户是否为 null、是否激活账户、是否有有效的邮箱地址等。如果所有条件都满足,就执行发送邮件操作。</p><h2 id="反例:未使用卫语句(嵌套过深)"><a href="#反例:未使用卫语句(嵌套过深)" class="headerlink" title="反例:未使用卫语句(嵌套过深)"></a>反例:未使用卫语句(嵌套过深)</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">sendWelcomeEmail</span><span class="params">(User user)</span> {</span><br><span class="line"> <span class="keyword">if</span> (user != <span class="literal">null</span>) { <span class="comment">// 检查用户是否为空</span></span><br><span class="line"> <span class="keyword">if</span> (user.isActive()) { <span class="comment">// 检查用户是否激活</span></span><br><span class="line"> <span class="keyword">if</span> (user.getEmail() != <span class="literal">null</span> && !user.getEmail().isEmpty()) { <span class="comment">// 检查邮箱是否有效</span></span><br><span class="line"> System.out.println(<span class="string">"正在向 "</span> + user.getEmail() + <span class="string">" 发送欢迎邮件"</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> System.out.println(<span class="string">"邮箱地址无效"</span>);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> System.out.println(<span class="string">"用户未激活"</span>);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> System.out.println(<span class="string">"用户为空"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>问题分析:</strong></p><ul><li><strong>多重嵌套</strong>使得代码的可读性差,很难一眼看出主要的逻辑是“发送欢迎邮件”。</li><li>需要<strong>逐层阅读</strong>每个 if-else 条件,理解起来费力。</li></ul><h2 id="使用了卫语句重构"><a href="#使用了卫语句重构" class="headerlink" title="使用了卫语句重构"></a>使用了卫语句重构</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><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="keyword">public</span> <span class="keyword">void</span> <span class="title function_">sendWelcomeEmail</span><span class="params">(User user)</span> {</span><br><span class="line"> <span class="comment">// 使用卫语句提前返回处理特殊情况</span></span><br><span class="line"> <span class="keyword">if</span> (user == <span class="literal">null</span>) {</span><br><span class="line"> System.out.println(<span class="string">"用户为空"</span>);</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 class="keyword">if</span> (!user.isActive()) {</span><br><span class="line"> System.out.println(<span class="string">"用户未激活"</span>);</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 class="keyword">if</span> (user.getEmail() == <span class="literal">null</span> || user.getEmail().isEmpty()) {</span><br><span class="line"> System.out.println(<span class="string">"邮箱地址无效"</span>);</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 class="comment">// 通过条件后,执行主逻辑</span></span><br><span class="line"> System.out.println(<span class="string">"正在向 "</span> + user.getEmail() + <span class="string">" 发送欢迎邮件"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>重构后的优点:</strong></p><ul><li>通过<strong>提前退出</strong> (return) 来处理各种不符合条件的情况,避免了多层 if-else 嵌套。</li><li>主逻辑部分(发送欢迎邮件)保持在函数的末尾,使代码清晰地表达出主要目的:发送邮件。</li><li>更容易维护:如果需要新增条件,只需添加新的卫语句,而不影响主逻辑的可读性。</li></ul><h2 id="进一步优化:使用异常代替-return"><a href="#进一步优化:使用异常代替-return" class="headerlink" title="进一步优化:使用异常代替 return"></a>进一步优化:使用异常代替 return</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">sendWelcomeEmail</span><span class="params">(User user)</span> {</span><br><span class="line"> <span class="keyword">if</span> (user == <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<span class="string">"用户对象不能为 null"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!user.isActive()) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">"用户未激活,无法发送欢迎邮件"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (user.getEmail() == <span class="literal">null</span> || user.getEmail().isEmpty()) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalArgumentException</span>(<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"> System.out.println(<span class="string">"正在向 "</span> + user.getEmail() + <span class="string">" 发送欢迎邮件"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="卫语句的特点"><a href="#卫语句的特点" class="headerlink" title="卫语句的特点"></a>卫语句的特点</h1><ol><li>条件判断简洁明了:卫语句通过在函数的开头放置判断条件,使得代码的控制流清晰易懂。</li><li>提前退出:如果某个条件不满足,函数会立刻终止(返回或抛出异常),不再执行后续的代码逻辑。</li><li>减少嵌套:由于提前处理不符合条件的分支,主逻辑部分的代码不会受到多重嵌套的影响,从而保持代码的平坦性。</li></ol><h1 id="卫语句的作用"><a href="#卫语句的作用" class="headerlink" title="卫语句的作用"></a>卫语句的作用</h1><ol><li><p>提高代码的可读性和可维护性: 卫语句能将函数中的异常情况和特殊条件处理放在前面,使得正常流程保持在函数的主干部分,从而让代码更易于理解和维护。</p></li><li><p>减少嵌套和代码复杂度: 通过尽早返回或抛出异常,避免了在函数中层层嵌套 if-else 语句。这样不仅减少了代码的层次深度,还让主流程更加直观。</p></li><li><p>提高错误处理的透明度: 通过卫语句可以清楚地表明哪些输入参数、条件是合法的,并能够快速处理不合法的情况,使得代码对异常情况的处理变得更加直接。</p></li></ol><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><blockquote><p>阿里巴巴Java开发手册(嵩山版)八、控制语句第7点:<br>超过3层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现<br><img src="/../images/60db17a5/012d2a30701a45afb68a4b116f11a501.png"></p></blockquote>]]></content>
<categories>
<category> 代码优化 </category>
</categories>
<tags>
<tag> 代码优化 </tag>
</tags>
</entry>
<entry>
<title>部署之道:整合、分离与容器化的前后端部署方法</title>
<link href="/junwei/b42087a8.html"/>
<url>/junwei/b42087a8.html</url>
<content type="html"><![CDATA[<h1 id="将-Vue打包文件直接集成到后端部署"><a href="#将-Vue打包文件直接集成到后端部署" class="headerlink" title="将 Vue打包文件直接集成到后端部署"></a>将 Vue打包文件直接集成到后端部署</h1><p>这里使用我的大二课程作业给大家在 Windows 下演示将 Vue打包文件直接集成到后端部署</p><h2 id="配置前端构建输出"><a href="#配置前端构建输出" class="headerlink" title="配置前端构建输出"></a>配置前端构建输出</h2><p>打开你的 Vue 项目,在终端中输入<code>npm run build</code>命令,直到看见<code>Build complete.</code>说明打包完成</p><p><img src="/../images/b42087a8/1.png" alt="1"></p><p>打包完成后你会在目录中看见一个<code>dist</code>文件夹,里面会有<code>index.html</code>文件和<code>css</code>、<code>img</code>、<code>js</code>等文件夹</p><p><img src="/../images/b42087a8/2.png" alt="2"></p><h2 id="将-dist-中文件复制到后端项目中"><a href="#将-dist-中文件复制到后端项目中" class="headerlink" title="将 dist 中文件复制到后端项目中"></a>将 <code>dist</code> 中文件复制到后端项目中</h2><p>假设你的后端项目使用的是Spring Boot,你需要将<code>dist</code>文件夹中的内容复制到<code>src/main/resources/static</code>目录下。这个目录是Spring Boot默认的静态资源文件目录。</p><img src="../images/b42087a8/3.png" alt="3" style="zoom: 80%;" /><h2 id="配置后端服务器以服务静态文件"><a href="#配置后端服务器以服务静态文件" class="headerlink" title="配置后端服务器以服务静态文件"></a>配置后端服务器以服务静态文件</h2><p>如果是 Spring Boot 项目,确保在<code>application.properties</code>或<code>application.yml</code>中有以下配置,这会告诉Spring Boot在处理请求时,会首先在<code>static</code>目录下查找匹配的静态资源</p><figure class="highlight properties"><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="attr">spring</span>:<span class="string"></span></span><br><span class="line"> <span class="attr">web</span>:<span class="string"></span></span><br><span class="line"> <span class="attr">resources</span>:<span class="string"></span></span><br><span class="line"> <span class="attr">static-locations</span>: <span class="string">classpath:/static</span></span><br></pre></td></tr></table></figure><h2 id="部署与测试"><a href="#部署与测试" class="headerlink" title="部署与测试"></a>部署与测试</h2><p>项目的启动可以直接使用 IDE 启动,也可以打包成 Jar 包后命令启动,这里直接使用一键启动</p><p>访问自己电脑的 IP 地址或者使用<code>localhost</code>都可,访问 Spring Boot 设置的后端端口,能看到前端页面和请求状态码200说明成功</p><p>在这种方式下,你只需要启动后端服务即可实现前后端部署</p><img src="../images/b42087a8/4.png" alt="4" style="zoom: 50%;" /><h1 id="使用-Nginx-反向代理实现前后端分离部署"><a href="#使用-Nginx-反向代理实现前后端分离部署" class="headerlink" title="使用 Nginx 反向代理实现前后端分离部署"></a>使用 Nginx 反向代理实现前后端分离部署</h1><p>这里使用我的大二课程作业给大家在Windows下演示使用 Nginx 反向代理实现前后端分离部署</p><h2 id="配置前端构建输出-1"><a href="#配置前端构建输出-1" class="headerlink" title="配置前端构建输出"></a>配置前端构建输出</h2><p>打开你的 Vue 项目,在终端中输入<code>npm run build</code>命令,直到看见<code>Build complete.</code>说明打包完成</p><p><img src="/../images/b42087a8/1.png" alt="1"></p><p>打包完成后你会在目录中看见一个<code>dist</code>文件夹,里面会有<code>index.html</code>文件和<code>css</code>、<code>img</code>、<code>js</code>等文件夹</p><p><img src="/../images/b42087a8/2.png" alt="2"></p><p>将<code>dist</code>整个文件夹复制到 Nginx 根目录下的<code>html</code>文件夹中,同时可以修改文件夹为你需要的名字,我这里就叫<code>maiba</code>好了</p><p><img src="/../images/b42087a8/5.png" alt="5"></p><h2 id="设置-Nginx-反向代理"><a href="#设置-Nginx-反向代理" class="headerlink" title="设置 Nginx 反向代理"></a>设置 Nginx 反向代理</h2><p>打开<code>conf</code>目录下的<code>nginx.conf</code>文件夹,修改配置,注意修改为自己<strong>对应的文件夹</strong>名和<strong>后端服务地址</strong></p><figure class="highlight properties"><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></pre></td><td class="code"><pre><span class="line"><span class="attr">worker_processes</span> <span class="string">1; # 定义工作进程数量</span></span><br><span class="line"></span><br><span class="line"><span class="attr">events</span> <span class="string">{</span></span><br><span class="line"> <span class="attr">worker_connections</span> <span class="string">1024; # 定义每个工作进程的最大连接数</span></span><br><span class="line"><span class="attr">}</span></span><br><span class="line"></span><br><span class="line"><span class="attr">http</span> <span class="string">{</span></span><br><span class="line"> <span class="attr">include</span> <span class="string">mime.types; # 包含文件类型定义</span></span><br><span class="line"> <span class="attr">default_type</span> <span class="string">application/octet-stream; # 默认文件类型</span></span><br><span class="line"> <span class="attr">sendfile</span> <span class="string">on; # 开启高效文件传输模式</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">keepalive_timeout</span> <span class="string">65; # 连接保持超时时间</span></span><br><span class="line"> <span class="attr">server</span> <span class="string">{</span></span><br><span class="line"> <span class="attr">listen</span> <span class="string">80; # 监听端口号</span></span><br><span class="line"> <span class="attr">server_name</span> <span class="string">localhost; # 服务器名称</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">location</span> <span class="string">/ {</span></span><br><span class="line"> <span class="attr">root</span> <span class="string">html/maiba; # 网站根目录</span></span><br><span class="line"> <span class="attr">index</span> <span class="string">index.html index.htm; # 默认首页文件</span></span><br><span class="line"> <span class="attr">}</span></span><br><span class="line"> <span class="attr">error_page</span> <span class="string">500 502 503 504 /50x.html; # 错误页面配置</span></span><br><span class="line"> <span class="attr">location</span> = <span class="string">/50x.html {</span></span><br><span class="line"> <span class="attr">root</span> <span class="string">html; # 错误页面根目录</span></span><br><span class="line"> <span class="attr">}</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">location</span> <span class="string">/api/ {</span></span><br><span class="line"> <span class="attr">proxy_pass</span> <span class="string">http://localhost:8081; # 后端服务地址</span></span><br><span class="line"> <span class="attr">proxy_set_header</span> <span class="string">Host $host; # 设置请求头Host</span></span><br><span class="line"> <span class="attr">proxy_set_header</span> <span class="string">X-Real-IP $remote_addr; # 设置请求头X-Real-IP</span></span><br><span class="line"> <span class="attr">proxy_set_header</span> <span class="string">X-Forwarded-For $proxy_add_x_forwarded_for; # 设置请求头X-Forwarded-For</span></span><br><span class="line"> <span class="attr">proxy_set_header</span> <span class="string">X-Forwarded-Proto $scheme; # 设置请求头X-Forwarded-Proto</span></span><br><span class="line"> <span class="attr">}</span></span><br><span class="line"> <span class="attr">}</span></span><br><span class="line"><span class="attr">}</span></span><br></pre></td></tr></table></figure><blockquote><p>在server中listen 80,表示服务器监听 HTTP 的 80端口,这是默认的 HTTP 端口。<br>在 location / 中,定义了网站的根目录 html/maiba 和默认首页文件 index.html,这是静态资源的主要入口。<br>而 location /api/ 通过 proxy_pass 把 API 请求转发到后端服务<code>http://localhost:8081</code>,实现前后端的通信。</p></blockquote><h2 id="部署与测试-1"><a href="#部署与测试-1" class="headerlink" title="部署与测试"></a>部署与测试</h2><p>项目的启动可以直接使用 IDE 启动,也可以打包成 Jar 包后命令启动,这里直接使用一键启动</p><p>访问自己电脑的 IP 地址或者使用<code>localhost</code>都可,由于 Nginx 中配置的是<code>80</code>端口,所以直接访问 IP 地址或者<code>localhost</code>即可,无需特意输入端口,能看到前端页面和请求状态码200说明成功</p><p>在这种方式下,你需要同时运行 Nginx 和后端项目</p><img src="../images/b42087a8/6.png" alt="6" style="zoom:50%;" /><h1 id="通过-Docker-实现前后端的容器化部署"><a href="#通过-Docker-实现前后端的容器化部署" class="headerlink" title="通过 Docker 实现前后端的容器化部署"></a>通过 Docker 实现前后端的容器化部署</h1><p>在传统的部署方式上,我们需要自己在服务器一个个安装并配置需要的环境,非常麻烦,主要核心体现在三点:<strong>命令太多了,记不住</strong>、<strong>软件安装包名字复杂,不知道去哪里找</strong>、<strong>安装和部署步骤复杂,容易出错</strong>。</p><p>而Docker就能很好的解决这个问题,即便你对Linux不熟悉,你也能轻松部署各种常见软件、Java项目。</p><p>如果你需要查看Docker的基础知识,可以访问<a href="http://junwei.site/junwei/bb88bfb7.html">后端开发入门Docker:从基础到实践 </a></p><p>如果你需要我使用的终端工具,可用访问<a href="http://junwei.site/junwei/ba0dd480.html">Tabby:一款出色的开源终端工具</a></p><p>这里使用我的大二课程作业给大家在 CentOS 7 下演示通过 Docker 实现前后端的容器化部署,同时开启<code>Nginx</code>、<code>MySQL</code>、<code>Redis</code>等服务</p><p>如果不使用 Redis 可以省略对应的步骤,并且注意在 Compose 文件中去掉 Redis 的配置</p><h2 id="Linux系统中创建目录"><a href="#Linux系统中创建目录" class="headerlink" title="Linux系统中创建目录"></a>Linux系统中创建目录</h2><p>在root目录下创建文件夹,用于本地目录挂载</p><ol><li>创建maiba文件夹(自定义名称,这里使用项目名),用于存放所有文件</li><li>在<code>maiba</code>文件夹中创建三个文件夹<code>nginx</code>、<code>mysql</code>、<code>redis</code></li></ol><img src="../images/b42087a8/7.png" alt="7" style="zoom:80%;" /><ol start="3"><li>其他文件等我们配置好,在后面的步骤再单独放入</li></ol><h2 id="前端构建"><a href="#前端构建" class="headerlink" title="前端构建"></a>前端构建</h2><p>打开你的 Vue 项目,在终端中输入<code>npm run build</code>命令,直到看见<code>Build complete.</code>说明打包完成</p><p><img src="/../images/b42087a8/1.png" alt="1"></p><p>打包完成后你会在目录中看见一个<code>dist</code>文件夹,里面会有<code>index.html</code>文件和<code>css</code>、<code>img</code>、<code>js</code>等文件夹</p><p><img src="/../images/b42087a8/2.png" alt="2"></p><p>将<code>dist</code>文件夹名改为自定义名称(这里使用 maiba ),放入创建好的<code>nginx</code>文件夹中的<code>html</code>文件夹中</p><p><img src="/../images/b42087a8/8.png" alt="image-20240805154044362"></p><h2 id="Nginx基本配置"><a href="#Nginx基本配置" class="headerlink" title="Nginx基本配置"></a>Nginx基本配置</h2><p>准备<code>nginx.conf</code>文件,设置与本地部署基本一致,但是需要注意:</p><ul><li><p><strong>网站根目录</strong>设置需要设置你的文件夹名,且路径是Docker容器中的路径(已挂载到本地<code>html</code>文件夹)</p></li><li><p><strong>服务器名称</strong>和<strong>后端的服务地址</strong>不再使用<code>IP</code>,而是使用Docker中自定义桥接网络的名称(这里使用<code>maiba</code>)</p></li></ul><figure class="highlight properties"><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></pre></td><td class="code"><pre><span class="line"><span class="attr">worker_processes</span> <span class="string">1; # 定义工作进程数量</span></span><br><span class="line"></span><br><span class="line"><span class="attr">events</span> <span class="string">{</span></span><br><span class="line"> <span class="attr">worker_connections</span> <span class="string">1024; # 定义每个工作进程的最大连接数</span></span><br><span class="line"><span class="attr">}</span></span><br><span class="line"></span><br><span class="line"><span class="attr">http</span> <span class="string">{</span></span><br><span class="line"> <span class="attr">include</span> <span class="string">mime.types; # 包含文件类型定义</span></span><br><span class="line"> <span class="attr">default_type</span> <span class="string">application/octet-stream; # 默认文件类型</span></span><br><span class="line"> <span class="attr">sendfile</span> <span class="string">on; # 开启高效文件传输模式</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">keepalive_timeout</span> <span class="string">65; # 连接保持超时时间</span></span><br><span class="line"> <span class="attr">server</span> <span class="string">{</span></span><br><span class="line"> <span class="attr">listen</span> <span class="string">80; # 监听端口号</span></span><br><span class="line"> <span class="attr">server_name</span> <span class="string">maiba; # 服务器名称</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">location</span> <span class="string">/ {</span></span><br><span class="line"> <span class="attr">root</span> <span class="string">/usr/share/nginx/html/maiba; # 网站根目录</span></span><br><span class="line"> <span class="attr">index</span> <span class="string">index.html index.htm; # 默认首页文件</span></span><br><span class="line"> <span class="attr">}</span></span><br><span class="line"> <span class="attr">error_page</span> <span class="string">500 502 503 504 /50x.html; # 错误页面配置</span></span><br><span class="line"> <span class="attr">location</span> = <span class="string">/50x.html {</span></span><br><span class="line"> <span class="attr">root</span> <span class="string">html; # 错误页面根目录</span></span><br><span class="line"> <span class="attr">}</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">location</span> <span class="string">/api/ {</span></span><br><span class="line"> <span class="attr">proxy_pass</span> <span class="string">http://maiba:8080; # 后端服务地址</span></span><br><span class="line"> <span class="attr">proxy_set_header</span> <span class="string">Host $host; # 设置请求头Host</span></span><br><span class="line"> <span class="attr">proxy_set_header</span> <span class="string">X-Real-IP $remote_addr; # 设置请求头X-Real-IP</span></span><br><span class="line"> <span class="attr">proxy_set_header</span> <span class="string">X-Forwarded-For $proxy_add_x_forwarded_for; # 设置请求头X-Forwarded-For</span></span><br><span class="line"> <span class="attr">proxy_set_header</span> <span class="string">X-Forwarded-Proto $scheme; # 设置请求头X-Forwarded-Proto</span></span><br><span class="line"> <span class="attr">}</span></span><br><span class="line"> <span class="attr">}</span></span><br><span class="line"><span class="attr">}</span></span><br></pre></td></tr></table></figure><p>将准备好的<code>nginx.conf</code>文件放入<code>nginx</code>文件夹,与<code>html</code>文件夹并列</p><p><img src="/../images/b42087a8/9.png" alt="9"></p><h2 id="后端打包"><a href="#后端打包" class="headerlink" title="后端打包"></a>后端打包</h2><ol><li>在项目<code>pom.xml</code>文件中修改配置,避免出现<code>no main manifest attribute</code>错误,找到<code><plugin></code>下的<code><configuration></code>标签</li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">configuration</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">mainClass</span>></span>com.xxx.SpringbootMaibaApplication<span class="tag"></<span class="name">mainClass</span>></span></span><br><span class="line"> <span class="comment"><!-- <skip>true</skip> --></span></span><br><span class="line"> <span class="comment"><!-- 将上面skip标签注释掉或删掉,添加如下includeSystemScope标签 --></span></span><br><span class="line"> <span class="tag"><<span class="name">includeSystemScope</span>></span>true<span class="tag"></<span class="name">includeSystemScope</span>></span></span><br><span class="line"><span class="tag"></<span class="name">configuration</span>></span></span><br></pre></td></tr></table></figure><ol start="2"><li>检查确保在<code>application.properties</code>或<code>application.yml</code>中关于MySQL和Redis的配置正确<ul><li>使用我们设置的自定义的桥接网络名(这里用maiba)代替<code>localhost</code></li><li>使用<code>redis</code>作为 redis 的 host (在后面的 Docker Compose 文件中,Redis 服务的名称是 <code>redis</code>)</li></ul></li></ol><p><img src="/../images/b42087a8/17.png" alt="17"></p><ol start="3"><li>在IDEA右侧的Maven工具栏中点击<code>package</code>打包,这时候会在<code>target</code>文件夹中看到一个<code>jar</code>包</li></ol><img src="../images/b42087a8/10.png" alt="10" style="zoom: 40%;" /><ol start="4"><li>复制<code>jar</code>包,修改成自定义名字(这里使用 maiba ),将其发送到Linux中的<code>maiba</code>根目录下</li></ol><img src="../images/b42087a8/11.png" alt="11" style="zoom: 60%;" /><h2 id="准备-Dockerfile"><a href="#准备-Dockerfile" class="headerlink" title="准备 Dockerfile"></a>准备 Dockerfile</h2><ol><li>准备<code>Dockerfile</code>文件,将其中的<code>jar</code>包名修改成自己的(这里为maiba)</li></ol><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 基础镜像</span></span><br><span class="line"><span class="attr">FROM</span> <span class="string">openjdk:11.0-jre-buster</span></span><br><span class="line"><span class="comment"># 设定时区</span></span><br><span class="line"><span class="attr">ENV</span> <span class="string">TZ=Asia/Shanghai</span></span><br><span class="line"><span class="attr">RUN</span> <span class="string">ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone</span></span><br><span class="line"><span class="comment"># 拷贝jar包</span></span><br><span class="line"><span class="attr">COPY</span> <span class="string">maiba.jar /app.jar</span></span><br><span class="line"><span class="comment"># 入口</span></span><br><span class="line"><span class="attr">ENTRYPOINT</span> <span class="string">["java", "-jar", "/app.jar"]</span></span><br></pre></td></tr></table></figure><ol start="2"><li>确保<code>Dockerfile</code>被放在你的项目根目录下(这里为maiba),与<code>jar</code>包要在一个目录,因为Dockerfile文件中使用了相对路径</li></ol><img src="../images/b42087a8/16.png" alt="16" style="zoom:67%;" /><h2 id="放入MySQL和Redis的初始化文件"><a href="#放入MySQL和Redis的初始化文件" class="headerlink" title="放入MySQL和Redis的初始化文件"></a>放入MySQL和Redis的初始化文件</h2><ol><li>在MySQL文件夹中创建两个文件夹<code>init</code>和<code>conf</code></li></ol><img src="../images/b42087a8/12.png" alt="12" style="zoom:67%;" /><ol start="2"><li>在<code>init</code>文件夹中放入你的<code>sql</code>文件,这样在MySQL容器创建的时候,你的数据库就创建好了</li><li>在<code>conf</code>文件夹中放入一个简单的<code>maiba.cnf</code>文件(名字自定义,这里叫 maiba ),简单设置一下字符集</li></ol><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">[client]</span></span><br><span class="line"><span class="attr">default_character_set</span>=<span class="string">utf8mb4</span></span><br><span class="line"><span class="attr">[mysql]</span></span><br><span class="line"><span class="attr">default_character_set</span>=<span class="string">utf8mb4</span></span><br><span class="line"><span class="attr">[mysqld]</span></span><br><span class="line"><span class="attr">character_set_server</span>=<span class="string">utf8mb4</span></span><br><span class="line"><span class="attr">collation_server</span>=<span class="string">utf8mb4_unicode_ci</span></span><br><span class="line"><span class="attr">init_connect</span>=<span class="string">'SET NAMES utf8mb4'</span></span><br></pre></td></tr></table></figure><ol start="4"><li>确保 mysql 文件夹中的两个文件夹中文件都正确放置</li></ol><div style="display: flex; justify-content: center; align-items: center;"> <img src="../images/b42087a8/14.png" width="400"/> <img src="../images/b42087a8/13.png" width="430"/></div><ol start="5"><li><p>在<code>redis</code>文件夹中放入准备好的<code>dump.rdb</code>文件和<code>redis.conf</code>文件</p><ul><li><p>前者为你需要初始化的的Redis数据,由Redis生成,找到复制过来即可</p></li><li><p>后者为redis的配置文件,注意需要设置 <code>daemonize</code> 为 <code>no</code>,否则会出现<code>redis</code>启动后又正常退出的问题</p></li></ul></li></ol><blockquote><p>在 Docker 容器中运行 Redis 时,不需要设置 <code>daemonize</code> 为 <code>yes</code>。原因是 Docker 容器本身就是一个单独的进程,如果 Redis 容器以守护进程模式运行,会导致容器立即退出。</p><ul><li><strong>守护进程模式</strong>:当 <code>daemonize yes</code> 时,Redis 会在后台运行,并且主进程会退出。而在 Docker 中,主进程的退出意味着容器的退出。</li><li><strong>前台运行</strong>:Docker 容器的最佳实践是让主进程在前台运行,以便 Docker 可以管理进程生命周期。</li></ul></blockquote><ol start="6"><li>确保redis文件夹中的两文件都正确放置</li></ol><img src="../images/b42087a8/15.png" alt="15" style="zoom:80%;" /><h2 id="创建-Docker-Compose-文件"><a href="#创建-Docker-Compose-文件" class="headerlink" title="创建 Docker Compose 文件"></a>创建 Docker Compose 文件</h2><ol><li>创建<code>docker-compose.yml</code>文件,输入下面内容</li></ol><figure class="highlight yaml"><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="attr">version:</span> <span class="string">"3.8"</span></span><br><span class="line"></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line"> <span class="attr">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">container_name:</span> <span class="string">mysql</span></span><br><span class="line"> <span class="attr">ports:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"3306:3306"</span></span><br><span class="line"> <span class="attr">environment:</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">MYSQL_ROOT_PASSWORD:</span> <span class="number">123456</span></span><br><span class="line"> <span class="attr">volumes:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"./mysql/conf:/etc/mysql/conf.d"</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"./mysql/data:/var/lib/mysql"</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"./mysql/init:/docker-entrypoint-initdb.d"</span></span><br><span class="line"> <span class="attr">networks:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">new</span></span><br><span class="line"> <span class="attr">maiba:</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><br><span class="line"> <span class="attr">dockerfile:</span> <span class="string">Dockerfile</span></span><br><span class="line"> <span class="attr">container_name:</span> <span class="string">maiba</span></span><br><span class="line"> <span class="attr">ports:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"8080:8080"</span></span><br><span class="line"> <span class="attr">networks:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">new</span></span><br><span class="line"> <span class="attr">depends_on:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">mysql</span></span><br><span class="line"> <span class="attr">nginx:</span></span><br><span class="line"> <span class="attr">image:</span> <span class="string">nginx</span></span><br><span class="line"> <span class="attr">container_name:</span> <span class="string">nginx</span></span><br><span class="line"> <span class="attr">ports:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"80:80"</span></span><br><span class="line"> <span class="attr">volumes:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"./nginx/nginx.conf:/etc/nginx/nginx.conf"</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"./nginx/html:/usr/share/nginx/html"</span></span><br><span class="line"> <span class="attr">depends_on:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">maiba</span></span><br><span class="line"> <span class="attr">networks:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">new</span></span><br><span class="line"> <span class="attr">redis:</span></span><br><span class="line"> <span class="attr">image:</span> <span class="string">redis</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">volumes:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">./redis/dump.rdb:/data/dump.rdb</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">./redis/redis.conf:/usr/local/etc/redis/redis.conf</span></span><br><span class="line"> <span class="attr">command:</span> [<span class="string">"redis-server"</span>, <span class="string">"/usr/local/etc/redis/redis.conf"</span>]</span><br><span class="line"> <span class="attr">ports:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"6379:6379"</span></span><br><span class="line"> <span class="attr">networks:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">new</span></span><br><span class="line"><span class="attr">networks:</span></span><br><span class="line"> <span class="attr">new:</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">maiba</span></span><br></pre></td></tr></table></figure><blockquote><p>我们四个容器都使用了自定义桥接网络<code>maiba</code>,所以在之前的Nginx配置文件和后端连接MySQL的配置中我们都使用了<code>maiba</code>代替</p><p>需要注意密码等设置要和自己在后端的Properties中配置的一致,端口也根据需要进行设置</p><p>并且我们都对容器挂载了本地目录,可以实现数据的永久保存</p></blockquote><ol start="2"><li>确保<code>docker-compose.yml</code>被放在你的项目根目录下(这里为maiba)</li></ol><p><img src="/../images/b42087a8/18.png" alt="18"></p><h2 id="构建和启动-Docker-容器"><a href="#构建和启动-Docker-容器" class="headerlink" title="构建和启动 Docker 容器"></a>构建和启动 Docker 容器</h2><p>前面的步骤做完,确保好文件都放置正确了,我们终于可以一键构建容器并运行了</p><ol><li>在终端中使用<code>cd ~</code>回到 root 目录,运行<code>cd maiba</code>进入项目文件夹,运行<code>docker compose up -d</code>,完成所有的容器创建</li></ol><ul><li>第一次运行,会根据 Dockerfile 文件生成 maiba 后端容器镜像(蓝色部分),其他镜像如果曾经<code>docker pull</code>过,则无需下载,省时</li><li>然后就会依次创建所有容器,并挂载到我们指定的目录,并加入相同的网络(与Nginx中配置的和后端配置的相同)</li></ul><p><img src="/../images/b42087a8/19.png" alt="19"></p><ol start="2"><li>执行<code>docker ps</code>查看到所有容器都启动成功并且显示端口,证明正常创建</li></ol><p><img src="/../images/b42087a8/20.png" alt="20"></p><ol start="3"><li>执行<code>docker network inspect maiba</code>,查看该网络,可以看到四个容器都添加到了同一网络中</li></ol><img src="../images/b42087a8/22.png" alt="22" style="zoom:60%;" /><blockquote><p>如果遇到了失败的情况,可以执行<code>docker logs container_name</code>查看具体错误并解决它即可</p></blockquote><h2 id="部署与测试-2"><a href="#部署与测试-2" class="headerlink" title="部署与测试"></a>部署与测试</h2><p>所有容器正常启动,在浏览器中输入Linux系统的 IP 地址或云服务器的地址访问,能看到前端页面和请求状态码200说明成功了(注意开放Nginx的端口)</p><p><img src="/../images/b42087a8/23.png" alt="23"></p><p>Navicat中也可以访问MySQL和Redis中的数据,说明数据根据<code>sql</code>和<code>rdb</code>文件也初始化成功(需开放服务器的MySQL和Redis端口,才能远程访问)</p><p><img src="/../images/b42087a8/21.png" alt="21"></p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>本文档详细介绍了三种前后端部署的方法:集成dist文件到后端、使用Nginx反向代理实现前后端分离部署,以及使用Docker容器化部署。每种方法都提供了具体的步骤和技术细节,帮助读者理解如何有效地部署应用程序。</p><p>对于集成dist文件到后端的方法,文档演示了如何在Vue项目中构建前端代码,并将构建产物集成到Spring Boot后端项目中,通过简单的后端配置就能实现前后端的集成部署。</p><p>使用Nginx反向代理的方式,则实现了前后端的分离部署。通过配置Nginx服务器来代理前端静态资源,并将API请求转发给后端服务,这种方式能够更好地解耦前后端,便于维护和扩展。</p><p>最后,文档深入讲解了如何利用Docker进行容器化部署。不仅涵盖了前端构建、Nginx配置,还详细说明了如何准备Dockerfile、配置MySQL和Redis的初始化文件,以及如何使用Docker Compose文件来编排多个服务容器,包括后端应用、数据库、缓存服务等,从而实现一键式的自动化部署流程。</p><p>通过这些方法,开发者可以根据项目的实际需求选择最合适的部署策略,无论是简单的集成部署还是复杂的多服务容器化部署都能应对自如。</p>]]></content>
<categories>
<category> 部署 </category>
</categories>
<tags>
<tag> 部署 </tag>
</tags>
</entry>
<entry>
<title>后端开发入门Docker:从基础到实践</title>
<link href="/junwei/bb88bfb7.html"/>
<url>/junwei/bb88bfb7.html</url>
<content type="html"><![CDATA[<h1 id="Docker"><a href="#Docker" class="headerlink" title="Docker"></a>Docker</h1><p><font color=red><strong>Docker</strong> 是一个开源的容器化平台,它使开发人员能够在隔离的环境中构建、打包和部署应用程序。通过使用 Docker,应用程序可以在任何环境中运行,而不必担心底层硬件或操作系统的差异。</font></p><p>虽然我们学习的主要是后端开发,但了解 Docker 及其相关工具可以帮助开发人员更好地理解 DevOps 流程,提高整体技术素养和职业竞争力。当然,我们不是运维,并不会全部都学到。</p><h1 id="Docker的好处"><a href="#Docker的好处" class="headerlink" title="Docker的好处"></a>Docker的好处</h1><p>Docker 是一种开源的容器化平台,它能够显著简化软件开发、部署和运行过程中的许多方面。</p><ol><li><strong>环境一致性</strong>:</li></ol><ul><li>Docker 容器提供了一致的运行环境,不管在开发、测试还是生产环境中,都能保证应用程序的运行一致性,减少了“在我机器上可以工作”的问题。</li></ul><ol start="2"><li><strong>依赖管理</strong>:</li></ol><ul><li>Docker 容器包含了应用程序运行所需的所有依赖项(例如库、工具和配置文件),使得依赖管理变得简单和可靠。</li></ul><ol start="3"><li><strong>快速部署</strong>:</li></ol><ul><li>通过使用 Docker 镜像,可以快速地在不同的环境中部署应用程序。镜像是可移植的,确保了快速启动和部署过程的一致性。</li></ul><ol start="4"><li><strong>资源隔离</strong>:</li></ol><ul><li>Docker 容器提供了轻量级的资源隔离机制,确保不同的应用程序或服务在独立的容器中运行,减少了资源冲突和相互影响。</li></ul><ol start="5"><li><strong>版本控制</strong>:</li></ol><ul><li>Docker 镜像可以像代码一样进行版本控制,方便管理和回滚到之前的版本。这有助于快速恢复和测试不同的应用版本。</li></ul><ol start="6"><li><strong>扩展性和可伸缩性</strong>:</li></ol><ul><li>使用 Docker,可以轻松地扩展应用程序。通过编排工具如 Docker Compose 或 Kubernetes,可以实现容器的自动扩展和管理。</li></ul><ol start="7"><li><strong>持续集成和持续部署(CI/CD)</strong>:</li></ol><ul><li>Docker 与 CI/CD 工具集成,可以实现自动化的构建、测试和部署流程,提升开发效率和代码质量。</li></ul><ol start="8"><li><strong>开发效率</strong>:</li></ol><ul><li>开发人员可以在本地快速搭建和测试完整的开发环境,而不需要担心环境配置问题,从而提高开发效率。</li></ul><h1 id="前置准备"><a href="#前置准备" class="headerlink" title="前置准备"></a>前置准备</h1><ul><li><p>首先你得有一台Linux操作系统,可以是虚拟机上的,也可以是云服务器上的,我在这里就简单使用我的虚拟机里面的CentOS 7。</p></li><li><p>其次你总得有个终端软件吧,用命令操作Linux系统,推荐我正在使用的Tabby,没有的可以看我另外一篇博客安装<a href="https://panda-l1.github.io/junwei/ba0dd480.html">Tabby:一款出色的开源终端工具 | Panda)</a></p></li><li><p>最后就是一些Linux命令的使用,不会再查就好咯</p></li><li><p>对了,还有一颗好学的心</p></li></ul><h1 id="Docker安装"><a href="#Docker安装" class="headerlink" title="Docker安装"></a>Docker安装</h1><h2 id="卸载旧版"><a href="#卸载旧版" class="headerlink" title="卸载旧版"></a>卸载旧版</h2><p>首先如果系统中已经存在旧的Docker,则先卸载:</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><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">yum remove docker \</span><br><span class="line"> docker-client \</span><br><span class="line"> docker-client-latest \</span><br><span class="line"> docker-common \</span><br><span class="line"> docker-latest \</span><br><span class="line"> docker-latest-logrotate \</span><br><span class="line"> docker-logrotate \</span><br><span class="line"> docker-engine \</span><br><span class="line"> docker-selinux </span><br></pre></td></tr></table></figure><h2 id="配置Docker的yum库"><a href="#配置Docker的yum库" class="headerlink" title="配置Docker的yum库"></a>配置Docker的yum库</h2><p>首先要安装一个yum工具</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"><span class="built_in">sudo</span> yum install -y yum-utils device-mapper-persistent-data lvm2</span><br></pre></td></tr></table></figure><p>安装成功后,执行命令,配置Docker的yum源(已更新为阿里云源):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo</span><br><span class="line"><span class="built_in">sudo</span> sed -i <span class="string">'s+download.docker.com+mirrors.aliyun.com/docker-ce+'</span> /etc/yum.repos.d/docker-ce.repo</span><br></pre></td></tr></table></figure><p>更新yum,建立缓存</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"><span class="built_in">sudo</span> yum makecache fast</span><br></pre></td></tr></table></figure><h2 id="安装Docker"><a href="#安装Docker" class="headerlink" title="安装Docker"></a>安装Docker</h2><p>最后,执行命令,安装Docker,静待下载,当看到<code>Complete! </code>说明就安装好了</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin</span><br></pre></td></tr></table></figure><h2 id="启动和校验"><a href="#启动和校验" class="headerlink" title="启动和校验"></a>启动和校验</h2><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 启动Docker</span></span><br><span class="line">systemctl start docker</span><br><span class="line"></span><br><span class="line"><span class="comment"># 停止Docker</span></span><br><span class="line">systemctl stop docker</span><br><span class="line"></span><br><span class="line"><span class="comment"># 重启</span></span><br><span class="line">systemctl restart docker</span><br><span class="line"></span><br><span class="line"><span class="comment">#当我们真正部署了程序上去,我们更希望设置开机自启和容器自启</span></span><br><span class="line"><span class="comment"># Docker开机自启</span></span><br><span class="line">systemctl <span class="built_in">enable</span> docker</span><br><span class="line"><span class="comment"># Docker容器开机自启</span></span><br><span class="line">docker update --restart=always [容器名/容器<span class="built_in">id</span>]</span><br></pre></td></tr></table></figure><p>启动Docker后运行<code>systemctl status docker</code>看到active(running)证明启动成功</p><p><img src="/../images/docker/2.png" alt="2"></p><h2 id="配置镜像加速"><a href="#配置镜像加速" class="headerlink" title="配置镜像加速"></a>配置镜像加速</h2><p>这里以阿里云镜像加速为例。</p><h3 id="注册阿里云账号"><a href="#注册阿里云账号" class="headerlink" title="注册阿里云账号"></a>注册阿里云账号</h3><p>首先访问阿里云<a href="https://www.aliyun.com注册一个账号./">https://www.aliyun.com注册一个账号。</a></p><h3 id="开通镜像服务"><a href="#开通镜像服务" class="headerlink" title="开通镜像服务"></a>开通镜像服务</h3><p>在首页的产品中,找到阿里云的<strong>容器镜像服务</strong>:</p><p><img src="/../images/docker/3.png" alt="3"></p><p>点击后进入管理控制台:</p><p><img src="/../images/docker/4.png" alt="4"></p><h3 id="配置镜像加速-1"><a href="#配置镜像加速-1" class="headerlink" title="配置镜像加速"></a>配置镜像加速</h3><p>找到<strong>镜像工具</strong>下的<strong>镜像加速器</strong>,选择好对应的系统,<strong>直接复制下端命令去终端运行</strong></p><img src="../images/docker/5.png" alt="5" style="zoom:50%;" /><h1 id="Docker基础"><a href="#Docker基础" class="headerlink" title="Docker基础"></a>Docker基础</h1><p>安装成功后,接下来,我们一起来学习Docker使用的一些基础知识,为将来独立部署项目打下基础。</p><h2 id="查找配置"><a href="#查找配置" class="headerlink" title="查找配置"></a>查找配置</h2><p>在<a href="https://hub.docker.com/">Docker Hub Container Image Library | App Containerization</a>上可以查找想要安装的软件,可以看到很全的信息,包含了很多配置信息,要善于去Hub中查信息</p><p><img src="/../images/docker/16.png" alt="16"></p><p><img src="/../images/docker/20.png" alt="20"></p><h2 id="Docker命令"><a href="#Docker命令" class="headerlink" title="Docker命令"></a>Docker命令</h2><p>查阅命令可以到<a href="https://docs.docker.com/engine/reference/commandline/cli/">Use the Docker command line | Docker Docs</a>官网查询</p><h3 id="常见命令"><a href="#常见命令" class="headerlink" title="常见命令"></a>常见命令</h3><p>加粗的是我觉得比较常用的,方便回来查找</p><table><thead><tr><th align="center"><strong>命令</strong></th><th align="center"><strong>说明</strong></th><th align="center"><strong>文档地址</strong></th></tr></thead><tbody><tr><td align="center">docker pull</td><td align="center">拉取镜像</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/pull/">docker pull</a></td></tr><tr><td align="center">docker push</td><td align="center">推送镜像到DockerRegistry</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/push/">docker push</a></td></tr><tr><td align="center"><strong>docker images</strong></td><td align="center">查看本地镜像</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/images/">docker images</a></td></tr><tr><td align="center"><strong>docker rmi</strong></td><td align="center">删除本地镜像</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/rmi/">docker rmi</a></td></tr><tr><td align="center"><strong>docker run</strong></td><td align="center">创建<strong>并</strong>运行容器(不能重复创建)</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/run/">docker run</a></td></tr><tr><td align="center"><strong>docker stop</strong></td><td align="center">停止指定容器</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/stop/">docker stop</a></td></tr><tr><td align="center"><strong>docker start</strong></td><td align="center">启动指定容器</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/start/">docker start</a></td></tr><tr><td align="center">docker restart</td><td align="center">重新启动容器</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/restart/">docker restart</a></td></tr><tr><td align="center"><strong>docker rm</strong></td><td align="center">删除指定容器</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/rm/">docs.docker.com</a></td></tr><tr><td align="center"><strong>docker ps</strong></td><td align="center">查看容器</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/ps/">docker ps</a></td></tr><tr><td align="center"><strong>docker logs</strong></td><td align="center">查看容器运行日志</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/logs/">docker logs</a></td></tr><tr><td align="center">docker exec</td><td align="center">进入容器</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/exec/">docker exec</a></td></tr><tr><td align="center">docker save</td><td align="center">保存镜像到本地压缩文件</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/save/">docker save</a></td></tr><tr><td align="center"><strong>docker load</strong></td><td align="center">加载本地压缩文件到镜像</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/load/">docker load</a></td></tr><tr><td align="center"><strong>docker inspect</strong></td><td align="center">查看容器详细信息</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/inspect/">docker inspect</a></td></tr></tbody></table><p>用图来说明他们之间的关系,别担心,我们会慢慢学习</p><blockquote><p>特别注意:通过镜像来创建容器</p><ul><li>镜像:英文是image</li><li>容器:英文是container</li><li>仓库:当创建容器的时候,本地没有想要的镜像,就会到镜像仓库中下载。像Maven一样,DockerHub网站是官方仓库,阿里云、华为云会提供一些第三方仓库,我们也可以自己搭建私有的镜像仓库。</li></ul></blockquote><p><img src="/../images/docker/1.png" alt="1"></p><h3 id="命令演示与快速上手"><a href="#命令演示与快速上手" class="headerlink" title="命令演示与快速上手"></a>命令演示与快速上手</h3><p>我们简单地通过Docker来安装一个MySQL,来体验Docker安装MySQL的快捷</p><ol><li>使用<code>docker pull</code>拉取MySQL镜像(目前本地是没有的,所以需要从云仓库中拉取)</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull mysql</span><br></pre></td></tr></table></figure><p><img src="/../images/docker/6.png" alt="6"></p><p>因为我们没有指定版本,默认是拉取latest版本,如果需要版本可以在名字后加上<code>:[tag]</code>,<code>tag</code>为版本号</p><ol start="2"><li>使用<code>docker images</code>查看本地有的镜像,可以看到有刚才拉取的镜像</li></ol><p><img src="/../images/docker/7.png" alt="7"></p><ol start="3"><li>使用<code>docker run</code>命令创建容器。注意:没有<code>pull</code>拉取镜像也可运行该命令,会自动下载镜像(在第一步我们手动<code>pull</code>了,所以会简短创建容器的时间,不到一秒即可创建成功)</li></ol><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></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line"> --name mysql \</span><br><span class="line"> -p 3306:3306 \</span><br><span class="line"> -e TZ=Asia/Shanghai \</span><br><span class="line"> -e MYSQL_ROOT_PASSWORD=123456 \</span><br><span class="line"> mysql</span><br></pre></td></tr></table></figure><blockquote><p>解读:</p><ul><li><code>docker run -d</code> :创建并运行一个容器,<code>-d</code>则是让容器以后台进程运行</li><li><code>--name mysql </code> : 给容器起个名字叫<code>mysql</code>,可以自定义</li><li><code>-p 3306:3306</code> : 设置端口映射。<ul><li><strong>容器是隔离环境</strong>,外界不可访问。但是可以<strong>将宿主机端口</strong>映射<strong>容器内的端口</strong>,当访问宿主机指定端口时,就是在访问容器内的端口了。</li><li>容器内端口往往是由容器内的进程决定,例如MySQL进程默认端口是3306,因此容器内端口一定是3306;而宿主机端口则可以任意指定,一般与容器内保持一致。</li><li>格式: <code>-p 宿主机端口:容器内端口</code>,示例中就是将宿主机的3306映射到容器内的3306端口</li></ul></li><li><code>-e</code> TZ=Asia/Shanghai` : 配置容器内进程运行时的一些参数<ul><li>格式:<code>-e KEY=VALUE</code>,KEY和VALUE都由容器内进程决定</li><li>案例中,<code>TZ=Asia/Shanghai</code>是设置时区;<code>MYSQL_ROOT_PASSWORD=123456</code>是设置MySQL默认密码</li></ul></li><li><code>mysql</code> : 设置<strong>镜像</strong>名称,Docker会根据这个名字搜索并下载镜像<strong>(不可自定义,必须是厂商起的名字)</strong><ul><li>格式:<code>REPOSITORY:TAG</code>,例如<code>mysql:8.0</code>,其中<code>REPOSITORY</code>可以理解为镜像名,<code>TAG</code>是版本号</li><li>在未指定<code>TAG</code>的情况下,默认是最新版本,也就是<code>mysql:latest</code></li></ul></li></ul></blockquote><p><img src="/../images/docker/8.png" alt="8"></p><p>返回的<code>67d27537ef58131225d158465ed90da0f054c4c44bd8e824a3e5cc8c78f71c65</code>用于唯一标识该容器</p><ol start="4"><li><p><code>docker create</code>与<code>docker run</code>相同,只不过<code>create</code>后不会帮你启动,<code>run</code>相当于<code>create+run</code>,</p></li><li><p><code>docker ps</code>查看正在运行的容器,<code>docker ps -a</code>查看所有容器</p></li></ol><p><img src="/../images/docker/9.png" alt="9"></p><ul><li>CONTAINER ID:容器id</li><li>IMAGE:创建容器使用的镜像</li><li>COMMAND:表示容器最后运行的命令</li><li>CREATED:创建时间</li><li>STATUS:容器的状态。可能是启动时间也可能是关闭时间</li><li>PORTS:容器对外开放的端口</li><li>NAMES:容器的名字,自定义的那个</li></ul><p>这个时候MySQL已经安装成功,并且按照我们设定的配置好了,所以打开Navicat,我们即可连接上MySQL,是不是比传统方式安装快了很多,只需要一个<code>docker run</code>。</p><blockquote><p>如果连接不上可以查看端口3306是否开放或者3306端口是否被占用(如果你原装了MySQL并且正在运行)</p><p><code>firewall-cmd --zone=public --add-port=3306/tcp --permanent</code>可以开放3306端口,然后使用<code>systemctl restart firewalld</code>重启防火墙即可</p></blockquote><img src="../images/docker/10.png" alt="10" style="zoom:40%;" /><ol start="6"><li><code>docker inspect</code>查看容器详细信息,会出现很多信息,包括配置参数、网络设置、挂载卷、环境变量等</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker inspect mysql</span><br></pre></td></tr></table></figure><p><img src="/../images/docker/11.png" alt="11"></p><ol start="7"><li><code>docker exec</code>用于进入容器内部执行操作,例如以下命令,可以直接到容器内部并且连接MySQL</li></ol><ul><li><code>-it</code>参数用于在容器中启动一个交互式终端</li></ul><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker exec -it mysql mysql -uroot -p</span><br></pre></td></tr></table></figure><p><img src="/../images/docker/12.png" alt="12"></p><ol start="8"><li><code>docker rm</code>删除容器,容器正在运行不可删除,这时我们可以先<code>docker stop</code>该容器或者force remove</li></ol><p><img src="/../images/docker/13.png" alt="13"></p><ol start="9"><li><code>docker rm -f</code>强制删除容器,会返回容器名字代表删除成功,使用<code>docker ps</code>查看容器为空</li></ol><p><img src="/../images/docker/14.png" alt="14"></p><ol start="10"><li><code>docker rmi</code>删除镜像(不是容器,多了i表示images),使用<code>docker images</code>为空</li></ol><p><img src="/../images/docker/15.png" alt="15"></p><ol start="11"><li>加载和保存自己本地的镜像包</li></ol><ul><li>通过SFTP上传<code>tar</code>压缩包到虚拟机</li></ul><p><img src="/../images/docker/17.png" alt="17"></p><ul><li>使用<code>docker load -i file_name</code>命令即可加载镜像</li></ul><p><img src="/../images/docker/18.png" alt="18"></p><ul><li>使用<code>docker save -o file_name images_name</code>保存本地镜像为tar包</li></ul><p><img src="/../images/docker/19.png" alt="19"></p><p>到这里你就已经体验完了Docker的快速入门,只需要记好命令即可</p><h2 id="数据卷"><a href="#数据卷" class="headerlink" title="数据卷"></a>数据卷</h2><p>容器是隔离环境,容器内程序的文件、配置、运行时产生的容器都在容器内部,我们要读写容器内的文件非常不方便。因此,容器提供程序的运行环境,但是<strong>程序运行产生的数据、程序运行依赖的配置都应该与容器解耦</strong>。</p><h3 id="什么是数据卷"><a href="#什么是数据卷" class="headerlink" title="什么是数据卷"></a>什么是数据卷</h3><p>简单来说<strong>数据卷(volume)</strong>是一个虚拟目录,是<strong>容器内目录</strong>与<strong>宿主机目录</strong>之间映射的桥梁,通过这个桥梁,把容器和宿主机的文件互通,这样放在我们Linux系统上面的文件,能被容器内部找到,本质上是同个文件,主机和容器之间的文件是共享的。</p><p>使用Docker卷时,Docker会在宿主机上创建一个指定目录,并将其挂载到容器内。这种情况下,容器和主机之间的数据是共享的,修改一个地方的文件会在另一个地方反映出来,这样我们就可以只操作系统的文件而不用去到容器内部操作。</p><h3 id="数据卷命令"><a href="#数据卷命令" class="headerlink" title="数据卷命令"></a>数据卷命令</h3><p>数据卷的相关命令有:</p><table><thead><tr><th align="center"><strong>命令</strong></th><th align="center"><strong>说明</strong></th><th align="center"><strong>文档地址</strong></th></tr></thead><tbody><tr><td align="center">docker volume create</td><td align="center">创建数据卷</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/volume_create/">docker volume create</a></td></tr><tr><td align="center">docker volume ls</td><td align="center">查看所有数据卷</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/volume_ls/">docs.docker.com</a></td></tr><tr><td align="center">docker volume rm</td><td align="center">删除指定数据卷</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/volume_prune/">docs.docker.com</a></td></tr><tr><td align="center">docker volume inspect</td><td align="center">查看某个数据卷的详情</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/volume_inspect/">docs.docker.com</a></td></tr><tr><td align="center">docker volume prune</td><td align="center">清除数据卷</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/volume_prune/">docker volume prune</a></td></tr></tbody></table><p>注意:容器与数据卷的挂载要<strong>在创建容器时</strong>配置,对于创建好的容器,是不能设置数据卷的。而且<strong>创建容器的过程中,数据卷会自动创建</strong>。</p><p>通过<code>docker inspect</code>查看MySQL容器的信息</p><p><img src="/../images/docker/21.png" alt="21"></p><p>可以发现,其中有几个关键属性:</p><ul><li>Name:数据卷名称。由于定义容器未设置容器名,这里的就是匿名卷自动生成的名字,一串hash值。</li><li>Source:宿主机目录</li><li>Destination : 容器内的目录</li></ul><p>因为我们没有显示地定义数据卷,所以他是一个匿名数据卷。</p><h3 id="挂载到数据卷(Docker管理)"><a href="#挂载到数据卷(Docker管理)" class="headerlink" title="挂载到数据卷(Docker管理)"></a>挂载到数据卷(Docker管理)</h3><p>通过<code>-v 数据卷名:容器中的目录</code>在创建容器时实现挂载</p><p>到Hub上查看想要挂载的目录在容器中的位置</p><img src="../images/docker/22.png" alt="22" style="zoom: 67%;" /><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><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">1.首先创建容器并指定数据卷,注意通过 -v 参数来指定数据卷</span></span><br><span class="line">docker run -d --name nginx -p 80:80 -v html:/usr/share/nginx/html nginx</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">2.然后查看数据卷</span></span><br><span class="line">docker volume ls</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">3.查看数据卷详情</span></span><br><span class="line">docker volume inspect html</span><br></pre></td></tr></table></figure><p><img src="/../images/docker/23.png" alt="23"></p><p><code>/var/lib/docker/volumes</code>这个目录就是默认的存放所有容器数据卷的目录,其下再根据数据卷名称创建新目录,格式为<code>/数据卷名/_data</code>。所以挂载到的目录为<code>/var/lib/docker/volumes/html/_data</code>。这种是把目录挂载到数据卷上。</p><h3 id="挂载本地目录(用户管理)"><a href="#挂载本地目录(用户管理)" class="headerlink" title="挂载本地目录(用户管理)"></a>挂载本地目录(用户管理)</h3><p>正常情况下,我们可能需要明确<strong>指定将宿主机的一个目录挂载到容器中</strong>,比如Nginx的html文件夹</p><p>注意:每一个不同的镜像,将来创建容器后内部有哪些目录可以挂载,可以参考DockerHub对应的页面</p><p>例如Nginx的html在容器中的目录路径为<code>/usr/share/nginx/html</code>,我们希望他挂载到我们本地的<code>./nginx/html</code>文件夹而不是默认创建的<code>/var/lib/docker/volumes/html/_data</code></p><p>那么我们在创建容器的时候就可以直接指定<code>-v 本地目录:容器中目录</code>或<code>-v 本地文件:容器内文件</code></p><p><strong>注意</strong>:本地目录或文件必须以 <code>/</code> 或 <code>./</code>开头,如果直接以名字开头,会被识别为数据卷名而非本地目录名。</p><p>这个时候我们<strong>由于挂载的是本地目录</strong>,所以我们<strong>要自己准备好</strong>html这个本地文件夹中的内容,保证容器能够访问到里面的页面</p><p>在index.html中我加多了一行<code><h2>My name is Pandali!</h2></code></p><img src="../images/docker/26.png" alt="26" style="zoom:50%;" /><img src="../images/docker/24.png" alt="24" style="zoom:67%;" /><p>我们手动上传文件,然后运行命令</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -d --name nginx -p 80:80 -v ./nginx/html:/usr/share/nginx/html nginx</span><br></pre></td></tr></table></figure><p>在浏览器中访问nginx,可以看到我们传上去的自定义后的html</p><p><img src="/../images/docker/25.png" alt="25"></p><p>挂载到本地目录的方法适合我们需要根据自己的需要来自定义初始化,或者配置参数,而不是使用厂商自带的初始化和配置</p><hr><p>又或者我们使用MySQL来挂载本地</p><p>我们提前将conf文件和我们想要的sql文件复制到Linux系统当中</p><p><img src="/../images/docker/27.png" alt="27"></p><p>运行以下命令,具体的目录绑定可以到Hub中查询,完成三个目录的绑定</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><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line"> --name mysql \</span><br><span class="line"> -p 3306:3306 \</span><br><span class="line"> -e TZ=Asia/Shanghai \</span><br><span class="line"> -e MYSQL_ROOT_PASSWORD=123456 \</span><br><span class="line"> -v ./mysql/data:/var/lib/mysql \</span><br><span class="line"> -v ./mysql/conf:/etc/mysql/conf.d \</span><br><span class="line"> -v ./mysql/init:/docker-entrypoint-initdb.d \</span><br><span class="line"> mysql</span><br></pre></td></tr></table></figure><p>运行之后可以才看到新创建的MySQL以及包含了我们想要的数据库文件</p><p><img src="/../images/docker/28.png" alt="28"></p><p>这样只要本地的目录不被删除,无论MySQL升级还是被重装,只要绑定了这个目录,数据就一直存在,我们也可以这样对其进行初始化</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><h4 id="挂载数据卷"><a href="#挂载数据卷" class="headerlink" title="挂载数据卷"></a>挂载数据卷</h4><p>当你挂载一个Docker数据卷时,Docker会在主机上创建一个目录用于存储数据卷的内容。如果你在运行容器时挂载了一个数据卷,那么你对这个卷<strong>所做的更改会持久化</strong>,并且<strong>这个卷可以在不同的容器之间共享</strong>。</p><h4 id="挂载本地目录"><a href="#挂载本地目录" class="headerlink" title="挂载本地目录"></a>挂载本地目录</h4><p>当你挂载本地的一个目录到容器中时,你<strong>需要在本地目录中提前准备好文件</strong>,这样容器启动时就可以访问这些文件。<strong>否则,容器启动时会发现挂载目录为空。</strong></p><h4 id="区别总结"><a href="#区别总结" class="headerlink" title="区别总结"></a>区别总结</h4><ul><li><strong>数据卷</strong>:由Docker管理,用于持久化和共享数据。在创建数据卷时,如果数据卷是空的,容器中的初始内容不会自动复制到卷中。需要在容器运行时或之后在容器内部添加内容。</li><li><strong>本地目录挂载</strong>:由用户管理,需要用户提前准备好内容。容器会使用本地目录中的文件和目录,任何修改会反映在本地目录中。</li></ul><h2 id="网络"><a href="#网络" class="headerlink" title="网络"></a>网络</h2><p>在学校,我们连接了校园网的电脑可以互通,处在一个局域网中,Docker也很类似</p><p>当我们部署了后端服务,想要访问MySQL,就必须使他们的容器处在同一个网关下。</p><h3 id="常见命令-1"><a href="#常见命令-1" class="headerlink" title="常见命令"></a>常见命令</h3><table><thead><tr><th align="center"><strong>命令</strong></th><th align="center"><strong>说明</strong></th><th align="center"><strong>文档地址</strong></th></tr></thead><tbody><tr><td align="center">docker network create</td><td align="center">创建一个网络</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/network_create/">docker network create</a></td></tr><tr><td align="center">docker network ls</td><td align="center">查看所有网络</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/network_ls/">docs.docker.com</a></td></tr><tr><td align="center">docker network rm</td><td align="center">删除指定网络</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/network_rm/">docs.docker.com</a></td></tr><tr><td align="center">docker network prune</td><td align="center">清除未使用的网络</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/network_prune/">docs.docker.com</a></td></tr><tr><td align="center">docker network connect</td><td align="center">使指定容器连接加入某网络</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/network_connect/">docs.docker.com</a></td></tr><tr><td align="center">docker network disconnect</td><td align="center">使指定容器连接离开某网络</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/network_disconnect/">docker network disconnect</a></td></tr><tr><td align="center">docker network inspect</td><td align="center">查看网络详细信息</td><td align="center"><a href="https://docs.docker.com/engine/reference/commandline/network_inspect/">docker network inspect</a></td></tr></tbody></table><h3 id="两种常见的网络模式"><a href="#两种常见的网络模式" class="headerlink" title="两种常见的网络模式"></a>两种常见的网络模式</h3><p>除了<strong>默认网络桥接</strong>和<strong>用户自定义桥接网络</strong>,还有主机网络 (Host Network)、容器网络 (Container Network)、覆盖网络 (Overlay Network)等等,读者可以根据自己需要进行学习</p><p>通过<code>docker network ls</code>可以查看docker默认已有的网络</p><p><img src="/../images/docker/29.png" alt="29"></p><ul><li><strong>bridge 网络</strong>:默认创建的网络类型,当你不指定网络类型时,Docker 容器会自动连接到 bridge 网络。</li><li><strong>host 网络</strong>:在这种模式下,容器不会有独立的网络命名空间,直接使用宿主机的网络堆栈。</li><li><strong>none 网络</strong>:这种模式下,容器没有任何网络连接,相当于禁用了网络功能。</li></ul><h4 id="默认网络桥接-Bridge-Network"><a href="#默认网络桥接-Bridge-Network" class="headerlink" title="默认网络桥接 (Bridge Network)"></a>默认网络桥接 (Bridge Network)</h4><ul><li>默认情况下,当 Docker 守护进程启动时,会创建一个名为 <code>bridge</code> 的默认网络。这种网络允许同一主机上的所有容器通过 IP 地址互相通信。</li><li>可以通过 <code>--network bridge</code> 参数在启动容器时连接到这个默认桥接网络。</li></ul><p>我们通过<code>docker inspect mysql</code>和<code>docker inspect nginx</code>来查看他们现在的网络,我们在创建容器时并没有设定,所以是默认</p><p>他们处于bridge默认网络中,有着共同的网关172.17.0.1,可以进行容器间的访问</p><div style="display: flex; justify-content: center; align-items: center;"> <img src="../images/docker/30.png" width="500"/> <img src="../images/docker/31.png" width="500"/></div><h4 id="用户定义的桥接网络-User-Defined-Bridge-Network"><a href="#用户定义的桥接网络-User-Defined-Bridge-Network" class="headerlink" title="用户定义的桥接网络 (User-Defined Bridge Network)"></a>用户定义的桥接网络 (User-Defined Bridge Network)</h4><ul><li>用户可以创建自定义桥接网络,通过 <code>docker network create <network-name></code> 命令来实现。</li><li>在这个自定义网络中,容器<strong>可以通过容器名称进行互相通信</strong>,而不仅仅是通过 IP 地址。</li></ul><p>我们可以通过<code>-l</code>来对加入网络的容器起别名,方便访问(不起别名默认为容器名)</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><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 class="meta prompt_"># </span><span class="language-bash">创建一个junwei网络</span></span><br><span class="line">docker network create junwei</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">将mysql接入该网络,并起别名sql</span></span><br><span class="line">docker network connect junwei mysql --alias sql</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">将nginx也接入网络</span></span><br><span class="line">docker network connect junwei nginx</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">查看junwei网络</span></span><br><span class="line">docker inspect junwei</span><br></pre></td></tr></table></figure><p>可以看到两个容器处于同个网络当中</p><p><img src="/../images/docker/32.png" alt="32"></p><h2 id="镜像"><a href="#镜像" class="headerlink" title="镜像"></a>镜像</h2><p>镜像就是包含了应用程序、程序运行的系统函数库、运行配置等文件的文件包。构建镜像的过程是把他们打包起来。</p><p>镜像之所以能让我们快速跨操作系统部署应用而忽略其运行环境、配置,就是因为镜像中包含了程序运行需要的系统函数库、环境、配置、依赖。</p><p><img src="/../images/docker/33.png" alt="33"></p><p><strong>镜像就是一堆文件的集合</strong>。</p><p>但是,镜像文件不是随意堆放的,而是按照操作的步骤分层叠加而成,每一层形成的文件都会单独打包并标记一个唯一id,称为<strong>Layer</strong>(<strong>层</strong>)。这样,如果我们构建时用到的某些层其他人已经制作过,就可以直接拷贝使用这些层,而不用重复制作。</p><img src="../images/docker/34.png" alt="34" style="zoom:50%;" /><h3 id="Dockerfile"><a href="#Dockerfile" class="headerlink" title="Dockerfile"></a>Dockerfile</h3><p>由于制作镜像的过程中,需要逐层处理和打包,比较复杂,所以Docker就提供了自动打包镜像的功能。我们只需要将打包的过程,每一层要做的事情用固定的语法写下来,交给Docker去执行即可。</p><p>而这种记录镜像结构的文件就称为<strong>Dockerfile</strong>,其对应的语法可以参考官方文档:</p><p><a href="https://docs.docker.com/engine/reference/builder/">https://docs.docker.com/engine/reference/builder/</a></p><p>其中的语法比较多,比较常用的有:</p><table><thead><tr><th align="center"><strong>指令</strong></th><th align="center"><strong>说明</strong></th><th align="center"><strong>示例</strong></th></tr></thead><tbody><tr><td align="center"><strong>FROM</strong></td><td align="center">指定基础镜像</td><td align="center"><code>FROM centos:6</code></td></tr><tr><td align="center"><strong>ENV</strong></td><td align="center">设置环境变量,可在后面指令使用</td><td align="center"><code>ENV key value</code></td></tr><tr><td align="center"><strong>COPY</strong></td><td align="center">拷贝本地文件到镜像的指定目录</td><td align="center"><code>COPY ./xx.jar /tmp/app.jar</code></td></tr><tr><td align="center"><strong>RUN</strong></td><td align="center">执行Linux的shell命令,一般是安装过程的命令</td><td align="center"><code>RUN yum install gcc</code></td></tr><tr><td align="center"><strong>EXPOSE</strong></td><td align="center">指定容器运行时监听的端口,是给镜像使用者看的</td><td align="center">EXPOSE 8080</td></tr><tr><td align="center"><strong>ENTRYPOINT</strong></td><td align="center">镜像中应用的启动命令,容器运行时调用</td><td align="center">ENTRYPOINT java -jar xx.jar</td></tr></tbody></table><p>在别人构建好的jdk镜像的基础上制作java镜像,就可以省去JDK的配置了,方便很多,下面就是一个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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 基础镜像</span></span><br><span class="line"><span class="keyword">FROM</span> openjdk:<span class="number">11.0</span>-jre-buster</span><br><span class="line"><span class="comment"># 设定时区</span></span><br><span class="line"><span class="keyword">ENV</span> TZ=Asia/Shanghai</span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">ln</span> -snf /usr/share/zoneinfo/<span class="variable">$TZ</span> /etc/localtime && <span class="built_in">echo</span> <span class="variable">$TZ</span> > /etc/timezone</span></span><br><span class="line"><span class="comment"># 拷贝jar包</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> your_jar_package_name.jar /app.jar</span></span><br><span class="line"><span class="comment"># 入口</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> [<span class="string">"java"</span>, <span class="string">"-jar"</span>, <span class="string">"/app.jar"</span>]</span></span><br></pre></td></tr></table></figure><h3 id="自定义镜像"><a href="#自定义镜像" class="headerlink" title="自定义镜像"></a>自定义镜像</h3><p>当Dockerfile文件(记住得叫这个名字,让Docker识别)写好以后,将Dockerfile文件和Jar包放在同个目录下,就可以利用命令来构建镜像了</p><p>执行命令,构建镜像:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入镜像目录</span></span><br><span class="line"><span class="built_in">cd</span> /root/demo</span><br><span class="line"><span class="comment"># 开始构建</span></span><br><span class="line">docker build -t your_jar_package_name:1.0 .</span><br></pre></td></tr></table></figure><p>命令说明:</p><ul><li><p><code>docker build </code>: 就是构建一个docker镜像</p></li><li><p><code>-t your_jar_package_name:1.0</code> :<code>-t</code>参数是指定镜像的名称(<code>repository</code>和<code>tag</code>)</p></li><li><p><code>.</code> : 最后的点是指构建时Dockerfile所在路径,由于我们进入了demo目录,所以指定的是<code>.</code>代表当前目录,也可以直接指定Dockerfile目录:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 直接指定Dockerfile目录</span></span><br><span class="line">docker build -t your_jar_package_name:1.0 /root/demo</span><br></pre></td></tr></table></figure></li></ul><p>这样我们就可以创建一个属于我们的镜像了,使用<code>docker run</code>即可将他启动起来</p><h2 id="DockerCompose"><a href="#DockerCompose" class="headerlink" title="DockerCompose"></a>DockerCompose</h2><p>如果我们要部署一个简单的java项目,其中包含3个容器:</p><ul><li>MySQL</li><li>Nginx</li><li>Java项目(Jar)</li></ul><p>而稍微复杂的项目,其中还会有各种各样的其它中间件,如Redis等,需要部署的东西远不止3个。如果还像之前那样手动的逐一部署,就太麻烦了。</p><p><strong>docker-compose.yml文件中可以定义多个相互关联的应用容器</strong>,每一个应用容器被称为一个服务(service)。由于service就是在定义某个应用的运行时参数,因此与<code>docker run</code>参数非常相似。</p><p>举例来说,用docker run部署MySQL的命令如下:</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></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line"> --name mysql \</span><br><span class="line"> -p 3306:3306 \</span><br><span class="line"> -e TZ=Asia/Shanghai \</span><br><span class="line"> -e MYSQL_ROOT_PASSWORD=123456 \</span><br><span class="line"> -v ./mysql/data:/var/lib/mysql \</span><br><span class="line"> -v ./mysql/conf:/etc/mysql/conf.d \</span><br><span class="line"> -v ./mysql/init:/docker-entrypoint-initdb.d \</span><br><span class="line"> --network junwei</span><br><span class="line"> mysql</span><br></pre></td></tr></table></figure><p>如果用<code>docker-compose.yml</code>文件来定义,就是这样:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">"3.8"</span></span><br><span class="line"></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line"> <span class="attr">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">container_name:</span> <span class="string">mysql</span></span><br><span class="line"> <span class="attr">ports:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"3306:3306"</span></span><br><span class="line"> <span class="attr">environment:</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">MYSQL_ROOT_PASSWORD:</span> <span class="number">123456</span></span><br><span class="line"> <span class="attr">volumes:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"./mysql/conf:/etc/mysql/conf.d"</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"./mysql/data:/var/lib/mysql"</span></span><br><span class="line"> <span class="attr">networks:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">new</span></span><br><span class="line"><span class="attr">networks:</span></span><br><span class="line"> <span class="attr">new:</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">junwei</span></span><br></pre></td></tr></table></figure><p>对比如下:</p><table><thead><tr><th align="center"><strong>docker run 参数</strong></th><th align="center"><strong>docker compose 指令</strong></th><th align="center"><strong>说明</strong></th></tr></thead><tbody><tr><td align="center">–name</td><td align="center">container_name</td><td align="center">容器名称</td></tr><tr><td align="center">-p</td><td align="center">ports</td><td align="center">端口映射</td></tr><tr><td align="center">-e</td><td align="center">environment</td><td align="center">环境变量</td></tr><tr><td align="center">-v</td><td align="center">volumes</td><td align="center">数据卷配置</td></tr><tr><td align="center">–network</td><td align="center">networks</td><td align="center">网络</td></tr></tbody></table><h3 id="基础命令"><a href="#基础命令" class="headerlink" title="基础命令"></a>基础命令</h3><p>编写好docker-compose.yml文件,就可以部署项目了。常见的命令:<a href="https://docs.docker.com/compose/reference/">https://docs.docker.com/compose/reference/</a></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">docker compose [OPTIONS] [COMMAND]</span><br></pre></td></tr></table></figure><p>其中,OPTIONS和COMMAND都是可选参数,比较常见的有:</p><table><thead><tr><th align="center"><strong>类型</strong></th><th align="center"><strong>参数或指令</strong></th><th align="center"><strong>说明</strong></th></tr></thead><tbody><tr><td align="center">Options</td><td align="center">-f</td><td align="center">指定compose文件的路径和名称</td></tr><tr><td align="center">-p</td><td align="center">指定project名称。project就是当前compose文件中设置的多个service的集合,是逻辑概念</td><td align="center"></td></tr><tr><td align="center">Commands</td><td align="center">up</td><td align="center">创建并启动所有service容器</td></tr><tr><td align="center">down</td><td align="center">停止并移除所有容器、网络</td><td align="center"></td></tr><tr><td align="center">ps</td><td align="center">列出所有启动的容器</td><td align="center"></td></tr><tr><td align="center">logs</td><td align="center">查看指定容器的日志</td><td align="center"></td></tr><tr><td align="center">stop</td><td align="center">停止容器</td><td align="center"></td></tr><tr><td align="center">start</td><td align="center">启动容器</td><td align="center"></td></tr><tr><td align="center">restart</td><td align="center">重启容器</td><td align="center"></td></tr><tr><td align="center">top</td><td align="center">查看运行的进程</td><td align="center"></td></tr><tr><td align="center">exec</td><td align="center">在指定的运行中容器中执行命令</td><td align="center"></td></tr></tbody></table><blockquote><p>利用docker-compose.yml实现多个容器的一键部署我们下一章单独讲</p></blockquote>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> Docker </tag>
</tags>
</entry>
<entry>
<title>Tabby:一款出色的开源终端工具</title>
<link href="/junwei/ba0dd480.html"/>
<url>/junwei/ba0dd480.html</url>
<content type="html"><![CDATA[<h1 id="Tabby"><a href="#Tabby" class="headerlink" title="Tabby"></a>Tabby</h1><p><strong>Tabby - a terminal for a more modern age</strong>,是一种现代化、轻量级的终端(terminal)应用程序。它旨在提供更好的用户体验和可扩展性,适用于开发者、系统管理员和其他需要频繁使用终端的用户,帮助他们更高效地管理和操作终端任务,支持Windows、macOS和Linux操作系统,支持文件快速传输……最重要的是,长在了年轻人的审美上面!</p><img src="../images/tools/tabby/1.png" alt="1" style="zoom:30%;" /><h1 id="安装Tabby"><a href="#安装Tabby" class="headerlink" title="安装Tabby"></a>安装Tabby</h1><p>直接打开<a href="https://tabby.sh/">Tabby</a>官网,点击「download」按钮就可以跳转到下载页面,Windows用户可以直接找到setup-x64.exe下载安装。</p><img src="../images/tools/tabby/2.png" alt="2" style="zoom:30%;" /><p>安装完成后你就拥有了一个炫酷的终端软件</p><img src="../images/tools/tabby/12.png" alt="12" style="zoom: 50%;" /><h1 id="配置Tabby"><a href="#配置Tabby" class="headerlink" title="配置Tabby"></a>配置Tabby</h1><p>用户可以根据自己的喜好自定义外观和行为,包括主题、配色方案、字体等</p><h2 id="语言设置"><a href="#语言设置" class="headerlink" title="语言设置"></a>语言设置</h2><p>打开Tabby,找到右上角齿轮图标,打开设置,在<code>Application settings</code>中的<code>Language</code>中选择中文即可</p><img src="../images/tools/tabby/3.png" alt="3" style="zoom:50%;" /><h2 id="配色方案"><a href="#配色方案" class="headerlink" title="配色方案"></a>配色方案</h2><p>在左侧配色方案可以选着自己想要的主题,<code>暗色</code> or <code>亮色</code>,有丰富的配色方案可供选择</p><div style="display: flex; justify-content: center; align-items: center;"> <img src="../images/tools/tabby/4.png" width="400"/> <img src="../images/tools/tabby/5.png" width="400"/></div><h2 id="插件下载"><a href="#插件下载" class="headerlink" title="插件下载"></a>插件下载</h2><p>左侧插件中提供了Tabby的插件下载</p><ul><li><a href="https://github.com/Eugeny/tabby-docker">docker</a> - 连接到 Docker 容器</li><li><a href="https://github.com/kbjr/terminus-title-control">title-control</a> - 允许通过提供要删除的前缀、后缀和/或字符串来修改终端选项卡的标题</li><li><a href="https://github.com/Domain/terminus-quick-cmds">quick-cmds</a> - 快速向一个或所有终端选项卡发送命令</li><li><a href="https://github.com/Eugeny/tabby-save-output">save-output</a> - 将终端输出记录到文件中</li></ul><p><img src="/../images/tools/tabby/6.png" alt="6"></p><h1 id="SSH连接"><a href="#SSH连接" class="headerlink" title="SSH连接"></a>SSH连接</h1><p>SSH连接是我们使用终端最常用的功能,可以方便我们操作Linux系统</p><ol><li><p>找到设置中左侧的配置和连接,新建,新配置,选择SSH连接为模板</p></li><li><p>输入名称,主机IP,用户名和密码,点击保存即可完成自定义配置</p></li></ol><img src="../images/tools/tabby/7.png" alt="7" style="zoom:50%;" /><ol start="3"><li>点击某个配置启动按钮,即可建立连接</li></ol><img src="../images/tools/tabby/8.png" alt="8" style="zoom:67%;" /><ol start="4"><li>这样你就拥有了一个好看的终端,可以操作你的Linux系统</li></ol><img src="../images/tools/tabby/9.png" alt="9" style="zoom:50%;" /><ol start="5"><li>当然,你也可以做到分屏效果,同时开多个终端,在标签处(名字)右键,点击拆分即可,同时操作更多</li></ol><img src="../images/tools/tabby/10.png" alt="10" style="zoom: 50%;" /><h1 id="SFTP文件传输"><a href="#SFTP文件传输" class="headerlink" title="SFTP文件传输"></a>SFTP文件传输</h1><p>在右上角的SFTP处可以打开SFTP窗口,可以<strong>创建文件夹</strong>和<strong>上传文件</strong>,也支持<strong>直接将文件拖拽入窗口</strong>实现上传(记得先去到目标文件夹哦!),双击某个文件可以对该文件进行下载或者鼠标右键点击本地编辑。</p><img src="../images/tools/tabby/11.png" alt="11" style="zoom:50%;" /><h1 id="cmd使用"><a href="#cmd使用" class="headerlink" title="cmd使用"></a>cmd使用</h1><p>在每次重新打开系统cmd的时候,没有历史记录,我都要不断地重复输入,很繁琐</p><p>并且某些时候你还需要去到指定的文件夹,这时设置cmd的工作目录,再将该配置保存起来,下次点击配置运行即可</p><img src="../images/tools/tabby/13.png" alt="13" style="zoom: 80%;" /><p>Tabby可以帮助我们记住历史命令,即使软件被关闭,可以一键➡补全,减去繁琐的输入</p><img src="../images/tools/tabby/14.png" alt="14" style="zoom: 70%;" /><p>功能与cmd完全一样,还有Power shell,Git bash等等</p><img src="../images/tools/tabby/15.png" alt="15" style="zoom: 67%;" />]]></content>
<categories>
<category> 工具 </category>
</categories>
<tags>
<tag> 工具 </tag>
</tags>
</entry>
<entry>
<title>Redis分布式实现:持久化和集群方案</title>
<link href="/junwei/af87aed8.html"/>
<url>/junwei/af87aed8.html</url>
<content type="html"><![CDATA[<h1 id="分布式缓存"><a href="#分布式缓存" class="headerlink" title="分布式缓存"></a>分布式缓存</h1><p>单机的Redis存在<strong>四大问题</strong>:</p><img src="../images/redis/redis3.1/1.png" alt="1" style="zoom:70%;" /><h1 id="Redis持久化"><a href="#Redis持久化" class="headerlink" title="Redis持久化"></a>Redis持久化</h1><p>Redis有<strong>两种持久化方案</strong>:</p><ul><li><strong>RDB</strong>持久化</li><li><strong>AOF</strong>持久化</li></ul><h2 id="RDB持久化"><a href="#RDB持久化" class="headerlink" title="RDB持久化"></a>RDB持久化</h2><p>RDB全称<strong>Redis Database Backup file</strong>(Redis数据备份文件),也被叫做Redis数据快照。</p><p><font color=red>简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,<strong>从磁盘读取快照文件恢复数据</strong>。快照文件称为RDB文件,默认是保存在当前运行目录。</font></p><h3 id="RDB执行时机"><a href="#RDB执行时机" class="headerlink" title="RDB执行时机"></a>RDB执行时机</h3><p>RDB持久化在<strong>以下四种情况下会执行</strong>:</p><ul><li><strong>执行save命令</strong></li><li><strong>执行bgsave命令</strong></li><li><strong>Redis停机时</strong></li><li><strong>触发RDB条件时</strong></li></ul><p><strong>1)save命令:会导致主进程执行RDB,这个过程中其它所有命令都会被阻塞。</strong></p><p>只有在数据迁移时可能用到,其他时候尽量不用,会阻塞所有其他操作,业务就会中断</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">save</span><br></pre></td></tr></table></figure><p><strong>2)bgsave命令:执行后会开启独立进程完成RDB,主进程可以持续处理用户请求,不受影响。</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bgsave</span><br></pre></td></tr></table></figure><p><strong>3)Redis手动关闭时:会先保存再关闭(强制关闭进程和宕机不会)</strong></p><p><strong>4)触发设定的RDB条件:在config文件中的配置</strong></p><figure class="highlight properties"><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="comment"># 如果是save "" 则表示禁用RDB</span></span><br><span class="line"><span class="comment"># 900秒内,如果至少有1个key被修改,则执行bgsave</span></span><br><span class="line"><span class="attr">save</span> <span class="string">900 1</span></span><br><span class="line"><span class="comment"># 300秒内,如果有10个key被修改,则执行bgsave</span></span><br><span class="line"><span class="attr">save</span> <span class="string">300 10</span></span><br><span class="line"><span class="comment"># 60秒内,如果有1万个key被修改,则执行bgsave</span></span><br><span class="line"><span class="attr">save</span> <span class="string">60 10000</span></span><br></pre></td></tr></table></figure><h3 id="RDB原理"><a href="#RDB原理" class="headerlink" title="RDB原理"></a>RDB原理</h3><p><font color=red><code>bgsave</code>开始时会<code>fork</code>主进程得到子进程,子进程通过复制页表指向相同的共享内存来共享主进程的内存数据。完成fork后读取内存数据并写入<code> RDB</code> 文件。</font></p><p>fork采用的是<code>copy-on-write</code>技术:</p><ul><li>当主进程执行读操作时,访问共享内存;</li><li>当主进程执行写操作时,则会<strong>拷贝一份数据作为副本</strong>,<strong>在副本上执行写操作</strong>。</li></ul><p><img src="/../images/redis/redis3.1/2.png" alt="2"></p><h3 id="RDB小结"><a href="#RDB小结" class="headerlink" title="RDB小结"></a>RDB小结</h3><p>RDB方式<code>bgsave</code>的基本流程?</p><ul><li>fork主进程得到一个子进程,共享内存空间</li><li>子进程读取内存数据并写入新的RDB文件</li><li>用新RDB文件替换旧的RDB文件</li></ul><p>RDB的缺点?</p><ul><li>RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险</li><li>fork子进程、压缩、写出RDB文件都比较耗时</li></ul><h2 id="AOF持久化"><a href="#AOF持久化" class="headerlink" title="AOF持久化"></a>AOF持久化</h2><p><font color=red>AOF全称为Append Only File(追加文件)。Redis处理的<strong>每一个写命令都会记录</strong>在AOF文件,可以看做是命令<strong>日志文件</strong>。</font></p><p><img src="/../images/redis/redis3.1/3.png" alt="3"></p><h3 id="AOF配置"><a href="#AOF配置" class="headerlink" title="AOF配置"></a>AOF配置</h3><p>AOF<strong>默认是关闭的</strong>,需要修改<code>redis.conf</code>配置文件来开启AOF:</p><figure class="highlight properties"><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="comment"># 是否开启AOF功能,默认是no</span></span><br><span class="line"><span class="attr">appendonly</span> <span class="string">yes</span></span><br><span class="line"><span class="comment"># AOF文件的名称</span></span><br><span class="line"><span class="attr">appendfilename</span> <span class="string">"appendonly.aof"</span></span><br></pre></td></tr></table></figure><p>AOF的命令记录的频率也可以通过redis.conf文件来配:</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 表示每执行一次写命令,立即记录到AOF文件</span></span><br><span class="line"><span class="attr">appendfsync</span> <span class="string">always </span></span><br><span class="line"><span class="comment"># 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案(常用)</span></span><br><span class="line"><span class="attr">appendfsync</span> <span class="string">everysec </span></span><br><span class="line"><span class="comment"># 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘</span></span><br><span class="line"><span class="attr">appendfsync</span> <span class="string">no</span></span><br></pre></td></tr></table></figure><p>三种策略对比</p><p><img src="/../images/redis/redis3.1/4.png" alt="4"></p><h3 id="AOF文件重写"><a href="#AOF文件重写" class="headerlink" title="AOF文件重写"></a>AOF文件重写</h3><p>因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。</p><p>通过执行<code>bgrewriteaof</code>命令,可以让AOF文件执行重写功能,Redis 会 fork 一个子进程来执行重写操作,在子进程重写 AOF 文件的过程中,主进程会继续接收和处理写操作,Redis 会维护一个重写缓冲区(rewrite buffer),将这些新的写操作也记录下来。</p><p>重写可以<strong>显著减少 AOF 文件的大小</strong>,因为新文件只包含当前数据库状态<strong>所需的最小操作集</strong>,而不是所有历史操作日志。这<strong>不仅降低了磁盘空间占用</strong>,还<strong>加快了 Redis 重启时的恢复速度</strong>。</p><h1 id="Redis主从复制"><a href="#Redis主从复制" class="headerlink" title="Redis主从复制"></a>Redis主从复制</h1><p>单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。</p><p><img src="/../images/redis/redis3.1/5.png" alt="5"></p><h2 id="主从数据同步原理"><a href="#主从数据同步原理" class="headerlink" title="主从数据同步原理"></a>主从数据同步原理</h2><p><font color=red>主从之间的数据是自动同步的,<strong>无需额外操作</strong></font></p><h3 id="全量同步"><a href="#全量同步" class="headerlink" title="全量同步"></a>全量同步</h3><p><font color=red>主从第一次建立连接时,会执行<strong>全量同步</strong>,将<code>master</code>节点的<strong>所有数据都拷贝</strong>给<code>slave</code>节点</font></p><p><img src="/../images/redis/redis3.1/6.png"></p><h4 id="如何确定master和slave的数据是否一致"><a href="#如何确定master和slave的数据是否一致" class="headerlink" title="如何确定master和slave的数据是否一致"></a>如何确定master和slave的数据是否一致</h4><ul><li><strong>Replication Id</strong>:简称replid,是数据集的标记,id一致则说明是同一数据集。<strong>每一个master都有唯一的replid,slave则会继承master节点的replid</strong></li><li><strong>offset</strong>:偏移量,随着记录在<code>repl_baklog</code>中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。<strong>如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。</strong></li></ul><p><code>slave</code>原本也是一个<code>master</code>,有自己的<code>replid</code>和<code>offset</code>:</p><blockquote><p>当第一次变成<code>slave</code>,与<code>master</code>建立连接时,发送的是自己的<code>replid</code>和<code>offset</code>。</p><p><code>master</code>判断发现slave发送来的replid与自己的不一致,说明这是一个全新的<code>slave</code>,就知道要做全量同步了。</p><p><code>master</code>会<strong>将自己的replid和offset都发送给这个slave</strong>,<code>slave</code>存储后保证<code>slave</code>的<code>replid</code>与<code>master</code>一致。</p></blockquote><p>因此,<strong>master判断一个节点是否是第一次同步的依据,就是看replid是否一致</strong>。</p><p><img src="/../images/redis/redis3.1/7.png" alt="7"></p><h4 id="完整流程"><a href="#完整流程" class="headerlink" title="完整流程"></a>完整流程</h4><ul><li><code>slave</code>节点请求增量同步</li><li><code>master</code>节点判断<code>replid</code>,发现不一致,拒绝增量同步</li><li><code>master</code>将完整内存数据生成RDB,发送<code>RDB</code>到<code>slave</code></li><li><code>slave</code>清空本地数据,加载<code>master</code>的RDB</li><li><code>master</code>将RDB期间的命令记录在<code>repl_baklog</code>,并持续将log中的命令发送给<code>slave</code></li><li><code>slave</code>执行接收到的命令,保持与<code>master</code>之间的同步</li></ul><h3 id="增量同步"><a href="#增量同步" class="headerlink" title="增量同步"></a>增量同步</h3><p><font color=red>只更新slave与master存在差异的部分数据</font></p><p>全量同步需要先做RDB,然后将RDB文件通过网络传输给<code>slave</code>,成本太高了。因此除了<strong>第一次</strong>做全量同步,其它大多数时候<code>slave</code>与<code>master</code>都是做<strong>增量同步</strong>。</p><p><img src="/../images/redis/redis3.1/8.png" alt="8"></p><h4 id="repl-backlog原理"><a href="#repl-backlog原理" class="headerlink" title="repl_backlog原理"></a>repl_backlog原理</h4><p><code>master</code>怎么知道<code>slave</code>与自己的数据差多少?靠全量同步时的<code>repl_baklog</code>文件记录。</p><p>这个文件是一个<strong>固定大小的数组</strong>,只不过数组是环形,也就是说<strong>角标到达数组末尾后,会再次从0开始读写</strong>,这样数组头部的数据就会被覆盖。</p><p>repl_baklog中会记录Redis处理过的命令日志及<code>offset</code>,<strong>包括master当前的offset,和slave已经拷贝到的offset</strong></p><img src="../images/redis/redis3.1/9.png" alt="9" style="zoom: 80%;" /><blockquote><p><code>slave</code>与<code>master</code>的offset之间的差异,就是<code>salve</code>需要<strong>增量拷贝</strong>的数据了。</p><p>如果<code>slave</code><strong>落后</strong><code>master</code><strong>一个环</strong>,就<strong>只能做全量同步</strong>,因为<code>repl_baklog</code>已经有部分数据是被覆盖了</p></blockquote><p><img src="/../images/redis/redis3.1/14.png" alt="14"></p><h2 id="主从同步优化"><a href="#主从同步优化" class="headerlink" title="主从同步优化"></a>主从同步优化</h2><p>主从同步可以保证主从数据的一致性,非常重要。</p><p>可以从以下几个方面来优化Redis主从就集群:</p><ul><li>在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。</li><li>Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO</li><li>适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步</li><li>限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力</li></ul><p><strong>主从从</strong>架构图:</p><p><img src="/../images/redis/redis3.1/15.png" alt="15"></p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><h3 id="全量同步和增量同步区别?"><a href="#全量同步和增量同步区别?" class="headerlink" title="全量同步和增量同步区别?"></a>全量同步和增量同步区别?</h3><ul><li><strong>全量同步:</strong><code>master</code>将完整内存数据生成RDB,发送RDB到<code>slave</code>。后续命令则记录在<code>repl_baklog</code>,逐个发送给<code>slave</code>。</li><li><strong>增量同步:</strong><code>slave</code>提交自己的offset到<code>master</code>,<code>master</code>获取<code>repl_baklog</code>中从offset之后的数据传给slave</li></ul><h3 id="什么时候执行全量同步?"><a href="#什么时候执行全量同步?" class="headerlink" title="什么时候执行全量同步?"></a>什么时候执行全量同步?</h3><ul><li><code>slave</code>节点第一次连接<code>master</code>节点时</li><li><code>slave</code>节点断开时间太久,<code>repl_baklog</code>中的<code>offset</code>已经被覆盖时</li></ul><h3 id="什么时候执行增量同步?"><a href="#什么时候执行增量同步?" class="headerlink" title="什么时候执行增量同步?"></a>什么时候执行增量同步?</h3><ul><li>slave节点断开又恢复,并且在<code>repl_baklog</code>中能找到<code>offset</code>时</li></ul><h1 id="Redis哨兵"><a href="#Redis哨兵" class="headerlink" title="Redis哨兵"></a>Redis哨兵</h1><p>Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。</p><h2 id="哨兵原理"><a href="#哨兵原理" class="headerlink" title="哨兵原理"></a>哨兵原理</h2><p><font color=red>哨兵自动监测集群的情况,<strong>无需额外操作</strong>,即可将<code>master</code>和<code>slave</code>切换</font></p><h3 id="集群结构和作用"><a href="#集群结构和作用" class="headerlink" title="集群结构和作用"></a>集群结构和作用</h3><p>哨兵的结构如图:</p><img src="../images/redis/redis3.1/16.png" alt="16" style="zoom:50%;" /><p>哨兵的作用如下:</p><ul><li><strong>监控</strong>:Sentinel 会不断检查您的master和slave是否按预期工作</li><li><strong>自动故障恢复</strong>:如果<code>master</code>故障,Sentinel会**将一个<code>slave</code>提升为<code>master</code>**。当故障实例恢复后也以新的<code>master</code>为主</li><li><strong>通知</strong>:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会<strong>将最新信息推送给Redis的客户端</strong></li></ul><h3 id="集群监控原理"><a href="#集群监控原理" class="headerlink" title="集群监控原理"></a>集群监控原理</h3><p>Sentinel基于<strong>心跳机制</strong>监测服务状态,每隔2秒向集群的每个实例发送ping命令:</p><p>•主观下线:如果某sentinel节点发现某实例<strong>未在规定时间响应</strong>,则认为该实例<strong>主观下线</strong>。</p><p>•客观下线:若<strong>超过指定数量(quorum)的sentinel都认为该实例主观下线</strong>,则该实例<strong>客观下线</strong>。quorum值最好超过Sentinel实例数量的一半。</p><p><img src="/../images/redis/redis3.1/17.png" alt="17"></p><h3 id="集群故障恢复原理"><a href="#集群故障恢复原理" class="headerlink" title="集群故障恢复原理"></a>集群故障恢复原理</h3><p>一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:</p><ul><li>首先会判断slave节点与master节点<strong>断开时间长短</strong>,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点</li><li>然后判断slave节点的<code>slave-priority</code>值,<strong>越小优先级越高</strong>,如果是0则永不参与选举</li><li>如果slave-prority一样,则判断slave节点的<code>offset</code>值,<strong>越大说明数据越新</strong>,优先级越高</li><li>最后是判断slave节点的运行<code>id</code>大小,<strong>越小优先级越高</strong>。</li></ul><blockquote><p>选出新的master后,实现切换的流程</p><ul><li>sentinel给备选的slave节点发送<code>slaveof no one</code>命令,让该节点成为master</li><li>sentinel给所有其它slave发送<code>slaveof new_master_host new_master_port</code>命令,让这些slave成为新master的从节点,开始从新的master上同步数据。</li><li>最后,sentinel<strong>将故障节点标记为slave</strong>,当故障节点<strong>恢复后会自动成为新的master的slave节点</strong></li></ul></blockquote><img src="../images/redis/redis3.1/18.png" alt="18" style="zoom: 67%;" /><h3 id="小结-1"><a href="#小结-1" class="headerlink" title="小结"></a>小结</h3><h4 id="Sentinel的三个作用是什么?"><a href="#Sentinel的三个作用是什么?" class="headerlink" title="Sentinel的三个作用是什么?"></a>Sentinel的三个作用是什么?</h4><ul><li>监控</li><li>故障转移</li><li>通知</li></ul><h4 id="Sentinel如何判断一个redis实例是否健康?"><a href="#Sentinel如何判断一个redis实例是否健康?" class="headerlink" title="Sentinel如何判断一个redis实例是否健康?"></a>Sentinel如何判断一个redis实例是否健康?</h4><ul><li>每隔2秒发送一次ping命令,如果超过一定时间没有相向则认为是主观下线</li><li>如果大多数sentinel都认为实例主观下线,则判定服务下线</li></ul><h4 id="故障转移步骤有哪些?"><a href="#故障转移步骤有哪些?" class="headerlink" title="故障转移步骤有哪些?"></a>故障转移步骤有哪些?</h4><ul><li>首先选定一个slave作为新的master,执行slaveof no one</li><li>然后让所有节点都执行slaveof 新master</li><li>修改故障节点配置,添加slaveof 新master</li></ul><h2 id="在RedisTemplate中配置哨兵"><a href="#在RedisTemplate中配置哨兵" class="headerlink" title="在RedisTemplate中配置哨兵"></a>在RedisTemplate中配置哨兵</h2><p>在Sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发生变化,Redis的客户端必须感知这种变化,及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。</p><h3 id="引入依赖"><a href="#引入依赖" class="headerlink" title="引入依赖"></a>引入依赖</h3><p>在项目的pom文件中引入依赖:</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>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">dependency</span>></span></span><br></pre></td></tr></table></figure><h3 id="配置Redis地址"><a href="#配置Redis地址" class="headerlink" title="配置Redis地址"></a>配置Redis地址</h3><p>然后在配置文件application.yml中指定redis的sentinel相关信息:</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring</span>:<span class="string"></span></span><br><span class="line"> <span class="attr">redis</span>:<span class="string"></span></span><br><span class="line"> <span class="attr">sentinel</span>:<span class="string"></span></span><br><span class="line"> <span class="attr">master</span>: <span class="string">mymaster</span></span><br><span class="line"> <span class="attr">nodes</span>:<span class="string"></span></span><br><span class="line"> <span class="attr">-</span> <span class="string">192.168.150.101:27001</span></span><br><span class="line"> <span class="attr">-</span> <span class="string">192.168.150.101:27002</span></span><br><span class="line"> <span class="attr">-</span> <span class="string">192.168.150.101:27003</span></span><br></pre></td></tr></table></figure><h3 id="配置读写分离"><a href="#配置读写分离" class="headerlink" title="配置读写分离"></a>配置读写分离</h3><p>在项目的启动类中,添加一个新的bean:</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"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> LettuceClientConfigurationBuilderCustomizer <span class="title function_">clientConfigurationBuilderCustomizer</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">return</span> clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个bean中配置的就是读写策略,包括四种:</p><ul><li><strong>MASTER:从master读取</strong></li><li><strong>MASTER_PREFERRED:优先从master节点读取</strong>,master不可用才读取slave(replica)</li><li><strong>REPLICA:****从slave(replica)节点读取</strong></li><li><strong>REPLICA _PREFERRED:优先从slave(replica)节点读取</strong>,所有的slave都不可用才读取master<strong>(常用)</strong></li></ul><h1 id="Redis分片集群"><a href="#Redis分片集群" class="headerlink" title="Redis分片集群"></a>Redis分片集群</h1><p>主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:</p><ul><li><p>海量数据存储问题</p></li><li><p><strong>高并发写</strong>的问题</p></li></ul><p>使用分片集群可以解决上述问题</p><img src="../images/redis/redis3.1/19.png" alt="19" style="zoom:80%;" /><h2 id="搭建分片集群"><a href="#搭建分片集群" class="headerlink" title="搭建分片集群"></a>搭建分片集群</h2><p>分片集群特征:</p><ul><li><p>集群中有多个master,<strong>每个master保存不同数据</strong></p></li><li><p>每个master都可以有多个slave节点</p></li><li><p><strong>master之间通过ping监测彼此健康状态</strong></p></li><li><p>客户端请求可以<strong>访问集群任意节点</strong>,最终都会被<strong>转发到正确节点</strong></p></li></ul><h2 id="散列插槽"><a href="#散列插槽" class="headerlink" title="散列插槽"></a>散列插槽</h2><h3 id="插槽原理"><a href="#插槽原理" class="headerlink" title="插槽原理"></a>插槽原理</h3><p>Redis会<strong>把每一个master节点映射到0~16383共16384个插槽(hash slot)上</strong>,查看集群信息时就能看到:</p><p><img src="/../images/redis/redis3.1/20.png" alt="20"></p><p><strong>数据key不是与节点绑定,而是与插槽绑定。</strong></p><blockquote><p>redis会根据key的有效部分计算插槽值,分两种情况:</p><ul><li>key中包含”{}”,且“{}”中至少包含1个字符,“{}”中的部分是有效部分</li><li>key中不包含“{}”,整个key都是有效部分</li></ul></blockquote><p>在执行命令前先利用CRC16算法得到一个hash值,然后对16384取余,得到<code>slot</code>值后会重定向到指定的节点</p><p><img src="/../images/redis/redis3.1/21.png" alt="21"></p><h3 id="小结-2"><a href="#小结-2" class="headerlink" title="小结"></a>小结</h3><h4 id="Redis如何判断某个key应该在哪个实例?"><a href="#Redis如何判断某个key应该在哪个实例?" class="headerlink" title="Redis如何判断某个key应该在哪个实例?"></a>Redis如何判断某个key应该在哪个实例?</h4><ul><li>将16384个插槽分配到不同的实例</li><li>根据key的有效部分计算哈希值,对16384取余</li><li>余数作为插槽,寻找插槽所在实例即可</li></ul><h4 id="如何将同一类数据固定的保存在同一个Redis实例?"><a href="#如何将同一类数据固定的保存在同一个Redis实例?" class="headerlink" title="如何将同一类数据固定的保存在同一个Redis实例?"></a>如何将同一类数据固定的保存在同一个Redis实例?</h4><ul><li>这一类数据使用相同的有效部分,例如key都以{typeId}为前缀</li></ul><h2 id="集群伸缩"><a href="#集群伸缩" class="headerlink" title="集群伸缩"></a>集群伸缩</h2><p>redis-cli –cluster提供了很多操作集群的命令,可以通过下面方式查看:</p><p><img src="/../images/redis/redis3.1/22.png" alt="22"></p><p><img src="/../images/redis/redis3.1/23.png" alt="23"></p><h3 id="添加新节点到redis"><a href="#添加新节点到redis" class="headerlink" title="添加新节点到redis"></a>添加新节点到redis</h3><p>执行命令:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli --cluster add-node new_host:new_port existing_host:existing_port</span><br></pre></td></tr></table></figure><p>如果不加可选参数就是把新节点直接当成master,新加入的节点是没有分配插槽的,<code>slot</code>为0</p><h3 id="转移插槽"><a href="#转移插槽" class="headerlink" title="转移插槽"></a>转移插槽</h3><p>转移插槽命令格式如下:</p><img src="../images/redis/redis3.1/24.png" alt="24" style="zoom: 60%;" /><h2 id="故障转移"><a href="#故障转移" class="headerlink" title="故障转移"></a>故障转移</h2><h3 id="自动故障转移"><a href="#自动故障转移" class="headerlink" title="自动故障转移"></a>自动故障转移</h3><p>当集群中有一个master宕机时</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli -p 7002 shutdown</span><br></pre></td></tr></table></figure><p>1)首先是该实例与其它实例失去连接</p><p>2)然后是疑似宕机:</p><p><img src="/../images/redis/redis3.1/25.png" alt="25"></p><p>3)最后是确定下线,自动提升一个slave为新的master:</p><p><img src="/../images/redis/redis3.1/26.png" alt="26"></p><p>4)当7002再次启动,就会变为一个slave节点了:</p><p><img src="/../images/redis/redis3.1/27.png" alt="27"></p><h3 id="手动故障转移"><a href="#手动故障转移" class="headerlink" title="手动故障转移"></a>手动故障转移</h3><p>利用<code>cluster failover</code>命令可以手动让集群中的某个master宕机</p><p>此时执行<code>cluster failover</code>命令的这个<code>slave</code>节点会成为<code>master</code>,实现无感知的数据迁移。</p><p><img src="/../images/redis/redis3.1/28.png" alt="28"></p><p>这种<code>failover</code>命令可以指定三种模式:</p><ul><li>缺省:默认的流程,如图1~6歩<strong>(常用)</strong></li><li>force:省略了对offset的一致性校验</li><li>takeover:直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见</li></ul><h2 id="RedisTemplate访问分片集群"><a href="#RedisTemplate访问分片集群" class="headerlink" title="RedisTemplate访问分片集群"></a>RedisTemplate访问分片集群</h2><p>RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:</p><p>1)引入redis的starter依赖</p><p>2)配置分片集群地址</p><p>3)配置读写分离</p><p>与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:</p><figure class="highlight yaml"><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"><span class="attr">spring:</span></span><br><span class="line"> <span class="attr">redis:</span></span><br><span class="line"> <span class="attr">cluster:</span></span><br><span class="line"> <span class="attr">nodes:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:7001</span></span><br><span class="line"> <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:7002</span></span><br><span class="line"> <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:7003</span></span><br><span class="line"> <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:8001</span></span><br><span class="line"> <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:8002</span></span><br><span class="line"> <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:8003</span></span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><h2 id="Redis持久化-1"><a href="#Redis持久化-1" class="headerlink" title="Redis持久化"></a>Redis持久化</h2><p>Redis提供了多种持久化机制,确保数据在内存中的同时能够安全地保存到磁盘。</p><p>以下是Redis主要的持久化方式:</p><ol><li><strong>快照(Snapshotting,RDB)</strong>:<ul><li>Redis会在特定间隔时间内创建数据集的快照并将其保存到磁盘上。</li><li>生成的文件称为RDB文件,包含某一时刻的数据快照。</li><li>优点:对Redis的性能影响较小,因为在保存RDB文件时,Redis可以继续处理客户端请求。</li><li>缺点:如果Redis在快照之间宕机,会丢失自上次快照以来的所有数据。</li></ul></li><li><strong>只追加文件(Append-Only File,AOF)</strong>:<ul><li>每次写操作都会追加记录到AOF文件中。</li><li>Redis会在后台定期对AOF文件进行压缩,以减少文件大小并提高恢复速度。</li><li>优点:AOF提供了更高的数据持久性,因为每次写操作都会被记录下来。</li><li>缺点:由于每个写操作都要写入磁盘,AOF可能会影响Redis的写性能。</li></ul></li><li><strong>混合持久化(Hybrid Persistence)</strong>:<ul><li>Redis 6.0引入了一种新的持久化方式,结合了RDB和AOF的优点。</li><li>在后台生成RDB快照的同时,AOF记录正在进行的写操作。</li><li>优点:恢复速度快,数据持久性高。</li><li>缺点:实现较为复杂,磁盘占用可能会增加。</li></ul></li><li><strong>内存中的复制(In-Memory Replication)</strong>:<ul><li>虽然不是传统的持久化方式,但通过主从复制,可以实现数据的高可用性和持久性。</li><li>主节点将数据复制到从节点,从节点作为数据的备份。</li><li>优点:高可用性,快速故障恢复。</li><li>缺点:需要额外的内存和服务器资源。</li></ul></li></ol><p>在实际应用中,选择哪种持久化方式取决于具体的业务需求和系统架构设计。一般情况下,可以结合使用RDB和AOF,以在性能和数据持久性之间取得平衡。</p><h2 id="Redis集群"><a href="#Redis集群" class="headerlink" title="Redis集群"></a>Redis集群</h2><p>Redis集群是一种分布式实现,允许在多个Redis节点上分布数据和负载,以提高可用性、扩展性和性能。Redis集群有以下几种主要类型:</p><ol><li><strong>主从复制(Master-Slave Replication)</strong>:<ul><li>一个主节点负责写操作,并将数据同步到多个从节点。</li><li>从节点可以处理读操作,这样可以分担读负载。</li><li>如果主节点故障,从节点可以手动或自动提升为主节点。</li></ul></li><li><strong>哨兵模式(Sentinel Mode)</strong>:<ul><li>由Sentinel进程监控Redis主从集群的健康状态。</li><li>自动故障转移(failover):如果主节点故障,Sentinel会自动将从节点提升为主节点。</li><li>提供高可用性(HA),但不提供数据分片(sharding)。</li></ul></li><li><strong>分片集群:Redis Cluster</strong>:<ul><li>原生的Redis分布式方案,支持数据分片和高可用性。</li><li>数据分布在多个节点上,每个节点负责一部分数据。</li><li>通过hash槽(hash slot)进行数据分片,总共有16384个hash槽,节点之间通过hash槽分配数据。</li><li>支持自动故障转移和重新分片。</li></ul></li></ol><p>每种类型的Redis集群都有其适用场景和优缺点,选择哪种集群模式需要根据具体需求和应用场景来决定。</p>]]></content>
<categories>
<category> Redis </category>
</categories>
<tags>
<tag> Redis </tag>
</tags>
</entry>
<entry>
<title>Redis实战篇(三):达人探店、好友关注、附近商户、用户签到、UV统计</title>
<link href="/junwei/d031d93f.html"/>
<url>/junwei/d031d93f.html</url>
<content type="html"><![CDATA[<h1 id="达人探店"><a href="#达人探店" class="headerlink" title="达人探店"></a>达人探店</h1><p>由于学习的是Redis,所以查看博客和发布博客的功能实现我就不多赘述了,只记录关于Redis方面的内容</p><h2 id="使用Redis改进点赞功能"><a href="#使用Redis改进点赞功能" class="headerlink" title="使用Redis改进点赞功能"></a>使用Redis改进点赞功能</h2><p>现有的点赞功能没有判断用户,直接操作数据库,导致一个用户可以多次点赞同一篇Blog</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="meta">@PutMapping("/like/{id}")</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">likeBlog</span><span class="params">(<span class="meta">@PathVariable("id")</span> Long id)</span> {</span><br><span class="line"> <span class="comment">// 修改点赞数量</span></span><br><span class="line"> blogService.update()</span><br><span class="line"> .setSql(<span class="string">"liked = liked + 1"</span>).eq(<span class="string">"id"</span>, id).update();</span><br><span class="line"> <span class="keyword">return</span> Result.ok();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>代码修改:</p><p>给Blog新建一个isLike属性,使用@TableField(exist = false)注解说明该属性不存在于数据库中</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@TableField(exist = false)</span></span><br><span class="line"><span class="keyword">private</span> Boolean isLike; <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">@TableField(exist = false)</span></span><br><span class="line"> <span class="keyword">private</span> Boolean isLike;</span><br></pre></td></tr></table></figure><p>判断文章是否已经被当前用户点赞stringRedisTemplate.opsForSet().isMember()来查询Set集合中是否有当前用户</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">isBlogLiked</span><span class="params">(Blog blog)</span> {</span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"> <span class="type">Boolean</span> <span class="variable">isMember</span> <span class="operator">=</span> stringRedisTemplate.opsForSet().isMember(RedisConstants.BLOG_LIKED_KEY + blog.getId(), userId.toString());</span><br><span class="line"> blog.setIsLike(Boolean.TRUE.equals(isMember));</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">isLike</span><span class="params">(Long id)</span> {</span><br><span class="line"> <span class="comment">// 1.判断当前用户是否已经点赞</span></span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"> <span class="type">Boolean</span> <span class="variable">isMember</span> <span class="operator">=</span> stringRedisTemplate.opsForSet().isMember(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());</span><br><span class="line"> <span class="comment">// 2.当前用户未点赞</span></span><br><span class="line"> <span class="keyword">if</span>(Boolean.FALSE.equals(isMember)){</span><br><span class="line"> <span class="comment">// 2.1数据库点赞数+1</span></span><br><span class="line"> <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> update().setSql(<span class="string">"liked = liked + 1"</span>).eq(<span class="string">"id"</span>, id).update();</span><br><span class="line"> <span class="comment">// 2.2保存用户到Redis的Set集合中</span></span><br><span class="line"> <span class="keyword">if</span>(isSuccess) {</span><br><span class="line"> stringRedisTemplate.opsForSet().add(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());</span><br><span class="line"> }</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"> <span class="comment">// 3.1数据库点赞数-1</span></span><br><span class="line"> <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> update().setSql(<span class="string">"liked = liked - 1"</span>).eq(<span class="string">"id"</span>, id).update();</span><br><span class="line"> <span class="comment">// 3.2把用户从Redis的Set集合中删除</span></span><br><span class="line"> <span class="keyword">if</span> (isSuccess) {</span><br><span class="line"> stringRedisTemplate.opsForSet().remove(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> Result.ok();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="点赞排行榜"><a href="#点赞排行榜" class="headerlink" title="点赞排行榜"></a>点赞排行榜</h2><p>在前面我们把点赞的用户放在了Set集合中,但是我们现在需要显示点赞前五的用户,我们都知道Set集合是无序的</p><img src="../images/redis/redis2.3/1.png" alt="1" style="zoom: 67%;" /><ul><li>List值不唯一,排除</li><li>Set无序,排除</li><li>ZSet满足要求,且底层基于哈希表能快速判断是否存在,查找也更加高效</li></ul><p>复习一下ZSet的命令</p><ul><li><strong>ZSCORE key member :</strong> 获取sorted set中的指定元素的score值</li><li><strong>ZRANGE key min max:</strong>按照score排序后,获取指定排名范围内的元素</li></ul><p>点赞逻辑代码,修改为使用ZSet集合,根据score查询,score值为当前的时间戳<code>System.currentTimeMillis()</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Result <span class="title function_">isLike</span><span class="params">(Long id)</span> {</span><br><span class="line"> <span class="comment">// 1.判断当前用户是否已经点赞</span></span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"> <span class="type">Double</span> <span class="variable">score</span> <span class="operator">=</span> stringRedisTemplate.opsForZSet().score(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());</span><br><span class="line"> <span class="comment">// 2.当前用户未点赞</span></span><br><span class="line"> <span class="keyword">if</span>(score == <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// 2.1数据库点赞数+1</span></span><br><span class="line"> <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> update().setSql(<span class="string">"liked = liked + 1"</span>).eq(<span class="string">"id"</span>, id).update();</span><br><span class="line"> <span class="comment">// 2.2保存用户到Redis的ZSet集合中</span></span><br><span class="line"> <span class="keyword">if</span>(isSuccess) {</span><br><span class="line"> stringRedisTemplate.opsForZSet().add(RedisConstants.BLOG_LIKED_KEY + id, userId.toString(),System.currentTimeMillis());</span><br><span class="line"> }</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"> <span class="comment">// 3.1数据库点赞数-1</span></span><br><span class="line"> <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> update().setSql(<span class="string">"liked = liked - 1"</span>).eq(<span class="string">"id"</span>, id).update();</span><br><span class="line"> <span class="comment">// 3.2把用户从Redis的ZSet集合中删除</span></span><br><span class="line"> <span class="keyword">if</span> (isSuccess) {</span><br><span class="line"> stringRedisTemplate.opsForZSet().remove(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> Result.ok();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>点赞查询列表代码</p><ul><li>当返回的数据想要有序时,不能直接使用MybatisPlus中的listByIds()方法,底层用了in无法保证查到结果的顺序</li><li>Order By Field()来控制数据库按集合中对应顺序查询</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogLikes</span><span class="params">(Long id)</span> {</span><br><span class="line"> <span class="comment">// 1.在Redis中查出前五名,返回的是一个Set集合 zrange key 0 4</span></span><br><span class="line"> Set<String> top5 = stringRedisTemplate.opsForZSet().range(RedisConstants.BLOG_LIKED_KEY + id, <span class="number">0</span>, <span class="number">4</span>);</span><br><span class="line"> <span class="keyword">if</span>(top5 == <span class="literal">null</span> || top5.isEmpty()){</span><br><span class="line"> <span class="keyword">return</span> Result.ok(Collections.emptyList());</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 2.从Set集合中拿出用户ID集合</span></span><br><span class="line"> List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());</span><br><span class="line"> <span class="type">String</span> <span class="variable">idStr</span> <span class="operator">=</span> StrUtil.join(<span class="string">","</span>, ids);</span><br><span class="line"> <span class="comment">// 3.根据用户从数据库中查询用户信息 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)</span></span><br><span class="line"> List<UserDTO> userDTOList = userService.query().in(<span class="string">"id"</span>,ids).last(<span class="string">"ORDER BY FIELD(id,"</span> + idStr +<span class="string">")"</span>).list()</span><br><span class="line"> .stream()</span><br><span class="line"> .map(user -> BeanUtil.copyProperties(user, UserDTO.class))</span><br><span class="line"> .collect(Collectors.toList());</span><br><span class="line"> <span class="keyword">return</span> Result.ok(userDTOList);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="好友关注"><a href="#好友关注" class="headerlink" title="好友关注"></a>好友关注</h1><p>这章主要实现用户之间的关注功能</p><h2 id="关注和取关"><a href="#关注和取关" class="headerlink" title="关注和取关"></a>关注和取关</h2><p>这个业务逻辑特别简单,直接上代码</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><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></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">@param</span> followUserId 被关注者id</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> isFollow 关注/取关</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">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">follow</span><span class="params">(Long followUserId, Boolean isFollow)</span> {</span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"> <span class="keyword">if</span>(Boolean.TRUE.equals(isFollow)){</span><br><span class="line"> <span class="type">Follow</span> <span class="variable">follow</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Follow</span>()</span><br><span class="line"> .setUserId(userId)</span><br><span class="line"> .setFollowUserId(followUserId)</span><br><span class="line"> .setCreateTime(LocalDateTime.now());</span><br><span class="line"> save(follow);</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> remove(<span class="keyword">new</span> <span class="title class_">QueryWrapper</span><Follow>()</span><br><span class="line"> .eq(<span class="string">"user_id"</span>,userId).eq(<span class="string">"follow_user_id"</span>,followUserId));</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> Result.ok();</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 class="doctag">@param</span> id 用户id</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> true已关注,false未关注</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="keyword">public</span> Result <span class="title function_">isFollow</span><span class="params">(Long id)</span> {</span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"> <span class="type">Integer</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">"user_id"</span>, userId).eq(<span class="string">"follow_user_id"</span>, id).count();</span><br><span class="line"> <span class="keyword">return</span> Result.ok(count><span class="number">0</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="共同关注"><a href="#共同关注" class="headerlink" title="共同关注"></a>共同关注</h2><p>要求共同关注,我们可以把用户的关注列表存入Set集合中,使用Set来求交集获得共同关注列表</p><ul><li><strong>SINTER key1 key2 … :</strong>求key1与key2的交集</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Result <span class="title function_">follow</span><span class="params">(Long followUserId, Boolean isFollow)</span> {</span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"> <span class="keyword">if</span>(Boolean.TRUE.equals(isFollow)){</span><br><span class="line"> <span class="type">Follow</span> <span class="variable">follow</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Follow</span>()</span><br><span class="line"> .setUserId(userId)</span><br><span class="line"> .setFollowUserId(followUserId)</span><br><span class="line"> .setCreateTime(LocalDateTime.now());</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> save(follow);</span><br><span class="line"> <span class="keyword">if</span>(success){</span><br><span class="line"> stringRedisTemplate.opsForSet().add(RedisConstants.FOLLOW + userId,followUserId.toString());</span><br><span class="line"> }</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> remove(<span class="keyword">new</span> <span class="title class_">QueryWrapper</span><Follow>()</span><br><span class="line"> .eq(<span class="string">"user_id"</span>, userId).eq(<span class="string">"follow_user_id"</span>, followUserId));</span><br><span class="line"> <span class="keyword">if</span>(success){</span><br><span class="line"> stringRedisTemplate.opsForSet().remove(RedisConstants.FOLLOW + userId,followUserId.toString());</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> Result.ok();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>查询共同关注(两个Set集合交集)代码</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">followCommons</span><span class="params">(Long id)</span> {</span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"> Set<String> intersect = stringRedisTemplate.opsForSet()</span><br><span class="line"> .intersect(RedisConstants.FOLLOW + userId, RedisConstants.FOLLOW + id);</span><br><span class="line"> <span class="keyword">if</span>(intersect == <span class="literal">null</span> || intersect.isEmpty()){</span><br><span class="line"> <span class="keyword">return</span> Result.ok(Collections.emptyList());</span><br><span class="line"> }</span><br><span class="line"> List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());</span><br><span class="line"> List<UserDTO> users = userService.listByIds(ids)</span><br><span class="line"> .stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class))</span><br><span class="line"> .collect(Collectors.toList());</span><br><span class="line"> <span class="keyword">return</span> Result.ok(users);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="关注推送-Feed流方案"><a href="#关注推送-Feed流方案" class="headerlink" title="关注推送-Feed流方案"></a>关注推送-Feed流方案</h2><p>当我们关注了用户后,这个用户发了动态,那么我们应该把这些数据推送给用户,这个需求,其实我们又把他叫做Feed流,关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。</p><p>对于传统的模式的内容解锁:我们是需要用户去通过搜索引擎或者是其他的方式去解锁想要看的内容</p><img src="../images/redis/redis2.3/2.png" alt="2" style="zoom:60%;" /><p>对于新型的Feed流的的效果:不需要我们用户再去推送信息,而是系统分析用户到底想要什么,然后直接把内容推送给用户,从而使用户能够更加的节约时间,不用主动去寻找。</p><img src="../images/redis/redis2.3/3.png" alt="3" style="zoom:60%;" /><p>Feed流的实现有两种模式:</p><p>Feed流产品有两种常见模式:<br>Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈</p><ul><li>优点:信息全面,不会有缺失。并且实现也相对简单</li><li>缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低</li></ul><p>智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户</p><ul><li>优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷</li><li>缺点:如果算法不精准,可能起到反作用<br>本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:</li></ul><p>我们本次针对好友的操作,采用的就是Timeline的方式,只需要拿到我们关注用户的信息,然后按照时间排序即可</p><p>,因此采用Timeline的模式。该模式的实现方案有三种:</p><ul><li>拉模式</li><li>推模式</li><li>推拉结合</li></ul><p><strong>拉模式</strong>:也叫做读扩散</p><p>该模式的核心含义就是:当张三和李四和王五发了消息后,都会保存在自己的邮箱中,假设赵六要读取信息,那么他会从读取他自己的收件箱,此时系统会从他关注的人群中,把他关注人的信息全部都进行拉取,然后在进行排序</p><p>优点:比较节约空间,因为赵六在读信息时,并没有重复读取,而且读取完之后可以把他的收件箱进行清楚。</p><p>缺点:比较延迟,当用户读取数据时才去关注的人里边去读取数据,假设用户关注了大量的用户,那么此时就会拉取海量的内容,对服务器压力巨大。</p><img src="../images/redis/redis2.3/4.png" alt="4" style="zoom:50%;" /><p><strong>推模式</strong>:也叫做写扩散。</p><p>推模式是没有写邮箱的,当张三写了一个内容,此时会主动的把张三写的内容发送到他的粉丝收件箱中去,假设此时李四再来读取,就不用再去临时拉取了</p><p>优点:时效快,不用临时拉取</p><p>缺点:内存压力大,假设一个大V写信息,很多人关注他, 就会写很多分数据到粉丝那边去</p><img src="../images/redis/redis2.3/5.png" alt="5" style="zoom: 67%;" /><p><strong>推拉结合模式</strong>:也叫做读写混合,兼具推和拉两种模式的优点。</p><p>推拉模式是一个折中的方案,站在发件人这一段,如果是个普通的人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝中去,因为普通的人他的粉丝关注量比较小,所以这样做没有压力,如果是大V,那么他是直接将数据先写入到一份到发件箱里边去,然后再直接写一份到活跃粉丝收件箱里边去,现在站在收件人这端来看,如果是活跃粉丝,那么大V和普通的人发的都会直接写入到自己收件箱里边来,而如果是普通的粉丝,由于他们上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息。</p><img src="../images/redis/redis2.3/6.png" alt="6" style="zoom:50%;" /><h2 id="关注推送-Feed推模式实现"><a href="#关注推送-Feed推模式实现" class="headerlink" title="关注推送-Feed推模式实现"></a>关注推送-Feed推模式实现</h2><p>需求:</p><ul><li>修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱</li><li>收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现</li><li>查询收件箱数据时,可以实现分页查询</li></ul><p>Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。</p><p>传统了分页在feed流是不适用的,因为我们的数据会随时发生变化:</p><p>假设在t1 时刻,我们去读取第一页,此时page = 1 ,size = 5 ,那么我们拿到的就是10<del>6 这几条记录,假设现在t2时候又发布了一条记录,此时t3 时刻,我们来读取第二页,读取第二页传入的参数是page=2 ,size=5 ,那么此时读取到的第二页实际上是从6 开始,然后是6</del>2 ,那么我们就读取到了重复的数据,所以feed流的分页,不能采用原始方案来做。</p><img src="../images/redis/redis2.3/7.png" alt="1653813047671" style="zoom:60%;" /><p>Feed流的滚动分页:</p><p>我们需要记录每次操作的最后一条,然后从这个位置开始去读取数据</p><p>举个例子:我们从t1时刻开始,拿第一页数据,拿到了10~6,然后记录下当前最后一次拿取的记录,就是6,t2时刻发布了新的记录,此时这个11放到最顶上,但是不会影响我们之前记录的6,此时t3时刻来拿第二页,第二页这个时候拿数据,还是从6后一点的5去拿,就拿到了5-1的记录。</p><p>我们这个地方可以采用sortedSet来做,可以进行范围查询,并且还可以记录当前获取数据时间戳最小值,就可以实现滚动分页了</p><img src="../images/redis/redis2.3/8.png" alt="1653813462834" style="zoom:60%;" /><p>核心的意思:就是我们在保存完探店笔记后,获得到当前笔记的粉丝,然后把数据推送到粉丝的redis中去。</p><p>修改保存博客的代码,从原先的直接写入数据库改为写入数据库后再写入Redis中Set集合</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Result <span class="title function_">saveBlog</span><span class="params">(Blog blog)</span> {</span><br><span class="line"> <span class="comment">// 获取登录用户</span></span><br><span class="line"> <span class="type">UserDTO</span> <span class="variable">user</span> <span class="operator">=</span> UserHolder.getUser();</span><br><span class="line"> blog.setUserId(user.getId());</span><br><span class="line"> <span class="comment">// 保存探店博文</span></span><br><span class="line"> <span class="type">boolean</span> <span class="variable">isSuccess</span> <span class="operator">=</span> save(blog);</span><br><span class="line"> <span class="keyword">if</span> (!isSuccess) {</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"博客发布失败"</span>);</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> List<Follow> follows = followService.query().eq(<span class="string">"follow_user_id"</span>, user.getId()).list();</span><br><span class="line"> <span class="keyword">for</span> (Follow follow : follows) {</span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> follow.getUserId();</span><br><span class="line"> stringRedisTemplate.opsForZSet().add(RedisConstants.FEED_KEY + userId,blog.getId().toString(),System.currentTimeMillis());</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> Result.ok();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><img src="/../images/redis/redis2.3/9.png" alt="9"></p><h2 id="关注推送-实现分页查询推送"><a href="#关注推送-实现分页查询推送" class="headerlink" title="关注推送-实现分页查询推送"></a>关注推送-实现分页查询推送</h2><p>这个要实现的效果就是每次查询几条Blogs,只有用户向下滑动才查询接下来的几条Blogs</p><p>具体操作如下:</p><p>1、每次查询完成后,我们要分析出查询出数据的最小时间戳,这个值会作为下一次查询的条件</p><p>2、我们需要找到与上一次查询相同的查询个数作为偏移量,下次查询时,跳过这些查询过的数据,拿到我们需要的数据</p><p>综上:我们的请求参数中就需要携带 lastId:上一次查询的最小时间戳 和偏移量这两个参数。</p><p>这两个参数第一次会由前端来指定,以后的查询就根据后台结果作为条件,再次传递到后台。</p><p>为什么要判断重复的次数来确定offset:</p><p>因为他是根据这个<=score来查询,如果offset指定为1的话,那么只会排除=score的第一个值,而其他相同的值还是算了进去,就会重复出现把已经出现过的数据一样算了进来传给前端。</p><ul><li>定义出来具体的返回值实体类</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ScrollResult</span> {</span><br><span class="line"> <span class="keyword">private</span> List<?> list;</span><br><span class="line"> <span class="keyword">private</span> Long minTime;</span><br><span class="line"> <span class="keyword">private</span> Integer offset;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>BlogController</li></ul><p>注意:RequestParam 表示接受url地址栏传参的注解,当方法上参数的名称和url地址栏不相同时,可以通过RequestParam 来进行指定</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">@GetMapping("/of/follow")</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogOfFollow</span><span class="params">(</span></span><br><span class="line"><span class="params"> <span class="meta">@RequestParam("lastId")</span> Long max, <span class="meta">@RequestParam(value = "offset", defaultValue = "0")</span> Integer offset)</span>{</span><br><span class="line"> <span class="keyword">return</span> blogService.queryBlogOfFollow(max, offset);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>BlogServiceImpl</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryBlogOfFollow</span><span class="params">(Long max, Integer offset)</span> {</span><br><span class="line"> <span class="comment">// 1.获取当前用户</span></span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"> <span class="comment">// 2.查询收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> FEED_KEY + userId;</span><br><span class="line"> Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()</span><br><span class="line"> .reverseRangeByScoreWithScores(key, <span class="number">0</span>, max, offset, <span class="number">2</span>);</span><br><span class="line"> <span class="comment">// 3.非空判断</span></span><br><span class="line"> <span class="keyword">if</span> (typedTuples == <span class="literal">null</span> || typedTuples.isEmpty()) {</span><br><span class="line"> <span class="keyword">return</span> Result.ok();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 4.解析数据:blogId、minTime(时间戳)、offset</span></span><br><span class="line"> List<Long> ids = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>(typedTuples.size());</span><br><span class="line"> <span class="type">long</span> <span class="variable">minTime</span> <span class="operator">=</span> <span class="number">0</span>; <span class="comment">// 上次查询最小的时间戳</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">os</span> <span class="operator">=</span> <span class="number">1</span>; <span class="comment">// 记录出现了几次重复的</span></span><br><span class="line"> <span class="keyword">for</span> (ZSetOperations.TypedTuple<String> tuple : typedTuples) {</span><br><span class="line"> <span class="comment">// 4.1.获取id</span></span><br><span class="line"> ids.add(Long.valueOf(tuple.getValue()));</span><br><span class="line"> <span class="comment">// 4.2.获取分数(时间戳)</span></span><br><span class="line"> <span class="type">long</span> <span class="variable">time</span> <span class="operator">=</span> tuple.getScore().longValue();</span><br><span class="line"> <span class="keyword">if</span>(time == minTime){</span><br><span class="line"> os++;<span class="comment">//重复则++</span></span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> minTime = time;</span><br><span class="line"> os = <span class="number">1</span>;<span class="comment">//不重复归位</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 5.根据id查询blog</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">idStr</span> <span class="operator">=</span> StrUtil.join(<span class="string">","</span>, ids);</span><br><span class="line"> List<Blog> blogs = query().in(<span class="string">"id"</span>, ids).last(<span class="string">"ORDER BY FIELD(id,"</span> + idStr + <span class="string">")"</span>).list();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (Blog blog : blogs) {</span><br><span class="line"> <span class="comment">// 5.1.查询blog有关的用户</span></span><br><span class="line"> queryBlogUser(blog);</span><br><span class="line"> <span class="comment">// 5.2.查询blog是否被点赞</span></span><br><span class="line"> isBlogLiked(blog);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 6.封装并返回</span></span><br><span class="line"> <span class="type">ScrollResult</span> <span class="variable">r</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ScrollResult</span>();</span><br><span class="line"> r.setList(blogs);</span><br><span class="line"> r.setOffset(os);</span><br><span class="line"> r.setMinTime(minTime);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> Result.ok(r);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="附近商户-GEO"><a href="#附近商户-GEO" class="headerlink" title="附近商户(GEO)"></a>附近商户(GEO)</h1><p>Redis在3.2版本中加入了对GEO(Geolocation)的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。</p><h2 id="GEO数据结构用法"><a href="#GEO数据结构用法" class="headerlink" title="GEO数据结构用法"></a>GEO数据结构用法</h2><ul><li><strong>GEOADD:</strong>添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)</li><li><strong>GEODIST:</strong>计算指定的两个点之间的距离并返回</li><li><strong>GEOHASH:</strong>将指定member的坐标转为hash字符串形式并返回</li><li><strong>GEOPOS:</strong>返回指定member的坐标</li><li><strong>GEOSEARCH:</strong>在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能</li><li><strong>GEOSEARCHSTORE:</strong>与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。 6.2.新功能</li></ul><h2 id="导入店铺数据到Redis"><a href="#导入店铺数据到Redis" class="headerlink" title="导入店铺数据到Redis"></a>导入店铺数据到Redis</h2><p>我们需要手动将地理坐标按店铺类型存入Redis中方便查询附近商户,信息只存x,y,shopId即可</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">GeosImport</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 1.查询店铺信息</span></span><br><span class="line"> List<Shop> list = service.list();</span><br><span class="line"> <span class="comment">// 2.把店铺分组,按照typeId分组,typeId一致的放到一个集合</span></span><br><span class="line"> Map<Long,List<Shop>> listMap = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));</span><br><span class="line"> <span class="comment">// 3.分批完成写入Redis</span></span><br><span class="line"> <span class="keyword">for</span> (Map.Entry<Long,List<Shop>> entry : listMap.entrySet()) {</span><br><span class="line"> <span class="comment">// 3.1.获取类型id</span></span><br><span class="line"> <span class="type">Long</span> <span class="variable">type</span> <span class="operator">=</span> entry.getKey();</span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> RedisConstants.SHOP_GEO_KEY + type;</span><br><span class="line"> <span class="comment">// 3.2.获取同类型的店铺的集合</span></span><br><span class="line"> List<Shop> value = entry.getValue();</span><br><span class="line"> <span class="comment">// stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString());这个是直接添加多次Redis,改进后通过GeoLocation只操作一次</span></span><br><span class="line"> List<RedisGeoCommands.GeoLocation<String>> locations = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>(value.size());</span><br><span class="line"> <span class="comment">// 3.3.写入redis GEOADD key 经度 纬度 member</span></span><br><span class="line"> <span class="keyword">for</span> (Shop shop : value) {</span><br><span class="line"> locations.add(<span class="keyword">new</span> <span class="title class_">RedisGeoCommands</span>.GeoLocation<>(</span><br><span class="line"> shop.getId().toString(),</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">Point</span>(shop.getX(),shop.getY())</span><br><span class="line"> ));</span><br><span class="line"> }</span><br><span class="line"> stringRedisTemplate.opsForGeo().add(key,locations);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过创建GeoLocation可以实现只操作一次Redis,让Redis根据GeoLocation里面元素添加,而不是每循环一个操作一次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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@RequiredArgsConstructor</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">GeoLocation</span><T> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> T name;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Point point;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>外层循环解释<code>for (Map.Entry<Long,List<Shop>> entry : listMap.entrySet())</code>:</li></ul><p>-在代码中,<code>listMap</code> 是一个 <code>Map<Long, List<Shop>></code> 类型的对象,这意味着它是一个键为 <code>Long</code> 类型,值为 <code>List<Shop></code> 类型的哈希表(也就是键值对集合)。这里每一个键(<code>Long</code>)对应一个 <code>typeId</code>(店铺类型),而每一个值(<code>List<Shop></code>)对应该类型的所有店铺。</p><p>使用 <code>listMap.entrySet()</code> 方法时,它会返回这个哈希表中所有键值对的集合,每个键值对用一个 <code>Map.Entry</code> 对象表示。</p><p>具体来说,<code>Map.Entry<Long, List<Shop>> entry</code> 代表哈希表中的一个具体的键值对。<code>entry.getKey()</code> 会返回这个键值对的键(即店铺类型 <code>typeId</code>),<code>entry.getValue()</code> 会返回这个键值对的值(即该类型的所有店铺的列表)。</p><p><code>for (Map.Entry<Long, List<Shop>> entry : listMap.entrySet())</code> 这段代码的意思是遍历 <code>listMap</code> 中的每一个键值对,并将每一个键值对分别赋值给 <code>entry</code> 变量。这样你就可以在循环体内使用 <code>entry.getKey()</code> 获取店铺类型,使用 <code>entry.getValue()</code> 获取该类型的店铺列表。</p><p>到Redis中查看,添加成功</p><p><img src="/../images/redis/redis2.3/10.png" alt="10"></p><h2 id="实现附近商户功能"><a href="#实现附近商户功能" class="headerlink" title="实现附近商户功能"></a>实现附近商户功能</h2><p>实现代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryShopByType</span><span class="params">(Integer typeId, Integer current, Double x, Double y)</span> {</span><br><span class="line"> <span class="comment">// 1.判断是否需要根据坐标查询</span></span><br><span class="line"> <span class="keyword">if</span>(x ==<span class="literal">null</span> || y == <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// 根据类型分页查询</span></span><br><span class="line"> Page<Shop> page = query()</span><br><span class="line"> .eq(<span class="string">"type_id"</span>, typeId)</span><br><span class="line"> .page(<span class="keyword">new</span> <span class="title class_">Page</span><>(current, SystemConstants.DEFAULT_PAGE_SIZE));</span><br><span class="line"> <span class="comment">// 返回数据</span></span><br><span class="line"> <span class="keyword">return</span> Result.ok(page.getRecords());</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 2.计算分页参数</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">from</span> <span class="operator">=</span> (current - <span class="number">1</span>) * SystemConstants.DEFAULT_PAGE_SIZE;</span><br><span class="line"> <span class="type">int</span> <span class="variable">end</span> <span class="operator">=</span> current * SystemConstants.DEFAULT_PAGE_SIZE;</span><br><span class="line"> <span class="comment">// 3.查询redis、按照距离排序、分页。结果:shopId、distance</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> RedisConstants.SHOP_GEO_KEY + typeId;</span><br><span class="line"> GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()</span><br><span class="line"> .search(key, GeoReference.fromCoordinate(x, y), <span class="keyword">new</span> <span class="title class_">Distance</span>(<span class="number">5000</span>),</span><br><span class="line"> RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));</span><br><span class="line"> <span class="keyword">if</span> (results == <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">return</span> Result.ok(Collections.emptyList());</span><br><span class="line"> }</span><br><span class="line"> List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();</span><br><span class="line"> <span class="keyword">if</span> (list.size() <= from) {</span><br><span class="line"> <span class="comment">// 没有下一页了,结束</span></span><br><span class="line"> <span class="keyword">return</span> Result.ok(Collections.emptyList());</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 4.1.截取 from ~ end的部分</span></span><br><span class="line"> <span class="comment">// 记录id的List集合</span></span><br><span class="line"> List<Long> ids = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>(list.size());</span><br><span class="line"> <span class="comment">// 记录id及其对应的距离的Map集合</span></span><br><span class="line"> Map<String,Distance> distanceMap = <span class="keyword">new</span> <span class="title class_">HashMap</span><>(list.size());</span><br><span class="line"> list.stream().skip(from).forEach(result -> {</span><br><span class="line"> <span class="comment">// 4.2.获取店铺id</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">shopIdStr</span> <span class="operator">=</span> result.getContent().getName();</span><br><span class="line"> ids.add(Long.valueOf(shopIdStr));</span><br><span class="line"> <span class="comment">// 4.3.获取距离</span></span><br><span class="line"> <span class="type">Distance</span> <span class="variable">distance</span> <span class="operator">=</span> result.getDistance();</span><br><span class="line"> distanceMap.put(shopIdStr,distance);</span><br><span class="line"> });</span><br><span class="line"> <span class="comment">// 5.根据id查询Shop</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">idStr</span> <span class="operator">=</span> StrUtil.join(<span class="string">","</span>, ids);</span><br><span class="line"> List<Shop> shops = query().in(<span class="string">"id"</span>, ids).last(<span class="string">"ORDER BY FIELD(id,"</span> + idStr + <span class="string">")"</span>).list();</span><br><span class="line"> <span class="keyword">for</span> (Shop shop : shops) {</span><br><span class="line"> shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 6.返回</span></span><br><span class="line"> <span class="keyword">return</span> Result.ok(shops);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="用户签到-BitMap"><a href="#用户签到-BitMap" class="headerlink" title="用户签到(BitMap)"></a>用户签到(BitMap)</h1><p>我们按月来统计用户签到信息,签到记录为1,未签到则记录为0.</p><p>把每一个bit位对应当月的每一天,形成了映射关系。用0和1标示业务状态,这种思路就称为位图(BitMap)。这样我们就用极小的空间,来实现了大量数据的表示</p><p>Redis中是利用<code>string</code>类型数据结构实现<code>BitMap</code>,因此最大上限是512M,转换为bit则是 2^32个bit位,而签到31天才需要31bit</p><p><img src="/../images/redis/redis2.3/11.png" alt="11"></p><h2 id="BitMap的操作命令有:"><a href="#BitMap的操作命令有:" class="headerlink" title="BitMap的操作命令有:"></a>BitMap的操作命令有:</h2><ul><li><strong>SETBIT:</strong>向指定位置(offset)存入一个0或1</li><li><strong>GETBIT :</strong>获取指定位置(offset)的bit值</li><li><strong>BITCOUNT :</strong>统计BitMap中值为1的bit位的数量</li><li><strong>BITFIELD :</strong>操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值</li><li><strong>BITFIELD_RO :</strong>获取BitMap中bit数组,并以十进制形式返回</li><li><strong>BITOP :</strong>将多个BitMap的结果做位运算(与 、或、异或)</li><li><strong>BITPOS :</strong>查找bit数组中指定范围内第一个0或1出现的位置</li></ul><h2 id="实现签到功能"><a href="#实现签到功能" class="headerlink" title="实现签到功能"></a>实现签到功能</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Result <span class="title function_">sign</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 1.获取当前登录用户</span></span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"> <span class="comment">// 2.获取日期</span></span><br><span class="line"> <span class="type">LocalDateTime</span> <span class="variable">now</span> <span class="operator">=</span> LocalDateTime.now();</span><br><span class="line"> <span class="comment">// 3.拼接key</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">keySuffix</span> <span class="operator">=</span> now.format(DateTimeFormatter.ofPattern(<span class="string">":yyyyMM"</span>));</span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> RedisConstants.USER_SIGN_KEY + userId + keySuffix;</span><br><span class="line"> <span class="comment">// 4.获取今天是本月的第几天</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">dayOfMonth</span> <span class="operator">=</span> now.getDayOfMonth();</span><br><span class="line"> <span class="comment">// 5.写入Redis SETBIT key offset 1</span></span><br><span class="line"> stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - <span class="number">1</span>, <span class="literal">true</span>);</span><br><span class="line"> <span class="keyword">return</span> Result.ok();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="实现连续登录日期统计"><a href="#实现连续登录日期统计" class="headerlink" title="实现连续登录日期统计"></a>实现连续登录日期统计</h2><p><strong>问题1:</strong>什么叫做连续签到天数?<br>从最后一次签到开始向前统计,直到遇到第一次未签到为止,计算总的签到次数,就是连续签到天数。</p><p><img src="/../../../../BaiduNetdiskDownload/02-%E5%AE%9E%E6%88%98%E7%AF%87/%E8%AE%B2%E4%B9%89/Redis%E5%AE%9E%E6%88%98%E7%AF%87.assets/1653834455899.png" alt="1653834455899"></p><p>Java逻辑代码:获得当前这个月的最后一次签到数据,定义一个计数器,然后不停的向前统计,直到获得第一个非0的数字即可,每得到一个非0的数字计数器+1,直到遍历完所有的数据,就可以获得当前月的签到总天数了</p><p><strong>问题2:</strong>如何得到本月到今天为止的所有签到数据?</p><p> BITFIELD key GET u[dayOfMonth] 0</p><p>假设今天是10号,那么我们就可以从当前月的第一天开始,获得到当前这一天的位数,是10号,那么就是10位,去拿这段时间的数据,就能拿到所有的数据了,那么这10天里边签到了多少次呢?统计有多少个1即可。</p><p><strong>问题3:如何从后向前遍历每个bit位?</strong></p><p>注意:bitMap返回的数据是10进制,哪假如说返回一个数字8,那么我哪儿知道到底哪些是0,哪些是1呢?我们只需要让得到的10进制数字和1做与运算就可以了,因为1只有遇见1 才是1,其他数字都是0 ,我们把签到结果和1进行与操作,每与一次,就把签到结果向右移动一位,依次内推,我们就能完成逐个遍历的效果了。</p><p>需求:实现下面接口,统计当前用户截止当前时间在本月的连续签到天数</p><p>有用户有时间我们就可以组织出对应的key,此时就能找到这个用户截止这天的所有签到记录,再根据这套算法,就能统计出来他连续签到的次数了</p><p><img src="/../images/redis/redis2.3/12.png" alt="12"></p><p><strong>UserController</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping("/sign/count")</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">signCount</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">return</span> userService.signCount();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在Java中>>>表示无符号右移,对bit的操作封装在opsForValue()中了,底层是字符串</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">signCount</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 1.获取当前登录用户</span></span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"> <span class="comment">// 2.获取日期</span></span><br><span class="line"> <span class="type">LocalDateTime</span> <span class="variable">now</span> <span class="operator">=</span> LocalDateTime.now();</span><br><span class="line"> <span class="comment">// 3.拼接key</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">keySuffix</span> <span class="operator">=</span> now.format(DateTimeFormatter.ofPattern(<span class="string">":yyyyMM"</span>));</span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> USER_SIGN_KEY + userId + keySuffix;</span><br><span class="line"> <span class="comment">// 4.获取今天是本月的第几天</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">dayOfMonth</span> <span class="operator">=</span> now.getDayOfMonth();</span><br><span class="line"> <span class="comment">// 5.获取本月截止今天为止的所有的签到记录,返回的是一个十进制的数字 BITFIELD sign:5:202203 GET u14 0</span></span><br><span class="line"> List<Long> result = stringRedisTemplate.opsForValue().bitField(</span><br><span class="line"> key,</span><br><span class="line"> BitFieldSubCommands.create()</span><br><span class="line"> .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(<span class="number">0</span>)</span><br><span class="line"> );</span><br><span class="line"> <span class="keyword">if</span> (result == <span class="literal">null</span> || result.isEmpty()) {</span><br><span class="line"> <span class="comment">// 没有任何签到结果</span></span><br><span class="line"> <span class="keyword">return</span> Result.ok(<span class="number">0</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="type">Long</span> <span class="variable">num</span> <span class="operator">=</span> result.get(<span class="number">0</span>);</span><br><span class="line"> <span class="keyword">if</span> (num == <span class="literal">null</span> || num == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> Result.ok(<span class="number">0</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 6.循环遍历</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">while</span> (<span class="literal">true</span>) {</span><br><span class="line"> <span class="comment">// 6.1.让这个数字与1做与运算,得到数字的最后一个bit位 // 判断这个bit位是否为0</span></span><br><span class="line"> <span class="keyword">if</span> ((num & <span class="number">1</span>) == <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 如果为0,说明未签到,结束</span></span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }<span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 如果不为0,说明已签到,计数器+1</span></span><br><span class="line"> count++;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 把数字右移一位,抛弃最后一个bit位,继续下一个bit位</span></span><br><span class="line"> num >>>= <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> Result.ok(count);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="UV统计-HyperLogLog"><a href="#UV统计-HyperLogLog" class="headerlink" title="UV统计(HyperLogLog)"></a>UV统计(HyperLogLog)</h1><p>两个概念:</p><ul><li>UV:全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。</li><li>PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。</li></ul><p>通常来说UV会比PV大很多,所以衡量同一个网站的访问量,我们需要综合考虑很多因素,所以我们只是单纯的把这两个值作为一个参考值</p><p>UV统计在服务端做会比较麻烦,因为要判断该用户是否已经统计过了,需要将统计过的用户信息保存。但是如果每个访问的用户都保存到Redis中,数据量会非常恐怖</p><h2 id="HyperLogLog数据类型"><a href="#HyperLogLog数据类型" class="headerlink" title="HyperLogLog数据类型"></a>HyperLogLog数据类型</h2><p>Hyperloglog(HLL)是从Loglog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所有值。</p><p>相关算法原理大家可以参考:<a href="https://juejin.cn/post/6844903785744056333#heading-0">https://juejin.cn/post/6844903785744056333#heading-0</a></p><p>Redis中的HLL是基于string结构实现的,单个HLL的内存<strong>永远小于16kb</strong>,<strong>内存占用低</strong>的令人发指!作为代价,其测量结果是概率性的,<strong>有小于0.81%的误差</strong>。不过对于UV统计来说,这完全可以忽略。</p><p><img src="/../images/redis/redis2.3/13.png" alt="13"></p><h2 id="测试百万数据的统计"><a href="#测试百万数据的统计" class="headerlink" title="测试百万数据的统计"></a>测试百万数据的统计</h2><p>测试思路:我们直接利用单元测试,向HyperLogLog中添加100万条数据,看看内存占用和统计效果如何</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">testHyperLogLog</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 准备数组, 装用户数据</span></span><br><span class="line"> String[] users = <span class="keyword">new</span> <span class="title class_">String</span>[<span class="number">1000</span>];</span><br><span class="line"> <span class="comment">// 数组角标</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">index</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">1</span>; i <= <span class="number">1000000</span>; i++) {</span><br><span class="line"> <span class="comment">// 赋值</span></span><br><span class="line"> users[index++] = <span class="string">"user_"</span> + i;</span><br><span class="line"> <span class="comment">// 每1000条发送一次</span></span><br><span class="line"> <span class="keyword">if</span> (i % <span class="number">1000</span> == <span class="number">0</span>) {</span><br><span class="line"> index = <span class="number">0</span>;</span><br><span class="line"> stringRedisTemplate.opsForHyperLogLog().add(<span class="string">"hll1"</span>, users);</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="type">Long</span> <span class="variable">size</span> <span class="operator">=</span> stringRedisTemplate.opsForHyperLogLog().size(<span class="string">"hll1"</span>);</span><br><span class="line"> System.out.println(<span class="string">"size = "</span> + size);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>经过测试:我们会发生他的误差是在允许范围内,100万少400次左右,并且内存占用极小,仅14KB</p><p><img src="/../images/redis/redis2.3/14.png" alt="14"></p><p><img src="/../images/redis/redis2.3/15.png" alt="15"></p><h1 id="完结撒花"><a href="#完结撒花" class="headerlink" title="完结撒花"></a>完结撒花</h1><p>到这里Redis实战篇就学完了,感谢坚持的自己!</p>]]></content>
<categories>
<category> Redis </category>
</categories>
<tags>
<tag> Redis </tag>
</tags>
</entry>
<entry>
<title>Redis实战篇(二):秒杀问题及其优化和分布式锁与Redisson</title>
<link href="/junwei/a621503d.html"/>
<url>/junwei/a621503d.html</url>
<content type="html"><![CDATA[<h1 id="优惠券秒杀"><a href="#优惠券秒杀" class="headerlink" title="优惠券秒杀"></a>优惠券秒杀</h1><p>今天继续来学习Redis实战中的优惠券秒杀场景下碰到的问题和解决的方案</p><h2 id="全局唯一ID"><a href="#全局唯一ID" class="headerlink" title="全局唯一ID"></a>全局唯一ID</h2><p>优惠券在购买后会一个购买优惠券的订单ID,这个ID是要返还给用户的。这个ID也会被用于用户退款,或者商家自己查询到底被谁买了,那么问题就来了:</p><ul><li>如果使用MySQL数据库的自增id,那么总是从1开始,这样的数字过于简短,有太明显的规律,易于被用户和商业对手猜测出购买的数量等等</li><li>如果数量规模过大,要分库分表的话,不同的表又是从1开始,这样就会出现都是优惠券订单表,但是id相同的情况,这是很不应该的</li></ul><p>所以我们就应该构造一个全局ID生成器,我们便可以利用Redis的可increment特性,再拼接一些信息(不同公司规则不同),去产生一个全局唯一ID</p><p>实现方法:</p><p><img src="/../images/redis/redis2.2/1.png" alt="1"></p><p>成部分:符号位:1bit,永远为0</p><p>时间戳:31bit,以秒为单位,可以使用69年</p><p>序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID</p><h3 id="使用Redis实现全局唯一ID"><a href="#使用Redis实现全局唯一ID" class="headerlink" title="使用Redis实现全局唯一ID"></a>使用Redis实现全局唯一ID</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisIdWorker</span> {</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 2022年1月1日0:0:0的时间戳</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">BEGIN_TIMESTAMP</span> <span class="operator">=</span> <span class="number">1640995200L</span>;</span><br><span class="line"> <span class="meta">@Resource</span></span><br><span class="line"> <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"> <span class="keyword">public</span> <span class="type">long</span> <span class="title function_">nextId</span><span class="params">(String keyPrefix)</span>{</span><br><span class="line"> <span class="comment">// 1.生成时间戳</span></span><br><span class="line"> <span class="type">LocalDateTime</span> <span class="variable">now</span> <span class="operator">=</span> LocalDateTime.now();</span><br><span class="line"> <span class="type">long</span> <span class="variable">nowTimestamp</span> <span class="operator">=</span> now.toEpochSecond(ZoneOffset.UTC);</span><br><span class="line"> <span class="type">long</span> <span class="variable">timestamp</span> <span class="operator">=</span> nowTimestamp - BEGIN_TIMESTAMP;</span><br><span class="line"> <span class="comment">// 2.生成序列号</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">date</span> <span class="operator">=</span> now.format(DateTimeFormatter.ofPattern(<span class="string">"yyyyMMdd"</span>));</span><br><span class="line"> <span class="type">Long</span> <span class="variable">incrCount</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().increment(<span class="string">"icr:"</span> + keyPrefix + <span class="string">":"</span> + date);</span><br><span class="line"> <span class="comment">// 3.拼接并返回</span></span><br><span class="line"> <span class="keyword">return</span> timestamp << <span class="number">32</span> | incrCount;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在上面的代码中,使用<strong>long型</strong>的时间戳(当前时间戳与指定时间戳的差值),<strong>左移32位</strong>,再<strong>或运算</strong>上Redis中increment后生成的序列号,则可生成全局唯一ID</p><h2 id="代码实现优惠券秒杀"><a href="#代码实现优惠券秒杀" class="headerlink" title="代码实现优惠券秒杀"></a>代码实现优惠券秒杀</h2><p>这里的业务逻辑较为简单,就不多赘述,只需注意判断是否有库存即可</p><p><img src="/../images/redis/redis2.2/2.png" alt="2"></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><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span><span class="params">(Long voucherId)</span> {</span><br><span class="line"> <span class="comment">// 1.查询优惠券信息</span></span><br><span class="line"> <span class="type">SeckillVoucher</span> <span class="variable">seckillVoucher</span> <span class="operator">=</span> seckillVoucherService.getById(voucherId);</span><br><span class="line"> <span class="comment">// 2.判断秒杀是否开始</span></span><br><span class="line"> <span class="keyword">if</span> (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"秒杀还未开始"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 3.秒杀没有开始或已结束,返回异常结果</span></span><br><span class="line"> <span class="keyword">if</span>(seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"秒杀已经结束"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 4.秒杀正在进行,判断库存</span></span><br><span class="line"> <span class="type">Integer</span> <span class="variable">stock</span> <span class="operator">=</span> seckillVoucher.getStock();</span><br><span class="line"> <span class="comment">// 5.无库存,返回异常结果</span></span><br><span class="line"> <span class="keyword">if</span>(stock < <span class="number">1</span>){</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"该优惠券已经抢光"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 6.有库存</span></span><br><span class="line"> <span class="comment">// 6.1扣减库存</span></span><br><span class="line"> <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update().setSql(<span class="string">"stock = stock - 1"</span>).eq(<span class="string">"voucher_id"</span>,voucherId).update();</span><br><span class="line"> <span class="comment">// 6.2扣减失败</span></span><br><span class="line"> <span class="keyword">if</span>(!success){</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"库存不足"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 6.3创建订单</span></span><br><span class="line"> <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line"> <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">"order"</span>);</span><br><span class="line"> voucherOrder.setId(orderId);</span><br><span class="line"> voucherOrder.setUserId(UserHolder.getUser().getId());</span><br><span class="line"> voucherOrder.setVoucherId(voucherId);</span><br><span class="line"> save(voucherOrder);</span><br><span class="line"> <span class="comment">// 7.返回订单id</span></span><br><span class="line"> <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="库存超卖问题"><a href="#库存超卖问题" class="headerlink" title="库存超卖问题"></a>库存超卖问题</h2><h3 id="超卖问题分析"><a href="#超卖问题分析" class="headerlink" title="超卖问题分析"></a>超卖问题分析</h3><p>我们现在来分析一下原有逻辑的问题,在下面的代码中,我们使用了<1作为库存量的判断。</p><p>假设库存还剩1件,现在有两个线程:第一个线程查到了库存还剩1,但是还未删减库存时,另一线程也查到了库存为1,这下第一个线程先-1,由于后一个线程也查到了库存可用,也会-1,就会出现库存的超卖问题。</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"><span class="comment">// 5.无库存,返回异常结果</span></span><br><span class="line"><span class="keyword">if</span>(stock < <span class="number">1</span>){</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"该优惠券已经抢光"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同样,我们使用JMeter来模拟200个人同时购买这个优惠券的情况,如图所示,会出现部分成功部分失败的情况,因为库存只有100个,只有成功的才会返回了订单ID</p><p><img src="/../images/redis/redis2.2/4.png" alt="4"></p><p>但是,并没有达到我们的预期,错误率只为45.5%而不是50%,证明还是有部分不应该成功的请求成功了</p><p><img src="/../images/redis/redis2.2/5.png" alt="5"></p><p>我们回到数据库中查看,此时的库存变成了-9,有109个人买到了库存仅为100的优惠券,这是绝对不允许发生的</p><p><img src="/../images/redis/redis2.2/6.png" alt="6"></p><h3 id="超卖问题解决办法"><a href="#超卖问题解决办法" class="headerlink" title="超卖问题解决办法"></a>超卖问题解决办法</h3><p>超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:而对于加锁,我们通常有两种解决方案:见下图:</p><p><img src="/../images/redis/redis2.2/3.png" alt="3"></p><p><strong>悲观锁:</strong></p><p> 悲观锁可以实现对于数据的串行化执行,比如syn,和lock都是悲观锁的代表,同时,悲观锁中又可以再细分为公平锁,非公平锁,可重入锁,等等</p><p><strong>乐观锁:</strong></p><p> 乐观锁:会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如cas</p><p><strong>之前用到的避免缓存击穿的互斥锁就是一个典型的悲观锁,所以我们尝试用乐观锁来解决炒卖问题</strong></p><h3 id="乐观锁解决超卖问题"><a href="#乐观锁解决超卖问题" class="headerlink" title="乐观锁解决超卖问题"></a>乐观锁解决超卖问题</h3><p>乐观锁的关键是判断之前查询到的数据有没有被修改过,常见的方法有两种</p><p><img src="/../images/redis/redis2.2/7.png" alt="77"></p><p><img src="/../images/redis/redis2.2/8.png" alt="8"></p><p>在本案例中,由于是库存,我们可以直接把stock(库存)当作是版本号,就不用新加版本号了</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="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line"> .setSql(<span class="string">"stock = stock - 1"</span>).</span><br><span class="line"> eq(<span class="string">"voucher_id"</span>,voucherId).</span><br><span class="line"> eq(<span class="string">"stock"</span>,stock)<span class="comment">//加上库存判断where stock = oldStock</span></span><br><span class="line"> .update();</span><br></pre></td></tr></table></figure><p>再次执行测试,发现错误率高达92.50%,仅有200人中只有15人成功了!!</p><p><img src="/../images/redis/redis2.2/9.png" alt="9"></p><p>这是乐观锁的一个弊端,在并发线程中,同时访问到相同库存的,只有第一个可以改,其他的都发现库存被改过了,然后就不能去更改,相当于直接取消了,所以失败率大大升高</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//eq("stock",stock)</span></span><br><span class="line">gt(<span class="string">"stock"</span>,<span class="number">0</span>)<span class="comment">//去掉判断stock和原来相等,取而代之的是stock>0</span></span><br></pre></td></tr></table></figure><p>此时刚好库存用完,成功率100/200=0.5,达到预期,一张不超一张不少,且前100个请求成功,符合逻辑</p><p><img src="/../images/redis/redis2.2/10.png" alt="10"></p><p>当然还有其他乐观锁的解决方案,但是这里的库存比较简单,就没有用上。此方案还是查询到了数据库,对于高高高并发的场景可能还会出现问题,继续学习吧,方法总比困难多。</p><h2 id="一人一单"><a href="#一人一单" class="headerlink" title="一人一单"></a>一人一单</h2><p>前面我们并没有对用户进行限制,而且为了方便,也是拿了一个token(用户)进行的测试。</p><p>实际情况中,秒杀情况正常下不允许一人买多单,出现囤货情况,避免黄牛等等</p><ul><li>最简单的方法,我们判断这个userId和voucherId是否在order表中出现</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 5.一人一单逻辑</span></span><br><span class="line"><span class="comment">// 5.1.用户id</span></span><br><span class="line"><span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"><span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">"user_id"</span>, userId).eq(<span class="string">"voucher_id"</span>, voucherId).count();</span><br><span class="line"><span class="comment">// 5.2.判断是否存在</span></span><br><span class="line"><span class="keyword">if</span> (count > <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 用户已经购买过了</span></span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"用户已经购买过一次!"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>代码完成后执行,还是存在问题:并发查询数据库都显示不存在购买记录的情况,所以我们应该给他加上一个锁</p><p><strong>乐观锁比较适合更新数据,而现在是插入数据,所以我们需要使用悲观锁操作</strong></p><p><strong>注意:</strong>在这里提到了非常多的问题,我们需要慢慢的来思考,首先我们的初始方案是封装了一个createVoucherOrder方法,同时为了确保他线程安全,在方法上添加了一把synchronized 锁</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">synchronized</span> Result <span class="title function_">createVoucherOrder</span><span class="params">(Long voucherId)</span> {</span><br><span class="line"></span><br><span class="line"><span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"> <span class="comment">// 5.1.查询订单</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">"user_id"</span>, userId).eq(<span class="string">"voucher_id"</span>, voucherId).count();</span><br><span class="line"> <span class="comment">// 5.2.判断是否存在</span></span><br><span class="line"> <span class="keyword">if</span> (count > <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 用户已经购买过了</span></span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"用户已经购买过一次!"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 6.扣减库存</span></span><br><span class="line"> <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line"> .setSql(<span class="string">"stock = stock - 1"</span>) <span class="comment">// set stock = stock - 1</span></span><br><span class="line"> .eq(<span class="string">"voucher_id"</span>, voucherId).gt(<span class="string">"stock"</span>, <span class="number">0</span>) <span class="comment">// where id = ? and stock > 0</span></span><br><span class="line"> .update();</span><br><span class="line"> <span class="keyword">if</span> (!success) {</span><br><span class="line"> <span class="comment">// 扣减失败</span></span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"库存不足!"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 7.创建订单</span></span><br><span class="line"> <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line"> <span class="comment">// 7.1.订单id</span></span><br><span class="line"> <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">"order"</span>);</span><br><span class="line"> voucherOrder.setId(orderId);</span><br><span class="line"> <span class="comment">// 7.2.用户id</span></span><br><span class="line"> voucherOrder.setUserId(userId);</span><br><span class="line"> <span class="comment">// 7.3.代金券id</span></span><br><span class="line"> voucherOrder.setVoucherId(voucherId);</span><br><span class="line"> save(voucherOrder);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 7.返回订单id</span></span><br><span class="line"> <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>但是这样添加锁,锁的粒度太粗了,在使用锁过程中,控制<strong>锁粒度</strong> 是一个非常重要的事情,因为如果锁的粒度太大,会导致每个线程进来都会锁住,所以我们需要去控制锁的粒度。</p><p>以下这段代码需要修改为:<br>intern() 这个方法是从常量池中拿到数据,如果我们<strong>直接使用userId.toString() 他拿到的对象实际上是不同的对象</strong>,new出来的对象,我们使用锁<strong>必须保证锁必须是同一把</strong>,所以我们**需要使用intern()**方法</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">createVoucherOrder</span><span class="params">(Long voucherId)</span> {</span><br><span class="line"><span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"><span class="keyword">synchronized</span>(userId.toString().intern()){</span><br><span class="line"> <span class="comment">// 5.1.查询订单</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> query().eq(<span class="string">"user_id"</span>, userId).eq(<span class="string">"voucher_id"</span>, voucherId).count();</span><br><span class="line"> <span class="comment">// 5.2.判断是否存在</span></span><br><span class="line"> <span class="keyword">if</span> (count > <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 用户已经购买过了</span></span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"用户已经购买过一次!"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 6.扣减库存</span></span><br><span class="line"> <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line"> .setSql(<span class="string">"stock = stock - 1"</span>) <span class="comment">// set stock = stock - 1</span></span><br><span class="line"> .eq(<span class="string">"voucher_id"</span>, voucherId).gt(<span class="string">"stock"</span>, <span class="number">0</span>) <span class="comment">// where id = ? and stock > 0</span></span><br><span class="line"> .update();</span><br><span class="line"> <span class="keyword">if</span> (!success) {</span><br><span class="line"> <span class="comment">// 扣减失败</span></span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"库存不足!"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 7.创建订单</span></span><br><span class="line"> <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line"> <span class="comment">// 7.1.订单id</span></span><br><span class="line"> <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">"order"</span>);</span><br><span class="line"> voucherOrder.setId(orderId);</span><br><span class="line"> <span class="comment">// 7.2.用户id</span></span><br><span class="line"> voucherOrder.setUserId(userId);</span><br><span class="line"> <span class="comment">// 7.3.代金券id</span></span><br><span class="line"> voucherOrder.setVoucherId(voucherId);</span><br><span class="line"> save(voucherOrder);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 7.返回订单id</span></span><br><span class="line"> <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>但是以上代码还是存在问题,问题的原因在于当前方法被spring的事务控制。<strong>如果在方法内部加锁,可能会导致当前方法事务还没有提交,但是锁已经释放也会导致问题</strong>。所以我们选择<strong>将当前方法整体包裹起来</strong>,确保事务不会出现问题:如下:</p><p>在seckillVoucher 方法中,添加以下逻辑,这样就能保证事务的特性,同时也控制了锁的粒度</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"><span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"><span class="keyword">synchronized</span> (userId.toString().intern()) {</span><br><span class="line"> <span class="keyword">return</span> createVoucherOrder(voucherId);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>但是以上做法依然有问题,因为你调用的方法,其实是this.的方式调用的,<strong>事务想要生效,还得利用代理来生效</strong>,所以这个地方,我们需要获得原始的事务对象, 来操作事务</p><ul><li>引入依赖</li></ul><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>org.aspectj<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>aspectjweaver<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><p>给启动类加上@EnableAspectJAutoProxy</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableAspectJAutoProxy(exposeProxy = true)</span></span><br></pre></td></tr></table></figure><p>在IVoucherOrderService接口中声明方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Result <span class="title function_">createVoucherOrder</span><span class="params">(Long voucherId)</span>;</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></pre></td><td class="code"><pre><span class="line"><span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"><span class="keyword">synchronized</span> (userId.toString().intern()) {</span><br><span class="line"> <span class="comment">// 获取与事务相关的代理对象</span></span><br><span class="line"> <span class="type">IVoucherOrderService</span> <span class="variable">proxy</span> <span class="operator">=</span> (IVoucherOrderService)AopContext.currentProxy();</span><br><span class="line"> <span class="keyword">return</span> proxy.createVoucherOrder(voucherId);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>运行测试,同ID只有一个请求成功,功能实现</p><p><img src="/../images/redis/redis2.2/11.png" alt="11"></p><h2 id="集群环境下的并发问题"><a href="#集群环境下的并发问题" class="headerlink" title="集群环境下的并发问题"></a>集群环境下的并发问题</h2><p>通过加syn锁可以解决在单机情况下的一人一单,但是到集群就不行了</p><ul><li>在IDEA中开启两个服务</li></ul><img src="../images/redis/redis2.2/12.png" alt="12" style="zoom:67%;" /><ul><li>在nginx中开启负载均衡,请求到后端两台服务器上</li></ul><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"> <span class="attr">proxy_pass</span> <span class="string">http://backend;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">upstream</span> <span class="string">backend {</span></span><br><span class="line"> <span class="attr">server</span> <span class="string">127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1;</span></span><br><span class="line"> <span class="attr">server</span> <span class="string">127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;</span></span><br><span class="line"><span class="attr">}</span> <span class="string"></span></span><br></pre></td></tr></table></figure><p>然后我们使用Apifox请求两次8080前端端口,请求会分别发送到8081服务器和8082服务器,会出现一人两单的情况</p><p>原因分析:</p><p>由于现在我们部署了多个tomcat,<strong>每个tomcat都有一个属于自己的JVM</strong>,JVM中有自己的锁监视器。</p><p>那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的。</p><p>但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是集群环境下,syn锁失效的原因。</p><p><strong>在这种情况下,我们就需要使用分布式锁来解决这个问题,实现不同JVM使用同一个锁</strong></p><p><img src="/../images/redis/redis2.2/13.png" alt="13"></p><h1 id="分布式锁"><a href="#分布式锁" class="headerlink" title="分布式锁"></a>分布式锁</h1><p><strong><font color=red>分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。</font></strong></p><h2 id="分布式锁的基本原理"><a href="#分布式锁的基本原理" class="headerlink" title="分布式锁的基本原理"></a>分布式锁的基本原理</h2><p>synchronized只能保证当前JVM中的线程互斥,而没有办法让集群下的多个JVM中线程互斥</p><p>分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路</p><h2 id="分布式锁的实现方式"><a href="#分布式锁的实现方式" class="headerlink" title="分布式锁的实现方式"></a>分布式锁的实现方式</h2><p>常见的分布式锁有三种</p><p>Mysql:mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使用mysql作为分布式锁比较少见</p><p>Redis:redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁</p><p>Zookeeper:zookeeper也是企业级开发中较好的一个实现分布式锁的方案,由于本套视频并不讲解zookeeper的原理和分布式锁的实现,所以不过多阐述</p><p><img src="/../images/redis/redis2.2/14.png" alt="14"></p><h2 id="Redis分布式锁的核心思路"><a href="#Redis分布式锁的核心思路" class="headerlink" title="Redis分布式锁的核心思路"></a>Redis分布式锁的核心思路</h2><p>实现分布式锁时需要实现的两个基本方法:</p><ul><li><p>获取锁:</p><ul><li>互斥:确保只能有一个线程获取锁</li><li>非阻塞:尝试一次,成功返回true,失败返回false(阻塞式会浪费CPU资源)</li></ul></li><li><p>释放锁:</p><ul><li>手动释放</li><li>超时释放:获取锁时添加一个超时时间(兜底,避免宕机没有完成手动释放)</li></ul></li></ul><p><strong>还是基于Redis的SetNX命令来获取锁,DEL命令来释放锁</strong></p><h2 id="实现初级Redis分布式锁"><a href="#实现初级Redis分布式锁" class="headerlink" title="实现初级Redis分布式锁"></a>实现初级Redis分布式锁</h2><p>定义ILock接口,定义两个抽象方法:tryLock()方法和unlock()方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Author</span> JunWei Li</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2024-07-24 9:56</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">ILock</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 class="doctag">@param</span> timeoutSec 锁的过期时间,过期后自动释放</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> true表示获取锁成功,false表示获取锁失败</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(<span class="type">long</span> timeoutSec)</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="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>定义ILock接口的实现类SimpleRedisLock,其中key为lock:order:userId,value为当前线程ID</p><p>利用setnx方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Author</span> JunWei Li</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2024-07-24 9:58</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SimpleRedisLock</span> <span class="keyword">implements</span> <span class="title class_">ILock</span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> String name;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">SimpleRedisLock</span><span class="params">(String name, StringRedisTemplate stringRedisTemplate)</span> {</span><br><span class="line"> <span class="built_in">this</span>.name = name;</span><br><span class="line"> <span class="built_in">this</span>.stringRedisTemplate = stringRedisTemplate;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">LOCK_PREFIX</span> <span class="operator">=</span> <span class="string">"lock:"</span>;</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(<span class="type">long</span> timeoutSec)</span> {</span><br><span class="line"> <span class="comment">// 获取线程</span></span><br><span class="line"> <span class="type">long</span> <span class="variable">threadId</span> <span class="operator">=</span> Thread.currentThread().getId();</span><br><span class="line"> <span class="comment">// 获取锁</span></span><br><span class="line"> <span class="type">Boolean</span> <span class="variable">success</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + name,String.valueOf(threadId), timeoutSec, TimeUnit.SECONDS);</span><br><span class="line"> <span class="keyword">return</span> BooleanUtil.isTrue(success);</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="keyword">public</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 释放锁</span></span><br><span class="line"> stringRedisTemplate.delete(LOCK_PREFIX + name);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>修改业务代码,取消synchronized锁,取而代之的是我们自己创建的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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建锁对象</span></span><br><span class="line"><span class="type">SimpleRedisLock</span> <span class="variable">lock</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SimpleRedisLock</span>(<span class="string">"order:"</span> + userId, stringRedisTemplate);</span><br><span class="line"><span class="comment">// 获取锁</span></span><br><span class="line"><span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> lock.tryLock(<span class="number">20000</span>);</span><br><span class="line"><span class="keyword">if</span>(!isLock){</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"此优惠券限购一单"</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"> <span class="type">IVoucherOrderService</span> <span class="variable">proxy</span> <span class="operator">=</span> (IVoucherOrderService)AopContext.currentProxy();</span><br><span class="line"> <span class="keyword">return</span> proxy.createVoucherOrder(voucherId);</span><br><span class="line">} <span class="keyword">finally</span> {</span><br><span class="line"> lock.unlock();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>至此,我们就完成了初级的Redis分布式锁实现</p><h2 id="Redis分布式锁误删情况说明"><a href="#Redis分布式锁误删情况说明" class="headerlink" title="Redis分布式锁误删情况说明"></a>Redis分布式锁误删情况说明</h2><p>逻辑说明:</p><p>持有锁的线程在锁的<strong>内部出现了阻塞</strong>,导致他的<strong>锁自动释放</strong>。这时其他线程,线程2来尝试获得锁,就拿到了这把锁。然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况</p><p><img src="/../images/redis/redis2.2/15.png" alt="15"></p><p>解决方案:<strong>每个线程释放锁的时候,去判断一下当前这把锁是否属于自己</strong>。如果不属于自己,则不进行锁的删除。</p><ul><li>存入线程标识</li></ul><p><img src="/../images/redis/redis2.2/16.png" alt="16"></p><h2 id="解决Redis分布式锁误删问题"><a href="#解决Redis分布式锁误删问题" class="headerlink" title="解决Redis分布式锁误删问题"></a>解决Redis分布式锁误删问题</h2><p>改造tryLock()方法和unlock()方法</p><ul><li>获取锁前存入线程标识(原先使用ThreadId,但是问题是不同JVM中线程都是自增的,所以会有重复,可以使用UUID结合一下)</li><li>释放锁时现判断锁是不是自己的</li></ul><p>使用UUID生成一个不带“-”(传入true参数)的随机值</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">ID_PREFIX</span> <span class="operator">=</span> UUID.randomUUID().toString(<span class="literal">true</span>) + <span class="string">"-"</span>;</span><br></pre></td></tr></table></figure><p>修改tryLock()方法</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="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(<span class="type">long</span> timeoutSec)</span> {</span><br><span class="line"> <span class="comment">// 获取线程标识,拼上UUID</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">threadId</span> <span class="operator">=</span> ID_PREFIX + Thread.currentThread().getId();</span><br><span class="line"> <span class="comment">// 获取锁</span></span><br><span class="line"> <span class="type">Boolean</span> <span class="variable">success</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + name,threadId, timeoutSec, TimeUnit.SECONDS);</span><br><span class="line"> <span class="keyword">return</span> BooleanUtil.isTrue(success);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>修改unlock()方法</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 class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 获取锁</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">threadId</span> <span class="operator">=</span> ID_PREFIX + Thread.currentThread().getId();</span><br><span class="line"> <span class="type">String</span> <span class="variable">id</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(LOCK_PREFIX + name);</span><br><span class="line"> <span class="comment">// 判断锁是否是自己的</span></span><br><span class="line"> <span class="keyword">if</span>(!threadId.equals(id)){</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 释放锁</span></span><br><span class="line"> stringRedisTemplate.delete(LOCK_PREFIX + name);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>至此,误删问题就可以得到解决</p><h2 id="分布式锁的原子性问题"><a href="#分布式锁的原子性问题" class="headerlink" title="分布式锁的原子性问题"></a>分布式锁的原子性问题</h2><p>更为极端的误删逻辑说明:</p><p>线程1现在持有锁之后,在执行业务逻辑过程中,他正准备删除锁,而且已经走到了条件判断的过程中,比如他已经拿到了当前这把锁<strong>确实是属于他自己的</strong>,<strong>正准备删除锁,但是此时他的锁到期了</strong>。那么此时线程2进来,但是线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题。</p><p>之所以有这个问题,是因为线程1的拿锁,比锁,删锁,实际上并不是原子性的</p><p><img src="/../images/redis/redis2.2/17.png" alt="17"></p><h2 id="Lua脚本解决多条命令原子性问题"><a href="#Lua脚本解决多条命令原子性问题" class="headerlink" title="Lua脚本解决多条命令原子性问题"></a>Lua脚本解决多条命令原子性问题</h2><p>我们想要比较锁和删除锁的操作具有原子性,就需要用到Lua脚本</p><p>Lua是一种编程语言,它的基本语法大家可以参考网站:<a href="https://www.runoob.com/lua/lua-tutorial.html">https://www.runoob.com/lua/lua-tutorial.html</a></p><p>这里不对Lua语言进行深究,只学习怎么写Lua去操作Redis,保证原子性</p><ul><li>Redis提供的调用函数</li></ul><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis.call(<span class="string">'命令名称'</span>, <span class="string">'key'</span>, <span class="string">'其它参数'</span>, ...)</span><br></pre></td></tr></table></figure><ul><li>在Redis中使用<code>EVAL</code>调用脚本,例如下面的脚本可以添加name-Rose,age-12两个key-value</li></ul><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">EVAL <span class="string">"return redis.call('set',KEYS[2],ARGV[2])"</span> <span class="number">2</span> name age Rose <span class="number">12</span></span><br></pre></td></tr></table></figure><ul><li>key类型参数会放入<code>KEYS数组</code>,其它参数会放入<code>ARGV数组</code>,在脚本中可以从KEYS和ARGV数组获取这些参数</li><li>调用函数和参数类型间的数字代表<strong>脚本想要的key类型的参数个数</strong></li></ul><p><strong>回顾释放锁的业务流程:</strong></p><ol><li><p>获取锁中的线程标识</p></li><li><p>判断是否与指定的标示(当前线程标示)一致</p></li><li><p>如果一致则释放锁(删除)</p></li><li><p>如果不一致则什么都不做</p></li></ol><p>对应的Lua脚本</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span>(redis.call(<span class="string">'get'</span>,KEYS[<span class="number">1</span>]) == ARGV[<span class="number">1</span>]) <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">return</span> redis.call(<span class="string">'del'</span>,KEYS[<span class="number">1</span>])</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span></span><br></pre></td></tr></table></figure><h2 id="利用Java代码调用Lua脚本改造分布式锁"><a href="#利用Java代码调用Lua脚本改造分布式锁" class="headerlink" title="利用Java代码调用Lua脚本改造分布式锁"></a>利用Java代码调用Lua脚本改造分布式锁</h2><p>在RedisTemplate中有这样一个方法,对应着EVAL命令</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * (non-Javadoc)</span></span><br><span class="line"><span class="comment"> * @see org.springframework.data.redis.core.RedisOperations#execute(org.springframework.data.redis.core.script.RedisScript, java.util.List, java.lang.Object[])</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="keyword">public</span> <T> T <span class="title function_">execute</span><span class="params">(RedisScript<T> script, List<K> keys, Object... args)</span> {</span><br><span class="line"> <span class="keyword">return</span> scriptExecutor.execute(script, keys, args);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在IDEA中下载EmmyLua插件方便写Lua脚本代码,并插件unlock.lua文件,写入代码</p><p><img src="/../images/redis/redis2.2/18.png" alt="18"></p><p>Lua脚本为一个文件,到用的时候才初始化读取是不应该的,我们应该事先读取好,所以可以使用static在类加载时静态初始化</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> DefaultRedisScript<Long> UNLOCK_SCRIPT;</span><br><span class="line"><span class="keyword">static</span> {</span><br><span class="line"> <span class="comment">// Lua脚本初始化</span></span><br><span class="line"> UNLOCK_SCRIPT = <span class="keyword">new</span> <span class="title class_">DefaultRedisScript</span><>();</span><br><span class="line"> <span class="comment">// 到resource文件夹中读取文件</span></span><br><span class="line"> UNLOCK_SCRIPT.setLocation(<span class="keyword">new</span> <span class="title class_">ClassPathResource</span>(<span class="string">"unlock.lua"</span>));</span><br><span class="line"> UNLOCK_SCRIPT.setResultType(Long.class);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>修改unlock()方法,取代原先代码,可以看到使用Lua脚本就只有一行代码,很好的保证了原子性</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="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 调用Lua脚本,不需要管返回值,成功与否由系统判定</span></span><br><span class="line"> stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(LOCK_PREFIX + name),</span><br><span class="line"> ID_PREFIX + Thread.currentThread().getId());</span><br><span class="line"><span class="comment">// // 获取锁</span></span><br><span class="line"><span class="comment">// String threadId = ID_PREFIX + Thread.currentThread().getId();</span></span><br><span class="line"><span class="comment">// String id = stringRedisTemplate.opsForValue().get(LOCK_PREFIX + name);</span></span><br><span class="line"><span class="comment">// // 判断锁是否是自己的</span></span><br><span class="line"><span class="comment">// if(!threadId.equals(id)){</span></span><br><span class="line"><span class="comment">// return;</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">// stringRedisTemplate.delete(LOCK_PREFIX + name);</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>到此,我们就实现了一个生产可用、相对完善的锁了。</p><h2 id="小总结"><a href="#小总结" class="headerlink" title="小总结"></a>小总结</h2><p>基于Redis的分布式锁实现思路:</p><ul><li>利用<code>set nx ex</code>获取锁,设置过期时间,保存线程标识</li><li>释放锁现判断线程标识是否与自己的一致,避免锁误删</li></ul><p>特性:</p><ul><li>利用<code>set nx</code>满足互斥性</li><li>利用<code>set ex</code>保证故障时锁仍能释放,避免死锁</li><li>利用<code>Lua</code>脚本保证释放锁的原子性</li><li>利用Redis集群保证高可用和高并发</li></ul><h1 id="分布式锁-Redisson"><a href="#分布式锁-Redisson" class="headerlink" title="分布式锁-Redisson"></a>分布式锁-Redisson</h1><p><a href="https://redisson.org/">Redisson: Easy Redis Java client and Real-Time Data Platform</a>是 Redis Java 客户端和实时数据平台。</p><p>Redisson 对象提供了关注点分离,使您可以专注于数据建模和应用程序逻辑。</p><h2 id="基于setnx实现的分布式锁存在的问题"><a href="#基于setnx实现的分布式锁存在的问题" class="headerlink" title="基于setnx实现的分布式锁存在的问题"></a>基于setnx实现的分布式锁存在的问题</h2><p><strong>重入问题</strong>:获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。</p><p><strong>不可重试</strong>:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。</p><p><strong>超时释放:</strong>我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患</p><p><strong>主从一致性:</strong> 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。</p><p>使用现成的分布式锁工具Redisson可以很好的解决这些问题</p><p><img src="/../images/redis/redis2.2/19.png" alt="19"></p><h2 id="Redisson快速入门"><a href="#Redisson快速入门" class="headerlink" title="Redisson快速入门"></a>Redisson快速入门</h2><p>在pom中引入Redisson依赖</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr"><!--</span> <span class="string">Redission--></span></span><br><span class="line"><span class="attr"><dependency></span></span><br><span class="line"> <span class="attr"><groupId>org.redisson</groupId></span></span><br><span class="line"> <span class="attr"><artifactId>redisson</artifactId></span></span><br><span class="line"> <span class="attr"><version>3.33.0</version></span></span><br><span class="line"><span class="attr"></dependency></span></span><br></pre></td></tr></table></figure><p>创建RedissonConfig类,完成Redisson中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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Author</span> JunWei Li</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2024-07-25 9:38</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedissonConfig</span> {</span><br><span class="line"> <span class="meta">@Bean</span></span><br><span class="line"> <span class="keyword">public</span> RedissonClient <span class="title function_">redissonClient</span><span class="params">()</span>{</span><br><span class="line"> <span class="type">Config</span> <span class="variable">config</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Config</span>();</span><br><span class="line"> config.useSingleServer().setAddress(<span class="string">"redis://192.168.228.128:6379"</span>).setPassword(<span class="string">"junwei.com"</span>);</span><br><span class="line"> <span class="keyword">return</span> Redisson.create(config);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>使用Redisson创建的锁代替我们写的SimpleRedisLock</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> RedissonClient redissonClient;</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建锁对象</span></span><br><span class="line"><span class="comment">// SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);</span></span><br><span class="line"><span class="type">RLock</span> <span class="variable">lock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">"lock:order:"</span> + userId);</span><br><span class="line"><span class="comment">// 获取锁</span></span><br><span class="line"><span class="comment">// boolean isLock = lock.tryLock(20000);</span></span><br><span class="line"><span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> lock.tryLock();</span><br></pre></td></tr></table></figure><p>其中Redisson的tryLock()方法中可以接收三个参数,分别为<strong>获取锁的最大等待时间</strong>,<strong>锁自动释放时间</strong>,<strong>时间单位</strong></p><p><img src="/../images/redis/redis2.2/20.png" alt="20"></p><h2 id="Redisson可重入锁原理"><a href="#Redisson可重入锁原理" class="headerlink" title="Redisson可重入锁原理"></a>Redisson可重入锁原理</h2><p>在分布式锁中,他采用hash结构用来存储锁,其中key表示表示这把锁是否存在,用field表示当前这把锁被哪个线程持有</p><p>流程图如下,需要注意的是值为0才可以释放锁,不为0时要刷新时间</p><p><img src="/../images/redis/redis2.2/21.png" alt="21"></p><p>获取锁的Lua脚本</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><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="keyword">local</span> key = KEYS[<span class="number">1</span>]; <span class="comment">--锁的key</span></span><br><span class="line"><span class="keyword">local</span> threadId = ARGV[<span class="number">1</span>]; <span class="comment">-- 线程唯一标识</span></span><br><span class="line"><span class="keyword">local</span> releaseTime = ARGV[<span class="number">2</span>]; <span class="comment">-- 锁的自动释放时间</span></span><br><span class="line"><span class="comment">-- 判断是否存在</span></span><br><span class="line"><span class="keyword">if</span>(redis.call(<span class="string">'exists'</span>,key) == <span class="number">0</span>) <span class="keyword">then</span></span><br><span class="line"> <span class="comment">-- 不存在,获取锁</span></span><br><span class="line"> redis.call(<span class="string">'hset'</span>,key,threadId,<span class="string">'1'</span>);</span><br><span class="line"> <span class="comment">-- 设置有效期</span></span><br><span class="line"> redis.call(<span class="string">'expire'</span>,key,releaseTime);</span><br><span class="line"> <span class="comment">-- 返回结果</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 锁已存在,判断threadId是否是自己</span></span><br><span class="line"><span class="keyword">if</span>(redis.call(<span class="string">'hexists'</span>,key,threadId) == <span class="number">1</span>) <span class="keyword">then</span></span><br><span class="line"> <span class="comment">-- 存在,获取锁,重入次数+1</span></span><br><span class="line"> redis.call(<span class="string">'hincrby'</span>,key,threadId,<span class="number">1</span>);</span><br><span class="line"> <span class="comment">-- 重置有效期</span></span><br><span class="line"> redis.call(<span class="string">'expire'</span>,key,releaseTime);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 锁不是自己的,获取锁失败</span></span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br></pre></td></tr></table></figure><p>释放锁的Lua脚本</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><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="keyword">local</span> key = KEYS[<span class="number">1</span>]; <span class="comment">--锁的key</span></span><br><span class="line"><span class="keyword">local</span> threadId = ARGV[<span class="number">1</span>]; <span class="comment">-- 线程唯一标识</span></span><br><span class="line"><span class="keyword">local</span> releaseTime = ARGV[<span class="number">2</span>]; <span class="comment">-- 锁的自动释放时间</span></span><br><span class="line"><span class="comment">-- 判断锁是否为自己持有</span></span><br><span class="line"><span class="keyword">if</span>(redis.call(<span class="string">'hexists'</span>,key,threadId) == <span class="number">0</span>) <span class="keyword">then</span></span><br><span class="line"> <span class="comment">-- 锁不是自己的返回nil</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 是自己的锁,则重入次数-1</span></span><br><span class="line"><span class="keyword">local</span> count = redis.call(<span class="string">'hincrby'</span>,key,threadId,<span class="number">-1</span>);</span><br><span class="line"><span class="comment">-- 判断重入次数是否为0</span></span><br><span class="line"><span class="keyword">if</span>(count > <span class="number">0</span>) <span class="keyword">then</span></span><br><span class="line"> <span class="comment">-- 大于0说明还不能释放锁,刷新有效期后返回</span></span><br><span class="line"> redis.call(<span class="string">'expire'</span>,key,releaseTime);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line"> <span class="comment">-- 等于0说明可以释放锁</span></span><br><span class="line"> redis.call(<span class="string">'del'</span>,key);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><h2 id="Redission锁重试和WatchDog机制"><a href="#Redission锁重试和WatchDog机制" class="headerlink" title="Redission锁重试和WatchDog机制"></a>Redission锁重试和WatchDog机制</h2><ul><li><p>重试:在第一次尝试锁失败以后,不会立刻失败,而是去做一个等待,去<strong>订阅和等待释放锁的消息</strong>,利用PubSub锁的机制实现等待、唤醒,在其他线程释放锁的时候会去发送一条锁已可用的消息,可以被等待的线程捕获到,那么就可以重新获取锁了,再次获取又失败了又继续等待。当然不是无限次尝试,会有一个等待的时间,如果说超过了这个时间,就不重试了。</p></li><li><p>锁超时释放:在没有传入过期时间时,在获取锁成功后会由WatchDog开启一个定时任务,每隔一段时间就会去重置锁的有效时间,那么锁的时间就会重新计时</p></li></ul><p><img src="/../images/redis/redis2.2/25.png" alt="25"></p><p>watchdog的默认过期时间为30秒,而规定了刷新时间为<code>internalLockLeaseTime / 3</code></p><p><img src="/../images/redis/redis2.2/22.png" alt="22"></p><p>Redisson源码中的Lua脚本:</p><p><img src="/../images/redis/redis2.2/23.png" alt="22"></p><p><img src="/../images/redis/redis2.2/24.png" alt="23"></p><p>在释放锁的Lua脚本中<code>redis.call('publish', KEYS[2], ARGV[1]); </code>会向订阅者发送锁已经释放的消息,那么订阅者就可以试着拿锁</p><p>如果是没有传入时间,则此时也会进行抢锁, 而且抢锁时间是默认看门狗时间 <code>commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()</code></p><p><code>ttlRemainingFuture.onComplete((ttlRemaining, e)</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></pre></td><td class="code"><pre><span class="line">RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,</span><br><span class="line"> commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),</span><br><span class="line"> TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);</span><br><span class="line">ttlRemainingFuture.onComplete((ttlRemaining, e) -> {</span><br><span class="line"> <span class="keyword">if</span> (e != <span class="literal">null</span>) {</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 class="comment">// lock acquired</span></span><br><span class="line"> <span class="keyword">if</span> (ttlRemaining == <span class="literal">null</span>) {</span><br><span class="line"> scheduleExpirationRenewal(threadId);</span><br><span class="line"> }</span><br><span class="line">});</span><br><span class="line"><span class="keyword">return</span> ttlRemainingFuture;</span><br></pre></td></tr></table></figure><p>此逻辑就是续约逻辑,注意看commandExecutor.getConnectionManager().newTimeout() 此方法</p><p>Method( <strong>new</strong> TimerTask() {},参数2 ,参数3 )</p><p>指的是:通过参数2,参数3 去描述什么时候去做参数1的事情,现在的情况是:10s之后去做参数一的事情</p><p>因为锁的失效时间是30s,当10s之后,此时这个timeTask 就触发了,他就去进行续约,把当前这把锁续约成30s,如果操作成功,那么此时就会递归调用自己,再重新设置一个timeTask(),于是再过10s后又再设置一个timerTask,完成不停的续约</p><p>假设我们的线程出现了宕机,因为没有人再去调用renewExpiration这个方法,所以等到时间之后自然就释放了,也不会尝试死锁问题</p><h2 id="WatchDog"><a href="#WatchDog" class="headerlink" title="WatchDog"></a>WatchDog</h2><p>在Redisson的源码中,拿到锁之后开启watchdog(看门狗)更新有效期的主要原因是为了防止由于锁过期而导致的锁丢失问题,从而确保锁的可靠性。具体原因如下:</p><ol><li><strong>自动续期</strong>:当一个线程获取到分布式锁时,会设置一个初始的过期时间。如果在锁持有期间,该线程由于某些原因(例如长时间的业务处理、网络延迟等)未能及时释放锁,那么锁就有可能在过期后自动释放,从而被其他线程获取。为了避免这种情况,Redisson引入了watchdog机制。</li><li><strong>保持锁的有效性</strong>:watchdog会定期(默认每隔30秒)检查并延长锁的过期时间,确保在锁持有期间锁不会意外释放。这样,即使业务处理时间较长,也能保证锁一直由当前线程持有,不会被其他线程误抢。</li><li><strong>防止死锁</strong>:虽然watchdog能有效延长锁的持有时间,但也设置了一个锁的最大持有时间(默认为30分钟)。即使出现了极端情况,比如持有锁的线程发生了故障,也能通过这个机制防止死锁的发生,确保系统的健壮性。</li></ol><p>Redisson中watchdog机制的实现原理如下:</p><ul><li>当一个线程获取到锁时,会启动一个看门狗定时任务。</li><li>该定时任务会定期检查当前线程是否仍然持有锁,如果是则续期。</li><li>续期操作会不断延长锁的有效期,直到锁被显式释放或者超过最大持有时间为止。</li></ul><p>这个机制保证了在分布式环境中,锁能够更加可靠地被持有和释放,避免了由于锁过期导致的并发问题。</p><h2 id="Redission锁的MutiLock"><a href="#Redission锁的MutiLock" class="headerlink" title="Redission锁的MutiLock"></a>Redission锁的MutiLock</h2><p><font color=red>多个独立的Redis节点,必须在所有节点都重入锁,才算获取锁成功。简而言之,每个Redis节点都看看有没有这把锁,都有才行</font></p><ul><li>以主从为例:我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。</li></ul><img src="../images/redis/redis2.2/26.png" alt="26" style="zoom:50%;" /><ul><li>解决方法</li></ul><p>Redission提出来了MutiLock锁,使用这把锁就不使用主从了,每个节点的地位都是一样的。</p><p>这把锁加锁的逻辑需要写入到每一个主丛节点上,只有<strong>所有的服务器都写入成功</strong>,此时<strong>才是加锁成功</strong>,假设现在某个节点挂了,那么他去获得锁的时候,<strong>只要有一个节点拿不到,都不能算是加锁成功</strong>,就保证了加锁的可靠性。</p><img src="../images/redis/redis2.2/27.png" alt="27" style="zoom:50%;" /><h2 id="MutiLock(联锁)原理"><a href="#MutiLock(联锁)原理" class="headerlink" title="MutiLock(联锁)原理"></a>MutiLock(联锁)原理</h2><p><code>multiLock</code> 的核心思想是将多个锁组合在一起,以确保这些锁在同一个操作中被同时获取和释放。具体来说,<code>multiLock</code> 使用 <code>ArrayList</code> 存储每一个锁,并按照特定的逻辑依次尝试获取和释放这些锁。</p><ol><li><strong>创建 MultiLock 对象</strong></li></ol><p>首先,通过传入多个 <code>RLock</code> 对象来创建一个 <code>RedissonMultiLock</code> 实例。以下是一个简单的示例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">RLock</span> <span class="variable">lock1</span> <span class="operator">=</span> redisson.getLock(<span class="string">"lock1"</span>);</span><br><span class="line"><span class="type">RLock</span> <span class="variable">lock2</span> <span class="operator">=</span> redisson.getLock(<span class="string">"lock2"</span>);</span><br><span class="line"><span class="type">RLock</span> <span class="variable">lock3</span> <span class="operator">=</span> redisson.getLock(<span class="string">"lock3"</span>);</span><br><span class="line"></span><br><span class="line"><span class="type">RedissonMultiLock</span> <span class="variable">multiLock</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RedissonMultiLock</span>(lock1, lock2, lock3);</span><br></pre></td></tr></table></figure><ol start="2"><li><strong>存储锁对象</strong></li></ol><p><code>RedissonMultiLock</code> <strong>内部使用一个 <code>ArrayList</code> 来存储传入的锁对象</strong>。这些锁对象会在<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"><span class="keyword">private</span> <span class="keyword">final</span> List<RLock> locks = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="title function_">RedissonMultiLock</span><span class="params">(RLock... locks)</span> {</span><br><span class="line"> <span class="keyword">for</span> (RLock lock : locks) {</span><br><span class="line"> <span class="built_in">this</span>.locks.add(lock);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol start="3"><li><strong>获取锁</strong></li></ol><p><code>multiLock</code> 的 <code>lock</code> 方法会依次尝试获取所有存储在 <code>ArrayList</code> 中的锁。如果<strong>某一个锁获取失败</strong>,<strong>会释放已经获取的所有锁</strong>,并返回获取失败的状态。</p><p><strong>在获取锁失败的时候还会对失败次数继续判断:</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">locks.size() - acquiredLocks.size() == failedLocksLimit()</span><br></pre></td></tr></table></figure><p><code>failedLocksLimit</code> 方法可能返回一个允许的锁获取失败的数量上限。例如,如果 <code>failedLocksLimit</code> 返回 1,表示在获取多个锁时,允许有一个锁获取失败。这个机制的主要目的可能是为了实现某种宽松的锁策略,<strong>在某些场景下,即使部分锁获取失败,也可以继续执行后续操作,即允许某个不太重要的资源锁获取失败。</strong></p><ol start="4"><li><strong>释放锁</strong></li></ol><p><code>multiLock</code> 的 <code>unlock</code> 方法会依次释放所有存储在 <code>ArrayList</code> 中的锁。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">for</span> (RLock lock : locks) {</span><br><span class="line"> lock.unlock();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>实现细节</p><ol><li><strong>顺序处理</strong>:获取和释放锁时,按照锁在 <code>ArrayList</code> 中的<strong>顺序依次处理</strong>,确保每个锁的获取和释放都是有序的。</li><li><strong>超时处理</strong>:在尝试获取多个锁时,会计算剩余的等待时间,<strong>确保在指定的时间内尝试获取所有锁</strong>。</li><li><strong>失败处理</strong>:如果在获取锁的过程中<strong>任意一个锁获取失败</strong>,会<strong>释放已经获取的所有锁</strong>,以避免资源的死锁和浪费。</li></ol><h2 id="小总结-1"><a href="#小总结-1" class="headerlink" title="小总结"></a>小总结</h2><p>不可重入Redis分布式锁:</p><ul><li><p>原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示</p></li><li><p>缺陷:不可重入,无法重试,锁超时失效,主从一致性问题</p></li></ul><img src="../images/redis/redis2.2/19.png" style="zoom: 50%;" /><p>可重入的Redis分布式锁(Redisson):</p><ul><li><p>原理</p><ul><li>可重入:利用hash结构记录线程id和重入次数</li><li>可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制</li><li>超时续约:利用watchDog,每隔一段时间(releaseTime / 3),重置超时时间</li><li>主从一致性:使用Redisson的multiLock,采用多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功</li></ul></li><li><p>前三个容易出现缺陷:redis宕机引起锁失效问题</p></li><li><p>使用Redisson的multiLock的运维成本高,实现复杂</p></li></ul><h1 id="Redis秒杀优化"><a href="#Redis秒杀优化" class="headerlink" title="Redis秒杀优化"></a>Redis秒杀优化</h1><p>之前的秒杀流程图如下,6个过程是串行执行的,并且有4个过程是需要操作到数据库的,甚至最后两个过程还是数据库的写操作</p><p><img src="/../images/redis/redis2.2/28.png" alt="28"></p><p><strong>优化方案:</strong>我们将耗时比较短的逻辑判断放入到redis中,比如是否库存足够,比如是否一人一单,这样的操作,只要这种逻辑可以完成,就意味着我们是一定可以下单完成的,我们只需要进行快速的逻辑判断,根本就不用等下单逻辑走完,我们直接给用户返回成功, 再在后台开一个线程,后台线程慢慢的去执行queue里边的消息,这样程序不就超级快了吗?而且也不用担心线程池消耗殆尽的问题,因为这里我们的程序中并没有手动使用任何线程池</p><p><img src="/../images/redis/redis2.2/29.png" alt="29"></p><ul><li><p><strong>难点1:</strong>是我们怎么在redis中去快速校验一人一单,还有库存判断</p></li><li><p><strong>难点2:</strong>是由于我们校验和tomct下单是两个线程,那么我们如何知道到底哪个单他最后是否成功,或者是下单完成,为了完成这件事我们在redis操作完之后,我们会将一些信息返回给前端,同时将这些信息丢到异步queue中去,后续操作中,可以通过这个id来查询我们tomcat中的下单逻辑是否完成了。</p></li></ul><h2 id="秒杀优化实现思路"><a href="#秒杀优化实现思路" class="headerlink" title="秒杀优化实现思路"></a>秒杀优化实现思路</h2><p>根据Lua脚本中不同的返回值完成业务的判断,开启新线程进行异步下单</p><p>库存使用String类型存储,实现自减;下单的用户id存入Set集合,具有不可重复的特性</p><p><img src="/../images/redis/redis2.2/30.png" alt="30"></p><h2 id="Redis完成库存存储和秒杀资格判断"><a href="#Redis完成库存存储和秒杀资格判断" class="headerlink" title="Redis完成库存存储和秒杀资格判断"></a>Redis完成库存存储和秒杀资格判断</h2><p>需求:</p><ul><li><p>新增秒杀优惠券的同时,将优惠券信息保存到Redis中</p></li><li><p>基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功</p></li><li><p>如果抢购成功,将优惠券id和用户id封装后存入阻塞队列</p></li><li><p>开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能</p></li></ul><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></pre></td><td class="code"><pre><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="meta">@Transactional</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addSeckillVoucher</span><span class="params">(Voucher voucher)</span> {</span><br><span class="line"> <span class="comment">// …………保存秒杀优惠券到数据库</span></span><br><span class="line"> <span class="comment">//将优惠券库存写入Redis</span></span><br><span class="line"> stringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY +voucher.getId(),voucher.getStock().toString());</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>基于Lua脚本判断库存和一人一单</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><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="comment">-- 1.参数列表</span></span><br><span class="line"><span class="keyword">local</span> voucherId = ARGV[<span class="number">1</span>];</span><br><span class="line"><span class="keyword">local</span> userId = ARGV[<span class="number">2</span>];</span><br><span class="line"><span class="comment">-- 2.数据key</span></span><br><span class="line"><span class="keyword">local</span> stockKey = <span class="string">'seckill:stock:'</span> .. voucherId</span><br><span class="line"><span class="keyword">local</span> orderKey = <span class="string">'seckill:order:'</span> .. voucherId</span><br><span class="line"><span class="comment">-- 判断库存</span></span><br><span class="line"><span class="keyword">if</span> (<span class="built_in">tonumber</span>(redis.call(<span class="string">'get'</span>, stockKey)) <= <span class="number">0</span>) <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 判断是否第一次下单</span></span><br><span class="line"><span class="keyword">if</span> (redis.call(<span class="string">'SISMEMBER'</span>, orderKey, userId) == <span class="number">1</span>) <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">2</span>;</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 扣库存</span></span><br><span class="line">redis.call(<span class="string">'incrby'</span>, stockKey, <span class="number">-1</span>)</span><br><span class="line"><span class="comment">-- 下单</span></span><br><span class="line">redis.call(<span class="string">'sadd'</span>, orderKey, userId)</span><br><span class="line"><span class="comment">-- 返回0表示正常完成</span></span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span> <span class="params">(Long voucherId)</span>{</span><br><span class="line"> <span class="comment">// 1.执行Lua脚本</span></span><br><span class="line"> <span class="type">Long</span> <span class="variable">result</span> <span class="operator">=</span> stringRedisTemplate</span><br><span class="line"> .execute(SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(), UserHolder.getUser().getId().toString());</span><br><span class="line"> <span class="comment">// 2.判断结果 1->库存不足 2->已购买过 0->购买成功</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">res</span> <span class="operator">=</span> result.intValue();</span><br><span class="line"> <span class="keyword">if</span>(res != <span class="number">0</span>){</span><br><span class="line"> <span class="keyword">return</span> Result.fail(res == <span class="number">1</span>?<span class="string">"库存不足"</span>:<span class="string">"每个用户限购一单"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">"order"</span>);</span><br><span class="line"> <span class="comment">// TODO:保存阻塞队列</span></span><br><span class="line"> <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>到Navicat中检查Redis,确认Lua脚本执行无误</p><p><img src="/../images/redis/redis2.2/31.png" alt="31"></p><p>至此,我们完成了前两步,接下来继续完成后两步</p><h2 id="基于阻塞队列完成异步秒杀优化"><a href="#基于阻塞队列完成异步秒杀优化" class="headerlink" title="基于阻塞队列完成异步秒杀优化"></a>基于阻塞队列完成异步秒杀优化</h2><p>我们继续实现后两步</p><ul><li><p>如果抢购成功,将优惠券id和用户id封装后存入阻塞队列</p></li><li><p>开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能</p></li></ul><p>创建阻塞队列</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> BlockingQueue<VoucherOrder> orderTasks = <span class="keyword">new</span> <span class="title class_">ArrayBlockingQueue</span><>(<span class="number">1024</span> * <span class="number">1024</span>);</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ExecutorService</span> <span class="variable">SECKILL_ORDER_EXECUTOR</span> <span class="operator">=</span> Executors.newSingleThreadExecutor();</span><br></pre></td></tr></table></figure><p>定义proxy为变量,与ThreadLocal同理,拿不到代理对象,只能给值</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> IVoucherOrderService proxy;</span><br></pre></td></tr></table></figure><p>修改前两步未完成seckillVoucher()方法,补上放入阻塞队列的代码,并获取代理对象赋值</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建订单ID,封装订单信息</span></span><br><span class="line"><span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">"order"</span>);</span><br><span class="line"><span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>();</span><br><span class="line">voucherOrder.setId(orderId);</span><br><span class="line">voucherOrder.setUserId(userId);</span><br><span class="line">voucherOrder.setVoucherId(voucherId);</span><br><span class="line"><span class="comment">// 放入阻塞队列</span></span><br><span class="line">orderTasks.add(voucherOrder);</span><br><span class="line"><span class="comment">// 获取与事务相关的代理对象</span></span><br><span class="line">proxy = (IVoucherOrderService) AopContext.currentProxy();</span><br></pre></td></tr></table></figure><p>@PostConstruct注解在类加载时执行,死循环一致尝试读取阻塞队列</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostConstruct</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">()</span>{</span><br><span class="line"> SECKILL_ORDER_EXECUTOR.submit(<span class="keyword">new</span> <span class="title class_">VoucherOrderHandler</span>());</span><br><span class="line">}</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">class</span> <span class="title class_">VoucherOrderHandler</span> <span class="keyword">implements</span> <span class="title class_">Runnable</span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">while</span>(<span class="literal">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"> <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> orderTasks.take();</span><br><span class="line"> <span class="comment">// 创建订单</span></span><br><span class="line"> handleVoucherOrder(voucherOrder);</span><br><span class="line"> } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line"> log.error(<span class="string">"处理订单异常"</span>,e);</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>修改订单处理代码,其中的锁代码可以省略,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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">handleVoucherOrder</span><span class="params">(VoucherOrder voucherOrder)</span> {</span><br><span class="line"> <span class="comment">// 只能从voucherOrder中取用户ID,因为这个是单独子线程,拿不到ThreadLocal中的值</span></span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> voucherOrder.getUserId();</span><br><span class="line"> <span class="comment">// 创建锁对象</span></span><br><span class="line"> <span class="type">RLock</span> <span class="variable">lock</span> <span class="operator">=</span> redissonClient.getLock(<span class="string">"lock:order:"</span> + userId);</span><br><span class="line"> <span class="comment">// 获取锁</span></span><br><span class="line"> <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> lock.tryLock();</span><br><span class="line"> <span class="keyword">if</span> (!isLock) {</span><br><span class="line"> log.error(<span class="string">"不允许重复下单"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> proxy.createVoucherOrder(voucherOrder);</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> lock.unlock();</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">createVoucherOrder</span><span class="params">(VoucherOrder voucherOrder)</span> {</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">success</span> <span class="operator">=</span> seckillVoucherService.update()</span><br><span class="line"> .setSql(<span class="string">"stock = stock - 1"</span>).</span><br><span class="line"> eq(<span class="string">"voucher_id"</span>, voucherOrder.getVoucherId()).</span><br><span class="line"> gt(<span class="string">"stock"</span>,<span class="number">0</span>)</span><br><span class="line"> .update();</span><br><span class="line"> <span class="comment">// 6.2扣减失败</span></span><br><span class="line"> <span class="keyword">if</span>(!success){</span><br><span class="line"> log.error(<span class="string">"库存不足"</span>);</span><br><span class="line"> }</span><br><span class="line"> save(voucherOrder);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="小总结-2"><a href="#小总结-2" class="headerlink" title="小总结"></a>小总结</h2><p>秒杀业务的优化思路</p><ul><li>先利用Redis完成库存余量、一人一单判断,完成抢单业务</li><li>再将下单业务放入阻塞队列,利用独立线程异步下单</li><li>基于阻塞队列的异步秒杀存在哪些问题?<ul><li>内存限制问题(手动设置了阻塞队列的长度,容易溢出)</li><li>数据安全问题<ul><li>数据存储在Redis中,宕机后数据丢失。</li><li>阻塞队列存在JVM中,JVM宕机时阻塞队列全部订单信息会丢失</li></ul></li></ul></li></ul><h1 id="Redis消息队列"><a href="#Redis消息队列" class="headerlink" title="Redis消息队列"></a>Redis消息队列</h1><p>消息队列:存放消息的队列</p><ul><li>消息队列:存储和管理消息,也被称为消息代理(Message Broker)</li><li>生产者:发送消息到消息队列</li><li>消费者:从消息队列获取消息并处理消息</li></ul><img src="../images/redis/redis2.2/32.png" alt="32" style="zoom: 60%;" /><p>使用队列的好处在于<strong>解耦:</strong>快递员会把快递放到菜鸟驿站,然后再由菜鸟驿站通知您的快递到达菜鸟了,而快递员无需等待,就可以去送下一批货,而不是说你不在家就一直等你。</p><p>由于这门课是学习Redis的,所以我们使用Redis实现消息队列,对于中小型企业或者不是大型项目,Redis已经足够了。</p><p>如果要求更高的话,可以使用<a href="https://www.rabbitmq.com/">RabbitMQ: One broker to queue them all</a>或者<a href="https://kafka.apache.org/">Apache Kafka</a>,功能更加强大,可以去学习<a href="https://www.bilibili.com/video/BV1S142197x7/?vd_source=414786025eeb24c0f40ed9fedc2e4eb0">2024最新SpringCloud微服务开发与实战,java黑马商城项目微服务实战开发(涵盖MybatisPlus、Docker、MQ、ES、Redis高级等)</a></p><h2 id="基于List实现的消息队列"><a href="#基于List实现的消息队列" class="headerlink" title="基于List实现的消息队列"></a>基于List实现的消息队列</h2><p>Redis的list数据结构是一个双向链表,很容易模拟出队列效果。搭配LPUSH和RPOP或者RPUSH和LPOP就好了</p><p>基于List的消息队列有哪些优缺点?<br>优点:</p><ul><li>利用Redis存储,不受限于JVM内存上限</li><li>基于Redis的持久化机制,数据安全性有保证</li><li>可以满足消息有序性</li></ul><p>缺点:</p><ul><li>无法避免消息丢失</li><li>只支持单消费者</li></ul><h2 id="基于PubSub实现的消息队列"><a href="#基于PubSub实现的消息队列" class="headerlink" title="基于PubSub实现的消息队列"></a>基于PubSub实现的消息队列</h2><p>PubSub(发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。<a href="https://redis.io/docs/latest/commands/?group=pubsub">Commands | Docs (PubSub)</a></p><ul><li><code>SUBSCRIBE channel [channel]</code> :订阅一个或多个频道</li><li><code>PUBLISH channel msg</code>:向一个频道发送消息</li><li><code>PSUBSCRIBE pattern[pattern] </code>:订阅与pattern格式匹配的所有频道<ul><li>Supported glob-style patterns:<ul><li><code>h?llo</code> subscribes to <code>hello</code>, <code>hallo</code> and <code>hxllo</code></li><li><code>h*llo</code> subscribes to <code>hllo</code> and <code>heeeello</code></li><li><code>h[ae]llo</code> subscribes to <code>hello</code> and <code>hallo,</code> but not <code>hillo</code></li></ul></li></ul></li></ul><p>基于PubSub的消息队列有哪些优缺点?<br>优点:</p><ul><li>采用发布订阅模型,支持多生产、多消费</li></ul><p>缺点:</p><ul><li>不支持数据持久化</li><li>无法避免消息丢失</li><li>消息堆积有上限,超出时数据丢失</li></ul><h2 id="基于Stream的消息队列"><a href="#基于Stream的消息队列" class="headerlink" title="基于Stream的消息队列"></a>基于Stream的消息队列</h2><p>Stream 是 Redis 5.0 引入的一种新数据类型,可以实现一个功能非常完善的消息队列。<a href="https://redis.io/docs/latest/commands/?group=stream">Commands | Docs (Stream)</a></p><ul><li><code>XADD</code>发送消息</li></ul><p><img src="/../images/redis/redis2.2/33.png" alt="33"></p><ul><li><code>XREAD</code>读取消息</li></ul><p><img src="/../images/redis/redis2.2/34.png" alt="34"></p><p>注意:当我们指定起始ID为$时,代表读取最新的消息,如果我们处理一条消息的过程中,又有超过1条以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题</p><p><strong>STREAM类型消息队列的XREAD命令特点:</strong></p><ul><li>消息可回溯</li><li>一个消息可以被多个消费者读取</li><li>可以阻塞读取</li><li>有消息漏读的风险</li></ul><h2 id="基于Stream的消息队列-消费者组"><a href="#基于Stream的消息队列-消费者组" class="headerlink" title="基于Stream的消息队列-消费者组"></a>基于Stream的消息队列-消费者组</h2><p>消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列。具备下列特点:</p><p><img src="/../images/redis/redis2.2/35.png" alt="35"></p><ul><li><strong>创建消费者组:</strong></li></ul><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">XGROUP CREATE key groupName Id [MKSTREAM]</span><br></pre></td></tr></table></figure><p>key:队列名称<br>groupName:消费者组名称<br>ID:起始ID标示,$代表队列中最后一个消息,0则代表队列中第一个消息<br>MKSTREAM:队列不存在时自动创建队列</p><ul><li><strong>删除指定的消费者组</strong></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">XGROUP DESTORY key groupName</span><br></pre></td></tr></table></figure><ul><li><strong>给指定的消费者组添加消费者</strong></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">XGROUP CREATECONSUMER key groupname consumername</span><br></pre></td></tr></table></figure><ul><li><strong>删除消费者组中的指定消费者</strong></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">XGROUP DELCONSUMER key groupname consumername</span><br></pre></td></tr></table></figure><ul><li><strong>从消费者组读取消息:</strong></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]</span><br></pre></td></tr></table></figure><ul><li><p>group:消费组名称</p></li><li><p>consumer:消费者名称,如果消费者不存在,会自动创建一个消费者</p></li><li><p>count:本次查询的最大数量</p></li><li><p>BLOCK milliseconds:当没有消息时最长等待时间</p></li><li><p>NOACK:无需手动ACK,获取到消息后自动确认</p></li><li><p>STREAMS key:指定队列名称</p></li><li><p>ID:获取消息的起始ID:</p><ul><li>“>”:从下一个未消费的消息开始</li><li>其它:根据指定id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个消息开始</li></ul></li></ul><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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span>(<span class="literal">true</span>){</span><br><span class="line"> <span class="comment">// 读取监测实例,返回批处理消息,最长等待 2000 毫秒</span></span><br><span class="line"> <span class="type">Object</span> <span class="variable">msg</span> <span class="operator">=</span> redis.call(<span class="string">"XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >"</span>);</span><br><span class="line"> <span class="keyword">if</span>(msg == <span class="literal">null</span>){ <span class="comment">// null说明没有消息,继续下一次</span></span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 处理消息,完成后一定要ACK</span></span><br><span class="line"> handleMessage(msg);</span><br><span class="line"> } <span class="keyword">catch</span>(Exception e) {</span><br><span class="line"> <span class="keyword">while</span>(<span class="literal">true</span>) {</span><br><span class="line"> <span class="type">Object</span> <span class="variable">msg</span> <span class="operator">=</span> redis.call(<span class="string">"XREADGROUP GROUP g1 c1 COUNT 1 STREAMS s1 0"</span>);</span><br><span class="line"> <span class="keyword">if</span>(msg == <span class="literal">null</span>){ <span class="comment">// null说明没有异常消息,所有消息均已确认,结束循环</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">try</span> {</span><br><span class="line"> <span class="comment">// 说明有异常消息,再次处理</span></span><br><span class="line"> handleMessage(msg);</span><br><span class="line"> } <span class="keyword">catch</span>(Exception e) {</span><br><span class="line"> <span class="comment">// 再次出现异常,记录日志,继续循环</span></span><br><span class="line"> <span class="keyword">continue</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><strong>STREAM类型消息队列的XREADGROUP命令特点:</strong></p><ul><li>消息可回溯</li><li>可以多消费者争抢消息,加快消费速度</li><li>可以阻塞读取</li><li>没有消息漏读的风险</li><li>有消息确认机制,保证消息至少被消费一次</li></ul><h2 id="各种方式实现消息队列的对比"><a href="#各种方式实现消息队列的对比" class="headerlink" title="各种方式实现消息队列的对比"></a>各种方式实现消息队列的对比</h2><p><img src="/../images/redis/redis2.2/36.png" alt="36"></p><h2 id="基于Stream结构作消息队列,实现异步秒杀"><a href="#基于Stream结构作消息队列,实现异步秒杀" class="headerlink" title="基于Stream结构作消息队列,实现异步秒杀"></a>基于Stream结构作消息队列,实现异步秒杀</h2><p>实现步骤:</p><ul><li>创建一个Stream类型的消息队列,名为stream.orders</li><li>修改之前的秒杀下单Lua脚本,在认定有抢购资格后,直接向stream.orders中添加消息,内容包含voucherId、userId、orderId</li><li>项目启动时,开启一个线程任务,尝试获取stream.orders中的消息,完成下单</li></ul><ol><li>在Redis中新建一个stream类型的消息队列</li></ol><img src="../images/redis/redis2.2/37.png" alt="37" style="zoom:67%;" /><ol start="2"><li>修改Lua脚本,添加两行代码</li></ol><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 1.参数列表</span></span><br><span class="line"><span class="keyword">local</span> voucherId = ARGV[<span class="number">1</span>];</span><br><span class="line"><span class="keyword">local</span> userId = ARGV[<span class="number">2</span>];</span><br><span class="line"><span class="keyword">local</span> orderId = ARGV[<span class="number">3</span>];//**新增**</span><br><span class="line"><span class="comment">-- 2.数据key</span></span><br><span class="line"><span class="keyword">local</span> stockKey = <span class="string">'seckill:stock:'</span> .. voucherId</span><br><span class="line"><span class="keyword">local</span> orderKey = <span class="string">'seckill:order:'</span> .. voucherId</span><br><span class="line"><span class="comment">-- 判断库存</span></span><br><span class="line"><span class="keyword">if</span> (<span class="built_in">tonumber</span>(redis.call(<span class="string">'get'</span>, stockKey)) <= <span class="number">0</span>) <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 判断是否第一次下单</span></span><br><span class="line"><span class="keyword">if</span> (redis.call(<span class="string">'SISMEMBER'</span>, orderKey, userId) == <span class="number">1</span>) <span class="keyword">then</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">2</span>;</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 扣库存</span></span><br><span class="line">redis.call(<span class="string">'incrby'</span>, stockKey, <span class="number">-1</span>)</span><br><span class="line"><span class="comment">-- 下单</span></span><br><span class="line">redis.call(<span class="string">'sadd'</span>, orderKey, userId)</span><br><span class="line"><span class="comment">-- 发送消息到队列中</span></span><br><span class="line">redis.call(<span class="string">'XADD'</span>, <span class="string">'stream.orders'</span>,<span class="string">'*'</span>,<span class="string">'userId'</span>,userId,<span class="string">'voucherId'</span>,voucherId,<span class="string">'id'</span>,orderId);//**新增**</span><br><span class="line"><span class="comment">-- 返回0表示正常完成</span></span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br></pre></td></tr></table></figure><ol start="3"><li>修改操作Redis的代码,因为Lua中新增了将orderId需要传入,才能确保订单信息被存入消息队列时是完整的</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">seckillVoucher</span> <span class="params">(Long voucherId)</span>{</span><br><span class="line"> <span class="comment">// 创建订单ID,封装订单信息</span></span><br><span class="line"> <span class="type">long</span> <span class="variable">orderId</span> <span class="operator">=</span> redisIdWorker.nextId(<span class="string">"order"</span>);</span><br><span class="line"> <span class="type">Long</span> <span class="variable">userId</span> <span class="operator">=</span> UserHolder.getUser().getId();</span><br><span class="line"> <span class="comment">// 1.执行Lua脚本</span></span><br><span class="line"> <span class="type">Long</span> <span class="variable">result</span> <span class="operator">=</span> stringRedisTemplate</span><br><span class="line"> .execute(SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString(),String.valueOf(orderId));</span><br><span class="line"> <span class="comment">// 2.判断结果 1->库存不足 2->已购买过 0->购买成功</span></span><br><span class="line"> <span class="type">int</span> <span class="variable">res</span> <span class="operator">=</span> result.intValue();</span><br><span class="line"> <span class="keyword">if</span>(res != <span class="number">0</span>){</span><br><span class="line"> <span class="keyword">return</span> Result.fail(res == <span class="number">1</span>?<span class="string">"库存不足"</span>:<span class="string">"每个用户限购一单"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 获取与事务相关的代理对象</span></span><br><span class="line"> proxy = (IVoucherOrderService) AopContext.currentProxy();</span><br><span class="line"> <span class="keyword">return</span> Result.ok(orderId);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol start="4"><li>修改单线程拿取消息队列中数据的代码,使用StringRedisTemplate中的opsForStream()方法操作Stream类型</li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostConstruct</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">()</span>{</span><br><span class="line"> SECKILL_ORDER_EXECUTOR.submit(<span class="keyword">new</span> <span class="title class_">VoucherOrderHandler</span>());</span><br><span class="line">}</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">class</span> <span class="title class_">VoucherOrderHandler</span> <span class="keyword">implements</span> <span class="title class_">Runnable</span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">while</span>(<span class="literal">true</span>){</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 获取阻塞队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders ></span></span><br><span class="line"> <span class="type">String</span> <span class="variable">queueName</span> <span class="operator">=</span> <span class="string">"stream.orders"</span>;</span><br><span class="line"> List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(</span><br><span class="line"> Consumer.from(<span class="string">"g1"</span>, <span class="string">"c1"</span>),</span><br><span class="line"> StreamReadOptions.empty().count(<span class="number">1</span>).block(Duration.ofSeconds(<span class="number">2</span>)),</span><br><span class="line"> StreamOffset.create(queueName, ReadOffset.lastConsumed()));</span><br><span class="line"> <span class="comment">// 获取消息失败</span></span><br><span class="line"> <span class="keyword">if</span>(list == <span class="literal">null</span> || list.isEmpty()){</span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 获取消息成功,可以下单</span></span><br><span class="line"> <span class="comment">// 从消息list中拿到消息记录,获取第一条</span></span><br><span class="line"> MapRecord<String, Object, Object> record = list.get(<span class="number">0</span>);</span><br><span class="line"> <span class="comment">// 获取出其中的键值对Map</span></span><br><span class="line"> Map<Object, Object> value = record.getValue();</span><br><span class="line"> <span class="comment">// 转化为Bean对象</span></span><br><span class="line"> <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> BeanUtil.fillBeanWithMap(value, <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>(), <span class="literal">true</span>);</span><br><span class="line"> <span class="comment">// 下单业务</span></span><br><span class="line"> handleVoucherOrder(voucherOrder);</span><br><span class="line"> <span class="comment">// ACK确认 SACK stream.orders g1 id</span></span><br><span class="line"> stringRedisTemplate.opsForStream().acknowledge(queueName,<span class="string">"g1"</span>,record.getId());</span><br><span class="line"> } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line"> log.error(<span class="string">"处理订单异常"</span>,e);</span><br><span class="line"> handlePendingList();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><ol start="5"><li>新增handlePendingList()方法,处理已经读取消息但未ACK的情况,从pending-list中重新尝试拿到数据写入数据库并ACK</li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">handlePendingList</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">while</span>(<span class="literal">true</span>){</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 获取阻塞队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 STREAMS s1 0 取消等待,从0开始读</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">queueName</span> <span class="operator">=</span> <span class="string">"stream.orders"</span>;</span><br><span class="line"> List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(</span><br><span class="line"> Consumer.from(<span class="string">"g1"</span>, <span class="string">"c1"</span>),</span><br><span class="line"> StreamReadOptions.empty().count(<span class="number">1</span>),</span><br><span class="line"> StreamOffset.create(queueName, ReadOffset.from(<span class="string">"0"</span>)));</span><br><span class="line"> <span class="comment">// 获取消息失败,说明pending-list中没有消息</span></span><br><span class="line"> <span class="keyword">if</span>(list == <span class="literal">null</span> || list.isEmpty()){</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"> <span class="comment">// 从消息list中拿到消息记录,获取第一条</span></span><br><span class="line"> MapRecord<String, Object, Object> record = list.get(<span class="number">0</span>);</span><br><span class="line"> <span class="comment">// 获取出其中的键值对Map</span></span><br><span class="line"> Map<Object, Object> value = record.getValue();</span><br><span class="line"> <span class="comment">// 转化为Bean对象</span></span><br><span class="line"> <span class="type">VoucherOrder</span> <span class="variable">voucherOrder</span> <span class="operator">=</span> BeanUtil.fillBeanWithMap(value, <span class="keyword">new</span> <span class="title class_">VoucherOrder</span>(), <span class="literal">true</span>);</span><br><span class="line"> <span class="comment">// 下单业务</span></span><br><span class="line"> handleVoucherOrder(voucherOrder);</span><br><span class="line"> <span class="comment">// ACK确认</span></span><br><span class="line"> stringRedisTemplate.opsForStream().acknowledge(queueName,<span class="string">"g1"</span>,record.getId());</span><br><span class="line"> } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line"> log.error(<span class="string">"处理订单异常"</span>,e);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>执行代码检查无误,库存正常扣减,数据库写入正常,消息队列中保存了order消息</p><img src="../images/redis/redis2.2/38.png" alt="38" style="zoom: 67%;" /><p>Redis消息队列中的订单ID与实际存入数据库中的一致</p><p><img src="/../images/redis/redis2.2/39.png" alt="39"></p>]]></content>
<categories>
<category> Redis </category>
</categories>
<tags>
<tag> Redis </tag>
</tags>
</entry>
<entry>
<title>Redis实战篇(一):短信登录和商品缓存</title>
<link href="/junwei/a6b15897.html"/>
<url>/junwei/a6b15897.html</url>
<content type="html"><![CDATA[<h1 id="Redis实战"><a href="#Redis实战" class="headerlink" title="Redis实战"></a>Redis实战</h1><p><font size=4>  本Redis实战基于<a href="https://www.bilibili.com/video/BV1cr4y1671t/?p=34&share_source=copy_web&vd_source=fd21aeae35ab0ec1535d36492bae2adc">黑马程序员Redis入门到实战教程,深度透析redis底层原理+redis分布式锁+企业解决方案+黑马点评实战项目</a>中的黑马点评项目,只写出一些关键的代码和一些编程中新颖想法以及常见问题的解决方案,方便读者重温学习思路,具体想要完成实战学习建议跟着视频学习!</font></p><h1 id="短信登录"><a href="#短信登录" class="headerlink" title="短信登录"></a>短信登录</h1><p>本章主要学习Redis的共享Session应用</p><h2 id="传统的基于Session的登录流程"><a href="#传统的基于Session的登录流程" class="headerlink" title="传统的基于Session的登录流程"></a>传统的基于Session的登录流程</h2><p><strong>发送验证码:</strong></p><p>用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号</p><p>如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户</p><p><strong>短信验证码登录、注册:</strong></p><p>用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息</p><p><strong>校验登录状态:</strong></p><p>用户在请求时候,会从cookie中携带者sessionId到后台,后台通过sessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到ThreadLocal中,并且放行</p><p><img src="/../images/redis/redis2.1/1.png" alt="1"></p><p>基于上述流程,写出对应的代码</p><h3 id="发送验证码"><a href="#发送验证码" class="headerlink" title="发送验证码"></a>发送验证码</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">sendCode</span><span class="params">(String phone, HttpSession session)</span> {</span><br><span class="line"> <span class="comment">// 1.校验手机号,校验是否不符合</span></span><br><span class="line"> <span class="keyword">if</span>(RegexUtils.isPhoneInvalid(phone)){</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"手机号格式错误!"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 2.生成验证码</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">code</span> <span class="operator">=</span> RandomUtil.randomNumbers(<span class="number">6</span>);</span><br><span class="line"> <span class="comment">// 3.保存验证码到Session</span></span><br><span class="line"> session.setAttribute(<span class="string">"code"</span>,code);</span><br><span class="line"> <span class="comment">// 4.发送验证码,虚假发送</span></span><br><span class="line"> log.debug(<span class="string">"验证码:"</span>+code);</span><br><span class="line"> <span class="keyword">return</span> Result.ok();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="登录功能"><a href="#登录功能" class="headerlink" title="登录功能"></a>登录功能</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">login</span><span class="params">(LoginFormDTO loginForm, HttpSession session)</span> {</span><br><span class="line"> <span class="comment">// 1.校验手机号</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">phone</span> <span class="operator">=</span> loginForm.getPhone();</span><br><span class="line"> <span class="keyword">if</span>(RegexUtils.isPhoneInvalid(phone)){</span><br><span class="line"> <span class="comment">// 2.如果不符合,返回错误信息</span></span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"手机号格式错误!"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 3.校验验证码</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">code</span> <span class="operator">=</span> loginForm.getCode();</span><br><span class="line"> <span class="type">Object</span> <span class="variable">cacheCode</span> <span class="operator">=</span> session.getAttribute(<span class="string">"code"</span>);</span><br><span class="line"> <span class="keyword">if</span>(cacheCode == <span class="literal">null</span> || !cacheCode.toString().equals(code)){</span><br><span class="line"> <span class="comment">// 4.不一致,报错</span></span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"验证码错误"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 5.一致,根据手机号查询用户</span></span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> query().eq(<span class="string">"phone"</span>, phone).one();</span><br><span class="line"> <span class="comment">// 6.判断用户是否存在</span></span><br><span class="line"> <span class="keyword">if</span>(user == <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// 7.用户不存在,创建新用户并保存到数据库</span></span><br><span class="line"> user = createUserWithPhone(phone);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 8.用户存在,将用户所需数据存到Session</span></span><br><span class="line"> <span class="type">UserDTO</span> <span class="variable">userDTO</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UserDTO</span>();</span><br><span class="line"> BeanUtils.copyProperties(user,userDTO);</span><br><span class="line"> session.setAttribute(<span class="string">"user"</span>,userDTO);</span><br><span class="line"> <span class="keyword">return</span> Result.ok();</span><br><span class="line">}</span><br><span class="line"><span class="keyword">private</span> User <span class="title function_">createUserWithPhone</span><span class="params">(String phone)</span> {</span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> User.builder()</span><br><span class="line"> .phone(phone)</span><br><span class="line"> .nickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(<span class="number">10</span>))</span><br><span class="line"> .build();</span><br><span class="line"> <span class="comment">// 将新用户存入数据库</span></span><br><span class="line"> save(user);</span><br><span class="line"> <span class="keyword">return</span> user;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="登录拦截功能"><a href="#登录拦截功能" class="headerlink" title="登录拦截功能"></a>登录拦截功能</h3><p><img src="/../images/redis/redis2.1/2.png" alt="2"></p><p>手动创建一个LoginInterceptor拦截器,判断Session中是否有user用户,从而达到拦截的目的</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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LoginInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="comment">// 1.获取Session中的用户</span></span><br><span class="line"> <span class="type">HttpSession</span> <span class="variable">session</span> <span class="operator">=</span> request.getSession();</span><br><span class="line"> <span class="comment">// 2.判断用户是否存在</span></span><br><span class="line"> <span class="type">Object</span> <span class="variable">user</span> <span class="operator">=</span> session.getAttribute(<span class="string">"user"</span>);</span><br><span class="line"> <span class="keyword">if</span>(user == <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// 3.用户不存在,拦截并返回401状态码</span></span><br><span class="line"> response.setStatus(<span class="number">401</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 4.用户存在,保存用户信息到ThreadLocal</span></span><br><span class="line"> UserHolder.saveUser((UserDTO) user);</span><br><span class="line"> <span class="comment">// 5.放行</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">afterCompletion</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="comment">//移除用户</span></span><br><span class="line"> UserHolder.removeUser();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中使用到了TheadLocal来存储已登录用户的信息,每个用户访问程序的时候,都会生成对应的线程,使用TheadLocal可以做到线程隔离,将信息存到自己的线程当中,每个线程操作自己的一份数据!</p><p>下面的UserHolder类就方便快捷地实现了TheadLocal中值的存取和销毁:</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="keyword">class</span> <span class="title class_">UserHolder</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> ThreadLocal<UserDTO> tl = <span class="keyword">new</span> <span class="title class_">ThreadLocal</span><>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">saveUser</span><span class="params">(UserDTO user)</span>{</span><br><span class="line"> tl.set(user);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> UserDTO <span class="title function_">getUser</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">return</span> tl.get();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">removeUser</span><span class="params">()</span>{</span><br><span class="line"> tl.remove();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注册拦截器,使拦截器生效,创建MvcConfig类实现WebMvcConfigurer接口,需使用@Configuration注解</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="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MvcConfig</span> <span class="keyword">implements</span> <span class="title class_">WebMvcConfigurer</span> {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> {</span><br><span class="line"> registry.addInterceptor(<span class="keyword">new</span> <span class="title class_">LoginInterceptor</span>()).excludePathPatterns(</span><br><span class="line"> <span class="string">"/user/code"</span>,</span><br><span class="line"> <span class="string">"/user/login"</span>,</span><br><span class="line"> <span class="string">"/shop/**"</span>,</span><br><span class="line"> <span class="string">"/voucher/**"</span>,</span><br><span class="line"> <span class="string">"/shop-type"</span>,</span><br><span class="line"> <span class="string">"/upload/**"</span>,</span><br><span class="line"> <span class="string">"/blog/hot"</span></span><br><span class="line"> );</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="基于Redis实现共享Session登录"><a href="#基于Redis实现共享Session登录" class="headerlink" title="基于Redis实现共享Session登录"></a>基于Redis实现共享Session登录</h2><h3 id="Session共享问题"><a href="#Session共享问题" class="headerlink" title="Session共享问题"></a>Session共享问题</h3><p><strong>多台Tomcat并不共享Session存储空间,当请求切换到不同Tomcat服务器时导致数据丢失的问题</strong></p><p>分析:在集群分布模型当中,每个Tomcat中都有一份属于自己的Session,他们之间的Session是不共享的。在基于Session的登录流程中,当你第一次访问请求分到了一号Tomcat,而第二次访问请求分到了二号Tomcat,那么就可能出现了登录拦截错误的问题,因为第二台Tomcat没有第一台Tomcat中的Session。</p><p><img src="/../images/redis/redis2.1/3.png" alt="3"></p><h3 id="基于Redis实现共享Session的值存储和访问流程"><a href="#基于Redis实现共享Session的值存储和访问流程" class="headerlink" title="基于Redis实现共享Session的值存储和访问流程"></a>基于Redis实现共享Session的值存储和访问流程</h3><p>Key的使用分析:Key要有一致性,不可与其他业务的key重合,所以要加上业务的唯一前缀</p><p>发送验证码功能当中,我们可以使用手机号作为Key,验证码作为value来实现Redis的存储</p><p>登录功能当中,想要存储用户的登录信息,方便后面的业务需要,这时使用手机号并不合适,建议使用UUID生成随机token,值使用Hash存储对象,当然也可以转为Json存String类型的Value</p><p>我们不直接用手机号存储用户对象?</p><p>token不像session,Tomcat不会自动把token写在cookie中,需要我们手动返回token给前端,前端再通过代码保存到浏览器中,访问时在请求头authorization中携带token到后端。所以我们不应该使用手机号这样涉及隐私的数据传到前端保存到浏览器本地中,会有泄露的风险。</p><p><img src="/../images/redis/redis2.1/4.png" alt="4"></p><h3 id="业务逻辑代码修改优化"><a href="#业务逻辑代码修改优化" class="headerlink" title="业务逻辑代码修改优化"></a>业务逻辑代码修改优化</h3><p>存验证码使用String-String即可,存储对象建议使用String-Hash,可对对象的每个字段单独存储,可以针对单个字段做CRUD,并且占用内存更少(如果是转为Json格式使用String-String存储的话,格式会占用更多内存)。</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">login</span><span class="params">(LoginFormDTO loginForm, HttpSession session)</span> {</span><br><span class="line"> <span class="comment">// 1.校验手机号</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">phone</span> <span class="operator">=</span> loginForm.getPhone();</span><br><span class="line"> <span class="keyword">if</span>(RegexUtils.isPhoneInvalid(phone)){</span><br><span class="line"> <span class="comment">// 2.如果不符合,返回错误信息</span></span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"手机号格式错误!"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 3.从Redis中取出验证码和前端传来的验证码校验</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">code</span> <span class="operator">=</span> loginForm.getCode();</span><br><span class="line"> <span class="type">String</span> <span class="variable">cacheCode</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);</span><br><span class="line"> <span class="keyword">if</span>(cacheCode == <span class="literal">null</span> || !cacheCode.equals(code)){</span><br><span class="line"> <span class="comment">// 4.不一致,报错</span></span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"验证码错误"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 5.一致,根据手机号查询用户</span></span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> query().eq(<span class="string">"phone"</span>, phone).one();</span><br><span class="line"> <span class="comment">// 6.判断用户是否存在</span></span><br><span class="line"> <span class="keyword">if</span>(user == <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// 7.用户不存在,创建新用户并保存到数据库</span></span><br><span class="line"> user = createUserWithPhone(phone);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 8.用户存在,将用户所有数据存到Redis</span></span><br><span class="line"> <span class="comment">// 8.1 随机生成token</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> UUID.randomUUID().toString();</span><br><span class="line"> <span class="comment">// 8.2 将用户数据转为HashMap进行存储</span></span><br><span class="line"> <span class="type">UserDTO</span> <span class="variable">userDTO</span> <span class="operator">=</span> BeanUtil.copyProperties(user, UserDTO.class);</span><br><span class="line"> Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, <span class="keyword">new</span> <span class="title class_">HashMap</span><>(),</span><br><span class="line"> CopyOptions.create().</span><br><span class="line"> setIgnoreNullValue(<span class="literal">true</span>).</span><br><span class="line"> setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));</span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> RedisConstants.LOGIN_USER_KEY + token;</span><br><span class="line"> <span class="comment">// 8.3存入Redis并设置过期时间</span></span><br><span class="line"> stringRedisTemplate.opsForHash().putAll(key,userMap);</span><br><span class="line"> stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);</span><br><span class="line"> <span class="comment">// 9.返回token</span></span><br><span class="line"> <span class="keyword">return</span> Result.ok(token);</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></pre></td><td class="code"><pre><span class="line"><span class="type">UserDTO</span> <span class="variable">userDTO</span> <span class="operator">=</span> BeanUtil.copyProperties(user, UserDTO.class);</span><br><span class="line"> Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, <span class="keyword">new</span> <span class="title class_">HashMap</span><>(),</span><br><span class="line"> CopyOptions.create().</span><br><span class="line"> setIgnoreNullValue(<span class="literal">true</span>).</span><br><span class="line"> setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));</span><br></pre></td></tr></table></figure><p><strong>该函数将userDTO对象转换为一个Map<String, Object>对象。在转换过程中,使用了BeanUtil.beanToMap方法,并传入了三个参数:userDTO对象本身、一个空的HashMap对象和一个CopyOptions对象。</strong></p><ul><li>userDTO对象是要转换的对象。</li><li>空的HashMap对象用于存储转换后的键值对。</li><li>CopyOptions对象用于设置转换过程中的选项。在这里,设置了两个选项:<ul><li>setIgnoreNullValue(true)表示忽略userDTO对象中值为null的属性,不会将其转换为Map中的键值对。</li><li>setFieldValueEditor用于设置一个字段值编辑器,该编辑器将每个属性的值转换为字符串类型。在这里,使用了一个lambda表达式(fieldName, fieldValue) -> fieldValue.toString(),表示将每个属性的值调用toString()方法后作为键值对的值<strong>(因为使用的是StringRedisTemplate,value的值必须为String,否则报错)</strong>。</li></ul></li></ul><p>简而言之就是将userDTO对象转为HashMap,但是转换后value中存在非String类型的值,所以构建了一个空的HashMap对象,用于修改后的拷贝。</p><h3 id="拦截器代码的优化代码"><a href="#拦截器代码的优化代码" class="headerlink" title="拦截器代码的优化代码"></a>拦截器代码的优化代码</h3><p>从前端的请求头中获取token,根据有效token来查询Redis得到用户数据存入TheadLocal中</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="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="comment">// 1.请求头中的token</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeader(<span class="string">"authorization"</span>);</span><br><span class="line"> <span class="keyword">if</span>(StrUtil.isBlank(token)){</span><br><span class="line"> response.setStatus(<span class="number">401</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 2.判断用户是否在Redis中</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> RedisConstants.LOGIN_USER_KEY + token;</span><br><span class="line"> Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);</span><br><span class="line"> <span class="keyword">if</span>(userMap.isEmpty()){</span><br><span class="line"> <span class="comment">// 3.用户不存在,拦截并返回401状态码</span></span><br><span class="line"> response.setStatus(<span class="number">401</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 3.将查到的Hash数据转为UserDTO对象</span></span><br><span class="line"> <span class="type">UserDTO</span> <span class="variable">userDTO</span> <span class="operator">=</span> BeanUtil.fillBeanWithMap(userMap, <span class="keyword">new</span> <span class="title class_">UserDTO</span>(), <span class="literal">false</span>);</span><br><span class="line"> <span class="comment">// 4.用户存在,保存用户信息到ThreadLocal</span></span><br><span class="line"> UserHolder.saveUser(userDTO);</span><br><span class="line"> <span class="comment">// 5.刷新token有效期</span></span><br><span class="line"> stringRedisTemplate.expire(key,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>使用BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false) 来将一个Map转换回对应的对象</strong></p><h3 id="拦截器的进一步优化"><a href="#拦截器的进一步优化" class="headerlink" title="拦截器的进一步优化"></a>拦截器的进一步优化</h3><p>原拦截器不是拦截一切路径,所以会导致访问不需拦截的路径时,不会刷新token的时间,会出现用户一直访问的是不拦截的网页时,虽然一直在浏览网页,但是还要重新登录的情况。</p><p>得新加一个全路径拦截器。全路径拦截器只判断Redis中是否存在用户,存在即写入TheadLocal并刷新时间,不存在则放行到下一个拦截器再一并拦截。</p><p><strong>Refresh拦截器:</strong></p><ul><li>获取<code>token</code></li><li>查询Redis中是否有该用户</li><li>无则放行到下一个Login拦截器一并拦截</li><li>有则存入<code>TheadLocal</code>,并刷新<code>token</code>有效期,放行到下一个Login拦截器</li></ul><p><strong>Login拦截器:</strong></p><ul><li>查询<code>TheadLocal</code>中是否有用户</li><li>无则拦截,有则放行</li></ul><p><strong>RefreshTokenInterceptor</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><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Author</span> JunWei Li</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Date</span> 2024-07-17 15:37</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RefreshTokenInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> {</span><br><span class="line"> <span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">RefreshTokenInterceptor</span><span class="params">(StringRedisTemplate stringRedisTemplate)</span> {</span><br><span class="line"> <span class="built_in">this</span>.stringRedisTemplate = stringRedisTemplate;</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="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="comment">// 1.请求头中的token</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeader(<span class="string">"authorization"</span>);</span><br><span class="line"> <span class="keyword">if</span>(StrUtil.isBlank(token)){</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 2.判断用户是否在Redis中</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> RedisConstants.LOGIN_USER_KEY + token;</span><br><span class="line"> Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);</span><br><span class="line"> <span class="keyword">if</span>(userMap.isEmpty()){</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 3.将查到的Hash数据转为UserDTO对象</span></span><br><span class="line"> <span class="type">UserDTO</span> <span class="variable">userDTO</span> <span class="operator">=</span> BeanUtil.fillBeanWithMap(userMap, <span class="keyword">new</span> <span class="title class_">UserDTO</span>(), <span class="literal">false</span>);</span><br><span class="line"> <span class="comment">// 4.用户存在,保存用户信息到ThreadLocal</span></span><br><span class="line"> UserHolder.saveUser(userDTO);</span><br><span class="line"> <span class="comment">// 5.刷新token有效期</span></span><br><span class="line"> stringRedisTemplate.expire(key,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">afterCompletion</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="comment">//移除用户</span></span><br><span class="line"> UserHolder.removeUser();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>LoginInterceptor</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="comment">// 1.判断ThreadLocal中是否有用户</span></span><br><span class="line"> <span class="type">UserDTO</span> <span class="variable">userDTO</span> <span class="operator">=</span> UserHolder.getUser();</span><br><span class="line"> <span class="keyword">if</span>(userDTO == <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// 无则拦截</span></span><br><span class="line"> response.setStatus(<span class="number">401</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</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="literal">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>到<code>MvcConfig</code>中注册新的拦截器,<strong>通过order()来对拦截器进行优先级排序</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">registry.addInterceptor(<span class="keyword">new</span> <span class="title class_">RefreshTokenInterceptor</span>(stringRedisTemplate)).addPathPatterns(<span class="string">"/**"</span>).order(<span class="number">0</span>);</span><br></pre></td></tr></table></figure><p>至此,短信登录登陆功能全部设计完成!!!</p><h1 id="商品查询缓存"><a href="#商品查询缓存" class="headerlink" title="商品查询缓存"></a>商品查询缓存</h1><p>这一章主要是企业的缓存使用技巧,涉及缓存穿透、缓存雪崩、缓存击穿等问题的解决</p><h2 id="什么是缓存"><a href="#什么是缓存" class="headerlink" title="什么是缓存"></a>什么是缓存</h2><p><strong>有高性能的地方就有缓存</strong></p><p><strong>缓存(<strong>Cache),就是数据交换的</strong>缓冲区</strong>,俗称的缓存就是<strong>缓冲区内的数据</strong>,一般从数据库中获取,存储于本地代码</p><p><img src="/../images/redis/redis2.1/5.png" alt="5"></p><p><strong>数据一致性成本:</strong>读取数据优先从缓存读取数据,所以当数据库中的数据被修改后,缓存必须删除,否则缓存中的数据和数据库中的数据会产生不一致的效果。</p><h3 id="为什么需要缓存"><a href="#为什么需要缓存" class="headerlink" title="为什么需要缓存"></a>为什么需要缓存</h3><p>缓存数据存储于代码中,而代码运行在内存中,<strong>内存的读写性能远高于磁盘</strong>,缓存可以大大降低<strong>用户访问并发量带来的</strong>服务器读写压力</p><p>实际开发过程中,企业的数据量,少则几十万,多则几千万,这么大数据量,<strong>如果没有缓存来作为”避震器”,系统是几乎撑不住的</strong>,所以企业会大量运用到缓存技术。</p><h3 id="多级缓存"><a href="#多级缓存" class="headerlink" title="多级缓存"></a>多级缓存</h3><p>实际开发中,会构筑多级缓存来使系统运行速度进一步提升</p><p><strong>浏览器缓存</strong>:主要是存在于浏览器端的缓存</p><p><strong>应用层缓存:</strong>可以分为tomcat本地缓存,比如之前提到的map,或者是使用redis作为缓存</p><p><strong>数据库缓存:</strong>在数据库中有一片空间是 buffer pool,增改查数据都会先加载到mysql的缓存中</p><p><strong>CPU缓存:</strong>当代计算机最大的问题是 cpu性能提升了,但内存读写速度没有跟上,所以为了适应当下的情况,增加了cpu的L1,L2,L3级的缓存</p><h2 id="添加店铺信息缓存到Redis"><a href="#添加店铺信息缓存到Redis" class="headerlink" title="添加店铺信息缓存到Redis"></a>添加店铺信息缓存到Redis</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryById</span><span class="params">(Long id)</span> {</span><br><span class="line"> <span class="comment">// 1.从Redis中查询缓存</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">shopJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);</span><br><span class="line"> <span class="comment">// 2.有缓存,直接返回</span></span><br><span class="line"> <span class="keyword">if</span>(StrUtil.isNotBlank(shopJson)){</span><br><span class="line"> <span class="comment">// 转为Bean返回</span></span><br><span class="line"> <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> JSONUtil.toBean(shopJson, Shop.class);</span><br><span class="line"> <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 3.没有缓存,转向查数据库</span></span><br><span class="line"> <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> getById(id);</span><br><span class="line"> <span class="comment">// 4.数据库中没有,返回错误</span></span><br><span class="line"> <span class="keyword">if</span>(shop ==<span class="literal">null</span>){</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"店铺不存在"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 5.有则返回,并存入Redis</span></span><br><span class="line"> <span class="comment">// 将对象转为Json</span></span><br><span class="line"> stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop));</span><br><span class="line"> <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>使用hutool提供的JSONUtil的toJsonStr()和toBean()方法,使得Json和Bean对象互相转换</strong></p><p>刷新前端页面,可以在Navicat中才看到对应的商店Cache数据</p><p><img src="/../images/redis/redis2.1/6.png" alt="6"></p><h2 id="添加店铺类型缓存到Redis"><a href="#添加店铺类型缓存到Redis" class="headerlink" title="添加店铺类型缓存到Redis"></a>添加店铺类型缓存到Redis</h2><p>对店铺类型进行缓存</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> List<ShopType> <span class="title function_">queryTypeList</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 1.从Redis中查询缓存</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">shopTypeJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_TYPE_KEY);</span><br><span class="line"> <span class="comment">// 2.有缓存,直接返回</span></span><br><span class="line"> <span class="keyword">if</span>(StrUtil.isNotBlank(shopTypeJson)){</span><br><span class="line"> <span class="comment">// 转为List集合</span></span><br><span class="line"> <span class="keyword">return</span> JSONUtil.toList(shopTypeJson, ShopType.class);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 3.没有缓存,转向查数据库</span></span><br><span class="line"> List<ShopType> shopTypeList = query().orderByAsc(<span class="string">"sort"</span>).list();</span><br><span class="line"> <span class="comment">// 4.返回数据,并存入Redis</span></span><br><span class="line"> <span class="comment">// 将对象转为Json</span></span><br><span class="line"> stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_TYPE_KEY, JSONUtil.toJsonStr(shopTypeList));</span><br><span class="line"> <span class="keyword">return</span> shopTypeList;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><img src="/../images/redis/redis2.1/7.png" alt="7"></p><h2 id="缓存更新策略"><a href="#缓存更新策略" class="headerlink" title="缓存更新策略"></a>缓存更新策略</h2><p><strong>缓存更新</strong>:Redis数据存储在内存中,不可能无限制存储数据,那么就要缓存更新</p><ul><li><p><strong>内存淘汰:</strong>redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)</p></li><li><p><strong>超时剔除:</strong>当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存</p></li><li><p><strong>主动更新:</strong>我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题<br><img src="/../images/redis/redis2.1/8.png" alt="8"></p></li></ul><p><strong>我们主要学习使用主动更新,一致性非常好,但是维护成本比较高,需要我们人为编码进行更新</strong></p><h3 id="数据库缓存不一致解决方案"><a href="#数据库缓存不一致解决方案" class="headerlink" title="数据库缓存不一致解决方案"></a>数据库缓存不一致解决方案</h3><p>由于我们的<strong>缓存的数据源来自于数据库</strong>,而数据库的<strong>数据是会发生变化的</strong>,因此,如果当数据库中<strong>数据发生变化,而缓存却没有同步</strong>,此时就会有<strong>一致性问题存在</strong>,其后果是:用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等</p><p>有如下几种方案解决:</p><p><img src="/../images/redis/redis2.1/9.png" alt="9"></p><p><strong>我们主要是用第一种方式(Cache Aside Pattern 人工编码方式),也就是主动更新,后两种方式更为复杂,涉及知识更多</strong></p><p>这里有两个问题:</p><ul><li><strong>删除缓存还是更新缓存?</strong><ul><li>更新缓存:每次更新数据库都更新缓存,无效写操作较多</li><li>删除缓存:更新数据库时让缓存失效,查询时再更新缓存</li></ul></li><li>如何保证缓存与数据库的操作的同时成功或失败?<ul><li>单体系统,将缓存与数据库操作放在一个事务</li><li>分布式系统,利用TCC等分布式事务方案</li></ul></li><li><strong>先操作缓存还是先操作数据库</strong>?<ul><li>先删除缓存,再操作数据库</li><li>先操作数据库,再删除缓存</li></ul></li></ul><ul><li><p><strong>对于第一个问题:</strong>我们<strong>采取删除缓存的方式</strong>。当用户在不断修改数据库的时候,如果采用更新缓存的方式,那么会产生很多次无效的更新缓存,只有最后一次有效。那么最好的方法就是删除缓存的方式,因为他只在下个人访问的时候再重新从数据库中读出数据写入缓存。</p></li><li><p>对于第二个问题已给出解决方案</p></li><li><p><strong>对于第三个问题:</strong>我们应当是<strong>先操作数据库,再删除缓存</strong>。原因在于,如果你选择第一种方案,在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入缓存的就是旧的数据,新的数据被旧数据覆盖了,造成了缓存和数据库不一致现象。如黑马画的下图所示:</p></li></ul><p><img src="/../images/redis/redis2.1/10.png" alt="10"></p><h3 id="代码实现商铺和缓存的读写一致"><a href="#代码实现商铺和缓存的读写一致" class="headerlink" title="代码实现商铺和缓存的读写一致"></a>代码实现商铺和缓存的读写一致</h3><ul><li>给Cache加TTL,假定为30min</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop));</span><br><span class="line">stringRedisTemplate.expire(RedisConstants.CACHE_SHOP_KEY + id,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br></pre></td></tr></table></figure><ul><li>修改店铺数据时,先更新数据库,再删除缓存,注意使用<code>@Transactional</code>注解</li></ul><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="meta">@Override</span></span><br><span class="line"><span class="meta">@Transactional</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">update</span><span class="params">(Shop shop)</span> {</span><br><span class="line"> <span class="type">Long</span> <span class="variable">id</span> <span class="operator">=</span> shop.getId();</span><br><span class="line"> <span class="keyword">if</span>(id == <span class="literal">null</span>){</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"店铺id不可为空"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 1.更新数据库</span></span><br><span class="line"> updateById(shop);</span><br><span class="line"> <span class="comment">// 2.删除缓存</span></span><br><span class="line"> stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);</span><br><span class="line"> <span class="keyword">return</span> Result.ok();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="缓存穿透"><a href="#缓存穿透" class="headerlink" title="缓存穿透"></a>缓存穿透</h2><p><strong>缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。</strong></p><p><strong>当碰到大量恶意查询缓存和数据库都没有的数据时,所有请求打到数据库,容易搞垮数据库</strong></p><p>常见的解决方案有两种:</p><ul><li><p>缓存空对象</p><ul><li>优点:实现简单,维护方便</li><li>缺点:<ul><li>额外的内存消耗</li><li>可能造成短期的不一致</li></ul></li></ul></li><li><p>布隆过滤</p><ul><li><p>优点:内存占用较少,没有多余key</p></li><li><p>缺点:</p><ul><li><p>实现复杂</p></li><li><p>存在误判可能</p></li></ul></li></ul></li></ul><p><strong>缓存空对象思路分析:</strong>当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了</p><p><strong>布隆过滤:</strong>布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,</p><p>假设布隆过滤器判断这个数据不存在,则直接返回</p><p>这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突</p><p><img src="/../images/redis/redis2.1/11.png" alt="11"></p><h3 id="编码解决缓存穿透问题"><a href="#编码解决缓存穿透问题" class="headerlink" title="编码解决缓存穿透问题"></a>编码解决缓存穿透问题</h3><p>在原先逻辑中,没有数据则返回错误信息,而不采取措施防御,会存在缓存穿透问题</p><p>现在我们要做的是,在缓存和数据库都没有该数据的情况下,<strong>给Redis写入一个缓存空对象</strong>,值为null,使得下一次同样的请求会命中null,<strong>直接返回空对象</strong>,而<strong>不让其到达数据库</strong></p><p><img src="/../images/redis/redis2.1/12.png" alt="12"></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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 2.有缓存,直接返回</span></span><br><span class="line"><span class="keyword">if</span>(StrUtil.isNotBlank(shopJson)){</span><br><span class="line"> <span class="comment">// 转为Bean返回</span></span><br><span class="line"> <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> JSONUtil.toBean(shopJson, Shop.class);</span><br><span class="line"> <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// ******多加一步 判断Cache是否为""******</span></span><br><span class="line"><span class="keyword">if</span>(<span class="string">""</span>.equals(shopJson)){</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"店铺数据不存在!"</span>);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 3.没有缓存,转向查数据库</span></span><br><span class="line"><span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> getById(id);</span><br><span class="line"><span class="comment">// 4.数据库中没有,返回错误</span></span><br><span class="line"><span class="keyword">if</span>(shop == <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// ******多加一步 写入缓存空对象******</span></span><br><span class="line"> stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,<span class="string">""</span>,RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"店铺不存在"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>由于StrUtil.isNotBlank在值为null时返回的是false,所以要单独加判断null值</strong>。同时写入缓存空对象,当查询一个不存在的店铺id的时候,会在Redis中存储空值,在设定的时间内不再经过关系型数据库读写,不给其产生压力</p><p><img src="/../images/redis/redis2.1/13.png" alt="13"></p><h3 id="缓存穿透产生的原因是什么?"><a href="#缓存穿透产生的原因是什么?" class="headerlink" title="缓存穿透产生的原因是什么?"></a>缓存穿透产生的原因是什么?</h3><ul><li>用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力</li></ul><h3 id="缓存穿透的解决方案有哪些?"><a href="#缓存穿透的解决方案有哪些?" class="headerlink" title="缓存穿透的解决方案有哪些?"></a>缓存穿透的解决方案有哪些?</h3><ul><li>缓存null值</li><li>布隆过滤</li><li>增强id的复杂度,避免被猜测id规律(强格式,有一定自己的逻辑)</li><li>做好数据的基础格式校验</li><li>加强用户权限校验</li><li>做好热点参数的限流</li></ul><h2 id="缓存雪崩"><a href="#缓存雪崩" class="headerlink" title="缓存雪崩"></a>缓存雪崩</h2><p><strong>缓存雪崩:在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。</strong></p><p>解决方案:</p><ul><li><p>给不同的Key的TTL添加随机值</p></li><li><p>利用Redis集群提高服务的可用性</p></li><li><p>给缓存业务添加降级限流策略</p></li><li><p>给业务添加多级缓存</p></li></ul><p><img src="/../images/redis/redis2.1/14.png" alt="14"></p><p><strong>由于缓存雪崩涉及了微服务的内容和集群分布等内容,在这里了解就好,目前已有知识暂时无法实现,还得再去多补充微服务知识</strong></p><p>推荐学习<a href="https://www.bilibili.com/video/BV1S142197x7/?vd_source=414786025eeb24c0f40ed9fedc2e4eb0">2024最新SpringCloud微服务开发与实战,java黑马商城项目微服务实战开发(涵盖MybatisPlus、Docker、MQ、ES、Redis高级等)_哔哩哔哩_bilibili</a></p><h2 id="缓存击穿"><a href="#缓存击穿" class="headerlink" title="缓存击穿"></a>缓存击穿</h2><p><strong>缓存击穿问题</strong>也叫<strong>热点Key问题</strong>,就是一个被<strong>高并发访问</strong>并且<strong>缓存重建业务较复杂</strong>的<strong>key突然失效了</strong>,<strong>无数的请求访问会在瞬间给数据库带来巨大的冲击</strong>。</p><p>常见的解决方案有两种:</p><ul><li><strong>互斥锁</strong></li><li><strong>逻辑过期</strong></li></ul><p>逻辑分析:</p><p>假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了</p><p>但是假设在<strong>线程1没有走完的时候</strong>,后续的线程2,线程3,线程4<strong>同时过来访问当前这个方法</strong>, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着<strong>同一时间去访问数据库</strong>,<strong>同时的去执行数据库代码</strong>,<strong>对数据库访问压力过大</strong></p><p><strong>高并发 && 缓存重建业务较复杂(时间长)</strong></p><p><img src="/../images/redis/redis2.1/15.png" alt="15"></p><h3 id="两种解决方法及其对比"><a href="#两种解决方法及其对比" class="headerlink" title="两种解决方法及其对比"></a>两种解决方法及其对比</h3><h4 id="解决方法一、使用互斥锁解决"><a href="#解决方法一、使用互斥锁解决" class="headerlink" title="解决方法一、使用互斥锁解决"></a><strong>解决方法一、使用互斥锁解决</strong></h4><p>因为锁能实现互斥性,使得只有拿到锁的那个线程可以访问数据库,避免所有线程都查询数据库,但是也影响了查询性能,查询的性能从并行变成了串行。</p><p>假设现在线程1过来访问,他查询缓存没有命中,但是此时他<strong>获得到了锁的资源</strong>,那么<strong>线程1就会一个人去执行逻辑</strong>,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么<strong>线程2就可以进行到休眠</strong>,<strong>直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑</strong>,此时就能够从缓存中拿到数据了。</p><p><img src="/../images/redis/redis2.1/16.png" alt="16"></p><h4 id="解决方案二、逻辑过期方案"><a href="#解决方案二、逻辑过期方案" class="headerlink" title="解决方案二、逻辑过期方案"></a>解决方案二、逻辑过期方案</h4><p>方案分析:我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。</p><p>我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。</p><p>这种方案巧妙在于,<strong>异步的构建缓存</strong></p><p>缺点在于<strong>在构建完缓存之前,返回的都是脏数据</strong></p><p><img src="/../images/redis/redis2.1/17.png" alt="17"></p><h4 id="两种方法的对比"><a href="#两种方法的对比" class="headerlink" title="两种方法的对比"></a>两种方法的对比</h4><p><strong>互斥锁方案:</strong>由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以<strong>没有额外的内存消耗</strong>,缺点在于<strong>有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响</strong></p><p><strong>逻辑过期方案:</strong> 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦</p><p><img src="/../images/redis/redis2.1/18.png" alt="18"></p><h3 id="利用互斥锁解决缓存击穿问题"><a href="#利用互斥锁解决缓存击穿问题" class="headerlink" title="利用互斥锁解决缓存击穿问题"></a>利用互斥锁解决缓存击穿问题</h3><p><img src="/../images/redis/redis2.1/19.png" alt="19"></p><p>我们来手动实现上锁和解锁函数,主要利用的是Redis的setnx方法来表示获取锁,他只在值不存在的时候才能修改并返回1(被Spring封装成了Boolean值true),这样如果返回true则证明该线程得到了锁,false则证明有线程已经拿到了锁,其他线程只能等待</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(String key)</span>{</span><br><span class="line"> <span class="comment">// 如果不存在锁,则返回值为true,可获得锁</span></span><br><span class="line"> <span class="type">Boolean</span> <span class="variable">flag</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().setIfAbsent(key, <span class="string">"1"</span>, <span class="number">10</span>, TimeUnit.SECONDS);</span><br><span class="line"> <span class="keyword">return</span> BooleanUtil.isTrue(flag);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">(String key)</span>{</span><br><span class="line"> <span class="comment">// 释放锁</span></span><br><span class="line"> stringRedisTemplate.delete(key);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>修改queryById方法,将互斥锁解决缓存击穿封装成queryWithMutex函数</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Result <span class="title function_">queryById</span><span class="params">(Long id)</span> {</span><br><span class="line"> <span class="comment">// 互斥锁解决缓存击穿</span></span><br><span class="line"> <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> queryWithMutex(id);</span><br><span class="line"> <span class="keyword">if</span>(shop == <span class="literal">null</span>){</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"店铺不存在"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> Result.ok(shop);</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Shop <span class="title function_">queryWithMutex</span><span class="params">(Long id)</span>{</span><br><span class="line"> <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> <span class="string">"lock:shop:"</span> + id;</span><br><span class="line"> <span class="comment">// 1.从Redis中查询缓存</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">shopJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);</span><br><span class="line"> <span class="comment">// 2.有缓存,直接返回</span></span><br><span class="line"> <span class="keyword">if</span>(StrUtil.isNotBlank(shopJson)){</span><br><span class="line"> <span class="comment">// 转为Bean返回</span></span><br><span class="line"> <span class="keyword">return</span> JSONUtil.toBean(shopJson, Shop.class);</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">""</span>.equals(shopJson)){</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 3.互斥锁解决缓存击穿</span></span><br><span class="line"> <span class="comment">// 3.1.实现缓存重建,获取互斥锁</span></span><br><span class="line"> <span class="comment">// 3.2.判断是否获取锁成功</span></span><br><span class="line"> <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span>(!tryLock(lockKey)){</span><br><span class="line"> <span class="comment">// 3.3.失败,休眠并重试</span></span><br><span class="line"> Thread.sleep(<span class="number">50</span>);</span><br><span class="line"> <span class="keyword">return</span> queryWithMutex(id);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 3.4.成功,没有缓存,转向查数据库</span></span><br><span class="line"> shop = getById(id);</span><br><span class="line"> <span class="comment">// 5.数据库中没有,返回错误</span></span><br><span class="line"> <span class="keyword">if</span>(shop == <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// 缓存穿透:写入缓存空对象</span></span><br><span class="line"> stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,<span class="string">""</span>,RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 6.将对象转为Json存入Redis</span></span><br><span class="line"> stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br><span class="line"> } <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> <span class="comment">// 7.释放互斥锁</span></span><br><span class="line"> unlock(lockKey);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 8.返回</span></span><br><span class="line"> <span class="keyword">return</span> shop;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在拿到锁且没有缓存需要查询数据库时加入缓存重建延迟,使用Apache Jmeter模拟高并发的情况,加入缓存重建延迟,模拟缓存重建业务较复杂情况</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="comment">// 3.4.成功,没有缓存,转向查数据库</span></span><br><span class="line">shop = getById(id);</span><br><span class="line">Thread.sleep(<span class="number">200</span>);<span class="comment">//模拟缓存重建业务较复杂情况</span></span><br></pre></td></tr></table></figure><p><img src="/../images/redis/redis2.1/20.png" alt="20"></p><p>可以看到1000个http请求,但是只有一条查询数据库的记录,说明互斥锁生效确实解决了高并发下同时访问数据库的问题</p><p><img src="/../images/redis/redis2.1/21.png" alt="21"></p><h3 id="利用逻辑过期解决缓存击穿"><a href="#利用逻辑过期解决缓存击穿" class="headerlink" title="利用逻辑过期解决缓存击穿"></a>利用逻辑过期解决缓存击穿</h3><p>当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。</p><ul><li>我们假设缓存中一定会有数据,<strong>对热点数据已进行缓存预热</strong>,所以在查询Redis时,<strong>没命中直接返回空数据</strong></li><li>一旦命中后,由第一个<strong>检查到该逻辑过期时间已经过期的线程</strong>来<strong>开启一个新的线程</strong>去<strong>拿锁重构数据</strong>,在该线程重构数据完成之前,即<strong>其他无法获得锁的线程,只能返回旧数据(脏数据)</strong>,重构数据完成后即可全部返回新数据</li></ul><p><img src="/../images/redis/redis2.1/22.png" alt="22"></p><p>定义一个封装类,包含一个逻辑过期时间,并且包含想要操作的Bean对象:</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">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisData</span> {</span><br><span class="line"> <span class="keyword">private</span> LocalDateTime expireTime;</span><br><span class="line"> <span class="keyword">private</span> Object data;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>不添加TTL,封装一个逻辑过期时间:</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 class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">saveShop2Redis</span><span class="params">(Long id, Long expireSeconds)</span>{</span><br><span class="line"> <span class="comment">// 1.查询店铺信息</span></span><br><span class="line"> <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> getById(id);</span><br><span class="line"> <span class="comment">//Thread.sleep(200);模拟缓存重建业务较复杂情况所用时间</span></span><br><span class="line"> <span class="comment">// 2.封装逻辑过期时间</span></span><br><span class="line"> <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RedisData</span>();</span><br><span class="line"> redisData.setData(shop);</span><br><span class="line"> redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));</span><br><span class="line"> <span class="comment">// 3.写入Redis</span></span><br><span class="line"> stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(redisData));</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ExecutorService</span> <span class="variable">CACHE_REBUILD_EXECUTOR</span> <span class="operator">=</span> Executors.newFixedThreadPool(<span class="number">10</span>);</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Shop <span class="title function_">queryWithLogicalExpire</span><span class="params">( Long id )</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> RedisConstants.CACHE_SHOP_KEY + id;</span><br><span class="line"> <span class="comment">// 1.从redis查询商铺缓存</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line"> <span class="comment">// 2.判断是否存在</span></span><br><span class="line"> <span class="keyword">if</span> (StrUtil.isBlank(json)) {</span><br><span class="line"> <span class="comment">// 3.存在,直接返回</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 4.命中,需要先把json反序列化为对象</span></span><br><span class="line"> <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> JSONUtil.toBean(json, RedisData.class);</span><br><span class="line"> <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);</span><br><span class="line"> <span class="type">LocalDateTime</span> <span class="variable">expireTime</span> <span class="operator">=</span> redisData.getExpireTime();</span><br><span class="line"> <span class="comment">// 5.判断是否过期</span></span><br><span class="line"> <span class="keyword">if</span>(expireTime.isAfter(LocalDateTime.now())) {</span><br><span class="line"> <span class="comment">// 5.1.未过期,直接返回店铺信息</span></span><br><span class="line"> <span class="keyword">return</span> shop;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 5.2.已过期,需要缓存重建</span></span><br><span class="line"> <span class="comment">// 6.缓存重建</span></span><br><span class="line"> <span class="comment">// 6.1.获取互斥锁</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> RedisConstants.LOCK_SHOP_KEY + id;</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> tryLock(lockKey);</span><br><span class="line"> <span class="comment">// 6.2.判断是否获取锁成功</span></span><br><span class="line"> <span class="keyword">if</span> (isLock){</span><br><span class="line"> CACHE_REBUILD_EXECUTOR.submit( ()->{</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"> <span class="built_in">this</span>.saveShop2Redis(id,<span class="number">20L</span>);</span><br><span class="line"> }<span class="keyword">catch</span> (Exception e){</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line"> }<span class="keyword">finally</span> {</span><br><span class="line"> unlock(lockKey);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 6.4.返回过期的商铺信息</span></span><br><span class="line"> <span class="keyword">return</span> shop;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>手动修改数据库的值,把店铺id为1的名称改为10086茶餐厅,当前缓存还为100茶餐厅</p><p>使用Jmeter测试,可以看到,在200ms的延迟中,有一些数据还为旧数据,当延迟结束后重构数据后,才缓存入了新数据,获取到的自然也就是新数据</p><p><img src="/../images/redis/redis2.1/23.png" alt="23"></p><h2 id="封装Redis工具类"><a href="#封装Redis工具类" class="headerlink" title="封装Redis工具类"></a>封装Redis工具类</h2><ul><li><strong>这部分涉及的知识点太多,涉及到泛型,函数的封装等等,还未理解,需要慢慢消化,并且到了公司之后,都会有现成公司的工具类可供使用,理解即可</strong></li></ul><p>基于StringRedisTemplate封装一个缓存工具类,满足下列需求:</p><ul><li>方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间</li><li>方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓</li></ul><p>存击穿问题</p><ul><li>方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题</li><li>方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题</li></ul><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><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></pre></td><td class="code"><pre><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CacheClient</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ExecutorService</span> <span class="variable">CACHE_REBUILD_EXECUTOR</span> <span class="operator">=</span> Executors.newFixedThreadPool(<span class="number">10</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">CacheClient</span><span class="params">(StringRedisTemplate stringRedisTemplate)</span> {</span><br><span class="line"> <span class="built_in">this</span>.stringRedisTemplate = stringRedisTemplate;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">set</span><span class="params">(String key, Object value, Long time, TimeUnit unit)</span> {</span><br><span class="line"> stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setWithLogicalExpire</span><span class="params">(String key, Object value, Long time, TimeUnit unit)</span> {</span><br><span class="line"> <span class="comment">// 设置逻辑过期</span></span><br><span class="line"> <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RedisData</span>();</span><br><span class="line"> redisData.setData(value);</span><br><span class="line"> redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));</span><br><span class="line"> <span class="comment">// 写入Redis</span></span><br><span class="line"> stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <R,ID> R <span class="title function_">queryWithPassThrough</span><span class="params">(</span></span><br><span class="line"><span class="params"> String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit)</span>{</span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> keyPrefix + id;</span><br><span class="line"> <span class="comment">// 1.从redis查询商铺缓存</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line"> <span class="comment">// 2.判断是否存在</span></span><br><span class="line"> <span class="keyword">if</span> (StrUtil.isNotBlank(json)) {</span><br><span class="line"> <span class="comment">// 3.存在,直接返回</span></span><br><span class="line"> <span class="keyword">return</span> JSONUtil.toBean(json, type);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 判断命中的是否是空值</span></span><br><span class="line"> <span class="keyword">if</span> (json != <span class="literal">null</span>) {</span><br><span class="line"> <span class="comment">// 返回一个错误信息</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 4.不存在,根据id查询数据库</span></span><br><span class="line"> <span class="type">R</span> <span class="variable">r</span> <span class="operator">=</span> dbFallback.apply(id);</span><br><span class="line"> <span class="comment">// 5.不存在,返回错误</span></span><br><span class="line"> <span class="keyword">if</span> (r == <span class="literal">null</span>) {</span><br><span class="line"> <span class="comment">// 将空值写入redis</span></span><br><span class="line"> stringRedisTemplate.opsForValue().set(key, <span class="string">""</span>, CACHE_NULL_TTL, TimeUnit.MINUTES);</span><br><span class="line"> <span class="comment">// 返回错误信息</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 6.存在,写入redis</span></span><br><span class="line"> <span class="built_in">this</span>.set(key, r, time, unit);</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="keyword">public</span> <R, ID> R <span class="title function_">queryWithLogicalExpire</span><span class="params">(</span></span><br><span class="line"><span class="params"> String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> keyPrefix + id;</span><br><span class="line"> <span class="comment">// 1.从redis查询商铺缓存</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line"> <span class="comment">// 2.判断是否存在</span></span><br><span class="line"> <span class="keyword">if</span> (StrUtil.isBlank(json)) {</span><br><span class="line"> <span class="comment">// 3.存在,直接返回</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 4.命中,需要先把json反序列化为对象</span></span><br><span class="line"> <span class="type">RedisData</span> <span class="variable">redisData</span> <span class="operator">=</span> JSONUtil.toBean(json, RedisData.class);</span><br><span class="line"> <span class="type">R</span> <span class="variable">r</span> <span class="operator">=</span> JSONUtil.toBean((JSONObject) redisData.getData(), type);</span><br><span class="line"> <span class="type">LocalDateTime</span> <span class="variable">expireTime</span> <span class="operator">=</span> redisData.getExpireTime();</span><br><span class="line"> <span class="comment">// 5.判断是否过期</span></span><br><span class="line"> <span class="keyword">if</span>(expireTime.isAfter(LocalDateTime.now())) {</span><br><span class="line"> <span class="comment">// 5.1.未过期,直接返回店铺信息</span></span><br><span class="line"> <span class="keyword">return</span> r;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 5.2.已过期,需要缓存重建</span></span><br><span class="line"> <span class="comment">// 6.缓存重建</span></span><br><span class="line"> <span class="comment">// 6.1.获取互斥锁</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_SHOP_KEY + id;</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> tryLock(lockKey);</span><br><span class="line"> <span class="comment">// 6.2.判断是否获取锁成功</span></span><br><span class="line"> <span class="keyword">if</span> (isLock){</span><br><span class="line"> <span class="comment">// 6.3.成功,开启独立线程,实现缓存重建</span></span><br><span class="line"> CACHE_REBUILD_EXECUTOR.submit(() -> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 查询数据库</span></span><br><span class="line"> <span class="type">R</span> <span class="variable">newR</span> <span class="operator">=</span> dbFallback.apply(id);</span><br><span class="line"> <span class="comment">// 重建缓存</span></span><br><span class="line"> <span class="built_in">this</span>.setWithLogicalExpire(key, newR, time, unit);</span><br><span class="line"> } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line"> }<span class="keyword">finally</span> {</span><br><span class="line"> <span class="comment">// 释放锁</span></span><br><span class="line"> unlock(lockKey);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 6.4.返回过期的商铺信息</span></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="keyword">public</span> <R, ID> R <span class="title function_">queryWithMutex</span><span class="params">(</span></span><br><span class="line"><span class="params"> String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> keyPrefix + id;</span><br><span class="line"> <span class="comment">// 1.从redis查询商铺缓存</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">shopJson</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(key);</span><br><span class="line"> <span class="comment">// 2.判断是否存在</span></span><br><span class="line"> <span class="keyword">if</span> (StrUtil.isNotBlank(shopJson)) {</span><br><span class="line"> <span class="comment">// 3.存在,直接返回</span></span><br><span class="line"> <span class="keyword">return</span> JSONUtil.toBean(shopJson, type);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 判断命中的是否是空值</span></span><br><span class="line"> <span class="keyword">if</span> (shopJson != <span class="literal">null</span>) {</span><br><span class="line"> <span class="comment">// 返回一个错误信息</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 4.实现缓存重建</span></span><br><span class="line"> <span class="comment">// 4.1.获取互斥锁</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> LOCK_SHOP_KEY + id;</span><br><span class="line"> <span class="type">R</span> <span class="variable">r</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">isLock</span> <span class="operator">=</span> tryLock(lockKey);</span><br><span class="line"> <span class="comment">// 4.2.判断是否获取成功</span></span><br><span class="line"> <span class="keyword">if</span> (!isLock) {</span><br><span class="line"> <span class="comment">// 4.3.获取锁失败,休眠并重试</span></span><br><span class="line"> Thread.sleep(<span class="number">50</span>);</span><br><span class="line"> <span class="keyword">return</span> queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 4.4.获取锁成功,根据id查询数据库</span></span><br><span class="line"> r = dbFallback.apply(id);</span><br><span class="line"> <span class="comment">// 5.不存在,返回错误</span></span><br><span class="line"> <span class="keyword">if</span> (r == <span class="literal">null</span>) {</span><br><span class="line"> <span class="comment">// 将空值写入redis</span></span><br><span class="line"> stringRedisTemplate.opsForValue().set(key, <span class="string">""</span>, CACHE_NULL_TTL, TimeUnit.MINUTES);</span><br><span class="line"> <span class="comment">// 返回错误信息</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 6.存在,写入redis</span></span><br><span class="line"> <span class="built_in">this</span>.set(key, r, time, unit);</span><br><span class="line"> } <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line"> }<span class="keyword">finally</span> {</span><br><span class="line"> <span class="comment">// 7.释放锁</span></span><br><span class="line"> unlock(lockKey);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 8.返回</span></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="keyword">private</span> <span class="type">boolean</span> <span class="title function_">tryLock</span><span class="params">(String key)</span> {</span><br><span class="line"> <span class="type">Boolean</span> <span class="variable">flag</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().setIfAbsent(key, <span class="string">"1"</span>, <span class="number">10</span>, TimeUnit.SECONDS);</span><br><span class="line"> <span class="keyword">return</span> BooleanUtil.isTrue(flag);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">unlock</span><span class="params">(String key)</span> {</span><br><span class="line"> stringRedisTemplate.delete(key);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在ShopServiceImpl 中</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> CacheClient cacheClient;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> Result <span class="title function_">queryById</span><span class="params">(Long id)</span> {</span><br><span class="line"> <span class="comment">// 解决缓存穿透</span></span><br><span class="line"> <span class="type">Shop</span> <span class="variable">shop</span> <span class="operator">=</span> cacheClient</span><br><span class="line"> .queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, <span class="built_in">this</span>::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 互斥锁解决缓存击穿</span></span><br><span class="line"> <span class="comment">// Shop shop = cacheClient</span></span><br><span class="line"> <span class="comment">// .queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// 逻辑过期解决缓存击穿</span></span><br><span class="line"> <span class="comment">// Shop shop = cacheClient</span></span><br><span class="line"> <span class="comment">// .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (shop == <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">return</span> Result.fail(<span class="string">"店铺不存在!"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 7.返回</span></span><br><span class="line"> <span class="keyword">return</span> Result.ok(shop);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>在这篇文章中,我们详细探讨了如何利用Redis实现短信登录功能和商品缓存机制。我们通过Redis的快速读写性能,实现了短信验证码的生成、存储和验证,并通过<code>EXPIRE</code>命令有效管理验证码的有效期,确保验证码过期后自动失效,提高了系统的安全性。</p><p>在商品缓存方面,Redis被用来将常用的商品信息缓存到内存中,减少数据库的读操作,从而提升系统性能。我们深入探讨了<strong>缓存穿透</strong>、<strong>缓存击穿</strong>和<strong>缓存雪崩</strong>等常见问题及其解决方案。缓存穿透通过检查和过滤无效请求避免对数据库的冲击;缓存击穿通过<strong>互斥锁</strong>或<strong>逻辑过期</strong>策略防止大量并发请求击穿缓存;缓存雪崩则通过设置不同的缓存过期时间和预热策略来避免大量缓存同时失效导致的系统崩溃。</p><p>通过这些实战应用,我们不仅掌握了Redis的基本使用方法,还学会了如何将其应用于实际项目中,为进一步深入学习和使用Redis奠定了坚实的基础。</p>]]></content>
<categories>
<category> Redis </category>
</categories>
<tags>
<tag> Redis </tag>
</tags>
</entry>
<entry>
<title>Redis入门篇(二):Redis的常用命令和Java客户端</title>
<link href="/junwei/14c0f7d1.html"/>
<url>/junwei/14c0f7d1.html</url>
<content type="html"><![CDATA[<h1 id="Redis常见命令"><a href="#Redis常见命令" class="headerlink" title="Redis常见命令"></a>Redis常见命令</h1><p>本章学习Redis的常见五大命令,其余的等到后面实战再边用边学习<img src="/../images/redis/redis1.2/1.png" alt="1"></p><p>也可以到Redis官网,找到<a href="https://redis.io/docs/latest/commands/">Commands | Docs</a>进行类型的快速查询和学习</p><p><img src="/../images/redis/redis1.2/2.png" alt="2"></p><h2 id="通用命令"><a href="#通用命令" class="headerlink" title="通用命令"></a>通用命令</h2><p>通用指令是部分数据类型的,都可以使用的指令,常见的有:</p><ul><li><strong>KEYS:</strong>查看符合模板的所有key</li><li><strong>DEL:</strong>删除一个指定的key</li><li><strong>EXISTS:</strong>判断key是否存在</li><li><strong>EXPIRE:</strong>给一个key设置有效期,有效期到期时该key会被自动删除</li><li><strong>TTL:</strong>查看一个KEY的剩余有效期</li></ul><h2 id="String类型"><a href="#String类型" class="headerlink" title="String类型"></a>String类型</h2><p>其<strong>value是字符串</strong>,不过根据字符串的格式不同,又可以分为3类:</p><ul><li><p>string:普通字符串</p></li><li><p>int:整数类型,可以做<strong>自增</strong>、<strong>自减</strong>操作</p></li><li><p>float:浮点类型,可以做<strong>自增</strong>、<strong>自减</strong>操作</p></li></ul><p><img src="/../images/redis/redis1.2/3.png" alt="3"></p><h3 id="String的常见命令有:"><a href="#String的常见命令有:" class="headerlink" title="String的常见命令有:"></a>String的常见命令有:</h3><ul><li><strong>SET:</strong>添加或者修改已经存在的一个String类型的键值对</li><li><strong>GET:</strong>根据key获取String类型的value</li><li><strong>MSET:批量</strong>添加多个String类型的键值对</li><li><strong>MGET:</strong>根据多个key获取多个String类型的value</li><li><strong>INCR:</strong>让一个整型的key自增1</li><li><strong>INCRBY:</strong>让一个整型的key自增并指定步长,例如:incrby num 2 让num值自增2</li><li><strong>INCRBYFLOAT:</strong>让一个浮点类型的数字自增<strong>并指定步长</strong></li><li><strong>SETNX:</strong>添加一个String类型的键值对,前提是这个key不存在,<strong>否则不执行(互斥锁的使用)</strong></li><li><strong>SETEX:</strong>添加一个String类型的键值对,并且指定有效期</li></ul><h2 id="Hash类型"><a href="#Hash类型" class="headerlink" title="Hash类型"></a>Hash类型</h2><p>Hash类型,也叫散列,其<strong>value是一个无序字典</strong>,类似于<code>Java</code>中的<code>HashMap</code>结构。</p><p>Hash结构可以将对象中的每个字段独立存储,可以<strong>针对单个字段做CRUD</strong></p><p><img src="/../images/redis/redis1.2/4.png" alt="4"></p><h3 id="Hash的常见命令有:"><a href="#Hash的常见命令有:" class="headerlink" title="Hash的常见命令有:"></a>Hash的常见命令有:</h3><ul><li><p><strong>HSET key field value:</strong>添加或者修改hash类型key的field的值</p></li><li><p><strong>HGET key field:</strong>获取一个hash类型key的field的值</p></li><li><p><strong>HMSET:</strong>批量添加多个hash类型key的field的值</p></li><li><p><strong>HMGET:</strong>批量获取多个hash类型key的field的值</p></li><li><p><strong>HGETALL:</strong>获取一个hash类型的key中的所有的field和value</p></li><li><p><strong>HKEYS:</strong>获取一个hash类型的key中的所有的field</p></li><li><p><strong>HINCRBY:</strong>让一个hash类型key的字段值自增并指定步长</p></li><li><p><strong>HSETNX:</strong>添加一个hash类型的key的field值,前提是这个field不存在,否则不执行</p></li></ul><h2 id="List类型"><a href="#List类型" class="headerlink" title="List类型"></a>List类型</h2><p>Redis中的List类型与<code>Java</code>中的<code>LinkedList</code>类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。</p><p>特征也与LinkedList类似:</p><ul><li><strong>有序</strong></li><li>元素<strong>可以重复</strong></li><li>插入和删除快</li><li>查询速度一般</li></ul><p>常用来存储一个<strong>有序数据</strong>,例如:<strong>朋友圈点赞列表</strong>,<strong>评论列表</strong>等。</p><p><img src="/../images/redis/redis1.2/5.png" alt="5"></p><h3 id="List的常见命令有:"><a href="#List的常见命令有:" class="headerlink" title="List的常见命令有:"></a>List的常见命令有:</h3><ul><li><strong>LPUSH key element … :</strong>向列表左侧插入一个或多个元素</li><li><strong>LPOP key:</strong>移除并返回列表左侧的第一个元素,没有则返回nil</li><li><strong>RPUSH key element … :</strong>向列表右侧插入一个或多个元素</li><li><strong>RPOP key:</strong>移除并返回列表右侧的第一个元素</li><li><strong>LRANGE key star end:</strong>返回一段角标范围内的所有元素</li><li><strong>BLPOP和BRPOP:</strong>与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil</li></ul><h2 id="Set类型"><a href="#Set类型" class="headerlink" title="Set类型"></a>Set类型</h2><p>Redis的Set结构与<code>Java</code>中的<code>HashSet</code>类似,可以看做是一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征:</p><ul><li><p><strong>无序</strong></p></li><li><p><strong>元素不可重复</strong></p></li><li><p>查找快</p></li><li><p>支持<strong>交集、并集、差集</strong>等功能</p></li></ul><p><img src="/../images/redis/redis1.2/6.png" alt="6"></p><h3 id="Set的常见命令有:"><a href="#Set的常见命令有:" class="headerlink" title="Set的常见命令有:"></a>Set的常见命令有:</h3><ul><li><strong>SADD key member … :</strong>向set中添加一个或多个元素</li><li><strong>SREM key member … :</strong> 移除set中的指定元素</li><li><strong>SCARD key:</strong> 返回set中元素的个数</li><li><strong>SISMEMBER key member:</strong>判断一个元素是否存在于set中</li><li><strong>SMEMBERS:</strong>获取set中的所有元素</li><li><strong>SINTER key1 key2 … :</strong>求key1与key2的交集</li></ul><h2 id="SortedSet(ZSet)类型"><a href="#SortedSet(ZSet)类型" class="headerlink" title="SortedSet(ZSet)类型"></a>SortedSet(ZSet)类型</h2><p>Redis的SortedSet是一个可排序的set集合,与<code>Java</code>中的<code>TreeSet</code>有些类似,但底层数据结构却差别很大。SortedSet中的每一个元素都带<strong>有一个score(分数)属性</strong>,<strong>可以基于score属性对元素排序</strong>,底层的实现是一个跳表(SkipList)加 hash表。</p><p>SortedSet具备下列特性:</p><ul><li><strong>可排序</strong></li><li><strong>元素不重复</strong></li><li>查询速度快</li></ul><p>因为SortedSet的可排序特性,经常被用来实现<strong>排行榜</strong>这样的功能。</p><p><img src="/../images/redis/redis1.2/7.png" alt="7"></p><h3 id="SortedSet的常见命令有:"><a href="#SortedSet的常见命令有:" class="headerlink" title="SortedSet的常见命令有:"></a>SortedSet的常见命令有:</h3><ul><li><p><strong>ZADD key score member:</strong>添加一个或多个元素到sorted set ,如果已经存在则更新其score值</p></li><li><p><strong>ZREM key member:</strong>删除sorted set中的一个指定元素</p></li><li><p><strong>ZSCORE key member :</strong> 获取sorted set中的指定元素的score值</p></li><li><p><strong>ZRANK key member:</strong>获取sorted set 中的指定元素的排名</p></li><li><p><strong>ZCARD key:</strong>获取sorted set中的元素个数</p></li><li><p><strong>ZCOUNT key min max:</strong>统计score值在给定范围内的所有元素的个数</p></li><li><p><strong>ZINCRBY key increment member:</strong>让sorted set中的指定元素自增,步长为指定的increment值</p></li><li><p><strong>ZRANGE key min max:</strong>按照score排序后,获取指定排名范围内的元素</p></li><li><p><strong>ZRANGEBYSCORE key min max:</strong>按照score排序后,获取指定score范围内的元素</p></li><li><p><strong>ZDIFF、ZINTER、ZUNION:</strong>求差集、交集、并集</p></li></ul><h1 id="Redis的Java客户端"><a href="#Redis的Java客户端" class="headerlink" title="Redis的Java客户端"></a>Redis的Java客户端</h1><p>打开Redis官网提供的<a href="https://redis.io/docs/latest/develop/connect/clients/java/">Client</a>页面,在Java客户端中我们可以看到Redis官方是非常推荐<code>Jedis</code>和<code>Lettuce</code>这两款客户端的</p><p><img src="/../images/redis/redis1.2/8.png" alt="8"></p><h2 id="Jedis客户端"><a href="#Jedis客户端" class="headerlink" title="Jedis客户端"></a>Jedis客户端</h2><p>Jedis的官网地址: <a href="https://github.com/redis/jedis%EF%BC%8C%E6%88%91%E4%BB%AC%E5%9F%BA%E4%BA%8E%E4%BB%A3%E7%A0%81%E6%9D%A5%E8%AE%A9%E5%A4%A7%E5%AE%B6%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8">https://github.com/redis/jedis,我们基于代码来让大家快速入门</a></p><p>Jedis中封装的方法签名和命令行操作Redis大同小异,例如Set命令,在Jedis中也是Set方法</p><h3 id="快速入门"><a href="#快速入门" class="headerlink" title="快速入门"></a>快速入门</h3><ul><li>新建Maven项目,在pom.xml中引入依赖</li></ul><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"><dependency></span><br><span class="line"> <groupId>redis.clients</groupId></span><br><span class="line"> <artifactId>jedis</artifactId></span><br><span class="line"> <version><span class="number">5.0</span><span class="number">.0</span></version></span><br><span class="line"></dependency></span><br><span class="line"> </span><br><span class="line"><dependency></span><br><span class="line"> <groupId>org.junit.jupiter</groupId></span><br><span class="line"> <artifactId>junit-jupiter</artifactId></span><br><span class="line"> <version><span class="number">5.7</span><span class="number">.0</span></version></span><br><span class="line"> <scope>test</scope></span><br><span class="line"></dependency></span><br></pre></td></tr></table></figure><p>新建一个单元测试类,测试String、Hash、List类型的使用</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.junwei;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.junit.jupiter.api.AfterEach;</span><br><span class="line"><span class="keyword">import</span> org.junit.jupiter.api.BeforeEach;</span><br><span class="line"><span class="keyword">import</span> org.junit.jupiter.api.Test;</span><br><span class="line"><span class="keyword">import</span> redis.clients.jedis.Jedis;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.Map;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JedisTest</span>{</span><br><span class="line"> <span class="keyword">private</span> Jedis jedis;</span><br><span class="line"> <span class="meta">@BeforeEach</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">setUp</span><span class="params">()</span> {</span><br><span class="line"> jedis = <span class="keyword">new</span> <span class="title class_">Jedis</span>(<span class="string">"localhost"</span>,<span class="number">6379</span>);</span><br><span class="line"> jedis.auth(<span class="string">"123456"</span>);</span><br><span class="line"> jedis.select(<span class="number">0</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testString</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">res</span> <span class="operator">=</span> jedis.set(<span class="string">"name"</span>, <span class="string">"junwei"</span>);</span><br><span class="line"> System.out.println(res);</span><br><span class="line"> <span class="type">String</span> <span class="variable">name</span> <span class="operator">=</span> jedis.get(<span class="string">"name"</span>);</span><br><span class="line"> System.out.println(<span class="string">"Name = "</span> + name);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testHash</span><span class="params">()</span> {</span><br><span class="line"> jedis.hset(<span class="string">"stus"</span>,<span class="string">"name"</span>,<span class="string">"junwei"</span>);</span><br><span class="line"> jedis.hset(<span class="string">"stus"</span>,<span class="string">"number"</span>,<span class="string">"1"</span>);</span><br><span class="line"> Map<String, String> stus = jedis.hgetAll(<span class="string">"stus"</span>);</span><br><span class="line"> System.out.println(stus);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">testList</span><span class="params">()</span> {</span><br><span class="line"> jedis.lpush(<span class="string">"list"</span>,<span class="string">"1"</span>,<span class="string">"2"</span>,<span class="string">"4"</span>,<span class="string">"第一个"</span>);</span><br><span class="line"> System.out.println(jedis.lpop(<span class="string">"list"</span>));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AfterEach</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">tearDown</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">if</span>(jedis != <span class="literal">null</span>){</span><br><span class="line"> jedis.close();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="SpringDataRedis客户端"><a href="#SpringDataRedis客户端" class="headerlink" title="SpringDataRedis客户端"></a>SpringDataRedis客户端</h2><p>SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis,官网地址:<a href="https://spring.io/projects/spring-data-redis">https://spring.io/projects/spring-data-redis</a></p><ul><li>提供了对不同Redis客户端的整合(Lettuce和Jedis)</li><li>提供了RedisTemplate统一API来操作Redis</li><li>支持Redis的发布订阅模型</li><li>支持Redis哨兵和Redis集群</li><li>支持基于Lettuce的响应式编程</li><li>支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化</li><li>支持基于Redis的JDKCollection实现</li></ul><p>SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中:</p><p><img src="/../images/redis/redis1.2/9.png" alt="9"></p><h3 id="使用-Spring-Data-Redis-的好处:"><a href="#使用-Spring-Data-Redis-的好处:" class="headerlink" title="使用 Spring Data Redis 的好处:"></a>使用 Spring Data Redis 的好处:</h3><ul><li><strong>抽象层</strong>:开发者可以更方便地切换底层 Redis 客户端(比如从 Jedis 切换到 Lettuce),而无需修改大量代码。</li><li><strong>一致的编程模型</strong>:无论底层使用的是哪个客户端,Spring Data Redis 都提供一致的 API。</li><li><strong>集成性</strong>:可以与 Spring 生态系统中的其他组件无缝集成,如 Spring Boot、Spring Data 等。</li></ul><h3 id="序列化器和反序列化器"><a href="#序列化器和反序列化器" class="headerlink" title="序列化器和反序列化器"></a>序列化器和反序列化器</h3><p>在Java中,使用<code>redisTemplate</code>与Redis进行交互时,序列化器和反序列化器的配置非常重要,因为它们<strong>负责将Java对象转换为可以存储在Redis中的字节数据,以及将字节数据转换回Java对象</strong>。</p><p>常用的序列化器和反序列化器包括:<code>StringRedisSerializer</code>、<code>JdkSerializationRedisSerializer</code>、<code>Jackson2JsonRedisSerializer</code>、<code>GenericJackson2JsonRedisSerializer</code>等。</p><p><code>StringRedisSerializer</code>用于将字符串类型的数据进行序列化和反序列化。它非常适用于键的序列化,因为Redis中的键通常是字符串。</p><p><code>JdkSerializationRedisSerializer</code>使用Java的序列化机制将对象转换为字节数组。它适用于任何实现了<code>Serializable</code>接口的Java对象。</p><p><code>Jackson2JsonRedisSerializer</code>使用Jackson库将对象转换为JSON格式的字符串。它适用于需要以JSON格式存储的对象。</p><p><code>GenericJackson2JsonRedisSerializer</code>是<code>Jackson2JsonRedisSerializer</code>的改进版本,它可以处理更多类型的对象并提供更好的兼容性。</p><p><strong>我们可以自定义RedisTemplate的序列化方式:</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> RedisTemplate<String, Object> <span class="title function_">redisTemplate</span><span class="params">(RedisConnectionFactory connectionFactory)</span>{</span><br><span class="line"> <span class="comment">// 创建RedisTemplate对象</span></span><br><span class="line"> RedisTemplate<String, Object> template = <span class="keyword">new</span> <span class="title class_">RedisTemplate</span><>();</span><br><span class="line"> <span class="comment">// 设置连接工厂</span></span><br><span class="line"> template.setConnectionFactory(connectionFactory);</span><br><span class="line"> <span class="comment">// 创建JSON序列化工具</span></span><br><span class="line"> <span class="type">GenericJackson2JsonRedisSerializer</span> <span class="variable">jsonRedisSerializer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">GenericJackson2JsonRedisSerializer</span>();</span><br><span class="line"> <span class="comment">// 设置Key的序列化</span></span><br><span class="line"> template.setKeySerializer(RedisSerializer.string());</span><br><span class="line"> template.setHashKeySerializer(RedisSerializer.string());</span><br><span class="line"> <span class="comment">// 设置Value的序列化</span></span><br><span class="line"> template.setValueSerializer(jsonRedisSerializer);</span><br><span class="line"> template.setHashValueSerializer(jsonRedisSerializer);</span><br><span class="line"> <span class="comment">// 返回</span></span><br><span class="line"> <span class="keyword">return</span> template;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="快速入门-1"><a href="#快速入门-1" class="headerlink" title="快速入门"></a>快速入门</h3><p>这里只教如何配置Springboot中的Redis以及给出一个小Demo,实践的话后面的章节都用到,这里就不多赘述了</p><p>在pom.xml中引入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></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">dependency</span>></span></span><br></pre></td></tr></table></figure><p>在Springboot中配置Redis,并使用lettuce客户端连接池</p><figure class="highlight properties"><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 class="attr">spring</span>:<span class="string"></span></span><br><span class="line"> <span class="attr">redis</span>:<span class="string"></span></span><br><span class="line"> <span class="attr">host</span>: <span class="string">192.168.150.101</span></span><br><span class="line"> <span class="attr">port</span>: <span class="string">6379</span></span><br><span class="line"> <span class="attr">password</span>: <span class="string">123321</span></span><br><span class="line"> <span class="attr">lettuce</span>:<span class="string"></span></span><br><span class="line"> <span class="attr">pool</span>:<span class="string"></span></span><br><span class="line"> <span class="attr">max-active</span>: <span class="string">8</span></span><br><span class="line"> <span class="attr">max-idle</span>: <span class="string">8</span></span><br><span class="line"> <span class="attr">min-idle</span>: <span class="string">0</span></span><br><span class="line"> <span class="attr">max-wait</span>: <span class="string">100ms</span></span><br></pre></td></tr></table></figure><p>SpringDataRedis提供了RedisTemplate的子类:StringRedisTemplate,它的key和value的序列化方式默认就是String方式。</p><p>注入RedisTemplate即可使用</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> RedisTemplate redisTemplate;</span><br></pre></td></tr></table></figure><p>省去了我们自定义RedisTemplate的序列化方式的步骤,而是直接使用:</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">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"><span class="comment">// JSON序列化工具</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ObjectMapper</span> <span class="variable">mapper</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ObjectMapper</span>();</span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">testSaveUser</span><span class="params">()</span> <span class="keyword">throws</span> JsonProcessingException {</span><br><span class="line"> <span class="comment">// 创建对象</span></span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>(<span class="string">"虎哥"</span>, <span class="number">21</span>);</span><br><span class="line"> <span class="comment">// 手动序列化</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> mapper.writeValueAsString(user);</span><br><span class="line"> <span class="comment">// 写入数据</span></span><br><span class="line"> stringRedisTemplate.opsForValue().set(<span class="string">"user:200"</span>, json);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 获取数据</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">jsonUser</span> <span class="operator">=</span> stringRedisTemplate.opsForValue().get(<span class="string">"user:200"</span>);</span><br><span class="line"> <span class="comment">// 手动反序列化</span></span><br><span class="line"> <span class="type">User</span> <span class="variable">user1</span> <span class="operator">=</span> mapper.readValue(jsonUser, User.class);</span><br><span class="line"> System.out.println(<span class="string">"user1 = "</span> + user1);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>通过本篇文章的学习,我们掌握了Redis的常用命令,了解了不同数据类型的基本操作,并学会了在Java中使用Redis客户端进行编程操作。这为我们在实际开发中高效地使用Redis奠定了坚实的基础。Redis的高性能、灵活性和多样的数据类型支持,使其成为解决高速数据访问需求的理想选择。</p>]]></content>
<categories>
<category> Redis </category>
</categories>
<tags>
<tag> Redis </tag>
</tags>
</entry>
<entry>
<title>Redis入门篇(一):初始Redis及其安装</title>
<link href="/junwei/b6c30fde.html"/>
<url>/junwei/b6c30fde.html</url>
<content type="html"><![CDATA[<h1 id="初识Redis"><a href="#初识Redis" class="headerlink" title="初识Redis"></a>初识Redis</h1><p><font size=4>  <a href="https://redis.io/">Redis</a> 是一个基于<strong>内存</strong>的<strong>键值</strong>存储<strong>NoSQL</strong>数据库。</font></p><h2 id="内存存储"><a href="#内存存储" class="headerlink" title="内存存储"></a>内存存储</h2><p><font size=4>  所有<strong>数据都存储在内存</strong>中,这使得<strong>读写操作非常快速</strong>。虽然 Redis 也支持数据持久化,将数据保存到磁盘,但其核心是内存数据库。</font></p><h2 id="键值对(Key-Value)存储"><a href="#键值对(Key-Value)存储" class="headerlink" title="键值对(Key-Value)存储"></a>键值对(Key-Value)存储</h2><p><font size=4>  数据以键值对的形式存储,键是字符串,<strong>值可以是多种数据结构</strong>,例如下面的这些数据类型:</font></p><ul><li><strong>字符串(Strings):</strong>普通的文本或二进制数据。</li><li><strong>散列(Hashes):</strong>包含键值对的集合,适合存储对象。</li><li><strong>列表(Lists):</strong>按插入顺序排序的字符串列表。</li><li><strong>集合(Sets):</strong>无序且不重复的字符串集合。</li><li><strong>有序集合(Sorted Sets):</strong>带有分数的字符串集合,按分数排序。</li><li><strong>位图(Bitmaps)</strong></li><li><strong>基数统计(HyperLogLogs)</strong></li><li><strong>地理空间索引(Geospatial Indexes)</strong></li><li><strong>流(Streams)</strong></li></ul><h2 id="NoSQL-数据库"><a href="#NoSQL-数据库" class="headerlink" title="NoSQL 数据库"></a>NoSQL 数据库</h2><p><font size=4>  <strong>NoSQL(Not Only SQL)</strong>数据库是一类<strong>不使用传统关系模型</strong>的数据库系统。与关系型数据库(如 MySQL、PostgreSQL)相比,NoSQL 数据库具有以下特点:</font></p><ul><li><strong>灵活的模式</strong>:不需要预定义数据模式,适合处理多变和非结构化数据。</li><li><strong>高扩展性</strong>:通常设计为分布式系统,能够水平扩展。</li><li><strong>高性能</strong>:专注于特定类型的数据存储和操作,性能优化较好。</li></ul><h2 id="SQL-vs-NoSQL"><a href="#SQL-vs-NoSQL" class="headerlink" title="SQL vs NoSQL"></a>SQL vs NoSQL</h2><h3 id="结构化和非结构化"><a href="#结构化和非结构化" class="headerlink" title="结构化和非结构化"></a>结构化和非结构化</h3><ul><li>传统关系型数据库是结构化数据,<strong>每一张表都有严格的约束信息</strong>:字段名、字段数据类型、字段约束等等信息,插入的数据必须遵守这些约束</li><li>NoSQL则对<strong>数据库格式没有严格约束</strong>,往往形式松散,自由,可以是键值型(Key-Value)也可以是文档型(Document),还可以是图格式(Graph)。</li></ul><h3 id="关联和非关联"><a href="#关联和非关联" class="headerlink" title="关联和非关联"></a>关联和非关联</h3><ul><li>传统数据库的表与表之间往往<strong>存在关联</strong>,例如外键,表的1-1、1-N和N-M关系。</li><li>非关系型数据库<strong>不存在关联关系</strong>,要维护关系要么靠代码中的业务逻辑,要么靠数据之间的耦合</li></ul><h3 id="查询方式"><a href="#查询方式" class="headerlink" title="查询方式"></a>查询方式</h3><ul><li>传统关系型数据库会基于Sql语句做查询,<strong>语法有统一标准</strong>,无论是MySQL、SQL Server还是其他数据库,基本的SQL语法相同,个别语法不同。</li><li>非关系型数据库<strong>有自己的独特语法</strong>,如Redis的GET、SET、LPUSH、RPOP……</li></ul><h3 id="事务"><a href="#事务" class="headerlink" title="事务"></a>事务</h3><ul><li>传统关系型数据库<strong>能满足事务ACID的原则</strong>,适用于对数据的安全性和一致性有较高要求。</li><li>非关系型数据库<strong>往往不支持事务</strong>,或者不能严格保证ACID的特性,只能实现基本的一致性。</li></ul><h3 id="存储方式"><a href="#存储方式" class="headerlink" title="存储方式"></a>存储方式</h3><ul><li>关系型数据库<strong>基于磁盘进行存储</strong>,会有大量的磁盘IO,对性能有一定影响</li><li>非关系型数据库,他们的操作更多的是<strong>依赖于内存来操作</strong>,内存的读写速度会非常快,性能自然会好一些</li></ul><h3 id="扩展性"><a href="#扩展性" class="headerlink" title="扩展性"></a>扩展性</h3><ul><li>关系型数据库集群模式一般是主从,主从数据一致,起到<strong>数据备份</strong>的作用,称为<strong>垂直扩展</strong>。</li><li>非关系型数据库可以<strong>将数据拆分,存储在不同机器上</strong>,可以保存海量数据,解决内存大小有限的问题。称为<strong>水平扩展</strong>。</li></ul><h1 id="再识Redis"><a href="#再识Redis" class="headerlink" title="再识Redis"></a>再识Redis</h1><p><font size=4>  <strong>Redis</strong>诞生于2009年全称是<strong>Re</strong>mote <strong>D</strong>ictionary <strong>S</strong>erver 远程词典服务器,是一个基于内存的键值型NoSQL数据库。</font></p><h2 id="Redis-的特点和优势包括:"><a href="#Redis-的特点和优势包括:" class="headerlink" title="Redis 的特点和优势包括:"></a>Redis 的特点和优势包括:</h2><ul><li><strong>高速性能:</strong>由于所有数据都存储在内存中,读写操作非常快,通常用于需要快速访问数据的应用程序。</li><li><strong>丰富的数据类型:</strong>支持多种数据类型,适用于不同的应用场景。</li><li><strong>持久化:</strong>支持将数据从内存持久化到磁盘,有多种持久化策略,如RDB(快照)和AOF(追加文件)。</li><li><strong>高可用性和分布式:</strong>通过Redis Sentinel和Redis Cluster实现高可用性和数据分片,保证系统的稳定性和扩展性。</li><li><strong>简单易用:</strong>提供简单直观的命令行接口和多种编程语言的客户端库,方便开发人员集成和使用。</li></ul><h2 id="Redis-常用于以下场景:"><a href="#Redis-常用于以下场景:" class="headerlink" title="Redis 常用于以下场景:"></a>Redis 常用于以下场景:</h2><ul><li><strong>缓存:</strong>减少数据库负载和提高应用程序响应速度。</li><li><strong>会话存储:</strong>存储用户会话信息,实现快速读取和写入。</li><li><strong>排行榜:</strong>利用有序集合实现排行榜功能。</li><li><strong>发布/订阅:</strong>实现消息发布和订阅机制。</li><li><strong>任务队列:</strong>使用列表或流来实现任务队列,处理异步任务。</li></ul><h1 id="安装Redis-Linux"><a href="#安装Redis-Linux" class="headerlink" title="安装Redis(Linux)"></a>安装Redis(Linux)</h1><p><font size=4>  大多数企业都是<strong>基于Linux服务器来部署项目</strong>,而且Redis官方也没有提供Windows版本的安装包,所以我们学习Linux系统下的Redis安装。</font></p><h2 id="依赖库安装"><a href="#依赖库安装" class="headerlink" title="依赖库安装"></a>依赖库安装</h2><p>  Redis是基于C语言编写的,因此首先需要安装Redis所需要的gcc依赖:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install -y gcc tcl</span><br></pre></td></tr></table></figure><h2 id="上传压缩包并解压"><a href="#上传压缩包并解压" class="headerlink" title="上传压缩包并解压"></a>上传压缩包并解压</h2><p>  到Redis官网下载后缀为gz结尾的安装包,通过任一shell软件将压缩包传到Linux系统当中,运行下列命令(替换版本号)进行解压缩:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tar -xzf redis-x.x.x.tar.gz</span><br></pre></td></tr></table></figure><p>  进入解压后的Redis目录,运行编译命令,如果没有出错,应该就安装成功了。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">make && make install</span><br></pre></td></tr></table></figure><p>  默认的安装路径在<code>/usr/local/bin</code>目录下</p><p>该目录已经默认配置到环境变量,因此可以在任意目录下运行这些命令。其中:</p><ul><li>redis-cli:是redis提供的命令行客户端</li><li>redis-server:是redis的服务端启动脚本</li><li>redis-sentinel:是redis的哨兵启动脚本</li></ul><h2 id="启动方式"><a href="#启动方式" class="headerlink" title="启动方式"></a>启动方式</h2><ul><li><p>默认启动</p></li><li><p>指定配置启动</p></li><li><p>开机自启</p></li></ul><h3 id="默认启动"><a href="#默认启动" class="headerlink" title="默认启动"></a>默认启动</h3><p><font size=4>  在任意目录输入<code>redis-server</code>命令即可启动Redis,会看到Redis的图标。</font></p><p>这种启动属于<code>前台启动</code>,会阻塞整个会话窗口,窗口关闭或者按下<code>CTRL + C</code>则Redis停止。不推荐使用。</p><h3 id="指定配置启动"><a href="#指定配置启动" class="headerlink" title="指定配置启动"></a>指定配置启动</h3><p><font size=4>  如果要让Redis以<strong>后台</strong>方式启动,则必须修改Redis配置文件,就在我们之前解压的redis安装包,名字叫redis.conf</font></p><p>我们先将这个配置文件备份一份:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cp redis.conf redis.conf.bck</span><br></pre></td></tr></table></figure><p>然后修改redis.conf文件中的一些配置,用vi编辑器打开该文件:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi redis.conf</span><br></pre></td></tr></table></figure><p>用<code>/</code>加上想要查找的字符,查找到对应行,输入<code>i</code>进行修改,修改完成<code>:wq</code>保存并退出,不小心修改错了则<code>:q!</code>不保存退出</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 允许访问的地址,默认是127.0.0.1,会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问,生产环境不要设置为0.0.0.0</span></span><br><span class="line"><span class="attr">bind</span> <span class="string">0.0.0.0</span></span><br><span class="line"><span class="comment"># 守护进程,修改为yes后即可后台运行</span></span><br><span class="line"><span class="attr">daemonize</span> <span class="string">yes </span></span><br><span class="line"><span class="comment"># 密码,设置后访问Redis必须输入密码</span></span><br><span class="line"><span class="attr">requirepass</span> <span class="string">123321</span></span><br></pre></td></tr></table></figure><p>Redis的其它常见配置:</p><figure class="highlight properties"><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"><span class="comment"># 监听的端口</span></span><br><span class="line"><span class="attr">port</span> <span class="string">6379</span></span><br><span class="line"><span class="comment"># 工作目录,默认是当前目录,也就是运行redis-server时的命令,日志、持久化等文件会保存在这个目录</span></span><br><span class="line"><span class="attr">dir</span> <span class="string">.</span></span><br><span class="line"><span class="comment"># 数据库数量,设置为1,代表只使用1个库,默认有16个库,编号0~15</span></span><br><span class="line"><span class="attr">databases</span> <span class="string">1</span></span><br><span class="line"><span class="comment"># 设置redis能够使用的最大内存</span></span><br><span class="line"><span class="attr">maxmemory</span> <span class="string">512mb</span></span><br><span class="line"><span class="comment"># 日志文件,默认为空,不记录日志,可以指定日志文件名</span></span><br><span class="line"><span class="attr">logfile</span> <span class="string">"redis.log"</span></span><br></pre></td></tr></table></figure><p>启动Redis:</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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">进入redis安装目录</span> </span><br><span class="line">cd /usr/local/src/redis-x.x.x</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">启动</span></span><br><span class="line">redis-server redis.conf</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">利用redis-cli来执行 shutdown 命令,即可停止 Redis 服务,</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">因为之前配置了密码,因此需要通过 -u 来指定密码</span></span><br><span class="line">redis-cli -u 123321 shutdown</span><br></pre></td></tr></table></figure><h3 id="开机自启"><a href="#开机自启" class="headerlink" title="开机自启"></a>开机自启</h3><p><font size=4>  我们也可以通过配置来实现开机自启。</font></p><p>首先,新建一个系统服务文件:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /etc/systemd/system/redis.service</span><br></pre></td></tr></table></figure><p>内容如下:</p><figure class="highlight plaintext"><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">[Unit]</span><br><span class="line">Description=redis-server</span><br><span class="line">After=network.target</span><br><span class="line"></span><br><span class="line">[Service]</span><br><span class="line">Type=forking</span><br><span class="line">ExecStart=/usr/local/bin/redis-server /usr/local/src/redis-6.2.6/redis.conf</span><br><span class="line">PrivateTmp=true</span><br><span class="line"></span><br><span class="line">[Install]</span><br><span class="line">WantedBy=multi-user.target</span><br></pre></td></tr></table></figure><p>然后重载系统服务:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">systemctl daemon-reload</span><br></pre></td></tr></table></figure><p>现在,我们可以用下面这组命令来操作redis了:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 启动</span></span><br><span class="line">systemctl start redis</span><br><span class="line"><span class="comment"># 停止</span></span><br><span class="line">systemctl stop redis</span><br><span class="line"><span class="comment"># 重启</span></span><br><span class="line">systemctl restart redis</span><br><span class="line"><span class="comment"># 查看状态</span></span><br><span class="line">systemctl status redis</span><br></pre></td></tr></table></figure><p>执行下面的命令,可以让redis开机自启:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">systemctl <span class="built_in">enable</span> redis</span><br></pre></td></tr></table></figure><h1 id="Redis客户端的使用"><a href="#Redis客户端的使用" class="headerlink" title="Redis客户端的使用"></a>Redis客户端的使用</h1><p>安装完成Redis,我们就可以操作Redis,实现数据的CRUD了。这需要用到Redis客户端,包括:</p><ul><li>命令行客户端</li><li>图形化桌面客户端</li><li>编程客户端</li></ul><h2 id="Redis命令行客户端"><a href="#Redis命令行客户端" class="headerlink" title="Redis命令行客户端"></a>Redis命令行客户端</h2><p><font size=4>  Redis安装完成后就自带了命令行客户端:redis-cli</font></p><p>使用方式如下:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli [options] [commonds]</span><br></pre></td></tr></table></figure><p>其中常见的options有:</p><ul><li><code>-h 127.0.0.1</code>:指定要连接的redis节点的IP地址,默认是127.0.0.1</li><li><code>-p 6379</code>:指定要连接的redis节点的端口,默认是6379</li><li><code>-a 123321</code>:指定redis的访问密码</li></ul><p>例如我们可以通过以下命令行连接上Redis</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli -a 123321</span><br></pre></td></tr></table></figure><h2 id="图形化桌面客户端"><a href="#图形化桌面客户端" class="headerlink" title="图形化桌面客户端"></a>图形化桌面客户端</h2><p><font size=4>  这里我推荐使用<a href="https://www.navicat.com.cn/">Navicat</a>,当然也可以使用其他的客户端</font></p><p>个人觉得Navicat界面美观,在Navicat 17版本后可以连接Redis,可以实现一个软件连接多个不同类型的数据库,不需要为了多个数据库而在电脑上安装多个软件客户端,便捷高效</p><img src="../images/redis/redis1.1/1.png" style="zoom: 60%;" /><p>点击左上角的连接,选择Redis,填入对应的数据:</p><ul><li><p>连接名称随意</p></li><li><p>主机为你安装了Redis的Linux系统的IP地址</p></li><li><p>由于没有设置用户名,所以选择通过Password进行验证,输入密码后点击连接即可</p></li></ul><p><img src="/../images/redis/redis1.1/2.png" alt="2"></p><p>如果确认信息都对,但是连接不上,则需要在Linux将Redis使用端口放行,再次测试连接即可</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">firewall-cmd --zone=public --add-port=6379/tcp --permanent</span><br></pre></td></tr></table></figure><h2 id="编程IDE客户端"><a href="#编程IDE客户端" class="headerlink" title="编程IDE客户端"></a>编程IDE客户端</h2><p><font size=4>  本章使用<strong>IntelliJ IDEA</strong>作为例子,说明如何在编程IDE中连接Redis</font></p><p>启动IntelliJ IDEA,打开任一项目</p><img src="../images/redis/redis1.1/3.png" style="zoom: 67%;" /><p>在右侧工具栏中选着Database,点击+号,选择Redis</p><img src="../images/redis/redis1.1/4.png" style="zoom:80%;" /><p>在弹出的窗口中输入正确的参数,点击左下角Test Connection测试连接,出现Succeeded即为连接成功,同样的方法也可以连接MySQL等其他数据库,与在Navicat中查看无异</p><p><img src="/../images/redis/redis1.1/5.png" alt="5"></p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p><font size=4>  至此,Redis入门篇(一)就结束了。通过本篇文章,我们初步了解了Redis作为一个基于内存的键值存储NoSQL数据库的特点和优势。Redis因其高速的读写性能、多样的数据类型支持和灵活的持久化方案,被广泛应用于需要快速数据访问的场景。此外,我们学习了Redis与传统关系型数据库的区别,掌握了基本的安装和配置方法,并了解了不同客户端(命令行客户端、图形化客户端和编程IDE客户端)的使用方式。通过这些内容,我们为进一步深入学习和使用Redis奠定了坚实的基础。</font></p>]]></content>
<categories>
<category> Redis </category>
</categories>
<tags>
<tag> Redis </tag>
</tags>
</entry>
<entry>
<title>后端技术栈</title>
<link href="/junwei/c38bdf9f.html"/>
<url>/junwei/c38bdf9f.html</url>
<content type="html"><![CDATA[<h1 id="后端技术栈"><a href="#后端技术栈" class="headerlink" title="后端技术栈"></a>后端技术栈</h1><p><font size=4>  本文旨在为您提供一份后端技术栈的速览指南,我们将深入浅出地解析其核心组件,帮助您快速掌握后端开发的关键技术要点。以下是技术栈的主要构成部分以及其的简单介绍:</font></p><h1 id="项目框架"><a href="#项目框架" class="headerlink" title="项目框架"></a>项目框架</h1><ul><li><font size=4><strong>Spring Boot</strong>: <a href="https://spring.io/projects/spring-boot">Spring Boot</a>是Spring框架的扩展,用于简化Spring应用程序的开发和部署。它通过提供一系列开箱即用的配置,减少了繁琐的配置工作。</font></li><li><font size=4><strong>Spring Security</strong>: <a href="https://spring.io/projects/spring-security">Spring Security</a>是一个强大的认证和授权框架,用于保护Java应用程序的安全。</font></li><li><font size=4><strong>Spring Task</strong>: <a href="https://spring.io/projects/spring-framework">Spring Task</a>用于简化在Spring应用程序中调度和执行任务的工作。</font></li><li><font size=4><strong>MyBatis</strong>: <a href="https://mybatis.org/">MyBatis</a>是一款优秀的数据持久层框架,它支持定制SQL、存储过程以及高级映射。</font></li><li><font size=4><strong>MyBatis-Plus</strong>: <a href="https://baomidou.com/">MyBatis-Plus</a>是在MyBatis基础上的增强工具,简化了CRUD操作,减少了开发者的工作量。</font></li><li><font size=4><strong>Swagger</strong>: <a href="https://swagger.io/">Swagger</a>是一种规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务。</font></li><li><font size=4><strong>Druid</strong>: <a href="https://github.com/alibaba/druid">Druid</a>是一个数据库连接池,它提供了监控和扩展功能,并且性能非常优秀。</font></li><li><font size=4><strong>Lombok</strong>: <a href="https://projectlombok.org/">Lombok</a>通过注解的方式,简化了Java代码中常见的重复代码,如getter、setter、构造函数等。</font></li><li><font size=4><strong>Hutool</strong>: <a href="https://hutool.cn/">Hutool</a>是一个小而全的Java工具包,通过简洁、底层、高效的工具类封装,提高了开发效率。</font></li></ul><h1 id="数据存储"><a href="#数据存储" class="headerlink" title="数据存储"></a>数据存储</h1><ul><li><font size=4><strong>MySQL</strong>: <a href="https://www.mysql.com/">MySQL</a>是最流行的关系型数据库管理系统之一,广泛应用于Web应用程序中。</font></li><li><font size=4><strong>Redis</strong>: <a href="https://redis.io/">Redis</a>是一个高性能的键值对存储数据库,广泛应用于缓存、会话管理等场景。</font></li><li><font size=4><strong>Elasticsearch</strong>: <a href="https://www.elastic.co/elasticsearch">Elasticsearch</a>是一个分布式的搜索和分析引擎,适用于实时全文搜索和分析。</font></li><li><font size=4><strong>MongoDB</strong>: <a href="https://www.mongodb.com/">MongoDB</a>是一个基于文档的NoSQL数据库,支持高性能、大数据量的存储和查询。</font></li><li><font size=4><strong>OSS</strong>: 阿里云对象存储服务(Object Storage Service,<a href="https://www.alibabacloud.com/product/oss">OSS</a>)是阿里云提供的海量、安全、低成本、高可靠的云存储服务。</font></li><li><font size=4><strong>MinIO</strong>: <a href="https://min.io/">MinIO</a>是一个高性能的对象存储服务,兼容Amazon S3云存储服务接口。</font></li><li><font size=4><strong>Linux</strong>: <a href="https://www.linux.org/">Linux</a>是一个开源的类Unix操作系统,是服务器操作系统的主流选择。</font></li><li><font size=4><strong>Docker</strong>: <a href="https://www.docker.com/">Docker</a>是一种容器技术,可以简化应用程序的部署和管理,确保在任何环境下的一致性。</font></li><li><font size=4><strong>Nginx</strong>: <a href="https://nginx.org/">Nginx</a>是一款高性能的HTTP和反向代理服务器,也可以作为负载均衡器使用。</font></li></ul><h1 id="微服务"><a href="#微服务" class="headerlink" title="微服务"></a>微服务</h1><ul><li><font size=4><strong>Spring Cloud</strong>: <a href="https://spring.io/projects/spring-cloud">Spring Cloud</a>为分布式系统中的配置管理、服务发现、断路器等模式提供了一系列工具。</font></li><li><font size=4><strong>Gateway</strong>: <a href="https://spring.io/projects/spring-cloud-gateway">Spring Cloud Gateway</a>是Spring Cloud中的API网关实现,用于处理所有请求的路由和提供统一的跨领域功能。</font></li><li><font size=4><strong>Nacos</strong>: <a href="https://nacos.io/">Nacos</a>是一个动态服务发现、配置管理和服务管理平台。</font></li><li><font size=4><strong>Oauth2</strong>: <a href="https://oauth.net/2">OAuth2</a>是一个开放标准,用于在不同服务之间进行安全授权。</font></li></ul><h1 id="开发工具"><a href="#开发工具" class="headerlink" title="开发工具"></a>开发工具</h1><ul><li><font size=4><strong>Intellij IDEA</strong>: <a href="https://www.jetbrains.com/idea">IntelliJ IDEA</a>是一款功能强大的Java集成开发环境(IDE),广泛应用于Java开发。</font></li><li><font size=4><strong>Git</strong>: <a href="https://git-scm.com/">Git</a>是一个分布式版本控制系统,用于高效地管理项目源代码。</font></li><li><font size=4><strong>Navicat</strong>: <a href="https://www.navicat.com/">Navicat</a>是一款数据库管理工具,支持多种数据库的可视化管理。</font></li><li><font size=4><strong>Postman/Apifox</strong>: <a href="https://www.postman.com/">Postman</a>和<a href="https://www.apifox.cn/">Apifox</a>是API测试工具,用于测试和调试API接口。</font></li><li><font size=4><strong>Arthas</strong>: <a href="https://arthas.aliyun.com/">Arthas</a>是Alibaba开源的Java诊断工具,帮助开发者诊断和调试生产环境问题。</font></li><li><font size=4><strong>Maven</strong>: <a href="https://maven.apache.org/">Maven</a>是一个项目管理和构建工具,用于依赖管理和项目构建。</font></li></ul><h1 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h1><ul><li><p><font size=4><strong>RabbitMQ</strong>: <a href="https://www.rabbitmq.com/">RabbitMQ</a>是一个消息代理,支持多种消息协议,用于在分布式系统中传递消息。</font></p></li><li><p><font size=4><strong>ELK</strong>: <a href="https://www.elastic.co/what-is/elk-stack">ELK</a>是Elasticsearch、Logstash和Kibana的组合,用于日志管理和分析。</font></p></li><li><p><font size=4><strong>JWT</strong>: JSON Web Token (<a href="https://jwt.io/">JWT</a>) 是一种开放标准(RFC 7519),用于作为各方之间传递信息的紧凑、安全的JSON对象。</font></p></li></ul><p><font size=4>以上是该后端技术栈的简要介绍,每种技术在实际开发中都有其独特的优势和应用场景,希望对你有所帮助。在掌握后端技术栈的路上,我们需要坚定信念,勇往直前。学习编程就是一个由混沌到有序的过程。如果碰到了理解不了的知识,不要怀疑自己是否适合编程,跳过就行了,这是再正常不过的事了——必须抱有一颗越挫越勇的心。请记住,不求面面俱到地掌握所有技术栈,根据自己需要和感兴趣的部分深入学习才是最好的。每种技术在实际开发中都有其独特的优势和应用场景,理解其中的关键部分将极大地提升你的开发能力。保持学习的热情和勇气,你一定能够在编程的道路上取得成功。</font></p>]]></content>
<categories>
<category> 后端 </category>
</categories>
<tags>
<tag> 后端 </tag>
</tags>
</entry>
<entry>
<title>欢迎来到我的博客</title>
<link href="/junwei/4a17b156.html"/>
<url>/junwei/4a17b156.html</url>
<content type="html"><![CDATA[<h1 id="学习计划"><a href="#学习计划" class="headerlink" title="学习计划"></a>学习计划</h1><p>  目前已经可以基于SpringBoot和Vue开发一个简易的Java Web项目了,可以自己实现前后端联调,也根据黑马的课程实现了Tails和苍穹外卖,学习Redis也就把黑马点评也做了一遍,过程中也把MP和Docker基础学了一下,剩下的就是往原理深入,JVM、Spring Boot的底层、Redis原理、MySQL进阶等等,这些就会比较枯燥,慢慢来吧,有空再把Spring Cloud学了。</p><h2 id="暑假和大三上计划"><a href="#暑假和大三上计划" class="headerlink" title="暑假和大三上计划"></a>暑假和大三上计划</h2><blockquote><p>暑假时间:2024/07/07~2024/09/01</p></blockquote><blockquote><p>放假第一周因为有学校的勤工任务而且还做了个国培班的培训助理,所以耽搁了点时间。简单地搭建了这个个人博客,花了几天在博客的美化和研究上面,花费了两周的时间</p><p>截止07月05日,本博客搭建成功</p><p>截止07月29日,Redis已经完成</p><p>截止07月30日,学习了MybatisPlus基础,与mybatis不同的地方,IService接口和BaseMapper接口和Db静态工具</p><p>截止07月30日,学习了Docker基础,作为后端开发的必备基础知识,懂得通过容器一键部署前后端,启动服务</p></blockquote><ul><li><input checked="" disabled="" type="checkbox"> 搭建自己的个人博客</li><li><input checked="" disabled="" type="checkbox"> 学习Redis</li><li><input checked="" disabled="" type="checkbox"> MybatisPlus基础,Docker基础</li><li><input disabled="" type="checkbox"> Java基础(老师没讲到的地方集合框架,网络编程,JVM,JUC,Java8新特性)</li><li><input disabled="" type="checkbox"> 软件工程师证书(大概11月份考试,在准备)</li><li><input disabled="" type="checkbox"> LeetCode100道题(完成10/100,因为没有回去学完Java的集合框架,很多API不是很懂,数据结构也要复习)</li></ul><h2 id="后续计划"><a href="#后续计划" class="headerlink" title="后续计划"></a>后续计划</h2><blockquote><p>计划在大三下的前半学期完成后续计划,毕竟还有学校的学习任务</p><p>争取在大三下后半学期开始简历投递,赶在秋招前先做个日常实习</p></blockquote><ul><li><input disabled="" type="checkbox"> 实现一个实战项目(等把Java四大件都学完了,知识储备到达一定程度)</li><li><input disabled="" type="checkbox"> 根据实战项目完成自己的简历</li><li><input disabled="" type="checkbox"> Java面经</li><li><input disabled="" type="checkbox"> 实现一个轮子项目(手写一个数据库)</li><li><input disabled="" type="checkbox"> Spring Cloud微服务(如果在实习前还有时间,估计没有,还要准备面试)</li></ul><h1 id="为什么选择计算机科学与技术?"><a href="#为什么选择计算机科学与技术?" class="headerlink" title="为什么选择计算机科学与技术?"></a>为什么选择计算机科学与技术?</h1><p>  根据数据来看,计算机科学与技术专业不仅在就业市场上具有很高的竞争力,而且在高校教育中也是极为热门的选择。随着科技的发展和信息化的推进,计算机科学与技术专业的重要性和吸引力持续上升。自己是小学开始接触电脑的,比较早的懂得了很多电脑的操作,从小学开始,班上的电脑有啥问题老师都会叫我看一下。又或者是很早前在家里和哥哥玩双人小游戏,都培养了自己对计算机的兴趣。高考前我就没有选专业的焦虑,我说我一定要学计算机,我就要学计算机,即使在报考的时候家人们都叫我选做老师,稳定还有寒暑假,和学生一起放假,但我还是选择了计算机。本可以去到更好的一个学校,但是达不到计算机的分数,所以就选了现在的大学,把计算机放在了第一志愿,也如愿选上了计科这个专业,算是圆梦了。出去可以说自己是科班出身,小小骄傲一下。还有一点就是自己家里不是什么富贵家庭,那时候看到计算机薪资挺高的哈哈哈,说不定能赚一笔,谁也不曾想到现在的计算机就业形势那么严峻,谁也想不到下一个风口是什么。</p><div style="display: flex; justify-content: center; align-items: center;"> <img src="../images/my/1.jpg" width="350"/> <img src="../images/my/6.png" width="400"/></div><h1 id="为什么自己不考研、不考公"><a href="#为什么自己不考研、不考公" class="headerlink" title="为什么自己不考研、不考公"></a>为什么自己不考研、不考公</h1><p>  还记得自己以前高中的时候,听到亲戚考上了研究生,自己也想着一定要提高自己的学历,考一个研究生。打算不考研究生是自己在大二上做的决定,带自己的老师都是研究生导师,偶尔会给我们聊一下考研究生的事情。考研究生怎么说呢,需要自己沉下心来好好研究,并且有刻苦钻研的精神,但是我自己比较活泼好动,感觉自己不能完全沉下心来搞科研,而且考虑到专业的特殊性,假如我没考上211等好一点的学校,那么读完研究生出去可能也还是走后端的方向,又因为研究生要搞科研那些,学的肯定不是后端的东西,如果再捡起来后端的知识可能还不如本科毕业,得到的只是研究生的一个头衔,当然,这个头衔也能让自己的优势大大增加。还是专业的特殊性,企业比较看重的是个人的技术能力,如果没有好一点的实习经历和工作经验,也难去到好的大公司,他需要较长的工作经历,比如三年以上才能达到中级开发工程师的一个水平。综合这些,考研三年 or 工作三年经历,我还是选择了直接去就业,并准备自己的知识,为实习做准备,三年后看看能不能去到一个比较好的公司。(<strong>仅代表个人观点,因人而异</strong>)</p><p>  根据数据来看,理工类毕业生考公的也是比较少的,当然还是有的,看大家对自己的要求和向往吧。考公上岸确实会比较稳定一点,老一辈的想法就是想着越稳越好,也确实,工作难找。但是我不太喜欢太稳的,我想让自己出去闯一闯世界,看看会怎么样。另外一种就是,计科毕业其实也可以去到国企或银行中就业,也相对稳定,而且会比互联网公司轻松一点,没有那么大的压力,也没有那么多工作,还逃不掉经常加班。如果去到了小公司,就什么都要干,全栈开发。去到大公司会相对少一些活,专注自己的那份活,累了也可以休息一下,完成自己的任务即可。大概了解到这些,毕竟自己还是个准大三学生。</p><div style="display: flex; justify-content: center; align-items: center;"> <img src="../images/my/3.jpg" width="399"/> <img src="../images/my/5.jpg" width="399"/></div><h1 id="当下就业环境是否需要焦虑?"><a href="#当下就业环境是否需要焦虑?" class="headerlink" title="当下就业环境是否需要焦虑?"></a>当下就业环境是否需要焦虑?</h1><p>  根据数据来看求职迷茫的同学还是很多的,特别是和我一样的双非院校。</p><p>  普通人,压根没必要去操心太多大环境的事,大环境卷和不卷,难和不难,与单独的个体其实关联性不是特别大。找不到工作的人,要么学历差,要么技术差,要么两个都差,学到了东西找一份工作其实并没有那么难,我是这样认为的,肯学就好。如果你只是按照学校的计划学习那点皮毛,为了完成一个课程大作业去学习,那么不被淘汰谁被淘汰。学校又不会教微服务和中间件,但是开发中又确确实实用到,不自学怎么会找到工作。已经不再是十年前,按部就班教材,那个会CRUD就可以胜任程序员的工作的年代了。</p><p>  偶尔有些焦虑是正常的,像我也偶尔活在焦虑当中,大二上才学Java,大二下学Java Web中的JSP和Servlet,因为准备一个网页设计大赛,自学了Vue和SpringBoot,然后就会发现自己觉悟的还是太晚了,也怀疑为什么大二下才开始Java Web的学习,转眼就到了大三上了。<br>  如果到了大三还懵懵懂懂,那么会被社会鞭打的自我怀疑。如果是跟着老师在大三上才学Spring Boot那么只能把很多该学的放到大三下了,这样大三下的压力就会非常大,毕竟你需要靠自己去理解JVM的知识和熟悉JUC的知识,还要学习中间件和做自己的项目,不然简历上就会空空如也。在这些其中你还要考一些证,比如四六级,软考证书。软考又是一本厚厚的书,当然也是看你计算机基础(计组、计网、操作系统、数据库)学的怎么样,复习时间就会不一样。最后的最后为了面试,你还得去加上自己的理解去背面试八股文,战线太长,有些知识难免会忘。</p><p>  企业还是看重实习,好好准备知识去面试,投简历去实习还是非常关键的。企业更希望能找到能快速上手的人,好培养,就看你在自己项目上下的功夫了。</p><p>  不让自己陷入到无限的焦虑当中,因为焦虑这些对求职一点帮助也没有。最好就是一路小跑,不停不靠,每天都坚持让自己自学一点,积少成多。如果睡不好,学不下去的时候也可以打游戏kill一下time,我是倡导劳逸结合的(白天能做的,别熬夜做,不然第二天整个人的精神就废了),我会保证每天的游戏时间,让自己在睡前放松下,方便第二天接着干。</p><p>  按照自己的节奏,剩下的就交给时间吧,当然想要进好一点的公司,还有些运气的成分,谁也不知道面试题和算法题会出到什么,只能说自己好好努力,不留遗憾!</p><div style="display: flex; justify-content: center; align-items: center;"> <img src="../images/my/2.jpg" width="399"/> <img src="../images/my/4.jpg" width="399"/></div><h1 id="写博客有用吗?"><a href="#写博客有用吗?" class="headerlink" title="写博客有用吗?"></a>写博客有用吗?</h1><p>  学校教的知识都是些基础,像计组、计网、操作系统等等,都是最为基础的计算机知识,即使教了编程语言,也还是最基础的内容,而且每周几节课根本就很难学到太多东西。</p><p>  拿Java来说吧:老师讲了前面的基础知识,但是真正重要的JVM(Java虚拟机)、JUC(并发编程)、网络编程,甚至是Java8的新特性(Lambda、Stream流),老师都没有提及,Collection集合框架也是含糊带过,而且课堂上从来没见老师敲过代码进行演示,只是PPT演示。后面学到的一些技术和框架,也是为了完成课时任务能做出大作业就行,很多同学都是CV一下,都不需要思考。</p><p>  当然也不能说是老师的错,老师有自己的研究生要带,也有自己的科研项目等等其他事情,本科教学只占据了极小部分。所以上课按着大纲讲,能讲到哪就考到哪,也是很正常的事情。可能好一点的学校会好一点?我也不清楚。况且技术日新月异,以前的课程教材当然不会那么快跟上,而且一些老师也离开企业很久了,来到学校教书,对企业的要求了解可能又会少一些。</p><p>  那么你就需要自学了,自学的话没有博客之前我是很少做笔记的,跟着视频敲下代码就一遍带过了。即使做了笔记,也是做一些关键的思路,想看代码就打开源文件查看。回头发下有很多细节自己会忘记,即使整体框架流程不会那么快忘记。其中的一些编程细节和技巧,还是得记录下来的,把代码也顺便放入博客中,这样离开电脑也能温习一下思路。</p><p>  个人博客不太和CSDN那些一样,既然是个人博客,我觉得重要是记录自己的学习之路,当然如果别人看了也能学到东西那当然两全其美。最重要的是当我忘记知识的时候,我可以快速查找,比起纸质版,博客更容易随时查看,也是当初自己花时间话精力写的,自己会熟悉一点。比如我要查找Redis的一些基础命令,我就可以到我的博客找,快速定位,我知道我写过。而到了CSDN上别人写的可能不是自己想要的,自己只是想速查一下,却看到了很多乱七八糟的东西……</p><p>  当然,在学习技术的时候,本就应该先看视频讲解一遍,自己再尝试敲一下代码完成,这其中已经花费了大量时间。突然要写个博客,就更花费了我的时间,写文章要好好地排版,思考怎么样总结可以让自己以后更方便查看,还需要插入自己实战过程中的一些截图帮助理解,确实会花费很多时间。有写博客的时间又可以看多几集教学视频,后来想想,写博客确实还是益处大一点,加深了知识的记忆,而不是为了赶进度而赶视频,看完一章就可以停下来总结一下。毕竟,老师总结的总归不是自己总结的,欠点意思,除非是理论。</p>]]></content>
</entry>
</search>