内容;Seata AT、Sentinel、QLExpress、DockerCompose 四大核心技术 + Redis 底层、Caffeine+Redis 二级缓存、缓存三大问题
Seata AT 分布式事务
AT 模式三阶段完整执行流程
一阶段(执行业务本地事务)
执行业务 SQL(新增 AI 短信记录、扣商户额度),同步生成 undo 回滚日志存入库,提交本地 MySQL 事务,给对应业务行加锁;此时数据入库,但全局事务未结束,外部其他服务看不到本次修改。
二阶段提交
TC 事务协调器通知分支提交,服务删除本地 undo 日志,释放行锁,数据对外完全可见。
二阶段回滚
TC 通知分支回滚,读取 undo 日志生成反向补偿 SQL,撤销一阶段所有增删改数据,释放行锁,恢复到执行前状态。
项目落地业务场景
批量 AI 短信生成、定时营销任务需要同时操作三张数据:短信生成记录表、商户额度表、Redis 缓存额度。
若不加 Seata AT,会出现:短信记录插入成功、商户额度扣减 SQL 报错,导致商户免费下发短信,数据不一致。
添加@GlobalTransactional全局事务注解,保证多库操作原子性,要么全成功、要么全撤销。
AT / TCC / SAGA 三种分布式事务对比
| 方案 | 代码侵入度 | 锁机制 | 适用场景 | 项目选用原因 |
|---|---|---|---|---|
| AT | 极低,仅注解 | 行锁,同步锁 | MySQL 同步数据库操作 | 短信平台以 MySQL 为主,开发成本低 |
| TCC | 极高,需手写 Try/Confirm/Cancel | 无数据库锁,业务自定义 | 对接第三方支付、非关系型存储 | 项目几乎不用,开发工作量大 |
| SAGA | 中等,手动写补偿逻辑 | 无锁,最终一致性 | 超长异步流程、跨异构系统 | 批量异步任务极少使用,一致性弱 |
线上高频失效踩坑点
- 方法不是 public、同类内部调用全局事务失效;
- 多数据源未配置 Seata 代理数据源;
- MQ 异步、子线程无法传递全局事务上下文;
- undo 日志长期堆积,需定时清理 undo 表。
项目排查和问题处理
1.使用 Seata AT 分布式事务,出现「短信记录入库成功,商户额度扣减失败」数据不一致,请写出完整排查思路。
第一步:检查业务方法上是否添加 @GlobalTransactional 全局事务注解,无注解则完全不生效;
第二步:校验当前事务方法访问修饰符,必须是 public;如果是 private /protected/ 同类内部自调用,Spring AOP 无法生成代理,全局事务直接失效;
第三步:查看日志,定位扣减额度 SQL 抛出的异常;确认异常是否被 try-catch 捕获且没有主动抛出,被吞掉的异常不会触发 Seata 回滚;
第四步:核对项目多数据源配置,是否全部使用 Seata 代理数据源,未代理的库操作不参与全局事务;
第五步:查询 undo_log 回滚日志表,确认一阶段是否正常生成回滚记录;
第六步:排查是否存在异步 MQ、新开子线程执行扣减逻辑,子线程无法传递全局事务上下文,事务隔离;
第七步:观察 TC 事务协调器日志,查看二阶段是提交指令还是回滚指令,定位异常分支。
2.简述 Seata AT 一阶段、二阶段提交、二阶段回滚分别做了哪些核心操作?
一阶段
执行业务增删改 SQL,同步生成 undo 日志(里面存反向补偿 SQL),提交本地 MySQL 事务,同时对操作的数据加行锁,此时外部看不到本次修改的数据。
二阶段提交
TC 事务协调器收集所有 RM 分支执行状态,全部无异常,则通知各分支删除本地 undo 日志、释放行锁,修改后的数据对外可见。
二阶段回滚
只要任意分支执行异常,TC 下发回滚指令;RM 读取 undo 日志里的反向 SQL 执行数据恢复,完成后释放行锁,undo 日志会做归档清理。
3.结合咱们营销短信平台业务,说一说项目为什么选择 Seata AT,而不用 TCC、SAGA?
我们平台核心业务都是 MySQL 同步落库场景,所以优先选用 Seata AT:
- AT 侵入极低 ,仅需在方法上加
@GlobalTransactional注解,框架自动生成 undo 日志实现事务控制,几乎不用改动原有业务代码,开发维护成本低; - TCC 侵入性极强,需要手动编写 Try、Confirm、Cancel 三段逻辑,代码改动量大,适合对接第三方支付、非数据库异构场景,我们项目没有这类需求;
- SAGA 无锁、依靠手动补偿,适合超长异步业务流程,一致性偏弱;我们短信下发、AI 文案生成都是短同步事务,追求强一致,完全不匹配 SAGA 适用场景。
Sentinel 限流熔断
熔断三大状态流转
- 关闭 Closed:接口正常运行,持续统计 QPS、超时率、异常比例;指标未超阈值,请求直接放行。
- 打开 Open:异常 / 超时占比超过阈值,熔断器开启,直接拒绝所有请求,执行降级逻辑,防止雪崩拖垮服务。
- 半开 Half-Open:冷却窗口期结束,放行少量探测流量;探测请求正常→切回关闭;探测仍报错→重回打开。
三种限流算法对比
| 算法 | 特点 | 项目使用场景 |
|---|---|---|
| 滑动窗口 | 精准统计固定时间窗口流量,粒度细 | Sentinel 单机限流、Redis Lua 集群限流(主力) |
| 漏桶 | 流出速率固定,削峰,无法应对突发流量 | MQ 消费者批量消费控并发 |
| 令牌桶 | 可积累令牌,允许瞬时突发高并发 | 大促活动临时扩容流量场景 |
项目落地场景
- 网关层:后台
/admin/**接口全局粗粒度限流,防止运营并发压垮服务; - AI 大模型、第三方短信通道接口:配置熔断,设计三级降级;
- 验证码接口:手机号、IP 防刷限流;
- 配置持久化:限流熔断规则统一存 Nacos,支持热更新,控制台仅用来监控,不存储生产规则。
AI 接口三级降级策略
1 级降级:切换备用大模型;
2 级降级:读取 MySQL 预制短信模板;
3 级降级:返回通用 "系统繁忙" 兜底提示。
集群限流兜底方案
Redis Lua 分布式滑动窗口实现全集群统一限流;若 Redis 宕机,自动切换 Sentinel 本地内存限流兜底,避免裸跑无防护。
项目排查和问题处理
1.项目对接第三方 AI 大模型频繁超时,如何使用 Sentinel 做服务保护?
首先我们会给 AI 生成接口单独定义 Sentinel 资源标识,持续统计接口的响应超时率、异常比例;
- 限流管控:配置接口整体 QPS 限流,也支持热点参数限流,限制单商户、单 IP 的调用频次,避免单个客户疯狂调用占用资源;限流阈值统一放在 Nacos 可动态调整;
- 熔断保护:当大模型接口超时、报错占比超过设定阈值,熔断器直接打开,不再持续调用不稳定的第三方 LLM,避免大量超时请求堆积拖垮我们自身服务;
- 三级降级兜底:熔断触发后依次执行降级策略,优先切换备用大模型;备用模型不可用就读取 MySQL 预制合规短信模板;两种方案都失效则返回 "系统繁忙,请稍后重试" 友好提示;
- 故障兜底:分布式限流依赖 Redis Lua,一旦 Redis 宕机,自动切换 Sentinel 本地内存限流,保证接口始终有防护,不会裸跑。
2.简述滑动窗口、漏桶、令牌桶三种限流算法的特点,以及项目各自使用场景。
-
滑动窗口 特点:将时间切分为多个小段滚动统计流量,统计粒度细,能精准识别短时间突发流量,避免临界流量误放行; 项目场景:Sentinel 单机本地限流、Redis Lua 集群统一限流,平台主流限流方案。
-
漏桶算法 特点:流出速率恒定,无论请求涌入多快,都以固定速度处理,强制削峰,无法应对瞬时突发流量; 项目场景:RabbitMQ 消费者批量拉取消息,控制批量 AI 生成任务的消费速度,防止一次性压垮向量库与大模型。
-
令牌桶算法 特点:系统匀速生成令牌存入桶内,空闲时段令牌可累积,突发高并发时能一次性取出存量令牌,支持瞬时流量峰值; 项目场景:营销大促活动,允许短时间爆发式请求,适配活动流量突增场景。
3.我们项目 Redis 宕机后,限流不会完全失效,依靠什么兜底方案?逻辑是什么?
项目采用双层限流兜底架构:正常情况下通过 Redis Lua 滑动窗口实现集群统一限流,保证多实例流量管控标准一致;一旦 Redis 宕机、无法访问,系统自动降级切换到 Sentinel 本地内存限流,单机独立统计接口流量,避免服务完全裸跑无防护,保障接口基础限流能力不丢失。
QLExpress 规则引擎
硬编码 if/else 人群筛选的痛点
如果客户分群、营销筛选逻辑写死在代码里,新增、修改筛选条件必须改代码、打包发版、重启服务,迭代周期长,运营无法自主配置规则,开发工作量大。
QLExpress 核心能力
- 可视化页面拖拽配置表达式,运营不用写代码就能自定义人群筛选条件;
- 内置安全沙箱,拦截危险执行语法,杜绝表达式注入安全风险;
- 表达式本地缓存,重复执行不用重复解析,提升批量圈选人群性能;
- 支持逻辑判断、数值区间、多条件组合,完全匹配客户分层业务。
项目落地场景
批量定时营销任务中,运营在后台配置人群筛选表达式,执行定时推送时,通过 QLExpress 动态解析规则,批量圈选出符合条件的目标客户,无需开发介入发版迭代。
项目排查和问题处理
1.项目为什么不用 if-else 硬编码做人群筛选,选择 QLExpress?
业务里营销人群筛选规则变动十分频繁,如果用 if-else 硬编码实现,每次新增、修改筛选条件都需要开发改代码、打包、发版、重启服务,迭代周期长,占用大量开发人力。 引入 QLExpress 后解决了这些痛点:
- 后台提供可视化拖拽页面,运营可以自主配置筛选表达式,不用依赖开发、无需发版就能调整人群规则;
- 内置安全沙箱,限制危险语法执行,防止表达式注入带来安全漏洞;
- 表达式做本地缓存,批量圈选人群时不用重复解析,提升批量筛选性能;
- 支持多条件逻辑、区间判断等各类筛选语法,完全适配我们客户分层、营销圈人业务。
2.QLExpress 安全沙箱有什么作用,对应项目什么安全风险?
QLExpress 内置安全沙箱机制,会拦截危险、非法语法,通过黑白名单管控可执行方法与操作;
我们项目运营自主编写筛选表达式,如果没有沙箱限制,恶意表达式可能执行文件读写、反射等高危操作,造成表达式注入漏洞,威胁服务安全;
沙箱能隔离这类风险,只开放业务允许的逻辑判断、数值计算语法,从源头避免恶意表达式攻击。
DockerCompose 容器编排
核心作用
依靠一份 yml 配置文件,一条命令就能一键拉起项目全套中间件:MySQL、Redis、RabbitMQ、PgVector、ES;自动完成端口映射、容器网络创建、数据持久化挂载。
落地业务价值
- 统一团队所有人本地开发环境,规避中间件版本不一致引发的各种诡异 bug;
- 新人上手成本极低,无需手动下载安装、配置各类中间件,拉取项目后直接启动 compose 即可;
- 区分环境隔离,开发、测试环境可通过不同 compose 配置快速切换。
Docker / DockerCompose / K8s 三者定位区分表格
| 工具 | 核心定位 | 使用场景 |
|---|---|---|
| Docker | 容器引擎,打包单个应用镜像 | 打包业务服务、单独启动单个中间件 |
| DockerCompose | 单机多容器编排工具 | 本地开发、小型测试环境,单机管理一组容器 |
| K8s | 分布式集群容器编排平台 | 线上生产大规模集群服务部署、扩缩容、自愈 |
项目排查和问题处理
1.本地开发环境,为什么不直接在 Windows 系统手动安装 MySQL、Redis,而是选用 DockerCompose?
如果团队成员在 Windows 本地手动安装 MySQL、Redis 等中间件,很容易出现中间件版本、配置参数不统一的问题,经常出现本地运行正常,提交代码到测试环境就报错的环境差异 bug。 使用 DockerCompose 有几点核心优势:
- 统一约束所有中间件版本与配置,团队所有人本地环境完全一致,规避环境差异类 bug;
- 新人上手简单,不用手动下载、安装、配置各类中间件,一条命令就能启动整套依赖;
- 支持多套 yml 配置,快速隔离开发、测试两套环境,切换方便;
- 自动完成端口映射、数据持久化、容器网络配置,省去大量手动配置工作。
2.简述 DockerCompose 核心作用,以及它的配置文件是什么?
DockerCompose 是单机多容器编排工具,核心作用:通过一份统一配置文件,一键批量启动、管理项目所需全部中间件容器,自动处理网络、端口、数据持久化,统一团队开发环境。 配置文件固定名称为 docker-compose.yml。
Redis 全套底层 + Caffeine+Redis 二级缓存
Redisson 分布式锁核心原理
- 底层通过 Lua 脚本实现加锁、解锁,保证操作原子性;
- 支持可重入锁:记录持有锁的线程标识,同一线程多次加锁不会死锁;
- 看门狗自动续期:长任务后台自动延长锁过期时间,防止任务没执行完锁提前释放;
- RedLock 红锁机制:过半 Redis 节点加锁成功才算拿到锁,解决主从切换锁丢失问题,项目用于 Quartz 集群定时任务防重复执行。
原生 SETNX 锁四大缺陷:不可重入、无自动续期、解锁非原子操作、主从切换锁丢失,项目全部用 Redisson 替代原生方案。
Lua 脚本原子性项目落地场景
- Redis Lua 滑动窗口多维度限流(手机号、商户 ID、IP);
- RedLock 分布式锁加解锁逻辑;
- 批量更新商户额度缓存,多条缓存操作保持原子执行。
Redis 在项目中的使用场景
分布式锁、分布式限流、JWT 黑名单、布隆过滤器、二级分布式缓存、接口幂等校验、计数器。
缓存三大问题(穿透 / 击穿 / 雪崩)完整方案
缓存穿透
现象:查询数据库不存在的数据,缓存无记录,请求全部打穿到 MySQL,压垮数据库 业务场景:恶意请求不存在的商户 ID、无效手机号 解决方案:前置布隆过滤器拦截无效 key;空值短期缓存;接口参数合法性校验
缓存击穿
现象:热点 key 过期瞬间,大量并发请求同时查询数据库 业务场景:高热度商户额度配置高频查询 解决方案:热点 key 永不过期;分布式锁控制单线程查库回填缓存
缓存雪崩
现象:大量 key 同一时刻过期;Redis 整体宕机,流量全部涌向 MySQL 解决方案:过期时间随机偏移;Caffeine+Redis 多级缓存;接口限流熔断;Redis 集群高可用
Caffeine+Redis 二级缓存完整查询链路
查询顺序:优先读取 Caffeine 本地缓存(进程内存,无网络 IO,速度最快)→未命中查询 Redis 分布式缓存→两级都未命中才查询 MySQL;查询成功双向回填两级缓存。 优势:
- Caffeine 拦截大部分热点请求,大幅减少 Redis 网络请求;
- Redis 保证多服务实例之间缓存数据一致;
- 双重兜底,Redis 故障时本地缓存仍可提供基础数据访问。
生活化举例
Redisson 分布式锁
把锁想象成卫生间钥匙:
SETNX 是一次性钥匙,进去之后没法再开门,中途超时钥匙自动消失;
Redisson 可重入锁支持同一个人反复进出,看门狗相当于定时续时,防止洗澡没结束门锁自动打开;
RedLock 相当于多间卫生间,超过一半房间拿到钥匙才算占用,避免主卫生间故障导致锁失效。
缓存三大问题
- 穿透:不停查不存在的物品,仓库没有就全都去翻数据库库房,库房被挤爆;布隆过滤器相当于门卫,不存在的物品直接拦在门外。
- 击穿:爆款商品标签刚好过期,所有人同一时间冲去库房;给标签永久保存,或者只放一个人去库房更新。
- 雪崩:所有商品标签同时过期,瞬间所有人冲进库房;每个商品过期时间错开,同时办公桌存一份备份。
项目排查和问题处理
1.原生 SETNX 实现分布式锁存在哪些缺陷,项目为什么选用 Redisson?
原生 SETNX 分布式锁四大缺陷:
- 不可重入:同一个线程多次加锁会直接死锁,不支持嵌套调用;
- 无自动续期:任务执行时长超过锁过期时间,锁会提前释放,并发错乱;
- 解锁非原子性:先判断持有者再删除锁,并发场景下极易误删别人的锁;
- 主从切换锁丢失:主节点拿到锁还未同步给从节点就宕机,新主节点不存在该锁,出现并发安全问题。
Redisson 全部补齐以上痛点:自带可重入锁、看门狗自动续期、Lua 脚本保证加解锁原子性、提供 RedLock 红锁规避主从锁丢失风险,开发开箱即用,稳定性更强,因此项目统一使用 Redisson。
2.分别说明缓存穿透、缓存击穿、缓存雪崩三者区别,以及各自对应的解决办法。
1. 缓存穿透
现象:请求一直查询 Redis、MySQL 都不存在的数据,缓存永远无法留存数据,大量请求持续直达数据库,压垮 MySQL;大多是恶意刷无效 ID、手机号造成。
解决方案
- 前置布隆过滤器,提前拦截不存在的 key;
- 查询为空时,在 Redis 写入短期空值缓存;
- 接口前置参数校验,过滤非法参数。
2. 缓存击穿
现象:单个超高热度缓存 key 到期一瞬间,海量并发请求同时绕过缓存涌向数据库。
解决方案
- 热点 key 设置永不过期;
- 使用分布式锁,仅放行一个请求去数据库查询并回填缓存,其余请求等待缓存刷新完成。
3. 缓存雪崩
现象分两种 ① 大批量缓存 key 集中同一时间过期,流量瞬间全部打入数据库; ② Redis 集群整体宕机,缓存彻底失效,请求全部访问 MySQL。
解决方案
- 给过期时间加上随机偏移值,错开批量过期时间;
- 搭建 Caffeine+Redis 二级缓存,Redis 故障时本地缓存兜底;
- Redis 做主从集群保证高可用;
- 搭配 Sentinel 限流熔断,限制打库流量。
三者核心区分: 穿透:查不存在的数据;击穿:单个热点 key 过期 ;雪崩:大批 key 集体过期 / Redis 整体瘫痪
3.项目引入 Caffeine+Redis 二级缓存有什么好处?
1.访问速度大幅提升
Caffeine 属于进程内本地内存缓存,无网络 IO 开销,绝大多数热点请求可直接命中本机缓存,响应速度远快于 Redis,大幅降低接口耗时。
2.削减 Redis 压力与网络开销
高频热点数据常驻本地缓存,不必频繁和 Redis 交互,有效减轻 Redis 集群负载,节省内网网络传输损耗。
3.多重防护,抵御缓存异常场景
- 缓解缓存雪崩:大量 key 集中过期或 Redis 宕机时,本地 Caffeine 依旧可以兜底提供数据访问;
- 缓解缓存击穿:热点数据留存本地,规避热点 key 过期瞬间流量涌入数据库; 搭配限流策略一起,给数据库多层防护。
4.兼顾分布式数据一致
本地 Caffeine 负责高速访问,Redis 统一存储集群共享数据,服务实例间数据以 Redis 为准,兼顾性能与分布式一致性。
JVM 调优 + OOM 问题排查
JVM 内存区域划分
1.堆 Heap:存放所有对象实例,分新生代(Eden、S0、S1)+ 老年代;GC 主要回收区域,OOM 最常发生在这里。
2.虚拟机栈:每个线程私有,存放局部变量、方法调用栈帧;栈太深会栈溢出 StackOverflowError。
3.本地方法栈:支撑 native 本地方法调用。
4.程序计数器:记录线程执行字节码位置,唯一一个无 OOM 的内存区域。
5.元空间 Metaspace:存放类信息、常量、静态变量、字节码;JDK8 取消永久代,改用元空间,直接使用操作系统内存。
新生代 GC 流程(复制算法)
- 新对象优先分配在 Eden 区;
- Eden 满了触发 Minor GC,存活对象复制到空闲 Survivor 区;
- 两个 Survivor 来回交换存放存活对象;
- 对象熬过 15 次 Minor GC 晋升到老年代; 大对象直接进入老年代,避免新生代来回复制开销。
常见 OOM 四种场景 + 诱因
1.Java heap space 堆内存溢出
最普遍:集合无限添加对象未释放、大文件一次性读取加载、缓存无上限堆积。
2.Metaspace 元空间溢出
频繁动态创建类、动态代理大量生成 class、热部署频繁重载类。
3.栈溢出 StackOverflowError
递归深度过大、方法循环调用无终止条件。
4.直接内存 Direct Buffer 溢出
Netty、NIO 大量使用堆外内存,未主动释放。
线上 OOM 排查标准步骤
- 发生 OOM 时开启
-XX:+HeapDumpOnOutOfMemoryError自动导出堆快照 dump 文件; - 使用 MAT 内存分析工具导入 dump 文件;
- 查看内存占用最大对象、引用链,定位代码里未释放的集合 / 缓存 / 大资源;
- 修复代码逻辑、调整 JVM 参数、限制缓存容量。
项目 JVM 基础参数配置示例
- Xms:初始堆内存;Xmx:最大堆内存;两者设一致,避免运行时堆扩容消耗性能
- Xmn:新生代大小
- SurvivorRatio:Eden 和 Survivor 比例
- MetaspaceSize:元空间初始大小
生活化举例
JVM 内存好比办公仓库:
堆内存 = 开放式大储物区,绝大部分货物(对象)都放这里,堆满就要清理垃圾(GC);
Eden 区 = 临时置物桌,新东西先放桌面,满了收拾一遍,常用物品搬到储物架(Survivor);
老年代 = 固定储物柜,长期不用但还要保留的物品放这里;
虚拟机栈 = 每个人手边笔记本,记录当下工作步骤,写太多本子装不下就栈溢出;
元空间 = 产品说明书档案柜,存放各类产品图纸(类字节码)。
OOM 就是仓库彻底塞满再也放不下货物,必须清理无用货品、扩容仓库或者改掉疯狂囤货的代码。
项目排查和问题处理
1.JDK8 为什么废弃永久代,改用元空间 Metaspace?永久代存在哪些弊端?
永久代(PermGen)弊端
- 永久代属于 JVM 堆内存的一部分,内存上限固定,很难预估所需大小:设置过小容易频繁 Full GC 甚至溢出;设置过大又造成内存闲置浪费;
- 永久代内存回收复杂,类卸载条件严苛,热部署、动态代理、频繁加载类的场景极易出现永久代溢出;
- 不同 JDK 版本永久代大小参数不一致,兼容性差。
JDK8 替换为元空间 Metaspace 的原因
- 元空间直接使用本地操作系统内存,不再占用 JVM 堆内存,理论上限是物理内存,大幅降低溢出概率;
- 元空间会动态伸缩内存,无需人为精准预估容量,运维更省心;
- 优化了类加载与卸载机制,Spring 热部署、动态创建类、反射代理等场景更稳定;
- 统一了不同平台内存管理逻辑,跨系统兼容性更好。
2.线上出现 java.lang.OutOfMemoryError: Java heap space 堆内存溢出,完整排查流程是什么?
1.提前配置 JVM 启动参数
项目启动时预先加上参数:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/xxx/dump 一旦触发堆 OOM,JVM 会自动生成堆快照 dump 文件,留存现场内存数据。
2.下载 dump 文件,使用 MAT 工具解析
将 dump 文件拉取至本地,借助 Eclipse MAT 内存分析工具打开快照。
3.定位内存泄漏根源
- 查看内存占用排行最高的对象;
- 追踪对象引用链,找出长期持有对象却未释放的代码位置:常见为无限扩容的 List/Map 集合、无上限缓存、一次性读取超大文件、大对象常驻内存等。
4。落地优化方案
- 代码层面:清理无用集合、读取文件采用流式分批加载、给本地缓存设置最大容量与淘汰策略;
- JVM 参数层面:合理调整堆初始值 Xms、最大值 Xmx,新生代配比;
- 架构层面:超大批量数据拆分处理,避免一次性加载至内存。
5.上线验证 优化后观察 GC 日志、堆内存走势、FullGC 频次,确认 OOM 不再复现。
3.简述 Minor GC 和 Full GC 的触发时机以及二者区别
一、触发时机
1.Minor GC(新生代 GC)
Eden 区内存被新对象占满时自动触发,只回收新生代(Eden + 两块 Survivor)。
2.Full GC
常见触发场景:
① 老年代空间不足,放不下晋升过来的对象;
② 主动调用System.gc();
③ 元空间内存耗尽;
④ 空间分配担保失败;
会回收整个堆:新生代 + 老年代,部分垃圾回收器还会顺带回收元空间。
二、核心区别
1.回收范围不同
Minor GC:仅新生代; Full GC:整堆(新生代 + 老年代)。
2.耗时与影响差距极大
Minor GC:频次高、速度快,停顿很短,对系统性能影响微弱;
Full GC:开销巨大、STW 停顿时间很长,频繁 Full GC 会严重拖慢接口响应,线上要极力避免。
3.对象流转逻辑不一样
Minor GC:存活对象复制到另一块 Survivor 区;熬过多次 Minor GC 后满足年龄阈值,晋升到老年代;
Full GC:全盘扫描存活对象,压缩整理老年代空闲空间,清理全部无效对象。
4.频次
Minor GC 日常频繁发生;Full GC 应当极少出现。
RabbitMQ 消息队列
MQ 三大核心作用
1.系统解耦
短信推送、日志记录、营销通知等下游业务剥离出去,主业务下单 / 提交请求完成即可返回,不用串行等待多个附属服务执行;上下游互不影响,改动互不干涉。
2.流量削峰填谷
大促、定时批量营销时段瞬间海量请求涌入,MQ 缓冲瞬时流量,消费端匀速拉取消息处理,避免瞬间流量压垮数据库。
3.异步提速
耗时业务转为异步执行,缩短主接口响应时长,提升用户体验。
消息可靠性保障(防止消息丢失)
消息丢失四个节点,逐一处理:
- 生产者发送阶段丢失:开启生产者确认机制(publisher-confirm-type),消息投递到 Broker 收到回执才算发送成功,失败重试;
- 交换机转发队列丢失:设置备份交换机;
- Broker 服务器丢失:镜像队列、集群部署,消息多副本持久化磁盘;
- 消费端丢失:关闭自动 ACK,业务处理完成后手动 ACK;处理失败不确认,消息重回队列。
消息重复消费问题(幂等性解决方案)
网络卡顿、ACK 超时会导致消息重复投递,必须做幂等:
- 每条消息携带唯一业务 ID;
- 消费前先查询数据库 / Redis 判断该 ID 是否已处理;
- 已处理直接跳过,未处理执行业务逻辑,执行完成标记已消费。
死信队列 DLX
消息满足以下条件会进入死信队列,不会一直重试阻塞正常队列:
- 消息被多次消费均失败;
- 消息过期;
- 队列达到最大长度被丢弃;
用处:存放异常消息,人工排查错误原因、重新补发,避免无效消息无限循环占用资源。
项目落地场景
批量营销短信异步推送、定时任务分发、日志异步存储、订单后续附属流程解耦。
项目排查和问题处理
1.项目使用 RabbitMQ 最核心的三个作用分别是什么?
RabbitMQ 三大核心用途:
- 系统解耦 把短信推送、消息通知这类附属业务从主业务剥离,主服务和下游消费服务互不依赖,一方修改、故障不会直接牵连另一方。
- 流量削峰填谷 大促、批量发短信等高并发瞬间流量先存入 MQ 缓冲,消费端平稳匀速处理,避免瞬时海量请求压垮数据库与业务服务。
- 异步提升响应速度 耗时的后置流程异步化处理,主接口完成核心逻辑就直接返回结果,大幅缩短接口耗时,优化使用体验。
2.RabbitMQ 消息丢失一共分为四个环节,分别是哪些?各自如何保障消息不丢失?
RabbitMQ 消息丢失四大环节及对应保障方案:
1.生产者投递阶段丢失
开启生产者确认机制(Confirm),Broker 成功接收消息后返回确认回执;未收到回执则触发重试投递,保证消息可靠送出。
2.交换机转发至队列时丢失
配置备份交换机,路由失败的消息会转发至备份交换机对应的队列,避免消息路由异常直接丢弃。
3.Broker 服务端消息丢失
开启消息、队列持久化,消息写入磁盘;搭建 RabbitMQ 集群 + 镜像队列,消息存储多副本,节点宕机数据不会丢失。
4.消费端消息丢失
关闭自动 ACK,改为手动 ACK 模式;业务逻辑完整处理完毕后,再手动发送确认指令;业务异常处理失败则拒绝 ACK,消息重新退回原队列等待再次消费。
3.说说幂等性在 RabbitMQ 里具体怎么落地实现?
1.生成全局唯一消息标识
每条消息封装唯一业务 ID(订单号、短信批次号、uuid 均可),生产者发送时把该 ID 一并塞入消息体。
2.消费前置校验
消费者拿到消息后,先去 Redis / 数据库查询这条消息 ID 是否已经处理完成:
- 若已存在记录:判定为重复消息,直接丢弃,不再执行业务逻辑;
- 若无记录:继续往下处理业务。
3.业务执行成功后做标记
业务逻辑完整执行完毕,写入标识:
- 方案 1:Redis 存入该消息 ID,并设置过期时间;
- 方案 2:数据库新建消息消费记录表,写入主键 id + 处理状态。
业务失败不打标
处理异常时不记录消费状态,消息重回队列重试,不会被误判为已消费。
补充两种常用方案
- 数据库唯一索引 针对入库类业务,给业务流水字段加唯一索引,重复消息插入直接报错,天然拦截重复数据;
- 状态机控制 依靠业务单据状态(待处理、已完成)判断,已完结单据不再二次处理。
校验逻辑放在消费端而非发送端:重复消费是 ACK 超时、网络抖动导致 MQ 重复推送消息引发的问题,生产者一般只会发送一次。
SkyWalking + ELK 链路追踪与日志监控
SkyWalking 核心定位
面向微服务分布式架构的 APM(应用性能监控)工具 核心三大能力:
1.分布式调用链路追踪
一次前端请求依次经过:网关 → 短信服务 → 营销服务 → 缓存 → MQ → 数据库
SkyWalking 会生成一条完整链路:统一 TraceId 串联全流程,每个服务分段生成 SpanId;
链路页面能看清:每一段耗时、调用顺序、哪个服务卡顿、哪一步抛出异常。
2.服务性能指标监控
自动采集各项数据:接口平均响应时间、QPS 吞吐量、错误率、JVM 运行状态、线程池占用、数据库耗时;
绘制曲线图,直观看到高峰期性能下滑、接口抖动。
3.告警推送
接口错误率飙升、响应超时、服务离线、JVM 内存占用过高,可配置邮件 / 消息告警,提早发现线上故障。
关键术语通俗解释
1.TraceId
一次完整请求全局唯一编号,整条链路所有服务共用同一个 TraceId,凭借它就能检索整条调用记录。
2.Span
链路里的每一小段调用:比如网关调用后端服务、服务查询 MySQL、发送 MQ 都单独算作一个 Span,会记录耗时、状态、异常信息。
3.采样率
线上请求量巨大,不会采集所有链路,配置采样比例(比如 10%)减轻服务与存储压力;排查问题时可临时调高采样。
SkyWalking Java Agent 核心优势
全程零业务代码修改,只需要在服务启动命令添加-javaagent探针参数即可接入监控;探针依附 JVM 运行,自动拦截接口调用、数据库访问、MQ 收发、Feign 远程调用等行为,自动采集链路耗时、异常信息,开发无需埋点编码。
探针分为两种采集模式:
- 字节码增强(默认):运行期修改字节码,无侵入,主流方案;
- SDK 埋点:手动编码埋点,极少场景才会使用。
ELK 是什么?和 SkyWalking 分工区别
ELK 由三款组件构成:Elasticsearch + Logstash + Kibana
作用:统一收集、存储、检索、可视化系统日志
- 日志:代码打印的 info、error、异常堆栈、参数详情
- 场景:报错后想看具体报错堆栈、入参、上下文日志,用 ELK
Logstash(日志采集管道)
负责日志收集、过滤、清洗、转换
- 从各个微服务、服务器抓取零散日志;
- 剔除无用空格、杂乱字符、拆分日志字段、过滤无效日志;
- 处理完毕后,统一推送至 Elasticsearch。
补充:现在很多场景会用 Filebeat 替代 Logstash 做轻量采集,占用资源更低。
lasticsearch ES(日志存储 + 检索引擎)
日志最终存放仓库,核心能力:
- 海量日志持久化存储;
- 支持全文快速检索、按 TraceId、时间、报错关键词精准查询日志;
- 日志分片分布式存储,量大也不会卡顿。
Kibana(可视化展示面板)
负责前端可视化:
- 绘制日志量曲线图、报错统计饼图;
- 提供检索页面,输入关键词、TraceId 就能查询日志;
- 配置日志告警、仪表盘,直观查看系统日志健康状态。
三者流转链路
各个服务散落日志 → Logstash 采集清洗 → Elasticsearch 存储索引 → Kibana 查看检索
二者分工(重点区分)
| 工具 | 核心侧重点 | 适用场景 |
|---|---|---|
| SkyWalking | 调用链路、性能耗时、服务调用关系、慢接口定位 | 查接口为什么慢、哪个服务拖后腿、服务间调用报错 |
| ELK | 原始日志、异常堆栈、请求参数详情 | 定位具体代码报错、查看入参出参、详细错误日志 |
二者搭配使用流程:
- 发现接口卡顿 / 报错 → SkyWalking 查链路,锁定出问题的服务节点;
- 拿到该链路 TraceId → 去 ELK 根据 TraceId 检索整条链路的详细日志,查看具体异常堆栈、参数,定位代码 bug。
项目部署简单流程
- 服务启动挂载 SkyWalking Agent 探针(javaagent),无代码侵入,自动采集链路数据;
- 探针将数据上报 SkyWalking OAP 服务;
- UI 面板可视化展示调用链、性能报表;
- 日志统一采集推送至 ELK,链路与日志通过 TraceId 关联打通。
适用场景(短信营销项目举例)
批量发送短信接口缓慢:
- SkyWalking 查看链路:网关耗时很短,卡在 RabbitMQ 消息投递 + 数据库写入环节;
- ELK 根据 TraceId 检索日志:发现数据库索引缺失,批量插入耗时太久;
- 优化索引之后,响应速度明显下降。