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


      上篇文章主要讲述的是如何实现的思路问题,本文内容主要是结合我们代码的具体实现来讲解一下(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

      

  • 相关阅读:
    java
    Java 自定义异常(转载)
    java中更新文件时,指定原文件的编码格式,防止编码格式不对,造成乱码
    tar命令压缩和解压
    微服务之服务注册与发现--Consul(转载)
    git push 时:报missing Change-Id in commit message footer的错误
    git 版本回退
    item 快捷键
    mac下mysql的卸载和安装
    JAVA正则表达式:Pattern类与Matcher类详解(转)
  • 原文地址:https://www.cnblogs.com/daishoucheng/p/9845742.html
Copyright © 2020-2023  润新知