Redis——简单总结

611 阅读34分钟

Redis 和 Memcached 的区别和共同点

共同点

  1. 都是基于内存的数据库,一般都用来当做缓存使用。
  2. 都有过期策略。
  3. 两者的性能都非常高。

区别

  1. Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
  2. Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用, 所以有容灾恢复机制。而 Memecache 把数据全部存在内存之中。一旦重启,数据将都会丢失。
  3. Memcached 是多线程,非阻塞 I/O 复用的网络模型;Redis 使用单线程的多路 I/O 复用模型。
  4. Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
  5. Memcached过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
  6. Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的.

Redis—文件事件处理器

Redis 基于 Reactor 模式来设计开发了自己的一套高效的事件处理模型 ,叫文件事件处理器 ,并且这个文件事件处理器是单线程的,所以我们一般说 redis 是单线程的。

Redis采用 IO 多路复用机制来同时监听多个 socket,并且将产生事件的socket放入到队列中,然后每次向文件事件分派器发送一个socket,文件事件分派器根据事件类型调用相应的事件处理器来处理,处理完返回,然后I/O多路复用程序才会继续向文件事件分派器传送下一个socket。

为啥 redis 单线程模型也能效率这么高?

  • 纯内存操作。
  • I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。因为Redis 服务中运行的绝大多数操作的性能瓶颈都不是 CPU,而是内存和网络。
  • 单线程反而避免了多线程的频繁上下文切换问题,多线程就会存在死锁等问题,甚至会影响性能。

缓存穿透

啥是缓存穿透? 如果查询数据的时候,在redis查不到,然后再去mysql中查询,这种现象叫缓存穿透,缓存穿透是很常见的,一般来说,我们怕的不是低频的缓存穿透,而是高频的缓存穿透。比如说,一个黑客恶意的访问数据库不存在的id,这就可能出现高频的缓存穿透。

如何解决? 在redis和mysql的中间加一个布隆过滤器。布隆过滤器是一个通过错误率来换取空间的数据标识算法,原理就是先利用布隆过滤器的多个hash算法计算mysql中的id的多个hash值,转换成下标,在布隆过滤器中标识为1。然后如果这个时候要查询一个key,我们也通过相同的几个hash算法来计算,查看对应的索引下表是否都为1,只要有一个不为1,就认为key不在mysql中,直接return。只需要很小的内存空间就能建立一个这样的布隆过滤器来解决缓存穿透问题。


缓存击穿

啥是缓存击穿? 缓存击穿实质上是缓存穿透的一种特殊表现形式。比如说redis中有一个热点key一直承载着大量的请求,如果在某一时刻,这个key失效了,那么访问这个热点key的请求会一瞬间都打向mysql,可能会造成mysql的宕机。这个现象就叫缓存击穿。

如何解决?(一般中小型不需要解决)

  • 将热点Key不设置失效时间。
  • 还有个方法就是加分布式锁,这样就保证同一时间只有一个请求能访问这个Key。但是并发越大,效率问题越明显。
  • 将mysql中查询出来的数据,缓存到redis中。

缓存雪崩

啥是缓存雪崩?

缓存雪崩实质也是缓存穿透的特殊表现形式。比如说我们给大量的热点数据设置相同的失效时间,也就是说当失效时间过的那一瞬间,它们还是热门数据,这个时候就会有大量的请求穿过redis打到mysql上,这样的现象叫做缓存雪崩。

如何解决? 最简单的方法就是给缓存设置失效时间的时候,给它们加上一个随机因子,让它们不在同一时间失效。

setRedis(Key,value,time + Math.random() * 10000);


Redis的过期删除策略

定期删除:隔一段时间,就会从设置了过期时间的key中随机选择一些检查是否过期,过期了就直接删除。

惰性删除:当用户查询这个key的时候,检查一下key是否过期,如果过期了,就什么也不返回,然后将key删除。


内存淘汰机制?

  • noeviction:当内存不足时新写入数据会报错,不会淘汰任何键

  • volatile-ttl:从设置了过期时间的键中淘汰马上就要过期的键

  • allkeys-lru:当内存不足以容纳新写入数据时,移除最近最少使用的 key(这个是最常用的)

  • volatile-lru:从已设置过期时间的键中挑选最近最少使用的key淘汰

  • allkeys-random:从所有键中随机选择key淘汰

  • volatile-random:从已设置过期时间的键中随机选择key淘汰

  • allkeys-lfu:当内存不足以容纳新写入数据时,移除最不经常使用的 key

  • volatile-lfu:从已设置过期时间的键中挑选最不经常使用的key淘汰

    后两个是4.0版本新增


五种基本数据类型

String

如何实现

  • 底层由**简单动态字符串(SDS)**来实现的,因为我们会经常对redis里的值进行变动,所以我们需要一个动态可变的字符串的数据结构,所以redis自己构建了一个叫SDS的抽象类型,用作redis的默认字符串的表示。
  • SDS沿用了C语言的空字符结尾的惯例,是为了可以直接重用一部分C字符串函数库中的函数。
  • SDS与C字符串的区别:
    • SDS获取字符串长度是时间复杂度是常数级的:SDS的len属性就保存着字符串的长度。
    • 可以杜绝缓冲区溢出:对C字符串进行修改的时候可能造成缓冲区的溢出,如果对SDS进行修改的话,SDS会自动扩容至所需的大小,这样不会造成缓冲区的溢出。
    • 可以减少修改字符串时带来的内存重分配次数:
      • C字符串进行字符串的增加和缩减的时候都要对内存进行重新分配,不然会造成缓冲区溢出或是内存泄漏的问题。
      • SDS有两种优化策略:
        • 空间预分配:用于优化SDS的字符串增长的操作,对字符串进行修改的时候,不仅会满足SDS分配修改所需要的空间,还会有一个未使用空间(SDS的长度小于1MB时,free和len的长度一致;大于1MB时,会分配1MB的未使用空间。注:扩展空间的时候先检查未使用空间是否够用,不够时才会执行内存重分配操作。)
        • 惰性空间释放:用于优化SDS的字符串缩短的操作。当缩短SDS时,不立即回收多出来的字节,利用free来记录回收的字节数,便于下次使用,它也提供了相应的API当我们有需要的时候,帮我们释放。
    • SDS是二进制安全的:C字符串必须符合编码规范,并且其中不能出现‘\0’,不然会被误识别为结尾,所以只能保存文本数据,而无法保存图片,视频这样的二进制文件。而SDS都会以二进制的方式来处理存放在buf里面的数据,保证了写入时什么样子,读取时就是什么样子的。
C字符串SDS
获取字符串长度的时间复杂度O(N)O(1)
API是不安全的,可能会造成缓冲区溢出或是内存泄漏API是安全的,不会造成这些问题
每修改一次字符串就要进行一次内存重新分配两种优化策略可以减少内存分配的次数
只能保存文本数据可以保存文本和二进制数据,二进制安全

有何用途

  • 存放字符串的key和value,当做缓存
  • 可以直接进行数值计算:用于限流,秒杀场景
  • 因为可以存放二进制,所以可以用于统计,比如说:统计用户任意时间窗口内登陆的天数,统计时间窗口活跃用户数量等。

List

如何实现: ZipList或是LinkedList,列表对象保存的所有字符串元素的长度都小于64字节且元素数量小于512个,用ZipList,其余的LinkedList。 有何用途:

  • 代替一些数据结构,比如说:栈,队列,数组等
  • 用于帖子和评论的实现

Hash

如何实现:
Hash也叫字典,底层通过哈希表实现的。一个字典有两个哈希表ht[0]和ht[1],ht[0]平时使用,ht[1]是在rehash的时候才会使用。如果要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。

  • 扩容操作:ht[1]的大小为第一个大于等于ht[0].used*2的2的整数幂。
  • 缩容操作:ht[1]的大小为第一个大于等于ht[0].used的2的整数幂。

rehash操作不是一次性完成的,它是多次性,渐进式完成的。 哈希算法:MurmurHash算法 哈希冲突:用链地址法解决 有何用途:

  • 详情页,聚合

Set

如何实现: IntSet或是HashTable 有何用途:

  • 抽奖(srandmember key 3, srandmember key -4, 不重复抽奖 正数;重复抽奖 负数)
  • 随机事件
  • 推荐系统(集合操作)
    • 差集:推荐可能认识的人
    • 交集:共同好友
    • 并集:大家的好友圈

ZSet

如何实现: 底层由ziplist或skiplist实现,数据较少的情况下使用的ziplist,数据变多或是某个节点很大的时候转为skiplist。大部分情况下,跳跃表的效率和平衡树差不多,并且它的实现比平衡树更加简单。

跳跃表:对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳跃表是基于有序链表的扩展,是一个用于快速查找的多层链结构。是一种以空间换时间的算法实现。有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要局部锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 O(logn) 所以在并发数据结构中。跳表的本质是同时维护了多个链表,并且链表是分层的,最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。跳表内的所有链表的元素都是排序的。查找时,可以从最上层链表开始找。一旦发现要查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,它这种搜索是跳跃式的。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。

有何用途:

  • 排行榜(自身是从小到大的,使用时应使用反向指令)
  • 分页

skiplist与平衡树

  • 内存占用上来说,skiplist比平衡树更灵活一些,不是非常占用内存。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
  • 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
  • 性能考虑,平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
  • 从算法实现难度上来比较,skiplist比平衡树要简单得多。

Redis的持久化

RDB

它是用来持久化redis的一种方式,像是一种快照的形式。它有两种方式来生成RDB文件,一个是SAVE(会阻塞服务器进程),另一个是BGSAVE(fork一个子进程来处理)。除了手动的输入命令来生成RDB文件之外,可以通过配置让服务器每隔一段时间执行一次BGSAVE命令。比如说下面的配置:

  • save 900 1
  • save 300 10
  • save 60 10000

主要是通过两个参数来检查的:dirty计数器(距离上一次SAVE或BGSAVE之后有多少次的修改)和lastsave属性(记录上一次SAVE后BGSAVE的时间戳)。 然后RDB文件在服务器启动的时候就会自动的加载执行。

AOF

它是以追加文件的方式来持久化数据的。工作方式非常简单:每次执行修改内存数据的写操作时,都会记录该操作。假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例顺序执行所有的指令,也就是「重放」,来恢复 Redis 当前实例的内存数据结构的状态。 AOF重写:Redis 在长期运行的过程中,AOF 日志会越变越长。如果实例宕机重启,重放整个AOF日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志进行重写。Redis 提供了 bgrewriteaof 指令用于对 AOF 日志重写。其原理就是 fork一个子进程对内存进行遍历转换成一系列Redis的操作指令,序列化到一个新的AOF日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件。为了解决在子线程重写AOF的时候,服务器进程还在继续处理请求,这时候产生的命令会使AOF文件与数据库的状态不一致。所以Redis服务器设置了一个AOF重写缓冲区,在子线程重写的时候,新产生的命令会添加到这个AOF缓冲区和AOF重写缓冲区,然后重写完之后再把AOF重写缓冲区的内容写入重写的AOF文件中。 :当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核的缓存中,然后内核会异步将脏数据刷回到磁盘的。所以我们需要fsync函数来将AOF日志内容强制从内核缓存刷到磁盘。但是fsync是一个很消耗资源的一个过程!通常来说,Redis 每隔 1s 左右执行一次 fsync 操作就可以了。其实一共有三种策略。

  • no:不主动fsync,让操作系统来决定同步磁盘,不安全。
  • everysec:每秒fsync一次,默认的折中策略。
  • always:每次写一个指令就要fsync一次,效率很慢。

Redis分布式锁

分布式锁是什么

  • 分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现。如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此干扰。

思路

  • 在整个系统提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。

分布锁设计目的

  • 可以保证在分布式部署的应用集群中,同一个方法在同一操作只能被一台机器上的一个线程执行。

Redis的分布式锁的几个要点

  • 要用redis2.8优化过的 SET key value NX PX milliseconds 命令。如果不用,先设置了值,再设置过期时间,这个不是原子性操作,有可能在设置过期时间之前宕机,会造成死锁(Key 永久存在)。
  • Value 要具有唯一性。这个是为了在解锁的时候,需要验证 Value 是和加锁的一致才删除锁。

缺点

  • 如果采用单机部署模式,会存在单点问题,只要 Redis 故障了。加锁就不行了。
  • 采用主从模式,加锁的时候只对一个节点加锁,即便通过 Sentinel 做了高可用,但是如果 Master 节点故障了,发生主从切换,此时就会有可能出现锁丢失的问题。
  • 还有就是过期时间的设置问题,过长会导致单位时间里能卖出的数量变少,过短会导致一物多买,发生超卖问题。

RedLock 假设 Redis 的部署模式是 Redis Cluster,总共有 5 个 Master 节点。通过以下步骤获取一把锁:

  • 获取当前时间戳,单位是毫秒。
  • 轮流尝试在每个 Master 节点上创建锁,过期时间设置较短,一般就几十毫秒。
  • 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点(n / 2 +1)。
  • 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了。
  • 要是锁建立失败了,那么就依次删除这个锁。

但是这样的这种算法还是颇具争议的,可能还会存在不少的问题,无法保证加锁的过程一定正确。

**Redisson ** Redisson解决了redis分布式锁的过期时间设置问题,它考虑了很多细节:

  • Redisson 所有指令都通过 Lua 脚本执行,Redis 支持 Lua 脚本原子性执行。
  • Redisson 设置一个 Key 的默认过期时间为 30s,如果某个客户端持有一个锁超过了 30s 怎么办?Redisson 中有一个 Watchdog 的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔 10s 帮你把 Key 的超时时间设为 30s。这样的话,就算一直持有锁也不会出现 Key 过期了,其它线程获取到锁的问题了。
  • Redisson 的“看门狗”逻辑保证了没有死锁发生。(如果机器宕机了,看门狗也就没了。此时就不会延长 Key 的过期时间,到了 30s 之后就会自动过期了,其它线程可以获取到锁)

Zookeeper分布式锁

ZooKeeper可以是一个分布式锁一致性服务,它主要的特点有:

  • 可以以文件系统的方式去储存数据。
  • 可以创建多种类型的节点,比如说持久节点、临时节点、持久有序节点、临时有序节点
  • 有事件回调机制。

Zookeeper分布式锁基本也是围绕着它这三个特点来实现的,具体实现过程如下: 每当一个客户端连上一个服务器节点,zookeeper就创建一个临时有序节点,这个节点是全局有顺序且唯一的,就好像一把锁。因为是有序节点,所以每个客户端就像是在排队访问资源。并且只有顺序最小的目录节点才可以访问资源,当客户端发现自己的节点不是最小的时候,就会在它前一个节点上注册一个事件,然后当它前一个目录节点释放删除后,就会触发这个客户端上的一个回调事件。然后客户端就知道这会儿该它去访问资源了。而zookeeper分布式锁的好处就是可以节省大量的计算资源。

**Curator ** Curator 是一个 ZK 的开源客户端,也提供了分布式锁的实现。

InterProcessMutex interProcessMutex = new InterProcessMutex(client,"/anyLock"); 
interProcessMutex.acquire(); 
interProcessMutex.release(); 

Redis分布式锁 VS Zookeeper分布式锁

Redis分布式锁:

  1. redis set px nx + 唯一id + lua脚本
  • 优点:redis本身的读写性能很高,因此基于redis的分布式锁效率比较高。
  • 缺点:依赖中间件,分布式环境下可能会有节点数据同步问题,可靠性有一定的影响,如果发生则需要人工介入。
  1. Redisson 分布式锁
  • 优点:可以解决redis分布式锁的锁超时问题,通过watchdog实现。
  • 缺点
  1. 基于redis的redlock
  • 优点:可以解决redis集群的同步可用性问题。
  • 缺点
    • 依赖中间件,并没有被广泛验证,维护成本高,需要多个独立的master节点;需要同时对多个节点申请锁,降低了一些效率
    • 锁删除失败 过期时间不好控制
    • 非阻塞,操作失败后,需要轮询,占用cpu资源

Zookeeper分布式锁:

优点:不存在redis的超时、数据同步(zookeeper是同步完以后才返回)、主从切换(zookeeper主从切换的过程中服务是不可用的)的问题,可靠性很高。

缺点:依赖中间件,保证了可靠性的同时牺牲了一部分效率(但是依然很高)。性能上不如redis,因为当客户端越多,频繁的创建和删除节点会消耗性能。

==效率对比:==

  • 性能角度:redis > zookeeper
  • 可靠性角度:redis < zookeeper

www.cnblogs.com/owenma/p/12…


主从同步的实现

(旧版)复制功能的实现(SYNC):

  • 从服务器向主服务器发送SYNC命令
  • 收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录RDB文件生成期间的所有写命令。
  • 当主服务器的BGSAVE命令执行完毕时,主服务器会将RDB文件发送给从服务器,从服务器接收并加载RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE之前的状态。
  • 主服务器将缓冲区的所有写命令都发给从服务器,从服务器执行这些写操作命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。

缺点:断线后重新复制效率比较低,要复制主服务器全部内容,显然这是不必要的。

(新版)复制功能的实现(PSYNC):

  • 完整重同步:步骤和SYNC类似。
  • 部分重同步:用于断线后复制的情况:从服务器断线重连后,只要主服务器把它断线期间的所有写命令传给它,就能将数据库更新至主服务器当前所在的状态。
    • 实现原理(复制偏移量+复制积压缓冲区+服务器运行ID):
      • 主从服务器双方都需要维护一个复制偏移量,通过对比复制偏移量就可以知道主从是否一致,复制积压缓冲区是由主服务器维护的一个固定长度的先进先出的队列,用来存储命令传播时的写命令。
        • 如果从服务器复制偏移量之后的数据都在主服务器的复制积压缓冲区中,则进行部分重同步
        • 否则进行完整重同步

具体步骤:

  • 设置需要复制的主服务器的地址和端口
  • 建立套接字连接
  • 发送PING命令检测套接字的读写是否正常
  • 身份验证,验证密码之类的
  • 发送端口信息,从服务器向主服务器发送自己的端口
  • 同步,从服务器向主服务器发送PSYNC命令,执行同步操作。
  • 命令传播

心跳检测:在命令传播阶段,从服务器默认会以每秒一次的频率向主服务器发送心跳命令。主要作用有:

  • 检测主从服务器的网络状态连接是否正常:如果主服务器超过1s没有收到从服务器发来的REPLCONF ACK命令,就知道主从服务器之间的连接出问题了。
  • 辅助实现min-slaves选项
  • 检测命令丢失:如果因为网络故障,主服务器传给从服务器的命令丢失,那么在从服务器向主服务器发送REPLCONF ACK的时候就会发觉偏移量少于自己的偏移量,这时就会把之前丢失的数据一并传输给从服务器。

redis事务

Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务功能。

**Redis事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。**但是不支持回滚操作。


哨兵

Sentinel是Redis的高可用的解决方案:由一个或多个sentinel实例组成sentinel系统可以监视多个主服务器,以及这些主服务器下的所有从服务器,当监视到有主服务器下线后,自动将挑选其下属的其中一个从服务器升级为新的主服务器。

  • Sentinel每10s一次向监视的服务器发送一条INFO命令。(命令连接)
  • Sentinel每10s一次向监视的服务器发送一条INFO命令。(命令连接)
  • Sentinel每2s一次向主服务器和从服务器的__sentinel__:hello频道发送一条消息。(订阅连接)

检测主观下线:Sentinel每秒1次向与它创建了连接的实例(主服务器,从服务器,Sentinel)发送PING命令,若无PONG返回命令,则判断对方已下线。 检测客观下线:Sentinel将一个主服务器判断为下线之后,为了确认这个主服务器是否真的下线,它会向同样监视这个主服务器的其它Sentinel进行询问。看它们是否也认为主服务器已经进入下线状态。当Sentinel从其它Sentinel那接收到足够数量的已下线判断后,就判定这个主服务器已客观下线。这个Sentinel的数量是在Sentinel配置中的quorum参数配置的。

选举领头Sentinel:

复杂版本:

当一个主服务器被判断客观下线了,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器执行故障转移操作:

  • 所有监控客观下线Master的Sentinel都有可能成为领头Sentinel。
  • 每次进行领头Sentinel选举之后,不论是否选举成功,所有Sentinel的配置纪元(configuration epoch)的值都会自动增加一次。
  • 在一个配置纪元里面,所有Sentinel都有一次将某个Sentinel设置为局部领头Sentinel的机会,并且局部领头Sentinel一旦设置,在这个配置纪元里面将不能再更改。
  • 监视Master客观下线的所有在线Sentinel都有要求其它Sentinel将自己设置为局部领头Sentinel的机会。
  • 当一个Sentinel(源Sentinel)向另一个Sentinel(目标Sentinel)发送SENTINEL is-master-down-by-addr命令(这个命令形象点就是我要你把我的服务器运行ID写到你的配置纪元里),这表示源Sentinel要求目标Sentinel将自己设置为领头Sentinel。
  • Sentinel设置局部领头Sentinel的规则是先到先得。即最先向目标Sentinel发送设置要求的Sentinel将会成为局部领头Sentinel,之后接受到的请求都会被拒绝。
  • 目标Sentinel接收到SENTINEL is-master-down-by-addr命令后,将向源Sentinel返回一条命令回复,回复中的leader_runid参数和leader_epoch参数分别记录了目标Sentinel的局部领头Sentinel的运行ID和配置纪元。
  • 源Sentinel在接收到目标Sentinel返回的命令回复之后,会检查回复中leader_epoch参数的值和自己的配置纪元是否相同,如果相同的话,那么源Sentinel继续取出回复中的leader_runid参数,如果leader_runid参数的值和源Sentinel的运行ID一致,那么表示目标Sentinel将源Sentinel设置成了局部领头Sentinel。
  • 如果有某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel。
  • 领头Sentinel的产生需要半数以上的Sentinel支持,并且每个Sentinel在每个配置纪元里面只能设置一次局部Sentinel,所以在一个配置纪元里面,只会出现一个领头Sentinel。
  • 如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,知道选出领头Sentinel为止。

简单版本:

  • 发现master下线的Sentinel节点(我们称他为A)向每个Sentinel发送命令,要求对方选自己为局部领头Sentinel
  • 如果目标Sentinel节点没有选过其他Sentinel,则会同意选举A为局部领头Sentinel
  • 如果有超过一半的Sentinel同意选举A为领头,则A当选
  • 如果有多个Sentinel节点同时参选领头,此时有可能存在一轮投票无竞选者胜出,此时每个参选的节点等待一个随机时间后再次发起参选请求,进行下一轮投票竞选,直至选举出领头Sentinel

故障转移

  • 在已下线的Master主机下面挑选一个Slave将其转换为主服务器。
  • 让其余所有Slave服务器复制新的Master服务器。
  • 当已下线的服务器在此上线后将成为新的主服务器的从服务器。

选举新的主服务器: 领头Sentinel会将已下线Master的所有从服务器保存在一个列表中,按照规则进行挑选。

  • 删除列表中所有处于下线或者短线状态的Slave。
  • 删除列表中所有最近5s内没有回复过领头Sentinel的INFO命令的Slave。
  • 删除所有与下线Master连接断开超过down-after-milliseconds * 10毫秒的Slave。

之后,领头Sentinel将根据Slave优先级,对列表中剩余的Slave进行排序,并选出其中优先级最高的Slave。如果有多个具有相同优先级的Slave,那么领头Sentinel将按照Slave复制偏移量,选出其中偏移量最大的Slave。如果有多个优先级最高,偏移量最大的Slave,那么根据运行ID最小原则选出新的Master。 确定新的Master之后,领头Sentinel会以每秒一次的频率向新的Master发送SLAVEOF no one命令,当得到确切的回复role由slave变为master之后,当前服务器顺利升级为Master服务器。


集群

redis cluster,分片集群,一般缓存数据量比较多的时候建议用这个,主要是针对海量数据+高并发+高可用的场景。

其他的集群模式,比如说主从架构+sentinel,可用于数据量较小,主要是承载高并发性能的场景。

分片集群的槽指派: 每个集群节点都会分配一定的槽,并且每个节点除了保存自己的槽信息之外,还会把自己管理哪些槽告诉给其它节点,其它节点收到后,保存在对应的节点的结构中,也就是说集群中的每个节点都会知道哪个槽是属于哪个节点的。 当客户端向节点发送与数据库键有关的命令时:

  • 如果键所在的槽正好指派给了当前节点,那么节点直接执行这个命令。
  • 如果键所在的槽没有指派给当前节点,那么当前节点会向客户端返回一个MOVED错误,指引客户端转向至正确的集群节点,并再次发送之前的命令。

Hash算法:

最老土的hash算法和弊端(大量缓存重建),属于最简单的数据分布算法。但是如果某一台master宕机了,会导致 1/3(假设三台redis节点)的数据全部失效,从而大量的数据将会进入MySQL。

最老土的hash算法以及弊端一致性hash算法

一致性hash算法的讲解和优点一致性Hash环,不能解决缓存热点问题,即集中在某个Hash区间内的值特别多,这样就会导致大量的请求同时涌入一个master节点,而其它的节点处于空闲状态,从而造成master热点问题。

这个时候就引入了虚拟环(虚拟节点)的概念,目的是为了让每个master都做了均匀分布,这样每个区间内的数据都能够 均衡的分布到不同的节点中,而不是按照顺时针去查找,从而造成涌入一个master上的问题。

Hash slot算法:

Redis 集群没有使用一致性hash算法, 而是引入了哈希槽的概念,采用自动分片机制。集群有固定的16384个虚拟的哈希槽,所有的键根据哈希函数映射到 0 ~ 16383 整数槽内**,每个key通过CRC16校验后对16384取模来决定放置哪个槽(Slot)**,集群中每个master都会持有部分slot,比如有3个master,那么可能每个master持有5000多个hash slot。**使用哈希槽的好处就在于可以方便的添加或移除节点,**并且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。

  1. 当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;
  2. 当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了。

为什么Redis Cluster会设计成16384个槽呢?

  • 如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。

    • 如上所述,在消息头中,最占空间的是 myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。
  • redis的集群主节点数量基本不可能超过1000个。

    • 如上所述,集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者,不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。
  • 槽位越小,节点少的情况下,压缩率高。

    • Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。

而16384÷8÷1024=2kb,怎么样,神奇不!

综上所述,作者决定取16384个槽,不多不少,刚刚好!


Redis的发布与订阅

涉及命令:publish、subscribe、psubscribe

客户端可以订阅一个或多个频道,成为频道的订阅者,每当有其它客户端向频道发送消息时,频道的所有订阅者都会收到消息。

使用psubscribe可以按照模式来订阅频道。符合模式的频道上面有消息时,客户端都能收到。

频道的订阅与退订原理:

  • 订阅:redis将所有频道的订阅关系都维护在服务器里,其实就是一个字典,叫pubsub_channels。键为被订阅的频道,值为一个链表,链表里面是所有订阅该频道的客户端。订阅的过程是:当一个客户端订阅一个频道时:
    • 如果该频道还没有被任何客户端订阅,则在pubsub_channels中创建一个键,并将这个键设为空链表,然后将客户端添加到这个链表中,成为链表的第一个元素。
    • 如果该频道已经有其它的订阅者,那么直接将这个客户端加入到该频道键下的链表的尾部。
  • 退订:当一个客户端退订一个或某些频道时,服务器会从pubsub_channels中解除客户端与被退订频道之间的关联。
    • 删除该订阅者后,链表为空时,顺便也删除键。

模式的订阅与退订原理:

  • 订阅:订阅关系存放在pubsub_patterns中,它是一个链表,每个节点是pubsub_pattern,里面存放着订阅者和订阅的模式。订阅一个模式的时候也是新建一个pubsub_pattern结构,然后填充两个属性后加入到pubsub_patterns链表的尾部。
  • 退订:查找到匹配的pubsub_pattern节点删除即可。