Compartir tecnología

SpringBoot usa Redisson para operar Redis y escenarios de uso reales

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Prefacio

existirSpringBoot usa RedisTemplate y StringRedisTemplate para operar Redis, presentamos RedisTemplate y cómo SpringBoot opera Redis a través de RedisTemplate y StringRedisTemplate.
RedisTemplate的好处就是基于SpringBoot自动装配的原理,使得整合redis时比较简单。

Entonces, dado que SrpingBoot puede operar Redis a través de RedisTemplate, ¿por qué aparece Redisson nuevamente?Documentación china de Rddisson
Reddissin也是一个redis客户端,其在提供了redis基本操作的同时,还具备其他客户端一些不具备的高精功能,例如:分布式锁+看门狗、分布式限流、远程调用等等。Reddissin的缺点是api抽象,学习成本高。

I. Descripción general

A partir de la versión spring-boot 2.x, spring-boot-data-redis utiliza el cliente Lettuce para operar los datos de forma predeterminada.

1.1 Lechuga

SpringBoot2之后,默认就采用了lettuce。
Es un cliente Redis avanzado, una capa de comunicación basada en eventos basada en el marco Netty para sincronización segura para subprocesos, uso asincrónico y reactivo, compatible con clústeres, Sentinel, canalizaciones y codificadores.
La API de Lettuce es segura para subprocesos y puede operar una única conexión de Lettuce para completar varias operaciones. Se puede acceder a la instancia de conexión (StatefulRedisConnection) simultáneamente entre varios subprocesos.

1.2 Reddisson

Una capa de comunicación basada en eventos basada en el marco Netty, el método es asincrónico, la API es segura para subprocesos y puede operar una única conexión de Redisson para completar varias operaciones.
Implementa una estructura de datos Java distribuida y escalable, no admite operaciones de cadenas y no admite funciones de Redis como clasificación, transacciones, canalizaciones y particiones.
Proporciona muchos servicios de operaciones relacionadas distribuidas, como bloqueos distribuidos, colecciones distribuidas y puede admitir colas de retraso a través de Redis.

Resumir: Priorice el uso de Lettuce, que requiere funciones distribuidas avanzadas, como bloqueos distribuidos y colecciones distribuidas, para combinar con Redisson.

2. Spring-Boot integra Redisson

2.1 Introduciendo dependencias

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.6</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

Aviso: Después de introducir esta dependencia, no es necesario volver a introducirlaspring-boot-starter-data-redis,Esoredisson-spring-boot-starter Se introdujo internamente y excluye a los clientes Luttuce y Jedis de Redis. Por lo tanto, la configuración de Luttuce y Jedis en application.yaml no tendrá efecto.
Insertar descripción de la imagen aquí

Cuando usamos Redisson en un proyecto, generalmente usamos RedissonClient para operaciones de datos. Sin embargo, algunos amigos pueden encontrar inconvenientes para operar RedissonClient o prefieren usar RedisTemplate para operaciones. Categoría RedisTemplate.Referirse aSpringBoot usa RedisTemplate y StringRedisTemplate para operar Redis

Se descubrió que después de que el proyecto introdujo Redisson, la fábrica de conexiones utilizada en la parte inferior de RedisTemplate también era Redisson.
Insertar descripción de la imagen aquí

2.2 Archivo de configuración

Agregue información de configuración de Redis en application.yaml.

spring:
  data:
    redis:
      mode: master
      # 地址
      host: 30.46.34.190
      # 端口,默认为6379
      port: 6379
      # 密码,没有不填
      password: ''
      # 几号库
      database: 1
      sentinel:
        master: master
        nodes: 30.46.34.190
      cluster:
        nodes: 30.46.34.190
      lettuce:
        pool:
          # 连接池的最大数据库连接数
          max-active: 200
          # 连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
          # 连接池中的最大空闲连接
          max-idle: 50
          # 连接池中的最小空闲连接
          min-idle: 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

2.3 Clase de configuración

@Configuration
@EnableConfigurationProperties({RedisProperties.class})
public class RedissonConfig {

    private static final String REDIS_PROTOCOL_PREFIX = "redis://";

    @Value("${spring.data.redis.mode}")
    private String redisMode;

    private final RedisProperties redisProperties;

    public RedissonConfig(RedisProperties redisProperties) {
        this.redisProperties = redisProperties;
    }

    /**
     * 逻辑参考 RedissonAutoConfiguration#redisson()
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson(@Autowired(required = false) List<RedissonAutoConfigurationCustomizer> redissonAutoConfigurationCustomizers) throws IOException {
        Config config = new Config();
        config.setCheckLockSyncedSlaves(false);

        int max = redisProperties.getLettuce().getPool().getMaxActive();
        int min = redisProperties.getLettuce().getPool().getMinIdle();

        switch (redisMode) {
            case "master": {
                SingleServerConfig singleConfig = config.useSingleServer()
                        .setAddress(REDIS_PROTOCOL_PREFIX + redisProperties.getHost() + ":" + redisProperties.getPort())
                        .setDatabase(redisProperties.getDatabase())
                        .setPassword(redisProperties.getPassword());

                if (redisProperties.getConnectTimeout() != null) {
                    singleConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
                }

                singleConfig.setConnectionPoolSize(max);
                singleConfig.setConnectionMinimumIdleSize(min);
            }
            break;
            case "sentinel": {
                String[] nodes = convert(redisProperties.getSentinel().getNodes());

                SentinelServersConfig sentinelConfig = config.useSentinelServers()
                        .setMasterName(redisProperties.getSentinel().getMaster())
                        .addSentinelAddress(nodes)
                        .setDatabase(redisProperties.getDatabase())
                        .setPassword(redisProperties.getPassword());

                if (redisProperties.getConnectTimeout() != null) {
                    sentinelConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
                }

                sentinelConfig.setMasterConnectionPoolSize(max);
                sentinelConfig.setMasterConnectionMinimumIdleSize(min);
                sentinelConfig.setSlaveConnectionPoolSize(max);
                sentinelConfig.setSlaveConnectionMinimumIdleSize(min);
            }
            break;
            case "cluster": {
                String[] nodes = convert(redisProperties.getCluster().getNodes());

                ClusterServersConfig clusterConfig = config.useClusterServers()
                        .addNodeAddress(nodes)
                        .setPassword(redisProperties.getPassword());

                if (redisProperties.getConnectTimeout() != null) {
                    clusterConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
                }

                clusterConfig.setMasterConnectionMinimumIdleSize(min);
                clusterConfig.setMasterConnectionPoolSize(max);
                clusterConfig.setSlaveConnectionMinimumIdleSize(min);
                clusterConfig.setSlaveConnectionPoolSize(max);
            }
            break;
            default:
                throw new IllegalArgumentException("无效的redis mode配置");
        }

        if (redissonAutoConfigurationCustomizers != null) {
            for (RedissonAutoConfigurationCustomizer customizer : redissonAutoConfigurationCustomizers) {
                customizer.customize(config);
            }
        }

        return Redisson.create(config);

    }

    private String[] convert(List<String> nodesObject) {
        List<String> nodes = new ArrayList<String>(nodesObject.size());
        for (String node : nodesObject) {
            if (!node.startsWith(REDIS_PROTOCOL_PREFIX)) {
                nodes.add(REDIS_PROTOCOL_PREFIX + node);
            } else {
                nodes.add(node);
            }
        }
        return nodes.toArray(new String[0]);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103

2.4 Cómo utilizar

@Component
public class RedissonService {
	@Resource
    protected RedissonClient redissonClient;

	public void redissonExists(String key){
		RBucket<String> rBucketValue = redissonClient.getBucket(key, StringCodec.INSTANCE);
		if (rBucketValue.isExists()){
            String value = rBucketValue.get();
            // doSomething
        } else {
           // doElseSomething
        }
	}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

2.5 Escenarios prácticos

2.5.1 Bloqueo distribuido

Los estudiantes con cierta experiencia piensan en Redis cuando mencionan el uso de bloqueos distribuidos. Entonces, ¿cómo implementa Redis los bloqueos distribuidos?

El objetivo esencial de las cerraduras distribuidas es ocupar un hoyo en Redis (en pocas palabras, es el principio de que las zanahorias ocupan el hoyo). Cuando otros procesos también quieren ocupar el hoyo, descubren que ya hay rábanos grandes en el hoyo. tienes que rendirte o volver a intentarlo más tarde.

Métodos comúnmente utilizados para bloqueos distribuidos.

1. Utilice el comando setNx
La descripción detallada de este comando es (establecer si no existe). Si la clave especificada no existe, se configurará (ocupando con éxito el pozo). Una vez completada la ejecución comercial, llame al comando del para eliminar la clave (liberar). el hoyo). Por ejemplo:

# set 锁名 值
setnx distribution-lock  locked

// dosoming

del  distribution-lock
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Pero hay un problema con este comando. Si hay un problema en la lógica de ejecución, es posible que la instrucción del no se ejecute y el bloqueo se convertirá en un punto muerto.
Quizás algunos amigos hayan pensado cuidadosamente que podemos establecer otro tiempo de vencimiento para esta clave. Por ejemplo:

setnx distribution-lock  locked

expire distribution-lock  10

// dosoming

del  distribution-batch
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Incluso después de hacer esto, todavía hay problemas con la lógica. Dado que setnx y expire son dos comandos, si el servidor redis cuelga entre setnx y expire, expire no se ejecutará, por lo que la configuración del tiempo de vencimiento falla y el bloqueo aún está activo. Se convertirá en un punto muerto.

La causa principal es que los dos comandos setnx y expire no son comandos atómicos.

Y las cosas de Redis no pueden resolver el problema de setnx y expire, porque expire depende del resultado de la ejecución de setnx. Si setnx no tiene éxito, expire no debe ejecutarse. Las cosas no se pueden juzgar de otra manera, por lo que el método setnx+expire para implementar bloqueos distribuidos no es una solución óptima.

2. Utilice el comando setNx Ex
El problema de setNx+expire se mencionó anteriormente. Para resolver este problema, los funcionarios de Redis introdujeron los parámetros extendidos del comando set en la versión 2.8, para que los comandos setnx y expire se puedan ejecutar juntos. Por ejemplo:

# set 锁名 值 ex 过期时间(单位:秒) nx
set distribution-lock locked ex 5 nx

// doSomthing

del distribution-lock
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Lógicamente hablando, setNx Ex ya es una solución óptima y no provocará que el bloqueo distribuido se convierta en un punto muerto.

Pero todavía pueden surgir problemas durante nuestro desarrollo.
Dado que establecimos un tiempo de vencimiento para este bloqueo al principio, ¿qué pasa si el tiempo de ejecución de nuestra lógica de negocios excede el tiempo de vencimiento establecido? Habrá una situación en la que un subproceso no haya completado la ejecución y el segundo subproceso pueda mantener el bloqueo distribuido.
Por lo tanto, si utiliza la combinación setNx Ex, debe asegurarse de que el tiempo de espera de su bloqueo sea mayor que el tiempo de ejecución empresarial después del bloqueo.

3. Utilice el mecanismo de extensión automática lua script + watch dog
Puedo encontrar muchas de estas soluciones en línea, por lo que no entraré en detalles aquí.

Redisson implementa bloqueos distribuidos

Los comandos setNx y setNx Ex presentados anteriormente son comandos nativos proporcionados por el servidor Redis y existen más o menos problemas. Para resolver el problema de que la lógica empresarial del comando setNx Ex es mayor que el tiempo de espera de bloqueo, Redisson proporciona interna. Se instala un perro guardián para monitorear el bloqueo. Su función es extender continuamente el período de validez del bloqueo antes de que se cierre la instancia de Redisson. De forma predeterminada, el tiempo de espera del bloqueo de verificación del mecanismo de vigilancia es de 30 segundos (es decir, la renovación es de 30 segundos). También se puede especificar por separado modificando Config.lockWatchdogTimeout. El tiempo de vencimiento inicial del bloqueo también es de 30 segundos.

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
@Resource
RedissonClient redissonClient;

@GetMapping("/testDistributionLock")
public BaseResponse<String> testRedission(){
    RLock lock = redissonClient.getLock("redis:distributionLock");
    try {
        boolean locked = lock.tryLock(10, 3, TimeUnit.SECONDS);
        if(locked){
            log.info("获取锁成功");
            Thread.sleep(100);
            return ResultUtils.success("ok" );
        }else{
            log.error("获取锁失败");
            return ResultUtils.error(ErrorCode.SYSTEM_ERROR);
        }
    } catch (InterruptedException e) {
        throw new BusinessException(ErrorCode.SYSTEM_ERROR,"出异常了");
    } finally {
        lock.unlock();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

2.5.2 Limitación de corriente

Nos enfrentamos al problema de la necesidad de limitar el flujo actual de interfaces o lógica de negocios en condiciones de alta concurrencia. Podemos usar la implementación de RateLimiter basada en Guaua. De hecho, Redisssion también tiene una función de limitación de corriente similar.

RateLimiter se llama limitación de corriente del depósito de tokens. Este tipo de limitación actual consiste en definir primero un depósito de tokens, especificar cuántos tokens se generan dentro de un cierto período de tiempo y obtener la cantidad especificada de tokens del depósito de tokens cada vez que accede a él. Si obtiene Si tiene éxito, se establece como acceso válido.