缓存的几个特征
命中:缓存使用一次就称为命中一次。
命中率:缓存使用次数越多,命中率越高。
最大空间:缓存一般存储在内存中,当缓存超过最大的存储空间的时候,需要按一定的规则淘汰数据。
淘汰策略:
策略 | 特点 |
FIFO(保存最新的数据) | 先入先出,优先淘汰最旧的数据,对于实时性比较高的数据大多采用这种形式。 |
LRU(保存命中率高的数据) | Least Recently Used淘汰最久未被使用的数据。这样,缓存中存储的是经常被访问的数据,也就是命中率最高的数据,可以保证缓存的命中率。 |
LFU(保存高频使用数据) | Least Frequently Used淘汰最近使用次数最少的数据。 |
LRU基于双向链表及HashTable的实现:
数据访问时,先从HashTable中获取key的位置,链表删除元素,从首部插入。这样最新访问的数据变成了链表最前面,最近没有使用的数据离尾部较近。
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;
}
};
}
}
缓存的类型
1. 浏览器
浏览器在允许缓存情况下会缓存html、css、js文件及图片等。
2.ISP
网络服务提供商处是接入网络的第一步,缓存在ISP可以提高响应速度。
3.反向代理
反向代理往往进行负载均衡,首先请求到达反向代理,如果缓存在反向代理将省去到达服务器的步骤
4.本地缓存
使用Guava Cache将数据缓存在服务器本地内存中,可以提高反应速度。
5.分布式缓存
使用Redis、Memcache等分布式缓存系统进行缓存。分布式缓存对于缓存的共享具有便利性,本地缓存适合单体应用。
6. 数据库缓存
Mysql等数据库有自己的查询缓存机制
7. Java缓存
java内部包含字符串常量池及Byte、Short、Character、Integer、Long、Boolean等六种包装类缓冲池。
8. CPU多级缓存
为了解决运算速度与IO速度不匹配的问题,使用多级缓存。并使用MESI等缓存一致性协议解决多核CPU的缓存数据一致性问题。
CDN
内容分发网络(Content distribution network):利用最近的服务器为用户服务,可以将html、css、js、音乐、视频、图片等静态文件分发给用户。
缓存问题
缓存穿透:
对某一个一定不存在的数据进行请求,该请求会穿透缓存到达数据库。
解决方法:
1. 对不存在的数据缓存一个空数据
2. 对此类请求进行过滤
缓存雪崩:
数据没有加载到缓存或者缓存在同一时间大面积失效,或缓存服务器宕机,导致大量请求压到数据库。
在有缓存的系统中,系统对于缓存的依赖性很强,缓存分担了很大一部分数据,当缓存不可用时,数据库无法处理大量的请求并发,导致数据库崩溃。
解决方法:
1. 合理设置缓存过期时间
2. 防止缓存服务器宕机,采用服务器集群,进行分布式缓存,每个节点只缓存部分数据,当某个节点宕机不至于服务不可用。
3. 缓存预热。避免在系统刚启动的时候没有将大量数据缓存,而导致缓存雪崩。
数据一致性:
缓存的一致性要求数据更新的时候,缓存也能够实时更新。
解决方法:
1. 在数据更新的同时立即去更新缓存
2. 在读取缓存之前先判断是否是最新的。如果不是最新的先更新。
保证缓存的一致性代价很高,一般是对一致性要求不高的数据,允许缓存数据存在一些脏数据。
缓存无底洞
为了提高性能,添加了很多缓存节点,但是性能不但没好转反而下降。
一次批量操作涉及到多个节点的缓存访问,导致产生多条网络请求。
解决办法:
1. 优化批量数据操作的命令
2. 减少网络通信次数
3. 降低接入成本,使用长连接、连接池、NIO等等
数据分布
哈希分布
将数据计算哈希值,将哈希值分布到不同的节点上,例如有N个节点,主键是Key,将数据分配到hash(key) % N的节点上
传统的哈希分布存在问题,当节点数发生变化的时候,几乎所有的数据需要重新计算位置,重新分配。
顺序分布
将数据划分到多个连续的部分,按照id或时间分布到不同的节点上,例如user的id范围是1-7000,可以划分多个子表,对应范围1-1000,1001-2000,2001-300···
顺序分布相对于哈希分布的优点:
1. 可以保持数据的原有顺序
2. 能够准确控制每台服务器存储的数据量,从而使存储空间的利用率最大。
一致性哈希
DHT(distribution Hash Table)
为了克服哈希分布在服务器节点数量发生变大较大时出现大量数据迁移问题,提出了一种哈希分布方式。
将0~(2^n)-1的数据空间看做哈希环,每个服务器节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值后,存放到顺时针方向第一个大于等于该哈希值的节点上。
当发生节点的变化时,只需要更新相邻节点的数据即可。
虚拟节点
当节点数较小的时候容易出现数据不均匀的现象,增设虚拟节点,通过将虚拟节点映射到真实节点上,让数据尽可能分布地均匀一些。