微服务后端缓存的使用

128 阅读15分钟

缓存的作用

缓存是我们提高系统性能的一项必不可少的技术,无论是前端、还是后端,都应用到了缓存技术。前端使用缓存,可以降低多次请求服务的压力;后端使用缓存,可以降低数据库操作的压力,提升读取数据的性能。

缓存资源的类型

静态资源

不会频繁变动的文件,如图片、CSS和JavaScript文件。这些资源可以被有效地缓存以提高性能和减少带宽消耗。

动态资源

根据请求的参数或状态而动态生成的内容,如页面模板、API响应等。由于动态资源的生成是基于实时数据或用户特定请求的,因此不能永久缓存,但仍然可以使用缓存机制来减少数据库查询和计算的频率。

CDN缓存

CDN(Content Delivery Network,内容分发网络)的缓存具有重要的作用和重要性,主要体现在以下几个方面:

  1. 提高性能和加速访问:CDN的主要目标之一是提供快速而稳定的内容传输。它通过在全球范围内分布节点,并在节点上缓存静态和动态内容,使用户可以从离他们最近的节点获取内容。这样可以大大减少延迟和网络拥塞,提高用户访问的速度和性能。
  2. 缓解源服务器负载:在传统的网络架构中,用户请求直接发送到源服务器,如果用户流量集中在某一台服务器上,就会给源服务器带来很大的负载压力。而CDN通过在全球范围内设置缓存节点,可以将用户请求均匀分散到不同的节点上,从而缓解了源服务器的压力,提高了后端服务器的可扩展性。
  3. 改善用户体验和可用性:由于CDN部署了全球范围的节点,它可以确保用户在任何地方都能够快速访问内容。而且CDN的节点通常都具备故障处理机制,以应对节点故障或网络中断的情况。这样可以显著提高应用程序的可用性,并为用户提供更好的体验。
  4. 减少带宽成本:CDN的部署通常会采用就近分发原则,将内容缓存在边缘节点上,并通过就近节点服务用户。这样可以减少数据中心之间的数据传输量,从而节省带宽成本。此外,由于CDN节点通常与网络服务提供商进行互联,还可以通过缓存和一次性获取内容来减少对互联网的依赖,降低网络出口的带宽消耗和成本。

总之,CDN的缓存作用不仅可以提高性能和加速访问,减轻源服务器负载,改善用户体验和可用性,还可以降低带宽成本。它在现代网络应用中的重要性不可忽视,被广泛应用于各种领域,包括电子商务、媒体传输、流媒体、游戏等。

服务端缓存分类

本地缓存(进程级缓存)

在运行的过程中,应用数据被载入到进程中的,通过本地内存的低延迟高吞吐的特性来提高数据资源的查询效率。

常用的有Java自带容器类、Guava Cache和Ehcache等

缓存组件对比

1、Java自带容器类
  • Java自带的容器类,如List、HashMap和ConcurrentHashMap,可以被用作基本的缓存结构。它们是线程安全的,提供了键值对的存储,并具有良好的性能。
  • 优点:简单易用,无需引入额外的依赖,适合在小规模应用中使用。
  • 缺点:没有自动过期策略,需要手动管理缓存的过期和清理,缺乏一些高级的缓存功能。
2、Guava Cache

github.com/google/guav…

  • Guava Cache是Google Guava库中提供的一个内存缓存实现。它提供了一些高级的缓存功能,如缓存过期、缓存大小限制、自动加载等。
  • 优点:具有灵活的过期策略和缓存配置选项,可以根据需求进行自定义配置。支持缓存的最大大小限制,可以防止内存溢出。具备高性能和线程安全的特性。
  • 缺点:Guava Cache是基于内存的缓存实现,不适用于大规模缓存数据或分布式环境。
3、Ehcache
  • Ehcache是一个流行的开源的Java缓存框架。它为应用程序提供了灵活的缓存解决方案,支持本地缓存和分布式缓存。
  • 优点:支持缓存过期、缓存大小限制、持久化缓存等高级功能。可以与Hibernate、Spring等框架集成。支持本地和分布式环境,提供了分布式缓存一致性的保证。
  • 缺点:Ehcache在配置和使用上相对复杂一些,需要一定的学习和配置成本。对于大规模缓存和高并发环境,可能需要额外的配置和调优。
4、Caffeine

github.com/ben-manes/c…

  • Caffeine是一个高性能的Java缓存库,专注于提供快速、高效的本地缓存。它具有类似Guava Cache的功能,但性能更好。
  • 优点:具有高性能、低延迟的特点,适用于对性能要求较高的应用。支持缓存过期、缓存大小限制、异步加载等功能。具备内存优化和高并发访问的能力。
  • 缺点:Caffeine是基于内存的缓存实现,不适用于大规模缓存数据或分布式环境。
5、Guava Cache和Caffeine的区别

Guava Cache和Caffeine都是基于Java的缓存库,它们在特性和性能方面有一些区别。

区别
  1. 项目来源:Guava Cache是Google Guava开源库中的一个模块,而Caffeine是Guava Cache的改进版本,由Caffeine团队维护和开发。
  2. 特性和功能:Guava Cache和Caffeine提供了类似的缓存功能,包括缓存大小限制、过期控制和回收策略等。不过Caffeine相对于Guava Cache提供了更多的缓存优化和高级功能。
  3. 并发访问:Caffeine在并发访问方面性能更好,采用了无锁算法和细粒度的并发策略,减少了锁竞争,提供了更高的并发性能。
性能差异
  1. 内存管理:Caffeine支持堆外内存,能够更好地管理Java堆内外内存的分配和使用,减少了GC压力和堆内存消耗。
  2. 数据结构:Caffeine的缓存实现使用了比Guava Cache更高效的数据结构,如哈希表和跳表,以及针对无锁操作的优化,提高了缓存的读写性能和并发能力。
  3. 数据加载方式:Caffeine提供了更灵活的数据加载方式,包括同步加载、异步加载和缓存刷新等,以支持动态数据的加载和更新。
总结
  • Caffeine是Guava Cache的改进版本,在功能和性能上进行了优化。
  • Caffeine相较于Guava Cache,具有更好的并发访问性能、更高效的内存管理和更灵活的数据加载方式。
  • 在高并发场景下,Caffeine能够提供更好的性能和响应能力

各自应用场景

1、Java自带的容器
  1. 数据集合:用于存储、管理和操作对象的集合数据,例如使用ArrayList存储一组对象。
  2. 键值对存储:使用HashMap或ConcurrentHashMap来存储键值对数据,以快速检索和访问。
  3. 线程安全性要求低的场景:如果不需要强制的线程安全性,Java自带容器类提供了很好的性能和易用性。
2、Guava Cache
  1. 有限容量的缓存:Guava Cache提供了LRU(最近最少使用)和LFU(最近最少使用)等缓存回收策略,适用于有限容量的缓存需求。
  2. 缓存读写和过期控制:Guava Cache支持自定义的缓存加载逻辑、缓存过期策略和缓存监听器,能够方便地进行缓存数据的读写和控制。
3、Ehcache
  1. 大规模缓存:Ehcache提供了分级缓存结构、内存管理、磁盘存储和数据复制等功能,适用于大规模缓存需求。
  2. 高性能和高可用性:Ehcache支持数据复制、容错和故障恢复机制,可提供高可用性和性能的缓存服务。
  3. 分布式缓存:Ehcache可以作为分布式缓存,支持多个缓存节点之间的数据复制和一致性维护。
4、Caffeine
  1. 低延迟和高并发:Caffeine通过使用堆外内存和无锁数据结构,实现低延迟和高并发的缓存访问,适用于读密集型应用场景。

  2. 本地缓存:Caffeine主要面向本地应用,能够提供近乎实时的数据访问和处理。

  3. 高级功能和配置: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):使用布隆过滤器可以预先将可能的查询键存储在过滤器中,用于快速判断一个键是否可能在缓存中存在,从而可以避免无效查询。

  参考:blog.csdn.net/Danny_idea/…

  布隆过滤器的误判有一个特点,那就是,它只会对存在的情况有误判。如果某个数字经过布隆过滤器判断不存在,那说明这个数字真的不存在,不会发生误判;如果某个数字经过布隆过滤器判断存在,这个时候才会有可能误判,有可能并不存在。

  • 针对缓存不存在的数据,可以设置一个较短的过期时间,或者缓存一个特殊的值,以避免频繁查询数据库。

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,以及常见的问题和解决办法