Python学习(5)- 爬虫 - 队列/消息队列

请求管理业务

  • 请求去重管理:防止重复请求。(可以布隆过滤器)
  • 请求缓冲管理:临时存储请求
    • 常用数据结构:队列 Queue(常用), 列表,字典,元组
  • 请求调度管理:派遣并控制请求调度顺序,请求优先级管理
    • 请求调度优先级
      • 广度优先(FIFO队列)
      • 深度优先(LIFO队列)
      • 权重优先(优先级队列)

队列

临时队列

library/queue

  1. 内置队列模块 (Queue 常用于多线程,多进程从mutiprocess中引入)
  2. asyncio中的队列模块
  3. gevent中的队列模块
  4. tornado中的队列模块

为什么有这么多的队列? 因为每个队列一般都和异步模型,多线程,多进程通信用的,由于每个异步模型,在底层实现的原理不同,所以通信的时候处理方式有不同,所以会有多个队列模块。 但每个队列模块,api基本一样

持久化队列

  1. scrapy - queuelib 队列 (硬盘中 disk_queue),基于
    1. 有基于文件 sqlite 的 持久化
  2. pyspider - redis_queue 模块
    1. 基于 redis 的队列
      1. fifo队列 用 lists 列表,lpush,rpop
      2. lifo队列 用 lists 列表,lpush,lpop
      3. 优先级队列,用 Sorted Set 有序集合,zadd ,zrang ,zrem

redis锁及分布式锁

  • 因为在实现优先级队列时,zrange 取出队列中的值后,再zrem删除这个值(符合队列取出的逻辑),这中间会时间间隔,会出现多线程取值错误的情况
  • 所以需要锁机制
  1. 一般redis事务也可以保证操作的原子性,但是不像mysql事务,不能百分百保证

redis锁

py-crawler-demo

  • 一般用在单进程的多线程场景,如果是多进程的场景,则需要使用分布式锁
  • 说明:
  1. 使用了锁机制后,能确保在同一份数据只会被某一个线程获取到,而 不会被多个线程同时获取,从而保证了数据不会被处理多次的情况发 生 2.此处相当于实现了同一个线程内部zrange与zrem是一个原子性操作

注意

  1. 一般的内存中的锁只能解决单进程中多个线程间的资源共享问题;
  2. 如果是不同进程间甚至不同服务器上线程间资源共享问题,则需要考虑使用如redis分布式锁来实现
  3. redis虽有事务机制,但仍不足以保证前面理想的执行结果百分百出现

redis分布式锁

  • 说明:
  1. 获取锁:利用redis的setnx命令特征 如果key不存在则执行操作,返回值将是1,此时表明锁获取成功,即上锁;
  2. 如果key存在则不执行任何操作,返回值将是0,此时表明锁获取失败,因为已经被上锁了
  3. 释放锁:获取设置的值,判断是否是当前线程设置的值
  4. get命令获取对应key的值
  5. 判断值是否和预先设置的一样(thread id),保证不是其他线程解开的锁 .如果一致,就把该key删除,表示释放锁,此时其他线程便可以获取到锁
  • 注意:
  1. 同一把锁,注意lock_name一致
  2. 使用同一把锁的各个线程,必须维护好各自的 thread id,不能重复。否则可能出现,如a线程上的锁却被b线程解开了,这样的bug
  3. 为防止死锁问题(如a线程上了锁,但在解开锁前a线程挂了),应当给lock_name这个数据设置一定过期时间,具体时间,依实际情况定

锁代码示例

py-crawler-demo

python 复制代码
from request_manager.utils.redis_tools import get_redis_queue_cls

Queue = get_redis_queue_cls('priority')
PRIORITY_REDIS_QUEUE_CONFIG = {
    "name":"pqueue",
    "use_lock":True,  # 锁
    "redis_lock_config":{
        "lock_name": "pqueue-lock"
    }
}

pqueue = Queue(**PRIORITY_REDIS_QUEUE_CONFIG)
pqueue.put((100, 'value100'))
print(pqueue.get())
  1. 因为 优先级队列 priority,是基于 redis 中的有序集合来实现的,要弹出某个值,是需要 zrange,和 zrem,两条命令中有时间差,所有加锁比较好

在优先级队列实现加锁

  1. 在取值方法中,先获取锁 self.lock.acquire_lock()
  2. 再进行一些列操作后,再释放锁 self.lock.release_lock()
python 复制代码
  def get_nowait(self):
        """
        -1,-1默认取权重最大的
        0,,0 取权重最小的
        :return:
        """
        if self.use_lock is True:
            from .redis_lock import RedisLock
            if self.lock is None:
                self.lock = RedisLock(**self.redis_lock_config)

            if self.lock.acquire_lock():
                ret = self.redis.zrange(self.name, -1, -1)
                if not ret:
                    raise self.Empty
                self.redis.zrem(self.name, ret[0])
                self.lock.release_lock()
                return pickle.loads(ret[0])
        else:
            ret = self.redis.zrange(self.name, -1, -1)
            if not ret:
                raise self.Empty
            self.redis.zrem(self.name, ret[0])
            return pickle.loads(ret[0])
  1. 锁本身的实现
  • 基于 redis 中的用setnx设置某个值,(顺带加上过期时间,防止死锁,具体时间按照自己的业务逻辑来定合理的值)
  • 如果能设置,说明没锁;不能设置,说明已经上锁
  • 如果用户传了 block = True 参数,说明要阻塞,用一个 while 循环
python 复制代码
  def acquire_lock(self, thread_id=None, expire=10 ,block = True):

    if thread_id is None:
        thread_id = self._get_thread_id()


    while block:
        # 如果 lock_name 存在,ret == 0 否则 ret==1
        ret = self.redis.setnx(self.lock_name, pickle.dumps(thread_id))
        if ret == 1:
            self.redis.expire(self.lock_name, expire)
            print('上锁成功')
            return True
        time.sleep(0.01)
python 复制代码
 def release_lock(self, thread_id=None):
        if thread_id is None:
            thread_id = self._get_thread_id()

        ret = self.redis.get(self.lock_name)

        if ret is not None and pickle.loads(ret) == thread_id:
            self.redis.delete(self.lock_name)
            print('解锁成功')
            return True
        else:
            print('解锁失败')
            return False
python 复制代码
# 测试
if __name__ == '__main__':
    redis_lock = RedisLock('redis_lock')

    if redis_lock.acquire_lock(expire=2):
        print('执行对应的操作')
        # redis_lock.release_lock()  # 加锁后不解锁试试

消息队列 - kafka

  1. kafka 服务,Broker 相当于这个服务的服务端,每个 Broker 就是一个kafka的实例,Producer 负责生产数据,Consumer消费数据,kafka接收到数据是持久化存储在 Disk 中的(可以海量数据)

  2. topic 相当于一条队列,分类

  3. 内部实现,数据均匀放在分区 part

    1. 比如一个1000条的数据,有3个part,有4个 Broker (相当于4个 kafka实例),副本集设置为2
    2. 如果Broker 只有1个,则不能有副本,副本必须与原数据在不同的Broer上

kafka 特点

  • Kafka启动:
    • 单节点单broker
    • 单节点多broker
  • Kafka使用时的显著特征
    • 分区之间是无序的,但分区内的消息是有序的
    • 对于topic的消费,消费者的数量应不多于该topic分区的数量,否则多余的消费者将必定无法接收到消息
    • 一个消费者可同时消费多个topic
    • Kafka保证每条消息在同---个Consumer Group里只会被某一个Consumer消费
相关推荐
0思必得01 分钟前
[Web自动化] 爬虫实例(获取时光网某个年度电影数据)
前端·爬虫·python·selenium·自动化
程序猿_极客7 分钟前
【2026】Spring IOC 与 DI 依赖注入深度解析:从原理到实战(附带面试高频问题)
java·后端·spring·ioc·di依赖注入
观音山保我别报错7 分钟前
SpringBoot 项目学习内容详解(二)
spring boot·后端·学习
小信丶8 分钟前
BlockExceptionHandler类介绍、应用场景和示例代码
java·spring boot·后端·spring·spring cloud
计算机徐师兄10 分钟前
Python基于深度学习的商品推荐系统(附源码,文档说明)
python·深度学习·python深度学习·python商品推荐系统·pytho深度学习商品推荐系统·python电商平台商品分类·电商平台商品分类系统
小白学大数据12 分钟前
使用随机时间间隔提升爬虫隐蔽性
开发语言·c++·爬虫·python
喵手18 分钟前
Python爬虫实战:B站综合排行榜数据采集实战:从静态抓取到数据分析全流程(附 CSV 导出)!
爬虫·python·爬虫实战·python爬虫工程化实战·零基础python爬虫教学·b站排行榜数据采集·采集数据导出csv
m0_7066532339 分钟前
Python数据库操作:SQLAlchemy ORM指南
jvm·数据库·python
试剂小课堂 Pro43 分钟前
Ald-PEG-Ald:丙醛与聚乙二醇两端连接的对称分子
java·c语言·c++·python·ffmpeg
玄同7651 小时前
SQLAlchemy 初始化全流程详解:从引擎到会话的每一步
数据库·人工智能·python·sql·mysql·语言模型·知识图谱