字节一面 面经(补充版)

发布于:2025-09-12 ⋅ 阅读:(22) ⋅ 点赞:(0)
  • 什么是RabbitMQ,特点是什么
  • 怎么理解保障消息的一致性
  • String、StringBuffer、StringBuilder
  • 解释一下线程安全
  • 先操作数据库再删缓存还是先删缓存再操作数据库
  • 这种办法能杜绝数据不一致问题吗
  • 解释一下AOP
  • 介绍Redis的特点(Redis比较快)
  • Redis为什么快
  • 解释一下Redis的数据类型以及各个数据类型的功能
  • list是怎么实现的
  • Redis是单线程的吗,为什么
  • 解释一下垃圾回收机制
  • 笔试:三数之和

如果问到项目中使用的技术,我们的回答应该包括这几个层面

  • 这个技术是什么?
  • 解决了什么问题?
  • 为什么不用其他的?
  • 此技术对当前项目当前架构的适配性

涉及到项目内容的回答请结合自己项目作答,这里不举例,感谢理解。

1. 什么是RabbitMQ,特点是什么

RabbitMQ 是一个 基于 AMQP(高级消息队列协议)实现的开源消息中间件,它的核心功能是消息的可靠投递和异步解耦。

  • 它支持生产者(Producer)发送消息、消息存储与路由、消费者(Consumer)订阅与消费。
  • 提供 可靠性(持久化、确认机制)灵活的路由策略(Exchange + Binding)消息堆积削峰能力

特点:(结合项目中的使用引导面试官)

  • 可靠性: RabbitMQ 使用一些机制来保证可靠性, 如持久化、传输确认及发布确认等。
  • 灵活的路由 : 在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能, RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起, 也可以通过插件机制来实现自己的交换器。
  • 扩展性: 多个 RabbitMQ 节点可以组成一个集群,也可以根据实际业务情况动态地扩展 集群中节点。
  • 高可用性 : 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队 列仍然可用。
  • 多种协议: RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP, MQTT 等多种消息 中间件协议。
  • 多语言客户端 :RabbitMQ 几乎支持所有常用语言,比如 Java、 Python、 Ruby、 PHP、 C#、 JavaScript 等。
  • 管理界面 : RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集 群中的节点等。
  • 插件机制 : RabbitMQ 提供了许多插件 , 以实现从多方面进行扩展,当然也可以编写自己的插件。

2. 怎么理解保障消息的一致性

在 RabbitMQ 中,保障消息一致性主要是指 保证消息在生产者、队列和消费者之间不会丢失或重复。

答案可以从以下几个方面组织:

  1. 消息可靠投递:RabbitMQ 提供多种机制确保消息可靠送达:
  • Publisher Confirm(生产者确认):生产者发送消息后,可以等待 RabbitMQ 返回确认(ack),确保消息已经成功入队。
  • Mandatory 标记 + Return 回调:当消息无法路由到队列时,生产者能收到通知,避免消息丢失。
  1. 队列持久化:
  • 队列和消息都可以设置持久化,即使 RabbitMQ 宕机重启,消息仍然存在磁盘上。
  • 搭配 消息确认机制(ack),消费者处理完消息才通知 RabbitMQ 删除,这样避免消息处理失败导致丢失。
  1. 事务机制:RabbitMQ 支持事务模式( txSelect、txCommit、txRollback ,可以保证一批消息要么全部成功,要么全部失败,但性能开销非常大。一般我们不会使用。

3. String、StringBuffer、StringBuilder的区别

  • 可变性:String是不可变的(Immutable),创建之后内容修改,每次修改都会创建一个新对象。StringBufferStringBuilder是可变的(mutable),允许修改储存的内容而不会创建新的对象。
  • 线程安全性:String因为不可变,所以天然线程安全。StringBuilder是不是线程安全的,适用于单线程环境。StringBuffer是线程安全的,其方法通过synchronized关键字实现同步,适用于多线程环境。
  • 性能:String的性能最低,尤其是涉及大量字符串的修改时会产生大量临时对象,增加内存开销和垃圾回收压力。StringBuffer性能最高,因为没有线程安全的开销,适合单线程下的字符串操作。StringBuilder性能略低于StringBuffer,因为他的线程安全机制引入了部分开销。
  • 使用场景:如果字符串内容固定或不常变化,使用String。如果需要频繁修改并且在单线程下,使用StringBuilder。如果在频繁修改并且在多线程环境下,使用StringBuffer

对比总结如下:

特性 String StringBuilder StringBuffer
不可变性 不可变 可变 可变
线程安全 安全 不安全 安全
性能 低(频繁修改时) 高(单线程) 中(多线程安全)
使用场景 静态字符串 单线程动态字符串 多线程动态字符串

4. 解释一下线程安全

线程安全是指在多线程环境下,程序的执行结果是正确的、可预期的,不会因为线程的并发执行而出现数据错误或状态不一致。
线程不安全往往出现在多个线程同时读写共享变量时,可能导致竞态条件。
常见的解决方式有:

  • synchronized关键字: 可以使用synchronized关键字来同步代码块或方法,确保同一时刻只有一个线程可以访问这些代码。对象锁是通过synchronized关键字锁定对象的监视器(monitor)来实现的。
  • volatile关键字:volatile关键字用于变量,确保所有线程看到的是该变量的最新值,而不是可能存储在本地寄存器中的副本。
  • Lock接口和ReentrantLock类java.util.concurrent.locks.Lock接口提供了比synchronized更强大的锁定机制,ReentrantLock是一个实现该接口的例子,提供了更灵活的锁管理和更高的性能。
  • 原子类:Java并发库(java.util.concurrent.atomic)提供了原子类,如AtomicIntegerAtomicLong等,这些类提供了原子操作,可以用于更新基本类型的变量而无需额外的同步。
  • 线程局部变量ThreadLocal类可以为每个线程提供独立的变量副本,这样每个线程都拥有自己的变量,消除了竞争条件。
  • 并发集合:使用java.util.concurrent包中的线程安全集合,如ConcurrentHashMapConcurrentLinkedQueue等,这些集合内部已经实现了线程安全的逻辑。
  • JUC 工具类: 使用<font style="background-color:rgba(27, 31, 35, 0.05);">java.util.concurrent</font>包中的一些工具类可以用于控制线程间的同步和协作。例如:<font style="background-color:rgba(27, 31, 35, 0.05);">Semaphore</font><font style="background-color:rgba(27, 31, 35, 0.05);">CyclicBarrier</font>等。

5. 先操作数据库再删缓存还是先删缓存再操作数据库

结论是,推荐先操作数据库,再删缓存(Cache Aside Pattern):

如果先删缓存再操作数据库:

当前线程一旦删掉缓存,其他线程查缓存未命中,去查数据库把旧数据写到缓存中。随后当前线程再更新数据库,这就导致数据不一致了。而且这种情况是非常容易出现的。

如果先操作数据库再删缓存:

在已经保证数据库中是最新数据的情况下,这种情况下即使出现了缓存不一致,下次缓存失效或者淘汰时再查数据库更新缓存,依旧可以保证数据的一致性。

话虽然这么说,但是也不代表这种方法就能杜绝数据不一致问题,只是概率要小于前者。

6. 这种办法能杜绝数据不一致问题吗

不能完全杜绝。

比如,线程一查缓存未命中,去查数据库准备把数据写入缓存。突然,线程二更新了数据库,并删除了缓存。恰好这时,线程一把数据库旧值写入了缓存,这就导致数据不一致了。

所以在极端情况下,数据不一致问题还是会出现的,还是得看业务场景对不一致数据的容忍程度。一般情况下,推荐使用这种操作,并根据业务场景进一步优化。

7. 解释一下AOP

Spring AOP是Spring框架中的一个重要模块,用于实现面向切面编程。

在 AOP 中最小的单元是“切面”。一个“切面”可以包含很多种类型和对象,对它们进行模块化管理,例如事务管理。

在面向切面编程的思想里面,把功能分为两种

  • 核心业务:登陆、注册、增、删、改、查、都叫核心业务
  • 周边功能:日志、事务管理这些次要的为周边业务

在面向切面编程中,核心业务功能和周边功能是分别独立进行开发,两者不是耦合的,然后把切面功能和核心业务功能 “编织” 在一起,这就叫AOP。

AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码降低模块间的耦合度,并有利于未来的可拓展性和可维护性

在 AOP 中有以下几个概念:

  • AspectJ:切面,只是一个概念,没有具体的接口或类与之对应,是 Join point,Advice 和 Pointcut 的一个统称。
  • Join point:连接点,指程序执行过程中的一个点,例如方法调用、异常处理等。在 Spring AOP 中,仅支持方法级别的连接点。
  • Advice:通知,即我们定义的一个切面中的横切逻辑,有“around”,“before”和“after”三种类型。在很多的 AOP 实现框架中,Advice 通常作为一个拦截器,也可以包含许多个拦截器作为一条链路围绕着 Join point 进行处理。
  • Pointcut:切点,用于匹配连接点,一个 AspectJ 中包含哪些 Join point 需要由 Pointcut 进行筛选。
  • Introduction:引介,让一个切面可以声明被通知的对象实现任何他们没有真正实现的额外的接口。例如可以让一个代理对象代理两个目标类。
  • Weaving:织入,在有了连接点、切点、通知以及切面,如何将它们应用到程序中呢?没错,就是织入,在切点的引导下,将通知逻辑插入到目标方法上,使得我们的通知逻辑在方法调用时得以执行。
  • AOP proxy:AOP 代理,指在 AOP 实现框架中实现切面协议的对象。在 Spring AOP 中有两种代理,分别是 JDK 动态代理和 CGLIB 动态代理。
  • Target object:目标对象,就是被代理的对象。

当然,我们面试过程中肯定不能就这么说,这么说就扯远了。面试官可不是想来听碎碎念的,我们在适当解释之后就要引到自己项目中的实现了

8. 介绍Redis的特点(Redis比较快)、为什么快?

这里就是回答的时候,一定要按照自己项目中实际使用的说,甚至很多常用的功能都可以不说,想方设法引导面试官往下问,给他做局。

  • 完全基于内存操作,读写速度非常快。
  • 使用单线程,避免了线程切换和锁竞争的开销(全都是单线程吗)。
  • 基于非阻塞的 IO 多路复用机制(下一个提问点)
  • C 语言实现,优化过的数据结构,基于几种基础的数据结构,redis 做了大量优化,性能极高

我们可以看到,这里几乎全是提问点,面试时我们应该怎么做呢?更熟悉哪里,就在哪部分直接展开说,也不去列其他的点了,除非他问我们还有吗?当然这种方法有点小聪明了,我们还是最好把知识都掌握住,除非实在记不清别的了。

10. Redis的数据类型及功能

  • String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
  • List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
  • Hash 类型:缓存对象、购物车等。
  • Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
  • Zset 类型:排序场景,比如排行榜、电话和姓名排序等。

Redis 后续版本又支持四种数据类型,它们的应用场景如下:

  • BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
  • HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
  • GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
  • Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。

难道要一个一个介绍?当然不是,还是那句话,引导面试官向自己项目中涉及的数据结构以及实现方式。肯定不是考察面试者背诵能力,而是实践中使用的功底。

11. Redis的List是怎么实现的

List 类型的底层数据结构是由双向链表压缩列表实现的:

  • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
  • 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;
  • 但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。

12. Redis是单线程的吗,为什么

Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。

但是,Redis 程序不是单线程的,Redis 在启动时会启动后台线程(BIO)。

用来处理关闭文件、AOF 刷盘、释放内存这些任务。

Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。

回答这个问题又涉及到了文件、AOF、内存,这绝对还有下文。再问 Redis 持久化…内存淘汰。。

可以看到这些内容都是串在一起的,有些东西说出来就必被问,这下说啥也逃不掉了,所以还是得多提升自己!

13. 解释一下垃圾回收机制

垃圾回收(Garbage Collection, GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。垃圾回收可以通过多种方式触发,具体如下:

  • 内存不足时:当JVM检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收。
  • 手动请求:虽然垃圾回收是自动的,开发者可以通过调用 <font style="background-color:rgba(27, 31, 35, 0.05);">System.gc()</font><font style="background-color:rgba(27, 31, 35, 0.05);">Runtime.getRuntime().gc()</font> 建议 JVM 进行垃圾回收。不过这只是一个建议,并不能保证立即执行。
  • JVM参数:启动 Java 应用时可以通过 JVM 参数来调整垃圾回收的行为,比如:<font style="background-color:rgba(27, 31, 35, 0.05);">-Xmx</font>(最大堆大小)、<font style="background-color:rgba(27, 31, 35, 0.05);">-Xms</font>(初始堆大小)等。
  • 对象数量或内存使用达到阈值:垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发垃圾回收。

14. 笔试:三数之和

题目:给定一个包含n个整数的数组nums,判断是否存在三个元素a,b,c,使得a+b+c=0。找出所有不重复的三元组。

思路:排序 + 双指针(避免三重循环,降低时间复杂度)
步骤:

  1. 排序数组,便于去重和双指针操作
  2. 固定第一个数nums[i],用双指针left=i+1、right=n-1寻找nums[left]+nums[right] = -nums[i]
  3. 跳过重复元素,避免结果重复

代码实现:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ThreeSum {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        if (nums == null || nums.length < 3) return result;
        
        Arrays.sort(nums); // 排序
        
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] > 0) break; // 第一个数大于0,三数之和必大于0
            if (i > 0 && nums[i] == nums[i-1]) continue; // 去重
            
            int left = i + 1;
            int right = nums.length - 1;
            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];
                if (sum == 0) {
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    // 去重
                    while (left < right && nums[left] == nums[left+1]) left++;
                    while (left < right && nums[right] == nums[right-1]) right--;
                    left++;
                    right--;
                } else if (sum < 0) {
                    left++; // 和太小,左指针右移
                } else {
                    right--; // 和太大,右指针左移
                }
            }
        }
        return result;
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到