八股题
Java 基础 + 集合
面向对象和面向过程的区别?
面向对象:将数据和方法封装成对象,作为程序的基本单元来组织代码,包含封装、继承、多态三大特性,方便代码复用和灵活性。
面向过程:以过程做为基本单元来组织代码,过程对应到代码中就是函数,将函数和数据分离,比较关注步骤和流程。其实就是一条路走到底的思想,关注如何设计一系列顺序执行的过程实现。
封装、继承、多态?
封装(Encapsulation):通过将对象的属性和方法结合为独立单元,并利用访问修饰符(如private
)隐藏内部细节,仅通过公共接口(如getter/setter
)控制访问,从而提升安全性和可维护性
继承(Inheritance):允许子类基于父类的属性和方法进行扩展,实现代码复用,Java采用单继承机制(仅支持一个直接父类),但可通过接口实现多重继承的效果
多态(Polymorphism):同一方法调用因对象实际类型不同而产生不同行为,通常通过父类引用指向子类对象及方法重写实现,依赖运行时动态绑定机制决定具体执行逻辑。(重写和重载)
常见排序算法?时间复杂度?
-
直接插入排序:o(n^2)
-
冒泡排序:o(n^2)
-
快速排序: o(nlogn)
-
堆排序:o(nlogn)
-
归并排序:o(nlogn)
算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
---|---|---|---|---|---|
插入排序 | O(n²) | O(n²) | O(1) | 稳定 | 部分有序数据 |
冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 | 教学示例、小数据 |
快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 | 大规模随机数据 |
归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 | 大数据、外部排序 |
堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 | 实时系统、内存受限场景 |
ArrayList 和 LinkedList 的区别?
- ArrayList:底层是动态数组,有扩容机制,内存连续,查询快,增删慢。
- LinkedList:底层是双向链表,内存不连续,查询慢,增删快。
【注意】 实际过程中,ArrayList
的增删操作比 LinkedList
快了进百倍。
随着集合容量的增加,
LinkedList
add累计耗时直线上升,ArrayList
add累计耗时上升很慢;LinkedList
慢的原因应该是每次add一个值都需要封装成Node然后追加到链表尾部,每次封装Node(实例化Node对象)的耗时不容小觑;ArrayList add 不需要封装,影响耗时的只有扩容,而每次扩容都是调用底层System.arraycopy
数组拷贝,这个拷贝函数内存复制执行很快,而且每次扩容int newCapacity = oldCapacity + (oldCapacity >> 1);
为原容量的1.5倍,所以随着容量增加其实扩容不了几次,这也是ArrayList add速度快的原因。
Java的基本数据类型?
String创建对象时如何创建,创建几个对象,用new和不用的区别?
String创建对象的时候,可能会创建1个或者2个对象。
如果不使用 new
- 第一次需要创建字符串字面量存入字符串常量池中,
String s1 = "SwimmingLiu"
,引用该字符串字面量 - 第二次如果需要相同的字符串,
String s2 = "SwimmingLiu"
,引用该字符串字面量且不会创建任何对象
如果使用new
:每次创建字符串对象都会在堆内存中存放一个新的对象
- 如果字符串字面量不在字符串常量池中,会创建两个对象(堆内存对象 + 字符串常量池中的对象),
String s3 = new String("SwimmingLiu")
- 如果字符串字面量已经在字符串常量池中,只会创建一个对象(堆内存对象),
String s4 = new String("SwimmingLiu")
字符串操作有哪些类?有什么区别?
String
、StringBuilder
、StringBuffer
- 线程安全:
String
、StringBuffer
- String:
String
是不可变量,底层用final
修饰,每次对String
修改都会产生新的副本,从而占用更多的资源,频繁大量的修改会造成资源的浪费 - StringBuffer:
StringBuffer
是为了解决String
可能造成资源浪费的问题,底层用char[]
数组,所有修改方法采用synchronized
锁, 所以线程安全
- String:
- 线程不安全:
StringBuilder
- StringBuilder:
StringBuilder
在StringBuffer
的基础上把synchronized
锁去掉了,舍弃了线程安全单性能更高
- StringBuilder:
如何通过Stream流进行过滤、集合和映射的操作?
JUC 并发
Java的锁有哪些?
- 内置锁(synchronized):Java语言层面提供的关键字,隐式加锁,使用简单。
- 显示锁(Lock接口及其实现):比如 ReentrantLock、ReentrantReadWriteLock(读写锁)、StampedLock 等,提供更灵活的锁操作,如可中断、公平性等。
【不同锁的区别】
-
synchronized:内置锁(Monitor Lock),可以用于方法或代码块,提供互斥访问。当一个线程进入 synchronized 方法或块时,它会自动获取对象的锁,其他线程则需等待锁释放后才能进入。
synchronized
是一种非公平,悲观,独享,互斥,可重入的重量级锁。 -
ReentrantLock:是一个重入锁,是
java.util.concurrent.locks
包中的接口 Lock 的实现,提供了比synchronized
更灵活的锁操作,如尝试获取锁、可中断的获取锁、超时获取锁等。它也支持公平锁和非公平锁策略。ReentrantLock
是一种默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。 -
ReentrantReadWriteLock(读写锁):也是 java.util.concurrent.locks 包中的一部分,允许同时有多个读取者,但只允许一个写入者。它分为读锁和写锁,读锁之间不互斥,读锁与写锁互斥,写锁之间也互斥,适用于读多写少的场景。
ReentrantReadWriteLock
是一种 默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。 -
StampedLock(Java 8 引入):提供了三种锁模式:读锁、写锁和乐观读锁。相较于
ReentrantReadWriteLock
,StampedLock
提供了更细粒度的控制,支持乐观读取操作,可以提高并发性能。
可以给我简述一下你都有什么并发经验吗 (JUC当中用到了哪些功能)?平时项目中有哪些地方用到了锁?项目中哪些部分考虑了线程安全?怎么用的?
面试官您好,我平时在项目中大部分用到的都是分布式锁,主要是为了避免项目服务宕机和锁带来的内存压力。不过,我之前在学习的过程中,是整个方法区/代码块这种粒度比较大的地方,涉及到多个线程功能访问操作。我可能会选用 sychronized
。如果需要更细粒度的显示控制,或者需要让所有线程按顺序执行,我会采用ReentantLock
。
Synchronized可以用在哪里?Synchronized锁升级的机制?
sychronized
一般用于需要存在线程安全问题的代码块/方法区,上锁需要一个锁对象。
sychronized
锁升级机制主要是三种状态,从偏向锁(一个线程) -> 轻量级锁 (多个线程) -> 重量级锁 (多个线程竞争激烈)
- 偏向锁:最开始有一个线程第一次获取锁的时候,
JVM
会记录修改锁对象的对象头,标记为偏向状态。对象头里面会记录线程id
和 对应的epoch
偏向锁版本。后续该线程再获取这个锁,基本没啥开销。 - 轻量级锁:当有另外的线程尝试去获取已经被偏向的锁时,锁会升级为轻量级锁。上锁的过程中,
JVM
会在当前线程的栈帧中,创建一个锁记录LockRecord
,当锁记录指向锁对象。然后用CAS
替换锁对象的标记字Mark Word
, 并将Mark Word
的值存入锁记录。如果替换成功,锁对象的Mark Word
就变成当前线程的所锁记录。使用CAS
操作的目的是减少锁竞争的开销。 - 重量级锁:当
CAS
失败无法获取锁的时候,JVM判定其为多个线程竞争锁激烈,锁会升级成为重量锁。会使用操作系统的互斥量Mutex
来实现线程的阻塞和唤醒。如果获取锁成功,线程会被放入Monitor的owner
当中
Java内存模型(JMM), jdk8中有什么变化?
JMM
是用来解决由硬件速度差异引起的并发编程问题。CPU、内存、I/O设备之间的速度差异回影响程序性能。为了提高效率,采用了缓存、多任务处理和指令重排序等技术,但是这也导致了并发程序当中的可见性、有序性、原子性问题。JMM
就定义了一系列关键字 volatile
、synchronized
、final
确保程序能正确执行,还定义了Happens-Before
规则来明确操作之间的顺序关系。
【JDK8 变化】
JDK 8
通过元空间替代永久代、锁机制优化及内存屏障指令增强,JDK 8通过元空间替代永久代、锁机制优化及内存屏障指令增强
JVM 虚拟机
JVM的垃圾回收机制和JVM性能调优了解?
垃圾回收一般都是发生在堆内存里面,所以下面所有垃圾回收操作的对象基本都是在堆内存里面
【JVM垃圾回收机制】
JVM垃圾回收机制有三种,标记-清除 (CMS) 、**标记-整理 (G1) **、标记-复制
- 标记-清除:主要分为两个阶段,标记 和 清除
- 标记:从
GC Roots
开始,通过DFS
或者BFS
遍历所有被引用的对象,并且在对象的头部Header
标记为存活 (标记的都是不需要回收的对象-存活对象)。GC Roots
的对象包括JVM
中引用的对象(如局部变量、方法参数)、方法区中类静态属性引用的对象(全局变量)、方法区中常量引用的对象、本地方法栈中JNI
引用的对象 - 清除:遍历堆中的对象,将所有没有被标记的对象进行垃圾回收。垃圾回收的过程,不会移动和整理内存空间。一般都是通过空闲链表(双向链表)来标记被垃圾回收的区域,内存是空闲可用的。所以这种算法会导致内存空间碎片的产生
- 标记:从
- 标记-复制:主要分为两个阶段,标记 和 复制,标记部分和上面一样
- 复制:该算法会把堆分成两块 (
From
区和To
区),所有对象创建的时候都在From
区里面(标记的对象也都在From
区)。发生GC
垃圾回收的时候,会将标记的对象(存活对象)从From
区 复制到To
区,然后整体回收From
区。然后再从To
区中,将存活对象复制回From
区
- 复制:该算法会把堆分成两块 (
- 标记-整理:主要分为两个阶段,标记 和 整理,标记部分和上面一样
- 整理:将被标记的对象(存活对象)往边界上整理,对其他的部分进行垃圾回收。它的优点是不会出现内存碎片,也不需要像复制算法那样腾出一半的空间,所以内存利用率也挺高的。它的缺点是需要对堆内存进行多次搜索,因为需要在同一个空间里面,完成标记和整理(移动)的操作。
【JVM性能调优】
GC
垃圾回收器调优的核心原理就是尽可能在年轻代 Young GC
回收对象 (年轻代包含 Eden
区 和 两个 Survivor
区)。
具体实现步骤如下:
- 选择合适的GC:
CMS
(实时Web服务、电商秒杀等对响应时间敏感的场景)、G1
(平衡吞吐与延迟,如微服务集群、分布式缓存) - 调整堆和新生代大小:内存设置合理可以减少
GC
频率,通过设置-Xms
和-Xmx
调整堆内存初始/最大值,结合-Xmn
或-XX:NewRatio
控制新生代占比,并通过-XX:SurvivorRatio
调节 Eden 与 Survivor 区比例,根据应用对象生命周期和 GC 监控动态优化。 - 启用GC日志检测:监控和分析GC的行为,找出性能瓶颈
- 调整GC线程:提高并行GC性能
【CMS 和 G1对比】
维度 | CMS | G1 |
---|---|---|
算法 | 标记-清除(内存碎片) | 标记-整理(减少碎片) |
停顿时间 | 短(但可能因碎片触发Full GC) | 可预测(默认200ms,可调) |
堆内存范围 | 中小堆(<32GB) | 大堆(4GB~32GB+) |
CPU占用 | 高(并发阶段占用25% CPU) | 较低(并发线程自适应) |
JVM 运行时内存情况,每个地方存储的是什么?(JVM内存区域如何划分?)
堆内存、方法区(元空间)、直接内存、虚拟机栈、本地方法栈、程序计数器
线程的共享区域以及非共享?
- 共享区域:堆内存、直接内存、方法区(元空间)
- 私有区域:虚拟机栈、本地方法栈、程序计数器
MySQL
一条SQL语句的执行过程?
- 检查连接: 校验账号密码,确定用户的连接权限
- 缓存查询:如果存在缓存,直接返回查询结果。(MySQL 8.0之后废弃)
- 语法分析:通过语法树分析SQL语句的语法是否正确
- 查询条件优化:优化
where
语句中的查询条件,比如联合索引拼接等等 - 执行SQL语句:执行器执行SQL语句,并返回执行结果
说一下MySQL索引?有什么优点?
划分方向 | 索引类型 |
---|---|
数据结构 | B+树索引、Hash索引、倒排索引 (全文索引)、R-树索引 (多维空间树)、位图索引(Bitmap) |
物理存储 | 聚簇索引、非聚簇索引 |
字段特性 | 主键索引、唯一索引、普通索引(二级索引、辅助索引)、前缀索引 |
字段个数 | 单列索引、联合索引 |
优点:索引可以加速SQL语句条件查询的检索过程,快速定位到关键行
为什么我们不每一列数据都创建一个索引?索引过多的缺点是什么?
索引不是越多越好,如果创建的索引过多,因为每次修改都需要维护索引数据,会消耗资源和导致查询时间增长。
**【时间开销】**进行增删改操作的时候,索引也必须更新。索引越多,需要修改的地方就越多,时间开销大。B+树可能会出现页分裂、合并等操作,时间开销更大。
【空间开销】 建立二级索引,都需要新建一个B+树,每个数据页面都是16KB。如果数据大,索引又多,占用的空间不小。
什么情况会出先索引失效?简单说三种让索引失效的情况?
- 联合索引不满足最左前缀匹配原则
- 联合索引的首列使用
>
或者<
(单列索引不会失效) - 对索引列使用运算
where id + 8 = 16
、函数count()、distinct()
、like '%xx%'
等操作 - 对索引列使用不同的数据类型进行条件筛选 (强制转换 -> 函数)
- 对索引列和非索引列使用
or
操作 (where name = "swimmingliu" or age = 34
)
怎么判断查询是走索引还是走全表 ?
使用 EXPLAIN
对指定的SQL语句进行分析,EXPLAIN
分析结果 的 type
表示查询的访问类型,影响查询的效率。常见的值:
- ref: 使用索引,查找匹配某个单一列的值(比如通过外键查找)。比
range
更高效。 - range: 使用索引扫描某个范围内的值,适用于
BETWEEN
、> <
等条件。 - index: 全索引扫描,扫描整个索引结构,不读表数据,通常效率比全表扫描好。
- all: 全表扫描,没有使用索引
总结:ref
> range
> index
> all
。
了解分页查询吗?第一页查询和最后一页查询哪一个快?
第一页查询通常比最后一页快。 使用 LIMIT
和 OFFSET
进行分页时,数据库必须跳过前面的记录。例如,LIMIT 10 OFFSET 0
(第一页)只需读取前10条记录,而 LIMIT 10 OFFSET 99990
(最后一页)则需要跳过99990条记录后再读取10条,导致性能下降。
为提高性能,建议使用 “键集分页”(Keyset Pagination),即基于唯一字段(如自增ID)进行分页:
SELECT * FROM table WHERE id > ? ORDER BY id ASC LIMIT 10;
MySQL索引怎么实现 (原理)?
MySQL 索引主要通过 B+ 树实现,其原理是将所有数据存储在叶子节点,非叶子节点仅存储索引键。这种结构使得树的高度较低,减少磁盘 I/O 次数,提高查询效率。此外,叶子节点之间通过链表连接,支持高效的范围查询和顺序访问。由于非叶子节点不存储实际数据,每个节点可以容纳更多的索引键,从而进一步降低树的高度。这种设计充分利用磁盘预读特性,适合存储在磁盘上的大规模数据,提高了查询性能。
怎么优化SQL语句查询效率?
可以分为预防和解决慢查询两种方案。
【预防】
- 创建合适的索引:对需要频繁查询、数据分布一致性低、
group by
分组、order by
排序的列建立单列索引或者联合索引。 - 避免索引失效:联合索引最左前缀匹配原则、单列索引避免使用函数、计算、
like %xx%
等 - 减少回表和I/O次数:避免
select *
操作。因为正常情况下,部分字段是没有二次索引的,它会用主键id或者rowid
进行回表查询,会增加系统的I/O。
【解决慢查询】
- 开启慢SQL日志记录功能:使用
set global slow_query_log = "ON"
, 默认是关闭的。设置一个查询延迟的阈值,把超过规定时间的SQL查询找出来。 - 分析慢SQL:利用
explain
关键字分析慢SQL的原因,比如看看是否有索引失效、select *
等情况
MySQL用了哪些优化方式?
- 存储引擎优化:默认使用 InnoDB,支持事务、行级锁和崩溃恢复。
- 索引优化:使用 B+ 树索引,支持复合索引和覆盖索引,提升查询效率。
- 查询优化:利用查询优化器选择最佳执行计划,避免全表扫描。
- 缓存机制:通过调整
innodb_buffer_pool_size
等参数,提高数据和索引的缓存命中率。 - 连接管理:使用连接池减少连接开销,提升并发处理能力。
- 配置调整:根据负载调整参数,如
max_connections
、query_cache_size
等,优化资源使用。 - 分库分表:对大数据量进行水平或垂直拆分,减小单表压力。
- 读写分离:主从复制架构中,主库负责写操作,从库负责读操作,提升整体性能。
MySQL是左优先还是右优先?
MySQL 使用 “最左前缀匹配原则”,即索引匹配从最左列开始,必须连续匹配。例如,联合索引 (a, b, c)
中,查询条件必须包含 a
,才能利用索引。如果查询条件是 b=1
或 c=1
,索引将不会被使用。此外,遇到范围查询(如 >
、<
、BETWEEN
、LIKE
)时,匹配会停止,后续列无法利用索引。查询优化器会尝试重排条件顺序,但必须包含最左列才能命中索引。
聊一下MySQL隔离级别 ?
MySQL的隔离级别有四种:读未提交 RU、读已提交 RC、可重复读 RR、串行化 MySQL的默认隔离级别为 可重复度 RR
隔离性 | 读未提交 RU | 读已提交 RC | 可重复读 RR | 串行读 |
---|---|---|---|---|
脏读 | ❌ | ✅ | ✅ | ✅ |
不可重复读 | ❌ | ❌ | ✅ | ✅ |
幻读 | ❌ | ❌ | ❌ | ✅ |
- 脏读:事务A开启事务准备查询
name = SwimmingLiu
的学生信息 (数据库当中age = 23
),网络延迟还没开始执行。此时,事务B修改name = SwimmingLiu
的age = 99
, 然后区执行其他的操作,还没提交事务。事务A读取到的数据为age = 99
, 但是此时数据库中的数据为age = 23
,出现数据不一致的情况。 - 不可重复读:事务A开启事务准备查询
name = SwimmingLiu
的学生信息age = 23
,然后去执行其他的事务。此时,事务B修改name = SwimmingLiu
的age = 99
,提交事务。事务A 第二次重新去读取name = SwimmingLiu
的学生信息 (age = 99
), 第二次读取的数据和第一次读取的数据出现数据不一致的情况。 - 幻读:事务A最开始查询
age = 23
的学生人数,发现有 10 个人,然后去执行其他的操作。事务B 新增了一条age = 23
的学生信息数据,提交事务。事务A第二次去查询age = 23
的学生人数的时候,发现学生人数变成了 11 个人,和第一次读取的数据总量相比不一样。
MySQL的乐观锁,悲观锁 ?
-
悲观锁:假设会发生并发冲突,操作前加锁。
-- 悲观锁,使用SELECT ... FOR UPDATE START TRANSACTION; -- 开启事务 SELECT * FROM products WHERE id = 1 FOR UPDATE; -- 上锁(排他锁) UPDATE products SET stock = stock - 1 WHERE id = 1; COMMIT;
-
乐观锁:假设不会发生冲突,更新时校验版本号。
-- 表结构添加version字段 ALTER TABLE products ADD COLUMN version INT DEFAULT 0; -- 查询当前版本 SELECT id, stock, version FROM products WHERE id = 1; -- 乐观锁更新,条件中包含版本号 UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 当前版本号;
Redis
聊一下Redis的应用场景 ?
- 数据缓存:消息缓存、商品缓存预览
- 分布式锁:操作临界资源的时候,使用分布式锁进行上锁。
- 计数器(Counter):适用于实现访问量统计、点赞数等功能,支持高并发下的原子自增操作。
- 排行榜(Leaderboard):通过有序集合(Sorted Set)实现实时排名功能,如游戏积分榜等。
- 位图(Bitmap):用于实现签到、活跃用户统计等功能,节省存储空间。
- 全局唯一 ID 生成(Unique ID Generation):通过自增键生成全局唯一的标识符,适用于订单号等场景。
缓存三件套和对应的解决方案?
- 穿透:客户端查询的数据在数据库中不存在,所有的请求都打到数据库上,导致数据库压力过大。解决方法有两种,一种是对数据库查询不到的数据,同样存入Redis当中,值设置为
null
。另外一种方案是采用布隆过滤器的策略,过滤掉部分数据不存在的请求。 - 雪崩:Redis当中大量的key同时过期,导致大部分请求都打到数据库上。解决方法是设置随机的过期时间
TTL
,防止大量key同时过期。 - 击穿:Redis当中某个热点Key突然过期,大量的流量同一时间全部打到数据库上。可以采用互斥锁、逻辑过期两种方案。如果对数据一致性要求高,可以采用互斥锁。如果要求不高,可以采用逻辑过期的方案,可能返回的缓存数据和数据库不一致。
另外,雪崩和击穿都可以采用限流+熔断的机制,暂停服务对于缓存服务的访问,直接返回错误。或者启用限流规则,只允许商家或指定类型的用户请求发送数据库进行处理,过多的请求就会拒接。一般会使用 Hystrix
或者 Sentinel
实现熔断或者限流。
为什么要用Redis进行缓存?
因为Redis是基于内存的,查询速度比MySQL数据库的速度要快很多倍。用Redis进行缓存,可以加速查询操作。
Redis持久化机制有哪些?各自的优缺点?
Redis的持久化机制主要有两种:RDB
和 AOF
-
RDB: Redis在指定的时间间隔内,生成数据库的快照并将其保存为二进制文件(dump.rdb)。
优点:
- 性能高:RDB是通过生成快照的方式进行持久化,不会阻塞客户端操作。
- 备份方便:可以通过RDB文件进行数据备份,且RDB文件较小。
- 恢复速度快:Redis重启时,加载RDB文件比AOF恢复速度要快。
缺点:
- 数据丢失:如果Redis在快照保存期间宕机,会丢失未持久化的数据。
- 持久化频率不灵活:需要根据业务需求手动设置持久化的间隔。
-
**AOF: **Redis将每次写操作追加到AOF日志文件中,保存所有写命令。
优点:
- 数据持久性高:AOF保证了所有写操作都会被记录,可以做到较高的数据可靠性。
- 灵活的持久化频率:AOF有三种同步策略:
- 每次写入后同步:最安全,但性能最差。
- 每秒同步:安全与性能的折衷。
- 从不同步:性能最佳,但最容易丢失数据。
缺点:
- 性能开销大:频繁的文件追加和同步可能会影响性能。
- 恢复速度慢:AOF文件较大,恢复时需要重新执行所有写命令。
- 文件大小:AOF文件相较于RDB文件要大,且随着操作增多,AOF文件会变得越来越大。
你刚刚说到的RDB和AOF,是怎么开启的呢?你有修改过Redis的配置文件吗?开启的参数是什么?
-
RDB:
-
redis.conf保留/添加
save 900 1
,save 300 10
,save 60 10000
等行即可(默认已启用 RDB)。save 900 1 # 15分钟至少1次修改触发 save 60 10000 # 1分钟至少10000次修改触发 rdbcompression yes # 启用压缩减少磁盘占用
-
在线开启:
CONFIG SET save "900 1 300 10 60 10000"
,随后CONFIG REWRITE
写回文件。
-
-
AOF
- redis.conf 设置
appendonly yes
,通常配置appendfsync everysec
。 - 在线开启:
CONFIG SET appendonly yes
,Redis 会自动触发 AOF 重写;完成后执行CONFIG REWRITE
。 - AOF 文件名默认 appendonly.aof,可用 appendfilename 自定义。
- redis.conf 设置
聊一下Redis集群 ?
Redis集群有两种模式:一种是主从节点+ 哨兵机制 Sential
模式,另外一种是 Redis Cluster
模式
Cluster
集群当中包含多个 哨兵机制 Sential
模式的主从节点
-
Cluster集群模式:集群模式用于对数据进行分片,主要用于解决大数据、高吞吐量的场景。将数据自动分不到多个Redis实例上,支持自动故障转移(如果某个实例失效,集群会自动重写配置和平衡,不需要手动进行调整,因为内置了哨兵逻辑)
-
Sentinel哨兵模式: 哨兵模式用于保证主从节点的高可用,读写分离场景。如果主节点宕机,哨兵会将从节点升为主节点。
如果集群的Redis中一台突然挂了,此时有请求未处理怎么办?
-
选取新的主节点:从有效的从节点中选取一个从节点做为新的主节点(选取条件:优先级 > 复制进度
offset
> 从节点id
大小) -
客户端重试与重定向:Redis集群的Smart客户端(如JedisCluster)内置路由表,当请求发送至故障节点时,客户端会收到
MOVED
或ASK
重定向指令,自动将请求转发至新主节点。
Redis如何设置均衡负载的?
Redis 缓存是如何更新的 (数据一致性问题)?
- 先写数据库,再删缓存:除了删除缓存操作失败以外,能确保数据一致性问题
- 延迟双删 (先删缓存,再写数据库,再延迟删除缓存):除了删除缓存操作失败以外,能确保数据一致性问题,另外也不好确定延迟的时间(一般是手动设置的)
【强一致性】
binlog
+Canal
:通过binlog
检测数据库是否发生改动,如果出现改动,就触发删除缓存的机制。删除缓存操作使用消息队列的方式实现,之后当Redis消息被成功删除,才消费这条消息。
为什么Redis是单线程的 ?
- 基于内存操作,Redis的瓶颈主要是内存,多数操作的性能瓶颈不是CPU带来的 (增加多线程也没啥用)
- 单线程模型的代码简单,可以减少线程上下文切换的性能开销。
- 单线程结合I/O多路复用模型,能提高I/O利用率
【注意】 Redis的单线程是指网络请求模块和数据操作模块是单线程的, 但是持久化存储模块和集群支撑模块是多线程的。
Spring
Spring的两大特性?IOC和AOP?
-
IoC(控制反转):将对象的创建和依赖关系交由 Spring 容器管理,实现解耦和灵活配置。
-
AOP(面向切面编程):将横切关注点(如日志、事务)从业务逻辑中抽离,集中管理,提高代码的模块化和可维护性。
Spring 的 bean 生命周期?
Spring
的 Bean
生命周期从容器启动开始,首先加载 Bean
定义并实例化 Bean
。然后 Spring
容器注入依赖,并调用初始化方法,如 @PostConstruct
注解的方法或配置文件中指定的 init-method 方法。Bean
完成初始化后,准备好供应用程序使用。当容器关闭时,Spring 会调用销毁方法,如 @PreDestroy
注解的方法或配置文件中指定的 destroy-method 方法,最后销毁 Bean 实例。
SpringMVC 中的Bean作用域 ?
- singleton(默认):整个 Spring 容器中仅有一个实例,适用于无状态的共享组件。
- prototype:每次请求都会创建一个新的实例,适用于有状态的组件。
- request:每个 HTTP 请求创建一个新的实例,适用于每次请求需要独立状态的组件。
- session:每个 HTTP 会话创建一个新的实例,适用于需要在用户会话中共享状态的组件。
- application:整个
ServletContext
共享一个实例,适用于需要在整个应用中共享状态的组件。
Springboot如何加载配置文件 ?
Spring Boot通过多源层级化的配置加载机制,支持从多种来源(如属性文件、YAML文件、环境变量、命令行参数等)动态加载配置,并按优先级从高到低覆盖同名属性。默认情况下,它会优先加载命令行参数(如 --server.port=8080
),其次是环境变量(如SPRING_DATASOURCE_URL
)和外部配置文件(application.yml
或 application.properties
),最后是项目内部资源目录下的默认配置文件。另外,可以通过spring.profiles.active
激活特定环境配置,比如dev
、test
、prod
环境
【注意】application.properties
的优先级高于 application.yml
SpringMVC的启动流程 ?
首先,启动时加载 DispatcherServlet
,它是 SpringMVC
的核心控制器;
其次,DispatcherServlet
通过 web.xml
配置文件读取相关配置,初始化Spring容器;
然后,DispatcherServlet
根据 URL请求 通过 HandlerMapping
查找对应的处理器(Controller);
最后,执行处理器方法后,通过 ViewResolver
解析视图并返回给客户端。
整个过程实现了请求的分发、处理和响应。
其他
你自己在部署项目的时候有排查过问题没有?简单描述一下查询日志的linux命令
排查过问题, 我使用的是 tail -n 20 -f xxx.log
Linux中pwd命令,以及如何查找指定文件命令
pwd
命令:这个命令是用来显示当前路径的绝对路径的- 查找指定文件命令:
find /home -name xxx.txt
AI嵌入进IDEA中的底层原理是什么?什么算法?
copilot
: 大语言模型接口 + 当前文件 / 指定文件做为上下文
设计模式了解哪些,设计模式有哪些原则
【设计模式】
设计模式是对软件设计中常见问题的通用可复用解决方案,分为三大类:
- 创建型模式:解决对象创建的复杂性,如 Singleton(单例)、Factory Method(工厂方法)、Abstract Factory(抽象工厂)、Builder(建造者)、Prototype(原型)。
- 结构型模式:处理类和对象的组合,简化结构,如 Adapter(适配器)、Bridge(桥接)、Composite(组合)、Decorator(装饰器)、Facade(外观)、Flyweight(享元)、Proxy(代理)。
- 行为型模式:关注对象之间的通信和职责分配,如 Chain of Responsibility(责任链)、Command(命令)、Iterator(迭代器)、Observer(观察者)、Strategy(策略)、Template Method(模板方法)、Visitor(访问者)。
【设计原则】
设计原则则是指导软件开发的高层次准则,帮助构建可维护、可扩展的系统。其中,SOLID 是五个核心面向对象设计原则的首字母缩写:
- 单一职责原则(SRP):一个类应仅有一个引起其变化的原因,即只承担一个职责。
- 开闭原则(OCP):软件实体应对扩展开放,对修改关闭,即可以在不修改现有代码的情况下扩展功能。
- 里氏替换原则(LSP):子类型对象应能够替换任何父类型对象,并且程序行为不受影响。
- 接口隔离原则(ISP):不应强迫客户依赖它们不使用的方法,建议为不同的客户提供专门的接口。
- 依赖倒置原则(DIP):高层模块不应依赖低层模块,二者都应依赖于抽象;抽象不应依赖细节,细节应依赖抽象。
请求转发和重定向区别 ?
特性 | 请求转发(Forward) | 请求重定向(Redirect) |
---|---|---|
跳转方式 | 服务器端内部跳转 | 浏览器发起新请求 |
请求次数 | 一次请求 | 至少两次请求 |
URL 地址栏 | 不变 | 更新为新 URL |
数据共享 | 同一请求对象,共享数据 | 不共享数据,需通过 session 或 URL 参数传递 |
性能 | 较快 | 较慢 |
使用场景 | 同一 Web 应用内部跳转 | 跨域、跨站点跳转,或避免重复提交等场景 |
TCP三次握手,4次挥手
简历相关八股
为什么要设置一人一单?如何解决超卖问题和一人一单问题?
- 设置一人一单:防止顾客反复刷单
- 超卖问题:MySQL 乐观锁检测
stock > 0
/ Redis用lua
脚本 - 一人一单问题:MySQL 统计是否存在该用户的订单 / Redis 用
lua
脚本
消息队列怎么保证数据可靠性 ?
消息队列(MQ)通过多种机制确保数据的可靠性,主要包括:
- 消息持久化:将消息存储到磁盘,确保在系统崩溃或重启后消息不会丢失。
- 消息确认机制:生产者在发送消息后,等待消息被消费者确认处理成功,未确认的消息会被重试发送。
- 重试机制:在消息发送或消费失败时,系统会按照预定策略进行重试,直至消息成功处理或达到最大重试次数。
- 幂等性处理:消费者在处理消息时,确保同一消息多次消费不会导致数据不一致。
- 死信队列:将无法成功消费的消息转移到专门的队列中,供后续人工或系统处理。
- 事务机制:在生产者和消费者之间实现事务,确保消息的发送和消费操作要么都成功,要么都失败。
Websocket,websocket 与 http 区别?
Websocket
相当于全双工通信,例如直播聊天就是采用 Websocket
全双工通信,客户端和服务端可以双向发送消息
【websocket 和 http 区别】
HTTP
是基于请求-响应模式的无状态短连接协议,客户端每次需主动发起请求,服务器返回响应后连接立即断开,适用于传统网页加载等低频交互场景;WebSocket
则通过一次 HTTP 握手升级为全双工长连接协议,支持客户端与服务器实时双向通信,数据以二进制帧高效传输,减少了头部冗余开销,适用于在线聊天、实时游戏等高频低延迟场景。此外,HTTP 默认无加密(HTTPS 需额外配置),而 WebSocket 天然支持 WSS 加密,安全性更优。
场景题
我现在有两千万的客户数据,但是我的Redis中只能存储20万的高净值用户数据,那么我该怎么确保每次拿出来的都是高净值的用户呢?
筛选的步骤如图所示:
-
高净值用户定义与数据建模: 根据业务特征定义多维度评估模型,构建指标体系。例如,金融资产总额 (40%)、年消费频次 (30%)、最近活跃时间 (20%)、风险等级 (10%)
@Data public class Customer { private String id; // 用户ID(唯一标识) private BigDecimal assets; // 金融资产(精确计算) private int annualPurchases; // 年消费次数 private LocalDateTime lastActive; // 最近活跃时间 private int riskLevel; // 风险等级(1-5级) // 计算综合得分(需缓存避免重复计算) private transient BigDecimal score; }
-
高效筛选2000万中的20万优质数据:
- 方案1:Stream API + 并行计算(适合全量筛选)
List<Customer> allCustomers = loadFromDatabase(); // 从数据库加载2000万数据 Predicate<Customer> highValueFilter = customer -> customer.getRiskLevel() <= 3 && // 风险等级≤3 customer.getLastActive().isAfter(LocalDateTime.now().minusMonths(3)); // 近3个月活跃 List<Customer> topCustomers = allCustomers.parallelStream() // 启用并行流 .filter(highValueFilter) .sorted(Comparator.comparing(Customer::getScore).reversed()) // 按得分降序 .limit(200_000) // 取前20万 .collect(Collectors.toList());
- 方案2:数据库分页+实时计算(适合增量更新)
// 使用JPA/Hibernate分页查询(避免内存溢出) int pageSize = 5000; int page = 0; List<Customer> batchList; do { batchList = customerRepository.findHighValueCustomers( PageRequest.of(page, pageSize, Sort.by("score").descending()) ); redisClient.batchInsert(batchList); // 批量写入Redis page++; } while (!batchList.isEmpty());
-
Redis 存储数据 (对应上面的批量写入Redis):
-
存储客户排名数据:采用 Sorted Set(ZSET) 以用户ID为
member
,综合得分为score
,自动按score
排序,天然支持TOP N
查询。Jedis jedis = RedisPool.getResource(); Pipeline pipeline = jedis.pipelined(); topCustomers.forEach(c -> pipeline.zadd("high_value_users", c.getScore().doubleValue(), c.getId()) ); pipeline.sync();
-
存储客户信息数据:用Hash存储用户详细信息
topCustomers.forEach(c -> { pipeline.hset("user:" + c.getId(), "assets", c.getAssets().toString(), "lastActive", c.getLastActive().toString() ); });
-
容量控制 (只保留20万数据):
// 保留前20万,自动淘汰低分用户 jedis.zremrangeByRank("high_value_users", 200_000, -1); // 设置TTL避免数据过期(数据不一致) jedis.expire("high_value_users", 86400);
-
-
动态更新机制 (定时更新)
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行 public void refreshHighValueUsers() { List<Customer> newHighValue = customerRepository.findNewHighValueUsers(); // 使用Lua脚本保证原子性(网页6[6](@ref)) String luaScript = "redis.call('ZADD', KEYS[1], ARGV[1], ARGV[2]) " + "redis.call('ZREMRANGEBYRANK', KEYS[1], 200000, -1)"; jedis.eval(luaScript, 1, "high_value_users", newHighValue.getScore(), newHighValue.getId()); }
我们在实际部署过程中,经常会遇到节点挂掉或者是整个Redis挂掉的情况,那么怎么才能保证Redis挂掉之后能够让数据进行恢复呢?
- 打开 AOF (everysec) + RDB 双持久化;重启时先加载 RDB 再回放 AOF,几乎零丢数据。
- 部署主从复制+ 大于等于3 个 的哨兵Sentinel 或直接用 Redis Cluster,节点宕机秒级自动选主并切换客户端。
- 定时把 AOF/RDB 备份到外部存储;灾难时拉回文件启动或 RESTORE 即可,注意多 TB 备份的加载时间并定期演练。
【AOF 和 RDB 如何打开?】
-
RDB:
-
redis.conf保留/添加
save 900 1
,save 300 10
,save 60 10000
等行即可(默认已启用 RDB)。save 900 1 # 15分钟至少1次修改触发 save 60 10000 # 1分钟至少10000次修改触发 rdbcompression yes # 启用压缩减少磁盘占用
-
在线开启:
CONFIG SET save "900 1 300 10 60 10000"
,随后CONFIG REWRITE
写回文件。
-
-
AOF
- redis.conf 设置
appendonly yes
,通常配置appendfsync everysec
。 - 在线开启:
CONFIG SET appendonly yes
,Redis 会自动触发 AOF 重写;完成后执行CONFIG REWRITE
。 - AOF 文件名默认 appendonly.aof,可用 appendfilename 自定义。
- redis.conf 设置
我现在Redis是个单机,但是我现在有很多个系统,同时对一个类进行操作,那么我的一个key肯定会导致被多并发的去竞争?怎么解决这样的竞争问题?
-
分布式锁(SETNX):使用Redis的
SETNX
命令或者Redisson
实现分布式锁,确保同一时间只有一个系统实例操作该key。 -
消息队列串行化:将对同一
key
的操作放入消息队列,确保操作按顺序执行,避免并发冲突。例如,使用RabbitMQ或Kafka等消息中间件,将操作封装为消息,按顺序处理。 -
时间戳控制:在写入key时,携带时间戳,只有当新操作的时间戳晚于当前存储的时间戳时,才执行写入,确保数据的时序性。
HSET your_key value new_value timestamp new_timestamp
高并发主要要考虑哪些问题?
- 线程/连接池容量:
Tomcat workThreads
、HikariCP max‑pool‑size
必须按CPU×2+I/O
负载估算,防止池耗尽和排队 - 熔断‑限流‑隔离:用
Hystrix/Sentinel/Gateway
做线程池隔离、熔断与基于 Redis 的限流,阻断级联雪崩 - 缓存策略:热点预加载、互斥锁、布隆过滤器,分散过期时间,解决穿透/击穿/雪崩
- 数据一致性:幂等键+
TCC/SAGA
分布式事务,避免重复写与脏数据 - JVM GC停顿:
G1/ZGC + -XX:MaxGCPauseMillis=*
,持续压测、监控 - 超时/重试/背压:超时 > 重试总时长;Reactive 流背压守护线程池
- 可观测性:指标、日志、分布式 Tracing 持续审计性能瓶颈。
对一个字符串进行排序然后转换为大写怎么实现?
将字符串专程数组,然后调用 Arrays.sort()
函数,然后调用 new String(chars).toUpperCase()
将字符数组转成字符串,然后将所有的字母都转换成大小。
String originalStr = "helloWorld";
// 1. 转换为字符数组并排序
char[] chars = originalStr.toCharArray();
Arrays.sort(chars);
// 2. 生成排序后的字符串并转大写
String sortedUpperStr = new String(chars).toUpperCase();
System.out.println(sortedUpperStr); // 输出: DEHLLLOORW