Zookeeper和Curator-Framework實踐之:分散式訊息佇列

五柳-先生發表於2015-07-21

之前寫過:

  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主動識別並重新連線,基本不用擔心一些基礎問題上處理,可以專心解決我們的業務需要。


轉載: http://www.cnblogs.com/xguo/archive/2013/06/15/3137948.html

相關文章