对java序列化有了解吗?
Java 序列化是将对象转换为字节流的过程,以便可以将其保存到文件、通过网络传输或在内存中缓存。
JDK自带的序列化,要让一个类支持序列化,需要实现 java.io.Serializable
接口。
缺点:
- 生成的字节流体积大。可读性查。
- Java 特有的,其他语言无法直接解析其生成的字节流。
- JDK 反序列化机制存在安全漏洞,攻击者可以通过构造恶意字节流执行任意代码。
Jackson 库序列化,广泛用于Spring框架。我项目使用jackson的objectmapper序列化。
对java反序列框架有了解吗?
使用jackson进行反序列化。
讲一下java的常用gc算法以及各自的侧重点
标记-清除:先标记不被回收的,在清除没有被标记的。会产生较多碎片。适用于老年代。
标记-整理:先标记不被回收的,将存活的对象向内存一端移动,然后清理边界外的内存。不会产生较多碎片。适用于老年代。
复制算法:将内存分为两块,每次只使用其中一块。当一块内存用满时,将存活的对象复制到另一块内存,然后清空当前内存。内存利用率较低。适用于新生代。
新生代中的对象大多数是临时对象,生命周期很短,很快就会被回收。
复制算法只需要复制少量存活对象,效率高。新生代的内存区域通常较小(如几十MB到几百MB),复制算法的内存开销(需要一半内存作为空闲区域)可以接受。
cms和g1各自优缺点和适用场景,对比一下这两个
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器。已经过时。
优点:并发收集所以低停顿。
缺点:
标记清除算法产生较多碎片。
在并发清理阶段,应用程序可能产生新的垃圾(浮动垃圾),这些垃圾只能等到下一次GC时回收。
使用场景:适用于老年代
G1,目前在使用的垃圾收集器。
- G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) ,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的,产生的内存碎片少
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
缺点:
- 实现复杂:G1的实现较为复杂,可能导致额外的性能开销。
使用场景:
适用于大内存和低延迟场景中,整堆收集。
讲一下有哪些场景打破双亲委派机制
Java 的JDBC允许第三方提供实现类。
DriverManager位于
rt.jar` 中,由启动类加载器加载,而数据库驱动类由第三方提供,由应用类加载器加载。
启动类加载器无法直接加载应用类路径下的类,因此需要通过线程上下文类加载器(Thread Context ClassLoader
)打破双亲委派机制。
动态代理:动态代理(如 JDK 动态代理、CGLIB)需要动态生成代理类并加载。动态代理生成的类通常需要由自定义类加载器加载,而不委托父类加载器。
对springboot自动化配置有了解吗?
@SpringBootApplication
注解中包含了 @EnableAutoConfiguration
,用通过 SpringFactoriesLoader 最终加载META-INF/spring.factories
中的自动配置类实现自动装配.
慢sql如何排查?可能有哪些原因导致慢sql
开启慢查询日志,
缺少索引,索引失效。
mysql的索引是什么?b+树,相较于hash有什么优点
范围查询+有序
讲一下tcp报文格式
调表的时间复杂度空间复杂度
O(logn), 跳表通过多层链表存储数据,每层的节点数逐渐减少,空间复杂度为线性级别。
n+n/2+n/4+⋯≈2n,因此空间复杂度为 O(n)。
如果一个进程创建了过多的线程(比如几万个),可能会导致以下问题:
- 每个线程都需要分配独立的栈空间。如果创建了几万个线程,内存消耗会非常巨大,可能导致内存耗尽(Out of Memory)。
- 线程切换时,操作系统需要保存当前线程的状态(如寄存器、程序计数器等)并恢复下一个线程的状态。过多的线程会导致频繁的上下文切换,消耗大量 CPU 资源,降低系统整体性能。
- 操作系统对单个进程可以创建的线程数有限制(如 Linux 的
ulimit -u
或/proc/sys/kernel/threads-max
)。超过限制会导致线程创建失败。
http post如何实现幂等性?
服务端记录多个历史版本,请求的时候携带版本号。
Mysql事务出现死锁怎么处理?除了顺序分配资源以外?
MySQL 内置了死锁检测机制,当检测到死锁时,会自动选择一个事务作为“牺牲者”,回滚该事务以解除死锁。
了解过缓存行或者伪共享吗?
- 缓存行是 CPU 缓存中的最小数据单元,通常大小为 64 字节(具体大小取决于 CPU 架构)。当 CPU 从内存中读取数据时,会一次性加载一个缓存行,而不是单个字节或字。
- 伪共享是指多个线程同时修改位于同一个缓存行中的不同变量,导致缓存行在 CPU 核心之间频繁无效化,从而降低性能。
对应Redis的热点key有什么解决方案?
推荐:
Key 分片
- 思路:将热点 Key 拆分为多个子 Key,分散到不同的 Redis 节点。
实现例如,将
hot_key
拆分为hot_key_1
、hot_key_2
、hot_key_3
,分别存储在不同的节点优点:分散热点 Key 的访问压力。
- 缺点:数据一致性需要额外处理。
本地缓存
- 思路:使用本地缓存工具(如 Guava Cache、Caffeine)缓存热点 Key 的值。
- 优点:减少对 Redis 的访问压力。
- 缺点:本地缓存占用应用服务器的内存。
保底:
缓存降级
- 在 Redis 无法承受压力时,降级到其他存储或直接返回默认值。
- 优点:保证系统的可用性。
- 缺点:数据可能不准确。
限流
- 对热点 Key 的访问进行限流,避免单个节点过载。
- 实现:使用限流工具(如 Redis 的
INCR
命令或分布式限流框架)限制访问频率。超出限流阈值时,直接返回错误或默认值。 - 优点:保护 Redis 节点不被压垮。
- 缺点:可能影响用户体验。
新技术:
使用分布式缓存
将热点 Key 存储到多个分布式缓存节点。通过哈希算法决定访问哪个节点。
优点:分散热点 Key 的访问压力。
- 缺点:需要引入新的技术栈,增加运维成本。
如何解决缓存击穿问题?
解决方案
逻辑过期:只允许一个线程去重建缓存,其他线程返回脏数据。
互斥锁:只允许一个线程去获得锁,其他线程等待重建完成。
预防方案:
提前预热缓存。
保底方案:
在缓存失效时,通过限流或熔断机制控制请求量,避免数据库被压垮。
为什么数据库采用b+树,不是b树?
- 树的高度低,查询效率高,查询速度稳定:因为非叶子节点,只存索引,不存数据,树的高度更低。
- 适合范围查找和排序操作:所有数据都存储在叶子节点,且叶子节点通过指针连接成链表,便于范围查询。
- 更高效的插入和删除:插入和删除操作主要集中在叶子节点,且叶子节点通过指针连接,调整更简单。而b树插入和删除可能涉及内部节点,调整更复杂。
缓存一致性
什么是TCP粘包和拆包?如何实现?
TCP粘包:
发生在应用层。用户发送的多条数据包,比如hello和world,使用一个TCP来发送,接收方无法区分两条消息的边界。
使用固定长度的数据包、使用特殊字符分割、使用标志位+长度。
TCP拆包:
发生在传输层。用户发生的一条数据包,由于MSS的限制,被用多条TCP发生。
解决方案:
- 使用HTTP,使用消息头加消息体(长度)。
- Netty 供了多种内置的解码器来解决粘包和拆包问题。
字节的文化
创业、多元、务实、坦诚、极致、共同成长
说一下你对redis的理解
高性能的键值对存储系统,常用作缓存中间件。
基于内存,NoSQL,优化过的数据结构,支持持久化、集群。
MySQL最多可以存多少数据?为什么是2000W而不是2亿?
从理论上讲,MySQL 单表可以存储 数十亿甚至上百亿 条数据。
但是维护成本显著增高:单表数据量超过 2000W 时,B+ 树的深度可能从 3 层增加到 4 层,查询时需要多一次磁盘 I/O,性能明显下降。当单表数据量超过 2000W 时,通常建议使用 分区表 或 分表 来优化性能。
UV和DAU怎么统计?
UV 是指在 一定时间范围内,访问网站或应用的 独立用户数量。同一个用户无论访问多少次,都只计为 1 个 UV。
DAU 是指在 一天内,访问网站或应用的 独立用户数量。同一个用户无论访问多少次,都只计为 1 个 DAU。
Redis的HyperLogLog , 占用内存极小,且 计算速度快,非常适合处理大规模数据的去重统计。
HyperLogLog 是 Redis 提供的一种 基数统计 算法,用于高效地估计一个集合中 唯一元素的数量(即基数)。它的特点是 占用内存极小,且 计算速度快,非常适合处理大规模数据的去重统计。
优点
- 内存占用低:每个 HyperLogLog 仅占用 12KB 内存。
- 计算速度快:时间复杂度为 O(1),适合实时统计。
- 支持大规模数据:可以统计多达 2^64 个唯一元素。
缺点
- 近似统计:结果存在一定误差,不适合需要精确统计的场景。
- 不支持元素查询:无法判断某个元素是否已存在于 HyperLogLog 中。
MySQL的主从同步是如何实现的
使用binLog日志。
static关键字
volatile 和 AtomicInteger的区别
AtomicIntege原子类使用了volatile关键字修饰,保证了可见性,但是不能保证原子性,如 i++
是非原子的。
AtomicIntege原子类提供了一些复合操作函数,使用CAS(底层是unsafe类的nativ方法)保证原子性。
进程间可以资源共享吗?
其实就是进程之间通信。
分布式锁
实现思路:
- 利用set nx ex获取锁,并设置过期时间,保存线程标示
- 释放锁时先判断线程标示是否与自己一致,一致则删除锁。(Lua脚本保持原子性)
Redis分布式锁特性:
- 利用set nx满足互斥性
- 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
- 利用Redis集群保证高可用和高并发特性
如果要实现可重入,可以把string改成hash结构。
redis.call('hset', key, 'threadId', threadId)
redis.call('hset', key, 'count', 1)
redis.call('expire', key, expireTime)
但是还是有缺点,没有超市续约功能。
Redisson分布式锁原理
- 可重入:利用hash结构记录线程id和重入次数
- 可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制,不浪费cpu资源
- 超时续约:利用watchD0g,每隔一段时间(releaseTime/3),重置超时时间,也就是重新expire。
缺点:redis单个节点崩溃就会失效,
解决方案:联锁, 通过在多个 Redis 实例(通常是 5 个)上同时获取锁,来确保即使某个 Redis 实例宕机,锁依然安全有效。
单例模式有哪些
⼀个单例类在任何情况下都只存在⼀个实例。
- 构造⽅法必须是私有的。
- 由⾃⼰创建⼀个私有的静态变量存储实例。
- 对外提供⼀个静态公有⽅法获取实例。
饿汉式:在类加载时就创建实例,线程安全。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
懒汉式,在第一次调用 getInstance
时创建实例。线程不安全,需要额外处理多线程问题。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
线程安全的double-check的懒汉式
//线程安全的单例模式
public class Singleton {
// 使用 volatile 关键字确保 instance 的可见性
private static volatile Singleton instance;
// 私有构造函数,防止外部实例化
private Singleton() {
// 初始化代码
}
// 获取单例实例的静态方法
public static Singleton getInstance() {
// 第一次检查,避免不必要的同步
if (instance == null) {
// 加锁,确保线程安全
synchronized (Singleton.class) {
// 第二次检查,防止多个线程同时通过第一次检查
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
MySQL查询语句执行顺序
FROM → 2. WHERE → 3. GROUP BY → 4. HAVING → 5. SELECT → 6. DISTINCT → 7. ORDER BY → 8. LIMIT。
HTTP1.1,2.0,3.0
HTTP1.1
- 队头阻塞:http会先请求html网页,然后请求css,图片等东西,如果css不收到,后面的请求也不会发出。
- 基于文本协议,明文传输 ,不安全
- 不支持服务端推送
HTTP2
- 多路复用解决队头堵塞:多个请求和响应可以在同一个 TCP 连接上并行传输,互不干扰。但是HTTP/2 仍然依赖 TCP 作为传输层协议,而 TCP 是面向连接的、可靠的协议。如果 TCP 数据包在传输过程中丢失,TCP 会重传丢失的包,导致后续数据包被阻塞(即使它们已经到达接收端)。
- 二进制协议,性能高,一定程度加密。
- 头部压缩(HPACK):减少了 HTTP 头部的冗余数据。
- 服务器推送(Server Push):服务器可以主动向客户端推送资源,减少请求次数。
HTTP/3
- 基于 QUIC 传输层协议,运行在 UDP 之上,完全解决了 TCP 的队头阻塞问题。
- UDP 本身不提供可靠性,因此可以在应用层(如 QUIC)实现自定义的可靠性机制
- 默认加密(使用 TLS 1.3)。
读写穿透
读/写穿透模式 通过将缓存与数据库整合为一个服务,由服务维护一致性,简化了调用者的操作。实现该模式的关键在于:
- 读操作:先查缓存,未命中时加载数据库数据并写入缓存。
- 写操作:先更新数据库,再更新缓存。
- 服务封装:提供统一的接口,隐藏缓存与数据库的细节。
通过这种模式,可以显著提升系统性能,同时保证数据的一致性。希望以上内容能帮助你实现读/写穿透模式!
异步缓存写入
- 读操作 主要依赖缓存,缓存命中时直接返回数据,缓存未命中时从数据库加载数据并写入缓存。
- 写操作 只操作缓存,由后台线程异步将缓存数据持久化到数据库,缓存和数据库之间可能存在短暂的不一致,但最终会保持一致。
Redis内存是有上限的,如何进行淘汰?
redis.conf` 中设置 `maxmemory-policy
默认策略:当内存不足时,新写入操作会返回错误,不会淘汰任何数据。
allkeys-lru
:从所有键中淘汰最近最少使用(Least Recently Used, LRU)的键。
volatile-lru 从设置了过期时间的键中淘汰最近最少使用的键。
volatile-ttl
:设置了过期时间的键中淘汰剩余生存时间(Time To Live, TTL)最短的键。
MySQl执行查询语句后的日志如何记录的
通用查询日志、慢查询日志、错误查询日志。
数据库错误恢复如何实现的
RedoLog(已提交但未写入磁盘),UndoLog(回滚未提交的事务)
检查点:
- 数据库定期将内存中已修改的数据页写入磁盘。
- 写入完成后,记录一个检查点,表示在此之前的日志不再需要重放。
什么是分布式事务
分布式事务是指跨越多个分布式系统或数据库的事务操作,需要保证这些操作要么全部成功,要么全部失败,以满足事务的 ACID 特性(原子性、一致性、隔离性、持久性)。
两阶段提交(2PC,Two-Phase Commit)
- 阶段一(准备阶段)
- 事务协调者向所有参与者发送准备请求。
- 参与者执行事务操作,但不提交,返回准备结果(成功或失败)。
- 阶段二(提交阶段)
- 如果所有参与者都准备成功,协调者发送提交请求,参与者提交事务。
- 如果有参与者准备失败,协调者发送回滚请求,参与者回滚事务。
- 优点:强一致性。
- 缺点:同步阻塞、性能低、单点故障。
线程之间如何通信,举例子
为什么Redis如此快
- 基于内存
- 优化过的数据结构
- 单线程模型,没有线程切换,也不会存在锁竞争
- 非阻塞IO,比如epoll模式
- 网络协议简单高效
http和websorcket区别
- HTTP: HTTP 是一种请求-响应协议。客户端(如浏览器)向服务器发送请求,服务器处理请求后返回响应。
- WebSocket: WebSocket 是一种全双工通信协议。客户端和服务器之间建立一个持久的连接,双方可以随时发送数据,而不需要等待请求。WebSocket 连接一旦建立,就可以持续通信,直到其中一方主动关闭连接。
- HTTP: 每次请求和响应都需要携带完整的 HTTP 头部信息,包括方法、路径、状态码、Cookie 等。
- WebSocket: 在连接建立后,数据传输时只需要携带少量的控制信息,头部开销较小。
- HTTP: HTTP 本身不支持实时通信。适用于传统的 Web 应用,如网页浏览、文件下载。
- WebSocket: WebSocket 是专门为实时通信设计的。它允许服务器主动向客户端推送数据,非常适合需要实时交互的应用场景,如在线聊天、实时游戏、股票行情等。
对于Java的OOM(Out of Memory)如何解决
堆内存不足:对象过多,堆内存耗尽。
栈内存不足:线程栈内存耗尽(通常是递归调用过深或线程过多)。
元空间(Metaspace)不足:加载的类过多,元空间内存耗尽。
解决方案:对于大内存应用,使用 G1
或 ZGC
垃圾回收器。增加栈和堆空间。及时释放资源
DNS为什么使用UDP而不是TCP
- 低开销 UDP无需建立和断开连接,减少了通信开销,适合DNS查询这种短小的请求。
- 速度快 UDP没有握手过程,响应更快,适合对实时性要求高的DNS查询。
- 小数据包 DNS查询和响应通常很小,UDP的512字节限制在大多数情况下足够使用。
尽管UDP是首选,但在以下情况下DNS会使用TCP:
- 响应数据超过512字节(启用EDNS0时)。
- 区域传输(AXFR/IXFR)需要可靠传输。
- 某些DNSSEC查询需要TCP的可靠性。
TIME_WAIT 堆积是什么原因如何解决
- 短连接过多:如果客户端频繁创建和关闭 TCP 连接(如 HTTP 短连接),会导致大量连接进入 TIME_WAIT 状态。
- 主动关闭连接的一方:TCP 连接中,主动关闭连接的一方会进入 TIME_WAIT 状态。如果服务器或客户端频繁主动关闭连接,会导致 TIME_WAIT 堆积。
使用长连接:尽量避免频繁创建和关闭连接,改用长连接(如 HTTP 的 Keep-Alive)
确保连接关闭由合适的一方发起。例如,尽量让客户端主动关闭连接,减少服务器端的TIME_WAIT 状态。
如果是服务器端问题,可以通过增加服务器或使用负载均衡分散连接压力。
DNS有什么安全问题,什么是DNSSEC
DNS欺骗、劫持,重定向到攻击者的钓鱼网站。
DNSSEC(DNS Security Extensions) 是一组用于增强 DNS 安全性的扩展协议。它通过数字签名和公钥加密技术,解决了 DNS 的安全问题。
项目频繁出现fullgc如何排查?
看看堆内存设置是否过小。
避免频繁创建和销毁对象,尽量复用对象。
检查是否有大对象频繁创建。
Redis 某个节点失效,大量的请求打过来,怎么办。
使用Redis Sentinel 或 Redis Cluster 实现高可用。
手动故障转移。
如果 Redis 不可用,可以暂时降级到本地缓存或数据库,确保服务基本可用。
MyBatis的缓存机制
一级缓存(Local Cache)
特点
- 作用范围:默认开启,作用范围是 SqlSession 级别。
- 生命周期:与 SqlSession 绑定,当 SqlSession 关闭或清空时,缓存也会被清空。
- 共享性:一级缓存是 SqlSession 私有的,不同 SqlSession 之间无法共享。
工作原理
- 在同一个 SqlSession 中,如果多次执行相同的 SQL 查询(相同的 SQL 和参数),MyBatis 会从一级缓存中直接返回结果,而不会再次查询数据库。
- 如果执行了 INSERT、UPDATE、DELETE 操作,MyBatis 会自动清空一级缓存,以保证数据一致性。
二级缓存(Global Cache)
- 作用范围:默认关闭,需要手动开启,作用范围是 Mapper 级别。
- 生命周期:与应用程序的生命周期一致,只有当应用程序关闭时,缓存才会被清空。
- 共享性:二级缓存是跨 SqlSession 的,多个 SqlSession 可以共享同一个二级缓存。
工作原理
- 当 SqlSession 提交或关闭时,MyBatis 会将一级缓存中的数据存入二级缓存。
- 后续的 SqlSession 查询时,如果二级缓存中存在数据,则直接从缓存中返回结果,而不会访问数据库。
- 如果执行了 INSERT、UPDATE、DELETE 操作,MyBatis 会自动清空二级缓存,以保证数据一致性。
二者区别
- 一级缓存:适合单次会话中重复查询的场景。
- 二级缓存:适合跨会话共享数据且数据更新频率较低的场景。
- 二者都可能出现脏读。
Spring的bean为什么默认是单例?
Spring 容器只会创建一个 Bean 实例,并在整个应用程序中共享。这避免了频繁创建和销毁对象的开销,提高了性能。
在大多数场景中,Bean 是无状态的,线程安全。
常见linux命令
- ls:列出目录内容
- cd: 切换目录
- ping:测试链接情况
- cp:复制文件 -r递归复制
- tar:压缩文件。
- mv:移动文件
- rm:删除文件 -r递归删除 -f强制删除
- vim:文本编辑器 :wq保存 i编辑模式
AOP的原理
Spring AOP
使用的是动态代理。所谓的动态代理,就是说AOP
框架不会去修改字节码,而是在内存中临时为方法生成一个AOP
对象,这个AOP
对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
动态代理原理
动态代理通过反射机制调用目标对象的方法,用于在运行时动态获取和操作类的信息。
两种实习方式:JDK
动态代理、CGLIB
动态代理。