Redis

暂存,md文件需要精简,这部分文字是较全的

Redis

1、什么是Redis?

Redis是一个使用C语言开发的数据库,与传统数据库不同的是,Redis的数据存在内存中,是内存数据库,读写速度快,被广泛用于数据缓存。
Redis除了用作缓存,还可以用来做分布式锁,甚至是消息队列。
Redis提供了多种数据类型支持不同的业务场景。
Redis还支持事务、持久化、Lua脚本、多种集群方案等。
什么场景下使用Redis?
  • 配合关系型数据库用作高速缓存
  • 用作分布式锁
  • 用作缓存队列
  • 发布订阅场景
为什么redis快?

(1)redis是运行在内存的,自然就快

(2)redis的数据结构简单,操作节省时间

(3)redis是单线程(基于内存,单线程不会让cpu成为瓶颈) 的,节省了上下文切换时间采用了多路复用io阻塞机制,一个线程可以复用多个连接,自然就读取速度快。

2、分布式缓存常见技术选型方案(Redis、Memcached)及异同:

分布式缓存主要解决:单机缓存的容量受服务器限制并且无法保存通用信息。
因为本地缓存只在当前服务里有效,如部署了两个相同的服务,二者之间的缓存数据时无法共通的。

分布式缓存主要使用:Memcached和Redis

共同点:

1、都是基于内存的数据库,一般都可以当做缓存来用。
2、都有过期策略。
3、性能都很高。

区别:

1、Redis支持更丰富的数据类型,可以应用于更复杂的应用场景。list、set、zset、hash等----k/v;
2、Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载使用;而Memcached把数据全部存在内存中。
3、Redis有灾难恢复机制。因为可以把缓存中的数据持久化到磁盘上。
4、Redis在服务器内存使用完之后,可以把不用的数据放在磁盘上。Memcached在服务器内存使用完后直接报异常。
5、Memcached没有原生的集群模式,需要依靠客户端实现往集群中分片写入数据,Redis原生支持cluster模式。
6、Redis使用单线程的多路IO复用模型(Redis6.0引入了多线程IO),Memcached是多线程、非阻塞IO复用的网络模型。
7、Redis支持发布订阅模型、Lua脚本、事务等功能,且支持更多的编程语言。
8、Memcached过期数据的删除策略只用了惰性删除,Redis使用的是惰性删除和定期删除。

3、缓存数据的处理流程:

1. 如果⽤户请求的数据在缓存中就直接返回。
2. 缓存中不存在的话就看数据库中是否存在。
3. 数据库中存在的话就更新缓存中的数据。
4. 数据库中不存在的话就返回空数据。

4、为什么要使用Redis?/ 为什么要使用缓存?

1、高性能:
	在用户下一次访问之前访问过的数据时,可以直接从缓存中获取。操作缓存就是直接操作内存,速度远快于从数据库中读取数据。
2、高并发:
	像MySQL数据库(4核8G)的QPS(服务器每秒可以执行的查询次数)大概在1W左右;但是Redis缓存可以轻松达到10W+,集群更高。
	通过把数据库的部分数据转移到缓存中,可以提高系统的整体的并发。

Redis如何实现高并发和高可用

  • redis高并发主从架构,一主多从,一般来说,很多项目足够了,单主用来写入数据,单机几万QPS,多从用来查询数据,多个从实例可以提供每秒10万的QPS。

  • redis高并发的同时,还需要容纳大量的数据: 一主多从,每个实例都容纳了完整的数据,比如redis主就10G的内存量,其实你就最多只能容纳10G的数据量,如果你的缓存要容纳的数据量很大,达到了几十G,甚至几百G,那就需要reids集群。而且用redis集群之后,可以提供可能每秒几十万的读写并发。

  • redis高可用:如果做主从架构部署,其实就是加上哨兵就可以,就可以实现,任何一个实例宕机,自动会进行主备切换。

5、Redis常见数据结构:

Redis的数据结构可以结合Java中的对应的类来进行理解,其中:

  • String数据结构对应Object类 (任意对象都会序列化成string来存储)

  • List数据结构对应java.util.List接口的实现类java.util.LinkedList

  • Set数据结构对应java.util.Set接口

  • SortedSet数据结构对应java.util.SortedSet接口,

  • Hash数据结构对应java.util.HashMap类。

1、String

1、介绍:
		String类型是Redis中最为基础的数据存储类型,是二进制安全的字符串,该类型可以接受任何格式的数据。string 数据结构是简单的 key-value 类型。虽然 Redis 是⽤ C 语⾔写的,但是 Redis并没有使⽤ C 的字符串表示,⽽是⾃⼰构建了一种 简单动态字符串(simple dynamic string, SDS)。相⽐于 C 的原⽣字符串, Redis 的 SDS 不光可以保存⽂本数据还可以保存二进制数据,并且获取字符串⻓度复杂度为 O(1)(C 字符串为 O(N)) ,除此之外,Redis的SDS API 是安全的,不会造成缓冲区溢出。
		
2、常⽤命令: 
		set,get,strlen,exists,dect,incr,setex 等等。
3、应⽤场景 :
		--value较小、模型简单的 value可以使用String类型存储
		⼀般常⽤在需要计数的场景,⽐如⽤户的访问次数、热点⽂章的点赞转发数量等等。
			
补充:
		在Redis中String类型的Value最多可以容纳的数据长度是512M,在squirrel-client中Value限制的大小是1M。相对于其他的几种数据结构,只有String类型的命令在写入key的时候可以带有默认的过期时间(在squirrel-client中,对于String类型的命令只有set,add和multiset命令会自动设置过期时间,且过期时间为使用category的过期时间),对于其他的数据结构,key默认是不过期的,如果需要设置过期时间,必须显示调用expire函数设置过期时间。


--在squirrel-client中,所有的对象都会被序列化成String存到集群中,因此所有的数据都可以作为String类型来存储。

2、list

  • 有序列表,可以通过list存储一些列表型的数据结构,类似粉丝列表,文章的评论列表之类的东西。
1. 介绍:
		List类型是按照插入顺序排序的字符串链表。链表是⼀种⾮常常⻅的数据结构,可以在头部、尾部添加新的元素。特点是易于数据元素的插⼊和删除,并且且可以灵活调整链表⻓度,但是链表的随机访问困难。
		插入时,如果该key不存在,Redis会为该key创建一个新的链表;如果链表中所有元素都被移除,该key也会被从数据库中删除。
		许多编程语言都内置了链表的实现⽐如 Java 中的 LinkedList,但是 C 语⾔并没有实现链表,所以 Redis 实现了自己的链表数据结构。 
		Redis 的 list 的实现为⼀个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
		
2. 常⽤命令: 
		rpush,lpop,lpush,rpop,lrange(从某个元素开始读取多少个元素)、llen 等。
3. 应⽤场景: 
		发布与订阅或者说消息队列、慢查询。--可以基于list实现分页查询,基于Redis实现简单的高性能分页
		--在评级系统中,比如社会化新闻网站,你可以把每个新提交的链接添加到一个list,用LRANGE可简单的对结果分页;
		--在博客引擎实现中,你可为每篇日志设置一个list,在该list中推入进博客评论等等。

3、hash

1. 介绍: 
		hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 hash 做了更多优化。
		hash 是⼀个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,后续可以直接仅仅修改这个对象中的某个字段的值。 
		该类型非常适合于存储值对象的信息,比如User对象含有Username、Password和Age等属性,可以使用hash来存储User,每个field对应一个属性,好处是可以做到部分更新、获取。
		
2. 常⽤命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
3. 应⽤场景: 系统中对象数据的存储。

		1,对于海量数据的情况,可以自己对数据进行分桶,然后使用Hash结构来存储。对于很多value为简单的字符串,采用hash存储更节省空间。
		2,将对象存储为Hash结构而不是String,可以每次只更新、获取Hash中的一个field,这样可以提高效率。

4、set

  • 无序列表,自动去重
1. 介绍:
		set 类似于 Java 中的 HashSet 。 Redis 中的 set 类型是⼀种无序集合,集合中的元素没有先后顺序,可以在set上执行添加、删除或判断某一元素是否存在等操作。如果多次添加相同元素,Set中将仅保留该元素的一份拷贝。
		当你需要存储⼀个列表数据,⼜不希望出现重复数据时, set 是⼀个很好的选择,并且 set 提供了判断某个成员是否在⼀个 set 集合内的重要接⼝,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
		
2. 常⽤命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
3. 应⽤场景: 
		需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景  
		--⽐如:你可以将⼀个用户所有的关注⼈存在⼀个集合中,将其所有粉丝存在⼀个集合。 Redis 可以⾮常⽅便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
		--可以使用Redis的Set数据类型跟踪一些唯一性数据,比如访问某一博客的唯一IP地址信息。对于此场景,仅需在每次访问该博客时将访问者的IP存入Redis中,Set数据类型会自动保证IP地址的唯一性。

5、zset(sorted set)

  • 排序的set,去重但是可以排序,增加了一个权重参数,自动根据分数排序,可以自定义排序规则

  • 底层采用压缩表ziplist或跳表skiplist的数据结构实现

    • 跳表的本质是一个多层链表,它能快速地查询、插入、删除【时间复杂度均为O(logn)】

      基于多指针有序链表实现的,可以看成**多个有序链表。**在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。

    • 当ziplist作为zset的底层存储结构时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值。

1. 介绍:
		和 set 相⽐, sorted set 增加了⼀个权重参数 score,使得集合中的元素能够按 score进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap和 TreeSet 的结合体。
		SortedSet中的成员必须是唯一的,但是分数(score)却是可以重复的。
		在SortedSet中添加、删除或更新一个成员都是非常快速的操作,其时间复杂度为O(logn)。
		
2. 常⽤命令: 
		zadd,zcard,zscore,zrange,zrevrange,zrem 等。
3. 应⽤场景: 需要对数据根据某个权重进行排序的场景。⽐如在直播系统中,实时排行信息包含直播间在线⽤户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。  

		1. 可以用于一个大型在线游戏的积分排行榜。每当玩家的分数发生变化时,可以执行ZADD命令更新玩家的分数,此后再通过ZRANGE命令获取积分TOP TEN的用户信息。当然也可以利用ZRANK命令通过username来获取玩家的排行信息。最后将组合使用ZRANGE和ZRANK命令快速的获取和某个玩家积分相近的其他用户的信息。
		2. SortedSet类型还可用于构建索引数据。
		3. 建立一个SortedSet中元素个数不要超过 1 W。

6、HyperLogLog:

HyperLogLog类型用来进行基数统计。
利用HyperLogLog,在输入元素的数量或者体积非常非常大时,用户可以使用少量固定大小的内存,来统计集合中唯一元素的数量。
(每个HyperLogLog占用12KB内存,可以计算接近 2^64 个不同元素的的基数)。

利用HyperLogLog得到的基数统计结果,不是精确值,而是一个带有0.81%标准差(standard error)的近似值。所以,HyperLogLog适用于一些对于统计结果精确度要求不是特别高的场景。

使用场景:
1.可以用于统计一个网站的UV。利用HyperLogLog来统计访问一个网站的不同ip的个数。

6、Redis单线程模型-多路IO复用:

Redis 基于 Reactor 模式来设计开发了自己的⼀套高效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式–高性能 IO 的基⽯),这套事件处理模型对应的是 Redis中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们⼀般都说 Redis 是单线程模型。

单线程模型Redis通过IO多路复用程序来监听来自客户端的大量连接(监听多个Socket),会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。

I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗 (和 NIO 中的 Selector 组件很像)。

Redis 服务器是⼀个事件驱动程序,服务器需要处理两类事件:
1、文件事件(客户端进行读取写⼊等操作,涉及⼀系列网络通信 ); 2、 时间事件

在这里插入图片描述

文件事件处理器(File Event Handler)主要包含:

1、多个socket(客户端连接)
2、IO多路复用程序(支持多个客户端连接的关键)
3、文件事件分派器(将socket关联到相应的事件处理器)
4、事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IRZ7eSOv-1628855797101)(Java笔记.assets/image-20210412165017711.png)]

7、Redis为什么不使用多线程?

虽说Redis是单线程模型,但是实际上Redis在 4.0 之后的版本加入了对多线程的支持。大体上讲,Redis 6.0之前主要还是单线程处理。

为什么之前不使用多线程?

1、单线程编程容易且更容易维护
2、Redis的性能瓶颈不在CPU,主要在于内存和网络
3、多线程会存在死锁、线程上下文切换等问题,甚至影响性能

为什么之后引入了多线程?

引入多线程主要是为了提高网络IO读写功能。

并且,Redis的多线程只是在网络数据的读写这类耗时操作上使用,执行命令仍是单线程顺序执行,不需要担心线程安全问题。

Redis 6.0的多线程默认是禁用的,只使用多线程。开启需要修改配置文件redis.conf并设置线程数。

8、Redis给缓存数据设置过期时间有什么用处?

因为内存是有限的,如果缓存中的所有数据都一直保存,可能分分钟Out Of Memory。

Redis自带了给缓存数据设置过期时间的功能—缓解内存消耗
Redis中除了字符串类型有自己独有设置过期时间的命令 setex 外,其他方法都需要依靠expire 命令来设置过期时间 。另外, persist 命令可以移除⼀个键的过期时间:

过期时间其他用途:业务场景就是需要某个数据只在某⼀时间段内存在有效–短信验证码、⽤户登录的 token

9、Redis如何判断数据是否过期?

Redis通过一个叫过期字典(可以看作是hash表)来保存数据过期的时间。

过期字典的键指向Redis数据库中的某个key(键),过期字典的值是⼀个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)。

在这里插入图片描述

Redis 可以为键值设置生存周期(TTL),并在过期后自动删除这些键值对。

expire(pexpire)、expireat(pexpireat)

Redis 提供了 EXPIRE(PEXPIRE) 和 ***EXPIREAT(PEXPIREAT)***两个命令以秒或者毫秒精度来设置过期时间,区别是前者是生存时间,后者是具体的过期时间戳。

只有当键值被删除或者值被覆盖的时候,例如执行DEL, SET, GETSET 和所有STORE相关的命令,过期时间会被移除;而修改值的操作如INCR, LPUSH, HSET不会影响过期时间。另外,我们也可以通过 **PERSIST **命令手动移除键的过期时间。

***TTL***和***PTTL***两个命令可以以秒或者毫秒精度查询键的剩余生存时间。

AOF、RDB和复制功能对过期键的处理:
  • AOF

AOF 文件写入时,某个键已经过期,但还没有被惰性或定期删除,AOF文件不会因为这个过期键产生任何影响。只有当键被删除后,AOF会追加一条DEL命令。

AOF重写时,程序会对键进行检查,已经过期的键不会保存到重写的AOF文件中。

  • RDB

生成RDB文件时,程序会对数据库中的键进行检查,已过期的键不会保存到新创建的RDB文件中。

载入RDB文件时,主节点和从节点采取不同的策略:

1、主节点会对文件中保存的键值对进行检查,未过期的键都会被载入到数据库,过期的键会被忽略;

2、从节点则不会进行检查,把文件中包含的所有键无论过期与否,都载入到数据库中。

  • 复制

复制模式下,过期键的删除动作由主节点控制:

1、主节点在删除一个过期键之后,会显式地向所有从节点发送一个DEL命令;

2、从节点在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,只有在接受到主节点发送的DEL命令后才会删除过期键。

10、Redis过期数据的删除策略:

0、定时删除:设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间到来时,立即执行对键的删除。

**1、惰性删除 :**放任键过期不管, 只会在取出key的时候才对数据进行过期检查 ,如果过期的话就删除该键;没有过期就返回该键。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。

2、定期删除 : **每隔一段时间,程序对数据库进行一次检查,删除里面过期的键。**删除多少过期键、检查多少个数据库,由算法决定。并且, Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。 ---- redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。

定时和定期删除是主动删除策略,惰性删除是被动删除策略。主动删除(定时、定期)对内存更加友好,惰性删除对CPU更加友好。 Redis 采⽤的是 定期删除+惰性删除

但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致⼤量过期 key 堆积在内存⾥,然后就Out of memory了。通过 Redis 内存淘汰机制解决这个问题。

11、Redis内存淘汰机制

相关问题: MySQL ⾥有 2000w 数据, Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?

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

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

12、Redis持久化机制(怎么保证Redis挂掉之后再重启数据可以恢复?)

持久化数据 == 将内存中的数据写入到硬盘中,大部分原因是为了以后重用数据(比如机器故障、重启机器之后恢复数据)或者是为了防止系统故障而将数据备份到一个远程位置。

Redis支持两种持久化方式:快照(snapshotting, RDB)只追加文件(append-only file, AOF)

  • RDB 持久化机制,会在一段时间内生成指定时间点的数据集快照(snapshot)

  • AOF 持久化机制,记录 server 端收到的每一条写命令,当 server 重启时会进行重放以此来重建之前的数据集。AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加(append)到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite) ,使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。

如果你仅使用 Redis 作为缓存加速访问,你可以关闭这两个持久化设置

你也可以同时开启这两个持久化设置,但是在这种情况下,Redis 重启时会使用 AOF 文件来重建数据集,因为 AOF 文件保存的数据往往更加完整

1、快照(snapshotting, RDB)
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。
Redis创建快照之后,可以对快照进行备份,可以将快照复制到其它服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。

快照持久化是Redis默认采用的持久化方式。

RDB的创建与载入:

Redis 提供了 SAVE 和 BGSAVE 两个命令来生成 RDB 文件,区别是前者是阻塞的,后者是后台 fork 子进程进行不会阻塞主进程处理命令请求。载入 RDB 文件不需要手工运行,而是 server 端自动进行,只要启动时检测到 RDB 文件存在 server 端便会载入 RDB 文件重建数据集。当然上面简介中已经提到,如果 同时存在 AOF 的话会优先使用 AOF 重建数据集因为其保存的数据更完整。

RDB相关配置:

1、SAVE POINT
可以配置保存点(save point),Redis 如果每 N 秒数据发生了 M 次改变就保存快照文件
格式:save <seconds> <changes>
save 60 1000  #配置表示每60秒,如果数据发生了1000次以上的变动,Redis就会自动保存快照文件

2、stop-writes-on-bgsave-error
如果Redis执行RDB持久化失败(操作系统内存不足),Redis将不再接受client写入数据的请求。
在实践中,通常将 stop-writes-on-bgsave-error 设置为 false,同时让监控系统在 Redis 执行 RDB 持久化失败时发送告警,以便介入解决,而不是粗暴地拒绝 client 的写入请求。

3、rdbcompression
当生成 RDB 文件时,Redis 会判断字符串长度 >=20字节则压缩,否则不压缩存储,默认 Redis 会采用 LZF 算法进行数据压缩。

4、rdbchecksum
从版本5的 RDB 的开始,一个 CRC64 的校验码会放在文件的末尾。这样更能保证文件的完整性,但是在保存或者加载文件时会损失一定的性能(大概10%)。如果想追求更高的性能,可以把它禁用掉,这样文件在写入校验码时会用 0 替代,加载的时候看到 0 就会直接跳过校验。

RDB的优点:

1、RDB文件是一个很简洁的单文件,它保存了某个时间点的 Redis 数据集,很适合用于做备份。你可以设定一个时间点对 RDB 文件进行归档,这样就能在需要的时候很轻易的把数据恢复到不同的版本。
2、RDB 文件很适合用于灾备,因为单文件可以很方便地传输到另外的数据中心。
3、RDB的性能很好,需要进行持久化时,主进程会 fork 一个子进程出来,然后把持久化的工作交给子进程,自己不会有相关的 I/O 操作。
4、比起 AOF,在数据量比较大的情况下,RDB的启动速度更快。

RDB的缺点:

1、RDB容易造成数据的丢失,当你希望在 Redis 停止工作时尽量减少数据丢失的话,那 RDB 不适用。假设每5分钟保存一次快照,如果Redis因为某些原因不能正常工作,那么从上次产生快照到 Redis 出现问题这段时间的数据就会丢失了。你可以通过配置不同的 save point 来减轻数据丢失的程度,但是越紧凑的 save point 会越频繁地触发 RDB 生成操作,从而对 Redis 性能产生影响。

2、RDB 使用 fork 子进程进行数据的持久化,如果数据比较大的话可能就会花费点时间,造成 Redis 停止服务几毫秒,如果数据量很大且 CPU 性能不是很好的时候,停止服务的时间甚至会到一秒。AOF 也需要 fork 但是你可以自己调整 rewrite 的频率,它不会造成数据丢失。在 Linux 系统中,fork 会拷贝进程的 page table。随着进程占用的内存越大,进程的 page table 也会越大,那么 fork 也会占用更多的时间。 如果 Redis 占用的内存很大 (例如 20 GB),那么在 fork 子进程时,会出现明显的停顿现象(无法处理 client 的请求)。另外,在不同机器上,fork 的性能是不同的。

3、Linux fork 子进程采用的是 copy-on-write 的方式。在 Redis 执行 RDB 持久化期间,如果 client 写入数据很频繁,那么将增加 Redis 占用的内存,最坏情况下,内存的占用将达到原先的两倍。
2、只追加文件(append-only file, AOF)

相较于快照持久化,AOF持久化的实时性更好,已成为主流的持久化方案。
不同于RDB持久化数据库键值对来记录数据库状态,AOF通过保存对数据库的写命令集来记录数据库状态

AOF持久化实现可以分为:命令追加(append)、文件写入(write)、文件同步(fsync) 三个步骤。
		Append 追加命令到 AOF 缓冲区,
		Write 将缓冲区的内容写入到程序缓冲区,
		Fsync 将程序缓冲区的内容写入到文件。 
默认情况下Redis没有开启AOF方式的持久化,可通过appendonly参数开启:appendonly yes

命令追加:
当 AOF 持久化功能打开时,server 端每执行完一个写命令,会以协议格式将被执行的写命令追加到 server 端 redisServer 结构体中的 aof_buf 缓冲区末尾

文件写入与同步:
Redis server 进程是一个事件循环(event loop),server 每结束一个事件循环之前都会调用 flushAppendOnlyFile 函数,考虑是否将 aof_buf 缓冲区中的内容吸入和保存到硬盘中的 AOF 文件,而 flushAppendOnlyFile 函数的行为由 appendfsync 选项来控制。

AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置,默认文件名是appendonly.aof。

配置文件中存在三种不同的AOF持久化方式,不同的appendfsync值对应不同的flushAppendOnlyFile行为:
	1、appendfsync always     # 每次有数据修改或者每个事件循环都会将aof_buf缓冲区的内容写入到AOF文件,并调用文件同步fsync将其同步到磁盘。
	--可以保证最好的数据持久性,开销大,效率慢,安全性最高,宕机也只会丢失一个事件循环中的数据。
	
	2、appendfsync everysec   # 每个事件循环都将aof_buf缓冲区的内容写入到AOF文件,每秒在子线程中执行一次fsync()同步到硬盘。
	
	3、appendfsync no         # 每个事件循环都将aof_buf缓冲区的内容写入到AOF文件,但不对其进行同步,让操作系统决定何时同步至磁盘。
	--AOF的写入速度最快,但是因为系统缓存中数据的积累,同步时间最长。宕机会丢失自上一次同步AOF文件起所有的数据。
	
	
为了兼顾数据和写入性能,用户可以考虑appendfsync everysec选项,让Redis每秒同步一次AOF文件,Redis性能几乎没受到影响。
即使出现系统崩溃,用户最多也只是丢失一秒内产生的数据。当硬盘忙于执行写入操作时,Redis还会放慢自己的速度适应硬盘的最大写入速度。

AOF 重写(rewrite)

AOF 持久化并不是没有缺点的,Redis 会不断将接收到的写命令追加到 AOF 文件中,导致 AOF 文件越来越大。过大的 AOF 文件会消耗磁盘空间,并且导致 Redis 重启时更加缓慢。为了解决这个问题,在适当情况下,Redis 会对 AOF 文件进行重写,去除文件中冗余的命令,以减小 AOF 文件的体积。

AOF 重写可以产生一个新的 AOF ⽂件,这个新的 AOF ⽂件和原有的 AOF ⽂件所保存的数据库状态⼀样,但体积更小。
AOF 重写是⼀个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无需对现有AOF ⽂件进行任何读入、分析或者写⼊操作。

AOF的重写会执行大量的写入操作,Redis是单线程的,所以如果有服务器直接调用重写,服务器就不能处理其他命令了,因此Redis服务器新起了单独一个子进程来执行AOF重写。

在执⾏ BGREWRITEAOF 命令时,Redis 服务器会维护⼀个 AOF 重写缓冲区rewrite_buf,该缓冲区会在子进程创建新 AOF ⽂件期间,记录服务器执行的所有写命令。在子进程执行AOF重写时,服务端接收到客户端的命令之后,先执行客户端发来的命令,然后将执行后的写命令追加到AOF缓冲区中,同时将执行后的写命令追加到AOF重写缓冲区中。 等到子进程完成了重写工作后,会发一个完成的信号给服务器,服务器就将AOF重写缓冲区中的所有内容追加到AOF文件中,使得新旧两个 AOF ⽂件所保存的数据库状态⼀致,然后原子性地覆盖现有的AOF文件。重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。

在这里插入图片描述

AOF相关配置:

# 你可以在 redis.conf 中通过以下配置开启 AOF 功能
appendonly yes

# 文件存放目录,与RDB共用。默认为当前工作目录。
dir ./

# 默认文件名为appendonly.aof
appendfilename "appendonly.aof"

# fsync 相关配置
# appendfsync always
appendfsync everysec
# appendfsync no

# Redis会记住自从上一次重写后AOF文件的大小(如果自Redis启动后还没重写过,则记住启动时使用的AOF文件的大小)。
# 如果当前的文件大小比起记住的那个大小超过指定的百分比,则会触发重写。
# 同时需要设置一个文件大小最小值,只有大于这个值文件才会重写,以防文件很小,但是已经达到百分比的情况。
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 上面两个配置的作用:当 AOF 文件的体积大于 64MB,并且 AOF 文件的体积比上一次重写之后的体积大了至少一倍,那么 Redis 就会执行 AOF 重写。

# 要禁用自动的日志重写功能,我们可以把百分比设置为0:
auto-aof-rewrite-percentage 0

AOF的优点:

1、比RDB可靠。你可以制定不同的 fsync 策略:no、everysec 和 always。默认是 everysec。这意味着你最多丢失一秒钟的数据。

2、AOF日志文件是一个纯追加的文件。就算是遇到突然停电的情况,也不会出现日志的定位或者损坏问题。甚至如果因为某些原因(例如磁盘满了)命令只写了一半到日志文件里,我们也可以用 redis-check-aof 这个工具很简单的进行修复。

3、当AOF文件太大时,Redis 会自动在后台进行重写。重写很安全,因为重写是在一个新的文件上进行,同时 Redis 会继续往旧的文件追加数据。新文件上会写入能重建当前数据集的最小操作命令的集合。当新文件重写完,Redis 会把新旧文件进行切换,然后开始把数据写到新文件上。

4、AOF 把操作命令以简单易懂的格式一条接一条的保存在文件里,很容易导出来用于恢复数据。例如我们不小心用 FLUSHALL 命令把所有数据刷掉了,只要文件没有被重写,我们可以把服务停掉,把最后那条命令删掉,然后重启服务,这样就能把被刷掉的数据恢复回来。

AOF的缺点:

1、在相同的数据集下,AOF 文件的大小一般会比 RDB 文件大。

2、在某些 fsync 策略下,AOF 的速度会比 RDB 慢。通常 fsync 设置为每秒一次就能获得比较高的性能,而在禁止 fsync 的情况下速度可以达到 RDB 的水平。

3、在过去曾经发现一些很罕见的BUG导致使用AOF重建的数据跟原数据不一致的问题。

Redis 4.0 对于持久化机制的优化

Redis 4.0 开始⽀持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdbpreamble 开启)。

如果把混合持久化打开, AOF 重写的时候就直接把 RDB 的内容写到 AOF ⽂件开头。
这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。
当然缺点也是有的,AOF ⾥⾯的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

13、Redis事务

Redis事务提供了一种将多个命令请求打包的功能。然后按顺序执行打包的所有命令,且不会被中途打断。

Redis可以通过MULTI、EXEC、DISCARD和WATCH等命令来实现事务(Transaction)功能。
使用MULTI命令后可以输入多个命令。Redis不会立即执行这些命令,而是将它们放到队列,当调用了EXEC命令将执行所有命令。

MULTI命令 :用于开启一个事务,它总是返回OK。
	MULTI执行之后,客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行,而是被放到一个队列中,
	当 EXEC命令被调用时, 所有队列中的命令才会被执行。

EXEC命令 :负责触发并执行事务中的所有命令: 
	如果客户端成功开启事务后执行EXEC,那么事务中的所有命令都会被执行。 
	如果客户端在使用MULTI开启了事务后,却因为断线而没有成功执行EXEC,那么事务中的所有命令都不会被执行。 
	需要特别注意的是:即使事务中有某条/某些命令执行失败了,事务队列中的其他命令仍然会继续执行
		——Redis不会停止执行事务中的命令,而不会像我们通常使用的关系型数据库一样进行回滚。

DISCARD命令 
	当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空,并且客户端会从事务状态中退出。

WATCH 命令 
	可以为Redis事务提供 check-and-set (CAS)行为。
	被WATCH的键会被监视,并会发觉这些键是否被改动过了。 
	如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。

关系型数据库的事务具备ACID四大特性–原子、一致、隔离、持久。
Redis不支持roll back回滚,因此不满足原子性,并且不满足持久性
–原因:Redis开发者认为没必要进行回滚,这样更简单便捷且性能更好。
开发者认为即使命令执行错误也应该在开发过程中就被发现而不是在生产过程中。

14、缓存穿透、缓存雪崩

1、缓存雪崩:缓存中的很多key失效,导致数据库负载过重宕机

在这里插入图片描述

缓存在同一时间大面积失效,后面的请求都直接落到了数据库上,造成数据库短时间承受大量请求。
     
可能是缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,导致的db崩溃。
     
* 解决方法:
     针对Redis服务不可用的情况:
     	1、采用Redis集群,避免单机出现问题整个缓存服务器都没办法使用。
     	2、限流操作,避免同时处理大量请求。
     	
     针对热点缓存失效的情况:
     	1、设置缓存永不失效
     	2、设置不同的缓存失效时间

​ 缓存雪崩的事前事中事后的解决方案

​ 事前:redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
​ 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL被打死
​ 事后:redis持久化,快速恢复缓存数据,一般重启,自动从磁盘上加载数据恢复内存中的数据。

请添加图片描述

缓存击穿和缓存穿透:失去了redis的拦截高并发的能力,直接打到数据库上

2、缓存穿透:利用不存在的key去攻击mysql数据库

是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,
并且处于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

* 解决: 
     1、空结果进行缓存设置过期时间,但它的过期时间会很短,最长不超过五分钟。
     2、布隆过滤器(这个不主动说)

3、缓存击穿:在正常的访问情况下,如果缓存失效,如果保护mysql,重启缓存的过程

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
这个时候,需要考虑一个问题:如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

* 是某一个热点key在高并发访问的情况下,突然失效,导致大量的并发打进mysql数据库的情况

* 解决:使用redis数据库的分布式锁,解决mysql的访问压力问题

     * 1、redis自带的分布式锁,set px nx
     * -----  String token = UUID.randomUUID().toString();
     * ------ String lock = jedis.set(key, token, "NX", "EX",20);
     
     * 2、redission框架:带juc的lock锁的redis客户端,是一个redis的juc实现(既有jedis功能又有juc功能)
     *-------- jedis本身无法实现多线程锁的机制
     * ------- synchronized () 只能解决本地的多线程并发问题

15、如何保证缓存和数据库数据的一致性?

如何保证缓存(redis)与数据库(MySQL)的一致性?

客户端对于数据库中数据的操作主要有读、写两种操作。
读操作不会导致数据库和缓存中数据的不一致问题。
对于写操作,缓存和数据库中的内容都需要修改,但二者存在先后顺序,可能导致二者数据不一致问题。

主要需要考虑两个问题:
  1、执行顺序的问题:先更新缓存还是先更新数据库?
  2、更新缓存的策略问题:当缓存中的内容变化时,是选择修改缓存(update),还是直接淘汰缓存(delete)?

针对这两点问题,一共可以分为四种方案:
  1、先更新缓存,再更新数据库;
  2、先更新数据库,再更新缓存;
  3、先淘汰缓存,再更新数据库;
  4、先更新数据库,再淘汰缓存。


对于是更新缓存还是直接淘汰缓存?

淘汰cache:
	优点:操作简单,无论更新操作是否复杂,直接将缓存中的旧值淘汰
	缺点:淘汰cache后,下一次查询无法在cache中查到,会有一次cache miss,这时需要重新读取数据库
更新cache:
  更新chache的意思就是将更新操作也放到缓冲中执行,并不是数据库中的值更新后再将最新值传到缓存
	优点:命中率高,直接更新缓存,不会有cache miss的情况
	缺点:更新cache消耗较大
	
  当更新操作简单,如只是将这个值直接修改为某个值时,更新cache与淘汰cache的消耗差不多
  但当更新操作的逻辑较复杂时,需要涉及到其它数据,如用户购买商品付款时,需要考虑打折等因素,这样需要缓存与数据库进行多次交互,将打折等信息传入缓存,再与缓存中的其它值进行计算才能得到最终结果,此时更新cache的消耗要大于直接淘汰cache。
  
所以选择直接淘汰缓存更好,如果之后需要再次读取这个数据,最多会有一次缓存失败

并发情况下,更新缓存的两种方案:

对于前两种方案,也就是更新缓存和更新数据库顺序不同的两种方案。
在并发较大时,同时有两个线程需要对同一个数据进行更新时:
	如果不同的线程对同一个数据进行更新时,更新的先后顺序有明确要求,那么两种方案都会导致数据的不一致。
解决的思路是串行化,对同一个数据的修改要以串行化的方式先后执行。

因此:更新缓存消耗更大,多线程操作可能导致数据的不一致,因此直接淘汰缓存。


对于淘汰缓存和更新数据库的执行顺序问题:

主要分为两个方面来考虑:
  * 1、更新数据库与淘汰缓存是两个步骤,只能先后执行,如果在执行过程中后一步执行失败,哪种方案的影响最小?
  * 2、如果不考虑执行失败的情况,但更新数据库与淘汰缓存必然存在一个先后顺序,在上一个操作执行完毕,下一个操作还未完成时,如果并发较大,仍旧会导致数据库与缓存中的数据不一致,在这种情况下,用哪种方案影响最小?

同时,对于主从结构数据库,也就是读操作放在从库、写操作放在主库的情况,还需要考虑主从延迟。
这里讲一下单节点模式,也就是读写操作在同一台服务器上,底层只有一个数据库的情况。

第一个问题:淘汰缓存和更新数据库需要先后执行,后一步执行失败的情况,哪种方案影响最小?

方案一、先淘汰缓存,再更新数据库
	如果第一步淘汰缓存成功,第二步更新数据库失败,此时再次查询缓存,最多会有一次cache miss
方案二、先更新数据库,再淘汰缓存
	如果第一步更新数据库成功,第二部淘汰缓存失败,则会出现数据库中是新数据,缓存中是旧数据,即数据不一致
	
对于方案二:为确保缓存删除成功,需要用到“重试机制”,即当删除缓存失效后,返回一个错误,由业务代码再次重试,直到缓存被删除。
对于方案一,如果更新数据库失败其实也是一个问题,为了确保数据库中的数据被正常更新,也需要“重试机制”,即当数据库中的数据更新失败后,也需要人工或业务代码再次重试,直到更新成功。

总体而言,两种方案并没有什么优劣之分。

在这里插入图片描述

第二个问题:假设操作不会执行失败,但执行前一个操作后无法立即完成下一个操作,并发情况下导致数据不一致。


方案3:先淘汰缓存,再更新数据库。

1、在正常情况下,A、B两个线程先后对同一个数据进行读写操作:
  A线程进行写操作,先淘汰缓存,再更新数据库
  B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取更新后的新数据
此时没有问题

2、在并发量较大的情况下,采用同步更新缓存的策略:
  A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新
  B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取数据,但此时A线程还未完成更新操作,所以读取到的是旧数据,并且B线程将旧数据放入缓存。注意此时是没有问题的,因为数据库中的数据还未完成更新,所以数据库与缓存此时存储的都是旧值,数据没有不一致
  在B线程将旧数据读入缓存后,A线程终于将数据更新完成,此时是有问题的,数据库中是更新后的新数据,缓存中是更新前的旧数据,数据不一致。如果在缓存中没有对该值设置过期时间,旧数据将一直保存在缓存中,数据将一直不一致,直到之后再次对该值进行修改时才会在缓存中淘汰该值
此时可能会导致cache与数据库的数据一直或很长时间不一致

3、在并发量较大的情况下,采用异步更新缓存的策略:
  A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新
  B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取数据,但B线程只是从数据库中读取想要的数据,并不将这个数据放入缓存中,所以并不会导致缓存与数据库的不一致
  A线程更新数据库后,通过订阅binlog来异步更新缓存
此时数据库与缓存的内容将一直都是一致的

如果采取同步更新缓存的策略(即如果缓存中没有数据,就读取数据库并将数据直接放入缓存),可能导致数据长时间的不一致。

优化思路:

1、用串行化的思路
  即保证对同一个数据的读写严格按照先后顺序串行化进行,避免并发较大的情况下,多个线程同时对同一数据进行操作时带来的数据不一致性。
  
2、延时双删+设置缓存的超时时间
  不一致的原因是,在淘汰缓存之后,旧数据再次被读入缓存,且之后没有淘汰策略,所以解决思路就是,在旧数据再次读入缓存后,再次淘汰缓存,即淘汰缓存两次(延迟双删)
  
引入延时双删后,执行步骤变为下面这种情形:
  A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新
  B线程进行读操作,从数据库中读入旧数据,共耗时N秒
  在B线程将旧数据读入缓存后,A线程将数据更新完成,此时数据不一致
  A线程将数据库更新完成后,休眠M秒(M比N稍大即可),然后再次淘汰缓存,此时缓存中即使有旧数据也会被淘汰,此时可以保证数据的一致性
  其它线程进行读操作时,缓存中无数据,从数据库中读取的是更新后的新数据

利用延时双删,可以很好的解决数据不一致的问题,其中A线程休眠M秒,需要根据业务上读取的时间来衡量,只要比正常读取消耗的实际稍大即可。

引入延时双删后存在的两个问题:
1、A线程需要在更新数据库后,还要休眠M秒再次淘汰缓存,等所有操作都执行完,这一个更新操作才真正完成,降低了更新操作的吞吐量
	解决办法:用“异步淘汰”的策略,将休眠M秒以及二次淘汰放在另一个线程中,A线程在更新完数据库后,可以直接返回成功而不用等待。
2、如果第二次缓存淘汰失败,则不一致依旧会存在
	解决办法:用“重试机制”,即当二次淘汰失败后,报错并继续重试,直到执行成功。

“先删缓存,再更新”的策略,如果采用同步更新缓存的策略,可能会导致数据长时间的不一致,

可以通过串行化、延时双删等方法避免数据不一致问题;如果采用异步更新缓存的策略,就不会导致数据不一致


方案4:先更新数据库,再淘汰缓存

先更新数据库,再删除缓存。并增加重试机制,将二者放到一个MQ中,避免出现缓存删除失败的问题。

在正常情况下:
  A线程进行写操作,更新数据库,淘汰缓存
  B线程进行读操作,从数据库中读取新的数据
不会有问题

在并发较大的情况下,情形1:
  A线程进行写操作,更新数据库,还未淘汰缓存
  B线程从缓存中可以读取到旧数据,此时数据不一致
  A线程完成淘汰缓存操作
  其它线程进行读操作,从数据库中读入最新数据,此时数据一致
不过这种情况并没有什么大问题,因为数据不一致的时间很短,数据最终是一致的

在并发较大的情况下,情形2:
  A线程进行写操作,更新数据库,但更新较慢,缓存也未淘汰
  B线程进行读操作,读取了缓存中的旧数据
但这种情况没什么问题,毕竟更新操作都还未完成,数据库与缓存中都是旧数据,没有数据不一致

在并发较大的情况下,情形3:
  A线程进行读操作,缓存中没有相应的数据,将从数据库中读数据到缓存,
此时分为两种情况,还未读取数据库的数据,已读取数据库的数据,不过由于网络等问题数据还未传输到缓存
  B线程执行写操作,更新数据库,淘汰缓存
  B线程写操作完成后,A线程才将数据库的数据读入缓存,对于第一种情况,A线程读取的是B线程修改后的新数据,没有问题,对于第二种情况,A线程读取的是旧数据,此时数据会不一致
不过这种情况发生的概率极低,因为一般读操作要比写操作要更快
万一担心存在这种可能,可以用“延迟双删”策略,在A线程读操作完成后再淘汰一次缓存

这个方案,无论采用同步更新缓存(从数据库读取的数据直接放入缓存中)还是异步更新缓存(数据库中的数据更新完成后,再将数据同步到缓存中),都不会导致数据的不一致

存在问题:如果第二步缓存淘汰失败,会导致数据的不一致。

解决方案:使用“重试机制”,如果淘汰缓存失败就报错,然后重试直到成功


简单来讲,可以通过**Cache Aside Pattern(旁路缓存模式)**实现。

该模式下,遇到写的请求时,会更新 数据库,然后淘汰缓存

这种情况下,如果淘汰缓存失败,会导致数据不一致问题。

  1. 缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适⽤。
  2. 增加重试机制(常⽤) : 如果 cache 服务当前不可⽤导致缓存删除失败的话,我们就隔⼀段时间进⾏重试,重试次数可以⾃⼰定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存⼊队列中,等缓存服务可⽤之后,再将 缓存中对应的 key 删除即可

两种方案对比:

1、先淘汰cache,再更新数据库:
	采用同步更新缓存的策略,可能会导致数据长时间不一致,如果用延迟双删来优化,还需考虑究竟需要延时多长时间的问题
		——读的效率较高,但数据的一致性需要靠其它手段来保证
	采用异步更新缓存的策略,不会导致数据不一致,但在数据库更新完成之前,都需要到数据库层面读取数据,读的效率不太好
		——保证了数据的一致性,适用于对一致性要求高的业务
  
2、先更新数据库,再淘汰缓存:
  无论是同步/异步更新缓存,都不会导致数据的最终不一致,在更新数据库期间,缓存中的旧数据会被读取,可能会有一段时间的数据不一致,但读的效率很好
  		——保证了数据读取的效率,如果业务对一致性要求不是很高,这种方案最合适

【其它】
重试机制可以采利用“消息队列MQ”来实现
通过订阅binlog来异步更新缓存,可以通过canal中间件来实现

16、Redis主从复制

Redis主从复制、故障转移源码

主从架构:

多台数据服务器中,只有一台主服务器,而主服务器只负责写入数据,不负责让外部程序读取数据。

存在多台从服务器,从服务器不写入数据,只负责同步主服务器的数据,并让外部程序读取数据。主服务器在写入数据后,即刻将写入数据的命令发送给从服务器,完成主从数据同步。

应用程序可以随机读取某一台从服务器的数据,分担了读数据的压力。当从服务器不能工作时,整个系统不受影响;当主服务器不能工作时,可以方便地将某从服务器作为主服务器工作。


--从服务器与主服务器之间的数据同步就是通过主从复制实现得到。

在这里插入图片描述

Redis 允许通过 SLAVEOF 命令或者 slaveof 配置项来让一个 Redis server 通过异步的方式复制另一个 Redis server 的数据集和状态,-----主从复制

复制机制的运行依靠三个特性:

  1. 当一个 master 和一个 slave 连接正常时,master 会发送一连串的命令流来保持对 slave 的更新,以便于将自身数据集的变更复制给 slave :包括客户端的写入、key 的过期或被逐出等
  2. masterslave 之间的连接断开后(断开的原因可能是网络问题或者连接超时) slave 重连上 master 并尝试进行部分重同步,这意味着它只会尝试获取在断开连接期间内丢失的命令流
  3. 当无法进行部分重同步时, slave 会请求进行全量重同步。这会涉及到一个更复杂的过程,例如 master 需要创建所有数据的快照,将之发送给 slave ,之后在数据集更改时持续发送命令流到 slave

SLAVEOF配置:

主从复制的开启完全是在slave发起的,不需要master做什么事。

slave开启主从复制的方式:

# 1、配置文件,在从服务器的配置文件中加入:
slaveof <masterip> <masterport>

# 2、启动命令,Redis server 启动命令后加入:
--slaveof <masterip> <masterport>

# 3、客户端命令 Redis server 启动后,直接通过客户端执行下面命令,则该Redis实例成为slave。
slaveof <masterip> <masterport>
主从复制的作用:
1、读写分离:master写、slave读,提高服务器的读写负载能力;master可以关闭持久化机制,减少不必要的IO操作且降低延迟。
2、负载均衡:基于主从结构,配合读写分离,由slave分担master负载,并根据需求的变化,改变slave的数量,通过多个从节点分担数据读取负载,大大提高服务器的并发量和数据吞吐量;但是由于复制机制的原因,主从数据存在不一致的时间窗口。
3、故障恢复:当mater出现问题时,由slave提供服务,实现快速的故障恢复;
4、数据冗余:实现数据热备份,是持久化之外的一种数据冗余方式;
5、高可用:基于主从复制,构建哨兵模式和集群,实现Redis的高可用方案。

--使Redis可以告别单机版本的单点风险,采用副本形式提高可用性,在master宕机时可以将slave提升为master继续提供服务,也为Redis集群模式的诞生奠定了基础。
主从复制和集群的区别:
主从复制:
复制机制中包含了一个master和多个slave,其中写请求只能master处理,数据的变更转化为数据流异步发送给slave进行更新;读请求则可以根据使用场景来规定是否由slave处理从而增加系统的读吞吐量。一旦 master 发生故障,slave 可以被提升为 master 从而继续提供服务。因此总结起来,slave 在复制机制的场景下,可以提供故障恢复、分担读流量和数据备份的功能。


集群Cluster:
集群机制的使用意味着你的数据量较大,数据会根据 Key 计算出的 slot 值自动在多个分片上进行分区(Partitioning),客户端对某个 Key 的请求会被转发到持有那个 Key 的分片上。分片由一个 master 和若干个 slave 组成,二者间通过复制机制同步数据。因此总结来看,集群模式更像分区和复制机制的组合。
主从复制机制的演变:

Redis 2.8之后对复制方式进行了优化:

将成本极高的sync替换为了psync,增加了断线重连情况下根据主从保存的offset–复制偏移量进行增量同步的功能。

从 Redis 2.6 到 4.0 开发人员对复制流程进行逐步的优化,以下是演进过程:

  • 2.8 版本之前 Redis 复制采用 sync 命令,无论是第一次主从复制还是断线重连后再进行复制都采用全量同步,成本高
  • 2.8 ~ 4.0 之间复制采用 psync 命令,这一特性主要添加了 Redis 在断线重连时候可通过 offset 信息使用部分同步
  • 4.0 版本之后也采用 psync,相比于 2.8 版本的 psync 优化了增量复制,这里我们称为 psync2,2.8 版本的 psync 可以称为 psync1
主从复制的原理:

Redis主从复制

主从复制过程可分为三个阶段:复制初始化、数据同步和命令传播。

1、复制初始化阶段
当执行完 slaveof 命令后,slave 根据指明的 master 地址向 master 发起 socket 连接,master 收到 socket 连接之后将连接信息保存,此时连接建立完成;当 socket 连接建立完成以后,slave 向 master 发送 PING 命令,以确认 master 是否存活,此时的结果返回如果是 PONG 则代表 master 可用,否则可能出现超时或者 master 此时在处理其他任务阻塞了,那么此时 slave 将断开 socket 连接,然后进行重试;

如果 master 连接设置了密码,则 slave 需要设置 masterauth 参数,此时 slave 会发送 auth 命令,命令格式为 auth + 密码 进行密码验证,其中密码为 masterauth 参数配置的密码,需要注意的是如果 master 设置了密码验证,从库未配置 masterauth 参数则会报错,socket 连接断开。当身份验证完成以后,slave 发送自己的监听端口,master 保存其端口信息,此时进入下一个阶段:数据同步阶段。

2、数据同步阶段
master 和 slave 都确认对方信息以后,便可开始数据同步,此时 slave 向主库发送 psync 命令(需要注意的是 redis 4.0 对 2.8 版本的 psync 做了优化),主库收到该命令后判断是进行增量同步还是全量同步,然后根据策略进行数据的同步,当 master 有新的写操作时候,此时进入复制第三阶段:命令传播阶段。

3、命令传播阶段
当数据同步完成以后,在此后的时间里 master-slave 之间维护着心跳检查来确认对方是否在线,每隔一段时间(默认10秒,通过 repl-ping-slave-period 参数指定)master 向 slave 发送 PING 命令判断 slave 是否在线,而 slave 每秒一次向 master 发送 REPLCONF ACK 命令,命令格式为:REPLCONF ACK {offset} ,其中 offset 指 slave 保存的复制偏移量,作用有:
	1)汇报自己复制偏移量,master 会对比复制偏移量向 slave 发送未同步的命令
	2)判断 master 是否在线

slave 接送命令并执行,最终实现与主库数据相同

全量复制(同步)和增量复制(同步)

在这里插入图片描述

Redis主从复制分为全量复制和增量复制。

Redis 在进行全量同步时,master 会将内存数据通过 bgsave 落地到 rdb,同时,将构建 内存快照期间 的写指令,存放到复制缓冲中,当 rdb 快照构建完毕后,master 将 rdb 和复制缓冲队列中的数据全部发送给 slave,slave 完全重新创建一份数据。

这个过程,对 master 的性能损耗较大,slave 构建数据的时间也比较长,而且传递 rdb 时还会占用大量带宽,对整个系统的性能和资源的访问影响都比较大。

而增量复制,master 只发送 slave 上次复制位置之后的写指令,不用构建 rdb,而且传输内容非常有限,对 master、slave 的负荷影响很小,对带宽的影响可以忽略,整个系统受影响非常小。

在 Redis 2.8 之前,Redis 基本只支持全量复制。在 slave 与 master 断开连接,或 slave 重启后,都需要进行全量复制。在 2.8 版本之后,Redis 引入 psync,增加了一个复制积压缓冲,在将写指令同步给 slave 时,会同时在复制积压缓冲中也写一份。

在 slave 短时断开重连后,上报master runid 及复制偏移量。如果 runid 与 master 一致,且偏移量仍然在 master 的复制缓冲积压中,则 master 进行增量同步。

但如果 slave 重启后,master runid 会丢失,或者切换 master 后,runid 会变化,仍然需要全量同步。

因此 Redis 自 4.0 强化了 psync,引入了 psync2。在 pysnc2 中,主从复制不再使用 runid,而使用 replid(即复制id) 来作为复制判断依据。同时 Redis 实例在构建 rdb 时,会将 replid 作为 aux 辅助信息存入 rbd。重启时,加载 rdb 时即可得到 master 的复制 id。从而在 slave 重启后仍然可以增量同步。

在 psync2 中,Redis 每个实例除了会有一个复制 id 即 replid 外,还有一个 replid2。Redis 启动后,会创建一个长度为 40 的随机字符串,作为 replid 的初值,在建立主从连接后,会用 master的 replid 替换自己的 replid。同时会用 replid2 存储上次 master 主库的 replid。这样切主时,即便 slave 汇报的复制 id 与新 master 的 replid 不同,但和新 master 的 replid2 相同,同时复制偏移仍然在复制积压缓冲区内,仍然可以实现增量复制。

全量复制一般发生在Slave初始化阶段,这时slave需要将master上的所有数据都复制一份。步骤如下:

  • 从服务器连接主服务器,发送psync命令;

  • 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;

  • 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;

  • 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;

  • 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;

  • 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

    增量复制一般是 Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。步骤如下:

  • 如果全量复制过程中,master-slave 网络连接断掉,那么 slave 重新连接 master 时,会触发增量复制。

  • master 直接从自己的 backlog 中获取部分丢失的数据,发送给 slave node,默认 backlog 就是 1MB。

  • master 就是根据 slave 发送的 psync 中的 offset 来从 backlog 中获取数据的。

Redis主从复制的功能及实现原理,哨兵

哨兵模式

Sentinel(哨兵)是Redis 的高可用性解决方案:由一个或多个Sentinel 实例 组成的Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。

选择规则:

1、所有在线的slave中选择优先级最高的,优先级可以通过slave-priority配置。
2、如果有多个最高优先级的slave,则选取复制偏移量最大(即复制越完整)的当选。
3、如果以上条件都一样,选取runid最小的slave。
也可以手动指定从服务器作为主服务器。

哨兵的功能:

Redis Sentinel(哨兵)主要功能包括主节点存活检测、主从运行情况检测、自动故障转移、主从切换。Redis Sentinel最小配置是一主一从。

Redis的Sentinel系统可以用来管理多个Redis服务器,该系统可以执行以下四个任务:

  • **监控:**不断检查主服务器和从服务器是否正常运行。
  • **通知:**当被监控的某个redis服务器出现问题,Sentinel通过API脚本向管理员或者其他应用程序发出通知。
  • **自动故障转移:**当主节点不能正常工作时,Sentinel会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点,这样人工干预就可以免了。
  • **配置提供者:**在Redis Sentinel模式下,客户端应用在初始化时连接的是Sentinel节点集合,从中获取主节点的信息。

在这里插入图片描述

异地多活源码优化

待补充

17、Redis集群模式

Redis主从复制、故障转移源码

Redis集群

1、Redis Cluster

大规模的数据存储系统,数据集越来越大,一主多从的模式无法支撑如此大量的数据存储,于是考虑将多个主从模式结合在一起提供服务。那么如何实现数据分片的逻辑?在哪里实现这部分逻辑?

解决方案:

1、引入 Proxy 层来向应用端屏蔽身后的集群分布客户端可以借助 Proxy 层来进行请求转发和 Key 值的散列从而进行数据分片,这种方案会损失部分性能但是迁移升级等运维操作都很方便,业界 Proxy 方案的代表有 Twitter 的 Twemproxy 和豌豆荚的 Codis

2、 smart client 方案,即将 Proxy 的逻辑放在客户端做,客户端根据维护的映射规则和路由表直接访问特定的 Redis 实例,但是增减 Redis 实例都需要重新调整分片逻辑。

Redis 3.0 版本开始官方正式支持集群模式,Redis 集群模式提供了一种能将数据在多个节点上进行分区存储的方法,采取了和上述两者不同的实现方案————去中心化的集群模式集群通过分片进行数据共享分片内采用一主多从的形式进行副本复制,并提供复制和故障恢复功能。

官方集群模式的设计考量:

在这里插入图片描述

在这里插入图片描述

(图是一个三主三从的Redis Cluster,在三个机房部署(其中一主一从构成一个分片,之间通过异步复制同步数据,一旦某个机房掉线,则分片上位于另一个机房的slave会被提升成master继续提供服务);每个 master 负责一部分 slot,数目尽量均摊;客户端对于某个 Key 操作先通过公式计算出所映射到的 slot,然后直连某个分片,写请求一律走 master,读请求根据路由规则选择连接的分片节点)

三种模式的优缺点:

在这里插入图片描述

2、哈希槽slot

Redis Cluster 中,数据分片借助哈希槽 ***(***slot) 来实现,集群预先划分 16384 个 slot,对于每个请求集群的键值对,根据 Key 进行散列生成的值唯一匹配一个 slot。Redis Cluster 中每个分片的 master 负责 16384 个 slot 中的一部分,当且仅当每个 slot 都有对应负责的节点时,集群才进入可用状态。当动态添加或减少节点时,需要将 16384 个 slot 做个再分配,slot 中的键值也要迁移。

HASH_SLOT = CRC16(key) mod 16384

实际使用时,会做一些改变支持哈希标签(Hash Tag)-- 确保两个键都在同一个哈希槽中。

内部实现:

Redis 集群中每个节点都会维护集群中所有节点的 clusterNode 结构体,其中的 slots 属性是个二进制位数组,长度为 2048 bytes,共包含 16384 个 bit 位,节点可以根据某个 bit 的 0/1 值判断对应的 slot 是否由当前节点处理。

每个节点通过 clusterStats 结构体来保存从自身视角看去的集群状态,其中 nodes 属性是一个保存节点名称和 clusterNode 指针的字典,而 slots 数组是一个记录哪个 slot 属于哪个 clusterNode 结构体的数组。

typedef struct clusterState {
  ... ...
    // 保存集群节点的字典,键是节点名字,值是clusterNode结构的指针
    dict *nodes;          /* Hash table of name -> clusterNode structures */
  // 槽和负责槽节点的映射
    clusterNode *slots[CLUSTER_SLOTS];
  ... ...
} clusterState;


typedef struct clusterNode {
  ... ...
    unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
    int numslots;   /* Number of slots handled by this node */
  ... ...
} clusterNode;

在这里插入图片描述

哈希槽的迁移

线上集群因为扩容和缩容操作,经常需要迁移 slot 对数据进行重新分片,原生的 Redis Cluster 可以借助 redis-trib 工具进行迁移。Squirrel 使用自研的 Squirrel migrate 进行数据迁移和分片 rebalance。

slot 在迁移过程有两个状态,在迁出节点会对该 slot 标记为 MIGRATING,在迁入节点会对该 slot 标记为 **IMPORTING。当该 slot 内的 Key 都迁移完毕之后,新的 slot 归属信息都经过消息协议进行传播,最终集群中所有节点都会知道该 slot 已经迁移到了目标节点,并更新自身保存的 slot 和节点间的映射关系。

3、MOVED & ASK

redis-cli 是官方提供的客户端脚本,我们可以通过 redis-cli -c -p port 命令连接任意一个 master,开始使用集群。

(1)MOVED

通过 redis-cli 可以发起对集群的读写请求,节点会计算我们请求的 Key 所属的 slot,一旦发现该 slot 并非由自己负责的话,会向客户端返回一个 MOVED 错误(需要注意的是集群模式下 redis-cli 不会打印 MOVED 错误而是会直接显示 Redirected,使用单机版 redis-cli 连接则可以看到 MOVED 错误),指引客户端重定向到正确的节点,并再次发送先前的命令,得到正确的结果。

//cluster 模式
10.72.227.3:6380> set gfdsdf sdf
-> Redirected to slot [6901] located at 10.72.227.2:6381
OK

//stand alone 模式
192.168.0.16:6379> set myKey myValue
(error) MOVED 16281 192.168.0.14:6379
192.168.0.16:6379> get myKey
(error) MOVED 16281 192.168.0.14:6379

MOVED 意为这个 slot 的负责已经永久转交给另一个节点,因此可以直接把请求转发给现在负责该 slot 的节点。

(2)ASK

但是考虑在 slot 迁移过程中,会出现属于该 slot 的一部分 Key 已经迁移到目的地节点,而另一部分 Key 还在源节点,那如果这时收到了关于这个 slot 的请求,那么源节点会现在自己的数据库里查找是否有这个 Key,查到的话说明还未迁移那么直接返回结果,查询失败的话就说明 Key 已经迁移到目的地节点,那么就向客户端返回一个 ASK 错误,指引客户端转向目的地节点查询该 Key。同样该错误仅在单机版 redis-cli 连接时打印。

(3)实际处理

客户端一般会在启动时通过解析 CLUSTER NODES 或者 CLUSTER SLOTS 命令返回的结果得到 slot 和节点的映射关系并缓存在本地,一旦遇到这两个错误时会再次调用命令刷新本地路由(因为线上集群一旦出现 MOVED 或者是 ASK 往往是因为扩容分片导致数据迁移,涉及到许多 slot 的重新分配而非单个,因此需要整体刷新一次),集群稳定时可以直接通过本地路由表迅速找到需要连接的节点。

4、故障检测

同大多数分布式系统一样,Redis Cluster***的节点间通过持续的***heart beat 来保持信息同步,不过 Redis Cluster 节点信息同步是内部实现的,并不依赖第三方组件如 Zookeeper。集群中的节点持续交换 PINGPONG 数据,消息协议使用 Gossip,这两种数据包的数据结构一样,之间通过 type 字段进行区分。

Redis 集群中的每个节点都会定期向集群中的其他节点发送 PING 消息,以此来检测对方是否存活,如果接收 PING 消息的节点在规定时间内(node_timeout)没有回复 PONG 消息,那么之前向其发送 PING 消息的节点就会将其标记为疑似下线状态(PFAIL)。每次当节点对其他节点发送 PING 命令的时候,它都会随机地广播三个它所知道的节点的信息,这些信息里面的其中一项就是说明节点是否已经被标记为 PFAIL 或者 FAIL。当节点接收到其他节点发来的信息时,它会记下那些被集群中其他节点标记为 PFAIL 的节点,这称为失效报告(failure report)。如果节点已经将某个节点标记为 PFAIL ,并且根据自身记录的失效报告显示,集群中的大部分 master 也认为该节点进入了 PFAIL 状态,那么它会进一步将那个失效的 master 的状态标记为 FAIL 。随后它会向集群广播 “该节点进一步被标记为 FAIL ” 的这条消息,所有收到这条消息的节点都会更新自身保存的关于该 master 节点的状态信息为 FAIL

5、故障转移–failover

(1)纪元–epoch

Redis Cluster使用了类似 Raft 算法中 term(任期)的概念 epoch(纪元),用来给事件增加版本号。Redis 集群中的纪元主要是两种:currentEpochconfigEpoch

  • currentEpoch --集群状态相关

可以记录集群状态变更的递增版本号。每个集群节点都会通过server.cluster->currentEpoch 记录当前的 currentEpoch

集群节点创建时,不管是 master 还是 slave,都置 currentEpoch 为 0。当前节点接收到来自其他节点的包时,如果发送者的 currentEpoch(消息头部会包含发送者的 currentEpoch)大于当前节点的***currentEpoch***,那么当前节点会更新 currentEpoch 为发送者的 currentEpoch

因此,集群中所有节点的 currentEpoch 最终会达成一致,相当于对集群状态的认知达成了一致。

currentEpoch 作用:

当集群的状态发生改变,某个节点为了执行一些动作需要寻求其他节点的同意时,就会增加 currentEpoch 的值。

目前 currentEpoch 只用于 slave 的故障转移流程,这就跟哨兵中的sentinel.current_epoch 作用是一模一样的。

slave A 发现其所属的 master 下线时,就会试图发起故障转移流程。首先就是增加 currentEpoch 的值,这个增加后的 currentEpoch 是所有集群节点中最大的。然后***slave A*** 向所有节点发起拉票请求,请求其他 master 投票给自己,使自己能成为新的 master。其他节点收到包后,发现发送者的 currentEpoch 比自己的 currentEpoch 大,就会更新自己的 currentEpoch,并在尚未投票的情况下,投票给 slave A,表示同意使其成为新的 master

  • configEpoch – 集群节点配置相关

每个集群节点都有自己的configepoch,节点配置是指节点负责的槽位信息。

每一个 master 在向其他节点发送包时,都会附带其 configEpoch 信息,以及一份它所负责的 slots 信息。而 slave 向其他节点发送包时,其包中的 **configEpoch信息和所负责槽位信息,是其 masterconfigEpoch 和所负责的 slot 信息。节点收到包之后,就会根据包中的 configEpoch 和所负责的 slots 信息,记录到相应节点属性中。

configEpoch 作用:

configEpoch 主要用于解决不同的节点的配置发生冲突的情况。

例子:

节点A 宣称负责 slot 1,其向外发送的包中,包含了自己的 configEpoch 和负责的 slots 信息。节点 C 收到 A 发来的包后,发现自己当前没有记录 slot 1 的负责节点(也就是 server.cluster->slots[1] 为 NULL),就会将 A 置为 slot 1 的负责节点(server.cluster->slots[1] = A),并记录节点 A 的 configEpoch。后来,节点 C 又收到了 B 发来的包,它也宣称负责 slot 1,此时,如何判断 slot 1 到底由谁负责呢?

这就是 configEpoch 起作用的时候了,C 在 B 发来的包中,发现 B 的 configEpoch,要比 A 的大,说明 B 是更新的配置。因此,就将 slot 1 的负责节点设置为 B(server.cluster->slots[1] = B)。

slave 发起选举,获得足够多的选票之后,成功当选时,也就是 slave 试图替代其已经下线的旧 master,成为新的 master 时,会增加它自己的 configEpoch,使其成为当前所有集群节点的 configEpoch 中的最大值。这样,该 slave 成为 master 后,就会向所有节点发送广播包,强制其他节点更新相关 slots 的负责节点为自己。

(2)自动 Failover

当一个 slave 发现自己正在复制的 master 进入了已下线(FAIL)状态时,slave 将开始对已下线状态的 master 进行故障转移,以下是故障转移执行的步骤:

  • 该下线的 master 下所有 slave 中,会有一个 slave 被选中。具体的选举流程为:slave 自增它的 currentEpoch 值,然后向其他 masters 请求投票,每个 slave 都向集群其他节点广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息用于拉票,集群中具有投票权的 master 收到消息后,如果在当前选举纪元中没有投过票,就会向第一个发送来消息的 slave 返回 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示投票给该 slave。某个 slave 如果在一段时间内收到了大部分 master 的投票,则表示选举成功。
  • 被选中的 slave 会执行 SLAVEOF no one 命令,成为新的 master
  • 新的 master 会撤销所有对已下线 masterslot 指派,并将这些 slot 全部指派给自己
  • 新的 master 向集群广播一条 PONG 消息,这条 PONG 消息可以让集群中的其他节点立即知道自己已经由 slave 变成了 master ,并且这个 master 已经接管了原本由已下线节点负责处理的 slot
  • 新的 master 开始接收和自己负责处理的 slot 有关的命令请求,故障转移完成

(3)手动 Failover

Redis 集群支持手动故障转移,也就是向 slave 发送 CLUSTER FAILOVER 命令,使其在 master 未下线的情况下,发起故障转移流程,升级为新的 master ,而原来的 master 降级为 slave

为了不丢失数据,向 slave 发送 CLUSTER FAILOVER 命令后,流程如下:

  1. slave 收到命令后,向 master 发送 CLUSTERMSG_TYPE_MFSTART 命令
  2. master 收到该命令后,会将其所有客户端置于阻塞状态,也就是在 10s 的时间内,不再处理客户端发来的命令,并且在其发送的心跳包中,会带有 CLUSTERMSG_FLAG0_PAUSED 标记
  3. slave 收到 master 发来的,带 CLUSTERMSG_FLAG0_PAUSED 标记的心跳包后,从中获取 master 当前的复制偏移量,slave 等到自己的复制偏移量达到该值后,才会开始执行故障转移流程:发起选举、统计选票、赢得选举、升级为 master 并更新配置

CLUSTER FAILOVER 命令支持两个选项:FORCETAKEOVER。使用这两个选项,可以改变上述的流程。

  • 如果有 FORCE 选项,则 slave 不会与 master 进行交互,master 也不会阻塞其客户端,而是 slave 立即开始故障转移流程:发起选举、统计选票、赢得选举、升级为 master 并更新配置。

  • 如果有 TAKEOVER 选项,则更加简单直接,slave 不再发起选举,而是直接将自己升级为 master ,接手原 masterslot,增加自己的 configEpoch 后更新配置。

因此,使用 FORCETAKEOVER 选项,master 可以已经下线;而不使用任何选项,只发送 CLUSTER FAILOVER 命令的话,master 必须在线。

Redis集群中的消息:

搭建 Redis Cluster 时,首先通过 CLUSTER MEET 命令将所有的节点加入到一个集群中,但是并没有在所有节点两两之间都执行 CLUSTER MEET 命令,因为节点之间使用 Gossip 协议进行工作–随时间推移,集群内所有节点会互相知道对方的存在。

Redis集群中,节点信息如何传播?

通过发送 PING 或 PONG 消息时,会包含节点信息,然后进行传播。

一个消息对象可以是 PING、PONG、MEET,也可以是 PUBLISH、FAIL 等。都是 clusterMsg 类型的结构,该类型主要由消息包头部和消息数据组成:

  • 消息包头部包含签名、消息总大小、版本和发送消息节点的信息。
  • 消息数据则是一个联合体 union clusterMsgData,联合体中又有不同的结构体来构建不同的消息。

其中***PING、PONG、MEET*** 属于一类,是 clusterMsgDataGossip 类型的数组,可以存放多个节点的信息,该结构如下:

typedef struct {
    // 节点名字
    char nodename[CLUSTER_NAMELEN];
    // 最近一次发送PING的时间戳
    uint32_t ping_sent;
    // 最近一次接收PONG的时间戳
    uint32_t pong_received;
    // 上次看到的节点的IP地址
    char ip[NET_IP_STR_LEN]; 
    // 上次看到的节点的端口号
    uint16_t port;              
    // 节点的标识
    uint16_t flags;            
    // 未使用
    uint16_t notused1;   
    uint32_t notused2;
} clusterMsgDataGossip;

每次发送 MEET、PING、PONG 消息时,发送者都从自己的已知节点列表中随机选出两个节点(可以是主节点或者从节点),并将这两个被选中节点的信息分别保存到两个结构中。当接收者收到消息时,接收者会访问消息正文中的两个结构,并根据自己是否认识 clusterMsgDataGossip 结构中记录的被选中节点进行操作:

  1. 如果被选中节点不存在于接收者的已知节点列表,那么说明接收者是第一次接触到被选中节点,接收者将根据结构中记录的IP地址和端口号等信息,与被选择节点进行握手。
  2. 如果被选中节点已经存在于接收者的已知节点列表,那么说明接收者之前已经与被选中节点进行过接触,接收者将根据 clusterMsgDataGossip 结构记录的信息,对被选中节点对应的 clusterNode 结构进行更新。

虽然 PING PONG 发送的频率越高就可以越实时得到其它节点的状态数据,但 Gossip 消息体积较大,高频发送接收会加重网络带宽和消耗 CPU 的计算能力,因此每次 Redis 集群都会有目的性地选择一些节点;但节点选择过少又会影响故障判断的速度,Redis 集群的 Gossip 协议选择了一种折中方案。

Redis集群数据一致性:

因为Redis的异步复制机制及可能出现的网络分区造成的脑裂问题,在特定条件下会出现丢失数据的问题。

1、异步复制

master 以及对应的 slaves 之间使用异步复制机制,考虑如下场景:

写命令提交到 mastermaster 执行完毕后向客户端返回 OK,但由于复制的延迟此时数据还没传播给 slave;如果此时 master 不可达的时间超过阀值,此时集群将触发 failover,将对应的 slave 选举为新的***master***,此时由于该 slave 没有收到复制流,因此没有同步到 slave 的数据将丢失。

2、脑裂(split-brain)

在发生网络分区时,有可能出现新旧 master 同时存在的情况,考虑如下场景:

由于网络分区,此时 master 不可达,且客户端与 master 处于一个分区,并且由于网络不可达,此时客户端仍会向 master 写入。由于 failover 机制,将其中一个 slave 提升为新的 master,等待网络分区消除后,老的 master 再次可达,但此时该节点会被降为 slave 清空自身数据然后复制新的 master ,而在这段网络分区期间,客户端仍然将写命令提交到老的 master,但由于被降为 slave 角色这些数据将永远丢失。

在这里插入图片描述

3、大多数master宕机

如果集群中少数 master 节点宕机,那么 Redis Cluster 依靠自身的机制可以提升相应的 slave 为 master 对外继续提高服务,但是一旦集群中大多数 master 节点都挂掉,那么集群本质上已经不可用了,即使双机房部署下,你的 slave 全部存活也没办法,

—Squirrel机房容灾进行优化

Squirrel 通过在 HA (Squirrel的高可用保障服务,在集群发生故障时进行相应的集群状态恢复操作)中引入 ZK Leader 争抢调度机制来解决在网络分区情况下可能出现的问题。

--------------------------------------------------------------------------———————————————————————————————

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值