Redis: Cache y Lock distribuido (II). Cache distribuida

Estamos de vuelta con el tema de Redis en la segunda parte de esta serie de artículos. En esta entrada hablaremos de la implementación de la caché distribuida usando Redis, esta entrada incluye vídeo explicativo.

¿Qué es la cache distribuida ?

Tomamos este concepto que emite el equipo de Microsoft

Una caché distribuida es una memoria caché compartida por varios servidores de aplicaciones, que normalmente se mantiene como un servicio externo a los servidores de aplicaciones que tienen acceso a ella.

Creo que queda bastante claro, no hay mucho más que agregar. En el vídeo que ponemos al final del artículo hay más elementos teóricos asociados, incluyendo las ventajas del uso de caché distribuida. Te recomiendo que le des un vistazo, y si lo consideras suscríbete a mi canal de YouTube.

¿Cómo funciona la caché distribuida ?

Un sistema de caché distribuida, como se definió anteriormente, emplea uno o varios servicios externos para servir como lugar temporal de alojamiento de la cache. Eventualmente este tipo de sistemas están totalmente optimizados para esta función.

Supongamos que tenemos un microservicio que se encarga de gestionar la información de los usuarios, en el ejemplo que se vera mas adelante, se muestra la entidad User y se emplea un mecanismo de persistencia relacional soportado en MySQL. Supongamos que el microservicio User es altamente demandado y se precisa implementar un mecanismo de escalabilidad horizontal desplegando múltiples instancias.

La información que maneja nuestro servicio relacionado al usuario sería el name, age y salary. Como puede notarse estos datos prácticamente son invariantes temporales, por lo que sería bueno poder emplear algún sistema de caché que ayude en esta tarea.

Al elegir Redis o cualquier otro proveedor de caché, su funcionamiento quedaría de la siguiente forma:

Arquitectura de cache distribuida con múltiples instancias

Supongamos que tenemos un API Gateway u otro MS por delante de User, representado en este esquema por API Gateway + Balancer, y ademas empleando un algoritmo de balanceo basado en Round Robin (RR).

Cuando se inicia una solicitud, por ejemplo: Obtener información del user con id = 1, se indica que vaya por la Instancia 1, y el flujo sería:

  1. API Gateway + Balancer solicita a Instancia 1.
  2. Instancia 1 obtiene de la base de datos SQL información del user con id=1.
  3. SQL devuelve el resultado o null sino existe un user con id=1.
  4. Si existe el objeto, la Instancia 1 lo va a guardar en Redis, sino existe no lo debería guardar.
  5. Instancia 1 devuelve el resultado al solicitante.

Cuando se inicia una nueva solicitud para obtener la información del usuario de id=1, el algoritmo RR enviará la solicitud a la Instancia 2, luego Instancia 3, y así sucesivamente. De ahora en adelante, mientras el objeto esté en caché, ya no se irá más a la base datos SQL, siempre se obtendrá de la cache distribuida.

La caché tiene retos, entre ellos están:

  • Definir el tiempo de expiración de los elementos en la caché.
  • Definir la estrategia de borrado de caché (LRU, LFU).
  • Mantener la consistencia de los objetos de la caché con respecto al lugar de origen de los objetos (en este ejemplo MySQL).
  • Así como otro grupo de requerimientos de administración del proveedor de caché.

Caché en Java con Redis

En este punto existen tres librerías principales para operar con Redis desde Java, cada una tiene sus pros y sus contras, en dependencia del uso que le vayamos a dar a Redis (recordemos que Redis puede ser empleado como sistema de persistencia, caché en memoria, para lock y soporta un grupo de implementaciones distribuidas de tipos similares a los de Java).

En este artículo no vamos a comparar librerías, solo mencionaremos las tres que consideramos que son las mas populares.

Clientes Java para Redis

Solicito disculpas, no encontré un logo para Jedis :-).

Spring Caché

Spring provee desde la versión 3.1 un mecanismo de abstracción de caché. Esto es algo similar a JPA, separa la forma en que implementamos la gestión de caché en Spring, del proveedor de caché que usemos.

En la versión 4.1 y con la salida de la JSR 107 se introducen las anotaciones de caché, para gestionar la forma en que se manipulan los objetos de caché.

Las principales anotaciones de manipulación de objetos de la caché de Spring framework son:

  • @Cacheable: Permite obtener y leer objetos de la caché.
  • @CachePut: Permite actualizar un objeto de la caché.
  • @CacheEvict: Permite eliminar un objeto de la caché.

Además tenemos @EnableCaching, que permite habilitar el sistema de caché.

Las anotaciones de caché de Spring Caché emplean Spring AOP, por lo que debemos usarlas adecuadamente.

Código de ejemplo

Como comentamos arriba, implementamos un microservicio User, el stack que utilizamos fue:

  • Spring Boot
  • Spring Starter Data Redis, incluye ya la dependencia de Lettuce, que será la librería que usaremos para caché, en el vídeo comentamos más de esto.
  • Redis
  • MySQL

La arquitectura de la aplicación de ejemplo es en capas, el código está disponible en GitHub.

Entidad User

@Data
@Entity
public class User implements Serializable {
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String name;
    private int age;
    private float salary;
    private static final long serialVersionUID = 1L;
}

Solo destacar que implementamos Serializable, un requisito necesario para poder guardar a la caché el/los objetos User

Repositorio de acceso a datos para User

public interface UserRepository extends JpaRepository<User, Long> {
	@Modifying(clearAutomatically=true, flushAutomatically = true)
	@Transactional
	@Query(value ="UPDATE user SET salary = :salary WHERE id = :id",
		   nativeQuery = true)
    public int updateSalary(@Param("id") Long id, @Param("salary") float salary);
	/* Use only as example to log when call repository from service*/
    default public Optional<User> findUser(Long id)  {
    	System.out.println("Call repository to get id=" + id);
    	return this.findById(id);
    }
}

Tenemos una consulta de actualización parcial del objeto, que usamos para demostrar el uso de @CachePut, y un método “tonto” que implementamos para incluir un log cada vez que llamemos al repositorio MySQL. Esto es solo para ilustrar cuando estemos obteniendo del repositorio o desde la caché. Cuando se obtiene de la cache ese log no se muestra.

Servicio User

@Service
public class UserService {
    private UserRepository userRepository;
    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    public User save(User user) {
    	return this.userRepository.save(user);
    }
    public List<User> findAll() {
        return this.userRepository.findAll();
    }
    @Cacheable(cacheNames = CacheConfig.USER_CACHE, unless = "#result == null")
    public User findById(Long id) {
        return this.userRepository.findUser(id).orElse(null);
    }
    @CachePut(cacheNames = CacheConfig.USER_CACHE, key = "#id", unless = "#result == null")
    public User updateSalary(Long id, float salary) {
        int res = this.userRepository.updateSalary(id, salary);
        return res > 0 ? this.userRepository.findById(id).orElse(null): null;
    }
    @CacheEvict(cacheNames = CacheConfig.USER_CACHE, key = "#id")
    public void deleteById(Long id) {
        this.userRepository.deleteById(id);
    }
}

Acorde al método vemos el uso de las anotaciones, nótese que hay parámetros en las anotaciones, principalmente:

  • cacheNames: Indica el nombre de la caché dentro de redis.
  • key: Indica el identificador/llave del objeto (llave->valor)
  • unless: Indica cuando no cachear algo, en este ejemplo #result es una referencia a la respuesta, y se le indica que cuando sea null no cachear nada o modificar el estado actual de la caché.

Los parámetros de anotaciones soportan Spring Expression Language. Te recomiendo esta otra entrada en la que explicamos este poderoso lenguaje de Spring.

El controlador no tiene nada relevante, no lo agregaremos acá.

Adicionalmente, para que la caché funcione debemos configurarla y habilitarla.

Configuración de la caché

@Configuration
public class RedisConfig {
    @Bean
    public LettuceConnectionFactory redisConnectionFactory(){
        return new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379));
    }
}

En el Bean anterior, establecemos la conexión al Redis local. En el siguiente fichero configuramos la caché.

@Configuration
public class CacheConfig extends CachingConfigurerSupport {
    public static final String USER_CACHE = "user-cache";
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        redisCacheConfigurationMap.put(USER_CACHE, createConfig(1, ChronoUnit.MINUTES));
        return RedisCacheManager
            .builder(redisConnectionFactory)
            .withInitialCacheConfigurations(redisCacheConfigurationMap)
            .build();
    }
    private static RedisCacheConfiguration createConfig(long time, ChronoUnit temporalUnit) {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.of(time, temporalUnit));
    }
}

Resaltar del segmento de código anterior el Bean CacheManager, que es la interfaz de Spring Cache que define el “todo” para el proveedor de caché que estamos usando. Básicamente creamos nuestras caches (en este caso user-cache), le setteamos las propiedades y el RedisConnectionFactory, que es independiente incluso de la librería de Java que usemos, si desearamos cambiar a Redisson por ejemplo, sería sencillo.

Habilitando la caché

Para habilitar la caché, como comentamos basta con @EnableCaching

@EnableCaching
@SpringBootApplication
public class SpringRedisCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringRedisCacheApplication.class, args);
    }
}

Todo esto no es complejo, pero para entenderlo mejor, he dejado un vídeo explicativo acá:

Video de cache distribuida con Redis y Spring Boot

Adicional a esto, comentarles que en el repositorio de GitHub está la colección Postman mostrada en el vídeo.

Si te ha gustado este ejemplo, puedes regalarme una estrella en el proyecto de GitHub, no cuesta nada, pero aporta mucho.

Nos vemos en el próximo artículo para hablar de Lock distribuido.

1 thought on “Redis: Cache y Lock distribuido (II). Cache distribuida

Comments are closed.