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消费
相关推荐
吃面不喝汤661 小时前
Flask + Swagger 完整指南:从安装到配置和注释
后端·python·flask
讓丄帝愛伱2 小时前
spring boot启动报错:so that it conforms to the canonical names requirements
java·spring boot·后端
weixin_586062022 小时前
Spring Boot 入门指南
java·spring boot·后端
AI原吾5 小时前
掌握Python-uinput:打造你的输入设备控制大师
开发语言·python·apython-uinput
毕设木哥5 小时前
25届计算机专业毕设选题推荐-基于python的二手电子设备交易平台【源码+文档+讲解】
开发语言·python·计算机·django·毕业设计·课程设计·毕设
weixin_455446175 小时前
Python学习的主要知识框架
开发语言·python·学习
D11_6 小时前
Pandas缺失值处理
python·机器学习·数据分析·numpy·pandas
花生了什么树~.6 小时前
python基础知识(四)--if语句,for\while循环
python
IT毕设梦工厂7 小时前
计算机毕业设计选题推荐-在线拍卖系统-Java/Python项目实战
java·spring boot·python·django·毕业设计·源码·课程设计
luthane8 小时前
python 实现average mean平均数算法
开发语言·python·算法