Integración de Redis con SpringBoot

Resumen

La integración de Redis con SpringBoot se realiza mediante el uso de SpringData.

SpringData es un proyecto de primer nivel que se desarrolló al mismo tiempo que SpringBoot, y proporciona soporte para operaciones de plantilla en bases de datos comunes.

A partir de SpringBoot 2.x, Jedis ha sido reemplazado por Lettuce.

Jedis

Utiliza una conexión directa, por lo tanto, no es segura para múltiples hilos concurrentes.

Para evitar este problema, se puede emplear un pool de conexiones Jedis.

Sin embargo, esto presenta inconvenientes como la posible sobrecarga del servidor Redis si hay demasiados hilos, además de ser bloqueante.

Lettuce

Se basa en Netty, permitiendo compartir instancias entre múltiples hilos.

No presenta problemas de seguridad en entornos multihilo, reduciendo así la cantidad de hilos necesarios.

Uso

Incluir dependencias

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>


Configuración en yaml

spring:
  redis:
    host: 47.100.222.85
    port: 6379
    jedis:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 500
        min-idle: 0
    lettuce:
      shutdown-timeout: 0ms


Prueba

@Resource
private RedisTemplate redisTemplate;

@Test
void contextLoads() {
    redisTemplate.opsForValue().set("myKey", "myValue");
    System.out.println(redisTemplate.opsForValue().get("myKey"));
}


La salida será: myValue, indicando que la conexión fue exitosa.

Análisis del código fuente

RedisAutoConfiguration

@AutoConfiguration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // El RedisTemplate predeterminado no tiene configuraciones adicionales, los objetos Redis requieren serialización
        // Ambos tipos genéricos son Object, lo cual requiere conversiones al usarlo, siendo poco conveniente, ideal sería <String, Object>
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    // Dado que String es el tipo más usado en Redis, se crea un Bean específico para él
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}


El código muestra cómo SpringBoot automáticamente crea en el contenedor un RedisTemplate y un StringRedisTemplate.

Sin embargo, el RedisTemplate tiene genéricos de tipo <Object, Object>, lo que complica su uso y requiere muchas conversiones.

Es necesario tener un RedisTemplate con genéricos <String, Object>.

Además, no se configuran las estrategias de serialización para las claves ni valores al almacenar en Redis.

Gracias a @ConditionalOnMissingBean, si ya existe un RedisTemplate personalizado en el contenedor, la configuración automática no creará uno nuevo.

Por ello, podemos crear nuestra propia clase de configuración para RedisTemplate.

Integración

Clase de configuración de Redis

import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

/**
 * Clase de configuración de Redis
 */
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisConfig extends CachingConfigurerSupport {

    @Value("${spring.redis.database}")
    private int database;

    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // Establecer base de datos
        connectionFactory.setDatabase(database);
        // Actualizar configuración
        connectionFactory.afterPropertiesSet();
        // Reiniciar conexión
        connectionFactory.resetConnection();
        // Establecer conexión
        template.setConnectionFactory(connectionFactory);
        // Usar Jackson2JsonRedisSerialize en lugar de la serialización por defecto
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.activateDefaultTyping(
                om.getPolymorphicTypeValidator(),
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        om.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        this.registerLocalDateTime(om);
        serializer.setObjectMapper(om);
        // Serialización de clave usando String
        template.setKeySerializer(new StringRedisSerializer());
        // Serialización de valor usando jackson
        template.setValueSerializer(serializer);
        // Serialización de clave hash usando String
        template.setHashKeySerializer(new StringRedisSerializer());
        // Serialización de valor hash usando jackson
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();
        // Añadir soporte para transacciones
        template.setEnableTransactionSupport(true);
        return template;
    }

    /**
     * Manejo de tipos de fecha
     *
     * @param objectMapper objeto a serializar
     */
    private void registerLocalDateTime(ObjectMapper objectMapper) {
        // Establecer formato para clases de fecha java.util.Date
        objectMapper.setDateFormat(new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN));

        JavaTimeModule timeModule = new JavaTimeModule();
        // LocalDateTime
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN);
        timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter));
        timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter));
        // LocalDate
        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN);
        timeModule.addSerializer(LocalDate.class, new LocalDateSerializer(dateFormatter));
        timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(dateFormatter));
        // LocalTime
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN);
        timeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(timeFormatter));
        timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(timeFormatter));
        objectMapper.registerModule(timeModule);

    }

}



Operar directamente con RedisTemplate puede resultar complicado.

Por esta razón, se encapsula en una clase utilitaria llamada RedisUtils, facilitando su uso.

Esta clase RedisUtils se registra en el contenedor Spring, permitiendo su inyección mediante anotaciones.

Utilidad de Redis

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnectionCommands;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Utilidad de Redis
 */
@Component
public class RedisUtils {

    private static RedisTemplate<String, Object> redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        RedisUtils.redisTemplate = redisTemplate;
    }

    /**
     * Prueba de conexión
     *
     * @return resultado de prueba
     */
    public static String ping() {
        return redisTemplate.execute(RedisConnectionCommands::ping);
    }

    /**
     * Cambiar base de datos
     *
     * @param dbIndex número de base de datos (hay 16 bases por defecto, desde 0 hasta 15)
     */
    public static void select(int dbIndex) {
        // Obtener factoría de conexión
        LettuceConnectionFactory connectionFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory();
        if (ObjUtil.isNull(connectionFactory)) {
            return;
        }
        // Establecer base de datos
        connectionFactory.setDatabase(dbIndex);
        // Actualizar configuración
        connectionFactory.afterPropertiesSet();
        redisTemplate.setConnectionFactory(connectionFactory);
    }

    /**
     * Establecer tiempo de expiración para una clave
     *
     * @param key  clave
     * @param time tiempo en segundos, -1 indica permanencia indefinida
     */
    public static boolean expire(String key, long time) {
        return Boolean.TRUE.equals(redisTemplate.expire(key, time, TimeUnit.SECONDS));
    }

    /**
     * Obtener tiempo de expiración de una clave
     *
     * @param key clave
     * @return tiempo en segundos. -1: indefinido; -2: clave expirada o inexistente
     */
    public static long getExpire(String key) {
        Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS);
        return ObjUtil.isNull(expire) ? 0 : expire;
    }

    /**
     * Verificar existencia de clave
     *
     * @param key clave
     * @return resultado
     */
    public static boolean has(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    /**
     * Eliminar caché
     *
     * @param key clave, puede ser una o varias
     */
    public static void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollUtil.toList(key));
            }
        }
    }

    /**
     * Obtener valor de cadena de caché (no se pueden obtener tokens por cifrado interno)
     *
     * @param key clave
     * @return valor
     */
    public static Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * Establecer valor de cadena de caché
     *
     * @param key   clave
     * @param value valor
     */
    public static void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * Establecer valor solo si la clave no existe
     *
     * @param key   clave
     * @param value valor
     */
    public static void setIfAbsent(String key, String value) {
        redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    /**
     * Establecer valor de cadena de caché con tiempo de vida
     *
     * @param key   clave
     * @param value valor
     * @param time  tiempo en segundos, debe ser mayor a 0, si es menor o igual a 0, se establece indefinido
     */
    public static void set(String key, Object value, long time) {
        if (time > 0) {
            redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
        } else {
            set(key, value);
        }
    }

    /**
     * Incrementar
     *
     * @param key clave
     * @param num cantidad a incrementar
     */
    public static long inc(String key, long num) {
        Long increment = redisTemplate.opsForValue().increment(key, num);
        return increment == null ? 0 : increment;
    }

    /**
     * Decrementar
     *
     * @param key clave
     * @param num cantidad a decrementar
     */
    public static long dec(String key, long num) {
        Long decrement = redisTemplate.opsForValue().decrement(key, num);
        return decrement == null ? 0 : decrement;
    }

    /**
     * Obtener contenido de lista
     *
     * @param key clave
     * @return lista
     */
    public static List<Object> getList(String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * Obtener contenido de lista con límites
     *
     * @param key   clave
     * @param start inicio
     * @param end   fin. 0 a -1 representa todos los valores
     */
    public static List<Object> getList(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }

    /**
     * Obtener longitud de lista
     *
     * @param key clave
     */
    public static long getListSize(String key) {
        Long size = redisTemplate.opsForList().size(key);
        return size == null ? 0 : size;
    }

    /**
     * Obtener elemento de lista por índice
     *
     * @param key   clave
     * @param index índice, >=0 indica posición desde inicio, <0 indica posición desde final
     */
    public static Object getList(String key, long index) {
        return redisTemplate.opsForList().index(key, index);
    }

    /**
     * Insertar lista en caché
     *
     * @param key   clave
     * @param value valor
     */
    public static void setList(String key, Object value) {
        redisTemplate.opsForList().rightPush(key, value);
    }

    /**
     * Insertar lista en caché con tiempo de vida
     *
     * @param key   clave
     * @param value valor
     * @param time  tiempo en segundos
     */
    public static void setList(String key, Object value, long time) {
        redisTemplate.opsForList().rightPush(key, value);
        if (time > 0) {
            expire(key, time);
        }
    }

    /**
     * Insertar lista en caché
     *
     * @param key   clave
     * @param value valores
     */
    public static void setList(String key, List<Object> value) {
        redisTemplate.opsForList().rightPushAll(key, value);
    }

    /**
     * Insertar lista en caché con tiempo de vida
     *
     * @param key   clave
     * @param value valores
     * @param time  tiempo en segundos
     */
    public static void setList(String key, List<Object> value, long time) {
        redisTemplate.opsForList().rightPushAll(key, value);
        if (time > 0) {
            expire(key, time);
        }
    }

    /**
     * Reemplazar elemento en lista por índice
     *
     * @param key   clave
     * @param index índice
     * @param value valor
     */
    public static void setList(String key, long index, Object value) {
        redisTemplate.opsForList().set(key, index, value);
    }

    /**
     * Eliminar elementos repetidos de lista
     *
     * @param key   clave
     * @param count cantidad a eliminar
     * @param value valor
     */
    public static void delList(String key, long count, Object value) {
        redisTemplate.opsForList().remove(key, count, value);
    }

    /**
     * Extraer elemento izquierdo de lista
     *
     * @param key clave
     */
    public static void popList(String key) {
        redisTemplate.opsForList().leftPop(key);
    }

    /**
     * Obtener todos los valores de conjunto por clave
     *
     * @param key clave
     */
    public static Set<Object> getSet(String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * Obtener elemento aleatorio de conjunto
     *
     * @param key clave
     * @return valor
     */
    public static Object randomSet(String key) {
        return redisTemplate.opsForSet().randomMember(key);
    }
    
    /**
     * Obtener tamaño de conjunto
     *
     * @param key clave
     */
    public static long getSetSize(String key) {
        Long size = redisTemplate.opsForSet().size(key);
        return size == null ? 0 : size;
    }

    /**
     * Insertar datos en conjunto
     *
     * @param key    clave
     * @param values valores, pueden ser múltiples
     */
    public static void setSet(String key, Object... values) {
        redisTemplate.opsForSet().add(key, values);
    }

    /**
     * Insertar datos en conjunto con tiempo de vida
     *
     * @param key    clave
     * @param time   tiempo en segundos
     * @param values valores, pueden ser múltiples
     */
    public static void setSetTime(String key, long time, Object... values) {
        redisTemplate.opsForSet().add(key, values);
        if (time > 0) {
            expire(key, time);
        }
    }

    /**
     * Verificar existencia de valor en conjunto
     *
     * @param key   clave
     * @param value valor
     * @return resultado
     */
    public static boolean hasSet(String key, Object value) {
        return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, value));

    }

    /**
     * Eliminar valores de conjunto
     *
     * @param key    clave
     * @param values valores, pueden ser múltiples
     */
    public static void delSet(String key, Object... values) {
        redisTemplate.opsForSet().remove(key, values);
    }
    
    /**
     * Agregar elementos, conjuntos ordenados se ordenan por valor de score ascendente
     *
     * @param key   clave
     * @param value nombre del objeto
     * @param score valor de datos
     */
    public static void setZset(String key, TypedTuple<Object> value, double score) {
        redisTemplate.opsForZSet().add(key, value, score);
    }

    /**
     * Agregar múltiples elementos a conjunto ordenado
     *
     * @param key    clave
     * @param values conjunto de elementos
     */
    public static void setZset(String key, Set<TypedTuple<Object>> values) {
        redisTemplate.opsForZSet().add(key, values);
    }

    /*
     * Obtener elementos ordenados por score
     * @param key
     * @param start posición inicial
     * @param end posición final, -1 para todos
     * @return conjunto de elementos
     */
    public static Set<Object> getZset(String key, long start, long end) {
        return redisTemplate.opsForZSet().range(key, start, end);
    }

    /**
     * Eliminar elementos específicos de conjunto ordenado
     *
     * @param key    clave
     * @param values valores
     */
    public static void delZset(String key, Object... values) {
        redisTemplate.opsForZSet().remove(key, values);
    }

    /**
     * Incrementar score de elemento y retornar nuevo valor
     *
     * @param key   clave
     * @param value valor
     * @param num   cantidad a incrementar
     * @return nuevo score
     */
    public static double incScoreZset(String key, String value, double num) {
        Double result = redisTemplate.opsForZSet().incrementScore(key, value, num);
        return ObjUtil.isNull(result) ? 0 : result;
    }

    /**
     * Obtener posición de elemento en conjunto ordenado
     *
     * @param key   clave
     * @param value valor
     * @return posición (0 es primero)
     */
    public static long rankZset(String key, Object value) {
        Long result = redisTemplate.opsForZSet().rank(key, value);
        return ObjUtil.isNull(result) ? 0 : result;
    }

    /**
     * Obtener tamaño del conjunto ordenado
     *
     * @param key clave
     * @return tamaño
     */
    public static long getZsetSize(String key) {
        Long result = redisTemplate.opsForZSet().size(key);
        return ObjUtil.isNull(result) ? 0 : result;
    }

    /**
     * Obtener todos los pares clave-valor asociados a una clave
     *
     * @param key clave
     * @return pares clave-valor
     */
    public static Map<Object, Object> getHash(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * Obtener valor específico de hash por clave y campo
     *
     * @param key  clave
     * @param item campo
     */
    public static Object getHash(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * Establecer múltiples pares clave-valor en hash
     *
     * @param key clave
     * @param map mapa de clave-valor
     */
    public static void setHash(String key, Map<String, Object> map) {
        redisTemplate.opsForHash().putAll(key, map);
    }

    /**
     * Establecer múltiples pares clave-valor en hash con tiempo de vida
     *
     * @param key  clave
     * @param map  mapa de clave-valor
     * @param time tiempo en segundos
     */
    public static void setHash(String key, Map<String, Object> map, long time) {
        redisTemplate.opsForHash().putAll(key, map);
        if (time > 0) {
            expire(key, time);
        }
    }

    /**
     * Insertar par clave-valor en hash, crear si no existe
     *
     * @param key   clave
     * @param item  campo
     * @param value valor
     */
    public static void setHash(String key, String item, Object value) {
        redisTemplate.opsForHash().put(key, item, value);
    }

    /**
     * Insertar par clave-valor en hash, crear si no existe con tiempo de vida
     *
     * @param key   clave
     * @param item  campo
     * @param value valor
     * @param time  tiempo en segundos, si ya existe tiempo, se reemplaza
     */
    public static void setHash(String key, String item, Object value, long time) {
        redisTemplate.opsForHash().put(key, item, value);
        if (time > 0) {
            expire(key, time);
        }
    }

    /**
     * Verificar existencia de campo en hash
     *
     * @param key  clave, no puede ser null
     * @param item campo, no puede ser null
     * @return resultado
     */
    public static boolean hasHash(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * Eliminar campos de hash
     *
     * @param key  clave, no puede ser null
     * @param item campo, puede ser múltiple, no puede ser null
     */
    public static void delHash(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }

    /**
     * Incrementar valor de campo en hash
     *
     * @param key  clave
     * @param item campo
     * @param num  cantidad a incrementar (mayor a 0)
     */
    public static double incHash(String key, String item, double num) {
        return redisTemplate.opsForHash().increment(key, item, num);
    }

    /**
     * Decrementar valor de campo en hash
     *
     * @param key  clave
     * @param item campo
     * @param num  cantidad a decrementar (menor a 0)
     */
    public static double decHash(String key, String item, double num) {
        return redisTemplate.opsForHash().increment(key, item, -num);
    }

}


Referencia

https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0

Versión

6.2.6

Etiquetas: SpringBoot Redis springdata jackson lettuce

Publicado el 10-17 06:03