Redis面试题

Redis是一个使用c语言编写的,开源的高性能非关系型数据库(NoSQL)

数据类型

回答:一共五种
(一)String
这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。
(二)hash
这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
(三)list
使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。本人还用一个场景,很合适—取行情信息。就也是个生产者和消费者的场景。LIST可以很好的完成排队,先进先出的原则。
(四)set
因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。
另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
(五)sorted set
sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。

内部结构

  • dict 本质上是为了解决算法中的查找问题(Searching)是一个用于维护key和value映射关系的数据结构,与很多语言中的Map或dictionary类似。 本质上是为了解决算法中的查找问题(Searching)
  • sds sds就等同于char * 它可以存储任意二进制数据,不能像C语言字符串那样以字符’\0’来标识字符串的结 束,因此它必然有个长度字段。
  • skiplist (跳跃表) 跳表是一种实现起来很简单,单层多指针的链表,它查找效率很高,堪比优化过的二叉平衡树,且比平衡树的实现,
  • quicklist
  • ziplist 压缩表 ziplist是一个编码后的列表,是由一系列特殊编码的连续内存块组成的顺序型数据结构,

redis怎么用

命令

1. Key操作

命令 含义 返回值
exists key 名称 判断key是否存在 数字/0
expire key 秒数 设置key过期时间 1/0
ttl key 查看key的剩余过期时间 返回剩余时间,如果不过期返回-1
del key 根据key删除键值对。 被删除key的数量

2. 字符串值(String)

命令 含义 返回值
set key value 设置指定key的值 成功OK
get key 获取指定key的值 key的值。不存在返回nil
setnx key value 当且仅当key不存在时才新增。 不存在时返回1,存在返回0
setex key seconds value 设置key的存活时间,无论是否存在指定key都能新增,如果存在key覆盖旧值。同时必须指定过期时间。 OK

3.哈希表(Hash)

​ Hash类型的值中包含多组field value。

3.1 hset

​ 给key中field设置值。

​ 语法:hset key field value

​ 返回值:成功1,失败0

3.2 hget

​ 获取key中某个field的值

​ 语法:hget key field

​ 返回值:返回field的内容

3.3 hmset

​ 给key中多个filed设置值

​ 语法:hmset key field value field value

​ 返回值:成功OK

3.4 hmget

​ 一次获取key中多个field的值

​ 语法:hmget key field field

​ 返回值:value列表

3.5 hvals

​ 获取key中所有field的值

​ 语法:hvals key

​ 返回值:value列表

3.6 hgetall

​ 获取所有field和value

​ 语法:hgetall key

​ 返回值:field和value交替显示列表

3.7 hdel

​ 删除key中任意个field

​ 语法:hdel key field field

​ 返回值:成功删除field的数量

4. 列表(List)

4.1 Rpush

​ 向列表末尾中插入一个或多个值

​ 语法;rpush key value value

​ 返回值:列表长度

4.2 lrange

​ 返回列表中指定区间内的值。可以使用-1代表列表末尾

​ 语法:lrange list 0 -1

​ 返回值:查询到的值

4.3 lpush

​ 将一个或多个值插入到列表前面

​ 语法:lpush key value value

​ 返回值:列表长度

4.4 llen

​ 获取列表长度

​ 语法:llen key

​ 返回值:列表长度

4.5 lrem

​ 删除列表中元素。count为正数表示从左往右删除的数量。负数从右往左删除的数量。

​ 语法:lrem key count value

​ 返回值:删除数量。

5 集合(Set)

​ set和java中集合一样。

5.1 sadd

​ 向集合中添加内容。不允许重复。

​ 语法:sadd key value value value

​ 返回值:集合长度

5.2 scard

​ 返回集合元素数量

​ 语法:scard key

​ 返回值:集合长度

5.3 **smembers **

​ 查看集合中元素内容

​ 语法:smembers key

​ 返回值:集合中元素

6 .有序集合(Sorted Set)

​ 有序集合中每个value都有一个分数(score),根据分数进行排序。

6.1 zadd

​ 向有序集合中添加数据

​ 语法:zadd key score value score value

​ 返回值:长度

6.2 zrange

​ 返回区间内容,withscores表示带有分数

​ 语法:zrange key 区间 [withscores]

​ 返回值:值列表

使用SpringBoot整合SpringDataRedis操作redis

​ Spring Data是Spring公司的顶级项目,里面包含了N多个二级子项目,这些子项目都是相对独立的项目。每个子项目是对不同API的封装。

​ 所有Spring Boot整合Spring Data xxxx的启动器都叫做spring-boot-starter-data-xxxx

​ Spring Data 好处很方便操作对象类型。

​ 把Redis不同值得类型放到一个opsForXXX方法中。

​ opsForValue : String值

​ opsForList : 列表List

​ opsForHash: 哈希表Hash

​ opsForZSet: 有序集合Sorted Set

​ opsForSet : 集合

1. 添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>

2. 配置配置文件

​ spring.redis.host=localhost 默认值

​ spring.redis.port=6379 端口号默认值

1
2
3
4
5
spring:
redis:
host: 192.168.52.133
# cluster:
# nodes: 192.168.93.10:7001,192.168.93.10:7002,192.168.93.10:7003,192.168.93.10:7004,192.168.93.10:7005,192.168.93.10:7006

3.编写配置类

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
return redisTemplate;
}
}

4. 编写代码

4.1 编写对象新增
1
2
3
4
5
6
7
8
@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Test
public void testString() {
People peo = new People(1, "张三");
redisTemplate.opsForValue().set("peo1", peo);
}
4.2 编写对象获取

此处必须编写值序列化器。不指定时返回类型为LinkedHashMap

1
2
3
4
5
6
@Test
public void testGetString() {
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<People>(People.class));
People peo = (People) redisTemplate.opsForValue().get("peo1");
System.out.println(peo);
}
4.3 编写List
1
2
3
4
5
6
7
@Test
public void testList() {
List<People> list = new ArrayList<>();
list.add(new People(1, "张三"));
list.add(new People(2, "李四"));
redisTemplate.opsForValue().set("list2", list);
}
4.4 编写List取值
1
2
3
4
5
6
@Test
public void testGetList(){
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<List>(List.class));
List<People> list2 = (List<People>) redisTemplate.opsForValue().get("list2");
System.out.println(list2);
}

redis持久化

​ Redis不仅仅是一个内存型数据库,还具备持久化能力。

1. RDB

​ rdb模式是默认模式,可以在指定的时间间隔内生成数据快照(snapshot),默认保存到dump.rdb文件中。当redis重启后会自动加载dump.rdb文件中内容到内存中。用户可以使用SAVE(同步)或BGSAVE(异步)手动保存数据。比如save 900 1 的设置就会让服务器在900 秒以内,如果对数据库修改了至少一次的时候就会执行BGSAVE。

优点就是这样的恢复效率会比aof高,而且直接用文件就可以还原,但是如果出现不可预知的关闭的时候,就会可能丢失数据。并且每次保存数据的时候都会进行fork()子进程,这样会导致大量的耗费性能。

2 AOF

​ AOF默认是关闭的,需要在配置文件中开启AOF。Redis支持AOF和RDB同时生效,如果同时存在,AOF优先级高于RDB,也就是Redis重新启动时会使用AOF进行数据恢复,他存储的是redis所有的修改数据的操作。开启方法就是修改redis.conf文件里面appendonly yes(默认是no)

优点就是比RDB数据更加安全不容易丢失数据,在重启的时候就会进行数据恢复,但是就是数据集很大,比RDB也会更慢一点。

1
2
3
4
# 默认no
appendonly yes
# aof文件名
appendfilename "appendonly.aof"

穿透、击穿、雪崩、预热、更新过期key

穿透

就是数据库和缓存都没有这个数据,这样就导致了用户查询的时候再缓存里面找不到,每次又去数据库里面找,然后返回空,相当于进行了两次无效请求,要解决这种重复的无效请求,可以用两种办法,第一种就是简单暴力的将key对应null值存入缓存,但是这样就会有对空间的占用,第二种就是使用布隆过滤器,布隆过滤器是将所有可能存在的数据hash到一个足够大的bitmap里面,一个一定不会存在的数据,就会被这个bitmap给拦截掉,从而避免了直接对底层数据库查询的压力,并且因为有hash冲突的可能,底层算法使用多个hash函数来解决“冲突”,因为同一个hash函数算出来的值有可能相等,所以引入多个hash,通过一个hash某元素不在集合里面,说明就不在,判断在的时候就得当所有hash算出来在才算在。这是布隆过滤器的实现核心。

击穿

击穿就是当一个key很热点的时候,大并发集中对这个点进行访问,当这个key在缓存过期失效的一瞬间,就会持续的穿破缓存,直接对数据库进行一个请求,简单来说就是缓存里面没有,数据库里面有。

解决的话可以在访问key之前,把这些请求用锁锁起来,设置一个短期的key来锁,访问结束再删除短期key。 处理的案例之前做项目的时候我记得,多个机器拿token,token再存一份到redis里面,保证系统再token过期的时候只有一个线程去获取token,使用分布式锁。

雪崩

缓存雪崩也是一种缓存里面没有数据库里面有的情况,只不过是比击穿多了很多过期的key,比如设置了相同的过期时间,同一时间内出现了大面积的缓存过期,所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
解决办法:
大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开。

预热

缓存预热就是在系统上线后,我们将相关的缓存数据直接加载到系统之中,这样提前就避免了一个用户请求直接去查询数据库的情况。

具体操作可以是先写一个缓存刷新页面,然后数据量如果不打就自行加载,后面再定时刷新

更新

自定义的缓存淘汰(更),第一种是定时去清理过期的缓存; 第二种是用户请求过来时再去判断这个缓存是否过期。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!

降级

降级服务,是为了防止redis服务故障导致数据库一起发生雪崩问题,所以对于不要的缓存数据,可以进行降级,举个例子就是,redis出现问题了,就不去数据库查询,而是返回默认值给用户。

redis单线程

image-20220320220341675

Redis是单线程的,做的都是纯内存操作,使用单线程就避免了频繁的上下文切换,并且在6.x版本以后就增加了非阻塞I/O多路复用机制,但是工作线程其实还是单线程,计算都是串行的,redis从内核读取数据搬运数据的操作在6.x版本以后就是并行的在多个cpu上进行,但是主要的计算工作线程里还是将多个事件进行串行处理的,多路复用器里面使用selec、epoll等进行读写事件的一个判断,有就取出来进行处理。

快的关键

(一)纯内存操作
(二)单线程操作,避免了频繁的上下文切换
(三)采用了非阻塞I/O多路复用机制

分布式锁

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。
将 key 的值设为 value ,当且仅当 key 不存在。 若给定的 key 已经存在,则 SETNX 不做任何动作

解锁:使用 del key 命令就能释放锁
解决死锁:
1)通过Redis中expire()给锁设定最大持有时间,如果超过,则Redis来帮我们释放锁。
2) 使用 setnx key “当前系统时间+锁持有的时间”和getset key “当前系统时间+锁持有的时间”组合的命令就可以实现。

锁续期:好像是redission这个客户端里面自动就会实现的一个,底层我看过一篇文章好像是watchdog来每30秒判断是否client还持有锁,如果还持有就不断延长key,我还记得好像还使用的是可重入锁。

redis的过期策略以及内存淘汰机制

redis采用的是定期删除+惰性删除策略。定期删除呢举个例子就是每一段时间去检查是否有过期的key,但是这个检查不是说检查所有的,是随机抽取检查,所以如果只采用定期删除的话,就会导致很多key没有被定时删除,所以就引入惰性删除,就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间,就判断是否过期进行删除操作。

但是即使采用了这两种策略,也会有这种情况,就是如果定期删除没删掉,也没有及时去请求一个key,也就是惰性删除也没生效,就会导致一个内存越来越高,所以就还有一个内存淘汰机制。

内存淘汰机制是redis.conf文件的一行配置, 使用maxmemory-policy 加上 具体淘汰机制的这个名称: 机制我记得有这几种:

  • 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(驱逐):禁止驱逐数据,新写入操作会报错
  • ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。

主从复制 集群(cluster)

支持集群功能,并且还支持主从复制,就是每个节点有N个复制品,然后其中一个复制品是主,另外都是slave从,支持一个 一主多从,每个从又可以看作主,优点就是增强单一节点的健壮性,从节点可以对主节点做一个复制数据备份,提升了容灾能力。还有一点就是redis里面主节点一般用来写,从节点只能读,这样实现了读写分离,减少并发出现的一些同步错误。

搭建主从节点: replicaof + 主节点ip+ 端口

启动:在starup.sh文件里面进行设置 cd + 目录+ redis.conf

收于权限:chmod a+x startup.sh

哨兵

由于主从复制默认从只有读能力,所以如果主节点出现宕机,整个节点就不具备写能力了,所以呢我们用哨兵Sentinel来监控节点,如果主节点宕机,就重新选取主节点。

Memcache与Redis的区别都有哪些?

(1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

(2) redis的速度比memcached快很多

(3) redis可以持久化其数据:存储方式 Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。 Redis有部份存在硬盘上,redis可以持久化其数据

(4)使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。 Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。