四、事務拓撲(Transactional Topolgoy)
1、問題的提出
怎樣做到每個出錯的tuple只被處理一次?這樣才能統計所有發射出的tuple的數量。
2、簡介
Storm 0.7.0引入了Transactional Topology, 它可以保證每個tuple”被且僅被處理一次”, 這樣你就可以實現一種非常準確,非常可擴充套件,並且高度容錯方式來實現計數類應用。跟DRPC類似, transactional topology其實不能算是storm的一個特性,它其實是用storm的底層原語spout, bolt, topology, stream等等抽象出來的一個特性。
3、三個設計<1>強順序流:順序id的tuple+資料庫
比如你想統一個stream裡面tuple的總數。那麼為了保證統計數字的準確性,你在資料庫裡面不但要儲存tuple的個數, 還要儲存這個數字所對應的最新的transaction id。 當你的程式碼要到資料庫裡面去更新這個數字的時候,你要判斷只有當新的transaction id跟資料庫裡面儲存的transaction id不一樣的時候才去更新。考慮兩種情況:
- 資料庫裡面的transaction id跟當前的transaction id不一樣: 由於我們transaction的強順序性,我們知道當前的tuple肯定沒有統計在資料庫裡面。所以我們可以安全地遞增這個數字,並且更新這個transaction id.
- 資料庫裡面的transaction id一樣: 那麼我們知道當前tuple已經統計在資料庫裡面了,那麼可以忽略這個更新。這個tuple肯定之前在更新了資料庫之後,反饋給storm的時候失敗了(ack超時之類的)。
給整個batch一個transaction id,batch與batch之間的處理是強順序性的, 而batch內部是可以並行的
優點: 減少資料庫呼叫;利用了storm的平行計算能力(每個batch內部可以並行)
缺點:考慮下面這個topology
在bolt
1完成它的處理之後, 它需要等待剩下的bolt去處理當前batch, 直到發射下一個batch
<3>storm的設計
- processing階段: 這個階段很多batch可以平行計算。
- commit階段: 這個階段各個batch之間需要有強順序性的保證。所以第二個batch必須要在第一個batch成功提交之後才能提交。
4、例子
原始碼如下:
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import backtype.storm.Config;
import backtype.storm.LocalCluster;
import backtype.storm.coordination.BatchOutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.testing.MemoryTransactionalSpout;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseBatchBolt;
import backtype.storm.topology.base.BaseTransactionalBolt;
import backtype.storm.transactional.ICommitter;
import backtype.storm.transactional.TransactionAttempt;
import backtype.storm.transactional.TransactionalTopologyBuilder;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Tuple;
import backtype.storm.tuple.Values;
public class TransactionalGlobalCount {
public static final int PARTITION_TAKE_PER_BATCH=3;
public static final Map<Integer,List<List<Object>>> DATA=new HashMap<Integer, List<List<Object>>>(){
{
put(0,new ArrayList<List<Object>>(){
{
add(new Values("cat"));
add(new Values("dog"));
add(new Values("chicken"));
add(new Values("cat"));
add(new Values("dog"));
add(new Values("apple"));
}
});
put(1,new ArrayList<List<Object>>(){
{
add(new Values("cat"));
add(new Values("dog"));
add(new Values("apple"));
add(new Values("banana"));
}
});
put(2,new ArrayList<List<Object>>(){
{
add(new Values("cat"));
add(new Values("cat"));
add(new Values("cat"));
add(new Values("cat"));
add(new Values("cat"));
add(new Values("dog"));
add(new Values("dog"));
add(new Values("dog"));
add(new Values("dog"));
}
});
}
};
public static class Value{
int count=0;
BigInteger txid;
}
public static Map<String, Value> DATABASE=new HashMap<String, Value>();
public static final String GLOBAL_COUNT_KEY="GLOBAL-COUNT";
public static class BatchCount extends BaseBatchBolt{
Object _id;
BatchOutputCollector _collector;
int _count=0;
@Override
public void prepare(Map conf, TopologyContext context,
BatchOutputCollector collector, Object id) {
// TODO Auto-generated method stub
_collector=collector;
_id=id;
}
@Override
public void execute(Tuple tuple) {
// TODO Auto-generated method stub
_count++;
}
@Override
public void finishBatch() {
// TODO Auto-generated method stub
_collector.emit(new Values(_id,_count));
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// TODO Auto-generated method stub
declarer.declare(new Fields("id","count"));
}
}
public static class UpdateGlobalCount extends BaseTransactionalBolt implements ICommitter {
TransactionAttempt _attempt;
BatchOutputCollector _collector;
int _sum = 0;
@Override
public void prepare(Map conf,
TopologyContext context,
BatchOutputCollector collector,
TransactionAttempt attempt) {
_collector = collector;
_attempt = attempt;
}
@Override
public void execute(Tuple tuple) {
_sum+=tuple.getInteger(1);
}
@Override
public void finishBatch() {
Value val = DATABASE.get(GLOBAL_COUNT_KEY);
Value newval;
if(val == null ||
!val.txid.equals(_attempt.getTransactionId())) {
newval = new Value();
newval.txid = _attempt.getTransactionId();
if(val==null) {
newval.count = _sum;
} else {
newval.count = _sum + val.count;
}
DATABASE.put(GLOBAL_COUNT_KEY, newval);
} else {
newval = val;
}
_collector.emit(new Values(_attempt, newval.count));
System.out.println(_attempt);
System.out.println(newval.count);
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("id", "sum"));
}
}
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
MemoryTransactionalSpout spout = new MemoryTransactionalSpout(
DATA, new Fields("word"), PARTITION_TAKE_PER_BATCH);
TransactionalTopologyBuilder builder = new TransactionalTopologyBuilder(
"global-count", "spout", spout, 3);
builder.setBolt("partial-count", new BatchCount(), 5)
.shuffleGrouping("spout");
builder.setBolt("sum", new UpdateGlobalCount())
.globalGrouping("partial-count");
LocalCluster cluster=new LocalCluster();
Config config=new Config();
config.setDebug(true);
config.setMaxSpoutPending(3);
cluster.submitTopology("global-count-topology", config, builder.buildTopology());
Thread.sleep(3000);
cluster.shutdown();
}
}
詳解如下:
<1>構建Topology
MemoryTransactionalSpout spout = new MemoryTransactionalSpout(
DATA, new Fields("word"), PARTITION_TAKE_PER_BATCH);
TransactionalTopologyBuilder builder = new TransactionalTopologyBuilder(
"global-count", "spout", spout, 3);
builder.setBolt("partial-count", new BatchCount(), 5)
.shuffleGrouping("spout");
builder.setBolt("sum", new UpdateGlobalCount())
.globalGrouping("partial-count");</span>
TransactionalTopologyBuilder
接受如下的引數
- 這個transaction topology的id
- spout在整個topology裡面的id。
- 一個transactional spout。
- 一個可選的這個transactional spout的並行度。
一個transaction topology裡面有一個唯一的TransactionalSpout
,
這個spout是通過TransactionalTopologyBuilder
的建構函式來制定的。在這個例子裡面,MemoryTransactionalSpout
被用來從一個記憶體變數裡面讀取資料(DATA)。第二個引數制定資料的fields,
第三個引數指定每個batch的最大tuple數量。
<2>第一個bolt:BatchBolt:
隨機地把輸入tuple分給各個task,然後各個task各自統計區域性數量
public static class BatchCount extends BaseBatchBolt{
Object _id;
BatchOutputCollector _collector;
int _count=0;
@Override
public void prepare(Map conf, TopologyContext context,
BatchOutputCollector collector, Object id) {
// TODO Auto-generated method stub
_id=id;
}
@Override
public void execute(Tuple tuple) {
// TODO Auto-generated method stub
_count++;
}
@Override
public void finishBatch() {
// TODO Auto-generated method stub
_collector.emit(new Values(_id,_count));
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// TODO Auto-generated method stub
declarer.declare(new Fields("id","count"));
}
}
storm會為每個batch建立這個一個BatchCount
物件。而這些BatchCount
是執行在BatchBoltExecutor
裡面的。
這個物件的prepare方法接收如下引數:
- 包含storm config資訊的map。
- TopologyContext
- OutputCollector
- 這個batch的id。而在Transactional Topologies裡面, 這個id則是一個TransactionAttempt物件。
在transaction topology裡面發射的所有的tuple都必須以TransactionAttempt
作為第一個field,然後storm可以根據這個field來判斷哪些tuple屬於一個batch。所以你在發射tuple的時候需要滿足這個條件。TransactionAttempt
包含兩個值:
一個transaction id,一個attempt id。transaction id的作用就是我們上面介紹的對於每個batch是唯一的,而且不管這個batch replay多少次都是一樣的。 我們可以把attempt id理解成replay-times, storm利用這個id來區別一個batch發射的tuple的不同版本。transaction
id對於每個batch加一, 所以第一個batch的transaction id是”1″, 第二個batch是”2″,以此類推。execute方法會為batch裡面的每個tuple執行一次。最後, 當這個bolt接收到某個batch的所有的tuple之後,
finishBatch方法會被呼叫。這個例子裡面的BatchCount類會在這個時候發射它的區域性數量到它的輸出流裡面去。
<3>第二個bolt:UpdateBlobalCount
,
用全域性grouping來從彙總這個batch的總的數量。然後再把總的數量更新到資料庫裡面去。
public static class UpdateGlobalCount extends BaseTransactionalBolt implements ICommitter {
TransactionAttempt _attempt;
BatchOutputCollector _collector;
int _sum = 0;
@Override
public void prepare(Map conf,
TopologyContext context,
BatchOutputCollector collector,
TransactionAttempt attempt) {
_collector = collector;
_attempt = attempt;
}
@Override
public void execute(Tuple tuple) {
_sum+=tuple.getInteger(1);
}
@Override
public void finishBatch() {
Value val = DATABASE.get(GLOBAL_COUNT_KEY);
Value newval;
if(val == null ||
!val.txid.equals(_attempt.getTransactionId())) {
newval = new Value();
newval.txid = _attempt.getTransactionId();
if(val==null) {
newval.count = _sum;
} else {
newval.count = _sum + val.count;
}
DATABASE.put(GLOBAL_COUNT_KEY, newval);
} else {
newval = val;
}
_collector.emit(new Values(_attempt, newval.count));
}
UpdateGlobalCount
是Transactional Topologies相關的類, 所以它繼承自BaseTransactionalBolt
。在execute方法裡面, UpdateGlobalCount
累積這個batch的計數,
比較有趣的是finishBatch方法。首先, 注意這個bolt實現了ICommitter
介面。這告訴storm要在這個事務的commit階段呼叫finishBatch
方法。所以對於finishBatch的呼叫會保證強順序性(順序就是transaction
id的升序), 而相對來說execute方法在任何時候都可以執行,processing或者commit階段都可以。UpdateGlobalCount
裡面finishBatch方法的邏輯是首先從資料庫中獲取當前的值,並且把資料庫裡面的transaction
id與當前這個batch的transaction id進行比較。如果他們一樣, 那麼忽略這個batch。否則把這個batch的結果加到總結果裡面去,並且更新資料庫。
相關文章
- Storm入門之第8章事務性拓撲ORM
- Java開啟事務(@Transactional)Java
- Rama透過拓撲通用語言實現ACID事務
- @Transactional spring 配置事務 注意事項Spring
- 網路拓撲—FTP服務搭建FTP
- 拓撲排序排序
- 《四 spring原始碼》spring的事務註解@Transactional 原理分析Spring原始碼
- Spring宣告式事務@Transactional使用Spring
- 11.日誌和事務@Transactional
- Spring中@Transactional事務使用陷阱Spring
- 拓撲排序,YYDS排序
- 拓撲排序模板排序
- 網路拓撲—WEB-IIS服務搭建Web
- Spring @Transactional 宣告式事務揭祕Spring
- 【Spring註解】事務註解@TransactionalSpring
- 關於事務回滾註解@Transactional
- @Transactional註解管理事務和手動提交事務
- JPA的事務註解@Transactional使用總結
- 拓撲排序小結排序
- 圖論——拓撲排序圖論排序
- 筆記:拓撲排序筆記排序
- Spring中的AOP,以及宣告式事務 @Transactional無法攔截事務Spring
- Spring非同步Async和事務Transactional註解Spring非同步
- @Transactional 註解下,事務失效的多種場景
- 網路拓撲圖:網路拓撲圖介紹及線上製作
- Reward (圖論+拓撲排序)圖論排序
- 拓撲排序 - Topological Sort排序
- 拓撲排序核心程式碼排序
- HDU 4857 逃生(拓撲排序)排序
- 網路拓撲結構
- 【筆記/模板】拓撲排序筆記排序
- AOV網與拓撲排序排序
- DFS實現拓撲排序排序
- 網路拓撲例項之VRRP負載分擔(四)VR負載
- Spring事務的介紹,以及基於註解@Transactional的宣告式事務Spring
- 事務註解(@Transactional)引起的資料覆蓋故障
- 【沃趣科技】MySQL高可用工具Orchestrator系列四:拓撲恢復MySql
- 拓撲排序就這麼回事排序