摘要: ##简介 在当今互联网应用中,缓存作为一把尖刀利器对应用的性能起着举足轻重的作用。缓存的使用可以说无处不在,从应用请求的访问路径来看,用户user -> 浏览器缓存 -> 反向代理缓存-> WEB服务器缓存 -> 应用程序缓存 -> 数据库缓存等,几乎每条链路都充斥着缓存的使用,当然交换机,网络适配器,硬盘上也有Cache 但这不是我们要讨论的范围。今天我们讨论的“缓存”,自然就是“用空间换时间
简介
在当今互联网应用中,缓存作为一把尖刀利器对应用的性能起着举足轻重的作用。缓存的使用可以说无处不在,从应用请求的访问路径来看,用户user -> 浏览器缓存 -> 反向代理缓存-> WEB服务器缓存 -> 应用程序缓存 -> 数据库缓存等,几乎每条链路都充斥着缓存的使用,当然交换机,网络适配器,硬盘上也有Cache 但这不是我们要讨论的范围。今天我们讨论的“缓存”,自然就是“用空间换时间”的算法。缓存就是把一些数据暂时存放于某些地方,可能是内存,也有可能硬盘。总之,目的就是为了避免某些耗时的操作。我们常见的耗时的操作,比如数据库的查询、一些数据的计算结果,或者是为了减轻服务器的压力。其实减轻压力也是因查询或计算,虽然短耗时,但操作很频繁,累加起来也很长,造成严重排队等情况,服务器抗不住。
缓存介质
虽然从硬件介质上来看,无非就是内存和硬盘两种,但从技术上,可以分成内存、硬盘文件、数据库。
- 内存 将缓存存储于内存中是最快的选择,无需额外的I/O开销,但是内存的缺点是没有持久化落地物理磁盘,一旦应用异常break down而重新启动,数据很难或者无法复原。
- 硬盘 一般来说,很多缓存框架会结合使用内存和硬盘,在内存分配空间满了或是在异常的情况下,可以被动或主动的将内存空间数据持久化到硬盘中,达到释放空间或备份数据的目的。
- 数据库 前面有提到,增加缓存的策略的目的之一就是为了减少数据库的I/O压力。这里所指的数据库只是简单的key-value存储结构的特殊NOSQL数据库(如BerkeleyDB和Redis),响应速度和吞吐量都远远高于我们常用的关系型数据库等。
缓存命中率
缓存命中率通常指的是缓存查询命中总数与缓存查询总数的比率,应用缓存命中率越高越好,这是衡量缓存使用是否良好的重要指标。
缓存回收策略
缓存的回收策略在分布式缓存中类型主要有如下几种
1. 基于时间
TTL:存活期,一条缓存自创建时间起多久后失效
TTI:空闲期,一条缓存自最后读取或更新起多久后失效
2. 基于空间
通常指的是设置存储空间大小,比如TAIR申请时空间大小,超过这个阈值后,会按照一定的策略算法移除数据。
3. 基于容量
通常指的是设置缓存条目数量大小,超过这个阈值后,会按照一定的策略算法移除数据。
缓存系统的整体回收首先根据时间进行移除过期数据,如果超过空间或者容量设置的阈值,会根据相应的算法移除数据,移除数据的算法主要有LRU、FIFO、LFU,最常用的算法是LRU,分布式缓存memcached、redis以及tair都支持该算法,本地缓存guava cache、ehcache也同样支持LRU算法。
数据淘汰算法简要介绍:
-
FIFO(first in first out)
先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。 -
LFU(less frequently used)
最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。 -
LRU(least recently used)
最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。
缓存使用场景与分类
使用缓存的目的提高系统的整体性能,缓存的工作机制是先从缓存中读取数据,如果没有,则再从慢速设备上读取实际数据并同步到缓存。那些经常查询的数据、频繁访问的数据、热点数据、IO瓶颈数据、计算昂贵的数据、符合五分钟法则和局部性原理的数据都可以进行缓存。
在互联网应用中常见的缓存的场景主要:
- 数据库缓存: 随着业务量的上升,数据库存储的数据量越来越大,并发请求逐渐增大,随之而来的问题就是数据库系统的负载升高,响应延迟下降,严重的时候,甚至有可能因此而导致服务中断,这时启用缓存利器可以提高系统性能。
- 临时数据存储: 应用程序需要维护大量临时数据,例如计数器、分布式锁、用户session等,将临时数据存储在分布式缓存中,可以降低内存管理的开销,改进应用程序工作负载。
在互联网应用中,从应用与缓存耦合度角度缓存主要分为本地缓存、分布式缓存两大类。
-
本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
Guava Cache、Ehcache、MapDB都可以实现JAVA堆内存本地缓存,谈堆内存其实JAVA还支持堆外内存,Ehcache 3.x、MapDB 3.x也同样支持堆外内存,堆外内存意味着把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),Netty就是使用堆外内存来管理内存,建议慎用堆外内存,使用不当容易导致OOM,关于堆外内存与堆内存接下来不做重点介绍。 -
分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存,像memcached、redis、tair都是分布式缓存。
缓存使用实践
缓存的使用也是讲究一定技巧性,如果使用不当会导致数据一致性问题、缓存被穿透导致应用雪崩等。
上面讲到局部性原理,简单介绍下与缓存相关的局部性原理:
- 时间局部性(temporal locality):数据将被多次访问
- 空间局部性(spatial locality):邻近数据将被访问
基于局部性原理,缓存在设计上需要考虑许多的因素:
- 缓存关联性(cache associativity)
- 写策略(writing policy)
- 替换策略(cache replacement)
- 缓存一致性(cache coherency)
- cache失效可能引发的dog-piling效应(cache stampede)
- .....
接下来将会根据以上几点介绍缓存使用的一些实践。
缓存与DB数据一致性
数据的更新与缓存同步,没有高科技含量,但要做好并不容易,有些场景需要做到实时一致性,有些场景需要做到最终一致性。
如果要做到强一致性,可以采取以下方案:
-
数据库更新后,删除缓存。
这种方式的优点是实现简单,缺点是删除缓存后,如果有多个查询请求并发过来,都发现缓存中没数据,都会将请求落到数据库上,导致数据库压力瞬间增加。 -
数据库更新后,更新缓存。
这是对删除方式的改进,但也有缺点,写入前要多一次查询,在部分场景下是没法使用的,比如分页查询场景,各种请求参数组合很多,应用无法知道有多少种key,自然无法主动写入,只能等缓存失效。
以上两种实时同步缓存机制,先操作数据库然后操作缓存,因异构数据存储无法通过事务保证一致。当然缓存涉及到网络IO开销,如果连接分布式缓存超时也需要考虑,否则会出现事务超时,导致应用线程挂起。
如果要做到最终一致性,可以采取以下方案:
- MQ异步刷新、定时刷新 采用MQ异步消息机制刷新,如果更新失败要有适当的补偿机制。所有需要更新的对象存储到一张定时任务表,定时任务扫描任务表异步更新。这两种更新机制不能保证查询缓存同DB的一致性,但是能够保证最终一致性。
- 自动失效 合理设置缓存失效时间,需根据业务场景设置每个缓存的失效时间,一致性要求越高,自然失效时间也要越短。
缓存并发
缓存过期后将尝试从后端数据库获取数据,这是一个看似合理的流程。但是,在高并发场景下,有可能多个请求并发的去从数据库获取数据,对后端数据库造成极大的冲击,甚至导致 “雪崩”现象。此外,当某个缓存key在被更新时,同时也可能被大量请求在获取,这也会导致一致性的问题。那如何避免类似问题呢?我们会想到类似"锁"的机制(可重入锁),在缓存更新或者过期的情况下,先尝试获取到锁,当更新或者从数据库获取完成后再释放锁,其他的请求只需要牺牲一定的等待时间,即可直接从缓存中继续获取数据。
缓存被穿透
在高并发场景下,如果某一个key被高并发访问,没有被命中,出于对容错性考虑,会尝试去从后端数据库中获取,从而导致了大量请求达到数据库,而当该key对应的数据本身就是空的情况下,这就导致数据库中并发的去执行了很多不必要的查询操作,从而导致巨大冲击和压力。
我们在应用中使用缓存的时候,很可能就是使用的如下代码所表示的逻辑的方式。 先获取缓存中的数据,如果为空则查询数据库或者其他方式获取数据,然后再存入缓存,返回数据,如下伪代码。
data=cache.get(key);
if(data=null || !isValid(data)){
sql="SELECT ......";
data=db.query(sql);
//data可能为null
if(data != null){
cache.set(key,data,expire);
}
}
return data;
相信大多数人会认为这段代码没有问题,很多人也是这么去写的。
问题:
当key的内容在数据库也不存在时,那么上面代码中的data始终为null,缓存中也始终没有数据,如果这个key的请求突然变得很大(很多情况下都会发生,比如查询请求不存在的数据),那么将会有大量的请求绕过缓存,直接到了后端数据库,对数据库的IOQPS造成过大的冲击,最后很可能导致系统崩溃。
解决方案:
1. 缓存空对象
解决这个问题的办法就是当数据库查询到null时,我们也应该把null进行相应的缓存。
比如数据库返回的是一个list,那么我们可以存入一个空的list来处理cache.set(key,new List(),expire)。
同时,也需要保证缓存数据的时效性。这种方式实现起来成本较低,比较适合命中不高,但可能被频繁更新的数据。
2. 单独做过滤处理
对所有可能对应数据为空的key进行统一的存放,并在请求前做拦截,这样避免请求穿透到后端数据库。这种方式实现起来相对复杂,比较适合命中不高,但是更新不频繁的数据。
比较常用的方案是通过Bloom Filter提前拦截,Bloom Filter是一个空间效率很高的随机数据结构,它由一个位数组和一组hash映射函数组成。Bloom Filter可以用于检索一个元素是否在一个集合中,它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率。因此Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。
像Guava Cache、Google的bigtable都有类似bloomfilter实现,关于bloomfilter具体可以参考:https://www.javacodegeeks.com/2012/11/bloom-filter-implementation-in-java-on-github.html
以上两种方案均可以保障的缓存被穿透问题,第一种方案更简单,但需要额外占一些缓存空间。第二种方案复杂一些,但是占用缓存空间少。
热点缓存
比如key XXX对应的数据访问量特别大,但是XXX在缓存中是有失效时间的。一旦缓存失效,会有N多线程并发的去请求数据库,然后更新缓存,这个时候会导致系统压力过大。通常有这么几种解决方法:
1. 加锁,同时只允许一个线程去查询数据库并更新缓存
2. 缓存不加失效时间,但后台有个异步线程定期的去更新它
3. 引入类似于Hystrix的熔断机制,只允许一定量的请求去请求数据库并更新缓存
比如缓存使用memcached、redis,可以考虑如上方案针对热点数据。如果使用tair缓存的话,tair提供了hotkey的解决方案,主要原理开启本地LocalCache功能,每次写操作会自动强制删除 Localcache 里存在的 key,读操作后会自动从 Localcache 里读取, Localcache 中不存在则从服务端获取,成功后存储到 Localcache 里。在 Hotkey 防御系统中,客户端要开启 hot-running 模式,该模式下只能缓存带热点标记的 key,Localcache 中非热点的 key 将逐步被淘汰。即一旦开启客户端的该模式,会强制改变 Localcache 的工作模式。
数据缓存模式通常有懒加载和预加载两种模式,但是对于热点缓存最好采取预加载模式,笔者曾经遇到应用系统刚发布完就直接挂掉的案例,一些热点数据查询在高并发场景下还没有加载进来,请求流量就涌入进来。
缓存大对象
某些场景下我们想要把一些大对象缓存起来,因为产生一次大对象的代价很大,我们需要产生一次,尽可能的多次使用,从而提升QPS。
通常的解决方案是对数据进行压缩,压缩可以在客户端进行压缩,缓存服务端也可以压缩,像memcached的话需要在客户端进行压缩,可以采用gzip压缩算法进行压缩,像tair的话如果value超过一定的阈值服务端会自动对value进行压缩。
相关推荐
互联网缓存实践是互联网服务提供者用来提升用户体验、降低服务器压力和减少网络带宽消耗的重要技术。它通过在用户本地或靠近用户的边缘节点存储常用数据,使得数据访问速度大大提高,延迟减少,尤其对于高流量的网页...
### 消除知识盲区:互联网缓存架构从理论到实践 #### 一、引言 随着互联网技术的快速发展,用户对于数据访问速度的要求越来越高。为了提高网站响应速度和用户体验,减少数据库压力,缓存技术成为了不可或缺的一部分...
分布式多级缓存实践是一种优化高并发环境下数据访问性能的重要技术。在现代互联网应用中,随着用户数量的增长和数据量的膨胀,单纯依赖数据库进行实时读写操作往往会导致性能瓶颈。为了解决这个问题,分布式多级缓存...
1. **云计算分布式缓存及应用实践**:本文件探讨的是云计算技术中的分布式缓存技术及其在实际应用中的案例和实施方法。分布式缓存作为一种存储技术,允许数据在多个节点间分布存储,提高了数据存取的并发性能,并能...
在“网络应用程序实践”这一主题中,我们主要探讨的是如何设计、开发以及优化网络应用程序,以便在互联网上高效运行。东北大学的这个实践课程旨在帮助学生掌握网络编程的基础知识,理解网络通信协议,并能实际动手...
本资源"Android互联网应用开发实践--代码.zip"显然是一份教学资料,旨在帮助开发者掌握如何在Android环境中构建与互联网交互的应用。下面将详细探讨相关知识点。 1. **Android网络请求**:Android应用可以通过多种...
### 互联网应用架构模式 在当今的信息时代,随着互联网技术的飞速发展,各种复杂的互联网应用不断涌现。这些应用不仅需要高效稳定地运行,还要具备良好的扩展性和可维护性。因此,选择合适的架构模式对于构建高质量...
"银行互联网核心云原生数据库应用与实践" 本文档主要介绍了银行互联网核心云原生数据库的应用和实践,涵盖了云原生数据库的分类、特点、应用场景、技术要求、架构规划、应用开发规范、容灾体系建设等方面的知识点。...
**互联网应用开发综合实践报告** 本实践报告详细记录了针对“机械工业品电子商城”的开发过程,涵盖了项目从需求分析到系统设计的各个环节。以下是关键知识点的深入解析: ### 1. 项目简介 机械工业品电子商城是...
在本次的"互联网应用架构实战峰会-01"中,我们有幸听到来自业界顶级公司的专家分享他们的技术实践和架构演进经验。以下是他们演讲的主要内容: 首先,阿里巴巴的陈康贤详细介绍了阿里直播平台的架构演进过程。他...
Flex是由Adobe开发的一种开源框架,用于构建富互联网应用程序(RIA)。它主要基于ActionScript编程语言和MXML标记语言,能够生成SWF文件,这些文件可以在Adobe Flash Player或Adobe AIR中运行。 在Flex3中,客户端...
本文档“TWEB-企业级项目基于PWA缓存的最佳实践.pdf”通过王涵冰的介绍,深入探讨了企业级项目中应用PWA缓存技术的最佳实践。 首先,文档介绍了业务现状,提到了当前项目中存在的性能瓶颈,例如,在不同的客户端...
分布式缓存是现代互联网应用程序中不可或缺的技术,它能显著提高数据读取速度,降低数据库压力。本文主要探讨了分布式缓存的最佳实践,旨在避免生产环境中可能遇到的问题,确保系统的稳定性和高可用性。 首先,缓存...
Redis核心原理与应用实践 Redis 作为互联网技术架构中最为广泛使用的中间件,它在存储系统中发挥着至关重要的作用。特别是在大型互联网公司中,Redis 是面试官最喜欢问的工程技能之一,这也反映了 Redis 在行业中的...
总结来说,互联网缓存和CDN的攻击研究揭示了互联网基础设施中存在的安全漏洞,为后续的安全研究和实践提供了宝贵的参考。随着互联网技术的不断进步,网络安全问题将会更加复杂多变,因此持续关注和研究这一领域,...
分布式缓存是现代大型互联网应用中不可或缺的一部分,它在处理海量数据、提高系统性能和响应速度方面发挥着关键作用。本书“深入分布式缓存:从原理到实践”旨在全面解析分布式缓存的核心概念、工作原理及其在实际...
### 深信服应用交付网络技术应用与实践 #### 一、应用交付网络概述 应用交付网络(Application Delivery Networking,简称ADN)是一种利用网络优化技术确保业务应用能够稳定、快速、安全地传递给最终用户的解决...
综上所述,实现【高性能高流量大数据互联网应用实战】需要对分布式系统、大数据处理框架、负载均衡、缓存技术、实时处理、源码分析、工具选择和CI/CD流程有深入理解和实践经验。这些知识点构成了支撑高性能互联网...