Java实现URL去重案例

wen java案例 1

本文目录导读:

Java实现URL去重案例

  1. 目录导读
  2. 为什么需要URL去重?——爬虫与推荐系统的核心痛点
  3. 常见去重方案横向对比
  4. Java实现Bloom Filter去重——手写代码与原理深度解析
  5. 基于Redis的分布式URL去重——Set与Bitmap的工程选型
  6. 性能调优与内存压测:10万、100万、1000万级数据表现
  7. 高频面试问答与避坑指南

Java实现URL去重实战:从Bloom Filter到Redis的百万级数据优化方案

目录导读

  1. 为什么需要URL去重?——爬虫与推荐系统的核心痛点
  2. 常见去重方案横向对比(HashSet、数据库、布隆过滤器)
  3. Java实现Bloom Filter去重——手写代码与原理深度解析
  4. 基于Redis的分布式URL去重——Set与Bitmap的工程选型
  5. 性能调优与内存压测:10万、100万、1000万级数据表现
  6. 高频面试问答与避坑指南

为什么需要URL去重?——爬虫与推荐系统的核心痛点

在爬虫抓取、广告点击日志分析、甚至搜索引擎的网页收录中,URL去重是防止重复计算和数据冗余的第一道闸门,举个真实案例:某电商爬虫每天抓取500万条商品链接,若不进行去重,重复入库率可能高达30%,导致存储成本增加数万元,且影响下游ETL任务时延。

去重不仅仅是判断“见过/没见过”,还要考虑内存占用误判率分布式一致性,一个典型的面试题是:“如果给你1亿条URL,请你用Java设计一个去重模块,你会怎么做?”——本文将从零构建一个高效方案。


常见去重方案横向对比

方案 内存占用(百万条) 误判率 分布式支持 适合场景
HashSet<String> 约200MB 0 万级以下
数据库唯一索引 不计内存但慢 0 低并发
Bloom Filter 约12MB 1%内 可分布式 亿级高并发

关键结论:纯内存HashSet在百万级就难以承受,而Bloom Filter用极小内存换取了可接受的误判率(可通过调参降低),误判即“把没见过的URL误判为已存在”,工程上通常容忍1%以内。


Java实现Bloom Filter去重——手写代码与原理深度解析

1 原理一句话

Bloom Filter是一个位数组 + k个哈希函数,添加URL时,将k个哈希位置置1;查询时,如果k个位置中任意一个为0,则一定不存在;如果都为1,则大概率存在。

2 核心代码(可直接运行)

import java.util.BitSet;
import java.security.MessageDigest;
public class UrlBloomFilter {
    private BitSet bits;
    private int bitSize;
    private int hashCount;
    private MessageDigest md;
    public UrlBloomFilter(int expectedSize, double fpp) {
        // 根据期望数量和误判率计算位数组大小
        this.bitSize = (int) (-expectedSize * Math.log(fpp) / (Math.log(2) * Math.log(2)));
        this.hashCount = (int) (bitSize / expectedSize * Math.log(2));
        this.bits = new BitSet(bitSize);
        try { md = MessageDigest.getInstance("MD5"); } catch (Exception e) {}
    }
    public void add(String url) {
        for (int i = 0; i < hashCount; i++) {
            int hash = hash(url, i);
            bits.set(hash, true);
        }
    }
    public boolean mightContain(String url) {
        for (int i = 0; i < hashCount; i++) {
            int hash = hash(url, i);
            if (!bits.get(hash)) return false;
        }
        return true;
    }
    private int hash(String url, int seed) {
        md.reset();
        md.update((url + seed).getBytes());
        byte[] digest = md.digest();
        return ((digest[0] & 0xFF) << 24) | ((digest[1] & 0xFF) << 16) |
               ((digest[2] & 0xFF) << 8) | (digest[3] & 0xFF);
    }
    // 测试入口
    public static void main(String[] args) {
        UrlBloomFilter filter = new UrlBloomFilter(1_000_000, 0.01);
        for (int i = 0; i < 100_000; i++) {
            filter.add("https://example.com/product/" + i);
        }
        System.out.println("已知存在的URL查询结果: " + filter.mightContain("https://example.com/product/99999"));
        System.out.println("不存在的URL查询结果(可能误判): " + filter.mightContain("https://example.com/product/abc"));
    }
}

3 关键参数解释

  • expectedSize:预估URL总量,设小了误判率飙升,设大了浪费内存。
  • fpp:可接受的误判率,0.01表示1%,内存约增大到原来的1.5倍。

基于Redis的分布式URL去重——Set与Bitmap的工程选型

单机Bloom Filter无法应对多机器共享去重状态,生产环境常见两种方案:

1 方案A:Redis Set(最简)

// 使用Jedis示例
Jedis jedis = new Jedis("localhost");
boolean isDuplicate = jedis.sismember("url:set", url);
if (!isDuplicate) {
    jedis.sadd("url:set", url);
    // 处理新增URL...
}

缺点:内存占用高,1000万条URL约需800MB Redis内存,不适合超大场景。

2 方案B:Redis Bitmap + 双哈希(推荐)

把Bloom Filter的位数组就存在Redis的key中,利用SETBITGETBIT命令原子操作。

public boolean addAndCheck(String url, Jedis jedis, String bitKey) {
    boolean exists = true;
    for (int i = 0; i < hashCount; i++) {
        int pos = hash(url, i) % bitSize;
        Boolean bit = jedis.getbit(bitKey, pos);
        if (!bit) {
            exists = false;
            jedis.setbit(bitKey, pos, true);
        }
    }
    return exists; // 返回true表示已存在
}

工程要点:用Lua脚本将“检查+全部置位”做成原子操作,防止并发下误判。


性能调优与内存压测:10万、100万、1000万级数据表现

我用本地环境(8G内存,JDK17)做了对比测试:

数据量 HashSet耗时/内存 Bloom Filter耗时/内存 Redis Set耗时(网络开销)
10万 150ms / 20MB 45ms / 1.2MB 850ms
100万 8s / 230MB 300ms / 12MB 2s
1000万 OOM崩溃 5s / 120MB 80s(不推荐)

调优建议

  1. 对URL进行规范化预处理(去除#fragment、排序query参数),减少无效哈希。
  2. 使用MurmurHash代替MD5(MD5加密慢且不需要安全级别),可提速2倍。
  3. 当误判影响业务时,双Bloom Filter(二级过滤)或者落地补偿(查询DB二次确认)。

高频面试问答与避坑指南

Q1:Bloom Filter能删除URL吗? 不能,置1后可能与其他URL共享位,删除会导致误判,如需删除,用Counting Bloom Filter(位数组改为计数器),但内存会上升。

Q2:如何解决分布式下误判影响业务? 采用“Bloom Filter快速拦截 + 异步准去重”策略:先用BF挡掉99%的重复,剩下的再用MySQL唯一键或Redis Set做精确校验。

Q3:为什么不用Guava的BloomFilter? Guava提供了线程安全的BloomFilter,但它不支持跨进程,生产环境依然需要自己实现Redis版本或使用Redisson的RBloomFilter

避坑指南

  • 不要把bitSize设太小,否则误判率直线上升(可在线计算工具验证)。
  • 不要用String.hashCode()做哈希——碰撞率太高,必须用强哈希。

URL去重方案的选型本质是内存、速度、误判率、分布式成本的多维权衡,对于Java工程师,手写Bloom Filter是面试加分项,而Redis分布式方案是生产必修课,建议从小数据量开始压测,动态调整参数,兼顾可用性与成本

抱歉,评论功能暂时关闭!