

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

# 用戶端的最佳實務 (Valkey 和 Redis OSS)
<a name="BestPractices.Clients.redis"></a>

了解常見案例的最佳實務，並遵循一些最熱門開放原始碼 Valkey 和 Redis OSS 用戶端程式庫 (redis-py、PHPRedis 和 Lettuce) 的程式碼範例，以及使用常用開放原始碼 Memcached 用戶端程式庫與 ElastiCache 資源互動的最佳實務。

**Topics**
+ [大量的連線 (Valkey 和 Redis OSS)](BestPractices.Clients.Redis.Connections.md)
+ [叢集用戶端探索和指數退避 (Valkey 和 Redis OSS)](BestPractices.Clients.Redis.Discovery.md)
+ [設定用戶端逾時 (Valkey 和 Redis OSS)](BestPractices.Clients.Redis.ClientTimeout.md)
+ [設定伺服器端閒置逾時 (Valkey 和 Redis OSS)](BestPractices.Clients.Redis.ServerTimeout.md)
+ [Lua 指令碼](BestPractices.Clients.Redis.LuaScripts.md)
+ [儲存大型複合項目 (Valkey 和 Redis OSS)](BestPractices.Clients.Redis.LargeItems.md)
+ [Lettuce 用戶端組態 (Valkey 和 Redis OSS)](BestPractices.Clients-lettuce.md)
+ [為雙堆疊叢集設定偏好的通訊協定 (Valkey 和 Redis OSS)](#network-type-configuring-dual-stack-redis)

# 大量的連線 (Valkey 和 Redis OSS)
<a name="BestPractices.Clients.Redis.Connections"></a>

無伺服器快取和個別 ElastiCache for Redis OSS 節點支援高達 65，000 個並行用戶端連線。然而，為了獲得最佳效能，我們建議用戶端應用程式不要以如此大量的連線持續操作。Valkey 和 Redis OSS 各自都有單一執行緒程序，以事件迴圈為基礎，其中依序處理傳入的用戶端請求。這表示，特定用戶端的回應時間會隨著連線的用戶端數量增加而變長。

您可以採取以下一組動作，以避免遇到 Valkey 或 Redis OSS 伺服器上的連線瓶頸：
+ 從讀取複本執行讀取操作。使用停用叢集模式的 ElastiCache 讀取器端點，或使用啟用叢集模式的複本進行讀取 (包括無伺服器快取)，即可達到這個目的。
+ 將寫入流量分散到多個主節點。您可以透過兩種方式進行。您可以使用多碎片 Valkey 或 Redis OSS 叢集搭配具備叢集模式功能的用戶端。您也可以利用用戶端分片寫入多個停用叢集模式的主節點。無伺服器快取中會自動進行此操作。
+ 在用戶端程式庫中使用連線集區 (可用時)。

一般而言，與一般 Valkey 或 Redis OSS 命令相比，建立 TCP 連線是一項運算成本高昂的操作。例如，重複使用現有連線時，處理 SET/GET 請求的速度加快一級。使用大小有限的用戶端連線集區可減輕連線管理的額外負荷。此外還限制了來自用戶端應用程式的並行傳入連線數。

下列 PHPRedis 的程式碼範例將示範為每個新的使用者請求建立新連線：

```
$redis = new Redis();
if ($redis->connect($HOST, $PORT) != TRUE) {
	//ERROR: connection failed
	return;
}
$redis->set($key, $value);
unset($redis);
$redis = NULL;
```

我們在連接到 Graviton2 (m6g.2xlarge) ElastiCache for Redis OSS 節點的 Amazon Elastic Compute Cloud (Amazon EC2) 執行個體的迴圈中對此程式碼進行了基準測試。 ElastiCache 我們將用戶端和伺服器放在相同可用區域內。整個操作的平均延遲為 2.82 毫秒。

當我們更新程式碼並使用持續連線和連線集區時，整個操作的平均延遲為 0.21 毫秒：

```
$redis = new Redis();
if ($redis->pconnect($HOST, $PORT) != TRUE) {
	// ERROR: connection failed
	return;
}
$redis->set($key, $value);
unset($redis);
$redis = NULL;
```

必要的 redis.ini 組態：
+ `redis.pconnect.pooling_enabled=1`
+ `redis.pconnect.connection_limit=10`

下列程式碼是 [Redis-py 連線集區](https://redis.readthedocs.io/en/stable/)的範例：

```
conn = Redis(connection_pool=redis.BlockingConnectionPool(host=HOST, max_connections=10))
conn.set(key, value)
```

下列程式碼是 [Lettuce 連線集區](https://lettuce.io/core/release/reference/#_connection_pooling)的範例：

```
RedisClient client = RedisClient.create(RedisURI.create(HOST, PORT));
GenericObjectPool<StatefulRedisConnection> pool = ConnectionPoolSupport.createGenericObjectPool(() -> client.connect(), new GenericObjectPoolConfig());
pool.setMaxTotal(10); // Configure max connections to 10
try (StatefulRedisConnection connection = pool.borrowObject()) {
	RedisCommands syncCommands = connection.sync();
	syncCommands.set(key, value);
}
```

# 叢集用戶端探索和指數退避 (Valkey 和 Redis OSS)
<a name="BestPractices.Clients.Redis.Discovery"></a>

在叢集模式中連線至 ElastiCache Valkey 或 Redis OSS 叢集時，對應的用戶端程式庫必須注意叢集。用戶端必須取得雜湊位置與叢集中相對應節點的對應，才能將請求傳送至正確的節點，並避免處理叢集重新導向的額外效能負荷。因此，用戶端必須在兩種不同的情況下，探索位置和所對應節點的完整清單：
+ 用戶端已初始化，且必須填入初始位置組態
+ 收到來自伺服器的 MOVED 重新導向，例如，在舊有主節點提供的所有位置都由複本接管時發生容錯移轉的情況，或是將位置從來源主節點移至目標主節點時進行重新分片的情況

用戶端探索通常是透過向 Valkey 或 Redis OSS 伺服器發出 CLUSTER SLOT 或 CLUSTER NODE 命令來完成。我們建議使用 CLUSTER SLOT 方法，因為它會將一組位置範圍，以及相關聯的主節點和複本節點傳回至用戶端。這不需要從用戶端進行額外剖析，而且較有效率。

根據叢集拓撲而定，CLUSTER SLOT 命令的回應大小可能因叢集大小而有所不同。具有較多節點的較大型叢集會產生較大型的回應。因此，請務必確保執行叢集拓撲探索的用戶端數量不會無限增加。例如，當用戶端應用程式啟動或中斷來自伺服器的連線，而且必須執行叢集探索時，常見的錯誤是，用戶端應用程式發出數個重新連線和探索請求，但未在重試時加上指數退避。這會使 Valkey 或 Redis OSS 伺服器長時間沒有回應，CPU 使用率為 100%。如果每個 CLUSTER SLOT 命令都必須處理叢集匯流排中的大量節點，則中斷時間會延長。我們過去在多種不同語言都觀察到因為這種行為而發生的許多用戶端中斷情形，包括 Python (redis-py-cluster) 和 Java (Lettuce 和 Redisson)。

在無伺服器快取中，許多問題會自動緩解，因為公告的叢集拓撲是靜態的，並且由兩個項目組成：寫入端點和讀取端點。使用快取端點時，叢集探索也會自動分散到多個節點上。不過，以下建議仍很實用。

為了減輕突然湧入連線和探索請求所造成的影響，以下是我們的建議做法：
+ 實作具有大小限制的用戶端連線集區，以限制來自用戶端應用程式的並行傳入連線數。
+ 當用戶端因逾時而中斷與伺服器的連線時，請使用指數退避和抖動進行重試。這樣有助於避免多個用戶端同時癱瘓伺服器。
+ 使用位於 [在 ElastiCache 中尋找連線端點](Endpoints.md) 的指南尋找叢集端點來執行叢集探索。這樣做就能將探索負載分散到叢集中的所有節點 (最多 90 個)，而不會集中在叢集中少數幾個硬式編碼的種子節點。

下列程式碼範例將示範使用 redis-py、PHPRedis 和 Lettuce 的指數退避重試邏輯。

**退避邏輯範例 1：redis-py**

redis-py 有內建的重試機制，會在失敗後立即重試一次。此機制可透過建立 [Redis OSS](https://redis.readthedocs.io/en/stable/examples/connection_examples.html#redis.Redis) 物件時提供的`retry_on_timeout`引數啟用。我們在這裡示範搭配指數退避和抖動的自動重試機制。我們已提交了提取請求，以在 [redis-py (\$11494)](https://github.com/andymccurdy/redis-py/pull/1494) 中以原生方式實作指數退避。未來可能不需要手動實作。

```
def run_with_backoff(function, retries=5):
base_backoff = 0.1 # base 100ms backoff
max_backoff = 10 # sleep for maximum 10 seconds
tries = 0
while True:
try:
  return function()
except (ConnectionError, TimeoutError):
  if tries >= retries:
	raise
  backoff = min(max_backoff, base_backoff * (pow(2, tries) + random.random()))
  print(f"sleeping for {backoff:.2f}s")
  sleep(backoff)
  tries += 1
```

您可以使用下面的程式碼來設定值：

```
client = redis.Redis(connection_pool=redis.BlockingConnectionPool(host=HOST, max_connections=10))
res = run_with_backoff(lambda: client.set("key", "value"))
print(res)
```

根據您的工作負載而定，您可能需要針對延遲敏感的工作負載，將基本退避值從 1 秒變更為數十或數百毫秒。

**退避邏輯範例 2：PHPRedis**

PHPRedis 有內建的重試機制，會重試最多 10 次 (無法設定)。您可設定兩次重試之間的延遲 (從第二次重試開始使用抖動)。如需詳細資訊，請參閱下列[範例程式碼](https://github.com/phpredis/phpredis/blob/b0b9dd78ef7c15af936144c1b17df1a9273d72ab/library.c#L335-L368)。我們已提交了提取請求，以在 [PHPredis (\$11986)](https://github.com/phpredis/phpredis/pull/1986) 中以原生方式實作從那時起即已合併且[記錄](https://github.com/phpredis/phpredis/blob/develop/README.md#retry-and-backoff)的指數退避。若使用最新版 PHPRedis，則不需手動實作，但我們已在此處針對使用舊版者納入參考。目前，下列程式碼範例會設定重試機制的延遲：

```
$timeout = 0.1; // 100 millisecond connection timeout
$retry_interval = 100; // 100 millisecond retry interval
$client = new Redis();
if($client->pconnect($HOST, $PORT, $timeout, NULL, $retry_interval) != TRUE) {
	return; // ERROR: connection failed
}
$client->set($key, $value);
```

**退避邏輯範例 3：Lettuce**

Lettuce 採用以[指數退避和抖動](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)文章中所述指數退避策略為基礎的內建重試機制。以下程式碼摘錄顯示完整的抖動方法：

```
public static void main(String[] args)
{
	ClientResources resources = null;
	RedisClient client = null;

	try {
		resources = DefaultClientResources.builder()
				.reconnectDelay(Delay.fullJitter(
			Duration.ofMillis(100),     // minimum 100 millisecond delay
			Duration.ofSeconds(5),      // maximum 5 second delay
			100, TimeUnit.MILLISECONDS) // 100 millisecond base
		).build();

		client = RedisClient.create(resources, RedisURI.create(HOST, PORT));
		client.setOptions(ClientOptions.builder()
	.socketOptions(SocketOptions.builder().connectTimeout(Duration.ofMillis(100)).build()) // 100 millisecond connection timeout
	.timeoutOptions(TimeoutOptions.builder().fixedTimeout(Duration.ofSeconds(5)).build()) // 5 second command timeout
	.build());

	    // use the connection pool from above example
	} finally {
		if (connection != null) {
			connection.close();
		}

		if (client != null){
			client.shutdown();
		}

		if (resources != null){
			resources.shutdown();
		}

	}
}
```

# 設定用戶端逾時 (Valkey 和 Redis OSS)
<a name="BestPractices.Clients.Redis.ClientTimeout"></a>

**設定用戶端逾時**

適當地設定用戶端逾時，讓伺服器有足夠的時間來處理請求並產生回應。此外還能在無法建立伺服器連線時，讓伺服器快速檢錯。某些 Valkey 或 Redis OSS 命令可能比其他命令更昂貴。例如，包含多個必須以原子方式執行之命令的 Lua 指令碼或 MULTI/EXEC 交易。一般而言，建議設定較長的用戶端逾時，以避免用戶端還未收到來自伺服器的回應就已逾時，包括下列情況：
+ 在多個索引鍵之間執行命令
+ 執行包含多個個別 Valkey 或 Redis OSS 命令的 MULTI/EXEC 交易或 Lua 指令碼
+ 讀取較大的值
+ 執行如 BLPOP 等封鎖操作

若是 BLPOP 這類封鎖操作，最佳實務是將命令逾時設定為低於通訊端逾時的數字。

下列程式碼範例將示範以 redis-py、PHPRedis 和 Lettuce 實作用戶端逾時。

**逾時組態範例 1：redis-py**

下列程式碼範例使用 redis-py：

```
# connect to Redis server with a 100 millisecond timeout
# give every Redis command a 2 second timeout
client = redis.Redis(connection_pool=redis.BlockingConnectionPool(host=HOST, max_connections=10,socket_connect_timeout=0.1,socket_timeout=2))

res = client.set("key", "value") # will timeout after 2 seconds
print(res)                       # if there is a connection error

res = client.blpop("list", timeout=1) # will timeout after 1 second
                                      # less than the 2 second socket timeout
print(res)
```

**逾時組態範例 2：PHPRedis**

下列程式碼範例使用 PHPRedis：

```
// connect to Redis server with a 100ms timeout
// give every Redis command a 2s timeout
$client = new Redis();
$timeout = 0.1; // 100 millisecond connection timeout
$retry_interval = 100; // 100 millisecond retry interval
$client = new Redis();
if($client->pconnect($HOST, $PORT, 0.1, NULL, 100, $read_timeout=2) != TRUE){
	return; // ERROR: connection failed
}
$client->set($key, $value);

$res = $client->set("key", "value"); // will timeout after 2 seconds
print "$res\n";                      // if there is a connection error

$res = $client->blpop("list", 1); // will timeout after 1 second
print "$res\n";                   // less than the 2 second socket timeout
```

**逾時組態範例 3：Lettuce**

下列程式碼範例使用 Lettuce：

```
// connect to Redis server and give every command a 2 second timeout
public static void main(String[] args)
{
	RedisClient client = null;
	StatefulRedisConnection<String, String> connection = null;
	try {
		client = RedisClient.create(RedisURI.create(HOST, PORT));
		client.setOptions(ClientOptions.builder()
	.socketOptions(SocketOptions.builder().connectTimeout(Duration.ofMillis(100)).build()) // 100 millisecond connection timeout
	.timeoutOptions(TimeoutOptions.builder().fixedTimeout(Duration.ofSeconds(2)).build()) // 2 second command timeout 
	.build());

		// use the connection pool from above example

		commands.set("key", "value"); // will timeout after 2 seconds
		commands.blpop(1, "list"); // BLPOP with 1 second timeout
	} finally {
		if (connection != null) {
			connection.close();
		}

		if (client != null){
			client.shutdown();
		}
	}
}
```

# 設定伺服器端閒置逾時 (Valkey 和 Redis OSS)
<a name="BestPractices.Clients.Redis.ServerTimeout"></a>

我們發現了下列情況：客戶的應用程式有大量連線的閒置用戶端，但未主動傳送命令。在這類情況下，大量閒置的用戶端可能造成您耗盡全部 65,000 個連線。為避免這類情況發生，請在伺服器上透過 [Valkey 和 Redis OSS 參數](ParameterGroups.Engine.md#ParameterGroups.Redis) 設定適當的逾時設定。這樣做可確保伺服器主動中斷閒置用戶端的連線，以避免連線數目增加。無伺服器快取不適用此設定。

# Lua 指令碼
<a name="BestPractices.Clients.Redis.LuaScripts"></a>

Valkey 和 Redis OSS 支援超過 200 個命令，包括執行 Lua 指令碼的命令。不過，在 Lua 指令碼方面，有幾個陷阱可能會影響 Valkey 或 Redis OSS 的記憶體和可用性。

**未參數化的 Lua 指令碼**

每個 Lua 指令碼都會在執行之前快取在 Valkey 或 Redis OSS 伺服器上。無參數 Lua 指令碼是唯一的，這可能會導致 Valkey 或 Redis OSS 伺服器存放大量 Lua 指令碼並耗用更多記憶體。為了緩解這種情況，務必確保所有 Lua 指令碼皆參數化，並且在需要時定期執行 SCRIPT FLUSH 來清除快取的 Lua 指令碼。

另請注意，必須提供金鑰。如果未提供 KEY 參數的值，指令碼將會失敗。例如，這將無法運作：

```
serverless-test-lst4hg.serverless.use1.cache.amazonaws.com:6379> eval 'return "Hello World"' 0
(error) ERR Lua scripts without any input keys are not supported.
```

這將可運作：

```
serverless-test-lst4hg.serverless.use1.cache.amazonaws.com:6379> eval 'return redis.call("get", KEYS[1])' 1 mykey-2
"myvalue-2"
```

以下範例說明如何使用參數化指令碼。首先將示範非參數化的方法來產生三個不同的已快取 Lua 指令碼，這是不建議的方法：

```
eval "return redis.call('set','key1','1')" 0
eval "return redis.call('set','key2','2')" 0
eval "return redis.call('set','key3','3')" 0
```

建議改用下列模式來建立可接受所傳遞參數的單一指令碼：

```
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 key1 1 
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 key2 2 
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 key3 3
```

**長時間執行的 Lua 指令碼**

Lua 指令碼能以原子方式執行多個命令，因此完成所需的時間可能比一般 Valkey 或 Redis OSS 命令更長。如果 Lua 指令碼只執行唯獨操作，就可以讓它中途停止執行。然而，一旦 Lua 指令碼執行寫入操作，就無法讓它終止，而必須執行到完成為止。長時間執行且正在變動的 Lua 指令碼可能會導致 Valkey 或 Redis OSS 伺服器長時間沒有回應。為了解決此問題，請避免長時間執行的 Lua 指令碼，並且在生產前環境中測試指令碼。

**具有隱匿寫入的 Lua 指令碼**

有幾種方式可以讓 Lua 指令碼繼續將新資料寫入 Valkey 或 Redis OSS，即使 Valkey 或 Redis OSS 超過 `maxmemory`：
+ 指令碼會在 Valkey 或 Redis OSS 伺服器低於 時啟動`maxmemory`，並在 內包含多個寫入操作
+ 指令碼的第一個寫入命令不會耗用記憶體 (例如 DEL)，但後續更多的寫入操作則會耗用記憶體
+ 您可以在 以外的 Valkey 或 Redis OSS 伺服器上設定適當的移出政策，以緩解此問題`noeviction`。這可讓 Redis OSS 在 Lua 指令碼之間移出項目並釋放記憶體。

# 儲存大型複合項目 (Valkey 和 Redis OSS)
<a name="BestPractices.Clients.Redis.LargeItems"></a>

在某些情況下，應用程式可能會在 Valkey 或 Redis OSS 中存放大型複合項目 （例如多 GB 雜湊資料集）。這不是建議的做法，因為它通常會導致 Valkey 或 Redis OSS 的效能問題。例如，用戶端可能會執行 HGETALL 命令來擷取整個多 GB 雜湊集合。這可能會對在用戶端輸出緩衝區中緩衝大型項目的 Valkey 或 Redis OSS 伺服器產生顯著的記憶體壓力。此外，對於叢集模式下的位置遷移，ElastiCache 不會遷移包含序列化大小大於 256 MB 之項目的位置。

為了解決大型項目的問題，以下是我們的建議做法：
+ 將大型複合項目分割成多個較小的項目。例如，將大型雜湊集合分割成個別的索引鍵-值欄位，且索引鍵名稱結構描述適當反映集合，例如，在索引鍵名稱中使用通用首碼來識別項目集合。如果您必須以原子方式存取同一集合中的多個欄位，您可以使用 MGET 命令在同一命令中擷取多個索引鍵-值。
+ 如果您評估了所有選項，但仍無法分割大型集合資料集，請嘗試使用對集合中的資料子集而非整個集合執行操作的命令。避免採用要求您在同一命令中以原子方式擷取整個多 GB 集合的使用案例。例如，在雜湊集合上使用 HGET 或 HMGET 命令，而非 HGETALL 命令。

# 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);
```

## 為雙堆疊叢集設定偏好的通訊協定 (Valkey 和 Redis OSS)
<a name="network-type-configuring-dual-stack-redis"></a>

對於啟用叢集模式的 Valkey 或 Redis OSS 叢集，您可以控制通訊協定用戶端將使用 IP 探索參數連接到叢集中的節點。IP 探索參數可以設為 IPv4 或 IPv6。

對於 Valkey 或 Redis OSS 叢集，IP 探索參數會設定[叢集插槽 ()](https://valkey.io/commands/cluster-slots/)、[叢集碎片 ()](https://valkey.io/commands/cluster-shards/) 和[叢集節點 ()](https://valkey.io/commands/cluster-nodes/) 輸出中使用的 IP 通訊協定。用戶端會使用這些命令來探索叢集拓撲。用戶端會使用這些命令中的 IP，連線至叢集的其他節點。

變更 IP 探索不會導致連線用戶端停機。但是，該變更需要一些傳播時間。若要判斷變更何時完全傳播至 Valkey 或 Redis OSS 叢集，請監控 的輸出`cluster slots`。一旦叢集槽命令傳回的所有節點都報告了具新通訊協定的 IP，就表示變更已完成傳播。

Redis-Py 範例：

```
cluster = RedisCluster(host="xxxx", port=6379)
target_type = IPv6Address # Or IPv4Address if changing to IPv4

nodes = set()
while len(nodes) == 0 or not all((type(ip_address(host)) is target_type) for host in nodes):
    nodes = set()

   # This refreshes the cluster topology and will discovery any node updates.
   # Under the hood it calls cluster slots
    cluster.nodes_manager.initialize()
    for node in cluster.get_nodes():
        nodes.add(node.host)
    self.logger.info(nodes)

    time.sleep(1)
```

Lettuce 範例：

```
RedisClusterClient clusterClient = RedisClusterClient.create(RedisURI.create("xxxx", 6379));

Class targetProtocolType = Inet6Address.class; // Or Inet4Address.class if you're switching to IPv4

Set<String> nodes;
    
do {
   // Check for any changes in the cluster topology.
   // Under the hood this calls cluster slots
    clusterClient.refreshPartitions();
    Set<String> nodes = new HashSet<>();

    for (RedisClusterNode node : clusterClient.getPartitions().getPartitions()) {
        nodes.add(node.getUri().getHost());
    }

    Thread.sleep(1000);
} while (!nodes.stream().allMatch(node -> {
            try {
                return finalTargetProtocolType.isInstance(InetAddress.getByName(node));
            } catch (UnknownHostException ignored) {}
            return false;
}));
```