高并发系统设计之缓存

Posted 码农BookSea

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高并发系统设计之缓存相关的知识,希望对你有一定的参考价值。

本文已收录至Github,推荐阅读 👉 Java随想录

这篇文章来讲讲缓存。缓存是优化网站性能的第一手段,目的是让访问速度更快。

说起缓存,第一反应可能想到的就是Redis。目前比较好的方案是使用多级缓存,如CPU→Ll/L2/L3→内存→磁盘就是一个典型的例子,CPU需要数据时先从L1读取,如果没有找到,则查找L2/L3读取,如果没有,则到内存中查找,如果还没有,会到磁盘中查找。

文章目录

堆缓存

使用Java堆内存来存储缓存对象。使用堆缓存的好处是没有序列化/反序列化,是最快的缓存。缺点也很明显,当缓存的数据量很大时,GC(垃圾回收)暂停时间会变长,存储容量受限于堆空间大小。一般通过软引用/弱引用来存储缓存对象,即当堆内存不足时,可以强制回收这部分内存释放堆内存空间。一般使用堆缓存存储较热的数据。可以使用Caffeine Cache实现。

集成Caffeine Cache详情见我另外一片文章:本地缓存无冕之王Caffeine Cache

nginx代理缓存

Nginx 的Proxy Cache可以实现代理缓存,特别需要说明的是,Proxy Cache机制依赖于Proxy Buffer机制,只有在Proxy Buffer机制开启的情况下Proxy Cache的配置才发挥作用,Proxy Buffer默认是开启的,缓存内容将存放在tmpfs(内存文件系统)以提升性能。

Proxy Buffer相关参数:

proxy_buffering  on;
该参数设置是否开启proxy的buffer功能,参数的值为on或者off,默认是on。

proxy_buffer_size  4k;
该参数用来设置一个特殊的buffer大小的。
从被代理服务器上获取到的第一部分响应数据内容到代理服务器上,通常是header,就存到了这个buffer中。 
如果该参数设置太小,会出现502错误码,这是因为这部分buffer不够存储header信息。建议设置为4k。

proxy_buffers  8  4k;
这个参数设置存储被代理服务器上的数据所占用的buffer的个数和每个buffer的大小。
所有buffer的大小为这两个数字的乘积。

proxy_busy_buffer_size 16k;
在所有的buffer里,我们需要规定一部分buffer把自己存的数据传给服务器,这部分buffer就叫做busy_buffer。
proxy_busy_buffer_size参数用来设置处于busy状态的buffer有多大。

proxy_temp_path
语法:proxy_temp_path  path [level1 level2 level3]
定义proxy的临时文件存在目录以及目录的层级。

proxy_buffering 与 proxy_cache的区别:

缓冲(buffer)

  • Nginx 将上游服务器的响应报文保存几秒钟,等整个接收之后,再发送给客户端。
  • 可以尽早与上游服务器断开连接,减少其负载。但是会增加客户端等待响应的时间。
  • 如果不启用缓冲,则 Nginx 收到上游服务器的一部分响应就会立即发送给客户端,通信延迟低。

缓存(cache)

  • Nginx 将上游服务器的响应报文保存几分钟,当客户端再次请求同一个响应报文时就直接回复,不必请求上游服务器。
  • 可以避免重复向上游服务器请求一些固定不变的响应报文,减少上游服务器的负载,减少客户端等待响应的时间。

Proxy Cache 相关的参数配置:

  • proxy_cache_path:Nginx 使用该参数指定缓存位置,有两个必填参数, 第一个参数为缓存目录。 第二个参数keys_zone指定缓存名称和占用内存空间的大小。
  • proxy_cache:该参数为之前指定的缓存名称。
  • proxy_cache_key:该指令用来设置web缓存的Key值。
  • proxy_cache_min_uses:指定请求至少被发送了多少次以上时才缓存,可以防止低频请求被缓存。
  • proxy_cache_methods:指定哪些方法的请求被缓存。
  • proxy_cache_valid:该指令用于对于不同返回状态码的URL设置不同的缓存时间。
  • proxy_cache_bypass:指定Nginx使用缓存的条件。

如下示例:

http 

    proxy_cache_path /data/nginx/cache keys_zone=one:10m max_size=10g;
    proxy_cache_methods GET HEAD POST PUT;
    proxy_cache_min_uses 1;
    proxy_cache_valid 200 302 10m;
    proxy_cache_key "$host:$server_port$uri$is_args$args";

    upstream www.baidu.com 
        server 127.0.0.1:8880;
        server 127.0.0.1:8881;
        server 127.0.0.1:8882;
    
    server 
        listen 80 ;
        proxy_cache one ;
        server_name www.baidu.com;
        location / 
            proxy_pass http://www.baidu.com;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_cache_bypass $cookie_nocache $arg_nocache $arg_comment ;
        
    


多级缓存

Nginx缓存→分布式Redis缓存(可以使用Lua脚本直接在Nginx里读取Redis)→堆内存

  1. 接入 Nginx将请求负载均衡到应用Nginx,此处常用的负载均衡算法是轮询或者一致性哈希。轮询可以使服务器的请求更加均衡,而一致性哈希可以提升应用Nginx的缓存命中率。

  2. 应用Nginx读取本地缓存(本地缓存可以使用Lua Shared Dict,Nginx Proxy Cache(磁盘/内存)、Local Redis实现)。如果本地缓存命中,则直接返回,使用应用Nginx本地缓存可以提升整体的吞吐量,降低后端压力,尤其应对热点问题非常有效。

  3. 如果Nginx本地缓存没命中,则会读取相应的分布式缓存(如 Redis缓存,还可以考虑使用主从架构来提升性能和吞吐量),如果分布式缓存命中,则直接返回相应数据(并回写到Nginx本地缓存)。

  4. 如果分布式缓存也没有命中,则会回源到Tomcat集群,在回源到Tomcat 集群时,也可以使用轮询和一致性哈希作为负载均衡算法。

  5. 在 Tomcat应用中,首先读取本地堆缓存。如果有,则直接返回(并会写到主Redis集群)。

  6. 作为可选部分,如果步骤4没有命中,则可以再尝试一次读主Redis集群操作,目的是防止当从集群有问题时的流量冲击。

  7. 如果所有缓存都没有命中,则只能查询DB或相关服务获取相关数据并返回。

  8. 步骤7返回的数据异步写到主Redis集群。

整体分了三部分缓存:应用Nginx本地缓存、分布式缓存、Tomcat堆缓存。每一层缓存都用来解决相关问题,如应用Nginx本地缓存用来解决热点缓存问题,分布式缓存用来减少访问回源率,Tomcat堆缓存用于防止相关缓存失效/崩溃之后的冲击。

热点Key自动探测

热点数据其实可以分为:静态热点数据 和 动态热点数据

所谓“静态热点数据”,就是能够提前预测的热点数据。例如,我们可以通过卖家报名的方式提前筛选出来,通过报名系统对这些热点商品进行打标。另外,我们还可以通过大数据分析来提前发现热点商品,比如我们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是可以提前分析出来的热点,分析出来之后,我们可以提前放入缓存中保护起来,这在技术上是可以实现的。

所谓“动态热点数据”,就是不能被提前预测到的,系统在运行过程中临时产生的热点。例如,卖家在抖音上做了广告,然后商品一下就火了,导致它在短时间内被大量购买。

所以如何动态预测热点数据就成了我们缓存系统的关键

这里我给出一个动态热点发现系统的具体实现。

  1. 构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点Key,如Nginx、缓存、RPC服务框架等这些中间件(一些中间件产品本身已经有热点统计模块)。

  2. 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,主要目的是通过交易链路上各个系统(包括详情、购物车、交易、优惠、库存、物流等)访问的时间差把上游已经发现的热点透传给下游系统,提前做好保护。比如,对于大促高峰期,详情系统是最早知道的,在统一接入层上 Nginx模块统计的热点URL。

  3. 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护,对于热点的统计可以很简单的对访问的商品进行访问计数,然后排序还有就是用通常的队列的淘汰算法如lru等都可以实现。

实现思路步骤如下:
1.接入Nginx将请求转发给应用Nginx。
2.应用 Nginx首先读取本地缓存。如果命中,则直接返回,不命中会读取分布式缓存、回源到Tomcat进行处理。
3.应用Nginx 会将请求上报给实时热点发现系统,如使用UDP直接上报请求,或者将请求写到本地kafka,或者使用flume订阅本地 Nginx日志。上报给实时热点发现系统后,它将进行热点统计(可以考虑storm实时计算)。
4.根据设置的阈值将热点数据推送到应用Nginx本地缓存。

我们主要是依赖前面的导购页面(包括首页、搜索页面、商品详情、购物车等)提前识别哪些商品的访问量高,通过这些系统中的中间件来收集热点数据,并记录到日志中。

我们通过部署在每台机器上的Agent把日志汇总到聚合和分析集群中,然后把符合一定规则的热点数据,通过订阅分发系统再推送到相应的系统中。你可以是把热点数据填充到Cache中,或者直接推送到应用服务器的内存中,还可以对这些数据进行拦截,总之下游系统可以订阅这些数据,然后根据自己的需求决定如何处理这些数据。

热点发现要做到接近实时(3s内完成热点数据的发现),因为只有做到接近实时,动态发现才有意义,才能实时地对下游系统提供保护。如果10s内才发送热点就没意义了,因为10s内用户可以进行的操作太多了。时间越长,不可控元素越多,热点缓存命中率越低。

京东在网上开源了热点key探测技术的具体实现:https://gitee.com/jd-platform-opensource/hotkey?_from=gitee_search


本篇文章就到这里,感谢阅读,如果本篇博客有任何错误和建议,欢迎给我留言指正。

以上是关于高并发系统设计之缓存的主要内容,如果未能解决你的问题,请参考以下文章

并发编程高并发相关技术

十月阿里社招Java面试题:数据库+分布式+高并发+JVM+Spring

秒杀系统设计思路

史上最强大型分布式架构详解:高并发+数据库+缓存+分布式+微服务+秒杀

如何设计一个高并发系统

java多线程高并发的学习