记录一下遇到的笔试面试题

1 mq如何发送延时消息

通过redis(待补充)

2 mq如何保证消息的时序性(以RocketMQ为例)

问题的由来: 一个用户在电商网站上下订单到交易完成,中间会经历一系列动作,订单的状态也会随之变化,一个订单会产生多条MQ消息,下单、付款、发货、买家确认收货,消费端需要严格按照业务状态机的顺序处理,否则,就会出现业务问题。
问题的解决: 只要通过一定的路由策略,保证一个订单的多条状态消息在同一个分区(保证局部有序),便可以满足业务需求。虽然保证了单个分片的消息有序,但每个分片的消费者只能是单线程处理,因为多线程无法控制消费顺序,这个可能会损失一些性能。但是这里会引出另一个问题。
如何保证一个队列只能有一个消费端呢?
在RocketMQ中会遍历一个topic下所有的MessageQueue,通过isOrder && !this.lock(mq) 尝试对它加锁,确保一个MessageQueue只能被一个消费者处理。
那如何保证一个队列,只有一个线程在处理消息呢?
ConsumeMessageService 中有两个实现类,因为我们有消费顺序要求,会选择ConsumeMessageOrderlyService来处理业务。它会从ConcurrentMap中获取messageQueue对应的锁对象。通过 synchronized 关键字,线程来抢占锁,互斥关系,从而保证了一个MessageQueue只能有一个线程并发处理。
那如果分区临时扩容了怎么办?
因为扩容后可能导致路由后的结果不一样,会导致一个订单的消息分布在两个分区中,无法保证顺序,所以只能先将存量消息处理完,再扩容。如果是在线业务,可以搞个临时topic,先将消息暂时堆积,待扩容后,按新的路由规则重新发送。
顺序消息,如果某条失败了怎么办?会不会一直阻塞?
如果失败,不会提交消费,系统会自动重试(有重试上限),此时会阻塞后面的消息消费,直到这条消息处理完。如果这个消息达到重试上限,依然失败,会进入死信队列,可以继续处理后面的消息。

3 String,StringBuffer,StringBuilder相关

StringStringBuilderStringBuffer
String的值是不可变的,每次对String进行操作都会生成一个新的对象,效率低且浪费大量内存空间单线程,但是速度快StringBuffer是可变的,线程安全(通过synchronized实现)的字符串操作类,每个StringBuffer对象都有一定的缓冲区容量,当字符串超过容量时,会自动增加容量

下面的3个方法会有什么问题

   public void testString() {
        String string = "";
        for (int i = 0; i < 1000; i++) {
            string += i;
        }
        System.out.print("String : " + string);
    }

    public void testStringBuffer() {
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < 1000; i++) {
            buffer.append(i);
        }
        System.out.print("StringBuffer : " + buffer.toString(););
    }

    public void testStringBuilder() {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 1000; i++) {
            builder.append(i);
        }
        System.out.print("StringBuilder : " + builder.toString(););
    }

4 HashMap根据value排序(手写)

代码如下:

public static void main(String[] args) {
		//产生一个map并添加一些参数
		Map<String,Integer> map = new HashMap<>();
		map.put("ddd", 1);
		map.put("aaa", 2);
		map.put("bbb", 3);
		map.put("ccc", 4);
		//先将map的entryset放入list集合
		List<Map.Entry<String,Integer>> list = new ArrayList<>(map.entrySet()); 
		//对list进行排序,并通过Comparator传入自定义的排序规则
		Collections.sort(list,new Comparator<Map.Entry<String, Integer>>() {
			@Override
			public int compare(Entry<String, Integer> o1, Entry<String, Integer> o2) {
				//重写排序规则,小于0表示升序,大于0表示降序
				return o1.getValue()-o2.getValue(); 
			}
		});
		//用迭代器对list中的键值对元素进行遍历
		Iterator<Map.Entry<String, Integer>> iter = list.iterator();
		while(iter.hasNext()){
			Map.Entry<String, Integer> item = iter.next();
			String key = item.getKey();
			int value = item.getValue();
			System.out.println("key:"+key+"value:"+value);
		}
	}

5 设计系统间的接口通信方案,要求防篡改,时效性,高并发

1、权限问题
通过Token:在第一次校验权限的时候传输用户名和密码,校验成功后,通过token来识别。一般情况下客户端(接口调用方)需要先向服务器端申请一个接口调用的账号(服务对接的时候),服务器会给出一个appId和一个key, key用于参数签名使用,注意key保存到客户端,需要做一些安全处理,防止泄露。Token的值一般是UUID,服务端生成Token后需要将token做为key,将一些和token关联的信息作为value保存到缓存服务器中(redis),当一个请求过来后,服务器就去缓存服务器中查询这个Token是否存在,存在则调用接口,不存在返回接口错误,一般通过拦截器或者过滤器来实现,Token分为两种:
API Token(接口令牌): 用于访问不需要用户登录的接口,如登录、注册、一些基本数据的获取等。获取接口令牌需要拿appId、timestamp和sign来换,sign=加密(timestamp+key)
USER Token(用户令牌): 用于访问需要用户登录之后的接口,如:获取我的基本信息、保存、修改、删除等操作。获取用户令牌需要拿用户名和密码来换
关于Token的时效性:token可以是一次性的、也可以在一段时间范围内是有效的,具体使用哪种看业务需要。
2、时效性(同时也能减轻DoS攻击)
在接口参数中加入时间戳,是客户端调用接口时对应的当前时间戳,时间戳用于防止DoS攻击。
当黑客劫持了请求的url去DoS攻击,每次调用接口时接口都会判断服务器当前系统时间和接口中传的的timestamp的差值,如果这个差值超过某个设置的时间(假如5分钟),那么这个请求将被拦截掉,如果在设置的超时时间范围内,是不能阻止DoS攻击的。timestamp机制只能减轻DoS攻击的时间,缩短攻击时间。如果黑客修改了时间戳的值可通过sign签名机制来处理。
3、防篡改
接口参数中经常会有一些敏感参数,如用户名,用户手机号,我们需要保证这些信息不会被恶意篡改。
sign:一般用于参数签名,防止参数被非法篡改,最常见的是修改金额等重要敏感参数, sign的值一般是将所有非空参数按照升续排序然后+token+key+timestamp+nonce(随机数)拼接在一起,然后使用某种加密算法进行加密,作为接口中的一个参数sign来传递,也可以将sign放到请求头中。
如果接口在网络传输过程中如果被黑客挟持,并修改其中的参数值,然后再继续调用接口,虽然参数的值被修改了,但是因为黑客不知道sign是如何计算出来的,不知道sign都有哪些值构成,不知道以怎样的顺序拼接在一起的,最重要的是不知道签名字符串中的key是什么,所以黑客可以篡改参数的值,但没法修改sign的值,当服务器调用接口前会按照sign的规则重新计算出sign的值然后和接口传递的sign参数的值做比较,如果相等表示参数值没有被篡改,如果不等,表示参数被非法篡改了,就不执行接口了。
注:
nonce:随机值,是客户端随机生成的值,作为参数传递过来,随机值的目的是增加sign签名的多变性。随机值一般是数字和字母的组合,6位长度,随机值的组成和长度没有固定规则。
4、防止重复提交
对于一些重要的操作需要防止客户端重复提交的(如非幂等操作),具体办法是当请求第一次提交时将sign作为key保存到redis,并设置超时时间,超时时间和Timestamp中设置的差值相同。
当同一个请求第二次访问时会先检测redis是否存在该sign,如果存在则证明重复提交了,接口就不再继续调用了。如果sign在缓存服务器中因过期时间到了,而被删除了,此时当这个url再次请求服务器时,因token的过期时间和sign的过期时间一致,sign过期也意味着token过期,那样同样的url再访问服务器会因token错误会被拦截掉,这就是为什么sign和token的过期时间要保持一致的原因。拒绝重复调用机制确保URL被别人截获了也无法使用(如抓取数据)。

参考 https://www.jianshu.com/p/c7e1743d6990

6 linux中部署了java程序,cpu100%,排查出哪一行的问题写出步骤

#用top -c命令查看cpu占用高的进程
top -c
#用top -Hp 24857查看cpu占用最高的线程
top -Hp 上一条命令执行结果中cpu占用最高的进程pid
#将线程的pid转换成16进制
printf "%x\n" pid
#通过jstack查看进程信息
jstack pid | grep "上一条的结果" -A 30

其他调优命令:jvm调优

7 多线程按次序打印100个数字

public class PrintNum extends Thread {
	// 线程数量
    static int n = 6; 
    // 计数,到100结束,volatile关键字
    static volatile int num = 0; 
	// 线程编号
    private int id;  
    public PrintNum(int id) {
        this.id = id;
    }
    @Override
    public void run() {
        while (num <= 100) {
            if (num % n == id) {
                synchronized (PrintNum.class) {
                    num++;
                    if(cnt > 100){
                        break;
                    }
                    System.out.println("thread-" + id + " num:" + num);
                }
            }
        }
    }
    public static void main(String[] args) {
        for(int i = 0; i < n; i++){
            new PrintNum(i).start();
        }
    }
}

8 通过一个按钮来统计每个IP访问的次数,保证准确高可用

1、nginx配置x-forwarded-for
2、用redis的incr记录累加访问次数

9 nginx如何实现负载均衡

nginx内置负载均衡策略主要分为三大类,分别是轮询、最少连接和ip hash

轮询

以循环方式分发对应用服务器的请求,将请求平均分发到每台服务器上。

普通轮询方式

该方式是默认方式,轮询适合服务器配置相当,无状态且短平快的服务使用。另外在轮询中,如果服务器挂掉,会自动剔除该服务器。

http {

    # 定义转发分配规则

    upstream myapp1 {

        server srv1.com; # 要转发到的服务器,如ip、ip:端口号、域名、域名:端口号

        server srv2.com:8088;

        server 192.168.0.100:8088;

    }

    server {

        listen 80; # nginx监听的端口

        location / {

         # 使用myapp1分配规则,即刚自定义添加的upstream节点

         # 将所有请求转发到myapp1服务器组中配置的某一台服务器上

            proxy_pass http://myapp1;

        }

    }

}
权重轮询方式

如果在 upstream 中配置的server参数后追加 weight 配置,则会根据配置的权重进行请求分发。此策略可以与least_conn和ip_hash结合使用,适合服务器的硬件配置差别比较大的情况。


upstream myapp1 {

server srv1.com weight=1; # 该台服务器接受1/6的请求量

server srv2.com:8088 weight=2; # 该台服务器接受2/6的请求量

server 192.168.0.100:8088 weight=3; # 该台服务器接受3/6的请求量;

}
最少连接

轮询算法是把请求平均的转发给各个后端,使它们的负载大致相同;但是,有些请求占用的时间很长,会导致其所在的后端负载较高。这种情况下,least_conn这种方式就可以达到更好的负载均衡效果,适合请求处理时间长短不一造成服务器过载的情况。

upstream myapp1 {

least_conn; # 把请求分派给连接数最少的服务器

server srv1.com;

server srv2.com:8088;

server 192.168.0.100:8088;

}
ip hash

这个方法确保了相同的客户端的请求一直发送到相同的服务器,这样每个访客都固定访问一个后端服务器。如用户需要分片上传文件到服务器下,然后再由服务器将分片合并,这时如果用户的请求到达了不同的服务器,那么分片将存储于不同的服务器目录中,导致无法将分片合并,该场景则需要使用ip hash策略。

需要注意的是,ip_hash不能与backup同时使用,另外当有服务器需要剔除,必须手动down掉,此模式适合有状态服务,比如session。

upstream myapp1 {

ip_hash; # #保证每个请求固定访问一个后端服务器

server srv1.com;

server srv2.com:8088;

server 192.168.0.100:8088;

}

参考:使用nginx进行负载均衡

10 gateway底层如何实现接口转发(没太懂问的是啥,动态路由?)

11 Mybatis如何实现分页

首先分页可分为逻辑分页和物理分页:

  • 逻辑分页:逻辑分页是一次性把全部数据查询加载进内存 ,然后再进行分页。这样优点是减少IO次数,适合频繁访问、数据量少的情况。缺点是不适合大数据量,容易造成内存溢出。
  • 物理分页:物理分页是利用limit语法在数据库中进行分页。他的优点是适合分页大数据量数据。缺点是频繁查询数据库,消耗性能。

mybatis实现分页有三种方式
1.直接使用SQL语句,利用limit关键字分页(物理分页)
这种方式的缺点是比较麻烦,但是自定义性强。
2.RowBounds(逻辑分页)
这种方式是将所有的数据都查询出来放在内存中,再进行分页,不适合数据量大的情况。
3.第三方插件PageHelper(物理分页)
原理是在执行PageHelper.startPage(page,size)之后,分页参数会设置在ThreadLocal中,PageHelper会在mybatis执行sql前进行拦截,取出分页参数对sql语句进行修改,最后执行修改之后的分页语句。

12 重载和重写的区别

  • 重写:
    方法重写(Override)是一种语言特性,它是多态的具体表现,它允许子类重新定义父类中已有的方法,且子类中的方法名和参数类型及个数都必须与父类保持一致,这就是方法重写。
    注意事项 1:子类方法的权限控制符不能变小,也就是如果父类方法的权限控制符是 protected,那么子类的方法权限控制符只能是 protected 或 public;
    注意事项 2:子类方法返回的类型只能变小,也就是说如果父类方法返回的是 Number 类型,那么子类方法只能返回 Number 类型或 Number 类的子类 Long 类型,而不能返回 Number 类型的父类类型 Object;
    注意事项 3:子类抛出异常的类型只能变小;
    注意事项 4:子类方法名必须和父类方法名保持一致;
    注意事项 5:子类方法的参数类型和个数必须和父类保持一致。

  • 重载:
    方法重载是指在同一个类中,定义了多个同名方法,但同名方法的参数类型或参数个数不同就是方法重载。

13 抽象类和接口的区别

抽象类接口
构造方法可以有不可以有
方法可以有抽象方法(抽象方法只能被abstract修饰,不可被private、static、synchronized和native修饰)和普通方法只能有抽象方法,但1.8版本之后可以有默认方法。接口只有定义,不可有方法实现
实现extendimplments
类修饰符public、default、protected默认public
变量可以有常量也可以有变量只能是静态常量默认有public static final修饰,必须附上初始值,不能被修改
多继承单继承多个接口
静态方法可以有不可以

14 内存溢出和内存泄露的区别

  1. 定义不同
  • 内存溢出(Out Of Memory):程序在申请内存时,没有足够的内存空间供其使用,就会发生内存溢出。
  • 内存泄漏(Memory Leak):程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏可能影响不大,但内存泄漏积累后,会极大影响程序运行,可能导致内存耗尽甚至系统崩溃。
  1. 产生原因不同
  • 内存溢出:内存溢出的产生通常是因为程序申请的内存超出了系统能够提供的范围,比如试图创建一个超大的数组或对象,超过了系统或虚拟机的限制。
  • 内存泄漏:内存泄漏通常是由于程序的设计问题导致的,比如忘记释放已经不再使用的内存,或者引用已不需要的对象,使得这部分内存无法被回收。
  1. 处理方式不同
  • 内存溢出:对于内存溢出的问题,通常需要检查程序是否有不必要的大内存申请,或者优化程序使得内存使用更加高效。
  • 内存泄漏:对于内存泄漏的问题,首先需要找到程序中导致内存泄漏的部分,然后修复这些问题,比如及时释放不再使用的内存,或者取消对不再需要的对象的引用。
  1. 影响程度不同
  • 内存溢出:内存溢出会导致程序立即崩溃或者抛出错误,影响较大。
  • 内存泄漏:一次小的内存泄漏可能不会立即影响程序运行,但是如果大量内存泄漏累积,最终会导致内存耗尽,影响系统的正常运行。

15 springboot如何从war包部署改成jar包部署

  1. 修改pom.xml中的配置
  • 修改打包方式
<packaging>war</packaging>
  修改为 
<packaging>jar</packaging>
  • 去除spring-boot-starter-tomcat相关依赖
<!--<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-war-plugin</artifactId> 
	<configuration>
		<resourceEncodinq>utf-8</resourceEncoding>
	</configuration>
</plugin>-->
  • 添加spring-boot-maven-plugin依赖
<plugin>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-maven-plugin</artifactId>
   <configuration>
      <mainClass>com.test.one.testApplication</mainClass>
   </configuration>
</plugin>
  1. 修改启动类
  • 注释Application.java中SpringApplicationBuilder
public class testApplication SpringBootServletInitializer {
	public static void main ( String[] args ){
		new SpringApplicationBuilder(testApplication.class).web(WebApplicationType.SERVLET).run(args);
	/* @Override
		protected SpringApplicationBuilder configure(SpringApplicationBuilder builder){
		return builder.sources(SourcePoolServiceApplication.class):
*/

16 启动springboot的时候把web容器从tomcat换成jetty

  1. tomcat和jetty的对比
  • Jetty架构对比Tomcat架构更简单、更轻便,更容易拓展。
  • 二者在性能上差异不大。
    Jetty支持处理大量连接和长连接,顾更适用于web聊天室,即时通信等场景方面。
    Jetty默认采用非阻塞IO(NIO),在处理I/O请求上更占优势,在处理静态资源时,性能较高。
    Tomcat默认采用阻塞IO(BIO),在处理I/O请求相对较差,在处理静态资源时,性能较弱
  • Jetty的应用更加快速,修改更简单,对新的Servlet支持较好。而业界的谷歌,对于应用引擎也已经全面切换为Jetty。
    Tomcat目前应用比较广泛,对JavaEE和Servlet的支持也更加全面,很多特性会直接集成进来。
    2.如何替换
    在项目的依赖文件中排除tomcat的依赖并引入jetty的依赖
<!--web依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <!--排除spring-boot-starter-tomcat依赖-->
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--Jetty依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

17 什么情况下切面会失效

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值