spring cache实现方法缓存(spring boot + redis)

本文代码示例:https://gitee.com/imlichao/redis_cache-example

分析

    缓存是分布式系统中的重要组件,主要解决高并发,大数据场景下,热点数据访问的性能问题。提供高性能的数据快速访问。本文主要介绍基于springboot 框架下使用spring cache和redis进行方法缓存的方案。

    方法缓存比较适合于静态页面或查询结果复用性较高的业务。举个例子,一个接口负责查询首页广告,如果每个用户看到的首页广告都是一样的,那么就可以直接从缓存中读取大大减少了系统性能消耗。对于不适合使用缓存的业务也举个例子,一个接口负责查询离当前用户坐标最近的商家,显然用户坐标相同的几率会非常低,所以这种缓存即消耗空间又很难命中。综上所述,访问量大且出入参是一个有限集合的业务更加适合缓存。

    缓存与数据源的一致性也是比较重要的问题。如果数据已经产生变化而缓存不更新,那么我们将读取不到最新的数据。所以业务的实时性要求决定了缓存一致性的实时性。由于一致性问题需要在更新节点增加大量的缓存失效逻辑,所以我们的项目目前并没有进行改造。当前的方案是缓存在一定时间内自动失效,也就是说缓存设置为5分钟失效,那么修改的内容将在5分钟后才能被查询出来。当然这只是在实时性要求不高的业务中使用。推荐一篇解决一致性问题的文章:https://blog.csdn.net/java_dyq/article/details/51997045

 

Spring 声明式缓存管理

API文档:https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/cache/annotation/package-summary.html

Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。

Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。

其特点总结如下:

  • 通过少量的配置 annotation 注释即可使得既有代码支持缓存
  • 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存
  • 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
  • 支持 AspectJ,并通过其实现任何方法的缓存支持
  • 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性

 

声明式缓存常用注释

spring cache 中最主要使用三个注释标签,即 @Cacheable、@CachePut 和 @CacheEvict

@Cacheable 的作用
主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。
@Cacheable 主要的参数
value缓存的名称

每一个缓存名称代表一个缓存对象。当一个方法填写多个缓存名称时将创建多个缓存对象。当多个方法使用同一缓存名称时相同参数的缓存会被覆盖。所以通常情况我们使用“包名+类名+方法名”或者使用接口的RequestMapping作为缓存名称防止命名重复引起的问题。

单缓存名称:@Cacheable(value=”mycache”) 
多缓存名称:@Cacheable(value={”cache1”,”cache2”}

key缓存的 key

key标记了缓存对象下的每一条缓存。如果不指定key则系统自动按照方法的所有入参生成key,也就是说相同的入参值将会返回同样的缓存结果。

如果指定key则要按照 SpEL 表达式编写使用的入参列表。如下列无论方法存在多少个入参,只要userName值一致,则会返回相同的缓存结果。

@Cacheable(value=”testcache”,key=”#userName”)

condition缓存的条件

满足条件后方法结果才会被缓存。不填写则认为无条件全部缓存。

条件使用 SpEL表达式编写,返回 true 或者 false,只有为 true 才进行缓存

如下例,只有用户名长度大于2时参会进行缓存
@Cacheable(value=”testcache”,condition=”#userName.length()>2”)

 

@CachePut 的作用
主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。和 @Cacheable 不同的是,它每次都会触发真实方法的调用,此注解常被用于更新缓存使用。
@CachePut 主要的参数
value缓存的名称

例如:
@CachePut(value=”mycache”) 
@CachePut(value={”cache1”,”cache2”}

key缓存的 key例如:
@CachePut(value=”testcache”,key=”#userName”)
condition缓存的条件例如:
@CachePut(value=”testcache”,condition=”#userName.length()>2”)

 

@CacheEvict 的作用
主要针对方法配置,能够根据一定的条件对缓存进行清空
@CacheEvict 主要的参数
value缓存的名称

删除指定名称的缓存对象。必须与下面的其中一个参数配合使用

例如:
@CacheEvict(value=”mycache”) 或者
@CacheEvict(value={”cache1”,”cache2”}

key缓存的 key

删除指定key的缓存对象

例如:
@CacheEvict(value=”testcache”,key=”#userName”)

condition缓存的条件

删除指定条件的缓存对象

例如:
@CacheEvict(value=”testcache”,condition=”#userName.length()>2”)

allEntries方法执行后清空所有缓存

缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存。

例如:
@CacheEvict(value=”testcache”,allEntries=true)

beforeInvocation方法执行前清空所有缓存

缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存。

例如:
@CacheEvict(value=”testcache”,beforeInvocation=true)

 

spring boot中开启缓存

Spring Boot 为我们提供了多种缓存CacheManager配置方案。默认情况下会使用基于内存map一种缓存方案ConcurrenMapCacheManager。当然我没也可以通过配置使用 Generic、JCache (JSR-107)、EhCache 2.x、Hazelcast、Infinispan、Redis、Guava、Simple等技术进行缓存实现。

这里使用默认的基于内存的方案进行举例

引入依赖

在pom文件中引入缓存包

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

启用缓存

在启动类增加启用缓存注解@EnableCaching

@SpringBootApplication 
@EnableCaching //启用缓存
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

缓存测试方法

测试方法做了一个2秒的延时

public class CacheTest {
    /**
     * 缓存测试方法延时两秒
     * @param i
     * @return
     */
    @Cacheable(value = "cache_test")
    public String cacheFunction(int i){
        try {
            long time = 2000L;
            Thread.sleep(time);
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }

        return "success"+ i;
    }
}

调用缓存测试方法

这里需要注意:不能在同一个类中调用被注解缓存了的方法。也就是说缓存调用方法和缓存注解方法不能在一个类中出现。

public class HelloController {
    @Autowired
    CacheTest cacheTest;

    @GetMapping(value = "/")
    public String hello(){
        for(int i=0;i<5;i++){
            System.out.println(new Date() + " " + cacheTest.cacheFunction(i));
        }
        return "/hello";
    }
}

测试结果

我们可以看出第一次执行时每间隔2秒打印了一次success

而第二次同一时间全部打印完成

Tue Jun 12 15:35:01 CST 2018 success0
Tue Jun 12 15:35:03 CST 2018 success1
Tue Jun 12 15:35:05 CST 2018 success2
Tue Jun 12 15:35:07 CST 2018 success3
Tue Jun 12 15:35:09 CST 2018 success4

Tue Jun 12 15:35:26 CST 2018 success0
Tue Jun 12 15:35:26 CST 2018 success1
Tue Jun 12 15:35:26 CST 2018 success2
Tue Jun 12 15:35:26 CST 2018 success3
Tue Jun 12 15:35:26 CST 2018 success4

 

spring boot中使用redis缓存

在上例得的基础上我们将使用redis作为缓存的存储方案。

引入redis依赖

在pom文件中引入redis缓存包

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

这里需要注意有些资料依赖的资源为spring-boot-starter-redis,这个依赖在spring boot 1.4版本之后被弃用了,改为使用spring-boot-starter-data-redis。

官方说明:

spring-boot-starter-redis

Starter for using Redis key-value data store with Spring Data Redis and the Jedis client. Deprecated as of 1.4 in favor of spring-boot-starter-data-redis

redis配置

在application.properties配置文件中增加redis配置

#redis配置
#Redis数据库索引(缓存将使用此索引编号的数据库)
spring.redis.database=10
#Redis服务器地址  
spring.redis.host=123.56.8.125 
#Redis服务器连接端口
spring.redis.port=6379 
#Redis服务器连接密码(默认为空)  
spring.redis.password=****** 
#连接超时时间 毫秒(默认2000)
#请求redis服务的超时时间,这里注意设置成0时取默认时间2000
spring.redis.timeout=2000
#连接池最大连接数(使用负值表示没有限制)  
#建议为业务期望QPS/一个连接的QPS,例如50000/1000=50
#一次命令时间(borrow|return resource+Jedis执行命令+网络延迟)的平均耗时约为1ms,一个连接的QPS大约是1000
spring.redis.pool.max-active=50 
#连接池中的最大空闲连接 
#建议和最大连接数一致,这样做的好处是连接数从不减少,从而避免了连接池伸缩产生的性能开销。
spring.redis.pool.max-idle=50
#连接池中的最小空闲连接  
#建议为0,在无请求的状况下从不创建链接
spring.redis.pool.min-idle=0 
#连接池最大阻塞等待时间 毫秒(-1表示没有限制)  
#建议不要为-1,连接池占满后无法获取连接时将在该时间内阻塞等待,超时后将抛出异常。
spring.redis.pool.max-wait=2000

spring boot使用JedisPool来作为redis连接池。推荐一篇JedisPool资源池优化的文章:

https://yq.aliyun.com/articles/236383

Spring Boot会在侦测到存在Redis的依赖并且Redis的配置是可用的情况下,使用RedisCacheManager 初始化CacheManager。

设置缓存生存时间

我们可以对redis缓存数据指定生存时间从而达到缓存自动失效的目的。

通过创建缓存配置文件类可以设置缓存各项参数

@Configuration
public class RedisCacheConfig {
    @Bean
    public RedisCacheManager cacheManager(RedisTemplate redisTemplate) {
        //获得redis缓存管理类
        RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
        // 开启使用缓存名称做为key前缀(这样所有同名缓存会整理在一起比较容易查找)
        redisCacheManager.setUsePrefix(true);

        //这里可以设置一个默认的过期时间 单位是秒
        redisCacheManager.setDefaultExpiration(600L);
        // 设置缓存的过期时间 单位是秒
        Map<String, Long> expires = new HashMap<>();
        expires.put("pub.imlichao.CacheTest.cacheFunction", 100L);
        redisCacheManager.setExpires(expires);

        return redisCacheManager;
    }
}

设置缓存序列化方式

redisTemplate 默认的序列化方式为 jdkSerializeable,我们也可以使用其他序列化方式来达到不同的需求。比如我们希望缓存的数据具有可读性就可以将其序列化为json格式,json序列化可以使用Jackson2JsonRedisSerialize或FastJsonRedisSerializer。如果我们希望拥有更快的速度和占用更小的存储空间推荐使用KryoRedisSerializer进行序列化。

由于redis缓存对可读性没什么要求,而存储空间和速度是比较重要的,所以这里使用KryoRedisSerializer进行对象序列化。

添加Kryo依赖

<dependency>
     <groupId>com.esotericsoftware</groupId>
     <artifactId>kryo</artifactId>
     <version>4.0.2</version>
</dependency>

实现RedisSerializer接口创建KryoRedisSerializer序列化工具

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.io.ByteArrayOutputStream;

public class KryoRedisSerializer<T> implements RedisSerializer<T> {

    public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

    private static final ThreadLocal<Kryo> kryos = ThreadLocal.withInitial(Kryo::new);

    private Class<T> clazz;

    public KryoRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return EMPTY_BYTE_ARRAY;
        }

        Kryo kryo = kryos.get();
        kryo.setReferences(false);
        kryo.register(clazz);

        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             Output output = new Output(baos)) {
            kryo.writeClassAndObject(output, t);
            output.flush();
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return EMPTY_BYTE_ARRAY;
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0) {
            return null;
        }

        Kryo kryo = kryos.get();
        kryo.setReferences(false);
        kryo.register(clazz);

        try (Input input = new Input(bytes)) {
            return (T) kryo.readClassAndObject(input);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

}

修改配置文件替换默认序列化工具

@Configuration
public class RedisCacheConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // KryoRedisSerializer 替换默认序列化
        KryoRedisSerializer kryoRedisSerializer = new KryoRedisSerializer(Object.class);
        redisTemplate.setValueSerializer(kryoRedisSerializer);
        redisTemplate.setKeySerializer(kryoRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

测试结果

我们可以看到redis里面创建了缓存

并且第二次成功的获取了缓存

Fri Jun 15 10:51:04 CST 2018 success0
Fri Jun 15 10:51:07 CST 2018 success1
Fri Jun 15 10:51:09 CST 2018 success2
Fri Jun 15 10:51:11 CST 2018 success3
Fri Jun 15 10:51:13 CST 2018 success4

Fri Jun 15 10:52:49 CST 2018 success0
Fri Jun 15 10:52:49 CST 2018 success1
Fri Jun 15 10:52:49 CST 2018 success2
Fri Jun 15 10:52:49 CST 2018 success3
Fri Jun 15 10:52:49 CST 2018 success4
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值