trident State应用指南

  • 一State基础示例
    • 1主类
    • 2Aggregator的用法
      • 1Aggregator接口
      • 2init方法
      • 3aggregate方法
      • 4complete方法
    • 3state的用法
      • 1拓扑定义
      • 2工厂类NameSumStateFactory
      • 3更新类NameSumUpdater
      • 4状态类NameSumState
    • 4state应用思路总结

Trident及State的原理请见另一篇文章:http://blog.csdn.net/lujinhong2/article/details/47132305

一、State基础示例

trident通过spout的事务性与state的事务处理,保证了恰好一次的语义。这里介绍了如何使用state。

完整代码请见 https://github.com/lujinhong/tridentdemo

1、主类

主类定义了拓扑的整体逻辑,这个拓扑通过一个固定的spout循环产生数据,然后统计消息中每个名字出现的次数。

拓扑中先将消息中的内容提取出来成name, age, title, tel4个field,然后通过project只保留name字段供统计,接着按照name分区后,为每个分区进行聚合,最后将聚合结果通过state写入map中。

    storm.trident.Stream Origin_Stream = topology
            .newStream("tridentStateDemoId", spout)
            .parallelismHint(3)
            .shuffle()
            .parallelismHint(3)
            .each(new Fields("msg"), new Splitfield(),
                    new Fields("name", "age", "title", "tel"))
            .parallelismHint(3)
            .project(new Fields("name")) //其实没什么必要,上面就不需要发射BCD字段,但可以示范一下project的用法
            .parallelismHint(3)
            .partitionBy(new Fields("name"));   //根据name的值作分区

    Origin_Stream.partitionAggregate(new Fields("name"), new NameCountAggregator(),
            new Fields("nameSumKey", "nameSumValue")).partitionPersist(
            new NameSumStateFactory(), new Fields("nameSumKey", "nameSumValue"),
            new NameSumUpdater());

2、Aggregator的用法

这里涉及了一些trident常用的API,但project等相对容易理解,这里只介绍partitionAggregate的用法。

再看看上面代码中对partitionAggregate的使用:

Origin_Stream.partitionAggregate(new Fields("name"), new NameCountAggregator(),
            new Fields("nameSumKey", "nameSumValue"))

第一,三个参数分别表示输入流的名称与输出流的名称。中间的NameCountAggregator是一个Aggregator的对象,它定义了如何对输入流进行聚合。我们看一下它的代码:

public class NameCountAggregator implements Aggregator<Map<String, Integer>> {
    private static final long serialVersionUID = -5141558506999420908L;

    @Override
    public Map<String, Integer> init(Object batchId,TridentCollector collector) {
        return new HashMap<String, Integer>();
    }

    //判断某个名字是否已经存在于map中,若无,则put,若有,则递增
    @Override
    public void aggregate(Map<String, Integer> map,TridentTuple tuple, TridentCollector collector) {
        String key=tuple.getString(0);
        if(map.containsKey(key)){
            Integer tmp=map.get(key);
            map.put(key, ++tmp);

        }else{
            map.put(key, 1);
        }
    }

    //将聚合后的结果emit出去
    @Override
    public void complete(Map<String, Integer> map,TridentCollector collector) {
        if (map.size() > 0) {

            for(Entry<String, Integer> entry : map.entrySet()){
                System.out.println("Thread.id="+Thread.currentThread().getId()+"|"+entry.getKey()+"|"+entry.getValue());
                collector.emit(new Values(entry.getKey(),entry.getValue()));
            }

            map.clear();
        } 
    }

    @Override
    public void prepare(Map conf, TridentOperationContext context) {

    }

    @Override
    public void cleanup() {

    }

}

(1)Aggregator接口

它实现了Aggregator接口,这个接口有3个方法:

public interface Aggregator<T> extends Operation {
    T init(Object batchId, TridentCollector collector);
    void aggregate(T val, TridentTuple tuple, TridentCollector collector);
    void complete(T val, TridentCollector collector);
}

init方法:在处理batch之前被调用。init的返回值是一个表示聚合状态的对象,该对象会被传递到aggregate和complete方法。
aggregate方法:为每个在batch分区的输入元组所调用,更新状态
complete方法:当batch分区的所有元组已经被aggregate方法处理完后被调用。

除了实现Aggregator接口,还可以实现ReducerAggregator或者CombinerAggregator,它们使用更方便。详见《从零开始学storm》或者官方文档
https://storm.apache.org/documentation/Trident-API-Overview.html

下面我们看一下这3个方法的实现。

(2)init方法

@Override
public Map<String, Integer> init(Object batchId,TridentCollector collector) {
    return new HashMap<String, Integer>();
}

仅初始化了一个HashMap对象,这个对象会作为参数传给aggregate和complete方法。对一个batch只执行一次。

(3)aggregate方法

aggregate方法对于batch内的每一个tuple均执行一次。这里将这个batch内的名字出现的次数放到init方法所初始化的map中。

@Override
public void aggregate(Map<String, Integer> map,TridentTuple tuple, TridentCollector collector) {
    String key=tuple.getString(0);
    if(map.containsKey(key)){
        Integer tmp=map.get(key);
        map.put(key, ++tmp);

    }else{
        map.put(key, 1);
    }
}

(4)complete方法

这里在complete将aggregate处理完的结果发送出去,实际上可以在任何地方emit,比如在aggregate里面。
这个方法对于一个batch也只执行一次。

@Override
public void complete(Map<String, Integer> map,TridentCollector collector) {
    if (map.size() > 0) {

        for(Entry<String, Integer> entry : map.entrySet()){
            System.out.println("Thread.id="+Thread.currentThread().getId()+"|"+entry.getKey()+"|"+entry.getValue());
            collector.emit(new Values(entry.getKey(),entry.getValue()));
        }

        map.clear();
    } 
}

3、state的用法

(1)拓扑定义

先看一下主类中如何将结果写入state:

partitionPersist(
            new NameSumStateFactory(), new Fields("nameSumKey", "nameSumValue"),
            new NameSumUpdater());

它的定义为:

TridentState storm.trident.Stream.partitionPersist(StateFactory stateFactory, Fields inputFields, StateUpdater updater)

其中的第二个参数比较容易理解,就是输入流的名称,这里是名字与它出现的个数。下面先看一下Facotry。

(2)工厂类:NameSumStateFactory

很简单,它实现了StateFactory,只有一个方法makeState,返回一个State类型的对象。

public class NameSumStateFactory implements StateFactory {

    private static final long serialVersionUID = 8753337648320982637L;

    @Override
    public State makeState(Map arg0, IMetricsContext arg1, int arg2, int arg3) {
        return new NameSumState();  
    } 
}

(3)更新类:NameSumUpdater

这个类继承自BaseStateUpdater,它的updateState对batch的内容进行处理,这里是将batch的内容放到一个map中,然后调用setBulk方法

public class NameSumUpdater extends BaseStateUpdater<NameSumState> {

    private static final long serialVersionUID = -6108745529419385248L;
    public void updateState(NameSumState state, List<TridentTuple> tuples, TridentCollector collector) {
        Map<String,Integer> map=new HashMap<String,Integer>();
        for(TridentTuple t: tuples) {
            map.put(t.getString(0), t.getInteger(1));
        }
        state.setBulk(map);
    }
}

(4)状态类:NameSumState

这是state最核心的类,它实现了大部分的逻辑。NameSumState实现了State接口:

public interface State {
    void beginCommit(Long txid); 
    void commit(Long txid);
}

分别在提交之前与提交成功的时候调用,在这里只打印了一些信息。

另外NameSumState还定义了如何处理NameSumUpdater传递的消息:

public void setBulk(Map<String, Integer> map) {
    // 将新到的tuple累加至map中
    for (Entry<String, Integer> entry : map.entrySet()) {
        String key = entry.getKey();
        if (this.map.containsKey(key)) {
            this.map.put(key, this.map.get(key) + map.get(key));
        } else {
            this.map.put(key, entry.getValue());
        }
    }
    System.out.println("-------");
    // 将map中的当前状态打印出来。
    for (Entry<String, Integer> entry : this.map.entrySet()) {
        String Key = entry.getKey();
        Integer Value = entry.getValue();
        System.out.println(Key + "|" + Value);
    }
}

即将NameSumUpdater传送过来的内容写入一个HashMap中,并打印出来。
此处将state记录在一个HashMap中,如果需要记录在其它地方,如mysql,则使用jdbc写入mysql代替下面的map操作即可。

事实上,这个操作不一定要在state中执行,可以在任何类中,但建议还是在state类中实现。

4、state应用思路总结

(1)使用state,你不再需要比较事务id,在数据库中同时写入多个值等内容,而是专注于你的逻辑实现
(2)除了实现State接口,更常用的是实现MapState接口,下次补充。
(3)在拓扑中指定了StateFactory,这个工厂类找到相应的State类。而Updater则每个批次均会调用它的方法。State中则定义了如何保存数据,这里将数据保存在内存中的一个HashMap,还可以保存在mysql, hbase等等。
(4)trident会自动比较txid的值,如果和当前一样,则不更改状态,如果是当前txid的下一个值,则更新状态。这种逻辑不需要用户处理。
(5)如果需要实现透明事务状态,则需要保存当前值与上一个值,在update的时候2个要同时处理。即逻辑由自己实现。在本例子中,大致思路是在NameSumState中创建2个HashMap,分别对应当前与上一个状态的值,而NameSumUpdater每次更新这2个Map。

你可能感兴趣的:(trident,state)