

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# Lettuce 客户端配置（Valkey 和 Redis OSS）
<a name="BestPractices.Clients-lettuce"></a>

本节介绍建议的 Java 和 Lettuce 配置选项，以及它们如何应用于 ElastiCache 集群。

本节中的建议已在 Lettuce 版本 6.2.2 中进行测试。

**Topics**
+ [示例：启用集群模式和 TLS 的 Lettuce 配置](BestPractices.Clients-lettuce-cme.md)
+ [示例：禁用集群模式并启用 TLS 的 Lettuce 配置](BestPractices.Clients-lettuce-cmd.md)

**Java DNS 缓存 TTL**

Java 虚拟机 (JVM) 缓存 DNS 名称查找。当 JVM 将主机名解析为 IP 地址时，它会在指定时间段内 (称为*生存时间* (TTL)) 缓存 IP 地址。

选择 TTL 值就是在延迟和对变化的响应能力之间进行权衡。TTL 越短，DNS 解析器就能越快注意到集群 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 或更高版本。

**了解如何查看、监控和管理 SageMaker 端点**

当您使用启用集群模式的集群时，将 `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-)。有关更多信息，请参阅本主题后面的“超时”部分。

**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--)。有关更多详细信息，请参阅本主题后面的“超时”部分。

设置 [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 为 true 时）可以减小出现此问题的可能性，因为在使用新提升的主节点进行故障转移后，至少某些种子节点会有更新的拓扑视图。

**ClusterTopologyRefreshOptions：用于控制启用集群模式的客户端刷新集群拓扑的选项**

**注意**  
已禁用集群模式的集群不支持集群发现命令，并且与所有客户端的动态拓扑发现功能不兼容。  
使用 ElastiCache 禁用的集群模式与 Lettuce 的 `MasterSlaveTopologyRefresh` 不兼容。相反，如果禁用了集群模式，则可以配置 `StaticMasterReplicaTopologyProvider` 并提供集群读取和写入端点。  
有关连接到已禁用集群模式的集群的更多信息，请参阅[查找 Valkey 或 Redis OSS（已禁用集群模式）集群端点（控制台）](Endpoints.md#Endpoints.Find.Redis)。  
如果您想使用 Lettuce 的动态拓扑发现功能，则可以使用与现有集群相同的分片配置创建启用集群模式的集群。但是，对于启用集群模式的集群，我们建议至少配置 3 个分片以及至少一个副本，以支持快速失效转移。

启用 [enablePeriodicRefresh](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.Builder.html#enablePeriodicRefresh-java.time.Duration-)。这将启用定期集群拓扑更新，以便客户端按照 refreshPeriod 的间隔（默认为 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)自适应拓扑刷新：MMOVED\$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--) 为 true 时，它才会生效。启用此选项后，客户端可以关闭过时的连接并在后台创建新连接。这减少了在命令运行时期间处理失败连接的需求。

启用 [dynamicRefreshResources](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.Builder.html#dynamicRefreshSources-boolean-)。我们建议为小型集群启用 dynamicRefreshResources，为大型集群禁用 dynamicRefreshResources。dynamicRefreshResources 支持从提供的种子节点（例如，群集配置终端节点）发现集群节点。它使用所有发现的节点作为刷新集群拓扑的源。

使用动态刷新查询所有已发现的集群拓扑节点，并尝试选择最准确的集群视图。如果将其设置为 false，则仅使用初始种子节点作为拓扑发现的源，并且仅获取初始种子节点的客户端数量。禁用后，如果将集群配置终端节点解析为故障节点，则尝试刷新集群视图会失败并导致异常。之所以会发生这种情况，是因为从集群配置端点中删除故障节点的条目需要一些时间。因此，仍然会在短时间内将配置终端节点随机解析为故障节点。

但是，启用后，我们会使用从集群视图接收到的所有集群节点来查询其当前视图。因为我们从该视图中筛选掉了故障的节点，所以拓扑刷新将会成功。但是，当 dynamicRefreshSources 为 true 时，Lettuce 会查询所有节点来获取集群视图，然后比较结果。因此，对于拥有大量节点的集群来说，这可能会很昂贵。我们建议您为具有多个节点的集群关闭此功能。

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

**ClientResources**

配置 [DnsResolver](https://lettuce.io/core/release/api/io/lettuce/core/resource/DefaultClientResources.Builder.html#dnsResolver-io.lettuce.core.resource.DnsResolver-) 与 [DirContextDnsResolver](https://lettuce.io/core/release/api/io/lettuce/core/resource/DirContextDnsResolver.html)。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）使用较短的超时。

**注意**  
以下示例中配置的超时适用于运行键和值长度最多为 20 字节的 SET/GET 命令的测试。当命令较复杂或键和值较大时，处理时间可能会更长。您应该根据应用程序的用例设置超时。

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

# 示例：启用集群模式和 TLS 的 Lettuce 配置
<a name="BestPractices.Clients-lettuce-cme"></a>

**注意**  
以下示例中的超时适用于运行键和值长度最多 20 字节的 SET/GET 命令的测试。当命令较复杂或键和值较大时，处理时间可能会更长。您应该根据应用程序的用例设置超时。

```
// 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();
```

# 示例：禁用集群模式并启用 TLS 的 Lettuce 配置
<a name="BestPractices.Clients-lettuce-cmd"></a>

**注意**  
以下示例中的超时适用于运行键和值长度最多 20 字节的 SET/GET 命令的测试。当命令较复杂或键和值较大时，处理时间可能会更长。您应该根据应用程序的用例设置超时。

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