布隆过滤器必须与Redis Set配合使用,单用任一方案会导致漏判或吞吐下降;需归一化URL后同步更新二者,并用Lua脚本保证Redis原子操作,去重逻辑应集中于调度中心。

Go语言实现分布式爬虫去重,核心不在“要不要用协程”,而在于**布隆过滤器必须和Redis Set配合用,单靠任何一种都会漏判或拖慢吞吐**。
为什么布隆过滤器不能单独做URL去重
布隆过滤器是概率型结构,存在误判(false positive),但不会漏判(false negative)。实际抓取中,如果只依赖它,filter.Check(url)返回true时你得再查一次Redis确认——否则可能丢掉真实新URL;而返回false时能直接放行,这是它加速的关键。但若跳过Redis校验,重复率会上升5%~15%,尤其在URL路径含随机参数(如/article?id=123&ts=1718234567)时更明显。
实操建议:
- 初始化布隆过滤器时,容量设为预估总URL数的1.2倍,错误率控制在0.01(即1%)以内,用
gotiny/bloom或spiegler/bloom库 - 每次
filter.Add(url)后,必须同步写入redis.SetAdd("urls_seen", url),两者不可割裂 - 避免对原始URL直接哈希——先归一化:去掉查询参数中的
utm_*、sessionid等噪声字段,再计算指纹
goroutine并发下Redis原子操作怎么写才不丢数据
多个fetcher goroutine同时调用redis.SIsMember("urls_seen", url)再SAdd,必然出现竞态:两个协程几乎同时查到不存在,然后都写入,导致重复抓取。必须用Lua脚本保证原子性。
立即学习“go语言免费学习笔记(深入)”;
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
实操建议:
- 用Redis的
EVAL执行内联脚本,例如:local exists = redis.call("SISMEMBER", KEYS[1], ARGV[1])\nif exists == 1 then\n return 0\nelse\n redis.call("SADD", KEYS[1], ARGV[1])\n return 1\nend - Go中调用:
client.Eval(ctx, script, []string{"urls_seen"}, url).Val(),返回1表示新URL,0表示已存在 - 别用
SETNX替代——它只支持字符串,无法对Set做“存在则跳过,否则添加”的原子判断
去重模块部署在调度中心还是爬虫节点
去重逻辑必须集中,不能每个节点自己维护一份布隆过滤器+Redis连接。否则节点间看不到彼此刚插入的URL,布隆过滤器各自训练,误判率飙升。
实操建议:
- 所有
fetchergoroutine统一通过HTTP或gRPC向调度中心的/check-url接口提交URL,由中心完成布隆过滤+Redis双重校验并返回结果 - 若网络延迟敏感,可在每个节点缓存最近10分钟高频域名的布隆过滤器快照(定期从中心同步),但缓存仅作快速放过,最终仍以中心判为准
- Redis连接池需配置
MaxActive: 50以上,避免大量goroutine阻塞在conn.Get()
真正难的不是写几个go fetch(url),而是让成百上千个goroutine在去重这件事上既不打架也不撒谎——布隆过滤器负责“快”,Redis负责“准”,调度中心负责“唯一真相源”。漏掉任意一环,爬虫跑三天后就会开始反复抓同一页面。

















