redis作为目前比较主流的缓存数据库,一直是中高级后端工程师面试的宠儿。缓存可以提高用户查询的效率,可以应付数据库熔断的事件,为数据库层面减轻了压力。


Redis基本知识

  • 1、缓存如何解决穿透以及熔断问题
  • 2、缓存中间件Memcache与redis的区别
  • 3、Redis为什么这么快?
  • 4、Redis的数据类型
  • 5、从海量数据中取出相同前缀的Key
  • 6、如何实现分布式锁
  • 7、Redis如何持久化
  • RDB:保存某个时间点当前的全部数据快照,保存为二进制文件,RDB保存方式分为自动化保存以及手动保存。
  • AOF:保存写状态(记录数据变更时候的操作记录)
  • Redis数据的恢复
  • RDB和AOF的优缺点
  • RDB—AOF混合模式下的持久化方式
  • 使用pipeline的好处
  • Redis主从同步原理
  • 全同步
  • 增量同步
  • Redis SentInel(哨兵)
  • 如何使用Redis实现异步队列
  • Redis集群


1、缓存如何解决穿透以及熔断问题

目前,主流的数据请求应用架构基本都是 请求——>缓存层——>持久层。
用户发送请求先到缓存层查询数据,如果查询的数据没有的话,才会到数据库层面去查询数据。
穿透:缓存层没有对应的数据,向存储层发起请求。
熔断:当数据库突然发生不可预料的错误或者崩溃,此时,为了能继续向客户提供服务,会将请求打在缓存层,即使缓存层没有,也会向请求端返回响应,在一定程度上做到了即时止损。

2、缓存中间件Memcache与redis的区别

mencache代码层次类似Hash,仅支持简单类型,不支持主从,不支持分片,不支持持久化。
redis数据类型丰富,支持主从分离,支持持久化,支持分片。
俗话说的好,没有最好的技术,只有更合适的场景,memcache适合在仅仅需要缓存而不需要持久化的条件下。redis适合于复杂条件下需要持久化以及主从分离的场景。

3、Redis为什么这么快?

官方提供的Redis的查询效率是10W+QPS(每秒查询次数)。
原因是因为

  • redis完全基于内存,绝大部分操作都是在内存中做的
  • redis的数据类型简单,数据操作简单,redis没有像关系型数据库那样,要求各个表之间有关联,各个属性之间有关联,它是标准的K-V数据库。
  • redis是单进程处理请求,避免了频繁的上下文切换以及锁的竞争。
  • redis虽然是单进程(这里的单进程指的是处理用户请求单进程,如持久化还需生成子进程),但是使用了多路I/O复用技术,即每一个文件都会生成一个FD(文件描述符),利用系统调用函数select、poll、epoll等实时监控每个文件,如果当前文件可读或者可写,即通知系统可以处理该文件,而非阻塞型I/O。

4、Redis的数据类型

redis有五种基本的数据类型
String、List、Hash、Set、Sorted Set

  • String:作为最基础的String类型,String可以储存许多数据,如数字、字符串、图片等二进制文件,并且单个String最大可以储存512M的文件。
set key value//设置某个属性
get key//取出某个属性
set key value1 //可以通过该方式来更改key对应的value
del key //删除某个key
  • List:List是一个列表,同我们在Java中看到的List有几分相似,List是一个有序集合,该数据类型可以从头部删除或者添加,也可以从尾部删除或者添加,类似一个双向的队列。
lpush list value //向key为list的列表中头部插入一个数据
lpop list value//向key为list的列表中头部拿出一个数据
rpush key value
rpop key value
  • Hash:hash是一个String类型的集合,适用于存储一个对象信息,该格式为 hmset hash name “张三” age 18 sex “男”,这句话创建了一个名为hash的Hash字典,适用于存储json字符串。
hmset hash name "张三" age 18 sex "男"
hget hash age //取出hash中key为age的属性
  • Set:无序集合,由String元素组成,类似Java中的Set,其中元素不可以重复,通过hash表实现。
sadd set1 "aaa"
sadd set1 "bbb" //向set中添加了aaa 和 bbb
smembers key //查看当前key的所有元素
  • Zet(Selected Set)有序集合,同样不允许重复,由String元素组成,在设置时,必须设置一个分数,用于zset的排序。
zadd set1 2 "haha"
zadd set1 3 "heihei"//添加数据
zrengescore set1 0 10 //输出分数从0到1的set中的值

5、从海量数据中取出相同前缀的Key

从海量数据中取出相同前缀的数据,需要先搞清楚数据量的大小,如果数据量比较小,则可以使用keys [pattern]来直接获取匹配pattern的key,但是,这种方法会阻塞主进程,如果数据量比较大,则会出现服务器卡顿,如果这时服务器正在向其他用户提供服务的话,则会出现恶劣的影响。
数据量比较大时,我们可以使用SCAN cursor [match pattern] [count count]迭代器来查找,其中cursor是游标,每次迭代时都要输入上次的游标,第一次查询时要输入0,每次查询后,服务器会返回一定数量的key以及迭代到到什么位置了,这个数量可能小于等于count,也可能不返回,每次返回的游标位置也可能比查询时的小,这就要解决重复key的问题了。直到迭代返回的游标为0的时候,才算结束了迭代。
使用游标返回key时,服务器不会有卡顿现象产生,但是使用游标迭代时,所花费时间要大于直接使用keys pattern的查询方式。

6、如何实现分布式锁

实现分布式锁主要要解决以下问题

  • 互斥性:当不同的客户端操作同一个资源时,要进行互斥
  • 安全性:锁只能由锁的创建者所释放
  • 死锁:当某个客户端获得锁之后没有来得及释放就宕机了,此时,必须要释放该锁
  • 容错:当部分节点失效后,有客户端申请获得锁,必须要能申请到锁

初始解决方法
使用setnx key value可以设置当前key长期有效,且不能被更改(创建成功后返回1,此后修改会返回0表示失败),但是出现了一个问题,怎样去解决该key长期有效的问题,此时,我们可以使用expire key time为当前的key设置过期时间,这样两个操作结合,就可以实现分布式锁了
问题:我们在Java中写一段程序

String status = getKey(key,value);//该操作获取锁
if (status == 1) {
setTime(key,time);//为其设置过期时间
//进行操作
}

乍一看这段伪代码没什么问题,但是仔细看就会发现,如果程序在获取锁的时候,还没执行到if那块的时候挂了,此时,获得了锁,但是该程序却永远也没办法解除锁了(关于为什么其他人不能去解锁,因为要保持安全性,上面有解释),这就形成了死锁。形成该情况的原因是虽然两个操作都是原子性的,但是合并在一起就不是原子性了。
解决方法:使用set key value [EX seconds] [PX milliseconds] [NX|XX]其中EX和PX只需要设置一个,分别为秒和毫秒,后边的NX意为只在不存在的时候创建,XX与之相反只在存在的时候改变。

7、Redis如何持久化

Redis有两种持久化方式,分别为RDB持久化以及AOF持久化

RDB:保存某个时间点当前的全部数据快照,保存为二进制文件,RDB保存方式分为自动化保存以及手动保存。

自动化保存:自动化保存会在四个条件下触发。

一、通过save n m时间策略进行保存(save 900 1 900秒内有一次修改操作,save 300 10 300秒内有10次改动 60 10000 1min内有10000次修改)。

二、主从复制时,redis会自动触发RDB持久化,为了将该文件拷贝到从属数据库上。

三、执行debug reload 四、执行shutdown,并且没有开启AOF持久化时

手动保存

一、通过 save指令,此时数据库会在主进程内执行该方法,用来持久化数据,此方法会严重阻塞主进程,不建议使用。

二、通过bgsave 指令,此方法执行之后,Redis会(fork)生成一个子线程,该子线程会在后台进行持久化,并不会干扰到主进程的运行。此时,父子进程会执行一个copt-on-write策略(写时复制),redis子进程在进行复制时,父进程不会直接复制一份资源给子进程,而是与子进程同时使用相同的资源,这样可以让子进程选择要备份的数据,不会造成资源的浪费,当父进程要对资源进行修改的时候,系统会复制一份资源给父进程,供其操作。

三、bgsave实现原理

redis的lrange一次取多少合适_redis的lrange一次取多少合适

父进程在生成子进程时会查看是否此时有AOF/RDB子进程是否正在进行,如果有则返回错误,这样是为了避免进行资源竞争。
缺点:内存数据的全量同步,如果数据量大的话会由于I/O频繁而严重影响服务器性能。如果redis挂掉了,会丢失最近一次快照到此时间节点的全部数据。

AOF:保存写状态(记录数据变更时候的操作记录)

AOF会记录处理查询之外所有对数据进行变更的指令,并且保存在.aof文件中。指令会以追加的方式来写入到AOF文件(增量)。AOF默认是关闭的,需要通过redis.conf文件打开。
AOF配置策略(三选一)

appendfsync : always //如果有改变,总是将其写入
appendfsync : everysec//每秒写入一次
appendfsync : no //交给OS来管理,OS一般会等到缓存满了之后才进行写入

解决AOF日志大小不断增加的问题
原因:每当我们有一个改动操作时就会写入一个,但是,有时候我们并不需要那么多的指令,比如我们在使用 incr key 计数器时,我们只需要最后的那条指令,之前的指令载入只会浪费空间。
解决方法:使用AOF重写bgwriteaof 1、父进程调用fork方法生成一个子进程,子进程将当前全部数据生成AOF指令,并且写入新的AOF文件中。
2、父进程在子进程生成文件时,将新的AOF指令不断的写入缓存以及旧的AOF文件中(为了防止子进程失败)。
3、子进程写完之后通知父进程,父进程将缓存中的AOF再次发送新的增量变动。
4、子进程接收到新的增量之后再次写入新的AOF文件中,写完之后将该文件替换旧的AOF文件。

Redis数据的恢复

数据恢复非常的简单,只需要重启Redis-Server,重启之后,Redis会子自动加载AOF/RDB文件进行数据恢复。

redis的lrange一次取多少合适_redis的lrange一次取多少合适_02

RDB和AOF的优缺点

RDB
优点:全量数据快照,文件小(二进制)恢复快
缺点:无法保存最近一次快照之后的数据
AOF
优点:可读性高,适合保存增量数据,数据不易丢失
缺点:文件体积大,恢复时间长

RDB—AOF混合模式下的持久化方式

既然两者都有优缺点,我们是否可以取之优点来互补呢?
当然可以,redis4.0之后实现了这个方法,在进行持久化的时候,父进程fork一个子进程,之后子进程将当前全量数据保存下来并且写入一个新的AOF文件中(以rdb格式),同时父进程将增量数据保存在缓存中,当子进程告知父进程写完了,父进程会将增量数据发送给子进程,子进程再将增量写到新的AOF文件中。
此时的AOF文件,前半段是rdb格式的全量快照,后半段是增量数据的aof。
这个流程同AOF进行重写的时候非常相似。

使用pipeline的好处

我们在与redis服务器进行交互是发送一个请求,返回一个响应,如果我们要向服务器发送大批量的命令(请求),就会非常慢,这时,我们可以使用pipeline批量上传指令,上传的指令之间不可以有依赖关系。如果有依赖关系则使用分批上传。

Redis主从同步原理

redis的主从同步是master与salve之间进行同步,其中又分为全同步和增量同步。

全同步

  • salve发送synu命令到master
  • master启动一个后台进程(子进程),将全量数据保存到临时文件中
  • master同时将之后的写命令缓存起来。
  • master完成第二步的写操作之后,将该文件发送给salve,salve接收到该文件之后开始更新数据,并向master反馈一个信息
  • master将缓存的指令发送给salve,之后salve接收到之后继续更新操作

增量同步

  • master接收到用户的指令后,判断是否应该传到salve,(增删改)
  • master将操作记录记录到AOF文件中
  • 之后master将操作传递到其他的salve中。这里需要注意两点 1、需要对齐主从库(更新对应的数据)2、向响应缓存中写入指令,之后将缓存的数据发送给solve

Redis SentInel(哨兵)

哨兵的存在是为了解决主从同步的时候,主master宕机之后的主从切换问题,哨兵主要进行三方面的作用。

  1. 监控:哨兵通过检查主从服务器是否正常而实现监控功能
  2. 提醒:如果主从库出现了问题,哨兵会通过API提醒管理员或者向其他应用程序发送故障通知。
  3. 主从切换:哨兵会执行Gossip算法(流言协议)来进行主从切换,如果这时有其他请求发送到了瘫痪的master,哨兵进程会将新的master服务器地址返回给前台。

Gossip算法(流言协议)

  1. 每个节点随机的同其他节点通信,最终所有的节点达成一致。
  2. 种子节点定期想起他节点发送节点列表以及需要传播的消息。
  3. 不能保证消息一定传递到所有的节点,但是最终所有节点的状态是趋于一致的。

如何使用Redis实现异步队列

实现异步队列可以使用redis中的List,使用其RPUSHLPOP来进行队列的实现。
rpush产生消息,lpop消费消息。
缺点:lpol没有实现等待消息队列中有值就直接消费(一般会设置一个轮询来不断的pop,但是如果队列中没有值的话,pop会报错)
解决方法:
1、使用sleep方法,过一段时间再去访问消息队列。
2、使用blpop key time来实现定时pop,(如果在时间内队列中有值的话就接收,如果没有值的话就超时)一般还是设置一个循环来轮询
缺点:以上的队列虽然实现了等待消息队列中有值才消费,但是只能作用于一个消费者。
解决方法:使用pub/sub订阅者模式实现多个消费者

subscroibe key //订阅
pulish key "信息" //向订阅中发送一个信息

俗话说,没有完美的技术,虽然这个能实现多个消费者通信,但是其缺点是消息的发布是无状态的,无法保证可达,如果想使用,最好使用kafka等专业的消息队列。

Redis集群

如何从海量数据中快速找到所需要的数据呢?如果只单纯的靠一个数据库,在大数据量十分庞大的今天,似乎已经行不通了,于是,大家就产生了分片的想法,按照某种规则去划分数据,分散存储到多个节点中,降低当前节点压力,数据少了,查的也就快了。

  • 一致性哈希算法:对232取模,将哈希值空间模拟成一个虚拟圆环(哈希环)0~(232)-1。
  • redis集群中,有两种类型的节点:主节点(master)、从节点(slave)。
  • 将服务器的关键信息,比如(IP地址)使用一致性哈希算法计算之后得到一个hash值,这样可以使其分布在环上。
  • 将数据库中的key也使用一致性hash算法,计算hash值,按顺时针旋转,遇到的第一个服务器节点就是存储这些数据的节点。
  • 如果有一个节点宕机了,会将其上一个节点到宕机节点的数据按顺时针定位下一个遇到的节点中。
  • 要添加节点时,将添加节点到逆时针遇到的第一个节点之间的数据定位到新的节点。
    问题:容易出现哈希环数据倾斜,如果节点较少,且两个节点分布距离较近,就会出现其中一个节点涌入大量数据,有可能被撑爆。
    解决方法:引入虚拟节点,虚拟节点是将原本的节点关键字计算时再加入其他的信息,使其计算出hash值,分布在其他位置(但实际存储还在原节点)这样就可以解决分布不均匀的隐患了。

再具体的一致性哈希算法可以看看这篇文章一致性哈希算法