缓存.md 10.5 KB
Newer Older
C
CyC2018 已提交
1
# 一、缓存特征
C
CyC2018 已提交
2

C
CyC2018 已提交
3
## 命中率
C
CyC2018 已提交
4 5 6 7 8

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

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

C
CyC2018 已提交
9
## 最大空间
C
CyC2018 已提交
10 11 12 13 14

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

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

C
CyC2018 已提交
15
## 淘汰策略
C
CyC2018 已提交
16

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

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

C
CyC2018 已提交
21
# 二、LRU
C
CyC2018 已提交
22

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

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

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

C
CyC2018 已提交
31 32 33 34
    private Node head;
    private Node tail;
    private HashMap<K, Node> map;
    private int maxSize;
C
CyC2018 已提交
35

C
CyC2018 已提交
36
    private class Node {
C
CyC2018 已提交
37

C
CyC2018 已提交
38 39 40 41
        Node pre;
        Node next;
        K k;
        V v;
C
CyC2018 已提交
42

C
CyC2018 已提交
43 44 45 46 47
        public Node(K k, V v) {
            this.k = k;
            this.v = v;
        }
    }
C
CyC2018 已提交
48

C
CyC2018 已提交
49

C
CyC2018 已提交
50
    public LRU(int maxSize) {
C
CyC2018 已提交
51

C
CyC2018 已提交
52 53
        this.maxSize = maxSize;
        this.map = new HashMap<>(maxSize * 4 / 3);
C
CyC2018 已提交
54

C
CyC2018 已提交
55 56
        head = new Node(null, null);
        tail = new Node(null, null);
C
CyC2018 已提交
57

C
CyC2018 已提交
58 59 60
        head.next = tail;
        tail.pre = head;
    }
C
CyC2018 已提交
61

C
CyC2018 已提交
62

C
CyC2018 已提交
63
    public V get(K key) {
C
CyC2018 已提交
64

C
CyC2018 已提交
65 66 67
        if (!map.containsKey(key)) {
            return null;
        }
C
CyC2018 已提交
68

C
CyC2018 已提交
69 70 71
        Node node = map.get(key);
        unlink(node);
        appendHead(node);
C
CyC2018 已提交
72

C
CyC2018 已提交
73 74
        return node.v;
    }
C
CyC2018 已提交
75

C
CyC2018 已提交
76

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

C
CyC2018 已提交
79 80 81 82
        if (map.containsKey(key)) {
            Node node = map.get(key);
            unlink(node);
        }
C
CyC2018 已提交
83

C
CyC2018 已提交
84 85 86
        Node node = new Node(key, value);
        map.put(key, node);
        appendHead(node);
C
CyC2018 已提交
87

C
CyC2018 已提交
88 89 90 91 92
        if (map.size() > maxSize) {
            Node toRemove = removeTail();
            map.remove(toRemove.k);
        }
    }
C
CyC2018 已提交
93

C
CyC2018 已提交
94

C
CyC2018 已提交
95
    private void unlink(Node node) {
C
CyC2018 已提交
96

C
CyC2018 已提交
97 98
        Node pre = node.pre;
        Node next = node.next;
C
CyC2018 已提交
99

C
CyC2018 已提交
100 101
        pre.next = next;
        next.pre = pre;
C
CyC2018 已提交
102

C
CyC2018 已提交
103 104 105
        node.pre = null;
        node.next = null;
    }
C
CyC2018 已提交
106

C
CyC2018 已提交
107

C
CyC2018 已提交
108 109 110 111 112 113 114
    private void appendHead(Node node) {
        Node next = head.next;
        node.next = next;
        next.pre = node;
        node.pre = head;
        head.next = node;
    }
C
CyC2018 已提交
115

C
CyC2018 已提交
116

C
CyC2018 已提交
117
    private Node removeTail() {
C
CyC2018 已提交
118

C
CyC2018 已提交
119
        Node node = tail.pre;
C
CyC2018 已提交
120

C
CyC2018 已提交
121 122 123
        Node pre = node.pre;
        tail.pre = pre;
        pre.next = tail;
C
CyC2018 已提交
124

C
CyC2018 已提交
125 126
        node.pre = null;
        node.next = null;
C
CyC2018 已提交
127

C
CyC2018 已提交
128 129
        return node;
    }
C
CyC2018 已提交
130

C
CyC2018 已提交
131

C
CyC2018 已提交
132 133
    @Override
    public Iterator<K> iterator() {
C
CyC2018 已提交
134

C
CyC2018 已提交
135 136
        return new Iterator<K>() {
            private Node cur = head.next;
C
CyC2018 已提交
137

C
CyC2018 已提交
138 139 140 141
            @Override
            public boolean hasNext() {
                return cur != tail;
            }
C
CyC2018 已提交
142

C
CyC2018 已提交
143 144 145 146 147 148 149 150
            @Override
            public K next() {
                Node node = cur;
                cur = cur.next;
                return node.k;
            }
        };
    }
C
CyC2018 已提交
151 152 153
}
```

C
CyC2018 已提交
154
# 三、缓存位置
C
CyC2018 已提交
155

C
CyC2018 已提交
156
## 浏览器
C
CyC2018 已提交
157

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

C
CyC2018 已提交
160
## ISP
C
CyC2018 已提交
161

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

C
CyC2018 已提交
164
## 反向代理
C
CyC2018 已提交
165

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

C
CyC2018 已提交
168
## 本地缓存
C
CyC2018 已提交
169

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

C
CyC2018 已提交
172
## 分布式缓存
C
CyC2018 已提交
173

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

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

C
CyC2018 已提交
178
## 数据库缓存
C
CyC2018 已提交
179

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

C
CyC2018 已提交
182
# 四、CDN
C
CyC2018 已提交
183

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

C
CyC2018 已提交
186
CDN 主要有以下优点:
C
CyC2018 已提交
187

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

C
CyC2018 已提交
192
![](index_files/15313ed8-a520-4799-a300-2b6b36be314f.jpg)
C
CyC2018 已提交
193

C
CyC2018 已提交
194
# 五、缓存问题
C
CyC2018 已提交
195

C
CyC2018 已提交
196
## 缓存穿透
C
CyC2018 已提交
197

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

解决方案:

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

C
CyC2018 已提交
205
## 缓存雪崩
C
CyC2018 已提交
206

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

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

解决方案:

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

C
CyC2018 已提交
217
## 缓存一致性
C
CyC2018 已提交
218 219 220 221 222

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

解决方案:

C
CyC2018 已提交
223 224
- 在数据更新的同时立即去更新缓存;
- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。
C
CyC2018 已提交
225 226 227

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

C
CyC2018 已提交
228
# 六、数据分布
C
CyC2018 已提交
229

C
CyC2018 已提交
230
## 哈希分布
C
CyC2018 已提交
231

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

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

C
CyC2018 已提交
236
## 顺序分布
C
CyC2018 已提交
237

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

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

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

C
CyC2018 已提交
245
# 七、一致性哈希
C
CyC2018 已提交
246

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

C
CyC2018 已提交
249
## 基本原理
C
CyC2018 已提交
250

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

C
CyC2018 已提交
253
![](index_files/68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg)
C
CyC2018 已提交
254

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

C
CyC2018 已提交
257
![](index_files/66402828-fb2b-418f-83f6-82153491bcfe.jpg)
C
CyC2018 已提交
258

C
CyC2018 已提交
259
## 虚拟节点
C
CyC2018 已提交
260 261 262

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

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

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

C
CyC2018 已提交
267
# 参考资料
C
CyC2018 已提交
268

C
CyC2018 已提交
269 270 271 272 273
- 大规模分布式存储系统
[缓存那些事](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/)