【场景面试题】二、超大规模人群与数据处理(11~18) 二、超大规模人群与数据处理111811. 有 8 亿用户如何快速筛出符合条件的 100 万人先改变问题模型不要在活动开始时执行一条扫描 8 亿行的 SQL。命中率只有约 0.125%这是低命中率候选发现问题应把经常使用的用户属性预计算成集合索引让在线阶段做集合运算而不是逐行判断。数据链路用户画像、行为、地域、风控和历史参与数据经过离线 Spark/Hive 与实时 Flink 形成版本化标签例如active_7d、city_target、risk_pass、pay_level_3_plus、joined_before。每个标签按 UID 空间分片保存 RoaringBitmap规则引擎把条件编译成集合表达式eligible active_7d AND city_target AND risk_pass AND pay_level_3_plus AND NOT joined_before各分片并行执行 AND/OR/ANDNOT快速得到约 100 万候选再对候选做一次精确校验处理标签延迟和高价值条件最终写入带activity_id version的白名单。在线只做 O(1) membership check活动开始时不再计算全量。容量与工程细节800,000,000 bit 原始位图约 100MB一个标签并不大RoaringBitmap 对稀疏集合还能压缩。标签必须记录数据时间、版本和规则版本保证结果可复现。计算任务按 shard 记录完成状态和 checksum失败可重跑白名单发布采用新版本完整生成后原子切换不能边算边覆盖线上版本。为什么这样做位图把大量 UID 集合运算变为 CPU 友好的位运算先粗筛再精校验把昂贵条件只施加在 100 万人上。固定规则适合直接物化白名单动态规则则保留标签位图按需组合。追问实时资格变化通过增量标签事件更新小位图或资格 KV强实时风控仍在领取时二次校验。若 UID 不连续先映射为 dense internal id或使用 Roaring64Map。具体示例具体设定。活动要求“最近 7 天活跃、北京/上海、消费等级 3 以上、非风险、没参加过”预估命中 100 万。落地例子。每个标签保存 Bitmapactive_7d、city_bj_or_sh、pay_level_ge_3、risk_block、joined_before。规则编译为active_7d AND city_bj_or_sh AND pay_level_ge_3 AND NOT risk_block AND NOT joined_before各 UID shard 并行算结果写activity_whitelist(activity_id, version, uid)或 whitelist bitmap。面试可讲。低命中率场景不要扫 8 亿行用预计算标签集合求交先得到 100 万候选再做精确校验。在线领取时只做 O(1) 白名单判断和实时风控兜底。12. 如果 8 亿人中要筛出 7 亿人方案为什么必须改变反转模型命中率约 87.5% 时不应维护 7 亿白名单作为主要在线结构。维护约 1 亿“不符合者”的排除集合excluded risk_block OR joined_before OR region_block OR account_invalid在线默认允许uid in excluded - 不符合否则符合。这叫高命中率反向建模能显著减少存储、更新和点查成本。是否8亿用户计算排除条件Exclusion Bitmap在线 uid在排除集合?不符合默认符合只有下游要求全量时才分片输出7亿结果真正的瓶颈转移判断是否符合仍然很快难点变成“是否必须物理输出 7 亿条”。如果只是在线校验排除集合足够如果下游要求完整名单就按 UID 分片并行生成 Parquet/ORC 文件路径带 activity/version/shard支持断点续跑和校验。不要把 7 亿行塞进单表或单队列下游应按分区消费并使用 manifest 标记版本完整发布。为什么这样做低命中率优化“发现少量候选”高命中率优化“表达少量例外”。两者用同一套巨型白名单是没有根据命中率改变数据结构。优秀回答还要指出输出 7 亿条的网络、存储、下游吞吐成本通常远大于集合计算本身。具体示例具体设定。活动几乎人人可参加只排除黑产、未实名、已参加和受地域限制用户约 1 亿人不符合。落地例子。不建 7 亿白名单而建excluded_bitmap risk OR not_real_name OR joined_before OR region_block。在线逻辑是uid in excluded - reject否则默认通过。只有下游必须拿全量名单时才按 UID shard 输出 7 亿 Parquet 文件和 manifest。面试可讲。命中率高时要反向表达少量例外。真正贵的不是算集合而是物理输出 7 亿条给下游所以要问清是“在线判断”还是“必须导出名单”。RoaringBitmap高效压缩位图RoaringBitmap 可以理解成一种“压缩过的、高性能整数集合”。它常用于这种问题8 亿用户里快速找出“近 7 天活跃 AND 北京用户 AND 非风险用户 AND 没参加过活动”的人。普通写法可能是扫表selectuidfromuser_profilewhereactive_7d1andcityin(北京)andrisk0andjoined0;但 8 亿行不能每次活动都扫。更好的方式是把每个条件提前做成一个集合active_7d {1, 3, 8, 10, 20, ...} city_beijing {3, 10, 18, 20, ...} risk_block {8, 99, 120, ...} joined_before {1, 18, ...}然后做集合运算eligible active_7d ∩ city_beijing - risk_block - joined_beforeRoaringBitmap 就是专门让这种集合运算又快又省内存的数据结构。先理解 Bitmap普通 Bitmap 是一个 bit 数组。假设用户 ID 是 0 到 9用户ID: 0 1 2 3 4 5 6 7 8 9 是否命中: 0 1 0 1 0 0 0 1 0 0这表示集合{1, 3, 7}。它的优点是集合运算非常快AND 求交集 OR 求并集 ANDNOT 求差集比如活跃用户 bitmap 城市用户 bitmap 两个 bitmap 按位 AND就得到“既活跃又在目标城市”的用户缺点是普通 Bitmap 有时浪费空间。比如 ID 范围很大但实际只有很少用户命中还是要占一大片 bit。RoaringBitmap 怎么压缩RoaringBitmap 的核心思想是把整数空间切成很多小段每段根据稀疏或密集程度用不同方式存。以 32 位整数为例一个数字会被拆成两部分uid 高 16 位 低 16 位高 16 位决定它属于哪个分桶低 16 位存在桶里面。每个桶最多表示 65536 个数字。桶里再根据数据密度选择三种容器容器类型适合场景怎么存Array Container很稀疏直接存命中的低 16 位列表Bitmap Container比较密集用 65536 个 bit 表示Run Container连续区间多存区间比如 10005000举个例子集合 A {1, 2, 3, 100000, 100001, 100002, ...}RoaringBitmap 不会傻傻地开一个超大 bit 数组而是拆桶桶 0: {1, 2, 3} 桶 1: {34464, 34465, 34466, ...}如果桶里很少就用数组如果桶里很多就用 bitmap如果连续就用 run-length 编码。所以它兼顾了两点稀疏时省内存 密集时运算快为什么它快因为 RoaringBitmap 做交集、并集、差集时不是一个一个 UID 判断而是容器级批量计算。比如活动筛选eligible active_7d AND city_beijing AND pay_level_3_plus AND NOT risk_block AND NOT joined_before底层大概就是RoaringBitmapresultRoaringBitmap.and(active7d,cityBeijing);result.and(payLevel3Plus);result.andNot(riskBlock);result.andNot(joinedBefore);这比遍历 8 亿用户逐个判断快很多因为它利用了 CPU 的位运算和压缩结构。和 HashSet 比有什么区别假设要存 100 万个 UID。用 HashSet优点点查方便 缺点内存大集合求交慢GC 压力大用 RoaringBitmap优点内存小交并差非常快适合批量集合运算 缺点主要适合整数集合复杂对象不适合直接存在系统设计面试里如果你说低命中率人群筛选可以用标签 Bitmap 或 RoaringBitmap 做集合求交最后物化白名单。这会比“用 Redis 存一下”专业很多因为你说清楚了数据结构和计算方式。典型使用场景RoaringBitmap 很适合用户标签活跃用户、城市用户、风险用户、付费用户 广告定向多个标签求交和排除 活动白名单8 亿用户筛 100 万 权限集合某资源可见用户集合 UV 去重按天保存访问用户集合 推荐召回某类兴趣用户集合比如active_7d bitmap 近 7 天活跃用户 city_bj bitmap 北京用户 risk_block bitmap 风险用户 joined_before bitmap 已参加用户 target active_7d AND city_bj AND NOT risk_block AND NOT joined_before最终得到的target就是候选用户集合。需要注意的坑第一RoaringBitmap 通常处理的是整数集合。如果 UID 是 64 位可以用Roaring64Map或者把 UID 映射成连续的内部 ID。第二它适合“集合运算”不适合存用户复杂属性。比如“用户年龄、城市、等级、最近消费金额”不能都塞进一个 bitmap而是每个标签一个 bitmap。第三它不是数据库。它解决的是快速算出候选集合最终结果通常还要做一次精确校验比如查实时风控、资格状态、活动库存等。第四高命中率场景要换思路。比如 8 亿人里要筛 7 亿人不一定要存 7 亿白名单更适合存 1 亿排除集合默认符合 只判断 uid 是否在 excluded bitmap 里一句话总结RoaringBitmap 是一种高性能压缩位图特别适合“大规模整数集合”的交集、并集、差集计算。面试里遇到“海量用户筛选、人群包、广告定向、标签系统、UV 去重”这类题它是非常好用的数据结构。13. 如何设计可被活动、推荐、风控复用的实时标签系统分层。原始事实层保存不可变事件特征层计算近 7 日活跃、消费等级等标签层输出稳定语义和版本。标签必须有 owner、口径、数据时间、TTL、质量指标和权限避免同名不同义。计算。离线任务生成全量快照流任务消费 CDC/行为事件做增量使用事件时间和 watermark 处理迟到。全量与增量结果通过版本合并不能让重跑覆盖更新的数据。热点标签可生成 Bitmap稀疏复杂属性用 KV/列存。服务。在线标签服务支持按 UID 批量查询和集合计算本地缓存只缓存高频稳定标签。发布采用 snapshot version 原子切换规则引用固定版本以便审计和回放。质量。监控覆盖率、空值率、分布漂移、更新延迟和与权威源的抽样差异。敏感标签需要最小权限、用途审计和过期清理。面试中不要只讲 Flink要讲口径治理与可重现性比如有一个有效时长的指标当天的是实时通过flink统计因为当天的还会不断累计而过往日期的每天都用离线快照存起来了因为以往日期的数据不会变了。具体示例具体设定。活动、推荐、风控都想用“高价值用户”但活动定义为近 30 天消费推荐定义为互动频率风控还要排除异常设备。落地例子。标签元数据必须写清tag_namepay_level_3_plus、owner、口径 SQL、数据时间、TTL、权限和版本。离线每天全量产snapshot_v20260711Flink 消费支付/退款事件生成 delta服务按 UID 批量查标签。面试可讲。标签系统不只是 Flink 计算更重要是口径治理和版本可复现。否则同名标签被多个业务各自解释事故时无法回放当时为什么命中。14. 广告定向条件很多如何快速求交、排除和估算人群包大小集合执行。把性别、地域、兴趣、活跃度等离散标签映射为分片 Bitmap。规则解析成表达式树按基数从小集合开始求交尽早缩小候选NOT 转成 ANDNOTOR 合并同类条件。昂贵的动态特征放在 Bitmap 粗筛之后精算。基数估算。规划阶段不必每次完整计算可维护标签基数、交集采样和 HyperLogLog 草图复杂表达式先给近似人数真正投放前生成精确版本。执行计划类似数据库优化器应根据统计信息选择集合运算顺序。版本与审计。人群包保存 rule、tag_version、population_version、count、checksum 和生成时间上线只引用不可变版本。增量变化通过 delta bitmap 更新周期全量重建校准。为什么。人群规则本质是集合代数。让规则引擎具备统计信息和执行计划比固定从左到右运算更稳定。还需讨论数据权限、最小人群阈值和隐私保护。具体示例具体设定。广告主选择“18 到 30 岁、北京、喜欢游戏、近 7 天活跃、排除已购买用户”想先看到预估人数。落地例子。规则树先按基数小的条件执行比如city_beijing和game_interest先 AND再 ANDactive_7d最后 ANDNOTpurchased_brand_x。预估阶段用 HLL/采样统计给 230 万投放前生成精确 bitmap 版本并记录 checksum。面试可讲。广告定向本质是集合代数加执行计划。不能固定从左到右算要像数据库优化器一样按基数和成本选择顺序。15. 设计一个实时 Top 100 排行榜用户规模 8 亿。先问窗口。是全量累计、日榜、小时榜还是滚动窗口是否要求精确同分如何排序只有 Top 100没必要让所有更新都竞争一个全局 ZSet。两级聚合。按 user_id 分片每个分片维护 Local Top K周期或事件触发把各分片候选送到 Global Merger再求全局 Top 100。分数增量先在流计算或分桶计数中聚合减少每个行为都写排行存储。榜单带版本和窗口读请求只查缓存。一致性。事件用 event_id 去重窗口关闭后允许迟到修正奖金结算使用离线重算和行为流水审计展示榜允许秒级最终一致。防刷在进入计分前完成不能先加分后异步扣回导致榜单频繁抖动。追问。单用户更新极热时按来源分桶后合并同分使用(score, last_update_time, user_id)确定稳定顺序。若要查“我的排名”需另建分数分布结构或分段计数不能扫描 8 亿人。具体示例具体设定。直播活动按用户积分排日榜行为峰值 50 万/s但榜单只展示前 100。落地例子。按user_id % 1024分片维护 local top 1000相当于用1024个分片每个分片维护前1000名的榜单用户上榜时进入某一个分片榜单进行排序这样1024个分片可抗住50万/s的峰值请求Global Merger 每秒合并 1024 个小榜得到全局 top 100又是Top K归并排序。读接口只读rank:daily:20260711:version。奖金结算用离线流水重算不直接信实时展示榜。面试可讲。不要让 50 万次/s 更新都打一个全局 ZSet。两级 TopK 让写入分散展示秒级最终一致结算走可审计的精确重算。16. 如何统计百亿事件的 UV精确与近似方案怎么选精确方案。按天/业务分片保存去重 UID Bitmap最终 OR 后统计基数UID 稠密时非常高效也支持交并差。但标签维度组合多时 Bitmap 数量爆炸不能为每个任意组合都物化。近似方案。HyperLogLog 用固定小内存估算基数误差可控支持合并适合监控和大维度聚合不能返回用户明细也不擅长集合差。更复杂的交集估算可用 MinHash/Theta Sketch。选择。结算、合规报表或需要人群明细时用精确 Bitmap/去重表实时看板和趋势用 HLL。可以先 HLL 预估达到阈值后再触发精确任务。所有统计都要定义事件时间、去重窗口、跨端用户标识和迟到修正。具体示例具体设定。一天 100 亿曝光事件要看活动页 UV也要给财务结算真实人数。落地例子。实时看板用 HLL每个 shard 更新hll:activity:day:shard合并后给大约 UV误差 1% 左右。结算用 UID Bitmap 或去重明细表按天 OR 后精确 count并可导出用户明细。面试可讲。HLL 省内存但不能返回明细Bitmap 精确且能做交并差但成本更高。监控看趋势用近似钱和合规用精确。17. 海量数据去重内存放不下时怎么办先判断输出。只判断“是否见过”可用 Bloom Filter需要精确唯一集合则 Bloom 只能做前置过滤假阳性必须回源确认。离线去重可按 key hash 分桶使同一 key 必定进入同一分区再在分区内排序或哈希去重。流式去重。用(business_key, window)保存状态结合 TTL 控制大小Exactly-once 依赖 source offset、状态 checkpoint 和 sink 事务协同。跨长周期的全局去重需要外部 KV/LSM 存储并通过批量请求和本地缓存降低成本。工程细节。热 key 单独拆分状态 TTL 必须与业务重复到达上界一致事件时间迟到超过窗口后要定义补偿。磁盘外排排序适合一次性全量分桶 KV 适合持续在线不能只谈哈希表复杂度。具体示例具体设定。50 亿条日志里按device_id event_id去重单机内存放不下。落地例子。离线任务先按hash(device_id,event_id) % 4096分桶同一个 key 一定落到同桶每桶内部排序去重或用 RocksDB/LSM 去重。流式去重则保存(key, window)状态TTL 设为业务允许迟到时间例如 24 小时。面试可讲。内存不够时先让相同 key 汇聚到同一分区再在分区内处理。Bloom Filter 只能减少回源不能作为精确去重最终依据。18. 需要导出或补发数亿条数据如何做到可控、可恢复任务模型。创建 job 与固定数据快照把范围切成 shard每个 shard 记录 start/end、cursor、attempt、count、checksum 和状态。Worker 领取租约批量读、批量写定期提交游标宕机后租约过期由其他 Worker 续跑。输出。大导出写对象存储分区文件并生成 manifest清单每个 shard 记录 start/end、cursor、attempt、count、checksum 和状态不经单机内存或 HTTP 长连接。大补发写 MQ 时按下游容量令牌桶限速消息携带 job_id、shard_id、record_id 幂等。失败分可重试、永久失败和需人工处理最终报告成功数、失败数及具体原因。一致性。使用快照时间或版本避免扫描过程中数据变化输出原子发布所有 shard 完成并校验后才让下游看到 manifest。取消任务只停止新批次已发消息仍靠幂等消费。为什么。规模化批任务的核心不是 for 循环而是分片、背压、断点、幂等、审计和结果核对。具体示例具体设定。需要给 3 亿用户补发一条活动消息下游每秒只能承受 5 万条。落地例子。建job(job_id, snapshot_time)和job_shard(shard_id, cursor, status, count, checksum)。每个 worker 每批读 5000 个 UID写 MQ 时按令牌桶限制总速率 5 万/s消息带job_id uid消费端用唯一键幂等。面试可讲。大批量任务核心是分片、游标、限速、断点和结果核对。不能一个 for 循环直接扫库发消息也不能让补发流量压垮正常业务。