Has your project gotten to the point when big data sets and/or time-consuming calculations have begun to affect performance? Or are you struggling to optimize your queries and need to cache some information to avoid continually hitting your database? Then caching could be your solution.
For this article
What is Amazon ElastiCache for Redis?
ElastiCache for Redis is a super fast, in memory, key-value store database. It supports many different kinds of abstract data structures, such as strings, lists, maps, sets, sorted sets,
Setting up ElastiCache for Redis
Begin by navigating to the ElasticCache dashboard, selecting Redis, and create a new cluster. You will be prompted to define a cache name, description, node type (server size), and number of replicates.
VPC & Security Groups
To be able to access your Redis cluster, the instance(s) running our app must have be in the same Virtual Private Network (VPC) and contain the proper security groups. Your EC2 instances must allow the port of your Redis cluster (6379) to be able to communicate. By default
If you wish to run the app locally, consider installing Redis using Docker. The variables outlined in our application.properties file below can be modified to run locally.
Launching your Redis Cluster
Once
Building Our A
For this example application
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>
These libraries allow us to setup our caching config in Spring. A important concept to understand is that the Spring Framework provides its own abstraction for transparently adding caching. You do not only have to use Redis, the abstraction provides a list of providers; Couchbase, EhCache 2.x, Hazelcast, etc. As you will see adding caching to a service method is as simple as providing the appropriate annotation.
Now that we have included the required libraries in our pom file, Spring will try to autoconfigure a RedisCacheManager
. I personally do not like magic behind the scenes so we are going to setup and configure our own annotation based RedisCacheManager
.
A RedisCacheManager
is how we configure and build a cacheManger
for Spring to use. Notice that I have defined the redisHostName
, redisPort
, and redisPrefix
for the Jedis (client library for redis) to use to connect to our cluster.
@Configuration @EnableCaching public class RedisConfig { @Value("${redis.hostname}") private String redisHostName; @Value("${redis.port}") private int redisPort; @Value("${redis.prefix}") private String redisPrefix; @Bean JedisConnectionFactory jedisConnectionFactory() { RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisHostName, redisPort); return new JedisConnectionFactory(redisStandaloneConfiguration); } @Bean(value = "redisTemplate") public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); return redisTemplate; } @Primary @Bean(name = "cacheManager") // Default cache manager is infinite public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(RedisCacheConfiguration.defaultCacheConfig().prefixKeysWith(redisPrefix)).build(); } @Bean(name = "cacheManager1Hour") public CacheManager cacheManager1Hour(RedisConnectionFactory redisConnectionFactory) { Duration expiration = Duration.ofHours(1); return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig().prefixKeysWith(redisPrefix).entryTtl(expiration)).build(); } }
I have defined two cache managers. One that is infinite (default), and once that will cause all keys to expire in 1 hour called cacheManager1Hour
.
The cluster information is passed in from the applications.properties:
redis.hostname=URL_TO_ELASTIC_CACHE_REDIS_CLUSTER redis.port=6379 redis.prefix=testing
Implementing a Simple Service
Now that our Redis cluster is configured in Spring, annotation-based caching is enabled. Let’s assume you have a long-running task that takes 2 seconds to do its work. By annotation the service with @Cacheable
the result of the method call will be cached. I have given this cachable a value of getLongRunningTaskResult
which will be used in its compound key, a key (by default is generated for you), and a cacheManager
(cacheManager1Hour
configured above).
@Cacheable(value = "getLongRunningTaskResult", key="{#seconds}", cacheManager = "cacheManager1Hour") public Optional<TaskDTO> getLongRunningTaskResult(long seconds) { try { long randomMultiplier = new Random().nextLong(); long calculatedResult = randomMultiplier * seconds; TaskDTO taskDTO = new TaskDTO(); taskDTO.setCalculatedResult(calculatedResult); Thread.sleep(2000); // 2 Second Delay to Simulate Workload return Optional.of(taskDTO); } catch (InterruptedException e) { return Optional.of(null); } }
Note: It is important that the resulting object of the method is serializable otherwise the cacheManager will not be able to cache the result.
Testing for Performance Improvements
To easy test the API, I have included swagger-ui which make it simple for developers to interact with the api we have built. I have also created a few simple endpoints to create and flush the cache.
@ApiOperation( value = "Perform the long running task") @RequestMapping(value = "/{seconds}", method = RequestMethod.GET) public ResponseEntity<TaskDTO> longRunningTask(@PathVariable long seconds) { Optional<TaskDTO> user = taskService.getLongRunningTaskResult(seconds); return ResponseUtil.wrapOrNotFound(user); } @ApiOperation( value = "Resets the cache for a key") @RequestMapping(value = "/reset/{seconds}", method = RequestMethod.DELETE) public ResponseEntity<?> reset(@PathVariable long seconds) { taskService.resetLongRunningTaskResult(seconds); return new ResponseEntity<>(HttpStatus.ACCEPTED); }
Once you deploy your app to EC2 navigate to URL path: /swagger-ui.html
From this GUI you can easily test your API performance improvements. Calling the GET endpoint for the first time should take roughly two seconds to return the new calculated result. Calling it subsequently will return an almost instant response as the long-running task is now cached in Redis.
Final Thoughts
Today’s applications demand a responsive user experience. Design your queries and calculations to be as performant as possible, but every once in a while, when you can sacrifice real-time data, just cache it.
Full project code is available at: https://github.com/sixthpoint/spring-boot-elasticache-redis-tutorial.