Redis(四):Redis扩展类型Bitmap、HyperLogLog、GEO

Published on 2025-07-01 22:37 in 分类: 博客 with 狂盗一枝梅
分类: 博客

在前面第一篇文章中介绍了redis中的string、hash、list、set、zset五种基本数据类型,除了这五种基本数据类型,还有位图(Bitmap)、基数估算(HyperLogLog)、地理位置(GEO)三种扩展数据类型。

一、位图Bitmap

Bitmap并非单独的一种数据类型,它操作的实际上还是string类型,只不过我们通过set/get命令操作的是整个字符串,而bitmap则操作字符串的某个二进制位,这样在只需要存储0或者1的状态的场景中,将会大大提高操作效率并且能更节省空间(节省空间不是绝对的,得看场景)。

我们已经知道位图底层其实就是一个 Redis 字符串,而 Redis 字符串的最大存储限制是 512 MB,这换算成字节数就是 512 * 1024 * 1024 = 536870912 字节 = 2^29 字节,换算成二进制位就是 2^29 * 8 = 2^32 位,这能满足任何状态存储需求了。

bitmap操作相关的命令如下所示:

命令 作用
setbit key offset value 对指定 key 的某一二进制位进行赋值,并返回该位置上原来的二进制值,初始值为0
getbit key offset 获取指定 key 上指定位置的二进制值,若超出当前位图范围,返回0
bitcount key [start end] 获取指定 key 中被设置为 1 的数量,可以通过 start 和 end 参数来指定区间,该数值是指字节数
bitop operation destkey key [key ...] 对一个或多个保存二进制位的字符串 key 进行位运算操作,并将结果保存到 destkey 上
bitpos key bit [start] [end] 获取指定 key 中被第一个二进制位为0或1的bit位,可以通过 start 和 end 参数来指定区间,该数值是指字节数。

在bitmap的命令中,offset是偏移量,指的是第几个bit,从0开始算起;start和end则是以字节为单位。

redis的bitmap结构示意图

1、操作命令

setbit/getbit

setbit命令格式:setbit key offset value ,返回值是原bit的值。

getbit命令格式:getbit key offset

redis的setbit、getbit使用方法

bitcount

bitcount命令格式:bitcount key [start end]

bitcount命令用于获取指定 key 中被设置为 1 的数量,可以通过 start 和 end 参数来指定区间,该数值是指字节数。

redis的bitcount使用方法

bitpos

bitpos是bit postion的简写。bitpos命令格式:bitpos key bit [start] [end] ,该命令用于 获取指定 key 中被第一个二进制位为0或1的bit位,可以通过 start 和 end 参数来指定区间,该数值是指字节数。

  • key 表示键

  • bit 表示要目标二进制值是0还是1

  • start 表示指定区间的起始字节数

  • end 表示指定区间的终止字节数

redis的bitpos命令使用方法

bitop

bitop是bit operation的简写。bitop命令格式:bitop operation destkey key [key ...],该命令对一个或多个保存二进制位的字符串 key 进行位运算操作,并将结果保存到 destkey 上。

operation 可以是 ANDORNOTXOR 这四种操作中的任意一种:

  • BITOP AND destkey key [key ...] ,对一个或多个 key 求逻辑并,并将结果保存到destkey 。
  • BITOP OR destkey key [key ...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey。
  • BITOP XOR destkey key [key ...] ,对一个或多个 key 求逻辑异或,并将结果保存到destkey 。
  • BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey 。

除了 NOT 操作之外,其他操作都可以接受一个或多个 key 作为输入。

127.0.0.1:6379> setbit a 8 0
(integer) 0
127.0.0.1:6379> setbit b 8 1
(integer) 0
127.0.0.1:6379> bitop and a_b_and a b  #and操作
(integer) 2
127.0.0.1:6379> getbit a_b_and 8
(integer) 0
127.0.0.1:6379> bitop or a_b_or a b #or操作
(integer) 2
127.0.0.1:6379> getbit a_b_or 8
(integer) 1
127.0.0.1:6379> bitop xor a_b_xor a b #xor操作
(integer) 2
127.0.0.1:6379> getbit a_b_xor 8
(integer) 1
127.0.0.1:6379> bitop not a_not a #not操作
(integer) 2
127.0.0.1:6379> getbit a_not 8
(integer) 1

2、Bitmap优劣势分析

优势:

  • 极低存储成本(1bit/状态)
  • O(1)时间复杂度的查询和设置

局限性:

  • 仅支持二值状态,无法存储多值信息
  • 稀疏数据(如少数位为1)可能浪费空间,此时集合更优

让我们分析一个命令setbit asdf 10000001 1,该命令浪费了大量内存空间:

内存预分配机制:Redis的Bitmap底层是字符串,当设置一个较大的偏移量时,Redis会预分配足够的内存来覆盖从偏移量0到目标偏移量的所有位。

稀疏数据问题:如果实际只有少数位被设置为1(例如仅第10000001 位),其余位默认填充0,这些未使用的0值仍会占用内存,导致内存利用率极低

可以通过命令查看asdf占用的存户空间:

127.0.0.1:6379> setbit asdf 10000001 1
(integer) 0
127.0.0.1:6379> memory usage asdf
(integer) 1310768
127.0.0.1:6379>

1310768字节等于1310768/1024/1024约等于1.25MB大小。

如何解决这个问题呢?

1、避免大偏移量初始化连续偏移量设计,将用户ID或时间戳等标识符映射为连续整数(如从0开始),避免跳跃式偏移量。比如用户ID为150000000001时,可减去基准值(如150000000000),使偏移量变为1。

2、分区或分片:将大Bitmap拆分为多个小Bitmap。比如按天分片:SETBIT user:sign:20240630 1001 1;按用户范围分片:SETBIT segment:1 1001 1(用户ID 1-100万归到segment:1)。

3、使用其他数据结构:若数据极度稀疏(如仅有少量位为1),改用集合(Set)或哈希(Hash)可能更节省内存。

二、基数统计HyperLogLog

HyperLogLog是Redis提供的一种概率型数据结构,专门用于解决大规模数据集的基数统计(即唯一元素计数)问题。与传统方法相比,HyperLogLog能够在极低的内存消耗下(仅约12KB)提供接近线性的时间复杂度,使其成为大数据分析和实时统计领域的有力工具。

HyperLogLog具有以下几个显著特点:

  1. 极低内存消耗:无论统计多少元素,每个HyperLogLog键通常只占用约12KB内存(极端情况下最多64KB),可以统计高达2^64个元素的基数
  2. 固定误差率:标准误差率约为0.81%,对于大多数统计场景来说是可接受的
  3. 高效合并:支持多组HyperLogLog的合并去重(PFMERGE),时间复杂度为O(1),适合分布式统计
  4. 自动去重:会忽略重复元素,多次添加同一元素不会影响结果
  5. 常数时间复杂度:插入(PFADD)和查询(PFCOUNT)操作都是O(1)复杂度

HyperLogLog算法比较复杂,但是使用起来比较简单,Redis提供了三个核心命令来操作HyperLogLog:

命令 作用
pfadd key element [element ...] 将一个或多个元素添加到指定的HyperLogLog结构中。
pfcount key [key ...] 查询一个或多个HyperLogLog的基数估算值。
pfmerge destkey sourcekey [sourcekey ...] 将一个或多个源HyperLogLog合并到目标HyperLogLog中,用于合并不同数据集的基数估算

pfadd / pfcount

pfadd key element [element ...]:将一个或多个元素添加到指定的HyperLogLog结构中。如果至少有一个元素是新添加的(即影响了基数估算),则返回1,否则返回0

pfcount key [key ...]:查询一个或多个HyperLogLog的基数估算值。当传入多个key时,返回的是这些HyperLogLog并集的基数估算值

redis的pfadd、pfcount使用方法

pfmerge

pfmerge destkey sourcekey [sourcekey ...]:将一个或多个源HyperLogLog合并到目标HyperLogLog中,用于合并不同数据集的基数估算

redis的pfmerge使用方法

三、GEO操作

Redis GEO 是 Redis 3.2 版本引入的地理空间数据处理功能,基于有序集合zset(Sorted Set)实现,支持存储经纬度坐标及高效的地理位置查询。

为了更好讲解geo各种命令,我们先准备好一组数据插入redis:

地点 坐标
北京 116.41,39.90
上海 121.47,31.23
青岛 120.31,36.06
石家庄 114.51,38.04
重庆 106.55,29.56
香港 114.17,22.28
澳门 116.41,39.91
连云港 119.22,34.60
乌鲁木齐 87.62,43.83
济南 117.12,36.65
郑州 113.66,34.75
太原 112.55,37.87
南京 118.80,32.06
武汉 114.30,30.59

注意,为了防止中文乱码,要使用./redis-cli --raw命令进入控制台,批量插入的命令为:

GEOADD cities:china 116.41 39.90 北京 121.47 31.23 上海 120.31 36.06 青岛 114.51 38.04 石家庄 106.55 29.56 重庆 114.17 22.28 香港 116.41 39.91 澳门 119.22 34.60 连云港 87.62 43.83 乌鲁木齐 117.12 36.65 济南 113.66 34.75 郑州 112.55 37.87 太原 118.80 32.06 南京 114.30 30.59 武汉

1、新增GEO数据:geoadd

geo新增命令是geoadd命令,geoadd命令完整格式如下:

geoadd key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]

geoadd命令部分格式和zadd命令有些相似,zadd命令如下:

zadd key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]

两个命令中,[NX|XX]以及[CH]参数的意义是一样的。

可选参数[NX|XX]

NX:不存在则添加,否则忽略

XX:存在则更新分数,否则忽略

可选参数[CH]

CH参数的作用:返回值改为所有被修改的成员数量(包括新增和更新的成员)。zadd的默认行为是仅统计新添加的成员。

longitude latitude member

经度 维度 名字,比如 116.41 39.90 北京

举例:新增一条数据(116.41,39.9,北京),如果不存在则添加,否则忽略,返回值要统计被修改的成员数量,可以使用如下命令

127.0.0.1:6379> geoadd cities:china NX CH 116.41 39.9 北京
0
127.0.0.1:6379>

由于之前已经添加过了北京的geo数据,所以没有添加成功,返回了0。

2、查看GEO数据

查看zset数据

geoadd命令添加的数据在redis中是以zset形式存储的,geoadd 命令将经纬度坐标转换为 52 位整数作为 score 存储,同时将地点名称(如 "北京")作为 member,所以没有直接查询geo数据的命令,可以使用zset相关命令直接查询geo数据。

127.0.0.1:6379> zrange cities:china 0 -1
乌鲁木齐
重庆
香港
武汉
上海
郑州
济南
南京
连云港
青岛
太原
石家庄
北京
澳门
127.0.0.1:6379>

注意,如果出现了中文乱码,一定要在redis-cli后加上--raw参数启动redis-cli。

geopos

geopos用于查询某地的经纬度,完整命令格式如下:

geopos key member [member ...]

比如查询北京的经纬度信息:

127.0.0.1:6379> geopos cities:china 北京
116.40999823808670044
39.90000009167092543
127.0.0.1:6379>

注意这里,redis并没有存储经纬度信息,geopos命令是通过动态解码score获得经纬度信息系的,所以这里产生了精度丢失。

geohash

Redis 的 GEOHASH 命令用于获取指定地理位置的 GeoHash 编码,这是一种将二维经纬度转换为一维字符串的算法,便于高效存储和查询地理位置数据,完整命令格式如下所示:

geohash key member [member ...]

member是地理位置名称,返回一个或多个成员的 Base32 编码字符串(长度通常为 11 字符),例如 "wx4fbzx4me0"

127.0.0.1:6379> geohash cities:china 北京
wx4fbzx4me0
127.0.0.1:6379> 

3、经纬度计算

接下来说一说geo命令中的经纬度计算相关的命令。

geodist

Redis 的 geodist 命令用于计算两个地理位置之间的直线距离(基于经纬度),支持多种单位(如米、千米、英里等),是 Redis GEO 地理信息功能的核心命令之一,其完整格式如下所示:

geodist key member1 member2 [m|km|ft|mi]
  • m:米
  • km:千米
  • mi:英里
  • ft:英尺

举个例子,计算北京和上海的直线距离:

127.0.0.1:6379> geodist cities:china 北京 上海 km
1066.9923
127.0.0.1:6379>

这个数据和官方给的数据1088差不多,有差距是因为我是在高德地图上随意选点和官方的计算方式不同导致的。

georadius

georadius 是 Redis 中用于查询指定经纬度为中心、半径范围内的地理位置成员的强大命令,适用于附近地点搜索、LBS 服务等场景。georadius命令已经不再被官方推荐使用,推荐使用geosearch命令替代。georadius命令完整格式如下所示:

georadius key longitude latitude radius m|km|ft|mi 
[WITHCOORD] 
[WITHDIST] 
[WITHHASH] 
[COUNT count [ANY]] 
[ASC|DESC] 
[STORE key] 
[STOREDIST key]

必选参数

  • key:存储地理位置的 Redis 键名。
  • longitude latitude:中心点的经度和纬度。
  • radius:搜索半径,需搭配单位:
    • m:米
    • km:千米
    • ft:英尺
    • mi:英里

可选参数

可选参数 作用
WITHCOORD 在返回结果中包含匹配位置的经纬度坐标
WITHDIST 在返回结果中包含匹配位置与中心点的距离
WITHHASH 返回位置元素经过原始geohash编码的有序集合分值(52位有符号整数)
COUNT count [ANY] 限制返回结果的数量。添加ANY选项时,命令会在找到足够结果后立即返回,可能不精确但更快
ASC|DESC 结果排序方式,ASC表示从近到远,DESC表示从远到近。默认是ASC
STORE key 将返回结果的地理位置信息保存到指定key(以GeoHash值作为分数)
STOREDIST key 将返回结果离中心点的距离保存到指定key(以距离作为分数)

我查到临沂的坐标是(118.36,35.10),现在我以该坐标为中心,依次查询10km、100km、200km、300km范围内的城市:

127.0.0.1:6379> georadius cities:china 118.36 35.10 10 km

127.0.0.1:6379> georadius cities:china 118.36 35.10 100 km
连云港
127.0.0.1:6379> georadius cities:china 118.36 35.10 200 km
连云港
127.0.0.1:6379> georadius cities:china 118.36 35.10 300 km
连云港
青岛
济南
127.0.0.1:6379> 

加上可选参数重新运行命令

127.0.0.1:6379> georadius cities:china 118.36 35.10 300 km withcoord withdist withhash count 3 desc 
青岛
206.1915          		#距离
4067544670361346		#score值
120.30999988317489624 	#经度
36.05999892411877994 	#纬度
济南
205.4495
4065935147208104
117.12000042200088501
36.65000089893579371
连云港
96.2017
4067136777713544
119.21999841928482056
34.59999953633954561

georadiusbymember

georadius命令根据指定的坐标查询指定范围内的地点不同的是,georadiusbymember命令可以根据给定的成员位置来查找指定范围内的其他成员。完整的命令如下所示:

georadiusbymember key member radius m|km|ft|mi 
[WITHCOORD] 
[WITHDIST] 
[WITHHASH] 
[COUNT count [ANY]] 
[ASC|DESC] 
[STORE key] 
[STOREDIST key]

可选参数都和georadius一样,不再赘述。

127.0.0.1:6379> georadiusbymember cities:china 青岛 300 km withcoord withdist withhash desc
济南
293.1738 				#距离
4065935147208104		#score值
117.12000042200088501	#经度
36.65000089893579371	#纬度
连云港
190.1374
4067136777713544
119.21999841928482056
34.59999953633954561
青岛
0.0000
4067544670361346
120.30999988317489624
36.05999892411877994
127.0.0.1:6379> 

geosearch

geosearch 是 Redis 6.2 版本新增的地理空间查询命令,用于在指定的地理空间索引中搜索符合条件的成员。该命令提供了比旧版 georadiusgeoradiusbymember 更强大和灵活的查询功能,支持圆形和矩形两种搜索范围,并且同时支持基于指定坐标以及指定地点的坐标的查询。完整的命令格式如下所示:

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]

虽然大多数的参数都是可选参数,但是实际上只是使用”geosearch key“命令直接查询会报错。可选参数大多数都必须"二选一"。

可选参数 作用
FROMMEMBER member 使用已存在于地理空间索引中的成员作为中心点
FROMLONLAT longitude latitude 直接指定经纬度作为中心点
BYRADIUS radius m|km|ft|mi 圆形范围查询,指定半径和单位
BYBOX width height m|km|ft|mi 矩形范围查询,指定宽度、高度和单位
ASC|DESC 结果排序 ASC:按距离从近到远排序;DESC:按距离从远到近排序
COUNT count [ANY] 限制返回结果的数量,Any与COUNT配合使用,表示只要找到足够数量的匹配项就立即返回,不保证是最接近的结果,但能提高查询效率
WITHCOORD 返回匹配项的经度和纬度
WITHDIST 返回匹配项到指定中心点的距离
WITHHASH 以52位无符号整数的形式返回原始geohash编码(主要用于调试)

可以看到geosearch是个大而全的命令,功能上基本上等于georadius+georadiusbymember+矩形范围查询

还是以临沂市(118.36,35.10)为中心查询的例子,现在我以该坐标为中心,依次查询10km、100km、200km、300km范围内的城市:

127.0.0.1:6379> geosearch cities:china fromlonlat 118.36 35.10 byradius 10 km desc withcoord withdist withhash

127.0.0.1:6379> geosearch cities:china fromlonlat 118.36 35.10 byradius 100 km desc withcoord withdist withhash
连云港
96.2017
4067136777713544
119.21999841928482056
34.59999953633954561
127.0.0.1:6379> 
127.0.0.1:6379> geosearch cities:china fromlonlat 118.36 35.10 byradius 200 km desc withcoord withdist withhash
连云港
96.2017
4067136777713544
119.21999841928482056
34.59999953633954561
127.0.0.1:6379> geosearch cities:china fromlonlat 118.36 35.10 byradius 300 km desc withcoord withdist withhash
青岛
206.1915				#距离
4067544670361346		#score值
120.30999988317489624	#横坐标
36.05999892411877994	#纵坐标
济南
205.4495
4065935147208104
117.12000042200088501
36.65000089893579371
连云港
96.2017
4067136777713544
119.21999841928482056
34.59999953633954561
127.0.0.1:6379> 

geosearchstore

geosearchstore 是 Redis 6.2 版本引入的地理空间命令,它结合了搜索和存储功能,允许用户执行地理空间搜索并将结果存储到一个新的键中。这个命令是 geosearch 命令的变体,主要区别在于它会将搜索结果保存到指定的目标键中。该命令的完整格式如下所示:

geosearchstore destination source 
[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] 
[STOREDIST]

可以看到geosearchstore命令的格式和geosearch的命令如出一辙。

接下来我将查询临沂市方圆300km范围的城市并将其保存到新的key new_cities。

127.0.0.1:6379> geosearchstore new_cities cities:china fromlonlat 118.36 35.10 byradius 300 km desc
3
127.0.0.1:6379> zrange new_cities 0 -1 withscores
济南
4065935147208104
连云港
4067136777713544
青岛
4067544670361346
127.0.0.1:6379> geopos new_cities 济南
117.12000042200088501
36.65000089893579371

注意,该命令在实践中发现并不能使用[WITHCOORD][WITHDIST][WITHHASH] 三个可选参数,使用之后会报错。

另外,STOREDIST可选参数的意思是将结果存储为 destination 键的 zset,并将距离作为分值(Score)。若不指定,默认存储 GeoHash 编码。

georadius_ro

georadius命令的只读版本,完整命令如下:

georadius_ro key arg arg arg arg ...options...

实际上可以等同于georadius命令删除store参数之后的版本:

georadius key longitude latitude radius m|km|ft|mi 
[WITHCOORD] 
[WITHDIST] 
[WITHHASH] 
[COUNT count [ANY]] 
[ASC|DESC] 

但是因为没有了写操作,所以更安全。

127.0.0.1:6379> georadius_ro cities:china 118.36 35.10 300 km
连云港
青岛
济南
127.0.0.1:6379>

georadiusbymember_ro

georadiusbymember命令的只读版本,完整命令如下:

georadiusbymember_ro key arg arg arg ...options...

实际上等同于georadiusbymember命令删除store参数之后的版本:

georadiusbymember key member radius m|km|ft|mi 
[WITHCOORD] 
[WITHDIST] 
[WITHHASH] 
[COUNT count [ANY]] 
[ASC|DESC] 

但是没有了写操作,所以更安全。

127.0.0.1:6379> georadiusbymember_ro cities:china 青岛 300 km desc
济南
连云港
青岛
127.0.0.1:6379> 


END.


#redis
复制 复制成功