Redis常见面试题汇总

Redis

接下来内容概述:

  • 安装redis6.0.8

  • redis传统五大数据类型的落地应用

  • 知道分布式锁吗?有哪些实现方案?你谈谈对redis分布式锁的理解,删key的时候有什么问题?

  • redis缓存过期淘汰策略

  • redis的LRU算法简介

     

01_redis两个小细节说明

redis基本类型

  • string

  • list

  • set

  • zset(sorted set)

  • hash

其他redis的类型

  • bitmap

  • HyperLogLogs

  • GEO

  • Stream

备注

  • 命令不区分大小写而key是区分大小写的

  • help @类型名词

02_redis传统五大类型使用场景

1_string类型使用场景

最常用

  • SET key value

  • GET key

同时设置/获取多个键值

  • MSET key value [key value…]

  • MGET key [key…]

数值增减

  • 递增数字 INCR key(可以不用预先设置key的数值。如果预先设置key但值不是数字,则会报错)

  • 增加指定的整数 INCRBY key increment

  • 递减数值 DECR key

  • 减少指定的整数 DECRBY key decrement

获取字符串长度

  • STRLEN key

分布式锁

  • SETNX key value

  • SET key value [EX seconds] [PX milliseconds] [NX|XX]

    • EX:key在多少秒之后过期

    • PX:key在多少毫秒之后过期

    • NX:当key不存在的时候,才创建key,效果等同于setnx

    • XX:当key存在的时候,覆盖key

应用场景

  • 商品编号、订单号采用INCR命令生成

  • 是否喜欢的文章

2_hash类型使用场景--Map>

Redis的Hash类型相当于Java中Map>

一次设置一个字段值 HSET key field value

一次获取一个字段值 HGET key field

一次设置多个字段值 HMSET key field value [field value …]

一次获取多个字段值 HMGET key field [field …]

获取所有字段值 HGETALL key

获取某个key内的全部数量 HLEN key

删除一个key HDEL

应用场景 - 购物车早期,当前小中厂可用

  • 新增商品 hset shopcar:uid1024 334488 1

  • 新增商品 hset shopcar:uid1024 334477 1

  • 增加商品数量 hincrby shopcar:uid1024 334477 1

  • 商品总数 hlen shopcar:uid1024

  • 全部选择 hgetall shopcar:uid1024

3_list类型使用场景--有序可重复

向列表左边添加元素 LPUSH key value [value …]

向列表右边添加元素 RPUSH key value [value …]

查看列表 LRANGE key start stop

获取列表中元素的个数 LLEN key

应用场景 - 微信文章订阅公众号

  • 大V作者李永乐老师和ICSDN发布了文章分别是11和22

  • 阳哥关注了他们两个,只要他们发布了新文章,就会安装进我的List

    • lpush likearticle:阳哥id1122

  • 查看阳哥自己的号订阅的全部文章,类似分页,下面0~10就是一次显示10条

    • lrange likearticle:阳哥id 0 10

4_set类型使用场景--无序无重复

添加元素 SADD key member [member …]

删除元素 SREM key member [member …]

获取集合中的所有元素 SMEMBERS key

判断元素是否在集合中 SISMEMBER key member

获取集合中的元素个数 SCARD key

从集合中随机弹出一个元素,元素不删除 SRANDMEMBER key [数字]

从集合中随机弹出一个元素,出一个删一个 SPOP key[数字]

集合运算

  • 集合的差集运算A - B

    • 属于A但不属于B的元素构成的集合

    • SDIFF key [key …]

  • 集合的交集运算A ∩ B

    • 属于A同时也属于B的共同拥有的元素构成的集合

    • SINTER key [key …]

  • 集合的并集运算A U B

    • 属于A或者属于B的元素合并后的集合

    • SUNION key [key …]

应用场景

  • 微信抽奖小程序

    • 用户ID,立即参与按钮

      • SADD key 用户ID

    • 显示已经有多少人参与了、上图23208人参加

      • SCARD key

    • 抽奖(从set中任意选取N个中奖人)

      • SRANDMEMBER key 2(随机抽奖2个人,元素不删除,可重复抽奖)

      • SPOP key 3(随机抽奖3个人,元素会删除,不能重复抽奖)

  • 微信朋友圈点赞

    • 新增点赞

      • sadd pub:msglD 点赞用户ID1 点赞用户ID2

    • 取消点赞

      • srem pub:msglD 点赞用户ID

    • 展现所有点赞过的用户

      • SMEMBERS pub:msglD

    • 点赞用户数统计,就是常见的点赞红色数字

      • scard pub:msgID

    • 判断某个朋友是否对楼主点赞过

      • SISMEMBER pub:msglD用户ID

  • 微博好友关注社交关系

    • 共同关注:我去到局座张召忠的微博,马上获得我和局座共同关注的人

      • sadd s1 1 2 3 4 5

      • sadd s2 3 4 5 6 7

      • SINTER s1 s2

    • 我关注的人也关注他(大家爱好相同)

  • QQ内推可能认识的人

    • sadd s1 1 2 3 4 5

    • sadd s2 3 4 5 6 7

    • SINTER s1 s2

    • SDIFF s1 s2

    • SDIFF s2 s1

5_zset类型使用场景--有序集合

向有序集合中加入一个元素和该元素的分数

添加元素 ZADD key score member [score member …]

按照元素分数从小到大的顺序返回索引从start到stop之间的所有元素 ZRANGE key start stop [WITHSCORES]

获取元素的分数 ZSCORE key member

删除元素 ZREM key member [member …]

获取指定分数范围的元素 ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

增加某个元素的分数 ZINCRBY key increment member

获取集合中元素的数量 ZCARD key

获得指定分数范围内的元素个数 ZCOUNT key min max

按照排名范围删除元素 ZREMRANGEBYRANK key start stop

获取元素的排名

  • 从小到大 ZRANK key member

  • 从大到小 ZREVRANK key member

应用场景

  • 根据商品销售对商品进行排序显示

    • 定义商品销售排行榜(sorted set集合),key为goods:sellsort,分数为商品销售数量。

      • 商品编号1001的销量是9,商品编号1002的销量是15 - zadd goods:sellsort 9 1001 15 1002

      • 有一个客户又买了2件商品1001,商品编号1001销量加2 - zincrby goods:sellsort 2 1001

      • 求商品销量前10名 - ZRANGE goods:sellsort 0 10 withscores

  • 抖音热搜

    • 点击视频

      • ZINCRBY hotvcr:20200919 1 八佰

      • ZINCRBY hotvcr:20200919 15 八佰 2 花木兰

    • 展示当日排行前10条

      • ZREVRANGE hotvcr:20200919 0 9 withscores

03_redis分布式锁前情说明

  • JVM层面的加锁,单机版的锁

  • 分布式微服务架构,拆分后各个微服务之间为了避免冲突和数据故障而加入的一种锁,分布式锁

分布式锁的实现方案:

  • mysql

  • zookeeper

  • redis 常用 redlock-----redisson lock/unlock

常见的面试题:

  • Redis除了拿来做缓存,你还见过基于Redis的什么用法?

  • Redis做分布式锁的时候有需要注意的问题?

  • 如果是Redis是单点部署的,会带来什么问题?那你准备怎么解决单点问题呢?

  • 集群模式下,比如主从模式,有没有什么问题呢?

  • 那你简单的介绍一下Redlock吧?你简历上写redisson,你谈谈。

  • Redis分布式锁如何续期?看门狗知道吗?

04_boot整合redis搭建超卖程序

使用场景:多个服务间 + 保证同一时刻内 + 同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)

建两个Module:boot_redis01,boot_redis02

  • POM


    org.springframework.boot
    spring-boot-starter-data-redis
  • application.properties

server.port=1111
#2222
​
#=========================redis相关配香========================
#Redis数据库索引(默认方0)
spring.redis.database=0
#Redis服务器地址
spring.redis.host=127.0.0.1
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
spring.redis.password=123456
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默犬认0
spring.redis.lettuce.pool.min-idle=0
  • RedisConfig

@Configuration
public class RedisConfig {
​
    @Bean
    public RedisTemplate redisTemplate(LettuceConnectionFactory connectionFactory){
        RedisTemplate redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }
​
}
  • GoodController

@RestController
public class GoodController{
​
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
​
    @Value("${server.port}")
    private String serverPort;
​
​
    @GetMapping("/buy_goods")
    public String buy_Goods(){
​
        String result = stringRedisTemplate.opsForValue().get("goods:001");// get key ====看看库存的数量够不够
​
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
​
        if(goodsNumber > 0){
​
            int realNumber = goodsNumber - 1;
​
            stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber));
​
            System.out.println("成功买到商品,库存还剩下: "+ realNumber + " 件" + "\t服务提供端口" + serverPort);
​
            return "成功买到商品,库存还剩下:" + realNumber + " 件" + "\t服务提供端口" + serverPort;
​
        }else{
​
            System.out.println("商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort);
        }
​
        return "商品已经售完/活动结束/调用超时,欢迎下次光临" + "\t服务提供端口" + serverPort;
    }
​
}

测试

  • redis:set goods:001 100

  • 浏览器:http://localhost:1111/buy_goods

 

05_redis分布式锁

没有加锁,并发下数字不对,出现超卖现象

1_JVM层面的加锁,单机版的锁

  • synchronized

  • ReentraLock

class X {
    private final ReentrantLock lock = new ReentrantLock();
    // ...
​
    public void m() {
        lock.lock();  // block until condition holds//不见不散
        try {
            // ... method body
        } finally {
            lock.unlock()
        }
    }
     
     
    public void m2() {
​
        if(lock.tryLock(timeout, unit)){//过时不候
            try {
            // ... method body
            } finally {
                lock.unlock()
            }   
        }else{
            // perform alternative actions
        }
   }
 }
​

2_分布式部署后,单机锁还是出现超卖现象,需要分布式锁

使用Nginx配置负载均衡

img

Nginx配置文件修改内容:

upstream myserver{
    server 127.0.0.1:1111 weight=1;
    server 127.0.0.1:2222 weight=1;
}
​
server {
    listen       80;
    server_name  localhost;
​
    #charset koi8-r;
​
    #access_log  logs/host.access.log  main;
​
    location / {
        # 负责用到的配置
        proxy_pass  http://myserver;
        root   html;
        index  index.html index.htm;
    }
​
    #error_page  404              /404.html;
​
    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}
​

启动两个微服务:1111,2222,多次访问http://localhost/buy_goods,服务提供端口在1111,2222两者之间横跳

上面手点,下面高并发模拟

redis:set goods:001 100,恢复到100

用到Apache JMeter,100个线程同时访问http://localhost/buy_goods。 img

img

启动测试,后台打印如下:01和02两个项目都同时买一送一,单机锁还是无法解决超卖现象。

Redis常见面试题汇总_第1张图片Redis常见面试题汇总_第2张图片

01和02两个项目中有重复的订单号。

 

这就是所谓分布式部署后出现超卖现象。

Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理。

SET-----SETNX

  • EX seconds – Set the specified expire time, in seconds.

  • PX milliseconds – Set the specified expire time, in milliseconds.

  • NX – Only set the key if it does not already exist.

  • XX – Only set the key if it already exist.

GoodController

public static final String REDIS_LOCK = "redis_lock";
​
@Autowired
private StringRedisTemplate stringRedisTemplate;
​
public void m(){
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
​
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);  //SETNX加锁
​
​
    if(!flag) {
        return "抢锁失败";
    }
 
    ...//业务逻辑
    
    stringRedisTemplate.delete(REDIS_LOCK);   //解锁
}
​

3_上述分布式锁带来的几个问题

问题一出现异常的话,可能无法释放锁,必须要在代码层面finally释放锁。使用try--finally释放锁

public static final String REDIS_LOCK = "redis_lock";
​
@Autowired
private StringRedisTemplate stringRedisTemplate;
​
public void m(){
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
​
    try{
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
​
        if(!flag) {
            return "抢锁失败";
        }
        
        ...//业务逻辑
            
    }finally{
        stringRedisTemplate.delete(REDIS_LOCK);   
    }
}
​

问题二部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key。

public static final String REDIS_LOCK = "redis_lock";

@Autowired
private StringRedisTemplate stringRedisTemplate;

public void m(){
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

    try{
		Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
		//设定时间
        stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS);
        
   		if(!flag) {
        	return "抢锁失败";
	    }
        
    	...//业务逻辑
            
    }finally{
	    stringRedisTemplate.delete(REDIS_LOCK);   
    }
}

问题三:设置key(加锁)+设置过期时间分开了,必须要合并成一行具备原子性。

 //使用另一个带有设置超时操作的方法
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);

问题四:张冠李戴,删除了别人的锁。A线程执行到一半的时候,A的锁过期了被自动删除了,B线程进来加锁,然后执行命令,此时A线程做完了任务,走到最后一步把B线程的锁给删除了。

img

 // 解决方法:只能自己删除自己的,不许动别人的。
if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equals(value)){
    stringRedisTemplate.delete(REDIS_LOCK);  //解锁
}

!!问题五:finally块的判断 + del删除操作不是原子性的

解决方案:

  • 用lua脚本

  • 用redis自身的事务

事务介绍

  • Redis的事条是通过MULTI,EXEC,DISCARD和WATCH这四个命令来完成。

  • Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合

  • Redis将命令集合序列化并确保处于一事务的命令集合连续且不被打断的执行。

  • Redis不支持回滚的操作。

命令 描述
DISCARD 取消事务,放弃执行事务块内的所有命令。
EXEC 执行所有事务块内的命令。
MULTI 标记一个事务块的开始。
UNWATCH 取消 WATCH 命令对所有 key 的监视。
WATCH key [key …] 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
finally {

    while (true){

        stringRedisTemplate.watch(REDIS_LOCK);

        if (stringRedisTemplate.opsForValue().get(REDIS_LOCK).equals(value)) {

            stringRedisTemplate.setEnableTransactionSupport(true);  //开启事务
            stringRedisTemplate.multi(); //表示事务的开始
            stringRedisTemplate.delete(REDIS_LOCK);  //解锁
            List list = stringRedisTemplate.exec();  //执行事务
            if (list == null) {
                continue;
            }
        }

        stringRedisTemplate.unwatch();
        break;

    }

} 
  
  • Redis调用Lua脚本通过eval命令保证代码执行的原子性

finally {

    // lua脚本
    Jedis jedis = RedisUtils.getJedis();
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] "
        + "then "
        + "    return redis.call('del', KEYS[1]) "
        + "else "
        + "    return 0 "
        + "end";
    try {
        Object o = jedis.eval(script, Collections.singletonList(REDIS_LOCK),//
                              Collections.singletonList(value));

        if("1".equals(o.toString())) {
            System.out.println("---del redis lock ok.");
        }else {
            System.out.println("---del redis lock error.");
        }

    }finally {
        if(jedis != null)
            jedis.close();
    }
}

!!问题六:确保RedisLock过期时间大于业务执行时间的问题-----Redis分布式锁如何续期?--Redisson

  • redis单机是CP,集群是AP

集群 + CAP对比ZooKeeper 对比ZooKeeper,重点,CAP

  • Redis - AP -redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。redis的matser一拿到锁就返回说拿到了锁,如果此时从机还没有来得及复制锁,就将matser断开了,再用哨兵模式选了新的matser,则锁就丢失了。效率高。

  • ZooKeeper - CP zookeeper是当master得到锁了,会通知自己的从机先复制好锁,然后再返回说拿到锁了 。可靠性高。

CAP

  • C:Consistency(强一致性)

  • A:Availability(可用性)

  • P:Partition tolerance(分区容错性)

小总结

Redis集群环境下,我们自己写的也不OK直接上RedLock之Redisson落地实现

Redisson POM



    org.redisson
    redisson
    3.13.4

RedisConfig

@Bean
public Redisson redisson() {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);  // 单机的redisson配置
    return (Redisson)Redisson.create(config);
}

Controller

public static final String REDIS_LOCK = "REDIS_LOCK";

@Autowired
private Redisson redisson;

@GetMapping("/doSomething")
public String doSomething(){

    RLock redissonLock = redisson.getLock(REDIS_LOCK);
    redissonLock.lock();
    try {
        //doSomething
    }finally {
        redissonLock.unlock();
    }
}

编写判断,让代码更严谨 Controller

public static final String REDIS_LOCK = "REDIS_LOCK";

@Autowired
private Redisson redisson;

@GetMapping("/doSomething")
public String doSomething(){

    RLock redissonLock = redisson.getLock(REDIS_LOCK);
    redissonLock.lock();
    try {
        //doSomething
    }finally {
    	//添加后,更保险
		if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
    		redissonLock.unlock();
    	}
    }
}

可避免如下异常:

IllegalMonitorStateException: attempt to unlock lock,not loked by current thread by node id:da6385f-81a5-4e6c-b8c0

redisson 如何实现过期时间自动续期?

客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

简单!只要客户端1一旦加锁成功,就会启动一个定时任务,即watch dog看门狗他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

通过源码分析我们知道,默认情况下,加锁的时间是30秒.如果加锁的业务没有执行完,那么到 30-10 = 20秒的时候,就会进行一次续期,把锁重置成30秒.那这个时候可能又有同学问了,那业务的机器万一宕机了呢?宕机了定时任务跑不了,就续不了期,那自然30秒之后锁就解开了呗.

redisson 锁的原理

在Redisson中,使用key来作为是否上锁的标志,当通过getLock(String key)方法获得相应的锁之后,这个key即作为一个锁存储到Redis集群中。

在接下来如果有其他的线程尝试获取名为key的锁时,便会向集群中进行查询,如果能够查到这个锁并发现相应的value的值不为0,则表示已经有其他线程申请了这个锁同时还没有释放,则当前线程进入阻塞,否则由当前线程获取这个锁并将value值加一。

如果是可重入锁的话,则当前线程每获得一个自身线程的锁,就将value的值加一,而每释放一个锁则将value值减一,直到减至0,完全释放这个锁。因为底层是基于分布式的Redis集群,所以Redisson实现了分布式的锁机制。

 

4_redis分布式锁总结回顾

synchronized单机版oK,上分布式

nginx分布式微服务单机锁不行

取消单机锁,上Redis分布式锁setnx

只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁

宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定

为redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间必须同一行

必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3

Redis集群环境下,我们自己写的也不oK直接上RedLock之Redisson落地实现

06_redis内存调整默认查看

一些面试题:

  • 生产上你们你们的redis内存设置多少?

  • 如何配置、修改redis的内存大小

  • 如果内存满了你怎么办?

  • redis清理内存的方式?定期删除和惰性删除了解过吗

  • redis缓存淘汰策略

  • redis的LRU了解过吗?可否手写一个LRU算法

  • Redis内存满了怎么办?Redis默认内存多少?在哪里查看?如何设置修改?

查看Redis最大占用内存

配置文件redis.conf的maxmemory参数,maxmemory是bytes字节类型,注意转换。

redis默认内存多少可以用?

如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存

一般生产上你如何配置?

一般推荐Redis设置内存为最大物理内存的四分之三

如何修改redis内存设置

  • 修改配置文件redis.conf的maxmemory参数,如:maxmemory 104857600 --100M,1kb = 1024bytes

  • 通过命令修改

    • config set maxmemory 1024

    • config get maxmemory

什么命令查看redis内存使用情况?

info memory

 

07_redis打满内存OOM

真要打满了会怎么样?如果Redis内存使用超出了设置的最大值会怎样?

我改改配置,故意把最大值设为1个

127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "0"
127.0.0.1:6379> config set maxmemory 1
OK
127.0.0.1:6379> set a 123
(error) OOM command not allowed when used memory > 'maxmemory'.

没有加上过期时间就会导致数据写满maxmemory为了避免类似情况,引出下一节内存淘汰策略

 

08_redis内存淘汰策略

往redis里写的数据是怎么没了的?

redis过期键的删除策略

如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被被删除呢?

如果回答yes,你自己走还是面试官送你?

如果不是,那过期后到底什么时候被删除呢??是个什么操作?

三种不同的删除策略

  • 定时删除 - 总结:对CPU不友好,用处理器性能换取存储空间(拿时间换空间)

  • 惰性删除 - 总结:对memory不友好,用存储空间换取处理器性能(拿空间换时间)

  • 上面两种方案都走极端 - 定期删除 - 定期抽样key,判断是否过期(存在漏网之鱼)

定时删除

Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。

立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死。

这会产生大量的性能消耗,同时也会影响数据的读取操作。

惰性删除

数据到达过期时间,不做处理。等下次访问该数据时,

如果未过期,返回数据;

发现已过期,删除,返回不存在。

惰性删除策略的缺点是,它对内存是最不友好的。

如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放。

在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏 – 无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息。

定期删除每100ms随机抽取一些key进行检查。 定期删除策略是前两种策略的折中:

定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。

周期性轮询Redis库中的时效性数据,来用随机抽取的策略,利用过期数据占比的方式控制删除频度

特点1:CPU性能占用设置有峰值,检测频度可自定义设置

特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理

总结:周期性抽查存储空间(随机抽查,重点抽查

redis默认每个100ms检查,是否有过期的key,有过期key则删除。注意:redis不是每隔100ms将所有的key检查一次而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis直接进去ICU)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。

定期删除策略的难点是确定删除操作执行的时长和频率:如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将CPU时间过多地消耗在删除过期键上面。如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除束略一样,出现浪费内存的情况。因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。

上述步骤都过堂了,还有漏洞吗?

  1. 定期删除时,从来没有被抽查到

  2. 惰性删除时,也从来没有被点中使用过

上述2步骤====>大量过期的key堆积在内存中,导致redis内存空间紧张或者很快耗尽

必须要有一个更好的兜底方案

内存淘汰策略登场(Redis 6.0.8版本)8种策略

  • noeviction:不会驱逐任何key

  • volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除

  • volatile-Iru:对所有设置了过期时间的key使用LRU算法进行删除

  • volatile-random:对所有设置了过期时间的key随机删除

  • volatile-ttl:删除马上要过期的key

  • allkeys-lfu:对所有key使用LFU算法进行删除

  • allkeys-Iru:对所有key使用LRU算法进行删除

  • allkeys-random:对所有key随机删除

上面总结

  • 2*4得8

  • 2个维度

    • 过期键中筛选

    • 所有键中筛选

  • 4个方面

    • LRU 最近最少使用

    • LFU 最近频率上最少使用

    • random

    • ttl(Time To Live)

  • 8个选项

如何配置,修改

  • 命令

    • config set maxmemory-policy noeviction

    • config get maxmemory

  • 配置文件 - 配置文件redis.conf的maxmemory-policy参数

 

09_lru算法设计思想

lru是什么

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。

leetcode 146 LRU缓存机制:

https://leetcode-cn.com/problems/lru-cache/?utm_source=LCUS&utm_medium=ip_redirect&utm_campaign=transfer2china

设计思想

  • 所谓缓存,必须要有读+写两个操作,按照命中率的思路考虑,写操作+读操作时间复杂度都需要为O(1)

  • 特性要求

    • 必须要有顺序之分,一区分最近使用的和很久没有使用的数据排序。

    • 写和读操作一次搞定。

    • 如果容量(坑位)满了要删除最不长用的数据,每次新访问还要把新的数据插入到队头(按照业务你自己设定左右那一边是队头) 查找快、插入快、删除快,且还需要先后排序---------->什么样的数据结构可以满足这个问题?

你是否可以在O(1)时间复杂度内完成这两种操作?如果一次就可以找到,你觉得什么数据结构最合适?

答案:LRU的算法核心是哈希链表

编码手写如何实现LRU

本质就是HashMap + DoubleLinkedList

时间复杂度是O(1),哈希表+双向链表的结合体

解题思路1:巧用LinkedHashMap完成lru算法

class LRUCache extends LinkedHashMap{

    private int capacity;  //缓存坑位

    public LRUCache(int capacity) {
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > capacity;
    }

}

解题思路2:lru算法

Redis常见面试题汇总_第3张图片

 

添加到头示意图

Redis常见面试题汇总_第4张图片

 

public class LRUCacheDemo02 {

    // map负责查找,构建一个虚拟的双向链表,里面安装的就是一个Node节点,作为数据载体

    //1 构造一个Node节点作为数据载体
    class Node{

        K key;
        V value;
        Node prev;
        Node next;

        public Node() {
            this.prev = this.next = null;
        }
        public Node(K key, V value) {
            super();
            this.key = key;
            this.value = value;
        }
    }

    //2 构建一个虚拟的双向链表,里面安放的就是我们的Node,负责增加和删除
    class DoublyLinkedList{

        Node head;
        Node tail;

        //2.1 构造方法
        public DoublyLinkedList() {
            //头尾哨兵节点
            this.head = new Node();
            this.tail = new Node();
            this.head.next = this.tail;
            this.tail.prev = this.head;
        }

        //2.2 添加到头
        public void addHead(Node node){
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;
        }

        //2.3 删除节点
        public void removeNode(Node node){
            node.next.prev = node.prev;
            node.prev.next = node.next;
            node.prev = null;
            node.next = null;
        }

        //2.4 获取最后一个节点
        public Node getLast(){

            if (tail.prev == head){
                return null;
            }
            return tail.prev;
        }
    }


    private int cacheSize;
    private Map> map;
    private DoublyLinkedList doublyLinkedList;


    public LRUCacheDemo02(int cacheSize) {
        this.cacheSize = cacheSize;
        map = new HashMap<>();
        doublyLinkedList = new DoublyLinkedList<>();
    }

    public int get(int key) {

        if (!map.containsKey(key)){
            return -1;
        }

        Node node = map.get(key);

        // 此时需要进行移动该节点的位置
        doublyLinkedList.removeNode(node);
        doublyLinkedList.addHead(node);

        return node.value;

    }
    public void put(int key,int value) {

        if (map.containsKey(key)){

            Node node = map.get(key);
            node.value = value;
            map.put(key, node);

            doublyLinkedList.removeNode(node);
            doublyLinkedList.addHead(node);
        }else {

            if (map.size() == cacheSize){  //已达到最大容量了,把旧的移除,让新的进来

                Node lastNode = doublyLinkedList.getLast();
                map.remove(lastNode.key);  //node.key主要用处,反向连接map
                doublyLinkedList.removeNode(lastNode);
            }

            // 此时才是新增
            Node newNode = new Node<>(key, value);
            map.put(key,newNode);
            doublyLinkedList.addHead(newNode);
        }
    }

 

你可能感兴趣的:(java面试题,redis,分布式,缓存)