系统设计-直播间打标服务
直播业务里,“直播间打标”是非常常见的基础能力:给直播间打上「新主播」「带货」「高价值」「PK场景」等标签,供推荐、风控、运营圈选、报表统计使用。看似简单的 CRUD,真正落到线上会很快遇到性能瓶颈:写入不算多,但查询极多,尤其是“按标签查直播间”“多标签组合圈选”这类需求。
本文以一个典型的企业场景为背景,梳理直播间打标服务从最朴素的三表设计开始,逐步演进到位图/RoaringBitmap/ES,并解释为什么最后想要速度,通常绕不过“缓存 + 索引”的组合。
1. 需求与典型查询
我们要提供的能力:
- 打上标:给直播间新增一个或多个标签
- 修改标:删除标签、替换标签、批量变更
- 查询标:
- 查询某直播间有哪些标签(room → tags)
- 查询某个标签下有哪些直播间(tag → rooms)
- 多标签组合查询(AND/OR/NOT),用于圈选与统计
其中,“查询”的 QPS 往往远高于写入,且多标签组合会让查询代价呈指数增长。
2. 初版方案:经典三张表(规范化建模)
最直观的设计是三张表:
- 直播间信息表:
live_room(room_id, …) - 标签表:
tag(tag_id, tag_name, …) - 直播间-标签关联表:
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 存在数据库里,每次查询仍然要:
- 读出这些 bitmap blob
- 反序列化
- 运算
- 返回结果
这并没有天然减少“访问存储”的次数,只是把 “多表 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 缓存什么?
按查询类型分两类:
-
room → tags(查某直播间有哪些标签)
- 缓存 key:
room:{room_id}:tags - value:tag_id 列表 / bitmask / 小 bitmap
- 缓存 key:
-
tag → rooms(查某标签下有哪些直播间、或圈选)
- 缓存 key:
tag:{tag_id}:bitmap - value:RoaringBitmap 序列化 blob(或 ES 结果的热数据)
- 缓存 key:
对于“多标签圈选”:
- 可以缓存常见组合的结果(segment 缓存),但要控制爆炸(组合数会很大)
- 更常见做法是:缓存单标签底层集合(每个 tag 的 bitmap),组合时在服务内存中快速运算
6.2 缓存带来的关键设计:权威源与可恢复性
缓存一定会遇到“缓存丢失/击穿/崩溃”问题,所以必须明确:
- 权威数据在数据库(或事件日志)
- 缓存是派生加速层,可重建
典型实现:
- 打标请求:先落库(或落事件日志),再异步更新缓存/索引
- Redis 崩了:用 DB/日志重建 tag bitmap 与 room tags 缓存
- 热点保护:本地缓存 + 限流 + 预热
7. 一个现实可落地的组合方案(推荐)
如果目标是“查询快、圈选快、可恢复、可扩展”,常见的最终形态是:
- MySQL/TiDB:存权威关系与日志(审计、回放)
- Redis:存热点缓存与 RoaringBitmap(二级索引)
- (可选)ES:承接复杂搜索/聚合场景(例如还要按标题、类目、主播属性、时间范围等综合筛选)
也就是:
- “真相”在 DB
- “快”在缓存/索引
- “复杂检索”交给 ES(如果确有需求)
企业级
当然,对于大企业的话其实无需考虑这些,其实可以直接用字符串存储打标的信息,比如“[标签1,标签2]”等。毕竟大企业利润大,也不会在意小鱼小虾。
8. 小结
直播间打标看起来是简单的增删查,但在企业规模下,性能瓶颈通常来自:查询多、组合筛选多、数据量大。
演进路径可以概括为:
- 三张表:规范、易用,但查询压力大
- 标签少时的 bitmask:减少行数、room→tags 快,但 tag→rooms 难优化
- RoaringBitmap:适合做 tag→rooms 的集合索引与交并差,但不自动减少回源次数
- ES:擅长倒排与组合过滤,但成本和一致性模式需要评估
- 最终提速:几乎都要上缓存,并确保 DB/日志作为权威源可重建