常见的Java面试题

时间:2024-10-23 22:47:00
1.在项目中遇到难点是如何处理

数据库性能问题:在数据库处理数据操作(如查询、插入、更新、删除等)时表现出的响应速度慢、资源利用率高、吞吐量低等不良情况,影响了系统的整体性能和用户体验

解决方案有以下这些:

  1. 优化查询语句:避免使用不必要的子查询和复杂的函数;确保查询条件使用了合适的索引。
  2. 建立和优化索引:根据经常用于查询、连接和排序的字段创建索引。
  3. 调整数据库配置:合理配置内存缓冲区、连接数、线程池等参数。
  4. 分库分表:当数据量过大时,将表按照一定规则进行水平或垂直分表,或者进行数据库的垂直分割和水平分割功能。
  5. 解决锁竞争:尽量缩短事务的执行时间,减少锁的持有时间;采用合适的锁级别,如行锁而不是表锁。
  6. 监控和分析:使用数据库自带的性能监控工具或第三方工具,定期分析性能指标,发现问题及时解决。


 2.项目的并发量多大,如何实现限流控制

如果不考虑高并发的情况,即使业务系统平时运行得好好的,并发量一旦增加就会频繁出现各种诡异的业务问题,比如,在电商业务中,可能会出现用户订单丢失、库存扣减异常、超卖等问题。

限流是服务降级的一种手段,顾名思义,通过限制系统的流量,从而实现保护系统的目的。

合理的限流配置,需要了解系统的吞吐量,所以,限流一般需要结合 容量规划 和 压测 来进行。

当外部请求接近或者达到系统的最大阈值时,触发限流,采取其他的手段进行降级,保护系统不被压垮。常见的降级策略包括 延迟处理 、 拒绝服务 、 随机拒绝 等。

限流后的策略,其实和 Java 并发编程中的线程池非常类似,我们都知道,线程池在任务满的情况下,可以配置不同的拒绝策略,比如:

AbortPolicy,会丢弃任务并抛出异常
DiscardPolicy,丢弃任务,不抛出异常
DiscardOldestPolicy 等,当然也可以自己实现拒绝策略

Java 的线程池是开发中一个小的功能点,但是见微知著,也可以引申到系统的设计和架构上,将知识进行合理地迁移复用。

限流方案中有一点非常关键,那就是 如何判断当前的流量已经达到我们设置的最大值 ,具体有不同的实现策略,下面进行简单分析。

1. 计数器法

一般来说,我们进行限流时使用的是单位时间内的请求数,也就是平常说的 QPS,统计 QPS 最直接的想法就是实现一个计数器。

计数器法是限流算法里最简单的一种算法,我们假设一个接口限制 100 秒内的访问次数不能超过 10000 次,维护一个计数器,每次有新的请求过来,计数器加 1。

这时候判断,如果计数器的值小于限流值,并且与上一次请求的时间间隔还在 100 秒内,允许请求通过,否则拒绝请求
如果超出了时间间隔,要将计数器清零
下面的代码里使用 AtomicInteger 作为计数器,可以作为参考:

public class CounterLimiter { 
    //初始时间 
    private static long startTime = System.currentTimeMillis(); 
    //初始计数值 
    private static final AtomicInteger ZERO = new AtomicInteger(0); 
    //时间窗口限制 
    private static final int interval = 10000; 
    //限制通过请求 
    private static int limit = 100; 
    //请求计数 
    private AtomicInteger requestCount = ZERO; 
    //获取限流 
    public boolean tryAcquire() { 
        long now = System.currentTimeMillis(); 
        //在时间窗口内 
        if (now < startTime + interval) { 
            //判断是否超过最大请求 
            if (requestCount.get() < limit) { 
                requestCount.incrementAndGet(); 
                return true; 
            } 
            return false; 
        } else { 
            //超时重置 
            requestCount = ZERO; 
            startTime = now; 
            return true; 
        } 
    } 
} 

计数器策略进行限流,可以从单点扩展到集群,适合应用在分布式环境中。

单点限流使用内存即可,如果扩展到集群限流,可以用一个单独的存储节点,比如 Redis 或者 Memcached 来进行存储,在固定的时间间隔内设置过期时间,就可以统计集群流量,进行整体限流。

计数器策略有一个很大的缺点, 对临界流量不友好,限流不够平滑 。

假设这样一个场景,我们限制用户一分钟下单不超过 10 万次,现在在两个时间窗口的交汇点,前后一秒钟内,分别发送 10 万次请求。也就是说,窗口切换的这两秒钟内,系统接收了 20 万下单请求,这个峰值可能会超过系统阈值,影响服务稳定性。

对计数器算法的优化,就是避免出现两倍窗口限制的请求,可以使用滑动窗口算法实现,感兴趣的同学可以去了解一下。

2. 漏桶和令牌桶算法

漏桶算法和令牌桶算法,在实际应用中更加广泛,也经常被拿来对比。

漏桶算法可以用漏桶来对比,假设现在有一个固定容量的桶,底部钻一个小孔可以漏水,我们通过控制漏水的速度,来控制请求的处理,实现限流功能。

漏桶算法的拒绝策略很简单:如果外部请求超出当前阈值,则会在水桶里积蓄,一直到溢出,系统并不关心溢出的流量。

漏桶算法是从出口处限制请求速率,并不存在上面计数器法的临界问题,请求曲线始终是平滑的。

它的一个核心问题是 对请求的过滤太精准了 ,我们常说“水至清则无鱼”,其实在限流里也是一样的,我们限制每秒下单 10 万次,那 10 万零 1 次请求呢?是不是必须拒绝掉呢?

大部分业务场景下这个答案是否定的,虽然限流了,但还是希望系统允许一定的突发流量,这时候就需要令牌桶算法。

在令牌桶算法中,假设我们有一个大小恒定的桶,这个桶的容量和设定的阈值有关,桶里放着很多令牌,通过一个固定的速率,往里边放入令牌,如果桶满了,就把令牌丢掉,最后桶中可以保存的最大令牌数永远不会超过桶的大小。当有请求进入时,就尝试从桶里取走一个令牌,如果桶里是空的,那么这个请求就会被拒绝。

不知道你有没有使用过 Google 的 Guava 开源工具包?在 Guava 中有限流策略的工具类 RateLimiter,RateLimiter 基于令牌桶算法实现流量限制,使用非常方便。

RateLimiter 会按照一定的频率往桶里扔令牌,线程拿到令牌才能执行,RateLimter 的 API 可以直接应用,主要方法是 acquire 和 tryAcquire 。

acquire 会阻塞, tryAcquire 方法则是非阻塞的。

下面是一个简单的示例:

public class LimiterTest { 
    public static void main(String[] args) throws InterruptedException { 
        //允许10个,permitsPerSecond 
        RateLimiter limiter = RateLimiter.create(100); 
        for(int i=1;i<200;i++){ 
            if (limiter.tryAcquire(1)){ 
                System.out.println("第"+i+"次请求成功"); 
            }else{ 
                System.out.println("第"+i+"次请求拒绝"); 
            } 
        } 
    } 
} 
不同限流算法的比较

计数器算法实现比较简单,特别适合集群情况下使用,但是要考虑临界情况,可以应用滑动窗口策略进行优化,当然也是要看具体的限流场景。

漏桶算法和令牌桶算法,漏桶算法提供了比较严格的限流,令牌桶算法在限流之外,允许一定程度的突发流量。在实际开发中,我们并不需要这么精准地对流量进行控制,所以令牌桶算法的应用更多一些。

如果我们设置的流量峰值是 permitsPerSecond=N ,也就是每秒钟的请求量,计数器算法会出现 2N 的流量,漏桶算法会始终限制 N 的流量,而令牌桶算法允许大于 N,但不会达到 2N 这么高的峰值。


3.mysql乐观锁与悲观锁的区别

悲观锁与乐观锁的区别:

1. 悲观锁是数据库的层次去加锁的

2. 乐观锁就是表字段里面加上版本号或者使用字段条件

3. 悲观锁效率比效率低,最好还是用一些缓存数据库去解决这个问题。但是涉及到钱的我觉得还是悲观锁好。乐观锁的效率要比悲观锁高。

悲观锁使用介绍:

假定MySQL有客户端A与客户端B同时开启事务

在A里面进行查询数据:SELECT * FROM tb WHERE id = 1 for update;

由于B端还没有提交事务,A端则会在执行这条SQL语句时锁住(卡住不动),B端结束了事务,A端才会继续执行SQL。

乐观锁使用介绍:

假定我的表里面有个库存字段stock 值是100、版本号字段version 值是0。当前记录id等于1的值是初始化的值。

那么我在更新他的时候可以这样写:UPDATE tb SET stock = stock - 1, version = version + 1 WHERE id = 1 AND stock = 100 AND version = 0;


4.redis如何保证原始性 

原子性是事务处理中的基本属性,它要求事务中的所有操作要么全部成功,要么全部失败。对于 Redis 这样的数据库来说,保证原子性非常重要,因为它可以防止数据损坏和不一致。

Redis 使用以下方法保证原子性:

1. 原子性操作:

Redis 实现了多种原子性操作,例如 SET、GET、INCR、DECR 等。这些操作一次只修改单个键的值,并且是不可分割的。这意味着要么整个操作成功,要么整个操作失败,不会出现部分成功的情况。

2. 多重执行:

Redis 支持 MULTI 和 EXEC 命令,可以将多个原子性操作组合成一个单一的原子性事务。在事务中执行的所有操作要么全部成功,要么全部失败,确保原子性。

3. 监控锁:

Redis 提供了 WATCH 命令,允许客户端监控一个或多个键。如果在客户端执行原子性操作之前,被监控的键被其他客户端修改,则该操作将失败。这可以防止多个客户端同时修改同一个键,导致数据不一致。

4. 事务日志:

Redis 持久化模块默认情况下会记录所有写入操作的命令日志。如果发生故障,Redis 可以使用命令日志来恢复写入操作,确保数据的一致性和完整性。

5. 哨兵和主从复制:

哨兵(Sentinel)和主从复制机制可以复制 Redis 实例,确保在主实例出现故障时,数据可以从其他实例恢复。这可以防止数据丢失,并保证在故障期间数据的原子性。


5.sql怎么优化

1.在表中建立索引,优先考虑where.group by使用到的字段。

2.查询条件中,一定不要使用select *,因为会返回过多无用的字段会降低查询效率。应该使用具体的字段代替*,只返回使用到的字段。

3.不要在where条件中使用左右两边都是%的like模糊查询,如:

SELECT * FROM t_order WHERE customer LIKE '%zhang%'

这样会导致数据库引擎放弃索引进行全表扫描。

优化:尽量在字段后面使用模糊查询。如下:

SELECT * FROM t_order WHERE customer LIKE 'zhang%'

4.尽量不要使用in 和not in,会造成全表扫描。如下:

SELECT * FROM t_order WHERE id IN (2,3)

SELECT * FROM t_order1 WHERE customer IN (SELECT customer FROM t_order2)

优化:

对于连续的数值,能用 between 就不要用 in ,如下:SELECT * FROM t_order WHERE id BETWEEN 2 AND 3

对于子查询,可以用exists代替。如下:SELECT * FROM t_order1 WHERE EXISTS (SELECT * FROM t_order2 WHERE t1.customer = t2.customer)

5.尽量不要使用or,会造成全表扫描。如下:

SELECT * FROM t_order WHERE id = 1 OR id = 3

优化:可以用union代替or。如下:

SELECT * FROM t_order WHERE id = 1

UNION

SELECT * FROM t_order WHERE id = 3

6.尽量不要在 where 子句中对字段进行表达式操作,这样也会造成全表扫描。如:

select id FROM t_order where num/2=100

应改为:

select id FROM t_order where num=100*2

7.where条件里尽量不要进行null值的判断,null的判断也会造成全表扫描。如下:

SELECT * FROM t_order WHERE score IS NULL

优化:

给字段添加默认值,对默认值进行判断。如:

SELECT * FROM t_order WHERE score = 0

8.尽量不要在where条件中等号的左侧进行表达式.函数操作,会导致全表扫描。如下:

SELECT * FROM t_order2 WHERE score/10 = 10

SELECT * FROM t_order2 WHERE SUBSTR(customer,1,5) = 'zhang'

优化:

将表达式.函数操作移动到等号右侧。如下:

SELECT * FROM t_order2 WHERE score = 10*10

SELECT * FROM t_order2 WHERE customer LIKE 'zhang%'

9.尽量不要使用where 1=1的条件

有时候,在开发过程中,为了方便拼装查询条件,我们会加上该条件,这样,会造成进行全表扫描。如下:

SELECT * FROM t_order WHERE 1=1

优化:

如果用代码拼装sql,则由代码进行判断,没where加where,有where加and

如果用mybatis,请用mybatis的where语法。

10.程序要尽量避免大事务操作,提高系统并发能力。

11.一个表的索引数最好不要超过6个,如果索引太多的话,就需要考虑一下那些不常使用到的列上建的索引是否有必要。


6. 场景设计:现在有5W条数据导入elx表络,请您设计下接口,保证效率。 
  1. 使用更快的 Excel 读取框架(推荐使用阿里 EasyExcel)
  2. 对于需要与数据库交互的校验、按照业务逻辑适当的使用缓存。用空间换时间(将参加校验的数据全部缓存到 HashMap 中。直接到 HashMap 去命中。)
  3. 使用 values(),(),() 拼接长 SQL 一次插入多行数据
  4. 使用多线程插入数据,利用掉网络IO等待时间(推荐使用并行流,简单易用)
  5. 避免在循环中打印无用的日志

7.如何保证代码质量 
1、采用编码标准
2、编写自动化测试
3、使用版本控制
4、定期重构代码
5、使用代码审查
6、使用静态代码分析工具(sonarqube
7、与其他开发者合作

8.生产内存若出现问题,您是如何解决 
1.生产环境发生 cpu 飙高的问题

1. 查看当前的操作系统中(top) 那个进程 cpu 使用率是最高的;
2. 找到该操作系统中 最高使用率 进程 分析该进程里面具体线程 谁 cpu 使用率是最高的
3. 在根据线程名称 搜索“java 代码” 找到具体发生 cpu 飙高的代码

2.生产环境发生内存泄漏问题

查找到 java 虚拟机 哪些对象占用空间最大 前 20 个 列出分析

 3.生产环境遇到了报错

1. 传统的方式 在生产环境中遇到报错问题,我们是通过搜索日志的方式,排查具体的错误。适合于服务器端 是单机或者少量集群的节点
Tail -200f
2.采用 aop 形式拦截系统错误日志,在将这些错误日志调用微信公众号接口 主动告诉给我们的开发人员生产环境发生了故障。

4.生产环境服务器宕机

1. 我们公司生产环境,会对我们服务器 实现多个节点集群,如果某台服务器发生了宕机 会自动实现故障转移,保证服务的高可用。

2. 如果服务器宕机 我们可以在服务器上安装 keepalived 监听 java 进程,如果该java 进程发生了宕机 会自动尝试重启该 java 进程,这是属于软件层面。如果是物理机器比如关机了,可以使用硬件方式自动重启服务器 例如向日葵
3.如果服务器发生了宕机,尝试重启 n 多次还是失败,我们可以使用容器快速动态的实现扩容(docker 或者 k8s)k8s
4.重启该服务,如果重启多次还是失败 则会发送短信模板的形式通知给运维人员。

5.你在开发过程中,遇到哪些难题?你是怎么解决的呢

例如 我们公司提供了一个接口,被其他公司进行调用。他的公司在调用我们公司接口的过程中,我们的接口响应超时了,最终触发了客户端重试了,重试的过程当中请求的参数都是相同的,导致我们接口会重复执行业务逻辑。
解决办法: 全局 id 业务上防重复、 在 db 层面去重复 例如 创建唯一约束 


9.常用的linux命令哪些

1.ls,通过 ls 命令不仅可以查看 linux 文件夹包含的文件,而且可以查看文件权限(包括目录、文件夹、文件权限)、查看目录信息等等。

2.cd,切换目录

3.pwd。该命令用于查看当前工作目录的路径

4.mkdir,该指令用于创建目录

5.rm指删除一个目录中的一个或多个文件或目录

6.rmdir是remove directory的缩写,指删除空目录

7.mv指移动文件、目录,移动时可修改文件或目录名

8.cp复制,将多文件或目录复制至目标目录

9.cat文本输出命令

10.more阅读命令,与 cat 类似, more 会以一页一页的显示方便逐页阅读

11.less 可以随意浏览文件,less 在查看之前不会加载整个文件

12.tail常用来查看日志文件

13.head 用来显示档案的开头至标准输出中

14.tar 本身不具有压缩功能,只具有打包功能

15.grep文本搜索命令

16.ps用来查看当前运行的进程状态

17.top显示当前系统正在执行的进程的 ID、内存占用率、CPU 占用率等相关信息

18.kill删除执行中的程序或者工作,发送指定的信号到相应的进程

19.free显示系统内存使用情况

20.df显示磁盘空间使用情况


10.MQ常见问题
1.如何保证消息不被重复消费?(如何保证消息消费时的幂等性?)
  • 生成者不重复发送消息到MQ

mq内部可以为每条消息生成一个全局唯一、与业务无关的消息id,当mq接收到消息时,会先根据该id判断消息是否重复发送,mq再决定是否接收该消息。

  • 消费者不重复消费

消费者怎么保证不重复消费的关键在于消费者端做控制,因为MQ不能保证不重复发送消息,所以应该在消费者端控制:即使MQ重复发送了消息,消费者拿到了消息之后,要判断是否已经消费过,如果已经消费,直接丢弃。所以根据实际业务情况,有下面几种方式:

  • 如果从MQ拿到数据是要存到数据库,那么可以根据数据创建唯一约束,这样的话,同样的数据从MQ发送过来之后,当插入数据库的时候,会报违反唯一约束,不会插入成功的。(或者可以先查一次,是否在数据库中已经保存了,如果能查到,那就直接丢弃就好了)。
  • 让生产者发送消息时,每条消息加一个全局的唯一id,然后消费时,将该id保存到redis里面。消费时先去redis里面查一下有么有,没有再消费。(其实原理跟第一点差不多)。
  • 如果拿到的数据是直接放到redis的set中的话,那就不用考虑了,因为set集合就是自动有去重的。
2.如何保证消息的可靠性传输(如何处理消息丢失的问题?)

主要分为三种:

  1. 生产者丢失
  2. MQ自己丢失了
  3. 消费的时候丢了

生产者弄丢了消息

写消息的过程中,消息都没到Rabbit MQ,在网络传输过程中就丢了;或者是消息到了Rabbit MQ但是MQ内部错乱没有存下来导致消息丢失。
  • 方案1:可以使用Rabbit MQ事务机制如下:
channel.txSelect();
try{
    // 发送消息
}catch{
    channel.txRollback();
}
channel.txCommit();
弊端:是事务机制,同步阻塞的,会导致生产者发送消息的吞吐量大大下降。
  • 方案2:把channel设置成comfirm模式,发送一个消息就不用管了,Rabbit MQ如果接收到了这个消息就会回调生产者本地的一个接口,通知你说这条消息已经发送成功并且接收成功。反之也会通知。
该方式的吞吐量会高一些

Rabbit MQ弄丢了消息

将Rabbit MQ设置成持久化的。除非有极其罕见的情况Rabbit MQ还没来得及持久化自己就挂了,可能回导致少量的数据丢失。这种概率很小。设置持久化的步骤(必须两个同时设置):

  1. 创建queue的时候将其设置为持久化的,这样保证Rabbit MQ持久化queue的元数据,但是不会持久化queue里的数据
  2. 发送消息的时候将消息的deliveryMode设置为2.就是将消息设置为持久化。此时Rabbit MQ就会将消息持久化到磁盘上去。

消费者弄丢了消息

只有当你打开了消费者的autoAck的这样一个机制;你消费到了数据之后消费者会自动通知Rabbit MQ说我已经消费到了这条数据;这样会出现一种情况就是假设消费到了一条数据还没处理完,此时消费者就自动autoAck了,此时恰巧消费者系统服务挂掉了,消息还没处理完而且Rabbit MQ以为该消息已经处理掉了。

  • 方案:关闭掉Rabbit MQ的自动ACK机制。
3. 如何保证消息的顺序性

假设做一个MySQL binlog同步系统,你在MySQL里增删改一个条数据,对应出来增删改3条binlog,接着将这三条binlog发送到MQ里面,到消费出来一次执行,需要保证消息的顺序性。不然数据就会出现问题。

不同MQ错乱的场景:

Rabbit MQ

  • Rabbit MQ:一个queue,多个consumer,这就会出现问题;因为多个消费者是同步一起执行的,无法保证顺序,并且也无法保证消费者消费到了哪条数据。

解决方案

  • 解决方案:每个消费者建立对应的queue,并且让保持顺序的消息只发送到一个queue上,这样消费者消费数据处理的时候就不会出现顺序错乱。

如何保证数据写入一个partition中去:

生产者在写入数据的时候可以指定一个key,比如指定某个订单id作为key,这个订单相关的数据就会被分发到一个partition中去。Kafka有一个原则是一个partition只能被一个消费者消费消费者从partition中取出来数据的时候,一定是有顺序的。

4.如何解决消息延时过期失效的问题

Rabbit MQ有一个TTL过期时间。关掉不要开启TTL

5.如何解决消息积压的问题

Rabbit MQ消息积压

  • 解决思路:

临时紧急扩容。具体操作如下:

由于消费者故障的解决方案
  1. 如果consumer有问题,先修复consumer的问题,确保其恢复消费速度。然后将现有consumer都停掉。
  2. 临时建立好原先10倍或者20被queue的数量。(Kafka-新建一个topic,partition是原来的10倍。)
  3. 写一个临时分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时处理,直接均匀轮询写入已经建立好10倍/20倍数量的queue。
  4. 接着临时征用10倍的机器来部署consumer,每一批consumer来消费一个临时queue的数据。
  5. 等快速消费完积压的数据之后,得恢复原来部署架构,重新用原来的consumer机器来消费