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。


接下来就看看如何在一个事务性拓扑中实现spout

Spout

一个事务性拓扑的spout与标准spout完全不同。

public class TestTransactionalSpout extends BaseTransactionalSpout{

正如你在这个类定义中看到的,TestTransactionalSpout继承了带范型的BaseTransactionalSpout。指定的范型类型的对象是事务元数据集合。


协调者Coordinator
下面是本例的协调者实现。

public static class TestTransactionalSpoutCoordinator implements ITransactionalSpout.Coordinator {
    TransactionMetadata lastTransactionMetadata;
    

    public TestTransactionalSpoutCoordinator () {
        
    }

    @Override
    public TransactionMetadata initializeTransaction(BigInteger txid, TransactionMetadata prevMetadata) {
        //处理代码
    }

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

    @Override
    public void close() {
        
    }
}

值得一提的是,在整个拓扑中只会有一个提交者实例

第一个方法是isReady。在initializeTransaction之前调用它确认数据源已就绪并可读取。此方法应当相应的返回truefalse

最后,执行initializeTransaction。正如你看到的,它接收txidprevMetadata作为参数。第一个参数是Storm生成的事务ID,作为批次的惟一性标识。prevMetadata是协调器生成的前一个事务元数据对象。

在这个例子中,首先确认有多少tweets可读。只要确认了这一点,就创建一个TransactionMetadata对象。元数据对象一经返回,Storm把它跟txid一起保存在zookeeper。这样就确保了一旦发生故障,Storm可以利用分发器(译者注:Emitter,见下文)重新发送批次。

Emitter

创建事务性spout的最后一步是实现分发器(Emitter)。实现如下:

public static class TestTransactionalSpoutEmitter implements ITransactionalSpout.Emitter {

   public TestTransactionalSpoutEmitter() {}
   @Override
    public void emitBatch(TransactionAttempt tx, TransactionMetadata coordinatorMeta, BatchOutputCollector collector) {
        //处理代码
        /**
            分发器从数据源读取数据并从数据流组发送数据。分发器应当问题能够为相同的事务id和事务元数据发送相同的批次。这样,如果在处理批次的过程中发生了故障,Storm就能够利用分发器重复相同的事务id和事务元数据,并确保批次已经重复过了.
          */
    }

    @Override
    public void cleanupBefore(BigInteger txid) {}

    @Override
    public void close() {
    }
}


在这里emitBatch是个重要方法。

Bolts

首先看一下这个拓扑中的标准bolt

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

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

    @Override
    public Map getComponentConfiguration() {
        return null;
    }

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

    @Override
    public void execute(Tuple input, BasicOutputCollector collector) {
        //业务代码
    }

    @Override
    public void cleanup(){}
}

TestSplitterBolt接收元组。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 getComponentConfiguration() {
        return null;
    }

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

    @Oerride
    public void execute(Tuple input, BasicOutputCollector collector) {
        //业务代码
    }

    @Override
    public void cleanup(){}
}

现在看看TestHashTagJoinBolt的实现。首先要注意的是它是一个BaseBatchBolt。这意味着,execute方法会操作接收到的元组,但是不会分发新的元组。批次完成时,Storm会调用finishBatch方法。

public void execute(Tuple tuple) {
   //业务代码
}

在批次处理完成时,调用finishBatch方法。

@Override
public void finishBatch() {
    //后续处理代码
}


提交者bolts

我们已经学习了,批次通过协调器和分发器怎样在拓扑中传递。在拓扑中,这些批次中的元组以并行的,没有特定次序的方式处理。


在这里向数据库保存提交的最后一个事务ID。为什么要这样做?记住,如果事务失败了,Storm将会尽可能多的重复必要的次数。如果你不确定已经处理了这个事务,你就会多算,事务拓扑也就没有用了。所以请记住:保存最后提交的事务ID,并在提交前检查。

分区的事务Spouts
对一个spout来说,从一个分区集合中读取批次是很普通的。通过实现IPartitionedTransactionalSpout,Storm提供了一些工具用来管理每个分区的状态并保证重播的能力。
下面我们修改TestTransactionalSpout,使它可以处理数据分区。
首先,继承BasePartitionedTransactionalSpout,它实现了IPartitionedTransactionalSpout

public class TestPartitionedTransactionalSpout extends
       BasePartitionedTransactionalSpout {
...
}

然后告诉Storm谁是你的协调器。

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

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

    @Override
    public void close() {}
}

在这个例子里,协调器很简单。numPartitions方法,告诉Storm一共有多少分区。而且你要注意,不要返回任何元数据。对于IPartitionedTransactionalSpout,元数据由分发器直接管理。
下面是分发器的实现:

public static class TestPartitionedTransactionalEmitter
       implements Emitter {

    @Override
    public TransactionMetadata emitPartitionBatchNew(TransactionAttempt tx,
            BatchOutputCollector collector, int partition,
            TransactionMetadata lastPartitioonMeta) {
            //业务处理代码
    }

    @Override
    public void emitPartitionBatch(TransactionAttempt tx, BatchOutputCollector collector,
            int partition, TransactionMetadata partitionMeta) {
       //业务处理代码
    }

    @Override
    public void close() {}
}

这里有两个重要的方法,emitPartitionBatchNew,和emitPartitionBatch。对于emitPartitionBatchNew,从Storm接收分区参数,该参数决定应该从哪个分区读取批次。在这个方法中,决定获取哪些数据,生成相应的元数据对象,调用emitPartitionBatch,返回元数据对象,并且元数据对象会在方法返回时立即保存到zookeeper。
Storm会为每一个分区发送相同的事务ID,表示一个事务贯穿了所有数据分区。通过emitPartitionBatch读取分区中的数据,并向拓扑分发批次。如果批次处理失败了,Storm将会调用emitPartitionBatch利用保存下来的元数据重复这个批次。


模糊的事务性拓扑

到目前为止,你可能已经学会了如何让拥有相同事务ID的批次在出错时重播。但是在有些场景下这样做可能就不太合适了。然后会发生什么呢?

事实证明,你仍然可以实现在语义上精确的事务,不过这需要更多的开发工作,你要记录由Storm重复的事务之前的状态。既然能在不同时刻为相同的事务ID得到不同的元组,你就需要把事务重置到之前的状态,并从那里继续。另外,在之前的一个事务被取消时,每个并行处理的事务都要被取消。这是为了确保你没有丢失任何数据。

你的spout可以实现IOpaquePartitionedTransactionalSpout,而且正如你看到的,协调器和分发器也很简单。

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

public static class TestOpaquePartitionedTransactionalSpoutEmitter
       implements IOpaquePartitionedTransactionalSpout.Emitter {
    
    @Override
    public TransactionMetadata emitPartitionBatch(TransactionAttempt tx,
           BatchOutputCollector collector, int partion,
           TransactionMetadata lastPartitonMeta) {
           //处理代码
        return null;
    }

    private void emitMessage(TransactionAttempt tx, BatchOutputCollector collector,
                 int partition, TransactionMetadata partitionMeta) {
        //处理代码
    }

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

    @Override
    public void close() {}
}

最有趣的方法是emitPartitionBatch,它获取之前提交的元数据。你要用它生成批次。这个批次不需要与之前的那个一致,你可能根本无法创建完全一样的批次。剩余的工作由提交器bolts借助之前的状态完成。