系统设计-标签服务

9 阅读7分钟

系统设计-直播间打标服务

直播业务里,“直播间打标”是非常常见的基础能力:给直播间打上「新主播」「带货」「高价值」「PK场景」等标签,供推荐、风控、运营圈选、报表统计使用。看似简单的 CRUD,真正落到线上会很快遇到性能瓶颈:写入不算多,但查询极多,尤其是“按标签查直播间”“多标签组合圈选”这类需求。

本文以一个典型的企业场景为背景,梳理直播间打标服务从最朴素的三表设计开始,逐步演进到位图/RoaringBitmap/ES,并解释为什么最后想要速度,通常绕不过“缓存 + 索引”的组合。


1. 需求与典型查询

我们要提供的能力:

  1. 打上标:给直播间新增一个或多个标签
  2. 修改标:删除标签、替换标签、批量变更
  3. 查询标
    • 查询某直播间有哪些标签(room → tags)
    • 查询某个标签下有哪些直播间(tag → rooms)
    • 多标签组合查询(AND/OR/NOT),用于圈选与统计

其中,“查询”的 QPS 往往远高于写入,且多标签组合会让查询代价呈指数增长。


2. 初版方案:经典三张表(规范化建模)

最直观的设计是三张表:

  1. 直播间信息表live_room(room_id, …)
  2. 标签表tag(tag_id, tag_name, …)
  3. 直播间-标签关联表room_tag(room_id, tag_id, created_at, …)

优点:

  • 结构清晰、易理解、易维护
  • 写入简单:插入/删除关联行即可
  • 支持灵活查询与统计(SQL 可表达性强)

但很快会遇到问题:查询次数多,且多标签查询会很重

2.1 为什么会慢?

  • 单标签查直播间:SELECT room_id FROM room_tag WHERE tag_id=?
    这会扫很多关联行,数据量大时容易变慢,且分页/排序成本高。
  • 多标签 AND:通常是 GROUP BY room_id HAVING COUNT(DISTINCT tag_id)=N
    或者多次 join/intersect,CPU 和临时表压力大。
  • room → tags 查询虽然相对轻,但在高并发下也会形成数据库热点。

所以初版三表能跑,但在“高 QPS + 圈选组合”场景下,DB 很容易成为瓶颈。


3. 优化思路一:标签较少时,用“二进制位”压缩为一张表

如果标签总数比较少(比如几十到几百个,且相对稳定),可以把标签集合编码成一个二进制位串:

  • 哪一位为 1,表示拥有对应标签
  • 存储在 room_tag 表里(或叫 room_tag_mask)的一列中

例如:

room_id | tag_bits
1001    | 01001001...
1002    | 00000010...

这样你可以只保留一张“直播间-标签集合”表,不再按 tag_id 一行一行存关联。

3.1 这能解决什么?

  • room → tags 查询变成单行读取(更快、更少行)
  • 修改标签变成位运算更新(更快、更少写放大)

3.2 它的局限是什么?

  • 按标签找直播间(tag → rooms)变难
    你要查“第 k 位为 1 的所有直播间”,在 MySQL 里往往意味着:
    • 使用位运算函数过滤,但依然可能是大范围扫描
    • 想走索引非常困难(除非为每一位做生成列 + 索引,但维护成本高,维度一多就不可控)
  • 标签一旦扩展到上千、上万位,存储与维护都变复杂
  • 多标签组合圈选也不一定快(仍受限于扫描与索引能力)

结论:这种方案适合“标签少 + room->tags 查询为主”的场景,一旦“tag->rooms / 圈选”成为核心,就会卡住。


4. 优化思路二:RoaringBitmap —— 适合“标签 → 大集合”的倒排索引

当业务真正需要“按标签圈选直播间、做交并差、统计基数”时,位图是最自然的结构。RoaringBitmap 是工业界常用的压缩位图实现,特点是:

  • 对稀疏/密集集合都有不错的压缩率
  • 支持高效集合运算:AND/OR/ANDNOT
  • 适合做“倒排索引”:tag_id -> {room_id集合}

因此你会想到:

  • 为每个 tag 维护一个 bitmap,里面放所有拥有该 tag 的 room_id
  • 多标签圈选就是把多个 bitmap 在内存里 AND/OR

4.1 关键误区:它不一定减少“表查询次数”

这一点你已经抓到本质了:
如果你只是把 bitmap 存在数据库里,每次查询仍然要:

  1. 读出这些 bitmap blob
  2. 反序列化
  3. 运算
  4. 返回结果

这并没有天然减少“访问存储”的次数,只是把 “多表 join + group by 的成本”,转移成 “读 bitmap + 内存集合运算”

也就是说:RoaringBitmap 的优势更多是计算效率存储压缩,不是“让你不访问存储”。

所以它通常需要搭配一个关键组件:缓存层(下文会讲)。


5. 优化思路三:用 ES 查询(倒排索引能力强)

Elasticsearch 的天然能力是:

  • 构建倒排索引
  • 支持条件过滤、布尔组合、聚合统计
  • 适合“按标签筛选、组合查询、实时搜索”这类场景

因此另一种路线是:

  • 直播间作为 document
  • tags 作为 keyword 数组
  • 通过 ES 的 bool query + filter 实现 tag AND/OR/NOT
  • 通过 aggregation 统计各标签数量、TopN 等

5.1 ES 的价值

  • “tag → rooms” 查询表达性强,组合查询也方便
  • 对搜索/聚合类需求更友好(尤其是要做复杂筛选条件时)

5.2 ES 的现实成本

  • 集群成本、运维复杂度、写入链路更长
  • 数据一致性通常是近实时(refresh interval)
  • 对“纯集合运算(超大集合交并差)”未必比位图更划算,尤其在极端大集合、极端高 QPS 下

所以 ES 往往用于“筛选 + 搜索 + 聚合”很强的场景;如果核心是“海量集合的交并差”,位图/roaring 仍然很有竞争力。


6. 最终绕不过去的一步:上缓存(并明确“谁是权威”)

当你的痛点是“查询次数多”,本质是 存储层被反复读取。不管你用三表、bitmask、Roaring 还是 ES,只要每次请求都要回源存储,高并发下迟早会遇到瓶颈。

所以最终提升速度的通用解是:缓存

6.1 缓存什么?

按查询类型分两类:

  1. room → tags(查某直播间有哪些标签)

    • 缓存 key:room:{room_id}:tags
    • value:tag_id 列表 / bitmask / 小 bitmap
  2. tag → rooms(查某标签下有哪些直播间、或圈选)

    • 缓存 key:tag:{tag_id}:bitmap
    • value:RoaringBitmap 序列化 blob(或 ES 结果的热数据)

对于“多标签圈选”:

  • 可以缓存常见组合的结果(segment 缓存),但要控制爆炸(组合数会很大)
  • 更常见做法是:缓存单标签底层集合(每个 tag 的 bitmap),组合时在服务内存中快速运算

6.2 缓存带来的关键设计:权威源与可恢复性

缓存一定会遇到“缓存丢失/击穿/崩溃”问题,所以必须明确:

  • 权威数据在数据库(或事件日志)
  • 缓存是派生加速层,可重建

典型实现:

  • 打标请求:先落库(或落事件日志),再异步更新缓存/索引
  • Redis 崩了:用 DB/日志重建 tag bitmap 与 room tags 缓存
  • 热点保护:本地缓存 + 限流 + 预热

7. 一个现实可落地的组合方案(推荐)

如果目标是“查询快、圈选快、可恢复、可扩展”,常见的最终形态是:

  • MySQL/TiDB:存权威关系与日志(审计、回放)
  • Redis:存热点缓存与 RoaringBitmap(二级索引)
  • (可选)ES:承接复杂搜索/聚合场景(例如还要按标题、类目、主播属性、时间范围等综合筛选)

也就是:

  • “真相”在 DB
  • “快”在缓存/索引
  • “复杂检索”交给 ES(如果确有需求)

企业级

当然,对于大企业的话其实无需考虑这些,其实可以直接用字符串存储打标的信息,比如“[标签1,标签2]”等。毕竟大企业利润大,也不会在意小鱼小虾。

image.png

8. 小结

直播间打标看起来是简单的增删查,但在企业规模下,性能瓶颈通常来自:查询多、组合筛选多、数据量大

演进路径可以概括为:

  1. 三张表:规范、易用,但查询压力大
  2. 标签少时的 bitmask:减少行数、room→tags 快,但 tag→rooms 难优化
  3. RoaringBitmap:适合做 tag→rooms 的集合索引与交并差,但不自动减少回源次数
  4. ES:擅长倒排与组合过滤,但成本和一致性模式需要评估
  5. 最终提速:几乎都要上缓存,并确保 DB/日志作为权威源可重建