2020-08-22---redis基础篇.md

Redis相关

这篇是之前整理的… 现在直接发出去

内容主要来自于 : https://github.com/Snailclimb/JavaGuide

缓存的常用概念

缓存穿透

​ 缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。

缓存雪崩

​ 缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求

缓存击穿

​ 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

穿透 和 击穿 十分的相似

穿透 是指的 一种故意的行为 , 数据库和缓存中都没有

击穿 通常是因为缓存过期了 , 大量请求进入数据库.


Redis五种基本数据结构 / 以及基本操作

1. 字符串 (string)

redis对字符串定义了很多的 结构体, 为了对内存做到极致管理, 在比较短的时候, 使用byte 和 short记录长度和allac

基本操作 :

设置 和 获取值

  • set key value
  • get key

查询存在 和 删除key

  • exsits key (1 存在 0不存在)
  • del key (1 存在并且删除 , 0不存在)

批量设置 和 批量获得值

  • mset key1 value1 key2 values2 …
  • mget key1 key2 key3

过期 和 set命令扩展

  • expire key time(秒级单位)
  • setex key time value (等价于 set key value + expire key time)
  • setnx key value (如果key 不存在 就设置成功)

设置并且马上返回(返回的是原来的值 如果原来是空的 就是 nil)

  • getset key value

注意 : 在redis 底层, 使用的并不是 c的字符串类型, 而是一种新的结构体

2. 列表(list)

相当于java中的linkedlist , 注意它是链表 , 不是数组, 插入 和删除 非常快

但是索引定位就很慢了 底层使用的是一个双向链表

基本操作

LPUSH 和 RPUSH 分别可以向list左边 ,和 右边 插入一个元素

LPUSH list 元素 ;

LRANGE 命令 可以从list中取出一定范围的元素

LRANGE list start end : 注意 , 可以使用负数进行定位, 表示最后第几个元素 , -1是最后一个元素

LINDEX list index : 注意这个操作 很消耗时间, 最好不要这么干, 获得第几个元素

LLEN list : 计算长度 o1的方式获取, 很快

list实现队列

LPOP 和 RPOP : 从哪个方向 pop出元素

list实现栈 , 都是可以的

也是可以使用expire 设定过期时间的

使用 ttl 查看是否过期, 永久有效 是-1 . 删除了 -2 , 如果有时间 就显示时间

可以使用persist 来让 这个key 不过期


3. 字典 (hash)

相当java中的hashmap , 内部实现也差不多 , 通过 数组 + 链表的方式来解决 hash冲突

但是在扩容机制上不太一样,

首先 hashmap 的扩容阈值 是 在hash表中元素等于第一维数组的长度时 , 开始扩容,

而且使用的是渐进式的一种扩容, 扩容到原来的2倍 , 如果redis在bgsave的时候, 不会扩容, 除非到达了第一位数组的长度的5倍

同时也会因为元素的缩小而缩容量

条件是元素个数小于 数组长度的10%

综上 : hash结构的存储消耗高于单个字符串, 所以到底该使用hash还是字符串 , 需要考量

基本操作

设置 和 获取

  • HSET map field value : 设置值
  • HGET map field : 获得值

批量设置 和 获取

  • HGETALL map : 获取这个hash里面的所有内容 (上面是field_name 下面是value)
  • HMGET map field1 field2 : 获得这个hash 里的 field1 field2 字段
  • HMSET map fiedl1 value1 field2 value2 : 批量设置

长度 :

HLEN …


4. 集合 (set)

相当于Java语言的HashSet , 它的内部键值是无序的唯一的, 特殊的字典, 字典的value 都是NULL , 这和java设计是一样的(java是Object)

基本操作

批量增加

  • SADD value1 value 2 value 3

获得set所有元素

  • SMEMBERS set : 获得所有元素

判断是否存在 :

  • SisMember set value : 有就是1 没有是0

获得长度 : (和其他的有一些不一样)

  • SCARD set

弹出一个元素 : (好像是随机的)

  • SPOP set count(默认 1)

5. 有序集合 (zset)

类似java中的SortedSet 和 HashMap的结合体

内部实现其实使用的是 : 跳跃表 的数据结构, 比较复杂

它底层使用的是一个 score(分数) + value的形式存放, scope 用来索引 具体的插入位置, value具体的值

基本操作

添加元素 (集合操作都是批量的)

  • ZADD zset score1 value1 score2 values2

删除元素 :

  • ZREM zset value

获得zset的长度 :

  • ZCARD zset

列出元素 (score 顺序 是指 升序)

  • 顺序列出 : ZRANGE zset start end (同 list 的 range 方法, 可以为负数)

  • 逆序输出 : ZrevRANGE zset start end

  • 根据score 返回获得 value : zrangebyscore zset min max : 取得的是 [min,max]区间 也可以指定获得几个, 或者offset 偏移多少
    (-inf 表示最小值) (inf 表示最大值) 无穷小 , 无穷大

列出value的scope

  • ZSCORE zset value (如果没有这个value 就返回 nil)
  • ZRANK zset value (排列出这个value 的排名 从0 开始) 默认升序

HyperLogLog (可以当做是一种redis特殊的数据结构)

基数统计 : 一个HyperLogLog 只有 12KB

技术来源于生活, 它的这个东西是来自 投掷硬币 这个东西, 通过连续出现正面的次数来统计估计出现了多少次

取多个桶 的 调和平均数 来估计 基数

最大可以统计 2^64 数量


对于一个桶来说, 把存入的对象 转为一个 hash值, 这个hash 值 来填入桶里面

很多很多这样的对象存入桶中, 通过记录从后往前最大的连续0 来估算这个桶里面有多少个基数

对多个桶 取调和平均. 大概精准

在redis中, 使用了 2^14 个桶 , 每个桶的大小 为6bit , 可以用来记录 最大 maxbit = 63

相关操作

PFADD key elment … : 可以在这个 hyperLogLog 一次存多个数

PFCOUNT key : 得到这个 Log 里面 大概有多少基数

PFMEGER key1 key2 …key3 key 4 : 合并多个桶


布隆过滤器(rebloom)

原理挺简单的, 使用一个合理长度的bitmap 桶

对一个元素 , 使用多个 hash函数进行hash , 然后把得到的值对这个bitmap的长度 进去取余 , 在对应的位置 标记为1

因此 可以用同样的方式 对 一个 key进行判断 是否存在, 可以保证的是 一定不存在, 无法保证一定存在

  • 使用的时候, 不要让实际的远大于桶的大小

在 redis 4.0之后, 推出了一个 插件功能, 拜这个插件功能 , 布隆过滤器 成功的 放入了redis 的服务中

可以使用

docker pull redislabs/rebloom # 拉取镜像
docker run -p6379:6379 redislabs/rebloom # 运行容器
redis-cli # 连接容器中的 redis 服务

拉取镜像 , 来体验一下 这个过滤器

基本使用

添加元素 , 判断是否存在

  • 添加一个元素 : bf.add key element
  • 一下添加多个元素 : bf.madd key element1 element2 element3…
  • 判断一个元素是否存在 : bf.exists key element
  • 判断多个元素 : bf.mexists key elment1 element2 elemenet3

查看 这个bf的具体情况

  • bf.info key

自定义一个布隆过滤器

  • bf.reserve 方法 (有3个参数)
    • key , 就是 key
    • error_rate (错误率 , 这个应该是判断存在的错误率 , 默认为0.01 越低 需要的空间越大)
    • initial_size (预计的数量, 预计放入的元素个数)

redis数据淘汰机制

Redis 提供 6 种数据淘汰策略:

  1. volatile-lru(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.0 版本后增加以下两种:

  1. volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  2. allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

分布式锁 (Redis实现)

原文地址 : https://github.com/Snailclimb/JavaGuide/blob/master/docs/database/Redis/redis-collection/Redis(3)%E2%80%94%E2%80%94%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E6%B7%B1%E5%85%A5%E6%8E%A2%E7%A9%B6.md

由于Redis是单线程的, 因此所有的操作都是串行化的方式执行的 , 并且本身提供了像SETNX 这样的指令, 本身具有互斥性

可以将 锁 存储redis来让多个线程进行竞争, 来实现 一个分布式的锁 , 但是这个锁 会带来很多的问题, 比如 锁超时 , 单点/多点问题

运行步骤

  1. 首先让应用来获得这个redis的setNX 这样的指令, 如果调用成功, 则成功加上了分布式的锁
  2. 然后进行业务逻辑的处理
  3. 解锁的过程, 就是把这个 key 删除

锁超时

发生锁超时, 主要有3个可能

  • 服务A 的操作时间真多过长了, 超过了锁应该被释放的时间
  • 服务A 在程序执行的时候发生错误 , 或者机器宕机, 锁来不及释放
  • 服务A 在释放的锁的时候 , 因为网络的情况, 发生了丢失

因此, 在使用 redis 的分布锁的时候, 可以在锁上加上一个过期时间.

但是这回导致另外一个问题, 对于 2, 3两种可能, 这个方案是可行的, 能够保证程序的正确性

但是 对于第一种可能… 这就造成了多余一个用户在访问临界区.

所以一种比较安全的方法是 : 在获取锁的时候, 持有一下这个锁的value , 在释放的时候对比一下 是不是这个value的值, 能够匹配再还掉.

注意 : 在这个情况下, 依然是有问题的, 比如第一种情况下, 服务A和另外的服务可能同时持有这个锁, 释放锁的时候表现是正确的, 但是这不代表 这俩服务同时操作redis 变更是正确的 对于这个问题, 会在后文说如何解决.


单点 / 多点 问题

使用分布锁的情况, 如果redis 是一个单点应用…如果它崩溃, 这将导致 多个 乃至 整个系统的瘫痪

而如果采用主从模式部署,我们想象一个这样的场景:服务 A 申请到一把锁之后,如果作为主机的 Redis 宕机了,那么 服务 B 在申请锁的时候就会从从机那里获取到这把锁,为了解决这个问题,Redis 作者提出了一种 RedLock 红锁 的算法 (Redission 同 Jedis)

// 三个 Redis 集群
RLock lock1 = redissionInstance1.getLock("lock1");
RLock lock2 = redissionInstance2.getLock("lock2");
RLock lock3 = redissionInstance3.getLock("lock3");

RedissionRedLock lock = new RedissionLock(lock1, lock2, lock2);
lock.lock();
// do something....
lock.unlock();

什么是 RedLock

Redis 官方站这篇文章提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:

  1. 安全特性:互斥访问,即永远只有一个 client 能拿到锁
  2. 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
  3. 容错性:只要大部分 Redis 节点存活就可以正常提供服务
怎么在单节点上实现分布式锁

SET resource_name my_random_value NX PX 30000

主要依靠上述命令,该命令仅当 Key 不存在时(NX保证)set 值,并且设置过期时间 3000ms (PX保证),值 my_random_value 必须是所有 client 和所有锁请求发生期间唯一的,释放锁的逻辑是:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

上述实现可以避免释放另一个client创建的锁,如果只有 del 命令的话,那么如果 client1 拿到 lock1 之后因为某些操作阻塞了很长时间,此时 Redis 端 lock1 已经过期了并且已经被重新分配给了 client2,那么 client1 此时再去释放这把锁就会造成 client2 原本获取到的锁被 client1 无故释放了,但现在为每个 client 分配一个 unique 的 string 值可以避免这个问题。至于如何去生成这个 unique string,方法很多随意选择一种就行了。

Redlock 算法

算法很易懂,起 5 个 master 节点,分布在不同的机房尽量保证可用性。为了获得锁,client 会进行如下操作:

  1. 得到当前的时间,微秒单位
  2. 尝试顺序地在 5 个实例上申请锁,当然需要使用相同的 key 和 random value,这里一个 client 需要合理设置与 master 节点沟通的 timeout 大小,避免长时间和一个 fail 了的节点浪费时间
  3. 当 client 在大于等于 3 个 master 上成功申请到锁的时候,且它会计算申请锁消耗了多少时间,这部分消耗的时间采用获得锁的当下时间减去第一步获得的时间戳得到,如果锁的持续时长(lock validity time)比流逝的时间多的话,那么锁就真正获取到了。
  4. 如果锁申请到了,那么锁真正的 lock validity time 应该是 origin(lock validity time) - 申请锁期间流逝的时间
  5. 如果 client 申请锁失败了,那么它就会在少部分申请成功锁的 master 节点上执行释放锁的操作,重置状态
失败重试

如果一个 client 申请锁失败了,那么它需要稍等一会在重试避免多个 client 同时申请锁的情况,最好的情况是一个 client 需要几乎同时向 5 个 master 发起锁申请。另外就是如果 client 申请锁失败了它需要尽快在它曾经申请到锁的 master 上执行 unlock 操作,便于其他 client 获得这把锁,避免这些锁过期造成的时间浪费,当然如果这时候网络分区使得 client 无法联系上这些 master,那么这种浪费就是不得不付出的代价了。

放锁

放锁操作很简单,就是依次释放所有节点上的锁就行了

性能、崩溃恢复和 fsync

如果我们的节点没有持久化机制,client 从 5 个 master 中的 3 个处获得了锁,然后其中一个重启了,这是注意 整个环境中又出现了 3 个 master 可供另一个 client 申请同一把锁! 违反了互斥性。如果我们开启了 AOF 持久化那么情况会稍微好转一些,因为 Redis 的过期机制是语义层面实现的,所以在 server 挂了的时候时间依旧在流逝,重启之后锁状态不会受到污染。但是考虑断电之后呢,AOF部分命令没来得及刷回磁盘直接丢失了,除非我们配置刷回策略为 fsnyc = always,但这会损伤性能。解决这个问题的方法是,当一个节点重启之后,我们规定在 max TTL 期间它是不可用的,这样它就不会干扰原本已经申请到的锁,等到它 crash 前的那部分锁都过期了,环境不存在历史锁了,那么再把这个节点加进来正常工作。

红锁 , 感觉就是把锁的顺序规定下来, 让应用去逐一加锁 , 如果加锁 失败, 就释放之前所有的锁


redis持久化方式

持久化发生了什么?
  1. 客户端向数据发送写命令 (数据在客户端的内存中)
  2. 数据库 接受 到客户端的 写命令, (数据在服务器的内存中)
  3. 服务器 调用系统API 将数据写入磁盘 , (数据在内核缓冲区中)
  4. 操作系统 写缓冲区 到磁盘控制器 (数据在磁盘缓冲区中)
  5. 操作系统 的磁盘控制器将数据 写入实际的物理媒介中 (数据在磁盘中)

这个过程已经被 极度的简化了 , 缓存和缓冲区会比这更多

如何保证持久化的安全

如果仅仅是涉及到软件层面, 到达 第三步 就其实已经存储成功了

但是 对于更糟糕的危害, 比如断电 , 火灾等… 那么只有到了第5步 才是安全的

… 但是 , 对于 第4 和 第5步 . 操作系统只有提供给我们 第4步的接口调用

操作系统提供给我们 一个叫做 fsync 的接口, 强制内核将缓存区的数据写入磁盘中, 但是这个非常消耗资源的操作

而且还会阻塞, redis 通常是每隔1s就会执行一次

但是对于第五步 , 我们就没有任何办法去控制了, 需要多等多几个毫秒. (问题其实也不大)

Redis中的两种持久化方式
快照 (生成 .rdb文件)

快照是最简单的redis 持久化方式, 当满足特定条件 (可以自己设定) 就会生成一次数据快照

在这个过程中, 我们都知道 redis是一个单线程的应用 , 在快照的过程中, 要一边响应用户请求, 一边做持久化的工作

所以 redis 的主进程 会进行 使用 操作系统的fork 命令 (fork命令 会复制出一样的栈, 数据和程序都是一样的)

COW (copy on writer) 之后 让子线程进行持久化的操作, redis主线程 继续响应用户的请求

这一种方式, 我们看出了, 在线程进行fork 的时候, 会消耗大量的资源, cpu 空间等, 假设 redis中存储的数据 足够的大, 意味着 在fork的时候, 还要消耗同样大小的空间, 意味着需要留出一半空间来使用redis

AOF(append only file , 产生的是.aof文件)

这个文件 仅仅是记录对 redis存储能造成影响的指令, 像select 这种查询操作, 不会记录

注意 : 这里有个比较细节的地方, redis 在使用 aof的时候
当redis 接受到 写命令的时候, 会先去执行这个命令, 命令 正确才会 写入 aof文件中
但是其他的 , 比如 mysql这种文件系统的数据库, 会先写入日志中, 再执行

AOF 瘦身( 重新进行 aof )

redis 在长期的运行过程中, AOF日志会越来越大 ,如果宕机之后, 重做这些指令, 可能需要很长的时间.

对于这种情况, redis 提供了 bgrewriteaof指令,用于对AOF进行瘦身

它的原理是 :

  • 开辟一个子进程对内存进行遍历转换成一系列redis操作指令序列到一个新的AOF日志文件
  • 在这个过程中, 会有新的操作进来, 同时记录这些新的操作, 叫做增量AOF日志
  • 第一个过程结束之后, 再把第二个过程中的增量AOF进行追加操作
  • 最后把这个新的AOF代替掉旧的AOF 就完成了 瘦身过程

fsync (强制 刷新缓冲区, 就是相当于完成 第四步)

通常情况下 redis 1秒一次fsync 就足够了

redis4.0混合持久化

顾名思义 : 就是使用 快照 和 AOF 同时进行持久化的一种手段

快照 VS AOF

快照(.rdb)

  • 优点…速度快 , 因为它是二进制文件, 加载的速度远远比aof速度快
  • 缺点…间隔太长, 每次触发快照, 都需要满足某些条件, 满足之后 快照产生的过程很消耗资源

AOF

  • 优点…间隔短, 能够调用fsync 强行进行刷新 完成持久化… 可以进行人为调时间
  • 缺点… 恢复起来速度比较慢, 没有rdb这么快, 因为记录的都是操作, 相当于重做这些记录

一种混合的方式 就是 如果 redis 宕机重启, 那么可以先找到最近的一次快照, 进行批量的数据恢复

然后再使用AOF 对它进行一个增量的操作, 回到最近的一次数据情况


redis集群

redis 单体应用 如果发生宕机, 那将是非常恐怖的影响

而且, 单体redis 容易出现瓶颈, 比如内存, cpu 啊 啥的

对于 redis 的集群, 有3种通用的集群方式

  • 主从复制 : (结构最简单, 效果比较好的一种方式) 把写操作和读操作进行分离
    • 由主redis进行写操作, slave 进行读的请求操作
  • Redis哨兵(Sentinel)
    • 这一一种 对主从复制架构的 进一步优化, 使用一些哨兵对集群进行监控, 如果集群中的主数据库挂掉了, 那么我们可以让哨兵在从数据库中选择一个合适的redis , 让它变成主 , 提高第一种架构的可用性
  • 集群化(cluster)
    • 上面的这些架构, 都可以看成是一个 redis 实例,
    • 而集群化, 就是把好多个这样的redis 集群 在进行一个集群化, 大大提高高可用 , 可扩展, 分布式 , 容错率

主从复制

其实没啥好说的,

就一个主(负责写) … 一堆slave (负责读)

复制 : 有两种模式 , 第一种 主从复制, 也就是 主主动把数据同步给所有的slave

还有一种 是从从复制… 是后序版本推出的一种方案, 用来减轻主节点的同步负担


主从复制主要的作用

  • 数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复: 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 (实际上是一种服务的冗余)
  • 负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
  • 高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的 基础,因此说主从复制是 Redis 高可用的基础。

使用起来十分方便… 使用docker 启动几个 实例

redis可以进行动态的增加/删除从节点

在redis实例中, 使用SLAVEOF 命令就能进行 从机配置 . 如 SLAVEOF 127.0.0.1 6380

也可以删除这个从机节点, 只要指定 SLAVEOF no one

实现原理图 :

在这里插入图片描述

在使用slaveof 命令的时候, 可以 设置 一个密码进行安全的保护

在主节点 配置 requirepass 来设置密码

从节点中 , 使用masterauth 参数, 设置密码

具体操作

在主获得了一个写操作, 之后, 完成了该操作, 对从机进行 同步操作

在 redis2.8之前

使用的是 sync 命令 , 主要动作

  1. 主服务器 需要执行 BGSAVE 命令来生成 RDB 文件,这个生成操作会 消耗 主服务器大量的 CPU、内存和磁盘 I/O 的资源
  2. 主服务器 需要将自己生成的 RDB 文件 发送给从服务器,这个发送操作会 消耗 主服务器 大量的网络资源 (带宽和流量),并对主服务器响应命令请求的时间产生影响;
  3. 接收到 RDB 文件的 从服务器 需要载入主服务器发来的 RBD 文件,并且在载入期间,从服务器 会因为阻塞而没办法处理命令请求

意味着每次有写请求, 都要进行一个全局的数据快照 … 这个想想就很慢

在redis2.8之后

所以在 Redis 2.8 中引入了 PSYNC 命令来代替 SYNC,它具有两种模式:

  1. 全量复制: 用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作;
  2. 部分复制: 用于网络中断等情况后的复制,只将 中断期间主节点执行的写命令 发送给从节点,与全量复制相比更加高效。需要注意 的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制;

部分复制的原理主要是靠主从节点分别维护一个 复制偏移量,有了这个偏移量之后断线重连之后一比较,之后就可以仅仅把从服务器断线之后确实的这部分数据给补回来了。


Redis Sentinel 哨兵

针对 主从复制 的一个缺点… 主机只有一个…挂掉了就木大了…

sentinel 就是为了解决 在 主redis 挂掉的时候, 从好的从中选择出一个作为主

用于监视的 就称之为 哨兵 (Sentinel)

在这个架构中, 由两部分节点组成

  • 哨兵节点: 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据;
  • 数据节点: 主节点和从节点都是数据节点;

在复制的基础上,哨兵实现了 自动化的故障恢复 功能,下方是官方对于哨兵功能的描述:

  • 监控(Monitoring): 哨兵会不断地检查主节点和从节点是否运作正常。
  • 自动故障转移(Automatic failover):主节点 不能正常工作时,哨兵会开始 自动故障转移操作,它会将失效主节点的其中一个 从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
  • 配置提供者(Configuration provider): 客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。
  • 通知(Notification): 哨兵可以将故障转移的结果发送给客户端。

其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移。而配置提供者和通知功能,则需要在与客户端的交互中才能体现。


客户端访问哨兵系统代码演示

上面我们在 快速体验 中主要感受到了服务端自己对于当前主从节点的自动化治理,下面我们以 Java 代码为例,来演示一下客户端如何访问我们的哨兵系统:

public static void testSentinel() throws Exception {
         String masterName = "mymaster";
         Set<String> sentinels = new HashSet<>();
         sentinels.add("127.0.0.1:26379");
         sentinels.add("127.0.0.1:26380");
         sentinels.add("127.0.0.1:26381");
         
         // 初始化过程做了很多工作
         JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels); 
         Jedis jedis = pool.getResource();
         jedis.set("key1", "value1");
         pool.close();
}

客户端原理

Jedis 客户端对哨兵提供了很好的支持。如上述代码所示,我们只需要向 Jedis 提供哨兵节点集合和 masterName ,构造 JedisSentinelPool 对象,然后便可以像使用普通 Redis 连接池一样来使用了:通过 pool.getResource() 获取连接,执行具体的命令。

在整个过程中,我们的代码不需要显式的指定主节点的地址,就可以连接到主节点;代码中对故障转移没有任何体现,就可以在哨兵完成故障转移后自动的切换主节点。之所以可以做到这一点,是因为在 JedisSentinelPool 的构造器中,进行了相关的工作;主要包括以下两点:

  1. 遍历哨兵节点,获取主节点信息: 遍历哨兵节点,通过其中一个哨兵节点 + masterName 获得主节点的信息;该功能是通过调用哨兵节点的 sentinel get-master-addr-by-name 命令实现;
  2. 增加对哨兵的监听: 这样当发生故障转移时,客户端便可以收到哨兵的通知,从而完成主节点的切换。具体做法是:利用 Redis 提供的 发布订阅 功能,为每一个哨兵节点开启一个单独的线程,订阅哨兵节点的 + switch-master 频道,当收到消息时,重新初始化连接池。

sentinel 如何选择出一个新的主redis呢

简单来说 Sentinel 使用以下规则来选择新的主服务器:

  1. 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被 淘汰
  2. 在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被 淘汰
  3. 经历了以上两轮淘汰之后 剩下来的从服务器中, 我们选出 复制偏移量(replication offset)最大 的那个 从服务器 作为新的主服务器;如果复制偏移量不可用,或者从服务器的复制偏移量相同,那么 带有最小运行 ID 的那个从服务器成为新的主服务器。

redis集群化

集群中的每2个节点之间, 都有联系

它在网络拓扑结构里面 , 是一种去中心化的 强连通图结构

Redis 集群中内置了 16384 个哈希槽。当客户端连接到 Redis 集群之后,会同时得到一份关于这个 集群的配置信息,当客户端具体对某一个 key 值进行操作时,会计算出它的一个 Hash 值,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,Redis 会根据节点数量 大致均等 的将哈希槽映射到不同的节点。

​ 再结合集群的配置信息就能够知道这个 key 值应该存储在哪一个具体的 Redis 节点中,如果不属于自己管,那么就会使用一个特殊的 MOVED 命令来进行一个跳转,告诉客户端去连接这个节点以获取数据:

GET x
-MOVED 3999 127.0.0.1:6381

MOVED 指令第一个参数 3999key 对应的槽位编号,后面是目标节点地址,MOVED 命令前面有一个减号,表示这是一个错误的消息。客户端在收到 MOVED 指令后,就立即纠正本地的 槽位映射表,那么下一次再访问 key 时就能够到正确的地方去获取了。

集群的主要作用
  1. 数据分区: 数据分区 (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点,一方面 突破了 Redis 单机内存大小的限制,存储容量大大增加另一方面 每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有提及,例如,如果单机内存太大,bgsavebgrewriteaoffork 操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出……
  2. 高可用: 集群支持主从复制和主节点的 自动故障转移 (与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。

数据分区的方案

在上文中, 说过 redis集群中内置了16384 … 2^16次 的哈希槽

那么如何对这些槽进行规划呢 , 或者如何分区 才合理

方案一 : 哈希值 % 节点数

哈希取余分区思路非常简单:计算 key 的 hash 值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。

不过该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要 重新计算映射关系,引发大规模数据迁移。


方案二 : 一致性哈希分区

一致性哈希算法将 整个哈希值空间 组织成一个虚拟的圆环,范围是 [0 , 232-1],对于每一个数据,根据 key 计算 hash 值,确数据在环上的位置,然后从此位置沿顺时针行走,找到的第一台服务器就是其应该映射到的服务器:

在这里插入图片描述

与哈希取余分区相比,一致性哈希分区将 增减节点的影响限制在相邻节点。以上图为例,如果在 node1node2 之间增加 node5,则只有 node2 中的一部分数据会迁移到 node5;如果去掉 node2,则原 node2 中的数据只会迁移到 node4 中,只有 node4 会受影响。

一致性哈希分区的主要问题在于,当 节点数量较少 时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。还是以上图为例,如果去掉 node2node4 中的数据由总数据的 1/4 左右变为 1/2 左右,与其他节点相比负载过高。


**方案三 : 带有虚拟节点的一致性哈希分区 **

该方案在 一致性哈希分区的基础上,引入了 虚拟节点 的概念。Redis 集群使用的便是该方案,其中的虚拟节点称为 槽(slot)。槽是介于数据和实际节点之间的虚拟概念,每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。

在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽 解耦数据和实际节点 之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有 4 个实际节点,假设为其分配 16 个槽(0-15);

  • 槽 0-3 位于 node1;4-7 位于 node2;以此类推…

如果此时删除 node2,只需要将槽 4-7 重新分配即可,例如槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4;可以看出删除 node2 后,数据在其他节点的分布仍然较为均衡。


redis节点之间的通讯

在集群中, 每个redis实例之间, 都需要进行通讯

两个端口

一个是普通端口, 用来给客户端通讯的

一个是特殊的通信端口, 是普通端口 + 10000

节点间通信,按照通信协议可以分为几种类型:单对单、广播、Gossip 协议等。重点是广播和 Gossip 的对比。

  • 广播是指向集群内所有节点发送消息。优点 是集群的收敛速度快(集群收敛是指集群内所有节点获得的集群信息是一致的),缺点 是每条消息都要发送给所有节点,CPU、带宽等消耗较大。
  • Gossip 协议的特点是:在节点数量有限的网络中,每个节点都 “随机” 的与部分节点通信 (并不是真正的随机,而是根据特定的规则选择通信的节点),经过一番杂乱无章的通信,每个节点的状态很快会达到一致。Gossip 协议的 优点 有负载 (比广播) 低、去中心化、容错性高 (因为通信有冗余) 等;缺点 主要是集群的收敛速度慢。
消息类型

集群中的节点采用 固定频率(每秒10次)定时任务 进行通信相关的工作:判断是否需要发送消息及消息类型、确定接收节点、发送消息等。如果集群状态发生了变化,如增减节点、槽状态变更,通过节点间的通信,所有节点会很快得知整个集群的状态,使集群收敛。

节点间发送的消息主要分为 5 种:meet 消息ping 消息pong 消息fail 消息publish 消息。不同的消息类型,通信协议、发送的频率和时机、接收节点的选择等是不同的:

  • MEET 消息: 在节点握手阶段,当节点收到客户端的 CLUSTER MEET 命令时,会向新加入的节点发送 MEET 消息,请求新节点加入到当前集群;新节点收到 MEET 消息后会回复一个 PONG 消息。
  • PING 消息: 集群里每个节点每秒钟会选择部分节点发送 PING 消息,接收者收到消息后会回复一个 PONG 消息。PING 消息的内容是自身节点和部分其他节点的状态信息,作用是彼此交换信息,以及检测节点是否在线。PING 消息使用 Gossip 协议发送,接收节点的选择兼顾了收敛速度和带宽成本,具体规则如下:(1)随机找 5 个节点,在其中选择最久没有通信的 1 个节点;(2)扫描节点列表,选择最近一次收到 PONG 消息时间大于 cluster_node_timeout / 2 的所有节点,防止这些节点长时间未更新。
  • PONG消息: PONG 消息封装了自身状态数据。可以分为两种:第一种 是在接到 MEET/PING 消息后回复的 PONG 消息;第二种 是指节点向集群广播 PONG 消息,这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播 PONG 消息。
  • FAIL 消息: 当一个主节点判断另一个主节点进入 FAIL 状态时,会向集群广播这一 FAIL 消息;接收节点会将这一 FAIL 消息保存起来,便于后续的判断。
  • PUBLISH 消息: 节点收到 PUBLISH 命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该 PUBLISH 命令。

常见面试题

https://github.com/Snailclimb/JavaGuide/blob/master/docs/database/Redis/redis-all.md

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值