关于后端:高并发系统设计之缓存

1次阅读

共计 9661 个字符,预计需要花费 25 分钟才能阅读完成。

本文已收录至 GitHub,举荐浏览 👉 Java 随想录

微信公众号:Java 随想录

原创不易,重视版权。转载请注明原作者和原文链接

这篇文章来聊聊缓存。在解决高流量的互联网利用时,缓存起着至关重要的作用,是优化网站性能的第一伎俩。

缓存能够显著地进步零碎的性能和用户体验,让访问速度更快。

提到缓存,咱们往往首先想到的就是 Redis。的确,Redis 是缓存最常见的实现伎俩,但 Redis 并不是「银弹」,在某些场景下 Redis 未必是最佳选项。

本文会介绍几种缓存计划,心愿能帮读者关上思路。具体理论利用场景中该应用哪种计划,各位读者见仁见智了。

Nginx 缓存

Nginx 能够通过「Proxy Buffer」和「Proxy Cache」实现缓存的目标。

Proxy Buffer

Nginx 的 Proxy Buffer 是用来长期存储从代理服务器收到的响应数据的。

在反向代理场景中,Nginx 会从后端服务器接管响应,而后再将这些响应发送给客户端。如果响应速度较慢,或者一次性数据量较大,可能会导致 Nginx 阻塞,不能及时处理其余申请。

这时候,Proxy Buffer 就显得十分有用,它能够暂存这部分数据,让 Nginx 可能持续解决其余申请。

举个例子,假如有一个大文件须要通过 Nginx 从后端服务器传输到用户浏览器。当 Nginx 从后端服务器获取该文件时,如果没有应用 Proxy Buffer,Nginx 就必须始终期待整个文件都传输结束,能力释放出来解决其余申请。

但如果启用了 Proxy Buffer,那么 Nginx 就能够把接管到的数据先寄存到 Buffer 里,而后逐渐传输给用户,同时也能解决其余申请。这样就大大提高了 Nginx 的并发解决能力。

Nginx 中 Proxy Buffer 默认是开启的,Proxy Buffer 相干参数次要有以下几个:

  • proxy_buffering:这是一个开关,用于管制是否启用 nginx 对后端服务器响应进行缓存。默认值为on
  • proxy_buffers:定义了须要应用多少个、每个多大的内存缓存区来解决响应。它的设置格局为 number size,比方 “8 4k” 或者 “4 8k”。nginx 的默认设置为 ”8 4k” 或 ”4 8k”,取决于操作系统的页面大小。
  • proxy_buffer_size:定义读取响应头部的缓冲区大小。这通常不须要批改,除非你预期会有十分大的响应头。这个大小也定义了一次能够读取的最大量数据,因而如果可能接管到超大的回应,则须要增大这个值。默认值为读取 proxy_buffers 的第一个参数。
  • proxy_busy_buffers_size:在 HTTP 响应从被代理服务器读入且尚未传送给客户端时,该值限度了能够在 busy buffer 中应用的内存数。这个值必须小于或等于 proxy_buffers 中提供的最大值。没有默认值,但必须设置为不大于 proxy_buffers 的大小。
  • proxy_temp_file_write_size:设置子申请写入临时文件时数据的大小。默认值是 8KB 或者与 proxy_buffers 雷同。
  • proxy_max_temp_file_size:设置能够存储在临时文件中的最大数据量,默认值为 1024MB。如果超过这个值,nginx 将开始删除旧的临时文件。
  • proxy_temp_path:定义了存储临时文件的门路。默认是零碎的长期目录。

联合下面参数,示例配置如下:

http {
    proxy_buffering on;
    proxy_buffer_size 4k;
    proxy_buffers 8 16k;
    proxy_busy_buffers_size 24k;
    proxy_temp_file_write_size 32k;
    proxy_max_temp_file_size 0;

    server {
        listen 80;
        location / {
            proxy_pass http://backend;
            proxy_temp_path /path/to/temp;
        }
    }
}

这个配置中,开启了 Nginx 对后端服务器响应的缓存。同时,设置了读取响应头部的缓冲区大小为 4KB,解决响应的内存缓冲区数量为 8 个,每个 16KB。在 HTTP 响应从被代理服务器读入且尚未传送给客户端时,能够在 busy buffer 中应用的内存数限度为 24KB。子申请写入临时文件时数据的大小设定为 32KB。并且设置了能够存储在临时文件中的最大数据量为 0,示意没有下限。而临时文件的存储门路则是 ”/path/to/temp”。

Proxy Cache

Proxy Cache 能够将代理的申请后果缓存下来,当有雷同的申请达到时,间接返回缓存的后果而不必再次向后端服务器申请,这大大减少了后端服务器的压力并进步了前端响应速度。

Proxy Cache 相干参数次要有以下几个:

  • proxy_cache_path:这个指令定义了缓存的存储门路和其余参数。例如,levels 定义了缓存目录的层次结构,keys_zone 定义了共享内存区域的名称和大小,用于存储缓存键表,而 max_size 则限度了缓存的最大大小。
  • proxy_cache:此参数设置应用哪个缓存。参数值应该与 proxy_cache_path 中定义的 keys_zone 雷同。
  • proxy_cache_valid:此参数指定不同 HTTP 响应状态码的缓存有效期。
  • proxy_cache_methods:确定哪种类型的申请会被缓存,默认只缓存 GET 和 HEAD。
  • proxy_cache_key:定义了用于存储每个响应的缓存键,如果没有默认设置,通常应用 URL 和 / 或 申请头作为键。
  • proxy_cache_use_stale:当谬误产生或更新的响应过期时,容许发送 ” 古老 ” 的响应给客户端,默认状况下是敞开的。
  • proxy_cache_background_update:此指令通知 Nginx 在后盾异步更新缓存项,当缓存项行将过期时,此指令能够确保客户端总是从缓存中获取响应,而不用期待新的响应,默认状况下是敞开的。
  • proxy_cache_lock:当多个雷同申请同时达到时,只容许一个申请更新缓存,其余申请将期待直到缓存更新实现,默认状况下是敞开的。

联合下面参数,示例配置如下:

http {
    proxy_cache_path /var/nginx/cache levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off;

    server {
        location / {
            proxy_cache my_cache;
            proxy_pass http://my_upstream;
            proxy_cache_key "$scheme$request_method$host$request_uri";
            proxy_cache_methods GET HEAD POST;
            proxy_cache_valid 200 302 10m;
            proxy_cache_valid 404      1m;
            proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
            proxy_cache_background_update on;
            proxy_cache_lock on;
        }
    }
}

这段配置会对通过反向代理的 GET、HEAD 和 POST 申请进行缓存,并设置了各种状态码的缓存无效工夫。它还启用了在后盾异步更新缓存项的性能,以及在有多个雷同申请时避免缓存雪崩的锁机制。

CDN 缓存

CDN,也就是内容散发网络(Content Delivery Network),它将网站的内容缓存在寰球范畴内的服务器上。当用户拜访网站时,CDN 会抉择与用户最近的服务器提供服务,从而缩小提早,放慢加载速度。

CDN 个别会对动态内容进行缓存,如 HTML 页面,CSS 样式表,Javascript 脚本,图片和视频等,因为这些文件类型的内容在给定的工夫内变动不大。

对于如何设置 CDN,这通常波及以下步骤:

  1. 抉择一个 CDN 服务提供商:依据你的需要,比方地区笼罩、价格、个性等来抉择一个适合的 CDN 提供商。
  2. 注册和购买 CDN 服务:在选定的 CDN 服务提供商那里注册账户,并购买 CDN 服务。
  3. 配置 CDN:依据提供商的领导配置 CDN,通常包含指定你的原始服务器(origin server)以及哪些内容须要通过 CDN 来散发。
  4. 更新 DNS 记录:将你的网站域名的 DNS 记录指向 CDN 提供商。这样,当用户拜访你的网站时,他们会被重定向到最近的 CDN 边缘节点。

至于 CDN 缓存如何失效,个别是这样的:

  1. 用户申请一个网页或其余资源(如图像、视频、CSS 或 JS 文件等)。
  2. 如果这个申请的资源曾经在 CDN 的边缘节点被缓存了,那么 CDN 会间接将此资源提供给用户,这样就大大减少了响应工夫。
  3. 如果申请的资源没有在 CDN 节点被缓存,那么 CDN 会向原始服务器申请该资源,而后将它提供给用户,并在本地边缘节点上存储一份正本,以便下次有用户申请同样的资源时能够间接提供。

CDN 服务商通常会提供各种工具和选项来定制你的 CDN 缓存行为,例如设置某些资源的缓存持续时间(TTL),或者设置某些 URL 门路不应用缓存等。

堆缓存

Java 堆内存也能够用来存储缓存对象。

相比拟分布式缓存,应用堆缓存最大的益处就是没有序列化 / 反序列化的老本。当然毛病也很显著,当缓存的数据量很大时,GC(垃圾回收)暂停工夫会变长,存储容量受限于堆空间大小,并且堆缓存无奈被多个过程或者多个节点共享

堆缓存个别用来存储拜访频率很高的大量数据,个别通过软援用 / 弱援用来存储缓存对象,即当堆内存不足时,能够强制回收这部分内存开释堆内存空间。

堆缓存的实现能够非常简单,上面是一段简略的 Java 堆缓存(Heap Cache)代码示例。在这个例子中,咱们将应用 Java 内置的 LinkedHashMap 类实现一个简略的 LRU(最近起码应用)缓存:

import java.util.LinkedHashMap;
import java.util.Map;

public class HeapCache<K, V> extends LinkedHashMap<K, V> {
    private static final long serialVersionUID = -842570167583755019L;
    private final int capacity;

    public HeapCache(int capacity) {super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {return size() > capacity;
    }

    public static void main(String[] args) {HeapCache<Integer, String> cache = new HeapCache<>(2);
        cache.put(1, "a");
        cache.put(2, "b");
        cache.get(1);   // 拜访元素使其变为最新
        cache.put(3, "c"); // 这会使得键为 2 的元素被移除,因为它是最近起码应用的

        System.out.println(cache.keySet()); // 输入应为 [1, 3]
    }
}

除了 JDK 自带的类,咱们还能够借助一些框架来实现堆缓存。其中,Caffeine Cache 是极为举荐的抉择。对于如何集成 Caffeine Cache 的具体信息,能够参阅我之前的文章:本地缓存无冕之王 Caffeine Cache。

分布式缓存

分布式缓存相比于本地缓存有以下几个次要长处:

  • 可共享:分布式缓存能够被多个过程或服务共享,这是本地缓存无奈做到的。
  • 可扩展性:分布式缓存能够很容易地增加更多的节点到零碎中,以减少总体的缓存容量。这对于须要解决大量数据的利用是十分重要的。
  • 容错性:如果单个节点失败,那么该节点上的缓存数据能够从其余节点复制或从新计算。因而,分布式缓存能够提供比本地缓存更高的可用性和数据持久性。

当提及分布式缓存,置信大家会首先想到 Redis。这里简略介绍下如何应用 Redisson 客户端在 Java 中操作 Redis。

首先,咱们须要在我的项目的 pom.xml 文件中增加 Redisson 的依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version> 最新版本 </version>
</dependency>

下一步,咱们创立一个 Java 类用来连贯到 Redis 服务器并执行一些操作:

import org.redisson.Redisson;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonExample {public static void main(String[] args) {
        // 创立配置对象
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");

        // 依据配置,创立 RedissonClient 实例
        RedissonClient redissonClient = Redisson.create(config);

        // 获取 bucket 对象,并将其设置为 "Hello, Redisson"
        RBucket<String> bucket = redissonClient.getBucket("exampleBucket");
        bucket.set("Hello, Redisson");

        // 确认设置胜利
        System.out.println(bucket.get());

        // 敞开 RedissonClient 连贯
        redissonClient.shutdown();}
}

网上对于 Redis 的文章泛滥,这里只做下简略介绍,不多赘述。

数据库缓存

MySQL 查问缓存是一个可选的个性,它能够保留数据库查问后果,当雷同的查问再次发生时,间接从缓存中返回后果,缩小了数据检索的工夫。

然而,要留神的是,查问缓存对于常常更新的数据库可能不实用,因为每次表的数据更改时,所有针对该表的查问都须要从缓存中删除。

以下是如何开启和配置 MySQL 查问缓存:

  1. 在 MySQL 服务器配置文件(my.cnf 或 my.ini)中增加或批改如下设置:

    [mysqld]
    query_cache_size = 26214400
    query_cache_type = ON

    其中 query_cache_size 设置缓存大小(单位:字节),query_cache_type设置为 ON 来启用查问缓存。

  2. 重启 MySQL 服务以利用更改。

当查问缓存被启用和配置后,MySQL 会主动缓存查问后果,无需手动干涉。

在 MySQL 5.7 及其之前的版本中,查问缓存是默认开启的。但在 MySQL 8.0 及后续版本中曾经被移除,取而代之的是性能模式 (Performance Schema) 和信息模式(Information Schema)。

长处:

  • 对于动态或者很少更新的表,查问缓存能够显著进步查问性能。
  • 对于有大量反复查问的场景,例如网站页面渲染,查问缓存可能缩小数据库压力。

毛病:

  • 对于频繁更新的表,查问缓存可能导致性能降落。每次表数据更改时,针对该表的所有缓存查问后果都须要被革除。
  • 查问缓存占用了肯定的内存资源。

MySQL 的查问缓存在某些状况下能够显著进步数据库性能,但也有可能成为性能瓶颈。

在启用查问缓存前,首先明确是否真正须要它。如果你的数据库更多地进行读取操作而不是写入,且大部分查问反复率高,查问缓存可能会有所帮忙。如果你的数据库常常进行写入操作,查问缓存可能会导致性能降落,因为每次数据变动都须要革除或者更新缓存。

多级缓存

以上所述,每种缓存计划都有其长处和局限性,并无相对的好坏之分。应依据具体的利用场景,选取最适宜的缓存策略。

然而咱们能够综合多种缓存计划,以达到绝对最优的成果,这就是「多级缓存计划」。

实际上,在计算机操作系统中,咱们就能看到多级缓存的例子:

操作系统的多级缓存档次通常由以下几个档次组成:

  1. 寄存器:这是计算机最快的存储区域。它位于 CPU 外部,并被用于存储行将被解决的数据和指令。
  2. CPU 高速缓存:也称作 L1、L2 和 L3 缓存,这些缓存被用于存储 CPU 可能会频繁应用的数据和指令。L1 是最快但容量最小的缓存,而后是 L2,最初是 L3。
  3. 主存(RAM):这是计算机的次要内存区域,用于存储正在运行的程序和数据。
  4. 磁盘缓存:在磁盘和主存之间提供了一层缓存,以放慢对磁盘数据的拜访。
  5. 虚拟内存 / 硬盘:当主存有余时,操作系统会应用硬盘空间作为虚拟内存。

缓存档次顺次是:寄存器 -> CPU 高速缓存 -> 主存(RAM)-> 磁盘缓存 -> 虚拟内存 / 硬盘

每一层都具备不同的速度和容量,更靠近 CPU 的存储设备速度会更快,但容量绝对较小。操作系统的工作就是尽可能无效地治理这些档次,以保障最佳性能。

咱们能够借鉴这种多级缓存的思维。先来看下多级缓存的架构图:

在多级缓存架构中,常见的执行程序是 CDN 缓存 -> Nginx 缓存 -> Tomcat 堆内缓存 -> Redis 分布式缓存 -> 数据库

每一层都试图解决来自上一层的负载。如果某一层未能找到申请的数据(缓存未命中),则会将申请传递给下一层。

  1. CDN 缓存:这是最靠近用户的缓存档次,通常用于散发动态资源,如图片、CSS、JavaScript 等。当用户申请这类资源时,首先查看 CDN 是否有缓存。如果 CDN 有,则间接返回;否则,申请传递给下一层。
  2. Nginx 缓存:Nginx 是反向代理服务器,也能够配置为缓存服务器。如果 CDN 没有命中,申请就到了这里。如果 Nginx 能找到申请的资源,它会发送给用户,并可能更新 CDN。如果 Nginx 也没有,则申请传递给应用服务器。
  3. Tomcat 堆缓存:Tomcat 能够配置堆内缓存,次要用于频繁拜访的动静生成的数据。如果此层缓存没有命中,那么申请将转发到后端的数据服务层。
  4. Redis 分布式缓存:Redis 是一个高速的键 - 值数据库,罕用作分布式缓存。如果 Tomcat 堆内缓存未命中,申请就会查问 Redis。如果 Redis 有数据,则返回给 Tomcat,并可能将数据载入堆内缓存以备后用。如果 Redis 还是没有找到数据,那么只好拜访最初一层——数据库。
  5. 数据库:数据库是数据的落地存储,也是最牢靠的数据源。如果后面所有的缓存层都未能命中,那么申请将间接拜访数据库获取数据。而后,这个数据可能会被载入 Redis、Tomcat 堆内缓存,甚至更新 Nginx 和 CDN 缓存,以便下次申请更快地获取数据。

以上就是这种多级缓存架构的执行程序。要留神的是,每一个缓存层都是为了缩小对下一层的负载和进步数据访问速度。然而,为了维持数据的一致性,也须要适当的过期策略和缓存刷新机制。

热点 Key 主动探测

缓存最重要的指标就是命中率,甚至都没有之一。

而「热点数据」会频繁被拜访或应用,是最适宜被缓存的数据。

所以,如果说咱们能「预测热点数据」,就能最大水平无效地施展缓存的作用。这在技术层面上也的确是可行的。

咱们先来说说热点数据,热点数据其实能够分为:「动态热点数据 」和「 动静热点数据」。

所谓动态热点数据,就是可能提前预测的热点数据。

例如,咱们能够通过一次流动卖家报名的形式提前筛选进去,通过报名系统对这些热点商品进行打标。

另外,咱们还能够通过大数据提前剖析,比方咱们能够通过 Flink 或者离线工作去剖析用户历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖。

这些都是能够提前剖析进去的热点,剖析进去之后,咱们能够提前放入缓存中爱护起来,这在技术上是能够实现的。

而所谓动静热点数据,就是不能被提前预测到的,零碎在运行过程中长期产生的热点。

例如,抖音的一个热搜,可能导致某个周边相干产品被疯狂抢购,这是人为无奈预测的。

所以如何动静预测热点数据就成了咱们缓存零碎的要害。

这里介绍一种名为「动静热点 key 探测」的技术,给出其技术架构,心愿能给大家提供思路:

  1. 首先构建一个异步零碎,此零碎能够异步收集链路上各个环节的利用和中间件的热点 Key。如 Nginx、Redis、RPC 服务框架等这些中间件。能够应用 Nginx 的 UDP 间接上报申请,或者将申请写到本地 Kafka,或者应用 Flume 订阅本地 Nginx 日志等形式进行上报。
  2. 建设一个热点上报和能够依照需要订阅的热点服务的下发标准。

    次要目标是通过链路上各个系统(包含详情、购物车、交易、优惠、库存、物流等)拜访的时间差把上游曾经发现的热点透传给上游零碎,提前做好爱护。

    比方,对于大促高峰期,详情零碎是最早晓得的,咱们能够通过部署在每台机器上的 Agent 把日志汇总到聚合和剖析集群中,而后把合乎肯定规定的热点数据进行上报,或者是在对立接入层上应用 Nginx 模块统计热点 URL。

  3. 将上游零碎和中间件收集的热点数据发送到「实时热点发现零碎」,对于热点的统计能够很简略的对拜访的商品进行拜访计数,而后排序还有就是用通常的队列的淘汰算法如 LRU 等都能够实现。
  4. 实时热点发现零碎能够被动推送热点数据到中间件,或者上游零碎(如交易系统)按需订阅实时热点发现零碎。

    你能够是把热点数据填充到 Cache 中,或者间接推送到应用服务器的内存中,还能够对这些数据进行拦挡,总之上游零碎能够订阅这些数据,而后依据本人的需要决定如何解决这些数据。

整体架构图大抵如下所示:

留神:所提供的架构图仅供参考,实现办法多种多样,无需局限于特定的架构设计。

这个零碎可能实现的技术前提是「时间差」!。次要依赖后面的导购页面(包含首页、搜寻页面、商品详情、购物车等)提前辨认哪些商品的访问量高,通过这些零碎中的中间件或利用来收集热点数据,并进行记录。

举个例子,依照用户购买行为来说,咱们个别网购商品的时候,会关上商品具体页,或者商品评估来看看。这两头就存在时间差,这个时间差实时热点发现零碎始终在工作,可能预测用户「接下来可能的行为」,从而预测热点对利用进行爱护。

热点发现要做到靠近实时(3s 内实现热点数据的发现),因为只有做到靠近实时,动静发现才有意义,能力实时地对上游零碎提供爱护。若热点信息在 10 秒后才发送,其实已失去了意义。因为在这 10 秒内,用户能够执行多项操作。工夫越长,不可控因素增多,导致热点缓存命中率降落。

京东在网上开源了一个热点 key 探测的具体实现:https://gitee.com/jd-platform-opensource/hotkey,大伙有趣味能够参考下。

本篇文章,咱们探讨了高并发零碎设计中缓存的重要性。适当应用缓存能够显著进步零碎性能,并且能够对消因为大量申请造成的负载。

总体来说,缓存是一个弱小的工具,但要充分利用它,你须要具体地了解你的应用程序,包含哪些信息被频繁地读取,哪些信息更新的频率较高,以及在特定状况下可能会呈现的问题。

同时,记住缓存并不能解决所有问题。在设计高并发零碎时,咱们还须要思考数据库优化、负载平衡、分布式系统设计等其余方面。通过全方位地了解和利用这些准则,咱们能力创立出稳固、可扩大和高效的高并发零碎。

心愿这篇文章能为你在解决高并发零碎设计问题时提供有价值的参考和启发。当然,每个我的项目和场景都有其特定的需要和挑战,所以请继续学习和实际,不断改进你的设计策略


感激浏览,如果本篇文章有任何谬误和倡议,欢送给我留言斧正。

老铁们,关注我的微信公众号「Java 随想录」,专一分享 Java 技术干货,文章继续更新,能够关注公众号第一工夫浏览。

一起交流学习,期待与你共同进步!

正文完
 0