【Redis】有序集合与位图Day5(2026年)

写在前面

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

文章目录


一、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的内存占用主要来自:

  1. 跳表节点:每个节点约32字节
  2. 哈希表条目:每个条目约24字节
  3. 元素本身:字符串或对象

估算公式

复制代码
内存占用 ≈ 元素数量 × (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为什么用跳表而不是红黑树?

答案

  1. 实现简单:跳表比红黑树更容易实现和调试
  2. 范围查询高效:跳表在找到起点后可以顺序遍历
  3. 内存占用:跳表和红黑树内存占用相当
  4. 并发友好:跳表在并发场景下更容易实现无锁操作

Q2:如何用ZSet实现延时队列?

答案

  1. 将任务执行时间戳作为分数存入ZSet
  2. 定时任务轮询到期任务:ZRANGEBYSCORE queue 0 current_time
  3. 执行任务并删除:ZREM queue task_id
  4. 使用Lua脚本保证原子性

Q3:Bitmap的内存占用如何计算?

答案

  • 1个bit可以表示1个布尔值
  • 1亿个用户只需要 100000000 / 8 = 12.5MB
  • 相比Set存储(每个用户ID约10字节),节省99%内存

Q4:布隆过滤器的原理是什么?

答案

  1. 使用多个哈希函数计算元素位置
  2. 将对应位置设为1
  3. 查询时检查所有位置
  4. 全为1则"可能存在",有0则"一定不存在"
  5. 存在误判率,但不会漏判

八、参考资料

Redis布隆过滤器实现


九、互动话题

你在项目中用过ZSet实现排行榜或延时队列吗?或者用Bitmap做过有趣的统计功能?欢迎在评论区分享你的实战经验!

至此,Redis系列文章已全部完成。感谢你的阅读,希望这个系列对你的Redis学习有所帮助!

相关推荐
喵了几个咪1 小时前
Headless 后端实践:基于Go的企业级多栈管理系统脚手架
开发语言·vue.js·后端·golang·reactjs·gowind
枫叶丹41 小时前
【HarmonyOS 6.0】Map Kit瓦片图层深度解析:本地加载方式与瓦片数据缓存能力
开发语言·缓存·华为·harmonyos
小小龙学IT1 小时前
Go 并发模式深度解析:Fan-out/Fan-in 高效处理大规模数据流
开发语言·后端·golang
我是一颗柠檬1 小时前
【Redis】持久化机制Day6(2026年)
数据库·redis·后端·缓存·database
huangdong_1 小时前
有什么软件可以下载淘宝和天猫店铺的商品图片?——从工具推荐到技术原理的完整解答
java·前端·数据库
Penge6669 小时前
Go 接口编译期断言
后端
我是一颗柠檬9 小时前
【MySQL全面教学】MySQL面试高频考点汇总Day15(2026年)
数据库·后端·mysql·面试
拽着尾巴的鱼儿10 小时前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端
Ceelog10 小时前
久坐党自救指南:屏幕前 8 小时,身体到底在经历什么
前端·后端