本文基于上一篇文章的改造,实现系统之间的双向数据传输。

一:  需求背景

甲公司的A系统和乙公司的B系统(乙公司是甲方),简称AB系统,通过RabbitMQ进行数据传输,因为原来B系统已经通过MQ对接过其他系统,所以对于A系统也使用MQ的方式进行对接,这样对于B系统来说改动量基本为0

优点

1:应用解耦

系统之间没有强耦合性

2:异步

不需要两个系统之间实时响应,提高用户体验和系统吞吐量

3:削峰

 提高系统的稳定性

4:多系统对接代码成本低

如果A系统后期再于C、D系统交互,A系统基本不需要重新开发,只需要把数据复制一份发送到新队里即可,开发效率高

缺点

1:系统复杂度提高
需要依赖MQ中间件,以及中间服务,对于中间服务和MQ的高可用要求高

2:维护成本高

开发人员需要熟悉相关MQ知识以及中间服务的可读性,发生人员变更后不好交接,对于开发人员要求高

适用场景

1:如果你的系统只需要对接一个外部系统,那么不建议使用这种方式,如果你的系统对接多个系统或者前期规划后面会对接多个,那么建议这种方式

2:如果你的系统需要对接企业微信、钉钉、飞书等综合协同办公软件系统,比如把你们系统的审批消息、其他消息推送过去,那么建议这种方式

二:  涉及到的系统

1:内部系统

   甲公司的自研系统、平台系统(后面统一叫A系统)

2:三方系统

   乙公司的自研系统(后面统一叫B系统)、ERP系统 

三:技术栈

1:Jdk8、Spring Boot2.7.15

2:Redis7.0.11 一主两从三哨兵模式

3:Mysql8.0 主从复制、读写分离

4:Nginx1.24.0

5:RabbitMQ3.7.4(三方已提供,不需要自己搭建)

6:阿里云服务器3台、16核8G

四:架构设计

甲公司的自研系统和平台系统自己内部有对接,不需要咱们处理(平台系统可以看做是个对接平台,专门用于对接其他系统,本文章平台系统对接的是咱们自己写的【中间服务】系统)

乙公司的自研系统和ERP系统内部有对接,不需要咱们处理(乙公司的自研系统和上面的甲公司平台系统功能类似,专门用于对接其他系统)

RabbitMQ是乙公司原来就有的,咱们直接使用

我们需要做的部分就是写一个【中间服务】,通过REST服务连接甲公司的平台系统,通过MQ连接乙公司的自研系统(为了方便,后面统一叫系统A和系统B),架构图如下

qctivemq连接数配置_List

                                                   整体架构设计图1

因为他们自己的内部都有自己的处理对接,不需要咱们处理,所以为了方便理解,精简一下如下,可以理解为A系统和B系统通过中间服务以及MQ进行数据传输

qctivemq连接数配置_qctivemq连接数配置_02

                                                   整体架构设计图2

中间服务部署信息

qctivemq连接数配置_系统架构_03

                                                 腾讯云部署图

最外面是腾讯云负载均衡到3个服务器,运维负责搭建,下面是咱们自己的部署

服务器1:Nginx、中间服务、Redis主、Redis哨兵1、Mysql数据库

服务器2:Nginx、中间服务、Redis从1、Redis哨兵2、Mysql从数据库1

服务器3:Nginx、中间服务、Redis从2、Redis哨兵3、Mysql从数据库2

五:详细架构

1:如果保证消息丢失问题(消息可靠性)

  新建2个表,主表用于定时任务的轮询、日志查看、提供手动重试策略 ,子表用于保存实际的消息体内容

     主表:id,mq_unique_id(消息唯一键),push_status(推送状态),retry_count(重试次数),err_msg(错误信息),push_to(推送方向)

     子表:id,pid(主表Id),content(消息内容),bill_name(单据名称),queue_name(消息队列名称)

     无论接收到系统A或者MQ的消息,首先落库(保证可靠性),落库成功后再进行后续处理,失败则返回失败消息到系统A或者通知MQ,由他们进行处理。
 

2:如果保证消息重复性问题

解析双方的json数据,通过唯一消息id进行处理,可设置唯一索引,java代码捕获DuplicateKeyException异常, 不进行后续处理。

3:  数据库设计

主表:mq_master

CREATE TABLE `mq_master` (
  `id` bigint NOT NULL COMMENT 'id',
  `mq_unique_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '消息唯一id',
  `push_status` smallint NOT NULL DEFAULT '0' COMMENT '推送状态(0未推送1推送成功2推送失败)',
  `retry_count` smallint DEFAULT '0' COMMENT '重试次数',
  `err_msg` varchar(2000) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '失败信息',
  `push_to` varchar(32) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '推送方向(0推送、1接收)',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  KEY `push_status` (`push_status`) USING BTREE
) ENGINE=InnoD

子表:mq_slave

CREATE TABLE `mq_slave` (
  `id` bigint NOT NULL,
  `pid` bigint NOT NULL COMMENT '主表id',
  `content` text CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '消息体',
  `bill_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '单据名称',
  `queue_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '队列名称',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `pid` (`pid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

4:中间服务与系统A的对接架构

在系统A和中间服务之间增加Nginx做负载均衡,因为服务部署了3台服务器

主要逻辑:

1:A系统调用后先入库,入库成功后异步发送消息到MQ,发送消息到MQ的时候,使用唯一id加分布式锁,预防发送的时候正好定时任务同时处理了此消息

/**
 * @Tutle: null.java
 * @description: rest服务统一入口,所有的rest服务都写在此处
 * @author: Carry
 * @create: 2023-07-20 10:03
 * @Version 1.0
 **/
@Slf4j
@RestController
@RequestMapping("/middleware/sync")
public class RestRouterController {

    @Autowired
    IMqService mqService;

    @Autowired
    ThreadPoolTaskExecutor threadPoolTaskExecutor;


    /*** @Author Carry
     * @Description rest服务统一入口,发送消息到MQ,为了方便,调用前需要约定调用格式
     * @Date 17:32 2023/11/9
     * @Param [params]
     * @return
     **/
    @PostMapping(value = "/start")
    public Result start(@RequestBody CommonDto commonDto) {
        Result result = new Result();
        String errMsg = "";
        result.setErrorCode(ResultEnum.SYSTEM_ERROR.getErrorCode());
        result.setErrorMessage(errMsg);
        
        if (commonDto.getBillName() == null) {
            errMsg = "单据名称必传";
            log.error(errMsg);

            return result;
        }

        String billName = commonDto.getBillName();
        // 根据不同的单据,获取不同的处理实现类
        IBusinessService businessService = BusinessBeanFactory.getBusinessBeanByBillName(billName);

        if (businessService == null) {
            errMsg = "未定义此业务处理类";
            log.error(errMsg);

            return result;
        }


        // 入库操作以及获取发送到MQ的转换后的数据
        Map<String, Object> trans2MqData;
        try {
            trans2MqData = businessService.getDataToMq(commonDto);
        } catch (Exception e) {
            errMsg = "信息入库中间服务失败,失败信息:" + e.getMessage();
            log.error(errMsg, e);

            return result;
        }

        Boolean saveMsgFlag = (Boolean) trans2MqData.get("saveMsgFlag");
        if (!saveMsgFlag) {
            String saveMsgErrMsg = String.valueOf(trans2MqData.get("saveMsgErrMsg"));
            errMsg = "信息入库中间服务失败:" + saveMsgErrMsg;
            log.error(errMsg);

            return result;
        }

        try {
            String routingKey = String.valueOf(trans2MqData.get("routingKey"));
            String msg = String.valueOf(trans2MqData.get("msg"));
            String uniqueId = String.valueOf(trans2MqData.get("uniqueId").toString());

            threadPoolTaskExecutor.execute(() -> mqService.sendMsgToMq(routingKey, msg, uniqueId));
        } catch (Exception e) {
            // 捕获异常 交给定时任务执行
            errMsg = "信息入库中间服务成功,发送至MQ失败,失败信息:" + e.getMessage();
            log.error(errMsg, e);

            return result;
        }


        return new Result();
    }

}

2:定时任务加粒度大的分布式锁,预防多个服务同时启动定时任务

/**
 * @Tutle: null.java
 * @description: 定时任务轮询处理
 * @author: Carry
 * @create: 2023-08-04 08:41
 * @Version 1.0
 **/
@Component
@EnableScheduling
public class EventTask implements IBusinessService {

    private static final Logger log = LoggerFactory.getLogger(EventTask.class);


    @Autowired
    IMqMasterService mqMasterService;

    @Autowired
    IMqSlaveService mqSlaveService;

    @Autowired
    Sender sender;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * @return void
     * @Author Carry
     * @Description 30s
     * @Date 8:42 2023/8/4
     * @Param []
     **/
    @Scheduled(cron = "0/30 * * * * ?")
    public void toAMqTask() {
        RLock rLock = redissonClient.getLock(Constants.LOCK_TASK_MQ);

        try {
            boolean isLocked = rLock.tryLock(Constants.EXPIRATION_60S, TimeUnit.SECONDS);
            if (!isLocked) {
                log.error("toAMqTask定时任务未获取到分布式锁,放弃执行任务。。。。。。。");

                return;
            }

            String pushTo = "0";
            // 获取待发送到三方的数据
            List<MqMaster> mqMasterList = this.getDataList(pushTo);

            if (CollectionUtils.isEmpty(mqMasterList)) {
                return;
            }

            this.doprocess(mqMasterList, pushTo);

        } catch (Exception e) {
            log.error("toAMqTask error :{},################################", e.getMessage(), e);
        } finally {
            if (rLock != null && rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }

    }

    /**
     * @return void
     * @Author Carry
     * @Description 30s
     * @Date 8:42 2023/8/4
     * @Param []
     **/
    @Scheduled(cron = "0/30 * * * * ?")
    public void toSelfTask() {
        RLock rLock = redissonClient.getLock(Constants.LOCK_TASK_MQ);

        try {
            boolean isLocked = rLock.tryLock(Constants.EXPIRATION_60S, TimeUnit.SECONDS);
            if (!isLocked) {
                log.error("toSelfTask定时任务未获取到分布式锁,放弃执行任务。。。。。。。");

                return;
            }


            String pushTo = "1";

            List<MqMaster> mqMasterList = this.getDataList(pushTo);

            if (CollectionUtils.isEmpty(mqMasterList)) {
                return;
            }

            this.doprocess(mqMasterList, pushTo);

        } catch (Exception e) {
            log.error("toSelfTask error :{},################################", e.getMessage(), e);
        } finally {
            if (rLock != null && rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }


    }


    public List<MqMaster> getDataList(String direction) {
        MqMaster mqMaster = new MqMaster();
        mqMaster.setPushTo(direction);
        mqMaster.setRetryCount(5);

        // 获取待发送的数据
        List<MqMaster> mqMasterList = mqMasterService.selecListToPush(mqMaster);

        return mqMasterList;
    }

    /**
     * @return
     * @Author Carry
     * @Description 统一处理
     * @Date 12:33 2023/8/16
     * @Param
     **/
    public void doprocess(List<MqMaster> mqMasterList, String pushTo) {
        List<String> pidList = mqMasterList.stream().map(MqMaster::getId).collect(Collectors.toList());

        List<MqSlave> msgContentList = mqSlaveService.selectListByPidList(pidList);
        if (CollectionUtils.isEmpty(msgContentList)) {
            return;
        }

        msgContentList.stream().forEach(x -> {
            String uniqueId = x.getPid();
            String msg = x.getContent();

            String routingKey;
            String queueName = x.getQueueName();
            String billName = x.getBillName();
            

            // 发送到MQ
            if ("0".equals(pushTo)) {
                // 根据billName 获取路由key 直接发送 因为数据库已经是转换后的格式
                routingKey = BillName2QueueConstant.billName2QueueMap.get(billName);

                sender.toMq(routingKey, msg, String.valueOf(uniqueId));
            } else {
                // 不同队列不同实现类处理
                try {
                    IBusinessService businessService = BusinessBeanFactory.getBusinessBeanByQueueName(queueName);

                    businessService.getDataToSelf(msg, uniqueId);
                } catch (Exception e) {
                    log.error("定时任务发送到系统出错 error :{},################################", e.getMessage(), e);
                }
            }
            
        });

    }
}

3:消息处理成功或者失败更新消息表,通过RabbitMQ的reliableCallback机制处理

/**
 * @Tutle: null.java
 * @description: 生产者端可靠性配置记录
 * @author: Carry
 * @create: 2023-07-20 17:21
 * @Version 1.0
 **/
@Configuration
@Component
public class ReliableConfig {

    private static final Logger log = LoggerFactory.getLogger(ReliableConfig.class);

    @Autowired
    IMqMasterService mqMasterService;

    /**
     * @return org.springframework.amqp.rabbit.core.RabbitTemplate
     * @Author Carry
     * @Description 生产者消息可靠性确认
     * @Date 9:37 2023/8/18
     * @Param [connectionFactory]
     **/
    @Bean
    public RabbitTemplate reliableCallback(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);
        // 设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
        rabbitTemplate.setMandatory(true);

        // 消息可靠性确认
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                String uniqueId = String.valueOf(correlationData.getId());

                if (ack) {
                    log.error("数据发送到MQ成功,消息ID============" + uniqueId);
                    mqMasterService.successToUpdate(uniqueId);
                } else {
                    log.error("ReliableConfigConfirmCallbackFalse:原因:" + cause);

                    mqMasterService.errorToUpdate(uniqueId, cause);
                }

            }
        });

 

}

qctivemq连接数配置_系统架构_04

                                                       中间服务详细设计图1

5:中间服务与MQ的对接架构

为了实现高效率以及减少网络开销,使用rabbitMQ的推模式,即使用Spring AMQP的SimpleMessageListenerContainer进行处理。

此模式的特点是消费者具有一个缓冲区会缓存这些消息,所以会导致总是有一堆在内存中待处理的消息,当消息体过大时,会导致缓冲区溢出、消耗大量内存问题,为了防止此问题,建议折中处理,设置prefetch为1

@Bean
    public SimpleMessageListenerContainer simpleMessageListenerContainer() {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        // 最少消费者数量
         container.setConcurrentConsumers(2);
        // 最大消费者数量
        container.setMaxConcurrentConsumers(5);
        // spring-amqp 2.0版开始,默认的prefetch值是250,提高吞吐量,【但是处理速度比较慢的大消息时】,消息可能在内存中大量堆积,消耗大量内存,
        // 最大吞吐量  max-concurrency*prefetch
          container.setPrefetchCount(1);
        // RabbitMQ默认是自动确认 , AcknowledgeMode.NONE,这里改为手动确认消息
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        container.setTaskExecutor(taskExecutor());
        // 设置多个如下: 前提是队列都是必须已经创建存在的
        container.setQueueNames(QueueConstant.XXXXX,QueueConstant.XXXXX);
        // 消息接收处理类 AckRecivicerListener
        container.setMessageListener(ackRecivicerListener);

        return container;
    }

主要逻辑和前面类似:

1:监听到MQ的 消息后先入库,入库成功后异步发送消息到A系统,发送消息的时候,使用唯一id加分布式锁,预防发送的时候正好定时任务同时处理了此消息

2:定时任务加粒度大的分布式锁,预防多个服务同时启动定时任务

3:消息处理成功或者失败更新消息表

qctivemq连接数配置_qctivemq连接数配置_05

                                                中间服务详细设计图2

六:代码结构

模块依赖关系,api模块为最外层,打包打此模块就行

qctivemq连接数配置_定时任务_06

1. api     			 ----对外暴露接口
2. base     		 ----公共类
3. business    		 ----业务操作
4. consumer     	 ----消费者
5. producer      	 ----生产者
6. scheduled-task    ----定时任务

qctivemq连接数配置_List_07

                                             代码依赖图

七:模块详设

1:api

api为主服务,提供rest服务接口调用,auth认证,以及resources配置文件,后面打jar包直接打此模块即可

qctivemq连接数配置_推送_08

2:scheduled-task

此服务主要用于定时任务轮训,服务重试使用

qctivemq连接数配置_List_09

3:producer

生产者服务,对于MQ来说,咱们的中间服务就是生产者,发送到MQ的消息依赖此服务

qctivemq连接数配置_qctivemq连接数配置_10

4:consumer

消费者服务,对于MQ来说,咱们的中间服务也是消费者,接收MQ的消息依赖此服务

5:business

业务模块,中间服务最重要的模块,代码的绝大部分都在此模块里,包含增删改查等功能以及业务单据的转换等,通用的消息相关的CRUD已经实现可以不用关注,主要关注自己的业务逻辑即可,比如转换成MQ的消息格式、接收MQ的消息转换成系统需要的格式等

qctivemq连接数配置_定时任务_11

里面很多通用代码都已经实现,需要自己实现的都加了TODO注释,比如获取数据入库以及发送到MQ的部分,使用了模版方法模式,使用者只需要关注重写的部分即可:

package com.carry.middleservice.business.service.trans.impl.tothird;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.carry.middleservice.business.service.common.impl.MsgServiceImpl;
import com.carry.middleservice.base.utils.SpringUtils;

import java.util.*;


/**
 * @Tutle: null.java
 * @description: 统一业务处理类,发送到MQ
 * @author: Carry
 * @create: 2023-07-25 09:42
 * @Version 1.0
 **/
public abstract class AbstractMqBusinessService {

    /**
     * @return java.util.Map<java.lang.String, java.lang.Object>
     * @Author Carry
     * @Description 组装发送到MQ的消息
     * @Date 15:32 2023/11/10
     * @Param [msgMap]
     **/
    public final Map<String, Object> trans2MqData(Map<String, Object> msgMap) {
        Map<String, Object> result = new HashMap<>();

        // 1入库
        result = this.self2Mysql(msgMap);

        // 2 获取路由
        String routingKey = this.getRoutingKey();

        result.put("routingKey", routingKey);

        return result;
    }



    /**
     * @return java.lang.String
     * @Author Carry
     * @Description 获取路由信息,不同的单据自己实现此方法
     * @Date 15:32 2023/11/10
     * @Param []
     **/
    public abstract String getRoutingKey();


    /**
     * @return java.util.Map<java.lang.String, java.lang.Object>
     * @Author Carry
     * @Description 主数据 主表数据通用处理
     * @Date 10:57 2023/8/15
     * @Param [syncData]
     **/
    public Map<String, Object> self2Mysql(Map<String, Object> selfData) {
        Map<String, Object> result = new HashMap<>();
        String uniqueId = String.valueOf(selfData.get("uniqueId"));

        MsgServiceImpl msgService = SpringUtils.getBean(MsgServiceImpl.class);

        String selfDataStr = JSON.toJSONString(selfData, SerializerFeature.WriteMapNullValue, SerializerFeature.QuoteFieldNames);

        // 入库
        boolean saveMsgFlag = msgService.saveMsg(null, selfDataStr, "0", uniqueId);

        // TODO 返回信息(根据自己系统的约定进行设计)
        result.put("saveMsgFlag", saveMsgFlag);
        result.put("uniqueId", uniqueId);
        result.put("msg", selfDataStr);


        return result;
    }



}

6:base

公共方法模块

qctivemq连接数配置_qctivemq连接数配置_12