一,ZSet
Redis 的 Set 数据类型是 无序,不重复,而 ZSet 是 有序,不重复的数据类型。 它在 Set 的基础上,给每个元素加了一个 score(分数) ,Redis 就根据这个分数自动排序。 分数越小 → 排名越靠前(索引越小)
ZSet 的 3 大特点
- 有序性 :按
score分数自动排序(默认从小到大) - 唯一性 :元素(member)不重复,分数可以重复
- 查询快:插入、删除、范围查询都是 O (logN) 效率
核心结构:一个 ZSet 里存的是:member(元素) + score(分数)
css
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 仅新增,不更新
- 元素不存在 → 新增
- 元素已存在 → 不做任何操作(不更新分数)
ruby
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 仅更新,不新增
- 元素已存在 → 更新分数
- 元素不存在 → 不创建
ruby
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 只负责 "只更新不新增",不负责改变返回值。 更新永远返回 0
ruby
127.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) 0
1.1.2 ZADD NX 实现 "类锁" 的两大典型场景
场景 1:防重复提交 / 防重复入队(最常用)
需求:同一个用户只能进入一次榜单 / 任务队列,不能重复添加。
arduino
# 用户 tom 尝试加入榜单,已存在则直接失败(禁止重复)
ZADD task:queue 0 tom NX
这里 ZADD NX 就起到了 "准入锁" 效果:
成功 = 准入通过
失败 = 已准入,拒绝重复操作
场景 2:分布式排队、抢名额(限流 / 名额锁)
需求:活动仅限前 100 人参与,每人只能报名一次。
bash
# 报名:用户id作为member,时间戳作为分数
ZADD activity:sign ${timestamp} ${userId} NX
# 再限制总人数
ZCARD activity:sign
NX 保证一人只能报一次名(报名锁)
ZSet 有序 + 可计数,天然适合带顺序的名额抢占
SET NX 锁 vs ZADD NX "锁" 核心对比
| 特性 | SET key NX(String 锁) | 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 只在【新分数 < 旧分数】时更新
bash
ZADD rank 100 tom
ZADD rank LT 80 tom # 80 < 100 → 更新成功
ZADD rank LT 120 tom # 120 > 100 → 不更新
含义: GT 只在【新分数 > 旧分数】时更新
bash
ZADD rank 100 tom
ZADD rank GT 120 tom # 120 > 100 → 更新成功
ZADD rank GT 80 tom # 80 < 100 → 不更新
1.5 ZADD CH 返回【变化总数】(新增 + 更新)
CH = Changed ,直译:发生变更的成员计数。
ZADD 加 CH 后返回:新增的数量 + 更新分数的数量 = 发生变化的总数
sql
ZADD key score member [score member ...]
返回值 :本次命令新成功添加的成员数量
成员原本不存在:新增 → 计数 +1
成员已存在:仅更新分数、成员不变 → 不计入返回值
ruby
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:新增 → 计入
# 返回结果:2(a + c 两个发生变更)
CH 核心价值:精准判断本次操作到底修改了多少数据 ,用于状态监控、业务统计、回调触发、数据同步。
1.2 ZRANGE 查看命令
ZRANGE 是 ZSet 有序集合 核心查询命令,作用:按索引 / 分数 / 字典序 取出集合成员,搭配多参数实现分页、倒序、按分数筛选、带分数返回等能力。
sql
ZRANGE key start stop
[BYSCORE | BYLEX] -- 按分数查 / 按字典序查(二选一)
[REV] -- 倒序
[LIMIT offset count] -- 分页
[WITHSCORES] -- 同时返回分数
ZSet 内部默认按 score 升序排列:
分数越小 → 排名越靠前(索引越小)
索引 start/stop:从 0 开始
负数索引:-1 最后一个元素,-2 倒数第二个
示例 1:查询全部成员(索引 0 ~ -1)
less
127.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(最常用)
less
127.0.0.1:6379> ZRANGE ztest 0 -1 WITHSCORES
a
10
b
20
c
30
d
40
e
50
示例 3:索引区间截取
less
127.0.0.1:6379> ZRANGE ztest 0 2 WITHSCORES
a
10
b
20
c
30
示例 4:倒序查全部 + 带分数
不加 REV:默认分数升序, 加 REV :分数降序(高分在前,排行榜常用)
注意: REV 只反转排序,不改变 start/stop 索引逻辑。
less
127.0.0.1:6379> ZRANGE ztest 0 -1 REV WITHSCORES
e
50
d
40
c
30
b
20
a
10
示例 5: 分页查询(第 1 页,每页 2 条)
less
127.0.0.1:6379> ZRANGE ztest -inf +inf BYSCORE LIMIT 0 2 WITHSCORES
a
10
b
20
ZRANGE, 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(索引 / 分数 / 字典序) | 支持 | 数据拷贝、榜单快照、临时集合生成 |
lua
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 处的有序集合中移除指定的成员。不存在的成员将被忽略。**** |
ruby
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
3
1.4 ZCARD
| 命令 | ZCARD key**** | 时间复杂度 | O(1);集合里面存储着总数的变量 |
|---|---|---|---|
| 描述 | 返回存储在 key 处的排序集的元素数量 |
ruby
127.0.0.1:6379> ZADD myzset:test 1 one 2 two 3 three
3
127.0.0.1:6379> ZCARD myzset:test
3
1.5 ZCOUNT
| 命令 | ZCOUNT key min max | 时间复杂度 | O(log(N)),其中 N 为排序集合中的元素数量 |
|---|---|---|---|
| 描述 | 统计有序集合(ZSet)中分数在 [min, max] 闭区间内的元素数量。 |
闭区间案例
ruby
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 # 返回:2
1.6 ZINTER & ZINTERSTORE 交集
Redis 中有序集合交集主要分两个命令:
ini
# 直接返回交集结果(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>]
ruby
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
2
1.7 ZDIFF & ZDIFFSTORE 差集
二者用于计算有序集合差集 ,规则:保留在第一个集合存在、但在后续集合中不存在的元素
css
# 初始化数据
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 | 数据复用、长期存储差集结果 |
| ZDIFF | 6.2.0+ | 临时查询,不存数据 | 一次性统计、临时筛选 |
1.8 ZUNION & ZUNIONSTORE 并集
vbnet
ZUNIONSTORE dest-key numkeys key [key ...] [WEIGHTS weight...] [AGGREGATE SUM|MIN|MAX]
css
# 初始化数据
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 10,b 40,c 30
# 2. 聚合规则 MIN / MAX
ZUNIONSTORE union_min 2 s1 s2 AGGREGATE MIN
ZRANGE union_min -1 -1 WITHSCORES
# a 10,b 20,c 30
ZUNIONSTORE union_max 2 s1 s2 AGGREGATE MAX
ZRANGE union_max -1 -1 WITHSCORES
# a 10,b 20,c 30
# 3. 使用权重
ZUNIONSTORE union_weight 2 s1 s2 WEIGHTS 2 1
ZRANGE union_weight -1 -1 WITHSCORES
# a 20(10*2),b 60(20*2+20),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 为用户ID,Value 为 1(签到)或 0(未签到)
优势:一个用户一年的签到记录只需要 365 bits ≈ 46 字节,极其节省空间。
ruby
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未签到,其余的都已签到
需求一:查询指定用户是否已签到
ruby
127.0.0.1:6379> GETBIT sign:20260612 45
0 # 查询 45号用户,返回 0,表示未签到
127.0.0.1:6379> GETBIT sign:20260612 10
1 # 查询 10号用户,返回 1,表示已签到
需求二:查询当天所有的签到情况
ruby
127.0.0.1:6379> BITCOUNT sign:20260612
41 # 一共有42 位用户已签到
但是位图有一个短板,就是查出用户的签到明细列表,比如导出今天签到的用户 ID 列表:0, 1, 2, 3...40"。
因为位图底层是一长串二进制(比如 1111111111...00000),Redis 没有 提供类似 SMEMBERS(获取集合所有元素)的命令来直接列出所有为 1 的偏移量。因为如果数据量是千万级,要把所有为 1 的位遍历一遍找出来,会极其消耗 CPU,甚至卡死 Redis。
首先我们要达成一个共识:位图(Bitmap)从娘胎里出来,就不是为了"查明细"设计的。 它的基因里只有两个技能:
极速的宏观统计 (今天多少人签到?用 BITCOUNT)
极速的集合运算 (连续三天签到的人有哪些?用BITOP )
如果你非要问"具体是哪几个人签到了?",这就好比你问一个秤:"请告诉我这根头发有多长"。秤只能告诉你重量(统计),不能告诉你长度(明细)。
三,HyperLogLog
HyperLogLog 是 Redis 专门用来做基数统计 的数据结构,核心作用:估算集合中不重复元素的数量(去重总数) 。
底层依旧基于 String 类型 ,单个 HLL 对象固定占用 12KB 内存。 理论可统计 上亿级唯一元素 ,内存几乎不增长。 存在标准误差 :默认误差率约 0.81% ,业务可接受 。 只存「基数估算值」,不存储原始数据,无法取出元素
适用场景: 只关心去重数量、不关心具体明细的场景:
- 页面每日独立访客 UV
- 接口独立调用用户数
- 直播间独立观看人数 (比如6月13号开播的《凡人修仙传》的在线人数的大概统计。数据成千上万之后不需要精确到个位数的计算。 )
- 统计各类唯一访问 / 参与人数
对比:Set 也能去重计数,但元素越多内存占用越大;HLL 内存恒定,海量数据首选。
3.1 PFADD:添加元素到 HLL
HyperLogLog 的命令都是以 PF 开头的(这是为了纪念它的发明者 Philippe Flajolet)。在 Redis 中,它底层依然是复用 String 类型的 12KB 空间。
ruby
# 用户 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.2 PFCOUNT:获取估算的基数
ruby
127.0.0.1:6379> PFCOUNT dau:20260612
(integer) 2 # 返回估算的独立用户数
3.3 PFMERGE:合并多个 HLL(计算周活/月活)
假设你想统计这周的周活(WAU),只需要把每天的 HLL 合并起来。
ruby
# 把周一到周日的 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 的 Geo(Geographic) 数据结构是 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. 增删改查基础命令
bash
# 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 bike1
2. 范围查询(核心)
css
# 格式: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 WITHDIST
3. 实战案例:查找附近的共享单车
业务场景:用户打开 App,系统需要获取用户当前位置,并找出附近 1 公里内的 3 辆共享单车,按距离从近到远排序。
Redis CLI 模拟
bash
# 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
返回结果:
lua
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 更远被过滤