-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
327 lines (188 loc) · 249 KB
/
atom.xml
File metadata and controls
327 lines (188 loc) · 249 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>小楼一夜醉酒</title>
<link href="http://yoursite.com/atom.xml" rel="self"/>
<link href="http://yoursite.com/"/>
<updated>2021-08-18T07:14:04.310Z</updated>
<id>http://yoursite.com/</id>
<author>
<name>Ran Binkesi</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>sql注入攻击</title>
<link href="http://yoursite.com/2021/07/20/sql%E6%B3%A8%E5%85%A5%E6%94%BB%E5%87%BB/"/>
<id>http://yoursite.com/2021/07/20/sql%E6%B3%A8%E5%85%A5%E6%94%BB%E5%87%BB/</id>
<published>2021-07-20T06:15:26.510Z</published>
<updated>2021-08-18T07:14:04.310Z</updated>
<content type="html"><![CDATA[<h2><span id="目录">目录</span></h2><ul><li>最基础的注入 - union 注入攻击</li><li>Boolean 注入攻击 - 布尔盲注</li><li>报错注入攻击</li><li>时间注入攻击 - 时间盲注</li><li>堆叠查询注入攻击</li><li>二次注入攻击</li><li>宽字节注入攻击</li><li>base64 注入攻击</li><li>cookie 注入攻击 - http 请求头参数注入</li><li>XFF 注入攻击 - http 请求头参数注入</li><li>知道绝对路径的注入<a id="more"></a></li></ul><h2><span id="0x01-最基础的注入-union-注入攻击">0x01 最基础的注入 - union 注入攻击</span></h2><ol><li><p>判断是 get 型还是 post 型注入;</p></li><li><p>找到正确的闭合规则;</p></li><li><p>order by 查询字段数;</p></li><li><p>union select 1,2….. 查看显示位是第几位,没有的话就试试把 id=1 的显示位让出来,让其等于 id=-1;</p><p><img src="https://img2018.cnblogs.com/blog/1740059/201909/1740059-20190930104009938-51110633.png" alt="img"></p></li><li><p>第二、三位显示出来了,那么即可在这两个位置写入 sql 语句;</p></li><li><p>查询当前数据库, 当前 mysql 用户 union select 1,user(),database();</p></li><li><p>查询当前数据库里面的表 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=”data_name”;</p></li><li><p>查询到敏感表名 user,继续查询表里面的字段 union select 1,2,group_concat(column_name) from information_schema.columns where table_name=”user”;</p></li><li><p>查询字段,例如 “id”、”passwd” 的内容 union select 1,id,passwd from user;</p></li><li><p>拿到用户、密码登入后台。</p></li></ol><h2><span id="0x02-boolean-注入攻击-布尔盲注">0x02 Boolean 注入攻击 - 布尔盲注</span></h2><ul><li>查看现象,能报错,但没有报错信息,正确查询也显示不了查询内容就属于布尔盲注,只存在两种状态,对或错;</li><li>由页面的两种不同返回的状态来判定我们的闭合规则;</li><li>为了方便,我们这里假设返回正确用 “yes”,返回错误用“no” 来表示这两种状态</li><li>找到闭合规则后,我们在闭合规则里面 and 1=1 和 and 1=2 测试一下,看看最后返回是不是两种状态;</li><li>布尔盲注要用到 length() 和 substr() 语句,用两种状态来猜解数据库、表名等的长度和正确字母;</li><li>先用 and length(database())>2 来猜数据库的长度,使用的是二分法;</li><li>再用 and substr(database(),1,1)=’t’ 来确定第一个字母,可用 burp 跑,26 个字母, 哪个字母返回 yes 则代表第一个字母就是它;</li><li>and substr(database(),2,1)=’t’ 代表当前数据库的第二个字母;</li><li>最后结合长度,成功的将数据库猜解出来;</li><li>后面的操作跟 union 注入的步骤差不多了,只是 sql 语句写在 上文的 database() 处。</li></ul><h2><span id="0x03-报错注入攻击">0x03 报错注入攻击</span></h2><ol><li><p>只要注入点有 sql 报错信息,那么就可以使用报错注入;</p></li><li><p>还是一样,引号报错,然后找到闭合规则,页面正常显示,则可以在闭合规则中开始写入报错注入的 sql 语句;</p></li><li><p>updatexml 报错获取当前数据库:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">and updatexml(1,concat(0x7e,(select database()),0x7e),1)</span><br></pre></td></tr></table></figure></li><li><p>floor 报错获取当前数据库:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">and (select 1 from (select count(*),concat((database()),floor (rand(0)*2))x from information_schema.tables group by x)a)</span><br></pre></td></tr></table></figure></li><li><p>两种方式都可行,如果第一个不行就试试第二个</p></li><li><p>接着可以利用 select 语句替换掉上面 database() 来继续获取数据库中的表名、字段名,查询语句和 union 注入攻击的语句相同;</p></li><li><p>只不过这里不能再使用 group_concat 了,因为报错注入只显示一条结果,所以需要使用 limit 语句;</p></li></ol><h2><span id="0x04-时间注入攻击-时间盲注">0x04 时间注入攻击 - 时间盲注</span></h2><ul><li><p>没有明确的现象,不管是对是错都返回一个状态;</p></li><li><p>但是如果用 sleep(5) 方法,能让响应时间延迟为 5 秒以上,那么就为时间盲注;</p></li><li><p>我们用 sleep(5) 函数构造了一个时间延时的状态,因此,我们又有了两种状态,像布尔盲注一样可以根据这两种状态来判定数据库、表名和字段名的长度和正确的每个字母;</p></li><li><p>同样的找到正确的闭合规则,当然,这个闭合规则得配合着 and sleep(5) 语句来构造,哪一个闭合规则执行了 sleep(5),那么就是正确得闭合规则;</p></li><li><p>时间盲注配合着 if(A,B,C) 语句结合使用,含义是:如果 A 是 true,则返回 B(也就是执行 B),否则返回 C(执行 C);</p></li><li><p>那么判断当前数据库名的长度的语句为:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">if (length(database())>1,sleep(5),1)</span><br></pre></td></tr></table></figure><p>就是说如果数据库长度大于 1,那么响应延时 5 秒,否则执行 select 1(也就是不延时),由此来推出数据库长度。</p></li><li><p>判断当前数据库名的第一个和第二个字母的语句:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">if(substr(database(),1,1)='s',sleep(5),1)</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">if(substr(database(),2,1)='s',sleep(5),1)</span><br></pre></td></tr></table></figure><p>只有第一个字母等于 26 个字母中正确的字母时,才会延时 5 秒,因此可以通过 burp 或者 sqlmap 来跑。</p></li><li><p>根据数据库名长度以此内推即可得出完整的数据库的库命、表名、字段名和具体内容。</p></li></ul><h2><span id="0x05-堆叠查询注入攻击">0x05 堆叠查询注入攻击</span></h2><ol><li><p>可以使用堆叠注入的地方也可以使用布尔盲注与时间盲注;</p></li><li><p>同样先找出正确的闭合规则,然后也看两种状态来猜解库名、表名等;</p></li><li><p>类似与下面在分号后面可执行新的语句:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">;select if(length(database())>1,sleep(3),1)</span><br><span class="line">;select if(substr(database(),1,1)='r',sleep(3),1)</span><br></pre></td></tr></table></figure></li><li><p>堆叠的; 分号后可以执行新的 sql 语句,因此在知道网站根目录的情况下可以直接写日志拿 shell。</p></li></ol><h2><span id="0x06-二次注入攻击">0x06 二次注入攻击</span></h2><ul><li>二次注入一共有两个 url,url 一用来注入,也就是注入点,插入 sql 语句的地方,另外一个 url 用来返回信息;</li><li>也就是 url 一插入了 sql 语句,url 一的响应里面就会返回这条信息对应的 id 值,然后 url 二就传入这个新 id 值,然后访问,响应回来之后将会爆出 sql 语句查询的结果,正确或者错误的 sql 信息;</li><li>就相当于 url 是一个用户注册的地方,用户注册后会在数据库里面加入新 id 存放用户的注册信息,那么这个 id 可以传给 url 二来访问,url 二就可以显示出用户的注册信息,但如果注册信息含义恶意 sql 语句,url 二就会显示出敏感的数据库信息;</li><li>跟 union 注入攻击差不多,只是回显的信息需要在另外的 url 中显示出来了;</li><li>后面就是 union 注入攻击的常规操作。</li></ul><h2><span id="0x07-宽字节注入攻击">0x07 宽字节注入攻击</span></h2><ol><li>如果遇到单、双引号被转义,变成了反斜杠,导致参数 id 无法逃逸单引号的包围;</li><li>一般情况下,此处就不存在 sql 注入漏洞的;</li><li>但是如果数据库的编码为 GBK 时,就可以使用宽字节注入,因此在不知道是否是 GBK 编码时,都可以尝试去使用宽字节注入;</li><li>宽字节的格式是在地址后先加一个 %df ,再加单引号,因为反斜杠的编码为 %5c,在 GBK 编码中,%df%5c 是繁体字 “連”,因此,单引号成功逃逸,爆出 sql 错误;</li><li>因此构造闭合规则时,在单引号前面加上 %df 就行了;</li><li>之后在闭合规则中写入同 union 注入的一些查询语句就行了;</li></ol><h2><span id="0x08-base64-注入攻击">0x08 base64 注入攻击</span></h2><ul><li>如果遇到 url 的参数 id 的值看起来像 base64 的,先拿去 url 解码,然后如果是 base64,拿去 base64 解码,解出来的应该就是 id 的值(1,2 等数字);</li><li>那么如果要对这个 url 进行 sql 注入测试,就需要对 id 后面的所有值进行 base64 编码;</li><li>注入的方式步骤都是跟 union 注入一样的,只不过后面的所有值(整个 payload)都要进行 base64 编码后传给 url 的 c 参数提交,包括闭合规则。</li></ul><h2><span id="0x09-http-请求头参数注入-cookie-注入攻击">0x09 HTTP 请求头参数注入 - cookie 注入攻击</span></h2><ol><li><p>抓包对一个 url 的 http 请求头的所有参数进行 sql 注入测试,里面的所有参数都有可能存在注入点,如果响应包出现 sql 报错,那么测试的这个参数就是注入点;</p></li><li><p>常见的 http 头部注入的参数有:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">【Referer】、【X-Forwarded-For】、【Cookie】、【X-Real-IP】、【Accept-Language】、【Authorization】</span><br></pre></td></tr></table></figure></li><li><p>如果测试到 cookie 参数的时候,响应有报 sql 错误,那么就是 cookie 注入攻击;</p></li><li><p>和 union 注入的差别就在于注入点不一样,之后使用 union 注入的方法即可。</p></li></ol><h2><span id="0x10-http-请求头参数注入-xff-注入攻击">0x10 HTTP 请求头参数注入 - XFF 注入攻击</span></h2><ul><li>XFF 注入即 HTTP 头部的 X-Forwarded-for 参数存在 sql 注入;</li><li>例如测试此参数的值 X-Forwarded-for:127.0.0.1’ 响应有 sql 报错,那么此处就是注入点;</li><li>之后使用 union 注入的方法完成即可。</li></ul><h2><span id="0x11-知道绝对路径的注入">0x11 知道绝对路径的注入</span></h2><ol><li>如果通过一些方式爆出了网站的根目录,并且知道此站点存在 sql 注入;</li><li>猜测此数据库可能有 file 权限,那么我们就可以使用语句:into outfile 来写 shell 到网站的根目录下,之后用菜刀连接;</li><li>如果数据库没有 file 权限,那么我们用 sqlmap 的参数 –is-dba 来查看当前数据库的用户是否有管理员权限;</li><li>如果有管理员权限,我们就可以使用 sqlmap 里面的参数命令 –os-shell 来上传、反弹 shell,最终 getshell;</li><li>如果 file、管理员权限都没有,那么另寻思路,日志、缓存写入等。</li></ol>]]></content>
<summary type="html"><h2 id="目录"><a href="#目录" class="headerlink" title="目录"></a>目录</h2><ul>
<li>最基础的注入 - union 注入攻击</li>
<li>Boolean 注入攻击 - 布尔盲注</li>
<li>报错注入攻击</li>
<li>时间注入攻击 - 时间盲注</li>
<li>堆叠查询注入攻击</li>
<li>二次注入攻击</li>
<li>宽字节注入攻击</li>
<li>base64 注入攻击</li>
<li>cookie 注入攻击 - http 请求头参数注入</li>
<li>XFF 注入攻击 - http 请求头参数注入</li>
<li>知道绝对路径的注入</summary>
<category term="Sql注入" scheme="http://yoursite.com/tags/Sql%E6%B3%A8%E5%85%A5/"/>
<category term="Web攻击" scheme="http://yoursite.com/tags/Web%E6%94%BB%E5%87%BB/"/>
</entry>
<entry>
<title>XSS 漏洞</title>
<link href="http://yoursite.com/2021/07/20/XSS%20%E6%BC%8F%E6%B4%9E/"/>
<id>http://yoursite.com/2021/07/20/XSS%20%E6%BC%8F%E6%B4%9E/</id>
<published>2021-07-20T06:08:11.337Z</published>
<updated>2021-08-18T07:14:05.787Z</updated>
<content type="html"><![CDATA[<p>跨站脚本漏洞 (XSS) 基础讲解</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-834e76bc8c471138.png" alt="img"></p><h1><span id="xss-漏洞">XSS 漏洞</span></h1><h2><span id="一-文章简介">一、文章简介</span></h2><p>XSS 漏洞是 Web 应用程序中最常见的漏洞之一。如果您的站点没有预防 XSS 漏洞的固定方法,那么很可能就存在 XSS 漏洞。<br>这篇文章将带你通过代码层面去理解三个问题:</p><a id="more"></a><ul><li>什么是 XSS 漏洞?</li><li>XSS 漏洞有哪些分类?</li><li>如何防范 XSS 漏洞?</li></ul><h2><span id="二-xss-漏洞简介">二、XSS 漏洞简介</span></h2><p>跨站脚本攻击是指恶意攻击者往 Web 页面里插入恶意 Script 代码,当用户浏览该页之时,嵌入其中 Web 里面的 Script 代码会被执行,从而达到恶意攻击用户的目的。<br>xss 漏洞通常是通过 php 的输出函数将 javascript 代码输出到 html 页面中,通过用户本地浏览器执行的,所以 xss 漏洞关键就是<strong>寻找参数未过滤的输出函数</strong>。<br>常见的输出函数有: <code>echo printf print print_r sprintf die var-dump var_export</code>.</p><p><strong>xss 分类:(三类)</strong></p><ul><li>反射型 XSS:<strong><非持久化></strong> 攻击者事先制作好攻击链接, 需要欺骗用户自己去点击链接才能触发 XSS 代码(服务器中没有这样的页面和内容),一般容易出现在搜索页面。</li><li>存储型 XSS:<strong><持久化></strong> 代码是存储在服务器中的,如在个人信息或发表文章等地方,加入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,每当有用户访问该页面的时候都会触发代码执行,这种 XSS 非常危险,容易造成蠕虫,大量盗窃 cookie(虽然还有种 DOM 型 XSS,但是也还是包括在存储型 XSS 内)。</li><li>DOM 型 XSS:基于文档对象模型 Document Objeet Model,DOM) 的一种漏洞。DOM 是一个与平台、编程语言无关的接口,它允许程序或脚本动态地访问和更新文档内容、结构和样式,处理后的结果能够成为显示页面的一部分。DOM 中有很多对象,其中一些是用户可以操纵的,如 uRI ,location,refelTer 等。客户端的脚本程序可以通过 DOM 动态地检查和修改页面内容,它不依赖于提交数据到服务器端,而从客户端获得 DOM 中的数据在本地执行,如果 DOM 中的数据没有经过严格确认,就会产生 DOM XSS 漏洞。</li></ul><h2><span id="三-xss-漏洞原理">三、XSS 漏洞原理</span></h2><h3><span id="31-反射型-xss">3.1 反射型 XSS</span></h3><p>在黑盒测试中,这种类型比较容易通过漏洞扫描器直接发现,我们只需要按照扫描结果进行相应的验证就可以了。</p><p>相对的在白盒审计中, 我们首先要寻找带参数的输出函数,接下来通过输出内容回溯到输入参数,观察是否过滤即可。<br>无案例不足以求真,这里我们选用<code>echo()函数</code>作为实例来分析:</p><p>新建 XssReflex.php,代码如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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"><html></span><br><span class="line"><head> </span><br><span class="line"><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </span><br><span class="line"><title>XSS</title> </span><br><span class="line"></head> </span><br><span class="line"><body> </span><br><span class="line"><form action="" method="get"> </span><br><span class="line"><input type="text" name="input"> </span><br><span class="line"><input type="submit"> </span><br><span class="line"></form> </span><br><span class="line"><br> </span><br><span class="line"><?php </span><br><span class="line">$XssReflex = $_GET['input'];</span><br><span class="line">echo 'output:<br>'.$XssReflex;</span><br><span class="line">?> </span><br><span class="line"></body> </span><br><span class="line"></html></span><br></pre></td></tr></table></figure><p>这是一个很简单、也很常见的页面:<br>变量 $XssReflex 获取 get 方式传递的变量名为 input 的变量值(值为一个字符串),然后直接通过 echo() 函数输出,注意这中间并未对用户输入进行任何过滤。</p><p>打开 Firefox 输入 url:<code>localhost/codeaudit/xss/XssReflex.php</code> :</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-aff389d031433dce.png" alt="img"></p><p>当我们输入 <code>1</code> ,页面返回 1 :</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-e74b7a5448e51311.png" alt="img"></p><p>当我们输入<code>hello</code>时,页面返回 hello :</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-60cffe4d6cd992aa.png" alt="img"></p><p>以上都为正常的输出,但如果我们输出一些<code>javascript</code>代码呢?</p><p>比如我们输入<code><script>alert('xss')</script></code> :</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-909939f59402f6b7.png" alt="img"></p><p>可以看到浏览器成功弹窗,说明我们输出的 JavaScript 代码成功被执行了。<br>我们查看网页 html 代码:</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-4b3951ff10f4d8df.png" alt="img"></p><p>第 12 行增加了:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><script>alert('xss')</script></span><br></pre></td></tr></table></figure><p>这个弹窗并没有什么实际的意义,但通过它我们知道输入 javascript 代码是可以被执行的,当我们输入一些其他函数,比如<code>document.cookie</code>就可以成功盗取用户的 cookie 信息,或者读取用户浏览器信息等,为我们进一步深入攻击做铺垫。</p><h3><span id="32-存储型-xss">3.2 存储型 XSS</span></h3><p>和反射性 XSS 的即时响应相比,存储型 XSS 则需要先把利用代码保存在比如数据库或文件中,当 web 程序读取利用代码时再输出在页面上执行利用代码。但存储型 XSS 不用考虑绕过浏览器的过滤问题,屏蔽性也要好很多。<br>存储型 XSS 攻击流程:</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-07ab49e8b1ea148a.png" alt="img"></p><p>存储型 XSS 的白盒审计同样要寻找未过滤的输入点和未过滤的输出函数。</p><p>使用 cat 命令查看 XssStorage.php 代码</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">shiyanlou:~/ $ cat XssStorage.php</span><br></pre></td></tr></table></figure><p>代码如下:< 参考自<code>JackholeLiu的博客</code>></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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 style="font-size:18px;"><meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> </span><br><span class="line"><html> </span><br><span class="line"><head> </span><br><span class="line"><title>XssStorage</title> </span><br><span class="line"></head> </span><br><span class="line"><body> </span><br><span class="line"><h2>Message Board<h2> </span><br><span class="line"><br></span><br><span class="line"><form action="XssStorage.php" method="post"> </span><br><span class="line">Message:<textarea id='Mid' name="desc"></textarea> </span><br><span class="line"><br> </span><br><span class="line"><br> </span><br><span class="line">Subuser:<input type="text" name="user"/><br> </span><br><span class="line"><br></span><br><span class="line"><input type="submit" value="submit" onclick='loction="XssStorage.php"'/> </span><br><span class="line"></form> </span><br><span class="line"><?php </span><br><span class="line">if(isset($_POST['user'])&&isset($_POST['desc'])){ </span><br><span class="line">$log=fopen("sql.txt","a"); </span><br><span class="line">fwrite($log,$_POST['user']."\r\n"); </span><br><span class="line">fwrite($log,$_POST['desc']."\r\n"); </span><br><span class="line">fclose($log); </span><br><span class="line">} </span><br><span class="line"> </span><br><span class="line">if(file_exists("sql.txt")) </span><br><span class="line">{ </span><br><span class="line">$read= fopen("sql.txt",'r'); </span><br><span class="line">while(!feof($read)) </span><br><span class="line">{ </span><br><span class="line"> echo fgets($read)."</br>"; </span><br><span class="line">} </span><br><span class="line">fclose($read); </span><br><span class="line">} </span><br><span class="line">?> </span><br><span class="line"></body> </span><br><span class="line"></html></span></span><br></pre></td></tr></table></figure><p>页面功能简述:</p><p>这个页面采用 POST 提交数据,生成、读取文本模拟数据库,提交数据之后页面会将数据写入 sql.txt,再打开页面时会读取 sql.txt 中内容并显示在网页上,实现了存储型 xss 攻击模拟。</p><p>打开 Firefox 输入 url:<code>localhost/codeaudit/xss/XssStorage.php</code> :</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-01d967d16884959b.png" alt="img"></p><p>我们随意输出一些内容:</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-75db875d30a09d1b.png" alt="img"></p><p>可以看到页面正常显示页面留言信息。<br>当我们在 Message 中输入<code><script>alert('xss')</script></code>时,页面成功弹窗 :</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-481b85eeb80c2726.png" alt="img"></p><p>并且我们重启浏览器之后再加载该页面,页面依然会弹窗, 这是因为恶意代码已经写入数据库中,每当有人访问该页面时,恶意代码就会被加载执行!</p><p>我们查看网页 html 代码:</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-9b17baa60cba0d96.png" alt="img"></p><p>这就是所谓的存储型 XSS 漏洞,<strong>一次提交之后,每当有用户访问这个页面都会受到 XSS 攻击,危害巨大。</strong></p><h3><span id="33-dom-xss">3.3 DOM XSS</span></h3><p>这种 XSS 用的相对较少,并且由于其特殊性,常见的漏扫工具都无法检测出来,这里先不做讲解。</p><p>记个待办,以后来补!</p><h2><span id="四-xss-漏洞防范">四、XSS 漏洞防范</span></h2><h3><span id="41-反射型-xss-漏洞防范">4.1 反射型 xss 漏洞防范</span></h3><p>php 中 xss 的漏洞防范方法总结:<参考自 Segmentfault></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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">A.PHP直接输出html的,可以采用以下的方法进行过滤:</span><br><span class="line"></span><br><span class="line"> 1.htmlspecialchars函数</span><br><span class="line"> 2.htmlentities函数</span><br><span class="line"> 3.HTMLPurifier.auto.php插件</span><br><span class="line"> 4.RemoveXss函数</span><br><span class="line"></span><br><span class="line">B.PHP输出到JS代码中,或者开发Json API的,则需要前端在JS中进行过滤:</span><br><span class="line"></span><br><span class="line"> 1.尽量使用innerText(IE)和textContent(Firefox),也就是jQuery的text()来输出文本内容</span><br><span class="line"> 2.必须要用innerHTML等等函数,则需要做类似php的htmlspecialchars的过滤</span><br><span class="line"></span><br><span class="line">C.其它的通用的补充性防御手段</span><br><span class="line"></span><br><span class="line"> 1.在输出html时,加上Content Security Policy的Http Header</span><br><span class="line"> (作用:可以防止页面被XSS攻击时,嵌入第三方的脚本文件等)</span><br><span class="line"> (缺陷:IE或低版本的浏览器可能不支持)</span><br><span class="line"> 2.在设置Cookie时,加上HttpOnly参数</span><br><span class="line"> (作用:可以防止页面被XSS攻击时,Cookie信息被盗取,可兼容至IE6)</span><br><span class="line"> (缺陷:网站本身的JS代码也无法操作Cookie,而且作用有限,只能保证Cookie的安全)</span><br><span class="line"> 3.在开发API时,检验请求的Referer参数</span><br><span class="line"> (作用:可以在一定程度上防止CSRF攻击)</span><br><span class="line"> (缺陷:IE或低版本的浏览器中,Referer参数可以被伪造)</span><br></pre></td></tr></table></figure><p>这里我们选用 htmlentities() 函数进行测试:</p><p>htmlentities() 函数把字符转换为 HTML 实体。</p><p>新建 Xss_htmlentities.php, 代码如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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"><html></span><br><span class="line"><head> </span><br><span class="line"><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </span><br><span class="line"><title>XSS</title> </span><br><span class="line"></head> </span><br><span class="line"><body> </span><br><span class="line"><form action="" method="get"> </span><br><span class="line"><input type="text" name="input"> </span><br><span class="line"><input type="submit"> </span><br><span class="line"></form> </span><br><span class="line"><br> </span><br><span class="line"><?php </span><br><span class="line">$XssReflex = $_GET['input'];</span><br><span class="line">echo 'output:<br>'.htmlentities($XssReflex);#仅在这里对变量 $XssReflex 做了处理.</span><br><span class="line">?> </span><br><span class="line"></body> </span><br><span class="line"></html></span><br></pre></td></tr></table></figure><p>在 Firefox 输入 url:<code>localhost/codoaudit/xss/Xsshtmlentities.php</code> :</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-b0177ceb2ba27531.png" alt="img"></p><p>当我们输入<code><script>alert('xss')</script></code> :</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-f8821c2140b06378.png" alt="img"></p><p>可以看到页面并没有弹窗。<br>我们再查看网页 html 代码:</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-678390ac7147eb2b.png" alt="img"></p><p>可以看到 htmlentities() 函数对用户输入的<code><></code>做了转义处理, 恶意代码当然也就没法执行了。<br>还有其他过滤函数,纸上学来终觉浅,有兴趣的同学可以自己去尝试一番</p><h3><span id="42-存储型-xss-漏洞防范">4.2 存储型 xss 漏洞防范</span></h3><p>存储型 XSS 对用户的输入进行过滤的方式和反射型 XSS 相同,这里我们使用<code>htmlspecialchars()</code>函数进行演示:</p><p>htmlentities() : 把预定义的字符 “<” (小于)和 “>” (大于)转换为 HTML 实体</p><p>htmlspecialchars 和 htmlentities 的区别:</p><p>htmlspecialchars 只转义 <code>& 、" 、' 、< 、></code> 这几个 html 代码,而 htmlentities 却会转化所有的 html 代码,连同里面的它无法识别的中文字符也会转化。</p><p>新建 Xss_htmlspecialchars_Storage.php ,代码如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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 style="font-size:18px;"><meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> </span><br><span class="line"><html> </span><br><span class="line"><head> </span><br><span class="line"><title>XssStorage</title> </span><br><span class="line"></head> </span><br><span class="line"><body> </span><br><span class="line"><h2>Message Board<h2> </span><br><span class="line"><br></span><br><span class="line"><form action="Xss_htmlspecialchars_Storage.php" method="post"> </span><br><span class="line">Message:<textarea id='Mid' name="desc"></textarea> </span><br><span class="line"><br> </span><br><span class="line"><br> </span><br><span class="line">Subuser:<input type="text" name="user"/><br> </span><br><span class="line"><br></span><br><span class="line"><input type="submit" value="submit" onclick='loction="XssStorage.php"'/> </span><br><span class="line"></form> </span><br><span class="line"><?php </span><br><span class="line">if(isset($_POST['user'])&&isset($_POST['desc'])){ </span><br><span class="line">$log=fopen("sqlStorage.txt","a"); </span><br><span class="line">fwrite($log,htmlspecialchars($_POST['user'])."\r\n"); # 在此对用户输入数据$_POST['user']进行过滤</span><br><span class="line">fwrite($log,htmlspecialchars($_POST['desc'])."\r\n"); # 在此对用户输入数据$_POST['desc']进行过滤</span><br><span class="line">fclose($log); </span><br><span class="line">} </span><br><span class="line"> </span><br><span class="line">if(file_exists("sqlStorage.txt")) </span><br><span class="line">{ </span><br><span class="line">$read= fopen("sqlStorage.txt",'r'); </span><br><span class="line">while(!feof($read)) </span><br><span class="line">{ </span><br><span class="line"> echo fgets($read)."</br>"; </span><br><span class="line">} </span><br><span class="line">fclose($read); </span><br><span class="line">} </span><br><span class="line">?> </span><br><span class="line"></body> </span><br><span class="line"></html></span></span><br></pre></td></tr></table></figure><p>在 Firefox 输入 url:<code>localhost/codoaudit/xss/Xss_htmlspecialchars_Storage.php</code> :</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-038c78407caae5d2.png" alt="img"></p><p>当我们在 Message 中输入<code><script>alert('xss')</script></code> :</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-4a47fed989a18325.png" alt="img"></p><p>可以看到页面并没有弹窗。<br>我们再查看网页 html 代码:</p><p><img src="http://upload-images.jianshu.io/upload_images/6230889-0b0257806e619f56.png" alt="img"></p><p>可以看到 htmlspecialchars() 函数对用户输入的<code><></code>做了转义处理。</p>]]></content>
<summary type="html"><p>跨站脚本漏洞 (XSS) 基础讲解</p>
<p><img src="http://upload-images.jianshu.io/upload_images/6230889-834e76bc8c471138.png" alt="img"></p>
<h1 id="XSS-漏洞"><a href="#XSS-漏洞" class="headerlink" title="XSS 漏洞"></a>XSS 漏洞</h1><h2 id="一、文章简介"><a href="#一、文章简介" class="headerlink" title="一、文章简介"></a>一、文章简介</h2><p>XSS 漏洞是 Web 应用程序中最常见的漏洞之一。如果您的站点没有预防 XSS 漏洞的固定方法,那么很可能就存在 XSS 漏洞。<br>这篇文章将带你通过代码层面去理解三个问题:</p></summary>
<category term="Web攻击" scheme="http://yoursite.com/tags/Web%E6%94%BB%E5%87%BB/"/>
<category term="XSS" scheme="http://yoursite.com/tags/XSS/"/>
</entry>
<entry>
<title>设计模式</title>
<link href="http://yoursite.com/2021/07/20/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
<id>http://yoursite.com/2021/07/20/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/</id>
<published>2021-07-20T06:07:06.577Z</published>
<updated>2021-08-18T07:14:46.218Z</updated>
<content type="html"><![CDATA[<p><strong>三种最基本的设计模式:</strong></p><ol><li>创建模式,提供实例化的方法,为适合的状况提供相应的对象创建方法。</li><li>结构化模式,通常用来处理实体之间的关系,使得这些实体能够更好地协同工作。</li><li>行为模式,用于在不同的实体建进行通信,为实体之间的通信提供更容易,更灵活的通信方法。<a id="more"></a></li></ol><p><strong>设计模式的六大原则</strong></p><ol><li>开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。</li><li>里氏(Liskov)替换原则:所有引用基类(父类)的地方必须能透明地使用其子类的对象。</li><li>依赖倒置原则:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。换言之,要针对接口编程,而不是针对实现编程。</li><li>接口隔离原则:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。</li><li>迪米特法则:一个软件实体应当尽可能少地与其他实体发生相互作用。</li><li>单一职责原则:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。</li></ol><h4><span id="创建型模式">创建型模式</span></h4><ol><li>单例模式<br>单例模式(Singleton Pattern)目的是确保某一个类只有一个实例存在。<br>实现单例有多种方法:装饰器,<em><em>new</em></em>, metaClass, import<br>每一个 python 模块都是天生的单例模式</li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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><br><span class="line">class Singleton(object):</span><br><span class="line"> def __init__(self):</span><br><span class="line"> pass</span><br><span class="line"></span><br><span class="line"> def __new__(cls, *args, **kwargs):</span><br><span class="line"> if not hasattr(cls, '_instance'):</span><br><span class="line"> cls._instance = object.__new__(cls)</span><br><span class="line"> return cls._instance</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">ob_1 = Singleton()</span><br><span class="line">ob_2 = Singleton()</span><br><span class="line">print(id(ob_1), id(ob_2))</span><br></pre></td></tr></table></figure><ol><li>工厂模式<br>当以下情形可以使用工厂模式:<br>\1. 不知道用户想要创建什么样的对象<br>\2. 当你想要创建一个可扩展的关联在创建类与支持创建对象的类之间。<br>工厂模式有很多种实现类型,下面只介绍一种工厂方法模式</li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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><br><span class="line">class Person:</span><br><span class="line"> def __init__(self):</span><br><span class="line"> self.name = None</span><br><span class="line"> self.gender = None</span><br><span class="line"> </span><br><span class="line"> def getName(self):</span><br><span class="line"> return self.name</span><br><span class="line"></span><br><span class="line"> def getGender(self):</span><br><span class="line"> return self.gender</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class Male(Person):</span><br><span class="line"> def __init__(self, name):</span><br><span class="line"> super().__init__()</span><br><span class="line"> print(f'Mr.{name}')</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class Female(Person):</span><br><span class="line"> def __init__(self, name):</span><br><span class="line"> super().__init__()</span><br><span class="line"> print(f"Miss.{name}")</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class PersonFactory:</span><br><span class="line"> @staticmethod</span><br><span class="line"> def getPerson(name, gender):</span><br><span class="line"> if gender == "M":</span><br><span class="line"> return Male(name)</span><br><span class="line"> if gender == "F":</span><br><span class="line"> return Female(name)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">ob_1 = PersonFactory.getPerson("big", "M")</span><br><span class="line">ob_2 = PersonFactory.getPerson("bigTwo", "F")</span><br></pre></td></tr></table></figure><ol><li>原型模式<br>用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。<br>浅拷贝(Shallow Copy): 指对象的字段被拷贝,而字段引用的对象不会被拷贝,拷贝的对象和源对象只是名称相同,但是他们共用一个实体。<br>深拷贝(deep copy): 对对象实例中字段引用的对象也进行拷贝。</li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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><br><span class="line">import copy</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class Book:</span><br><span class="line"> def __init__(self, *args, **kwargs):</span><br><span class="line"> self.__dict__.update(kwargs)</span><br><span class="line"></span><br><span class="line"> def __str__(self):</span><br><span class="line"> return str(sorted(self.__dict__.items()))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class ProtoType:</span><br><span class="line"> def __init__(self):</span><br><span class="line"> self.objects = dict()</span><br><span class="line"></span><br><span class="line"> def register(self, nid, obj):</span><br><span class="line"> self.objects[nid] = obj</span><br><span class="line"></span><br><span class="line"> def unregister(self, nid):</span><br><span class="line"> del self.objects[nid]</span><br><span class="line"></span><br><span class="line"> def clone(self, nid, **attr):</span><br><span class="line"> found = self.objects.get(nid)</span><br><span class="line"> if not found:</span><br><span class="line"> raise ValueError(f"{nid} No exist")</span><br><span class="line"> obj = copy.deepcopy(found)</span><br><span class="line"> obj.__dict__.update(attr)</span><br><span class="line"> return obj</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">b_1 = Book("aaa", k="kkk", b="bbb")</span><br><span class="line">prototype = ProtoType()</span><br><span class="line">prototype.register("b_1", b_1)</span><br><span class="line">b_2 = prototype.clone("b_1", e="eee", d="ddd")</span><br><span class="line">print(b_1)</span><br><span class="line">print(b_2)</span><br><span class="line">print(id(b_1))</span><br><span class="line">print(id(b_2))</span><br></pre></td></tr></table></figure><ol><li>建造者模式<br>包括一个限定方法抽象类,多个实际建造者类,一个指挥者类<br>以下情况适用:<br>\1. 当创建复杂对象的算法(Director)应该独立于该对象的组成部分以及它们的装配方式(Builder)时<br>\2. 当构造过程允许被构造的对象有不同的表示时(不同 Builder)。</li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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><br><span class="line">from abc import ABCMeta, abstractmethod</span><br><span class="line"></span><br><span class="line">class Builder:</span><br><span class="line"> _metaClass_ = ABCMeta</span><br><span class="line"></span><br><span class="line"> @abstractmethod</span><br><span class="line"> def draw_body(self):</span><br><span class="line"> pass</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class FatCreator(Builder):</span><br><span class="line"> def __init__(self):</span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> def draw_body(self):</span><br><span class="line"> print(f'画胖子')</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class ThinCreator(Builder):</span><br><span class="line"> def __init__(self):</span><br><span class="line"> pass</span><br><span class="line"></span><br><span class="line"> def draw_body(self):</span><br><span class="line"> print(f'画瘦子')</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class Director:</span><br><span class="line"> def __init__(self, obj):</span><br><span class="line"> self.obj = obj</span><br><span class="line"></span><br><span class="line"> def draw(self):</span><br><span class="line"> self.obj.draw_body()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">fat = FatCreator()</span><br><span class="line">Director(fat).draw()</span><br></pre></td></tr></table></figure><h4><span id="结构型模式">结构型模式</span></h4><ol><li>适配器模式<br>所谓适配器模式是指是一种接口适配技术,它可通过某个类来使用另一个接口与之不兼容的类,运用此模式,两个类的接口都无需改动。</li></ol><p>适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况,比如在需要对早期代码复用一些功能等应用上很有实际价值。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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><br><span class="line">class Target:</span><br><span class="line"> def request(self):</span><br><span class="line"> print("普通请求")</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class Adaptee:</span><br><span class="line"> def special_request(self):</span><br><span class="line"> print("特殊请求")</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class Adapter(Target):</span><br><span class="line"> def __init__(self):</span><br><span class="line"> self.adaptee = Adaptee()</span><br><span class="line"></span><br><span class="line"> def request(self):</span><br><span class="line"> self.adaptee.special_request()</span><br><span class="line"></span><br><span class="line">target= Adapter()</span><br><span class="line">target.request()</span><br></pre></td></tr></table></figure><ol><li>修饰器模式<br>Python 种经典的模式,用来拓展类的功能</li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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><br><span class="line">import functools</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">def memoize(fn):</span><br><span class="line"> know = dict()</span><br><span class="line"> @functools.wraps(fn)</span><br><span class="line"> def memoizer(*args):</span><br><span class="line"> if args not in know:</span><br><span class="line"> know[args] = fn(*args)</span><br><span class="line"> return know[args]</span><br><span class="line"> return memoizer</span><br><span class="line"></span><br><span class="line">@memoize</span><br><span class="line">def nsum(n):</span><br><span class="line"> """</span><br><span class="line"> 返回前那个数的和</span><br><span class="line"> :param n:</span><br><span class="line"> :return:</span><br><span class="line"> """</span><br><span class="line"> assert (n >= 0), 'n must be >= 0'</span><br><span class="line"> return 0 if n == 0 else n + nsum(n-1)</span><br><span class="line"></span><br><span class="line">@memoize</span><br><span class="line">def fibonacci(n):</span><br><span class="line"> """</span><br><span class="line"> 返回斐波那契数列的第n个数</span><br><span class="line"> :param n:</span><br><span class="line"> :return:</span><br><span class="line"> """</span><br><span class="line"> assert n >= 0 and isinstance(n, int), 'n must be int and >= 0'</span><br><span class="line"> return n if n in (0, 1) else fibonacci(n-1) + fibonacci(n-2)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">from timeit import Timer</span><br><span class="line">measure = [{'exec': 'fibonacci(100)', 'import': 'fibonacci', 'func': fibonacci},</span><br><span class="line"> {'exec': 'nsum(10)', 'import': 'nsum', 'func': nsum}]</span><br><span class="line">for m in measure:</span><br><span class="line"> t = Timer(f'{m["exec"]}', f'from __main__ import {m["import"]}')</span><br><span class="line"> print('name: {} \ndoc: {} \nexecuting: {} \n time:{}'.format(m['func'].__name__, m['func'].__doc__, m['exec'], t.timeit()))</span><br></pre></td></tr></table></figure><ol><li>享元模式<br>运用共享技术有效地支持大量细粒度的对象。<br>内部状态:享元对象中不会随环境改变而改变的共享部分。比如围棋棋子的颜色。<br>外部状态:随环境改变而改变、不可以共享的状态就是外部状态。比如围棋棋子的位置。</li></ol><p>应用场景:程序中使用了大量的对象,如果删除对象的外部状态,可以用相对较少的共享对象取代很多组对象,就可以考虑使用享元模式。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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><br><span class="line">class Tree(object):</span><br><span class="line"> obj_pool = {}</span><br><span class="line"> </span><br><span class="line"> def __new__(cls, tree_type):</span><br><span class="line"> if tree_type not in cls.obj_pool:</span><br><span class="line"> obj = object.__new__(cls)</span><br><span class="line"> cls.obj_pool[tree_type] = obj</span><br><span class="line"> obj.tree_type = tree_type</span><br><span class="line"> return cls.obj_pool[tree_type]</span><br><span class="line"></span><br><span class="line"> def render(self, age):</span><br><span class="line"> print(f'{self.tree_type}: age: {age}')</span><br><span class="line"></span><br><span class="line"> # obj_list = []</span><br><span class="line"></span><br><span class="line"> # def __init__(self, tree_type):</span><br><span class="line"> # self.tree_type = tree_type</span><br><span class="line"> # self.get_obj()</span><br><span class="line"></span><br><span class="line"> # @classmethod</span><br><span class="line"> # def get_obj(cls):</span><br><span class="line"> # cls.obj_list.append(cls.__new__(cls))</span><br><span class="line"></span><br><span class="line">from random import randint</span><br><span class="line">for _ in range(10):</span><br><span class="line"> a = "apple"</span><br><span class="line"> t1 = Tree(a)</span><br><span class="line"> t1.render(randint(10, 50))</span><br><span class="line">for _ in range(5):</span><br><span class="line"> t2 = Tree("cherry")</span><br><span class="line"> t2.render(randint(10, 50))</span><br><span class="line"></span><br><span class="line">for _ in range(3):</span><br><span class="line"> t3 = Tree("peach")</span><br><span class="line"> t3.render(randint(10, 50))</span><br><span class="line"> </span><br><span class="line"> print(len(Tree.obj_pool))</span><br></pre></td></tr></table></figure><ol><li>外观模式<br>外观模式又叫做门面模式。在面向对象程序设计中,解耦是一种推崇的理念。但事实上由于某些系统中过于复杂,从而增加了客户端与子系统之间的耦合度。例如:在家观看多媒体影院时,更希望按下一个按钮就能实现影碟机,电视,音响的协同工作,而不是说每个机器都要操作一遍。这种情况下可以采用外观模式,即引入一个类对子系统进行包装,让客户端与其进行交互。</li></ol><p>外观模式 (Facade Pattern):外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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></pre></td><td class="code"><pre><span class="line">from enum import Enum</span><br><span class="line">from abc import ABCMeta, abstractmethod</span><br><span class="line"></span><br><span class="line">State = Enum('State', 'new running sleeping restart zombie')</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class User:</span><br><span class="line"> pass</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class Process:</span><br><span class="line"> pass</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class File:</span><br><span class="line"> pass</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class Server(metaclass=ABCMeta):</span><br><span class="line"> @abstractmethod</span><br><span class="line"> def __init__(self):</span><br><span class="line"> pass</span><br><span class="line"></span><br><span class="line"> def __str__(self):</span><br><span class="line"> return self.name</span><br><span class="line"></span><br><span class="line"> @abstractmethod</span><br><span class="line"> def boot(self):</span><br><span class="line"> pass</span><br><span class="line"></span><br><span class="line"> @abstractmethod</span><br><span class="line"> def kill(self, restart=True):</span><br><span class="line"> pass</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class FileServer(Server):</span><br><span class="line"> def __init__(self):</span><br><span class="line"> '''初始化文件服务进程要求的操作'''</span><br><span class="line"> self.name = 'FileServer'</span><br><span class="line"> self.state = State.new</span><br><span class="line"></span><br><span class="line"> def boot(self):</span><br><span class="line"> print('booting the {}'.format(self))</span><br><span class="line"> '''启动文件服务进程要求的操作'''</span><br><span class="line"> self.state = State.running</span><br><span class="line"></span><br><span class="line"> def kill(self, restart=True):</span><br><span class="line"> print('Killing {}'.format(self))</span><br><span class="line"> '''终止文件服务进程要求的操作'''</span><br><span class="line"> self.state = State.restart if restart else State.zombie</span><br><span class="line"></span><br><span class="line"> def create_file(self, user, name, permissions):</span><br><span class="line"> '''检查访问权限的有效性、用户权限等'''</span><br><span class="line"> print("trying to create the file '{}' for user '{}' with permissions{}".format(name, user, permissions))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class ProcessServer(Server):</span><br><span class="line"> def __init__(self):</span><br><span class="line"> '''初始化进程服务进程要求的操作'''</span><br><span class="line"> self.name = 'ProcessServer'</span><br><span class="line"> self.state = State.new</span><br><span class="line"></span><br><span class="line"> def boot(self):</span><br><span class="line"> print('booting the {}'.format(self))</span><br><span class="line"> '''启动进程服务进程要求的操作'''</span><br><span class="line"> self.state = State.running</span><br><span class="line"></span><br><span class="line"> def kill(self, restart=True):</span><br><span class="line"> print('Killing {}'.format(self))</span><br><span class="line"> '''终止进程服务进程要求的操作'''</span><br><span class="line"> self.state = State.restart if restart else State.zombie</span><br><span class="line"></span><br><span class="line"> def create_process(self, user, name):</span><br><span class="line"> '''检查用户权限和生成PID等'''</span><br><span class="line"> print("trying to create the process '{}' for user '{}'".format(name, user))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class WindowServer:</span><br><span class="line"> pass</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class NetworkServer:</span><br><span class="line"> pass</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">class OperatingSystem:</span><br><span class="line"> '''外观'''</span><br><span class="line"></span><br><span class="line"> def __init__(self):</span><br><span class="line"> self.fs = FileServer()</span><br><span class="line"> self.ps = ProcessServer()</span><br><span class="line"></span><br><span class="line"> def start(self):</span><br><span class="line"> [i.boot() for i in (self.fs, self.ps)]</span><br><span class="line"></span><br><span class="line"> def create_file(self, user, name, permissions):</span><br><span class="line"> return self.fs.create_file(user, name, permissions)</span><br><span class="line"></span><br><span class="line"> def create_process(self, user, name):</span><br><span class="line"> return self.ps.create_process(user, name)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">def main():</span><br><span class="line"> os = OperatingSystem()</span><br><span class="line"> os.start()</span><br><span class="line"> os.create_file('foo', 'hello', '-rw-r-r')</span><br><span class="line"> os.create_process('bar', 'ls /tmp')</span><br></pre></td></tr></table></figure><p>全文完</p><p>本文由 <a href="http://ksria.com/simpread" target="_blank" rel="noopener">简悦 SimpRead</a> 优化,用以提升阅读体验</p><p>使用了 全新的简悦词法分析引擎 beta,<a href="http://ksria.com/simpread/docs/#/词法分析引擎" target="_blank" rel="noopener">点击查看</a>详细说明</p><p><a href="https://blog.csdn.net/qq_44484910/article/details/107892976#sr-toc-0" target="_blank" rel="noopener">创建型模式</a><a href="https://blog.csdn.net/qq_44484910/article/details/107892976#sr-toc-1" target="_blank" rel="noopener">结构型模式</a></p>]]></content>
<summary type="html"><p><strong>三种最基本的设计模式:</strong></p>
<ol>
<li>创建模式,提供实例化的方法,为适合的状况提供相应的对象创建方法。</li>
<li>结构化模式,通常用来处理实体之间的关系,使得这些实体能够更好地协同工作。</li>
<li>行为模式,用于在不同的实体建进行通信,为实体之间的通信提供更容易,更灵活的通信方法。</summary>
<category term="设计模式" scheme="http://yoursite.com/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
</entry>
<entry>
<title>zookeeper watch机制</title>
<link href="http://yoursite.com/2021/07/15/zookeeper%20watch%E6%9C%BA%E5%88%B6/"/>
<id>http://yoursite.com/2021/07/15/zookeeper%20watch%E6%9C%BA%E5%88%B6/</id>
<published>2021-07-15T09:01:38.473Z</published>
<updated>2021-08-18T07:14:07.311Z</updated>
<content type="html"><![CDATA[<p>zookeeper watch 机制与客户端实现原理</p><p>本文讨论如何用 zookeeper 做服务发现,zookeeper 的 watch 实现原理和机制,以及 python 使用 kazoo 客户端库连接 zookeeper 时如何在数据变化后更新数据,保证数据安全。</p><a id="more"></a><h2><span id="1-服务发现"><strong>1. 服务发现</strong></span></h2><p>你有一个服务,它需要请求下游的服务,假设下游的服务器 ip 有 192.168.0.1 和 192.168.0.2 。实践中面临的一个问题是,下游的服务器可能会增加机器,也可能减少机器,以实现动态扩容和收缩。一个可行的方案是在你的服务和下游服务之间架设一个 nginx,由它代为转发请求,你的请求发送给 nginx,nginx 向下游服务器转发请求,这样下游服务的变化情况对于你的服务来说是不可见的。另外一个方案是想办法让你的服务动态的感知下游服务的变化,当下游服务增加机器时,你需要知道新增机器的 ip 是多少,当下游服务的机器减少时,你需要知道减少机器的 ip,当你向下游发送请求时,你自行决定向哪台服务器发送请求。</p><p>上面这段所涉及的就是服务发现问题,nginx 做服务发现有很多种方案,使用 zookeeper 就是其中一种,在不重启 nginx 或者更新 nginx 配置的前提下,使用 dyups 模块 修改共享内存中的 upstream 。如果你的服务也需要具备这种能力,那么利用 zookeeper 恐怕是你最好的选择。</p><p>约定一个节点,你的服务监控这个节点,这里的监控,是指 watch 节点的子节点变化情况,下游的服务在启动后,在节点下新增临时节点,并在临时节点里存储有关自己的信息。你的服务发现了子节点变化情况,就可以更新下游服务器的 ip 了,当下游服务某个机器出现问题导致服务不可用时,由于当初启动时创建的是临时节点,一旦服务由于网络不通或者机器出现问题而导致不可用,那么这个临时节点会自行消失,这种情况,你的服务也会感知到。</p><h2><span id="2-zookeeper-是如何通知你的"><strong>2. zookeeper 是如何通知你的</strong></span></h2><p>以 python 代码做示例,你使用 kazoo 这个库与 zookeeper 建立连接,这个所谓的连接究竟是什么呢?和连接 http 服务器,mysql 一样,在最底层,这个连接其实就是 TCP 连接。你可以通过 TCP 连接向 zookeeper 发送数据,同理,zookeeper 也可以向你发送数据,当你所 watch 的节点发生变化时,它就会向你发送数据,提醒你数据有变化。</p><p>由此,带来了一个新的问题,我的程序使用 kazoo 与 zookeeper 建立了连接,对某个节点进行了 watch 监控,但这之后,我就去忙别的事情了,zookeeper 突然通知我节点有变化,那么这个通知对我的程序会产生什么影响呢?会终止我正在运行的程序么?kazoo 这个库是如何处理 zookeeper 发来的数据变化的通知呢?</p><p>这些问题之前一直困扰这我,直到最近需要在项目中使用 zookeeper,于是决定认真研究一番,结果真相却是那么的简单,kazoo 仅仅是启动了一个线程来接收 zookeeper 发来的消息,你程序里运行的其他的代码都是在另一个线程里,zookeeper 的通知对程序的执行不会产生任何影响。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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">import time</span><br><span class="line">from kazoo.client import KazooClient</span><br><span class="line"></span><br><span class="line">host = '192.168.1.12:3182'</span><br><span class="line">zk_client = KazooClient(host, auth_data=[('auth', 'auth')])</span><br><span class="line">zk_client.start()</span><br><span class="line"></span><br><span class="line">children_node = []</span><br><span class="line"></span><br><span class="line">@zk_client.ChildrenWatch('/config/server')</span><br><span class="line">def watch_child(children):</span><br><span class="line"> global children_node</span><br><span class="line"> children_node = children</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">while True:</span><br><span class="line"> # 模拟对children_node 的使用</span><br><span class="line"> print(children_node)</span><br><span class="line"> time.sleep(5)</span><br></pre></td></tr></table></figure><p>我代码里注册了一个 watch,来监听节点 / config/server 的子节点变化情况,这个监听是有另外一个线程来执行的,在我的代码里,你看不到这个线程。当子节点发生变化后,zookeeper 发来消息,线程收到消息后调用 watch_child 函数更新数据, children 会是所有子节点的列表。</p><h2><span id="3-多线程下修改数据不安全啊"><strong>3. 多线程下修改数据不安全啊</strong></span></h2><p>watch_child 函数是在另一个线程中被调用的,这就带来了一个隐患,在我的程序正常执行的过程中,某一刻,正在对 children_node 进行修改,此时,节点发生变化,watch_child 被调用,也要修改 children_node, 这样不就发生了两个线程同时修改一个数据的情况了么?这不是多线程编程里极力避免的事情么?难道需要做线程互斥操作来保证数据的安全?</p><p>5 年前,我就对 zookeeper 有过学习,但只是从概念上,使用方法进行学习,并没有在工作中实际使用过,对于 zookeeper 数据发生变化时的 watch 机制并不了解,在我了解了 watch 机制后,又对多线程修改同一份数据产生了如上的疑问。</p><p>以下,是我自己探索思考的结果,如果你有不同的看法,或者你对 zookeeper 的使用更加深入熟练,请指正我观点中错误的部分。</p><p>我认为除了所注册的 watch 函数,其他代码不应当对数据进行修改。道理很简单,一旦我们在程序里对数据进行了修改,那么我们所得到的信息就和 zookeeper 所保存的信息有了差异和不同,那么 zookeeper 还有什么存在的价值和意义呢?因此,前面所担心的多线程数据安全问题,原本就是一个伪命题,我们决不能擅自修改从 zookeeper 获取到的数据。</p><p>但用变化后的数据更新旧的数据仍然要注意数据安全,假设你的代码有这样一段</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">for i in range(len(children_node)):</span><br><span class="line"> node = children_node[i]</span><br><span class="line"> # do something</span><br></pre></td></tr></table></figure><p>如果 watch_child 函数触发时,你的主线程里正在执行上面这段代码,就有可能发生索引异常。watch_child 函数触发前,假设 children_node 的长度是 5, i = 3 时, watch_child 被触发,这时,修改了变量 children_node, 列表长度变成了 3, 那么就会产生索引异常。正确的写法应该是这样</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">for node in children_node:</span><br><span class="line"> # do something</span><br></pre></td></tr></table></figure><p>同样是 for 循环,为什么这种写法在多线程环境下就是安全的呢?原因在于 for 循环的本质是使用 iter 函数获得 children_node 的一个迭代器,然后使用 next 函数进行迭代并执行处理 StopIteration 异常。在上面的这个 for 循环执行期间,watch_child 函数被触发,修改了 children_node 的值,但却不会对 for 循环造成任何影响,因为 watch_child 函数中的赋值语句执行时,程序在内存中创建了一个新的对象,变量 children_node 指向了内存中的新对象,而在这之前 children_node 所指向,所引用的对象不会被销毁,因为 for 循环时通过 iter 函数获得了 children_node 的一个迭代器,这个迭代器指向了老的 children_node,因此不会被销毁。上面这段内容对于掌握 python 不是很深入的人来说,很难理解,这涉及到 python 变量的本质,变量是内存中对象的引用,下面这段代码可以向你证明我的观点</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">lst = [9, 4]</span><br><span class="line"></span><br><span class="line">print('lst 内存地址:',id(lst))</span><br><span class="line">_iter = iter(lst)</span><br><span class="line">print(next(_iter))</span><br><span class="line">lst = [8, 7, 6]</span><br><span class="line">print('lst 内存地址:',id(lst))</span><br><span class="line">print(next(_iter))</span><br></pre></td></tr></table></figure><p>程序输出结果</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">lst 内存地址: 2006395135624</span><br><span class="line">9</span><br><span class="line">lst 内存地址: 2006395135432</span><br><span class="line">4</span><br></pre></td></tr></table></figure><p>尽管中途修改了 lst 的值,但是遍历输出的仍然是那个最开始定义的列表 [9, 4]。在 watch_child 函数中,我建议直接对 children_node 进行赋值,并在使用 children_node 时避免使用索引,或许,你还有其他的想法,比如在 watch_child 函数中这样操作</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">def watch_child(children):</span><br><span class="line"> for child in children:</span><br><span class="line"> if not child in children_node:</span><br><span class="line"> children_node.append(child)</span><br></pre></td></tr></table></figure><p>如果你在主线程使用 children_node 时避免使用索引,这种更新数据的方法同样可行,但有两个致命缺陷,第一个缺陷,如果子节点减少,这段代码不能删除掉 children_node 存储的子节点,当然,你可以用更多的代码来解决这个问题,可你仍然要面临一个严峻的问题,这种修改方式可能会带来意想不到的后果,下面这段代码将向你展示这种可能</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">lst = [9, 4]</span><br><span class="line"></span><br><span class="line">_iter = iter(lst)</span><br><span class="line">print(next(_iter))</span><br><span class="line">lst.append(5) # 假设这行代码发生在另一个线程,最终效果是一样的</span><br><span class="line">print(next(_iter))</span><br><span class="line">print(next(_iter))</span><br></pre></td></tr></table></figure><p>与前面的代码不同,这一次,没有对 lst 进行赋值,而是使用 append 方法增加新的数据,这与上面的 watch_child 函数一样,都是修改了列表的内容,可这一次,第三个 print(next(_iter)) 可以完美的输出刚才所添加的 5,但你在程序里原本只想遍历 9 和 4, 但另一个线程向 lst 增加了新的数据,最终,程序的最终表现与预期不符,我认为这是危险的事情。</p><p>经过前面的分析,总结为两点实践要求:</p><ol><li>节点发生变化时,将新的数据赋值给变量,不要通过 append 或是其他方法对数据进行修改</li><li>使用 for item in lst 的方式遍历列表,避免使用索引</li></ol><p>我的示例是监控节点的子节点,watch 函数传入的参数是列表,如果你监控的是节点数据,同样是直接赋值就可以了,不用考虑 for 循环遍历避免使用索引的问题。</p>]]></content>
<summary type="html"><p>zookeeper watch 机制与客户端实现原理</p>
<p>本文讨论如何用 zookeeper 做服务发现,zookeeper 的 watch 实现原理和机制,以及 python 使用 kazoo 客户端库连接 zookeeper 时如何在数据变化后更新数据,保证数据安全。</p></summary>
<category term="zookeeper" scheme="http://yoursite.com/tags/zookeeper/"/>
</entry>
<entry>
<title>数据库存储和读取</title>
<link href="http://yoursite.com/2021/07/15/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AD%98%E5%82%A8%E5%92%8C%E8%AF%BB%E5%8F%96/"/>
<id>http://yoursite.com/2021/07/15/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AD%98%E5%82%A8%E5%92%8C%E8%AF%BB%E5%8F%96/</id>
<published>2021-07-15T08:10:08.180Z</published>
<updated>2021-08-18T07:14:47.924Z</updated>
<content type="html"><![CDATA[<h2><span id="数据库存储和读取">数据库存储和读取</span></h2><p>转载:<a href="https://my.oschina.net/u/1859679/blog/1581379" target="_blank" rel="noopener">https://my.oschina.net/u/1859679/blog/1581379</a></p><p>在整个数据库体系结构中,我们可以使用不同的存储引擎来存储数据,而绝大多数存储引擎都以二进制的形式存储数据。</p><p>在 InnoDB 存储引擎中,所有的数据都被逻辑地存放在表空间中,表空间(tablespace)是存储引擎中最高的存储逻辑单位,在表空间的下面又包括段(segment)、区(extent)、页(page):</p><a id="more"></a><p><img src="https://img-blog.csdnimg.cn/20181204093045330.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3p6dElzR29vZA==,size_16,color_FFFFFF,t_70" alt="img"></p><p>同一个数据库实例的所有表空间都有相同的页大小;默认情况下,表空间中的页大小都为 16KB,当然也可以通过改变 innodb_page_size 选项对默认大小进行修改,需要注意的是不同的页大小最终也会导致区大小的不同:</p><p>三、随机读取和顺序读取<br>在我们常用的 sql 理解中,数据是以行的形式读取出来的,其实不然,通过上述的结构,我们可以了解到,单次从磁盘读取单位是页,而不是行,也就是说,你即便只读取一行记录,从磁盘中也是会读取一页的,当然了单页读取代价也是蛮高的,一般都会进行预读,这些后续再说。</p><p><img src="https://img-blog.csdnimg.cn/20181204093214562.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3p6dElzR29vZA==,size_16,color_FFFFFF,t_70" alt="img"></p><p>关系型数据库管理系统最重要的一个目标就是,确保表或者索引中的数据是随时可以用的。那么为了尽可能的实现这个目标,会使用内存中的缓冲池来最小化磁盘活动。</p><p>每一个缓冲池都足够大,大到可以存放许多页,可能是成千上万的页。</p><p>缓冲池管理器将尽力确保经常使用的数据被保存于池中,以避免一些不必要的磁盘读。如果一个索引或者表页在缓冲池中被找到,那么将会处理很快。</p><p>如果在缓冲池中,没有找到数据,会从磁盘服务器的缓冲区里面去读取。</p><p>磁盘服务器的缓冲区,如同数据库的缓冲池读取一样,磁盘服务器试图将频繁使用的数据保留在内存中,以降低高昂的磁盘读取成本。这个读取成本大概会在 1ms 左右。</p><p>如果磁盘服务器的缓冲池中依然没有找到数据,此时就必须要从磁盘读取了,此时读取又分区随机读取和顺序读取。</p><p>2、随机 I/O<br>我们必须记住一个页包含了多条记录,我们可能需要该页上的所有行,也可能是其中一部分,或者是一行,但所花费的成本都是相同的,读取一个页,需要一次随机 I/O, 大约需要 10ms 的时间。</p><p><img src="https://img-blog.csdnimg.cn/20181204093253388.png" alt="img"></p><p><img src="https://img-blog.csdnimg.cn/20181204093303605.png" alt="img"></p><p>3、顺序读取<br>如果我们需要将多个页读取到缓冲池中,并按顺序处理它们,此时读取的速度回变的很快,具体的原理,在 B 树索引中也有过介绍,此时读取每个页面(4kb)所花费的时间大概为 0.1ms 左右,这个时间消耗对随机 IO 有很大的优势。</p><p><strong>以下几种情况,会对数据进行顺序读取。</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">全表扫描</span><br><span class="line"></span><br><span class="line">全索引扫描</span><br><span class="line"></span><br><span class="line">索引片扫描</span><br><span class="line"></span><br><span class="line">通过聚蔟索引扫描表行</span><br></pre></td></tr></table></figure><p><strong>顺序读取有两个重要的优势:</strong></p><p>同时读取多个页意味着平均读取每个页的时间将会减少。在当前磁盘服务器条件下,对于 4kb 大小的页而言,这一值可能会低于 0.1ms(40MB/s)<br>由于数据库引擎知道需要读取哪些页,所有可以在页被真正请求之前就提前将其读取进来,我们称为预读</p><h2><span id="最大与最小数据库的阀值">最大与最小(数据库的阀值)</span></h2><ol><li>一个表里最多可有 1017 列(在 MySQL 5.6.9 之前最大支持 1000 列)。虚拟列也受限这个限制。</li><li>一个表最多可以有 64 个二级索引。</li><li>如果 innodb_large_prefix 打开,在 InnoDB 表 DYNAMIC 或 COMPRESSED 列格式下,索引前缀最大支持前 3072 字节;如果不打开的话,在任意列格式下,最多支持前 767 字节。这个限制既适用于前缀索引也适用于全列索引。<br>基于一个 16KB 的页最多装 3072 个字节,如果你把 InnoDB 的 page 大小从 8KB 降到 4KB,索引的长度也相应的降低。也就是说,当页是 8KB 的时候最大索引长度是 1536 字节;当页大小是 4KB 的时候最大索引长度是 768 字节;(</li></ol><p>这里与 “紧凑冗余行有关系”</p><p>紧凑和冗余行格式</p><p>当为外部页外存储选择可变长度列时 InnoDB,将该行中的前 768 个字节存储在本地,其余外部存储到溢出页中。每个这样的列都有自己的溢出页列表。768 字节的前缀伴随着一个 20 字节的值,该值存储列的真实长度并指向溢出列表,其中存储了值的其余部分。请参见 第 14.11 节 “InnoDB 行格式”。</p><p>)<br><strong>转载</strong>:<a href="https://dev.mysql.com/doc/refman/5.7/en/innodb-file-space.html" target="_blank" rel="noopener">https://dev.mysql.com/doc/refman/5.7/en/innodb-file-space.html</a><br><a href="https://dev.mysql.com/doc/refman/5.7/en/innodb-file-space.html" target="_blank" rel="noopener">14.12.2 文件空间管理</a></p><p>联合索引最多支持 16 列,如果超过这个限制就会遇到以下错误:<br>ERROR 1070 (42000): Too many key parts specified; max 16 parts allowed</p><ol><li>行长度(除去可变长类型:VARBINARY/VARCHAR/BLOB/TEXT),要小于页长(如 4KB, 8KB, 16KB, and 32KB)的一半。例如:innodb_page_size 长度是 16KB 的话,行长不超过 8KB;如果 innodb_page_size 是 64KB 的话,行长不超过 16KB; LONGBLOB/LONGTEXT/BLOB/TEXT 列必须小于 4GB,整个行长也必须小于 4GB。如果一行小于一页的一半,它可以存在一个 page 里面。如果超过了页的一半,就会把可变长列放到额外的页存(如果对这个感兴趣的话可以看看,MySQL 页管理)。</li><li>行所存储的值的最大值为页的大小的一半,一般情况下页的大小是 16kB,所以每行的长度大小不能超过 8<br>kb;2 的 16 次刚好为 65535;</li></ol>]]></content>
<summary type="html"><h2 id="数据库存储和读取"><a href="#数据库存储和读取" class="headerlink" title="数据库存储和读取"></a>数据库存储和读取</h2><p>转载:<a href="https://my.oschina.net/u/1859679/blog/1581379" target="_blank" rel="noopener">https://my.oschina.net/u/1859679/blog/1581379</a></p>
<p>在整个数据库体系结构中,我们可以使用不同的存储引擎来存储数据,而绝大多数存储引擎都以二进制的形式存储数据。</p>
<p>在 InnoDB 存储引擎中,所有的数据都被逻辑地存放在表空间中,表空间(tablespace)是存储引擎中最高的存储逻辑单位,在表空间的下面又包括段(segment)、区(extent)、页(page):</p></summary>
<category term="数据存储" scheme="http://yoursite.com/tags/%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8/"/>
</entry>
<entry>
<title>AVL 树,红黑树,B 树,B + 树,Trie 树都分别应用在哪些现实场景中?</title>
<link href="http://yoursite.com/2021/07/15/AVL%20%E6%A0%91%EF%BC%8C%E7%BA%A2%E9%BB%91%E6%A0%91%EF%BC%8CB%20%E6%A0%91%EF%BC%8CB%20+%20%E6%A0%91%EF%BC%8CTrie%20%E6%A0%91%E9%83%BD%E5%88%86%E5%88%AB%E5%BA%94%E7%94%A8%E5%9C%A8%E5%93%AA%E4%BA%9B%E7%8E%B0%E5%AE%9E%E5%9C%BA%E6%99%AF%E4%B8%AD%EF%BC%9F/"/>
<id>http://yoursite.com/2021/07/15/AVL%20%E6%A0%91%EF%BC%8C%E7%BA%A2%E9%BB%91%E6%A0%91%EF%BC%8CB%20%E6%A0%91%EF%BC%8CB%20+%20%E6%A0%91%EF%BC%8CTrie%20%E6%A0%91%E9%83%BD%E5%88%86%E5%88%AB%E5%BA%94%E7%94%A8%E5%9C%A8%E5%93%AA%E4%BA%9B%E7%8E%B0%E5%AE%9E%E5%9C%BA%E6%99%AF%E4%B8%AD%EF%BC%9F/</id>
<published>2021-07-15T07:58:43.104Z</published>
<updated>2021-08-18T07:16:09.114Z</updated>
<content type="html"><![CDATA[<p>AVL 树,红黑树,B 树,B + 树,Trie 树都分别应用在哪些现实场景中?</p><p><strong>AVL 树</strong>: 最早的平衡二叉树之一。应用相对其他数据结构比较少。windows 对进程地址空间的管理用到了 AVL 树。<strong>红黑树</strong>: 平衡二叉树,广泛用在 C++ 的 STL 中。如 map 和 set 都是用红黑树实现的。<strong>B/B + 树:</strong> 用在磁盘文件组织 数据索引和数据库索引。<strong>Trie 树 (字典树):</strong> 用在统计和排序大量字符串,如自动机。</p><a id="more"></a><p>AVL 树是平衡二叉搜索树的鼻祖,它的平衡度也最好,左右高度差可以保证在「-1,0,1」,基于它的平衡性,它的查询时间复杂度可以保证是 O(log n)。但每个节点要额外保存一个平衡值,或者说是高度差。这种树是二叉树的经典应用,现在最主要是出现在教科书中。AVL 的平衡算法比较麻烦,需要左右两种 rotate 交替使用,需要分四种情况,是数据结构课的最理想课后作业之一。<strong>这里要重点强调,AVL 插入新节点所需要的最大旋转次数是常数,再说一遍,是常数,不要为这个再来回复或者私信我了。这个也有证明,不需要一直旋转到根节点。</strong><img src="https://pic1.zhimg.com/v2-76c73303b557e7bd2845a3312c3e5bb4_r.jpg?source=1940ef5c" alt="img">红黑树一样也是平衡二叉搜索树,也是工业界最主要使用的二叉搜索平衡树。但平衡度红黑树没 AVL 那么好。也就是说,如果从高度差来说,红黑树是大于 AVL 的,其实也就代表着它的实际查询时间 (最坏情况) 略逊于 AVL 的。数学证明红黑树的最大深度是 <img src="https://www.zhihu.com/equation?tex=2%5Ccdot+log_%7B2%7D+%28n%2B1%29" alt="img"> , 其实最差情况它从根到叶子的最长路可以是最短路的两倍,但也不是很差,所以它的查询时间复杂度也是 O(log n)。从实现角度来说,保存红黑状态,每个节点只需要一位二进制,也就是一个 bit(有些做法,可以把这个 bit 塞到其他地方,就可以不占用额外空间了)。可 AVL 每个节点需要额外存储一个平衡值(数)(按照评论中大神韦易笑的说法,avl 的平衡值可以用两个 bit 来存储,然后塞到指针里,惊才绝艳的搞法。当然指针少两位会有什么问题,我就不多想了,你去找他,他有个很好的 avl 实现)。所以(这里不能用所以,因为谁知道真实原因是啥),一般工业界把红黑树作为一种更通用的平衡搜索数来用,Java 用它来实现 TreeMap, C++ 的 std::set/map/multimap 等等。以上都是二叉(binary)树,其实没有太多实质差别。他们的实现都比较复杂,如果做得不好会带来性能的隐患,所以强烈建议入行不足 10 年的朋友尽量使用系统中的已有默认实现,不管是 avl 还是红黑树,这两个有什么就用什么,千万别自己写代码,能写也别写,留个坑给别人不合适。二叉平衡搜索树的问题在于每次插入和删除都有很大可能需要进行重新平衡,数据就要不停的搬来搬去,在内存中这问题不是特别大,可如果在磁盘中,这个开销可能就大了。有兴趣的朋友还可以看看 2-3-4 tree,它等价于红黑树,可以互相转换。它每个节点可以最多有四个孩子,重新平衡操作的次数稍稍少了一点点。以上这些树绝大多数应用都是作为内存中的数据结构。B/B+ 树就是 N 叉(N-ary)平衡树了,每个节点可以有更多的孩子,新的值可以插在已有的节点里,而不需要改变树的高度,从而大量减少重新平衡和数据迁移的次数,这非常适合做数据库索引这种需要持久化在磁盘,同时需要大量查询和插入操作的应用。以上几种树都是有序的,如果你采用合适的算法遍历整个数,可以得到一个有序的列表。这也是为什么如果有数据库索引的情况下,你 order by 你索引的值,就会速度特别快,因为它并没有给你真的排序,只是遍历树而已。Trie 并不是平衡树,也不一定非要有序。它主要用于前缀匹配,比如字符串,比如说 ip 地址,如果字符串长度是固定或者说有限的,那么 Trie 的深度是可控制的,你可以得到很好的搜索效果,而且插入新数据后不用平衡。不过 Trie 不像 B-tree 通用性那么强,你需要针对你自己的实际应用来设计你自己的 Trie,比如说你做个字典应用,是用 26 个字母,还是用 unicode 来前缀匹配?如果是 ip 地址搜索,是用二进制来前缀拼配,还是八进制来匹配?</p><p><img src="https://pic2.zhimg.com/e4f1b0d6aa782bb9b0dee1ea384fe556_xs.jpg?source=1940ef5c" alt="img"></p><p>韦易笑</p><p>楼上楼下 AVL 被批的很惨,给 AVL 树平反昭雪,今天优化了一下我之前的 AVL 树,总体跑的和 linux 的 rbtree 一样快了:<img src="https://pic3.zhimg.com/v2-3b0dd24fe1bc5e5940cc405233ce1e0e_r.jpg?source=1940ef5c" alt="img">他们都比 std::map 快很多(即便使用动态内存分配,为每个新插入节点临时分配个新内存)。其他 AVL/RBTREE 评测也有类似的结论,见:<a href="https://link.zhihu.com/?target=http%3A//stlavlmap.sourceforge.net/">STL AVL Map</a><br><strong>谣言 1:RBTREE 的平均统计性能比 AVL 好</strong>统计下来一千万个节点插入 AVL 共旋转 <a href="tel:7053316">7053316</a> 次(先左后右算两次),RBTREE 共旋转 <a href="tel:5887217">5887217</a> 次,RBTREE 看起来少是吧?应该很快?但是别忘了 RBTREE 再平衡的操作除了旋转外还有再着色,每次再平衡噼里啪啦的改一片颜色,父亲节点,叔叔,祖父,兄弟节点都要访问一圈,这些都是代价,再者平均树高比 AVL 高也成为各项操作的成本。<br><strong>谣言 2:RBTREE 一般情况只比 AVL 高一两层,这个代价忽略不计</strong>纯粹谣言,随便随机一下,一百万个节点的 RBTREE 树高 27,和一千万个节点的 AVL 树相同,而一千万个节点的 RBTREE 树高 33,比 AVL 多了 6 层,这还不是最坏情况,最坏情况 AVL 只有 1.440 * log(n + 2) - 0.328, 而 RBTREE 是 2 * log(n + 1),也就是说同样 100 万个节点,AVL 最坏情况是 28 层,rbtree 最坏可以到 39 层。<br><strong>谣言 3:AVL 树删除节点是需要回溯到根节点</strong>我以前也是这么写 AVL 树的,后来发现根据 AVL 的定义,可以做出两个推论,再平衡向上回溯时:插入更新时:如当前节点的<strong>高度没有改变</strong>,则上面所有父节点的高度和平衡也不会改变。<br>删除更新时:如当前节点的<strong>高度没有改变</strong>且<strong>平衡值在 [-1, 1] 区间</strong>,则所有父节点的高度和平衡都不会改变。根据这两个推论,AVL 的插入和删除大部分时候只需要向上回溯一两个节点即可,范围十分紧凑。<br><strong>谣言 4:虽然二者插入一万个节点总时间类似,但是 rbtree 树更平均,avl 有时很快,有时慢很多,rbtree 只需要旋转两次重新染色就行了,比 avl 平均</strong>完全说反了,avl 是公认的比 rbtree 平均的数据结构,插入时间更为平均,rbtree 才是不均衡,有时候直接插入就返回了(上面是黑色节点),有时候插入要染色几个节点但不旋转,有时候还要两次旋转再染色然后递归到父节点。该说法最大的问题是以为 rbtree 插入节点最坏情况是两次旋转加染色,可是忘记了一条,<strong>需要向父节点递归</strong>,比如:当前节点需要旋转两次重染色,然后递归到父节点再旋转两次重染色,再递归到父节点的父节点,直到满足 rbtree 的 5 个条件。这种说法直接把递归给搞忘记了,翻翻看 linux 的 rbtree 代码看看,再平衡时那一堆的 while 循环是在干嘛?不就是向父节点递归么?avl 和 rbtree 插入和删除的最坏情况都需要递归到根节点,都可能需要一路旋转上去,否则你设想下,假设你一直再树的最左边插入 1000 个新节点,每次都想再局部转两次染染色,而不去调整整棵树,不动根节点,可能么?只是说整个过程 avl 更加平均而已。<br>结论:AVL / RBTREE 真的差不多,AVL 被早期各种乱七八糟的实现和数学上的 “统计” 给害了,别盯着 linux 用了 rbtree 就觉得 rbtree 一定好过 avl 了,solaris 里面大范围的使用 avltree ,完全没有 rbtree 的痕迹那。—–更新:无图无真相,给一下我测试的编译器和标准库版本吧,否则疑惑我在和 vc6 做比较呢。主要开发环境:mingw gcc 5.2.0<code>linux rbtree with 10000000 nodes: insert time: 4451ms, height=32 search time: 2037ms error=0 delete time: 548ms total: 7036ms avlmini with 10000000 nodes: insert time: 4563ms, height=27 search time: 2018ms error=0 delete time: 598ms total: 7179ms std::map with 10000000 nodes: insert time: 4281ms search time: 4124ms error=0 delete time: 767ms total: 9172ms linux rbtree with 1000000 nodes: insert time: 355ms, height=26 search time: 171ms error=0 delete time: 46ms total: 572ms avlmini with 1000000 nodes: insert time: 438ms, height=24 search time: 141ms error=0 delete time: 47ms total: 626ms std::map with 1000000 nodes: insert time: 345ms search time: 360ms error=0 delete time: 62ms total: 767ms</code>又测试了一下 vs2017,结论类似:<code>linux rbtree with 10000000 nodes: insert time: 4201ms, height=32 search time: 3411ms error=0 delete time: 567ms total: 8179ms avlmini with 10000000 nodes: insert time: 4250ms, height=27 search time: 3233ms error=0 delete time: 658ms total: 8141ms std::map with 10000000 nodes: insert time: 4658ms search time: 4275ms error=0 delete time: 815ms total: 9748ms linux rbtree with 1000000 nodes: insert time: 330ms, height=26 search time: 316ms error=0 delete time: 62ms total: 708ms avlmini with 1000000 nodes: insert time: 409ms, height=24 search time: 266ms error=0 delete time: 53ms total: 728ms std::map with 1000000 nodes: insert time: 426ms search time: 375ms error=0 delete time: 78ms total: 879ms</code>注意,avlmini 和 linux rbtree 都是使用结构体内嵌的形式,这样和 std::map 这种需要 overhead 的容器比较起来 std::map 太吃亏了,所以我测试时,每次插入 avlmini 和 linux rbtree 之前都会模拟 std::map 为每对新的 (key, value) 分配一个结构体(包含 node 信息和 key, value),再插入,这样加入了内存分配的开销,才和 std::map 进行比较。参考:别人做的更多树和哈希表的评测 <a href="https://link.zhihu.com/?target=https%3A//github.com/rcarbone/kudb">rcarbone/kudb</a></p><p><img src="https://pic3.zhimg.com/v2-5e2e216cf09c38985dba7fb2fc19d133_xs.jpg?source=1940ef5c" alt="img"></p><p>程序员柠檬</p><p>/<strong><strong><strong>***</strong></strong></strong>20210119 更新 *<em>*<em>*</em>*<em>*</em>*<em>*</em>**\</em>/<strong>没想到这个老回答这两天突然火了,那再来更新点干货吧!本职工作是某大厂的 C++ 后台开发,术业有专攻,</strong>文末分享经验,如何成为一名合格的 Linux 后台服务器开发程序员,并推荐书单!<strong>希望对大家有帮助~</strong>/*<em>*<em>*</em>*<em>*</em>*<em>*</em>*<em>*</em>*<em>*</em>*<em>*</em>*<em>*</em>*<em>*</em>*<em>*</em>**\</em>/<strong>**以下正文</strong>我是自学数据结构,没有老师带着,刚接触数据结构中这么多关于<strong>「树的概念」</strong>的时候,也是一脸懵的状态。把书看完一遍之后,脑子里 <strong>AVL 树,红黑树,B 树,B + 树,Trie 树一团糟的状态~**</strong>先说数据结构<strong>数据结构这门课程是计算机相关专业的基础课,数据结构指的是数据在计算机中的存储、组织方式。我们在学习数据结构时候,会遇到各种各样的基础数据结构,比如堆栈、队列、数组、链表、树… 这些基本的数据结构类型有各自的特点,不同数据结构适用于解决不同场景下的问题。树形结构相比数组、链表、堆栈这些数据结构来说,稍微复杂一点点,但树形结构可以用于解决很多实际问题,因为现实世界事物之间的关系往往不是线性关联的,而「树」恰好适合描述这种非线性关系。今天就带大家一起学习下,数据结构中的各种「树」,这也是面试中经常考察的内容,手撕二叉树是常规套路,对候选人也很有区分度,看完我的这个回答,</strong>相信大家都会心中有「树」了<strong>。<br><img src="https://pic3.zhimg.com/v2-a5eb5cf0c31872d4ba4293660178aaaa_r.jpg?source=1940ef5c" alt="img"><br>从树说起什么是树?现实中的树大家都见过,在数据结构中也有树,此树非彼树,不过数据结构的树和现实中的树在形态上确实有点相像。树是非线性的数据结构,用来模拟具有树状结构性质的数据集合,它是由 n 个有限节点组成的具有层次关系的集合。在数据结构中树是非线性数据结构,那我们先来了解下,什么是线性与非线性数据结构?线性结构</strong>线性结构**是一个有序数据元素的集合。 其中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的,常用的线性结构有:线性表,栈,队列,双端队列,数组,串。<br><img src="https://pic1.zhimg.com/v2-3ee77261097b14b0561610039dc2c41f_r.jpg?source=1940ef5c" alt="img"></p><p><img src="https://pic2.zhimg.com/50/v2-0218d29654d19a81aa60bcc6e89b1d9f_720w.jpg?source=1940ef5c" alt="img"><br>可以想象,所谓的线性结构数据组织形式,就像一条线段一样笔直,元素之间首尾相接。比如现实中的火车进站、食堂打饭排队的队列。<br><img src="https://pic2.zhimg.com/v2-7f9c46dcec867762933a20f686d2216e_r.jpg?source=1940ef5c" alt="img"><br>非线性结构简而言之,线性结构的对立面就是<strong>非线性结构</strong>。树线性结构中节点是首位相接一对一关系,在树结构中节点之间不再是简单的一对一关系,而是较为复杂的一对多的关系,一个节点可以与多个节点发生关联,树是一种层次化的数据组织形式,树在现实中是可以找到例子的,比如现实中的族谱,亲戚之间的关系是层次关联的树形关系。数据结构中的「树」的名字由来,是因为如果把节点之间的关系直观展示出来,由于长得和现实世界中的树很像,由此得名。如图:<br><img src="https://pic3.zhimg.com/50/v2-f94f1194ecb976b050527391c875bacf_720w.jpg?source=1940ef5c" alt="img"><br>树的关键概念人们对树形结构的研究比较深入,为了方便研究树的各种性质,抽象出了一些树相关的概念,以便清晰简介的描述一颗树。下面几个基础概念必须了解,否则你当你刷 LeetCode 树相关题目时候,或者面试官向你描述问题时,你会连题目都看不懂事什么意思。先来上一个图解,下面的术语和概念对照着看,更容易理解。<br><img src="https://pica.zhimg.com/50/v2-ed1bffab5b2b8a662c057abe235b6799_720w.jpg?source=1940ef5c" alt="img"><br>什么是节点的度?度很好理解,直观来说,数一下节点有几个分叉就说这个节点的度是多少。什么是根节点?在一颗树形结构中,最顶层的那个节点就是根节点了,所有的子节点都源自它发散开来。什么是父节点?树的父子关系和现实中很相似,若一个节点含有子节点,则这个节点称为其子节点的父节点。什么是叶子节点?直观来看叶子节点都位于树的最底层,就是没有分叉的节点,严格的定义是度为 0 的节点叫叶子节点。什么是节点的高度?高度是从叶子节点开始「自底向上」逐层累加的路径长度,树叶的高度为 0(有些书上也说是 0,不用纠结)什么是节点的深度?深度是从根节点开始「自顶向下」逐层累加的路径长度,根的深度为 1(有些书上也说是 0,问题不大)小技巧:高度和深度,一个从下往上数,一个从上往下数。树的特点树形数据结构,具有以下的结构特点:每个节点都只有有限个子节点或无子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外,每个子节点可以分为多个不相交的子树;树里面没有环路,意思就是从一个节点出发,除非往返,否则不能回到起点。二叉树有了前面「树」的基础铺垫,二叉树是一种特殊的树,还记的上面我们学过「节点的度」吗?二叉树中每个节点的度不大于 2 ,即它的每个节点最多只有两个分支,通常称二叉树节点的左右两个分支为左右子树。二叉树是很多其他树型结构的基础结构,比如下面要讲的 AVL 树、二叉查找树,他们都是由二叉树增加一些约束条件进化而来。三种遍历方式二叉树的遍历就是逐个访问二叉树节点的数据,常见的二叉树遍历方式有三种,分别是前中后序遍历,初学者分不清这几个顺序的差别。<strong>有个简单的记忆方式,这里的「前中后」都是对于根节点而言</strong>。先访问根节点后访问左右子树的遍历方式是前序遍历,先访问左右子树最后访问根节点的遍历方式是后序遍历,先访问左子树再访问根节点最后访问右子树的遍历方式是中序遍历,下面详细说明:<br><img src="https://pic1.zhimg.com/50/v2-2f17da8d30428a899ed1f7b4500fda86_720w.jpg?source=1940ef5c" alt="img"><br>前序遍历遍历顺序是根节点 -> 左子树 -> 右子树遍历的得到的序列是:<code>1 2 4 5 3 6 7</code>中序遍历遍历顺序是左子树 -> 根节点 -> 右子树遍历的得到的序列是:<code>4 2 5 1 6 3 7</code>后序遍历遍历顺序是左子树 -> 右子树 -> 根节点遍历的得到的序列是:<code>4 5 2 6 7 3 1</code>二叉查找树由于最基础的二叉树节点是无序的,想象一下如果在二叉树中查找一个数据,最坏情况可能要要遍历整个二叉树,这样的查找效率是非常低下的。由于基础二叉树不利于数据的查找和插入,因此我们有必要对二叉树中的数据进行排序,所以就有了「二叉查找树」,可以说这种树是为了查找而生的二叉树,有时也称它为「二叉排序树」,都是同一种结构,只是换了个叫法。查找二叉树理解了也不难,简单来说就是二叉树上所有节点的,左子树上的节点都小于根节点,右子树上所有节点的值都大于根节点。<br><img src="https://pic1.zhimg.com/50/v2-9d960b2cb00419e06bc2a979a2f53b51_720w.jpg?source=1940ef5c" alt="img"><br>这样的结构设计,使得查找目标节点非常方便,可以通过关键字和当前节点的对比,很快就能知道目标节点在该节点的左子树还是右子树上,方便在树中搜索目标节点。如果对排序二叉树执行中序遍历,因为中序遍历的顺序是:左子树 -> 根节点 -> 右子树,最终可以得到一个节点值的有序列表。举个栗子:对上图的排序二叉树执行中序遍历,我们可以得到一个有序序列:<code>1 2 3 4 5 6 7</code>查询效率二叉查找树的查询复杂度取决于目标节点的深度,因此当节点的深度比较大时,最坏的查询效率是 O(n),其中 n 是树中的节点个数。实际应用中有很多改进版的二叉查找树,目的是尽可能使得每个节点的深度不要过深,从而提高查询效率。比如 AVL 树和红黑树,可以将最坏效率降低至 O(log n),下面我们就来看下这两种改进的二叉树。AVL 树AVL 也叫平衡二叉查找树。AVL 这个名字的由来,是它的两个发明者 G. M. Adelson-Velsky 和 Evgenii Landis 的缩写,AVL 最初是他们两人在 1962 年的论文「An algorithm for the organization of information」中提出来一种数据结构。定义AVL 树是一种平衡二叉查找树,二叉查找树我们已经知道,那平衡是什么意思呢?我们举个天平的例子,天平两端的重量要差不多才能平衡,否则就会出现向一边倾斜的情况。把这个概念迁移到二叉树中来,根节点看作是天平的中点,左子树的高度放在天平左边,右子树的高度放在天平右边,当左右子树的高度相差「不是特别多」,称为是平衡的二叉树。<br><img src="https://pic1.zhimg.com/50/v2-75c1582141d7a60b5f02a751b16e46b0_720w.jpg?source=1940ef5c" alt="img"><br>AVL 树有更严格的定义:在二叉查找树中,任一节点对应的两棵子树的最大高度差为 1,这样的二叉查找树称为平衡二叉树。其中左右子树的高度差也有个专业的叫法:平衡因子。AVL 树的旋转一旦由于插入或删除导致左右子树的高度差大于 1,此时就需要旋转某些节点调整树高度,使其再次达到平衡状态,这个过程称为旋转再平衡。<br><img src="https://pic1.zhimg.com/v2-f5e31d79bc68b55eb2bf2b4e8f732d97_r.jpg?source=1940ef5c" alt="img"><br>保持树平衡的目的是可以控制查找、插入和删除在平均和最坏情况下的时间复杂度都是 O(log n),相比普通二叉树最坏情况的时间复杂度是 O(n) ,AVL 树把最坏情况的复杂度控制在可接受范围,非常合适对算法执行时间敏感类的应用。B 树B 树是鲁道夫 · 拜尔(Rudolf Bayer)1972 年在波音研究实验室(Boeing Research Labs)工作时发明的,关于 B 树名字的由来至今是个未解之谜,有人猜是 Bayer 的首字母,也有人说是波音实验室(Boeing Research Labs)的 Boeing 首字母缩写,虽然 B 树这个名字来源扑朔迷离,我们心里也没点 B 树,但不影响今天我们来学习它。定义一个 <em>m</em> 阶的 B 树是一个有以下属性的树每一个节点最多有 <em>m</em> 个子节点每一个非叶子节点(除根节点)最少有 ⌈<em>m</em>/2⌉ 个子节点,⌈<em>m</em>/2⌉表示向上取整。如果根节点不是叶子节点,那么它至少有两个子节点有 <em>k</em> 个子节点的非叶子节点拥有 <em>k</em> − 1 个键所有的叶子节点都在同一层如果之前不了解,相信第一眼看完定义肯定是蒙圈,不过多看几遍好好理解一下就好了,画个图例,对照着看看:<br><img src="https://pic1.zhimg.com/50/v2-650390e1bd4a952cc6232c5c46208e5d_720w.jpg?source=1940ef5c" alt="img"><br>特点B 树是所有节点的平衡因子均等于 0 的多路查找树(AVL 树是平衡因子不大于 1 的二路查找树)。B 树节点可以保存多个数据,使得 B 树可以不用像 AVL 树那样为了保持平衡频繁的旋转节点。B 树的多路的特性,降低了树的高度,所以 B 树相比于平衡二叉树显得矮胖很多。B 树非常适合保存在磁盘中的数据读取,因为每次读取都会有一次磁盘 IO,高度降低减少了磁盘 IO 的次数。B 树常用于实现数据库索引,典型的实现,MongoDB 索引用 B 树实现,MySQL 的 Innodb 存储引擎用 B + 树存放索引。说到 B 树不得不提起它的好兄弟 B + 树,不过这里不展开细说,只需知道,B + 树是对 B 树的改进,数据都放在叶子节点,非叶子节点只存数据索引。红黑树红黑树也是一种特殊的「二叉查找树」。到目前为止我们学习的 AVL 树和即将学习的红黑树都是二叉查找树的变体,可见二叉查找树真的是非常重要基础二叉树,如果忘了它的定义可以先回头看看。特点红黑树中每个结点都被标记了红黑属性,红黑树除了有普通的「二叉查找树」特性之外,还有以下的特征:节点是红色或黑色。根是黑色。所有叶子都是黑色(叶子是 NIL 节点)。每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。这些性质有兴趣可以自行研究,不过,现在你只需要知道,这些约束确保了红黑树的关键特性:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。<br><img src="https://pic3.zhimg.com/v2-4f79124bb9f59d429f5679102ae7fef6_r.jpg?source=1940ef5c" alt="img"><br>而节点的路径长度决定着对节点的查询效率,这样我们确保了,最坏情况下的查找、插入、删除操作的时间复杂度不超过 O(log n),并且有较高的插入和删除效率。应用场景红黑树在实际应用中比较广泛,有很多已经落地的实践,比如学习 C++ 的同学都知道会接触到 STL 标准库,而 STL 容器中的 map、set、multiset、multimap 底层实现都是基于红黑树。再比如,Linux 内核中也有红黑树的实现,Linux 系统在实现 EXT3 文件系统、虚拟内存管理系统,都有使用到红黑树这种数据结构。Linux 内核中的红黑树定义在内核文件如下的位置:<br><img src="https://pic3.zhimg.com/v2-0ec841c6176da9160c2c4faa6f364385_r.jpg?source=1940ef5c" alt="img"><br>如果找不到,可以 <code>find / -name rbtree.h</code> 搜索一下即可,有兴趣可以打开文件看下具体实现。红黑树 VS 平衡二叉树(AVL 树)插入和删除操作,一般认为红黑树的删除和插入会比 AVL 树更快。因为,红黑树不像 AVL 树那样严格的要求平衡因子小于等于 1,这就减少了为了达到平衡而进行的旋转操作次数,可以说是牺牲严格平衡性来换取更快的插入和删除时间。红黑树不要求有不严格的平衡性控制,但是红黑树的特点,使得任何不平衡都会在三次旋转之内解决。而 AVL 树如果不平衡,并不会控制旋转操作次数,旋转直到平衡为止。查找操作,AVL 树的效率更高。因为 AVL 树设计比红黑树更加平衡,不会出现平衡因子超过 1 的情况,减少了树的平均搜索长度。Trie 树(前缀树或字典树)Trie 来源于单词 retrieve(检索),Trie 树也称为前缀树或字典树。利用字符串前缀来查找指定的字符串,缩短查找时间提高查询效率,主要用于字符串的快速查找和匹配。为什么要称其为字典树呢?因为 Trie 树的功能就像字典一样,想象一下查英文字典,我们会根据首字母找到对应的页码,接着根据第二、第三… 个单词,逐步查找到目标单词,Trie 树的组织思想和字典组织很像,字典树由此得名。<br><img src="https://pic2.zhimg.com/v2-e884d9b5b91500ac8ecfbe8bcfe3be60_r.jpg?source=1940ef5c" alt="img"><br>定义Trie 的核心思想是空间换时间,有 3 个基本性质:根节点不包含字符,除根节点外每一个节点都只包含一个字符。从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。每个节点的所有子节点包含的字符都不相同。比如对单词序列<code>lru, lua, mem, mcu</code> 建立 Trie 树如下:<br><img src="https://pic1.zhimg.com/50/v2-2de31d5fbc9262f4f4009da02258d49c_720w.jpg?source=1940ef5c" alt="img"><br>Trie 树建立和查询是可以同步进行的,可以在还没建立出完成的 Trie 树之前就找到目标数据,而如果用 Hash 表等结构存储是需要先建立完成表才能开始查询,这也是 Trie 树查询速度快的原因。应用Trie 树还用于搜索引擎的关键词提示功能。比如当你在搜索框中输入检索单词的开头几个字,搜索引擎就可以自动联想匹配到可能的词组出来,这正是 Trie 树的最直接应用。<br><img src="https://pic2.zhimg.com/v2-36fc12137f84953fe95b49e5fc095768_r.jpg?source=1940ef5c" alt="img"><br>这种结构在海量数据查询上很有优势,因为不必为了找到目标数据遍历整个数据集合,只需按前缀遍历匹配的路径即可找到目标数据。因此,Trie 树还可用于解决类似以下的面试题:给你 100000 个长度不超过 10 的单词。对于每一个单词,我们要判断他出没出现过,如果出现了,求第一次出现在第几个位置。<br>有一个 1G 大小的一个文件,里面每一行是一个词,词的大小不超过 16 字节,内存限制大小是 1M,求频数最高的 100 个词<br>1000 万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串,请问怎么设计和实现?<br>一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前 10 个词,请给出思想,给出时间复杂度分析。总结一下树形数据结构有许多变种,这篇文章我们从树开始,把几种常见树形数据结构学习了一遍,包括二叉树、二叉查找树(二叉搜索树)、AVL 树、红黑树、B 树、Trie 树。,因为数据结构中各种树的变体非常多,一篇文章实难细数,篇幅有限,也只能说是浅尝辄止,作为树形数据结构入门,如果要深入的学习,每个知识点还能写出一篇文章,比如 B+ 树原理及其在数据库索引中的应用、红黑树的详细分析等等,这次柠檬只是粗略带大家走一遍。在后端开发中,数据结构与算法是后端程序员的基本素养,除了基础架构部的后端开发同学,虽然我们平常不会经常造轮子,但是掌握基本的数据结构与算法仍然是非常有必要,面试也对相关能力有要求。学习算法和数据结构不仅能提高自身编程能力,关键是现在大厂面试几乎都要让你写几道算法题,我找到<strong>一本谷歌大神的 LeetCode 刷题笔记</strong>,总结出算法刷题套路、条理清晰,BAT 刷题必备:<a href="https://zhuanlan.zhihu.com/p/336587820" target="_blank" rel="noopener">https://zhuanlan.zhihu.com/p/336587820zhuanlan.zhihu.com<img src="https://pic2.zhimg.com/v2-1f4501045c33082f75a0cbd5f7167509_ipico.jpg" alt="img"></a><strong>说了这么多,其实最务实的学习方式是读书</strong>!从事软件行业这么些年,开始也有迷茫不知方向,也不懂要看啥书学些什么内容,为了避免这样的问题,我整理了程序员进阶书单,让你少走些弯路。<a href="https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/FG0EPNRt3suOFPeLtvmvEw">月薪 30K 的程序员都学啥?附书单mp.weixin.qq.com</a><strong>我目前的工作是某大厂 C++ 后台开发</strong>,<strong>术业有专攻</strong>,如果你看到这,一定要看下我这篇 1 万 5 千字的后端开发完全学习指南,一定让你有收获!<a href="https://www.zhihu.com/question/24952874/answer/1602200603" target="_blank" rel="noopener">后端都要学习什么?www.zhihu.com<img src="https://pic2.zhimg.com/v2-1cd22f48113342835d46aa9a34da826d_120x160.jpg" alt="img"></a>说到 C++ 的话,我长期在知乎分享 C++ 学习相关,有些经验分享,你看下对你是否有用,有帮助的话点个赞同呗(疯狂暗示哈哈)<a href="https://www.zhihu.com/question/27950576/answer/1763043840" target="_blank" rel="noopener">C++ 研发实习生面试通常会被问到什么问题?www.zhihu.com<img src="https://pic3.zhimg.com/v2-2656cca49526004e1b6b1a28b783203a_120x160.jpg" alt="img"></a><a href="https://www.zhihu.com/question/30315894/answer/1574277687" target="_blank" rel="noopener">Visual Studio Code 如何编写运行 C、C++ 程序?www.zhihu.com<img src="https://pic4.zhimg.com/v2-8bd23dab284354d118e91a95fd3a9843_180x120.jpg" alt="img"></a><a href="https://www.zhihu.com/question/435206521/answer/1681193532" target="_blank" rel="noopener">寒假 45 天如何自学入门 C++?www.zhihu.com<img src="https://pic2.zhimg.com/v2-5f8dd51395bec53552651e67d23213dd_180x120.jpg" alt="img"></a><a href="https://www.zhihu.com/question/23933514/answer/1662670344" target="_blank" rel="noopener">用一年时间如何能掌握 C++ ?www.zhihu.com<img src="https://pic3.zhimg.com/v2-f7d74bd59d0b68f0a53de1fe7205bd0e_120x160.jpg" alt="img"></a><a href="https://www.zhihu.com/question/400450117/answer/1760556292" target="_blank" rel="noopener">能不能推荐几本 c++ 的书?www.zhihu.com<img src="https://pic2.zhimg.com/v2-41d3bb56ac9197a5ad420dade2b28d3d_120x160.jpg" alt="img"></a><strong>更多干货文章,关注我的专栏:</strong><a href="https://www.zhihu.com/column/c_1326224372755771392" target="_blank" rel="noopener">学编程,涨工资www.zhihu.com<img src="https://pic2.zhimg.com/v2-b6546a28845e426a25b971ce7fcab919_ipico.jpg" alt="img"></a>/<strong>**</strong>20210307 更新 <strong>***</strong>金三银四跳槽面试季,整理了 68 个 C++ 常见面试题,希望对看到这篇文章的同学有帮助,offer 拿到手软!<a href="https://zhuanlan.zhihu.com/p/346821046" target="_blank" rel="noopener">https://zhuanlan.zhihu.com/p/346821046zhuanlan.zhihu.com<img src="https://pic3.zhimg.com/v2-1a4f94e73f4a6600b57b698fca90e76e_ipico.jpg" alt="img"></a><strong>我是</strong><a href="https://www.zhihu.com/people/5d04cc04c7454c02e398bdeba909143b" target="_blank" rel="noopener">@程序员柠檬</a><strong>关注我,学习更多编程知识</strong>!</p><p>/<strong><strong>****</strong></strong>20210122 更新** <strong><strong>***</strong></strong>/</p><p><strong>收藏 900+ 点赞 300+</strong></p><p><strong>原创分享,码字不易,如果回答对你有帮助,各位在收藏之前点个赞同感谢,或分享给更多需要的小伙伴呗~</strong></p><p><img src="https://pic1.zhimg.com/885ebd3739f06ea6a3f776f8b9f3fdcd_xs.jpg?source=1940ef5c" alt="img"></p><p>郁白</p><p>已有的几个答案都是从算法角度分析的,我尝试分析下从工程角度区分红黑树与 b + 树的应用场景,红黑树一个 node 只存一对 kv,因此可以使用类似嵌入式链表的方式实现,数据结构本身不管理内存,比较轻量级,使用更灵活也更省内存,比如一个 node 可以同时存在若干个树或链表中,内核中比较常见。而 b + 树由于每个 node 要存多对 kv,node 结构的内存一般就要由数据结构自己来管,是真正意义上的 container,相对嵌入式方法实现的红黑树,好处是用法简单,自己管理内存更容易做 lockfree,一个 node 存多对 kv 的方式 cpu cache 命中率更高,所以用户态实现的高并发索引一般还是选 b + 树。再说 b 树与 b + 树,btree 的中间节点比 b + 树多存了 value,同样出度的情况下,node 更大,相对来说 cpu cache 命中率是不如 b + 树的。另外再提一句,b + 树的扫描特性(链表串起来的叶子节点)在无锁情况下是很难做的(我还没见到过解法),因此我目前见到的无锁 b + 树叶子节点都是不串起来的。补充下,关于相同前缀的优化存储和优化查询 b + 树也可以做的。</p>]]></content>
<summary type="html"><p>AVL 树,红黑树,B 树,B + 树,Trie 树都分别应用在哪些现实场景中?</p>
<p><strong>AVL 树</strong>: 最早的平衡二叉树之一。应用相对其他数据结构比较少。windows 对进程地址空间的管理用到了 AVL 树。<strong>红黑树</strong>: 平衡二叉树,广泛用在 C++ 的 STL 中。如 map 和 set 都是用红黑树实现的。<strong>B/B + 树:</strong> 用在磁盘文件组织 数据索引和数据库索引。<strong>Trie 树 (字典树):</strong> 用在统计和排序大量字符串,如自动机。</p></summary>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
</entry>
<entry>
<title>ssrf</title>
<link href="http://yoursite.com/2021/07/06/ssrf/"/>
<id>http://yoursite.com/2021/07/06/ssrf/</id>
<published>2021-07-06T02:58:08.838Z</published>
<updated>2021-08-18T07:14:02.946Z</updated>
<content type="html"><![CDATA[<p><strong>一、SSRF 概述</strong></p><p>SSRF(Server-Side Request Forgery: 服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF 攻击的目标是从外网无法访问的内部系统。(正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统)</p><a id="more"></a><p>SSRF 形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制。比如从指定 URL 地址获取网页文本内容,加载指定地址的图片,下载等等。利用的是服务端的请求伪造。ssrf 是利用存在缺陷的 web 应用作为代理攻击远程和本地的服务器</p><p><strong>二、漏洞代码示例</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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></pre></td><td class="code"><pre><span class="line"><?php</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">if (isset($_GET['url'])) {</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">$content = file_get_contents($_GET['url']); </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">#echo $_GET['url'];</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> $filename = ''.rand().'img-tasfa.jpg';</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">$fopen = fopen($filename, 'wb ');</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">#echo $filename;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> file_put_contents($filename, $content);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> #echo $_GET['url'].""; </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> $img = "<img src=\"".$filename."\"/>";</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> } </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> echo $img;</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>测试环境: 物理机 - Firefox、xp-wamp(10.10.10.12)、xp-iis(10.10.10.11) </p><p>测试 Url: <a href="http://10.10.10.12/ssrf.php?url=http://10.10.10.11/11.jpg" target="_blank" rel="noopener">http://10.10.10.12/ssrf.php?url=http://10.10.10.11/11.jpg</a><br><strong>数据流: 客户端 Firefox 输入 (10.10.10.1)–>10.10.10.12 服务端 – 发起请求 –>10.10.10.11/11.jpg</strong></p><p><strong><img src="https://img-blog.csdn.net/20161004161118549?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="img"></strong></p><p><strong><img src="https://img-blog.csdn.net/20161004161153215?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="img"></strong></p><p>这里的关键点是由服务端发起的请求,这里抓包位置为服务端! </p><p><strong>三、利用手段</strong></p><p> 可以对外网、内网、本地进行端口扫描,某些情况下端口的 Banner 会回显出来(比如 3306 的);</p><ol start="2"><li><p>攻击运行在内网或本地的有漏洞程序(比如溢出);</p></li><li><p>可以对内网 Web 应用进行指纹识别,原理是通过请求默认的文件得到特定的指纹</p></li><li><p>攻击内网或外网有漏洞的 Web 应用</p></li><li><p>使用 file:/// 协议读取本地文件</p></li></ol><p><strong>四、漏洞出现点</strong> </p><p>1)分享:通过 URL 地址分享网页内容</p><p>2)转码服务</p><p>3)在线翻译</p><p>4)图片加载与下载:通过 URL 地址加载或下载图片</p><p>5)图片、文章收藏功能</p><p>6)未公开的 api 实现以及其他调用 URL 的功能</p><p>7)从 URL 关键字中寻找</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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></pre></td><td class="code"><pre><span class="line">share </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">wap </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">url </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">link </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">src </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">source </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">target </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">u </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">3g </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">display </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">sourceURl </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">imageURL </span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">domain</span><br></pre></td></tr></table></figure><p><strong>五、防御方法</strong></p><p>1, 过滤返回信息,验证远程服务器对请求的响应是比较容易的方法。如果 web 应用是去获取某一种类型的文件。那么在把返回结果展示给用户之前先验证返回的信息是否符合标准。<br>2, 统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。<br>3, 限制请求的端口为 http 常用的端口,比如,80,443,8080,8090。<br>4, 黑名单内网 ip。避免应用被用来获取获取内网数据,攻击内网。<br>5, 禁用不需要的协议。仅仅允许 http 和 https 请求。可以防止类似于 file:///,gopher://,ftp:// 等引起的问题。</p><p>参考: <a href="http://blog.csdn.net/u011066706/article/details/51175626" target="_blank" rel="noopener">http://blog.csdn.net/u011066706/article/details/51175626</a></p><p>这里有个问题,对于内网如果成千上网的机器,如何做一个限定或过滤或者其他的解决手段?(源于阿里巴巴校招面试)</p><p><strong>六、绕过方法</strong></p><p>1、@</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://abc@127.0.0.1</span><br></pre></td></tr></table></figure><p>2、添加端口号</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://127.0.0.1:8080</span><br></pre></td></tr></table></figure><p>3、短地址</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://dwz.cn/11SMa</span><br></pre></td></tr></table></figure><p>4、可以指向任意 ip 的域名:xip.io</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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"><pre> <strong> 10.0.0.1</strong>.xip.io resolves to 10.0.0.1</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> www.<strong>10.0.0.1</strong>.xip.io resolves to 10.0.0.1</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> mysite.<strong>10.0.0.1</strong>.xip.io resolves to 10.0.0.1</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> foo.bar.<strong>10.0.0.1</strong>.xip.io resolves to 10.0.0.1</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"> </span><br></pre></td></tr></table></figure><p>5、ip 地址转换成进制来访问</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">115.239.210.26 = 16373751032</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><p><strong>一、SSRF 概述</strong></p>
<p>SSRF(Server-Side Request Forgery: 服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF 攻击的目标是从外网无法访问的内部系统。(正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统)</p></summary>
<category term="Web攻击" scheme="http://yoursite.com/tags/Web%E6%94%BB%E5%87%BB/"/>
<category term="SSRF" scheme="http://yoursite.com/tags/SSRF/"/>
</entry>
<entry>
<title>zookeeper</title>
<link href="http://yoursite.com/2021/07/05/zookeeper/"/>
<id>http://yoursite.com/2021/07/05/zookeeper/</id>
<published>2021-07-05T09:09:20.231Z</published>
<updated>2021-08-18T07:13:57.803Z</updated>
<content type="html"><![CDATA[<p>本节将介绍 ZooKeeper 的架构,并结合实例分析原子广播 (ZAB) 协议的原理,包括但不限于 ZooKeeper 的读写流程,FastLeaderElection 算法的原理,ZAB 如何保证 Leader Failover 过程中的数据一致性。</p><a id="more"></a><p><strong>ZooKeeper 是什么</strong></p><p>ZooKeeper 是一个分布式协调服务,可用于服务发现、分布式锁、分布式领导选举、配置管理等。</p><p>这一切的基础,都是 ZooKeeper 提供了一个类似于 Linux 文件系统的树形结构(可认为是轻量级的内存文件系统,但只适合存少量信息,完全不适合存储大量文件或者大文件),同时提供了对于每个节点的监控与通知机制。</p><p>既然是一个文件系统,就不得不提 ZooKeeper 是如何保证数据的一致性的。本节将将介绍 ZooKeeper 如何保证数据一致性,如何进行领导选举,以及数据监控 / 通知机制的语义保证。</p><p><strong>ZooKeeper 服务器角色</strong></p><p>ZooKeeper 集群是一个基于主从复制的高可用集群,每个服务器承担如下三种角色中的一种:</p><ul><li><strong>Leader</strong> 一个 ZooKeeper 集群同一时间只会有一个实际工作的 Leader,它会发起并维护与各 Follwer 及 Observer 间的心跳。所有的写操作必须要通过 Leader 完成再由 Leader 将写操作广播给其它服务器。</li><li><strong>Follower</strong> 一个 ZooKeeper 集群可能同时存在多个 Follower,它会响应 Leader 的心跳。Follower 可直接处理并返回客户端的读请求,同时会将写请求转发给 Leader 处理,并且负责在 Leader 处理写请求时对请求进行投票。</li><li><strong>Observer</strong> 角色与 Follower 类似,但是无投票权。</li></ul><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041453524.jpg" alt="img"></p><p><strong>原子广播(ZAB)</strong></p><p>为了保证写操作的一致性与可用性,ZooKeeper 专门设计了一种名为原子广播(ZAB)的支持崩溃恢复的一致性协议。基于该协议,ZooKeeper 实现了一种主从模式的系统架构来保持集群中各个副本之间的数据一致性。</p><p>根据 ZAB 协议,所有的写操作都必须通过 Leader 完成,Leader 写入本地日志后再复制到所有的 Follower 节点。</p><p>一旦 Leader 节点无法工作,ZAB 协议能够自动从 Follower 节点中重新选出一个合适的替代者,即新的 Leader,该过程即为领导选举。该领导选举过程,是 ZAB 协议中最为重要和复杂的过程。</p><p><strong>1、写 Leader</strong></p><p>通过 Leader 进行写操作流程如下图所示:</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041615117.jpg" alt="img"></p><p>由上图可见,通过 Leader 进行写操作,主要分为五步:</p><ol><li>客户端向 Leader 发起写请求</li><li>Leader 将写请求以 Proposal 的形式发给所有 Follower 并等待 ACK</li><li>Follower 收到 Leader 的 Proposal 后返回 ACK</li><li>Leader 得到过半数的 ACK(Leader 对自己默认有一个 ACK)后向所有的 Follower 和 Observer 发送 Commmit</li><li>Leader 将处理结果返回给客户端</li></ol><p><strong>这里要注意:</strong></p><ul><li>Leader 并不需要得到 Observer 的 ACK,即 Observer 无投票权</li><li>Leader 不需要得到所有 Follower 的 ACK,只要收到过半的 ACK 即可,同时 Leader 本身对自己有一个 ACK。上图中有 4 个 Follower,只需其中两个返回 ACK 即可,因为 (2+1) / (4+1) > 1/2</li><li>Observer 虽然无投票权,但仍须同步 Leader 的数据从而在处理读请求时可以返回尽可能新的数据</li></ul><p><strong>2、写 Follower/Observer</strong></p><p>通过 Follower/Observer 进行写操作流程如下图所示:</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041632405.jpg" alt="img"></p><p>从上图可见:</p><ul><li>Follower/Observer 均可接受写请求,但不能直接处理,而需要将写请求转发给 Leader 处理</li><li>除了多了一步请求转发,其它流程与直接写 Leader 无任何区别</li></ul><p><strong>3、读操作</strong></p><p>Leader/Follower/Observer 都可直接处理读请求,从本地内存中读取数据并返回给客户端即可。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041645454.jpg" alt="img"></p><p>由于处理读请求不需要服务器之间的交互,Follower/Observer 越多,整体可处理的读请求量越大,也即读性能越好。</p><p><strong>支持的领导选举算法</strong></p><p>可通过 electionAlg 配置项设置 ZooKeeper 用于领导选举的算法。</p><p>到 3.4.10 版本为止,可选项有:</p><ul><li>0 基于 UDP 的 LeaderElection</li><li>1 基于 UDP 的 FastLeaderElection</li><li>2 基于 UDP 和认证的 FastLeaderElection</li><li>3 基于 TCP 的 FastLeaderElection</li></ul><p>在 3.4.10 版本中,默认值为 3,也即基于 TCP 的 FastLeaderElection。另外三种算法已经被弃用,并且有计划在之后的版本中将它们彻底删除而不再支持。</p><p><strong>FastLeaderElection 原理</strong></p><p><strong>1、myid</strong></p><p>每个 ZooKeeper 服务器,都需要在数据文件夹下创建一个名为 myid 的文件,该文件包含整个 ZooKeeper 集群唯一的 ID(整数)。例如,某 ZooKeeper 集群包含三台服务器,hostname 分别为 zoo1、zoo2 和 zoo3,其 myid 分别为 1、2 和 3,则在配置文件中其 ID 与 hostname 必须一一对应,如下所示。在该配置文件中,server. 后面的数据即为 myid</p><p>server.1=zoo1:2888:3888</p><p>server.2=zoo2:2888:3888</p><p>server.3=zoo3:2888:3888</p><p><strong>2、zxid</strong></p><p>类似于 RDBMS 中的事务 ID,用于标识一次更新操作的 Proposal ID。为了保证顺序性,该 zkid 必须单调递增。因此 ZooKeeper 使用一个 64 位的数来表示,高 32 位是 Leader 的 epoch,从 1 开始,每次选出新的 Leader,epoch 加一。低 32 位为该 epoch 内的序号,每次 epoch 变化,都将低 32 位的序号重置。这样保证了 zkid 的全局递增性。</p><p><strong>3、服务器状态</strong></p><ul><li><strong>LOOKING</strong> 不确定 Leader 状态。该状态下的服务器认为当前集群中没有 Leader,会发起 Leader 选举。</li><li><strong>FOLLOWING</strong> 跟随者状态。表明当前服务器角色是 Follower,并且它知道 Leader 是谁。</li><li><strong>LEADING</strong> 领导者状态。表明当前服务器角色是 Leader,它会维护与 Follower 间的心跳。</li><li><strong>OBSERVING</strong> 观察者状态。表明当前服务器角色是 Observer,与 Folower 唯一的不同在于不参与选举,也不参与集群写操作时的投票。</li></ul><p><strong>4、选票数据结构</strong></p><p>每个服务器在进行领导选举时,会发送如下关键信息:</p><ul><li><strong>logicClock</strong> 每个服务器会维护一个自增的整数,名为 logicClock,它表示这是该服务器发起的第多少轮投票</li><li><strong>state</strong> 当前服务器的状态</li><li><strong>self_id</strong> 当前服务器的 myid</li><li><strong>self_zxid</strong> 当前服务器上所保存的数据的最大 zxid</li><li><strong>vote_id</strong> 被推举的服务器的 myid</li><li><strong>vote_zxid</strong> 被推举的服务器上所保存的数据的最大 zxid</li></ul><p><strong>5、投票流程</strong></p><p><strong>自增选举轮次</strong></p><p>ZooKeeper 规定所有有效的投票都必须在同一轮次中。每个服务器在开始新一轮投票时,会先对自己维护的 logicClock 进行自增操作。</p><p><strong>初始化选票</strong></p><p>每个服务器在广播自己的选票前,会将自己的投票箱清空。该投票箱记录了所收到的选票。例:服务器 2 投票给服务器 3,服务器 3 投票给服务器 1,则服务器 1 的投票箱为 (2, 3), (3, 1), (1, 1)。票箱中只会记录每一投票者的最后一票,如投票者更新自己的选票,则其它服务器收到该新选票后会在自己票箱中更新该服务器的选票。</p><p><strong>发送初始化选票</strong></p><p>每个服务器最开始都是通过广播把票投给自己。</p><p><strong>接收外部投票</strong></p><p>服务器会尝试从其它服务器获取投票,并记入自己的投票箱内。如果无法获取任何外部投票,则会确认自己是否与集群中其它服务器保持着有效连接。如果是,则再次发送自己的投票;如果否,则马上与之建立连接。</p><p><strong>判断选举轮次</strong></p><p>收到外部投票后,首先会根据投票信息中所包含的 logicClock 来进行不同处理:</p><ul><li>外部投票的 logicClock 大于自己的 logicClock。说明该服务器的选举轮次落后于其它服务器的选举轮次,立即清空自己的投票箱并将自己的 logicClock 更新为收到的 logicClock,然后再对比自己之前的投票与收到的投票以确定是否需要变更自己的投票,最终再次将自己的投票广播出去。</li><li>外部投票的 logicClock 小于自己的 logicClock。当前服务器直接忽略该投票,继续处理下一个投票。</li><li>外部投票的 logickClock 与自己的相等。当时进行选票 PK。</li></ul><p><strong>选票 PK</strong></p><p>选票 PK 是基于 (self_id, self_zxid) 与(vote_id, vote_zxid)的对比:</p><ul><li>外部投票的 logicClock 大于自己的 logicClock,则将自己的 logicClock 及自己的选票的 logicClock 变更为收到的 logicClock</li><li>若 logicClock 一致,则对比二者的 vote_zxid,若外部投票的 vote_zxid 比较大,则将自己的票中的 vote_zxid 与 vote_myid 更新为收到的票中的 vote_zxid 与 vote_myid 并广播出去,另外将收到的票及自己更新后的票放入自己的票箱。如果票箱内已存在 (self_myid, self_zxid) 相同的选票,则直接覆盖</li><li>若二者 vote_zxid 一致,则比较二者的 vote_myid,若外部投票的 vote_myid 比较大,则将自己的票中的 vote_myid 更新为收到的票中的 vote_myid 并广播出去,另外将收到的票及自己更新后的票放入自己的票箱</li></ul><p><strong>统计选票</strong></p><p>如果已经确定有过半服务器认可了自己的投票(可能是更新后的投票),则终止投票。否则继续接收其它服务器的投票。</p><p><strong>更新服务器状态</strong></p><p>投票终止后,服务器开始更新自身状态。若过半的票投给了自己,则将自己的服务器状态更新为 LEADING,否则将自己的状态更新为 FOLLOWING。</p><p><strong>集群启动领导选举</strong></p><p><strong>1、初始投票给自己</strong></p><p>集群刚启动时,所有服务器的 logicClock 都为 1,zxid 都为 0。各服务器初始化后,都投票给自己,并将自己的一票存入自己的票箱,如下图所示。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041723580.jpg" alt="img"></p><p>在上图中,(1, 1, 0) 第一位数代表投出该选票的服务器的 logicClock,第二位数代表被推荐的服务器的 myid,第三位代表被推荐的服务器的最大的 zxid。由于该步骤中所有选票都投给自己,所以第二位的 myid 即是自己的 myid,第三位的 zxid 即是自己的 zxid。</p><p>此时各自的票箱中只有自己投给自己的一票。</p><p><strong>2、更新选票</strong></p><p>服务器收到外部投票后,进行选票 PK,相应更新自己的选票并广播出去,并将合适的选票存入自己的票箱,如下图所示。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041738662.jpg" alt="img"></p><p>服务器 1 收到服务器 2 的选票(1, 2, 0)和服务器 3 的选票(1, 3, 0)后,由于所有的 logicClock 都相等,所有的 zxid 都相等,因此根据 myid 判断应该将自己的选票按照服务器 3 的选票更新为(1, 3, 0),并将自己的票箱全部清空,再将服务器 3 的选票与自己的选票存入自己的票箱,接着将自己更新后的选票广播出去。此时服务器 1 票箱内的选票为 (1, 3),(3, 3)。</p><p>同理,服务器 2 收到服务器 3 的选票后也将自己的选票更新为(1, 3, 0)并存入票箱然后广播。此时服务器 2 票箱内的选票为 (2, 3),(3, ,3)。</p><p>服务器 3 根据上述规则,无须更新选票,自身的票箱内选票仍为(3, 3)。</p><p>服务器 1 与服务器 2 更新后的选票广播出去后,由于三个服务器最新选票都相同,最后三者的票箱内都包含三张投给服务器 3 的选票。</p><p><strong>3、根据选票确定角色</strong></p><p>根据上述选票,三个服务器一致认为此时服务器 3 应该是 Leader。因此服务器 1 和 2 都进入 FOLLOWING 状态,而服务器 3 进入 LEADING 状态。之后 Leader 发起并维护与 Follower 间的心跳。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041757212.jpg" alt="img"></p><p><strong>Follower 重启选举</strong></p><p><strong>1、Follower</strong> <strong>重启投票给自己</strong></p><p>Follower 重启,或者发生网络分区后找不到 Leader,会进入 LOOKING 状态并发起新的一轮投票。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041817539.jpg" alt="img"></p><p><strong>2、发现已有 Leader 后成为 Follower</strong></p><p>服务器 3 收到服务器 1 的投票后,将自己的状态 LEADING 以及选票返回给服务器 1。服务器 2 收到服务器 1 的投票后,将自己的状态 FOLLOWING 及选票返回给服务器 1。此时服务器 1 知道服务器 3 是 Leader,并且通过服务器 2 与服务器 3 的选票可以确定服务器 3 确实得到了超过半数的选票。因此服务器 1 进入 FOLLOWING 状态。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041833444.jpg" alt="img"></p><p><strong>Leader 重启选举</strong></p><p><strong>1、Follower 发起新投票</strong></p><p>Leader(服务器 3)宕机后,Follower(服务器 1 和 2)发现 Leader 不工作了,因此进入 LOOKING 状态并发起新的一轮投票,并且都将票投给自己。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041907813.jpg" alt="img"></p><p><strong>2、广播更新选票</strong></p><p>服务器 1 和 2 根据外部投票确定是否要更新自身的选票。这里有两种情况:</p><ul><li>服务器 1 和 2 的 zxid 相同。例如在服务器 3 宕机前服务器 1 与 2 完全与之同步。此时选票的更新主要取决于 myid 的大小</li><li>服务器 1 和 2 的 zxid 不同。在旧 Leader 宕机之前,其所主导的写操作,只需过半服务器确认即可,而不需所有服务器确认。换句话说,服务器 1 和 2 可能一个与旧 Leader 同步(即 zxid 与之相同)另一个不同步(即 zxid 比之小)。此时选票的更新主要取决于谁的 zxid 较大</li></ul><p>在上图中,服务器 1 的 zxid 为 11,而服务器 2 的 zxid 为 10,因此服务器 2 将自身选票更新为(3, 1, 11),如下图所示。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041923754.jpg" alt="img"></p><p><strong>3、选出新 Leader</strong></p><p>经过上一步选票更新后,服务器 1 与服务器 2 均将选票投给服务器 1,因此服务器 2 成为 Follower,而服务器 1 成为新的 Leader 并维护与服务器 2 的心跳。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041937775.jpg" alt="img"></p><p><strong>4、旧 Leader 恢复后发起选举</strong></p><p>旧的 Leader 恢复后,进入 LOOKING 状态并发起新一轮领导选举,并将选票投给自己。此时服务器 1 会将自己的 LEADING 状态及选票(3, 1, 11)返回给服务器 3,而服务器 2 将自己的 FOLLOWING 状态及选票(3, 1, 11)返回给服务器 3。如下图所示。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105041955650.jpg" alt="img"></p><p><strong>5、旧 Leader 成为 Follower</strong></p><p>服务器 3 了解到 Leader 为服务器 1,且根据选票了解到服务器 1 确实得到过半服务器的选票,因此自己进入 FOLLOWING 状态。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105042009165.jpg" alt="img"></p><p><strong>Commit 过的数据不丢失</strong></p><p><strong>1、Failover</strong> <strong>前状态</strong></p><p>为更好演示 Leader Failover 过程,本例中共使用 5 个 ZooKeeper 服务器。A 作为 Leader,共收到 P1、P2、P3 三条消息,并且 Commit 了 1 和 2,且总体顺序为 P1、P2、C1、P3、C2。根据顺序性原则,其它 Follower 收到的消息的顺序肯定与之相同。其中 B 与 A 完全同步,C 收到 P1、P2、C1,D 收到 P1、P2,E 收到 P1,如下图所示。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105042029601.jpg" alt="img"></p><p>这里要注意:</p><ul><li>由于 A 没有 C3,意味着收到 P3 的服务器的总个数不会超过一半,也即包含 A 在内最多只有两台服务器收到 P3。在这里 A 和 B 收到 P3,其它服务器均未收到 P3</li><li>由于 A 已写入 C1、C2,说明它已经 Commit 了 P1、P2,因此整个集群有超过一半的服务器,即最少三个服务器收到 P1、P2。在这里所有服务器都收到了 P1,除 E 外其它服务器也都收到了 P2</li></ul><p><strong>2、选出新 Leader</strong></p><p>旧 Leader 也即 A 宕机后,其它服务器根据上述 FastLeaderElection 算法选出 B 作为新的 Leader。C、D 和 E 成为 Follower 且以 B 为 Leader 后,会主动将自己最大的 zxid 发送给 B,B 会将 Follower 的 zxid 与自身 zxid 间的所有被 Commit 过的消息同步给 Follower,如下图所示。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105042045130.jpg" alt="img"></p><p>在上图中:</p><ul><li>P1 和 P2 都被 A Commit,因此 B 会通过同步保证 P1、P2、C1 与 C2 都存在于 C、D 和 E 中</li><li>P3 由于未被 A Commit,同时幸存的所有服务器中 P3 未存在于大多数据服务器中,因此它不会被同步到其它 Follower</li></ul><p><strong>3、通知 Follower 可对外服务</strong></p><p>同步完数据后,B 会向 D、C 和 E 发送 NEWLEADER 命令并等待大多数服务器的 ACK(下图中 D 和 E 已返回 ACK,加上 B 自身,已经占集群的大多数),然后向所有服务器广播 UPTODATE 命令。收到该命令后的服务器即可对外提供服务。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105042058968.jpg" alt="img"></p><p><strong>未 Commit 过的消息对客户端不可见</strong></p><p>在上例中,P3 未被 A Commit 过,同时因为没有过半的服务器收到 P3,因此 B 也未 Commit P3(如果有过半服务器收到 P3,即使 A 未 Commit P3,B 会主动 Commit P3,即 C3),所以它不会将 P3 广播出去。</p><p>具体做法是,B 在成为 Leader 后,先判断自身未 Commit 的消息(本例中即 P3)是否存在于大多数服务器中从而决定是否要将其 Commit。然后 B 可得出自身所包含的被 Commit 过的消息中的最小 zxid(记为 min_zxid)与最大 zxid(记为 max_zxid)。C、D 和 E 向 B 发送自身 Commit 过的最大消息 zxid(记为 max_zxid)以及未被 Commit 过的所有消息(记为 zxid_set)。B 根据这些信息作出如下操作:</p><ul><li>如果 Follower 的 max_zxid 与 Leader 的 max_zxid 相等,说明该 Follower 与 Leader 完全同步,无须同步任何数据</li><li>如果 Follower 的 max_zxid 在 Leader 的 (min_zxid,max_zxid) 范围内,Leader 会通过 TRUNC 命令通知 Follower 将其 zxid_set 中大于 Follower 的 max_zxid(如果有)的所有消息全部删除</li></ul><p>上述操作保证了未被 Commit 过的消息不会被 Commit 从而对外不可见。</p><p>上述例子中 Follower 上并不存在未被 Commit 的消息。但可考虑这种情况,如果将上述例子中的服务器数量从五增加到七,服务器 F 包含 P1、P2、C1、P3,服务器 G 包含 P1、P2。此时服务器 F、A 和 B 都包含 P3,但是因为票数未过半,因此 B 作为 Leader 不会 Commit P3,而会通过 TRUNC 命令通知 F 删除 P3。如下图所示。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105042119217.jpg" alt="img"></p><p><strong>本节总结</strong></p><ul><li>由于使用主从复制模式,所有的写操作都要由 Leader 主导完成,而读操作可通过任意节点完成,因此 ZooKeeper 读性能远好于写性能,更适合读多写少的场景</li><li>虽然使用主从复制模式,同一时间只有一个 Leader,但是 Failover 机制保证了集群不存在单点失败(SPOF)的问题</li><li>ZAB 协议保证了 Failover 过程中的数据一致性</li><li>服务器收到数据后先写本地文件再进行处理,保证了数据的持久性</li></ul><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105042918428.png" alt="img"></p><p>本节将结合实例演示了使用 ZooKeeper 实现分布式锁与领导选举的原理与具体实现方法。</p><p><strong>ZooKeeper 节点类型</strong></p><p>ZooKeeper 提供了一个类似于 Linux 文件系统的树形结构。该树形结构中每个节点被称为 znode ,可按如下两个维度分类:</p><p><strong>1、Persist vs. Ephemeral</strong></p><ul><li>Persist 节点,一旦被创建,便不会意外丢失,即使服务器全部重启也依然存在。每个 Persist 节点即可包含数据,也可包含子节点</li><li>Ephemeral 节点,在创建它的客户端与服务器间的 Session 结束时自动被删除。服务器重启会导致 Session 结束,因此 Ephemeral 类型的 znode 此时也会自动删除</li></ul><p><strong>2、Sequence vs. Non-sequence</strong></p><ul><li>Non-sequence 节点,多个客户端同时创建同一 Non-sequence 节点时,只有一个可创建成功,其它匀失败。并且创建出的节点名称与创建时指定的节点名完全一样</li><li>Sequence 节点,创建出的节点名在指定的名称之后带有 10 位 10 进制数的序号。多个客户端创建同一名称的节点时,都能创建成功,只是序号不同</li></ul><p><strong>ZooKeeper 语义保证</strong></p><p>ZooKeeper 简单高效,同时提供如下语义保证,从而使得我们可以利用这些特性提供复杂的服务。</p><ul><li>顺序性:客户端发起的更新会按发送顺序被应用到 ZooKeeper 上</li><li>原子性:更新操作要么成功要么失败,不会出现中间状态</li><li>单一系统镜像:一个客户端无论连接到哪一个服务器都能看到完全一样的系统镜像(即完全一样的树形结构)。注:根据上文《ZooKeeper 架构及 FastLeaderElection 机制》介绍的 ZAB 协议,写操作并不保证更新被所有的 Follower 立即确认,因此通过部分 Follower 读取数据并不能保证读到最新的数据,而部分 Follwer 及 Leader 可读到最新数据。如果一定要保证单一系统镜像,可在读操作前使用 sync 方法。</li><li>可靠性:一个更新操作一旦被接受即不会意外丢失,除非被其它更新操作覆盖</li><li>最终一致性:写操作最终(而非立即)会对客户端可见</li></ul><p><strong>ZooKeeper Watch 机制</strong></p><p>所有对 ZooKeeper 的读操作,都可附带一个 Watch 。一旦相应的数据有变化,该 Watch 即被触发。</p><p>Watch 有如下特点:</p><ul><li>主动推送:Watch 被触发时,由 ZooKeeper 服务器主动将更新推送给客户端,而不需要客户端轮询。</li><li>一次性:数据变化时,Watch 只会被触发一次。如果客户端想得到后续更新的通知,必须要在 Watch 被触发后重新注册一个 Watch。</li><li>可见性:如果一个客户端在读请求中附带 Watch,Watch 被触发的同时再次读取数据,客户端在得到 Watch 消息之前肯定不可能看到更新后的数据。换句话说,更新通知先于更新结果。</li><li>顺序性:如果多个更新触发了多个 Watch ,那 Watch 被触发的顺序与更新顺序一致。</li></ul><p><strong>分布式锁与领导选举关键点</strong></p><p><strong>1、最多一个获取锁 / 成为 Leader</strong></p><p>对于分布式锁(这里特指排它锁)而言,任意时刻,最多只有一个进程(对于单进程内的锁而言是单线程)可以获得锁。</p><p>对于领导选举而言,任意时间,最多只有一个成功当选为 Leader。否则即出现脑裂(Split brain)</p><p><strong>2、锁重入 / 确认自己是 Leader</strong></p><p>对于分布式锁,需要保证获得锁的进程在释放锁之前可再次获得锁,即锁的可重入性。</p><p>对于领导选举,Leader 需要能够确认自己已经获得领导权,即确认自己是 Leader。</p><p><strong>3、释放锁 / 放弃领导权</strong> </p><p>锁的获得者应该能够正确释放已经获得的锁,并且当获得锁的进程宕机时,锁应该自动释放,从而使得其它竞争方可以获得该锁,从而避免出现死锁的状态。</p><p>领导应该可以主动放弃领导权,并且当领导所在进程宕机时,领导权应该自动释放,从而使得其它参与者可重新竞争领导而避免进入无主状态。</p><p><strong>4、感知锁释放 / 领导权的放弃</strong></p><p>当获得锁的一方释放锁时,其它对于锁的竞争方需要能够感知到锁的释放,并再次尝试获取锁。</p><p>原来的 Leader 放弃领导权时,其它参与方应该能够感知该事件,并重新发起选举流程。</p><p><strong>非公平领导选举</strong></p><p>从上面几个方面可见,分布式锁与领导选举的技术要点非常相似,实际上其实现机制也相近。这里以领导选举为例来说明二者的实现原理,分布式锁的实现原理也几乎一致。 </p><p><strong>1、选主过程</strong></p><p>假设有三个 ZooKeeper 的客户端,如下图所示,同时竞争 Leader。这三个客户端同时向 ZooKeeper 集群注册 Ephemeral 且 Non-sequence 类型的节点,路径都为 /zkroot/leader(工程实践中,路径名可自定义)。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105042158212.jpg" alt="img"></p><p>如上图所示,由于是 Non-sequence 节点,这三个客户端只会有一个创建成功,其它节点均创建失败。此时,创建成功的客户端(即上图中的 Client 1)即成功竞选为 Leader 。其它客户端(即上图中的 Client 2 和 Client 3)此时匀为 Follower。</p><p><strong>2、放弃领导权</strong></p><p>如果 Leader 打算主动放弃领导权,直接删除 /zkroot/leader 节点即可。</p><p>如果 Leader 进程意外宕机,其与 ZooKeeper 间的 Session 也结束,该节点由于是 Ephemeral 类型的节点,因此也会自动被删除。</p><p>此时 /zkroot/leader 节点不复存在,对于其它参与竞选的客户端而言,之前的 Leader 已经放弃了领导权。</p><p><strong>3、感知领导权的放弃</strong></p><p>由上图可见,创建节点失败的节点,除了成为 Follower 以外,还会向 /zkroot/leader 注册一个 Watch ,一旦 Leader 放弃领导权,也即该节点被删除,所有的 Follower 会收到通知。</p><p><strong>4、重新选举</strong></p><p>感知到旧 Leader 放弃领导权后,所有的 Follower 可以再次发起新一轮的领导选举,如下图所示。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105042227598.jpg" alt="img"></p><p>从上图中可见:</p><ul><li>新一轮的领导选举方法与最初的领导选举方法完全一样,都是发起节点创建请求,创建成功即为 Leader,否则为 Follower ,且 Follower 会 Watch 该节点</li><li>新一轮的选举结果,无法预测,与它们在第一轮选举中的顺序无关。这也是该方案被称为非公平模式的原因</li></ul><p>小结</p><ol><li>非公平模式实现简单,每一轮选举方法都完全一样</li><li>竞争参与方不多的情况下,效率高。每个 Follower 通过 Watch 感知到节点被删除的时间不完全一样,只要有一个 Follower 得到通知即发起竞选,即可保证当时有新的 Leader 被选出</li><li>给 ZooKeeper 集群造成的负载大,因此扩展性差。如果有上万个客户端都参与竞选,意味着同时会有上万个写请求发送给 Zookeper。如《ZooKeeper 架构》一文所述,ZooKeeper 存在单点写的问题,写性能不高。同时一旦 Leader 放弃领导权,ZooKeeper 需要同时通知上万个 Follower,负载较大。</li></ol><p><strong>公平领导选举</strong></p><p><strong>1、选主过程</strong></p><p>如下图所示,公平领导选举中,各客户端均创建 /zkroot/leader 节点,且其类型为 Ephemeral 与 Sequence。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105042242375.jpg" alt="img"></p><p>由于是 Sequence 类型节点,故上图中三个客户端均创建成功,只是序号不一样。此时,每个客户端都会判断自己创建成功的节点的序号是不是当前最小的。如果是,则该客户端为 Leader,否则即为 Follower。</p><p>在上图中,Client 1 创建的节点序号为 1 ,Client 2 创建的节点序号为 2,Client 3 创建的节点序号为 3。由于最小序号为 1 ,且该节点由 Client 1 创建,故 Client 1 为 Leader 。</p><p><strong>2、放弃领导权</strong></p><p>Leader 如果主动放弃领导权,直接删除其创建的节点即可。</p><p>如果 Leader 所在进程意外宕机,其与 ZooKeeper 间的 Session 结束,由于其创建的节点为 Ephemeral 类型,故该节点自动被删除。</p><p><strong>3、感知领导权的放弃</strong></p><p>与非公平模式不同,每个 Follower 并非都 Watch 由 Leader 创建出来的节点,而是 Watch 序号刚好比自己序号小的节点。</p><p>在上图中,总共有 1、2、3 共三个节点,因此 Client 2 Watch /zkroot/leader1,Client 3 Watch /zkroot/leader2。(注:序号应该是 10 位数字,而非一位数字,这里为了方便,以一位数字代替)</p><p>一旦 Leader 宕机,/zkroot/leader1 被删除,Client 2 可得到通知。此时 Client 3 由于 Watch 的是 /zkroot/leader2 ,故不会得到通知。</p><p><strong>重新选举</strong></p><p>重新选举 Client 2 得到 /zkroot/leader1 被删除的通知后,不会立即成为新的 Leader 。而是先判断自己的序号 2 是不是当前最小的序号。在该场景下,其序号确为最小。因此 Client 2 成为新的 Leader 。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105042304441.jpg" alt="img"></p><p>这里要注意,如果在 Client 1 放弃领导权之前,Client 2 就宕机了,Client 3 会收到通知。此时 Client 3 不会立即成为 Leader,而是要先判断自己的序号 3 是否为当前最小序号。很显然,由于 Client 1 创建的 /zkroot/leader1 还在,因此 Client 3 不会成为新的 Leader ,并向 Client 2 序号 2 前面的序号,也即 1 创建 Watch。该过程如下图所示。</p><p><img src="http://dbaplus.cn/uploadfile/2018/0105/20180105042317577.jpg" alt="img"></p><p>小结</p><ol><li>实现相对复杂;</li><li>扩展性好,每个客户端都只 Watch 一个节点且每次节点被删除只须通知一个客户端;</li><li>旧 Leader 放弃领导权时,其它客户端根据竞选的先后顺序(也即节点序号)成为新 Leader,这也是公平模式的由来;</li><li>延迟相对非公平模式要高,因为它必须等待特定节点得到通知才能选出新的 Leader。</li></ol>]]></content>
<summary type="html"><p>本节将介绍 ZooKeeper 的架构,并结合实例分析原子广播 (ZAB) 协议的原理,包括但不限于 ZooKeeper 的读写流程,FastLeaderElection 算法的原理,ZAB 如何保证 Leader Failover 过程中的数据一致性。</p></summary>
<category term="zookeeper" scheme="http://yoursite.com/tags/zookeeper/"/>
</entry>
<entry>
<title>二叉树</title>
<link href="http://yoursite.com/2021/07/05/%E4%BA%8C%E5%8F%89%E6%A0%91/"/>
<id>http://yoursite.com/2021/07/05/%E4%BA%8C%E5%8F%89%E6%A0%91/</id>
<published>2021-07-05T06:27:45.913Z</published>
<updated>2021-08-18T07:14:49.360Z</updated>
<content type="html"><![CDATA[<h1><span id="概念篇">概念篇</span></h1><h2><span id="1bst-二叉查找树也叫二叉搜索树-二叉排序树binary-search-tree">1.BST, 二叉查找树,也叫二叉搜索树、二叉排序树,Binary Search Tree</span></h2><p>在二叉查找树中,有以下性质;</p><ol><li>若任意节点的左子树不为空,则左子树上的所有节点的值小于它的根节点;</li><li>若右子树不为空,则右子树的所有节点大于它的根节点;</li><li>任意节点的左右子树也都是二叉查找树;</li><li>树中不存在重复值;(也可以通过链表或动态数组把数值相同的数据存储在一个节点上,或者把新插入的数据当做大于这个节点的值来处理)</li></ol><a id="more"></a><p>二叉查找树的中序遍历一定是从小到大的。</p><h2><span id="2avl平衡搜索二叉树">2.AVL,平衡搜索二叉树</span></h2><p>平衡二叉树的提出就是为了保证树不会太倾斜,尽量维持两边平衡。定义如下:</p><ol><li>平衡二叉树要么是一棵空树</li><li>要么保证左右子树的高度之差不大于 1</li><li>子树也必须是一颗平衡二叉树</li></ol><h2><span id="3rbt红黑树">3.RBT,红黑树</span></h2><p>转自:<a href="https://blog.csdn.net/when_less_is_more/article/details/64131354" target="_blank" rel="noopener">BST 二叉排序树,AVL 平衡二叉树,RBT 红黑树,B - 树,B + 树,B * 树</a></p><p>AVL 是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多;<br>红黑是弱平衡的,用非严格的平衡来换取增删节点时候旋转次数的降低;<br>所以简单说,搜索的次数远远大于插入和删除,那么选择 AVL 树,如果搜索,插入删除次数几乎差不多,应该选择 RB 树。</p><p>红黑树上每个结点内含五个域,color,key,left,right,p。如果相应的指针域没有,则设为 NIL。<br>一般的,红黑树,满足以下性质,即只有满足以下全部性质的树,我们才称之为红黑树:<br>1)每个结点要么是红的,要么是黑的。<br>2)根结点是黑的。<br>3)每个叶结点,即空结点(NIL)是黑的。<br>4)如果一个结点是红的,那么它的俩个儿子都是黑的。<br>5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。</p><p>最后两点证明高度差是小于两倍的;</p><h2><span id="4size-balanced-treesb-树">4.Size Balanced Tree,SB 树</span></h2><p>同一层的节点称为叔叔节点,叔叔节点的子节点为侄子节点。<br>SB 树的平衡性来自于,任何一个叔叔节点的节点个数,不能少于任何一个侄子节点的节点个数。</p><h2><span id="5-各种树操作的时间复杂度对比">5. 各种树操作的时间复杂度对比</span></h2><p>最后附上一张对比图(来自:<a href="https://blog.csdn.net/wuhuagu_wuhuaguo/article/details/78531650)" target="_blank" rel="noopener">https://blog.csdn.net/wuhuagu_wuhuaguo/article/details/78531650)</a></p><p><img src="https://img-blog.csdnimg.cn/20200524210614684.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lhb2dlcGlsYQ==,size_16,color_FFFFFF,t_70" alt="img"></p>]]></content>
<summary type="html"><h1 id="概念篇"><a href="#概念篇" class="headerlink" title="概念篇"></a>概念篇</h1><h2 id="1-BST,-二叉查找树,也叫二叉搜索树、二叉排序树,Binary-Search-Tree"><a href="#1-BST,-二叉查找树,也叫二叉搜索树、二叉排序树,Binary-Search-Tree" class="headerlink" title="1.BST, 二叉查找树,也叫二叉搜索树、二叉排序树,Binary Search Tree"></a>1.BST, 二叉查找树,也叫二叉搜索树、二叉排序树,Binary Search Tree</h2><p>在二叉查找树中,有以下性质;</p>
<ol>
<li>若任意节点的左子树不为空,则左子树上的所有节点的值小于它的根节点;</li>
<li>若右子树不为空,则右子树的所有节点大于它的根节点;</li>
<li>任意节点的左右子树也都是二叉查找树;</li>
<li>树中不存在重复值;(也可以通过链表或动态数组把数值相同的数据存储在一个节点上,或者把新插入的数据当做大于这个节点的值来处理)</li>
</ol></summary>
<category term="算法" scheme="http://yoursite.com/tags/%E7%AE%97%E6%B3%95/"/>
</entry>
<entry>
<title>mysq优化</title>
<link href="http://yoursite.com/2021/07/05/mysq%E4%BC%98%E5%8C%96/"/>
<id>http://yoursite.com/2021/07/05/mysq%E4%BC%98%E5%8C%96/</id>
<published>2021-07-05T03:33:44.949Z</published>
<updated>2021-08-18T07:14:01.125Z</updated>
<content type="html"><![CDATA[<h2><span id="1mysql-数据库作发布系统的存储一天五万条以上的增量预计运维三年-怎么优化">1.MySQL 数据库作发布系统的存储,一天五万条以上的增量,预计运维三年, 怎么优化?</span></h2><a id="more"></a><p>a. 设计良好的数据库结构,允许部分数据冗余,尽量避免 join 查询,提高效率。<br>b. 选择合适的表字段数据类型和存储引擎,适当的添加索引。<br>c. mysql 库主从读写分离。<br>d. 找规律分表,减少单表中的数据量提高查询速度。<br>e。添加缓存机制,比如 memcached,apc 等。<br>f. 不经常改动的页面,生成静态页面。<br>g. 书写高效率的 SQL。比如 SELECT * FROM TABEL 改为 SELECT field_1, field_2, field_3 FROM TABLE.</p><h2><span id="2-实践中如何优化-mysql">2. 实践中如何优化 MySQL</span></h2><p>最好是按照以下顺序优化:</p><p>1.SQL 语句及索引的优化</p><ol start="2"><li><p>数据库表结构的优化</p></li><li><p>系统配置的优化</p></li><li><p>硬件的优化</p></li></ol><h2><span id="3-优化数据库的方法">3. 优化数据库的方法</span></h2><ol><li>选取最适用的字段属性,尽可能减少定义字段宽度,尽量把字段设置 NOTNULL,例如’省份’、’性别’最好适用 ENUM</li><li>使用连接 (JOIN) 来代替子查询</li><li>适用联合 (UNION) 来代替手动创建的临时表</li><li>事务处理</li><li>锁定表、优化事务处理</li><li>适用外键,优化锁定表</li><li>建立索引</li><li>优化查询语句</li></ol><h2><span id="4-如何通俗地理解三个范式">4. 如何通俗地理解三个范式?</span></h2><p>答:</p><p>第一范式:1NF 是对属性的原子性约束,要求属性具有原子性,不可再分解;</p><p>第二范式:2NF 是对记录的惟一性约束,要求记录有惟一标识,即实体的惟一性; </p><p>第三范式:3NF 是对字段冗余性的约束,即任何字段不能由其他字段派生出来,它要求字段没有冗余。。</p><p>范式化设计优缺点:</p><p><strong>优</strong>点:</p><p>可以尽量得减少数据冗余,使得更新快,体积小</p><p>缺点: </p><p>对于查询需要多个表进行关联,减少写得效率增加读得效率,更难进行索引优化</p><p><strong>反范式化:</strong></p><p>优点: 可以减少表得关联,可以更好得进行索引优化</p><p>缺点: 数据冗余以及数据异常,数据得修改需要更多的成本</p><h2><span id="5-说说对-sql-语句优化有哪些方法选择几条">5. 说说对 SQL 语句优化有哪些方法?(选择几条)</span></h2><p>(1)Where 子句中:where 表之间的连接必须写在其他 Where 条件之前,那些可以过滤掉最大数量记录的条件必须写在 Where 子句的末尾. HAVING 最后。</p><p>(2)用 EXISTS 替代 IN、用 NOT EXISTS 替代 NOT IN。</p><p>(3) 避免在索引列上使用计算</p><p>(4)避免在索引列上使用 IS NULL 和 IS NOT NULL</p><p>(5)对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。</p><p>(6)应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描</p><p>(7)应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描</p>]]></content>
<summary type="html"><h2 id="1-MySQL-数据库作发布系统的存储,一天五万条以上的增量,预计运维三年-怎么优化?"><a href="#1-MySQL-数据库作发布系统的存储,一天五万条以上的增量,预计运维三年-怎么优化?" class="headerlink" title="1.MySQL 数据库作发布系统的存储,一天五万条以上的增量,预计运维三年, 怎么优化?"></a>1.MySQL 数据库作发布系统的存储,一天五万条以上的增量,预计运维三年, 怎么优化?</h2></summary>
<category term="Mysql" scheme="http://yoursite.com/tags/Mysql/"/>
</entry>
<entry>
<title>shell 编程</title>
<link href="http://yoursite.com/2020/08/26/shell%20%E7%BC%96%E7%A8%8B/"/>
<id>http://yoursite.com/2020/08/26/shell%20%E7%BC%96%E7%A8%8B/</id>
<published>2020-08-26T07:09:01.061Z</published>
<updated>2020-08-26T07:09:01.062Z</updated>
<content type="html"><![CDATA[<h4><span id="参数传递">参数传递</span></h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$#: 参数个数</span><br><span class="line">$1: 第一个参数,2,3,4类比</span><br><span class="line">$?: 显示最后命令的退出状态,0表示没有错误</span><br><span class="line">$$: 当前进程id</span><br><span class="line">$*: 显示所有接收到的参数,与 $@ 作用一样, 不同点 前者把多各参数合并成了一个,后者不会</span><br><span class="line"></span><br><span class="line">$!:显示最后一个后台进程id</span><br></pre></td></tr></table></figure><a id="more"></a><h4><span id="括号-的作用">括号()、(())、[]、[[]]、{} 的作用</span></h4><h5><span id="小括号圆括号">小括号,圆括号()</span></h5><ol><li>单小括号 ()</li></ol><ul><li>①命令组。括号中的命令将会新开一个子shell顺序执行,所以括号中的变量不能够被脚本余下的部分使用。括号中多个命令之间用分号隔开,最后一个命令可以没有分号,各命令和括号之间不必有空格。</li><li>②命令替换。等同于<code>cmd</code>,shell扫描一遍命令行,发现了$(cmd)结构,便将$(cmd)中的cmd执行一次,得到其标准输出,再将此输出放到原来命令。有些shell不支持,如tcsh。</li><li>③用于初始化数组。如:array=(a b c d)</li></ul><ol start="2"><li>双小括号 (( ))</li></ol><ul><li>①整数扩展。这种扩展计算是整数型的计算,不支持浮点型。((exp))结构扩展并计算一个算术表达式的值,如果表达式的结果为0,那么返回的退出状态码为1,或者 是”假”,而一个非零值的表达式所返回的退出状态码将为0,或者是”true”。若是逻辑判断,表达式exp为真则为1,假则为0。</li><li>②只要括号中的运算符、表达式符合C语言运算规则,都可用在$((exp))中,甚至是三目运算符。作不同进位(如二进制、八进制、十六进制)运算时,输出结果全都自动转化成了十进制。如:echo $((16#5f)) 结果为95 (16进位转十进制)</li><li>③单纯用 (( )) 也可重定义变量值,比如 a=5; ((a++)) 可将 $a 重定义为6</li><li>④常用于算术运算比较,双括号中的变量可以不使用$符号前缀。括号内支持多个表达式用逗号分开。 只要括号中的表达式符合C语言运算规则,比如可以直接使用for((i=0;i<5;i++)), 如果不使用双括号, 则为for i in <code>seq 0 4</code>或者for i in {0..4}。再如可以直接使用if (($i<5)), 如果不使用双括号, 则为if [ $i -lt 5 ]。<h5><span id="中括号方括号">中括号,方括号[]</span></h5></li></ul><ol><li>单中括号 []</li></ol><ul><li>①bash 的内部命令,[和test是等同的。如果我们不用绝对路径指明,通常我们用的都是bash自带的命令。if/test结构中的左中括号是调用test的命令标识,右中括号是关闭条件判断的。这个命令把它的参数作为比较表达式或者作为文件测试,并且根据比较的结果来返回一个退出状态码。if/test结构中并不是必须右中括号,但是新版的Bash中要求必须这样。</li><li>②Test和[]中可用的比较运算符只有==和!=,两者都是用于字符串比较的,不可用于整数比较,整数比较只能使用-eq,-gt这种形式。无论是字符串比较还是整数比较都不支持大于号小于号。如果实在想用,对于字符串比较可以使用转义形式,如果比较”ab”和”bc”:[ ab < bc ],结果为真,也就是返回状态为0。[ ]中的逻辑与和逻辑或使用-a 和-o 表示。</li><li>③字符范围。用作正则表达式的一部分,描述一个匹配的字符范围。作为test用途的中括号内不能使用正则。</li><li>④在一个array 结构的上下文中,中括号用来引用数组中每个元素的编号。</li></ul><ol start="2"><li>双中括号[[ ]]</li></ol><ul><li>①[[是 bash 程序语言的关键字。并不是一个命令,[[ ]] 结构比[ ]结构更加通用。在[[和]]之间所有的字符都不会发生文件名扩展或者单词分割,但是会发生参数扩展和命令替换。</li><li>②支持字符串的模式匹配,使用=~操作符时甚至支持shell的正则表达式。字符串比较时可以把右边的作为一个模式,而不仅仅是一个字符串,比如[[ hello == hell? ]],结果为真。[[ ]] 中匹配字符串或通配符,不需要引号。</li><li>③使用[[ … ]]条件判断结构,而不是[ … ],能够防止脚本中的许多逻辑错误。比如,&&、||、<和> 操作符能够正常存在于[[ ]]条件判断结构中,但是如果出现在[ ]结构中的话,会报错。比如可以直接使用if [[ $a != 1 && $a != 2 ]], 如果不适用双括号, 则为if [ $a -ne 1] && [ $a != 2 ]或者if [ $a -ne 1 -a $a != 2 ]。</li><li>④bash把双中括号中的表达式看作一个单独的元素,并返回一个退出状态码。<br>例子:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">if ($i<5) </span><br><span class="line">if [ $i -lt 5 ] </span><br><span class="line">if [ $a -ne 1 -a $a != 2 ] </span><br><span class="line">if [ $a -ne 1] && [ $a != 2 ] </span><br><span class="line">if [[ $a != 1 && $a != 2 ]] </span><br><span class="line"> </span><br><span class="line">for i in $(seq 0 4);do echo $i;done </span><br><span class="line">for i in `seq 0 4`;do echo $i;done </span><br><span class="line">for ((i=0;i<5;i++));do echo $i;done </span><br><span class="line">for i in {0..4};do echo $i;done</span><br></pre></td></tr></table></figure><h5><span id="大括号-花括号">大括号、花括号 {}</span></h5><ol><li>常规用法</li></ol><ul><li>①大括号拓展。(通配(globbing))将对大括号中的文件名做扩展。在大括号中,不允许有空白,除非这个空白被引用或转义。第一种:对大括号中的以逗号分割的文件列表进行拓展。如 touch {a,b}.txt 结果为a.txt b.txt。第二种:对大括号中以点点(..)分割的顺序文件列表起拓展作用,如:touch {a..d}.txt 结果为a.txt b.txt c.txt d.txt</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># ls {ex1,ex2}.sh </span><br><span class="line">ex1.sh ex2.sh </span><br><span class="line"># ls {ex{1..3},ex4}.sh </span><br><span class="line">ex1.sh ex2.sh ex3.sh ex4.sh </span><br><span class="line"># ls {ex[1-3],ex4}.sh </span><br><span class="line">ex1.sh ex2.sh ex3.sh ex4.sh</span><br></pre></td></tr></table></figure><ul><li>②代码块,又被称为内部组,这个结构事实上创建了一个匿名函数 。与小括号中的命令不同,大括号内的命令不会新开一个子shell运行,即脚本余下部分仍可使用括号内变量。括号内的命令间用分号隔开,最后一个也必须有分号。{}的第一个命令和左括号之间必须要有一个空格。</li></ul><ol start="2"><li>几种特殊的替换结构</li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">${var:-string},${var:+string},${var:=string},${var:?string}</span><br></pre></td></tr></table></figure><ul><li>①${var:-string}和${var:=string}:若变量var为空,则用在命令行中用string来替换${var:-string},否则变量var不为空时,则用变量var的值来替换${var:-string};对于${var:=string}的替换规则和${var:-string}是一样的,所不同之处是${var:=string}若var为空时,用string替换${var:=string}的同时,把string赋给变量var: ${var:=string}很常用的一种用法是,判断某个变量是否赋值,没有的话则给它赋上一个默认值。</li><li>② ${var:+string}的替换规则和上面的相反,即只有当var不是空的时候才替换成string,若var为空时则不替换或者说是替换成变量 var的值,即空值。(因为变量var此时为空,所以这两种说法是等价的) </li><li>③${var:?string}替换规则为:若变量var不为空,则用变量var的值来替换${var:?string};若变量var为空,则把string输出到标准错误中,并从脚本中退出。我们可利用此特性来检查是否设置了变量的值。</li><li>补充扩展:在上面这五种替换结构中string不一定是常值的,可用另外一个变量的值或是一种命令的输出。</li></ul><ol start="3"><li>四种模式匹配替换结构</li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">模式匹配记忆方法:</span><br><span class="line"># 是去掉左边(在键盘上#在$之左边)</span><br><span class="line">% 是去掉右边(在键盘上%在$之右边)</span><br><span class="line">#和%中的单一符号是最小匹配,两个相同符号是最大匹配。</span><br><span class="line"></span><br><span class="line">${var%pattern},${var%%pattern},${var#pattern},${var##pattern}</span><br></pre></td></tr></table></figure><ul><li>第一种模式:${variable%pattern},这种模式时,shell在variable中查找,看它是否一给的模式pattern结尾,如果是,就从命令行把variable中的内容去掉右边最短的匹配模式</li><li>第二种模式: ${variable%%pattern},这种模式时,shell在variable中查找,看它是否一给的模式pattern结尾,如果是,就从命令行把variable中的内容去掉右边最长的匹配模式</li><li>第三种模式:${variable#pattern} 这种模式时,shell在variable中查找,看它是否一给的模式pattern开始,如果是,就从命令行把variable中的内容去掉左边最短的匹配模式</li><li>第四种模式: ${variable##pattern} 这种模式时,shell在variable中查找,看它是否一给的模式pattern结尾,如果是,就从命令行把variable中的内容去掉右边最长的匹配模式</li><li>这四种模式中都不会改变variable的值,其中,只有在pattern中使用了<em>匹配符号时,%和%%,#和##才有区别。结构中的pattern支持通配符,</em>表示零个或多个任意字符,?表示仅与一个任意字符匹配,[…]表示匹配中括号里面的字符,[!…]表示不匹配中括号里面的字符。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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"># var=testcase </span><br><span class="line"># echo $var </span><br><span class="line">testcase </span><br><span class="line"># echo ${var%s*e} </span><br><span class="line">testca </span><br><span class="line"># echo $var </span><br><span class="line">testcase </span><br><span class="line"># echo ${var%%s*e} </span><br><span class="line">te</span><br><span class="line"># echo ${var#?e} </span><br><span class="line">stcase</span><br><span class="line"># echo ${var##?e} </span><br><span class="line">stcase</span><br><span class="line"># echo ${var##*e} </span><br><span class="line"> </span><br><span class="line"># echo ${var##*s} </span><br><span class="line">e </span><br><span class="line"># echo ${var##test} </span><br><span class="line">case</span><br></pre></td></tr></table></figure><ol start="4"><li>字符串提取和替换</li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">${var:num},${var:num1:num2},${var/pattern/pattern},${var//pattern/pattern}</span><br></pre></td></tr></table></figure><ul><li>第一种模式:${var:num},这种模式时,shell在var中提取第num个字符到末尾的所有字符。若num为正数,从左边0处开始;若num为负数,从右边开始提取字串,但必须使用在冒号后面加空格或一个数字或整个num加上括号,如${var: -2}、${var:1-3}或${var:(-2)}</li><li>第二种模式:${var:num1:num2},num1是位置,num2是长度。表示从$var字符串的第$num1个位置开始提取长度为$num2的子串。不能为负数。</li><li>第三种模式:${var/pattern/pattern}表示将var字符串的第一个匹配的pattern替换为另一个pattern</li><li>第四种模式:${var//pattern/pattern}表示将var字符串中的所有能匹配的pattern替换为另一个pattern。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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">[root@centos ~]# var=/home/centos</span><br><span class="line">[root@centos ~]# echo $var</span><br><span class="line">/home/centos</span><br><span class="line">[root@centos ~]# echo ${var:5}</span><br><span class="line">/centos</span><br><span class="line">[root@centos ~]# echo ${var: -6}</span><br><span class="line">centos</span><br><span class="line">[root@centos ~]# echo ${var:(-6)}</span><br><span class="line">centos</span><br><span class="line">[root@centos ~]# echo ${var:1:4}</span><br><span class="line">home</span><br><span class="line">[root@centos ~]# echo ${var/o/h}</span><br><span class="line">/hhme/centos</span><br><span class="line">[root@centos ~]# echo ${var//o/h}</span><br><span class="line">/hhme/cenths</span><br></pre></td></tr></table></figure><h5><span id="符号后的括号">符号$后的括号</span></h5><p>(1)${a} 变量a的值, 在不引起歧义的情况下可以省略大括号。</p><p>(2)$(cmd) 命令替换,和<code>cmd</code>效果相同,结果为shell命令cmd的输,过某些Shell版本不支持$()形式的命令替换, 如tcsh。</p><p>(3)$((expression)) 和<code>exprexpression</code>效果相同, 计算数学表达式exp的数值, 其中exp只要符合C语言的运算规则即可, 甚至三目运算符和逻辑表达式都可以计算。</p><h5><span id="多条命令执行">多条命令执行()、{}</span></h5><p>(1)单小括号,(cmd1;cmd2;cmd3) 新开一个子shell顺序执行命令cmd1,cmd2,cmd3, 各命令之间用分号隔开, 最后一个命令后可以没有分号。</p><p>(2)单大括号,{ cmd1;cmd2;cmd3;} 在当前shell顺序执行命令cmd1,cmd2,cmd3, 各命令之间用分号隔开, 最后一个命令后必须有分号, 第一条命令和左括号之间必须用空格隔开。<br>对{}和()而言, 括号中的重定向符只影响该条命令, 而括号外的重定向符影响到括号中的所有命令。</p><h4><span id="命令">命令</span></h4><h5><span id="管道命令">管道命令 |</span></h5><ul><li><p>管道会将前面命令的标准输出当作后面命令的标准输入。一般程序错误时是没有标准输出的,其错误信息会送到标准错误输出。</p></li><li><p>通常shell会启动前一个程序,并通过系统调用读取它的输出,再通过系统调用启动后一个程序,通过管道向后一个程序的标准输入进行输出</p></li><li><p>出错时如何处理应该是依照各shell实现而定。bash中是后续程序仍然执行,但不会获得任何输入。所以后一个程序会执行,且你应该能在屏幕上看到前一个程序的错误输出。</p></li><li><p>另外,一般整个管道的返回状态是最后一个命令的退出状态</p></li></ul><h5><span id="选取命令-cut-grep">选取命令 cut, grep</span></h5><h5><span id="排序命令-sort-wc-uniq">排序命令 sort, wc, uniq</span></h5><h5><span id="双重数据流-tee">双重数据流 tee</span></h5><h5><span id="字符转换命令-tr-expand-col-join-paste">字符转换命令 tr, expand, col, join, paste</span></h5><h5><span id="切割命令-split">切割命令 split</span></h5><h5><span id="参数代换-xargs">参数代换 xargs</span></h5><h5><span id="文本处理-sed-awk">文本处理 sed, awk</span></h5><ol><li>sed</li></ol><ul><li>文件每行首添加内容,例如:添加双引号”<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sed -i 's/^/"&/g' file_name</span><br></pre></td></tr></table></figure></li><li>文件每行尾添加内容,例如:添加双引号”<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sed -i 's/$/&"/g' file_name</span><br></pre></td></tr></table></figure></li></ul><ol start="2"><li>awk</li></ol><ul><li>去重</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">awk '!a[$0]++{print}'</span><br></pre></td></tr></table></figure><ol start="3"><li>字符串是否存在与数组中<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">if [[ ” ${array[@]} ” =~ ” ${value} ” ]]; then</span><br><span class="line"> # whatever you want to do when arr contains value</span><br><span class="line">fi</span><br><span class="line"></span><br><span class="line">if [[ ! ” ${array[@]} ” =~ ” ${value} ” ]]; then</span><br><span class="line"> # whatever you want to do when arr doesn’t contain value</span><br><span class="line">fi</span><br></pre></td></tr></table></figure></li><li>文件或者文件夹是否存在<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">if [ -d "/data/" ];then</span><br><span class="line">echo "文件夹存在"</span><br><span class="line">else</span><br><span class="line">echo "文件夹不存在"</span><br><span class="line">fi</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">if [ -f "/data/filename" ];then</span><br><span class="line">echo "文件存在"</span><br><span class="line">else</span><br><span class="line">echo "文件不存在"</span><br><span class="line">fi</span><br></pre></td></tr></table></figure></li><li>awk 切换文本为数组<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"> $ echo test.txt</span><br><span class="line"> hello</span><br><span class="line"> world</span><br><span class="line"> $ local array=`cat test.txt | awk -v RS='' '{gsub("\n"," ")}; print'`</span><br><span class="line">:wq</span><br></pre></td></tr></table></figure></li></ol>]]></content>
<summary type="html"><h4 id="参数传递"><a href="#参数传递" class="headerlink" title="参数传递"></a>参数传递</h4><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$#: 参数个数</span><br><span class="line">$1: 第一个参数,2,3,4类比</span><br><span class="line">$?: 显示最后命令的退出状态,0表示没有错误</span><br><span class="line">$$: 当前进程id</span><br><span class="line">$*: 显示所有接收到的参数,与 $@ 作用一样, 不同点 前者把多各参数合并成了一个,后者不会</span><br><span class="line"></span><br><span class="line">$!:显示最后一个后台进程id</span><br></pre></td></tr></table></figure></summary>
<category term="shell" scheme="http://yoursite.com/tags/shell/"/>
</entry>
<entry>
<title>Docker 容器网络模式简介</title>
<link href="http://yoursite.com/2020/08/26/Docker%20%E5%AE%B9%E5%99%A8%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%BC%8F%E7%AE%80%E4%BB%8B/"/>
<id>http://yoursite.com/2020/08/26/Docker%20%E5%AE%B9%E5%99%A8%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%BC%8F%E7%AE%80%E4%BB%8B/</id>
<published>2020-08-26T07:08:34.743Z</published>
<updated>2021-08-19T03:14:20.505Z</updated>
<content type="html"><![CDATA[<h2><span id="docker-容器网络模式简介">Docker 容器网络模式简介</span></h2><h4><span id="bridge-桥模式">Bridge 桥模式</span></h4><p> Docker 容器启动时默认的 网络模式,如果不使用–network 指定网络模式,那么docker会为该容器创建一个网桥,用于连接该容器网络和主机网卡设备.</p><p> 此模式会为每一个容器分配Network Namespace、设置IP等,并将一个主机上的Docker容器连接到一个虚拟网桥上。</p><p> 创建一个后台容器,并查看网卡信息 与 宿主机 的网桥 做对比; 明显看出 bridge 模式 下 docker 会 在docker0 网桥下创建一对 veth 设备 作为 容器与桥 的连接,270 –> 271</p><a id="more"></a><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><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> docker run -d --name test_bridge --network=bridge busybox tail -f /dev/null</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> bridge link show</span></span><br><span class="line">271: veth3d1d764 state UP @(null): <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master docker0 state forwarding priority 32 c</span><br><span class="line">ost 2 </span><br><span class="line"><span class="meta">#</span><span class="bash"> docker <span class="built_in">exec</span> -i test_bridge ip a</span></span><br><span class="line">1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1</span><br><span class="line"> link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00</span><br><span class="line"> inet 127.0.0.1/8 scope host lo</span><br><span class="line"> valid_lft forever preferred_lft forever</span><br><span class="line">270: eth0@if271: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue </span><br><span class="line"> link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff</span><br><span class="line"> inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0</span><br><span class="line"> valid_lft forever preferred_lft forever</span><br></pre></td></tr></table></figure><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><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></pre></td><td class="code"><pre><span class="line">+----------------------------------------------------------------+-----------------------------------------+-----------------------------------------+</span><br><span class="line">| Host | Container 1 | Container 2 |</span><br><span class="line">| | | |</span><br><span class="line">| +------------------------------------------------+ | +-------------------------+ | +-------------------------+ |</span><br><span class="line">| | Newwork Protocol Stack | | | Newwork Protocol Stack | | | Newwork Protocol Stack | |</span><br><span class="line">| +------------------------------------------------+ | +-------------------------+ | +-------------------------+ |</span><br><span class="line">| ↑ ↑ | ↑ | ↑ |</span><br><span class="line">|............|.............|.....................................|...................|.....................|....................|....................|</span><br><span class="line">| ↓ ↓ | ↓ | ↓ |</span><br><span class="line">| +------+ +--------+ | +-------+ | +-------+ |</span><br><span class="line">| |.3.101| | .9.1 | | | .9.2 | | | .9.3 | |</span><br><span class="line">| +------+ +--------+ +-------+ | +-------+ | +-------+ |</span><br><span class="line">| | eth0 | | docker0|<--->| veth | | | eth0 | | | eth0 | |</span><br><span class="line">| +------+ +--------+ +-------+ | +-------+ | +-------+ |</span><br><span class="line">| ↑ ↑ ↑ | ↑ | ↑ |</span><br><span class="line">| | | +-------------------------------------------+ | | |</span><br><span class="line">| | ↓ | | | |</span><br><span class="line">| | +-------+ | | | |</span><br><span class="line">| | | veth | | | | |</span><br><span class="line">| | +-------+ | | | |</span><br><span class="line">| | ↑ | | | |</span><br><span class="line">| | +-------------------------------------------------------------------------------|--------------------+ |</span><br><span class="line">| | | | |</span><br><span class="line">| | | | |</span><br><span class="line">| | | | |</span><br><span class="line">+------------|---------------------------------------------------+-----------------------------------------+-----------------------------------------+</span><br><span class="line"> ↓</span><br><span class="line"> Physical Network (192.168.3.0/24)</span><br></pre></td></tr></table></figure><h4><span id="host-模式">Host 模式</span></h4><p> 这个模式下,容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口<br> 创建一个容器,并查看ip, 信息与宿主机一致,二者公用同一个网络命名空间</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 -i --network=host busybox ip a</span><br></pre></td></tr></table></figure><h4><span id="none-模式">None 模式</span></h4><pre><code>这种模式下,Docker容器拥有自己的Network Namespace,但是,并不为Docker容器进行任何网络配置。也就是说,这个Docker容器没有网卡、IP、路由等信息。需要我们自己为Docker容器添加网卡、配置IP等。</code></pre><p> 创建一个容器,查看该容器的 Network Namespace, 05 是上面 bridge 的, default 是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><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">docker run -d --name test_none --network=none busybox tail -f /dev/null</span><br><span class="line">docker inspect test_none</span><br><span class="line">...</span><br><span class="line">"NetworkSettings": {</span><br><span class="line"> "Bridge": "",</span><br><span class="line"> "SandboxID": "03240dad926fe76937281421fd32703e5a7c9e0e828c6cccb18b43f5194dda1e",</span><br><span class="line"> "HairpinMode": false,</span><br><span class="line"> "LinkLocalIPv6Address": "",</span><br><span class="line"> "LinkLocalIPv6PrefixLen": 0,</span><br><span class="line"> "Ports": {},</span><br><span class="line"> "SandboxKey": "/var/run/docker/netns/03240dad926f",</span><br><span class="line">...</span><br><span class="line">ls /var/run/docker/netns</span><br><span class="line">03240dad926f 0573eb6fb3e6 default</span><br></pre></td></tr></table></figure><h4><span id="container-模式">Container 模式</span></h4><p> 这个模式下,指定新创建的容器和已经存在的一个容器共享一个Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的IP,而是和一个指定的容器共享IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过lo网卡设备通信。</p><pre><code>使用test_bridge 容器网络,创建一个新容器,并比较二者网络信息,二者公用一块虚拟网卡都是 271</code></pre><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><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">#</span><span class="bash"> docker run -d --name test_none --network=container:test_bridge busybox tail -f /dev/null</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> docker <span class="built_in">exec</span> -i test_bridge ip a</span></span><br><span class="line">1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1 </span><br><span class="line"> link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 </span><br><span class="line"> inet 127.0.0.1/8 scope host lo </span><br><span class="line"> valid_lft forever preferred_lft forever </span><br><span class="line">270: eth0@if271: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue </span><br><span class="line"> link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff </span><br><span class="line"> inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0 </span><br><span class="line"> valid_lft forever preferred_lft forever</span><br><span class="line"><span class="meta">#</span><span class="bash"> docker <span class="built_in">exec</span> -i test_container ip a</span></span><br><span class="line">1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1 </span><br><span class="line"> link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 </span><br><span class="line"> inet 127.0.0.1/8 scope host lo </span><br><span class="line"> valid_lft forever preferred_lft forever </span><br><span class="line">270: eth0@if271: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue </span><br><span class="line"> link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff </span><br><span class="line"> inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0 </span><br><span class="line"> valid_lft forever preferred_lft forever</span><br></pre></td></tr></table></figure><h4><span id="user-自定义">User 自定义</span></h4><p> 该模式下,使用docker network 命令 创建自定义的网络,处于该网络下的docker可以通过 container名称进行通信,这里不能在使用busybox作为测试imgae需要使用完整的linux系统</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><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">#</span><span class="bash"> 创建一个isolated_nw 的孤立网络,并配置子网信息</span></span><br><span class="line">docker network create -d bridge --subnet 172.25.0.0/16 isolated_nw </span><br><span class="line"><span class="meta">#</span><span class="bash"> 创建一个容器,使用isolated_nw 网络,并指定ip地址</span></span><br><span class="line">docker run --network=isolated_nw --ip=172.25.3.3 -itd --name=test_user1 linkage_img</span><br><span class="line"><span class="meta">#</span><span class="bash"> 创建一个容器,并连接到isolated_nw</span></span><br><span class="line">docker run -itd --name test_user2 --network=isolated_nw linkage_img</span><br><span class="line">docker network connect isolated_nw test_user2</span><br><span class="line"><span class="meta">#</span><span class="bash"> 测试使用 docker name 按名字通信</span></span><br><span class="line">docker exec -i test_user2 ping -w 4 test_user1</span><br><span class="line">PING test_user1 (172.25.3.3): 56 data bytes</span><br><span class="line">64 bytes from 172.25.3.3: seq=0 ttl=64 time=0.070 ms</span><br><span class="line">64 bytes from 172.25.3.3: seq=1 ttl=64 time=0.080 ms</span><br><span class="line">64 bytes from 172.25.3.3: seq=2 ttl=64 time=0.080 ms</span><br><span class="line">64 bytes from 172.25.3.3: seq=3 ttl=64 time=0.097 ms</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><h2 id="Docker-容器网络模式简介"><a href="#Docker-容器网络模式简介" class="headerlink" title="Docker 容器网络模式简介"></a>Docker 容器网络模式简介</h2><h4 id="Bridge-桥模式"><a href="#Bridge-桥模式" class="headerlink" title="Bridge 桥模式"></a>Bridge 桥模式</h4><p> Docker 容器启动时默认的 网络模式,如果不使用–network 指定网络模式,那么docker会为该容器创建一个网桥,用于连接该容器网络和主机网卡设备.</p>
<p> 此模式会为每一个容器分配Network Namespace、设置IP等,并将一个主机上的Docker容器连接到一个虚拟网桥上。</p>
<p> 创建一个后台容器,并查看网卡信息 与 宿主机 的网桥 做对比; 明显看出 bridge 模式 下 docker 会 在docker0 网桥下创建一对 veth 设备 作为 容器与桥 的连接,270 –&gt; 271</p></summary>
<category term="docker" scheme="http://yoursite.com/tags/docker/"/>
<category term="网络" scheme="http://yoursite.com/tags/%E7%BD%91%E7%BB%9C/"/>
</entry>
<entry>
<title>祝君安康</title>
<link href="http://yoursite.com/2020/08/26/hello-world/"/>
<id>http://yoursite.com/2020/08/26/hello-world/</id>
<published>2020-08-26T06:59:08.822Z</published>
<updated>2020-08-26T06:59:08.822Z</updated>
<content type="html"><![CDATA[<p>Welcome to Mys!</p>]]></content>
<summary type="html"><p>Welcome to Mys!</p>
</summary>
</entry>
<entry>
<title>python</title>
<link href="http://yoursite.com/2020/07/08/python/"/>
<id>http://yoursite.com/2020/07/08/python/</id>
<published>2020-07-08T03:53:42.042Z</published>
<updated>2021-08-18T07:13:59.647Z</updated>
<content type="html"><![CDATA[<p>[TOC]</p><h3><span id="新旧类问题"><strong>新旧类问题</strong></span></h3><p> python2.2后引入了新式类,即显示继承自object的类,为了兼容旧式(经典)类,不显示继承的默认次采用旧式类方式;</p><p> python3.x 中已经完全去除经典类模式,默认全部采用新式类方式,无论是否显示继承自object。</p><a id="more"></a><h4><span id="旧式类">旧式类</span></h4><ul><li><p>不满足单调性</p><p> 如果类C,查找某个属性时,A排在B的前面,那么C的所有子类也要保证这个顺序</p></li><li><p>使用深度优先搜索方法调用顺序(MRO method resolution order)</p></li><li><p>不满足本地调用优先级(即根据继承顺序广度依次搜索)</p><p> 如果C(A,B), 本地优先级就是需要按照声明顺序,先找A,再找B</p></li></ul><h4><span id="新式类">新式类</span></h4><ul><li><p> 采用C3算法,C3原本是为Lisp写的</p><p>C3引入了MRO概念,python中任意对象都可以查看起mro,例如:str.mro()</p><ol><li>判断mro要先确定一个线性序列,然后查找路径由由序列中类的顺序决定。所以C3算法就是生成一个线性序列。</li><li>如果继承至一个基类:<br>class B(A)<br>这时B的mro序列为[B,A]</li><li>如果继承至多个基类<br>class B(A1,A2,A3 …)<br>这时B的mro序列 mro(B) = [B] + merge(mro(A1), mro(A2), mro(A3) …, [A1,A2,A3])</li><li>merge操作就是C3算法的核心。<br>遍历执行merge操作的序列,如果一个序列的第一个元素,是其他序列中的第一个元素,或不在其他序列出现,则从所有执行merge操作序列中删除这个元素,合并到当前的mro中;<br>merge操作后的序列,继续执行merge操作,直到merge操作的序列为空;<br>如果merge操作的序列无法为空,则说明不合法</li></ol></li></ul><h3><span id="元类及其应用">元类及其应用</span></h3><p> “元类就是深度的魔法,99%的用户应该根本不必为此操心。如果你想搞清楚究竟是否需要用到元类,那么你就不需要它。那些实际用到元类的人都非常清楚地知道他们需要做什么,而且根本不需要解释为什么要用元类。” —— Python界的领袖 Tim Peters</p><p> 一个不错的文章</p><p> python 中万物皆可攀(对象),类class 也是对象,类是创建实例的时候申明的,而元类就是为了创建类而申明的,可以简单理解为类的类;</p><p> 在我们写下如下代码时,python 解释器会查找是否含有<code>__metaclass__</code>属性,如果有就使用改属性定义的函数或者元类去创建这个类,没有就会使用默认python内置的type元类去创建;</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">class SomeClass(object):</span><br><span class="line">pass</span><br></pre></td></tr></table></figure><p>那么元类是在什么时候,去调用了<code>__new__</code> 方法去创建这个类的呢,如下</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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"># 1.类由type创建,创建类时,type的__init__方法自动执行,类() 执行type的 __call__方法(类的__new__方法,类的__init__方法)</span><br><span class="line"></span><br><span class="line"># 2.对象由类创建,创建对象时,类的__init__方法自动执行,对象()执行类的 __call__ 方法</span><br><span class="line"></span><br><span class="line">In [8]: class mt(type):</span><br><span class="line"> ...: def __call__(cls): # 重写type的call方法,加入打印注释</span><br><span class="line"> ...: print 'mt.__call__'</span><br><span class="line"> ...: super(mt, cls).__call__()</span><br><span class="line"> ...:</span><br><span class="line"></span><br><span class="line">In [11]: class A(object): # 在这个阶段,就会调用元类的__new__方法和__init__方法,这里由于并未重写type的这两个方法,所以看不到打印信息</span><br><span class="line"> ...: __metaclass__ = mt</span><br><span class="line"> ...: def __init__(self):</span><br><span class="line"> ...: print 'A.__init__'</span><br><span class="line"> ...:</span><br><span class="line"></span><br><span class="line">In [12]: a=A() # 这里会调用元类的__call__方法,如果上面的mt.__call__ 不去调用type的__call__的话,我们会发现a是个NoneType,</span><br><span class="line">mt.__call__</span><br><span class="line">A.__init__</span><br></pre></td></tr></table></figure><p> 在python中,type就是造物主,万象皆是由type而来,比如:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">fun</span><span class="params">()</span>:</span></span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line">>> fun.__class__</span><br><span class="line">>> <type <span class="string">'function'</span>></span><br><span class="line">>> fun.__class__.__class__</span><br><span class="line">>> <type <span class="string">'type'</span>></span><br><span class="line"></span><br><span class="line">>> num = <span class="number">1</span></span><br><span class="line">>> s = <span class="string">'hello'</span></span><br><span class="line"></span><br><span class="line">>> num.__class__</span><br><span class="line">>> <type <span class="string">'int'</span>></span><br><span class="line">>> int.__class__</span><br><span class="line">>> <type <span class="string">'type'</span>></span><br><span class="line"></span><br><span class="line">>> s.__class__</span><br><span class="line">>> <type <span class="string">'str'</span>></span><br><span class="line">>> str.__class__</span><br><span class="line">>> <type <span class="string">'type'</span>></span><br></pre></td></tr></table></figure><p> 如下我们定义了一个<em>mymeta</em>方法,在创建类A的时候会使用我们自定义的方法去创建,当然自定义的方法最终也要返回使用type去创建,毕竟人家是官方的</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">mymeta</span><span class="params">(class_name, class_bases, class_attr)</span>:</span></span><br><span class="line"> <span class="string">'''</span></span><br><span class="line"><span class="string"> class_name 会保存类的名字 Foo</span></span><br><span class="line"><span class="string"> class_bases 会保存类的父类 object</span></span><br><span class="line"><span class="string"> class_attr 会以字典的方式保存所有的类属性</span></span><br><span class="line"><span class="string"> '''</span></span><br><span class="line"> <span class="comment"># do something you like</span></span><br><span class="line"> <span class="keyword">return</span> type(class_name, class_bases, class_attr)</span><br><span class="line"> </span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span><span class="params">(object)</span>:</span></span><br><span class="line"> __metaclass__ = mymeta</span><br></pre></td></tr></table></figure><h4><span id="单例模式">单例模式</span></h4><p> 几种方式实现单例模式 </p><p> 使用元类可以轻松实现单例模式,单例模式的好处:</p><ul><li>节约内存,所有实例话的变量应用的都是同一块内存</li><li>统一管理</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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">import threading</span><br><span class="line"></span><br><span class="line">class SingletonType(type):</span><br><span class="line"> _instance_lock = threading.Lock()</span><br><span class="line"> def __call__(cls, *args, **kwargs):</span><br><span class="line"> if not hasattr(cls, "_instance"):</span><br><span class="line"> with SingletonType._instance_lock:</span><br><span class="line"> if not hasattr(cls, "_instance"):</span><br><span class="line"> cls._instance = super(SingletonType,cls).__call__(*args, **kwargs)</span><br><span class="line"> return cls._instance</span><br><span class="line"></span><br><span class="line">class Foo(metaclass=SingletonType):</span><br><span class="line"> def __init__(self,name):</span><br><span class="line"> self.name = name</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">obj1 = Foo('name')</span><br><span class="line">obj2 = Foo('name')</span><br><span class="line">print(obj1,obj2)</span><br></pre></td></tr></table></figure><h3><span id="迭代器-生成器-可迭代对象">迭代器、生成器、可迭代对象</span></h3><p> 先说可迭代对象,任何可以用iter函数调用的对象,都是可以迭代的,换言之,这个对象里面实现了<code>__iter__</code>或者<code>__getitem__</code> 所有的容器都实现了至少其中一种方法</p><p> 迭代器,实现了next方法的可迭代对象都是迭代器</p><p> 生成器,实现了next方法外,又实现了send方法,return next yielded value or raise StopIteration</p><p> 故而,生成器是一种特殊的迭代器,可以控制</p><h3><span id="进程间通信">进程间通信</span></h3><h4><span id="单工-半双工-全双工">单工、半双工、全双工</span></h4><p> 说到通信,无论什么方式的通信,无非分为这三种:单工、半双工、全双工</p><ol><li><p>单工</p><p> 单工,指在通信全程内线路的端点只能为接受方或发送方,信号只能从单一方向传输,例如:电视,广播</p></li><li><p>半双工</p><pre><code>半双工,指在通信的某一时刻,线路中只有一个信号流,从发送方流向接受方,但是双方可以角色互换,可以互相发,但是不能同时发,比如:对讲机</code></pre></li><li><p>全双工</p><p> 全双工,指线路中任意时刻的信号流都是双向的,双方互为接受方和发送方,比如:电话</p></li></ol><h4><span id="管道">管道</span></h4><p> 最早的UNIX中的通信方式,如下PIPE 使用python实现,管道是半双工的,两端在开启时既可以是读端,也可以是写端</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">import os, sys</span><br><span class="line"></span><br><span class="line">print("The child will write text to a pipe and ")</span><br><span class="line">print("the parent will read the text written by child...")</span><br><span class="line"></span><br><span class="line"># file descriptors r, w for reading and writing</span><br><span class="line">r, w = os.pipe()</span><br><span class="line"></span><br><span class="line">processid = os.fork() #fork 方法仅能在linux系统上运行,跨平台就用multiprocess</span><br><span class="line"></span><br><span class="line">print(processid)</span><br><span class="line"></span><br><span class="line">if processid:</span><br><span class="line"> # This is the paunameent process</span><br><span class="line"> # Closes file descriptor w</span><br><span class="line"> os.close(w) #这里必须关闭写fd,因为r端只有收到EOF表识才会返回,如果自己都不关闭,那么即便是子进程写完后退出,关闭了自己的w,那么主进程这里因为自己没关w,所以就会卡死在read</span><br><span class="line"> r = os.fdopen(r)</span><br><span class="line"> print("Parent reading")</span><br><span class="line"> str = r.read()</span><br><span class="line"> print("text =", str)</span><br><span class="line"> sys.exit(0)</span><br><span class="line">else:</span><br><span class="line"> # This is the child process</span><br><span class="line"> os.close(r) #关闭读端</span><br><span class="line"> w = os.fdopen(w, 'w')</span><br><span class="line"> print("Child writing")</span><br><span class="line"> w.write("Text written by child...")</span><br><span class="line"> w.close()</span><br><span class="line"> print("Child closing")</span><br><span class="line"> sys.exit(0)</span><br></pre></td></tr></table></figure><pre><code>管道只能用与亲缘关系进程间通信,不能用于没有关系的两个进程通信;</code></pre><p> 数据无格式;</p><p> <1>当写端存在时,管道中没有数据时,读取管道时将阻塞</p><p> <2>当读端请求读取的数据大于管道中的数据时,此时读取管道中实际大小的数据</p><p> <3>当读端请求读取的数据小于管道中的数据时,此时放回请求读取的大小数据</p><p> <4>当写端不存在,读取管道将返回0</p><p> <5>当写端存在多个,读取管道将阻塞</p><p> <6>当写入数据超过限制(一般是系统一个页大小),写入将阻塞</p><h4><span id="fifo">FIFO</span></h4><p> 先进先出,有名管道,解决了pipe只能在亲缘进程间通信的问题, c实现方式</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p></p>]]></content>
<summary type="html"><p>[TOC]</p>
<h3 id="新旧类问题"><a href="#新旧类问题" class="headerlink" title="新旧类问题"></a><strong>新旧类问题</strong></h3><p> python2.2后引入了新式类,即显示继承自object的类,为了兼容旧式(经典)类,不显示继承的默认次采用旧式类方式;</p>
<p> python3.x 中已经完全去除经典类模式,默认全部采用新式类方式,无论是否显示继承自object。</p></summary>
<category term="Python" scheme="http://yoursite.com/tags/Python/"/>
</entry>
</feed>