Redis面试以及在分布式集群环境中遇到的问题

Redis 面试题

前面已经系统的梳理了Redis的各个功能和搭建的方法,下面我们就来系统的梳理下,关于Redis在系统中可能会出现的面试题

  1. 介绍Redis的集中架构模式

    • 单机型

    这个模型很简单,多个客户端直接连接Redis服务器

    特点:内存容量有限,支持的QPS有限,无法扩容,并且没有高可用

    • 主从复制(读写分离)

    通过Redis的replication的功能,创建多个Master服务器从属的Slave服务器,其中Master服务器进行写操作,所有的Slave服务器进行读操作,这样只要网络正常的话,Master和Slave都具有相同的数据

    特点:支持了读QPS的扩容,但是内存容量的问题没有解决,也没有解决写QPS的高并发问题,同时也没有高可用

    • 哨兵

    利用Redis的sentinel分布式集群,来兼容Redis主从服务器,当Master服务器下线或者不再对外提供服务时,将Slave节点升级为Master节点,以保证Redis服务能继续对外提供服务

    特点:集群监控、消息通知、故障转移和配置中心,解决了上面两个架构模式中的高可用的问题,但是主从切换需要时间,可能会造成数据部分丢失

    • 集群

    Redis 3.x版本之后,提供RedisCluster来创建Redis集群环境,利用RedisCluster来管理多个Redis的服务

    特点:无中心架构、数据按照hashslot存储分布在多个节点,节点之间数据共享、可横向和纵向扩容,Master节点扩容,高可用并且能自动实现故障转移、节点之间通过gossip协议进行状态信息交换

  2. Redis集群对于存储节点采用的算法是什么?能否详细介绍该算法?

Redis在分布式的算法中采用了哈希槽的概念,在详细介绍哈希槽之前,我们需要先简单的了解下一致性hash算法的概念

  • 一致性hash算法

一致性hash算法的总结参考了博文《一致性哈希算法原理》,现整理如下:

1. 场景:如果一个集群接收多个请求,那么该怎么通过算法将每个请求分布到集群中对应的服务,并且满足高效率呢?
2. 一致性hash算法的原理:将集群中所有的服务器的节点先计算hash值,然后将该值存放管道0~2的32次方的圆形节点中;
发送过来的请求也计算对应的hash值,然后根据hash值到圆形节点中顺时针查找最新的服务节点,然后将请求发送过去,等待该节点服务器的响应;
3. 如果对集群进行扩容,新增一台服务,则相当于在圆形节点上新增一个节点,
这样当访问的请求进行hash计算后的值到圆形节点上进行匹配时,最多影响该节点相邻的节点,对其他的节点没有任何影响;
4. 一致性hash性质:平衡性,单调性,分散性,负载和平滑性;
  • hashslot

hashslot的算法参考了博文《Redis哈希槽》,先整理如下:

1. RedisCluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点和其他节点进行连接
2. 所有的redis节点彼此互联(PING-PONG机制),内部使用RESP协议进行通讯,该协议具有实现简单、快速解析和可读性好的优点;
3. 节点的fail是通过集群中超过半数的节点检测失效时才生效
4. redis-cluster把所有的物理节点映射到0-16383个slot上,cluster负责维护node-slot-vlaue
5. Redis集群预先分好16384个slot,当需要在Redis集群中放置一个key-value时,根据CRC16 mod 15384的值,决定将一个key放置到哪个slot中
  1. 什么是缓存穿透,缓存击穿,如果避免,什么是缓存雪崩,如何避免
  • 缓存穿透

一般前端传来请求,先查询缓存,缓存中没有了在查询数据库,而缓存穿透,前端大量请求过来,查询缓存中不存在,则再查询数据库,而数据库中也不存在,则这个值无法形成缓存,那么这个请求每次都会请求数据库,这对数据库就造成了极大的压力;

我们可以对空也进行缓存,缓存的失效时间可以设置短点;也可以对一定不存在的key进行过滤,可以将所有可能存在的key存放在一个大的bitmap中,查询时通过bitmap进行过滤;

  • 缓存击穿

是指一个Key非常热点,再不停的抗住高并发,大并发集中对这个key进行访问,如果该key失效了,则持续的高并发就会击穿缓存,直接请求数据库,严重时可能造成数据库宕机

其实对于热点key来说,很容易解决,只要将这个key对应的失效时间设置为永不失效即可,当然这个主要是根据业务的需求,一般大key是一段时间内的访问量是很恐怖的,但是过了这个时间段访问请求就下来了,到时再更改失效时间即可;

  • 缓存雪崩

缓存雪崩是指在某个时间段内,缓存集中过期失效。这样请求都会落在数据库上,对数据库造成极大的压力,出现系统崩溃;

1. 我们可以将系统中不同种类的key分别设置不同的失效时间,
比如说我们可以在原来的过期时间基础上增加一个随机值,这样的话大部分的key的过期时间就会有差别,不会集中失效<br/>
2. 其次使用缓存降级,利用eehcache等本地缓存,但主要还是对源服务访问进行限流、资源隔离、降级等。
当访问量剧增、服务出现问题仍然需要保证服务还是可用的。
系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级,这里会涉及到运维的配合
3. Redis缓存的备份和预热
  • 缓存并发

是指多个redis的client同时set

解决的思路可以将redis的set操作放到队列中使其进行串行执行

  1. Redis有哪些常见的用途
  • 会话缓存
  • 全页缓存
  • 队列
  • 排行榜/计数器
  • 发布/订阅
  1. Redis中事务相关的命令有哪些?
  • MULTI
  • EXEC
  • DISCARD
  • WATCH
  1. Redis如何做内存优化

在选择数据结构时,尽可能使用散列表(hashes),因为散列表使用的内存是非常小的,所以你尽可能的将你的数据模型抽象到一个散列表中

  1. Redis内存回收机制以及过期策略

Redis的内存监控是使用:redis-cli 中的info命令来查看的

    # Memory
    used_memory:26355360#当前Redis所有key-value值及内部开销理论上要占用的内存
    used_memory_human:25.13M#上一数据的可读版本
    used_memory_rss:28127232##(Resident Set Size常驻数据集大小),可理解为OS为Redis分配的物理内存总量
    used_memory_rss_human:26.82M
    used_memory_peak:26355360#峰值内存
    used_memory_peak_human:25.13M#峰值内存可读版本
    total_system_memory:8253997056
    total_system_memory_human:7.69G
    used_memory_lua:37888#lua引擎占用内存
    used_memory_lua_human:37.00K
    maxmemory:0
    maxmemory_human:0B
    maxmemory_policy:noeviction
    mem_fragmentation_ratio:1.07#内存碎片率,used_memory_rss 和 used_memory 之间的比率
    mem_allocator:jemalloc-4.0.3#所使用的内存分配器。可以是 libc 、 jemalloc 或者 tcmalloc

其中mem_fragmentation_ratio(内存碎片率)是分析Redis性能的重要数据指标

  • 大于1:OS为Redis分配的物理内存 > Redis所有key-value值及内部开销应占用的内存
    产生原因:物理内存多出的部分,Redis内移除对象的占用内存,但这部分内存由Redis自带内存分配器占用,没有向操作系统返回。这一部分就是内存碎片
  • 小于1:OS为Redis分配的物理内存 < Redis所有key-value值及内部开销理应占用的内存
    产生原因:应占内存比物理内存多出的部分,是被操作系统交换到虚拟内存,说明当前Redis的内存使用已经超出物理内存

内存碎片率保持在1.0至1.5之间是最理想的状态。假若碎片率超过了1.5,我所知道的最有效解决手段就是重启Redis服务器,释放内存回到操作系统;反之,若碎片率为0.9,说明物理内存已不够用,应增添硬件,或设置Redis最大内存限制maxmemory

最大内存限制maxmemory的设置非常重要,如果不设置maxmemory,Redis一直会为其分配内存,直至耗尽所有物理内存,直到操作系统进行虚拟内存交换。因此,一般情况下,作者建议还是把峰值设置设上。开启此配置,当超出限定内存情况发生,Redis会返回异常消息,操作系统不会因内存溢出而奔溃。还有一点建议是,开发者在系统设计之初,就应当制定Redis内存使用划分计划,而划分原则是,为Redis准备系统可能使用的峰值内存,而不是平均使用内存。例如系统大部分情况会以Redis作为分布式缓存写入10G数据,但大部分情况下只会跑到4G,但Redis依然推荐用户为其预留10G内存(used_memory_peak峰值)。

maxmemory的单位是bytes,默认为0,即不限制最大内存。

Redis的内存回收主要围绕Redis的过期策略和淘汰策略来进行的,Redis中过期策略和淘汰策略是不一样的,我们来详细了解下两者之间的区别:

  • 过期策略

过期策略分为三种:定时过期、惰性过期以及定期过期

1. 定时过期

是我们通过程序向redis中新增数据的同时就已经设置好该数据的过期时间,到了定时时间就会让该数据失效,
这种方式对内存友好,但是对CPU的计算相对就提高了很多,这个在系统提供高并发服务的受到影响,并且影响系统的响应和吞吐量

2. 惰性过期

所谓的惰性过期,就是我们在访问该数据时,在判断该数据是否过期失效,如果过期则删除,
这种方法能大量的节省CPU的资源,但是对内存非常不友好,可能出现大量的Key没有被访问,从而也不会删除,占用大量的内存

3. 定期过期

每隔一段时间,就会扫描一下redis中expires字典中一定数量的key,并且清除其中已经过期的key。
通过定时扫描的时间间隔和每次扫描的限定损耗,在不同的情况下,使得CPU和内存资源达到相对的一种平衡

Redis中同时用了惰性过期和定期过期

  • 淘汰策略

所谓淘汰策略就是当内存中内存不足,Redis中是达到redis.conf配置中的maxmemory的极限时,需要采用LUA淘汰算法来定期清理掉哪些数据

1. LUA算法

LUA算法就是最近最少使用算法

2. 缓存清理配置

redis.conf配置中的maxmemory配置内存达到多大时,进行内存请求,
在64位系统中,如果配置为0,则不限制内存的使用,
但是在32位系统中,默认显示内存大小是3G

3. Redis内存淘汰策略

淘汰策略是有redis.conf配置中的 maxmemory-policy来控制的,其值主要有以下几个:
策略名称策略说明
volatile-lru使用LRU算法删除一个键(只对设置了生存时间的键)
allkeys-lru使用LRU算法删除一个键
volatile-random随机删除一个键(只对设置了生存时间的键)
allkeys-random随机删除一个键
volatile-ttl删除生存时间接近的一个键
noeviction不删除键,只返回错误
  • noeviction: 当内存不足时写入新的数据,写入操作会报错
  • allkeys-lru: 当不存不足时写入新的数据,在键空间中,移除最近最少使用的key
  • allkeys-random:当不存不足时写入新的数据,在键空间中,随机移除某个key
  • volatile-lru: 当不存不足时写入新的数据,在设置过期时间的键中,移除最近最少使用的键
  • volatile-random:当不存不足时写入新的数据,在设置过期时间的键中,随机移除一个键
  • volatile-ttl:当内存不足时写入新的数据,在设置过期时间的键中,有更早过期时间的key优先删除
  1. Redis实现分布式锁

在开发中锁的概念就非常重要了,我们常接触到的有线程锁、进程锁和分布式锁。

分布式锁就是当多个进程不在同一系统中,用分布式锁控制多个线程对资源进行访问。

分布式锁不仅具有锁的特征,还多了一下其他的特征

  • 互斥性:任意时刻,只有一个客户端获取锁,不能同时又多个客户端获取锁
  • 安全性:锁只能被持有该锁的对象机进行解决,获取锁自动消除
  • 死锁:获取锁的客户端因为某些原因宕机了而未能释放锁,其他的客户端无法获取锁
  • 容错:当部分节点宕机了,客户端仍能获取锁和释放锁

Redis实现分布式锁有以下两种方案:

1. 在操作某个记录时,查看是否存在对应的key记录 ,如果存在说明该记录被加锁,如果没有的话在继续添加锁,
并且添加锁成功之后处理自己的业务逻辑,相关的伪代码如下
// 加锁
function boolean lock(taskId) {
    if (existsKey(taskId)) {
        return false;
    }else {
        // 向redis中查询新的key 设置过期时间
        setKey(taskId);
        return true;
    }
}

// 释放锁
function relaseLock(taskId) {
    if (existsKey(taskId)) {
        delKey(taskId);
    }
}

// 使用锁
booelan lock = false;
try{
    lock = lock(taskId);
    if (lock) {
        // 业务处理
    }
}catch(Exception e) {

}finally {
    if (lock) {
        relaseKey(taskId);
    }
}

上面的代码会出现两个问题

- 释放锁的代码必须保持原子性,否则在高并发的情况下会出现不可预知的问题,不能保证锁的互斥性
- 同一把锁同一个时间内可能会被其他的线程获取到,并且由其他的线程来释放锁,不能保证锁的安全性

2. 为了解决上述的问题,我们来优化代码,针对加锁和释放锁都添加原子性操作,这样单机版的redis环境就不会出现上述的问题了

Redis中支持一个语法 `SETEX key seconds value`, 该语法可以添加一个key,并且给这个key设置过期时间和value,为了达到谁加锁,就让谁来解锁,
则我们可以将value设置成用户的sessionId,并且在释放锁的时候确认是否是对应用户进行操作的,
同时鉴于redis目前没有针对这个业务提供具有原子性的操作指令,我们可以使用lua算法来实现锁的释放,
完善后的伪代码跟上述的代码很像,只是修改了加锁和释放锁的操作,我们这里描述下lua算法:
if readdirSync.call('get', KEYS[1] == ARGV[1] then
    return redis.call('del', KEYS[1]);
else
    return 0;
end

该分布式锁在单机版环境下能达到业务的需求,但是在Redis集群环境中,由于Redis的主从复制时异步的,所以上述的方案在集群环境中还是有问题的

  1. Redis缓存和Mysql数据库一致性的问题

在高并发的业务场景中,一般都是先从缓存中读取数据,缓存中没有的再从数据库中读取,并且写入到缓存中,一般来说读的情况还好,但是数据库更新和缓存更新容易出现缓存和数据库一致性的问题

不管是先写数据库,再删除Redis缓存,还是先删除缓存,再写数据库,都有可能出现数据不一致的问题

1. 如果先删除缓存,还没来得及写入数据库,此时又一个线程过来请求缓存了,发现缓存是空的,就会到数据库中去请求,此时缓存中的是脏数据

2. 如果先写库,在删除缓存之前,写库的线程宕机了,没有删除缓存,则此时缓存中的数据就和数据库中的不一致

有两种方案

  • 采用延时双删策略

在写库的前后都进行redis.del(key)的操作,并且设定合理的超时时间
具体的操作步骤:

1. 先删除缓存
2. 再写数据库
3. 休眠一段时间(具体时间由业务决定,考虑读写库的时间)
4. 再次删除缓存
5. 设置缓存超时

该方案的弊端:采用上述的方案,最差的情况下在超时时间内数据是不一致的,而且增加了写请求的耗时,高并发的情况下对系统响应造成一定的影响。

  • 异步更新缓存(基于订阅binlog的同步机制)

该方法的整体的思路如下:

MySql binlog增量订阅消费+消息队列+增量数据更新到Redis

具体的流程如下:

mysql中一旦产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,
Redis再根据binlog中的记录,对Redis进行更新。其实这种机制,很类似MySQL的主从备份机制,
因为MySQL的主备也是通过binlog来实现的数据一致性。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,
而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。
  1. Redis为什么是单线程及高并发
  • redis是基于内存的,内存的读写速度非常快
  • 之所以设计为单线程,是为了减少上下文切换的时间
  • 使用多路复用技术,可以处理并发链接,非阻塞IO,内部实现epoll+自己实现的简单的时间框架,将所有的请求转化为事件,不在IO上花费时间
  1. Redis的注意事项

实际生产活动中redis的注意事项:

1. Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件
2. 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
3. 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
4. 尽量避免在压力很大的主库上增加从库
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值