Zookeeper和CuratorFramework实践之:分布式消息队列

之前写过:

  1. Curator-Framework开源Zookeeper快速开发框架介绍
  2. Zookeeper和Curator-Framework实践系列之: 配置管理

本文说的是ZK另一个重要使用场景,消息队列!

场景

一个典型的生产消费者模型,如下图:

WEB点提交要处理的数据,注意是多结点的也就是多个生产者,数量可能比较大。在后台有个处理者也就是消费者,注意前后是分开的,生产者应用本身不做消费,而Curator提供的API好像默认是一起的,一个应用既是生产者又可以做消费。

配置

一切都基于前面的示例Zookeeper和Curator-Framework实践系列之: 配置管理,下面只说不一样的地方。

applicationContext.xml

同样与Zookeeper和Curator-Framework实践系列之: 配置管理相同,只需配置一下zookeeperFactoryBeanlisteners增加或修改成DistributedQueueDemo

<bean id="zookeeperFactoryBean" class="cn.bg.zk.core.ZookeeperFactoryBean" lazy-init="false">
    <property name="zkConnectionString" value="zookeepermaster:2181"/>
    <property name="listeners">
        <list>
            <bean class="cn.bg.zk.queues.DistributedQueueDemo"></bean>
        </list>
    </property>
</bean>

添加一个bean,指定CuratorFramework,充当生产者时需用用它来添加数据到队列

<bean id="mainController" class="cn.bg.controller.MainController">
    <constructor-arg ref="zookeeperFactoryBean" />
</bean>

代码

DistributedQueueDemo.java

分布式队列处理类

package cn.bg.zk.queues;

public class DistributedQueueDemo implements IZKListener{

    //申明两个队列实例
    private DistributedQueue<String> queue1 = null;
    private DistributedQueue<String> queue2 = null;

    //数据系列化转换工具类
    private QueueSerializer<String> serializer = new QueueItemSerializer();

    //消费者处理方法
    private QueueConsumer<String> consumer = new QueueConsumer<String>() {
        @Override
        public void consumeMessage(String message) throws Exception {
            //线程等待5秒,模拟数据处理,以达到数据抢夺的目的
            Thread.sleep(5000);
            //打印出是哪个线程处理了哪些数据
            System.out.println(Thread.currentThread().getId() +  " consume: " + message);
        }

        @Override
        public void stateChanged(CuratorFramework client, ConnectionState newState) {
            System.out.println("new state: " + newState);
        }
    };

    //Spring启动时调用此方法以启动所有队列实例    
    @Override
    public void executor(CuratorFramework client) {
        //实例化所有队列,指定ZK队列数据获取地址,和其它参数
        //由于它们的地址是相同的,都是*/zk_queue_test*,所以Curator会根据它们的空闲状态来分配新的任务,上面通过线程暂停5秒来拉开它们的处理间隔。
        queue1 = QueueBuilder.builder(client, consumer, serializer, "/zk_queue_test").buildQueue();
        queue2 = QueueBuilder.builder(client, consumer, serializer, "/zk_queue_test").buildQueue();

        try {
            //启动所有队列实例,让它们开始工作,注意所有指定的动作只有在调用了queue1.start()方法之后才会被执行,比如queue.put()等。
            //Curator提供了queue.put()方法来往队列里添加数据,但它同时也会处理,但我们不想这样,所以添加的过程我们通过其它的方式来实现。
            queue1.start();
            queue2.start();
            System.out.println("Queues started!");
        } catch (Exception e) {

        }
    }
}

QueueSerializer.java

数据系列化处理工具类

package cn.bg.zk.queues;

public class QueueItemSerializer implements QueueSerializer<String>
{
    @Override
    public byte[] serialize(String item)
    {
        return item.getBytes();
    }

    @Override
    public String deserialize(byte[] bytes)
    {
        return new String(new String(bytes));
    }
}

上面的是消息队列处理的部分,下面开始消息添加,也就是生产者部分:

生产者是一个Controller,也就是通过用户提交数据来做为生产者

MainController.java

package cn.bg.controller;

@Controller
public class MainController {

    private final CuratorFramework zkClient;
    //通过Spring注入CuratorFramework实例
    public MainController(final CuratorFramework zkClient) {
        Assert.notNull(zkClient, "zkClient cannot be null");
        this.zkClient = zkClient;
    }

    //简单的使用传递值来做数据处理的实体
    @RequestMapping("/put/{val}")
    @ResponseBody
    public String put(@PathVariable String val) throws Exception {
        //需要使用特定的格式来添加数据到队列,使用ItemSerializer来做格式化生成byte。
        byte[] bytes = ItemSerializer.serialize(val, new QueueItemSerializer());
        String path = "" ;    

        //创建znode并添加数据
        path = zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT_SEQUENTIAL).forPath("/zk_queue_test/queue-");
        zkClient.setData().forPath(path, bytes);
        return path;
    }

}

ItemSerializer.java

这个类是格式化数据,也就是设置一些znode的属性,并生成byte
此类来自Curator源码的简化版,主要目的是分离Curator Queue来添加队列数据用到。

package cn.bg.zk.queues;

public class ItemSerializer {
    private static final int VERSION = 0x00010001;

    private static final byte ITEM_OPCODE = 0x01;
    private static final byte EOF_OPCODE = 0x02;

    private static final int INITIAL_BUFFER_SIZE = 0x1000;

    public static <T> byte[] serialize(T item, QueueSerializer<T> serializer) throws Exception {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream(INITIAL_BUFFER_SIZE);
        DataOutputStream out = new DataOutputStream(bytes);
        out.writeInt(VERSION);

        byte[] itemBytes = serializer.serialize(item);
        out.writeByte(ITEM_OPCODE);
        out.writeInt(itemBytes.length);
        if (itemBytes.length > 0) {
            out.write(itemBytes);
        }

        out.writeByte(EOF_OPCODE);
        out.close();

        return bytes.toByteArray();
    }
}

运行

启动应用后所有队列都已处理待命状态,理论上只要在ZK目录/zk_queue_test/添加数据就会被处理掉,只是它有固定的添加格式。

通过访问/put/{?}等这样路径数据{?}就会被添加到队列并处理,所以可以刷多条数据到队列来观察队列的处理状态,基本的输出应该是这样的:

17 consume: 111
18 consume: 222
17 consume: 111
18 consume: 222
17 consume: 111
17 consume: 111

整个过程基本完成,经测试运行状态良好,Curator自己维护与ZK集群的连接,本人通过JMX将应用与ZK的连接强制断开后Curator主动识别并重新连接,基本不用担心一些基础问题上处理,可以专心解决我们的业务需要。

原文地址:https://www.cnblogs.com/xguo/p/3137948.html