上两天讲述了Redis的基本搭建和基于HA的集群布署方式以及相关的策略和注意点。今天开始讲述Redis的Cluster功能,而这块目前来说网上资料不是太全,就算有1,2篇也只是单讲服务端的搭建也并未提及相关的客户端怎么和Redis Cluster间的调用问题。
我们今天要讲述的Redis Cluster是真正的Load Balance,它和Sentinel不一样,Sentinel虽然也叫集群,可是它是一种HA策略即High Available或者又通俗的被称为“灾难转移”策略。
从Redis3.x开始已经支持Load Balance功能了。
Redis Cluster 是Redis的集群实现,内置数据自动分片机制,集群内部将所有的key映射到16384个Slot中,集群中的每个Redis Instance负责其中的一部分的Slot的读写。集群客户端连接集群中任一Redis Instance即可发送命令,当Redis Instance收到自己不负责的Slot的请求时,会将负责请求Key所在Slot的Redis Instance地址返回给客户端,客户端收到后自动将原请求重新发往这个地址,对外部透明。一个Key到底属于哪个Slot由crc16(key) % 16384 决定。在Redis Cluster里对于负载均衡和HA相关都已经支持的相当完善了。
主可以没有从,但是没有从 意味着主宕机后主负责的Slot读写服务不可用。
一个主可以有多个从,主宕机时,某个从会被提升为主,具体哪个从被提升为主,协议类似于Raft,参见这里。如何检测主宕机?Redis Cluster采用quorum+心跳的机制。从节点的角度看,节点会定期给其他所有的节点发送Ping,cluster-node-timeout(可配置,秒级)时间内没有收到对方的回复,则单方面认为对端节点宕机,将该节点标为PFAIL状态。通过节点之间交换信息收集到quorum个节点都认为这个节点为PFAIL,则将该节点标记为FAIL,并且将其发送给其他所有节点,其他所有节点收到后立即认为该节点宕机。从这里可以看出,主宕机后,至少cluster-node-timeout时间内该主所负责的Slot的读写服务不可用。
Redis Cluster依赖于其官方位于Redis编译包内(我们此处使用的是redis-stable版本)/src目录下的redis-trib.rb 文件,这是一个ruby脚本,为此你必须把你的服务器环境作一个先期准备。
sudo ruby setup.rb
gem install -l redis-3.2.1.gem
使用make PREFIX=/usr/local/redis1 install这样的命令连续搭建至少6个nodes,Redis Cluster的最低要求是(3个Master,3个Slave),这在我们的“亲密接触Redis-第一天”中有详细描述怎么编译安装一个redis了,此处全部是采用redis-stable这个版本。
每个节点在第一次配置时,除去:
所有Master(如3个Master的端口号以+1方式递增:7001,7002,7003)
所有的Slaver的端口号必须且一定要符合这样的原则:slave的端口比相关的master大1000号,如7001的slave的端口号为8001。
举例来说:
3个Master为7001,7002,7003,我们的3个Slave就为8001,8002,8003
redis.conf文件内:
一切准备就绪后,把6个redis节点全部启动起来。
redis-trib.rb位于下载下来的redis-stable目录的/src目录内。
使用如下命令创建Redis集群:
./redis-trib.rb create --replicas 1 192.168.0.1:7001 192.168.0.1:7002 192.168.0.1:7003 192.168.0.1:8001 192.168.0.1:8002 192.168.0.1:8003
命令行中一定要把所有的master列出后,再列出所有的slave(依照master的顺序列slave),6个节点全部列在命令行中。
集群创建成功后会显示如下信息:
这就说明Redis集群已经创建了,以后只要使用redis-server redis.conf这样的命令把每个节点启动起来就可以了。
目前Redis和Spring的结合都是依靠Jedis和Spring Data的redisTemplate来做的,而现在spring data似乎对redis cluster的功能支持并不好(此处指的是load balance功能),为此我们自己封装了一个redis的客户端来支持redis的cluster方式,也结合spring来用(如果redistemplate已经支持redis的load balance功能后建议大家使用spring data的redis template,因为那个无论是在封装性还是性能上会更好)。
先给出代码需要的相关jar包吧。
<!-- redis start --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.8.0</version> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>1.0.2</version> </dependency> <!-- redis end -->
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath:/spring/redis.properties" /> <context:component-scan base-package="org.sky.redis"> </context:component-scan> <bean name="genericObjectPoolConfig" class="org.apache.commons.pool2.impl.GenericObjectPoolConfig"> <property name="maxWaitMillis" value="-1" /> <property name="maxTotal" value="1000" /> <property name="minIdle" value="8" /> <property name="maxIdle" value="100" /> </bean> <bean id="jedisCluster" class="com.qf.platform.redis.cluster.JedisClusterFactory"> <property name="addressConfig"> <value>classpath:/spring/redis.properties</value> </property> <property name="addressKeyPrefix" value="clusternode" /> <!-- 属性文件里 key的前缀 --> <property name="timeout" value="300000" /> <property name="maxRedirections" value="6" /> <property name="genericObjectPoolConfig" ref="genericObjectPoolConfig" /> </bean> <bean id="customExceptionHandler" class="sample.MyHandlerExceptionResolver" /> </beans>
/** * @Title: [JedisClusterFactory.java] * @Package: [com.qf.platform.redis.cluster] * @author: [MingkaiYuan] * @CreateDate: [2016年3月9日 下午3:42:26] * @UpdateUser: [MingkaiYuan] * @UpdateDate: [2016年3月9日 下午3:42:26] * @UpdateRemark: [说明本次修改内容] * @Description: [TODO(用一句话描述该文件做什么)] * @version: [V1.0] */ package com.qf.platform.redis.cluster; import java.util.HashSet; import java.util.Properties; import java.util.Set; import java.util.regex.Pattern; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisCluster; /** * @ClassName: JedisClusterFactory * @author: [MingkaiYuan] * @CreateDate: [2016年3月9日 下午3:42:26] * @UpdateUser: [MingkaiYuan] * @UpdateDate: [2016年3月9日 下午3:42:26] * @UpdateRemark: [说明本次修改内容] * @Description: [TODO(用一句话描述该文件做什么)] * @version: [V1.0] */ public class JedisClusterFactory implements FactoryBean<JedisCluster>, InitializingBean { private Resource addressConfig; private String addressKeyPrefix; public void setAddressKeyPrefix(String addressKeyPrefix) { this.addressKeyPrefix = addressKeyPrefix; } private JedisCluster jedisCluster; private Integer timeout; private Integer maxRedirections; private GenericObjectPoolConfig genericObjectPoolConfig; private Pattern p = Pattern.compile("^.+[:]\\d{1,5}\\s*$"); @Override public JedisCluster getObject() throws Exception { return jedisCluster; } @Override public Class<? extends JedisCluster> getObjectType() { return (this.jedisCluster != null ? this.jedisCluster.getClass() : JedisCluster.class); } @Override public boolean isSingleton() { return true; } private Set<HostAndPort> parseHostAndPort() throws Exception { try { Properties prop = new Properties(); prop.load(this.addressConfig.getInputStream()); Set<HostAndPort> haps = new HashSet<HostAndPort>(); for (Object key : prop.keySet()) { if (!((String) key).startsWith(addressKeyPrefix)) { continue; } String val = (String) prop.get(key); boolean isIpPort = p.matcher(val).matches(); if (!isIpPort) { throw new IllegalArgumentException("ip 或 port 不合法"); } String[] ipAndPort = val.split(":"); System.out.println(String.valueOf(val)); HostAndPort hap = new HostAndPort(ipAndPort[0], Integer.parseInt(ipAndPort[1])); haps.add(hap); } return haps; } catch (IllegalArgumentException ex) { throw ex; } catch (Exception ex) { throw new Exception("解析 jedis 配置文件失败", ex); } } @Override public void afterPropertiesSet() throws Exception { Set<HostAndPort> haps = this.parseHostAndPort(); jedisCluster = new JedisCluster(haps, timeout, maxRedirections, genericObjectPoolConfig); } public void setAddressConfig(Resource addressConfig) { this.addressConfig = addressConfig; } public void setTimeout(int timeout) { this.timeout = timeout; } public void setMaxRedirections(int maxRedirections) { this.maxRedirections = maxRedirections; } public void setGenericObjectPoolConfig(GenericObjectPoolConfig genericObjectPoolConfig) { this.genericObjectPoolConfig = genericObjectPoolConfig; } }
<property name="addressConfig"> <value>classpath:/spring/redis.properties</value> </property> <property name="addressKeyPrefix" value="clusternode" />
# Redis settings cach.host.ip=192.168.0.101 cach.host.port=6380 redis.host.ip=192.168.0.101 redis.host.port=6379 redis.maxTotal=1000 redis.maxIdle=100 redis.maxWait=2000 redis.testOnBorrow=false redis.testOnReturn=true redis.sentinel.addr=172.30.32.127:26379 clusternode1=192.168.0.101:7001 clusternode2=192.168.0.101:7002 clusternode3=192.168.0.101:7003 clusternode4=192.168.0.101:8001 clusternode5=192.168.0.101:8002 clusternode6=192.168.0.101:8003
@Autowired JedisCluster jedisCluster; ...... jedisCluster.set(key, value); ......
使用redis-cli连接cluster中任意一个节点如:
可以看到7003节点内无 key=1的元素,但是集群内是有key=1的元素的,于是:
reds-cli会动态从集群中查到含有key=1的节点,把客户端连接重定向至该节点并调用该key所绑的value
新节点没有包含任何数据, 因为它没有包含任何slot。新加入的加点是一个主节点, 当集群需 要将某个从节点升级为新的主节点时, 这个新节点不会被选中,同时新的主节点因为没有包含任何slot,不参加选举和failover。
在线添加slave 时,需要bgsave整个master数据,并传递到slave,再由 slave加载rdb文件到内存,rdb生成和传输的过程中消耗Master大量内存和网络IO,以此不建议单实例内存过大,线上小心操作。
截止到今天,Redis所有搭建和使用讲解完毕,还有更深入的功能由其是性能优化、配置还需要大家在平时工作中不断的实践,我在文中也只能涉及到50%左右的内容,更多还是要靠读者自己去潜心发掘。
总得来说Redis是一个相当优秀的NOSQL,大都网站都在使用它作为主选NOSQL或者是缓存。
用,人人都会!
但用的精,这是需要付出一定的专研精神的。
谢谢!