文章目录
- Q1:@SpringBootApplication 这个注解包含哪三个注解,作用是什么
- Q2:@Controller @Component @Service 这三个注解,有什么区别
- Q3:线上的机器 CPU的占用率很高,我想知道是哪个哪个方法导致的,怎么查看
- 如何排查线上机器CPU占用率高的问题
- Q4:现在的redis有三个节点,这三个节点都是一主,一从,也就是一共6台机器。现在我要再加两个节点,这两个节点也是一主一从。怎么扩容?
- Q5:Dubbo+nacos。我现在A去调用B服务,发现超时,如何排查问题
- Q6:Redis 大key问题怎么解决
- Q7:Mybaits-plus 如何将一个字符串查出来之后直接转换成一个对象,不要使用 JsonUtil.toBean
- Q8:使用Arthas排查时间段内最耗时的三个方法
- Q9:completableFuture 有哪些常用方法?
- Q10:hashmap 1.7和1.8的差别
- Q11:ConcurrentHashMap原理
- Q12:Naocs是如何实现动态配置的
- Q13:Nacos和服务的通信,是短链接,还是长连接 还是 websocket?
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-web
、spring-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 层) |
为什么需要不同的注解?
- 代码可读性:通过注解名称直接明确类的职责(如
@Service
表示业务逻辑)。 - AOP 增强:未来可能针对特定注解实现切面(如
@Transactional
通常用于@Service
)。 - 框架扩展: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原因
- 死循环:检查线程堆栈中是否有明显的循环调用
- 频繁GC:通过jstat确认是否GC导致
- 锁竞争:大量线程处于BLOCKED状态
- 算法效率低:某些方法执行时间过长
- 第三方库问题:如日志组件、网络通信等
6. 解决方案
- 优化热点代码(火焰图显示最耗CPU的部分)
- 调整JVM参数(特别是堆大小和GC相关)
- 修复死循环或锁竞争问题
- 对高负载接口进行限流或降级
- 考虑水平扩展或服务拆分
注意事项
- 生产环境操作要谨慎,避免影响线上服务
- 尽量在低峰期进行诊断
- 保留现场数据(线程dump、GC日志等)供后续分析
- 考虑使用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:端口
注意事项
业务低峰期操作:数据迁移会影响性能
监控迁移进度:
redis-cli --cluster info 集群任意节点IP:端口
客户端更新:确保客户端支持集群动态发现,或更新客户端节点列表
备份数据:操作前备份集群数据
槽位迁移:迁移过程中相关键不可用,建议设置:
redis-cli --cluster set-timeout 集群任意节点IP:端口 60000 # 设置超时为60秒
从节点添加:确保每个主节点都有至少一个从节点
扩容后验证
检查集群状态:
redis-cli CLUSTER INFO
测试数据读写:
redis-cli -c -p 6379 SET test_key "hello" redis-cli -c -p 6379 GET test_key
检查各节点负载是否均衡
通过以上步骤,你可以安全地将Redis集群从3主3从扩容到5主5从架构。
Q5:Dubbo+nacos。我现在A去调用B服务,发现超时,如何排查问题
一、基础网络与连通性检查
网络连通性测试
- 使用
ping
和telnet
检查服务提供者网络连通性 - 执行
telnet B服务IP 20880
确认Dubbo端口是否可达 - 使用
traceroute
分析网络路径是否存在异常
- 使用
容器网络配置(如使用Docker)
- 检查Docker网络模式(建议使用host模式或确保自定义网桥配置正确)
- 确认Nacos注册的IP是否为容器实际可访问IP
二、Dubbo配置检查
超时时间设置
检查Dubbo消费者端的
timeout
配置是否合理建议基于业务P99响应时间设置,留有20%缓冲
示例配置:
<dubbo:reference interface="com.example.BService" timeout="3000" retries="0"/>
重试机制
- 非幂等操作必须设置
retries=0
防止重复请求 - 幂等操作可适当设置重试次数
- 非幂等操作必须设置
负载均衡策略
检查当前负载均衡策略(建议使用
leastactive
或random
)配置示例:
<dubbo:reference loadbalance="leastactive"/>
三、服务端性能排查
B服务性能分析
- 检查B服务CPU、内存使用情况
- 使用
jstack
分析线程堆栈,查找阻塞线程 - 监控GC日志,排除GC停顿导致的超时
数据库与外部依赖
- 检查是否存在慢SQL或数据库连接池问题
- 确认外部服务调用是否超时
Dubbo线程模型
检查Dubbo服务提供者的线程池状态
调整线程池参数:
<dubbo:protocol threads="200"/>
四、Nacos注册中心问题排查
Nacos健康状态
- 检查Nacos Server是否正常运行
- 查看Nacos日志是否有502等错误
- 确认磁盘IO性能(历史曾因磁盘问题导致心跳失败)
服务注册信息
- 检查Nacos控制台确认B服务是否正常注册
- 确认注册的IP和端口是否正确(特别注意Docker环境)
客户端TIME_WAIT问题
- 检查是否存在大量TIME_WAIT连接
- 升级Nacos客户端版本(旧版本存在HTTP连接不重用问题)
五、高级诊断工具使用
Arthas诊断
# 监控Dubbo调用 watch com.alibaba.dubbo.rpc.filter.ConsumerContextFilter invoke '{params,returnObj,throwExp}' -x 3 # 分析最忙线程 thread -n 3
分布式追踪
- 集成SkyWalking或Zipkin追踪跨服务调用链
- 定位具体超时的环节
网络抓包分析
tcpdump -i any port 20880 -w dubbo.pcap
六、常见问题解决方案
Docker环境特殊问题
确保Nacos注册的是宿主机可访问IP(而非容器内部IP)
尝试改用host网络模式:
docker run --network=host ...
GC导致的超时
添加JVM参数收集GC日志:
-XX:+PrintGCDetails -Xloggc:/path/to/gc.log
对于Docker环境,显式设置GC线程数:
-XX:ParallelGCThreads=4 -XX:ConcGCThreads=4
Nacos客户端优化
开启本地缓存加载:
namingLoadCacheAtStart=true
调整心跳间隔:
nacos.client.beat.interval=5000
七、系统性优化建议
异步化改造
将耗时操作改为异步执行:
CompletableFuture.runAsync(() -> { // 耗时操作 });
熔断降级
集成Sentinel实现熔断降级
配置示例:
@DubboReference(parameters = {"blockHandlerClass", "MyBlockHandler"}) private BService bService;
监控告警
- 部署Dubbo Admin控制台监控QPS和响应时间
- 设置超时率告警(如单接口超时率>5%)
通过以上步骤的系统性排查,通常可以定位到Dubbo+Nacos环境下服务调用超时的根本原因。建议按照从简到繁的顺序进行排查,先确认网络和基础配置,再深入分析性能问题。
Q6:Redis 大key问题怎么解决
一、什么是 Redis 大 Key 问题?
大 Key 是指存储在 Redis 中单个 Key 对应的 Value 过大(通常以 KB 或 MB 为单位),导致 Redis 性能下降或稳定性问题的现象。具体表现包括:
- 内存占用高:单个 Key 占用大量内存
- 操作阻塞:对大 Key 的操作耗时过长,阻塞 Redis 单线程
- 网络负载大:传输大 Key 消耗过多带宽
- 持久化问题: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 的最佳实践
设计阶段:
- 预估 Value 大小,提前设计拆分方案
- 避免使用 Redis 存储文件等二进制大数据
开发阶段:
- 代码审查时检查 Redis 使用方式
- 实现自动检测大 Key 的监控脚本
运维阶段:
- 定期扫描大 Key(如每周一次)
- 设置内存告警阈值(如单 Key > 1MB 告警)
监控体系:
# 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);
}
注意事项
- 性能考虑:频繁的 JSON 解析会影响性能,对于大量数据查询建议在数据库层面处理
- 空值处理:确保你的 TypeHandler 正确处理 null 值
- 复杂对象:如果对象结构复杂,确保有适当的默认构造函数和 setter 方法
- 版本兼容:不同版本的 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)' # 分析特定调用
结果解读技巧
- 火焰图:最顶层的宽条表示最耗时的代码路径
- monitor输出:关注
avg
和max
耗时高的方法 - trace结果:查找
#cost
值最大的调用链 - 线程状态:在dashboard中关注
RUNNABLE
状态的线程
注意事项
- 生产环境谨慎使用,采样会影响性能(通常<5%)
- 对于短时间方法调用,适当增加采样时间(建议至少30秒)
- 结合
-n
参数限制输出条目数,避免信息过载 - 分析完成后及时退出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);
注意事项
- 线程池管理:避免无限制使用默认线程池(尤其在大量任务时)。
- 异常传播:
thenApply
不会处理前序任务的异常,需配合exceptionally
或handle
。 - 阻塞风险:
get()
会阻塞线程,异步场景优先使用回调(如thenAccept
)。
如果需要更复杂的组合逻辑(如重试机制),可以结合 RetryUtils 或 Spring 的 @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; // 真正的哈希表
}
关键操作
put 操作:
- 先定位 Segment,然后锁定该 Segment
- 在对应 Segment 内执行类似 HashMap 的 put 操作
- 最后释放锁
get 操作:
- 不锁定 Segment,通过 volatile 保证可见性
- 可能读取到稍旧的数据(弱一致性)
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;
}
关键优化
put 操作流程:
- 计算 hash 定位桶位置
- 如果桶为空,CAS 插入新节点
- 否则 synchronized 锁定桶头节点
- 处理链表或红黑树插入
扩容机制:
- 多线程协同扩容
- 通过 ForwardingNode 标识正在迁移的桶
- 线程在操作时发现扩容会协助迁移
size 计数:
- 使用 LongAdder 思想(CounterCell 数组)
- 避免 CAS 竞争带来的性能问题
并发特性对比
特性 | JDK 1.7 | JDK 1.8 |
---|---|---|
锁粒度 | Segment 级别(默认16个) | 桶级别(更细粒度) |
并发度 | 最大 Segment 数量 | 理论上与桶数量相同 |
数据结构 | 数组+链表 | 数组+链表+红黑树 |
哈希冲突性能 | O(n) | 最坏 O(log n) |
内存占用 | 较高(Segment 对象开销) | 更低 |
关键设计思想
- 减小锁粒度:从分段到桶级别,减少竞争
- 无锁读操作:通过 volatile 保证可见性
- 乐观锁尝试:优先使用 CAS 操作
- 并发扩容:多线程协同完成数据迁移
- 死锁预防:按顺序锁定多个桶
使用建议
- 在 JDK 1.8+ 环境下性能更好
- 适合读多写少的场景
- 迭代器是弱一致性的(反映创建时的或更新后的状态)
- 不需要外部同步即可保证线程安全
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. 客户端实现原理
配置初始化流程
- 从本地快照加载配置
- 向Nacos服务器请求最新配置
- 比较MD5值决定是否更新
- 注册监听器
监听器机制
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. 性能优化设计
- 多级缓存:内存 → 本地文件 → 数据库
- 批量处理:配置变更合并处理
- 异步化:客户端回调采用线程池异步执行
- 压缩传输:大配置内容使用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主要基于以下考虑:
- 兼容性:HTTP协议更通用,不需要额外协议支持
- 服务端压力:长连接会占用大量服务端资源
- 实现复杂度:长轮询已能满足需求且更简单
- 防火墙友好:HTTP更易通过企业防火墙
4. 通信流程示例
配置监听流程(长轮询):
服务发现流程:
5. 版本差异
- Nacos 1.x:主要使用HTTP+UDP
- Nacos 2.x:引入了gRPC用于集群通信,但客户端通信仍保持HTTP为主
6. 性能优化策略
减少数据传输:
- 配置中心使用MD5比较
- 服务发现只推送变更通知
连接复用:
- HTTP客户端使用连接池
- 合理设置长轮询超时时间(默认30s)
本地缓存:
- 客户端缓存配置和服务列表
- 故障时降级使用本地缓存
Nacos这种混合通信设计在保证实时性的同时,兼顾了系统稳定性和可扩展性,适合大规模微服务场景。