答:Redis集群中内置了16384个哈希槽,当需要在Redis集群中放置一个key-value时,redis先对key使用crc16算法算出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,redis会根据节点数量大致均等的将哈希槽映射到不同的节点。
Redis Cluster将所有数据划分为16384个槽,每个节点负责其中一部分槽位。槽位的信息存储于每个节点中,当Redis Cluster的客户端来连接集群时,它也会得到一份集群的槽位配置信息。这样当客户端要查找某个key时,可以直接定位到目标节点。
槽位定位算法
Cluster默认会对key值使用crc32算法进行hash得到一个整数值,然后用这个整数值对16384进行取模来得到具体槽位。
Cluster还允许用户强制某个key挂在特定槽位上,通过在key字符串里面嵌入tag标记,这就可以强制key所挂在的槽位等于tag所在的槽位。
槽如何进行分配呢?
答:这是可以由人为设置的。一般情况下会平均分配。为了保证Redis集群的性能,要看Redis集群中有几个结点,还要看每个结点的性能怎么样。假如有3个结点,每个结点的性能都是完全一样的,那么我们就可以把这16384个槽平均分到3个结点上。
0~5000个槽分到第一个结点上
5001~10000个槽分到第二个结点上
10001~16383个槽分到第三个结点上(为了好计算,这样划分)
答:Redis集群中有一个投票机制。大家都知道,在选举的时候,是少数服从多数的原则,要判断Redis集群中的某个结点是否挂掉了,需要我们集群中超过半数的节点进行投票,半数以上的节点认为它挂了,它就挂了。假如集群中有5个结点,有三个认为某个结点已经挂了,那么集群就认为这个结点真挂了。这个时候就要看有没有备份结点,如果没有备份结点顶上来,那么集群就会宕机。如果有备份结点,备份结点顶上来,继续维持整个集群的工作,然后管理人员就需要赶快把那个挂掉的节点修理好。那么,集群中最少有几个结点呢?3个!3个结点就可以搭建起一个Redis集群。而在实际开发中,为了保证集群的高可用,还要保证每个结点都有一个备份机,所以,实际中,最小的集群会搭建6个结点。
答:至少有3个结点,至多有16384个结点。
至少有3个结点是由RedisCluster的投票机制决定的。投票机制指的是超过半数的节点投票认为某个节点挂掉就挂掉了!
至多有16384个结点是因为RedisCluster总共就有16384个槽。
答:是通过PING-PONG机制(一种特殊的二进制协议)来实现各个节点之间的通信。
(面试题)你知道哪些分布式缓存,如果要你设计一个分布式缓存,你会怎么去设计?
答:主要有Memcached和Redis。我使用Redis来做分布式缓存。
刚开始对Redis的操作都是单机版,虽然Redis的速度很快,但是在特别高的并发下,Redis也有性能瓶颈。Redis中的数据都放在内存里面,内存能有多大呢?64G,已经很大了,64G都放满了呢?还能放吗?可以,内存放满了会放在硬盘中的虚拟内存中,一旦用到虚拟内存了,性能就很低了,所以我们尽可能的不要超出内存的容量。如果存不下了,但是数据还是很多,还需要往缓存中放,那怎么办呢?通过搭Redis集群来扩展内存空间。官方给出的Redis集群名称为Redis-Cluster。
Redis-Cluster架构图如下:
集群一般都会有一个入口,有一个集群管理工具,但Redis集群没有入口,即没有代理层,集群中的节点都是相互连接的,通过PING-PONG机制来实现各个节点之间的通信,以及判断各个节点的状态,客户端想要连接集群,只需要连接到集群中的任意一个节点即可。
集群中有那么多个结点,结点中保存的数据一样吗?不一样。如果是一样的,那叫主备。既然是集群,就应该是可以扩容的,如果存储空间不足了,可以加结点,加一个服务器进入,存储空间就会变大。所有结点的内存容量加起来才是整个集群内存的总容量,如果每个结点存储的数据都一样,那总容量就只是一个Redis的内存容量了。
Redis集群中,每个结点保存的数据是不一样的。如果不一样,那么当一个结点挂了,那整个集群就不完整了,不完整了就不能用了,所以,要想保证Redis集群的高可用(长时间可使用,而不会宕机),每一个节点都需要加一个备份机,如果这个结点挂了,必须要有备份结点顶上来,来保证集群可以继续提供服务。
Redis集群中有一个投票:容错机制,我们前面说集群中一般都会有一个集群管理工具,但在Redis集群中并没有,那么,我们怎么才能知道集群中哪一个结点挂了呢?Redis集群中有一个投票机制。大家都知道,在选举的时候,是少数服从多数的原则,要判断Redis集群中的某个结点是否挂掉了,需要我们集群中超过半数的节点进行投票,半数以上的节点认为它挂了,它就挂了。假如集群中有5个结点,有三个认为某个结点已经挂了,那么集群就认为这个结点真挂了。这个时候就要看有没有备份结点,如果没有备份结点顶上来,那么集群就会宕机。如果有备份结点,备份结点顶上来,继续维持整个集群的工作,然后管理人员就需要赶快把那个挂掉的节点修理好。那么,集群中最少有几个结点呢?3个!3个结点就可以搭建起一个Redis集群。而在实际开发中,为了保证集群的高可用,还要保证每个结点都有一个备份机,所以,实际中,最小的集群会搭建6个结点。那如果面试官问你:
一个Redis集群至少有几个结点?为什么?
答:有3个结点。这是由Redis集群中的投票:容错机制决定的。Redis集群中,要判断一个结点是否挂掉了,是通过集群中的其他结点投票决定的。当集群中有一半以上的节点都投票认为该节点挂掉了,Redis集群才会认为该节点挂掉了,这就导致一个Redis集群中最少要有3个结点。
当我们使用单机版的Redis做缓存时,操作很简单,当单机版的Redis变成Redis集群后,操作是不是就会变得异常复杂?
答:并不会变多复杂。只需要连接上Redis集群中的任意一个结点,就能连接上整个Redis集群。只是在使用Jedis连接单机版Redis和连接Redis集群时,会有所不同,但对Redis的操作都是一样的。
Redis集群中,每个结点保存的数据是不一样的,那就会有一个问题,如何把数据分散到不同的节点进行存储呢?为解决这个问题,Redis集群中引入了一个概念,叫slot(槽,哈希槽)。Redis集群中一共有16384个槽(0~16383),这是固定的。这些槽有什么作用呢?
当要在Redis集群中放置一个key-value对时,先要对key使用crc16算法得出一个数,然后再用这个数对16384求余数,肯定会得到一个0~16383之间的数,这样每一个key值都会对应一个0~16383之间的哈希槽,然后将key-value键值对放在这个槽对应的Redis结点上就可以了。
槽如何进行分配呢?
答:这是可以由人为设置的。一般情况下会平均分配。为了保证Redis集群的性能,要看Redis集群中有几个结点,还要看每个结点的性能怎么样。假如有3个结点,每个结点的性能都是完全一样的,那么我们就可以把这16384个槽平均分到3个结点上。
0~5000个槽分到第一个结点上
5001~10000个槽分到第二个结点上
10001~16383个槽分到第三个结点上(为了好计算,这样划分)
Redis集群中最多有多少个结点?为什么?
答:最多有16384个结点(这里不考虑备份机的问题)。这是由Redis集群中哈希槽的数量决定的,极限情况下,每个结点有一个哈希槽。
架构细节:
(1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
(2)节点的fail是通过集群中超过半数的节点检测失效时才生效.
(3)客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
(4)redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster负责维护node<->slot<->value
Redis集群中内置了16384个哈希槽,当需要在Redis集群中放置一个key-value时,redis先对key使用crc16算法算出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,redis会根据节点数量大致均等的将哈希槽映射到不同的节点。
单击版Redis的安装与启动
前端启动模式:
直接运行bin/redis-server将以前端模式启动,前端模式启动的缺点是启动完成后,不能再进行其他操作,如果要操作必须使用ctrl+c,同时redis-server程序结束,不推荐使用此方法。
后端模式:
·修改redis.conf配置文件,daemonize yes以后端模式启动
·启动时,指定配置文件 cd redis-server ./redis.conf
·Redis默认端口6379,通过当前服务进行查看
Redis集群的搭建
Redis集群中至少应该有三个节点。要保证集群的高可用,需要每个节点有一个备份机。
Redis集群至少需要6台服务器。
搭建伪分布式。可以使用一台虚拟机运行6个redis实例。需要修改redis的端口号7001-7006
1、使用ruby脚本搭建集群。需要ruby的运行环境。
安装ruby
yum install ruby
yum install rubygems
[root@localhost ~]# gem install redis-3.0.0.gem
Successfully installed redis-3.0.0
1 gem installed
Installing ri documentation for redis-3.0.0...
Installing RDoc documentation for redis-3.0.0...
[root@localhost ~]#
----------------------------------------------------------------------
[root@localhost ~]# cd redis-3.0.0/src
[root@localhost src]# ll *.rb
-rwxrwxr-x. 1 root root 48141 Apr 1 2015 redis-trib.rb
需要6台redis服务器。搭建伪分布式。
需要6个redis实例。
需要运行在不同的端口7001-7006
第一步:创建6个redis实例,每个实例运行在不同的端口。需要修改redis.conf配置文件。配置文件中还需要把cluster-enabled yes前的注释去掉。
第二步:启动每个redis实例。(使用p处理统一启动)
第三步:使用ruby脚本搭建集群。
./redis-trib.rb create --replicas 1 192.168.25.153:7001 192.168.25.153:7002 192.168.25.153:7003 192.168.25.153:7004 192.168.25.153:7005 192.168.25.153:7006 |
创建关闭集群的脚本:
[root@localhost redis-cluster]# vim shutdow-all.sh
redis01/redis-cli -p 7001 shutdown
redis01/redis-cli -p 7002 shutdown
redis01/redis-cli -p 7003 shutdown
redis01/redis-cli -p 7004 shutdown
redis01/redis-cli -p 7005 shutdown
redis01/redis-cli -p 7006 shutdown
[root@localhost redis-cluster]# chmod u+x shutdow-all.sh
[root@localhost redis-cluster]# ./redis-trib.rb create --replicas 1 192.168.25.153:7001 192.168.25.153:7002 192.168.25.153:7003 192.168.25.153:7004 192.168.25.153:7005 192.168.25.153:7006 >>> Creating cluster Connecting to node 192.168.25.153:7001: OK Connecting to node 192.168.25.153:7002: OK Connecting to node 192.168.25.153:7003: OK Connecting to node 192.168.25.153:7004: OK Connecting to node 192.168.25.153:7005: OK Connecting to node 192.168.25.153:7006: OK >>> Performing hash slots allocation on 6 nodes... Using 3 masters: 192.168.25.153:7001 192.168.25.153:7002 192.168.25.153:7003 Adding replica 192.168.25.153:7004 to 192.168.25.153:7001 Adding replica 192.168.25.153:7005 to 192.168.25.153:7002 Adding replica 192.168.25.153:7006 to 192.168.25.153:7003 M: 2e48ae301e9c32b04a7d4d92e15e98e78de8c1f3 192.168.25.153:7001 slots:0-5460 (5461 slots) master M: 8cd93a9a943b4ef851af6a03edd699a6061ace01 192.168.25.153:7002 slots:5461-10922 (5462 slots) master M: 2935007902d83f20b1253d7f43dae32aab9744e6 192.168.25.153:7003 slots:10923-16383 (5461 slots) master S: 74f9d9706f848471583929fc8bbde3c8e99e211b 192.168.25.153:7004 replicates 2e48ae301e9c32b04a7d4d92e15e98e78de8c1f3 S: 42cc9e25ebb19dda92591364c1df4b3a518b795b 192.168.25.153:7005 replicates 8cd93a9a943b4ef851af6a03edd699a6061ace01 S: 8b1b11d509d29659c2831e7a9f6469c060dfcd39 192.168.25.153:7006 replicates 2935007902d83f20b1253d7f43dae32aab9744e6 Can I set the above configuration? (type 'yes' to accept): yes >>> Nodes configuration updated >>> Assign a different config epoch to each node >>> Sending CLUSTER MEET messages to join the cluster Waiting for the cluster to join..... >>> Performing Cluster Check (using node 192.168.25.153:7001) M: 2e48ae301e9c32b04a7d4d92e15e98e78de8c1f3 192.168.25.153:7001 slots:0-5460 (5461 slots) master M: 8cd93a9a943b4ef851af6a03edd699a6061ace01 192.168.25.153:7002 slots:5461-10922 (5462 slots) master M: 2935007902d83f20b1253d7f43dae32aab9744e6 192.168.25.153:7003 slots:10923-16383 (5461 slots) master M: 74f9d9706f848471583929fc8bbde3c8e99e211b 192.168.25.153:7004 slots: (0 slots) master replicates 2e48ae301e9c32b04a7d4d92e15e98e78de8c1f3 M: 42cc9e25ebb19dda92591364c1df4b3a518b795b 192.168.25.153:7005 slots: (0 slots) master replicates 8cd93a9a943b4ef851af6a03edd699a6061ace01 M: 8b1b11d509d29659c2831e7a9f6469c060dfcd39 192.168.25.153:7006 slots: (0 slots) master replicates 2935007902d83f20b1253d7f43dae32aab9744e6 [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. [root@localhost redis-cluster]# |
Redis-cli连接集群。
[root@localhost redis-cluster]# redis01/redis-cli -p 7002 -c
-c:代表连接的是redis集群
需要把jedis依赖的jar包添加到工程中。Maven工程中需要把jedis的坐标添加到依赖。
推荐添加到服务层。E3-content-Service工程中。
第一步:创建一个Jedis对象。需要指定服务端的ip及端口。
第二步:使用Jedis对象操作数据库,每个redis命令对应一个方法。
第三步:打印结果。
第四步:关闭Jedis
@Test public void testJedis() throws Exception { // 第一步:创建一个Jedis对象。需要指定服务端的ip及端口。 Jedis jedis = new Jedis("192.168.25.153", 6379); // 第二步:使用Jedis对象操作数据库,每个redis命令对应一个方法。 String result = jedis.get("hello"); // 第三步:打印结果。 System.out.println(result); // 第四步:关闭Jedis jedis.close(); } |
第一步:创建一个JedisPool对象。需要指定服务端的ip及端口。
第二步:从JedisPool中获得Jedis对象。
第三步:使用Jedis操作redis服务器。
第四步:操作完毕后关闭jedis对象,连接池回收资源。
第五步:关闭JedisPool对象。
@Test public void testJedisPool() throws Exception { // 第一步:创建一个JedisPool对象。需要指定服务端的ip及端口。 JedisPool jedisPool = new JedisPool("192.168.25.153", 6379); // 第二步:从JedisPool中获得Jedis对象。 Jedis jedis = jedisPool.getResource(); // 第三步:使用Jedis操作redis服务器。 jedis.set("jedis", "test"); String result = jedis.get("jedis"); System.out.println(result); // 第四步:操作完毕后关闭jedis对象,连接池回收资源。 jedis.close(); // 第五步:关闭JedisPool对象。 jedisPool.close(); } |
第一步:使用JedisCluster对象。需要一个Set
第二步:直接使用JedisCluster对象操作redis。在系统中单例存在。
第三步:打印结果
第四步:系统关闭前,关闭JedisCluster对象。
@Test public void testJedisCluster() throws Exception { // 第一步:使用JedisCluster对象。需要一个Set Set nodes.add(new HostAndPort("192.168.25.153", 7001)); nodes.add(new HostAndPort("192.168.25.153", 7002)); nodes.add(new HostAndPort("192.168.25.153", 7003)); nodes.add(new HostAndPort("192.168.25.153", 7004)); nodes.add(new HostAndPort("192.168.25.153", 7005)); nodes.add(new HostAndPort("192.168.25.153", 7006)); JedisCluster jedisCluster = new JedisCluster(nodes); // 第二步:直接使用JedisCluster对象操作redis。在系统中单例存在。 jedisCluster.set("hello", "100"); String result = jedisCluster.get("hello"); // 第三步:打印结果 System.out.println(result); // 第四步:系统关闭前,关闭JedisCluster对象。 jedisCluster.close(); } |
常用的操作redis的方法提取出一个接口,分别对应单机版和集群版创建两个实现类。
public interface JedisClient {
String set(String key, String value); String get(String key); Boolean exists(String key); Long expire(String key, int seconds); Long ttl(String key); Long incr(String key); Long hset(String key, String field, String value); String hget(String key, String field); Long hdel(String key, String... field); } |
public class JedisClientPool implements JedisClient {
@Autowired private JedisPool jedisPool;
@Override public String set(String key, String value) { Jedis jedis = jedisPool.getResource(); String result = jedis.set(key, value); jedis.close(); return result; }
@Override public String get(String key) { Jedis jedis = jedisPool.getResource(); String result = jedis.get(key); jedis.close(); return result; }
@Override public Boolean exists(String key) { Jedis jedis = jedisPool.getResource(); Boolean result = jedis.exists(key); jedis.close(); return result; }
@Override public Long expire(String key, int seconds) { Jedis jedis = jedisPool.getResource(); Long result = jedis.expire(key, seconds); jedis.close(); return result; }
@Override public Long ttl(String key) { Jedis jedis = jedisPool.getResource(); Long result = jedis.ttl(key); jedis.close(); return result; }
@Override public Long incr(String key) { Jedis jedis = jedisPool.getResource(); Long result = jedis.incr(key); jedis.close(); return result; }
@Override public Long hset(String key, String field, String value) { Jedis jedis = jedisPool.getResource(); Long result = jedis.hset(key, field, value); jedis.close(); return result; }
@Override public String hget(String key, String field) { Jedis jedis = jedisPool.getResource(); String result = jedis.hget(key, field); jedis.close(); return result; }
@Override public Long hdel(String key, String... field) { Jedis jedis = jedisPool.getResource(); Long result = jedis.hdel(key, field); jedis.close(); return result; } } |
配置:applicationContext-redis.xml
xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context4.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop4.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx4.2.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util4.2.xsd">
<bean id="jedisPool" class="redis.clients.jedis.JedisPool"> <constructor-arg name="host" value="192.168.25.153">constructor-arg> <constructor-arg name="port" value="6379">constructor-arg> bean> <bean id="jedisClientPool" class="cn.e3mall.jedis.JedisClientPool"/>
</beans> |
package cn.e3mall.jedis;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.JedisCluster;
public class JedisClientCluster implements JedisClient {
@Autowired private JedisCluster jedisCluster;
@Override public String set(String key, String value) { return jedisCluster.set(key, value); }
@Override public String get(String key) { return jedisCluster.get(key); }
@Override public Boolean exists(String key) { return jedisCluster.exists(key); }
@Override public Long expire(String key, int seconds) { return jedisCluster.expire(key, seconds); }
@Override public Long ttl(String key) { return jedisCluster.ttl(key); }
@Override public Long incr(String key) { return jedisCluster.incr(key); }
@Override public Long hset(String key, String field, String value) { return jedisCluster.hset(key, field, value); }
@Override public String hget(String key, String field) { return jedisCluster.hget(key, field); }
@Override public Long hdel(String key, String... field) { return jedisCluster.hdel(key, field); }
}
|
Spring的配置:
<bean id="jedisCluster" class="redis.clients.jedis.JedisCluster"> <constructor-arg> <set> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg name="host" value="192.168.25.153">constructor-arg> <constructor-arg name="port" value="7001">constructor-arg> bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg name="host" value="192.168.25.153">constructor-arg> <constructor-arg name="port" value="7002">constructor-arg> bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg name="host" value="192.168.25.153">constructor-arg> <constructor-arg name="port" value="7003">constructor-arg> bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg name="host" value="192.168.25.153">constructor-arg> <constructor-arg name="port" value="7004">constructor-arg> bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg name="host" value="192.168.25.153">constructor-arg> <constructor-arg name="port" value="7005">constructor-arg> bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg name="host" value="192.168.25.153">constructor-arg> <constructor-arg name="port" value="7006">constructor-arg> bean> set> constructor-arg> bean> <bean id="jedisClientCluster" class="cn.e3mall.jedis.JedisClientCluster"/> |
注意:单机版和集群版不能共存,使用单机版时注释集群版的配置。使用集群版,把单机版注释
@Test public void testJedisClient() throws Exception { //初始化Spring容器 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring/applicationContext-redis.xml"); //从容器中获得JedisClient对象 JedisClient jedisClient = applicationContext.getBean(JedisClient.class); jedisClient.set("first", "100"); String result = jedisClient.get("first"); System.out.println(result);
} |