Redis 数据结构 ZSet, BIT, HyperLogLog,Geo 空间数据

📅 2026/6/30 6:34:56
Redis 数据结构 ZSet, BIT, HyperLogLog,Geo 空间数据
一ZSetRedis 的 Set 数据类型是 无序不重复而 ZSet 是 有序不重复的数据类型。 它在 Set 的基础上给每个元素加了一个score分数Redis 就根据这个分数自动排序。 分数越小 → 排名越靠前索引越小ZSet 的 3 大特点有序性按score分数自动排序默认从小到大唯一性元素member不重复分数可以重复查询快插入、删除、范围查询都是 O (logN) 效率核心结构一个 ZSet 里存的是member元素 score分数zkey: { apple: 10, banana: 5, orange: 15 }Redis 会按分数排序banana(5) → apple(10) → orange(15)1.1 ZADD 简介命令ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]时间复杂度每添加一个元素所需的时间复杂度为 O(log(N))其中 N 为该有序集合中元素的数量。描述Sorted Set Add →往有序集合ZSet里添加元素给 ZSet 添加一个 / 多个【带分数的元素】如果元素已存在就更新它的分数【参数讲解】NX仅添加新元素不要更新已有的元素。XX仅更新已存在的元素不要添加新元素。LT只有当新得分低于当前得分时才更新现有元素。此选项并不会阻止新元素的添加。GT只有当新得分高于当前得分时才更新现有元素。此选项并不会阻止新元素的添加。CH将返回值从“新增元素的数量”改为“所有发生变动的元素的总数”。其中“Changed”即表示“发生变动的元素”。所谓“发生变动的元素”既包括新添加的元素也包括那些分数被更新的现有元素。需要注意的是命令行中那些分数与之前相同的元素不会被计入统计范围。注意通常情况下ZADD的返回值仅表示新增元素的数量。INCR当选择此选项时ZADD的行为与ZINCRBY相同。在这种模式下只能指定一对得分元素。1.1.1 ZADD NX/XX 判断key是否更新写入数据含义 NX 仅新增不更新元素不存在→ 新增元素已存在→不做任何操作不更新分数127.0.0.1:6379 ZADD rank NX 200 tom # tom 已存在 → 不更新返回 0 (integer) 0 127.0.0.1:6379 ZADD rank NX 99999 God # God 不存在 → 新增返回 1 (integer) 1含义 XX 仅更新不新增元素已存在→ 更新分数元素不存在→不创建127.0.0.1:6379 ZADD rank NX 200 tom # tom 已存在 → 不更新返回 0 (integer) 0 127.0.0.1:6379 ZADD rank NX 99999 God # God 不存在 → 新增返回 1 (integer) 1XX 只负责 “只更新不新增”不负责改变返回值。 更新永远返回 0127.0.0.1:6379 ZRANGE rank 0 -1 1) jack 2) luxi 3) tom 4) God 127.0.0.1:6379 ZADD rank XX 600 tom (integer) 0 127.0.0.1:6379 ZADD rank XX 700 one (integer) 01.1.2 ZADD NX 实现 “类锁” 的两大典型场景场景 1防重复提交 / 防重复入队最常用需求同一个用户只能进入一次榜单 / 任务队列不能重复添加。# 用户 tom 尝试加入榜单已存在则直接失败禁止重复 ZADD task:queue 0 tom NX这里ZADD NX就起到了“准入锁”效果成功 准入通过失败 已准入拒绝重复操作场景 2分布式排队、抢名额限流 / 名额锁需求活动仅限前 100 人参与每人只能报名一次。# 报名用户id作为member时间戳作为分数 ZADD activity:sign ${timestamp} ${userId} NX # 再限制总人数 ZCARD activity:signNX保证一人只能报一次名报名锁ZSet 有序 可计数天然适合带顺序的名额抢占SET NX 锁 vs ZADD NX “锁” 核心对比特性SET key NXString 锁ZSet 成员锁锁定粒度整个 Key独立互斥锁ZSet 内单个 member集合内元素互斥并发模型一对一互斥抢同一把锁多元素共存单个元素防重复典型用途全局分布式锁、接口防重、任务互斥榜单去重、报名防重、队列去重、有序抢占过期支持EX/EXAT自动过期ZSet不支持单 member 过期需额外清理XX 参数简单补充更新专用XX仅键 / 成员存在时才更新一般不做锁只做安全更新SET key newVal XX只更新已存在的 key不新建防止误创建脏数据。ZADD key newScore member XX只修改已有 member 的分数不新增成员常用于动态更新排名分数。1.4 ZADD LT / GT判断分数范围是否更新写入数据含义 LT 只在【新分数 旧分数】时更新ZADD rank 100 tom ZADD rank LT 80 tom # 80 100 → 更新成功 ZADD rank LT 120 tom # 120 100 → 不更新含义 GT 只在【新分数 旧分数】时更新ZADD rank 100 tom ZADD rank GT 120 tom # 120 100 → 更新成功 ZADD rank GT 80 tom # 80 100 → 不更新1.5 ZADD CH 返回【变化总数】新增 更新CHChanged直译发生变更的成员计数。ZADD 加 CH 后返回新增的数量 更新分数的数量 发生变化的总数ZADD key score member [score member ...]返回值本次命令新成功添加的成员数量成员原本不存在新增 → 计数 1成员已存在仅更新分数、成员不变 → 不计入返回值127.0.0.1:6379 ZADD test:zset 10 a 20 b 2 127.0.0.1:6379 ZADD test:zset CH 15 a 20 b 30 c 2 # a分数变更 → 计入 # b分数不变 → 不计入 # c新增 → 计入 # 返回结果2a c 两个发生变更CH核心价值精准判断本次操作到底修改了多少数据用于状态监控、业务统计、回调触发、数据同步。1.2 ZRANGE 查看命令ZRANGE是ZSet 有序集合核心查询命令作用按索引 / 分数 / 字典序 取出集合成员搭配多参数实现分页、倒序、按分数筛选、带分数返回等能力。ZRANGE key start stop [BYSCORE | BYLEX] -- 按分数查 / 按字典序查二选一 [REV] -- 倒序 [LIMIT offset count] -- 分页 [WITHSCORES] -- 同时返回分数ZSet 内部默认按 score 升序排列分数越小 → 排名越靠前索引越小索引start/stop从 0 开始负数索引-1最后一个元素-2倒数第二个示例 1查询全部成员索引 0 ~ -1127.0.0.1:6379 ZADD ztest 10 a 20 b 30 c 40 d 50 e 5 127.0.0.1:6379 ZRANGE ztest 0 -1 a b c d e示例 2带分数返回WITHSCORES最常用127.0.0.1:6379 ZRANGE ztest 0 -1 WITHSCORES a 10 b 20 c 30 d 40 e 50示例 3索引区间截取127.0.0.1:6379 ZRANGE ztest 0 2 WITHSCORES a 10 b 20 c 30示例 4倒序查全部 带分数不加REV默认分数升序 加REV分数降序高分在前排行榜常用注意REV只反转排序不改变 start/stop 索引逻辑。127.0.0.1:6379 ZRANGE ztest 0 -1 REV WITHSCORES e 50 d 40 c 30 b 20 a 10示例 5 分页查询第 1 页每页 2 条127.0.0.1:6379 ZRANGE ztest -inf inf BYSCORE LIMIT 0 2 WITHSCORES a 10 b 20ZRANGE, ZRANGEBYSCORE, ZRANGEBYLEX, ZRANGESTORE 之间的区别ZRANGE是统一入口Redis 6.2 整合通过后缀参数切换能力但不是能完全替代所有旧命令语法、适用场景仍有区分ZRANGEBYSCORE/ZRANGEBYLEX是传统独立命令功能被ZRANGE BYSCORE/BYLEX兼容ZRANGESTORE是结果写入新集合纯存储用途查询命令无法替代。命令核心作用检索维度能否用 LIMIT主要用途ZRANGE通用查询索引默认、分数、字典序仅搭配BYSCORE/BYLEX基础查索引、排行榜 TopN、简单分页ZRANGEBYSCORE按分数区间查询分数原生支持 LIMIT延时队列、分数筛选、分数分页旧版标准写法ZRANGEBYLEX按成员字典序查询成员字符串原生支持 LIMIT同分数下按字符串过滤、分组筛选ZRANGESTORE查询结果存入新 ZSet同 ZRANGE索引 / 分数 / 字典序支持数据拷贝、榜单快照、临时集合生成1. 按索引查询 ZRANGE key s e -- 独立功能无等价旧命令 2. 按分数查询 ZRANGE key min max BYSCORE ↔ ZRANGEBYSCORE key min max 3. 按字典序查询 ZRANGE key min max BYLEX ↔ ZRANGEBYLEX key min max 4. 结果存入新集合 ZRANGESTORE -- 独有功能任何查询命令都替代不了1.3 ZREM 删除命令命令ZREM key member [member ...]时间复杂度O(M*log(N))其中 N 是排序后集合中的元素数量M 是需要被移除的元素数量。描述从存储在key处的有序集合中移除指定的成员。不存在的成员将被忽略。127.0.0.1:6379 ZADD myzset 1 one 1 127.0.0.1:6379 ZADD myzset 2 two 1 127.0.0.1:6379 ZADD myzset 3 three 1 # 删除 two 成员 127.0.0.1:6379 ZREM myzset two 1 # 查看 127.0.0.1:6379 ZRANGE myzset 0 -1 WITHSCORES one 1 three 31.4 ZCARD命令ZCARD key时间复杂度O(1)集合里面存储着总数的变量描述返回存储在key处的排序集的元素数量127.0.0.1:6379 ZADD myzset:test 1 one 2 two 3 three 3 127.0.0.1:6379 ZCARD myzset:test 31.5 ZCOUNT命令ZCOUNT key min max时间复杂度O(log(N))其中 N 为排序集合中的元素数量描述统计有序集合ZSet中分数在[min, max]闭区间内的元素数量。闭区间案例127.0.0.1:6379 ZADD score 10 a 20 b 30 c 40 d 4 # 闭区间-统计分数 [20,30] 区间数量 127.0.0.1:6379 ZCOUNT score 20 30 2 # 返回2 b、c # 开区间-统计分数(20, 40) 127.0.0.1:6379 ZCOUNT score (20 (40 1 # 返回1 c # 无穷范围 127.0.0.1:6379 ZCOUNT score -inf inf 4 # 分数 30 127.0.0.1:6379 ZCOUNT score -inf (30 2 # 返回21.6 ZINTER ZINTERSTORE 交集Redis 中有序集合交集主要分两个命令# 直接返回交集结果Redis 6.2.0 新增 ZINTER numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM | MIN | MAX | COUNT] [WITHSCORES] # 把交集结果存入新 key经典常用 ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM | MIN | MAX | COUNT]127.0.0.1:6379 ZADD s1 10 a 20 b 30 c 3 127.0.0.1:6379 ZADD s2 20 b 30 c 40 d 3 # 2个集合求交集结果存入 inter_res 127.0.0.1:6379 ZINTERSTORE inter_res 2 s1 s2 21.7 ZDIFF ZDIFFSTORE 差集二者用于计算有序集合差集规则保留在第一个集合存在、但在后续集合中不存在的元素# 初始化数据 ZADD A 10 a 20 b 30 c 40 d ZADD B 20 b 50 e ZADD C 30 c 60 f # 求 A - B结果存入 diff1 ZDIFFSTORE diff1 2 A B ZRANGE diff1 -1 -1 WITHSCORES # 结果a(10)、c(30)、d(40) # 求 A - B - C结果存入 diff2 ZDIFFSTORE diff2 3 A B C ZRANGE diff2 -1 -1 WITHSCORES # 结果a(10)、d(40)命令版本特点适用场景ZDIFFSTORE全版本结果持久化到新 key数据复用、长期存储差集结果ZDIFF6.2.0临时查询不存数据一次性统计、临时筛选1.8 ZUNION ZUNIONSTORE 并集ZUNIONSTORE dest-key numkeys key [key ...] [WEIGHTS weight...] [AGGREGATE SUM|MIN|MAX]# 初始化数据 ZADD s1 10 a 20 b ZADD s2 20 b 30 c # 1. 基础并集默认 SUM ZUNIONSTORE union1 2 s1 s2 ZRANGE union1 -1 -1 WITHSCORES # 结果a 10b 40c 30 # 2. 聚合规则 MIN / MAX ZUNIONSTORE union_min 2 s1 s2 AGGREGATE MIN ZRANGE union_min -1 -1 WITHSCORES # a 10b 20c 30 ZUNIONSTORE union_max 2 s1 s2 AGGREGATE MAX ZRANGE union_max -1 -1 WITHSCORES # a 10b 20c 30 # 3. 使用权重 ZUNIONSTORE union_weight 2 s1 s2 WEIGHTS 2 1 ZRANGE union_weight -1 -1 WITHSCORES # a 20(10*2)b 60(20*220)c 30二位操作Redis 的位图Bitmap并不是一种独立的数据结构而是基于String字符串类型提供的一组按位bit操作的命令。它将字符串视为一个二进制位数组允许你对字符串中的特定位进行设置、获取和统计。命令SETBIT key offset value时间复杂度O(1)描述设置指定 key 的字符串在offset偏移量处的 bit 值。由于Redis 位图本质是 String 类型底层用二进制位 (bit)存储数据最小操作单位是 1 个 bit。 单个字符串最大 512MB对应4294967296 个 bit可实现海量二值状态存储极度省内存。适用场景只有 0/1 两种状态的数据。 1 个字节byte等于 8 个位bit位图在处理大量布尔值0 或 1如“是/否”、“在线/离线”、“签到/未签到”时能极大地节省内存空间并提供极高效的聚合计算能力。场景用户签到系统设计Key 为sign:{year}{month}{day}Offset 为用户IDValue 为 1签到或 0未签到优势一个用户一年的签到记录只需要 365 bits ≈ 46 字节极其节省空间。127.0.0.1:6379 SETBIT sign:20260612 0 1 # 6月12号0号用户 已签到 0 127.0.0.1:6379 SETBIT sign:20260612 1 1 # 6月12号1号用户 已签到 0 127.0.0.1:6379 SETBIT sign:20260612 2 1 # 6月12号2号用户 已签到 0 127.0.0.1:6379 SETBIT sign:20260612 3 1 # 6月12号3号用户 已签到 0 127.0.0.1:6379 SETBIT sign:20260612 4 1 # 6月12号4号用户 已签到 ...... 127.0.0.1:6379 SETBIT sign:20260612 41 0 0 127.0.0.1:6379 SETBIT sign:20260612 42 0 0 127.0.0.1:6379 SETBIT sign:20260612 43 0 0 127.0.0.1:6379 SETBIT sign:20260612 44 0 0 127.0.0.1:6379 SETBIT sign:20260612 45 0 # 6月12号45号用户 未签到 0这边小编 从0到45号用户其中只有41到45未签到其余的都已签到需求一查询指定用户是否已签到127.0.0.1:6379 GETBIT sign:20260612 45 0 # 查询 45号用户返回 0表示未签到 127.0.0.1:6379 GETBIT sign:20260612 10 1 # 查询 10号用户返回 1表示已签到需求二查询当天所有的签到情况127.0.0.1:6379 BITCOUNT sign:20260612 41 # 一共有42 位用户已签到但是位图有一个短板就是查出用户的签到明细列表比如导出今天签到的用户 ID 列表0, 1, 2, 3...40”。因为位图底层是一长串二进制比如1111111111...00000Redis没有提供类似SMEMBERS获取集合所有元素的命令来直接列出所有为 1 的偏移量。因为如果数据量是千万级要把所有为 1 的位遍历一遍找出来会极其消耗 CPU甚至卡死 Redis。首先我们要达成一个共识位图Bitmap从娘胎里出来就不是为了“查明细”设计的。它的基因里只有两个技能极速的宏观统计今天多少人签到用BITCOUNT极速的集合运算连续三天签到的人有哪些用BITOP如果你非要问“具体是哪几个人签到了”这就好比你问一个秤“请告诉我这根头发有多长”。秤只能告诉你重量统计不能告诉你长度明细。三HyperLogLogHyperLogLog 是 Redis 专门用来做基数统计的数据结构核心作用估算集合中不重复元素的数量去重总数。底层依旧基于String 类型单个 HLL 对象固定占用12KB 内存。理论可统计上亿级唯一元素内存几乎不增长。 存在标准误差默认误差率约0.81%业务可接受 。 只存「基数估算值」不存储原始数据无法取出元素适用场景只关心去重数量、不关心具体明细的场景页面每日独立访客 UV接口独立调用用户数直播间独立观看人数 比如6月13号开播的《凡人修仙传》的在线人数的大概统计。数据成千上万之后不需要精确到个位数的计算。统计各类唯一访问 / 参与人数对比Set 也能去重计数但元素越多内存占用越大HLL 内存恒定海量数据首选。3.1PFADD添加元素到 HLLHyperLogLog 的命令都是以PF开头的这是为了纪念它的发明者 Philippe Flajolet。在 Redis 中它底层依然是复用 String 类型的 12KB 空间。# 用户 1001 打开了 App 127.0.0.1:6379 PFADD dau:20260612 user_1001 (integer) 1 # 返回 1 表示内部估计的基数可能发生了变化 # 用户 1002 打开了 App 127.0.0.1:6379 PFADD dau:20260612 user_1002 (integer) 1 # 用户 1001 又打开了 App重复添加 127.0.0.1:6379 PFADD dau:20260612 user_1001 (integer) 0 # 返回 0 表示内部估计的基数没变自动去重了注HLL 接受字符串作为元素所以不管你的用户 ID 是雪花算法的长数字还是 UUID直接往里塞就行不需要像 Bitmap 那样非得转成连续自增数字3.2PFCOUNT获取估算的基数127.0.0.1:6379 PFCOUNT dau:20260612 (integer) 2 # 返回估算的独立用户数3.3PFMERGE合并多个 HLL计算周活/月活假设你想统计这周的周活WAU只需要把每天的 HLL 合并起来。# 把周一到周日的 HLL 合并到 wau:20260612 这个 key 中 127.0.0.1:6379 PFMERGE wau:20260612 dau:20260606 dau:20260607 ... dau:20260612 OK # 查看本周的总独立访客数 127.0.0.1:6379 PFCOUNT wau:20260612 (integer) 154000 # 自动去重后的周活估算值四Geo 空间数据Redis 的GeoGeographic数据结构是 Redis 3.2 版本引入的专门用于存储地理位置信息以及基于地理位置进行计算和查询如附近的人、共享单车查找、外卖骑手距离计算等Redis 并没有为 Geo 发明一种全新的底层数据结构而是巧妙地复用了 ZSet有序集合并结合了GeoHash 算法。为什么用 ZSet在 ZSet 中member存储位置名称如“张三”、“单车A”score存储该位置的 GeoHash 值。因为 ZSet 本身是有序的所以可以非常方便地按距离进行范围查询和排序。什么是 GeoHash 算法核心思想将二维的经纬度转换为一维的整数或字符串。实现方式把地球的经纬度范围进行二分法切割形成一个个网格。经度和纬度分别编码后再按位交叉合并。神奇特性前缀越相似的 GeoHash 值代表两个位置在物理上越接近。Redis 底层使用 52 位的整数来存储 GeoHash精度可以达到0.6米左右。Redis 的 Geo 数据结构仅仅只支持二维平面上的“点Point”数据它不支持“面Polygon/Area”也不支持“三维空间3D/包含海拔高度”。⚠️注意在 Redis 6.2 之前常用的是GEORADIUS和GEORADIUSBYMEMBER。但由于这两个命令在执行时会阻塞主线程存在性能和安全问题Redis 6.2 引入了全新的GEOSEARCH和GEOSEARCHSTORE命令来替代它们。以下以新版命令为主。1. 增删改查基础命令# 1. 添加地理位置 (经度在前纬度在后) # 格式GEOADD key 经度 纬度 成员名 GEOADD bikes:parking 116.404 39.915 bike1 116.405 39.916 bike2 # 2. 获取位置的经纬度 GEOPOS bikes:parking bike1 # 3. 计算两点之间的距离 (支持 m, km, mi, ft) GEODIST bikes:parking bike1 bike2 km # 4. 获取位置的 GeoHash 字符串 (常用于前端展示或缓存) GEOHASH bikes:parking bike1 # 5. 删除位置 (其实就是 ZSet 的 ZREM) ZREM bikes:parking bike12. 范围查询核心# 格式GEOSEARCH key [FROMMEMBER 成员名 | FROMLONLAT 经度 纬度] # [BYRADIUS 半径 m|km | BYBOX 宽 高 m|km] # [ASC|DESC] [COUNT 数量] [WITHCOORD] [WITHDIST] [WITHHASH] # 案例查找经度 116.403纬度 39.914 附近 3公里 内的单车按距离升序最多返回5个并返回距离和坐标 GEOSEARCH bikes:parking FROMLONLAT 116.403 39.914 BYRADIUS 3 km ASC COUNT 5 WITHCOORD WITHDIST3. 实战案例查找附近的共享单车业务场景用户打开 App系统需要获取用户当前位置并找出附近 1 公里内的 3 辆共享单车按距离从近到远排序。Redis CLI 模拟# 1. 运营人员录入几辆单车的位置 (北京国贸附近) GEOADD shared_bikes 116.4610 39.9075 bike_001 GEOADD shared_bikes 116.4625 39.9080 bike_002 GEOADD shared_bikes 116.4650 39.9100 bike_003 GEOADD shared_bikes 116.4800 39.9200 bike_004 # 这辆比较远 # 2. 用户当前定位在 (116.4615, 39.9078)查找 1km 内的单车 GEOSEARCH shared_bikes FROMLONLAT 116.4615 39.9078 BYRADIUS 1 km ASC COUNT 3 WITHDIST返回结果1) 1) bike_002 2) 0.0432 -- 距离 0.0432 km 2) 1) bike_001 2) 0.0556 -- 距离 0.0556 km # bike_003 距离超过 1km 被过滤bike_004 更远被过滤