Condivisione della tecnologia

SpringBoot utilizza Redisson per gestire Redis e scenari di utilizzo reali

2024-07-12

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

Prefazione

esistereSpringBoot utilizza RedisTemplate e StringRedisTemplate per gestire Redis, abbiamo introdotto RedisTemplate e il modo in cui SpringBoot gestisce Redis tramite RedisTemplate e StringRedisTemplate.
RedisTemplate的好处就是基于SpringBoot自动装配的原理,使得整合redis时比较简单。

Quindi, poiché SrpingBoot può gestire Redis tramite RedisTemplate, perché Redisson appare di nuovo?Documentazione cinese Rddisson
Reddissin也是一个redis客户端,其在提供了redis基本操作的同时,还具备其他客户端一些不具备的高精功能,例如:分布式锁+看门狗、分布式限流、远程调用等等。Reddissin的缺点是api抽象,学习成本高。

I. Panoramica

A partire dalla versione spring-boot 2.x, spring-boot-data-redis utilizza il client Lettuce per gestire i dati per impostazione predefinita.

1.1 Lattuga

SpringBoot2之后,默认就采用了lettuce。
È un client Redis avanzato, un livello di comunicazione basato sugli eventi basato sul framework Netty per la sincronizzazione thread-safe, l'utilizzo asincrono e reattivo, che supporta cluster, Sentinel, pipeline e codificatori.
L'API di Lettuce è thread-safe e può gestire una singola connessione Lettuce per completare varie operazioni. È possibile accedere contemporaneamente all'istanza di connessione (StatefulRedisConnection) tra più thread.

1.2 Reddisson

Un livello di comunicazione basato sugli eventi basato sul framework Netty, il metodo è asincrono, l'API è thread-safe e può gestire una singola connessione Redisson per completare varie operazioni.
Implementa una struttura dati Java distribuita e scalabile, non supporta operazioni su stringhe e non supporta funzionalità Redis come ordinamento, transazioni, pipeline e partizioni.
Fornisce molti servizi operativi correlati distribuiti, come blocchi distribuiti, raccolte distribuite e può supportare code di ritardo tramite Redis.

Riassumere: dai la priorità all'utilizzo di Lettuce, che richiede funzionalità distribuite avanzate come blocchi distribuiti e raccolte distribuite. Aggiungi Redisson da combinare con esso.

2. Spring-Boot integra Redisson

2.1 Introduzione alle dipendenze

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

Avviso: Dopo aver introdotto questa dipendenza, non è necessario introdurla nuovamentespring-boot-starter-data-redis,Quelloredisson-spring-boot-starter È stato introdotto internamente ed esclude i client Luttuce e Jedis di Redis. Pertanto, la configurazione di Luttuce e Jedis in application.yaml non avrà effetto.
Inserisci qui la descrizione dell'immagine

Quando utilizziamo Redisson in un progetto, generalmente utilizziamo RedissonClient per le operazioni sui dati Tuttavia, alcuni amici potrebbero trovare RedissonClient scomodo da utilizzare o preferire utilizzare RedisTemplate per le operazioni. In effetti, i due possono coesistere Categoria Redis.fare riferimento aSpringBoot utilizza RedisTemplate e StringRedisTemplate per gestire Redis

Si è scoperto che dopo l'introduzione di Redisson nel progetto, anche la factory di connessione utilizzata nella parte inferiore di RedisTemplate era Redisson.
Inserisci qui la descrizione dell'immagine

2.2 File di configurazione

Aggiungi le informazioni sulla configurazione di Redis in 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 Classe di configurazione

@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 Come utilizzare

@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 Scenari pratici

2.5.1 Blocco distribuito

Gli studenti con una certa esperienza pensano a Redis quando menzionano l'uso dei blocchi distribuiti. Quindi, come fa Redis a implementare i blocchi distribuiti?

L'obiettivo essenziale dei blocchi distribuiti è occupare una fossa in Redis (in parole povere, è il principio delle carote che occupano le fosse). Quando anche altri processi vogliono occupare la fossa, scoprono che ci sono già grandi ravanelli nella fossa. devi arrenderti o riprovare più tardi.

Metodi comunemente utilizzati per i blocchi distribuiti

1. Utilizzare il comando setNx
La descrizione dettagliata di questo comando è (imposta se non esiste). Se la chiave specificata non esiste, verrà impostata (occupando con successo la fossa). Dopo aver completato l'esecuzione dell'attività, chiamare il comando del per eliminare la chiave (release fossa). Per esempio:

# set 锁名 值
setnx distribution-lock  locked

// dosoming

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

Ma c'è un problema con questo comando. Se c'è un problema nella logica di esecuzione, l'istruzione del potrebbe non essere eseguita e il blocco diventerà una situazione di stallo.
Forse alcuni amici hanno pensato bene che possiamo fissare un'altra data di scadenza per questa chiave. Per esempio:

setnx distribution-lock  locked

expire distribution-lock  10

// dosoming

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

Anche dopo aver fatto ciò, ci sono ancora problemi con la logica Poiché setnx e scade sono due comandi, se il server Redis riattacca tra setnx e scade, scade non verrà eseguito e l'impostazione del tempo di scadenza fallirà e il blocco verrà interrotto. essere ancora impostato. Diventerà una situazione di stallo.

La causa principale è che i due comandi setnx e scade non sono comandi atomici.

E le cose redis non possono risolvere il problema di setnx e di scadenza, perché la scadenza dipende dal risultato dell'esecuzione di setnx. Se setnx non ha esito positivo, la scadenza non deve essere eseguita. Altrimenti non è possibile giudicare le cose, quindi il metodo setnx+expire per implementare i blocchi distribuiti non è una soluzione ottimale.

2. Utilizzare il comando setNx Ex
Il problema setNx+expire è stato menzionato sopra. Per risolvere questo problema, i funzionari di Redis hanno introdotto i parametri estesi del comando set nella versione 2.8, in modo che i comandi setnx e scadere possano essere eseguiti insieme. Per esempio:

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

// doSomthing

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

Logicamente setNx Ex è già una soluzione ottimale e non porterà il blocco distribuito a diventare un deadlock.

Ma potrebbero ancora sorgere problemi durante il nostro sviluppo. Perché?
Poiché all'inizio impostiamo una scadenza per questo blocco, cosa succede se il tempo di esecuzione della nostra logica aziendale supera la scadenza impostata? Si verificherà una situazione in cui un thread non ha completato l'esecuzione e il secondo thread potrebbe contenere il blocco distribuito.
Pertanto, se si utilizza la combinazione setNx Ex, è necessario assicurarsi che il timeout del blocco sia maggiore del tempo di esecuzione aziendale dopo il blocco.

3. Utilizzare lo script lua + il meccanismo di estensione automatica watch dog
Posso trovare molte di queste soluzioni online, quindi non entrerò nei dettagli qui.

Redisson implementa blocchi distribuiti

I comandi setNx e setNx Ex introdotti sopra sono comandi nativi forniti dal server Redis e ci sono più o meno problemi. Per risolvere il problema che la logica aziendale del comando setNx Ex è maggiore del timeout del blocco, Redisson fornisce soluzioni interne. È installato un watchdog per monitorare il blocco. La sua funzione è estendere continuamente il periodo di validità del blocco prima che l'istanza Redisson venga chiusa. Per impostazione predefinita, il timeout del blocco del controllo del watchdog è di 30 secondi (ovvero, il rinnovo è di 30 secondi. Può anche essere specificato separatamente modificando Config.lockWatchdogTimeout. Anche il tempo di scadenza iniziale del blocco è di 30 secondi).

// 加锁以后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 Limitazione di corrente

Ci troviamo di fronte al problema della necessità di limitare il flusso corrente delle interfacce o della logica aziendale in condizioni di concorrenza elevata. Possiamo utilizzare l'implementazione RateLimiter basata su Guaua. In effetti, anche Redisssion ha una funzione di limitazione della corrente simile.

RateLimiter è chiamato limitazione corrente del bucket di token. Questo tipo di limitazione corrente consiste nel definire innanzitutto un bucket di token, specificare il numero di token generati entro un determinato periodo di tempo e ottenere il numero specificato di token dal bucket di token ogni volta che si accede. Se ottieni Se ha successo, viene impostato come accesso valido.