Redis支持三种集群方案
主从复制模式
Sentinel(哨兵)模式
Cluster模式
【主从模式】
使用一个Redis实例作为主机,其余的作为备份机。主机和备份机的数据完全一致,主机支持数据的写入和读取等各项操作,而从机则只支持与主机数据的同步和读取。也就是说,客户端可以将数据写入到主机,由主机自动将数据的写入操作同步到从机。主从模式很好的解决了数据备份问题,并且由于主从服务数据几乎是一致的,因而可以将写入数据的命令发送给主机执行,而读取数据的命令发送给不同的从机执行,从而达到读写分离的目的。
【sentinel】
哨兵是redis集群中非常重要的一个组件,主要有以下功能。哨兵用于实现reds集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作,
故障转移时,判断一个master node是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举·即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,哨兵通常需要③个实例,来保证自己的健壮性
哨兵+redis主从的部架构,是不保证数据丢失的,只能保证redis集群的高可用性。
对于哨兵+eds主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试式和演练
【Redis Cluster】
是一种服务端Shardingt技术,3.0版本开始正式提供。采用slot(槽)的概念一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行Redis Cluster是一种服务端Sharding技木,3.0版本开始正式提供。采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点称查询请求发送到正确的节点上执行
优点
·无中心架构,支持动态扩容,对业务透明
·具备Sentinel的监控和自动Failover(故障转移)能力客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可高性能,客户端直连redis服务,
·免去了proxy代理的损耗
缺点
·运维也很复杂,数据迁移需要人工干预
·只能使用0号数据库
·不支持批量操作(pipeline管道操作)
·分布式逻辑和存储模块耦合等
什么是主从复制?
Redis的持久化保证了即使Redis服务重启也不会丢失数据,因为Redis服务重启后会将硬盘上持久化的数据恢复到内存中,但是当Redis服务器的硬盘损坏了可能会导致数据丢失,不过通过Redis的主从复制机制就可以避免这种单点故障
1. 基本原理
主从复制模式中包含一个主数据库实例(master)与一个或多个从数据库实例(slave),如下图
客户端可对主数据库进行读写操作,对从数据库进行读操作,主数据库写入的数据会实时自动同步给从数据库。
主redis中的数据有两个副本,即从redis1和从redis2,即使一台Redis服务器宕机其他两台Redis服务也可以继续提供服务
主redis中的数据和从redis中的数据保持实时同步,当主redis写入数据时通过主从复制机制会复制到两个从redis服务上
只有一个主redis,可以有多个从redis
master和slave都是一个redis实例(redis服务)
通过主从配置可以实现读写分离
主从复制不会阻塞master,在同步数据时,master可以继续处理客户端请求
一个Redis可以既是主又是从,即redis1和redis2下面又可以有多个从redis服务器,而它们又是redis的从服务器,如下:
对主从模式必须的理解(结论已经验证过,可以自行验证):
对有密码的情况说明一下,当master节点设置密码时:
主从模式的缺点其实从上面的描述中可以得出:
这个缺点影响是很大的,尤其是对生产环境来说,是一刻都不能停止服务的,所以一般的生产坏境是不会单单只有主从模式的。所以有了下面的sentinel模式。
具体工作机制为:
1、复制一份redis.conf文件
2、将相关配置修改为如下值:
port 6380
pidfile /var/run/redis_6380.pid # 把pid进程号写入pidfile配置的文件
logfile "6380.log"
dir /usr/local/redis-5.0.3/data/6380 # 指定数据存放目录
# 根据需求判断是否注释bind
bind 0.0.0.0
3、配置主从复制
replicaof 192.168.0.60 6379 # 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof
replica-read-only yes # 配置从节点只读
4、启动从节点
redis-server redis.conf
5、连接从节点
redis-cli -p 6380
6、测试在6379实例上写数据,6380实例是否能及时同步新修改数据
7、可以自己再配置一个6381的从节点
进入master数据库,写入一个数据,再进入一个slave数据库,立即便可访问刚才写入master数据库的数据。如下所示
[root@dev-server-1 master-slave]# redis-cli
127.0.0.1:6379> auth 123456
OK
127.0.0.1:6379> set site blog.jboost.cn
OK
127.0.0.1:6379> get site
"blog.jboost.cn"
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=7001,state=online,offset=13364738,lag=1
slave1:ip=127.0.0.1,port=7002,state=online,offset=13364738,lag=0
...
127.0.0.1:6379> exit
[root@dev-server-1 master-slave]# redis-cli -p 7001
127.0.0.1:7001> auth 123456
OK
127.0.0.1:7001> get site
"blog.jboost.cn"
执行info replication命令可以查看连接该数据库的其它库的信息
如果你为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个PSYNC命令给master请求复制数据。
master收到PSYNC命令后,会在后台进行数据持久化通过bgsave生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后再加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave。
当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。
数据部分复制
当master和slave断开重连后,一般都会对整份数据进行复制。但从redis2.8版本开始,redis改用可以支持部分数据复制的命令PSYNC去master同步数据,slave与master能够在网络连接断开重连后只进行部分数据复制(断点续传)。
master会在其内存中创建一个复制数据用的缓存队列,缓存最近一段时间的数据,master和它所有的slave都维护了复制的数据下标offset和master的进程id,因此,当网络连接断开后,slave会请求master继续进行未完成的复制,从所记录的数据下标开始。如果master进程id变化了,或者从节点数据下标offset太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。
主从复制的优缺点
优点:
缺点:
1. 基本原理
哨兵模式基于主从复制模式,只是引入了哨兵来监控与自动处理故障。
sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。
sentinel的中文含义是哨兵、守卫。也就是说既然主从模式中,当master节点挂了以后,slave节点不能主动选举一个master节点出来,那么我就安排一个或多个sentinel来做这件事,当sentinel发现master节点挂了以后,sentinel就会从slave中重新选举一个master。
哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过sentinel代理访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)
什么是哨兵
Redis-Sentinel是用于管理Redis集群,该系统执行以下三个任务:
1.监控(Monitoring):
Sentinel会不断地检查你的主服务器和从服务器是否运作正常
2.提醒(Notification):
当被监控的某个Redis服务器出现问题时,
Sentinel可以通过API向管理员或者其他应用程序发送通知
3.自动故障迁移(Automatic failover)
:当一个主服务器不能正常工作时,Sentinel 会开始一次自动故障迁移操作,
它会将失效主服务器的其中一个从服务器升级为新的主服务器,
并让失效主服务器的其他从服务器改为复制新的主服务器;
当客户端试图连接失效的主服务器时,
集群也会向客户端返回新主服务器的地址,
使得集群可以使用新主服务器代替失效服务器
功能概况即:
在配置文件中通过sentinel monitor
来定位master的IP、端口,一个哨兵可以监控多个master数据库,只需要提供多个该配置项即可。哨兵启动后,会与要监控的master建立两条连接:
与master建立连接后,哨兵会执行三个操作:
发送INFO命令可以获取当前数据库的相关信息从而实现新节点的自动发现。所以说哨兵只需要配置master数据库信息就可以自动发现其slave信息。获取到slave信息后,哨兵也会与slave建立两条连接执行监控。通过INFO命令,哨兵可以获取主从数据库的最新信息,并进行相应的操作,比如角色变更等。
接下来哨兵向主从数据库的sentinel:hello频道发送信息与同样监控这些数据库的哨兵共享自己的信息,发送内容为哨兵的ip端口、运行id、配置版本、master名字、master的ip端口还有master的配置版本。这些信息有以下用处:
如果被PING的数据库或者节点超时(通过 sentinel down-after-milliseconds master-name milliseconds 配置)未回复,哨兵认为其主观下线(sdown,s就是Subjectively —— 主观地)。
如果下线的是master,哨兵会向其它哨兵发送命令询问它们是否也认为该master主观下线,如果达到一定数目(即配置文件中的quorum)投票,哨兵会认为该master已经客观下线(odown,o就是Objectively —— 客观地),并选举领头的哨兵节点对主从系统发起故障恢复。
若没有足够的sentinel进程同意master下线,master的客观下线状态会被移除,若master重新向sentinel进程发送的PING命令返回有效回复,master的主观下线状态就会被移除。
哨兵认为master客观下线后,故障恢复的操作需要由选举的领头哨兵来执行,选举采用Raft算法:
选出领头哨兵后,领头者开始对系统进行故障恢复,从出现故障的master的从数据库中挑选一个来当选新的master,选择规则如下:
挑选出需要继任的slave后,领头哨兵向该数据库发送命令使其升格为master,然后再向其他slave发送命令接受新的master,最后更新数据。将已经停止的旧的master更新为新的master的从数据库,使其恢复服务后以slave的身份继续运行。
redis哨兵架构搭建步骤:
1、复制一份sentinel.conf文件
cp sentinel.conf sentinel-26379.conf
2、将相关配置修改为如下值:
port 26379
daemonize yes
pidfile "/var/run/redis-sentinel-26379.pid"
logfile "26379.log"
dir "/usr/local/redis-5.0.3/data"
# sentinel monitor <master-redis-name> <master-redis-ip> <master-redis-port> <quorum>
# quorum是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel总数/2 + 1),master才算真正失效
sentinel monitor mymaster 192.168.0.60 6379 2 # mymaster这个名字随便取,客户端访问时会用到
3、启动sentinel哨兵实例
src/redis-sentinel sentinel-26379.conf
4、查看sentinel的info信息
src/redis-cli -p 26379
127.0.0.1:26379>info
可以看到Sentinel的info里已经识别出了redis的主从
5、可以自己再配置两个sentinel,端口26380和26381,注意上述配置文件里的对应数字都要修改
sentinel集群都启动完毕后,会将哨兵集群的元数据信息写入所有sentinel的配置文件里去(追加在文件的最下面),我们查看下如下配置文件sentinel-26379.conf,如下所示:
sentinel known-replica mymaster 192.168.0.60 6380 #代表redis主节点的从节点信息
entinel known-replica mymaster 192.168.0.60 6381 #代表redis主节点的从节点信息
sentinel known-sentinel mymaster 192.168.0.60 26380
52d0a5d70c1f90475b4fc03b6ce7c3c56935760f #代表感知到的其它哨兵节点sentinel known-sentinel mymaster 192.168.0.60 26381
e9f530d3882f8043f76ebb8e1686438ba8bd5ca6 #代表感知到的其它哨兵节点
public class JedisSentinelTest {
public static void main(String[] args) throws IOException {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(20);
config.setMaxIdle(10);
config.setMinIdle(5);
String masterName = "mymaster";
Set<String> sentinels = new HashSet<String>();
sentinels.add(new HostAndPort("192.168.0.60",26379).toString());
sentinels.add(new HostAndPort("192.168.0.60",26380).toString());
sentinels.add(new HostAndPort("192.168.0.60",26381).toString());
//JedisSentinelPool其实本质跟JedisPool类似,都是与redis主节点建立的连接池
//JedisSentinelPool并不是说与sentinel建立的连接池,而是通过sentinel发现redis主节点并与其建立连接
JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, config, 3000, null);
Jedis jedis = null;
try {
jedis = jedisSentinelPool.getResource();
System.out.println(jedis.set("sentinel", "zhuge"));
System.out.println(jedis.get("sentinel"));
} catch (Exception e) {
e.printStackTrace();
} finally {
//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
if (jedis != null)
jedis.close();
}
}
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
springboot项目核心配置:
server:
port: 8080
spring:
redis:
database: 0
timeout: 3000
sentinel: #哨兵模式
master: mymaster #主服务器所在集群名称,即conf配置中的sentinel monitor mymaster 192.168.0.60 6379 2
nodes: 192.168.0.60:26379,192.168.0.60:26380,192.168.0.60:26381
lettuce:
pool:
max-idle: 50
min-idle: 10
max-active: 100
max-wait: 1000
访问代码:
(下面这段代码在运行以后自己手动关闭主节点,会发现程序先报错,之后又可以继续正常执行opsForValue().set操作,证明已经选举出来了新的主节点)
@RestController
public class IndexController {
private static final Logger logger = LoggerFactory.getLogger(IndexController.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 测试节点挂了哨兵重新选举新的master节点,客户端是否能动态感知到
* 新的master选举出来后,哨兵会把消息发布出去,客户端实际上是实现了一个消息监听机制,
* 当哨兵把新master的消息发布出去,客户端会立马感知到新master的信息,从而动态切换访问的masterip
*
* @throws InterruptedException
*/
@RequestMapping("/test_sentinel")
public void testSentinel() throws InterruptedException {
int i = 1;
while (true){
try {
stringRedisTemplate.opsForValue().set("zhuge"+i, i+"");
System.out.println("设置key:"+ "zhuge" + i);
i++;
Thread.sleep(1000);
}catch (Exception e){
logger.error("错误:", e);
}
}
}
}
虽然客户端访问的是sentinel,但是sentinel会实时监控到所有结点(包括主节点和从结点),且会感知到哪个结点是master结点,因此sentinel会把结点的信息推送给客户端,客户端的所有插入新键值对的操作会在从节点上进行
1. 基本原理
在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态,如果master节点异
常,则会做主从切换,将某一台slave作为master,哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般,特别是在主从切换的瞬间存在访问瞬断的情况,而且哨兵模式只有一个主节点对外提供服务,没法支持很高的并发,且单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率
Cluster模式实现了Redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题。如图
高可用集群模式
redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵∙也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单.
Cluster采用无中心结构,它的特点如下:
【Cluster模式的具体工作机制】:
Cluster模式集群节点最小配置6个节点(3主3从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
槽位定位算法
Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。
HASH_SLOT = CRC16(key) mod 16384
跳转重定位
当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有 key 将使用新的槽位映射表。
redis cluster节点间采取gossip协议进行通信
维护集群的元数据(集群节点信息,主从角色,节点数量,各节点共享的数据等)有两种方式:集中式和gossip
集中式:
优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到;不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。 很多中间件都会借助zookeeper集中式存储元数据。
gossip:
gossip协议包含多种消息,包括ping,pong,meet,fail等等。
meet:某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信;
ping:每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据(类似自己感知到的集群节点增加和移除,hash slot信息等);
pong: 对ping和meet消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新;
fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了。
gossip协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;缺点在于元数据更新有延时可能导致集群的一些操作会有一些滞后。
Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?
因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。
奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。(挂了的结点也算在超过半数的结点里面)
注意是超过半数,不是大于等于
redis集群搭建
redis集群需要至少三个master节点,我们这里搭建三个master节点,并且给每个master再搭建一个slave节点,总共6个redis节点,这里用三台机器部署6个redis实例,每台机器一主一从,搭建集群的步骤如下:
首先在redis.conf中
第一步:在第一台机器的/usr/local下创建文件夹redis-cluster,然后在其下面分别创建2个文件夾如下
(1)mkdir -p /usr/local/redis-cluster
(2)mkdir 8001 8004
第一步:把之前的redis.conf配置文件copy到8001下,修改如下内容:
(1)daemonize yes
(2)port 8001(分别对每个机器的端口号进行设置)
(3)pidfile /var/run/redis_8001.pid # 把pid进程号写入pidfile配置的文件
(4)dir /usr/local/redis-cluster/8001/(指定数据文件存放位置,必须要指定不同的目录位置,不然会丢失数据)
(5)cluster-enabled yes(启动集群模式)
(6)cluster-config-file nodes-8001.conf(集群节点信息文件,这里800x最好和port对应上)
(7)cluster-node-timeout 10000
(8)# bind 127.0.0.1(也可以写成0.0.0.0,允许公网访问)
(9)protected-mode no (关闭保护模式)
(10)appendonly yes
如果要设置密码需要增加如下配置:
(11)requirepass 123456 (设置redis访问密码)
(12)masterauth 123456 (设置集群节点间访问密码,跟上面一致)
第三步:把修改后的配置文件,copy到8004,修改第2、3、4、6项里的端口号,可以用批量替换:
:%s/源字符串/目的字符串/g
第四步:另外两台机器也需要做上面几步操作,第二台机器用8002和8005,第三台机器用8003和8006
第五步:分别启动6个redis实例,然后检查是否启动成功
(1)/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/800*/redis.conf
(2)ps -ef | grep redis 查看是否启动成功
第六步:用redis-cli创建整个redis集群(redis5以前的版本集群是依靠ruby脚本redis-trib.rb实现)
# 下面命令里的1代表为每个创建的主服务器节点创建一个从服务器节点
# 执行这条命令需要确认三台机器之间的redis实例要能相互访问,可以先简单把所有机器防火墙关掉,如果不关闭防火墙则需要打开redis服务端口和集群节点gossip通信端口16379(默认是在redis端口号上加1W)
# 关闭防火墙
# systemctl stop firewalld # 临时关闭防火墙
# systemctl disable firewalld # 禁止开机启动
# 注意:下面这条创建集群的命令大家不要直接复制,里面的空格编码可能有问题导致创建集群不成功
(1)/usr/local/redis-5.0.3/src/redis-cli -a 123456 --cluster create --cluster-replicas 1 192.168.0.61:8001 192.168.0.62:8002 192.168.0.63:8003 192.168.0.61:8004 192.168.0.62:8005 192.168.0.63:8006
第七步:验证集群:
(1)连接任意一个客户端即可:./redis-cli -c -h -p (-a访问服务端密码,-c表示集群模式,指定ip地址和端口号)
如:/usr/local/redis-5.0.3/src/redis-cli -a 123456 -c -h 192.168.0.61 -p 800*
(2)进行验证: cluster info(查看集群信息)、cluster nodes(查看节点列表)
(3)进行数据操作验证
(4)关闭集群则需要逐个进行关闭,使用命令:
/usr/local/redis-5.0.3/src/redis-cli -a 123456 -c -h 192.168.0.60 -p 800* shutdown
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>2.9.0version>
dependency>
public class JedisClusterTest {
public static void main(String[] args) throws IOException {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(20);
config.setMaxIdle(10);
config.setMinIdle(5);
Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
jedisClusterNode.add(new HostAndPort("192.168.0.61", 8001));
jedisClusterNode.add(new HostAndPort("192.168.0.62", 8002));
jedisClusterNode.add(new HostAndPort("192.168.0.63", 8003));
jedisClusterNode.add(new HostAndPort("192.168.0.61", 8004));
jedisClusterNode.add(new HostAndPort("192.168.0.62", 8005));
jedisClusterNode.add(new HostAndPort("192.168.0.63", 8006));
JedisCluster jedisCluster = null;
try {
//connectionTimeout:指的是连接一个url的连接等待时间
//soTimeout:指的是连接上一个url,获取response的返回等待时间
jedisCluster = new JedisCluster(jedisClusterNode, 6000, 5000, 10, "xiaoming", config);
System.out.println(jedisCluster.set("cluster", "xiaoming"));
System.out.println(jedisCluster.get("cluster"));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedisCluster != null)
jedisCluster.close();
}
}
}