假设你是一座魔法仓库的管家,仓库名叫“Redis“。仓库里不仅能存放各种货物,还藏着四件”镇库神器“,专门用来解决普通储物架搞不定的难题:一面能记录上亿人”今天来没来过“的开关墙(Bitmap)、一个立在门口就能估算客流的神秘小匣子(HyperLogLog)、一张画满网格能一键定位的魔法地图(GeoHash),以及一卷永远写不完的自动流水账本(Stream)。它们不是替代品,而是各有绝活的”特种装备“。学会了它们,你就能用一根铅笔的厚度记录一座城的人每天是否来访,用一只手提箱的空间估算千万级访客量。这篇文章,就是你的”神器使用手册”。

岗位关联

掌握 Redis 高级数据结构,是后端工程师从“会用缓存”走向“能设计高并发统计与消息方案”的分水岭。在日活、UV、LBS、消息队列等场景中,它直接决定你的内存成本和性能上限。

目录
  1. 可视化演示
  2. 一、Bitmap 位图:仓库的开关面板
  3. 二、HyperLogLog:门口的客流估算器
  4. 三、GeoHash:地板上的魔幻网格
  5. 四、Stream:写不完的流水账卷轴
  6. 五、场景选型:给神器贴工作标签
  7. 六、内存与性能:神器的体力图谱
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

Redis高级结构内存与性能对比 HyperLogLog 以固定12KB内存估算百万级基数,适合UV统计等去重计数场景 Bitmap 以每位1bit的密度存储布尔状态,适合签到打卡等大规模状态标记 GeoHash 将经纬度编码为52位分数,利用有序集合实现高效的附近搜索 HLL 内存 ≈ 12KB, 误差 ≤ 0.81% Bitmap: 1用户 = 1bit, 1亿用户 ≈ 12MB 四种高级结构以极低内存实现统计、状态、地理和消息功能

一、Bitmap 位图:仓库的开关面板

想象你管理着一座能容纳上亿人的魔法仓库。为了知道“每个人今天有没有来过”,你请工匠做了面巨大的开关墙:每个固定位置对应一位客人,位置号就是客人 ID。某人来过,就把他的开关拨到“开”。这面墙神奇的地方在于——一千万个开关,占用的墙面却只有一张 A4 纸那么厚。

这面“开关墙”在 Redis 中的名字叫 Bitmap(位图)。它并不是第五种魔法数据类型,而是寄居在 String 身体里的“位级操作术”。你可以通过 SETBIT 在指定下标写入 0 或 1,用 GETBIT 想读哪个读哪个,用 BITCOUNT 统计所有被拨到“开”的开关总数——也就是今天来过的人数。

假设要给 N 个用户每人分配一个开关位,内存开支就是 N/8 个字节。为什么这么省?因为一位(bit)是计算机的最小存储单位,8 个位才凑成一个字节。一个亿级用户的系统,如果每个用户用一个整数 ID 存 Set,至少数百 MB;而 Bitmap 只需要 1.25MB。

回到那面开关墙:与其说你在写代码,不如说你在安排一个个小小的金属拨片。Bitmap 的最大魅力不是“能存”,而是“能掐着内存算着存”——这正是它在高并发统计场景里经久不衰的原因。

内存(字节)=N8\text{内存(字节)} = \frac{N}{8}
  1. N 表示开关总数,即需要覆盖的用户 ID 数N 等于最大用户 ID 加一。比如为 1 亿用户预留位,N 就是 1 亿。
    N=max⁡(user_id)+1N = \max(\text{user\_id}) + 1
  2. 除以 8 把“位”换算成“字节”8 位 = 1 字节,这是计算机存储的基本换算关系。
    1 字节=8 位1 \text{ 字节} = 8 \text{ 位}

二、HyperLogLog:门口的客流估算器

仓库门口又添了一件新宝贝——一个巴掌大的青铜匣子。每位客人进门时只需轻轻“拍”一下匣子,匣子并不记录客人是谁,只在内部某个角落留下一道刮痕。晚上下班,你敲敲匣子,它报出一个数:“今日不同访客约 32850 人”。你清点人工记录,实际是 33120 人,误差不到 1%。

青铜匣子就是 HyperLogLog,简称 HLL。它解决的是“基数统计”问题——统计不重复元素的个数。它不存每个元素本身,而是把每个元素“拍”成一道哈希痕迹,从而用极小内存估算基数。Redis 提供的命令也很简单:PFADD 负责“拍”,PFCOUNT 负责“报数”,PFMERGE 合并多个匣子。

HLL 内部有 16384 个寄存器,每个寄存器占 6bit,所以总内存固定在 16384×6/8=12288 字节,约 12KB。当一个元素进来时,Redis 计算它的哈希值,再看这个哈希值的二进制串开头有多少个连续的 0(这叫“前导零”)——连续 0 越多,说明这个数越“稀有”——把这个信息更新到对应寄存器中。最后,用调和平均把 16384 个值融合成估算基数。

匣子虽然说不清“每个人是谁”,但在“今天大概来了多少人”这种问题上,它用 12KB 的恒定内存,把答案估得八九不离十。对于海量 UV 统计,这种“模糊的正确”远胜“精确的昂贵”。

标准误差≈1.04m=1.0416384=0.0081=0.81%\text{标准误差} \approx \frac{1.04}{\sqrt{m}} = \frac{1.04}{\sqrt{16384}} = 0.0081 = 0.81\%
  1. m 表示 HLL 内部的寄存器个数Redis 固定用 16384 个寄存器,这是 2 的 14 次方。
    m=16384=214m = 16384 = 2^{14}
  2. 1.04 是概率论修正常数为了消除调和平均带来的偏差而引入的经验系数。代入 m=16384,得到 0.008125,即约 0.81% 的误差率——100 万个元素大概会错 8000 个左右。
    1.0416384=1.04128=0.008125\frac{1.04}{\sqrt{16384}} = \frac{1.04}{128} = 0.008125

三、GeoHash:地板上的魔幻网格

为了帮客人快速找到货架,你让魔法师在仓库地板上画了一套网格。每块格子都有唯一的编号,譬如“北一-东三-中五”。只要你报出编号,管家立刻能跑到对应格子上。而且编号越接近,说明格子挨得越近——不需要拿尺子量。

这套网格就是 GeoHash。它的目标是把地球表面切成一层层越来越小的“方格”,并把经纬度这个二维坐标压缩成一串一维字符串。Redis 里的地理位置命令(GEOADD、GEOSEARCH)内部都用 GeoHash 实现。存入深圳的坐标,和存入广州的坐标,因为网格前缀不同,一眼就能判断相隔很远。

GEOADD key lon lat member 负责把某个成员的经纬度写进一个 ZSET(有序集合),score 就是 GeoHash 编码后的整数。GEOSEARCH 则把“附近的人”查询转换为 score 范围查询。编码过程分三步:先对经度区间 [-180,180] 做二分,落在右边记为 1、左边记为 0;再对纬度区间 [-90,90] 同样二分;然后把经度位和纬度位交替穿插,最后每 5 个二进制位用 Base32 编码成一个字符。

现在你明白为什么说“编码越接近,物理距离越近”——因为两个仓库格子挨得越近,它们共享的网格前缀就越长。这就是 GeoHash 把空间几何问题变成字符串比较问题的魔法精髓。

GeoHash=Base32(interleave(lon_bits,lat_bits))\text{GeoHash} = \text{Base32}\left( \text{interleave}(\text{lon\_bits}, \text{lat\_bits}) \right)
  1. lon_bits经度二分编码:把 [-180,180] 不断二分,目标经度落在右半侧记 1、左半侧记 0,直到满足精度。
    [−180,180]→[−180,0) 记0, [0,180] 记1[-180,180] \to [-180,0) \text{ 记} 0,\ [0,180] \text{ 记} 1
  2. lat_bits纬度二分编码:对纬度 [-90,90] 执行同样的二分过程,得到一组 0/1 序列。纬度值落在上半区间记 1,下半区间记 0。
    [−90,90]→[−90,0) 记0, [0,90] 记1[-90,90] \to [-90,0) \text{ 记} 0,\ [0,90] \text{ 记} 1
  3. interleave 交错合并把经度序列和纬度序列按“经度、纬度、经度、纬度”的顺序穿插成一条更长的位串,保证二维信息融合成一维数据。
    bits=[lon1,lat1,lon2,lat2,…]\text{bits} = [\text{lon}_1, \text{lat}_1, \text{lon}_2, \text{lat}_2, \ldots]
  4. Base32 编码每 5 个位映射为 1 个字符(0-9 和去掉易混淆字母的小写字母表)。字符越多,网格越细,精度越高。
    字符数↑⇒网格边长↓\text{字符数} \uparrow \Rightarrow \text{网格边长} \downarrow

四、Stream:写不完的流水账卷轴

仓库角落那卷泛黄的账本,是件自动延伸的魔法道具。每次进货或出货,账本自动在末尾流出一行新记录:时间、货物、数量,绝无涂改。更厉害的是,几个店员可以组成“小组”分头看账,每笔账目只能被小组里一个人认领,干完活要在账上画个勾——其他人看到勾就知道不必再干。

这卷账本就是 Stream,Redis 5.0 引入的“消息流”数据结构。它不像 List 那样只是临时队列,也不像 Pub/Sub 那样发完就消失。Stream 里的消息会持续累积,支持按 ID 范围读取、消费者组分配、消息确认和断点续读。这让它成为轻量级可靠消息队列的首选。

XADD 向 Stream 末尾追加一条消息;XREAD 从某个 ID 之后读取新消息;XGROUP CREATE 为消费组分配游标;XREADGROUP 让组内消费者各自领到不同消息;XACK 确认处理完毕。每一条消息都有一个自动生成的 ID,由“毫秒时间戳”和“序列号”两部分用短横线拼接而成。Redis 保证同一个实例内 ID 全局有序且唯一。

账本上每一行的“时间-序号”双联编号,让你既能按时间线重放所有流程,又能确保每笔账都被一个店员认真处理且只处理一次——这正是可靠消息队列最需要的两样东西。

消息ID=毫秒时间戳-序列号\text{消息ID} = \text{毫秒时间戳} \text{-} \text{序列号}
  1. 毫秒时间戳 (ms_timestamp)消息被追加那一刻的毫秒级时间,保证 ID 随时间单调递增。
    ms_timestamp=当前毫秒时间\text{ms\_timestamp} = \text{当前毫秒时间}
  2. 序列号 (sequence)同一毫秒内的递增序号,解决同毫秒多条消息的 ID 冲突,从 0 开始逐一增加。
    sequence=0,1,2,… (同毫秒内)\text{sequence} = 0, 1, 2, \ldots \ (\text{同毫秒内})

五、场景选型:给神器贴工作标签

四件神器各有绝活,但用错地方就会闹笑话。你不能靠门口的估算匣子确定一个人今天是否来过,也不能在账本卷轴上做签到统计。作为仓库管家,最重要的能力不是背出每件神器的参数,而是“见活拿器”。

先看几个典型需求:签到/日活,每个用户只有“有/无”两种状态,选 Bitmap;页面 UV,只关心不同访客总数且可容忍误差,选 HyperLogLog;附近的人,需要经纬度和距离排序,选 GeoHash;订单/任务流,需要顺序消费和多人协作,选 Stream。

用一个内存估算公式可以直观看到选型差距。假设要统计 N 个用户 ID:用 Set 逐个存 ID,每个 ID 至少 100 字节;用 Bitmap 每位用户只要 1 位;用 HLL 不管多少人都只占 12KB。

给神器贴标签这件事,本质上是把“数据的形状”和“结构的特长”配对:状态类数据找开关墙,总数类数据找估算匣子,坐标类数据找网格地图,流程类数据找账本卷轴。

内存={N×entry_sizeSet / StringN8Bitmap12288 字节HyperLogLog\text{内存} = \begin{cases} N \times \text{entry\_size} & \text{Set / String} \\ \dfrac{N}{8} & \text{Bitmap} \\[6pt] 12288 \text{ 字节} & \text{HyperLogLog} \end{cases}
  1. N × entry_size(Set/String)每个元素都有独立存储开销,内存和 N 线性正比,N 越大越肉疼。
    Mset(N)=N×100∼150 字节M_{set}(N) = N \times 100 \sim 150 \text{ 字节}
  2. N/8(Bitmap)一个用户只占 1 位,所以只需 N/8 字节,比 Set 省两个数量级。
    Mbitmap(N)=N8M_{bitmap}(N) = \frac{N}{8}
  3. 12288 字节(HyperLogLog)HLL 内存恒定不变,无论 N 是 1 万还是 1 亿,都是 12KB。
    Mhll(N)≡12288,∀NM_{hll}(N) \equiv 12288, \quad \forall N

六、内存与性能:神器的体力图谱

选对了神器,还要了解它们的“体力上限”。开关墙的拨动速度极快,但墙再多也不可能无限生长;估算匣子永远 12KB,几乎零负担;网格地图能精确定位,但每次画格子都要算半天;账本卷轴写起来顺畅,却要额外维护“读到哪一行”。

用时间复杂度和空间复杂度来画这幅“体力图谱”。复杂度不关心具体耗时数值,只看数据量翻倍时成本怎么变化。对高并发系统来说,这个“增长趋势”才是选型的真正依据。

Set 存 ID,内存是 O(N) 线性增长;Bitmap 同样是线性,但斜率是 1/8,所以是小头线性;HLL 直接 O(1),恒定 12KB。读写时间上,Bitmap 和 HLL 都是 O(1),GeoHash 需要额外计算编码,Stream 的写入是 O(1) 追加、消费靠游标推进,非常轻。

有了这张体力图谱,你就不再是只会背命令的新管家,而是懂得“哪个神器在哪些活上永不疲惫”的资深调度员。Redis 高级数据结构的真正价值,不是炫技,而是帮你在大规模场景下精准控制每字节成本。

---

空间复杂度={O(N)Set / StringO(N8)BitmapO(1)HyperLogLog\text{空间复杂度} = \begin{cases} O(N) & \text{Set / String} \\ O\left(\dfrac{N}{8}\right) & \text{Bitmap} \\ O(1) & \text{HyperLogLog} \end{cases}
  1. O(N)(Set/String)N 翻倍、内存约翻倍,属于“实打实的线性开销”。
    Mset(2N)≈2×Mset(N)M_{set}(2N) \approx 2 \times M_{set}(N)
  2. O(N/8)(Bitmap)同样线性、系数小,N 翻倍只增长 1/8 的字节数,适合海量状态记录。
    Mbitmap(2N)=2N8=2×Mbitmap(N)M_{bitmap}(2N) = \frac{2N}{8} = 2 \times M_{bitmap}(N)
  3. O(1)(HyperLogLog)N 从 1 千变到 1 亿,内存纹丝不动,这是概率算法的极致魅力。
    Mhll(N)=12288,Mhll(2N)=12288M_{hll}(N) = 12288, \quad M_{hll}(2N) = 12288

AI 应用场景

电商大促的实时 UV 统计

阶段:数据采集与指标计算

解决的问题:大促页面每秒数万次访问,若用 SQL 做 count(distinct user_id),数据量大时耗时极长且内存爆炸;即使存 Redis Set,每个用户 ID 约 100 字节,百万 UV 就是 100MB。HLL 用 12KB 内存 + 0.81% 误差换取实时估算,对运营大屏来说“约为 50 万”已经足够。

具体案例:某电商平台双 11 大屏每小时刷新一次“今日 UV”,后端在用户访问网关时 PFADD 一个 HLL key,大屏通过 PFCOUNT 读取估算值。整日 UV 约 2000 万,HLL 总内存始终保持约 12KB。

亿级用户的连续签到统计

阶段:特征存储与快速查询

解决的问题:用户签到是“今天来没来过”的布尔状态,Set 存用户 ID 浪费空间。Bitmap 用“用户 ID 当下标、日期当 key”的方式,每一天只有 N/8 字节;要统计连续 7 天签到的人,只需 BITOP AND 对 7 天的 Bitmap 做位运算,再 BITCOUNT 统计人数。这在 SQL 里需要大表 JOIN 的活,Redis 一条命令完成。

具体案例:某内容社区上线 7 天签到赢会员活动,每天一个 Bitmap,每日用户数 2000 万,每天内存仅 2.5MB。活动结束后用 BITOP AND 算连续签到用户,再 BITCOUNT 统计人数,全程毫秒级完成。

外卖平台的“附近商家”空间检索

阶段:空间检索与召回

解决的问题:用户在外卖 App 上看到 3 公里内的商家,数据库用经纬度包围盒计算,数据量大时有性能瓶颈。GeoHash 把位置编码成前缀字符串,先按前缀粗筛候选集,再精算真实距离,性能和准确性兼得。

具体案例:某外卖平台把 30 万商家坐标 GEOADD 到 Redis,用户定位后 GEOSEARCH 返回 3km 内的商家 ID 列表,再回表数据库取商家详情。整个空间检索流程稳定在 5ms 以内。

Python 演示

python68 行
import redisimport random # 连接 Redis (本地默认端口)r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)  def bitmap_demo():    """Bitmap: 记录 1000 个用户某天的签到状态"""    key = 'sign:20240101'    r.delete(key)    for uid in range(1000):        if random.random() > 0.4:            r.setbit(key, uid, 1)    signed = r.bitcount(key)    print(f'Bitmap 签到人数: {signed}/1000')    print(f'8号用户是否签到: {bool(r.getbit(key, 8))}')  def hll_demo():    """HyperLogLog: 估算首页 UV"""    key = 'uv:homepage'    r.delete(key)    real_users = set()    for _ in range(50000):        uid = random.randint(0, 9999)        r.pfadd(key, f'user:{uid}')        real_users.add(uid)    estimate = r.pfcount(key)    real = len(real_users)    ratio = abs(estimate - real) / real * 100    print(f'HLL 估算 UV: {estimate}, 真实 UV: {real}, 偏差 {ratio:.2f}%')  def geohash_demo():    """GeoHash: 存入商家坐标并搜索附近"""    key = 'geo:shops'    r.delete(key)    shops = [        ('cafe', 116.397, 39.909),        ('bookstore', 116.410, 39.920),        ('hotel', 116.405, 39.915),    ]    for name, lon, lat in shops:        r.geoadd(key, (lon, lat, name))    near = r.geosearch(key, longitude=116.400, latitude=39.912,                       radius=2, unit='km')    print(f'GeoHash 附近 2km 商家: {near}')  def stream_demo():    """Stream: 生产订单事件并消费"""    key = 'stream:orders'    r.delete(key)    msg_id = r.xadd(key, {'order_id': 'A001', 'amount': '99'})    print(f'Stream 消息 ID: {msg_id}')    msgs = r.xread({key: '0'}, count=5)    for stream_id, entries in msgs:        for mid, fields in entries:            print(f'消费事件: {mid} -> {fields}')    r.xdel(key, msg_id)  if __name__ == '__main__':    bitmap_demo()    hll_demo()    geohash_demo()    stream_demo()
目的:一段代码演示四种高级数据结构的核心操作,包括 Bitmap 签到、HLL 的 UV 估算、GeoHash 的附近商家搜索和 Stream 的生产消费。
  1. r.setbit(key, uid, 1)把第 uid 个开关拨到“开”,表示该用户签到。
  2. r.bitcount(key)统计开了多少个开关,即签到总人数。
  3. r.pfadd(key, f'user:{uid}')把访客 ID“拍”进 HLL 小匣子,不存原文。
  4. r.pfcount(key)让匣子报出估算的独立访客数。
  5. r.geoadd(key, (lon, lat, name))把商家的经纬度编码为网格 score 存入 ZSET。
  6. r.geosearch(...)以某点为中心,搜索半径 2km 内的所有商家名称。
  7. r.xadd(key, {...})向 Stream 账本末尾追加一条订单消息,返回自动生成的 ID。
  8. r.xread({key: '0'}, count=5)从 ID=0 开始读取,即从头消费所有消息。

常见误区

误区我以为 Bitmap 是 Redis 提供的第五种新数据类型,像 List 一样有一整套专属命令和内存结构,专门用来存 true/false 数组。

正解Bitmap 并不是独立的数据类型,它底层就是一个 String 字符串,只是把字符串的每个 bit 当作一个开关来读写。用 SETBIT 写位、GETBIT 读位、BITCOUNT 数位,本质都是对 String 的字节进行位操作。理解了这一点,才会明白为什么 Bitmap 最大只能有 2^32 位——因为 String 最大 512MB,512MB × 8 = 2^32。

为何常见:命令名 SETBIT/GETBIT 太像“专属命令”,让人误以为背后有一套全新的数据结构,实际上它只是 String 的“位级超频用法”。

误区我以为 HLL 和 Set 去重一样,只是内存更省,结果应该是精确的。PFCOUNT 返回一个整数,这个数就是准确人数。

正解HLL 是概率算法,标准误差约 0.81%。它适合“估算每日 UV 约 50 万”这种场景,但绝不适合精确对账、扣费、发奖等场景。需要精确去重时,用 Set 或 Bitmap。判断标准很简单:如果 1 万人算成 10080 人能接受,就用 HLL;差一个人都不行,就别用。

为何常见:PFCOUNT 返回的是一个普通整数,用户天然把整数当作精确值,忽略了它背后“基于哈希分布的概率估算”。

误区我以为 GEOADD 就是把经度、纬度两个数字存进一个二维表或 Hash 里,查询时对每个点算距离再排序。

正解GeoHash 底层是 ZSET(有序集合)。GEOADD 时 Redis 自动把经纬度编码成一个一维的整数 score(GeoHash 值),member 存名称。附近的人 的查询本质是 ZSET 的 score 范围查找,而不是全量距离计算。这也是为什么它在海量坐标下依然很快。

为何常见:命令参数太像“存坐标”,GEOADD 的名字也让用户以为内部就是个坐标表,忽略了“编码成一维 score”才是精髓。

误区我以为 Stream 和 Kafka 概念一样,Redis 只是把它做成了内存版,消息量大了无非就是慢一点。

正解Stream 是 Redis 5.0 引入的轻量可靠消息流,支持持久化、消费者组和 ACK 确认,但它本质是单机内存/磁盘模型。它没有 Kafka 的分布式分区、副本同步、磁盘顺序读写优化和水平扩展能力。消息量到 TB 级或需要跨多机高可用时,Stream 远不如 Kafka。它更适合中小流量的内部任务队列,比如“订单创建后发短信通知”。

为何常见:两者都有“消费者组”“偏移量”“确认”等概念,表面太像,让人忽略了“单机数据结构 vs 分布式消息系统”的本质差距。

误区我以为学会了 HLL、Bitmap 等高级结构,就可以抛弃 String、List、Hash,所有数据都用新结构来存。

正解高级结构是解决特定问题的“特种装备”。HLL 和 Bitmap 无法存储业务字段,GeoHash 无法替代 Hash 存商品详情,Stream 也替代不了 List 的临时缓存。真实项目里通常是基础类型 + 高级结构混搭:Hash 存用户详情,Bitmap 存签到状态,Stream 串联异步任务。

为何常见:教程单独介绍高级结构时,容易让初学者觉得它们是“升级版基础类型”,实际上它们更像是“补充工具”,各有明确分工。

面试题

Q1. Redis 的 Bitmap 底层是什么类型?

单选题 · easy

  • A.独立的位数组类型
  • B.String 类型← 正确答案
  • C.List 类型
  • D.Hash 类型

答案:B。Bitmap 不是独立的数据类型,它底层就是 String。Redis 把 String 的每个 bit 当作一个开关来读写,String 最大 512MB,所以 Bitmap 最多 2^32 位。其他选项都是 Redis 的其他基础类型。

Q2. HyperLogLog 的标准误差约为多少?

单选题 · mid

  • A.0.08%
  • B.0.81%← 正确答案
  • C.8.10%
  • D.81.00%

答案:B。HLL 使用 16384 个寄存器,标准误差公式为 1.04/√16384 ≈ 0.81%。它用可接受的误差换取了 12KB 的恒定内存,适合海量 UV 估算。

Q3. 下列哪些是 Bitmap 的常用命令?

多选题 · mid

  • A.SETBIT 设置位值← 正确答案
  • B.GETBIT 读取位值← 正确答案
  • C.BITCOUNT 统计位数← 正确答案
  • D.PFADD 增加基数

答案:ABC。SETBIT/GETBIT/BITCOUNT 都是 Bitmap 的核心命令,分别负责写位、读位、数位。PFADD 是 HyperLogLog 的命令,与 Bitmap 无关。

Q4. 一千万用户每天签到用 Bitmap 存储约需多少内存?

单选题 · easy

  • A.1.25MB← 正确答案
  • B.12.5MB
  • C.125MB
  • D.1.25GB

答案:A。Bitmap 每位用户只需 1 个 bit,1000 万 bit = 1000 万/8 字节 = 125 万字节 ≈ 1.22MB,约等于 1.25MB。同样的数据用 Set 存 ID 大约需要 1GB 以上。

Q5. GeoHash 编码精度主要取决于什么?

单选题 · mid

  • A.地球半径大小
  • B.编码字符串长度← 正确答案
  • C.哈希函数强弱
  • D.坐标基准位置

答案:B。GeoHash 编码字符串越长,网格被切得越细,定位精度越高。6 位编码误差约 ±0.61km,8 位约 ±19m。地球半径、哈希函数、基准位置与精度无关。

Q6. 关于 HyperLogLog 的描述正确的有哪些?

多选题 · hard

  • A.固定约 12KB 内存← 正确答案
  • B.存在 0.81% 误差← 正确答案
  • C.可精确去重统计
  • D.适合大规模基数← 正确答案

答案:ABD。HLL 的 16384 个寄存器固定占 12KB,标准误差约 0.81%,适合统计海量不重复元素。它本质是概率估算,不可能精确统计,所以 C 错误。

Q7. Stream 消息 ID 的特点有哪些?

多选题 · mid

  • A.包含毫秒时间戳← 正确答案
  • B.包含递增序列号← 正确答案
  • C.相同实例内唯一← 正确答案
  • D.包含随机因子防重

答案:ABC。Stream 消息 ID 格式为“毫秒时间戳-序列号”,由 Redis 自动生成。时间戳保证随时间的递增,序列号解决同一毫秒内的冲突,因此同一实例内 ID 全局有序且唯一。随机因子不属于 ID 结构。

Q8. Stream 消费者组相关的概念包括哪些?

多选题 · hard

  • A.XGROUP 创建组← 正确答案
  • B.XACK 确认消息← 正确答案
  • C.XPOP 弹出消息
  • D.XREADGROUP 组内读← 正确答案

答案:ABD。XGROUP 用于创建消费者组,XREADGROUP 让组内消费者读取消息,XACK 确认处理结果。XPOP 不是 Stream 的命令,List 才有 LPOP 弹出,ZSET 才有 ZPOPMAX。

Q9. 统计用户连续签到天数最适合用哪种结构?

单选题 · easy

  • A.String 存字符串
  • B.Bitmap 位图← 正确答案
  • C.List 存列表
  • D.Set 存集合

答案:B。签到是“今天来没来过”的布尔状态。Bitmap 用用户 ID 当位下标、日期当 key,每天只占 N/8 字节;配合 BITOP AND 可快速求多天连续签到用户。其他结构要么费内存、要么无法高效做位运算。

Q10. HyperLogLog 估算基数的核心原理是什么?

单选题 · hard

  • A.利用哈希值前导零分布← 正确答案
  • B.使用双端链表记录元素
  • C.通过 B+ 树索引排序
  • D.依赖哨兵位图统计

答案:A。HLL 把每个元素哈希成二进制串,记录“开头连续 0 的最大个数”。连续 0 越多,说明对应的随机值越罕见,由此反推集合的基数大小,再通过 16384 个寄存器的调和平均得到稳定估算。B/C/D 都与 HLL 的实现无关。