すべてのプロダクト
Search
ドキュメントセンター

Tair (Redis® OSS-Compatible):Jedisクライアントに関する第2504号の説明

最終更新日:Feb 17, 2025

Jedisクライアントコミュニティの問題 #2504では、ローカルにキャッシュされたルートテーブルを以前のバージョンのJedisClusterで更新できないという問題について説明しています。 この問題により、誤ったアクセスシナリオが発生する可能性があります。 例えば、ノードa (IPアドレス10.10.10.10:6379) がクラスタAから除去されると、IPアドレスは、特定の環境において新しいクラスタに再割り当てされ得る。 JedisClusterは、IPアドレスが割り当てられた新しいクラスタに、クラスタaから要求を送信することができる。 その結果、クエリは失敗します。 この問題は、Jedisの新しいバージョンで修正されました。 Jedisクライアントをバージョン3.10.0以降に更新してください。

更新の提案

  • Jedis 4.x. xまたは5.x. xを使用する場合、上記の問題は存在しません。 ただし、最新バージョンのJedisを使用することを推奨します。

  • Jedis 2.x. xまたは3.x. xを使用する場合は、クライアントをバージョン3.10.0以降に更新します。

説明

プロキシモードのTair (Redis OSS-compatible) クラスターインスタンスには、上記の問題はありません。

詳細

上記の問題が存在するJedis 2.9.0の場合、JedisClusterのローカルルートテーブル管理ロジックがJedisClusterInfoCache.javaファイルに含まれます。 ロジックは主に次の変数に依存します (JedisClusterInfoCache.javaの22〜23行目) 。

private final Map<String, JedisPool> nodes = new HashMap<String, JedisPool>();
private final Map<Integer, JedisPool> slots = new HashMap<Integer, JedisPool>();
  • nodes変数は、host:portとJedisPoolの関係をキャッシュします。

  • slots変数は、slotとJedisPoolの関係をキャッシュします。

前の2つの変数は、初期化中にCLUSTER SLOTS応答に基づいて入力されます。 特定のノード自体の接続を確立するだけです。 ただし、シャードには通常、複数のスロットがあります。 シャードが追加または削除されると、ノードとスロット間のマッピングが変更されます。 次のコードは、3つのシャードを持つクラスターインスタンスのルートテーブルの例を示しています。

xxx 10.3.255.248:6379@13007 master,nofailover - 0 1738808475717 1 connected 0-5461
xxx 10.3.255.249:6379@13007 myself,master,nofailover - 0 0 1 connected 5462-10922
xxx 10.3.255.250:6379@13007 master,nofailover - 0 1738808474709 1 connected 10923-16383

次の図は、ノードスロットの関係を示しています。

image

以前のバージョンのバグは、クラスターインスタンスのルートテーブルが変更された場合、JedisClusterはノードの追加のみを検出でき、期限切れのノードを積極的にリリースしないことです。 その結果、JedisClusterは、新しいルートテーブルをフェッチしようとするときに、期限切れのノードを使用する可能性があります。 次のコードは、JedisClusterがルートテーブルをフェッチする方法の例を示しています。 getShuffledNodesPoolメソッドはnodes変数を使用しますが、nodes変数自体が正しく更新または維持されないため、この問題が発生します。

for (JedisPool jp : getShuffledNodesPool()) {
  try {
    jedis = jp.getResource();
    discoverClusterSlots(jedis);
    return;
  } catch (JedisConnectionException e) {
    // try next nodes
  } finally {
    if (jedis != null) {
      jedis.close();
    }
  }
}

public List<JedisPool> getShuffledNodesPool() {
  r.lock();
  try {
    List<JedisPool> pools = new ArrayList<JedisPool>(nodes.values());
    Collections.shuffle(pools);
    return pools;
  } finally {
    r.unlock();
  }
}

この問題は、JedisコミュニティのPR #2462で修正されました。 この修正により、JedisClusterは期限切れのノードを自動的にリリースできます。

必要に応じて、Jedisで問題を修正するコードを展開して表示できます。

      // Remove dead nodes according to the latest query
      Iterator<Entry<String, JedisPool>> entryIt = nodes.entrySet().iterator();
      while (entryIt.hasNext()) {
        Entry<String, JedisPool> entry = entryIt.next();
        if (!hostAndPortKeys.contains(entry.getKey())) {
          JedisPool pool = entry.getValue();
          try {
            if (pool != null) {
              pool.destroy();
            }
          } catch (Exception e) {
            // pass, may be this node dead
          }
          entryIt.remove();
        }

再生方法

次のコードを実行して、Jedis 2.9.0で問題を再現できます。

説明

まず、接続モードで実行され、初期ノード数のクラスターインスタンスを作成する必要があります。 次に、手動でシャードを追加および削除します。

このプログラムは、ノード変数の値を60秒ごとに表示します。 シャードが追加されると、それに応じてノード変数の値が増加します。 ただし、シャードが削除されると、変更はノード変数に反映されません。

必要に応じて、期待される出力と一緒に再生コードを展開して表示できます。

import java.util.Map;

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisClusterTest{
    /** This method is used to display information about all nodes in the current JedisCluster. */
    private static void printNodes(JedisCluster jc) {
        Map<String, JedisPool> clusterNodes = jc.getClusterNodes();
        System.out.println("time: " + System.currentTimeMillis()/1000 + ", nodes map: " + clusterNodes.size());
        for (String key : clusterNodes.keySet()) {
            System.out.println(key);
        }
        System.out.println();
    }
    public static void main(String[] args) throws Exception {
        /** Check whether the command-line arguments are sufficient. If the arguments are insufficient, you are prompted with the correct usage. */
        if (args.length < 3) {
            System.out.println("Usage: java -jar JedisClusterTest.jar <host> <port> <password>");
            return;
        }

        /** Obtain the endpoint, port number, and account password of the cluster instance from the command-line arguments. */
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        String password = args[2];

        /** Connect to the specified cluster instance. */
        JedisCluster jc = new JedisCluster(new HostAndPort(host, port), 2000, 2000, 5, password,
            new JedisPoolConfig());

        try {
            for (int i = 0; i < Integer.MAX_VALUE; i++) {
                Thread.sleep(1000);
                jc.set("" + i, "" + i);
                if (i % 60 == 0) {
                    printNodes(jc);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

想定される出力:

time: 1738808668, nodes map: 3
10.3.255.248:6379
10.3.255.250:6379
10.3.255.249:6379

// Manually add a shard. 

time: 1738808848, nodes map: 4
10.3.255.248:6379
10.3.255.250:6379
10.3.255.249:6379
10.3.0.3:6379

...

// When you manually remove a shard, the number of nodes does not decrease. 
time: 1738811309, nodes map: 4
10.3.255.248:6379
10.3.255.250:6379
10.3.255.249:6379
10.3.0.3:6379

time: 1738811369, nodes map: 4
10.3.255.248:6379
10.3.255.250:6379
10.3.255.249:6379
10.3.0.3:6379