在爬虫工程里,“增量”这两个字意味着你不是把整个互联网拉一遍,而是每天定期去目标站点看有没有新东西。真正做过的人都知道,增量最难的不是抓取,而是判断“这条数据我是不是已经拿过了”。这个判断一旦做错,轻则重复抓取浪费带宽,重则把对方站点压垮、把自己的IP送进黑名单,甚至因为重复写入把数据库撑爆。而Redis之所以能在定时增量爬虫里成为去重的标准答案,靠的不是它快,而是它把“数据结构”和“过期策略”这两件事做到了恰到好处,既能当集合用,又能当缓存用,还能用一套机制同时搞定去重和资源回收。这篇文章我就把自己在几个真实爬虫项目里用Redis做增量去重的完整思路、配置细节和踩坑记录整理出来,给正在做定时抓取、增量更新任务的朋友一个可以直接落地的参考。
1. 定时增量爬虫为什么偏偏选中Redis做去重
先说结论:Redis在增量去重这个场景里几乎是不可替代的,原因不光是快,更在于它的数据结构天生贴合“判断是否见过某个东西”这个需求。而且定时任务跑起来之后,Redis既能当运行时的去重仓库,又能当任务队列,还能当状态存储,一个进程解决三件事。
1.1 SADD天然就是“见过没有”的答案
在增量爬虫里,核心操作就一个:拿到一个URL或者一条数据指纹,问Redis“这个东西我来过没有”,如果没来过就处理,如果来过了就跳过。这个操作在Redis里对应的是SADD和SISMEMBER,用集合去重语义上完全匹配。SADD在执行的时候,如果元素已经存在,返回0;如果不存在,返回1。就凭这个返回值,一个命令同时完成了“判断”和“写入”两件事,不需要额外的GET然后SET两步操作,也不用担心并发下两个进程同时判定“没来过”导致重复抓取。
有人可能会说,用MySQL查一下不也行吗?行,但你要考虑定时任务的执行频率。假如一个任务每5分钟跑一轮,每轮扫描5000个链接,那就意味着每秒要查十几次数据库,如果链接多、并发高,数据库的连接数和查询延迟会被拉高,而Redis的SISMEMBER在10万级成员的情况下响应时间仍然是亚毫秒级,这个差距在定时任务里就是“能跑”和“跑得动”的区别。
1.2 同一个Redis还能顺手干别的活
增量爬虫不是只需要去重,还需要调度。定时任务触发后,要把一批待抓取的URL分发到多个worker上,这个时候Redis的List结构(LPUSH/RPOP)或者Stream结构就能直接当任务队列用。去重用Set,队列用List,状态标记用String,用一个Redis实例把分布式的几个组件全串联起来。我自己有个项目就是一台2核4G的小服务器上跑着定时触发器、三个worker进程和一个Redis,整个系统非常轻,运维成本极低,换做用MySQL做去重加上用消息队列做调度,至少得部署两三个中间件。
1.3 定时增量场景对Redis的独特依赖
定时增量爬虫有个特点:每一轮任务之间有间隔,要么是小时级,要么是天级。这就意味着每一轮任务开始的时候,上一轮产生的临时数据其实已经没用了,比如上一轮的任务队列、上一轮的临时状态标记,这些如果不清理,Redis内存就会被慢慢占满。Redis提供的过期机制和SCAN清理模式,恰好就是为这种“周期性产生临时数据”的场景设计的。你要是用MySQL,你还要自己去写定时清理的脚本去删除过期记录,而Redis的EXPIRE一个命令就把这个事安排明白了。
2. 去重机制设计:从URL去重到内容指纹去重
去重逻辑看起来简单,但真正做增量爬虫,你会发现“去重”分好几个层次。URL去重只是第一层,更关键的是内容层面的去重。如果对方站点的URL参数带时间戳,同一个文章每天URL都在变,只看URL根本去不了重。所以一个成熟的增量爬虫体系,至少在两个层面做去重校验。
2.1 URL层去重:最基础的过滤手段
URL去重是最简单也最直接的一层防线,适用于URL结构相对稳定、参数不随意变化的站点。逻辑就是在抓取之前,先SADD一下试试,如果返回0说明这个URL已经处理过了,直接跳过。
import redis r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True) def is_url_seen(url: str) -> bool: # SADD 返回 1 表示插入成功(之前不存在),返回 0 表示已存在 return r.sadd("spider:seen:urls", url) == 0这里有个细节值得注意:我见过很多人写去重是先SISMEMBER判断再SADD写入,两步操作在单进程下没问题,但一旦开了多进程或多机部署,两个worker可能同时判断“不存在”,然后同时去抓取同一个URL,就会造成重复。用SADD的返回值一步完成判断和写入,才是真正并发安全的做法,这也是Redis官方推荐的方式。
另一个经验是,URL在写入之前最好做一次规范化处理,比如去掉#锚点、去掉utm_开头的追踪参数、统一大小写。不然同一个页面因为参数顺序不同可能被当成两个URL,导致去重失效。我一般会写一个normalize_url函数,按站点特性做裁剪,效果立竿见影。
2.2 内容指纹去重:应对URL变脸和页面更新
很多网站的URL天生就“不安分”。例如资讯类站点在URL里带上当前时间戳或者随机数,你就算把URL存下来,下一轮抓取的时候URL又变了,URL去重直接失效。还有一种情况是列表页的URL不变,但内容变了——比如一个榜单页面每天更新排名,URL永远是那个,内容已经在变。这两种情况下,只靠URL去重要么漏数据要么重复入库,必须在内容层面做指纹。
内容指纹的思路是,对抓取下来的正文内容做哈希,把哈希值存进Redis集合作为第二层去重判断。实际操作中不是对整篇全文做哈希,因为正文里可能包含动态生成的推荐位、广告位或者无意义的空白,这些内容一变化哈希值就会变,导致误判为新内容。正确做法是先抽取正文的关键区域,比如标题、正文主体文本、发布时间,把它们拼接后做MD5或者SHA1。
import hashlib import re def content_fingerprint(title: str, publish_time: str, body_text: str) -> str: # 拼接关键字段,过滤空白格式后再哈希 raw = f"{title}|{publish_time}|{re.sub(r'\\s+', '', body_text)}" return hashlib.md5(raw.encode("utf-8")).hexdigest() # 抓取完成后做二层判断 fp = content_fingerprint(item["title"], item["publish_time"], item["text"]) if r.sadd("spider:seen:contents", fp) == 0: # 已存在,跳过入库 return这里必须说明一个原因:为什么指纹要包含发布时间?因为在有些场景里,你希望重复内容不被再次入库,但如果是文章被更新了,比如标题和正文变化了,但ID没变,你仍然希望更新数据库记录。如果把ID加进指纹,那么任何一次修改都能被捕获;如果不加ID,那就意味着“同样的标题+同样的正文”被视为重复,适合那些转载类内容的去重。这个取舍取决于你项目的需求,两种方案我都用过,各有适用的场景。
2.3 布隆过滤器:当集合成员到达千万量级
SADD方案在数据量小的时候没有任何问题,但如果你做的是全网级别的增量监控,要盯着几十万个页面每天扫描,那么几个月下来URL集合可能达到几千万甚至上亿级别。一个URL字符串平均按100字节算,一亿条URL就是10GB的数据,光内存就吃不住了。这时候就该考虑布隆过滤器。
Redis 4.0之后可以通过模块引入BF命令,也可以用redis-py的bf模块,原理就是用多个哈希函数对元素进行映射,把元素信息存储在极小的位数组里。布隆过滤器可以做到:判断“一定不存在”是百分之百准确的,判断“可能存在”存在一定误判率。放在爬虫里去重场景里,误判意味着“明明没抓过但被认为抓过了”,丢一两条数据。
# 需要安装 redis-py 及 redisbloom 模块 r.bf().create("spider:seen:bloom", errorRate=0.001, capacity=100000000) # 判断并写入 if not r.bf().add("spider:seen:bloom", url): # 返回 False 表示之前不存在,现在已加入,可以抓取 pass布隆过滤器的关键参数是errorRate和capacity。errorRate建议设成0.001到0.0001之间,太低会导致空间占用变大;capacity预估一下未来6个月到1年可能产生的URL总量,设置过小会随着插入元素增多而导致误判率急剧上升。我一般在设计阶段做成可配置项,后续数据量增长时还可以重新创建并迁移。
2.4 两级去重组合策略
在实际项目里,我不会只依赖一个去重层,而是把两层组合起来用。流程是这样:
第一轮先查URL集合,如果URL没见过,直接抓取;如果URL见过,再查内容指纹,看内容是否变化,如果内容也一致就直接放弃;如果内容指纹不同,说明页面更新了,这时候抓取并更新库,同时更新URL集合里对应的指纹信息。
这个组合的好处是兼顾性能与准确性。URL查询快,大部分重复请求可以直接拦在第一层;只有少数URL重复但内容变化的页面,才需要走第二次指纹判断。这样Redis的查询压力也是最小的,整个增量过程的重复计算量大幅下降。我自己在跑了两个月的任务里,大约92%的请求被URL层拦截,只有8%会走到指纹层。
3. 过期策略:让Redis的生命周期跟着任务走
如果说去重是Redis在增量爬虫里的“矛”,那过期策略就是它的“盾”。定时任务产生的大量临时数据,如果不加控制地堆积,最终会把内存耗尽,拖垮整个Redis实例。过期策略设计的核心思路是:让Redis里每一个key的生命周期与业务数据的有效周期保持一致。
3.1 三种典型Key的差异化过期时间
在定时增量爬虫项目里,Redis里大致会存放三类数据,每类的生命周期完全不同,不能一视同仁:
第一类是永久去重集合,比如URL去重集合和内容指纹集合。这些数据只要站点没有改版,理论上需要一直保留,所以不设置过期时间。第二类是每轮任务的临时状态,比如“本轮任务队列”“本轮抓取失败列表”“本轮正在处理的URL锁”,这类数据在任务结束后就没用了,理想情况应该设置TTL,让它们自动消失,避免下一轮任务被脏数据干扰。第三类是短期缓存,比如请求频率控制的计数器、验证码出现次数等,这类数据的生命周期往往是分钟级或小时级,需要精确的过期时间。
# 临时任务队列:30分钟后自动清理 queue_key = f"spider:queue:{round_id}" r.rpush(queue_key, *urls) r.expire(queue_key, 1800) # 频率控制计数器:每60秒重置 r.incr("spider:rate:example.com") r.expire("spider:rate:example.com", 60)3.2 定期任务与清理窗口的配合
如果只用过期时间,有一个问题:Redis的过期删除不是实时的,而是惰性删除加定期删除结合。惰性删除是指当key被访问时才检查是否过期并删除;定期删除是后台每秒执行一定次数的采样删除。这个机制的后果是,在大批量key同一时间过期时,Redis可能在短时间内占用内存未及时释放,造成内存水位暂时偏高。对于定时任务来说,我一般会让过期时间错开,不要让几百个key都在同一秒过期。
另一个经验是,在每轮任务结束之后,额外执行一次SCAN清理,把该轮产生但未设置TTL的临时key主动删除。不要用KEYS命令,它在生产环境会阻塞Redis单线程,数据量大的时候能把整个服务卡死几秒钟。用SCAN加模式匹配才是安全做法。
def clean_up_by_prefix(pattern: str): cursor = 0 while True: cursor, keys = r.scan(cursor=cursor, match=pattern, count=500) if keys: r.delete(*keys) if cursor == 0: break clean_up_by_prefix("spider:queue:*")3.3 Redis内存淘汰策略的兜底作用
即使设了过期时间,也可能因为代码bug或者异常情况导致某些key没有正确设置TTL,key不断累积。在这个时候,Redis的maxmemory-policy设置就是最后一道防线。
在爬虫场景里,我一般把淘汰策略设置为allkeys-lru,也就是当内存达到上限时,优先淘汰最近最少使用的key。这样即使有遗漏的临时key没有被及时清理,它们也会因为长时间不被访问而自然被挤出内存。相较之下noeviction策略会让写入直接报错,导致爬虫任务中断,这个在生产环境绝对不能选;volatile-lru只淘汰设置了过期时间的key,对永久集合无效,保护性不够。
# redis.conf maxmemory 512mb maxmemory-policy allkeys-lru maxmemory-samples 10maxmemory-samples是LRU近似算法的采样数量,默认是5,调高到10可以让淘汰精度更高,但会略微增加CPU开销。如果服务器内存充足,可以把这个值设得偏大以保证长期运行后的数据质量。
3.4 持久化策略:要不要AOF,怎么平衡
增量爬虫的去重数据一旦丢失,代价不只是重新抓取那么简单,还可能是重复写入数据库、把对方站点再次请求一遍。所以我强烈建议至少开启RDB快照持久化,如果任务重要,开AOF也行,但要控制落盘频率。
我的通常是:RDB每天保存一次快照,AOF开启但appendfsync设为everysec,兼顾安全性和性能。这样最坏情况丢失一秒的数据,对爬虫场景完全可以接受。在Redis 6.2及以上版本,我还习惯开启aof-use-rdb-preamble,让AOF文件以RDB格式开头,既减小文件体积又加快重启加载速度。
save 900 1 save 300 10 save 60 10000 appendonly yes appendfsync everysec aof-use-rdb-preamble yes4. 增量任务的定时与姿态:从触发器到执行器
去重和过期策略解决了Redis内部的问题,但定时增量爬虫的整体架构还需要考虑“什么时候跑”“怎么编排任务”“抓取到什么程度算一轮结束”。这块的代码虽然不难,但设计不好会在运行一段时间后暴露出各种边界问题。
4.1 用系统定时器还是内部调度器
定时任务的第一选择是系统的crontab,配合Python脚本可以做到固定间隔执行。但crontab有个问题:它只管“到点启动”,不管上一次任务是否结束。如果上一轮跑得慢,下一轮又开始,两个任务同时跑,Redis里的数据还好,但数据库写入和网络请求会出现并发冲突。
我后来更倾向于在爬虫脚本内部用schedule库或APScheduler做单进程内调度,配合一个分布式锁来保证同一时刻只有一个任务实例在运行。Redis的SETNX配合过期时间实现锁非常简单。
lock_key = "spider:lock:incremental" # 用 SET NX EX 原子操作加锁,10分钟自动释放 acquired = r.set(lock_key, "1", nx=True, ex=600) if not acquired: print("上一轮任务仍在执行,本轮跳过") return这个锁能解决绝大多数重入问题。如果任务真的超时长达10分钟,锁会自动释放,但此时上一个任务可能还在跑,新的任务又进入,还是存在并发风险。稳妥的做法是在任务心跳里续期锁,或者每轮结束显式删除锁。
4.2 增量任务的四个阶段拆解
我习惯把每轮定时任务拆成四步,分别对应不同的Redis使用模式:
第一步是发现阶段,扫描目标列表页或者数据源,提取候选URL列表。这个阶段产生的原始URL先写入一个临时队列,不直接进入去重集合,因为可能很多URL是无效的。
第二步是过滤阶段,从临时队列取出URL,执行URL规范化,然后向Redis去重集合发起判断。这一步会把大量已抓取过的URL拦截掉,产生真正需要抓取的目标列表。
第三步是抓取与解析阶段,worker并发去抓取页面,解析出正文后计算内容指纹,进行第二层去重判断,最后入库。
第四步是清理阶段,删除本轮产生的临时key,保留去重集合,更新状态计数。整个过程里Redis的Set、List、String各司其职,井水不犯河水。
4.3 失败了怎么办:失败队列与轮次补偿
定时任务的另一个常见问题是失败处理。网络抖动、目标站反爬、超时、解析异常,总会有URL抓取失败。如果失败后直接丢弃,下轮也不会再抓,就产生了数据缺口。正确的做法是专门维护一个失败集合,记录失败URL和失败次数。下一轮任务启动时可以优先从失败集合里取出重试。
def record_failure(url: str): key = f"spider:failed:{date.today()}" r.hincrby(key, url, 1) # 设置7天过期,超过7天的失败记录自动清理 r.expire(key, 7 * 86400)用Hash存储失败URL和失败次数,比用Set多了一个计数维度。重试时可以根据失败次数决定优先级:失败次数少的优先重试,失败超过3次的先缓一缓甚至放弃,这样不会浪费资源在明显有问题的页面上。同时这个Hash本身也设置了7天过期,避免历史失败数据无限累计。
4.4 同一站点并发控制:Redis做令牌桶
多worker并发抓取同一个站点时,如果火力全开很容易触发对方防火墙。我在项目里用Redis的INCR加过期时间做了一个极简的滑动窗口限流,效果非常稳定。
思路是维护一个计数器,每请求一次就在当前秒的key上INCR一次,如果超过阈值就让当前worker等待;等下一秒新key创建,计数重置。这样每个worker单独做限流不够,必须用Redis共享,这样所有worker加起来的总请求速率才会被限制住。
def can_request(site: str, max_requests: int = 10) -> bool: key = f"spider:rate:{site}" current = r.incr(key) if current == 1: r.expire(key, 1) return current <= max_requests这个方案精度到秒,已经足够应对绝大多数站点的频率限制。如果遇到更严格的限制策略,可以把窗口调成10秒或者60秒,但需要相应调整计数阈值,注意别把INCR后的EXPIRE拆成两步,否则在极端情况下会有key永远不过期的风险。
5. 实测案例:一个持续运行半年的增量新闻抓取项目
这套Redis方案不是我纸上谈兵,而是来自一个真实跑了半年多的项目。我拿它做个复盘,把具体的参数配置和遇到的实际问题都摊开来说。
5.1 项目背景与Redis实际负载
当时做的是一个垂直领域的新闻聚合系统,需要每天定时抓取约30个新闻站点的列表页和详情页,每轮任务大约产生2万到3万个URL,其中大约15%是当天新增的,其余85%都是历史重复URL。整个系统由一台2核4G的云服务器承载,Redis部署在同一台机器上。
Redis实例分配了512MB内存,实际峰值占用约260MB。其中URL去重集合存储了约620万条URL记录,占用了约180MB;内容指纹集合大概260万条,占用约55MB;剩下的是临时队列和状态数据。去重命中率稳定在96%以上,真正每轮需要抓取的URL在800到3000条之间,多worker总耗时控制在10分钟以内。
5.2 去重数据统计:一个值得关注的漏判问题
项目运行到第三个月的时候,我发现某个站点的内容入库量出奇地少。排查后发现,这个站点的列表页URL带了动态的随机参数,而且正文底部有一段动态生成的相关推荐模块,导致内容指纹一直不稳定。第一次抓取时正文哈希包含了这些动态内容,下一次内容变了以后就被当成了新文章;但诡异的是入库量反而少了,原因是URL层和内容层同时误判。
最后定位到root cause:URL规范化没有去掉该站点的随机参数,导致URL集合大量膨胀,而指纹计算又没有排除动态模块,导致内容指纹频繁变化,两层判断互相干扰。修复方案是给该站点单独写了一个URL规则,在规范化阶段直接删除随机参数;指纹计算也改成只提取正文的正文主干区域,过滤掉底部推荐位。
5.3 Redis持久化与内存水位调整
运行期间遇到过两次服务器重启。第一次没有开AOF,只依赖RDB,重启后发现丢了将近一天的去重记录,导致大约一万多条URL被重新抓取。好在站点没有反爬,没有造成封IP,但数据库里出现了大量重复数据,清理花了半天时间。那次之后我立刻把AOF打开了,配置了appendfsync everysec,之后也遇到过几次意外重启,数据基本没有丢失。
内存水位方面,最初maxmemory-policy没有设置,默认是noeviction。跑到某天发现Redis开始拒绝写入,所有爬虫任务瞬间全部报错。那是在一次临时key没有设置过期时间的事故中暴露出来的,触发了OOM command not allowed when used memory > maxmemory。后来设置了allkeys-lru,并调整了项目代码,把所有队列key统一加过期时间,类似问题再也没有出现过。
5.4 定时周期对Redis高峰的影响
定时任务采用每10分钟执行一轮的频率,但30个站点分布在10分钟内启动,会造成Redis访问的集中尖峰。最初设计是到点同时启动所有站点的抓取任务,导致每10分钟有一次Redis的CPU瞬间飙到80%以上,SCAN和SADD大量并发执行,偶尔还会出现慢查询日志。
优化方案是把各站点的启动时间错开,比如奇数站点在每小时的00分、20分、40分启动,偶数站点在10分、30分、50分启动,让Redis的负载平摊开。这是纯业务层面的调度优化,不花一分钱成本,实际效果却非常明显,Redis的CPU尖峰从80%降到了40%左右。
6. 新版本特性与替代方案思考
使用Redis做爬虫去重是个成熟的方案,但并不意味着没有改进空间。Redis自身的新版本和一些替代技术,在某些场景下值得关注。
6.1 Redis 6.2之后更顺手的小改动
Redis 6.2之前,用SET做锁要自己拼SET key value NX EX 10这样的命令;6.2之后命令行和客户端对NX和EX的同时使用支持得更加规范。另外6.2版本引入了GETDEL、SINTERCARD等新命令,SINTERCARD在多个集合求交时可以不返回结果集而只返回数量,对于估算两个去重集合的重叠率很有用,消耗的带宽更小。
SINTERCARD spider:seen:urls spider:seen:contents这个命令在调试和统计阶段比较实用,例如想快速估算URL集合和内容集合之间存在多少交集,它可以在不传输大量数据的情况下给出数量。
6.2 Redis Stack和Bloom模块的价值
Redis Stack(以前叫Redis Modules)把布隆过滤器、CMS等数据结构打包了进来。如果你用Docker部署,拉取redis/redis-stack-server镜像一行命令就能拥有BF命令,不需要手动编译模块。在需要监控海量URL且对误判率容忍度较高的场景里,配合布隆过滤器的增量任务,内存开销能压缩到原来的十分之一以下。
我在另一个数据量更大的项目里做了对比测试:同一批约5000万条URL,用Set存储需要约4.8GB内存,而用布隆过滤器(误判率0.001)只需要约90MB内存。这个差距在云服务器上直接对应每月几百块的硬件成本差异。
6.3 什么情况下该换掉Redis
Redis也不是万能的。如果去重数据量达到十亿级以上,单机Redis的内存上限会成为瓶颈,这时候要么考虑Redis Cluster分片存储去重数据,要么使用更专门的位图存储方案。另外一个情况是,如果爬虫任务本身不是高并发、数据量每天只有几百条,用SQLite或者MySQL就能轻松搞定去重,没必要为了用Redis而用Redis,维护一个额外中间件也是成本。
不过就“定时增量爬虫”这个需求量而言,Redis目前仍然是最均衡的选择——简单、高性能、内存可控、运维成本低,再加上社区资料丰富,遇上问题基本都能搜到解决方案。我自己的判断是,除非数据量突破单机内存极限,或者公司架构里已经有了其他更适合的统一存储方案,否则Redis就是首选,没有之一。
7. 踩坑实录与推荐配置清单
文章最后,我把这几年做爬虫项目积累的Redis配置经验浓缩成一份可直接照抄的清单,同时把最容易踩的坑集中列出来,给正在搭这套系统的朋友当个参考。
7.1 我遇到过的六个高频坑
第一个坑是KEYS命令遍历全库。数据量少时没感觉,数据量过万后Redis会明显卡顿,因为它是全量扫描阻塞所有请求。替代方案一定是SCAN命令,配合游标分页处理。
第二个坑是设置过期时间时用了两步操作,先SET再EXPIRE。如果两步之间进程崩溃,key就没有过期时间,长期累积会把内存耗尽。正确方案是用SET key value EX seconds或者SETEX这样的原子命令。
第三个坑是SADD和SISMEMBER配合使用出现并发窗口。多进程部署时必须用SADD的返回值做判断,不要先查再写。
第四个坑是内容指纹计算包含了无关动态内容。这会导致指纹频繁变化,去重形同虚设。要在提取正文时过滤广告位、推荐位等动态模块。
第五个坑是maxmemory-policy设置为noeviction后,内存满了直接拒绝写入。线上跑定时任务时千万要提前改成allkeys-lru或者volatile-lru。
第六个坑是不开持久化。Redis不用来做缓存而用作去重时,它就是业务的关键状态存储,不开持久化等于把所有去重记录放在内存碎片上。
7.2 一份可直接套用的配置清单
bind 127.0.0.1 port 6379 # 内存上限根据服务器实际调整 maxmemory 512mb maxmemory-policy allkeys-lru maxmemory-samples 10 # 持久化配置 save 900 1 save 300 10 save 60 10000 appendonly yes appendfsync everysec aof-use-rdb-preamble yes # 慢查询日志 slowlog-log-slower-than 10000 slowlog-max-len 128# Python端初始化 import redis pool = redis.ConnectionPool( host="localhost", port=6379, db=0, decode_responses=True, max_connections=50, socket_timeout=5, socket_connect_timeout=5, ) r = redis.Redis(connection_pool=pool)7.3 关键命令速查表
| 用途 | 命令 | 说明 |
|---|---|---|
| URL去重 | SADD key url | 返回0表示已存在 |
| 内容指纹去重 | SADD fp_key fingerprint | 同上 |
| 分布式锁 | SET lock_key 1 NX EX 600 | 返回OK表示获取锁 |
| 临时队列 | RPUSH queue_key url | 配合LPOP或BRPOP消费 |
| 计数限流 | INCR rate_key | 配合EXPIRE每秒重置 |
| 批量清理 | SCAN cursor MATCH pattern COUNT 500 | 禁止用KEYS |
| 统计大小 | SCARD key | 查看集合成员数量 |
| 设置过期 | EXPIRE key seconds | 针对已有key追加TTL |
| 估算内存 | MEMORY USAGE key | 查看单个key占用的字节数 |
7.4 最后一句话的建议
定时增量爬虫这种任务,本质上是个“长期运转的无人值守系统”,稳定压倒一切。宁可多写两行代码把边界情况处理好,也别等到凌晨三点被报警短信叫醒。把Redis当成业务的一部分而不是一个临时缓存来对待,用前面这套思路去设计去重和过期策略,运行起来会省心很多。