Skip to content

高级数据能力 ​

除了五种基础类型,Redis 还提供四类常用扩展能力:BitMap、HyperLogLog、Geo 和 Stream。前三种建立在既有类型之上,Stream 才是独立的数据类型。

1. BitMap:位操作 ​

BitMap 不是独立类型,而是 String 上的一组位操作命令——把 String 当作一个可以按位读写的比特数组。

bash
SETBIT sign:20240101 100 1    # 将第 100 位设为 1
GETBIT sign:20240101 100      # 读取第 100 位
BITCOUNT sign:20240101        # 统计为 1 的位数
BITOP AND dest key1 key2      # 位运算(AND/OR/XOR/NOT)

典型场景:

  • 签到统计:每天一个 key,用户 ID 作为偏移量,1 表示已签到
  • 在线状态:一个超大 BitMap 记录所有用户在线与否
  • 布隆过滤器基础:位数组是布隆过滤器的底层载体

优势:一个 bit 记录一个状态,内存效率极高。1 亿用户签到约需 12.5MB 内存(1 亿 bit ÷ 8 = 1250 万字节,纯计算值,不含字符串头部开销)。

2. HyperLogLog:基数统计 ​

HyperLogLog 用于统计「不重复元素的数量」(基数),典型场景是 UV(独立访客)统计。它基于 String,用概率算法在固定内存下估算基数。

bash
PFADD uv:20240101 user1 user2 user3   # 添加元素
PFCOUNT uv:20240101                   # 估算不重复元素数量
PFMERGE dest uv:1 uv:2                # 合并多个 HLL

核心特性:

特性说明
内存占用最坏约 12KB,与元素数量无关
标准误差约 0.81%
适用场景允许误差的大规模去重计数

对比:用 Set 统计 1 亿 UV 需要数 GB 内存,HyperLogLog 最坏只需约 12KB,代价是约 0.81% 的标准误差(Redis 官方文档)。当「精确」不是刚需而「规模」是刚需时,HyperLogLog 是首选。

HyperLogLog 只能估算基数,不能返回具体元素。如果需要「去重后还能取出元素」,请用 Set。

3. Geo:地理位置 ​

Geo 基于 ZSet 实现,将经纬度编码为 Geohash 分值,从而复用 ZSet 的范围查询能力。

bash
GEOADD cities 116.40 39.90 "北京" 121.47 31.23 "上海"
GEODIST cities "北京" "上海" km         # 两点距离
GEOPOS cities "北京"                     # 获取坐标
GEOSEARCH cities FROMMEMBER "北京" BYRADIUS 1000 km   # 附近查询

典型场景:

  • 附近的人:GEOSEARCH 按半径查找
  • 配送范围:判断地址是否在配送半径内
  • 地理位置检索:结合距离排序

底层原理:Redis 用 Geohash 算法把二维坐标(经度、纬度各 26 位)交织成一个 52 位整数,直接作为 ZSet 的 score 存储,精度约 0.6 米。Geo 复用 ZSet 的有序索引,但地理区域查询不等同于普通 ZSet 范围查询,GEOSEARCH 的时间复杂度为 O(N + log M)(N 为边界框内元素数,M 为匹配元素数)。

两点精度相关的限制(Redis 官方文档):坐标纬度限制在 ±85.05112878° 内,极地附近无法索引;距离计算用 Haversine 公式、假设地球为球体,最坏情况误差约 0.5%。

4. Stream:消息流 ​

Stream 是 Redis 5.0 引入的专属数据类型,底层采用 Radix Tree(基数树),提供追加、读取、消费者组、确认和 Pending 列表等消息处理原语。它是否满足业务的可靠投递要求,仍取决于持久化、复制、重试和监控设计。

bash
XADD mystream * field1 value1      # 追加消息(* 表示自动生成 ID)
XRANGE mystream - +                # 读取所有消息
XREAD COUNT 2 STREAMS mystream 0   # 从指定位置读
XGROUP CREATE mystream group1 0    # 创建消费者组
XREADGROUP GROUP group1 c1 STREAMS mystream >   # 消费者组读取

核心特性:

特性说明
消息 ID单调递增,由「时间戳-序号」组成
消费者组支持多消费者分组、消息确认(ACK)、Pending 列表
持久化消息可随 RDB/AOF 持久化
阻塞读取XREAD BLOCK 支持阻塞等待

对比 List 做队列:List 的 BLPOP 实现的是「消费即销毁」,消息被弹出后无法回溯;Stream 的消息读取后仍保留,配合消费者组可支持 ACK 与重试,语义更接近 Kafka 这类消息系统。

Java 实现(Jedis):

java
// 生产者
jedis.xadd("mystream", StreamEntryID.NEW_ENTRY,
    Map.of("name", "张三", "age", "25"));

// 消费者(消费者组)
jedis.xgroupCreate("mystream", "mygroup", new StreamEntryID("0-0"), true);

List<Map.Entry<String, List<StreamEntry>>> streams = jedis.xreadGroup(
    "mygroup", "consumer1",
    XReadGroupParams.xReadGroupParams().count(1).block(5000),
    Map.of("mystream", new StreamEntryID(">"))
);

if (streams != null) {
    for (Map.Entry<String, List<StreamEntry>> stream : streams) {
        for (StreamEntry entry : stream.getValue()) {
            System.out.println("收到:" + entry.getFields());
            jedis.xack("mystream", "mygroup", entry.getID());
        }
    }
}

选型对比:

维度Pub/SubStreamKafka/RocketMQ
消息持久化否是是
消费者组否是是
确认机制否XACKACK
消息回放否支持支持
吞吐量高中高
运维复杂度低(Redis 自带)低(Redis 自带)高(独立部署)
适用场景实时通知轻量级消息大规模消息
场景可考虑
实时通知、缓存清除广播Pub/Sub
需要回放、确认和消费者组的轻量消息流程Stream
需要专业路由、生态和大规模运维能力的消息系统Kafka / RocketMQ

5. 扩展能力速查 ​

类型底层核心命令典型场景关键限制
BitMapStringSETBIT/BITCOUNT签到、在线状态偏移量宜密集,内存取决于最大偏移量
HyperLogLogStringPFADD/PFCOUNTUV 统计0.81% 误差、不可取元素
GeoZSetGEOADD/GEOSEARCH附近的人、配送约 0.6 米(52 位 Geohash)
StreamRadix TreeXADD/XREADGROUP消息队列需消费者组管理

6. 实操演示:四种高级数据能力场景 ​

6.1 场景一:每日签到(BitMap) ​

把用户 ID 当作位偏移量,1 表示当天已签到。

bash
SETBIT sign:20240101 100 1     # (integer) 0  用户 100 签到
SETBIT sign:20240101 200 1     # (integer) 0  用户 200 签到
SETBIT sign:20240101 300 1     # (integer) 0
GETBIT sign:20240101 100       # (integer) 1  用户 100 已签到
BITCOUNT sign:20240101         # (integer) 3  当天共 3 人签到

6.2 场景二:独立访客 UV(HyperLogLog) ​

bash
PFADD uv:20240101 user1 user2 user3 user3   # (integer) 1  重复的 user3 只记一次
PFCOUNT uv:20240101                          # (integer) 3  估算去重后 3 个
PFADD uv:20240102 user3 user4 user5          # (integer) 1
PFMERGE uv:week uv:20240101 uv:20240102     # OK  合并两天的 UV
PFCOUNT uv:week                              # (integer) 5

注意 PFCOUNT 返回的是估算值(有 0.81% 误差),别拿它做精确对账。它只回答「大约多少人」,不回答「具体是谁」。

6.3 场景三:附近的人(Geo) ​

bash
GEOADD cities 116.40 39.90 "北京" 121.47 31.23 "上海" 113.26 23.13 "广州"
# (integer) 3
GEODIST cities "北京" "上海" km        # "1067.3788"  北京到上海距离
GEOSEARCH cities FROMMEMBER "北京" BYRADIUS 1500 km ASC
# 1) "北京" 2) "上海" 3) "广州"   按距离升序

6.4 场景四:使用 Stream 消费者组 ​

Stream 解决 List 的「消费即销毁」问题,支持 ACK 与重试。先创建消费者组,再消费:

bash
# 1. 生产者发消息;示例使用可预测的毫秒时间戳 ID
XADD events 1704000000000-0 type login user "alice"  # "1704000000000-0"
XADD events 1704000000001-0 type login user "bob"    # "1704000000001-0"

# 2. 创建消费者组(从最开始 $ 或 0 消费)
XGROUP CREATE events group1 0             # OK

# 3. 消费者 c1 读取未确认的新消息(> 表示只读新消息)
XREADGROUP GROUP group1 c1 COUNT 2 STREAMS events >
# 返回两条消息及各自 ID

# 4. 处理完确认(ACK)
XACK events group1 1704000000000-0        # (integer) 1

# 5. 查看已读但未确认的消息(Pending 列表,用于重试)
XPENDING events group1

消费流程是「XREADGROUP 读 → 业务处理 → XACK 确认」。若消息处理失败,它会留在 Pending 列表并由运维策略重新读取或转给其他消费者。ACK 只表示 Redis 已记录确认;端到端可靠性还需要持久化、副本、超时认领、幂等消费和失败告警。

Released under the MIT License.