温馨提示:距离2026年结束还剩556天,剩余约为152.33%

转载

Java开发高频面试题(中)

11. Java8大基础数据类型/5大数据库约束

Java基础数据类型:

  1. 整数型:byte占1个字节、short占2个字节、int占4个字节、long占8个字节。
  2. 浮点型:float 占4个字节、double占8个字节。
  3. 字符型:char 占2个字节。
  4. 布尔:boolean,字节型,占用1个字节。

包装类 全部占8个字节。 数据库有哪些约束: 主键约束、外键约束、唯一约束、非空约束、默认约束。

12. 拦截器和过滤器区别?执行流程?

12.1 区别

  1. 拦截器可以获取IOC容器中的各个bean,而过滤器就不行。
  2. 过滤器几乎对所有的请求都可以起作用,而拦截器只能对SpringMVC请求起作用。
  3. 请求先被过滤器拦截,再进入拦截器,最后进入控制层。
  4. 拦截器是Spring容器中的,而过滤器是tomcat的Servlet容器。

12.2 执行流程

  1. 过滤器:当一个客户端发送请求时,先被过滤器拦截处理,执行过滤器中的方法,如果通过了过滤器中的校验,则会继续处理该请求;否则,直接返回错误信息给客户端,中断后续操作。
  2. 拦截器:在请求到达控制器前,拦截器会先对请求进行处理,在拦截器中的preHandle()方法中,可以对请求进行拦截、处理等操作,如果拦截器放行,则继续进入控制器执行业务逻辑;在控制器执行完成后,会执行拦截器中的postHandle()方法和afterCompletion()方法进行处理。
  3. 在执行顺序上,过滤器在拦截器之前执行。

13. java中有哪些集合?有什么区别?

有list、set、map三种:

  1. List集合:List是一种有序的集合,可以存储重复的元素,常见的List集合有ArrayList、LinkedList和Vector。其中,ArrayList是基于数组实现的,支持随机访问和快速添加和删除元素;LinkedList是基于链表实现的,支持快速添加和删除元素,但是不支持随机访问;Vector是线程安全的List集合,但是性能相对较差。List集合通常用于存储需要保持顺序的数据。使用场景:存储需要保持顺序的数据,例如用户操作日志、消息队列等。实现栈、队列等数据结构,例如使用LinkedList实现队列、栈等。在集合中进行随机访问,例如需要通过索引获取某个元素。
  2. Set集合:Set是一种不允许重复元素的集合,常见的Set集合有HashSet、LinkedHashSet和TreeSet。其中,HashSet是基于哈希表实现的,元素的存储顺序不固定;LinkedHashSet是基于哈希表和链表实现的,元素的存储顺序与添加顺序一致;TreeSet是基于红黑树实现的,元素会按照自然顺序或者指定的比较器进行排序。Set集合通常用于去重和判断某个元素是否存在。使用场景:去重操作,例如过滤重复的数据、统计数据中不同元素的个数等。判断某个元素是否存在,例如判断用户是否已经登录、统计数据中某个元素出现的次数等。在使用迭代器遍历集合时,避免重复遍历相同的元素。
  3. Map集合:Map是一种键值对的集合,常见的Map集合有HashMap、LinkedHashMap和TreeMap。其中,HashMap是基于哈希表实现的,键值对的存储顺序不固定;LinkedHashMap是基于哈希表和链表实现的,键值对的存储顺序与添加顺序一致;TreeMap是基于红黑树实现的,键值对会按照键的自然顺序或者指定的比较器进行排序。Map集合通常用于快速查找和存储键值对的数据。使用场景:存储键值对的数据,例如存储用户信息、缓存数据、记录数据统计信息等。快速查找,例如根据键快速查找对应的值、查找最近更新的数据等。存储需要排序的键值对,例如使用TreeMap实现有序映射。

14. ThreadLocal 是一个什么样的技术?

14.1 ThreadLocal的实现原理

ThreadLocal通过在每个线程中维护一个ThreadLocalMap对象来实现线程隔离。ThreadLocalMap以ThreadLocal对象作为键,线程私有的变量副本作为值。每个线程都有自己的ThreadLocalMap,线程可以通过 ThreadLocal的 get()和set()方法来获取和设置自己线程的 ThreadLocal 变量的值。

14.2 应用场景

  1. 多线程环境下需要独立存储和获取数据的场景,例如线程池中的任务需要使用各自独立的数据库连接计数器等。
  2. 线程上下文传递信息,例如Web框架中将请求信息或用户登录信息存储在 ThreadLocal中,方便各层次方法调用时获取,避免了传递参数的麻烦。

14.3 坑与解决方法

14.3.1 内存泄漏问题

由于ThreadLocal的生命周期和线程的生命周期绑定,使用完ThreadLocal后,需要调用 remove()方法进行清理,避免内存泄漏。

解决方法

在使用完ThreadLocal后,在合适的地方调用remove()方法清理资源,可以使用try-finally语句块确保清理操作的执行,或者使用ThreadLocal的initialValue()方法设置初始值,这样在线程结束后会自动清理。

// 设置初始化方法
private static ThreadLocal<Object> threadLocal = ThreadLocal.withInitial(()-> {
    return "初始化值,类型要与泛型相同";
});
// 使用完ThreadLocal后,调用remove()方法清理
threadLocal.remove();

14.3.2 共享变量问题

如果多个线程共享了同一个ThreadLocal变量,可能会导致数据错乱或不确定的结果。每个线程应持有自己的ThreadLocal变量实例。

解决方法: 对于需要在多个线程之间共享变量的情况,应该创建多个ThreadLocal实例,每个线程持有自己的实例。

14.3.3 线程池使用时注意

在使用线程池时,需要特别小心 ThreadLocal 的使用,避免由于线程的重用而导致 ThreadLocal数据的混乱。

解决方法: 使用线程池时,应避免使用ThreadLocal变量或者在使用前后显式清理ThreadLocal变量,确保每次任务执行时 ThreadLocal的状态是干净的。

总的来说,使用ThreadLocal时需要注意其生命周期、清理和共享的问题,合理使用并及时清理ThreadLocal,可以避免潜在的问题发生。

14.4 最简回答

ThreadLocal是一种 Java技术,它允许在多线程环境中维护线程私有的变量副本。底层实现会使用一个类似于 Map的结构来存储每个线程的变量副本。ThreadLocal并不是强引用或弱引用,它使用弱引用作为键来维护各个线程的变量副本,但变量本身由线程强引用。在使用ThreadLocal时,可能会出现内存泄漏的问题。如果线程结束了,但ThreadLocal中的变量没有被手动清理,那么该变量会一直存在于 ThreadLocal的 Map 中,导致内存泄漏。解决这个问题的常见方式是在使用完ThreadLocal后调用remove()方法将变量从ThreadLocal中移除,或者使用Java8中的ThreadLocal的InitialValue方法来提供默认值。另外,也可以使用ThreadLocal的弱引用方式来解决内存泄漏问题,例如使用InheritableThreadLocal。

15. Redis数据类型有哪些?雪崩、击穿、穿透?

15.1 数据类型

数据类型 底层数据结构 使用场景
String 普通字符串,由单个char组成的集合。类型有embStr, int, raw 用于缓存、计数器、分布式锁等场景
Set 有序数组:存储的数据都为整数且数目不超过512个 散列数组:数据较大,满足不了有序数组后 可以进行集合运算,例如并集、交集、差集等操作常用于标签、好友列表等场景
Zset 压缩列表6以后为Dict即HashTable:单个数据(字符串或其他)小于64字节,个数小于512个跳表:当数据过大,不满足以上2个条件后 可以按照分数排序,也可以进行范围查询。常用于排行榜、计分系统等场景
List 压缩列表zipList: 列表中保存的单个数据(字符串或其他)小于64字节,列表中数据个数小于512个。双向循环链表: 当数据过大,不满足以上2个条件后。 可以进行队列和栈的操作常用于消息队列、任务队列等场景
Hash 压缩列表zipList: 列表中保存的单个数据(字符串或其他)小于64字节,列表中数据个数小于512个。散列表: 当数据过大,不满足以上2个条件后。 用于存储对象,例如用户信息、商品信息等

15.2 缓存问题

  1. 缓存穿透:当一个查询的数据在缓存中不存在,但在数据库中也不存在时,这个查询会穿透到数据库,导致数据库负载过高 解决方法使用布隆过滤器,将不存在于数据库的数据进行过滤;对于查询结果为空的 key,设置空值缓存,有效时间设置短一些。
  2. 缓存击穿:当一个热点数据过期时,大量的请求会同时访问数据库,导致数据库负载过高。解决方法有:设置热点数据永不过期,定期更新缓存。
  3. 缓存雪崩:当大量的缓存数据同时过期时,大量的请求会同时访问数据库,导致数据库负载过高 解决方法:给缓存的key设置随机过期时间,防止大量的缓存数据同时过期。

16. Redis key过期策略?内存淘汰机制?

16.1 过期策略

  • 基于惰性删除的策略:当访问一个已经过期的key时,Redis会立即将它删除。
  • 基于定期删除的策略:Redis会定期地检查所有key是否过期,将过期的key删除。
  • 基于内存淘汰的策略:当Redis的内存达到一定限制时,Redis会通过一些算法,将一些冷门的、不常使用的key删除,腾出空间。

16.2 淘汰策略:

  1. noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
  2. allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
  3. volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
  4. allkeys-random:加入键的时候如果过限,从所有key随机删除
  5. volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
  6. volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
  7. volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
  8. allkeys-lfu:从所有键中驱逐使用频率最少的键

16.3 简单记忆

  1. 最近最少使用(LRU)算法:淘汰最近最少使用的key。
  2. 最不经常使用(LFU)算法:淘汰最近使用次数最少的key。
  3. 随机淘汰算法:随机选择一个key进行淘汰。
  4. 根据ttl:生存时间 默认是不进行淘汰,直接返回异常。

17. Redis持久化策略?RDB与AOF区别?底层原理?

  1. RDB: 是一种快照式持久化方式,当触发某些特定的事件时,Redis会将内存中的数据保存到磁盘上一个指定的文件中,文件后缀一般为rdb 。RDB持久化的优点是备份文件小、加载速度快,适合用于大规模数据恢复。缺点是数据可能会丢失,因为数据并不是实时保存的
  2. AOF: 则是将所有写操作追加到一个日志文件中,即追加式持久化方式。在AOF模式下,每个写操作都会以文本的形式记录在一个追加文件中,从而记录了所有数据的历史操作 OF持久化的优点是数据不会丢失,因为每个写操作都会记录在追加文件中,可以避免因为某个事件没有触发而导致数据丢失。缺点是备份文件大、加载速度相对RDB慢,适合用于小规模数据恢复。

18. 实现分布式锁的方法

  1. 基于数据库主键或唯一索引实现分布式锁:在数据库表中创建一个带有唯一性约束条件的字段作为分布式锁,当需要获得锁时,向数据库表中插入一条记录,如果插入成功则表示获得了锁,否则表示锁已被其他线程占用。释放锁时,删除该记录即可。
  2. 基于 ZooKeeper 实现分布式锁:ZooKeeper 的顺序节点可以用来实现分布式锁。当需要获得锁时,创建一个带有顺序号的节点,并检查当前所有节点中是否自己的节点编号最小,如果是则表示获得了锁,否则监听自己前面一个节点的删除事件,等待锁释放。释放锁时,删除自己创建的节点即可。
  3. 使用 Redisson 实现分布式锁:Redisson 是一个基于 Redis 的分布式 Java 对象框架,提供了分布式锁的实现。通过 Redisson 的 getLock 方法可以获得一个锁对象,调用锁对象的 lock 和 unlock 方法即可获得和释放锁。Redisson 支持多种锁模式,包括可重入锁、公平锁、联锁等。

19. Spring事务隔离级别?事务失效场景?如何改变传播行为?

19.1 事务隔离级别

读未提交、读已提交、可重复读和串行化 使用@Transactional注解的isolation属性来控制事务的隔离级别

19.2 事务失效

  1. 事务使用在静态方法上会失效
  2. 事务使用在非public上(访问权限问题)
  3. 事务中对异常进行捕获,出现异常后spring框架无法感知到异常(非RuntimeException),事务就会失效
  4. 方法用 final 修饰
  5. 方法内部调用
  6. 未被 spring 管理
  7. 多线程调用
  8. 表不支持事务

19.3 传播行为

  1. REQUIRED:如果当前存在一个事务,则加入该事务;否则创建一个新的事务。这是默认传播行为。(默认)
  2. REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则将当前事务挂起。
  3. MANDATORY:如果当前存在一个事务,则加入该事务;否则抛出异常。
  4. SUPPORTS:如果当前存在一个事务,则加入该事务;否则以非事务的方式执行。
  5. NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,则将当前事务挂起。
  6. NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  7. NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与REQUIRED类似的操作修改传播行为使用@Transactional注解的propagation属性来控制事务的传播行为

20. RabbitMQ如何保证消息可靠性?

20.1 生产者

对于发送消息的场景,正常情况没有问题,直接发送即可:

如果是非正常情况就需要特殊处理了,一般会有三种非正常情况需要处理: ● 第一种情况,消息发送到交换机(exchange),但是没有队列与交换机绑定,消息会丢失。

● 第二种情况,在消息的发送后进行确认,如果发送失败需要将消息持久化,例如:发送的交换机不存在的情况。

● 第三种情况,由于网络、MQ服务宕机等原因导致消息没有发送到MQ服务器。

第一种情况: 对于消息只是到了交换机,并没有到达队列,这种情况记录日志即可,因为我们也不确定哪个队列需要这个消息。 配置如下(nacos中的shared-spring-rabbitmq.yml文件):

package com.sl.mq.config;

import cn.hutool.core.util.StrUtil;
import com.sl.transport.common.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class MessageConfig implements ApplicationContextAware {

    /**
     * 发送者回执 没有路由到队列的情况
     *
     * @param applicationContext 应用上下文
     * @throws BeansException 异常
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 获取RabbitTemplate
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 设置ReturnCallback
        rabbitTemplate.setReturnsCallback(message -> {
            if (StrUtil.contains(message.getExchange(), Constants.MQ.DELAYED_KEYWORD)) {
                //延迟消息没有发到队列是正常情况,无需记录日志
                return;
            }
            // 投递失败,记录日志
            log.error("消息没有投递到队列,应答码:{},原因:{},交换机:{},路由键:{},消息:{}",
                    message.getReplyCode(), message.getReplyText(), message.getExchange(), message.getRoutingKey(), message.getMessage());
        });
    }

}

第二种情况: 在配文件中开启配置publisher-confirm-type,即可在发送消息时添加回调方法:

在代码中进行处理,将消息数据持久化到数据库中,后续通过xxl-job进行处理,将消息进行重新发送。

同样,如果出现异常情况也是将消息持久化:

第三种情况: 将发送消息的代码进行try{}catch{}处理,如果出现异常会通过Spring-retry机制进重试,最多重试3次,如果依然失败就将消息数据进行持久化:

设置重试:

最终的落库操作:

xxl-job任务,主要负责从数据库中查询出错误消息数据然后进行重试:

package com.sl.mq.job;

import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sl.mq.entity.FailMsgEntity;
import com.sl.mq.service.FailMsgService;
import com.sl.mq.service.MQService;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

/**
 * 失败消息的处理任务
 */
@Slf4j
@Component
@ConditionalOnBean({MQService.class, FailMsgService.class})
public class FailMsgJob {

    @Resource
    private FailMsgService failMsgService;
    @Resource
    private MQService mqService;

    @XxlJob("failMsgJob")
    public void execute() {
        //查询失败的数据,每次最多处理100条错误消息
        LambdaQueryWrapper<FailMsgEntity> queryWrapper = new LambdaQueryWrapper<FailMsgEntity>()
                .orderByAsc(FailMsgEntity::getCreated)
                .last("limit 100");
        List<FailMsgEntity> failMsgEntityList = this.failMsgService.list(queryWrapper);
        if (CollUtil.isEmpty(failMsgEntityList)) {
            return;
        }

        for (FailMsgEntity failMsgEntity : failMsgEntityList) {
            try {
                //发送消息
                this.mqService.sendMsg(failMsgEntity.getExchange(), failMsgEntity.getRoutingKey(), failMsgEntity.getMsg());
                //删除数据
                this.failMsgService.removeById(failMsgEntity.getId());
            } catch (Exception e) {
                log.error("处理错误消息失败, failMsgEntity = {}", failMsgEntity);
            }
        }
    }
}

xxl-job中的任务调度:

  1. 首先把消息内容持久化到数据库中
  2. 调用客户端发送消息: a. 连接不上MQ,重试3次后更新数据库中消息状态为发送失败,加上失败原因 b. 使用生产者确认: ⅰ. 消息没到达交换机,即返回了nack:重试3次后更新数据库中消息状态为发送失败,加上失败原因 ⅱ. 消息没到达队列:更新数据库中消息状态为发送失败,加上失败原因。忽略延迟交换机。 c. 使用xxl-job定时任务 对 连接不上的,消息没到交换机的进行重试。重试次数达到后,则发邮件通知运维人员。

20.2 MQ

20.2.1 持久化

声明(创建)交换机(durable为true)、 队列(durable为true) 发送消息时指定持久化(MessageDeliveryMode.PERSISTENT)

20.2.2不能自动删除

交换机与队列都不能自动删除

20.3 消费者

20.3.1 确认模式

共有3种确认模式:默认使用auto

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: none|auto|manual
  1. none:代表消费就删除队列中的消息,不管消费者成功与否。
  2. auto:如果消费者代码出异常,则自动返回nack并重入队列。如果消费者没出异常则返回ack。导致不断的重试。
  3. manual: 手工ack,消费者方法参数中,需要添加Channel与Message参数,通过channel.basicAck或channel.basicNack。

20.3.2 Spring重试机制

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 1000 # 初识的失败等待时长为1秒
          multiplier: 3 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 4 # 最大重试次数
          stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

20.3.3 失败策略

重试次数耗尽,如果消息依然失败,则需要有MessageRecover。有3种不同的实现

  1. RejectAndDoNotRequeueRecover:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
  2. ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
  3. RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机。
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
    return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}

20.3.4 死信

重试次数耗尽时,使用第一种RejectAndDoNotRequeueRecover,则消息会进入死信,由运维人员来处理。

  • 作者:CZC(关于作者)
  • 发表时间:2024-10-20 11:54
  • 版权声明
  • 评论区:

    留言