26年2月北京独创时代 Java开发工程师 二面
1. Spring Boot Actuator中/health端点如何自定义数据库健康检查逻辑?
思路
核心讲“实现HealthIndicator接口+配置”,区分“扩展默认检查”和“自定义全量检查”两种场景,结合代码示例说明。
回答示例
Spring Boot Actuator的/health端点默认提供基础数据库健康检查(如连接是否可用),自定义数据库健康检查需实现HealthIndicator接口,步骤如下:
1. 基础配置(开启健康检查)
management:
endpoints:
web:
exposure:
include: health # 暴露health端点
endpoint:
health:
show-details: always # 显示详细健康信息(生产建议when_authorized)
2. 自定义数据库健康检查(示例:检查核心表是否可访问)
@Component
public class CustomDbHealthIndicator implements HealthIndicator {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Health health() {
try {
// 自定义检查逻辑:查询核心表(如user)是否可访问
jdbcTemplate.queryForObject("SELECT 1 FROM user LIMIT 1", Integer.class);
// 可选:添加自定义健康详情
return Health.up()
.withDetail("db_status", "正常")
.withDetail("check_table", "user")
.build();
} catch (Exception e) {
// 检查失败,返回DOWN状态+异常信息
return Health.down()
.withDetail("error", e.getMessage())
.withDetail("db_status", "异常")
.build();
}
}
}
3. 扩展默认数据库健康检查(仅补充信息)
若需保留默认检查(如连接池状态),可继承AbstractHealthIndicator:
@Component
public class ExtendedDbHealthIndicator extends AbstractHealthIndicator {
@Autowired
private DataSource dataSource;
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
// 调用默认检查(如连接有效性)
try (Connection conn = dataSource.getConnection()) {
builder.up().withDetail("connection_url", conn.getMetaData().getURL());
} catch (Exception e) {
builder.down(e);
}
// 补充自定义检查(如慢查询数)
int slowQueryCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM slow_query_log WHERE create_time > NOW() - INTERVAL 5 MINUTE", Integer.class);
builder.withDetail("slow_query_count_5min", slowQueryCount);
}
}
核心逻辑:实现HealthIndicator接口,在health()方法中编写自定义检查逻辑,通过Health.up()/down()返回健康状态,Actuator会自动聚合所有HealthIndicator的结果到/health端点。
2. MySQL Online DDL执行期间,ALGORITHM=COPY与INPLACE对业务的影响差异?
思路
从“锁表策略、数据拷贝、业务可用性”三个核心维度对比,明确不同场景的选型建议。
回答示例
MySQL Online DDL的ALGORITHM参数决定DDL的执行方式,COPY和INPLACE对业务的影响差异显著:
特性 |
ALGORITHM=COPY |
ALGORITHM=INPLACE |
核心逻辑 |
拷贝原表数据到新表,DDL完成后替换原表 |
直接在原表上修改(仅重构数据/索引,无全量拷贝) |
锁表策略 |
全程加表级写锁(WRITE LOCK),读正常/写阻塞 |
大部分场景仅加元数据锁(MDL),读/写基本不阻塞(仅短时间MDL锁) |
数据拷贝 |
全量表拷贝,IO/CPU消耗大 |
无全量拷贝(索引重建仅处理索引数据),资源消耗小 |
业务影响 |
写操作长时间阻塞,适用于低峰期/小表 |
读写基本无感知,适用于生产环境/大表 |
典型场景 |
不支持INPLACE的DDL(如修改字段类型、字符集) |
加索引、删索引、修改字段注释、调整自增值 |
空间占用 |
需额外1倍表空间(存储新表) |
仅需少量临时空间(索引重建) |
关键补充:
INPLACE并非“无锁”:如修改字段长度(varchar(10)→varchar(20))仍会短时间阻塞写操作,但远短于COPY;- 8.0版本引入
INSTANT算法(部分DDL无需拷贝/修改数据,仅改元数据),比INPLACE更轻量。
3. Redis主从切换时,哨兵模式下客户端如何感知新主节点地址?
思路
讲哨兵的“主节点监控→故障判定→切换→通知客户端”全流程,核心是客户端的“哨兵监听+地址刷新”逻辑。
回答示例
Redis哨兵(Sentinel)模式下,客户端通过哨兵通知+地址刷新机制感知主节点切换,核心流程:
1. 哨兵侧核心动作
- 哨兵集群持续监控主节点(默认每1秒PING一次),判定主节点宕机(主观下线→客观下线);
- 哨兵集群选举出leader,执行主从切换(将最优从节点升级为新主节点);
- 切换完成后,哨兵通过发布订阅机制(频道
+switch-master)广播新主节点地址(IP+端口)。
2. 客户端侧感知逻辑
主流客户端(Jedis/Lettuce)内置哨兵适配逻辑,步骤如下:
- 初始化时关联哨兵:客户端配置哨兵地址(而非直接配置主节点),启动时从哨兵获取当前主节点地址;
- 监听哨兵频道:客户端订阅哨兵的
+switch-master频道,实时接收主节点切换通知; - 自动刷新主节点地址:
- 透明重试:切换过程中失败的命令,客户端自动重试新主节点(需业务保证幂等)。
示例(Jedis哨兵配置):
Set<String> sentinelNodes = new HashSet<>(Arrays.asList("127.0.0.1:26379", "127.0.0.1:26380"));
JedisPoolConfig poolConfig = new JedisPoolConfig();
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinelNodes, poolConfig);
// 客户端自动从哨兵获取主节点,切换时自动刷新
Jedis jedis = pool.getResource();
核心:客户端不直接依赖主节点地址,而是通过哨兵动态获取,切换过程对业务透明(仅需处理短暂的重试/超时)。
4. RabbitMQ死信队列(DLQ)触发条件及消息流转路径?
思路
先明确死信的触发条件,再拆解“原队列→死信交换机→死信队列”的流转路径,结合示例配置说明。
回答示例
死信队列(DLQ)是存储“无法正常消费”消息的队列,核心触发条件和流转路径如下:
1. 死信触发条件(满足任一即触发)
- 消息被拒绝(Reject/Nack)且不重入:消费者调用
basicReject/basicNack,且参数requeue=false; - 消息过期:队列设置
x-message-ttl(消息过期时间),或消息本身设置过期时间,超时未消费; - 队列达到最大长度:队列设置
x-max-length,消息数量超限,最早的消息被挤入死信队列。
2. 消息流转路径
graph LR
A[生产者] --> B[原队列(配置死信参数)]
B --> C{正常消费?}
C -- 否(触发死信条件) --> D[死信交换机(DLX)]
D --> E[死信队列(DLQ)]
C -- 是 --> F[消费者]
E --> G[死信消费者(人工处理/重试)]
3. 核心配置(原队列绑定死信参数)
// 1. 声明死信交换机(DLX)
channel.exchangeDeclare("dlx.exchange", BuiltinExchangeType.DIRECT, true);
// 2. 声明死信队列(DLQ)
channel.queueDeclare("dlq.queue", true, false, false, null);
// 3. 绑定死信队列到死信交换机
channel.queueBind("dlq.queue", "dlx.exchange", "dlx.routing.key");
// 4. 声明原队列,配置死信参数
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "dlx.exchange"); // 绑定死信交换机
args.put("x-dead-letter-routing-key", "dlx.routing.key"); // 死信路由键
args.put("x-message-ttl", 60000); // 消息过期时间60s
args.put("x-max-length", 1000); // 队列最大长度1000
channel.queueDeclare("origin.queue", true, false, false, args);
关键:死信队列需提前声明,原队列通过参数绑定死信交换机/路由键,消息成为死信后自动路由到死信队列。
5. JVM参数-XX:MaxMetaspaceSize未设置时,元空间OOM的典型场景?
思路
先说明MaxMetaspaceSize默认值(无上限,受物理内存限制),再拆解OOM的核心触发场景,结合JVM内存管理逻辑说明。
回答示例
-XX:MaxMetaspaceSize默认未设置时,元空间(Metaspace)理论上无上限(仅受操作系统物理内存限制),但仍会触发OutOfMemoryError: Metaspace,典型场景:
1. 动态生成大量类且类加载器未释放
- 场景:频繁使用动态代理(Spring AOP、MyBatis Mapper)、ASM字节码生成、Groovy/JSP动态编译,生成大量类元数据;
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏在精不在多,内容分为八股文、大厂真实面经,面试通过后将offer和面试题私发给我,可退还专栏的收益部分费用。欢迎大家共建专栏
