面试准备系列——Java基础技术篇(15)/Java常用框架篇之Redis

redis更全面的知识可以参考:redis

1.redis有了解吗?

redis(Remote Dictionary Server远程字典服务),是一款高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库。因为数据都在内存中,所以运行速度快。redis支持丰富的数据类型并且支持事务,事务中的所有命令会被序列化、按顺序执行,在执行的过程中不会被其他客户端发送来的命令打断。

解析:

这是对redis考察的一个试探性提问,应聘者可以根据自己的理解与掌握,先从整体上对redis进行一个阐述。针对redis基础知识的考察,面试官还可以选择继续追问如下的常见知识点。

(重点)redis相比memcached有哪些优势?

  • memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型。
  • redis的速度比memcached快很多,并且redis支持数据的持久化。
  • redis支持数据的备份,即master-slave模式的数据备份。
  • 使用底层模型不同,它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。redis自己构建了VM 管理机制。
  • value大小不同,redis最大可以达到512MB,而memcache只有1MB。

(重点)redis都支持哪些数据类型?应用场景有哪些?

redis支持五种数据类型作为其Value,redis的Key都是字符串类型的。

  • string: redis 中字符串 value 最大可为512M。可以用来做一些计数功能的缓存(也是实际工作中最常见的)。
  • list:简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部(左边)或者尾部(右边),其底层实现是一个链表。可以实现一个简单消息队列功能,做基于redis的分页功能等。
  • set:是一个字符串类型的无序集合。可以用来进行全局去重等。
  • sorted set:是一个字符串类型的有序集合,给每一个元素一个固定的分数score来保持顺序。可以用来做排行榜应用或者进行范围查找等。
  • hash:键值对集合,是一个字符串类型的 Key和 Value 的映射表,也就是说其存储的Value是一个键值对(Key- Value)。可以用来存放一些具有特定结构的信息。

其实redis还支持三种特殊的数据类型,分别为BitMap、Geo和HyperLogLog,感兴趣的同学可以自行学习。

一般情况下,可以认为redis的支持的数据类型有上述五种,其底层数据结构包括:简单动态字符串,链表,字典,跳表,整数集合以及压缩列表。

redis的配置文件有了解吗?

下载并且解压redis之后,在解压目录下有个配置文件 redis.windows.conf(大家可以自行查阅),在配置文件中对redis进行了分模块配置,常用的模块如下:

  • NETWORK:该模块可以配置一些redis服务器地址,端口以及超时时间等
  • GENERAL:该模块可以对日志文件的路径和日志级别等进行配置
  • SNAPSHOTTING: redis持久化配置信息等
  • REPLICATION: redis集群配置等信息
  • MEMORY MANAGEMENT:内存管理,包括数据过期删除策略信息的设置
  • APPEND ONLY MODE:日志持久化方式信息设置

2.(重点)redis是单线程的吗?为什么执行速度这么快?

redis是单线程的,redis的单线程是指网络请求模块使用了一个线程,所以不需考虑并发安全性。但是对于需要依赖多个操作的复合操作来说,还是需要锁的,而且有可能是分布式锁。

那么单线程的redis为什么执行速度如此之快?

  • 基于内存实现,完全内存计算
  • 单线程操作,避免了线程上下文切换操作
  • 多路I/O复用的线程模型,实现了一个线程监控多个IO流,及时响应请求
  • redis对外部的依赖比较少,属于轻量级内存数据库

解析:
redis的线程模型多路I/O复用机制是一个比较重要并且常见的考察点。目前支持I/O多路复用的系统调用有select,pselect,poll,epoll等函数。I/O多路复用就是通过一种机制一个进程可以监视多个描述符,一旦某个描述符读就绪或者写就绪,其能够通知应用程序进行相应的读写操作。

多路I/O复用机制与多进程和多线程技术相比系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

关于常见函数的特点如下所示:

  • select函数:

    • 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
    • 有最大监听连接数1024个的限制
    • 如果任何一个sock(I/O stream)出现了数据,select没有返回具体是哪个返回了数据,需要采用轮询的方式去遍历获取
    • 线程不安全(当你在一个线程中已经监听该socket,另一个线程想要将该socket关闭,则结果会不可预知)
    • “If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
  • poll函数:

    • 去掉了1024的限制(使用链表搞定)
    • 不再修改传入的参数数组
    • 依然是线程不安全的
  • epoll函数:

    • epoll 不仅返回socket组里面数据,还可以确定 具体哪个socket有数据
    • 线程安全

题目总结:

限于篇幅,在这个题目中,我们仅仅是简单对线程模型多路I/O复用机制做了一个阐述与介绍,有能力的同学可以继续研究其具体实现机制,这个题目是面试中的一个很好的加分项(如果大家可以阐述清楚)

3.使用redis可能出现的问题

在这里我们主要介绍Redis可能出现的四个问题,如下所示:

缓存雪崩:
举例:缓存同一时间大面积的失效,这个时候又来的一波请求都到数据库上,导致数据库连接异常。

解决办法:可以给缓存设置不同的缓存时间,更新数据使用互斥锁或者通过双缓存在避免缓存雪崩。

缓存击穿:
举例:redis中存储的是热点数据,当高并发请求访问redis中热点数据的时候,如果redis中的数据过期了,会造成缓存击穿的现象,请求都打到了数据库上。

解决办法:使用互斥锁,只让一个请求去load DB,成功之后重新写缓存,其余请求没有获取到互斥锁,可以尝试重新获取缓存中的数据。。

缓存穿透:
举例:故意的去请求缓存中不存在的数据,导致请求都打到了数据库上,导致数据库异常。

解决办法:可以使用互斥锁或者无论是否取到结果都将结果存入缓存,还可以使用有效的机制(比如布隆过滤器)来拦截不合法的key值等。

数据库和缓存的双写一致性问题:
在高并发请求下很容易导致数据不一致的问题,如果你的业务需要保证数据的强一致性,那么建议不要使用缓存。在数据库中和缓存数据的删除或者写入过程中,如果有失败的情况,会导致数据的不一致。

解决办法:

  • 双删延时的解决办法。可以先删除缓存数据,然后再更新数据库数据,最后再隔固定的时间再次删除缓存。
  • 更新数据库产生的binlog订阅(使用canal)。将有变化的key记录下来,并且尝试去不断的去删除缓存(如果上次删除缓存失败)

4.(重点)redis的持久化方式有哪些?

在redis中的介绍中,我们知道了redis不光是一个基于内存的缓存,同时还支持数据的持久化。在redis的配置文件模块介绍中,我们可以设置redis的持久化方式。redis的持久化方式有两种,即RDB和AOF的方式,分别介绍如下:

RDB(快照方式 snapshotting)(全量持久化):

将当前内存中的数据集快照写入磁盘,实现数据的持久化,恢复时可以将快照重新载入内存。

触发方式:

  • 自动触发:在配置文件中,可以配置执行了多少次save就自动触发自动持久化。
  • 手动触发:通过bgsave命令,在后台异步进行生成快照的操作,同时还可以响应客户端的请求。通过redis进程fork操作创建子进程,生成的快照由子进程负责,客户端请求只会在fork阶段被阻塞。

快照恢复:将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务,redis会自动加载快照文件数据到内存。但是,redis 服务器在载入 RDB 文件期间,会一直处于阻塞状态,直到载入工作完成为止。

优缺点分析:

  • RDB持久化方式存在数据的丢失,因为其没有办法实现实时持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高,会影响系统性能。自动触发也存在丢失部分数据的情况。
  • 在恢复大数据集时候,RDB方式相对于AOF要快。

AOF(append-only-file)(增量持久化):

在 redis配置文件的 APPEND ONLY MODE 中,可以设置AOF持久化。通过记录redis服务器所执行的写命令来记录数据库状态。恢复时可以将AOF文件载入内存,并且可以通过redis-check-aof --fix 进行修复AOF文件。

AOF日志重写:

  • AOF文件会随着服务器运行的时间越来越大,可以通过AOF重写来控制AOF文件的大小。
  • AOF重写会首先读取数据库中现有的键值对状态,然后根据类型使用一条命令来替代前面对键值对操作的多条命令。
  • 使用命令 bgrewriteaof 来实现AOF重写

AOF重写缓存区:
redis 是单线程工作,当AOF文件较大时重写时间会比较长,在重写 AOF 期间,redis将长时间无法处理客户端请求。为了解决这个问题,可以将 AOF 重写程序放到子进程中执行,好处如下:

  • 子进程进行 AOF 重写期间,服务器进程(父进程)可以继续处理其它客户端请求。
  • 子进程带有父进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。

子进程中AOF重写导致的问题:

  • 子进程在进行 AOF 重写期间,服务器进程依然可以处理其它客户端请求,这就会导致数据库状态已经发生了改变,使得当前数据库数据状态和重写后的 AOF 文件中的数据不一致。
  • 也就是出现了AOF文件和数据库中数据不一致的问题。

数据状态不一致解决办法:

  • redis 服务器设置了一个 AOF 重写缓冲区。这个缓冲区在创建子进程后开始使用,当redis服务器执行一个客户端的写请求命令,之后将这个写命令也发送到 AOF 重写缓冲区。
  • 当子进程完成 AOF 日志重写之后,给父进程发送信号,父进程接收此信号后,将 AOF 重写缓冲区的内容写到新的 AOF 文件中,保持数据的一致性。

优缺点分析:

  • AOF文件可以做到秒级持久化,使用追加写的方式来写入,可读性强并且可以使用命令进行文件修复。
  • 相比于RDB文件,同样数据下AOF文件体积要大。在redis负载较高时,秒级更新AOF文件会影响性能

持久化策略选择:

  • AOF更安全,可将数据及时同步到文件中,但需要较多的磁盘IO,AOF文件尺寸较大,文件内容恢复相对较慢也更加完整。
  • RDB持久化,安全性较差,它是正常时期数据备份及 master-slave数据同步的最佳手段,文件尺寸较小并且恢复速度较快。

解析:

持久化策略是redis相关知识点中一个相当常见的面试考察点,主要是两种持久化方式的比较和选择,希望大家可以有效掌握与理解。

5.redis数据的过期回收策略与内存淘汰机制

redis中的数据过期回收策略使用了定期删除和惰性删除相结合的方式。

  • 定期删除:
    redis会每隔一定的时间去抽查一定量的数据判断其是否过期,过期则进行删除。

  • 惰性删除:
    在获取一个key的时候,redis会检查这个key是否已经过期,若过期,则会进行删除操作。

内存淘汰机制:

在配置文件中,我们可以对内存淘汰机制进行配置。当内存使用达到最大值时,redis可以使用的清除策略如下:

  • volatile-lru:利用LRU算法移除设置过过期时间的key (LRU:最近使用 Least Recently Used )
  • allkeys-lru:利用LRU算法移除任何key
  • volatile-random:移除设置过过期时间的随机key
  • allkeys-random:移除随机key
  • volatile-ttl:移除即将过期的key(minor TTL)
  • noeviction :不移除任何key,只是返回一个写错误 ,默认选项

6.redis的主从复制机制

当项目比较大的时候,我们可以使用主从架构Master/Slave机制,Master 以写为主,Slave 以读为主,Master 主节点更新后根据配置,自动同步到从机Slave 节点。

主从复制的原理包括旧版同步和命令传播,主从复制的代价就是系统复制较重的时候会导致主从延迟,并且根据CAP理论,无法同时保证服务可用性和数据一致性。

解析:

主从复制属于较为复杂的知识点,限于篇幅,我们就不展开进行介绍了,有能力和兴趣的同学可以自行去学习其相关原理。在这里我们来简单看下分布式系统中著名的CAP理论吧。

什么是CAP理论?

CAP理论是指 当网络分区发生时,一致性和可用性不可能同时保证。

  • C:Consistent 一致性
  • A:Availability 可用性
  • P:Partition tolerance 分区容忍度
  • 网络分区:分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有网络断开的风险,网络断开也就意味着发生了网络分区。
  • 最终一致性:Redis可以保证最终一致性,从节点会努力追赶主节点,最终从节点的状态会和主节点的状态将保持一致。

7.redis对事务支持

redis对事务的支持主要可以概括如下:

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

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

Redis事务没有隔离级别的概念:

批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。

Redis不保证原子性:

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

Redis事务的三个阶段:

  • 开始事务
  • 命令入队
  • 执行事务

事务相关命令

redis操作事务的相关命令如下所示:

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

需要注意的是redis的事务不支持回滚操作,redis以 MULTI 开始一个事务,然后将多个命令入队到事务中,最后由 EXEC 命令触发事务, 一并执行事务中的所有命令。只有当被调用的redis命令有语法错误时,这条命令才会执行失败,或者对某个键执行不符合其数据类型的操作,但是应该在将命令入队列的时候就应该并且能够发现这些问题,所以redis的事务不支持进行回滚操作。

redis事务正常使用的一个案例:
在这里插入图片描述

8.redis集群具体怎么搭建?怎么解决集群中的主从不同步问题?

1.常见的redis数据库集群架构如何?

答:一主多从,主从同步,读写分离。

在这里插入图片描述

如上图:

(1)一个主库提供写服务

(2)多个从库提供读服务,可以增加从库提升读性能

(3)主从之间同步数据

画外音:任何方案不要忘了本心,加从库的本心,是提升读性能。

2.为什么会出现不一致?

答:主从同步有时延,这个时延期间读从库,可能读到不一致的数据。
在这里插入图片描述

如上图:

(1)服务发起了一个写请求

(2)服务又发起了一个读请求,此时同步未完成,读到一个不一致的脏数据

(3)数据库主从同步最后才完成

画外音:任何数据冗余,必将引发一致性问题。

3.如何避免这种主从延时导致的不一致?

答:常见的方法有这么几种。

  • 方案一:忽略

任何脱离业务的架构设计都是耍流氓,绝大部分业务,例如:百度搜索,淘宝订单,QQ消息,58帖子都允许短时间不一致。

画外音:如果业务能接受,最推崇此法。
如果业务能够接受,别把系统架构搞得太复杂

  • 方案二:强制读主

在这里插入图片描述

如上图:

(1)使用一个高可用主库提供数据库服务

(2)读和写都落到主库上

(3)采用缓存来提升系统读性能

这是很常见的微服务架构,可以避免数据库主从一致性问题。

  • 方案三:选择性读主

强制读主过于粗暴,毕竟只有少量写请求,很短时间,可能读取到脏数据。

有没有可能实现,只有这一段时间,可能读到从库脏数据的读请求读主,平时读从呢?

可以利用一个缓存记录必须读主的数据。
在这里插入图片描述

如上图,当写请求发生时:

(1)写主库

(2)将哪个库,哪个表,哪个主键三个信息拼装一个key设置到cache里,这条记录的超时时间,设置为“主从同步时延”

画外音:key的格式为“db:table:PK”,假设主从延时为1s,这个key的cache超时时间也为1s。

在这里插入图片描述

如上图,当读请求发生时:

这是要读哪个库,哪个表,哪个主键的数据呢,也将这三个信息拼装一个key,到cache里去查询,如果,

(1)cache里有这个key,说明1s内刚发生过写请求,数据库主从同步可能还没有完成,此时就应该去主库查询

(2)cache里没有这个key,说明最近没有发生过写请求,此时就可以去从库查询。以此,保证读到的一定不是不一致的脏数据。

总结

数据库主库和从库不一致,常见有这么几种优化方案:

(1)业务可以接受,系统不优化

(2)强制读主,高可用主库,用缓存提高读性能

(3)在cache里记录哪些记录发生过写请求,来路由读主还是读从

9.使用redis如何实现分布式锁

一般来说,实现分布式锁主要有三种方式:1.数据库的乐观锁;2.基于Redis的分布式锁;3.基于zookeeper的分布式锁。

为了确保分布式锁的可用性,我们至少要确保锁同时满足一下四个条件:

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

单机部署的环境下,要想使用redis实现分布式锁,可以使用redis的客户端工具,jedis完成。例如:

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
    
}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

  • 第一个为key,我们使用key来当锁,因为key是唯一的。

  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

解锁同样是使用jedis中的工具,加一行lua代码解释即可。

如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件。

10.介绍一下集群中的哨兵模式

参考:哨兵模式
哨兵模式是redis高可用的实现方式之一
使用一个或者多个哨兵(Sentinel)实例组成的系统,对redis节点进行监控,在主节点出现故障的情况下,能将从节点中的一个升级为主节点,进行故障转义,保证系统的可用性。
在这里插入图片描述

集群模式
官方提供的分布式方案(槽指派/重新分片/故障转移)
集群内的节点,都会有个数据结构存储整个集群内的节点信息。

集群模式和哨兵模式的区别

  • 哨兵模式监控权交给了哨兵系统,集群模式中是工作节点自己做监控
  • 哨兵模式发起选举是选举一个leader哨兵节点来处理故障转移,集群模式是在从节点选举一个新的主节点,来处理故障的转移。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值