使用直连模式连接实例

若您购买或开通了云数据库 Tair(兼容 Redis)直连模式集群,您可以将原生Redis集群架构无缝迁移到该实例中。云数据库 Tair(兼容 Redis)的直连地址支持原生Redis Cluster协议,在该模式下,客户端将直接与数据服务器进行连接,服务的响应速度非常快。

前提条件

  • 开通直连访问

  • 将客户端地址加入实例白名单

  • 使用JedisPhpRedis等支持Redis Cluster的客户端。

    说明
    • 使用不支持Redis Cluster的客户端,可能因客户端无法重定向请求到正确的分片而获取不到需要的数据。

    • 您可以在Redis官网的客户端列表里查找更多支持Redis Cluster的客户端。

  • 客户端所在的ECS与实例在同一VPC网络(相同VPC ID)。

背景信息

开启直连模式时,云数据库 Tair(兼容 Redis)会为该集群中所有数据分片的master节点分配一个虚拟IP(VIP)地址。客户端在首次向直连地址发送请求前会通过DNS服务器解析直连地址,解析结果会是集群中一个随机数据分片的VIP。获取到VIP后,客户端即可通过Redis Cluster协议操作该集群中的数据。下图展示了直连模式下集群的服务架构。

图 1. 直连模式集群的服务架构Redis集群版直连模式服务架构

注意事项

  • 由于部署架构的不同,相对标准架构来说,集群架构的实例在原生Redis命令的支持上有一定的区别(例如Lua存在使用限制等)。更多信息,请参见集群架构实例的命令限制

  • 直连模式下,如果执行变更实例配置,系统会采用Slot(槽)迁移的方式来完成,此场景下,客户端可能因访问到正在迁移的Slot而提示MOVEDTRYAGAIN等错误信息。如需确保请求的成功执行,请为客户端设计重试机制。更多信息,请参见Redis客户端重连指南

  • 直连模式支持使用SELECT命令切换DB,但部分Redis Cluster客户端(例如stackExchange.redis)不支持SELECT命令,如果使用该类客户端则只能使用DB0。

  • 直连地址仅支持通过阿里云内网访问,且同时支持VPC免密账号密码认证

redis-cli

使用集群架构直连地址连接实例。

重要

使用直连地址连接时必须添加-c参数,否则会导致连接失败。

./redis-cli -h r-bp1zxszhcgatnx****.redis.rds.aliyuncs.com -p 6379 -c

完成密码验证。

AUTH testaccount:Rp829dlwa

关于redis-cli的更多介绍请参见通过redis-cli连接实例

Jedis

本示例的Jedis版本为4.3.0,更多信息请参见Jedis

  • 使用自定义连接池(推荐)

    import redis.clients.jedis.*;
    import java.util.HashSet;
    import java.util.Set;
    
    public class DirectTest {
        private static final int DEFAULT_TIMEOUT = 2000;
        private static final int DEFAULT_REDIRECTIONS = 5;
        private static final ConnectionPoolConfig config = new ConnectionPoolConfig();
    
        public static void main(String args[]) {
            // 最大空闲连接数,由于直连模式为客户端直接连接某个数据库分片,需要保证:业务机器数 * MaxTotal < 单个数据库分片的最大连接数。
            config.setMaxTotal(30);
            // 最大空闲连接数, 根据业务需要设置。
            config.setMaxIdle(20);
            config.setMinIdle(15);
    
            // 开通直连访问时申请到的直连地址。
            String host = "r-bp1xxxxxxxxxxxx.redis.rds.aliyuncs.com";
            int port = 6379;
            // 实例的密码。
            String password = "xxxxx";
    
            Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
            jedisClusterNode.add(new HostAndPort(host, port));
            JedisCluster jc = new JedisCluster(jedisClusterNode, DEFAULT_TIMEOUT, DEFAULT_TIMEOUT, DEFAULT_REDIRECTIONS,
                password, "clientName", config);
    
            jc.set("key", "value");
            jc.get("key");
    
            jc.close();     // 当应用退出,需销毁资源时,调用此方法。此方法会断开连接、释放资源。
        }
    }
  • 使用默认连接池

    import redis.clients.jedis.ConnectionPoolConfig;
    import redis.clients.jedis.HostAndPort;
    import redis.clients.jedis.JedisCluster;
    import java.util.HashSet;
    import java.util.Set;
    
    public class DirectTest{
        private static final int DEFAULT_TIMEOUT = 2000;
        private static final int DEFAULT_REDIRECTIONS = 5;
        private static final ConnectionPoolConfig DEFAULT_CONFIG = new ConnectionPoolConfig();
    
        public static void main(String args[]){
    
            // 开通直连访问时申请到的直连地址。
            String host = "r-bp1xxxxxxxxxxxx.redis.rds.aliyuncs.com";
            int port = 6379;
            String password = "xxxx";
    
            Set<HostAndPort>  jedisClusterNode = new HashSet<HostAndPort>();
            jedisClusterNode.add(new HostAndPort(host, port));
    
            JedisCluster jc = new JedisCluster(jedisClusterNode, DEFAULT_TIMEOUT, DEFAULT_TIMEOUT,
                DEFAULT_REDIRECTIONS,password, "clientName", DEFAULT_CONFIG);
    
            jc.set("key","value");
            jc.get("key");
    
            jc.close();     // 当应用退出,需销毁资源时,调用此方法。此方法会断开连接、释放资源。
        }
    }

PhpRedis

本示例的PhpRedis版本为5.3.7,更多信息请参见PhpRedis

<?php
 // 直连地址和连接端口。
 $array = ['r-bp1xxxxxxxxxxxx.redis.rds.aliyuncs.com:6379'];
 // 连接密码。
 $pwd = "xxxx";
 
 // 使用密码连接集群。
 $obj_cluster = new RedisCluster(NULL, $array, 1.5, 1.5, true, $pwd);
 
 // 输出连接结果。
 var_dump($obj_cluster);
 
 if ($obj_cluster->set("foo", "bar") == false) {
     die($obj_cluster->getLastError());
 }
 $value = $obj_cluster->get("foo");
 echo $value;
 ?>

redis-py

本示例的Python版本为3.9、redis-py版本为4.4.1,更多信息请参见redis-py

# !/usr/bin/env python
# -*- coding: utf-8 -*-
from redis.cluster import RedisCluster
# 分别将host和port的值替换为实例的连接地址、端口号。
host = 'r-bp10noxlhcoim2****.redis.rds.aliyuncs.com'
port = 6379
# 分别将user和pwd的值替换为实例的账号和密码。
user = 'testaccount'
pwd = 'Rp829dlwa'
rc = RedisCluster(host=host, port=port, username=user, password=pwd)
# 连接建立后即可执行数据库操作,下述代码为您提供SET与GET的使用示例。
rc.set('foo', 'bar')
print(rc.get('foo'))

Spring Data Redis

本示例使用Maven方式进行构建,您也可以手动下载LettuceJedis客户端。

  1. 添加下述Maven依赖。

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.4.2</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.aliyun.tair</groupId>
        <artifactId>spring-boot-example</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>spring-boot-example</name>
        <description>Demo project for Spring Boot</description>
        <properties>
            <java.version>1.8</java.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
            </dependency>
            <dependency>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
                <version>6.3.0.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>io.netty</groupId>
                <artifactId>netty-transport-native-epoll</artifactId>
                <version>4.1.100.Final</version>
                <classifier>linux-x86_64</classifier>
            </dependency>
        </dependencies>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
  2. 在Spring Data Redis编辑器中输入下述代码,然后根据注释提示修改代码。

    本示例的Spring Data Redis版本为2.4.2。

    • Spring Data Redis With Jedis

      @Bean
           JedisConnectionFactory redisConnectionFactory() {
               List<String> clusterNodes = Arrays.asList("r-bp10noxlhcoim2****.redis.rds.aliyuncs.com:6379");
               RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(clusterNodes);
               redisClusterConfiguration.setUsername("user");
               redisClusterConfiguration.setPassword("password");
             
       
               JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
               // 最大空闲连接数,由于直连模式为客户端直接连接某个数据库分片,需要保证:业务机器数 * MaxTotal < 单个数据库分片的最大连接数。
               jedisPoolConfig.setMaxTotal(30);
               // 最大空闲连接数, 根据业务需要设置。
               jedisPoolConfig.setMaxIdle(20);
               // 关闭 testOn[Borrow|Return],防止产生额外的 PING
               jedisPoolConfig.setTestOnBorrow(false);
               jedisPoolConfig.setTestOnReturn(false);
       
               return new JedisConnectionFactory(redisClusterConfiguration, jedisPoolConfig);
           }
    • Spring Data Redis With Lettuce

           /**
           *  TCP_KEEPALIVE打开,并且配置三个参数分别为:
           *  TCP_KEEPIDLE = 30
           *  TCP_KEEPINTVL = 10
           *  TCP_KEEPCNT = 3
           */
          private static final int TCP_KEEPALIVE_IDLE = 30;
      
          /**
           * TCP_USER_TIMEOUT参数可以避免在故障宕机场景下,Lettuce持续超时的问题。
           * refer: https://github.com/lettuce-io/lettuce-core/issues/2082
           */
          private static final int TCP_USER_TIMEOUT = 30;
      
          @Bean
          public LettuceConnectionFactory redisConnectionFactory() {
              List<String> clusterNodes = Arrays.asList("r-bp10noxlhcoim2****.redis.rds.aliyuncs.com:6379");
              RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(clusterNodes);
              redisClusterConfiguration.setUsername("user");
              redisClusterConfiguration.setPassword("password");
      
              // Config TCP KeepAlive
              SocketOptions socketOptions = SocketOptions.builder()
                  .keepAlive(KeepAliveOptions.builder()
                      .enable()
                      .idle(Duration.ofSeconds(TCP_KEEPALIVE_IDLE))
                      .interval(Duration.ofSeconds(TCP_KEEPALIVE_IDLE / 3))
                      .count(3)
                      .build())
                  .tcpUserTimeout(TcpUserTimeoutOptions.builder()
                      .enable()
                      .tcpUserTimeout(Duration.ofSeconds(TCP_USER_TIMEOUT))
                      .build())
                  .build();
      
              ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                  .enablePeriodicRefresh(Duration.ofSeconds(15))
                  .dynamicRefreshSources(false)
                  .enableAllAdaptiveRefreshTriggers()
                  .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(15)).build();
      
              LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder().
                  clientOptions(ClusterClientOptions.builder()
                      .socketOptions(socketOptions)
                      .validateClusterNodeMembership(false)
                      .topologyRefreshOptions(topologyRefreshOptions).build()).build();
      
              return new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration);
          }

      ClusterTopologyRefreshOptions.builder参数说明如下:

      参数

      说明

      示例(推荐值)

      enablePeriodicRefresh(Duration refreshPeriod)

      启用定期集群拓扑刷新周期,建议为15s,若配置的值太小会产生大量的Cluster Nodes调用,影响性能。

      15s

      dynamicRefreshSources()

      是否采用使用Cluster Nodes中获取的IP用来作为集群拓扑刷新的调用节点。连接Tair(以及Redis开源版实例时,需要配置为false。由于实例通常都是使用VIP(Virtual IP address),如果在迁移可用区的情况下,VIP会全部替换,从而无法刷新路由。因此关闭这个参数,使用阿里云提供的域名来查询Cluster nodes,域名服务会自动进行负载均衡,并且指向当前的实例节点。

      false

      enableAllAdaptiveRefreshTriggers()

      开启集群拓扑刷新,包含遇到MOVED等消息就自动刷新集群一次。

      无需配置

      adaptiveRefreshTriggersTimeout(Duration timeout)

      为防止集群拓扑刷新频率过高,此参数只允许在对应时间内产生一次拓扑刷新。

      15s

      validateClusterNodeMembership()

      是否校验Cluster节点逻辑,阿里云Tair(以及Redis开源版实例无需校验。

      false

.Net

本示例的.Net版本为6.0,StackExchange.Redis版本为2.6.90。

using StackExchange.Redis;

class RedisConnSingleton {
    // 分别设置实例的连接地址、端口号和用户名、密码。
    private static ConfigurationOptions configurationOptions = ConfigurationOptions.Parse("r-bp10noxlhcoim2****.redis.rds.aliyuncs.com:6379,user=testaccount,password=Rp829dlwa,connectTimeout=2000");
    //the lock for singleton
    private static readonly object Locker = new object();
    //singleton
    private static ConnectionMultiplexer redisConn;
    //singleton
    public static ConnectionMultiplexer getRedisConn()
    {
        if (redisConn == null)
        {
            lock (Locker)
            {
                if (redisConn == null || !redisConn.IsConnected)
                {
                    redisConn = ConnectionMultiplexer.Connect(configurationOptions);
                }
            }
        }
        return redisConn;
    }
}

class Program
{
    static void Main(string[] args)
    {
        ConnectionMultiplexer cm = RedisConnSingleton.getRedisConn();
        var db = cm.GetDatabase();
        db.StringSet("key", "value");
        String ret = db.StringGet("key");
        Console.WriteLine("get key: " + ret);
    }
}

node-redis

本示例的Node.js版本为19.4.0、node-redis版本为4.5.1。

import { createCluster } from 'redis';

// 分别设置实例的端口号、连接地址、账号、密码,
// 注意,在url中配置用户和密码之后,还需要在defaults中设置全局用户和密码,
// 用于其余节点的认证,否则将出现NOAUTH的错误。

const cluster = createCluster({
    rootNodes: [{
        url: 'redis://testaccount:Rp829dlwa@r-bp10noxlhcoim2****.redis.rds.aliyuncs.com:6379'
    }],
    defaults: {
        username: 'testaccount',
        password: 'Rp829dlwa'
    }
});

cluster.on('error', (err) => console.log('Redis Cluster Error', err));

await cluster.connect();

await cluster.set('key', 'value');
const value = await cluster.get('key');
console.log('get key: %s', value);

await cluster.disconnect();

Go-redis

本示例的Go版本为1.19.7、Go-redis版本为9.5.1。

重要

请使用Go-redis v9.0及以上版本,否则在使用直连模式地址时,可能会产生不兼容报错

package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v9"
)

var ctx = context.Background()

func main() {
	rdb := redis.NewClusterClient(&redis.ClusterOptions{
		Addrs:    []string{"r-bp10noxlhcoim2****.redis.rds.aliyuncs.com:6379"},
		Username: "testaccount",
		Password: "Rp829dlwa",
	})

	err := rdb.Set(ctx, "key", "value", 0).Err()
	if err != nil {
		panic(err)
	}

	val, err := rdb.Get(ctx, "key").Result()
	if err != nil {
		panic(err)
	}
	fmt.Println("key", val)
}

Lettuce

推荐使用Lettuce 6.3.0及以上版本,Lettuce 6.3.0以下版本存在缺陷,不建议使用。本示例的Lettuce版本为6.3.0。

  1. 添加下述Maven依赖。

            <dependency>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
                <version>6.3.0.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>io.netty</groupId>
                <artifactId>netty-transport-native-epoll</artifactId>
                <version>4.1.65.Final</version>
                <classifier>linux-x86_64</classifier>
            </dependency>
  2. 添加下述代码,并根据注释提示修改代码。

    import io.lettuce.core.RedisURI;
    import io.lettuce.core.SocketOptions;
    import io.lettuce.core.cluster.ClusterClientOptions;
    import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
    import io.lettuce.core.cluster.RedisClusterClient;
    import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
    
    import java.time.Duration;
    
    public class ClusterDemo {
        /**
         *  TCP_KEEPALIVE 打开,并且配置三个参数分别为:
         *  TCP_KEEPIDLE = 30
         *  TCP_KEEPINTVL = 10
         *  TCP_KEEPCNT = 3
         */
        private static final int TCP_KEEPALIVE_IDLE = 30;
    
        /**
         * TCP_USER_TIMEOUT可以避免在故障宕机场景下Lettuce持续超时的问题。
         * refer: https://github.com/lettuce-io/lettuce-core/issues/2082
         */
        private static final int TCP_USER_TIMEOUT = 30;
    
        public static void main(String[] args) throws Exception {
            // 分别将host、port和password的值替换为实际的实例信息。
            String host = "r-bp1ln3c4kopj3l****.redis.rds.aliyuncs.com";
            int port = 6379;
            String password = "Da****3";
    
            RedisURI redisURI = RedisURI.Builder.redis(host)
                    .withPort(port)
                    .withPassword(password)
                    .build();
    
            ClusterTopologyRefreshOptions refreshOptions = ClusterTopologyRefreshOptions.builder()
                    .enablePeriodicRefresh(Duration.ofSeconds(15))
                    .dynamicRefreshSources(false)
                    .enableAllAdaptiveRefreshTriggers()
                    .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(15)).build();
    
            // Config TCP KeepAlive
            SocketOptions socketOptions = SocketOptions.builder()
                    .keepAlive(SocketOptions.KeepAliveOptions.builder()
                            .enable()
                            .idle(Duration.ofSeconds(TCP_KEEPALIVE_IDLE))
                            .interval(Duration.ofSeconds(TCP_KEEPALIVE_IDLE/3))
                            .count(3)
                            .build())
                    .tcpUserTimeout(SocketOptions.TcpUserTimeoutOptions.builder()
                            .enable()
                            .tcpUserTimeout(Duration.ofSeconds(TCP_USER_TIMEOUT))
                            .build())
                    .build();
    
            RedisClusterClient redisClient = RedisClusterClient.create(redisURI);
            redisClient.setOptions(ClusterClientOptions.builder()
                    .socketOptions(socketOptions)
                    .validateClusterNodeMembership(false)
                    .topologyRefreshOptions(refreshOptions).build());
    
            StatefulRedisClusterConnection<String, String> connection = redisClient.connect();
            connection.sync().set("key", "value");
            System.out.println(connection.sync().get("key"));
        }
    }
  3. 执行上述代码,预期会返回如下结果:

    value

    关于ClusterTopologyRefreshOptions.builder参数,请参见上方Spring Data Redis With Lettuce中的说明。

相关文档

直连模式适用于简化架构、快速上手的应用场景,而代理模式提供更高的可拓展性与高可用性,更多信息请参见Tair Proxy特性说明

常见问题

请参见常见报错