当前位置: 首页 > 面试经验 >

美团暑期后端实习面经解析: 拷打项目细节

优质
小牛编辑
78浏览
2024-03-21

美团暑期后端实习面经解析: 拷打项目细节

嗨~我是可拟雀,一个后端开发工程师,毕业于某985大学,目前供职于bat某大厂核心部门后端。每天分享最新面经答案,希望在大环境不好的当下能帮到你,让你多积累面试经验。需要内推或者面经合集请评论哦。

1.介绍一下redisson分布式锁?

答:Redisson是一个基于Redis实现的Java分布式对象存储和缓存框架,它提供了丰富的分布式数据结构和服务,如分布式锁、分布式队列、分布式Rate Limiter等。其中,Redisson的分布式锁是一种重要的功能,主要用于在分布式系统中实现互斥性,保证在任意时刻只有一个线程能够持有锁。

在Redisson中,使用key来作为是否上锁的标志。当通过getLock(String key)方法获得相应的锁之后,这个key即作为一个锁存储到Redis集群中。在接下来如果有其他的线程尝试获取名为key的锁时,便会向集群中进行查询。如果能够查到这个锁并发现相应的value的值不为0,则表示已经有其他线程申请了这个锁同时还没有释放,则当前线程进入阻塞;否则由当前线程获取这个锁并将value值加一。如果是可重入锁的话,则当前线程每获得一个自身线程的锁,就将value的值加一,而每释放一个锁则将value值减一,直到减至0,完全释放这个锁。

Redisson分布式锁具有一些重要的特性。首先,它支持锁的超时时间,这意味着如果一个线程在持锁期间挂掉了而没主动释放锁,那么通过超时时间可以保证该线程在超时后可以释放锁,这样其他线程才可以继续获取锁。其次,Redisson提供了可重入锁、公平锁等常用的分布式锁,还支持异步执行、锁的自动续期、锁的等待等特性。此外,由于Redis是缓存型数据库,拥有很高的性能,因此Redisson的加锁和释放锁开销较小,并且能够很轻易地实现分布式锁。

在使用上,Spring Boot项目可以方便地整合Redisson,通过添加Redisson依赖和配置RedissonClient客户端,就可以在项目中使用Redisson的分布式锁功能。

2.redission分布式锁底层如何实现

答:Redisson分布式锁底层实现主要依赖于Redis的特性和命令,结合Redisson的客户端库进行封装和优化。以下是Redisson分布式锁底层实现的大致原理:

Redis数据结构选择:

Redisson的分布式锁主要使用Redis的字符串(String)数据结构来存储锁的信息。每个锁在Redis中对应一个特定的key,该key的值用来表示锁的状态。

加锁机制:

当客户端尝试获取锁时,Redisson会向Redis发送一个SET命令,这个命令会尝试将锁对应的key设置为一个随机的UUID值,并设置过期时间(TTL)。同时,这个SET命令会带上NX(如果key不存在才设置)和PX(设置key的过期时间,单位为毫秒)参数。

如果SET命令执行成功,表示客户端成功获取到了锁。这个UUID值作为锁的持有者标识,用于后续的解锁操作。

如果SET命令执行失败(即key已经存在),说明锁已经被其他客户端持有,当前客户端需要等待或者返回失败。

锁的可重入性:

Redisson支持可重入锁,即同一个线程可以多次获取同一个锁而不会产生死锁。在Redisson中,当一个线程多次获取同一个锁时,它会在Redis中对应的key的value上增加重入次数,每次释放锁时,重入次数减一,直到重入次数为0时,才真正释放锁。

锁的自动续期:

为了避免客户端在持有锁期间因为网络问题或GC等原因导致与Redis服务器的连接断开,Redisson提供了锁的自动续期机制。当一个客户端获取到锁后,它会启动一个后台线程,定时向Redis发送命令延长锁的过期时间。

解锁机制:

当客户端需要释放锁时,它会向Redis发送一个Lua脚本,这个脚本会检查当前客户端是否持有锁(即比较UUID值),并删除对应的key,从而实现解锁。

如果客户端在持有锁期间崩溃,由于设置了锁的过期时间,锁最终会自动释放,避免死锁。

公平锁实现:

Redisson也支持公平锁,它通过Redis的有序集合(Sorted Set)数据结构来实现。当多个客户端请求锁时,它们会在有序集合中按照请求的顺序排队,Redis会根据集合中元素的顺序来决定哪个客户端能够获取到锁。

分布式环境下的安全性:

Redisson的分布式锁实现考虑了分布式环境下的安全性问题。例如,它使用UUID作为锁的持有者标识,确保每个锁只能被一个客户端持有;同时,通过Lua脚本原子性地执行加锁和解锁操作,避免在并发环境下出现竞态条件。

3.基于setnx实现的分布式锁的缺点有哪些?

答:基于SETNX实现的分布式锁虽然是一种常用的解决方案,但也存在一些明显的缺点。以下是这些缺点的主要方面:

单点风险:SETNX实现的分布式锁存在单点风险。如果存储分布式锁的Redis节点(或称为key)挂掉,就可能发生丢锁的情况。一旦丢锁,就可能导致多个客户端同时持有锁,从而使得分布式锁失效。

锁释放问题:SETNX本身只提供了加锁的机制,对于锁的释放通常依赖于额外的逻辑。例如,一个常见的做法是在业务逻辑完成后使用DEL命令删除锁。然而,如果持有锁的客户端在释放锁之前崩溃或网络中断,那么这个锁就可能永远不会被释放,造成死锁。此外,如果锁的释放逻辑没有正确实现,也可能导致锁的误释放或重复释放。

不可重入性:基于SETNX实现的分布式锁通常不具有可重入性。可重入锁允许同一个线程多次获取同一把锁,而不会造成死锁。然而,使用SETNX实现的锁通常不支持这种特性,可能导致同一线程在多次获取锁时发生冲突。

性能问题:在高并发场景下,基于SETNX实现的分布式锁可能会成为性能瓶颈。由于每次加锁和解锁都需要与Redis进行网络通信,这可能成为系统的瓶颈,尤其是在锁竞争激烈的情况下。

缺乏公平性:SETNX实现的分布式锁通常不提供公平性保证。公平性意味着等待时间最长的客户端应该优先获得锁。然而,基于SETNX的实现通常是基于先到先得的原则,可能导致某些客户端长时间无法获得锁。

4.基于redis如何实现可重入的分布式锁

答:基于Redis实现可重入的分布式锁,需要确保同一个线程或进程能够多次获取同一把锁而不会造成死锁,并且在释放锁时能够正确计数和解锁。以下是一个基于Redis实现可重入分布式锁的基本思路:

锁的数据结构:

使用Redis的哈希结构(Hash)来存储锁的信息。每个锁对应一个哈希表,哈希表的key是锁的标识(例如锁的名称),哈希表的value是一个包含多个字段的映射,其中至少包含以下字段:

lockCount:表示当前锁被持有的次数(可重入次数)。

ownerId:表示当前持有锁的客户端的唯一标识(例如线程ID或进程ID)。

expireTime:表示锁的过期时间,用于防止客户端崩溃导致的死锁。

加锁:

客户端尝试获取锁时,首先检查Redis中是否存在对应的哈希表。

如果不存在,则创建一个新的哈希表,并设置lockCount为1,ownerId为当前客户端的唯一标识,并设置一个合理的expireTime。

如果已经存在,则检查ownerId是否与当前客户端的唯一标识相同。如果相同,表示当前客户端已经持有该锁,将lockCount加1。

如果ownerId不同,表示锁被其他客户端持有,当前客户端需要等待或返回失败。

锁的重入:

当同一个客户端多次尝试获取同一把锁时,只需要检查ownerId是否匹配,并增加lockCount的值即可。

5.项目的qps大概多少

6.如何自己实现一个限流算法中的滑动窗口

答:滑动窗口限流算法的思路在于维护一个时间窗口,并跟踪这个窗口内的请求数量。当新的请求到达时,算***检查当前窗口内的请求数是否已经达到限制。如果没有达到限制,则允许该请求,并将其添加到窗口中;如果已经达到限制,则拒绝该请求。随着时间的推移,窗口会向前滑动,旧的请求会逐渐移出窗口,从而为新的请求腾出空间。

以下是实现滑动窗口限流算法的基本思路:

定义窗口大小和请求限制:首先,确定滑动窗口的大小(例如,每秒钟、每分钟或每小时)以及窗口内允许的最大请求数。

维护请求队列和时间戳:使用一个队列(如Python的deque)来存储窗口内的请求时间戳。当请求到达时,将其时间戳添加到队列中。

检查并移除过期请求:在每次判断请求是否允许之前,检查队列中的最早请求是否已过期(即其时间戳是否超出了窗口的边界)。如果是,则将其从队列中移除。

计算当前窗口内的请求数:队列中剩余的元素数量即代表了当前窗口内的请求数。

判断请求是否允许:如果当前窗口内的请求数未达到限制,允许新请求,并将其时间戳添加到队列中;如果已达到限制,则拒绝新请求。

处理窗口滑动:随着时间的推移,窗口会自动向前滑动,因为过期的请求会被移除。新的请求会填充到窗口的末尾。

8.code:用两个队列实现一个栈,java语言

9.jvm内存结构

答:堆(Heap):这是Java虚拟机中的一块内存区域,所有线程共享。它主要用于存储对象实例和数组。堆被划分为年轻代、老年代和永久代(在JDK8中取消了永久代)。年轻代又被划分为Eden区、Survivor区(含:S0和S1)。默认情况下,年轻代和老年代的比例为1:2,即年轻代占整个堆空间的1/3,老年代占整个堆空间的2/3。

方法区(Method Area):这也是线程共享的内存区域,主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

虚拟机栈(VM Stack):每个线程运行时所需要的内存称为虚拟机栈。每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存,用于存储局部变量表、操作数栈、常量池引用等信息。

程序计数器(Program Counter Register):用于记录下一条JVM指令的执行地址(如果正在执行的是本地方法则为空)。每一个线程都有一个程序计数器,当CPU因为时间片轮转等原因切换线程的时候,能保存当前线程的执行进度。同时,程序计数器不会存在内存溢出。

本地方法栈(Native Method Stack):这也是线程私有的,用于支持native方法的执行。

10.本地方法栈和虚拟机栈的区别

答:本地方法栈(Native Method Stacks)与虚拟机栈(VM Stack)在Java虚拟机中各自扮演着重要的角色,但它们之间存在一些显著的区别。

首先,两者的服务对象不同。虚拟机栈主要为Java方法(也就是字节码)服务,主管Java程序运行,保存方法的局部变量、部分结果,并参与方法的调用和返回。而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。本地方法是用其他语言(如C、C++)编写的方法,通过Java Native Interface(JNI)调用。因此,本地方法栈的存在主要是为了支持本地方法的调用。

其次,两者在结构上也有所不同。虚拟机栈是每个线程创建时都创建的,内部保存的单位是栈帧,对应一次次方法调用,其生命周期与线程一致。而本地方法栈与虚拟机栈一样,也是线程私有的,每个线程都有一个本地方法栈。这意味着每个线程都有自己独立的本地方法栈和虚拟机栈,它们之间进行独立的分配和释放。

最后,两者在异常处理上也有所区别。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时抛出异常。具体来说,当线程请求的栈容量超过固定大小时,会抛出StackOverflowError异常;而当没有足够的内存进行动态拓展时,会抛出OutOfMemoryError错误。

11.类加载的过程

答:类加载的过程在Java中是一个复杂且重要的机制,它确保了类的正确性和安全性。以下是类加载的详细过程:

加载(Loading):

加载阶段是类加载过程的起始点,主要由类加载器(ClassLoader)完成。

类加载器通过类的全限定名查找并读取定义此类的字节码文件(.class文件)。这些字节码文件通常来自文件系统、网络或其他来源。

读取到的字节码数据被加载到内存中,并且JVM会为此类创建一个Class对象,作为方法区中这个类的各种数据的访问入口。

链接(Linking):

链接阶段包括验证、准备和解析三个子阶段。

验证(Verification):对加载的类的字节码进行校验,以确保其符合Java虚拟机规范。这个校验过程包括结构验证、语义验证、字节码验证和符号引用验证等,旨在确保类的正确性、安全性和一致性。

准备(Preparation):为类的静态变量分配内存空间,并设置初始值。这些静态变量是类级别的变量,而非实例级别的变量。

解析(Resolution):将常量池内的符号引用转换为直接引用的过程。这涉及到对其他类或接口的符号引用的解析,以确保它们可以被正确地引用和使用。

初始化(Initialization):

初始化阶段是类加载过程的最后阶段,在这个阶段中才真正开始执行类中定义的Java代码。

如果类中有静态初始化块或静态变量赋值操作,那么这些操作将在初始化阶段被执行。

JVM会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,确保类的初始化只会被执行一次。

需要注意的是,类加载的过程并不是在程序一启动时就立即完成的,而是遵循“什么时候初始化”的原则进行加载。只有在首次主动使用某个类的时候(如创建类的实例、访问类的静态变量或静态方法、反射调用等),该类才会被初始化。

12.redis常见数据结构(常见

13.zset的底层数据结构(散列+跳表

14.redis缓存穿透的原因和解决办法

答:原因

缓存穿透是指查询一个不存在的数据,由于缓存中也没有该数据,因此每次查询都会穿透到数据库层,从而给数据库带来压力。这种情况通常发生在恶意攻击或者系统bug时,大量查询不存在的数据,导致数据库负载过高。

解决办法:

布隆过滤器:

布隆过滤器是一种空间效率极高的概率型数据结构,它利用多个哈希函数将元素映射到位图中。

当查询一个元素时,可以通过检查对应的位图位置来判断元素是否存在。

虽然布隆过滤器存在误判的可能性,即可能将不存在的元素误判为存在,但可以有效地过滤掉大部分不存在的元素,从而降低缓存穿透的概率。

缓存空对象:

对于不存在的数据,可以在缓存中存储一个空对象或者特殊标记,并设置较短的过期时间。

这样,当再次查询这个不存在的数据时,可以直接从缓存中获取空对象或特殊标记,从而避免了对数据库的查询。

但需要注意的是,这种方法可能会增加缓存的存储成本,并且如果大量查询不存在的数据,仍然可能给缓存带来压力。

数据校验与过滤:

在应用层增加数据校验逻辑,对于非法或不符合规范的请求进行过滤。

这可以减少恶意请求对缓存和数据库的冲击。

限流与熔断:

通过限流策略限制对数据库的查询频率,当达到一定的阈值时,可以暂时熔断对数据库的查询,从而保护数据库免受过大的压力。

这可以确保在高并发场景下,数据库不会因为过多的无效查询而崩溃。

15.mysql索引底层数据结构 (b+树

16.mysql对两个不同字段建立索引有几棵b+树

答:在MySQL中,对于每一个独立的索引,都会有一棵B+树与之对应。所以,如果你对两个不同的字段分别建立了索引,那么你会有两棵B+树。每棵B+树都是根据该索引字段的值进行排序的。

例如,假设你有一个表my_table,并且你对字段field1和field2分别建立了索引,那么:

对于field1的索引,MySQL会维护一棵B+树,其中的节点按照field1的值进行排序。

对于field2的索引,MySQL会维护另一棵B+树,其中的节点按照field2的值进行排序。

这两棵B+树是独立的,互不干扰。当你根据field1进行查询时,MySQL会使用第一棵B+树;当你根据field2进行查询时,MySQL会使用第二棵B+树。

17.mysql联合索引的最左前缀原理?

答:MySQL的联合索引(也称为复合索引或多列索引)的最左前缀原理是指,当MySQL使用联合索引进行查询时,它会从联合索引的最左列开始匹配查询条件。只有当查询条件使用了联合索引的最左侧列时,索引才会被有效利用。

具体来说,假设我们有一个联合索引 (a, b, c),这个索引是按照列 a、b 和 c 的顺序进行排序的。现在,我们考虑以下几种查询情况:

单独查询列a:在这种情况下,查询条件使用了联合索引的最左列(列a),因此MySQL可以直接使用这个索引来快速定位到匹配的行。

查询列a和列b:查询条件同时包含了列a和列b,这也满足了最左前缀原则,因为列a是联合索引的最左列。MySQL会利用这个联合索引来加速查询。

查询列a、列b和列c:当查询条件包含了联合索引的所有列(列a、列b和列c)时,索引的使用效率是最高的,因为所有列都被用来定位数据。

然而,以下情况则不会有效地利用联合索引 (a, b, c):

只查询列b或列c:由于列b和列c不是联合索引的最左列,因此当查询条件只包含这些列时,联合索引不会被使用。MySQL会进行全表扫描或者尝试使用其他可能的索引(如果有的话)。

最左前缀原则也适用于索引的列顺序。如果你有一个 (b, a, c) 的联合索引,那么它对于基于列 b 的查询是高效的,但对于基于列 a 的查询则不是,因为列 a 不是这个联合索引的最左列。

18.col1=, col2 between可以用这个联合索引吗

19.为什么不等于条件会导致索引失效

20.你实习想做什么

反问:部分与业务

日常吗

项目实习,类似日常

两个队列咋实现一个栈哇 给我整不会了

lc原题,有空做一下

q1,q2,push x就往q1加x,然后把q2按顺序全部接到q1后面,然把q1,q2的指针交换, pop就从q2 poll一个

原文传送门

 类似资料: