写在前面
ZSet(有序集合)和Bitmap(位图)是Redis中的高级数据类型。ZSet让排行榜、延时队列等场景变得简单,而Bitmap则用极小的内存实现海量数据的统计。今天我们深入探讨这两种强大类型的实战技巧。

文章目录
-
- 写在前面
- 一、ZSet命令详解
-
- [1.1 ZSet的特点与底层实现](#1.1 ZSet的特点与底层实现)
- [1.2 添加与更新命令](#1.2 添加与更新命令)
- [1.3 查询命令](#1.3 查询命令)
- [1.4 范围查询命令](#1.4 范围查询命令)
- [1.5 删除命令](#1.5 删除命令)
- [1.6 集合运算命令](#1.6 集合运算命令)
- 二、ZSet应用场景
-
- [2.1 排行榜](#2.1 排行榜)
- [2.2 延时队列](#2.2 延时队列)
- [2.3 热搜榜](#2.3 热搜榜)
- [2.4 带权重的标签](#2.4 带权重的标签)
- 三、Bitmap命令详解
-
- [3.1 Bitmap的特点](#3.1 Bitmap的特点)
- [3.2 基本位操作命令](#3.2 基本位操作命令)
- [3.3 位运算命令](#3.3 位运算命令)
- 四、Bitmap应用场景
-
- [4.1 签到统计](#4.1 签到统计)
- [4.2 用户在线状态](#4.2 用户在线状态)
- [4.3 布隆过滤器](#4.3 布隆过滤器)
- [4.4 活跃用户统计](#4.4 活跃用户统计)
- 五、踩坑提醒:ZSet内存占用
-
- [5.1 ZSet内存占用分析](#5.1 ZSet内存占用分析)
- [5.2 优化建议](#5.2 优化建议)
- [5.3 监控ZSet大小](#5.3 监控ZSet大小)
- 六、ZSet与Bitmap对比
-
- [6.1 功能对比](#6.1 功能对比)
- [6.2 场景选择](#6.2 场景选择)
- 七、面试高频考点
- 八、参考资料
- 九、互动话题
一、ZSet命令详解
1.1 ZSet的特点与底层实现
实际场景:排行榜、热搜榜、延时队列,ZSet都能优雅解决。
ZSet(Sorted Set)是有序集合类型,具有以下特点:
- 有序:按分数(score)排序
- 不重复:元素唯一
- 可范围查询:支持按分数或排名范围查询
底层实现:
- 使用skiplist(跳表)+ hashtable
- 跳表保证有序和范围查询效率
- 哈希表保证O(1)查找
1.2 添加与更新命令
redis
# 添加元素
zadd leaderboard 100 "user1"
# 添加多个元素
zadd leaderboard 200 "user2" 150 "user3" 180 "user4"
# 添加元素(仅当不存在时)
zadd leaderboard nx 100 "user1"
# 添加元素(仅当存在时)
zadd leaderboard xx 300 "user1"
# 添加元素并返回新增元素数量
zadd leaderboard ch 250 "user5"
# 添加元素并比较分数大小(GT大于才更新,LT小于才更新)
zadd leaderboard gt 300 "user1"
ZADD参数说明:
| 参数 | 说明 |
|---|---|
| NX | 仅当元素不存在时添加 |
| XX | 仅当元素存在时更新 |
| CH | 返回变化的元素数量(新增+更新) |
| GT | 仅当新分数大于旧分数时更新 |
| LT | 仅当新分数小于旧分数时更新 |
1.3 查询命令
redis
# 获取元素分数
zscore leaderboard "user1"
# 获取元素排名(从0开始,升序)
zrank leaderboard "user1"
# 获取元素排名(从0开始,降序)
zrevrank leaderboard "user1"
# 获取集合大小
zcard leaderboard
# 统计分数范围内的元素数量
zcount leaderboard 100 200
# 获取分数范围内的元素
zrangebyscore leaderboard 100 200
# 获取分数范围内的元素(带分数)
zrangebyscore leaderboard 100 200 withscores
# 获取分数范围内的元素(限制数量)
zrangebyscore leaderboard 100 200 limit 0 10
1.4 范围查询命令
redis
# 按排名范围获取(升序)
zrange leaderboard 0 9
# 按排名范围获取(降序)
zrevrange leaderboard 0 9
# 带分数返回
zrange leaderboard 0 9 withscores
zrevrange leaderboard 0 9 withscores
# 按分数范围获取
zrangebyscore leaderboard 100 200
# 按分数范围获取(降序)
zrevrangebyscore leaderboard 200 100
# 按字典序范围获取(分数相同时)
zrangebylex leaderboard [a [z
注意事项:
- ZRANGE的索引从0开始,-1表示最后一个元素
- ZRANGEBYSCORE的分数范围是闭区间,使用(表示开区间
- ZRANGEBYLEX仅适用于分数相同的元素
1.5 删除命令
redis
# 删除元素
zrem leaderboard "user1"
# 删除多个元素
zrem leaderboard "user2" "user3"
# 按排名范围删除
zremrangebyrank leaderboard 0 9
# 按分数范围删除
zremrangebyscore leaderboard 0 100
# 按字典序范围删除
zremrangebylex leaderboard [a [z
1.6 集合运算命令
redis
# 准备测试数据
zadd set:a 1 "one" 2 "two" 3 "three"
zadd set:b 2 "two" 3 "three" 4 "four"
# 交集(分数相加)
zinterstore set:a_b 2 set:a set:b
# 交集(指定权重)
zinterstore set:a_b 2 set:a set:b weights 2 3
# 交集(指定聚合方式)
zinterstore set:a_b 2 set:a set:b aggregate max
# 并集
zunionstore set:a_b_union 2 set:a set:b
聚合方式说明:
| 聚合方式 | 说明 |
|---|---|
| SUM | 分数相加(默认) |
| MIN | 取最小分数 |
| MAX | 取最大分数 |
二、ZSet应用场景
2.1 排行榜
实际场景:游戏排行榜、热搜榜、积分榜,ZSet天生就是为排行榜设计的。
redis
# 更新用户分数
zadd game:leaderboard 1500 "user:1001"
zadd game:leaderboard 2000 "user:1002"
zadd game:leaderboard 1800 "user:1003"
# 获取用户分数
zscore game:leaderboard "user:1001"
# 获取用户排名(从0开始,降序)
zrevrank game:leaderboard "user:1001"
# 获取前10名(带分数)
zrevrange game:leaderboard 0 9 withscores
# 获取用户所在排名区间(前后各5名)
zrevrange game:leaderboard 5 15 withscores
# 分数增加
zincrby game:leaderboard 100 "user:1001"
排行榜完整示例:
redis
# 1. 用户获得积分
zincrby game:leaderboard 100 "user:1001"
# 2. 获取用户当前排名
zrevrank game:leaderboard "user:1001"
# 返回:2(第3名,从0开始)
# 3. 获取排行榜前10
zrevrange game:leaderboard 0 9 withscores
# 4. 获取用户前后5名的玩家
# 先获取用户排名
zrevrank game:leaderboard "user:1001"
# 假设返回5,则获取排名0-10的玩家
zrevrange game:leaderboard 0 10 withscores
# 5. 获取指定分数区间的玩家
zrangebyscore game:leaderboard 1000 2000 withscores
2.2 延时队列
经验之谈:ZSet实现延时队列简单可靠,比定时任务扫描数据库高效得多。
redis
# 添加延时任务(分数为执行时间戳)
zadd delay:queue 1700000000 "task:1001"
zadd delay:queue 1700000100 "task:1002"
zadd delay:queue 1700000200 "task:1003"
# 获取当前需要执行的任务
zrangebyscore delay:queue 0 1700000100
# 删除已执行的任务
zrem delay:queue "task:1001"
# 原子性获取并删除任务(Lua脚本)
# local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1])
# if #tasks > 0 then
# redis.call('ZREM', KEYS[1], unpack(tasks))
# end
# return tasks
延时队列实现流程:
| 步骤 | 操作 | 命令 |
|---|---|---|
| 1 | 添加任务 | ZADD delay:queue timestamp task_id |
| 2 | 轮询到期任务 | ZRANGEBYSCORE delay:queue 0 current_time |
| 3 | 执行任务 | 业务逻辑处理 |
| 4 | 删除任务 | ZREM delay:queue task_id |
2.3 热搜榜
redis
# 搜索关键词,分数+1
zincrby hot:search 1 "redis"
zincrby hot:search 1 "mysql"
zincrby hot:search 1 "redis"
# 获取热搜前10
zrevrange hot:search 0 9 withscores
# 定时清理(保留最近24小时)
zremrangebyscore hot:search 0 (now-86400)
2.4 带权重的标签
redis
# 添加标签及权重
zadd user:1001:tags 10 "redis" 8 "mysql" 5 "java"
# 获取用户标签(按权重排序)
zrevrange user:1001:tags 0 -1 withscores
# 获取高权重标签
zrangebyscore user:1001:tags 8 10
三、Bitmap命令详解
3.1 Bitmap的特点
实际场景:用1bit表示一个用户状态,1亿用户只需要12MB内存!
Bitmap不是独立的数据类型,而是基于String类型的位操作。它允许对字符串的位进行操作:
- 极致省内存:1bit存储一个布尔值
- 位操作:支持位级别的设置、获取、统计
- 位运算:支持AND、OR、XOR等运算
3.2 基本位操作命令
redis
# 设置指定位的值(用户1001在第1001位)
setbit sign:20240101 1001 1
# 获取指定位的值
getbit sign:20240101 1001
# 统计值为1的位数(签到人数)
bitcount sign:20240101
# 统计指定范围内值为1的位数
bitcount sign:20240101 0 100
# 查找第一个值为0或1的位置
bitpos sign:20240101 1
bitpos sign:20240101 0
3.3 位运算命令
redis
# 准备测试数据
setbit sign:20240101 0 1
setbit sign:20240101 1 1
setbit sign:20240102 0 1
setbit sign:20240102 2 1
# 位运算并存储结果
bitop and sign:and sign:20240101 sign:20240102
bitop or sign:or sign:20240101 sign:20240102
bitop xor sign:xor sign:20240101 sign:20240102
bitop not sign:not sign:20240101
位运算说明:
| 运算 | 说明 | 示例场景 |
|---|---|---|
| AND | 与运算 | 连续签到统计 |
| OR | 或运算 | 累计签到统计 |
| XOR | 异或运算 | 差异统计 |
| NOT | 非运算 | 取反操作 |
四、Bitmap应用场景
4.1 签到统计
实际场景:用户签到功能,Bitmap比传统方案节省99%的内存。
redis
# 用户签到(用户ID作为偏移量)
setbit sign:202401 1001 1
# 检查用户是否签到
getbit sign:202401 1001
# 统计当月签到人数
bitcount sign:202401
# 统计当月某用户签到天数
# 方法:使用独立key存储每个用户的签到记录
setbit user:1001:sign:202401 1 1 # 1号签到
setbit user:1001:sign:202401 15 1 # 15号签到
bitcount user:1001:sign:202401
# 统计连续签到的用户
bitop and sign:continuous sign:20240101 sign:20240102 sign:20240103
bitcount sign:continuous
签到方案对比:
| 方案 | 内存占用 | 查询效率 | 适用场景 |
|---|---|---|---|
| MySQL表 | 高 | 低 | 需要详细记录 |
| Set存储 | 中 | 高 | 用户量小 |
| Bitmap | 极低 | 极高 | 用户量大、简单统计 |
4.2 用户在线状态
redis
# 用户上线
setbit online:users 1001 1
# 用户下线
setbit online:users 1001 0
# 检查用户是否在线
getbit online:users 1001
# 统计在线人数
bitcount online:users
4.3 布隆过滤器
经验之谈:布隆过滤器用于判断元素"可能存在"或"一定不存在",是缓存穿透的克星。
redis
# 添加元素到布隆过滤器(需要多个哈希函数)
setbit bloom:filter hash1("item1") 1
setbit bloom:filter hash2("item1") 1
setbit bloom:filter hash3("item1") 1
# 检查元素是否存在
getbit bloom:filter hash1("item1")
getbit bloom:filter hash2("item1")
getbit bloom:filter hash3("item1")
# 如果都返回1,则"可能存在"
# 如果有任一返回0,则"一定不存在"
布隆过滤器特点:
- 可能存在误判(判断存在但实际不存在)
- 不会漏判(判断不存在则一定不存在)
- 空间效率极高
- 删除困难
4.4 活跃用户统计
redis
# 记录每日活跃用户
setbit dau:20240101 1001 1
setbit dau:20240101 1002 1
setbit dau:20240102 1001 1
setbit dau:20240102 1003 1
# 计算周活跃用户(7天内活跃过)
bitop or wau:20240101-20240107 dau:20240101 dau:20240102 dau:20240103 dau:20240104 dau:20240105 dau:20240106 dau:20240107
bitcount wau:20240101-20240107
# 计算月活跃用户
bitop or mau:202401 dau:20240101 dau:20240102 ...
bitcount mau:202401
五、踩坑提醒:ZSet内存占用
踩坑提醒:ZSet功能强大但内存占用较高,数据量大时要特别注意!
5.1 ZSet内存占用分析
ZSet的内存占用主要来自:
- 跳表节点:每个节点约32字节
- 哈希表条目:每个条目约24字节
- 元素本身:字符串或对象
估算公式:
内存占用 ≈ 元素数量 × (32 + 24 + 元素大小)
5.2 优化建议
方案1:控制元素数量
redis
# 只保留前100名
zadd leaderboard 100 "user1"
zremrangebyrank leaderboard 100 -1
方案2:使用Hash替代
redis
# 如果不需要排序功能,使用Hash
hset scores user1 100
hset scores user2 200
方案3:分片存储
redis
# 按分数范围分片
zadd leaderboard:0-1000 500 "user1"
zadd leaderboard:1001-2000 1500 "user2"
5.3 监控ZSet大小
redis
# 查看ZSet元素数量
zcard leaderboard
# 查看内存占用
memory usage leaderboard
六、ZSet与Bitmap对比
6.1 功能对比
| 对比项 | ZSet | Bitmap |
|---|---|---|
| 有序性 | 有序 | 无序 |
| 元素类型 | 字符串 | 位(0/1) |
| 范围查询 | 支持 | 不支持 |
| 内存占用 | 较高 | 极低 |
| 适用场景 | 排行榜、延时队列 | 签到、状态统计 |
6.2 场景选择
| 场景 | 推荐 | 理由 |
|---|---|---|
| 排行榜 | ZSet | 自动排序,支持范围查询 |
| 延时队列 | ZSet | 时间戳作为分数 |
| 签到统计 | Bitmap | 极省内存 |
| 在线状态 | Bitmap | 极省内存,快速统计 |
| 布隆过滤器 | Bitmap | 位运算支持 |
| 热搜榜 | ZSet | 支持分数更新和排序 |
七、面试高频考点
Q1:ZSet为什么用跳表而不是红黑树?
答案:
- 实现简单:跳表比红黑树更容易实现和调试
- 范围查询高效:跳表在找到起点后可以顺序遍历
- 内存占用:跳表和红黑树内存占用相当
- 并发友好:跳表在并发场景下更容易实现无锁操作
Q2:如何用ZSet实现延时队列?
答案:
- 将任务执行时间戳作为分数存入ZSet
- 定时任务轮询到期任务:
ZRANGEBYSCORE queue 0 current_time - 执行任务并删除:
ZREM queue task_id - 使用Lua脚本保证原子性
Q3:Bitmap的内存占用如何计算?
答案:
- 1个bit可以表示1个布尔值
- 1亿个用户只需要 100000000 / 8 = 12.5MB
- 相比Set存储(每个用户ID约10字节),节省99%内存
Q4:布隆过滤器的原理是什么?
答案:
- 使用多个哈希函数计算元素位置
- 将对应位置设为1
- 查询时检查所有位置
- 全为1则"可能存在",有0则"一定不存在"
- 存在误判率,但不会漏判
八、参考资料
九、互动话题
你在项目中用过ZSet实现排行榜或延时队列吗?或者用Bitmap做过有趣的统计功能?欢迎在评论区分享你的实战经验!
至此,Redis系列文章已全部完成。感谢你的阅读,希望这个系列对你的Redis学习有所帮助!