- 演示工具版本
- Maven 依赖
- application.properties
- 使用 Lettuce 配置
- 使用 Jedis 配置
- RedisCacheManager
- 1. 自动配置的 RedisCacheManager
- 2. 自定义的 RedisCacheManager
- 使用 @EnableCaching 启用缓存
- 使用 @Cacheable
- 使用 @CachePut
- 使用 @CacheEvict
- 使用 @Caching
- Spring Boot Cache + Redis + MySQL CRUD 完整示例
- 测试应用程序
- 参考文献
- 源码下载
本页将介绍Spring Boot Redis缓存实例。RedisCacheManager是由Redis支持的CacheManager。如果Redis在我们的Spring Boot应用程序中可用并已配置,RedisCacheManager将被自动配置。Redis连接从Lettuce或Jedis Java Redis客户端获得。Redis的依赖性由spring-boot-starter-data-redis 启动器解决。在Spring Boot 2.0中,Lettuce被默认解决,而不是Jedis。要使用Jedis,我们需要在构建文件中包含jedis依赖。
Spring @EnableCaching在我们的应用程序中启用Spring缓存管理能力。它被@SpringBootApplication注解所注解。@Cacheable表示调用方法的结果可以被缓存,一旦结果被缓存,下一次调用方法的执行将被跳过,只有缓存的结果被提供。@CachePut 添加或更新缓存,但不跳过方法的执行。@CacheEvict 清空缓存但不跳过方法的执行。@Caching被用来分组多个缓存注释。
演示工具版本- Java 9
- Spring 5.0.8.RELEASE
- Spring Data 2.0.9.RELEASE
- Spring Boot 2.0.4.RELEASE
- Maven 3.5.2
- MySQL 5.5
- Eclipse Oxygen
Spring提供了spring-boot-starter-data-redis来解决Redis的依赖关系。
它为Lettuce和Jedis客户端库提供基本的自动配置。
默认情况下,Spring Boot 2.0使用Lettuce。为了获得池化连接工厂,我们需要提供commons-pool2依赖。
找到Maven文件。
pom.xml
application.properties4.0.0 com.concretepage spring-boot-app 0.0.1-SNAPSHOT jar spring-boot-app Spring Boot Application org.springframework.boot spring-boot-starter-parent 2.0.4.RELEASE 9 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 mysql mysql-connector-java 6.0.5 javax.xml.bind jaxb-api 2.3.0 org.springframework.boot spring-boot-devtools true org.springframework.boot spring-boot-maven-plugin
找到我们的演示程序中使用的应用程序属性文件。
application.properties
#Redis specific configurations spring.redis.host=localhost spring.redis.port=6379 spring.redis.password= spring.redis.lettuce.pool.max-active=7 spring.redis.lettuce.pool.max-idle=7 spring.redis.lettuce.pool.min-idle=2 spring.redis.lettuce.pool.max-wait=-1ms spring.redis.lettuce.shutdown-timeout=200ms spring.cache.redis.cache-null-values=false spring.cache.redis.time-to-live=600000 spring.cache.redis.use-key-prefix=true spring.cache.type=redis #spring.cache.cache-names=articleCache,allArticlesCache #Database specific configurations spring.datasource.url=jdbc:mysql://localhost:3306/concretepage spring.datasource.username=root spring.datasource.password=cp spring.datasource.hikari.connection-timeout=20000 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.maximum-pool-size=12 spring.datasource.hikari.idle-timeout=300000 spring.datasource.hikari.max-lifetime=1200000 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect spring.jpa.properties.hibernate.id.new_generator_mappings=false spring.jpa.properties.hibernate.format_sql=true使用 Lettuce 配置
Spring Boot 2.0启动器spring-boot-starter-data-redis默认解决了Lettuce。Spring提供LettuceConnectionFactory来获取连接。要获得池化连接工厂,我们需要在类路径上提供commons-pool2。要使用Lettuce,我们需要以下Maven依赖项。
org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2
为了配置Lettuce池,我们需要使用spring.redis.*前缀与Lettuce池连接属性。找到Lettuce池的样本配置。
application.properties
spring.redis.host=localhost spring.redis.port=6379 spring.redis.password= spring.redis.lettuce.pool.max-active=7 spring.redis.lettuce.pool.max-idle=7 spring.redis.lettuce.pool.min-idle=2 spring.redis.lettuce.pool.max-wait=-1ms spring.redis.lettuce.shutdown-timeout=200ms
我们可以覆盖默认的Redis主机、端口和密码配置。如果我们想无限期地阻塞,请使用max-wait这个负值。
使用 Jedis 配置默认情况下,Spring Boot 2.0启动器spring-boot-starter-data-redis使用Lettuce。要使用Jedis,我们需要排除Lettuce的依赖,并包含Jedis。找到使用Jedis的Maven依赖项。
org.springframework.boot spring-boot-starter-data-redis io.lettuce lettuce-core redis.clients jedis
jedis的依赖将自动解决类路径上的commons-pool2。
为了配置Jedis池,我们需要使用spring.redis.*前缀与Jedis池连接属性。找到Jedis池的样本配置。
application.properties
spring.redis.host=localhost spring.redis.port=6379 spring.redis.password= spring.redis.jedis.pool.max-active=7 spring.redis.jedis.pool.max-idle=7 spring.redis.jedis.pool.min-idle=2 spring.redis.jedis.pool.max-wait=-1msRedisCacheManager
在Spring Boot中,RedisCacheManager是自动配置的。这里我们将讨论如何配置Spring Boot Redis缓存属性以改变自动配置的RedisCacheManager的默认值,然后我们将创建一个自己的RedisCacheManager示例,以获得对配置的完全控制。
1. 自动配置的 RedisCacheManager如果Redis在我们的Spring Boot应用程序中可用并已配置,RedisCacheManager将被自动配置。我们可以使用spring.cache.*属性来控制Spring缓存的配置。
spring.cache.type: 定义了缓存类型。如果我们不配置这个属性,它将被自动检测到环境。对于Redis缓存,其值为redis。
spring.cache.cache-names: 在启动时创建额外的缓冲区。
Redis缓存的默认值可以通过spring.cache.redis.*进行配置。
spring.cache.redis.cache-null-values: 它接受布尔值。当该值为true时,它将允许缓存空值,否则不允许。
spring.cache.redis.time-to-live: 缓存过期时间。
spring.cache.redis.use-key-prefix: 它接受布尔值。如果是true,那么在写到Redis时将会使用键的前缀。默认值是true。
spring.cache.redis.key-prefix: 定义键前缀。默认情况下,当两个独立的缓存使用同一个键时,会添加一个键前缀以避免键重叠。
找到Redis缓存配置的示例。
application.properties
spring.cache.redis.cache-null-values=false spring.cache.redis.time-to-live=600000 spring.cache.redis.use-key-prefix=true spring.cache.type=redis spring.cache.cache-names=articleCache,allArticlesCache
缓存articleCache和allArticleCache将存活10分钟。
2. 自定义的 RedisCacheManager我们可以创建自己的RedisCacheManager来获得对Redis配置的完全控制。我们需要创建LettuceConnectionFactory Bean、RedisCacheConfiguration Bean和RedisCacheManager,如下所示。
RedisConfig.java
package com.concretepage;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@Configuration
@EnableCaching
@PropertySource("classpath:application.properties")
public class RedisConfig {
@Autowired
private Environment env;
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisConf = new RedisStandaloneConfiguration();
redisConf.setHostName(env.getProperty("spring.redis.host"));
redisConf.setPort(Integer.parseInt(env.getProperty("spring.redis.port")));
redisConf.setPassword(RedisPassword.of(env.getProperty("spring.redis.password")));
return new LettuceConnectionFactory(redisConf);
}
@Bean
public RedisCacheConfiguration cacheConfiguration() {
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.disableCachingNullValues();
return cacheConfig;
}
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager rcm = RedisCacheManager.builder(redisConnectionFactory())
.cacheDefaults(cacheConfiguration())
.transactionAware()
.build();
return rcm;
}
}
RedisCacheConfiguration是一个不可变的类,它有助于自定义Redis的缓存行为,如缓存过期时间,禁止缓存空值等。它也有助于自定义序列化策略。
使用 @EnableCaching 启用缓存为了在我们的应用程序中启用缓存,Spring提供了@EnableCaching注解。@EnableCaching启用注释驱动的缓存管理能力。
它负责注册所需的Spring组件以启用注释驱动的缓存管理。@EnableCaching是用@Configuration或@SpringBootApplication注解来注释的。
SpringBootAppStarter.java
@SpringBootApplication
@EnableCaching
public class SpringBootAppStarter {
public static void main(String[] args) {
SpringApplication.run(SpringBootAppStarter.class, args);
}
}
使用 @Cacheable
@Cacheable表示调用方法的结果可以被缓存,一旦结果被缓存,下一次调用方法的执行将被跳过,并且只提供缓存的结果。找出其中的一些元素。
cacheNames: 储存方法结果的缓存名称。
value: cacheNames的别名。
condition: Spring SpEL表达式可以进行有条件缓存。
key: SpEL动态地计算key。
keyGenerator: 自定义KeyGenerator的Bean名称。
unless: SpEL来否决方法缓存。
sync: 当多个线程试图为同一个key加载一个值时,它被用来同步方法的调用。
为了计算key, condition或unless,我们可以在SpEL中使用以下元数据。
#result: 引用方法的结果。
#root.method: 引用方法。
#root.target: 对目标对象的引用。
#root.caches: 引用受影响的缓存。
#root.methodName:方法名称的快捷方式。
#root.targetClass:目标类的快捷方式。
#root.args[1], #p1 or #a1: 它们给出了方法的第二个参数。改变数值,我们可以得到其他参数。我们也可以通过他们的名字来访问参数。
现在找到使用@Cacheable注解的示例代码片段。
@Cacheable(value= "articleCache", key= "#articleId")
public Article getArticleById(long articleId) {
------
}
在上面的代码中,方法的结果将用articleCache的缓存名称来缓存,使用的键是传递的文章ID。这意味着对于不同的文章ID,结果将以不同的键被缓存,但有相同的缓存名称。一旦方法的结果被缓存为一个键,那么对于相同的键,方法将不会执行,缓存的结果将被提供。
@Cacheable(value= "allArticlesCache", unless= "#result.size() == 0") public ListgetAllArticles(){ ------ }
在上面的代码中,如果结果的大小为0,方法的结果将不会被缓存。 如果我们不提供键,默认情况下,它将是("")或方法参数被用来计算键(如果有的话)。
使用 @CachePut@CachePut 触发了一个缓存投放操作。它不会跳过方法的执行,每次执行的结果都会缓存在相关的缓存中。@CachePut拥有与@Cacheable相同的元素,如cacheNames, value, condition, key, unless, keyGenerator等。找到使用@CachePut的示例代码片段。
@CachePut(value= "articleCache", key= "#article.articleId")
public Article addArticle(Article article){
------
}
上述方法将在每次调用时执行,方法结果将被添加或更新到与给定的缓存名称的键相对应的缓存中。
使用 @CacheEvict@CacheEvict 触发了一个缓存清空操作。它不会跳过方法的执行,并且每次执行都会清空缓存。它的元素包括cacheNames, value, condition, key, keyGenerator, allEntries等。如果allEntries= true,缓存中的所有条目都被移除。找到使用@CacheEvict的代码片段。
@CacheEvict(value= "allArticlesCache", allEntries= true)
public void deleteArticle(long articleId) {
------
}
上述方法将执行每一次调用,所有缓存的条目将被删除。
使用 @Caching@Caching是多个缓存注解的组注解。它有cacheable、put和evict元素。
找到使用@CachePut和@CacheEvict的代码片段,在组中使用@Caching。
@Caching(
put= { @CachePut(value= "articleCache", key= "#article.articleId") },
evict= { @CacheEvict(value= "allArticlesCache", allEntries= true) }
)
public Article updateArticle(Article article) {
------
}
在@Caching组中使用多个@CacheEvict的代码段。
@Caching(
evict= {
@CacheEvict(value= "articleCache", key= "#articleId"),
@CacheEvict(value= "allArticlesCache", allEntries= true)
}
)
public void deleteArticle(long articleId) {
------
}
Spring Boot Cache + Redis + MySQL CRUD 完整示例
这里我们将提供一个Spring Boot Cache + Redis + MySQL CRUD操作的完整例子。在Eclipse中查找项目结构。
现在找到完整的代码。
articles表
CREATE TABLE IF NOT EXISTS `articles` ( `article_id` int(5) NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL, `category` varchar(100) NOT NULL, PRIMARY KEY (`article_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1; INSERT INTO `articles` (`article_id`, `title`, `category`) VALUES (1, 'Spring REST Security', 'Spring'), (2, 'Java Concurrency', 'Java');
ArticleService.java
package com.concretepage.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import com.concretepage.entity.Article;
import com.concretepage.repository.ArticleRepository;
@Service
public class ArticleService implements IArticleService {
@Autowired
private ArticleRepository articleRepository;
@Override
@Cacheable(value= "articleCache", key= "#articleId")
public Article getArticleById(long articleId) {
System.out.println("--- Inside getArticleById() ---");
return articleRepository.findById(articleId).get();
}
@Override
@Cacheable(value= "allArticlesCache", unless= "#result.size() == 0")
public List getAllArticles(){
System.out.println("--- Inside getAllArticles() ---");
List list = new ArrayList<>();
articleRepository.findAll().forEach(e -> list.add(e));
return list;
}
@Override
@Caching(
put= { @CachePut(value= "articleCache", key= "#article.articleId") },
evict= { @CacheEvict(value= "allArticlesCache", allEntries= true) }
)
public Article addArticle(Article article){
System.out.println("--- Inside addArticle() ---");
return articleRepository.save(article);
}
@Override
@Caching(
put= { @CachePut(value= "articleCache", key= "#article.articleId") },
evict= { @CacheEvict(value= "allArticlesCache", allEntries= true) }
)
public Article updateArticle(Article article) {
System.out.println("--- Inside updateArticle() ---");
return articleRepository.save(article);
}
@Override
@Caching(
evict= {
@CacheEvict(value= "articleCache", key= "#articleId"),
@CacheEvict(value= "allArticlesCache", allEntries= true)
}
)
public void deleteArticle(long articleId) {
System.out.println("--- Inside deleteArticle() ---");
articleRepository.delete(articleRepository.findById(articleId).get());
}
}
IArticleService.java
package com.concretepage.service;
import java.util.List;
import com.concretepage.entity.Article;
public interface IArticleService {
List getAllArticles();
Article getArticleById(long articleId);
Article addArticle(Article article);
Article updateArticle(Article article);
void deleteArticle(long articleId);
}
Article.java
package com.concretepage.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="articles")
public class Article implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name="article_id")
private long articleId;
@Column(name="title")
private String title;
@Column(name="category")
private String category;
public long getArticleId() {
return articleId;
}
public void setArticleId(long articleId) {
this.articleId = articleId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
}
ArticleRepository.java
package com.concretepage.repository; import org.springframework.data.repository.CrudRepository; import com.concretepage.entity.Article; public interface ArticleRepository extends CrudRepository{ }
ArticleController.java
package com.concretepage.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.util.UriComponentsBuilder;
import com.concretepage.entity.Article;
import com.concretepage.service.IArticleService;
@Controller
@RequestMapping("user")
public class ArticleController {
@Autowired
private IArticleService articleService;
@GetMapping("article/{id}")
public ResponseEntity getArticleById(@PathVariable("id") Long id) {
Article article = articleService.getArticleById(id);
return new ResponseEntity(article, HttpStatus.OK);
}
@GetMapping("articles")
public ResponseEntity> getAllArticles() {
List list = articleService.getAllArticles();
return new ResponseEntity>(list, HttpStatus.OK);
}
@PostMapping("article")
public ResponseEntity addArticle(@RequestBody Article article, UriComponentsBuilder builder) {
Article savedArticle = articleService.addArticle(article);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(builder.path("/article/{id}").buildAndExpand(savedArticle.getArticleId()).toUri());
return new ResponseEntity(headers, HttpStatus.CREATED);
}
@PutMapping("article")
public ResponseEntity updateArticle(@RequestBody Article article) {
articleService.updateArticle(article);
return new ResponseEntity(article, HttpStatus.OK);
}
@DeleteMapping("article/{id}")
public ResponseEntity deleteArticle(@PathVariable("id") Long id) {
articleService.deleteArticle(id);
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
SpringBootAppStarter.java
package com.concretepage;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class SpringBootAppStarter {
public static void main(String[] args) {
SpringApplication.run(SpringBootAppStarter.class, args);
}
}
RestClientUtil.java
package com.concretepage;
import java.net.URI;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import com.concretepage.entity.Article;
public class RestClientUtil {
public void getArticleByIdDemo(long id) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8080/user/article/{id}";
HttpEntity requestEntity = new HttpEntity(headers);
ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.GET, requestEntity, Article.class, id);
Article article = responseEntity.getBody();
System.out.println("Id:"+article.getArticleId()+", Title:"+article.getTitle()
+", Category:"+article.getCategory());
}
public void getAllArticlesDemo() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8080/user/articles";
HttpEntity requestEntity = new HttpEntity(headers);
ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.GET, requestEntity, Article[].class);
Article[] articles = responseEntity.getBody();
for(Article article : articles) {
System.out.println("Id:"+article.getArticleId()+", Title:"+article.getTitle()
+", Category: "+article.getCategory());
}
}
public void addArticleDemo(Article objArticle) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8080/user/article";
HttpEntity requestEntity = new HttpEntity(objArticle, headers);
URI uri = restTemplate.postForLocation(url, requestEntity);
System.out.println(uri.getPath());
}
public void updateArticleDemo(Article objArticle) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8080/user/article";
HttpEntity requestEntity = new HttpEntity(objArticle, headers);
restTemplate.put(url, requestEntity);
}
public void deleteArticleDemo(long id) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8080/user/article/{id}";
HttpEntity requestEntity = new HttpEntity(headers);
restTemplate.exchange(url, HttpMethod.DELETE, requestEntity, Void.class, id);
}
public static void main(String args[]) {
RestClientUtil util = new RestClientUtil();
//Add article
Article objArticle = new Article();
objArticle.setTitle("Spring REST Security");
objArticle.setCategory("Spring");
//util.addArticleDemo(objArticle);
//Update article
objArticle.setArticleId(1);
objArticle.setTitle("Java Concurrency");
objArticle.setCategory("Java");
//util.updateArticleDemo(objArticle);
//util.deleteArticleDemo(2);
util.getArticleByIdDemo(1);
System.out.println("---- All articles ----");
util.getAllArticlesDemo();
}
}
测试应用程序
要测试我们的演示应用程序,请找到下面的步骤。
1. 使用链接安装并启动Redis。
2. 如果你使用的是Windows操作系统,你可以先安装Cygwin,然后在其中安装Redis。
3. Redis将在6379端口的localhost上启动。
4. 在上面的文章中给出的表导入MySQL数据库中。
5. 我们可以通过以下方式运行我们的Spring Boot应用程序。
a. 使用Eclipse
使用页面末尾的下载链接下载项目的源代码。
将该项目导入eclipse。
使用命令提示符,进入项目的根文件夹并运行。
mvn clean eclipse:eclipse
然后在eclipse中刷新该项目。点击Run as -> Java Application来运行主类MyApplication。
Tomcat服务器将被启动。
b. 使用Maven命令
下载项目的源代码。使用命令提示符进入项目的根文件夹并运行命令。
mvn spring-boot:run
Tomcat服务器将被启动。
c. 使用可执行的JAR
使用命令提示符,转到项目的根文件夹并运行该命令。
mvn clean package
我们将在目标文件夹中得到可执行的spring-boot-app-0.0.1-SNAPSHOT.jar。以下列方式运行这个JAR。
java -jar target/spring-boot-app-0.0.1-SNAPSHOT.jar
Tomcat服务器将被启动。
6. 现在我们已经准备好测试这个应用程序了。要运行客户端,在eclipse中进入RestClientUtil类,点击Run as -> Java Application。
参考文献【1】Spring Boot Reference Guide
【2】Spring Data Redis
【3】Spring Boot Redis
【4】Spring Data Redis Cache
【5】Spring Boot Redis Cache
提取码:mao4
spring-boot-redis-cache.zip



