Java面试题-Redis

1. 什么是Redis?

Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库

2. Redis支持哪些数据类型以及应用的场景?

Java面试题-Redis_第1张图片

3. Redis有哪些优缺点?

优点

  • 读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
  • 支持数据持久化,支持AOF和RDB两种持久化方式。
  • 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
  • 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。

缺点

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
  • Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。

4. Redis 和 Memcached 的区别和共同点?

共同点 :

  1. 都是基于内存的数据库,⼀般都⽤来当做缓存使⽤。
  2. 都有过期策略
  3. 两者的性能都⾮常⾼。

区别 :

  1. Redis ⽀持更丰富的数据类型(⽀持更复杂的应⽤场景)。Redis 不仅仅⽀持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只⽀持最简单的 k/v 数据类型。
  2. Redis ⽀持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进⾏使⽤,⽽ Memecache 把数据全部存在内存之中。
  3. Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
  4. Redis 在服务器内存使⽤完之后,可以将不⽤的数据放到磁盘上。但是,Memcached 在服务器内存使⽤完之后,就会直接报异常。
  5. Memcached 没有原⽣的集群模式,需要依靠客户端来实现往集群中分⽚写⼊数据;但是Redis ⽬前是原⽣⽀持 cluster 模式的.
  6. Memcached 是多线程,⾮阻塞 IO 复⽤的⽹络模型;Redis 使⽤单线程的多路 IO 复⽤模型。 (Redis 6.0 引⼊了多线程 IO
  7. Redis ⽀持发布订阅模型Lua 脚本事务等功能,⽽ Memcached 不⽀持。并且,Redis⽀持更多的编程语⾔。
  8. Memcached过期数据的删除策略只⽤了惰性删除,⽽ Redis 同时使⽤了惰性删除与定期删除。

5. Redis有哪些应用场景?

总结一

计数器

可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。

缓存

将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。

会话缓存

可以使用 Redis 来统一存储多台应用服务器会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。

全页缓存(FPC)

除基本的会话token之外,Redis还提供很简便的FPC平台。以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面

查找表

例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。

消息队列(发布/订阅功能)

List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息。不过最好使用 Kafka、RabbitMQ 等消息中间件。

分布式锁实现

在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。

其它

Set 可以实现交集、并集等操作,从而实现共同好友等功能。ZSet 可以实现有序性操作,从而实现排行榜等功能。

总结二

Redis相比其他缓存,有一个非常大的优势,就是支持多种数据类型。

数据类型说明string字符串,最简单的k-v存储hashhash格式,value为field和value,适合ID-Detail这样的场景。list简单的list,顺序列表,支持首位或者末尾插入数据set无序list,查找速度快,适合交集、并集、差集处理sorted set有序的set

其实,通过上面的数据类型的特性,基本就能想到合适的应用场景了。

string——适合最简单的k-v存储,类似于memcached的存储结构,短信验证码配置信息等,就用这种类型来存储。

hash——一般key为ID或者唯一标示,value对应的就是详情了。如商品详情个人信息详情新闻详情等。

list——因为list是有序的,比较适合存储一些有序且数据相对固定的数据。如省市区表字典表等。因为list是有序的,适合根据写入的时间来排序,如:最新的***,消息队列等。

set——可以简单的理解为ID-List的模式,如微博中一个人有哪些好友,set最牛的地方在于,可以对两个set提供交集、并集、差集操作。例如:查找两个人共同好友等。

Sorted Set——是set的增强版本,增加了一个score参数,自动会根据score的值进行排序。比较适合类似于top 10等不根据插入的时间来排序的数据。

如上所述,虽然Redis不像关系数据库那么复杂的数据结构,但是,也能适合很多场景,比一般的缓存数据结构要多。了解每种数据结构适合的业务场景,不仅有利于提升开发效率,也能有效利用Redis的性能。

6. 缓存数据的处理流程是怎样的?

1.如果⽤户请求的数据在缓存中就直接返回。
2. 缓存中不存在的话就看数据库中是否存在。
3. 数据库中存在的话就更新缓存中的数据。
4. 数据库中不存在的话就返回空数据。
Java面试题-Redis_第2张图片

7. Redis常见的数据结构使用

7.1 String

127.0.0.1:6379> set key value #设置 key-value 类型的值
OK
127.0.0.1:6379> get key # 根据 key 获得对应的 value
"value"
127.0.0.1:6379> exists key # 判断某个 key 是否存在
(integer) 1
127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的⻓度。
(integer) 5
127.0.0.1:6379> del key # 删除某个 key 对应的值
(integer) 1
127.0.0.1:6379> get key
(nil)

批量设置 :

127.0.0.1:6379> mset key1 value1 key2 value2 # 批量设置 key-value 类型的值
OK
127.0.0.1:6379> mget key1 key2 # 批量获取多个 key 对应的 value
1) "value1"
2) "value2"

计数器(字符串的内容为整数的时候可以使⽤):

127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> incr number # 将 key 中储存的数字值增⼀
(integer) 2
127.0.0.1:6379> get number
"2"
127.0.0.1:6379> decr number # 将 key 中储存的数字值减⼀
(integer) 1
127.0.0.1:6379> get number
"1"

过期:

127.0.0.1:6379> expire key 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56

7.2 list

通过 rpush/lpop 实现队列:

127.0.0.1:6379> rpush myList value1 # 向 list 的头部(右边)添加元素
(integer) 1
127.0.0.1:6379> rpush myList value2 value3 # 向list的头部(最右边)添加多个元素
(integer) 3
127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出
"value1"
127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end
1) "value2"
2) "value3"
127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第⼀
1) "value2"
2) "value3"

通过 rpush/rpop 实现栈:

127.0.0.1:6379> rpush myList2 value1 value2 value3
(integer) 3
127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出
"value3"

通过 lrange 查看对应下标范围的列表元素:

127.0.0.1:6379> rpush myList value1 value2 value3
(integer) 3
127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end
1) "value1"
2) "value2"
127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第⼀
1) "value1"
2) "value2"
3) "value3"

通过 lrange 命令,你可以基于 list 实现分⻚查询,性能⾮常⾼!

通过 llen 查看链表⻓度:

127.0.0.1:6379> llen myList
(integer) 3

7.3 hash

127.0.0.1:6379> hset userInfoKey name "guide" description "dev" age "24"
OK
127.0.0.1:6379> hexists userInfoKey name # 查看 key 对应的 value中指定的字段是否存
在。
(integer) 1
127.0.0.1:6379> hget userInfoKey name # 获取存储在哈希表中指定字段的值。
"guide"
127.0.0.1:6379> hget userInfoKey age
"24"
127.0.0.1:6379> hgetall userInfoKey # 获取在哈希表中指定 key 的所有字段和值
1) "name"
2) "guide"
3) "description"
4) "dev"
5) "age"
6) "24"
127.0.0.1:6379> hkeys userInfoKey # 获取 key 列表
1) "name"
2) "description"
3) "age"
127.0.0.1:6379> hvals userInfoKey # 获取 value 列表
1) "guide"
2) "dev"
3) "24"
127.0.0.1:6379> hset userInfoKey name "GuideGeGe" # 修改某个字段对应的值
127.0.0.1:6379> hget userInfoKey name
"GuideGeGe"

7.4 set

127.0.0.1:6379> sadd mySet value1 value2 # 添加元素进去
(integer) 2
127.0.0.1:6379> sadd mySet value1 # 不允许有重复元素
(integer) 0
127.0.0.1:6379> smembers mySet # 查看 set 中所有的元素
1) "value1"
2) "value2"
127.0.0.1:6379> scard mySet # 查看 set 的⻓度
(integer) 2
127.0.0.1:6379> sismember mySet value1 # 检查某个元素是否存在set 中,只能接收单个元
素
(integer) 1
127.0.0.1:6379> sadd mySet2 value2 value3
(integer) 2
127.0.0.1:6379> sinterstore mySet3 mySet mySet2 # 获取 mySet 和 mySet2 的交集并
存放在 mySet3 中
(integer) 1
127.0.0.1:6379> smembers mySet3
1) "value2"

7.5 zset(sorted set)

127.0.0.1:6379> zadd myZset 3.0 value1 # 添加元素到 sorted set 中 3.0 为权重
(integer) 1
127.0.0.1:6379> zadd myZset 2.0 value2 1.0 value3 # ⼀次添加多个元素
(integer) 2
127.0.0.1:6379> zcard myZset # 查看 sorted set 中的元素数量
(integer) 3
127.0.0.1:6379> zscore myZset value1 # 查看某个 value 的权重
"3"
127.0.0.1:6379> zrange myZset 0 -1 # 顺序输出某个范围区间的元素,0 -1 表示输出所有元
素
1) "value3"
2) "value2"
3) "value1"
127.0.0.1:6379> zrange myZset 0 1 # 顺序输出某个范围区间的元素,0 为 start 1 为
stop
1) "value3"
2) "value2"
127.0.0.1:6379> zrevrange myZset 0 1 # 逆序输出某个范围区间的元素,0 为 start 1
为 stop
1) "value1"
2) "value2"

8. Redis 给缓存数据设置过期时间有啥用?

因为内存是有限的,如果缓存中的所有数据都是⼀直保存的话,分分钟直接Out of memory。
可以用于短信验证码可能只在1分钟内有效,⽤户登录的 token 可能只在 1 天内有效。
Redis ⾃带了给缓存数据设置过期时间的功能,⽐如:

127.0.0.1:6379> exp key 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56

9. 过期的数据的删除策略了解么?

  1. 惰性删除 :只会在取出key的时候才对数据进⾏过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。

  2. 定期删除 : 每隔⼀段时间抽取⼀批 key 执⾏删除过期key操作。并且,Redis 底层会通过限制删除操作执⾏的时⻓和频率来减少删除操作对CPU时间的影响。

10. Redis 内存淘汰机制了解么?

Redis 提供 6 种数据淘汰策略:

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使⽤的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru(least recently used):当内存不⾜以容纳新写⼊数据时,在键空间中,移除最近最少使⽤的 key(这个是最常⽤的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁⽌驱逐数据,也就是说当内存不⾜以容纳新写⼊数据时,新写⼊操作会报
    错。这个应该没⼈使⽤吧!
    4.0 版本后增加以下两种:
  7. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中
    挑选最不经常使⽤的数据淘汰
  8. allkeys-lfu(least frequently used):当内存不⾜以容纳新写⼊数据时,在键空间中,移
    除最不经常使⽤的 key

11.Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)

Redis 的⼀种持久化⽅式叫快照(snapshotting,RDB),另⼀种⽅式是只追加⽂件(append-only file, AOF)。

快照(snapshotting)持久化(RDB)

Redis 可以通过创建快照来获得存储在内存⾥⾯的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进⾏备份,可以将快照复制到其他服务器从⽽创建具有相同数据的服务器副本(Redis 主从结构,主要⽤来提⾼ Redis 性能),还可以将快照留在原地以便重启服务器的时候使⽤。

快照持久化是 Redis 默认采⽤的持久化⽅式,在 Redis.conf 配置⽂件中默认有此下配置:

save 900 1 #在900(15分钟)之后,如果⾄少有1个key发⽣变化,Redis就会⾃动触发
BGSAVE命令创建快照。
save 300 10 #在300(5分钟)之后,如果⾄少有10个key发⽣变化,Redis就会⾃动触发
BGSAVE命令创建快照。
save 60 10000 #在60(1分钟)之后,如果⾄少有10000个key发⽣变化,Redis就会⾃动
触发BGSAVE命令创建快照。

AOF(append-only file)持久化

与快照持久化相⽐,AOF 持久化 的实时性更好,因此已成为主流的持久化⽅案。默认情况下Redis 没有开启 AOF(append only file)⽅式的持久化,可以通过 appendonly 参数开启:

appendonly yes

开启 AOF 持久化后每执⾏⼀条会更改 Redis 中的数据的命令,Redis 就会将该命令写⼊硬盘中的 AOF ⽂件。AOF ⽂件的保存位置和 RDB ⽂件的位置相同,都是通过 dir 参数设置的,默认的⽂件名是 appendonly.aof。

在 Redis 的配置⽂件中存在三种不同的 AOF 持久化⽅式,它们分别是:

appendfsync always #每次有数据修改发⽣时都会写⼊AOF⽂件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步⼀次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进⾏同步

为了兼顾数据和写⼊性能,⽤户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步⼀次AOF ⽂件,Redis 性能⼏乎没受到任何影响。⽽且这样即使出现系统崩溃,⽤户最多只会丢失⼀秒之内产⽣的数据。当硬盘忙于执⾏写⼊操作的时候,Redis 还会优雅的放慢⾃⼰的速度以便适应硬盘的最⼤写⼊速度。

12. Redis 事务

Redis 可以通过 MULTIEXECDISCARDWATCH 等命令来实现事务(transaction)功能。

事务一般都遵守 ACID原则;即 原子性、一致性、隔离性、持久性。

Redis中的事务是不包括原子性的,Redis中的事务也没有隔离级别的概念。既然如此,就不可能会存在 脏读、幻读、不可重复读等一系列问题了。

Redis不支持回滚

使⽤ MULTI命令后可以输⼊多个命令。Redis不会⽴即执⾏这些命令,⽽是将它们放到队列,当调⽤了EXEC命令将执⾏所有命令。

详情请参考:https://juejin.cn/post/6932447503816245262

13. 缓存穿透

13.1 什么是缓存穿透

⼤量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这⼀层。

举个例⼦:某个⿊客故意制造我们缓存中不存在的 key 发起⼤量请求,导致⼤量请求落到数据库。

13.2 有哪些解决办法?

最基本的就是⾸先做好参数校验,⼀些不合法的参数请求直接抛出异常信息返回给客户端。⽐如查询的数据库 id 不能⼩于 0、传⼊的邮箱格式不对的时候直接返回错误消息给客户端等等。

1)缓存无效 key

如果缓存和数据库都查不到某个 key 的数据就写⼀个到 Redis 中去并设置过期时间,具体命令如下: SET key value EX 10086 。这种⽅式可以解决请求的 key 变化不频繁的情况,如果⿊客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存⼤量⽆效的 key 。很明显,这种⽅案并不能从根本上解决此问题。如果⾮要⽤这种⽅式来解决穿透问题的话,尽量将⽆效的 key 的过期时间设置短⼀点⽐如 1 分钟

⼀般情况下我们是这样设计 key 的:表名:列名:主键名:主键值。如果⽤ Java 代码展示的话,差不多是下⾯这样的:

public Object getObjectInclNullById(Integer id) {
	 // 从缓存中获取数据
	 Object cacheValue = cache.get(id);
	 // 缓存为空
	 if (cacheValue == null) {
		 // 从数据库中获取
		 Object storageValue = storage.get(key);
		 // 缓存空对象
		 cache.set(key, storageValue);
		 // 如果存储数据为空,需要设置⼀个过期时间(300秒)
		 if (storageValue == null) {
			 // 必须设置过期时间,否则有被攻击的⻛险
			 cache.expire(key, 60 * 5);
		 }
		 return storageValue;
	 }
	 return cacheValue;
}

2)布隆过滤器

布隆过滤器是⼀个⾮常神奇的数据结构,通过它我们可以⾮常⽅便地判断⼀个给定数据是否存在于海量数据中。

具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当⽤户请求过来,先判断⽤户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会⾛下⾯的流程。

Java面试题-Redis_第3张图片

14. 缓存雪崩

14.1 什么是缓存雪崩

缓存在同⼀时间⼤⾯积的失效,后⾯的请求都直接落到了数据库上,造成数据库短时间内承受⼤量请求。

14.2 有哪些解决办法?

针对 Redis 服务不可用的情况:

  1. 采⽤ Redis 集群,避免单机出现问题整个缓存服务都没办法使⽤。
  2. 限流,避免同时处理⼤量的请求。

针对热点缓存失效的情况:

  1. 设置不同的失效时间⽐如随机设置缓存的失效时间。
  2. 缓存永不失效。

15. 缓存击穿

15.1 什么是缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

15.2 有哪些解决办法?

  1. 设置热点数据永远不过期。
  2. 加互斥锁,互斥锁

16. 缓存预热

16.1 什么是缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

16.2 有哪些解决办法?

  1. 直接写个缓存刷新页面,上线时手工操作一下;
  2. 数据量不大,可以在项目启动的时候自动进行加载;
  3. 定时刷新缓存;

17. 缓存降级

17.1 什么是缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

17.2 有哪些解决办法?

  1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

特此声明:内容是摘抄一些大佬的笔记来学习的。
需要更深入的了解请参考:https://blog.csdn.net/ThinkWon/article/details/103522351

你可能感兴趣的:(Java面试题,Java面试题,redis,缓存)