SpringBoot2基于Spring Cache实现缓存(包括集成Redis和EhCache)

SpringBoot基于Spring Cache实现缓存(包括集成Redis和EhCache)

  • 缓存的目的是:通过Cache来缓存不经常改变的数据以提高系统性能和增加系统吞吐量,避免直接访问数据库等低速的存储系统。
  • Spring Cache对Cache进行抽象,提供了@Cacheable、@CachePut、@CacheEvict等注解。
  • 可用于单体应用系统,也可集成Redis等缓存服务器用于大型系统或者分布式系统。

一、使用Spring自带的缓存管理器

  • Spring自带的缓存类型为Simple,这个缓存与Spring Boot应用在同一个Java虚拟机内,适合单体应用系统。
  1. pom文件加入依赖
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-cacheartifactId>
dependency>
  1. application.properties中配置属性
spring.cache.type=Simple
  • Simple:基于ConcurrentHashMap实现的缓存,只适合单体应用或者开发环境使用。
  • None:禁止使用缓存。
  • Redis:使用Redis缓存。
    等…
  1. 然后在启动或者配置类上加入@EnableCaching注解来开启缓存注解。
//开启缓存注解
@EnableCaching
@SpringBootApplication
public class  Application {
    public static void main(String[] args) {
        SpringApplication.run(Application .class, args);
    }
}
  1. 示例一个Service实现类,并进行统一解释。
import com.example.demo.bean.User;
import com.example.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
@CacheConfig(cacheNames = {"user"})
//统一指定value的值,这时可省略value,如果你在你的方法依旧写上了value,那么依然以方法的value值为准。
public class UserServiceImpl implements UserService {

    private static final Map<Integer, User> users = new HashMap<>(3);

    static {
        users.put(1, new User(1,"bym"));
        users.put(2, new User( 2,"wx"));
        users.put(3, new User( 3,"sqp"));
    }
    
	@Override
    //@Cacheable(key = "targetClass + methodName +#p0")
    //@Cacheable(value="users", key="#p0")
    //@Cacheable(key = "#id")
    @Cacheable
    public User getUser(int id) {
        log.info("缓存中没有,从数据中取...");
        return users.get(id);
    }

    /**
     * 在更新数据的同时,缓存也被更新
     * @param user
     * @return
     */
    @Override
    @CachePut(cacheNames = "space", key = "#user.id")
    public User updateUser(User user) {
        log.info("更新数据...");
        users.put(user.getId(), user);
        return user;
    }

    //清除一条缓存,key为要清空的数据
    @CacheEvict(value = "space", key = "#id")
    public void delete(int id) {
        users.remove(id);
    }

    //方法调用后清空所有缓存
    @CacheEvict(value = "space", allEntries = true)
    public void deleteAll() {
        users.clear();
    }

    //方法调用前清空所有缓存
    @CacheEvict(value = "space", beforeInvocation = true)
    public void deleteAllBefore() {
        users.clear();
    }

    /**
     *   @Caching是 @Cacheable、@CachePut、@CacheEvict注解的组合
     *   以下注解的含义:
     *   1.当使用指定名字查询数据库后,数据保存到缓存
     *   2.现在使用id、age就会直接查询缓存,而不是查询数据库
     */

}

注意:这里的User实体类要实现Serializable序列化接口。public class User implements Serializable,否则会报异常。
SpringBoot2基于Spring Cache实现缓存(包括集成Redis和EhCache)_第1张图片

@Cacheable

@Cacheable作用在方法上,声明了方法的结果是可缓存的。

  1. 如果缓存存在,则目标方法不会被调用,直接取出缓存。可以为方法声明多个缓存,如果至少有一个缓存有缓存项,则其缓存项将被返回。
  2. 如果缓存不存在,则进入实际业务方法,将业务方法返回的结果缓存下来。

属性

SpringBoot2基于Spring Cache实现缓存(包括集成Redis和EhCache)_第2张图片
打开此注解的源码,如下:

@AliasFor("cacheNames")
String[] value() default {};

@AliasFor("value")
String[] cacheNames() default {};

String key() default "";
String keyGenerator() default "";//key的生成器。key/keyGenerator二选一使用
String cacheManager() default "";//指定缓存管理器
String cacheResolver() default "";//或者指定获取解析器
String condition() default "";//条件符合则缓存
String unless() default "";//条件符合则不缓存
boolean sync() default false; //是否使用异步模式
  1. cacheNamesvalue是一个意思,都表示指定缓存的命名空间(或名称),注解中必须指定命名空间。(本文指定用cacheNames)
    写法:
@Cacheable
@Cacheable("user")
@Cacheable({"user","info"})
@Cacheable(cacheNames="user",key="xx")
@Cacheable(cacheNames={"user","info"},key="xx")
  1. key指缓存的key。
    1)可以为空,为空时,Spring使用KeyGenerator类来根据方法参数生成key。
KeyGenerator

KeyGenerator的规则:
1.如果方法只有一个参数,这个参数就是Key。
2.如果没有参数,则Key是SimpleKey.EMPTY。
3.如果有多个Key,则返回包含多个参数的SimpleKey。
Spring使用SimpleKeyGenerator来实现上述key的生成。

源码:

package org.springframework.cache.interceptor;

import java.lang.reflect.Method;

public class SimpleKeyGenerator implements KeyGenerator {
    public SimpleKeyGenerator() {
    }

    public Object generate(Object target, Method method, Object... params) {
        return generateKey(params);
    }

    public static Object generateKey(Object... params) {
        if (params.length == 0) {
            return SimpleKey.EMPTY;
        } else {
            if (params.length == 1) {
                Object param = params[0];
                if (param != null && !param.getClass().isArray()) {
                    return param;
                }
            }

            return new SimpleKey(params);
        }
    }
}

我们还可以实现自己的KeyGenerator方法:

@Cacheable(cacheNames="users",keyGenerator="myKg)
   public User getUser(int id) {...}

myKg实现了KeyGenerator接口,然后从user中获取到id作为缓存的Key。

@Override
public Object generate(Object target, Method method, Object... params) {
    User user = (User)params[0];
    return user.getId();
   }

通常情况下,直接使用SpEL表达式来指定Key比自定义KeyGenerator更简单。

2)可按照SpEL表达式编写。

SpEL
名称 位置 描述 示例
methodName root对象 当前被调用的方法名 #root.methodname
method root对象 当前被调用的方法 #root.method.name
target root对象 当前被调用的目标对象实例 #root.target
targetClass root对象 当前被调用的目标对象的类 #root.targetClass
args root对象 当前被调用的方法的参数列表 #root.args[0]
caches root对象 当前方法调用使用的缓存列表 #root.caches[0].name
Argument Name 执行上下文 当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数 #artsian.id
result 执行上下文 方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false) #result

1.当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。 如

@Cacheable(key = "targetClass + methodName +#p0")

2.使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。 如:

@Cacheable(value="users", key="#id")
public User method1(int id) {...}

@Cacheable(value="users", key="#p0")
public User method2(int id,String name) {...}

@Cacheable(value="users", key="#user.id")
public User method3(User user) {...}

3.SpEL提供了多种运算符

类型 运算符
关系 <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne
算术 +,- ,* ,/,%,^
逻辑 &&,
条件 ?: (ternary),?: (elvis)
正则表达式 matches
其他类型 ?.,?[…],![…],$[…]

@CachePut

@CachePut作用在方法上,总是会执行方法体,使用方法体返回的结果更新缓存。属性和Cacheable一样。需要注意的是该注解的value 和 key 必须与要更新的缓存相同,也就是与@Cacheable 相同。

@CacheEvict

@CacheEvict作用在方法上,能够根据一定的条件删除缓存项或清空缓存。
这里需要注意两个属性:

属性 解释 示例
allEntries 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 @CachEvict(value=”testcache”,allEntries=true)
beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 @CachEvict(value=”testcache”,beforeInvocation=true)

注意:CacheEvict只具备删除缓存功能,不具备加载缓存功能,只有相应的@Cacheable方法被调用后,才会加载最新缓存项。

缓存系统中不经常改变的业务数据一旦发生改变时,通常会更新此业务相关的所有缓存 。

  • 以菜单为例,如果添加菜单项目,应更新菜单相关的所有缓存。
//方法调用后清空所有缓存
    @CacheEvict(value = {"menu","menuTree"}, allEntries = true)
    public void addMenu(Menu menu) {
        users.clear();
    }

在更新菜单后,菜单树和菜单缓存都需要清空以便下次获取的时候重新构造。

@Caching

@Caching作用在方法上,可以混合以上三种注解,比如一个修改同时需要失效对应的用户缓存和用户扩展信息缓存。
Caching的属性

@Caching(evict = {
            @CacheEvict(value = "user",key = "#user.id"),
            @CacheEvict(value = "userExt",key = "#ext.id")
    })
public void updateUser(User user,UserExt ext) {...}

@CacheConfig

到目前为止,所有的Cache注解都需要提供Cache名称,如果每个Service方法上都包含一个Cache名称,可能写起来重复,注解@CacheConfig作用于类上,可以为此类的方法的缓存注解提供默认值,包括缓存的默认名称和KeyGenerator。
SpringBoot2基于Spring Cache实现缓存(包括集成Redis和EhCache)_第3张图片

用法:@CacheConfig("users")等。
这时可省略方法上的value,如果你在你的方法依旧写上了value,那么依然以方法的value值为准。

二、Spring Cache整合Redis

  • 对于分布式应用,通常都会将缓存放在一台或者多台专门的缓存服务器上。使用Redis作为缓存是一种常用的选择。
  1. pom文件加入依赖
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

<dependency>
    <groupId>org.apache.commonsgroupId>
    <artifactId>commons-pool2artifactId>
dependency>
  1. application.properties中配置属性
spring.cache.type=Redis
spring.redis.host=192.168.2.185
spring.redis.port=6379
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0

其注解使用方式与Simple方式一致。

  • 通过上面两步的配置,SpringBoot的CacheManager就会使用RedisCache。
定制缓存:对于RedisCacheManager来说,还可以定制缓存项的存活时间,还有修改缓存序列化策略。
  • 添加一个配置类:
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class CacheConfig extends CachingConfigurerSupport {
    //缓存管理器
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        // 生成一个默认配置,通过config对象即可对缓存进行自定义配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        //此配置可以使redis的值不为乱码。
             	.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));
        // 设置缓存的默认过期时间,也是使用Duration设置
//        config = config.entryTtl(Duration.ofMinutes(10))
//                .disableCachingNullValues();     // 不缓存空值

        // 设置一个初始化的缓存空间set集合
        Set<String> cacheNames = new HashSet<>();
        cacheNames.add("space");
        cacheNames.add("user");

        // 对每个缓存空间应用不同的配置
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        // 通过Duration可以自己实现以什么时间为单位
        configMap.put("space", config.entryTtl(Duration.ofMinutes(1)));
        configMap.put("user", config.entryTtl(Duration.ofSeconds(10)));

        return RedisCacheManager
                .builder(RedisCacheWriter.nonLockingRedisCacheWriter(factory))
                .initialCacheNames(cacheNames)  
                // 注意这两句的调用顺序,一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
                .withInitialCacheConfigurations(configMap)
                .cacheDefaults(config).build();
    }
//    修改redis默认的缓存序列化策略(jdk序列化),
//	  这里我们注入了一个RedisTemplate 设置了里面的序列化,然后呢把他注入到redisCacheManger里就可以了。
//    @Bean
//    public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
//        RedisTemplate template = new RedisTemplate<>();
//        template.setConnectionFactory(connectionFactory);
//        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
//        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
//        ObjectMapper mapper = new ObjectMapper();
//        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//        serializer.setObjectMapper(mapper);
//        template.setValueSerializer(serializer);
//        //使用StringRedisSerializer来序列化和反序列化redis的key值
//        template.setKeySerializer(new StringRedisSerializer());
//        template.afterPropertiesSet();
//        return template;
//    }
}
  • 配置上面一步前:SpringBoot2基于Spring Cache实现缓存(包括集成Redis和EhCache)_第4张图片
    同上
  • 配置成功后没有乱码问题
    SpringBoot2基于Spring Cache实现缓存(包括集成Redis和EhCache)_第5张图片

二、Spring Cache整合EhCache

  • EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认CacheProvider
  • Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。
主要特性:
  1. 快速
  2. 简单
  3. 多种缓存策略
  4. 缓存数据有两级:内存和磁盘,因此无需担心容量问题
  5. 缓存数据会在虚拟机重启的过程中写入磁盘
  6. 可以通过RMI、可插入API等方式进行分布式缓存
  7. 具有缓存和缓存管理器的侦听接口
  8. 支持多缓存管理器实例,以及一个实例的多个缓存区域
  9. 提供Hibernate的缓存实现

ehcache 对分布式支持不够好,多个节点不能同步,通常和redis一块使用

和Redis比较:
  • ehcache直接在jvm虚拟机中缓存,速度快,效率高;但是缓存共享麻烦,集群分布式应用不方便。

  • redis是通过socket访问到缓存服务,效率比ecache低,比数据库要快很多,
    处理集群和分布式缓存方便,有成熟的方案。如果是单个应用或者对缓存访问要求很高的应用,用ehcache。如果是大型系统,存在缓存共享、分布式部署、缓存内容很大的,建议用redis。

ehcache也有缓存共享方案,不过是通过RMI或者Jgroup多播方式进行广播缓存通知更新,缓存共享复杂,维护不方便;简单的共享可以,但是涉及到缓存恢复,大数据缓存,则不合适。

ehcache3.x与2.x的差距还是非常大的,主要区别在于3.x后使用了java的缓存规范JSR107!!!

EhCache3.x
  1. pom文件加入依赖

<dependency>
    <groupId>javax.cachegroupId>
    <artifactId>cache-apiartifactId>
dependency>
<dependency>
    <groupId>org.ehcachegroupId>
    <artifactId>ehcacheartifactId>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-cacheartifactId>
dependency>
  1. yml配置
    需要说明的是默认路径为config: classpath:/ehcache.xml如果在这个目录下这个配置可以不用写,但ehcache.xml必须有。
spring:
 cache:
   type: jcache
   jcache:
     config: classpath:/cache/ehcache.xml
  1. 配置文件
    在resources的cache目录下新建ehcache.xml

<config
    xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
    xmlns='http://www.ehcache.org/v3'
    xmlns:jsr107='http://www.ehcache.org/v3/jsr107'
    xsi:schemaLocation="
        http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
        http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">

    <cache-template name="heap-cache">
        <resources>
            <heap unit="entries">2000heap>
            <offheap unit="MB">100offheap>
        resources>
    cache-template>

    <cache alias="myuser" uses-template="heap-cache">
        <expiry>
            <ttl unit="seconds">40ttl>
        expiry>
    cache>

config>

然后使用的时候@CacheConfig(cacheNames = {"myuser"})中的cacheNames的名字,xml中的alias必须也有,不然会报找不到缓存名。

EhCache2.x
  1. pom文件加入依赖

 <dependency>
     <groupId>net.sf.ehcachegroupId>
     <artifactId>ehcacheartifactId>
 dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-cacheartifactId>
dependency>
  1. yml配置
    需要说明的是默认路径为config: classpath:/ehcache.xml如果在这个目录下这个配置可以不用写,但ehcache.xml必须有。
spring:
 cache:
   type: ehcache
   ehcache:
     config: classpath:/cache/ehcache.xml
  1. 配置文件
<ehcache>
    
    <diskStore path="java.io.tmpdir" />
    
    <defaultCache maxElementsInMemory="10000" eternal="false"
                  timeToIdleSeconds="600" timeToLiveSeconds="600" overflowToDisk="true" />
    <cache name="myCache" maxElementsInMemory="10000" eternal="false"
                  timeToIdleSeconds="120" timeToLiveSeconds="600" overflowToDisk="true" />
ehcache>

同样,使用的时候@CacheConfig(cacheNames = {"myuser"})中的cacheNames的名字,xml中的alias必须也有,不然会报找不到缓存名。

  • 也是通过注解使用缓存。

Demo

Simple方式与Redis方式

参考

《Spring Boot 2 精髓——从构建小系统到架构分布式大系统》
一起来学SpringBoot(十)缓存的使用

扩展

Spring Boot2.0+Redis+Ehcache实现二级缓存
spring boot + spring cache 实现两级缓存(redis + ehcache)
Spring Boot 使用Redis和Ehcache结合zookeeper实现二级缓存

你可能感兴趣的:(SpringBoot)