Redis缓存穿透实战:布隆过滤器原理与Go语言实现

配图:标题:Redis缓存穿透实战:布隆过滤器原理与Go语言实现;副标题:从原

环境准备

在动手写代码之前,先把开发环境搭好。本教程使用 Go 1.21 或更高版本,以及 Redis 7.0 或更高版本。请确保你的机器上已经安装了 Go 和 Redis 服务器。我们先用命令行检查一下版本。

go version
redis-cli --version

检查 Go 和 Redis CLI 版本

预期输出类似:`go version go1.21.5 darwin/amd64` 和 `redis-cli 7.0.12`。接下来,启动 Redis 服务器。如果你用 Docker,可以这样启动。

docker run -d -p 6379:6379 redis:7.0

使用 Docker 启动 Redis 7.0

然后,创建一个新的 Go 项目目录并初始化模块。

mkdir bloom-filter-demo
cd bloom-filter-demo
go mod init bloom-filter-demo

初始化 Go 项目模块

最后,安装 Go 的 Redis 客户端库。

go get github.com/redis/go-redis/v9

安装 go-redis 客户端库

布隆过滤器原理

布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否在一个集合中。它由一个位数组和一系列哈希函数组成。当插入一个元素时,通过多个哈希函数计算出多个位置,并将这些位置置为 1。查询时,同样计算这些位置,如果所有位置都是 1,则认为元素可能存在;如果有一个位置是 0,则元素一定不存在。布隆过滤器的误判率是可控的,但无法删除元素 [来源#2]。

  • 优点:空间占用极小,查询速度快。
  • 缺点:存在误判(假阳性),且不支持删除操作。
  • 适用场景:缓存穿透防护、垃圾邮件过滤、爬虫去重等。

步骤拆解:实现布隆过滤器服务

我们将分步骤实现一个布隆过滤器服务,该服务使用 Redis 作为存储后端。Redis 从 7.0 版本开始原生支持布隆过滤器模块 [来源#1]。但为了更深入理解原理,我们将先用 Go 实现一个纯内存的布隆过滤器,然后将其集成到 Redis 中。

步骤 1:创建 Go 布隆过滤器结构体

首先,定义布隆过滤器的结构体,包含位数组大小、哈希函数数量和位数组本身。

package main

import (
    "math"
    "sync"
)

// BloomFilter 布隆过滤器结构体
type BloomFilter struct {
    bitArray []bool // 位数组
    size     uint   // 位数组大小
    hashFunc int    // 哈希函数数量
    mu       sync.RWMutex
}

// NewBloomFilter 创建一个新的布隆过滤器
// m: 位数组大小, k: 哈希函数数量
func NewBloomFilter(m uint, k int) *BloomFilter {
    return &BloomFilter{
        bitArray: make([]bool, m),
        size:     m,
        hashFunc: k,
    }
}

定义布隆过滤器结构体

预期输出:无,但代码应能编译通过。接下来,实现哈希函数。

步骤 2:实现哈希函数

我们使用两个简单的哈希函数(FNV-1a 和 DJB2)来模拟多个哈希函数。在实际生产中,可以使用更复杂的哈希函数。

// fnv1aHash FNV-1a 哈希函数
func fnv1aHash(data string) uint {
    hash := uint(14695981039346656037) // FNV offset basis
    for _, b := range data {
        hash ^= uint(b)
        hash *= 1099511628211 // FNV prime
    }
    return hash
}

// djb2Hash DJB2 哈希函数
djb2Hash(data string) uint {
    hash := uint(5381)
    for _, b := range data {
        hash = ((hash << 5) + hash) + uint(b) // hash * 33 + b
    }
    return hash
}

// getIndices 获取元素对应的位数组索引
func (bf *BloomFilter) getIndices(data string) []uint {
    indices := make([]uint, bf.hashFunc)
    hash1 := fnv1aHash(data)
    hash2 := djb2Hash(data)
    for i := 0; i < bf.hashFunc; i++ {
        // 使用双哈希生成多个独立哈希值
        combined := hash1 + uint(i)*hash2
        indices[i] = combined % bf.size
    }
    return indices
}

实现哈希函数和索引计算

预期输出:无,但代码应能编译通过。接下来,实现插入和查询方法。

步骤 3:实现插入和查询方法

插入时,将计算出的所有索引位置置为 true。查询时,检查所有索引位置是否都为 true。

// Insert 插入元素到布隆过滤器
func (bf *BloomFilter) Insert(data string) {
    bf.mu.Lock()
    defer bf.mu.Unlock()
    indices := bf.getIndices(data)
    for _, idx := range indices {
        bf.bitArray[idx] = true
    }
}

// Contains 检查元素是否可能存在于布隆过滤器中
func (bf *BloomFilter) Contains(data string) bool {
    bf.mu.RLock()
    defer bf.mu.RUnlock()
    indices := bf.getIndices(data)
    for _, idx := range indices {
        if !bf.bitArray[idx] {
            return false
        }
    }
    return true
}

实现插入和查询方法

预期输出:无,但代码应能编译通过。接下来,将布隆过滤器集成到 Redis 中。

步骤 4:集成 Redis

我们将使用 Redis 的位图(bitmap)来存储布隆过滤器的位数组。Redis 的位图操作非常高效,适合存储布隆过滤器 [来源#1]。

package main

import (
    "context"
    "fmt"
    "log"
    "github.com/redis/go-redis/v9"
)

// RedisBloomFilter 基于 Redis 的布隆过滤器
type RedisBloomFilter struct {
    client *redis.Client
    key    string // Redis 中存储位图的键
    size   uint   // 位数组大小
    hashFunc int  // 哈希函数数量
}

// NewRedisBloomFilter 创建 Redis 布隆过滤器
func NewRedisBloomFilter(redisAddr string, key string, m uint, k int) *RedisBloomFilter {
    rdb := redis.NewClient(&redis.Options{
        Addr: redisAddr,
    })
    return &RedisBloomFilter{
        client:   rdb,
        key:      key,
        size:     m,
        hashFunc: k,
    }
}

// getIndices 获取元素对应的位数组索引(复用之前的哈希函数)
func (rbf *RedisBloomFilter) getIndices(data string) []uint {
    indices := make([]uint, rbf.hashFunc)
    hash1 := fnv1aHash(data)
    hash2 := djb2Hash(data)
    for i := 0; i < rbf.hashFunc; i++ {
        combined := hash1 + uint(i)*hash2
        indices[i] = combined % rbf.size
    }
    return indices
}

// Insert 插入元素到 Redis 布隆过滤器
func (rbf *RedisBloomFilter) Insert(data string) error {
    ctx := context.Background()
    indices := rbf.getIndices(data)
    pipe := rbf.client.Pipeline()
    for _, idx := range indices {
        // SETBIT key offset 1
        pipe.SetBit(ctx, rbf.key, int64(idx), 1)
    }
    _, err := pipe.Exec(ctx)
    return err
}

// Contains 检查元素是否可能存在于 Redis 布隆过滤器中
func (rbf *RedisBloomFilter) Contains(data string) (bool, error) {
    ctx := context.Background()
    indices := rbf.getIndices(data)
    pipe := rbf.client.Pipeline()
    for _, idx := range indices {
        // GETBIT key offset
        pipe.GetBit(ctx, rbf.key, int64(idx))
    }
    results, err := pipe.Exec(ctx)
    if err != nil {
        return false, err
    }
    for _, res := range results {
        val, _ := res.(*redis.IntCmd).Result()
        if val == 0 {
            return false, nil
        }
    }
    return true, nil
}

// Close 关闭 Redis 连接
func (rbf *RedisBloomFilter) Close() error {
    return rbf.client.Close()
}

func main() {
    // 示例:使用 Redis 布隆过滤器
    rbf := NewRedisBloomFilter("localhost:6379", "bloom:users", 1000, 3)
    defer rbf.Close()

    // 插入数据
    err := rbf.Insert("user:123")
    if err != nil {
        log.Fatal("插入失败:", err)
    }

    // 查询数据
    exists, err := rbf.Contains("user:123")
    if err != nil {
        log.Fatal("查询失败:", err)
    }
    fmt.Printf("Key 'user:123' exists: %t\n", exists)

    // 查询不存在的数据
    exists, err = rbf.Contains("user:456")
    if err != nil {
        log.Fatal("查询失败:", err)
    }
    fmt.Printf("Key 'user:456' exists: %t\n", exists)
}

完整的 Redis 布隆过滤器实现与示例

预期输出:运行此程序后,应看到类似以下输出:`Key 'user:123' exists: true` 和 `Key 'user:456' exists: false`。

结果验证

为了验证布隆过滤器的有效性,我们可以进行以下测试:

  • 插入多个键,然后查询这些键,确保它们都被正确识别为存在。
  • 查询一个从未插入的键,确保它被识别为不存在。
  • 使用 Redis 命令行工具检查位图状态。
# 连接到 Redis
redis-cli

# 检查位图中特定位置的值
# 假设我们插入了 'user:123',可以查看其哈希索引对应的位
GETBIT bloom:users 123
GETBIT bloom:users 456

# 统计位图中 1 的数量(可以粗略估计插入的元素数量,但不精确)
BITCOUNT bloom:users

使用 Redis CLI 验证位图状态

预期输出:根据插入的键,某些位应为 1,其他位为 0。`BITCOUNT` 命令会返回位数组中 1 的总数。

常见错误与排查

在实现过程中,可能会遇到以下常见错误:

  • Redis 连接失败:检查 Redis 服务器是否运行,地址和端口是否正确。使用 `redis-cli ping` 测试连接。
  • 哈希函数冲突:如果哈希函数设计不当,可能导致误判率升高。建议使用多个独立的哈希函数。
  • 位数组大小不足:如果位数组太小,误判率会急剧上升。根据预期元素数量和误判率计算合适的大小。

布隆过滤器的误判率公式为:(1 - e^(-kn/m))^k,其中 m 是位数组大小,n 是元素数量,k 是哈希函数数量。根据此公式调整参数以降低误判率 [来源#2]。


总结
本教程从原理到代码,完整实现了基于 Redis 的布隆过滤器服务。通过 Go 语言和 Redis 位图,我们能够有效拦截无效查询,解决缓存穿透问题。请根据实际业务需求调整参数,并持续监控误判率。

参考链接

阅读剩余
THE END