Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Caching in MongoDB With Redis Using Spring Boot

DZone's Guide to

Caching in MongoDB With Redis Using Spring Boot

Performance is really essential,especially with database management. Here's a look at MongoDB caching with Spring Boot.

· Performance Zone
Free Resource

Discover 50 of the latest mobile performance statistics with the Ultimate Guide to Digital Experience Monitoring, brought to you in partnership with Catchpoint.

Today, performance is one of the most important metrics we need to evaluate when developing a web service/Application. Keeping customers engaged is critical to any product and for this reason, it is extremely important to improve the performances and reduce page load times.

When running a web server that interacts with a database, its operations may become a bottleneck. MongoDB is no exception here, and as our MongoDB database scales up, things can really slow down. This issue can even get worse if the database server is detached from the web server. In such systems, the communication with the database can cause a big overhead.

Luckily, we can use a method called caching to speed things up. In this paper, we’ll introduce this method and see how we can use it to enhance the performance of our application using Spring Cache, Spring Data, and Redis.

Background

Caching is a strategy aimed at tackling the main storage problem, which means: the bigger the storage is, the slower will be, and vice versa. In a computer, we have a hard drive which is big but also relatively slow. You then have the RAM which is faster but smaller in its storage capabilities, and lastly, the CPU registers which are very fast but tiny. The following chart showcases the memory problem:

Storage

A cache is a component that stores recently accessed data in a faster storage system. Each time a request for that data is made, it can (with some probability) be pulled from the faster memory. The underlying assumption behind caching is that data which have been recently read, have a higher chance of being read again. Thus, they should be stored in a faster memory so that even the next read will be quicker.

To gain a better understanding of this concept, think about a group of people sitting in a library. The library itself represents a huge storage system, but it’s hard to find some books there. In our imagination, the library is a big and slow storage mechanism. Suppose that these people, whenever they find a book, read it but don’t return it since they rather prefer to keep it on their desks. They have this behavior because they are sure they’ll need it again soon, and it makes sense to keep that book on the table where it is more accessible. In this example, the library is the main storage system, while the table is our cache.

In this paper, we’ll build a web service that we’ll call “fast Library”. Here we’ll implement the concept of caching for a virtual library. MongoDB will be the main storage system, and we’ll build the cache using Redis. Our web server will work in Tomcat code written using Spring Boot, Data and Spring Caching Architecture

The Basic System

As the first step, we’ll build a basic web server that stores data in MongoDB. For this demonstration, we’ll name it “fast Library”. The server will have two basic operations:

POST /book: This endpoint will receive the title, the author, and the content of the book, and create a book entry in the database.

GET /book/ {title}: This endpoint will get a title and return its content. We assume that titles uniquely identify books (thus, there won’t be two books with the same title). A better alternative would be, of course, to use an ID. However, to keep things simple, we’ll simply use the title.

This is a simple library system, but we’ll add more advanced abilities later.

Now, let’s create the project using Spring Tool Suite (build using eclipse) and spring starter Project

STS Selection

We are building our project using Java and to build we are using maven, select values and click on next

Image title

Select MongoDB, Redis from NOSQL and Web from the web module and click on finish. We are using Lombok for auto generation of Setters and getters of model values so we need to add the Lombok dependency to the POM

Module Selection

POM

MongoDbRedisCacheApplication.java contains the main method which is used to run Spring Boot Application add

Create model class Book which contains id, book title, author, description and annotate with @Data to generate automatic setters and getters

package com.example;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;

import lombok.Data;

@Data
public class Book {
    @Id
    private String id;
    @Indexed
    private String title;
    private String author;
    private String description;
}

Spring Data creates all basic CRUD Operations for us automatically so let’s create BookRepository.Java which finds book by title and deletes book

package com.example;

import org.springframework.data.mongodb.repository.MongoRepository;

public interface BookRepository  extends MongoRepository<Book, String>
{
    Book findByTitle(String title);

    void delete(String title);
}

Let’s create webservicesController which saves data to MongoDB and retrieve data by idTitle(@PathVariable String title).

package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WebServicesController {
    @Autowired
    BookRepository repository;

    @Autowired
    MongoTemplate mongoTemplate;

    @RequestMapping(value = "/book", method = RequestMethod.POST)
    public Book saveBook(Book book) 
    {
        return repository.save(book);
    }

    @RequestMapping(value = "/book/{title}", method = RequestMethod.GET)
    public Book findBookByTitle(@PathVariable String title) 
    {
        Book insertedBook = repository.findByTitle(title);
        return insertedBook;
    }

}

Adding the Cache

 So far we’ve created a basic library web service, but it’s not astonishingly fast at all. In this section, we’ll try to optimize the findBookByTitle () method by caching the results.

 To get a better idea of how we’ll achieve this goal, let’s go back to the example of the people sitting in a traditional library. Let’s say they want to find the book with a certain title. First of all, they’ll look around the table to see if they already brought it there. If they have, that’s great! They just had a cache hit that is finding an item in the cache. If they haven’t found it, they had a cache miss, meaning they didn’t find the item in the cache. In the case of a missing item, they’ll have to look for the book in the library. When they find it, they’ll keep it on their table or insert it into the cache.

In our paper, we’ll follow exactly the same algorithm for the findBookByTitle () method. When asked for a book with a certain title, we’ll look for it in the cache. If not found, we’ll look for it in the main storage, that is our MongoDB database.

No changes are going to take place in the saveBook () function as it has no effect over the cache. We need to change the findBookByTitle (), which will have the following flow:

Cache ProcessIf we need to install Redis, we can learn how to do here. Once done, run Redis locally on your machine using command

redis-server

To use in our fast library application we need to configure Redis using spring’s CachingConfigurerSupport with below code

package com.example;

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
public class RedisConfiguration extends CachingConfigurerSupport{
    @Bean
    public JedisConnectionFactory jedisConnectionFactory()
    {
        JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
        jedisConnectionFactory.setHostName("127.0.0.1");
        jedisConnectionFactory.setPort(6379);
        return jedisConnectionFactory;
    }

    @Bean
    public RedisTemplate<Object, Object> redisTemplate()
    {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>();
        redisTemplate.setConnectionFactory(jedisConnectionFactory());
        redisTemplate.setExposeConnection(true);
        return redisTemplate;
    }

    @Bean
    public RedisCacheManager cacheManager()
    {
        RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate());
        redisCacheManager.setTransactionAware(true);
        redisCacheManager.setLoadRemoteCachesOnStartup(true);
        redisCacheManager.setUsePrefix(true);
        return redisCacheManager;
    }

}

In above code cache, manager bean is used to configure Redis Cache in our application usage, along with spring boot’s intelligent configuration.

If we want to use already stored cache we need to turn flag under cacheManger to true for the value setLoadRemoteCachesOnStartup. Let’s now write the method

@Cacheable (value = "book", key = "#title")

To delete from the cache when a record is deleted just annotate with below line in BookRepository and let Spring Boot handle cache deletion for us.

@CacheEvict (value = "book", key = "#title")

Caching Policy

We’ve created a basic system that works with Redis to cache all the query results from the database. However, we must admit that it’s not a smart system. It just saves each result to the Redis cache and keeps it there. In this way, the cache will slowly overload the computer’s RAM until it fills up.

Due to this memory limitation, we must delete some of the items in the cache and only keep a few of them. Ideally, we would want to keep those with the highest chances of getting read again only. To select the items we want to delete, we have to establish a sort of caching policy. Deleting random items would probably be a valid policy, but it obviously won’t be very effective. We’ll use one of the most popular policies: the LRU (Least Recently Used). This policy deletes the cache items that were (as the name implies) the least recently used.

Luckily for us, Redis has an LRU mechanism implemented within it, so we don’t have to bother with it on the application layer. To that end, all we have to do is to configure Redis to delete items on an LRU manner. To achieve that, we’ll add two arguments to the command starting Redis. The first will limit the amount of memory it can use (in this example we chose 512 MB) while the second will tell it to use the LRU policy. The command will look like that:

redis-server --maxmemory 10mb --maxmemory-policy allkeys-lru

Keeping the Cache Updated

One of the issues that caching introduces is that of keeping the cache up-to-date when data changes. For example, let’s create the endpoint PUT /book/{title}/{author} that enables us to update the text of a certain book. For that, we shall implement the method updateBookByTitle (title).

The natural thing to do would be to simply update the record in the main database containing that book. But what if the item is in the cache? In that case, the next time we read it, we’ll get a cache hit and read the item from the cache. But that item would be the not updated version of the book, which means that a user might not get its latest version. Not all the systems can tolerate this inaccuracy. Thus, we’ll update the cache with the new, updated data.

In that case, the implementation of the update method will be the following:

    @RequestMapping(value = "/updateByTitle/{title}/{author}", method = RequestMethod.GET)
    @CachePut(value = "book", key = "#title")
    public Book updateByTitle(@PathVariable(value = "title") String title,
            @PathVariable(value = "author") String author)
    {
        Query query = new Query(Criteria.where("title").is(title));
        Update update = new Update().set("author", author);
        Book result = mongoTemplate.findAndModify(query, update,
                new FindAndModifyOptions().returnNew(true).upsert(false), Book.class);
        return result;
    }

Just by adding @CachePut annotation to the database update method, spring boot will handle refreshing cache for us.

Metrics

Now that we have a nice working cached app, it’s time to enjoy the fruit of our work and test the performances of our application. For this test, we have first inserted 1,000 books into the library and then read them randomly. Now we’ll measure how fast the server response times are in the cached application versus the non-cached one. At the end of the test, these are the results in a graph:

Graph

Conclusions

In this paper, we have seen how to speed up a web server connected to a database by caching the data it accesses to. Although here we have used Redis as the cache, we can use other key-value stores for the purpose. An example of another popular database is Memcached. I chose Redis mainly because of its popularity, its detailed documentation, and ease of use.

While caching is a great performance booster, it isn’t a good fit for every application. Here are some considerations you can think about when thinking about caching:

  • Do database reads really have a big impact on your performances? You should make some tests and see if that is your real problem
  • Are you using many different keys for querying? In the main database, many parameters can be used to query a collection. In the cache, just one key (either one parameter or a collection of parameters) can be used for querying. Caching all the possible keys would probably be harmful. Try to think which queries are used the most and should be cached, instead.
  • Does your app perform a lot of database updates? While caching speeds up reads, it also slows down writes.
  • Are you trying to cache complex queries? Complex queries will be harder and less efficient to cache.

Lastly, it is worth keeping this idiom in mind:

Premature optimization is the source of all evil

This should remind us that optimizations have their proper time and place.

You can find full Project code at GitHub 

Is your APM strategy broken? This ebook explores the latest in Gartner research to help you learn how to close the end-user experience gap in APM, brought to you in partnership with Catchpoint.

Topics:
spring boot ,mongo db ,redis ,cache ,java ,performance

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}