1.上一段实习为什么离职
在我上一段实习中,我获得了宝贵的工作经验,并且学到了很多关于Java后端开发的实用技能。然而,离职的主要原因是实习期满,我需要继续我的学业并寻找新的挑战。此外,我也在寻找一个能够提供更长期职业发展机会的平台,这样我就可以更深入地参与项目,并在技术栈和业务理解上有更多的成长。我认为贵公司在业界的声誉和技术创新能力非常吸引我,我相信在这里我能获得更多的学习和成长机会,同时也能为团队贡献我的热情和技能
2.介绍一下你之前实习的部门干什么的,负责的哪些部分与整个业务的关系
在我上一段实习中,我所在的部门主要负责公司的核心业务系统,这是一个为企业提供定制化解决方案的平台。我们的工作是确保系统的稳定性、性能和可扩展性,以支持公司不断增长的用户基础和业务需求。
我在这个部门中主要负责以下几个部分:
-
API开发与维护:我参与了RESTful API的开发,这些API是前端应用与后端服务交互的桥梁。我负责确保API的响应速度和数据准确性,同时遵循最佳实践来设计易于理解和使用的接口。
-
数据库管理:我参与了数据库的设计和优化工作,包括创建索引、优化查询和处理数据迁移。我学习了如何通过数据库调优来提高应用性能。
-
系统架构优化:我协助团队对现有系统架构进行评估和优化,以提高系统的处理能力和容错性。这包括引入缓存机制、负载均衡和微服务架构的初步探索。
-
代码审查与团队协作:我积极参与代码审查过程,以保证代码质量,并与团队成员紧密合作,确保项目按时交付。
这些工作与整个业务的关系非常紧密。后端系统是支撑整个业务运行的基石,它直接影响到用户体验和业务效率。通过优化后端服务,我们能够提供更快的响应时间,处理更大的并发量,并且能够更快地推出新功能,从而保持公司在激烈的市场竞争中的领先地位。
在实习期间,我深刻理解了后端开发在业务中的重要性,并且积累了实际的工作经验,这让我更加渴望加入贵公司,继续在后端开发领域深耕细作
3.Https和Http的区别,了解过M-TLS吗?
HTTP(超文本传输协议)和HTTPS(安全超文本传输协议)是两种常用的网络协议,它们的主要区别在于安全性和数据传输方式:
-
HTTP(HyperText Transfer Protocol):
- 定义:HTTP是一种用于从网络传输超文本到本地浏览器的传输协议,它定义了客户端与服务器之间请求和响应的格式。
- 端口:HTTP默认使用80端口。
- 安全性:HTTP是明文传输,不提供数据加密,因此传输的数据可能会被截获和篡改。
-
HTTPS(HyperText Transfer Protocol Secure):
- 定义:HTTPS是HTTP的安全版本,它在HTTP的基础上通过SSL/TLS协议提供了数据加密、数据完整性验证和身份验证。
- 端口:HTTPS默认使用443端口。
- 安全性:HTTPS使用SSL/TLS协议对传输的数据进行加密,确保数据在传输过程中的安全性,防止数据在传输过程中被窃取或篡改。
- 证书:HTTPS需要服务器端有SSL证书,客户端通过验证证书来确认服务器的身份,防止中间人攻击。
关于M-TLS(Mutual TLS,双向TLS):
M-TLS是一种TLS协议的扩展,它不仅在服务器端进行身份验证,还在客户端进行身份验证。在M-TLS中,服务器和客户端都必须提供证书来证明自己的身份。这种双向认证机制提供了比单向TLS(仅服务器认证)更强的安全性。
M-TLS的工作流程:
- 客户端发起请求,并发送其证书。
- 服务器接收请求,并验证客户端证书的有效性。
- 如果客户端证书有效,服务器也发送其证书给客户端。
- 客户端验证服务器证书的有效性。
- 如果双方都验证通过,使用TLS协商加密算法和密钥,然后开始加密通信。
M-TLS在需要高度安全性的场景中非常有用,比如金融服务、企业内部通信等,因为它提供了更强的安全性和信任保证。然而,M-TLS的实施和管理比单向TLS更复杂,因为它需要在客户端和服务器端都管理证书。
4.Java内存空间,字符串存在哪?为什么方法区也有
在Java中,内存空间主要分为几个部分:堆(Heap)、栈(Stack)、方法区(Method Area)/ 元数据区(Metaspace,Java 8 及以后版本)、程序计数器(Program Counter Register)和直接内存(Direct Memory)。下面我将解释字符串存储的位置以及为什么方法区(或元数据区)也会包含字符串信息。
-
堆(Heap):
- 堆是Java虚拟机(JVM)中最大的一块内存区域,主要用于存储对象实例和数组。字符串对象在Java中也是对象,因此它们通常存储在堆上。
- Java中的字符串是不可变的,这意味着一旦创建了一个字符串对象,它的值就不能被改变。因此,如果两个字符串字面量相同,JVM会使用字符串常量池来存储这个字符串,以节省内存。
-
字符串常量池(String Pool):
- 在JVM中,字符串常量池是一个特殊的存储区域,用于存储字符串常量。在Java 7及以前版本中,字符串常量池位于方法区;从Java 8开始,字符串常量池被移到了堆上。
- 字符串常量池的主要目的是优化字符串的存储,避免相同字符串的重复创建,节省内存空间。
-
方法区/元数据区(Method Area/Metaspace):
- 在Java 7及以前版本中,方法区是JVM规范中定义的一个内存区域,用于存储类信息、常量、静态变量等数据。字符串常量池也是方法区的一部分。
- 从Java 8开始,方法区被元数据区(Metaspace)取代,元数据区使用本地内存来存储类元数据,而不是虚拟机内存。这样做的目的是为了减少Java堆的压力,并允许动态扩展。
- 尽管字符串常量池已经从方法区/元数据区中移出,但类和接口的名称、字段名称、方法名称等字符串信息仍然存储在方法区/元数据区中,因为这些信息是类和接口的元数据的一部分。
-
栈(Stack):
- 栈是线程私有的内存区域,用于存储局部变量和方法调用的信息。当一个方法被调用时,它的局部变量和操作数栈会被压入栈中。字符串变量的引用(指向堆中字符串对象的指针)可能会存储在栈上。
-
程序计数器(Program Counter Register):
- 程序计数器用于存储当前线程执行的字节码指令的地址。
-
直接内存(Direct Memory):
- 直接内存不是JVM内存模型的一部分,但它是JVM通过NIO库直接管理的内存区域,用于存储大对象,如文件或网络数据。
总结来说,字符串对象本身存储在堆上,而字符串常量(在Java 7及以前版本)或字符串字面量(在Java 8及以后版本)存储在字符串常量池中,这个池在Java 7及以前版本位于方法区,在Java 8及以后版本位于堆上。方法区/元数据区存储的是类和接口的元数据,包括类和接口的名称、字段名称、方法名称等字符串信息。
5.堆里面存什么?对象实例都放在里面吗?有没有例外
堆中主要存放对象实例和数组。几乎所有的对象实例都在堆上分配内存。以下是一些具体的点:
1. **对象实例**:包括用户定义的对象、数组等,都在堆上分配空间。
2. **数组**:所有的数组,无论其元素类型如何,都在堆上存储。
**例外**:
- **字符串常量**:在字符串常量池中(Java 8 及以后版本位于堆上,Java 7 及以前版本位于方法区)。
- **基本数据类型**:如int、double等,它们通常存储在栈上或寄存器中,而不是堆上。
- **部分匿名内部类**:如果匿名内部类没有引用外部类的this引用,那么它的实例可能不会存储在堆上,而是在栈上分配。
6.volatile关键字
`volatile`关键字在Java中用于修饰变量,它有以下几个主要作用:
1. **保证可见性**:当一个变量被声明为`volatile`时,JVM保证了对该变量的读写操作对所有线程都是可见的。这意味着,当一个线程修改了这个变量的值,新值对其他线程来说是立即可见的。
2. **禁止指令重排**:`volatile`关键字能够防止编译器和处理器对读写操作进行指令重排,确保在多线程环境下,对`volatile`变量的读写操作能够按照程序的顺序执行。
3. **不保证原子性**:尽管`volatile`保证了可见性和禁止指令重排,但它并不保证复合操作(如`i++`)的原子性。对于需要原子性的操作,应该使用`synchronized`关键字或`java.util.concurrent`包下的原子类。
简而言之,`volatile`关键字是一种轻量级的同步机制,适用于读操作多于写操作,且对性能要求较高的场景。但它不适用于需要保证复合操作原子性的场景。
7.redis数据结构redis为什么快
Redis之所以快,主要原因可以总结为以下几点:
1. **基于内存操作**:Redis的所有数据都存储在内存中,内存的访问速度远超过磁盘,这使得Redis能够快速地读取和写入数据。
2. **数据结构简单且高效**:Redis的数据结构都是专门设计的,如哈希表、跳表等,这些数据结构的操作时间复杂度大部分是O(1),因此性能非常强。
3. **多路复用和非阻塞I/O**:Redis使用I/O多路复用来监听多个socket连接,可以使用一个线程处理多个请求,减少线程切换带来的开销,同时也避免了I/O阻塞操作。
4. **单线程模型**:Redis的主线程是单线程的,避免了不必要的上下文切换和多线程竞争(比如锁),这就省去了多线程切换带来的时间和性能上的消耗。
5. **合理的数据编码**:Redis根据不同的数据类型和长度,适配不同的编码格式,以提高内存使用效率和操作性能。
6. **事件驱动模型**:Redis使用事件驱动模型来处理客户端请求和网络IO,通过事件循环机制来处理事件,避免了多线程或多进程的开销。
7. **持久化机制**:虽然Redis主要依赖内存存储,但它也提供了数据持久化机制(如RDB和AOF),以确保在内存数据丢失时能够从持久化文件中恢复数据,这种机制在性能和数据可靠性之间取得了平衡。
这些因素共同作用,使得Redis在处理大量并发请求时能够保持高性能。
9.redis后面引入了多线程,为什么
Redis在6.0版本中引入了多线程,主要原因有以下几点:
1. **提升I/O读写性能**:在Redis 6.0之前,尽管Redis采用了非阻塞的I/O模型,比如epoll,来处理大量的并发连接,但当网络成为瓶颈时(例如,大量的网络请求需要处理,或者网络延迟较高),单线程模型可能会成为限制性能的因素。因此,Redis 6.0通过引入多线程来提高I/O的读写性能。
2. **充分利用多核CPU资源**:在多核CPU时代,单线程模型无法充分利用服务器的CPU资源。Redis 6.0引入多线程可以更好地利用多核CPU,提高处理能力。
3. **分摊同步IO读写负荷**:读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的IO消耗。多线程任务可以分摊Redis同步IO读写负荷,降低耗时。
4. **提高网络请求处理的并行度**:随着网络硬件性能的提升,Redis的性能瓶颈有时会出现在网络IO的处理上,即单个主线程处理网络请求的速度跟不上底层网络硬件的速度。通过多个IO线程并行处理网络操作,可以提升实例的整体处理性能。
5. **解决大数据key删除的性能问题**:在Redis 4.0版本中,已经开始支持多线程用于大数据量的异步删除,例如unlink key、flushdb async、flushall async等。而Redis 6.0的多线程增加了对I/O读写的并发能力,进一步提升了性能。
综上所述,Redis引入多线程主要是为了解决网络I/O的性能瓶颈,提高并发处理能力,以及充分利用多核CPU资源,从而提升整体性能。
10.缓存三兄弟
缓存三兄弟指的是在Redis缓存使用中常见的三个问题:缓存穿透、缓存击穿和缓存雪崩。
1. **缓存穿透**:指请求查询数据库和缓存中都不存在的数据,导致请求每次都直接打到数据库上,对数据库造成压力。解决方案包括:
- 缓存空值:将不存在的数据也缓存起来,设置较短的过期时间。
- 使用布隆过滤器:通过布隆过滤器判断请求的数据是否一定不存在,从而避免对数据库的访问。
2. **缓存击穿**:指缓存中某个热点数据过期的瞬间,大量请求同时到达,导致数据库压力突增。解决方案包括:
- 使用互斥锁:在缓存过期时,使用锁确保只有一个请求能够查询数据库并回写缓存。
- 逻辑过期:通过设置合理的过期时间,避免缓存在同一时间大量过期。
3. **缓存雪崩**:指缓存中大面积数据同时过期,导致大量请求同时访问数据库。解决方案包括:
- 设置不同的过期时间:为不同的key设置不同的过期时间,避免同时过期。
- 使用多级缓存:通过不同级别的缓存设置不同的失效时间,减少对数据库的直接访问。
- 缓存预热:在系统启动或数据更新时,预先加载热点数据到缓存中。
这三个问题都是Redis缓存使用中需要特别注意和处理的问题,以确保系统的稳定性和性能。
11.MySQL事务隔离级别
MySQL中的事务隔离级别用于解决并发事务中的一些问题,如脏读、不可重复读和幻读。MySQL支持以下四种事务隔离级别:
1. **READ UNCOMMITTED(读未提交)**:
- 在这个级别,事务中的修改即使没有提交,对其他事务也是可见的。这会导致脏读,即一个事务读取到另一个事务未提交的数据。
2. **READ COMMITTED(读已提交)**:
- 事务只能读取到其他事务已经提交的数据。这个级别可以避免脏读,但仍然可能遇到不可重复读的问题,即在一个事务中,多次读取同一数据集合时可能会得到不同的结果。
3. **REPEATABLE READ(可重复读)**:
- MySQL的默认隔离级别。在这个级别,事务会看到在事务开始时已经存在的数据,对于在这个事务开始之后由其他事务提交的数据更新和插入操作,这个事务是看不到的。这个级别可以避免脏读和不可重复读,但仍然可能遇到幻读,即在同一个事务中,对于同一条记录的多次查询可能会得到不同的行数。
4. **SERIALIZABLE(串行化)**:
- 最严格的隔离级别,事务会完全隔离,事务会顺序执行,这可以避免脏读、不可重复读和幻读,但会严重影响并发性能。
每种隔离级别都有其适用场景,选择适当的隔离级别需要在数据一致性和系统性能之间做出权衡。
12.可重复读解决幻读了吗?
在MySQL中,可重复读(REPEATABLE READ)隔离级别解决了不可重复读的问题,但并没有解决幻读的问题。
**不可重复读**是指在一个事务内,多次读取同一数据集合时,由于其他事务的修改,可能会得到不同的结果。
**幻读**是指在一个事务内,多次查询返回的记录数不一致,这通常是因为其他事务插入了新行。
在MySQL的可重复读隔离级别下,事务会看到事务开始时已经存在的数据,对于在这个事务开始之后由其他事务提交的数据更新和插入操作,这个事务是看不到的。这意味着,如果你使用相同的查询条件,你将得到相同的结果集,解决了不可重复读的问题。
然而,幻读涉及到的是行数的变化,而不是行内容的变化。在可重复读隔离级别下,如果你在事务中执行了插入操作,而其他事务同时插入了新的行,你的事务再次查询时可能会发现行数增加了,这就是幻读现象。
要解决幻读问题,需要使用串行化(SERIALIZABLE)隔离级别,这个级别会锁定查询涉及的所有行,从而防止其他事务的插入操作,但这会严重影响并发性能。在实际应用中,通常需要根据业务需求和性能要求来选择合适的隔离级别。
13.Synchronized可重入原理
`synchronized`关键字在Java中是可重入锁,这意味着一个线程可以多次获得同一个锁。其可重入的原理主要基于以下几个点:
1. **锁对象的计数器**:当一个线程获取了某个对象的`synchronized`锁后,JVM会在这个对象的锁对象(Monitor)中增加一个计数器,记录该线程获取锁的次数。
2. **重入过程**:如果同一个线程再次尝试获取该对象的锁,JVM会检查计数器的值,并再次增加它,而不是阻塞线程。当线程离开同步代码块时,计数器会相应减少。只有当计数器减到零时,锁才会被释放。
3. **Monitor对象**:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。当线程执行到`monitorenter`指令时,会尝试获取Monitor对象的所有权(即获取锁),如果成功,计数器增加;执行`monitorexit`指令时,计数器减少。
4. **避免死锁**:由于`synchronized`是可重入的,它避免了自己把自己锁死的情况,即一个线程可以再次进入自己已经持有的对象锁的同步方法或代码块。
5. **线程安全**:这种机制确保了线程安全,因为锁的获取和释放是成对出现的,即使在递归或嵌套调用的情况下也能保持正确的锁状态。
总结来说,`synchronized`的可重入性是通过内部的计数器机制实现的,这个计数器跟踪了同一个线程获取锁的次数,从而允许一个线程多次进入临界区,而不会自己阻塞自己。
14.Synchronized和lock区别
`synchronized`和`Lock`都是Java中用于实现线程同步的手段,但它们之间存在一些关键区别:
1. **锁的实现方式**:
- `synchronized`是Java内置的关键字,由JVM直接支持,使用起来简单,但功能相对有限。
- `Lock`是一个接口,属于java.util.concurrent.locks包,提供了更灵活的锁定机制,需要显式地获取和释放锁。
2. **功能丰富性**:
- `synchronized`仅提供了基本的同步功能,如排他锁。
- `Lock`接口提供了更丰富的功能,如尝试非阻塞获取锁(`tryLock()`)、可中断的锁获取(`lockInterruptibly()`)、超时锁获取(`tryLock(long time, TimeUnit unit)`)等。
3. **锁的公平性**:
- `synchronized`无法保证线程获取锁的公平性,即无法确定等待时间最长的线程会先获得锁。
- `Lock`接口允许通过构造函数指定是否是公平锁,公平锁会按照线程等待的顺序来分配锁。
4. **锁绑定多个条件**:
- `synchronized`只能与一个条件(Condition)关联。
- `Lock`可以绑定多个条件,通过`newCondition()`方法创建。
5. **锁状态查询**:
- `synchronized`无法直接查询锁的状态,如是否被锁定。
- `Lock`提供了`isLocked()`、`isHeldByCurrentThread()`等方法来查询锁的状态。
6. **锁的释放**:
- `synchronized`在代码块执行完毕后自动释放锁,或者在`finally`块中通过`unlock()`方法释放。
- `Lock`需要程序员在`finally`块中显式调用`unlock()`方法来释放锁,否则可能导致死锁。
7. **异常处理**:
- `synchronized`在获取或释放锁时,如果发生异常,锁会被自动释放。
- `Lock`在获取或释放锁时,如果发生异常,需要程序员手动释放锁,否则也可能导致死锁。
8. **可重入性**:
- `synchronized`和`Lock`都是可重入锁,同一个线程可以多次获取同一个锁。
总的来说,`Lock`提供了比`synchronized`更高级的线程同步控制,适用于需要更复杂同步控制的场景,但也需要程序员更仔细地管理锁的获取和释放。
15.线程间通信的方式
线程间通信主要依赖于共享内存和同步机制。以下是一些线程间通信的常见方式:
1. **共享变量**:
- 线程可以通过访问共享变量来通信。这通常需要同步机制(如`synchronized`关键字、`Lock`接口)来保证线程安全。
2. **wait() 和 notify()/notifyAll()**:
- 这些方法是`Object`类的一部分,允许线程在某个条件不满足时等待,并通过`notify()`或`notifyAll()`方法唤醒一个或所有等待的线程。
3. **CountDownLatch**:
- 一个同步辅助类,允许一个或多个线程等待一组操作在其他线程中完成。
4. **CyclicBarrier**:
- 类似于`CountDownLatch`,但`CyclicBarrier`可以重复使用,并且所有线程必须到达屏障点后才能继续执行。
5. **Semaphore**:
- 一个计数信号量,用于控制对资源的访问数量。
6. **Exchanger**:
- 一个同步辅助类,允许两个线程交换数据。
7. **BlockingQueue**:
- 一个线程安全的队列,生产者线程可以往队列里添加元素,消费者线程可以从队列里取出元素,支持阻塞操作。
8. **Concurrent包中的原子类**:
- 如`AtomicInteger`、`AtomicLong`等,提供了一种无锁的线程安全编程方式。
9. **volatile关键字**:
- 确保变量的可见性,即一个线程修改的变量对其他线程立即可见。
10. **Condition**:
- `Lock`接口中的条件变量,允许线程在某个条件不满足时等待,并在条件满足时被唤醒。
11. **CompletableFuture**:
- 一个基于future的异步编程工具,支持异步操作的组合、转换和处理。
12. **管道通信(Piped Streams)**:
- 通过`PipedInputStream`和`PipedOutputStream`(或`PipedReader`和`PipedWriter`)连接两个线程,实现双向数据传输。
这些方式可以单独使用,也可以组合使用,以满足不同的线程间通信需求。选择哪种方式取决于具体的应用场景和性能要求。
16.kafka怎么实现精准一次
Kafka实现精准一次(Exactly-Once)语义主要依赖于两个核心特性:幂等性(Idempotence)和事务(Transactions)。
1. **幂等性(Idempotence)**:
- Kafka通过幂等性保证了单个分区内的精确一次语义。当生产者配置了`enable.idempotence=true`后,生产者发送的操作是幂等的,即如果出现导致生产者重试的错误,同样的消息由同样的生产者发送多次,将只被写入Kafka broker的日志中一次。
- 幂等性的实现依赖于为每条消息分配一个序列号,这个序列号被持久化到副本日志中,即使分区的leader发生变更,新的leader仍然可以识别重复的消息。
2. **事务(Transactions)**:
- Kafka通过事务API支持跨分区的原子写入,这允许生产者发送一批消息到不同分区,这些消息要么全部对任何一个消费者可见,要么对任何一个消费者都不可见。
- 事务的实现涉及到额外的RPC请求来定位TransactionCoordinator并初始化数据,消息发送前需要向TransactionCoordinator同步请求添加分区,并将事务状态的变化写入到`__transaction_state` Topic中。
3. **消费者侧的Exactly-Once**:
- Kafka通过消费位点的提交来控制消费进度,而消费位点的提交被抽象成向系统topic发送消息,这样就使得发送和消费行为统一起来,只要解决了多分区发送消息的一致性就能实现Exactly-Once语义。
4. **实现细节**:
- Kafka的Exactly-Once语义实现涉及到事务初始化、事务开始、Consume-Process-Produce流程、提交或回滚事务等步骤。这些步骤确保了在流计算场景下,数据从Kafka源主题消费、处理、再生产到目标主题的过程中,每条消息恰好被处理一次。
通过上述机制,Kafka能够在流处理场景中提供端到端的Exactly-Once语义,尽管在实际应用中可能存在一些性能和功能上的限制。开发者和架构师应当充分理解这些概念,并在设计系统时考虑如何有效地利用Kafka的事务功能,以构建更加健壮和可靠的数据处理流程。