在大数据的处理过程中会出现很多汇总类指标的计算,比如计算当日的每个类目下的用户的订单信息,就需要按类目分组,对用户做去重。Flink sql 提供了 “去重” 功能,可以在流模式的任务中做去重操作。

官网文档 去重 

官网链接: [去重](https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/dev/table/sql/queries.html#%E5%8E%BB%E9%87%8D)

** 注意 仅 Blink planner 支持去重。

去重是指对在列的集合内重复的行进行删除,只保留第一行或最后一行数据。 在某些情况下,上游的 ETL 作业不能实现精确一次的端到端,这将可能导致在故障恢复 时,sink 中有重复的记录。 由于重复的记录将影响下游分析作业的正确性(例如,SUM、COUNT), 所以在进一步分析之前需要进行数据去重。

与 Top-N 查询相似,Flink 使用 ROW_NUMBER() 去除重复的记录。理论上来说,去重是一个特殊的 Top-N 查询,其中 N 是 1 ,记录则是以处理时间或事件事件进行排序的。

以下代码展示了去重语句的语法:

SELECT [column_list]
FROM (
   SELECT [column_list],
     ROW_NUMBER() OVER ([PARTITION BY col1[, col2...]]
       ORDER BY time_attr [asc|desc]) AS rownum
   FROM table_name)
WHERE rownum = 1

参数说明:

ROW_NUMBER(): 从第一行开始,依次为每一行分配一个唯一且连续的号码。
PARTITION BY col1[, col2...]: 指定分区的列,例如去重的键。
ORDER BY time_attr [asc|desc]: 指定排序的列。所指定的列必须为 时间属性, 目前 Flink 支持 处理时间属性 和 事件时间属性 。升序( ASC )排列指只保留第一行,而降序排列( DESC )则指保留最后一行。
WHERE rownum = 1: Flink 需要 rownum = 1 以确定该查询是否为去重查询。
以下的例子描述了如何指定 SQL 查询以在一个流计算表中进行去重操作。

val env = StreamExecutionEnvironment.getExecutionEnvironment
val tableEnv = TableEnvironment.getTableEnvironment(env)

// 从外部数据源读取 DataStream
val ds: DataStream[(String, String, String, Int)] = env.addSource(...)
// 注册名为 “Orders” 的 DataStream
tableEnv.createTemporaryView("Orders", ds, $"order_id", $"user", $"product", $"number", $"proctime".proctime)

// 由于不应该出现两个订单有同一个order_id,所以根据 order_id 去除重复的行,并保留第一行
val result1 = tableEnv.sqlQuery(
    """
      |SELECT order_id, user, product, number
      |FROM (
      |   SELECT *,
      |       ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY proctime DESC) as row_num
      |   FROM Orders)
      |WHERE row_num = 1
    """.stripMargin)

** 注以上内容来自官网

demo

默认模式和 mini-batch 模式下都支持去重,但是默认模型是基于全局的,mini-batch 是基于 mini-batch 配置的
默认模式 下,去重是基于全局的,要么只输出第一条数据( asc 升序,第一条数据 rownum=1),要么全部输出(desc 降序,依次来的每条数据 rownum 都是 1)
mini-batch 模式下,数据是基于批次生效

去重 sql 如下:

--- 去重查询
-- kafka source
CREATE TABLE user_log (
  user_id VARCHAR
  ,item_id VARCHAR
  ,category_id VARCHAR
  ,behavior INT
  ,ts TIMESTAMP(3)
  ,process_time as proctime()
  , WATERMARK FOR ts AS ts
) WITH (
  'connector' = 'kafka'
  ,'topic' = 'user_behavior'
  ,'properties.bootstrap.servers' = 'localhost:9092'
  ,'properties.group.id' = 'user_log'
  ,'scan.startup.mode' = 'group-offsets'
  ,'format' = 'json'
);

---sink table
CREATE TABLE user_log_sink (
  user_id VARCHAR
  ,item_id VARCHAR
  ,category_id VARCHAR
  ,behavior INT
  ,ts TIMESTAMP(3)
  ,num BIGINT
  ,primary key (user_id) not enforced
) WITH (
'connector' = 'upsert-kafka'
  ,'topic' = 'user_behavior_sink'
  ,'properties.bootstrap.servers' = 'localhost:9092'
  ,'properties.group.id' = 'user_log'
  ,'key.format' = 'json'
  ,'key.json.ignore-parse-errors' = 'true'
  ,'value.format' = 'json'
  ,'value.json.fail-on-missing-field' = 'false'
  ,'value.fields-include' = 'ALL'
);

-- insert
insert into user_log_sink(user_id, item_id, category_id,behavior,ts,num)
SELECT user_id, item_id, category_id,behavior,ts,rownum
FROM (
   SELECT user_id, item_id, category_id,behavior,ts,
     ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY process_time desc) AS rownum -- desc use the latest one,
   FROM user_log)
WHERE rownum=1-- 只能使用 rownum=1,如果写 rownum=2(或<10),每个分区只会输出一条数据(小于是多条)rownum=2的,看起来基于全局去重了

mini-batch 配置参数

configuration.setString("table.exec.mini-batch.enabled", "true");
configuration.setString("table.exec.mini-batch.allow-latency", "5 s");
configuration.setString("table.exec.mini-batch.size", "5000");

 

去重是基于参数 "table.exec.mini-batch.allow-latency" 和 "table.exec.mini-batch.size" 生效,满足一个就算一个“批次”,每个批次每个 category_id 只输出一次 (可以控制数据源的数据,只输出一个用户一个 category_id,测试这两个参数)

 rownum=1 的问题

看了下源码,去重部分结构是这样的

大致分成两部分,分别以类: DeduplicateFunctionBase 和 MiniBatchDeduplicateFunctionBase 为基类,分别实现了 ProcTimeDeduplicateKeepFirstRowFunction 、ProcTimeDeduplicateKeepLastRowFunction 、RowTimeDeduplicateFunction 和 ProcTimeMiniBatchDeduplicateKeepFirstRowFunction、ProcTimeMiniBatchDeduplicateKeepLastRowFunction、RowTimeMiniBatchDeduplicateFunction 两个分支的数据
简单的区别就是: DeduplicateFunctionBase 接收的数据是单条的,mini-batch 的接收的数据都是集合

RowTimeDeduplicateFunction

@Override
public void processElement(RowData input, Context ctx, Collector<RowData> out) throws Exception {
    deduplicateOnRowTime(
            state,
            input,
            out,
            generateUpdateBefore,
            generateInsert,
            rowtimeIndex,
            keepLastRow);
}

RowTimeMiniBatchDeduplicateFunction

@Override
public void finishBundle(Map<RowData, List<RowData>> buffer, Collector<RowData> out) throws Exception {
    for (Map.Entry<RowData, List<RowData>> entry : buffer.entrySet()) {
        RowData currentKey = entry.getKey();
        List<RowData> bufferedRows = entry.getValue();
        ctx.setCurrentKey(currentKey);
        miniBatchDeduplicateOnRowTime(
                state,
                bufferedRows,
                out,
                generateUpdateBefore,
                generateInsert,
                rowtimeIndex,
                keepLastRow);
    }
}

所以就不需要问为什么 rownum 只能等于 1 了。

新的问题又来了: 为什么没看 mini-batch 的时候没有输出没有去重呢?

答: PARTITION BY category_id ORDER BY process_time desc 在 desc 的情况下,每次进来的数据 process_time 都是大于之前的数据,新数据的 rownum=1,所以 每条数据都输出了

完整代码参见:https://github.com/springMoon/sqlSubmit 

欢迎关注Flink菜鸟公众号,会不定期更新Flink(开发技术)相关的推文

 

posted on 2021-03-09 17:08  Flink菜鸟  阅读(3505)  评论(0编辑  收藏  举报