岗位:剪映Java后端开发 base深圳
博主当时项目还没做完,八股也看了不多,Boss上被字节HR打招呼要简历,仓忙之下只能把CQWM包装了一下,发给了HR,没想到直接被约面了。。。第一次面试直接被问穿了,但是有一说一内容就是围绕项目提问的,也没有说问很难的地方故意刁难,主要是没防备。
这次面试之后,我最大的感受就是:平时学习要多思考,不要想着临时突击八股文。
八股文当然重要,但真正能让知识记得牢、用得上,是在日常写代码、做题、项目实践中不断思考与理解。
比如我自己的例子,
例子一:MySQL 最左匹配原则
当时学 MySQL 的联合索引时,我就有个疑问:为什么查询条件如果不是从联合索引的最左边开始,就用不到索引,查询就会变慢?
后来我去查资料才明白,这其实和联合索引在 B+ 树中的存储结构 有关。
B+ 树是按照索引定义时的字段顺序依次排序的,如果跳过最左边字段,树结构就没法利用,导致无法走索引。
因为带着问题去查、去理解,这个知识点一下子就记住了。
例子二:HashSet 初始化容量
在刷题写代码的时候,我经常看到标准答案会写:
java
Set<Integer> set = new HashSet<>(capacity);
而不是简单的:
java
Set<Integer> set = new HashSet<>();
我就好奇,为什么一定要指定容量呢?
后来去翻了源码才知道:HashSet 底层基于 HashMap,如果元素数量超过阈值,就会触发扩容,扩容是一个比较消耗性能的操作。提前初始化合适的容量,就可以避免多次扩容带来的开销。
以下是面试官的提问:
- 你的项目里使用了RabbitMQ,说说什么是RabbitMQ,特点是什么
- 怎么理解保障消息的一致性
- String、StringBuffer、StringBuilder区别(提到了线程安全)
- 解释一下线程安全
- 先操作数据库再删缓存还是先删缓存再操作数据库
- 这种办法能杜绝数据不一致问题吗
- 解释一下AOP
- 介绍Redis的特点(Redis比较快)
- Redis为什么快
- 解释一下Redis的数据类型以及各个数据类型的功能
- Redis中list是怎么实现的
- Redis是单线程的吗,为什么
- 解释一下垃圾回收机制
- 笔试:三数之和
这个回答建议大家还是主要按照自己整理的去总结,因为博主这两天有事,写这篇博客临时AI的回答,后面我会整理详细的回答。
1. 什么是RabbitMQ,特点是什么
RabbitMQ是一个基于AMQP(高级消息队列协议)的开源消息中间件,用于在分布式系统中实现消息的存储、转发和路由。
特点:
- 支持多种消息模式(如点对点、发布订阅、主题匹配等)
- 灵活的路由机制,可通过交换机(Exchange)自定义消息路由规则
- 支持消息持久化、确认机制和重试机制,保证消息可靠性
- 轻量级、易部署,支持多语言客户端
- 可集群化部署,提高可用性和吞吐量
2. 怎么理解保障消息的一致性
消息一致性指分布式系统中,消息的发送、传递、处理过程与业务数据状态保持一致,避免出现"消息丢失""重复消费""数据不一致"等问题。
保障手段:
- 消息持久化:确保MQ服务重启后消息不丢失
- 生产者确认机制:确保消息成功投递到MQ
- 消费者确认机制(ACK):确保消息被正确处理后再删除
- 事务消息或本地消息表:解决"业务操作与消息发送"的原子性问题
3. String、StringBuffer、StringBuilder的区别
- String:不可变字符串,每次修改都会创建新对象,适用于少量、固定的字符串操作。
- StringBuffer:可变字符串,线程安全(方法加synchronized),适用于多线程环境下的字符串拼接。
- StringBuilder:可变字符串,线程不安全,但性能优于StringBuffer,适用于单线程环境下的高频字符串操作。
4. 解释一下线程安全
线程安全指多线程并发访问共享资源时,系统能保证操作结果的正确性和一致性,不会出现数据错乱、逻辑异常等问题。
实现方式:
- 加锁(synchronized、Lock)
- 使用线程安全的数据结构(如ConcurrentHashMap)
- 无状态设计或ThreadLocal隔离线程私有资源
5. 先操作数据库再删缓存还是先删缓存再操作数据库
推荐先操作数据库,再删缓存(Cache Aside Pattern):
- 流程:更新DB → 删除缓存 → 下次读取时从DB加载并更新缓存
- 原因:若先删缓存,可能出现"删缓存后、更新DB前"的间隙,其他线程读取到旧数据并写入缓存,导致不一致。
6. 这种办法能杜绝数据不一致问题吗
不能完全杜绝,极端情况仍可能出现不一致:
- 删缓存失败:更新DB后缓存未删除,导致读取旧值
- 并发读写:A更新DB时,B读取旧DB数据并写入缓存
解决思路:结合缓存过期时间、重试机制、分布式锁等手段降低概率。
7. 解释一下AOP
AOP(面向切面编程)是一种编程思想,通过分离"核心业务逻辑"和"横切关注点"(如日志、事务、权限),实现代码解耦和复用。
核心概念:
- 切面(Aspect):封装横切逻辑的类
- 切点(Pointcut):定义横切逻辑作用的位置(如某个方法)
- 通知(Advice):横切逻辑的具体实现(如前置通知、后置通知)
- 织入(Weaving):将切面代码嵌入到核心业务代码的过程
8. 介绍Redis的特点(Redis比较快)
Redis是高性能的键值对数据库,特点:
- 速度快:基于内存操作,单线程模型避免线程切换开销
- 支持多种数据类型:String、Hash、List、Set、Sorted Set等
- 持久化:支持RDB和AOF两种方式,保证数据不丢失
- 高可用:支持主从复制、哨兵模式、集群部署
- 功能丰富:支持缓存过期、事务、Lua脚本、发布订阅等
9. Redis为什么快
- 基于内存操作,避免磁盘IO瓶颈
- 单线程模型,减少线程切换和锁竞争开销
- 高效的数据结构(如跳表、压缩列表)
- IO多路复用模型,高效处理并发连接
- 底层用C语言实现,执行效率高
10. Redis的数据类型及功能
- String:存储字符串、数字,支持计数器、分布式锁
- Hash:存储键值对集合,适用于对象属性存储
- List:有序列表,支持两端插入/删除,适用于消息队列
- Set:无序去重集合,支持交集、并集运算,适用于标签、好友关系
- Sorted Set:有序去重集合(通过分数排序),适用于排行榜、延迟队列
- 其他:BitMap(位图)、HyperLogLog(基数统计)、Geospatial(地理位置)
11. Redis的List是怎么实现的
List底层基于双向链表 或压缩列表实现:
- 当元素数量少且值小时,用压缩列表(节省内存)
- 当元素数量或值超过阈值时,转为双向链表(支持高效插入/删除)
12. Redis是单线程的吗,为什么
Redis的核心网络IO和数据操作是单线程 的,但持久化、集群同步等辅助操作是多线程的。
采用单线程的原因:
- 内存操作速度快,单线程足够处理高并发
- 避免多线程的锁竞争和上下文切换开销
- 简化代码实现,降低复杂度
13. 解释一下垃圾回收机制
垃圾回收(GC)是Java等语言的自动内存管理机制,用于回收不再使用的对象内存,避免内存泄漏。
核心步骤:
- 标记:识别内存中"存活对象"(可达性分析,以GC Roots为起点)
- 清除:回收未被标记的对象内存
- 整理 :压缩存活对象,减少内存碎片
常见算法:标记-清除、标记-复制、标记-整理、分代收集(新生代用复制算法,老年代用标记-整理)
14. 笔试:三数之和
题目:给定一个包含n个整数的数组nums,判断是否存在三个元素a,b,c,使得a+b+c=0。找出所有不重复的三元组。
思路:排序 + 双指针(避免三重循环,降低时间复杂度)
步骤:
- 排序数组,便于去重和双指针操作
- 固定第一个数nums[i],用双指针left=i+1、right=n-1寻找nums[left]+nums[right] = -nums[i]
- 跳过重复元素,避免结果重复
代码实现:
java
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;
}
}
如果这篇文章对你有帮助,请点赞、评论、收藏,创作不易,你的支持是我创作的动力。