高级数据能力
除了五种基础类型,Redis 还提供四类常用扩展能力:BitMap、HyperLogLog、Geo 和 Stream。前三种建立在既有类型之上,Stream 才是独立的数据类型。
1. BitMap:位操作
BitMap 不是独立类型,而是 String 上的一组位操作命令——把 String 当作一个可以按位读写的比特数组。
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,用概率算法在固定内存下估算基数。
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 的范围查询能力。
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 列表等消息处理原语。它是否满足业务的可靠投递要求,仍取决于持久化、复制、重试和监控设计。
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):
// 生产者
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/Sub | Stream | Kafka/RocketMQ |
|---|---|---|---|
| 消息持久化 | 否 | 是 | 是 |
| 消费者组 | 否 | 是 | 是 |
| 确认机制 | 否 | XACK | ACK |
| 消息回放 | 否 | 支持 | 支持 |
| 吞吐量 | 高 | 中 | 高 |
| 运维复杂度 | 低(Redis 自带) | 低(Redis 自带) | 高(独立部署) |
| 适用场景 | 实时通知 | 轻量级消息 | 大规模消息 |
| 场景 | 可考虑 |
|---|---|
| 实时通知、缓存清除广播 | Pub/Sub |
| 需要回放、确认和消费者组的轻量消息流程 | Stream |
| 需要专业路由、生态和大规模运维能力的消息系统 | Kafka / RocketMQ |
5. 扩展能力速查
| 类型 | 底层 | 核心命令 | 典型场景 | 关键限制 |
|---|---|---|---|---|
| BitMap | String | SETBIT/BITCOUNT | 签到、在线状态 | 偏移量宜密集,内存取决于最大偏移量 |
| HyperLogLog | String | PFADD/PFCOUNT | UV 统计 | 0.81% 误差、不可取元素 |
| Geo | ZSet | GEOADD/GEOSEARCH | 附近的人、配送 | 约 0.6 米(52 位 Geohash) |
| Stream | Radix Tree | XADD/XREADGROUP | 消息队列 | 需消费者组管理 |
6. 实操演示:四种高级数据能力场景
6.1 场景一:每日签到(BitMap)
把用户 ID 当作位偏移量,1 表示当天已签到。
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)
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)
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 与重试。先创建消费者组,再消费:
# 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 已记录确认;端到端可靠性还需要持久化、副本、超时认领、幂等消费和失败告警。