为什么使用消息队列
优点比较核心的有 3 个:解耦、异步、削峰。
缺点有以下几个:
- 系统可用性降低
系统引入的外部依赖越多,越容易挂掉。 - 系统复杂度提高
硬生生加个 MQ 进来,如何保证消息的传递及消费成功 - 一致性问题
A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,这数据就不一致了。
所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉。
如何保证高可用
RabbitMQ 的高可用性
RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。
镜像集群模式(高可用性)可用集群搭建篇
这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。
如何保证消息的传递和成功消费
,需要考虑:
-
消息发送确认机制
-
消费确认机制
-
消息的重新投递
-
消费幂等性, 等等
幂等性概念
来源http://www.javaxl.com/blog/articles/380
幂等性是什么?
我们可以借鉴数据库的乐观锁机制
比如我们执行一条更新库存的SQL语句
Update t_repository set count = count -1,version = version + 1 where
version = 1Elasticsearch也是严格遵循幂等性概念,每次数据更新,version+1(博主博客前面有提到)
消费端-幂等性保障
在海量订单产生的业务高峰期,如何避免消息的重复消费问题?
消费实现幂等性,就意味着,我们的消息永远不会消费多次,即使我们收到了多条一样的消息
业界主流的幂等性操作
唯一ID+指纹码机制,利用数据库主键去重
利用Redis的原子性去实现
唯一ID+指纹码 机制
唯一ID+指纹码机制,利用数据库主键去重
Select count(1) from T_order where ID=唯一ID+指纹码
好处:实现简单
坏处:高并发下有数据库写入的性能瓶颈
解决方案:根据ID进行分库分表进行算法路由
利用Redis的原子性去实现
使用Redis进行幂等,需要考虑的问题
第一:我们是否要进行数据落库,如果落库的话,关键解决的问题是数据库和缓存如何做到原子性?
第二:如果不进行落库,那么都存储到缓存中,如何设置定时同步策略?
springboot+rabbitmq实战
目录结构
- rabbitmq搭建搭建篇
- springboot配置
spring:
rabbitmq:
addresses: 49.233.89.188::5672,49.233.89.188:5673,49.233.89.188:5674
password: ''
username: ''
virtual-host: /
- config
/**
* @Description: rabbitmq配置信息
*/
@Configuration
@Slf4j
@AllArgsConstructor
public class RabbitmqConfig {
private final RedisService redisService;
private final static Long EXPIRE_TIME_ONE_DAY = 60 * 60 * 24L;
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//设置confirm模式 消息发送失败 记录redis重发
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
log.info("【mq应答:{},correlationData:{},cause:{}】",ack,correlationData,cause);
if (ack){
// 如果应答,说明消息已发送至server 将redis中的mq消息移除
redisService.hmRemove(RedisConstants.MQ_SEND_QUEUE,correlationData.getId());
}else{
Object obj = redisService.hmGet(RedisConstants.MQ_SEND_QUEUE, correlationData.getId());
if (obj != null){
MqMessage mqMessage = JSONObject.parseObject(obj.toString(),MqMessage.class);
if(mqMessage!=null){
mqMessage.setReSendCount(mqMessage.getReSendCount() + 1);
// 缓存队列1天
redisService.hmSet(RedisConstants.MQ_SEND_QUEUE, correlationData.getId().toString(), mqMessage,
EXPIRE_TIME_ONE_DAY);
}
}
}
});
// 同时设置,找不到exchange或者queue的时候回调
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.error("text:{},code:{},exchange:{},routingKey:{}",message,replyCode,replyText,routingKey);
});
return rabbitTemplate;
}
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMaxConcurrentConsumers(20);
//自动应答模式
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
/**
* 人脸识别队列
* @return Queue
**/
@Bean
public Queue faceIdentifyQueue(){
Queue queue = new Queue(MqConstant.FACE_IDENTIFY_QUEUE);
return queue;
}
}
- 拦截器
*/
@Component
@Aspect
@Slf4j
@AllArgsConstructor
public class MqRejectInterceptor {
private final MqCommonCheckService mqCommonCheckService;
private final SendMsgService sendMsgService;
@Around(value = "execution(* com.xxx.xxx.rabbitmq.consumer.*.*(..)) && @annotation(rabbitListener)")
public Object aroundService(ProceedingJoinPoint joinPoint, RabbitListener rabbitListener) throws Throwable {
Object object = null;
String classname = joinPoint.getTarget().getClass().getName();
String method = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info(classname + ":" + method);
if (args.length == 3) {
Message message = (Message) args[0];
Channel channel = (Channel) args[1];
MqMessage mqMessage = (MqMessage) args[2];
String msg = JSON.toJSONString(mqMessage);
log.info(classname + ":" + method + ";mqMessage=" + msg);
//MqMessage mqMessage = JSONObject.parseObject(msg.toJSONString(),MqMessage.class);
// 拒绝重复消费
boolean checkFlag = mqCommonCheckService.receiveCheck(mqMessage, rabbitListener.queues()[0]);
String queueName = rabbitListener.queues()[0];
if (checkFlag) {
log.info("消息已经处理了,queue={};msg:{}", queueName, msg);
// 若消息已处理,告知Mq拒绝消费此消息
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
} else {
boolean resendCheck = mqCommonCheckService.resendCheck(mqMessage, channel, message);
if(resendCheck){
try {
object = joinPoint.proceed();
// 已收到消息 需要告知MQ从队列中删除此消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception ex) {
log.error("接收处理消息失败,classname={}, queue={};msg={}", classname, queueName,msg, ex);
// 消息消费失败 需要移除已处理的标识
mqCommonCheckService.removeReceiveCheck(mqMessage, rabbitListener.queues()[0]);
// 已收到消息 需要告知MQ从队列中删除此消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
// 将处理失败的消息放入redis重发队列
sendMsgService.saveToResendQueue(mqMessage);
log.info("消息处理失败,重新放入重发队列:{}, method:{},;msg={}", classname, method, msg);
}
}else{
// 重发次数上限 直接拒绝消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
}
}
} else {
object = joinPoint.proceed();
}
return object;
}
}
- 生产者
@Component
@AllArgsConstructor
public class FaceIdentifyProvide {
private final SendMsgService sendMsgService;
/**
* 发送消息
* @Param object
* @return void
**/
public void sendMessage(Object object){
sendMsgService.sendMsg(new MqMessage(MqConstant.DIRECT_EXCHANGE,MqConstant.FACE_IDENTIFY_KEY,object));
}
}
*/
@Service
@AllArgsConstructor
@Slf4j
public class SendMsgServiceImpl implements SendMsgService {
private final static Long EXPIRE_TIME_ONE_DAY = 60 * 60 * 24L;// 1day
private final RedisService cache;
private final RabbitTemplate rabbitTemplate;
@Override
public void sendMsg(MqMessage mqMessage) {
//final JSONObject jsonMessage = (JSONObject) JSON.toJSON(mqMessage);
final String exchangeName = mqMessage.getExchange();
final String message = objToString(mqMessage);
cache.hmSet(RedisConstants.MQ_SEND_QUEUE, mqMessage.getId().toString(), mqMessage,
EXPIRE_TIME_ONE_DAY);// 缓存队列1天
try {
log.info("发送MQ:" + message);
rabbitTemplate.convertAndSend(mqMessage.getExchange(), mqMessage.getRoutingKey(),
mqMessage, new CorrelationData(mqMessage.getId()));
} catch (RuntimeException e) {
log.error("发送MQ失败,exchangeName=" + exchangeName + ";message=" + message, e);
saveToResendQueue(mqMessage);
try {
// retry
rabbitTemplate.convertAndSend(mqMessage.getExchange(), mqMessage.getRoutingKey(),
mqMessage, new CorrelationData(mqMessage.getId()));
} catch (RuntimeException ex) {
log.error("发送MQ失败,exchangeName=" + exchangeName + ";message=" + message, ex);
saveToResendQueue(mqMessage);
}
}
}
@Override
public void saveToResendQueue(MqMessage mqMessage) {
mqMessage.setReSendCount(mqMessage.getReSendCount() + 1);
cache.hmSet(RedisConstants.MQ_SEND_QUEUE, mqMessage.getId().toString(), (JSONObject) JSON.toJSON(mqMessage),
EXPIRE_TIME_ONE_DAY);// 缓存队列1天
}
private String objToString(Object obj) {
if (obj == null) {
return null;
} else if (obj instanceof String) {
return (String) obj;
} else {
return JSON.toJSONString(obj);
}
}
}
- 消费者
* 消息接受 并发消费
*/
@RabbitHandler
@RabbitListener(queues = MqConstant.FACE_IDENTIFY_QUEUE)
public void receive(Message message, Channel channel, MqMessage mqMessage) throws IOException {
log.info("queueName:{}, message:{}" ,MqConstant.FACE_IDENTIFY_QUEUE, mqMessage);
try{
// 保存人脸识别日志
BsIdentifyRecord bsIdentifyRecord = bsIdentifyRecordService.saveIdentifyRecord(mqMessage.getObj());
// 考勤打卡
pushAttendance(bsIdentifyRecord);
// 记录部门负责人办公轨迹
bsDeptHeadEventService.updateDeptHeadEventByIdentify(bsIdentifyRecord.getDeviceNo(), bsIdentifyRecord.getGuid());
}catch (Exception e){
log.error("人脸识别消费失败:",e);
}
}