多个 worker 共享一份“已经抓过”的集合,最直接的写法是先查再加。但检查和加入之间有空隙。这篇用 8 个进程抢同一批 URL,量出这个空隙的代价,再比较几种去重结构的内存、吞吐和大 key 的清理问题。
先 SISMEMBER 再 SADD:3 次实验抓取次数 [17472, 16433, 18098](不同 URL 一共 3000 个,多出来的是重复抓取) 只用 SADD 的返回值(1 = 新加入):3 次实验抓取次数 [3000, 3000, 3000](不同 URL 一共 3000 个,多出来的是重复抓取)
SISMEMBER 和 SADD 是两条命令,两条之间别的 worker 也能通过检查。8 个步调一致的 worker 把 3000 个 URL 抓了一万七千多次。SADD 本身是原子的,它的返回值(1 表示新加入)就是“我是第一个”,只用返回值,3000 个 URL 恰好 3000 次。
URL 平均 64 字节
SET:完整 URL 字符串 30.5 MiB,平均每个 URL 106.5 字节
编码 hashtable
SET:blake2b 8 字节摘要 12.2 MiB,平均每个 URL 42.5 字节
SET:md5 16 字节摘要 16.7 MiB,平均每个 URL 58.5 字节
位图 Bloom(每个元素 9.6 位,7 个哈希) 0.6 MiB,平均每个 URL 1.9 字节
对 100000 个没加入过的 URL 查询,Bloom 误判 1064 个 = 1.06%(理论值约 1%)
HyperLogLog(只能数个数,不能判断是否见过) 0.0 MiB,平均每个 URL 0.1 字节
PFCOUNT = 299568(真实 300000,误差 0.14%)
8 字节摘要在 300000 个 URL 里出现碰撞的概率 ≈ n²/2^65 = 2.4e-09(这是算出来的,没有实测)30 万个平均 64 字节的 URL,SET 存完整字符串约 30.5 MiB(每个 106.5 字节),存 8 字节摘要 12.2 MiB,存 16 字节 md5 16.7 MiB。Bloom 位图只要 0.6 MiB,代价是有误判(实测 1.06%,即约 1% 的新 URL 会被当成见过而漏抓)。HyperLogLog 只能估计个数。8 字节摘要的碰撞概率是算出来的(30 万个时约 2.4e-09),没有实测。
逐个 SADD(一次往返一个,只跑 2 万个) 中位 22109 个/秒(21431–22462) pipeline,每批 100 个(每个 SADD 都返回新/旧) 中位 185879 个/秒(181536–198964) pipeline,每批 1000 个 中位 262806 个/秒(262218–262929) 一条 SADD 带 1000 个成员(只返回新加入的个数) 中位 820929 个/秒(791220–871517)
逐个 SADD 一次往返一个,约 2.2 万个/秒;pipeline 每批 1000 个约 26.3 万个/秒并保留每个返回值;一条 SADD 带多个成员更快(82 万个/秒),但只返回新加入的总数,不知道是哪几个。
先 GETBIT 全部检查,没见过再 SETBIT:3 次实验抓取次数 [9590, 9713, 9688](3000 个不同 URL;位图很大,Bloom 自身的误判可以忽略) 在 MULTI/EXEC 里对 7 个位置 SETBIT,用返回的旧值判断:3 次实验抓取次数 [3000, 3000, 3000](3000 个不同 URL;位图很大,Bloom 自身的误判可以忽略)
Bloom 的“先检查再置位”同样有空隙(抓了九千多次)。SETBIT 会返回旧值:在 MULTI/EXEC 里对 k 个位置 SETBIT,旧值全是 1 才算见过,一次完成测试和置位(3000 次)。
连续 3 次 SET NX EX 2 的返回:[True, None, None];等 2.2 秒后再 SET NX:True(返回 True 表示拿到了,None 表示已存在) 10 万个 URL:每个 URL 一个带过期时间的 key(SET NX EX):平均每个 84 字节;全部放进一个 SET(8 字节摘要):平均每个 37 字节
只想“多久之内不重复抓”时,每个 URL 一个 key,用 SET NX EX:第一次拿到,之后返回空,过期后又能拿到。内存比单个 SET 大(每个 84 字节对 37 字节)。
DEL:命令本身返回用了 242 ms;期间 PING 最大延迟 242 ms(共 373 次,中位 0.11 ms) UNLINK:命令本身返回用了 0 ms;期间 PING 最大延迟 1 ms(共 377 次,中位 0.12 ms)
Redis 单线程执行命令。DEL 一个 200 万成员的 SET 用了 242 ms,别的连接的 PING 也被挡了这么久;UNLINK 立刻返回,PING 最大 1 ms。去重集合会越长越大,清理时用 UNLINK,或按域名、日期拆分。