Redis
简介
Redis 缓存中间件。
支持的数据结构:
String、Hash、List、Set、SortedSet
HyperLogLog、Geo、Pub/Sub。
事务
生命周期
-
开启事务:使用MULTI开启一个事务
-
命令入队列:每次操作的命令都会加入到一个队列中,但命令此时不会真正被执行
-
提交事务:使用EXEC命令提交事务,开始顺序执行队列中的命令
事务的原子性
先看关系型数据库ACID 中关于原子性的定义:
原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
官方文档对事务的定义:
-
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
-
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。EXEC 命令负责触发并执行事务中的所有命令:如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,那么事务中的所有命令都不会被执行。另一方面,如果客户端成功在开启事务之后执行 EXEC ,那么事务中的所有命令都会被执行。
官方认为Redis事务是一个原子操作,这是站在执行与否的角度考虑的。但是从ACID原子性定义来看,严格意义上讲Redis事务是非原子型的,因为在命令顺序执行过程中,一旦发生命令执行错误Redis是不会停止执行然后回滚数据。
Redis为什么不支持回滚
在事务运行期间虽然Redis命令可能会执行失败,但是Redis依然会执行事务内剩余的命令而不会执行回滚操作。如果你熟悉mysql关系型数据库事务,你会对此非常疑惑,Redis官方的理由如下:
只有当被调用的Redis命令有语法错误时,这条命令才会执行失败(在将这个命令放入事务队列期间,Redis能够发现此类问题),或者对某个键执行不符合其数据类型的操作:实际上,这就意味着只有程序错误才会导致Redis命令执行失败,这种错误很有可能在程序开发期间发现,一般很少在生产环境发现。 支持事务回滚能力会导致设计复杂,这与Redis的初衷相违背,Redis的设计目标是功能简化及确保更快的运行速度。
对于官方的这种理由有一个普遍的反对观点:程序有bug怎么办?但其实回归不能解决程序的bug,比如某位粗心的程序员计划更新键A,实际上最后更新了键B,回滚机制是没法解决这种人为错误的。正因为这种人为的错误不太可能进入生产系统,所以官方在设计Redis时选用更加简单和快速的方法,没有实现回滚的机制。
事务失败处理
-
在事务提交之前:客户端执行的命令缓存(队列)失败,比如命令的语法错误(命令参数个数错误,不支持的命令等等)。如果发生这种类型的错误,Redis将向客户端返回包含错误提示信息的响应,同时Redis会清空队列中的命令并取消事务。
-
事务提交后:开始顺序执行命令,之前缓存在队列中的命令有可能执行失败。
-
乐观锁失败:事务提交时将丢弃之前缓存的所有命令序列。
事务相关命令
-
WATCH
可以为Redis事务提供 check-and-set (CAS)行为。被WATCH的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil来表示事务已经失败。
-
MULTI
用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行,而是被放到一个队列中,当 EXEC命令被调用时, 所有队列中的命令才会被执行。
-
UNWATCH
取消 WATCH 命令对所有 key 的监视,一般用于DISCARD和EXEC命令之前。如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。
-
DISCARD
当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空,并且客户端会从事务状态中退出。
-
EXEC
负责触发并执行事务中的所有命令。如果客户端成功开启事务后执行EXEC,那么事务中的所有命令都会被执行。
持久化
Persistence - 持久化,持久化的主要应用是将内存中的对象存储在数据库中,或者存储在磁盘文件中、XML数据文件中等等。
-
RDB持久化:能够在指定的时间间隔能对你的数据进行快照存储。
-
AOF持久化:记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾。Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
-
不使用持久化:如果你只希望你的数据在服务器运行的时候存在,你也可以选择不使用任何持久化方式。
-
同时开启RDB和AOF:你也可以同时开启两种持久化方式,在这种情况下当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
RDB持久化
RDB - Redis Database,持久化是把当前内存数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。
-
手动触发
手动触发对应
save
命令,会阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。 -
自动触发
自动触发对应
bgsave
命令,Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。在redis.conf配置文件中可以配置:
save <seconds> <changes>
表示xx秒内数据修改xx次时自动触发bgsave。 如果想关闭自动触发,可以在save命令后面加一个空串,即:
save ""
还有其他常见可以触发
bgsave
,如:-
如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。
-
默认情况下执行
shutdown
命令时,如果没有开启AOF持久化功能则 自动执行bgsave。
bgsave工作机制
-
-
执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进 程,如RDB/AOF子进程,如果存在,bgsave命令直接返回。
-
父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通 过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒
-
父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。
-
子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的 时间,对应info统计的rdb_last_save_time选项。
-
进程发送信号给父进程表示完成,父进程更新统计信息,具体见 info Persistence下的rdb_*相关选项。
AOF持久化
AOF - append only file,持久化:以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。
工作机制
开启AOF功能需要配置:appendonly yes,默认不开启。
AOF文件名 通过appendfilename
配置设置,默认文件名是appendonly.aof
。保存路径同 RDB持久化方式一致,通过dir
配置指定。
AOF的工作流程操作:命令写入-append
、文件同步-sync
、文件重写-rewrite
、重启加载-load
。
-
所有的写入命令会追加到aof_buf(缓冲区)中。
AOF为什么把命令追加到
aof_buf
中?Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。先写入缓冲区
aof_buf
中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。 -
AOF缓冲区根据对应的策略向硬盘做同步操作。
-
随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
-
当Redis服务器重启时,可以加载AOF文件进行数据恢复。
AOF重写(rewrite)机制
目的:
-
减小AOF文件占用空间;
-
更小的AOF 文件可以更快地被Redis加载恢复。
重写触发分类
-
手动触发:直接调用
bgrewriteaof
命令。 -
自动触发:根据
auto-aof-rewrite-min-size
和auto-aof-rewrite-percentage
参数确定自动触发时机。auto-aof-rewrite-min-size
:表示运行AOF重写时文件最小体积,默认 为64MB。auto-aof-rewrite-percentage
:代表当前AOF文件空间 (aof_current_size
)和上一次重写后AOF文件空间(aof_base_size
)的比值。
自动触发时机
当aof_current_size
> auto-aof-rewrite-minsize
并且(aof_current_size
- aof_base_size
)/ aof_base_size
>= auto-aof-rewritepercentage
。
其中aof_current_size
和aof_base_size
可以在info Persistence
统计信息中查看。
AOF文件重写后为什么会变小?
-
旧的AOF文件含有无效的命令,如:del key1, hdel key2等。重写只保留最终数据的写入命令。
-
多条命令可以合并,如lpush list a,lpush list b,lpush list c可以直接转化为lpush list a b c。
数据恢复流程
-
AOF持久化开启且存在AOF文件时,优先加载AOF文件。
-
AOF关闭或者AOF文件不存在时,加载RDB文件。
-
加载AOF/RDB文件成功后,Redis启动成功。
-
AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。
RDB与AOF比较
RDB优点
-
RDB 是一个非常紧凑的文件,它保存了某个时间点的数据集,非常适用于数据集的备份,比如你可以在每个小时报保存一下过去24小时内的数据,同时每天保存过去30天的数据,这样即使出了问题你也可以根据需求恢复到不同版本的数据集。
-
RDB 是一个紧凑的单一文件,很方便传送到另一个远端数据中心,非常适用于灾难恢复。
-
RDB 在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他 IO 操作,所以 RDB 持久化方式可以最大化 Redis 的性能。
-
与AOF相比,在恢复大的数据集的时候,RDB 方式会更快一些。
AOF优点
-
你可以使用不同的 fsync 策略:无 fsync、每秒 fsync 、每次写的时候 fsync .使用默认的每秒 fsync 策略, Redis 的性能依然很好( fsync 是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失1秒的数据。
-
AOF文件是一个只进行追加的日志文件,所以不需要写入seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用redis-check-aof工具修复这些问题。
-
Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
-
AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
RDB缺点
-
Redis 要完整的保存整个数据集是一个比较繁重的工作,你通常会每隔5分钟或者更久做一次完整的保存,万一在 Redis 意外宕机,你可能会丢失几分钟的数据。
-
RDB 需要经常 fork 子进程来保存数据集到硬盘上,当数据集比较大的时候, fork 的过程是非常耗时的,可能会导致 Redis 在一些毫秒级内不能响应客户端的请求。
AOF缺点
-
对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
-
数据恢复(load)时AOF比RDB慢,通常RDB 可以提供更有保证的最大延迟时间。
线程模型
redis 内部使用文件事件处理器 file event handler
,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。以下是单线程模式效率仍然高的原因:
-
纯内存操作
-
核心是基于非阻塞的 IO 多路复用机制
-
单线程反而避免了多线程的频繁上下文切换问题
缓存XX
-
缓存穿透
指用户请求的数据在缓存中不存在,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍。如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至击垮数据库系统。
解决方案
-
布隆过滤器 -
Bloom Filter
布隆过滤器专门用来检测集合中是否存在特定的元素。
设计思想
布隆过滤器由一个长度为m比特的位数组(
bit array
)与k个哈希函数(hash function
)组成的数据结构。位数组初始化均为0,所有的哈希函数都可以分别把输入数据尽量均匀地散列。当要向布隆过滤器中插入一个元素时,该元素经过k个哈希函数计算产生k个哈希值,以哈希值作为位数组中的下标,将所有k个对应的比特值由0置为1。
当要查询一个元素时,同样将其经过哈希函数计算产生哈希值,然后检查对应的k个比特值:如果有任意一个比特为0,表明该元素一定不在集合中;如果所有比特均为1,表明该集合有可能性在集合中。为什么不是一定在集合中呢?因为不同的元素计算的哈希值有可能一样,会出现哈希碰撞,导致一个不存在的元素有可能对应的比特位为1,这就是所谓“假阳性”(
false positive
)。相对地,“假阴性”(false negative
)在BF中是绝不会出现的。优点:
-
节省空间
不需要存储数据本身,只需要存储数据对应hash比特位
-
时间复杂度低
插入和查找的时间复杂度都为O(k),k为哈希函数的个数
缺点:
-
存在假阳性
布隆过滤器判断存在,可能出现元素不在集合中;判断准确率取决于哈希函数的个数
-
不能删除元素
如果一个元素被删除,但是却不能从布隆过滤器中删除,这也是造成假阳性的原因了
-
-
返回空对象
当缓存未命中,查询持久层也为空,可以将返回的空对象写到缓存中,这样下次请求该key时直接从缓存中查询返回空对象,请求不会落到持久层数据库。为了避免存储过多空对象,通常会给空对象设置一个过期时间。这种方法会存在两个问题:
-
如果有大量的key穿透,缓存空对象会占用宝贵的内存空间。
-
空对象的key设置了过期时间,在这段时间可能会存在缓存和持久层数据不一致的场景。
-
-
-
缓存击穿
指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,数据库瞬时压力骤增,造成大量请求阻塞。
解决方案
-
互斥锁 -
mutex key
如果是分布式应用就需要使用分布式锁。
让一个线程回写缓存,其他线程等待回写缓存线程执行完,重新读缓存即可。同一时间只有一个线程读数据库然后回写缓存,其他线程都处于阻塞状态。如果是高并发场景,大量线程阻塞势必会降低吞吐量。
-
热点数据永不过期
永不过期实际包含两层意思:
-
物理不过期
针对热点key不设置过期时间
-
逻辑不过期
把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建
从实战看这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,对于不追求严格强一致性的系统是可以接受的。
-
-
-
缓存雪崩
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,请求直接落到数据库上,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是h很多数据都过期了,很多数据都查不到从而查数据库。
解决方案
-
均匀过期
设置不同的过期时间,让缓存失效的时间点尽量均匀。通常可以为有效期增加随机值或者统一规划有效期。
-
加互斥锁
跟缓存击穿解决思路一致,同一时间只让一个线程构建缓存,其他线程阻塞排队。
-
缓存永不过期
跟缓存击穿解决思路一致,缓存在物理上永远不过期,用一个异步的线程更新缓存。
-
双层缓存策略
使用主备两层缓存
主缓存:有效期按照经验值设置,设置为主读取的缓存,主缓存失效后从数据库加载最新值。
备份缓存:有效期长,获取锁失败时读取的缓存,主缓存更新时需要同步更新备份缓存。
-
-
缓存预热
系统上线后,将相关的缓存数据直接加载到缓存系统,这样就可以避免在用户请求的时候,先查询数据库,然后再将数据回写到缓存。如果不进行预热, 那么 Redis 初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
缓存预热的操作方法
-
数据量不大的时候,工程启动的时候进行加载缓存动作;
-
数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
-
数据量太大的时候,优先保证热点数据进行提前加载到缓存。
-
-
缓存降级
指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。
在项目实战中通常会将部分热点数据缓存到服务的内存中,这样一旦缓存出现异常,可以直接使用服务的内存数据,从而避免数据库遭受巨大压力。降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。
内存淘汰策略
内存淘汰策略是指当缓存内存不足时,通过淘汰旧数据处理新加入数据选择的策略。Redis最大占用内存用完之后如果继续添加数据,有以下八种处理策略。
-
noeviction
默认策略,对于写请求直接返回错误,不进行淘汰。
-
allkeys-lru
lru(
less recently used
), 最近最少使用。从所有的key中使用近似LRU算法进行淘汰。 -
volatile-lru
最近最少使用。从设置了过期时间的key中使用近似LRU算法进行淘汰。
-
allkeys-random
从所有的key中随机淘汰。
-
volatile-random
从设置了过期时间的key中随机淘汰。
-
volatile-ttl
ttl(
time to live
),在设置了过期时间的key中根据key的过期时间进行淘汰,越早过期的越优先被淘汰。 -
allkeys-lfu
lfu(
Least Frequently Used
),最少使用频率。从所有的key中使用近似LFU算法进行淘汰。从Redis4.0开始支持。 -
volatile-lfu
最少使用频率。从设置了过期时间的key中使用近似LFU算法进行淘汰。从Redis4.0开始支持。
当使用
volatile-lru
、volatile-random
、volatile-ttl
这三种策略时,如果没有设置过期的key可以被淘汰,则和noeviction一样返回错误。
淘汰算法
LRU在Redis中的实现
LRU(
Least Recently Used
),即最近最少使用,是一种缓存置换算法。在使用内存作为缓存的时候,缓存的大小一般是固定的。当缓存被占满,这个时候继续往缓存里面添加数据,就需要淘汰一部分老的数据,释放内存空间用来存储新的数据。这个时候就可以使用LRU算法了。其核心思想是:如果一个数据在最近一段时间没有被用到,那么将来被使用到的可能性也很小,所以就可以被淘汰掉。
Redis使用的是近似LRU算法,它跟常规的LRU算法还不太一样。近似LRU算法通过随机采样法淘汰数据,每次随机出5个(默认)key,从里面淘汰掉最近最少使用的key。可以通过maxmemory-samples
参数修改采样数量, 如:maxmemory-samples 10
,maxmenory-samples
配置的越大,淘汰的结果越接近于严格的LRU算法,但因此耗费的CPU也很高。Redis为了实现近似LRU算法,给每个key增加了一个额外增加了一个24bit的字段,用来存储该key最后一次被访问的时间。
Redis3.0对近似LRU的优化
Redis3.0对近似LRU算法进行了一些优化。新算法会维护一个候选池(大小为16),池中的数据根据访问时间进行排序,第一次随机选取的key都会放入池中,随后每次随机选取的key只有在访问时间小于池中最小的时间才会放入池中,直到候选池被放满。当放满后,如果有新的key需要放入,则将池中最后访问时间最大(最近被访问)的移除。当需要淘汰的时候,则直接从池中选取最近访问时间最小(最久没被访问)的key淘汰掉就行。
LFU算法
LFU(Least Frequently Used
),是Redis4.0新加的一种淘汰策略,它的核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。LFU算法能更好的表示一个key被访问的热度。假如你使用的是LRU算法,一个key很久没有被访问到,只刚刚是偶尔被访问了一次,那么它就被认为是热点数据,不会被淘汰,而有些key将来是很有可能被访问到的则被淘汰了。如果使用LFU算法则不会出现这种情况,因为使用一次并不会使一个key成为热点数据。
主从复制
指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
主从复制的作用
-
数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
-
故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
-
负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
-
高可用基石:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
主从复制实现原理
-
连接建立
-
保存主节点信息
slaveof
命令是异步的,在从节点上执行slaveof
命令,从节点立即向客户端返回ok,从节点服务器内部维护了两个字段,即masterhost
和masterport
字段,用于存储主节点的ip和port信息。 -
建立socket连接
从节点每秒1次调用复制定时函数
replicationCron()
,如果发现了有主节点可以连接,便会根据主节点的ip
和port
,创建socket连接。从节点为该socket建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收RDB文件、接收命令传播等。主节点接收到从节点的
socket
连接后(即accept
之后),为该socket
创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。
-
发送ping命令
从节点成为主节点的客户端之后,发送ping命令进行首次请求,目的是:检查socket连接是否可用,以及主节点当前是否能够处理请求。从节点发送ping命令后,可能出现3种情况:
-
返回pong:说明socket连接正常,且主节点当前可以处理请求,复制过程继续。
-
超时:一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连。
-
返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开socket连接,并重连。
-
-
身份验证
如果从节点中设置了
masterauth
选项,则从节点需要向主节点进行身份验证;没有设置该选项,则不需要验证。从节点进行身份验证是通过向主节点发送auth
命令进行的,auth
命令的参数即为配置文件中的masterauth
的值。如果主节点设置密码的状态,与从节点
masterauth
的状态一致(一致是指都存在,且密码相同,或者都不存在),则身份验证通过,复制过程继续;如果不一致,则从节点断开socket连接,并重连。 -
发送从节点端口信息
身份验证之后,从节点会向主节点发送其监听的端口号(前述例子中为6380),主节点将该信息保存到该从节点对应的客户端的
slave_listening_port
字段中;该端口信息除了在主节点中执行info Replication
时显示以外,没有其他作用。
-
Sentinel - 哨兵模式
Redis 的主从复制模式下,一旦主节点由于故障不能提供服务,需要手动将从节点晋升为主节点,同时还要通知客户端更新主节点地址。Redis 2.8 以后提供了 Redis Sentinel
哨兵机制来解决这个问题。Redis Sentinel 是 Redis 高可用的实现方案。Sentinel 是一个管理多个 Redis 实例的工具,它可以实现对 Redis 的监控、通知、自动故障转移。
哨兵模式的原理
哨兵模式的主要作用在于它能够自动完成故障发现和故障转移,并通知客户端,从而实现高可用。哨兵模式通常由一组 Sentinel 节点和一组(或多组)主从复制节点组成。
心跳机制
-
Sentinel与Redis Node
Redis Sentinel 是一个特殊的 Redis 节点。在哨兵模式创建时,需要通过配置指定 Sentinel 与 Redis Master Node 之间的关系,然后 Sentinel 会从主节点上获取所有从节点的信息,之后 Sentinel 会定时向主节点和从节点发送 info 命令获取其拓扑结构和状态信息。
-
Sentinel与Sentinel
基于 Redis 的订阅发布功能, 每个 Sentinel 节点会向主节点的 sentinel:hello 频道上发送该 Sentinel 节点对于主节点的判断以及当前 Sentinel 节点的信息 ,同时每个 Sentinel 节点也会订阅该频道, 来获取其他 Sentinel 节点的信息以及它们对主节点的判断。
通过以上两步所有的 Sentinel 节点以及它们与所有的 Redis 节点之间都已经彼此感知到,之后每个 Sentinel 节点会向主节点、从节点、以及其余 Sentinel 节点定时发送 ping 命令作为心跳检测, 来确认这些节点是否可达。
故障转移
每个 Sentinel 都会定时进行心跳检查,当发现主节点出现心跳检测超时的情况时,此时认为该主节点已经不可用,这种判定称为主观下线。之后该 Sentinel 节点会通过 sentinel ismaster-down-by-addr
命令向其他 Sentinel 节点询问对主节点的判断, 当 quorum(法定人数) 个 Sentinel 节点都认为该节点故障时,则执行客观下线,即认为该节点已经不可用。这也同时解释了为什么必须需要一组 Sentinel 节点,因为单个 Sentinel 节点很容易对故障状态做出误判。
这里 quorum 的值是我们在哨兵模式搭建时指定的,后文会有说明,通常为 Sentinel节点总数/2+1,即半数以上节点做出主观下线判断就可以执行客观下线。
因为故障转移的工作只需要一个 Sentinel 节点来完成,所以 Sentinel 节点之间会再做一次选举工作, 基于 Raft 算法选出一个 Sentinel 领导者来进行故障转移的工作。被选举出的 Sentinel 领导者进行故障转移的具体步骤如下:
-
在从节点列表中选出一个节点作为新的主节点
-
过滤不健康或者不满足要求的节点;
-
选择
slave-priority
(优先级)最高的从节点, 如果存在则返回, 不存在则继续; -
选择复制偏移量最大的从节点 , 如果存在则返回, 不存在则继续;
-
选择
runid
最小的从节点。
-
-
Sentinel 领导者节点会对选出来的从节点执行
slaveof no one
命令让其成为主节点。 -
Sentinel 领导者节点会向剩余的从节点发送命令,让他们从新的主节点上复制数据。
-
Sentinel 领导者会将原来的主节点更新为从节点, 并对其进行监控, 当其恢复后命令它去复制新的主节点。
Cluster - 集群
引入Cluster模式的原因: 不管是主从模式还是哨兵模式都只能由一个master在写数据,在海量数据高并发场景,一个节点写数据容易出现瓶颈,引入Cluster模式可以实现多个节点同时写数据。
Redis-Cluster 采用无中心结构,每个节点都保存数据,节点之间互相连接从而知道整个集群状态。
如图所示Cluster模式其实就是多个主从复制的结构组合起来的,每一个主从复制结构可以看成一个节点,那么上面的Cluster集群中就有三个节点。
与Memcache的区别
-
存储方式
Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。
Redis有部份存在硬盘上,这样能保证数据的持久性。
-
数据支持类型
Memcache对数据类型支持相对简单。
Redis有丰富的数据类型。
-
使用底层模型不同
它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。
Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。