资讯详情

面试 Redis 没底?这 40 道面试题让你不再慌(附答案)

大厂面试!我和面试官有关系Redis一场游戏!

今天,我不自量力地采访了一家大工厂 Java 开发岗位,一个尘土飞扬的中年男子迎面而来,手里拿着屏幕还亮着。 Mac。他礼貌地对我微笑,然后说:对不起,让你等很长时间,然后示意我坐下说:让我们开始,看看你的简历,认为你是对的 Redis 要把握好,今天就来讨论一下 Redis……”。我想:来就来,兵来将挡水来土掩。

Redis 是什么

面试官:先说说 Redis 是什么吧! 我:(这不是总结 Redis 的定义和特点嘛)Redis 是 C 语言开发的开源(遵从 BSD 协议)高性能键值对(key-value)内存数据库库、缓存、消息中间件等内存数据库。 NoSQL(Not only SQL,一般指非关系数据库)的数据库。 我顿了一下,然后说,Redis 内存数据库:

  • 性能优异,数据在内存中读写速度快,支持并发 10W QPS。
  • 单过程单线程安全,采用 IO 多路复用机制。
  • 数据类型丰富,支持字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
  • 支持数据持久化,可在磁盘中保存内存中的数据,重启时加载。
  • 复制,哨兵,高可用性。
  • 可用作分布式
  • 可作为消息中间件使用,支持订阅发布。

Redis 介绍五种数据类型

面试官 : 总结得很好,似乎已经准备好了。刚才听你提到的 Redis 支持五种数据类型,能简单说说这五种数据类型吗? 我: 当然,但在说之前,我认为有必要先了解一下 Redis 如何描述内部内存管理? 5 数据类型。说着,我拿着笔给面试官画了一张图:

首先 Redis 内部使用一个 RedisObject 对象表示一切 key 和 value。RedisObject 如上图所示:type 表示一个 value 对象的具体数据类型是什么?encoding 不同类型的数据 Redis 内部存储模式。比如:type=string 表示 value 存储普通字符串,所以 encoding 可以是 raw 或者 int。 我顿了一会儿,然后说,下面我简单说一下 5 数据类型:

String

介绍: String 是 Redis 最基本的类型可以理解为和 memcached 一模一样的类型,一 key 对应一个 value。value 不仅是 string,也可以是数字。string 类型为二进制安全,意思是 Redis 的 string 类型可以包含任何数据,例如 jpg 图片或序列对象。string 最大可存储类型值 512M。 常用命令:set、get、decr、incr、mget 等。 应用场景:常规 key-value 缓存应用;常规计数:微博数,粉丝数等。

Hash

介绍 :Hash 是一个键值(key-value)的集合。Redis 的 hash 是一个 string 的 key 和 value 的映射表,Hash 特别适合存储对象。 常用命令 :hget、hset、hgetall 等。 应用场景 :hash 特别适合存储对象。在后续操作中,您可以直接修改对象中某个字段的值。例如,我们可以 hash 存储用户信息、商品信息等数据结构。例如,我将在下面使用它。 hash 类型存储了我自己的一些信息:

key=JavaUser293847 value={ “id”: 1, “name”: “SnailClimb”, “age”: 24, “location”: “Wuhan, Hubei” }  

List

介绍:List 列表是一个简单的字符串列表,按插入顺序排序。将一个元素添加到列表的头部(左)或尾部(右)。 常用命令:lpush、rpush、lpop、rpop、lrange(获取列表片段)等。 应用场景:List 有很多应用场景,也是 Redis 最重要的数据结构之一,比如 Twitter 可以使用关注列表和粉丝列表 list 实现结构。 List 是链表,可作为消息队列使用。Redis 提供了 List 的 push 和 pop 操作还提供操作某一段 api,可直接查询或删除某段元素。redis list 实现是一个双向链表,可以支持反向搜索和遍历,操作更方便,但会带来额外的内存费用。

Set

介绍:Set 是 string 类型的无序集合。集合是通过 hashtable 实现的。set 元素中没有顺序,也没有重复。 常用命令: sdd、spop、smembers、sunion 等。 应用场景 :redis set 外部功能和 list 同样是列表,特别之处在于 set 而且自动去重 set 判断一个成员是否在一个成员 set 集合中。

Zset

介绍 :zset 和 set 一样是 string 不允许重复类型元素的集合。 常用命令: zadd、zrange、zrem、zcard 等。 使用场景:sorted set 用户可以提供额外的优先级(score)参数为成员排序,插入有序,即自动排序。当您需要一个有序且不重复的集合列表时,可以选择 sorted set 结构。和 set 相比,sorted set 关联了一个 double 类型权重参数 score,使集合元素能够按照 score 有序排列,Redis 正是通过分数从小到大对集中成员进行排序。 Redis sorted set 的内部使用 HashMap 和跳跃表(skipList)确保数据的存储和有序,HashMap 会员到了 score 所有成员都存储在跳跃表中,排名依据是 HashMap 里存的 score,采用跳跃表的结构可获得较高的搜索效率,并且在实现上比较简单。 Redis 总结数据类型应用场景:

SpringBoot Redis 缓存

面试官:没想到你平时下了很多功夫。 Redis 你一定用过缓存吗? 我:用过。 面试官:那你告诉我你是怎么用的? 我:我是结合 Spring Boot 使用。一般有两种方式: 1、接通过 RedisTemplate 来使用。 2、使用 Spring Cache 集成 Redis pom.xml 加入以下依赖: 简单说一下代码。

<dependencies> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency>     <groupId>org.apache.commons</groupId>     <artifactId>commons-pool2</artifactId> </dependency> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency>     <groupId>org.springframework.session</groupId>     <artifactId>spring-session-data-redis</artifactId> </dependency>  <dependency>     <groupId>org.projectlombok</groupId>     <artifactId>lombok</artifactId>     <optional>true</optional> </dependency> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-test</artifactId>     <scope>test</scope> </dependency> </dependencies> 

spring-boot-starter-data-redis:在 Spring Boot 2.x 底层将不再使用 Jedis,而是换成了 Lettuce。 commons-pool2:用作 Redis 如果连接池不引入启动会报错。 spring-session-data-redis:Spring Session 介绍,作为共享 Session。配置文件 application.yml 的配置:

server: port:8082 servlet: session:   timeout:30ms spring: cache: type:redis redis: host:127.0.0.1 port:6379 password: # Redis 默认情况下有 16 个分片,具体使用的分片在此配置,默认为 0 database:0 lettuce:   pool:      连接池最大连接数(使用负数表示没有限制),默认 8
    max-active:100

创建实体类 User.java:

publicclass User implements Serializable{

privatestaticfinallong serialVersionUID = 662692455422902539L;

private Integer id;

private String name;

private Integer age;

public User() {
}

public User(Integer id, String name, Integer age) {
    this.id = id;
    this.name = name;
    this.age = age;
}

public Integer getId() {
    return id;
}

public void setId(Integer id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public Integer getAge() {
    return age;
}

public void setAge(Integer age) {
    this.age = age;
}

@Override
public String toString() {
    return"User{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", age=" + age +
            '}';
}
} 

RedisTemplate 的使用方式

默认情况下的模板只能支持 RedisTemplate<String, String>,也就是只能存入字符串,所以自定义模板很有必要。添加配置类 RedisCacheConfig.java:

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
publicclass RedisCacheConfig {

@Bean
public RedisTemplate<String, Serializable> redisCacheTemplate(LettuceConnectionFactory connectionFactory) {

    RedisTemplate<String, Serializable> template = new RedisTemplate<>();
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    template.setConnectionFactory(connectionFactory);
    return template;
}
} 

测试类:
@RestController
@RequestMapping("/user")
publicclass UserController {

publicstatic Logger logger = LogManager.getLogger(UserController.class);

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Autowired
private RedisTemplate<String, Serializable> redisCacheTemplate;

@RequestMapping("/test")
public void test() {
    redisCacheTemplate.opsForValue().set("userkey", new User(1, "张三", 25));
    User user = (User) redisCacheTemplate.opsForValue().get("userkey");
    logger.info("当前获取对象:{}", user.toString());
} 

  
  然后在浏览器访问,观察后台日志 http://localhost:8082/user/test。

使用 Spring Cache 集成 Redis

Spring Cache 具备很好的灵活性,不仅能够使用 SPEL(spring expression language)来定义缓存的 Key 和各种 Condition,还提供了开箱即用的缓存临时存储方案,也支持和主流的专业缓存如 EhCache、Redis、Guava 的集成。 定义接口 UserService.java:

publicinterface UserService {

User save(User user);

void delete(int id);

User get(Integer id);
} 

接口实现类 UserServiceImpl.java:

@Service
publicclass UserServiceImpl implements UserService{

publicstatic Logger logger = LogManager.getLogger(UserServiceImpl.class);

privatestatic Map<Integer, User> userMap = new HashMap<>();
static {
    userMap.put(1, new User(1, "肖战", 25));
    userMap.put(2, new User(2, "王一博", 26));
    userMap.put(3, new User(3, "杨紫", 24));
}


@CachePut(value ="user", key = "#user.id")
@Override
public User save(User user) {
    userMap.put(user.getId(), user);
    logger.info("进入save方法,当前存储对象:{}", user.toString());
    return user;
}

@CacheEvict(value="user", key = "#id")
@Override
public void delete(int id) {
    userMap.remove(id);
    logger.info("进入delete方法,删除成功");
}

@Cacheable(value = "user", key = "#id")
@Override
public User get(Integer id) {
    logger.info("进入get方法,当前获取对象:{}", userMap.get(id)==null?null:userMap.get(id).toString());
    return userMap.get(id);
}
} 

为了方便演示数据库的操作,这里直接定义了一个 Map<Integer,User> userMap。 这里的核心是三个注解:

  • @Cachable
  • @CachePut
  • @CacheEvict

测试类:UserController

@RestController
@RequestMapping("/user")
publicclass UserController {

publicstatic Logger logger = LogManager.getLogger(UserController.class);

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Autowired
private RedisTemplate<String, Serializable> redisCacheTemplate;

@Autowired
private UserService userService;

@RequestMapping("/test")
public void test() {
    redisCacheTemplate.opsForValue().set("userkey", new User(1, "张三", 25));
    User user = (User) redisCacheTemplate.opsForValue().get("userkey");
    logger.info("当前获取对象:{}", user.toString());
}


@RequestMapping("/add")
public void add() {
    User user = userService.save(new User(4, "李现", 30));
    logger.info("添加的用户信息:{}",user.toString());
}

@RequestMapping("/delete")
public void delete() {
    userService.delete(4);
}

@RequestMapping("/get/{id}")
public void get(@PathVariable("id") String idStr) throws Exception{
    if (StringUtils.isBlank(idStr)) {
        thrownew Exception("id为空");
    }
    Integer id = Integer.parseInt(idStr);
    User user = userService.get(id);
    logger.info("获取的用户信息:{}",user.toString());
}
} 

用缓存要注意,启动类要加上一个注解开启缓存:

@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)
@EnableCaching
publicclass Application {

public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
}

} 

1、先调用添加接口: http://localhost:8082/user/add

2、再调用查询接口,查询 id=4 的用户信息:

可以看出,这里已经从缓存中获取数据了,因为上一步 add 方法已经把 id=4 的用户数据放入了 Redis 缓存。 3、调用删除方法,删除 id=4 的用户信息,同时清除缓存

4、再次调用查询接口,查询 id=4 的用户信息:

没有了缓存,所以进入了 get 方法,从 userMap 中获取。

缓存注解

1、@Cacheable 根据方法的请求参数对其结果进行缓存

  • key:缓存的 key,可以为空,如果指定要按照 SPEL 表达式编写,如果不指定,则按照方法的所有参数进行组合。
  • value:缓存的名称,必须指定至少一个(如 @Cacheable (value='user')或者@Cacheable(value={'user1','user2'}))
  • condition:缓存的条件,可以为空,使用 SPEL 编写,返回 true 或者 false,只有为 true 才进行缓存。

@Cacheable 注解不支持配置过期时间,所有需要通过配置 cacheManager 来配置默认的过期时间和针对每个类或者是方法进行缓存失效时间配置。 2、@CachePut根据方法的请求参数对其结果进行缓存,和@Cacheable 不同的是,它每次都会触发真实方法的调用。参数描述见上。 3、@CacheEvict根据条件对缓存进行清空

  • key:同上
  • value:同上
  • condition:同上
  • allEntries:是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存
  • beforeInvocation:是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存。缺省情况下,如果方法执行抛出异常,则不会清空缓存。

缓存问题

面试官:看了一下你的 demo,简单易懂。那你在实际项目中使用缓存有遇到什么问题或者会遇到什么问题你知道吗? 我:缓存和数据库数据一致性问题:分布式环境下非常容易出现缓存和数据库间数据一致性问题,针对这一点,如果项目对缓存的要求是强一致性的,那么就不要使用缓存。

我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。合适的策略包括合适的缓存更新策略,更新数据库后及时更新缓存、缓存失败时增加重试机制。 面试官:Redis 雪崩了解吗? 我:我了解的,目前电商首页以及热点数据都会去做缓存,一般缓存都是定时任务去刷新,或者查不到之后去更新缓存的,定时任务刷新就有一个问题。举个栗子:如果首页所有 Key 的失效时间都是 12 小时,中午 12 点刷新的,我零点有个大促活动大量用户涌入,假设每秒 6000 个请求,本来缓存可以抗住每秒 5000 个请求,但是缓存中所有 Key 都失效了。

此时 6000 个/秒的请求全部落在了数据库上,数据库必然扛不住,真实情况可能 DBA 都没反应过来直接挂了,此时,如果没什么特别的方案来处理,DBA 很着急,重启数据库,但是数据库立马又被新流量给打死了。这就是我理解的缓存雪崩。 我心想:同一时间大面积失效,瞬间 Redis 跟没有一样,那这个数量级别的请求直接打到数据库几乎是灾难性的,你想想如果挂的是一个用户服务的库,那其他依赖他的库所有接口几乎都会报错,如果没做熔断等策略基本上就是瞬间挂一片的节奏,你怎么重启用户都会把你打挂,等你重启好的时候,用户早睡觉去了,临睡之前,骂骂咧咧“什么垃圾产品”。 面试官 :嗯,还不错,那这种情况你都是怎么应对的?(面试官摸摸了自己的头发) 我:处理缓存雪崩简单,在批量往 Redis 存数据的时候,把每个 Key 的失效时间都加个随机值就好了,这样可以保证数据不会再同一时间大面积失效。

setRedis(key, value, time+Math.random()*10000);

如果 Redis 是集群部署,将热点数据均匀分布在不同的 Redis 库中也能避免全部失效。

或者设置热点数据永不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就好了,不要设置过期时间),电商首页的数据也可以用这个操作,保险。 面试官:那你了解缓存穿透和击穿么,可以说说他们跟雪崩的区别吗? 我:嗯,了解,先说下缓存穿透吧,缓存穿透是指缓存和数据库中都没有的数据,而用户(黑客)不断发起请求,举个例子:我们数据库的 id 都是从 1 自增的,如果发起 id=-1 的数据或者 id 特别大不存在的数据,这样的不断攻击导致数据库压力很大,严重会击垮数据库。 我又接着说:至于缓存击穿嘛,这个跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了 DB,而缓存击穿不同的是缓存击穿是指一个 Key 非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个 Key 在失效的瞬间,持续的大并发直接落到了数据库上,就在这个 Key 的点上击穿了缓存。 面试官露出欣慰的眼光:那他们分别怎么解决? 我:缓存穿透我会在接口层增加校验,比如用户鉴权,参数做校验,不合法的校验直接 return,比如 id 做基础校验,id<=0 直接拦截。 面试官:那你还有别的方法吗? 我:我记得 Redis 里还有一个高级用法这个也能很好的预防缓存穿透的发生,他的原理也很简单,就是利用高效的数据结构和算法快速判断出你这个 Key 是否在数据库中存在,不存在你 return 就好了,存在你就去查 DB 刷新 KV 再 return。缓存击穿的话,设置热点数据永不过期,或者加上互斥锁就搞定了。作为暖男,代码给你准备好了,拿走不谢。

public static String getData(String key) throws InterruptedException {
    //从Redis查询数据
    String result = getDataByKV(key);
    //参数校验
    if (StringUtils.isBlank(result)) {
        try {
            //获得锁
            if (reenLock.tryLock()) {
                //去数据库查询
                result = getDataByDB(key);
                //校验
                if (StringUtils.isNotBlank(result)) {
                    //插进缓存
                    setDataToKV(key, result);
                }
            } else {
                //睡一会再拿
                Thread.sleep(100L);
                result = getData(key);
            }
        } finally {
            //释放锁
            reenLock.unlock();
        }
    }
    return result;
} 

面试官 :嗯嗯,还不错。

Redis 为何这么快

面试官 :Redis 作为缓存大家都在用,那 Redis 一定很快咯? 我:当然了,官方提供的数据可以达到 100000+ 的 QPS(每秒内的查询次数),这个数据不比 Memcached 差! 面试官:Redis 这么快,它的“多线程模型”你了解吗?(露出邪魅一笑) 我:您是想问 Redis 这么快,为什么还是单线程的吧。Redis 确实是单进程单线程的模型,因为 Redis 完全是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章的采用单线程的方案了(毕竟采用多线程会有很多麻烦)。 面试官:嗯,是的。那你能说说 Redis 是单线程的,为什么还能这么快吗? 我:可以这么说吧。

第一:Redis 完全基于内存,绝大部分请求是纯粹的内存操作,非常迅速,数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度是 O(1)。

第二:数据结构简单,对数据操作也简单。

第三:采用单线程,避免了不必要的上下文切换和竞争条件,不存在多线程导致的 CPU 切换,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有死锁问题导致的性能消耗。第四:使用多路复用 IO 模型,非阻塞 IO。

Redis 和 Memcached 的区别

面试官:嗯嗯,说的很详细。那你为什么选择 Redis 的缓存方案而不用 Memcached 呢? 我 :

  • 存储方式上:Memcached 会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis 有部分数据存在硬盘上,这样能保证数据的持久性。
  • 数据支持类型上:Memcached 对数据类型的支持简单,只支持简单的 key-value,而 Redis 支持五种数据类型。
  • 使用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。Redis 直接自己构建了 VM 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
  • value 的大小:Redis 可以达到 1GB,而 Memcached 只有 1MB。 淘汰策略

面试官 :那你说说你知道的 Redis 的淘汰策略有哪些? 我:Redis有六种淘汰策略:

4.0 版本后增加以下两种:

  • volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  • allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

Redis 持久化机制

面试官 :你对 Redis 的持久化机制了解吗?怎么保证 Redis 挂掉之后再重启数据可以进行恢复?能讲一下吗? 我:Redis 为了保证效率,数据缓存在了内存中,但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。Redis 的持久化策略有两种:1、RDB:快照形式是直接把内存中的数据保存到一个 dump 的文件中,定时保存,保存策略。

2、AOF:把所有的对 Redis 的服务器进行修改的命令都存到一个文件里,命令的集合。Redis 默认是快照 RDB 的持久化方式。当 Redis 重启的时候,它会优先使用 AOF 文件来还原数据集,因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。你甚至可以关闭持久化功能,让数据只在服务器运行时存。 面试官:那你再说下 RDB 是怎么工作的? 我:默认 Redis 是会以快照“RDB"的形式将数据持久化到磁盘的一个二进制文件 dump.rdb。工作原理简单说一下:当 Redis 需要做持久化时,Redis 会 fork 一个子进程,子进程将数据写到磁盘上一个临时 RDB 文件中。

当子进程完成写临时文件后,将原来的 RDB 替换掉,这样的好处是可以 copy-on-write。 我:RDB 的优点是:这种文件非常适合用于备份,比如,你可以在最近的 24 小时内,每小时备份一次,并且在每个月的每一天也备份一个 RDB 文件。这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。RDB 非常适合灾难恢复。RDB 的缺点是:如果你需要尽量避免在服务器故障时丢失数据,那么 RDB 不合适你。 面试官:那你要不再说下 AOF? 我:(说就一起说下吧)使用 AOF 做持久化,每一个写命令都通过 write 函数追加到 appendonly.aof 中,配置方式如下:

appendfsync yes
appendfsync always     #每次有数据修改发生时都会写入AOF文件。
appendfsync everysec   #每秒钟同步一次,该策略为AOF的缺省策略。

AOF 可以做到全程持久化,只需要在配置中开启 appendonly yes。这样 Redis 每执行一个修改数据的命令,都会把它添加到 AOF 文件中,当 Redis 重启时,将会读取 AOF 文件进行重放,恢复到 Redis 关闭前的最后时刻。 我顿了一下,继续说:使用 AOF 的优点是会让 Redis 变得非常耐久。可以设置不同的 fsync 策略,AOF 的默认策略是每秒钟 fsync 一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。缺点是对于相同的数据集来说,AOF 的文件体积通常要大于 RDB 文件的体积。根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB。 面试官又问:你说了这么多,那我该用哪一个呢? 我:如果你非常关心你的数据,但仍然可以承受数分钟内的数据丢失,那么可以额只使用 RDB 持久。AOF 将 Redis 执行的每一条命令追加到磁盘中,处理巨大的写入会降低 Redis 的性能,不知道你是否可以接受。数据库备份和灾难恢复:定时生成 RDB 快照非常便于进行数据库备份,并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度快。当然了,Redis 支持同时开启 RDB 和 AOF,系统重启后,redis 会优先使用 AOF 来恢复数据,这样丢失的数据会最少。

Redis 4.0 对于持久化机制的优化

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。 如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

补充内容:AOF 重写

AOF 重写可以产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。 AOF 重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作

主从复制

面试官:Redis 单节点存在单点故障问题,为了解决单点问题,一般都需要对 Redis 配置从节点,然后使用哨兵来监听主节点的存活状态,如果主节点挂掉,从节点能继续提供缓存功能,你能说说 Redis 主从复制的过程和原理吗? 我:我有点懵,这个说来就话长了。但幸好提前准备了:主从配置结合哨兵模式能解决单点故障问题,提高 Redis 可用性。从节点仅提供读操作,主节点提供写操作。对于读多写少的状况,可给主节点配置多个从节点,从而提高响应效率。 我顿了一下,接着说,关于复制过程,是这样的:

  • 从节点执行 slaveof[masterIP][masterPort],保存主节点信息
  • 从节点中的定时任务发现主节点信息,建立和主节点的 socket 连接
  • 从节点发送 Ping 信号,主节点返回 Pong,两边能互相通信
  • 连接建立后,主节点将所有数据发送给从节点(数据同步)
  • 主节点把当前的数据同步给从节点后,便完成了复制的建立过程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性。

面试官:那你能详细说下数据同步的过程吗? 我 :(我心想:这也问的太细了吧)可以。Redis 2.8 之前使用 sync[runId][offset] 同步命令,Redis 2.8 之后使用 psync[runId][offset] 命令。两者不同在于,sync 命令仅支持全量复制过程,psync 支持全量和部分复制。介绍同步之前,先介绍几个概念:

  • runId:每个 Redis 节点启动都会生成唯一的 uuid,每次 Redis 重启后,runId 都会发生变化。
  • offset:主节点和从节点都各自维护自己的主从复制偏移量 offset,当主节点有写入命令时,offset=offset+ 命令的字节长度。从节点在收到主节点发送的命令后,也会增加自己的 offset,并把自己的 offset 发送给主节点。这样,主节点同时保存自己的 offset 和从节点的 offset,通过对比 offset 来判断主从节点数据是否一致。
  • repl_backlog_size:保存在主节点上的一个固定长度的先进先出队列,默认大小是 1MB。

主节点发送数据给从节点过程中,主节点还会进行一些写操作,这时候的数据存储在复制缓冲区中。从节点同步主节点数据完成后,主节点将缓冲区的数据继续发送给从节点,用于部分复制。主节点响应写命令时,不但会把命名发送给从节点,还会写入复制积压缓冲区,用于复制命令丢失的数据补救。 下面是 psync 的执行流程。

从节点发送 psync[runId][offset]命令,主节点有三种响应:

  • FULLRESYNC:第一次连接,进行全量复制
  • CONTINUE:进行部分复制
  • ERR:不支持 psync 命令,进行全量复制

面试官:很好,那你能具体说下全量复制和部分复制的过程吗? 我:可以

上面是全量复制的流程。主要有以下几步:

  • 从节点发送 psync ? -1 命令(因为第一次发送,不知道主节点的 runId,所以为?,因为是第一次复制,所以 offset=-1)。
  • 主节点发现从节点是第一次复制,返回 FULLRESYNC {runId} {offset},runId 是主节点的 runId,offset 是主节点目前的 offset。
  • 从节点接收主节点信息后,保存到 info 中。
  • 主节点在发送 FULLRESYNC 后,启动 bgsave 命令,生成 RDB 文件(数据持久化)。
  • 主节点发送 RDB 文件给从节点。到从节点加载数据完成这段期间主节点的写命令放入缓冲区。
  • 从节点清理自己的数据库数据。
  • 从节点加载 RDB 文件,将数据保存到自己的数据库中。
  • 如果从节点开启了 AOF,从节点会异步重写 AOF 文件。

关于部分复制有以下几点说明:

  • 部分复制主要是 Redis 针对全量复制的过高开销做出的一种优化措施,使用 psync[runId][offset] 命令实现。当从节点正在复制主节点时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,主节点的复制积压缓冲区将这部分数据直接发送给从节点,这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据。
  • 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内的复制积压缓冲区依然可以保存最近一段时间的写命令数据。
  • 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行 ID。因此会把它们当做 psync 参数发送给主节点,要求进行部分复制。
  • 主节点接收到 psync 命令后首先核对参数 runId 是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数 offset 在复制积压缓冲区中查找,如果 offset 之后的数据存在,则对从节点发送 +COUTINUE 命令,表示可以进行部分复制。因为缓冲区大小固定,若发生缓冲溢出,则进行全量复制。
  • 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。

哨兵

面试官:那主从复制会存在哪些问题呢? 我:主从复制会存在以下问题:

  • 一旦主节点宕机,从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。
  • 主节点的写能力受到单机的限制。
  • 主节点的存储能力受到单机的限制。
  • 原生复制的弊端在早期的版本中也会比较突出,比如:Redis 复制中断后,从节点会发起 psync。此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时,可能会造成毫秒或秒级的卡顿。 面试官:那比较主流的解决方案是什么呢?我:当然是哨兵啊。面试官 :那么问题又来了。那你说下哨兵有哪些功能?

我:如图,是 Redis Sentinel(哨兵)的架构图。Redis Sentinel(哨兵)主要功能包括主节点存活检测、主从运行情况检测、自动故障转移、主从切换。Redis Sentinel 最小配置是一主一从。Redis 的 Sentinel 系统可以用来管理多个 Redis 服务器,该系统可以执行以下四个任务:

  • 监控 :不断检查主服务器和从服务器是否正常运行。
  • 通知 :当被监控的某个 redis 服务器出现问题,Sentinel 通过 API 脚本向管理员或者其他应用程序发出通知。
  • 自动故障转移 :当主节点不能正常工作时,Sentinel 会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点,这样人工干预就可以免了。
  • 配置提供者 :在 Redis Sentinel 模式下,客户端应用在初始化时连接的是 Sentinel 节点集合,从中获取主节点的信息。 面试官 :那你能说下哨兵的工作原理吗?我 :话不多说,直接上图。1、每个 Sentinel 节点都需要定期执行以下任务:每个 Sentinel 以每秒一次的频率,向它所知的主服务器、从服务器以及其他的 Sentinel 实例发送一个 PING 命令。

2、如果一个实例距离最后一次有效回复 PING 命令的时间超过down-after-milliseconds所指定的值,那么这个实例会被 Sentinel 标记为主观下线。

3、如果一个主服务器被标记为主观下线,那么正在监视这个服务器的所有 Sentinel 节点,要以每秒一次的频率确认主服务器的确进入了主观下线状态。

4、如果一个主服务器被标记为主观下线,并且有足够数量的 Sentinel(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断,那么这个主服务器被标记为客观下线。

5、一般情况下,每个 Sentinel 会以每 10 秒一次的频率向它已知的所有主服务器和从服务器发送 INFO 命令,当一个主服务器被标记为客观下线时,Sentinel 向下线主服务器的所有从服务器发送 INFO 命令的频率,会从 10 秒一次改为每秒一次。

6、Sentinel 和其他 Sentinel 协商客观下线的主节点的状态,如果处于 SDOWN 状态,则投票自动选出新的主节点,将剩余从节点指向新的主节点进行数据复制。

7、当没有足够数量的 Sentinel 同意主服务器下线时,主服务器的客观下线状态就会被移除。当主服务器重新向 Sentinel 的 PING 命令返回有效回复时,主服务器的主观下线状态就会被移除。

面试官:不错,面试前没少下工夫啊,今天 Redis 这关你过了,明天找个时间我们再聊聊其他的。(露出欣慰的微笑) 我:没问题。

总结

本文在一次面试的过程中讲述了 Redis 是什么,Redis 的特点和功能,Redis 缓存的使用,Redis 为什么能这么快,Redis 缓存的淘汰策略,持久化的两种方式,Redis 高可用部分的主从复制和哨兵的基本原理。只要功夫深,铁杵磨成针,平时准备好,面试不用慌。虽然面试不一定是这样问的,但万变不离其“宗”。

金三银四面试季,为了做好大家面试路上的助攻手,对于 Redis 这块心里还没底的同学,特整理 40 道Redis常见面试题,让你面试不慌,争取 Offer 拿到手软!

1、什么是 Redis?

Redis 是完全开源免费的,遵守 BSD 协议,是一个高性能的 key-value 数据库。 Redis 与其他 key - value 缓存产品相比有以下三个特点:

  • Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。
  • Redis 支持数据的备份,即 master-slave 模式的数据备份。 Redis 优势:
  • 性能极高:Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s。
  • 丰富的数据类型:Redis 支持二进制案例的 Strings,Lists,Hashes,Sets 及 Ordered Sets 数据类型操作。
  • 原子:Redis 的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过 MULTI 和 EXEC 指令包起来。
  • 丰富的特性:Redis 还支持 publish/subscribe,通知,key 过期等等特性。 Redis 与其他 key-value 存储有什么不同?
  • Redis 有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis 的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
  • Redis 运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样 Redis 可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。

2、Redis 的数据类型?

Redis 支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及 zsetsorted set(有序集合)。 我们实际项目中比较常用的是 string,hash 如果你是 Redis 中高级用户,还需要加上下面几种数据结构 HyperLogLog、Geo、Pub/Sub。 如果你说还玩过 Redis Module,像 BloomFilter,RedisSearch,Redis-ML,面试官的眼睛就开始发亮了。

3、使用 Redis 有哪些好处?

  • 速度快,因为数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O1)
  • 支持丰富数据类型,支持 string,list,set,Zset,hash 等
  • 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
  • 丰富的特性,可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除

4、Redis 相比 Memcached 有哪些优势?

  • Memcached 所有的值均是简单的字符串,Redis 作为其替代者,支持更为丰富的数据类
  • Redis 的速度比 Memcached 快很多
  • Redis 可以持久化其数据

5、Memcache 与 Redis 的区别都有哪些?

  • 存储方式 Memecache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis 有部分存在硬盘上,这样能保证数据的持久性。
  • 数据支持类型 Memcache 对数据类型支持相对简单。Redis 有复杂的数据类型。
  • 使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

6、Redis 是单进程单线程的?

Redis 是单进程单线程的,Redis 利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。

7、一个字符串类型的智能存储最大容量是多少?

512M。

8、Redis 的持久化机制是什么?各自的优缺点?

Redis提供两种持久化机制 RDB 和 AOF 机制: RDB(Redis DataBase)持久化方式:是指用数据集快照的方式半持久化模式记录 Redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。 优点:

  • 只有一个文件 dump.rdb,方便持久化。
  • 容灾性好,一个文件可以保存到安全的磁盘。
  • 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis的高性能。
  • 相对于数据集大时,比 AOF 的启动效率更高。 缺点:数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 Redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候AOF(Append-only file)持久化方式:是指所有的命令行记录以 Redis 命令请求协议的格式完全持久化存储保存为 aof 文件。优点:
  • 数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。
  • 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
  • AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall) 缺点:
  • AOF 文件比 RDB 文件大,且恢复速度慢。
  • 数据集大的时候,比 RDB 启动效率低。

9、Redis 常见性能问题和解决方案

  • Master 最好不要写内存快照,如果 Master 写内存快照,save 命令调度 rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务。
  • 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一。
  • 为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网。
  • 尽量避免在压力很大的主库上增加从。
  • 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1<- Slave2 <- Slave3……这样的结构方便解决单点故障问题,实现 Slave 对 Master 的替换。如果 Master 挂了,可以立刻启用 Slave1 做 Master,其他不变。

10、Redis 过期键的删除策略?

  • 定时删除:在设置键的过期时间的同时,创建一个定时器 timer。让定时器在键的过期时间来临时,立即执行对键的删除操作。
  • 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
  • 定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。

11、Redis 的回收策略(淘汰策略)?

  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  • no-enviction(驱逐):禁止驱逐数据 注意这里的 6 种机制,volatile 和 allkeys 规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,后面的 lru、ttl 以及 random 是三种不同的淘汰策略,再加上一种 no-enviction 永不回收的策略。使用策略规则:
  • 如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allkeys-lru
  • 如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用 allkeys-random

12、为什么 Redis 需要把所有数据放到内存中?

Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以 Redis 具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘 I/O 速度为严重影响 Redis 的性能。在内存越来越便宜的今天,Redis 将会越来越受欢迎。如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。

13、Redis 的同步机制了解么?

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

14、Pipeline 有什么好处,为什么要用 Pipeline?

可以将多次 IO 往返的时间缩减为一次,前提是 Pipeline 执行的指令之间没有因果相关性。使用 redis-benchmark 进行压测的时候可以发现影响 Redis 的 QPS 峰值的一个重要因素是 Pipeline 批次指令的数目。

15、是否使用过 Redis 集群,集群的原理是什么?

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

16、Redis 集群方案什么情况下会导致整个集群不可用?

有 A,B,C 三个节点的集群,在没有复制模型的情况下,如果节点 B 失败了,那么整个集群就会以为缺少 5501-11000 这个范围的槽而不可用。

17、Redis 支持的 Java 客户端都有哪些?官方推荐用哪个?

Redisson、Jedis、lettuce 等等,官方推荐使用 Redisson。

18、Jedis 与 Redisson 对比有什么优缺点?

Jedis 是 Redis 的 Java 实现的客户端,其 API 提供了比较全面的 Redis 命令的支持;Redisson 实现了分布式和可扩展的 Java 数据结构,和 Jedis 相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等 Redis 特性。 Redisson 的宗旨是促进使用者对 Redis 的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

19、Redis 如何设置密码及验证密码?

设置密码:config set requirepass 123456 授权密码:auth 123456

20、说说 Redis 哈希槽的概念?

Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。

21、Redis 集群的主从复制模型是怎样的?

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有 N-1 个复制品。

22、Redis 集群会有写操作丢失吗?为什么?

Redis 并不能保证数据的强一致性,这意味着在实际中集群在特定的条件下可能会丢失写操作。

23、Redis 集群之间是如何复制的?

异步复制。

24、Redis 集群最大节点个数是多少?

16384 个。

25、Redis 集群如何选择数据库?

Redis 集群目前无法做数据库选择,默认在 0 数据库。

26、怎么测试 Redis 的连通性?

使用 ping 命令。

27、怎么理解 Redis 事务?

事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

28、Redis 事务相关的命令有哪几个?

MULTI、EXEC、DISCARD、WATCH。

29、Redis key 的过期时间和永久有效分别怎么设置?

EXPIRE 和 PERSIST 命令。

30、Redis 如何做内存优化?

尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的 Web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的所有信息存储到一张散列表里面。

31、Redis 回收进程如何工作的?

一个客户端运行了新的命令,添加了新的数据。Redis 检查内存使用情况,如果大于 maxmemory 的限制,则根据设定好的策略进行回收。一个新的命令被执行,等等。所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。

32、都有哪些办法可以降低 Redis 的内存使用情况呢?

如果你使用的是 32 位的 Redis 实例,可以好好利用 Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的 Key-Value 可以用更紧凑的方式存放到一起。

33、Redis 的内存用完了会发生什么?

如果达到设置的上限,Redis 的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以将 Redis 当缓存来使用配置淘汰机制,当 Redis 达到内存上限时会冲刷掉旧的内容。

34、一个 Redis 实例最多能存放多少的 keys?List、Set、Sorted Set 他们最多能存放多少元素?

理论上 Redis 可以处理多达 232 的 keys,并且在实际中进行了测试,每个实例至少存放了 2 亿 5 千万的 keys。我们正在测试一些较大的值。任何 list、set、和 sorted set 都可以放 232 个元素。换句话说,Redis 的存储极限是系统中的可用内存值。

35、MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?

Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。 相关知识:Redis 提供 6 种数据淘汰策略:

  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  • no-enviction(驱逐):禁止驱逐数据

36、Redis 最适合的场景?

会话缓存(Session Cache),最常用的一种使用 Redis 的情景是会话缓存(session cache)。用 Redis 缓存会话比其他存储(如 Memcached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为人知的商业平台Magento 也提供 Redis 的插件。 全页缓存(FPC),除基本的会话 token 之外,Redis 还提供很简便的 FPC 平台。回到一致性问题,即使重启了 Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。

再次以 Magento 为例,Magento提供一个插件来使用 Redis 作为全页缓存后端。此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。 队列,Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得 Redis能作为一个很好的消息队列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop 操作。

如果你快速地在 Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用 Redis 创建非常好的后端工具,以满足各种队列需求。例如,Celery 有一个后台就是使用 Redis 作为 broker,你可以从这里去查看。 排行榜/计数器,Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。

所以,我们要从排序集合中获取到排名最靠前的 10 个用户——我们称之为“user_scores”,我们只需要像下面一样执行即可:当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:ZRANGE user_scores 0 10 WITHSCORES Agora Games 就是一个很好的例子,用 Ruby 实现的,它的排行榜就是使用 Redis 来存储数据的,你可以在这里看到。 发布/订阅,最后(但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建立聊天系统!

37、假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用 keys 指令可以扫出指定模式的 key 列表。 对方接着追问:如果这个 Redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问题? 这个时候你要回答 Redis 关键的一个特性:Redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞地提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。

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

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

39、使用过 Redis 做异步队列么,你是怎么用的?

答:一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep 一会再重试。如果对方追问可不可以不用 sleep 呢?list 还有个指令叫 blpop,在没有消息的时候,它会阻塞住直到消息到来。

如果对方追问能不能生产一次消费多次呢?使用 pub/sub 主题订阅者模式,可以实现1:N 的消息队列。 如果对方追问 pub/sub 有什么缺点? 在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 RabbitMQ等。 如果对方追问 Redis 如何实现延时队列? 我估计现在你很想把面试官一棒打死如果你手上有一根棒球棍的话,怎么问得这么详细。但是你很克制,然后神态自若地回答道:使用 sortedset,拿时间戳作为score,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。 到这里,面试官暗地里已经对你竖起了大拇指。但是他不知道的是此刻你却竖起了中指,在椅子背后。

40、使用过 Redis 分布式锁么,它是什么回事?

先拿 setnx 来争抢锁,抢到之后,再用 expire 给锁加一个过期时间防止锁忘记了释放。 这时候对方会告诉你说你回答得不错,然后接着问如果在 setnx 之后执行 expire之前进程意外 crash 或者要重启维护了,那会怎么样?这时候你要给予惊讶的反馈:唉,是喔,这个锁就永远得不到释放了。紧接着你需要抓一抓自己的脑袋,故作思考片刻,好像接下来的结果是你主动思考出来的,然后回答:我记得 set 指令有非常复杂的参数,这个应该是可以同时把 setnx 和expire 合成一条指令来用的!  

总结

“做程序员,圈子和学习最重要”因为有有了圈子可以让你少走弯路,扩宽人脉,扩展思路,学习他人的一些经验及学习方法!同时在这分享一下是一直以来整理的Java后端进阶笔记文档和学习资料免费分享给大家!

小伙伴们有兴趣想了解内容和更多相关学习资料的请点赞收藏+评论转发+关注我,后面会有很多干货。我有一些面试题、架构、设计类资料可以说是程序员面试必备!所有资料都整理到网盘了,需要的话欢迎下载!私信我回复【学习】即可免费获取

标签: 影响fpc连接器性能的四因素

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台