Flink雙流實時對賬

背景

在電商、金融闰围、銀行赃绊、支付等涉及到金錢相關(guān)的領(lǐng)域既峡,為了安全起見羡榴,一般都有對賬的需求。

比如运敢,對于訂單支付事件校仑,用戶通過某寶付款,雖然用戶支付成功传惠,但是用戶支付完成后并不算成功迄沫,我們得確認(rèn)平臺賬戶上是否到賬了。

針對上述的場景卦方,我們可以采用批處理羊瘩,或離線計算等技術(shù)手段,通過定時任務(wù)盼砍,每天結(jié)束后尘吗,掃描數(shù)據(jù)庫中的數(shù)據(jù),核對當(dāng)天的支付數(shù)據(jù)和交易數(shù)據(jù)浇坐,進行對賬睬捶。

想要達到實時對賬的效果,比如有的用戶支付成功但是并沒有到賬近刘,要及時發(fā)出報警擒贸,我們必須得依賴實時計算框架。

我們將問題簡單化觉渴,比如有如下場景介劫,在某電商網(wǎng)站,用戶創(chuàng)建訂單并支付成功案淋,會將相關(guān)信息發(fā)給kafka座韵,字段包括,用戶uid哎迄、動作回右、訂單id隆圆、時間等信息

{userId=1, action='create', orId='order01', timestamp=1606549513} 
{userId=1, action='pay', orId='order01', timestamp=1606549516} 
{userId=2, action='create', orId='order02', timestamp=1606549513}

支付成功并且金額已經(jīng)進入平臺賬戶,往往也會把相關(guān)信息發(fā)給kafka翔烁,如訂單id渺氧,支付方式、時間等信息蹬屹。

{orId='order01', payChannel='wechat', timestamp=1606549536}
{orId='order02', payChannel='alipay', timestamp=1606549537}

只有訂單在支付(action=pay)成功后侣背,并且成功到賬,這才算一次完整的交易慨默。本案例贩耐,就是要實時檢測那些不成功的交易,如有不成功的厦取,及時發(fā)出報警信息潮太。

上述行為本身會產(chǎn)生兩種事件流,一種是訂單事件流虾攻,另一種是交易事件流铡买,我們通過Flink將兩種類型的流進行關(guān)聯(lián),實時分析沒有到賬的數(shù)據(jù)霎箍,發(fā)出報警奇钞。

為了簡化,我們從socket讀取數(shù)據(jù)流漂坏,代替從kafka消費數(shù)據(jù)景埃。

代碼示例

本案例涉及到的知識點:

  • 狀態(tài)編程
  • 定時器
  • 延遲事件處理
  • 合流操作

首先,我們需要定義訂單事件OrderEvents和交易事件ReceiptEvents

// 訂單事件
public class OrderEvents {
    // 用戶id
    private Long userId;
    // 動作
    private String action;
    // 訂單id
    private String orId;
    // 時間 單位 s
    private Long timestamp;
    // get/set
}
// 交易事件
public class ReceiptEvents {
    // 訂單id
    private String orId;
    // 支付渠道
    private String payChannel;
    // 時間 單位 s
    private Long timestamp;
    // get/set
}

通過Flink程序顶别,聯(lián)合兩條流谷徙,實時檢測交易失敗的數(shù)據(jù)并輸出到側(cè)輸出流里。

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        // 定義測輸出流筋夏,輸出只有pay事件沒有receipt事件的異常信息
        OutputTag payEventTag = new OutputTag<String>("payEventTag-side") {};
        // 定義測輸出流蒂胞,輸出只有receipt事件沒有pay事件的異常信息
        OutputTag receiptEventTag = new OutputTag<String>("receiptEventTag-side") {};

        // 讀取訂單數(shù)據(jù)
        KeyedStream<OrderEvents, String> orderStream = env.socketTextStream("localhost", 8888).map(new MapFunction<String, OrderEvents>() {
            @Override
            public OrderEvents map(String value) throws Exception {
                String[] split = value.split(",");
                return new OrderEvents(Long.parseLong(split[0]), split[1], split[2], System.currentTimeMillis() / 1000);
            }
        }).assignTimestampsAndWatermarks(new AscendingTimestampExtractor<OrderEvents>() {
            @Override
            public long extractAscendingTimestamp(OrderEvents element) {
                return element.getTimestamp() * 1000;
            }
        }).filter(new FilterFunction<OrderEvents>() {
            @Override
            public boolean filter(OrderEvents value) throws Exception {
                return value.getAction().equals("pay");
            }
        }).keyBy(new KeySelector<OrderEvents, String>() {
            @Override
            public String getKey(OrderEvents value) throws Exception {
                return value.getOrId();
            }
        });

        // 讀取交易數(shù)據(jù)
        KeyedStream<ReceiptEvents, String> receiptStream = env.socketTextStream("localhost", 9999).map(new MapFunction<String, ReceiptEvents>() {
            @Override
            public ReceiptEvents map(String value) throws Exception {
                String[] split = value.split(",");
                return new ReceiptEvents(split[0], split[1], System.currentTimeMillis() / 1000);
            }
        }).assignTimestampsAndWatermarks(new AscendingTimestampExtractor<ReceiptEvents>() {
            @Override
            public long extractAscendingTimestamp(ReceiptEvents element) {
                return element.getTimestamp() * 1000;
            }
        }).keyBy(new KeySelector<ReceiptEvents, String>() {
            @Override
            public String getKey(ReceiptEvents value) throws Exception {
                return value.getOrId();
            }
        });

        // connect兩條流
        SingleOutputStreamOperator<String> process = orderStream.connect(receiptStream).process(new MyCoProcessFunction());

        // 輸出正常交易的數(shù)據(jù)
        process.print("success");
        // 輸出異常交易的數(shù)據(jù)
        process.getSideOutput(payEventTag).print("payEventTag");
        process.getSideOutput(receiptEventTag).print("receiptEventTag");

        env.execute("Tx Match job");
    }

上面代碼的主要邏輯是:

  • 從端口為8888和9999的兩個socket讀取訂單事件和交易事件(模擬從kafka消費),然后將事件數(shù)據(jù)包裝成OrderEvents和ReceiptEvents条篷。

  • 提取事件時間骗随。

  • 對于OrderEvents,只需要action=pay的數(shù)據(jù)赴叹,過濾無用的數(shù)據(jù)鸿染。

  • 將兩條流根據(jù)orId keyby,生成orderStream和receiptStream乞巧,并通過connect合并兩條流涨椒,將合并后的結(jié)果,交給CoProcessFunction函數(shù)計算。

  • 將正常交易事件輸出在success中蚕冬,異常的交易事件免猾,輸出到兩個側(cè)輸出流中。

所以囤热,我們需要自定義聚合函數(shù)猎提,繼承CoProcessFunction函數(shù),實現(xiàn)正常交易和異常交易行為的實時計算旁蔼。

class MyCoProcessFunction 
        extends CoProcessFunction<OrderEvents, ReceiptEvents, String> {

    // 定義測輸出流锨苏,輸出只有pay事件沒有receipt事件的異常信息
    OutputTag payEventTag = new OutputTag<String>("payEventTag-side") {};
    // 定義測輸出流,輸出只有receipt事件沒有pay事件的異常信息
    OutputTag receiptEventTag = new OutputTag<String>("receiptEventTag-side") {};

    // 定義狀態(tài),保存訂單pay事件和交易事件
    ValueState<OrderEvents> payEventValueState = null;
    ValueState<ReceiptEvents> receiptEventValueState = null;

    @Override
    public void open(Configuration parameters) throws Exception {
        ValueStateDescriptor<OrderEvents> descriptor1
                = new ValueStateDescriptor<OrderEvents>("payEventValueState", OrderEvents.class);
        ValueStateDescriptor<ReceiptEvents> descriptor2
                = new ValueStateDescriptor<ReceiptEvents>("receiptEventValueState", ReceiptEvents.class);
        payEventValueState = getRuntimeContext().getState(descriptor1);
        receiptEventValueState = getRuntimeContext().getState(descriptor2);
    }

    // 處理OrderEvents事件
    @Override
    public void processElement1(OrderEvents orderEvents, Context ctx, Collector<String> out) throws Exception {
        if (receiptEventValueState.value() != null) {
            // 正常輸出匹配
            out.collect("訂單事件:"+orderEvents.toString() + "和交易事件:" + receiptEventValueState.value().toString());
            receiptEventValueState.clear();
            payEventValueState.clear();
        } else {
            // 如果沒有到賬事件棺聊,注冊定時器等待
            payEventValueState.update(orderEvents);
            ctx.timerService().registerEventTimeTimer(orderEvents.getTimestamp() * 1000 + 5000L); // 5s
        }
    }

    // 處理receipt事件
    @Override
    public void processElement2(ReceiptEvents receiptEvents, Context ctx, Collector<String> out) throws Exception {
        if (payEventValueState.value() != null) {
            // 正常輸出
            out.collect("訂單事件:"+payEventValueState.value().toString() + "和交易事件:" + receiptEvents.toString()+" 屬于正常交易");
            receiptEventValueState.clear();
            payEventValueState.clear();
        } else {
            // 如果沒有訂單事件伞租,說明是亂序事件,注冊定時器等待
            receiptEventValueState.update(receiptEvents);
            ctx.timerService().registerEventTimeTimer(receiptEvents.getTimestamp() * 1000 + 3000L); // 3s
        }
    }

    // 定時器
    @Override
    public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
        // 判斷哪個狀態(tài)存在限佩,表示另一個事件沒有來
        if (payEventValueState.value() != null) {
            ctx.output(payEventTag, payEventValueState.value().toString() + " 有pay事件沒有receipt事件葵诈,屬于異常事件");
        }

        if (receiptEventValueState.value() != null) {
            ctx.output(receiptEventTag, receiptEventValueState.value().toString() + " 有receipt事件沒有pay事件。屬于異常事件");
        }
        receiptEventValueState.clear();
        payEventValueState.clear();
    }
}

上述代碼是我們自定義的窗口函數(shù)犀暑,主要的功能是:

  • 繼承了CoProcessFunction驯击,分別在processElement1和procossElement2方法中處理orderEvents和receiptEvent烁兰。

  • 定義狀態(tài)耐亏,側(cè)輸出流,注冊定時器沪斟,通過一些邏輯計算是是正常交易還是異常交易广辰。

  • 在processElement1方法中,如果只有pay事件沒有receipt事件主之,則注冊一個5s后觸發(fā)的定時器择吊,等待receipt事件的到來,如果5s后receipt事件仍沒有到來槽奕,則說明是一個異常交易事件几睛,觸發(fā)timer并將異常事件輸出到側(cè)輸出流中。

  • 在processElement2方法中粤攒,如果只有receipt事件沒有pay事件所森,表明pay事件和receipt事件亂序,則注冊一個3s的定時器夯接,等待pay事件焕济。如果3s后還是沒有pay事件到達,則觸發(fā)timer將延遲的亂序數(shù)據(jù)輸出到側(cè)輸出流中盔几。

  • 定義定時器timer晴弃,對于異常的交易行為,將交易輸出輸出到側(cè)輸出流。異常交易是指上鞠,在一定時間范圍內(nèi)际邻,只有pay事件沒有receipt事件 或 只有receipt事件沒有pay事件。如果在一定時間范圍內(nèi)這兩個事件都有芍阎,則屬于正常交易行為枯怖。

打開兩個socket,輸入數(shù)據(jù)模擬交易行為能曾。為了輸出一些異常信息度硝,我們的輸入方式,不光要正常輸入數(shù)據(jù)寿冕,還要輸入一些亂序的數(shù)據(jù)蕊程,比如只輸入payEvent不輸入receiptEvent等,使之觸發(fā)timer驼唱。

輸入訂單事件

nc -lk 8888
1,pay,orderId01
2,pay,orderId02
3,pay,orderId03
4,pay,orderId04
6,pay,orderId06
7,pay,orderId07
8,pay,orderId0

輸入交易事件

nc -lk 9999
orderId01,wechat
orderId03,alipay
orderId04,wechat
orderId05,alipay
orderId06,wechat
orderId08,alipa

控制臺輸出:

success> 訂單事件:OrderEvents{userId=1, action='pay', orId='orderId01', timestamp=1606555301}和交易事件:ReceiptEvents{orId='orderId01', payEquipment='wechat', timestamp=1606555307} 屬于正常交易
success> 訂單事件:OrderEvents{userId=3, action='pay', orId='orderId03', timestamp=1606555318}和交易事件:ReceiptEvents{orId='orderId03', payEquipment='alipay', timestamp=1606555325} 屬于正常交易
payEventTag> OrderEvents{userId=2, action='pay', orId='orderId02', timestamp=1606555313} 有pay事件沒有receipt事件藻茂,屬于異常事件
success> 訂單事件:OrderEvents{userId=4, action='pay', orId='orderId04', timestamp=1606555332}和交易事件:ReceiptEvents{orId='orderId04', payEquipment='wechat', timestamp=1606555338} 屬于正常交易
success> 訂單事件:OrderEvents{userId=6, action='pay', orId='orderId06', timestamp=1606555351}和交易事件:ReceiptEvents{orId='orderId06', payEquipment='wechat', timestamp=1606555358} 屬于正常交易
receiptEventTag> ReceiptEvents{orId='orderId05', payEquipment='alipay', timestamp=1606555345} 有receipt事件沒有pay事件。屬于異常事件
success> 訂單事件:OrderEvents{userId=8, action='pay', orId='orderId08', timestamp=1606555375}和交易事件:ReceiptEvents{orId='orderId08', payEquipment='alipay', timestamp=1606555382} 屬于正常交易
payEventTag> OrderEvents{userId=7, action='pay', orId='orderId07', timestamp=1606555368} 有pay事件沒有receipt事件玫恳,屬于異常事件
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辨赐,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子京办,更是在濱河造成了極大的恐慌掀序,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惭婿,死亡現(xiàn)場離奇詭異不恭,居然都是意外死亡,警方通過查閱死者的電腦和手機财饥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門换吧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人钥星,你說我怎么就攤上這事沾瓦。” “怎么了谦炒?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵贯莺,是天一觀的道長。 經(jīng)常有香客問我编饺,道長乖篷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任透且,我火速辦了婚禮撕蔼,結(jié)果婚禮上豁鲤,老公的妹妹穿的比我還像新娘。我一直安慰自己鲸沮,他們只是感情好琳骡,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著讼溺,像睡著了一般楣号。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上怒坯,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天炫狱,我揣著相機與錄音,去河邊找鬼剔猿。 笑死视译,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的归敬。 我是一名探鬼主播酷含,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼汪茧!你這毒婦竟也來了椅亚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤舱污,失蹤者是張志新(化名)和其女友劉穎呀舔,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體慌闭,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡别威,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了驴剔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡粥庄,死狀恐怖丧失,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情惜互,我是刑警寧澤布讹,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站训堆,受9級特大地震影響描验,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜坑鱼,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一膘流、第九天 我趴在偏房一處隱蔽的房頂上張望絮缅。 院中可真熱鬧,春花似錦呼股、人聲如沸耕魄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吸奴。三九已至,卻和暖如春缠局,著一層夾襖步出監(jiān)牢的瞬間则奥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工狭园, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留逞度,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓妙啃,卻偏偏與公主長得像档泽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子揖赴,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容