RabbitMQ系列—场景应用(电子政务平台:驾驶人违法记录同步功能)

来源:互联网 发布:e语言源码 编辑:程序博客网 时间:2024/05/23 19:21

一、业务场景说明

这里写图片描述

只是一个为了汇总全国机动车违法记录而设计的多系统数据同步功能。最主要的功能是进行违法记录的上传以及在各省间同步跨省违法记录。在进行架构设计之前,我们首先需要了解一些关于整个系统业务背景:任何系统设计都不能脱离系统实际业务背景而存在!

  • 首先整个系统分为全国系统和32个省级系统:由于每个省都有符合该省实际情况的、处理过程完全不同的违法记录处理操作。并且每个省的驾管系统电子化推进情况也不尽相同:有的省已经走在了全国的前列,基本上所有驾管业务数据都已经与全国系统实现了同步;有的省可能才开始建设,甚至都没有自己的违法记录电子信息。

  • 违法记录信息的同步过程分为上行同步和下行同步:驾驶人违法记录信息需要从省级系统实现到全国系统的同步(至于是省级系统确认违法信息时立即进行同步,还是省级系统在某个固定的时间周期统一进行同步,这就是给各省级系统自己的处置权了),这样的同步过程称为上行同步。如果某个违法者是在本省违法的,那么直接进行上行同步就可以了;如果某个违法者是在外省违法,那么除了进行上行同步外,当全国系统发现这是一条异地违法记录,并且违法者身份证所在省已经接入了全国系统,就需要通过全国系统将这条违法记录向违法者身份证所在省的省级系统进行同步,这样的同步过程称为下行同步

  • 如果某省的系统新接入了全国系统,那么全国系统需要在这个省的系统同步功能模块准备好后,将这个省接入全国系统前所相关的跨省异地违法记录全部进行一次上行同步和下行同步。那么什么叫“省级系统准备好”呢?不一定接入全国系统的每个省级系统都能立刻稳定的工作,任何系统都有一个稳定周期。在这个稳定周期内,开发团队需要完成诸如观察系统工作情况、调整功能模块的运算性能、进行软件Bug修改、进行软件操作过程优化等等工作。

  • 另外各省的软件供应商不尽一样,使用的开发语言也不相同。所以在考虑接入方案时,需要方案支持多种编程语言,或者是使用多种语言都支持的一种通用协议。另外省级系统和全国系统应该尽可能的进行业务脱耦,这样才可以保证省级系统的软件供应商不必为了实现违法记录上行同步和下行同步功能专门更改编程语言,也不必为了实现以上功能专门调整省级系统的固有业务过程和系统架构(有的时候因为技术问题调整业务过程客户方是绝对不会答应的)。

  • 由于是演示场景,目的是演示消息系统中间件在这个需求场景实现方案中的作用。所以我们假设整个需求环境是具有“要实现违法记录信息同步”的前置功能/前置条件的。这些前置功能/前置环境包括(但不限于):全国驾驶人基本档案信息库(这部分信息可能也是通过各省级系统同步而来)、全国人口身份信息库等。

二、总体设计思路

以上业务场景是一个典型的需要使用支持事务的消息中间件的应用场景——追求消息到达和处理的稳定性,您可以使用本章我们详细介绍的ActiveMQ也可以使用上一节介绍的RabbitMQ:因为他们都支持多语言接入,都提供消息事务支持,都支持消费者侧的消息回执确认。另外,这个业务场景中也要兼顾一定的数据吞吐量。

这里写图片描述

  • 在已有的系统中加入消息队列服务最大的目的是保持已有系统的原始架构不作调整。不作调整的原因可能是因为原有系统由于设计不当已经不可能再做大的调整,否则将付出无法承受的代价;也可能是由于非技术原因,技术团队没有相应的权限调整已有架构设计。

  • 采用消息队列服务方案的另一个优点是可以缓解数据洪峰。在这个示例场景中最典型的体现就是:需求中明确的提到,当一个省级系统新接入时,需要进行一次完整的违法记录的上行同步和下行同步。这样的话有可能在这个省级系统上积累了7、8年的违法记录会被同步到全国系统,这个过程可能会出现一定的数据堆积。但是由于我们给出的消息服务中间件的数据持久化性能较为强劲(请参见下一小节的详细设计),所以数据同步压力基本上不会传递到上层系统的业务处理层。

  • 分析场景中对于省级系统接入的需求描述,技术层面上最大的几个问题是:不同省级系统采用的架构不一样,使用的编程语言不一样,技术团队水平不一样。为了保证接入方案的安全效果、性能效果和工作效率,全国系统应该为省级系统提供不同的语言开发包和集成文档(类似于集成微信/支付宝/淘宝等开放平台);根据经验,全国系统应首先为各省级系统优先提供JAVA和C#的集成开发包。

  • 开发包中主要对连接消息服务队列的行为进行封装、对上行消息和下行消息的文本格式进行规范(保证各省系统上行消息的文本格式是一致的,保证各省收到的下行消息都是上级系统所统一的格式)、对消息的加密和解密协议进行封装、对消息发送过程和消息订阅过程进行封装(包括消息生产者进行上行消息的发送和消息消费者进行下行消息的接收)。另外,为了保证传输过程文本消息的通讯安全,开发包中还封装了SSL加密/解密过程。

  • 最后,由于要保证所有的上行消息和下行消息一定会被目标系统正常处理。所以这些消息都应该是PERSISTENT Meaage形式的消息。并且无论是上行消息还是下行消息,都应该在超出重试次数后被放置到“死信队列”(Dead Letter Queue),以便进行人工干预。重试次数应该设置为2——3次左右,因为ActiveMQ默认重发6次(redeliveryCounter==6)的值过大,在消息出现问题时重试次数过多会严重影响消息中间件服务的处理效率。

三、消息队列服务详细设计

下面我们来具体分析一下在这个实例场景下消息队列服务部分的架构设计(即上图中“基于ActiveMQ的消息队列服务”部分的设计)。架构详细设计部分分为硬件结构设计和软件规则设计部分,我们首先讨论硬件设计部分的方案。

3.1、硬件方案部分

其中硬件部分的设计来源于上一节文章中已经提到的ActiveMQ服务集群的综合应用。为了保证每个ActiveMQ节点都能高效工作,我们还按照上文提到的ActiveMQ服务单节点的性能优化原则进行了相应配置。

这里写图片描述

在这个示例的应用场景中,虽然高并发性并不是建设方主要追求的。但如上文所述,为了保证在数据洪峰出现时数据处理压力不传递给业务服务,并且ActiveMQ服务集群能够尽快完成数据洪峰的吞吐工作(在建设方预算允许的情况下),我们为每一组ActiveMQ M/S集群选择了IBM的基于SAN(Storage Area Network)的共享存储解决方案。其中使用的IBM Storwize V7000存储盘阵设置成RIDA5模式,并配置20TB存储空间。

实际上在这个示例场景中,之所以采用这样的硬件设计方案更是为了在有限的篇幅内为读者讲解更多的设计方式。由于使用了基于SAN的共享存储方案,所以之前提到的LevelDB + zookeeper的热备方案就不必再使用了(当然LevelDB + zookeeper的方案也是可选方案)。为了节约成本,也可以多组SAN共享存储使用用一台FC 光交换机和一台存储盘阵,但是这样可能出现因为FC光交换机的单点故障或者磁盘阵列单点故障导致整个集群宕机的情况

这里写图片描述

3.2、软件规则部分

在前文提到,由于省级系统都使用了全国系统统一提供的开发包进行上行消息和下行消息的处理,所以接入消息同步功能的所有系统都不必担心消息文本的格式问题;那么在ActiveMQ消息队列服务的业务规则部分,最重要的规则就是如何规划上行消息和下行消息存储的队列。

这里写图片描述

如上图所示所有省级系统的上行消息同时共享一个消息队列,这是因为这些省级系统都是使用上级系统统一提供的开发包进行二次开发,所以无论哪个省级系统向上同步的消息格式都是一致的(且进行了内容加密),所以它们可以共享一个消息队列,并由上级系统使用一套相同的处理逻辑进行接受。

当上级系统发现有跨省产生的违法记录时,就需要通过下行队列将这个违法记录发送给违法者所在省的省级系统,这些下行信息由于有不同的消费者(省级系统),且这些消费者所涉及的业务处理逻辑都可能不一样,所以应该使用不同的消息队列来发送针对不同省级系统的下行队列。另外,这样的消息下发机制还可以保证在省级系统出现故障时,下行消息不会丢失——直到这些下行消息被对应的省级系统正确处理。

3.3、主要代码片段

由于整个方案需要相当的代码编写工作,所以不可能在这个示例场景中演示所有的代码实现。为了让读者能够了解其中更细节的实现情况,在这个小节中我们重点演示主要的代码实现片段(使用Java语言)。包括省级系统开发包中如何进行上行队列的连接,如何开始监听下行队列——只有同时成功建立上行队列连接和下行队列连接,才能认为信息同步模块启动成功了。

为了保证信息同步模块独立于现有系统的其他功能模块进行工作,应该使用专门的新线程建立上行队列连接和下行队列连接:

import javax.jms.Connection;import javax.jms.JMSException;import javax.jms.Message;import javax.jms.MessageConsumer;import javax.jms.MessageListener;import javax.jms.MessageProducer;import javax.jms.Session;import org.apache.activemq.ActiveMQConnectionFactory;import org.apache.activemq.ActiveMQPrefetchPolicy;import org.apache.activemq.RedeliveryPolicy;import org.apache.activemq.command.ActiveMQQueue;/** * 这个启动器用于启动上行队列和下行队列的连接。 上行队列是一个独立的线程,下行队列也是一个独立的线程。 * 另外,上行队列和下行队列都可以使用同一个session * @author yinwenjie */public class ClientStartup implements Runnable {    /**     * 下行队列名称(可存放于配置文件中)     */    private String downStream = "downStream";    /**     * 保证整个进程只有一个Producer被创建和使用     */    private static MessageProducer PRODUCER;    /**     * 标示该启动器是否正常连接到消息中间件服务     */    private static boolean ISSTARTED = false;    /**     * 这个静态方法用于从ClientStartup启动器中获取整个进程中唯一一个消息生产者。     * 注意,为了保证该进程其它线程安全获取ClientStartup.PRODUCER,     * 所以只有等待run()方法成功运行完成,该ClientStartup.PRODUCER才能被其它线程拿到。     *      * @return     */    public static MessageProducer getNewInstanceProducer() {        synchronized (ClientStartup.class) {            while (!ClientStartup.ISSTARTED) {                try {                    ClientStartup.class.wait();                } catch (InterruptedException e) {                    e.printStackTrace(System.out);                }            }        }        return ClientStartup.PRODUCER;    }    @Override    public void run() {        // 开发包中对于消息中间件服务的连接一定要使用故障转移        ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(                "failover:(tcp://192.168.61.138:61616,tcp://192.168.61.139:61616)");        // 这是上行消息队列        ActiveMQQueue upstreamQueue = new ActiveMQQueue("upstream");        // 这是下行消息队列        // 下行消息队列必须由上级系统创建,并且在下级系统使用的开发包所对应的配置文件中进行配置        ActiveMQQueue downStreamQueue = new ActiveMQQueue(this.downStream);        // ============开始创建        Connection connection = null;        Session session = null;        try {            // ack优化选项            connectionFactory.setOptimizeAcknowledge(true);            connectionFactory.setProducerWindowSize(2048000);            connectionFactory.setSendAcksAsync(true);            // ack信息最大发送周期            connectionFactory.setOptimizeAcknowledgeTimeOut(5000);            // 连接属性优化:设置重试次数为2            RedeliveryPolicy redeliveryPolicy = connectionFactory.getRedeliveryPolicy();            redeliveryPolicy.setMaximumRedeliveries(2);            // 连接属性优化:设置预取数量            ActiveMQPrefetchPolicy prefetchPolicy = connectionFactory.getPrefetchPolicy();            prefetchPolicy.setQueuePrefetch(20);            // 设置获取消息的线程池大小            connectionFactory.setMaxThreadPoolSize(2);            connection = connectionFactory.createQueueConnection();            // 连接            connection.start();            // 建立会话(设置一个带有事务特性的会话)            session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);        } catch (Exception e) {            e.printStackTrace(System.out);            return;        }        // ===========首先进行订阅消费者连接(下行消息队列的连接)        // 注意,正式代码中不应该允许JMS Client创建一个新的队列        // 所以应该使用其它方式(例如其他查询接口),在创建前判断队列是否已经存在        MessageConsumer consumer;        try {            consumer = session.createConsumer(downStreamQueue);            consumer.setMessageListener(new MessageListener() {                @Override                public void onMessage(Message message) {                    /*                     * 这里进行正式业务的处理                     */                }            });        } catch (JMSException e) {            e.printStackTrace(System.out);            // 一旦出错,就关闭整个连接,退出启动过程            try {                connection.close();            } catch (JMSException e1) {                e.printStackTrace(System.out);            }            return;        }        // ==========然后创建消息生产者俩呢及(上行消息队列的连接)        try {            ClientStartup.PRODUCER = session.createProducer(upstreamQueue);        } catch (JMSException e) {            e.printStackTrace(System.out);            // 一旦出错,就关闭整个连接,退出启动过程            try {                connection.close();            } catch (JMSException e1) {                e.printStackTrace(System.out);            }            return;        }        // ==========通知其他线程可以获取producer了        ClientStartup.ISSTARTED = true;        synchronized (ClientStartup.class) {            ClientStartup.class.notify();        }        // ==========锁定该线程        synchronized (this) {            try {                this.wait();            } catch (InterruptedException e) {                e.printStackTrace(System.out);            }        }    }    public static void main(String[] args) {        new Thread(new ClientStartup()).start();    }}

3.4、其它说明

  • 安全性考量:在正式环境中使用消息队列中间件服务一定要做相关的安全性设置。包括启用消息队列服务的用户名和密码、启用消息队列服务自带的SSL加密设置。如果您使用的消息队列服务不自带SSL加密,则一定要自己进行加密。幸运的是,如果您使用的是ActiveMQ,那么以上两种安全性要求都可以满足。甚至ActiveMQ还支持为每一个队列单独进行用户名和密码设置。

  • 错误数据的处理:在正式环境中使用消息队列中间件服务一定要假设会发生传输的消息由于各种业务原因导致的消费者处理错误的情况。所以对超出redeliveryCounter重试次数的错误消息一定要转存到另外的“待处理区域”,并在后续进行人工干预。在ActiveMQ中这个“待处理区域”就是死消息队列:ActiveMQ.DLQ。

  • 在产品预算内赋予消息服务中间件最大的可用性:类似于ActiveMQ、RabbitMQ这样的消息队列中间件,其目的并不是一味地追求单位时间内消息数据的吞吐量/并发量的处理能力。它们的功能中涵盖了诸多功能:事务机制、确认机制、重试机制、热备机制等等,都是为了一个更重要的功能目的:保证消息完整可达。所以您和您的团队一定要按照业务特性来确定是否适合使用这样的中间件服务,并且您需要在预算范围内为您的消息服务中间件配置多个服务节点、多个存储单元,以便保证消息队列中间件能够完成它的任务——消息完整可达。

原创粉丝点击