Spring Boot Redis Cache
August 11, 2018
This page will walk through Spring Boot Redis cache example. RedisCacheManager
is the CacheManager
backed by Redis. If Redis is available and configured in our Spring Boot application, RedisCacheManager
will be auto-configured. Redis connections are obtained from Lettuce or Jedis Java Redis clients. Redis dependencies are resolved by spring-boot-starter-data-redis
starter. In Spring Boot 2.0 Lettuce are resolved by default instead of Jedis. To work with Jedis, we need to include jedis
dependency in our build file.
Spring
@EnableCaching
enables Spring cache management capability in our application. It is annotated with @SpringBootApplication
annotation. @Cacheable
indicates that the result of invoking method can be cached and once result is cached, next call to method execution is skipped and only cached result is served. @CachePut
adds or updates cache but does not skip method execution. @CacheEvict
evicts cache but does not skip method execution. @Caching
is used to group multiple cache annotations.
Contents
- Technologies Used
- Maven File
- application.properties
- Using Lettuce Configurations
- Using Jedis Configurations
- RedisCacheManager
- Enable Caching using @EnableCaching
- Using @Cacheable
- Using @CachePut
- Using @CacheEvict
- Using @Caching
- Spring Boot Cache + Redis + MySQL CRUD Example
- Test Application
- References
- Download Source Code
Technologies Used
Find the technologies being used in our example.1. Java 9
2. Spring 5.0.8.RELEASE
3. Spring Data 2.0.9.RELEASE
4. Spring Boot 2.0.4.RELEASE
5. Maven 3.5.2
6. MySQL 5.5
7. Eclipse Oxygen
Maven File
Spring providesspring-boot-starter-data-redis
to resolve Redis dependencies. It provides basic auto configurations for Lettuce and Jedis client libraries. By default Spring Boot 2.0 uses Lettuce. To get pooled connection factory we need to provide commons-pool2
dependency. Find the Maven file.
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.concretepage</groupId> <artifactId>spring-boot-app</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>spring-boot-app</name> <description>Spring Boot Application</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath/> </parent> <properties> <java.version>9</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>6.0.5</version> </dependency> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.properties
Find the application property file used in our demo application.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
Using Lettuce Configurations
Spring Boot 2.0 starterspring-boot-starter-data-redis
resolves Lettuce by default. Spring provides LettuceConnectionFactory
to get connections. To get pooled connection factory we need to provide commons-pool2
on the classpath. To work with Lettuce we need following Maven dependencies.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
spring.redis.*
prefix with Lettuce pool connection properties. Find the Lettuce pool sample configurations.
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
max-wait
a negative value if we want to block indefinitely.
Using Jedis Configurations
By default Spring Boot 2.0 starterspring-boot-starter-data-redis
uses Lettuce. To use Jedis we need to exclude Lettuce dependency and include Jedis. Find the Maven dependencies to use Jedis.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
jedis
dependency will automatically resolve commons-pool2
on the classpath.
To configure Jedis pool we need to use
spring.redis.*
prefix with Jedis pool connection properties. Find the Jedis pool sample configurations.
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=-1ms
RedisCacheManager
In Spring Boot,RedisCacheManager
is auto-configured. Here we will discuss how to configure Spring Boot Redis cache properties to change its default value for auto-configured RedisCacheManager
and then we will create a sample own RedisCacheManager
to get full control on configurations.
1. Auto-configured
RedisCacheManager
If Redis is available and configured in our Spring Boot application,
RedisCacheManager
will be auto-configured. We can control Spring cache configurations using spring.cache.*
property.
spring.cache.type: Defines cache type. If we do not configure this property, It will be auto-detected to the environment. For Redis cache its value is
redis
.
spring.cache.cache-names: Creates additional caches on startup.
Redis cache defaults can be configured by
spring.cache.redis.*
.
spring.cache.redis.cache-null-values: It accepts Boolean value. When the value is
true
, it will allow caching null values otherwise not.
spring.cache.redis.time-to-live: Cache expiration time.
spring.cache.redis.use-key-prefix: It accepts Boolean value. If
true
then key prefix will be used while writing to Redis. Default value is true
spring.cache.redis.key-prefix: Defines key prefix. By default a key prefix is added to avoid overlapping keys when two separate caches uses same key.
Find the sample Redis cache configurations.
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
and allArticlesCache
will be alive for 10 minutes.
2. Create own
RedisCacheManager
We can create our own
RedisCacheManager
to get the full control of the Redis configurations. We need to create LettuceConnectionFactory
bean, RedisCacheConfiguration
bean and RedisCacheManager
as following.
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
is immutable class that helps customizing Redis cache behavior such as cache expiry time, disable caching null values etc. It is also helpful in customizing serialization strategy.
Enable Caching using @EnableCaching
To enable cache abstraction in our application, Spring provides@EnableCaching
annotation. @EnableCaching
enables annotation-driven cache management capability. It is responsible to register required Spring components to enable annotation-driven cache management. @EnableCaching
is annotated with @Configuration
or @SpringBootApplication
annotations.
SpringBootAppStarter.java
@SpringBootApplication @EnableCaching public class SpringBootAppStarter { public static void main(String[] args) { SpringApplication.run(SpringBootAppStarter.class, args); } }
Using @Cacheable
@Cacheable
indicates that the result of invoking method can be cached and once result is cached, next call to method execution is skipped and only cached result is served. Find some of its elements.
cacheNames: Name of the caches in which method result are stored.
value: Alias for
cacheNames
.
condition: Spring SpEL expression to make conditional caching.
key: SpEL to compute key dynamically.
keyGenerator: Bean name for custom
KeyGenerator
.
unless: SpEL to veto method caching.
sync: It is uses to synchronize method invocation when several threads are attempting to load a value for same key.
To compute
key
, condition
or unless
, we can use following meta-data in SpEL.
#result: Reference to the result of method.
#root.method: Reference to the method.
#root.target: Reference to the target object.
#root.caches: Reference to the affected caches.
#root.methodName: Shortcut to the method name.
#root.targetClass: Shortcut to target class.
#root.args[1], #p1 or #a1: They give second argument of method. Changing numerical value, we can get other arguments. We can also access arguments by their name.
Now find the sample code snippet to use
@Cacheable
annotation.
@Cacheable(value= "articleCache", key= "#articleId") public Article getArticleById(long articleId) { ------ }
articleCache
cache name using key as passed article id. It means for different article id, result will be cached with different key but with same cache name. Once the method result is cached for a key, then for the same key, method will not execute and the cached result will be served.
Find one more example.
@Cacheable(value= "allArticlesCache", unless= "#result.size() == 0") public List<Article> getAllArticles(){ ------ }
Using @CachePut
@CachePut
triggers a cache put operation. It does not skip method execution and result is cached in associated cache for every execution. @CachePut
has elements same like @Cacheable
such as cacheNames
, value
, condition
, key
, unless
, keyGenerator
etc. Find the sample code snippet to use @CachePut
.
@CachePut(value= "articleCache", key= "#article.articleId") public Article addArticle(Article article){ ------ }
Using @CacheEvict
@CacheEvict
triggers a cache evict operation. It does not skip method execution and evicts cache for every execution. It has elements such as cacheNames
, value
, condition
, key
, keyGenerator
, allEntries
etc. If allEntries= true
, all entries inside the caches are removed. Find the code snippet to use @CacheEvict
.
@CacheEvict(value= "allArticlesCache", allEntries= true) public void deleteArticle(long articleId) { ------ }
Using @Caching
@Caching
is the group annotation for multiple cache annotations. It has cacheable
, put
and evict
elements.
Find the code snippet to use
@CachePut
and @CacheEvict
in group using @Caching
.
@Caching( put= { @CachePut(value= "articleCache", key= "#article.articleId") }, evict= { @CacheEvict(value= "allArticlesCache", allEntries= true) } ) public Article updateArticle(Article article) { ------ }
@CacheEvict
in group using @Caching
.
@Caching( evict= { @CacheEvict(value= "articleCache", key= "#articleId"), @CacheEvict(value= "allArticlesCache", allEntries= true) } ) public void deleteArticle(long articleId) { ------ }
Spring Boot Cache + Redis + MySQL CRUD Example
Here we will provide a complete example for Spring Boot Cache + Redis + MySQL CRUD operation. Find the project structure in Eclipse.
Table: 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');
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<Article> getAllArticles(){ System.out.println("--- Inside getAllArticles() ---"); List<Article> 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()); } }
package com.concretepage.service; import java.util.List; import com.concretepage.entity.Article; public interface IArticleService { List<Article> getAllArticles(); Article getArticleById(long articleId); Article addArticle(Article article); Article updateArticle(Article article); void deleteArticle(long articleId); }
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; } }
package com.concretepage.repository; import org.springframework.data.repository.CrudRepository; import com.concretepage.entity.Article; public interface ArticleRepository extends CrudRepository<Article, Long> { }
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<Article> getArticleById(@PathVariable("id") Long id) { Article article = articleService.getArticleById(id); return new ResponseEntity<Article>(article, HttpStatus.OK); } @GetMapping("articles") public ResponseEntity<List<Article>> getAllArticles() { List<Article> list = articleService.getAllArticles(); return new ResponseEntity<List<Article>>(list, HttpStatus.OK); } @PostMapping("article") public ResponseEntity<Void> 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<Void>(headers, HttpStatus.CREATED); } @PutMapping("article") public ResponseEntity<Article> updateArticle(@RequestBody Article article) { articleService.updateArticle(article); return new ResponseEntity<Article>(article, HttpStatus.OK); } @DeleteMapping("article/{id}") public ResponseEntity<Void> deleteArticle(@PathVariable("id") Long id) { articleService.deleteArticle(id); return new ResponseEntity<Void>(HttpStatus.NO_CONTENT); } }
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); } }
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<String> requestEntity = new HttpEntity<String>(headers); ResponseEntity<Article> 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<String> requestEntity = new HttpEntity<String>(headers); ResponseEntity<Article[]> 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<Article> requestEntity = new HttpEntity<Article>(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<Article> requestEntity = new HttpEntity<Article>(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<Article> requestEntity = new HttpEntity<Article>(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(); } }
Test Application
To test our demo application, find the steps.1. Install and start Redis using the link.
2. If you are using Windows OS, you can install Cygwin first and then install Redis in it.
3. Redis will start on localhost at port 6379.
4. Import table in MySQL database given above in the article.
5. We can run our Spring Boot application in following ways.
a. Using Maven Command: Download the project source code. Go to the root folder of the project using command prompt and run the command.
mvn spring-boot:run
b. Using Eclipse: Download the project source code using the download link given at the end of article. Import the project into eclipse. Using command prompt, go to the root folder of the project and run.
mvn clean eclipse:eclipse
SpringBootAppStarter
by clicking Run as -> Java Application. Tomcat server will be started.
c. Using Executable JAR: Using command prompt, go to the root folder of the project and run the command.
mvn clean package
java -jar target/spring-boot-app-0.0.1-SNAPSHOT.jar
6. Now we are ready to test the application. Run
RestClientUtil.java
as Java Application.
References
Spring Boot Reference GuideSpring Data Redis
Spring Boot Redis
Spring Data Redis Cache