版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a724888/article/details/80785403

为了提高性能,减少数据库的压力,使用缓存是非常好的手段之一。本文,讲解 Spring Boot 如何集成缓存管理。

Spring注解缓存

Spring 3.1之后,引入了注解缓存技术,其本质上不是一个具体的缓存实现方案,而是一个对缓存使用的抽象,通过在既有代码中添加少量自定义的各种annotation,即能够达到使用缓存对象和缓存方法的返回对象的效果。Spring的缓存技术具备相当的灵活性,不仅能够使用SpEL(Spring Expression Language)来定义缓存的key和各种condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存集成。其特点总结如下:

  • 少量的配置annotation注释即可使得既有代码支持缓存;
  • 支持开箱即用,不用安装和部署额外的第三方组件即可使用缓存;
  • 支持Spring Express Language(SpEL),能使用对象的任何属性或者方法来定义缓存的key和使用规则条件;
  • 支持自定义key和自定义缓存管理者,具有相当的灵活性和可扩展性。

和Spring的事务管理类似,Spring Cache的关键原理就是Spring AOP,通过Spring AOP实现了在方法调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。而Spring Cache利用了Spring AOP的动态代理技术,即当客户端尝试调用pojo的foo()方法的时候,给它的不是pojo自身的引用,而是一个动态生成的代理类。

图12 Spring动态代理调用图

如图12所示,实际客户端获取的是一个代理的引用,在调用foo()方法的时候,会首先调用proxy的foo()方法,这个时候proxy可以整体控制实际的pojo.foo()方法的入参和返回值,比如缓存结果,比如直接略过执行实际的foo()方法等,都是可以轻松做到的。Spring Cache主要使用三个注释标签,即@Cacheable、@CachePut和@CacheEvict,主要针对方法上注解使用,部分场景也可以直接类上注解使用,当在类上使用时,该类所有方法都将受影响。我们总结一下其作用和配置方法,如表1所示。

表1

标签类型 作用 主要配置参数说明
@Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 value:缓存的名称,在 Spring 配置文件中定义,必须指定至少一个; key:缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则默认按照方法的所有参数进行组合; condition:缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存
@CachePut 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用 value:缓存的名称,在 spring 配置文件中定义,必须指定至少一个; key:缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则默认按照方法的所有参数进行组合; condition:缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存
@CacheEvict 主要针对方法配置,能够根据一定的条件对缓存进行清空 value:缓存的名称,在 Spring 配置文件中定义,必须指定至少一个; key:缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则默认按照方法的所有参数进行组合; condition:缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存; allEntries:是否清空所有缓存内容,默认为 false,如果指定为 true,则方法调用后将立即清空所有缓存; beforeInvocation:是否在方法执行前就清空,默认为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,默认情况下,如果方法执行抛出异常,则不会清空缓存

可扩展支持:Spring注解cache能够满足一般应用对缓存的需求,但随着应用服务的复杂化,大并发高可用性能要求下,需要进行一定的扩展,这时对其自身集成的缓存方案可能不太适用,该怎么办?Spring预先有考虑到这点,那么怎样利用Spring提供的扩展点实现我们自己的缓存,且在不改变原来已有代码的情况下进行扩展?是否在方法执行前就清空,默认为false,如果指定为true,则在方法还没有执行的时候就清空缓存,默认情况下,如果方法执行抛出异常,则不会清空缓存。

这基本能够满足一般应用对缓存的需求,但现实总是很复杂,当你的用户量上去或者性能跟不上,总需要进行扩展,这个时候你或许对其提供的内存缓存不满意了,因为其不支持高可用性,也不具备持久化数据能力,这个时候,你就需要自定义你的缓存方案了,还好,Spring也想到了这一点。

我们先不考虑如何持久化缓存,毕竟这种第三方的实现方案很多,我们要考虑的是,怎么利用Spring提供的扩展点实现我们自己的缓存,且在不改原来已有代码的情况下进行扩展。这需要简单的三步骤,首先需要提供一个CacheManager接口的实现(继承至AbstractCacheManager),管理自身的cache实例;其次,实现自己的cache实例MyCache(继承至Cache),在这里面引入我们需要的第三方cache或自定义cache;最后就是对配置项进行声明,将MyCache实例注入CacheManager进行统一管理。

声明式缓存

Spring 定义 CacheManager 和 Cache 接口用来统一不同的缓存技术。例如 JCache、 EhCache、 Hazelcast、 Guava、 Redis 等。在使用 Spring 集成 Cache 的时候,我们需要注册实现的 CacheManager 的 Bean。

Spring Boot默认集成CacheManager

Spring Boot 为我们自动配置了多个 CacheManager 的实现。

Spring Boot 为我们自动配置了 JcacheCacheConfiguration、 EhCacheCacheConfiguration、HazelcastCacheConfiguration、GuavaCacheConfiguration、RedisCacheConfiguration、SimpleCacheConfiguration 等。

默认的 ConcurrenMapCacheManager

Spring 从 Spring3.1 开始基于 java.util.concurrent.ConcurrentHashMap 实现的缓存管理器。所以, Spring Boot 默认使用 ConcurrentMapCacheManager 作为缓存技术。

以下是我们不引入其他缓存依赖情况下,控制台打印的日志信息。

  1. Bean 'cacheManager' of type [class org.springframework.cache.concurrent.ConcurrentMapCacheManager]

实战演练

Maven 依赖

首先,我们先创建一个 POM 文件。

  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3. <modelVersion>4.0.0</modelVersion>
  4. <parent>
  5. <groupId>org.springframework.boot</groupId>
  6. <artifactId>spring-boot-starter-parent</artifactId>
  7. <version>1.3.3.RELEASE</version>
  8. </parent>
  9. <groupId>com.lianggzone.demo</groupId>
  10. <artifactId>springboot-action-cache</artifactId>
  11. <version>0.1</version>
  12. <packaging>jar</packaging>
  13. <name>springboot-action-cache</name>
  14. <dependencies>
  15. <dependency>
  16. <groupId>org.springframework.boot</groupId>
  17. <artifactId>spring-boot-starter</artifactId>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.springframework.boot</groupId>
  21. <artifactId>spring-boot-starter-web</artifactId>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-cache</artifactId>
  26. </dependency>
  27. </dependencies>
  28. <build>
  29. <plugins>
  30. <plugin>
  31. <groupId>org.apache.maven.plugins</groupId>
  32. <artifactId>maven-compiler-plugin</artifactId>
  33. <configuration>
  34. <defaultLibBundleDir>lib</defaultLibBundleDir>
  35. <source>1.7</source>
  36. <target>1.7</target>
  37. <encoding>UTF-8</encoding>
  38. </configuration>
  39. </plugin>
  40. <plugin>
  41. <groupId>org.apache.maven.plugins</groupId>
  42. <artifactId>maven-resources-plugin</artifactId>
  43. <configuration>
  44. <encoding>UTF-8</encoding>
  45. <useDefaultDelimiters>false</useDefaultDelimiters>
  46. <escapeString>\</escapeString>
  47. <delimiters>
  48. <delimiter>${*}</delimiter>
  49. </delimiters>
  50. </configuration>
  51. </plugin>
  52. <plugin>
  53. <groupId>org.springframework.boot</groupId>
  54. <artifactId>spring-boot-maven-plugin</artifactId>
  55. </plugin>
  56. </plugins>
  57. </build>
  58. </project>

其中,最核心的是添加 spring-boot-starter-cache 依赖。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-cache</artifactId>
  4. </dependency>

开启缓存支持

在 Spring Boot 中使用 @EnableCaching 开启缓存支持。

  1. @Configuration
  2. @EnableCaching
  3. public class CacheConfiguration {}

服务层

创建一个服务类

  1. @Service("concurrenmapcache.cacheService")
  2. public class CacheService {
  3. }

首先,我们先来讲解下 @Cacheable 注解。@Cacheable 在方法执行前 Spring 先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放进缓存。有两个重要的值, value,返回的内容将存储在 value 定义的缓存的名字对象中。key,如果不指定将使用默认的 KeyGenerator 生成。

我们在查询方法上,添加 @Cacheable 注解,其中缓存名称为 concurrenmapcache。

  1. @Cacheable(value = "concurrenmapcache")
  2. public long getByCache() {
  3. try {
  4. Thread.sleep(3 * 1000);
  5. } catch (InterruptedException e) {
  6. e.printStackTrace();
  7. }
  8. return new Timestamp(System.currentTimeMillis()).getTime();
  9. }

@CachePut 与 @Cacheable 类似,但是它无论什么情况,都会将方法的返回值放到缓存中, 主要用于数据新增和修改方法。

  1. @CachePut(value = "concurrenmapcache")
  2. public long save() {
  3. long timestamp = new Timestamp(System.currentTimeMillis()).getTime();
  4. System.out.println("进行缓存:" + timestamp);
  5. return timestamp;
  6. }

@CacheEvict 将一条或多条数据从缓存中删除, 主要用于删除方法,用来从缓存中移除相应数据。

  1. @CacheEvict(value = "concurrenmapcache")
  2. public void delete() {
  3. System.out.println("删除缓存");
  4. }

控制层

为了展现效果,我们先定义一组简单的 RESTful API 接口进行测试。

  1. @RestController("concurrenmapcache.cacheController")
  2. @RequestMapping(value = "/concurrenmapcache/cache")
  3. public class CacheController {
  4. @Autowired
  5. private CacheService cacheService;
  6. /**
  7. * 查询方法
  8. */
  9. @RequestMapping(value = "", method = RequestMethod.GET)
  10. public String getByCache() {
  11. Long startTime = System.currentTimeMillis();
  12. long timestamp = this.cacheService.getByCache();
  13. Long endTime = System.currentTimeMillis();
  14. System.out.println("耗时: " + (endTime - startTime));
  15. return timestamp+"";
  16. }
  17. /**
  18. * 保存方法
  19. */
  20. @RequestMapping(value = "", method = RequestMethod.POST)
  21. public void save() {
  22. this.cacheService.save();
  23. }
  24. /**
  25. * 删除方法
  26. */
  27. @RequestMapping(value = "", method = RequestMethod.DELETE)
  28. public void delete() {
  29. this.cacheService.delete();
  30. }
  31. }

运行

  1. @RestController
  2. @EnableAutoConfiguration
  3. @ComponentScan(basePackages = { "com.lianggzone.springboot" })
  4. public class WebMain {
  5. public static void main(String[] args) throws Exception {
  6. SpringApplication.run(WebMain.class, args);
  7. }
  8. }

课后作业

我们分为几个场景进行测试。

  • 多次调用查询接口,查看缓存信息是否变化,控制台日志是否如下?你得到的结论是什么?
  • 调用保存接口,再调用查询接口,查看缓存信息是否变化?你得到的结论是什么?
  • 调用删除接口,再调用查询接口,接口响应是否变慢了?你再看看控制台日志,你得到的结论是什么?

扩展阅读

如果想更深入理解 Spring 的 Cache 机制,这边推荐两篇不错的文章。

  • Spring Cache 抽象详解
  • Spring 4.1 新特性 - Spring 缓存框架增强

源代码

Spring Boot In Practice:Redis缓存实战

阅读本文需要对Spring和Redis比较熟悉。

Spring Framework 提供了Cache Abstraction对缓存层进行了抽象封装,通过几个annotation可以透明给您的应用增加缓存支持,而不用去关心底层缓存具体由谁实现。目前支持的缓存有java.util.concurrent.ConcurrentMap,Ehcache 2.x,Redis等。

一般我们使用最常用的Redis做为缓存实现(Spring Data Redis),

  • 需要引入的starter: spring-boot-starter-data-redis,spring-boot-starter-cache;
  • 自动配置生成的Beans: RedisConnectionFactoryStringRedisTemplate , RedisTemplateRedisCacheManager,自动配置的Bean可以直接注入我们的代码中使用;

I. 配置

application.properties

# REDIS (RedisProperties)
spring.redis.host=localhost # Redis server host.
spring.redis.port=6379 # Redis server port.
spring.redis.password= # Login password of the redis server.

具体对Redis cluster或者Sentinel的配置可以参考这里

开启缓存支持

@SpringBootApplication
@EnableCaching//开启caching
public class NewsWebServer {
//省略内容
}

定制RedisTemplate

自动配置的RedisTemplate并不能满足大部分项目的需求,比如我们基本都需要设置特定的Serializer(RedisTemplate默认会使用JdkSerializationRedisSerializer)。

Redis底层中存储的数据只是字节。虽然Redis本身支持各种类型(List, Hash等),但在大多数情况下,这些指的是数据的存储方式,而不是它所代表的内容(内容都是byte)。用户自己来决定数据如何被转换成String或任何其他对象。用户(自定义)类型和原始数据类型之间的互相转换通过RedisSerializer接口(包org.springframework.data.redis.serializer)来处理,顾名思义,它负责处理序列化/反序列化过程。多个实现可以开箱即用,如:StringRedisSerializer和JdkSerializationRedisSerialize。Jackson2JsonRedisSerializer或GenericJackson2JsonRedisSerializer来处理JSON格式的数据。请注意,存储格式不仅限于value 它可以用于key,Hash的key和value。

声明自己的RedisTemplate覆盖掉自动配置的Bean:

//通用的RedisTemplate
@Bean
public RedisTemplate<String, Object> redisTemplate(JedisConnectionFactory jedisConnectionFactory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(jedisConnectionFactory);template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(new GenericJackson2JsonRedisSerializer());//template.setHashKeySerializer(template.getKeySerializer());//template.setHashValueSerializer(template.getValueSerializer());return template;
}

这里我们使用GenericJackson2JsonRedisSerializer而不是Jackson2JsonRedisSerializer,后者的问题是你需要为每一个需要序列化进Redis的类指定一个Jackson2JsonRedisSerializer因为其构造函数中需要指定一个类型来做反序列化:

redis.setValueSerializer(new Jackson2JsonRedisSerializer<Product>(Product.class));

如果我们应用中有大量对象需要缓存,这显然是不合适的,而前者直接把类型信息序列化到了JSON格式中,让一个实例可以操作多个对象的反序列化。

定制RedisCacheManager

有时候Spring Boot自动给我们配置的RedisCacheManager也不能满足我们应用的需求,我看到很多用法都直接声明了一个自己的RedisCacheManager,其实使用CacheManagerCustomizer可以对自动配置的RedisCacheManager进行定制化:

    @Beanpublic CacheManagerCustomizer<RedisCacheManager> cacheManagerCustomizer() {return new CacheManagerCustomizer<RedisCacheManager>() {@Overridepublic void customize(RedisCacheManager cacheManager) {cacheManager.setUsePrefix(true); //事实上这是Spring Boot的默认设置,为了避免key冲突Map<String, Long> expires = new HashMap<>();expires.put("myLittleCache", 12L*60*60);  // 设置过期时间 key is cache-nameexpires.put("myBiggerCache", 24L*60*60);cacheManager.setExpires(expires);  // expire per cachecacheManager.setDefaultExpiration(24*60*60);// 默认过期时间:24 hours}};}

II. 使用

缓存Key的生成

我们都知道Redis是一个key-value的存储系统,无论我们想要缓存什么值,都需要制定一个key。

@Cacheable(cacheNames = "user")public User findById(long id) {return userMapper.findById(id);}

上面的代码中,findById方法返回的对象会被缓存起来,key由默认的org.springframework.cache.interceptor.SimpleKeyGenerator生成,生成策略是根据被标注方法的参数生成一个SimpleKey对象,然后由RedisTemplate中定义的KeySerializer序列化后作为key(注意StringRedisSerializer只能序列化String类型,对SimpleKey对象无能为力,你只能定义其他Serializer)。

不过大多数情况下我们都会采用自己的key生成方案,方式有两种:

1.实现自己的KeyGenerator;

@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {@Beanpublic KeyGenerator customKeyGenerator() {return new KeyGenerator() {@Overridepublic Object generate(Object o, Method method, Object... objects) {StringBuilder sb = new StringBuilder();sb.append(o.getClass().getName());sb.append(method.getName());for (Object obj : objects) {sb.append(obj.toString());}return sb.toString();}};}
}

2.在@Cacheable标注中直接声明key:

@Cacheable(cacheNames = "user", key="#id.toString()") public User findById(long id) {return userMapper.findById(id);}@Cacheable(cacheNames = "user", key="'admin'") public User findAdmin() {return userMapper.findAdminUser();}@Cacheable(cacheNames = "user", key="#userId + ':address'") public List<Address> findUserAddress(long userId) {return userMapper.findUserAddress(userId);}

key的声明形式支持SpEL。
最终生成的Redis key为:user:100234,user部分是因为cacheManager.setUsePrefix(true),cacheName会被添加到key作为前缀避免引起key的冲突。之所以#id.toString()要long型转为String是因为我们设置的KeySerializer为StringRedisSerializer只能用来序列化String。
如果被标注方法没有参数,我们可以用一个静态的key值,最终生成的key为user:admin
最终生成的key为user:100234:address

这种方式更符合我们以前使用Redis的习惯,所以推荐。

直接使用RedisTemplate

有时候标注不能满足我们的使用场景,我们想要直接使用更底层的RedisTemplate

@Service
public class FeedService {@Resource(name="redisTemplate") private ZSetOperations<String, Feed> feedOp;public List<Feed> getFeed(int count, long maxId) {return new ArrayList<>(feedOp.reverseRangeByScore(FEED_CACHE_KEY, 0, maxId, offset, count));}   //省略
}

我们可以直接把RedisTemplate的实例注入为ZSetOperationsListOperationsValueOperations等类型(Spring IoC Container帮我们做了转化工作,可以参考org.springframework.data.redis.core.ZSetOperationsEditor)。

搞懂分布式技术14:Spring Boot使用注解集成Redis缓存相关推荐

  1. 搞懂分布式技术19:使用RocketMQ事务消息解决分布式事务

    搞懂分布式技术19:使用RocketMQ事务消息解决分布式事务 初步认识RocketMQ的核心模块 rocketmq模块 rocketmq-broker:接受生产者发来的消息并存储(通过调用rocke ...

  2. 搞懂分布式技术16:浅谈分布式锁的几种方案

    搞懂分布式技术16:浅谈分布式锁的几种方案 前言 随着互联网技术的不断发展,数据量的不断增加,业务逻辑日趋复杂,在这种背景下,传统的集中式系统已经无法满足我们的业务需求,分布式系统被应用在更多的场景, ...

  3. springcloud分布式事务解决方案_搞懂分布式技术18:分布式事务常用解决方案

    本文转载自 http://linkedkeeper.com 本文内容参考网络,侵删 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 http ...

  4. 超完整!Springboot整合redis集群(Sentine),spring boot自动配置集成redis集群(Sentine)

    1.添加maven依赖 注意maven版本依赖,版本要相互匹配,如不知道如何查看相对应的版本时,可进入博主主页查看博主上一篇博文. <parent><groupId>org.s ...

  5. 搞懂分布式技术28:微服务(Microservice)那点事

    微服务(Microservice)那点事 肥侠 2016-01-13 09:46:53 浏览58371 评论15 分布式系统与计算 微服务 摘要: 微服务架构被提出很短的时间内,就被越来越多的开发人员 ...

  6. 搞懂分布式技术30:高并发解决方案——提升高并发量服务器性能解决思路

    高并发解决方案--提升高并发量服务器性能解决思路 一个小型的网站,可以使用最简单的html静态页面就实现了,配合一些图片达到美化效果,所有的页面均存放在一个目录下,这样的网站对系统架构.性能的要求都很 ...

  7. 搞懂分布式技术15:缓存更新的套路

    缓存更新的套路 看到好些人在写更新缓存数据代码时,先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中.然而,这个是逻辑是错误的.试想,两个并发操作,一个是更新操作,另一个是查询操作,更新 ...

  8. 搞懂分布式技术8:负载均衡原理剖析

    负载均衡的重要性无需多说,今天带来:负载均衡原理的解析. 开头先理解一下所谓的"均衡". 不能狭义地理解为分配给所有实际服务器一样多的工作量,因为多台服务器的承载能力各不相同,这可 ...

  9. Spring Boot最新版集成邮件发送功能大全

    Spring Boot最新版集成邮件发送功能大全 前言 一.开启SMTP服务并获取授权码 二.创建Spring Boot项目 1.配置邮箱基本信息: 2.简单邮件发送: 3.发送带附件的邮件: 5.使 ...

最新文章

  1. 十种经典排序算法精粹(c语言版本)
  2. 网络延迟和数据包丢失对网络性能的影响—Vecloud微云
  3. 强连通分量(Strongly_Connected_Components)
  4. 机器学习-分类算法-决策树,随机森林10
  5. 【WebGL】《WebGL编程指南》读书笔记——第2章
  6. Centerface:开源实用的边缘设备无锚人脸检测与对齐算法
  7. ubuntu系统VNC服务器安装配置
  8. 计算机英语四六级考试时间,2019年12月英语四六级考试时间
  9. 某Java大佬在地表最强Java企业(阿里)面试总结
  10. [异能程序员]第六章 遛狗(第二更)
  11. 如何解决因网络问题导致的 github 下载出错
  12. JavaScript的循环和
  13. vmstat 命令的用法说明
  14. PS抠图:将猫咪图片打造成唯美星空效果图
  15. 出圈c语言,c语言作业 出圈游戏
  16. 换钱问题(经典枚举样例)
  17. 常见的ftp工具有哪些,分享8款常见的ftp工具
  18. 强迫症犯了,Python整理百度云盘文件大量重复无用文件
  19. 第一代计算机主机主要使用,第一代计算机使用什么语言
  20. 用C语言求1-2000之间哪些是闰年

热门文章

  1. yii2 提供接口给java_yii2-fast-api 快速配置并开发基于 yii2 的 api 服务
  2. 计算机基础教程试题及答案,2017计算机基础教程考试题「附答案」
  3. a连接带id转送不了数据_数据库表连接的简单解释
  4. 配置SumatraPDF反向搜索
  5. python的有效变量名_python里用变量命名改善代码质量
  6. 探采大数据 构建信息化 浙江深谋“互联网+出行”
  7. python编写递归函数、求斐波那契数列_利用Python实现斐波那契数列的方法实例
  8. C# 连接SQLSERVER数据库 连接字符串
  9. C#餐厅管理系统6--呼叫管理员!
  10. 叫我小野就好呀-JAVA数据类型