面试遇到的问题

发布于:2025-07-16 ⋅ 阅读:(19) ⋅ 点赞:(0)

文章目录

Q1:@SpringBootApplication 这个注解包含哪三个注解,作用是什么

1. @SpringBootApplication(最核心注解)

作用:这是一个组合注解,包含以下三个关键注解的功能:

  • @SpringBootConfiguration:标记该类为 Spring Boot 的配置类(基于 @Configuration)。
  • @EnableAutoConfiguration:启用 Spring Boot 的自动配置机制(根据依赖自动配置 Bean)。
  • @ComponentScan:自动扫描当前包及其子包下的 @Component@Service@Controller 等注解的类,并注册为 Bean。

示例

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

2. @EnableAutoConfiguration

作用:单独启用 Spring Boot 的自动配置(通常不需要单独使用,因为 @SpringBootApplication 已包含)。

  • 根据项目的依赖(如 spring-boot-starter-webspring-boot-starter-data-jpa)自动配置 Spring 应用。
  • 例如:引入 spring-boot-starter-web 后,会自动配置 Tomcat 和 Spring MVC。

示例

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class MyApplication { /* ... */ }

3. @ComponentScan

作用:指定 Spring 扫描 Bean 的包路径(默认扫描启动类所在包及其子包)。

  • 可通过 basePackages 参数自定义扫描路径。
  • 通常不需要显式添加,除非需要覆盖默认扫描规则。

示例

@SpringBootApplication
@ComponentScan(basePackages = {"com.example", "org.utils"})
public class MyApplication { /* ... */ }

总结

注解 作用 是否必须
@SpringBootApplication 组合注解(含配置、自动配置、组件扫描) (启动类通常只需此注解)
@EnableAutoConfiguration 启用自动配置 已包含在 @SpringBootApplication
@ComponentScan 定义组件扫描范围 默认已包含,需自定义路径时才显式使用

实际开发中,启动类只需一个 @SpringBootApplication 即可,其他注解仅在特殊场景下单独使用。

Q2:@Controller @Component @Service 这三个注解,有什么区别

在 Spring 框架中,@Component@Service@Controller 都是用于标识 Spring 管理的 Bean 的注解,但它们有不同的语义和用途。以下是它们的核心区别:


1. @Component

  • 作用:最基础的注解,标记一个类为 Spring 的 组件(Bean),由 Spring 容器管理。

  • 用途:适用于任何需要被 Spring 管理的类(通用型)。

  • 示例

    @Component
    public class MyUtility {
        // 工具类或通用组件
    }
    
  • 衍生注解
    @Service@Controller@Repository 都是 @Component特化形式(功能相同,但语义更明确)。


2. @Service

  • 作用:标记一个类为 业务逻辑层(Service 层) 的组件。

  • 用途:用于实现业务逻辑(如计算、事务管理等),通常被 @Controller@RestController 调用。

  • @Component 的区别

    • 功能完全相同,但 @Service 的语义更清晰(表明这是业务逻辑类)。
    • Spring 本身不强制区别,但团队协作时推荐按分层使用。
  • 示例

    @Service
    public class UserService {
        public void createUser(User user) { /* 业务逻辑 */ }
    }
    

3. @Controller

  • 作用:标记一个类为 表现层(Web 层)控制器,处理 HTTP 请求。

  • 用途:用于 Spring MVC 或 Spring WebFlux,定义路由和请求处理逻辑。

  • @Component 的区别

    • @Controller@Component 的特化,专用于 Web 层。
    • 结合 @RequestMapping@GetMapping 等注解定义端点。
  • 示例

    @Controller
    public class UserController {
        @GetMapping("/users")
        public String listUsers(Model model) { /* 返回视图或数据 */ }
    }
    

关键区别总结

注解 层级 用途 是否可互换
@Component 通用 任何需要 Spring 管理的类 (但语义不明确)
@Service 业务层 业务逻辑处理 是(但推荐按分层使用)
@Controller 表现层 处理 HTTP 请求 否(专用于 Web 层)

为什么需要不同的注解?

  1. 代码可读性:通过注解名称直接明确类的职责(如 @Service 表示业务逻辑)。
  2. AOP 增强:未来可能针对特定注解实现切面(如 @Transactional 通常用于 @Service)。
  3. 框架扩展:Spring 可能对特定注解提供额外功能(如 @Controller 支持 Web 请求绑定)。

常见问题

Q:如果混用会怎样?
A:功能上无影响(Spring 均会注册为 Bean),但会降低代码可维护性。例如:

  • @Component 替代 @Controller:Web 请求仍能工作,但团队可能困惑为何不用 @Controller
  • @Service 替代 @Repository:事务管理可能正常,但数据库异常转换等特性可能丢失。

最佳实践:严格按分层使用对应注解!

Q3:线上的机器 CPU的占用率很高,我想知道是哪个哪个方法导致的,怎么查看

如何排查线上机器CPU占用率高的问题

当线上机器CPU占用率高时,你需要系统性地排查是哪个Java方法导致的。以下是详细的排查步骤:

1. 快速确认Java进程CPU使用情况

top -c

htop

查看哪个Java进程CPU占用率高,记下PID(进程ID)

2. 使用JDK工具分析Java进程

方法一:使用top命令定位高CPU线程

top -H -p [PID]  # 查看该进程下所有线程的CPU使用情况

记下高CPU占用的线程ID(TID),将其转换为16进制:

printf "%x\n" [TID]

方法二:使用jstack抓取线程堆栈

jstack [PID] > thread_dump.log

然后在thread_dump.log中搜索之前转换的16进制线程ID,找到对应的线程堆栈

方法三:使用jstat查看GC情况(排除GC问题)

jstat -gcutil [PID] 1000 5  # 每秒一次,共5次

3. 使用Arthas工具实时诊断(推荐)

# 下载并启动Arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar [PID]

# 常用命令
dashboard          # 整体监控面板
thread -n 3        # 查看最忙的3个线程
thread [TID]       # 查看指定线程堆栈
profiler start     # 开始采样
profiler stop      # 停止采样并生成火焰图

4. 生成火焰图精确定位

# 使用async-profiler(需安装)
./profiler.sh -d 30 -f /tmp/flamegraph.html [PID]

火焰图可以直观显示CPU时间消耗在哪些方法上

5. 常见高CPU原因

  1. 死循环:检查线程堆栈中是否有明显的循环调用
  2. 频繁GC:通过jstat确认是否GC导致
  3. 锁竞争:大量线程处于BLOCKED状态
  4. 算法效率低:某些方法执行时间过长
  5. 第三方库问题:如日志组件、网络通信等

6. 解决方案

  1. 优化热点代码(火焰图显示最耗CPU的部分)
  2. 调整JVM参数(特别是堆大小和GC相关)
  3. 修复死循环或锁竞争问题
  4. 对高负载接口进行限流或降级
  5. 考虑水平扩展或服务拆分

注意事项

  1. 生产环境操作要谨慎,避免影响线上服务
  2. 尽量在低峰期进行诊断
  3. 保留现场数据(线程dump、GC日志等)供后续分析
  4. 考虑使用APM工具(如SkyWalking、Pinpoint)长期监控

通过以上步骤,你应该能精确定位到导致CPU高的具体Java方法,然后针对性地进行优化。

Q4:现在的redis有三个节点,这三个节点都是一主,一从,也就是一共6台机器。现在我要再加两个节点,这两个节点也是一主一从。怎么扩容?

当前架构

  • 现有集群:3个主节点 + 3个从节点(共6台机器)
  • 计划扩容:新增2个主节点 + 2个从节点(共4台机器,总计10台)

扩容步骤

1. 准备新节点

  • 在新服务器上安装相同版本的Redis
  • 配置文件保持与现有集群一致(特别是集群相关参数)
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 15000

2. 加入新主节点

# 在任意一台现有Redis节点上执行(假设新节点IP为192.168.1.101)
redis-cli --cluster add-node 192.168.1.101:6379 现有集群任意节点IP:端口

3. 加入新从节点

# 先加入集群作为主节点
redis-cli --cluster add-node 192.168.1.102:6379 现有集群任意节点IP:端口

# 然后将其设置为某个主节点的从节点
redis-cli -h 192.168.1.102 -p 6379
> CLUSTER REPLICATE <目标主节点ID>  # 主节点ID可通过CLUSTER NODES命令查看

4. 重新分配哈希槽(关键步骤)

# 启动重新分片
redis-cli --cluster reshard 现有集群任意节点IP:端口

# 系统会交互式询问:
1. 要移动多少个槽位?(建议:16384/5≈3276个槽位/新节点)
2. 接收槽位的目标节点ID(输入新主节点ID)
3. 从哪些节点转移槽位?(可输入"all"从所有现有节点平均转移)
4. 确认分片方案

5. 平衡集群(可选)

# 自动平衡各节点槽位数量
redis-cli --cluster rebalance --cluster-use-empty-masters 集群任意节点IP:端口

6. 验证集群状态

redis-cli --cluster check 集群任意节点IP:端口

注意事项

  1. 业务低峰期操作:数据迁移会影响性能

  2. 监控迁移进度

    redis-cli --cluster info 集群任意节点IP:端口
    
  3. 客户端更新:确保客户端支持集群动态发现,或更新客户端节点列表

  4. 备份数据:操作前备份集群数据

  5. 槽位迁移:迁移过程中相关键不可用,建议设置:

    redis-cli --cluster set-timeout 集群任意节点IP:端口 60000  # 设置超时为60秒
    
  6. 从节点添加:确保每个主节点都有至少一个从节点

扩容后验证

  1. 检查集群状态:

    redis-cli CLUSTER INFO
    
  2. 测试数据读写:

    redis-cli -c -p 6379 SET test_key "hello"
    redis-cli -c -p 6379 GET test_key
    
  3. 检查各节点负载是否均衡

通过以上步骤,你可以安全地将Redis集群从3主3从扩容到5主5从架构。

Q5:Dubbo+nacos。我现在A去调用B服务,发现超时,如何排查问题

一、基础网络与连通性检查

  1. 网络连通性测试

    • 使用 pingtelnet 检查服务提供者网络连通性
    • 执行 telnet B服务IP 20880 确认Dubbo端口是否可达
    • 使用 traceroute 分析网络路径是否存在异常
  2. 容器网络配置(如使用Docker)

    • 检查Docker网络模式(建议使用host模式或确保自定义网桥配置正确)
    • 确认Nacos注册的IP是否为容器实际可访问IP

二、Dubbo配置检查

  1. 超时时间设置

    • 检查Dubbo消费者端的 timeout 配置是否合理

    • 建议基于业务P99响应时间设置,留有20%缓冲

    • 示例配置:

      <dubbo:reference interface="com.example.BService" timeout="3000" retries="0"/>
      
  2. 重试机制

    • 非幂等操作必须设置 retries=0 防止重复请求
    • 幂等操作可适当设置重试次数
  3. 负载均衡策略

    • 检查当前负载均衡策略(建议使用 leastactiverandom

    • 配置示例:

      <dubbo:reference loadbalance="leastactive"/>
      

三、服务端性能排查

  1. B服务性能分析

    • 检查B服务CPU、内存使用情况
    • 使用 jstack 分析线程堆栈,查找阻塞线程
    • 监控GC日志,排除GC停顿导致的超时
  2. 数据库与外部依赖

    • 检查是否存在慢SQL或数据库连接池问题
    • 确认外部服务调用是否超时
  3. Dubbo线程模型

    • 检查Dubbo服务提供者的线程池状态

    • 调整线程池参数:

      <dubbo:protocol threads="200"/>
      

四、Nacos注册中心问题排查

  1. Nacos健康状态

    • 检查Nacos Server是否正常运行
    • 查看Nacos日志是否有502等错误
    • 确认磁盘IO性能(历史曾因磁盘问题导致心跳失败)
  2. 服务注册信息

    • 检查Nacos控制台确认B服务是否正常注册
    • 确认注册的IP和端口是否正确(特别注意Docker环境)
  3. 客户端TIME_WAIT问题

    • 检查是否存在大量TIME_WAIT连接
    • 升级Nacos客户端版本(旧版本存在HTTP连接不重用问题)

五、高级诊断工具使用

  1. Arthas诊断

    # 监控Dubbo调用
    watch com.alibaba.dubbo.rpc.filter.ConsumerContextFilter invoke '{params,returnObj,throwExp}' -x 3
    
    # 分析最忙线程
    thread -n 3
    
  2. 分布式追踪

    • 集成SkyWalking或Zipkin追踪跨服务调用链
    • 定位具体超时的环节
  3. 网络抓包分析

    tcpdump -i any port 20880 -w dubbo.pcap
    

六、常见问题解决方案

  1. Docker环境特殊问题

    • 确保Nacos注册的是宿主机可访问IP(而非容器内部IP)

    • 尝试改用host网络模式:

      docker run --network=host ...
      
  2. GC导致的超时

    • 添加JVM参数收集GC日志:

      -XX:+PrintGCDetails -Xloggc:/path/to/gc.log
      
    • 对于Docker环境,显式设置GC线程数:

      -XX:ParallelGCThreads=4 -XX:ConcGCThreads=4
      
  3. Nacos客户端优化

    • 开启本地缓存加载:

      namingLoadCacheAtStart=true
      
    • 调整心跳间隔:

      nacos.client.beat.interval=5000
      

七、系统性优化建议

  1. 异步化改造

    • 将耗时操作改为异步执行:

      CompletableFuture.runAsync(() -> {
          // 耗时操作
      });
      
  2. 熔断降级

    • 集成Sentinel实现熔断降级

    • 配置示例:

      @DubboReference(parameters = {"blockHandlerClass", "MyBlockHandler"})
      private BService bService;
      
  3. 监控告警

    • 部署Dubbo Admin控制台监控QPS和响应时间
    • 设置超时率告警(如单接口超时率>5%)

通过以上步骤的系统性排查,通常可以定位到Dubbo+Nacos环境下服务调用超时的根本原因。建议按照从简到繁的顺序进行排查,先确认网络和基础配置,再深入分析性能问题。

Q6:Redis 大key问题怎么解决

一、什么是 Redis 大 Key 问题?

大 Key 是指存储在 Redis 中单个 Key 对应的 Value 过大(通常以 KB 或 MB 为单位),导致 Redis 性能下降或稳定性问题的现象。具体表现包括:

  1. 内存占用高:单个 Key 占用大量内存
  2. 操作阻塞:对大 Key 的操作耗时过长,阻塞 Redis 单线程
  3. 网络负载大:传输大 Key 消耗过多带宽
  4. 持久化问题:AOF 重写或 RDB 保存时处理大 Key 效率低

二、大 Key 的判定标准

数据类型 大 Key 标准
String Value > 10KB
Hash/Set/ZSet 元素数量 > 5,000
List 元素数量 > 10,000
Stream 条目数 > 5,000

三、检测大 Key 的方法

1. 使用 redis-cli 扫描

# 扫描整个实例
redis-cli --bigkeys

# 采样扫描(更快但可能遗漏)
redis-cli --bigkeys -i 0.1  # 每100ms扫描一次

2. 使用 MEMORY USAGE 命令

redis-cli MEMORY USAGE your_key_name

3. 使用 Redis RDB 工具分析

rdb -c memory dump.rdb --bytes 10240 > large_keys.csv

4. 线上实时监控(阿里云/腾讯云等提供的监控指标)

四、大 Key 的解决方案

1. 数据拆分(最推荐方案)

String 类型

# 原始大 Key
SET user:1000:profile "非常大的JSON数据..."

# 拆分为多个小 Key
SET user:1000:profile:basic "{基础信息}"
SET user:1000:profile:contact "{联系方式}"
SET user:1000:profile:history "{历史记录}"

Hash 类型

# 原始大 Hash
HSET product:1000 field1 val1 field2 val2 ... field5000 val5000

# 按字段前缀拆分
HSET product:1000:part1 field1 val1 ... field1000 val1000
HSET product:1000:part2 field1001 val1001 ... field2000 val2000

2. 使用压缩(适用于文本数据)

# 写入时压缩
SET user:1000:profile.gz "压缩后的数据"

# 读取时解压(需客户端处理)

3. 数据分片(Sharding)

// 客户端分片示例
public String getShardedData(String key, int shard) {
    String shardKey = key + ":" + (shard % 10);
    return redis.get(shardKey);
}

4. 过期时间管理

# 对大 Key 设置合理的过期时间
EXPIRE large_key 3600

5. 数据结构优化

原结构 问题 优化方案
大 String 更新成本高 改用 Hash 分字段存储
大 List 随机访问慢 改用 ZSet 分页查询
大 Set 交集计算慢 拆分多个 Set 并行计算

五、预防大 Key 的最佳实践

  1. 设计阶段

    • 预估 Value 大小,提前设计拆分方案
    • 避免使用 Redis 存储文件等二进制大数据
  2. 开发阶段

    • 代码审查时检查 Redis 使用方式
    • 实现自动检测大 Key 的监控脚本
  3. 运维阶段

    • 定期扫描大 Key(如每周一次)
    • 设置内存告警阈值(如单 Key > 1MB 告警)
  4. 监控体系

    # Prometheus + Grafana 监控示例
    redis_memory_usage_bytes{key="large_key"} > 1048576
    

六、特殊场景处理

1. 已存在大 Key 的迁移方案

# 使用 SCAN + DUMP/RESTORE 渐进式迁移
redis-cli --scan --pattern "large_*" | while read key; do
    redis-cli --pipe << EOF
    DUMP "$key" | head -c 102400 | redis-cli -x RESTORE "$key:part1" 0
    DUMP "$key" | tail -c +102401 | redis-cli -x RESTORE "$key:part2" 0
    DEL "$key"
EOF
done

2. 热点大 Key 处理

  • 增加本地缓存(如 Caffeine)
  • 实现多级缓存策略
  • 考虑读写分离架构

通过以上方法,可以有效解决和预防 Redis 大 Key 问题,保障 Redis 的高性能运行。

Q7:Mybaits-plus 如何将一个字符串查出来之后直接转换成一个对象,不要使用 JsonUtil.toBean

方法一:使用 TypeHandler(推荐)

1. 创建自定义 TypeHandler

@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(YourObject.class)
public class JsonToObjectTypeHandler extends BaseTypeHandler<YourObject> {
    
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                  YourObject parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, JSON.toJSONString(parameter));
    }

    @Override
    public YourObject getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String json = rs.getString(columnName);
        return parseJsonToObject(json);
    }

    @Override
    public YourObject getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String json = rs.getString(columnIndex);
        return parseJsonToObject(json);
    }

    @Override
    public YourObject getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String json = cs.getString(columnIndex);
        return parseJsonToObject(json);
    }

    private YourObject parseJsonToObject(String json) {
        if (StringUtils.isBlank(json)) {
            return null;
        }
        return JSON.parseObject(json, YourObject.class);
    }
}

2. 在实体类中使用

@TableName("your_table")
public class YourEntity {
    
    @TableField(value = "json_column", typeHandler = JsonToObjectTypeHandler.class)
    private YourObject yourObject;
    
    // getter/setter
}

方法二:使用 MyBatis-Plus 的自动结果映射

public interface YourMapper extends BaseMapper<YourEntity> {
    
    @Select("SELECT json_column FROM your_table WHERE id = #{id}")
    @Results({
        @Result(property = "yourObject", column = "json_column", 
                javaType = YourObject.class, 
                typeHandler = JsonToObjectTypeHandler.class)
    })
    YourEntity selectWithObject(@Param("id") Long id);
}

方法三:使用 MyBatis-Plus 的 Wrapper 查询

// 查询时自动转换
YourEntity entity = yourService.getOne(
    Wrappers.<YourEntity>lambdaQuery()
        .eq(YourEntity::getId, id)
        .select(YourEntity::getJsonColumn) // 假设getJsonColumn返回YourObject类型
);

方法四:使用 MyBatis 的 @ConstructorArgs 注解(适用于构造函数注入)

public interface YourMapper extends BaseMapper<YourEntity> {
    
    @Select("SELECT json_column FROM your_table WHERE id = #{id}")
    @ConstructorArgs({
        @Arg(column = "json_column", javaType = YourObject.class, 
             typeHandler = JsonToObjectTypeHandler.class)
    })
    YourEntity selectWithConstructor(@Param("id") Long id);
}

注意事项

  1. 性能考虑:频繁的 JSON 解析会影响性能,对于大量数据查询建议在数据库层面处理
  2. 空值处理:确保你的 TypeHandler 正确处理 null 值
  3. 复杂对象:如果对象结构复杂,确保有适当的默认构造函数和 setter 方法
  4. 版本兼容:不同版本的 MyBatis-Plus 对 TypeHandler 的支持可能略有不同

以上方法都能实现从数据库字符串到 Java 对象的自动转换,避免了手动调用 JsonUtil.toBean 的步骤。

Q8:使用Arthas排查时间段内最耗时的三个方法

1. 启动Arthas并附加到目标JVM

# 下载并启动Arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

# 选择目标Java进程(输入数字编号)

2. 使用profiler命令进行采样分析(推荐)

# 启动采样(默认CPU热点分析)
profiler start -d 60  # 采样60秒

# 停止采样并生成火焰图
profiler stop --format html  # 生成HTML格式报告

火焰图会直观显示最耗时的调用栈,顶部最宽的部分就是最耗时的代码路径。

3. 使用monitor命令监控方法耗时

# 监控特定类的方法(统计周期10秒,统计3次)
monitor -c 10 -n 3 com.example.YourClass *

输出示例:

method[com.example.YourClass.method1] cnt[1023] avg[12.34ms] max[45.67ms] min[8.90ms]
method[com.example.YourClass.method2] cnt[456] avg[23.45ms] max[67.89ms] min[15.67ms]

4. 使用trace命令追踪方法调用链

# 追踪特定方法(耗时超过10ms的调用)
trace com.example.YourClass yourMethod '#cost > 10' -n 3

5. 使用dashboard查看实时热点

dashboard  # 查看实时线程CPU占用

Q退出dashboard视图后,可以查看线程统计信息。

6. 组合使用time tunnel记录和重放(高级)

# 开始记录方法调用
tt -t com.example.YourClass *
# 一段时间后停止记录
tt -l  # 列出记录
tt -i 1004 -w 'target.method(args)'  # 分析特定调用

结果解读技巧

  1. 火焰图:最顶层的宽条表示最耗时的代码路径
  2. monitor输出:关注avgmax耗时高的方法
  3. trace结果:查找#cost值最大的调用链
  4. 线程状态:在dashboard中关注RUNNABLE状态的线程

注意事项

  1. 生产环境谨慎使用,采样会影响性能(通常<5%)
  2. 对于短时间方法调用,适当增加采样时间(建议至少30秒)
  3. 结合-n参数限制输出条目数,避免信息过载
  4. 分析完成后及时退出Arthas(stop命令)

通过以上方法,你可以准确找出指定时间段内最耗时的三个Java方法,并获取它们的调用上下文和性能指标。

Q9:completableFuture 有哪些常用方法?

CompletableFuture 是 Java 8 引入的异步编程工具,提供了丰富的链式调用和组合操作。以下是其 常用方法分类详解,附带示例代码和典型场景:


1. 创建异步任务

方法 说明 示例
runAsync(Runnable) 无返回值的异步任务 CompletableFuture.runAsync(() -> System.out.println("Task running"))
supplyAsync(Supplier) 有返回值的异步任务 CompletableFuture.supplyAsync(() -> "Result")
指定线程池 默认用 ForkJoinPool.commonPool(),可自定义 supplyAsync(() -> "Result", Executors.newFixedThreadPool(10))

2. 结果处理(链式调用)

(1)同步处理结果
方法 说明 示例
thenApply(Function) 对结果同步转换 future.thenApply(s -> s + " processed")
thenAccept(Consumer) 消费结果(无返回值) future.thenAccept(System.out::println)
thenRun(Runnable) 结果完成后执行操作 future.thenRun(() -> System.out.println("Done"))
(2)异步处理结果
方法 说明 示例
thenApplyAsync(Function) 异步转换结果 future.thenApplyAsync(s -> s + " async")
thenAcceptAsync(Consumer) 异步消费结果 future.thenAcceptAsync(System.out::println)

3. 组合多个 Future

(1)依赖前序任务
方法 说明 示例
thenCompose(Function) 扁平化嵌套 Future futureA.thenCompose(resultA -> futureB(resultA))
handle(BiFunction) 处理结果或异常 future.handle((res, ex) -> ex != null ? "fallback" : res)
(2)聚合多个任务
方法 说明 示例
thenCombine(CompletionStage, BiFunction) 合并两个任务结果 futureA.thenCombine(futureB, (a, b) -> a + b)
allOf(CompletableFuture...) 所有任务完成后触发 CompletableFuture.allOf(futures).thenRun(...)
anyOf(CompletableFuture...) 任意任务完成后触发 CompletableFuture.anyOf(futures).thenAccept(...)

4. 异常处理

方法 说明 示例
exceptionally(Function) 捕获异常并返回默认值 future.exceptionally(ex -> "Error: " + ex.getMessage())
whenComplete(BiConsumer) 无论成功/失败都执行 future.whenComplete((res, ex) -> { if (ex != null) log.error(ex); })

5. 主动控制

方法 说明 示例
complete(T value) 手动完成任务 future.complete("Manual result")
completeExceptionally(Throwable) 手动失败任务 future.completeExceptionally(new RuntimeException())
cancel(boolean mayInterrupt) 取消任务 future.cancel(true)

6. 状态检查

方法 说明 示例
isDone() 任务是否完成(成功/失败/取消) if (future.isDone()) { ... }
isCompletedExceptionally() 是否因异常完成 if (future.isCompletedExceptionally()) { ... }
get() / get(long, TimeUnit) 阻塞获取结果(需处理异常) String result = future.get(5, TimeUnit.SECONDS)
join() 类似 get(),但不抛受检异常 String result = future.join()

典型场景示例

1. 链式异步调用
CompletableFuture.supplyAsync(() -> fetchUserData())
    .thenApplyAsync(user -> processData(user))
    .thenAcceptAsync(result -> sendResult(result))
    .exceptionally(ex -> {
        System.err.println("Error: " + ex);
        return null;
    });
2. 并行任务聚合
CompletableFuture<String> futureA = fetchDataA();
CompletableFuture<String> futureB = fetchDataB();

futureA.thenCombine(futureB, (a, b) -> a + " & " + b)
       .thenAccept(System.out::println);
3. 超时控制
future.completeOnTimeout("Timeout Fallback", 2, TimeUnit.SECONDS)
      .thenAccept(System.out::println);

注意事项

  1. 线程池管理:避免无限制使用默认线程池(尤其在大量任务时)。
  2. 异常传播thenApply 不会处理前序任务的异常,需配合 exceptionallyhandle
  3. 阻塞风险get() 会阻塞线程,异步场景优先使用回调(如 thenAccept)。

如果需要更复杂的组合逻辑(如重试机制),可以结合 RetryUtilsSpring 的 @Async 扩展。

Q10:hashmap 1.7和1.8的差别

1. 底层数据结构

  • JDK 1.7:数组 + 链表
  • JDK 1.8:数组 + 链表 + 红黑树(当链表长度超过阈值8时转换为红黑树)

2. 哈希冲突处理

  • 1.7:仅使用链表解决冲突
  • 1.8:先使用链表,链表过长时转为红黑树,提高查询效率

3. 插入数据方式

  • 1.7:头插法(容易在多线程环境下导致死循环)
  • 1.8:尾插法(解决了死循环问题,但仍非线程安全)

4. 扩容机制

  • 1.7:先扩容再插入新元素
  • 1.8:先插入新元素再扩容
  • 1.8优化:扩容时利用高位低位链表,避免重新计算hash

5. hash算法简化

  • 1.7:使用多次位运算
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
    
  • 1.8:仅一次异或和移位
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    

6. 性能优化

  • 1.8:引入红黑树使最坏情况下的时间复杂度从O(n)提升到O(log n)
  • 1.8:扩容时更高效,利用节点已有的hash值

7. 方法变化

  • 1.8新增
    • getOrDefault()
    • putIfAbsent()
    • compute()
    • merge()等新方法
    • 支持函数式编程

8. 迭代器改进

  • 1.8:迭代器在遍历过程中检测到结构性修改会快速失败(fast-fail)

这些改进使JDK 1.8中的HashMap在性能上(特别是哈希冲突严重时)有显著提升,同时保持了良好的API兼容性。

Q11:ConcurrentHashMap原理

JDK 1.7 实现原理

分段锁机制 (Segment)

  • 数据结构:由 Segment 数组 + HashEntry 数组组成
  • 并发控制:每个 Segment 继承自 ReentrantLock,相当于一个独立的 HashMap
  • 分段优点:不同 Segment 的操作可以并行,默认 16 个 Segment(并发级别)
final Segment<K,V>[] segments; // 分段数组
static final class Segment<K,V> extends ReentrantLock {
    transient volatile HashEntry<K,V>[] table; // 真正的哈希表
}

关键操作

  1. put 操作

    • 先定位 Segment,然后锁定该 Segment
    • 在对应 Segment 内执行类似 HashMap 的 put 操作
    • 最后释放锁
  2. get 操作

    • 不锁定 Segment,通过 volatile 保证可见性
    • 可能读取到稍旧的数据(弱一致性)
  3. size 操作

    • 尝试无锁统计两次,如果修改次数相同则返回
    • 否则锁定所有 Segment 后统计

JDK 1.8 实现原理

重大改进:CAS + synchronized

  • 移除分段锁,改为 Node 数组 + 链表/红黑树
  • 锁粒度更细:只锁定当前桶(链表头/树根节点)
  • 并发控制
    • CAS 用于无锁初始化、扩容等
    • synchronized 锁定单个桶
transient volatile Node<K,V>[] table; // 类似 HashMap 的结构
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
}

关键优化

  1. put 操作流程

    • 计算 hash 定位桶位置
    • 如果桶为空,CAS 插入新节点
    • 否则 synchronized 锁定桶头节点
    • 处理链表或红黑树插入
  2. 扩容机制

    • 多线程协同扩容
    • 通过 ForwardingNode 标识正在迁移的桶
    • 线程在操作时发现扩容会协助迁移
  3. size 计数

    • 使用 LongAdder 思想(CounterCell 数组)
    • 避免 CAS 竞争带来的性能问题

并发特性对比

特性 JDK 1.7 JDK 1.8
锁粒度 Segment 级别(默认16个) 桶级别(更细粒度)
并发度 最大 Segment 数量 理论上与桶数量相同
数据结构 数组+链表 数组+链表+红黑树
哈希冲突性能 O(n) 最坏 O(log n)
内存占用 较高(Segment 对象开销) 更低

关键设计思想

  1. 减小锁粒度:从分段到桶级别,减少竞争
  2. 无锁读操作:通过 volatile 保证可见性
  3. 乐观锁尝试:优先使用 CAS 操作
  4. 并发扩容:多线程协同完成数据迁移
  5. 死锁预防:按顺序锁定多个桶

使用建议

  1. 在 JDK 1.8+ 环境下性能更好
  2. 适合读多写少的场景
  3. 迭代器是弱一致性的(反映创建时的或更新后的状态)
  4. 不需要外部同步即可保证线程安全

ConcurrentHashMap 通过精妙的并发控制设计,在保证线程安全的同时提供了接近 HashMap 的性能表现。

Q12:Naocs是如何实现动态配置的

1. 配置存储模型

Nacos 采用三层存储模型实现配置管理:

  • 内存配置缓存:使用 ConcurrentHashMap 存储,保证高性能读取
  • 本地快照文件:客户端缓存配置到本地,防止服务端不可用时使用
  • 持久化存储:支持 Derby(内置)和 MySQL(生产推荐)等数据库

2. 配置变更推送机制

长轮询 (Long Polling)

  • 服务端:持有客户端请求最多30秒,期间如有配置变更立即返回
  • 客户端:收到响应后立即重新发起请求,形成"准实时"推送效果
  • 优势:相比传统轮询减少无效请求,相比WebSocket更轻量
// 典型的长轮询实现代码逻辑
while (true) {
    String newConfig = checkConfigUpdate(lastConfigId);
    if (newConfig != null) {
        return newConfig; // 配置有更新立即返回
    }
    Thread.sleep(30000); // 最多等待30秒
}

增量更新

  • 客户端记录配置的MD5摘要,服务端只推送变更部分
  • 减少网络传输数据量

3. 客户端实现原理

配置初始化流程

  1. 从本地快照加载配置
  2. 向Nacos服务器请求最新配置
  3. 比较MD5值决定是否更新
  4. 注册监听器

监听器机制

configService.addListener(dataId, group, new Listener() {
    @Override
    public void receiveConfigInfo(String configInfo) {
        // 处理配置变更
        refreshBean(configInfo);
    }
});

4. 服务端集群同步

  • Distro协议:Nacos自研的AP分布式协议

    • 每个节点负责部分数据(分片)
    • 新数据写入负责节点后异步复制到其他节点
    • 保证最终一致性
  • Raft协议:用于持久化层的一致性保证

    • 选主机制
    • 日志复制

5. 动态配置应用场景示例

Spring Cloud 集成

# bootstrap.yml
spring:
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
        refresh-enabled: true # 启用自动刷新

动态日志级别调整

@NacosConfigListener(dataId = "logback-config")
public void onLogConfigUpdate(String newLogConfig) {
    LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
    // 动态调整日志级别
}

6. 性能优化设计

  1. 多级缓存:内存 → 本地文件 → 数据库
  2. 批量处理:配置变更合并处理
  3. 异步化:客户端回调采用线程池异步执行
  4. 压缩传输:大配置内容使用gzip压缩

7. 安全机制

  • 权限控制:命名空间(Namespace)隔离 + 账号权限体系
  • 配置加密:支持敏感配置项加密存储
  • 历史版本:保留配置修改历史,支持回滚
  • 发布审核:重要配置可配置审批流程

Nacos通过这种设计实现了配置的"一次发布,实时生效",在微服务架构中广泛应用,相比传统配置中心(Zookeeper/Consul等)具有更友好的UI和更灵活的配置管理能力。

Q13:Nacos和服务的通信,是短链接,还是长连接 还是 websocket?

Nacos 客户端与服务端之间的通信采用了混合模式,根据不同的功能和场景使用不同的通信机制:

1. 主要通信方式

配置中心通信

  • 长轮询 (Long Polling) - 主要方式
    • 客户端发起HTTP请求,服务端hold住连接(默认30秒)
    • 期间如有配置变更立即返回响应
    • 无变更则超时后返回,客户端立即重新发起请求
    • 本质上是HTTP短连接的变种,但实现了类似长连接的效果

服务注册发现

  • UDP + HTTP 混合
    • 服务注册:HTTP短连接(PUT请求)
    • 服务心跳:HTTP短连接(PUT请求)
    • 服务发现:首次HTTP查询,后续变更通过UDP推送
    • 健康检查:混合模式(TCP/HTTP/MYSQL等)

2. 具体协议分析

功能模块 通信协议 连接类型 端口
配置获取 HTTP 短连接+长轮询 8848
配置监听 HTTP长轮询 伪长连接 8848
服务注册 HTTP 短连接 8848
服务心跳 HTTP 短连接 8848
服务发现 HTTP+UDP 短连接+推送 8848/9848
集群节点间通信 gRPC/HTTP 混合 7848/9848

3. 为什么不是WebSocket?

Nacos没有采用WebSocket主要基于以下考虑:

  1. 兼容性:HTTP协议更通用,不需要额外协议支持
  2. 服务端压力:长连接会占用大量服务端资源
  3. 实现复杂度:长轮询已能满足需求且更简单
  4. 防火墙友好:HTTP更易通过企业防火墙

4. 通信流程示例

配置监听流程(长轮询):

Client Server 发起配置查询请求(带MD5) 立即返回新配置 保持连接30秒 立即返回新配置 返回空响应 alt [期间发生变更] [无变更] alt [配置有变更] [无变更] 立即发起新请求(循环) Client Server

服务发现流程:

Client Server HTTP查询服务列表(首次) 返回全量服务列表 UDP推送变更通知(后续) HTTP查询变更详情(当收到UDP通知时) Client Server

5. 版本差异

  • Nacos 1.x:主要使用HTTP+UDP
  • Nacos 2.x:引入了gRPC用于集群通信,但客户端通信仍保持HTTP为主

6. 性能优化策略

  1. 减少数据传输

    • 配置中心使用MD5比较
    • 服务发现只推送变更通知
  2. 连接复用

    • HTTP客户端使用连接池
    • 合理设置长轮询超时时间(默认30s)
  3. 本地缓存

    • 客户端缓存配置和服务列表
    • 故障时降级使用本地缓存

Nacos这种混合通信设计在保证实时性的同时,兼顾了系统稳定性和可扩展性,适合大规模微服务场景。


网站公告

今日签到

点亮在社区的每一天
去签到