跳到主要内容Java 全栈面试题及答案汇总 | 极客日志Javajava算法
Java 全栈面试题及答案汇总
综述由AI生成汇总了 Java 全栈开发常见面试题,涵盖 JDK 1.8 新特性、集合框架原理(HashMap、ArrayList 等)、并发编程、消息中间件(RabbitMQ、Kafka 区别及可靠性)、微服务架构(Nacos、Seata、Feign)、数据库设计及项目实战经验。内容包含理论解析与代码示例,适合面试复习参考。
修罗2.5K 浏览 JDK 1.8 特性
- Lambda 表达式:简化匿名内部类的编写,支持函数式编程。
- Stream API:提供声明式处理集合数据的能力,支持过滤、映射、归约等操作。比如 filter,sum,max,collect
- 新的日期时间 API(java.time 包):如 LocalDate、LocalTime,LocalDateTime 解决旧 Date 类的线程安全问题。
- 默认方法:允许在接口中定义具体实现方法,便于接口扩展。接口的方法使用 default 关键字修饰
- 函数式接口:Lambda 表达式的使用前提,是一种特殊的接口。可使用@FunctionalInterface 修饰
- Optional 类:用于避免空指针异常,封装可能为 null 的值。Optional.ofNullable(T t),orElse(T t)
- HashMap 底层优化(红黑树)
静态方法引用:ClassName::staticMethod
实例方法引用(特定对象):instance::method
任意对象的实例方法引用:ClassName::instanceMethod
构造方法引用:ClassName::new
JDK 1.8 中 Stream 用法
Stream 是 JDK 1.8 中用于处理集合数据的 API,支持函数式编程风格。常用用法包括:
- 创建 Stream:通过集合的 stream() 方法或 Stream.of() 创建。
- 中间操作:如 filter()(过滤)、map()(映射)、sorted()(排序),这些操作返回新 Stream,可链式调用。
- 终端操作:如 collect()(收集为集合)、forEach()(迭代)、reduce()(归约),触发实际计算。
- 示例:list.stream().filter(x -> x > 0).map(x -> x * 2).collect(Collectors.toList()) 过滤正数并加倍后收集到列表。
Stream 具有惰性求值特性,提高处理效率。
HashMap 底层原理 || 工作原理
HashMap 基于哈希表实现,工作原理如下:
HashMap 底层结构(JDK 8)
- 数组 + 链表 + 红黑树:
- 数组:默认容量 16(2^4),存储 Node 节点(包含 hash、key、value、next)。
- 链表:哈希冲突时链接相同索引的节点,长度>8 转为红黑树。
- 红黑树:优化长链表查询性能,时间复杂度从 O(n) 降至 O(log n)。
基于哈希表存储数据的。
JDK8 之前,哈希表 = 数组 + 链表
JDK8 开始,哈希表 = 数组 + 链表 + 红黑树
哈希表是一种增删改查数据,性能都较好的数据结构
Set<String> set = new HashSet<>();
- 使用元素的哈希值对数组的长度做运算计算出应存入的位置,可以想象这个运算是取余。
- 如果不为 null,表示有元素,则调用 equals 方法比较相等,则不存入链表;不相等,则存入链表。
- JDK 8 之前,新元素存入数组,占老元素位置,老元素挂下面
- 但是如果我们链表过长不就相当于 LinkedList 了吗。所以当这 16 个元素被占用了 16(当前长度) * 上面第一步的加载因子 (0.75) = 12 个元素后,会自动扩容。数组扩容为原 2 倍。
但是会有一种可能,我的链表可以一直无限长。但是我的 HashSet 长度的元素一直没有达到扩容标准。于是加了一个另一个准则:JDK8 开始,当链表长度超过 8,且数组长度>=64 时,自动将链表转成红黑树
JDK 8 开始之后,新元素直接使用链表挂在老元素下面。
判断当前位置是否为 null,如果是 null 直接存入
创建一个默认长度 16 的数组,默认加载因子为 0.75,数组名 table
哈希计算
- 哈希值:
hash = key.hashCode() ^ (key.hashCode() >>> 16)(高位参与运算,减少冲突)。
- 数组索引:
index = hash & (capacity - 1)(等价于取模运算)。
扩容机制
- 触发条件:元素数量 > 容量 × 负载因子(默认 0.75)。
- 扩容过程:
- 新容量 = 旧容量 × 2(翻倍)。
- 重新计算每个元素的索引,复制到新数组。
- JDK 8 优化:元素要么留在原索引,要么移至原索引 + 旧容量。
List,Set,Map,Queue 的区别
Java 集合框架 ├── Collection(单元素集合根接口)接口 │ ├── List(有序、可重复)接口 │ ├── Set(无序/有序、不可重复)接口 │ └── Queue(有序、按特定规则存取,可重复)接口 └── Map(键值对集合根接口,独立体系)接口 ├── HashMap(哈希无序、key 不可重复)实现类 ├── LinkedHashMap(保留插入顺序、key 不可重复)实现类 └── TreeMap(key 排序有序、key 不可重复)实现类
- List:有序集合,元素可重复,支持索引访问。实现类有 ArrayList、LinkedList。
- Set:无序集合(某些实现如 TreeSet 有序),元素不可重复。实现类有 HashSet、TreeSet。
- Map:键值对集合,键不可重复,值可重复。实现类有 HashMap、TreeMap。
- Queue:队列,通常遵循先进先出(FIFO)或优先级顺序。实现类有 LinkedList、PriorityQueue。
ArrayList 与 LinkedList,Vector 的区别
- ArrayList:基于动态数组,支持随机访问(通过索引),时间复杂度 O(1);但插入和删除元素需移动后续元素,平均 O(n)。非线程安全。
- LinkedList:基于双向链表,插入和删除元素效率高(只需修改指针),平均 O(1);但随机访问需遍历链表,平均 O(n)。非线程安全。
- Vector:类似 ArrayList,但线程安全(方法使用 synchronized 修饰),性能较低。在现代 Java 中,通常用 CopyOnWriteArrayList 替代。
Hashtable 与 HashMap,HashSet 的区别
| 类名 | 所属接口 | 存储形式 | 线程安全 | null 值支持 | 底层结构 | 核心用途 |
|---|
| HashMap | Map | <Key, Value> | 否 | 1 个 null Key + 多个 null Value | 数组 + 链表 + 红黑树(JDK1.8+) | 普通键值对存储(高性能) |
| Hashtable | Map | <Key, Value> | 是 | 不允许 null Key /null Value | 数组 + 链表(无红黑树优化) | 老旧系统兼容(现已过时) |
| HashSet | Set | 单个元素 E | 否 | 1 个 null 元素(依赖 HashMap) | 封装 HashMap(Key 存元素) | 元素去重(无需关联额外数据) |
JAVA 中那些集合是线程安全的
- 传统集合:Vector、Hashtable、Stack(通过 synchronized 实现)
- Collections工具类包装:
- Collections.synchronizedList(new ArrayList())
- Collections.synchronizedSet(new HashSet())
- Collections.synchronizedMap(new HashMap())
- JUC 包(并发编程核心工具包)中的并发集合:
- ConcurrentHashMap:分段锁实现的线程安全 Map
- CopyOnWriteArrayList:写时复制的 List
- CopyOnWriteArraySet:写时复制的 Set
- ConcurrentLinkedQueue:无锁队列
- BlockingQueue implementations:如 ArrayBlockingQueue、LinkedBlockingQueue
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
Map<String,Integer> safeMap = Collections.synchronizedMap(new HashMap<>());
面向对象三大特征及解释
- 封装:隐藏类的内部实现细节,对外提供一个可访问的接口。
- 继承:子类继承父类属性和方法,实现代码复用(如 extends 关键字)
- 多态:同一种事物,由于条件不同,产生的结果也不同。即同一个引用类型,使用不同的实例而执行不同的操作。
方法覆盖与方法重载的区别
- 在父类与子类之间
- 方法名相同
- 参数列表相同
- 返回值类型相同或是其子类
- 访问权限不能严于父类
- 在同一个类中
- 方法名相同
- 参数个数或类型不同
- 与返回值和访问修饰符无关
| 比较项 | 位置 | 方法名 | 参数表 | 返回值 | 访问修饰符 |
|---|
| 方法重写 | 子类 | 相同 | 相同 | 相同或是其子类 | 不能比父类更严格 |
| 方法重载 | 同类 | 相同 | 不相同 | 无关 | 无关 |
JAVA 接口与抽象类的区别
| 成员类型 | 抽象类(Abstract Class) | 接口(Interface) |
|---|
| 成员变量 | 可以有普通实例变量、静态变量 | 仅能有 public static final 常量(默认修饰符,可省略) |
| 构造方法 | 有构造方法(用于子类初始化父类成员) | 无构造方法(接口不能被实例化,也无需初始化成员) |
| 普通方法 | 可以有普通成员方法(有完整实现) | Java 8 前:无;Java 8 后:可包含 default 默认方法(有实现) |
| 抽象方法 | 可以有抽象方法(abstract 修饰,无实现) | Java 8 前:仅能有抽象方法(默认 public abstract,可省略);Java 8 后:仍支持抽象方法 |
| 静态方法 | 可以有静态方法(有实现) | Java 8 后:可包含静态方法(有实现,仅接口自身可调用) |
| 私有方法 | 可以有私有方法(Java 7+,用于内部复用) | Java 9 后:可包含私有方法(用于默认方法间的代码复用) |
| 继承性 | 一个类可以实现多个接口 | 一个类只能继承一个抽象类 |
| 关键字 | 使用 extends继承 | 使用 implements实现 |
hashCode() 和 equals() 方法的重要性体现在什么地方
把哈希表(比如 HashMap/HashSet)想象成小区的快递柜:
- hashCode():返回快递柜的编号(比如 10 号柜),作用是快速定位对象的存储位置,提高查找效率;
- equals():核对快递的收件人、手机号、地址等信息,是判断'两个对象是否真的相等'的最终依据。
显然,不同的快递可能被分配到同一个快递柜(哈希值相同),但这些快递并不是同一个(equals() 返回 false)——这就是哈希冲突的直观体现。
- equals() 定义了对象的逻辑相等规则,是业务层面判断'两个对象是否相同'的最终标准;
- hashCode() 为哈希集合提供快速定位能力,其实现必须遵守与 equals() 的核心约定(相等对象的哈希值必相等);
两者的正确重写是 HashMap/HashSet 等哈希集合正常工作的前提,否则会出现数据重复、查找失败、性能低下等问题。
说出你所知道的异常类型?
- 按继承结构分类
Throwable(所有异常/错误的超类)
- Error:系统级错误,程序无法处理(如 OutOfMemoryError)
- Exception:程序可处理的异常
- RuntimeException:运行时异常(未检查异常)
- 其他 Exception:检查异常
- 常见运行时异常(RuntimeException):
- NullPointerException:空指针异常
- ArrayIndexOutOfBoundsException:数组越界
- ClassCastException:类型转换异常
- IllegalArgumentException:非法参数异常
- NumberFormatException:数字格式异常
- 常见检查异常(Checked Exception):
- IOException:IO 操作异常
- SQLException:数据库操作异常
- FileNotFoundException:文件未找到
- ClassNotFoundException:类未找到
- InterruptedException:线程中断异常
RabbitMQ、RocketMQ 和 Kafka 区别及特点
| 特性维度 | RabbitMQ | RocketMQ | Kafka |
|---|
| 核心定位 | 企业级消息代理,强调可靠投递和灵活路由 | 高可靠、高并发的分布式消息中间件,尤其擅长事务场景 | 高吞吐量的分布式流式处理平台,为大数据和日志而生 |
| 吞吐量 | 万级到十万级/秒 | 十万级/秒 | 百万级/秒 |
| 消息延迟 | 微秒级,延迟极低 | 毫秒级 | 毫秒级到数十毫秒级 |
| 消息可靠性 | 极高,提供多种可靠性保障机制 | 高,支持同步刷盘和复制,保证消息不丢失 | 高,通过多副本机制保障,但可能因异步处理有微弱丢失风险 |
| 典型应用场景 | 实时任务、业务解耦、秒杀排队 | 电商交易、金融支付、订单处理 | 日志采集、大数据流处理、用户行为跟踪 |
RabbitMQ 的死信队列和延时队列的使用
死信队列
- 消息被消费者拒绝(basic.reject()/basic.nack()),且设置 requeue=false(不重新入队);
- 消息达到过期时间(TTL,Time-To-Live);
- 队列达到最大长度(x-max-length),新消息入队时挤掉旧消息。
@Configuration
public class DlxQueueConfig {
@Bean
public Queue normalQueue() {
Map<String,Object> args = new HashMap<>();
args.put("x-dead-letter-exchange","dlx.exchange");
args.put("x-dead-letter-routing-key","dlx.key");
return QueueBuilder.durable("normal.queue").withArguments(args).build();
}
}
延时队列
核心概念:延时队列是指消息发送后不立即被消费,而是延迟指定时间后才被消费的队列。
RabbitMQ 无原生延时队列,主流实现有 2 种:
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|
| 消息 TTL + 死信队列 | 无需安装插件、轻量 | 存在'消息堆积'问题(先过期的消息后消费) | 延时时间短、低并发 |
| RabbitMQ 延迟插件(推荐) | 精准延时、无堆积问题 | 需要安装插件 | 生产环境、高并发 |
实现方式 2:延迟插件(生产环境推荐)
步骤 1:安装延迟插件
下载对应 RabbitMQ 版本的 rabbitmq_delayed_message_exchange 插件,启用插件:
@Bean
public CustomExchange delayExchange() {
Map<String,Object> args = new HashMap<>();
args.put("x-delayed-type","direct");
return new CustomExchange(DELAY_EXCHANGE,"x-delayed-message",true,false, args);
}
实现方式 1:消息 TTL + 死信队列
只需在普通队列配置 x-message-ttl(消息过期时间),消息过期后自动进入死信队列,死信消费者即可'延时消费'。
@Configuration
public class DlxQueueConfig {
@Bean
public Queue normalQueue() {
Map<String,Object> args = new HashMap<>();
args.put("x-dead-letter-exchange","dlx.exchange");
args.put("x-dead-letter-routing-key","dlx.key");
args.put("x-message-ttl",10000);
args.put("x-max-length",10);
return QueueBuilder.durable("normal.queue").withArguments(args).build();
}
}
RabbitMQ 应用场景(轻量灵活,中小系统首选)
应用场景:
异步处理:邮件发送、短信通知
流量削峰:秒杀活动、高并发请求
日志处理:系统日志收集分析
任务调度:定时任务、工作流
应用解耦:系统间数据同步
实现方案:
消息队列:RabbitMQ、RocketMQ、Kafka
内存队列:Disruptor、LinkedBlockingQueue
数据库:使用表模拟队列
Redis:List 结构实现简单队列
如何保证消息的可靠性
- 生产者保证:
- 事务机制:性能较低,不推荐
- 确认机制(Confirm 模式):
普通确认:同步等待 broker 确认
批量确认:批量发送后统一确认
异步确认:回调函数处理确认结果
- Broker 保证:
- 持久化:
- 交换机持久化
- 队列持久化
- 消息持久化(delivery_mode=2)
- 镜像队列:多节点备份,防止单点故障
- 消费者保证:
- 手动确认(ACK 机制):
- 消费重试:设置重试次数和重试策略
项目中如何集成消息队列
- 引入依赖:添加消息队列客户端依赖
- 配置连接 (yml 文件):配置服务器地址、端口、认证信息 (账号 guest,密码 guest)
- 定义交换机/队列:声明需要的交换机和队列
- 生产者配置:配置消息发送模板
- 消费者配置:配置监听器和消息处理逻辑
- 异常处理:配置重试、死信队列等容错机制
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
connection-timeout: 15000
cache:
channel:
size: 50
connection:
size: 10
listener:
simple:
concurrency: 1
max-concurrency: 10
acknowledge-mode: auto
retry:
enabled: true
max-attempts: 3
@Configuration
public class RabbitMqConfig {
@Bean
public Queue demoQueue() {
return new Queue(RabbitMqConstants.DEMO_QUEUE,true,false,false);
}
}
@Component
@RequiredArgsConstructor
public class RabbitMqProducer {
private final RabbitTemplate rabbitTemplate;
public void sendDemoMessage(Object data) {
String msg = JSONUtil.toJsonStr(data);
rabbitTemplate.convertAndSend(RabbitMqConstants.DEMO_EXCHANGE,RabbitMqConstants.DEMO_ROUTING_KEY, msg );
log.info("[sendDemoMessage] 发送 RabbitMQ 消息成功,内容:{}", msg);
}
}
@Slf4j
@Component
public class RabbitMqConsumer {
@RabbitListener(queues = RabbitMqConstants.DEMO_QUEUE)
public void consumeDemoMessage(String message) {
log.info("[consumeDemoMessage] 接收 RabbitMQ 消息:{}", message);
}
}
public class DemoMqController {
private final RabbitMqProducer rabbitMqProducer;
@Operation(summary = "发送测试消息")
@GetMapping("/send")
public CommonResult<String> sendDemoMessage() {
rabbitMqProducer.sendDemoMessage("集成 RabbitMQ 测试消息");
return CommonResult.success("消息发送成功");
}
}
队列的类型有那些
- 普通队列
先进先出(FIFO)队列
最基本的消息队列类型
- 工作队列(Work Queue)
多个消费者竞争消费
实现负载均衡
- 发布/订阅队列
广播模式,所有消费者收到相同消息
通过交换机绑定实现
- 路由队列(Routing)
根据路由键选择性消费
实现消息过滤
- 主题队列(Topic)
支持通配符的路由匹配
灵活的消息路由
- 优先级队列
支持消息优先级
高优先级消息优先消费
- 延迟队列
消息延迟投递
用于定时任务
如何实现公平分发
首先先明确:RabbitMQ 默认采用轮询分发(Round-Robin) —— 不管消费者的处理能力,会把消息依次平均推送给所有消费者,这会导致处理速度慢的消费者堆积大量消息,处理快的消费者却处于空闲状态,资源利用率低。
而公平分发的核心目标是:让消费者处理完当前消息后,再获取下一条消息,按消费者的实际处理能力分配消息,避免资源浪费。实现关键有 2 个核心步骤:
- 关闭自动消息确认(autoAck=false):确保消费者只有处理完消息并手动确认后,RabbitMQ 才会认为这条消息已处理。
- 设置预取消息数(prefetchCount=1):限制 RabbitMQ 每次只给消费者推送 1 条未确认的消息,消费者处理完并确认后,才会收到下一条。
自动反馈
自动反馈是接收很多消息后再去进行处理。此时 A,B 就会争先恐后的拿数据,就会出现对半接收的情况。就 不会出现性能好的执行多一点,性能不好的执行少一点的情况。也就是 B 很菜,但是一直拿,不管我能不能跑的完,很贪心,B 拿多了,它还处理的慢。典型的贪多嚼不烂。
手动反馈
只有当我处理完当前的消息后才去拿新的消息。也就是能者多劳。此时就能出现 性能好的执行多一点,性能不好的执行少一点的情况。除此之外我们还需要设置每次只允许通道中只有一条消息。
核心配置(关键!决定公平分发)
在 application.yml 中配置 RabbitMQ 连接信息 + 公平分发核心参数:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
listener:
simple:
acknowledge-mode: manual
prefetch: 1
concurrency: 1
max-concurrency: 1
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitFairConfig {
public static final String FAIR_QUEUE = "fair_dispatch_queue";
public static final String NORMAL_QUEUE = "normal_queue";
@Bean(name = "fairContainerFactory")
public SimpleRabbitListenerContainerFactory fairContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setAcknowledgeMode(SimpleMessageListenerContainer.AcknowledgeMode.MANUAL);
factory.setPrefetchCount(1);
factory.setConcurrentConsumers(1);
factory.setMaxConcurrentConsumers(1);
return factory;
}
@Bean
public org.springframework.amqp.core.Queue fairQueue() {
return new org.springframework.amqp.core.Queue(FAIR_QUEUE,true);
}
@Bean
public org.springframework.amqp.core.Queue normalQueue() {
return new org.springframework.amqp.core.Queue(NORMAL_QUEUE,true);
}
}
解释 RabbitMQ 消息堆积的原因,如何临时紧急扩容
- 生产速度 > 消费速度:突发流量或消费端故障
- 消费者性能问题:处理逻辑复杂或资源不足
- 消费者数量不足:并发消费者配置过少
- 网络问题:消费端与 MQ 服务器网络延迟
- 增加消费者
水平扩展消费端实例
调整消费者并发数
- 优化消费者
简化消费逻辑
使用批量处理
增加线程池大小
- 队列优化
创建临时队列分流
调整 QoS 预取值
- 紧急处理
临时启用消息丢弃策略
使用死信队列转移积压消息
动态调整路由策略
RabbitMQ 如何保证消息不会被重复消费?
先明确:RabbitMQ 消息重复消费的根源
重复消费本质是「消息的投递 / 确认环节出现异常,导致 MQ 或生产者认为消息未被处理,进而重新投递」,常见场景:
- 生产者:发送消息后,MQ 已接收但未及时返回确认(网络波动 / 超时),生产者重发;
- 消费者:消费消息后,未发送 ACK 确认就宕机 / 重启,MQ 将消息重新入队并投递;
- 网络层:ACK 确认包丢失,MQ 误判消费失败,重新投递。
- 重复消费的核心解决方案是消费端实现幂等性(即同一消息消费多次,结果完全一致)。
- MQ 层面仅能减少重复投递,无法从根本杜绝。
因此以消费端幂等为主,生产端 / MQ 端的辅助策略
消费端实现幂等性
方案 1:基于数据库唯一索引(最常用,适合写库场景)
原理:为每条消息生成唯一标识(如 msgId),消费时先将 msgId 插入「幂等表」(或业务表),利用数据库唯一索引保证仅能插入一次,插入成功则执行业务逻辑,失败则说明消息已消费过。
Java 代码示例:
@Component
public class RabbitMQConsumer {
@Autowired
private JdbcTemplate jdbcTemplate;
@RabbitListener(queues = "business_queue")
public void consume(Message message, Channel channel) throws IOException {
String msgId = message.getMessageProperties().getMessageId();
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
String insertSql = "INSERT INTO idempotent_table (msg_id, create_time) VALUES (?, NOW())";
jdbcTemplate.update(insertSql, msgId);
String businessData = new String(message.getBody(), StandardCharsets.UTF_8);
doBusiness(businessData);
channel.basicAck(deliveryTag,false);
} catch (DuplicateKeyException e) {
System.out.println("消息已重复消费,msgId: " + msgId);
channel.basicAck(deliveryTag,false);
} catch (Exception e) {
channel.basicNack(deliveryTag,false,true);
}
}
private void doBusiness(String data) {
}
}
方案 2:基于 Redis 的 SETNX(适合非写库 / 高性能场景)
原理:利用 Redis 的 SETNX 命令(仅当 key 不存在时设置),将消息唯一标识作为 key,设置成功则消费,失败则跳过。同时设置过期时间,避免 Redis 内存溢出。
@Component
public class RabbitMQConsumer {
@Autowired
private StringRedisTemplate redisTemplate;
@RabbitListener(queues = "business_queue")
public void consume(Message message, Channel channel) throws IOException {
String msgId = message.getMessageProperties().getMessageId();
long deliveryTag = message.getMessageProperties().getDeliveryTag();
String redisKey = "idempotent:msg:" + msgId;
try {
Boolean success = redisTemplate.opsForValue().setIfAbsent(redisKey,"consumed",24, TimeUnit.HOURS);
if (Boolean.TRUE.equals(success)) {
doBusiness(new String(message.getBody()));
channel.basicAck(deliveryTag,false);
} else {
System.out.println("消息重复消费,msgId: " + msgId);
channel.basicAck(deliveryTag,false);
}
} catch (Exception e) {
channel.basicNack(deliveryTag,false,true);
}
}
}
方案 3:基于本地内存(适合单机、低并发场景)
原理:用 ConcurrentHashMap 存储已消费的 msgId,消费前先判断是否存在,适合无需持久化、单机部署的场景(集群场景需用分布式锁 / Redis)。
注意:需设置过期清理逻辑,避免内存泄漏。
MQ 层面:减少重复投递 (辅助)
通过配置 RabbitMQ 和消费端的 ACK 机制,降低重复投递概率:
- 禁用「自动 ACK」:消费端开启手动 ACK(ack-mode: manual),仅当业务逻辑执行成功后,才调用 channel.basicAck() 确认;
- 合理设置重试策略:避免无限制重试(如设置最大重试次数,超过后投递到死信队列);
- 开启生产者确认机制:确保生产者仅在 MQ 未接收时重发,而非盲目重发。
生产端:避免重复发送(辅助)
- 生成全局唯一 msgId:生产者发送消息时,为每条消息生成唯一 msgId(如 UUID),放入消息属性(messageProperties.setMessageId(msgId));
- 开启 Confirm 机制:生产者开启 RabbitMQ 的 Confirm 模式,仅当 MQ 确认接收消息后,才认为发送成功,否则重发(避免盲目重发);
- 事务消息(慎用):生产者通过 RabbitMQ 事务保证消息「要么发送成功,要么回滚」,但性能较低,一般用 Confirm 机制替代。
简述 RabbitMQ 基于什么进行传输的?协议是什么?
- RabbitMQ 基于 AMQP(Advanced Message Queuing Protocol)协议进行传输
- 使用 TCP 作为底层传输协议
- 二进制协议:高效、跨语言
- 面向消息:提供消息队列所需的所有功能
- 可靠性:支持持久化、确认机制
- 灵活性:支持多种消息模式
- Producer:消息生产者
- Consumer:消息消费者
- Broker:消息代理服务器
- Virtual Host:虚拟主机,隔离环境
- Exchange:接收消息并路由到队列
- Queue:存储消息的缓冲区
- Binding:交换机和队列的绑定关系
工作流程:
Producer → Exchange → Binding → Queue → Consumer
RabbitMQ 消息如何被优先消费
RabbitMQ 实现消息优先消费的核心是优先级队列(Priority Queue)机制:队列提前声明优先级范围,消息发送时指定优先级数值,队列会优先投递数值更高的消息(数值越大优先级越高)。
- 队列维度:声明队列时通过参数 x-max-priority 指定该队列支持的最大优先级(如 10),范围默认 0-255(建议 0-10,避免高优先级范围带来的内存 / 性能损耗)。
- 消息维度:发送消息时通过 priority 属性设置具体优先级(需≤队列的最大优先级)。
- 消费规则:当队列中有消息堆积时,RabbitMQ 会优先将高优先级消息投递给消费者;若消费速度>生产速度(队列无堆积),优先级机制无实际效果(消息刚生产就被消费,无排序机会)。
定义 RabbitMQ 优先级队列配置类
@Configuration
public class PriorityQueueConfig {
public static final String PRIORITY_QUEUE_NAME = "springboot_priority_queue";
@Bean
public Queue priorityQueue() {
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-max-priority",10);
return QueueBuilder.durable(PRIORITY_QUEUE_NAME).withArguments(arguments).build();
}
}
生产者
@Component
public class PriorityMessageProducer {
@Resource
private RabbitTemplate rabbitTemplate;
public void sendPriorityMessage(String messageContent,int priority) {
MessageProperties properties = new MessageProperties();
properties.setPriority(priority);
properties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
Message message = MessageBuilder.withBody(messageContent.getBytes()).andProperties(properties).build();
rabbitTemplate.send(PriorityQueueConfig.PRIORITY_QUEUE_NAME, message);
System.out.println("发送消息:" + messageContent + " | 优先级:" + priority);
}
}
消费者
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class PriorityMessageConsumer {
@RabbitListener(queues = PriorityQueueConfig.PRIORITY_QUEUE_NAME)
public void consumePriorityMessage(Message message) {
String content = new String(message.getBody());
Integer priority = message.getMessageProperties().getPriority();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("消费消息:" + content + " | 优先级:" + priority);
}
}
测试
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
public class PriorityQueueTest {
@Resource
private PriorityMessageProducer producer;
@Test
public void testPriorityQueue() {
producer.sendPriorityMessage("低优先级消息",1);
producer.sendPriorityMessage("中优先级消息",5);
producer.sendPriorityMessage("高优先级消息",10);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
输出:
发送消息:低优先级消息 | 优先级:1
发送消息:中优先级消息 | 优先级:5
发送消息:高优先级消息 | 优先级:10
消费消息:高优先级消息 | 优先级:10
消费消息:中优先级消息 | 优先级:5
消费消息:低优先级消息 | 优先级:1
微服务中的五大组件有那些?
- 服务注册与发现
- 核心作用:解决'微服务在哪里'的问题。服务启动时将自身信息(IP、端口、接口、健康状态)注册到注册中心;服务消费方从注册中心动态拉取可用服务列表,无需硬编码服务地址,同时支持服务上下线的自动感知。
- 典型技术:Nacos(主流)、Eureka(停更但仍有企业使用)、Consul、Zookeeper(结合 Curator)。
- API 网关
- 核心作用:作为微服务的'统一入口',承接所有前端 / 外部请求,核心能力包括:路由转发(将请求分发到对应微服务)、统一鉴权(token / 权限校验)、限流熔断、日志埋点、跨域处理、协议转换(HTTP/HTTPS ↔ RPC)。
- 典型技术:Spring Cloud Gateway(主流)、Zuul(1.x 停更,2.x 少用)、Kong、APISIX。
- 配置中心
- 核心作用:集中管理所有微服务的配置(数据库连接、服务地址、业务参数、环境配置等),支持配置动态刷新(无需重启服务即可生效),解决分布式场景下配置分散、修改繁琐的问题。
- 典型技术:Nacos(可同时做注册中心 + 配置中心)、Apollo(阿波罗,携程开源,配置管理更精细)、Spring Cloud Config。
- 服务熔断 / 降级 / 限流组件
- 核心作用:保障微服务高可用的'保险丝',核心解决分布式场景下的服务雪崩问题:
- 熔断:目标服务故障时,快速切断调用链,避免请求堆积拖垮整个系统;
- 降级:非核心功能临时关闭 / 返回默认值,保障核心流程可用;
- 限流:控制请求流量(如 QPS),避免服务被突发流量打垮。
- 典型技术:Sentinel(阿里开源,主流)、Resilience4j(轻量,替代 Hystrix)、Hystrix(已停更)。
- 远程服务调用(RPC)组件
- 核心作用:解决微服务之间的远程通信问题,相比原生 HTTP 更高效,支持负载均衡、序列化、超时重试、结果缓存等能力。
- 典型技术:
- 高性能 RPC:Dubbo(阿里开源,主流)、gRPC;
- 轻量 HTTP 调用:Spring Cloud OpenFeign(Spring 生态标配)。
CAP 原理解释
CAP 三个核心特性
- Consistency(一致性):
- 指分布式系统中,所有节点在同一时间看到的数据是完全相同的。比如用户更新了 A 节点的订单数据,那么 B、C 节点在读取时,必须立刻看到最新的订单信息,不能出现'部分节点是旧数据、部分是新数据'的情况。
- 通俗理解:分布式系统像一个班级,老师布置作业后,所有学生必须同时知道最新的作业内容,不能有人知道旧版本、有人知道新版本。
- Availability(可用性):
- 指分布式系统中,只要用户发起请求(不管是读还是写),系统必须在有限时间内给出明确的响应(成功 / 失败),不能出现'请求超时''系统无响应'的情况。
- 通俗理解:班级的答疑窗口永远不关门,学生任何时候提问,都能得到老师的回应(哪怕答案是'这个问题暂时解决不了'),不会没人理。
- Partition tolerance(分区容错性):
- 指分布式系统中,由于网络故障(比如机房断网、节点间网络不通)导致节点被分成多个'分区',系统依然能正常运行。
- 通俗理解:班级被分成两个教室(网络分区),哪怕两个教室之间无法通信,每个教室内部的学生和老师依然能正常完成学习、答疑等工作。
CAP 的核心结论:三者不可兼得,只能选其二
分布式系统的分区容错性(P)是必然要面对的(网络故障是常态),所以实际设计中,核心是在'一致性(C)'和'可用性(A)'之间做取舍:
- CP 方案(一致性 + 分区容错性):
- 优先保证一致性和分区容错,牺牲部分可用性。
- 例子:ZooKeeper。当 ZK 集群出现网络分区时,少数派分区的节点会停止对外提供服务(拒绝请求,牺牲可用性),确保多数派分区的数据一致,避免出现数据冲突。
- 适用场景:数据一致性要求极高的场景,比如分布式锁、配置中心。
- AP 方案(可用性 + 分区容错性):
- 优先保证可用性和分区容错,牺牲强一致性(允许短时间数据不一致,后续通过最终一致性补偿)。
- 例子:Eureka、Redis 集群(主从异步复制)。Eureka 集群分区后,每个节点依然接受注册和查询请求(保证可用性),分区恢复后再同步数据,短时间内不同节点的服务列表可能不一致,但最终会统一。
- 适用场景:高可用优先的场景,比如微服务注册中心、电商商品库存查询(允许短时间库存显示略有偏差)。
- CA 方案(一致性 + 可用性):
- 理论上存在,但不适合分布式系统(因为放弃了分区容错性,相当于只有一个节点 / 一个分区,本质是集中式系统)。比如单机 MySQL,没有分区问题,能同时保证一致性和可用性,但失去了分布式的扩展性。
什么是 BASE 理论?
BASE 理论是分布式系统设计中的重要理论,是对 CAP 理论的延伸和实践化,核心思想是放弃强一致性,追求最终一致性,以换取系统的高可用性和可扩展性。
- Basically Available(基本可用)
系统出现故障时,保证核心功能可用
允许损失部分非核心功能或降低性能
双十一期间关闭非核心功能保障下单;服务器故障时切换到备用节点
- Soft state(软状态)
允许系统存在中间状态
状态不需要立即同步到所有节点
转账时的'已扣款未到账'状态;分布式事务中的暂存状态
- Eventually consistent(最终一致性)
经过一段时间后,所有数据副本最终达到一致
不保证实时强一致性
NoSQL 数据库(如 Cassandra、DynamoDB);消息队列异步同步;DNS 系统
OpenFeign 怎么实现认证传递
核心思路是通过请求拦截器或显式注解将认证信息附加到 Feign 请求头中,以下是几种主流且规范的实现方式:
全局请求拦截器(最常用)
通过自定义 RequestInterceptor,在所有 Feign 请求发送前自动从当前上下文(如 ThreadLocal、SecurityContext)提取认证信息,添加到请求头,实现全局统一传递。
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
@Component
public class FeignAuthInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if(attributes == null){return;}
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if(headerNames != null){
while(headerNames.hasMoreElements()){String name = headerNames.nextElement();
if("Authorization".equals(name)||"Token".equals(name)){String value = request.getHeader(name);
requestTemplate.header(name, value);
}
}
}
}
}
局部定制
如果仅需为某个 Feign 接口 / 方法传递认证信息,可通过@RequestHeader 注解显式指定,适合个性化场景。
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
@FeignClient(name = "user-service")
public interface UserFeignClient {
@GetMapping("/user/info")
UserDTO getUserInfo(@RequestHeader("Authorization")String token);
@GetMapping("/user/roles")
default List<String> getUserRoles(){
String token = AuthContextHolder.getToken();
return getUserRoles(token);
}
@GetMapping("/user/roles")
List<String> getUserRoles(@RequestHeader("Authorization")String token);
}
@Service
public class OrderService {
@Autowired
private UserFeignClient userFeignClient;
public void getOrderUserInfo(){
String token = getCurrentToken();
UserDTO userInfo = userFeignClient.getUserInfo(token);
}
private String getCurrentToken(){
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
return attributes.getRequest().getHeader("Authorization");
}
}
结合 Spring Security/OAuth2(实战场景)
如果项目使用 Spring Security/OAuth2,认证信息会存储在 SecurityContextHolder 中,可直接从中提取 OAuth2 令牌传递:
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.stereotype.Component;
@Component
public class OAuth2FeignInterceptor implements RequestInterceptor {
private final OAuth2AuthorizedClientManager authorizedClientManager;
public OAuth2FeignInterceptor(OAuth2AuthorizedClientManager authorizedClientManager){
this.authorizedClientManager = authorizedClientManager;
}
@Override
public void apply(RequestTemplate requestTemplate) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication == null){return;}
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("my-client").principal(authentication).build();
String accessToken = authorizedClientManager.authorize(authorizeRequest).getAccessToken().getTokenValue();
requestTemplate.header("Authorization","Bearer " + accessToken);
}
}
总结
- OpenFeign 认证传递的核心方案是自定义 RequestInterceptor 全局拦截器,自动将认证头附加到请求中,适配大部分场景;
- 局部场景可通过@RequestHeader 注解显式传递认证信息,灵活性更高;
- 结合 Spring Security/OAuth2 时,需从 SecurityContextHolder 或 OAuth2AuthorizedClientManager 提取令牌,保证认证信息的合规性。
简单的介绍 Nacos 以及特点
Nacos 核心介绍
Nacos(全称 Dynamic Naming and Configuration Service)是阿里巴巴开源的一站式微服务治理平台,核心定位是动态服务发现、配置管理和服务管理平台。它整合了传统'服务注册中心'(如 Eureka、Consul)和'配置中心'(如 Apollo、Spring Cloud Config)的核心能力,无需单独部署多个组件,就能一站式解决微服务架构中服务注册发现、配置动态更新、服务元数据管理等核心问题,是微服务生态中非常主流的中间件。
Nacos 核心特点
- 一站式解决方案
同时支持服务注册发现和配置管理两大核心能力,无需整合多个组件(比如不用同时部署 Eureka+Spring Cloud Config),大幅降低微服务架构的复杂度和运维成本。
- 动态配置管理
支持配置的实时推送(基于长轮询 / 推送模式),修改配置后无需重启服务即可生效;还提供配置版本管理、灰度发布、配置回滚、多环境配置隔离(Namespace/Group)等能力,适配企业级场景。
- 灵活的服务发现能力
兼容 DNS 和 RPC(Dubbo/gRPC)两种服务发现模式,支持基于权重的负载均衡、多维度健康检查(TCP/HTTP/自定义脚本),服务异常下线时会自动剔除,保证服务调用的高可用;同时兼容 Eureka、Consul 等协议,迁移成本低。
- 高可用与易扩展
支持集群部署,基于 Raft 协议保证集群数据一致性;架构采用模块化设计,可按需扩展功能;支持跨机房部署,满足高可用架构要求。
- 易用性极强
提供可视化管理控制台,配置和服务管理操作直观;原生支持 Spring Cloud、Dubbo 等主流微服务框架,接入成本极低(几行配置即可集成),API 设计简洁,开箱即用。
- 企业级特性完善
支持命名空间(Namespace)、配置分组(Group)实现多环境 / 多业务的资源隔离;提供权限管控、审计日志、监控告警等企业级特性,满足生产环境的合规要求。
Nacos 如何实现 AP 与 CP 的切换
- nacos1.x 版本:
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
nacos 若需要强制整个集群以 CP 模式运行(比如早期配置中心场景),才会用到;2.x 版本几乎不用,官方明确建议用 ephemeral 控制实例维度的模式。
- nacos2.x 版本:
- 方式 2(也就是老版的):Nacos 服务端 API 动态切换(运维 / 应急场景,无侵入):
PUT /nacos/v1/ns/instance?serviceName=服务名&ip=实例 IP&port=实例端口&ephemeral=false
也就是在接口测试工具 (比如 apifox,postman) 发起 put 请求请求地址为:http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=order-service&ip=192.168.1.100&port=8080&ephemeral=false
方式 1:客户端代码配置:application.yml / application.properties 配置文件
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
ephemeral: true
Nacos 实现原理
Nacos 主要是两大作用一个是注册中心一个是配置中心,下面分开讲解
服务注册与发现
- 注册机制:
- 客户端侧:服务实例启动时,通过 Nacos SDK(HTTP/GRPC 协议)向 Nacos Server 发送注册请求,携带核心信息:服务名、实例 IP / 端口、元数据(如版本、机房)、健康检查参数等;客户端会启动心跳线程(默认 5 秒),定期向 Server 发送心跳包,证明实例存活。
- 服务端侧:Server 接收注册请求后,将实例信息存入双层存储结构:
- 内存层:用 ConcurrentHashMap 维护「服务名→集群→实例列表」的映射(保证高并发下的读写安全);
- 持久化层:异步将数据写入嵌入式数据库 Derby(默认)或外置 MySQL(生产推荐),避免内存数据丢失。
- 健康检查:Server 会主动检测(默认 60 秒)无心跳的实例,先标记为「不健康」,超过阈值(默认 15 秒无心跳)则从实例列表中剔除。
- 发现机制(兼顾实时性与性能):
- 拉取模式(默认):客户端定时(默认 30 秒)向 Server 拉取全量 / 增量服务实例列表,本地缓存(减少 Server 压力);
- 推送模式(核心优化):基于 HTTP 长轮询(Long Polling)实现增量推送 —— 客户端向 Server 发起长轮询请求(超时时间 30 秒),Server 若检测到服务实例无变化,则挂起请求;一旦实例状态变更(新增 / 下线 / 健康状态变化),立即唤醒挂起的请求,将变更数据推送给客户端,既减少无效轮询,又保证实时性。
配置管理
- 配置发布:
用户通过控制台 / OpenAPI 发布配置时,Server 先将配置存储到数据库(MySQL/Derby),同时缓存到内存(提升读取性能),并为每个配置生成版本号(用于版本回溯、冲突解决),还会记录配置的 DataID(配置唯一标识)、Group(分组)、Namespace(环境隔离)。
- 配置获取与监听
- 客户端启动时,向 Server 拉取指定 DataID/Group 的配置,本地缓存一份(容灾:即使 Server 宕机,客户端仍能基于本地缓存启动);
- 客户端通过 HTTP 长轮询向 Server 注册配置监听,Server 维护监听队列;当配置变更时,Server 立即唤醒对应的长轮询请求,将最新配置推送给客户端;
- 客户端接收到新配置后,先校验(如 MD5 比对),再更新本地缓存,同时触发自定义监听器(如 Spring Boot 的@RefreshScope),实现配置热更新。
你所知道的注册中心有那些?区别是什么?
一、主流的注册中心类型
目前业界常用的注册中心主要有 Zookeeper、Eureka、Nacos、Consul 这四类,其中 Nacos 是国内阿里开源后应用最广的,Eureka 虽已停更但仍有存量项目使用。
二、核心区别(从核心特性、CAP 模型、适用场景等维度对比)
| 特性 | Zookeeper | Eureka | Nacos | Consul |
|---|
| CAP 模型 | CP(强一致性,牺牲可用性) | AP(高可用性,牺牲强一致性) | 支持 AP/CP 动态切换(默认 AP) | CP(Raft 协议保证强一致性) |
| 一致性协议 | ZAB 协议 | 无(去中心化,客户端缓存) | Raft(CP 模式)/ 无(AP 模式) | Raft 协议 |
| 核心功能 | 分布式协调(注册中心是附加场景) | 仅服务注册发现 | 服务注册发现 + 配置中心(一站式) | 服务注册发现 + 健康检查 + 多数据中心 |
| 健康检查 | 临时节点(心跳超时删除,被动检查) | 客户端主动上报(心跳)+ 自我保护机制 | 支持 TCP/HTTP/自定义脚本等多种检查 | 主动健康检查(支持 HTTP/TCP/gRPC) |
| 部署运维 | 需部署集群(至少 3 节点),运维成本高 | 去中心化,单节点 / 集群均可,运维简单 | 单节点 / 集群部署,支持自动扩缩容 | 跨平台,支持容器化部署,多数据中心易扩展 |
| 生态适配 | 适配 Dubbo 等 RPC 框架 | 适配 Spring Cloud Netflix | 适配 Spring Cloud Alibaba,兼容 Eureka/Zookeeper | 适配 Spring Cloud Consul,跨语言友好 |
| 可用性 | 集群选举期间不可用(秒级) | 网络分区时仍可用(客户端缓存列表) | AP 模式下网络分区不影响服务发现 | 选举期间短时间不可用,但恢复快 |
补充细节(面试延伸)
- Zookeeper:基于临时节点实现服务注册,心跳超时(默认 40s)会删除节点,适合对一致性要求高的场景(如分布式锁),但作为注册中心时,若主节点挂了,选举期间整个注册中心不可用,不适合高可用要求的微服务场景;
- Eureka:核心设计是'去中心化',每个节点都是平等的,客户端会缓存服务列表,即使所有 Eureka 节点挂了,客户端仍能通过缓存调用服务;且有'自我保护机制'—— 网络波动时不会随意删除服务实例,避免误删,但缺点是数据可能存在短暂不一致;
- Nacos:阿里开源的'一站式'解决方案,既解决注册中心问题,又解决配置中心问题,AP 模式下兼容 Eureka 的高可用特性,CP 模式下满足金融级一致性要求,是目前国内微服务项目的首选;
- Consul:优势在多数据中心部署(跨地域服务发现),健康检查能力强,但国内生态不如 Nacos 成熟,主要在海外项目或对多数据中心有强需求的场景使用。
Seata 核心组件有那些?
Seata 核心组件及核心作用
Seata 的分布式事务能力依赖三大核心组件(TC/TM/RM),外加一个全局唯一标识 XID(虽非'组件'但为核心纽带),具体如下:
- TC(Transaction Coordinator)- 事务协调器
- 角色定位:Seata 的核心中心化节点,独立部署(可集群),是分布式事务的'大脑'。
- 核心作用:
- 维护全局事务和分支事务的状态(如'待提交''已回滚');
- 接收 TM 的全局事务开启 / 提交 / 回滚请求,生成全局事务 ID(XID);
- 协调所有 RM 执行分支事务的提交或回滚,最终决定全局事务的最终状态。
- TM(Transaction Manager)- 事务管理器
- 角色定位:由业务系统的'事务发起方'(如订单服务)充当,无需独立部署,嵌入在业务应用中。
- 核心作用:
- 定义全局事务的范围(即'哪些操作属于同一个分布式事务');
- 向 TC 发起'开启全局事务''提交全局事务''回滚全局事务'的请求;
- 是全局事务的'发起者和终结者'。
- RM(Resource Manager)- 资源管理器
- 角色定位:由所有参与分布式事务的'资源方'(如订单库、库存库、账户服务)充当,嵌入在业务应用中,对接具体的数据源 / 服务资源。
- 核心作用
- 管理本地分支事务的资源(如数据库连接、缓存连接);
- 向 TC 注册分支事务,将分支事务与全局事务(XID)关联;
- 执行本地分支事务,并向 TC 汇报分支事务的执行状态(成功 / 失败);
- 接收 TC 的指令,完成分支事务的最终提交或回滚。
- XID(全局事务 ID)- 核心纽带
- 角色定位:全局唯一的字符串标识,由 TC 生成。
- 核心作用:贯穿整个分布式事务链路,将 TM 发起的全局事务、各 RM 的分支事务'绑定'在一起,是分布式事务追踪和协调的核心标识(XID 会随微服务调用链路传递,如通过 Feign/RPC 的上下文传递)。
组件间核心交互流程(辅助理解)
以'下单扣库存扣账户'的分布式事务为例,组件交互逻辑如下:
- TM(订单服务)向 TC 申请开启全局事务,TC 生成 XID 并返回;
- XID 随调用链路(订单→库存、订单→账户)传递到各 RM;
- 各 RM(库存服务、账户服务)执行本地分支事务,向 TC 注册分支事务并汇报执行状态;
- 业务完成后,TM 向 TC 发起'全局提交'请求;
- TC 检查所有分支事务状态:若均成功,则向所有 RM 下发'提交'指令;若任一失败,则下发'回滚'指令;
- 各 RM 执行 TC 的指令,完成分支事务的最终提交 / 回滚,TC 更新全局事务状态。
工作流程:
TM → TC(开启全局事务) → RM → TC(注册分支事务) → TC → RM(提交/回滚)
- TC 是服务端'协调者',负责全局事务的决策和状态管控;
- TM 是客户端'发起者',定义全局事务的生命周期;
- RM 是客户端'执行者',管理本地分支事务并响应 TC 指令。
Seata 四大模式
Seata 四大模式详解
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里开源的分布式事务解决方案,四大模式是其核心,适配不同业务场景的一致性、性能、开发成本需求,核心差异在于一致性级别、侵入性、性能。
1. AT 模式(Automatic Transaction)—— 无侵入默认模式
- 核心原理:基于「本地事务 + 补偿」的思想,完全无业务侵入,分为两个阶段:
- 一阶段:执行业务 SQL,记录 undo_log(回滚日志),提交本地事务,释放数据库锁;
- 二阶段:若全局提交,删除 undo_log;若全局回滚,根据 undo_log 生成反向补偿 SQL 执行,恢复数据。
- 适用场景:绝大多数常规业务场景(如电商下单、支付、库存扣减),要求数据库支持行锁、事务(MySQL/Oracle 等)。
- 核心特点:开发成本极低(无侵入)、性能较好(锁持有时间短)、最终一致性,是 Seata默认推荐的模式。
2. TCC 模式(Try-Confirm-Cancel)—— 高性能侵入式模式
- 核心原理:强业务侵入,需手动实现三个核心方法,由 Seata 协调执行:
- Try:预留业务资源(如冻结库存、锁定资金),保证业务可执行;
- Confirm:确认执行业务(真正扣减库存 / 资金),仅 Try 成功后执行;
- Cancel:释放 Try 阶段预留的资源(解冻库存 / 资金),Try 失败或全局回滚时执行。
- 适用场景:对性能要求极高、业务逻辑复杂的场景(如金融交易、高并发秒杀),支持异构系统(跨数据库 / 服务 / 语言)。
- 核心特点:性能最优(无数据库锁等待)、灵活性强,但开发成本高,需手动处理幂等、空回滚、悬挂三大问题。
3. SAGA 模式 —— 长事务解决方案
- 核心原理:基于「补偿」思想,将长事务拆分为多个独立的短事务(正向操作),出错时执行反向补偿操作,支持通过状态机(StateMachine)编排业务流程。
- 适用场景:长事务、业务流程复杂且需灵活编排的场景(如物流履约、供应链流程、订单分期履约),适配最终一致性需求。
- 核心特点:适配超长事务(可跨小时 / 天)、开发灵活(支持状态机编排)、无数据库强依赖,一致性为最终一致,性能较好。
4. XA 模式 —— 强一致性原生模式
- 核心原理:基于数据库原生的 XA 协议,完全遵循两阶段提交(2PC):
- 一阶段:所有参与者执行 SQL 并执行 XA prepare(准备),持有数据库锁;
- 二阶段:协调者统一发起 XA commit(提交)或 XA rollback(回滚),所有参与者一致执行。
- 适用场景:对一致性要求极高的核心场景(如金融核心交易、资金清算),依赖数据库的 XA 协议支持。
- 核心特点:强一致性、无业务侵入,但性能极差(资源锁定时间长),兼容性依赖数据库(如 MySQL InnoDB 支持,MyISAM 不支持)。
总结
- AT 模式:无侵入、开发成本低,是 Seata 默认推荐的常规场景方案,最终一致性;
- TCC 模式:高性能但需手动实现三阶段,适配高并发 / 复杂业务,强一致性;
- SAGA 模式:适配长事务 / 流程化业务,最终一致性,开发灵活;
- XA 模式:强一致性但性能差,仅用于金融等极致一致性场景。
核心选型逻辑:优先 AT 模式,高性能场景选 TCC,长事务选 SAGA,极致一致性选 XA。
Seata 中 TC,TM,RM 分别代表什么?
- TC(Transaction Coordinator)- 事务协调器
- 定义:Seata 的核心服务端组件,独立部署的中间件节点。
- 核心职责:
- 维护全局事务和分支事务的状态(全局事务 ID、分支事务 ID 的关联);
- 协调全局事务的最终提交或回滚('决策层');
- 管理全局锁、记录事务日志,保障事务一致性。
- TM(Transaction Manager)- 事务管理器
- 定义:发起全局事务的客户端角色,通常是业务系统中'入口服务'(比如订单服务)。
- 核心职责:
- 定义全局事务的范围(触发全局事务的开始、提交、回滚);
- 向 TC 申请开启全局事务,获取全局事务 ID(XID);
- 业务完成后,向 TC 发起全局提交 / 回滚的指令,驱动整个分布式事务的收尾。
- RM(Resource Manager)- 资源管理器
- 定义:管理分支事务的客户端角色,通常是业务系统中操作具体资源(数据库、消息队列等)的微服务(比如库存服务、支付服务)。
- 核心职责:
- 管理本地分支事务的资源,执行具体的业务 SQL / 操作;
- 向 TC 注册分支事务,并关联全局事务 ID(XID);
- 汇报分支事务的执行状态(成功 / 失败);
- 接收 TC 的指令,执行分支事务的提交或回滚。
- TM 向 TC 申请开启全局事务,TC 生成并返回 XID;
- 微服务调用链路中,XID 通过 RPC 传递到各个 RM;
- 每个 RM 执行本地业务时,向 TC 注册分支事务,执行后汇报状态;
- TM 根据业务结果,向 TC 发起全局提交 / 回滚请求;
- TC 协调所有 RM 执行分支事务的提交 / 回滚,最终完成全局事务。
- TC 是服务端'协调者',负责全局事务的决策和状态管控;
- TM 是客户端'发起者',定义全局事务的生命周期;
- RM 是客户端'执行者',管理本地分支事务并响应 TC 指令。
介绍一下你最近做过的项目
自己做过的项目中有接触过硬件的调用吗?怎么调用?协议是什么,参数是什么
通过 HTTP 的 REST 接口操作 Orthanc,实现 PACS 环境存储和转发 DICOM 文件
内部通过 DICOM 协议,参数主要是文件头(File Meta Information),位于文件开头,存储文件的元信息确保文件被正确解析数据集(Data Set),由一系列数据元素组成,每个元素对应一个具体的属性。
如上图所示,整个过程清晰分为两个阶段。下面我将为您详细说明每个阶段的关键步骤和 Java 调用的具体实现方式。
第一阶段:CT 机连接与影像接收
首先,需要让 CT 机将生成的 DICOM 影像自动发送到 Orthanc 服务器。
- 配置 Orthanc 的 DICOM 连接参数:确保 Orthanc 的配置文件(orthanc.json)中,DICOM 服务器的设置正确,特别是 DicomAet(Orthanc 的 AE Title,如 ORTHANC)和 DicomPort(监听端口,默认为 4242)。
- 在 CT 机上配置 DICOM 发送目标:在 CT 机的系统设置中,需要添加一个 DICOM 发送目标(C-STORE SCU)。关键是要填写正确的 AE Title(与 Orthanc 的 DicomAet 一致)、主机地址(Orthanc 服务器的 IP)和端口号(Orthanc 的 DicomPort)。
配置成功后,CT 机执行扫描后,影像就会自动推送到 Orthanc。您可以在 Orthanc 的 Web 界面(默认地址为 http://Orthanc 服务器 IP:8042)的'Studies'下看到收到的检查。
第二阶段:使用 Java 调用 Orthanc
当影像存储在 Orthanc 后,您的 Java 应用程序可以通过其强大的 REST API 进行交互。以下是几种核心操作的 Java 实现思路。
导入 http 请求包
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
1. 上传 DICOM 文件 (STOW-RS)
C-STORE
如果已有本地的 DICOM 文件需要上传到 Orthanc,可以使用 Java 发送 POST 请求。关键在于设置正确的 Content-Type 为 application/dicom。
2. 查询与检索元数据 (QIDO-RS)
C-FIND ,CMOVE
查询患者、检查(Study)、序列(Series)的列表信息,可以使用 QIDO-RS 接口。它返回易于处理的 JSON 格式数据。
3. 获取 DICOM 图像或文件 (WADO-RS)
根据查询到的实例 ID,通过 WADO-RS 接口获取具体的图像(可转换为 JPEG/PNG)或原始的 DICOM 文件。
项目几个人开发的?
项目开发多久?
公司都有那些部分,你所在的部门,以及部门人数
市场销售部,软件开发部 (本人),技术支持/服务部,人力资源部,财务部,我所在的部门是开发部,部门 15 个人左右。
线程在项目中的应用
预约模块,获取患者信息,医生信息,预约的项目。查询速度由原先的:3 秒+→1.2 秒,耗时降 60%;
@Configuration
public class AppointmentThreadPoolConfig {
@Bean(name = "asyncTaskExecutor")
public ThreadPoolTaskExecutor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
int corePoolSize = Runtime.getRuntime().availableProcessors()+1;
int maxPoolSize = Runtime.getRuntime().availableProcessors()*2;
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
CallerRunsPolicy 拒绝策略:是 JDK 内置的 4 种拒绝策略之一,特点是'不丢任务、不抛异常',适合预约这类对数据完整性要求高的场景(缺点是可能阻塞提交任务的线程);
private IPage<Appointment> enhanceAppointmentData(IPage<Appointment> result) {
Set<Long> patientIds = new HashSet<>();
Set<Long> doctorIds = new HashSet<>();
Set<Long> itemIds = new HashSet<>();
result.getRecords().forEach(record->{
if(record.getPatientId()!=null) patientIds.add(record.getPatientId());
if(record.getDoctorId()!=null) doctorIds.add(record.getDoctorId());
if(record.getItemId()!=null) itemIds.add(record.getItemId());
});
try {
CompletableFuture<Map<Long,PatientDTO>> patientFuture = CompletableFuture.supplyAsync(
()->getPatientMapBatch(patientIds), asyncTaskExecutor);
CompletableFuture<Map<Long,AdminUserRespDTO>> doctorFuture = CompletableFuture.supplyAsync(
()->getDoctorMapBatch(doctorIds), asyncTaskExecutor);
CompletableFuture<Map<Long,ItemDto>> itemFuture = CompletableFuture.supplyAsync(
()->getItemMapBatch(itemIds), asyncTaskExecutor);
CompletableFuture.allOf(patientFuture, doctorFuture, itemFuture).join();
Map<Long,PatientDTO> patientMap = patientFuture.get();
Map<Long,AdminUserRespDTO> doctorMap = doctorFuture.get();
Map<Long,ItemDto> itemMap = itemFuture.get();
result.getRecords().parallelStream().forEach(record->{
record.setPatientDTO(patientMap.get(record.getPatientId()));
record.setAdminUserRespDTO(doctorMap.get(record.getDoctorId()));
record.setItemDTO(itemMap.get(record.getItemId()));
});
} catch(Exception e) {
log.error("关联数据查询失败", e);
}
return result;
}
队列在项目中的应用
项目中数据库有多少张表
你在项目中负责那些模块?
处置记录,缴费,患者模块,CT 机模块,小程序等等这些。
项目中遇到的困难,难点(技术)
项目中数据库数据量最大的表有多少数据?
预约表,大概有 2000 万条数据,用户日活 3 万左右,大概一天产生 1w 条预约数据,一个月差不多 33 万条,年均预约量 400 万条。
项目中数据库表拆分策略?
按照地区分的。
华东区预约表:t_appointment_core_01
华中区预约表:t_appointment_core_02
西南区预约表:t_appointment_core_03
预留表:t_appointment_core_04(华北)、t_appointment_core_05(华南)(提前创建,空表占位)
你们线上 QPS 多少?你怎么知道的?
线上系统的实际 QPS 取决于业务量与用户行为,核心结论:
若按日均预约 1.11 万条计算,峰值 QPS 可达 30 左右
日均预约量 1.11 万条计算(可能包含线上 + 线下),线上预约按 90% 计(1 万条),则:
预约核心 QPS:1 万条 ÷12 小时 ÷3600 秒≈0.23(日常),峰值≈0.7-1.2
整体系统 QPS:1 万条 ×5-8 次 / 条 ×3-5 峰值系数 ÷86400 秒≈17-46
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online