Java面试题(六)--Redis

1 Redis 基础篇

1、简单介绍一下 Redis 优点和缺点?

优点:

1、本质上是一个 Key-Value 类型的内存数据库,很像 memcached

2、整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬盘上进行保存

3、因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value DB

4、Redis 最大的魅力是支持保存多种数据结构 (string,list,set,hash,sortedset),此外单个 value 的最大限制是 1GB,不像 memcached 只能保存 1MB 的数据

5、Redis 也可以对存入的 Key-Value 设置 expire 时间,因此也可以被当作一个功能加强版的 memcached 来用

缺点:

1、Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。

2、没有丰富的搜索功能

2、系统中为什么要使用缓存?

主要从“高性能”和“高并发”这两点来看待这个问题。

高性能:

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

高并发:

直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

3、常见的缓存同步方案都有哪些?(高频)

同步方案:更改代码业务代码,加入同步操作缓存逻辑的代码 (数据库操作完毕以后,同步操作缓存)

异步方案:

1、使用消息队列进行缓存同步:更改代码加入异步操作缓存的逻辑代码 (数据库操作完毕以后,将要同步的数据发送到 MQ 中,MQ 的消费者从 MQ 中获取数据,然后更新缓存)

2、使用阿里巴巴旗下的 canal 组件实现数据同步:不需要更改业务代码,部署一个 canal 服务。canal 服务把自己伪装成 mysql 的一个从节点,当 mysql 数据更新以后,canal 会读取 binlog 数据,然后在通过 canal 的客户端获取到数据,更新缓存即可。

4、Redis 常见数据结构以及使用场景有哪些?(高频)

1、 string

常见命令:set、get、decr、incr、mget 等。

基本特点:string 数据结构是简单的 key-value 类型,value 其实不仅可以是 String,也可以是数字。

应用场景:常规计数:微博数,粉丝数等。

2、hash

常用命令: hget、hset、hgetall 等。

基本特点:hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。

应用场景:存储用户信息,商品信息等。

3、list

常用命令: lpush、rpush、lpop、rpop、lrange 等。

基本特点:类似于 Java 中的 list 可以存储多个数据,并且数据可以重复,而且数据是有序的。

应用场景:存储微博的关注列表,粉丝列表等。

4、set

常用命令: sadd、spop、smembers、sunion 等

基本特点:类似于 Java 中的 set 集合可以存储多个数据,数据不可以重复,使用 set 集合不可以保证数据的有序性。

应用场景:可以利用 Redis 的集合计算功能,实现微博系统中的共同粉丝、公告关注的用户列表计算。

5、sorted set

常用命令: zadd、zrange、zrem、zcard 等。

基本特点:和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。

应用场景:在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜等。

5、Redis 有哪些数据删除策略?(高频)

数据删除策略:Redis 中可以对数据设置数据的有效时间,数据的有效时间到了以后,就需要将数据从内存中删除掉。而删除的时候就需要按照指定的规则进行删除,这种删除规则就被称之为数据的删除策略。

Redis 中数据的删除策略:

定时删除

  • 概述:在设置某个 key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。

  • 优点:定时删除对内存是最友好的,能够保存内存的 key 一旦过期就能立即从内存中删除。

  • 缺点:对 CPU 最不友好,在过期键比较多的时候,删除过期键会占用一部分 CPU 时间,对服务器的响应时间和吞吐量造成影响。

惰性删除

  • 概述:设置该 key 过期时间后,我们不去管它,当需要该 key 时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该 key。

  • 优点:对 CPU 友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的 key 不用浪费时间进行过期检查。

  • 缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果数据库中有很多这种使用不到的过期键,这些键便永远不会被删除,内存永远不会释放。

定期删除

  • 概述:每隔一段时间,我们就对一些 key 进行检查,删除里面过期的 key(从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键)。

  • 优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。

  • 缺点:难以确定删除操作执行的时长和频率。

    如果执行的太频繁,定期删除策略变得和定时删除策略一样,对 CPU 不友好。如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。

另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。

Redis 的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用定期删除函数的运行频率,在 Redis2.6 版本中,规定每秒运行 10 次,大概 100ms 运行一次。在 Redis2.8 版本后,可以通过修改配置文件 redis.conf 的 hz 选项来调整这个次数。

6、Redis 中有哪些数据淘汰策略?(高频)

数据的淘汰策略:当 Redis 中的内存不够用时,此时在向 Redis 中添加新的 key,那么 Redis 就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

常见的数据淘汰策略:

noeviction # 不删除任何数据,内存不足直接报错 (默认策略)
 volatile-lru # 挑选最近最久使用的数据淘汰 (举例:key1 是在 3s 之前访问的, key2 是在 9s 之前访问的,删除的就是 key2)
 volatile-lfu # 挑选最近最少使用数据淘汰 (举例:key1 最近 5s 访问了 4 次, key2 最近 5s 访问了 9 次, 删除的就是 key1)
 volatile-ttl # 挑选将要过期的数据淘汰
 volatile-random # 任意选择数据淘汰
 allkeys-lru # 挑选最近最少使用的数据淘汰
 allkeys-lfu # 挑选最近使用次数最少的数据淘汰
 allkeys-random # 任意选择数据淘汰,相当于随机

注意:

1、不带 allkeys 字样的淘汰策略是随机从 Redis 中选择指定的数量的 key 然后按照对应的淘汰策略进行删除,带 allkeys 是对所有的 key 按照对应的淘汰策略进行删除。

2、缓存淘汰策略常见配置项

maxmemory-policy noeviction # 配置淘汰策略
 maxmemory ?mb # 最大可使用内存,即占用物理内存的比例,默认值为 0,表示不限制。生产环境中根据需求设定,通常设置在 50% 以上。
 maxmemory-samples count # 设置 redis 需要检查 key 的个数

7、Redis 中数据库默认是多少个 db 即作用?

Redis 默认支持 16 个数据库,可以通过配置 databases 来修改这一数字。客户端与 Redis 建立连接后会自动选择 0 号数据库,不过可以随时使用 select 命令更换数据库。

Redis 支持多个数据库,并且每个数据库是隔离的不能共享,并且基于单机才有,如果是集群就没有数据库的概念。

8、缓存穿透、缓存击穿、缓存雪崩解决方案?(高频)

加入缓存以后的数据查询流程:

缓存穿透

概述:指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。

解决方案:

1、查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短

2、布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对 DB 的查询

缓存击穿

概述:对于设置了过期时间的 key,缓存在某个时间点过期的时候,恰好这时间点对这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。

解决方案:

1、使用互斥锁:当缓存失效时,不立即去 load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db 的操作并回设缓存,否则重试 get 缓存的方法

2、永远不过期:不要对这个 key 设置过期时间

缓存雪崩

概述:设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多 key,击穿是某一个 key 缓存。

解决方案:

将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

9、什么是布隆过滤器?(高频)

概述:布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上由一个很长的二进制向量 (二进制数组) 和一系列随机映射函数(hash 函数)。

作用:布隆过滤器可以用于检索一个元素是否在一个集合中。

添加元素:将商品的 id(id1) 存储到布隆过滤器

假设当前的布隆过滤器中提供了三个 hash 函数,此时就使用三个 hash 函数对 id1 进行哈希运算,运算结果分别为:1、4、9 那么就会数组中对应的位置数据更改为 1。

判断数据是否存在:使用相同的 hash 函数对数据进行哈希运算,得到哈希值。然后判断该哈希值所对应的数组位置是否都为 1,如果不都是则说明该数据

肯定不存在。如果是说明该数据可能存在,因为哈希运算可能就会存在重复的情况。如下图所示:

假设添加完 id1 和 id2 数据以后,布隆过滤器中数据的存储方式如上图所示,那么此时要判断 id3 对应的数据在布隆过滤器中是否存在,按照上述的判断规则应该是存在,但是 id3 这个数据在布隆过滤器中压根就不存在,这种情况就属于误判。

误判率:数组越小误判率就越大,数组越大误判率就越小,但是同时带来了更多的内存消耗。

删除元素:布隆布隆器不支持数据的删除操作,因为如果支持删除那么此时就会影响判断不存在的结果。

使用布隆过滤器:在谷歌的 guava 缓存工具中提供了布隆过滤器的实现,使用方式如下所示:

pom.xml 文件


     com.google.guava
     guava
     20.0
 

测试代码:

// 创建一个 BloomFilter 对象
 // 第一个参数:布隆过滤器判断的元素的类型
 // 第二个参数:布隆过滤器存储的元素个数
 // 第三个参数:误判率,默认值为 0.03
 int size = 100_000 ;
 BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), size, 0.03);
 for(int x = 0 ; x < size ; x++) {
     bloomFilter.put("add" + x) ;
 }
 ​
 // 在向其中添加 100000 个数据测试误判率
 int count = 0 ;     // 记录误判的数据条数
 for(int x = size ; x < size * 2 ; x++) {
     if(bloomFilter.mightContain("add" + x)) {
         count++ ;
         System.out.println(count + "误判了");
    }
 }
 ​
 // 输出
 System.out.println("总的误判条数为:" + count);

Redis 中使用布隆过滤器防止缓存穿透流程图如下所示:

10、Redis 数据持久化有哪些方式?各自有什么优缺点?(高频)

在 Redis 中提供了两种数据持久化的方式:1、RDB 2、AOF

RDB:定期更新,定期将 Redis 中的数据生成的快照同步到磁盘等介质上,磁盘上保存的就是 Redis 的内存快照

优点:数据文件的大小相比于 aof 较小,使用 rdb 进行数据恢复速度较快

缺点:比较耗时,存在丢失数据的风险

AOF:将 Redis 所执行过的所有指令都记录下来,在下次 Redis 重启时,只需要执行指令就可以了

优点:数据丢失的风险大大降低了

缺点:数据文件的大小相比于 rdb 较大,使用 aof 文件进行数据恢复的时候速度较慢

11、Redis 都存在哪些集群方案?

在 Redis 中提供的集群方案总共有三种:

1、主从复制

  • 保证高可用性

  • 实现故障转移需要手动实现

  • 无法实现海量数据存储

2、哨兵模式

  • 保证高可用性

  • 可以实现自动化的故障转移

  • 无法实现海量数据存储

  • 监控

  • 故障转移

  • 通知客户端

3、Redis 分片集群

  • 保证高可用性

  • 可以实现自动化的故障转移

  • 可以实现海量数据存储

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

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

13、Redis 中的管道有什么用?

一次请求 / 响应服务器能实现处理新的请求即使旧的请求还未被响应,这样就可以将多个命令发送到服务 器,而不用等待回复,最后在一个步骤中读取该答复。

14、谈谈你对 Redis 中事务的理解?(高频)

事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

Redis 中的事务:Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

15、Redis 事务相关的命令有哪几个?(高频)

事务相关的命令:

1、MULTI:用来组装一个事务

2、EXEC:执行一个事务

3、DISCARD:取消一个事务

4、WATCH:用来监视一些 key,一旦这些 key 在事务执行之前被改变,则取消事务的执行

5、UNWATCH:取消 WATCH 命令对所有 key 的监视

如下所示:

16、Redis 如何做内存优化?

尽可能使用散列表(hash),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。

比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key, 而是应该把这个用户的所有信息存储到一张散列表里面。

17、Redis 是单线的,但是为什么还那么快?(高频)

Redis 总体快的原因:

1、完全基于内存的

2、采用单线程,避免不必要的上下文切换可竞争条件

3、使用多路 I/O 复用模型,非阻塞 IO

2 分布式锁篇

18、什么是分布式锁?

概述:在分布式系统中,多个线程访问共享数据就会出现数据安全性的问题。而由于 jdk 中的锁要求多个线程在同一个 jvm 中,因此在分布式系统中无法使用 jdk 中的锁保证数据的安全性,那么此时就需要使用分布式锁。

作用:可以保证在分布式系统中多个线程访问共享数据时数据的安全性

举例:

在电商系统中,用户在进行下单操作的时候需要扣减库存。为了提高下单操作的执行效率,此时需要将库存的数据存储到 Redis 中。订单服务每一次生成订单之前需要查询一下库存数据,如果存在则生成订单同时扣减库存。在高并发场景下会存在多个订单服务操作 Redis,此时就会出现线程安全问题。

分布式锁的工作原理:

分布式锁应该具备哪些条件:

1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行

2、高可用的获取锁与释放锁

3、高性能的获取锁与释放锁

4、具备可重入特性

5、具备锁失效机制,防止死锁

可重入特性:获取到锁的线程再次调用需要锁的方法的时候,不需要再次获取锁对象。
 使用场景:遍历树形菜单的时候的递归调用。

注意:锁具备可重入性的主要目的是为了防止死锁。

19、分布式锁的实现方案都有哪些?(高频)

分布式锁的实现方案:

1、数据库

2、zookeeper

3、redis

20、Redis 怎么实现分布式锁思路?(高频)

Redis 实现分布式锁主要利用 Redis 的setnx命令。setnx 是 SET if not exists(如果不存在,则 SET) 的简写。

127.0.0.1:6379> setnx lock value1 #在键 lock 不存在的情况下,将键 key 的值设置为 value1
 (integer) 1
 127.0.0.1:6379> setnx lock value2 #试图覆盖 lock 的值,返回 0 表示失败
 (integer) 0
 127.0.0.1:6379> get lock #获取 lock 的值,验证没有被覆盖
 "value1"
 127.0.0.1:6379> del lock #删除 lock 的值,删除成功
 (integer) 1
 127.0.0.1:6379> setnx lock value2 #再使用 setnx 命令设置,返回 0 表示成功
 (integer) 1
 127.0.0.1:6379> get lock #获取 lock 的值,验证设置成功
 "value2"

上面这几个命令就是最基本的用来完成分布式锁的命令。

加锁:使用setnx key value命令,如果 key 不存在,设置 value(加锁成功)。如果已经存在 lock(也就是有客户端持有锁了),则设置失败 (加锁失败)。

解锁:使用del命令,通过删除键值释放锁。释放锁之后,其他客户端可以通过setnx命令进行加锁。

21、Redis 实现分布式锁如何防止死锁现象?(高频)

产生死锁的原因:如果一个客户端持有锁的期间突然崩溃了,就会导致无法解锁,最后导致出现死锁的现象。

所以要有个超时的机制,在设置 key 的值时,需要加上有效时间,如果有效时间过期了,就会自动失效,就不会出现死锁。然后加锁的代码就会变成这样。

22、Redis 实现分布式锁如何合理的控制锁的有效时长?(高频)

有效时间设置多长,假如我的业务操作比有效时间长?我的业务代码还没执行完就自动给我解锁了,不就完蛋了吗。

解决方案:

1、第一种:程序员自己去把握,预估一下业务代码需要执行的时间,然后设置有效期时间比执行时间长一些,保证不会因为自动解锁影响到客户端业务代码的执行。

2、第二种:给锁续期。

锁续期实现思路:当加锁成功后,同时开启守护线程,默认有效期是用户所设置的,然后每隔 10 秒就会给锁续期到用户所设置的有效期,只要持有锁的客户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁。

上述的第二种解决方案可以使用 redis 官方所提供的 Redisson 进行实现。

Redisson 是 Redis 官方推荐的 Java 版的 Redis 客户端。它提供的功能非常多,也非常强大分布式服务,使用 Redisson 可以轻松的实现分布式锁。Redisson 中进行锁续期的这种机制被称为 "看门狗" 机制。

redission 支持 4 种连接 redis 方式,分别为单机、主从、Sentinel、Cluster 集群。

23、Redis 实现分布式锁如何保证锁服务的高可用?(高频)

解决方案:

1、使用 Redis 的哨兵模式构建一个主从架构的 Redis 集群

2、使用 Redis Cluster 集群

24、当同步锁数据到从节点之前,主节点宕机了导致锁失效,那么此时其他线程就可以再次获取到锁,这个问题怎么解决?(高频)

使用 Redission 框架中的RedLock进行处理。

RedLock 的方案基于 2 个前提:

1、不再需要部署从库和哨兵实例,只部署主库

2、但主库要部署多个,官方推荐至少 5 个实例

也就是说,想使用 RedLock,你至少要部署 5 个 Redis 实例,而且都是主库,它们之间没有任何关系,都是一个个孤立的实例。

工作流程如下所示:

1、客户端先获取【当前时间戳 T1】

2、客户端依次向这个 5 个 Redis 实例发起加锁请求,且每个请求会设置超时时间 (毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败 (包括网络超时,锁被其他的人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁

3、如果客户端从 >=3 个 (大多数) 以上 Redis 实例加锁成功,则再次获取【当前时间戳 T2】, 如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则加锁失败

4、加锁成功,去操作共享资源

5、加锁失败,向【全部节点】发起释放锁请求

总结 4 个重点:

1、客户端在多个 Redis 实例上申请加锁

2、必须保证大多数节点加锁成功

3、大多数节点加锁的总耗时,要小于锁设置的过期时间

4、锁释放,要向全部节点发起释放锁请求

24.1 为什么要在多个实例上加锁?

本质上是为了【容错】, 部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。

24.2 为什么步骤 3 加锁成功后,还要计算加锁的累计耗时?

因为操作的是多个节点,所以耗时肯定会比操作单个实例耗时更久,而且,因为是网络请求,网络情况是复杂的,有可能存在延迟、丢包、超时等情况发生,网络请求越多,异常发生的概率就越大。所以,即使大多数节点加锁成功,如果加锁的累计耗时已经超过了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。

代码大致如下所示:

Config config1 = new Config();
 config1.useSingleServer().setAddress("redis://192.168.0.1:5378").setPassword("a123456").setDatabase(0);
 RedissonClient redissonClient1 = Redisson.create(config1);
 ​
 Config config2 = new Config();
 config2.useSingleServer().setAddress("redis://192.168.0.1:5379").setPassword("a123456").setDatabase(0);
 RedissonClient redissonClient2 = Redisson.create(config2);
 ​
 Config config3 = new Config();
 config3.useSingleServer().setAddress("redis://192.168.0.1:5380").setPassword("a123456").setDatabase(0);
 RedissonClient redissonClient3 = Redisson.create(config3);
 ​
 String resourceName = "REDLOCK_KEY";
 ​
 RLock lock1 = redissonClient1.getLock(resourceName);
 RLock lock2 = redissonClient2.getLock(resourceName);
 RLock lock3 = redissonClient3.getLock(resourceName);
 ​
 // 向 3 个 redis 实例尝试加锁
 RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
 boolean isLock;
 try {
     // isLock = redLock.tryLock();
     // 500ms 拿不到锁, 就认为获取锁失败。10000ms 即 10s 是锁失效时间。
     isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
     System.out.println("isLock ="+isLock);
     if (isLock) {
         //TODO if get lock success, do something;
    }
     
 } catch (Exception e) {
 } finally {
     // 无论如何, 最后都要解锁
     redLock.unlock();
 }


__EOF__

  • 本文作者: OLD SALTED FISH
  • 本文链接: https://www.cnblogs.com/xy1857/p/16557940.html
  • 关于博主: I am a good person
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。