分布式缓存

分布式缓存——RedisCluster

 

  1. RedisCluster是如何确定数据要存在集群中的哪个节点的?

答: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个槽分到第三个结点上(为了好计算,这样划分)

 

  1. 如何判断Redis Cluster集群中的节点是否挂掉?

答:Redis集群中有一个投票机制。大家都知道,在选举的时候,是少数服从多数的原则,要判断Redis集群中的某个结点是否挂掉了,需要我们集群中超过半数的节点进行投票,半数以上的节点认为它挂了,它就挂了。假如集群中有5个结点,有三个认为某个结点已经挂了,那么集群就认为这个结点真挂了。这个时候就要看有没有备份结点,如果没有备份结点顶上来,那么集群就会宕机。如果有备份结点,备份结点顶上来,继续维持整个集群的工作,然后管理人员就需要赶快把那个挂掉的节点修理好。那么,集群中最少有几个结点呢?3个!3个结点就可以搭建起一个Redis集群。而在实际开发中,为了保证集群的高可用,还要保证每个结点都有一个备份机,所以,实际中,最小的集群会搭建6个结点。

  1. 一个RedisCluster集群至少有几个结点?至多有几个节点?为什么?

答:至少有3个结点,至多有16384个结点。

至少有3个结点是由RedisCluster的投票机制决定的。投票机制指的是超过半数的节点投票认为某个节点挂掉就挂掉了!

至多有16384个结点是因为RedisCluster总共就有16384个槽。

  1. RedisCluster集群中没有统一管理器,那么各个节点之间是如何进行通信的呢?

答:是通过PING-PONG机制(一种特殊的二进制协议)来实现各个节点之间的通信。

 

(面试题)你知道哪些分布式缓存,如果要你设计一个分布式缓存,你会怎么去设计?

答:主要有Memcached和Redis。我使用Redis来做分布式缓存。

刚开始对Redis的操作都是单机版,虽然Redis的速度很快,但是在特别高的并发下,Redis也有性能瓶颈。Redis中的数据都放在内存里面,内存能有多大呢?64G,已经很大了,64G都放满了呢?还能放吗?可以,内存放满了会放在硬盘中的虚拟内存中,一旦用到虚拟内存了,性能就很低了,所以我们尽可能的不要超出内存的容量。如果存不下了,但是数据还是很多,还需要往缓存中放,那怎么办呢?通过搭Redis集群来扩展内存空间。官方给出的Redis集群名称为Redis-Cluster。

Redis-Cluster架构图如下:

分布式缓存_第1张图片

集群一般都会有一个入口,有一个集群管理工具,但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的安装与启动

  1. redis是C语言开发,安装redis需要先将官网下载的源码进行编译,编译依赖gcc环境。如果没有gcc环境,需要安装gcc。
  2. 将Windows下下载的压缩文件上传到Linux下。通过SSH进行上传。
  3. 解压文件。
  4. 编译redis。进入解压文件夹,cd redis-3.0.0,执行make。
  5. 安装。make PREFIX=/usr/local/redis install
  6. copy文件。Redis启动需要一个配置文件(redis.conf),可以修改端口号等信息。
  7. 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

 

  1. 安装ruby脚本运行使用的包。

[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前的注释去掉。

分布式缓存_第2张图片

第二步:启动每个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

需要把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参数。Redis节点的列表。

第二步:直接使用JedisCluster对象操作redis。在系统中单例存在。

第三步:打印结果

第四步:系统关闭前,关闭JedisCluster对象。

 

@Test

public void testJedisCluster() throws Exception {

// 第一步:使用JedisCluster对象。需要一个Set参数。Redis节点的列表。

Set nodes = new HashSet<>();

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

 

 

}

 

 

 

你可能感兴趣的:(阿里巴巴)