目录
1、为什么要使用Redis集群
基于内存大小
Redis是一个内存数据库,也就是说存储数据的容量不能超过主机内存大小。普通主机服务器的内存一般几十G,但是我们需要存储大容量的数据(比如上百G的数据)怎么办?
基于访问吞吐量
Redis是基于内存处理,速度非常快,但是人的欲望是永无止境的,如果因为业务需要更快的处理,怎么办?
2、Redis集群的方式
主从复制 哨兵 Redis Cluster(推荐)
2.1 主从复制
2.1.1 主从复制实现
特点:
1、Master会将数据同步到slave,而slave不会将数据同步到master。Slave启动时会连接master来同步数据,Master数据变更时会将数据同步刷新到Slave 2、典型的分布式读写分离模型。我们可以利用master来处理写操作,slave提供读操作。这样可以有效减少单个机器的并发访问数量
实现:
三个节点
192.168.223.128 6379 master
192.168.223.129 6379 slave
192.168.223.130 6379 slave
实现主从复制这种模式非常简单,主节点不用做任何修改,直接启动服务即可。从节点需要修改redis.conf配置文件,加入配置:slaveof <主节点ip地址> <主节点端口号>,例如master的ip地址为192.168.223.128,端口号为6379,那么slave只需要在redis.conf文件中配置slaveof 192.168.223.128 6379即可。
slaveof 192.168.223.128 6379
#PS:默认情况下cluster-enabled配置为yes或者被注释的,标识不允许内置集群模式下直连(3.0之后出现的),如果你的为yes,改成no
分别连接主节点和从节点,测试发现主节点的写操作,从节点立刻就能看到相同的数据。但是在从节点进行写操作,提示 READONLY You can't write against a read only slave 不能写数据到从节点。我们就可以通过这种方式配置多个从节点进行读操作,主节点进行写操作,实现读写分离。
启动后主节点日志信息
启动后节点信息
2.1.2 主从复制原理
2.1.2.1 执行流程
下面来看一下执行一下过程
开启master/slave的日志:
cd /usr/local/redis-4.0.6
tail -f log/redis.log
在129或者130客户端执行执行slaveof命令或者直接配置到redis.conf文件中重新启动,为了演示完整过程我们这边以命令形式启动主从复制:
2.1.2.2 保存主节点信息
主节点128打印日志如下:
主节点接受主从复制连接请求,同意建立主从同步连接
从节点129/130打印日志,从节点保存主节点信息:
2.1.2.3 建立socket连接
主从建立socket连接
如果从节点无法建立连接,定时任务会无限重试直到连接成功或者执行 slaveof no one 取消复制 连接
2.1.2.4 发送ping命令
发送首次ping命令,如果能接受主节点的pong回复,复制可以继续;如果不能,继续重连:
从节点日志:
2.1.2.5 权限验证
重新起两个redis服务:129,130,同样开启日志记录
-
如果主节点设置了 requirepass 参数,则需要密码验证,从节点必须配置 masterauth 参数保证与主节点相同的密码才能通过验证;如果验证失败复制将终止,从节点重新发起复制流程
-
如果两个密码不匹配,会验证报错
2.1.2.6 同步数据集
主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这部分操作是耗时最长的步骤 ,因为所有数据包括持久化的RBD和AOF数据
2.1.2.7 维持心跳检测
2.1.2.8 命令持续复制
主节点会持续地把写命令发送给从节点,保证主从数据一致性。可以连续新增多个key测试
2.1.3 问题思考
在主从复制这种模式下只有一个主节点,一旦主节点宕机,就无法再进行写操作了。也就是说主从复制这种模式并没有实现高可用!
高可用(HA)是分布式系统架构设计中必须考虑的因素之一,它是通过架构设计减少系统不能提供服务的时间。保证高可用通常遵循下面几点:
1)、单点是系统高可用的大敌,应该尽量在系统设计的过程中避免单点。
2)、通过架构设计而保证系统高可用的,其核心准则是:冗余。
3)、实现自动故障转移。
2.2 哨兵(Sentinel)
2.2.1 哨兵原理
Sentinel(哨兵)是用于监控Redis集群中Master状态的工具,其本身也是一个独立运行的进程,是Redis的高可用解决方案,Sentinel哨兵模式已经被集成在redis2.4之后的版本中。Sentinel可以监视一个或者多个Redis Master服务,以及这些Master服务的所有从服务;当某个Master服务下线时,自动将该Master下的某个从服务升级为Master服务替代已下线的Master服务继续处理请求,并且其余从节点开始从新的主节点复制数据
2.2.2 哨兵实现
#在redis的根目录下,可以看到已经有了一个sentinel.conf配置文件,直接修改之:
# 修改配置文件:
vim sentinel.conf
#增加以下内容:注意如有重复内容需要去掉
-----------------------------------
#绑定外网访问权限DNS路由
bind 0.0.0.0
#mymaster 主节点名,可以任意起名,但必须和后面的配置保持一致
# 1 为需要主服务器断掉时需要1个sentinel同意(sentinel本身也可以集群),咱们这里只有一个sentinel,写1就好
sentinel monitor mymaster 192.168.223.128 6379 1
#设置Sentinel认为服务器已经断线所需的毫秒数
sentinel down-after-milliseconds mymaster 10000
#设置failover(故障转移)的过期时间。当failover开始后,在此时间内仍然没有触发任何failover操作,当前 sentinel 会认为此次failover失败
sentinel failover-timeout mymaster 6000
#设置在执行故障转移时, 最多可以有多少个从服务器同时对新的主服务器进行同步, 这个数字越小,表示同时进行同步的从服务器越少,那么完成故障转移所需的时间就越长。
sentinel parallel-syncs mymaster 1
2.2.3 启动哨兵
#把三台redis服务跑起来,原先的主从集群关系不变
./bin/redis-server redis.conf
#再启动哨兵进程,哨兵启动需要使用redis-sentinel启动脚本启动
./bin/redis-sentinel sentinel.conf
可以看到,129/130作为从节点已经挂载到了128主节点下了
哨兵测试,将128 shutdown,再看日志
主节点已经变成了130了!128再次启动,已经不是主节点了:
哨兵模式的优缺点
优点: 1、当Master节点挂掉时,会通过vote算法从Slave节点中选举出一个新的节点作为Master节点继续工作 2、任何一个数据的增删改都需要Master节点处理,能保障数据的强一致性,符合CAP定理中的CP原则 缺点: 1、主从服务器的数据要经常进行数据全量复制,这样造成性能下降。 2、当主服务器宕机后,从服务器切换成主服务器的那段时间,服务是不可用的,降低了高可用性
2.4 Redis内置集群Cluster
模式概述:
Redis Cluster是Redis的内置集群,在Redis3.0推出的实现方案。在Redis3.0之前是没有这个内置集群的。Redis Cluster是无中心节点的集群架构,依靠Gossip协议协同自动化修复集群的状态。 Redis Cluster在设计的时候,就考虑到了去中心化,也就是说,集群中的每个节点都是平等的关系,都是对等的,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。 需要注意的是,这种集群模式下集群中每个节点保存的数据并不是所有的数据,而只是一部分数据。那么数据是如何合理的分配到不同的节点上的呢? Redis 集群是采用一种叫做 哈希槽 (hash slot) 的方式来分配数据的。Redis Cluster 默认分配了16384 个slot,当我们set一个key 时,会用 CRC16 算法来取模得到所属的 slot ,然后将这个key 分到哈希槽区间的节点上,具体算法就是: CRC16(key) % 16384 。
Redis Cluster的主从模式
Redis Cluster 为了保证数据的高可用性,加入了主从模式,一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份,当这个主节点挂掉后,就会在这些从节点中选取一个来充当主节点,从而保证集群不会挂掉。 Redis Cluster加入了主从模式后的效果如下:
Redis Cluster实现
2.4.1 准备Redis节点
为了保证可以进行投票,需要至少3个主节点。 每个主节点都需要至少一个从节点,所以需要至少3个从节点。 一共需要6台redis服务器,我们这里使用6个redis实例,端口号为7001~7006。(没有那么多虚拟机,所以做单机伪集群) 先准备一个干净的redis环境,复制原来的bin文件夹,清理后作为第一个redis节点,具体命令如下:
2.4.2 配置
复制bin目录6份,redis.conf文件6份
cd /usr/local/redis-4.0.6
mkdir cluster
cp -R bin/ cluster/node1
cp redis.conf cluster/node1/
cp -R bin/ cluster/node2
cp redis.conf cluster/node2/
cp -R bin/ cluster/node3
cp redis.conf cluster/node3/
cp -R bin/ cluster/node4
cp redis.conf cluster/node4/
cp -R bin/ cluster/node5
cp redis.conf cluster/node5/
cp -R bin/ cluster/node6
cp redis.conf cluster/node6/
修改每一个节点的配置文件 主要是配置端口和集群配置文件,集群一般都是默认开启的
vim redis.conf
#删掉之前配置的slaveof 192.168.223.128 6379主从配置
# Redis服务器可以跨网络访问
bind 0.0.0.0
# 修改端口号
port 7001
# Redis后台启动
daemonize yes
# 开启aof持久化 (命令重刷模式)
appendonly yes
# 开启集群
cluster-enabled yes
#持久化数据根目录改为cluster
dir "/usr/local/redis-4.0.6/cluster"
# 集群的配置 配置文件首次启动自动生成 其他节点改为相应的端口,保证不同名或者目录不一样即可
cluster-config-file nodes7001.conf
#AOF持久化日志,其他节点改为相应的端口
appendfilename "appendonly7001.aof"
#RDB持久化日志,其他节点改为相应的端口
dbfilename "dump7001.rdb"
# 集群节点请求超时
cluster-node-timeout 5000
#关闭集群部分数据槽丢失不完整兼容性
cluster-require-full-coverage no
为了避免启动麻烦,编写一个shell脚本来一次性启动这6台服务
cd /usr/local/redis-4.0.6/cluster
vim startAll.sh
#添加如下启动脚本
-----------------------------------------------
cd node1
./redis-server redis.conf
cd ..
cd node2
./redis-server redis.conf
cd ..
cd node3
./redis-server redis.conf
cd ..
cd node4
./redis-server redis.conf
cd ..
cd node5
./redis-server redis.conf
cd ..
cd node6
./redis-server redis.conf
------------------------------------------------
2.4.3 启动Redis
设置脚本的权限并启动(并未建立真正的集群):
cd /usr/local/redis-4.0.6/cluster
chmod 777 startAll.sh
./startAll.sh
启动效果如下:
2.4.4 安装ruby脚本语言环境
redis集群的管理工具使用的是ruby脚本语言,安装集群需要ruby环境,先安装ruby环境:
# 安装ruby
yum -y install ruby ruby-devel rubygems rpm-build
# 升级ruby版本,redis4.0.6集群环境需要2.2.0以上的ruby版本
yum install centos-release-scl-rh
yum install rh-ruby23 -y
scl enable rh-ruby23 bash
# 查看ruby版本
ruby -v
2.4.5 安装Gem并启动集群管理
#下载符合环境要求的gem,下载地址如下:
#https://rubygems.org/gems/redis/versions/4.1.0
#课程资料中已经提供了redis-4.1.0.gem,直接上传安装即可,安装命令:
gem install redis-4.1.0.gem
#进入redis安装目录,使用redis自带的集群管理脚本,执行命令:
cd /usr/local/redis-4.0.6/src
# 查看集群管理脚本 ll *.rb
# 使用集群管理脚本启动集群,下面命令中的1表示为每个主节点创建1个从节点
./redis-trib.rb create --replicas 1 192.168.223.128:7001 192.168.223.128:7002 192.168.223.128:7003 192.168.223.128:7004 192.168.223.128:7005 192.168.223.128:7006
#PS,第一次启动时注意要没有相同的数据,因为cluster集群是数据分片的,所以把RDB,AOF持久化文件删除,并且将cluster-config-file配置的内置集群生成的配置文件干掉
2.4.6 启动测试效果
可以看出,Gem已经为我们分配了3个主节点和三个从节点并分配了各节点负责的hash槽位
按照Redis Cluster的特点,它是去中心化的,每个节点都是对等的,所以连接哪个节点都可以获取和设置数据。
./redis-cli -h 192.168.223.128 -p 7006 -c
其中-c 一定要加,这个是Redis集群连接时,进行节点跳转的参数。
连接到集群后可以设置一些值,可以看到这些值根据前面提到的哈希槽方式分散存储在不同的节点上了。
2.4.7 Redis Cluster的优缺点
优点: 1、数据分布存储到不同的节点,降低了数据的冗余 2、任何一个节点都可以作为数据的入口,提高了集群的高可用性, 符合CAP定理中的AP原则
缺点: 1、同样因为任何一个节点都可以作为数据的入口,降低了数据的一致性
附:集群常用命令
cluster info :打印集群的信息
cluster nodes :列出集群当前已知的所有节点( node),以及这些节点的相关信息。
节点
cluster meet <ip> <port> :将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。
cluster forget <node_id> :从集群中移除 node_id 指定的节点。
cluster replicate <master_node_id> :将当前从节点设置为 node_id 指定的master节点的slave节点。只能针对slave节点操作。
cluster saveconfig :将节点的配置文件保存到硬盘里面。
槽(slot)
cluster addslots <slot> [slot ...] :将一个或多个槽( slot)指派( assign)给当前节点。
cluster delslots <slot> [slot ...] :移除一个或多个槽对当前节点的指派。
cluster flushslots :移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
cluster setslot <slot> node <node_id> :将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给
另一个节点,那么先让另一个节点删除该槽>,然后再进行指派。
cluster setslot <slot> migrating <node_id> :将本节点的槽 slot 迁移到 node_id 指定的节点中。
cluster setslot <slot> importing <node_id> :从 node_id 指定的节点中导入槽 slot 到本节点。
cluster setslot <slot> stable :取消对槽 slot 的导入( import)或者迁移( migrate)。
键
cluster keyslot <key> :计算键 key 应该被放置在哪个槽上。
cluster countkeysinslot <slot> :返回槽 slot 目前包含的键值对数量。
cluster getkeysinslot <slot> <count> :返回 count 个 slot 槽中的键 。
3、Java应用
3.1 依赖坐标
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.3</version>
</dependency>
<!-- spring-redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.6.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
</dependencies>
3.2 哨兵模式Spring配置
<!--Jedis连接池的相关配置-->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal">
<value>200</value>
</property>
<property name="maxIdle">
<value>50</value>
</property>
<property name="testOnBorrow" value="true"/>
<property name="testOnReturn" value="true"/>
</bean>
<!--哨兵服务配置-->
<bean id="sentinelConfig"
class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
<!–服务名称 –>
<property name="master">
<bean class="org.springframework.data.redis.connection.RedisNode">
<!–主节点名称,与哨兵配置保持一致–>
<property name="name" value="mymaster" />
</bean>
</property>
<!–哨兵服务IP和端口 –>
<property name="sentinels">
<set>
<bean class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="192.168.223.128" />
<constructor-arg name="port" value="26379" />
</bean>
</set>
</property>
</bean>
<!--连接工厂设置-->
<bean id="connectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<constructor-arg name="sentinelConfig" ref="sentinelConfig" />
<constructor-arg name="poolConfig" ref="jedisPoolConfig"></constructor-arg>
</bean>
<!--配置RedisTemplate-->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<!–以下为key/value序列化转换,避免出现16进制文本–>
<property name="keySerializer" >
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
<property name="valueSerializer" >
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
</bean>
3.3 Redis Cluster配置
<!--redis cluster 服务配置 ,需要将主备节点全部配上去,否则主节点挂掉后,数据访问不了-->
<bean class="redis.clients.jedis.JedisCluster">
<constructor-arg name="nodes">
<set>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.223.128"></constructor-arg>
<constructor-arg name="port" value="7001"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.223.128"></constructor-arg>
<constructor-arg name="port" value="7002"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.223.128"></constructor-arg>
<constructor-arg name="port" value="7003"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.223.128"></constructor-arg>
<constructor-arg name="port" value="7004"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.223.128"></constructor-arg>
<constructor-arg name="port" value="7005"></constructor-arg>
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="192.168.223.128"></constructor-arg>
<constructor-arg name="port" value="7006"></constructor-arg>
</bean>
</set>
</constructor-arg>
<constructor-arg name="poolConfig" ref="jedisClusterPoolConfig"></constructor-arg>
<constructor-arg name="timeout" value="1000"></constructor-arg>
</bean>
3.4 测试
@Test
public void testSentinel(){
RedisTemplate redisTemplate = (RedisTemplate) context.getBean("redisTemplate");
redisTemplate.boundValueOps("length").set("18");
}
@Test
public void testCluster(){
JedisCluster cluster = context.getBean(JedisCluster.class);
cluster.set("age", String.valueOf(18));
}