10.Flink實時專案之訂單維度表關聯

選手一號位發表於2022-03-31

1. 維度查詢

在上一篇中,我們已經把訂單和訂單明細表join完,本文將關聯訂單的其他維度資料,維度關聯實際上就是在流中查詢儲存在 hbase 中的資料表。但是即使通過主鍵的方式查詢,hbase 速度的查詢也是不及流之間的 join。外部資料來源的查詢常常是流式計算的效能瓶頸,所以我們在查詢hbase維度資料的基礎上做一些優化及封裝。

phoenix查詢封裝

phoenix作為hbase的一個上層sql封裝,或者叫做皮膚,可以使用標準的sql語法來使用hbase,我們做一些簡單的查詢hbase的工具類。

import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSONObject;
import com.zhangbao.gmall.realtime.common.GmallConfig;
import org.apache.commons.beanutils.BeanUtils;
​
import java.io.PrintStream;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
 * @author zhangbaohpu
 * @date 2021/11/13 21:26
 * @desc phoenix 工具類,操作hbase資料
 */
public class PhoenixUtil {
​
    private static Connection conn = null;
​
    public static void init(){
        try {
            Class.forName(GmallConfig.PHOENIX_DRIVER);
            conn = DriverManager.getConnection(GmallConfig.PHOENIX_SERVER);
            conn.setSchema(GmallConfig.HBASE_SCHEMA);
        }catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException("連線phoenix失敗 -> " + e.getMessage());
        }
    }
​
    public static <T> List<T> getList(String sql, Class<T> clazz){
        if(conn == null){
            init();
        }
        PreparedStatement ps = null;
        ResultSet rs = null;
        List<T> resultList = new ArrayList<>();
        try {
            //獲取資料庫物件
            ps = conn.prepareStatement(sql);
            //執行sql語句
            rs = ps.executeQuery();
            //獲取後設資料
            ResultSetMetaData metaData = rs.getMetaData();
            while (rs.next()){
                //建立物件
                T rowObj = clazz.newInstance();
                //動態給物件賦值
                for (int i = 1; i <= metaData.getColumnCount(); i++) {
                    BeanUtils.setProperty(rowObj,metaData.getColumnName(i),rs.getObject(i));
                }
                resultList.add(rowObj);
            }
        }catch (Exception e){
            throw new RuntimeException("phoenix 查詢失敗 -> " + e.getMessage());
        }finally {
            if(rs!=null){
                try {
                    rs.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
            if(ps!=null){
                try {
                    ps.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
            if(conn!=null){
                try {
                    conn.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
        }
        return resultList;
    }
​
    public static void main(String[] args) {
        String sql = "select * from GMALL_REALTIME.BASE_TRADEMARK";
        System.out.println(getList(sql,JSONObject.class));
    }
}

有了對hbase的查詢,我們再對維度資料的查詢做一個封裝,根據某個表的id查詢維度資料。

import com.alibaba.fastjson.JSONObject;
import org.apache.flink.api.java.tuple.Tuple2;
​
import java.util.List;
/**
 * @author zhangbaohpu
 * @date 2021/11/13 22:24
 * @desc 維度查詢封裝,底層呼叫PhoenixUtil
 */
public class DimUtil {
    //直接從 Phoenix 查詢,沒有快取
    public static JSONObject getDimInfoNoCache(String tableName, Tuple2<String, String>...
            colNameAndValue) {
        //組合查詢條件
        String wheresql = new String(" where ");
        for (int i = 0; i < colNameAndValue.length; i++) {
            //獲取查詢列名以及對應的值
            Tuple2<String, String> nameValueTuple = colNameAndValue[i];
            String fieldName = nameValueTuple.f0;
            String fieldValue = nameValueTuple.f1;
            if (i > 0) {
                wheresql += " and ";
            }
            wheresql += fieldName + "='" + fieldValue + "'";
        }
        //組合查詢 SQL
        String sql = "select * from " + tableName + wheresql;
        System.out.println("查詢維度 SQL:" + sql);
        JSONObject dimInfoJsonObj = null;
        List<JSONObject> dimList = PhoenixUtil.getList(sql, JSONObject.class);
        if (dimList != null && dimList.size() > 0) {
            //因為關聯維度,肯定都是根據 key 關聯得到一條記錄
            dimInfoJsonObj = dimList.get(0);
        }else{
            System.out.println("維度資料未找到:" + sql);
        }
        return dimInfoJsonObj;
    }
    public static void main(String[] args) {
        JSONObject dimInfooNoCache = DimUtil.getDimInfoNoCache("base_trademark",
                Tuple2.of("id", "13"));
        System.out.println(dimInfooNoCache);
    }
}

2. 優化1:加入旁路快取模式

我們在上面實現的功能中,直接查詢的 Hbase。外部資料來源的查詢常常是流式計算的效能瓶頸,所以我們需要在上面實現的基礎上進行一定的優化。我們這裡使用旁路快取。

旁路快取模式是一種非常常見的按需分配快取的模式。如下圖,任何請求優先訪問快取,快取命中,直接獲得資料返回請求。如果未命中則,查詢資料庫,同時把結果寫入快取以備後續請求使用。

10.Flink實時專案之訂單維度表關聯

1) 這種快取策略有幾個注意點

快取要設過期時間,不然冷資料會常駐快取浪費資源。

要考慮維度資料是否會發生變化,如果發生變化要主動清除快取。

2) 快取的選型

一般兩種:堆快取或者獨立快取服務(redis,memcache),

 

堆快取,從效能角度看更好,畢竟訪問資料路徑更短,減少過程消耗。但是管理性差,其他程式無法維護快取中的資料。

獨立快取服務(redis,memcache)本身效能也不錯,不過會有建立連線、網路 IO 等 消耗。但是考慮到資料如果會發生變化,那還是獨立快取服務管理性更強,而且如果資料量特別大,獨立快取更容易擴充套件。

因為我們們的維度資料都是可變資料,所以這裡還是採用 Redis 管理快取。

程式碼優化

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.flink.api.java.tuple.Tuple2;
import redis.clients.jedis.Jedis;
import java.util.List;
/**
 * @author zhangbaohpu
 * @date 2021/11/13 22:24
 * @desc 維度查詢封裝,底層呼叫PhoenixUtil
 */
public class DimUtil {
​
    /**
     * 查詢優化
     * redis快取
     *      型別  string  list set zset hash
     * 這裡使用key格式:
     *      key dim:table_name:value  示例:dim:base_trademark:13
     *      value   json字串
     *      過期時間:24*3600
     */
​
    public static JSONObject getDimInfo(String tableName, Tuple2<String, String>...
            colNameAndValue) {
​
        //組合查詢條件
        String wheresql = new String(" where ");
        //redis key
        String redisKey = "dim:"+tableName+":";
        for (int i = 0; i < colNameAndValue.length; i++) {
            //獲取查詢列名以及對應的值
            Tuple2<String, String> nameValueTuple = colNameAndValue[i];
            String fieldName = nameValueTuple.f0;
            String fieldValue = nameValueTuple.f1;
            if (i > 0) {
                wheresql += " and ";
                redisKey += "_";
            }
            wheresql += fieldName + "='" + fieldValue + "'";
            redisKey += fieldValue;
        }
        Jedis jedis = null;
        String redisStr = null;
        JSONObject dimInfoJsonObj = null;
        try {
            jedis = RedisUtil.getJedis();
            redisStr = jedis.get(redisKey);
            dimInfoJsonObj = null;
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("獲取redis資料錯誤");
        }
​
        if(redisStr!=null && redisStr.length()>0){
            dimInfoJsonObj = JSON.parseObject(redisStr);
        }else {
            //從phoenix中去資料
            //組合查詢 SQL
            String sql = "select * from " + tableName + wheresql;
            System.out.println("查詢維度 SQL:" + sql);
​
            List<JSONObject> dimList = PhoenixUtil.getList(sql, JSONObject.class);
            if (dimList != null && dimList.size() > 0) {
                //因為關聯維度,肯定都是根據 key 關聯得到一條記錄
                dimInfoJsonObj = dimList.get(0);
                if(jedis!=null){
                    jedis.setex(redisKey,3600*24,dimInfoJsonObj.toString());
                }
            }else{
                System.out.println("維度資料未找到:" + sql);
            }
        }
        //關閉jedis
        if(jedis!=null){
            jedis.close();
        }
​
        return dimInfoJsonObj;
    }
​
    public static JSONObject getDimInfoNoCacheById(String tableName, String idValue) {
        return getDimInfoNoCache(tableName,new Tuple2<>("id",idValue));
    }
    //直接從 Phoenix 查詢,沒有快取
    public static JSONObject getDimInfoNoCache(String tableName, Tuple2<String, String>...
            colNameAndValue) {
        //組合查詢條件
        String wheresql = new String(" where ");
        for (int i = 0; i < colNameAndValue.length; i++) {
            //獲取查詢列名以及對應的值
            Tuple2<String, String> nameValueTuple = colNameAndValue[i];
            String fieldName = nameValueTuple.f0;
            String fieldValue = nameValueTuple.f1;
            if (i > 0) {
                wheresql += " and ";
            }
            wheresql += fieldName + "='" + fieldValue + "'";
        }
        //組合查詢 SQL
        String sql = "select * from " + tableName + wheresql;
        System.out.println("查詢維度 SQL:" + sql);
        JSONObject dimInfoJsonObj = null;
        List<JSONObject> dimList = PhoenixUtil.getList(sql, JSONObject.class);
        if (dimList != null && dimList.size() > 0) {
            //因為關聯維度,肯定都是根據 key 關聯得到一條記錄
            dimInfoJsonObj = dimList.get(0);
        }else{
            System.out.println("維度資料未找到:" + sql);
        }
        return dimInfoJsonObj;
    }
    public static void main(String[] args) {
        JSONObject dimInfooNoCache = DimUtil.getDimInfoNoCache("base_trademark",
                Tuple2.of("id", "13"));
        System.out.println(dimInfooNoCache);
    }
}

快取依賴於redisUtil.java工具類

import redis.clients.jedis.*;
/**
 * @author zhangbaohpu
 * @date 2021/11/13 23:31
 * @desc
 */
public class RedisUtil {
    public static JedisPool jedisPool=null;
    public static Jedis getJedis(){
        if(jedisPool==null){
            JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(100); //最大可用連線數
            jedisPoolConfig.setBlockWhenExhausted(true); //連線耗盡是否等待
            jedisPoolConfig.setMaxWaitMillis(2000); //等待時間
            jedisPoolConfig.setMaxIdle(5); //最大閒置連線數
            jedisPoolConfig.setMinIdle(5); //最小閒置連線數
            jedisPoolConfig.setTestOnBorrow(true); //取連線的時候進行一下測試 ping pong
            jedisPool=new JedisPool( jedisPoolConfig, "hadoop101",6379 ,1000);
            System.out.println("開闢連線池");
            return jedisPool.getResource();
        }else{
            System.out.println(" 連線池:"+jedisPool.getNumActive());
            return jedisPool.getResource();
        }
    }
​
    public static void main(String[] args) {
        Jedis jedis = getJedis();
        System.out.println(jedis.ping());
    }
}

維度資料發生變化

如果維度資料發生了變化,這時快取的資料就不是最新的了,所以這裡優化將發生變化的維度資料,在快取中清除。

在DimUtil.java加入清除快取方法

//根據 key 讓 Redis 中的快取失效
public static void deleteCached( String tableName, String id){
    String key = "dim:" + tableName.toLowerCase() + ":" + id;
    try {
        Jedis jedis = RedisUtil.getJedis();
        // 通過 key 清除快取
        jedis.del(key);
        jedis.close();
    } catch (Exception e) {
        System.out.println("快取異常!");
        e.printStackTrace();
    }
}

另外一個,在實時同步mysql資料BaseDbTask任務中,將維度資料通過DimSink.java放入hbase,在invoke方法中新增清除快取操作

@Override
public void invoke(JSONObject jsonObject, Context context) throws Exception {
    String sinkTable = jsonObject.getString("sink_table");
    JSONObject data = jsonObject.getJSONObject("data");
    PreparedStatement ps = null;
    if(data!=null && data.size()>0){
        try {
            //生成phoenix的upsert語句,這個包含insert和update操作
            String sql = generateUpsert(data,sinkTable.toUpperCase());
            log.info("開始執行 phoenix sql -->{}",sql);
            ps = conn.prepareStatement(sql);
            ps.executeUpdate();
            conn.commit();
            log.info("執行 phoenix sql 成功");
        } catch (SQLException throwables) {
            throwables.printStackTrace();
            throw new RuntimeException("執行 phoenix sql 失敗!");
        }finally {
            if(ps!=null){
                ps.close();
            }
        }
        //如果是更新維度資料,則把redis資料清空
        if(jsonObject.getString("type").endsWith("update")){
            DimUtil.deleteCached(sinkTable,data.getString("id"));
        }
    }
}

3. 優化2:非同步查詢

在 Flink 流處理過程中,經常需要和外部系統進行互動,用維度表補全事實表中的欄位。例如:在電商場景中,需要一個商品的 skuid 去關聯商品的一些屬性,例如商品所屬行業、商品的生產廠家、生產廠家的一些情況;在物流場景中,知道包裹 id,需要去關聯包裹的行業屬性、發貨資訊、收貨資訊等等。

預設情況下,在 Flink 的 MapFunction 中,單個並行只能用同步方式去互動: 將請求傳送到外部儲存,IO 阻塞,等待請求返回,然後繼續傳送下一個請求。這種同步互動的方式往往在網路等待上就耗費了大量時間。為了提高處理效率,可以增加 MapFunction 的並行度,但增加並行度就意味著更多的資源,並不是一種非常好的解決方式。

Flink 在 1.2 中引入了 Async I/O,在非同步模式下,將 IO 操作非同步化,單個並行可以連續傳送多個請求,哪個請求先返回就先處理,從而在連續的請求間不需要阻塞式等待,大大提高了流處理效率。

Async I/O 是阿里巴巴貢獻給社群的一個呼聲非常高的特性,解決與外部系統互動時網路延遲成為了系統瓶頸的問題。

10.Flink實時專案之訂單維度表關聯

 

非同步查詢實際上是把維表的查詢操作託管給單獨的執行緒池完成,這樣不會因為某一個查詢造成阻塞,單個並行可以連續傳送多個請求,提高併發效率。

這種方式特別針對涉及網路 IO 的操作,減少因為請求等待帶來的消耗。

flink非同步查詢官方文件:

https://nightlies.apache.org/flink/flink-docs-release-1.14/zh/docs/dev/datastream/operators/asyncio/#%e5%bc%82%e6%ad%a5-io-api

3.1 封裝執行緒池工具

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
 * @author zhangbaohpu
 * @date 2021/11/28 12:18
 * @desc 執行緒池工具類
 * 
 */
public class ThreadPoolUtil {
    private static ThreadPoolExecutor poolExecutor;
​
    /**
     * 獲取單例的執行緒池物件
     *  corePoolSize:指定了執行緒池中的執行緒數量,它的數量決定了新增的任務是開闢新的執行緒去執行,還是放到 workQueue任務佇列中去;
     *  maximumPoolSize:指定了執行緒池中的最大執行緒數量,這個引數會根據你使用的 workQueue 任務佇列的型別,決定執行緒池會開闢的最大執行緒數量;
     *  keepAliveTime:當執行緒池中空閒執行緒數量超過 corePoolSize 時,多餘的執行緒會在多長時間內被銷燬;
     *  unit:keepAliveTime 的單位
     *  workQueue:任務佇列,被新增到執行緒池中,但尚未被執行的任務
     * @return
     */
    public static ThreadPoolExecutor getPoolExecutor(){
        if (poolExecutor == null){
            synchronized (ThreadPoolUtil.class){
                if (poolExecutor == null){
                    poolExecutor = new ThreadPoolExecutor(
                            4,20,300, TimeUnit.SECONDS,new LinkedBlockingDeque<>(Integer.MAX_VALUE)
                    );
                }
            }
        }
        return poolExecutor;
    }
}

3.2 自定義維度介面

這個非同步維表查詢的方法適用於各種維表的查詢,用什麼條件查,查出來的結果如何合併到資料流物件中,需要使用者自己定義。

這就是自己定義了一個介面 DimJoinFunction<T>包括兩個方法。

import com.alibaba.fastjson.JSONObject;
​
/**
 * @author zhangbaohpu
 * @date 2021/11/28 12:34
 * @desc  維度關聯介面
 */
public interface DimJoinFunction<T> {
​
    //根據流中獲取主鍵
    String getKey(T obj);
​
    //維度關聯
    void join(T stream, JSONObject dimInfo);
}

3.3 封裝維度非同步查詢類

新建包func下建立DimAsyncFunction.java,該類繼承非同步方法類 RichAsyncFunction,實現自定義維度查詢介面,其中 RichAsyncFunction<IN,OUT>是 Flink 提供的非同步方法類,此處因為是查詢操作輸入類和返回類一致,所以是<T,T>。

RichAsyncFunction 這個類要實現兩個方法:

  • open 用於初始化非同步連線池。

  • asyncInvoke 方法是核心方法,裡面的操作必須是非同步的,如果你查詢的資料庫有非同步api 也可以用執行緒的非同步方法,如果沒有非同步方法,就要自己利用執行緒池等方式實現非同步查詢。

import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.zhangbao.gmall.realtime.utils.DimUtil;
import com.zhangbao.gmall.realtime.utils.ThreadPoolUtil;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.async.ResultFuture;
import org.apache.flink.streaming.api.functions.async.RichAsyncFunction;
​
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
​
/**
 * @author zhangbaohpu
 * @date 2021/11/28 12:24
 * @desc 通用的維度關聯查詢介面
 * 模板方法設計模式
 *   在父類中只定義方法的宣告
 *   具體實現由子類完成
 */
public abstract class DimAsyncFunction<T> extends RichAsyncFunction<T,T> implements DimJoinFunction<T> {
​
    private String tableName;
    
    private static ExecutorService executorPool;
​
    public DimAsyncFunction(String tableName) {
        this.tableName = tableName;
    }
​
    @Override
    public void open(Configuration parameters) throws Exception {
        //初始化執行緒池
        executorPool = ThreadPoolUtil.getPoolExecutor();
    }
​
    @Override
    public void asyncInvoke(T obj, ResultFuture<T> resultFuture) throws Exception {
        executorPool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    long start = System.currentTimeMillis();
                    String key = getKey(obj);
                    //獲取維度資訊
                    JSONObject dimInfoJsonObj = DimUtil.getDimInfo(tableName, key);
​
                    //關聯維度
                    if (dimInfoJsonObj != null){
                        join(obj,dimInfoJsonObj);
                    }
                    long end = System.currentTimeMillis();
                    System.out.println("關聯維度資料,耗時:"+(end - start)+" 毫秒。");
                    resultFuture.complete(Arrays.asList(obj));
                } catch (Exception e) {
                    e.printStackTrace();
                    throw new RuntimeException(tableName+"維度查詢失敗");
                }
            }
        });
    }
}

3.4 新增到主任務

將維度資料加入到訂單寬表任務中,在訂單寬表任務中OrderWideApp.java,完成對訂單明細的雙流join後,將使用者維度資料關聯到訂單寬表中。

/**
 * 關聯使用者維度資料
 * flink非同步查詢
 * https://nightlies.apache.org/flink/flink-docs-release-1.14/zh/docs/dev/datastream/operators/asyncio/#%e5%bc%82%e6%ad%a5-io-api
 */
SingleOutputStreamOperator<OrderWide> orderWideWithUserDs = AsyncDataStream.unorderedWait(orderWideDs, new DimAsyncFunction<OrderWide>("DIM_USER_INFO") {
    @Override
    public String getKey(OrderWide obj) {
        return obj.getOrder_id().toString();
    }
​
    @Override
    public void join(OrderWide orderWide, JSONObject dimInfo) {
        Date birthday = dimInfo.getDate("BIRTHDAY");
        Long age = DateUtil.betweenYear(birthday, new Date(), false);
        orderWide.setUser_age(age.intValue());
        orderWide.setUser_gender(dimInfo.getString("GENDER"));
    }
}, 60, TimeUnit.SECONDS);
​
orderWideWithUserDs.print("order wide with users >>>");

3.5 測試

開啟的服務:zk,kf,redis,hdfs,hbase,maxwell,BaseDbTask.java

注:要清除的資料

  • mysql配置表,之前手動加的配置表刪除,通過指令碼執行要同步的表

    /*Data for the table `table_process` */
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('activity_info', 'insert', 'hbase', 'dim_activity_info', 'id,activity_name,activity_type,activity_desc,start_time,end_time,create_time', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('activity_info', 'update', 'hbase', 'dim_activity_info', 'id,activity_name,activity_type,activity_desc,start_time,end_time,create_time', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('activity_rule', 'insert', 'hbase', 'dim_activity_rule', 'id,activity_id,activity_type,condition_amount,condition_num,benefit_amount,benefit_discount,benefit_level', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('activity_rule', 'update', 'hbase', 'dim_activity_rule', 'id,activity_id,activity_type,condition_amount,condition_num,benefit_amount,benefit_discount,benefit_level', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('activity_sku', 'insert', 'hbase', 'dim_activity_sku', 'id,activity_id,sku_id,create_time', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('activity_sku', 'update', 'hbase', 'dim_activity_sku', 'id,activity_id,sku_id,create_time', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_category1', 'insert', 'hbase', 'dim_base_category1', 'id,name', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_category1', 'update', 'hbase', 'dim_base_category1', 'id,name', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_category2', 'insert', 'hbase', 'dim_base_category2', 'id,name,category1_id', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_category2', 'update', 'hbase', 'dim_base_category2', 'id,name,category1_id', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_category3', 'insert', 'hbase', 'dim_base_category3', 'id,name,category2_id', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_category3', 'update', 'hbase', 'dim_base_category3', 'id,name,category2_id', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_dic', 'insert', 'hbase', 'dim_base_dic', 'id,dic_name,parent_code,create_time,operate_time', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_dic', 'update', 'hbase', 'dim_base_dic', 'id,dic_name,parent_code,create_time,operate_time', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_province', 'insert', 'hbase', 'dim_base_province', 'id,name,region_id,area_code,iso_code,iso_3166_2', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_province', 'update', 'hbase', 'dim_base_province', 'id,name,region_id,area_code,iso_code,iso_3166_2', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_region', 'insert', 'hbase', 'dim_base_region', 'id,region_name', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_region', 'update', 'hbase', 'dim_base_region', 'id,region_name', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_trademark', 'insert', 'hbase', 'dim_base_trademark', 'id,tm_name', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('base_trademark', 'update', 'hbase', 'dim_base_trademark', 'id,tm_name', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('cart_info', 'insert', 'kafka', 'dwd_cart_info', 'id,user_id,sku_id,cart_price,sku_num,img_url,sku_name,is_checked,create_time,operate_time,is_ordered,order_time,source_type,source_id', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('comment_info', 'insert', 'kafka', 'dwd_comment_info', 'id,user_id,nick_name,head_img,sku_id,spu_id,order_id,appraise,comment_txt,create_time,operate_time', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('coupon_info', 'insert', 'hbase', 'dim_coupon_info', 'id,coupon_name,coupon_type,condition_amount,condition_num,activity_id,benefit_amount,benefit_discount,create_time,range_type,limit_num,taken_count,start_time,end_time,operate_time,expire_time,range_desc', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('coupon_info', 'update', 'hbase', 'dim_coupon_info', 'id,coupon_name,coupon_type,condition_amount,condition_num,activity_id,benefit_amount,benefit_discount,create_time,range_type,limit_num,taken_count,start_time,end_time,operate_time,expire_time,range_desc', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('coupon_range', 'insert', 'hbase', 'dim_coupon_range', 'id,coupon_id,range_type,range_id', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('coupon_range', 'update', 'hbase', 'dim_coupon_range', 'id,coupon_id,range_type,range_id', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('coupon_use', 'insert', 'kafka', 'dwd_coupon_use', 'id,coupon_id,user_id,order_id,coupon_status,get_type,get_time,using_time,used_time,expire_time', 'id', ' SALT_BUCKETS = 3');
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('coupon_use', 'update', 'kafka', 'dwd_coupon_use', 'id,coupon_id,user_id,order_id,coupon_status,get_type,get_time,using_time,used_time,expire_time', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('favor_info', 'insert', 'kafka', 'dwd_favor_info', 'id,user_id,sku_id,spu_id,is_cancel,create_time,cancel_time', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('financial_sku_cost', 'insert', 'hbase', 'dim_financial_sku_cost', 'id,sku_id,sku_name,busi_date,is_lastest,sku_cost,create_time', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('financial_sku_cost', 'update', 'hbase', 'dim_financial_sku_cost', 'id,sku_id,sku_name,busi_date,is_lastest,sku_cost,create_time', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('order_detail', 'insert', 'kafka', 'dwd_order_detail', 'id,order_id,sku_id,sku_name,order_price,sku_num,create_time,source_type,source_id,split_activity_amount,split_coupon_amount,split_total_amount', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('order_detail_activity', 'insert', 'kafka', 'dwd_order_detail_activity', 'id,order_id,order_detail_id,activity_id,activity_rule_id,sku_id,create_time', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('order_detail_coupon', 'insert', 'kafka', 'dwd_order_detail_coupon', 'id,order_id,order_detail_id,coupon_id,coupon_use_id,sku_id,create_time', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('order_info', 'insert', 'kafka', 'dwd_order_info', 'id,consignee,consignee_tel,total_amount,order_status,user_id,payment_way,delivery_address,order_comment,out_trade_no,trade_body,create_time,operate_time,expire_time,process_status,tracking_no,parent_order_id,img_url,province_id,activity_reduce_amount,coupon_reduce_amount,original_total_amount,feight_fee,feight_fee_reduce,refundable_time', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('order_info', 'update', 'kafka', 'dwd_order_info_update', 'id,consignee,consignee_tel,total_amount,order_status,user_id,payment_way,delivery_address,order_comment,out_trade_no,trade_body,create_time,operate_time,expire_time,process_status,tracking_no,parent_order_id,img_url,province_id,activity_reduce_amount,coupon_reduce_amount,original_total_amount,feight_fee,feight_fee_reduce,refundable_time', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('order_refund_info', 'insert', 'kafka', 'dwd_order_refund_info', 'id,user_id,order_id,sku_id,refund_type,refund_num,refund_amount,refund_reason_type,refund_reason_txt,refund_status,create_time', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('payment_info', 'insert', 'kafka', 'dwd_payment_info', 'id,out_trade_no,order_id,user_id,payment_type,trade_no,total_amount,subject,payment_status,create_time,callback_time,callback_content', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('payment_info', 'update', 'kafka', 'dwd_payment_info', 'id,out_trade_no,order_id,user_id,payment_type,trade_no,total_amount,subject,payment_status,create_time,callback_time,callback_content', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('refund_payment', 'insert', 'kafka', 'dwd_refund_payment', 'id,out_trade_no,order_id,sku_id,payment_type,trade_no,total_amount,subject,refund_status,create_time,callback_time,callback_content', 'id', NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('refund_payment', 'update', 'kafka', 'dwd_refund_payment', 'id,out_trade_no,order_id,sku_id,payment_type,trade_no,total_amount,subject,refund_status,create_time,callback_time,callback_content', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('sku_info', 'insert', 'hbase', 'dim_sku_info', 'id,spu_id,price,sku_name,sku_desc,weight,tm_id,category3_id,sku_default_img,is_sale,create_time', 'id', ' SALT_BUCKETS = 4');
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('sku_info', 'update', 'hbase', 'dim_sku_info', 'id,spu_id,price,sku_name,sku_desc,weight,tm_id,category3_id,sku_default_img,is_sale,create_time', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('spu_info', 'insert', 'hbase', 'dim_spu_info', 'id,spu_name,description,category3_id,tm_id', 'id', ' SALT_BUCKETS = 3');
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('spu_info', 'update', 'hbase', 'dim_spu_info', 'id,spu_name,description,category3_id,tm_id', NULL, NULL);
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('user_info', 'insert', 'hbase', 'dim_user_info', 'id,login_name,name,user_level,birthday,gender,create_time,operate_time', 'id', ' SALT_BUCKETS = 3');
    INSERT INTO `table_process`(`source_table`, `operate_type`, `sink_type`, `sink_table`, `sink_columns`, `sink_pk`, `sink_extend`) VALUES ('user_info', 'update', 'hbase', 'dim_user_info', 'id,login_name,name,user_level,birthday,gender,create_time,operate_time', NULL, NULL);
    ​
  • hbase資料清除,重新建立維度表

    !tables:檢視所有表

    drop table GMALL_REALTIME.BASE_TRADEMARK;:刪除表

  • 初始化維度資料

    將使用者錶的歷史全量同步到hbase中,通過Maxwell的Bootstrap完成,Maxwell安裝及使用可檢視之前的文章。

    bin/maxwell-bootstrap --user maxwell --password 123456 --host hadoop101 --database gmall2021 --table user_info --client_id maxwell_1
  • 啟動模擬生成業務資料jar

經過測試,可以看到訂單寬表中使用者資訊的年齡及性別分別都有值。

4. 其他維度關聯

4.1 關聯省份維度

關聯省份維度和關聯使用者維度處理邏輯一樣,這裡就要以關聯使用者維度後的結果流為基礎,再去關聯省份

需要做的要先把省份的維度資料全同步到hbase,還是通過Maxwell完成

bin/maxwell-bootstrap --user maxwell --password 123456 --host hadoop101 --database gmall2021 --table base_province --client_id maxwell_1
/**
 * 關聯省份維度
 * 以上一個流為基礎,關聯省份資料
 */
SingleOutputStreamOperator<OrderWide> orderWideWithProvinceDs = AsyncDataStream.unorderedWait(orderWideWithUserDs,
        new DimAsyncFunction<OrderWide>("DIM_BASE_PROVINCE") {
            @Override
            public String getKey(OrderWide orderWide) {
                return orderWide.getProvince_id().toString();
            }
            @Override
            public void join(OrderWide orderWide, JSONObject dimInfo) {
                orderWide.setProvince_name(dimInfo.getString("NAME"));
                orderWide.setProvince_iso_code(dimInfo.getString("ISO_CODE"));
                orderWide.setProvince_area_code(dimInfo.getString("AREA_CODE"));
                orderWide.setProvince_3166_2_code(dimInfo.getString("ISO_3166_2"));
            }
        }, 60, TimeUnit.SECONDS);
orderWideWithProvinceDs.print("order wide with province>>>");

4.2 關聯sku維度

初始化sku維度資料

bin/maxwell-bootstrap --user maxwell --password 123456 --host hadoop101 --database gmall2021 --table sku_info --client_id maxwell_1
/**
 * 關聯sku資料
 */
SingleOutputStreamOperator<OrderWide> orderWideWithSkuDs = AsyncDataStream.unorderedWait(orderWideWithProvinceDs,
        new DimAsyncFunction<OrderWide>("DIM_SKU_INFO") {
            @Override
            public String getKey(OrderWide orderWide) {
                return orderWide.getSku_id().toString();
            }
​
            @Override
            public void join(OrderWide orderWide, JSONObject dimInfo) {
                orderWide.setSku_name(dimInfo.getString("SKU_NAME"));
                orderWide.setSpu_id(dimInfo.getLong("SPU_ID"));
                orderWide.setCategory3_id(dimInfo.getLong("CATEGORY3_ID"));
                orderWide.setTm_id(dimInfo.getLong("TM_ID"));
            }
        }, 60, TimeUnit.SECONDS);

 

4.3 關聯spu維度

初始化spu維度資料

bin/maxwell-bootstrap --user maxwell --password 123456 --host hadoop101 --database gmall2021 --table spu_info --client_id maxwell_1
/**
 * 關聯spu資料
 */
SingleOutputStreamOperator<OrderWide> orderWideWithSpuDs = AsyncDataStream.unorderedWait(orderWideWithSkuDs, new DimAsyncFunction<OrderWide>("DIM_SPU_INFO") {
    @Override
    public String getKey(OrderWide orderWide) {
        return orderWide.getSpu_id().toString();
    }
​
    @Override
    public void join(OrderWide orderWide, JSONObject dimInfo) {
        orderWide.setSpu_name(dimInfo.getString("SPU_NAME"));
​
    }
}, 60, TimeUnit.SECONDS);

 

4.4 關聯品類維度

初始化品類維度資料

bin/maxwell-bootstrap --user maxwell --password 123456 --host hadoop101 --database gmall2021 --table base_category3 --client_id maxwell_1
/**
 * 關聯品類資料
 */
​
SingleOutputStreamOperator<OrderWide> orderWideWithCategoryDs = AsyncDataStream.unorderedWait(orderWideWithSpuDs, new DimAsyncFunction<OrderWide>("DIM_BASE_CATEGORY3") {
    @Override
    public String getKey(OrderWide orderWide) {
        return orderWide.getCategory3_id().toString();
    }
​
    @Override
    public void join(OrderWide orderWide, JSONObject dimInfo) {
        orderWide.setCategory3_name(dimInfo.getString("NAME"));
    }
}, 60, TimeUnit.SECONDS);

 

4.5 關聯品牌維度

初始化品牌維度資料

bin/maxwell-bootstrap --user maxwell --password 123456 --host hadoop101 --database gmall2021 --table base_trademark --client_id maxwell_1
/**
 * 關聯品牌資料
 */
​
SingleOutputStreamOperator<OrderWide> orderWideWithTmDs = AsyncDataStream.unorderedWait(orderWideWithCategoryDs, new DimAsyncFunction<OrderWide>("DIM_BASE_TRADEMARK") {
    @Override
    public String getKey(OrderWide orderWide) {
        return orderWide.getTm_id().toString();
    }
​
    @Override
    public void join(OrderWide orderWide, JSONObject dimInfo) {
        orderWide.setTm_name(dimInfo.getString("TM_NAME"));
    }
}, 60, TimeUnit.SECONDS);
orderWideWithTmDs.print("order wide with sku_spu_category_tm >>> ");

5. 訂單寬表寫入kafka

/**
 * 將關聯後的訂單寬表資料傳送到kafka的dwm層
 */
orderWideWithTmDs.map(orderWide -> JSONObject.toJSONString(orderWide))
        .addSink(MyKafkaUtil.getKafkaSink(orderWideTopic));

專案地址:https://github.com/zhangbaohpu/gmall-flink-parent

更多請在某公號平臺搜尋:選手一號位,本文編號:1010,回覆即可獲取。

相關文章