Redis面试题


Redis面试题

1.介绍Redis中数据类型

redis中的五种常用类型分别是string,Hash,List,Set,ZSet。

类型特点使用场景
string简单的key-value类型,
value其实不仅是String,也可以是数字
定时持久化,操作日志,常规计数, 微博数, 粉丝数等功能
hash是一个string类型的field和value的映射表,
hash特别适合用于存储对象
存储部分变更数据,如用户信息等
list有序可重复的列表twitter的关注列表,粉丝列表,最新消息排行,消息队列
set无序不可重复的列表可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。
Redis还为集合提供了求交集、并集、差集等操作,可以方便的实现如共同关注、共同喜好等功能
zset带有score的Set分数排行榜

2.redis中的持久化方案

RDB:快照形式,定期把内存中当前时刻的数据保存到磁盘。Redis默认支持的持久化方案。
   速度快但是服务器断电的时候会丢失部分数据

AOF:append only file。把所有对redis数据库操作的命令,增删改操作的命令。保存到文件中。
   数据库恢复时把所有的命令执行一遍即可。
   两种持久化方案同时开启使用AOF文件来恢复数据库,能保证数据的完整性,但是速度慢。

两者如何选择?

  1. 如果redis仅仅是用来做为缓存服务器的话,我们可以不使用任何的持久化
  2. 一般情况下我们会将两种持久化的方式都开启。redis优先加载AOF文件来回复数据。RDB的好处是快速
  3. 在主从节点中,RDB作为我们的备份数据,只在salve(从节点)上启动,同步时间可以设置的长一点,只留(save 900 1)这条规则就可以了
  4. 开启AOF的情况下,主从同步是时候必然会带来IO的性能影响,此时我们可以调大auto-aof-rewrite-min-size的值,比如5GB。来减少IO的频率
  5. 不开启AOF的情况下,可以节省IO的性能影响,这是主从间通过RDB持久化同步,但如果主从都挂掉,影响较大~

3.redis的优点

  1. 读写速度快. 数据存放在内存中,数据结构类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
  2. 支持丰富的数据类型,string,hash,list,set,sorted
  3. 支持事务,watch
  4. 丰富的特性:可以用于缓存,消息队列,按key设置过期时间,到期后自动删除
  5. 支持数据持久化(将内存数据持久化到磁盘),支持AOF和RDB两种持久化方式,从而进行数据恢复操作,可以有效地防止数据丢失
  6. 支持主从(master-slave)复制来实现数据备份,主机会自动将数据同步到从机

4.主从模式

主从结构一是可以进行冗余备份,二是可以实现读写分离.

主从复制:
  当我们在redis中开启了持久化(RDB或者AOF)但是这样也不能保证数据一定不会丢失,当Redis服务因为硬盘损坏那么数据也就丢失了,这时我们通过主从模式,一个master设置多个slave,那么在slave上面就会有多个备份数据,提高了我们数据的抗灾能力。

  1. 一个Master可以有多个Slave,不仅主服务器可以有从服务器,从服务器也可以有自己的从服务器
  2. 复制在Master端是非阻塞模式的,这意味着即便是多个Slave执行首次同步时,Master依然可以提供查询服务
  3. 复制在Slave端也是非阻塞模式的:如果你在redis.conf做了设置,Slave在执行首次同步的时候仍可以使用旧数据集提供查询;你也可以配置为当Master与Slave失去联系时,让Slave返回客户端一个错误提示
  4. 当Slave要删掉旧的数据集,并重新加载新版数据时,Slave会阻塞连接请求

读写分离:
  主从架构中,可以考虑关闭主服务器的数据持久化功能,只让从服务器进行持久化,这样可以提高主服务器的处理性能。
  从服务器通常被设置为只读模式,这样可以避免从服务器的数据被误修改

5.主从模式下宕机怎么办

slave宕机

   相对简单,slave启动后会自动同步数据,增量同步。
  
master宕机

手动恢复

  1. 在从数据库中执行SLAVEOFNO ONE命令,断开主从关系并且将从库提升为主库继续服务
  2. 将主库重新启动后,执行SLAVEOF命令,将其设置为其他库的从库,这时数据就能更新回来

哨兵功能自动恢复
  通过sentinel模式启动redis后,自动监控master/slave的运行状态,
  已经被集成在redis2.4+的版本中如果Master异常,则会进行Master-Slave切换,
  将其中一个Slave作为Master,将之前的Master作为Slave
  
基本原理是:心跳机制+投票裁决

6. Redis主从,哨兵,集群的区别

redis主从:是备份关系, 我们操作主库,数据也会同步到从库。
   如果主库机器坏了,从库可以上。就好比你 D盘的片丢了,但是你移动硬盘里边备份有。

redis哨兵:哨兵保证的是HA,保证特殊情况故障自动切换,
   哨兵盯着你的“redis主从集群”,如果主库死了,它会告诉你新的老大是谁。

redis集群:集群保证的是高并发,因为多了一些兄弟帮忙一起扛。
   同时集群会导致数据的分散,整个redis集群会分成一堆数据槽,即不同的key会放到不不同的槽中。

主从保证了数据备份,哨兵保证了HA 即故障时切换,集群保证了高并发性

7.Redis实现消息队列

普通队列:一般使用list结构作为队列,rpush生产消息,lpop消费消息,blpop阻塞消费。
消费多次:生产一次消费多次的情况使用发布/订阅模式
延时队列:使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理

8.如果有大量的key需要设置同一时间过期,一般需要注意什么?

   如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。
   一般需要在时间上加一个随机值,使得过期时间分散一些。

9.Redis的同步机制了解么?

主从同步
   第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。
   加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。

10.是否使用过Redis集群,集群的原理是什么?

   Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
   Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

11. Redis 管道技术

  Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。
  这意味着通常情况下一个请求会遵循以下步骤:

  • 客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应
  • 服务端处理命令,并将结果返回给客户端

  redis采用的是CS架构,客户端与服务器端通过tcp协议进行连接通信,因此无论是发出请求还是接收响应,都必须经过网络传输。
  在tcp连接过程中,客户端和服务器端是通过阻塞式的一问一答方式进行通信的,即客户端必须接收到服务端完整的响应,才能进行后续请求。
  有时我们会在短时间内发送大量互不依赖的命令(例如:后执行的命令不需要使用前面返回的结果)。
  由于网络传输不可避免的会造成一定的延迟,特别是在跨机器远程访问redis的时候,如果使用常规的方式,一条命令对应一次请求和响应的话,大量命令累计的延迟会显得很高。
  redis的设计者考虑到这一点,在底层的通信协议上,通过支持"管道(pipeline)"来解决这一问题。

管道技术的优势
 管道技术最显著的优势是提高了 redis 服务的性能。

简单示例:

普通命令:3次请求/响应
  client:INCR num001;
  server:1;
  client:INCR num001;
  server:2;
  client:INCR num001;
  server:3;
在这里插入图片描述

管道命令:1次请求/响应
  client:INCR num001;
  client:INCR num001;
  client:INCR num001;
  server:1;
  server:2;
  server:3;
  在这里插入图片描述
  redis管道可以在一次tcp的请求中同时发送多条命令,并且将响应结果一次性的返回给客户端。
  对于既定数量的命令请求,redis管道通过减少客户端和服务器端的通信次数,来达到减少通信传输中往返时间的目的,提高效率。
  
2.redis管道注意事项

    1.由于redis的管道要求服务器一次性的将请求返回,因此redis服务端只能将靠前命令处理的结果暂时缓存起来。
    如果管道一次响应的数据量过多(大规模查询之类的),可能会对redis服务器的内存造成较大的压力。
    因此,管道批量处理的命令数量并不是越多越好,需要结合实际需求,合理的决定一次管道批处理命令的数量。

    2.redis的管道在客户端通常会设置一个命令缓冲区来存储即将被批量发送的命令,当缓冲区被填满时,才会一次性的将缓冲区的命令发送。
    这里需要注意的一点是:当业务上的同一批命令使用管道进行请求时,如果最后剩余的命令无法填满缓冲区,如果不使用相应的flush操作,这些命令将不会被发送出去,而是保留在命令缓冲区等待新的命令来填满缓冲区。
  
3.redis管道总结

    redis的管道可以将多个命令打包,一次性的发送给服务器端处理,当命令之间不存在依赖关系时,相比于一条命令一次请求的普通操作方式,管道的操作几乎是对使用者透明的。

    和redis的事务类似,redis管道能完成的操作也能够被更加灵活的redis脚本实现,但是脚本的可读性不强、可维护性差。
    个人认为,如果批量处理的命令之间不存在依赖关系时,优先使用管道;反之,则只能使用脚本了。

    redis的管道技术在客户端和服务器端的实现还有很多细节值得探讨,限于个人能力,就不展开说明了。

12. 缓存问题

缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

解决办法

1.对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃。
 还有最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,
 一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

  1. 也可以采用一个更为简单粗暴的方法,如果一个查询返回的数据为空
    ( 不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存雪崩

缓存雪崩,是指在某一个时间段,缓存集中过期失效,后面的请求全部都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方式:    就是上面设置过期时间中使用的方式,灵活设置过期时间。
      事前:尽量保证整个Redis集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略
      事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
      事后:利用Redis持久化机制保存的数据尽快恢复缓存

  1. 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  2. 可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存
  3. 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀. 比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
  4. 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀. 比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件

缓存击穿

   缓存击穿,是指一种key是非常"热点"的数据,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
   
    缓存击穿和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
    缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候高并发的请求可能会瞬间把后端DB压垮。

解决方式   直接设置为永久key就可以了。

缓存预热

  缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。
  避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

缓存预热解决方案:

  1. 直接写个缓存刷新页面,上线时手工操作下
  2. 数据量不大,可以在项目启动的时候自动进行加载
  3. 定时刷新缓存

缓存更新

我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?
  除了缓存服务器自带的缓存失效策略之外(Redis默认的有6种策略可供选择),
  我们还可以根据具体的业务需求进行自定义的缓存淘汰,
  常见的策略有两种:

  1. 定时去清理过期的缓存
  2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存

两者各有优劣,
  第一种的缺点是维护大量缓存的key是比较麻烦的,
  第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!
  具体用哪种方案,根据自己的应用场景来权衡。

缓存降级

  当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
  降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
  在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级
  2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警
  3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级
  4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级

缓存 “无底洞” 现象

简介

    指的是为了满足业务要求添加了大量缓存节点,但是性能不但没有好转反而下降了的现象。

产生原因

    缓存系统通常采用 hash 函数将 key 映射到对应的缓存节点,随着缓存节点数目的增加,键值分布到更多的节上,导致客户端一次批量操作会涉及多次网络操作,这意味着批量操作的耗时会随着节点数目的增加而不断增大。
    此外,网络连接数变多,对节点的性能也有一定影响。

解决方案

  1. 优化批量数据操作命令
  2. 减少网络通信次数
  3. 降低接入成本,使用长连接 / 连接池,NIO 等

缓存一致性

简介
      缓存一致性要求数据更新的同时缓存数据也能够实时更新
解决方案

  1. 在数据更新的同时立即去更新缓存
  2. 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新
  3. 要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据

缓存热点key

使用缓存 + 过期时间的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。但是有两个问题如果同时出现,可能就会对应用造成致命的危害:

  1. 当前 key 是一个热点 key( 可能对应应用的热卖商品、热点新闻、热点评论等),并发量非常大
  2. 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等

在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
热点 key 失效后大量线程重建缓存
要解决这个问题也不是很复杂,但是不能为了解决这个问题给系统带来更多的麻烦,所以需要制定如下目标:
减少重建缓存的次数
数据尽可能一致
较少的潜在危险

1)互斥锁 (mutex key)

此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可,

下面代码使用 Redis 的 setnx 命令实现上述功能,伪代码:

String get(String key) {
	 //从redis中获取key 
	 String value = redis.get(key); 
	 //如果value为空则开始重构缓存 
	 if (value == null) { 
		 //只允许一个线程重构缓存,使用nx,并设置过期时间ex 
		 String mutexKey = "mutex:key" + key; 
		 if (redis.set(mutexKey, "1", "ex 180", "nx")) { 
			 //从数据源获取数据 
			 value = db.get(key); 
			 //回写redis并设置过期时间 
			 redis.set(key, value, timeout); 
			 //删除mutexKey 
			 redis.del(mutexKey); 
		 } else {
			  //其他线程睡眠50秒再重试 
			  Thread.sleep(50); get(key); 
		  } 
	  } 
	  return value; 
}

从 Redis 获取数据,如果值不为空,则直接返回值。
如果 set(nx 和 ex) 结果为 true,说明此时没有其他线程重建缓存,那么当前线程执行缓存构建逻辑。
如果 setnx(nx 和 ex) 结果为 false,说明此时已经有其他线程正在执行构建缓存的工作,那么当前线程将休息指定时间 (例如这里是 50 毫秒,取决于构建缓存的速度 ) 后,重新执行函数,直到获取到数据。

2)永远不过期

永远不过期”包含两层意思:

从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期。

从功能层面来看,为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。

从实战看,此方法有效杜绝了热点 key 产生的问题,但唯一不足的就是重构缓存期间,会出现数据不一致的情况,这取决于应用方是否容忍这种不一致。下面代码使用 Redis 进行模拟:

String get(final String key) { 
	V v = redis.get(key); 
	String value = v.getValue(); 
	//逻辑过期时间 
	final Long logicTimeout = v.getLogicTimeout(); 
	//如果逻辑时间小于当前时间,开始重建缓存 
	if (logicTimeout <= System.currentTimeMillis()) { 
		final String mutexKey = "mutex:key" + key; 
		if (redis.set(mutexKey, "1", "ex 180", "nx")) { 
		//重建缓存 
		threadPool.execute(new Runnable() { 
			@Override 
			public void run() { 
				String dbValue = db.get(key); 
				redis.set(key, (dbValue, newLogicTimeout));
				redis.del(mutexKey); 
				} 
			}); 
		} 
	}
	 return value; 
}

作为一个并发量较大的应用,在使用缓存时有三个目标:

  1. 加快用户访问速度,提高用户体验
  2. 降低后端负载,减少潜在的风险,保证系统平稳
  3. 保证数据“尽可能”及时更新

下面将按照这三个维度对上述两种解决方案进行分析。

互斥锁 (mutex key):这种方案思路比较简单,但是存在一定的隐患,如果构建缓存过程出现问题或者时间较长,可能会存在死锁和线程池阻塞的风险,但是这种方法能够较好的降低后端存储负载并在一致性上做的比较好。

” 永远不过期 “:这种方案由于没有设置真正的过期时间,实际上已经不存在热点 key 产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值