java常见面试场景题

1. 如何定位线上OOM

  1. 造成OOM的原因

  1. 如何快速定位OOM
    图片[1]-java常见面试场景题 - 拾光赋-拾光赋

2. 如何防止重复下单

方案一:前端提交订单按钮置灰

用户点击下单按钮后置灰,防止用户无意点击多次

方案二: 后端Redis setnx

用户token +商品URL+ KEY 用setnx 命令并设置过期时间3-5秒防止重复下单

3. 如何设计分布式日志存储架构

单体项目 使用Logback, Log4j记录日志到文件中

  • 优点:部署简单,成本低,容易维护,性能高,稳定
  • 缺点:在分布式环境下不方便排查问题

如何实现日志分布式系统

  1. MongoDB存储

图片[2]-java常见面试场景题 - 拾光赋-拾光赋
2. ELK存储

4. 给你以一亿个 Redis keys, 统计双方共同好友

要统计双方共同好友,可以使用Redis的Set数据结构来实现。以下是一个详细的步骤:

  1. 使用Redis的Set数据结构‌ :
    将每个用户的好友列表存储在一个Set中。例如,用户userid:20002和userid:20003的好友列表可以分别存储在两个Set中。

  2. 使用交集命令SINTERSTORE‌

Redis自带的交集命令SINTERSTORE可以用来求两个Set的交集,并将结果存储在一个新的Set中。示例命令如下:

SINTERSTORE userid:new userid:20002 userid:20003

这条命令会计算userid:20002和userid:20003的好友列表的交集,并将结果存储在Set userid中。

  1. 处理大量数据‌:

存储一亿个数据在Redis中成本可能过高,因为Redis是内存数据库,存储大量数据会占用大量内存资源。因此,需要考虑其他解决方案。
一个更适合的解决方案是使用MySQL作为主要存储,通过分库分表策略分散数据存储压力,并使用缓存(如Redis)存储热点数据,以提高查询效率

  1. ‌使用其他数据库‌:

如果社交数据非常复杂,可以考虑使用图数据库(如Neo4J)来存储和查询好友关系。Neo4J能够通过命令直接查询可能认识的好友、共同好友等关系。

  1. 监控和优化‌:

对于如此大量的数据,需要持续监控Redis的性能和内存使用情况,确保系统的稳定性和高效性。
可以通过使用Redis的INFO命令来获取服务器的状态信息,如内存使用情况、连接数、命中率等,以便及时进行调优。
请注意,处理如此大量的数据需要谨慎设计系统架构,并考虑到数据的扩展性、安全性和性能。在实际操作中,可能还需要根据具体情况进行进一步的优化和调整。

另外,由于Redis是内存数据库,对于存储大量数据(如一亿个key)的情况,需要确保有足够的物理内存来支持,并考虑使用持久化机制(如RDB和AOF)来防止数据丢失。

5. 如何使用redis记录上亿用户连续登录天数

图片[3]-java常见面试场景题 - 拾光赋-拾光赋
可以采用Redis提供的Bitmap(位图)数据结构来实现。Bitmap实际上是由一个一个的bit的二进制位所组成的数组,每一个位只能存0和1,非常适合用于这种二值统计的场景。

以下是使用Redis记录上亿用户连续登录天数的基本步骤:

选择数据结构‌:

使用Redis的Bitmap数据结构。Redis的Bitmap是由string类型所实现的,string类型最大可以存储512兆字节,换算成bit位,可以存储42亿多个bit位,因此足够存储上亿用户的连续登录状态。

设计key和offset‌:

使用日期作为Bitmap的key,例如“20250209”表示2025年2月9日。
将用户的ID映射到Bitmap的偏移位(offset)。假设用户ID是唯一的数字,那么可以直接将用户ID作为偏移量。例如,用户ID为5的登录状态就存储在offset为5的位上。

记录登录状态‌:

当用户登录时,使用Redis的SETBIT命令将对应日期和用户ID的位设置为1。例如,如果今天是2025年2月9日,用户ID为5的用户登录了,那么执行命令SETBIT 20250209 5 1。

统计连续登录天数‌:

要统计某个用户的连续登录天数,可以从当天开始,往前面的日期去推算。例如,要统计用户ID为5的连续登录天数,从当天开始,逐日检查对应位是否为1,直到遇到0为止。
使用Redis的GETBIT命令获取指定位置的bit值

设置key的过期时间‌:

一般不需要统计超过30天的连续登录天数,因此可以将当前key的过期时间设置为30天,超过30天自动过期,这样可以有效利用Redis的内存。

6. 查询200条数据耗时200毫秒,怎么在500毫秒内查询1000条数据

如果查询可以并行化(即多个查询可以同时进行而不会相互影响),你可以考虑使用多线程或异步编程来同时发起多个查询。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MultiThreadedQuery { 

    // 假设这是你的查询方法,单线程查询200条数据耗时200毫秒
    private static List<Data> queryData(int start, int count) { 
        // 模拟查询操作
        try { 
            Thread.sleep(count / 2); // 假设每条数据查询耗时0.5毫秒(仅为示例)
        } catch (InterruptedException e) { 
            Thread.currentThread().interrupt();
        }
        
        List<Data> dataList = new ArrayList<>();
        for (int i = 0; i < count; i++) { 
            dataList.add(new Data(start + i)); // 添加模拟数据
        }
        return dataList;
    }

    // 使用多线程查询数据
    public static List<Data> queryDataInParallel(int totalCount, int threadCount, int batchSize) { 
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        List<Future<List<Data>>> futures = new ArrayList<>();
        
        for (int i = 0; i < totalCount; i += batchSize) { 
            final int start = i;
            final int count = Math.min(batchSize, totalCount - i);
            futures.add(executor.submit(new Callable<List<Data>>() { 
                @Override
                public List<Data> call() throws Exception { 
                    return queryData(start, count);
                }
            }));
        }
        
        List<Data> result = new ArrayList<>();
        for (Future<List<Data>> future : futures) { 
            try { 
                result.addAll(future.get());
            } catch (InterruptedException | ExecutionException e) { 
                e.printStackTrace();
            }
        }
        
        executor.shutdown();
        return result;
    }

    public static void main(String[] args) { 
        int totalCount = 1000;
        int threadCount = 5; // 假设使用5个线程
        int batchSize = 200; // 每个线程查询的批次大小

        long startTime = System.currentTimeMillis();
        List<Data> data = queryDataInParallel(totalCount, threadCount, batchSize);
        long endTime = System.currentTimeMillis();

        System.out.println("Queried " + data.size() + " items in " + (endTime - startTime) + " ms.");
    }

    // 假设的数据类
    static class Data { 
        private int id;

        public Data(int id) { 
            this.id = id;
        }

        // getter, setter, toString等方法可以根据需要添加
    }
}

7. 怎么用java实现一个简单的消息队列

public class SharedQueue { 
    //声明队列最大长度
    private int queueSize = 10;

    private ArrayBlockingQueue<Integer> queue= new ArrayBlockingQueue<>(10);

    public static void main(String[] args) { 
        SharedQueue sharedQueue = new SharedQueue();
        //消费者持续运行
        Consumer consumer = sharedQueue.new Consumer();
        consumer.start();

        //生产10条消息
        for (int i = 1; i <= 10; i++) { 
            //创建10个生产者线程
            Producer producer = sharedQueue.new Producer();
            producer.start();
        }
    }


   //生产者
   class Producer extends Thread{ 
       @Override
       public void run() { 
           //保证生产者在整个过程中是安全的
           synchronized (queue){ 
               //1.判断当前队列长度是否小于最大长度
               if(queue.size()<queueSize){ 
                   //2.如果小于,生产者就可以生产消息了
                   //2.1 往队列添加一条消息
                   queue.add(queue.size()+1);
                   System.out.println("生产者往队列中加入消息,队列当前长度:"+queue.size());
                   //唤醒消费者有活了
                   queue.notify();

                   try { 
                       //模拟业务处理
                       Thread.sleep(1000);
                   } catch (InterruptedException e) { 
                       throw new RuntimeException(e);
                   }
               }else{ 
                   //3.如果大于,生产者停止工作,稍微歇一歇
                   try { 
                       queue.wait();
                   } catch (InterruptedException e) { 
                       e.printStackTrace();
                   }
               }
           }
       }
   }


    class Consumer extends Thread{ 
        @Override
        public void run() { 
            //消费者需要重复的工作
            while(true){ 
                //保证整个消费过程是线程安全的
                synchronized (queue){ 
                    //如果队列为空,睡眠
                    if(queue.isEmpty()){ 
                        System.out.println("当前队列为空....");
                        try { 
                            queue.wait();
                        } catch (InterruptedException e) { 
                            e.printStackTrace();
                            //如果出现异常,手动唤醒
                            queue.notify();
                        }
                    }else{ 
                        //队列不为空,消费者进行消费
                        //消费头部信息
                        Integer value = queue.poll();
                        System.out.println("消费者从队列中消费了信息"+value+",队列当前长度:"+queue.size());
                        //消费完消息,唤醒生产者可以继续生产了
                        queue.notify();

                        try { 
                            //模拟业务处理
                            Thread.sleep(1000);
                        } catch (InterruptedException e) { 
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        }
    }
}

8. redis如何实现上亿用户实时积分排行榜

图片[4]-java常见面试场景题 - 拾光赋-拾光赋
实现上亿用户实时积分排行榜是一个复杂且高性能要求的任务,但Redis凭借其出色的数据结构和性能,非常适合处理这种场景。以下是一个基于Redis的实现思路:

数据结构设计

使用Sorted Set(有序集合)
Redis的Sorted Set(ZSET)是带有权重的集合,其内部实现是跳表加字典,能够高效地支持按权重(这里是积分)的排序和范围查询。因此,我们可以使用Sorted Set来存储用户的积分信息。

  • ‌Key‌:可以是一个固定的字符串,比如”user_leaderboard”。
  • ‌Member‌:用户的唯一标识,比如用户ID。
  • ‌Score‌:用户的积分。

数据操作

添加或更新用户积分
当用户获得或失去积分时,需要更新排行榜。这可以通过Redis的ZADD命令来实现,该命令可以添加或更新集合中成员的分数。

ZADD user_leaderboard score member

例如,给用户ID为123的用户增加100积分:

ZADD user_leaderboard 100 123

如果用户已经存在,则ZADD会更新其积分。

查询排行榜

要获取积分排行榜,可以使用ZRANGE或ZREVRANGE命令。这两个命令分别按升序和降序返回集合中的元素。

# 获取积分前100名的用户
ZREVRANGE user_leaderboard 0 99 WITHSCORES

获取用户排名
要获取特定用户在排行榜中的排名,可以使用ZREVRANK命令。

# 获取用户ID123的用户在排行榜中的排名(从0开始)
ZREVRANK user_leaderboard 123

优化方案:分桶而治

一个key存储上亿用户太大,根据key来进行区分,1000以上的存到一个桶里

9. 内存200M读取1G文件并统计重复内容

分块读取并放到hashMap中

  • 我们使用BufferedReader以流的方式逐块读取文件,每次读取一个字符数组(缓冲区)。
  • 当我们遇到换行符时,我们认为这是一行的结束,并处理这一行(在这里,我们简单地统计了每一行的出现次数)。
  • 我们使用一个HashMap来存储每行内容及其出现的次数。
  • 最后,我们遍历HashMap并打印出出现次数大于1的内容。

这种方法的关键在于它不需要一次性将整个文件加载到内存中,而是逐行(或逐块)处理文件内容,因此非常适合处理大文件。你可以根据需要调整缓冲区的大小(BUFFER_SIZE),以及处理内容的方式(例如,按单词而不是按行统计)

极端情况下内容不重复,那这个时候map就会产生大量的key, 此时可以先将数据写到每个分片文件,
读取每个分片文件,用map存储并统计

总结: 分块读取大文件 --> 文件分片 -->逐个统计

10. Springboot防盗链的几种方式

在Spring Boot中实现防盗链功能,可以通过多种方式来实现,主要包括使用过滤器(Filter)、拦截器(Interceptor)以及配置反向代理服务器(如Nginx)等。以下是几种方式的详细说明:

  1. 使用过滤器(Filter)
    通过创建一个自定义过滤器,可以在请求到达实际资源之前检查HTTP头中的Referer字段。如果Referer不在允许的域名列表中,则返回403 Forbidden响应或重定向到其他页面。

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class HotlinkProtectionFilter implements Filter { 
    private final String[] allowedDomains = { "yourdomain.com"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String referer = httpRequest.getHeader("Referer");

        if (referer == null || Arrays.stream(allowedDomains).anyMatch(referer::contains)) { 
            chain.doFilter(request, response);
        } else { 
            ((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN, "Hotlinking not allowed");
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException { }

    @Override
    public void destroy() { }
}

然后,将这个过滤器注册到Spring的上下文中:

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebConfig { 
    @Bean
    public FilterRegistrationBean<HotlinkProtectionFilter> loggingFilter() { 
        FilterRegistrationBean<HotlinkProtectionFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new HotlinkProtectionFilter());
        registrationBean.addUrlPatterns("/resources/*"); // 替换为你的资源路径
        return registrationBean;
    }
}

  1. 配置Nginx等反向代理服务器

在网关层使用Nginx等反向代理服务器进行防盗链配置也是一种常见且简单的方式。通过拦截访问资源的请求,并检查请求头中的Referer地址是否为本站,如果不是则进行阻止或重定向。

server { 
    listen 80;
    server_name www.yourdomain.com;

    location / { 
        root /web;
        index index.html;
    }

    location ~* \.(gif|jpg|png|jpeg)$ { 
        root /web;
        valid_referers none blocked yourdomain.com;

        if ($invalid_referer) { 
            return 403;
        }
    }
}

11. springboot,springmvc和spring的区别

三者是包含的关系,springboot包含spring, spring 包含springmvc

‌Spring是全面的企业级开发框架,Spring MVC是Spring的Web MVC模块,Spring Boot是简化Spring应用开发的快速开发框架‌。

‌Spring‌:提供IoC和AOP等核心功能,支持企业级应用开发,涵盖web层、业务层、持久层等‌12。
‌Spring MVC‌:Spring的Web MVC框架,专注于Web应用的MVC架构,解决WEB开发问题,配置相对繁琐‌12。
‌Spring Boot‌:简化Spring应用开发的框架,提供自动配置和默认配置,降低项目搭建复杂度,快速上手开发‌

12. 如何防止SpringBoot反编译

可以使用一些工具(如ClassFinal-maven-plugin, JBCO)将字节码加密,并在运行时动态解密。这种方法可以有效地保护源代码的安全性,但需要在运行时进行解密操作,可能会影响程序的性能。

  1. 引入插件
<!-- ClassFinal加密 -->
<plugin>
    <groupId>net.roseboy</groupId>
    <artifactId>classfinal-maven-plugin</artifactId>
    <version>1.2.1</version>
    <configuration>
        <!-- 可采用机器码的方式绑定机器 需要提前获得机器码写入code -->
        <!--<code></code> -->
        <!-- 密码为#可以直接用javaagent启动 也可以设置其他密码 dockerFile脚本中需要标注=“-pwd 123456” -->
        <password>123456</password>
        <excludes>org.spring&l

原文链接:java常见面试场景题

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容