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

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

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

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

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

  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 的分片和查询效率,再推广到亿级规模,避免 "纸上谈兵" 导致的线上故障。

相关推荐
Marktowin5 小时前
Mybatis-Plus更新操作时的一个坑
java·后端
赵文宇5 小时前
CNCF Dragonfly 毕业啦!基于P2P的镜像和文件分发系统快速入门,在线体验
后端
程序员爱钓鱼5 小时前
Node.js 编程实战:即时聊天应用 —— WebSocket 实现实时通信
前端·后端·node.js
Libby博仙6 小时前
Spring Boot 条件化注解深度解析
java·spring boot·后端
源代码•宸6 小时前
Golang原理剖析(Map 源码梳理)
经验分享·后端·算法·leetcode·golang·map
小周在成长6 小时前
动态SQL与MyBatis动态SQL最佳实践
后端
瓦尔登湖懒羊羊7 小时前
TCP的自我介绍
后端
小周在成长7 小时前
MyBatis 动态SQL学习
后端
子非鱼9217 小时前
SpringBoot快速上手
java·spring boot·后端