Java面试突击每日十题【Day06】——面试高频必问

Java面试突击每日十题【Day06】——面试高频必问,第1张

  1. 说说I/O多路复用模型?
  2. 怎么解决mq消息堆积?
  3. 怎么保证mq消息不丢失?
  4. 怎么保证mq消息幂等性?
  5. 怎么保证mq消息顺序性?
  6. 说说Redis的缓存预热?
  7. 说说Redis的缓存雪崩、缓存击穿、缓存穿透及解决方案?
  8. 什么时候建立索引?
  9. 说说explain?explin需要注意哪些字段?type字段有哪些值?ref是什么级别?
  10. 什么时候索引会失效?

一、首先需要知道两个概念socket和fd(文件描述符),socket直接翻译套接字/插口,什么玩意,用人话来说就是客户端和服务端都开了一个口子,这个口子用来接收数据和发送数据,这个口子就是socket。
Linux中一切皆文件,一切资源都能以文件的方式访问和管理。文件描述符字面意义就是用来描述文件的符号,它类似文件的索引,指向某个资源。socket实际上也是一个文件,也会对应一个文件描述符。

I/O多路复用,看这几个字直接不理解了,最后找到一句话暂时还算满意:I/O就是我们网络I/O,多路指的是多个TCP连接(或多个Channel),复用指复用一个或者少量线程。连在一块就是很多个网络I/O复用一个或者少量的线程来处理这些连接。

常见的IO模型有四种:

  1. 同步阻塞IO(Blocking IO):即传统的IO模型。
  2. 同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库。
  3. IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。
  4. 异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。

最开始Linux是同步阻塞IO,实现原理采用单线程,这样当某个socket阻塞的时候,会影响其他socket处理,这样就都处于阻塞状态了;这时候就考虑使用多线程是不是好点,当客户端较多时,会造成资源的浪费,因为全部的socket可能在某一个时刻只有几个就绪,一些线程就会白白的等着,而线程的调度、上下文的切换及占用的资源,都可能达到瓶颈。既然多线程也不行那能不能进行线程的复用,于是就有了同步非阻塞I/O,线程一直轮询socket是否就绪,就算没有就绪也会立即返回,这样就避免了阻塞。但是这需要不断的遍历进行系统调用,开销较大。紧接着出现了I/O多路复用模型select,将socket是否就绪逻辑通过 *** 作系统来完成避免了大量的系统调用,会告诉你有事件就绪,但没有告诉你是哪个FD。优点是不需要每个FD都进行一次系统调用,解决了频繁的用户态和内核态的切换问题,缺点是单线程监听的FD存在限制,默认1024,每次调用都需要将FD从用户态拷贝到内核态再拷贝回去,不知道具体是哪个文件描述符就绪,需要遍历所有的文件描述符。poll优化了监听1024个文件描述符的限制,但还是需要将文件描述符从用户态拷贝到内核态的来回拷贝,不知道具体是哪个文件描述符就绪,需要遍历全部的文件描述符。epoll解决了select和poll的拷贝问题,和文件描述符会告知哪个准备就绪,不需要全部遍历,epoll首先会通过epoll_create在内核创建一个epoll对象,然后通过epoll_ctl函数把带监控的socket加入到内核的红黑树中,epoll在内核使用红黑树来跟踪进程所有待检测的文件描述符,同时维护了一个链表来记录就绪事件的链表,当某个socket有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用epoll_wait函数时,只会返回有事件发生的文件描述符返回,不需要像select/poll那样轮询扫描整个socket集合。查了一下资料并没有共享内存这个东西。

epoll支持两种事件触发模式,分别是边缘触发和水平触发。
使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
两者的区别:边缘触发是发一次,一次发完。水平触发是,持续发。

目前Linux还不支持AIO,Windows支持。

二、消息堆积主要是生产方生产消息远超过消费方的承受能力这时候我们可以:
工作模型:对消费者搭建集群。
开启多线程消费:spring.rabbitmq.listener.simple.concurrency=4
能者多劳:channel.basicQos(1) 或者 spring.rabbitmq.listener.simple.prefetch=1

三、保证mq消息不丢失,RabbitMq举例:

  1. 生产者确认:spring.rabbitmq.publisher-confirm-type=correlated/simple/none
    ​ spring.rabbitmq.publisher-returns=true;​ java配置​ rabbitTemplate.setConfirmCallback()​ rabbitTemplate.setReturnCallback()

  2. 消息持久化:交换机持久化(默认) 队列的持久化 消息持久化(默认),主要将队列设置持久化

  3. 消费者确认:原生代码:自动确认(ACK) 手动ACK(channel.basicAck()),​ springRabbit:三种模式​ 自动确认模式:消费者监听器正常执行,则确认消息;消费者监听器执行异常,无线重试。​ 不确认模式:相当于原生中的自动确认,不推荐​ ,手动确认模式:​ channel.basicAck​ channel.basicNack​ channel.basicReject

四、让每个消息携带一个全局的唯一ID比如(UUID),即可保证消息的幂等性,具体消费过程为:

消费者获取到消息后先根据Id去查询redis/db是否存在该消息;
如果不存在,则正常消费,消费完毕后将该ID写入redis/db。
如果存在,则证明消息被消费过,直接丢弃。
如果需要存入db的话,可以直接将这个ID设为消息的主键,下次如果获取到重复消息进行消费时,由于数据库主键的唯一性,则会直接抛出异常。

五、保证消息的有序性:
RabbitMQ:

  1. 在 MQ 里面创建多个 queue,同一规则的数据(对唯一标识进行 hash),有顺序的放入 MQ 的 queue 里面,消费者只取一个 queue 里面获取数据消费,这样执行的顺序是有序的。
  2. 或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。

Kafka:

  1. 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。
  2. 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。

六、Redis的缓存预热:

  1. 提前把数据放入redis(你知道哪些是热数据吗?肯定不知道,可能会造成很多数据没有缓存命中)
  2. 开发逻辑上也要规避差集(没有进行缓存的)。
  3. 我遇到过缓存预热没有覆盖全面引起的缓存穿透、缓存击穿、缓存雪崩,实施这几个问题的解决方案即可解决。

七、

  1. 缓存雪崩:缓存时间相同导致大量的缓存数据同时过期,此时大量请求进来就会直达数据库,导致数据库宕机。
    解决方案:给缓存时间设置一个随机值,也可以热点数据永不过期,如果有数据更新直接更新redis即可。
  2. 缓存击穿:一个热点key过期,此时大量请求同时访问该数据,就会直达数据库,导致数据库宕机。
    解决方案:设置这个key永不过期,加JVM锁或者分布式锁,缓存预热时尽量全面覆盖热点key,可以服务器熔断、降级,限制每个用户的访问次数,并且如果获取不到数据即返回一个固定的推荐页面。
    notes:当然这几个也可能是缓存预热时没有缓存到这些数据,所以缓存预热时应尽可能覆盖这些热点数据。
  3. 缓存穿透:大量的请求同时访问不存在的数据,就会越过缓存到达数据库,导致数据库宕机。(比如说:根据产品id获取产品详情的接口,那么我用一个id=-1去请求,那肯定是获取不到任何数据的。一般这样的情况有可能服务器遭到了恶意攻击。)
    解决方案:数据即使为null也缓存,(缓存key–>null)、使用布隆过滤器,也可以在请求接口层增加一些校验,例如:用户鉴权校验、参数传值校验等不合法的参数请求直接返回。

八、虽然索引可以加快查询速度,提高 MySQL 的处理性能,但是过多地使用索引也会造成以下弊端:

创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。 除了数据表占数据空间之外,每一个索引还要占一定的物理空间。如果要建立聚簇索引,那么需要的空间就会更大。 当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。 注意:索引可以在一些情况下加速查询,但是在某些情况下,会降低效率。 索引只是提高效率的一个因素,因此在建立索引的时候应该遵循以下原则:

在经常需要搜索的列上建立索引,可以加快搜索的速度。 在作为主键的列上创建索引,强制该列的唯一性,并组织表中数据的排列结构。 在经常使用表连接的列上创建索引,这些列主要是一些外键,可以加快表连接的速度。 在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,所以其指定的范围是连续的。 在经常需要排序的列上创建索引,因为索引已经排序,所以查询时可以利用索引的排序,加快排序查询。 在经常使用 WHERE 子句的列上创建索引,加快条件的判断速度。 现在大家知道索引为啥能这么快了吧,其实就是一句话,通过索引的结构最大化的减少数据库的IO次数,毕竟,一次IO的时间真的是太久了
九、通过解析函数explain,进行分析,type显示的是访问类型一般来说得保证查询至少达到range级别,最好能达到此ref(常见:system > const > eq_ref > ref > range > index > ALL ),key表示实际使用到的索引,如果为null则没有使用索引。
十、索引失效情形:

  1. 在索引列上 *** 作(计算、函数、(自动or手动,比如使用字符串类型不加单引号,MySQL底层会做自动类型转换)类型转换),会导致索引失效而转向全表扫描。
  2. like以通配符开头(’%abc…’)mysql索引失效会变成全表扫描的 *** 作
  3. mysql 在使用**不等于(!=或者<>)**的时候无法使用索引会导致全表扫描
  4. is not null 也无法使用索引,但是is null是可以使用索引的
  5. 字符串不加单引号索引失效
    扩展:其实IS NOT NULL、!=这些条件说索引失效是不准确的,使不使用依据是根据花费的成本,成本有两个方面组成:读取二级索引记录的成本,将二级索引记录执行回表 *** 作,也就是到聚簇索引中找到完整的用户记录的 *** 作所付出的成本。成本小就使用。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/904858.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-15
下一篇 2022-05-15

发表评论

登录后才能评论

评论列表(0条)

保存