HyperLogLog
-
在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站
PV
(PageView页面访问量
),可以使用Redis的incr、incrby轻松实现。
-
但像
UV
(UniqueVisitor,独立访客
)、独立IP数
、搜索记录数
等需要去重计数
的问题如何解决?这种求集合中不重复元素个数
的问题
称为基数问题
。 -
解决基数问题
有很多种方案:- 数据存储在
MySQL
表中,使用distinct count
计算不重复个数 - 使用
Redis
提供的hash、set、bitmaps
等数据结构来处理
- 数据存储在
-
以上的方案结果精确,但随着数据不断增加,导致
占用空间
越来越大,对于非常大的数据集是不切实际的。
-
为了能够降低一定的精度来平衡存储空间,
Redis
推出了HyperLogLog
。 -
HyperLogLog
是用来做基数统计的算法
,HyperLogLog
的优点
是:在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。 -
在
Redis
里面,每个HyperLogLog
键只需要花费12 KB
内存,就可以计算接近2^64
个不同元素的基数
。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。 -
但是
,因为HyperLogLog
只会根据输入元素来计算基数,而不会储存输入元素本身
,所以HyperLogLog
不能像集合那样,返回输入的各个元素。
什么是基数?
- 比如
数据集 {1, 3, 5, 7, 5, 7, 8}
,那么这个数据集
的基数集为 {1, 3, 5 ,7, 8}
,基数(不重复元素)为5
。 基数估计就是在误差可接受的范围内,快速计算基数。
HyperLogLog
介绍
在Redis
中,HyperLogLog
(简称HLL
)是一个用于统计大量数据中不同元素(基数)数量的概率性数据结构。以下是关于Redis
中HyperLogLog
的详细解释:
基本原理
- 哈希函数:
HyperLogLog
使用一个强散列函数
将输入的元素映射为固定长度的二进制串。 - 位前导零统计:对每个元素经过哈希后的二进制串,统计从最高位开始连续零的个数(即前导零个数)。这个值反映了元素哈希值的稀有程度,间接表示了元素的独特性。
- 存储与计数:
Redis
中的HyperLogLog
结构内部维护了一个大小固定的桶数组,默认大小为2^14 = 16384
个桶。每个桶用于存储对应的元素哈希值所观察到的最大前导零个数。当添加新的元素时,它会被哈希并找到对应的桶来更新该桶中的最大前导零计数值。 - 基数估算:利用统计的所有桶中最长的前导零序列,通过预定义的公式计算出一个近似的基数(唯一元素数量),这个估算值通常会非常接近实际基数,但不是精确值。
标准误差大约是0.81%
,这意味着对于大量数据,HyperLogLog
能够以相对较小的误差估计基数。
主要特点
- 高效的内存使用:即使可以处理多达
2^64(约18亿)
个不同的元素,Redis
中单个HyperLogLog
键只需要大约12KB
的固定内存空间。 - 概率估计:
HyperLogLog
提供的结果是概率性
的,而不是精确的基数计数
。但由于其误差范围较小,通常在实际应用中这个误差是可接受的。 - 高速计算:
HyperLogLog
可以在常量时间内计算估计的基数,无论集合的大小如何。
应用场景
- 网站UV统计:用于统计网站的独立访客数、独立IP数等,避免使用传统的去重方法会消耗大量的内存和时间。
- 数据流量分析:对数据流量中的独立元素进行统计,例如分析用户在某个时间段内访问的不同页面数、点击不同广告的用户数等。
- 数据去重:对重复数据进行去重,节省存储空间。
- 数据分布估计:估计数据集的分布情况,例如估计某个关键词在搜索引擎中的热度。
合并操作
HyperLogLog
还支持多个集合的合并操作
(pfmerge
命令),允许将多个HyperLogLog键
合并成一个新的键
,同时正确估算所有源键包含的唯一元素总数,这对于分布式环境下的基数统计尤为有用。
相关命令
命令语法 | 描述 |
---|---|
pfadd key element1 [element2...] | 添加指定元素 到 HyperLogLog 中 |
pfcount key1 [key2...] | 返回给定HyperLogLog 的基数估算值 |
pfmerge destkey sourcekey1 [sourcekey2...] | 将多个HyperLogLog 合并为一个 HyperLogLog |
pfadd key element1 [element2...]
PFADD
是Redis
中的一个命令,它用于将一个或多个元素添加到指定的 HyperLogLog 集合中
。如果HyperLogLog 集合
不存在,PFADD
会创建一个新的集合
。
命令的语法如下:
PFADD key element1 [element2 ...]
key
是你希望操作的HyperLogLog
集合的键名
。element1 [element2 ...]
是你想要添加到集合中的一个或多个元素。
当 PFADD
命令执行后,它会更新 HyperLogLog
集合以包含所有指定的元素,并返回 1
(如果至少有一个元素被添加到集合中)或 0
(如果所有元素都已经在集合中)。
这里有一个简单的例子:
# 假设我们有一个空的 HyperLogLog 集合名为 "myHyperLogLog"
# 使用 PFADD 命令添加一些元素
redis> PFADD myHyperLogLog user1 user2 user3
(integer) 1
# 尝试再次添加相同的元素,但 PFADD 不会返回错误,它只返回是否至少添加了一个新元素
redis> PFADD myHyperLogLog user1 user4
(integer) 1
# 使用 PFCOUNT 命令查看集合中的唯一元素数量(这是一个估计值)
redis> PFCOUNT myHyperLogLog
(integer) 4
# 注意:PFCOUNT 返回的是估计值,而不是精确值
请注意,尽管 HyperLogLog
可以为大量数据提供非常准确的基数估计,但它仍然是一个概率数据结构,因此返回的计数是一个近似值
,而不是精确值
。然而,这个误差范围通常很小,足以满足大多数应用的需求。
pfcount key1 [key2...]
PFCOUNT
是Redis
中的一个命令,用于获取一个或多个HyperLogLog
集合的基数估计值(即集合中不同元素的近似数量)。如果指定了多个键
,PFCOUNT
会将它们视为一个单独
的HyperLogLog
集合,并返回这些集合的并集
的基数估计值。
命令的语法如下:
PFCOUNT key1 [key2 ...]
key1 [key2 ...]
是你想要获取基数估计值的HyperLogLog
集合的键名。
当 PFCOUNT
命令执行后,它会返回一个整数,表示指定 HyperLogLog
集合(或集合的并集)中不同元素的近似数量。
这里有一个简单的例子:
# 假设我们有两个 HyperLogLog 集合,分别名为 "users:set1" 和 "users:set2"
# 使用 PFADD 命令向 "users:set1" 添加一些元素
redis> PFADD users:set1 user1 user2 user3
(integer) 1
# 使用 PFADD 命令向 "users:set2" 添加一些元素,包括与 "users:set1" 相同的元素和新的元素
redis> PFADD users:set2 user2 user3 user4 user5
(integer) 1
# 使用 PFCOUNT 命令分别获取两个集合的基数估计值
redis> PFCOUNT users:set1
(integer) 3
redis> PFCOUNT users:set2
(integer) 4
# 使用 PFCOUNT 命令获取两个集合并集的基数估计值
redis> PFCOUNT users:set1 users:set2
(integer) 5
# 注意:PFCOUNT 返回的是估计值,而不是精确值
在上面的例子中,虽然 “users:set1
” 和 “users:set2
” 分别有 3
个和4
个不同的元素,但它们的并集只有 5
个不同的元素(因为 “user2
” 和 “user3
” 在两个集合中都出现了)。PFCOUNT
命令能够正确地返回这个并集的基数估计值。
pfmerge destkey sourcekey1 [sourcekey2...]
PFMERGE
是Redis
中的一个命令,它用于将多个HyperLogLog
集合合并为一个新的HyperLogLog
集合。这个新的集合会包含所有源集合中的不同元素,但是它会消耗额外的内存空间,并且只能用于进一步执行基数估计操作,而不能直接添加新的元素。
命令的语法如下:
PFMERGE destkey sourcekey1 [sourcekey2 ...]
destkey
是合并后新的HyperLogLog
集合的键名。sourcekey1 [sourcekey2 ...]
是你想要合并的源HyperLogLog
集合的键名。
当 PFMERGE
命令执行后,它会创建一个新的 HyperLogLog
集合(如果 destkey
不存在的话),并将所有源集合中的不同元素合并到这个新的集合中。然后,你可以使用 PFCOUNT
命令来获取这个新集合的基数估计值。
这里有一个简单的例子:
# 假设我们有两个 HyperLogLog 集合,分别名为 "users:set1" 和 "users:set2"
# 使用 PFADD 命令向 "users:set1" 添加一些元素
redis> PFADD users:set1 user1 user2 user3
(integer) 1
# 使用 PFADD 命令向 "users:set2" 添加一些元素,包括与 "users:set1" 相同的元素和新的元素
redis> PFADD users:set2 user2 user3 user4 user5
(integer) 1
# 使用 PFMERGE 命令将两个集合合并为一个新的集合 "users:merged"
redis> PFMERGE users:merged users:set1 users:set2
OK
# 使用 PFCOUNT 命令获取合并后集合的基数估计值
redis> PFCOUNT users:merged
(integer) 5
# 注意:PFMERGE 命令不会返回任何值(除了 OK),你需要使用 PFCOUNT 来获取结果
# 并且 "users:merged" 是一个新的集合,你可以继续用它进行其他操作,但不能向它添加新元素
在上面的例子中,“users:set1
” 和 “users:set2
” 分别有 3
个和 4
个不同的元素,但是它们的并集只有 5
个不同的元素(因为 “user2
” 和 “user3
” 在两个集合中都出现了)。PFMERGE
命令能够创建一个新的集合 “users:merged
”,它包含了这两个源集合中的所有不同元素,并且 PFCOUNT
命令能够正确地返回这个新集合的基数估计值。
Geospatial
Redis 3.2
中增加了对GEO
类型的支持。GEO,Geographic
,地理信息的缩写
。该类型,就是元素的2维坐标
,在地图上就是经纬度
。redis
基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash
等常见操作。
Geospatial
介绍
Redis
中的Geospatial
(地理位置
)功能是一个强大的工具,主要用于存储和查询地理位置
信息。以下是关于Redis Geospatial
的详细介绍:
-
版本引入:
Redis在3.2
版本中引入了Geospatial
数据类型,允许用户存储和查询与地理位置相关的数据。
-
数据结构:
Geospatial
功能并没有引入新的底层数据结构
,而是直接使用了Sorted Set(有序集合)
来实现。- 在
Sorted Set
中,地理位置的经度
和纬度
被编码为分数
,而成员信息(如用户ID、商铺名称等)则作为元素存储。
-
常用命令:
GEOADD
:将指定的地理位置(经度、纬度、名称)添加到指定的key
中。GEOPOS
:返回指定地理位置的经纬度。
GEODIST
:计算两个地理位置之间的距离。
GEORADIUS
:根据给定的经纬度查询指定半径内的地理位置。
GEORADIUSBYMEMBER
:根据给定的成员(如用户ID、商铺名称)查询指定半径内的地理位置。
-
应用场景:
LBS(基于位置的服务)
:如搜索附近的餐馆、叫车等。智能推荐
:基于用户的地理位置推荐相关内容或产品。出行规划
:计算两地之间的距离,规划最优路线。
-
数据存储:
- 使用
GEOADD
命令时,可以将经纬度编码为分数,并存入zset中。例如,使用ZADD city_geo_location 116.4074 39.9042 "北京"
可以将北京的经纬度信息添加到city_geo_location
这个key中。
- 使用
-
性能:
- 由于
Geospatial
功能是基于Sorted Set
实现的,因此它具有很高的查询效率。 - 在存储和查询大量地理位置数据时,
Redis
的Geospatial
功能能够提供快速且可靠的性能。
- 由于
-
注意事项:
- 在实际开发中,需要注意数据的准确性和完整性,确保存储的地理位置信息准确无误。
- 同时,也需要考虑查询的性能和效率,避免对系统造成过大的压力。
相关命令
命令语法 | 描述 |
---|---|
geoadd key longitude latitude member [longitude latitude member...] | 添加地理位置(经度,纬度,名称) |
geopos key member [member...] | 获得指定地区的坐标值 |
geodist key member1 member2 [m|km|ft|mi] | 获取两个位置之间的直线距离 |
georadius key longitude latitude radius [m|km|ft|mi] | 以给定的经纬度为中心,找出某一半径内的元素 |
georadiusbymember key member radius [m|km|ft|mi] | 以给定某一成员为中心,找出某一半径内的元素 |
geoadd key longitude latitude member [longitude latitude member...]
是的,GEOADD
是 Redis
中用于向键(key
)添加地理位置信息的命令。该命令将一个或多个地理位置(经度和纬度)以及与之关联的成员(member
)添加到指定的键中。每个键都是一个地理位置集合。
命令的语法如下:
GEOADD key longitude latitude member [longitude latitude member ...]
key
:指定存储地理位置信息的键名。longitude
:指定地理位置的经度。latitude
:指定地理位置的纬度。member
:与经纬度关联的成员名称,可以是任何字符串。
你可以在 GEOADD
命令后跟上多组经纬度和成员,一次性添加多个地理位置。
下面是一个示例:
# 添加北京的地理位置
GEOADD cities 116.4074 39.9042 "beijing"
# 添加上海和深圳的地理位置(一次性添加多个)
GEOADD cities 121.4737 31.2304 "shanghai" 114.0578 22.5433 "shenzhen"
在上面的示例中,cities
是存储地理位置信息的键名,后面的参数是多个经纬度对和与之关联的成员名称。
一旦你添加了地理位置信息,你就可以使用其他 Geospatial
命令(如 GEOPOS
、GEODIST
、GEORADIUS
、GEORADIUSBYMEMBER
和 GEOHASH
)来查询和操作这些信息了。
geopos key member [member...]
GEOPOS
是Redis
中的一个命令,用于获取一个或多个地理位置的经度
和纬度
。它接受一个键(key
)和一个或多个成员(member
)作为参数,并返回与这些成员关联的经纬度。
命令的语法如下:
GEOPOS key member [member ...]
key
:包含地理位置信息的键名。member
:与地理位置关联的成员名称。
命令返回一个数组,其中包含每个请求成员的经纬度。如果成员不存在,那么它的位置将被表示为 [nil nil]
。
下面是一个示例:
# 假设我们已经使用 GEOADD 命令添加了北京和上海的位置信息
# GEOADD cities 116.4074 39.9042 "beijing" 121.4737 31.2304 "shanghai"
# 使用 GEOPOS 命令获取北京和上海的经纬度
GEOPOS cities "beijing" "shanghai"
# 可能的输出(取决于 Redis 的版本和格式设置)
# 1) 1) "116.40739999999998"
# 2) "39.90419999999999"
# 2) 1) "121.47370000000002"
# 2) "31.230399999999996"
在上面的示例中,GEOPOS
命令返回了两个数组,分别对应于北京和上海的经纬度。每个数组中的第一个元素是经度,第二个元素是纬度。如果请求的成员在键中不存在,那么它的位置将被表示为 [nil nil]
。
请注意,返回的经纬度可能有一些微小的精度损失,因为 Redis 内部使用 52 位精度来存储经纬度信息。这通常对于大多数应用来说是足够的,但如果你需要更高的精度,可能需要考虑其他解决方案。
geodist key member1 member2 [unit]
是的,GEODIST
是 Redis 中用于计算两个地理位置之间距离的命令。这个命令接受一个键(key)和两个成员(member1 和 member2)作为参数,并返回这两个成员之间的距离。
命令的语法如下:
GEODIST key member1 member2 [unit]
key
:包含地理位置信息的键名。member1
和member2
:与地理位置关联的成员名称。unit
(可选):距离的单位,可以是m
(米)、km
(千米)、ft
(英尺)或mi
(英里)。如果不指定单位,则默认返回以米为单位的结果。
命令返回一个浮点数,表示两个成员之间的距离。
下面是一个示例:
# 假设我们已经使用 GEOADD 命令添加了北京和上海的位置信息
# GEOADD cities 116.4074 39.9042 "beijing" 121.4737 31.2304 "shanghai"
# 使用 GEODIST 命令计算北京和上海之间的距离,以千米为单位
GEODIST cities "beijing" "shanghai" km
# 可能的输出(取决于实际的地理位置和 Redis 的版本)
# "1066.2408"
在上面的示例中,GEODIST
命令返回了北京和上海之间的距离,单位为千米。这个距离是近似值,具体取决于 Redis 内部用于计算距离的算法和地理数据的精度。
请注意,如果 member1
或 member2
在指定的键中不存在,或者指定的键不存在,那么 GEODIST
命令将返回一个错误。
georadius key longitude latitude radius [m|km|ft|mi]
GEORADIUS
是Redis
中的一个命令,用于查询指定经纬度范围内
的地理位置信息
。该命令会返回与给定经纬度距离在指定半径内的所有地理位置成员。
命令的语法如下:
GEORADIUS key longitude latitude radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
参数说明:
key
:包含地理位置信息的键名。longitude
和latitude
:指定查询的中心点的经度和纬度。radius
:指定查询的半径。[m|km|ft|mi]
:半径的单位,可以是米(m
)、千米(km
)、英尺(ft
)或英里(mi
)。
接下来是可选的参数:
WITHCOORD
:返回每个位置成员的经纬度。WITHDIST
:返回每个位置成员与中心点的距离。WITHHASH
:返回每个位置成员的geohash
字符串。COUNT count
:返回结果的最大数量。ASC
或DESC
:按照距离中心点的距离升序或降序返回结果。STORE key
:将结果存储到另一个键中,而不是直接返回。STOREDIST key
:将结果和对应的距离存储到另一个键中,每个结果是一个包含成员和距离的数组。
下面是一个示例:
# 假设我们已经使用 GEOADD 命令添加了多个城市的位置信息
# ...
# 使用 GEORADIUS 查询距离上海 100 公里内的所有城市,返回结果包括距离和经纬度
GEORADIUS cities 121.4737 31.2304 100 km WITHDIST WITHCOORD
# 可能的输出(取决于实际的地理位置和 Redis 的版本)
# 1) 1) "shanghai"
# 2) "0.0000"
# 3) 1) "121.47373437011719"
# 2) "31.23043441772461"
# 2) 1) "city_nearby"
# 2) "99.9573"
# 3) 1) "121.48000000000002"
# 2) "31.220000000000003"
# ...
在上面的示例中,GEORADIUS
命令返回了距离上海(经度为 121.4737,纬度为 31.2304)100 公里内的所有城市,并且每个结果都包含了与中心点的距离和经纬度。注意,上海本身也被包含在结果中,并且距离显示为 0。
georadiusbymember key member radius [m|km|ft|mi]
GEORADIUSBYMEMBER
是Redis
中的一个命令,用于根据指定
的位置成员
和距离,在指定的键中查找附近的位置。这个命令与GEORADIUS
命令类似,但不同的是,GEORADIUSBYMEMBER
的中心点是由给定的位置成员决定的
,而不是直接输入经纬度。
命令语法
GEORADIUSBYMEMBER key member radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
参数说明
key
:用于指定存储地理位置信息的键。member
:位置成员的名称,表示查询的中心点。radius
:范围半径,表示以中心点为圆心,查找的半径范围。[m|km|ft|mi]
:距离单位,可以是米(m
)、千米(km
)、英尺(ft
)或英里(mi
)。
可选参数
WITHCOORD
:返回每个位置成员的经纬度。WITHDIST
:返回每个位置成员与中心点的距离。WITHHASH
:返回每个位置成员的geohash
字符串。COUNT count
:返回结果的最大数量。ASC
或DESC
:按照距离中心点的距离升序或降序返回结果。STORE key
:将结果存储到另一个键中,而不是直接返回。STOREDIST key
:将结果和对应的距离存储到另一个键中,每个结果是一个包含成员和距离的数组。
示例
假设我们在名为 Sicily
的键中存储了多个城市的地理位置信息,并且想要查找距离 “Agrigento
” 100
公里内的所有城市,我们可以使用以下命令:
GEORADIUSBYMEMBER Sicily Agrigento 100 km WITHDIST
可能的输出(取决于实际的地理位置和 Redis 的版本):
1) 1) "Agrigento"
2) "0.0000" # Agrigento 到自己的距离是 0
3) 1) "13.5833333"
2) "37.316667"
2) 1) "Palermo"
2) "70.5374" # 假设 Palermo 到 Agrigento 的距离是 70.5374 公里
3) 1) "13.361389"
2) "38.115556"
...
注意事项
GEORADIUSBYMEMBER
命令的时间复杂度是 O(log(N)+M),其中 N 是指定范围之内的元素数量,而 M 是被返回的元素数量。- 如果
member
在指定的key
中不存在,那么GEORADIUSBYMEMBER
命令将返回一个错误。 - 当使用
STORE
或STOREDIST
选项时,原始键(key
)中的位置信息不会被修改。新的结果将被存储在新的键中。 Redis
中的地理位置索引使用geohash
算法进行编码,以支持高效的范围查询。
redis
中有效经度
与有效纬度
- 有效的
经度
从-180
度到180
度。 - 有效的
纬度
从-85.05112878
度到85.05112878
度。 当坐标位置超出指定范围时,该命令将会返回一个错误。
已经
添加的数据,是无法
再次往里面添加的。