拿什么拯救你的性能(2) - 更换Cache实现

作者: 虾游于海 | 来源:发表于2018-02-12 16:35 被阅读166次

    在上一集中,我们使用了默认的ConcurrentMapCacheManager作为Spring Cache的默认实现。在小型的应用中,这已经足够了,但在实际的项目中,我们可能需要引入JCache,Redis,MemCache等更加成熟的缓存技术.下面我们就来看看吧.

    JCache

    JCache通常也叫JSR-107,JCache是一套规范.
    JCACHE规范承诺为Java提供一套标准API,通过这套API,编程人员可以透明地操作数据,不用关心数据放在哪里.
    我们来看看,它的初衷和Spring Cache的目的是一样的,都是一套规范.
    同时,JCache提供了一套类似于Spring Cache的annotation来标注方法和类.包括@CacheResult,@CachePut,@CacheRemove等,这些注解都位于javax.cache.annotation包下.
    在使用Spring Cache的时候,我们也可以使用JCache的注解,Spring 能正确的根据注解实现相应的缓存逻辑.
    JCache有各种实现,比较典型的是EhCache3,Hazelcast等,Spring Cache可以直接使用JCache的各种实现来作为自己的Cache实现. 下面我们以Spring Boot使用EhCache为例。改造上一次的代码

    首先,导入必要依赖

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-cache</artifactId>
     </dependency>
    <dependency>
      <groupId>javax.cache</groupId>
      <artifactId>cache-api</artifactId>
    </dependency>
    <dependency>
      <groupId>org.ehcache</groupId>
      <artifactId>ehcache</artifactId>
    </dependency>
    

    这里的cache-api就是JSR-107所定义的相关的Cache的接口.
    ehcache就是ehcache3.

    启用Spring Cache

    @SpringBootApplication
    @EnableCaching
    public class EhCacheApplication {
      public static void main(String[] args) throws Exception {
        SpringApplication.run(EhCacheApplication.class, args);
      }
    }
    

    我们所有的代码没有变化,在controller中加入一个接口,用来获取系统中实际使用的cacheManager

    /**
     * 注入Spring Boot生成的CacheManager
     */
    @Autowired
    private CacheManager cacheManager;
    
    /**
     * 获取并显示实际使用的CacheManager
     */
    @GetMapping("manager")
    public String getManager() {
      return cacheManager.toString();
    }
    

    在浏览器中输入相关URL,可以获得结果
    org.springframework.cache.jcache.JCacheCacheManager@xxxx
    我们可以注释掉相关的依赖,即pom.xml中的cache-api和ehcache两项,再运行相关的接口,接口会返回相应的数据
    org.springframework.cache.concurrent.ConcurrentMapCacheManager@xxxx.

    Spring Boot会按照如下优先级来自动装配一个CacheManager
    Generic
    JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, etc)
    EhCache 2.x
    Hazelcast
    Infinispan
    Couchbase
    Redis
    Caffeine
    Guava (deprecated)
    Simple
    参见Spring Boot文档.
    当Spring Boot按照以上顺序检测到某一个缓存实现存在的话,会自动构建一个使用相应实现的CacheManager.并跳过后续检测。

    配置JCache的配置文件

    我们使用ehcache3作为缓存实现的时候,可以对ehcache进行配置,以实现缓存过期等策略。具体的包括

    • 简单配置
      我们的代码,到目前为止,当我们启用了ehcache3,并进行了相关的接口调用的时候,会抛出异常java.lang.IllegalArgumentException,Cannot find cache named 'test' for Builder.这是因为ehcahce需要我们显示的声明Cache.
      我们可以在Spring Boot的配置文件 src\main\resources\application.yml中声明Cache
    spring:
      cache:
        cache-names:
        - test
    

    cache-names是一个列表,声明了在项目中用到的cache的名称。

    • 完整的Ehcache配置
      在yml/properties文件中,我们只能进行一些简单的配置。如果要进行复杂的配置,我们需要指定一个配置文件所在的路径。
      修改后的yml如下
    spring:
      cache:
        jcache:
          config: classpath:cache.xml
    

    并且,我们在resources目录下面建立cache.xml文件。进行cache的配置

    <?xml version="1.0" encoding="UTF-8"?>
    <config xmlns='http://www.ehcache.org/v3' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
        xsi:schemaLocation="http://www.ehcache.org/v3
            http://www.ehcache.org/schema/ehcache-core-3.0.xsd">
    
        <cache-template name="default">
            <heap unit="MB">1</heap>
        </cache-template>
    
        <cache alias="test" uses-template="default">
            <expiry>
                <ttl unit="seconds">10</ttl>
            </expiry>
        </cache>
    </config>
    

    在这里,我们通过alias值定了cache的名称,通过ttl指定了cache过期时间,这里指定为10秒。我们重新访问接口时,当距离上次缓存的时间大于10秒钟的时候,都会重新执行一次方法。
    更多ehcache的配置文件,可以参考Ehcache官方网站,不在这里展开。

    • 缓存并发穿透
      并发穿透,指的是当缓存过期失效后。如果瞬间有大量的请求进来,这些请求在执行的时候,均会查询缓存,这些查询都会导致缓存没有命中,进而执行实际的代码。如果这些代码是持久层的操作,或者是比较耗时的操作的,会导致计算压力瞬间倍增。
      通常要解决这些场景的问题,需要一些线程同步的能力,但在Spring Cache 4.3以后的版本中,这种情况大为改善。我们只需要在Cacheable的注解中,指定sync=true即可。示例代码如下。
    @Override
    @Cacheable(cacheNames = "test", sync = true)
     public String get(String id) {
        // 记录数据产生的时间,用于测试对比
        long time = new Date().getTime();
        // 打印使用到的cacheManager
        logger.info("The cacheManager is" + cacheManager);
        // 当数据不是从cache里面获取时,打印日志
        logger.info("Get value by id=" + id + ", The time is " + time);
        return "Get value by id=" + id + ",the value is " + enties.get(id);
    }
    

    当缓存失效之后,有请求并发访问到这里的时,只会有一个线程实际执行方法体,其它的请求等待之前的线程执行并缓存结果。这大大简化了并发的处理逻辑。

    Redis

    redis是一个常用的集中式缓存服务。Spring对Redis也进行了集成,我们可以方法的使用RedisTemplate进行Redis的读写操作。当然,我们也可以非常方便的将Spring的缓存实现更改为Redis实现,你只需要加入Redis相关的依赖。

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    

    然后在配置文件中配置相关的redis连接信息。以上的代码不用更改,即可实现使用Redis作为缓存的实现。

    指定缓存类型

    当项目中同时存在多种技术时,Spring会按照一定的顺序去寻找缓存的实现。但有时候我们需要显示的指定缓存的实现,比如当比如Jcache和redis共存时,Spring会使用JCache作为缓存的实现,而事实上我们可能需要的是Redis.这时,就需要我们手工显示的指定实现。在yml/properties文件中指定即可

    spring:
      cache:
        type: redis
    

    可供选择的类型在org.springframework.boot.autoconfigure.cache.CacheType枚举中。

    点击这里下载相关代码

    小结:缓存在互联网时代是非常重要的技术,也不是一两篇文章就能讲完的,大家一起研究,一起学习。
    距离上一篇已经过去了一个月了,码字的速度好慢。

    过年了,新年快乐。

    相关文章

      网友评论

        本文标题:拿什么拯救你的性能(2) - 更换Cache实现

        本文链接:https://www.haomeiwen.com/subject/qkzjoxtx.html