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

简介: 本文探讨了高并发数据采集中避免重复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 + 持久化,正好是一种在速度、准确和可恢复性之间的平衡。

相关文章
|
5月前
|
数据采集 人工智能 NoSQL
抓取任务队列精简化:延迟队列、优先级队列与回退策略设计
描述了作者在处理抓取任务队列时遇到的挑战,包括任务堆积、线程阻塞和超时重试问题。通过引入延迟队列、优先级队列和回退策略,作者成功优化了任务调度策略,提高了系统的稳定性和资源利用率。核心代码示例展示了如何使用Redis实现延迟和优先级队列,以及如何执行任务和处理失败重试。最终,系统变得更加智能和高效,实现了更好的调度和资源管理。
227 1
|
8月前
|
数据采集 存储 缓存
构建“天气雷达”一样的网页监控系统
证券级信息精准监测系统,具备雷达感知能力,实时探测网页变动,快速响应公告更新,助力投资决策抢占先机。
340 0
构建“天气雷达”一样的网页监控系统
|
6月前
|
数据采集 监控 NoSQL
优化分布式采集的数据同步:一致性、去重与冲突解决的那些坑与招
本文讲述了作者在房地产数据采集项目中遇到的分布式数据同步问题,通过实施一致性、去重和冲突解决的“三板斧”策略,成功解决了数据重复和同步延迟问题,提高了系统稳定性。核心在于时间戳哈希保证一致性,URL归一化和布隆过滤器确保去重,分布式锁解决写入冲突。
314 2
 优化分布式采集的数据同步:一致性、去重与冲突解决的那些坑与招
|
7月前
|
消息中间件 数据采集 NoSQL
秒级行情推送系统实战:从触发、采集到入库的端到端架构
本文设计了一套秒级实时行情推送系统,涵盖触发、采集、缓冲、入库与推送五层架构,结合动态代理IP、Kafka/Redis缓冲及WebSocket推送,实现金融数据低延迟、高并发处理,适用于股票、数字货币等实时行情场景。
1019 3
秒级行情推送系统实战:从触发、采集到入库的端到端架构
|
SQL 运维 监控
高效定位 Go 应用问题:Go 可观测性功能深度解析
为进一步赋能用户在复杂场景下快速定位与解决问题,我们结合近期发布的一系列全新功能,精心梳理了一套从接入到问题发现、再到问题排查与精准定位的最佳实践指南。
|
存储 机器人 计算机视觉
接入了支付宝账户体系的旅客入住无人酒店解决方案
本书第一章介绍了一套复杂的无人酒店云平台系统,涵盖核心云平台、容灾备份、数据存储、旅客服务、嵌入式设备管理、远程人工坐席、综合业务处理、问题解决、智慧监控安防等多个子系统。各平台协同工作,确保从旅客入住、服务请求、智能设备控制到退房的全流程高效运作,并与外部机构实时对接,保障数据安全与应急响应。系统通过人脸识别、语音交互等技术,提供个性化服务,同时具备严格的实名验证机制,确保合规性与安全性。
|
存储 人机交互 语音技术
基于RT-Thread的智能家居助手
一、项目简介 智能家居助手主要基于RT-Thread开发的,该系统主要分为语音子系统,环境监测子系统,智能控制子系统,智能网关子系统,音乐播放器,云端以及应用软件七大部分。语音子系统可通过语音进行人机交互来控制家电设备。环境监测子系统为智能家居提供环境信息输入,实时监测室内的环境信息。智能控制子系统为智能家居提供控制接口,用户可根据实际需求来控制家电设备。 智能网关是整个系统的核心和枢纽,为整个智能家居提供网络,同时与云平台进行交互,不断更新室内信息,实时将数据上传至云端,用户就能在远程进行查室内的各种环境信息,实时掌握家中的最新动态。音乐播放器为用户提供音乐服务。云端部分为智能家居系统云
384 6
|
存储 物联网 计算机视觉