缓存那些事儿

Posted 越测越开森

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了缓存那些事儿相关的知识,希望对你有一定的参考价值。

一般而言,互联网应用(网站或App)的整体流程,可以概括为如图所示,用户请求从界面(浏览器或App界面)到网络转发、应用服务再到存储(数据库或文件系统),然后返回到界面呈现内容。

随着互联网的普及,内容信息越来越复杂,用户数和访问量越来越大,我们的应用需要支撑更多的并发量,同时我们的应用服务器和数据库服务器所做的计算也越来越多。但是往往我们的应用服务器资源是有限的,且技术变革是缓慢的,数据库每秒能接受的请求次数也是有限的(或者文件的读写也是有限的),如何能够有效利用有限的资源来提供尽可能大的吞吐量?一个有效的办法就是引入缓存,打破标准流程,每个环节中请求可以从缓存中直接获取目标数据并返回,从而减少计算量,有效提升响应速度,让有限的资源服务更多的用户。


一.缓存的特征


缓存也是一个数据模型对象,那么必然有它的一些特征:

命中率

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

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


最大空间

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

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

淘汰策略

FIFO(First In First Out)

先进先出策略,在实时性的场景下,需要经常访问最新的数据,那么就可以使用 FIFO,使得最先进入的数据(最晚的数据)被淘汰。

LRU(Least Recently Used)

最近最久未使用策略,优先淘汰最久未使用的数据,也就是上次被访问时间距离现在最久的数据。该策略可以保证内存中的数据都是热点数据,也就是经常被访问的数据,从而保证缓存命中率。

LFU(Least Frequently Used)

最不经常使用策略,优先淘汰一段时间内使用次数最少的数据。


二.缓存的位置

浏览器

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

ISP

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

反向代理

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

本地缓存

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

分布式缓存

使用 Redis、Memcache 等分布式缓存将数据缓存在分布式缓存系统中。

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

数据库缓存

mysql 等数据库管理系统具有自己的查询缓存机制来提高查询效率。

Java 内部的缓存

Java 为了优化空间,提高字符串、基本数据类型包装类的创建效率,设计了字符串常量池及 Byte、Short、Character、Integer、Long、Boolean 这六种包装类缓冲池。

CPU 多级缓存

CPU 为了解决运算速度与主存 IO 速度不匹配的问题,引入了多级缓存结构,同时使用 MESI 等缓存一致性协议来解决多核 CPU 缓存数据一致性的问题。


三.CDN

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

CDN 主要有以下优点:

更快地将数据分发给用户;

通过部署多台服务器,从而提高系统整体的带宽性能;

多台服务器可以看成是一种冗余机制,从而具有高可用性。


缓存那些事儿

四.缓存问题

缓存穿透

指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。

解决方案:

对这些不存在的数据缓存一个空数据;

使用位图对这类请求进行过滤。


缓存雪崩

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

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

解决方案:

为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;

为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。

也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。


缓存一致性

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

解决方案:

在数据更新的同时立即去更新缓存;

在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。

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


缓存 “无底洞” 现象

指的是为了满足业务要求添加了大量缓存节点,但是性能不但没有好转反而下降了的现象。

产生原因:缓存系统通常采用 hash 函数将 key 映射到对应的缓存节点,随着缓存节点数目的增加,键值分布到更多的节点上,导致客户端一次批量操作会涉及多次网络操作,这意味着批量操作的耗时会随着节点数目的增加而不断增大。此外,网络连接数变多,对节点的性能也有一定影响。

解决方案:

优化批量数据操作命令;

减少网络通信次数;

降低接入成本,使用长连接 / 连接池,NIO 等。


五.数据分布



哈希分布

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

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


顺序分布

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

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

(1)能保持数据原有的顺序;

(2)能够准确控制每台服务器存储的数据量,从而使得存储空间的利用率最大。


六.一致性哈希

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

基本原理

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


缓存那些事儿

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


虚拟节点

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

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


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


下面是基于 双向链表 + HashMap 的 LRU 算法实现,对算法的解释如下:

访问某个节点时,将其从原来的位置删除,并重新插入到链表头部。这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就淘汰链表尾部的节点。

为了使删除操作时间复杂度为 O(1),就不能采用遍历的方式找到某个节点。HashMap 存储着 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。

public class LRU  
         
           implements   Iterable 
          
            { 
           
         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(nullnull);
    tail = new Node(nullnull);

    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;
        }
    };
}

七.几种提升redis性能方式

  • 显示排行榜:Redis使用的是常驻内存的缓存,速度非常快。LPUSH用来插入一个内容ID,作为关键字存储在列表头部。LTRIM用来限制列表中的数据最多为5000。如果用户需要的检索的数据量超越这个缓存容量,这时才需要把请求发送到数据库。


  • 删除和过滤:如果一条数据被删除,可以使用LREM从缓存中彻底清除掉。

  • 排行榜及相关问题:排行榜(leader board)按照得分进行排序。ZADD命令可以直接实现这个功能,而ZREVRANGE命令可以用来按照得分来获取前100名的用户,ZRANK可以用来获取用户排名,非常直接而且操作容易。

  • 按照用户投票和时间排序:排行榜,得分会随着时间变化。LPUSH和LTRIM命令结合运用,把数据添加到一个列表中。一项后台任务用来获取列表,并重新计算列表的排序,ZADD命令用来按照新的顺序填充生成列表。列表可以实现非常快速的检索,即使是负载很重的站点。

  • 过期数据处理:使用Unix时间作为关键字,用来保持列表能够按时间排序。对current_time和time_to_live进行检索,完成查找过期数据的艰巨任务。另一项后台任务使用ZRANGE…WITHSCORES进行查询,删除过期的条目。

  • 特定时间内的特定数据:这是特定访问者的问题,可以通过给每次页面浏览使用SADD命令来解决。SADD不会将已经存在的成员添加到一个集合。

  • Pub/Sub:在更新中保持用户对数据的映射是系统中的一个普遍任务。Redis的pub/sub功能使用了SUBSCRIBE、UNSUBSCRIBE和PUBLISH命令,让这个变得更加容易。

  • 队列:在当前的编程中队列随处可见。除了push和pop类型的命令之外,Redis还有阻塞队列的命令,能够让一个程序在执行时被另一个程序添加到队列。

  • 实际工程中,对于缓存的应用可以有多种的实战方式,包括侵入式硬编码,抽象服务化应用,以及轻量的注解式使用等


八.缓存测试点

功能:

1.系统运行过程中,redis缓存数据生效。缓存的数据读取正确、数据写入落地正确,数据有效期设置合理。

2.redis集群分片策略验证正确。

3.缓存与数据库的数据一致性检测。

4.DB事务性导致回滚,缓存是否回滚,有没有产生脏数据。

5.注意测试环境与线上环境的区别,尤其是单例与集群分片、读写分离。尽量保持测试环境与线上一致或者是其缩小版。


自动化:

1.自动化用例中断言部分设计缓存层断言并且自动化框架本身对于断层层次可配置。


性能及稳定性:

1.关注业务本身应用场景及缓存结构,是否使用缓存。

2.预防缓存穿透、缓存雪崩、缓存击穿引发的系统风险。


扩容:

1.关注扩容方案设计、老数据备份策略、回滚方案

2.关注扩容后分片策略的变化

3.扩容后热点数据失效率或命中率以及对后端DB带来的压力





推荐阅读:





以上是关于缓存那些事儿的主要内容,如果未能解决你的问题,请参考以下文章

缓存那些事儿

UWP关于图片缓存的那些破事儿

说说 CDN缓存插件速度优化那些事儿

数据库事务那些事儿

初遇 Asp.net MVC 数据库依赖缓存那些事儿

关于O_DIRECT的那些事儿