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消费
相关推荐
Jelena15779585792几秒前
1688.item_get_app接口:包装尺寸重量信息深度解析
开发语言·前端·python
Carsene1 分钟前
🎉 AutoScan v1.1.0 发布 - 通配符包扫描、排除过滤、自定义注解三大新特性
spring boot·后端
loserwang1 分钟前
Fluss#1386: 从日志恢复中的 OutOfOrder 来看 LEO、HW 与 Checkpoint 的区别
java·后端
PaytonD2 分钟前
基于 GPUI 实现 WebScoket 服务端之服务篇
后端·rust
用户8356290780518 分钟前
使用 Python 精准控制 Word 段落格式
后端·python
NULIWEIMENXIANG9 分钟前
ArcPy 程序调用 QGIS 进程实现几何拓扑检查
python·arcgis·gis
rookie软工12 分钟前
Qt代理委托实现
开发语言·python·qt
love530love16 分钟前
Windows 本地部署 IDM-VTON 虚拟试衣:排障版教程
人工智能·windows·python·virtual try-on
StackNoOverflow16 分钟前
Spring Boot 整合 MyBatis + 自动配置原理详解
spring boot·后端·mybatis
忘忧记18 分钟前
pytest进阶参数化用法
前端·python·pytest