全文采用苏格拉底逐层追问模式:核心本质→独有特性→为什么适配业务→执行流程→反问自查,全程大白话,无晦涩书面语,适合面试背诵+源码复盘。

覆盖结构:Bitmap、HyperLogLog、Geo、Stream、布隆过滤器


一、Bitmap 位图

1. 核心本质是什么?

Bitmap 底层就是 Redis String 字符串,区别在于:它不以整串字符为操作单位,而是以单个二进制 bit 位作为最小操作单元。

  • 1 字节 = 8 个 bit,每个 bit 只有 0 和 1 两种取值

  • 本质:用大量二进制位,批量存储二值状态数据

2. 它具备哪些独有特性?

  1. 内存占用极低:1 个 bit 保存 1 条状态,亿级数据仅占用十几 MB

  2. 仅支持 0/1 两种值,只适合「是/否」类场景

  3. 原生支持批量统计、位运算

  4. 偏移位可自动扩容,无需提前申请内存空间

3. 为什么能解决签到、在线状态、已读标记这类场景?

这类业务有明确共性:

  • 每条数据只有两种结果:签到/未签到、在线/离线、已读/未读

  • 用户体量庞大,对内存占用要求高

  • 经常需要统计总数

Bitmap 可以完美匹配:

  • 二值数据用单个 bit 存储,完全不浪费空间

  • 海量数据场景下,内存开销大幅降低

  • 内置统计命令,可快速统计状态为 1 的总数量

4. 它是如何一步步实现业务逻辑的?

以用户每日签到为例

规则定义:

  • Key:sign:日期,按天拆分数据

  • 偏移量 offset = 用户ID,一个用户独占一个 bit 位

  • bit=1 代表已签到,bit=0 代表未签到

执行流程:

  1. 用户签到
    SETBIT sign:20260529 10001 1
    定位到对应用户的 bit 位,设置为 1。重复签到offset一致,不会重复计数,天然去重。

  2. 查询单个用户是否签到
    GETBIT sign:20260529 10001
    读取对应 bit 位,0=未签到,1=已签到。

  3. 统计当日总签到人数
    BITCOUNT sign:20260529
    遍历所有 bit 位,累加值为 1 的数量,得到签到总人数。

5. 结构示意图

底层依托

String 字符串

字节1

字节2

bit0 bit1 bit2 bit3 bit4 bit5 bit6 bit7

bit0 bit1 bit2 bit3 bit4 bit5 bit6 bit7

6. 延伸思考(反问自查)

为什么不用 String/Hash 单独存储每个用户的签到状态?

千万级用户会产生海量 Key/Field,内存会膨胀上千倍,不适合海量二值状态场景。


二、HyperLogLog (HLL) 基数统计

1. 核心本质是什么?

HLL 是基于概率估计算法实现的数据结构。

  • 不存储原始数据,仅通过哈希采样、概率模型,计算不重复元素的总数量

  • 内存固定:单个 HLL 对象最大占用 12KB,和存入数据量无关

2. 它具备哪些独有特性?

  1. 内存大小恒定,存 10 条数据和存 1 亿条数据,最多只占 12KB

  2. 统计结果为估算值,标准误差约 0.81%,并非绝对精确

  3. 只能查询去重总数,无法取出存入的原始数据

  4. 支持多key合并统计,可以把多天、多页面数据合并汇总

3. 为什么能解决 UV、日活、海量去重计数这类场景?

这类业务有明确共性:

  • 核心需求:统计去重数量,同一用户多次访问只计算一次

  • 数据量极大,动辄千万、亿级

  • 多用于运营统计,允许微小误差

  • 不需要查询单条数据明细

传统方案痛点:

使用 Set 存储所有用户ID,会完整保存全部原始数据,亿级数据会占用 GB 级内存,成本极高。

HLL 匹配逻辑:

  • 只算数量、不存明细,极致节省内存

  • 微小误差在运营统计中完全可接受

  • 支持数据合并,轻松实现多日期、多页面汇总统计

4. 它是如何一步步实现业务逻辑的?

以页面 UV 统计为例

规则定义:

  • Key:uv:page:页面ID:日期

  • 存入元素:用户唯一ID

执行流程:

  1. 用户访问页面
    PFADD uv:page:1:20260529 10001
    对用户ID做哈希、采样计算,更新内部统计结构,不保存原始ID。重复添加同一个用户ID自动去重。

  2. 查询当日页面独立访客数
    PFCOUNT uv:page:1:20260529
    基于内部采样数据,通过算法估算出不重复用户总数。

  3. 合并多天 UV 数据
    PFMERGE uv:page:total uv:page:1:day1 uv:page:1:day2
    合并多个 HLL 的统计数据,再通过 PFCOUNT 获取汇总结果。

5. 结构示意图

用户ID 10001

哈希运算 & 采样

用户ID 10002

HLL 内部统计结构
仅保存特征,不存原始数据

PFCOUNT 估算 → 输出去重总数

6. 延伸思考(反问自查)

哪些场景绝对不能使用 HLL?

支付、订单、库存等要求绝对精确计数的场景;需要查询数据明细的场景也禁止使用。


补充:Bitmap VS HyperLogLog 相似度 + 核心区别

1. 二者相似度

  1. 都适合大数据量统计:都为海量统计场景设计,内存极小

  2. 都天然自动去重:同一个用户重复上报数据,都不会重复计数

  3. 都不存储原始业务数据:都拿不到具体是哪些用户签到/访问,只能拿到总数

  4. 都只能做统计,不能存详细业务信息

2. 核心本质区别

  1. 精度区别:Bitmap 100%精确;HLL 有 0.81% 固定误差,只能做运营大屏

  2. 内存上限区别:Bitmap 内存随用户ID最大值线性上涨;HLL 永远固定12KB,亿级数据内存无变化

  3. 合并能力:HLL支持PFMERGE多key合并汇总;Bitmap无法跨key合并统计

  4. 附加能力:Bitmap除了计数,还能查询单个人是否签到;HLL只能看总数,查不了单个人状态

3. 选型口诀

  • 需要精准统计 + 需要查单个用户状态 → 用Bitmap(签到)

  • 只需要大盘总数、不在乎微小误差、需要多天合并UV → 用HLL(页面访客)


三、Geo 地理位置

1. 核心本质是什么?

Geo 底层完全复用 ZSet(有序集合),是基于 ZSet 的业务封装:

  1. 通过 Geohash 算法,将二维的「经度+纬度」坐标,转换为一维字符串

  2. 把 Geohash 编码转为 ZSet 的 score,位置标识作为 ZSet 的 member

  3. 依托 ZSet 自带的排序、范围查询能力,实现地理位置检索

2. 它具备哪些独有特性?

  1. 二维坐标转为一维有序值,地理位置相近的点位,编码前缀基本一致

  2. 支持范围检索、两点距离计算、数据排序

  3. 支持单个点位新增、删除

  4. 存在Geohash边界缺陷:地理上紧紧相邻的两个点,编码前缀可能完全不一样,导致搜不到附近的人

3. 通俗易懂举例:什么是Geohash边界缺陷?

以国境线/海岸线场景举例,最直观:

  • A点:在中国最东边,坐标紧贴国境线

  • B点:仅仅隔一条马路,就在境外,物理距离相隔只有 50米(现实超级近)

  • 但是:两个点刚好落在Geohash网格的两个不同格子里

  • 最终现象:物理距离只有50米,但是geohash编码前缀完全不同

最终业务bug:你站在边界搜附近1公里的人,死活搜得到不到马路对面紧挨着的人。
解决方案:业务层同时查询当前格子 + 周边8个相邻格子,兜底解决边界遗漏问题。

4. 为什么能解决附近的人、附近门店、LBS 距离计算这类场景?

这类业务有明确共性:

  • 核心需求:根据当前坐标,查找指定半径范围内的其他点位

  • 需要实时计算两个坐标之间的直线距离

  • 点位支持动态新增、删除

普通结构痛点:

String、Hash、Set 无法高效实现范围+距离检索;全量遍历计算距离,执行效率极低。

Geo 匹配逻辑:

  • Geohash 完成降维,让相近位置在 ZSet 中天然相邻

  • 借助 ZSet 范围查询,快速筛选指定半径内的点位

  • 内置距离计算命令,无需手动实现算法

5. 它是如何一步步实现业务逻辑的?

以查询附近门店为例

规则定义:

  • Key:geo:shop,统一管理所有门店坐标

  • member:门店ID;score:经纬度转换后的 Geohash 值

执行流程:

  1. 录入门店经纬度
    GEOADD geo:shop 116\.481028 39\.921983 shop\_001
    将经纬度转为 Geohash 编码,存入 ZSet。

  2. 查询当前坐标 1000 米内的门店
    GEORADIUS geo:shop 116\.480000 39\.920000 1000 m
    以当前坐标为中心,筛选出指定半径内的所有门店。

  3. 计算两个门店之间的距离
    GEODIST geo:shop shop\_001 shop\_002 km
    解析坐标并计算两点直线距离,单位为千米。

6. 结构示意图

Geohash降维

经度 + 纬度
二维坐标

一维hash编码

转为 ZSet Score

门店ID

ZSet Member

ZSet 有序集合

范围查询 → 获取附近点位

7. 延伸思考(反问自查)

为什么不单独开发新结构,而是复用 ZSet?

ZSet 已成熟支持排序、范围查询,复用现有结构可以降低开发复杂度,同时保证性能稳定。


四、Stream 流

1. 核心本质是什么?

Redis 5.0 及以上版本新增结构,专为消息队列、数据流设计,属于增强型有序日志结构。

  • 每条消息拥有全局唯一、自增的 ID,格式:时间戳\-序列号

  • 消息支持持久化,消费完成后不会自动删除

  • 原生支持消费组、消息 ACK、断点续读、异常重试

2. 它具备哪些独有特性?

  1. 消息持久化,服务宕机后数据不丢失

  2. 支持消费组,多消费者分摊任务,适配集群消费

  3. ACK 确认机制,手动标记消费完成,避免消费者宕机丢失消息

  4. 支持消息回溯、重复消费

  5. 和 List 队列不同,消息不会被取出后直接删除

3. List队列有什么痛点?Stream是如何一一解决的?

List消息队列痛点Stream解决方案
消息弹出立刻删除,消费者宕机消息直接丢失消息持久化留存,消费不删除,ACK之后才清除待消费标记
无消费组,无法集群多消费者负载均衡原生消费组,天然支持分布式负载均衡
消费失败无法重试,不能回溯历史消息无ACK可以一直重读,支持任意位置消息回溯
只能一头进一头出,消息顺序简单,无全局唯一ID自带时间戳全局ID,支持多维度消息管理

4. 延伸对比:Redis Stream VS 专业MQ(RocketMQ/Kafka)

很多面试官追问:既然Stream补齐了List所有短板,为什么还要用RocketMQ?这里直白分层对比,不长篇大论:

✅ Stream优势

  • 无需额外部署中间件,项目本来就有Redis,零运维成本

  • 轻量易用,代码简单,适合小型异步解耦

❌ Stream硬伤(不如专业MQ)

  1. 消息堆积能力弱:Redis内存数据库,消息大量堆积会打爆内存;Kafka/RocketMQ磁盘存储,无惧海量堆积

  2. 没有消息重试队列、死信队列、消息延时队列:业务异常重试、延时订单完全没法原生支持

  3. 没有消息轨迹、消息重试次数、消息回溯时间粒度粗糙

  4. 分区/分片能力弱,单机上限明显,无法支撑百万级TPS大流量

📌 最终选型标准

  • 内部小系统、低TPS、简单异步解耦、不想部署MQ → Stream

  • 交易订单、高并发流量、需要死信/延时/重试、消息轨迹 → 必须RocketMQ/Kafka

5. 它是如何一步步实现业务逻辑的?

以订单异步通知队列为例

规则定义:

  • Key:stream:order:notify,消息队列名称

执行流程:

  1. 生产者发送订单消息
    XADD stream:order:notify \* userId 1001 orderId 2026052901 money 99
    \* 表示由 Redis 自动生成消息ID,消息写入队列并持久化。

  2. 创建消费组
    XGROUP CREATE stream:order:notify group1 0
    创建消费组,从队列第一条消息开始消费。

  3. 消费者阻塞拉取消息
    XREADGROUP GROUP group1 consumer1 COUNT 1 BLOCK 0 STREAMS stream:order:notify \>
    阻塞等待新消息,拉取待处理数据。

  4. 消费完成,手动确认
    XACK stream:order:notify group1 1789000000000\-0
    告知队列当前消息已正常处理,清除待消费标记。

6. 架构示意图

生产者

Stream 消息队列
消息持久化留存,消费不删除

消费组 group1

消费者 1

消费者 2

执行业务逻辑

XACK确认消费完成,消息才真正标记已处理

7. 延伸思考(反问自查)

Stream 和 Kafka、RocketMQ 如何选型?

中小流量、简单异步任务选用 Stream;高并发交易、海量消息堆积、需要延时/死信队列,必须使用专业消息中间件。


五、布隆过滤器 Bloom Filter

1. 核心本质是什么?

布隆过滤器是概率型存在判断结构,Redis 无原生命令,业界标准实现方案:Bitmap + 多个哈希函数。

  1. 开辟一段连续的 bit 内存空间

  2. 使用多个不同哈希函数,将一个元素映射到多个不同 bit 位

  3. 根据 bit 位状态,判断元素是否存在

2. 它具备哪些独有特性?

  1. 判断规则:判定不存在,则一定不存在;判定存在,有可能误判(假阳性)

  2. 内存占用极小,海量数据场景下,开销远低于 Set、Hash

  3. 普通布隆过滤器无法删除元素,删除操作会干扰其他元素的判断

  4. 增大 bit 空间、增加哈希函数数量,可以降低误判概率

3. 为什么能解决缓存穿透、黑名单、URL 去重这类场景?

这类业务有明确共性:

  • 核心需求:快速拦截绝对不存在的数据

  • 数据体量庞大,不适合存储全部数据明细

  • 允许极低概率的误判

传统方案痛点:

大量请求查询不存在的数据,会直接穿透到数据库,压垮 DB;使用 Set 存储全量数据,内存开销巨大。

布隆过滤器匹配逻辑:

  • 先做前置拦截,判定不存在则直接返回,不查询缓存与数据库

  • 判定存在才走正常查询流程

  • 极小的内存开销,即可拦截绝大部分无效请求

4. 它是如何一步步实现业务逻辑的?

以**防缓存穿透(用户查询)**为例

规则定义:

  • 底层依托 Bitmap,Key:bloom:user

  • 使用 3 个独立哈希函数,一个元素对应 3 个 bit 位

执行流程:

  1. 预热阶段:将数据库已存在的用户写入过滤器
   // 对用户ID计算3个哈希偏移量
   h1 = hash1\(userId\)
   h2 = hash2\(userId\)
   h3 = hash3\(userId\)
   //将对应 3 个 bit 位全部置 1。
   SETBIT bloom:user h1 1
   SETBIT bloom:user h2 1
   SETBIT bloom:user h3 1`
  1. 线上查询:判断用户是否存在
   b1 = GETBIT bloom:user h1
   b2 = GETBIT bloom:user h2
   b3 = GETBIT bloom:user h3
   
   if b1 && b2 && b3:
       // 大概率存在 → 先查Redis缓存,缓存未命中再查DB
   else:
      // 一定不存在 → 直接返回,不查询缓存与DB`

5. 原理示意图

是

否

元素ID

哈希函数1 → 偏移位1

哈希函数2 → 偏移位2

哈希函数3 → 偏移位3

Bitmap 位图

所有bit位都为1?

可能存在

一定不存在

6. 延伸思考(反问自查)

如何解决普通布隆过滤器不能删除元素的问题?

改用计数布隆过滤器,将单个 bit 位改为计数器,支持数值递减,实现安全删除。


六、全文汇总对比表

数据结构底层依赖核心能力设计取舍典型业务场景
BitmapStringbit 级二值状态存储、精准批量统计内存随ID增大上涨,无法跨key合并用户签到、在线状态、权限标记
HyperLogLog独立结构海量数据去重大盘统计牺牲微小精度,永久固定12KB内存,支持多key合并页面UV、日活运营大盘
GeoZSet地理位置检索、距离计算存在Geohash网格边界bug附近门店、附近的人、外卖LBS
Stream独立结构可靠轻量级消息队列内存存储,消息堆积能力差,无死信/延时队列内部系统简单异步解耦
布隆过滤器Bitmap快速拦截不存在请求,防缓存穿透存在假阳性误判,普通版本不能删除缓存穿透防护、黑名单过滤

七、Redis所有高级结构统一设计思想(面试升华回答)

  1. 不重复造轮子:能复用String/ZSet就复用底层结构,降低内核复杂度

  2. 业务定向取舍:根据场景主动牺牲精度、功能,换取极致内存和性能

  3. 只做单一专精:每个高级结构只解决一类垂直业务问题,不做大而全

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐