面试官: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进制线程号,定位到具体代码行。还可以用arthasthread -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注入容器

核心流程:

  1. @SpringBootApplication 包含 @EnableAutoConfiguration
  2. AutoConfigurationImportSelector 使用 SpringFactoriesLoader 扫描所有依赖jar包中的 META-INF/spring.factories
  3. 以Redis为例,spring-boot-autoconfigurespring.factories 里配置了 RedisAutoConfiguration
  4. @ConditionalOnClass(RedisOperations.class) 检测到Jedis/Lettuce在classpath中才生效
  5. 自动创建 RedisTemplateStringRedisTemplate 等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模式(自动挡🏎️)
  • 原理:两阶段提交
    1. 一阶段:各服务执行本地事务,Seata代理数据源自动生成undolog(前镜像+后镜像)
    2. 二阶段-提交:异步删除undolog
    3. 二阶段-回滚:根据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岁时焦虑。趁着年轻,多看看源码,多思考系统设计,多积累线上排查经验。共勉!💪


本文纯属娱乐,如有雷同,那可能是你同事。

觉得有用的话,点个赞收藏吧~

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐