【深入浅出Spring原理及实战】「缓存Cache开发系列」带你深入分析Spring所提供的缓存Cache抽象详解的核心原理探索

简介: 缓存的工作机制是先从缓存中读取数据,如果没有再从慢速设备上读取实际数据,并将数据存入缓存中。通常情况下,我们会将那些经常读取且不经常修改的数据或昂贵(CPU/IO)的且对于相同请求有相同计算结果的数据存储到缓存中。

缓存的理解

缓存的工作机制是先从缓存中读取数据,如果没有再从慢速设备上读取实际数据,并将数据存入缓存中。通常情况下,我们会将那些经常读取且不经常修改的数据或昂贵(CPU/IO)的且对于相同请求有相同计算结果的数据存储到缓存中。

它能够让数据更加接近于使用者,下图所示。

                +---------------------+
                |      应用程序       |
                +---------------------+
                           |
                           v
                +---------------------+
                |       缓存层        |
                +---------------------+
                           |
                           v
          +-----------------------------+
          |         数据源              |
          +-----------------------------+

这个图表示在应用程序和数据源之间添加了一个缓存层。在访问数据时,应用程序首先会尝试从缓存层中获取数据。如果数据已经存在于缓存中,则直接返回。否则,需要从数据源获取数据,并将其存储到缓存中,以便以后能够更快地获取相同的数据。这样做可以显著提高应用程序的性能和响应速度,并减轻数据源的负载。

缓存使用的案例

一个典型的例子是CPU--L1/L2--内存--磁盘的存储结构。当CPU需要数据时,它会先从L1/L2缓存中读取数据,如果没有则到内存中查找,最后再到磁盘上找。此外,在使用Maven时,我们的依赖通常会先从本地仓库中查找,然后再到本地服务器仓库查找,最后才去远程仓库服务器查找。京东的物流为什么如此快?这是因为他们在各个地方都建立了分仓库,如果货物在该仓库内,则配送速度非常快。

             +---------------------+ 
             |         CPU         |
             +---------------------+
                       |
                       v
           +------------------------+
           |       L1/L2 Cache      |
           +------------------------+
                       |
                       v
            +-----------------+
            |       内存        |
            +-----------------+
                       |
                       v
            +----------------------------+
            |           磁盘             |
            +----------------------------+

缓存命中率

即从缓存中读取数据的次数 与 总读取次数的比率,命中率越高越好:

命中率 = 从缓存中读取次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])
Miss率 = 没有从缓存中读取的次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])
这是一个非常重要的监控指标,如果做缓存一定要健康这个指标来看缓存是否工作良好;

基本概念

在合理应用缓存前,需要了解缓存领域里相关的几个常用术语:

  • 【缓存命中】: 表示数据已经存在于缓存中,可以直接从缓存获取数据,无需重新访问原始数据源。
  • 【缓存未命中】:表示数据不在缓存中,如果缓存空间有剩余,则会将数据添加到缓存中。
  • 【存储成本】:当出现缓存未命中时,需要从原始数据源拉取数据并将其放置到缓存中。这个操作的时间和空间成本称为“存储成本”。
  • 【缓存失效】:当数据源发生更改时,缓存中的数据将变得过时,需要更新或清除缓存以确保下次请求时能够访问最新数据。
  • 【缓存污染】:将不常访问的数据存储到缓存中,可能会占用缓存空间,导致高频访问的数据无法存储到缓存中。
  • 【移除策略】(Eviction policy):即如果缓存满了,从缓存中移除数据的策略;常见的有LFU、LRU、FIFO等,需要通过替换策略删除缓存中的一些数据来腾出空间。常见的替换策略包括以下几种:

    • 先进先出(FIFO,First In First Out):先进先出算法,即删除缓存中最早添加的数据。
    • 最近最少使用(LRU,Least Recently Used):最久未使用算法,删除最长时间未被访问的数据,使用时间距离现在最久的那个被移除;
    • 最少使用(LFU,Least Frequently Used):最近最少使用算法,删除最少访问次数的数据,一定时间段内使用次数(频率)最少的那个被移除;
    • 随机替换(RR):随机选择一个数据进行删除。
  • 【存活期】TTL(Time To Live ),即从缓存中创建时间点开始直到它到期的一个时间段(不管在这个时间段内有没有访问都将过期)

  • 【空闲期】TTI(Time To Idle),即一个数据多久没被访问将从缓存中移除的时间。

到此,我们已经基本了解了缓存的概念。在Java中,我们通常会对方法调用进行缓存控制,例如对于方法 "getValueByKey(String key)",我们应该首先从缓存中查找是否有对应的数据,如果没有再调用该方法从数据库中加载用户数据,并将其添加到缓存中。这样,在下一次调用相同的方法时,就可以直接从缓存中获取数据,而无需再次访问数据库。


Spring Cache API及默认提供的实现

自Spring 3.1起,提供了Cache支持,且提供了Cache抽象,在此之前一般通过AOP实现,使用Spring Cache的好处:提供基本的Cache抽象,方便切换各种底层Cache;通过注解Cache可以实现类似于事务一样,缓存逻辑透明的应用到我们的业务代码上,且只需要更少的代码就可以完成;

对于Spring Cache抽象,主要从以下几个方面学习:


+------------------------+           +-------------------------+
|       Cache API        |           |   默认提供的Cache实现   |
+------------------------+           +-------------------------+
         |                                     |
         v                                     v
+------------------------+           +-------------------------+
|      Cache注解         |           | 实现复杂的Cache逻辑      |
+------------------------+           +-------------------------+

Spring提供的核心Cache接口

开发者首先可以通过Cache API与默认提供的Cache实现进行交互,获取并存储数据。而在一些情况下,可以通过使用Cache注解来简化对缓存的使用。此外,如果需要实现更复杂的缓存逻辑,也可以自己实现相应的缓存逻辑。

Java代码

package org.springframework.cache;  

public interface Cache {
     
    String getName();  //缓存的名字  
    Object getNativeCache(); //得到底层使用的缓存,如Ehcache  
    ValueWrapper get(Object key); //根据key得到一个ValueWrapper,然后调用其get方法获取值  
    <T> T get(Object key, Class<T> type);//根据key,和value的类型直接获取value  
    void put(Object key, Object value);//往缓存放数据  
    void evict(Object key);//从缓存中移除key对应的缓存  
    void clear(); //清空缓存  
    interface ValueWrapper {
    //缓存值的Wrapper  
        Object get(); //得到真实的value  
    }  
}

通过上面的源码可以看出来,Spring-Cache是提供了缓存操作的读取/写入/移除方法。

默认提供了如下实现:

  • ConcurrentMapCache:使用java.util.concurrent.ConcurrentHashMap实现的Cache;
  • GuavaCache:对Guava com.google.common.cache.Cache进行的Wrapper,需要Google Guava 12.0或更高版本,@since spring 4;
  • EhCacheCache:使用Ehcache实现
  • JCacheCache:对javax.cache.Cache进行的wrapper,@since spring 3.2;spring4将此类更新到JCache 0.11版本;

另外,因为我们在应用中并不是使用一个Cache,而是多个,因此Spring还提供了CacheManager抽象,用于缓存的管理:

Java代码

package org.springframework.cache;  
import java.util.Collection;  
public interface CacheManager {
     
    Cache getCache(String name); //根据Cache名字获取Cache   
    Collection<String> getCacheNames(); //得到所有Cache的名字  
}

默认提供的实现:

  • ConcurrentMapCacheManager/ConcurrentMapCacheFactoryBean:管理ConcurrentMapCache;
    GuavaCacheManager;
  • EhCacheCacheManager/EhCacheManagerFactoryBean;
  • JCacheCacheManager/JCacheManagerFactoryBean;

另外还提供了CompositeCacheManager用于组合CacheManager,即可以从多个CacheManager中轮询得到相应的Cache,如

Java代码
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">  
    <property name="cacheManagers">  
        <list>  
            <ref bean="ehcacheManager"/>  
            <ref bean="jcacheManager"/>  
        </list>  
    </property>  
    <property name="fallbackToNoOpCache" value="true"/>  
</bean>

当我们调用cacheManager.getCache(cacheName) 时,会先从第一个cacheManager中查找有没有cacheName的cache,如果没有接着查找第二个,如果最后找不到,因为fallbackToNoOpCache=true,那么将返回一个NOP的Cache否则返回null。

除了GuavaCacheManager之外,其他Cache都支持Spring事务的,即如果事务回滚了,Cache的数据也会移除掉。

Spring不进行Cache的缓存策略的维护,这些都是由底层Cache自己实现,Spring只是提供了一个Wrapper,提供一套对外一致的API。

Spring缓存的问题

Spring缓存模糊匹配Evict的问题

缓存都是key-value风格的,模糊匹配本来就不应该是Cache要做的;而是通过自己的缓存代码实现;

Spring cache的缺陷

假设有一个缓存,其中存储了list类型的数据。现在我执行了一个update(user)方法,但并不想清除整个缓存,而只是希望替换掉更新过的元素。

在现有的缓存抽象中,并没有很好的解决方案,可以考虑通过在之前的Helper方法中使用condition来解决这个问题,但这并不是一种十分优雅的方式。

因此,我认为Spring Cache注解还存在一些不完美之处。针对这种情况,我们可以通过对Cacheable注解进行增强来实现更加灵活的缓存控制。具体地说,我们可以在Cacheable注解中添加SpEL表达式,用于定制要缓存的数据、方法执行前后的条件以及缓存成功后的其他逻辑。例如:

@Cacheable(cacheName = "缓存名称", key="缓存key/SpEL", value="缓存值/SpEL/不填默认返回值", beforeCondition="方法执行之前的条件/SpEL", afterCondition="方法执行后的条件/SpEL", afterCache="缓存之后执行的逻辑/SpEL")

当然,对于大多数场景,Spring Cache注解已经足够使用。如果需要处理更加复杂的场景,则可以考虑使用AOP。如果需要自定义实现缓存逻辑,则应该使用Spring Cache API进行缓存抽象。

相关文章
|
25天前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
38 4
|
18天前
|
自然语言处理 Java API
Spring Boot 接入大模型实战:通义千问赋能智能应用快速构建
【10月更文挑战第23天】在人工智能(AI)技术飞速发展的今天,大模型如通义千问(阿里云推出的生成式对话引擎)等已成为推动智能应用创新的重要力量。然而,对于许多开发者而言,如何高效、便捷地接入这些大模型并构建出功能丰富的智能应用仍是一个挑战。
74 6
|
19天前
|
XML Java 数据格式
提升效率!Spring Boot 开发中的常见失误轻松规避
本文深入探讨了在 Spring Boot 开发中常见的失误,包括不当使用注解、不良异常处理、低效日志记录等,提供了有效的规避策略,帮助开发者提升代码质量和系统性能,构建更健壮、高效的应用程序。
|
21天前
|
缓存 NoSQL Java
Spring Boot与Redis:整合与实战
【10月更文挑战第15天】本文介绍了如何在Spring Boot项目中整合Redis,通过一个电商商品推荐系统的案例,详细展示了从添加依赖、配置连接信息到创建配置类的具体步骤。实战部分演示了如何利用Redis缓存提高系统响应速度,减少数据库访问压力,从而提升用户体验。
55 2
|
3天前
|
安全 Java 测试技术
Java开发必读,谈谈对Spring IOC与AOP的理解
Spring的IOC和AOP机制通过依赖注入和横切关注点的分离,大大提高了代码的模块化和可维护性。IOC使得对象的创建和管理变得灵活可控,降低了对象之间的耦合度;AOP则通过动态代理机制实现了横切关注点的集中管理,减少了重复代码。理解和掌握这两个核心概念,是高效使用Spring框架的关键。希望本文对你深入理解Spring的IOC和AOP有所帮助。
8 0
|
28天前
|
Java BI API
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
这篇文章介绍了如何在Spring Boot项目中整合iTextPDF库来导出PDF文件,包括写入大文本和HTML代码,并分析了几种常用的Java PDF导出工具。
325 0
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
|
24天前
|
XML Java 数据格式
Spring IOC容器的深度解析及实战应用
【10月更文挑战第14天】在软件工程中,随着系统规模的扩大,对象间的依赖关系变得越来越复杂,这导致了系统的高耦合度,增加了开发和维护的难度。为解决这一问题,Michael Mattson在1996年提出了IOC(Inversion of Control,控制反转)理论,旨在降低对象间的耦合度,提高系统的灵活性和可维护性。Spring框架正是基于这一理论,通过IOC容器实现了对象间的依赖注入和生命周期管理。
61 0
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
68 6
|
1天前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构
下一篇
无影云桌面