在上一篇文章中,我们详细介绍了如何在 Golang 中使用 go-redis 操作 Redis 的 GEO 地理空间数据类型。如果你还没有阅读过,可以点击这里进行回顾。本篇文章,我们将深入探讨 Redis 中一个非常实用但相对不太为人所知的数据类型——HyperLogLog,以及如何在 Golang 中使用 go-redis 进行相关操作。
在《go-redis 使用指南》系列文章中,我们将详细介绍如何在 Golang 项目中使用 redis/go-redis 库与 Redis 进行交互。以下是该系列文章的全部内容:
HyperLogLog 是一种概率数据结构,用于估算基数(即去重后元素的数量)。它在提供极小空间消耗的同时,能够在一定误差范围内高效地计算基数。相较于传统的计数数据结构,HyperLogLog 能在处理海量数据时保持极低的内存消耗。
基数就是指一个集合中不同值的数目,比如 a, b, c, d 的基数就是 4,a, b, c, d, a 的基数还是 4。虽然 a 出现两次,只会被计算一次。
基数估算指的是对一个集合中唯一元素数量的估算。传统的计算方法往往需要记录每个元素,从而消耗大量内存。而 HyperLogLog 则通过概率算法提供了一种高效的估算方法,使其能够在有限的内存中处理海量数据。
HyperLogLog 算法基于哈希函数和桶(或称寄存器)的概念。其基本实现原理如下:
HyperLogLog 的核心优势在于其在计算基数时只需固定的内存空间,且随着数据量的增加,内存使用不会显著增加。
HyperLogLog 主要的应用场景就是进行基数统计,比如:
使用 Redis 统计集合的基数一般有三种方法,分别是使用 Redis 的 Set, HashMap,BitMap 和 HyperLogLog。前面几个数据结构在集合的数量级增长时,所消耗的内存会大大增加,但是 HyperLogLog 则不会。
Redis 的 HyperLogLog 通过牺牲准确率来减少内存空间的消耗,只需要 12K 内存,在标准误差 0.81%的前提下,能够统计 2^64 个数据。所以 HyperLogLog 是否适合在比如统计日活月活此类的对精度要不不高的场景。
以下是 go-redis 提供的 HyperLogLog 操作方法及其功能描述:
PFAdd
- 将指定元素添加到 HyperLogLogPFCount
- 返回给定 HyperLogLog 的基数估算值PFMerge
- 将多个 HyperLogLog 合并为一个将指定元素添加到 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
}
返回给定 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
}
将多个 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
}
假设你在一个网站上统计每天的独立访客数。为了优化内存使用,你选择使用 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 使用指南 可查看更多相关教程!如果你有任何问题或建议,欢迎在评论区留言!