CS-Notes/docs/notes/缓存.md
2019-06-09 19:32:01 +08:00

314 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- GFM-TOC -->
* [缓存特征](#一缓存特征)
* [LRU](#二lru)
* [缓存位置](#三缓存位置)
* [CDN](#四cdn)
* [缓存问题](#五缓存问题)
* [数据分布](#六数据分布)
* [一致性哈希](#七一致性哈希)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 缓存特征
## 命中率
当某个请求能够通过访问缓存而得到响应时称为缓存命中
缓存命中率越高缓存的利用率也就越高
## 最大空间
缓存通常位于内存中内存的空间通常比磁盘空间小的多因此缓存的最大空间不可能非常大
当缓存存放的数据量超过最大空间时就需要淘汰部分数据来存放新到达的数据
## 淘汰策略
- FIFOFirst In First Out先进先出策略在实时性的场景下需要经常访问最新的数据那么就可以使用 FIFO使得最先进入的数据最晚的数据被淘汰
- LRULeast Recently Used最近最久未使用策略优先淘汰最久未使用的数据也就是上次被访问时间距离现在最久的数据该策略可以保证内存中的数据都是热点数据也就是经常被访问的数据从而保证缓存命中率
- LFULeast Frequently Used最不经常使用策略优先淘汰一段时间内使用次数最少的数据
# LRU
以下是基于 双向链表 + HashMap LRU 算法实现对算法的解释如下
- 访问某个节点时将其从原来的位置删除并重新插入到链表头部这样就能保证链表尾部存储的就是最近最久未使用的节点当节点数量大于缓存最大空间时就淘汰链表尾部的节点
- 为了使删除操作时间复杂度为 O(1)就不能采用遍历的方式找到某个节点HashMap 存储着 Key 到节点的映射通过 Key 就能以 O(1) 的时间得到节点然后再以 O(1) 的时间将其从双向队列中删除
```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;
}
}
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;
}
public V get(K key) {
if (!map.containsKey(key)) {
return null;
}
Node node = map.get(key);
unlink(node);
appendHead(node);
return node.v;
}
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();
map.remove(toRemove.k);
}
}
private void unlink(Node node) {
Node pre = node.pre;
Node next = node.next;
pre.next = next;
next.pre = pre;
node.pre = null;
node.next = null;
}
private void appendHead(Node node) {
Node next = head.next;
node.next = next;
next.pre = node;
node.pre = head;
head.next = node;
}
private Node removeTail() {
Node node = tail.pre;
Node pre = node.pre;
tail.pre = pre;
pre.next = tail;
node.pre = null;
node.next = null;
return node;
}
@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 响应允许进行缓存时浏览器会将 HTMLCSSJavaScript图片等静态资源进行缓存
## ISP
网络服务提供商ISP是网络访问的第一跳通过将数据缓存在 ISP 中能够大大提高用户的访问速度
## 反向代理
反向代理位于服务器之前请求与响应都需要经过反向代理通过将数据缓存在反向代理在用户请求反向代理时就可以直接使用缓存进行响应
## 本地缓存
使用 Guava Cache 将数据缓存在服务器本地内存中服务器代码可以直接读取本地内存中的缓存速度非常快
## 分布式缓存
使用 RedisMemcache 等分布式缓存将数据缓存在分布式缓存系统中
相对于本地缓存来说分布式缓存单独部署可以根据需求分配硬件资源不仅如此服务器集群都可以访问分布式缓存而本地缓存需要在服务器集群之间进行同步实现难度和性能开销上都非常大
## 数据库缓存
MySQL 等数据库管理系统具有自己的查询缓存机制来提高查询效率
## Java 内部的缓存
Java 为了优化空间提高字符串基本数据类型包装类的创建效率设计了字符串常量池及 ByteShortCharacterIntegerLongBoolean 这六种包装类缓冲池
## CPU 多级缓存
CPU 为了解决运算速度与主存 IO 速度不匹配的问题引入了多级缓存结构同时使用 MESI 等缓存一致性协议来解决多核 CPU 缓存数据一致性的问题
# CDN
内容分发网络Content distribution networkCDN是一种互连的网络系统它利用更靠近用户的服务器从而更快更可靠地将 HTMLCSSJavaScript音乐图片视频等静态资源分发给用户
CDN 主要有以下优点
- 更快地将数据分发给用户
- 通过部署多台服务器从而提高系统整体的带宽性能
- 多台服务器可以看成是一种冗余机制从而具有高可用性
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/15313ed8-a520-4799-a300-2b6b36be314f.jpg"/> </div><br>
# 缓存问题
## 缓存穿透
指的是对某个一定不存在的数据进行请求该请求将会穿透缓存到达数据库
解决方案
- 对这些不存在的数据缓存一个空数据
- 对这类请求进行过滤
## 缓存雪崩
指的是由于数据没有被加载到缓存中或者缓存数据在同一时间大面积失效过期又或者缓存服务器宕机导致大量的请求都到达数据库
在有缓存的系统中系统非常依赖于缓存缓存分担了很大一部分的数据请求当发生缓存雪崩时数据库无法处理这么大的请求导致数据库崩溃
解决方案
- 为了防止缓存在同一时间大面积过期导致的缓存雪崩可以通过观察用户行为合理设置缓存过期时间来实现
- 为了防止缓存服务器宕机出现的缓存雪崩可以使用分布式缓存分布式缓存中每一个节点只缓存部分的数据当某个节点宕机时可以保证其它节点的缓存仍然可用
- 也可以进行缓存预热避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩
## 缓存一致性
缓存一致性要求数据更新的同时缓存数据也能够实时更新
解决方案
- 在数据更新的同时立即去更新缓存
- 在读缓存之前先判断缓存是否是最新的如果不是最新的先进行更新
要保证缓存一致性需要付出很大的代价缓存数据最好是那些对一致性要求不高的数据允许缓存数据存在一些脏数据
## 缓存 无底洞 现象
指的是为了满足业务要求添加了大量缓存节点但是性能不但没有好转反而下降了的现象
产生原因缓存系统通常采用 hash 函数将 key 映射到对应的缓存节点随着缓存节点数目的增加键值分布到更多的节点上导致客户端一次批量操作会涉及多次网络操作这意味着批量操作的耗时会随着节点数目的增加而不断增大此外网络连接数变多对节点的性能也有一定影响
解决方案
- 优化批量数据操作命令
- 减少网络通信次数
- 降低接入成本使用长连接 / 连接池NIO
# 数据分布
## 哈希分布
哈希分布就是将数据计算哈希值之后按照哈希值分配到不同的节点上例如有 N 个节点数据的主键为 key则将该数据分配的节点序号为hash(key)%N
传统的哈希分布算法存在一个问题当节点数量变化时也就是 N 值变化那么几乎所有的数据都需要重新分布将导致大量的数据迁移
## 顺序分布
将数据划分为多个连续的部分按数据的 ID 或者时间分布到不同节点上例如 User 表的 ID 范围为 1 \~ 7000使用顺序分布可以将其划分成多个子表对应的主键范围为 1 \~ 10001001 \~ 2000...6001 \~ 7000
顺序分布相比于哈希分布的主要优点如下
- 能保持数据原有的顺序
- 并且能够准确控制每台服务器存储的数据量从而使得存储空间的利用率最大
# 一致性哈希
Distributed Hash TableDHT 是一种哈希分布方式其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据迁移的问题
## 基本原理
将哈希空间 [0, 2<sup>n</sup>-1] 看成一个哈希环每个服务器节点都配置到哈希环上每个数据对象通过哈希取模得到哈希值之后存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg"/> </div><br>
一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点例如下图中新增节点 X只需要将它前一个节点 C 上的数据重新进行分布即可对于节点 ABD 都没有影响
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/66402828-fb2b-418f-83f6-82153491bcfe.jpg"/> </div><br>
## 虚拟节点
上面描述的一致性哈希存在数据分布不均匀的问题节点存储的数据量有可能会存在很大的不同
数据不均匀主要是因为节点在哈希环上分布的不均匀这种情况在节点数量很少的情况下尤其明显
解决方式是通过增加虚拟节点然后将虚拟节点映射到真实节点上虚拟节点的数量比真实节点来得多那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好从而使得数据分布也更加均匀
# 参考资料
- 大规模分布式存储系统
- [缓存那些事](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/)
<img width="580px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/other/公众号海报1.png"></img>