备战秋招,Redis面试题万字详解

备战秋招,Redis面试题万字详解

本篇旨在讲清楚redis在面试常问的面试题比如:缓存击穿、缓存雪崩、缓存穿透、布隆过滤器等等经典面试题,将他们的来龙去脉尽我所能介绍得一清二楚!开车!

缓存架构

首先网站为什么需要缓存,相信这不用过多赘述;那么缓存在网站架构中又是如何设计的呢?

如今用户访问一个大型网站首先接触的就是:nginx;然后才是我们的web层,所以许多大型的网站也会采用nginx做缓存设计,主要缓存静态页面;本地缓存等;然后到我们的web层,为了避免大规模的数据库查询操作,自然就选用redis做数据缓存;好的工具也应该有好的调教;不然会暴露出许多细节上的问题导致网站面临许多压力;

缓存穿透

  • 首先介绍缓存穿透:我们都知道redis是缓存数据,但是如果redis里面没有数据,可巧的是又有一大批请求冲进来;这岂不是全往数据库走?

  • 缓存没命中的原因:有人可能会说,那有那么巧的时,怎么可能大批请求都去数据库,几十毫秒查出数据放到reddis缓存不就完事了?--------道理确实是这个道理;但如果是恶意攻击呢,我发现了网站一些请求规则:比如携带参数就可以访问,那我岂不是可以伪造大量不存在的参数,一直发起请求,线程去数据库查也查不到;那就redis也放不了,受伤的还是数据库;

  • 解决办法:不卖关子,解决办法很简单,往redis放空数据,这样就可以走缓存了;可是数据量空对象多了也占空间;最佳的还属于布隆过滤器

    • 布隆过滤器:对于恶意攻击;可以用过滤器做一次过滤;对于不存在的数据一般都能过滤掉;阻止请求发送到后端造成压力;布隆过滤器说不存在的数据一定不存在;说存在的可能不存在;
    • 实现原理

    由图可见:布隆过滤器的构成是一个1、0的位数组;通过位上的标记信息就可判断状态;比方说往过滤器中添加一个key,该key会通过几种hash算法算出多个位置;将这些位置的信息都置为1;代表key的存在;

    当线程请求key时,就会将该key做hash判断所有位置是不是都为1;如果是就说明存在,就可以进行后端调用;否则直接阻拦在外;

    很容易就会发现:为什么布隆过滤器说不存在的数据肯定不存在,存在的也有可能不存在了,为了尽量避免数据的hash冲突,这就需要更多的位;位越多key的hash值越散裂;那么占用的空间会不会变得多起来呢?----当然不会;一个字节八位;一MB就有1024 * 1024 * 8 = 8388608个位信息;更合况谁又舍不得拿个十几MB来提高系统安全性呢?你愿意吗?反正我愿意;

    布隆过滤器的实现很简单,感兴趣可以搜一下;

缓存失效

也叫缓存击穿,但是容易和缓存穿透混淆,所以我更愿意叫这个名字;

见名知意;失效嘛,我们通常都会给key设置过期时间;这明显是缓存过期了导致的问题;

可是缓存失效会导致啥问题?过期不是我们自己设置的嘛;失效了不是很正常,

导致的问题:缓存失效了请求就会伤害都我们胆小怕事 的数据库;一两个失效了到没啥事;可是如果是定时的一大批失效了呢,那请求起步就多了来;

  • 场景:比方一个商品购物网站,我们通常会调用定时任务将批量商品放到缓存并设置过期时间;那这就会同一时刻一大批量key同时失效,此刻访问这些商品的请求就会都冲进来,打垮数据库;

  • 解决办法:很简单;不让他们同时失效就行了;也就是过期时间能一致,需要有一些偏差;那么在设置过期时间的时候加一个随机数时间就能实现;伪代码如下:

String get(String key) {
    // 从缓存中获取数据
    String cacheValue = cache.get(key);
    // 缓存为空
    if (StringUtils.isBlank(cacheValue)) {
        // 从存储中获取
        String storageValue = storage.get(key);
        cache.set(key, storageValue);
        //设置一个过期时间(300到600之间的一个随机数)
        int expireTime = new Random().nextInt(300)  + 300;
        if (storageValue == null) {
            cache.expire(key, expireTime);
        }
        return storageValue;
    } else {
        // 缓存非空
        return cacheValue;
    }
}

缓存失效就到此为止,解决办法也很简单;接下来看缓存雪崩;

缓存雪崩

雪崩的时候没有一片雪花是无辜的;雪崩指的就是缓存层扛不住,redis挂了导致的雪崩

缓存层挂了之后,一大批请求如泄洪般冲进后端,对数据库拳打脚踢;这谁顶得住;当然会挂;缓存 挂的原因可能是因为并发加剧,也有可能是缓存设计失误;类似大量请求访问bigkey,导致缓存能支持的并发下降;知道了问题出现的原因;那我们该如何来解决它?

无非从这几个方面入手:增加缓存层的可用性,让它不会轻易挂掉;限制请求的数量,控制并发

  • 保证缓存的可用性:那就用集群呗,一个挂了另一个顶上;redis cluster, 哨兵;
  • 限流:这个就需要依赖三方组件了,比如springCloudAlibaba中很好用的sentinel组件;
  • 熔断&降级:降级,也就是牺牲局部,保全整体;弃车保帅;用户访问直接返回预准备的接口返回消息如:系统繁忙;比如淘宝双十一九关闭了评论等非核心业务功能达到一个降级的效果; 而服务熔断与之不同的是他能正常访问,如果调用响应时间过长才会降级返回提示;熔断是因为调用链的一部分奔溃导致整体阻塞,开启熔断后效果如降级一般,
  • 提前演练:项目上线前,演示缓存层宕机后端会出现什么情况;做一些预定解决方案。

热点key重建

  • 说的是一个比较热点的key在开始设定了过期时间,在某一刻失效过后一大堆请求进来重建key的行为;恰好这个key的重建比较复杂,比如包含复杂的计算;sql等拖慢时间;这样大量线程就会竞争资源;造成后端压力突然剧增;

解决办法就是让重建行为不要发生并发行为;那就需要加锁;加什么锁呢?redis的互斥锁;演示代码如下

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

完美解决; 接下来来看一个双写不一致问题

双写不一致

见名知意;所谓双写不一致,说的就是redis中的数据与数据库中的数据不同步;这确实是一个比较难处理的问题;因为确实不好规避;下面就来介绍几种解决办法:

  • 对于并发量较小的数据,我们往往只需要保证他的最终一致性就好了;所以给key设计过期时间就可以解决;就像我博客中用到的博客key;不常修改,只要最终是一致的就行;另外还有一些不一致但又对主业务没多大影响的数据;比如商品的名字和分类之类的,段时间内不一致也没啥影响;设置过期时间就可以解决;
  • 如果强烈要求一致性的话,那就可以使用分布式读写锁来实现;都是读的时候互不影响;设计到修改数据的时候就需要排队执行;这样只能牺牲性能了
  • 引入中间件:阿里开源的canal监听数据库binlog日志文件,及时修改缓存;

总结:放入缓存的应该是一些针对实时性、一致性要求不高的数据;要求高的话就没必要使用缓存了,只是为了增加系统复杂度而已;

BigKey问题

  • 啥是bigkey?

bigkey指的是key对应的Value占用空间过大,导致IO慢,占用网络宽带;

redis中一个字符串最大是512MB;但是一般过10kb都认为是Bigkey了;而对于其他数据结构:list\hash\set等;判断bigkey的依据则是根据他们的元素个数来看的;元素不能超过5000个;

  • bigkey危害

慢;导致redis阻塞;数据走网络通道时导致网络阻塞;

过期删除,bigkey的删除是个耗时的操作;着就会导致其他命令阻塞无法执行;当然这个问题可以使用redis4.0的过期异步删除,

  • 优化bigkey:

使用合适的数据结构存储数据;不要造成资源的浪费;

控制key的过期时间,不要把redis当成垃圾桶;

  • bigkey删除

使用异步方式删除;渐进式删除hscan、sscan、zscan方式;例如一个500万个元素的list;

客户端使用优化

  • 避免多个应用使用一个redis实例;将不相干的业务拆开,公共数据做服务化;
  • 使用连接池:
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(5);
jedisPoolConfig.setMaxIdle(2);
jedisPoolConfig.setTestOnBorrow(true);

JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.0.60", 6379, 3000, null);

Jedis jedis = null;
try {
    jedis = jedisPool.getResource();
    //具体的命令
    jedis.executeCommand()
} catch (Exception e) {
    logger.error("op key {} error: " + e.getMessage(), key, e);
} finally {
    //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
    if (jedis != null) 
        jedis.close();
}

参数表:

  • maxTotal:最大连接数: 需要考虑的因素有

    • 业务希望redis的并发量
    • 客户端执行命令时间
    • redis资源:应用个数 * maxtotal不能超过最大连接数maxclients
    • 资源开销:希望控制空间连接,但是不希望连接吃频繁创建销毁这些不必要的开销;

    举个例子:

    • 一次命令时间(borrow|return resource + Jedis执行命令(含网络) )的平均耗时约为1ms,一个连接的QPS大约是1000
    • 业务期望的QPS是50000

    那么理论上需要的资源池大小是50000 / 1000 = 50个。但事实上这是个理论值,还要考虑到要比理论值预留一些资源,通常来讲maxTotal可以比理论值大一些。

    但这个值不是越大越好,一方面连接太多占用客户端和服务端资源,另一方面对于Redis这种高QPS的服务器,一个大命令的阻塞即使设置再大资源池仍然会无济于事。

  • maxIdle和minIdle

    maxIdle实际上才是业务需要的最大连接数,maxTotal是为了给出余量,所以maxIdle不要设置过小,否则会有new Jedis(新连接)开销。

    连接池的最佳性能是maxTotal = maxIdle,这样就避免连接池伸缩带来的性能干扰。但是如果并发量不大或者maxTotal设置过高,会导致不必要的连接资源浪费。一般推荐maxIdle可以设置为按上面的业务期望QPS计算出来的理论连接数,maxTotal可以再放大一倍。

    minIdle(最小空闲连接数),与其说是最小空闲连接数,不如说是"至少需要保持的空闲连接数",在使用连接的过程中,如果连接数超过了minIdle,那么继续建立连接,如果超过了maxIdle,当超过的连接执行完业务后会慢慢被移出连接池释放掉。

    如果系统启动完马上就会有很多的请求过来,那么可以给redis连接池做预热,比如快速的创建一些redis连接,执行简单命令,类似ping(),快速的将连接池里的空闲连接提升到minIdle的数量。

    List<Jedis> minIdleJedisList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());
    
    for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
        Jedis jedis = null;
        try {
            jedis = pool.getResource();
            minIdleJedisList.add(jedis);
            jedis.ping();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        } finally {
            //注意,这里不能马上close将连接还回连接池,否则最后连接池里只会建立1个连接。。
            //jedis.close();
        }
    }
    //统一将预热的连接还回连接池
    for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
        Jedis jedis = null;
        try {
            jedis = minIdleJedisList.get(i);
            //将连接归还回连接池
            jedis.close();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        } finally {
        }
    }
    

Key的清除

一般有三种键清除的方式;

  1. 惰性删除:当触发读写一个过期的key时,就会删除该key;
  2. 主动删除:redis每隔100ms触发一次扫描;将部分过期的key清除,注意是部分清除
  3. 当前内存超出预警maxmemory;触发主动清除;

主动清理策略在Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略,总共8种:

  • 针对过期key

    • volatile-ttl:针对设置了过期时间的键值对,根据过期时间先后删除;
    • volatile-random:随机删除过期时间的键值对;
    • volatile-lfu:使用LFU算法清除
    • volatile-lru:使用LRU算法清除
  • 针对所有key处理:

    • allkeys-random:不用解释
    • -lru:对所有数据进行LRU
    • -LFU:~~~~
  • 不处理:noeviction:不删除任何数据;拒绝所有写操作并且返回错误信息;同时只响应读操作;

**LRU **:last Recently Use 最近最少使用;淘汰很久没有使用的数据

LFU:last Frequently Use 最不经常使用 ;淘汰最近访问次数最少的数据

当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。这时使用LFU可能更好点。

根据自身业务类型,配置好maxmemory-policy(默认是noeviction),推荐使用volatile-lru。如果不设置最大内存,当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap),会让 Redis 的性能急剧下降。

当Redis运行在主从模式时,只有主结点才会执行过期删除策略,然后把删除操作”del key”同步到从结点删除数据。

进阶技巧优化

  • 系统内核参数优化

    vm.swapiness

    swap对于操作系统来说比较重要,当物理内存不足时,可以将一部分内存页进行swap到硬盘上,以解燃眉之急。但世界上没有免费午餐,swap空间由硬盘提供,对于需要高并发、高吞吐的应用来说,磁盘IO通常会成为系统瓶颈。在Linux中,并不是要等到所有物理内存都使用完才会使用到swap,系统参数swppiness会决定操作系统使用swap的倾向程度。swappiness的取值范围是0~100,swappiness的值越大,说明操作系统可能使用swap的概率越高,swappiness值越低,表示操作系统更加倾向于使用物理内存。swappiness的取值越大,说明操作系统可能使用swap的概率越高,越低则越倾向于使用物理内存。

    如果linux内核版本<3.5,那么swapiness设置为0,这样系统宁愿swap也不会oom killer(杀掉进程)

    如果linux内核版本>=3.5,那么swapiness设置为1,这样系统宁愿swap也不会oom killer

    一般需要保证redis不会被kill掉:

    cat /proc/version  #查看linux内核版本 echo 1 > /proc/sys/vm/swappiness echo vm.swapiness=1 >> /etc/sysctl.conf              
    

    PS:OOM killer 机制是指Linux操作系统发现可用内存不足时,强制杀死一些用户进程(非内核进程),来保证系统有足够的可用内存进行分配。

  • vm.overcommit_memory(默认0)

    0:表示内核将检查是否有足够的可用物理内存(实际不一定用满)供应用进程使用;如果有足够的可用物理内存,内存申请允许;否则,内存申请失败,并把错误返回给应用进程

    1:表示内核允许分配所有的物理内存,而不管当前的内存状态如何

    如果是0的话,可能导致类似fork等操作执行失败,申请不到足够的内存空间

    Redis建议把这个值设置为1,就是为了让fork操作能够在低内存下也执行成功。

    cat /proc/sys/vm/overcommit_memory 
    echo "vm.overcommit_memory=1" >> /etc/sysctl.conf 
    sysctl vm.overcommit_memory=1        
    
  • 慢日志查询

Redis慢日志命令说明:
config get slow* #查询有关慢日志的配置信息
config set slowlog-log-slower-than 20000  #设置慢日志使时间阈值,单位微秒,此处为20毫秒,即超过20毫秒的操作都会记录下来,生产环境建议设置1000,也就是1ms,这样理论上redis并发至少达到1000,如果要求单机并发达到1万以上,这个值可以设置为100
config set slowlog-max-len 1024  #设置慢日志记录保存数量,如果保存数量已满,会删除最早的记录,最新的记录追加进来。记录慢查询日志时Redis会对长命令做截断操作,并不会占用大量内存,建议设置稍大些,防止丢失日志
config rewrite #将服务器当前所使用的配置保存到redis.conf
slowlog len #获取慢查询日志列表的当前长度
slowlog get 5 #获取最新的5条慢查询日志。慢查询日志由四个属性组成:标识ID,发生时间戳,命令耗时,执行命令和参数
slowlog reset #重置慢查询日志
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值