Springboot集成RabbitMq

本篇简单的记录一下我使用RabbitMq的经历。主要解决以下几个问题。
1.保证消息的百分百投递
2.保证消息不重复消费
3.服务宕机后的备用方案

导入服务依赖


 <dependency>
         <groupId>org.springframework.bootgroupId>
         <artifactId>spring-boot-starter-amqpartifactId>
 dependency>
<dependency>
         <groupId>org.springframework.bootgroupId>
         <artifactId>spring-boot-starter-jdbcartifactId>
dependency>

yaml 配置

spring:
	rabbitmq:
        host: localhost
        port: 5672
        username: guest
        password: guest
        publisher-confirm-type: correlated
        publisher-returns: true
        listener:
            simple:
                acknowledge-mode: manual #手动确认模式
                prefetch: 100 #预取消息数

消息生产者

如何保证消息的百分百发送成功,我们应该建立重发机制,可以把每次发送的日志入库。
建立日志表msg_log

CREATE TABLE msg_log (
	msg_id varchar(255) NOT NULL,
	msg text NULL,
	exchange varchar(255) NOT NULL DEFAULT ''::character varying,
	routing_key varchar(255) NOT NULL DEFAULT ''::character varying,
	status int4 NULL DEFAULT 0,
	try_count int4 NULL DEFAULT 0,
	max_try_count int4 NULL DEFAULT 0,
	message_level int4 NULL DEFAULT 1,
	consume_try_count int4 NULL DEFAULT 0,
	next_try_time timestamp NULL,
	create_time timestamp NULL,
	update_time timestamp NULL,
	CONSTRAINT unq_msg_id PRIMARY KEY (msg_id)
);
comment on column msg_log.status is '0-IN_DELIVERY,1-DELIVERY_FAIL,2-DELIVERY_SUCCESS,3-CONSUMED_SUCCESS';
comment on column msg_log.message_level is '1-low,2-medium,3-high';

对应的日志类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class MsgLog  {
    private String msgId;
    private String msg;
    private String exchange;
    private String routingKey;
    private Integer status;
    private Integer tryCount;
    private Integer maxTryCount;
    private Integer consumeTryCount;
    private Timestamp nextTryTime;
    private Integer messageLevel;
    private Timestamp createTime;
    private Timestamp updateTime;
}

声明队列

@Configuration
public class RabbitDeclareConfig {
    @Bean
    public Queue queueYYDS(){
    	//四个参数分别是队列名称,是否要持久化,是否排他的,是否自动删除
        return new Queue("yyds",true,false,false);
    }
    @Bean
    public Queue queueDelay(){
        return new Queue("delay",true,false,false);
    }
    @Bean
    public DirectExchange directExchange1(){
    	//定义direct交换机,参数分别为交换机名称,是否持久化,是否自动删除
        return new DirectExchange("directExchange1",true,false);
    }
    @Bean
    public Exchange delayExchange1(){
    	//定义延迟交换机,方便实现延迟队列。前提是需要rabbitmq服务端安装一个插件,以便支持延迟交换机类型
        Map<String, Object> args = new HashMap<>(2);
        args.put("x-delayed-type", "direct");
        return new CustomExchange("delayExchange1", "x-delayed-message", true, false, args);
    }


    @Bean
    public Binding bindingQueueYYDS(Queue queueYYDS, DirectExchange directExchange1){
    	//把队列和交换机进行一个绑定。这里的routingKey使用的是队列名
        return BindingBuilder.bind(queueYYDS).to(directExchange1).withQueueName();
    }
    @Bean
    public Binding bindingQueueDelay(Queue queueDelay, Exchange delayExchange1){
    	//其中的“delay”是routingKey
        return BindingBuilder.bind(queueDelay).to(delayExchange1).with("delay").noargs();
    }

}

封装消息体

@Data
public class MessageContent {
    private String queueName;
    private String exchangeName;
    private String routingKey;
    private Integer messageLevel;
    private Integer delayTime;
    private Integer maxTryCount;
    private Object content;
}

消息发送方法

 public String send(MessageDTO dto){
        MessageContent content = new MessageContent();
        content.setContent(dto);
        content.setExchangeName(EXCHANGE_LOAN_DELAY);
        content.setRoutingKey(QUEUE_T24_UPDATE_ACCOUNT);
        content.setDelayTime(10*1000);
        sender.sendMsg(content);
        return "success";
    }

把消息内容封装成类,如下MessageDTO 是一个具体的消息对象

@Data
public class MessageDTO {
    private String message;
    private Integer id;
    //....有更多的内容可以继续填充
}

消息消费者

定义消费者接口,所有的消息消费者均实现这个接口,主要是为了保证在rabbitmq服务宕机后,依然可以保证消息被消费掉。

public interface RabbitBaseConsumer {
    void doExecute(String msg) throws Exception;
    String getExchange();
}

具体的某个消费者代码如下

@Slf4j
@RequiredArgsConstructor
//指定bean名称,后期从数据库消费消息时需要使用。
@Component("yyds")
public class YydsRabbitMQ implements RabbitBaseConsumer {

    private final RabbitMQService mqService;
    private ObjectMapper objectMapper = new ObjectMapper();
    //配置消息最大重试消费次数
	private Integer maxRetryCount = 3;
    @RabbitListener(queues = "yyds")
    public void consume(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
        long tag = message.getMessageProperties().getDeliveryTag();
        //判断消息是否被消费过(主要是根据消息的uuid进行判断)
        if(mqService.isConsumed(message)){
            channel.basicAck(tag, false);
            return;
        }
        if(mqService.isAchieveMaxRetryCount(message,maxRetryCount)){
            //达到最大的消费重试次数,手动把消息ack掉
            channel.basicAck(tag, false);
            return;
        }
        String msg = new String(message.getBody());
        // execute business code
        try {
            doExecute(msg);
        }catch (Exception e){
            mqService.msgTryCountIncreaseOne(message);
            channel.basicNack(tag, false,true);
            return;
        }
        channel.basicAck(tag, false);
        mqService.msgConsumeSuccess(message.getMessageProperties().getMessageId());
    }

    private void execute() throws Exception {
       	//do something  业务代码
    }
    @Override
    public void doExecute(String msg) throws Exception {
    	//把字符串消息转为具体消息对象。其中的MessageDTO是和发送消息的时候的消息是同一类型
        messageDTO = objectMapper.readValue(msg, MessageDTO.class);
        execute();
    }
    @Override
    public String getExchange() {
    	//返回队列所绑定的交换机名称
        return "directExchange1";
    }
}

可抽离的信息

常量信息

public class RabbitMQConstants {

    public final static Integer IN_DELIVERY = 0;
    public final static Integer DELIVERY_FAIL = 1;
    public final static Integer DELIVERY_SUCCESS = 2;
    public final static Integer CONSUMED_SUCCESS = 3;

    public final static Integer MAX_TRY_COUNT = 3;
    public final static Integer INIT_CONSUME_TRY_COUNT = 0;

    public final static String FIND_BY_ID_SQL = "select * from msg_log where msg_id = ?";
    public final static String UPDATE_STATUS_SQL = "update msg_log set update_time = ?,status = ? where msg_id = ?";
    public final static String INSERT_SQL = "insert into msg_log(msg_id,msg,exchange,routing_key,status,try_count,consume_try_count,create_time,message_level,max_try_count) " +
            "values(?,?,?,?,?,?,?,?,?,?)";
    public final static String CONSUME_TRY_COUNT_INCREASE_ONE_SQL = "update msg_log set update_time = ?,consume_try_count = ? where msg_id = ?";
    public final static String GET_DELIVERY_FAIL_MSG_SQL = "select * from msg_log where status = 1";
    public final static String TRY_COUNT_INCREASE_ONE_SQL = "update msg_log set update_time = ?,try_count = ? where msg_id = ?";
    public final static String GET_UNCONSUME_MESSAGE_BY_EXCHANGE_AND_ROUTINGKEY = "select * from msg_log where status = 1 and exchange = ? and routing_key = ?";
    public final static String DELETE_SEVEN_DAYS_AGO_MSG_SQL = "delete from loan_arrangement.msg_log where status = 3 and create_time < now()::timestamp + '-7 day'";
}

消息发送回调
当生产者发送消息后,会触发回调函数,根据返回的状态信息来判断是否发送成功。如果失败则入库等候重发

@Slf4j
@RequiredArgsConstructor
@Component
public class RabbitMQConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback  {
    private final JdbcTemplate jdbcTemplate;

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String s) {
        String uuid = correlationData.getId();
        MsgLog msgLog = jdbcTemplate.queryForObject(FIND_BY_ID_SQL, new BeanPropertyRowMapper<>(MsgLog.class), uuid);
        Integer status;
        if(Objects.isNull(msgLog)){
            return;
        }
        if (ack) {
            log.info("{}:message send to exchange", correlationData.getId());
            status = RabbitMQConstants.DELIVERY_SUCCESS;
        } else {
            log.error("{}:message send fail", correlationData.getId());
            status = RabbitMQConstants.DELIVERY_FAIL;
        }
        jdbcTemplate.update(UPDATE_STATUS_SQL, ps -> {
            Timestamp time = new Timestamp(System.currentTimeMillis());
            ps.setTimestamp(1,time);
            ps.setInt(2,status);
            ps.setString(3,uuid);
        });
    }
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        log.error("ReturnedMessage: " + returnedMessage);
    }
}

消息发送封装

@Slf4j
@RequiredArgsConstructor
@Component
public class RabbitMQSender {

    private final RabbitTemplate rabbitTemplate;
    private final JdbcTemplate jdbcTemplate;
    private ObjectMapper objectMapper = new ObjectMapper();
    
    public void sendMsg(MessageContent messageContent){
        String uuid = UUID.randomUUID().toString();
        saveMsgLog(uuid,messageContent.getContent(),messageContent.getExchangeName(),messageContent.getRoutingKey(),
                messageContent.getMessageLevel(),messageContent.getMaxTryCount());

        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    sendMsg( uuid, messageContent.getContent(),messageContent.getExchangeName(),messageContent.getRoutingKey(),messageContent.getDelayTime());
                }
            });
        } else {
            sendMsg(uuid, messageContent.getContent(),messageContent.getExchangeName(),messageContent.getRoutingKey(),messageContent.getDelayTime());
        }

    }

    private void sendMsg(String id,Object content,String exchange,String routingKey,Integer delay){
        CorrelationData correlationData = new CorrelationData(id);
        rabbitTemplate.convertAndSend(exchange,routingKey, content, message -> {
            message.getMessageProperties().setMessageId(id);
            if(Objects.nonNull(delay)) {
                message.getMessageProperties().setDelay(delay);
            }
            return message;
        },correlationData);
    }

    private void saveMsgLog(String msgId,Object content,String exchange,String routingKey,Integer messageLevel,Integer maxTryCount){
        String msg;
        try {
            msg = objectMapper.writeValueAsString(content);
        } catch (IOException e) {
            throw new RuntimeException();
        }
        jdbcTemplate.update(RabbitMQConstants.INSERT_SQL, ps -> {
            ps.setString(1,msgId);
            ps.setString(2,msg);
            ps.setString(3,exchange);
            ps.setString(4,routingKey);
            ps.setInt(5,RabbitMQConstants.IN_DELIVERY);
            ps.setInt(6,0);
            ps.setInt(7,RabbitMQConstants.INIT_CONSUME_TRY_COUNT);
            Timestamp time = new Timestamp(System.currentTimeMillis());
            ps.setTimestamp(8,time);
            ps.setInt(9,messageLevel == null ? MessageLevel.LOW.getLevel() : messageLevel);
            ps.setInt(10,maxTryCount == null ? MAX_TRY_COUNT : maxTryCount);
        });
    }
}

消息服务类

@Slf4j
@RequiredArgsConstructor
@Component
public class RabbitMQService {
    private final JdbcTemplate jdbcTemplate;
    public boolean isConsumed(Message message){
        String msgId = message.getMessageProperties().getMessageId();
        if (StringUtils.isBlank(msgId)) {
            return true;
        }
        MsgLog msgLog = jdbcTemplate.queryForObject(FIND_BY_ID_SQL, new BeanPropertyRowMapper<>(MsgLog.class), msgId);

        if(Objects.isNull(msgLog) || RabbitMQConstants.CONSUMED_SUCCESS.equals(msgLog.getStatus())){
            return true;
        }
        return false;
    }
    private MsgLog getMsgLogById(String id){
        MsgLog msgLog = jdbcTemplate.queryForObject(FIND_BY_ID_SQL, new BeanPropertyRowMapper<>(MsgLog.class), id);
        return msgLog;
    }
    public boolean isAchieveMaxRetryCount(Message message,Integer maxRetryCount){
        if(isConsumed(message)){
            return true;
        }
        String msgId = message.getMessageProperties().getMessageId();
        MsgLog msgLog = getMsgLogById(msgId);
        if(msgLog.getConsumeTryCount() < maxRetryCount){
            return false;
        }
        return true;
    }
    public void msgTryCountIncreaseOne(Message message){
        String msgId = message.getMessageProperties().getMessageId();
        MsgLog msgLog = getMsgLogById(msgId);
        jdbcTemplate.update(CONSUME_TRY_COUNT_INCREASE_ONE_SQL, ps -> {
            Timestamp time = new Timestamp(System.currentTimeMillis());
            ps.setTimestamp(1,time);
            ps.setInt(2,msgLog.getConsumeTryCount() + 1);
            ps.setString(3,msgId);
        });
    }
    public void msgConsumeSuccess(String msgId){
        jdbcTemplate.update(UPDATE_STATUS_SQL, ps -> {
            Timestamp time = new Timestamp(System.currentTimeMillis());
            ps.setTimestamp(1,time);
            ps.setInt(2,RabbitMQConstants.CONSUMED_SUCCESS);
            ps.setString(3,msgId);
        });
    }
    public List<MsgLog> getUnConsumeMessage(String exchangeName,String routingKey){
        List<MsgLog> msgLogs = jdbcTemplate.query(GET_UNCONSUME_MESSAGE_BY_EXCHANGE_AND_ROUTINGKEY,new MsgLogMapper(), exchangeName,routingKey);
        return msgLogs;
    }
}

定时任务——定期删除七天前已经完成的日志记录

为节省空间,把七天前已经消费过的消息日志记录删除。

@Slf4j
@RequiredArgsConstructor
@Component
public class DeleteMsgSchedule {
    private final JdbcTemplate jdbcTemplate;

    @Scheduled(cron = "0 0 0 * * ?")
    public void deleteSevenDaysAgoMsg() {
        log.info("-----------start delete seven days ago message data--------");
        jdbcTemplate.update(DELETE_SEVEN_DAYS_AGO_MSG_SQL);
        log.info("---------end delete seven days ago message data----------");
    }
}

定时任务——每半个小时扫描发送失败的消息进行重发

消息日志的包装类 这是jdbc查询列表所需要的一个包装类

@Data
public class MsgLogMapper implements RowMapper<MsgLog> {
    @Override
    public MsgLog mapRow(ResultSet rs, int rowNum) throws SQLException {
        MsgLog msgLog = new MsgLog();
        msgLog.setMsgId(rs.getString("msg_id"));
        msgLog.setMsg(rs.getString("msg"));
        msgLog.setStatus(rs.getInt("status"));
        msgLog.setTryCount(rs.getInt("try_count"));
        msgLog.setConsumeTryCount(rs.getInt("consume_try_count"));
        msgLog.setExchange(rs.getString("exchange"));
        msgLog.setRoutingKey(rs.getString("routing_key"));
        msgLog.setCreateTime(rs.getTimestamp("create_time"));
        msgLog.setNextTryTime(rs.getTimestamp("next_try_time"));
        msgLog.setUpdateTime(rs.getTimestamp("update_time"));
        msgLog.setMessageLevel(rs.getInt("message_level"));
        return msgLog;
    }
}

定时重发任务

@Slf4j
@RequiredArgsConstructor
public class MsgRetrySchedule {
    private final JdbcTemplate jdbcTemplate;
    private final RabbitTemplate rabbitTemplate;
    private final RabbitWarningService warningService;

    @Scheduled(cron = "0 0/30 * * * ? ")
    public void retryResendMsg() {
        log.info("-----------start scan msg_log table--------");
        List<MsgLog> msgLogs = jdbcTemplate.query(GET_DELIVERY_FAIL_MSG_SQL, new MsgLogMapper());
        for(MsgLog msgLog : msgLogs){
            if(msgLog.getTryCount() >= msgLog.getMaxTryCount()){
                log.warn("current msg receive max count,id="+msgLog.getMsgId());
                if(msgLog.getMessageLevel() == MessageLevel.HIGH.getLevel()){
                    log.warn("this message level is high");
                    //do something,such as send email for warning
                    sendWarnInfo();
                }
                continue;
            }

            sendMsg(msgLog.getMsgId(),msgLog.getMsg(),msgLog.getExchange(),msgLog.getRoutingKey());
            jdbcTemplate.update(TRY_COUNT_INCREASE_ONE_SQL, ps -> {
                Timestamp time = new Timestamp(System.currentTimeMillis());
                ps.setTimestamp(1,time);
                ps.setInt(2,msgLog.getTryCount()+1);
                ps.setString(3, msgLog.getMsgId());
            });
        }
        log.info("---------end scan msg_log table----------");
    }
    private void sendMsg(String id,Object content,String exchange,String routingKey){
        CorrelationData correlationData = new CorrelationData(id);
        rabbitTemplate.convertAndSend(exchange,routingKey, content, message -> {
            message.getMessageProperties().setMessageId(id);
            //todo delay queue retry send is need delay again?
            return message;
        },correlationData);
    }

    private void sendWarnInfo(){
        warningService.sendWarningEmail();
    }
}

告警接口 自己实现

public interface RabbitWarningService {
    void sendWarningEmail();
}

服务宕机后的备用方案

简单说一下思路,就是定时扫描mq的服务端口是否可用,如果端口不可用就认为服务挂了(应该有更好的方式,欢迎补充)。采用策略模式,在项目初始化时,把所有实现了RabbitBaseConusmer接口的消费者放到一个map里。然后调用doExecute方法即可,消息从数据库的日志表取出。
配置类

@ConfigurationProperties(prefix = "spring.rabbitmq")
@Data
public class RabbitProperties implements Serializable {
    private String host;
    private Integer port;
}

监视器

@Component
public class RabbitMqLocator implements ApplicationContextAware {
    private Map<String, RabbitBaseConsumer> map;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        map = applicationContext.getBeansOfType(RabbitBaseConsumer.class);
    }
    public Map<String, RabbitBaseConsumer> getMap(){
        return map;
    }
    public RabbitBaseConsumer getService(String beanName){
        return map.get(beanName);
    }
}

context

public class RabbitMQContext {
    private RabbitBaseConsumer rabbitMQ;
    public RabbitMQContext(RabbitBaseConsumer rabbitMQ){
        this.rabbitMQ = rabbitMQ;
    }
    public void doExecute(String msg) throws Exception {
        rabbitMQ.doExecute(msg);
    }
}

定期扫描任务

@Slf4j
@RequiredArgsConstructor
@Component
public class RabbitMQListener {
    private final RabbitProperties properties;
    private final RabbitMQService mqService;
    private final RabbitMqLocator rabbitMqLocator;

    @Scheduled(cron = "0 0/10 * * * ? ")
    public void scan(){
        log.info("-----------------scan rabbit mq server is active---------------");
        try{
            InetAddress theAddress = InetAddress.getByName(properties.getHost());
            new Socket(theAddress, properties.getPort());
        } catch (Exception e){
            log.error(e.getMessage());
            log.info("rabbit mq server is down, start backup plan");
            scanData();
        }
    }
    private void scanData(){
        Map<String,RabbitBaseConsumer> map = rabbitMqLocator.getMap();
        Set<String> queueSet = map.keySet();
        String exchangeName;
        for(String queueName : queueSet){
            exchangeName = rabbitMqLocator.getService(queueName).getExchange();
            List<MsgLog> logs = mqService.getUnConsumeMessage(exchangeName,queueName);
            for(MsgLog msgLog : logs){
                dealLog(msgLog.getMsg(),msgLog.getMsgId(),map.get(queueName));
            }
        }
    }
    private void dealLog(String msg, String msgId, RabbitBaseConsumer baseConsumer){
        try {
            RabbitMQContextcontext = new RabbitMQContext(baseConsumer);
            context.doExecute(msg);
            mqService.msgConsumeSuccess(msgId);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

简单示意图
Springboot集成RabbitMq_第1张图片

至此就是发送消息的全过程了。

你可能感兴趣的:(java-rabbitmq,rabbitmq,spring,boot)