JStorm源码分析(二)JStorm中的基本数据结构—— Fields,Values&Tuples(草稿|无图版)

来源:互联网 发布:浮云淘宝小号出售平台 编辑:程序博客网 时间:2024/05/17 07:48

描述某一行值——Values

毫无疑问,存储在数据流中的值字段是绝大多数业务逻辑关注的重点,包含着实实在在的信息。然而,越庞大越重要的事物往往越简洁越直观。体现在JStorm使用Values类来描述要在数据流中的“一行”值,而Values类的实现非常简单:

public class Values extends ArrayList<Object> 

Values类直接继承自Java中的ArrayList类,因为ArrayList类恰好能够很好地满足描述“一行”值的需要——有序、不去重、可伸缩。唯一美中不足的地方在于,Java要求ArrayList中存储的值类型必须相同,而数据流中的“一行”值却经常由不同类型的值组成。虽然可以声明ArrayList存储通用的Object类型来解决这个问题,但是仍然需要对ArrayList进行一些小的改造,添加一些类似“语法糖”的方法,使其更符合实际需要。

Values类所作的优化主要是提供若干个支持可变列表的构造方法,包括全为String的参数类型、全为Integer的参数类型以及通用的Object类型:

public Values(Object... vals) {        super(vals.length);        for (Object o : vals) {            add(o);        }        type = OBJECT;    }    public Values(String... strs) {        super(strs.length);        for (String s : strs) {            add(s);        }        type = STRING;    }    public Values(Integer... ints) {        super(ints.length);        for (Integer i : ints) {            add(i);        }        type = INTEGER;    }

这样做的一个好处是,Values类能够专门针对全是StringInteger类型值的情况做一些优化。而另一个好处则是方便用户填充这“一行”值,按照以下方式:

new Values("fields1",2,3)

然而,虽然在构造时能够根据传入参数类型是否一致作类型判断,但若往Values对象中添加新的值,则Values对象中值的类型可能会发生改变,因此需要重写add()方法:

  @Override    public boolean add(Object obj) {        Object val = obj;        if (type == STRING && !(obj instanceof String)) {            type = OBJECT;        } else if (type == INTEGER && !(obj instanceof Integer)) {            type = OBJECT;        }        return super.add(val);    }

虽然Values类在ArrayList类的基础上进行了修改,但这些修改是细枝末节的。在阅读后续源码的时候,读者们大可以将其当做ArrayList类来看待,或许会更亲切易懂一些。

 

字段声明——Fields

在使用Values描述了一行值的概念之后,接下来如何知道这行值当中每一列值代表的含义,也就是要知道字段声明。在了解了Values的设计之后,一个直接的想法是——我们能不能也在ArrayList的基础上作一些小的更改来描述字段声明?

答案是肯定的,但是做法却不像Values一样,直接继承自ArrayList。其关键在于,字段声明需要支持一个关键的功能需求:不仅能够根据序号索引找到相对应的字段,还要能够根据字段反过来找到相对应的序号索引。至于为什么会有这个需求,后面我们在讲到包含核心逻辑的源代码时会解释。现在我们来看一下这个需求,和ArrayList的特性差别有点大,因而不适合用继承的方法来做。当不能使用继承机制,又想借助一些现成类的特性时,怎么办呢?用组合而不是继承就可以了。

比起一行具体的值可能包含多种不同的类型,字段声明就简单得多了——它只由String类型组成。所以我们首先将一个List<String>封装到Fields类中。为了支持根据字段反查相对应的序号索引这个需求,很自然地想到非常擅长解决这种需求的Map,所以我们也将一个HashMap封装进来:

public class Fields implements Iterable<String>, Serializable {    private List<String> _fields;    // 倒排映射表    private Map<String, Integer> _index = new HashMap<>();


同样地,为了能够让我们以这样的方式声明字段:

 public Fields(String... fields) {        this(Arrays.asList(fields));    }

我们提供了一个支持可变参数的构造方法:

而核心的构造方法很简单,就是检查传入的字段列表中有无重复字段,有就抛异常,没有就放入内嵌的List中。但这还没有结束,我们还需要支持“倒排索引”,所以需要建立相应的HashMap

 /**     * 建立倒排索引     */    private void index() {        for (int i = 0; i < _fields.size(); i++) {            _index.put(_fields.get(i), i);        }    }

而剩下的事情就简单了,为了支持遍历,我们实现Iterable接口,把Listiterator扔给调用者就可以,再补上一些获得字段大小、获得字段列表等等一些辅助方法,Fields类就能够很好地描述字段声明的概念了。

最后补充一点,Values类继承自ArrayList,所以不需要考虑序列化的问题。但Fields用的是组合方式,因而需要实现Serializable接口,否则无法在流中传输:

public class Fields implements Iterable<String>, Serializable 


字段+=元组——Tuple

在分别给出了字段和值的定义之后,元组就很好定义了。我们使用组合的方式,把字段和值组合起来就可以了。但流处理对元组这个基本数据结构还提出了很多其他的需求,因而描述元组的Tuple类也较为复杂。不过不用怕,这些复杂性仅仅是为了给Tuple增加更多功能性的支持,我们完全可以根据添加功能的不同来依次学习这些源码。

JStorm在设计元组概念时,具有如下的类结构:

(贴UML图)

最顶层的ITuple接口定义了元组应该支持的最基本的方法,包括获得字段数目、判断是否包含某个字段、获得所有字段名称以及一系列用于获取特定位置或特定字段值的方法。由于这些方法我们在编写JStorm应用的时候也会经常遇到,所以这里将这些方法声明全部贴出来,加深大家的记忆:

public interface ITuple {    /**     * return the number of fields in this tuple.     */    public int size();    /**     * Returns true if this tuple contains the specified name of the field.     */    public boolean contains(String field);    /**     * Gets the names of the fields in this tuple.     */    public Fields getFields();    /**     * Returns the position of the specified field in this tuple.     */    public int fieldIndex(String field);    /**     * Returns a subset of the tuple based on the fields selector.     */    public List<Object> select(Fields selector);    /**     * Gets the field at position i in tuple. Returns object since tuples are dynamically typed.     */    public Object getValue(int i);    /**     * Returns the String at position i in the tuple. If that field is not a String, you will get a runtime error.     */    public String getString(int i);    /**     * Returns the Integer at position i in the tuple. If that field is not a Integer, you will get a runtime error.     */    public Integer getInteger(int i);    /**     * Returns the Long at position i in the tuple. If that field is not a Long, you will get a runtime error.     */    public Long getLong(int i);    /**     * Returns the Boolean at position i in the tuple. If that field is not a Boolean, you will get a runtime error.     */    public Boolean getBoolean(int i);    /**     * Returns the Short at position i in the tuple. If that field is not a Byte, you will get a runtime error.     */    public Short getShort(int i);    /**     * Returns the Byte at position i in the tuple. If that field is not a Byte, you will get a runtime error.     */    public Byte getByte(int i);    /**     * Returns the Double at position i in the tuple. If that field is not a Double, you will get a runtime error.     */    public Double getDouble(int i);    /**     * Returns the Float at position i in the tuple. If that field is not a Float, you will get a runtime error.     */    public Float getFloat(int i);    /**     * Returns the byte array at position i in the tuple. If that field is not a byte array, you will get a runtime error.     */    public byte[] getBinary(int i);    public Object getValueByField(String field);    public String getStringByField(String field);    public Integer getIntegerByField(String field);    public Long getLongByField(String field);    public Boolean getBooleanByField(String field);    public Short getShortByField(String field);    public Byte getByteByField(String field);    public Double getDoubleByField(String field);    public Float getFloatByField(String field);    public byte[] getBinaryByField(String field);    /**     * Gets all the values in this tuple.     */    public List<Object> getValues();}

为什么有了顶层的ITuple接口之后,还要再定义一个子接口Tuple呢?这是因为ITuple接口是从数据结构的角度来定义的,而Tuple则是从JStorm集群的角度来定义的,包含一系列用于唯一标识和定位元组的方法:

public interface Tuple extends ITuple {    /**     * Returns the global stream id (component + stream) of this tuple.     */    public GlobalStreamId getSourceGlobalStreamid();    /**     * Gets the id of the component that created this tuple.     */    public String getSourceComponent();    /**     * Gets the id of the task that created this tuple.     */    public int getSourceTask();    /**     * Gets the id of the stream that this tuple was emitted to.     */    public String getSourceStreamId();    /**     * Gets the message id that associated with this tuple.     */    public MessageId getMessageId();}

可以看出,这些方法使得Storm可以根据元组的标识和所属的组件、所属的流等信息来执行元组传递、分组分发等操作,而这些方法对应用层来说,是不怎么需要的,所以分成两个接口,在我们自己编写项目代码时也可以借鉴这种设计。

TupleImplTuple的具体实现,其中一部分需要兼容主要用Clojure编写的Storm代码,所以比较复杂,但是这里我们不需要单独把这些内容拿出来研究。我们只需要知道,这些代码是给Tuple增加更多支持,使得JStorm和我们使用Tuple起来更方便快捷就可以了。感兴趣的同学可以结合Clojure的知识来详细了解一下这个类(backtype.storm.utils.TupleImpl),这里不再赘述。

另外一部分内容是我们要注意的。由于实现了Tuple接口,所以我们需要获得集群拓扑的上下文信息,来标识和定位元组:

// topology context    private GeneralTopologyContext context;// ...@Override    public Fields getFields() {        return context.getComponentOutputFields(getSourceComponent(), getSourceStreamId());    }    @Override    public List<Object> select(Fields selector) {        return getFields().select(selector, values);    }    @Override    public GlobalStreamId getSourceGlobalStreamid() {        return new GlobalStreamId(getSourceComponent(), streamId);    }

元组本身其实是流处理中的一个抽象概念,具体到流处理过程中,在集群内机器之间传递的,其实是包含了已经序列化的元组的消息(Message)。要定位和标识元组,消息编号也是必不可少的:

一般情况下,使用一个long类型来表示消息编号也就足够了。但是这里有一个特殊的需求——我们不仅要知道元组关联的当前消息的编号,还要知道这个元组关联的“父消息”(就是上游消息)的编号,因此我们定义一个MessageId对象,包装了一个Map,如下所示:

MessageId的设计上还有一些值得我们关注的点,我们留到之后讲到集群之间元组传递的代码时再详细阐述。现在,读者们大可以将它当成一个long类型的编号来看待。

Tuple的增补支持——TupleExt

Tuple的定义基本上能够满足JStorm绝大多数核心逻辑对元组的需求,但是随着流处理的发展,对元组的功能需求也就越来越多,这些功能需求提现在TupleExt接口上,定义了一些方法:

public interface ITupleExt {    /**     * Get Target TaskId     */    int getTargetTaskId();    void setTargetTaskId(int targetTaskId);    /**     * Get the timestamp of creating tuple     */    long getCreationTimeStamp();    void setCreationTimeStamp(long timeStamp);    boolean isBatchTuple();    void setBatchTuple(boolean isBatchTuple);}

targetTaskId相关的方法用于直接将元组发送到指定task上;

creationTimeStamp相关的方法用于获取元组创建的时间戳;

isBatchTuple则用于支持具有批量处理操作的窗口特性。

1 0