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
- 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.
- 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.
- 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.
- 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.
- 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.
Cons of Redis
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.
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.
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
services:
redis:
image: redis
ports:
- 6379:6379
volumes:
- redis-data:/data
volumes:
redis-data:
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.1.1'
lombok.config
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value
application.yml
redis:
host: localhost
port: 6379
handler/DefaultCacheErrorHandler.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
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
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
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
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
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
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
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
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
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
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
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
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.
By fetching the user Jane for the first time, we can see that the API took 3.01 seconds to return the data.
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.
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.
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