转载自:https://blog.youkuaiyun.com/l1028386804/article/details/79120204
https://blog.youkuaiyun.com/jediael_lu/article/details/76794843
实例一
通过实例来了解Trident topology。需求是收集医学诊断报告来判断是否有疾病暴发。这个topology会处理的医学诊断事件包括如下信息:
Latitude | Longitude | Timestamp | Diagnosis Code(ICD9-CM) |
---|---|---|---|
39.9522 | -75.1624 | 01/21/2018 at 11:00 AM | 320.0(Hemophilus meningitis) |
40.3588 | -75.6269 | 01/21/2018 at 11:30 AM | 324.0(Intracranial abscess) |
每个事件包括事件发生时的全球定位系统(GPS)的位置坐标,经度和维度使用十进制小数表示,事件还包括ICD9-CM编码,表示诊断结果,以及事件发生的时间戳。
为了判断是否有疾病暴发,系统会按照地理位置来统计各种疾病代码在一段时间内出现的次数。为了简化例子,按照城市划分诊断结果的地理位置。实际系统会对地理位置做出更精细的划分。
另外,实例中会按小时对针对时间进行分组。使用移动平均值来计算趋势。
最后使用简单的阈值来判断是否有疾病暴发。如果某个时间时间发生的系数超过了阈值,系统会发生告警。同时,为了维护历史记录,还需要将每个城市、小时、疾病的统计量持久化存储。
一、Trident Spout
Trident引入了"数据批次"的概念,不像Storm的spout,Trident spout必须成批的发送tuple。每个Batch会分配一个唯一的事务标识符。spout基于约定决定batch的组成方式,spout有三种约定:非事务型、事务型、非透明型。
非事务型spout:对batch的组成部分不提供保证,并且可能出现重复,两个不同的batch可能含有相同的tuple
事务型spout:保证batch是非重复的,并且batch总是包含相同的tuple。
非透明型spout:保证数据是非重复的,但不能保证batch的内容是不变的。
DiagnosisEventSpout如下:
public class DiagnosisEventSpout implements ITridentSpout<Long> {
private BatchCoordinator<Long> coordinator = new DefaultCoordinator();
private Emitter<Long> emitter = new DiagnosisEventEmitter();
@Override
public BatchCoordinator<Long> getCoordinator(String txStateId, Map conf, TopologyContext context) {
return coordinator;
}
@Override
public Emitter<Long> getEmitter(String txStateId, Map conf, TopologyContext context) {
return emitter;
}
@Override
public Map getComponentConfiguration() {
return null;
}
@Override
public Fields getOutputFields() {
return new Fields("event");
}
}
如上述代码中的getOutputFields()方法所示,该spout发射一个字段event,值是由Emitter实例发射的DiagnosisEvent,DefultCoordinator类来进行协调:
public class DefaultCoordinator implements ITridentSpout.BatchCoordinator<Long>, Serializable {
private static final Logger LOG = LoggerFactory.getLogger(DefaultCoordinator.class);
@Override
public boolean isReady(long txid) {
return true;
}
@Override
public void close() {
}
@Override
public Long initializeTransaction(long txid, Long prevMetadata, Long currMetadata) {
LOG.info("Initializing Transaction [" + txid + "]");
return null;
}
@Override
public void success(long txid) {
LOG.info("Successful Transaction [" + txid + "]");
}
}
public class DiagnosisEventEmitter implements ITridentSpout.Emitter<Long>, Serializable {
AtomicInteger successfulTransactions = new AtomicInteger(0);
@Override
public void emitBatch(TransactionAttempt tx, Long coordinatorMeta, TridentCollector collector) {
for (int i = 0; i < 10000; i++) {
List<Object> events = new ArrayList<>();
double lat = (double) (-30 + (int) (Math.random() * 75));
double lng = (double) (-120 + (int) (Math.random() * 70));
long time = System.currentTimeMillis();
String diagnosisCode = Integer.toString(320 + (int) (Math.random() * 7));
DiagnosisEvent event = new DiagnosisEvent(lat, lng, time, diagnosisCode);
events.add(event);
collector.emit(events);
}
}
@Override
public void success(TransactionAttempt tx) {
successfulTransactions.incrementAndGet();
}
@Override
public void close() {
}
}
Emitter#emitBatch
函数接收的参数coordinatorMeta是由coordinator生成的。
发送的工作在emitBatch()中进行。随机分配了一个经纬度,使用System.currentTimeStamp()
方法生成时间戳,使用320-327
之间的诊断码。
DiagosisEvent是一个简单的JavaBean。时间戳使用long变量存储,存储的是时间的秒数。经度和维度使用double存储。
public class DiagnosisEvent implements Serializable {
private static final long serialVersionUID = 1L;
public double lat;
public double lng;
public long time;
public String diagnosisCode;
public DiagnosisEvent(double lat, double lng, long time, String diagnosisCode) {
super();
this.time = time;
this.lat = lat;
this.lng = lng;
this.diagnosisCode = diagnosisCode;
}
}
二、Trident tology
使用Trident创建tology:
public static StormTopology buildTopology() {
TridentTopology topology = new TridentTopology();
DiagnosisEventSpout spout = new DiagnosisEventSpout();
Stream inputStream = topology.newStream("event", spout);
inputStream.each(new Fields("event"), new DiseaseFilter())
.each(new Fields("event"), new CityAssignment(), new Fields("city"))
.each(new Fields("event", "city"), new HourAssignment(), new Fields("hour", "cityDiseaseHour"))
.groupBy(new Fields("cityDiseaseHour"))
.persistentAggregate(new OutbreakTrendFactory(), new Count(), new Fields("count")).newValuesStream()
.each(new Fields("cityDiseaseHour", "count"), new OutbreakDetector(), new Fields("alert"))
.each(new Fields("alert"), new DispatchAlert(), new Fields());
return topology.build();
}
DiagnosisEventSpout函数发射疾病事件,然后事件由DiseaseFilter函数过滤,过滤掉不关心的疾病事件。之后,事件由CityAssignment函数赋值一个对应的城市名。然后HourAssignment函数复制一个表示小时的时间戳,并且增加一个Key CityDiseaseHour到tuple的字段中,这个Key包括城市、小时和疾病代码。后续就使用这个Key进行分组统计并使用persistAggregate函数对统计量持久性存储。统计量传递给OutbreakDetector函数,如果统计量超过阈值,OutbreakDetector向后发送一个告警信息。最后DispatchAlert接收到告警信息,记录日志,并且流程结束。
三、Trident filter
为了通过疾病代码过滤事件,需要利用Trident filter。Trident提供BaseFilter类可以方便的对tuple过滤,滤除系统不需要的tuple。
public class DiseaseFilter extends BaseFilter {
private static final long serialVersionUID = 1L;
private static final Logger LOG = LoggerFactory.getLogger(DiseaseFilter.class);
@Override
public boolean isKeep(TridentTuple tuple) {
DiagnosisEvent diagnosis = (DiagnosisEvent) tuple.getValue(0);
int code = Integer.parseInt(diagnosis.diagnosisCode);
if (code <= 322) {
LOG.debug("Emitting disease [" + diagnosis.diagnosisCode + "]");
return true;
}
else {
LOG.debug("Filtering disease [" + diagnosis.diagnosisCode + "]");
return false;
}
}
}
Filter操作结果返回true的tuple将会被发送到下游进行操作,如果返回false,该tuple就不会发送到下游。在数据流上使用each(inputFields, filter)方法,可以将这个过滤器应用到每个tuple中。
四、Trident function
STorm还提供了一个更通用的功能接口function。function和Storm的bolt类似,读取tuple并且发送新的tuple。其中一个区别是Trident function只能添加数据。function发送数据时,将新字段添加到tuple中,并不会删除或者变更已有的字段。
和Storm的bolt类似,function实现了一个包括逻辑的方法execute。function的实现也可以选用TridentCollector来发送tuple到新的function中。用这种方式,function也可以用来过滤tuple,起到filter的作用。
来看下CityAssignment类:
public class CityAssignment extends BaseFunction {
private static final long serialVersionUID = 1L;
private static final Logger LOG = LoggerFactory.getLogger(CityAssignment.class);
private static Map<String, double[]> CITIES = new HashMap<>();
static {
// Initialize the cities
CITIES.put("PHL", new double[]{39.875365, -75.249524});
CITIES.put("NYC", new double[]{40.71448, -74.00598});
CITIES.put("SF", new double[]{-31.4250142, -62.0841809});
CITIES.put("LA", new double[]{-34.05374, -118.24307});
}
@Override
public void execute(TridentTuple tuple, TridentCollector collector) {
DiagnosisEvent diagnosis = (DiagnosisEvent) tuple.getValue(0);
double leastDistance = Double.MAX_VALUE;
String closestCity = "NONE";
for (Map.Entry<String, double[]> city : CITIES.entrySet()) {
double R = 6371; // km
double x = (city.getValue()[0] - diagnosis.lng) * Math.cos((city.getValue()[0] + diagnosis.lng) / 2);
double y = (city.getValue()[1] - diagnosis.lat);
double d = Math.sqrt(x * x + y * y) * R;
if (d < leastDistance) {
leastDistance = d;
closestCity = city.getKey();
}
}
List<Object> values = new ArrayList<>();
values.add(closestCity);
LOG.info("Closest city to lat=[" + diagnosis.lat + "], lng=[" + diagnosis.lng + "] == [" + closestCity
+ "], d=[" + leastDistance + "]");
collector.emit(values);
}
}
使用静态初始化方式建立了一个城市地图。在execute()方法中,函数遍历城市计算事件和城市之间的距离。function声明的字段数量必须和它发射出的值的字段数一致。
接下来,HourAssignment用来转化Unix时间戳,对事件进行时间分组操作。HourAssignment代码如下:
public class HourAssignment extends BaseFunction {
private static final long serialVersionUID = 1L;
private static final Logger LOG = LoggerFactory.getLogger(HourAssignment.class);
@Override
public void execute(TridentTuple tuple, TridentCollector collector) {
DiagnosisEvent diagnosis = (DiagnosisEvent) tuple.getValue(0);
String city = (String) tuple.getValue(1);
long timestamp = diagnosis.time;
long hourSinceEpoch = timestamp / 1000 / 60 / 60;
LOG.info("Key = [" + city + ":" + hourSinceEpoch + "]");
String key = city + ":" + diagnosis.diagnosisCode + ":" + hourSinceEpoch;
List<Object> values = new ArrayList<>();
values.add(hourSinceEpoch);
values.add(key);
collector.emit(values);
}
}
HourAssignment发射小时的数据,以及由城市、疾病代码、小时组合而成的key。实际上,这个组合值会作为聚合计数的唯一标识符。
再来看最后两个function用来侦测疾病暴发并改进。OutbreakDetector类代码如下:
public class OutbreakDetector extends BaseFunction {
private static final long serialVersionUID = 1L;
public static final int THRESHOLD = 10000;
@Override
public void execute(TridentTuple tuple, TridentCollector collector) {
String key = (String) tuple.getValue(0);
Long count = (Long) tuple.getValue(1);
if (count > THRESHOLD) {
List<Object> values = new ArrayList<>();
values.add("Outbreak detected for [" + key + "]");
collector.emit(values);
}
}
}
这个function提取出了特定城市、疾病、时间的发生次数,并且检查计数是否超过了设定的阈值。若超过,则发送一个新的字段包括一条告警信息。
DispatchAlert功能就是发布一个告警(并且结束程序):
public class DispatchAlert extends BaseFunction {
private static final long serialVersionUID = 1L;
private static final Logger LOG = LoggerFactory.getLogger(CityAssignment.class);
@Override
public void execute(TridentTuple tuple, TridentCollector collector) {
String alert = (String) tuple.getValue(0);
LOG.error("ALERT RECEIVED [" + alert + "]");
LOG.error("Dispatch the national guard!");
System.exit(0);
}
}
五、Trident state
接下来要完成持久化的操作,来看OutbreakTrendFactory类:
public class OutbreakTrendFactory implements StateFactory {
private static final long serialVersionUID = 1L;
@Override
public State makeState(Map conf, IMetricsContext metrics, int partitionIndex, int numPartitions) {
return new OutbreakTrendState(new OutbreakTrendBackingMap());
}
}
工厂类返回一个State对象,Storm用它来持久化存储信息。在Storm中,有三种类型的状态。
1)非事务型:没有回滚能力,更新操作是永久性的,commit操作会被忽略;
2)重复事务型:由同一批tuple提供的结果是幂等的;
3)不透明事务型: 更新操作基于先前的值,这样一批数据组成不同,持久化的数据也会变。
在分布式环境下,数据可能被重放,为了支持计数和状态更新,Trident将状态更新操作进行序列化,使用不同的状态更新模式对重放和错误数据进行容错。
来看OutBreakTrendState和OutbreakTrendBackingMap:
public class OutbreakTrendState extends NonTransactionalMap<Long> {
protected OutbreakTrendState(OutbreakTrendBackingMap outbreakBackingMap) {
super(outbreakBackingMap);
}
}
public class OutbreakTrendBackingMap implements IBackingMap<Long> {
private static final Logger LOG = LoggerFactory.getLogger(OutbreakTrendBackingMap.class);
private Map<String, Long> storage = new ConcurrentHashMap<>();
@Override
public List<Long> multiGet(List<List<Object>> keys) {
List<Long> values = new ArrayList<>();
for (List<Object> key : keys) {
Long value = storage.get(key.get(0));
if (value == null) {
values.add(0L);
}
else {
values.add(value);
}
}
return values;
}
@Override
public void multiPut(List<List<Object>> keys, List<Long> vals) {
for (int i = 0; i < keys.size(); i++) {
LOG.info("Persisting [" + keys.get(i).get(0) + "] ==> [" + vals.get(i) + "]");
storage.put((String) keys.get(i).get(0), vals.get(i));
}
}
}
在示例的topology中,实际上没有固化存储数据。只是简单的将数据放入ConcurrentHashMap中。显然,对于多个机器的环境下,这样是不可行的。然而,BackingMap是一个抽象。只需要将传入的MapState对象的backing map的实例替换就可以更换持久层的实现。
最后执行topology:
public static void main(String[] args) throws Exception {
Config conf = new Config();
LocalCluster cluster = new LocalCluster();
cluster.submitTopology("cdc", conf, buildTopology());
Thread.sleep(200000);
cluster.shutdown();
}
实例二
该示例是将消息中的内容提取出来成name, age, title, tel4个field,然后通过project只保留name字段供统计,接着按照name分区后,为每个分区进行聚合,最后将聚合结果通过state写入map中。
public static StormTopology buildTopology() {
TridentTopology topology = new TridentTopology();
DiagnosisEventSpout spout = new DiagnosisEventSpout();
Stream inputStream = topology
.newStream("tridentStateDemoId", spout)
.parallelismHint(3) //设置并行度
.shuffle()
.parallelismHint(3)
.each(new Fields("msg"), new Split(), new Fields("name", "age", "title", "tel"))
.parallelismHint(3)
.project(new Fields("name")) //不需要发射age、title、tel字段
.parallelismHint(3)
.partitionBy(new Fields("name"));
inputStream.partitionAggregate(new Fields("name"), new NameCountAggregator(), new Fields("nameSumKey", "nameSumValue"))
.partitionPersist(new NameSumStateFactory(), new Fields("nameSumKey", "nameSumValue"), new NameSumUpdater());
return topology.build();
}
这里涉及了一些trident常用的API,但project等相对容易理解,这里介绍下partitionAggregate的用法。代码中对partitionAggregate的使用:
.partitionAggregate(new Fields("name"), new NameCountAggregator(),
new Fields("nameSumKey", "nameSumValue"))
第一、三个参数分别表示输入流与输出流的名称。中间的NameCountAggregator是一个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方法处理完后被调用。
NameCountAggregator实现了该接口:
public class NameCountAggregator implements Aggregator<Map<String, Integer>> {
private static final Logger LOG = LoggerFactory.getLogger(NameCountAggregator.class);
private static final long serialVersionUID = -5141558506999420908L;
@Override
public Map<String, Integer> init(Object batchId, TridentCollector collector) {
LOG.info("init {}", batchId);
return new HashMap<>();
}
//判断某个名字是否已经存在于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 (Map.Entry<String, Integer> entry : map.entrySet()) {
LOG.info("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() {
}
}
init方法初始化了一个HashMap对象,这个对象会作为参数传给aggregate和complete方法,对一个batch只执行一次。aggregate方法对于batch内的每一个tuple均执行一次。这里将这个batch内的名字出现的次数放到init方法所初始化的map中。complete中会将aggregate处理完的结果发送出去,实际上可以在任何地方emit,比如在aggregate里面。这个方法对于一个batch也只执行一次。
topology中将结果写入state:
partitionPersist(
new NameSumStateFactory(), new Fields("nameSumKey", "nameSumValue"),
new NameSumUpdater());
它的定义为:
TridentState storm.trident.Stream.partitionPersist(StateFactory stateFactory, Fields inputFields, StateUpdater updater)
其中的第二个参数比较容易理解,就是输入流的名称,这里是名字与它出现的个数。下面先看一下Facotry:
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();
}
}
它实现了StateFactory,只有一个方法makeState,返回一个State类型的对象。
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<>();
for (TridentTuple t : tuples) {
map.put(t.getString(0), t.getInteger(1));
}
state.setBulk(map);
}
}
状态类NameSumState是state最核心的类,它实现了大部分的逻辑。NameSumState实现了State接口:
public class NameSumState implements State {
private Map<String, Integer> map = new HashMap<>();
@Override
public void beginCommit(Long txid) {
}
@Override
public void commit(Long txid) {
}
public void setBulk(Map<String, Integer> map) {
// 将新到的tuple累加至map中
for (Map.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 (Map.Entry<String, Integer> entry : this.map.entrySet()) {
String Key = entry.getKey();
Integer Value = entry.getValue();
System.out.println(Key + "|" + Value);
}
}
}
beginCommit、commit分别在提交之前与提交成功的时候调用。另外NameSumState还定义了如何处理NameSumUpdater传递的消息setBulk方法。setBulk方法中即将NameSumUpdater传送过来的内容写入一个HashMap中,并打印出来。 此处将state记录在一个HashMap中,如果需要记录在其它地方,如mysql,则使用jdbc写入mysql代替map操作即可。
state的应用步骤相当简单,原理也很简单。NameSumStateFactory()指定了将结果保存在哪里,NameSumUpdater()指定了更新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。
state与MapState的差异
由上面可以看出,state需要自己指定如何更新数据
if (this.map.containsKey(key)) {
this.map.put(key, this.map.get(key) + map.get(key));
} else {
this.map.put(key, entry.getValue());
}
}
这里是将原有的值,加上新到的值。而MapState会根据选择的类型(Transactional, Opaque, NonTransactional)定义好逻辑,只要定义如何向state中读写数据即可。
MapState将State的aggreate与persistent 这两部分操作合在一起了,由方法名也可以看出。在State中最后2步是partitionAggregate()与partitionPersistent(),而在MapState中最后1步是persistentAggregate()
事实上,查看persistentAggregate()的实现,它最终也是分成aggregate和persistent两个步骤的。
public TridentState persistentAggregate(StateSpec spec, Fields inputFields, CombinerAggregator agg, Fields functionFields) {
return aggregate(inputFields, agg, functionFields)
.partitionPersist(spec,
TridentUtils.fieldsUnion(_groupFields, functionFields),
new MapCombinerAggStateUpdater(agg, _groupFields, functionFields),
TridentUtils.fieldsConcat(_groupFields, functionFields));
}
1、persistentAggregate
Trident有另外一种更新State的方法叫做persistentAggregate。如下:
TridentTopology topology = new TridentTopology();
TridentState wordCounts =
topology.newStream("spout1", spout)
.each(new Fields("sentence"), new Split(), new Fields("word"))
.groupBy(new Fields("word"))
.persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count"))
persistentAggregate是在partitionPersist之上的另外一层抽象。它知道怎么去使用一个Trident 聚合器来更新State。在这个例子当中,因为这是一个group好的stream,Trident会期待提供的state是实现了MapState接口的。用来进行group的字段会以key的形式存在于State当中,聚合后的结果会以value的形式存储在State当中。MapState接口看上去如下所示:
public interface MapState<T> extends State {
List<T> multiGet(List<List<Object>> keys);
List<T> multiUpdate(List<List<Object>> keys, List<ValueUpdater> updaters);
void multiPut(List<List<Object>> keys, List<T> vals);
}
当在一个未经过group的stream上面进行聚合的话,Trident会期待你的state实现Snapshottable接口:
public interface Snapshottable<T> extends State {
T get();
T update(ValueUpdater updater);
void set(T o);
}
MemoryMapState 和 MemcachedState 都实现了上面的2个接口。
在Trident中实现MapState是非常简单的,它几乎帮你做了所有的事情。OpaqueMap, TransactionalMap, 和 NonTransactionalMap 类实现了所有相关的逻辑,包括容错的逻辑。只需要将一个IBackingMap 的实现提供给这些类就可以了。IBackingMap接口看上去如下所示:
public interface IBackingMap<T> {
List<T> multiGet(List<List<Object>> keys);
void multiPut(List<List<Object>> keys, List<T> vals);
}
multiGet 的参数是一个List,可以根据key来查询数据,key本身也是一个List,以方便多个值组合成key的情形。 multiPut的参数是一个List类型的keys和一个List类型的values,它们的size应该是相等的,把这些值写入state中。
OpaqueMap’s会用OpaqueValue的value来调用multiPut方法,TransactionalMap’s会提供TransactionalValue中的value,而NonTransactionalMaps只是简单的把从Topology获取的object传递给multiPut。
Trident还提供了一种CachedMap类来进行自动的LRU cache。
另外,Trident 提供了 SnapshottableMap 类将一个MapState 转换成一个 Snapshottable 对象.
可以看看 MemcachedState的实现,从而了解怎样将这些工具组合在一起形成一个高性能的MapState实现。MemcachedState是允许选择使用opaque transactional, transactional, 还是 non-transactional 语义的。
实现一个MapState,可以实现IBackingMap接口(mutliGet()/multiPut),并且实现StateFactory接口(makeState()),返回一个State对象,这是常见的用法。
以事务型状态为例,看一下整个存储过程的逻辑:
首先,persistentAggregate收到一批数据,它的第一个参数返回的是事务型的MapState。然后,TransactionalMap在multiUpdate中会判断这个事务的txid与当前state中的txid是否一致。如果txid一致的话,则保持原来的值即可,如果txid不一致,则更新数值。 如果更新数据呢?它是拿新来的值和state中的原有的值,使用persistentAggregate中第2个参数定义的类方法作聚合计算。
MapState读写mysql示例
(1)MysqlMapStateFactory
public class MysqlMapStateFactory<T> implements StateFactory {
private static final long serialVersionUID = 1987523234141L;
@Override
public State makeState(Map conf, IMetricsContext metrics, int partitionIndex, int numPartitions) {
return TransactionalMap.build((IBackingMap<TransactionalValue>) new MysqlMapStateBacking());
}
}
很简单,就一行,返回一个IBacking对象。这里使用的Transactioal,当然还可以使用NonTransactional和Opaque。
(2)MysqlMapStateBacking
最核心的还是multiGet()和multiPut:
@Override
public List<TransactionalValue> multiGet(List<List<Object>> keys) {
if (stmt == null) {
stmt = getStatment();
}
List<TransactionalValue> values = new ArrayList<TransactionalValue>();
for (List<Object> key : keys) {
String sql = "SELECT req_count FROM edt_analysis where id='" + key.get(0) + "'";
LOG.debug("============sql: " + sql);
try (ResultSet rs = stmt.executeQuery(sql)) {
if (rs.next()) {
LOG.info("Get value:{} by key:{}", rs.getObject(1), key);
values.add(derialize(rs.getObject(1)));
} else {
values.add(null);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
return values;
}
@Override
public void multiPut(List<List<Object>> keys, List<TransactionalValue> vals) {
if (stmt == null) {
stmt = getStatment();
}
for (int i = 0; i < keys.size(); i++) {
String sql = "replace into edt_analysis values('" + keys.get(i).get(0) + "','" + serialize(vals.get(i))
+ "')";
LOG.debug("===================put sql " + sql);
try {
stmt.execute(sql);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
但mysql与redis之类的不同,它需要将一个TransactionalValue对象转换为mysql中的一行数据,同理,需要将mysql中的一行数据转换为一个TransactionalValue对象:
// 将数据库中的varchar转换为TransactionalValue对象
private TransactionalValue derialize(Object object) {
String value[] = object.toString().split(",");
return new TransactionalValue(Long.parseLong(value[0]), Long.parseLong(value[1]));
}
// 将TransactionalValue转换为String
private String serialize(TransactionalValue transactionalValue) {
return transactionalValue.getTxid() + "," + transactionalValue.getVal();
}
这是使用了最简单的方式,只有2列,一行是key,一列是value,value中保存了txid及真实的value,之间以逗号分隔。
以HBaseMapState为例分析MapState代码调用全过程
1、调用过程
(1)SubtopologyBolt implements ITridentBatchBolt
这个bolt在完成一个batch的处理后会调用finishBatch(BatchInfo batchInfo)
(2)然后调用PartitionPersistProcessor implements TridentProcessor
这个处理器的finishBatch(ProcessorContext processorContext)
(3)接着调用MapCombinerAggStateUpdater implements StateUpdater<MapState>
的updateState(MapState map, List<TridentTuple> tuples, TridentCollector collector)
(4)再接着调用TransactionalMap<T> implements MapState<T>
的 multiUpdate(List<List<Object>> keys, List<ValueUpdater> updaters)
(5)最后就是调用用户定义的MapState类
(如HBaseMapState)的multiGet()
和multiPut()
方法了。
ITridentBatchBolt,简单的说就是一个blot被处理完后,会调用finishBatch()方法,然后这个方法会调用MapState()框架的updateState(),接着调用mutliUpdate(),最后调用用户定义的multiGet()和multiPut()。