省钱返利小助手价格监控内核:Redis ZSet 时间轮 + 增量爬虫去重策略解析
·
省钱返利小助手价格监控内核:Redis ZSet 时间轮 + 增量爬虫去重策略解析
大家好,我是 微赚淘客系统3.0 的研发者省赚客!
微赚淘客“省钱返利小助手”需对百万级商品进行周期性价格监控(如每 6 小时),以触发降价提醒。若采用传统定时任务轮询数据库,将面临调度爆炸、重复抓取、资源浪费等问题。我们基于 Redis ZSet 构建时间轮调度器,结合 布隆过滤器 + 内容指纹 实现高效增量爬虫去重,使监控吞吐提升 5 倍,无效请求下降 92%。
一、时间轮调度模型设计
使用 Redis Sorted Set(ZSet)模拟时间轮,score 为下次抓取时间戳(毫秒),member 为商品唯一标识(如 item_675849302):
package juwatech.cn.price.scheduler;
@Service
public class PriceMonitorScheduler {
private static final String SCHEDULE_ZSET = "price:monitor:schedule";
private static final long DEFAULT_INTERVAL_MS = 6 * 3600 * 1000; // 6小时
public void scheduleItem(Long itemId, long nextTime) {
String member = "item_" + itemId;
redisTemplate.opsForZSet().add(SCHEDULE_ZSET, member, nextTime);
}
// 初始调度:全量商品按分片加入时间轮
@Scheduled(fixedRate = 30000) // 每30秒调度一次
public void dispatchReadyItems() {
long now = System.currentTimeMillis();
Set<String> readyItems = redisTemplate.opsForZSet()
.rangeByScore(SCHEDULE_ZSET, 0, now, 0, 1000); // 批量取1000个
if (readyItems != null && !readyItems.isEmpty()) {
for (String itemKey : readyItems) {
Long itemId = extractItemId(itemKey);
// 提交到爬虫队列
crawlTaskQueue.offer(new CrawlTask(itemId));
// 重新调度下次抓取
scheduleItem(itemId, now + DEFAULT_INTERVAL_MS);
}
// 从ZSet中移除已调度项
redisTemplate.opsForZSet().remove(SCHEDULE_ZSET, readyItems.toArray());
}
}
private Long extractItemId(String key) {
return Long.parseLong(key.substring(5));
}
}
该模型支持动态调整监控频率(如热销品 1 小时,冷门品 24 小时)。
二、增量爬虫去重:布隆过滤器 + 内容指纹
即使调度精准,仍需防止因网络抖动或重试导致的重复抓取。我们采用两级去重:
1. 请求级去重(布隆过滤器)
在爬虫入口拦截重复请求:
package juwatech.cn.price.crawler;
@Service
public class BloomFilterDeduper {
private final RedisBloomFilter bloomFilter;
public boolean shouldSkip(Long itemId) {
String key = "price:bloom:item:" + itemId;
if (bloomFilter.contains(key)) {
return true; // 可能已处理
}
bloomFilter.add(key);
return false;
}
}
RedisBloomFilter 基于 Redis 的 BF.ADD / BF.EXISTS(需加载 RedisBloom 模块)。
2. 内容级去重(SimHash 指纹)
即使请求不同,若商品价格/券未变,则无需更新。计算页面内容 SimHash:
package juwatech.cn.price.fingerprint;
public class SimHashDeduper {
private static final String FINGERPRINT_HASH = "price:fingerprint";
public boolean isChanged(Long itemId, String htmlContent) {
String newHash = SimHashUtils.simHash(htmlContent);
String oldHash = (String) redisTemplate.opsForHash().get(FINGERPRINT_HASH, itemId.toString());
if (oldHash == null) {
// 首次抓取,记录指纹
redisTemplate.opsForHash().put(FINGERPRINT_HASH, itemId.toString(), newHash);
return true;
}
if (newHash.equals(oldHash)) {
return false; // 未变化
}
// 计算汉明距离(容忍少量噪声)
int distance = SimHashUtils.hammingDistance(oldHash, newHash);
if (distance <= 3) {
return false; // 视为未变
}
// 更新指纹
redisTemplate.opsForHash().put(FINGERPRINT_HASH, itemId.toString(), newHash);
return true;
}
}
SimHash 工具类核心逻辑:
public class SimHashUtils {
public static String simHash(String content) {
int[] v = new int[64];
for (String word : jiebaSegmenter.sentenceProcess(content)) {
long hash = MurmurHash3.hash64(word);
for (int i = 0; i < 64; i++) {
if ((hash & (1L << i)) != 0) {
v[i] += 1;
} else {
v[i] -= 1;
}
}
}
StringBuilder simHash = new StringBuilder();
for (int i = 0; i < 64; i++) {
simHash.append(v[i] > 0 ? '1' : '0');
}
return simHash.toString();
}
public static int hammingDistance(String s1, String s2) {
int dist = 0;
for (int i = 0; i < 64; i++) {
if (s1.charAt(i) != s2.charAt(i)) dist++;
}
return dist;
}
}
三、完整爬虫流程
package juwatech.cn.price.crawler;
@Service
public class PriceCrawler {
public void crawl(CrawlTask task) {
Long itemId = task.getItemId();
// 1. 请求去重
if (bloomDeduper.shouldSkip(itemId)) {
return;
}
// 2. 调用淘宝详情页 API
String html = taobaoClient.getItemHtml(itemId);
// 3. 内容去重
if (!simHashDeduper.isChanged(itemId, html)) {
log.debug("商品 {} 价格未变动,跳过", itemId);
return;
}
// 4. 解析价格、券、库存
ItemPriceInfo info = HtmlParser.parse(html);
// 5. 比较历史价格,触发降价通知
if (priceAlertService.shouldAlert(itemId, info.getCurrentPrice())) {
alertService.sendPriceDropNotice(itemId, info);
}
// 6. 持久化最新价格
priceHistoryService.save(itemId, info);
}
}
四、性能与存储优化
- ZSet 分片:当商品量超 1000 万,按
itemId % 16拆分为 16 个 ZSet,避免单 key 过大; - 布隆过滤器参数:预计 500 万商品,误判率 0.1%,分配 1.2GB 内存;
- 指纹 TTL:设置 30 天过期,自动清理长期未监控商品。
压测结果:
- 单机每秒可调度 800+ 商品;
- 无效抓取(内容未变)占比从 76% 降至 8%;
- Redis 内存占用稳定在 4.5GB(含 ZSet + Hash + Bloom)。
该内核支撑每日 1200 万次价格监控任务,准确触发 15 万+ 降价提醒。
本文著作权归 微赚淘客系统3.0 研发团队,转载请注明出处!
更多推荐




所有评论(0)