Springboot缓存的使用

Source

我们将在Springboot框架中分别测试EhCache和Redis两种缓存技术。

一、Springboot开启默认缓存。

1. 创建Springboot工程,添加一些必要的依赖。

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.6</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.16</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.1</version>
</dependency>

2. 启动类上面添加@EnableCaching,开启缓存。

package com.chris.cache;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@EnableCaching
@SpringBootApplication
public class EhcacheApplication {

    public static void main(String[] args) {
        SpringApplication.run(EhcacheApplication.class, args);
    }

}

3. 创建一个数据类,用以处理数据。

package com.chris.cache;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserModel implements Serializable {
    private int id;
    private String name;
    private int age;
    private String address;
}

4. 创建controller类,用以测试。

package com.chris.cache;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Chris Chan
 * Create on 2021/5/5 0:11
 * Use for:
 * Explain:
 * @link . https://my.oschina.net/u/2474629/blog/4868878
 */
@RestController
@RequestMapping("api/test")
public class EhCacheController {
    @Autowired
    CacheManager cacheManager;
    /**
     * 缓存
     * 固定key
     *
     * @return
     */
    @GetMapping("list")
    @Cacheable(cacheNames = "user_model", key = "'list_all'")
    public List<UserModel> list() {
        List<UserModel> userModelList = new ArrayList<>(16);
        userModelList.add(new UserModel(1, "Chris", 40, "中国上海"));
        userModelList.add(new UserModel(2, "Marry", 20, "中国北京"));
        userModelList.add(new UserModel(3, "Mike", 30, "中国广州"));
        System.out.println("从数据库获取用户列表");
        return userModelList;
    }

    /**
     * 缓存
     * 固定key
     *
     * @param id
     * @return
     */
    @GetMapping("find")
    @Cacheable(cacheNames = "user_model", key = "'find_1'")
    public UserModel find(int id) {
        UserModel userModel = new UserModel(1, "Chris", 40, "中国上海");
        System.out.println("从数据库获取用户");
        return userModel;
    }

    /**
     * 缓存
     * 依照不同的请求参数进行缓存
     * 相同参数读缓存,不同参数再缓存
     *
     * @param id
     * @return
     */
    @GetMapping("findById")
    @Cacheable(cacheNames = "user_model", key = "#id")
    public UserModel findById(int id) {
        UserModel userModel = new UserModel(id, "Chris", 40, "中国上海");
        System.out.println("从数据库获取用户:" + id);
        return userModel;
    }

    /**
     * 修改缓存
     * 调用这个方法,无论如何都会将结果写缓存
     *
     * @param id
     * @return
     */
    @GetMapping("update")
    @CachePut(cacheNames = "user_model", key = "#id")
    public UserModel update(int id) {
        UserModel userModel = new UserModel(id, "Kalychen", 40, "中国北京");
        System.out.println("修改用户:" + id);
        return userModel;
    }

    /**
     * 删除缓存
     * 调用删除key匹配的缓存
     *
     * @param id
     * @return
     */
    @GetMapping("delete")
    @CacheEvict(cacheNames = "user_model", key = "#id")
    public String delete(int id) {
        System.out.println("从缓存删除用户:" + id);
        return "Delete User " + id + " success.";
    }
}

yml配置文件没有特别的设置。

5. 测试。

分别调用上述四个接口,从后台打印的信息可以看出是否读取了缓存。

如果打印了信息,表示方法被执行,接口模拟了从数据库查询的过程,否则没有执行查询,而是从缓存读取了需要的数据。

@Cacheable(cacheNames = "user_model", key = "#id") 查询语句一般使用这个注解处理,在第一次使用的时候会执行数据库查询,并把返回结果写入缓存。后续调用优先从缓存中读取,命中则不查询,直接返回。

@CachePut(cacheNames = "user_model", key = "#id") 写入缓存,增加和修改数据一般使用这个注解,无论有无缓存都会去操作数据库,并把返回结果写入缓存。如果有相同key则更新,以保证下次查询获取的是最新的数据。所以方法最后一定要返回完整的数据,向数据库添加数据时可能需要查询一次,以便获得正确的id,不过也可以在增加时使用缓存删除注解,在下一次有查询请求时自动缓存,这样做最好。更新操作可以把更新前的数据直接返回,不需要查询。

@CacheEvict(cacheNames = "user_model", key = "#id") 删除缓存,在删除数据库数据的时候一定要使用这个注解,否则请求结果与数据库就不一致了。

key和condition的构建请参考SpEl表达式。

6. 特别说明。

以上设置,Springboot使用的是默认的缓存管理器ConcurrentMapCacheManager,内部使用一个ConcurrentHashMap来管理缓存数据的。在上述控制层定义的cacheManager;就是用来观察的。通过断点调试可以看出当前使用的缓存技术。

二、Springboot集成EhCache缓存框架。

不用再新创建工程,就在原来的工程上面做一点改动即可。

1. 新增依赖。

<!--Spring Boot应用程序提供缓存支持-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--Ehcache缓存实现-->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
<!--JSR-107缓存规范-->
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
</dependency>

cache-api的作用没有去测试。也不知道去掉之后有没有影响。此次不关注。

2. 在resources/config下创建ehcache.xml配置文件。

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="myEncache">

    <diskStore path="G:/home/Tmp_Ehcache"/>
    
    <defaultCache
            eternal="false"
            maxElementsInMemory="1000"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="0"
            timeToLiveSeconds="600"
            memoryStoreEvictionPolicy="LRU"
    />
    <cache
            name="user_model"
            eternal="false"
            maxElementsInMemory="100"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="0"
            timeToLiveSeconds="300"
            memoryStoreEvictionPolicy="LRU"
    />

</ehcache>

具体设置参考相关文档。其中user_model是我们创建好用以测试的缓存。

3. application.yml增加相关配置。

server:
    port: 8001
spring:
    application:
        name: springboot-cache-ehcache-demo-20210505
    cache:
        ehcache:
            config: classpath:/config/ehcache.xml

主要是指明ehcache.xml的位置。

4. 测试。

测试方法同上。但是我们不知道系统当前到底是用了哪种缓存技术。于是,依然是采用断点调试的方式,观察cacheManager。

上图是我在请求过一次之后,第二次更换参数请求的截图。因为再次请求不更换参数的话,数据直接读缓存了,断点的位置就到不了。

可以看出来,cacheManager已经更换为EhCacheCacheManager了,可知当前使用的是EhCache缓存技术。不过我们发现,原来EhCache内部也是使用ConcurrentHashMap来管理缓存数据的。默认的缓存模式和EhCache都是在本地内存中存放数据的。

三、Springboot使用Redis缓存技术。

本地缓存不但消耗本地内存,也无法应用到分布式场景中。所以有一定规模的项目还是大多采用Redis缓存。

仍然是对以上项目做一点修改就好了。这次修改更简单,因为不需要EhCache那样的专门的xml配置文件。

1. 添加依赖。

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

这是Springboot封装好的实现。

2. application.yml修改。

server:
    port: 8001
spring:
    application:
        name: springboot-cache-ehcache-demo-20210505
    redis:
      host: dev.chris.com
      port: 6379
      database: 1

EhCache的设置就不要了,相关依赖包也可以清理掉。

3. 测试。

这就改完了,依计而行,观察cacheManager。不过,你确定你的Redis服务器在工作吗

依然是更换参数请求两次,当前管理器的确已经被更换为RedisCacheManager了。~~他们都超爱ConcurrentHashMap。

我们这时可以看看Redis。

我们调用的是findById这个接口,缓存中缓存了两次的数据,以id为key。

将项目重启,再调用一次接口请求,获取这两个id的数据,观察后台,并没有打印"从数据库获取用户"的字符串,表明数据还是从Redis缓存中获取的。

附加一个RedisCacheManager的配置。

package com.chris.cache;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @author Chris Chan
 * Create on 2021/5/5 3:50
 * Use for:
 * Explain:
 */
@Configuration
public class RedisCacheConfig {
    /**
     * 需要好好研究
     *
     * @param factory
     * @return
     */
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {

        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);

        jackson2JsonRedisSerializer.setObjectMapper(om);

        //配置序列化(解决乱码的问题)
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
//                .entryTtl(Duration.ZERO)
                .entryTtl(Duration.ofSeconds(15L))      //设置默认缓存15秒
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();

        Set<String> cacheNames = new HashSet<>();
        cacheNames.add("user_model");

        // 对每个缓存空间应用不同的配置
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        configMap.put("user_model", config.entryTtl(Duration.ofSeconds(60L))); //这个缓存空间60秒

        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .initialCacheNames(cacheNames)// 注意这两句的调用顺序,一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
                .withInitialCacheConfigurations(configMap)
                .build();
        return cacheManager;
    }
}

有了这个配置,缓存数据存入Redis会被序列化为json数据,直接可读。而且,也可以将需要的缓存设置过期时间。