美团一面
1.介绍一下你的第一个项目
这个就不多说了,主要是根据自己的简历上面的项目进行一个简短的概括使用的技术栈和什么背景解决了什么问题等等。
2.线程安全的类有哪些,平时有使用过哪些,主要解决什么问题
在Java中线程安全的类比如在Collection接口下的vector,queue底下的Bolockingqueue,还有map接口下的Hashtable,ConcurrentHashMap都是线程安全的,还有StringBuffer也是线程安全的,还有在Java的JUC包下的类都是线程安全的。
平时用过的是StringBuffer使用过,StringBuffer主要是用于在对可变字符串的频繁添加和修改过程过中使用。比如在家政项目的门户模块中,有一个添加地址的使用,就是用到了StringBuffer,顾名思义线程安全的类主要是为了保障线程安全的,避免多线程对共享变量的操作引起数据的一致性问题。
3.mysql的日志文件有哪些,分别介绍一下其作用
mysql主要有有三种日志文件分别为:binlog,undolog,redolog三类
binlog又称为归档日志(二进制日志),主要是对数据库的中的数据进行数据备份,崩溃恢复,数据复制等操作,binlog主要是记载了有关DDL,DML语句对数据库的修改和添加操作,例如:insert,update,delete等操作都会记录在binlog日志文件中。在mysql的主从复制方面,会用到binlog,主节点主要是对数据的写操作记录到binlog中,而从节点主要是读操作,从而减少了数据库的压力。
undolog又称为回滚日志,主要是为了事务的回滚操作,当一个数据要进行修改时,首先undo log会记录修改前的数据状态,当该事务出现故障或者崩溃时,会通过Undolog进行回滚操作,并且在保证事务的隔离性方面使用了MVCC机制(采用的就是undolog链+redeView)
redolog又称为重做日志,主要是为了事务的崩溃恢复和数据持久操作,redolog会记录该数据的操作状态,当一个事务出现崩溃时,会通过redolog进行崩溃恢复,并且会通过一定的刷盘机制,将修改后的数据刷新到磁盘中。
4.项目中为什么使用redis,redis快在哪里,redis是怎么保证高性能,高并发的
项目中主要使用redis做缓存操作,比如在家政项目的门户界面使用了redis做缓存,可以提高门户界面的效率
redis的快主要是因为redis是基于纯内存的操作,相比于磁盘的操作来说速度会比较快,并且redis的主线程是单线程嘛,单线程避免了多线程带来的上下文切换所带来的性能开销和多线程安全型的保证,因此也可以提高运行效率,再者redis还采用了IO多路复用技术技术,在面对多个网络请求时,可以显著提高响应速度,并且redis还有这高效的数据结构,例如散列表,集合等。
redis在保证高性能方面,redis是基于key-value键值对的数据机构嘛,因此在查找数据方面得时间复杂度为O(1),并且redis也提供了数据的持久化机制,RDB,AOF和两者得混合使用都保证了redis得高性能方面
redis在保证高并发方面,redis可以通过集群方案,哨兵方案,主从复制等方面来理解。
5.redis字典结构,,rehash,hash冲突怎么办,负载因子
Redis 中的字典(dict)是一种用于存储键值对的数据结构。它是一个散列表(Hash Table),也被称为哈希表。字典结构由三部分组成:哈希表(dictEntry **ht [2])、rehash 索引(int rehashidx)和一些统计信息(如字典中键值对的数量等)。
哈希表主要由哈希表节点+哈希表实际大小组成,哈希表节点是由三个域组成的(键,值,指向下一个哈希表节点的指针),而哈希表实际大小一般是2的幂次方
rehash索引是指;当哈希表中的键值对数量(dictEntry 的数量)与哈希表大小(size)的比例达到一定阈值(如 Redis 中负载因子大于等于 1 时),为了保持哈希表的性能,需要对哈希表进行扩展(或者收缩,如果键值对数量过少)。这一过程就是 rehash。
rehash的过程:在 rehash 过程中,Redis 会创建一个新的、更大(或更小)的哈希表(ht [1])。新哈希表的大小通常是原哈希表大小的 2 倍(扩展时)或者原哈希表大小的 1/2(收缩时)。
rehashidx 初始化为 0,表示开始对原哈希表(ht [0])中的第 0 个桶进行 rehash 操作。在 rehash 过程中,会逐个将原哈希表中的键值对重新计算哈希值,然后插入到新的哈希表中。
当 rehash 操作完成时,rehashidx 会被设置为 - 1,表示 rehash 过程结束,此时新的哈希表(ht [1])会替换原哈希表(ht [0])成为正式的哈希表
负载因子为 1 的原因
空间与时间的平衡
减少内存浪费:如果负载因子设置得过低,例如小于 1,意味着哈希表中有较多的空闲空间。虽然这可能会减少哈希冲突的概率(因为每个桶对应较少的键值对),但会造成内存的浪费。Redis 是一个内存数据库,内存资源是非常宝贵的,过多的空闲空间不利于内存的高效利用。
控制哈希冲突在可接受范围内:当负载因子为 1 时,表明哈希表的使用程度相对较高,但又不至于过度拥挤。在这个负载因子下,哈希冲突的数量在可接受的范围内。哈希冲突是指不同的键经过哈希计算后得到相同的桶索引,从而在桶中形成链表(用于存储这些冲突的键值对)。如果负载因子过高,哈希冲突会过多,导致查询操作在链表中查找的时间复杂度增加,影响性能;而负载因子为 1 时,在保证内存利用率的同时,哈希冲突对性能的影响相对较小。
性能优化与调整开销
触发 rehash 操作的时机:当负载因子达到 1 时,Redis 会触发 rehash 操作。rehash 操作是为了扩展哈希表的大小,以适应更多的键值对存储需求。选择负载因子为 1 作为触发 rehash 的阈值,可以在哈希表即将变得拥挤之前进行调整,避免性能急剧下降。如果负载因子的阈值设置得过高,哈希表可能会过度拥挤,导致查找、插入和删除操作的性能显著降低;而如果阈值设置得过低,会导致频繁的 rehash 操作,rehash 操作本身也需要消耗一定的系统资源(如 CPU 时间和内存),影响整体性能。
6.jvm有哪些参数,用过哪些指令
JVM(Java Virtual Machine)有众多的参数,可分为标准参数、非标准参数(-X 参数)和不稳定参数(-XX 参数):
一、标准参数
帮助相关参数
-help或-?:用于显示所有的标准参数信息。这有助于开发人员快速了解 JVM 的基本参数使用方法。
版本相关参数
-version:打印 JVM 版本信息并退出。例如,在命令行中执行java -version,会显示 Java 的版本号、Java 运行时环境(JRE)的构建版本以及 Java HotSpot ™ 64 - Bit Server VM 等相关信息。
-showversion:在启动 Java 程序时先显示版本信息,然后正常启动程序。
类路径相关参数
**-cp或-classpath:**用于指定类路径。例如java -cp /path/to/classes:/path/to/jars MyClass,其中/path/to/classes和/path/to/jars是包含类文件和 JAR 文件的目录,MyClass是要运行的主类。这对于确保 JVM 能够找到所需的类文件非常重要。
二、非标准参数(-X 参数)
堆内存相关参数
-Xms:设置 JVM 初始堆内存大小。例如-Xms512m表示初始堆内存为 512MB。合适的初始堆内存大小可以避免 JVM 在运行初期频繁调整堆内存大小。
-Xmx:设置 JVM 最大堆内存大小。如-Xmx1024m,限制了堆内存的最大容量为 1024MB。这有助于防止 JVM 因内存不足而导致程序崩溃或出现性能问题。
栈内存相关参数
-Xss:设置每个线程的栈内存大小。例如-Xss1m表示每个线程的栈内存为 1MB。栈内存大小会影响线程的创建数量和每个线程的执行情况,过小的栈内存可能导致栈溢出错误。
垃圾回收相关参数(部分)
-Xloggc::指定垃圾回收日志文件的路径。例如-Xloggc:/var/log/gc.log,这样可以方便地记录和分析垃圾回收的情况,有助于排查内存泄漏等问题。
三、不稳定参数(-XX 参数)
垃圾回收器相关参数
-XX:+UseSerialGC:启用串行垃圾回收器。在单 CPU 环境或者对暂停时间要求不高的小型应用中,串行垃圾回收器可能是一种简单有效的选择。
-XX:+UseParallelGC:启用并行垃圾回收器。适用于多 CPU 环境下,注重吞吐量的应用程序。并行垃圾回收器可以利用多个 CPU 同时进行垃圾回收操作,提高垃圾回收效率。
-XX:+UseConcMarkSweepGC(简称 CMS):启用并发标记 - 清除垃圾回收器。这种垃圾回收器旨在减少垃圾回收时的停顿时间,适用于对响应时间要求较高的应用程序。
-XX:+UseG1GC:启用 G1 垃圾回收器。G1 垃圾回收器是一种面向服务器端应用的垃圾回收器,它具有可预测的停顿时间,并且在处理大内存应用时表现出色。
堆内存结构相关参数
-XX:NewRatio=:设置年轻代(Young Generation)和年老代(Old Generation)的比例。例如-XX:NewRatio = 2表示年老代与年轻代的比例为 2:1。合理调整这个比例可以根据应用程序的对象分配和回收特性来优化内存使用。
-XX:SurvivorRatio=:设置 Eden 区和 Survivor 区的比例。例如-XX:SurvivorRatio = 8表示 Eden 区与每个 Survivor 区的比例为 8:1。这有助于优化年轻代内存的分配和回收效率。
其他性能相关参数
-XX:MaxTenuringThreshold=:设置对象晋升到老年代的最大年龄阈值。例如-XX:MaxTenuringThreshold = 15,对象在年轻代中经过的垃圾回收次数达到这个阈值后就会晋升到老年代。这个参数可以根据对象的生命周期特点进行调整,以优化内存管理。
-XX:+PrintGCDetails:打印详细的垃圾回收细节。这对于分析垃圾回收过程、排查内存相关问题非常有用。
7.如何保证缓存和数据库中的数据一致性问题
关于保证缓存和数据库中的数据一致性问题,主要的策略就是延迟双删+消息队列或者使用阿里巴巴开源的组件Canal 监听mysql的binlog日志,在同步写入到MQ中,redis客户端去拉取MQ中的数据即可。
关于第一种方案:延迟双删+消息队列,双删的目的是最大程度的保证缓存和数据库中数据的一致性问题,延迟的作用在于第二次删除的时候要等待一会再删除是为了避免因为并发问题导致保存旧值的情况发生,所以会延迟一段时间之后在删除,这样即使有并发问题,也可最大限度解决保存旧值问题,因为是延迟后删除的,可以最大程度保证缓存和数据库的最终一致性,而为什么需要到消息队列呢?是因为消息队列有消息确认应答机制,它可以保证执行完第一次删除缓存之后,即使此时服务器宕机了,也可以保证执行后续的流程,可以保证业务的完整性。
关于第二种方案:Canal 监听mysql的binlog日志,在同步写入到MQ中,redis客户端去拉取MQ中的数据即可。整体流程如下:
关于Canal的原理:
8.容器化技术,主要解决什么问题,原理是啥
容器化技术主要解决以下几个问题:环境一致性问题,资源利用率问题,应用程序隔离问题,运维和部署等问题。
环境一致性问题:
- 在传统的软件开发和部署过程中,开发环境、测试环境和生产环境往往存在差异。例如,开发人员在自己的本地环境开发应用程序,使用特定版本的操作系统、库和依赖项。当将应用程序部署到测试环境或生产环境时,由于环境的不同(如操作系统版本、库的更新等),可能会出现应用程序无法正常运行的情况。
- 容器化技术通过将应用程序及其所有依赖项(包括操作系统、运行时环境、库等)打包成一个独立的容器,确保在不同的环境(开发、测试、生产等)中运行时具有完全相同的环境配置。这样,无论在哪个环境中运行容器,应用程序都能按照预期工作。
资源利用率问题:
- 在传统的服务器部署中,为了运行多个应用程序,往往需要为每个应用程序单独分配物理服务器或者虚拟机。这可能导致资源的浪费,因为每个应用程序的资源需求在不同时间可能会有很大变化,而分配的资源却是固定的。
- 容器化技术允许在同一台物理机或虚拟机上运行多个容器,这些容器共享宿主机的操作系统内核,相比于虚拟机大大减少了额外的资源开销(如不需要为每个虚拟机单独运行一个完整的操作系统)。通过对容器的资源(如 CPU、内存等)进行合理的分配和限制,可以更高效地利用宿主机的资源,提高资源的整体利用率。
应用程序隔离问题:
- 在共享服务器环境中,如果多个应用程序直接运行在同一操作系统上,可能会相互干扰。例如,一个应用程序的错误或者资源占用可能会影响到其他应用程序的正常运行。
- 容器为每个应用程序提供了独立的运行环境,各个容器之间相互隔离。容器使用内核的命名空间(Namespace)和控制组(CGroup)等技术实现进程、网络、文件系统等资源的隔离。这使得即使一个容器出现问题,也不会影响到其他容器中的应用程序。
运维和部署等问题:
- 传统的应用程序部署过程往往比较复杂,需要在目标服务器上安装各种依赖项、配置环境等,这个过程容易出错且耗时。在运维方面,升级应用程序或对应用程序进行故障排查也比较困难。
- 容器化技术将应用程序及其依赖项打包成一个容器镜像,这个镜像可以在任何支持容器运行时的环境中快速部署。容器的启动和停止非常迅速,而且可以方便地进行版本控制和滚动更新。在运维方面,通过容器编排工具(如 Kubernetes)可以轻松地对容器进行管理、监控和故障排查。
容器化技术的原理:
- 内核命名空间(Namespace)
- 进程命名空间(PID Namespace)
- 进程命名空间用于隔离进程的标识符(PID)。在不同的进程命名空间中,进程可以有相同的 PID。例如,在容器内部的进程在容器自己的进程命名空间中可能被标识为 PID 1,但在宿主机的进程命名空间中,它有一个完全不同的 PID。这使得容器内的进程看起来像是在一个独立的系统中运行,与宿主机和其他容器中的进程隔离开来。
- 网络命名空间(NET Namespace)
- 网络命名空间为容器提供了独立的网络环境。每个容器可以有自己的网络接口、IP 地址、路由表等。例如,一个容器可以有自己的虚拟以太网接口(如 eth0),并被分配一个独立的 IP 地址,就像它是一个独立的网络设备一样。这使得容器可以构建自己的网络拓扑结构,与其他容器和外部网络进行通信时互不干扰。
- 文件系统命名空间(MNT Namespace)
- 文件系统命名空间实现了容器内文件系统的隔离。容器可以挂载自己的文件系统,这个文件系统可以是宿主机文件系统的一部分(通过挂载点),也可以是基于镜像构建的独立文件系统。容器内的进程只能看到和访问容器内部的文件系统,无法直接访问宿主机或其他容器的文件系统,除非进行特殊的配置。
- 其他命名空间(如 IPC Namespace、UTS Namespace 等)
- IPC 命名空间用于隔离进程间通信(IPC)机制,如消息队列、共享内存等。UTS 命名空间用于隔离主机名和域名等系统标识信息。这些命名空间共同作用,从多个方面为容器提供了类似独立系统的运行环境。
- 进程命名空间(PID Namespace)
- 控制组(CGroup)
- CGroup 用于对容器的资源(如 CPU、内存、磁盘 I/O、网络带宽等)进行限制和管理。
- CPU 资源管理
- 通过 CGroup,可以为容器分配特定比例的 CPU 时间。例如,可以设置一个容器最多只能使用宿主机 50% 的 CPU 资源。这有助于防止某个容器过度占用 CPU 资源,影响其他容器或宿主机的正常运行。
- 内存资源管理
- 对于内存资源,CGroup 可以限制容器的内存使用量。如果容器尝试使用超过其分配的内存量,可能会触发内存不足的处理机制(如进程被暂停或者被杀死)。这确保了容器在使用内存资源时不会对宿主机的其他进程或容器造成资源短缺的影响。
- 磁盘 I/O 和网络带宽管理
- 类似地,CGroup 也可以对容器的磁盘 I/O 和网络带宽进行限制。例如,限制容器的磁盘写入速度或者网络上传 / 下载速度,以实现资源的公平分配和整体性能的优化。
- 容器镜像(Container Image)
- 容器镜像是容器化技术的核心组成部分。容器镜像是一个只读的文件,它包含了运行一个容器所需的所有内容,包括应用程序代码、运行时环境、系统库、配置文件等。
- 容器镜像是分层构建的,每个层都包含了特定的文件或者修改。例如,基础层可能包含了操作系统的基本文件,上层可能包含了特定的应用程序依赖项或者应用程序本身。这种分层结构使得镜像的构建和分发更加高效。当拉取一个容器镜像时,只有与本地已有的层不同的层才会被下载,减少了下载的数据量。同时,镜像的分层结构也便于镜像的版本控制和管理。在容器启动时,容器运行时会根据镜像创建一个可写的容器层,容器内的进程可以在这个可写层上进行读写操作,而不会影响到镜像本身的只读层。
9.算法:对于一个回文串,计算其最长回文子串的长度(力扣第五题:https://leetcode.cn/problems/longest-palindromic-substring/description/)
算法思路:所有的状态在转移的时候的可能性都是唯一的。也就是说,我们可以从每一种边界情况开始「扩展」,也可以得出所有的状态对应的答案。
边界情况即为子串长度为 1 或 2 的情况。我们枚举每一种边界情况,并从对应的子串开始不断地向两边扩展。如果两边的字母相同,我们就可以继续扩展,例如从 P(i+1,j−1) 扩展到 P(i,j);如果两边的字母不同,我们就可以停止扩展,因为在这之后的子串都不能是回文串了。
可以发现,「边界情况」对应的子串实际上就是我们「扩展」出的回文串的「回文中心」。本质即为:我们枚举所有的「回文中心」并尝试「扩展」,直到无法扩展为止,此时的回文串长度即为此「回文中心」下的最长回文串长度。我们对所有的长度求出最大值,即可得到最终的答案。
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
if(n<2){
return s;
}
int start = 0,end = 0;
for(int i=0;i<n;i++){
//检查奇数字符串
int len1 = CheckString(s,i,i);
//检查偶数字符串
int len2 = CheckString(s,i,i+1);
int MaxString = Math.max(len1,len2);
if(MaxString > end - start){
start = i - (MaxString - 1) / 2;
end = i + MaxString / 2;
}
}
return s.substring(start,end+1);
}
private int CheckString(String s,int left,int right){
while(left>=0 && right<s.length() && s.charAt(left) == s.charAt(right)){
--left;
++right;
}
return right - left -1;
}
}