技術共有

SpringBootはRedissonを利用したRedisの動作と実際の利用シーン

2024-07-12

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

序文

存在するSpringBoot は RedisTemplate と StringRedisTemplate を使用して Redis を操作しますでは、RedisTemplate と、SpringBoot が RedisTemplate と StringRedisTemplate を通じて Redis を操作する方法について紹介しました。
RedisTemplate的好处就是基于SpringBoot自动装配的原理,使得整合redis时比较简单。

SrpingBoot は RedisTemplate 経由で Redis を操作できるのに、なぜ再び Redisson が登場するのでしょうか?Rddisson 中国語ドキュメント
Reddissin也是一个redis客户端,其在提供了redis基本操作的同时,还具备其他客户端一些不具备的高精功能,例如:分布式锁+看门狗、分布式限流、远程调用等等。Reddissin的缺点是api抽象,学习成本高。

I. 概要

spring-boot 2.x バージョン以降、spring-boot-data-redis はデフォルトで Lettuce クライアントを使用してデータを操作します。

1.1 レタス

SpringBoot2之后,默认就采用了lettuce。
高度な Redis クライアントであり、スレッドセーフな同期、非同期およびリアクティブな使用のための Netty フレームワークに基づくイベント駆動型の通信層であり、クラスター、Sentinel、パイプライン、およびエンコーダーをサポートしています。
Lettuce の API はスレッドセーフであり、単一の Lettuce 接続を操作してさまざまな操作を実行でき、接続インスタンス (StatefulRedisConnection) は複数のスレッド間で同時にアクセスできます。

1.2 レディソン

Netty フレームワークに基づくイベント駆動型の通信層であり、メソッドは非同期で、API はスレッドセーフであり、単一の Redisson 接続を操作してさまざまな操作を完了できます。
分散型でスケーラブルな Java データ構造を実装し、文字列操作をサポートせず、並べ替え、トランザクション、パイプライン、パーティションなどの Redis 機能をサポートしません。
分散ロック、分散コレクションなどの多くの分散関連操作サービスを提供し、Redis を介して遅延キューをサポートできます。

要約する: Lettuce の使用を優先します。これには、分散ロックや分散コレクションなどの高度な分散機能が必要です。これと組み合わせるには、Redisson を追加します。

2. Spring-Boot は Redisson を統合します

2.1 依存関係の導入

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

知らせ: この依存関係を導入した後、再度導入する必要はありません。spring-boot-starter-data-redis、それredisson-spring-boot-starterこれは内部的に導入されており、Redis の Luttuce および Jedis クライアントは除外されます。したがって、application.yaml 内の Luttuce と Jedis の構成は有効になりません。
ここに画像の説明を挿入します

プロジェクトで Redisson を使用する場合、通常はデータ操作に RedissonClient を使用しますが、一部の友人は操作に RedissonClient を使用することを好む場合があり、実際には 2 つの構成を定義するだけで済みます。 RedisTemplate のカテゴリ。参照するSpringBoot は RedisTemplate と StringRedisTemplate を使用して Redis を操作します

プロジェクトで Redisson が導入された後、RedisTemplate の下部で使用される接続ファクトリーも Redisson であることが判明しました。
ここに画像の説明を挿入します

2.2 設定ファイル

application.yaml に Redis 構成情報を追加します。

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 構成クラス

@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 使用方法

@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 実践的なシナリオ

2.5.1 分散ロック

ある程度の経験がある学生は、分散ロックの使用について言及するとき、redis を思い浮かべます。では、redis はどのように分散ロックを実装するのでしょうか。

分散ロックの本質的な目的は、Redis のピットを占有することです (簡単に言うと、ニンジンがピットを占有するという原理です) 他のプロセスもそのピットを占有しようとするとき、そのピットにはすでに大きな大根があることがわかります。諦めるか、後でもう一度やり直す必要があります。

分散ロックで一般的に使用される方法

1. setNxコマンドを使用します。
このコマンドの詳細は「存在しない場合は設定」となります。 指定したキーが存在しない場合は設定されます(ピット占有に成功) 業務実行完了後、delコマンドを呼び出してキーを削除します(解放)。ピット)。例えば:

# set 锁名 值
setnx distribution-lock  locked

// dosoming

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

ただし、このコマンドには問題があり、実行ロジックに問題があると del 命令が実行されず、ロックがデッドロックになってしまうことがあります。
おそらく友人の中には、このキーに別の有効期限を設定できると考えた人もいるかもしれません。例えば:

setnx distribution-lock  locked

expire distribution-lock  10

// dosoming

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

これを実行した後でも、依然としてロジックに問題があります。setnx とexpire は 2 つのコマンドであるため、setnx とexpire の間で Redis サーバーがハングアップすると、expire は実行されず、有効期限の設定は失敗し、ロックが残ったままになります。膠着状態になってしまいます。

根本的な原因は、2 つのコマンド setnx とexpired がアトミック コマンドではないことです。

また、setnx の実行結果に依存するため、redis では setnx と期限切れの問題を解決できません。setnx が成功しない場合は、期限切れを実行すべきではありません。そうでなければ物事を判断できないため、分散ロックを実装するための setnx+expire メソッドは最適な解決策ではありません。

2. setNx Ex コマンドを使用します。
setNx+expire の問題は前述しましたが、この問題を解決するために、Redis 担当者はバージョン 2.8 で set コマンドの拡張パラメータを導入し、setnx コマンドとexpire コマンドを同時に実行できるようにしました。例えば:

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

// doSomthing

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

論理的に言えば、setNx Ex はすでに最適なソリューションであり、分散ロックがデッドロックになることはありません。

しかし、開発中に問題が発生する可能性はあります。なぜでしょうか。
このロックには最初に有効期限を設定しているため、ビジネス ロジックの実行時間が設定された有効期限を超えた場合はどうなるでしょうか。 1 つのスレッドが実行を完了しておらず、2 番目のスレッドが分散ロックを保持している状況が発生します。
したがって、setNx Ex の組み合わせを使用する場合は、ロックのタイムアウトがロック後のビジネス実行時間よりも大きくなるようにする必要があります。

3. luaスクリプト+ウォッチドッグ自動拡張機構を利用する
この解決策はオンラインでたくさん見つかるので、ここでは詳しく説明しません。

Redisson は分散ロックを実装します

上記で紹介した setNx および setNx Ex コマンドは Redis サーバーが提供するネイティブ コマンドですが、setNx Ex コマンドのビジネス ロジックがロック タイムアウトを超えるという問題を解決するために、Redisson は内部で提供しています。ロックを監視するためにウォッチドッグがインストールされており、その機能は、Redisson インスタンスが閉じられる前にロックの有効期間を継続的に延長することです。デフォルトでは、ウォッチドッグ チェック ロックのタイムアウトは 30 秒です (つまり、更新は 30 秒です)。Config.lockWatchdogTimeout を変更することで、ロックの初期有効期限もデフォルトで 30 秒に指定できます。

// 加锁以后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 電流制限

高い同時実行性の下では、インターフェイスまたはビジネス ロジックの現在のフローを制限する必要があるという問題に直面しています。実際、Redisssion にも同様の電流制限機能があります。

RateLimiter はトークン バケット電流制限と呼ばれ、最初にトークン バケットを定義し、一定期間内に生成されるトークンの数を指定し、アクセスするたびにトークン バケットから指定された数のトークンを取得します。成功した場合は、有効なアクセスとして設定されます。