亿级别黑名单与短链接:该选什么数据结构?从需求到落地的技术选型指南

亿级别黑名单与短链接:该选什么数据结构?从需求到落地的技术选型指南

面对亿级规模的数据(如黑名单、短链接),技术选型的核心矛盾永远是 "查询效率 " 与 "空间占用" 的平衡 ------ 用数组存亿级数据会导致查询卡死,用普通哈希表会耗尽内存,用树结构会牺牲查询速度。真正合适的数据结构,必须贴合场景的核心需求:黑名单需要 "快速判断存在性 + 省空间",短链接需要 "双向快速映射 + 短码唯一"。本文将针对这两个高频场景,拆解需求、对比方案、给出落地建议,帮你避开 "选对结构却用错方式" 的坑。

一、先统一认知:亿级数据的选型核心标准

无论黑名单还是短链接,选型前需先明确三个不可妥协的标准,这是区别于 "万级 / 百万级数据" 的关键:

  1. 查询效率 :核心操作(如 "判断 IP 是否在黑名单""短码转长链接")必须达到毫秒级甚至微秒级,否则会成为系统瓶颈(如网关拦截黑名单 IP 时拖慢所有请求);
  1. 空间效率:亿级数据不能占用过多内存 / 磁盘(如 1 亿条数据若用哈希表存,可能占用几十 GB 内存,远超单机承载能力);
  1. 工程可行性:数据结构需适配分布式场景(单机存不下亿级数据),且支持动态插入 / 删除(如黑名单需定期新增,短链接需持续生成)。

二、亿级别黑名单:首选「布隆过滤器 + 二次校验」,空间与效率的最优解

黑名单的核心需求是 "快速判断一个元素是否在集合中"(如 IP / 手机号 / 设备号是否在黑名单),次要需求是 "低空间占用",允许 "极低误判"(误判可通过二次校验修正)。

1. 黑名单的核心需求拆解

需求维度 具体要求 为什么重要?
存在性判断效率 单次判断耗时<1ms(网关 / 接口拦截场景,不能拖慢主流程) 若判断需 10ms,1 万 QPS 的接口会因黑名单校验延迟 100ms
空间占用 1 亿条数据占用内存<1GB(单机内存通常为 16-64GB,需预留其他业务空间) 普通哈希表存 1 亿 IP 需 2-3GB,布隆过滤器仅需 250MB 左右
误判与漏判 允许≤0.01% 的误判率(误判可修正),绝对不允许漏判(黑名单内元素不能判为 "不在集合") 漏判会导致黑名单失效,误判可通过查数据库修正
动态性 支持批量插入(如每天导入 10 万条新黑名单),删除需求低(黑名单通常长期有效) 电商大促前需快速导入恶意 IP 黑名单,不能停服务

2. 可选数据结构对比:为什么布隆过滤器是最优解?

直接对比四种常见数据结构,优劣一目了然:

数据结构 存在性判断效率 1 亿数据空间占用 误判率 支持删除 适配性结论
数组 O (n)(遍历) 低(1 亿 IP 约 400MB) 0 支持 ❌ 完全不适用:遍历 1 亿数据需秒级,无法满足低延迟
哈希表(HashMap) O(1) 极高(2-3GB,含负载因子冗余) 0 支持 ❌ 空间占用过高:单机存 1 亿数据会导致内存溢出,分布式哈希表(DHT)复杂度高
红黑树 O (logn)(约 27 次比较) 中(1-2GB,含树结构开销) 0 支持 ❌ 效率不足:log₂(1e8)≈27,判断耗时比布隆过滤器高 10 倍以上
布隆过滤器 O (k)(k=3-5 次哈希) 极低(约 250MB,0.01% 误判率) 可控制(0.001%-1%) 普通版不支持,计数版支持 ✅ 完美适配:空间仅为哈希表 1/10,判断效率接近 O (1),误判可修正

3. 落地方案:布隆过滤器 + 持久化存储(Redis/LevelDB)

布隆过滤器的短板是 "可能误判"(判为 "存在" 的元素实际不在集合中),需搭配 "持久化存储" 做二次校验,形成 "快速过滤 + 精确校验" 的闭环。

(1)核心原理:布隆过滤器如何实现 "省空间 + 快查询"?

布隆过滤器本质是一个 "二进制数组 + 多个哈希函数":

  • 插入元素时:用 3-5 个哈希函数将元素映射到数组的 3-5 个位置,将这些位置的二进制位设为 1;
  • 判断元素时:用相同哈希函数计算映射位,若所有位都是 1 → 元素 "可能存在"(需二次校验);若有任何位是 0 → 元素 "绝对不存在"(直接返回)。

空间计算公式(关键!):

所需二进制位数 = 1.44 × 数据量(N) × log₂(1 / 误判率(ε))

以 1 亿数据(N=1e8)、0.01% 误判率(ε=0.0001)为例:

1.44 × 1e8 × log₂(10000) ≈ 1.44×1e8×14 ≈ 2016 万字节 ≈ 252MB,仅需 250MB 左右内存!

(2)完整流程:从初始化到查询
  1. 初始化阶段
    • 从持久化存储(如 MySQL 黑名单表)批量加载 1 亿条黑名单数据,插入布隆过滤器(如用 Guava 的 BloomFilter,或 Redis 的 BloomFilter 插件);
    • 将 1 亿条黑名单数据同步到 Redis(Key 为黑名单元素,Value 为 "1",标记存在),用于二次校验。
  1. 查询阶段(以 "判断 IP 是否在黑名单" 为例):
typescript 复制代码
// 伪代码:黑名单查询流程
public boolean isInBlacklist(String ip) {
    // 第一步:布隆过滤器快速过滤(耗时≈0.1ms)
    if (!bloomFilter.mightContain(ip)) {
        return false; // 绝对不在黑名单,直接返回
    }
    // 第二步:Redis二次校验(耗时≈1ms,解决误判)
    Boolean isExist = redisTemplate.hasKey("blacklist:ip:" + ip);
    return Boolean.TRUE.equals(isExist); // 精确结果
}
  1. 插入阶段(新增黑名单元素):
    • 先将元素插入 Redis(确保后续二次校验能查到);
    • 再将元素插入布隆过滤器(确保后续快速过滤能命中);
    • 批量插入时(如每天 10 万条),用 Redis Pipeline 和布隆过滤器批量插入 API,避免单条操作耗时累积。
(3)进阶优化:支持删除(可选)

普通布隆过滤器不支持删除(删除一个位会影响其他元素),若业务需要 "定期清理黑名单"(如临时封禁 IP7 天后解除),可改用计数布隆过滤器(Counting Bloom Filter):

  • 原理:将二进制位改为 "4 位计数器",插入时计数器 + 1,删除时计数器 - 1;
  • 代价:空间占用变为原来的 4 倍(1 亿数据约 1GB),但仍远低于哈希表;
  • 选型建议:临时黑名单用计数布隆过滤器,永久黑名单用普通布隆过滤器。

三、亿级别短链接:首选「哈希表 + 自增 ID 编码」,双向映射的高效方案

短链接的核心需求是 "双向快速映射"(短码→长链接:用户点击跳转;长链接→短码:生成短链接去重),且需保证 "短码唯一"(避免一个短码对应多个长链接),查询需毫秒级。

1. 短链接的核心需求拆解

需求维度 具体要求 为什么重要?
双向映射效率 短码→长链接<1ms(高频,用户跳转场景);长链接→短码<10ms(低频,生成短链接去重) 用户点击短链接后若延迟超过 100ms,会感知卡顿
短码唯一性 每个长链接对应唯一短码,每个短码对应唯一长链接(绝对不允许冲突) 短码冲突会导致用户跳转到错误页面,引发投诉
短码长度 短码长度≤8 位(如t.cn/abc123,便于传播和记忆) 10 位以上的短码失去 "短" 的意义,用户不愿转发
分布式支持 支持亿级数据分片存储(单机 Redis 存不下 1 亿条映射),且能水平扩展 短链接服务日均生成 100 万条,年增长 3.6 亿条

2. 可选数据结构对比:为什么哈希表是唯一解?

短链接的 "双向映射" 需求,决定了哈希表是最优选择 ------ 其他结构要么无法兼顾双向效率,要么不支持短码唯一性。

数据结构 短码→长链接效率 长链接→短码效率 1 亿数据空间占用 短码唯一性保障 适配性结论
数组 O (1)(短码转索引) O (n)(遍历去重) 低(1 亿条约 4GB) 需手动维护 ❌ 不适用:长链接去重需遍历,亿级数据需小时级
红黑树 / B + 树 O (logn)(约 27 次比较) O(logn) 中(5-8GB) 支持 ❌ 效率不足:跳转查询比哈希表慢 10 倍,且短链接无需有序查询(如范围查询)
基数树(Trie) O (k)(k = 短码长度,6-8 次) O (m)(m = 长链接长度,约 50 次) 低(3-5GB) 支持 ❌ 长链接效率低:长链接转短码需遍历字符串,比哈希表慢 5 倍
哈希表 O(1) O(1) 中(8-10GB) 需手动去重 ✅ 唯一解:双向映射均为 O (1),配合去重逻辑可保障短码唯一,空间可通过分布式拆分

3. 落地方案:分布式哈希表(Redis Cluster)+ 自增 ID 编码

亿级短链接需解决 "分布式存储" 和 "短码生成" 两个核心问题,推荐用 "Redis Cluster 做哈希表"+"自增 ID 转 62 进制生成短码" 的方案。

(1)核心设计:双哈希表映射

用两个哈希表分别实现 "短码→长链接" 和 "长链接→短码",确保双向快速查询:

  • 哈希表 1(短码→长链接) :Key 为短码(如abc123),Value 为长链接(如xxx.com/long-url?pa...),用于用户跳转(高频,占 90% 以上请求);
  • 哈希表 2(长链接→短码) :Key 为长链接的 MD5 哈希值(避免长 Key 占用过多空间),Value 为短码,用于生成短链接时去重(低频,占 10% 以下请求)。

为什么用 MD5 哈希长链接?

若长链接过长(如超过 100 字符),直接作为 Key 会占用大量 Redis 内存(1 亿条长链接约 5GB),用 MD5 哈希后生成 32 位字符串,Key 长度固定,空间占用减少 50% 以上(且 MD5 碰撞概率极低,可忽略)。

(2)短码生成:自增 ID+62 进制转换(保障唯一)

短码需满足 "短 + 唯一",最成熟的方案是 "自增 ID 转 62 进制"------ 用 62 个字符(0-9、a-z、A-Z)表示数字,6 位短码可支持 62⁶≈568 亿条数据,完全满足亿级需求。

生成流程(以 "用户提交长链接生成短码" 为例):

ini 复制代码
// 伪代码:短码生成流程
public String generateShortUrl(String longUrl) {
    // 第一步:长链接去重(查哈希表2)
    String longUrlMd5 = DigestUtils.md5DigestAsHex(longUrl.getBytes());
    String existShortCode = redisTemplate.opsForValue().get("shorturl:long2short:" + longUrlMd5);
    if (existShortCode != null) {
        return existShortCode; // 已存在,直接返回短码
    }
    // 第二步:生成全局唯一自增ID(用Redis INCR,支持分布式)
    Long id = redisTemplate.opsForValue().increment("shorturl:id:seq"); // 从1开始自增
    // 第三步:自增ID转62进制,生成短码
    String shortCode = idToBase62(id); // 如ID=123456 → 短码=e9t
    // 第四步:双哈希表插入(用Redis Pipeline批量操作,减少网络开销)
    RedisPipeline pipeline = redisTemplate.pipeline();
    // 插入哈希表1:短码→长链接(设置过期时间,如3年)
    pipeline.opsForValue().set("shorturl:short2long:" + shortCode, longUrl, 3, TimeUnit.YEARS);
    // 插入哈希表2:长链接MD5→短码
    pipeline.opsForValue().set("shorturl:long2short:" + longUrlMd5, shortCode, 3, TimeUnit.YEARS);
    pipeline.execute();
    return shortCode;
}
// 辅助函数:自增ID转62进制
private String idToBase62(Long id) {
    char[] chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
    StringBuilder sb = new StringBuilder();
    while (id > 0) {
        int remainder = (int) (id % 62);
        sb.append(chars[remainder]);
        id = id / 62;
    }
    // 补全6位(不足6位前面补0,如ID=123 → 短码=0001e9)
    while (sb.length() < 6) {
        sb.insert(0, '0');
    }
    return sb.toString();
}
(3)分布式存储:Redis Cluster 分片

1 亿条短链接约占用 8-10GB 内存,单机 Redis(默认最大内存 16GB)虽能存下,但需预留扩展空间(如后续增长到 5 亿条),推荐用 Redis Cluster 做分片:

  • 分片规则:按短码的 CRC16 哈希值分配到 Redis Cluster 的 16384 个槽中,不同槽对应不同节点;
  • 持久化:开启 AOF(append-only file)持久化,确保节点故障后数据不丢失(RDB 可作为定时备份);
  • 性能优化:短码查询走 Redis 主节点,避免从节点延迟导致的 "短码查不到" 问题;长链接去重查询可走从节点,分担主节点压力。

四、总结:场景决定选型,工程优化决定落地效果

亿级数据的选型没有 "万能数据结构",只有 "适配场景的最优解",关键是抓住核心需求,再用工程手段弥补数据结构的短板:

场景 核心数据结构 核心优势 工程优化关键
亿级黑名单 布隆过滤器 + Redis 空间占用极小(250MB / 亿级),查询快(≈0.1ms) 二次校验解决误判,计数版支持删除,批量插入提效
亿级短链接 哈希表(Redis Cluster)+ 62 进制编码 双向映射 O (1),短码唯一可控,支持分布式 长链接 MD5 压缩省空间,自增 ID 保障唯一,分片存储扩容量

最后一个建议:选型前先做 "小体量验证"------ 用 100 万条测试数据模拟布隆过滤器的空间和误判率,用 10 万条短链接测试 Redis Cluster 的分片和查询效率,再推广到亿级规模,避免 "纸上谈兵" 导致的线上故障。

相关推荐
用户68545375977695 小时前
📬 分布式消息队列:三大终极难题!
后端
调试人生的显微镜5 小时前
Wireshark抓包教程:JSON和HTTPS抓取
后端
间彧6 小时前
Java CompletableFuture详解与应用实战
后端
seanmeng20226 小时前
在EKS上部署ray serve框架
后端
Java水解6 小时前
Go基础:Go语言中 Goroutine 和 Channel 的声明与使用
java·后端·面试
用户41429296072396 小时前
一文读懂 API:连接数字世界的 “隐形桥梁”
后端
PFinal社区_南丞6 小时前
别再盲接 OTel:Go 可观察性接入的 8 个大坑
后端