《剑指Java面试-Offer直通车》--Redis

目录

一、Redis简介

缓存中间件--Memcache和Redis的区别

为什么Redis这么快?

多路IO复用模型

FD

传统的阻塞I/O模型

多路复用函数

二、Redis常用数据类型

常用数据类型

底层实现

三、Redis数据过期策略、缓存雪崩、缓存穿透、缓存击穿

数据过期策略

如何解决 Redis 缓存雪崩问题

如何解决 Redis 缓存穿透问题

如何解决 Redis 缓存击穿问题

四、Redis和MySQL的双写一致性

五、从海量Key里查询出某一固定前缀的Key

六、如何实现分布式锁

分布式锁需要解决的问题

如何通过Redis实现分布式锁?

如何解决SETNX长期有效的问题?

七、如何使用Redis做异步队列?

使用List作为队列,rpush生产消息,lpop消费消息

BLPOP key [key...] timeout

pub/sub,主题订阅者模式

八、Redis如何做持久化?

1)基于BGSave的RDB快照持久化方式

RDB的创建与载入

触发rdb持久化的方式

BGSave的原理

RDB持久化的缺点

2)AOF增量持久化方式

Redis数据的恢复

RDB和AOF的优缺点?

3)RDB-AOF混合持久化方式

九、Redis高可用

1)主从同步

pipeline

Redis的同步机制

Redis Sentinel

2)Redis集群


一、Redis简介

MySQL的数据都是存放在磁盘中的,虽然在数据库层也做了对应的缓存,但这种数据库层次的缓存一般针对查询的内容,而且粒度也比较小。一般只有表中数据没有发生变动时,数据库对应的Cache才会发挥作用,这不能减少业务系统对数据库产生的增删改查的IO压力。因此缓存数据库应运而生,该技术实现了对热点数据的高速缓存,提高应用的响应速度,极大缓解后端数据库的压力。

缓存中间件--Memcache和Redis的区别

Memcache对数据类型的支持简单,只支持简单的key-value;不支持数据持久化存储(数据全部存在内存之中,一旦服务器宕机数据没办法保存);不支持主从;不支持分片。

Redis数据类型丰富;有部分数据存在硬盘上,这样能保证数据的持久性;支持主从;支持分片。redis目前官方只支持LINUX 上去行,从而省去了对于其它系统的支持,这样的话可以更好的把精力用于本系统 环境上的优化。底层模型上,新版本的redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

为什么Redis这么快?

100000+QPS(QPS即query per second,每秒内查询次数)

1)完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高。Redis是采用单进程单线程模型的K-V数据库,由c语言编写,将数据存储到内存中,读写数据的时候都不会受到硬盘IO速度的限制。

2)数据结构简单,对数据操作也简单。Redis不使用表,它的数据库不会预定义或者强制要求用户对Redis存储的不同数据进行关联,因此性能相比关系型数据库要高出不止一个量级,其存储结构就是键值对,类似于hashMap。hashMap的优势就是查找和操作的时间复杂度都是O(1)的。

3)采用单线程,单线程也能处理高并发请求,想多核也可启动多实例。在面对高并发的请求的时候,首先想要的是多线程来进行处理,将IO线程和业务线程分开,业务线程使用线程池来避免频繁创建线程和销毁线程,即便是一次请求,阻塞了也不会影响到其它请求。Redis单线程结构是指主线程是单线程的,主线程包含IO事件的处理,以及IO对应的相关请求的业务处理。此外,主线程还负责过期键的处理、复制协调、集群协调等等。这些除了IO事件之外的逻辑会被封装成周期性的任务,由主线程周期性的处理。因为采用单线程的设计,对于客户端的所有读写请求,都由一个主线程串行的处理,因此多个客户端同时对一个键进行写操作的时候,就不会有并发的问题,避免了频繁的上下文切换和锁竞争,使得Redis执行起来效率更高。单线程是可以处理高并发的请求的,并发不是并行,并行性意味着服务器能够同时执行几个事情,具有多个计算单元,而并发性IO流意味着能够让一个计算单元来处理来自多个客户端的流请求。Redis使用单线程配合上IO多路复用,可以大幅度的提升性能。CPU不是制约redis的性能瓶颈,此外,可以在多核的服务器中启动多个Redis实例来利用多核的特性。

注意,这里的单线程只是在处理我们的网络请求的时候,只有一个单线程来处理,一个正式的Redis server,在运行的时候,肯定不止一个线程的。例如Redis在进行持久化的时候,会根据实际情况,以子进程或者子线程的方式执行。

4)使用多路I/O复用模型,非阻塞IO。Redis是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或者输出都是阻塞的,所以IO操作在一般情况下,往往不能直接返回,就会导致某一文件的IO阻塞,进而导致整个进程无法对其它客户端提供服务。而IO多路复用就是为了解决这个问题而出现的。

多路IO复用模型

  • FD

File Descriptor,文件描述符。在操作系统中,一个打开的文件通过唯一的描述符进行引用,该描述符是打开文件的元数据到文件本身的映射,在Linux内核中,该描述符称为文件描述符即File Descriptor,文件描述符用一个整数来表示。

  • 传统的阻塞I/O模型

当使用read或者write对某一个文件描述符FD进行读写的时候,如果当前的FD不可读或者不可写,整个Redis服务就不会对其它的操作做出响应,导致整个服务不可用,这也就是传统意义上的阻塞模型,阻塞模型会影响其它FD对应的服务,所以在需要处理多个客户端任务的时候,往往都不会使用阻塞模型。此时,需要一种更高效的I/O模型来支持Redis的高并发处理,就是I/O多路复用模型。

I/O多路复用模型,最重要的函数调用就是Select系统调用。Select可以同时监控多个文件描述符的可读可写情况,当其中的某些文件描述符可读或者可写时,Select方法就会返回可读以及可写的文件描述符个数,也就是说,Selector是负责监听我们的文件是否可读或者可写的,监听的任务交给Selector之后,程序就可以做其它的事情,而不被阻塞。

  • 多路复用函数

与此同时,也有其它的I/O多路复用函数,Redis采用的I/O多路复用函数:epoll、kqueue、evport、select。epoll、kqueue、evport相比select的性能更加优秀的,同时也可以支撑更多的服务。

Redis采用的多路复用函数是因地制宜的。Redis需要在多个平台下运行,为了最大化的提高执行效率和性能,会根据编译平台的不同选择不同的IO多路复用函数作为子模块,提供给上层统一的接口。

Redis优先选择时间复杂度为O(1)的IO多路复用函数作为底层实现。

以时间复杂度为O(n)的select作为保底。如果没有epoll、kqueue、evport,就会使用select,select在使用时会扫描全部的文件描述符,性能较差,时间复杂度是O(n)。

基于react设计模式监听I/O事件。Redis服务采用react设计模式来实现文件处理器。文件事件处理器使用I/O多路复用模块,同时监听多个FD,当accept、read、write等文件事件产生的时候,文件事件处理器就会回调FD绑定的事件处理器,虽然整个文件事件处理器是在单线程运行的,但是通过I/O多路复用模块的引用,实现了同时对多个FD读写的监控,提高了网络通信模型的性能,同时来保证了整个Redis服务实现的简单。

 

二、Redis常用数据类型

常用数据类型

String:最基本的数据类型,二进制安全,Redis的String可以包含任何数据,比如jpg图片或者序列化的图像。常用在缓存、计数、共享Session、限速等。

Hash:hash类型是一个string类型的field和value的映射表,每个 hash 可以存储 2^32 - 1 键值对(40多亿),hash类型的结构(key, field, value),适合用于存储对象。哈希可以用来存放用户信息,比如实现购物车。

List:列表,按照String元素插入顺序排序,是简单的字符串列表。类似栈,先进后出的顺序。可以做简单的消息队列的功能。

Set:集合,String元素组成的无序集合,通过哈希表实现,不允许重复。添加、删除、查找的复杂度是O(1)。提供了并集、交集、叉集操作。

Sorted Set:通过分数来为集合中的成员进行从小到大的排序。Redis的zset和set集合一样,也是String集合组成的集合,且不允许重复的成员,不同的是有序集合每个元素都会关联一个double类型的分数,redis正是通过这个分数,来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但是分数却可以重复。分数越小越靠前。可以做排行榜应用,取 TOP N 操作。

用于计数的HyperLoglog,用于支持存储地理位置信息的Geo等等。

底层实现

参考:Redis的五大数据类型的底层实现Redis数据结构底层实现

 

三、Redis数据过期策略、缓存雪崩、缓存穿透、缓存击穿

数据过期策略

Redis 中数据过期策略采用定期删除+惰性删除策略

1)定期删除策略:Redis 启用一个定时器定时监视所有的 key,判断key是否过期,过期的话就删除。这种策略可以保证过期的 key 最终都会被删除,但是也存在严重的缺点:每次都遍历内存中所有的数据,非常消耗 CPU 资源,并且当 key 已过期,但是定时器还处于未唤起状态,这段时间内 key 仍然可以用。

2)惰性删除策略:在获取 key 时,先判断 key 是否过期,如果过期则删除。这种方式存在一个缺点:如果这个 key 一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量的空间。

这两种策略天然的互补,结合起来之后,定时删除策略就发生了一些改变,不在是每次扫描全部的 key 了,而是随机抽取一部分 key 进行检查,这样就降低了对 CPU 资源的损耗,惰性删除策略互补了为检查到的key,基本上满足了所有要求。但是有时候就是那么的巧,既没有被定时器抽取到,又没有被使用,这些数据又如何从内存中消失?没关系,还有内存淘汰机制(保证Redis中存放的都是热点数据),当内存不够用时,内存淘汰机制就会上场。淘汰策略分为:

    a)当内存不足以容纳新写入数据时,新写入操作会报错。(Redis 默认策略)

    b)当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(LRU推荐使用)

    c)当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。

    d)当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。

    e)当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。

    f)当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。

如何解决 Redis 缓存雪崩问题

缓存雪崩: 缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

1)缓存时间不一致,给缓存的失效时间,加上一个随机值,避免集体失效

2)使用 Redis 高可用架构:使用 Redis 集群来保证 Redis 服务不会挂掉

3)限流降级策略:有一定的备案,比如个性推荐服务不可用了,换成热点数据推荐服务

如何解决 Redis 缓存穿透问题

缓存穿透: 缓存穿透是指缓存和数据库中都没有的数据

1)在接口做校验

2)存null值

3)布隆过滤器拦截: 将所有可能的查询key 先映射到布隆过滤器中,查询时先判断key是否存在布隆过滤器中,存在才继续向下执行,如果不存在,则直接返回。布隆过滤器将值进行多次哈希bit存储,布隆过滤器说某个元素在,可能会被误判。布隆过滤器说某个元素不在,那么一定不在。

如何解决 Redis 缓存击穿问题

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

1)设置热点数据永远不过期

2)加互斥锁。缓存失效的时候,先锁住,等有缓存了,再解锁

3)接口限流与熔断、降级

 

四、Redis和MySQL的双写一致性

参考:Redis与Mysql双写一致性方案解析

 

五、从海量Key里查询出某一固定前缀的Key

假如Redis里面有一亿个key,其中十万个key是以某个固定已知的前缀开头的,如何将它们全部找出来?

摸清数据规模,即问清楚边界。

1)KEYS pattern:查找所有符合给定模式pattern的key。由于keys一次性返回所有的key,如果key的数量过大,会导致客户端被卡住。Redis中的key非常多的时候,对内存的消耗和Redis服务器都是一个隐患。

2)Scan指令,可以无阻塞的提取出指定的默认的key列表,scan每次执行只会返回少量元素,所以可以用于生产环境,而不会出现像keys命令带来的可能会阻塞服务器的问题。

Scan指令模式如下所示:

SCAN cursor [MATCH pattern] [COUNT count]:基于游标cursor的迭代器,需要基于上一次的游标延续之前的迭代过程。以0作为游标开始一次新的迭代,直到命令返回游标0完成一次遍历。(当scan指令的游标参数即cursor被置为0的时候,服务器将开始一次新的迭代,而当服务器向用户返回值为0的游标的时候,就表示迭代完成。以0作为游标开始新一次的迭代,一直调用scan指令直到命令返回游标0,称这个过程为一次完整的遍历。)

不保证每次执行都返回某个给定数量的元素,支持模糊查询。(Scan增量式迭代命令并不保证每次执行都会返回某个给定数量的元素,甚至可能返回0个元素,但只要命令返回的游标不是0,应用程序就不应该将迭代视作结束,命令返回的元素数量总是符合一定的规则的。对于一个大数据集来说,增量式迭代命令每次最多可能会返回数十个元素,而对于一个足够小的数据集来说,可能会一次迭代返回所有的key,类似于keys指令,scan可以通过给定match参数的方式传入要查找键位的模糊匹配方式,让命令只返回给定模式下相匹配的元素。)

一次返回的数量不可控,只能是大概率符合count参数。(对于增量式迭代命令是没有办法保证每次迭代所返回的元素数量的,我们可以使用count选项对命令的行为进行一定程度的调整,count选项的作用就是让用户告知迭代命令,在每次迭代中,应该从数据集里返回多少元素,使用count选项对于增量式迭代命令相当于是一种提示,大多数情况下,这种提示都是比较有效的控制返回的数量。值得注意的是,count数量并不能严格的控制返回的key的数量,只能说是一个大致的约束,并非每次迭代都会返回count数量的约束,用户可以根据自己的需求在每次迭代中随意改变count的值,只要记得将上次迭代返回的游标用到下次迭代的游标里面就可以了。)

可能会获取到重复key,在程序中进行处理。

 

六、如何实现分布式锁

分布式锁是控制分布式系统或者不同系统之间共同访问共享资源的一种锁的实现,如果不同的系统或者同一个系统不同主机之间共享了某个资源的时候,往往需要互斥来防止彼此干扰,进而保证一致性。

分布式锁需要解决的问题

1)互斥性:任意时刻只能有一个客户端获取锁,不能同时有两个客户端获取到锁。

2)安全性:锁只能被持有该锁的客户端删除,不能由其它客户端删除掉。

3)死锁:获取锁的客户端因为某些原因而宕机,而未能释放锁,其它客户端再也无法获取到该锁,而导致的死锁,此时需要有机制避免这种问题的发生 。

4)容错:当部分节点宕机的时候,客户端仍然能够获取锁和释放锁。

如何通过Redis实现分布式锁?

SETNX key value:如果key不存在,则创建并赋值。

时间复杂度:O(1)

返回值:设置成功返回1,设置失败返回0。

正因为SETNX的操作是原子性的,因此初期便被用在实现分布式锁。在执行某段代码逻辑的时候,先尝试使用SETNX对某个key设值,如果设值成功,则证明此时没有别的线程在执行该段代码,或者说占用该独占资源,这个时候线程就可以顺利的去执行该段代码逻辑了;如果设值失败,则证明此时有别的程序或者线程占用该资源,那么当前线程就需要等待直至设置SETNX成功。如果设置SETNX的key,这个key就会长久有效了,后续线程如何能再次获得到锁,此时需要给该key设置一个过期时间。

如何解决SETNX长期有效的问题?

EXPIRE key seconds

设置key的生存时间,当key过期时(生存时间为0),会被自动删除。使用EXPIRE设置过期时间的缺点就是原子性得不到满足。如果SETNX与EXPIRE结合使用,它们分别都是原子性的,但是组合到一起却不是原子性的。

Redis2.6.12之后,通过set原子操作将SETNX和EXPIRE揉到一起去执行。

SET key value [EX seconds] [PX milliseconds] [NX | XX]

1)EX seconds,设置键的过期时间为second秒。

2)PX milliseconds,设置键的过期时间为millisecond毫秒。

3)NX,只在键不存在的时候,才对键进行设置操作。效果等同于SETNX。

4)XX,只在键已经存在的时候,才对键进行设置操作。

SET操作成功完成时候,返回OK,否则返回nil。  

 

七、如何使用Redis做异步队列?

使用List作为队列,rpush生产消息,lpop消费消息

在这种生产者和消费者的模式里面,当lpop没有消息的时候,说明消息暂时被消费完毕,并且生产者还没有来得及生产数据。

缺点:没有等待队列里面有值就直接消费(lpop是不会等待队列里有值才会去消费的)。

弥补:可以通过在应用层引入sleep机制去调用lpop重试。进而实现一个简单的异步队列。

BLPOP key [key...] timeout

如果不想使用sleep重试,可以使用blpop的方式。

阻塞直到队列有消息或者超时。在没有消息的时候会阻塞住,直到消息的到来或者超时。blpop可以替代sleep做更精准的阻塞控制。
缺点:只能供一个消费者消费,lpop或者blpop之后就没了。

pub/sub,主题订阅者模式

是否可以只生产一次,就让多个消费者消费呢?可以使用redis的pub/sub,主题订阅者模式,实现一对多的消息队列。

发送者pub发送消息,订阅者sub接收消息。

订阅者可以订阅任意数量的频道(topic,消费者关注的主题)。

pub/sub的缺点:消息的发布是无状态的,无法保证可达。无法保证消息是否被接收到,是否在传输过程中丢失。对于消费者来说,消息是即发即失的。若某个消费者在生产者发布消息时下线,重新上线后接收不到该消息。要解决这个问题需要专业的消息队列,如Kafaka。
 

八、Redis如何做持久化?

Redis提供了三种持久化的方案,将内存中的数据保存到磁盘中,避免数据丢失。

1)基于BGSave的RDB快照持久化方式

特定的间隔保存那个时间点的全量数据快照。

RDB相关的配置在redis根目录下的redis.conf中。redis服务启动时,会自动加载redis.conf中的信息。

redis.conf中的主要配置:

1)RDB持久化的时间策略举例:save 900 1指900秒内有一条写入指令就促发产生一次快照。产生一次快照就理解为进行一次备份。

2)stop-writes-on-bgsave-error yes指备份进程出错时,主进程停止接收新的写入操作。

3)rdbcompression yes和rdb文件压缩相关,设置成yes表示在备份时对rdb文件进行压缩后才去做保存。建议设置为no,redis本身属于cpu密集型服务器,开启压缩会带来更多的cpu消耗,相比硬盘成本,cpu更值钱。禁用rdb配置在save后加一行save ""。

src目录下有dump.rdb文件,表明当前选择的方式是rdb方式。dump.rdb是一个二进制文件。

RDB的创建与载入

RDB文件可以通过以下两个命令来生成:

1)SAVE,阻塞Redis的服务器进程,直到RDB文件被创建完毕。SAVE很少被使用,因为save操作是在主线程中保存快照的,由于Redis是用一个主线程来处理所有的请求的,这种方式会阻塞所有的客户端请求。

2)BGSAVE,fork出一个子进程来创建RDB文件,记录接收BGSAVE当时的数据库状态,父进程继续处理接收到的命令,子进程完成文件的创建之后会发送信号给父进程即Redis的主进程,而于此同时,父进程处理命令的同时,通过轮询来接收子进程的信号,不阻塞服务器进程。BGSAVE指令是使用后台方式保存RDB文件,调用此命令后会立刻返回OK返回码,Redis会产生一个子进程进行处理,并立刻恢复对客户端的服务,在客户端可以使用last save这个指令,查看操作是否成功,last save记录了上一次成功执行save或者bgsave命令的时间。

触发rdb持久化的方式

可以使用java计时器或者quartz来定期调用redis的bgsave指令去备份rdb文件,并按照时间戳存储不同的rdb文件,作为redis某段时间的全量备份脚本。

除了上面的手动方式触发rdb持久化,还有自动化触发RDB持久化的方式。自动触发的场景主要有以下几点:

1)根据redis.conf配置里面的save m n规则定时触发,这里面的save使用的是bgsave异步备份。

2)主从复制的时候,主节点自动触发。主节点发送RDB文件给从节点完成复制操作,主节点就会触发bgsave。

3)执行Debug Reload。

4)redis服务执Shutdown且没有开启AOF持久化。

BGSave的原理

先检查当前主进程有没有AOF或RDB子进程,有返回错误,防止子进程间的竞争。意味着在执行BGsave期间客户端发送的save、bgsave命令会被服务器拒绝执行。如果此时没有发现相关子进程,则会促发持久化,调用redis源码中的rdbSaveBackground方法,执行fork系统调用。操作系统fork指令用来创建进程。Linux下fork系统调用实现了Copy-on-Write,写时复制。传统方式下fork函数创建子进程时直接把所有资源复制给子进程,实现方式简单,但效率低下,而且复制的资源可能对子进程毫无用处。Linux为了降低创建子进程的成本,改进fork实现方式,当父进程创建子进程时,内核只为子进程创建虚拟空间,父子两个进程使用相同的两个物理空间,只有父子两个进程发生更改时,才会为子进程分配独立的物理空间。

Copy-on-Write(简称COW)写时复制,是计算机程序设计领域的优化策略,核心思想是如果有多个调用者同时要求相同资源(如内存或者磁盘上的数据存储),它们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容的时候,系统才会真正复制一份专用副本给该调用者,而其它调用者所见到的最初的资源仍然保持不变。这个过程对其他的调用者是透明的。此做法的主要优点是如果调用者没有修改该资源,就不会有副本被创建,多个调用者只是读取操作时可以共享同一份资源。

COW在处理过程中需要维持一个为读请求使用的指针,并在新数据写入完成后更新这个指针,以提升读写并发能力。因此COW也间接提供了数据更新过程中的原子性,在保证完整性的同时还保持读写效率。当redis需要持久化时,redis会fork一个子进程,子进程将数据写入磁盘上一个临时的rdb文件中,当子进程完成写临时文件时,将原来的rdb替换掉。这样的好处是实现copyOnWrite,子进程继续接收其他请求,确保了redis的性能。当redis需要做持久化时,redis就会调用fork创建子进程,父进程继续处理client请求,子进程负责将内存内容写入到临时文件中,父子进程共享相同的物理页面,父进程处理写请求时,OS为父进程要修改的页面创建副本,而不是写共享的页面。子进程第一次空间内的数据是fork时刻整个数据库的一个快照。当子进程完成临时空间的写入后,用临时文件替换掉原来的快照文件,子进程退出,完后一次备份操作。

RDB文件的载入一般是自动的,redis服务器再次启动时,如果检测到RDB文件的存在,redis会自动载入这个文件。

RDB持久化的缺点

1)内存数据的全量同步,数据量大会由于I/O而严重影响性能的。每次快照持久化都是将快照数据完整的写入到磁盘一次,并不是增量的只同步脏数据,如果数据量大的话,并且写操作比较多的时候必然会引起大量的磁盘IO操作,可能会严重影响性能。

2)可能会因为Redis挂掉而丢失从当前至最近一次快照期间的数据,由于快照方式是在一定间隔时间做一次的快照后的所有修改,如果应用要求不能丢失任何修改的话,可以采用AOF。

2)AOF增量持久化方式

AOF(append-only-file)持久化,通过保存Redis服务器所执行的写状态来记录数据库的。

记录下除了查询以外的所有变更数据库状态的指令。RDB持久化备份数据库状态;AOF持久化是备份数据库接收到的指令,所有被写入AOF的命令都是以redis协议格式来保存的。

在AOF中,以append的形式追加保存到aof文件中,以增量的形式。数据库会记录下所有变更数据库状态的指令,除了指定数据库的查询命令,其它的命令都是来自client的。

aof持久化默认是关闭的,可以通过修改refis.conf的配置appendonly no修改为appendonly yes即可,生成的文件名称是appendonly.aof。

appendfsync everysec该配置主要用来配置aof文件的写入方式的,可以接收三个不同的参数,分别是always、everysec、no。always表示一旦缓存区的内容发生变化,就总是及时的将缓存区的内容写入到aof中;everysec是将缓存区的内容每隔一秒去写入到aof中;no是将写入aof的操作交由操作系统来决定。一般而言,为了提高效率,操作系统会将缓存区被填满才会开始同步数据到磁盘中。一般推荐everysec默认的方式,速度比较快,安全性比较高。修改配置需要重启redis服务器。

AOF日志文件是一个纯追加的文件,就算遇到突然断电也可以尽最大权力去保证数据的无损。

日志重写(rewrite)解决AOF文件大小不断增大的问题,Redis支持在不中断服务的情况下,在后台重建AOF文件,原理如下:

1)首先调用fork(),创建一个子进程。

2)子进程把新的AOF写到一个临时文件里面,新的AOF的重写是直接把当前内存的数据生成对应的命令,并不需要读取老的AOF文件进行分析或者合并,不依赖原来的AOF文件。

3)主进程持续将新的变动同时写到内存和原来的AOF里面。这样即使写入失败,也能保证数据的安全。

4)主进程获取子进程重写AOF的完成信号,往新AOF同步增量变动。

5)使用新的AOF文件替换掉旧的AOF文件。

Redis数据的恢复

RDB和AOF文件共存情况下的恢复流程

RDB和AOF的优缺点?

1)RDB优点:RDB本质上是一个内存快照,保存了创建RDB文件那个时间点的Redis全量数据,全量数据快照,文件小,创建恢复快。缺点:无法保存最近一次快照之后的数据。

2)AOF优点:AOF本质上是一份执行日志,保存所有被Redis更改的指令,可读性高,适合保存增量数据,数据不易丢失。缺点:文件体积大,恢复时间长。

3)RDB-AOF混合持久化方式

Redis4.0之后,推出了结合AOF和RDB的混合模式,并且作为默认的方式来使用。即使用RDB作为全量备份,AOF作为增量备份,来提升备份的效率。

AOF重写机制,也是先写一份全量数据到AOF文件中,再追加增量,只不过全量数据是以redis命令格式写入的,那么是否可以先以RDB格式写入全量数据,再追加增量数据呢,这样既可以提高读写和恢复速度,也可以减少文件的大小,还同时可以保证数据的完整性,能够结合RDB和AOF的优点,AOF和RDB的混合模式正是在这种需求下诞生的。在此种方式下,子进程在做AOF重写时,会通过管道从父进程读取增量数据并缓存下来,那么在以RDB格式保存全量数据的时候,也会从管道读取数据,同时不会造成管道的阻塞。也就是说,AOF文件前半段是RDB格式的全量数据,而后半段是Redis命令格式的增量数据。

总结,BGSAVE做镜像全量持久化,AOF做增量持久化,因为BGSAVE会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据的问题,需要AOF配合使用,在Redis重启的时候会使用BGSAVE持久化文件,重新构建内容,再使用AOF重放近期的操作指令,来实现完整恢复之前的状态。

 

九、Redis高可用

1)主从同步

pipeline

pipeline和Linux管道类似。

Redis基于请求/响应模型,单个请求处理需要一一应答。同时需要执行大量命令,需要等待上一行命令应答后再执行后面的命令。这中间不仅有来回交互的时间,而且还频繁调用系统IO,发送网络请求。

pipeline批量执行指令,节省多次IO往返时间。为了提升效率,pipeline一次发送多行命令,不需要等待上一行命令执行的结果。客户端将执行的命令写入缓存中,一次性发给Redis。pipeline可以将多次IO往返的时间缩减为1次,前提是pipeline指令间没有依赖的相关性。有顺序依赖的指令建议分批发送。

Redis的同步机制

redis正常部署中都是由一个master进行写操作,其他若干个slave进行读操作。master和slave分为一个个独立的redis server实例。为了支持数据的弱一致性(最终一致性),不需要实时保证master和slave之间的数据是同步的,但是过一段时间数据是趋于一致性的,即所谓的最终一致性。redis可以使用主从同步和从从同步,第一次同步时主节点bgsave,同时将后续修改操作记录到内存buff里面,待完成后将RDB文件全量同步到从节点里面。从节点接受完成后将RDB镜像加载进内存中,加载完成后再通知主节点将期间修改的操作记录即增量数据同步到从节点进行存放。到某个时间点前的数据同步完后该时间点之后的增量数据也去进行存放,完成整个同步的过程。

按照同步内容的多少分为全同步和增量同步

  • 全同步流程

1)Slave发送sync命令到Master。

2)Master启动一个后台进程,将Redis中的数据快照保存到文件中,即BGSave。

3)Master将保存数据快照期间接收到的写命令缓存起来。

4)Master完成写文件操作后,将该文件发送给Salve。

5)Slave接收文件后将文件保存到磁盘中,加载到内存中恢复数据快照。(使用新的AOF文件替换掉旧的AOF文件)

6)Master将这期间收集的增量写命令发送给Slave端。

全量同步操作完成后后续所有写操作都是在Master上进行,读操作都是在Slave上进行。Master也可以读,一般为了提升性能将读操作放到Slave上。因此用户的写操作需要及时扩散到Slave,以便保持数据最大程度上同步。Redis的Master、Slave进程在正常运行期间更新操作,包括写、删除、更改操作。

  • 增量同步过程

Redis的主从进程在正常运行期间更新操作的增量同步方式如下:

1)Master接收到用户的操作指令,判断是否需要传播到Slave。增删改需要扩散到Slave,查不需要。

2)将操作记录追加到AOF文件。将操作转换为Redis内部的协议格式,并以字符串的形式存储。将字符串存储的操作追加到AOF之后

3)将操作传播到其他Slave:a)对齐主从库;b)将命令参数按照redis协议格式写入响应Slave的缓存中

4)将缓存中的数据发送给Slave

Redis Sentinel

主从模式的弊端是不具备高可用性,当Master挂掉后Redis将不能对外提供写入操作。Redis Sentinel应运而生。Redis Sentinel即Redis哨兵,是Redis官方提供的集群管理工具,其本身也是一个独立运行的进程,能监控多个Master、Slave集群,发现Master宕机后能自动切换。主要功能如下:

1)监控:检查主从服务器是否正常运行

2)提醒:通过API向管理员或者其他应用程序发送故障通知。

3)自动故障迁移:主从切换。

Redis Sentinel是一个分布式系统,可以在一个架构中运行多个Sentinel进程。这些进程使用流言协议Gossip接收关于主服务器是否下线的信息,使用投票协议决定是否执行自动故障迁移,以及选择哪个从服务器作为新的主服务器。跟Zookeeper比较类似。

流言协议Gossip:在杂乱无章中保持一致。每个节点都随机地与对方通信,最终所有节点的状态达成一致。种子节点定期随机向其他节点发送节点列表以及需要传播的信息。不保证信息一定传递给所有节点,但是最终会趋于一致。

2)Redis集群

在网站承受高并发访问压力的同时,如何从海量数据里找到所需,并快速响应?

分片:按照某种规则去划分数据,分散存储在多个节点上。

通过实现数据分片,降低单节点服务器压力。

Redis集群采用无中心结构,每个节点保存数据和整个集群的状态,每个节点都和其他所有节点连接。节点之间使用Gossip协议传播信息以及发现新的节点。

Redis集群的目的是将不同的key分散放置到不同Redis节点,实现原理如下:

获取key的hash值,根据节点数求模。动态增加或减少节点时,会造成大量key无法被命中。为了解决这个问题,redis引入了一致性hash算法。

一致性hash算法:对2^32取模,将hash值空间组织成虚拟的圆环。各个服务器使用hash进行hash变换,具体可以选择服务器ip或者主机名作为关键字进行hash,这样每台服务器可以确定在hash环上的位置。对数据使用同样的hash算法定位到相应的服务器。沿环顺时针行走,第一台遇到的服务器就是数据的目标存储服务器。

在一致性hash算法中,如果一台服务器不可用,受影响的仅仅是此服务器到其环空间中前一台服务器(逆时针方向)之间的数据,其他数据不会受影响。并且该路径上新增的数据会存储到离它顺时针方向最近的节点上。做到最小化的有损服务。

综上所述,一致性hash算法对节点的增减都只需要重新定位环中一小部分数据,具有较好的容错性和扩展性。

hash环的数据倾斜问题:一致性hash算法在服务器节点很少时,容易因为节点分布不均匀造成数据倾斜,被缓存的对象大部分集中在某一台服务器上。为解决数据倾斜问题,一致性hash算法引入虚拟节点。对每个服务器节点计算多个hash,计算结果位置放置一个子服务器节点,称为虚拟节点。具体做法可以在服务器ip或主机名后增加编号来实现。在实际应用中,通常将虚拟节点设置为32甚至更大,因此即使很少服务节点,也能做到相对均匀的数据分布。结合redis集群技术,还可以在期间引入主从同步、redis哨兵机制进一步确保集群的高可用性。

参考:redis高可用方案

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值