

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# Lettuce 用戶端組態 (Valkey 和 Redis OSS)
<a name="BestPractices.Clients-lettuce"></a>

本節說明建議的 Java 和 Lettuce 組態選項，以及它們如何套用至 ElastiCache 叢集。

本節中的建議已使用 Lettuce 版本 6.2.2 進行測試。

**Topics**
+ [

# 範例：叢集模式的 Lettuce 組態，啟用 TLS
](BestPractices.Clients-lettuce-cme.md)
+ [

# 範例：停用叢集模式的 Lettuce 組態、啟用 TLS
](BestPractices.Clients-lettuce-cmd.md)

**Java DNS 快取 TTL**

Java 虛擬機器 (JVM) 會快取 DNS 名稱查詢。當 JVM 將主機名稱解析為 IP 位址時，它會在指定的時間段內快取 IP 位址，稱為*存留時間* (TTL)。

選擇 TTL 值是為了在延遲和對變化的回應能力之間進行權衡。DNS 解析程式使用較短的 TTL，可以更快地注意到叢集 DNS 中的更新。這可讓您的應用程式更快回應叢集所經歷的替換或其他工作流程。不過，如果 TTL 太低，則會增加查詢量，進而增加應用程式的延遲時間。雖然沒有正確的 TTL 值，但在設定 TTL 值時，值得考量可以等待變更生效的時間長度。

由於 ElastiCache 節點使用可能會變更的 DNS 名稱項目，因此我們建議您將 JVM 設定為 5 到 10 秒的低 TTL。這可確保當節點的 IP 地址變更時，您的應用程式將可透過重新查詢 DNS 項目來接收並使用資源的新 IP 地址。

在一些 Java 組態上，JVM 的預設 TTL 會如此設定，在重新啟動 JVM 之前，「絕不」重新整理 DNS 項目。

如需如何設定 JVM TTL 的詳細資訊，請參閱[如何設定 JVM TTL](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/java-dg-jvm-ttl.html#how-to-set-the-jvm-ttl)。

**Lettuce 版本**

建議使用 Lettuce 6.2.2 或更新版本。

**端點**

當您使用已啟用叢集模式的叢集時，請將 `redisUri` 設為叢集組態端點。此 URI 的 DNS 查閱會傳回叢集中所有可用節點的清單，並在叢集初始化期間隨機解析到其中之一。如需拓撲重新整理如何運作的詳細資訊，請參閱本主題稍後的 *dynamicRefreshResources*。

**SocketOption**

啟用 [KeepAlive](https://lettuce.io/core/release/api/io/lettuce/core/SocketOptions.KeepAliveOptions.html)。啟用此選項可減少在命令執行期間處理失敗連線的需求。

請務必根據應用程式需求和工作負載設定[連線逾時](https://lettuce.io/core/release/api/io/lettuce/core/SocketOptions.Builder.html#connectTimeout-java.time.Duration-)。如需詳細資訊，請參閱本主題稍後的 Timeouts (逾時) 章節。

**ClusterClientOption：已啟用叢集模式的用戶端選項**

當連線中斷時，啟用 [AutoReconnect](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterClientOptions.Builder.html#autoReconnect-boolean-)。

設置 [CommandTimeout](https://lettuce.io/core/release/api/io/lettuPrce/core/RedisURI.html#getTimeout--)。如需更多詳細資料，請參閱本主題稍後的 Timeouts (逾時) 章節。

設定 [nodeFilter](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterClientOptions.Builder.html#nodeFilter-java.util.function.Predicate-) 以篩選出拓樸中故障的節點。Lettuce 會將「叢集節點」輸出中找到的所有節點 (包含具有 PFAIL/FAIL 狀態的節點) 儲存在用戶端的「分割區」(也稱為碎片) 中。在建立叢集拓撲的過程中，它會嘗試連線到所有的分割區節點。當節點因任何原因被取代時，新增故障節點的這種 Lettuce 行為可能會導致連線錯誤 (或警告)。

例如，在容錯移轉完成且叢集啟動復原程序之後，clusterTopology 正在重新整理時，叢集匯流排節點對應的時間很短，在完全從拓撲中移除之前，向下節點會列為 FAIL 節點。在此期間，Lettuce 用戶端會將其視為正常運作的節點，並持續與其連線。這會在重試耗盡後導致失敗。

例如：

```
final ClusterClientOptions clusterClientOptions = 
    ClusterClientOptions.builder()
    ... // other options
    .nodeFilter(it -> 
        ! (it.is(RedisClusterNode.NodeFlag.FAIL) 
        || it.is(RedisClusterNode.NodeFlag.EVENTUAL_FAIL) 
        || it.is(RedisClusterNode.NodeFlag.HANDSHAKE)
        || it.is(RedisClusterNode.NodeFlag.NOADDR)))
    .validateClusterNodeMembership(false)
    .build();
redisClusterClient.setOptions(clusterClientOptions);
```

**注意**  
節點篩選最適合用於 DynamicRefreshSources 設為 true 時。否則，如果拓撲檢視是從單一有問題的種子節點中取得，並將某些碎片的主節點視為故障，則它將篩除此主節點，這將導致槽不被涵蓋。擁有多個種子節點 (DynamicrefreshSources 為真時) 可降低發生此問題的可能性，因為在使用新升級的主節點進行容錯移轉之後，至少部分種子節點應具有更新的拓撲檢視。

**ClusterTopologyRefreshOptions：用於控制啟用叢集模式的用戶端的叢集拓撲重新整理選項**

**注意**  
已停用叢集模式的叢集不支援叢集探索命令，也不相容於所有用戶端動態拓撲探索功能。  
ElastiCache 的停用叢集模式與 Lettuce 的 `MasterSlaveTopologyRefresh` 不相容。相反地，對於停用的叢集模式，您可以設定 `StaticMasterReplicaTopologyProvider` 並提供叢集讀取和寫入端點。  
如需連接至已停用叢集模式之叢集的詳細資訊，請參閱 [尋找 Valkey 或 Redis OSS （停用叢集模式） 叢集的端點 （主控台）](Endpoints.md#Endpoints.Find.Redis)。  
如果您想要使用 Lettuce 的動態拓撲探索功能，可以使用與現有叢集相同的碎片組態來建立已啟用叢集模式的叢集。不過，對於已啟用叢集模式的叢集，建議至少設定 3 個具有至少 1 個複本的碎片，以支援快速容錯移轉。

啟用 [enablePeriodicRefresh](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.Builder.html#enablePeriodicRefresh-java.time.Duration-)。這會啟用定期叢集拓撲更新，以便用戶端以重新整理期間的間隔更新叢集拓撲 (預設值：60 秒)。停用時，用戶端只有在嘗試對叢集執行命令發生錯誤時，叢集拓撲才會更新。

啟用此選項後，您可以將此工作新增至背景任務，以減少與重新整理叢集拓撲相關的延遲。雖然拓撲重新整理是在背景工作中執行，但對於具有許多節點的叢集而言，可能會有些慢。這是因為所有節點都在查詢其檢視以取得最新的叢集檢視。如果您執行的是大型叢集，則可能需要增加期間。

啟用 [enableAllAdaptiveRefreshTriggers](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.Builder.html#enableAllAdaptiveRefreshTriggers--)。這會啟用使用所有[觸發器](https://lettuce.io/core/6.1.6.RELEASE/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.RefreshTrigger.html)的適應性拓樸重新整理：MOVED\$1REDIRECT, ASK\$1REDIRECT, PERSISTENT\$1RECONNECTS, UNCOVERED\$1SLOT, UNKNOWN\$1NODE。自適應重新整理觸發會根據 Valkey 或 Redis OSS 叢集操作期間發生的事件啟動拓撲檢視更新。啟用此選項會在上述觸發程序發生時立即重新整理拓撲。適應性觸發程序重新整理會使用逾時來限制速率，因為事件可能會大規模發生 (更新之間的預設逾時：30)。

啟用 [closeStaleConnections](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.Builder.html#closeStaleConnections-boolean-)。如此可在重新整理叢集拓樸時關閉過時的連線。只有在 [ClusterTopologyRefreshOptions.isPeriodicRefreshEnabled()](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.html#isPeriodicRefreshEnabled--) 為真時，它才會生效。啟用後，用戶端可以關閉過時的連線，並在背景建立新連線。這可減少在命令執行期間處理失敗連線的需求。

啟用 [dynamicRefreshResources](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.Builder.html#dynamicRefreshSources-boolean-)。我們建議您為小型叢集啟用 dynamicRefreshResources，並在大型叢集時停用。dynamicRefreshResources 可讓您從提供的種子節點 (例如，叢集組態端點) 探索叢集節點。它會使用所有探索到的節點做為重新整理叢集拓撲的來源。

使用動態重新整理查詢所有探索到的叢集拓撲節點，並嘗試選擇最準確的叢集檢視。如果其設定為 false，則只會使用初始種子節點做為拓撲探索的來源，而且只會取得初始種子節點的用戶端數量。在停用時，如果叢集配置端點解析為故障的節點，則嘗試重新整理叢集檢視會失敗，並導致例外狀況。這種情況可能會發生，因為從叢集組態端點移除故障節點的項目需要一些時間。因此，組態端點仍可在短時間內隨機解析為故障的節點。

但是，在其啟用後，我們會使用從叢集檢視接收到的所有叢集節點，來查詢其目前的檢視。因為我們會從該檢視中篩選出故障的節點，所以拓撲重新整理將會成功。不過，當動態重新整理來源為真時，Lettuce 會查詢所有節點以取得叢集檢視，然後比較結果。因此，對於具有大量節點的叢集來說，它可能很昂貴。建議您在多節點的叢集時關閉此功能。

```
final ClusterTopologyRefreshOptions topologyOptions = 
    ClusterTopologyRefreshOptions.builder()
    .enableAllAdaptiveRefreshTriggers()
    .enablePeriodicRefresh()
    .dynamicRefreshSources(true)
    .build();
```

**ClientResources**

使用 [DirContextDnsResolver](https://lettuce.io/core/release/api/io/lettuce/core/resource/DirContextDnsResolver.html) 設定 [DnsResolver](https://lettuce.io/core/release/api/io/lettuce/core/resource/DefaultClientResources.Builder.html#dnsResolver-io.lettuce.core.resource.DnsResolver-)。該 DNS 解析程式係以 Java 的 com.sun.jndi.dns.DnsContextFactory 為基礎。

使用指數退避和完全抖動來設定 [reconnectDelay](https://lettuce.io/core/release/api/io/lettuce/core/resource/DefaultClientResources.Builder.html#reconnectDelay-io.lettuce.core.resource.Delay-)。Lettuce 具有以指數退避策略為基礎的內建重試機制。如需詳細資訊，請參閱 AWS 架構部落格上的[指數退避和抖動](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter)。如需重試退避策略重要性的詳細資訊，請參閱 AWS 資料庫部落格上[最佳實務部落格文章](https://aws.amazon.com/blogs/database/best-practices-redis-clients-and-amazon-elasticache-for-redis/)的退避邏輯區段。

```
ClientResources clientResources = DefaultClientResources.builder()
   .dnsResolver(new DirContextDnsResolver())
    .reconnectDelay(
        Delay.fullJitter(
            Duration.ofMillis(100),     // minimum 100 millisecond delay
            Duration.ofSeconds(10),      // maximum 10 second delay
            100, TimeUnit.MILLISECONDS)) // 100 millisecond base
    .build();
```

**逾時**

使用低於指令逾時的連線逾時值。Lettuce 使用延遲連接建立。因此，如果連線逾時高於指令逾時，而 Lettuce 嘗試連線到健康狀態不良的節點，且永遠超過指令逾時，則您可能會在拓撲重新整理後發生一段時間的持續性失敗。

針對不同的指令使用動態指令逾時。我們建議您根據指令預期持續時間設定指令逾時。例如，對重複多個金鑰的指令 (例如 FLUSHDB、FLUSHALL、KEYS、SMEMBERS 或 Lua 指令碼) 使用較長的逾時時間。對於單一金鑰指令 (例如 SET、GET 和 HSET) 使用較短的逾時。

**注意**  
下列範例中設定的逾時適用於執行 SET/GET 指令的測試，且金鑰和值長度最多 20 個位元組。在指令很複雜或金鑰和值較大時，處理時間可能會更長。您應該根據應用程式的使用案例設定逾時。

```
private static final Duration META_COMMAND_TIMEOUT = Duration.ofMillis(1000);
private static final Duration DEFAULT_COMMAND_TIMEOUT = Duration.ofMillis(250);
// Socket connect timeout should be lower than command timeout for Lettuce
private static final Duration CONNECT_TIMEOUT = Duration.ofMillis(100);
    
SocketOptions socketOptions = SocketOptions.builder()
    .connectTimeout(CONNECT_TIMEOUT)
    .build();
 

class DynamicClusterTimeout extends TimeoutSource {
     private static final Set<ProtocolKeyword> META_COMMAND_TYPES = ImmutableSet.<ProtocolKeyword>builder()
          .add(CommandType.FLUSHDB)
          .add(CommandType.FLUSHALL)
          .add(CommandType.CLUSTER)
          .add(CommandType.INFO)
          .add(CommandType.KEYS)
          .build();

    private final Duration defaultCommandTimeout;
    private final Duration metaCommandTimeout;

    DynamicClusterTimeout(Duration defaultTimeout, Duration metaTimeout)
    {
        defaultCommandTimeout = defaultTimeout;
        metaCommandTimeout = metaTimeout;
    }

    @Override
    public long getTimeout(RedisCommand<?, ?, ?> command) {
        if (META_COMMAND_TYPES.contains(command.getType())) {
            return metaCommandTimeout.toMillis();
        }
        return defaultCommandTimeout.toMillis();
    }
}

// Use a dynamic timeout for commands, to avoid timeouts during
// cluster management and slow operations.
TimeoutOptions timeoutOptions = TimeoutOptions.builder()
.timeoutSource(
    new DynamicClusterTimeout(DEFAULT_COMMAND_TIMEOUT, META_COMMAND_TIMEOUT))
.build();
```

# 範例：叢集模式的 Lettuce 組態，啟用 TLS
<a name="BestPractices.Clients-lettuce-cme"></a>

**注意**  
下列範例中的逾時適用於執行 SET/GET 指令的測試，且金鑰和值長度最多 20 個位元組。在指令很複雜或金鑰和值較大時，處理時間可能會更長。您應該根據應用程式的使用案例設定逾時。

```
// Set DNS cache TTL
public void setJVMProperties() {
    java.security.Security.setProperty("networkaddress.cache.ttl", "10");
}

private static final Duration META_COMMAND_TIMEOUT = Duration.ofMillis(1000);
private static final Duration DEFAULT_COMMAND_TIMEOUT = Duration.ofMillis(250);
// Socket connect timeout should be lower than command timeout for Lettuce
private static final Duration CONNECT_TIMEOUT = Duration.ofMillis(100);

// Create RedisURI from the cluster configuration endpoint
clusterConfigurationEndpoint = <cluster-configuration-endpoint> // TODO: add your cluster configuration endpoint
final RedisURI redisUriCluster =
    RedisURI.Builder.redis(clusterConfigurationEndpoint)
        .withPort(6379)
        .withSsl(true)
        .build();

// Configure the client's resources                
ClientResources clientResources = DefaultClientResources.builder()
    .reconnectDelay(
        Delay.fullJitter(
            Duration.ofMillis(100),     // minimum 100 millisecond delay
            Duration.ofSeconds(10),      // maximum 10 second delay
            100, TimeUnit.MILLISECONDS)) // 100 millisecond base
    .dnsResolver(new DirContextDnsResolver())
    .build(); 

// Create a cluster client instance with the URI and resources
RedisClusterClient redisClusterClient = 
    RedisClusterClient.create(clientResources, redisUriCluster);

// Use a dynamic timeout for commands, to avoid timeouts during
// cluster management and slow operations.
class DynamicClusterTimeout extends TimeoutSource {
     private static final Set<ProtocolKeyword> META_COMMAND_TYPES = ImmutableSet.<ProtocolKeyword>builder()
          .add(CommandType.FLUSHDB)
          .add(CommandType.FLUSHALL)
          .add(CommandType.CLUSTER)
          .add(CommandType.INFO)
          .add(CommandType.KEYS)
          .build();

    private final Duration metaCommandTimeout;
    private final Duration defaultCommandTimeout;

    DynamicClusterTimeout(Duration defaultTimeout, Duration metaTimeout)
    {
        defaultCommandTimeout = defaultTimeout;
        metaCommandTimeout = metaTimeout;
    }

    @Override
    public long getTimeout(RedisCommand<?, ?, ?> command) {
        if (META_COMMAND_TYPES.contains(command.getType())) {
            return metaCommandTimeout.toMillis();
        }
        return defaultCommandTimeout.toMillis();
    }
}

TimeoutOptions timeoutOptions = TimeoutOptions.builder()
    .timeoutSource(new DynamicClusterTimeout(DEFAULT_COMMAND_TIMEOUT, META_COMMAND_TIMEOUT))
     .build();

// Configure the topology refreshment options
final ClusterTopologyRefreshOptions topologyOptions = 
    ClusterTopologyRefreshOptions.builder()
    .enableAllAdaptiveRefreshTriggers()
    .enablePeriodicRefresh()
    .dynamicRefreshSources(true)
    .build();

// Configure the socket options
final SocketOptions socketOptions = 
    SocketOptions.builder()
    .connectTimeout(CONNECT_TIMEOUT) 
    .keepAlive(true)
    .build();

// Configure the client's options
final ClusterClientOptions clusterClientOptions = 
    ClusterClientOptions.builder()
    .topologyRefreshOptions(topologyOptions)
    .socketOptions(socketOptions)
    .autoReconnect(true)
    .timeoutOptions(timeoutOptions) 
    .nodeFilter(it -> 
        ! (it.is(RedisClusterNode.NodeFlag.FAIL) 
        || it.is(RedisClusterNode.NodeFlag.EVENTUAL_FAIL) 
        || it.is(RedisClusterNode.NodeFlag.NOADDR))) 
    .validateClusterNodeMembership(false)
    .build();
    
redisClusterClient.setOptions(clusterClientOptions);

// Get a connection
final StatefulRedisClusterConnection<String, String> connection = 
    redisClusterClient.connect();

// Get cluster sync/async commands   
RedisAdvancedClusterCommands<String, String> sync = connection.sync();
RedisAdvancedClusterAsyncCommands<String, String> async = connection.async();
```

# 範例：停用叢集模式的 Lettuce 組態、啟用 TLS
<a name="BestPractices.Clients-lettuce-cmd"></a>

**注意**  
下列範例中的逾時適用於執行 SET/GET 指令的測試，且金鑰和值長度最多 20 個位元組。在指令很複雜或金鑰和值較大時，處理時間可能會更長。您應該根據應用程式的使用案例設定逾時。

```
// Set DNS cache TTL
public void setJVMProperties() {
    java.security.Security.setProperty("networkaddress.cache.ttl", "10");
}

private static final Duration META_COMMAND_TIMEOUT = Duration.ofMillis(1000);
private static final Duration DEFAULT_COMMAND_TIMEOUT = Duration.ofMillis(250);
// Socket connect timeout should be lower than command timeout for Lettuce
private static final Duration CONNECT_TIMEOUT = Duration.ofMillis(100);

// Create RedisURI from the primary/reader endpoint
clusterEndpoint = <primary/reader-endpoint> // TODO: add your node endpoint
RedisURI redisUriStandalone =
    RedisURI.Builder.redis(clusterEndpoint).withPort(6379).withSsl(true).withDatabase(0).build();

ClientResources clientResources =
    DefaultClientResources.builder()
        .dnsResolver(new DirContextDnsResolver())
        .reconnectDelay(
            Delay.fullJitter(
                Duration.ofMillis(100), // minimum 100 millisecond delay
                Duration.ofSeconds(10), // maximum 10 second delay
                100,
                TimeUnit.MILLISECONDS)) // 100 millisecond base
        .build();

// Use a dynamic timeout for commands, to avoid timeouts during
// slow operations.
class DynamicTimeout extends TimeoutSource {
     private static final Set<ProtocolKeyword> META_COMMAND_TYPES = ImmutableSet.<ProtocolKeyword>builder()
          .add(CommandType.FLUSHDB)
          .add(CommandType.FLUSHALL)
          .add(CommandType.INFO)
          .add(CommandType.KEYS)
          .build();

    private final Duration metaCommandTimeout;
    private final Duration defaultCommandTimeout;

    DynamicTimeout(Duration defaultTimeout, Duration metaTimeout)
    {
        defaultCommandTimeout = defaultTimeout;
        metaCommandTimeout = metaTimeout;
    }

    @Override
    public long getTimeout(RedisCommand<?, ?, ?> command) {
        if (META_COMMAND_TYPES.contains(command.getType())) {
            return metaCommandTimeout.toMillis();
        }
        return defaultCommandTimeout.toMillis();
    }
}

TimeoutOptions timeoutOptions = TimeoutOptions.builder()
    .timeoutSource(new DynamicTimeout(DEFAULT_COMMAND_TIMEOUT, META_COMMAND_TIMEOUT))
     .build();                      
                                    
final SocketOptions socketOptions =
    SocketOptions.builder().connectTimeout(CONNECT_TIMEOUT).keepAlive(true).build();

ClientOptions clientOptions =
    ClientOptions.builder().timeoutOptions(timeoutOptions).socketOptions(socketOptions).build();

RedisClient redisClient = RedisClient.create(clientResources, redisUriStandalone);
redisClient.setOptions(clientOptions);
```