一只阿木木

如何保证 Redis 的高并发和高可用?讨论redis的单点,高可用,集群

如何保证 Redis 的高并发和高可用?讨论redis的单点,高可用,集群

Image

Image

如何保证 Redis 的高并发和高可用?讨论redis的单点,高可用,集群。

打开GitHub搜索redis,边可以看到,该项目的介绍是这样的:

Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Sets, Hashes, HyperLogLogs, Bitmaps.

我们可以提取其特性的关键字:

  • in-memory database ,内存数据库

  • support:Strings , lists, sets ,hashes ,hyperloglogs, bitmaps 也就是高性能,支持数据类型多。

高可用有两个含义:一是数据尽量不丢失,二是保证服务尽可能可用。 AOF 和 RDB 数据持久化保证了数据尽量不丢失,那么多节点来保证服务尽可能提供服务。

一般在实际生产中,服务不会部署成单节点,主要是有三个原因.

  • 容易出现单点故障,导致服务不可用

  • 单节点处理所有的请求,吞吐量有限

  • 单节点容量有限

为了实现高可用,通常的做法是,将数据库复制多个副本以部署在不同的服务器上,其中一台挂了也可以继续提供服务。Redis 实现高可用有三种部署模式:主从模式,哨兵模式,集群模式。

本文讨论redis的单点,高可用,集群。

前言:Redis提供了哪些高可用方案?

Redis基于一个Master主节点多Slave从节点的模式和Redis持久化机制,将一份数据保持在多个实例中实现增加副本冗余量,又使用哨兵机制实现主备切换, 在master故障时,自动检测,将某个slave切换为master,最终实现Redis高可用 。

Redis主从复制

Redis主从复制,主从库模式一个Master主节点多Slave从节点的模式,将一份数据保存在多Slave个实例中,增加副本冗余量,当某些出现宕机后,Redis服务还可以使用。

但是这会存在数据不一致问题,那redis的副本集是如何数据一致性?

Redis为了保证数据副本的一致,主从库之间采用读写分离的方式:

读操作:主库、从库都可以执行处理;

写操作:先在主库执行,再由主库将写操作同步给从库。

使用读写分离方式的好处,可以避免当主从库都可以处理写操作时,主从库处理写操作加锁等一系列巨额的开销。

采用读写分离方式,写操作只会在主库中进行后同步到从库中,那主从库是如何同步数据的呢?

主从库是同步数据方式有两种:

  • 全量同步:通常是主从服务器刚刚连接的时候,会先进行全量同步

  • 增量同步:一般在全同步结束后,进行增量同步,比如主从库间网络断,再进行数据同步。

全量同步

主从库间第一次全量同步,具体分成三个阶段:

  • 当一个从库启动时,从库给主库发送psync命令进行数据同步(psync命令包含:主库的runID和复制进度offset两个参数),

  • 当主库接收到psync 命令后将会保存RDB 文件并发送给从库,发送期间会使用缓存区(replication buffer)记录后续的所有写操作 ,从库收到数据后,会先清空当前数据库,然后加载从主库获取的RDB 文件,

  • 当主库完成 RDB 文件发送后,也会把将保存发送RDB文件期间写操作的replication buffer发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。

另,为了分担主库生成 RDB 文件和传输 RDB 文件压力,提高效率,可以使用“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。

增量同步

增量同步,基于环形缓冲区repl_backlog_buffer缓存区实现。

在环形缓冲区,主库会记录自己写到的位置master_repl_offset,从库则会记录自己已经读到的位置slave_repl_offset, 主库并通过master_repl_offset和slave_repl_offset的差值的数据同步到从库。

主从库间网络断了, 主从库会采用增量复制的方式继续同步,主库会把断连期间收到的写操作命令,写入replication buffer,同时也会把这些操作命令也写入repl_backlog_buffer这个缓冲区,然后主库并通过master_repl_offset和slave_repl_offset的差值数据同步到从库。

因为repl_backlog_buffer是一个环形缓冲区,当在缓冲区写满后,主库会继续写入,此时,会出现什么情况呢?

覆盖掉之前写入的操作。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。因此需要关注repl_backlog_size参数,调整合适的缓冲空间大小,避免数据覆盖,主从数据不一致。

主从复制,除了会出现数据不一致外,甚至可能出现主库宕机的情况,Redis会有主从自主切换机制,那如何实现的呢?

Redis哨兵机制

当主库挂了,redis写操作和数据同步无法进行,为了避免这样情况,可以在主库挂了后重新在从库中选举出一个新主库,并通知到客户端,redis提供了哨兵机制,哨兵为运行在特殊模式下的 Redis 进程。

Redis会有主从自主切换机制,那如何实现的呢?

哨兵机制是实现主从库自动切换的关键机制,其主要分为三个阶段:

  • 监控:哨兵进程会周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。

  • 选主(选择主库):主库挂了以后,哨兵基于一定规则评分选选举出一个从库实例新的主库 。

  • 通知 : 哨兵会将新主库的信息发送给其他从库,让它们和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的信息广播通知给客户端,让它们把请求操作发到新主库上。

其中,在监控中如何判断主库是否处于下线状态?

哨兵对主库的下线判断分为:

  • 主观下线:哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态,如果单哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”

  • 客观下线:在哨兵集群中,基于少数服从多数,多数实例都判定主库已“主观下线”,则认为主库“客观下线”。

为什么会有这两种"主观下线"和“客观下线”的下线状态呢?

由于单机哨兵很容易产生误判,误判后主从切换会产生一系列的额外开销,为了减少误判,避免这些不必要的开销,采用哨兵集群,引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况,

基于少数服从多数原则, 当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线” (可以自定义设置阙值)

那么哨兵之间是如何互相通信的呢?

哨兵集群中哨兵实例之间可以相互发现,基于Redis提供的发布 / 订阅机制(pub/sub机制),

哨兵可以在主库中发布/订阅消息,在主库上有一个名为“_sentinel_:hello”的频道,不同哨兵就是通过它来相互发现,实现互相通信的,而且只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换。

哨兵 1连接相关信息(IP端口)发布到“_sentinel_:hello”频道上,哨兵 2 和 3 订阅了该频道。

哨兵 2 和 3 就可以从这个频道直接获取哨兵 1连接信息,以这样的方式哨兵集群就形成了,实现各个哨兵互相通信。

哨兵集群中各个实现通信后,就可以判定主库是否已客观下线。

在已判定主库已下线后,又如何选举出新的主库?

新主库选举按照一定条件筛选出的符合条件的从库,并按照一定规则对其进行打分,最高分者为新主库。

通常一定条件包括:

从库的当前在线状态,

判断它之前的网络连接状态,通过down-after-milliseconds * num(断开连接次数),当断开连接次数超过阈值,不适合为新主库。

一定规则包括:

从库优先级 , 通过slave-priority配置项,给不同的从库设置不同优先级,优先级最高的从库得分高

从库复制进度,和旧主库同步程度最接近的从库得分高,通过repl_backlog_buffer缓冲区记录主库master_repl_offset和从库slave_repl_offset相差最小高分

从库 ID 号 , ID 号小的从库得分高。

全都都基于在只有在一定规则中的某一轮评出最高分从库就选举结束,哨兵发起主从切换。

leader哨兵

选举完新的主库后,不能每个哨兵都发起主从切换,需要选举成leader哨兵,那如何选举leader哨兵执行主从切换?

选举leader哨兵,也是基于少数服从多数原则"投票仲裁"选举出来,

当任何一个从库判定主库“主观下线”后,发送命令s-master-down-by-addr命令发送想要成为Leader的信号,

其他哨兵根据与主机连接情况作出相对的响应,赞成票Y,反对票N,而且如果有多个哨兵发起请求,每个哨兵的赞成票只能投给其中一个,其他只能为反对票。

想要成为Leader 的哨兵,要满足两个条件:

第一,获得半数以上的赞成票;

第二,获得的票数同时还需要大于等于哨兵配置文件中的quorum值。

选举完leader哨兵并新主库切换完毕之后,那么leader哨兵怎么通知客户端?

还是基于哨兵自身的 pub/sub 功能,实现了客户端和哨兵之间的事件通知,客户端订阅哨兵自身消息频道 ,而且哨兵提供的消息订阅频道有很多,不同频道包含了:

事件相关频道

主库下线事件+sdown(实例进入“主观下线”状态)

-sdown(实例退出“主观下线”状态)

+odown(实例进入“客观下线”状态)

-odown(实例退出“客观下线”状态)

新主库切换+ switch-master(主库地址发生变化)

其中,当客户端从哨兵订阅消息主从库切换,当主库切换后,端户端就会接收到新主库的连接信息:

switch-master 复制代码

在这样的方式哨兵就可以通知客户端切换了新库。

基于上述的机制和原理Redis实现了高可用,但也会带了一些潜在的风险,比如数据缺失。

数据问题

Redis实现高可用,但实现期间可能产出一些风险:

主备切换的过程, 异步复制导致的数据丢失

  • 脑裂导致的数据丢失

  • 主备切换的过程,异步复制导致数据不一致

数据丢失-主从异步复制

因为master将数据复制给slave是异步实现的,在复制过程中,这可能存在master有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了。

总结:主库的数据还没有同步到从库,结果主库发生了故障,未同步的数据就丢失了。

数据丢失-脑裂

何为脑裂?当一个集群中的 master 恰好网络故障,导致与 sentinal 通信不上了,sentinal会认为master下线,且sentinal选举出一个slave 作为新的 master,此时就存在两个 master了。

此时,可能存在client还没来得及切换到新的master,还继续写向旧master的数据,当master再次恢复的时候,会被作为一个slave挂到新的master 上去,自己的数据将会清空,重新从新的master 复制数据,这样就会导致数据缺失。

总结:主库的数据还没有同步到从库,结果主库发生了故障,等从库升级为主库后,未同步的数据就丢失了。

数据丢失解决方案

数据丢失可以通过合理地配置参数 min-slaves-to-write 和 min-slaves-max-lag 解决,比如

min-slaves-to-write1

min-slaves-max-lag10

如上两个配置:要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒,如果超过 1 个 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。

数据不一致

在主从异步复制过程,当从库因为网络延迟或执行复杂度高命令阻塞导致滞后执行同步命令,这样就会导致数据不一致

解决方案: 可以开发一个外部程序来监控主从库间的复制进度(master_repl_offset和slave_repl_offset),通过监控master_repl_offset与slave_repl_offset差值得知复制进度,当复制进度不符合预期设置的Client不再从该从库读取数据。

Image

一 .redis 安装及配置

1 redis 安装

打开redis的官网 http://redis.io 。

下载一个最新版本的安装包,如 redis-version.tar.gz 解压 tar zxvf redis-version.tar.gz 执行 make (执行此命令可能会报错,例如确实gcc,一个个解决即可) 如果是 mac 电脑,安装redis将十分简单执行brew install redis即可。

安装好redis之后,我们先不慌使用,先进行一些配置。打开redis.conf文件,我们主要关注以下配置:

port 6379             # 指定端口为 6379,也可自行修改 daemonize yes         # 指定后台运行

1.1 redis 单点

安装好redis之后,我们来运行一下。启动redis的命令为 :

redishome/bin/redis-server path/to/redis.config

假设我们没有配置后台运行(即:daemonize no),那么我们会看到如下启动日志:

93825:C 20 Jan 2019 11:43:22.640 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo93825:C 20 Jan 2019 11:43:22.640 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=93825, just started93825:C 20 Jan 2019 11:43:22.640 # Configuration loaded93825:S 20 Jan 2019 11:43:22.641 * Increased maximum number of open files to 10032 (it was originally set to 256).                _._                                                  
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 5.0.3 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6380
| `-._ `._ / _.-' | PID: 93825
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'

无论是否配置了后台运行,启动成功之后,我们可以新开一个命令行窗口来操作试试。

1.1.2 在命令窗口操作redis

使用命令:telnet localhost 6379 来连接redis,或者你可以直接使用代码来连接测试。连接之后,看到如下信息:

Connected to localhost.Escape character is '^]'.

我们输入几个命令试试:

set hello world     设置key-valueget hello           获取key值expire hello 10     设置10秒过期ttl hello           查看过期时间del hello           删除key

刚才我们是使用命令行来操作redis的,下面我们来使用代码操作一下redis,以Java为例,我们使用一个开源的 java - redis客户端。

1.1.3 使用jedis客户端操作redis

打开GitHub,搜索redis,进入到项目主页之后,我们可以看到使用方法:

1 加入jedis依赖

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.0.0</version>
<type>jar</type>
<scope>compile</scope></dependency>

2 编写代码如下

Jedis jedis = new Jedis("localhost",6379);

jedis.set("hello", "world");

String value = jedis.get("hello");System.out.println(value); // get world

jedis.del("hello");System.out.println(jedis.get("hello"));// get null

1.1.4 使用spring-redis操作

上面jedis操作redis的例子很简单,除了使用jedis之外,还可以使用spring-redis。步骤如下

配置redis

<bean id="jedisConnFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
p:use-pool="true"/>
<!-- redis template definition --><bean id="redisTemplate"
class="org.springframework.data.redis.core.RedisTemplate"
p:connection-factory-ref="jedisConnFactory"/>

编写代码

public class Example {    // inject the actual template
@Autowired private RedisTemplate<String, String> template; // inject the template as ListOperations
// can also inject as Value, Set, ZSet, and HashOperations
@Resource(name="redisTemplate")
private ListOperations<String, String> listOps;

public void addLink(String userId, URL url) {
listOps.leftPush(userId, url.toExternalForm()); // or use template directly
redisTemplate.boundListOps(userId).leftPush(url.toExternalForm());
}
}

1.1.5 使用Lettuce操作redis

Lettuce是一个基于netty的 非阻塞的 redis客户端。支持Java8以及响应式。其官网为 https://lettuce.io/。Lettuce也可以和spring搭配使用。 使用Lettuce需要加入如下maven依赖:

<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>5.1.5.RELEASE</version></dependency>

基本的 get,set示例代码如下:

public class LettuceTest {    public static void main(String[] args) {
RedisURI uri = new RedisURI();
uri.setHost("myredishost");
uri.setPort(6379);
uri.setDatabase(0);
RedisClient redisClient = RedisClient.create(uri);
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisCommands<String, String> syncCommands = connection.sync();

syncCommands.set("testKey", "Hello, Redis!");
System.out.println(syncCommands.get("testKey"));

connection.close();
redisClient.shutdown();
}
}

二 redis 主从

上面我们启动了一台redis,并对其进行操作。当然这只是实验性的玩玩。假设我们生产环境使用了一台redis,redis挂了怎么办?如果等到运维重启redis,并恢复好数据,可能需要花费很长时间。那么在这期间,我们的服务是不可用的,这应该是不能容忍的。假设我们做了主从,主库挂了之后,运维让从库接管,那么服务可以继续运行,正所谓有备无患。

redis主从配置非常简单,过程如下(ps 演示情况下主从配置在一台电脑上):

复制两个redis配置文件(启动两个redis,只需要一份redis程序,两个不同的redis配置文件即可)

mkdir redis-master-slavecp path/to/redis/conf/redis.conf path/to/redis-master-slave master.confcp path/to/redis/conf/redis.conf path/to/redis-master-slave slave.conf

1 修改配置

## master.confport 6379## master.confport 6380
slaveof 127.0.0.1 6379

分别启动两个redis

redis-server path/to/redis-master-slave/master.conf
redis-server path/to/redis-master-slave/slave.conf

启动之后,打开两个命令行窗口,分别执行telnet localhost 6379 telnet localhost 6380 然后分别在两个窗口中执行 info 命令,可以看到

# Replicationrole:master# Replicationrole:slavemaster_host:127.0.0.1master_port:6379

主从配置没问题。

然后在master 窗口执行 set 之后,到slave窗口执行get,可以get到,说明主从同步成功。

这时,我们如果在slave窗口执行 set ,会报错:

-READONLY You can't write against a read only replica.

因为从节点是只读的。

三 哨兵sentinel

上面我们介绍了主从,从库作为一个“傀儡”,可以在需要的时候“顶上来”,”接盘“。我们配置的主从是为了”有备无患“,在主redis挂了之后,可以立马切换到从redis上,可能只需要花几分钟的时间,但是仍然是需要人为操作。假设主redis在晚上23点挂了,10分钟之后你接到电话,老板让你赶紧修复,于是你从被窝爬起来整,岂不是很头疼。假如你关机了,又其他人知道服务器密码,那系统岂不是要停机一晚上?太可怕了。

这个时候redis sentinel 就派上用场了。sentinel 通常翻译成哨兵,就是放哨的,这里它就是用来监控主从节点的健康情况。客户端连接redis主从的时候,先连接 sentinel,sentinel会告诉客户端主redis的地址是多少,然后客户端连接上redis并进行后续的操作。当主节点挂掉的时候,客户端就得不到连接了因而报错了,客户端重新想sentinel询问主master的地址,然后客户端得到了[新选举出来的主redis],然后又可以愉快的操作了。

3.2 哨兵sentinel配置

为了说明sentinel的用处,我们做个试验。配置3个redis(1主2从),1个哨兵。步骤如下:

mkdir redis-sentinelcd redis-sentinelcp redis/path/conf/redis.conf path/to/redis-sentinel/redis01.confcp redis/path/conf/redis.conf path/to/redis-sentinel/redis02.confcp redis/path/conf/redis.conf path/to/redis-sentinel/redis03.conftouch sentinel.conf

上我们创建了 3个redis配置文件,1个哨兵配置文件。我们将 redis01设置为master,将redis02,redis03设置为slave。

vim redis01.confport 63791

vim redis02.confport 63792
slaveof 127.0.0.1 63791

vim redis03.confport 63793
slaveof 127.0.0.1 63791

vim sentinel.conf
daemonize yesport 26379
sentinel monitor mymaster 127.0.0.1 63793 1 # 下面解释含义

上面的主从配置都熟悉,只有哨兵配置 sentinel.conf,需要解释一下:

mymaster        为主节点名字,可以随便取,后面程序里边连接的时候要用到127.0.0.1 63793 为主节点的 ip,port1               后面的数字 1 表示选举主节点的时候,投票数。1表示有一个sentinel同意即可升级为master

3.3 启动哨兵,使用jedis连接哨兵操作redis

上面我们配置好了redis主从,1主2从,以及1个哨兵。下面我们分别启动redis,并启动哨兵

redis-server path/to/redis-sentinel/redis01.conf
redis-server path/to/redis-sentinel/redis02.conf
redis-server path/to/redis-sentinel/redis03.conf

redis-server path/to/redis-sentinel/sentinel.conf --sentinel

启动之后,可以分别连接到 3个redis上,执行info查看主从信息。

3.4 编写程序&运行

下面使用程序来连接哨兵,并操作redis。

   public static void main(String[] args) throws Exception{

Set<String> hosts = new HashSet<>();
hosts.add("127.0.0.1:26379"); //hosts.add("127.0.0.1:36379"); 配置多个哨兵

JedisSentinelPool pool = new JedisSentinelPool("mymaster",hosts);
Jedis jedis = null; for(int i=0 ;i<20;i++){
Thread.sleep(2000); try{

jedis = pool.getResource(); String v = randomString();
jedis.set("hello",v);

System.out.println(v+"-->"+jedis.get("hello").equals(v));

}catch (Exception e){
System.out.println(" [ exception happened]" + e);
}
}
}

程序非常简单,循环运行20次,连接哨兵,将随机字符串 set到redis,get结果。打印信息,异常捕获。

3.5模拟主节点宕机情况

运行上面的程序(注意,在实验这个效果的时候,可以将sleep时间加长或者for循环增多,以防程序提前停止,不便看整体效果),然后将主redis关掉,模拟redis挂掉的情况。现在主redis为redis01,端口为63791

redis-cli -p 63791 shutdown

这个时候如果sentinel没有设置后台运行,可以在命令行窗口看到 master切换的情况日志。

# Sentinel ID is fd0634dc9876ec60da65db5ff1e50ebbeefdf5ce
# +monitor master mymaster 127.0.0.1 63791 quorum 1* +slave slave 127.0.0.1:63792 127.0.0.1 63792 @ mymaster 127.0.0.1 63791* +slave slave 127.0.0.1:63793 127.0.0.1 63793 @ mymaster 127.0.0.1 63791# +sdown master mymaster 127.0.0.1 63791# +odown master mymaster 127.0.0.1 63791 #quorum 1/1# +new-epoch 1# +try-failover master mymaster 127.0.0.1 63791# +vote-for-leader fd0634dc9876ec60da65db5ff1e50ebbeefdf5ce 1# +elected-leader master mymaster 127.0.0.1 63791# +failover-state-select-slave master mymaster 127.0.0.1 63791# +selected-slave slave 127.0.0.1:63793 127.0.0.1 63793 @ mymaster 127.0.0.1 63791* +failover-state-send-slaveof-noone slave 127.0.0.1:63793 127.0.0.1 63793 @ mymaster 127.0.0.1 63791* +failover-state-wait-promotion slave 127.0.0.1:63793 127.0.0.1 63793 @ mymaster 127.0.0.1 63791# +promoted-slave slave 127.0.0.1:63793 127.0.0.1 63793 @ mymaster 127.0.0.1 63791# +failover-state-reconf-slaves master mymaster 127.0.0.1 63791* +slave-reconf-sent slave 127.0.0.1:63792 127.0.0.1 63792 @ mymaster 127.0.0.1 63791* +slave-reconf-inprog slave 127.0.0.1:63792 127.0.0.1 63792 @ mymaster 127.0.0.1 63791* +slave-reconf-done slave 127.0.0.1:63792 127.0.0.1 63792 @ mymaster 127.0.0.1 63791# +failover-end master mymaster 127.0.0.1 63791# +switch-master mymaster 127.0.0.1 63791 127.0.0.1 63793* +slave slave 127.0.0.1:63792 127.0.0.1 63792 @ mymaster 127.0.0.1 63793* +slave slave 127.0.0.1:63791 127.0.0.1 63791 @ mymaster 127.0.0.1 63793# +sdown slave 127.0.0.1:63791 127.0.0.1 63791 @ mymaster 127.0.0.1 63793# -sdown slave 127.0.0.1:63791 127.0.0.1 63791 @ mymaster 127.0.0.1 63793* +convert-to-slave slave 127.0.0.1:63791 127.0.0.1 63791 @ mymaster 127.0.0.1 63793

上面的日志较多,仔细找找可以看到下面几行主要的:

初始情况下,1主2从# +monitor master mymaster 127.0.0.1 63791 quorum 1* +slave slave 127.0.0.1:63792 127.0.0.1 63792 @ mymaster 127.0.0.1 63791* +slave slave 127.0.0.1:63793 127.0.0.1 63793 @ mymaster 127.0.0.1 63791发现主挂了,准备 故障转移# +try-failover master mymaster 127.0.0.1 63791将主切换到了 63793 即redis03 
# +switch-master mymaster 127.0.0.1 63791 127.0.0.1 63793

这个日志比较晦涩,从代码运行效果看,如下:

14:45:20.675 [main] INFO redis.clients.jedis.JedisSentinelPool - Trying to find master from available Sentinels...
14:45:25.731 [main] DEBUG redis.clients.jedis.JedisSentinelPool - Connecting to Sentinel 192.168.1.106:26379
14:45:25.770 [main] DEBUG redis.clients.jedis.JedisSentinelPool - Found Redis master at 127.0.0.1:63792
14:45:25.771 [main] INFO redis.clients.jedis.JedisSentinelPool - Redis master running at 127.0.0.1:63792, starting Sentinel listeners...
14:45:25.871 [main] INFO redis.clients.jedis.JedisSentinelPool - Created JedisPool to master at 127.0.0.1:63792
ejahaeegig-->truedeeeadejjf-->true
[ exception happened]redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool [ exception happened]........
[ exception happened]........
[ exception happened]........
[ exception happened]........
[ exception happened]........
[ exception happened]........
[ exception happened]........
[ exception happened]........
[ exception happened]........
14:46:02.737 [MasterListener-mymaster-[192.168.1.106:26379]] DEBUG redis.clients.jedis.JedisSentinelPool - Sentinel 192.168.1.106:26379 published: mymaster 127.0.0.1 63792 127.0.0.1 63793.
14:46:02.738 [MasterListener-mymaster-[192.168.1.106:26379]] INFO redis.clients.jedis.JedisSentinelPool - Created JedisPool to master at 127.0.0.1:63793
haiihiihbb-->trueifgebdcicd-->trueaajhbjagag-->trueProcess finished with exit code 0

从结果看出

开始正常操作redis,并设置了两次。 主redis挂了,jedis得不到连接,报错了JedisConnectionException:Could not get a resource from the pool 主redis没选好之前,程序持续报错。 主redis选好了,程序正常运行,最后结束。 我们看到最后一次运行设置的值是aajhbjagag,我们可以连接剩下的2台redis中的任意一台,get hello,结果肯定是一致的。

四 redis cluster

redis 单点,redis主从,并增加了高可用的 sentinel 哨兵模式。我们所做的这些工作只是保证了数据备份以及高可用,目前为止我们的程序一直都是向1台redis写数据,其他的redis只是备份而已。实际场景中,单个redis节点可能不满足要求,因为:

单个redis并发有限

单个redis接收所有的数据,最终回导致内存太大,内存太大回导致rdb文件过大,从很大的rdb文件中同步恢复数据会很慢。 所有,我们需要redis cluster 即redis集群。

Redis 集群是一个提供在多个Redis间节点间共享数据的程序集。

Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误.

Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令. Redis 集群的优势:

自动分割数据到不同的节点上。 整个集群的部分节点失败或者不可达的情况下能够继续处理命令。 为了配置一个redis cluster,我们需要准备至少6台redis,为啥至少6台呢?我们可以在redis的官方文档中找到如下一句话:

Note that the minimal cluster that works as expected requires to contain at least three master nodes. 

因为最小的redis集群,需要至少3个主节点,既然有3个主节点,而一个主节点搭配至少一个从节点,因此至少得6台redis。然而对我来说,就是复制6个redis配置文件。本实验的redis集群搭建依然在一台电脑上模拟。

4.1 配置 redis cluster 集群

上面提到,配置redis集群需要至少6个redis节点。因此我们需要准备及配置的节点如下:

主:redis01  从 redis02    slaveof redis01主:redis03  从 redis04    slaveof redis03主:redis05  从 redis06    slaveof redis05mkdir redis-clustercd redis-clustermkdir redis01 到 redis06 6个文件夹cp redis.conf 到 redis01 ... redis06修改端口分别配置3组主从关系

4.2启动redis集群

上面的配置完成之后,分别启动6个redis实例。配置正确的情况下,都可以启动成功。然后运行如下命令创建集群:

redis-5.0.3/src/redis-cli --cluster create 127.0.0.1:6371 127.0.0.1:6372 127.0.0.1:6373 127.0.0.1:6374 127.0.0.1:6375 127.0.0.1:6376 --cluster-replicas 1

注意,这里使用的是ip:port,而不是 domain:port ,因为我在使用 localhost:6371 之类的写法执行的时候碰到错误:

ERR Invalid node address specified: localhost:6371

执行成功之后,连接一台redis,执行 cluster info 会看到类似如下信息:

cluster_state:okcluster_slots_assigned:16384cluster_slots_ok:16384cluster_slots_pfail:0cluster_slots_fail:0cluster_known_nodes:6cluster_size:3cluster_current_epoch:6cluster_my_epoch:1cluster_stats_messages_ping_sent:1515cluster_stats_messages_pong_sent:1506cluster_stats_messages_sent:3021cluster_stats_messages_ping_received:1501cluster_stats_messages_pong_received:1515cluster_stats_messages_meet_received:5cluster_stats_messages_received:3021

我们可以看到cluster_state:ok,cluster_slots_ok:16384,cluster_size:3。

4.3 使用jedis连接redis cluster 集群

上面我们配置了一个redis集群,包含6个redis节点,3主3从。下面我们来使用jedis来连接redis集群。代码如下:

    public static void main(String[] args) {        Set<HostAndPort> jedisClusterNodes = new HashSet<HostAndPort>();
//Jedis Cluster will attempt to discover cluster nodes automatically
jedisClusterNodes.add(new HostAndPort("127.0.0.1", 6371));
jedisClusterNodes.add(new HostAndPort("127.0.0.1", 6372));
jedisClusterNodes.add(new HostAndPort("127.0.0.1", 6373));
jedisClusterNodes.add(new HostAndPort("127.0.0.1", 6374));
jedisClusterNodes.add(new HostAndPort("127.0.0.1", 6375));
jedisClusterNodes.add(new HostAndPort("127.0.0.1", 6376));
JedisCluster jc = new JedisCluster(jedisClusterNodes);
jc.set("foo", "bar");
String value = jc.get("foo"); System.out.println(" ===> " + value);
}

上面我们设置了信息set foo bar,但是不知道被设置到那一台redis上去了。请读者思考一下,我们是集群模式,所以数据被分散放到不同的槽中了,Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:

节点 A 包含 0 到 5500号哈希槽. 节点 B 包含5501 到 11000 号哈希槽. 节点 C 包含11001 到 16384号哈希槽. 看到这里你应该还是不知道set foo bar 放到哪台redis上去了,不妨尝试连接任意一台redis探索一下,你会知道的。

总结

主从模式的优缺点

优点

做到读写分离,提高服务器性能。Salve可以分载Master的读操作压力,当然写服务依然必须由Master来完成; 当Master节点服务挂了,可以让Slave变成Master节点继续提供服务;

缺点

在主从模式中,一旦Master节点由于故障不能提供服务,需要人工将Slave节点晋升为Master节点,同时还要通知应用方更新Master节点地址。显然,大多数业务场景都不能接受这种故障处理方式; redis的Master节点和Slave节点中的数据是一样的,降低的内存的可用性,而且存储能力也有限。 主从复制写还都是在Master节点,所以写的压力并没有减少。 因此,主从复制其实并不能满足我们高可用的要求。

哨兵模式的优缺点

优点

哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。 主从可以自动切换,系统更健壮,可用性更高。

缺点

具有主从模式的缺点,每台机器上的数据是一样的,内存的可用性较低。 还要多维护一套哨兵模式,实现起来也变的更加复杂增加维护成本。 Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

集群

哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存。因此,在Redis3.0后Cluster集群应运而生,

它实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。

在整个redis cluster架构中,如果出现以下情况

  • 新加入节点

  • slot迁移

  • 节点宕机

  • slave选举成为master

我们希望这些变化能够让整个集群中的每个节点都能够尽快发现,传播到整个集群并且集群中所有节点达成一致,那么各个节点之间就需要相互连通并且携带相关状态数据进行传播,按照正常的逻辑是采用广播的方式想集群中的所有节点发送消息,有点是集群中的数据同步较快,但是每条消息都需要发送给所有节点,对CPU和带宽的消耗过大,所以这里采用了gossip协议。

它的特点是,在节点数量有限的网络中,每个节点都会“随机”(不是真正随机,而是根据规则选择通信节点)与部分节点通信,经过一番杂乱无章的通信后,每个节点的状态在一定时间内会达成一致

优点: gossip协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新有一定的延时,降低了压力; 去中心化、可扩展、

容错、一致性收敛、简单。 由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。

缺点: 元数据更新有延时可能导致集群的一些操作会有一些滞后。 消息的延迟 , 消息冗余 。

回到我们最初的问题:

如何保证 Redis 的高并发和高可用?

一般来说,使用 Redis 主要是用作缓存,如果数据量大,一台机器肯定是不够的,肯定要考虑如何用 Redis 来加多台机器,保证 Redis 是高并发的,还有就是如何让 Redis 保证自己不是挂掉以后就直接死掉了,即 Redis 高可用。

对于高可用,通过 Redis 主从架构 + 哨兵可以实现高可用,一主多从,任何一个实例宕机,可以进行主备切换。一般来说,很多项目其实就足够了,单主用来写入数据,单机几万 QPS,多从用来查询数据,多个从实例可以提供每秒 10w 的 QPS。

对于高并发,那么就需要 Redis 集群,多主多从,使用 Redis 集群之后,可以提供每秒几十万的读写并发。

未完待续... ...

文:一只阿木木

欢迎转发、交流小数据。

Image

松花酿酒,春水煎茶。

眉上风止,见字如晤。

一只阿木木  

Image