文档首页/ 云数据库 GeminiDB/ GeminiDB Redis接口/ 开发参考/ GeminiDB Redis客户端重试指南
更新时间:2024-10-30 GMT+08:00

GeminiDB Redis客户端重试指南

Redis客户端设置重试机制可以在网络不稳定或服务器临时故障的情况下,尽量保持应用程序的高可用性和稳定性。

GeminiDB Redis可能会遇到如下一些临时性故障场景

原因

说明

触发高可用机制的场景

GeminiDB Redis支持自动监测各个节点的健康状态,在节点宕机后自动触发主备倒换或集群数据分片接管。通常,触发高可用机制可能有以下场景:

  • 某个节点的GeminiDB进程发生重启(例如OOM/底层故障等)。
  • 规格变更/节点扩缩容过程中主动剔除/加入节点。

    在这些场景下,客户端可能遇到秒级连接闪断或命令超时等故障。

网络波动

客户端与GeminiDB服务端之间的网络链路通常较为复杂,偶发性的网络抖动、数据丢包重传是难以避免的。一旦触发网络波动,可能会导致客户端发起的请求出现超时。

服务端过载

在数据库服务出现高负载、慢查询等场景,客户端发起的请求可能无法立即响应,从而出现超时。

客户端设置重试机制时,需注意以下最佳实践指南

最佳实践

说明

设置合适的重试次数和间隔

应根据实际业务情况,设置合适的重试次数和间隔。重试次数过多可能会延长故障恢复时间,间隔过短可能会对服务器造成额外压力。对于高负载场景,建议使每次重试的间隔时间指数增长,避免在服务器负载高时大量请求同时重试,导致雪崩效应。

避免对非幂等命令执行重试

当客户端检测到命令执行超时后,存在服务端已经执行完命令但是在回包阶段出现异常的场景,此时执行重试可能会导致命令被重复执行。因此,通常推荐只针对幂等操作进行重试(例如SET操作,多次执行得到的结果不变),而对于非幂等操作,则需业务确认是否能容忍重复数据(例如INCR操作,多次操作可能导致计数器额外增加)。

客户端记录日志

建议重试时打印客户端日志,例如连接的IP和端口,报错的命令和涉及的key等,方便问题定位和排查。

以下提供部分语言SDK的代码示例,仅供参考。

Jedis

对于JedisPool模式,Jedis版本在4.0.0以上支持重试,以4.0.0为例:

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

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

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

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

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;
}