消息驱动Spring Cloud Stream与RabbitMQ整合

5 阅读11分钟

消息驱动 - 不用打电话,写信也能办事

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

一、先白话白话消息队列有啥用

场景1:用户注册发短信

以前的做法(同步调用)

public void register(User user) {
    // 1. 保存用户(50ms)
    userService.save(user);
    
    // 2. 发欢迎短信(2000ms)
    smsService.sendWelcomeSms(user.getPhone());
    
    // 3. 送优惠券(100ms)
    couponService.sendCoupon(user.getId());
    
    // 总耗时:2150ms,用户等2秒多!
}

问题:发短信慢,用户得等着!

现在的做法(异步消息)

public void register(User user) {
    // 1. 保存用户(50ms)
    userService.save(user);
    
    // 2. 发消息到队列(5ms)
    messageQueue.send("user.register", user);
    
    // 总耗时:55ms,用户秒响应!
    
    // 短信服务、优惠券服务自己从队列取消息处理
    // 用户不用等了!
}

场景2:双11下单

问题:瞬间千万人下单,数据库撑不住! 解决:订单先放消息队列,慢慢处理,这叫削峰填谷

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

场景3:订单状态变更

问题:好多服务关心订单状态:

  • 物流服务:发货要通知
  • 库存服务:扣库存要通知
  • 客服系统:退款要通知
  • 数据分析:统计要数据

解决:订单服务发个消息,谁关心谁自己听。

二、消息队列选哪个?

消息队列优点缺点适用场景
RabbitMQ成熟稳定,功能全性能一般,Erlang语言企业级应用,复杂路由
Kafka吞吐量高,持久化好功能简单,配置复杂日志收集,大数据
RocketMQ阿里出品,功能全文档一般,社区小电商,金融
ActiveMQ老牌,支持协议多性能差,不活跃老系统升级

咱们用RabbitMQ,因为

  1. Spring Cloud Stream支持好
  2. 管理界面漂亮
  3. 功能全(路由、死信、延迟)
  4. 社区活跃

三、RabbitMQ核心概念(得先明白)

1. 生产者(Producer)

  • 发消息的
  • 就像写信的人

2. 消费者(Consumer)

  • 收消息的
  • 就像收信的人

3. 消息(Message)

  • 信的内容
  • 有header(信封)和body(信纸)

4. 交换机(Exchange)

  • 邮局
  • 决定信往哪寄

5. 队列(Queue)

  • 邮箱
  • 信先放这,等人来取

6. 绑定(Binding)

  • 邮局和邮箱的关系
  • 告诉邮局:这类信放这个邮箱

7. 路由键(Routing Key)

  • 信封上的地址
  • 邮局根据这个决定放哪个邮箱

四、安装RabbitMQ

方式1:Docker(推荐)

# 拉取镜像(带管理界面)
docker pull rabbitmq:3.12-management

# 运行
docker run -d \
  --name rabbitmq \
  -p 5672:5672 \      # 消息端口
  -p 15672:15672 \    # 管理界面端口
  -e RABBITMQ_DEFAULT_USER=admin \
  -e RABBITMQ_DEFAULT_PASS=admin123 \
  rabbitmq:3.12-management

方式2:直接安装

  1. 官网下载:www.rabbitmq.com
  2. 需要Erlang环境
  3. 安装完启动服务

访问管理界面

  1. 浏览器打开:http://localhost:15672
  2. 用户名:admin
  3. 密码:admin123
  4. 看到管理界面就中了

五、Spring Cloud Stream是啥?

不用直接操作RabbitMQ API,Spring Cloud Stream封装好了:

  • 生产者:用@EnableBinding(Source.class) + MessageChannel
  • 消费者:用@EnableBinding(Sink.class) + @StreamListener

新的方式(推荐,简单)

  • 生产者:用@EnableBinding + StreamBridge
  • 消费者:用@Bean定义Consumer

六、开整!用户注册发消息

步骤1:创建消息服务

新建项目:message-service

1. 加依赖

pom.xml

<!-- Spring Cloud Stream RabbitMQ -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

<!-- Nacos -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2. 配置文件

application.yml

server:
  port: 8083

spring:
  application:
    name: message-service
  
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    
    stream:
      # 绑定器(用RabbitMQ)
      binders:
        rabbit:
          type: rabbit
          environment:
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: admin
                password: admin123
                virtual-host: /  # 虚拟主机,相当于数据库
      
      # 绑定配置
      bindings:
        # 输出通道(发消息)
        userRegisterOutput:
          destination: user-register-exchange  # 交换机名
          content-type: application/json
          binder: rabbit
          group: user-register-group  # 消费者组,同一组只有一个消费者消费
        
        # 输入通道(收消息)
        userRegisterInput:
          destination: user-register-exchange
          content-type: application/json
          binder: rabbit
          group: user-register-group
          consumer:
            concurrency: 3  # 并发消费者数
        
        # 发短信的输入通道
        smsInput:
          destination: sms-exchange
          content-type: application/json
          binder: rabbit
        
        # 发优惠券的输入通道
        couponInput:
          destination: coupon-exchange
          content-type: application/json
          binder: rabbit
      
      # RabbitMQ配置
      rabbit:
        bindings:
          userRegisterOutput:
            producer:
              # 消息确认(确保消息不丢)
              confirm-type: correlated  # 异步确认
              # 消息持久化
              delivery-mode: persistent
          
          userRegisterInput:
            consumer:
              # 手动确认(确保消息处理完才删)
              acknowledge-mode: manual
              # 重试
              max-attempts: 3
              # 死信队列
              auto-bind-dlq: true  # 自动绑定死信队列
              dead-letter-queue-name: user-register-dlq
              dead-letter-exchange: user-register-dlq-exchange

步骤2:发消息(生产者)

1. 创建消息实体
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterEvent {
    private Long userId;
    private String username;
    private String phone;
    private String email;
    private Date registerTime;
}
2. 创建生产者
@Service
@Slf4j
public class MessageProducer {
    
    @Autowired
    private StreamBridge streamBridge;  // Spring Cloud Stream提供的
    
    /**
     * 发送用户注册消息
     */
    public void sendUserRegisterEvent(UserRegisterEvent event) {
        log.info("发送用户注册消息:{}", event);
        
        // 方式1:用StreamBridge(简单)
        boolean sent = streamBridge.send("userRegisterOutput", event);
        
        if (sent) {
            log.info("消息发送成功");
        } else {
            log.error("消息发送失败");
            // 可以存数据库,定时重发
        }
        
        // 方式2:用MessageChannel(老方式)
        // Message<UserRegisterEvent> message = MessageBuilder
        //     .withPayload(event)
        //     .setHeader("message-type", "user-register")
        //     .build();
        // userRegisterOutputChannel.send(message);
    }
    
    /**
     * 发送短信消息
     */
    public void sendSmsEvent(String phone, String content) {
        Map<String, Object> event = new HashMap<>();
        event.put("phone", phone);
        event.put("content", content);
        event.put("sendTime", new Date());
        
        streamBridge.send("smsOutput", event);
    }
    
    /**
     * 发送优惠券消息
     */
    public void sendCouponEvent(Long userId, String couponType) {
        Map<String, Object> event = new HashMap<>();
        event.put("userId", userId);
        event.put("couponType", couponType);
        event.put("sendTime", new Date());
        
        streamBridge.send("couponOutput", event);
    }
}
3. 测试接口
@RestController
@RequestMapping("/message")
public class MessageController {
    
    @Autowired
    private MessageProducer messageProducer;
    
    @PostMapping("/send/register")
    public String sendRegisterMessage(@RequestParam String username,
                                      @RequestParam String phone) {
        UserRegisterEvent event = new UserRegisterEvent(
            1L, username, phone, "test@example.com", new Date()
        );
        
        messageProducer.sendUserRegisterEvent(event);
        
        return "注册消息发送成功";
    }
}

步骤3:收消息(消费者)

1. 短信消费者
@Service
@Slf4j
public class SmsConsumer {
    
    /**
     * 监听用户注册消息,发欢迎短信
     * 方法名就是Bean名,要跟配置的binding名对应
     */
    @Bean
    public Consumer<UserRegisterEvent> userRegisterInput() {
        return event -> {
            log.info("收到用户注册消息,准备发短信:{}", event);
            
            // 模拟发短信
            sendSms(event.getPhone(), 
                   "亲爱的" + event.getUsername() + ",欢迎注册!");
            
            log.info("短信发送完成");
        };
    }
    
    /**
     * 监听短信队列
     */
    @Bean
    public Consumer<Message<Map<String, Object>>> smsInput() {
        return message -> {
            Map<String, Object> payload = message.getPayload();
            String phone = (String) payload.get("phone");
            String content = (String) payload.get("content");
            
            log.info("收到短信消息,手机号:{},内容:{}", phone, content);
            
            // 实际发短信逻辑
            boolean success = sendSms(phone, content);
            
            if (success) {
                log.info("短信发送成功");
                // 手动确认(如果配置了手动确认)
                // Channel channel = (Channel) message.getHeaders().get(AmqpHeaders.CHANNEL);
                // Long deliveryTag = (Long) message.getHeaders().get(AmqpHeaders.DELIVERY_TAG);
                // channel.basicAck(deliveryTag, false);
            } else {
                log.error("短信发送失败");
                // 可以抛异常,触发重试
                // throw new RuntimeException("短信发送失败");
            }
        };
    }
    
    private boolean sendSms(String phone, String content) {
        log.info("发送短信到 {}:{}", phone, content);
        // 模拟发短信,成功概率90%
        return Math.random() > 0.1;
    }
}
2. 优惠券消费者
@Service
@Slf4j
public class CouponConsumer {
    
    /**
     * 监听用户注册消息,送优惠券
     */
    @Bean
    public Consumer<UserRegisterEvent> userRegisterInput() {
        return event -> {
            log.info("收到用户注册消息,准备送优惠券:{}", event);
            
            // 模拟送优惠券
            sendCoupon(event.getUserId(), "WELCOME_10");
            
            log.info("优惠券发放完成");
        };
    }
    
    /**
     * 监听优惠券队列
     */
    @Bean
    public Consumer<Message<Map<String, Object>>> couponInput() {
        return message -> {
            Map<String, Object> payload = message.getPayload();
            Long userId = ((Number) payload.get("userId")).longValue();
            String couponType = (String) payload.get("couponType");
            
            log.info("收到优惠券消息,用户ID:{},优惠券类型:{}", 
                    userId, couponType);
            
            // 实际发优惠券逻辑
            boolean success = sendCoupon(userId, couponType);
            
            if (!success) {
                // 发优惠券失败,重试3次后进入死信队列
                throw new RuntimeException("发优惠券失败");
            }
        };
    }
    
    private boolean sendCoupon(Long userId, String couponType) {
        log.info("给用户 {} 发放优惠券 {}", userId, couponType);
        // 模拟发优惠券,成功概率80%
        return Math.random() > 0.2;
    }
}

七、实际场景:订单创建流程

场景:用户下单后

  1. 扣库存(同步)
  2. 创建订单(同步)
  3. 发短信通知(异步)
  4. 更新销量(异步)
  5. 推荐系统(异步)
  6. 数据分析(异步)

实现:订单服务

@Service
@Slf4j
public class OrderServiceWithMQ {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private ProductService productService;
    
    @Autowired
    private MessageProducer messageProducer;
    
    @Transactional
    public Order createOrderWithMQ(Long userId, Long productId, Integer count) {
        // 1. 扣库存(同步,必须成功)
        boolean stockSuccess = productService.decreaseStock(productId, count);
        if (!stockSuccess) {
            throw new RuntimeException("库存不足");
        }
        
        // 2. 创建订单(同步)
        Order order = createOrder(userId, productId, count);
        
        // 3. 发送订单创建消息(异步)
        sendOrderCreatedEvent(order);
        
        return order;
    }
    
    private Order createOrder(Long userId, Long productId, Integer count) {
        Order order = new Order();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setCount(count);
        order.setStatus(0);  // 待支付
        order.setCreateTime(new Date());
        
        orderMapper.insert(order);
        log.info("订单创建成功,订单ID:{}", order.getId());
        
        return order;
    }
    
    private void sendOrderCreatedEvent(Order order) {
        Map<String, Object> event = new HashMap<>();
        event.put("orderId", order.getId());
        event.put("userId", order.getUserId());
        event.put("productId", order.getProductId());
        event.put("amount", order.getTotalAmount());
        event.put("createTime", order.getCreateTime());
        
        // 发送到订单创建交换机
        messageProducer.sendOrderCreatedEvent(event);
        
        log.info("订单创建消息发送成功,订单ID:{}", order.getId());
    }
}

各个消费者处理:

// 短信服务消费者
@Component
@Slf4j
public class OrderSmsConsumer {
    
    @Bean
    public Consumer<Message<Map<String, Object>>> orderCreatedSmsInput() {
        return message -> {
            Map<String, Object> event = message.getPayload();
            Long orderId = (Long) event.get("orderId");
            Long userId = (Long) event.get("userId");
            
            log.info("收到订单创建消息,发送短信通知,订单ID:{}", orderId);
            
            // 查询用户手机号
            String phone = getUserPhone(userId);
            
            // 发送短信
            sendSms(phone, "您的订单已创建,订单号:" + orderId);
        };
    }
}

// 销量统计消费者
@Component
@Slf4j  
public class OrderStatConsumer {
    
    @Bean
    public Consumer<Message<Map<String, Object>>> orderCreatedStatInput() {
        return message -> {
            Map<String, Object> event = message.getPayload();
            Long productId = (Long) event.get("productId");
            Integer count = (Integer) event.get("count");
            
            log.info("更新商品销量,商品ID:{},数量:{}", productId, count);
            
            // 更新销量(这里可能更新Redis或数据库)
            updateProductSales(productId, count);
        };
    }
}

// 推荐系统消费者
@Component
@Slf4j
public class OrderRecommendConsumer {
    
    @Bean
    public Consumer<Message<Map<String, Object>>> orderCreatedRecommendInput() {
        return message -> {
            Map<String, Object> event = message.getPayload();
            Long userId = (Long) event.get("userId");
            Long productId = (Long) event.get("productId");
            
            log.info("更新用户偏好,用户ID:{},商品ID:{}", userId, productId);
            
            // 更新用户偏好
            updateUserPreference(userId, productId);
            
            // 实时推荐
            generateRecommendations(userId);
        };
    }
}

八、高级特性

1. 消息确认机制

spring:
  cloud:
    stream:
      rabbit:
        bindings:
          orderInput:
            consumer:
              acknowledge-mode: manual  # 手动确认
@Bean
public Consumer<Message<String>> orderInput() {
    return message -> {
        try {
            // 处理消息
            processOrder(message.getPayload());
            
            // 手动确认
            Channel channel = (Channel) message.getHeaders()
                .get(AmqpHeaders.CHANNEL);
            Long deliveryTag = (Long) message.getHeaders()
                .get(AmqpHeaders.DELIVERY_TAG);
            channel.basicAck(deliveryTag, false);
            
        } catch (Exception e) {
            // 拒绝消息,不放回队列(进入死信队列)
            Channel channel = (Channel) message.getHeaders()
                .get(AmqpHeaders.CHANNEL);
            Long deliveryTag = (Long) message.getHeaders()
                .get(AmqpHeaders.DELIVERY_TAG);
            channel.basicNack(deliveryTag, false, false);
        }
    };
}

2. 死信队列(DLQ)

消息处理失败时,进入死信队列:

spring:
  cloud:
    stream:
      rabbit:
        bindings:
          orderInput:
            consumer:
              auto-bind-dlq: true  # 自动创建死信队列
              dead-letter-queue-name: order-dlq
              dead-letter-exchange: order-dlq-exchange
              # 重试3次后进入死信队列
              max-attempts: 3
              # 重试间隔
              back-off-initial-interval: 1000
              back-off-multiplier: 2.0
              back-off-max-interval: 10000

3. 延迟消息(RabbitMQ插件)

安装延迟插件:

# 下载插件
# 放到RabbitMQ插件目录
# 启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

发送延迟消息:

public void sendDelayMessage() {
    Map<String, Object> headers = new HashMap<>();
    headers.put("x-delay", 60000);  // 延迟60秒
    
    Message<String> message = MessageBuilder
        .withPayload("延迟消息")
        .copyHeaders(headers)
        .build();
    
    streamBridge.send("delayOutput", message);
}

4. 消息重试

spring:
  cloud:
    stream:
      bindings:
        orderInput:
          consumer:
            max-attempts: 3  # 最大重试次数
            back-off-initial-interval: 1000  # 初始间隔1秒
            back-off-multiplier: 2.0  # 倍数递增
            back-off-max-interval: 10000  # 最大间隔10秒

九、消息队列的最佳实践

1. 消息设计

// 好的消息设计
@Data
public class OrderEvent {
    private String eventId;      // 消息ID(唯一)
    private String eventType;    // 事件类型:ORDER_CREATED
    private Long timestamp;      // 时间戳
    private String source;       // 来源服务
    private Object data;         // 业务数据
    private Map<String, Object> extra;  // 扩展字段
}

2. 幂等性处理

@Service
public class OrderConsumer {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Bean
    public Consumer<Message<OrderEvent>> orderInput() {
        return message -> {
            OrderEvent event = message.getPayload();
            
            // 1. 检查是否已处理(幂等)
            String key = "msg:" + event.getEventId();
            if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
                log.info("消息已处理,跳过:{}", event.getEventId());
                return;
            }
            
            // 2. 处理消息
            processOrder(event);
            
            // 3. 记录已处理
            redisTemplate.opsForValue()
                .set(key, "processed", 24, TimeUnit.HOURS);
        };
    }
}

3. 消息顺序性

// 如果需要顺序消费,用同一个队列,一个消费者
@Bean
public Consumer<Message<OrderEvent>> orderInput() {
    return message -> {
        // 处理消息
        // 保证同一个订单ID的消息顺序处理
    };
}

// 或者用RabbitMQ的单一消费者
@Bean
public MessageChannel orderInput() {
    return new DirectChannel();
}

4. 监控告警

@Component
@Slf4j
public class MessageMonitor {
    
    @EventListener
    public void handleMessageEvent(MessageEvent event) {
        // 记录消息处理日志
        log.info("消息事件:{}", event);
        
        // 监控指标
        monitorMessageCount(event.getDestination());
        
        // 错误告警
        if (event.isError()) {
            sendAlert("消息处理失败:" + event.getErrorMessage());
        }
    }
    
    private void monitorMessageCount(String queueName) {
        // 记录到监控系统
        // Prometheus, InfluxDB等
    }
}

十、常见问题

1. 消息丢失怎么办?

解决方案

  1. 生产者确认模式(publisher confirm)
  2. 消息持久化(delivery mode = persistent)
  3. 消费者手动确认(acknowledge-mode = manual)
  4. 死信队列监控

2. 消息重复消费怎么办?

解决方案

  1. 消息加唯一ID
  2. 消费者做幂等处理
  3. 用Redis记录已处理消息

3. 消息堆积怎么办?

解决方案

  1. 增加消费者数量
  2. 批量消费
  3. 监控队列长度,及时扩容
  4. 优化消费者处理速度

4. 顺序消息怎么保证?

解决方案

  1. 单个队列,单个消费者
  2. 按业务ID哈希到不同队列
  3. 消费者内部排队处理

5. 如何测试?

@SpringBootTest
@RunWith(SpringRunner.class)
public class MessageTest {
    
    @Autowired
    private StreamBridge streamBridge;
    
    @Test
    public void testSendMessage() {
        // 发送测试消息
        Map<String, Object> event = new HashMap<>();
        event.put("test", "value");
        
        boolean sent = streamBridge.send("testOutput", event);
        assertTrue(sent);
    }
    
    @Test
    public void testConsumeMessage() throws InterruptedException {
        // 等待消费者处理
        Thread.sleep(5000);
        
        // 验证处理结果
        // ...
    }
}

十一、今儿个总结

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

学会了啥?

  1. ✅ 消息队列的作用(异步、解耦、削峰)
  2. ✅ RabbitMQ核心概念
  3. ✅ Spring Cloud Stream使用
  4. ✅ 生产者发消息(StreamBridge)
  5. ✅ 消费者收消息(@Bean Consumer)
  6. ✅ 消息确认、重试、死信队列
  7. ✅ 实际应用场景

关键点

  1. 异步处理:耗时操作放消息队列
  2. 削峰填谷:流量高峰用队列缓冲
  3. 解耦:服务之间不直接调用
  4. 可靠性:消息确认、持久化、重试
  5. 幂等性:防止重复消费

十二、明儿个学啥?

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

明天咱学分布式链路追踪

  • 一个请求经过多个服务,出问题咋排查?
  • 哪个服务慢?哪个服务报错?
  • Sleuth + Zipkin追踪调用链
  • 跟破案一样,顺藤摸瓜找问题

明天咱当微服务侦探,追踪请求的每一步!🔍