缓存.md 10.0 KB
Newer Older
C
CyC2018 已提交
1
[⭐️ 面试进阶专栏 ⭐️](https://xiaozhuanlan.com/CyC2018)
C
CyC2018 已提交
2 3 4 5 6 7 8 9 10 11
<!-- GFM-TOC -->
* [一、缓存特征](#一缓存特征)
* [二、LRU](#二lru)
* [三、缓存位置](#三缓存位置)
* [四、CDN](#四cdn)
* [五、缓存问题](#五缓存问题)
* [六、数据分布](#六数据分布)
* [七、一致性哈希](#七一致性哈希)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
C
CyC2018 已提交
12

C
CyC2018 已提交
13 14 15 16

# 一、缓存特征

## 命中率
C
CyC2018 已提交
17 18 19 20 21

当某个请求能够通过访问缓存而得到响应时,称为缓存命中。

缓存命中率越高,缓存的利用率也就越高。

C
CyC2018 已提交
22
## 最大空间
C
CyC2018 已提交
23 24 25 26 27

缓存通常位于内存中,内存的空间通常比磁盘空间小的多,因此缓存的最大空间不可能非常大。

当缓存存放的数据量超过最大空间时,就需要淘汰部分数据来存放新到达的数据。

C
CyC2018 已提交
28
## 淘汰策略
C
CyC2018 已提交
29

C
CyC2018 已提交
30
- FIFO(First In First Out):先进先出策略,在实时性的场景下,需要经常访问最新的数据,那么就可以使用 FIFO,使得最先进入的数据(最晚的数据)被淘汰。
C
CyC2018 已提交
31

C
CyC2018 已提交
32
- LRU(Least Recently Used):最近最久未使用策略,优先淘汰最久未使用的数据,也就是上次被访问时间距离现在最久的数据。该策略可以保证内存中的数据都是热点数据,也就是经常被访问的数据,从而保证缓存命中率。
C
CyC2018 已提交
33

C
CyC2018 已提交
34
# 二、LRU
C
CyC2018 已提交
35

C
CyC2018 已提交
36
以下是基于 双向链表 + HashMap 的 LRU 算法实现,对算法的解释如下:
C
CyC2018 已提交
37

C
CyC2018 已提交
38 39
- 访问某个节点时,将其从原来的位置删除,并重新插入到链表头部。这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就淘汰链表尾部的节点。
- 为了使删除操作时间复杂度为 O(1),就不能采用遍历的方式找到某个节点。HashMap 存储着 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。
C
CyC2018 已提交
40 41

```java
C
CyC2018 已提交
42
public class LRU<K, V> implements Iterable<K> {
C
CyC2018 已提交
43

C
CyC2018 已提交
44 45 46 47
    private Node head;
    private Node tail;
    private HashMap<K, Node> map;
    private int maxSize;
C
CyC2018 已提交
48

C
CyC2018 已提交
49
    private class Node {
C
CyC2018 已提交
50

C
CyC2018 已提交
51 52 53 54
        Node pre;
        Node next;
        K k;
        V v;
C
CyC2018 已提交
55

C
CyC2018 已提交
56 57 58 59 60
        public Node(K k, V v) {
            this.k = k;
            this.v = v;
        }
    }
C
CyC2018 已提交
61

C
CyC2018 已提交
62

C
CyC2018 已提交
63
    public LRU(int maxSize) {
C
CyC2018 已提交
64

C
CyC2018 已提交
65 66
        this.maxSize = maxSize;
        this.map = new HashMap<>(maxSize * 4 / 3);
C
CyC2018 已提交
67

C
CyC2018 已提交
68 69
        head = new Node(null, null);
        tail = new Node(null, null);
C
CyC2018 已提交
70

C
CyC2018 已提交
71 72 73
        head.next = tail;
        tail.pre = head;
    }
C
CyC2018 已提交
74

C
CyC2018 已提交
75

C
CyC2018 已提交
76
    public V get(K key) {
C
CyC2018 已提交
77

C
CyC2018 已提交
78 79 80
        if (!map.containsKey(key)) {
            return null;
        }
C
CyC2018 已提交
81

C
CyC2018 已提交
82 83 84
        Node node = map.get(key);
        unlink(node);
        appendHead(node);
C
CyC2018 已提交
85

C
CyC2018 已提交
86 87
        return node.v;
    }
C
CyC2018 已提交
88

C
CyC2018 已提交
89

C
CyC2018 已提交
90
    public void put(K key, V value) {
C
CyC2018 已提交
91

C
CyC2018 已提交
92 93 94 95
        if (map.containsKey(key)) {
            Node node = map.get(key);
            unlink(node);
        }
C
CyC2018 已提交
96

C
CyC2018 已提交
97 98 99
        Node node = new Node(key, value);
        map.put(key, node);
        appendHead(node);
C
CyC2018 已提交
100

C
CyC2018 已提交
101 102 103 104 105
        if (map.size() > maxSize) {
            Node toRemove = removeTail();
            map.remove(toRemove.k);
        }
    }
C
CyC2018 已提交
106

C
CyC2018 已提交
107

C
CyC2018 已提交
108
    private void unlink(Node node) {
C
CyC2018 已提交
109

C
CyC2018 已提交
110 111
        Node pre = node.pre;
        Node next = node.next;
C
CyC2018 已提交
112

C
CyC2018 已提交
113 114
        pre.next = next;
        next.pre = pre;
C
CyC2018 已提交
115

C
CyC2018 已提交
116 117 118
        node.pre = null;
        node.next = null;
    }
C
CyC2018 已提交
119

C
CyC2018 已提交
120

C
CyC2018 已提交
121 122 123 124 125 126 127
    private void appendHead(Node node) {
        Node next = head.next;
        node.next = next;
        next.pre = node;
        node.pre = head;
        head.next = node;
    }
C
CyC2018 已提交
128

C
CyC2018 已提交
129

C
CyC2018 已提交
130
    private Node removeTail() {
C
CyC2018 已提交
131

C
CyC2018 已提交
132
        Node node = tail.pre;
C
CyC2018 已提交
133

C
CyC2018 已提交
134 135 136
        Node pre = node.pre;
        tail.pre = pre;
        pre.next = tail;
C
CyC2018 已提交
137

C
CyC2018 已提交
138 139
        node.pre = null;
        node.next = null;
C
CyC2018 已提交
140

C
CyC2018 已提交
141 142
        return node;
    }
C
CyC2018 已提交
143

C
CyC2018 已提交
144

C
CyC2018 已提交
145 146
    @Override
    public Iterator<K> iterator() {
C
CyC2018 已提交
147

C
CyC2018 已提交
148 149
        return new Iterator<K>() {
            private Node cur = head.next;
C
CyC2018 已提交
150

C
CyC2018 已提交
151 152 153 154
            @Override
            public boolean hasNext() {
                return cur != tail;
            }
C
CyC2018 已提交
155

C
CyC2018 已提交
156 157 158 159 160 161 162 163
            @Override
            public K next() {
                Node node = cur;
                cur = cur.next;
                return node.k;
            }
        };
    }
C
CyC2018 已提交
164 165 166
}
```

C
CyC2018 已提交
167
# 三、缓存位置
C
CyC2018 已提交
168

C
CyC2018 已提交
169
## 浏览器
C
CyC2018 已提交
170

C
CyC2018 已提交
171
当 HTTP 响应允许进行缓存时,浏览器会将 HTML、CSS、JavaScript、图片等静态资源进行缓存。
C
CyC2018 已提交
172

C
CyC2018 已提交
173
## ISP
C
CyC2018 已提交
174

C
CyC2018 已提交
175
网络服务提供商(ISP)是网络访问的第一跳,通过将数据缓存在 ISP 中能够大大提高用户的访问速度。
C
CyC2018 已提交
176

C
CyC2018 已提交
177
## 反向代理
C
CyC2018 已提交
178

C
CyC2018 已提交
179
反向代理位于服务器之前,请求与响应都需要经过反向代理。通过将数据缓存在反向代理,在用户请求反向代理时就可以直接使用缓存进行响应。
C
CyC2018 已提交
180

C
CyC2018 已提交
181
## 本地缓存
C
CyC2018 已提交
182

C
CyC2018 已提交
183
使用 Guava Cache 将数据缓存在服务器本地内存中,服务器代码可以直接读取本地内存中的缓存,速度非常快。
C
CyC2018 已提交
184

C
CyC2018 已提交
185
## 分布式缓存
C
CyC2018 已提交
186

C
CyC2018 已提交
187
使用 Redis、Memcache 等分布式缓存将数据缓存在分布式缓存系统中。
C
CyC2018 已提交
188

C
CyC2018 已提交
189
相对于本地缓存来说,分布式缓存单独部署,可以根据需求分配硬件资源。不仅如此,服务器集群都可以访问分布式缓存,而本地缓存需要在服务器集群之间进行同步,实现难度和性能开销上都非常大。
C
CyC2018 已提交
190

C
CyC2018 已提交
191
## 数据库缓存
C
CyC2018 已提交
192

C
CyC2018 已提交
193
MySQL 等数据库管理系统具有自己的查询缓存机制来提高查询效率。
C
CyC2018 已提交
194

C
CyC2018 已提交
195
# 四、CDN
C
CyC2018 已提交
196

C
CyC2018 已提交
197
内容分发网络(Content distribution network,CDN)是一种互连的网络系统,它利用更靠近用户的服务器从而更快更可靠地将 HTML、CSS、JavaScript、音乐、图片、视频等静态资源分发给用户。
C
CyC2018 已提交
198

C
CyC2018 已提交
199
CDN 主要有以下优点:
C
CyC2018 已提交
200

C
CyC2018 已提交
201 202 203
- 更快地将数据分发给用户;
- 通过部署多台服务器,从而提高系统整体的带宽性能;
- 多台服务器可以看成是一种冗余机制,从而具有高可用性。
C
CyC2018 已提交
204

C
CyC2018 已提交
205
<div align="center"> <img src="pics/15313ed8-a520-4799-a300-2b6b36be314f.jpg"/> </div><br>
C
CyC2018 已提交
206

C
CyC2018 已提交
207
# 五、缓存问题
C
CyC2018 已提交
208

C
CyC2018 已提交
209
## 缓存穿透
C
CyC2018 已提交
210

C
CyC2018 已提交
211
指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。
C
CyC2018 已提交
212 213 214

解决方案:

C
CyC2018 已提交
215 216
- 对这些不存在的数据缓存一个空数据;
- 对这类请求进行过滤。
C
CyC2018 已提交
217

C
CyC2018 已提交
218
## 缓存雪崩
C
CyC2018 已提交
219

C
CyC2018 已提交
220
指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。
C
CyC2018 已提交
221

C
CyC2018 已提交
222
在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。
C
CyC2018 已提交
223 224 225

解决方案:

C
CyC2018 已提交
226 227 228
- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;
- 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。
- 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。
C
CyC2018 已提交
229

C
CyC2018 已提交
230
## 缓存一致性
C
CyC2018 已提交
231 232 233 234 235

缓存一致性要求数据更新的同时缓存数据也能够实时更新。

解决方案:

C
CyC2018 已提交
236 237
- 在数据更新的同时立即去更新缓存;
- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。
C
CyC2018 已提交
238 239 240

要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据。

C
CyC2018 已提交
241
# 六、数据分布
C
CyC2018 已提交
242

C
CyC2018 已提交
243
## 哈希分布
C
CyC2018 已提交
244

C
CyC2018 已提交
245
哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key,则将该数据分配的节点序号为:hash(key)%N。
C
CyC2018 已提交
246

C
CyC2018 已提交
247
传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。
C
CyC2018 已提交
248

C
CyC2018 已提交
249
## 顺序分布
C
CyC2018 已提交
250

C
CyC2018 已提交
251
将数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如 User 表的 ID 范围为 1 \~ 7000,使用顺序分布可以将其划分成多个子表,对应的主键范围为 1 \~ 1000,1001 \~ 2000,...,6001 \~ 7000。
C
CyC2018 已提交
252 253 254

顺序分布相比于哈希分布的主要优点如下:

C
CyC2018 已提交
255 256
- 能保持数据原有的顺序;
- 并且能够准确控制每台服务器存储的数据量,从而使得存储空间的利用率最大。
C
CyC2018 已提交
257

C
CyC2018 已提交
258
# 七、一致性哈希
C
CyC2018 已提交
259

C
CyC2018 已提交
260
Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据迁移的问题。
C
CyC2018 已提交
261

C
CyC2018 已提交
262
## 基本原理
C
CyC2018 已提交
263

C
CyC2018 已提交
264
将哈希空间 [0, 2<sup>n</sup>-1] 看成一个哈希环,每个服务器节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。
C
CyC2018 已提交
265

C
CyC2018 已提交
266
<div align="center"> <img src="pics/68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg"/> </div><br>
C
CyC2018 已提交
267

C
CyC2018 已提交
268
一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将它前一个节点 C 上的数据重新进行分布即可,对于节点 A、B、D 都没有影响。
C
CyC2018 已提交
269

C
CyC2018 已提交
270
<div align="center"> <img src="pics/66402828-fb2b-418f-83f6-82153491bcfe.jpg"/> </div><br>
C
CyC2018 已提交
271

C
CyC2018 已提交
272
## 虚拟节点
C
CyC2018 已提交
273 274 275

上面描述的一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。

C
CyC2018 已提交
276 277 278
数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。

解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得多,那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。
C
CyC2018 已提交
279

C
CyC2018 已提交
280
# 参考资料
C
CyC2018 已提交
281

C
CyC2018 已提交
282 283 284 285 286
- 大规模分布式存储系统
- [缓存那些事](https://tech.meituan.com/cache_about.html)
- [一致性哈希算法](https://my.oschina.net/jayhu/blog/732849)
- [内容分发网络](https://zh.wikipedia.org/wiki/%E5%85%A7%E5%AE%B9%E5%82%B3%E9%81%9E%E7%B6%B2%E8%B7%AF)
- [How Aspiration CDN helps to improve your website loading speed?](https://www.aspirationhosting.com/aspiration-cdn/)