Redis 数据类型完整参考
数据类型选型速查表
| 需求场景 | 推荐类型 | 备选方案 | 选型理由 |
|---|---|---|---|
| 简单键值缓存 | String | — | 最通用,单 key 单 value |
| 缓存整个对象(JSON) | String(序列化) | Hash | 整体读写用 String,部分字段更新用 Hash |
| 用户信息/对象属性 | Hash | String(JSON) | 按字段读写,节省网络开销 |
| 消息队列 | List(BRPOP/LPUSH) | Stream(5.0+) | 简单场景用 List,需消费组/确认用 Stream |
| 最新动态/时间线 | List(LPUSH + LTRIM) | ZSet | 简单时间序用 List,需按权重/时间范围查用 ZSet |
| 排行榜/积分排名 | ZSet | — | 唯一支持按 score 排序的类型 |
| 去重集合/标签系统 | Set | — | 无序去重,支持交并差集 |
| 点赞/共同好友 | Set | — | 交集/并集/差集运算 |
| 附近的人/LBS | GEO | — | 底层 ZSet,支持半径/距离查询 |
| 用户签到/活跃统计 | Bitmap | String | 极省内存,位运算批量统计 |
| UV 统计/大数据去重估算 | HyperLogLog | — | 12KB 统计上亿数据,误差 0.81% |
| 发布/订阅 | Pub/Sub(不在本文档范围) | Stream | 广播用 Pub/Sub,持久化/回溯用 Stream |
| 分布式锁 | String(SET NX PX) | — | SETNX + 过期时间原子操作 |
| 计数器/限流 | String(INCR) | — | 原子自增,天然线程安全 |
| 延迟队列 | ZSet(score=时间戳) | Stream | 简单场景用 ZSet,复杂场景用 Stream |
各类型时间复杂度速查表
| 命令类别 | String | Hash | List | Set | ZSet | GEO | Bitmap | HyperLogLog |
|---|---|---|---|---|---|---|---|---|
| 单元素读 | O(1) | O(1) | O(1) (LINDEX: O(N)) | O(1) | O(1) | O(log N) | O(1) | — |
| 单元素写 | O(1) | O(1) | O(1) | O(1) | O(log N) | O(log N) | O(1) | — |
| 全量获取 | O(N) | O(N) | O(S+N) LRANGE | O(N) SMEMBERS | O(log N + M) ZRANGE | O(N) | O(N) | — |
| 批量操作 | O(N) MGET/MSET | O(N) HMGET/HMSET | — | — | — | — | — | — |
| 集合运算 | — | — | — | O(N*M) 最差 | O(NK)+O(Mlog M) | — | O(N) | — |
| 添加元素 | — | — | O(N) 指定位置 | — | O(log N) 每次 | — | — | O(1) 每个 |
| 统计数量 | — | — | O(1) | O(1) | O(1) 区间 O(log N) | — | — | O(1) |
| N = 元素数量, M = 返回元素数量, K = 参与运算的集合数量 |
各类型内部编码总结表
| 数据类型 | 内部编码 | 触发条件 | 优势 | 劣势 |
|---|---|---|---|---|
| String | int |
值是整数,能用 long 表示 | O(1) INCR/DECR,内存极省 | 仅限整数 |
embstr |
长度 <= 44 字节 | 一次内存分配,缓存友好 | 只读,修改即转 raw | |
raw |
长度 > 44 字节 | 支持任意长度 | 两次内存分配 | |
| Hash | listpack (7.0+) |
元素 < 512 个,且每个 value < 64 字节 | 连续内存,省内存 | 查询 O(N) |
hashtable |
超过阈值 | 查询 O(1) | 内存开销大 | |
| List | quicklist (3.2+) |
所有情况 | linkedlist + listpack 混合 | — |
| Set | intset |
全整数且元素 < 512 个 | 连续内存,省内存 | 查询 O(log N) |
hashtable |
超过阈值或有非整数 | 查询 O(1) | 内存开销大 | |
| ZSet | listpack (7.0+) |
元素 < 128 个,且每个 member < 64 字节 | 连续内存,省内存 | 查询 O(N) |
skiplist + dict |
超过阈值 | 查询 O(log N),有序性 | 内存开销大 | |
| GEO | zset(skiplist+dict) |
所有情况 | 继承 ZSet 操作 | 精度有限 |
| Bitmap | raw(String) |
所有情况 | 最大 2^32 bits | 底层是 String |
| HyperLogLog | 稀疏/稠密 |
自动切换 | 固定 12KB | 误差 0.81% |
一、String(字符串)
1.1 概述
String 是 Redis 最基础的类型,一个 key 对应一个 value。value 最大 512MB。虽然是"字符串",但底层的 SDS(Simple Dynamic String)使其能存储文本、整数、浮点数、二进制数据(图片、序列化对象等)。
1.2 内部编码
| 编码 | 条件 | 结构 | 说明 |
|---|---|---|---|
int |
value 是整数,且在 long 范围内(64 位有符号) |
直接存整数 | 内存极省,INCR/DECR 是 O(1) |
embstr |
value 长度 <= 44 字节(Redis 5.0+;3.2 以前是 39) | 一次内存分配,SDS 与 RedisObject 连续 | 只读优化;任何修改都会转为 raw |
raw |
value 长度 > 44 字节 | 两次内存分配,SDS 与 RedisObject 分离 | 支持 append 等动态扩展 |
编码验证命令:
OBJECT ENCODING key🔬 深入原理:SDS(Simple Dynamic String)
Redis 不直接使用 C 语言的 char*,而是封装了 SDS:
- O(1) 获取长度:len 字段已存,无需
strlen - 杜绝缓冲区溢出:自动检查并扩容
- 减少内存重分配:预分配(len < 1MB 时翻倍;>=1MB 时每次加 1MB)+ 惰性释放
- 二进制安全:不依赖
\0结尾,可存任意二进制数据 - 详细原理见
10-底层数据结构.md
1.3 基本读写命令
SET — 设置键值
语法:
SET key value [NX | XX] [GET] [EX seconds | PX milliseconds |
EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]参数说明:
| 参数 | 含义 |
|---|---|
NX |
仅当 key 不存在时设置(Not eXists) |
XX |
仅当 key 已存在时设置(eXists) |
GET |
返回旧值(6.2+),SET 仍会执行 |
EX seconds |
过期时间(秒) |
PX milliseconds |
过期时间(毫秒) |
EXAT timestamp |
Unix 时间戳过期(秒,6.2+) |
PXAT timestamp |
Unix 时间戳过期(毫秒,6.2+) |
KEEPTTL |
保留原有 TTL(6.0+) |
示例:
# 基本设置
SET username "alice"
# → OK
# 带过期时间(10秒后过期)
SET session:token "abc123" EX 10
# → OK
# 仅当不存在时设置(分布式锁场景)
SET lock:order:1001 "locked" NX EX 30
# → OK(成功获取锁)
SET lock:order:1001 "locked" NX EX 30
# → (nil)(锁已被持有)
# 获取旧值并设置新值(6.2+)
SET counter 100 GET
# → (nil)(旧值不存在)
SET counter 200 GET
# → "100"(返回旧值)GET — 获取值
GET keyGET username
# → "alice"
GET nonexistent
# → (nil)GETSET — 设置新值并返回旧值(已废弃,推荐 SET … GET)
GETSET key valueSET visits 10
GETSET visits 20
# → "10"(旧值)
GET visits
# → "20"💡 最佳实践: Redis 6.2+ 推荐使用 SET key value GET 代替 GETSET。
GETDEL — 获取值并删除键(6.2+)
GETDEL keySET temp "data"
GETDEL temp
# → "data"
GET temp
# → (nil)GETEX — 获取值并设置过期时间(6.2+)
GETEX key [EX seconds | PX milliseconds |
EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST]GETEX session:token EX 3600
# → "abc123"(同时续期 1 小时)GETRANGE — 获取子串
GETRANGE key start end- 索引从 0 开始,
-1表示最后一个字符 - start/end 可越界,自动截断
SET message "Hello World"
GETRANGE message 0 4
# → "Hello"
GETRANGE message -5 -1
# → "World"SETRANGE — 覆盖子串
SETRANGE key offset value- 如果 offset 超出原长度,中间用
\x00填充 - 返回修改后的字符串长度
SET message "Hello World"
SETRANGE message 6 "Redis"
# → (integer) 11
GET message
# → "Hello Redis"STRLEN — 获取字符串长度
STRLEN keySET name "Alice"
STRLEN name
# → (integer) 5APPEND — 追加内容
APPEND key value- 返回追加后的总长度
- 如果 key 不存在,等同于 SET
SET log "Error: "
APPEND log "connection timeout"
# → (integer) 25
GET log
# → "Error: connection timeout"1.4 批量操作命令
MSET — 批量设置
MSET key1 value1 [key2 value2 ...]- 原子操作,全部成功或全部不做(但非事务性回滚)
- 返回
OK
MSET name "Alice" age "30" city "Beijing"
# → OKMGET — 批量获取
MGET key1 [key2 key3 ...]- 返回数组,不存在的 key 返回
nil - 比 N 次 GET 少 N-1 次网络往返
MGET name age city nonexistent
# → 1) "Alice"
# → 2) "30"
# → 3) "Beijing"
# → 4) (nil)⚡ 性能提示: 批量获取时永远优先使用 MGET 而不是多次 GET,大幅减少 RTT(往返时间)。
MSETNX — 批量原子设置
MSETNX key1 value1 [key2 value2 ...]- 原子性:仅当所有 key 都不存在时才设置
- 返回 1(成功)或 0(失败,全部不设置)
MSETNX user:1:name "Alice" user:1:email "a@b.com"
# → (integer) 1 (全部创建成功)
MSETNX user:1:name "Bob" user:2:name "Charlie"
# → (integer) 0 (user:1:name 已存在,全部不设置)1.5 计数器命令
INCR / DECR — 自增/自减 1
INCR key
DECR key- 如果 key 不存在,默认从 0 开始
- 值必须是整数(或能解析为整数),否则报错
- 返回操作后的值
SET page:views 100
INCR page:views
# → (integer) 101
DECR page:views
# → (integer) 100
# key 不存在
INCR new:counter
# → (integer) 1INCRBY / DECRBY — 自增/自减指定值
INCRBY key increment
DECRBY key decrementINCRBY score 10
# → (integer) 10
INCRBY score -5
# → (integer) 5
DECRBY score 3
# → (integer) 2INCRBYFLOAT — 浮点数自增
INCRBYFLOAT key increment- 支持科学计数法
- 不保证精确精度(IEEE 754 浮点数)
SET price "19.99"
INCRBYFLOAT price 0.01
# → "20.00"
SET scientific "1.0e3"
INCRBYFLOAT scientific 1.0
# → "1001"🚨 陷阱: INCRBYFLOAT 返回的是字符串,不是整数。多次操作后有浮点精度累积误差,金融场景请用整数(分)代替小数。
1.6 SETNX 与 SETEX
SETNX key value # 仅当不存在时设置(SET key value NX 等效)
SETEX key seconds value # 设置值并指定过期秒数(SET key value EX seconds 等效)💡 最佳实践: Redis 2.6.12+ 推荐直接用 SET 的 NX/EX 选项组合,而非分开使用 SETNX + EXPIRE(两个命令非原子,可能因宕机导致死锁)。
1.7 LCS — 最长公共子序列(7.0+)
LCS key1 key2 [LEN] [IDX] [MINMATCHLEN minlen] [WITHMATCHLEN]参数说明:
| 参数 | 含义 |
|---|---|
LEN |
仅返回 LCS 长度 |
IDX |
返回每个匹配在原始字符串中的位置(索引) |
MINMATCHLEN minlen |
仅报告长度 >= minlen 的匹配 |
WITHMATCHLEN |
返回每个匹配的长度 |
SET str1 "ABCDEFG"
SET str2 "ACDEGH"
LCS str1 str2
# → "ACDEG"
LCS str1 str2 LEN
# → (integer) 51.8 实战场景
| 场景 | 命令组合 | 说明 |
|---|---|---|
| 缓存 | SET key value EX ttl |
最常用:缓存数据库查询结果、页面片段 |
| 计数器 | INCR / INCRBY |
文章阅读量、点赞数、API 调用次数 |
| 分布式锁 | SET lock_key rand_val NX EX 30 |
原子获取锁+设置超时,释放时用 Lua 验证 value 防误删 |
| Session 共享 | SET session:id data EX 1800 |
多服务器共享用户会话 |
| 限流 | INCR rate:user:minute + EXPIRE |
固定窗口计数器,配合 TTL 自动重置 |
| 序列号生成 | INCR order:id |
分布式唯一 ID 生成 |
| 配置存储 | SET config:xxx value |
动态配置,无需重启 |
| 临时数据 | SETEX key 300 value |
验证码、临时 Token |
1.9 常见陷阱
🚨 bigkey 问题: 单个 String 过大(>10KB)会导致网络传输慢、主从同步阻塞。单个 value 最大 512MB,但强烈建议控制在 10KB 以内。
🚨 SETNX 非原子问题: SETNX key value 然后 EXPIRE key seconds 是非原子的,宕机可能导致永不过期。请用 SET key value NX EX seconds。
🚨 MSET 不是事务: MSET 全部命令打包发送,但中途如果 Redis 宕机,可能部分已写入。它不保证回滚。
🚨 INCRBYFLOAT 精度: 浮点数运算有精度损失。需要精确小数时,用整数表示最小单位(如金额用分)。
🚨 GETSET 已弃用: 新代码请使用 SET key value GET。
二、Hash(哈希)
2.1 概述
Hash 是键值对的映射表(field-value map),适合存储对象(如用户信息、商品属性)。相比将对象序列化为 JSON String,Hash 可以按 field 单独读写,节省网络开销。
2.2 内部编码
| 编码 | 条件 | 结构 | 说明 |
|---|---|---|---|
listpack(7.0+) / ziplist(6.2 及以前) |
field-value 对数量 < hash-max-listpack-entries(默认 512),且每个 value 长度 < hash-max-listpack-value(默认 64 字节) |
连续内存紧凑数组 | k1,v1,k2,v2 交替存放,内存极省 |
hashtable |
超过任一阈值 | 标准哈希表(dict) | O(1) 查询,但内存开销大 |
配置阈值(redis.conf):
hash-max-listpack-entries 512
hash-max-listpack-value 64🔬 深入原理:编码转换
当 listpack/ziplist 超阈值时,Redis 自动转为 hashtable。但反过来不会自动降级——即使删到只剩 1 个 field,仍是 hashtable。如需降级,须用 HSET 重新写入或重启加载 RDB。
2.3 基本命令
HSET — 设置 field 的值
HSET key field value [field value ...]- Redis 4.0+ 支持多 field-value 对,等同于废弃的 HMSET
HSET user:1001 name "Alice" age "30" email "alice@example.com"
# → (integer) 3HGET — 获取单个 field
HGET key fieldHGET user:1001 name
# → "Alice"
HGET user:1001 nonexistent
# → (nil)HMGET — 获取多个 field
HMGET key field1 [field2 field3 ...]HMGET user:1001 name age nonexistent
# → 1) "Alice"
# → 2) "30"
# → 3) (nil)HGETALL — 获取所有 field-value
HGETALL key- 返回 field, value, field, value 交替的数组
- O(N),N 为 field 数量
HGETALL user:1001
# → 1) "name"
# → 2) "Alice"
# → 3) "age"
# → 4) "30"
# → 5) "email"
# → 6) "alice@example.com"🚨 陷阱: HGETALL 是 O(N) 操作,大 Hash(数万 field)会阻塞 Redis。大 Hash 请使用 HSCAN 分批迭代。
HKEYS / HVALS — 获取所有 field 名/所有 value
HKEYS key
HVALS keyHKEYS user:1001
# → 1) "name"
# → 2) "age"
# → 3) "email"
HVALS user:1001
# → 1) "Alice"
# → 2) "30"
# → 3) "alice@example.com"HDEL — 删除 field
HDEL key field1 [field2 field3 ...]- 返回成功删除的 field 数量(不包括不存在的 field)
HDEL user:1001 email tmp_field
# → (integer) 1(仅 email 存在并被删除)HEXISTS — 判断 field 是否存在
HEXISTS key fieldHEXISTS user:1001 name
# → (integer) 1
HEXISTS user:1001 phone
# → (integer) 0HLEN — 获取 field 数量
HLEN keyHLEN user:1001
# → (integer) 3HSTRLEN — 获取 field 值长度
HSTRLEN key fieldHSTRLEN user:1001 name
# → (integer) 5HSETNX — 仅当 field 不存在时设置
HSETNX key field valueHSETNX user:1001 phone "123456"
# → (integer) 1(设置成功)
HSETNX user:1001 phone "654321"
# → (integer) 0(已存在,不设置)HINCRBY / HINCRBYFLOAT — 数值自增
HINCRBY key field increment
HINCRBYFLOAT key field incrementHSET product:1001 stock 50
HINCRBY product:1001 stock -3
# → (integer) 47
HSET product:1001 price "19.99"
HINCRBYFLOAT product:1001 price 1.50
# → "21.49"HRANDFIELD — 随机获取 field(6.2+)
HRANDFIELD key [count [WITHVALUES]]- count 为正:返回 count 个不重复的 field
- count 为负:返回 |count| 个可能重复的 field
- WITHVALUES:同时返回 field 和 value
HRANDFIELD user:1001 1
# → 1) "age"
HRANDFIELD user:1001 -3 WITHVALUES
# → 1) "name"
# → 2) "Alice"
# → 3) "email"
# → 4) "alice@example.com"
# → 5) "age"
# → 6) "30"HSCAN — 增量迭代
HSCAN key cursor [MATCH pattern] [COUNT count]HSCAN user:1001 0 MATCH "n*" COUNT 10
# → 1) "0" (下一次迭代的游标,0 表示结束)
# → 2) 1) "name"
# → 2) "Alice"💡 最佳实践: 生产环境遍历大 Hash 请务必使用 HSCAN,避免 HGETALL/HKEYS/HVALS 阻塞。
2.4 实战场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 用户信息 | 一个用户一个 Hash,field 为属性名 | HSET user:1001 name "Alice" age 30 |
| 购物车 | 用户 ID 为 key,商品 ID 为 field,数量为 value | HSET cart:user1 sku:1001 2 |
| 商品属性 | 一个商品一个 Hash | HSET item:5001 title "Book" price "29.99" |
| 计数器组 | 多维度统计用 Hash 的 HINCRBY | HINCRBY stats:daily views 1 |
| 配置项 | 一个模块一个 Hash | HSET config:cache ttl 300 max_size 1000 |
| 去重判断 | 用 field 做快速存在性检查 | HEXISTS blacklist:ip 10.0.0.1 |
2.5 常见陷阱
🚨 大 Hash 全量获取阻塞: HGETALL/HKEYS/HVALS 在大 Hash 上会阻塞主线程。使用 HSCAN 分批获取,或拆分为多个小 key。
🚨 编码不自动降级: listpack 转 hashtable 后,即使删减数据也不会回退,内存开销长期偏高。
🚨 field 同名问题: Hash 的 field 没有 schema 约束,注意同一 key 内 field 命名一致性,避免 name 和 Name 并存。
🚨 过期只针对 key: EXPIRE user:1001 3600 会让整个 Hash 过期,不能给单个 field 设 TTL。如需字段级过期,用 String 独立存储。
三、List(列表)
3.1 概述
List 是有序的字符串链表,按插入顺序排序。可从头部(left)或尾部(right)操作。底层用 quicklist 实现,结合了链表和紧凑数组的优点。
3.2 内部编码
Redis 3.2+ 统一使用 quicklist:
| 版本 | 编码 | 说明 |
|---|---|---|
| < 3.2 | linkedlist |
双向链表,每个节点存一个元素,内存开销大 |
| < 3.2 | ziplist |
小 List 用压缩列表存所有元素(阈值同 Hash) |
| >= 3.2 | quicklist |
linkedlist + ziplist/listpack 混合:链表节点存一片连续数据 |
🔬 深入原理:quicklist
quicklist 是一个 linkedlist,每个节点是一个 ziplist/listpack(多个元素压缩在一起):
- 减少指针开销:每个链表节点存多个元素,减少 next/prev 指针比例
- 减少内存碎片:连续内存块
- 可配置
list-max-listpack-size控制每个 quicklist 节点的最大大小 list-compress-depth控制中间节点是否压缩(LZF),省内存但多 CPU
3.3 基本命令
LPUSH / RPUSH — 从头部/尾部插入
LPUSH key element1 [element2 element3 ...]
RPUSH key element1 [element2 element3 ...]- 返回操作后 List 的长度
- 支持一次插入多个元素(2.4+,按从左到右顺序插入头部/尾部)
LPUSH tasks "task1" "task2"
# → (integer) 2
LRANGE tasks 0 -1
# → 1) "task2" (最后 push 的在最前面)
# → 2) "task1"
RPUSH tasks "task3" "task4"
# → (integer) 4
LRANGE tasks 0 -1
# → 1) "task2"
# → 2) "task1"
# → 3) "task3"
# → 4) "task4"LPOP / RPOP — 从头部/尾部弹出
LPOP key [count]
RPOP key [count]- Redis 6.2+ 支持 count 参数,一次弹出多个元素
- count 默认 1
- List 为空时返回
(nil)
LPOP tasks
# → "task2"
LPOP tasks 3
# → 1) "task1"
# → 2) "task3"
# → 3) "task4"LRANGE — 获取范围内的元素
LRANGE key start stop- 索引从 0 开始,
-1表示最后一个 - O(S+N):S 为 start 到头部/尾部的距离(取小值),N 为返回元素数
RPUSH colors red green blue yellow
LRANGE colors 0 2
# → 1) "red"
# → 2) "green"
# → 3) "blue"
LRANGE colors -2 -1
# → 1) "blue"
# → 2) "yellow"🚨 陷阱: LRANGE 在大 List 上性能较差,尤其是 LRANGE key 0 -1 获取全部元素。用 LTRIM 限制 List 长度,或用 Stream 替代大消息队列。
LTRIM — 修剪列表
LTRIM key start stop- 只保留 [start, stop] 范围内的元素,其余删除
RPUSH queue a b c d e
LTRIM queue 0 2
LRANGE queue 0 -1
# → 1) "a"
# → 2) "b"
# → 3) "c"💡 最佳实践: LPUSH list item + LTRIM list 0 999 组合维持 List 最多 1000 个元素,实现固定大小的最新列表。
LLEN — 获取长度
LLEN keyLLEN queue
# → (integer) 3LINDEX — 按索引获取元素
LINDEX key index- O(N),N 为到头部或尾部较近的距离
- 负数索引从尾部算
LINDEX colors 1
# → "green"
LINDEX colors -1
# → "yellow"LINSERT — 在指定元素前/后插入
LINSERT key BEFORE|AFTER pivot element- O(N),需要扫描找到 pivot
- 如果 pivot 不存在,返回 -1
RPUSH letters a b d
LINSERT letters BEFORE d c
# → (integer) 4
LRANGE letters 0 -1
# → 1) "a" 2) "b" 3) "c" 4) "d"LSET — 按索引替换元素
LSET key index element- 索引越界报错
LSET letters 2 "X"
# → OKLREM — 删除指定元素
LREM key count element- count > 0:从左到右删除最多 count 个匹配元素
- count < 0:从右到左删除最多 |count| 个匹配元素
- count = 0:删除所有匹配元素
- 返回实际删除的数量
RPUSH dup a b a a c a
LREM dup 2 a
# → (integer) 2 (从左删除前两个 a)
LREM dup -1 a
# → (integer) 1 (从右删除一个 a)LPOS — 查找元素位置(6.0.6+)
LPOS key element [RANK rank] [COUNT num-matches] [MAXLEN len]- RANK:指定第 N 个匹配项(1 为第一个,-1 为最后一个)
- COUNT:返回最多 num-matches 个位置
- MAXLEN:最多扫描 len 个元素
RPUSH lst a b c d a e a
LPOS lst a
# → (integer) 0
LPOS lst a RANK 2
# → (integer) 4
LPOS lst a COUNT 3
# → 1) (integer) 0
# → 2) (integer) 4
# → 3) (integer) 63.4 阻塞命令
BLPOP / BRPOP — 阻塞式弹出
BLPOP key1 [key2 key3 ...] timeout
BRPOP key1 [key2 key3 ...] timeout- 如果所有 key 都为空,阻塞等待直到有元素可用或超时
- timeout 为 0 表示永久阻塞
- 返回
key + element数组,超时返回(nil) - 支持多个 key:按顺序检查,第一个有数据的 key 被弹出
# 终端 1:阻塞等待
BLPOP queue 0
# (阻塞中...)
# 终端 2:插入数据
LPUSH queue "new_task"
# 终端 1 返回:
# → 1) "queue"
# → 2) "new_task"
# → (27.5s) (阻塞时长)💡 最佳实践: BLPOP/BRPOP 是实现消息队列(消费者-生产者)的经典模式。多个消费者阻塞在同一个 key 上时,Redis 会依次分发,实现负载均衡。
BLPOP/BRPOP 多 Key 处理顺序
BLPOP key1 key2 0先检查 key1,如果为空再检查 key2。只有 key1 为空时才弹出 key2。如果两个都有数据,只弹出 key1。
3.5 移动命令(6.2+)
LMOVE / BLMOVE — 原子移动元素
LMOVE source destination LEFT|RIGHT LEFT|RIGHT
BLMOVE source destination LEFT|RIGHT LEFT|RIGHT timeout- 从 source 弹出一个元素,push 到 destination
- 6.2+ 引入,替代 RPOPLPUSH/BRPOPLPUSH(已弃用)
RPUSH source a b c
LMOVE source dest RIGHT LEFT
# → "c"(从 source 右弹出,push 到 dest 左)
LRANGE source 0 -1
# → 1) "a" 2) "b"
LRANGE dest 0 -1
# → 1) "c"LMPOP — 批量弹出(7.0+)
LMPOP numkeys key1 [key2 ...] LEFT|RIGHT [COUNT count]- 从第一个非空 List 中弹出 count 个元素
- 返回
key + elements[]数组
RPUSH lst1 a b c
LMPOP 2 lst1 lst2 LEFT COUNT 2
# → 1) "lst1"
# → 2) 1) "a"
# → 2) "b"3.6 常用组合模式
| 模式 | 命令组合 | 说明 |
|---|---|---|
| 栈(Stack) | LPUSH + LPOP |
后进先出(LIFO) |
| 队列(Queue) | LPUSH + RPOP(或 RPUSH + LPOP) |
先进先出(FIFO) |
| 阻塞消息队列 | LPUSH/RPUSH + BRPOP/BLPOP |
消费者阻塞等待消息 |
| 固定大小最新列表 | LPUSH + LTRIM |
维持固定长度,淘汰旧数据 |
| 可靠队列 | RPOPLPUSH / LMOVE |
弹出后放入备份队列,防止处理失败丢失 |
# 栈:后进先出
LPUSH stack item1 item2
LPOP stack # → item2
# 队列:先进先出
RPUSH queue item1 item2
LPOP queue # → item1
# 固定大小最新 100 条
LPUSH news "article1"
LTRIM news 0 99
# 可靠队列模式(原子备份)
LMOVE queue processing RIGHT LEFT # 从 queue 右侧弹出,放入 processing 左侧
# ... 处理成功后再从 processing 删除
LREM processing -1 "task_done"3.7 实战场景
| 场景 | 模式 | 说明 |
|---|---|---|
| 消息队列 | LPUSH + BRPOP | 多个消费者阻塞等待,实现简单 MQ |
| 最新动态 | LPUSH + LTRIM | 保留最近 N 条记录(朋友圈、最新评论) |
| 时间线 | LPUSH + LRANGE | 按时间倒序排列 |
| 工作队列 | RPOPLPUSH / LMOVE | 弹出并备份,处理失败可恢复 |
| 分页列表 | LRANGE key start end | 简单的基于偏移量的分页 |
3.8 常见陷阱
🚨 大 List O(N) 操作: LINDEX、LINSERT、LSET、LREM 都是 O(N)。大 List 频繁使用这些命令会严重阻塞。
🚨 LRANGE 全量获取: LRANGE mylist 0 -1 在 10 万级别元素时可能造成明显延迟。如果需要遍历大 List,考虑 Stream 或分批 LRANGE。
🚨 List 没有去重: 与 Set 不同,List 允许重复元素。如果需要去重,请用 Set。
🚨 BLPOP 客户端异常断开: 客户端阻塞等待时如果断开,Redis 会自动释放该连接,不会造成永久阻塞。
🚨 RPOPLPUSH 已弃用: Redis 6.2+ 请使用 LMOVE 替代 RPOPLPUSH/BRPOPLPUSH。
四、Set(集合)
4.1 概述
Set 是无序的字符串集合,不允许重复元素。支持高效的添加、删除、存在性判断,以及交集、并集、差集运算。底层使用哈希表实现,O(1) 的增删查。
4.2 内部编码
| 编码 | 条件 | 结构 | 说明 |
|---|---|---|---|
intset |
元素全为整数,且数量 < set-max-intset-entries(默认 512) |
有序整数数组 | 内存连续紧凑,查询 O(log N) |
hashtable |
超过阈值或有非整数元素 | 哈希表(dict) | O(1) 查询,value 全为 NULL |
配置阈值(redis.conf):
set-max-intset-entries 512🔬 深入原理:intset 编码转换
intset 是升序排列的整数数组,支持 int16、int32、int64 三种内部编码,自动升级但不会降级。插入非整数或超过 512 个元素时升级为 hashtable。
4.3 基本命令
SADD — 添加元素
SADD key member1 [member2 member3 ...]- 返回成功添加的元素个数(已存在的忽略不计)
SADD tags:article:1 "redis" "database" "nosql"
# → (integer) 3
SADD tags:article:1 "redis" "cache"
# → (integer) 1 ("redis" 已存在,只添加了 "cache")SREM — 删除元素
SREM key member1 [member2 member3 ...]- 返回成功删除的个数
SREM tags:article:1 "nosql" "mysql"
# → (integer) 1 ("mysql" 本就不存在)SCARD — 获取集合大小
SCARD keySCARD tags:article:1
# → (integer) 3SISMEMBER — 判断元素是否存在
SISMEMBER key memberSISMEMBER tags:article:1 "redis"
# → (integer) 1
SISMEMBER tags:article:1 "mongodb"
# → (integer) 0SMEMBERS — 获取全部成员
SMEMBERS key- O(N),N 为集合大小
- 返回无序排列
SMEMBERS tags:article:1
# → 1) "database"
# → 2) "redis"
# → 3) "cache"🚨 陷阱: SMEMBERS 是 O(N) 操作,百万级 Set 会严重阻塞。大 Set 请用 SSCAN 分批迭代。
SRANDMEMBER — 随机获取元素
SRANDMEMBER key [count]- count 为正:返回 count 个不重复的随机元素
- count 为负:返回 |count| 个可能重复的随机元素
- 不删除元素
SRANDMEMBER tags:article:1 2
# → 1) "cache"
# → 2) "redis"SPOP — 随机弹出元素
SPOP key [count]- 随机删除并返回 count 个元素
- count 默认 1
SADD lottery user1 user2 user3
SPOP lottery
# → "user2"
SPOP lottery 2
# → 1) "user3"
# → 2) "user1"SMOVE — 移动元素到另一个集合
SMOVE source destination member- 原子操作:从 source 删除,添加到 destination
- 如果 member 不在 source 中,返回 0
- 如果 member 已在 destination 中,仅从 source 删除
SADD set1 a b
SADD set2 c
SMOVE set1 set2 a
# → (integer) 1
SMEMBERS set1
# → 1) "b"
SMEMBERS set2
# → 1) "a" 2) "c"SSCAN — 增量迭代
SSCAN key cursor [MATCH pattern] [COUNT count]SSCAN tags:article:1 0 MATCH "r*"
# → 1) "0"
# → 2) 1) "redis"4.4 集合运算
差集 SDIFF / SDIFFSTORE
SDIFF key1 [key2 key3 ...] # 返回差集(key1 有,其他 key 没有的)
SDIFFSTORE destination key1 [key2 ...] # 将差集存入 destination- O(N),N 为所有集合元素总数
SADD setA 1 2 3 4
SADD setB 3 4 5 6
SDIFF setA setB
# → 1) "1"
# → 2) "2"
SDIFF setB setA
# → 1) "5"
# → 2) "6"交集 SINTER / SINTERSTORE / SINTERCARD
SINTER key1 [key2 key3 ...] # 返回交集
SINTERSTORE destination key1 [key2 key3 ...] # 交集存入 destination
SINTERCARD numkeys key1 [key2 ...] [LIMIT limit] # 返回交集元素数量(7.0+)SINTER setA setB
# → 1) "3"
# → 2) "4"
# 7.0+:仅计数量(更快,不传输数据)
SINTERCARD 2 setA setB
# → (integer) 2
SINTERCARD 2 setA setB LIMIT 1
# → (integer) 1 (最多统计到 1 就停止)⚡ 性能提示: 多个 Set 做 SINTER 时,Redis 会按元素数量升序处理。将最小集合放在参数列表最前面可获得最佳性能。
并集 SUNION / SUNIONSTORE
SUNION key1 [key2 key3 ...]
SUNIONSTORE destination key1 [key2 ...]SUNION setA setB
# → 1) "1" 2) "2" 3) "3" 4) "4" 5) "5" 6) "6"4.5 集合运算复杂度速查
| 运算 | 复杂度 | 说明 |
|---|---|---|
| SDIFF | O(N) | N 是所有集合元素总数 |
| SINTER | O(N*M) | N 是最小集合大小,M 是集合数量 |
| SUNION | O(N) | N 是所有集合元素总数 |
| SINTERCARD | O(N*M) | 同 SINTER,但只返回计数,不含数据传输 |
4.6 实战场景
| 场景 | 命令 | 说明 |
|---|---|---|
| 标签系统 | SADD/SREM | 文章、商品、用户打标签 |
| 点赞/收藏去重 | SADD + SISMEMBER | 防止重复点赞,快速检查是否已赞 |
| 共同关注/好友 | SINTER | 取两个人的关注集合交集 |
| 可能认识的人 | SDIFF | A 的关注 - B 的关注 = B 认识但 A 不认识的 |
| 黑名单/白名单 | SISMEMBER | 快速判断 IP/用户是否在名单中 |
| 抽奖/随机 | SRANDMEMBER / SPOP | 随机抽取获奖用户 |
| 用户画像/标签集合 | SUNION | 合并多个标签维度的用户 |
| 独立访客统计 | SADD + SCARD | 每日 UV 去重(数据量大时用 HyperLogLog) |
4.7 常见陷阱
🚨 SMEMBERS 阻塞: 大集合做 SMEMBERS 会把所有数据传输到客户端,阻塞 Redis。使用 SSCAN 分批遍历。
🚨 SINTER 内存开销: 对两个大集合做 SINTER,Redis 需要把最小集合的全部元素加载到内存中的哈希表。集合越大,内存峰值越高。
🚨 intset 编码不降级: 删除元素后 intset 不会自动降级为更小的整数类型(如 int64 不会降为 int32)。
🚨 集合运算复杂度: SDIFF/SINTER/SUNION 的复杂度是 O(N),大集合运算可能耗时很长(秒级)。对时间敏感的场景,考虑将计算结果用 SINTERSTORE/SDIFFSTORE 缓存。
🚨 Set 无序: 不能按索引获取元素,也不能保证迭代顺序一致。如需有序,用 ZSet 或 List。
五、Sorted Set(有序集合)
5.1 概述
Sorted Set(ZSet)是带分数的有序集合,每个 member 关联一个 score(double 类型的浮点数),按 score 从小到大排序。score 可以重复,member 必须唯一。底层使用 skiplist(跳表)+ dict(字典)实现,兼顾 O(log N) 的排序和 O(1) 的查找。
5.2 内部编码
| 编码 | 条件 | 结构 | 说明 |
|---|---|---|---|
listpack(7.0+)/ ziplist(6.2 及以前) |
元素数量 < zset-max-listpack-entries(默认 128),且每个 member 长度 < zset-max-listpack-value(默认 64 字节) |
连续内存紧凑数组 | member1, score1, member2, score2… |
skiplist + dict |
超过阈值 | 跳表 + 哈希表 | 双数据结构,skiplist 负责排序,dict 负责 O(1) 查找 score |
配置阈值(redis.conf):
zset-max-listpack-entries 128
zset-max-listpack-value 64🔬 深入原理:skiplist + dict 双数据结构
ZSet 同时维护 skiplist 和 dict:
- skiplist(跳表):按 score 排序,支持范围查询、排名计算,O(log N)
- dict(字典):member → score 的映射,O(1) 查找某个 member 的 score
- 两个结构共享 member 字符串(指针复用),不会双倍内存
5.3 基本命令
ZADD — 添加/更新元素
ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]参数说明:
| 参数 | 含义 |
|---|---|
NX |
仅添加新元素,不更新已存在的 |
XX |
仅更新已存在的元素,不添加新元素 |
GT |
仅在新 score > 旧 score 时更新(Greater Than) |
LT |
仅在新 score < 旧 score 时更新(Less Than) |
CH |
返回变更数量(新增 + 更新),默认只返回新增数 |
INCR |
对 score 做增量(相当于 ZINCRBY),此时只能操作一个 member |
# 基本添加
ZADD leaderboard 100 "alice" 200 "bob" 150 "charlie"
# → (integer) 3
# 仅当不存在时添加
ZADD leaderboard NX 300 "bob"
# → (integer) 0 (bob 已存在)
# 仅当新分数更大时更新
ZADD leaderboard GT 250 "alice"
# → (integer) 0 (250 > 100,更新了,但默认不统计更新)
ZADD leaderboard GT CH 180 "charlie"
# → (integer) 1 (使用 CH 统计更新)
ZADD leaderboard GT CH 50 "alice"
# → (integer) 0 (50 < 250,GT 阻止了更新)
# INCR 模式
ZADD leaderboard INCR 10 "alice"
# → "260"(返回新 score)ZREM — 删除元素
ZREM key member1 [member2 member3 ...]- 返回实际删除的数量(不存在的忽略)
ZREM leaderboard "charlie" "nonexistent"
# → (integer) 1ZSCORE — 获取分数
ZSCORE key memberZSCORE leaderboard "alice"
# → "260"ZCARD — 获取元素数量
ZCARD keyZCARD leaderboard
# → (integer) 2ZRANK / ZREVRANK — 获取排名
ZRANK key member # 升序排名(0 开始,score 最小的为 0)
ZREVRANK key member # 降序排名(0 开始,score 最大的为 0)ZADD scores 80 "A" 90 "B" 70 "C"
ZRANK scores "A"
# → (integer) 1 (70<80<90,A 排第二,索引 1)
ZREVRANK scores "A"
# → (integer) 1 (90>80>70,A 排倒数第二,索引 1)ZCOUNT — 统计分数范围内元素数
ZCOUNT key min maxmin/max默认闭区间,(前缀表示开区间-inf和+inf表示无穷
ZADD temps 20 "Beijing" 30 "Shanghai" 15 "Harbin" 35 "Sanya"
ZCOUNT temps 20 30
# → (integer) 2 (Beijing 20, Shanghai 30)
ZCOUNT temps (20 35
# → (integer) 2 (Shanghai 30, Sanya 35,不含 20)
ZCOUNT temps -inf 25
# → (integer) 2 (Harbin 15, Beijing 20)ZINCRBY — 增加分数
ZINCRBY key increment member- 返回新 score(字符串格式)
- member 不存在时,等同于 ZADD key increment member
ZINCRBY leaderboard 50 "alice"
# → "310"5.4 范围查询命令
ZRANGE — 按索引范围查询
ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count]
[WITHSCORES]参数(6.2+ 统一语法):
| 参数 | 含义 |
|---|---|
BYSCORE |
按 score 范围而非索引(替代旧的 ZRANGEBYSCORE) |
BYLEX |
按字典序范围(替代旧的 ZRANGEBYLEX) |
REV |
逆序(替代 ZREVRANGE / ZREVRANGEBYSCORE) |
LIMIT offset count |
类似 SQL LIMIT |
WITHSCORES |
同时返回 score |
ZADD rank 100 "A" 200 "B" 150 "C" 300 "D" 250 "E"
# 按索引(默认升序)
ZRANGE rank 0 2 WITHSCORES
# → 1) "A" 2) "100"
# → 3) "C" 4) "150"
# → 5) "B" 6) "200"
# 按索引逆序
ZRANGE rank 0 2 REV WITHSCORES
# → 1) "D" 2) "300"
# → 3) "E" 4) "250"
# → 5) "B" 6) "200"
# 按 score 范围
ZRANGE rank 150 250 BYSCORE WITHSCORES
# → 1) "C" 2) "150"
# → 3) "B" 4) "200"
# → 5) "E" 6) "250"
# 按 score 范围 + LIMIT
ZRANGE rank 100 300 BYSCORE LIMIT 1 2 WITHSCORES
# → 1) "C" 2) "150"
# → 3) "B" 4) "200"
# 按 score 范围 + 逆序
ZRANGE rank 300 100 BYSCORE REV WITHSCORES
# → 1) "D" 2) "300"
# → 3) "E" 4) "250"
# → 5) "B" 6) "200"
# → 7) "A" 8) "100"💡 最佳实践: Redis 6.2+ 的统一 ZRANGE 替代了旧的 ZRANGEBYSCORE、ZREVRANGE、ZREVRANGEBYSCORE、ZRANGEBYLEX、ZREVRANGEBYLEX。新代码请使用统一语法。
旧版范围查询命令(6.2 以前)
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] # 按分数范围
ZREVRANGE key start stop [WITHSCORES] # 逆序按索引
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count] # 逆序按分数范围ZRANGESTORE — 存储范围查询结果(6.2+)
ZRANGESTORE dst src min max [BYSCORE | BYLEX] [REV] [LIMIT offset count]ZRANGESTORE top3 rank 0 2 WITHSCORES
# 结果存入 top3,但 WITHSCORES 不作用于 STORE(返回分数会被忽略)
ZRANGESTORE top_by_score rank 150 250 BYSCOREZSCAN — 增量迭代
ZSCAN key cursor [MATCH pattern] [COUNT count]ZSCAN rank 0
# → 1) "0"
# → 2) 1) "A" 2) "100"
# → 3) "C" 4) "150"
# → ...5.5 删除命令
ZREMRANGEBYRANK — 按排名删除
ZREMRANGEBYRANK key start stopZADD z 10 a 20 b 30 c 40 d 50 e
ZREMRANGEBYRANK z 0 2
# → (integer) 3 (删除 a,b,c)ZREMRANGEBYSCORE — 按分数删除
ZREMRANGEBYSCORE key min maxZREMRANGEBYSCORE z 20 40
# → (integer) 2 (删除 b 20, c 30 - 注意上面已经删了 a, b, c,这只是示意)5.6 弹出命令(5.0+)
ZPOPMIN / ZPOPMAX — 弹出最小/最大 score 的元素
ZPOPMIN key [count] # 弹出 score 最小的 count 个
ZPOPMAX key [count] # 弹出 score 最大的 count 个ZADD pq 1 "low" 5 "mid" 10 "high"
ZPOPMIN pq 2
# → 1) "low"
# → 2) "1"
# → 3) "mid"
# → 4) "5"
ZPOPMAX pq 1
# → 1) "high"
# → 2) "10"BZPOPMIN / BZPOPMAX — 阻塞式弹出(5.0+)
BZPOPMIN key1 [key2 key3 ...] timeout
BZPOPMAX key1 [key2 key3 ...] timeout- 阻塞直到有元素可供弹出或超时
- timeout 为 0 表示永久阻塞
- 返回
key + member + score三元素数组
BZPOPMIN delay_queue 0
# (阻塞中...)
# 另一个客户端:
ZADD delay_queue 1600000000 "task1"
# 返回:
# → 1) "delay_queue"
# → 2) "task1"
# → 3) "1600000000"5.7 多集合运算(ZDIFF / ZINTER / ZUNION)
ZDIFF — 差集(6.2+)
ZDIFF numkeys key1 [key2 ...] [WITHSCORES]
ZDIFFSTORE destination numkeys key1 [key2 ...]ZADD z1 10 a 20 b 30 c
ZADD z2 20 b 30 c
ZDIFF 2 z1 z2 WITHSCORES
# → 1) "a"
# → 2) "10"ZINTER — 交集
ZINTER numkeys key1 [key2 ...] [WEIGHTS weight1 [weight2 ...]]
[AGGREGATE SUM|MIN|MAX] [WITHSCORES]
ZINTERSTORE destination numkeys key1 [key2 ...]
[WEIGHTS weight1 [weight2 ...]] [AGGREGATE SUM|MIN|MAX]参数说明:
| 参数 | 含义 |
|---|---|
WEIGHTS |
每个集合 score 的乘数,默认全为 1 |
AGGREGATE SUM |
score 求和(默认) |
AGGREGATE MIN |
取最小 score |
AGGREGATE MAX |
取最大 score |
ZADD z1 10 a 20 b 30 c
ZADD z2 1 a 2 b 5 c
# 交集 score 求和(默认)
ZINTER 2 z1 z2 WITHSCORES
# → 1) "a" 2) "11"
# → 3) "b" 4) "22"
# → 5) "c" 6) "35"
# 带权重(z1 * 2 + z2 * 3)
ZINTER 2 z1 z2 WEIGHTS 2 3 WITHSCORES
# → 1) "a" 2) "23" (10*2 + 1*3)
# → 3) "b" 4) "46" (20*2 + 2*3)
# → 5) "c" 6) "75" (30*2 + 5*3)
# 取最大值
ZINTER 2 z1 z2 AGGREGATE MAX WITHSCORES
# → 1) "a" 2) "10"
# → 3) "b" 4) "20"
# → 5) "c" 6) "30"ZUNION — 并集
ZUNION numkeys key1 [key2 ...] [WEIGHTS weight1 [weight2 ...]]
[AGGREGATE SUM|MIN|MAX] [WITHSCORES]
ZUNIONSTORE destination numkeys key1 [key2 ...]
[WEIGHTS weight1 [weight2 ...]] [AGGREGATE SUM|MIN|MAX]ZADD z1 10 a 20 b
ZADD z2 1 a 5 c
ZUNION 2 z1 z2 WITHSCORES
# → 1) "b" 2) "20" (z2 中不存在,保持原 score)
# → 3) "c" 4) "5" (z1 中不存在,保持原 score)
# → 5) "a" 6) "11" (10 + 1 = 11)
ZUNION 2 z1 z2 AGGREGATE MAX WITHSCORES
# → 1) "a" 2) "10"
# → 3) "b" 4) "20"
# → 5) "c" 6) "5"🔬 深入原理:AGGREGATE 的工作方式
- SUM(默认):
result_score = weight1 * score1 + weight2 * score2 + ... - MIN:
result_score = min(weight1*score1, weight2*score2, ...) - MAX:
result_score = max(weight1*score1, weight2*score2, ...)
如果某个 member 只在部分集合中出现,非参与的集合 score 视为 0(对 MIN 则是 +inf)。
5.8 实战场景
| 场景 | 命令组合 | 说明 |
|---|---|---|
| 排行榜 | ZADD + ZRANGE … REV WITHSCORES | score 为分数/热力值,逆序取 TOP N |
| 延迟队列 | ZADD + BZPOPMIN(或定时 ZRANGE) | score 为 Unix 时间戳,到期弹出处理 |
| 带权重的队列 | ZADD + ZPOPMIN | score 为优先级(越小越优先) |
| 时间线排序 | ZADD + ZRANGE BYSCORE | score 为时间戳,按时间范围过滤 |
| 分页查询 | ZRANGE … LIMIT offset count | 类似 SQL 的分页排序 |
| 多维排序 | ZINTER + WEIGHTS | 不同维度加权求和后排序(如:热度 = 阅读1 + 点赞2 + 评论*3) |
| 滑动窗口限流 | ZADD + ZREMRANGEBYSCORE + ZCARD | score 为时间戳,每次请求清理窗口外数据后计数 |
延迟队列示例
# score = Unix 时间戳(未来某个时间点执行)
ZADD delay_queue 1700000000 "send_email:1001"
ZADD delay_queue 1700000100 "send_sms:2001"
# 消费者:循环弹出到期的任务
# 方式 1:BZPOPMIN 取 score 最小的(最早到期的)
BZPOPMIN delay_queue 0
# 方式 2:定时扫描(可用 Lua 保证原子性)
ZRANGE delay_queue 0 0 BYSCORE -inf <当前时间戳> LIMIT 0 1滑动窗口限流示例
# Lua 脚本原子操作:
# 1. 清理窗口外的旧记录
# 2. 统计当前窗口内的请求数
# 3. 判断是否超过阈值
-- eval 脚本示例
local key = KEYS[1] -- rate:user:1001
local window = ARGV[1] -- 60 秒
local limit = ARGV[2] -- 100 次
local now = redis.call('TIME')[1] -- 秒级时间戳
redis.call('ZREMRANGEBYSCORE', key, 0, now - window) -- 清理过期
local count = redis.call('ZCARD', key)
if count < tonumber(limit) then
redis.call('ZADD', key, now, now .. ':' .. math.random())
redis.call('EXPIRE', key, window) -- 整个 key 在窗口结束后过期
return 1 -- 放行
else
return 0 -- 限流
end5.9 常见陷阱
🚨 skiplist + dict 内存开销: ZSet 维护双数据结构,内存占用比 Set 高。少量元素用 listpack 编码可大幅节省。
🚨 ZRANGE 全量返回: 大 ZSet 上用 ZRANGE key 0 -1 会阻塞。使用 LIMIT 分页。
🚨 score 类型是 double: score 是 IEEE 754 双精度浮点,存在精度问题。精确比较时慎用相等(==),改用范围。
🚨 ZINTERSTORE 目标 key 已存在: 会覆盖目标 key 的全部数据。如果目标 key 存有重要数据,确认语法正确。
🚨 ZDIFF/ZINTER/ZUNION 性能: 这些运算可能非常耗时(O(N*K) + O(M*log M))。对于大数据集,优先使用 *STORE 版本缓存结果。
🚨 member 长度影响编码: 长 member(>64 字节)会使 ZSet 从 listpack/ziplist 升级为 skiplist+dict,内存大幅增加。
六、GEO(地理位置)
6.1 概述
GEO 是 Redis 3.2 引入的地理位置数据类型,用于存储经纬度坐标。底层基于 ZSet,将经纬度通过 GeoHash 算法编码为 52 位整数作为 ZSet 的 score,member 即为位置名称。这意味着 GEO 可以使用 ZSet 的全部命令。
6.2 内部编码与底层原理
| 组件 | 说明 |
|---|---|
| 底层数据结构 | ZSet(skiplist + dict) |
| score | GeoHash 编码:经纬度 -> 52 位整数 |
| member | 位置名称(如 “Beijing”、“Store:1001”) |
| 编码方式 | 将经纬度交替编码到 52 位整数(26 位经度 + 26 位纬度),使用 Z-order 曲线 |
🔬 深入原理:GeoHash 精度与误差
GeoHash 将二维经纬度降为一维编码。52 位编码的精度:
| GeoHash 位数 | 纬度位数 | 经度位数 | 纬度误差 | 经度误差 | 距离误差 |
|---|---|---|---|---|---|
| 52 bits | 26 bits | 26 bits | ~0.6 米 | ~0.6 米 | ~0.8 米 |
- 原理:二分空间,经度 [-180, 180] 和纬度 [-90, 90] 交替二分
- 编码越短:范围越大,精度越低;编码越长:范围越小,精度越高
- 相邻位置的 GeoHash 前缀可能不同(Z-order 曲线边界问题)
💡 为什么基于 ZSet?
- 免费获得 ZSet 的分页(ZRANGE)、删除(ZREM)、计数(ZCARD)功能
- GEO 元素可以混用 ZSet 命令:
ZREM locations "Store:1"可以删除 GEO 位置 - 但 不要 用 ZADD/ZSCORE 直接设置/读取 GEO score,必须用 GEO 命令
6.3 基本命令
GEOADD — 添加地理位置
GEOADD key [NX | XX] [CH] longitude latitude member [longitude latitude member ...]参数说明:
| 参数 | 含义 |
|---|---|
NX |
仅添加新位置,不更新已存在的(6.2+) |
XX |
仅更新已存在的位置(6.2+) |
CH |
返回变更数量(包括更新) |
longitude |
经度,范围 [-180, 180] |
latitude |
纬度,范围 [-85.05112878, 85.05112878] |
GEOADD cities 116.397 39.908 "Beijing"
# → (integer) 1
GEOADD cities 121.473 31.230 "Shanghai" 113.264 23.129 "Guangzhou"
# → (integer) 2
# NX 模式:仅添加不存在的
GEOADD cities NX 116.397 39.908 "Beijing"
# → (integer) 0 (Beijing 已存在,不更新)🚨 陷阱: 纬度范围是 [-85.05112878, 85.05112878],接近两极的点无法添加(这是 Mercator 投影的限制)。
GEOPOS — 获取经纬度
GEOPOS key member1 [member2 member3 ...]- 返回
[longitude, latitude]数组对 - 不存在的 member 返回
(nil)
GEOPOS cities Beijing Shanghai nonexistent
# → 1) 1) "116.39700299501419"
# → 2) "39.90799964970965"
# → 2) 1) "121.47300010919571"
# → 2) "31.229999155818104"
# → 3) (nil)🔬 浮点精度说明: 返回的经纬度与输入值略有差异,因为 GeoHash 编码带来的精度损失(约 0.6 米级别)。
GEODIST — 计算两点距离
GEODIST key member1 member2 [m | km | ft | mi]单位选项:
| 单位 | 含义 |
|---|---|
m |
米(默认) |
km |
千米 |
ft |
英尺 |
mi |
英里 |
GEODIST cities Beijing Shanghai km
# → "1067.6087"
GEODIST cities Beijing Shanghai m
# → "1067608.6843"GEOHASH — 获取 GeoHash 字符串
GEOHASH key member1 [member2 member3 ...]- 返回 Base32 编码的 GeoHash 字符串(11 个字符)
- GeoHash 越长精度越高,可用于客户端自行计算
GEOHASH cities Beijing Shanghai
# → 1) "wx4g0bdrfz0"
# → 2) "wtw3sj5zbj0"💡 最佳实践: 使用 GEOHASH 可以在应用层快速估算距离。两个 GeoHash 字符串前缀匹配长度越长,两点越近(但有边界效应,不是绝对的)。
6.4 范围查询命令
GEOSEARCH — 通用范围查询(6.2+ 统一命令)
GEOSEARCH key <FROMMEMBER member | FROMLONLAT longitude latitude>
<BYRADIUS radius m|km|ft|mi | BYBOX width height m|km|ft|mi>
[ASC | DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]参数说明:
| 参数 | 含义 |
|---|---|
FROMMEMBER member |
以某个已存在的位置为圆心 |
FROMLONLAT lon lat |
以指定经纬度为圆心 |
BYRADIUS radius unit |
按半径搜索 |
BYBOX width height unit |
按矩形搜索(宽、高) |
ASC / DESC |
按距离升序/降序 |
COUNT count |
限制返回数量 |
ANY |
只要找到 count 个就返回(不保证最近的 count 个) |
WITHCOORD |
返回经纬度 |
WITHDIST |
返回距离 |
WITHHASH |
返回 GeoHash 编码的 52 位整数值 |
# 以 Beijing 为圆心,半径 500 km
GEOSEARCH cities FROMMEMBER Beijing BYRADIUS 500 km ASC WITHCOORD WITHDIST
# → 1) 1) "Beijing"
# → 2) "0.0000"
# → 3) 1) "116.39700299501419"
# → 2) "39.90799964970965"
# 以给定经纬度为圆心,半径 1000 km
GEOSEARCH cities FROMLONLAT 116.397 39.908 BYRADIUS 1000 km ASC COUNT 2 WITHDIST
# → 1) 1) "Beijing"
# → 2) "0.0001"
# → 2) 1) "Shanghai"
# → 2) "1067.6087"
# 矩形搜索
GEOSEARCH cities FROMMEMBER Beijing BYBOX 2000 2000 km ASC WITHDISTGEOSEARCHSTORE — 存储搜索结����(6.2+)
GEOSEARCHSTORE destination source <FROMMEMBER member | FROMLONLAT lon lat>
<BYRADIUS radius unit | BYBOX width height unit>
[ASC | DESC] [COUNT count [ANY]] [STOREDIST]STOREDIST:将距离作为 score 存入 destination(默认存 GeoHash)
GEOSEARCHSTORE nearby_cities cities FROMMEMBER Beijing BYRADIUS 500 km
# → (integer) 1
ZRANGE nearby_cities 0 -1 WITHSCORES
# → 1) "Beijing" 2) "4069885361241787" (GeoHash 52 位整数)
# STOREDIST 将距离存为 score
GEOSEARCHSTORE nearby_dist cities FROMMEMBER Beijing BYRADIUS 500 km STOREDIST
ZRANGE nearby_dist 0 -1 WITHSCORES
# → 1) "Beijing" 2) "0"6.5 旧版命令(6.2 以前)
GEORADIUS — 按经纬度半径搜索
GEORADIUS key longitude latitude radius m|km|ft|mi
[WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC]
[STORE key] [STOREDIST key]GEORADIUSBYMEMBER — 按成员半径搜索
GEORADIUSBYMEMBER key member radius m|km|ft|mi
[WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC]
[STORE key] [STOREDIST key]GEORADIUS_RO / GEORADIUSBYMEMBER_RO — 只读版本(6.2+)
GEORADIUS_RO key longitude latitude radius m|km|ft|mi
[WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC]
GEORADIUSBYMEMBER_RO key member radius m|km|ft|mi
[WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC]_RO= Read-Only,不支持STORE/STOREDIST,可在只读副本上执行
💡 最佳实践: Redis 6.2+ 推荐使用统一的 GEOSEARCH/GEOSEARCHSTORE 替代所有旧版 GEORADIUS 命令。
6.6 实战场景
| 场景 | 命令 | 说明 |
|---|---|---|
| 附近的人 | GEOSEARCH ... FROMMEMBER user1 BYRADIUS 5 km |
以用户位置为圆心,搜索 5km 内的其他用户 |
| 外卖配送 | GEOSEARCH ... FROMLONLAT ... BYRADIUS 3 km |
搜索附近 3km 内的骑手 |
| 门店搜索 | GEOSEARCH ... FROMLONLAT ... BYRADIUS 10 km ASC COUNT 20 |
搜索最近 20 家门店 |
| 配送范围判断 | GEOSEARCH ... FROMMEMBER store BYRADIUS 3 km |
判断用户地址是否在配送范围内 |
| 距离排序 | GEOSEARCH + ASC |
按距离由近到远排序 |
| LBS 应用 | GEO 全套命令 | 打车、找房、社交等一切基于位置的场景 |
6.7 与其他类型互通
GEO 底层是 ZSet,可以混合使用 ZSet 命令:
# GEO 添加
GEOADD locations 116.397 39.908 "A"
GEOADD locations 121.473 31.230 "B"
# ZSet 命令查看
ZRANGE locations 0 -1 WITHSCORES
# → 1) "A" 2) "4069885361241787"
# → 3) "B" 4) "4052022549971571"
# ZSet 命令删除(合法)
ZREM locations "A"
# → (integer) 1
# ❌ 不要用 ZADD 直接写 GEO 数据(score 是 GeoHash 编码,手工计算不可行)⚡ 性能提示: 如果需要批量删除 GEO 数据,用 ZREM key member1 member2 ... 比多次 GEOADD ... 重新覆盖更高效(GEO 没有提供删除命令)。
6.8 常见陷阱
🚨 纬度限制: 纬度必须在 [-85.05112878, 85.05112878] 范围内,极地坐标无法存储。
🚨 GeoHash 精度限制: GEOPOS 返回的经纬度有约 0.6 米的误差。不是 GPS 精度的替代方案,只能做近似查询。
🚨 不要直接用 ZSet 命令写 GEO 数据: GEO 的 score 是 GeoHash 编码,直接用 ZADD 设置 score 会导致 GEO 命令结果错误。
🚨 大 GEO 集合查询: GEOSEARCH 底层是 ZRANGEBYSCORE + 平面距离计算(Haversine 公式)。在百万级数据中查询 500km 范围可能很慢(需扫描大量 GeoHash 区间)。
🚨 GeoHash 边界效应: 两个位置在 GeoHash 边界两侧时,前缀几乎完全不同,但实际距离很近。这就是 Z-order 曲线的局限性。
七、Bitmap(位图)
7.1 概述
Bitmap 不是独立的数据类型,而是 String 类型的位操作扩展。一个 String 可以看作一个位数组,支持对每一位进行 get/set。最大长度为 2^32 bits = 512 MB,可表示约 42.9 亿个位。
7.2 底层原理
- Bitmap 底层是 String(SDS),只是把每个字节(8 bits)按位操作
- offset 0 对应第一个字节的最高位(Redis 内部偏向左移)
- 最大 offset 为 2^32 - 1
🔬 深入原理:Bitmap 内存效率
| 数据规模 | 传统方式 | Bitmap 方式 |
|---|---|---|
| 1 亿用户签到标记 | 1 亿个 key(每个 1bit -> 56+ bytes overhead) | 1 个 key ~12.5 MB |
| 1 亿个布尔标记 | ~5.6 GB(String overhead) | ~12.5 MB |
| 8 亿用户日活 | ~44.8 GB | ~100 MB |
- 每个位的存储成本是 1 bit,但 SDS 有结构和 padding 开销
- SETBIT 可设置任意 offset,中间的位自动填充 0
7.3 基本命令
SETBIT — 设置某一位的值
SETBIT key offset value- offset 从 0 开始
- value 为 0 或 1
- 返回旧值(0 或 1)
- offset 超出当前长度时,自动扩展并填充 0
SETBIT sign:user:1001 0 1
# → (integer) 0
SETBIT sign:user:1001 6 1
# → (integer) 0
SETBIT sign:user:1001 6 0
# → (integer) 1GETBIT — 获取某一位的值
GETBIT key offsetGETBIT sign:user:1001 0
# → (integer) 1
GETBIT sign:user:1001 1
# → (integer) 0BITCOUNT — 统计值为 1 的位数
BITCOUNT key [start end [BYTE | BIT]]- start/end:按字节索引(默认),支持负数
- 7.0+ 支持
BIT选项:按位索引 - O(N),N 为统计范围的字节数
SETBIT counter 0 1
SETBIT counter 10 1
SETBIT counter 100 1
BITCOUNT counter
# → (integer) 3
# 按字节范围统计
BITCOUNT counter 0 0
# → (integer) 1 (第 0 字节:偏移 0-7 中只有 offset 0 为 1)
# 按位范围统计(7.0+)
BITCOUNT counter 0 10 BIT
# → (integer) 2 (offset 0-10 中有 offset 0 和 10 为 1)BITPOS — 查找第一个 0 或 1 的位置
BITPOS key bit [start [end [BYTE | BIT]]]- bit:0 或 1
- 返回第一个匹配的 offset(从 0 开始)
- 找不到返回 -1
- start/end 默认按字节索引
SETBIT bitmap 10 1
SETBIT bitmap 20 1
BITPOS bitmap 1
# → (integer) 10 (第一个 1 在 offset 10)
BITPOS bitmap 0
# → (integer) 0 (第一个 0 在 offset 0)
BITPOS bitmap 1 2 2
# → (integer) -1 (第 2-2 字节范围内没有 1)7.4 BITFIELD — 任意位宽整数读写(3.2+)
BITFIELD 将 Bitmap 看作任意位宽的整数数组,支持对指定偏移量的整数读写。
BITFIELD key [GET type offset] [SET type offset value]
[INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]type 格式: [u|i]<bits>,如 u8(无符号 8 位)、i16(有符号 16 位)
| 类型 | 范围 | 说明 |
|---|---|---|
u1 ~ u63 |
0 ~ 2^n - 1 | 无符号整数 |
i1 ~ i64 |
-2^(n-1) ~ 2^(n-1) - 1 | 有符号整数 |
OVERFLOW 溢出策略:
| 策略 | 含义 |
|---|---|
WRAP |
环绕(默认):u8 的 255 + 1 = 0, i8 的 127 + 1 = -128 |
SAT |
截断饱和:超出最大值则停在最大值,超出最小值停在最小值 |
FAIL |
溢出时不执行操作,返回 nil |
# SET: 在 offset 0 处设置一个 8 位无符号整数
BITFIELD user:stats SET u8 0 100
# → 1) (integer) 0 (旧值)
# GET: 读取
BITFIELD user:stats GET u8 0
# → 1) (integer) 100
# INCRBY: 自增
BITFIELD user:stats INCRBY u8 0 1
# → 1) (integer) 101
# 多条操作组合(原子执行)
BITFIELD user:stats SET u8 #0 100 INCRBY u8 #1 50 GET u8 #1
# → 1) (integer) 0 (SET 旧值)
# → 2) (integer) 50 (INCRBY 结果,key 不存在认为 0)
# → 3) (integer) 50 (GET 结果)
# 溢出控制
BITFIELD key OVERFLOW SAT INCRBY u8 0 1 # 使用 SAT 策略
# → 如果当前是 255,则仍返回 255(不溢出)💡 按位偏移 vs 整数字段偏移:
#前缀:按整数字段索引,如u8 #0是第 0 个 u8(offset 0),u8 #1是第 1 个 u8(offset 8)- 不带
#:直接指定位偏移量,如u8 0(offset 0),u8 8(offset 8)
BITFIELD_RO — 只读版 BITFIELD(6.0+)
BITFIELD_RO key GET type offset [GET type offset ...]- 仅支持 GET,不能在只读副本上报错
- 适合在 slave / read-only 副本上查询
7.5 BITOP — 位运算
BITOP operation destkey key1 [key2 key3 ...]| 操作 | 含义 |
|---|---|
AND |
按位与 |
OR |
按位或 |
XOR |
按位异或 |
NOT |
按位取反(只能跟一个源 key) |
- O(N),N 为最长 key 的长度
- 返回 destkey 的长度(字节数)
SETBIT a 0 1
SETBIT a 3 1
SETBIT b 0 1
SETBIT b 1 1
BITOP AND result a b
BITCOUNT result
# → (integer) 1 (只有 offset 0 同时为 1)
BITOP OR result a b
BITCOUNT result
# → (integer) 3 (offset 0, 1, 3 为 1)7.6 实战场景
| 场景 | 命令 | 说明 |
|---|---|---|
| 用户签到 | SETBIT + BITCOUNT | 用户 ID 映射到 offset,day of year 做 key |
| 活跃用户统计 | BITOP AND/OR + BITCOUNT | 多天签到做 AND 得到连续活跃用户 |
| 布隆过滤器(简化版) | SETBIT + GETBIT | 多个哈希函数映射到不同 offset |
| 权限位 | SETBIT + GETBIT | 每位代表一个权限开关 |
| 在线状态 | SETBIT + BITCOUNT | 每位代表一个用户的在线状态 |
| Bloom Filter(计数位图) | BITFIELD INCRBY | 多位计数器(如 u3 表示 0-7 次) |
| 用户画像标签 | SETBIT + BITOP AND/OR | 多标签取交集/并集 |
签到统计连续天数示例
# 假设:今天是 day 200,用户 ID=1001
# 用户 ID 作为 offset,每天一个 Bitmap key
# 记录签到
SETBIT sign:200 1001 1 # 第 200 天签到
SETBIT sign:199 1001 1 # 第 199 天签到
SETBIT sign:198 1001 1 # 第 198 天签到
# 统计用户本月签到天数(day 170-199)
BITOP OR monthly sign:170 sign:171 ... sign:199
BITCOUNT monthly
# 统计连续 3 天都签到的用户(取交集)
BITOP AND consecutive:3 sign:198 sign:199 sign:200
BITCOUNT consecutive:3
# 统计某用户连续签到天数(应用层逻辑):
# 从今天往前逐天 GETBIT,遇到 0 停止布隆过滤器简化实现
# 使用 3 个哈希函数映射到同一个 Bitmap
SETBIT bloom:users <hash1(uid)> 1
SETBIT bloom:users <hash2(uid)> 1
SETBIT bloom:users <hash3(uid)> 1
# 检查是否存在
GETBIT bloom:users <hash1(uid)> # 都为 1 → 可能存在
GETBIT bloom:users <hash2(uid)> # 任一为 0 → 一定不存在
GETBIT bloom:users <hash3(uid)>7.7 常见陷阱
🚨 Bitmap 不是独立类型: Bitmap 底层是 String,可以用 GET/STRLEN 查看。SETBIT 设置高位 offset 时,整个 String 被扩展到相应长度,中间填充 \x00。
🚨 BITCOUNT 的 start/end 是字节索引: 不是位索引。BITCOUNT key 0 0 统计的是第 1 个字节(offset 0-7),而非第 1 个位。7.0+ 可用 BIT 选项按位索引。
🚨 SETBIT offset 极大造成的扩容: SETBIT mykey 1000000000 1 会创建约 125 MB 的字符串(内存分配阻塞)。
🚨 BITFIELD 溢出默认 WRAP: u8 的 255 + 1 = 0。需要饱和溢出用 OVERFLOW SAT。
🚨 BITOP 时间复杂度 O(N): 两个大 Bitmap 做 BITOP 需要遍历全部字节,耗时与最长 key 的长度成正比。
八、HyperLogLog(基数统计)
8.1 概述
HyperLogLog(HLL)是一种概率性数据结构,用于估算集合的基数(不重复元素的数量)。其核心优势是:无论存储多少元素,单个 key 只需约 12 KB 内存,标准误差仅 0.81%。
8.2 底层原理
| 特性 | 值 |
|---|---|
| 内存占用(单个 key) | ~12 KB(固定) |
| 标准误差 | 0.81%(即 99.19% 置信区间) |
| 内部桶数 | 16384 个(2^14) |
| 每个桶位数 | 6 bits |
| 算法 | HyperLogLog(基于 Philippe Flajolet 论文) |
🔬 深入原理:HyperLogLog 工作流程
- 哈希:对每个元素计算 64 位哈希值
- 分桶:取哈希值的低 14 位,确定放入哪个桶(0~16383)
- 记录前导零:取哈希值的高 50 位,计算前导零长度 + 1,存入该桶(取最大值)
- 估算:对所有桶使用调和平均数公式计算基数近似值
为什么是 16384 个桶?
- 桶越多,精度越高,但内存越多
- 16384 桶 * 6 bits/桶 = 98304 bits = 12288 bytes ~ 12 KB
- 这是精度(0.81%)和内存的最佳平衡点
稀疏 vs 稠密编码:
- 稀疏编码:元素少时(HLL 内部计数器多为 0),用稀疏结构省内存
- 稠密编码:元素多时,自动切换为标准 16384 桶的 6-bit 存储
- 转换阈值由
hll-sparse-max-bytes控制(默认 3000 字节)
8.3 命令
HLL 只有 3 个命令,API 极其简洁。
PFADD — 添加元素
PFADD key element1 [element2 element3 ...]- 返回 1 表示内部寄存器被修改(基数增加),返回 0 表示无变化
- 注意:返回 0 不代表元素已存在,只是估算值未变(概率性)
PFADD visitors "user:1" "user:2" "user:3"
# → (integer) 1 (基数发生了变化)
PFADD visitors "user:1"
# → (integer) 0 (估算值未变)PFCOUNT — 估算基数
PFCOUNT key1 [key2 key3 ...]- 单个 key:返回该 key 的基数估算值
- 多个 key:返回所有 key 的并集基数估算值(等于临时 PFMERGE)
- 基数较小时,使用 HLL 的稀疏编码,结果非常精确
PFCOUNT visitors
# → (integer) 3
PFADD visitors2 "user:3" "user:4" "user:5"
PFCOUNT visitors visitors2
# → (integer) 5 (并集:user:1,2,3,4,5 = 5 个)PFMERGE — 合并多个 HLL
PFMERGE destkey sourcekey1 [sourcekey2 sourcekey3 ...]- 将多个 HLL 合并为一个,结果存入 destkey
- 可用于计算多日 UV 的去重总和
PFADD day1 "user:1" "user:2" "user:3"
PFADD day2 "user:2" "user:3" "user:4"
PFMERGE week1 day1 day2
PFCOUNT week1
# → (integer) 4 (user:1,2,3,4 去重后 4 个)8.4 实战场景
| 场景 | 命令 | 说明 |
|---|---|---|
| UV 统计 | PFADD + PFCOUNT | 页面/PV 独立访客统计 |
| 每日/每周/每月 UV | PFMERGE | 每日 HLL 合并为周/月 HLL |
| 独立 IP 统计 | PFADD + PFCOUNT | API 调用来源 IP 去重 |
| 大数据去重估算 | PFADD + PFCOUNT | 海量数据中估算不重复元素数 |
| 广告曝光去重 | PFADD | 广告独立观众数 |
| 搜索关键词 UV | PFADD + PFCOUNT | 每个关键词的独立搜索用户数 |
UV 统计完整示例
# 统计每个页面每天的独立访客
PFADD uv:page:home:20240101 "user:1" "user:2"
PFADD uv:page:home:20240102 "user:2" "user:3"
# 单日 UV
PFCOUNT uv:page:home:20240101
# → 2
# 合并为两天去重 UV
PFMERGE uv:page:home:2days uv:page:home:20240101 uv:page:home:20240102
PFCOUNT uv:page:home:2days
# → 3 (user:1, user:2, user:3)
# 多天可以直接传给 PFCOUNT(不持久化合并结果)
PFCOUNT uv:page:home:20240101 uv:page:home:20240102
# → 38.5 使用限制与精度
| 数据规模 | 实际误差表现 |
|---|---|
| < 10000 个元素 | 几乎没有误差(稀疏编码) |
| 10000 ~ 100 万 | 标准误差 0.81% 以内 |
| > 1 亿 | 误差仍保持在 0.81% 左右 |
| 极小基数(< 100) | 使用线性计数(Linear Counting)修正,精度较高 |
💡 最佳实践:
- HLL 适合 百万级及以上 数据的去重统计
- 小数据量(< 1000)用 Set(SCARD 精确)即可
- HLL 无法获取成员、删除元素,只能添加和统计
- 统计"最近 N 天累计 UV"用 PFMERGE 优于多次 PFCOUNT(不用重复计算)
8.6 HLL vs Set vs Bitmap 基数统计对比
| 维度 | HyperLogLog | Set | Bitmap |
|---|---|---|---|
| 内存(1 亿个不同元素) | ~12 KB | ~5.6 GB | ~12.5 MB |
| 精确度 | 估算,误差 0.81% | 精确 | 精确 |
| 能否获取元素 | 不能 | 能(SMEMBERS) | 不能 |
| 能否删除元素 | 不能 | 能 | 能(SETBIT offset 0) |
| 添加速度 | O(1) 极快 | O(1) | O(1) |
| 统计速度 | O(1) 极快 | O(1) | O(N) BITCOUNT |
| 适合场景 | 大基数估算 | 小基数,需精确 | 用户 ID 用数字、偏移量已知 |
8.7 常见陷阱
🚨 不能获取元素: HLL 只存概率状态,不存储原始数据。无法列出有哪些元素,无法删除,无法判断某个元素是否已添加。
🚨 误差存在: 0.81% 的标准误差意味着 100 万的实际 UV 可能显示为 99.2 万到 100.8 万。需要精确统计时用 Set(但内存代价大)。
🚨 PFADD 返回 0 不等于元素重复: PFADD 返回 0 只说明基数估算值没变——即使是新元素,如果哈希分布碰巧不改变寄存器值,也会返回 0。
🚨 PFMERGE 会覆盖 destkey: 目标 key 原有数据会被覆盖,不是追加。
🚨 12KB 对小数据过重: 如果只有几百个元素,12KB 显得浪费(稀疏编码阶段会小很多)。元素极多时才体现 HLL 的内存优势。
附录:数据类型完整命令速查
String 命令总表
| 命令 | 复杂度 | 用途 |
|---|---|---|
SET |
O(1) | 设置值(支持 NX/XX/GET/EX/PX 等) |
GET |
O(1) | 获取值 |
GETSET |
O(1) | 设置并返回旧值(已弃用,用 SET…GET) |
GETDEL |
O(1) | 获取并删除(6.2+) |
GETEX |
O(1) | 获取并设置过期(6.2+) |
GETRANGE |
O(N) | 获取子串 |
SETRANGE |
O(N) | 覆写指定位置子串 |
STRLEN |
O(1) | 获取字符串长度 |
APPEND |
O(N) | 追加内容 |
MGET |
O(N) | 批量获取 |
MSET |
O(N) | 批量设置 |
MSETNX |
O(N) | 原子批量设置 |
INCR |
O(1) | 自增 1 |
DECR |
O(1) | 自减 1 |
INCRBY |
O(1) | 自增指定值 |
DECRBY |
O(1) | 自减指定值 |
INCRBYFLOAT |
O(1) | 浮点自增 |
SETNX |
O(1) | 不存在时设置 |
SETEX |
O(1) | 设置并设置过期秒 |
LCS |
O(N*M) | 最长公共子序列(7.0+) |
Hash 命令总表
| 命令 | 复杂度 | 用途 |
|---|---|---|
HSET |
O(1)/O(N) | 设置 field-value |
HGET |
O(1) | 获取单个 field |
HMGET |
O(N) | 获取多个 field |
HGETALL |
O(N) | 获取全部 field-value |
HKEYS |
O(N) | 获取全部 field |
HVALS |
O(N) | 获取全部 value |
HDEL |
O(N) | 删除 field |
HEXISTS |
O(1) | 判断 field 是否存在 |
HLEN |
O(1) | 获取 field 数量 |
HSTRLEN |
O(1) | 获取 field 值长度 |
HSETNX |
O(1) | 不存在时设置 |
HINCRBY |
O(1) | 整数自增 |
HINCRBYFLOAT |
O(1) | 浮点自增 |
HRANDFIELD |
O(N) | 随机获取 field(6.2+) |
HSCAN |
O(1) 每步 | 增量迭代 |
List 命令总表
| 命令 | 复杂度 | 用途 |
|---|---|---|
LPUSH / RPUSH |
O(1)/O(N) | 头部/尾部插入 |
LPOP / RPOP |
O(1)/O(N) | 头部/尾部弹出 |
BLPOP / BRPOP |
O(1) | 阻塞式弹出 |
LRANGE |
O(S+N) | 范围查询 |
LTRIM |
O(N) | 修剪列表 |
LLEN |
O(1) | 获取长度 |
LINDEX |
O(N) | 按索引获取 |
LINSERT |
O(N) | 指定位置插入 |
LSET |
O(N) | 按索引替换 |
LREM |
O(N+M) | 删除匹配元素 |
LPOS |
O(N) | 查找元素位置(6.0.6+) |
LMOVE / BLMOVE |
O(1) | 原子移动元素(6.2+) |
LMPOP |
O(N) | 批量弹出(7.0+) |
Set 命令总表
| 命令 | 复杂度 | 用途 |
|---|---|---|
SADD |
O(1)/O(N) | 添加元素 |
SREM |
O(1)/O(N) | 删除元素 |
SCARD |
O(1) | 获取集合大小 |
SISMEMBER |
O(1) | 判断元素是否存在 |
SMEMBERS |
O(N) | 获取全部元素 |
SRANDMEMBER |
O(1)/O(N) | 随机获取元素 |
SPOP |
O(1)/O(N) | 随机弹出元素 |
SMOVE |
O(1) | 移动元素到另一个集合 |
SDIFF |
O(N) | 差集 |
SINTER |
O(N*M) | 交集 |
SUNION |
O(N) | 并集 |
SDIFFSTORE |
O(N) | 差集存入目标 |
SINTERSTORE |
O(N*M) | 交集存入目标 |
SUNIONSTORE |
O(N) | 并集存入目标 |
SINTERCARD |
O(N*M) | 交集基数(7.0+) |
SSCAN |
O(1) 每步 | 增量迭代 |
ZSet 命令总表
| 命令 | 复杂度 | 用途 |
|---|---|---|
ZADD |
O(log N) | 添加/更新元素 |
ZREM |
O(M*log N) | 删除元素 |
ZSCORE |
O(1) | 获取分数 |
ZCARD |
O(1) | 获取元素数量 |
ZRANK |
O(log N) | 升序排名 |
ZREVRANK |
O(log N) | 降序排名 |
ZCOUNT |
O(log N) | 统计分数范围内元素数 |
ZINCRBY |
O(log N) | 增加分数 |
ZRANGE |
O(log N + M) | 范围查询(6.2+ 统一语法) |
ZRANGESTORE |
O(log N + M) | 存储范围查询结果(6.2+) |
ZPOPMIN / ZPOPMAX |
O(log N*M) | 弹出最小/最大(5.0+) |
BZPOPMIN / BZPOPMAX |
O(log N) | 阻塞式弹出(5.0+) |
ZREMRANGEBYRANK |
O(log N+M) | 按排名删除 |
ZREMRANGEBYSCORE |
O(log N+M) | 按分数删除 |
ZDIFF |
O(N*K)+O(M*log M) | 差集(6.2+) |
ZINTER |
O(N*K)+O(M*log M) | 交集 |
ZUNION |
O(N)+O(M*log M) | 并集 |
ZDIFFSTORE |
O(N*K)+O(M*log M) | 差集存储 |
ZINTERSTORE |
O(N*K)+O(M*log M) | 交集存储 |
ZUNIONSTORE |
O(N)+O(M*log M) | 并集存储 |
ZSCAN |
O(1) 每步 | 增量迭代 |
GEO 命令总表
| 命令 | 复杂度 | 用途 |
|---|---|---|
GEOADD |
O(log N) | 添加位置 |
GEOPOS |
O(log N) | 获取经纬度 |
GEODIST |
O(log N) | 计算两点距离 |
GEOHASH |
O(log N) | 获取 GeoHash 字符串 |
GEOSEARCH |
O(N+log(M)) | 通用范围查询(6.2+) |
GEOSEARCHSTORE |
O(N+log(M)) | 存储查询结果(6.2+) |
GEORADIUS_RO |
O(N+log(M)) | 只读半径查询 |
Bitmap 命令总表
| 命令 | 复杂度 | 用途 |
|---|---|---|
SETBIT |
O(1) | 设置指定位 |
GETBIT |
O(1) | 获取指定位 |
BITCOUNT |
O(N) | 统计 1 的位数 |
BITPOS |
O(N) | 查找第一个 1 或 0 |
BITFIELD |
O(1) 单个子命令 | 任意位宽整数字段读写 |
BITFIELD_RO |
O(1) 单个子命令 | 只读 BITFIELD(6.0+) |
BITOP |
O(N) | 位运算(AND/OR/XOR/NOT) |
HyperLogLog 命令总表
| 命令 | 复杂度 | 用途 |
|---|---|---|
PFADD |
O(1) | 添加元素 |
PFCOUNT |
O(1) / O(N) | 估算基数(多 key 时为 O(N)) |
PFMERGE |
O(N) | 合并多个 HLL |
关键命令版本演进一览
| 版本 | 新增/变更 | 类型 |
|---|---|---|
| 1.0 | String, Hash, List, Set, ZSet 基本命令 | 全类型 |
| 2.0 | SETBIT, GETBIT | Bitmap |
| 2.6 | BITCOUNT, BITOP, INCRBYFLOAT, HINCRBYFLOAT | Bitmap, String, Hash |
| 2.8 | BITPOS, SSCAN, HSCAN, ZSCAN, SRANDMEMBER count, ZRANGEBYLEX | Bitmap, 迭代, Set, ZSet |
| 3.2 | GEO 类型, BITFIELD, quicklist | GEO, Bitmap, List |
| 4.0 | 统一 HSET 语法(废弃 HMSET) | Hash |
| 5.0 | ZPOPMIN/ZPOPMAX, BZPOPMIN/BZPOPMAX | ZSet |
| 6.0 | BITFIELD_RO | Bitmap |
| 6.0.6 | LPOS | List |
| 6.2 | GETDEL, GETEX, GETRANGE 负索引, HRANDFIELD, LMOVE/BLMOVE, ZRANGE 统一语法, ZDIFF/ZINTER/ZUNION, ZRANGESTORE, GEOSEARCH/GEOSEARCHSTORE, GEORADIUS_RO | 全类型 |
| 7.0 | LCS, LMPOP, SINTERCARD, BITCOUNT BIT | String, List, Set, Bitmap |