缓存.md 9.8 KB
Newer Older
C
CyC2018 已提交
1 2 3 4 5 6 7 8
<!-- GFM-TOC -->
* [一、缓存特征](#一缓存特征)
* [二、LRU](#二lru)
* [三、缓存位置](#三缓存位置)
* [四、CDN](#四cdn)
* [五、缓存问题](#五缓存问题)
* [六、数据分布](#六数据分布)
* [七、一致性哈希](#七一致性哈希)
C
CyC2018 已提交
9
* [参考资料](#参考资料)
C
CyC2018 已提交
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
<!-- GFM-TOC -->


# 一、缓存特征

## 命中率

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

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

## 最大空间

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

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

## 淘汰策略

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

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

# 二、LRU

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

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

```java
public class LRU<K, V> implements Iterable<K> {

    private Node head;
    private Node tail;
    private HashMap<K, Node> map;
    private int maxSize;

    private class Node {

        Node pre;
        Node next;
        K k;
        V v;

        public Node(K k, V v) {
            this.k = k;
            this.v = v;
        }
    }

C
CyC2018 已提交
61

C
CyC2018 已提交
62 63 64 65 66 67 68 69 70 71 72 73
    public LRU(int maxSize) {

        this.maxSize = maxSize;
        this.map = new HashMap<>(maxSize * 4 / 3);

        head = new Node(null, null);
        tail = new Node(null, null);

        head.next = tail;
        tail.pre = head;
    }

C
CyC2018 已提交
74

C
CyC2018 已提交
75 76 77 78 79 80 81 82 83 84 85 86 87
    public V get(K key) {

        if (!map.containsKey(key)) {
            return null;
        }

        Node node = map.get(key);
        unlink(node);
        appendHead(node);

        return node.v;
    }

C
CyC2018 已提交
88

C
CyC2018 已提交
89 90 91 92 93 94 95 96 97 98 99 100 101
    public void put(K key, V value) {

        if (map.containsKey(key)) {
            Node node = map.get(key);
            unlink(node);
        }

        Node node = new Node(key, value);
        map.put(key, node);
        appendHead(node);

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

C
CyC2018 已提交
106

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

C
CyC2018 已提交
114

C
CyC2018 已提交
115 116
    private void appendHead(Node node) {
        node.next = head.next;
C
CyC2018 已提交
117
        node.next.pre = node;
C
CyC2018 已提交
118
        node.pre = head;
C
CyC2018 已提交
119 120 121
        head.next = node;
    }

C
CyC2018 已提交
122

C
CyC2018 已提交
123 124
    private Node removeTail() {
        Node node = tail.pre;
C
CyC2018 已提交
125
        tail.pre = node.pre;
C
CyC2018 已提交
126
        node.pre.next = tail;
C
CyC2018 已提交
127 128 129
        return node;
    }

C
CyC2018 已提交
130

C
CyC2018 已提交
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
    @Override
    public Iterator<K> iterator() {

        return new Iterator<K>() {
            private Node cur = head.next;
            @Override
            public boolean hasNext() {
                return cur != tail;
            }

            @Override
            public K next() {
                Node node = cur;
                cur = cur.next;
                return node.k;
            }
        };
    }
}
```

# 三、缓存位置

## 浏览器

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

## ISP

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

## 反向代理

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

## 本地缓存

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

## 分布式缓存

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

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

## 数据库缓存

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

# 四、CDN

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

CDN 主要有以下优点:

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

<div align="center"> <img src="../pics//15313ed8-a520-4799-a300-2b6b36be314f.jpg"/> </div><br>

# 五、缓存问题

## 缓存穿透

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

解决方案:

- 对这些不存在的数据缓存一个空数据;
- 对这类请求进行过滤。

## 缓存雪崩

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

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

解决方案:

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

## 缓存一致性

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

解决方案:

- 在数据更新的同时立即去更新缓存;
- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。

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

# 六、数据分布

## 哈希分布

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

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

## 顺序分布

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

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

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

# 七、一致性哈希

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

## 基本原理

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

<div align="center"> <img src="../pics//68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg"/> </div><br>

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

<div align="center"> <img src="../pics//66402828-fb2b-418f-83f6-82153491bcfe.jpg"/> </div><br>

## 虚拟节点

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

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

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

C
CyC2018 已提交
265
# 参考资料
C
CyC2018 已提交
266

C
CyC2018 已提交
267 268
- 大规模分布式存储系统
- [缓存那些事](https://tech.meituan.com/cache_about.html)
C
CyC2018 已提交
269
- [一致性哈希算法](https://my.oschina.net/jayhu/blog/732849)
C
CyC2018 已提交
270 271
- [内容分发网络](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/)