Trident及State的原理请见另一篇文章:http://blog.csdn.net/lujinhong2/article/details/47132305
trident通过spout的事务性与state的事务处理,保证了恰好一次的语义。这里介绍了如何使用state。
完整代码请见 https://github.com/lujinhong/tridentdemo
主类定义了拓扑的整体逻辑,这个拓扑通过一个固定的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());
这里涉及了一些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() {
}
}
它实现了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个方法的实现。
@Override
public Map<String, Integer> init(Object batchId,TridentCollector collector) {
return new HashMap<String, Integer>();
}
仅初始化了一个HashMap对象,这个对象会作为参数传给aggregate和complete方法。对一个batch只执行一次。
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);
}
}
这里在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();
}
}
先看一下主类中如何将结果写入state:
partitionPersist(
new NameSumStateFactory(), new Fields("nameSumKey", "nameSumValue"),
new NameSumUpdater());
它的定义为:
TridentState storm.trident.Stream.partitionPersist(StateFactory stateFactory, Fields inputFields, StateUpdater updater)
其中的第二个参数比较容易理解,就是输入流的名称,这里是名字与它出现的个数。下面先看一下Facotry。
很简单,它实现了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();
}
}
这个类继承自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);
}
}
这是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类中实现。
(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。