第 10 章

Bitmap、HyperLogLog 与 GEO:三种特殊数据类型

第十章:Bitmap、HyperLogLog 与 GEO——三种特殊数据类型

10.1 Bitmap:位图操作

10.1.1 底层存储原理

Bitmap 在 Redis 中并非独立的数据类型,而是建立在 String 类型的 SDS 之上的位操作接口。

Redis String 内存结构(存储 Bitmap 时):

SDS 结构:
┌──────────────┬───────┬───────────────────────────────────┐
│  sdshdr8     │  len  │              buf[]                 │
│  alloc       │  free │  [byte0][byte1][byte2]...[byteN]   │
└──────────────┴───────┴───────────────────────────────────┘

每个字节存储8个bit,采用大端(Big-Endian)位序:
byte0: bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
        ↑最高位=offset 0              ↑最低位=offset 7

SETBIT key 7 1  →  byte0 的 bit0 设为1
SETBIT key 0 1  →  byte0 的 bit7 设为1
SETBIT key 8 1  →  byte1 的 bit7 设为1

字节偏移计算

/* bitops.c — SETBIT/GETBIT 核心逻辑 */
void setBitCommand(client *c) {
    robj *o;
    char *err = "bit is not an integer or out of range";
    uint64_t bitoffset;
    ssize_t byte, bit;
    int byteval, bitval;

    if (getUnsignedLongLongFromObjectOrReply(c, c->argv[2], &bitoffset, err))
        return;

    byte = bitoffset >> 3;         /* bitoffset / 8 → 字节索引 */
    bit  = 7 - (bitoffset & 0x7);  /* 7 - (bitoffset % 8) → 字节内位偏移(大端) */

    /* 如果字符串需要扩展,扩展到 byte+1 字节 */
    o = lookupKeyWrite(c->db, c->argv[1]);
    if (!o) {
        o = createObject(OBJ_STRING, sdsnewlen(NULL, byte+1));
        dbAdd(c->db, c->argv[1], o);
    } else {
        /* 扩展 SDS */
        if (sdslen(o->ptr) <= (size_t)byte) {
            o->ptr = sdsgrowzero(o->ptr, byte+1);
        }
    }

    /* 读取当前字节,修改指定位,写回 */
    byteval = ((uint8_t*)o->ptr)[byte];
    bitval  = (byteval >> bit) & 1;    /* 读取原值 */
    ((uint8_t*)o->ptr)[byte] &= ~(1 << bit);       /* 清零该位 */
    ((uint8_t*)o->ptr)[byte] |= (newbitval << bit); /* 设置新值 */

    addReply(c, bitval ? shared.cone : shared.czero);
}

10.1.2 BITCOUNT:SWAR 算法统计 1 的个数

/* bitops.c — 统计1的个数,使用 SWAR(SIMD Within A Register)算法 */

/* 标准的 popcount 技巧 — 每次处理 8 字节 */
static long popcount(void *s, long count) {
    long bits = 0;
    unsigned char *p = s;
    uint64_t *p8 = (uint64_t *)s;

    /* 主循环:每次处理8字节(64位) */
    while (count >= 8) {
        uint64_t x = *p8;
        /* SWAR 算法:并行计算64位中每个位的1的个数 */
        x = x - ((x >> 1) & 0x5555555555555555ULL);
        x = (x & 0x3333333333333333ULL) + ((x >> 2) & 0x3333333333333333ULL);
        x = (x + (x >> 4)) & 0x0f0f0f0f0f0f0f0fULL;
        /* 将每个字节的4bit计数累加 */
        bits += (x * 0x0101010101010101ULL) >> 56;
        p8++;
        count -= 8;
    }

    /* 处理剩余字节(< 8字节)—— 使用查表法 */
    p = (unsigned char *)p8;
    while (count--) bits += _bitsinbyte[*p++];

    return bits;
}

SWAR 算法解析(以 16bit 为例)

原始数:1011 0110 1101 0011 = 9个1

步骤1:相邻位相加(2个一组)
  x = x - ((x >> 1) & 0x5555) = 0x5555 = 0101 0101 0101 0101
  1011 0110 1101 0011
- 0101 0011 0110 0001   (x >> 1) & 0x5555
= 0110 0011 0111 0010   每2bit存储该组1的个数

步骤2:相邻2bit组相加(4个一组)
  x & 0x3333 + (x>>2) & 0x3333
  = 0010 0011 0011 0010   每4bit存储该组1的个数(0,2,3,2)
    ↑0   ↑2    ↑3   ↑2  = 7+2=9 ✓

步骤3:相邻4bit组相加(8个一组,即每字节)
  (x + (x >> 4)) & 0x0f0f = 每字节存储1的个数

步骤4:乘以 0x0101...0101 后取最高字节 = 所有字节1的数量之和

Redis 7.0 还支持 BITCOUNT key start end BYTE|BIT 语法,其中 BIT 模式允许指定精确的位范围而非字节范围:

BITCOUNT key 0 7          → 统计第0-7字节中1的个数(整个key)
BITCOUNT key 0 7 BYTE     → 同上(显式指定字节模式)
BITCOUNT key 0 63 BIT     → 统计第0-63个bit中1的个数(前8字节)

10.1.3 BITOP:位运算操作

/* bitops.c — 对多个字符串做 AND/OR/XOR/NOT */
void bitopCommand(client *c) {
    char *opname = c->argv[1]->ptr;
    /* op:AND=0, OR=1, XOR=2, NOT=3 */
    
    /* 找到最长字符串的长度 */
    maxlen = 0;
    for (j = 0; j < numkeys; j++) {
        if (strlen(o[j]) > maxlen) maxlen = strlen(o[j]);
    }

    /* 分配结果缓冲区,O(maxlen) 时间复杂度 */
    unsigned char *res = zmalloc(maxlen);

    /* 按字节逐字节计算 */
    for (j = 0; j < maxlen; j++) {
        unsigned char output = (j < strlen(src[0])) ? src[0][j] : 0;
        if (op == BITOP_NOT) output = ~output;
        for (i = 1; i < numkeys; i++) {
            unsigned char byte = (j < strlen(src[i])) ? src[i][j] : 0;
            switch (op) {
                case BITOP_AND: output &= byte; break;
                case BITOP_OR:  output |= byte; break;
                case BITOP_XOR: output ^= byte; break;
            }
        }
        res[j] = output;
    }
    /* 写入目标 key */
}

时间复杂度:O(N),N 为最长字符串字节数。注意:若两个字符串长度差异大,BITOP AND 会用0填充短的那个。

10.1.4 实际应用案例

案例一:用户签到系统

设计:
  key 格式: "sign:{year}:{month}:{userid}"
  offset = day - 1(1号对应0,31号对应30)

操作:
  SETBIT sign:2024:01:10086 0  1   # 1月1日签到
  SETBIT sign:2024:01:10086 14 1   # 1月15日签到
  BITCOUNT sign:2024:01:10086      # 统计1月签到总天数

内存计算:
  1个月:ceil(31/8) = 4字节 per user per month
  1亿用户 × 12个月 × 4字节 = 4.8GB(可接受)

BITPOS 应用——查找最早未签到日:
  BITPOS sign:2024:01:10086 0   # 找第一个0位(未签到的天)
  → 返回位偏移,即第几天(0-indexed)

案例二:用户在线状态

key: "online:{date}"
offset: user_id(假设 user_id 为整数)

SETBIT online:2024-01-15 12345678 1   # 用户12345678上线
GETBIT online:2024-01-15 12345678     # 查询是否在线
BITCOUNT online:2024-01-15            # 当日在线用户数

内存计算(1亿用户):
  最大 user_id = 100000000
  ceil(100000000 / 8) = 12.5MB per day
  远比 Hash 或 Set 存储用户ID高效(Set:每个ID 8字节 = 800MB)

案例三:手动实现布隆过滤器

布隆过滤器原理:
  k 个哈希函数,每个函数将元素映射到 m 个 bit 位之一

Redis 实现:
  key: "bloom:filter"
  插入元素 x:
    h1 = hash1(x) % m → SETBIT bloom:filter h1 1
    h2 = hash2(x) % m → SETBIT bloom:filter h2 1
    h3 = hash3(x) % m → SETBIT bloom:filter h3 1
  
  查询元素 x:
    h1, h2, h3 三个位置均为1 → 可能存在(有误判率)
    任一位置为0 → 一定不存在

推荐:直接使用 RedisBloom 模块(BLOOM.ADD/BLOOM.EXISTS)
     提供比手动实现更优的误判率和内存效率

10.2 HyperLogLog:基数估算

10.2.1 基数估算问题

问题定义:给定一个数据流,统计其中不重复元素的数量(基数)。

精确方法 vs 近似方法:

精确方法(使用 Set):
  SADD uv:page user1 user2 user3 ...
  SCARD uv:page → 精确基数
  内存:每个元素 ~60字节 → 1亿 UV ≈ 6GB

HyperLogLog(误差 0.81%):
  PFADD uv:page user1 user2 user3 ...
  PFCOUNT uv:page → 近似基数
  内存:最大 12KB(固定)→ 节省 99.99%+ 的内存

10.2.2 LogLog 算法原理

基础 LogLog 直觉:
  均匀随机哈希函数 h:element → [0, 2^64)
  考察哈希值的前导零个数(leading zeros):
    P(前导零 >= k) = 1/2^k
  
  如果我们见过最长的前导零序列为 k:
    → 估算:大约遇到了 2^k 个不同元素
  
  例子:
    hash(user1) = 0b0001xxxx...  → 3个前导零
    hash(user2) = 0b01xxxxxx...  → 1个前导零
    hash(user3) = 0b00001xxx...  → 4个前导零
    max_leading_zeros = 4
    估算基数 ≈ 2^4 = 16(实际只有3个,误差大!)

问题:单个估算器方差太大。

HyperLogLog 改进:
  将元素分成 m = 16384 个桶
  用哈希值的前 14bit 决定桶编号
  用剩余 bit 计算前导零
  对所有桶的估算值取调和平均(Harmonic Mean)
  标准误差 = 1.04 / sqrt(m) = 1.04 / sqrt(16384) ≈ 0.81%

10.2.3 Redis HyperLogLog 实现

/* hyperloglog.c — HyperLogLog 的核心数据结构 */

/* 稠密表示:固定 6bit/桶,16384桶 */
#define HLL_P 14                    /* 桶数指数:2^14 = 16384 桶 */
#define HLL_REGISTERS (1<<HLL_P)    /* = 16384 */
#define HLL_BITS 6                  /* 每个寄存器 6bit(最大值63,足够存储 2^63 的前导零) */
#define HLL_DENSE_SIZE                                              \
    (HLL_REGISTERS * HLL_BITS / 8) /* = 16384 * 6 / 8 = 12288 字节 = 12KB */

/* HyperLogLog 对象头部 */
struct hllhdr {
    char magic[4];     /* "HYLL" */
    uint8_t encoding;  /* HLL_DENSE=0 or HLL_SPARSE=1 */
    uint8_t notused[3];
    uint8_t card[8];   /* 缓存的基数估算(最后一次 PFCOUNT 的结果)*/
    uint8_t registers[]; /* 实际的寄存器数组(稠密或稀疏) */
};

稀疏表示(Sparse Representation)

当大多数桶的值为0时(初始状态或很少数据),用变长编码节省内存:

编码字节格式:
  00xxxxxx  — ZERO:连续 xxxxxx+1 个桶(值为0),最多64个连续零桶
  01xxxxxx  — XZERO:连续 xxxxxx*256+下一字节 个桶(值为0),最多16384个
  1vvvvvxx  — VAL:接下来 xx+1 个桶的值均为 vvvvv

示例(16384个桶,只有3个非零):
  XZERO 1000      → 桶0-999全为0
  VAL 5 1         → 桶1000 = 5
  XZERO 5000      → 桶1001-5999全为0
  VAL 3 1         → 桶6000 = 3
  ...
  总大小:远小于 12KB

升级到稠密表示的条件:
  稀疏表示字节数 > server.hll_sparse_max_bytes (默认3000)

PFADD 操作

int hllAdd(robj *o, unsigned char *ele, size_t elesize) {
    struct hllhdr *hdr = o->ptr;
    switch (hdr->encoding) {
        case HLL_DENSE:  return hllDenseAdd(hdr->registers, ele, elesize);
        case HLL_SPARSE: return hllSparseAdd(o, ele, elesize);
    }
}

/* 稠密模式:更新寄存器 */
int hllDenseAdd(uint8_t *registers, unsigned char *ele, size_t elesize) {
    long index;
    uint8_t count;

    /* 计算元素哈希值,取前 HLL_P=14bit 作为桶索引 */
    hllPatLen(ele, elesize, &index, &count);
    /* count = 哈希值剩余部分的前导零个数 + 1 */

    /* 如果 count > 当前寄存器值,则更新 */
    HLL_DENSE_GET_REGISTER(old, registers, index);  /* 读取6bit寄存器 */
    if (count > old) {
        HLL_DENSE_SET_REGISTER(registers, index, count);  /* 写入6bit */
        return 1;  /* 修改了寄存器,通知调用方缓存失效 */
    }
    return 0;  /* 无变化 */
}

/* 6bit 寄存器的读取(跨字节访问)*/
#define HLL_DENSE_GET_REGISTER(target, p, regnum) do { \
    uint8_t *_p = (uint8_t*) p; \
    unsigned long _byte = regnum*HLL_BITS/8; \
    unsigned long _fb = regnum*HLL_BITS&7; \
    unsigned long _fb8 = 8 - _fb; \
    unsigned long b0 = _p[_byte]; \
    unsigned long b1 = _p[_byte+1]; \
    target = ((b0 >> _fb) | (b1 << _fb8)) & HLL_REGISTER_MAX; \
} while(0)

PFCOUNT 操作(计算调和平均)

uint64_t hllCount(struct hllhdr *hdr, int *invalid) {
    double m = HLL_REGISTERS;
    double E;
    int ez = 0;  /* 值为0的寄存器数量 */
    double sum = 0;

    /* 调和平均:sum = Σ(2^(-M[j])) for j = 0 to m-1 */
    for (j = 0; j < HLL_REGISTERS; j++) {
        HLL_DENSE_GET_REGISTER(val, hdr->registers, j);
        if (val == 0) {
            ez++;
            sum += 1;  /* 2^(-0) = 1 */
        } else {
            sum += 1.0 / (1LL << val);  /* 2^(-val) */
        }
    }

    /* 基本估算:E = alpha_m * m^2 / sum */
    E = (0.7213/(1+1.079/m)) * m * m / sum;

    /* 小范围修正(当估算值 < 2.5m 时,使用 LinearCounting) */
    if (E < m * 2.5 && ez != 0) {
        E = m * log(m / ez);
    }
    /* 大范围修正(接近 2^32 时修正哈希碰撞) */
    /* ... */

    return (uint64_t)E;
}

10.2.4 PFMERGE:合并多个 HyperLogLog

PFMERGE uv:week uv:monday uv:tuesday uv:wednesday

操作原理:
  对每个桶 j,取所有源 HLL 中该桶值的最大值
  max_register[j] = max(hll1[j], hll2[j], hll3[j])

  这是 HyperLogLog 的数学特性:并集的基数估算
  等价于将所有元素加入同一个 HLL 的结果

时间复杂度:O(m × k),m=16384桶,k=源HLL数量

10.2.5 适用与不适用场景

适用:
  - 网站 UV 统计(每天独立访客数)
  - 唯一 IP 计数
  - 搜索词去重计数
  - 误差 ≤ 1% 可接受的所有基数问题

不适用:
  - 需要精确计数 → 用 Set(SCARD)
  - 需要查询某元素是否存在 → 用 Set(SISMEMBER)或 Bloom Filter
  - 需要遍历/导出元素列表 → 用 Set
  - 基数很小(< 100)→ HLL 精度反而差,用 Set 更好

HyperLogLog 的限制:
  - 只支持 PFADD(不支持删除元素!)
  - 误差是统计性的,偶尔会有 > 1% 的误差(尾部概率)
  - PFCOUNT 合并多个 key 时有额外内存开销(临时合并)

10.3 GEO:地理位置数据类型

10.3.1 GeoHash 编码原理

Redis GEO 使用 GeoHash 将二维坐标(经纬度)编码为一维整数,存入 ZSet。

GeoHash 编码过程(52bit 精度):

1. 经度范围:[-180, 180]
   纬度范围:[-90, 90]

2. 二分编码(以经度 116.397128 为例):
   第1次:[-180, 0] | [0, 180] → 116.397 > 0 → bit=1,范围缩小为[0, 180]
   第2次:[0, 90] | [90, 180] → 116.397 > 90 → bit=1,范围缩小为[90, 180]
   第3次:[90, 135] | [135, 180] → 116.397 < 135 → bit=0,范围缩小为[90, 135]
   ...(共26次)

3. 纬度同理:26次二分 → 26bit

4. 交织编码(Interleave):
   lon_bits = b25 b24 b23 ... b1 b0  (26bit)
   lat_bits = b25 b24 b23 ... b1 b0  (26bit)
   geohash  = lon25 lat25 lon24 lat24 ... lon0 lat0  (52bit)

5. 存入 ZSet:
   member = "beijing"
   score  = geohash_52bit(作为 double 存储,double 有 53bit 有效精度,够用)

精度分析

bit 精度 vs 地理精度:
  52bit:精度约 0.6mm(每格约 0.6mm × 0.6mm)
  实际 Redis 精度(受 double 精度限制):
    经度精度:约 0.0000001 度 ≈ 11mm
    纬度精度:约 0.0000001 度 ≈ 11mm
  实用精度:约厘米级

10.3.2 GEO 命令

GEOADD

/* geo.c — 将经纬度编码为 GeoHash,存入 ZSet */
void geoaddCommand(client *c) {
    /* 参数:GEOADD key [NX|XX] [CH] lon lat member [lon lat member ...] */
    int i;
    for (i = 3; i < c->argc; i += 3) {
        double xy[2];
        xy[0] = atof(c->argv[i]->ptr);    /* longitude */
        xy[1] = atof(c->argv[i+1]->ptr);  /* latitude  */
        
        /* 编码为 52bit GeoHash */
        GeoHashBits hash;
        geohashEncodeWGS84(xy[1], xy[0], GEO_STEP_MAX, &hash);
        GeoHashFix52Bits bits = geohashAlign52Bits(hash);
        
        /* 以 double 形式存入 ZSet */
        score = (double)bits;
        zsetAdd(zobj, score, member_sds, ...);
    }
}

GEODIST

GEODIST locations beijing shanghai km

计算过程:
  1. 从 ZSet 中取出两个 member 的 score(GeoHash 52bit)
  2. 解码 GeoHash → 经纬度
  3. 用 Haversine 公式计算球面距离

Haversine 公式:
  a = sin²(Δlat/2) + cos(lat1) × cos(lat2) × sin²(Δlon/2)
  c = 2 × atan2(√a, √(1-a))
  d = R × c   (R = 6372797.560856 米,地球平均半径)

GEOSEARCH(Redis 6.2 新增,替代 GEORADIUS):

GEOSEARCH key FROMMEMBER member | FROMLONLAT lon lat
          BYRADIUS radius m|km|ft|mi | BYBOX width height m|km|ft|mi
          [ASC|DESC]
          [COUNT count [ANY]]
          [WITHCOORD] [WITHDIST] [WITHHASH]

示例:
  GEOSEARCH locations FROMLONLAT 116.397 39.916 BYRADIUS 10 km ASC COUNT 10
  → 返回距离 (116.397, 39.916) 10km 内最近的10个地点

替代了已废弃的:
  GEORADIUS key longitude latitude radius unit
  GEORADIUSBYMEMBER key member radius unit

10.3.3 圆形范围查询原理

GEO 范围查询的核心在于 GeoHash 的空间局部性:相邻的 GeoHash 值对应地理上相近的位置。

圆形范围查询算法(GEORADIUS/GEOSEARCH):

步骤1:找到圆心对应的 GeoHash 格
步骤2:计算需要覆盖目标圆的最少 GeoHash 格集合
       GeoHash 格是矩形,需要多个矩形覆盖圆形
       
  示意(圆形被矩形 GeoHash 格覆盖):
  ┌───┬───┬───┐
  │   │   │   │
  ├───┼───┼───┤
  │   │ ◎ │   │  ← 中心格 + 8个相邻格 = 9个格(最少情况)
  ├───┼───┼───┤
  │   │   │   │
  └───┴───┴───┘
  
步骤3:将每个 GeoHash 格转换为 ZSet score 范围
       [格的最小score, 格的最大score]
步骤4:对 ZSet 执行多次 ZRANGEBYSCORE,取并集
步骤5:过滤结果:计算精确距离,去掉超出半径的点(GeoHash 格是矩形,会有误判)
/* geo.c — 找到覆盖目标区域的 GeoHash 格 */
int geoGetPointsInRange(robj *zobj, double lon, double lat,
                        double radius, double shape_type, ...) {
    GeoHashRadius georadius;
    if (shape_type == CIRCULAR_TYPE) {
        georadius = geohashGetAreasByRadius(lat, lon, radius);
    } else { /* RECTANGLE_TYPE */
        georadius = geohashGetAreasByBoxWGS84(lat, lon, height/2, width/2);
    }

    /* georadius 包含9个 GeoHash 格的范围 */
    GeoHashRange ranges[9];
    int count = geohashCalculateAreasByShape(georadius, ranges);

    /* 对每个 GeoHash 格范围查 ZSet */
    for (i = 0; i < count; i++) {
        /* ZRANGEBYSCORE 获取该格内所有 member */
        /* 对每个 member 计算精确距离,过滤 */
    }
}

10.3.4 实际案例

案例一:附近的人

GEOADD users 116.397 39.916 "user:10086"
GEOADD users 116.410 39.920 "user:20086"
GEOADD users 121.473 31.230 "user:30086"  # 上海用户

# 查找距离用户10086 1km内的其他用户
GEOSEARCH users FROMMEMBER "user:10086" BYRADIUS 1 km ASC WITHCOORD WITHDIST

# 返回示例:
# 1) 1) "user:20086"
#    2) "0.8532"   (km)
#    3) 1) "116.41"
#       2) "39.92"

案例二:外卖配送范围

# 商家坐标
GEOADD restaurants 116.400 39.915 "kfc:001"
GEOADD restaurants 116.405 39.918 "mcdonalds:001"

# 用户下单,查找3km内的商家,按距离排序,最多返回20家
GEOSEARCH restaurants FROMLONLAT 116.397 39.916 BYRADIUS 3 km ASC COUNT 20 WITHDIST

案例三:门店距离排序

# 批量添加门店坐标
GEOADD stores 
  116.397 39.916 "store:beijing-01"
  121.473 31.230 "store:shanghai-01"
  ...

# 计算用户到特定门店的距离
GEODIST stores store:beijing-01 store:shanghai-01 km
→ "1068.48"  (北京到上海直线距离约1068km)

10.3.5 GEO 的局限性

1. 只支持圆形或矩形范围(GEOSEARCH BYBOX)
   不支持多边形范围(如行政区划边界)
   → 需要多边形支持时,考虑 Tair GIS(阿里云 Redis 扩展)

2. 查询精度限制
   GeoHash 编码后精度约 0.6mm,但实际受 double 浮点精度影响约11mm
   足够日常应用,不足以用于精密测量

3. 不支持高度/海拔
   GEO 只处理二维坐标
   需要三维时,需自定义编码方案

4. 大范围查询性能
   GEOSEARCH BYRADIUS 100 km 可能扫描很多 ZSet 成员
   建议:大城市场景,radius 控制在 50km 以内以保证性能

10.4 三种类型的内存对比

场景:统计 1 亿用户中有多少人访问过某页面

方案一:Set(精确)
  SADD uv:page userID1 userID2 ...
  内存:1亿 × 平均60字节 ≈ 6GB

方案二:Bitmap(精确,需要用户ID是整数且范围有限)
  SETBIT uv:page userID 1
  内存:maxUserID/8 ≈ 100000000/8 = 12.5MB

方案三:HyperLogLog(近似,0.81% 误差)
  PFADD uv:page userID1 userID2 ...
  内存:最大 12KB(与数量无关!)

场景:记录 1 亿个地理位置坐标

GEO(ZSet 底层):
  每个成员:SDS成员名 + ZSet节点开销 ≈ 50-100字节
  1亿个:5-10GB

Bitmap 地理位置编码:
  不适用(二维坐标无法直接用Bitmap表达)

结论:
  精确计数,小基数 → Set
  精确计数,整数ID → Bitmap(内存最省)
  近似计数 → HyperLogLog(极省内存,固定12KB)
  地理位置 → GEO(ZSet + GeoHash)

10.5 小结

类型 底层结构 典型命令 内存效率 误差
Bitmap String(SDS) SETBIT/GETBIT/BITCOUNT 1bit/用户 无(精确)
HyperLogLog String + 12KB结构 PFADD/PFCOUNT/PFMERGE 12KB固定(任意数量) 0.81%
GEO ZSet(跳表+字典) GEOADD/GEOSEARCH/GEODIST ~60-100字节/点 坐标精度约11mm

三种类型体现了 Redis "数据结构+算法"的设计哲学:在特定约束下(允许误差、特定数据分布),用更少的内存解决更大规模的问题。HyperLogLog 尤为典型——牺牲不到 1% 的精度,换来内存使用量从 GB 级降至 12KB,这种工程上的取舍是大数据场景的精彩设计。

本章评分
4.5  / 5  (38 评分)

💬 留言讨论