有个需求场景是这样的,使用redis控制scrapy运行的数量。当系统的后台设置为4时,只允许scapry启动4个任务,多余的任务则进行排队。

概况

最近做了一个django + scrapy + celery + redis 的爬虫系统,客户购买的主机除了跑其他程序外,还要跑我开发的这套程序,所以需要手动控制scrapy的实例数量,避免过多的爬虫给系统造成负担。

流程设计

1、爬虫任务由用户以请求的方式发起,所有的用户的请求统一进入到celery进行排队;

2、任务数量控制的执行就交给reids,经由celery保存到redis,包含了爬虫启动所需要的必要信息,从redis取一条信息即可启动一个爬虫;

3、通过scrapyd的接口来获取当前在运行的爬虫数量,以便决定下一步流程:如果小于4,则从redis中取相应数量的信息来启动爬虫,如果大于等于4,则继续等待;

4、如果在运行爬虫的数量有所减少,则及时从reids中取相应数量的信息来启动爬虫。

代码实现

业务代码有点复杂和啰嗦,此处使用伪代码来演示

import redis

# 实例化一个redis连接池
pool = redis.ConnectionPool(host='127.0.0.1', port=6379, decode_responses=True, db=4, password='') r = redis.Redis(connection_pool=pool)
# 爬虫实例限制为4 即只允许4个scrapy实例在运行
limited = 4 # 声明redis的乐观锁
lock = r.Lock() # lock.acquire中有while循环,即它会线程阻塞,直到当前线程获得redis的lock,才会继续往下执行代码
if lock.acquire():
# 1、从reids中取一条爬虫信息
info = redis.get() # 2、while循环监听爬虫运行的数量
while True:
req = requests.get('http://127.0.0.1:6800/daemonstatus.json').json()
# 统计当前有多少个爬虫在运行
running = req.get('running') + req.get('pending') # 3、判断是否等待还是要增加爬虫数量
# 3.1 如果在运行的数量大于等于设置到量 则继续等待
if running >= limited:
continue # 3.2 如果小于 则启动爬虫
start_scrapy(info)
# 3.3 将info从redis中删除
redis.delete(info)
# 3.4 释放锁
lock.release()
break

当前,这只是伪代码而已,实际的业务逻辑可能是非常复杂的,如:

@shared_task
def scrapy_control(key_uuid): r = redis.Redis(connection_pool=pool)
db = MysqlDB()
speed_limited = db.fetch_config('REPTILE_SPEED')
speed_limited = int(speed_limited[0]) keywords_num = MysqlDB().fetch_config('SEARCH_RANDOM')
keywords_num = int(keywords_num[0]) # while True:
lock = r.lock('lock')
with open('log/celery/info.log', 'a') as f: f.write(str(datetime.datetime.now()) + '--' + str(key_uuid) + ' 进入处理环节' + '\n')
try:
# acquire默认阻塞 如果获取不到锁时 会一直阻塞在这个函数的while循环中
if lock.acquire():
with open('log/celery/info.log', 'a') as f: f.write(str(datetime.datetime.now()) + '--' + str(key_uuid) + ' 获得锁' + '\n')
# 1 从redis中获取信息
redis_obj = json.loads(r.get(key_uuid))
user_id = redis_obj.get('user_id')
contents = redis_obj.get('contents') # 2 使用while循环处理核心逻辑
is_hold_print = True
while True:
req = requests.get('http://127.0.0.1:6800/daemonstatus.json').json()
running = req.get('running') + req.get('pending')
# 3 如果仍然有足够的爬虫在运行 则hold住redis锁,等待有空余的爬虫位置让出
if running >= speed_limited:
if is_hold_print:
with open('log/celery/info.log', 'a') as f: f.write(str(datetime.datetime.now()) + '--' + str(key_uuid) + ' 爬虫在运行,线程等待中' + '\n')
is_hold_print = False
time.sleep(1)
continue # 4 有空余的爬虫位置 则往下走
# 4.1 处理完所有的内容后 释放锁
if len(contents) == 0:
r.delete(key_uuid)
with open('log/celery/info.log', 'a') as f: f.write(str(datetime.datetime.now()) + '--' + str(key_uuid) + ' 任务已完成,从redis中删除' + '\n')
lock.release()
with open('log/celery/info.log', 'a') as f: f.write(str(datetime.datetime.now()) + '--' + str(key_uuid) + ' 释放锁' + '\n')
break # 4.2 创建task任务
task_uuid = str(uuid.uuid4())
article_obj = contents.pop()
article_id = article_obj.get('article_id')
article = article_obj.get('content')
try:
Task.objects.create(
task_uuid = task_uuid,
user_id = user_id,
article_id = article_id,
content = article
)
except Exception as e:
with open('log/celery/error.log', 'a') as f: f.write(str(datetime.datetime.now()) + '--' + str(key_uuid) + '->' + str(task_uuid) + ' 创建Task出错: ' + str(e) + '\n')
# finally:
# 4.3 启动爬虫任务 即便创建task失败也会启动
try:
task_chain(user_id, article, task_uuid, keywords_num)
except Exception as e:
with open('log/celery/error.log', 'a') as f: f.write(str(datetime.datetime.now()) + '--' + str(key_uuid) + ' 启动任务链失败: ' + str(e) + '\n') # 加入sleep 防止代码执行速度快于爬虫启动速度而导致当前线程启动额外的爬虫
time.sleep(5) except Exception as e:
with open('log/celery/error.log', 'a') as f: f.write(str(datetime.datetime.now()) + '--' + str(key_uuid) + ' 获得锁之后的操作出错: ' + str(e) + '\n')
lock.release()

小坑

scrapy启动速度相对较慢,所以while循环中,代码中执行到了爬虫的启动,需要sleep一下再去通过scrapyd接口获取爬虫运行的数量,如果立刻读取,可能会造成误判。

基于redis乐观锁实现并发排队 - 基于scrapy运行数量的控制的更多相关文章

  1. RedLock.Net - 基于Redis分布式锁的开源实现

    工作中,经常会遇到分布式环境中资源访问冲突问题,比如商城的库存数量处理,或者某个事件的原子性操作,都需要确保某个时间段内只有一个线程在访问或处理资源. 因此现在网上也有很多的分布式锁的解决方案,有数据 ...

  2. 基于Redis分布式锁的正确打开方式

    分布式锁是在分布式环境下(多个JVM进程)控制多个客户端对某一资源的同步访问的一种实现,与之相对应的是线程锁,线程锁控制的是同一个JVM进程内多个线程之间的同步.分布式锁的一般实现方法是在应用服务器之 ...

  3. 使用mysql乐观锁解决并发问题

    案例说明: 银行两操作员同时操作同一账户.比如A.B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户扣除50元,A先提交,B后提交.最后实际账户余额为1000 ...

  4. 使用mysql乐观锁解决并发问题思路

    本文摘自网络,仅供个人学习之用 案例说明: 银行两操作员同时操作同一账户.比如A.B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户扣除50元,A先提交,B后 ...

  5. Python Redis pipeline操作和Redis乐观锁保持数据一致性

    Redis是建立在TCP协议基础上的CS架构,客户端client对redis server采取请求响应的方式交互. redis 乐观锁:也可理解为版本号比较机制,主要是说在读取数据逇时候同时读取其版本 ...

  6. 基于Redis分布式锁(获取锁及解锁)

    目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题.分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency).可用性( ...

  7. redis乐观锁

    乐观锁(又名乐观并发控制,Optimistic Concurrency Control,缩写"OCC"),是一种并发控制的方法.它假设多用户并发的事务在处理时不会彼此互相影响,各事 ...

  8. redis乐观锁(适用于秒杀系统)

    redis事务中的WATCH命令和基于CAS的乐观锁  在Redis的事务中,WATCH命令可用于提供CAS(check-and-set)功能.假设我们通过WATCH命令在事务执行之前监控了多个Key ...

  9. redis 乐观锁实践秒杀

    需求:有一个标(理解成抢红包也行,accountBalance预赋值1000元),大家可以抢购,每个用户抢购成功后,更新最后标的总数,在并发情况下,使用redis的乐观锁,保证更新标总值正确性,先往r ...

  10. 使用Redis分布式锁处理并发,解决超卖问题

    一.使用Apache ab模拟并发压测 1.压测工具介绍 $ ab -n 100 -c 100 http://www.baidu.com/ -n表示发出100个请求,-c模拟100个并发,相当是100 ...

随机推荐

  1. WPF绘制圆形调色盘

    本文使用writeableBitmap类和HSB.RGB模式来绘制圆形的调色盘. 开源项目地址:https://github.com/ZhiminWei/Palette RGB为可见光波段三个颜色通道 ...

  2. hibernate validation 手动参数校验 不经过spring

    /** * 校验工具类 * @author wdmcygah * */ public class ValidationUtils { private static Validator validato ...

  3. Java注解(4):一个真实的Elasticsearch案例

    昨天把拼了一半的注解+Elasticsearch积木放下了,因为东西太多了拼不好,还容易乱.休息了一晚上接着来. 接着昨天,创建elasticsearch文档注解(相当于数据表的注解): /** * ...

  4. mujoco d4rl 安装问题

    最近mujoco免费了,属实爽歪歪,安装d4rl没有以前那么麻烦了(不知为何半年前我安装d4rl时走了那么多弯路) mujoco安装 在 https://mujoco.org/download 上面下 ...

  5. 设计一个网上书店,该系统中所有的计算机类图书(ComputerBook)每本都有10%的折扣,所有的语言类图书(LanguageBook)每本都有2元的折扣,小说类图书(NovelBook)每100元

    现使用策略模式来设计该系统,绘制类图并编程实现 UML类图 书籍 package com.zheng; public class Book { private double price;// 价格 p ...

  6. cmd中pip加速的方法

    临时加速: pip install dlib -i https://pypi.tuna.tsinghua.edu.cn/simple/ 永久加速: 在user文件夹里新建pip文件夹,再建pip.in ...

  7. Python 根据两个字段排序 中文排序 汉字排序 升序 降序

    Python3写法 代码 # -*- coding: utf-8 -*- # 需求:年龄倒序,姓名正序 from itertools import chain from pypinyin import ...

  8. 1.Python面向对象基础

    面向对象(OOP) 面向对象编程--object oriented programming 简写 OOP   面向过程和面向对象的区别: 面向过程: 1.把完成某一个需求的所有步骤从头到尾逐步实现 2 ...

  9. CSS 渐变锯齿消失术

    在 CSS 中,渐变(Gradient)可谓是最为强大的一个属性之一. 但是,经常有同学在使用渐变的过程中会遇到渐变图形产生的锯齿问题. 何为渐变锯齿? 那么,什么是渐变图形产生的锯齿呢? 简单的一个 ...

  10. 基于 .NET 7 的 QUIC 实现 Echo 服务

    前言 随着今年6月份的 HTTP/3 协议的正式发布,它背后的网络传输协议 QUIC,凭借其高效的传输效率和多路并发的能力,也大概率会取代我们熟悉的使用了几十年的 TCP,成为互联网的下一代标准传输协 ...