1. 核心痛点:为什么 List 结构不再适用?
在早期的代理池实现中,开发者习惯使用 Redis 的 List 结构。List 简单易用,但它存在两个致命缺陷:
- 无法原生去重:同一个 IP 被多个爬虫抓取并入库时,List 会产生大量重复记录。
- 维护成本高:如果你想检查某个 IP 是否已存在,List 需要 的遍历开销,这在高并发下是不可接受的。
2. 方案一:使用 Set 结构实现自动去重
正如原稿所述,Redis 的 Set 结构具有天然的去重特性。通过 SADD 指令,我们可以确保池内每一个代理字符串都是唯一的。
优化后的代码实现
import redis
from proxypool.setting import HOST, PORT, PASSWORD
class RedisClient(object):
def __init__(self, host=HOST, port=PORT, db=0):
# 使用连接池优化性能
connection_pool = redis.ConnectionPool(
host=host, port=port, password=PASSWORD, db=db, decode_responses=True
)
self._db = redis.StrictRedis(connection_pool=connection_pool)
def add(self, proxy):
"""利用 SADD 的返回值判断是否为新代理"""
return self._db.sadd("proxies:set", proxy)
def pop(self):
"""随机弹出一个可用代理"""
proxy = self._db.spop("proxies:set")
if not proxy:
return None
return proxy
def count(self):
return self._db.scard("proxies:set")
评价:Set 方案解决了“重复”问题,但它依然是“盲目”的——它无法区分哪个代理更快,哪个代理已经失效。
3. 方案二:进阶之选 —— Sorted Set (ZSet)
在 2026 年的生产环境中,我们更推荐使用 Sorted Set。它不仅能去重,还能给每个 IP 绑定一个分数(Score),这个分数可以代表代理的“健康度”或“响应速度”。
ZSet 方案逻辑:
- 去重:ZSet 保证 Member(IP)唯一。
- 分级管理:初始分数设为 100。每检测失败一次,减 10 分;每成功一次,恢复至 100 分。
- 智能提取:爬虫永远只从分数最高的区间(如 100 分)提取代理。
def add_with_score(self, proxy, score=100):
"""添加代理并设置初始分数"""
return self._db.zadd("proxies:zset", {proxy: score})
def get_best(self):
"""获取分数最高的代理(不弹出,仅读取)"""
# 返回分数在 100-100 之间的随机一个
result = self._db.zrangebyscore("proxies:zset", 100, 100)
if result:
from random import choice
return choice(result)
return None
4. 不同数据结构的对比决策表
| 特性 | List (列表) | Set (集合) | Sorted Set (有序集合) |
|---|---|---|---|
| 去重能力 | ❌ 需手动逻辑 | ✅ 原生支持 | ✅ 原生支持 |
| 查询性能 | |||
| 排序依据 | 插入顺序 | 无序 | 自定义分数 (Score) |
| 适用场景 | 简单队列 | 基础去重需求 | 工业级高可用代理池 |
5. 总结与建议
解决代理 IP 重复只是优化的第一步。
- 如果你的项目规模较小,Set 结构通过
spop提供的随机性足以应付。 - 如果你追求爬虫的极高成功率,请务必转向 Sorted Set。通过给 IP 打分,你可以实现“优胜劣汰”的自净化机制,让失效代理自动沉底,让高质量代理始终处于活跃状态。
技术提示:在 Redis 中存储 IP 时,建议使用 ip:port 格式的字符串,并利用 Redis 的 EXPIRE 功能为那些长时间未更新的 Set 键设置过期时间,进一步节省内存。