上一篇文章基于redis的分布式锁实现写了基于redis实现的分布式锁。分布式环境下,不会还使用单点的redis,做到高可用和容灾,起码也是redis主从。redis的单线程工作,一台物理机只运行一个redis实例太过浪费,redis单机显然是存在单点故障的隐患。内存资源往往受限,纵向不停扩展内存并不是很实际,因此横向可伸缩扩展,需要多台主机协同提供服务,即分布式下多个Redis实例协同运行。
在之前的文章Redis Cluster深入与实践介绍过Redis Cluster的相关内容,之前特地花时间在redis官网看了redis cluster的相关文档和实现。本文是那篇文章的续集,因为笔者最近在调研redis的主从切换到redis 集群的方案,将会讲下redis集群的几种方案选型和redis cluster的实践。
redis集群的几种实现方式如下:
下面我们分别介绍下这几种方案。
Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。java redis客户端驱动jedis,支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool。
Redis Sentinel提供了主备模式下Redis监控、故障转移功能达到系统的高可用性。在主Redis宕机时,备Redis接管过来,上升为主Redis,继续提供服务。主备共同组成一个Redis节点,通过自动故障转移,保证了节点的高可用性。
客户端sharding技术其优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强。
客户端sharding的劣势也是很明显的。由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化。
客户端发送请求到一个代理组件,代理解析客户端的数据,并将请求转发至正确的节点,最后将结果回复给客户端。
该模式的特性如下:
简单的结构图如下:
主流的组件有:Twemproxy和Codis。
Twemproxy也叫nutcraker,是twtter开源的一个redis和memcache代理服务器程序。redis作为一个高效的缓存服务器,非常具有应用价值。但在用户数据量增大时,需要运行多个redis实例,此时将迫切需要一种工具统一管理多个redis实例,避免在每个客户端管理所有连接带来的不方便和不易维护,Twemproxy即为此目标而生。
Twemproxy有以下几个特点:
TwemProxy 官网介绍了如上的特性。TwemProxy的使用可以像访问redis客户端一样访问TwemProxy。然而Twitter已经很久放弃了更新TwemProxy。Twemproxy最大的痛点在于,无法平滑地扩容/缩容。Twemproxy另一个痛点是,运维不友好,甚至没有控制面板。
Codis是豌豆荚开源的redis集群方案,是一个分布式 Redis 解决方案, 对于上层的应用来说, 连接到 Codis Proxy 和连接原生的 Redis Server 没有显著区别 , 上层应用可以像使用单机的 Redis 一样使用, Codis 底层会处理请求的转发, 不停机的数据迁移等工作, 所有后边的一切事情, 对于前面的客户端来说是透明的, 可以简单的认为后边连接的是一个内存无限大的 Redis 服务。
Codis当前最新release 版本为 codis-3.2,codis-server 基于 redis-3.2.8。有一下组件组成:
至于具体的安装与使用,见官网CodisLabs,不在此涉及。
Codis的特性:
Redis Cluster是一种服务器Sharding技术,3.0版本开始正式提供。Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行。当客户端操作的key没有分配到该node上时,就像操作单一Redis实例一样,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的node,这有点儿像浏览器页面的302 redirect跳转。
Redis集群,要保证16384个槽对应的node都正常工作,如果某个node发生故障,那它负责的slots也就失效,整个集群将不能工作。为了增加集群的可访问性,官方推荐的方案是将node配置成主从结构,即一个master主节点,挂n个slave从节点。这时,如果主节点失效,Redis Cluster会根据选举算法从slave节点中选择一个上升为主节点,整个集群继续对外提供服务。
特点:
缺点是运维也很复杂,数据迁移需要人工干预,只能使用0号数据库,不支持批量操作,分布式逻辑和存储模块耦合等。
选型最后确定redis cluster。主要原因是性能高,去中心化支持扩展。运维方面的数据迁移暂时业内也没有特别成熟的方案解决,redis cluster是redis官方提供,我们期待redis官方在后面能够完美支持。
官方推荐集群至少需要六个节点,即三主三从。六个节点的配置文件基本相同,只需要修改端口号。
port 7000
cluster-enabled yes #开启集群模式
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
启动后,可以看到如下的日志。
[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1
由于没有nodes.conf存在,每个实例启动后都会给自己分配一个ID。为了在集群的环境中有一个唯一的名字,该ID将会被永久使用。每个实例都会保存其他节点使用的ID,而不是通过IP和端口。IP和端口可能会改变,但是唯一的node ID将不会改变直至该node的死亡。
我们现在已经启动了六个redis实例, 需要通过写一些有意义的配置信息到各个节点来创建集群。 redis cluster的命令行工具redis-trib,利用Ruby程序在实例上执行一些特殊的命令,很容易实现创建新的集群、检查或者reshard现有的集群等。
./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
--replicas 1
参数是将每个master带上一个slave。
@Configuration
public class JedisClusterConfig {
private static Logger logger = LoggerFactory.getLogger(JedisClusterConfig.class);
@Value("${redis.cluster.nodes}")
private String clusterNodes;
@Value("${redis.cluster.timeout}")
private int timeout;
@Value("${redis.cluster.max-redirects}")
private int redirects;
@Autowired
private JedisPoolConfig jedisPoolConfig;
@Bean
public RedisClusterConfiguration getClusterConfiguration() {
Map<String, Object> source = new HashMap();
source.put("spring.redis.cluster.nodes", clusterNodes);
logger.info("clusterNodes: {}", clusterNodes);
source.put("spring.redis.cluster.max-redirects", redirects);
return new RedisClusterConfiguration(new MapPropertySource("RedisClusterConfiguration", source));
}
@Bean
public JedisConnectionFactory getConnectionFactory() {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(getClusterConfiguration());
jedisConnectionFactory.setTimeout(timeout);
return jedisConnectionFactory;
}
@Bean
public JedisClusterConnection getJedisClusterConnection() {
return (JedisClusterConnection) getConnectionFactory().getConnection();
}
@Bean
public RedisTemplate getRedisTemplate() {
RedisTemplate clusterTemplate = new RedisTemplate();
clusterTemplate.setConnectionFactory(getConnectionFactory());
clusterTemplate.setKeySerializer(new StringRedisSerializer());
clusterTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
return clusterTemplate;
}
}
可以配置密码,cluster对密码支持不太友好,如果对集群设置密码,那么requirepass和masterauth都需要设置,否则发生主从切换时,就会遇到授权问题。
redis:
cluster:
enabled: true
timeout: 2000
max-redirects: 8
nodes: 127.0.0.1:7000,127.0.0.1:7001
主要配置了redis cluster的节点、超时时间等。
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisConfigTest {
@Autowired
RedisTemplate redisTemplate;
@Test
public void clusterTest() {
redisTemplate.opsForValue().set("foo", "bar");
System.out.println(redisTemplate.opsForValue().get("foo"));
}
}
用法很简单,注入RedisTemplate即可进行操作,RedisTemplate用法比较丰富,可以自行查阅。
本文主要讲了redis集群的选型,主要有三种:客户端分片、基于代理的分片以及路由查询。对于前两种方式,分别进行简单地介绍,最后选择redis官方提供的redis cluster方案,并进行了实践。虽然正式版的推出时间不长,目前成功实践的案例也还不多,但是总体来说,redis cluster的整个设计是比较简单的,大部分操作都可以按照单点的操作流程进行操作。笔者使用的jedis客户端支持JedisCluster也是比较好,用起来也很方便。其实还有个压测的数据,后面再补上吧。