基础
1.说说什么是Redis、及特点?
Redis是一个基于内存存储数据运行并支持持久化、使用key/value形式存储的高性能的nosql数据库,适合用于存储频繁访问,数据量较小的场景下。
特点:
1.支持数据持久化:可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
2.支持多种数据结构:不仅支持简单的key-value类型数据,同时还提供string、list、set、zset、hash等数据结构的存储
3.支持数据备份:master-salve模式的数据备份
2.Redis使用场景
1.缓存:这是Redis应用最广泛地方,基本所有的Web应用都会使用Redis作为缓存,来降低数据源压力,提高响应速度。
2.计数器:Redis天然支持计数功能,而且计数性能非常好,可以用来记录浏览量、点赞量等等。
3.排行榜:Redis提供了列表和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行榜系统。
4.社交网络:赞/踩、粉丝、共同好友/喜好、推送、下拉刷新。
5.消息队列:Redis提供了发布订阅功能和阻塞队列的功能,可以满足一般消息队列功能。
6.分布式锁:分布式环境下,利用Redis实现分布式锁,也是Redis常见的应用。
Redis的应用一般会结合项目去问,以一个电商项目的用户服务为例:
Token存储:用户登录成功之后,使用Redis存储Token、分布式环境下登录、注册等操作加分布式锁、登录失败次数计数:使用Redis计数,登录失败超过一定次数,锁定账号、地址缓存:对省市区数据的缓存、短信验证码、订 单有效期等。
3.Redis有哪些数据结构,简单介绍下
string:字符串最基础的数据结构。字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512M。
字符串主要有以下几个典型使用场景:缓存功能、计数、共享Session、限速。
hash:哈希类型是指键值本身又是一个键值对结构。哈希主要有以下典型应用场景:缓存用户信息、缓存对象
list:列表(list)类型是用来存储多个有序的字符串。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色。列表主要有以下几种使用场景:消息队列、文章列表
set:集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的。集合主要有如下使用场景:标签(tag)、共同关注
sorted set:有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个权重(score)作为排序的依据。有序集合主要应用场景:用户点赞统计、用户排序
5.0之后类型新增Bitmaps(位图)、 HyperLogLog、GEO(地理信息定位)。想了解更多请访问这里。
4.Redis为什么这么快
1.完全基于内存操作 2.使用单线程,避免了线程切换和竞态产生的消耗 3.基非阻塞的IO多路复用机制 4.C语言实现,优化过的数据结构,基于几种基础的数据结构,redis做了大量的优化,性能极高
5.能说一下I/O多路复用吗?
引用知乎上一个高赞的回答来解释什么是I/O多路复用。假设你是一个老师,让30个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:
第一种选择:按顺序逐个检查,先检查A,然后是B,之后是C、D。。。这中间如果有一个学生卡住,全班都会被耽误。这种模式就好比,你用循环挨个处理socket(套接字),根本不具有并发能力。
第二种选择:你创建30个分身,每个分身检查一个学生的答案是否正确。这种类似于为每一个用户创建一个进程或者- 线程处理连接。
第三种选择,你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。
第一种就是阻塞IO模型,第三种就是I/O复用模型。
6.为什么使用单线程、后面Redis6.0又可以使用多线程了呢?
官方解释,因为Redis是基于内存的操作,CPU成为Redis的瓶颈的情况很少见,Redis的瓶颈最有可能是内存的大小或者网络限制。
如果想要最大程度利用CPU,可以在一台机器上启动多个Redis实例。
PS:网上有这样的回答,吐槽官方的解释有些敷衍,其实就是历史原因,开发者嫌多线程麻烦,后来这个CPU的利用问题就被抛给了使用者。
同时还提到了,Redis 4.0之后开始变成多线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大Key的删除等等。
Redis6.0的多线程是用多线程来处理数据的读写和协议解析,但是Redis执行命令还是单线程的。
这样做的⽬的是因为Redis的性能瓶颈在于⽹络IO⽽⾮CPU,使⽤多线程能提升IO读写的效率,从⽽整体提⾼Redis的性能。
持久化
1.Redis持久化方式有哪些?持久化过程及如何配置?
Redis持久化方案分为RDB和AOF两种。
RDB:RDB持久化是在指定的时间间隔内将数据以生成快照的方式写入磁盘。
RDB文件是一个压缩的二进制文件,通过它可以还原某个时刻数据库的状态。由于RDB⽂件是保存在硬盘上的,所以即使Redis崩溃或者退出,只要RDB文件存在,就可以用它来恢复还原数据库的状态。
手动触发分别对应save和bgsave命令(触发机制):
save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。
bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。
以下场景会自动触发RDB持久化(触发机制):
在redis.conf文件使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点
执行debug reload命令重新加载Redis时,也会自动触发save操作
默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。
配置:
默认rdb文件存放路径是当前目录,文件名是:dump.rdb。可以在配置文件中修改路径和文件名,分别是dir和dbfilename
stop-writes-on-bgsave-error yes 当写入数据到硬盘出错,则停止保存到硬盘 推荐yes
rdchecksum yes # 在存储快照后,可以让redis使用CRC64算法来进行数据校验 # 但是会增加大约10%的性能消耗 # 我觉得10%不算什么
持久化过程:
Redis会单独创建(fork)一个子进程来进行持久化,会将数据写入到一个临时RBD文件中,待持久化过程结束,用这个临时文件替换上次持久化好的文件。而Fork的作用是复制一个与当前进程一样的进程,新进程的所有数据数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。在执行fork的时候会使用写时复制策略,即fork函数发生的一刻父子进程共享同一内存数据,当父进程要更改其中某个数据时,操作系统会将该数据复制一份以保证子进程数据不受影响,所以新的RDB文件存储的是执行fork那一刻的内存数据。
AOF持久化:以日志的方式记录每次写命令,重启时读取该AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。
AOF持久化流程:1.客户端的请求写命令会被append追加到aof缓冲区内。2.aof缓冲区根据aof持久化策略【always,everysec,no】将操作sync同步到磁盘的aof文件中。3.随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩 的目的。4.redis服务重启时,会加载aof文件中的写操作达到数据恢复的目的。
配置:
在redis.conf文件中appendonly 开启,appendonlyname 日志文件名称 ,生成路径和RDB路径相同。
当aof文件发送异常时,可使用usr/local/bin/redis-check-aof--fix aof文件名称 进行恢复 ,备份被写坏的文件,容器redis重新加载。
重写、重写原理、触发机制,何时重写、重写流程、如何配置 Redis 多久才将数据 fsync 到磁盘一次、重写步骤等原理请点击查看
2.各自的优缺点,及如何选择、数据怎么恢复?
RDB优点:可以定期备份,适合大规模数据恢复,容灾性强,节省磁盘空间。性能优化强,主线程不需要进行任何io操作由子进程执行备份。恢复速度快(比AOF快)。
缺点:因为是定期备份做不到实时持久化,在持久化期间redis意外宕机会造成最后一次的数据丢失。老版本redis无法兼容新版本RDB。fork过程中比较耗时,不能达到毫秒级响应。
AOF优点:实时性好,通过配置appendfsync always每执行一次写命令操作就会记录到aof文件中,就算发生故障停机最多也就丢失一秒钟的数据。可读的日志文件,可以处理误操作。通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
缺点:AOF 文件通常要大于 RDB 文件且恢复速度慢。存在个别bug时,会恢复失败。数据集大 的时候,比 RDB 启动效率低。
根据以上优缺点自己总结:aof安全,rdb性能比aof好;要使用二种一起使用,单独用rdb故障情况下会丢失很多数据,单独用aof数据恢复没有rdb快,建议第一时间用RDB恢复库AOF来做数据补全。如果只需要数据在服务器运行的时候存在,也可以不使用任何持久化方式。
恢复的过程也很简单,把RDB或者AOF文件拷贝到Redis的数据目录下,如果使用AOF恢复,配置文件开启AOF,然后启动redis-server即可。AOF加载优先于RDB
加载过程:AOF开启且存在AOF文件优先加载AOF文件,AOF关闭或者AOF文件不存在时,加载RDB文件。加载成功后,Redis启动成功。文件存在错误时候Redis启动失败并打印错误信息。
3.Redis 4.0 的混合持久化了解吗?
重启Redis,使用RDB来恢复内存状态,会丢失大量数据。我们通常使用AOF日志重放,但是性能比RDB慢,这样在Redis实例很大的情况下,启动需要花费很长的时间。混合持久化就是为了解决这个问题,将rdb文件的内容和增量的AOF日志文件存在一起。这里的AOF日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量AOF日志,通常这部分 AOF 日志很小:于是在Redis重启的时候,可以先加载rdb的内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,重启效率因此大幅得到提升。
高可用
1.说一下你理解的主从复制?
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave)。且数据的复制是单向的,只能由主节点到从节点。Redis主从复制支持主从同步和从从同步两种,后者是Redis 后续版本新增的功能,以减轻主节点的同步负担。
其主要的作用:
数据热备份:将主库上的数据复制到了从库上,相当于是一种热备份机制,也就是在主库正常运行下进行备份,不会影响到服务;
故障恢复(高可用): 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 *(实际上是一种服务的冗余)*。
负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的 基础,因此说主从复制是 Redis 高可用的基础。
2.说一下主从有几种常见的拓扑结构?
一主一从结构:最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持。
一主多从结构:又称为星形拓扑结构,可以利用多个从节点实现读写分离。对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力。
树状主从结构:又称为树状拓扑结构,从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层,可以有效降低主节点负载和需要传送给从节点的数据量。
3.那你知道主从的原理么?
设置主节点信息:保存主节点(master)信息 这一步只是保存主节点信息,保存主节点的ip和port。
建立套接字连接:主从建立连接从节点(slave)发现新的主节点后,会尝试和主节点建立网络连接。
发送ping命令:连接建立成功后从节点发送ping请求进行首次通信,主要是检测主从之间网络套接字是否可用、主节点当前是否可接受处理命令。
身份验证:如果主节点要求密码验证,从节点必须正确的密码才能通过验证。
同步数据集:主从复制连接正常通信后,主节点会把持有的数据全部发送给从节点。
命令传播:接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。
4.说说主从数据同步的方式(就是问你全量复制和部分同步)?
全量复制一般用于初次复制场景,Redis早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。
发送psync命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行ID,所以发送psync-1。 主节点根据psync-1解析出当前为全量复制,回复+FULLRESYNC响应。 从节点接收主节点的响应数据保存运行ID和偏移量offset 主节点执行bgsave保存RDB文件到本地 主节点发送RDB文件给从节点,从节点把接收的RDB文件保存在本地并直接作为从节点的数据文件 对于从节点开始接收RDB快照到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完RDB文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性。 从节点接收完主节点传送来的全部数据后会清空自身旧数据 从节点清空数据后开始加载RDB文件 从节点成功加载完RDB后,如果当前节点开启了AOF持久化功能, 它会立刻做bgrewriteaof操作,为了保证全量复制后AOF持久化文件立刻可用。
部分复制部分复制主要是Redis针对全量复制的过高开销做出的一种优化措施,使用psync{runId}{offset}命令实现。当从节点(slave)正在复制主节点 (master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向 主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。
当主从节点之间网络出现中断时,如果超过repl-timeout时间,主节点会认为从节点故障并中断复制连接 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在的复制积压缓冲区,依然可以保存最近一段时间的写命令数据,默认最大缓存1MB。 当主从节点网络恢复后,从节点会再次连上主节点 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们当作psync参数发送给主节点,要求进行部分复制操作。 主节点接到psync命令后首先核对参数runId是否与自身一致,如果一 致,说明之前复制的是当前主节点;之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+CONTINUE响应,表示可以进行部分复制。 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。
5.主从复制存在哪些问题呢?
一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预。主节点的写能力受到单机的限制。主节点的存储能力受到单机的限制。第一个问题是Redis的高可用问题,第二、三个问题属于Redis的分布式问题。
6.Sentinel(哨兵)了解吗、组成、领导者Sentinel节点选举、新的主节点是怎样被挑选出来的?
7.你理解的集群、及集群中数据如何分区、原理、集群的伸缩?
缓存设计(老八股文了)
1.说一下你理解的缓存击穿、缓存穿透、缓存血崩?
缓存击穿:一个并发访问量比较大的key在某个时间过期,导致所有的请求直接打在DB上。
解决方案:
加锁更新,比如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写入缓存,再返回给用户,这样后面的请求就可以从缓存中拿到数据了。
设置热点数据永不过期或者异步延长过期时间。
将过期时间组合写在value中,通过异步的方式不断的刷新过期时间,防止此类现象。
缓存穿透:缓存穿透是指使用不存在的key进行大量的高并发查询,导致查询不到数据,每次请求都要都要穿透到数据库查询,就好像缓存不存在一样,失去了缓存保护后端存储的意义,使数据库压力非常大,导致数据库崩掉。
解决方案:
产生的原因:自身业务代码问题,恶意攻击,爬虫造成空命中。
接口层增加校验,如用户鉴权校验, id 做基础校验, id<=0 的直接拦截;
从缓存和数据库都取不到数据的话,设置一个key并将数据库空值放入缓存中做value,设置30s有效期避免使用同一个id对数据库攻击压力大(空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致。这时候可以利用消息队列或者其它异步方式清理缓存中的空对象。)。
使用布隆过滤器,布隆过滤器除了缓存空对象,我们还可以在存储和缓存之前,加一个布隆过滤器,做一层过滤。布隆过滤器里会保存数据是否存在,如果判断数据不存在,就不会访问存储。
缓存雪崩:某一时刻发生大规模的缓存失效的情况,例如缓存服务宕机、大量key在同一时间过期,这样的后果就是大量的请求进来直接打到DB上,可能导致整个系统的崩溃,称为雪崩。
解决方案:
集群部署:通过集群来提升缓存的可用性,可以利用Redis本身的Redis Cluster或者第三方集群方案如Codis等。
多级缓存:设置多级缓存,第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。
均匀过期:为了避免大量的缓存在同一时间过期,可以把不同的 key 过期时间随机生成,避免过期时间太过集中。热点数据永不过期。
服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。
服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback(退路)错误处理信息。
2.布隆过滤器
布隆过滤器,它是一个连续的数据结构,每个存储位存储都是一个bit,即0或者1, 来标识数据是否存在。存储数据的时时候,使用K个不同的哈希函数将这个变量映射为bit列表的的K个点,把它们置为1。
我们判断缓存key是否存在,同样,K个哈希函数,映射到bit列表上的K个点,判断是不是1:如果全不是1,那么key不存在;如果都是1,也只是表示key可能存在。
布隆过滤器也有一些缺点:它在判断元素是否在集合中时是有一定错误几率,因为哈希算法有一定的碰撞的概率。不支持删除元素。
3.如何保证缓存和数据库数据的一致性
需要知道的是根据CAP理论,在保证可用性和分区容错性的前提下,无法保证一致性,所以缓存和数据库的绝对一致是不可能实现的,但是最后都是采用最终一致性方案。
方案如下:先更数据库,在删除缓存。使用mq异步订阅mysql binlog实现增量同步。使用alibaba的canal框架
缓存不一致处理
消息队列保证key被删除可以引入消息队列,把要删除的key或者删除失败的key丢尽消息队列,利用消息队列的重试机制,重试删除对应的key。这种方案看起来不错,缺点是对业务代码有一定的侵入性。
数据库订阅+消息队列保证key被删除可以用一个服务(比如阿里的 canal)去监听数据库的binlog,获取需要操作的数据。然后用一个公共的服务获取订阅程序传来的信息,进行缓存删除操作。这种方式降低了对业务的侵入,但其实整个系统的复杂度是提升的,适合基建完善的大厂。
延时双删防止脏数据还有一种情况,是在缓存不存在的时候,写入了脏数据,这种情况在先删缓存,再更数据库的缓存更新策略下发生的比较多,解决方案是延时双删。简单说,就是在第一次删除缓存之后,过了一段时间之后,再次删除缓存。这种方式的延时时间设置需要仔细考量和测试。设置缓存过期时间兜底这是一个朴素但是有用的办法,给缓存设置一个合理的过期时间,即使发生了缓存数据不一致的问题,它也不会永远不一致下去,缓存过期的时候,自然又会恢复一致。
4.怎么处理热点key
对热key的处理,最关键的是对热点key的监控,可以从这些端来监控热点key:
客户端:其实是距离key“最近”的地方,因为Redis命令就是从客户端发出的,例如在客户端设置全局字典(key和调用次数),每次调用Redis命令时,使用这个字典进行记录。
代理端:像Twemproxy、Codis这些基于代理的Redis分布式架构,所有客户端的请求都是通过代理端完成的,可以在代理端进行收集统计。
Redis服务端:使用monitor命令统计热点key是很多开发和运维人员首先想到,monitor命令可以监控到Redis执行的所有命令。
只要监控到了热key,对热key的处理就简单了:把热key打散到不同的服务器,降低压力。加⼊⼆级缓存,提前加载热key数据到内存中,如果redis宕机,⾛内存查询
5.缓存预热怎么做?
直接写个缓存刷新页面或者接口,上线时手动操作。
数据量不大,可以在项目启动的时候自动进行加载。
定时任务刷新缓存。
6.热点key重建,会出现什么问题,怎么解决
开发的时候一般使用“缓存+过期时间”的策略,既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。
但是有两个问题如果同时出现,可能就会出现比较大的问题:
当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。
重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
怎么处理呢?
要解决这个问题也不是很复杂,解决问题的要点在于:
- 减少重建缓存的次数。
- 数据尽可能一致。
- 较少的潜在危险。
所以一般采用如下方式:
互斥锁(mutex key) 这种方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
永远不过期 “永远不过期”包含两层意思:
从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。
从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
7.什么是无底洞问题,应该怎么优化解决
什么是无底洞问题?
2010年,Facebook的Memcache节点已经达到了3000个,承载着TB级别的缓存数据。但开发和运维人员发现了一个问题,为了满足业务要求添加了大量新Memcache节点,但是发现性能不但没有好转反而下降了,当时将这种现象称为缓存的“无底洞”现象。
那么为什么会产生这种现象呢?
通常来说添加节点使得Memcache集群 性能应该更强了,但事实并非如此。键值数据库由于通常采用哈希函数将 key映射到各个节点上,造成key的分布与业务无关,但是由于数据量和访问量的持续增长,造成需要添加大量节点做水平扩容,导致键值分布到更多的 节点上,所以无论是Memcache还是Redis的分布式,批量操作通常需要从不同节点上获取,相比于单机批量操作只涉及一次网络操作,分布式批量操作会涉及多次网络时间。
无底洞问题如何优化呢?
先分析一下无底洞问题:
- 客户端一次批量操作会涉及多次网络操作,也就意味着批量操作会随着节点的增多,耗时会不断增大。
- 网络连接数变多,对节点的性能也有一定影响。
常见的优化思路如下:
- 命令本身的优化,例如优化操作语句等。
- 减少网络通信次数。
- 降低接入成本,例如客户端使用长连/连接池、NIO等。
Redis运维
1.Redis保内存不足怎么处理?
- 修改配置文件 redis.conf 的 maxmemory 参数,增加 Redis 可用内存
- 也可以通过命令set maxmemory动态设置内存上限
- 修改内存淘汰策略,及时释放内存空间
- 使用 Redis 集群模式,进行横向扩容。
2.Redis的过期数据回收策略有哪些?
定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期:每隔一段时间,会去数据库里扫描一定数量的key,并清除其中已过期的key。由于不可能对所有key去做轮询来删除,所以Redis会每次随机取一些key去做检查和删除。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
3.Redis的内存淘汰策略?
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。
全局的键空间选择性移除
- noeviction:当内存使用达到阈值的时候,执行命令直接报错 默认
- allkeys-lru:在所有的key中,优先移除最近未使用的key。(推荐)
- allkeys-random:在所有的key中,随机移除某个key。
设置过期时间的键空间选择性移除
- volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key。
- volatile-random:在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。
4.Redis阻塞怎么解决?
API或数据结构使用不合理
通常Redis执行命令速度非常快,但是不合理地使用命令,可能会导致执行速度很慢,导致阻塞,对于高并发的场景,应该尽量避免在大对象上执行算法复杂 度超过O(n)的命令。
对慢查询的处理分为两步:
- 发现慢查询:slowlog get{n}命令可以获取最近 的n条慢查询命令;
- 发现慢查询后,可以从两个方向去优化慢查询:1)修改为低算法复杂度的命令,如hgetall改为hmget等,禁用keys、sort等命令 2)调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。
CPU饱和的问题
单线程的Redis处理命令时只能使用一个CPU。而CPU饱和是指Redis单核CPU使用率跑到接近100%。
针对这种情况,处理步骤一般如下:
- 判断当前Redis并发量是否已经达到极限,可以使用统计命令redis-cli-h{ip}-p{port}--stat获取当前 Redis使用情况
- 如果Redis的请求几万+,那么大概就是Redis的OPS已经到了极限,应该做集群化水品扩展来分摊OPS压力
- 如果只有几百几千,那么就得排查命令和内存的使用
持久化相关的阻塞
对于开启了持久化功能的Redis节点,需要排查是否是持久化导致的阻塞。
- fork阻塞 fork操作发生在RDB和AOF重写时,Redis主线程调用fork操作产生共享 内存的子进程,由子进程完成持久化文件重写工作。如果fork操作本身耗时过长,必然会导致主线程的阻塞。
- AOF刷盘阻塞 当我们开启AOF持久化功能时,文件刷盘的方式一般采用每秒一次,后台线程每秒对AOF文件做fsync操作。当硬盘压力过大时,fsync操作需要等 待,直到写入完成。如果主线程发现距离上一次的fsync成功超过2秒,为了数据安全性它会阻塞直到后台线程执行fsync操作完成。
- HugePage写操作阻塞 对于开启Transparent HugePages的 操作系统,每次写命令引起的复制内存页单位由4K变为2MB,放大了512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。
5.大key问题了解么?
Redis使用过程中,有时候会出现大key的情况,比如:
- 单个简单的key存储的value很大,size超过10KB
- hash, set,zset,list 中存储过多的元素(以万为单位)
大key会造成什么问题呢?
- 客户端耗时增加,甚至超时
- 对大key进行IO操作时,会严重占用带宽和CPU
- 造成Redis集群中数据倾斜
- 主动删除、被动删等,可能会导致阻塞
如何找到大key?
- bigkeys命令:使用bigkeys命令以遍历的方式分析Redis实例中的所有Key,并返回整体统计信息与每个数据类型中Top1的大Key
- redis-rdb-tools:redis-rdb-tools是由Python写的用来分析Redis的rdb快照文件用的工具,它可以把rdb快照文件生成json文件或者生成报表用来分析Redis的使用详情。
如何处理大key?
删除大key:
- 当Redis版本大于4.0时,可使用UNLINK命令安全地删除大Key,该命令能够以非阻塞的方式,逐步地清理传入的Key。
- 当Redis版本小于4.0时,避免使用阻塞式命令KEYS,而是建议通过SCAN命令执行增量迭代扫描key,然后判断进行删除。
压缩和拆分key:
- 当vaule是string时,比较难拆分,则使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。
- 当value是string,压缩之后仍然是大key,则需要进行拆分,一个大key分为不同的部分,记录每个部分的key,使用multiget等操作实现事务读取。
- 当value是list/set等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。
6.Redis常见性能问题和解决方案
- Master 最好不要做任何持久化工作,包括内存快照和 AOF 日志文件,特别是不要启用内存快照做持久化。
- 如果数据比较关键,某个 Slave 开启 AOF 备份数据,策略为每秒同步一次。
- 为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。
- 尽量避免在压力较大的主库上增加从库。
- Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。
- 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现 Slave 对 Master 的替换,也即,如果 Master 挂了,可以立马启用 Slave1 做 Master,其他不变。
Redis应用
1.Redis如何实现异步队列?
使用list作为队列,lpush生产消息,rpop消费消息:
这种方式,消费者死循环rpop从队列中消费消息。但是这样,即使队列里没有消息,也会进行rpop,会导致Redis CPU的消耗。图片可以通过让消费者休眠的方式的方式来处理,但是这样又会又消息的延迟问题。
使用list作为队列,lpush生产消息,brpop消费消息:
brpop是rpop的阻塞版本,list为空的时候,它会一直阻塞,直到list中有值或者超时。
这种方式只能实现一对一的消息队列。
使用Redis的pub/sub来进行消息的发布/订阅:
发布/订阅模式可以1:N的消息发布/订阅。发布者将消息发布到指定的频道频道(channel),订阅相应频道的客户端都能收到消息。
但是这种方式不是可靠的,它不保证订阅者一定能收到消息,也不进行消息的存储。
所以,一般的异步队列的实现还是交给专业的消息队列。
2.Redis如何实现延时队列?
使用zset,利用排序实现,用设置好的时间戳作为score进行排序,使用 zadd score1 value1 ....命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可。
3.说说你理解的Redis事务?
Redis通过MULTI、EXEC、WATCH等命令来实现事务功能,事务执行的过程,该事务首先以一个MULTI命令为开启组队,接着将多个命令放入事务当中,最后由EXEC命令将这个事务提交(commit)给服务器执行:discard解散组队;执行前可以使用watch命令监控一个或多个key,如果在事务执行前key被改动那么事务会被打断;unwatch取消监视。Redis事务的原理,是所有的指令在 exec 之前不执行,而是缓存在 服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。因为Redis执行命令是单线程的,所以这组命令顺序执行,而且不会被其它线程打断。一个事务从开始到结束通常会经历以下三个阶段:事务开始、命令入队、事务执行。
几大特性:
单独的隔离操作:知道事务中所有命令都会按照顺序执行不会被其它命令插队干扰;
没有隔离级别的概念:队列中的命令没有提交前都不会实际被执行;
不保证原子性:说明事务中如果有一条运行错误其它正确命令也会执行(命令编译错误,其它的命令都不执行)没有回滚;ps:事务与事务之间是队列保证原子性,同一个队列(事务)中命令和命令之间是不能保证原子性的。
具有隔离性么?为什么不支持回滚
因为Redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis的总是以串行的方式运行的,并且事务也总是具有隔离性的。
redis不支持行锁不支持事务回滚只支持事务的取消,这种复杂的功能和Redis追求简单高效的设计主旨不相符,并且他认为,Redis事务的执行时错误通常都是编程错误产生的,只有程序发出错误才停止,其他错误都是正常运行 ,而这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为Redis开发事务回滚功能。
4.Redis和Lua脚本的使用了解吗?
Redis的事务功能比较简单,平时的开发中,可以利用Lua脚本来增强Redis的命令。
Lua脚本能给开发人员带来这些好处:
- Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
- Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这 些命令常驻在Redis内存中,实现复用的效果。
- Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
比如这一段很(烂)经(大)典(街)的秒杀系统利用lua扣减Redis库存的脚本:
-- 库存未预热 if (redis.call('exists', KEYS[2]) == 1) then return -9; end; -- 秒杀商品库存存在 if (redis.call('exists', KEYS[1]) == 1) then local stock = tonumber(redis.call('get', KEYS[1])); local num = tonumber(ARGV[1]); -- 剩余库存少于请求数量 if (stock < num) then return -3 end; -- 扣减库存 if (stock >= num) then redis.call('incrby', KEYS[1], 0 - num); -- 扣减成功 return 1 end; return -2; end; -- 秒杀商品库存不存在 return -1;
5.Redis管道
Redis 提供三种将客户端多条命令打包发送给服务端执行的方式:
Pipelining(管道) 、 Transactions(事务) 和 Lua Scripts(Lua 脚本) 。
Pipelining(管道)
Redis 管道是三者之中最简单的,当客户端需要执行多条 redis 命令时,可以通过管道一次性将要执行的多条命令发送给服务端,其作用是为了降低 RTT(Round Trip Time) 对性能的影响,比如我们使用 nc 命令将两条指令发送给 redis 服务端。
Redis 服务端接收到管道发送过来的多条命令后,会一直执命令,并将命令的执行结果进行缓存,直到最后一条命令执行完成,再所有命令的执行结果一次性返回给客户端 。
Pipelining的优势
在性能方面, Pipelining 有下面两个优势:
- 节省了RTT:将多条命令打包一次性发送给服务端,减少了客户端与服务端之间的网络调用次数
- 减少了上下文切换:当客户端/服务端需要从网络中读写数据时,都会产生一次系统调用,系统调用是非常耗时的操作,其中设计到程序由用户态切换到内核态,再从内核态切换回用户态的过程。当我们执行 10 条 redis 命令的时候,就会发生 10 次用户态到内核态的上下文切换,但如果我们使用 Pipeining 将多条命令打包成一条一次性发送给服务端,就只会产生一次上下文切换。
6.Redis实现分布式锁
Redis是分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。
- V1:setnx命令
占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。
> setnx lock:fighter true OK ... do something critical ... > del lock:fighter (integer) 1
但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
- V2:锁超时释放
所以在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。
> setnx lock:fighter true OK > expire lock:fighter 5 ... do something critical ... > del lock:fighter (integer) 1
但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。
这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。如果这两条指令可以一起执行就不会出现问题。
- V3:set指令
这个问题在Redis 2.8 版本中得到了解决,这个版本加入了 set 指令的扩展参数,使得 setnx 和expire 指令可以一起执行。
set lock:fighter3 true ex 5 nx OK ... do something critical ... > del lock:codehole
上面这个指令就是 setnx 和 expire 组合在一起的原子指令,这个就算是比较完善的分布式锁了。
当然实际的开发,没人会去自己写分布式锁的命令,因为有专业的轮子——Redisson。
其他问题
1.假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?
使用 keys
指令可以扫出指定模式的 key 列表。但是要注意 keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan
指令,scan
指令可以无阻塞的提取出指定模式的 key
列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys
指令长。
2.Jedis与Redisson对比有什么优缺点?
Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
3.Redis和Menmcache区别?
- redis支持多种数据类型
- 可以持久化数据
- 虽然都是基于内存存储,但是redis比Memcache快
底层结构(简历不写精通没人会问)
1.Redis底层结构
2.了解redis中的SDS么?和C中字符串相比有什么优势?C语言的字符串会有什么问题?Redis是如何解决的?空间预分配和惰性空间释放原理?
3.字典、跳跃表、压缩列表、快速列表了解么?
文中图片、原文大多数来源于https://mp.weixin.qq.com/s/19u34NXALB1nOlBCE6Eg-Q和书籍Redis设计与实现