缓存的作用
缓存是我们提高系统性能的一项必不可少的技术,无论是前端、还是后端,都应用到了缓存技术。前端使用缓存,可以降低多次请求服务的压力;后端使用缓存,可以降低数据库操作的压力,提升读取数据的性能。
缓存资源的类型
静态资源
不会频繁变动的文件,如图片、CSS和JavaScript文件。这些资源可以被有效地缓存以提高性能和减少带宽消耗。
动态资源
根据请求的参数或状态而动态生成的内容,如页面模板、API响应等。由于动态资源的生成是基于实时数据或用户特定请求的,因此不能永久缓存,但仍然可以使用缓存机制来减少数据库查询和计算的频率。
CDN缓存
CDN(Content Delivery Network,内容分发网络)的缓存具有重要的作用和重要性,主要体现在以下几个方面:
- 提高性能和加速访问:CDN的主要目标之一是提供快速而稳定的内容传输。它通过在全球范围内分布节点,并在节点上缓存静态和动态内容,使用户可以从离他们最近的节点获取内容。这样可以大大减少延迟和网络拥塞,提高用户访问的速度和性能。
- 缓解源服务器负载:在传统的网络架构中,用户请求直接发送到源服务器,如果用户流量集中在某一台服务器上,就会给源服务器带来很大的负载压力。而CDN通过在全球范围内设置缓存节点,可以将用户请求均匀分散到不同的节点上,从而缓解了源服务器的压力,提高了后端服务器的可扩展性。
- 改善用户体验和可用性:由于CDN部署了全球范围的节点,它可以确保用户在任何地方都能够快速访问内容。而且CDN的节点通常都具备故障处理机制,以应对节点故障或网络中断的情况。这样可以显著提高应用程序的可用性,并为用户提供更好的体验。
- 减少带宽成本:CDN的部署通常会采用就近分发原则,将内容缓存在边缘节点上,并通过就近节点服务用户。这样可以减少数据中心之间的数据传输量,从而节省带宽成本。此外,由于CDN节点通常与网络服务提供商进行互联,还可以通过缓存和一次性获取内容来减少对互联网的依赖,降低网络出口的带宽消耗和成本。
总之,CDN的缓存作用不仅可以提高性能和加速访问,减轻源服务器负载,改善用户体验和可用性,还可以降低带宽成本。它在现代网络应用中的重要性不可忽视,被广泛应用于各种领域,包括电子商务、媒体传输、流媒体、游戏等。
服务端缓存分类
本地缓存(进程级缓存)
在运行的过程中,应用数据被载入到进程中的,通过本地内存的低延迟高吞吐的特性来提高数据资源的查询效率。
常用的有Java自带容器类、Guava Cache和Ehcache等
缓存组件对比
1、Java自带容器类
- Java自带的容器类,如List、HashMap和ConcurrentHashMap,可以被用作基本的缓存结构。它们是线程安全的,提供了键值对的存储,并具有良好的性能。
- 优点:简单易用,无需引入额外的依赖,适合在小规模应用中使用。
- 缺点:没有自动过期策略,需要手动管理缓存的过期和清理,缺乏一些高级的缓存功能。
2、Guava Cache
- Guava Cache是Google Guava库中提供的一个内存缓存实现。它提供了一些高级的缓存功能,如缓存过期、缓存大小限制、自动加载等。
- 优点:具有灵活的过期策略和缓存配置选项,可以根据需求进行自定义配置。支持缓存的最大大小限制,可以防止内存溢出。具备高性能和线程安全的特性。
- 缺点:Guava Cache是基于内存的缓存实现,不适用于大规模缓存数据或分布式环境。
3、Ehcache
- Ehcache是一个流行的开源的Java缓存框架。它为应用程序提供了灵活的缓存解决方案,支持本地缓存和分布式缓存。
- 优点:支持缓存过期、缓存大小限制、持久化缓存等高级功能。可以与Hibernate、Spring等框架集成。支持本地和分布式环境,提供了分布式缓存一致性的保证。
- 缺点:Ehcache在配置和使用上相对复杂一些,需要一定的学习和配置成本。对于大规模缓存和高并发环境,可能需要额外的配置和调优。
4、Caffeine
- Caffeine是一个高性能的Java缓存库,专注于提供快速、高效的本地缓存。它具有类似Guava Cache的功能,但性能更好。
- 优点:具有高性能、低延迟的特点,适用于对性能要求较高的应用。支持缓存过期、缓存大小限制、异步加载等功能。具备内存优化和高并发访问的能力。
- 缺点:Caffeine是基于内存的缓存实现,不适用于大规模缓存数据或分布式环境。
5、Guava Cache和Caffeine的区别
Guava Cache和Caffeine都是基于Java的缓存库,它们在特性和性能方面有一些区别。
区别
- 项目来源:Guava Cache是Google Guava开源库中的一个模块,而Caffeine是Guava Cache的改进版本,由Caffeine团队维护和开发。
- 特性和功能:Guava Cache和Caffeine提供了类似的缓存功能,包括缓存大小限制、过期控制和回收策略等。不过Caffeine相对于Guava Cache提供了更多的缓存优化和高级功能。
- 并发访问:Caffeine在并发访问方面性能更好,采用了无锁算法和细粒度的并发策略,减少了锁竞争,提供了更高的并发性能。
性能差异
- 内存管理:Caffeine支持堆外内存,能够更好地管理Java堆内外内存的分配和使用,减少了GC压力和堆内存消耗。
- 数据结构:Caffeine的缓存实现使用了比Guava Cache更高效的数据结构,如哈希表和跳表,以及针对无锁操作的优化,提高了缓存的读写性能和并发能力。
- 数据加载方式:Caffeine提供了更灵活的数据加载方式,包括同步加载、异步加载和缓存刷新等,以支持动态数据的加载和更新。
总结
- Caffeine是Guava Cache的改进版本,在功能和性能上进行了优化。
- Caffeine相较于Guava Cache,具有更好的并发访问性能、更高效的内存管理和更灵活的数据加载方式。
- 在高并发场景下,Caffeine能够提供更好的性能和响应能力
各自应用场景
1、Java自带的容器
- 数据集合:用于存储、管理和操作对象的集合数据,例如使用ArrayList存储一组对象。
- 键值对存储:使用HashMap或ConcurrentHashMap来存储键值对数据,以快速检索和访问。
- 线程安全性要求低的场景:如果不需要强制的线程安全性,Java自带容器类提供了很好的性能和易用性。
2、Guava Cache
- 有限容量的缓存:Guava Cache提供了LRU(最近最少使用)和LFU(最近最少使用)等缓存回收策略,适用于有限容量的缓存需求。
- 缓存读写和过期控制:Guava Cache支持自定义的缓存加载逻辑、缓存过期策略和缓存监听器,能够方便地进行缓存数据的读写和控制。
3、Ehcache
- 大规模缓存:Ehcache提供了分级缓存结构、内存管理、磁盘存储和数据复制等功能,适用于大规模缓存需求。
- 高性能和高可用性:Ehcache支持数据复制、容错和故障恢复机制,可提供高可用性和性能的缓存服务。
- 分布式缓存:Ehcache可以作为分布式缓存,支持多个缓存节点之间的数据复制和一致性维护。
4、Caffeine
-
低延迟和高并发:Caffeine通过使用堆外内存和无锁数据结构,实现低延迟和高并发的缓存访问,适用于读密集型应用场景。
-
本地缓存:Caffeine主要面向本地应用,能够提供近乎实时的数据访问和处理。
-
高级功能和配置:Caffeine提供了缓存统计、数据加载和回收机制等高级功能,具有较高的定制性和灵活性。
分布式缓存
由于本地缓存存在以下问题:
1、无法共享:本地缓存是无法多个进程共享。
2、不支持技术异构:另外进程级的缓存一般都和对应的开发语言绑定,无法提供给不同的开发语言使用。
3、无法扩展:缓存是绑定着进程共生共死的,其资源的分配和使用都限制其应用程序,也无法进行节点扩展。
4、没有持久化机制:同时因为数据都保存在内存中的不会进行持久化,所以一旦进程停止了缓存数据就都没有了。
所以综合来看,我们需要有一种支持多进程共享、数据持久化、技术异构的缓存,这种缓存也就是我们所熟知的分布式缓存。
分布式缓存是将缓存数据分布式地存储在多个节点上,以提高数据访问的性能和扩展性。它将缓存的数据分散在多个服务器节点上,使得数据可以更接近应用程序,并且可以通过并行处理和负载均衡来提高读取和更新缓存的性能。
缓存组件对比
1、Redis
- 优点:Redis 是一个高性能的内存数据存储系统,支持丰富的数据类型和数据结构,适合用作分布式缓存。它具有快速读写能力、丰富的功能和灵活的数据存储方式。Redis 还提供了持久化选项,支持数据的持久化存储,并且能够支持集群和分布式部署。
- 缺点:Redis 的缺点是存储空间受限于单个节点的内存容量,对于大规模数据或高并发访问的场景,可能需要考虑使用分片来扩展容量和性能。
2、Memcached
- 优点:Memcached 是一个高性能的分布式内存对象缓存系统,适用于缓存键值对或简单的数据对象。它具有快速读写能力和线性扩展性,可以通过增加节点来扩展存储容量。Memcached 的协议简单,易于使用和集成。
- 缺点:Memcached 的主要缺点是缺乏数据持久化支持,数据存储在内存中,重启或节点故障可能导致数据丢失。此外,Memcached 仅支持简单的键值对存储,不适合复杂的数据结构。
常见问题与解决方案:
1、缓存击穿(Cache Breakdown)
缓存击穿是指一个热门的缓存键在缓存失效时被大量并发请求访问,导致这些请求都落到数据库或其他存储后端,从而对后端系统造成了很大的负载压力。通常情况下,该缓存键是有时效性的,但在某一时刻失效后,下一次请求就无法从缓存中命中,要去查询更新后的数据。当热门键失效后,大量并发请求同时落到后端存储,导致数据库负载激增,性能下降。
解决方案
- 添加互斥锁(Mutex Lock)或分布式锁(Distributed Lock):在缓存失效时,只允许一个请求去查询数据库,其他请求等待并共享查询结果。
- 使用热点数据预加载:在缓存过期之前,提前异步加载热门数据到缓存中,从而避免缓存失效后的高并发情况。
2、缓存穿透(Cache Penetration)
缓存穿透是指一个请求查询缓存中不存在的数据,导致请求直接落到数据库或其他存储后端。这种情况下,频繁的无效查询会增加后端负载,并导致缓存失去了提升性能的效果。
解决方案
- 布隆过滤器(Bloom Filter):使用布隆过滤器可以预先将可能的查询键存储在过滤器中,用于快速判断一个键是否可能在缓存中存在,从而可以避免无效查询。
布隆过滤器的误判有一个特点,那就是,它只会对存在的情况有误判。如果某个数字经过布隆过滤器判断不存在,那说明这个数字真的不存在,不会发生误判;如果某个数字经过布隆过滤器判断存在,这个时候才会有可能误判,有可能并不存在。
- 针对缓存不存在的数据,可以设置一个较短的过期时间,或者缓存一个特殊的值,以避免频繁查询数据库。
3、缓存雪崩(Cache Avalanche)
缓存雪崩是指多个缓存键在同一时间失效或redis服务故障宕机,导致大量请求落到数据库或其他存储后端。这会造成严重的性能问题,甚至导致系统崩溃。
解决方案
- 设置不同的过期时间:可以将缓存键的过期时间设置为稍有差异的随机值,以避免它们同时失效。
- 实施缓存高可用和容错机制:使用分布式缓存集群、数据备份和故障转移等技术,确保缓存的高可用性和容错性。
- 对关键数据做持久化保存:对于重要的数据,可以将数据同时保存在缓存和持久化存储中,以防止缓存失效时导致数据丢失。
项目中缓存的使用(Goblin-cache)
基本介绍
- 会配置两套缓存,一套非持久化,用于存储常规表数据,提高查询效率,另一套持久化,存储用户登录信息及临时性的非表数据
- 默认所有的数据表都会建立以主键为key缓存,可以根据需求再建立其他字段为key的缓存,比如用户id,可以设置不同维度的缓存
- 非持久化缓存的有效期默认为当天,如有特殊需求可以单独指定,如下:
enum Policy {
THIS_MONTH, // 截止到本月的最后一秒
THIS_WEEK, // 截止到本周的最后一秒
TODAY, // 截止到今天的最后一秒
FIXED // 固定的秒数,需要自行指定
}
持久化缓存,也都需要设置合理的失效时间
- 手动修改数据库数据后,需要手动清空非持久化缓存。如果清空持久化缓存,会导致用户登录token丢失,需要用户重新登录,影响用户体验
- 目前开发、测试、预发、生产,各自都有两套缓存,但是预发和生产使用的是相同的mysql数据库,可能会导致数据不一致的问题
- 线上环境删除缓存,不要使用flushall,flushall会清空所有的缓存,如果用户量大,使用flushall,可能会导致缓存雪崩,建议使用key删除指定的缓存
缓存数据交互图
用户查询数据(读操作)
例如:查询用户正在学习的词书:key为用户id
暂时无法在飞书文档外展示此内容
用户更新数据(写操作)
例如:用户对某一本词书,点击学习此书,将词书设置为正在学习的词书:key为用户id
暂时无法在飞书文档外展示此内容
常见问题及解决办法
数据不一致问题
- 读操作时,查询缓存一直返回空,但是查询数据表,发现有数据,即缓存数据为空,数据库数据不为空
- 读操作时,从缓存中查到的数据和数据库中的数据不一致
- 写操作后,用户进行读操作时,发现返回的数据还是之前的,即缓存和数据库中数据不一致
原因
- 读操作时,更新mysql数据库后,没有清空对应key的缓存,导致缓存中还是空的缓存或之前的缓存数据。
- 读操作时,缓存的key不正确,导致没有读取到正确的缓存,本质原因是因为清空缓存时的key和读取缓存的key不一致
解决办法
- 清空缓存key
// 检查所有的缓存key,都需要添加到该方法中,如果未添加,会导致数据不一致问题
@Override
protected void calculateCacheDimensions(VetWordsUserWordbookRef document, GoblinCacheDimension dimension) {
String key = VetWordsUserWordbookRef.ckId(document.getId());
String userIdKey = VetWordsUserWordbookRef.ckUserId(document.getUserId());
dimension.get().add(key);
dimension.get().add(userIdKey);
}
- 查询缓存key
// 检查具体的查询方法,是否正确使用了缓存key
@CacheMethod(value = VetWordsUserWordbookRef.class)
public List<VetWordsUserWordbookRef> loadByUserId(@CacheParameter(value = "UID") Long userId) {
Criteria criteria = CriteriaUtils.noDelete()
.and("user_id").is(userId);
return this.__find(Query.query(criteria));
}
总结
- 缓存的分类:静态资源缓存、动态资源缓存
- 服务端缓存的分类:进程级缓存、分布式缓存,以及不同缓存可以使用的工具框架、根据不同的应用场景,选择不同的缓存组件
- 介绍了项目中使用到的缓存框架goblin-cache,以及常见的问题和解决办法