Storm入門之第8章事務性拓撲

boxti發表於2017-05-02

Storm入門之第8章事務性拓撲

本文翻譯自《Getting Started With Storm》譯者:吳京潤    編輯:郭蕾 方騰飛

正如書中之前所提到的,使用Storm程式設計,可以通過呼叫ack和fail方法來確保一條訊息的處理成功或失敗。不過當元組被重發時,會發生什麼呢?你又該如何砍不會重複計算?

 

Storm0.7.0實現了一個新特性——事務性拓撲,這一特性使訊息在語義上確保你可以安全的方式重發訊息,並保證它們只會被處理一次。在不支援事務性拓撲的情況下,你無法在準確性,可擴充套件性,以空錯性上得到保證的前提下完成計算。

 

NOTE:事務性拓撲是一個構建於標準Storm spoutbolt之上的抽象概念。

設計

在事務性拓撲中,Storm以並行和順序處理混合的方式處理元組。spout並行分批建立供bolt處理的元組(譯者注:下文將這種分批建立、分批處理的元組稱做批次)。其中一些bolt作為提交者以嚴格有序的方式提交處理過的批次。這意味著如果你有每批五個元組的兩個批次,將有兩個元組被bolt並行處理,但是直到提交者成功提交了第一個元組之後,才會提交第二個元組。 NOTE: 使用事務性拓撲時,資料來源要能夠重發批次,有時候甚至要重複多次。因此確認你的資料來源——你連線到的那個spout——具備這個能力。 這個過程可以被描述為兩個階段: 處理階段 純並行階段,許多批次同時處理。 提交階段 嚴格有序階段,直到批次一成功提交之後,才會提交批次二。 這兩個階段合起來稱為一個Storm事務。 NOTE: Storm使用zookeeper儲存事務後設資料,預設情況下就是拓撲使用的那個zookeeper。你可以修改以下兩個配置引數鍵指定其它的zookeeper——transactional.zookeeper.servers和transactional.zookeeper.port。

事務實踐

下面我們要建立一個Twitter分析工具來了解事務的工作方式。我們從一個Redis資料庫讀取tweets,通過幾個bolt處理它們,最後把結果儲存在另一個Redis資料庫的列表中。處理結果就是所有話題和它們的在tweets中出現的次數列表,所有使用者和他們在tweets中出現的次數列表,還有一個包含發起話題和頻率的使用者列表。 這個工具的拓撲見圖8-1。拓撲概覽  圖8-1 拓撲概覽

正如你看到的,TweetsTransactionalSpout會連線你的tweet資料庫並向拓撲分發批次。UserSplitterBoltHashTagSplitterBolt兩個bolt,從spout接收元組。UserSplitterBolt解析tweets並查詢使用者——以@開頭的單詞——然後把這些單詞分發到名為users的自定義資料流組。HashtagSplitterBolt從tweet查詢#開頭的單詞,並把它們分發到名為hashtags的自定義資料流組。第三個boltUserHashtagJoinBolt,接收前面提到的兩個資料流組,並計算具名使用者的一條tweet內的話題數量。為了計數並分發計算結果,這是個BaseBatchBolt(稍後有更多介紹)。

最後一個bolt——RedisCommitterBolt——接收以上三個bolt的資料流組。它為每樣東西計數,並在對一個批次完成處理時,把所有結果儲存到redis。這是一種特殊的bolt,叫做提交者,在本章後面做更多講解。

TransactionalTopologyBuilder構建拓撲,程式碼如下:

TransactionalTopologyBuilder builder=
    new TransactionalTopologyBuilder("test", "spout", new TweetsTransactionalSpout());

builder.setBolt("users-splitter", new UserSplitterBolt(), 4).shuffleGrouping("spout");
buildeer.setBolt("hashtag-splitter", new HashtagSplitterBolt(), 4).shuffleGrouping("spout");

builder.setBolt("users-hashtag-manager", new UserHashtagJoinBolt(), r)
       .fieldsGrouping("users-splitter", "users", new Fields("tweet_id"))
       .fieldsGrouping("hashtag-splitter", "hashtags", new Fields("tweet_id"));

builder.setBolt("redis-commiter", new RedisCommiterBolt())
       .globalGrouping("users-splitter", "users")
       .globalGrouping("hashtag-splitter", "hashtags")
       .globalGrouping("user-hashtag-merger");

接下來就看看如何在一個事務性拓撲中實現spout

Spout

一個事務性拓撲的spout與標準spout完全不同。

public class TweetsTransactionalSpout extends BaseTransactionalSpout<TransactionMetadata>{

正如你在這個類定義中看到的,TweetsTransactionalSpout繼承了帶範型的BaseTransactionalSpout。指定的範型型別的物件是事務後設資料集合。它將在後面的程式碼中用於從資料來源分發批次。

在這個例子中,TransactionMetadata定義如下:

public class TransactionMetadata implements Serializable {
    private static final long serialVersionUID = 1L;
    long from;
    int quantity;

    public TransactionMetadata(long from, int quantity) {
        this.from = from;
        this.quantity = quantity;
    }
}

該類的物件維護著兩個屬性fromquantity,它們用來生成批次。

spout的最後需要實現下面的三個方法:

@Override
public ITransactionalSpout.Coordinator<TransactionMetadata> getCoordinator(
       Map conf, TopologyContext context) {
    return new TweetsTransactionalSpoutCoordinator();
}

@Override
public backtype.storm.transactional.ITransactionalSpout.Emitter<TransactionMetadata> getEmitter(Map conf, TopologyContext contest) {
    return new TweetsTransactionalSpoutEmitter();
}

@Override
public void declareOutputFields(OuputFieldsDeclarer declarer) {
    declarer.declare(new Fields("txid", "tweet_id", "tweet"));
}

getCoordinator方法,告訴Storm用來協調生成批次的類。getEmitter,負責讀取批次並把它們分發到拓撲中的資料流組。最後,就像之前做過的,需要宣告要分發的域。

RQ類
為了讓例子簡單點,我們決定用一個類封裝所有對Redis的操作。

public class RQ {
    public static final String NEXT_READ = "NEXT_READ";
    public static final String NEXT_WRITE = "NEXT_WRITE";

    Jedis jedis;

    public RQ() {
        jedis = new Jedis("localhost");
    }

    public long getavailableToRead(long current) {
        return getNextWrite() - current;
    }

    public long getNextRead() {
        String sNextRead = jedis.get(NEXT_READ);
        if(sNextRead == null) {
            return 1;
        }
        return Long.valueOf(sNextRead);
    }

    public long getNextWrite() {
        return Long.valueOf(jedis.get(NEXT_WRITE));
    }

    public void close() {
        jedis.disconnect();
    }

    public void setNextRead(long nextRead) {
        jedis.set(NEXT_READ, ""+nextRead);
    }

    public List<String> getMessages(long from, int quantity) {
        String[] keys = new String[quantity];
        for (int i = 0; i < quantity; i++) {
            keys[i] = ""+(i+from);
        }
        return jedis.mget(keys);
    }
}

仔細閱讀每個方法,確保自己理解了它們的用處。

協調者Coordinator
下面是本例的協調者實現。

public static class TweetsTransactionalSpoutCoordinator implements ITransactionalSpout.Coordinator<TransactionMetadata> {
    TransactionMetadata lastTransactionMetadata;
    RQ rq = new RQ();
    long nextRead = 0;

    public TweetsTransactionalSpoutCoordinator() {
        nextRead = rq.getNextRead();
    }

    @Override
    public TransactionMetadata initializeTransaction(BigInteger txid, TransactionMetadata prevMetadata) {
        long quantity = rq.getAvailableToRead(nextRead);
        quantity = quantity > MAX_TRANSACTION_SIZE ? MAX_TRANSACTION_SIZE : quantity;
        TransactionMetadata ret = new TransactionMetadata(nextRead, (int)quantity);
        nextRead += quantity;
        return ret;
    }

    @Override
    public boolean isReady() {
        return rq.getAvailableToRead(nextRead) > 0;
    }

    @Override
    public void close() {
        rq.close();
    }
}

值得一提的是,在整個拓撲中只會有一個提交者例項。建立提交者例項時,它會從redis讀取一個從1開始的序列號,這個序列號標識要讀取的tweet下一條。

第一個方法是isReady。在initializeTransaction之前呼叫它確認資料來源已就緒並可讀取。此方法應當相應的返回truefalse。在此例中,讀取tweets數量並與已讀數量比較。它們之間的不同就在於可讀tweets數。如果它大於0,就意味著還有tweets未讀。

最後,執行initializeTransaction。正如你看到的,它接收txidprevMetadata作為引數。第一個引數是Storm生成的事務ID,作為批次的惟一性標識。prevMetadata是協調器生成的前一個事務後設資料物件。

在這個例子中,首先確認有多少tweets可讀。只要確認了這一點,就建立一個TransactionMetadata物件,標識讀取的第一個tweet(譯者注:物件屬性from),以及讀取的tweets數量(譯者注:物件屬性quantity)。

後設資料物件一經返回,Storm把它跟txid一起儲存在zookeeper。這樣就確保了一旦發生故障,Storm可以利用分發器(譯者注:Emitter,見下文)重新傳送批次。

Emitter

建立事務性spout的最後一步是實現分發器(Emitter)。實現如下:

public static class TweetsTransactionalSpoutEmitter implements ITransactionalSpout.Emitter<TransactionMetadata> {

</pre>
<pre>    RQ rq = new RQ();</pre>
<pre>    public TweetsTransactionalSpoutEmitter() {}</pre>
<pre>    @Override
    public void emitBatch(TransactionAttempt tx, TransactionMetadata coordinatorMeta, BatchOutputCollector collector) {
        rq.setNextRead(coordinatorMeta.from+coordinatorMeta.quantity);
        List<String> messages = rq.getMessages(coordinatorMeta.from, <span style="font-family: Georgia, `Times New Roman`, `Bitstream Charter`, Times, serif; font-size: 13px; line-height: 19px;">coordinatorMeta.quantity);
</span>        long tweetId = coordinatorMeta.from;
        for (String message : messages) {
            collector.emit(new Values(tx, ""+tweetId, message));
            tweetId++;
        }
    }

    @Override
    public void cleanupBefore(BigInteger txid) {}

    @Override
    public void close() {
        rq.close();
    }</pre>
<pre>
}

分發器從資料來源讀取資料並從資料流組傳送資料。分發器應當問題能夠為相同的事務id和事務後設資料傳送相同的批次。這樣,如果在處理批次的過程中發生了故障,Storm就能夠利用分發器重複相同的事務id和事務後設資料,並確保批次已經重複過了。Storm會在TransactionAttempt物件裡為嘗試次數增加計數(譯者注:attempt id)。這樣就能知道批次已經重複過了。

在這裡emitBatch是個重要方法。在這個方法中,使用傳入的後設資料物件從redis得到tweets,同時增加redis維持的已讀tweets數。當然它還會把讀到的tweets分發到拓撲。

Bolts

首先看一下這個拓撲中的標準bolt

public class UserSplitterBolt implements IBasicBolt{
    private static final long serialVersionUID = 1L;

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declareStream("users", new Fields("txid","tweet_id","user"));
    }

    @Override
    public Map<String, Object> getComponentConfiguration() {
        return null;
    }

    @Override
    public void prepare(Map stormConf, TopologyContext context) {}

    @Override
    public void execute(Tuple input, BasicOutputCollector collector) {
        String tweet = input.getStringByField("tweet");
        String tweetId = input.getStringByField("tweet_id");
        StringTokenizer strTok = new StringTokenizer(tweet, " ");
        HashSet<String> users = new HashSet<String>();

        while(strTok.hasMoreTokens()) {
            String user = strTok.nextToken();

            //確保這是個真實的使用者,並且在這個tweet中沒有重複
            if(user.startsWith("@") && !users.contains(user)) {
                collector.emit("users", new Values(tx, tweetId, user));
                users.add(user);
            }
        }
    }

    @Override
    public void cleanup(){}
}

正如本章前面提到的,UserSplitterBolt接收元組,解析tweet文字,分發@開頭的單詞————tweeter使用者。HashtagSplitterBolt的實現也非常相似。

public class HashtagSplitterBolt implements IBasicBolt{
    private static final long serialVersionUID = 1L;

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declareStream("hashtags", new Fields("txid","tweet_id","hashtag"));
    }

    @Override
    public Map<String, Object> getComponentConfiguration() {
        return null;
    }

    @Override
    public void prepare(Map stormConf, TopologyContext context) {}

    @Oerride
    public void execute(Tuple input, BasicOutputCollector collector) {
        String tweet = input.getStringByField("tweet");
        String tweetId = input.getStringByField("tweet_id");
        StringTokenizer strTok = new StringTokenizer(tweet, " ");
        TransactionAttempt tx = (TransactionAttempt)input.getValueByField("txid");
        HashSet<String> words = new HashSet<String>();

        while(strTok.hasMoreTokens()) {
            String word = strTok.nextToken();

            if(word.startsWith("#") && !words.contains(word)){
                collector.emit("hashtags", new Values(tx, tweetId, word));
                words.add(word);
            }
        }
    }

    @Override
    public void cleanup(){}
}

現在看看UserHashTagJoinBolt的實現。首先要注意的是它是一個BaseBatchBolt。這意味著,execute方法會操作接收到的元組,但是不會分發新的元組。批次完成時,Storm會呼叫finishBatch方法。

public void execute(Tuple tuple) {
    String source = tuple.getSourceStreamId();
    String tweetId = tuple.getStringByField("tweet_id");

    if("hashtags".equals(source)) {
        String hashtag = tuple.getStringByField("hashtag");
        add(tweetHashtags, tweetId, hashtag);
    } else if("users".equals(source)) {
        String user = tuple.getStringByField("user");
        add(userTweets, user, tweetId);
    }
}

既然要結合tweet中提到的使用者為出現的所有話題計數,就需要加入前面的bolts建立的兩個資料流組。這件事要以批次為單位程式,在批次處理完成時,呼叫finishBatch方法。

@Override
public void finishBatch() {
    for(String user:userTweets.keySet()){
        Set<String> tweets = getUserTweets(user);
        HashMap<String, Integer> hashtagsCounter = new HashMap<String, Integer>();
        for(String tweet:tweets){
            Set<String> hashtags=getTweetHashtags(tweet);
            if(hashtags!=null){
                for(String hashtag:hashtags){
                    Integer count=hashtagsCounter.get(hashtag);
                    if(count==null){count=0;}
                    count++;
                    hashtagsCounter.put(hashtag,count);
                }
            }
        }
        for(String hashtag:hashtagsCounter.keySet()){
            int count=hashtagsCounter.get(hashtag);
            collector.emit(new Values(id,user,hashtag,count));
        }
    }
}

這個方法計算每對使用者-話題出現的次數,併為之生成和分發元組。

你可以在GitHub上找到並下載完整程式碼。(譯者注:https://github.com/storm-book/examples-ch08-transactional-topologies這個倉庫裡沒有程式碼,誰知道哪裡有程式碼麻煩說一聲。)

提交者bolts

我們已經學習了,批次通過協調器和分發器怎樣在拓撲中傳遞。在拓撲中,這些批次中的元組以並行的,沒有特定次序的方式處理。

協調者bolts是一類特殊的批處理bolts,它們實現了IComh mitter或者通過TransactionalTopologyBuilder呼叫setCommiterBolt設定了提交者bolt。它們與其它的批處理bolts最大的不同在於,提交者boltsfinishBatch方法在提交就緒時執行。這一點發生在之前所有事務都已成功提交之後。另外,finishBatch方法是順序執行的。因此如果同時有事務ID1和事務ID2兩個事務同時執行,只有在ID1沒有任何差錯的執行了finishBatch方法之後,ID2才會執行該方法。

下面是這個類的實現

public class RedisCommiterCommiterBolt extends BaseTransactionalBolt implements ICommitter {
    public static final String LAST_COMMITED_TRANSACTION_FIELD = "LAST_COMMIT";
    TransactionAttempt id;
    BatchOutputCollector collector;
    Jedis jedis;

    @Override
    public void prepare(Map conf, TopologyContext context,
                        BatchOutputCollector collector, TransactionAttempt id) {
        this.id = id;
        this.collector = collector;
        this.jedis = new Jedis("localhost");
    }

    HashMap<String, Long> hashtags = new HashMap<String,Long>();
    HashMap<String, Long> users = new HashMap<String, Long>();
    HashMap<String, Long> usersHashtags = new HashMap<String, Long>();

    private void count(HashMap<String, Long> map, String key, int count) {
        Long value = map.get(key);
        if(value == null){value = (long)0;}
        value += count;
        map.put(key,value);
    }

    @Override
    public void execute(Tuple tuple) {
        String origin = tuple. getSourceComponent();
        if("sers-splitter".equals(origin)) {
            String user = tuple.getStringByField("user");
            count(users, user, 1);
        } else if("hashtag-splitter".equals(origin)) {
            String hashtag = tuple.getStringByField("hashtag");
            count(hashtags, hashtag, 1);
        } else if("user-hashtag-merger".quals(origin)) {
            String hashtag = tuple.getStringByField("hashtag");
            String user = tuple.getStringByField("user");
            String key = user + ":" + hashtag;
            Integer count = tuple.getIntegerByField("count");
            count(usersHashtags, key, count);
        }
    }

    @Override
    public void finishBatch() {
        String lastCommitedTransaction = jedis.get(LAST_COMMITED_TRANSACTION_FIELD);
        String currentTransaction = ""+id.getTransactionId();

        if(currentTransaction.equals(lastCommitedTransaction)) {return;}

        Transaction multi = jedis.multi();

        multi.set(LAST_COMMITED_TRANSACTION_FIELD, currentTransaction);

        Set<String> keys = hashtags.keySet();
        for (String hashtag : keys) {
            Long count = hashtags.get(hashtag);
            multi.hincrBy("hashtags", hashtag, count);
        }

        keys = users.keySet();
        for (String user : keys) {
            Long count =users.get(user);
            multi.hincrBy("users",user,count);
        }

        keys = usersHashtags.keySet();
        for (String key : keys) {
            Long count = usersHashtags.get(key);
            multi.hincrBy("users_hashtags", key, count);
        }

        multi.exec();
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {}
}

這個實現很簡單,但是在finishBatch有一個細節。

...
multi.set(LAST_COMMITED_TRANSACTION_FIELD, currentTransaction);
...

在這裡向資料庫儲存提交的最後一個事務ID。為什麼要這樣做?記住,如果事務失敗了,Storm將會盡可能多的重複必要的次數。如果你不確定已經處理了這個事務,你就會多算,事務拓撲也就沒有用了。所以請記住:儲存最後提交的事務ID,並在提交前檢查。

分割槽的事務Spouts
對一個spout來說,從一個分割槽集合中讀取批次是很普通的。接著這個例子,你可能有很多redis資料庫,而tweets可能會分別儲存在這些redis資料庫裡。通過實現IPartitionedTransactionalSpout,Storm提供了一些工具用來管理每個分割槽的狀態並保證重播的能力。
下面我們修改TweetsTransactionalSpout,使它可以處理資料分割槽。
首先,繼承BasePartitionedTransactionalSpout,它實現了IPartitionedTransactionalSpout

public class TweetsPartitionedTransactionalSpout extends
       BasePartitionedTransactionalSpout<TransactionMetadata> {
...
}

然後告訴Storm誰是你的協調器。

public static class TweetsPartitionedTransactionalCoordinator implements Coordinator {
    @Override
    public int numPartitions() {
        return 4;
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void close() {}
}

在這個例子裡,協調器很簡單。numPartitions方法,告訴Storm一共有多少分割槽。而且你要注意,不要返回任何後設資料。對於IPartitionedTransactionalSpout,後設資料由分發器直接管理。
下面是分發器的實現:

public static class TweetsPartitionedTransactionalEmitter
       implements Emitter<TransactionMetadata> {
    PartitionedRQ rq = new ParttionedRQ();

    @Override
    public TransactionMetadata emitPartitionBatchNew(TransactionAttempt tx,
            BatchOutputCollector collector, int partition,
            TransactionMetadata lastPartitioonMeta) {
        long nextRead;

        if(lastPartitionMeta == null) {
            nextRead = rq.getNextRead(partition);
        }else{
            nextRead = lastPartitionMeta.from + lastPartitionMeta.quantity;
            rq.setNextRead(partition, nextRead); //移動遊標
        }

        long quantity = rq.getAvailableToRead(partition, nextRead);
        quantity = quantity > MAX_TRANSACTION_SIZE ? MAX_TRANSACTION_SIZE : quantity;
        TransactionMetadata metadata = new TransactionMetadata(nextRead, (int)quantity);

        emitPartitionBatch(tx, collector, partition, metadata);
        return metadata;
    }

    @Override
    public void emitPartitionBatch(TransactionAttempt tx, BatchOutputCollector collector,
            int partition, TransactionMetadata partitionMeta) {
        if(partitionMeta.quantity <= 0){
            return;
        }

        List<String> messages = rq.getMessages(partition, partitionMeta.from,
               partitionMeta.quantity);

        long tweetId = partitionMeta.from;
        for (String msg : messages) {
            collector.emit(new Values(tx, ""+tweetId, msg));
            tweetId++;
        }
    }

    @Override
    public void close() {}
}

這裡有兩個重要的方法,emitPartitionBatchNew,和emitPartitionBatch。對於emitPartitionBatchNew,從Storm接收分割槽引數,該引數決定應該從哪個分割槽讀取批次。在這個方法中,決定獲取哪些tweets,生成相應的後設資料物件,呼叫emitPartitionBatch,返回後設資料物件,並且後設資料物件會在方法返回時立即儲存到zookeeper。
Storm會為每一個分割槽傳送相同的事務ID,表示一個事務貫穿了所有資料分割槽。通過emitPartitionBatch讀取分割槽中的tweets,並向拓撲分發批次。如果批次處理失敗了,Storm將會呼叫emitPartitionBatch利用儲存下來的後設資料重複這個批次。

NOTE: 完整的原始碼請見:https://github.com/storm-book/examples-ch08-transactional-topologies(譯者注:原文如此,實際上這個倉庫裡什麼也沒有)

模糊的事務性拓撲

到目前為止,你可能已經學會了如何讓擁有相同事務ID的批次在出錯時重播。但是在有些場景下這樣做可能就不太合適了。然後會發生什麼呢?

事實證明,你仍然可以實現在語義上精確的事務,不過這需要更多的開發工作,你要記錄由Storm重複的事務之前的狀態。既然能在不同時刻為相同的事務ID得到不同的元組,你就需要把事務重置到之前的狀態,並從那裡繼續。

比如說,如果你為收到的所有tweets計數,你已數到5,而最後的事務ID是321,這時你多數了8個。你要維護以下三個值——previousCount=5,currentCount=13,以及lastTransactionId=321。假設事物ID321又發分了一次,而你又得到了4個元組,而不是之前的8個,提交器會探測到這是相同的事務ID,它將會把結果重置到previousCount的值5,並在此基礎上加4,然後更新currentCount為9。

另外,在之前的一個事務被取消時,每個並行處理的事務都要被取消。這是為了確保你沒有丟失任何資料。

你的spout可以實現IOpaquePartitionedTransactionalSpout,而且正如你看到的,協調器和分發器也很簡單。

public static class TweetsOpaquePartitionedTransactionalSpoutCoordinator implements IOpaquePartitionedTransactionalSpout.Coordinator {
    @Override
    public boolean isReady() {
        return true;
    }
}

public static class TweetsOpaquePartitionedTransactionalSpoutEmitter
       implements IOpaquePartitionedTransactionalSpout.Emitter<TransactionMetadata> {
    PartitionedRQ rq  = new PartitionedRQ();

    @Override
    public TransactionMetadata emitPartitionBatch(TransactionAttempt tx,
           BatchOutputCollector collector, int partion,
           TransactionMetadata lastPartitonMeta) {
        long nextRead;

        if(lastPartitionMeta == null) {
            nextRead = rq.getNextRead(partition);
        }else{
            nextRead = lastPartitionMeta.from + lastPartitionMeta.quantity;
            rq.setNextRead(partition, nextRead);//移動遊標
        }

        long quantity = rq.getAvailabletoRead(partition, nextRead);
        quantity = quantity > MAX_TRANSACTION_SIZE ? MAX_TRANSACTION_SIZE : quantity;
        TransactionMetadata metadata = new TransactionMetadata(nextRead, (int)quantity);
        emitMessages(tx, collector, partition, metadata);
        return metadata;
    }

    private void emitMessage(TransactionAttempt tx, BatchOutputCollector collector,
                 int partition, TransactionMetadata partitionMeta) {
        if(partitionMeta.quantity <= 0){return;}

        List<String> messages = rq.getMessages(partition, partitionMeta.from, partitionMeta.quantity);
        long tweetId = partitionMeta.from;
        for(String msg : messages) {
            collector.emit(new Values(tx, ""+tweetId, msg));
            tweetId++;
        }
    }

    @Override
    public int numPartitions() {
        return 4;
    }

    @Override
    public void close() {}
}

最有趣的方法是emitPartitionBatch,它獲取之前提交的後設資料。你要用它生成批次。這個批次不需要與之前的那個一致,你可能根本無法建立完全一樣的批次。剩餘的工作由提交器bolts藉助之前的狀態完成。

文章轉自 併發程式設計網-ifeve.com


相關文章