蚂蚁实习一面面经

蚂蚁实习一面面经

希望可以帮助到大家

tcp建立连接为什么要三次握手?

三次握手的过程

注意:三次握手的最主要目的是保证连接是双工的,可靠更多的是通过重传机制来保证的

所谓三次握手,即建立TCP连接,需要客户端和服务端总共发送至少三个包确认连接的建立。流程如下

蚂蚁实习一面面经_第1张图片

为什么要三次挥手

第一次握手:

Client什么都不能确认

Server确认:自己接收正常 ,对方发送正常

第二次握手:

Client确认:自己发送/接收正常,对方发送/接收正常

Server确认:自己接收正常 ,对方发送正常

第三次握手:

Client确认:自己发送/接收正常, 对方发送/接收正常

Server确认:自己发送/接收正常,对方发送/接收正常

如果两次握手会怎么样?

有这样一种情况,当A发送一个消息给B,但是由于网络原因,消息被阻塞在了某个节点,然后阻塞的时间超出设定的时间,A会认为这个消息丢失了,然后重新发送消息。

当A和B通信完成后,这个被A认为失效的消息,到达了B

对于B而言,以为这是一个新的请求链接消息,就向A发送确认,

对于A而言,它认为没有给B再次发送消息(因为上次的通话已经结束)所有A不会理睬B的这个确认,但是B则会一直等待A的消息

这就导致了B的时间被浪费(对于服务器而言,CPU等资源是一种浪费),这样是不可行的,这就是为什么不能两次握手的原因了。

tcp断开连接为什么四次挥手

挥手过程

第一次挥手:

Clien发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

第二次挥手:

Server收到FIN后,发送一个ACK给Client,Server进入CLOSE_WAIT状态。

第三次挥手:

Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

第四次挥手:

Client收到FIN后,Client进入TIME_WAIT状态,发送ACK给Server,Server进入CLOSED状态,完成四次握手。

蚂蚁实习一面面经_第2张图片

为什么要四次挥手

四次挥手的本质原因是tcp是全双公的,通信是双向的, A到B是一个通道,B到A又是另一个通道。

A端确认没有数据发送后,发出结束报文,此时B端返回确认后,B端也不会接收A端数据。

但是此时B端可能还有数据没有传输完,A端还是可以接收数据。

只有当B端数据发送完之后,才能发出结束报文,并且确认A端接收到的时候,两边才会真正的断开连接,双方的读写分开。

四次挥手释放连接时,等待2MSL的意义?

MSL是TCP报文的最大生命周期,因为TIME_WAIT持续在2MSL就可以保证在两个传输方向上的尚未接收到或者迟到的报文段已经消失,否则服务器立即重启,可能会收到来自上一个进程迟到的数据,但是这种数据很可能是错误的,同时也是在理论上保证最后一个报文可靠到达,假设最后一个ACK丢失,那么服务器会再重发一个FIN,这是虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK。

kafaka如何保证消息可达?

消息传递语义:

蚂蚁实习一面面经_第3张图片

1)首先当 Producer 向 Broker 发送数据后,会进行 commit,如果commit成功,由于 Replica 副本机制的存在,则意味着消息不会丢失,但是 Producer 发送数据给 Broker 后,遇到网络问题而造成通信中断,那么 Producer 就无法准确判断该消息是否已经被提交(commit),这就可能造成 at least once 语义。

2)在 Kafka 0.11.0.0 之前, 如果 Producer 没有收到消息 commit 的响应结果,它只能重新发送消息,确保消息已经被正确的传输到 Broker,重新发送的时候会将消息再次写入日志中;而在 0.11.0.0 版本之后, Producer 支持幂等传递选项,保证重新发送不会导致消息在日志出现重复。为了实现这个, Broker 为 Producer 分配了一个ID,并通过每条消息的序列号进行去重。也支持了类似事务语义来保证将消息发送到多个 Topic 分区中,保证所有消息要么都写入成功,要么都失败,这个主要用在 Topic 之间的

exactly once 语义。

3)从 Consumer 角度来剖析, 我们知道 Offset 是由 Consumer 自己来维护的, 如果 Consumer 收到消息后更新 Offset, 这时

Consumer 异常 crash 掉, 那么新的 Consumer 接管后再次重启消费,就会造成 at most once 语义(消息会丢,但不重复)。

4) 如果 Consumer 消费消息完成后, 再更新 Offset,如果这时 Consumer crash 掉,那么新的 Consumer 接管后重新用这个

Offset 拉取消息, 这时就会造成 at least once 语义(消息不丢,但被多次重复处理)。

具体策略

  1. 消息持久化:Kafka使用持久化存储方式,将所有的消息写入磁盘,并且使用复制机制来保证消息的可靠性,即使在某些节点出现故障的情况下也能够保证消息的可靠性。
  2. 分区机制:Kafka将每个主题分为多个分区,每个分区都有自己的副本。当消息发送到某个分区时,它将被写入该分区的所有副本中。如果一个副本无法接收消息,则Kafka会将消息写入其他可用的副本中。
  3. Leader选举:每个分区都有一个leader副本和多个follower副本。当leader副本出现故障时,follower副本会参与到leader选举过程中,Kafka会自动选举一个新的leader副本来保证消息的可达性。
  4. 确认机制:生产者可以选择同步发送或异步发送消息。同步发送将等待所有副本都收到消息之后才会返回确认,而异步发送则会立即返回确认。这个机制可以确保消息已被正确写入所有副本。
  5. 偏移量:Kafka使用偏移量来追踪消费者消费的位置。当消费者读取完一个消息后,它会将偏移量提交回Kafka,以便下一次读取时能够从正确的位置开始读取。这样可以保证消息不会被重复消费,也不会漏掉任何消息。

线程的状态?

  1. 新建状态(New):当通过调用Thread类的构造方法创建一个线程对象时,该线程将处于新建状态。在这种状态下,线程已经被创建,但尚未开始执行。
  2. 就绪状态(Runnable):当调用线程对象的start()方法后,线程将进入就绪状态。在这种状态下,线程已经准备好执行,但尚未获得CPU时间片。
  3. 运行状态(Running):当线程获得CPU时间片并开始执行时,它将进入运行状态。
  4. 阻塞状态(Blocked):线程在执行过程中可能需要等待某些事件的发生,如I/O操作的完成或锁的释放等。在这种情况下,线程将被阻塞,并进入阻塞状态。Java中的阻塞状态分为三种:
  • 等待阻塞(Wait):线程通过调用wait()方法等待某个条件的满足。
  • 同步阻塞(Blocked on synchronization):线程等待获取某个对象的锁。
  • 其他阻塞(Blocked on Others):线程等待一些其他事件的发生,如Thread.sleep()方法调用或等待I/O操作的完成等。
  1. 暂停状态(Suspended):Java中的线程暂停状态不是线程的正式状态,而是一种用于调试的状态。当线程被挂起后,它将暂停执行,但仍处于运行状态。
  2. 终止状态(Terminated):当线程执行完毕或出现未处理的异常时,它将进入终止状态。线程在终止状态下无法被重新启动。

数据库了解哪些索引?

  1. 主键索引(Primary Key Index):是一种唯一索引,它强制要求每个记录都必须有一个唯一的标识,通常是一个自增长的整数值。
  2. 唯一索引(Unique Index):保证某个列中的所有值都是唯一的,可以用于加速查找和避免重复数据。
  3. 普通索引(Normal Index):是最基本的索引类型,可以提高查询的速度。
  4. 聚集索引(Clustered Index):在数据库表中只能有一个聚集索引,它决定了数据在磁盘上的物理存储顺序,即表数据的存储顺序和索引顺序是一致的。
  5. 非聚集索引(Non-Clustered Index):在数据库表中可以有多个非聚集索引,它不会改变数据在磁盘上的物理存储顺序,而是单独维护一个索引表来记录数据的位置信息。
  6. 全文索引(Full Text Index):用于加速全文搜索,它可以在文本数据中查找包含特定关键词的记录。
  7. 空间索引(Spatial Index):用于加速地理信息系统(GIS)等应用程序中的空间查询。
  8. 复合索引(Composite Index):基于多个列的组合来创建的索引,可以提高复合查询的性能。

索引多了有什么坏处?

  1. 空间占用:索引需要占用磁盘空间,如果数据量很大,索引也会变得很大,占用大量的磁盘空间。
  2. 写入操作变慢:每次写入操作都需要更新索引,这会导致写入操作变慢。
  3. 维护成本高:索引需要维护,例如重建、重新组织等操作,这些操作都需要时间和资源。
  4. 查询变慢:尽管索引可以提高查询效率,但是如果索引不合理或者不正确,查询效率反而会变慢。例如,如果创建了过多的索引,查询时需要扫描的索引也会增加,导致查询变慢。
  5. 数据不一致:如果索引不正确或者不同步,会导致查询结果不一致。

实现一个静态单例模式

静态单例模式是指单例对象在程序运行时就已经创建好,而不是在使用时动态创建。下面是一个实现静态单例模式的示例:静态单例模式是指单例对象在程序运行时就已经创建好,而不是在使用时动态创建。下面是一个实现静态单例模式的示例:

public class MySingleton {
    // 静态成员变量,存储单例对象
    private static MySingleton instance = new MySingleton();

    // 私有构造方法,防止其他类创建该类的实例
    private MySingleton() {
        // 初始化操作
    }

    // 静态方法,获取单例对象
    public static MySingleton getInstance() {
        return instance;
    }
}

在高并发的环境下如何设置mysql?

MySQL主从复制实现读写分离

  • 数据存储Master将数据改变记录到二进制日志(binary log)中,也就是配置文件log-bin指定的文件,这些记录叫做二进制日志事件(binary log events);
  • 数据传输Slave 通过 I/O 线程读取 Master 中的 binary log events 并写入到它的中继日志(relay log);
  • 数据重放Slave 重做中继日志中的事件,把中继日志中的事件信息一条一条的在本地执行一次,完成数据在本地的存储,从而实现将改变反映到它自己的数据。

分区

把一张表的数据分成多个区块,在逻辑上看最终只是一张表,但底层是由多个物理区块组成的。

MySQL的物理数据,存储在表空间文件(.ibdata1和.ibd)中,这里讲的分区的意思是指将同一表中不同行的记录分配到不同的物理文件中,几个分区就有几个.idb文件。

分表

分表分为水平分表和垂直分表。

水平分表

水平分表和分区很像,或者说分区就是水平分表的数据库实现版本,它们分的都是行记录,就像用一把刀,水平的将一个表切成多张表一样。

蚂蚁实习一面面经_第4张图片

垂直分表

水平分表分的是行记录,而垂直分表,分的是列字段,它就像用一把刀,垂直的将一个表切成多张表一样。

垂直分表是基于列字段进行的。一般是表中的字段较多,或者有数据较大长度较长(比如text,blob,varchar(1000)以上的字段)的字段时,我们将不常用的,或者数据量大的字段拆分到“扩展表”上。这样避免查询时,数据量太大造成的“跨页”问题。

蚂蚁实习一面面经_第5张图片

分表和分区的区别

  1. 分区只是一张表中的数据和索引的存储位置发生改变,分表则是将一张表分成多张表,是真实的有多套表的配套文件
  2. 分区没法突破数据库层面,不论怎么分区,这些分区都要在一个数据库下。而分表可以将子表分配在同一个库中,也可以分配在不同库中,突破数据库性能的限制。
  3. 分区只能替代水平分表的功能,无法取代垂直分表的功能。

分库的类型

分库同样分为水平分库和垂直分库。

水平分库
  • 水平分库和水平分表相似,并且关系紧密,水平分库就是将单个库中的表作水平分表,然后将子表分别置于不同的子库当中,独立部署。
  • 因为库中内容的主要载体是表,所以水平分库和水平分表基本上如影随形。
  • 例如用户表,我们可以使用注册时间的范围来分表,将2020年注册的用户表usrtb2020部署在usrdata20中,2021年注册的用户表usrtb2021部署在usrdata21中。

垂直分库
  • 同样的,垂直分库和垂直分表也十分类似,不过垂直分表拆分的是字段,而垂直分库,拆分的是表。
  • 垂直分库是将一个库下的表作不同维度的分类,然后将其分配给不同子库的策略。
  • 例如,我们可以将用户相关的表都放置在usrdata这个库中,将订单相关的表都放置在odrdata中,以此类推。
  • 垂直分库的分类维度有很多,可以按照业务模块划分(用户/订单...),按照技术模块分(日志类库/图片类库...),或者空间,时间等等。

如何保证线程的有序性?

join方法

用join方法来保证线程顺序,其实就是让main这个主线程等待子线程结束,然后主线程再执行接下来的其他线程任务

ExecutorService方式

这种方式的原理其实就是将线程用排队的方式扔进一个线程池里,让所有的任务以单线程的模式,按照FIFO先进先出、LIFO后进先出、优先级等特定顺序执行,但是这种方式也是存在缺点的,就是当一个线程被阻塞时,其它的线程都会受到影响被阻塞,不过依然都会按照自身调度来执行,只是会存在阻塞延迟。

volatile的使用

在Java中,volatile是一种关键字,用于告诉编译器和JVM,该变量的值可能被其他线程修改,因此每次访问该变量时都需要从内存中读取最新的值,而不是使用缓存的值。这可以确保多线程环境下该变量的值的一致性。

//使用了volatile
public class VolatileDemo {
    public static volatile boolean stop = false;//任务是否停止,volatile变量
    public static void main(String[] args) throws Exception {
        Thread thread1 = new Thread(() -> {
            while (!stop) { //stop=false,不满足停止条件,继续执行
                //do someting
            }
            System.out.println("stop=true,满足停止条件。" +
                    "停止时间:" + System.currentTimeMillis());
        });
        thread1.start();

        Thread.sleep(100);//保证主线程修改stop=true,在子线程启动后执行。
        stop = true; //true
        System.out.println("主线程设置停止标识 stop=true。" +
                "设置时间:" + System.currentTimeMillis());
    }
}

threadlocal的作用以及可能产生的问题?

作用:

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果实现每个线程都有自己的专属变量该如何设置?ThreadLocal类主要就是解决让每个线程绑定自己的专属变量。

ThreadLocal主要用来存储当前上下文的变量信息,他可以保障存储进去的信息只能被当前的线程读取到,并且线程之间不会受到影响。ThreadLocal为变量在每个线程都创建了一个副本,那么每个线程可以访问自己的内部的副本变量。

可能产生的问题

1.ThreadLocal内存泄漏是指ThreadLocal对象中持有的变量副本没有被及时清理而导致的内存泄漏问题。当一个线程结束时,如果ThreadLocal对象中持有的变量副本没有被清理,这些变量副本会一直存在于内存中,无法被回收,从而导致内存泄漏。

解决方案:

ThreadLocalMap实现中已经考虑了这种情况,在调用set()、get()、remove()方法时会处理掉key为null的记录。使用ThreadLocal时,最好手动调用remove()方法。

2.上下文切换开销:每个线程都有自己的变量副本,当线程切换时,需要保存和恢复这些变量,这可能会带来一些上下文切换的开销。

你可能感兴趣的:(面试题记录,网络,服务器,阿里巴巴,蚂蚁,面试,实习,Java)