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