Teknologian jakaminen

SpringBoot käyttää Redissonia Rediksen ja todellisten käyttöskenaarioiden ohjaamiseen

2024-07-12

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

Esipuhe

olla olemassaSpringBoot käyttää RedisTemplatea ja StringRedisTemplatea Redisin käyttämiseen, esittelimme RedisTemplaten ja sen, kuinka SpringBoot käyttää Redistä RedisTemplate- ja StringRedisTemplate-palvelun kautta.
RedisTemplate的好处就是基于SpringBoot自动装配的原理,使得整合redis时比较简单。

Joten koska SrpingBoot voi käyttää Redisiä RedisTemplaten kautta, miksi Redisson ilmestyy uudelleen?Rddissonin kiinalainen dokumentaatio
Reddissin也是一个redis客户端,其在提供了redis基本操作的同时,还具备其他客户端一些不具备的高精功能,例如:分布式锁+看门狗、分布式限流、远程调用等等。Reddissin的缺点是api抽象,学习成本高。

I. Yleiskatsaus

Spring-boot 2.x -versiosta alkaen spring-boot-data-redis käyttää Lettuce-asiakasta oletuksena tietojen käyttämiseen.

1.1 salaattia

SpringBoot2之后,默认就采用了lettuce。
On edistynyt Redis-asiakas, tapahtumaohjattu viestintäkerros, joka perustuu Netty-kehykseen säikeen turvalliseen synkronointiin, asynkroniseen ja reaktiiviseen käyttöön, tukee klustereita, Sentinelia, putkia ja koodereita.
Lettucen API on säikeen turvallinen ja voi käyttää yhtä Lettuce-yhteyttä erilaisten toimintojen suorittamiseksi. Yhteysesiintymää (StatefulRedisConnection) voidaan käyttää samanaikaisesti useiden säikeiden kesken.

1.2 Reddisson

Tapahtumaohjattu viestintäkerros, joka perustuu Netty-kehykseen, menetelmä on asynkroninen, API on säikeen turvallinen ja voi käyttää yhtä Redisson-yhteyttä erilaisten toimintojen suorittamiseen.
Se toteuttaa hajautetun ja skaalautuvan Java-tietorakenteen, ei tue merkkijonotoimintoja eikä tue Redis-ominaisuuksia, kuten lajittelua, tapahtumia, liukuputkia ja osioita.
Tarjoaa monia hajautettuja liittyviä käyttöpalveluita, kuten hajautettuja lukot, hajautetut kokoelmat, ja voi tukea viivejonoja Redisin kautta.

Tee yhteenveto: Priorisoi käyttämällä Lettucea, jonka yhdistäminen edellyttää edistyneitä hajautettuja ominaisuuksia, kuten hajautettuja lukkoja ja hajautettuja kokoelmia.

2. Spring-Boot integroi Redissonin

2.1 Riippuvuuksien esittely

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

Ilmoitus: Tämän riippuvuuden käyttöönoton jälkeen sitä ei tarvitse ottaa uudelleen käyttöönspring-boot-starter-data-redis,Ettäredisson-spring-boot-starter Se on otettu käyttöön sisäisesti, eikä se sisällä Redisin Luttuce- ja Jedis-asiakkaita. Tästä syystä Luttucen ja Jedisin asetukset application.yamlissa eivät tule voimaan.
Lisää kuvan kuvaus tähän

Kun käytämme Redissonia projektissa, käytämme yleensä RedissonClientiä datatoimintoihin. Joillekin ystäville voi kuitenkin olla hankalaa käyttää RedisTemplatea RedisTemplate.viitataSpringBoot käyttää RedisTemplatea ja StringRedisTemplatea Redisin käyttämiseen

Todettiin, että sen jälkeen kun projekti esitteli Redissonin, RedisTemplaten alaosassa käytetty yhteystehdas oli myös Redisson.
Lisää kuvan kuvaus tähän

2.2 Asetustiedosto

Lisää redis-määritystiedot tiedostoon 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 Konfigurointiluokka

@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 Kuinka käyttää

@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 Käytännön skenaariot

2.5.1 Hajautettu lukko

Kokeneet opiskelijat ajattelevat redis, kun he mainitsevat hajautettujen lukkojen käytön. Joten miten redis toteuttaa hajautetut lukot?

Hajautettujen lukkojen olennainen tavoite on miehittää kuoppa Redisissä (yksinkertaisesti sanottuna se on periaate, että porkkanat miehittävät kuopat, kun myös muut prosessit haluavat miehittää kuopan, he huomaavat, että kuoppassa on jo suuria retiisiä). sinun täytyy luovuttaa tai yrittää myöhemmin uudelleen.

Yleisesti käytetyt menetelmät hajautettuihin lukoihin

1. Käytä setNx-komentoa
Tämän komennon yksityiskohtainen kuvaus on (asetettu, jos sitä ei ole olemassa, se asetetaan (suoritetaan onnistuneesti), kun liiketoiminto on suoritettu, kutsu del-komento poistaaksesi avain (vapauta). kuoppa). esimerkiksi:

# set 锁名 值
setnx distribution-lock  locked

// dosoming

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

Tässä komennossa on kuitenkin ongelma.
Ehkä jotkut ystävät ovat miettineet, että voimme asettaa tälle avaimelle toisen vanhenemisajan. esimerkiksi:

setnx distribution-lock  locked

expire distribution-lock  10

// dosoming

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

Jopa tämän tekemisen jälkeen logiikassa on edelleen ongelmia Koska setnx ja expire ovat kaksi komentoa, jos redis-palvelin katkaisee puhelun setnx:n ja expiren välillä, expire-toimintoa ei suoriteta ja vanhenemisajan asetus epäonnistuu ja lukitus epäonnistuu. siitä tulee umpikuja.

Perimmäinen syy on se, että kaksi komentoa setnx ja expire eivät ole atomikomentoja.

Ja redis asiat eivät voi ratkaista setnx:n ja expire-ongelmaa, koska expire riippuu setnx:n suoritustuloksesta. Jos setnx ei onnistu, expireä ei pitäisi suorittaa. Asioita ei voi arvioida muuten, joten setnx+expire -menetelmä hajautettujen lukkojen toteuttamiseksi ei ole optimaalinen ratkaisu.

2. Käytä setNx Ex -komentoa
SetNx+expire-ongelma on mainittu edellä. Tämän ongelman ratkaisemiseksi Redis-virkailijat ottivat käyttöön set-komennon laajennetut parametrit versiossa 2.8, jotta setnx- ja expire-komennot voidaan suorittaa yhdessä. esimerkiksi:

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

// doSomthing

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

Loogisesti katsottuna setNx Ex on jo optimaalinen ratkaisu, eikä se aiheuta hajautetun lukon lukkiutumista.

Mutta ongelmia voi silti ilmetä kehityksemme aikana. Miksi?
Koska asetimme tälle lukolle vanhenemisajan alussa, entä jos liiketoimintalogiikkamme suoritusaika ylittää asetetun vanhenemisajan? Tulee tilanne, jossa yksi säie ei ole suorittanut suorittamista loppuun ja toinen säie saattaa pitää hajautetun lukon.
Siksi, jos käytät setNx Ex -yhdistelmää, sinun on varmistettava, että lukon aikakatkaisu on suurempi kuin liiketoiminnan suoritusaika lukituksen jälkeen.

3. Käytä lua-skriptiä + vahtikoiran automaattista laajennusmekanismia
Löydän paljon tätä ratkaisua verkosta, joten en mene yksityiskohtiin tässä.

Redisson ottaa käyttöön hajautetut lukot

Yllä esitellyt setNx- ja setNx Ex-komennot ovat Redis-palvelimen tarjoamia alkuperäisiä komentoja, ja niissä on enemmän tai vähemmän ongelmia. Sen ongelman ratkaisemiseksi, että setNx Ex -komennon liikelogiikka on suurempi kuin lukituksen aikakatkaisu, Redisson tarjoaa sisäisen. Lukkoa valvomaan asennetaan vahtikoira. Sen tehtävänä on pidentää lukon voimassaoloaikaa jatkuvasti ennen Redisson-instanssin sulkemista. Watchdog-lukon aikakatkaisu on oletuksena 30 sekuntia (eli uusiminen on 30 sekuntia). Se voidaan määrittää myös erikseen muokkaamalla asetusta Config.lockWatchdogTimeout. Lukon alkuperäinen vanhenemisaika on myös oletusarvoisesti 30 sekuntia.

// 加锁以后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 Virran rajoitus

Meillä on ongelma rajoittaa nykyistä rajapintojen virtaa tai liiketoimintalogiikkaa. Voimme käyttää Guauaan perustuvaa RateLimiter-toteutusta. Itse asiassa Redissionilla on myös samanlainen virtaa rajoittava toiminto.

RateLimiter on nimeltään token bucket nykyinen rajoitus Tämän tyyppinen virran rajoitus on määrittää ensin token-ämpäri, määrittää kuinka monta merkkiä luodaan tietyn ajanjakson aikana ja hankkia määrätty määrä tokeneita token-säilöstä joka kerta, kun sitä käytetään. Jos hankit, se määritetään kelvollisiksi käyttöoikeuksiksi.