在上一篇文章中,我们详细介绍了如何在 Golang 中使用 go-redis 操作 Redis 的 GEO 地理空间数据类型。如果你还没有阅读过,可以点击这里进行回顾。本篇文章,我们将深入探讨 Redis 中一个非常实用但相对不太为人所知的数据类型——HyperLogLog,以及如何在 Golang 中使用 go-redis 进行相关操作。
👉 点击查看 go-redis 使用指南目录
在《go-redis 使用指南》系列文章中,我们将详细介绍如何在 Golang 项目中使用 redis/go-redis 库与 Redis 进行交互。以下是该系列文章的全部内容:
- Golang 操作 Redis:快速上手 - go-redis 使用指南
- Golang 操作 Redis:连接设置与参数详解 - go-redis 使用指南
- Golang 操作 Redis:基础的字符串键值操作 - go-redis 使用指南
- Golang 操作 Redis:如何设置 key 的过期时间 - go-redis 使用指南
- Golang 操作 Redis:Hash 哈希数据类型操作用法 - go-redis 使用指南
- Golang 操作 Redis:Set 集合数据类型操作用法 - go-redis 使用指南
- Golang 操作 Redis:为 Hash 中的字段设置过期时间 - go-redis 使用指南
- Golang 操作 Redis:List 列表数据类型操作用法 - go-redis 使用指南
- Golang 操作 Redis:SortedSet 有序集合数据类型操作用法 - go-redis 使用指南
- Golang 操作 Redis:bitmap 数据类型操作用法 - go-redis 使用指南
- Golang 操作 Redis:事务处理操作用法 - go-redis 使用指南
- Golang 操作 Redis:地理空间数据类型操作用法 - go-redis 使用指南
- Golang 操作 Redis:HyperLogLog 操作用法 - go-redis 使用指南
- Golang 操作 Redis:Pipeline 操作用法 - go-redis 使用指南
- Golang 操作 Redis:PubSub发布订阅用法 - go-redis 使用指南
- Golang 操作 Redis:布隆过滤器(Bloom Filter)操作用法 - go-redis 使用指南
- Golang 操作 Redis:Cuckoo Filter操作用法 - go-redis 使用指南
- Golang 操作 Redis:Stream操作用法 - go-redis 使用指南
Redis HyperLogLog 简介
HyperLogLog 是一种概率数据结构,用于估算基数(即去重后元素的数量)。它在提供极小空间消耗的同时,能够在一定误差范围内高效地计算基数。相较于传统的计数数据结构,HyperLogLog 能在处理海量数据时保持极低的内存消耗。
什么是基数估算
基数就是指一个集合中不同值的数目,比如 a, b, c, d 的基数就是 4,a, b, c, d, a 的基数还是 4。虽然 a 出现两次,只会被计算一次。
基数估算指的是对一个集合中唯一元素数量的估算。传统的计算方法往往需要记录每个元素,从而消耗大量内存。而 HyperLogLog 则通过概率算法提供了一种高效的估算方法,使其能够在有限的内存中处理海量数据。
HyperLogLog 算法基本实现原理
HyperLogLog 算法基于哈希函数和桶(或称寄存器)的概念。其基本实现原理如下:
- 哈希化:将每个输入元素通过哈希函数映射为一个哈希值。
- 桶划分:将哈希值的前若干位用于确定桶的位置。桶的数量是固定的,通常是 2 的幂次方。
- 桶计数:对于每个桶,记录哈希值后若干位的最大前导零位数。这个值用来估算桶中元素的数量。
- 基数估算:通过对所有桶的前导零位数进行统计,并使用特定的算法将结果转换为基数的估算值。
HyperLogLog 的核心优势在于其在计算基数时只需固定的内存空间,且随着数据量的增加,内存使用不会显著增加。
HyperLogLog 的应用场景
HyperLogLog 主要的应用场景就是进行基数统计,比如:
- 网站独立用户统计:统计某一时间段内访问网站的独立用户数量。
- 日志分析:分析日志中的独特 IP 地址数量,评估独立访客数。
- 社交网络分析:统计社交网络中独特用户的互动数,例如点赞、评论等。
HyperLogLog 与其他数据结构的差别
使用 Redis 统计集合的基数一般有三种方法,分别是使用 Redis 的 Set, HashMap,BitMap 和 HyperLogLog。前面几个数据结构在集合的数量级增长时,所消耗的内存会大大增加,但是 HyperLogLog 则不会。
Redis 的 HyperLogLog 通过牺牲准确率来减少内存空间的消耗,只需要 12K 内存,在标准误差 0.81%的前提下,能够统计 2^64 个数据。所以 HyperLogLog 是否适合在比如统计日活月活此类的对精度要不不高的场景。
- Set:Redis 的 Set 数据结构用于存储唯一元素,但在存储大量元素时会消耗大量内存。HyperLogLog 则通过概率算法来大幅减少内存消耗,即使在存储数百万级别的元素时,也能保持低内存使用。
- HashMap:HashMap 用于存储键值对,并且可以有效地统计唯一键的数量。然而,HashMap 也需要大量内存来存储数据,而 HyperLogLog 则可以在固定的内存空间中完成基数估算。
- Bitmap:Bitmap 是一种位图数据结构,适用于处理稀疏数据的基数统计,但对于大规模数据的处理不如 HyperLogLog 高效。Bitmap 通常在处理小范围的数据时较为有效,而 HyperLogLog 在处理大规模数据时表现更优。
- HyperLogLog:存在一定误差,占用内存少,稳定占用 12k 左右内存,可以统计 2^64 个元素
go-redis 中 HyperLogLog 操作的方法
以下是 go-redis 提供的 HyperLogLog 操作方法及其功能描述:
PFAdd
- 将指定元素添加到 HyperLogLogPFCount
- 返回给定 HyperLogLog 的基数估算值PFMerge
- 将多个 HyperLogLog 合并为一个
go-redis HyperLogLog 操作方法详细讲解及示例代码
PFAdd
将指定元素添加到 HyperLogLog 中。如果 HyperLogLog 不存在,将创建一个新的。
方法签名:
PFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。key
:HyperLogLog 的键名。els
:要添加的元素,可以是一个或多个。
返回结果说明:
- 返回一个
*IntCmd
,结果为 1 表示 HyperLogLog 被修改,0 表示没有修改。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
key := "hll_example"
// 添加元素到 HyperLogLog
result, err := rdb.PFAdd(ctx, key, "user1", "user2", "user3").Result()
if err != nil {
panic(err)
}
fmt.Printf("PFAdd result: %d\n", result)
// 输出:PFAdd result: 1
}
PFCount
返回给定 HyperLogLog 的基数估算值。
方法签名:
PFCount(ctx context.Context, keys ...string) *IntCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。keys
:一个或多个 HyperLogLog 的键名。
返回结果说明:
- 返回一个
*IntCmd
,结果为基数估算值。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
key := "hll_example"
// 获取 HyperLogLog 的基数估算值
count, err := rdb.PFCount(ctx, key).Result()
if err != nil {
panic(err)
}
fmt.Printf("PFCount result: %d\n", count)
// 输出:PFCount result: 3
}
PFMerge
将多个 HyperLogLog 合并为一个。
方法签名:
PFMerge(ctx context.Context, dest string, keys ...string) *StatusCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。dest
:目标 HyperLogLog 的键名。keys
:要合并的 HyperLogLog 的键名列表。
返回结果说明:
- 返回一个
*StatusCmd
,表示合并操作的状态。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
key1 := "hll_example1"
key2 := "hll_example2"
destKey := "hll_merged"
// 添加元素到不同的 HyperLogLog
rdb.PFAdd(ctx, key1, "user1", "user2")
rdb.PFAdd(ctx, key2, "user3", "user4")
// 合并两个 HyperLogLog
status, err := rdb.PFMerge(ctx, destKey, key1, key2).Result()
if err != nil {
panic(err)
}
fmt.Printf("PFMerge status: %s\n", status)
// 输出:PFMerge status: OK
// 获取合并后的 HyperLogLog 的基数估算值
count, err := rdb.PFCount(ctx, destKey).Result()
if err != nil {
panic(err)
}
fmt.Printf("Merged PFCount result: %d\n", count)
// 输出:Merged PFCount result: 4
}
完整示例:使用 HyperLogLog 实现网站独立用户统计
假设你在一个网站上统计每天的独立访客数。为了优化内存使用,你选择使用 Redis 的 HyperLogLog 数据结构。这个示例包含以下功能:
- 记录每日独立访客:使用
PFAdd
将每日访问的用户添加到 HyperLogLog 中。 - 计算独立访客数:使用
PFCount
获取每天的独立用户数量。 - 合并统计数据:使用
PFMerge
合并来自不同时间段的独立访客统计数据。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"time"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 示例数据
days := []string{"2024-08-01", "2024-08-02", "2024-08-03"}
usersPerDay := map[string][]string{
"2024-08-01": {"user1", "user2", "user3"},
"2024-08-02": {"user2", "user3", "user4"},
"2024-08-03": {"user3", "user4", "user5"},
}
// 记录每日独立访客
for _, day := range days {
key := "hll:" + day
users := usersPerDay[day]
_, err := rdb.PFAdd(ctx, key, users).Result()
if err != nil {
fmt.Printf("PFAdd error: %v\n", err)
return
}
fmt.Printf("Added users for %s\n", day)
}
// 计算每一天的独立访客数
for _, day := range days {
key := "hll:" + day
count, err := rdb.PFCount(ctx, key).Result()
if err != nil {
fmt.Printf("PFCount error: %v\n", err)
return
}
fmt.Printf("Unique visitors on %s: %d\n", day, count)
}
// 合并统计数据
destKey := "hll:merged"
keys := []string{}
for _, day := range days {
keys = append(keys, "hll:"+day)
}
_, err := rdb.PFMerge(ctx, destKey, keys...).Result()
if err != nil {
fmt.Printf("PFMerge error: %v\n", err)
return
}
fmt.Println("Merged HyperLogLog keys")
// 获取合并后的独立访客数
totalCount, err := rdb.PFCount(ctx, destKey).Result()
if err != nil {
fmt.Printf("PFCount (merged) error: %v\n", err)
return
}
fmt.Printf("Total unique visitors: %d\n", totalCount)
}
运行输出:
Added users for 2024-08-01
Added users for 2024-08-02
Added users for 2024-08-03
Unique visitors on 2024-08-01: 3
Unique visitors on 2024-08-02: 3
Unique visitors on 2024-08-03: 3
Merged HyperLogLog keys
Total unique visitors: 5
结语
通过本文的详细介绍,我们不仅学习了 HyperLogLog 数据结构的基本原理和实际应用,还掌握了在 Golang 中使用 go-redis 进行 HyperLogLog 操作的方法。HyperLogLog 在处理大规模数据时提供了高效的基数估算,并能显著减少内存消耗。希望这篇文章对你有所帮助!点击 go-redis 使用指南 可查看更多相关教程!如果你有任何问题或建议,欢迎在评论区留言!