Skip to main content

Redis Boosting Performance and Simplifying Data Storage

00h 08m 28s

In the world of software development, efficient data storage and retrieval are essential for building high-performing applications. One technology that has gained significant popularity in recent years is Redis (Remote Dictionary Server).

Redis is an in-memory data structure store that can be used as a database, cache, or message broker, offering numerous advantages for developers.

In this story, we'll explore the pros and cons of Redis, explain its implementation using code examples, and highlight the benefits of using Redis in your applications.

Pros of Redis

  1. Lightning-Fast Performance: Redis thrives in performance due to its in-memory storage and optimized data structures. By keeping data in memory, Redis eliminates disk I/O (Input/Output) overhead, resulting in significantly lower latency. It can handle millions of operations per second, making it an excellent choice for applications that require real-time data processing or high throughput.
Redis Performance
  1. Versatile Data Structures: Redis provides various data structures, such as strings, lists, sets, sorted sets, and hashes. These structures allow you to model complex data scenarios efficiently. For instance, using Redis lists, you can implement queues or stacks easily. Sorted sets are perfect for leaderboards and ranking systems. The flexibility of these data structures simplifies application development and reduces the need for additional layers in your stack.
Redis Data Structures
  1. Caching Capabilities: Redis is widely used as a caching layer due to its speed and scalability. By caching frequently accessed data in Redis, you can avoid expensive database queries or computations, resulting in improved response times and reduced load on your primary data store. With Redis' built-in expiration and eviction mechanisms, you can manage cache size and ensure that the most relevant data is readily available.
Redis Caching
  1. Pub/Sub Messaging: Redis includes a powerful publish/subscribe (pub/sub) mechanism, allowing different parts of your application to communicate asynchronously. Publishers send messages to specific channels, and subscribers receive messages from those channels. This feature enables the development of real-time applications, chat systems, or event-driven architectures with ease.
Redis Pub/Sub Messaging
  1. Scalability: Redis can scale horizontally by distributing data across multiple Redis instances using sharding or clustering techniques. This scalability enables Redis to handle growing data volumes and increasing workloads, making it a suitable choice for high-traffic applications or systems with demanding scalability requirements.
Redis Scalability

Cons of Redis

  1. Limited Storage Capacity: As Redis keeps data in memory, the available storage capacity is limited by the available RAM. Storing large datasets might require additional hardware or careful management of memory consumption. However, Redis provides persistence options, allowing you to save data to disk periodically or replicate it to multiple Redis instances for increased fault tolerance.

  2. Lack of Complex Querying: Redis is primarily designed for simple key-value storage and lacks the advanced querying capabilities found in traditional databases. While Redis supports basic operations like fetching data by key, it does not provide complex SQL-like querying or indexing features. As a result, applications heavily relying on complex querying might require additional layers or integration with other database systems.

  3. Single-Threaded Nature: Redis operates as a single-threaded server, processing commands sequentially. While this design simplifies Redis' implementation and ensures data consistency, it limits the potential for parallel execution across multiple CPU cores. However, Redis is highly optimized to minimize latency, and for many use cases, its single-threaded architecture doesn't pose a significant performance bottleneck.

Code example

Here is a basic example implementation of Redis Cache using the Java Spring Boot 3+ version.

docker-compose.yml

yml
services:
  redis:
    image: redis
    ports:
      - 6379:6379
    volumes:
      - redis-data:/data

volumes:
  redis-data:

build.gradle

markdown
implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.1.1'

lombok.config

markdown
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value

application.yml

yml
redis:
  host: localhost
  port: 6379

handler/DefaultCacheErrorHandler.java

java
package com.example.demo.handler;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.interceptor.SimpleCacheErrorHandler;
import org.springframework.lang.NonNull;

@Slf4j
public class DefaultCacheErrorHandler extends SimpleCacheErrorHandler {

    @Override
    public void handleCacheGetError(
            @NonNull RuntimeException exception,
            @NonNull Cache cache,
            @NonNull Object key
    ) {
        log.info(
                "handleCacheGetError ~ {}: {} - {}",
                exception.getMessage(),
                cache.getName(),
                key
        );
    }

    @Override
    public void handleCachePutError(
            @NonNull RuntimeException exception,
            @NonNull Cache cache,
            @NonNull Object key,
            Object value
    ) {
        log.info(
                "handleCachePutError ~ {}: {} - {}",
                exception.getMessage(),
                cache.getName(),
                key
        );
        super.handleCachePutError(exception, cache, key, value);
    }

    @Override
    public void handleCacheEvictError(
            @NonNull RuntimeException exception,
            @NonNull Cache cache,
            @NonNull Object key
    ) {
        log.info(
                "handleCacheEvictError ~ {}: {} - {}",
                exception.getMessage(),
                cache.getName(),
                key
        );
        super.handleCacheEvictError(exception, cache, key);
    }

    @Override
    public void handleCacheClearError(
            @NonNull RuntimeException exception,
            @NonNull Cache cache
    ) {
        log.info(
                "handleCacheClearError ~ {}: {}",
                exception.getMessage(),
                cache.getName()
        );
        super.handleCacheClearError(exception, cache);
    }

}

config/RedisConfig.java

java
package com.example.demo.config;

import com.example.demo.handler.DefaultCacheErrorHandler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurer;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

import java.time.Duration;

@Configuration
public class RedisCacheConfig implements CachingConfigurer {

    @Value("${redis.host}")
    private String redisHost;

    @Value("${redis.port}")
    private int redisPort;

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(redisHost);
        configuration.setPort(redisPort);
        return new LettuceConnectionFactory(configuration);
    }

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration
                .defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(15));
    }

    @Bean
    @Override
    public CacheErrorHandler errorHandler() {
        return new DefaultCacheErrorHandler();
    }

    @Bean("longLifeCacheManager")
    public CacheManager longLifeCacheManager() {
        RedisCacheConfiguration defaultConfiguration = RedisCacheConfiguration
                .defaultCacheConfig()
                .entryTtl(Duration.ofDays(90));

        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory())
                .cacheDefaults(defaultConfiguration)
                .build();
    }

    @Primary
    @Bean("shortLifeCacheManager")
    public CacheManager shortLifeCacheManager() {
        RedisCacheConfiguration defaultConfiguration = RedisCacheConfiguration
                .defaultCacheConfig()
                .entryTtl(Duration.ofDays(1));

        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory())
                .cacheDefaults(defaultConfiguration)
                .build();
    }

}

model/User.java

java
package com.example.demo.model;

import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

@Data
@Builder
@RedisHash("user")
public class User {

    @Id
    private String id;
    private String name;
    private Integer age;

}

dto/request/CreateUserRequestDTO.java

java
package com.example.demo.dto.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class CreateUserRequestDTO {

    @JsonProperty(value = "name")
    private String name;

    @JsonProperty(value = "age")
    private Integer age;

}

dto/request/UpdateUserRequestDTO.java

java
package com.example.demo.dto.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class UpdateUserRequestDTO {

    @JsonProperty(value = "name")
    private String name;

    @JsonProperty(value = "age")
    private Integer age;

}

dto/UserDTO.java

java
package com.example.demo.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO implements Serializable {

    @JsonProperty(value = "id")
    private String id;

    @JsonProperty(value = "name")
    private String name;

    @JsonProperty(value = "age")
    private Integer age;

}

repository/UserRepository.java

java
package com.example.demo.repository;

import com.example.demo.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.lang.NonNull;

import java.util.List;

public interface UserRepository extends CrudRepository<User, String> {

    @NonNull
    List<User> findAll();

}

dao/UserDao.java

java
package com.example.demo.dao;

import com.example.demo.model.User;

import java.util.List;
import java.util.Optional;

public interface UserDao {

    User createUser(User user);
    List<User> findAllUsers();
    Optional<User> findUserById(String userId);
    User updateUser(User user);
    void deleteUserById(String userId);

}

daoImpl/UserDaoJpaImpl.java

java
package com.example.demo.daoImpl;

import com.example.demo.dao.UserDao;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository("jpa")
@AllArgsConstructor
public class UserDaoJpaImpl implements UserDao {

    private UserRepository userRepository;

    @Override
    public User createUser(User user) {
        return userRepository.save(user);
    }

    @Override
    public List<User> findAllUsers() {
        return userRepository.findAll();
    }

    @Override
    public Optional<User> findUserById(String userId) {
        return userRepository.findById(userId);
    }

    @Override
    public User updateUser(User user) {
        return userRepository.save(user);
    }

    @Override
    public void deleteUserById(String userId) {
        userRepository.deleteById(userId);
    }

}

util/UserDTOMapperUtil.java

java
package com.example.demo.util;

import com.example.demo.dto.UserDTO;
import com.example.demo.model.User;
import org.springframework.stereotype.Component;

import java.util.function.Function;

@Component
public class UserDTOMapperUtil implements Function<User, UserDTO> {

    @Override
    public UserDTO apply(User user) {
        return UserDTO
                .builder()
                .id(user.getId())
                .name(user.getName())
                .age(user.getAge())
                .build();
    }

}

service/UserService.java

java
package com.example.demo.service;

import com.example.demo.dto.UserDTO;
import com.example.demo.dto.request.CreateUserRequestDTO;
import com.example.demo.dto.request.UpdateUserRequestDTO;

import java.util.List;

public interface UserService {

    UserDTO createUser(CreateUserRequestDTO request);
    UserDTO findUserById(String userId);
    List<UserDTO> findAll();
    UserDTO updateUserById(String userId, UpdateUserRequestDTO request);
    void deleteUserById(String userId);

}

serviceImpl/UserServiceImpl.java

java
package com.example.demo.serviceImpl;

import lombok.AllArgsConstructor;
import com.example.demo.dao.UserDao;
import com.example.demo.dto.UserDTO;
import com.example.demo.dto.request.CreateUserRequestDTO;
import com.example.demo.dto.request.UpdateUserRequestDTO;
import com.example.demo.util.UserDTOMapperUtil;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
@EnableCaching
@AllArgsConstructor
public class UserServiceImpl implements UserService {

    @Qualifier("jpa")
    private UserDao userDao;

    private UserDTOMapperUtil userDTOMapperUtil;

    @Override
    public UserDTO createUser(CreateUserRequestDTO request) {
        User user = User
                .builder()
                .name(request.getName())
                .age(request.getAge())
                .build();

        User createdUser = userDao.createUser(user);

        return UserDTO
                .builder()
                .id(createdUser.getId())
                .name(createdUser.getName())
                .age(createdUser.getAge())
                .build();
    }

    @Override
    @Cacheable(value = "user", key = "#userId")
    public UserDTO findUserById(String userId) {
        doLongRunningTask();
        return userDao
                .findUserById(userId)
                .map(userDTOMapperUtil)
                .orElseThrow();
    }

    @Override
    public List<UserDTO> findAll() {
        return userDao
                .findAllUsers()
                .stream()
                .map(userDTOMapperUtil)
                .collect(Collectors.toList());
    }

    @Override
    @CacheEvict(value = "user", key = "#userId")
    public UserDTO updateUserById(
            String userId,
            UpdateUserRequestDTO request
    ) {
        User userToUpdate = userDao
                .findUserById(userId)
                .orElseThrow();

        if (request.getName() != null) {
            userToUpdate.setName(request.getName());
        }

        if (request.getAge() != null) {
            userToUpdate.setAge(request.getAge());
        }

        User updatedUser = userDao.updateUser(userToUpdate);

        return UserDTO
                .builder()
                .id(updatedUser.getId())
                .name(updatedUser.getName())
                .age(updatedUser.getAge())
                .build();
    }

    @Override
    @CacheEvict(value = "user", key = "#userId")
    public void deleteUserById(String userId) {
        userDao.deleteUserById(userId);
    }

    private void doLongRunningTask() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

controller/UserController.java

java
package com.example.demo.controller;

import com.example.demo.dto.UserDTO;
import com.example.demo.dto.request.CreateUserRequestDTO;
import com.example.demo.dto.request.UpdateUserRequestDTO;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.example.demo.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Slf4j
@Controller
@AllArgsConstructor
@RequestMapping(value = "/api/v1/redis")
public class RedisController {

    private UserService userService;

    @PostMapping()
    public ResponseEntity<UserDTO> createUser(
            @RequestBody CreateUserRequestDTO request
    ) {
        UserDTO createdUserDTO = userService.createUser(request);
        return ResponseEntity
                .status(201)
                .body(createdUserDTO);
    }

    @GetMapping("/{userId}")
    public ResponseEntity<UserDTO> findUserById(
            @PathVariable("userId") String userId
    ) {
        UserDTO userDTO = userService.findUserById(userId);
        return ResponseEntity
                .status(200)
                .body(userDTO);
    }

    @GetMapping()
    public ResponseEntity<List<UserDTO>> findAll() {
        List<UserDTO> userDTOs = userService.findAll();
        return ResponseEntity
                .status(200)
                .body(userDTOs);
    }

    @PutMapping("/{userId}")
    public ResponseEntity<UserDTO> updateUserById(
            @PathVariable("userId") String userId,
            @RequestBody UpdateUserRequestDTO request
    ) {
        UserDTO updatedUserDTO = userService.updateUserById(userId, request);
        return ResponseEntity
                .status(200)
                .body(updatedUserDTO);
    }

    @DeleteMapping("/{userId}")
    public ResponseEntity<Void> deleteFromCache(
            @PathVariable("userId") String userId
    ) {
        userService.deleteUserById(userId);
        return ResponseEntity
                .status(200)
                .body(null);
    }

}

Results

After creating two users (John and Jane) and then fetching all users, we can see that the API fetches both of them in 6 ms. User find all

By fetching the user Jane for the first time, we can see that the API took 3.01 seconds to return the data. User find by id without redis

By fetching the user Jane again from this point on, we can see that the API took 5 ms to return the exact same data. This is because the API fetched data from the Redis Cache, making it about 602 times faster for performance. User find by id with redis

Conclusion

This example was just a small fraction of what Redis can do. Overall, Redis offers a powerful and efficient solution for data storage, caching, messaging, and more. Its exceptional performance, versatile data structures, and ease of use make it an attractive choice for a wide range of applications, enabling developers to build robust and scalable systems.

Share this story:

Contact Us

Take the first step of your journey today, let us help you build a unique software that stands out from the crowd and creates a ripple effect of success.

Contact Peopleoid