消息驱动 - 不用打电话,写信也能办事
零基础全栈开发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,因为:
- Spring Cloud Stream支持好
- 管理界面漂亮
- 功能全(路由、死信、延迟)
- 社区活跃
三、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:直接安装
- 官网下载:www.rabbitmq.com
- 需要Erlang环境
- 安装完启动服务
访问管理界面
- 浏览器打开:
http://localhost:15672 - 用户名:
admin - 密码:
admin123 - 看到管理界面就中了
五、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;
}
}
七、实际场景:订单创建流程
场景:用户下单后
- 扣库存(同步)
- 创建订单(同步)
- 发短信通知(异步)
- 更新销量(异步)
- 推荐系统(异步)
- 数据分析(异步)
实现:订单服务
@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. 消息丢失怎么办?
解决方案:
- 生产者确认模式(publisher confirm)
- 消息持久化(delivery mode = persistent)
- 消费者手动确认(acknowledge-mode = manual)
- 死信队列监控
2. 消息重复消费怎么办?
解决方案:
- 消息加唯一ID
- 消费者做幂等处理
- 用Redis记录已处理消息
3. 消息堆积怎么办?
解决方案:
- 增加消费者数量
- 批量消费
- 监控队列长度,及时扩容
- 优化消费者处理速度
4. 顺序消息怎么保证?
解决方案:
- 单个队列,单个消费者
- 按业务ID哈希到不同队列
- 消费者内部排队处理
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 ,获取本文所有示例代码、配置模板及导出工具。
学会了啥?
- ✅ 消息队列的作用(异步、解耦、削峰)
- ✅ RabbitMQ核心概念
- ✅ Spring Cloud Stream使用
- ✅ 生产者发消息(StreamBridge)
- ✅ 消费者收消息(@Bean Consumer)
- ✅ 消息确认、重试、死信队列
- ✅ 实际应用场景
关键点
- 异步处理:耗时操作放消息队列
- 削峰填谷:流量高峰用队列缓冲
- 解耦:服务之间不直接调用
- 可靠性:消息确认、持久化、重试
- 幂等性:防止重复消费
十二、明儿个学啥?
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
明天咱学分布式链路追踪!
- 一个请求经过多个服务,出问题咋排查?
- 哪个服务慢?哪个服务报错?
- 用Sleuth + Zipkin追踪调用链
- 跟破案一样,顺藤摸瓜找问题
明天咱当微服务侦探,追踪请求的每一步!🔍