202527 | RabbitMQ-基础 | 队列 | Direct + Fanout + Topic 交换机 | 消息转换器

RabbitMQ

RabbitMQ 架构与核心概念详解


一、整体架构图

RabbitMQ Server 连接 存储 元数据 路由 路由 路由 发布消息 消费 消费 交换机 交换机 队列 队列 客户端应用 RabbitMQ Server 消息存储 元数据库 消费者 消费者


二、核心组件
组件 作用 类比现实
Producer 消息生产者,将消息发送到交换机 快递发货方
Exchange 消息路由中心,根据规则将消息分发到队列 快递分拣中心
Queue 存储消息的缓冲区,先进先出 快递暂存仓库
Consumer 消息消费者,从队列获取消息处理 快递收件人
Channel 复用TCP连接的轻量级通信通道(减少TCP连接开销) 快递运输专线
VHost 虚拟主机,实现资源隔离(类似MySQL的database) 独立物流园区
Broker RabbitMQ服务实例 整个物流公司

三、交换机类型(核心路由逻辑)
类型 路由规则 代码声明示例
Direct 精确匹配routingKey channel.exchangeDeclare("logs", "direct")
Fanout 广播到所有绑定队列(忽略routingKey channel.exchangeDeclare("alerts", "fanout")
Topic 通配符匹配(*匹配1个词,#匹配0+个词) channel.exchangeDeclare("orders", "topic")
Headers 根据消息头键值对匹配(性能差,少用) channel.exchangeDeclare("meta", "headers")

路由示例

java 复制代码
// Topic交换机示例:路由订单相关消息
channel.basicPublish("orders", "order.create.us", null, message.getBytes());
// 匹配规则:order.*.us -> 匹配成功
// 匹配规则:order.# -> 匹配成功

四、消息流转全流程

Producer Exchange Queue Consumer Disk 发布消息(routingKey=order.pay) 根据绑定规则路由 消息持久化到磁盘 订阅消息(basicConsume) 推送消息 发送ACK确认 删除已确认消息 Producer Exchange Queue Consumer Disk


五、核心特性实现原理
  1. 消息持久化

    • 交换机/队列声明时设置durable=true
    • 消息设置deliveryMode=2
    java 复制代码
    channel.queueDeclare("orders", true, false, false, null); // 持久化队列
    channel.basicPublish("", "orders", 
        MessageProperties.PERSISTENT_TEXT_PLAIN, // 持久化消息
        message.getBytes());
  2. ACK机制

    • 自动ACK(易丢失消息)

      java 复制代码
      channel.basicConsume(queue, true, consumer);
    • 手动ACK(推荐)

      java 复制代码
      channel.basicConsume(queue, false, (tag, delivery) -> {
          // 处理逻辑
          channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
      }, consumerTag -> {});
  3. QoS控制

    java 复制代码
    // 每次最多推送1条未ACK消息给消费者
    channel.basicQos(1);

六、高级特性架构
  1. 死信队列(DLX)

    消息超时/拒收 主队列 DLX交换机 死信队列 异常处理服务

    java 复制代码
    Map<String, Object> args = new HashMap<>();
    args.put("x-dead-letter-exchange", "dlx.exchange");
    channel.queueDeclare("orders", true, false, false, args);
  2. 延迟队列(插件实现)

    java 复制代码
    headers.put("x-delay", 60000); // 延迟60秒
    AMQP.BasicProperties props = new Builder().headers(headers).build();
    channel.basicPublish("delayed.exchange", "", props, message.getBytes());
  3. 集群架构

    镜像 镜像 镜像 Node1 Queue Node2 Node3

    bash 复制代码
    # 设置镜像策略
    rabbitmqctl set_policy ha-all "^ha\." '{"ha-mode":"all"}'

RabbitMQ 数据隔离详解

一、数据隔离的核心需求

45% 30% 15% 10% 数据隔离的应用场景 多租户环境 业务模块隔离 开发/测试/生产环境隔离 安全合规要求


二、RabbitMQ 数据隔离方案
1. VHost(虚拟主机)隔离

原理

每个VHost是独立的消息域,包含专属的交换机、队列和权限体系,类似MySQL的Database概念。

操作示例

bash 复制代码
# 创建VHost
rabbitmqctl add_vhost /order_service

# 设置权限(用户order_user可访问)
rabbitmqctl set_permissions -p /order_service order_user ".*" ".*" ".*"

# 删除VHost(慎用!会删除所有关联资源)
rabbitmqctl delete_vhost /order_service

Java连接配置

java 复制代码
ConnectionFactory factory = new ConnectionFactory();
factory.setVirtualHost("/order_service");  // 关键隔离设置

特点

  • 不同VHost间完全隔离
  • 适用于多租户系统
  • 资源消耗极低(无需额外进程)

2. 用户权限控制

权限矩阵

权限类型 命令示例 作用范围
configure ^order\. 创建/删除队列/交换机
write ^order\. 发布消息到队列
read ^order\. 从队列消费消息

用户分级示例

bash 复制代码
# 管理员(全权限)
rabbitmqctl set_permissions -p /order_service admin ".*" ".*" ".*"

# 生产者用户(只能写)
rabbitmqctl set_permissions -p /order_service producer "^order\..*" "" ""

# 消费者用户(只能读)
rabbitmqctl set_permissions -p /order_service consumer "" "^order\..*" ""

# 监控用户(只读)
rabbitmqctl set_permissions -p /order_service monitor "" "" ".*"

3. 队列命名规范隔离

命名规则示例

复制代码
[业务域].[子系统].[功能]

实际案例

复制代码
payment.notify.sms       # 支付短信通知队列
inventory.stock.update    # 库存更新队列
log.audit.tracking        # 审计日志队列

Java声明示例

java 复制代码
// 支付业务队列
Map<String, Object> paymentArgs = new HashMap<>();
channel.queueDeclare(
    "payment.transaction",  // 队列名
    true,                   // 持久化
    false,                  // 非排他
    false,                  // 非自动删除
    paymentArgs             // 扩展参数
);

4. 物理隔离(终极方案)

部署架构
金融专区集群 测试集群 生产集群 RabbitMQ Node2 RabbitMQ Node1 RabbitMQ Node2 RabbitMQ Node1 RabbitMQ Node2 RabbitMQ Node1

适用场景

  • 金融级数据隔离要求
  • 不同安全等级的业务
  • 合规性要求(如等保三级)

三、Spring Boot集成最佳实践
1. 多VHost配置
yaml 复制代码
# application-order.yml
spring:
  rabbitmq:
    virtual-host: /order_service

# application-payment.yml 
spring:
  rabbitmq:
    virtual-host: /payment_service
2. 动态路由选择
java 复制代码
@Bean
public RabbitTemplate orderRabbitTemplate(
    @Qualifier("orderConnectionFactory") ConnectionFactory cf) {
    return new RabbitTemplate(cf);
}

@Bean 
public ConnectionFactory orderConnectionFactory() {
    CachingConnectionFactory cf = new CachingConnectionFactory();
    cf.setVirtualHost("/order_service");
    return cf;
}
3. 监听器隔离
java 复制代码
@RabbitListener(
    queues = "payment.notify",
    containerFactory = "secureContainerFactory")
public void handlePayment(Message message) {
    // 安全处理逻辑
}

@Bean
public SimpleRabbitListenerContainerFactory secureContainerFactory() {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(secureConnectionFactory());
    factory.setConsumerTagStrategy(queue -> "secure_" + UUID.randomUUID());
    return factory;
}

四、监控与运维要点
1. 隔离资源监控
bash 复制代码
# 查看各VHost资源占用
rabbitmqctl list_vhosts name messages messages_ready messages_unacknowledged

# 输出示例
/order_service    0       0       0
/payment_service  125     80      45
2. 权限审计命令
bash 复制代码
# 查看用户权限
rabbitmqctl list_permissions -p /order_service

# 查看用户标签
rabbitmqctl list_users
3. 故障排查流程

正确 错误 消息无法路由 检查VHost配置 检查交换机/队列绑定 修正连接参数 验证routingKey匹配 检查消费者状态


五、不同方案的选型建议
方案 隔离强度 性能影响 适用场景
VHost隔离 ★★★★ 多业务模块/多环境隔离
用户权限控制 ★★☆ 内部系统角色隔离
队列命名规范 ★☆☆ 开发规范辅助
物理集群隔离 ★★★★★ 高成本 金融/政务等高安全要求

黄金法则

80%的场景使用 VHost+权限控制 即可满足需求,剩余20%的特殊场景考虑物理隔离。

Spring Boot 集成 RabbitMQ 极简指南

一、3步快速集成
1. 添加依赖
xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2. 配置连接
yaml 复制代码
# application.yml
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
3. 发送/接收消息
java 复制代码
@RestController
public class DemoController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 发送消息
    @GetMapping("/send")
    public String send(@RequestParam String msg) {
        rabbitTemplate.convertAndSend("demo.queue", msg);
        return "Sent: " + msg;
    }

    // 接收消息
    @RabbitListener(queues = "demo.queue")
    public void receive(String msg) {
        System.out.println("Received: " + msg);
    }
}
二、核心组件说明
组件 作用 示例代码
RabbitTemplate 消息发送工具 convertAndSend("队列名",消息)
@RabbitListener 消息监听注解 @RabbitListener(queues="队列名")
AmqpAdmin 队列/交换机管理 declareQueue(new Queue("q1"))
三、高级配置示例**
1. 手动创建队列
java 复制代码
@Configuration
public class RabbitConfig {

    @Bean
    public Queue demoQueue() {
        return new Queue("demo.queue", true); // 持久化队列
    }
}
2. 发送复杂对象
java 复制代码
// 发送端
rabbitTemplate.convertAndSend("demo.queue", new Order(1001L));

// 接收端
@RabbitListener(queues = "demo.queue")
public void handleOrder(Order order) {
    // 处理订单对象
}

提示 :对象需实现Serializable接口

四、常用注解速查**
注解 作用
@RabbitListener 声明消息监听器
@RabbitHandler 多方法处理不同类型消息
@QueueBinding 绑定队列到交换机

RabbitMQ Work Queues 详解

一、核心概念

Work Queues(工作队列)是RabbitMQ最基础的消息模式,主要用于任务分发。其核心特点是:

  1. 一个生产者多个消费者共同消费同一个队列
  2. 消息采用轮询分发(Round-robin)策略
  3. 适合异步处理耗时任务(如图片处理、邮件发送等)

Task 1,2,3 Producer Task Queue Worker1 Worker2 Worker3

二、与简单队列的区别
特性 简单队列 工作队列
消费者数量 1个 多个
消息分发 全部发给单个消费者 轮询分发给所有消费者
典型应用场景 简单消息传递 并行任务处理
三、Java实现(Spring Boot版)
1. 生产者代码
java 复制代码
@RestController
public class TaskController {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/send-task")
    public String sendTask(@RequestParam String task) {
        // 发送到名为"task_queue"的队列
        rabbitTemplate.convertAndSend("task_queue", task);
        return "Task sent: " + task;
    }
}
2. 消费者代码
java 复制代码
@Component
public class TaskWorker {

    @RabbitListener(queues = "task_queue")
    public void processTask(String task) throws InterruptedException {
        System.out.println(" [x] Processing: " + task);
        // 模拟耗时操作
        Thread.sleep(1000); 
        System.out.println(" [x] Completed: " + task);
    }
}
3. 队列配置(可选)
java 复制代码
@Configuration
public class RabbitConfig {
    @Bean
    public Queue taskQueue() {
        return new Queue("task_queue", true); // 持久化队列
    }
}
四、关键特性
1. 消息确认(ACK机制)
java 复制代码
@RabbitListener(queues = "task_queue")
public void processTask(String task, Channel channel, 
                      @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
    try {
        // 业务处理...
        channel.basicAck(tag, false); // 手动确认
    } catch (Exception e) {
        channel.basicNack(tag, false, true); // 失败重试
    }
}
2. 公平分发(Prefetch)
yaml 复制代码
# application.yml
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 每次只给消费者1条未ACK的消息
3. 消息持久化
java 复制代码
// 发送持久化消息
rabbitTemplate.convertAndSend("task_queue", 
    MessageBuilder.withBody(task.getBytes())
        .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
        .build());
五、工作流程示例
  1. 发送3个任务:

    bash 复制代码
    curl http://localhost:8080/send-task?task=Task1
    curl http://localhost:8080/send-task?task=Task2
    curl http://localhost:8080/send-task?task=Task3
  2. 启动2个消费者实例

  3. 观察控制台输出:

    复制代码
    Worker1: Processing Task1
    Worker2: Processing Task2
    Worker1: Processing Task3
六、生产环境建议
  1. 始终启用消息持久化
    • 队列持久化
    • 消息持久化
  2. 合理设置prefetch
    • CPU密集型任务:prefetch=1~5
    • IO密集型任务:prefetch=10~50
  3. 实现死信队列处理失败消息
七、管理界面监控

访问 http://localhost:15672 查看:

  • Queues 标签页:
    • Ready:待处理消息数
    • Unacked:正在处理的消息数
    • Total:消息总数

RabbitMQ Direct交换机深度解析

一、核心本质

Direct交换机是精确路由器 ,根据消息的routing key完全匹配 队列绑定键,实现点对点精准投递。其核心特征:

  1. 严格匹配routing key必须等于binding key
  2. 单队列投递:每条消息只会路由到一个队列(除非重复绑定)
  3. 高性能:哈希表实现O(1)复杂度路由

routingKey=payments 精确匹配 匹配失败 Producer Direct Exchange payments orders 支付服务

二、三大核心特性
特性 说明 与Fanout/Topic对比
精确路由 全等匹配routing key Fanout不检查key,Topic支持通配符
单点投递 消息只会进入一个匹配队列(除非多队列同binding key) Fanout广播到所有队列
路由高效 哈希表直接查找 Topic需要模式匹配,性能略低
三、Spring Boot实战示例
1. 声明配置(Java Config)
java 复制代码
@Configuration
public class DirectConfig {
    
    // 声明直连交换机
    @Bean
    public DirectExchange orderExchange() {
        return new DirectExchange("order.direct"); 
    }

    // 声明业务队列
    @Bean
    public Queue paymentQueue() { 
        return new Queue("payment"); 
    }
    
    @Bean 
    public Queue stockQueue() {
        return new Queue("stock"); 
    }

    // 绑定队列到交换机(指定binding key)
    @Bean
    public Binding bindPayment(DirectExchange orderExchange, Queue paymentQueue) {
        return BindingBuilder.bind(paymentQueue)
               .to(orderExchange)
               .with("pay"); // binding key
    }
    
    @Bean
    public Binding bindStock(DirectExchange orderExchange, Queue stockQueue) {
        return BindingBuilder.bind(stockQueue)
               .to(orderExchange)
               .with("stock.update");
    }
}
2. 生产者发送(指定routing key)
java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void processPayment(Long orderId) {
        // routing key必须与binding key完全匹配
        rabbitTemplate.convertAndSend("order.direct", "pay", 
            new PaymentEvent(orderId, "SUCCESS"));
    }

    public void updateStock(Long skuId, int num) {
        // 匹配stock.update队列
        rabbitTemplate.convertAndSend("order.direct", "stock.update",
            Map.of("skuId", skuId, "delta", num));
    }
}
3. 消费者监听
java 复制代码
@Component
public class OrderListener {

    // 支付结果处理
    @RabbitListener(queues = "payment")
    public void handlePayment(PaymentEvent event) {
        paymentService.confirm(event.getOrderId());
    }

    // 库存更新处理
    @RabbitListener(queues = "stock")
    public void handleStockUpdate(@Payload Map<String, Object> data) {
        stockService.adjust((Long)data.get("skuId"), (Integer)data.get("delta"));
    }
}
四、路由匹配规则详解
场景 routing key binding key 是否匹配
完全匹配 pay pay
大小写敏感 Pay pay
多队列同binding key alert alert(队列A) 同时投递A/B
alert(队列B)
不同binding key order.create order.pay
五、高级应用场景
1. 多消费者负载均衡

routingKey=log P Direct log Consumer1 Consumer2

java 复制代码
// 两个消费者实例监听同一队列
@RabbitListener(queues = "log")
public class LogConsumer1 { /* ... */ }

@RabbitListener(queues = "log")
public class LogConsumer2 { /* ... */ }
2. 优先级队列
java 复制代码
@Bean
public Queue priorityQueue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-max-priority", 10); // 设置优先级范围
    return new Queue("priority.queue", true, false, false, args);
}

// 发送优先级消息
rabbitTemplate.convertAndSend("exchange", "key", message, m -> {
    m.getMessageProperties().setPriority(5); // 设置优先级
    return m;
});
3. 死信路由
java 复制代码
@Bean
public Queue workQueue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-dead-letter-exchange", "dlx.direct");
    args.put("x-dead-letter-routing-key", "dead");
    return new Queue("work.queue", true, false, false, args);
}
六、性能优化方案
  1. 批量绑定声明
java 复制代码
@Bean
public Declarables bindingBatch() {
    return new Declarables(
        new DirectExchange("batch.direct"),
        new Queue("q1"),
        new Queue("q2"),
        BindingBuilder.bind("q1").to("batch.direct").with("key1"),
        BindingBuilder.bind("q2").to("batch.direct").with("key2")
    );
}
  1. 路由键哈希优化
java 复制代码
// 使用一致性哈希路由
@Bean
public Exchange hashedExchange() {
    Map<String, Object> args = new HashMap<>();
    args.put("hash-header", "routing-key"); // 指定哈希字段
    return new DirectExchange("hashed.direct", true, false, args);
}
  1. 监控路由命中
bash 复制代码
rabbitmqctl list_bindings source_name routing_key destination_name
七、常见问题解决方案

问题1:消息路由失败怎么办?

java 复制代码
// 启用mandatory模式捕获未路由消息
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnsCallback(returned -> {
    log.error("消息未路由: {}", returned.getMessage());
});

问题2:需要多重条件匹配?

java 复制代码
// 改用Topic交换机
rabbitTemplate.convertAndSend("order.topic", "pay.wechat", message);

问题3:如何实现延迟重试?

java 复制代码
// 组合使用DLX+TTL
args.put("x-dead-letter-exchange", "retry.direct");
args.put("x-message-ttl", 5000); // 5秒后重试

Direct交换机是RabbitMQ中最简单高效的路由方案,适用于需要精确控制消息去向的场景。当你的业务需要明确的"一对一"消息路由时,Direct交换机永远是最可靠的选择。

RabbitMQ Fanout交换机详解

一、核心特性

Fanout交换机是RabbitMQ中最简单的广播型交换机,其核心特点是:

  1. 无条件广播:将消息发送到所有绑定队列,完全忽略Routing Key
  2. 一对多分发:每个绑定队列都会收到消息的完整副本
  3. 零过滤逻辑:不执行任何路由匹配计算

发布消息 复制分发 复制分发 复制分发 生产者 Fanout交换机 邮件队列 短信队列 APP推送队列 邮件服务 短信服务 推送服务

二、与Direct/Topic交换机的区别
特性 Fanout Direct Topic
路由依据 精确匹配Routing Key 通配符匹配Routing Key
消息去向 所有绑定队列 单个匹配队列 多个匹配队列
性能 最高 中等
典型场景 系统通知、事件广播 订单支付、库存扣减 多条件消息路由
三、Spring Boot实战实现
1. 声明交换机和队列(配置类)
java 复制代码
@Configuration
public class FanoutConfig {
    
    // 定义广播交换机
    @Bean
    public FanoutExchange notificationExchange() {
        return new FanoutExchange("notify.fanout"); 
    }

    // 定义三个业务队列
    @Bean
    public Queue emailQueue() {
        return new Queue("notify.email"); 
    }
    
    @Bean 
    public Queue smsQueue() {
        return new Queue("notify.sms"); 
    }
    
    @Bean
    public Queue appQueue() {
        return new Queue("notify.app");
    }

    // 绑定队列到交换机(无需指定路由键)
    @Bean
    public Binding bindEmail(FanoutExchange notificationExchange, Queue emailQueue) {
        return BindingBuilder.bind(emailQueue).to(notificationExchange);
    }
    
    @Bean
    public Binding bindSms(FanoutExchange notificationExchange, Queue smsQueue) {
        return BindingBuilder.bind(smsQueue).to(notificationExchange);
    }
    
    @Bean
    public Binding bindApp(FanoutExchange notificationExchange, Queue appQueue) {
        return BindingBuilder.bind(appQueue).to(notificationExchange);
    }
}
2. 消息生产者(Controller示例)
java 复制代码
@RestController
public class NotificationController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostMapping("/broadcast")
    public String sendAlert(@RequestBody String message) {
        // 发送到fanout交换机(第二个参数routing key被忽略)
        rabbitTemplate.convertAndSend("notify.fanout", "", 
            MessageBuilder.withBody(message.getBytes())
                .setHeader("alert-level", "URGENT") // 添加消息头
                .build());
        
        return "广播消息已发送: " + message;
    }
}
3. 消息消费者(监听器示例)
java 复制代码
@Component
public class NotificationListener {
    
    // 邮件服务消费者
    @RabbitListener(queues = "notify.email")
    public void handleEmail(String message, 
                          @Header("alert-level") String level) {
        System.out.printf("[邮件][%s] %s%n", level, message);
    }

    // 短信服务消费者
    @RabbitListener(queues = "notify.sms")
    public void handleSms(Message message) throws IOException {
        String content = new String(message.getBody());
        System.out.println("[短信] " + content);
    }

    // APP推送消费者
    @RabbitListener(queues = "notify.app")
    public void handleAppPush(String message, Channel channel,
                            @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
        try {
            pushService.sendToAll(message);
            channel.basicAck(tag, false); // 手动确认
        } catch (Exception e) {
            channel.basicNack(tag, false, true); // 失败重试
        }
    }
}
四、高级应用场景
1. 微服务配置刷新
java 复制代码
// 配置中心发送刷新命令
public void refreshAllServices() {
    rabbitTemplate.convertAndSend("config.fanout", "", 
        new ConfigRefreshEvent("ALL", Instant.now()));
}

// 各微服务监听(每个服务有自己的队列)
@RabbitListener(queues = "#{configQueue.name}")
public void handleRefresh(ConfigRefreshEvent event) {
    if ("ALL".equals(event.getScope()) || 
        serviceName.equals(event.getScope())) {
        configManager.refresh();
    }
}
2. 分布式日志收集

日志事件 日志事件 日志事件 服务A logs.fanout 服务B 服务C ES存储队列 报警分析队列 归档备份队列

3. 实时数据同步
java 复制代码
// 数据库变更时广播事件
@TransactionalEventListener
public void handleDataChange(DataChangeEvent event) {
    rabbitTemplate.convertAndSend("data.sync.fanout", "", event);
}

// 多个子系统同步处理
@RabbitListener(queues = "cache.update.queue")
public void syncCache(DataChangeEvent event) {
    cache.evict(event.getKey());
}
五、生产环境注意事项
  1. 消息堆积预防

    yaml 复制代码
    # 限制队列最大长度
    spring:
      rabbitmq:
        listener:
          simple:
            prefetch: 50 # 每个消费者预取数量
    java 复制代码
    @Bean
    public Queue limitedQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-max-length", 10000); // 队列最大消息数
        return new Queue("limited.queue", true, false, false, args);
    }
  2. 监控关键指标

    bash 复制代码
    # 查看消息分发状态
    rabbitmqctl list_queues name messages_ready messages_unacknowledged
    
    # 查看交换机绑定
    rabbitmqctl list_bindings source_name destination_name
  3. 异常处理方案

    java 复制代码
    // 全局异常处理器
    @RabbitListener(queues = "notify.email", errorHandler = "emailErrorHandler")
    public void processEmail(String message) {
        emailService.send(message);
    }
    
    @Bean
    public RabbitListenerErrorHandler emailErrorHandler() {
        return (msg, ex) -> {
            log.error("邮件发送失败: {}", msg.getPayload(), ex);
            // 记录失败消息到DB
            failoverService.save(msg.getPayload()); 
            return null;
        };
    }
六、性能优化技巧
  1. 消息压缩

    java 复制代码
    rabbitTemplate.setBeforePublishPostProcessors(message -> {
        if (message.getBody().length > 1024) {
            message.getMessageProperties()
                   .setContentEncoding("gzip");
            return new GZipPostProcessor().process(message);
        }
        return message;
    });
  2. 批量绑定声明

    java 复制代码
    @Bean
    public Declarables fanoutBindings() {
        FanoutExchange exchange = new FanoutExchange("batch.fanout");
        return new Declarables(
            exchange,
            new Queue("batch.q1"),
            new Queue("batch.q2"),
            BindingBuilder.bind(new Queue("batch.q1")).to(exchange),
            BindingBuilder.bind(new Queue("batch.q2")).to(exchange)
        );
    }
  3. 多消费者并发

    yaml 复制代码
    spring:
      rabbitmq:
        listener:
          simple:
            concurrency: 5 # 每个监听器启动5个消费者线程

Fanout交换机是RabbitMQ中最简单直接的广播机制,适用于需要一对多消息分发不关心路由逻辑的场景。当您需要确保多个系统接收完全相同的信息副本时,Fanout交换机是最可靠的选择。

RabbitMQ Topic交换机

一、核心特性

Topic交换机是智能路由器 ,通过routing key通配符匹配实现灵活的消息分发。核心特点:

  1. 模式匹配 :支持*(匹配一个词)和#(匹配零或多个词)
  2. 多队列投递:一条消息可同时路由到多个匹配队列
  3. 动态路由:绑定关系可随时变更,不影响生产者

routingKey=order.payment.wechat 匹配*.payment.* 匹配order.# 匹配#.wechat Producer Topic Exchange 支付队列 订单队列 微信队列 支付服务 订单服务 微信服务

二、与Direct/Fanout交换机的区别
特性 Topic Direct Fanout
路由精度 通配符匹配 精确匹配 无匹配
消息去向 多个匹配队列 单个匹配队列 所有绑定队列
灵活性 高(动态路由) 中(固定路由) 低(仅广播)
典型场景 多条件事件分发 点对点任务 系统广播
三、通配符规则详解
通配符 含义 示例 匹配示例
* 匹配一个单词 order.*.pay order.wechat.pay
# 匹配零或多个单词 log.#.error log.app.error / log.error
. 单词分隔符 china.beijing 必须严格包含分隔符

特殊案例

  • usa.# 可匹配 usausa.nyc.weather
  • *.temp 不匹配 temp(必须有一个前置单词)
四、Spring Boot实战实现
1. 声明交换机和队列(配置类)
java 复制代码
@Configuration
public class TopicConfig {
    
    // 定义Topic交换机
    @Bean
    public TopicExchange eventExchange() {
        return new TopicExchange("events.topic"); 
    }

    // 定义业务队列
    @Bean
    public Queue paymentQueue() {
        return new Queue("payment.events"); 
    }
    
    @Bean 
    public Queue orderQueue() {
        return new Queue("order.events"); 
    }
    
    @Bean
    public Queue logQueue() {
        return new Queue("system.log");
    }

    // 绑定队列到交换机(指定通配符)
    @Bean
    public Binding bindPayment(TopicExchange eventExchange, Queue paymentQueue) {
        return BindingBuilder.bind(paymentQueue)
               .to(eventExchange)
               .with("*.payment.*"); // 匹配如: wechat.payment.success
    }
    
    @Bean
    public Binding bindOrder(TopicExchange eventExchange, Queue orderQueue) {
        return BindingBuilder.bind(orderQueue)
               .to(eventExchange)
               .with("order.#"); // 匹配如: order.create/order.cancel
    }
    
    @Bean
    public Binding bindLog(TopicExchange eventExchange, Queue logQueue) {
        return BindingBuilder.bind(logQueue)
               .to(eventExchange)
               .with("#.error"); // 匹配如: db.error/app.error
    }
}
2. 消息生产者(Service示例)
java 复制代码
@Service
public class EventPublisher {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 支付成功事件
    public void publishPaymentSuccess(String channel) {
        String routingKey = channel + ".payment.success";
        rabbitTemplate.convertAndSend("events.topic", routingKey, 
            new PaymentEvent(channel, "SUCCESS"));
    }

    // 订单状态变更
    public void publishOrderEvent(String action) {
        rabbitTemplate.convertAndSend("events.topic", 
            "order." + action,  // 如: order.create
            new OrderEvent(action));
    }

    // 系统错误日志
    public void publishErrorLog(String module) {
        rabbitTemplate.convertAndSend("events.topic", 
            module + ".error",  // 如: db.error
            new ErrorLog(module));
    }
}
3. 消息消费者(监听器示例)
java 复制代码
@Component
public class EventListener {
    
    // 处理所有支付事件
    @RabbitListener(queues = "payment.events")
    public void handlePayment(PaymentEvent event) {
        paymentService.process(event);
    }

    // 处理所有订单事件
    @RabbitListener(queues = "order.events")
    public void handleOrderEvent(OrderEvent event, 
                               @Header(AmqpHeaders.RECEIVED_ROUTING_KEY) String key) {
        log.info("收到订单事件[{}]: {}", key, event);
        orderService.handle(event.getAction());
    }

    // 处理所有错误日志
    @RabbitListener(queues = "system.log")
    public void handleErrorLog(ErrorLog log) {
        alertService.notifyAdmin(log);
        logService.archive(log);
    }
}
五、高级应用场景
1. 多维度消息路由

routingKey=order.eu.payment 匹配 order.*.payment 匹配 order.eu.# 匹配 *.payment P Topic Exchange 支付队列 欧洲订单队列 全局支付队列

2. 动态绑定调整
java 复制代码
// 运行时添加新路由
public void addNewRoute(String serviceName) {
    Queue queue = new Queue(serviceName + ".queue");
    Binding binding = BindingBuilder.bind(queue)
                  .to(topicExchange)
                  .with(serviceName + ".#");
    rabbitAdmin.declareBinding(binding);
}
3. 多级日志处理
java 复制代码
// 发送不同级别日志
public void log(String module, String level, String msg) {
    String routingKey = module + "." + level; // 如: auth.warning
    rabbitTemplate.convertAndSend("logs.topic", routingKey, msg);
}

// 监听不同级别
@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "critical.log"),
    exchange = @Exchange(name = "logs.topic"),
    key = "#.critical" // 监听所有critical日志
))
public void handleCriticalLog(String log) {
    alertService.notifyOnCall(log);
}
六、性能优化方案
  1. 路由键设计原则

    java 复制代码
    // 推荐分层结构
    "区域.业务.动作"  // 如: eu.order.payment
    "模块.级别"      // 如: auth.error
  2. 绑定缓存优化

    yaml 复制代码
    spring:
      rabbitmq:
        cache:
          connection:
            mode: CONNECTION # 复用连接提高绑定查询效率
  3. 监控路由性能

    bash 复制代码
    # 查看交换机消息统计
    rabbitmqctl list_exchanges name message_stats.publish_in message_stats.publish_out
七、常见问题解决方案

问题1:通配符匹配失效?

java 复制代码
// 检查单词分隔符
String validKey = "china.beijing"; // 正确
String invalidKey = "china_beijing"; // 无法匹配china.*

问题2:需要更复杂的路由逻辑?

java 复制代码
// 组合使用Header交换机
Map<String, Object> headers = new HashMap<>();
headers.put("region", "asia");
headers.put("priority", "high");
rabbitTemplate.convertAndSend("headers.ex", "", msg, 
    m -> { m.getMessageProperties().setHeaders(headers); return m; });

问题3:如何实现灰度发布?

java 复制代码
// 通过路由键版本控制
String routingKey = "v2.order.create"; 
if (isGrayUser(userId)) {
    rabbitTemplate.convertAndSend("orders.topic", routingKey, order);
}

Topic交换机是RabbitMQ中最灵活的智能路由器 ,特别适合需要多维度消息分发的场景。当您的业务需要根据多种条件动态路由消息时,Topic交换机的通配符匹配能力将成为最佳选择。

RabbitMQ 队列与交换机声明


一、按声明时机分类
类型 执行阶段 特点 适用场景
静态声明 应用启动时 配置集中,易于管理 固定拓扑结构
动态声明 运行时 灵活调整拓扑 多租户/动态路由需求
混合声明 启动时+运行时 兼顾稳定性和灵活性 核心业务+扩展功能

二、按技术实现分类
1. Spring AMQP注解方式
java 复制代码
// 声明队列+交换机+绑定(三合一)
@RabbitListener(bindings = @QueueBinding(
    value = @Queue(
        name = "alert.queue",
        durable = "true",
        arguments = @Argument(name = "x-max-length", value = "1000")
    ),
    exchange = @Exchange(
        name = "alert.exchange",
        type = ExchangeTypes.TOPIC
    ),
    key = "alert.#"
))
public void handleAlert(AlertMessage message) {
    // 消息处理逻辑
}

优点 :声明与消费代码一体,简洁
缺点 :无法复用声明配置
适用:简单业务场景

2. Java Config配置类
java 复制代码
@Configuration
public class RabbitDeclareConfig {

    // 声明持久化Topic交换机
    @Bean
    public TopicExchange orderExchange() {
        return new TopicExchange("order.topic", true, false);
    }

    // 声明带死信队列的订单队列
    @Bean
    public Queue orderQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange", "dlx.order");
        args.put("x-max-priority", 10);
        return new Queue("order.queue", true, false, false, args);
    }

    // 批量声明(优化性能)
    @Bean
    public Declarables businessBindings() {
        Queue q1 = new Queue("inventory.queue");
        Queue q2 = new Queue("payment.queue");
        
        TopicExchange exchange = new TopicExchange("business.exchange");
        
        return new Declarables(
            exchange, q1, q2,
            BindingBuilder.bind(q1).to(exchange).with("stock.*"),
            BindingBuilder.bind(q2).to(exchange).with("pay.#")
        );
    }
}

优点 :配置集中管理,支持复杂参数
缺点 :代码量较大
适用:生产环境推荐方案

3. RabbitMQ管理API
java 复制代码
@Autowired
private AmqpAdmin amqpAdmin;

// 动态创建延迟队列
public void createDelayQueue(String queueName, long ttl) {
    Map<String, Object> args = new HashMap<>();
    args.put("x-message-ttl", ttl);
    args.put("x-dead-letter-exchange", "processed.exchange");
    
    amqpAdmin.declareQueue(new Queue(queueName, true, false, false, args));
    
    // 动态绑定
    Binding binding = new Binding(
        queueName,
        Binding.DestinationType.QUEUE,
        "delay.exchange",
        "delay.key",
        null
    );
    amqpAdmin.declareBinding(binding);
}

优点 :运行时灵活控制
缺点 :需处理异常情况
适用:灰度发布、多租户系统

4. 管理界面手动操作

步骤

  1. 访问 http://localhost:15672
  2. 进入 QueuesExchanges 标签页
  3. 点击 Add a new queue/exchange
  4. 填写参数并保存

优点 :可视化即时生效
缺点 :难以版本控制
适用:开发测试环境调试


三、按消息模式分类
1. 简单队列模式
java 复制代码
// 无需交换机(使用默认交换机)
@Bean
public Queue simpleQueue() {
    return new Queue("simple.queue");
}
2. 工作队列模式
java 复制代码
@Bean
public Queue workQueue() {
    return new Queue("work.queue", true); // 持久化
}

// 多个消费者监听同一队列实现负载均衡
@RabbitListener(queues = "work.queue", concurrency = "3")
public void worker(Message message) {
    // 处理任务
}
3. 发布/订阅模式
java 复制代码
@Bean
public FanoutExchange pubSubExchange() {
    return new FanoutExchange("broadcast.exchange");
}

@Bean
public Queue subQueue1() {
    return new Queue("sub.queue1");
}

@Bean
public Binding binding1(FanoutExchange pubSubExchange, Queue subQueue1) {
    return BindingBuilder.bind(subQueue1).to(pubSubExchange);
}
4. 路由模式
java 复制代码
@Bean
public DirectExchange routingExchange() {
    return new DirectExchange("routing.exchange");
}

@Bean
public Binding errorBinding(DirectExchange routingExchange, Queue errorQueue) {
    return BindingBuilder.bind(errorQueue)
           .to(routingExchange)
           .with("error"); // 精确匹配routing key
}
5. 主题模式
java 复制代码
@Bean
public TopicExchange topicExchange() {
    return new TopicExchange("topic.exchange");
}

@Bean
public Binding auditBinding(TopicExchange topicExchange, Queue auditQueue) {
    return BindingBuilder.bind(auditQueue)
           .to(topicExchange)
           .with("*.audit.#"); // 通配符匹配
}

四、按业务场景分类
1. 订单系统
java 复制代码
@Bean
public TopicExchange orderExchange() {
    return new TopicExchange("order.topic", true, false);
}

@Bean
public Queue orderCreateQueue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-max-priority", 10); // 支持优先级
    return new Queue("order.create", true, false, false, args);
}

@Bean
public Binding orderCreateBinding() {
    return BindingBuilder.bind(orderCreateQueue())
           .to(orderExchange())
           .with("order.create");
}
2. 日志收集
java 复制代码
@Bean
public FanoutExchange logExchange() {
    return new FanoutExchange("log.fanout");
}

@Bean
public Queue esLogQueue() {
    return new Queue("log.es", true);
}

@Bean
public Binding esLogBinding() {
    return BindingBuilder.bind(esLogQueue()).to(logExchange());
}
3. 延迟任务
java 复制代码
@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);
}

@Bean
public Queue delayQueue() {
    return new Queue("delay.queue", true);
}

@Bean
public Binding delayBinding() {
    return BindingBuilder.bind(delayQueue())
           .to(delayExchange())
           .with("delay.key")
           .noargs();
}

五、高级特性配置
1. 死信队列
java 复制代码
@Bean
public Queue businessQueue() {
    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", 60000); // 1分钟TTL
    return new Queue("business.queue", true, false, false, args);
}
2. 惰性队列
java 复制代码
@Bean
public Queue lazyQueue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-queue-mode", "lazy"); // 节省内存
    return new Queue("lazy.queue", true, false, false, args);
}
3. 优先级队列
java 复制代码
@Bean
public Queue priorityQueue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-max-priority", 5); // 支持5个优先级
    return new Queue("priority.queue", true, false, false, args);
}

六、声明策略最佳实践
  1. 生产环境标配

    java 复制代码
    @Bean
    public Queue productionQueue() {
        return new Queue("prod.queue", 
            true,   // durable
            false,  // exclusive
            false,  // autoDelete
            Map.of( // arguments
                "x-max-length", 5000,
                "x-overflow", "reject-publish",
                "x-queue-mode", "lazy"
            ));
    }
  2. 开发环境简化

    java 复制代码
    @Profile("dev")
    @Bean
    public Queue devQueue() {
        return new Queue("dev.queue", false, true, true); // 临时队列
    }
  3. 灰度发布方案

    java 复制代码
    public void declareGrayQueue(String version) {
        Queue queue = new Queue("order." + version);
        Binding binding = new Binding(
            queue.getName(),
            Binding.DestinationType.QUEUE,
            "order.exchange",
            "order." + version,
            null
        );
        amqpAdmin.declareQueue(queue);
        amqpAdmin.declareBinding(binding);
    }

通过这种结构化分类,您可以根据具体业务需求快速选择最适合的声明方式。建议:

  1. 核心业务:采用Java Config静态声明
  2. 动态需求:结合AmqpAdmin API
  3. 特殊场景:使用注解简化开发
  4. 生产环境:务必配置持久化、长度限制等保护参数

RabbitMQ 消息转换器

一、消息转换器的核心作用

消息转换器(Message Converter)是Spring AMQP中用于Java对象与消息体相互转换的组件,主要处理:

  1. 序列化:将Java对象转为MQ可传输的字节流
  2. 反序列化:将MQ消息体还原为Java对象
  3. 内容协商:根据Content-Type处理不同数据格式

序列化 反序列化 Java对象 Message Body Java对象

二、Spring AMQP内置转换器对比
转换器类型 依赖库 特点 适用场景
SimpleMessageConverter 支持String/byte[]/Serializable 基础类型传输
Jackson2JsonMessageConverter Jackson JSON序列化,支持复杂对象 主流JSON交互场景
MarshallingMessageConverter JDK序列化 二进制格式,高效但跨语言差 Java系统内部通信
ContentTypeDelegatingMessageConverter 多种 根据Content-Type动态选择 多协议混合系统
三、配置与使用
1. 基础配置方式
java 复制代码
@Configuration
public class RabbitConfig {
    
    @Bean
    public MessageConverter jsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
    
    // 或使用更灵活的内容协商转换器
    @Bean 
    public MessageConverter contentTypeConverter() {
        ContentTypeDelegatingMessageConverter converter = 
            new ContentTypeDelegatingMessageConverter();
        converter.addDelegate("application/json", new Jackson2JsonMessageConverter());
        converter.addDelegate("text/plain", new SimpleMessageConverter());
        return converter;
    }
}
2. 发送复杂对象
java 复制代码
@RestController
public class OrderController {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostMapping("/order")
    public String createOrder(@RequestBody Order order) {
        // 自动使用配置的Jackson转换器
        rabbitTemplate.convertAndSend("order.exchange", "order.create", order);
        return "Order sent";
    }
}
3. 接收消息处理
java 复制代码
@Component
public class OrderListener {
    
    // 自动反序列化为Order对象
    @RabbitListener(queues = "order.queue")
    public void processOrder(Order order) {
        System.out.println("Received order: " + order.getId());
    }
    
    // 获取原始Message对象
    @RabbitListener(queues = "audit.queue")
    public void auditOrder(Message message) {
        byte[] body = message.getBody();
        MessageProperties props = message.getMessageProperties();
        System.out.println("ContentType: " + props.getContentType());
    }
}
四、高级定制方案
1. 自定义Jackson配置
java 复制代码
@Bean
public Jackson2JsonMessageConverter customJsonConverter() {
    ObjectMapper mapper = new ObjectMapper()
        .registerModule(new JavaTimeModule())
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    return new Jackson2JsonMessageConverter(mapper);
}
2. 类型转换增强
java 复制代码
public class SmartMessageConverter extends Jackson2JsonMessageConverter {
    
    @Override
    public Object fromMessage(Message message) throws MessageConversionException {
        MessageProperties props = message.getMessageProperties();
        if (props.getHeaders().containsKey("__TypeId__")) {
            props.setHeader(JsonTypeIdResolver.CLASS_NAME_FIELD, 
                props.getHeaders().get("__TypeId__"));
        }
        return super.fromMessage(message);
    }
}
3. 多版本兼容处理
java 复制代码
@Bean
public MessageConverter versionedJsonConverter() {
    Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
    converter.setClassMapper(new ClassMapper() {
        @Override
        public void fromClass(Class<?> clazz, MessageProperties properties) {
            properties.setHeader("X-Object-Type", clazz.getName());
            properties.setHeader("X-Data-Version", "2.1");
        }

        @Override
        public Class<?> toClass(MessageProperties properties) {
            String version = properties.getHeader("X-Data-Version");
            return "2.1".equals(version) ? OrderV2.class : Order.class;
        }
    });
    return converter;
}
五、生产环境最佳实践
  1. 强制内容类型声明

    java 复制代码
    rabbitTemplate.convertAndSend(exchange, routingKey, message, m -> {
        m.getMessageProperties().setContentType("application/json");
        return m;
    });
  2. 消息大小监控

    java 复制代码
    @Bean
    public MessageConverter monitoringConverter(MessageConverter delegate) {
        return new MessageConverter() {
            @Override
            public Message toMessage(Object object, MessageProperties messageProperties) {
                Message msg = delegate.toMessage(object, messageProperties);
                monitor.messageSize(msg.getBody().length);
                return msg;
            }
            //...其他方法实现
        };
    }
  3. 安全防护建议

    java 复制代码
    @Bean
    public Jackson2JsonMessageConverter safeJsonConverter() {
        ObjectMapper mapper = new ObjectMapper();
        // 防止JSON注入
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 
            ObjectMapper.DefaultTyping.NON_FINAL);
        return new Jackson2JsonMessageConverter(mapper);
    }
六、常见问题解决方案

问题1:反序列化ClassNotFound

java 复制代码
// 解决方案:统一类型映射
@Bean
public Jackson2JsonMessageConverter converter() {
    Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
    converter.setTypePrecedence(TypePrecedence.TYPE_ID);
    converter.setClassMapper(new DefaultClassMapper() {
        {
            setDefaultType(Order.class);
            setIdClassMapping(Map.of("order", Order.class));
        }
    });
    return converter;
}

问题2:日期格式不兼容

java 复制代码
@Bean
public Jackson2JsonMessageConverter dateFormatConverter() {
    ObjectMapper mapper = new ObjectMapper()
        .setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))
        .registerModule(new JavaTimeModule());
    return new Jackson2JsonMessageConverter(mapper);
}

问题3:大文件传输优化

java 复制代码
// 使用压缩转换器
public class CompressingMessageConverter implements MessageConverter {
    private final MessageConverter delegate = new SimpleMessageConverter();

    @Override
    public Message toMessage(Object object, MessageProperties messageProperties) {
        byte[] data = (byte[]) delegate.toMessage(object, messageProperties).getBody();
        return new Message(compress(data), messageProperties);
    }
    
    private byte[] compress(byte[] data) { /* GZIP压缩实现 */ }
}
七、性能对比测试数据
转换器类型 1KB对象序列化耗时 反序列化耗时 消息体积
JDK序列化 2ms 1ms 589B
Jackson(无类型信息) 3ms 2ms 328B
Jackson(含类型信息) 4ms 3ms 412B
Protobuf(需预先编译) 1ms 0.5ms 215B

测试环境:Spring Boot 2.7 + RabbitMQ 3.10,对象包含10个字段

通过合理选择消息转换器,您可以:

  • 实现跨语言系统集成(推荐JSON)
  • 提升Java内部通信效率(二进制协议)
  • 处理特殊数据格式需求(自定义转换器)
相关推荐
Hanson Huang2 小时前
【数据结构】堆排序详细图解
java·数据结构·排序算法·堆排序
路在脚下@3 小时前
Redis实现分布式定时任务
java·redis
xrkhy3 小时前
idea的快捷键使用以及相关设置
java·ide·intellij-idea
somdip3 小时前
若伊微服务版本教程(自参)
微服务·云原生·架构
巨龙之路3 小时前
Lua中的元表
java·开发语言·lua
小样vvv3 小时前
【微服务管理】深入理解 Gateway 网关:原理与实现
微服务·gateway
掘金-我是哪吒4 小时前
分布式微服务系统架构第102集:JVM调优支撑高并发、低延迟、高稳定性场景
jvm·分布式·微服务·架构·系统架构
花花鱼4 小时前
itext7 html2pdf 将html文本转为pdf
java·pdf
小丁爱养花4 小时前
驾驭 Linux 云: JavaWeb 项目安全部署
java·linux·运维·服务器·spring boot·后端·spring
我爱拉臭臭4 小时前
kotlin音乐app之自定义点击缩放组件Shrink Layout
android·java·kotlin