Redis

Redis数据结构

讲讲Redis的5种数据结构的理解

5种,字符串,列表,哈希表对象,集合set,有序集合zset。
Redis对象有统一的数据结构,RedisObj,里面包含对象类型,对象底层实现数据结构标识encoding,指向底层结构的指针。

先说字符串
底层是名为SDS的结构,有buf字节数组,对应长度,未使用空间长度。好处是:
1.O(1)定位长度,
2.对\0兼容的二进制安全,
3.追加字符串,可利用未使用空间,减少内存重新分配空间次数;
4.空间不够时,通过自动分配新空间而不会像strcat函数引起缓冲区溢出:小于1MB,按2倍扩容,扩容阈值为1MB。
5.用5种不同大小的SDS,灵活保存,节省内存看空间。

  1. 空间节省还体现在编译优化上,我们先了解,编译器会有一个字节数对齐的工作,以更好地让CPU对内存的规整读写。SDS取消字节对齐,压缩空间。比如,结构体有char,int,对齐下是4+4字节,压缩后1+4字节。

不过,空间只增不减,需要手动调API惰性释放

列表
双向链表或压缩列表,3.2后用quicklist,50后用了listpack
双向链表
表头:头节点指针,尾节点指针,长度
节点:前驱指针,后驱指针,值指针

插入多时会比数组好,但不连续内存存储,产生空间碎片,无法利用CPU缓存;多出存储头尾节点的空间开销;

压缩列表
内存紧凑列表,没有所谓对齐空闲,很好利用CPU缓存;
表头3个,整体字节数,尾部偏移量,节点数,表尾1个,8位1结束标记;
节点内部:上个节点长度,当前节点类型及长度,节点数据

这种设计,大大节省内存空间。
举例:
列表中存储字符串,如果用双链表,每个节点有三个指针,加上SDS的2个长度的存储,再加上字符串本身。32位操作系统,一个指针6-7字节左右,三指针占18字节
如果字符串是个位数,基本每个整数占用1个字节,加起来20个字节左右,不包含字符串本身。
开启压缩列表,存储结构变化,它会按照序列化的形式存储,每个节点除了存储字符串本身,只需存2个整数,一个是前一个节点的字符串长度,另一个是当前节点字符串类型长度。若它是个位数长度,只需每个整数只占1个字节,总之,字符串本身加上两个字节的额外开销,跟之前字符串加上20字节的额外开销,差了N陪。这样,开启压缩列表,每项存储节省18字节。在此情况,做内存节省,适当将redis里的列表长度的阈值调整,默认512调整到2048或1024,不建议特别大。这样满足更长列表的列表压缩,节省更多空间。

但是,压缩列表会产生一定的性能消耗
一是读写过程需要编码解码;
二是连锁更新问题。新增或修改元素时,一旦空间不够了,压缩列表就会重新分配。特别是插入元素较大,会导致后续的上个节点长度的整型记录发生连锁空间变大,即后续所有空间都要重新分配,造成访问压缩列表的性能下降。
比如,前节点原本长度小于254字节,prevlen上节点长度只需1字节空间保存,如果前节点增大超过253,prevlen上节点长度要用5字节保存,而这,又导致当前节点的增大,同样,下一个节点为了记录当前节点长度,也要增大,这是一个多米诺牌的效应。

结论是,压缩列表保存小数据和几乎不变的数据,控制在连锁更新能够接受的尺度。

哈希桶 Hash
底层是压缩列表 + 字典,新版本有变化,压缩列表改为listpack
压缩列表已经讲过,这里主要是用来存储键key。
字典的结构是:2个ht哈希表,ht指向dictEntry哈希节点数组,数组元素指向dictEntry哈希节点链表;
1.先说哈希节点,内有key,v,next三个指针,v是联合体结构,里面val有两种含义,一种是8字节指针,一种是8字节长度整型或浮点数,无指向,空间节省优化。

2.采用数组+链表,是解决哈希冲突的链式哈希法。一个ht哈希表内部有数组,数组大小,掩码,节点数量;set的时候,通过key.hash对掩码与运输,定位数组桶位,头插法插入链表。

3.扩容缩容时的渐进式rehash。与hasmap不同,rehash执行过程分散到crud过程,减少单次set操作的性能开销。利用两个ht哈希表实现,一个ht用于存储旧数据;另一个空ht用于扩缩容数组。当达到负载因子阈值,空ht会扩容2倍,新数据进入到在新数组,同时,旧数据在每次crud时分批分次迁移。这是一种归并思想。

集合(sadd)
底层是整数集合或字典
字典已经介绍过。
下面介绍
整数集合intset
内部有:元素编码,数量,数组
优点:

  1. 连续内存空间,规整的元素大小划分,减少空间碎片
  2. 升级操作,针对不同规格的类型,选择编码合适的集合。当新元素超过当前编码,比如int16集合中新增int32,会对整个集合进行一个大小的升级。好处是节省内存资源。

不过,只增不减。

有序集合(Zset)
底层是压缩列表或跳表。新版压缩列表改为listpack。

跳表
一个带有层级关系的有序双向链表。
主要介绍三个参数,层级,分值,跨度。
每个节点有随机层级,不同层级有不同跨度的指向关系,越高层级跨度越大。利用浮点数分值的有序性,实现一种下楼梯式地层层检索,似二分查询,沿途跳过过多少节点,就是节点跨度。
还有前进指针和后退指针,后推指针用于从尾到头的检索。
好处是,查询效率下降一定小于log(N),且保持顺序性。

能够实现排行榜。

3.2版本的
quicklist
跟链表类似,区别是节点val采用了压缩列表指针,并额外存储字节大小,元素个数,能控制压缩列表的大小或者元素个数,规避潜在的连锁更新风险,并没有完全解决

5.0版本的
listpack
为了解决连锁更新问题,listpack代替压缩列表,舍弃前一个节点长度。
结构是:表头: 字节数,元素数量,表尾:结束标识
节点:编码,数据,编码+数据总长度。

Redis 3种高级结构

bitmaps
01数组,实际上String类型上的一组面向bit操作的集合。
用于活跃用户统计,布隆过滤器

HyperLogLogs
是用来做基数统计的算法,HyperLogLogs 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。

底层是散列出1w多个6bit的桶。类似bitmaps

实现滑动时间的UV数据统计

GEO
地理坐标
使用了GeoHash编码后,经纬度[120,40]就被编码成了1110011101,这个值就可以作为key对应的score值,把二维变成一维。

pipeline
网络管道技术,降低RT次数。
pipeline打包命令发送,节省网络时间。pipeline不是原子操作。
pipeline都会将数据顺序的传送顺序地返回(redis 单线程)

脚本
 大量 pipeline 应用场景可通过 Redis 脚本(Redis 版本 >= 2.6)得到更高效的处理,后者在服务器端执行大量工作。脚本的一大优势是可通过最小的延迟读写数据,让读、计算、写等操作变得非常快(pipeline 在这种情况下不能使用,因为客户端在写命令前需要读命令返回的结果)。

数据库空间

数据库空间是16隔DB,包括键-值空间和键-过期时间的字典。

删除策略

每一个键定时删除,CPU开销大;
每一次查询检查的惰性删除,容易内存泄露;
设置时间定期一次性检查删除,需要合理的频率和执行时长;
默认惰性+定期

RDB AOF对过期键的处理:
不同点:二者在主从服务器保持数据一致性载入文件对过期键的方式不同。
RDB文件,1.主服务器运行时载入会判断过期键不载入;2.从服务器运行时载入不会判断全载入,因为在主从服务器保持数据一致性时会先删除从服务器数据。
AOF文件,1.主服务器运行时载入判断过期键会在AOF文件中追加一条del命令;2.从服务器对过期键暂不处理,因为在主从服务器保持数据一致性时,主服务器统一发送del命令,从服务器才会处理过期键;AOF文件处理过期键是由主服务期统一管理。

垃圾回收

用引用计数器,因为没有循环引用。

AOF持久化日志

只记录写命令操作,追加方式;
先执行成功再记录
好处:
一是避开额外的检查开销
二是不会阻塞当前写操作命令执行;
风险:
一是非原子操作,未及时刷盘导致丢失
二是redis单线程写日志,受限于IO压力,可能给下一个命令造成阻塞

这都给写硬盘的时机有关

三种写回策略

写入过程:命令追加至AOF缓存区,然后write调用拷贝至内核缓冲区,等待内核刷盘,什么时候刷盘,由内核决定
Redis刷盘的3种策略:
Always:总是,每次同步
Everysec:每隔一秒
No:转交给系统控制

要么减少数据丢失,要么阻塞主进程,高可靠,高性能只能选其一;

系统刷盘函数:fsync()

AOF 重写

为了解决AOF日志过大问题,AOF会读取当前数据库所有数据,生成新的AOF文件,再进行旧AOF日志一次性覆盖。

AOF重写由子进程完成,避免阻塞主线程。
不使用线程是因为,多线程内存共享,修改 共享数据,需要加锁保证数据安全,降低性能。父子进程的采用数据副本,通过fork,系统把父进程的页表复制给子进程,页表是记录虚拟内存映射关系,不会物理复制。也就是说,虚拟空间不同,物理空间相同。
这样,实现内存共享,不过只标记为可读。当父子任意一方进行写一个页表项,CPU就会触发违反权限导致的缺页中断机制,会进行一部分页表项的物理复制,并重新调整映射,该过程就是写时复制:写操作时,才会对在写的物理内存复制。防止整体内存复制过长而引发父进程写阻塞当问题。
当然,复制进程页表,父进程会阻塞,但页表其实很小,开销不大。对在写的物理内存复制也会阻塞,这要数据量大小。如果是个bigkey,就有阻塞风险。

如果AOF重写过程中,主进程写入导致两块物理内存不一致,怎么办呢?
增加AOF重写缓冲区:重写AOF期间,写命令写入到”AOF缓冲区“与”AOF重写缓冲区“。
正常写入”AOF缓冲区“,是为了保证旧AOF文件日志也是ok的;
AOF文件重写工作完成后,此时还没覆盖旧AOF日志,会向主进程发送信号,这是进程之间的异步通信方式。

主进程接收到信号,把AOF重写缓冲区追加到新的AOF中,再把旧AOF日志覆盖。这个过程是阻塞的

整个过程,进程页表复制,写时复制,信号接收处理,都是阻塞的。其他情况不阻塞。

RDB 快照 默认

所谓快照,就是记录一瞬间的东西,好比拍一张照片。

RDB记录某一个瞬间的二进制内存数据,是全量快照,而AOF是命令日志

好处是Redis要恢复数据时,RDB效率更高。

提供了两个命令:
save 和 bgsave: 区别是是否在主进程执行。
开启以后,会定期执行保存,这是比较重的操作,这个频率要控制。

缺点:
发送宕机时,RDB记录旧的数据,比秒级AOF丢失更多。

执行快照时,数据能被修改吗?
可以,使用bgsave,也是子进程写时复制技术。
能同步刚修改的数据吗?
不同步,只能交由下一次bgsave操作。

RDB和AOF 混合文件持久化

为了高可用和高性能,控制持久化好频率。

混合持久化工作在 AOF 日志重写过程

当开启了混合持久化时,在 AOF 重写日志时,fork出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,RDB写入完成后通知主进程,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,然后,将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据,不再有AOF全量数据。

这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快

加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失

缓存三个异常问题

缓存雪崩

大量缓存失效,同一时间过期或者Redis 宕机。
导致Mysql压力剧增,引发系统崩溃。

解决:
如果是缓存过期:
在设置缓存时,提前过期时间散列化,增加随机数;

如果访问缓存不存在,使用互斥锁,保证一个时间内只有一个请求来构建缓存。其他请求等待完成。当然,要设置超时时间,防止长时间阻塞。但锁开销大。

主备双key,一个主key,有过期时间;一个备key,永久缓存。
但,更新缓存需要同时更新主备双key。

定期程序更新,缓存不设置过期时间。
但,导致缓存紧张,引发淘汰策略。
解决上面可以用:
一是线程不仅要定期更新缓存,还要频繁检查缓存是否有效,就是把BD和缓存对比检查,有被淘汰的,就马上读取DB更新缓存。几乎实时,不然只查到空值。不推荐

二是消息队列,发现数据淘汰后,通过消息队列发送一条消息到后台线程,后台线程接收消息,先检查缓存是否存在,再读取DB更新到缓存。

在业务上线,要把缓存先加载,这就是所谓缓存预热。

如果是Redis 宕机
首先,最好集群化保证高可用,进行一个主从节点的复制。
再一个是,构建限流,熔断机制。
为了减少业务的影响,触发请求限流,只允许少部分请求进入,等待重启Redis并缓存预热后,再解除限流。
还有就是,启动熔断机制,暂停业务应用对缓存服务的访问,直接返回服务不可用错误。等Redis恢复,再允许访问。这主要保证除了Redis缓存外,其他的整体的服务依然可用。

缓存击穿

频繁访问的热点数据过期,大量高并发请求直接访问数据库,造成数据库被冲垮。这问题算是缓存雪崩的一个子集:
解决:
互斥锁:未查到缓存时,保证一个时间内只有一个业务对一个缓存构建,其他请求该缓存,要么等待要么返回默认值/null

不设置过期时间,后台异步更新缓存,或者热点数据准备过期前,通知后台线程更新以及重新设置过期时间。

缓存穿透

访问的数据不在缓存,也不在数据库。
一是校验请求合法性,二是返回空值或默认值,三是布隆过滤器。

FAQ

Q:redis 到底是单线程 还是 多线程?

Redis 是单线程模型,这是指【从网络IO处理到实际的读写命令处理】都是单个线程完成的。有些命令是可用用后台子进程执行,比如快照生产,AOF重写。

严格意义上说,Redis4.0后并不是单线程,除了主线程之外,后台线程在处理一些比较长的操作,比如垃圾回收,无用连接的释放,大Key大删除。

在Redis6.0后,采用了多线程等待Socket读些,最终执行读写命令的过程依然在主线程。

Q:为什么处理操作命令的过程在单线程呢?

这样,Redis  不存在CPU瓶颈,主要受限于内存和网络。
如果采用多线程读写命令,虽然提高并发性能,但却引入了程序执行顺序的不确定性,带来并发读写的一系列问题, 增加系统复杂度,还有个线程的切换,甚至加锁解锁,死锁造成的性能消耗。

Q:AOF重写缓冲区会被占满吗?

理论不会,缓冲区维护着一些内存块的双向链表,每个节点指向对应aofblock空间。

Q:为什么AOF重写和RDB生成要开启子进程而不是线程?

因为如果使用线程,多线程之前就会共享内存。那么修改共享数据,需要加锁,锁会阻塞主线程。

子进程的方式,可以利用写时复制的技术,不用加锁。

Q:什么情况才会save?

当 Redis 内存数据高达几十 G,甚至上百 G 的时候,如果用 bgsave 进行 RDB 快照的话,在创建子进程的时候,会因为复制太大的页表而导致 Redis 阻塞在 fork() 函数,主线程无法继续执行,相当于停顿了

所以针对这种情况建议用 save。

虽然 save 会一直阻塞 Redis 直到快照生成完毕,但是它这个阻塞并不是意味着停顿了,而是在执行生成快照的程序,只是期间主线程无法处理接下来的读写命令。

Q:混合持久化如何区分开?

头部有有个REDIS 表明锁RDB内容,中间遇到RDB结束标记,然后再解析AOF格式。

主从复制

避免单点故障,最好做集群化,做主从备份。
多台服务器要保存同一份数据,就要保持数据一致性。
Redis 提供了主从复制模式,且主从之间,采用读写分离。

客户端写操作:
对所有的数据写入只在主服务上进行,然后,讲最新的数据同步到从服务器,这样,主从服务器的数据保持一致;
客户端读操作:
通过负载均衡策略,可以在任意一台服务器读取。

第一次同步

一是从服务器输入命令: replicaof 主IP+端口
建立连接,协商同步;
二是主服务器同步数据到从服务器,全量复制:主服务期生成并发送RDB文件,从接收后先清除,再载入。为了保证数据一执行,新写入的命令写到replication buffer 缓冲区中。
三是主服务器发送新写操作命令给从服务器;
将 replication buffer 缓冲区里所记录的写操作命令发送给从服务器,然后从服务器重新执行这些操作。

主从服务器再完成第一次同步后,会维护一个TCP连接。

后续主服务器可以通过这个连接继续将写操作命令传播给从服务器,然后从服务器执行该命令,使得与主服务器的数据库状态相同。

而且这个连接是长连接的,目的是避免频繁的 TCP 连接和断开带来的性能开销。

上面的这个过程被称为基于长连接的命令传播,通过这种方式来保证第一次同步后的主从服务器的数据一致性。

分摊主服务器的压力

刚刚讲到,第一次会有生成RDB文件和传输RDB文件两个耗时操作。
特别是从服务器非常多,数据量非常大,有两个问题:
一是fork阻塞主线程,redis无法正常速度处理读写请求;
二是RDB占用主服务网络带宽,也会影响命令请求的速度;

这个时候,需要设置助手,主从架构分成树状架构。从不仅可以接收主服务器的同步数据,自己也可以同时作为主服务器形式将数据同步给从服务器。

这种方式,把同步压力分摊到多个服务器。

增量复制

主从服务器再完成第一次同步后,会基于长连接进行命令传播。
这有个问题,就是网络的不稳定性。
一旦网络断开,就无法保持数据一致性。
2.8之前,断开恢复会进行一次全量复制,这种开销是很恐怖的。
从2.8开始,从服务器会采用增量复制,继续同步,也就是只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。

主要有3步:
1从服务器在恢复网络后,会发送psync命令到主服务器,与第一次有区别,offset参数不是-1
2主服务器收到命令后,然后用continue响应命令告诉从服务器,要 采用增量复制的方式同步数据;
3然后主服务器将从服务器断线期间,所执行的写命令发送给从服务器,然后从服务器执行这些命令。

Q:主服务器如何知道要传输哪些增量数据发送给从服务器?

依靠两个东西:
repl_backlog_buff环形缓冲区,用于主从服务器断连后,从中找到差异的数据;
replication offset,标记上面的那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用master_repl_offset来记录自己写到的位置,从服务器使用slave_repl_offset 来记录自己 读到的位置

Q:repl_backlog_buff缓冲区是什么时候写入的呢?

在主服务器进行命令传播时,不仅会将写命令发送从服务器,还会将写命令写入到repl_backlog_buff 缓冲区里,因此,这个缓冲区里会保存着最近传播到写命令

当网络断开后,当从服务器重新连上主服务器时,从服务器会通过psync命令将自己的复制偏移量,slave_repl_ofset发送给主服务器,主服务根据自己的master_repl_offset和slave_repl_offset 之间的差距,然后决定对从服务器执行哪种同步操作:
如果从服务器要读取的数据数据在缓冲区,采用增量同步;
否则,采用全量同步。

当主服务器在 repl_backlog_buffer 中找到主从服务器差异(增量)的数据后,就会将增量的数据写入到 replication buffer 缓冲区,这个缓冲区我们前面也提到过,它是缓存将要传播给从服务器的命令。

repl_backlog_buffer 缓行缓冲区的默认大小是 1M,并且由于它是一个环形缓冲区,所以当缓冲区写满后,主服务器继续写入的话,就会覆盖之前的数据。

因此,当主服务器的写入速度远超于从服务器的读取速度,缓冲区的数据一下就会被覆盖。

那么在网络恢复时,如果从服务器想读的数据已经被覆盖了,主服务器就会采用全量同步,这个方式比增量同步的性能损耗要大很多。

Q:如何解决覆盖问题?

因此,为了避免在网络恢复时,主服务器频繁地使用全量同步的方式,我们应该调整下 repl_backlog_buffer 缓冲区大小,尽可能的大一些,减少出现从服务器要读取的数据被覆盖的概率,从而使得主服务器采用增量同步的方式。

那 repl_backlog_buffer 缓冲区具体要调整到多大呢?

repl_backlog_buffer 最小的大小可以根据这面这个公式估算。

second* write_size_per_second

我来解释下这个公式的意思:

  • second 为从服务器断线后重新连接上主服务器所需的平均 时间(以秒计算)。

  • write_size_per_second 则是主服务器平均每秒产生的写命令数据量大小。

举个例子,如果主服务器平均每秒产生 1 MB 的写命令,而从服务器断线之后平均要 5 秒才能重新连接主服务器。

那么 repl_backlog_buffer 大小就不能低于 5 MB,否则新写地命令就会覆盖旧数据了。

当然,为了应对一些突发的情况,可以将 repl_backlog_buffer 的大小设置为此基础上的 2 倍,也就是 10 MB。

关于 repl_backlog_buffer 大小修改的方法,只需要修改配置文件里下面这个参数项的值就可以。

总结

主从复制共有三种模式:全量复制、基于长连接的命令传播、增量复制

主从服务器第一次同步的时候,就是采用全量复制,此时主服务器会两个耗时的地方,分别是生成 RDB 文件和传输 RDB 文件。为了避免过多的从服务器和主服务器进行全量复制,可以把一部分从服务器升级为「经理角色」,让它也有自己的从服务器,通过这样可以分摊主服务器的压力。

第一次同步完成后,主从服务器都会维护着一个长连接,主服务器在接收到写操作命令后,就会通过这个连接将写命令传播给从服务器,来保证主从服务器的数据一致性。

如果遇到网络断开,增量复制就可以上场了,不过这个还跟 repl_backlog_size 这个大小有关系。

如果它配置的过小,主从服务器网络恢复时,可能发生「从服务器」想读的数据已经被覆盖了,那么这时就会导致主服务器采用全量复制的方式。所以为了避免这种情况的频繁发生,要调大这个参数的值,以降低主从服务器断开后全量同步的概率。

双写一致性

先更新数据库,再更新缓存
一是 db:1 2 缓存:2 1

二是 非原子操作,缓存可能会失败

先更新缓存,再更新数据库
缓存:1 2 db:2 1

加分布式锁,保证一个时间只允许一个请求更新? 性能差,过期很快

旁路缓存策略。

  • 先删除缓存,再更新数据库;
    缓存: A删 B读旧改

  • 先更新数据库,再删除缓存。
    一是会写问题 A读db B更db B写缓存 A写旧缓存
    二是删除失败 写db后,删除失败,等过期才回写生效

缓存过期 + 延迟双删
第二次删除前加睡眠,确保A请求在双删内部时间中,B能够读取db,回写缓存,然后A睡醒再删除缓存。

这方案尽可能保证一致性,极端情况,也会出现不一致。
比如,第二次的删除全部崩溃,这样,与先删缓存,再更新数据无差别。

如何保证「先更新数据库 ,再删除缓存」这两个操作能执行成功?
一 是重试机制
二 是 订阅binlog,再操作缓存。

基于消息队列的重试

  1. 更新数据库数据;
  2. 缓存因为种种问题删除失败
  3. 将需要删除的key发送至消息队列
  4. 自己消费消息,获得需要删除的key
  5. 继续重试删除操作,直到成功

基于binlog的阿里中间件canal
canal 工作原理 canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump(转储) 协议,MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal ),canal 解析 binary log 对象(原始为 byte 流)

摘抄自:小林coding


Redis主从复制

为什么要主从复制?

1数据冗余 “数据的热备份” 2单机故障 “服务冗余” 3读写分离 “增加读请求能力” 4负载均衡 “分散单服务的压力” 5 高可用的基石 “哨兵和集群化”

什么是主从复制

将一台Redis服务器的数据,复制到其他Redis节点上,前者是主节点,后者是从节点,写操作交给主节点,读操作主要给从节点。

Redis主从工作原理

slave节点初次连接master节点,会发送psync命令,并且触发全量复制,此时master节点fork一个后台进程,开始生成一份RDB快照,同时将那些从外面接收 到写命令缓存到缓冲区中,RDB文件生成完毕后,将此文件发送给slave节点,slave节点写入磁盘,再从磁盘价值到内存,接着master会将增加缓冲区的写命令发送给slave,slave执行写命令并同步数据,如果slave节点和master节点因网络故障而中断,会自动重连,连接之后master节点会复制缺少的数据给slave节点。

同步流程

建立长连接, 全量数据同步, 增量同步:命令传播,数据反复同步。

主从同步类型

1全量复制 全量同步一般发生在slave初始化过程,这时slave需要将master上的所有数据复制一份; 2增量复制 增量同步一般是slave初始化后开始正常工作时,主服务器发生写操作同步到从服务器的过程;

主从刚连接,全量同步,再增量同步; redis优先考虑增量,如果不成功,就是会全量。

名词解释

runId 主节点ID offset 复制便宜量 replication buffer 内部队列缓冲区 建立连接时创建,全量和增量都会用 repl_backlog_buffer 环形缓冲区 开启命令传播之前,会建立buffer。 buffer记录当前的master接收到新的写操作,offset和命令本身,是所有slave公用的buffer,salve发送psync之后,会和master的offset比较,,来决定是否增量复制。

全量复制流程 (初始化)
  • master连接slave:【slave上输入save of命令】,发送psync ? -1 命令,用于同步数据 “?” 表示是master的runID,“-1”表示offset第一次复制;

  • master回复ACK告知slave,它的runId和offset,slave将其保存

  • master fork子进程(如果单线程做耗时动作,性能非常差),生成RDB+AOF组合快照,然后向所有slave发送快照文件,并通过写时复制技术,继续执行接下来用户的一些写命令,并存放到replication buff 缓冲区当中;

  • slave 接收后,把本地数据清空,然后把快照加载到本地磁盘,接着加载到内存来执行。

  • master 发送replication buffer缓冲区给salve同步

增量复制流程 (重连接)

1从服务器在恢复网络后,会发送psync runId offset命令到主服务器,与第一次有区别,offset参数不是-1 ;(runId比较,如果不一样,就全量复制) 2主服务器收到命令后,两offset求距离差,小于replication buff缓冲区大小,然后用continue响应命令告诉从服务器,要 采用增量复制的方式同步数据; 3然后主服务器将从服务器断线期间,所执行的写命令发送给从服务器,然后从服务器执行这些命令。

面试题
Q:长连接还是短连接?

长,减少连接开销;

Q:判断某个节点是否正常?

主从ping-pong心跳检查机制,一半以上的节点区ping一个节点,如果没有pong回应,集群认为该节点宕机; master 10s发送一次,检查对方在线 slave 1s发一次,给从节点发送自己的复制偏移量,获取最新变更+检查对方在线

Q:过期key如何处理?

处理key或者淘汰key,master模拟del命令发送给slave;

Q: redis主从切换如何减少数据丢失?

1 一般是异步复制丢失。 解决: 一master本地缓存或持久化磁盘,在一段时间后写入新master; 二先写入rocketMQ,再发送一个延时消息去写入master。 2 脑裂丢失 脑裂就是一部分slave误以为master已死,选举新的master后,旧的master依旧存活,导致出现了两个master,从而读写数据混乱。 避免脑裂: 一种是投票超过半数作为leader,才被其他节点认可,这也是zookeeper方案。 重点是第二种,哨兵模式。 哨兵群会监控节点存活情况,当超过半数哨兵认为master主观下线,即master转为客户下线,就要推举新master。 哨兵内部讨论,每人1票,投超过半数,成为选举人。 选1举人,根据先优先级,再是节点的同步率,最后是创建时间,选择新的master。 slave会全量同步新的master。 旧的master恢复后,降级为slave,也会全量同步。 这个过程中,旧master全量同步过程就是产生脑裂丢失 解决: 两个参数 min-slaves-to-write 2 至少2个slave min-slaves-max-lag 5 至少5秒 至少2个slave进行同步复制延迟不超过5秒,一旦违反,master停止接收任何请求,让原master下线,选举新的master。尽可能

Q:redis如何做到故障自动切换?

哨兵模式内部选举出一个选举人,选举人选举新的master。

Q:数据备份方式

1热备 主库承担业务流量,通过实时备份数据到从库。

2冷备 主库承担业务流量,通过定期或者手动执行脚本备份数据到从库。 (redis)

3多活 由两个数据中心承担业务流量,互为主备,一般主数据中心会承担大部分流量,备数据会承担小部分流量。(多地域)

摘抄自:tojson