面试官:Redis缓存雪崩怎么办?谢飞机:多穿点衣服不就行了!——大厂Java面试爆笑实录(含答案详解)
面试官:Redis缓存雪崩怎么办?谢飞机:多穿点衣服不就行了!——大厂Java面试爆笑实录(含答案详解)
前言
某互联网大厂办公室,下午两点。面试官老李端着保温杯走进会议室,里面坐着一个穿着格子衫、头发略微稀疏的年轻人——谢飞机。
谢飞机的简历上写着:5年Java开发经验,精通Spring Cloud全家桶,熟悉高并发系统设计。
老李推了推眼镜:"那我们开始吧。"
第一轮:Java基础与框架(电商用户模块)
面试官老李:"谢飞机,看你简历上写了Spring Boot,先来个简单的——Spring Boot的自动装配原理是什么?"
谢飞机(挺直腰板):"这个我熟!Spring Boot通过@SpringBootApplication注解,里面包含了@EnableAutoConfiguration,它会通过SpringFactoriesLoader去读取META-INF/spring.factories文件里配置的自动配置类,然后根据条件注解@ConditionalOnClass、@ConditionalOnBean等按需加载。比如我们引入了spring-boot-starter-web,它就自动帮我们配置了DispatcherServlet、Tomcat这些。"
老李(微微点头):"不错,基础扎实。那接着问——你项目里用的是MyBatis还是Hibernate?能说说MyBatis的一级缓存和二级缓存的区别吗?"
谢飞机(来了精神):"我们电商项目用的是MyBatis。一级缓存是SqlSession级别的,默认开启,同一个SqlSession内多次查询同一个数据,第二次直接从缓存拿。二级缓存是Mapper级别的,跨SqlSession共享,需要手动配置开启,实体类还要实现Serializable接口。不过我们线上其实没怎么用二级缓存,因为分布式环境下容易产生脏数据,都用Redis替代了。"
老李(赞许):"嗯,知道技术选型的取舍,不错。那接下来说说——Spring的事务传播机制有哪些?在电商下单场景中,创建订单和扣减库存,你会怎么配置事务?"
谢飞机(稍显紧张):"呃……Spring事务传播有……REQUIRED、REQUIRES_NEW、NESTED……还有……"
老李:"还有呢?一共七种。"
谢飞机:"还有SUPPORTS、NOT_SUPPORTED、NEVER、MANDATORY……对吧?"
老李:"对。那下单场景呢?"
谢飞机(挠头):"创建订单和扣减库存……我得想想……是不是用REQUIRED就行了?"
老李(皱眉):"你确定吗?如果扣减库存失败,订单已经创建了怎么办?而且你说说看,REQUIRES_NEW在什么场景下用?"
谢飞机(汗开始下来):"这个……REQUIRES_NEW就是……新开一个事务嘛……扣库存失败……那就回滚?"
老李(沉默两秒):"行,先记下。最后一个——JVM的内存模型,堆和栈的区别,还有垃圾回收算法,简单说说。"
谢飞机(恢复了一点信心):"堆是线程共享的,存对象实例;栈是线程私有的,存局部变量和方法调用。GC算法有标记清除、标记整理、复制算法,分代收集里新生代用复制算法,老年代用标记整理或标记清除。CMS和G1都是并发收集器。"
老李:"G1和CMS的区别?"
谢飞机:"G1……G1把堆分成了Region,可以预测停顿时间……嗯……就这些。"
老李:"还行,但不够深入。歇口气,咱们进入第二轮。"
第二轮:微服务与中间件(电商秒杀场景)
老李:"假设我们电商平台要做秒杀活动,QPS预估上万。Redis在你的秒杀系统中怎么用?缓存穿透、缓存击穿、缓存雪崩分别是什么?怎么解决?"
谢飞机(眼睛一亮):"这个我背过!缓存穿透是查不存在的数据,用布隆过滤器解决;缓存击穿是热点key过期,用互斥锁或者逻辑过期解决;缓存雪崩是大量key同时过期……"
老李:"怎么解决缓存雪崩?"
谢飞机(脱口而出):"多穿点衣服不就行了!"
老李(嘴角抽搐):"……你认真的?"
谢飞机(意识到失态):"不是不是!我是说……给过期时间加随机值,不让它们同时过期。还有用Redis集群做高可用,主从哨兵或者Cluster模式。"
老李(叹了口气):"行吧。下一个问题——Kafka在秒杀系统中怎么保证消息不丢失?生产者、Broker、消费者三方面说说。"
谢飞机:"生产者用ack=all,保证ISR全部同步;Broker设置replication.factor至少为3,min.insync.replicas大于1;消费者关闭自动提交,手动提交offset。"
老李:"消息重复消费怎么处理?"
谢飞机:"消费者做幂等,比如用数据库唯一键约束,或者Redis记录已消费的消息ID。"
老李(稍微满意):"不错。那——分布式事务呢?你的电商系统里,下单要调用订单服务、库存服务、优惠券服务,怎么保证一致性?"
谢飞机(额头冒汗):"用……用Seata?"
老李:"Seata的AT模式和TCC模式有什么区别?AT模式的原理是什么?"
谢飞机:"AT模式……就是……自动挡?TCC是手动挡?AT模式是两阶段提交,Seata代理数据源,生成undolog……然后……全局事务协调器……嗯……"
老李(打断):"你刚刚说AT跟TCC的关系,能具体讲一下TCC的Try-Confirm-Cancel每个阶段干什么吗?"
谢飞机:"Try就是……试一下?Confirm就是……确认?Cancel……取消?"
老李:"……你觉得这个回答能拿多少分?"
谢飞机(低头):"大概……及格线以下?"
老李:"你觉得呢?不纠结了,下一个——Spring Cloud Gateway和Zuul有什么区别?你们用的哪个做网关?"
谢飞机:"我们用的Gateway,它是基于WebFlux的,响应式编程,性能比Zuul好。Zuul 1.x是同步阻塞的,Zuul 2.x也是响应式的但我们没用过。Gateway还支持动态路由、限流、熔断这些。"
老李:"那Resilience4j的熔断器有哪三种状态?"
谢飞机:"CLOSED、OPEN、HALF_OPEN!关闭状态正常调用,失败率达到阈值变OPEN直接拒绝,过一段时间变HALF_OPEN尝试放行部分请求,成功就变回CLOSED,失败继续OPEN。"
老李(难得点头):"这个答得还行。继续。"
第三轮:系统设计与性能优化(电商双十一)
老李:"第三轮了,谢飞机。你的电商系统数据库用了读写分离,那主从延迟你怎么处理?"
谢飞机:"这个……我们用的是ShardingSphere做分库分表,读写分离它自动路由。主从延迟的话……强制走主库?"
老李:"什么场景强制走主库?"
谢飞机:"写入后立即要读取的场景,比如下单后马上查订单详情。"
老李:"还有别的方案吗?"
谢飞机:"还有……等从库同步完再返回给用户?但这影响体验……还可以用缓存过渡?"
老李:"思路还行,但不够系统。下一个——JVM线上CPU飙升100%,你怎么排查?"
谢飞机(来了精神):"先用top找到高CPU的Java进程PID,然后用top -H -p PID找高CPU的线程,把线程ID转成16进制,用jstack dump出线程快照,搜索那个16进制线程号,定位到具体代码行。还可以用arthas的thread -n 3直接看最忙的线程。"
老李:" arthas的 dashboard 命令能看什么?"
谢飞机:"能看线程、内存、GC的实时数据面板。"
老李:"trace命令呢?"
谢飞机:"追踪方法调用链路和耗时。"
老李(终于露出笑容):"不错,arthas用得挺熟。那——Prometheus + Grafana监控JVM,你们监控了哪些指标?"
谢飞机:"Micrometer采集指标暴露给Prometheus,监控了堆内存使用率、GC频率和耗时、线程数、CPU使用率、接口QPS和RT。Grafana做了大盘,还配了告警规则,堆内存超过80%就钉钉通知。"
老李:"嗯,运维这块确实有经验。最后问一个设计题——设计一个短链接系统,类似微博的t.cn,要考虑高并发,你怎么设计?"
谢飞机(深吸一口气):"首先,用发号器生成唯一ID,比如雪花算法或者号段模式,然后把ID转成62进制作为短码。存储用MySQL,短码和原始URL的映射。高并发的话前面加Redis缓存热点URL。URL可以做布隆过滤器防穿透。跳转用302重定向。"
老李:"雪花算法在分布式环境下有什么问题?"
谢飞机:"时钟回拨问题,美团的Leaf方案用号段模式可以避免。"
老李:"如果短链接需要支持用户自定义后缀呢?"
谢飞机(卡壳):"自定义后缀……那就在生成前校验是否已存在……加个唯一索引……然后……然后……"
老李:"行了,差不多了。"
结局
老李合上笔记本电脑,端起保温杯喝了口茶,看着满头大汗的谢飞机。
老李:"谢飞机啊,你说你做了5年Java,基础的东西还行,Spring Boot、MyBatis、Redis基础用法能答上来。但深入到分布式事务、JVM调优、系统设计,就有点……怎么说呢,水分有点大。"
谢飞机(尴尬):"面试官您说得对,我平时确实CRUD写得多,高并发场景接触得少……"
老李:"不过也不全是坏事,你arthas用得不错,网关和熔断那块也还行。这样吧——"
谢飞机(期待地看着老李)
老李:"你先回去等通知吧,我们综合评估一下再联系你。"
谢飞机走出会议室,掏出了手机,打开Boss直聘,默默地把"精通分布式系统设计"改成了"了解分布式系统设计"。
📚 答案解析(小白也能看懂!)
以下是面试中涉及的核心技术点的详细解答,建议收藏慢慢消化。
一、Spring Boot 自动装配原理
业务场景:电商项目中引入spring-boot-starter-data-redis后,为啥啥都没配置就能直接用RedisTemplate?
技术原理:
启动类@SpringBootApplication
└── @EnableAutoConfiguration
└── @Import(AutoConfigurationImportSelector.class)
└── 读取 META-INF/spring.factories
└── 加载 xxxAutoConfiguration 类
└── @ConditionalOnXxx 条件判断
└── 创建Bean注入容器
核心流程:
@SpringBootApplication包含@EnableAutoConfigurationAutoConfigurationImportSelector使用SpringFactoriesLoader扫描所有依赖jar包中的META-INF/spring.factories- 以Redis为例,
spring-boot-autoconfigure的spring.factories里配置了RedisAutoConfiguration @ConditionalOnClass(RedisOperations.class)检测到Jedis/Lettuce在classpath中才生效- 自动创建
RedisTemplate、StringRedisTemplate等Bean
关键条件注解: | 注解 | 作用 | |------|------| | @ConditionalOnClass | 类路径存在指定类时生效 | | @ConditionalOnBean | 容器中存在指定Bean时生效 | | @ConditionalOnMissingBean | 容器中不存在时生效 | | @ConditionalOnProperty | 配置文件中存在指定属性时生效 |
二、MyBatis 一级缓存与二级缓存
业务场景:电商商品详情页,同一个请求里多次查同一商品SKU信息。
一级缓存(SqlSession级别):
- 默认开启,无法关闭
- 作用范围:同一个SqlSession内
- 原理:基于
PerpetualCache的HashMap - 失效条件:执行了增删改操作、手动清缓存、SqlSession关闭
// 同一个SqlSession中
SqlSession session = sqlSessionFactory.openSession();
ProductMapper mapper = session.getMapper(ProductMapper.class);
Product p1 = mapper.selectById(1L); // 查数据库
Product p2 = mapper.selectById(1L); // 走一级缓存,不查数据库
// p1 == p2 为 true(同一个对象引用)
二级缓存(Mapper级别):
- 需要手动配置开启
- 作用范围:同一个namespace下所有SqlSession共享
- 实体类必须实现
Serializable - 分布式环境下不建议使用,容易产生脏数据
<!-- mapper.xml中开启二级缓存 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
为什么线上用Redis替代二级缓存:
- MyBatis二级缓存是本地缓存,多实例间不共享
- Redis是集中式缓存,所有实例共享同一份数据
- Redis支持过期策略、持久化、高可用
三、Spring 事务传播机制(7种)
业务场景:电商下单——创建订单+扣减库存+扣优惠券,三个操作的事务协调。
| 传播行为 | 说明 | 典型场景 | |----------|------|----------| | REQUIRED(默认) | 有事务则加入,无则新建 | 大多数增删改操作 | | REQUIRES_NEW | 总是新建事务,挂起当前事务 | 日志记录(不能因日志失败回滚业务) | | NESTED | 嵌套事务,内层回滚不影响外层 | 下单中扣库存失败只回滚库存 | | SUPPORTS | 有事务加入,无事务非事务执行 | 查询操作 | | NOT_SUPPORTED | 总是非事务执行 | 不需要事务的查询 | | MANDATORY | 必须有事务,否则抛异常 | 强制调用方开启事务 | | NEVER | 必须无事务,否则抛异常 | 不允许在事务中执行 |
下单场景推荐配置:
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(OrderDTO dto) {
// 1. 创建订单(主事务)
orderMapper.insert(order);
// 2. 扣减库存(REQUIRES_NEW:独立事务,失败不回滚订单)
try {
inventoryService.deductStock(dto.getSkuId(), dto.getQuantity());
} catch (Exception e) {
// 库存不足,需要回滚订单
throw new RuntimeException("库存不足");
}
// 3. 扣优惠券(NESTED:嵌套回滚点,失败不影响订单和库存)
try {
couponService.useCoupon(dto.getCouponId());
} catch (Exception e) {
// 优惠券异常不影响主流程
log.warn("优惠券使用失败", e);
}
}
}
四、JVM 内存模型与垃圾回收
业务场景:双十一大促,JVM堆内存飙高,频繁Full GC导致接口超时。
JVM内存结构:
┌─────────────────────────────────────┐
│ 运行时数据区 │
├───────────┬───────────┬─────────────┤
│ 线程私有 │ 线程私有 │ 线程共享 │
│ 程序计数器 │ 虚拟机栈 │ 堆(Heap) │
│ │ 本地方法栈│ 方法区 │
└───────────┴───────────┴─────────────┘
| 区域 | 存储内容 | 线程 | GC | |------|----------|------|-----| | 堆(Heap) | 对象实例、数组 | 共享 | GC主要区域 | | 方法区(Metaspace) | 类信息、常量、静态变量 | 共享 | 也会GC | | 虚拟机栈 | 局部变量表、操作数栈 | 私有 | ❌ | | 程序计数器 | 字节码行号指示器 | 私有 | ❌ |
垃圾回收算法对比:
| 算法 | 原理 | 优点 | 缺点 | 适用 | |------|------|------|------|------| | 标记-清除 | 标记存活对象,清除未标记 | 简单 | 内存碎片 | 老年代 | | 标记-整理 | 标记后移动存活对象到一端 | 无碎片 | 耗时,STW长 | 老年代 | | 复制算法 | 分两块,存活对象复制到另一块 | 高效,无碎片 | 浪费一半内存 | 新生代 |
CMS vs G1:
| 对比维度 | CMS | G1 | |----------|-----|-----| | 内存布局 | 连续的新生代+老年代 | 划分为多个Region | | 回收算法 | 标记-清除 | 标记-整理+复制 | | 停顿时间 | 不可预测 | 可设置停顿目标(-XX:MaxGCPauseMillis) | | 内存碎片 | 严重,需Full GC整理 | 无碎片 | | 适用场景 | 低延迟(已过时) | 大堆(>4G),JDK9+默认 |
五、Redis 缓存三大问题(穿透/击穿/雪崩)
业务场景:秒杀活动开始瞬间,缓存全部过期,数据库被打挂。
缓存穿透
- 现象:查询一个根本不存在的数据(id=-1),每次都穿透缓存查数据库
- 解决:
- 布隆过滤器:先把所有合法ID存布隆过滤器,拦截非法请求
- 缓存空值:查不到也缓存一个null,TTL设短一些(如5分钟)
// 布隆过滤器示例
public Product getProduct(Long id) {
if (!bloomFilter.mightContain(id)) {
return null; // 一定不存在,直接返回
}
// 可能存在,继续查缓存和数据库
Product product = redisTemplate.opsForValue().get("product:" + id);
if (product == null) {
product = productMapper.selectById(id);
if (product != null) {
redisTemplate.opsForValue().set("product:" + id, product, 30, TimeUnit.MINUTES);
} else {
// 缓存空值,防穿透
redisTemplate.opsForValue().set("product:" + id, new Product(), 5, TimeUnit.MINUTES);
}
}
return product;
}
缓存击穿
- 现象:热点key过期瞬间,大量请求直接打到数据库
- 解决:
- 互斥锁:只让一个请求去查库重建缓存
- 逻辑过期:永不过期,后台异步更新
// 互斥锁方案
public Product getHotProduct(Long id) {
Product product = redisTemplate.opsForValue().get("product:" + id);
if (product != null) return product;
// 获取分布式锁
String lockKey = "lock:product:" + id;
if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) {
try {
product = productMapper.selectById(id);
redisTemplate.opsForValue().set("product:" + id, product, 30, TimeUnit.MINUTES);
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 没拿到锁,等一会再查缓存
Thread.sleep(50);
return getHotProduct(id);
}
return product;
}
缓存雪崩
- 现象:大量key同时过期,或Redis宕机,所有请求打挂数据库
- 解决:
- 过期时间加随机值:避免同时过期(谢飞机说的"多穿衣服"其实也没全错😅)
- Redis高可用:主从+哨兵 / Cluster集群
- 本地缓存兜底:Ehcache/Caffeine做二级缓存
- 限流降级:Sentinel/Hystrix保护数据库
// 过期时间加随机值
int ttl = 30 * 60 + new Random().nextInt(5 * 60); // 30~35分钟随机
redisTemplate.opsForValue().set("product:" + id, product, ttl, TimeUnit.SECONDS);
六、Kafka 消息可靠性
业务场景:用户下单后发送Kafka消息通知物流系统,绝对不能丢消息。
生产者端
# acks=all:等待所有ISR副本确认
spring.kafka.producer.acks=all
# 重试次数
spring.kafka.producer.retries=10
# 幂等性(防止重试导致重复)
spring.kafka.producer.properties.enable.idempotence=true
Broker端
# 副本数至少3
default.replication.factor=3
# ISR最小副本数
min.insync.replicas=2
# 不允许非ISR副本当选Leader
unclean.leader.election.enable=false
消费者端
@KafkaListener(topics = "order-topic")
public void onMessage(ConsumerRecord<String, String> record, Acknowledgment ack) {
try {
// 处理业务
processOrder(record.value());
// 手动提交offset
ack.acknowledge();
} catch (Exception e) {
log.error("消费失败", e);
// 不提交,消息会被重新消费
}
}
# 关闭自动提交
spring.kafka.consumer.enable-auto-commit=false
# 从最早开始消费(新group)
spring.kafka.consumer.auto-offset-reset=earliest
消费幂等方案:
- 数据库唯一键约束(order_id + event_type)
- Redis记录消费过的消息ID
- 业务逻辑本身幂等(如库存扣减用版本号)
七、分布式事务 Seata
业务场景:下单→订单服务+库存服务+优惠券服务,三个微服务需要保证数据一致性。
AT模式(自动挡🏎️)
- 原理:两阶段提交
- 一阶段:各服务执行本地事务,Seata代理数据源自动生成undolog(前镜像+后镜像)
- 二阶段-提交:异步删除undolog
- 二阶段-回滚:根据undolog反向补偿
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 订单服务 │ │ 库存服务 │ │ 优惠券 │
│ insert │ │ update │ │ update │
│ undo_log │ │ undo_log │ │ undo_log │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└───────┬───────┴───────┬───────┘
│ TC(协调器) │
│ 全局事务管理 │
└───────────────┘
TCC模式(手动挡🏎️)
- Try:资源预留(冻结库存、预扣余额)
- Confirm:确认提交(真正扣减)
- Cancel:取消回滚(释放冻结资源)
// TCC示例 - 库存服务
public class InventoryTccAction {
// Try:冻结库存
public boolean tryDeduct(String orderId, Long skuId, int count) {
// UPDATE inventory SET frozen = frozen + count
// WHERE sku_id = #{skuId} AND stock - frozen >= #{count}
}
// Confirm:确认扣减
public boolean confirm(String orderId) {
// UPDATE inventory SET stock = stock - frozen, frozen = 0
}
// Cancel:释放冻结
public boolean cancel(String orderId) {
// UPDATE inventory SET frozen = frozen - count
}
}
| 对比 | AT模式 | TCC模式 | |------|--------|---------| | 侵入性 | 低(自动生成undolog) | 高(需实现三个方法) | | 性能 | 一般(有全局锁) | 高(无锁) | | 适用场景 | 大部分场景 | 对性能要求极高的场景 |
八、Spring Cloud Gateway vs Zuul
业务场景:电商平台API网关,统一鉴权、限流、路由转发。
| 对比 | Gateway | Zuul 1.x | Zuul 2.x | |------|---------|----------|----------| | 底层框架 | Spring WebFlux(Reactor) | Servlet(阻塞) | Netty(非阻塞) | | 线程模型 | 异步非阻塞 | 同步阻塞(Tomcat) | 异步非阻塞 | | 性能 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | | Spring Cloud集成 | 官方推荐 | 已停止维护 | Netflix内部使用 | | 动态路由 | 原生支持 | 需配合Archaius | 支持 |
Gateway核心三大组件:
- Route(路由):ID + 目标URI + Predicate集合 + Filter集合
- Predicate(断言):匹配请求条件(路径、Header、参数等)
- Filter(过滤器):对请求/响应做修改
spring:
cloud:
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/product/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
九、Resilience4j 熔断器
业务场景:库存服务挂了,不能让大量请求堆积等待,需要快速失败。
三种状态转换:
失败率达到阈值
CLOSED ──────────────► OPEN
▲ │
│ 等待时间到 │
│ ▼
└──────────────── HALF_OPEN
成功调用 │
│ 失败调用
▼
OPEN
// 配置
resilience4j:
circuitbreaker:
instances:
inventoryService:
sliding-window-size: 100 # 滑动窗口大小
failure-rate-threshold: 50 # 失败率阈值50%
wait-duration-in-open-state: 10s # OPEN状态等待时间
permitted-number-of-calls-in-half-open-state: 5 # HALF_OPEN允许请求数
@Service
public class InventoryService {
@CircuitBreaker(name = "inventoryService", fallbackMethod = "deductFallback")
public boolean deductStock(Long skuId, int count) {
// 远程调用库存服务
return inventoryClient.deduct(skuId, count);
}
// 降级方法
public boolean deductFallback(Long skuId, int count, Exception e) {
log.error("库存服务熔断降级", e);
return false; // 或返回缓存数据
}
}
十、数据库主从延迟处理
业务场景:用户下单后立即查看订单详情,读到从库发现订单不存在。
解决方案:
| 方案 | 描述 | 优缺点 | |------|------|--------| | 强制走主库 | 关键操作后立即读主库 | 简单,但增加主库压力 | | 半同步复制 | 主库等待至少一个从库同步完才返回 | 减少延迟但性能下降 | | 缓存过渡 | 写操作后同步写缓存,读先查缓存 | 增加复杂度 | | 读主兜底 | 从库读不到时降级查主库 | 优雅,需要框架支持 |
// ShardingSphere Hint强制走主库
@Transactional
public OrderDTO createAndQuery(OrderDTO dto) {
orderMapper.insert(dto.toEntity());
// 强制走主库
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.setWriteRouteOnly(); // 只走主库
return orderMapper.selectById(dto.getOrderId());
}
}
十一、JVM CPU飙升排查
场景:线上突然告警,某个服务CPU 100%。
排查步骤:
# 1. 找Java进程
top -c
# 找到PID,比如 12345
# 2. 找高CPU线程
top -H -p 12345
# 找到线程TID,比如 12388
# 3. TID转16进制
printf "%x\n" 12388
# 输出:3064
# 4. dump线程快照
jstack 12345 > jstack.log
# 5. 搜索线程
grep -A 20 "3064" jstack.log
Arthas快速定位(推荐⭐):
# 查看最忙的3个线程
thread -n 3
# 实时监控面板
dashboard
# 追踪方法耗时
trace com.example.OrderService createOrder
# 查看方法出入参
watch com.example.OrderService createOrder '{params, returnObj}'
# 火焰图分析CPU热点
profiler start
profiler stop --format html
十二、Prometheus + Grafana JVM监控
核心指标(Micrometer采集):
// 常用JVM监控指标
jvm_memory_used_bytes{area="heap"} // 堆内存使用
jvm_memory_max_bytes{area="heap"} // 堆内存最大值
jvm_gc_pause_seconds_sum // GC暂停总时间
jvm_gc_pause_seconds_count // GC次数
jvm_threads_live_threads // 活跃线程数
jvm_threads_states_threads{state="BLOCKED"} // 阻塞线程数
Grafana大盘配置:
┌────────────────────────────────────────────┐
│ QPS: 12,345 RT: P99=200ms P50=15ms │
├────────────┬──────────────┬────────────────┤
│ 堆内存使用 │ GC时间/次数 │ 线程状态分布 │
│ 60%/4G │ 12次/2.5s │ RUN:200 │
│ ██████░░ │ ┌────┐ │ WAIT:50 │
│ │ │ │ │ BLOCKED:3 ⚠ │
├────────────┴──────────────┴────────────────┤
│ 接口耗时Top10 │
│ /order/create 2.3s ⚠ │
│ /product/list 45ms │
└────────────────────────────────────────────┘
告警规则:
groups:
- name: jvm_alerts
rules:
- alert: HeapMemoryHigh
expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.85
for: 1m
annotations:
summary: "堆内存使用率超过85%"
- alert: HighGCTime
expr: rate(jvm_gc_pause_seconds_sum[5m]) > 0.5
annotations:
summary: "GC耗时占比过高"
十三、短链接系统设计
业务场景:类似微博短链接t.cn,长URL转短URL,高并发访问。
核心流程:
长URL → 发号器生成唯一ID → 62进制转短码 → 存储映射 → 返回短链接
短链接请求 → 解析短码 → 查缓存/数据库 → 302重定向到长URL
发号器方案对比:
| 方案 | 实现 | 优点 | 缺点 | |------|------|------|------| | UUID | UUID.randomUUID() | 简单 | 太长,无序 | | 雪花算法 | Twitter Snowflake | 趋势递增,高性能 | 时钟回拨问题 | | 号段模式 | 美团Leaf | 无时钟问题 | 需DB |
雪花算法结构(64位):
0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000
| 时间戳(41位) | 机器ID | 机房ID | 序列号
核心代码:
public class ShortUrlService {
private static final String BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
public String createShortUrl(String longUrl) {
// 1. 去重检查
String exist = redisTemplate.opsForValue().get("short:reverse:" + longUrl);
if (exist != null) return exist;
// 2. 发号器生成ID(雪花算法)
long id = snowflakeIdGenerator.nextId();
// 3. ID转62进制
String shortCode = toBase62(id);
// 4. 存库(短码→长URL)
urlMappingMapper.insert(id, shortCode, longUrl);
// 5. 写缓存
redisTemplate.opsForValue().set("short:" + shortCode, longUrl);
redisTemplate.opsForValue().set("short:reverse:" + longUrl, shortCode);
return "https://t.cn/" + shortCode;
}
public String getLongUrl(String shortCode) {
// 1. 查缓存
String longUrl = redisTemplate.opsForValue().get("short:" + shortCode);
if (longUrl != null) return longUrl;
// 2. 布隆过滤器防穿透
if (!bloomFilter.mightContain(shortCode)) return null;
// 3. 查库
UrlMapping mapping = urlMappingMapper.selectByShortCode(shortCode);
if (mapping != null) {
redisTemplate.opsForValue().set("short:" + shortCode, mapping.getLongUrl(), 7, TimeUnit.DAYS);
return mapping.getLongUrl();
}
return null;
}
private String toBase62(long num) {
StringBuilder sb = new StringBuilder();
while (num > 0) {
sb.append(BASE62.charAt((int)(num % 62)));
num /= 62;
}
return sb.reverse().toString();
}
}
总结
谢飞机虽然"水"了点,但他踩过的坑、暴露的问题,恰恰是大多数初中级Java程序员面试时的真实写照:
| 层级 | 表现 | 提升方向 | |------|------|----------| | 会用 | Spring Boot、MyBatis、Redis基本操作 ✅ | — | | 知道原理 | 自动装配、缓存策略、GC算法 ⚠️ | 深入源码 | | 会排查问题 | Arthas、jstack、监控 ✅ | 积累经验 | | 会设计方案 | 分布式事务、系统设计 ❌ | 多看经典方案 |
给谢飞机们的一句话:CRUD不会让你失业,但只会CRUD一定会让你在35岁时焦虑。趁着年轻,多看看源码,多思考系统设计,多积累线上排查经验。共勉!💪
本文纯属娱乐,如有雷同,那可能是你同事。
觉得有用的话,点个赞收藏吧~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)