Technologieaustausch

SpringBoot verwendet Redisson, um Redis und tatsächliche Nutzungsszenarien zu betreiben

2024-07-12

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

Vorwort

existierenSpringBoot verwendet RedisTemplate und StringRedisTemplate, um Redis zu betreiben, wir haben RedisTemplate vorgestellt und wie SpringBoot Redis über RedisTemplate und StringRedisTemplate betreibt.
RedisTemplate的好处就是基于SpringBoot自动装配的原理,使得整合redis时比较简单。

Warum wird Redisson erneut angezeigt, da SrpingBoot Redis über RedisTemplate betreiben kann?Rddisson Chinesische Dokumentation
Reddissin也是一个redis客户端,其在提供了redis基本操作的同时,还具备其他客户端一些不具备的高精功能,例如:分布式锁+看门狗、分布式限流、远程调用等等。Reddissin的缺点是api抽象,学习成本高。

I. Übersicht

Ab der Spring-Boot-Version 2.x verwendet Spring-Boot-Data-Redis standardmäßig den Lettuce-Client, um Daten zu verwalten.

1.1 Kopfsalat

SpringBoot2之后,默认就采用了lettuce。
Ist ein erweiterter Redis-Client, eine ereignisgesteuerte Kommunikationsschicht, die auf dem Netty-Framework für threadsichere Synchronisierung, asynchrone und reaktive Nutzung basiert und Cluster, Sentinel, Pipelines und Encoder unterstützt.
Die API von Lettuce ist threadsicher und kann eine einzelne Lettuce-Verbindung betreiben, um verschiedene Vorgänge auszuführen. Auf die Verbindungsinstanz (StatefulRedisConnection) kann gleichzeitig von mehreren Threads aus zugegriffen werden.

1.2 Reddisson

Eine ereignisgesteuerte Kommunikationsschicht, die auf dem Netty-Framework basiert. Die Methode ist asynchron, die API ist threadsicher und kann eine einzelne Redisson-Verbindung betreiben, um verschiedene Vorgänge abzuschließen.
Es implementiert eine verteilte und skalierbare Java-Datenstruktur, unterstützt keine Zeichenfolgenoperationen und unterstützt keine Redis-Funktionen wie Sortierung, Transaktionen, Pipelines und Partitionen.
Bietet viele verteilte verwandte Betriebsdienste, wie z. B. verteilte Sperren und verteilte Sammlungen, und kann Verzögerungswarteschlangen über Redis unterstützen.

Zusammenfassen: Priorisieren Sie die Verwendung von Lettuce, für dessen Kombination erweiterte verteilte Funktionen wie verteilte Sperren und verteilte Sammlungen erforderlich sind.

2. Spring-Boot integriert Redisson

2.1 Einführung von Abhängigkeiten

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

Beachten: Nach der Einführung dieser Abhängigkeit besteht keine Notwendigkeit, sie erneut einzuführenspring-boot-starter-data-redis,Dasredisson-spring-boot-starter Es wurde intern eingeführt und schließt die Luttuce- und Jedis-Kunden von Redis aus. Daher wird die Konfiguration von Luttuce und Jedis in application.yaml nicht wirksam.
Fügen Sie hier eine Bildbeschreibung ein

Wenn wir Redisson in einem Projekt verwenden, verwenden wir im Allgemeinen RedissonClient für den Betrieb. Einige Freunde bevorzugen jedoch die Verwendung von RedisTemplate für den Betrieb. Wir müssen nur die Konfiguration definieren RedisTemplate-Kategorie.beziehen aufSpringBoot verwendet RedisTemplate und StringRedisTemplate, um Redis zu betreiben

Es wurde festgestellt, dass nach der Einführung von Redisson im Projekt die unten in RedisTemplate verwendete Verbindungsfabrik ebenfalls Redisson war.
Fügen Sie hier eine Bildbeschreibung ein

2.2 Konfigurationsdatei

Fügen Sie Redis-Konfigurationsinformationen in application.yaml hinzu.

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 Konfigurationsklasse

@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 Verwendung

@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 Praktische Szenarien

2.5.1 Verteilte Sperre

Studierende mit etwas Erfahrung denken an Redis, wenn sie die Verwendung verteilter Sperren erwähnen. Wie implementiert Redis also verteilte Sperren?

Das wesentliche Ziel verteilter Schlösser besteht darin, eine Grube in Redis zu besetzen (vereinfacht ausgedrückt handelt es sich um das Prinzip der Karottenbelegung. Wenn andere Prozesse die Grube ebenfalls besetzen möchten, stellen sie fest, dass sich bereits große Radieschen in der Grube befinden). Sie müssen aufgeben oder es später noch einmal versuchen.

Häufig verwendete Methoden für verteilte Sperren

1. Verwenden Sie den Befehl setNx
Die detaillierte Beschreibung dieses Befehls lautet (Setzen, wenn nicht vorhanden). Wenn der angegebene Schlüssel nicht vorhanden ist, wird er festgelegt (die Grube wird erfolgreich belegt). Rufen Sie nach Abschluss der Geschäftsausführung den Befehl del auf, um den Schlüssel zu löschen (Release). die Grube). Zum Beispiel:

# set 锁名 值
setnx distribution-lock  locked

// dosoming

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

Bei diesem Befehl liegt jedoch ein Problem vor. Wenn in der Ausführungslogik ein Problem vorliegt, wird die Del-Anweisung möglicherweise nicht ausgeführt und die Sperre wird zu einem Deadlock.
Vielleicht haben einige Freunde darüber nachgedacht, dass wir für diesen Schlüssel eine andere Ablaufzeit festlegen können. Zum Beispiel:

setnx distribution-lock  locked

expire distribution-lock  10

// dosoming

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

Auch danach gibt es immer noch Probleme mit der Logik. Da es sich bei setnx und Expire um zwei Befehle handelt, wird Expire nicht ausgeführt, wenn der Redis-Server zwischen Setnx und Expire aufhängt, und die Einstellung der Ablaufzeit schlägt fehl noch festgelegt werden. Es wird zu einem Stillstand kommen.

Die Hauptursache liegt darin, dass die beiden Befehle setnx und expire keine atomaren Befehle sind.

Und Redis-Dinge können das Problem von Setnx und Expire nicht lösen, da Expire vom Ausführungsergebnis von Setnx abhängt. Wenn Setnx nicht erfolgreich ist, sollte Expire nicht ausgeführt werden. Wenn nicht, kann die Sache nicht beurteilt werden, daher ist die setnx+expire-Methode zur Implementierung verteilter Sperren keine optimale Lösung.

2. Verwenden Sie den setNx Ex-Befehl
Das Problem von setNx+expire wurde oben erwähnt. Um dieses Problem zu lösen, haben Redis-Beamte in Version 2.8 die erweiterten Parameter des set-Befehls eingeführt, sodass die Befehle setnx und expire zusammen ausgeführt werden können. Zum Beispiel:

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

// doSomthing

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

Logischerweise ist setNx Ex bereits eine optimale Lösung und führt nicht dazu, dass die verteilte Sperre zu einem Deadlock wird.

Aber während unserer Entwicklung können immer noch Probleme auftreten.
Da wir zu Beginn eine Ablaufzeit für diese Sperre festgelegt haben, was passiert, wenn die Ausführungszeit unserer Geschäftslogik die festgelegte Ablaufzeit überschreitet? Es kann vorkommen, dass ein Thread die Ausführung noch nicht abgeschlossen hat und der zweite Thread möglicherweise die verteilte Sperre hält.
Wenn Sie die setNx Ex-Kombination verwenden, müssen Sie daher sicherstellen, dass das Zeitlimit Ihrer Sperre größer ist als die Geschäftsausführungszeit nach der Sperre.

3. Verwenden Sie den automatischen Erweiterungsmechanismus Lua-Skript + Watch Dog
Da ich viele dieser Lösungen online finden kann, werde ich hier nicht näher darauf eingehen.

Redisson implementiert verteilte Sperren

Die oben vorgestellten Befehle setNx und setNx Ex sind native Befehle, die vom Redis-Server bereitgestellt werden, und es gibt mehr oder weniger Probleme. Um das Problem zu lösen, dass die Geschäftslogik des Befehls setNx Ex größer als das Sperrzeitlimit ist, stellt Redisson interne Befehle bereit Zur Überwachung der Sperre ist ein Watchdog installiert. Seine Funktion besteht darin, die Gültigkeitsdauer der Sperre kontinuierlich zu verlängern, bevor die Redisson-Instanz geschlossen wird. Standardmäßig beträgt die Zeitüberschreitung der Watchdog-Sperre 30 Sekunden (d. h. die Erneuerungszeit beträgt 30 Sekunden). Sie kann auch separat durch Ändern von Config.lockWatchdogTimeout festgelegt werden. Die anfängliche Ablaufzeit der Sperre beträgt standardmäßig ebenfalls 30 Sekunden.

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

Wir stehen vor dem Problem, den aktuellen Schnittstellen- oder Geschäftslogikfluss bei hoher Parallelität begrenzen zu müssen. Wir können die auf Guaua basierende RateLimiter-Implementierung verwenden. Tatsächlich verfügt Redisssion auch über eine ähnliche Strombegrenzungsfunktion.

RateLimiter wird als Token-Bucket-Strombegrenzung bezeichnet. Diese Art der Strombegrenzung besteht darin, zunächst einen Token-Bucket zu definieren, anzugeben, wie viele Token innerhalb eines bestimmten Zeitraums generiert werden, und bei jedem Zugriff die angegebene Anzahl von Token aus dem Token-Bucket abzurufen . Wenn Sie erfolgreich sind, wird es als gültiger Zugriff festgelegt.