本文代码示例:https://gitee.com/imlichao/redis_cache-examplehtml
缓存是分布式系统中的重要组件,主要解决高并发,大数据场景下,热点数据访问的性能问题。提供高性能的数据快速访问。本文主要介绍基于springboot 框架下使用spring cache和redis进行方法缓存的方案。java
方法缓存比较适合于静态页面或查询结果复用性较高的业务。举个例子,一个接口负责查询首页广告,若是每一个用户看到的首页广告都是同样的,那么就能够直接从缓存中读取大大减小了系统性能消耗。对于不适合使用缓存的业务也举个例子,一个接口负责查询离当前用户坐标最近的商家,显然用户坐标相同的概率会很是低,因此这种缓存即消耗空间又很难命中。综上所述,访问量大且出入参是一个有限集合的业务更加适合缓存。git
缓存与数据源的一致性也是比较重要的问题。若是数据已经产生变化而缓存不更新,那么咱们将读取不到最新的数据。因此业务的实时性要求决定了缓存一致性的实时性。因为一致性问题须要在更新节点增长大量的缓存失效逻辑,因此咱们的项目目前并无进行改造。当前的方案是缓存在必定时间内自动失效,也就是说缓存设置为5分钟失效,那么修改的内容将在5分钟后才能被查询出来。固然这只是在实时性要求不高的业务中使用。推荐一篇解决一致性问题的文章:https://blog.csdn.net/java_dyq/article/details/51997045redis
Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,经过在既有代码中添加少许它定义的各类 annotation,即可以达到缓存方法的返回对象的效果。数据库
Spring 的缓存技术还具有至关的灵活性,不只可以使用 SpEL(Spring Expression Language)来定义缓存的 key 和各类 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。json
其特色总结以下:api
声明式缓存经常使用注释缓存
spring cache 中最主要使用三个注释标签,即 @Cacheable、@CachePut 和 @CacheEvictspringboot
@Cacheable 的做用 |
---|
主要针对方法配置,可以根据方法的请求参数对其结果进行缓存。 |
@Cacheable 主要的参数 |
value | 缓存的名称 | 每个缓存名称表明一个缓存对象。当一个方法填写多个缓存名称时将建立多个缓存对象。当多个方法使用同一缓存名称时相同参数的缓存会被覆盖。因此一般状况咱们使用“包名+类名+方法名”或者使用接口的RequestMapping做为缓存名称防止命名重复引发的问题。 单缓存名称:@Cacheable(value=”mycache”) |
key | 缓存的 key | key标记了缓存对象下的每一条缓存。若是不指定key则系统自动按照方法的全部入参生成key,也就是说相同的入参值将会返回一样的缓存结果。 若是指定key则要按照 SpEL 表达式编写使用的入参列表。以下列不管方法存在多少个入参,只要userName值一致,则会返回相同的缓存结果。 @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件 | 知足条件后方法结果才会被缓存。不填写则认为无条件所有缓存。 条件使用 SpEL表达式编写,返回 true 或者 false,只有为 true 才进行缓存 以下例,只有用户名长度大于2时参会进行缓存 |
@CachePut 的做用 |
---|
主要针对方法配置,可以根据方法的请求参数对其结果进行缓存。和 @Cacheable 不一样的是,它每次都会触发真实方法的调用,此注解常被用于更新缓存使用。 |
@CachePut 主要的参数 |
value | 缓存的名称 | 例如: |
key | 缓存的 key | 例如: @CachePut(value=”testcache”,key=”#userName”) |
condition | 缓存的条件 | 例如: @CachePut(value=”testcache”,condition=”#userName.length()>2”) |
@CacheEvict 的做用 |
---|
主要针对方法配置,可以根据必定的条件对缓存进行清空 |
@CacheEvict 主要的参数 |
value | 缓存的名称 | 删除指定名称的缓存对象。必须与下面的其中一个参数配合使用 例如: |
key | 缓存的 key | 删除指定key的缓存对象 例如: |
condition | 缓存的条件 | 删除指定条件的缓存对象 例如: |
allEntries | 方法执行后清空全部缓存 | 缺省为 false,若是指定为 true,则方法调用后将当即清空全部缓存。 例如: |
beforeInvocation | 方法执行前清空全部缓存 | 缺省为 false,若是指定为 true,则在方法尚未执行的时候就清空缓存,缺省状况下,若是方法执行抛出异常,则不会清空缓存。 例如: |
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
在上例得的基础上咱们将使用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。
官方说明:
|
Starter for using Redis key-value data store with Spring Data Redis and the Jedis client. Deprecated as of 1.4 in favor of |
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; } }
设置过时时间时也能够不采用expires.put("pub.imlichao.CacheTest.cacheFunction", 100L)的写法,而是使用@Cacheable标签的value值进行声明,以下
@Configuration public class RedisCacheConfig { ...... // 设置缓存的过时时间 单位是秒 Map<String, Long> expires = new HashMap<>(); expires.put("cache_test", 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