java高速缓存技术,java常用缓存技术
java基础栏目今天介绍java高并发系统设计的缓存篇。
常见硬件组件的延迟如下:
从这些数据中,您可以看到,做一个内存地址大约需要100ns,而做一个磁盘搜索需要10ms。可以看出,使用内存作为缓存的存储介质的性能会比使用磁盘作为主要存储介质的数据库提高几个数量级。因此,内存是最常见的缓存数据的介质。
一、缓存案例
1、TLB
Linux内存管理通过一块叫做MMU(内存管理单元)的硬件实现虚拟地址到物理地址的转换。但是如果每次转换都要做这么复杂的计算,无疑会造成性能损失,所以我们会使用一个叫做TLB(Translation Lookaside Buffer)的组件来缓存最近转换的虚拟地址和物理地址。TLB是一个缓存组件。
2、抖音
平台上的短视频其实是用内置的网络播放器完成的。网络播放器接收数据流,下载数据,分离音视频流,解码,然后输出到外围设备播放。播放器通常会设计一些缓存组件,在视频未打开时缓存一些视频数据。例如,当我们打开Tik Tok时,服务器可能一次返回三条视频消息。在我们播放第一个视频的时候,播放器已经帮我们缓存了第二个和第三个视频的一些数据,这样在看第二个视频的时候,就能给用户“秒开”的感觉。
3、HTTP协议缓存
当我们第一次请求一个静态资源,比如一个图片,服务器返回图片信息,在响应头中也有一个“Etag”字段。浏览器缓存图片信息和该字段的值。下次请求该图像时,浏览器发起的请求头中将有一个“如果不匹配”字段,缓存的“Etag”值将被写入并发送到服务器。比较服务器的图片信息是否有变化,如果没有,会返回一个状态码304给浏览器,浏览器会继续使用缓存的图片信息。通过这种缓存协商的方式,可以减少网络传输的数据量,从而提高页面显示的性能。
二、缓存分类
1、静态缓存
静态缓存在Web 1.0时期非常有名。它通常通过生成Velocity模板或静态HTML文件来实现静态缓存。在Nginx上部署静态缓存可以减轻后台应用服务器的压力。
2、分布式缓存
分布式缓存的名称是众所周知的。我们平时熟悉的Memcached和Redis就是分布式缓存的典型例子。它们具有很强的性能,通过一些分布式方案进行集群可以突破单机的限制。因此,分布式缓存在整个体系结构中起着非常重要的作用。
3、本地缓存
番石榴缓存或Ehcache等。它们与应用程序部署在同一个进程中。优点是不需要跨网络调度,速度极快,可以用来在短时间内阻塞热点查询。
三、缓存的读写策略
1、Cache Aside策略
更新数据时,不会更新缓存,但会删除缓存中的数据。在读取数据时,发现缓存中没有数据,然后从数据库中读取数据,更新到缓存中。
这种策略是我们使用缓存的最常见的策略,即缓存备用策略(也称为旁路缓存策略)。该策略中的数据基于数据库中的数据,缓存中的数据按需加载。
缓存靠边策略是我们日常开发中使用最频繁的缓存策略,但是在使用的时候要学会随情况而变,并不是一成不变的。先不说缓存最大的问题,就是在频繁写的时候,缓存中的数据会被频繁清理,对缓存的命中率会有一些影响。如果您的企业对缓存命中率有严格的要求,那么您可以考虑两种解决方案:
一种方法是在更新数据的时候更新缓存,只需要在更新缓存之前添加一个分布式锁,因为同一时间只允许一个线程更新缓存,所以不会有并发问题。当然这样会对写作的表现有一些影响(推荐);
还有一种方式是在更新数据的时候更新缓存,只需要给缓存加上一个短的过期时间,这样即使缓存不一致,缓存的数据也会很快过期,对业务的影响是可以接受的。
2、Read/Write Through
这种策略的核心原理是用户只处理缓存,缓存与数据库通信,写入或读取数据。
Write Through
策略如下:首先查询要写入的数据是否已经存在于缓存中;如果是,更新缓存中的数据,缓存组件会同步更新到数据库中;如果缓存中的数据不存在,我们称这种情况为“写未命中”。一般来说,我们可以选择两种“写未命中”的方式:一种是“写分配”,即写到缓存的相应位置,然后缓存组件同步更新到数据库;另一种是“无写分配”,不写入缓存,直接更新到数据库。
我们可以看到,在直写策略中,写数据库是同步的,这会对性能产生很大的影响,因为同步写数据库的延迟远远高于写缓存。通过写回策略异步更新数据库。
Read Through
策略更简单,其步骤如下:首先查询缓存中的数据是否存在,如果存在则直接返回;如果没有,缓存组件负责从数据库同步加载数据。
3、Write Back
该策略的核心思想是写数据时只写缓存,并将缓存块标记为“脏”。只有当脏块被重用时,其中的数据才会被写入后端存储。
在“写入未命中”的情况下,我们采用“写入分配”的方法,即在写入后端存储的同时写入缓存,这样我们只需要在后续的写入请求中更新缓存,而不需要更新后端存储。注意和上面的写通策略区分开来。
当我们读取缓存时,如果我们发现缓存命中,我们将直接返回缓存数据。如果缓存未命中,请查找可用的缓存块。如果缓存块是脏的,则将缓存块中以前的数据写入后端存储,并将数据从后端存储加载到缓存块中。如果它不是脏的,缓存组件会将数据从后端存储加载到缓存中。最后,我们将设置缓存不脏,只返回数据。
回写策略主要用于将数据写入磁盘。比如:操作系统级的页面缓存、异步刷日志、异步将消息队列中的消息写到磁盘等。因为这种策略的性能优势毋庸置疑,避免了直接写盘带来的随机写问题。毕竟写内存和磁盘的随机I/O的延迟是有几个数量级的不同的。
四、缓存高可用
缓存命中率是缓存需要监控的数据索引。缓存的高可用性可以在一定程度上降低缓存穿透的概率,提高系统的稳定性。缓存的高可用性方案主要包括客户端方案、中间代理方案和服务器方案:
1、客户端方案
在客户端方案中,需要注意缓存的读写:
写数据时,需要将写入缓存的数据分散到多个节点,也就是将数据切片;
在读取数据时,可以使用多组缓存进行容错,提高缓存系统的可用性。关于读取数据,有主从和多副本两种策略,是针对不同的问题提出的。
具体实现细节包括:数据分片、主从和多副本。
数据分片
一致性哈希算法。在这个算法中,我们将整个哈希值空间组织成一个虚拟的圆,然后将缓存节点的IP地址或主机名哈希后放在这个圆上。当我们需要确定一个键需要访问哪个节点时,首先要对这个键做相同的Hash值,确定它在环上的位置,然后在环上顺时针“行走”,我们遇到的第一个缓存节点就是要访问的节点。
此时,如果在节点1和节点2之间增加一个节点5,可以看到原来命中节点2的键3现在命中节点5,而其他键不变;同样,如果我们从集群中删除节点3,它只会影响键5。于是,你看,在添加和删除节点时,只有少部分键会“漂移”到其他节点,而大部分被键命中的节点保持不变,从而保证命中率不会明显下降。
【提示】一致哈希的缓存雪崩现象由虚拟节点解决。一致哈希碎片和哈希碎片的区别在于缓存命中率的问题。当机器增加或减少时,哈希碎片会导致缓存失败,降低缓存命中率。
主从
Redis本身支持主从部署模式,Memcached不支持。Memcached的主从机制是如何在客户端实现的?每套主机配置一套从机,更新数据时主机和从机同步更新。读取时,优先从从机读取数据。如果无法读取数据,可以通过主服务器读取,并将数据重新植入从服务器,以保持从服务器数据的热度。主从机制最大的好处就是当一个从机宕机时,会有一个主机作为后盾,这样大量的请求就不会渗透到数据库中,提高了缓存系统的高可用性。
多副本
主从模式已经能够解决大部分场景下的问题,但是对于极端流量场景,一组从机通常无法完全承担所有流量,从机网卡的带宽可能成为瓶颈。为了解决这个问题,我们考虑在主/从之前增加一个复制层。整体架构如下:
在该方案中,当客户端发起查询请求时,该请求将首先从多个副本组中选择一个副本组来发起查询。如果查询失败,它将继续查询主/从,并将查询结果播种到所有副本组中,以避免副本组中存在脏数据。考虑到成本,每个副本组的容量都小于主副本组和从副本组的容量,所以只存储较热的数据。在这种架构中,主设备和从设备的请求将大大减少。为了保证它们数据存储的热度,我们在实践中会使用主从作为一组副本组。
2、中间代理层
业界也有很多中间代理方案,比如脸书的Mcrouter,Twitter的Twemproxy,豌豆荚的Codis。它们的原理基本上可以用一个图表来概括:
3、服务端方案
Redis在2.4版本中提出Redis Sentinel模式,解决主从Redis部署的高可用性问题。它可以在主节点挂机后自动将从节点升级为主节点,保证整个集群的可用性。整体架构如下图所示:
RedSentinel也部署在集群中,可以避免Sentinel节点因为挂机而无法自动恢复的问题。每个前哨淋巴结都是无状态的。主服务器的地址将在Sentinel中配置,Sentinel将始终监控主服务器的状态。当发现主设备在配置的时间间隔内没有响应时,则认为主设备已经挂断。Sentinel将选择一个从节点提升为主节点,并将所有其他从节点作为新主节点的从节点。在Sentinel cluster内的仲裁期间,将根据配置的值来决定。当几个哨兵节点认为主节点挂机时,可以从主节点切换到从节点,即需要在集群内约定缓存节点的状态。
【提示】上述客户端与Sentinel集群的连接是虚线,因为缓存读写请求不会经过sentinel node。
五、缓存穿透
1、帕累托
一般互联网系统的数据访问模型都会遵循“80/20原则”。“80/20原理”,又称帕累托定律,是意大利经济学家帕累托提出的一种经济理论。简单来说,就是指在一组事物中,最重要的部分通常只占20%,而其他80%并没有那么重要。将其应用到数据访问领域,意味着我们经常访问20%的热数据,而另外80%的热数据不会被频繁访问。由于缓存的容量是有限的,大多数访问只请求20%的热数据,所以从理论上来说,我们只需要在有限的缓存空间中存储20%的热数据,就可以有效地保护脆弱的后端系统,所以我们可以放弃缓存另外80%的非热数据。所以这种少量的缓存穿透是不可避免的,但对系统无害。
2、回种空值
当我们从数据库中查询空值或发生异常时,我们可以将空值植入缓存中。但是由于空值并不是准确的业务数据,而且会占用缓存空间,所以我们会给这个空值加上一个短的过期时间,让它在短时间内快速过期并被消除。虽然返回空值可以阻塞大量的穿透请求,但是如果有大量的空值缓存,那么缓存空间就会被浪费。如果缓存空间已满,将会删除一些缓存的用户信息,导致缓存命中率下降。所以我建议你在使用的时候要评估一下缓存容量是否能支持这个方案。如果需要大量缓存节点来支持,就不能通过播种空值来解决。这时候可以考虑使用Bloom filter。
3、布隆过滤器
1970年,Bloom提出了Bloom filter的算法来判断一个元素是否在一个集合中。该算法由二进制数组和哈希算法组成。它的基本思想是:我们根据提供的Hash算法计算集合中每个值对应的Hash值,然后对数组的长度取模得到需要包含在数组中的索引值,将数组这个位置的值从0改为1。在判断这个集合中是否存在一个元素时,只需要按照同样的算法计算这个元素的索引值。如果该位置的值为1,则认为该元素在集合中;否则,它被认为不在集合中。
如何使用布隆过滤器解决缓存穿透呢?
以存储用户信息的表为例。首先我们初始化一个大数组,比如一个长度为20亿的数组。接下来,我们选择哈希算法。然后,我们计算所有现有用户id的哈希值,并将它们映射到这个大数组中。映射位置的值设置为1,其他值设置为0。新注册用户除了被写入数据库外,还需要根据相同的算法更新Bloom filter数组中相应位置的值。那么当我们需要查询某个用户的信息时,首先要查询这个ID在Bloom filter中是否存在,如果不存在,就直接返回一个null值,而不是继续查询数据库和缓存,这样可以大大减少异常查询带来的缓存穿透。
布隆过滤器优点:
(1)高性能。无论是写操作还是读操作,时间复杂度都是O(1)为常数值。
(2)节省空间。比如一个20亿用户的数组需要200000000/8/1024/1024=238m,如果用一个数组来存储,假设每个用户ID占用4个字节,那么需要2000000000 * 4/1024/1024=7600M来存储20亿用户,这是Bloom filter空间的30%。
布隆过滤器缺点:
(1)在判断元素是否在集合中时有一定的错误概率。例如,它会将不在集合中的元素判断为在集合中。
原因:哈希算法本身的缺陷。
解决方案:使用多个哈希算法来计算一个元素的多个哈希值。只有当所有哈希值对应的数组中的值都为1时,才会在集合中考虑该元素。
(2)不支持删除元素。Bloom filter不支持删除元素的缺陷也与哈希碰撞有关。例如,如果两个元素A和B是集合中的元素,并且它们具有相同的哈希值,则它们将映射到数组中的相同位置。这时我们删除一个,数组中对应位置的值也由1变为0。所以当我们判断B时,会发现值为0,也会判断B不是集合中的元素,会得出错误的结论。
解决方案:我将使数组不再只有0和1值,而是存储一个计数。比如A和B同时命中数组的索引,那么这个位置的值就是2。如果删除了A,则将该值从2更改为1。这种方案中的数组不再存储位,而是存储值,会增加空间的消耗。
4、狗桩效应
比如有一个非常热的缓存项,一旦失效,大量的请求就会渗透到数据库中,对数据库造成很大的压力。我们把这种现象称为“狗堆效应”。解决狗堆效应的思路是尽量减少缓存穿透后的并发,方案也比较简单:
(1)在代码中,控制在一个热缓存条目失败后启动一个后台线程,渗透到数据库中,将数据加载到缓存中。在缓存没有加载之前,所有访问这个缓存的请求都不会再穿透,直接返回。
(2)通过在Memcached或Redis中设置分布式锁,只有对锁的请求才能穿透数据库。
六、CDN
1、静态资源加速的原因
我们的系统中有大量的静态资源请求:对于手机APP来说,这些静态资源主要是图片、视频和流媒体信息;对于网站来说,包括JavaScript文件、CSS文件、静态HTML文件等等。它们有大量的读取请求,要求高访问速度,并占用高带宽。这时候就会出现访问速度慢,满带宽影响动态请求的问题,所以你需要考虑如何针对这些静态资源加快读取速度。
2、CDN
静态资源访问的关键点是邻近访问,即北京用户访问北京数据,杭州用户访问杭州数据,这样才能达到最佳性能。我们考虑在业务服务器的上层增加一层特殊的缓存,承担大部分对静态资源的访问。这层专用缓存的节点需要分布在全国各地,以便用户选择最近的节点访问。缓存命中率也需要一定的保证,这样才能尽量减少访问资源存储源站的请求(回源请求)数量。这层缓存就是CDN。
Cdn(内容交付网络/内容分发网络)。简单来说,CDN就是将静态资源分布到位于多个地理位置的机房内的服务器上,这样就可以解决数据就近访问的问题,从而加快静态资源的访问速度。
3、搭建CDN系统
构建CDN系统时应考虑哪两点:
(1)如何将用户的请求映射到 CDN 节点上
你可能觉得这很简单,只需要告诉用户CDN节点的IP地址,然后请求部署在这个IP地址上的CDN服务。然而,事实并非如此。您需要将ip替换为相应的域名。那么你是如何做到这一点的呢?我们需要依靠DNS来帮助我们解决域名映射的问题。DNS(域名系统)实际上是一个分布式数据库,存储域名和IP地址的对应关系。域名解析的结果一般有两种,一种叫做“A记录”,返回域名对应的IP地址;另一个是“CNAME记录”,返回另一个域名,也就是说,当前域名的分辨率要跳转到另一个域名的分辨率。
比如你公司的一级域名是example.com,你可以将你图片服务的域名定义为“img.example.com”,然后将这个域名的解析结果CNAME配置为CDN提供的域名。比如uclound可能会提供一个“80f21f91.cdn.ucloud.com.cn”的域名。你的电商系统使用的图片地址可以是“img.example.com/1.jpg”。
当用户请求该地址时,DNS服务器会将该域名解析为80f21f91.cdn.ucloud.com.cn域名,然后将该域名解析为CDN的节点IP,这样就可以获得CDN上的资源数据。
域名层级解析优化
因为域名解析过程是分层的,每一层都有专门的域名服务器负责解析,域名解析过程可能需要跨公网进行多次DNS查询,性能较差。一种解决方案是在应用程序启动时预解析要解析的域名,然后将解析结果缓存到本地LRU缓存中。这样,当我们想要使用这个域名时,只需要直接从缓存中获取所需的IP地址即可。如果它不存在于缓存中,我们将经历整个DNS查询过程。同时,为了避免DNS解析结果的变化导致缓存中的数据失效,我们可以启动一个定时器来定时更新缓存中的数据。
(2)如何根据用户的地理位置信息选择最近的节点。
GSLB(全局服务器负载平衡)是指部署在不同地区的服务器之间的负载平衡。可能有许多受管理的本地负载平衡组件。它有两个作用:一方面是一种负载均衡服务器,顾名思义就是平均分配流量,使下面管理的服务器负载更加均匀;另一方面,它还需要确保流量流经的服务器在地理上靠近流量源。
GSLB可以通过各种策略保证返回的CDN节点和用户尽可能在同一地理区域。比如可以将用户的IP地址按照地理位置划分成几个区域,然后将CDN节点映射到一个区域,根据用户所在的区域返回合适的节点。您还可以通过发送数据包来测量RTT,从而决定返回哪个节点。
总结:DNS技术是CDN实现中使用的核心技术,可以将用户请求映射到CDN节点;DNS解析结果需要在本地缓存,以减少DNS解析过程的响应时间;GSLB可以返回一个离用户更近的节点,加速静态资源的访问。
拓展
(1)百度域名的解析过程
一开始,域名解析请求会先检查本机的hosts文件,看是否有对应www.baidu.com的IP;如果没有,则请求本地DNS是否有域名解析结果的缓存,如果有,则返回非权威DNS返回标识的结果;如果没有,就开始DNS的迭代查询。先请求根DNS,根DNS返回顶级DNS的地址(。com);然后请求。com顶级DNS获取Baidu.com的域名服务器地址;然后从Baidu.com的域名服务器查询www.baidu.com对应的IP地址,返回这个IP地址,标记结果来自权威DNS,并写入本地DNS的解析结果缓存,这样下次解析同一个域名时就不需要做DNS迭代查询了。
(2)CDN延时
一般我们会通过CDN厂商的接口将静态资源写入某个CDN节点,然后由CDN内部的同步机制将资源分散同步到各个CDN节点。即使CDN内部网络优化,同步过程也会延迟。一旦我们无法从选定的CDN节点获取数据,就不得不从源站获取数据,而从用户网络到源站的网络可能会跨越多个骨干网,不仅会造成性能损失,还会消耗源站的带宽,造成更高的研发,因此我们在使用CDN时需要注意CDN的命中率和源站的带宽。
以上是关于java高并发系统设计的cache文章的详细内容。请多关注我们的其他相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。