八股题

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的基本数据类型?

Java基础数据类型

String创建对象时如何创建,创建几个对象,用new和不用的区别?

String创建对象的时候,可能会创建1个或者2个对象。

如果不使用 new

  • 第一次需要创建字符串字面量存入字符串常量池中,String s1 = "SwimmingLiu",引用该字符串字面量
  • 第二次如果需要相同的字符串,String s2 = "SwimmingLiu",引用该字符串字面量且不会创建任何对象

如果使用new :每次创建字符串对象都会在堆内存中存放一个新的对象

  • 如果字符串字面量不在字符串常量池中,会创建两个对象(堆内存对象 + 字符串常量池中的对象),String s3 = new String("SwimmingLiu")
  • 如果字符串字面量已经在字符串常量池中,只会创建一个对象(堆内存对象),String s4 = new String("SwimmingLiu")

字符串创建对象区别

字符串操作有哪些类?有什么区别?

StringStringBuilderStringBuffer

  • 线程安全StringStringBuffer
    • StringString 是不可变量,底层用 final 修饰,每次对String修改都会产生新的副本,从而占用更多的资源,频繁大量的修改会造成资源的浪费
    • StringBufferStringBuffer 是为了解决String 可能造成资源浪费的问题,底层用 char[] 数组,所有修改方法采用 synchronized 锁, 所以线程安全
  • 线程不安全StringBuilder
    • StringBuilderStringBuilderStringBuffer 的基础上把 synchronized 锁去掉了,舍弃了线程安全单性能更高

如何通过Stream流进行过滤、集合和映射的操作?

JUC 并发

Java的锁有哪些?

  • 内置锁(synchronized):Java语言层面提供的关键字,隐式加锁,使用简单。
  • 显示锁(Lock接口及其实现):比如 ReentrantLockReentrantReadWriteLock(读写锁)StampedLock 等,提供更灵活的锁操作,如可中断、公平性等。

【不同锁的区别】

  1. synchronized:内置锁(Monitor Lock),可以用于方法或代码块,提供互斥访问。当一个线程进入 synchronized 方法或块时,它会自动获取对象的锁,其他线程则需等待锁释放后才能进入。 synchronized 是一种非公平,悲观,独享,互斥,可重入的重量级锁

  2. ReentrantLock:是一个重入锁,是 java.util.concurrent.locks 包中的接口 Lock 的实现,提供了比 synchronized 更灵活的锁操作,如尝试获取锁、可中断的获取锁、超时获取锁等。它也支持公平锁和非公平锁策略。 ReentrantLock 是一种默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁

  3. ReentrantReadWriteLock(读写锁):也是 java.util.concurrent.locks 包中的一部分,允许同时有多个读取者,但只允许一个写入者。它分为读锁和写锁,读锁之间不互斥,读锁与写锁互斥,写锁之间也互斥,适用于读多写少的场景。

    ReentrantReadWriteLock 是一种 默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。

  4. StampedLock(Java 8 引入):提供了三种锁模式:读锁、写锁和乐观读锁。相较于 ReentrantReadWriteLockStampedLock 提供了更细粒度的控制,支持乐观读取操作,可以提高并发性能。

可以给我简述一下你都有什么并发经验吗 (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 就定义了一系列关键字 volatilesynchronizedfinal 确保程序能正确执行,还定义了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 区)。

具体实现步骤如下:

  1. 选择合适的GCCMS (实时Web服务、电商秒杀等对响应时间敏感的场景)、G1 (平衡吞吐与延迟,如微服务集群、分布式缓存)
  2. 调整堆和新生代大小:内存设置合理可以减少 GC 频率,通过设置 -Xms-Xmx 调整堆内存初始/最大值,结合 -Xmn-XX:NewRatio 控制新生代占比,并通过 -XX:SurvivorRatio 调节 Eden 与 Survivor 区比例,根据应用对象生命周期和 GC 监控动态优化。
  3. 启用GC日志检测:监控和分析GC的行为,找出性能瓶颈
  4. 调整GC线程:提高并行GC性能

【CMS 和 G1对比】

维度 CMS G1
算法 标记-清除(内存碎片) 标记-整理(减少碎片)
停顿时间 短(但可能因碎片触发Full GC) 可预测(默认200ms,可调)
堆内存范围 中小堆(<32GB) 大堆(4GB~32GB+)
CPU占用 高(并发阶段占用25% CPU) 较低(并发线程自适应)

JVM 运行时内存情况,每个地方存储的是什么?(JVM内存区域如何划分?)

堆内存、方法区(元空间)、直接内存、虚拟机栈、本地方法栈、程序计数器

JVM内存区域

线程的共享区域以及非共享?

  • 共享区域:堆内存、直接内存、方法区(元空间)
  • 私有区域:虚拟机栈、本地方法栈、程序计数器

MySQL

一条SQL语句的执行过程?

  1. 检查连接: 校验账号密码,确定用户的连接权限
  2. 缓存查询:如果存在缓存,直接返回查询结果。(MySQL 8.0之后废弃)
  3. 语法分析:通过语法树分析SQL语句的语法是否正确
  4. 查询条件优化:优化 where 语句中的查询条件,比如联合索引拼接等等
  5. 执行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 表示查询的访问类型,影响查询的效率。常见的值:

  1. ref: 使用索引,查找匹配某个单一列的值(比如通过外键查找)。比 range 更高效。
  2. range: 使用索引扫描某个范围内的值,适用于 BETWEEN> < 等条件。
  3. index: 全索引扫描,扫描整个索引结构,不读表数据,通常效率比全表扫描好。
  4. all: 全表扫描,没有使用索引

总结:ref > range > index > all

了解分页查询吗?第一页查询和最后一页查询哪一个快?

第一页查询通常比最后一页快。 使用 LIMITOFFSET 进行分页时,数据库必须跳过前面的记录。例如,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_connectionsquery_cache_size 等,优化资源使用。
  • 分库分表:对大数据量进行水平或垂直拆分,减小单表压力。
  • 读写分离:主从复制架构中,主库负责写操作,从库负责读操作,提升整体性能。

MySQL是左优先还是右优先?

MySQL 使用 “最左前缀匹配原则”,即索引匹配从最左列开始,必须连续匹配。例如,联合索引 (a, b, c) 中,查询条件必须包含 a,才能利用索引。如果查询条件是 b=1c=1,索引将不会被使用。此外,遇到范围查询(如 ><BETWEENLIKE)时,匹配会停止,后续列无法利用索引。查询优化器会尝试重排条件顺序,但必须包含最左列才能命中索引。

聊一下MySQL隔离级别 ?

MySQL的隔离级别有四种:读未提交 RU、读已提交 RC、可重复读 RR、串行化 MySQL的默认隔离级别为 可重复度 RR

隔离性 读未提交 RU 读已提交 RC 可重复读 RR 串行读
脏读
不可重复读
幻读
  • 脏读:事务A开启事务准备查询 name = SwimmingLiu 的学生信息 (数据库当中 age = 23 ),网络延迟还没开始执行。此时,事务B修改 name = SwimmingLiuage = 99, 然后区执行其他的操作,还没提交事务。事务A读取到的数据为 age = 99, 但是此时数据库中的数据为 age = 23,出现数据不一致的情况。
  • 不可重复读:事务A开启事务准备查询 name = SwimmingLiu 的学生信息 age = 23 ,然后去执行其他的事务。此时,事务B修改 name = SwimmingLiuage = 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的持久化机制主要有两种:RDBAOF

  • 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集群 ?

Redis集群有两种模式:一种是主从节点+ 哨兵机制 Sential 模式,另外一种是 Redis Cluster 模式 Cluster 集群当中包含多个 哨兵机制 Sential 模式的主从节点

  • Cluster集群模式:集群模式用于对数据进行分片,主要用于解决大数据、高吞吐量的场景。将数据自动分不到多个Redis实例上,支持自动故障转移(如果某个实例失效,集群会自动重写配置和平衡,不需要手动进行调整,因为内置了哨兵逻辑

  • Sentinel哨兵模式: 哨兵模式用于保证主从节点的高可用,读写分离场景。如果主节点宕机,哨兵会将从节点升为主节点。

Cluster集群

如果集群的Redis中一台突然挂了,此时有请求未处理怎么办?

  • 选取新的主节点:从有效的从节点中选取一个从节点做为新的主节点(选取条件:优先级 > 复制进度 offset > 从节点 id 大小)

  • 客户端重试与重定向:Redis集群的Smart客户端(如JedisCluster)内置路由表,当请求发送至故障节点时,客户端会收到 MOVEDASK 重定向指令,自动将请求转发至新主节点。

Redis如何设置均衡负载的?

Redis 缓存是如何更新的 (数据一致性问题)?

  • 先写数据库,再删缓存:除了删除缓存操作失败以外,能确保数据一致性问题
  • 延迟双删 (先删缓存,再写数据库,再延迟删除缓存):除了删除缓存操作失败以外,能确保数据一致性问题,另外也不好确定延迟的时间(一般是手动设置的)

【强一致性】

  • binlog + Canal:通过 binlog 检测数据库是否发生改动,如果出现改动,就触发删除缓存的机制。删除缓存操作使用消息队列的方式实现,之后当Redis消息被成功删除,才消费这条消息。

为什么Redis是单线程的 ?

  1. 基于内存操作,Redis的瓶颈主要是内存,多数操作的性能瓶颈不是CPU带来的 (增加多线程也没啥用)
  2. 单线程模型的代码简单,可以减少线程上下文切换性能开销
  3. 单线程结合I/O多路复用模型,能提高I/O利用率

【注意】 Redis的单线程是指网络请求模块和数据操作模块是单线程的, 但是持久化存储模块和集群支撑模块是多线程的。

Spring

Spring的两大特性?IOC和AOP?

  • IoC(控制反转):将对象的创建和依赖关系交由 Spring 容器管理,实现解耦和灵活配置。

  • AOP(面向切面编程):将横切关注点(如日志、事务)从业务逻辑中抽离,集中管理,提高代码的模块化和可维护性。

Spring 的 bean 生命周期?

SpringBean 生命周期从容器启动开始,首先加载 Bean 定义并实例化 Bean。然后 Spring 容器注入依赖,并调用初始化方法,如 @PostConstruct 注解的方法配置文件中指定的 init-method 方法Bean 完成初始化后,准备好供应用程序使用。当容器关闭时,Spring 会调用销毁方法,如 @PreDestroy 注解的方法配置文件中指定的 destroy-method 方法,最后销毁 Bean 实例

SpringMVC 中的Bean作用域 ?

  1. singleton(默认):整个 Spring 容器中仅有一个实例,适用于无状态的共享组件。
  2. prototype:每次请求都会创建一个新的实例,适用于有状态的组件。
  3. request:每个 HTTP 请求创建一个新的实例,适用于每次请求需要独立状态的组件。
  4. session:每个 HTTP 会话创建一个新的实例,适用于需要在用户会话中共享状态的组件。
  5. application:整个 ServletContext 共享一个实例,适用于需要在整个应用中共享状态的组件。

Springboot如何加载配置文件 ?

Spring Boot通过多源层级化的配置加载机制,支持从多种来源(如属性文件、YAML文件、环境变量、命令行参数等)动态加载配置,并按优先级从高到低覆盖同名属性。默认情况下,它会优先加载命令行参数(如 --server.port=8080),其次是环境变量(如SPRING_DATASOURCE_URL)和外部配置文件application.ymlapplication.properties),最后是项目内部资源目录下的默认配置文件。另外,可以通过spring.profiles.active 激活特定环境配置,比如devtestprod 环境

【注意】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)通过多种机制确保数据的可靠性,主要包括:

  1. 消息持久化:将消息存储到磁盘,确保在系统崩溃或重启后消息不会丢失。
  2. 消息确认机制:生产者在发送消息后,等待消息被消费者确认处理成功,未确认的消息会被重试发送。
  3. 重试机制:在消息发送或消费失败时,系统会按照预定策略进行重试,直至消息成功处理或达到最大重试次数。
  4. 幂等性处理:消费者在处理消息时,确保同一消息多次消费不会导致数据不一致。
  5. 死信队列:将无法成功消费的消息转移到专门的队列中,供后续人工或系统处理。
  6. 事务机制:在生产者和消费者之间实现事务,确保消息的发送和消费操作要么都成功,要么都失败。

Websocket,websocket 与 http 区别?

Websocket 相当于全双工通信,例如直播聊天就是采用 Websocket 全双工通信,客户端和服务端可以双向发送消息

【websocket 和 http 区别】

HTTP 是基于请求-响应模式无状态短连接协议,客户端每次需主动发起请求,服务器返回响应后连接立即断开,适用于传统网页加载等低频交互场景WebSocket 则通过一次 HTTP 握手升级为全双工长连接协议,支持客户端与服务器实时双向通信,数据以二进制帧高效传输,减少了头部冗余开销,适用于在线聊天实时游戏等高频低延迟场景。此外,HTTP 默认无加密(HTTPS 需额外配置),而 WebSocket 天然支持 WSS 加密,安全性更优。

场景题

我现在有两千万的客户数据,但是我的Redis中只能存储20万的高净值用户数据,那么我该怎么确保每次拿出来的都是高净值的用户呢?

筛选的步骤如图所示:

  1. 高净值用户定义与数据建模: 根据业务特征定义多维度评估模型,构建指标体系。例如,金融资产总额 (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; 
    }
    
  2. 高效筛选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());
    
  3. 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); 
      
  4. 动态更新机制 (定时更新)

    @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());
    }
    

image-20250505234704461

我们在实际部署过程中,经常会遇到节点挂掉或者是整个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是个单机,但是我现在有很多个系统,同时对一个类进行操作,那么我的一个key肯定会导致被多并发的去竞争?怎么解决这样的竞争问题?

  • 分布式锁(SETNX):使用Redis的 SETNX 命令或者Redisson实现分布式锁,确保同一时间只有一个系统实例操作该key。

  • 消息队列串行化:将对同一 key 的操作放入消息队列,确保操作按顺序执行,避免并发冲突。例如,使用RabbitMQ或Kafka等消息中间件,将操作封装为消息,按顺序处理。

  • 时间戳控制:在写入key时,携带时间戳,只有当新操作的时间戳晚于当前存储的时间戳时,才执行写入,确保数据的时序性。

    HSET your_key value new_value timestamp new_timestamp
    

高并发主要要考虑哪些问题?

  • 线程/连接池容量Tomcat workThreadsHikariCP 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

综合题 & 智力题

高考完学生投档到各个学校,应该怎样设计算法?

求两支股票在一段时间内的最长同步上升时间