05 - 进阶数据类型
进阶数据类型
除了五大基础类型,Redis 还提供了四种特殊用途的进阶数据类型,在特定场景下能以极低的内存消耗实现强大的功能。
5.1 Bitmap(位图)
Bitmap 本质上是 String 类型的位操作,但可以把它看作一个以位为单位的数组,每个位只能存储 0 或 1。
内存效率
1 亿用户的签到状态:
- 使用 Set: 1亿 × 8字节 ≈ 800 MB
- 使用 Bitmap: 1亿 ÷ 8 = 12.5 MB ← 仅 12.5 MB!
基本操作
# 设置位
SETBIT sign:1001:2026-05 0 1 # 5月1日签到
SETBIT sign:1001:2026-05 1 1 # 5月2日签到
SETBIT sign:1001:2026-05 3 1 # 5月4日签到(跳过3号)
# 获取位
GETBIT sign:1001:2026-05 1 # 1(已签到)
GETBIT sign:1001:2026-05 2 # 0(未签到)
# 统计本月签到天数
BITCOUNT sign:1001:2026-05 # 3
# 统计某一段的签到天数(第 0 天到第 6 天,即第一周)
BITCOUNT sign:1001:2026-05 0 0 # 按字节范围(0-0 字节,即 bit 0-7)
# 查找第一个未签到的天
BITPOS sign:1001:2026-05 0 # 2(第 3 天,从 0 开始)
# 查找第一个已签到的天
BITPOS sign:1001:2026-05 1 # 0
Bitmap 集合运算
# 5月1日有多少用户签到
SETBIT sign:day:01 1001 1
SETBIT sign:day:01 1002 1
SETBIT sign:day:01 1003 1
# 5月2日有多少用户签到
SETBIT sign:day:02 1001 1
SETBIT sign:day:02 1003 1
SETBIT sign:day:02 1004 1
# 连续签到两天的用户(AND 交集)
BITOP AND sign:both sign:day:01 sign:day:02
BITCOUNT sign:both # 2(用户 1001 和 1003)
# 至少签到一天的用户(OR 并集)
BITOP OR sign:either sign:day:01 sign:day:02
BITCOUNT sign:either # 4(1001, 1002, 1003, 1004)
# 1日签到但2日未签到的用户(NOT + AND 差集)
BITOP NOT sign:day:02:inv sign:day:02
BITOP AND sign:only_day1 sign:day:01 sign:day:02:inv
BITCOUNT sign:only_day1 # 1(用户 1002)
Bitmap 应用场景
| 场景 | 说明 |
|---|---|
| 用户签到 | 每天一个 Bitmap,用户 ID 作为 offset |
| 在线状态 | Bitmap 天然记录在线/离线 |
| 特征标记 | 用户标签、权限位标记 |
| 布隆过滤器 | 基于 Bitmap 的概率数据结构 |
1. 连续签到奖励
# 检查用户最近 7 天是否连续签到
SETBIT sign:1001:week 0 1 # 周一
SETBIT sign:1001:week 1 1 # 周二
SETBIT sign:1001:week 2 1 # 周三
SETBIT sign:1001:week 3 1 # 周四
SETBIT sign:1001:week 4 1 # 周五
SETBIT sign:1001:week 5 1 # 周六
SETBIT sign:1001:week 6 1 # 周日
# 如果 BITCOUNT 返回 7,说明连续签到
BITCOUNT sign:1001:week # 7 → 连续签到,发放奖励
2. 日活统计(DAU)
# 每个用户访问时
SETBIT dau:2026-05-10 100001 1
SETBIT dau:2026-05-10 100002 1
SETBIT dau:2026-05-10 100001 1 # 重复访问不影响
# 统计日活
BITCOUNT dau:2026-05-10
# 周活跃用户(OR 合并 7 天)
BITOP OR wau:2026-w19 dau:2026-05-04 dau:2026-05-05 dau:2026-05-06 \
dau:2026-05-07 dau:2026-05-08 dau:2026-05-09 dau:2026-05-10
BITCOUNT wau:2026-w19
5.2 HyperLogLog
HyperLogLog 是 Redis 提供的概率数据结构,用于基数估算(估算集合中不重复元素的数量)。
特点
| 特性 | 说明 |
|---|---|
| 内存固定 | 无论数据量多大,只占用 12 KB |
| 误差率 | 约 0.81% |
| 不可获取元素 | 只能获取基数估算值,不能获取具体元素 |
| 不可删除 | 添加后不能删除 |
基本操作
# 添加元素
PFADD uv:2026-05-10 "user:1001" "user:1002" "user:1003"
PFADD uv:2026-05-10 "user:1001" # 重复添加不影响
# 获取基数估算值
PFCOUNT uv:2026-05-10 # 3
# 合并多个 HyperLogLog
PFADD uv:2026-05-09 "user:1001" "user:1004" "user:1005"
PFMERGE uv:2026-05-09-to-10 uv:2026-05-09 uv:2026-05-10
PFCOUNT uv:2026-05-09-to-10 # 5(两个日期的去重 UV)
# 直接对多个 HLL 计算合并后的基数(不创建新 Key)
PFCOUNT uv:2026-05-09 uv:2026-05-10 # 5
内存对比
统计 1 亿独立用户:
├── Set: 1亿 × (平均50字节) ≈ 5 GB
├── HashSet: 1亿 × 8字节 ≈ 800 MB
└── HyperLogLog: 固定 12 KB ← 12 KB!
应用场景
1. 大规模 UV 统计
# 页面浏览时记录 UV
PFADD page:home:uv:2026-05 "user:100001"
PFADD page:home:uv:2026-05 "user:100002"
# 统计 UV(误差约 0.81%)
PFCOUNT page:home:uv:2026-05
# 月度 UV
PFMERGE page:home:uv:2026-05 \
page:home:uv:2026-05-01 page:home:uv:2026-05-02 ...
2. 搜索去重
# 统计独立搜索词数
PFADD search:terms:2026-05 "redis tutorial"
PFADD search:terms:2026-05 "python guide"
PFADD search:terms:2026-05 "redis tutorial" # 重复
PFCOUNT search:terms:2026-05 # 2
⚠️ 注意:HyperLogLog 只适合统计基数,不适合需要知道具体有哪些元素的场景。
5.3 GeoSpatial(地理位置)
GeoSpatial 是 Redis 3.2 引入的地理位置数据类型,底层使用 Sorted Set 实现(将经纬度编码为 GeoHash 作为 score)。
基本操作
# 添加位置(经度,纬度,名称)
GEOADD locations 116.397128 39.916527 "天安门"
GEOADD locations 116.403963 39.915119 "故宫博物院"
GEOADD locations 116.310003 39.992838 "颐和园"
GEOADD locations 116.397560 39.908740 "前门大街"
GEOADD locations 116.410425 39.905993 "王府井大街"
# 计算两点之间的距离
GEODIST locations "天安门" "故宫博物院" km # "0.6975"
GEODIST locations "天安门" "颐和园" km # "14.8436"
GEODIST locations "天安门" "故宫博物院" m # "697.5000"
# 以某点为中心搜索附近的位置
GEOSEARCH locations FROMLONLAT 116.397128 39.916527 \
BYRADIUS 2 km ASC WITHDIST WITHCOORD
# 1) 1) "天安门"
# 2) "0.0000"
# 3) 1) "116.39712899923324585"
# 2) "39.91652690382922145"
# 2) 1) "故宫博物院"
# 2) "0.6975"
# 3) 1) "116.40396326780319214"
# 2) "39.91511900330216474"
# 3) 1) "前门大街"
# 2) "0.9539"
# ...
# 获取元素的 GeoHash
GEOHASH locations "天安门" # "wx4g0f64yj0"
# 获取元素的经纬度
GEOPOS locations "天安门"
# 1) 1) "116.39712899923324585"
# 2) "39.91652690382922145"
# 以某点为中心,按矩形范围搜索
GEOSEARCH locations FROMLONLAT 116.397128 39.916527 \
BYBOX 3 3 km ASC WITHDIST
核心命令对比
| 命令 | 说明 | 版本要求 |
|---|---|---|
GEOADD | 添加地理位置 | 3.2+ |
GEODIST | 计算两点距离 | 3.2+ |
GEOHASH | 获取 GeoHash 编码 | 3.2+ |
GEOPOS | 获取经纬度 | 3.2+ |
GEORADIUS | 以坐标为中心半径搜索 | 3.2+(6.2+ 已弃用) |
GEOSEARCH | 搜索(支持圆形/矩形) | 6.2+ |
GEOSEARCHSTORE | 搜索并存储结果 | 6.2+ |
⚠️ 注意:Redis 6.2+ 推荐使用 GEOSEARCH 替代 GEORADIUS,前者功能更强且性能更好。
应用场景
1. 附近的人/商家
# 添加商家位置
GEOADD shops 116.397 39.916 "星巴克-国贸店"
GEOADD shops 116.405 39.920 "星巴克-东单店"
GEOADD shops 116.388 39.912 "瑞幸-CBD店"
# 用户在天安门,搜索 1km 内的咖啡店
GEOSEARCH shops FROMLONLAT 116.397128 39.916527 \
BYRADIUS 1 km ASC WITHDIST
2. 骑手配送范围
# 检查骑手是否在配送范围内(3km)
GEOADD riders 116.400 39.920 "rider:1001"
# 搜索 3km 内的骑手
GEOSEARCH riders FROMLONLAT 116.397 39.916 \
BYRADIUS 3 km ASC COUNT 5 WITHDIST
3. 打卡签到
# 员工打卡(公司坐标 116.400, 39.920)
GEOADD attendance:2026-05-10 116.4001 39.9202 "employee:1001"
# 检查是否在 100m 范围内
GEODIST attendance:2026-05-10 "employee:1001" "office" m
# 如果返回值 <= 100 则打卡成功
5.4 Stream
Stream 是 Redis 5.0 引入的数据类型,专为消息队列设计。相比 List 和 Pub/Sub,Stream 提供了更完善的消息传递保证。
为什么需要 Stream?
| 特性 | List | Pub/Sub | Stream |
|---|---|---|---|
| 持久化 | ✅ | ❌ | ✅ |
| 消费确认(ACK) | ❌ | ❌ | ✅ |
| 消费者组 | ❌ | ❌ | ✅ |
| 历史消息回溯 | 有限 | ❌ | ✅ |
| 消息 ID | ❌ | ❌ | ✅ 时间戳 ID |
| 多消费者 | ❌ | ✅ | ✅ |
基本操作
# 添加消息(* 表示自动生成 ID)
XADD mystream * name "Alice" action "login"
# "1715318400000-0" ← 返回的 ID
XADD mystream * name "Bob" action "purchase" item "laptop"
# "1715318401000-0"
XADD mystream * name "Charlie" action "logout"
# "1715318402000-0"
# 指定 ID(需大于当前最大 ID)
XADD mystream 1715318410000-0 name "Dave" action "login"
# 读取消息
XLEN mystream # 消息数量
XRANGE mystream - + # 获取所有消息(从头到尾)
XRANGE mystream - + COUNT 2 # 获取前 2 条
XRANGE mystream 1715318400000-0 + # 从指定 ID 开始
# 逆序读取
XREVRANGE mystream + - # 从尾到头
# 读取新消息(阻塞等待,从 ID 之后开始)
XREAD COUNT 10 BLOCK 5000 STREAMS mystream 0 # 从头读
XREAD COUNT 10 BLOCK 5000 STREAMS mystream $ # 从最新消息开始
# 删除消息
XDEL mystream 1715318400000-0
# 修剪(保留最近 N 条)
XTRIM mystream MAXLEN 1000
# 查看 Stream 信息
XINFO STREAM mystream
# 消息详细信息
XINFO STREAM mystream FULL
消费者组(Consumer Group)
消费者组是 Stream 的核心特性,允许多个消费者协作消费同一个 Stream,每条消息只会被组内的一个消费者处理。
# 创建消费者组
XGROUP CREATE mystream mygroup $ MKSTREAM
# $ 表示从最新消息开始消费
# MKSTREAM:如果 Stream 不存在则自动创建
# 消费者 A 读取消息
XREADGROUP GROUP mygroup consumer-a COUNT 1 BLOCK 2000 STREAMS mystream >
# > 表示读取未被消费的新消息
# 消费者 B 读取消息
XREADGROUP GROUP mygroup consumer-b COUNT 1 BLOCK 2000 STREAMS mystream >
# 确认消息已被处理(ACK)
XACK mystream mygroup 1715318400000-0
# 查看待确认消息(PEL - Pending Entries List)
XPENDING mystream mygroup
# 查看待确认消息详情
XPENDING mystream mygroup - + 10
# 转移超时未确认的消息给其他消费者
XCLAIM mystream mygroup consumer-b 60000 1715318401000-0
# 60000ms = 60秒,消息空闲超过 60 秒则转移
# 查看消费者组信息
XINFO GROUPS mystream
# 查看组内消费者信息
XINFO CONSUMERS mystream mygroup
Stream 消息生命周期
┌───────────┐
│ Producer │
└─────┬─────┘
│ XADD
▼
┌─────────────────────┐
│ Stream │
│ msg:1 → msg:2 → ... │
└────────┬────────────┘
│ XREADGROUP
┌────────┼────────┐
│ │ │
┌─────▼───┐ ┌──▼────┐ ┌▼──────┐
│Consumer A│ │Cons B │ │Cons C │
└─────┬────┘ └──┬────┘ └┬──────┘
│ XACK │ │
└─────────┴───────┘
│
(消息标记为已确认)
完整消费者示例(Python)
import redis
import json
import signal
import sys
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
STREAM_KEY = 'orders'
GROUP_NAME = 'order-processors'
CONSUMER_NAME = 'consumer-1'
# 创建消费者组(忽略已存在的错误)
try:
r.xgroup_create(STREAM_KEY, GROUP_NAME, id='0', mkstream=True)
except redis.exceptions.ResponseError:
pass
def process_message(message_id, data):
"""处理单条消息"""
print(f"Processing message {message_id}: {data}")
# 模拟业务处理
order_id = data.get('order_id')
action = data.get('action')
print(f" Order {order_id}: {action}")
return True
def consume():
"""消费循环"""
print(f"Consumer {CONSUMER_NAME} started...")
while True:
try:
# 读取新消息(阻塞 2 秒)
messages = r.xreadgroup(
GROUP_NAME, CONSUMER_NAME,
{STREAM_KEY: '>'},
count=10,
block=2000
)
if not messages:
continue
for stream, entries in messages:
for message_id, data in entries:
if process_message(message_id, data):
# 处理成功,确认消息
r.xack(STREAM_KEY, GROUP_NAME, message_id)
print(f" Acknowledged: {message_id}")
except KeyboardInterrupt:
print("Shutting down...")
break
except Exception as e:
print(f"Error: {e}")
if __name__ == '__main__':
consume()
生产消息:
import redis
import json
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# 生产订单消息
for i in range(10):
msg_id = r.xadd('orders', {
'order_id': f'ORD-{1000 + i}',
'action': 'create',
'amount': str(99.9 + i),
})
print(f"Produced: {msg_id}")
Stream vs 消息队列中间件
| 特性 | Redis Stream | RabbitMQ | Kafka |
|---|---|---|---|
| 吞吐量 | 10w+/s | 5w+/s | 百万+/s |
| 持久化 | RDB/AOF | 磁盘 | 磁盘 |
| 消息确认 | ✅ | ✅ | ✅(offset) |
| 消息回溯 | ✅ | ❌ | ✅ |
| 运维复杂度 | 低 | 中 | 高 |
| 适用场景 | 轻量级、低延迟 | 企业级、复杂路由 | 大数据、日志 |
💡 技巧:Redis Stream 适合中小规模、对延迟敏感的场景。如果日消息量超过千万,建议使用 Kafka。
📌 业务场景
场景一:用户活跃度分析(Bitmap + HyperLogLog)
# 每日用户登录打卡
SETBIT login:2026-05 1001 1
SETBIT login:2026-05 1002 1
# 统计日活
BITCOUNT login:2026-05
# 精确统计月活(小规模)
BITOP OR login:2026-05-all login:2026-05-01 login:2026-05-02 ...
BITCOUNT login:2026-05-all
# 估算月活(大规模,12KB 固定内存)
PFMERGE mau:2026-05 mau:2026-05-01 mau:2026-05-02 ...
PFCOUNT mau:2026-05
场景二:外卖平台商家搜索(GeoSpatial)
# 用户在 116.40, 39.92,搜索 3km 内的外卖商家
GEOSEARCH merchants FROMLONLAT 116.40 39.92 \
BYRADIUS 3 km ASC COUNT 20 WITHDIST
场景三:订单异步处理(Stream + 消费者组)
# 订单创建后推入 Stream
XADD orders * order_id "ORD-1001" action "pay" amount "299"
# 多个消费者组并行处理
# 组 1:库存服务
XREADGROUP GROUP inventory-svc svc-1 COUNT 5 STREAMS orders >
# 组 2:通知服务
XREADGROUP GROUP notify-svc svc-1 COUNT 5 STREAMS orders >