-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
258 lines (137 loc) · 161 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>张暮冉的博客</title>
<link href="https://zhangmuran.github.io/atom.xml" rel="self"/>
<link href="https://zhangmuran.github.io/"/>
<updated>2023-08-19T10:37:11.052Z</updated>
<id>https://zhangmuran.github.io/</id>
<author>
<name>张暮冉</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title></title>
<link href="https://zhangmuran.github.io/posts/0.html"/>
<id>https://zhangmuran.github.io/posts/0.html</id>
<published>2023-07-24T15:12:29.660Z</published>
<updated>2023-08-19T10:37:11.052Z</updated>
<content type="html"><![CDATA[<h1 id="基本用法">基本用法</h1><p>lambda表达式是C++11最重要也是最常用的特性之一,这是现代编程语言的一个特点,lambda表达式有如下的一些优点:</p><ul><li>声明式的编程风格:就地匿名定义目标函数或函数对象,不需要额外写一个命名函数或函数对象。</li><li>简洁:避免了代码膨胀和功能分散,让开发更加高效。</li><li>在需要的时间和地点实现功能闭包,使程序更加灵活。</li></ul><p>lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。lambda表达式的语法形式简单归纳如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[capture](params) opt -> ret {body;};</span><br></pre></td></tr></table></figure><p>其中capture是捕获列表,params是参数列表,opt是函数选项,ret是返回值类型,body是函数体。</p><ol><li><p>捕获列表 <code>[]</code> : 捕获一定范围内的变量</p></li><li><p>参数列表 <code>()</code> : 和普通函数的参数列表一样,如果没有参数参数列表可以省略不写。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> f = [](){<span class="keyword">return</span> <span class="number">1</span>;}<span class="comment">// 没有参数, 参数列表为空</span></span><br><span class="line"><span class="keyword">auto</span> f = []{<span class="keyword">return</span> <span class="number">1</span>;}<span class="comment">// 没有参数, 参数列表省略不写</span></span><br></pre></td></tr></table></figure></li><li><p><code>opt</code> 选项, 不需要可以省略</p><ul><li>mutable: 可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)</li><li>exception: 指定函数抛出的异常,如抛出整数类型的异常,可以使用throw();</li></ul></li><li><p>返回值类型:在C++11中,lambda表达式的返回值是通过返回值后置语法来定义的。</p></li><li><p>函数体:函数的实现,这部分不能省略,但函数体可以为空。</p></li></ol><h1 id="捕获列表">捕获列表</h1>]]></content>
<summary type="html"><h1 id="基本用法">基本用法</h1>
<p>lambda表达式是C++11最重要也是最常用的特性之一,这是现代编程语言的一个特点,lambda表达式有如下的一些优点:</p>
<ul>
<li>声明式的编程风格:就地匿名定义目标函数或函数对象,不需要额外写一个命名函数或</summary>
</entry>
<entry>
<title>MySQL锁相关</title>
<link href="https://zhangmuran.github.io/posts/b1104ef7.html"/>
<id>https://zhangmuran.github.io/posts/b1104ef7.html</id>
<published>2023-07-16T13:12:01.881Z</published>
<updated>2023-07-16T13:49:39.126Z</updated>
<content type="html"><![CDATA[<p>在 MySQL 里,根据加锁的范围,可以分为<strong>全局锁、表级锁和行锁</strong>三类。</p><h1 id="全局锁">全局锁</h1><blockquote><p>全局锁是怎么用的?</p></blockquote><p>要使用全局锁,则要执行这条命令:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flush tables <span class="keyword">with</span> read lock</span><br></pre></td></tr></table></figure><p>执行后,<strong>整个数据库就处于只读状态了</strong>,这时其他线程执行以下操作,都会被阻塞:</p><ul><li>对数据的增删改操作,比如 insert、delete、update等语句;</li><li>对表结构的更改操作,比如 alter table、drop table 等语句。</li></ul><p>如果要释放全局锁,则要执行这条命令:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">unlock tables</span><br></pre></td></tr></table></figure><p>当然,当会话断开了,全局锁会被自动释放。</p><blockquote><p>全局锁应用场景是什么?</p></blockquote><p>全局锁主要应用于做<strong>全库逻辑备份</strong>,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。</p><p>如果在全库逻辑备份期间,有用户购买了一件商品,一般购买商品的业务逻辑是会涉及到多张数据库表的更新,比如在用户表更新该用户的余额,然后在商品表更新被购买的商品的库存。</p><p>那么,有可能出现这样的顺序:</p><ol><li>先备份了用户表的数据;</li><li>然后有用户发起了购买商品的操作;</li><li>接着再备份商品表的数据。</li></ol><p>也就是在备份用户表和商品表之间,有用户购买了商品。</p><p>这种情况下,备份的结果是用户表中该用户的余额并没有扣除,反而商品表中该商品的库存被减少了,如果后面用这个备份文件恢复数据库数据的话,用户钱没少,而库存少了,等于用户白嫖了一件商品。</p><p>所以,在全库逻辑备份期间,加上全局锁,就不会出现上面这种情况了。</p><blockquote><p>加全局锁又会带来什么缺点呢?</p></blockquote><p>加上全局锁,意味着整个数据库都是只读状态。</p><p>那么如果数据库里有很多数据,备份就会花费很多的时间,关键是备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。</p><blockquote><p>既然备份数据库数据的时候,使用全局锁会影响业务,那有什么其他方式可以避免?</p></blockquote><p>有的,如果数据库的引擎支持的事务支持<strong>可重复读的隔离级别</strong>,那么在备份数据库之前先开启事务,会先创建 Read View,然后整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。</p><p>因为在可重复读的隔离级别下,即使其他事务更新了表的数据,也不会影响备份数据库时的 Read View,这就是事务四大特性中的隔离性,这样备份期间备份的数据一直是在开启事务时的数据。</p><p>备份数据库的工具是 mysqldump,在使用 mysqldump 时加上 <code>–single-transaction</code> 参数的时候,就会在备份数据库之前先开启事务。这种方法只适用于支持「可重复读隔离级别的事务」的存储引擎。</p><p>InnoDB 存储引擎默认的事务隔离级别正是可重复读,因此可以采用这种方式来备份数据库。</p><p>但是,对于 MyISAM 这种不支持事务的引擎,在备份数据库时就要使用全局锁的方法。</p><h1 id="表级锁">表级锁</h1><p>MySQL 里面表级别的锁有这几种:</p><ul><li>表锁</li><li>元数据锁(MDL)</li><li>意向锁</li><li>AUTO-INC 锁</li></ul><h2 id="表锁">表锁</h2><p>如果我们想对学生表(t_student)加表锁,可以使用下面的命令:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="operator">/</span><span class="operator">/</span>表级别的共享锁,也就是读锁;</span><br><span class="line">lock tables t_student read;</span><br><span class="line"></span><br><span class="line"><span class="operator">/</span><span class="operator">/</span>表级别的独占锁,也就是写锁;</span><br><span class="line">lock tables t_stuent write;</span><br></pre></td></tr></table></figure><p>要释放表锁,可以使用下面这条命令,会释放当前会话的所有表锁:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">unlock tables</span><br></pre></td></tr></table></figure><p>另外,当会话退出后,也会释放所有表锁。</p><p>不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能,<strong>InnoDB 牛逼的地方在于实现了颗粒度更细的行级锁</strong>。</p><h2 id="元数据锁">元数据锁</h2>]]></content>
<summary type="html">全局锁、表级锁、行级锁</summary>
<category term="数据库" scheme="https://zhangmuran.github.io/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
<category term="MySQL" scheme="https://zhangmuran.github.io/tags/MySQL/"/>
</entry>
<entry>
<title>MySQL索引</title>
<link href="https://zhangmuran.github.io/posts/43a71ae4.html"/>
<id>https://zhangmuran.github.io/posts/43a71ae4.html</id>
<published>2023-07-16T13:11:09.796Z</published>
<updated>2023-07-16T13:49:39.132Z</updated>
<content type="html"><![CDATA[<p>数据库中,索引的定义就是帮助存储引擎快速获取数据的一种数据结构,形象的说就是<strong>索引是数据的目录</strong>。</p><h1 id="索引的分类">索引的分类</h1><p>我们可以按照四个角度来分类索引。</p><ul><li>按「数据结构」分类:<strong>B+tree索引、Hash索引、Full-text索引</strong>。</li><li>按「物理存储」分类:<strong>聚簇索引(主键索引)、二级索引(辅助索引)</strong>。</li><li>按「字段特性」分类:<strong>主键索引、唯一索引、普通索引、前缀索引</strong>。</li><li>按「字段个数」分类:<strong>单列索引、联合索引</strong>。</li></ul><h2 id="按数据结构分类">按数据结构分类</h2><p>从数据结构的角度来看,MySQL 常见索引有 B+Tree 索引、HASH 索引、Full-Text 索引。</p><img src="https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E7%B4%A2%E5%BC%95/%E7%B4%A2%E5%BC%95%E5%88%86%E7%B1%BB.drawio.png" alt="img" style="zoom:70%;"><p>InnoDB 是在 MySQL 5.5 之后成为默认的 MySQL 存储引擎,B+Tree 索引类型也是 MySQL 存储引擎采用最多的索引类型。</p><p>在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引:</p><ul><li>如果有主键,默认会使用主键作为聚簇索引的索引键(key)</li><li>如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键(key)</li><li>在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键(key)</li></ul><p>其它索引都属于辅助索引(Secondary Index),也被称为二级索引或非聚簇索引。<strong>创建的主键索引和二级索引默认使用的是 B+Tree 索引</strong>。</p><h2 id="按物理存储分类">按物理存储分类</h2><p>从物理存储的角度来看,索引分为聚簇索引(主键索引)、二级索引(辅助索引)。</p><p>这两个区别在前面也提到了:</p><ul><li>主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;</li><li>二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。</li></ul><p>所以,在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。</p><h2 id="按字段特性分类">按字段特性分类</h2><p>从字段特性的角度来看,索引分为主键索引、唯一索引、普通索引、前缀索引。</p><ul><li><p>主键索引</p><p>主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。</p></li><li><p>唯一索引</p><p>唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。</p></li><li><p>普通索引</p><p>普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。</p></li><li><p>前缀索引</p><p>前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。</p></li></ul><h2 id="按字段个数分类">按字段个数分类</h2><p>从字段个数的角度来看,索引分为单列索引、联合索引(复合索引)。</p><ul><li>建立在单列上的索引称为单列索引,比如主键索引</li><li>建立在多列上的索引称为联合索引</li></ul><p>使用联合索引时,存在<strong>最左匹配原则</strong>,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。</p><p>比如,如果创建了一个 <code>(a, b, c)</code> 联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:</p><ul><li>where a=1</li><li>where a=1 and b=2 and c=3</li><li>where a=1 and b=2</li></ul><p>但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:</p><ul><li>where b=2</li><li>where c=3</li><li>where b=2 and c=3</li></ul><h1 id="索引优劣">索引优劣</h1><p>索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:</p><ul><li>需要占用物理空间,数量越大,占用空间越大;</li><li>创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增大;</li><li>会降低表的增删改的效率,因为每次增删改索引,B+ 树为了维护索引有序性,都需要进行动态维护。</li></ul><blockquote><p>什么时候适合用索引</p></blockquote><ul><li>字段有唯一性限制的,比如商品编码;</li><li>经常用于 <code>WHERE</code> 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。</li><li>经常用于 <code>GROUP BY</code> 和 <code>ORDER BY</code> 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。</li></ul><blockquote><p>什么时候不适合用索引</p></blockquote><ul><li><code>WHERE</code> 条件,<code>GROUP BY</code>,<code>ORDER BY</code> 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。</li><li>字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。</li><li>表数据太少的时候,不需要创建索引;</li><li>经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。</li></ul><h1 id="索引优化方法">索引优化方法</h1><h2 id="前缀索引优化">前缀索引优化</h2><p>前缀索引顾名思义就是使用某个字段中字符串的前几个字符建立索引,那我们为什么需要使用前缀来建立索引呢?</p><p>使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。</p><p>不过,前缀索引有一定的局限性,例如:</p><ul><li>order by 就无法使用前缀索引</li><li>无法把前缀索引用作覆盖索引</li></ul><h2 id="覆盖索引优化">覆盖索引优化</h2><p>覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。</p><p>假设我们只需要查询商品的名称、价格,有什么方式可以避免回表呢?</p><p>我们可以建立一个联合索引,即「商品ID、名称、价格」作为一个联合索引。如果索引中存在这些数据,查询将不会再次检索主键索引,从而避免回表。</p><p>所以,使用覆盖索引的好处就是,不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作。</p><h2 id="主键索引最好是自增的">主键索引最好是自增的</h2><p>我们在建表的时候,都会默认将主键索引设置为自增的,具体为什么要这样做呢?又什么好处?</p><p>InnoDB 创建主键索引默认为聚簇索引,数据被存放在了 B+Tree 的叶子节点上。也就是说,同一个叶子节点内的各个数据是按主键顺序存放的,因此,每当有一条新的数据插入时,数据库会根据主键将其插入到对应的叶子节点中。</p><p><strong>如果我们使用自增主键</strong>,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次<strong>插入一条新记录,都是追加操作,不需要重新移动数据</strong>,因此这种插入数据的方法效率非常高。</p><p><strong>如果我们使用非自增主键</strong>,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为<strong>页分裂</strong>。<strong>页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率</strong>。</p><p>另外,主键字段的长度不要太大,因为<strong>主键字段长度越小,意味着二级索引的叶子节点越小(二级索引的叶子节点存放的数据是主键值),这样二级索引占用的空间也就越小</strong>。</p><h2 id="索引最好设置为-not-null">索引最好设置为 NOT NULL</h2><p>为了更好的利用索引,索引列要设置为 NOT NULL 约束。有两个原因:</p><ul><li>第一原因:索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化,因为可为 NULL 的列会使索引、索引统计和值比较都更复杂,比如进行索引统计时,count 会省略值为NULL 的行。</li><li>第二个原因:NULL 值是一个没意义的值,但是它会占用物理空间,所以会带来的存储空间的问题,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式中<strong>至少会用 1 字节空间存储 NULL 值列表</strong></li></ul><h2 id="防止索引失效">防止索引失效</h2><p>用上了索引并不意味着查询的时候会使用到索引,所以我们心里要清楚有哪些情况会导致索引失效,从而避免写出索引失效的查询语句,否则这样的查询效率是很低的。</p><p>发生索引失效的情况:</p><ul><li>当我们使用左或者左右模糊匹配的时候,也就是 <code>like %xx</code> 或者 <code>like %xx%</code>这两种方式都会造成索引失效;</li><li>当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;</li><li>联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。</li><li>在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。</li></ul><p>实际过程中,可能会出现其他的索引失效场景,这时我们就需要查看执行计划,通过执行计划显示的数据判断查询语句是否使用了索引。</p><p>如下图,就是一个没有使用索引,并且是一个全表扫描的查询语句。</p><img src="https://cdn.xiaolincoding.com//mysql/other/798ab1331d1d6dff026e262e788f1a28.png" alt="img" style="zoom:80%;"><p>对于执行计划,参数有:</p><ul><li>possible_keys 字段表示可能用到的索引;</li><li>key 字段表示实际用的索引,如果这一项为 NULL,说明没有使用索引;</li><li>key_len 表示索引的长度;</li><li>rows 表示扫描的数据行数。</li><li>type 表示数据扫描类型,我们需要重点看这个。</li></ul><p>type 字段就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的<strong>执行效率从低到高的顺序为</strong>:</p><ul><li>All(全表扫描)</li><li>index(全索引扫描)</li><li>range(索引范围扫描)</li><li>ref(非唯一索引扫描)</li><li>eq_ref(唯一索引扫描)</li><li>const(结果只有一条的主键或唯一索引扫描)</li></ul><p>在这些情况里,all 是最坏的情况,因为采用了全表扫描的方式。index 和 all 差不多,只不过 index 对索引表进行全扫描,这样做的好处是不再需要对数据进行排序,但是开销依然很大。所以,要尽量避免全表扫描和全索引扫描。</p><p>range 表示采用了索引范围扫描,一般在 where 子句中使用 < 、>、in、between 等关键词,只检索给定范围的行,属于范围查找。<strong>从这一级别开始,索引的作用会越来越明显,因此我们需要尽量让 SQL 查询可以使用到 range 这一级别及以上的 type 访问方式</strong>。</p><p>ref 类型表示采用了非唯一索引,或者是唯一索引的非唯一性前缀,返回数据返回可能是多条。因为虽然使用了索引,但该索引列的值并不唯一,有重复。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描。但它的好处是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描。</p><p>eq_ref 类型是使用主键或唯一索引时产生的访问方式,通常使用在多表联查中。比如,对两张表进行联查,关联条件是两张表的 user_id 相等,且 user_id 是唯一索引,那么使用 EXPLAIN 进行执行计划查看的时候,type 就会显示 eq_ref。</p><p>const 类型表示使用了主键或者唯一索引与常量值进行比较,比如 select name from product where id=1。</p><p>需要说明的是 const 类型和 eq_ref 都使用了主键或唯一索引,不过这两个类型有所区别,<strong>const 是与常量进行比较,查询效率会更快,而 eq_ref 通常用于多表联查中</strong>。</p><blockquote><p>除了关注 type,我们也要关注 extra 显示的结果。</p></blockquote><p>这里说几个重要的参考指标:</p><ul><li>Using filesort :当查询语句中包含 group by 操作,而且无法利用索引完成排序操作的时候, 这时不得不选择相应的排序算法进行,甚至可能会通过文件排序,效率是很低的,所以要避免这种问题的出现。</li><li>Using temporary:使了用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表,常见于排序 order by 和分组查询 group by。效率低,要避免这种问题的出现。</li><li>Using index:所需数据只需在索引即可全部获得,不须要再到表中取数据,也就是使用了覆盖索引,避免了回表操作,效率不错。</li></ul>]]></content>
<summary type="html">MySQL索引相关知识</summary>
<category term="数据库" scheme="https://zhangmuran.github.io/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
<category term="MySQL" scheme="https://zhangmuran.github.io/tags/MySQL/"/>
</entry>
<entry>
<title>MySQL事务</title>
<link href="https://zhangmuran.github.io/posts/f91535f.html"/>
<id>https://zhangmuran.github.io/posts/f91535f.html</id>
<published>2023-07-16T13:10:16.706Z</published>
<updated>2023-07-16T13:49:39.138Z</updated>
<content type="html"><![CDATA[<h1 id="事务特性">事务特性</h1><ul><li><strong>原子性(Atomicity)</strong>:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样,就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。</li><li><strong>一致性(Consistency)</strong>:是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元)。</li><li><strong>隔离性(Isolation)</strong>:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。</li><li><strong>持久性(Durability)</strong>:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。</li></ul><blockquote><p>InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?</p></blockquote><ul><li>持久性是通过 redo log (重做日志)来保证的</li><li>原子性是通过 undo log(回滚日志) 来保证的</li><li>隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的</li><li>一致性则是通过持久性+原子性+隔离性来保证</li></ul><h1 id="并行事务会引发的问题">并行事务会引发的问题</h1><p><strong>在同时处理多个事务的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题</strong>。</p><h2 id="脏读">脏读</h2><p><strong>如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。</strong></p><img src="https://s3.bmp.ovh/imgs/2023/07/15/5d6eb51176dca45f.webp" style="zoom:50%;"><h2 id="不可重复读">不可重复读</h2><p><strong>在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。</strong></p><img src="https://s3.bmp.ovh/imgs/2023/07/15/2f254234b58f744a.webp" style="zoom:50%;"><h2 id="幻读">幻读</h2><p><strong>在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。</strong></p><img src="https://s3.bmp.ovh/imgs/2023/07/15/5de36914c5b4feee.webp" style="zoom:50%;"><h1 id="事务的隔离级别">事务的隔离级别</h1><p>前面我们提到,当多个事务并发执行时可能会遇到「脏读、不可重复读、幻读」的现象,这些现象会对事务的一致性产生不同程序的影响。</p><ul><li>脏读:读到其他事务未提交的数据;</li><li>不可重复读:前后读取的数据不一致;</li><li>幻读:前后读取的记录数量不一致。</li></ul><p>SQL 标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:</p><ul><li><strong>读未提交(read uncommitted)</strong>,指一个事务还没提交时,它做的变更就能被其他事务看到</li><li><strong>读提交(read committed)</strong>,指一个事务提交之后,它做的变更才能被其他事务看到</li><li><strong>可重复读(repeatable read)</strong>,指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,<strong>MySQL InnoDB 引擎的默认隔离级别</strong></li><li><strong>串行化(serializable )</strong>;会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;</li></ul><p>针对不同的隔离级别,并发事务时可能发生的现象也会不同。</p><ul><li>在「读未提交」隔离级别下,可能发生脏读、不可重复读和幻读现象;</li><li>在「读提交」隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象;</li><li>在「可重复读」隔离级别下,可能发生幻读现象,但是不可能脏读和不可重复读现象;</li><li>在「串行化」隔离级别下,脏读、不可重复读和幻读现象都不可能会发生。</li></ul><blockquote><p>四种隔离级别具体是如何实现的呢?</p></blockquote><ul><li>对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;</li><li>对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;</li><li>对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 <strong>Read View <strong>来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照。</strong>「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View</strong>。</li></ul><h1 id="read-view-在-mvcc-里如何工作的">Read View 在 MVCC 里如何工作的?</h1><p>Read View 有四个重要的字段:</p><ul><li>m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的<strong>事务 id 列表</strong>,注意是一个列表,<strong>“活跃事务”指的就是,启动了但还没提交的事务</strong>。</li><li>min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 <strong>id 最小的事务</strong>,也就是 m_ids 的最小值。</li><li>max_trx_id :这个并不是 m_ids 的最大值,而是<strong>创建 Read View 时当前数据库中应该给下一个事务的 id 值</strong>,也就是全局事务中最大的事务 id 值 + 1;</li><li>creator_trx_id :指的是<strong>创建该 Read View 的事务的事务 id</strong>。</li></ul><img src="https://s3.bmp.ovh/imgs/2023/07/15/86cdf00f711cb858.webp" style="zoom:67%;"><p>知道了 Read View 的字段,我们还需要了解聚簇索引记录中的两个隐藏列。</p><p>假设在账户余额表插入一条小林余额为 100 万的记录,然后我把这两个隐藏列也画出来,该记录的整个示意图如下:</p><img src="https://cdn.xiaolincoding.com//mysql/other/f595d13450878acd04affa82731f76c5.png" alt="图片" style="zoom:67%;"><p>对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:</p><ul><li>trx_id,当一个事务对某条聚簇索引记录进行改动时,就会<strong>把该事务的事务 id 记录在 trx_id 隐藏列里</strong>;</li><li>roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后<strong>这个隐藏列是个指针,指向每一个旧版本记录</strong>,于是就可以通过它找到修改前的记录。</li></ul><p>在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:</p><ul><li>如果记录的 trx_id 值小于 Read View 中的 <code>min_trx_id</code> 值,表示这个版本的记录是在创建 Read View <strong>前</strong>已经提交的事务生成的,所以该版本的记录对当前事务<strong>可见</strong>。</li><li>如果记录的 trx_id 值大于等于 Read View 中的 <code>max_trx_id</code> 值,表示这个版本的记录是在创建 Read View <strong>后</strong>才启动的事务生成的,所以该版本的记录对当前事务<strong>不可见</strong>。</li><li>如果记录的 trx_id 值在 Read View 的 <code>min_trx_id</code> 和 <code>max_trx_id</code> 之间,需要判断 trx_id 是否在 m_ids 列表中:<ul><li>如果记录的 trx_id <strong>在</strong> <code>m_ids</code> 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务<strong>不可见</strong>。</li><li>如果记录的 trx_id <strong>不在</strong> <code>m_ids</code>列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务<strong>可见</strong>。</li></ul></li></ul><p><strong>这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)</strong></p><h1 id="可重复读隔离级别与幻读">可重复读隔离级别与幻读</h1><p>在开启了可重复读隔离级别之后,MySQL 里除了普通查询是快照读,其他都是<strong>当前读</strong>,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。</p><p>这很好理解,假设你要 update 一个记录,另一个事务已经 delete 这条记录并且提交事务了,这样不是会产生冲突吗,所以 update 的时候肯定要知道最新的数据。另外,<code>select ... for update</code> 这种查询语句是当前读,每次执行的时候都是读取最新的数据。</p>]]></content>
<summary type="html">MySQL事务特性以及隔离级别</summary>
<category term="数据库" scheme="https://zhangmuran.github.io/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
<category term="MySQL" scheme="https://zhangmuran.github.io/tags/MySQL/"/>
</entry>
<entry>
<title>IP基础</title>
<link href="https://zhangmuran.github.io/posts/e13cb3bb.html"/>
<id>https://zhangmuran.github.io/posts/e13cb3bb.html</id>
<published>2023-07-16T13:09:06.270Z</published>
<updated>2023-07-16T13:49:39.135Z</updated>
<content type="html"><![CDATA[<p>IP 在 TCP/IP 参考模型中处于第三层,也就是<strong>网络层</strong>。</p><p>网络层的主要作用是:<strong>实现主机与主机之间的通信,也叫点对点(end to end)通信。</strong></p><img src="https://s3.bmp.ovh/imgs/2023/07/15/48858abe56740332.webp" style="zoom: 67%;"><blockquote><p>网络层与数据链路层有什么关系呢?</p></blockquote><p>IP 的作用是主机之间通信用的,而 <strong>MAC 的作用则是实现「直连」的两个设备之间通信,而 IP 则负责在「没有直连」的两个网络之间进行通信传输。</strong></p><h1 id="ip-地址">IP 地址</h1><p>在 TCP/IP 网络通信时,为了保证能正常通信,每个设备都需要配置正确的 IP 地址,否则无法实现正常的通信。</p><p>IP 地址(IPv4 地址)由 <code>32</code> 位正整数来表示,IP 地址在计算机是以二进制的方式处理的。</p><p>而人类为了方便记忆采用了<strong>点分十进制</strong>的标记方式,也就是将 32 位 IP 地址以每 8 位为组,共分为 <code>4</code> 组,每组以「<code>.</code>」隔开,再将每组转换成十进制。</p><p>IP 地址最大值也就是<br>$$<br>2^{32} = 4294967296<br>$$<br>也就说,最大允许 43 亿台计算机连接到网络。</p><p>实际上,IP 地址并不是根据主机台数来配置的,而是以网卡。像服务器、路由器等设备都是有 2 个以上的网卡,也就是它们会有 2 个以上的 IP 地址。</p><img src="https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/IP/6.jpg" alt="每块网卡可以分配一个以上的IP地址" style="zoom:50%;"><p>因此,让 43 亿台计算机全部连网其实是不可能的,更何况 IP 地址是由「网络标识」和「主机标识」这两个部分组成的,所以实际能够连接到网络的计算机个数更是少了很多。</p><h2 id="ip-地址的分类">IP 地址的分类</h2><p>互联网诞生之初,IP 地址显得很充裕,于是计算机科学家们设计了<strong>分类地址</strong>。IP 地址分类成了 5 种类型,分别是 A 类、B 类、C 类、D 类、E 类。</p><img src="https://s3.bmp.ovh/imgs/2023/07/15/e67c0559ed890c03.webp" style="zoom: 67%;"><blockquote><p>什么是 A、B、C 类地址?</p></blockquote><p>对于 A、B、C 类主要分为两个部分,分别是<strong>网络号和主机号</strong>。用下面这个表格, 就能很清楚的知道 A、B、C 分类对应的地址范围、最大主机个数。</p><table><thead><tr><th style="text-align:center">类别</th><th style="text-align:center">IP地址范围</th><th style="text-align:center">最大主机数</th></tr></thead><tbody><tr><td style="text-align:center">A</td><td style="text-align:center">0.0.0.0 ~ 127.255.255.255</td><td style="text-align:center">16777214</td></tr><tr><td style="text-align:center">B</td><td style="text-align:center">128.0.0.0 ~ 191.255.255.255</td><td style="text-align:center">65534</td></tr><tr><td style="text-align:center">C</td><td style="text-align:center">192.0.0.0 ~ 223.255.255.255</td><td style="text-align:center">254</td></tr></tbody></table><blockquote><p>A、B、C 分类地址最大主机个数是如何计算的呢?</p></blockquote><p>最大主机个数,就是要看主机号的位数,如 C 类地址的主机号占 8 位,那么 C 类地址的最大主机个数:<br>$$<br>2 ^ 8 - 2 = 254<br>$$<br>因为在 IP 地址中,有两个 IP 是特殊的,分别是主机号全为 1 和 全为 0 地址。</p><ul><li>主机号全为 1 指定某个网络下的所有主机,用于广播</li><li>主机号全为 0 指定某个网络</li></ul><p>因此,在分配过程中,应该去掉这两种情况。</p><blockquote><p>广播地址用于什么?</p></blockquote><p>广播地址用于在<strong>同一个链路中相互连接的主机之间发送数据包</strong>。</p><p>当主机号全为 1 时,就表示该网络的广播地址。例如把 <code>172.20.0.0/16</code> 用二进制表示如下:<br>$$<br>10101100.00010100.00000000.00000000<br>$$<br>将这个地址的<strong>主机部分全部改为 1</strong>,则形成广播地址:<br>$$<br>10101100.00010100.\textcolor{red}{11111111.11111111}<br>$$<br>再将这个地址用十进制表示,则为 <code>172.20.255.255</code></p><p>广播地址可以分为本地广播和直接广播两种。</p><ul><li><strong>在本网络内广播的叫做本地广播</strong>。例如网络地址为 192.168.0.0/24 的情况下,广播地址是 192.168.0.255 。因为这个广播地址的 IP 包会被路由器屏蔽,所以不会到达 192.168.0.0/24 以外的其他链路上。</li><li><strong>在不同网络之间的广播叫做直接广播</strong>。例如网络地址为 192.168.0.0/24 的主机向 192.168.1.255/24 的目标地址发送 IP 包。收到这个包的路由器,将数据转发给 192.168.1.0/24,从而使得所有 192.168.1.1~192.168.1.254 的主机都能收到这个包(由于直接广播有一定的安全问题,多数情况下会在路由器上设置为不转发。) 。</li></ul><blockquote><p>什么是 D、E 类地址?</p></blockquote><p>而 D 类和 E 类地址是没有主机号的,所以不可用于主机 IP,D 类常被用于<strong>多播</strong>,E 类是预留的分类,暂时未使用。</p><table><thead><tr><th style="text-align:center">类别</th><th style="text-align:center">IP地址范围</th><th style="text-align:center">用途</th></tr></thead><tbody><tr><td style="text-align:center">D</td><td style="text-align:center">224.0.0.0 ~ 239.255.255.255</td><td style="text-align:center">IP 多播</td></tr><tr><td style="text-align:center">E</td><td style="text-align:center">240.0.0.0 ~ 255.255.255.255</td><td style="text-align:center">预留使用</td></tr></tbody></table><blockquote><p>多播地址用于什么?</p></blockquote><p>多播用于<strong>将包发送给特定组内的所有主机。</strong></p><p>由于广播无法穿透路由,若想给其他网段发送同样的包,就可以使用可以穿透路由的多播。</p><img src="https://s3.bmp.ovh/imgs/2023/07/15/654ced8e7d02a0a8.webp" style="zoom:50%;"><p>多播使用的 D 类地址,其前四位是 <code>1110</code> 就表示是多播地址,而剩下的 28 位是多播的组编号。</p><p>从 224.0.0.0 ~ 239.255.255.255 都是多播的可用范围,其划分为以下三类:</p><ul><li>224.0.0.0 ~ 224.0.0.255 为预留的组播地址,只能在局域网中,路由器是不会进行转发的。</li><li>224.0.1.0 ~ 238.255.255.255 为用户可用的组播地址,可以用于 Internet 上。</li><li>239.0.0.0 ~ 239.255.255.255 为本地管理组播地址,可供内部网在内部使用,仅在特定的本地范围内有效。</li></ul><h2 id="无分类地址-cidr">无分类地址 CIDR</h2><p>普通 IP 分类存在许多缺点,有时我们希望地址划分的再详细一些,所以提出了无分类地址的方案,即 <code>CIDR</code>。这种方式不再有分类地址的概念,32 比特的 IP 地址被划分为两部分,前面是<strong>网络号</strong>,后面是<strong>主机号</strong>。</p><p>表示形式 <code>a.b.c.d/x</code>,其中 <code>/x</code> 表示前 x 位属于<strong>网络号</strong>, x 的范围是 <code>0 ~ 32</code>,这就使得 IP 地址更加具有灵活性。</p><p>比如 10.100.122.2/24,这种地址表示形式就是 CIDR,/24 表示前 24 位是网络号,剩余的 8 位是主机号。</p><img src="https://s3.bmp.ovh/imgs/2023/07/15/e2a858847fbab031.webp" style="zoom:50%;"><p>还有另一种划分网络号与主机号形式,那就是<strong>子网掩码</strong>,掩码的意思就是掩盖掉主机号,剩余的就是网络号。</p><p><strong>将子网掩码和 IP 地址按位计算 AND,就可得到网络号。</strong></p><blockquote><p>为什么要分离网络号和主机号?</p></blockquote><p>因为两台计算机要通讯,首先要判断是否处于同一个广播域内,即网络地址是否相同。如果网络地址相同,表明接受方在本网络上,那么可以把数据包直接发送到目标主机。</p><p>路由器寻址工作中,也就是通过这样的方式来找到对应的网络号的,进而把数据包转发给对应的网络内。</p><blockquote><p>怎么进行子网划分?</p></blockquote><p>在上面我们知道可以通过子网掩码划分出网络号和主机号,那实际上子网掩码还有一个作用,那就是<strong>划分子网</strong>。</p><p><strong>子网划分实际上是将主机地址分为两个部分:子网网络地址和子网主机地址</strong>。</p><ul><li>未做子网划分的 ip 地址:网络地址+主机地址</li><li>做子网划分后的 ip 地址:网络地址+(子网网络地址+子网主机地址)</li></ul><p>假设对 C 类地址进行子网划分,网络地址 192.168.1.0,使用子网掩码 255.255.255.192 对其进行子网划分。C 类地址中前 24 位是网络号,最后 8 位是主机号,根据子网掩码可知<strong>从 8 位主机号中借用 2 位作为子网号</strong>。</p><p>由于子网网络地址被划分成 2 位,那么子网地址就有 4 个,分别是 00、01、10、11</p><img src="https://s3.bmp.ovh/imgs/2023/07/15/5445ad2fea376e13.webp" style="zoom:40%;"><p>最后划分好的四个子网的信息</p><table><thead><tr><th style="text-align:center">子网号</th><th style="text-align:center">网络地址</th><th style="text-align:center">主机地址范围</th><th style="text-align:center">广播地址</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:center">192.168.1.0</td><td style="text-align:center">192.168.1.1 ~ 192.168.1.62</td><td style="text-align:center">192.168.1.63</td></tr><tr><td style="text-align:center">1</td><td style="text-align:center">192.168.1.64</td><td style="text-align:center">192.168.1.65 ~ 192.168.1.126</td><td style="text-align:center">192.168.1.127</td></tr><tr><td style="text-align:center">2</td><td style="text-align:center">192.168.1.128</td><td style="text-align:center">192.168.1.129 ~ 192.168.1.190</td><td style="text-align:center">192.168.1.191</td></tr><tr><td style="text-align:center">3</td><td style="text-align:center">192.168.1.192</td><td style="text-align:center">192.168.1.193 ~ 192.168.1.254</td><td style="text-align:center">192.168.1.255</td></tr></tbody></table><h2 id="公有-ip-地址与私有-ip-地址">公有 IP 地址与私有 IP 地址</h2><p>在 A、B、C 分类地址,实际上有分公有 IP 地址和私有 IP 地址。</p><table><thead><tr><th style="text-align:center">类别</th><th style="text-align:center">IP 地址范围</th><th style="text-align:center">最大主机数</th><th style="text-align:center">私有 IP 地址范围</th></tr></thead><tbody><tr><td style="text-align:center">A</td><td style="text-align:center">0.0.0.0 ~ 127.255.255.255</td><td style="text-align:center">15777214</td><td style="text-align:center">10.0.0.0 ~ 10.255.255.255</td></tr><tr><td style="text-align:center">B</td><td style="text-align:center">128.0.0.0 ~ 191.255.255.255</td><td style="text-align:center">65534</td><td style="text-align:center">172.16.0.0 ~ 172.31.255.255</td></tr><tr><td style="text-align:center">C</td><td style="text-align:center">192.0.0.0 ~ 223.255.255.255</td><td style="text-align:center">254</td><td style="text-align:center">192.168.0.0 ~ 192.168.255.255</td></tr></tbody></table><p>平时我们办公室、家里、学校用的 IP 地址,一般都是私有 IP 地址。因为这些地址允许组织内部的 IT 人员自己管理、自己分配,而且可以重复。因此,你学校的某个私有 IP 地址和我学校的可以是一样的。</p><p>就像每个小区都有自己的楼编号和门牌号,你小区家可以叫 1 栋 101 号,我小区家也可以叫 1 栋 101,没有任何问题。但一旦出了小区,就需要带上中山路 666 号(公网 IP 地址),是国家统一分配的,不能两个小区都叫中山路 666。</p><p>所以,公有 IP 地址是有个组织统一分配的,假设你要开一个博客网站,那么你就需要去申请购买一个公有 IP,这样全世界的人才能访问。并且公有 IP 地址基本上要在整个互联网范围内保持唯一。</p><h2 id="ip-地址与路由控制">IP 地址与路由控制</h2><p>IP地址的<strong>网络地址</strong>这一部分是用于进行路由控制。路由控制表中记录着网络地址与下一步应该发送至路由器的地址。在主机和路由器上都会有各自的路由器控制表。</p><p>在发送 IP 包时,首先要确定 IP 包首部中的目标地址,再从路由控制表中找到与该地址具有<strong>相同网络地址</strong>的记录,根据该记录将 IP 包转发给相应的下一个路由器。如果路由控制表中存在多条相同网络地址的记录,就选择相同位数最多的网络地址,也就是最长匹配。</p><img src="https://s3.bmp.ovh/imgs/2023/07/15/11b1a816f1e26acf.webp" style="zoom:50%;"><ol><li>主机 A 要发送一个 IP 包,其源地址是 <code>10.1.1.30</code> 和目标地址是 <code>10.1.2.10</code>,由于没有在主机 A 的路由表找到与目标地址 <code>10.1.2.10</code> 相同的网络地址,于是包被转发到默认路由(路由器 <code>1</code> )</li><li>路由器 <code>1</code> 收到 IP 包后,也在路由器 <code>1</code> 的路由表匹配与目标地址相同的网络地址记录,发现匹配到了,于是就把 IP 数据包转发到了 <code>10.1.0.2</code> 这台路由器 <code>2</code></li><li>路由器 <code>2</code> 收到后,同样对比自身的路由表,发现匹配到了,于是把 IP 包从路由器 <code>2</code> 的 <code>10.1.2.1</code> 这个接口出去,最终经过交换机把 IP 数据包转发到了目标主机</li></ol><blockquote><p>环回地址是不会流向网络</p></blockquote><p>环回地址是在同一台计算机上的程序之间进行网络通信时所使用的一个默认地址。</p><p>计算机使用一个特殊的 IP 地址 <strong>127.0.0.1 作为环回地址</strong>。与该地址具有相同意义的是一个叫做 <code>localhost</code> 的主机名。使用这个 IP 或主机名时,数据包不会流向网络。</p><h2 id="ip-分片与重组">IP 分片与重组</h2><p>每种数据链路的最大传输单元 <code>MTU</code> 都是不相同的,如 FDDI 数据链路 MTU 4352、以太网的 MTU 是 1500 字节等。每种数据链路的 MTU 之所以不同,是因为每个不同类型的数据链路的使用目的不同。使用目的不同,可承载的 MTU 也就不同。</p><p>其中,我们最常见数据链路是以太网,它的 MTU 是 <code>1500</code> 字节。那么当 IP 数据包大小大于 MTU 时, IP 数据包就会被分片。经过分片之后的 IP 数据报在被重组的时候,只能由目标主机进行,路由器是不会进行重组的。</p><p>在分片传输中,一旦某个分片丢失,则会造成整个 IP 数据报作废,所以 TCP 引入了 <code>MSS</code> 也就是在 TCP 层进行分片不由 IP 层分片,那么对于 UDP 我们尽量不要发送一个大于 <code>MTU</code> 的数据报文。</p><h2 id="ipv6-基本认识">IPv6 基本认识</h2><p>IPv4 的地址是 32 位的,大约可以提供 42 亿个地址,但是早在 2011 年 IPv4 地址就已经被分配完了。但是 IPv6 的地址是 <code>128</code> 位的,这可分配的地址数量是大的惊人,说个段子 <strong>IPv6 可以保证地球上的每粒沙子都能被分配到一个 IP 地址。</strong></p><p>但 IPv6 除了有更多的地址之外,还有更好的安全性和扩展性,说简单点就是 IPv6 相比于 IPv4 能带来更好的网络体验。</p><p>但是因为 IPv4 和 IPv6 不能相互兼容,所以不但要我们电脑、手机之类的设备支持,还需要网络运营商对现有的设备进行升级,所以这可能是 IPv6 普及率比较慢的一个原因。</p><blockquote><p>IPv6 的亮点</p></blockquote><p>IPv6 不仅仅只是可分配的地址变多了,它还有非常多的亮点。</p><ul><li>IPv6 可自动配置,即使没有 DHCP 服务器也可以实现自动分配IP地址,真是<strong>便捷到即插即用</strong>啊。</li><li>IPv6 包头包首部长度采用固定的值 <code>40</code> 字节,去掉了包头校验和,简化了首部结构,减轻了路由器负荷,大大<strong>提高了传输的性能</strong>。</li><li>IPv6 有应对伪造 IP 地址的网络安全功能以及防止线路窃听的功能,大大<strong>提升了安全性</strong>。</li></ul><p>IPv4 首部与 IPv6 首部的差异如下图:</p><img src="https://s3.bmp.ovh/imgs/2023/07/15/9623a8a925a5aa61.webp" style="zoom:50%;"><ul><li><strong>取消了首部校验和字段。</strong> 因为在数据链路层和传输层都会校验,因此 IPv6 直接取消了 IP 的校验。</li><li><strong>取消了分片/重新组装相关字段。</strong> 分片与重组是耗时的过程,IPv6 不允许在中间路由器进行分片与重组,这种操作只能在源与目标主机,这将大大提高了路由器转发的速度。</li><li><strong>取消选项字段。</strong> 选项字段不再是标准 IP 首部的一部分了,但它并没有消失,而是可能出现在 IPv6 首部中的「下一个首部」指出的位置上。删除该选项字段使的 IPv6 的首部成为固定长度的 <code>40</code> 字节。</li></ul><h1 id="ip-协议相关技术">IP 协议相关技术</h1><h2 id="dns">DNS</h2><p>我们在上网的时候,通常使用的方式是域名,而不是 IP 地址,因为域名方便人类记忆。那么实现这一技术的就是 <strong>DNS 域名解析</strong>,DNS 可以将域名网址自动转换为具体的 IP 地址。</p><blockquote><p>域名的层级关系</p></blockquote><p>DNS 中的域名都是用<strong>句点</strong>来分隔的,比如 <code>www.server.com</code>,这里的句点代表了不同层次之间的<strong>界限</strong>。在域名中,<strong>越靠右</strong>的位置表示其层级<strong>越高</strong>。</p><p>根域是在最顶层,它的下一层就是 com 顶级域,再下面是 <a href="http://server.com">server.com</a>。</p><p>所以域名的层级关系类似一个树状结构:</p><ul><li>根 DNS 服务器</li><li>顶级域 DNS 服务器(com)</li><li>权威 DNS 服务器(<a href="http://server.com">server.com</a>)</li></ul><p>根域的 DNS 服务器信息保存在互联网中所有的 DNS 服务器中。这样一来,任何 DNS 服务器就都可以找到并访问根域 DNS 服务器了。</p><p>因此,客户端只要能够找到任意一台 DNS 服务器,就可以通过它找到根域 DNS 服务器,然后再一路顺藤摸瓜找到位于下层的某台目标 DNS 服务器。</p><p>浏览器首先看一下自己的缓存里有没有,如果没有就向操作系统的缓存要,还没有就检查本机域名解析文件 <code>hosts</code>,如果还是没有,就会 DNS 服务器进行查询,查询的过程如下:</p><ol><li>客户端首先会发出一个 DNS 请求,问 <a href="http://www.server.com">www.server.com</a> 的 IP 是啥,并发给本地 DNS 服务器(也就是客户端的 TCP/IP 设置中填写的 DNS 服务器地址)。</li><li>本地域名服务器收到客户端的请求后,如果缓存里的表格能找到 <a href="http://www.server.com">www.server.com</a>,则它直接返回 IP 地址。如果没有,本地 DNS 会去问它的根域名服务器:“老大, 能告诉我 <a href="http://www.server.com">www.server.com</a> 的 IP 地址吗?” 根域名服务器是最高层次的,它不直接用于域名解析,但能指明一条道路。</li><li>根 DNS 收到来自本地 DNS 的请求后,发现后置是 .com,说:“<a href="http://www.server.com">www.server.com</a> 这个域名归 .com 区域管理”,我给你 .com 顶级域名服务器地址给你,你去问问它吧。”</li><li>本地 DNS 收到顶级域名服务器的地址后,发起请求问“老二, 你能告诉我 <a href="http://www.server.com">www.server.com</a> 的 IP 地址吗?”</li><li>顶级域名服务器说:“我给你负责 <a href="http://www.server.com">www.server.com</a> 区域的权威 DNS 服务器的地址,你去问它应该能问到”。</li><li>本地 DNS 于是转向问权威 DNS 服务器:“老三,www.server.com对应的IP是啥呀?” <a href="http://server.com">server.com</a> 的权威 DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。</li><li>权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。</li><li>本地 DNS 再将 IP 地址返回客户端,客户端和目标建立连接。</li></ol><p>DNS 域名解析的过程蛮有意思的,整个过程就和我们日常生活中找人问路的过程类似,<strong>只指路不带路</strong>。</p><h2 id="arp">ARP</h2><p>在传输一个 IP 数据报的时候,确定了源 IP 地址和目标 IP 地址后,就会通过主机「路由表」确定 IP 数据包下一跳。然而,网络层的下一层是数据链路层,所以我们还要知道「下一跳」的 MAC 地址。</p><p>由于主机的路由表中可以找到下一跳的 IP 地址,所以可以通过 <strong>ARP 协议</strong>,求得下一跳的 MAC 地址。</p><blockquote><p>那么 ARP 又是如何知道对方 MAC 地址的呢?</p></blockquote><p>简单地说,ARP 是借助 <strong>ARP 请求与 ARP 响应</strong>两种类型的包确定 MAC 地址的。</p><ul><li>主机会通过<strong>广播发送 ARP 请求</strong>,这个包中包含了想要知道的 MAC 地址的主机 IP 地址。</li><li>当同个链路中的所有设备收到 ARP 请求时,会去拆开 ARP 请求包里的内容,如果 ARP 请求包中的目标 IP 地址与自己的 IP 地址一致,那么这个设备就将自己的 MAC 地址塞入 <strong>ARP 响应包</strong>返回给主机。</li></ul><p>操作系统通常会把第一次通过 ARP 获取的 MAC 地址缓存起来,以便下次直接从缓存中找到对应 IP 地址的 MAC 地址。</p><p>不过,MAC 地址的缓存是有一定期限的,超过这个期限,缓存的内容将被清除。</p><blockquote><p>RARP 协议你知道是什么吗?</p></blockquote><p>ARP 协议是已知 IP 地址求 MAC 地址,那 RARP 协议正好相反,它是<strong>已知 MAC 地址求 IP 地址</strong>。例如将打印机服务器等小型嵌入式设备接入到网络时就经常会用得到。</p><h2 id="dhcp">DHCP</h2><p>DHCP 在生活中我们是很常见的了,我们的电脑通常都是通过 DHCP 动态获取 IP 地址,大大省去了配 IP 信息繁琐的过程。</p><p>接下来,我们来看看我们的电脑是如何通过 4 个步骤的过程,获取到 IP 的。</p><ul><li>客户端首先发起 <strong>DHCP 发现报文(DHCP DISCOVER)</strong> 的 IP 数据报,由于客户端没有 IP 地址,也不知道 DHCP 服务器的地址,所以使用的是 UDP <strong>广播</strong>通信,其使用的广播目的地址是 255.255.255.255(端口 67) 并且使用 0.0.0.0(端口 68) 作为源 IP 地址。DHCP 客户端将该 IP 数据报传递给链路层,链路层然后将帧广播到所有的网络中设备。</li><li>DHCP 服务器收到 DHCP 发现报文时,用 <strong>DHCP 提供报文(DHCP OFFER)</strong> 向客户端做出响应。该报文仍然使用 IP 广播地址 255.255.255.255,该报文信息携带服务器提供可租约的 IP 地址、子网掩码、默认网关、DNS 服务器以及 <strong>IP 地址租用期</strong>。</li><li>客户端收到一个或多个服务器的 DHCP 提供报文后,从中选择一个服务器,并向选中的服务器发送 <strong>DHCP 请求报文(DHCP REQUEST</strong>进行响应,回显配置的参数。</li><li>最后,服务端用 <strong>DHCP ACK 报文</strong>对 DHCP 请求报文进行响应,应答所要求的参数。</li></ul><p>一旦客户端收到 DHCP ACK 后,交互便完成了,并且客户端能够在租用期内使用 DHCP 服务器分配的 IP 地址。</p><p>如果租约的 DHCP IP 地址快期后,客户端会向服务器发送 DHCP 请求报文:</p><ul><li>服务器如果同意继续租用,则用 DHCP ACK 报文进行应答,客户端就会延长租期。</li><li>服务器如果不同意继续租用,则用 DHCP NACK 报文,客户端就要停止使用租约的 IP 地址。</li></ul><h2 id="nat">NAT</h2><p>IPv4 的地址是非常紧缺的,在前面我们也提到可以通过无分类地址来减缓 IPv4 地址耗尽的速度,但是互联网的用户增速是非常惊人的,所以 IPv4 地址依然有被耗尽的危险。于是,提出了一种<strong>网络地址转换 NAT</strong> 的方法,再次缓解了 IPv4 地址耗尽的问题。</p><p>简单的来说 NAT 就是同个公司、家庭、教室内的主机对外部通信时,把私有 IP 地址转换成公有 IP 地址。</p><p>由于绝大多数的网络应用都是使用传输层协议 TCP 或 UDP 来传输数据的。因此,可以把 IP 地址 + 端口号一起进行转换。</p><p>这样,就用一个全球 IP 地址就可以了,这种转换技术就叫<strong>网络地址与端口转换 NAPT。</strong></p><img src="https://s3.bmp.ovh/imgs/2023/07/15/531e3282956f695f.webp" style="zoom:50%;"><p>两个客户端 192.168.1.10 和 192.168.1.11 同时与服务器 183.232.231.172 进行通信,并且这两个客户端的本地端口都是 1025。</p><p>此时,<strong>两个私有 IP 地址都转换 IP 地址为公有地址 120.229.175.121,但是以不同的端口号作为区分。</strong></p><p>于是,生成一个 NAPT 路由器的转换表,就可以正确地转换地址跟端口的组合,令客户端 A、B 能同时与服务器之间进行通信。</p><p>这种转换表在 NAT 路由器上自动生成。例如,在 TCP 的情况下,建立 TCP 连接首次握手时的 SYN 包一经发出,就会生成这个表。而后又随着收到关闭连接时发出 FIN 包的确认应答从表中被删除。</p><h2 id="icmp">ICMP</h2><p>ICMP 全称是 <strong>Internet Control Message Protocol</strong>,也就是<strong>互联网控制报文协议</strong>。</p><p>网络包在复杂的网络传输环境里,常常会遇到各种问题。当遇到问题的时候,总不能死个不明不白,没头没脑的作风不是计算机网络的风格。所以需要传出消息,报告遇到了什么问题,这样才可以调整传输策略,以此来控制整个局面。</p><blockquote><p>ICMP 功能都有啥?</p></blockquote><p><code>ICMP</code> 主要的功能包括:<strong>确认 IP 包是否成功送达目标地址、报告发送过程中 IP 包被废弃的原因和改善网络设置等。<strong>在 <code>IP</code> 通信中如果某个 <code>IP</code> 包因为某种原因未能达到目标地址,那么这个具体的原因将</strong>由 ICMP 负责通知</strong>。</p><p>主机 <code>A</code> 向主机 <code>B</code> 发送了数据包,由于某种原因,途中的路由器 <code>2</code> 未能发现主机 <code>B</code> 的存在,这时,路由器 <code>2</code> 就会向主机 <code>A</code> 发送一个 <code>ICMP</code> 目标不可达数据包,说明发往主机 <code>B</code> 的包未能成功。ICMP 的这种通知消息会使用 <code>IP</code> 进行发送 。</p><h2 id="igmp">IGMP</h2><p>前面我们知道了组播地址,也就是 D 类地址,既然是组播,那就说明是只有一组的主机能收到数据包,不在一组的主机不能收到数组包,怎么管理是否是在一组呢?那么,就需要 <code>IGMP</code> 协议了。</p>]]></content>
<summary type="html">IP基础知识</summary>
<category term="计算机网络" scheme="https://zhangmuran.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
<category term="IP" scheme="https://zhangmuran.github.io/tags/IP/"/>
</entry>
<entry>
<title>TCP可靠性的实现</title>
<link href="https://zhangmuran.github.io/posts/16357893.html"/>
<id>https://zhangmuran.github.io/posts/16357893.html</id>
<published>2023-07-13T16:37:52.164Z</published>
<updated>2023-07-14T16:27:33.023Z</updated>
<content type="html"><![CDATA[<h1 id="重传机制">重传机制</h1><p>TCP 实现可靠传输的方式之一,是通过序列号与确认应答。</p><h2 id="超时重传">超时重传</h2><p>重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 <code>ACK</code> 确认应答报文,就会重发该数据,也就是我们常说的<strong>超时重传</strong>。</p><p>TCP 会在以下两种情况发生超时重传:</p><ul><li>数据包丢失</li><li>确认应答丢失</li></ul><img src="https://s3.bmp.ovh/imgs/2023/07/13/0dcf0ada644571f8.webp" style="zoom:50%;"><p><strong>每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。</strong></p><p>超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?</p><h2 id="快速重传">快速重传</h2><p>TCP 还有另外一种<strong>快速重传(Fast Retransmit)机制</strong>,它<strong>不以时间为驱动,而是以数据驱动重传</strong>。</p><img src="https://s3.bmp.ovh/imgs/2023/07/13/622ea7cd456921a7.webp" style="zoom: 67%;"><p>快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是<strong>重传的时候,是重传一个,还是重传所有的问题。</strong></p><p>比如上图情况中,是重传Seq 2的数据,还是从Seq 2开始所有数据都要传呢,为了解决不知道该重传哪些 TCP 报文,于是就有 <code>SACK</code> 方法。</p><h2 id="sack-方法">SACK 方法</h2><p>还有一种实现重传机制的方式叫:<code>SACK</code>( Selective Acknowledgment), <strong>选择性确认</strong>。</p><p>这种方式需要在 TCP 头部「选项」字段里加一个 <code>SACK</code> 的东西,它<strong>可以将已收到的数据的信息发送给「发送方」</strong>,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以<strong>只重传丢失的数据</strong>。</p><img src="https://s3.bmp.ovh/imgs/2023/07/13/56b546378a1d576a.webp" style="zoom:50%;"><h2 id="duplicate-sack">Duplicate SACK</h2><p>Duplicate SACK 又称 <code>D-SACK</code>,其主要<strong>使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。</strong></p><ul><li><p>当接收方收到消息发送的ACK丢包时</p><img src="https://s3.bmp.ovh/imgs/2023/07/13/9b6a72fa32feec9c.webp" style="zoom:67%;"></li><li><p>当发送方发送的消息延时时</p><img src="https://s3.bmp.ovh/imgs/2023/07/13/eeb577afedb29068.webp" style="zoom:50%;"></li></ul><h1 id="滑动窗口">滑动窗口</h1><p>如果没有滑动窗口,TCP 每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。导致数据包的<strong>往返时间越长,通信的效率就越低</strong>。</p><p>为解决这个问题,TCP 引入了<strong>窗口</strong>这个概念。那么有了窗口,就可以指定窗口大小,窗口大小就是指<strong>无需等待确认应答,而可以继续发送数据的最大值</strong>。</p><p>窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。</p><p>假设窗口大小为 <code>3</code> 个 TCP 段,那么发送方就可以「连续发送」 <code>3</code> 个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。</p><img src="https://s3.bmp.ovh/imgs/2023/07/14/415753f933c94b7a.webp" style="zoom: 67%;"><blockquote><p>窗口大小由哪一方决定?</p></blockquote><p>TCP 头里有一个字段叫 <code>Window</code>,也就是窗口大小。</p><p><strong>这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。</strong></p><p>所以,通常窗口的大小是由接收方的窗口大小来决定的。</p><p>发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。</p><h1 id="流量控制">流量控制</h1><p>发送方不能无脑的发数据给接收方,要考虑接收方处理能力。如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。</p><p>为了解决这种现象发生,<strong>TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。</strong></p><p>实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,不是一成不变的,会<strong>被操作系统调整</strong>。</p><blockquote><p>操作系统的缓冲区,是如何影响发送窗口和接收窗口的呢?</p></blockquote><p>当应用程序没有及时读取缓存时,发送窗口和接收窗口的变化。</p><p>考虑以下场景:</p><ul><li>客户端作为发送方,服务端作为接收方,发送窗口和接收窗口初始大小为 <code>360</code></li><li>服务端非常的繁忙,当收到客户端的数据时,应用层不能及时读取数据</li></ul><img src="https://s3.bmp.ovh/imgs/2023/07/14/a25c067ff0cc52b1.webp" style="zoom: 40%;"><p>可见最后窗口都收缩为 0 了,也就是发生了<strong>窗口关闭</strong>。当发送方可用窗口变为 0 时,发送方实际上会定时发送窗口探测报文,以便知道接收方的窗口是否发生了改变。</p><p>当服务端系统资源非常紧张的时候,操作系统可能会直接减少了接收缓冲区大小,这时应用程序又无法及时读取缓存数据,那么这时候就有严重的事情发生了,会出现数据包丢失的现象。</p><img src="https://s3.bmp.ovh/imgs/2023/07/14/f53440ee94e97287.webp" style="zoom:40%;"><p>所以,如果发生了先减少缓存,再收缩窗口,就会出现丢包的现象。</p><p><strong>为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。</strong></p><h2 id="窗口关闭">窗口关闭</h2><p><strong>如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。</strong></p><blockquote><p>窗口关闭潜在的危险</p></blockquote><p>接收方向发送方通告窗口大小时,是通过 <code>ACK</code> 报文来通告的。</p><p>那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,那麻烦就大了。</p><img src="https://s3.bmp.ovh/imgs/2023/07/14/d1d9f1a665d51eb7.webp" style="zoom:50%;"><p>如不采取措施,这种相互等待的过程,会造成了死锁的现象。</p><blockquote><p>TCP 是如何解决窗口关闭时,潜在的死锁现象呢?</p></blockquote><p>为了解决这个问题,TCP 为每个连接设有一个持续定时器,<strong>只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。<strong>如果持续计时器超时,就会发送</strong>窗口探测 ( Window probe ) 报文</strong>,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。</p><img src="https://s3.bmp.ovh/imgs/2023/07/14/958d7e1368d8a93a.webp" style="zoom:50%;"><h2 id="糊涂窗口综合症">糊涂窗口综合症</h2><p>如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。到最后,<strong>如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症</strong>。</p><p>要知道,我们的 <code>TCP + IP</code> 头有 <code>40</code> 个字节,为了传输那几个字节的数据,要搭上这么大的开销,这太不经济了。</p><p>要解决糊涂窗口综合症,同时解决两个问题就可以了:</p><ul><li>让接收方不通告小窗口给发送方</li><li>让发送方避免发送小数据</li></ul><h1 id="拥塞控制">拥塞控制</h1><p>流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。</p><p>一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。</p><p><strong>在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大…</strong></p><p>所以,TCP 不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。</p><p>于是,就有了<strong>拥塞控制</strong>,控制的目的就是<strong>避免「发送方」的数据填满整个网络。</strong></p><p>为了在「发送方」调节所要发送数据的量,定义了一个叫做「<strong>拥塞窗口</strong>」的概念。</p><blockquote><p>什么是拥塞窗口?和发送窗口有什么关系呢?</p></blockquote><p><strong>拥塞窗口 cwnd</strong>是发送方维护的一个的状态变量,它会根据<strong>网络的拥塞程度动态变化的</strong>。</p><p>我们在前面提到过发送窗口 <code>swnd</code> 和接收窗口 <code>rwnd</code> 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。</p><p>拥塞窗口 <code>cwnd</code> 变化的规则:</p><ul><li>只要网络中没有出现拥塞,<code>cwnd</code> 就会增大</li><li>但网络中出现了拥塞,<code>cwnd</code> 就减少</li></ul><blockquote><p>那么怎么知道当前网络是否出现了拥塞呢?</p></blockquote><p>其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是<strong>发生了超时重传,就会认为网络出现了拥塞。</strong></p><blockquote><p>拥塞控制有哪些控制算法?</p></blockquote><p>拥塞控制主要是四个算法:</p><ul><li>慢启动</li><li>拥塞避免</li><li>拥塞发生</li><li>快速恢复</li></ul><h2 id="慢启动">慢启动</h2><p>TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,如果一上来就发大量的数据,这不是给网络添堵吗?</p><p>慢启动的算法记住一个规则就行:<strong>当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。</strong></p><p>但是慢启动算法,发包的个数是<strong>指数性的增长</strong>。因为每次收到的ACK等于窗口数,因此是倍增式的。</p><h2 id="拥塞避免算法">拥塞避免算法</h2><p>拥塞窗口比较也不能无休止的增长下去,当拥塞窗口 <code>cwnd</code> 「超过」慢启动门限 <code>ssthresh</code> 就会进入拥塞避免算法。</p><p>一般来说 <code>ssthresh</code> 的大小是 <code>65535</code> 字节。那么进入拥塞避免算法后,它的规则是:<strong>每当收到一个 ACK 时,cwnd 增加 1/cwnd。</strong></p><p>接上前面的慢启动的栗子,现假定 <code>ssthresh</code> 为 <code>8</code>:</p><p>当 8 个 ACK 应答确认到来时,每个确认增加 1/8,8 个 ACK 确认 cwnd 一共增加 1,于是这一次能够发送 9 个 <code>MSS</code> 大小的数据,变成了<strong>线性增长。</strong></p><p>就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。</p><p>当触发了重传机制,也就进入了「拥塞发生算法」。</p><h2 id="拥塞发生算法">拥塞发生算法</h2><p>当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:</p><ul><li>超时重传</li><li>快速重传</li></ul><p>这两种使用的拥塞发送算法是不同的,接下来分别来说说。</p><blockquote><p>发生超时重传的拥塞发生算法</p></blockquote><p>当发生了「超时重传」,则就会使用拥塞发生算法。</p><p>这个时候,ssthresh 和 cwnd 的值会发生变化:</p><ul><li><code>ssthresh</code> 设为 <code>cwnd/2</code>,</li><li><code>cwnd</code> 重置为 <code>1</code> (恢复为 cwnd 初始化值,这里假定 cwnd 初始化值 1)</li></ul><blockquote><p>发生快速重传的拥塞发生算法</p></blockquote><p>还有更好的方式,前面我们讲过「快速重传算法」。当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。</p><p>TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 <code>ssthresh</code> 和 <code>cwnd</code> 变化如下:</p><ul><li><code>cwnd = cwnd/2</code> ,也就是设置为原来的一半;</li><li><code>ssthresh = cwnd</code>;</li><li>进入快速恢复算法</li></ul><h2 id="快速恢复算法">快速恢复算法</h2><p>快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 <code>RTO</code> 超时那么强烈。</p><p>正如前面所说,进入快速恢复之前,<code>cwnd</code> 和 <code>ssthresh</code> 已被更新了:</p><ul><li><code>cwnd = cwnd/2</code> ,也就是设置为原来的一半;</li><li><code>ssthresh = cwnd</code>;</li></ul><p>然后,进入快速恢复算法如下:</p><ul><li>拥塞窗口 <code>cwnd = ssthresh + 3</code> ( 3 的意思是确认有 3 个数据包被收到了);</li><li>重传丢失的数据包</li><li>如果再收到重复的 ACK,那么 cwnd 增加 1</li><li>如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态</li></ul><p>也就是没有像「超时重传」一夜回到解放前,而是还在比较高的值,后续呈线性增长</p>]]></content>
<summary type="html">重传,滑动窗口,流量控制,拥塞控制</summary>
<category term="计算机网络" scheme="https://zhangmuran.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
<category term="TCP" scheme="https://zhangmuran.github.io/tags/TCP/"/>
</entry>
<entry>
<title>TCP基础</title>
<link href="https://zhangmuran.github.io/posts/dc6974ce.html"/>
<id>https://zhangmuran.github.io/posts/dc6974ce.html</id>
<published>2023-07-13T16:37:50.743Z</published>
<updated>2023-07-13T16:40:11.574Z</updated>
<content type="html"><![CDATA[<p>TCP 是<strong>面向连接的、可靠的、基于字节流</strong>的传输层通信协议。</p><img src="https://s3.bmp.ovh/imgs/2023/07/12/aaa8f66e2fe993bc.webp" style="zoom:50%;"><p><strong>序列号</strong>:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。<strong>用来解决网络包乱序问题。</strong></p><p><strong>确认应答号</strong>:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。<strong>用来解决丢包的问题。</strong></p><p><strong>控制位:</strong></p><ul><li><em>ACK</em>:该位为 <code>1</code> 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 <code>SYN</code> 包之外该位必须设置为 <code>1</code> 。</li><li><em>RST</em>:该位为 <code>1</code> 时,表示 TCP 连接中出现异常必须强制断开连接。</li><li><em>SYN</em>:该位为 <code>1</code> 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。</li><li><em>FIN</em>:该位为 <code>1</code> 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 <code>FIN</code> 位为 1 的 TCP 段。</li></ul><blockquote><p>TCP和UDP的区别</p></blockquote><ul><li>TCP 是面向连接的传输层协议,传输数据前先要建立连接。UDP 是不需要连接,即刻传输数据。</li><li>TCP 是一对一的两点服务,即一条连接只有两个端点。UDP 支持一对一、一对多、多对多的交互通信</li><li>TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。UDP 是尽最大努力交付,不保证可靠交付数据。</li><li>TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。</li><li>TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 <code>20</code> 个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。</li><li>TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。</li><li>TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。</li></ul><blockquote><p>为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?</p></blockquote><p>原因是 TCP 有<strong>可变长</strong>的「选项」字段,而 UDP 头部长度则是<strong>不会变化</strong>的,无需多一个字段去记录 UDP 的首部长度。</p><blockquote><p>为什么 UDP 头部有「包长度」字段,而 TCP 头部则没有「包长度」字段呢?</p></blockquote><p>TCP的包长度可以通过 <strong>IP总长度-IP首部长度-TCP首部长度</strong> 计算出来</p><blockquote><p>TCP 和 UDP 可以使用同一个端口吗?</p></blockquote><p>TCP 和 UDP在内核中是两个完全独立的软件模块。</p><p>当主机收到数据包后,可以在 IP 包头的「协议号」字段知道该数据包是 TCP/UDP,所以可以根据这个信息确定送给哪个模块(TCP/UDP)处理,送给 TCP/UDP 模块的报文根据「端口号」确定送给哪个应用程序处理。</p><p>TCP/UDP 各自的端口号也相互独立,如 TCP 有一个 80 号端口,UDP 也可以有一个 80 号端口,二者并不冲突。</p><h1 id="tcp-三次握手">TCP 三次握手</h1><img src="https://s3.bmp.ovh/imgs/2023/07/13/07b60e4bd7918a2d.webp" style="zoom:50%;"><p><strong>第三次握手是可以携带数据的,前两次握手是不可以携带数据的</strong></p><blockquote><p>为什么是三次握手?不是两次、四次?</p></blockquote><ul><li><p>避免历史连接</p><p><strong>如果是两次握手连接,就无法阻止历史连接</strong>。如果客户端向服务器发送SYN报文,然后客户端宕机了,这时候如果是两次连接,就依然会在服务端回复会建立连接并发送数据。但是这次连接是没有意义的。</p></li><li><p>同步双方初始序列号</p><p>**一来一回,才能确保双方的初始序列号能被可靠的同步。**两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。</p></li><li><p>避免资源浪费</p><p>三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。</p></li></ul><blockquote><p>为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?</p></blockquote><ul><li>为了防止历史报文被下一个相同四元组的连接接收(主要方面)</li><li>为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收</li></ul><blockquote><p>既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?</p></blockquote><img src="https://s3.bmp.ovh/imgs/2023/07/13/93963fe2c354aa7f.webp" style="zoom:50%;"><ul><li><code>MTU</code>:一个网络包的最大长度,以太网中一般为 <code>1500</code> 字节</li><li><code>MSS</code>:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度</li></ul><p>当 IP 层有一个超过 <code>MTU</code> 大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,再交给上一层 TCP 传输层。</p><p>因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。<strong>如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传</strong>。</p><p>当某一个 IP 分片丢失后,接收方的 IP 层就无法组装成一个完整的 TCP 报文(头部 + 数据)。所以会触发超时重传,就会重发「整个 TCP 报文(头部 + 数据)」。</p><p>为了达到最佳的传输效能 TCP 协议在<strong>建立连接的时候通常要协商双方的 MSS 值</strong>,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。如果一个 TCP 分片丢失后,<strong>进行重发时也是以 MSS 为单位</strong>,而不用重传所有的分片,大大增加了重传的效率。</p><blockquote><p>第一次握手丢失了,会发生什么?</p></blockquote><p>客户端迟迟收不到服务端的第二次握手,会触发「超时重传」机制,重传 SYN 报文,而且<strong>重传的 SYN 报文的序列号都是一样的</strong>。</p><blockquote><p>第二次握手丢失了,会发生什么?</p></blockquote><p>如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己第一次握手丢失了,于是<strong>客户端就会触发超时重传机制,重传 SYN 报文</strong>。</p><p>服务端就收不到第三次握手,于是<strong>服务端这边会触发超时重传机制,重传 SYN-ACK 报文</strong></p><blockquote><p>第三次握手丢失了,会发生什么?</p></blockquote><p><strong>ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文</strong>,所以第二次和第三次握手都会重传</p><blockquote><p>什么是 SYN 攻击?如何避免 SYN 攻击?</p></blockquote><p>在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:</p><ul><li>半连接队列,也称 SYN 队列</li><li>全连接队列,也称 Accept 队列</li></ul><img src="https://s3.bmp.ovh/imgs/2023/07/13/b2436e982b89ea7d.webp" style="zoom: 67%;"><p>不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。</p><p>TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 <code>SYN</code> 报文,服务端每接收到一个 <code>SYN</code> 报文,就进入<code>SYN_RCVD</code> 状态,但服务端发送出去的 <code>ACK + SYN</code> 报文,无法得到未知 IP 主机的 <code>ACK</code> 应答,久而久之就会<strong>占满服务端的半连接队列</strong>,使得服务端不能为正常用户服务。</p><p>SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样<strong>当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃</strong>,导致客户端无法和服务端建立连接。</p><p>解决办法:</p><ul><li>调大 netdev_max_backlog</li><li>增大 TCP 半连接队列</li><li>开启 tcp_syncookies</li><li>减少 SYN+ACK 重传次数</li></ul><h1 id="tcp-四次挥手">TCP 四次挥手</h1><img src="https://s3.bmp.ovh/imgs/2023/07/13/92f353d7ecde0cbf.webp" style="zoom: 67%;"><blockquote><p>为什么挥手需要四次?</p></blockquote><p>客户端向服务端发送 <code>FIN</code> 时,仅仅表示客户端不再发送数据了但是还能接收数据。</p><p>服务端收到客户端的 <code>FIN</code> 报文时,先回一个 <code>ACK</code> 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 <code>FIN</code> 报文给客户端来表示同意现在关闭连接。</p><p>从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 <code>ACK</code> 和 <code>FIN</code> 一般都会分开发送,因此是需要四次挥手。</p><blockquote><p>第一次挥手丢失了,会发生什么?</p></blockquote><p>如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文</p><p>当客户端超时重传多次 FIN 报文后,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次挥手(ACK报文),那么客户端就会断开连接。</p><blockquote><p>第二次挥手丢失了,会发生什么?</p></blockquote><p>ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。</p><p>对于 close 函数关闭的连接,由于无法再发送和接收数据,所以<code>FIN_WAIT2</code> 状态不可以持续太久,而 <code>tcp_fin_timeout</code> 控制了这个状态下连接的持续时长,默认值是 60 秒。这意味着对于调用 close 关闭的连接,如果在 60 秒后还没有收到 FIN 报文,客户端(主动关闭方)的连接就会直接关闭</p><p>如果主动关闭方使用 shutdown 函数关闭连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。此时,如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于 <code>FIN_WAIT2</code> 状态死等</p><blockquote><p>第三次挥手丢失了,会发生什么?</p></blockquote><p>当服务端重传第三次挥手报文的次数达到了重传最大次数,于是再等待一段时间,如果还是没能收到客户端的第四次挥手(ACK报文),那么服务端就会断开连接。</p><blockquote><p>第四次挥手丢失了,会发生什么?</p></blockquote><p>当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 <code>TIME_WAIT</code> 状态。在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。</p><p>如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文。客户端如果收到第三次挥手(FIN 报文)后,就会重置定时器,当等待 2MSL 时长后,客户端就会断开连接。</p><blockquote><p>为什么需要 TIME_WAIT 状态?</p></blockquote><ul><li>让本次连接中的数据结束,避免被其他连接接收</li><li>保证「被动关闭连接」的一方,能被正确的关闭</li></ul><blockquote><p>服务器出现大量 TIME_WAIT 状态的原因有哪些?</p></blockquote><p>所以如果服务器出现大量的 TIME_WAIT 状态的 TCP 连接,就是说明服务器主动断开了很多 TCP 连接。</p><ul><li>第一个场景:HTTP 没有使用长连接</li><li>第二个场景:HTTP 长连接超时</li><li>第三个场景:HTTP 长连接的请求数量达到上限</li></ul><blockquote><p>服务器出现大量 CLOSE_WAIT 状态的原因有哪些?</p></blockquote>]]></content>
<summary type="html">三次握手与四次挥手</summary>
<category term="计算机网络" scheme="https://zhangmuran.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
<category term="TCP" scheme="https://zhangmuran.github.io/tags/TCP/"/>
<category term="UDP" scheme="https://zhangmuran.github.io/tags/UDP/"/>
</entry>
<entry>
<title>Redis缓存管理机制</title>
<link href="https://zhangmuran.github.io/posts/d1b998ec.html"/>
<id>https://zhangmuran.github.io/posts/d1b998ec.html</id>
<published>2023-07-10T16:04:42.435Z</published>
<updated>2023-07-10T16:09:17.460Z</updated>
<content type="html"><![CDATA[<p>用户的数据一般都是存储于数据库,数据库的数据是落在磁盘上的,磁盘的读写速度可以说是计算机里最慢的硬件了。</p><p>当用户的请求,都访问数据库的话,请求数量一上来,数据库很容易就奔溃的了,所以为了避免用户直接访问数据库,会用 Redis 作为缓存层。</p><p>因为 Redis 是内存数据库,我们可以将数据库的数据缓存在 Redis 里,相当于数据缓存在内存,内存的读写速度比硬盘快好几个数量级,这样大大提高了系统性能。</p><img src="https://s3.bmp.ovh/imgs/2023/07/10/226e6f9d09c75a2f.png" style="zoom:90%;"><h1 id="保证数据库和缓存的一致性">保证数据库和缓存的一致性</h1><p><strong>由于引入了缓存,那么在数据更新时,不仅要更新数据库,而且要更新缓存</strong>,那么,就出现了新的问题,在数据更新时,应该先更新数据库还是先更新缓存?</p><p>我们来分析一下,考虑到<strong>并发</strong>问题,比如「请求 A 」和「请求 B 」两个请求,同时更新「同一条」数据,则可能出现这样的顺序:</p><img src="https://s3.bmp.ovh/imgs/2023/07/10/cc320f7175e469d4.webp" style="zoom: 67%;"><p>A 请求先将数据库的数据更新为 1,然后在更新缓存前,请求 B 将数据库的数据更新为 2,紧接着也把缓存更新为 2,然后 A 请求更新缓存为 1。</p><p>此时,数据库中的数据是 2,而缓存中的数据却是 1,<strong>出现了缓存和数据库中的数据不一致的现象</strong>。</p><p>反过来,这次我们<strong>先更新缓存,后更新数据库</strong></p><p>可以发现,依然会出现缓存和数据库中数据不一致的情况</p><img src="https://s3.bmp.ovh/imgs/2023/07/10/dc56badbff3726ae.webp" style="zoom:67%;"><p>所以,<strong>无论是「先更新数据库,再更新缓存」,还是「先更新缓存,再更新数据库」,这两个方案都存在并发问题,当两个请求并发更新同一条数据的时候,可能会出现缓存和数据库中的数据不一致的现象</strong>。</p><p>这次换个思路,数据发生变化时,<strong>不更新缓存,而是删除缓存中的数据。然后,到读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。</strong></p><p>出于对刚刚问题的拓展,我们是<strong>先更新数据库还是先更新缓存</strong>呢?</p><p>假设先删除缓存。删除缓存后在数据库更新之前来了一发查询,结果空缓存又读取上了数据库的值,随后我们更新了数据库。相当于删除缓存的动作被省略了,发生了缓存和数据库中的数据不一致</p><img src="https://s3.bmp.ovh/imgs/2023/07/10/29dae5624e1496ec.webp" style="zoom:67%;"><p>接下来考虑先更新数据库</p><p>假如某个用户数据在缓存中不存在,从数据库读取数据写入缓存之前发生了更新数据库,删除缓存的过程,又导致了不一致的结果</p><img src="https://s3.bmp.ovh/imgs/2023/07/10/10aaec64c6a9e712.webp" style="zoom: 67%;"><p>从上面的理论上分析,先更新数据库,再删除缓存也是会出现数据不一致性的问题,<strong>但是在实际中,这个问题出现的概率并不高</strong>。</p><p><strong>因为缓存的写入通常要远远快于数据库的写入</strong>,所以在实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况。</p><p>所以,<strong>「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的</strong>。</p><p>为了确保万无一失,还可以给缓存数据加上「<strong>过期时间</strong>」,就算在这期间存在缓存数据不一致,有过期时间来兜底,这样也能达到最终一致。</p><p>引入了缓存层,就会有缓存异常的三个问题,分别是<strong>缓存雪崩、缓存击穿、缓存穿透</strong></p><h1 id="缓存雪崩">缓存雪崩</h1><p>为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间。当缓存数据过期后,需要重新访问数据库生成缓存,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。</p><p>那么,当<strong>大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机</strong>时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是<strong>缓存雪崩</strong>的问题。</p><img src="https://s3.bmp.ovh/imgs/2023/07/10/715e8458a04cf357.png" style="zoom: 50%;"><p>为了<strong>防止大量缓存数据在同一时间过期</strong>,可以:</p><ul><li><p><strong>均匀设置过期时间</strong></p><p>可以在对缓存数据设置过期时间时,<strong>给这些数据的过期时间加上一个随机数</strong>,这样就保证数据不会在同一时间过期。</p></li><li><p><strong>互斥锁</strong></p><p>当业务线程在处理用户请求时,<strong>如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存</strong>(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。</p><p>实现互斥锁的时候,最好设置<strong>超时时间</strong>,避免请求发生了某种意外而一直阻塞,导致其他请求也一直拿不到锁,整个系统就会出现无响应的现象。</p></li><li><p><strong>后台更新缓存</strong></p></li></ul><p>除此之外,<strong>Redis故障宕机</strong>也会导致缓存雪崩,我们可以:</p><ul><li><p>服务熔断或请求限流机制</p><p>启动<strong>服务熔断</strong>机制,<strong>暂停业务应用对缓存服务的访问,直接返回错误</strong>,不用再继续访问数据库,从而降低对数据库的访问压力,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。</p><p>服务熔断机制是保护数据库的正常允许,但是暂停了业务应用访问缓存服系统,全部业务都无法正常工作</p><p>为了减少对业务的影响,我们可以启用<strong>请求限流</strong>机制,<strong>只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务</strong>,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制</p></li><li><p>构建 Redis 缓存高可靠集群</p><p>服务熔断或请求限流机制是缓存雪崩发生后的应对方案,我们最好通过<strong>主从节点的方式构建 Redis 缓存高可靠集群</strong>。</p><p>如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。</p></li></ul><h1 id="缓存击穿">缓存击穿</h1><p>我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。</p><p>如果缓存中的<strong>某个热点数据过期</strong>了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是<strong>缓存击穿</strong>的问题。</p><p>缓存击穿跟缓存雪崩很相似,可以认为缓存击穿是缓存雪崩的一个子集。</p><p>应对缓存击穿可以采取前面说到两种方案:</p><ul><li>互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。</li><li>不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间</li></ul><h1 id="缓存穿透">缓存穿透</h1><p>当用户访问的数据,<strong>既不在缓存中,也不在数据库中</strong>,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是<strong>缓存穿透</strong>的问题。</p><img src="https://s3.bmp.ovh/imgs/2023/07/10/9bae65e30b107998.png" style="zoom:50%;"><p>应对缓存穿透的方案,常见的方案有三种。</p><ul><li><p>非法请求的限制</p><p>当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。</p></li><li><p>缓存空值或者默认值</p><p>当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库</p></li><li><p>使用<strong>布隆过滤器</strong>快速判断数据是否存在,避免通过查询数据库来判断数据是否存在</p><p>我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。</p><p>即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。</p></li></ul><h1 id="总结">总结</h1><img src="https://s3.bmp.ovh/imgs/2023/07/10/797959df0f34c29f.webp" style="zoom: 67%;">]]></content>
<summary type="html">关于缓存雪崩、击穿、穿透以及如何实现数据库和缓存一致性</summary>
<category term="数据库" scheme="https://zhangmuran.github.io/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
<category term="Redis" scheme="https://zhangmuran.github.io/tags/Redis/"/>
</entry>
<entry>
<title>Redis常见数据类型</title>
<link href="https://zhangmuran.github.io/posts/ad7e0bd5.html"/>
<id>https://zhangmuran.github.io/posts/ad7e0bd5.html</id>
<published>2023-07-10T14:19:44.352Z</published>
<updated>2023-07-10T14:40:01.815Z</updated>
<content type="html"><![CDATA[<h1 id="基本命令">基本命令</h1><p><code>keys *</code> 查看当前库中有哪些 key</p><figure class="highlight shell"><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">127.0.0.1:6379> keys *</span><br><span class="line">1) "users"</span><br></pre></td></tr></table></figure><p><code>exists key</code> 判断某个 key 是否存在</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> exists users</span><br><span class="line">(integer) 1</span><br><span class="line">127.0.0.1:6379> exists user</span><br><span class="line">(integer) 0</span><br></pre></td></tr></table></figure><p><code>type key</code> 查看 key 对应的数据类型</p><figure class="highlight shell"><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">127.0.0.1:6379> type users</span><br><span class="line">hash</span><br></pre></td></tr></table></figure><p><code>del key</code> 和 <code>unlink key</code> 都是删除指定的 key 数据。区别在于del key是直接删除,而 <code>unlink key</code> 是将key 从 keyspace元数据中删除,真正的删除会在后续异步操作。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> del key1</span><br><span class="line">(integer) 1</span><br><span class="line">127.0.0.1:6379> unlink key2</span><br><span class="line">(integer) 1</span><br></pre></td></tr></table></figure><p><code>expire key seconds</code> 为key设置指定的过期时间,可以使用 <code>ttl key</code> 查看 key 还有多久过期,-1 代表永不过期,-2 代表已经过期</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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> keys *</span><br><span class="line">1) "key1"</span><br><span class="line">127.0.0.1:6379> ttl key1</span><br><span class="line">(integer) -1</span><br><span class="line">127.0.0.1:6379> expire key1 10</span><br><span class="line">(integer) 1</span><br><span class="line">127.0.0.1:6379> ttl key1</span><br><span class="line">(integer) 5</span><br><span class="line">127.0.0.1:6379> ttl key1</span><br><span class="line">(integer) -2</span><br><span class="line">127.0.0.1:6379> keys *</span><br><span class="line">(empty list or set)</span><br></pre></td></tr></table></figure><p><code>select index</code> 可以切换数据库</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> select 2</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379[2]> select 0</span><br><span class="line">OK</span><br></pre></td></tr></table></figure><p><code>dbsize</code> 查看当前数据库中key的数量</p><figure class="highlight shell"><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">127.0.0.1:6379> dbsize</span><br><span class="line">(integer) 1</span><br></pre></td></tr></table></figure><p><code>flushdb</code> 和 <code>flushall</code> 分别代表清空当前数据库和清空所有数据库</p><h1 id="字符串-string">字符串 String</h1><p>String是Redis最基本的类型,一个key对应一个value。String类型是<strong>二进制安全</strong>的。意味着Redis的String可以包含任何数据。比如jpg图片或者序列化的对象。String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是<strong>512M</strong></p><p><code>set key value [expiration EX seconds|PX milliseconds] [NX|XX]</code> 来添加String类型的键值对,重复对一个key操作会覆盖value</p><p><code>get key</code> 查询对应键值</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> set key1 value1</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> get key1</span><br><span class="line">"value1"</span><br></pre></td></tr></table></figure><p><code>append key value</code> 为key对应的字符串后追加value</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> append key1 abc</span><br><span class="line">(integer) 9</span><br><span class="line">127.0.0.1:6379> get key1</span><br><span class="line">"value1abc"</span><br></pre></td></tr></table></figure><p><code>strlen key</code> 获取对应字符串的长度</p><figure class="highlight shell"><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">127.0.0.1:6379> strlen key1</span><br><span class="line">(integer) 9</span><br></pre></td></tr></table></figure><p><code>setnx key value</code> 只有在key不存在时,才能成功设置key的值</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> setnx key1 value1</span><br><span class="line">(integer) 0</span><br><span class="line">127.0.0.1:6379> setnx key2 value2</span><br><span class="line">(integer) 1</span><br></pre></td></tr></table></figure><p>如果对应的value是<strong>数字类型</strong>,则可以使用 <code>incr key</code> 和 <code>decr key</code> 来为对应的数字加一和减一</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> incr key1</span><br><span class="line">(error) ERR value is not an integer or out of range</span><br><span class="line">127.0.0.1:6379> set num 100</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> incr num</span><br><span class="line">(integer) 101</span><br><span class="line">127.0.0.1:6379> decr num</span><br><span class="line">(integer) 100</span><br></pre></td></tr></table></figure><p>如果要指定步长,则可用 <code>incrby key increment</code> 以及 <code>decrby key increment</code> 指定步长对数字加减操作</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> incrby num 100</span><br><span class="line">(integer) 200</span><br><span class="line">127.0.0.1:6379> decrby num 300</span><br><span class="line">(integer) -100</span><br></pre></td></tr></table></figure><p><code> mset key value [key value ...]</code> <code> mget key [key ...]</code> <code>msetnx key value [key value ...]</code> 三个命令分别对应 <code>set</code> <code>get</code> <code>setnx</code> ,区别在一次可以操作多个键值对,需要注意<code>msetnx</code> 只有所有的key都不存在时才会成功</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><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">127.0.0.1:6379> mset k1 v1 k2 v2</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> mget k1 k2 k3</span><br><span class="line">1) "v1"</span><br><span class="line">2) "v2"</span><br><span class="line">3) (nil)</span><br><span class="line">127.0.0.1:6379> msetnx k1 v1 k3 v3</span><br><span class="line">(integer) 0</span><br><span class="line">127.0.0.1:6379> mget k1 k2 k3</span><br><span class="line">1) "v1"</span><br><span class="line">2) "v2"</span><br><span class="line">3) (nil)</span><br><span class="line">127.0.0.1:6379> msetnx k3 v3 k4 v4</span><br><span class="line">(integer) 1</span><br><span class="line">127.0.0.1:6379> mget k3 k4</span><br><span class="line">1) "v3"</span><br><span class="line">2) "v4"</span><br></pre></td></tr></table></figure><p><code>getrange key start end</code> 拿字符串上的指定区间</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> get key</span><br><span class="line">"helloworld"</span><br><span class="line">127.0.0.1:6379> getrange key 0 4</span><br><span class="line">"hello"</span><br><span class="line">127.0.0.1:6379> getrange key 0 100</span><br><span class="line">"helloworld"</span><br></pre></td></tr></table></figure><p><code>setrange key offset value</code> 从指定偏移量开始设置字符串</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> setrange key 12 abc</span><br><span class="line">(integer) 15</span><br><span class="line">127.0.0.1:6379> get key</span><br><span class="line">"helloworld\x00\x00abc"</span><br></pre></td></tr></table></figure><p><code>setex key seconds value</code> 设置键值对同时设置过期时间</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> setex key 10 value</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> ttl key</span><br><span class="line">(integer) 5</span><br></pre></td></tr></table></figure><h1 id="列表-list">列表 List</h1><p>单键多值。<br>Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。·<br>它的底层实际是个<strong>双向链表</strong>,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。</p><p><code>lpush key value [value ...]</code> 和 <code>rpush key value [value ...]</code> 分别表示从左边和右边向列表key中放入数据(压栈)</p><p><code>lpop key</code> 和 <code>rpop key</code> 分别表示从列表key中弹出左边或者右边的元素</p><p><code>lrange key start stop</code> 从左边顺序查看列表中元素,stop为-1表示到倒数第一个元素</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><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> lpush list v1 v2 v3 v4 v5</span><br><span class="line">(integer) 5</span><br><span class="line">127.0.0.1:6379> lrange list 0 -1</span><br><span class="line">1) "v5"</span><br><span class="line">2) "v4"</span><br><span class="line">3) "v3"</span><br><span class="line">4) "v2"</span><br><span class="line">5) "v1"</span><br><span class="line">127.0.0.1:6379> lpop list</span><br><span class="line">"v5"</span><br><span class="line">127.0.0.1:6379> lrange list 0 -1</span><br><span class="line">1) "v4"</span><br><span class="line">2) "v3"</span><br><span class="line">3) "v2"</span><br><span class="line">4) "v1"</span><br></pre></td></tr></table></figure><p><code>lindex key index</code> 获取从左往右下标第index的元素</p><figure class="highlight shell"><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">127.0.0.1:6379> lindex list 1</span><br><span class="line">"v3"</span><br></pre></td></tr></table></figure><p><code>llen key</code> 获取列表的长度</p><figure class="highlight shell"><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">127.0.0.1:6379> llen list</span><br><span class="line">(integer) 4</span><br></pre></td></tr></table></figure><p><code>linsert key BEFORE|AFTER pivot value</code> 在列表key中找到从左开始找到第一个pivot并在其 之前/之后 插入value</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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> linsert list BEFORE v2 v22</span><br><span class="line">(integer) 5</span><br><span class="line">127.0.0.1:6379> lrange list 0 -1</span><br><span class="line">1) "v4"</span><br><span class="line">2) "v3"</span><br><span class="line">3) "v22"</span><br><span class="line">4) "v2"</span><br><span class="line">5) "v1"</span><br></pre></td></tr></table></figure><p><code>lrem key count value</code> 从左边开始删除count个value</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> lrem list 2 v22</span><br><span class="line">(integer) 1</span><br><span class="line">127.0.0.1:6379> lrange list 0 -1</span><br><span class="line">1) "v4"</span><br><span class="line">2) "v3"</span><br><span class="line">3) "v2"</span><br><span class="line">4) "v1"</span><br></pre></td></tr></table></figure><p><code>lset key index value</code> 将列表key下表index的元素替换为value</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> lset list 1 hello</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> lrange list 0 -1</span><br><span class="line">1) "v4"</span><br><span class="line">2) "hello"</span><br><span class="line">3) "v2"</span><br><span class="line">4) "v1"</span><br></pre></td></tr></table></figure><h1 id="集合-set">集合 Set</h1><p>Redis Set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,Set是一个很好的选择,并且Set提供了判断某个成员是否在一个Set集合内的重要接口,这个也是list所不能提供的。<br>Redis的Set是String类型的<strong>无序集合</strong>。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是 <strong>O(1)</strong>。</p><p><code>sadd key member [member ...]</code> 向集合中添加元素</p><p><code>smembers key</code> 取出该集合的所有值</p><p><code>scard key</code> 返回该集合中的元素的个数</p><p><code>srem key member [member ...]</code> 删除集合中的元素</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> sadd set k1 k2 k2 k3</span><br><span class="line">(integer) 3</span><br><span class="line">127.0.0.1:6379> smembers set</span><br><span class="line">1) "k1"</span><br><span class="line">2) "k3"</span><br><span class="line">3) "k2"</span><br><span class="line">127.0.0.1:6379> scard set</span><br><span class="line">(integer) 3</span><br><span class="line">127.0.0.1:6379> srem set k3</span><br><span class="line">(integer) 1</span><br><span class="line">127.0.0.1:6379> smembers set</span><br><span class="line">1) "k1"</span><br><span class="line">2) "k2"</span><br></pre></td></tr></table></figure><p><code>spop key [count]</code> 从集合中<strong>随机</strong>弹出count个元素,默认一个</p><p><code>srandmember key [count]</code> 从集合中<strong>随机</strong>取出count个元素</p><p><code>smove source destination member</code> 把集合source中的member转移到destination中</p><p><code>sinter key [key ...]</code> 返回集合中的交集</p><p><code>sunion key [key ...]</code> 返回集合中的并集</p><p><code>sdiff key [key ...]</code> 返回集合中的差集,a中有,b中没有</p><h1 id="哈希-hash">哈希 Hash</h1><p>Redis Hash是一个键值对集合。<br>Redis Hash是一个String类型的 field和value的映射表,Hash 特别适合用于存储对象。</p><p><code>hset key field value</code> 设置哈希key的field字段为value,可以用 <code>hmset key field value [field value ...]</code> 批量操作</p><p><code>hget key field</code> 获取field字段下的值,用 <code>hmget key field [field ...]</code> 批量操作</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> hmset user name zmr age 18</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> hmget user name age</span><br><span class="line">1) "zmr"</span><br><span class="line">2) "18"</span><br></pre></td></tr></table></figure><p><code>hexists key field</code> 查看哈希表中key下是否有field</p><figure class="highlight shell"><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">127.0.0.1:6379> hexists user age</span><br><span class="line">(integer) 1</span><br></pre></td></tr></table></figure><p><code>hkeys key</code> 查看key下的所有field</p><p><code>hvals key</code> 查看key下的所有values</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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> hkeys user</span><br><span class="line">1) "name"</span><br><span class="line">2) "age"</span><br><span class="line">127.0.0.1:6379> hvals user</span><br><span class="line">1) "zmr"</span><br><span class="line">2) "18"</span><br></pre></td></tr></table></figure><p><code>hincrby key field increment</code> 给field的数字值加increment</p><p><code>hsetnx key field value</code> 不重复的field才可以加</p><h1 id="有序集合-zset">有序集合 Zset</h1><p>Redis有序集合Zset与普通集合Set非常相似,是一个<strong>没有重复元素</strong>的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score) ,这个评分 (score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了。<br>因为元素是有序的,所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。<br>访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。</p><p><code>zadd key [NX|XX] [CH] [INCR] score member [score member ...]</code> 将一个或多个元素的值以及其评分加入Zset</p><p><code>zrange key start stop [WITHSCORES]</code> 返回有序的Zset指定下标的元素,如果有WITHSCORES,则会显示评分</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> zadd topn 100 c++ 50 java 150 go 10 php</span><br><span class="line">(integer) 4</span><br><span class="line">127.0.0.1:6379> zrange topn 0 -1 WITHSCORES</span><br><span class="line">1) "php"</span><br><span class="line">2) "10"</span><br><span class="line">3) "java"</span><br><span class="line">4) "50"</span><br><span class="line">5) "c++"</span><br><span class="line">6) "100"</span><br><span class="line">7) "go"</span><br><span class="line">8) "150"</span><br></pre></td></tr></table></figure><p><code>zrangebyscore key min max [WITHSCORES] [LIMIT offset count]</code> 返回评分介于 min 和 max 的元素,从小到大。<code>zrevrangebyscore key max min [WITHSCORES] [LIMIT offset count]</code> 则为从大到小</p><p><code>zincrby key increment member</code> 为元素member的score加上increment</p><p><code>zrem key member [member ...]</code> 删除元素member</p><p><code>zcount key min max</code> 返回分数区间内的元素个数</p><p><code>zrank key member</code> 返回该值在集合中的排名,从0开始</p><h1 id="事务和锁">事务和锁</h1><p>Redis事务是一个单独的隔离操作︰事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。<br>Redis事务的主要作用就是串联多个命令防止别的命令插队。</p><h2 id="milti-exex-discard">Milti Exex Discard</h2><p>使用 <code>multi</code> 开始组队,随后写入redis命令,完成后 <code>exec</code> 便开始执行命令,<code>discard</code> 则放弃执行</p><img src="https://s3.bmp.ovh/imgs/2023/07/08/3911a9917cc66370.png" style="zoom:50%;"><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> multi</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> set k1 v1</span><br><span class="line">QUEUED</span><br><span class="line">127.0.0.1:6379> set k2 v2</span><br><span class="line">QUEUED</span><br><span class="line">127.0.0.1:6379> exec</span><br><span class="line">1) OK</span><br><span class="line">2) OK</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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> multi</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> set k1 v1</span><br><span class="line">QUEUED</span><br><span class="line">127.0.0.1:6379> set k2 v2</span><br><span class="line">QUEUED</span><br><span class="line">127.0.0.1:6379> discard</span><br><span class="line">OK</span><br></pre></td></tr></table></figure><h2 id="错误处理">错误处理</h2><p>如果在组队的过程中发生了错误,那么所有的命令都无法正常的执行</p><img src="https://s3.bmp.ovh/imgs/2023/07/08/83bc2c133f92b01a.png" style="zoom:50%;"><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> multi</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> set k1 v1</span><br><span class="line">QUEUED</span><br><span class="line">127.0.0.1:6379> set k2</span><br><span class="line">(error) ERR wrong number of arguments for 'set' command</span><br><span class="line">127.0.0.1:6379> exec</span><br><span class="line">(error) EXECABORT Transaction discarded because of previous errors.</span><br></pre></td></tr></table></figure><p>如果在执行的过程中出错,那么只有失败的命令无法正常执行,其他的命令都会执行</p><img src="https://s3.bmp.ovh/imgs/2023/07/08/b35ab92d5ad19c5e.png" style="zoom:50%;"><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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> multi</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> set k1 v1</span><br><span class="line">QUEUED</span><br><span class="line">127.0.0.1:6379> incr k1</span><br><span class="line">QUEUED</span><br><span class="line">127.0.0.1:6379> set k2 v2</span><br><span class="line">QUEUED</span><br><span class="line">127.0.0.1:6379> exec</span><br><span class="line">1) OK</span><br><span class="line">2) (error) ERR value is not an integer or out of range</span><br><span class="line">3) OK</span><br></pre></td></tr></table></figure><h2 id="事务冲突">事务冲突</h2><p>可以用<strong>乐观锁</strong>来解决,在执行 <code>multi</code> 之前,先执行 <code>watch key1 [key...]</code> ,可以监视一个(或多个) key,如果在事务执行之前这个(或这些) key被其他命令所改动,那么事务将被打断。</p><p>用两个客户端来演示一下,现在有String类型的数据</p><figure class="highlight shell"><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">127.0.0.1:6379> set k1 100</span><br><span class="line">OK</span><br></pre></td></tr></table></figure><p>两个客户端都对k1进行watch,并且执行事务对k1进行操作</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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> watch k1</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> multi</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> decrby k1 10</span><br><span class="line">QUEUED</span><br></pre></td></tr></table></figure><p>此时一个客户端率先执行exec</p><figure class="highlight shell"><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">127.0.0.1:6379> exec</span><br><span class="line">1) (integer) 90</span><br></pre></td></tr></table></figure><p>另外一个在进行操作,就会收到乐观锁的限制执行失败</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379> exec</span><br><span class="line">(nil)</span><br></pre></td></tr></table></figure><h1 id="持久化">持久化</h1><p>Redis 是一种基于内存的数据库,所有的数据都存储在内存中。但是也有办法把数据放到磁盘中,这个过程就叫做持久化</p><h2 id="rdb">RDB</h2><p>在指定的<strong>时间间隔</strong>内将内存中的数据集<strong>快照</strong>写入磁盘,也就是行话讲的 Snapshot快照,它恢复时是将快照文件直接读到内存里。</p><h2 id="aof">AOF</h2><p>AOF和RDB同时开启,系统默认读取AOF中的数据</p><h1 id="主从复制">主从复制</h1><h1 id="集群">集群</h1>]]></content>
<summary type="html">String,List,Hash,Set,Zset</summary>
<category term="数据库" scheme="https://zhangmuran.github.io/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
<category term="Redis" scheme="https://zhangmuran.github.io/tags/Redis/"/>
</entry>
<entry>
<title>HTTP版本变化</title>
<link href="https://zhangmuran.github.io/posts/0.html"/>
<id>https://zhangmuran.github.io/posts/0.html</id>
<published>2023-07-03T15:29:03.000Z</published>
<updated>2023-07-11T15:59:14.790Z</updated>
<content type="html"><![CDATA[<h1 id="http11">HTTP/1.1</h1><p>HTTP/1.1 相比 HTTP/1.0 性能上的改进:</p><ul><li>使用长连接的方式改善了 HTTP/1.0 短连接造成的性能开销。</li><li>支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。</li></ul><p>但 HTTP/1.1 还是有性能瓶颈:</p><ul><li>请求 / 响应头部(Header)未经压缩就发送,首部信息越多延迟越大。只能压缩 <code>Body</code> 的部分;</li><li>发送冗长的首部。每次互相发送相同的首部造成的浪费较多;</li><li>服务器是按请求的顺序响应的,如果服务器响应慢,会招致客户端一直请求不到数据,也就是队头阻塞;</li><li>没有请求优先级控制;</li><li>请求只能从客户端开始,服务器只能被动响应。</li></ul><h1 id="http2">HTTP/2</h1><p>HTTP/2 协议是基于 HTTPS 的,所以 HTTP/2 的安全性也是有保障的。</p><ul><li><p>头部压缩</p><p>HTTP/2 会<strong>压缩头</strong>(Header)如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你<strong>消除重复的部分</strong>。</p><p>这就是所谓的 <code>HPACK</code> 算法:在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就<strong>提高速度</strong>了。</p></li><li><p>二进制格式</p><p>HTTP/2 不再像 HTTP/1.1 里的纯文本形式的报文,而是全面采用了<strong>二进制格式</strong>,头信息和数据体都是二进制,并且统称为帧(frame):<strong>头信息帧(Headers Frame)和数据帧(Data Frame)</strong>。</p><p>比如状态码 200 ,在 HTTP/1.1 是用 ‘2’‘0’‘0’ 三个字符来表示(二进制:00110010 00110000 00110000),共用了 3 个字节。收到报文后,无需再将明文的报文转成二进制,而是直接解析二进制报文,这<strong>增加了数据传输的效率</strong>。</p></li><li><p>并发传输</p><p>HTTP/1.1 的实现是基于请求-响应模型的。会造成<strong>队头阻塞</strong>。HTTP/2 引出了 Stream 概念,多个 Stream 复用在一条 TCP 连接。</p><p>Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成。Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体)。</p></li><li><p>服务器推送</p><p>HTTP/2 还在一定程度上改善了传统的「请求 - 应答」工作模式,服务端不再是被动地响应,可以<strong>主动</strong>向客户端发送消息。</p><p>比如,客户端通过 HTTP/1.1 请求从服务器那获取到了 HTML 文件,而 HTML 可能还需要依赖 CSS 来渲染页面,这时客户端还要再发起获取 CSS 文件的请求,需要两次消息往返。在 HTTP/2 中,客户端在访问 HTML 时,服务器可以直接主动推送 CSS 文件,减少了消息传递的次数。</p></li></ul><p>HTTP/2 还是存在“队头阻塞”的问题,只不过问题不是在 HTTP 这一层面,而是在 TCP 这一层。</p><p><strong>HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。</strong></p><h1 id="http3">HTTP/3</h1><p>HTTP/1.1 中的管道( pipeline)虽然解决了请求的队头阻塞,但是<strong>没有解决响应的队头阻塞</strong></p><p>HTTP/2 虽然通过多个请求复用一个 TCP 连接解决了 HTTP 的队头阻塞 ,但是<strong>一旦发生丢包,就会阻塞住所有的 HTTP 请求</strong>,这属于 TCP 层队头阻塞。</p><p>HTTP/2 队头阻塞的问题是因为 TCP,所以 <strong>HTTP/3 把 HTTP 下层的 TCP 协议改成了 UDP</strong></p><p>UDP 是不可靠传输的,但基于 UDP 的 <strong>QUIC 协议</strong> 可以实现类似 TCP 的可靠性传输。</p><p>QUIC 有以下 3 个特点</p><ul><li><p>无队头阻塞</p><p>QUIC 协议也有类似 HTTP/2 Stream 与多路复用的概念,也是可以在同一条连接上并发传输多个 Stream,Stream 可以认为就是一条 HTTP 请求。</p><p>QUIC 有自己的一套机制可以保证传输的可靠性的。<strong>当某个流发生丢包时,只会阻塞这个流,其他流不会受到影响,因此不存在队头阻塞问题</strong>。这与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,其他流也会因此受影响。</p><p>所以,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,某个流发生丢包了,只会影响该流,其他流不受影响。</p></li><li></li></ul>]]></content>
<summary type="html">由浅入深学习HTTP和HTTPS</summary>
<category term="计算机网络" scheme="https://zhangmuran.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
<category term="HTTP" scheme="https://zhangmuran.github.io/tags/HTTP/"/>
<category term="HTTPS" scheme="https://zhangmuran.github.io/tags/HTTPS/"/>
</entry>
<entry>
<title>HTTP基础</title>
<link href="https://zhangmuran.github.io/posts/22f97770.html"/>
<id>https://zhangmuran.github.io/posts/22f97770.html</id>
<published>2023-07-03T14:35:03.000Z</published>
<updated>2023-07-03T15:26:25.734Z</updated>
<content type="html"><![CDATA[<p>HTTP 是超文本传输协议,也就是 <strong>H</strong>yperText <strong>T</strong>ransfer <strong>P</strong>rotocol。</p><h1 id="http基本概念">HTTP基本概念</h1><h2 id="http报文结构">HTTP报文结构</h2><p>HTTP的请求报文由四部分组成:请求行(request line)、请求头部(header)、空行和请求数据(request data)</p><p><img src="https://s3.bmp.ovh/imgs/2023/07/02/0f05d854449273d7.png" alt></p><p>HTTP响应报文由状态行(status line)、响应头部(headers)、空行(blank line)和响应数据(response body)四个部分组成</p><p><img src="https://s3.bmp.ovh/imgs/2023/07/02/a443ac90965d23e7.png" alt></p><h2 id="http常见状态码">HTTP常见状态码</h2><img src="https://s3.bmp.ovh/imgs/2023/07/02/781699fdddba25f5.webp" width="80%" height="80%"><p><code>1xx</code> 类状态码属于<strong>提示信息</strong>,是协议处理中的一种中间状态,实际用到的比较少。</p><p><code>2xx</code> 类状态码表示服务器<strong>成功</strong>处理了客户端的请求,也是我们最愿意看到的状态。</p><ul><li>「<strong>200 OK</strong>」是最常见的成功状态码,表示一切正常。如果是非 <code>HEAD</code> 请求,服务器返回的响应头都会有 body 数据。</li><li>「<strong>204 No Content</strong>」也是常见的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。</li><li>「<strong>206 Partial Content</strong>」是应用于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。</li></ul><p><code>3xx</code> 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是<strong>重定向</strong>。</p><ul><li>「<strong>301 Moved Permanently</strong>」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。</li><li>「<strong>302 Found</strong>」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。</li></ul><p>301 和 302 都会在响应头里使用字段 <code>Location</code>,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。</p><ul><li>「<strong>304 Not Modified</strong>」不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,也就是告诉客户端可以继续使用缓存资源,用于缓存控制。</li></ul><p><code>4xx</code> 类状态码表示客户端发送的<strong>报文有误</strong>,服务器无法处理,也就是错误码的含义。</p><ul><li>「<strong>400 Bad Request</strong>」表示客户端请求的报文有错误,但只是个笼统的错误。</li><li>「<strong>403 Forbidden</strong>」表示服务器禁止访问资源,并不是客户端的请求出错。</li><li>「<strong>404 Not Found</strong>」表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。</li></ul><p><code>5xx</code> 类状态码表示客户端请求报文正确,但是<strong>服务器处理时内部发生了错误</strong>,属于服务器端的错误码。</p><ul><li>「<strong>500 Internal Server Error</strong>」与 400 类型,是个笼统通用的错误码,服务器发生了什么错误,我们并不知道。</li><li>「<strong>501 Not Implemented</strong>」表示客户端请求的功能还不支持,类似“即将开业,敬请期待”的意思。</li><li>「<strong>502 Bad Gateway</strong>」通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。</li><li>「<strong>503 Service Unavailable</strong>」表示服务器当前很忙,暂时无法响应客户端,类似“网络服务正忙,请稍后重试”的意思。</li></ul><h2 id="http-常见字段">HTTP 常见字段</h2><ul><li><p><em>Host</em> 字段</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Host</span><span class="punctuation">: </span>www.baidu.com</span><br></pre></td></tr></table></figure></li><li><p><em>Content-Length 字段</em></p><p>HTTP 是基于 TCP 传输协议进行通信的,而使用了 TCP 传输协议,就会存在一个“粘包”的问题,<strong>HTTP 协议通过设置回车符、换行符作为 HTTP header 的边界,通过 Content-Length 字段作为 HTTP body 的边界,这两个方式都是为了解决“粘包”的问题</strong>。</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Content-Length</span><span class="punctuation">: </span>1000</span><br></pre></td></tr></table></figure></li><li><p><em>Connection 字段</em></p><p><code>Connection</code> 字段最常用于客户端要求服务器使用「HTTP 长连接」机制,以便其他请求复用。</p><p>HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。</p><p>HTTP/1.1 版本的默认连接都是长连接,但为了兼容老版本的 HTTP,需要指定 <code>Connection</code> 首部字段的值为 <code>Keep-Alive</code>。</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Connection</span><span class="punctuation">: </span>Keep-Alive</span><br></pre></td></tr></table></figure><p>开启了 HTTP Keep-Alive 机制后, 连接就不会中断,而是保持连接。当客户端发送另一个请求时,它会使用同一个连接,一直持续到客户端或服务器端提出断开连接。</p></li><li><p><em>Content-Type 字段</em></p><p><code>Content-Type</code> 字段用于服务器<strong>回应</strong>时,告诉客户端,本次数据是什么格式。</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Content-Type</span><span class="punctuation">: </span>text/html; Charset=utf-8</span><br></pre></td></tr></table></figure><p>上面的类型表明,发送的是网页,而且编码是UTF-8。</p><p>客户端请求的时候,可以使用 <code>Accept</code> 字段声明自己可以接受哪些数据格式。</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Accept</span><span class="punctuation">: </span>*/*</span><br></pre></td></tr></table></figure><p>上面代码中,客户端声明自己可以接受任何格式的数据。</p></li><li><p><em>Content-Encoding 字段</em></p><p><code>Content-Encoding</code> 字段说明数据的压缩方法。表示服务器返回的数据使用了什么压缩格式</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Content-Encoding</span><span class="punctuation">: </span>gzip</span><br></pre></td></tr></table></figure><p>上面表示服务器返回的数据采用了 gzip 方式压缩,告知客户端需要用此方式解压。</p><p>客户端在请求时,用 <code>Accept-Encoding</code> 字段说明自己可以接受哪些压缩方法。</p><figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Accept-Encoding</span><span class="punctuation">: </span>gzip, deflate</span><br></pre></td></tr></table></figure></li></ul><h1 id="get-和-post">GET 和 POST</h1><p><strong>GET 的语义是从服务器获取指定的资源</strong>,这个资源可以是静态的文本、页面、图片视频等。GET 请求的参数位置一般是写在 URL 中,而且浏览器会对 URL 的长度有限制(HTTP协议本身对 URL长度并没有做任何规定)。</p><p><strong>POST 的语义是根据请求负荷(报文body)对指定的资源做出处理</strong>,具体的处理方式视资源类型而不同。POST 请求携带数据的位置一般是写在报文 body 中,body 中的数据可以是任意格式的数据,只要客户端与服务端协商好即可,而且浏览器不会对 body 大小做限制。</p><blockquote><p>GET 和 POST 方法都是安全和幂等的吗?</p></blockquote><ul><li>「安全」是指请求方法不会「破坏」服务器上的资源。</li><li>「幂等」是多次执行相同的操作,结果都是「相同」的。</li></ul><p>从 RFC 规范定义的语义来看:</p><ul><li><strong>GET 方法就是安全且幂等的</strong>,因为它是「只读」操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。所以,<strong>可以对 GET 请求的数据做缓存,这个缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),而且在浏览器中 GET 请求可以保存为书签</strong>。</li><li><strong>POST</strong> 因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是<strong>不安全</strong>的,且多次提交数据就会创建多个资源,所以<strong>不是幂等</strong>的。所以,<strong>浏览器一般不会缓存 POST 请求,也不能把 POST 请求保存为书签</strong>。</li></ul><p>但是实际过程中,开发者不一定会按照 RFC 规范定义的语义来实现 GET 和 POST 方法。比如:</p><ul><li>可以用 GET 方法实现新增或删除数据的请求,这样实现的 GET 方法自然就不是安全和幂等。</li><li>可以用 POST 方法实现查询数据的请求,这样实现的 POST 方法自然就是安全和幂等。</li></ul><h1 id="http缓存">HTTP缓存</h1><p>对于一些具有重复性的 HTTP 请求,每次请求得到的数据都一样的,我们可以把这对「请求-响应」的数据都<strong>缓存在本地</strong>,那么下次就直接读取本地的数据,不必再通过网络获取服务器的响应了,这样的话 HTTP/1.1 的性能肯定肉眼可见的提升。</p><p>HTTP 缓存有两种实现方式,分别是<strong>强制缓存和协商缓存</strong>。</p><h2 id="强制缓存">强制缓存</h2><p>强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在于浏览器这边。</p><img src="https://s3.bmp.ovh/imgs/2023/07/02/5c0336f4665703d2.webp" style="zoom: 33%;"><p>强缓存是利用下面这两个 HTTP 响应头部(Response Header)字段实现的,它们都用来表示资源在客户端缓存的有效期:</p><ul><li><code>Cache-Control</code>, 是一个相对时间</li><li><code>Expires</code>,是一个绝对时间</li></ul><p>如果同时有 Cache-Control 和 Expires 字段的话,<strong>Cache-Control 的优先级高于 Expires</strong> 。</p><p>当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 Cache-Control,Cache-Control 中设置了过期时间大小。再次请求访问该资源时,会先<strong>通过请求资源的时间与 Cache-Control 中设置的过期时间大小,来计算出该资源是否过期</strong>,如果没有,则使用该缓存,否则重新请求服务器。服务器再次收到请求后,会再次更新 Response 头部的 Cache-Control。</p><h2 id="协商缓存">协商缓存</h2><p><strong>协商缓存就是与服务端协商之后,通过协商结果来判断是否使用本地缓存</strong>。</p><img src="https://s3.bmp.ovh/imgs/2023/07/02/ccfd0648f8505dfd.webp" style="zoom:50%;"><p>协商缓存可以基于两种头部来实现。</p><p>第一种:请求头部中的 <code>If-Modified-Since</code> 字段与响应头部中的 <code>Last-Modified</code> 字段实现,这两个字段的意思是:</p><ul><li>响应头部中的 <code>Last-Modified</code>:标示这个响应资源的最后修改时间;</li><li>请求头部中的 <code>If-Modified-Since</code>:当资源过期了,发现响应头中具有 Last-Modified 声明,则再次发起请求的时候带上 Last-Modified 的时间,服务器收到请求后发现有 If-Modified-Since 则与被请求资源的最后修改时间进行对比(Last-Modified),如果最后修改时间较新,说明资源又被改过,则返回最新资源,<strong>HTTP 200 OK</strong>;如果最后修改时间较旧,说明资源无新修改,响应 <strong>HTTP 304</strong> ,不返回资源走缓存。</li></ul><p>第二种:请求头部中的 <code>If-None-Match</code> 字段与响应头部中的 <code>ETag</code> 字段,这两个字段的意思是:</p><ul><li>响应头部中 <code>Etag</code>:唯一标识响应资源;</li><li>请求头部中的 <code>If-None-Match</code>:当资源过期时,浏览器发现响应头里有 Etag,则再次向服务器发起请求时,会将请求头 If-None-Match 值设置为 Etag 的值。服务器收到请求后进行比对,如果资源没有变化返回 <strong>304</strong>,如果资源变化了返回 <strong>200</strong>。</li></ul><blockquote><p>第二种方式优先级更高,可以想一下为什么</p></blockquote><p><strong>协商缓存这两个字段都需要配合强制缓存中 Cache-Control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求</strong>。</p><img src="https://s3.bmp.ovh/imgs/2023/07/02/26681768449a00ef.webp" style="zoom:50%;"><h1 id="http特性">HTTP特性</h1><h1 id="https">HTTPS</h1><p>HTTP 信息是明文传输,TCP 三次握手之后便可进行 HTTP 的报文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,使得报文能够加密传输。</p><p>两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443。HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。</p><p>HTTP 由于是明文传输,所以安全上存在以下三个风险:</p><ul><li><strong>窃听风险</strong>,比如通信链路上可以获取通信内容,用户号容易没。</li><li><strong>篡改风险</strong>,比如强制植入垃圾广告,视觉污染,用户眼容易瞎。</li><li><strong>冒充风险</strong>,比如冒充淘宝网站,用户钱容易没。</li></ul><p>HTTP<strong>S</strong> 在 HTTP 与 TCP 层之间加入了 <code>SSL/TLS</code> 协议,可以很好的解决了上述的风险:</p><ul><li><strong>信息加密</strong>:交互信息无法被窃取,但你的号会因为「自身忘记」账号而没。</li><li><strong>校验机制</strong>:无法篡改通信内容,篡改了就不能正常显示,但百度「竞价排名」依然可以搜索垃圾广告。</li><li><strong>身份证书</strong>:证明淘宝是真的淘宝网,但你的钱还是会因为「剁手」而没。</li></ul><p>可见,只要自身不做「恶」,SSL/TLS 协议是能保证通信是安全的。</p><h2 id="https如何解决http的问题">HTTPS如何解决HTTP的问题</h2><ul><li><strong>混合加密</strong>的方式实现信息的<strong>机密性</strong>,解决了窃听的风险。</li><li><strong>消息认证码,数字签名</strong>的方式来实现<strong>完整性</strong>,它能够为数据生成独一无二的「指纹」,指纹用于校验数据的完整性,解决了篡改的风险。</li><li>将服务器公钥放入到<strong>数字证书</strong>中,解决了冒充的风险。</li></ul><h3 id="混合加密">混合加密</h3><p>HTTPS 采用的是<strong>对称加密</strong>和<strong>非对称加密</strong>结合的「混合加密」方式:</p><ul><li>在通信建立前采用<strong>非对称加密</strong>的方式交换「会话秘钥」,后续就不再使用非对称加密。</li><li>在通信过程中全部使用<strong>对称加密</strong>的「会话秘钥」的方式加密明文数据。</li></ul><p>采用「混合加密」的方式的原因:</p><ul><li><strong>对称加密</strong>只使用一个密钥,运算速度快,密钥必须保密,无法做到安全的密钥交换。</li><li><strong>非对称加密</strong>使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,解决了密钥交换问题但速度慢。</li></ul><h3 id="消息认证码">消息认证码</h3><p>为了保证传输的内容不被篡改,我们需要对内容做哈希操作得到消息认证码,然后将消息认证码同内容一起传输给对方。</p><p>对方收到内容和消息认证码后,先是重新对内容计算出一个消息认证码,然后跟发送方发送的消息认证码做比较,如果相同,说明内容没有被篡改,否则就可以判断出内容被篡改了。</p><h3 id="数字签名">数字签名</h3><p>通过哈希算法可以确保内容不会被篡改,<strong>但是并不能保证「内容 + 哈希值」不会被中间人替换,因为这里缺少对客户端收到的消息是否来源于服务端的证明</strong>。这种情况还可能造成<strong>事后否认</strong></p><p>为了避免这种情况,计算机里会用<strong>非对称加密算法</strong>来解决,</p><ul><li><strong>公钥加密,私钥解密</strong>。这个目的是为了<strong>保证内容传输的安全</strong>,因为被公钥加密的内容,其他人是无法解密的,只有持有私钥的人,才能解密出实际的内容;</li><li><strong>私钥加密,公钥解密</strong>。这个目的是为了<strong>保证消息不会被冒充</strong>,因为私钥是不可泄露的,如果公钥能正常解密出私钥加密的内容,就能证明这个消息是来源于持有私钥身份的人发送的。</li></ul><p>在数字签名这里,我们自然要使用<strong>私钥加密,公钥解密</strong></p><p>这样凡是可以用发送方提供的公钥解开的数字签名,就可以说明该信息一定是由公钥的提供者发送的,避免了<strong>假冒</strong>的出现</p><h3 id="数字证书">数字证书</h3><p>在非对称加密的过程中,需要先发送公钥给对方。如果这个公钥是第三方发送的呢。为了解决这个问题,就需要数字证书。</p><p>颁发数字证书的权威机构就是 CA (数字证书认证机构),CA将服务器公钥和由CA的私钥生成的数字签名放在数字证书中。这样数字证书中不仅包含了服务器的公钥,又可以通过数字签名保证数字证书一定是CA发布的而并非第三方伪造。于是就可以安全的拿到服务器的公钥进行对称加密了。</p><p>同时为了防止第三方篡改数字证书的内容以及CA的公钥,CA的上级还有更高级CA为其提供数字证书来防止这种情况,一直到最高级的几个RCA再互相提供数字证书,形成了规模庞大的CA链,这大大提高了第三方的难度和成本。</p><h2 id="https建立连接的过程">HTTPS建立连接的过程</h2><p>在完成了TCP三次握手之后,会进行SSL/TLS 的建立过程,也就是 TLS 握手阶段。</p><p>TLS 的「握手阶段」涉及<strong>四次</strong>通信,使用不同的密钥交换算法,TLS 握手流程也会不一样的,现在常用的密钥交换算法有两种:RSA 算法和 ECDHE 算法。</p><p>这里说一下比较简单的基于RSA算法的的TLS握手。</p>]]></content>
<summary type="html">由浅入深学习HTTP和HTTPS</summary>
<category term="计算机网络" scheme="https://zhangmuran.github.io/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
<category term="HTTP" scheme="https://zhangmuran.github.io/tags/HTTP/"/>
<category term="HTTPS" scheme="https://zhangmuran.github.io/tags/HTTPS/"/>
</entry>
</feed>