详解Redis
- 1.什么是redis
- 2.为什么要用redis?
- 2.1 为什么要用缓存?
- 3.redis的数据结构
- 3.1 string 类型 (key : value(string/int/float))
- 3.2 list 类型 有序列表 (key:(value1,value2,value3,,,,,valuen)) 元素值可以重复
- 3.3 set 类型 无序列表 (key:(value1,value2,value3,,,,,valuen))
- 3.4 hash类型 散列类型 key:(key1:value1,key2:value2...)
- 3.5 sort set类型 有序分数集 key:(score1:value1:rank,score2:value2,rank)
- 4.redis服务器的数据库
- 4.1 redis数据库的原理
- 4.2 键的过期时间
- 4.2.1 过期策略
- 4.2.2 内存淘汰机制
- 5.redis持久化
- 5.1 RDB(快照持久化)
- 5.2 AOF(文件重写)
- 5.2.1 AOF重写
- 5.2.2 AOF文件重写
- 5.3 RDB和AOF 对过期键的策略
- 5.4 RDB和AOF用哪个?
- 6. Redis事件
- 6.1 文件事件
- 6.2 时间事件
- 6.3 时间事件与文件事件
- 7.Redis单线程为什么快?
- 8 客户端与服务器
- 8.1 客户端
- 8.2 服务端
- 9. Redis的主从架构(主从复制)
- 9.1 为什么要用主从架构
- 9.2 主从架构的特点
- 10 复制功能
- 10.1 复制功能的具体实现
- 10.1.1 复制的前置工作
- 10.1.2 完整重同步
- 10.1.3 部分重同步
- 10.1.4 命令传播
- 11 提一个问题
- 11.1 哨兵(sentinel)机制
- 11.1.1 启动和初始化Sentinel
- 11.1.2 获取和更新信息
- 11.1.3 判断主服务器是否下线了
- 11.1.4 选举领头Sentinel 和故障转移
1.什么是redis
Redis是一个开源的,基于内存的数据结构存储,可用作于数据库、缓存、消息中间件。
从官方的解释上,我们可以知道:Redis是基于内存,支持多种数据结构。
从经验的角度上,我们可以知道:Redis常用作于缓存。
就我个人认为:学习一种新技术,先把握该技术整体的知识(思想),再扣细节,这样学习起来会比较轻松一些。所以我们先以“内存”、“数据结构”、“缓存”来对Redis入门。
2.为什么要用redis?
从上面可知:Redis是基于内存,常用作于缓存的一种技术,并且Redis存储的方式是以key-value的形式。
我们可以发现这不就是Java的Map容器所拥有的特性吗,那为什么还需要Redis呢?
Java实现的Map是本地缓存,如果有多台实例(机器)的话,每个实例都需要各自保存一份缓存,缓存不具有一致性
Redis实现的是分布式缓存,如果有多台实例(机器)的话,每个实例都共享一份缓存,缓存具有一致性。
Java实现的Map不是专业做缓存的,JVM内存太大容易挂掉的。一般用做于容器来存储临时数据,缓存的数据随着JVM销毁而结束。Map所存储的数据结构,缓存过期机制等等是需要程序员自己手写的。
Redis是专业做缓存的,可以用几十个G内存来做缓存。Redis一般用作于缓存,可以将缓存数据保存在硬盘中,Redis重启了后可以将其恢复。原生提供丰富的数据结构、缓存过期机制等等简单好用的功能。
2.1 为什么要用缓存?
如果我们的网站出现了性能问题(访问时间慢),按经验来说,一般是由于数据库撑不住了。因为一般数据库的读写都是要经过磁盘的,而磁盘的速度可以说是相当慢的(相对内存来说)
如果学过Mybaits、Hibernate的同学就可以知道,它们有一级缓存、二级缓存这样的功能(终究来说还是本地缓存)。目的就是为了:不用每次读取的时候,都要查一次数据库。有了缓存之后,我们的访问就变成这样了:
3.redis的数据结构
Redis有五种数据类型,具体如下:
通用常用方法:
keys pattern #查找key,可以使用*和?进行搜索。
del key #删除key。
3.1 string 类型 (key : value(string/int/float))
常用方法:
set key value #赋值
get key #获取
incr key [int] #自增操作
decrby key [int] #自减操作
3.2 list 类型 有序列表 (key:(value1,value2,value3,valuen)) 元素值可以重复
常用方法:
lpush key value #从左侧插入值
rpop key #从右侧弹出值
llen key #获取列表元素数量
3.3 set 类型 无序列表 (key:(value1,value2,value3,valuen))
常用方法:
sadd key value #插入值
smembers key #获取列表所有值
scard key #获取列表元素数量
sismember key value #列表是否存在此元素
srem key value #移除值
3.4 hash类型 散列类型 key:(key1:value1,key2:value2…)
常用方法:
hset key key1 value #添加键值对
hget key key1 #获取值
hlen key #获取键值对数量
hmget key key1 key2 #获取多个键值
3.5 sort set类型 有序分数集 key:(score1:value1:rank,score2:value2,rank)
常用方法:
zadd key score value #添加值
zcard key #获取元素数量
zrank key rank #获取key中排序为rank的值
zrank key rank1 rank2 withscores #获取排序为rank1至rank2之间的值
4.redis服务器的数据库
我们应该都用过MySQL,MySQL我们可以在里边创建好几个库,同样地,Redis服务器中也有数据库这么一个概念。如果不指定具体的数量,默认会有16个数据库。
上面的命令我们也可以发现:当切换到15号数据库,存进15号库的数据,再切换到0号数据库时,是获取不到的!
这说明,数据库与数据库之间的数据是隔离的。
4.1 redis数据库的原理
Redis服务器用redisServer结构体来表示,其中redisDb是一个数组,用来保存所有的数据库,dbnum代表数据库的数量(这个可以配置,默认是16)
struct redisServer{
//redisDb数组,表示服务器中所有的数据库
redisDb *db;
//服务器中数据库的数量
int dbnum;
};
我们知道Redis是C/S结构,Redis客户端通过redisClient结构体来表示:
typedef struct redisClient{
//客户端当前所选数据库
redisDb *db;
}redisClient;
Redis客户端连接Redis服务端时的示例图:
Redis中对每个数据库用redisDb结构体来表示:
typedef struct redisDb {
int id; // 数据库ID标识
dict *dict; // 键空间,存放着所有的键值对
dict *expires; // 过期哈希表,保存着键的过期时间
dict *watched_keys; // 被watch命令监控的key和相应client
long long avg_ttl; // 数据库内所有键的平均TTL(生存时间)
} redisDb;
从代码上我们可以发现最重要的应该是dict *dict,它用来存放着所有的键值对。对于dict数据结构(哈希表)我们在上一篇也已经详细说了。一般我们将存储所有键值对的dict称为键空间。
Redis的数据库就是使用字典(哈希表)来作为底层实现的,对数据库的增删改查都是构建在字典(哈希表)的操作之上的。
例如:
redis > GET message
"hello world"
4.2 键的过期时间
Redis是基于内存,内存是比较昂贵的,容量肯定比不上硬盘的。就我们现在一台普通的机子,可能就8G内存,但硬盘随随便便都1T了。
因为我们的内存是有限的。所以我们会干掉不常用的数据,保留常用的数据。这就需要我们设置一下键的过期(生存)时间了。
设置键的生存时间可以通过EXPIRE或者PEXPIRE命令。
设置键的过期时间可以通过EXPIREAT或者PEXPIREAT命令。
其实EXPIRE、PEXPIRE、EXPIREAT这三个命令都是通过PEXPIREAT命令来实现的。
我们在redisDb结构体中还发现了dict *expires;属性,存放所有键过期的时间。
举个例子基本就可以理解了:
redis > PEXPIREAT message 1391234400000
(integer) 1
设置了message键的过期时间为1391234400000
既然有设置过期(生存)时间的命令,那肯定也有移除过期时间,查看剩余生存时间的命令了:
PERSIST(移除过期时间)
TTL(Time To Live)返回剩余生存时间,以秒为单位
PTTL以毫秒为单位返回键的剩余生存时间
4.2.1 过期策略
上面我们已经能够了解到:过期键是保存在哈希表中了。那这些过期键到了过期的时间,就会立马被删除掉吗??
要回答上面的问题,需要我们了解一下删除策略的知识,删除策略可分为三种
定时删除(对内存友好,对CPU不友好)
到时间点上就把所有过期的键删除了。
惰性删除(对CPU极度友好,对内存极度不友好)
每次从键空间取键的时候,判断一下该键是否过期了,如果过期了就删除。
定期删除(折中)
每隔一段时间去删除过期键,限制删除的执行时长和频率。
Redis采用的是惰性删除+定期删除两种策略,所以说,在Redis里边如果过期键到了过期的时间了,未必被立马删除的!
4.2.2 内存淘汰机制
如果定期删除漏掉了很多过期key,也没及时去查(没走惰性删除),大量过期key堆积在内存里,导致redis内存块耗尽了,咋整?
我们可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
Redis的内存淘汰机制有以下几种:
一般场景:
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用allkeys-lru淘汰策略,将最近最少使用的数据淘汰
5.redis持久化
Redis是基于内存的,如果不想办法将数据保存在硬盘上,一旦Redis重启(退出/故障),内存的数据将会全部丢失。
我们肯定不想Redis里头的数据由于某些故障全部丢失(导致所有请求都走MySQL),即便发生了故障也希望可以将Redis原有的数据恢复过来,这就是持久化的作用。
Redis提供了两种不同的持久化方法来讲数据存储到硬盘里边:
RDB(基于快照),将某一时刻的所有数据保存到一个RDB文件中。
AOF(append-only-file),当Redis服务器执行写命令的时候,将执行的写命令保存到AOF文件中。
5.1 RDB(快照持久化)
RDB持久化可以手动执行,也可以根据服务器配置定期执行。RDB持久化所生成的RDB文件是一个经过压缩的二进制文件,Redis可以通过这个文件还原数据库的数据。
有两个命令可以生成RDB文件:
SAVE会阻塞Redis服务器进程,服务器不能接收任何请求,直到RDB文件创建完毕为止。
BGSAVE创建出一个子进程,由子进程来负责创建RDB文件,服务器进程可以继续接收请求。
Redis服务器在启动的时候,如果发现有RDB文件,就会自动载入RDB文件(不需要人工干预)
服务器在载入RDB文件期间,会处于阻塞状态,直到载入工作完成。
除了手动调用SAVE或者BGSAVE命令生成RDB文件之外,我们可以使用配置的方式来定期执行:
在默认的配置下,如果以下的条件被触发,就会执行BGSAVE命令
save 900 1 #在900秒(15分钟)之后,至少有1个key发生变化,
save 300 10 #在300秒(5分钟)之后,至少有10个key发生变化
save 60 10000 #在60秒(1分钟)之后,至少有10000个key发生变化
原理大概就是这样子的(结合上面的配置来看):
struct redisServer{
// 修改计数器
long long dirty;
// 上一次执行保存的时间
time_t lastsave;
// 参数的配置
struct saveparam *saveparams;
};
遍历参数数组,判断修改次数和时间是否符合,如果符合则调用besave()来生成RDB文件
总结:通过手动调用SAVE或者BGSAVE命令或者配置条件触发,将数据库某一时刻的数据快照,生成RDB文件实现持久化。
5.2 AOF(文件重写)
上面已经介绍了RDB持久化是通过将某一时刻数据库的数据“快照”来实现的,下面我们来看看AOF是怎么实现的。
AOF是通过保存Redis服务器所执行的写命令来记录数据库的数据的。
比如说我们对空白的数据库执行以下写命令:
redis> SET meg "hello"
OK
redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> RPUSH numbers 128 256 512
(integer) 3
Redis会产生以下内容的AOF文件:
这些都是以Redis的命令请求协议格式保存的。
AOF持久化功能的实现可以分为3个步骤:
命令追加:命令写入aof_buf缓冲区
文件写入:调用flushAppendOnlyFile函数,考虑是否要将aof_buf缓冲区写入AOF文件中
文件同步:考虑是否将内存缓冲区的数据真正写入到硬盘
flushAppendOnlyFile函数的行为由服务器配置的appendfsyn选项来决定的:
appendfsync always # 每次有数据修改发生时都会写入AOF文件。
appendfsync everysec # 每秒钟同步一次,该策略为AOF的默认策略。
appendfsync no # 从不同步。高效但是数据不会被持久化。
从字面上应该就更好理解了,这里我就不细说了…
下面来看一下AOF是如何载入与数据还原的:
创建一个伪客户端(本地)来执行AOF的命令,直到AOF命令被全部执行完毕。
5.2.1 AOF重写
从前面的示例看出,我们写了三条命令,AOF文件就保存了三条命令。如果我们的命令是这样子的:
redis > RPUSH list "Java" "3y"
(integer)2
redis > RPUSH list "Java3y"
integer(3)
redis > RPUSH list "yyy"
integer(4)
同样地,AOF也会保存3条命令。我们会发现一个问题:上面的命令是可以合并起来成为1条命令的,并不需要3条。这样就可以让AOF文件的体积变得更小。
AOF重写由Redis自行触发(参数配置),也可以用BGREWRITEAOF命令手动触发重写操作。
要值得说明的是:AOF重写不需要对现有的AOF文件进行任何的读取、分析。AOF重写是通过读取服务器当前数据库的数据来实现的!
比如说现在有一个Redis数据库的数据如下:
新的AOF文件的命令如下,没有一条是多余的!
5.2.2 AOF文件重写
Redis将AOF重写程序放到子进程里执行(BGREWRITEAOF命令),像BGSAVE命令一样fork出一个子进程来完成重写AOF的操作,从而不会影响到主进程。
AOF后台重写是不会阻塞主进程接收请求的,新的写命令请求可能会导致当前数据库和重写后的AOF文件的数据不一致!
为了解决数据不一致的问题,Redis服务器设置了一个AOF重写缓冲区,这个缓存区会在服务器创建出子进程之后使用。
5.3 RDB和AOF 对过期键的策略
RDB持久化对过期键的策略:
执行SAVE或者BGSAVE命令创建出的RDB文件,程序会对数据库中的过期键检查,已过期的键不会保存在RDB文件中。
载入RDB文件时,程序同样会对RDB文件中的键进行检查,过期的键会被忽略。
RDB持久化对过期键的策略:
如果数据库的键已过期,但还没被惰性/定期删除,AOF文件不会因为这个过期键产生任何影响(也就说会保留),当过期的键被删除了以后,会追加一条DEL命令来显示记录该键被删除了
重写AOF文件时,程序会对RDB文件中的键进行检查,过期的键会被忽略。
复制模式:
主服务器来控制从服务器统一删除过期键(保证主从服务器数据的一致性)
5.4 RDB和AOF用哪个?
RDB和AOF并不互斥,它俩可以同时使用。
RDB的优点:载入时恢复数据快、文件体积小。
RDB的缺点:会一定程度上丢失数据(因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。)
AOF的优点:丢失数据少(默认配置只丢失一秒的数据)。
AOF的缺点:恢复数据相对较慢,文件体积大
如果Redis服务器同时开启了RDB和AOF持久化,服务器会优先使用AOF文件来还原数据(因为AOF更新频率比RDB更新频率要高,还原的数据更完善)
可能涉及到RDB和AOF的配置:
redis持久化,两种方式
1、rdb快照方式
2、aof日志方式
----------rdb快照------------
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/rdb/
-----------Aof的配置-----------
appendonly no # 是否打开 aof日志功能
appendfsync always #每一个命令都立即同步到aof,安全速度慢
appendfsync everysec
appendfsync no 写入工作交给操作系统,由操作系统判断缓冲区大小,统一写入到aof 同步频率低,速度快
no-appendfsync-on-rewrite yes 正在导出rdb快照的时候不要写aof
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
./bin/redis-benchmark -n 20000
6. Redis事件
Redis服务器是一个事件驱动程序,主要处理以下两类事件:
文件事件:文件事件其实就是对Socket操作的抽象,Redis服务器与Redis客户端的通信会产生文件事件,服务器通过监听并处理这些事件来完成一系列的网络操作
时间事件:时间事件其实就是对定时操作的抽象,前面我们已经讲了RDB、AOF、定时删除键这些操作都可以由服务端去定时或者周期去完成,底层就是通过触发时间事件来实现的!
6.1 文件事件
Redis开发了自己的网络事件处理器,这个处理器被称为文件事件处理器。
文件事件处理器由四部分组成:
文件事件处理器使用I/O多路复用程序来同时监听多个Socket。当被监听的Socket准备好执行连接应答(accept)、读取(read)等等操作时,与操作相对应的文件事件就会产生,根据文件事件来为Socket关联对应的事件处理器,从而实现功能。
要值得注意的是:Redis中的I/O多路复用程序会将所有产生事件的Socket放到一个队列里边,然后通过这个队列以有序、同步、每次一个Socket的方式向文件事件分派器传送套接字。也就是说:当上一个Socket处理完毕后,I/O多路复用程序才会向文件事件分派器传送下一个Socket。
首先,IO多路复用程序首先会监听着Socket的AE_READABLE事件,该事件对应着连接应答处理器
可以理解简单成SocketServet.accpet()
此时,一个名字叫做3y的Socket要连接服务器啦。服务器会用连接应答处理器处理。创建出客户端的Socket,并将客户端的Socket与命令请求处理器进行关联,使得客户端可以向服务器发送命令请求。
相当于Socket s = ss.accept();,创建出客户端的Socket,然后将该Socket关联命令请求处理器
此时客户端就可以向主服务器发送命令请求了
假设现在客户端发送一个命令请求set Java3y “关注、点赞、评论”,客户端Socket将产生AE_READABLE事件,引发命令请求处理器执行。处理器读取客户端的命令内容,然后传给对应的程序去执行。客户端发送完命令请求后,服务端总得给客户端回应的。此时服务端会将客户端的Scoket的AE_WRITABLE事件与命令回复处理器关联。
最后客户端尝试读取命令回复时,客户端Socket产生AE_WRITABLE事件,触发命令回复处理器执行。当把所有的回复数据写入到Socket之后,服务器就会解除客户端Socket的AE_WRITABLE事件与命令回复处理器的关联。最后以《Redis设计与实现》的一张图来概括:
6.2 时间事件
持续运行的Redis服务器会定期对自身的资源和状态进行检查和调整,这些定期的操作由serverCron函数负责执行,它的主要工作包括:
更新服务器的统计信息(时间、内存占用、数据库占用)
清理数据库的过期键值对
AOF、RDB持久化
如果是主从服务器,对从服务器进行定期同步
如果是集群模式,对进群进行定期同步和连接
…
Redis服务器将时间事件放在一个链表中,当时间事件执行器运行时,会遍历整个链表。时间事件包括:
周期性事件(Redis一般只执行serverCron时间事件,serverCron时间事件是周期性的)
定时事件
6.3 时间事件与文件事件
文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程中不会发生抢占。
时间事件的实际处理事件通常会比设定的到达时间晚一些
7.Redis单线程为什么快?
1)纯内存操作
2)核心是基于非阻塞的IO多路复用机制
3)单线程避免了多线程的频繁上下文切换问题
8 客户端与服务器
在《Redis设计与实现》中各用了一章节来写客户端与服务器,我看完觉得比较底层的东西,也很难记得住,所以我决定总结一下比较重要的知识。如果以后真的遇到了,再来补坑~
服务器使用clints链表连接多个客户端状态,新添加的客户端状态会被放到链表的末尾
一个服务器可以与多个客户端建立网络连接,每个客户端可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。
Redis服务器使用单线程单进程的方式处理命令请求。在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转。
8.1 客户端
客户端章节中主要讲解了Redis客户端的属性(客户端状态、输入/输出缓冲区、命令参数、命令函数等等)
typedef struct redisClient{
//客户端状态的输入缓冲区用于保存客户端发送的命令请求,最大1GB,否则服务器将关闭这个客户端
sds querybuf;
//负责记录argv数组的长度。
int argc;
// 命令的参数
robj **argv;
// 客户端要执行命令的实现函数
struct redisCommand *cmd, *lastcmd;
//记录了客户端的角色(role),以及客户端所处的状态。 (REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI)
int flags;
//记录客户端是否通过了身份验证
int authenticated;
//时间相关的属性
time_t ctime; /* Client creation time */
time_t lastinteraction; /* time of the last interaction, used for timeout */
time_t obuf_soft_limit_reached_time;
//固定大小的缓冲区用于保存那些长度比较小的回复
/* Response buffer */
int bufpos;
char buf[REDIS_REPLY_CHUNK_BYTES];
//可变大小的缓冲区用于保存那些长度比较大的回复
list *reply; //可变大小缓冲区由reply 链表和一个或多个字符串对象组成
//...
}
8.2 服务端
服务器章节中主要讲解了Redis服务器读取客户端发送过来的命令是如何解析,以及初始化的过程。
服务器从启动到能够处理客户端的命令请求需要执行以下的步骤:
初始化服务器状态
载入服务器配置
初始化服务器的数据结构
还原数据库状态
执行事件循环
总的来说是这样子的:
def main():
init_server();
while server_is_not_shutdown();
aeProcessEvents()
clean_server();
从客户端发送命令道完成主要包括的步骤:
客户端将命令请求发送给服务器
服务器读取命令请求,分析出命令参数
命令执行器根据参数查找命令的实现函数,执行实现函数并得出命令回复
服务器将命令回复返回给客户端
9. Redis的主从架构(主从复制)
9.1 为什么要用主从架构
Redis也跟关系型数据(MySQL)一样,如果有过多请求还是撑不住的。
因为Redis如果只有一台服务器的话,那随着请求越来越多:
Redis的内存是有限的,可能放不下那么多的数据
单台Redis支持的并发量也是有限的。
万一这台Redis挂了,所有的请求全走关系数据库了,那就更炸了。
显然,出现的上述问题是因为一台Redis服务器不够,所以多搞几台Redis服务器就可以了
为了实现我们服务的高可用性,可以将这几台Redis服务器做成是主从来进行管理
9.2 主从架构的特点
下面我们来看看Redis的主从架构特点:
主服务器负责接收写请求
从服务器负责接收读请求
从服务器的数据由主服务器复制过去。主从服务器的数据是一致的
主从架构的好处:
读写分离(主服务器负责写,从服务器负责读)
高可用(某一台从服务器挂了,其他从服务器还能继续接收请求,不影响服务)
处理更多的并发量(每台从服务器都可以接收读请求,读QPS就上去了)
主从架构除了上面的形式,也有下面这种的(只不过用得比较少):
10 复制功能
主从架构的特点之一:主服务器和从服务器的数据是一致的。
因为主服务器是能接收写请求的,主服务器处理完写请求,会做什么来保证主从数据的一致性呢?如果主从服务器断开了,过一阵子才重连,又会怎么处理呢?下面将会了解到这些细节~
在Redis中,用户可以通过执行SALVEOF命令或者设置salveof选项,让一个服务器去复制(replicate)另一个服务器,我们称呼被复制的服务器为主服务器(master),而对主服务器进行复制的服务器则被称为从服务器(salve)
10.1 复制功能的具体实现
复制功能分为两个操作:
同步(sync)
将从服务器的数据库状态更新至主服务器的数据库状态
命令传播(command propagate)
主服务器的数据库状态被修改,导致主从服务器的数据库状态不一致,让主从服务器的数据库状态重新回到一致状态。
从服务器对主服务器的同步又可以分为两种情况:
初次同步:从服务器没有复制过任何的主服务器,或者从服务器要复制的主服务器跟上次复制的主服务器不一样。
断线后同步:处于命令传播阶段的主从服务器因为网络原因中断了复制,从服务器通过自动重连重新连接主服务器,并继续复制主服务器
在Redis2.8以前,断线后复制这部分其实缺少的只是部分的数据,但是要让主从服务器重新执行SYNC命令,这样的做法是非常低效的。(因为执行SYNC命令是把所有的数据再次同步,而不是只同步丢失的数据)
接下来我们来详细看看Redis2.8以后复制功能是怎么实现的:
10.1.1 复制的前置工作
首先我们来看一下前置的工作:
从服务器设置主服务器的IP和端口
建立与主服务器的Socket连接
发送PING命令(检测Socket读写是否正常与主服务器的通信状况)
身份验证(看有没有设置对应的验证配置)
从服务器给主服务器发送端口的信息,主服务器记录监听的端口
前面也提到了,Redis2.8之前,断线后同步会重新执行SYNC命令,这是非常低效的。下面我们来看一下Redis2.8之后是怎么进行同步的。
Redis从2.8版本开始,使用PSYNC命令来替代SYNC命令执行复制时同步的操作。
PSYNC命令具有完整重同步和部分重同步两种模式(其实就跟上面所说的初次复制和断线后复制差不多个意思)。
10.1.2 完整重同步
下面先来看看完整重同步是怎么实现的:
从服务器向主服务器发送PSYNC命令
收到PSYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件。并用一个缓冲区来记录从现在开始执行的所有写命令。
当主服务器的BGSAVE命令执行完后,将生成的RDB文件发送给从服务器,从服务器接收和载入RBD文件。将自己的数据库状态更新至与主服务器执行BGSAVE命令时的状态。
主服务器将所有缓冲区的写命令发送给从服务器,从服务器执行这些写命令,达到数据最终一致性。
10.1.3 部分重同步
接下来我们来看看部分重同步,部分重同步可以让我们断线后重连只需要同步缺失的数据(而不是Redis2.8之前的同步全部数据),这是符合逻辑的!
部分重同步功能由以下部分组成:
主从服务器的复制偏移量
主服务器的复制积压缓冲区
服务器运行的ID(run ID)
首先我们来解释一下上面的名词:
复制偏移量:执行复制的双方都会分别维护一个复制偏移量
主服务器每次传播N个字节,就将自己的复制偏移量加上N
从服务器每次收到主服务器的N个字节,就将自己的复制偏移量加上N
通过对比主从复制的偏移量,就很容易知道主从服务器的数据是否处于一致性的状态!
那断线重连以后,从服务器向主服务器发送PSYNC命令,报告现在的偏移量是36,那么主服务器该对从服务器执行完整重同步还是部分重同步呢??这就交由复制积压缓冲区来决定。
当主服务器进行命令传播时,不仅仅会将写命令发送给所有的从服务器,还会将写命令入队到复制积压缓冲区里面(这个大小可以调的)。如果复制积压缓冲区存在丢失的偏移量的数据,那就执行部分重同步,否则执行完整重同步。
服务器运行的ID(run ID)实际上就是用来比对ID是否相同。如果不相同,则说明从服务器断线之前复制的主服务器和当前连接的主服务器是两台服务器,这就会进行完整重同步。
所以流程大概如此:
10.1.4 命令传播
当完成了同步之后,主从服务器就会进入命令传播阶段。这时主服务器只要将自己的写命令发送给从服务器,而从服务器接收并执行主服务器发送过来的写命令,就可以保证主从服务器一直保持数据一致了!
在命令传播阶段,从服务器默认会以每秒一次的频率,向服务器发送命令REPLCONF ACK <replication_offset> 其中replication_offset是从服务器当前的复制偏移量
发送这个命令主要有三个作用:
检测主从服务器的网络状态
辅助实现min-slaves选项
检测命令丢失
11 提一个问题
如果从服务器挂了,没关系,我们一般会有多个从服务器,其他的请求可以交由没有挂的从服务器继续处理。如果主服务器挂了,怎么办?因为我们的写请求由主服务器处理,只有一台主服务器,那就无法处理写请求了?
Redis提供了哨兵(Sentinel)机制供我们解决上面的情况。如果主服务器挂了,我们可以将从服务器升级为主服务器,等到旧的主服务器(挂掉的那个)重连上来,会将它(挂掉的主服务器)变成从服务器。
这个过程叫做主备切换(故障转移)
在正常的情况下,主从加哨兵(Sentinel)机制是这样子的:
主服务器挂了,主从复制操作就中止了,并且哨兵系统是可以察觉出主服务挂了。
Redis提供哨兵机制可以将选举一台从服务器变成主服务器
然后旧的主服务器如果重连了,会变成从服务器:
11.1 哨兵(sentinel)机制
哨兵(Sentinel)机制主要用于实现Redis的高可用性,主要的功能如下:
Monitoring. Sentinel constantly checks if your master and slave instances are working as expected.
Sentinel不停地监控Redis主从服务器是否正常工作
Notification. Sentinel can notify the system administrator, another computer programs, via an API, that something is wrong with one of the monitored Redis instances.
如果某个Redis实例有故障,那么哨兵负责发送消息通知管理员
Automatic failover. If a master is not working as expected, Sentinel can start a failover process where a slave is promoted to master, the other additional slaves are reconfigured to use the new master, and the applications using the Redis server informed about the new address to use when connecting.
如果主服务器挂掉了,会自动将从服务器提升为主服务器(包括配置都会修改)。
Configuration provider. Sentinel acts as a source of authority for clients service discovery: clients connect to Sentinels in order to ask for the address of the current Redis master responsible for a given service. If a failover occurs, Sentinels will report the new address.
Sentinel可以作为配置中心,能够提供当前主服务器的信息。
下面来具体讲讲Sentinel是如何将从服务器提升为主服务器的。
tips:Sentinel可以让我们的Redis实现高可用,Sentinel作为这么一个组件,自身也必然是高可用的(不可能是单点的)
11.1.1 启动和初始化Sentinel
首先我们要知道的是:Sentinel本质上只是一个运行在特殊模式下的Redis服务器。因为Sentinel做的事情和Redis服务器是不一样的,所以它们的初始化是有所区别的(比如,Sentinel在初始化的时候并不会载入AOF/RDB文件,因为Sentinel根本就不用数据库)。
然后,在启动的时候会将普通Redis服务器的代码替换成Sentinel专用代码。(所以Sentinel虽然作为Redis服务器,但是它不能执行SET、DBSIZE等等命令,因为命令表的代码被替换了)
接着,初始化Sentinel的状态,并根据给定的配置文件初始化Sentinel监视的主服务器列表。
最后,Sentinel会创建两个连向主服务器的网络连接:
命令连接(发送和接收命令)
订阅连接(订阅主服务器的sentinel:hello频道)
11.1.2 获取和更新信息
Sentinel通过主服务器发送INFO命令来获得主服务器属下所有从服务器的地址信息,并为这些从服务器创建相应的实例结构。
当发现有新的从服务器出现时,除了创建对应的从服务器实例结构,Sentinel还会创建命令连接和订阅连接。
在Sentinel运行的过程中,通过命令连接会以每两秒一次的频率向监视的主从服务器的_sentinel_:hello频道发送命令(主要发送Sentinel本身的信息,监听主从服务器的信息),并通过订阅连接接收_sentinel_:hello频道的信息。
这样一来一回,我们就可以更新每个Sentinel实例结构的信息。
11.1.3 判断主服务器是否下线了
判断主服务器是否下线有两种情况:
**主观下线**
Sentinel会以每秒一次的频率向与它创建命令连接的实例(包括主从服务器和其他的Sentinel)发送PING命令,通过PING命令返回的信息判断实例是否在线
如果一个主服务器在down-after-milliseconds毫秒内连续向Sentinel发送无效回复,那么当前Sentinel就会主观认为该主服务器已经下线了。
**客观下线**
当Sentinel将一个主服务器判断为主观下线以后,为了确认该主服务器是否真的下线,它会向同样监视该主服务器的Sentinel询问,看它们是否也认为该主服务器是否下线。
如果足够多的Sentinel认为该主服务器是下线的,那么就判定该主服务为客观下线,并对主服务器执行故障转移操作。
在多少毫秒内无效回复才认定主服务器是主观下线的,以及多少个Sentinel认为主服务器是下线的,才认定为客观下线。这都是可以配置的
11.1.4 选举领头Sentinel 和故障转移
当一个主服务器认为为客观下线以后,监视这个下线的主服务器的各种Sentinel会进行协商,选举出一个领头的Sentinel,领头的Sentinel会对下线的主服务器执行故障转移操作。
选举领头Sentinel的规则也比较多,总的来说就是先到先得(哪个快,就选哪个)
选举出领头的Sentinel之后,领头的Sentinel会对已下线的主服务器执行故障转移操作,包括三个步骤:
在已下线主服务器属下的从服务器中,挑选一个转换为主服务器
让已下线主服务器属下的所有从服务器改为复制新的主服务器
已下线的主服务器重新连接时,让他成为新的主服务器的从服务器
(这三步实际上就是文章开头的图片)
挑选某一个从服务器作为主服务器也是有策略的,大概如下:
(1)跟master断开连接的时长
(2)slave优先级
(3)复制offset
(4)run id
tips:目前为止的主从+哨兵架构可以说Redis是高可用的,但要清楚的是:Redis还是会丢失数据的
丢失数据有两种情况:
异步复制导致的数据丢失
有部分数据还没复制到从服务器,主服务器就宕机了,此时这些部分数据就丢失了
脑裂导致的数据丢失
有时候主服务器脱离了正常网络,跟其他从服务器不能连接。此时哨兵可能就会认为主服务器下线了(然后开启选举,将某个从服务器切换成了主服务器),但是实际上主服务器还运行着。这个时候,集群里就会有两个服务器(也就是所谓的脑裂)。
虽然某个从服务器被切换成了主服务器,但是可能客户端还没来得及切换到新的主服务器,客户端还继续写向旧主服务器写数据。旧的服务器重新连接时,会作为从服务器复制新的主服务器(这意味着旧数据丢失)。
可以通过以下两个配置尽量减少数据丢失的可能:
min-slaves-to-write 1
min-slaves-max-lag 10