为什么要用RabbitMQ
前面已经完成了商品详情和搜索系统的开发,但是还有一些问题
- 商品的原始数据保存在数据库中,增删改查都在数据库中完成。
- 搜索服务数据来源是索引库,如果数据库商品发生变化,索引库数据不能及时更新。
- 商品详情做了页面静态化,静态页面数据也不会随着数据库商品发生变化。
如果后台更改了商品价格,搜索页面和商品详情页显示的还是是旧的价格。
所以要用消息队列(MQ)来解决这个问题。
RabbitMQ介绍
消息队列是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。
也就是
- 商品服务对商品增删改以后,不用去操作索引库或静态页面,只是发送一条消息,也不关心消息被谁接收。
- 搜索服务和静态页面服务接收消息,分别去处理索引库和静态页面。
消息队列实现有两种方式:
AMQP和JMS
区别:
- JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
- JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
- JMS规定了两种消息模型;而AMQP的消息模型更加丰富
RabbitMQ基于AMQP协议,erlang语言开发,稳定性更好。项目使用RabbitMQ。
RabbitMQ几种模式
第一种:生产者——消息队列(MQ)——消费者
第二种:生产者——消息队列(MQ)——消费者 x 2(可以多个)
第三种:生产者——交换机——消息队列(MQ)——消费者 x 2(可以多个)
- 交换机决定消息交给谁(一般都给),生产者不能决定给谁。
第四种:生产者——交换机——消息队列(MQ)——消费者 x 2(可以多个)
- 生产者可以设置RoutingKey(一个标识)来决定给哪个队列。
第五种:生产者——交换机——消息队列(MQ)——消费者 x 2(可以多个)
- 跟第四种一样,只不过可以使用通配符,比第四种更加灵活。
第六种: 不是消息队列,干掉不学。
项目中使用的是第五种。
实际使用
以发送短息的服务为例子
service:(生产者)
因为RabbitMQ是AMQP的一种实现方式,所以这里注入的是AmqpTemplate
其中:
- ly.sms.exchange是 交换机的名字
- sms.verify.code 是 RoutingKey
@Autowired
private AmqpTemplate amqpTemplate;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 验证手机号
*
* @param phone
*/
public void sendCode(String phone) {
// 生成key
String key = KEY_PREFIX + phone;
// 生成验证码
HashMap<String, Object> msg = new HashMap<>();
// 验证码
String code = NumberUtils.generateCode(6);
msg.put("phone", phone);
msg.put("code", code);
// 发送验证码
amqpTemplate.convertAndSend("ly.sms.exchange", "sms.verify.code", msg);
// 保存验证码
redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);
}
SmsListener:(消费者,在短信微服务里)
@RabbitListener是在本地创建RabbitMQ里的交换机,队列等信息,也可以在RabbitMQ管理页面创建,不过这里创建更安全。
@Slf4j
@Component
@EnableConfigurationProperties(value = SmsProperties.class)
public class SmsListener {
@Autowired
private SmsProperties props;
@Autowired
private SmsUtil smsUtil;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "ly.sms.verify.queue"),
exchange = @Exchange(name = "ly.sms.exchange", type = ExchangeTypes.TOPIC),
key = "sms.verify.code"
))
public void listenVerifyCode(Map<String, Object> msg) {
if (msg == null) {
return;
}
String phone = (String) msg.remove("phone");
if (StringUtils.isBlank(phone)) {
return;
}
smsUtil.sendSms(props.getSignName(), props.getVerifyCodeTemplate(), phone, msg);
}
}
消息丢失问题
消息丢失一共有四种解决办法
- ack(消费者确认)
- 持久化
- 发送消息前,消息持久化到数据库,并记录消息状态(可靠消息服务)
- 生产者确认(publisher confirm)
第一种:
当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。
- 自动ACK:消息一旦被接收,消费者自动发送ACK
- 手动ACK:消息接收后,不会发送ACK,需要手动调用
看情况选择哪一种,重要的手动,不重要的选择自动就可以。
第二种:
了解这几种持久化方式就好,SpringBoot已经默认开启。
第三种:
发送消息前,消息持久化到数据库,并记录消息状态(可靠消息服务)
发送消息专门有一个微服务,有准备状态(在这个状态持久化),等待发送,如果发送失败,写一个扫描程序,扫描发送失败的消息,重新发送。不过要保证幂等性(统一接口重复执行,结果一致),假如通知转账,有网络原因发送两次,可以通过设置标识,如果之前已经消费过一次,第二次就不执行了。
第四种:
最后一行就是开启生产者确认(publisher confirm),RabbitMQ接收到消息向生存者发送一条回值,说明MQ已经收到了。
RabbitMQ,有第四种方式,但其他的mq没有。