应避免用 map[string]struct{} 去重千万级 URL,因其内存开销达 1.2+ GB;布隆过滤器仅需约 12MB,但存在极小误判率(false positive),绝不漏判(false negative),故仅可作首道筛选,不可替代最终校验。

为什么不用 map[string]struct{} 直接去重
1000 万条平均 50 字节的 URL,map[string]struct{} 实测吃掉 1.2+ GB 内存——不是因为字符串本身,而是 Go runtime 的哈希桶、指针、内存对齐和 GC 元信息叠加导致。布隆过滤器把空间压到约 12MB,代价是允许极小概率误判(false positive),但**绝不漏判(false negative)**。这意味着:你用它拦下的 URL,100% 是重复的;它放过的,可能重复,也可能不重复——所以它只能当第一道筛子,不能替代最终校验。
github.com/yourbasic/bloom 初始化参数怎么填才不翻车
填错 cap 和 fpRate,后面所有操作都白搭:
-
cap必须填「整个生命周期内最多会插入多少个唯一 URL」,不是当前量。比如爬虫日增 500 万,想撑 3 天且留 20% 余量,就该填10_000_000 * 1.2→12_000_000 -
fpRate别盲目设成0.0001。从0.01降到0.001,位数组大小涨约 45%;再降到0.0001,又涨约 30%,且初始化更慢。日常 URL 去重,0.001(千分之一)是性价比拐点 - 示例:
filter := bloom.New(12_000_000, 0.001)—— 库内部自动算出最优位数和哈希轮数,不用手算公式
并发写必须加锁,但读可以完全无锁
bloom.Filter 底层是 []byte,多个 goroutine 同时调 Add() 会竞态写同一位,导致位被意外清零或漏置,误判率瞬间失控。但 Test() 只读不写,且单字节读取在 Go 中是原子的,天然线程安全。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 写操作(
Add/TestAndAdd)必须包一层sync.RWMutex或用sync.Pool管理单例 filter - 读操作(
Test)可放心并发调用,无需任何同步开销 - 别用
Test()判断后再手动Add()—— 这中间有竞态窗口,两个 goroutine 可能同时通过Test返回false,然后都执行Add,造成逻辑重复
Test() 返回 true 后必须二次校验
这是最容易被忽略的逻辑责任。布隆过滤器只回答“很可能存在”,不是“确定存在”。一旦跳过校验,就把误判当成真实重复,URL 就丢了。
立即学习“go语言免费学习笔记(深入)”;
-
Test()返回false→ 可直接处理(如入库、发消息) -
Test()返回true→ 必须查真实存储(如Redis SETNX、本地map或 DBSELECT)确认是否真重复 - 校验存储建议用带 TTL 的
Redis SETNX,避免并发写入冲突;TTL 要覆盖业务最大处理周期,防止 key 残留 - 别在
Test()为true时再往布隆里写 —— 它不负责去重,只负责快速拦截
布隆过滤器的边界很清晰:它不解决“精确去重”,只解决“以极低成本筛掉绝大多数重复”。真正卡住你的,从来不是库选得对不对,而是 cap 估得准不准、并发锁加没加对、以及 Test() 后那一行二次校验写没写。

















