微服务框架下的数据一致性第二篇

  上篇文章主要讲述的是如何实现的思路问题,本文内容主要是结合我们代码的具体实现来讲解一下(springboot环境下实现)。

  首先我们需要定义一些常量和工具类:如ExchangeEnum(交换器常量)和QueueEnum(队列常量)以及一些工具类,这都是准备工作(具体可以看github上的代码)。下面正式上代码。

  创建Rabbitmq的基本配置主要是为了创建ConnectionFactory连接,单例的RabbitTemplate连接以及监听者SimpleMessageListenerContainer,代码如下

/**
 * @功能:【RabbitMQConfig RabbitMQ配置】
 * @作者:代守诚
 * @日期:2018/10/19
 * @时间:9:38
 */
@Configuration
@ConfigurationProperties("common.rabbitmq.config")
public class RabbitmqConfig {

    /**
     * 服务器主机地址
     */
    @Value("${spring.rabbitmq.config.host}")
    private String host;
    /**
     * 端口
     */
    @Value("${spring.rabbitmq.config.port}")
    private Integer port;
    /**
     * 用户名
     */
    @Value("${spring.rabbitmq.config.username}")
    private String username;
    /**
     * 密码
     */
    @Value("${spring.rabbitmq.config.password}")
    private String password;
    /**
     * 虚拟机地址
     */
    @Value("${spring.rabbitmq.config.virtualHost}")
    private String virtualHost;

    /**
     * 连接器
     */
    @Bean
    public ConnectionFactory createConnectionFactory() {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost(virtualHost);
        /**
         * 同样一个RabbitTemplate只支持一个ConfirmCallback。
         * 对于发布确认,template要求CachingConnectionFactory的publisherConfirms属性设置为true。
         * 如果客户端通过setConfirmCallback(ConfirmCallback callback)注册了RabbitTemplate.ConfirmCallback,那么确认消息将被发送到客户端。
         * 这个回调函数必须实现以下方法:
         * void confirm(CorrelationData correlationData, booleanack);
         */
        connectionFactory.setPublisherConfirms(true);//开启confirm模式
        /**
         * 对于每一个RabbitTemplate只支持一个ReturnCallback。
         * 对于返回消息,模板的mandatory属性必须被设定为true,
         * 它同样要求CachingConnectionFactory的publisherReturns属性被设定为true。
         * 如果客户端通过调用setReturnCallback(ReturnCallback callback)注册了RabbitTemplate.ReturnCallback,那么返回将被发送到客户端。
         * 这个回调函数必须实现下列方法:
         *void returnedMessage(Message message, intreplyCode, String replyText,String exchange, String routingKey);
         */
        connectionFactory.setPublisherReturns(true);
        System.out.println("创建消息ConnectionFactory" + username);
        return connectionFactory;
    }

    /***
     *  消息操作模板 这里写成多例,是为了后续 针对特定业务定义特定的回调
     * @return
     */
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) //每次注入的时候回自动创建一个新的bean实例
    public RabbitTemplate amqpTemplate() {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(createConnectionFactory());
        rabbitTemplate.setExchange(ExchangeEnum.BD_FENQI_EXCHANGE.getValue());
        System.out.println("*************" + JsonTool.getJsonString(rabbitTemplate));
        return rabbitTemplate;
    }

    /**
     * queue listener  观察 监听模式
     * 当有消息到达时会通知监听在对应的队列上的监听对象
     *
     * @return
     */
    public SimpleMessageListenerContainer getSimpleMessageListenerContainer(ChannelAwareMessageListener mqConsumer) {
        System.err.println("#############进入监听者#############");
        SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(createConnectionFactory());
        listenerContainer.setExposeListenerChannel(true);//暴露监听渠道
        listenerContainer.setMaxConcurrentConsumers(10); //最大消费者数,大于ConcurrentConsumers
        listenerContainer.setConcurrentConsumers(3); //消费者数量
        listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL); //设置确认模式手工确认
        listenerContainer.setMessageListener(mqConsumer);//监听的消费者
        return listenerContainer;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public Integer getPort() {
        return port;
    }

    public void setPort(Integer port) {
        this.port = port;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getVirtualHost() {
        return virtualHost;
    }

    public void setVirtualHost(String virtualHost) {
        this.virtualHost = virtualHost;
    }
}

  具体的实现意义都是有注释的,同时我们还需要创建持久化的队列,持久化的交换器以及绑定关系

/**
 * @功能:【QueueConfig 队列配置】
 * @作者:代守诚
 * @日期:2018/10/19
 * @时间:10:36
 */
@Configuration
public class QueueConfig {
    /**
     * 注释
     * durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列
     * auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
     * exclusive  表示该消息队列是否只在当前connection生效,默认是false
     */

    @Bean
    public Queue applyTestQueue() {
        return new Queue(QueueEnum.BD_FENQI_APPLY_QUEUE.getQueueValue(), true, false, false);
    }

}
/**
 * @功能:【ExchangeConfig 消息交换机配置】
 * @作者:代守诚
 * @日期:2018/10/19
 * @时间:9:41
 */
@Configuration
public class ExchangeConfig {

    /**
     * 注释
     * 1.定义direct exchange,绑定queueTest
     * 2.durable="true" rabbitmq重启的时候不需要创建新的交换机
     * 3.direct交换器相对来说比较简单,匹配规则为:如果路由键匹配,消息就被投送到相关的队列
     * fanout交换器中没有路由键的概念,他会把消息发送到所有绑定在此交换器上面的队列中。
     * topic交换器你采用模糊匹配路由键的原则进行转发消息到队列中
     * key: queue在该direct-exchange中的key值,当消息发送给direct-exchange中指定key为设置值时,
     * 消息将会转发给queue参数指定的消息队列
     */
    @Bean
    public DirectExchange bdfqChange() {
        return new DirectExchange(ExchangeEnum.BD_FENQI_EXCHANGE.getValue(), true, false);
    }
}
/**
 * @功能:【BindConfig 绑定队列消息】
 * @作者:代守诚
 * @日期:2018/10/19
 * @时间:15:49
 */
@Configuration
public class BindConfig extends RabbitmqConfig {

    @Bean
    public Binding bdfqBinding(@Qualifier("applyTestQueue") Queue queue, @Qualifier("bdfqChange") DirectExchange directExchange) {
        return BindingBuilder.bind(queue).to(directExchange).with(QueueEnum.BD_FENQI_APPLY_QUEUE.getRoutingKey());
    }

    /**
     * 绑定监听
     */
    @Bean
    public SimpleMessageListenerContainer applyContainer(@Qualifier("applyTestQueue") Queue applyTestQueue, LoanApplyMQConsumer loanApplyMQConsumer) {
        System.err.println("***************" + JsonTool.getFormatJsonString(loanApplyMQConsumer));
        SimpleMessageListenerContainer applyContainer = getSimpleMessageListenerContainer(loanApplyMQConsumer);
        applyContainer.setQueues(applyTestQueue);//设置监听队列的名称
        System.err.println("***************" + JsonTool.getFormatJsonString(applyContainer));
        return applyContainer;
    }
}

接下来我们需要创建生产者和消费者的基类,供具体的生产者和消费者继承使用:

/**
 * @功能:【MsgSendConfirmCallBack 继承RabbitTemplate.ConfirmCallback, 消息确认监听器】
 * @作者:代守诚
 * @日期:2018/10/19
 * @时间:17:44
 */
public abstract class MsgSendConfirmReturnsCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    private RabbitTemplate rabbitTemplate;

    public MsgSendConfirmReturnsCallBack(RabbitTemplate re) {
        System.out.println("**************开始加载********************");
        this.rabbitTemplate = re;
        this.rabbitTemplate.setConfirmCallback(this); //设置
        this.rabbitTemplate.setMessageConverter(new FastJsonMQMessageConvert());
        System.out.println("**************加载成功********************" + JsonTool.getJsonString(rabbitTemplate));
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            System.out.println("***********消息发送成功id**************" + correlationData.getId());
            changeStatus(correlationData.getId());//处理自己的业务逻辑
        } else {
            System.out.println("***********消息发送失败id**************" + correlationData.getId());
            //处理自己的业务逻辑,比如说开启重试机制(本系统根据自身的业务需求,在定时任务中统一处理)
        }
    }

    @Override
    public void returnedMessage(org.springframework.amqp.core.Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.err.println("*******收到回调*******");
        System.err.println("********return--message:" + new String(message.getBody()) + ",replyCode:" + replyCode + ",replyText:" + replyText + ",exchange:" + exchange + ",routingKey:" + routingKey + "********");
    }

    /**
     * 外部请求发送消息
     *
     * @param routingKey
     * @return
     */
    public boolean sendMsg(String routingKey, Object msg) {
        Message message = new Message(null, msg, 1, System.currentTimeMillis(), routingKey);
        return sendMsg(message);
    }

    /**
     * 回调处理
     *
     * @param correlationDataId
     */
    protected abstract void changeStatus(String correlationDataId);

    private boolean sendMsg(Message inputMessage) {
        String id = UUID.randomUUID().toString().replaceAll("-", "");
        boolean isSuccess;
        try {
            // //重置消息id
            inputMessage.setId(id);
            //存入redis中
//            baseRedisDao.zadd(ERedisCacheKey.KEY_RABBIT_MSG_CACHE.getCode(),Double.parseDouble(String.valueOf(System.currentTimeMillis())),
//                    id,0); //保存消息编号
//            baseRedisDao.saveOrUpdate(ERedisCacheKey.KEY_RABBIT_ROUTING_KEY.getCode()+id,inputMessage.getRoutingKey()); //保存消息类型,以routingKey作为标志
//            baseRedisDao.saveOrUpdate(ERedisCacheKey.KEY_RABBIT_MSG_CONTENT_CACHE.getCode()+id,inputMessage); //保存消息主体内容
            rabbitTemplate.convertAndSend(inputMessage.getRoutingKey(), inputMessage.getMsg(), new CorrelationData(id));
            isSuccess = true;
        } catch (Exception e) {
            System.err.println("发送消息异常id:" + id);
            System.err.println(e);
            isSuccess = false;
        }
        return isSuccess;
    }

    public static class Message {
        /**
         * uuid
         */
        private String id;
        /**
         * 消息内容
         */
        private Object msg;
        /**
         * 已发送次数
         */
        private Integer count;
        /**
         * 有效期
         */
        private Long time;
        /**
         * routingKey
         */
        private String routingKey;

        public String getRoutingKey() {
            return routingKey;
        }

        public void setRoutingKey(String routingKey) {
            this.routingKey = routingKey;
        }

        public Message() {
        }

        public Message(String id, Object msg, Integer count, Long time, String routingKey) {
            this.id = id;
            this.msg = msg;
            this.count = count;
            this.time = time;
            this.routingKey = routingKey;
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public Object getMsg() {
            return msg;
        }

        public void setMsg(Object msg) {
            this.msg = msg;
        }

        public Integer getCount() {
            return count;
        }

        public void setCount(Integer count) {
            this.count = count;
        }

        public Long getTime() {
            return time;
        }

        public void setTime(Long time) {
            this.time = time;
        }
    }
}

其中confirm用于处理消费发送成功后MQ给我们回调确认,returnMessage同样的道理,上一篇文章也是有介绍过的。

/**
 * @功能:【BaseConsumer 消费者基类】
 * @作者:代守诚
 * @日期:2018/10/22
 * @时间:15:46
 */
public abstract class BaseConsumer implements ChannelAwareMessageListener {

    private FastJsonMQMessageConvert convert = new FastJsonMQMessageConvert();

    protected <T> T fromMessage(Message message, Class<T> cls) throws MessageConversionException {
        return JsonTool.getObj(String.valueOf(convert.fromMessage(message)), cls);
    }

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        try {
            processMessage(message);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            System.err.println("********处理异常:" + JsonTool.getString(message));
            System.err.println("*********" + e);
            processExceptionMessage(message, channel);
        }
    }

    protected abstract void processExceptionMessage(Message message, Channel channel) throws Exception;

    protected abstract void processMessage(Message message);

}

onMessage用于接收MQ服务器推送过来的消息,成功的话正确给响应,出现异常不发送响应。

下面进入正式的生产者和消费者:

生产者:

/**
 * @功能:【LoanApplyMQProducer 申请生产者】
 * @作者:代守诚
 * @日期:2018/10/19
 * @时间:17:01
 */
@Component
public class LoanApplyMQProducer extends MsgSendConfirmReturnsCallBack {

    public LoanApplyMQProducer(@Qualifier("amqpTemplate") RabbitTemplate rabbitTemplate) {
        super(rabbitTemplate);
    }

    @Override
    protected void changeStatus(String correlationDataId) {
        //根据需求完成状态修改
    }

    public boolean send(MemberPojo memberPojo) {
        memberPojo.setId(92L);
        memberPojo.setIdCardCode("3*************8");
        memberPojo.setBirthday(new Timestamp(System.currentTimeMillis()));
        memberPojo.setNickName("王者荣耀");
        memberPojo.setPhoneNumber("176****5555");
        memberPojo.setSex(1);
        memberPojo.setUserName("CN Dota2");
        return sendMsg(QueueEnum.BD_FENQI_APPLY_QUEUE.getRoutingKey(), memberPojo);
    }

消费者:

/**
 * @功能:【LoanApplyMQConsumer 申请消费者】
 * @作者:代守诚
 * @日期:2018/10/19
 * @时间:17:04
 */
@Component
public class LoanApplyMQConsumer extends BaseConsumer {


    @Override
    protected void processExceptionMessage(Message message, Channel channel) throws Exception {
        channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
    }

    @Override
    protected void processMessage(Message message) {
        //处理自身的业务逻辑
        System.err.println("************消费者开始处理业务逻辑***************");
        MemberPojo memberPojo = fromMessage(message, MemberPojo.class);
        System.err.println("**********" + JsonTool.getJsonString(fromMessage(message, MemberPojo.class)));
    }
}

测试方法:

/**
 * @功能:【RabbitmqTestController 测试rabbitmq方法】
 * @作者:代守诚
 * @日期:2018/10/23
 * @时间:11:38
 */
@RestController
@RequestMapping("/rabbit")
public class RabbitmqTestController {
    @Autowired
    private LoanApplyMQProducer loanApplyMQProducer;

    @RequestMapping(value = "/test", method = {RequestMethod.POST, RequestMethod.GET})
    public void rabbitmqTest() {
        System.err.println("**********发送方法准备就绪***********");
        boolean flag = loanApplyMQProducer.send(new MemberPojo());
        System.err.println("*********发送成功***********" + flag);
    }
}

  这就是具体的实现代码。如果你细心就会发现我们的代码中还有一个没有持久化,那就是message(消息)。如果一旦发生服务器宕机的情况,那么发送的队列里面的message都会消失,但是如果我们使用rabbitmq自带的消息持久化工具,我们同样会遇到如果消息的数据量过大的情况,会造成持久化的时候发生大量的IO操作,十分的消耗性能。这样我们就需要考虑一个更加优秀的方案来执行。作者的公司使用的是redis来完成这个操作,我们会在发送消息的时候将message(消息)存放在指定的redis队列中,在生产者收到ack回执后我们将其删除,对于没有收到回执的消息队列会将其对应的消息id打印出来方便日后查询。另外我们会启动一个定时任务,定时去redis中轮询那些在一定时间内(比如说十分钟)还没有删除的数据,查询到这些数据我们会将其重新发送到rabbitmq中,但是重试的次数一般都是有限制的(我们限制4次重试失败默认删除),一旦超过这个限制我们就会默认删除数据,进行手动处理。

  ps:由于这只是一个demo版本,我并没有加入logger和redis的操作。

 

代码连接:https://github.com/shouchengdai/rabbitmq_test

  

posted @ 2018-10-24 19:54  技术博客这里开始  Views(401)  Comments(0Edit  收藏  举报