
# GeminiDB Redis客户端重试机制
Redis客户端设置重试机制可以在网络不稳定或服务器临时故障的情况下，尽量保持应用程序的高可用性和稳定性。
GeminiDB Redis可能会遇到如下一些临时性故障场景:
| **原因**     | **说明**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
|:---|:---|
| 触发高可用机制的场景 | GeminiDB Redis支持自动监测各个节点的健康状态，在节点宕机后自动触发主备倒换或集群数据分片接管。通常，触发高可用机制可能有以下场景： - 某个节点的GeminiDB进程发生重启（例如OOM/底层故障等）。  - 规格变更/节点扩缩容过程中主动剔除/加入节点。 在这些场景下，客户端可能遇到秒级连接闪断或命令超时等故障。    |
| 网络波动       | 客户端与GeminiDB服务端之间的网络链路通常较为复杂，偶发性的网络抖动、数据丢包重传是难以避免的。一旦触发网络波动，可能会导致客户端发起的请求出现超时。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| 服务端过载      | 在数据库服务出现高负载、慢查询等场景，客户端发起的请求可能无法立即响应，从而出现超时。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
   
客户端设置重试机制时，需注意以下原则：
| **原则**       | **说明**                                                                                                                                                            |
|:---|:---|
| 设置合适的重试次数和间隔 | 应根据实际业务情况，设置合适的重试次数和间隔。重试次数过多可能会延长故障恢复时间，间隔过短可能会对服务器造成额外压力。对于高负载场景，建议使每次重试的间隔时间指数增长，避免在服务器负载高时大量请求同时重试，导致雪崩效应。                                                    |
| 避免对非幂等命令执行重试 | 当客户端检测到命令执行超时后，存在服务端已经执行完命令但是在回包阶段出现异常的场景，此时执行重试可能会导致命令被重复执行。因此，通常推荐只针对幂等操作进行重试（例如**SET** 操作，多次执行得到的结果不变），而对于非幂等操作，则需业务确认是否能容忍重复数据（例如**INCR**操作，多次操作可能导致计数器额外增加）。 |
| 客户端记录日志      | 建议重试时打印客户端日志，例如连接的IP和端口，报错的命令和涉及的key等，方便问题定位和排查。                                                                                                                  |
   
以下提供部分语言SDK的代码示例，仅供参考。
- [Jedis（Java客户端）]
  对于JedisPool模式，Jedis版本在4.0.0以上支持重试，以4.0.0为例，该示例展示如何通过设置MAX_ATTEMPTS参数来设置合适的重试次数。
  ```
  package nosql.cloud.huawei.jedis;
   
  import redis.clients.jedis.DefaultJedisClientConfig;
  import redis.clients.jedis.HostAndPort;
  import redis.clients.jedis.JedisClientConfig;
  import redis.clients.jedis.UnifiedJedis;
  import redis.clients.jedis.providers.PooledConnectionProvider;
  import java.time.Duration;
   
  // UnifiedJedis API supported in Jedis >= 4.0.0
  public class UnifiedJedisDemo {
      private static final int MAX_ATTEMPTS = 5;
      private static final Duration MAX_TOTAL_RETRIES_DURATION = Duration.ofSeconds(15);
      public static void main(String[] args) {
          // Basic connection config
          JedisClientConfig jedisClientConfig = DefaultJedisClientConfig.builder().password("xxx").build();
          // Implement retry
          PooledConnectionProvider provider = new 
              PooledConnectionProvider(HostAndPort.from("{ip}:{port}"), jedisClientConfig);
          // MAX_ATTEMPTS 设置合适的重试次数
          UnifiedJedis jedis = new UnifiedJedis(provider, MAX_ATTEMPTS, MAX_TOTAL_RETRIES_DURATION);
          try {
              System.out.println("set key: " + jedis.set("key", "value"));
          } catch (Exception e) {
              // Signifies reaching either the maximum number of failures,
              MAX_ATTEMPTS, or the maximum query time, MAX_TOTAL_RETRIES_DURATION
              e.printStackTrace();
          }
      }
  }
  ```
- [Redisson（Java客户端）]
  以下是一个简单的示例，展示如何通过设置RETRY_ATTEMPTS、RETRY_INTERVAL参数来设置合适的重试次数和间隔。
  ```
  package nosql.cloud.huawei.jedis;
  import org.redisson.Redisson;
  import org.redisson.api.RBucket;
  import org.redisson.api.RedissonClient;
  import org.redisson.config.Config;
   
  public class RedissonDemo {
      private static final int TIME_OUT = 3000;
      private static final int RETRY_ATTEMPTS = 5;
      private static final int RETRY_INTERVAL = 1500;
      
      public static void main(String[] args) {
          Config config = new Config();
          config.useSingleServer()
                .setPassword("xxx")
                .setTimeout(TIME_OUT)
                .setRetryAttempts(RETRY_ATTEMPTS)
                .setRetryInterval(RETRY_INTERVAL) // 设置合适的重试次数和间隔
                .setAddress("redis://{ip}:{port}");
          RedissonClient redissonClient = Redisson.create(config);
          RBucket<String> bucket = redissonClient.getBucket("key");
          bucket.set("value");
      }
  }
  ```
- [Go-redis（Go客户端）]
  以下是一个简单的示例，展示如何通过设置MaxRetries、MinRetryBackoff、MaxRetryBackoff参数来设置合适的重试次数和间隔。
  ```
  package main
   
  import (
         "context"
         "fmt"
         "time"
         "github.com/redis/go-redis/v9"
  )
   
  var ctx = context.Background()
   
  func main() {
   
      client := redis.NewClient(&redis.Options{
          Addr:     "localhost:6379",
          Password: "", // no password set
          DB:       0,  // use default DB
          MaxRetries: 3, // set max retry times
          MinRetryBackoff: time.Duration(1) * time.Second, // set retry interval
          MaxRetryBackoff: time.Duration(2) * time.Second, // set retry interval
      })
   
      // Execute command
      err := client.Set(ctx, "key", "value", 0).Err()
      if err != nil {
          panic(err)
      }
   
      // Test
      pong, err := client.Ping(ctx).Result()
      if err != nil {
          fmt.Println("Failed:", err)
          return
      }
      fmt.Println("Success:", pong)
  }
  ```
- [Redis-py（Python客户端）]
  以下是一个简单的示例，展示如何通过设置Retry函数的参数来设置合适的重试次数。
  ```
  import redis
  from redis.retry import Retry
  from redis.exceptions import ConnectionError
  from redis.backoff import ExponentialBackoff
  from redis.client import Redis
  from redis.exceptions import (
     BusyLoadingError,
     ConnectionError,
     TimeoutError
  )
   
  # Run 3 retries with exponential backoff strategy
  retry_strategy = Retry(ExponentialBackoff(), 3)
   
  # Redis client with retries
  client = redis.Redis(
      host = 'localhost',
      port = 6379,
      retry = retry_strategy,
      # Retry on custom errors
      retry_on_error = [BusyLoadingError, ConnectionError, TimeoutError],
      # Retry on timeout
      retry_on_timeout = True
  )
   
  try:
      client.ping()
      print("Connected to Redis!")
  except ConnectionError:
      print("Failed to connect to Redis after retries.")
   
  try:
      client.set('key', 'value')
      print("Set key and value success!")
  except ConnectionError:
      print("Failed to set key after retries.")
  ```
- [Hiredis（C客户端）]
  Hiredis 是一个低级别的C语言库，不提供内置的自动重试机制，需要手动编写逻辑。
  以下是一个简单的示例，展示如何通过一个循环和延迟来实现建立连接阶段的自动重试，对于执行命令的重试，也是类似的方案。
  ```
  #include <hiredis/hiredis.h>
  #include <stdio.h>
  #include <unistd.h>
   
  redisContext* connect_with_retry(const char *hostname, int port, int max_retries, int retry_interval) {
      redisContext *c = NULL;
      int attempt = 0;
      // 通过一个循环和延迟来实现建立连接阶段的自动重试
      while (attempt < max_retries) {
          c = redisConnect(hostname, port);
          if (c != NULL && c->err == 0) {
              printf("Connection success!\n");
              return c;
          }
   
          if (c != NULL) {
              printf("Connection error: %s\n", c->errstr);
              redisFree(c);
          } else {
              printf("Connection failed\n");
          }
   
          printf("Retrying in %d seconds...\n", retry_interval);
          sleep(retry_interval);
          attempt++;
      }
   
      return NULL;
  }
   
  int main() {
      const char* hostname = "127.0.0.1";
      int port = 6379;
      int max_retries = 5;
      int retry_interval = 2;
   
      redisContext *c = connect_with_retry(hostname, port, max_retries, retry_interval);
      if (c == NULL) {
          printf("Failed to connect to Redis after %d attempts\n", max_retries);
          return 1;
      }
   
      redisFree(c);
      return 0;
  }
  ```
  
