说到缓存,作为java开发第一时间想到的是不是上图所示的Redis,又或者是Guava Cache、Caffeine、EhCache这些;Redis作为分布式缓存、其他的可以作为本地缓存。但是作为一名资深开发人员,着眼的层面应该再提升一个级别,从结构层面去考虑缓存,其实缓存指的是“多级缓存”。我们所说的Java Web应用,在当前技术栈下指的是基于springcloud的微服务应用,如下图微服务请求响应示意图所示,从客户端到服务端每个环节都有缓存。随着互联网业务的增长,微服务架构为了应对三高(高可用、高性能、高并发)中的高性能、高并发,需要引入“缓存”来应对高并发问题,从而达到高性能的目的。因此,在微服务架构中、要聊缓存、需要了解一下软件架构“三高”中的高并发。
微服务请求响应示意图
软件架构“三高”特性
- 高并发(High Concurrency):高并发指的是系统能够同时处理大量的用户请求或操作。在高并发环境下,系统需要有效地管理资源,如线程和数据库连接,以便同时服务于大量用户或执行大量任务,而不会降低性能或导致服务中断。
- 高可用(High Availability):高可用性指的是系统能够持续不断地为用户提供服务,即使面临部分故障或维护操作。这通常通过冗余设计(如多服务器、负载均衡、故障转移等)实现,确保系统的关键部分在任何时候都有一个备份可以接管工作。
- 高性能(High Performance):高性能涉及到系统响应用户请求的速度和处理数据的能力。这不仅包括快速响应用户的交互请求,还包括在后端处理大量数据时的效率。提升性能通常涉及到优化代码、使用高效的算法、以及合理地利用硬件资源。
这三个特性相互依赖,是打造健壮系统的基础。例如,提高系统的并发能力可能需要牺牲一部分性能;同样地,为了保证高可用性,可能需要投入更多的硬件资源和复杂的系统设计,这也可能影响性能。因此,在设计和优化系统时,需要在这三个方面之间找到一个平衡点。
高性能
什么是高性能呢?高性能是指程序处理速度非常快,所占内存少,cpu占用率低。高性能的指标经常和高并发的指标紧密相关,想要提高性能,那么就要提高系统发并发能力,两者互相捆绑在一起。应用性能优化的时候,对于计算密集型和IO密集型还是有很大差别,需要分开来考虑。还有可以增加服务器的数量,内存,IO等参数提升系统的并发能力和性能,但不要浪费资源,要考虑硬件的使用率最高才能发挥到极致。
怎么样提高性能呢?
避免因为IO阻塞让CPU闲置,导致CPU的浪费避免多线程间增加锁来保证同步,导致并行系统串行化免创建、销毁、维护太多进程、线程,导致操作系统浪费资源在调度上。
高可用
高可用通常来描述一个系统经过专门的设计,从而减少停工时间,而保持其服务的高度可用性。高可用注意如果使用单机,一旦挂机将导致服务不可用,可以使用集群来代替单机,一台服务器挂了,还有其他后备服务器能够顶上。或者使用分布式部署项。比如现在redis的高可用的集群方案有: Redis单副本,Redis多副本(主从),Redis Sentinel(哨兵),Redis Cluster,Redis自研。
高并发的场景
高并发系统是指在同一时间内有大量用户同时访问和操作的应用软件系统。例如:互联网(热门门户网站、微博、社交媒体、电商)、金融、游戏等领域。由于用户量大、访问频繁,高并发系统需要具备高性能、高可用性、高扩展性等特点,以满足用户的需求。
例如,在电商平台上有大量用户同时浏览、搜索商品,提交订单等操作;社交媒体平台上有大量用户同时发布、点赞、评论等操作。这些场景需要系统能够同时处理大量请求,并保证系统的性能、可用性和用户体验。
高并发带来的问题
1. 请求延迟(系统性能的下降和延迟增加)
在高并发系统中,请求延迟是一个常见的问题。由于大量的用户同时发起请求,系统需要处理大量的请求,导致请求的响应时间变长,甚至出现请求超时的情况。这种情况不仅影响用户体验,还可能对业务造成影响。
2. 资源竞争(资源竞争和资源耗尽、系统性能的下降和延迟增加)
由于多个线程或进程同时访问共享资源,高并发系统容易出现资源竞争的问题。当多个请求同时访问同一份资源时,系统需要对其进行加锁或同步处理,以避免数据不一致和冲突。如果资源竞争过于激烈,会导致系统性能下降,甚至出现死锁和崩溃的情况。
3. 数据库瓶颈(资源竞争和资源耗尽、系统性能的下降和延迟增加)
许多高并发系统都会使用数据库来存储和处理数据。在大量用户同时访问和操作时,数据库容易成为系统的瓶颈。数据库的读写性能、连接数和数据量等都可能成为制约系统性能的因素。如果数据库无法承受高并发请求,会导致查询速度变慢、数据丢失等问题。
4. 系统宕机(系统稳定性和可用性的挑战)
在高并发场景下,如果系统的容量和性能没有得到充分的规划和设计,一旦遭遇流量高峰,系统容易宕机。此外,系统的硬件故障、网络故障以及软件缺陷等也可能导致系统宕机。系统宕机不仅影响用户体验,还可能对业务造成重大损失。
高并发的特点
- 大量请求:高并发场景下,系统需要同时处理大量的请求,这些请求可能来自于不同的用户或客户端。
- 同时访问:这些请求几乎同时到达系统,需要在短时间内进行处理和响应。
- 资源竞争:由于大量请求同时到达,系统的资源(如CPU、内存、网络带宽等)可能会面临竞争和争夺。
- 响应时间要求高:高并发场景通常对系统的响应速度有较高的要求,用户期望能够快速获取响应结果
高并发的指标
高并发是现在互联网分布式框架设计必须要考虑的因素之一,它是可以保证系统能被同时并行处理很多请求,对于高并发来说,它的指标有:
- 响应时间:系统对进来的请求反应的时间,比如你打开一个页面需要1秒,那么这1秒就是响应时间。
- 吞吐量:吞吐量是指每秒能处理多少请求数量,好比你吃饭,每秒能吃下多少颗米饭。秒查询率:秒查询率是指每秒响应请求数,和吞吐量差不多。
- 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。
高并发应对策略:缓存、限流、降级
- 缓存:缓解系统负载压力,提高系统响应速度。Java web应用性能分析之【高并发之缓存-多级缓存】-CSDN博客
- 限流:控制并发访问量,保护系统免受过载影响。Java web应用性能分析之【高并发之限流】-CSDN博客
- 降级:保证核心功能的稳定性,舍弃非关键业务或简化处理。Java web应用性能分析之【高并发之降级】-CSDN博客
缓存定义
缓存是一种临时存储数据的技术,意味着在数据被使用之前将其复制到一个更快的存储介质中。在计算机领域,缓存一般用于提高系统的响应速度和性能。
缓存是一种提高系统性能的技术,但需要适时清理以避免问题。删除缓存可以释放存储空间并保持系统的稳定性,但也可能带来一些不便。因此,在删除缓存之前,我们需要慎重考虑,并了解系统的特殊需求。
1.磁盘缓存:存储在硬盘等永久性存储介质上,用于加速数据的读取和访问。
2.CPU缓存:位于处理器内部的高速存储器,用于暂时存储频繁访问的数据或指令,提高计算机的性能。
3.应用缓存:存储在内存中的应用程序数据或资源,用于提高应用程序的响应速度和用户体验。在这里,下面讲的“微服务架构下的缓存”即是应用缓存。
微服务架构下的缓存
如下图“微服务请求响应示意图”所示,从web请求响应这个链路来说,每个节点都有自己的缓存,所以将缓存又叫做“多级缓存”,这样更具体。从前到后的多级缓存包括:浏览器缓存----》CDN缓存---》Nginx缓存-----》分布缓存------》java进程内的缓存
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能:
- 浏览器访问静态资源时,优先读取浏览器本地缓存
- 访问非静态资源(ajax查询数据)时,访问服务端
- 请求到达Nginx后,优先读取Nginx本地缓存
- 如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat)
- 如果Redis查询未命中,则查询Tomcat
- 请求进入Tomcat后,优先查询JVM进程缓存
- 如果JVM进程缓存未命中,则查询数据库
各种介质数据访问延迟
操作类型 | 粗略时间 |
---|---|
访问本地内存 | 100ns |
SSD磁盘搜索 | 100,000ns |
网络数据包在同一个数据中心来回一次的时间 | 500,000ns |
非SSD磁盘搜索 | 10,000,000ns |
按顺序从网络读取1MB数据 | 10,000,000ns |
按顺序从非SSD磁盘读取1MB数据 | 30,000,000ns |
跨大西洋网络数据包来回一次的时间 | 150,000,000ns |
跨太平洋网络数据包来回一次的时间 | 300,000,000ns |
每秒等于多少 | 1,000,000,000ns |
- 美国访问中国的数据中心网络延迟就有300ms
- redis/memcached一次请求(1-2k)大概耗时0.5ms
- 加索引的数据库的一次请求(1-2k)大概耗时50ms,是缓存的100倍
技术栈各个层次的缓存
缓存为什么显著提升性能
- 缓存数据通常来自内存,比磁盘等其他介质有更快的访问速度
- 缓存的数据通常是终态,不需要中间计算,节省了CPU资源消耗
- 缓存降低了数据库、磁盘、网络的负载压力,使得这些IO设备能获得更好的响应特性,提升系统整体性能
缓存不适宜场景
- 频繁修改的数据
此类型数据应用还来不及读取就失效了徒增系统负担,一般数据的读写比在2:1以上,缓存才有意义。
- 没有热点的访问
缓存使用内存存储,内存资源有限且宝贵,如果数据没有二八定律即大部分访问集中在小部分数据上,则缓存效果不会明显
- 数据不一致与脏读不允许
一般会对缓存数据设置过期时间,过期时间内可能会和数据库不一致,如果业务容忍则也可以使用,如果业务不能容忍,则需要数据在数据库变更时也要清除缓存。
客户端缓存--浏览器缓存
浏览器缓存是指将网页中的资源(如HTML、CSS、JavaScript、图像等)存储在用户的浏览器内部,以便在后续请求同一资源时可以直接从本地缓存中获取,而无需再次从服务器下载。
适用场景
浏览器缓存适用于那些静态内容变化较少的网页和静态资源,可以显著提升网站性能和用户体验,并减少服务器的负载。
常见用法
使用浏览器缓存可以通过设置响应头中的Expires和Cache-Control字段来控制缓存的行为。
1.使用Expires字段:Expires字段指定了缓存的过期时间,是一个具体的日期和时间。服务器可以在响应头中添加Expires字段,告诉浏览器在该时间之前可以直接从缓存中获取资源,而无需再向服务器发起请求。例如:Expires: Mon, 31 Dec 2022 23:59:59 GMT。
2.使用Cache-Control字段:Cache-Control字段提供了更灵活的缓存控制选项。可以通过设置max-age指令来指定缓存的最大有效时间,单位是秒。例如:Cache-Control: max-age=3600 表示资源可以在1小时内直接从缓存中获取。还可以使用其他指令,如no-cache表示缓存但不使用缓存、no-store表示禁止缓存等。
注意事项
浏览器缓存存储实时性不敏感的数据,如商品框架、商家评分、评价和广告词。它有过期时间,并通过响应头进行控制。实时性要求高的数据不适合使用浏览器缓存。
服务端缓存--CDN缓存
CDN(Content Delivery Network)是建立在承载网之上的分布式网络,由分布在不同区域的边缘节点服务器组成。
CDN缓存通常用于存放静态页面数据、活动页面、图片等数据。它有两种缓存机制:推送机制(将数据主动推送到CDN节点)和拉取机制(首次访问时从源服务器获取数据并存储在CDN节点)。
缓存相关常见问题_CDN(CDN)-阿里云帮助中心
服务端缓存--Nginx缓存
第一步:客户端第一次向Nginx请求数据A;
第二步:当Nginx发现缓存中没有数据A时,会向服务端请求数据A;
第三步:服务端接收到Nginx发来的请求,则返回数据A到Nginx,并且缓存在Nginx;
第四步:Nginx返回数据A给客户端应用;此时状态码是200
第五步:客户端第二次向Nginx请求数据A;
第六步:当Nginx发现缓存中存在数据A时,则不会请求服务端;
第七步:Nginx把缓存中的数据A返回给客户端应用。此时状态码是304
Nginx-cache配置
Nginx通过proxy_cache来实现缓存。Buffer(缓冲)主要用于传输效率不同步或者优先级不相同的设备之间传输数据,通过对一方数据进行临时存放,在统一发送的方式传递给另一方,以降低进程间的等待时间;Cache(缓存)主要用于将硬盘上已有的数据在内存中建立缓存数据,提高数据的访问效率。
而proxy_cache只有在Proxy Buffer机制开启的情况下Proxy Cache的配置才会发挥作用
相关配置
-
proxy_zone:zone | off 默认是off,即关闭proxy_cache功能,zone为用于存放缓存的内存区域名称,可以在http/server、location块内使用
- proxy_cache_path: path [levels=levels] keys_zone-name:size [inactive=time] [max_size=size] 只能在http块内使用
-
path设置缓存数据存放的路径
-
levels设置目录层级,如levels=1:2,表示有两个子目录
-
keys_zone 设置内存zone的名称和大小,如keys_zone=my:10m
-
inactive设置缓存多长时间失效,当磁盘上的缓存数据在该时间段内没有被访问过,就会失效,数据将被删除,默认10s
-
max_size 设置硬盘中最多缓存多少数据,数据超出,则删除最少访问的数据
-
-
proxy_cache_methods GET HEAD POST 设置缓存哪些方法
-
proxy_cache_min_uses 1 设置缓存的最小使用次数
-
proxy_cache_valid code time 对不同的状态码缓存不同的时间
-
proxy_cache_key line 设置缓存的key值
http {proxy_cache_path /var/www/cache #缓存地址levels=1:2 #目录分级keys_zone=test_cache:10m #开启的keys空间名字:空间大小(1m可以存放8000个key)max_size=10g #目录最大大小(超过时,不常用的将被删除)inactive=60m #60分钟内没有被访问的缓存将清理use_temp_path=off; #是否开启存放临时文件目录,关闭默认存储在缓存地址server {# 使用缓存location / {proxy_cache test_cache; #开启缓存对应的名称,在keys_zone命名好proxy_cache_valid 200 304 12h; #状态码为200 304的缓存12小时proxy_cache_valid any 10m; #其他状态缓存10分钟proxy_cache_key $host$uri$is_args$args; #设置key值add_header Nginx-Cache "$upstream_cache_status";}#不使用缓存if ($request_uri ~ ^/(login|register) ) { #当请求地址有login或register时set $nocache = 1; #设置一个自定义变量为true}location / {proxy_no_cache $nocache $arg_nocache $arg_comment;proxy_no_cache $http_pragma $http_authoriztion;}}
}expires配置
location ~ .*.(jpg|jpeg|gif|png)$ {# 设置图片缓存过期时间expires 1d;
} # 匹配静态目录
location ~^ /(|css|js) / {expires 2h;
}location ~ .*\.(?:htm|html)$ #不缓存html{add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";}}响应会返回给浏览器Expires属性,展示的是过期时间,之后再次请求该资源时,如果没有超过响应返回的Expires时,则不需要向服务器访问,直接从缓存中获取静态资源缓存
# 缓存zone levels表示缓存层级以及目录位数 keys_zone表示缓存内存大小 inactive有效期 max_size所占用的最大磁盘大小
proxy_cache_path /data/nginx/cache_ad levels=1:2 keys_zone=cache_cache_ad:100m inactive=7d max_size=200m;server {location / {# 缓存zoneproxy_cache cache_ad;# 缓存key 进行md5proxy_cache_key $host$uri$is_args$args;# 什么情况下进行缓存存储proxy_cache_valid 200 304 12h;}
}
检查是否使用Nginx缓存
如果需要知道Nginx是否使用了缓存,你可以在响应头中加入$upstream_cache_status变量以进行检测。
add_header X-Cache-Status $upstream_cache_status;
这是在Nginx添加缓存命中状态的add_header指令。
此示例X-Cache-Status
在响应客户端时添加HTTP标头。以下是$upstream_cache_status
可能的值。
MISS
在缓存中找不到响应,因此从原始服务器获取响应。然后缓存响应。HIT
响应直接来自有效的缓存。BYPASS
响应是从原始服务器获取的,而不是从缓存中提供的,因为请求与proxy_cache_bypass
指令匹配。EXPIRED
缓存中的记录已过期。响应包含来自原始服务器的新内容。STALE
内容过时,因为源服务器未正确响应但proxy_cache_use_stale
已配置。UPDATING
缓存内容已过时,因为当前正在更新以响应先前的请求,并且proxy_cache_use_stale updating
已配置。REVALIDATED
-proxy_cache_revalidate
指令已启用,Nginx验证当前缓存的内容是否仍然有效通过If-Modified-Since
或If-None-Match
。
服务端缓存--分布式缓存
分布式寻址算法是分布式对象缓存的关键,即缓存键如何分布到不同服务器,集群增加节点时如何处理
均匀hash算法
- 针对一个key,计算hashcode,然后在对节点数量取模,完成寻址。
- 当新增节点或某个节点故障时,会有大量key的缓存失效,给数据库带来压力。
一致性hash算法
- 解决分布式缓存集群扩容时数据访问不一致问题的算法,防止缓存雪崩。
- 实现步骤
- 构建一个一致性hash环(0-(232-1),也是hashcode的范围即4个字节的范围)
- 首先根据node的hashcode把node加入到环上
- 再根据key的hashcode把key加入到环上
- 最后沿着环按顺时针找到最近的node完成寻址
- 当新增节点或某个节点故障时,仅有少量的key的缓存失效,把压力降到最低。
- 但此算法的缺点就是node的hashcode可能分布不均匀导致负载不均衡,需要基于虚拟节点的一致性hash
基于虚拟节点的一致性hash算法
- 把node拆分成M个虚拟节点(nodeN_0......nodeN_M)
- 然后把虚拟节点按hashcode放入hash环,解决均衡问题
- 虚拟节点越多,对增减节点时缓存失效的概率越低,同时算法的效率也会降低,综合起来M应该在150-200
Redis VS Memcached
- Redis支持复杂的数据结构,Memcached只支持字符串
- Redis支持多路复用、异步IO保证高性能
- Redis支持主从复制保证高可用
- Redis原生支持集群模式
Redis集群
- 集群预分好16384个slot,根据CRC16(key) mod 16384的值决定key放入哪个slot,结果是均匀的
- Redis-cluster把所有物理节点映射到[0-16383]slot上,并负责维护slot与服务器的映射关系
- slot使得增/删节点变得非常简单,可以像磁盘分区一样自由分配slot,在配置文件里可显示指定也可默认
- 管理员可以根据机器的配置和负载情况进行slot的动态调整,基本上解决了最开始的负载均衡问题
- 当新增节点时,各节点会分一些slot到新节点。当删除节点时,该节点上的slot也会分给其他节点
- 所有的Redis节点彼此互联,客户端连接集群上任何一个可用节点即可
服务端缓存--java进程内的缓存(本地缓存)
在Java中,本地缓存通常是指在应用程序中缓存数据的一种方式,以便更快地访问经常需要的数据。这种缓存通常是在应用程序的生命周期内有效的。
在JAVA里一般我们有几种类型的应用缓存可以选择,比如JAVA本身提供的缓存(HashMap,ConcurrentHashMap)、还有第三方提供的应用缓存Ecache、 Guava 、caffeine。因为JAVA提供的缓存只提供了简单的缓存增加删除机制,基本上没有任何扩展的功能,所以就衍生了一批第三方的应用缓存,第三方应用缓存提供了丰富的扩展功能,比如说我们上面的缓存自动失效机制、缓存事件订阅机制,这些扩展机制能给开发人员管理缓存提供很大的便捷性。
本地缓存对比
框架 | 命中率 | 速度 | 回收算法 | 使用难度 | 集群 | 适用场景 | 内存 | 线程安全 |
---|---|---|---|---|---|---|---|---|
Guava cache | 中 | 第三 | LRU、LFU、FIFO | 易 | 不支持 | 读多写少,允许少量缓存偏移 | jvm堆内存 | |
Caffeine | 高 | 第一 | W-TinyLFU | 易 | 不支持 | 读多写少,允许少量缓存偏移,能用 Caffeine 就别用 Guava cache | jvm堆内存 | |
Ehcache | 中 | 第二 | LRU、LFU、FIFO | 中 | 支持 | 分布式系统中对数据一致性要求高 | jvm堆内存+直接内存+磁盘 | |
HashMap | ||||||||
ConcurrentHashMap |
引入缓存带来的问题
-
缓存预热
缓存中存放的是热点数据,热点数据通过LRU算法筛选出来的,整个过程时间比较长,过程内性能一般,需要在缓存系统启动时就把热点数据加载好就是缓存预热warm up,
-
缓存穿透
如果不恰当的业务或恶意请求持续高并发的请求某个不存在的缓存,如果缓存没有相应的对策,那所有的查询请求都落到数据库上,带来很大的压力,甚至崩溃。一个简单的对策是将不存在的数据也缓存起来值为null,并设置较短的过期时间
-
缓存雪崩
当缓存服务器崩溃时,请求压力全部打倒数据库,导致数据库也宕机,进而整个服务失效。发生这种问题时甚至不能简单的重启缓存服务器和数据库服务器来恢复。
-
缓存一致性:数据库和分布式缓存不一致
分两种情况,读数据和写数据(更新)
1. 读数据:读数据时候先读取缓存,如果缓存没有(miss hit)就读取数据库,然后在从数据库中取出数据并添加到缓存中,最后在返回数据给客户端。
2. 更新数据: 先更新数据库数据在删除缓存(也有人认为先删除缓存在更新数据库)。
-
缓存一致性:数据库和多级缓存不一致
但事无完美,当引入多级缓存后,我们又会遇到缓存数据一致性的挑战,以下图为例:
缓存一致性问题
我们都知道作为数据库写操作,是不通过缓存的。假设商品服务实例 1 将 1 号商品价格调整为 80 元,这会衍生一个新问题:如何主动向应用程序推送数据变更的消息来保证它们也能同步更新缓存呢?
相信此时你已经有了答案。没错,我们需要在当前架构中引入 MQ 消息队列,利用 RocketMQ 的主动推送功能来向其他服务实例以及 Redis 缓存服务发起变更通知。
通过 RocketMQ 解决保证消息一致性
如上图所示,在商品服务实例 1 对商品调价后,主动向 RocketMQ Broker 发送变更消息,Broker 将变更信息推送至其他实例与 Redis 集群,这些服务实例在收到变更消息后,在缓存中先删除过期缓存,再创建新的数据,以此保证各实例数据一致。
看到这里你会发现,对于缓存来说,并没有终极的解决方案。虽然多级缓存设计带来了更好的应用性能,但也为了缓存一致性必须引入 MQ 增加了架构的复杂度。那到底多级缓存设计该如何取舍呢?在我看来,有三种情况特别适合引入多级缓存。
-
第一种情况,缓存的数据是稳定的。例如邮政编码、地域区块、归档的历史数据这些信息适合通过多级缓存减小 Redis 与数据库的压力。
-
第二种情况,瞬时可能会产生极高并发的场景。例如春运购票、双 11 零点秒杀、股市开盘交易等,瞬间的流量洪峰可能击穿 Redis 缓存,产生流量雪崩。这时利用预热的进程内缓存分摊流量,减少后端压力是非常有必要的。
-
第三种情况,一定程度上允许数据不一致。例如某博客平台中你修改了自我介绍这样的非关键信息,此时在应用集群中其他节点缓存不一致也并不会带来严重影响,对于这种情况我们采用T+1的方式在日终处理时保证缓存最终一致就可以了。
以上是我总结的三种适合服务层做多级缓存的场景。当然如果你们的应用并发量不大,在未来的1~2 年内利用 Redis 分布式缓存集群完全可以胜任应用性能要求,那自然就没有必要设计多级缓存,我们要根据业务特点灵活调整架构。
-
缓存不可用之旁路缓存
缓存不可用时,不能阻塞正常业务请求。Redis 是一个独立的系统软件,和业务应用程序是两个软件,当我们部署了 Redis 实例后,它只会被动地等待客户端发送请求,然后再进行处理。所以,如果应用程序想要使用 Redis 缓存,我们就要在程序中增加相应的缓存操作代码。所以,我们也把 Redis 称为旁路缓存,也就是说,读取缓存、读取数据库和更新缓存的操作都需要在应用程序中来完成。