Redis官方集群搭建&使用
阅读原文时间:2021年04月20日阅读:1

Redis官方集群设计目标

  • 高性能,并且多达1000个节点的线性可扩展性。没有代理,使用异步复制,并且在进行赋值时没有合并操作。
  • 可接受程度的写安全:当客户端与大多数master节点建立连接后,系统努力(使用最优的方式)保持来自客户端的写操作。通常有小窗口,其中确认的写操作可能会丢失。当客户端在一个小的分区中,窗口丢失写操作会更大。
  • 可用性:Redis集群支持网络分区——其中大部分主节点都可访问,并且不可访问的各master节点对应的从至少一个可访问。而且采用副本迁移,有多个从的主会提供一个从给没有从的主。

Redis集群特点

  • Redis集群不为客户端代理重定向服务,需要客户端自己重定向或缓存slot-node映射
  • Redis集群是无中心架构
  • Redis集群中存在Master-Slave结构
  • Redis集群的Re-sharding等管理需要管理员手动触发

几个约束

  • Redis版本需要>=3.0
  • Redis集群至少需要3个Master节点,考虑到基本的HA,至少需要3个Master节点+3个Slave节点
  • 创建集群使用安装Redis官方提供的Ruby实现的工具
  • 执行复杂的多键操作, 像set类型的合集或交集的命令,要求键必须是属于同一个节点
  • 事务只支持同一个节点的操作,不支持分布式事务

Redis集群示意

一些重要的资料

创建Redis集群

在《Redis集群官方教程》中有相当详细的创建集群的方法,这里做一些摘要。

手动方式创建集群

要创建集群,首先需要以集群模式运行的空redis实例。也就说,以普通模式启动的redis是不能作为集群的节点的,需要以集群模式启动的redis实例才能有集群节点的特性、支持集群的指令,成为集群的节点。
下面是最小的redis集群的配置文件:

port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

开启集群模式只需打开cluster-enabled配置项即可。每一个redis实例都包含一个配置文件,默认是nodes.conf,用于存储此节点的一些配置信息。这个配置文件由redis集群的节点自行创建和更新,不能由人手动地去修改。
一个最小的集群需要最少3个主节点。第一次测试,强烈建议你配置6个节点:3个主节点和3个从节点。
开始测试,步骤如下:先进入新的目录,以redis实例的端口为目录名,创建目录,我们将在这些目录里运行我们的实例。
类似这样:

mkdir cluster-test
cd cluster-test
mkdir 7000 7001 7002 7003 7004 7005

在7000-7005的每个目录中创建配置文件redis.conf,内容就用上面的最简配置做模板,注意修改端口号,改为跟目录一致的端口。
把你的redis服务器(用GitHub中的不稳定分支的最新的代码编译来)拷贝到cluster-test目录,然后打开6个终端页准备测试。
在每个终端启动一个redis实例,指令类似这样:

cd 7000
../redis-server ./redis.conf

在日志中我们可以看到,由于没有nodes.conf文件不存在,每个节点都给自己一个新的ID。

[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1

这个ID将一直被此节点使用,作为此节点在整个集群中的唯一标识。节点区分其他节点也是通过此ID来标识,而非IP或端口。IP可以改,端口可以改,但此ID不能改,直到这个节点离开集群。这个ID称之为节点ID(Node ID)。
现在6个实例已经运行起来了,我们需要给节点写一些有意义的配置来创建集群。redis集群的命令工具redis-trib可以让我们创建集群变得非常简单。redis-trib是一个用ruby写的脚本,用于给各节点发指令创建集群、检查集群状态或给集群重新分片等。redis-trib在Redis源码的src目录下,需要gem redis来运行redis-trib。

gem install redis

创建集群只需输入指令:

./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

这里用的命令是create,因为我们需要创建一个新的集群。选项”–replicas 1”表示每个主节点需要一个从节点。其他参数就是需要加入这个集群的redis实例的地址。
我们创建的集群有3个主节点和3个从节点。
redis-trib会给你一些配置建议,输入yes表示接受。集群会被配置并彼此连接好,意思是各节点实例被引导彼此通话并最终形成集群。最后,如果一切顺利,会看到类似下面的信息:

[OK] All 16384 slots covered

这表示,16384个哈希槽都被主节点正常服务着。

使用create-cluster脚本创建redis集群

如果你不想像上面那样,单独的手工配置各节点的方式来创建集群,还有一个更简单的系统(当然也没法了解到集群运作的一些细节)。
在utils/create-cluster目录下,有一个名为create-cluster的bash脚本。如果需要启动一个有3个主节点和3个从节点的集群,只需要输入以下指令

1. create-cluster start
2. create-cluster create

在步骤2,当redis-trib要你接受集群的布局时,输入”yes”。

现在你可以跟集群交互,第一个节点的起始端口默认是30001。当你完成后,停止集群用如下指令:

1. create-cluster stop.

请查看目录下的README,它有详细的介绍如何使用此脚本。

使用Redis集群

使用命令行方式操作集群请参照《Redis官方文档》,这里主要描述一下在Java代码中如何操作Redis集群。
这里主要介绍两种大家熟悉的工具:Jedis和Spring data redis。

Jedis

Jedis从v2.3.0开始逐步支持Redis官方集群,到目前最新的v2.8.1对集群特性已经支持的相当完善了。考虑到我们一般只使用Redis做简单的缓存,这里只介绍基础应用。
1.添加依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.8.0</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

2.创建Jedis集群客户端

public class RedisCluster {

    static JedisCluster redisCluster = null;

    public RedisCluster() {
        if(redisCluster == null) {
            Set<HostAndPort> jedisClusterNodes = new HashSet<HostAndPort>();

            jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7000));
            jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7001));
            jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7002));
            jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7003));
            jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7004));
            jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7005));

            redisCluster = new JedisCluster(jedisClusterNodes);
        }

        return;
    }

    public JedisCluster getRedisCluster() {
        return redisCluster;
    }
}

3.操作Jedis集群

public class Application {

    public static void main(String[] args) {
        RedisCluster rc = new RedisCluster();
        JedisCluster jc = rc.getRedisCluster();

        jc.set("foo", "bar");
        String value = jc.get("foo");

        System.out.printf("\r\nValue is %s", value);
    }
}

4.说明
事实上,由于JedisCluster实现了JedisCommands接口,因此一般操作和使用单点的Redis服务器并无不同。Redis集群的分片、Slot-Node Cach等实现细节,Jedis客户端已经做了很好的封装,很多情况下使用者并无法感知到集群和单点的区别。
另一方面,对于线程安全的考虑,由于JedisCluster对每一个集群节点的连接都保存了一个JedisPool,而JedisPool是线程安全,因此JedisCluster天然线程安全(注:这一点是我主观猜测,网上没有很多讨论JedisCluster线程安全方面的讨论)。

Spring data redis

Spring data redis项目在最新的开发版本1.7.0 RC1中增加了对Redis集群的支持(注意,该版本还未发布,目前还在开发状态),当前的稳定版本1.6.4不支持集群特性。
1.添加依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-redis</artifactId>
        <version>1.6.4.RELEASE</version>
    </dependency>
</dependencies>

2.创建RedisConnectionFactory
集群配置

spring:
  redis:
    cluster:
      nodes:
       - 127.0.0.1:7000
       - 127.0.0.1: 7001
       - 127.0.0.1: 7002
       - 127.0.0.1: 7003
       - 127.0.0.1: 7004
       - 127.0.0.1: 7005

3.通过RedisClusterConnection操作集群使用

@Component
@ConfigurationProperties(prefix = "spring.redis.cluster")
public class ClusterConfigurationProperties {
    /*
     * spring.redis.cluster.nodes[0] = 127.0.0.1:7379
     * spring.redis.cluster.nodes[1] = 127.0.0.1:7380
     * ...
     */
    List<String> nodes;

    /**
     * Get initial collection of known cluster nodes in format {@code host:port}.
     *
     * @return
     */
    public List<String> getNodes() {
        return nodes;
    }

    public void setNodes(List<String> nodes) {
        this.nodes = nodes;
    }
}


@Configuration
public class AppConfig {
    /**
     * Type safe representation of application.properties
     */
    @Autowired
    ClusterConfigurationProperties clusterProperties;

    public @Bean
    RedisConnectionFactory connectionFactory() {

        return new JedisConnectionFactory(new RedisClusterConfiguration(clusterProperties.getNodes()));
    }
}


@Service
public class UseSdr {
    @Autowired
    private RedisConnectionFactory connectionFactory;

    public void test() {
        RedisClusterConnection connection = connectionFactory.getClusterConnection();

        connection.set("a1".getBytes(), "111".getBytes());
        connection.set("a2".getBytes(), "222".getBytes());

        Set<byte[]> ret = connection.keys("a1".getBytes());

        for(byte[] each:ret) {
            System.out.printf("\r\n"  + new String(each));
        }
    }
}

4.通过RedisTemplate方式操作集群
RedisTemplate提供了更高层次的抽象,提供了ValueOperations、ListOperations、SetOperations等接口

@Component
@ConfigurationProperties(prefix = "spring.redis.cluster")
public class ClusterConfigurationProperties {
    /*
     * spring.redis.cluster.nodes[0] = 127.0.0.1:7379
     * spring.redis.cluster.nodes[1] = 127.0.0.1:7380
     * ...
     */
    List<String> nodes;

    /**
     * Get initial collection of known cluster nodes in format {@code host:port}.
     *
     * @return
     */
    public List<String> getNodes() {
        return nodes;
    }

    public void setNodes(List<String> nodes) {
        this.nodes = nodes;
    }
}


@Configuration
public class AppConfig {
    /**
     * Type safe representation of application.properties
     */
    @Autowired
    ClusterConfigurationProperties clusterProperties;

    @Bean
    public RedisConnectionFactory connectionFactory() {

        return new JedisConnectionFactory(new RedisClusterConfiguration(clusterProperties.getNodes()));
    }

    @Bean
    @Autowired
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate rt = new RedisTemplate();

        rt.setConnectionFactory(redisConnectionFactory);

        return rt;
    }
}


@Service
public class UseSdr {
    @Autowired
    private RedisTemplate redisTemplate;

    public void test() {
        ValueOperations<String, String> valueOp = redisTemplate.opsForValue();

        valueOp.set("hello", "world");
        String ret = valueOp.get("hello");

        System.out.printf("\r\nReturn value is " + ret);
    }
}