其他问题
其他问题
MySQL Buffer Pool里都有什么?
MySQL 的 Buffer Pool(缓冲池)是 InnoDB 存储引擎中最核心的内存区域,主要用于缓存磁盘上的数据页,加速数据读写。
类型 | 说明/作用 |
---|---|
数据页(Data Page) | 存储表的实际数据行 |
索引页(Index Page) | 存储 B+ 树索引节点 |
Undo 页 | 存储回滚日志,支持事务和 MVCC |
Change Buffer 页 | 缓存二级索引的变更操作,提升插入效率 |
自适应哈希索引 | 热点数据自动生成哈希索引,加速查询 |
锁信息 | 记录锁定信息,支持并发控制 |
数据字典信息 | 缓存表结构、索引结构等元数据 |
Redis底层相关数据结构了解吗?
底层结构 | 主要用途/描述 | 典型应用类型 | 优点 | 缺点 | 适用场景/切换条件 |
---|---|---|---|---|---|
SDS | 动态字符串存储 | string、key | 安全高效,支持二进制(图片),长度可变 | 仅适合字符串 | 所有字符串、key 名称 |
ziplist | 紧凑连续内存,存小对象 | list、hash、zset | 占用内存小,遍历快 | 插入/删除慢,数据多时效率低 | 元素数量少且小(如list<8、元素<64B) |
listpack | 新一代紧凑结构,比ziplist更高效 | stream、list | 更省空间,结构简单 | 只适合小数据量 | stream entries、list小数据量 |
quicklist | ziplist/listpack链表混合体 | list | 插入/删除快,遍历快,省内存 | 结构复杂 | list主力实现,数据量大或有频繁操作 |
intset | 整数集合,内存连续 | set | 占用小,查找快 | 只支持整数,数量有限 | set全为整数且数量少(<512) |
dict | 哈希表,key-value结构 | hash、set、zset | 查找/插入/删除快,扩容灵活 | 内存占用大,哈希冲突需处理 | hash/大set/zset、key管理 |
skiplist | 多层链表,支持有序和范围查找 | zset | 查找/插入/删除O(logN),有序遍历快 | 内存占用大,结构复杂 | zset大数据量,需排序/范围查找 |
链表 | 节点指针串联,插入/删除快 | 早期list | 插入/删除快,双向遍历 | 内存碎片多,查找慢 | 早期list实现,现已被quicklist替代 |
bitmap | 位数组,布尔状态存储 | 签到、活跃统计 | 占用极小,适合大规模布尔统计 | 只适合0/1状态,操作需位运算 | 签到、活跃用户、布隆过滤器 |
HyperLogLog | 概率型基数统计 | UV(Unique Visitor)、去重 | 极省内存,统计大规模唯一数 | 只能估算,不能存具体元素 | UV、去重计数 |
- 压缩结构(ziplist、listpack):适合小数据量,省空间。
- 链表/quicklist/skiplist:适合大数据量,插入/删除/查找高效。
- dict/intset:哈希表和整数集合,分别适合大/小数据量的hash、set。
- bitmap/HyperLogLog:特殊统计场景,极省内存。
- SDS:所有字符串和key的底层实现。
可以基于UDP实现可靠通信吗?HTTP3知道吗?
虽然 UDP(User Datagram Protocol)本身是无连接、不可靠的协议(不保证数据到达、不保证顺序、不重传),但可以在应用层或传输层之上,通过额外机制实现可靠通信,常见方法有:
- 序列号:每个数据包加上序号,接收方可检测丢包、乱序。
- 确认应答(ACK):接收方收到数据后回发确认,发送方未收到ACK则重传。
- 超时重传:发送方在超时未收到ACK时重发数据包。
- 滑动窗口:支持流量控制和有序传输。
- 校验和:保证数据完整性。
- 拥塞控制:防止网络拥堵。
实际例子:
- 许多实时音视频、游戏、定制协议(如 TFTP、QUIC)都在 UDP 上实现了可靠性机制。
- 你可以把“可靠的UDP”理解为“自定义的TCP”。
HTTP/3 就是基于 UDP 实现可靠通信的典型代表。
- HTTP/3 是新一代 HTTP 协议,底层基于 QUIC 协议。
- QUIC(Quick UDP Internet Connections)是 Google 推出的基于 UDP 的传输层协议,实现了可靠传输、拥塞控制、顺序保证、加密等功能,本质上是“用UDP实现的更快的TCP+TLS”。
- HTTP/3 用 QUIC 替代了 TCP,解决了 TCP 的队头阻塞、慢启动等问题,连接建立更快,抗丢包能力更强,适合现代互联网和移动场景。
UDP 本身不可靠,但可以通过协议设计实现可靠通信。
HTTP/3 就是用 UDP + QUIC 实现可靠、高效、安全的 Web 通信。
项目是基于HTTP吗,为什么要基于HTTP来做,TCP不行吗?
对比项 | HTTP(基于TCP之上的应用层协议) | 直接用TCP(传输层协议) |
---|---|---|
协议层级 | 应用层(基于TCP) | 传输层 |
通用性/标准化 | 标准化,全球通用,跨平台、跨语言 | 需自定义协议,通用性差 |
开发难度 | 易用,框架/工具丰富,开发效率高 | 需自行处理数据格式、分包、粘包、重传等,开发难度大 |
兼容性 | 浏览器、APP、API、第三方服务等原生支持 | 需自定义客户端/服务端,兼容性差 |
网络穿透 | 80/443端口易穿透防火墙/代理 | 其他端口常被封锁,穿透难 |
安全性 | 支持HTTPS(加密、认证),安全性高 | 需自定义加密/认证,安全性难保障 |
生态/工具 | 丰富(Postman、curl、API网关、负载均衡等) | 工具少,需自研 |
适用场景 | Web开发、API、微服务、移动端、第三方集成等 | 游戏、物联网、金融等高性能/定制场景 |
典型应用 | 网站、RESTful API、微服务、Web应用等 | 游戏服务器、实时通信、专用协议等 |
- HTTP:开发效率高、通用性强、易于集成和维护,是Web和API开发的首选。
- TCP:更底层,适合对性能、实时性有极高要求且能自定义协议的特殊场景。
- 绝大多数项目选HTTP,是因为它标准化、易用、安全、兼容性好,极大提升开发效率和系统可维护性。
如果要在分布式中保持强一致性和最终一致性分别有什么做法?
一、强一致性(Strong Consistency)
定义:
所有节点对同一份数据的读写顺序一致,任何时刻读到的数据都是最新的,像单机一样。
常见实现方式:
方案/协议 | 简要说明 | 典型应用场景 |
---|---|---|
两段式提交(2PC) | 事务协调者分两阶段通知所有参与者提交或回滚,保证所有节点要么都成功要么都失败 | 分布式数据库、分布式事务 |
三段式提交(3PC) | 在2PC基础上增加预提交阶段,进一步降低阻塞风险 | 理论为主,实际较少用 |
Paxos/Raft等一致性协议 | 多节点投票达成一致后再提交,保证分布式系统状态同步 | 分布式一致性存储、分布式锁 |
分布式锁 | 通过ZooKeeper、Redis等实现全局锁,保证同一时刻只有一个节点能操作关键资源 | 订单扣减、唯一性约束 |
单主写入 | 只允许一个主节点写入,其他节点只读,保证写入顺序一致 | 主从数据库、分布式缓存 |
二、最终一致性(Eventual Consistency)
定义:
系统不要求实时一致,只要经过一段时间,所有节点最终能达到一致状态。
常见实现方式:
方案/协议 | 简要说明 | 典型应用场景 |
---|---|---|
消息队列+异步补偿 | 业务操作先写本地,异步通过消息队列通知其他系统,失败后可重试或补偿 | 电商订单、库存扣减 |
TCC(Try-Confirm-Cancel) | 业务分为三步:预留资源、确认提交、取消补偿,允许部分失败后补偿 | 金融、支付、库存等 |
Saga模式 | 将大事务拆分为多个本地事务,每步有补偿操作,保证最终一致性 | 微服务长事务 |
本地消息表 | 业务操作和消息写入同一数据库事务,消息异步投递,保证消息可靠送达 | 订单、支付等 |
定时任务/对账 | 定期扫描、对账、补偿,修正不一致的数据 | 账务、积分、库存等 |
总结
一致性类型 | 典型方案/协议 | 适用场景/特点 |
---|---|---|
强一致性 | 2PC、3PC、Paxos、Raft、分布式锁 | 事务性强、金融、订单、核心数据 |
最终一致性 | 消息队列、TCC、Saga、本地消息表、定时补偿 | 高可用、可容忍短暂不一致、异步业务场景 |
Redis的事务了解吗,Redis的事务和MySQL的事务最大的区别在什么地方?
1. Redis 的事务
- 基本命令:MULTI(开启事务)、EXEC(提交事务)、DISCARD(放弃事务)、WATCH(乐观锁)。
- 执行方式:
- MULTI 后输入的一系列命令会被依次入队,等 EXEC 时一次性、顺序性地执行。
- 事务中的命令不会中途打断,但也不会回滚。
- 特性:
- 原子性:事务内的命令要么全部执行,要么都不执行(如果用 DISCARD 放弃)。
- 不保证隔离性:事务执行前后,其他客户端可以修改数据(除非用 WATCH 实现乐观锁)。
- 无回滚:事务中某条命令出错(如语法错),整个事务会被丢弃;但如果是运行时错误(如类型错误),其他命令仍会执行,不会回滚。
- 不支持持久锁定:没有行级锁、表级锁。
2. MySQL 的事务
- 基本命令:BEGIN/START TRANSACTION、COMMIT、ROLLBACK。
- 执行方式:
- 事务内的 SQL 语句在提交前对外不可见,提交后才生效。
- 特性(ACID):
- 原子性(Atomicity):要么全部成功,要么全部失败。
- 一致性(Consistency):事务前后数据完整性不被破坏。
- 隔离性(Isolation):并发事务互不干扰(支持多种隔离级别)。
- 持久性(Durability):提交后数据永久保存。
3. 最大区别
对比项 | Redis 事务 | MySQL 事务 |
---|---|---|
原子性 | 只保证命令队列的原子性,不保证单条命令的原子性 | 保证整个事务的原子性 |
隔离性 | 无法保证,除非用 WATCH 实现乐观锁 | 支持多种隔离级别,强隔离性 |
回滚/错误处理 | 不支持回滚,命令出错不会自动撤销已执行命令 | 支持回滚,出错可撤销整个事务 |
持久性 | 依赖持久化配置(RDB/AOF),不如MySQL强 | 强持久性,写入磁盘保证数据不丢失 |
锁机制 | 无锁,乐观锁(WATCH) | 支持行锁、表锁等多种锁机制 |
适用场景 | 高性能、简单原子操作、缓存、计数等 | 复杂业务逻辑、强一致性要求的数据操作 |
一句话总结:
Redis 事务不支持回滚和强隔离,主要保证命令的批量顺序执行;MySQL 事务支持完整的ACID特性,能保证强一致性和回滚,是“真正的数据库事务”。
毒性反转是什么?是哪一层的概念?
毒性反转(Poison Reverse)是一种用于防止路由环路的机制,常见于距离矢量路由协议(如RIP)。
- 当路由器A通过路由器B到达某个网络X时,A会告诉B:“我到X的距离是无穷大(不可达)”,即“把这条路‘毒死’”。
- 这样可以防止B再把到X的路由信息传回A,避免A、B之间形成路由环路。
- 属于网络层(第三层,Network Layer的路由协议机制。
- 主要用于RIP等距离矢量型路由协议。
简要记忆:
毒性反转是网络层的路由环路防止机制,常用于RIP协议。
死锁和OOM如何排查
问题类型 | 排查工具/方法 | 关键点/建议 |
---|---|---|
死锁 | jstack、jconsole、Arthas、日志分析 | 查锁等待链、加锁顺序、避免嵌套锁、可视化分析 |
OOM | jmap、jvisualvm、MAT、GC日志 | 导出堆快照、分析大对象/泄漏、优化内存参数、代码审查 |
布隆过滤器的原理?什么情况下是误判?
- 布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在集合中。
- 结构:一个很长的二进制位数组(bit array)+ 多个独立的哈希函数。
- 添加元素:将元素分别用k个哈希函数计算,得到k个位置,把这些位置的bit都置为1。
- 查询元素:同样用k个哈希函数计算k个位置,只要有一个bit为0,说明元素一定不在集合中;如果都为1,则“可能在集合中”。
- 误判(假阳性,False Positive):布隆过滤器可能会把“不在集合中的元素”误判为“在集合中”。
- 原因:不同元素经过哈希后可能落在同一组bit上,导致查询时所有bit都为1,但其实这些1是其他元素置的。
- 不会出现“假阴性”:即不会把“实际在集合中的元素”误判为“不在集合中”。
简要记忆:
布隆过滤器通过多个哈希函数和位数组判断元素是否存在,可能出现“误判存在”,但不会漏判已存在的元素。
- 如果数据量不大,直接 Redis 精确判断完全可以。
- 如果数据量非常大或者需要高性能判断,布隆过滤器更合适。
- 真实场景中常用 Redis + 布隆过滤器:布隆过滤器快速判断可能重复,再去 Redis 做精确判断,兼顾性能和精度。
JWT组成部分有哪些,怎么配合拦截器实现校验和刷新的,拦截器是怎么做的?
步骤 | 说明/作用 |
---|---|
JWT组成 | Header、Payload、Signature |
校验流程 | 拦截器获取token,校验签名和过期,放行或拒绝 |
刷新机制 | 检查token快过期时刷新,或用refresh token换新token |
拦截器实现 | preHandle中解析校验token,失败返回401/403 |
项目类型 | 推荐用法 | 原因/说明 |
---|---|---|
不用 Spring Security | 拦截器(HandlerInterceptor) | 简单好用,控制请求前置逻辑 |
用 Spring Security | 自定义过滤器(Filter) | 更标准,能控制认证与授权流程 |
- 如果你用了 Spring Security,那应该在过滤器链中处理 JWT,因为:
- 拦截器晚于过滤器执行,无法控制 Spring Security 的认证过程
- Spring Security 本身就有过滤器链机制,用来处理认证、授权、异常等
JWT由Header、Payload、Signature三部分组成。拦截器通过解析和校验JWT实现接口安全控制,并可结合刷新机制保证用户体验和安全。拦截器一般在preHandle中校验token,校验通过放行,否则拒绝访问。
拦截器和过滤器有什么区别?
对比项 | 过滤器(Filter) | 拦截器(Interceptor) |
---|---|---|
所属层级 | Servlet规范/容器 | Web框架(如Spring MVC) |
作用范围 | 所有请求(包括静态资源) | 仅Controller相关请求 |
典型用途 | 编码、日志、权限、XSS、CORS等 | 登录校验、权限、业务日志、数据预处理等 |
实现接口 | javax.servlet.Filter | org.springframework.web.servlet.HandlerInterceptor |
配置方式 | web.xml/@WebFilter注解 | Spring配置类/注解 |
执行时机 | Servlet前后 | Controller前后 |
静态资源拦截 | 可以 | 默认不拦截 |
简要记忆:
- 过滤器:Servlet层,作用广,适合通用处理。
- 拦截器:框架层,作用于业务控制,适合业务相关的请求处理。
阶段 | 类型 | 方法 / 行为 |
---|---|---|
请求到达服务器 | ||
🚦 过滤器前置处理 | Filter#doFilter() (前) | 比拦截器先执行 |
🛡 拦截器前置处理 | preHandle() | 控制请求是否继续 |
🧭 控制器方法执行 | @Controller | 业务逻辑处理 |
🛡 拦截器后置处理 | postHandle() | 控制器执行完但视图未渲染 |
🛡 拦截器完成处理 | afterCompletion() | 请求完全结束,适合释放资源 |
🚦 过滤器收尾处理 | Filter#doFilter() (后) | 最后执行 |
响应返回客户端 |
ThreadLocalMap 的 key 为什么是弱引用?
防止内存泄漏。
ThreadLocalMap 的 key 是 ThreadLocal 对象的弱引用(WeakReference),value 是强引用。
如果 key(ThreadLocal对象)没有外部强引用,GC 时会被回收,避免 ThreadLocal 对象“遗留”在 ThreadLocalMap 里,导致 value 永远无法访问和回收,造成内存泄漏。
如果 key 是强引用,即使外部不再使用 ThreadLocal 对象,ThreadLocalMap 还持有它,GC 也不会回收,value 也不会被清理,内存泄漏风险极大。
用弱引用后,ThreadLocal 对象没外部引用时,key 会被GC回收,ThreadLocalMap 里的 entry 的 key 变成 null,虽然 value 还在,但下次访问/设置/扩容时会自动清理这些“key为null”的 entry,释放 value。
设计点 | 作用/好处 |
---|---|
key用弱引用 | ThreadLocal对象无引用时能被GC回收,防止内存泄漏 |
value用强引用 | 保证数据可用,直到手动remove或key被回收 |
ThreadLocalMap 的 key 用弱引用,是为了防止 ThreadLocal 对象失去外部引用后无法被GC,避免内存泄漏。ThreadLocalMap 的 key 用弱引用能缓解但不能彻底避免内存泄漏,正确做法是用完 ThreadLocal 后及时 remove()。
MySQL锁的原理
1. MySQL 常见锁类型
锁类型 | 说明/作用 | 典型应用场景 |
---|---|---|
全局锁 | 锁住整个数据库实例 | 备份、维护等 |
表级锁 | 锁住整张表 | MyISAM、DDL操作 |
行级锁 | 锁住单行数据 | InnoDB、事务操作 |
意向锁 | 标记表中某些行将被加锁,辅助行锁管理 | InnoDB |
间隙锁 | 锁定索引区间,防止幻读 | InnoDB、可重复读隔离级别 |
临键锁 | 行锁+间隙锁的组合,防止插入/幻读 | InnoDB |
2. 不同存储引擎的锁实现
MyISAM(表级锁)
- 只支持表级锁(读锁、写锁),不支持行级锁。
- 读写互斥,写锁独占,适合读多写少场景。
InnoDB(行级锁、表级锁、意向锁、间隙锁)
- 行级锁:通过索引实现,锁定某一行数据,支持高并发。
- 表级锁:如
LOCK TABLES
命令。 - 意向锁:事务加行锁前,先加意向锁,标记本事务要对哪些行加锁,便于表锁和行锁兼容。
- 间隙锁/临键锁:防止幻读,锁定索引区间,保证可重复读。
3. 行级锁的实现原理(InnoDB)
- 基于索引实现:InnoDB 的行锁是加在索引上的,而不是加在物理行上。
- 加锁方式:
- 共享锁(S锁):允许多个事务读同一行,不能写。
- 排他锁(X锁):允许事务修改/删除一行,其他事务不能读写。
- 加锁粒度:
- 精确到索引记录,非索引列加锁会退化为表锁。
- 锁的存储:
- InnoDB 通过锁信息结构在内存中维护锁状态,挂在事务对象上。
4. 间隙锁/临键锁的实现原理
- 间隙锁(Gap Lock):锁定一个区间,防止其他事务在该区间插入新记录,解决幻读问题。
- 临键锁(Next-Key Lock):锁定当前索引记录+区间,防止并发插入/幻读。
- 只有在 **可重复读(REPEATABLE READ)**隔离级别下才会自动加间隙锁/临键锁。
5. 意向锁的实现原理
- 表级的标记锁,用于标记事务将要对表中哪些行加锁。
- 便于表锁和行锁的兼容与冲突检测,提高加锁效率。
6. 死锁检测与处理
- InnoDB 支持自动死锁检测,发现死锁会主动回滚部分事务,释放锁资源。
总结
锁类型 | 实现方式/原理 | 适用场景/优缺点 |
---|---|---|
表级锁 | MyISAM/DDL,整表加锁 | 实现简单,粒度大,冲突多,适合读多写少 |
行级锁 | InnoDB,基于索引加锁 | 粒度小,支持高并发,开销大,死锁风险 |
意向锁 | 表级标记,辅助行锁管理 | 提高加锁效率,便于表锁与行锁兼容 |
间隙锁/临键锁 | 锁定索引区间,防止幻读 | 解决幻读,影响并发性能,只有可重复读隔离级别下用 |
MySQL 锁分为表级锁、行级锁、意向锁、间隙锁等。InnoDB 的行锁是基于索引实现的,支持高并发和事务隔离。间隙锁和临键锁用于防止幻读。意向锁用于优化表锁和行锁的兼容。InnoDB 支持自动死锁检测和回滚。
深拷贝和浅拷贝的区别?
拷贝类型 | 含义 | 影响/特点 |
---|---|---|
浅拷贝 | 只复制对象的引用,不复制引用对象本身。新对象和原对象共享内部子对象。 | 改变子对象会互相影响,顶层对象是新,底层对象是同一个 |
深拷贝 | 复制对象以及其引用的所有子对象,完全独立。新对象和原对象没有任何共享部分。 | 改变任何一方都不会影响另一方,完全独立 |
- 浅拷贝:适合对象结构简单、内部无可变引用类型,或只需复制顶层对象时。
- 深拷贝:适合对象复杂、包含可变引用类型,且需要完全独立副本时。
浅拷贝只复制引用,深拷贝连引用对象本身都复制,深拷贝更彻底,互不影响。
知道协程吗?
对比项 | 进程(Process) | 线程(Thread) | 协程(Coroutine) |
---|---|---|---|
概念 | 操作系统资源分配的最小单位 | 程序执行的最小单位,属于进程 | 用户态的轻量级线程/可暂停函数 |
是否有独立内存 | 有,进程间内存隔离 | 共享进程内存空间 | 共享线程/进程内存空间 |
创建/切换开销 | 最大 | 较大 | 极小(用户态切换) |
调度方式 | 操作系统调度 | 操作系统调度 | 程序/用户态调度 |
通信方式 | 进程间通信(IPC) | 共享内存、同步机制 | 共享内存、消息传递 |
并发性 | 真并发(多核可并行) | 真并发(多核可并行) | 单线程内并发(伪并发) |
适用场景 | 高隔离、高可靠、独立服务 | 高并发、并行计算 | 高并发、IO密集、异步编程 |
数量 | 少(资源消耗大) | 多(受限于系统资源) | 可成千上万(极轻量) |
典型语言/支持 | 所有操作系统 | 所有主流语言/系统 | Python、Go、JS等原生支持 |
- 进程:最重,资源隔离最强,开销最大,适合独立服务。
- 线程:比进程轻,能并发执行,资源共享,适合并行计算。
- 协程:最轻,用户态切换,适合高并发、IO密集,单线程内实现并发。
进程最重,线程较轻,协程最轻;进程/线程由操作系统调度,协程由程序调度,协程适合高并发和异步场景。
Spring中事务嵌套事务是怎么处理的?
- Spring 通过**事务传播行为(Propagation)**来控制“事务中套事务”的行为。
- 常见传播属性:
REQUIRED
(默认):如果当前有事务,就加入当前事务;没有就新建一个事务。REQUIRES_NEW
:每次都新建一个新事务,原事务挂起。NESTED
:嵌套事务,内层回滚不影响外层(需底层数据库支持)。- 其他还有 SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER 等。
如何保证事务不会出问题?
1)同一个类内直接调用,事务可能不会生效!
- Spring 的事务是基于 AOP 代理实现的,只有通过代理对象调用,事务才会生效。
- 如果在同一个类里直接调用另一个
@Transactional
方法,实际上不会经过代理,内层事务注解失效,事务传播属性不会生效。比如:
// outer()直接调用inner(),inner()的事务注解不会生效,只有outer()的事务生效。
@Service
public class MyService {
@Transactional
public void outer() {
inner(); // 直接调用,事务注解无效
}
@Transactional
public void inner() {
// 这里的事务注解不会生效
}
}
// 只有通过Spring容器管理的代理对象去调用@Transactional方法,Spring才能拦截到方法调用,事务才会生效。
// 通常做法是:把内层事务方法放到另一个Spring Bean里,通过注入调用。
@Service
public class InnerService {
@Transactional
public void inner() {
// 事务生效
}
}
@Service
public class OuterService {
@Autowired
private InnerService innerService;
@Transactional
public void outer() {
innerService.inner(); // 通过代理对象调用,事务生效
}
}
解决办法:
- 让内层事务方法在不同的类,通过 Spring 容器注入调用,确保走代理。
2)合理设置传播属性
- 如果希望内外方法共用一个事务(默认),用
REQUIRED
。 - 如果希望内外方法各自独立,互不影响,用
REQUIRES_NEW
。 - 如果需要嵌套事务(部分回滚),用
NESTED
(需数据库支持)。
总结
场景/方式 | 事务传播属性 | 事务行为说明 |
---|---|---|
默认(REQUIRED) | REQUIRED | 内外方法共用一个事务,任何一个异常都整体回滚 |
新事务(REQUIRES_NEW) | REQUIRES_NEW | 内外方法各自独立,内层异常只回滚自己,外层不受影响 |
嵌套事务(NESTED) | NESTED | 内层异常只回滚自己,外层可选择是否回滚(需数据库支持) |
同类内直接调用 | 任意 | 事务传播属性失效,内层事务注解无效,需通过代理对象调用 |
Spring 事务中套事务时,要通过事务传播属性(如REQUIRED、REQUIRES_NEW、NESTED)控制事务边界。注意同类内直接调用不会生效,需通过代理对象调用。合理设置传播属性和调用方式,才能保证事务不会出现问题。
IOC介绍 循环依赖如何解决?交给spring会不会出现内存溢出问题?
1. IOC 介绍
- IOC(Inversion of Control,控制反转):把对象的创建和依赖关系的维护交给容器(如Spring),而不是在代码中手动new对象。
- 核心思想:对象不再自己管理依赖,而是由容器统一注入(依赖注入,DI)。
- 好处:解耦、易扩展、易测试、便于管理对象生命周期。
2. Spring 循环依赖如何解决?
循环依赖:A依赖B,B又依赖A,构成闭环。
Spring的三级缓存机制可以解决单例Bean的构造器循环依赖(即构造方法无参或只依赖属性注入的情况):
- singletonObjects:一级缓存,存放完全初始化好的单例Bean。
- earlySingletonObjects:二级缓存,存放早期暴露的Bean(未完成依赖注入)。
- singletonFactories:三级缓存,存放Bean工厂对象(ObjectFactory),用于创建早期Bean引用。
解决流程:
- 创建A时,发现需要B,先把A的“半成品”放到三级缓存。
- 创建B时,发现需要A,从三级缓存拿到A的“半成品”引用,完成依赖注入。
- 最终A、B都能被正确创建。
注意:只能解决单例、属性注入的循环依赖,构造器注入的循环依赖无法解决,会抛出异常。
3. 交给Spring会不会出现内存溢出问题?
- 正常情况下不会。Spring的三级缓存机制会在Bean创建完成后清理缓存,避免内存泄漏。
- 但如果循环依赖链过长、Bean数量极大,或有自定义Bean生命周期管理不当,可能导致内存占用升高,极端情况下有OOM风险。
- 常见OOM原因:
- 循环依赖链过长,Bean未及时释放。
- Bean作用域为prototype(原型),Spring不管理其生命周期,容易内存泄漏。
- 代码中有静态变量、线程等持有Bean引用,导致GC无法回收。
IOC是控制反转,Spring通过依赖注入管理对象和依赖。Spring用三级缓存机制解决单例Bean的属性注入循环依赖,不能解决构造器注入的循环依赖。正常情况下不会导致内存溢出,但如果循环依赖链过长或Bean管理不当,极端情况下可能OOM。
ICMP是什么知道吗?
是一种网络层协议,用于在主机、路由器之间传递控制消息和差错信息。
AtomicXXX类底层如何实现的?
以 Java 的 AtomicInteger
(atomicxxx)为例,底层实现主要依赖于CAS(Compare And Swap,比较并交换)原语和CPU指令,实现无锁的原子操作。
CAS(Compare And Swap)原理
CAS 是一种原子操作指令,流程如下:
- 读取变量的当前值(假设为A)。
- 比较当前值是否等于期望值(A)。
- 如果相等,则将变量值更新为新值(B);否则不做任何操作。
- 整个过程是原子的,不会被线程切换中断。
伪代码:
if (value == expected) { value = newValue; return true; } else { return false; }
Java AtomicInteger 的底层实现
AtomicInteger
内部用Unsafe
类的compareAndSwapInt
方法实现原子加法等操作。该方法最终会调用CPU的原子指令(如 x86 的
LOCK CMPXCHG
)。以
incrementAndGet()
为例,底层是一个自旋CAS循环:public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
如果CAS失败(变量被其他线程修改),会不断重试,直到成功。
atomicxxx底层通过CAS原语和CPU原子指令实现无锁原子操作,保证并发安全。
OOM这种可以被catch吗,哪些可以?
- 可以被catch,因为 OOM(java.lang.OutOfMemoryError)是 Error 的子类,理论上可以用 catch (Throwable t) 或 catch (Error e) 或 catch (OutOfMemoryError e) 捕获。
- 但不建议捕获和处理 OOM,因为 OOM 通常表示JVM已经无法分配内存,系统处于极不稳定状态,继续运行可能导致更多不可预期的问题(如线程无法创建、对象无法分配、GC失效等)。
动态代理的底层原理?
动态代理通过运行时生成代理类,JDK代理基于接口+反射,CGLIB代理基于继承+字节码增强,方法调用最终由代理逻辑统一处理。
多线程中的异常如何处理?
1. 普通线程的异常处理
- 线程run()方法内的异常不会抛到主线程,如果不捕获,异常会导致该线程终止,但不会影响其他线程。
- 最佳实践:在run()方法内部用try-catch捕获并处理异常,避免线程意外终止。
示例:
new Thread(() -> {
try {
// 业务代码
} catch (Exception e) {
// 记录日志、报警、补偿等
}
}).start();
2. 线程池中的异常处理
- execute()提交的任务:如果任务抛出异常,异常会被吞掉,不会抛到主线程,也不会有任何提示。
- submit()提交的任务:异常会被封装在Future中,只有调用get()时才会抛出ExecutionException。
示例:
// execute方式
executor.execute(() -> {
throw new RuntimeException("error"); // 异常被吞掉
});
// submit方式
Future<?> future = executor.submit(() -> {
throw new RuntimeException("error");
});
try {
future.get(); // 这里会抛出异常
} catch (ExecutionException e) {
// 处理异常
}
3. 统一异常处理方式
- 自定义线程工厂,为线程设置UncaughtExceptionHandler,统一处理未捕获异常。
- 线程池可通过ThreadFactory设置异常处理器。
示例:
ThreadFactory factory = r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((thread, e) -> {
// 统一异常处理,如日志、报警
});
return t;
};
ExecutorService executor = Executors.newFixedThreadPool(2, factory);
4. 总结
场景 | 异常处理方式 | 说明/建议 |
---|---|---|
普通线程 | run()内try-catch | 避免线程意外终止,记录日志/补偿 |
线程池execute | 任务内try-catch或设置UncaughtExceptionHandler | 否则异常被吞掉,需统一处理 |
线程池submit | get()时catch ExecutionException | 不get则异常被吞掉,建议及时处理 |
多线程异常不会抛到主线程,需在线程内部catch或用UncaughtExceptionHandler统一处理,线程池submit还可通过Future.get()捕获异常。
submit和execute的区别?
对比项 | execute() | submit() |
---|---|---|
定义 | 只接受Runnable任务 | 可接受Runnable和Callable任务 |
返回值 | 无(void) | 返回Future对象,可获取结果/异常 |
异常处理 | 任务抛出异常会被吞掉,不会抛到主线程 | 任务抛出异常会封装在Future,get()时抛出ExecutionException |
获取结果 | 无法获取 | 可通过Future.get()获取返回值/异常 |
适用场景 | 只需执行任务,无需结果/异常处理 | 需要获取任务结果或处理异常 |
// execute
executor.execute(() -> {
// 只执行任务,不能获取结果
});
// submit
Future<Integer> future = executor.submit(() -> {
return 123;
});
Integer result = future.get(); // 获取结果或异常
execute只执行任务无返回值,submit能获取结果和异常,推荐用submit处理需要结果或异常的场景。
雪花算法?时钟回拨问题?
雪花算法
雪花算法(Snowflake)是分布式系统中常用的全局唯一ID生成算法,最早由Twitter提出。
经典的Snowflake 64位ID结构如下:
位数 | 含义 | 说明 |
---|---|---|
1 | 符号位 | 固定为0,正数 |
41 | 时间戳 | 距某个起始时间的毫秒数(可用69年) |
10 | 机器标识 | 数据中心ID+机器ID(支持1024台机器) |
12 | 序列号(保证同一毫秒内生成多个ID时不重复) | 同一毫秒内的自增序列(支持4096个ID) |
雪花算法用时间戳+机器ID+序列号拼成64位唯一ID,支持高并发分布式唯一ID生成,且大致递增。
时钟回拨问题解决
方案 | 适用场景 | 说明与优缺点 |
---|---|---|
等待恢复 | 对可用性要求不高 | 当发现系统时钟回拨时,程序暂停生成ID,等待系统时间恢复正常。 优点:实现简单,保证ID唯一且有序。 缺点:服务暂停,影响可用性,可能造成阻塞。 |
备用序列号 | 对ID递增性要求不高 | 当时钟回拨时,启用备用的序列号生成逻辑(比如增加序列号范围)以继续生成ID,保证服务不中断。 优点:服务不中断。 缺点:ID不再严格递增,可能导致顺序性下降,并发处理能力受影响。 |
时间戳补偿 | 对ID递增性要求高 | 通过逻辑维护一个补偿时间戳,人工或自动调节时间戳,使ID仍保持递增顺序。 优点:保持ID的递增性。 缺点:实现较复杂,代码和运维成本增加。 |
多时钟源 | 高可靠性要求 | 采用多个时间源(如NTP服务器、GPS时间等),确保系统时间准确且稳定。 优点:高可靠性,减少回拨风险。 缺点:系统设计复杂,硬件和维护成本高。 |
索引下推?
索引下推(Index Condition Pushdown,简称ICP)是 MySQL 5.6+ 引入的一种优化查询性能的技术,常用于InnoDB和MyISAM存储引擎。
什么是索引下推?
- 在没有ICP之前,MySQL在用索引查找时,如果有多个条件(如
where a=1 and b=2
),只有最左前缀(如a=1)会用到索引,后续条件(如b=2)需要回表到数据行再判断。 - 有了ICP后,MySQL会在索引遍历阶段,尽量把更多的where条件“下推”到索引层过滤,减少回表次数,提升查询效率。
工作原理
- MySQL在扫描索引时,先用索引能判断的条件过滤一部分数据(如a=1)。
- 对于复合索引,能在索引中判断的其他条件(如b=2)也在索引遍历时判断。
- 只有通过所有下推条件的索引记录,才会回表读取完整数据行。
例子
假设有如下表和索引:
CREATE TABLE t (
a INT,
b INT,
c INT,
INDEX idx_ab(a, b)
);
查询语句:
SELECT * FROM t WHERE a=1 AND b=2 AND c=3;
- 没有ICP:a=1用索引,b=2和c=3要回表判断。
- 有ICP:a=1和b=2都在索引遍历时判断,只有b=2通过的才回表判断c=3。
优点
- 减少回表次数,提升查询性能,尤其是大表和复合索引场景。
- 对于只用到索引字段的查询,甚至可以做到覆盖索引,无需回表。
如何查看是否用到ICP?
- 执行
EXPLAIN
,如果Extra
列出现Using index condition
,说明用到了索引下推。
索引下推是MySQL把更多where条件下推到索引遍历阶段过滤,减少回表次数,提高查询效率的优化技术。
InnoDB和MyISAM的区别?
对比项 | InnoDB | MyISAM |
---|---|---|
存储结构 | 行存储(Row-based) | 行存储(Row-based) |
事务支持 | 支持事务(ACID) | 不支持事务(非ACID) |
锁机制 | 行级锁(Row-level) | 表级锁(Table-level) |
索引 | 支持B+树索引(主键、唯一、普通) | 支持B+树索引(主键、唯一、普通) |
索引实现 | 支持全文索引(Full-text) | 不支持全文索引(Full-text) |
外键支持 | 支持外键(Foreign Key) | 不支持外键(Foreign Key) |
TCP已建立连接时客户端突然断电 / 进程崩溃会怎样?
检测方式 | 是否默认启用 | 检测原理 | 响应速度 | 优点 | 缺点 |
---|---|---|---|---|---|
无任何操作 | ✅ 是 | TCP 不主动检测 | ❌ 极慢(永久挂住) | 简单 | 无法感知对端异常断开 |
应用层心跳机制 | ❌ 否(需手动) | 应用每隔 T 秒发送 ping 或请求 | ✅ 快(自定义) | 快速准确、可控 | 实现复杂度稍高 |
主动读/写操作 | ✅ 是 | 调用 read/write 发现异常 | ✅ 中等 | 实现简单 | 必须有数据交互触发,空闲时无效 |
TCP KeepAlive | ❌ 否(默认关闭) | 内核发送探测包检测连接状态 | ❌ 慢(默认2h+) | 自动完成 | 开启麻烦,默认超时时间非常长 |
场景 | 建议做法 |
---|---|
通信频繁(如游戏/推送系统) | 主动读写结合心跳机制 |
长连接但低频通信(如RPC) | 开启 TCP keepalive + 应用层超时 |
重要服务/金融级稳定性需求 | 心跳 + 超时 + TCP keepalive 三重保障 |
Raft介绍一下 和Paxos区别?
Raft是一个分布式一致性算法,用来保证多个节点之间的数据一致性。简单说就是:多个服务器如何达成共识,选出一个"老大"来管理数据。
核心概念
三种角色
- Leader(领导者):处理所有客户端请求,管理数据复制
- Follower(跟随者):被动响应Leader和Candidate的请求
- Candidate(候选人):选举过程中的临时角色
任期(Term)
- 每个任期都有一个数字标识(1, 2, 3...)
- 每个任期最多只能有一个Leader
- 任期是递增的,不会回退
选举过程(Leader Election)
触发条件
- 节点启动时
- Leader宕机时
- Follower超时未收到Leader心跳时
选举步骤
- Follower → Candidate:超时后发起选举
- 投票阶段:
- Candidate给自己投票
- 向其他节点请求投票
- 其他节点只能投给任期号更大的Candidate
- 结果:
- 获得多数票 → 成为Leader
- 收到更高任期号 → 变回Follower
- 超时未获得多数票 → 重新选举
日志复制(Log Replication)
基本流程
- 客户端请求 → Leader
- Leader记录日志 → 本地
- 并行发送 → 所有Follower
- Follower确认 → 回复Leader
- Leader提交 → 通知Follower提交
- 返回结果 → 客户端
日志一致性
- 每个日志条目都有任期号和索引号
- Leader确保所有Follower的日志与自己一致
- 通过AppendEntries RPC同步日志
安全性保证
选举限制
- 只有包含所有已提交日志的节点才能成为Leader
- 确保新Leader不会丢失已提交的数据
日志匹配
- 如果两个日志在相同索引位置有相同任期号,则它们包含相同的命令
- 如果两个日志在相同索引位置有相同任期号,则它们之前的所有日志条目都相同
简单示例
场景:5个节点的集群
初始状态:所有节点都是Follower
Term 1: 节点A超时 → 发起选举 → 获得3票 → 成为Leader
Term 1: 客户端写请求 → Leader A处理 → 复制到其他节点 → 提交成功
Term 2: Leader A宕机 → 节点B超时 → 发起选举 → 成为新Leader
与Paxos的区别
特性 | Raft | Paxos |
---|---|---|
理解难度 | 相对简单 | 复杂 |
角色划分 | 明确(Leader/Follower/Candidate) | 角色不固定 |
日志管理 | 有明确的日志复制机制 | 需要额外实现 |
实现复杂度 | 较低 | 较高 |
实际应用
- etcd:Kubernetes的配置存储
- Consul:服务发现和配置管理
- TiKV:分布式数据库
- CockroachDB:分布式SQL数据库
要点
Raft通过明确的Leader选举和日志复制机制,保证分布式系统中的数据一致性,相比Paxos更容易理解和实现。
- 强一致性保证
- 易于理解和实现
- 自动故障恢复
- 支持动态成员变更
选举选老大,复制保一致,任期防冲突,日志要匹配。
hashCode()和equals()的区别,为什么重写equals()就要重写hashCode()?
- Java 对 equals() 和 hashCode() 的契约如下:
- 如果两个对象通过 equals() 比较相等,则它们的 hashCode() 必须相等
- 如果两个对象 hashCode() 相等,它们不一定 equals() 相等(哈希冲突是允许的)
问题 | 结论 |
---|---|
equals() 比较什么? | 值是否相等 |
hashCode() 用来干嘛? | 快速查找桶 |
为什么要同时重写? | 保证集合类正常工作,避免逻辑错误 |
不重写会怎样? | 数据重复、查找失败、逻辑异常 |
你对反射的了解?
反射就是在程序运行期间动态的获取对象的属性和方法的功能叫做反射。它能够在程序运行期间,对于任意一个类,都能知道它所有的方法和属性,对于任意一个对象,都能知道他的属性和方法。
- 获取Class对象的三种方式:
getClass()
、xx.class
、Class.forName("xxx")
- 反射的优缺点:优点:运行期间能够动态的获取类,提高代码的灵活性。缺点:性能比直接的Java代码要慢很多。应用场景:spring的xml配置模式,以及动态代理模式都用到了反射。
优先队列
PriorityQueue不是线程安全的,线程安全适用PriorityBlockingQueue,使用了ReentrantLock实现线程安全。优先队列是一种按优先级顺序出队的数据结构,常用堆实现,用于调度、路径搜索和任务管理等场景。
Spring Boot用哪个动态代理?
特性 | JDK 动态代理 | CGLIB 动态代理 |
---|---|---|
是否需要接口 | ✅ 必须实现接口 | ❌ 不需要接口 |
实现方式 | Java 反射 + 接口 | 继承目标类 + 字节码增强(ASM) |
性能 | JDK 1.6 之前慢,之后较快 | 创建代理时稍慢,执行时更快 |
final 方法支持 | ❌ 无法代理 final 方法 | ❌ final 方法无法代理 |
原理 | Proxy.newProxyInstance() | Enhancer.create() |
- Spring AOP 默认使用:
- JDK 动态代理:如果目标类实现了接口
- CGLIB 动态代理:如果目标类没有实现接口
消息队列的推拉模式知道吗?
对比项 | 推(Push) | 拉(Pull) |
---|---|---|
消息发送 | 队列主动将消息推送给消费者 | 消费者主动向队列轮询拉取消息 |
实时性 | 高(消息到达即发送) | 相对低(轮询间隔影响) |
控制权 | 队列控制(可能导致消费者过载) | 消费者控制拉取频率、数量 |
复杂性 | 需要处理消费者负载问题 | 需设计合适的拉取策略 |
可靠性 | 可能消息丢失(若未确认即宕机) | 更容易处理确认机制与重试 |
✅ 推(Push)适用场景:
典型场景 | 原因 |
---|---|
即时消息通知系统(如短信、邮件、推送) | 实时性要求高,用户操作立刻响应 |
服务调用或RPC结果通知 | 一旦有消息,立即通知消费者执行 |
低并发高实时 | 消息不多,立刻推送能带来更快响应体验 |
👉 代表系统: RabbitMQ 默认使用 Push 模式
✅ 拉(Pull)适用场景:
典型场景 | 原因 |
---|---|
大数据/日志分析系统 | 需要按批处理、避免频繁中断 |
爬虫/异步任务系统 | 任务处理复杂,消费者需要自己调度 |
消费速率不均/波动大 | 避免消费者压力过大,自控节奏 |
离线处理(如电商大促后统计) | 没有强实时性要求,按需拉取更高效 |
👉 代表系统: Kafka 默认使用 Pull 模式
条件 | 建议使用方式 |
---|---|
实时性优先 | 推 |
控制消费节奏 | 拉 |
消息量大 | 拉(避免推爆消费者) |
网络不稳定 | 拉(消费者可断点续拉) |
消费者数量不定 | 推(结合队列分发) |
一些中间件(如 Kafka Connect 或 Redis Stream)可以:
- 推送消息到缓冲队列
- 消费者从缓冲中拉取处理
这样可以兼顾实时性和负载控制。
负载均衡算法有哪些?
算法名称 | 类型 | 核心原理 | 优点 | 缺点 |
---|---|---|---|---|
轮询(Round Robin) | 静态 | 请求依次分配到后端服务器 | 实现简单、请求均衡 | 忽略服务器性能、负载差异 |
加权轮询 | 静态 | 根据权重分配请求,权重大者分得多 | 考虑性能差异,灵活配置 | 需人工设定权重,不够智能 |
随机(Random) | 静态 | 随机选择一台服务器 | 实现简单,适合节点性能相近 | 容易负载不均 |
加权随机 | 静态 | 按权重进行随机分配 | 随机性+性能考虑 | 同样需人工设定权重 |
源地址哈希 | 静态 | 根据请求源 IP 哈希分配,保证同一客户端命中相同节点 | 保持会话一致性(粘性会话) | 不利于动态扩容 |
最少连接(Least Connections) | 动态 | 分配到当前连接数最少的服务器 | 动态反映负载状态,分配合理 | 实现复杂,需要连接状态监控 |
加权最少连接 | 动态 | 综合考虑权重与连接数 | 更精细的负载控制 | 权重设置复杂 |
响应时间优先(Fastest Response) | 动态 | 分配到响应时间最短的节点 | 高性能优先 | 需要实时采样响应时间 |
一致性哈希 | 静态/分布式 | 哈希请求 key 映射到 hash 环,适用于缓存或服务治理等场景 | 缓存命中率高,节点变化影响小 | 实现复杂,适合特定应用 |
Redis哨兵运行的时候如果主节点抖了一下咋办 假如抖的时候来请求了咋办 刚好没有超过超时配置?
情况描述
- 主节点短暂“抖动”或响应延迟,但没有完全宕机,且延迟时间没超过哨兵的**故障判定超时(
down-after-milliseconds
)**配置。 - 这个时候,有客户端请求打到主节点。
哨兵和请求处理过程
哨兵对主节点的健康检查是周期性的,只有连续超过配置时间的不可用才认定为主节点宕机
- 例如
down-after-milliseconds
= 30秒,主节点延迟10秒响应,哨兵不会立即判定它挂掉。
- 例如
如果抖动时间没超过超时,哨兵认为主节点仍然正常
- 哨兵不会触发故障转移(failover)。
客户端请求会继续发给主节点
- 如果主节点还能响应请求,客户端会等待响应,可能会体验到延迟。
- 如果主节点真的卡死或拒绝请求,客户端会报错或超时。
哨兵只会在确定主节点“下线”后启动故障转移流程
- 故障转移会选举新的主节点,通知客户端(或客户端通过哨兵获取新主节点信息)。
综上,抖动情况下请求表现
事件 | 影响与处理 |
---|---|
抖动时间 < 哨兵判定时间 | 哨兵认为主节点正常,请求继续发往主节点,可能有响应延迟 |
抖动时间 > 哨兵判定时间 | 哨兵判定主节点故障,启动故障转移,客户端请求转向新主节点 |
主节点完全不可用 | 请求失败或超时,等待哨兵切换主节点 |
如何降低抖动带来的影响?
- 调优哨兵故障判定参数,比如
down-after-milliseconds
,根据业务对延迟和可用性的容忍度权衡设置。 - 客户端设置合理的超时和重试机制。
- 使用哨兵集群监控多个节点,避免单点误判。
- 主节点性能调优,避免抖动。
如何实现防抖和节流?
- 前端实现
特性 | 防抖(Debounce) | 节流(Throttle) |
---|---|---|
执行时机 | 事件停止触发后执行一次 | 按固定时间间隔执行一次 |
执行频率 | 多次触发只执行一次 | 多次触发按间隔执行多次 |
适用场景 | 输入框防抖、搜索、窗口大小调整完成后执行 | 滚动事件、窗口大小实时变化处理 |
触发函数次数 | 一定时间内只执行一次 | 一定时间内按频率执行多次 |
进程之间的通信方式?
通信方式 | 是否共享内存 | 特点 | 应用场景 |
---|---|---|---|
管道(Pipe) | 否 | 单向通信,父子进程之间使用 | 简单通信 |
命名管道(FIFO) | 否 | 支持无亲缘关系的进程 | 本地通信 |
信号(Signal) | 否 | 传递少量信号,用于进程控制 | 通知或中断 |
消息队列 | 否 | 结构化数据传递,有消息格式 | 解耦通信 |
共享内存 | 是 | 速度最快,需配合同步机制(如信号量) | 大数据通信 |
信号量(Semaphore) | 是/否 | 主要用于进程同步(可辅助共享内存) | 同步控制 |
套接字(Socket) | 否(可远程) | 支持网络通信,可跨主机 | 分布式系统 |
内存映射文件(mmap) | 是 | 将文件映射进内存供多个进程访问 | 文件共享 |
Linux查看进程有哪些命令?
常用进程查看命令:
命令 | 作用 | 常用参数 | 示例 |
---|---|---|---|
ps | 查看进程快照 | -ef , -aux | ps -ef | grep java |
top | 实时查看进程 | -p PID | top -p 1234 |
htop | 增强版top | 交互式操作 | htop |
pgrep | 按名称查找PID | -f 完整命令行 | pgrep -f tomcat |
pidof | 查找程序PID | 程序名 | pidof nginx |
pstree | 树形显示进程 | -p 显示PID | pstree -p |
面试重点回答:
最常用的是ps和top:
- ps -ef:查看所有进程的完整信息
- ps -aux:显示详细的资源使用情况
- top:实时监控进程CPU、内存使用
- pgrep:快速根据进程名查找PID
DB一次请求正常时间应该是多少?Redis呢?
数据库请求响应时间标准:
数据库类型 | 正常响应时间 | 优秀水平 | 告警阈值 | 说明 |
---|---|---|---|---|
MySQL | 10-50ms | <10ms | >100ms | 简单查询,有索引 |
PostgreSQL | 10-50ms | <10ms | >100ms | 简单查询,有索引 |
Redis | 0.1-1ms | <0.5ms | >5ms | 内存操作,单线程 |
MongoDB | 5-20ms | <5ms | >50ms | 文档查询 |
影响因素:
因素 | MySQL影响 | Redis影响 | 优化建议 |
---|---|---|---|
网络延迟 | 1-5ms | 0.1-0.5ms | 内网部署,减少跳数 |
查询复杂度 | 较大 | 很小 | 优化SQL,添加索引 |
数据量大小 | 较大 | 一般 | 分页查询,数据分片 |
并发连接数 | 较大 | 一般 | 连接池管理 |
面试重点回答:
- MySQL简单查询:10-50ms正常,超过100ms需要优化
- Redis操作:0.1-1ms正常,超过5ms有问题
- 复杂查询可能需要几百毫秒,需要优化SQL和索引
- 网络延迟通常占1-5ms,内网部署可以降低
数据库ORM连接池有哪些点需要考量?
连接池核心参数:
参数 | 作用 | 推荐值 | 设置依据 |
---|---|---|---|
初始连接数 | 启动时创建的连接数 | 5-10 | 避免冷启动,不宜过大 |
最大连接数 | 连接池最大容量 | CPU核数×2 | 数据库承载能力 |
最小空闲连接 | 保持的最少连接数 | 5 | 保证响应速度 |
最大空闲时间 | 连接空闲多久被回收 | 10-30分钟 | 平衡资源利用和响应 |
连接超时时间 | 获取连接的超时时间 | 30秒 | 避免请求长时间等待 |
验证查询 | 检测连接有效性 | SELECT 1 | 确保连接可用 |
连接池选型对比:
连接池 | 特点 | 性能 | 适用场景 |
---|---|---|---|
HikariCP | 轻量级,启动快 | 最优 | Spring Boot默认推荐 |
Druid | 功能丰富,监控强 | 良好 | 需要详细监控的场景 |
C3P0 | 老牌稳定 | 一般 | 传统项目 |
DBCP | Apache出品 | 一般 | 简单场景 |
关键考量点:
考量维度 | 关键问题 | 最佳实践 |
---|---|---|
性能 | 连接获取速度、资源消耗 | 选择HikariCP,合理设置连接数 |
稳定性 | 连接泄漏、死锁检测 | 启用连接验证,设置超时时间 |
监控 | 连接使用情况、异常统计 | 集成监控,设置告警 |
扩展性 | 动态调整连接数 | 支持运行时参数调整 |
面试重点回答:
主要考虑四个方面:
1. 连接数设置:最大连接数=CPU核数×2,避免过大造成数据库压力
2. 超时管理:连接超时、空闲超时要合理设置
3. 连接验证:定期检查连接有效性,避免使用无效连接
4. 监控告警:监控连接池使用情况,及时发现问题
推荐使用HikariCP,性能最优,Spring Boot默认选择。
IP 分片(IP Fragmentation)
💡 背景:
IP 层最大传输单元(MTU)是有限的。以太网典型 MTU 为 1500 字节。若上层数据超过 MTU,IP 层会自动分片。
📦 分片原理:
IP 会将大的报文分成多个「分片片段(Fragment)」发送。
每个分片都有自己的 IP 头,其中含有如下关键字段:
Identification
:表示这些分片属于同一 IP 报文Fragment Offset
:表示当前分片在原始数据中的偏移量(单位是 8 字节)MF (More Fragments)
:如果为 1 表示后面还有分片,为 0 表示这是最后一片
📌 特点:
- 由发送端进行分片,接收端重组(组装)
- 一旦有任何一个分片丢失,整个 IP 报文重组失败
❗问题:
- 分片降低性能,增加丢包风险
- TCP 尽量避免分片(通过 MSS 协商)
TCP 报文长度管理
TCP 是面向字节流的协议,不关心「一次发送」对应「一次接收」,但必须控制每个报文长度。
🚧 方式:
MSS(Maximum Segment Size)最大报文段长度
- MSS = MTU - IP头(20B) - TCP头(20B) ≈ 1460 字节
- 双方在三次握手时协商
TCP 头中的字段
Sequence Number
: 表示当前报文的起始字节序号ACK Number
: 表示期望接收对方下一个字节Window Size
: 表示本端还能接受多少字节(用于流控)
应用层的分段(Segmenting)
- 上层写入多大,TCP 可能按需拆分为多个段,或者合并多个小数据包(Nagle 算法)
UDP 最长报文长度是多少?
UDP 头部只有 8 字节,非常轻量。
🧱 UDP 报文长度:
- UDP 报文长度字段是 16 位 → 最大长度为 65535 字节
- UDP 报文 = UDP头(8字节)+ 数据
- 所以 UDP 有效负载最大为 65507 字节
❗但注意:
- 实际中大多数链路 MTU 限制了 UDP 报文大小
- 典型情况下 UDP 报文应 ≤ 1472 字节,否则容易被分片
UDP 和 TCP 报文头细节对比
项目 | TCP 报文头 | UDP 报文头 |
---|---|---|
头部长度 | 最小 20 字节(可变) | 固定 8 字节 |
是否有连接 | 有(面向连接) | 无(无连接) |
是否可靠传输 | 是(重传、校验、确认) | 否 |
流控/拥塞控制 | 有 | 无 |
分段处理 | 自动处理,保证顺序 | 无序、应用层处理 |
头部字段 | 复杂,包含序号、确认号、窗口、标志位等 | 简单,只包含端口和长度等 |
📌 TCP 报文头结构(20 字节起):
字段 | 描述 |
---|---|
Source Port | 源端口 |
Destination Port | 目标端口 |
Sequence Number | 序列号(字节编号) |
Acknowledgment Number | 确认号 |
Data Offset | 头部长度 |
Flags | 如 SYN/ACK/FIN 等 |
Window Size | 滑动窗口大小 |
Checksum | 校验和 |
Urgent Pointer | 紧急数据指针 |
Options | 可选字段,如 MSS、窗口扩大等(可变长度) |
📌 UDP 报文头结构(固定 8 字节):
字段 | 描述 |
---|---|
Source Port | 源端口 |
Destination Port | 目标端口 |
Length | UDP 报文长度 |
Checksum | 校验和(IPv6 中必须) |
消息队列如何实现分布式事务的?
分布式事务:多个系统(服务 A、服务 B、数据库、消息队列)需要在一个事务中「要么全部成功,要么全部失败」。
⚠️ 问题是:
- 每个服务/组件是独立系统,没有统一的事务管理器
- 网络异常、系统崩溃很常见,传统两阶段提交(2PC)效率低、风险高
🎯 解决目标:
保证 最终一致性(而不是强一致性)
即:各系统最终都会到达正确状态,中间可能短暂不一致。
💡 消息队列的作用
消息队列(如 Kafka、RabbitMQ、RocketMQ)是实现分布式事务的核心中间件,它不直接帮你做事务控制,而是:
- 解耦系统
- 通过可靠消息机制,让各系统异步处理
- 引入消息状态回查机制,确保最终一致性
🧠 主流方案一:本地事务 + 消息发送(异步保证一致性)
示例场景:
订单系统下单后,异步通知库存系统扣减库存。
流程图:
[订单服务]
1. 本地事务:写订单数据库
2. 同时发送 MQ 消息(半消息)
3. 本地事务成功后提交消息
[库存服务]
4. 收到消息 → 执行扣库存逻辑
RocketMQ 的典型实现(事务消息机制)
Producer 发送 prepare 消息(Half Message)
Broker 持久化但不投递
Producer 执行本地事务(如写订单库)
根据结果:
- 成功 → 提交消息(Broker 投递给 Consumer)
- 失败 → 回滚消息(丢弃)
💡 如果消息长时间未提交,Broker 会回查事务状态。
🔁 主流方案二:可靠消息 + 本地事务(最终一致性)
这个方式更适用于不支持“半消息”的队列,比如 Kafka、RabbitMQ。
步骤:
- 写一条本地“事务消息表(Outbox Table)”
- 与业务操作一起放入数据库事务中提交 ✅
- 后台定时任务或消息中间件扫描消息表 → 推送到 MQ
- 发送成功后将消息标记为“已发送”
- 消费方处理成功后 → 发确认回执
🟢 这种方式不依赖消息中间件支持事务,适配性强。
✨ 核心技术点总结
技术 | 说明 |
---|---|
事务消息 | 支持半消息机制(如 RocketMQ) |
本地消息表 + 定时投递 | 异步发送 + 手动投递,兼容性高 |
幂等消费 | 防止消息重复投递导致重复操作(如扣库存) |
消息回查机制 | Broker 超时未收到提交 → 主动询问事务状态 |
最终一致性保障 | 即使部分失败,可通过补偿、重试实现修复 |
🧪 示例:下单 + 扣减库存分布式事务
订单服务:
BEGIN
1. 插入订单记录
2. 插入事务消息表(库存扣减消息)
COMMIT
后台任务:
3. 轮询消息表,发送消息给 MQ
4. 更新消息状态为已投递
库存服务:
5. 接收消息,扣减库存
6. 消费成功,记录日志用于幂等控制
✅ 总结:消息队列实现分布式事务的 2 大核心模式
模式 | 是否强依赖 MQ 支持 | 适用场景 | 优点 | 缺点 |
---|---|---|---|---|
事务消息(Half Message) | ✅ 是(如 RocketMQ) | 核心业务流程 | 原子性强,机制健全 | 实现复杂,MQ耦合 |
本地消息表(Outbox) | ❌ 否 | 兼容任意 MQ | 可控、兼容性好 | 需要开发定时投递逻辑 |
判断丢包
TCP层面
- 序列号机制:通过序列号检测丢包
- 重传机制:丢包后自动重传
- RTT计算:往返时间计算
应用层面
- 心跳机制:定期发送心跳包
- 确认机制:应用层确认机制
- 超时重传:超时后重传数据
网络层面
- ICMP:网络层丢包检测
- ping命令:测试网络连通性
- traceroute:路由路径检测
TCP服务器创建过程中的socket系统调用
- socket():创建socket文件描述符
- bind():绑定IP地址和端口
- listen():开始监听连接
- accept():接受客户端连接
- close():关闭socket
整个流程详解
服务器端流程
1. socket() → 2. bind() → 3. listen() → 4. accept() → 5. 处理连接
详细步骤
- socket():创建TCP socket
- bind():绑定IP和端口
- listen():设置监听队列
- accept():等待客户端连接
- 数据收发:read()/write()或send()/recv()
- close():关闭连接
客户端流程
1. socket() → 2. connect() → 3. 数据收发 → 4. close()
应用层解决UDP错包问题
UDP的不可靠性
- 丢包:网络拥塞导致数据包丢失
- 乱序:数据包到达顺序不确定
- 重复:网络重传导致重复包
具体实现策略
策略 | 实现方式 | 优点 | 缺点 |
---|---|---|---|
序列号 | 每个包分配唯一序列号 | 检测丢包和乱序 | 增加包头开销 |
确认机制 | 接收方发送ACK | 可靠传输 | 增加网络开销 |
超时重传 | 超时后重传数据包 | 保证数据到达 | 增加延迟 |
滑动窗口 | 控制发送窗口大小 | 流量控制 | 实现复杂 |
epoll用红黑树为什么没有FD2048数量限制?
select的限制
- FD_SETSIZE:默认1024,最大2048
- 位图结构:使用位图表示FD集合
- 轮询机制:需要遍历所有FD
epoll的优势
- 红黑树:高效的数据结构,O(log n)复杂度
- 事件驱动:只处理就绪的FD
- 无数量限制:受系统资源限制,理论上无上限
数据结构对比
特性 | select | epoll |
---|---|---|
数据结构 | 位图 | 红黑树 |
时间复杂度 | O(n) | O(log n) |
FD数量限制 | 2048 | 无限制 |
效率 | 低 | 高 |
网络包拆包是在哪一层?
层级 | 是否拆包 | 原因 |
---|---|---|
数据链路层(第 2 层) | ✅ 是 | Ethernet 最大帧长为 1500 字节,超出会被拆分成多个帧发送(MTU 限制) |
网络层(第 3 层) | ✅ 是 | IP 协议支持“分片”(Fragmentation),当数据包超过 MTU 时发生 |
传输层(第 4 层) | ✅ 是 | TCP 有“报文段(segment)”,应用层数据会被拆成多个 TCP 段发送 |
Redis集群如果多个 key 是连续的或相关的,是否可以插入到同一个节点?
- 策略:使用 “hash tag” 让相关 key 落在同一个 slot
key1 = user:{123}:name
key2 = user:{123}:age
key3 = user:{123}:email
- Redis Cluster 只会对大括号 {} 中的内容做哈希
- 上面三个 key 都会使用 123 作为 hash tag → 落在相同的 slot → 插入同一节点
✅ 场景好处:
- 可以批量操作这些 key(如 MGET、MSET)
- 可以减少跨节点操作,提高性能
- 避免 CROSSSLOT 错误
两个地址在网络中的通信流程?
- 应用层(HTTP / FTP 等)
- 应用发起请求(如浏览器访问网站)
- 传输层(TCP / UDP)
- 把数据切成若干“段”,加上源端口和目标端口(如 TCP 80)
- 网络层(IP)
- 给包加上源 IP、目标 IP → 决定目标地址是谁
- 数据链路层(Ethernet)
- 把数据封装成帧,加上 MAC 地址,准备发送
- 物理层(网线/无线)
- 以比特流的形式通过电信号/光信号等传输出去
大数据去重?
BitMap
- 空间效率极高
- 传统方法:存储每个元素需要4-8字节
- BitMap:每个元素只需要1位(bit)
- 空间节省:节省32-64倍存储空间
- 时间复杂度优秀
- 插入操作:O(1) - 直接设置对应位
- 查询操作:O(1) - 直接检查对应位
- 去重操作:O(n) - 遍历一次即可
DDD架构
核心概念
- 领域驱动设计:以业务领域为核心,通过领域模型驱动软件设计
- 领域模型:反映业务概念和规则的抽象模型
- 分层架构:按业务领域分层,而非技术分层
DDD分层架构
用户界面层(Interface/User Interface)
↓
应用层(Application)
↓
领域层(Domain)
↓
基础设施层(Infrastructure)
DDD vs 传统架构对比
1. 分层方式
维度 | 传统三层架构 | DDD架构 |
---|---|---|
分层依据 | 技术职责 | 业务领域 |
核心层 | 业务逻辑层 | 领域层 |
依赖方向 | 上层依赖下层 | 内层不依赖外层 |
关注点 | 技术实现 | 业务价值 |
2. 设计思路
传统三层架构
表现层 → 业务逻辑层 → 数据访问层
- 技术导向:按技术职责分层
- 数据驱动:以数据为中心
- 贫血模型:实体类只有getter/setter
DDD架构
用户界面层 → 应用层 → 领域层 → 基础设施层
- 业务导向:按业务领域分层
- 领域驱动:以领域模型为中心
- 充血模型:实体类包含业务逻辑
DDD核心组件
1. 领域层(Domain Layer)
- 实体(Entity):有唯一标识的对象
- 值对象(Value Object):无唯一标识的对象
- 领域服务(Domain Service):跨实体的业务逻辑
- 聚合(Aggregate):业务边界内的对象集合
- 仓储(Repository):数据访问抽象
2. 应用层(Application Layer)
- 应用服务(Application Service):协调领域对象
- DTO:数据传输对象
- 命令/查询:CQRS模式
3. 基础设施层(Infrastructure Layer)
- 仓储实现:具体的数据访问实现
- 外部服务:第三方服务集成
- 消息队列:异步消息处理
架构特点对比
特性 | 传统三层架构 | DDD架构 |
---|---|---|
设计导向 | 技术导向 | 业务导向 |
模型设计 | 贫血模型 | 充血模型 |
业务逻辑 | 服务层 | 领域层 |
数据访问 | DAO模式 | 仓储模式 |
依赖关系 | 上层依赖下层 | 内层不依赖外层 |
可维护性 | 中等 | 高 |
可扩展性 | 中等 | 高 |
学习成本 | 低 | 高 |
适用场景对比
传统三层架构适用场景
- 简单业务:业务逻辑相对简单
- 快速开发:需要快速交付
- 团队技能:团队对DDD不熟悉
- 遗留系统:维护现有系统
DDD架构适用场景
- 复杂业务:业务逻辑复杂
- 长期维护:需要长期维护和演进
- 团队技能:团队具备DDD经验
- 新系统:从零开始设计
优势劣势对比
DDD优势
- 业务导向:以业务价值为核心
- 可维护性:业务逻辑集中,易于维护
- 可扩展性:领域模型清晰,易于扩展
- 团队协作:业务和技术团队沟通顺畅
DDD劣势
- 学习成本:概念多,学习曲线陡峭
- 开发成本:初期开发成本高
- 过度设计:简单业务可能过度设计
- 团队要求:需要团队具备DDD经验
DDD核心思想:
- 以业务领域为核心,通过领域模型驱动设计
- 按业务领域分层,而非技术分层
- 充血模型,实体包含业务逻辑
- 内层不依赖外层,依赖倒置
与传统架构对比:
- 传统架构:技术导向,贫血模型,三层架构
- DDD架构:业务导向,充血模型,分层架构
选择建议:
- 简单业务、快速开发 → 传统三层架构
- 复杂业务、长期维护 → DDD架构
Linux管道是什么?
MySQL架构分层及工作流程
- 连接管理层:处理客户端连接认证,线程管理。
- SQL解析层:解析SQL语句成解析树。
- 优化器层:基于统计信息生成执行计划,决定使用哪些索引,JOIN顺序。
- 执行引擎层:根据执行计划,调用存储引擎接口执行。
- 存储引擎层:负责数据存储,读取,索引操作(如 InnoDB)。
MySQL执行SQL时如何知道扫描行数(执行计划)
- 通过 EXPLAIN 命令查看执行计划,包含:
- 访问类型(全表扫描、索引扫描等)
- 预计扫描的行数(rows字段)
- 使用的索引
- 优化器基于统计信息和索引信息估算扫描行数。
性能毛刺及死锁原因
- 性能毛刺:CPU突发高负载、缓存失效、慢查询、锁竞争、I/O瓶颈。
- 死锁原因:两个或多个事务互相等待对方持有的锁,导致永远等待。常见于多表复杂事务或索引不合理。
MySQL连接池状态查看
- 通过连接池管理工具或应用日志查看连接池参数。 在数据库端可查看当前活动连接:
SHOW PROCESSLIST;
SHOW STATUS LIKE 'Threads_connected';
结合应用端连接池监控(如 HikariCP、Druid)查看连接池使用情况。
静态代码块加载顺序
类加载时执行
- 类第一次被加载到 JVM 中时,静态代码块会被执行。
- 执行时机是在类的静态变量初始化之前或之后(按代码顺序),且只执行一次。
多个静态代码块按顺序执行
- 如果一个类中有多个静态代码块,它们会按照在类中出现的先后顺序依次执行。
- 与静态变量初始化按代码顺序交叉执行。
子类加载时,先执行父类静态代码块,再执行子类的
- JVM 加载子类时,先加载父类,父类的静态代码块先执行,之后执行子类的。
例子说明
public class Test {
static {
System.out.println("静态代码块1");
}
static int x = initializeX();
static {
System.out.println("静态代码块2");
}
private static int initializeX() {
System.out.println("静态变量初始化");
return 5;
}
public static void main(String[] args) {
System.out.println("main方法执行");
}
}
输出顺序:
静态代码块1
静态变量初始化
静态代码块2
main方法执行
二叉树类型
二叉树类型 | 说明 | 查找/插入/删除平均时间复杂度 | 备注 |
---|---|---|---|
普通二叉树 | 每个节点最多有两个子节点,结构不限 | O(n) | 没有顺序限制,遍历时最坏是全部节点 |
二叉搜索树 (BST) | 左子树所有节点 < 根节点 < 右子树所有节点 | 平均 O(log n),最坏 O(n) | 若退化成链表,性能退化 |
平衡二叉搜索树 | 自平衡的 BST,如 AVL 树、红黑树 | O(log n) | 保证树高是 O(log n),性能稳定 |
满二叉树 | 除了叶子节点,每个节点都有两个子节点 | 与普通二叉树类似 | 结构紧凑,节点数量固定 |
完全二叉树 | 除了最底层外,其他层节点数都满,且叶子尽可能左对齐 | 与普通二叉树类似 | 常用于堆的实现 |
平衡二叉树 | 任意节点左右子树高度差 ≤ 1 | O(log n) | AVL 是典型平衡二叉树 |
消息队列的投递机制?至多一次,至少一次,恰好一次?
投递语义 | 说明 | 优点 | 缺点 | 实现关键点 |
---|---|---|---|---|
At most once(至多一次) | 消息最多被投递一次,不会重复,但可能丢失。 | 延迟低,性能高 | 消息可能丢失 | 不做确认(ACK),或 ACK 在处理前就发出 |
At least once(至少一次) | 消息至少被投递一次,不会丢失,但可能重复。 | 不丢消息 | 可能重复,需要去重 | ACK 在处理完成后发送,失败会重试 |
Exactly once(恰好一次) | 消息恰好投递一次,既不丢失,也不重复。 | 数据一致性最佳 | 实现复杂,性能开销大 | 去重 + 幂等 + 两阶段提交/事务 |
设计银行存取款和转账业务的具体实现,除了加锁还有什么?
方法 | 作用 | 优缺点 | 适用场景 |
---|---|---|---|
数据库事务 | 保证操作原子性 | 简单可靠,性能有限 | 单库操作,简单转账 |
分布式事务 | 跨系统保证一致性 | 实现复杂,性能开销大 | 跨库或跨系统转账 |
乐观锁 | 减少锁竞争,适合高并发 | 可能需要重试,写冲突时失败 | 高并发写操作 |
悲观锁 | 严格控制并发访问 | 可能导致阻塞、死锁 | 冲突高且必须保证绝对一致性 |
幂等设计 | 防止重复执行 | 需要额外设计和存储请求ID | 网络重试、重复请求防护 |
异步消息+补偿 | 提升吞吐,保证最终一致 | 复杂,开发成本高 | 分布式环境,高吞吐 |
分库分片 | 降低单点压力 | 复杂度高,跨库操作复杂 | 超大规模账户系统 |
并发限流 | 防止流量暴涨导致系统崩溃 | 可能导致请求拒绝或延迟 | 高峰期流量控制 |
业务校验和风控 | 保证合法性和安全 | 业务复杂,需维护规则 | 所有业务流程 |
2PC和3PC的区别
比较维度 | 两阶段提交(2PC) | 三阶段提交(3PC) |
---|---|---|
阶段数 | 2阶段:准备(Prepare)、提交(Commit) | 3阶段:询问(CanCommit)、预提交(PreCommit)、提交(DoCommit) |
通信次数 | 2次往返(协调者→参与者,参与者→协调者) | 3次往返 |
阻塞风险 | 存在阻塞风险;协调者崩溃,参与者可能无限等待 | 阻塞风险降低;增加预提交阶段,参与者状态更明确 |
协调者崩溃处理 | 协调者崩溃时,参与者状态不确定,可能阻塞 | 协调者崩溃时,参与者通过超时自动决定回滚或提交 |
参与者崩溃处理 | 参与者崩溃可通过重启恢复 | 参与者崩溃恢复策略相同 |
协议复杂度 | 简单,易实现 | 较复杂,增加了预提交阶段和状态管理 |
实时性 | 较高,阶段较少 | 较低,因多一阶段通信 |
适用场景 | 适合对阻塞可接受、实现简单的分布式事务 | 适合对可用性和非阻塞有更高要求的分布式系统 |
安全性 | 有阻塞死锁隐患 | 改善了阻塞死锁问题,但不能完全避免网络分区 |
用rand5()实现rand7()
扩大随机空间
两次调用
rand5()
可以生成 25 种等概率情况:num = (rand5() - 1) * 5 + rand5() → [1,25]
丢弃超出范围的部分
- 只取
[1,21]
,因为 21 能被 7 整除,保证等概率。 - 如果生成 >21,重新生成(reject sampling)。
- 只取
映射到 1~7
result = 1 + (num - 1) % 7
假如error影响到线上业务 应该重新上传代码还是重启服务器?
决策矩阵
问题类型 | 重启服务器 | 重启应用 | 重新部署代码 |
---|---|---|---|
内存泄漏 | ✅ | ✅ | ❌ |
代码Bug | ❌ | ❌ | ✅ |
配置错误 | ❌ | ✅ | ❌ |
系统资源耗尽 | ✅ | ✅ | ❌ |
网络问题 | ✅ | ❌ | ❌ |
数据库连接问题 | ❌ | ✅ | 可能需要 |
关键原则
- 先恢复服务,再解决问题
- 最小影响原则 - 重启应用 > 重启服务器
- 记录所有操作 - 便于后续分析
- 准备回滚方案 - 确保能快速撤销更改
Nacos如何从AP切换到CP?
- 启动参数添加
-DserverMode=CP
- 或者在application配置文件里添加
nacos.core.auth.system.type=cp
动态切换
Nacos 在 2.x 后,支持根据服务类型动态选择 AP/CP,不需要全局强制。
- 配置中心 → 强一致性(CP)
- 注册中心(临时实例) → AP
- 注册中心(非临时实例) → CP
Tomcat为啥用自定义类加载器?
Tomcat 之所以要用 自定义类加载器,主要是因为它既要运行自己(容器),又要运行用户的 Web 应用(WAR 包),两者的类和依赖要 隔离又部分共享。
类隔离(避免冲突)
- 不同 Web 应用可能依赖不同版本的库(例如
spring.jar
、log4j.jar
)。 - 如果所有应用都用系统类加载器,必然会发生冲突。
- Tomcat 用每个 WebApp 自己的
WebAppClassLoader
,保证它们的 jar 包互不干扰。
- 不同 Web 应用可能依赖不同版本的库(例如
热部署 / 热加载
- 如果直接用 JVM 的 AppClassLoader,类一旦加载就卸不掉。
- Tomcat 自定义类加载器,可以在应用卸载时丢弃整个类加载器,从而“回收”类,支持应用热重启。
部分共享
Tomcat 的运行本身需要一些库(servlet-api.jar、JSP 引擎等),但这些库 不能让 Web 应用自己带,否则会冲突。
所以 Tomcat 自定义了一个类加载层次:
Bootstrap → System → Common → Catalina → Shared → WebApp
- 公共的(如
servlet-api.jar
)放在common
- 每个 WebApp 自己的 jar 在
WEB-INF/lib
- 容器自己运行的类在
catalina
- 公共的(如
安全性
- 防止应用代码覆盖 Tomcat 自身的关键类。
Tomcat 用自定义类加载器,是为了 隔离 Web 应用依赖、防止类冲突、支持热部署、保证容器和应用的类库边界清晰。
Lua脚本
- Redis 单条命令足够简单操作,但 多步骤逻辑 + 要保证原子性 时,就必须用 Lua 脚本。
BeanFactory, FactoryBean, ApplicationContext
1. BeanFactory
- Spring 最基本的 IoC 容器接口。
- 负责 实例化、配置和管理 Bean。
- 按需加载(懒加载),用到时才创建对象。 👉 面试关键词:最低层容器,延迟加载,轻量级。
2. ApplicationContext
BeanFactory 的子接口,功能更强大。
默认 启动时就加载单例 Bean(预加载)。
提供更多企业特性:
- 国际化(MessageSource)
- 事件发布(ApplicationEventPublisher)
- AOP、事务等扩展 👉 面试关键词:BeanFactory 的增强版,常用容器实现。
3. FactoryBean
- 一个能生产 Bean 的 Bean。
- 开发者可以自定义复杂对象的创建逻辑。比如mybatis的mapper接口
getObject()
方法返回的才是容器中的 Bean。- 如果要获取 FactoryBean 本身,要在 id 前加
&
。 👉 面试关键词:特殊 Bean,定制复杂 Bean 创建逻辑。
- BeanFactory:IoC 的最基础容器。
- ApplicationContext:BeanFactory 的增强版,功能更全,实际开发常用。
- FactoryBean:一个特殊的 Bean,用来自定义创建其他 Bean。
MyBatis和FactoryBean
1. MyBatis 的 Mapper 接口
- 你写的
UserMapper.java
其实只是一个 接口,并没有实现类。 - MyBatis 会用 动态代理 给它生成实现类,代理对象才是真正执行 SQL 的。
- 这些 Mapper 最终要交给 Spring 管理,成为 Bean。
2. FactoryBean 在 MyBatis 里的作用
- MyBatis 整合 Spring 时,用了
MapperFactoryBean
。 - MapperFactoryBean 本身是一个 Bean,但它不直接返回自己,而是通过
getObject()
返回代理后的 Mapper。 - 所以你在 Spring 容器里注入的
UserMapper
,其实是MapperFactoryBean
帮你生成的代理对象。
3. 关系总结
- Mapper:只是接口,定义数据库操作。
- MapperFactoryBean:负责把接口转成可用的代理对象,注入到 Spring 容器。
- Spring 用 FactoryBean 机制,屏蔽了复杂的代理逻辑,你拿到的就是可直接调用的 Mapper。
- MyBatis 的 Mapper 是接口,MapperFactoryBean 利用 FactoryBean 机制生成代理对象交给 Spring 管理。
Redis的乐观锁和悲观锁
悲观锁(Pessimistic Lock)
思路:操作数据之前先“锁住”,别人不能动。
在 Redis 中的常见实现:
- 用
SETNX
(set if not exists) 或SET key value NX PX expire
来实现分布式锁。 - 只有一个线程能成功设置锁键,其他线程会阻塞/重试,直到锁释放。
- 典型库:
Redisson
、RedisLock
。
- 用
✅ 特点:
- 确保同一时刻只有一个客户端能操作某个资源。
- 适合强一致性、竞争激烈的场景。
❌ 缺点:
- 性能开销大(需要加锁/解锁)。
- 容易出现死锁(需要加过期时间/看门狗续命机制)。
乐观锁(Optimistic Lock)
思路:不加锁,大家都能并发操作;提交更新时检查有没有冲突。
在 Redis 中的常见实现:
- 用
WATCH key
监视某个 key。 - 在事务 (
MULTI
...EXEC
) 提交时,如果key
在这期间被修改了,事务会失败(返回null
)。 - 程序可以选择重试。
- 用
✅ 特点:
- 没有额外的锁结构,性能高。
- 适合读多写少、冲突概率低的场景。
❌ 缺点:
- 写冲突会导致事务失败,需要客户端重试。
- 不保证能成功更新,存在“饿死”风险。
对比
类型 | Redis实现方式 | 适用场景 |
---|---|---|
悲观锁 | SETNX / SET key value NX PX + 解锁 | 写冲突频繁,需要强一致性 |
乐观锁 | WATCH + MULTI /EXEC | 冲突概率低,读多写少 |
Redis慢查询
什么是 Redis 慢查询
- Redis 是单线程处理命令的,如果某条命令耗时过长,会阻塞后续请求。
- 慢查询(Slow Query) = 执行时间超过配置阈值的命令。
Redis 慢查询配置
Redis 通过两个配置参数控制:
slowlog-log-slower-than
- 单位:微秒(1秒 = 1,000,000微秒)
- 默认:
10000
(即 > 10ms 的命令算慢查询) - 设置成
0
表示所有命令都记录。
CONFIG SET slowlog-log-slower-than 5000 # 超过 5ms 记录
slowlog-max-len
- 最大存储的慢查询日志条数(FIFO)。
- 默认:
128
。
CONFIG SET slowlog-max-len 256
查看慢查询
获取慢查询日志:
SLOWLOG GET [n] # 获取最近 n 条记录,默认 10
输出内容格式:
1) (integer) 14 # 慢查询 id 2) (integer) 1640778673 # 时间戳 3) (integer) 12500 # 执行时间 (微秒) 4) 1) "SET" 2) "foo" 3) "bar" # 执行的命令
获取总数:
SLOWLOG LEN
清空日志:
SLOWLOG RESET
常见慢查询原因
原因 | 说明 | 例子 |
---|---|---|
大 key 操作 | key 里存了大量数据,操作时很慢 | LRANGE key 0 -1 取一个百万级列表 |
阻塞命令 | 命令本身 O(n) 或阻塞 | SORT 、SUNION 、KEYS |
网络延迟 | 客户端与 Redis 之间的 RTT 高 | 云服务跨区访问 |
AOF 重写/持久化 | 后台触发 RDB/AOF 导致阻塞 | fork + IO 抢占 CPU |
Lua 脚本 | 脚本运行时间长 | 大量循环逻辑 |
优化方案
- 优化数据结构:避免大 key,拆分成小 key。
- 禁用危险命令:生产中禁止
KEYS
,用SCAN
代替。 - 合理设置超时时间:比如慢查询阈值、
timeout
。 - 持久化优化:合理设置 RDB/AOF 策略,避免高峰时触发。
- 监控告警:接入
SLOWLOG
监控,比如 Prometheus + Grafana。 - 集群拆分:数据量太大时,考虑 Redis Cluster 或分片。
怎么判断是网络波动问题还是 key 设置不合理问题
- 网络检测:用 ping 命令(或专业网络工具,如 mtr )测试 Redis 服务器网络延迟、丢包率,若延迟忽高忽低、丢包多,大概率网络波动;也可在 Redis 客户端(如 Java 客户端 )统计命令往返时间(RTT ),观察是否异常。
- 慢查询日志分析:开启 Redis 慢查询日志(配置 slowlog-log-slower-than 等参数 ),查看慢查询命令详情。若慢查询集中在大 Key 操作(如对超大字符串 GET/SET )、复杂命令(如 SORT 带多参数 ),就是 Key 或命令设计不合理;若各种简单命令也慢,结合网络检测,更倾向网络问题。
Redis的 gossip协议
- 集群节点间用 gossip 协议交换信息(如节点状态、槽分布 ),让节点感知集群拓扑变化,实现自动发现、故障检测等,保证集群协同工作。
消息队列容器或者消费者宕机,通过什么机制保证数据的一致性
- 容器宕机(生产者 / 队列服务侧 ):若消息队列本身有持久化(如 Kafka 把消息存磁盘、RabbitMQ 开启队列持久化 + 消息持久化 ),容器重启后,未消费消息还在队列;生产者可结合重试机制(失败消息定时重发 )、幂等设计(消费者接收重复消息也不影响结果 ),保证数据最终能正确入队、消费。
- 消费者宕机:
- 消息堆积与重试:队列暂存未消费消息,消费者重启后重新拉取;消息队列一般有重试策略(如 RabbitMQ 的重试队列、Kafka 消费者提交位移机制 ,若消费者没提交消费位移就宕机,重启后会重新消费未确认消息 )。
- 幂等性保障:消费者要实现幂等,比如基于业务唯一标识(订单号 )去重,避免重复消费导致数据重复(如重复创建订单 )。
- 消息丢失预防:通过确认机制(如 Kafka 的 ack 应答、RabbitMQ 的 basic.ack ),确保消息成功消费后才从队列标记删除,否则重新投递。
AOP的原理
好的,我来给你梳理一下 Spring AOP 的原理,从底层机制到实际执行流程。
AOP 的本质
AOP(面向切面编程)的核心是 在不修改业务代码的情况下,在方法执行的前后插入额外逻辑。
在 Spring 里,这主要依赖 动态代理 来实现:
- JDK 动态代理(接口代理):基于
java.lang.reflect.Proxy
,要求目标类实现接口。 - CGLIB 动态代理(类代理):基于字节码生成库 CGLIB,在运行时生成目标类的子类,重写方法插入切面逻辑。
执行流程(Spring AOP)
假设你写了一个切面:
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeMethod() {
System.out.println("方法执行前打印日志");
}
}
Spring AOP 执行过程:
Bean 创建阶段
- Spring IOC 在实例化 Bean 时,会检查该 Bean 是否需要 AOP 增强(有没有切点匹配)。
- 如果需要,Spring 不直接返回目标对象,而是返回一个 代理对象。
代理生成阶段
- 如果目标类实现了接口 → 用 JDK 动态代理。
- 如果没有接口 → 用 CGLIB 生成子类。
方法调用阶段
- 你调用的实际上是代理对象的方法。
- 代理对象内部会先执行切面逻辑(比如
@Before
),再调用目标方法。 - 执行完成后,如果有
@After
或@Around
,则在对应时机执行。
JDK 动态代理核心原理
public class JDKProxyExample {
interface Service {
void doSomething();
}
static class ServiceImpl implements Service {
public void doSomething() {
System.out.println("执行业务逻辑");
}
}
public static void main(String[] args) {
Service target = new ServiceImpl();
Service proxy = (Service) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxyObj, method, methodArgs) -> {
System.out.println("方法执行前");
Object result = method.invoke(target, methodArgs);
System.out.println("方法执行后");
return result;
}
);
proxy.doSomething();
}
}
输出:
方法执行前
执行业务逻辑
方法执行后
CGLIB 动态代理核心原理
- 通过 继承目标类,在子类中重写目标方法,插入切面逻辑。
- 比如代理类会生成类似:
class ServiceImpl$$EnhancerByCGLIB extends ServiceImpl {
@Override
public void doSomething() {
System.out.println("方法执行前");
super.doSomething();
System.out.println("方法执行后");
}
}
- Spring AOP 的原理是 动态代理。
- 如果目标类实现了接口,Spring 使用 JDK 动态代理,底层通过反射调用目标方法。
- 如果没有接口,就用 CGLIB 生成子类,在子类方法里织入切面逻辑。
Bean的生命周期
Spring中bean的生命周期包括以下步骤:
- 通过BeanDefinition获取bean的定义信息。
- 调用构造函数实例化bean。
- 进行bean的依赖注入,例如通过setter方法或@Autowired注解。
- 处理实现了Aware接口的bean。
- 执行BeanPostProcessor的前置处理器。
- 调用初始化方法,如实现了InitializingBean接口或自定义的init-method。
- 执行BeanPostProcessor的后置处理器,可能在这里产生代理对象。
- 最后是销毁bean。
AOF会丢吗
会丢,但取决于你的 AOF 配置策略。Redis 的 AOF(Append Only File)通过将写命令追加到日志文件里来保证持久化,但不是 100% 不丢,原因在于写入磁盘的时机不同。
📌 AOF 写入策略(appendfsync
参数)
策略 | 行为 | 优点 | 缺点 | 可能丢失的数据 |
---|---|---|---|---|
always | 每次写命令都立即 fsync 到磁盘 | 数据最安全,几乎不丢 | 性能最差 | 理论上 0 |
everysec (默认) | 每秒 fsync 一次 | 性能和安全折中 | 最常用 | 最多丢 1 秒的数据 |
no | 由操作系统决定何时刷盘 | 性能最好 | 安全性最差 | 可能丢失几秒甚至更多数据 |
📌 其他导致丢失的场景
AOF 重写(Rewrite)过程异常
- Redis 会在后台 fork 子进程重写 AOF,如果 crash 或磁盘写满,可能导致新 AOF 文件不完整。
- Redis 在启动时会检测并尝试修复(删除尾部坏命令)。
磁盘损坏 / 容量不足
- 写入失败直接导致数据丢失。
未启用 AOF
- 如果只用 RDB 快照,丢失窗口会更大(几分钟甚至几小时)。
📌 总结
- AOF 不能保证绝对不丢数据,但配置
appendfsync always
基本能做到“几乎不丢”。 - 默认
everysec
,在大多数业务场景下可接受,最多丢 1 秒。 - 如果你想要 强一致性(比如金融系统),一般会用 AOF always + 主从复制 + 持久化存储(RAID/SSD)。
进程中的数据段
区域 | 主要内容 | 特点 |
---|---|---|
代码段(Text Segment / Code Segment) | 程序的机器指令,函数实现 | 只读,防止被修改;可共享(多个进程运行同一程序时共用代码段) |
数据段(Data Segment) | 已初始化的全局变量、静态变量 | 可读可写,程序启动时就分配 |
BSS 段(Block Started by Symbol) | 未初始化的全局变量、静态变量 | 程序启动时初始化为 0,占虚拟地址空间但不占可执行文件空间 |
堆(Heap) | 动态分配的内存(malloc 、new 等) | 从低地址往高地址增长;程序员手动申请/释放 |
栈(Stack) | 函数调用相关:局部变量、参数、返回地址 | 从高地址往低地址增长;由系统自动分配/释放 |
内存映射区(Memory Mapping Segment, mmap 区) | 动态库、共享内存、文件映射 | 介于堆和栈之间,用于高效 I/O 和共享 |
数字证书
1. 数字证书是什么
数字证书(Digital Certificate)本质上就是一个 由权威机构签发的身份证明,用于证明网站的真实身份,并携带网站的 公钥。
一般是由 CA(证书颁发机构) 签发的 X.509 格式证书。
2. 数字证书里包含什么
一个标准的 HTTPS 数字证书(X.509)大概包括:
- 公钥(Public Key)
- 持有者信息(域名、公司等)
- 签发机构信息(CA)
- 有效期(开始时间/截止时间)
- 证书用途(只能用于 SSL/TLS,还是也可用于签名等)
- 签名算法(SHA256 with RSA/ECDSA 等)
- 签名结果(CA 用私钥签发的哈希值)
3. 证书工作原理
HTTPS 证书的核心是 身份认证 + 公钥分发:
- 客户端访问服务器时,服务器会把证书发给客户端。
- 客户端用内置的 CA 公钥验证证书的签名是否正确,保证证书没被篡改。
- 确认证书中的域名和访问的域名一致,且证书在有效期内。
- 验证通过后,客户端取出服务器的 公钥,用于后续 TLS 握手(协商对称密钥)。
这样就能做到:
- 证明网站身份(防止中间人伪造网站)。
- 安全交换密钥(用公钥加密会话密钥)。
4. 信任链(Certificate Chain)
证书并不是单独存在的,而是形成 证书链:
- 根证书(Root CA):操作系统/浏览器内置信任。
- 中间证书(Intermediate CA):由根证书签发。
- 服务器证书:由中间 CA 签发,安装在网站服务器上。
客户端通过逐级验证签名,直到信任的根证书为止。
5. 常见面试问法 & 简答
Q: HTTPS 为什么需要数字证书? 👉 用来证明网站身份,并安全分发公钥,防止中间人攻击。
Q: 数字证书里存的是什么? 👉 包含网站公钥、域名信息、签发者、有效期,以及 CA 的签名。
Q: 怎么验证证书? 👉 浏览器用内置的 CA 公钥验证证书签名,并检查域名和有效期。
Q: 为什么要有证书链? 👉 根证书数量有限,不直接签发所有证书,通过中间证书层层签发提高安全性和可管理性。
MySQL Buffer Pool 有什么机制能够保证不会因为一次大查询把所有的数据都替换掉?
关键在 InnoDB Buffer Pool 的预读机制 + 老化淘汰策略:
Buffer Pool 使用 LRU 链表管理页面
- 热数据在链表的前半段(old、new 两个区域)。
- 新加载的数据页会先放到 old 区域(大约占 3/8)。
防止全表扫描冲掉热点页
- 如果是大查询(例如全表扫描),会短时间加载大量数据页。
- 这些页会先进入 old 区域,如果这些页很快没有再次被访问,就会被淘汰掉,不会影响 new 区域的热点数据。
访问提升机制
- 只有在 old 区域的页被访问超过一定阈值(默认 1 秒后再次访问),它才会提升到 new 区域,成为热点页。
- 这样就保证了 一次性的大量顺序访问,不会挤掉真正的热点数据。
📌 总结:
InnoDB 的 Buffer Pool 把 LRU 分为 old/new 两部分,新加载的数据先进入 old 区,如果没有被再次访问就会被淘汰,从而避免一次大查询把热点页全部替换掉。
小表驱动大表的原则有哪些?
- 小表:指数据量少、结果集小、能过滤数据的那个表。
- 大表:数据量大、行数多的那个表。
- 驱动:指在 嵌套循环(Nested Loop Join) 中,外层循环的表。
👉 原则:让小表作为驱动表,去匹配大表,减少大表的扫描次数。
🔹 MySQL Nested Loop Join 底层逻辑
假设:SELECT * FROM A JOIN B ON A.id = B.id
如果
A
是驱动表(外层循环),B
是被驱动表(内层循环):for each row in A: go to B and find matching rows (using index if possible)
如果
A
很大,B
很小 → 外层要循环很多次,代价高。如果
A
很小,B
很大 → 外层循环次数少,每次用索引去大表里查,效率高。
所以 小表驱动大表。
🔹 落地场景和优化点
索引配合
- 被驱动表(大表)必须有 连接字段的索引,否则就会全表扫描。
WHERE 条件过滤
优先让 WHERE 条件过滤后的“小结果集表”去驱动大表。
例如:
SELECT * FROM orders o JOIN users u ON o.user_id = u.id WHERE u.status = 'active';
- 如果
active
用户很少,就让users
先驱动orders
。
- 如果
SQL Hint / Join 顺序
MySQL 的优化器会自动决定驱动表,但你也可以用
STRAIGHT_JOIN
强制:SELECT STRAIGHT_JOIN * FROM small s JOIN big b ON s.id = b.sid;
原则
- 驱动表要尽量小 —— 外层循环次数越少越好。
- 被驱动表要能走索引 —— 内层查找必须高效,避免全表扫描。
- 先过滤再 JOIN —— 让过滤后的结果集(小表)去驱动大表。
数据对账
数据对账(Data Reconciliation)一般是指 两个系统或两个环节之间的数据进行比对,确保一致性 的过程。它常见于金融、电商、交易、账务、库存等场景。
目标
- 发现差异:识别出两个系统中的数据是否存在缺失、多余或数值不一致。
- 定位问题:明确差异来源(是系统 Bug、网络延迟、补偿未完成、人工操作错误等)。
- 修复数据:通过补偿、重试、人工干预等方式,恢复一致性。
📊 常见的对账场景
支付系统
- 内部交易流水 vs 第三方支付平台(支付宝、微信支付)的交易流水。
- 确保用户支付金额、订单状态一致。
库存系统
- 销售订单的扣减记录 vs 仓库库存实际数量。
- 避免“超卖”或“少卖”。
财务系统
- 业务账(交易记录) vs 财务账(记账系统)。
- 确保资金流与账务流对齐。
消息系统
- 发送端 Outbox 表 vs 消费端落库表。
- 确保消息没丢、没多。
⚙️ 数据对账的实现方式
全量对账
- 定期(如每日)导出数据,跑批比对。
- 优点:覆盖全面;缺点:开销大。
增量对账
- 按时间区间、流水号范围、主键 ID 范围来比对。
- 常用于 T+1(隔天)对账。
实时对账(流式比对)
- 在写入时同时写两份数据,异步比对。
- 优点:问题发现更快;缺点:复杂度高。
🛡️ 对账的幂等和补偿
- 幂等:保证同一笔差异修复不会重复影响业务。
- 补偿:发现差异 → 触发补偿逻辑(比如重新发消息、回滚、补写一笔交易)。
👉 总结一句话: 数据对账就是在分布式系统中,帮助我们验证“最终一致性”,发现并修复不同系统间的数据差异。
红包系统
模块 | 核心点 | 技术实现 |
---|---|---|
发红包 | 扣减用户余额,生成红包记录(普通 / 随机) | 账户服务 + 红包表记录,资金冻结 |
金额拆分 | 普通红包(均分)、拼手气红包(二倍均值法等) | 预拆分金额存入 Redis List |
抢红包 | 并发安全、金额不超发、用户只能抢一次 | Redis+Lua 原子操作、唯一索引、乐观锁 |
资金入账 | 抢到金额实时入账 | 账户服务 → 资金流水表,消息队列异步通知 |
过期退款 | 未抢完金额退回发红包人 | 定时任务(XXL-JOB)+ Outbox 补偿 |
存储设计 | 红包表、红包明细表、资金流水表 | MySQL(持久化)+ Redis(高并发抢红包) |
幂等性 | 防止重复抢、重复入账 | Redis Set/DB 唯一约束 + 幂等校验 |
对账 | 资金安全校验 | 红包账单 vs 账户流水 vs 第三方支付 |
高并发优化 | 缓存 & 异步化,热点隔离 | Redis 缓存、消息队列、分库分表 |
风控 | 防刷、防作弊、防超限 | 限流、设备指纹/IP 限制、金额次数上限 |
SETNX和SETEX区别?
命令 | 功能 | 是否带过期时间 | 是否有条件 | 典型场景 |
---|---|---|---|---|
SETNX | 只有键不存在时才设置 | ❌ | ✅(仅限不存在时) | 分布式锁、只初始化一次 |
SETEX | 设置键并加过期时间 | ✅ | ❌(无条件覆盖) | 缓存、验证码、临时数据 |
JVM内存不断增加如何处理?
类别 | 典型现象 | 可能原因 | 优化手段 |
---|---|---|---|
配置问题 | 内存一直涨,GC 频繁或回收慢 | 堆大小不合理,新生代/老年代比例不合适 | 调整 -Xms 、-Xmx 保持一致,合理设置 -XX:NewRatio |
GC 策略 | GC 回收效果不好,停顿时间长 | 使用不适合的垃圾回收器(CMS/Parallel) | JDK8 用 CMS/Parallel,JDK11+ 用 G1,JDK17+ 可用 ZGC/Shenandoah |
内存泄漏 | Full GC 后内存不下降 | 缓存无限增长、线程池队列过大、静态变量持有对象、DirectBuffer 未释放 | 设置缓存上限/TTL,控制线程池队列,用 WeakReference ,显式释放堆外内存 |
对象创建过多 | GC 频繁,新生代对象很多 | 频繁创建短生命周期对象(字符串拼接、JSON 解析等) | 使用对象池,StringBuilder 优化拼接,减少反序列化开销 |
堆外内存问题 | 堆内存正常但进程内存占用高 | Netty/NIO 分配 DirectBuffer 未释放 | 调用 System.gc() 触发 Cleaner,升级 Netty,限制 direct memory 大小 |
监控缺失 | 无法判断内存涨的原因 | 没有监控/报警 | jmap -histo 、jmap -dump ,用 MAT/Arthas 分析,接入 JMX、Prometheus+Grafana |
父线程数据存在threadlocal 子线程被污染咋办?
1. 为什么会“被污染”
- ThreadLocal 原理:数据存储在
Thread
对象的ThreadLocalMap
里。 - 父线程设置了值后,如果子线程是新建的,不会继承父线程的值;
- 但如果用了 线程池(线程复用),线程可能带着之前的
ThreadLocal
数据 → 下一个任务就被“污染”了。
2. 解决办法
方案 | 做法 | 优缺点 |
---|---|---|
手动清理 | 每次用完 ThreadLocal.remove() | 简单直接,必须养成习惯,否则容易遗忘 |
使用 InheritableThreadLocal | 子线程会继承父线程的值 | 只能在新建线程时继承,线程池复用时还是有问题 |
使用 TransmittableThreadLocal (TTL) | 阿里开源的 TTL 库,能在 线程池/异步场景下自动传递和清理数据 | 最彻底,适合微服务/异步任务场景 |
避免滥用 ThreadLocal | 改用显式参数传递(例如 RequestContext) | 代码侵入大,但更清晰 |
3. 总结:
- 普通场景:用完记得
remove()
。 - 线程池/异步场景:用 TransmittableThreadLocal。
限流怎么做?服务级别和接口级别?
1. 接口级别限流
- 令牌桶算法:为每个接口分配独立的令牌桶,控制请求频率
- 滑动窗口:统计时间窗口内的请求数量,超过阈值则限流
- 实现方式:使用Redis + Lua脚本保证原子性,或使用Guava RateLimiter
2. 服务级别限流
- 服务总QPS限制:对整个服务的总请求量进行限制
- 资源维度限流:基于CPU、内存、数据库连接数等资源使用情况
- 实现方式:通过Nginx、Gateway网关层实现,或应用层监控
3. 流量分发策略 除了负载均衡,还可以:
- 一致性哈希:根据用户ID或业务ID进行路由,保证同一用户请求到同一节点
- 权重路由:根据节点性能动态调整权重
- 熔断降级:当某个节点异常时,自动切换到健康节点
- 灰度发布:新版本逐步放量,降低风险
ORM和MyBatis的区别?
- Hibernate 有 “全自动屏蔽 SQL” 的设计理念,与现代开发中 “需要灵活控制 SQL、快速响应业务变化、重视性能优化” 的需求逐渐脱节。而 MyBatis 和 MyBatis Plus 以 “半自动化” 为核心,在 “开发效率” 和 “SQL 控制权” 之间找到了更优的平衡点,因此更受开发者青睐。
- 传统 ORM 会全自动管理对象与数据库的映射,包括复杂关联查询(如一对一、一对多)的 SQL 生成,开发者几乎无需接触 SQL; 而 MP 虽然简化了基础操作,但对于复杂业务逻辑(如多表关联、子查询等),仍需要开发者主动介入 SQL 编写(或通过条件构造器手动拼接),本质上还是「SQL 映射」的增强,而非完全的「对象关系映射」。
监控方面有哪些工具和指标 都是如何做监控的?
监控是保障系统稳定运行、快速定位问题的核心手段,通常需覆盖基础设施、应用性能、业务指标三大层面。不同层面的监控目标、核心指标及工具选型差异较大,以下从「监控分层」「核心指标」「常用工具」「监控实现流程」四个维度详细说明:
一、监控分层与核心指标
监控的本质是“采集关键数据→分析异常→触发告警→定位问题”,不同层级关注的“关键数据”不同,需针对性设计指标。
1. 基础设施监控(底层保障)
针对服务器、网络、数据库、中间件等硬件/基础软件,核心目标是确保“底层资源可用、性能不瓶颈”。
核心指标
监控对象 | 关键指标 | 指标意义(异常影响) |
---|---|---|
服务器 | - CPU:使用率(%)、负载(load 1/5/15)、就绪队列长度 - 内存:已用率(%)、缓存/缓冲大小、Swap使用率 - 磁盘:使用率(%)、IOPS(读写次数/秒)、吞吐量(MB/s)、IO等待时间 - 进程:关键进程存活状态、CPU/内存占用 | CPU负载高→应用卡顿;内存不足→OOM崩溃;磁盘满→无法写入日志/数据;进程挂掉→服务不可用 |
网络 | - 带宽:入网/出网速率(Mbps)、带宽使用率 - 延迟(Latency):Ping延迟、TCP连接延迟 - 丢包率(Packet Loss)、TCP重传率 - 端口:关键端口(如80/443/3306)监听状态 | 带宽跑满→请求超时;延迟高→用户体验差;丢包→数据传输错误;端口未监听→服务无法访问 |
数据库 | - 连接:活跃连接数、最大连接数使用率、空闲连接数 - 性能:QPS(查询/秒)、TPS(事务/秒)、慢查询数(如>1s的SQL)、锁等待时间 - 存储:表空间使用率、索引命中率、缓存命中率(如MySQL的InnoDB Buffer Pool) - 复制:主从延迟(秒)、复制状态 | 连接满→新请求被拒;慢查询多→响应延迟;缓存命中率低→IO压力大;主从延迟高→数据不一致 |
中间件 | - 消息队列(Kafka/RabbitMQ):队列长度(堆积数)、消费延迟(消息生产到消费的时间)、分区健康度、生产/消费速率 - 缓存(Redis):内存使用率、Key过期数、命中率、QPS、集群主从同步状态 - 容器(Docker/K8s):Pod存活状态、CPU/内存限制使用率、Node节点健康度 | 队列堆积→业务延迟;消费延迟高→数据处理滞后;Redis命中率低→数据库压力大;Pod挂掉→服务实例减少 |
常用工具
工具类型 | 代表工具 | 特点与适用场景 |
---|---|---|
服务器/网络监控 | - Prometheus + Grafana(主流) - Zabbix(传统企业级) - Nagios(老牌开源) | Prometheus:时序数据库,适合存储监控数据;Grafana:可视化仪表盘,支持自定义图表;Zabbix:功能全,支持自动发现设备,适合大规模集群 |
数据库监控 | - Percona Monitoring and Management(PMM,MySQL/MongoDB专用) - RedisInsight(Redis专用) - pgHero(PostgreSQL专用) | PMM:集成慢查询分析、性能趋势,适合MySQL优化;RedisInsight:可视化Redis数据结构、集群状态 |
中间件监控 | - Prometheus + 专属Exporter(如Kafka Exporter、RabbitMQ Exporter) - Kafka Eagle(Kafka专用) - kubectl + Prometheus(K8s监控) | Exporter:将中间件指标转换为Prometheus可采集格式;Kafka Eagle:支持队列堆积、消费组监控 |
2. 应用性能监控(APM,代码与链路层)
针对应用程序(如Java/Python/Go服务),核心目标是“定位代码级瓶颈、追踪分布式链路问题”,需覆盖“请求链路、代码性能、资源消耗”。
核心指标
指标类别 | 关键指标 | 指标意义(异常影响) |
---|---|---|
请求链路 | - 响应时间(RT):平均RT、P95/P99 RT(95%/99%请求的耗时上限)、最大RT - 吞吐量(TPS/QPS):每秒处理的请求数 - 错误率:失败请求数/总请求数(如HTTP 5xx/4xx占比) - 链路追踪:请求经过的服务/接口调用链、各节点耗时占比 | RT高→用户体验差;错误率高→业务不可用;链路耗时不均→定位瓶颈服务(如某个微服务耗时占80%) |
代码性能 | - 接口耗时:单个接口的平均耗时、慢接口数(如>500ms) - 方法耗时:核心业务方法(如订单创建、支付)的执行时间 - 异常数:未捕获异常次数、自定义业务异常次数 | 接口/方法耗时高→代码效率低;异常数多→逻辑bug多 |
JVM/运行时 | - JVM(Java应用):GC次数(Young/Old GC)、GC耗时、堆内存(Eden/Survivor/Old区)使用率、非堆内存(Metaspace)使用率 - 线程池:活跃线程数、队列等待数、拒绝任务数 | GC频繁→应用卡顿;堆内存满→OOM崩溃;线程池队列满→请求被拒 |
常用工具
工具类型 | 代表工具 | 特点与适用场景 |
---|---|---|
分布式链路追踪 | - SkyWalking(开源,国内主流) - Pinpoint(开源,字节码埋点) - Jaeger(Uber开源,兼容OpenTelemetry) | SkyWalking:支持多语言,链路可视化清晰,可关联日志/指标;Pinpoint:无侵入埋点,适合不修改代码的场景 |
应用性能分析 | - Arthas(Alibaba开源,Java诊断) - New Relic(商业,全栈APM) - Datadog(商业,云原生APM) | Arthas:在线诊断Java应用,查看方法耗时、GC情况,适合生产环境排障;New Relic:支持云服务、移动端全链路监控 |
日志与错误监控 | - ELK Stack(Elasticsearch+Logstash+Kibana) - Loki(轻量日志聚合,搭配Grafana) - Sentry(错误追踪,实时告警) | ELK:日志收集→存储→分析,适合大规模日志检索;Sentry:捕获代码异常(如前端JS错误、后端Java异常),定位到具体行号 |
3. 业务监控(顶层价值层)
针对业务场景(如电商、支付、社交),核心目标是“感知业务健康度、发现业务异常”,指标需与“用户体验、业务收入”强关联。
核心指标
业务场景 | 关键指标 | 指标意义(异常影响) |
---|---|---|
电商平台 | - 核心流程:下单量、支付成功率(支付成功数/下单数)、订单超时数、库存不足次数 - 用户行为:注册量、登录数、DAU(日活跃用户)/MAU(月活跃用户)、转化率(浏览→加购→下单) - 收入相关:GMV(成交总额)、客单价、退款率 | 支付成功率低→收入损失;订单超时多→用户投诉;DAU下降→用户流失 |
支付系统 | - 交易成功率、接口响应时间(如支付回调耗时)、失败交易数(如风控拒绝、渠道超时)、对账差异数 | 交易成功率低→业务中断;对账差异→财务风险 |
API服务 | - 接口调用量、成功率、响应时间(按接口维度统计)、限流触发次数(如QPS超过阈值) | 接口成功率低→下游服务异常;限流频繁→服务压力过载 |
常用工具
工具类型 | 代表工具 | 特点与适用场景 |
---|---|---|
业务指标采集 | - Prometheus + 自定义埋点(如通过SDK上报业务指标) - 埋点平台(如百度统计、友盟,前端/移动端) | 自定义埋点:在业务代码中插入指标上报逻辑(如订单创建后上报“order_count=1”);友盟:适合APP用户行为统计 |
可视化与告警 | - Grafana(自定义业务仪表盘) - 业务监控平台(如内部开发的监控系统) - 告警渠道(钉钉/企业微信/Slack + 告警平台如AlertManager) | Grafana:支持按业务维度展示(如“支付成功率仪表盘”);AlertManager:支持告警分级(紧急/普通)、抑制重复告警 |
日志分析 | - ELK Stack - ClickHouse + Superset(海量业务日志分析) | ClickHouse:列式存储,适合快速查询大规模业务日志(如分析“近1小时超时订单分布”) |
二、监控实现的核心流程(闭环管理)
监控不是“只采集数据”,而是要形成“采集→展示→告警→排查→优化”的闭环,否则数据只是“无用的数字”。具体步骤如下:
1. 需求分析:明确“监控什么”
- 按角色确定目标:运维关注“基础设施可用”,开发关注“应用无bug、链路无瓶颈”,产品关注“业务指标正常(如订单量不暴跌)”;
- 排除“无效指标”:避免监控冗余数据(如非关键进程的CPU占用),聚焦“影响系统稳定/业务收入”的核心指标(如支付成功率、服务器CPU负载)。
2. 指标选型与采集:确保“数据准确”
- 指标定义:明确指标计算方式(如“支付成功率=支付成功数/下单数,统计粒度1分钟”);
- 数据采集:
- 无侵入采集:通过Exporter(如Node Exporter采集服务器指标)、日志解析(如ELK采集接口日志);
- 侵入式采集:在业务代码中埋点(如通过SkyWalking SDK上报链路数据)、调用API拉取(如从支付网关拉取交易数据);
- 采集频率:核心指标(如RT、错误率)10秒/次,非核心指标(如磁盘使用率)1分钟/次,避免过度消耗资源。
3. 可视化展示:让“数据可读懂”
- 分角色设计仪表盘:
- 运维仪表盘:服务器CPU/内存、数据库连接数、K8s Pod状态;
- 开发仪表盘:接口RT/P95、JVM GC、慢查询TOP10;
- 产品仪表盘:DAU、下单量、支付成功率趋势图;
- 工具选型:优先用Grafana(支持多数据源、自定义图表),业务指标可搭配内部平台。
4. 告警配置:实现“异常早发现”
- 阈值设定:基于历史数据定合理阈值(如CPU使用率>80%持续5分钟告警,支付成功率<99.5%告警),避免“误报”(如瞬时峰值触发告警);
- 告警分级:
- P0(紧急):服务不可用、支付成功率暴跌(需10分钟内响应);
- P1(重要):CPU负载高、队列轻微堆积(需30分钟内响应);
- P2(普通):非核心接口RT升高(工作时间内响应);
- 告警渠道:优先用即时通讯(钉钉/企业微信),紧急告警叠加电话/短信(如通过阿里云短信接口)。
5. 问题排查与优化:形成“闭环”
- 定位问题:结合多维度数据(如“支付成功率低”→查支付接口日志→发现数据库慢查询→看慢查询SQL的索引);
- 优化与复盘:解决问题后(如给SQL加索引),监控指标是否恢复正常;定期复盘告警原因(如频繁GC→优化代码内存泄漏),避免重复问题。
三、总结
监控的核心逻辑是“分层覆盖、聚焦核心、闭环管理”:
- 底层(基础设施)保障“资源可用”,用Prometheus+Grafana/Zabbix;
- 中层(应用)定位“代码/链路瓶颈”,用SkyWalking/Arthas;
- 顶层(业务)感知“用户与收入影响”,用自定义埋点+Grafana/业务平台;
- 最终通过“采集→展示→告警→排查”的闭环,实现“事前预警、事中定位、事后优化”,保障系统稳定与业务正常。
Spring Boot Actuator 如何实现监控?
端点(Endpoint) | 核心功能 | 访问方式(HTTP) | 关键说明/注意事项 |
---|---|---|---|
/actuator/health | 检查应用及依赖组件(数据库、Redis等)的健康状态(UP/DOWN/UNKNOWN) | GET | 1. 默认返回简洁状态,配置 management.endpoint.health.show-details=always 显示详情;2. 可实现 HealthIndicator 自定义业务健康检查(如订单数据量阈值) |
/actuator/info | 展示应用自定义元数据(版本、名称、Git信息等) | GET | 1. 需通过 info.* 前缀在配置文件中定义数据(如 info.app.version=1.0.0 );2. 引入 spring-boot-starter-git 可自动拉取Git提交信息 |
/actuator/metrics | 提供应用运行指标(JVM内存、CPU、HTTP请求、数据库连接等) | GET(所有指标) GET /{指标名}(具体指标) | 1. 示例:/actuator/metrics/jvm.memory.used 查看JVM已用内存;2. 支持按维度筛选(如HTTP请求按接口、状态码分组) |
/actuator/env | 展示应用所有环境配置(系统变量、配置文件、环境变量等) | GET | 1. 会暴露敏感信息(如数据库密码),生产环境必须通过Spring Security限制访问; 2. 可查看活跃Profiles(如dev/prod) |
/actuator/loggers | 查看/动态修改应用日志级别(无需重启应用) | GET /{包名}(查级别) POST /{包名}(改级别) | 1. 修改示例:POST请求体 {"configuredLevel": "DEBUG"} 调整指定包日志级别;2. 支持细粒度控制(如 com.example.service 包) |
/actuator/httptrace | 记录最近HTTP请求明细(方法、路径、状态码、响应时间、客户端IP等) | GET | 1. 默认保留100条记录; 2. 需引入 spring-boot-starter-web 才会自动配置;3. 适合排查近期接口调用问题 |
/actuator/beans | 展示Spring容器中所有Bean的信息(类型、依赖关系、作用域等) | GET | 1. 辅助理解Spring Bean的加载和依赖情况; 2. 生产环境建议限制访问(避免暴露内部结构) |
/actuator/mappings | 展示所有URL映射关系(控制器接口、请求方法、处理类等) | GET | 1. 快速定位接口对应的Controller方法; 2. 支持Spring MVC/WebFlux接口映射查看 |
/actuator/threaddump | 生成线程快照(类似JVM的jstack命令),用于排查线程阻塞、死锁、高CPU问题 | GET | 1. 响应为JSON格式,可搜索关键词(如 BLOCKED )定位异常线程;2. 生产环境排查线程问题的核心端点 |
/actuator/shutdown | 通过HTTP请求关闭应用(优雅停机) | POST | 1. 默认禁用,需配置 management.endpoint.shutdown.enabled=true ;2. 生产环境慎用,建议配合认证授权 |
核心总结
- 开箱即用:无需手动开发监控逻辑,添加依赖+简单配置即可启用;
- 扩展性强:可自定义健康检查、指标采集,也可集成Prometheus(拉取
/actuator/prometheus
指标)、Grafana(可视化); - 安全优先:敏感端点(
/env
//loggers
//shutdown
)需通过Spring Security做认证授权,避免信息泄露或误操作。
类的实例化
- 类的实例化就是“用类的模板,在堆上开辟一块空间,创建一个具体对象,并返回引用”的过程。
步骤 | 操作/描述 | 内存位置 | 说明 |
---|---|---|---|
1 | 加载类 | 方法区(Method Area) | JVM 加载类字节码,如果还未加载,则先加载、连接、初始化。 |
2 | 分配内存 | 堆(Heap) | 为对象实例分配内存,大小由类成员变量决定。 |
3 | 默认初始化 | 堆 | 对象成员变量被赋默认值(int → 0,boolean → false,引用 → null)。 |
4 | 调用构造器 | 堆 | 执行构造器代码,给成员变量赋初始值或执行逻辑。 |
5 | 返回引用 | 栈/堆(引用变量在栈) | 返回对象引用给变量,变量存储在栈中,引用指向堆上的对象。 |
6 | 使用对象 | 堆 + 栈 | 可以通过引用访问对象成员和方法,方法存储在方法区,不占实例内存。 |
QPS如何统计?
场景 | 推荐工具/方法 | 优势 |
---|---|---|
本地开发调试 | 应用内埋点(拦截器/中间件) | 简单、无依赖 |
网关层全局监控 | Nginx stub_status | 无需侵入业务代码 |
分布式微服务线上监控 | Prometheus+Grafana | 实时、可告警、支持历史查询 |
性能压测 | JMeter/Gatling | 模拟高并发,测最大QPS |
事后日志分析 | ELK Stack | 排查历史QPS异常原因 |
监控组件
- spring-boot-starter-actuator:Actuator提供生产就绪的监控基础设施,暴露各种管理和监控端点。它是应用与外部监控系统交互的窗口,但本身不负责指标数据的收集。
- micrometer-core:Micrometer是真正的指标收集引擎,负责收集JVM、HTTP、数据库等各种指标数据它提供统一的API让开发者可以创建自定义指标(类似于一个门面),是整个监控体系的数据生产者。
- micrometer-registry-prometheus:Prometheus Registry专门负责将Micrometer收集的指标数据转换为Prometheus格式。它创建/actuator/prometheus端点,让Prometheus服务器可以直接拉取标准格式的监控数据。Prometheus可以定期访问/actuator/prometheus端点拉取指标类数据,实现对Spring Boot应用的持续监控和告警。
总结一下作用:
- spring-boot-starter-actuator=端点提供者(开门的)
- micrometer-core=数据收集者(干活的)
- micrometer-registry-prometheus=格式转换者(翻译的)
- Prometheus=数据拉取者(消费的)
整合后的 Spring Boot + Actuator + Micrometer + Prometheus 交互流程
阶段/组件 | Spring Boot 应用 | Actuator(端点提供者) | Micrometer(数据收集者) | Prometheus Registry(格式转换者) | Prometheus 服务器 |
---|---|---|---|---|---|
启动阶段 | 启动并自动配置内部组件 | ← 被 Spring Boot 自动引入并初始化 | ← 被 Spring Boot 自动引入并初始化 | ← 检测到依赖后,被自动配置(与 Micrometer 联动) | -(等待后续数据拉取) |
运行时数据收集 | 业务事件触发(如 HTTP 请求、方法调用等) | -(无直接动作,依赖下层组件) | 持续收集各类指标数据(JVM、业务等) | 同步 Micrometer 收集的数据 → 转换为 Prometheus 格式 | -(无直接动作,等待主动拉取) |
外部访问(Prometheus 拉取数据) | -(无直接动作,通过 Actuator 暴露端点) | ← 接收 Prometheus 发起的 GET /actuator/prometheus 请求 | ← 向 Actuator 提供已被转换为 Prometheus 格式的指标数据 | ← 向 Actuator 输出格式化后的指标 | → 发起 GET /actuator/prometheus 请求拉取数据 |
流程核心逻辑解释
- 启动联动:Spring Boot 利用自动配置机制,依次初始化 Actuator(负责暴露监控端点)、Micrometer(负责采集指标);Micrometer 感知到 Prometheus 相关依赖后,自动配置
Prometheus Registry
以完成“指标 → Prometheus 格式”的转换。 - 运行时采集:应用运行过程中,只要有业务行为(如接口被调用、定时任务执行),Micrometer 就会实时采集对应的性能/业务指标;同时,
Prometheus Registry
会持续将这些指标同步转换为 Prometheus 能识别的格式。 - 外部拉取:Prometheus 服务器会按照配置的“采集周期”,主动向 Spring Boot 应用的
Actuator
端点(/actuator/prometheus
)发起 HTTP 请求;Actuator 从 Micrometer 侧获取已格式化的指标数据后,返回给 Prometheus 服务器,供其存储、查询与可视化(如对接 Grafana)。
MySQL版本链堆积?
原因
长事务
- 比如一个事务开启了很久不提交,其他事务更新的数据版本都要保留,不能清理。
- 导致 Undo Log 一直无法回收,历史版本链越来越长。
大批量更新
- 一次性更新大量数据,产生大量 Undo 版本。
读一致性快照(快照读)
- 当有长时间的快照读(
SELECT
),Undo Log 中旧版本也无法清理。
- 当有长时间的快照读(
危害
- Undo 表空间膨胀(即
ibd
文件变大) - 查询性能下降(走版本链查到符合快照的数据更慢)
- 可能引发锁等待
解决办法
避免长事务 ✅
- 及时提交事务,不要在事务里做太多非数据库操作(比如 HTTP 请求)。
- 可以开启
innodb_undo_log_truncate
(MySQL 5.7+),支持自动回收 Undo 空间。
控制大事务 ✅
- 大更新/删除操作要拆小批次执行(比如每次 1000 行)。
优化读一致性 ✅
- 尽量减少长时间的快照读,必要时用 当前读(for update / lock in share mode) 替代。
监控和手工处理 ✅
- 监控
information_schema.innodb_trx
看是否有长事务。 - 如果 Undo 表空间太大,可以通过 导出 + 导入 或
OPTIMIZE TABLE
回收空间。
- 监控
类比一句话
版本链堆积就像垃圾一直没清理:
- 原因:有人(长事务/快照读)一直不让你倒垃圾。
- 危害:家里越来越乱(Undo 空间膨胀,查询慢)。
- 解决:勤打扫(快提交、拆小事务)、开垃圾回收机制(Undo truncate)。
排查思路
一、日志排查
- 应用日志:查看应用服务器的日志,重点关注超时或宕机发生时段的日志,寻找异常堆栈信息、错误提示(如空指针异常、数据库连接异常等),这些能快速定位到代码层面的问题。
- 系统日志:检查操作系统的系统日志,看是否有硬件故障(如磁盘、内存等问题)、资源耗尽(CPU、内存使用率异常飙升)等相关记录。
- 中间件日志:如果使用了中间件(如Web服务器、消息队列等),查看中间件的日志,比如Tomcat日志,确认中间件是否出现异常,是否有请求处理超时等情况。
二、资源监控
- CPU:查看CPU使用率,是否在超时或宕机时出现过高(接近100%)的情况,若有,进一步排查是哪个进程占用了大量CPU,可能是应用程序自身逻辑问题,也可能是其他进程干扰。
- 内存:监控内存使用情况,检查是否存在内存泄漏(内存占用持续上升不释放),或者内存不足导致系统崩溃。可通过内存分析工具(如Java的JProfiler等)分析应用内存状态。
- 磁盘:检查磁盘空间是否不足,磁盘I/O是否过高,磁盘故障也可能导致系统响应异常甚至宕机。
- 网络:查看网络带宽使用情况,是否有网络波动、丢包等情况,网络问题可能导致服务间通信超时。
三、数据库排查
- 慢查询:检查数据库的慢查询日志,找出执行时间长的SQL语句,分析这些SQL是否存在优化空间(如缺少索引、查询逻辑复杂等),慢SQL可能导致数据库响应慢,进而引发应用超时。
- 连接池:查看数据库连接池的配置和使用情况,确认连接池是否被耗尽,或者连接池参数设置不合理(如最大连接数过小),导致应用无法获取数据库连接而超时。
- 数据库性能:监控数据库的整体性能,如数据库服务器的CPU、内存、磁盘I/O等是否过高,是否存在锁表、死锁等情况影响查询和操作效率。
四、代码与业务逻辑排查
- 代码逻辑:检查筛选或创建流程相关的代码,看是否存在死循环、递归过深、资源未及时释放(如文件流、数据库连接等)等问题,这些问题可能导致应用性能下降甚至崩溃。
- 并发处理:查看系统的并发处理机制,是否存在线程安全问题,高并发情况下是否因为同步锁等导致线程阻塞,进而引发超时。
- 第三方依赖:如果流程中依赖了第三方服务(如外部API、缓存服务等),检查第三方服务是否稳定,是否存在响应慢或不可用的情况,影响整个流程的执行。
五、压力测试与复现
通过压力测试工具,模拟高并发场景,尝试复现超时或宕机问题,以便更准确地定位问题所在,比如在高并发下是数据库瓶颈、还是应用服务器资源不足,或者是代码逻辑在高负载下出现问题。
CompletableFuture任务编排的底层原理?
CompletableFuture 任务编排的本质是:通过 Completion 回调链记录任务依赖关系,基于状态变更驱动回调执行,结合线程池实现异步调度。具体可概括为:
- 每个编排方法(如 thenApply)生成新的 CompletableFuture,并将后续任务封装为 Completion 节点,加入前序任务的回调链;
- 前序任务完成时,触发回调链遍历,按顺序执行后续任务,并传递结果或异常;
- 通过 Async 方法指定线程池,实现任务的异步并行执行;
- 多任务聚合(allOf/anyOf)通过计数器跟踪依赖任务的完成状态,满足条件时触发自身完成。 这种设计让开发者无需手动管理线程和回调顺序,即可实现复杂的异步任务编排,大幅简化了并发编程的复杂度。
阿里的ARMS和Arthas?
一、本质与定位
ARMS(Application Real-Time Monitoring Service)
- 阿里云的应用性能管理(APM)服务,属于云端监控工具,提供从前端到后端的全链路性能监控和业务指标分析。
- 核心价值:通过实时数据可视化、异常报警和调用链追踪,帮助用户长期监控应用健康状态,预防问题并优化性能。例如,它可以展示接口响应时间、数据库调用耗时、服务器资源利用率等宏观指标,并支持自定义业务监控逻辑。
- 适用场景:日常运营中持续观察应用表现,快速发现潜在瓶颈或异常趋势。
Arthas
- 阿里巴巴开源的Java诊断工具,属于本地诊断工具,通过字节码增强技术在不重启JVM的情况下深入分析应用运行时状态。
- 核心价值:在问题发生时,通过命令行或白屏化界面(如ARMS集成的版本)进行微观诊断,例如追踪方法调用链路、查看线程堆栈、实时修改类加载行为等。
- 适用场景:临时排查线上故障,例如定位某个接口突然变慢的具体原因,或验证代码变更的即时效果。
二、功能对比
维度 | ARMS | Arthas |
---|---|---|
数据范围 | 全局视角:涵盖前端、后端、数据库、服务器等全链路数据。 | 局部视角:聚焦单个JVM进程的运行时细节,如特定方法的参数、返回值、异常信息等。 |
操作方式 | 配置SDK后自动采集数据,通过控制台或API查看结果,支持报警规则设置。 | 需手动连接目标进程,通过命令(如trace 、watch 、thread )主动触发诊断动作。 |
技术实现 | 依赖SDK采集数据,通过云端计算和存储实现统计分析。 | 基于字节码增强技术(如ASM),直接注入代码到目标进程中获取实时信息,对应用侵入性较低但需临时开启。 |
集成关系 | ARMS专家版深度集成Arthas,用户可通过ARMS一键启用Arthas诊断功能,且诊断结果可关联ARMS的调用链上下文(如TraceID)。 | 独立工具,也可通过ARMS平台便捷使用(如无需手动安装JDK环境)。 |
典型功能 | - 调用链拓扑图 - 数据库慢SQL分析 - 服务器CPU/内存监控 - 自定义业务指标报警。 | - 方法调用耗时追踪(trace )- 线程堆栈分析( thread )- 类加载信息查询( sc )- 实时修改类字段( jad/redefine )。 |
三、协同工作
ARMS集成Arthas
- ARMS专家版提供Arthas诊断入口,用户可在发现异常时直接从ARMS控制台启动Arthas,快速定位问题根源。例如,通过ARMS的调用链追踪发现某个接口耗时异常后,可一键跳转至Arthas界面,追踪该接口的具体方法调用路径。
- 这种集成避免了单独部署Arthas的复杂性,且诊断结果可与ARMS的监控数据联动(如查看TraceID对应的完整链路)。
分工互补
- ARMS:负责发现问题(例如报警“接口响应时间超过阈值”)。
- Arthas:负责定位问题(例如通过
trace
命令找出接口内部哪个方法耗时最长)。 - 两者结合形成“监控-诊断-修复”的闭环,显著提升故障排查效率。
四、总结
- ARMS是长期监控的眼睛,帮助用户从宏观上掌握应用状态,预防问题发生。
- Arthas是临时急救的手术刀,在问题发生时深入剖析细节,快速定位根因。
- 选择建议:
- 日常监控和业务指标分析 → ARMS。
- 线上故障排查和代码级调试 → Arthas(推荐通过ARMS集成版使用,简化操作)。
两者并非替代关系,而是通过功能互补,共同构建完整的应用可观测性体系。
取一个文件的过程
用户通过应用程序发起文件读取 → 应用程序调用系统调用(如 read)→ 操作系统文件系统解析路径、查索引节点,确定数据位置 → 存储管理和驱动程序发起磁盘 I/O,读取物理块数据 → 数据从磁盘到内核缓冲区,再复制到应用程序缓冲区 → 应用程序拿到文件内容。
HTTP的keep-alive和TCP的keep-alive?
- HTTP Keep-Alive 是 “连接复用工具”,目的是提高应用层 HTTP 通信的效率,减少连接建立开销。
- TCP Keepalive 是 “连接检测工具”,目的是维护传输层 TCP 连接的有效性,避免无效连接占用资源。
redisson分布式锁为啥要用lua而不是事务
Redisson 分布式锁使用 Lua 脚本而非事务,核心原因是 Lua 能保证分布式锁操作的原子性和逻辑完整性,而 Redis 事务在分布式锁场景下存在关键局限:
原子性保证更彻底:Lua 脚本在 Redis 中以单命令方式执行,整个脚本的所有操作要么全部完成,要么全部不执行,不会被其他命令打断,完美保证锁的获取、释放、续约等逻辑的原子性(如判断锁是否持有再释放的复合操作)。
避免事务的局限性:Redis 事务依赖
MULTI
/EXEC
,但无法在事务中根据前一个命令的结果动态决定后续操作(缺乏条件判断能力),且事务执行中若某命令失败,其他命令仍会继续执行,无法满足分布式锁对操作结果的严格一致性要求。简化逻辑与性能:用 Lua 可将多个 Redis 命令(如
GET
+DEL
、HSET
+PEXPIRE
)封装成一个脚本,减少网络交互次数,同时通过脚本内的条件判断(如检查锁的持有者)实现复杂逻辑,比事务更灵活高效。