强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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?

特性ListPub/SubStream
持久化
消费确认(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 StreamRabbitMQKafka
吞吐量10w+/s5w+/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 >

🔗 扩展阅读