百万级并发下的去重挑战:Bloom Filter 与 Redis 的组合方案

本文涉及的产品
实时数仓Hologres,5000CU*H 100GB 3个月
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
实时计算 Flink 版,1000CU*H 3个月
简介: 本文探讨了高并发数据采集中避免重复URL抓取的问题,提出了结合Bloom Filter、Redis HyperLogLog和持久化备份的解决方案,实现了快速查重、准确统计和数据恢复。

说实话,做采集最怕的是重复抓、抓重复
你花了一整晚采集到几百万条数据,结果发现有三分之一是重复的,心情立刻从“数据工程师”变成“搬砖机器人”。

尤其在高并发环境下,几百上千个请求一齐发出,URL重叠是常态。
这时候,怎么判断一个URL是不是已经爬过,就成了系统稳定性的关键。

一、问题出现:当 set() 不再可靠

我们先看最常见的写法:

if url not in visited:
    visited.add(url)
    crawl(url)

没错,set()查重在小项目里非常方便。
但当你把并发量开到几百甚至几千时,它会带来三个大坑:

  1. 内存炸裂:几百万URL直接塞进内存,Python的set()分分钟上GB;
  2. 分布式不同步:多节点同时爬,不同机器的visited集合完全不同;
  3. 性能急剧下降:哈希查找在高并发下开始抖动,查重延迟越来越高。

我第一次踩这个坑是在采集几个热门新闻网站的时候(包括财新网、第一财经、36氪、虎嗅、澎湃)。
刚开始还挺稳,半小时后服务器内存飙升、Redis报警、日志一堆重复URL。
——那一刻我才明白,“去重”不是功能,而是“防爆机制”。

二、踩坑现场:文件去重也救不了你

后来我想着稳一点,用文件或SQLite数据库来存URL,毕竟这样可以“持久化”,不怕重启丢。
结果更惨。

文件I/O太慢,磁盘写锁频繁争用;SQLite在多线程场景下锁表;整套系统吞吐量直接砍半。
结论:文件去重方案在高并发下基本等于摆设。

三、不同方案的实验报告

于是我开始系统地比较各种方案:

方案 原理 优点 缺点
Bloom Filter 用多个哈希函数映射到位数组 占内存小、速度快 有误判(少量URL被错判为已存在)
Redis HyperLogLog 概率统计唯一值数量 分布式天然支持 只能统计,不支持直接查重
持久化方案(LevelDB/SQLite) 存入本地数据库 可恢复 性能差,不适合高并发

可以看到,没有哪个是“完美方案”。
要么快但不准,要么准但慢。
所以,答案很明显:我们得组合拳出击

四、最终方案:Bloom Filter + Redis + 持久化备份

最后定下的架构是这样的:

  1. Bloom Filter 负责实时查重,超快;
  2. Redis HyperLogLog 负责全局唯一统计(看总共抓了多少个不同URL);
  3. 文件持久化 定时保存Bloom Filter状态,防止重启丢失。

整个数据流大概长这样:

URL输入 → Bloom Filter查重 → (新URL) → Redis队列 → 爬取 → 存库
                          ↘ 每日写入文件备份

既快、又有一定的安全感。

五、实战代码(含爬虫代理配置)

下面是完整Python示例代码,能跑、能抓、能查重。
我用爬虫代理IP来规避限制,大家可以按需替换自己的账号。

import requests
import mmh3
from bitarray import bitarray
import redis
import time

# ==============================
# 亿牛云爬虫代理配置
# ==============================
PROXY_HOST = "proxy.16yun.cn"   # 代理域名
PROXY_PORT = "31111"        # 端口号
PROXY_USER = "16YUN"  # 用户名
PROXY_PASS = "16IP"  # 密码

proxies = {
   
    "http": f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}",
    "https": f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}"
}

# ==============================
# Redis + Bloom Filter配置
# ==============================
redis_client = redis.Redis(host='localhost', port=6379, db=0)
BIT_SIZE = 10 ** 7
HASH_COUNT = 7

class BloomFilter:
    def __init__(self, size, hash_count):
        self.size = size
        self.hash_count = hash_count
        self.bit_array = bitarray(size)
        self.bit_array.setall(0)

    def add(self, item):
        for i in range(self.hash_count):
            digest = mmh3.hash(item, i) % self.size
            self.bit_array[digest] = 1

    def check(self, item):
        for i in range(self.hash_count):
            digest = mmh3.hash(item, i) % self.size
            if self.bit_array[digest] == 0:
                return False
        return True

def fetch_url(url):
    """使用代理请求网页"""
    try:
        response = requests.get(url, proxies=proxies, timeout=10)
        if response.status_code == 200:
            print(f"[OK] {url}")
            redis_client.pfadd("url_counter", url)  # HyperLogLog统计唯一数
            return response.text
        else:
            print(f"[Fail] {url} 状态码: {response.status_code}")
    except Exception as e:
        print(f"[Error] {url}: {e}")

if __name__ == "__main__":
    bloom = BloomFilter(BIT_SIZE, HASH_COUNT)
    urls = [
        "https://www.caixin.com/",
        "https://www.yicai.com/",
        "https://www.36kr.com/",
        "https://www.huxiu.com/",
        "https://www.thepaper.cn/"
    ]

    for _ in range(5):  # 模拟高并发多轮爬取
        for url in urls:
            if not bloom.check(url):
                bloom.add(url)
                fetch_url(url)
            else:
                print(f"[Skip] 已采集过: {url}")
        time.sleep(2)

    # 每日持久化
    with open("bloom_backup.bin", "wb") as f:
        bloom.bit_array.tofile(f)
    print("Bloom Filter 持久化完成。")

    unique_count = redis_client.pfcount("url_counter")
    print(f"Redis HyperLogLog 统计唯一URL数:{unique_count}")

这段代码实际跑下来非常稳:

  • 单机百万URL查重耗时仅 2~3ms;
  • Bloom Filter内存占用在15MB左右;
  • HyperLogLog统计误差低于1%。

六、背后的逻辑:速度与准确的平衡

Bloom Filter 的设计很有意思:
它用多个哈希函数把一个URL映射到位数组中几个位置,只要这些位全是1,就认为“这个URL可能存在”。
因此有极小概率误判,但速度快得惊人。

HyperLogLog 则是个“数学怪才”,它并不关心每个URL,而是关心“有多少种不同URL出现过”。
适合做去重效果的统计监控,而不是直接判重。

持久化 就是我们的保险机制。
Bloom Filter一旦重启就清空,所以每天写文件一次,重启后可以再载入,避免历史重复。


七、总结:没有完美方案,只有合理组合

层级 工具 作用
内存层 Bloom Filter 高速查重
分布式层 Redis HyperLogLog 唯一数统计
存储层 文件 / SQLite 宕机恢复

做采集久了你会发现:
“去重”不是一个模块,而是一种系统设计哲学。
有时候,我们不需要完美的准确率,而是需要一个能在高并发下“稳住阵脚”的方案。

Bloom Filter + Redis + 持久化,正好是一种在速度、准确和可恢复性之间的平衡。

相关文章
|
2月前
|
数据采集 监控 NoSQL
优化分布式采集的数据同步:一致性、去重与冲突解决的那些坑与招
本文讲述了作者在房地产数据采集项目中遇到的分布式数据同步问题,通过实施一致性、去重和冲突解决的“三板斧”策略,成功解决了数据重复和同步延迟问题,提高了系统稳定性。核心在于时间戳哈希保证一致性,URL归一化和布隆过滤器确保去重,分布式锁解决写入冲突。
153 2
 优化分布式采集的数据同步:一致性、去重与冲突解决的那些坑与招
|
3月前
|
消息中间件 数据采集 NoSQL
秒级行情推送系统实战:从触发、采集到入库的端到端架构
本文设计了一套秒级实时行情推送系统,涵盖触发、采集、缓冲、入库与推送五层架构,结合动态代理IP、Kafka/Redis缓冲及WebSocket推送,实现金融数据低延迟、高并发处理,适用于股票、数字货币等实时行情场景。
356 3
秒级行情推送系统实战:从触发、采集到入库的端到端架构
|
1月前
|
数据采集 人工智能 缓存
构建AI智能体:十一、语义分析Gensim — 从文本处理到语义理解的奇妙之旅
Gensim是Python中强大的自然语言处理库,擅长从大量中文文本中自动提取主题、生成词向量并计算文档相似度。它支持LDA、Word2Vec等模型,结合jieba分词可有效实现文本预处理、主题建模与语义分析,适用于新闻分类、信息检索等任务,高效且易于扩展。
272 17
|
2月前
|
关系型数据库 Apache 微服务
《聊聊分布式》分布式系统基石:深入理解CAP理论及其工程实践
CAP理论指出分布式系统中一致性、可用性、分区容错性三者不可兼得,必须根据业务需求进行权衡。实际应用中,不同场景选择不同策略:金融系统重一致(CP),社交应用重可用(AP),内网系统可选CA。现代架构更趋向动态调整与混合策略,灵活应对复杂需求。
|
3月前
|
数据采集 弹性计算 Kubernetes
单机扛不住,我把爬虫搬上了 Kubernetes:弹性伸缩与成本优化的实战
本文讲述了作者在大规模爬虫项目中遇到的挑战,包括任务堆积、高失败率和成本失控。通过将爬虫项目迁移到Kubernetes并使用HPA自动伸缩、代理池隔离和Redis队列,作者成功解决了这些问题,提高了性能,降低了成本,并实现了系统的弹性伸缩。最终,作者通过这次改造学到了性能、代理隔离和成本控制的重要性。
135 2
单机扛不住,我把爬虫搬上了 Kubernetes:弹性伸缩与成本优化的实战
|
4月前
|
数据采集 存储 缓存
构建“天气雷达”一样的网页监控系统
证券级信息精准监测系统,具备雷达感知能力,实时探测网页变动,快速响应公告更新,助力投资决策抢占先机。
174 0
构建“天气雷达”一样的网页监控系统
|
8天前
|
数据采集 Java 调度
从10个协程到1000个协程:性能下降的背后究竟发生了什么?
本文探讨了异步程序中常见的误解“协程越多越快”,并通过一个实际的异步抓取学术论文元数据的例子来阐明这一点。文章首先解释了协程过多可能导致的效率低下的原因,包括事件循环的调度限制、网络瓶颈、代理并发限制以及Python协程切换的成本。接着,文章提供了一个使用代理、从DOAJ抓取开放论文元数据并存入SQLite数据库的完整异步代码示例,并强调了合理设置并发量的重要性。最后,文章总结了初学者在编写异步抓取程序时容易遇到的几个陷阱,并提供了相应的解决方案。
|
3月前
|
数据采集 JSON 自然语言处理
超越传统XPath:用LLM理解复杂网页信息
本文深入探讨网页信息抽取技术的演进,从传统 XPath/CSS 结构匹配,到结合 LLM(大语言模型)的语义理解方法。分析了旧技术在动态渲染、结构变化和语义识别方面的局限,并通过架构图、实验数据和示例代码展示 LLM 在新闻、电商、社交等复杂场景中的高效应用。同时强调爬虫代理等基础设施的重要性,为信息抓取提供稳定网络环境。
142 1
超越传统XPath:用LLM理解复杂网页信息
|
1月前
|
数据采集 人工智能 NoSQL
抓取任务队列精简化:延迟队列、优先级队列与回退策略设计
描述了作者在处理抓取任务队列时遇到的挑战,包括任务堆积、线程阻塞和超时重试问题。通过引入延迟队列、优先级队列和回退策略,作者成功优化了任务调度策略,提高了系统的稳定性和资源利用率。核心代码示例展示了如何使用Redis实现延迟和优先级队列,以及如何执行任务和处理失败重试。最终,系统变得更加智能和高效,实现了更好的调度和资源管理。
116 1