实时数仓实战篇一:长周期去重指标建设

大家好,我是土哥~

关注我的老粉应该都知道,我从最开始写文章时,分享的都是 Flink 相关的技术文章,也发表了多篇爆款文章。今年呢,从技术规划上,我将做以下几大规划,主要分为四大方向:

1. 公司爆料内推篇

2. 成长职业规划篇

3. 粉丝求职面试篇

求职面试篇一:面试的反套路思维,教你用业务思维破局

求职面试篇二:简历优化重构高级表述

求职面试篇三:携程资深数据开发面试总结

4. 数仓优化实战篇

离线JOIN优化篇一:大表JOIN小表实践

离线JOIN优化篇二:大表关联中表实践

离线JOIN优化篇三:空值长尾数据倾斜优化

希望从公司简介内推、个人职业规划、粉丝求职面试、技术沉淀等方面形成一个闭环,全方位帮助我的粉丝得到进一步的成长~

今天给大家带来实时数仓实战篇一:长周期去重指标相关的文章

1 前言

做实时数仓的同学,有没有遇到过这种崩溃场景?

你的产品提了一个需求:想看最近 30 天卷入 UV 实时看板 ,要求分钟级更新

于是你用 Flink 实现长周期任务, 通过 state 存全量用户 ID ,但任务跑了 3 天就 OOM(内存溢出);

于是又换成离线批处理,但 T+1 时效又被业务 diss 太慢,在实时时效和精确统计中,一直无法做到鱼和熊掌兼得。。。

今天,土哥就用一套实时增量+历史存量+湖仓一体的方案 ,从业务痛点到技术选型,从核心步骤到踩坑经验,帮你搞定长周期去重的所有问题,同时把周 UV 做到分钟级更新 ,误差控制在0.8%以内,且任务运行资源可以下降30%。

2 背景

长周期去重指标含义

长周期去重指标 = 时间窗口 + 唯一标识去重

简单说,就是在一个较长时间窗口内(比如 7 天、30天),对用户 ID、设备 ID 等唯一标识做去重计数。常见的有:

  • 电商:店铺 7 天 UV、商品 30 天点击设备数;

  • 直播:直播间 24 小时观看用户数;

  • 广告:广告投放 15 天触达独立用户数。

这类指标的核心矛盾是:窗口越长,数据量越大,实时计算和精确统计的冲突越明显

2.2 传统方案的死穴

我们先盘点下常见方案的痛点,看看为啥它们解决不了问题:

方案类型 实现逻辑 痛点
纯离线批处理 Hive/Spark每天算一次 时效差(滞后 12-24 小时),无法支撑实时决策
纯实时Flink 开长窗口+状态去重 状态爆炸(30 天窗口存上亿用户 ID)、OOM 频繁

比如纯实时方案算 30 天卷入 UV,假设每天 1000 万用户,30 天就是 3 亿 ID ------ Flink 状态要存这么多数据,不仅内存扛不住,同时 checkpoint 时间能长达 10 分钟,当出现 failover 时,任务重启一次就得等半小时。

3 技术方案

3.1 技术架构

本方案设计核心是实时增量算精准近期数据,离线批量补全历史数据,最后通过中间存储合并。整体架构方案如下:

链路1(ODS层):ODS 层使用 kafka 存储,作为消息中间件,上游来自前端埋点或服务端埋点或 OB 库。

链路2(DWD层):DWD 层使用 Paimon 进行离线、实时存储,可以做到流批一体。

链路3(DWS离线) :DWS离线侧,负责算<=T-1 的历史数据,保证最终精确,可以使用 Doris 进行存储,用户 OLAP 数据分析;

链路4(DWS实时) :DWS实时侧:负责实时计算当日数据,可以通过dt、hh 进行分区,使用 Doris 进行存储,用户 OLAP 数据分析,可以做到秒级;

3.2 关键技术选型

很多同学会问:为啥用 Paimon 不用 Hudi ?为啥选 Theta/RBM 不直接存原始 ID ?为啥 ADM 层要用 Doris 呢?

这里直接给结论:

(1)Paimon:中间存储的最优解

我们需要一个支持实时写入、离线批量读取、增量合并 的存储------Paimon(原 Flink Table Store)刚好满足:

  • 实时写入:Flink 能 1 分钟内(checkpoint间隔时间)把数据写入 Paimon 分区,且数据可以进行验数分析;

  • 离线读取:Flink Batch/Spark能直接读 Paimon 的历史分区,不用转格式;

  • 增量合并:支持 CDC 变更和分区覆盖,实时和离线数据能无缝合并。

对比 Hudi,Paimon 的小文件合并更轻量,适合分钟级的小批量写入;

对比 Iceberg,Paimon 的 Flink 集成更原生,不用写复杂的 UDF。

(2)Theta vs RBM:

这两个都是去重计数的算法,如何选取,主要取决于业务需求,我们把它们存在 Doris/Paimon 里做中间结果:

  • Theta Sketch近似估算算法(HyperLogLog,简称 HLL),用哈希值+位图存去重结果,占用空间小(1000 万用户 ID 仅需10 KB),计算快,但有误差(默认误差 2% 以内);

  • RBM(Roaring Bitmap)精确统计算法,本质是存去重后的原始 ID,但用了压缩和索引优化,比直接存 ID 省 50% 空间,适合误差必须为 0 的场景(比如电商的支付 UV)。

我们的方案里,实时增量用RBM(准),离线批量用RBM(准),最后合并时取 RBM 的精确结果。

(3)Doris

DWS、ADM 层用 Doris,主要是两个原因:

  • 物化视图自动合并:Doris 能把 Theta/RBM 结果自动聚合,生成按分钟、小时、天调度一次的物化视图,查询时直接读视图,速度比查原始数据快 10 倍;

  • 高并发低延迟:支持每秒 1000+ 查询,响应时间在 100ms 以内,刚好满足运营看板、API 接口的需求。

4.实战

4.1 DWD 层

Flink 实时写入 Paimon

业务日志(用户访问日志)先发到 Kafka,Flink 流读作业按分钟写入 Paimon DWD 层,这里的关键参数:

  • Paimon的partition.fields设为dt,hh(日、时)即可;

  • Flink的checkpoint.interval设为1分钟,确保数据 1 分钟内落地Paimon。

代码片段(Flink SQL):

复制代码
CREATE TABLE dwd_paimon_log_ri ( 
 `create_log` VARCHAR COMMENT '时间戳',
 `user_id` VARCHAR COMMENT 'userid',
 `dt` VARCHAR COMMENT '',
 `hh` VARCHAR COMMENT ''
)
PARTITIONED BY (`dt`,`hh`)
 WITH ( 
 'ant.version'='10'
,'partition.time-interval'='1d'
,'partition.timestamp-formatter'='yyyyMMdd HH'
,'partition.idle-time-to-done'='5min'
,'snapshot.time-retained'='48h'
,'partition.timestamp-pattern'='$$dt $hh'
,'bucket'='-1'
,'connector'='paimon'
,'snapshot.expire.execution-mode'='async'
,'partition.expiration-time'='63d'
,'target-file-size'='256MB'
);

INSERT INTO dwd_paimon_log_ri
SELECT  create_log
        ,user_id
        ,REPLACE(SUBSTR(create_log, 1, 10), '-', '') AS dt
        ,SUBSTR(create_log, 12, 2)                   AS hh
FROM    ods_kafka_log_ri t1 

DWD 层主要职责为 Flink 流式写入 Paimon,通过 dt、hh 进行分区,这样下游可以离线、实时分别读取。

4.2 DWS 层

4.1.1 DWS实时侧

DWS 层实时侧通过 Flink 流读 Paimon 写入 Doris。

这里的关键是用 rbm 代替原始 ID 存储

  • 本质是存去重后的原始 ID,但用了压缩和索引优化,比直接存 ID 省 50% 空间,适合误差必须为 0 的场景(比如电商的支付 UV)

代码片段:

复制代码
CREATE VIEW log AS
SELECT  create_log     
        ,DATE_FORMAT(create_log, 'yyyyMMdd')            AS day
        ,DATE_FORMAT(create_log, 'HH')                  AS hour
        ,DATE_TRUNC(SUBSTRING(create_log, 1, 19), 'HH') AS b_time
        ,user_id
FROM    dwd_paimon_log_ri
WHERE   is_date(create_log)
AND     UNIX_TIMESTAMP(create_log, 'yyyy-MM-dd HH:mm:ss') >= (
                                                                UNIX_TIMESTAMP() - 86400 * 2
                                                             )
AND     UNIX_TIMESTAMP(create_log, 'yyyy-MM-dd HH:mm:ss') < (
                                                                UNIX_TIMESTAMP() + 60
                                                            );

INSERT INTO ads_doris_log_rs
SELECT  day AS dt
        ,hour
        ,b_time
        ,UNIX_TIMESTAMP() * 1000000 AS version
        ,'v1'  AS reset_version
        ,'EVENT' AS event_code
        ,CAST(rbm_acc_udaf(user_id) AS BINARY) AS uv_rbm_bytes
        ,COUNT(1) AS pv
FROM    log
GROUP BY 
,day
,hour
,b_time
4.1.2 DWS 离线侧

通过 rbm 精准计算历史 T-1 的全量数据。

复制代码
INSERT INTO adm_doris_log_dd PARTITION (dt = '${bizdate}')
SELECT  '${bizdate}' AS dt
        ,UNIX_TIMESTAMP() * 1000000 AS version
        ,'v1'   AS reset_version
        ,'EVENT' AS event_code
        ,CAST(rbm_acc_udaf(user_id) AS BINARY) AS uv_rbm_bytes
        ,COUNT(1)  AS pv             
FROM    dwd_paimon_log_ri
WHERE   dt <= '${bizdate}'
GROUP BY dt

4.3 ADM 层

合并历史与增量

这里采用离线 T-2 的数据 UNION ALL 实时 T-1 的数据 UNION ALL 实时增量数据。

为什么要这样做呢?防止离线 T-1 在当天未产出,造成数据合并错误。

部分代码:

复制代码
SELECT 
 rbm_distinct(base64_decode(uv_rbm_bytes)) as uv,
 SUM(pv) pv
 FROM (
    -- 实时
    SELECT 
      ,uv_rbm_bytes
      ,pv
    FROM ads_doris_log_rs
    WHERE dt = TO_CHAR (
      DATE_SUB (TIMESTAMP '${current_hh}', INTERVAL '0' HOUR),
      'yyyyMMdd'
    )
    and hour <= SUBSTR (
      TO_CHAR (
        DATE_SUB (TIMESTAMP '${current_hh}', INTERVAL '0' HOUR),
        'yyyy-MM-dd HH:mm:ss'
      ),
      12,
      2
    )
    -- T-1 实时
    UNION ALL 
    SELECT 
      ,uv_rbm_bytes
      ,pv
    FROM ads_doris_log_rs
    WHERE 
    dt = TO_CHAR (
      DATE_SUB (TIMESTAMP '${current_hh}', INTERVAL '1' DAY),
      'yyyyMMdd'
    )
    -- T-2 离线
    UNION ALL 
    SELECT 
      ,uv_rbm_bytes
      ,pv
    FROM ads_doris_log_dd
    WHERE 
    dt = TO_CHAR (
      DATE_SUB (TIMESTAMP '${current_hh}', INTERVAL '2' DAY),
      'yyyyMMdd'
    )
)

四、总结

4.1. 平衡

上述方案的最大价值是在成本可控的前提下,做到了分钟级时效和最终精确

若对时效性要求很高,同时 UV 数据误差可以接受在2%以内,推荐:

  • 实时增量用 Theta,资源消耗只有纯实时的1/10;

  • 离线批量用 RBM,保证历史数据 100% 精确;

2. 场景复用

这套方案适用于所有长周期去重+实时需求的场景:

  • 电商:店铺7天UV、商品30天点击设备数;

  • 广告:广告投放 15 天触达独立用户数。

如果你的场景不需要 100% 精确,可以全用 Theta(省资源)。

如果需要 100% 精确,可以把实时增量也换成RBM(但资源消耗会增加)。

长周期去重是实时数仓的必考题, 你在做长周期去重时,遇到过哪些问题?是误差太大?还是资源不够?欢迎在评论区留言,我们一起交流!

增值服务:简历修改|面试辅导|Flink资料|模拟面试

你好,我是土哥,计算机硕士毕业,现某大厂资深大数据开发工程师。出生在一个 18 线开外的小村庄,通过自己努力毕业一年在新一线城市买房,在社招、校招斩获 28 家中大厂 offer。

2026年,很多公司岗位流动性非常大,已经到了一年中最佳的求职高峰期。如果你想跳槽,但苦于一个人孤军奋战、无人指导、复习无从下手,或者不擅长写简历,手上只有拿不出手的毫无难点亮点的项目经历...

那么我的建议是多和身边的大佬沟通,哪怕是付费咨询,只要你能从他身上学到经验,那就是值得的。如果身边没有这样的人,那么我就毛遂自荐一下吧,毕竟,茫茫网络你能看到这篇文章何尝不是一种命运安排。

土哥社招参加 28 场面试,100% 通过率,拿到全部 offer!

土哥这半年的悲惨人生,经历过被鸽 offer,最终触底反弹~

小伙伴找工作合集~

相关推荐
蓝眸少年CY3 小时前
什么是Hadoop
大数据·hadoop·分布式
Hello.Reader4 小时前
Flink Java 版本兼容性与 JDK 模块化(Jigsaw)踩坑11 / 17 / 21 怎么选、怎么配、怎么稳
java·大数据·flink
TechPioneer_lp4 小时前
小红书后端实习一面|1小时高强度技术追问实录
java·后端·面试·个人开发
白日与明月5 小时前
Hive 大表 Join 优化:从策略选择到倾斜处理
数据仓库·hive·hadoop
康王有点困5 小时前
Flink部署模式
java·大数据·flink
AC赳赳老秦5 小时前
Confluence + DeepSeek:构建自动化、智能化的企业知识库文档生成与维护体系
大数据·运维·人工智能·自动化·jenkins·数据库架构·deepseek
综合热讯5 小时前
燧光确立“数字化训练应用引领者与实战派服务商”核心定位
大数据
G皮T6 小时前
【Elasticsearch】OpenDistro Security 中 backend_roles 的设计哲学(含实战验证)
大数据·elasticsearch·kibana·权限管理·role·opensearch·backend role
Java程序员威哥6 小时前
SpringBoot2.x与3.x自动配置注册差异深度解析:从原理到迁移实战
java·大数据·开发语言·hive·hadoop·spring boot·后端