请求管理业务
- 请求去重管理:防止重复请求。(可以布隆过滤器)
- 请求缓冲管理:临时存储请求
- 常用数据结构:队列 Queue(常用), 列表,字典,元组
- 请求调度管理:派遣并控制请求调度顺序,请求优先级管理
- 请求调度优先级
- 广度优先(FIFO队列)
- 深度优先(LIFO队列)
- 权重优先(优先级队列)
- 请求调度优先级
队列
临时队列
- 内置队列模块 (Queue 常用于多线程,多进程从mutiprocess中引入)
- asyncio中的队列模块
- gevent中的队列模块
- tornado中的队列模块
为什么有这么多的队列? 因为每个队列一般都和异步模型,多线程,多进程通信用的,由于每个异步模型,在底层实现的原理不同,所以通信的时候处理方式有不同,所以会有多个队列模块。 但每个队列模块,api基本一样
持久化队列
- scrapy - queuelib 队列 (硬盘中 disk_queue),基于
- 有基于文件 sqlite 的 持久化
- pyspider - redis_queue 模块
- 基于 redis 的队列
- fifo队列 用 lists 列表,lpush,rpop
- lifo队列 用 lists 列表,lpush,lpop
- 优先级队列,用 Sorted Set 有序集合,zadd ,zrang ,zrem
- 基于 redis 的队列
redis锁及分布式锁
- 因为在实现优先级队列时,zrange 取出队列中的值后,再zrem删除这个值(符合队列取出的逻辑),这中间会时间间隔,会出现多线程取值错误的情况
- 所以需要锁机制
- 一般redis事务也可以保证操作的原子性,但是不像mysql事务,不能百分百保证
redis锁
- 一般用在单进程的多线程场景,如果是多进程的场景,则需要使用分布式锁
- 说明:
- 使用了锁机制后,能确保在同一份数据只会被某一个线程获取到,而 不会被多个线程同时获取,从而保证了数据不会被处理多次的情况发 生 2.此处相当于实现了同一个线程内部zrange与zrem是一个原子性操作
注意:
- 一般的内存中的锁只能解决单进程中多个线程间的资源共享问题;
- 如果是不同进程间甚至不同服务器上线程间资源共享问题,则需要考虑使用如redis分布式锁来实现
- redis虽有事务机制,但仍不足以保证前面理想的执行结果百分百出现
redis分布式锁
- 说明:
- 获取锁:利用redis的setnx命令特征 如果key不存在则执行操作,返回值将是1,此时表明锁获取成功,即上锁;
- 如果key存在则不执行任何操作,返回值将是0,此时表明锁获取失败,因为已经被上锁了
- 释放锁:获取设置的值,判断是否是当前线程设置的值
- get命令获取对应key的值
- 判断值是否和预先设置的一样(thread id),保证不是其他线程解开的锁 .如果一致,就把该key删除,表示释放锁,此时其他线程便可以获取到锁
- 注意:
- 同一把锁,注意lock_name一致
- 使用同一把锁的各个线程,必须维护好各自的 thread id,不能重复。否则可能出现,如a线程上的锁却被b线程解开了,这样的bug
- 为防止死锁问题(如a线程上了锁,但在解开锁前a线程挂了),应当给lock_name这个数据设置一定过期时间,具体时间,依实际情况定
锁代码示例
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())
- 因为 优先级队列 priority,是基于 redis 中的有序集合来实现的,要弹出某个值,是需要 zrange,和 zrem,两条命令中有时间差,所有加锁比较好
在优先级队列实现加锁
- 在取值方法中,先获取锁 self.lock.acquire_lock()
- 再进行一些列操作后,再释放锁 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])
- 锁本身的实现
- 基于 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
-
kafka 服务,Broker 相当于这个服务的服务端,每个 Broker 就是一个kafka的实例,Producer 负责生产数据,Consumer消费数据,kafka接收到数据是持久化存储在 Disk 中的(可以海量数据)
-
topic 相当于一条队列,分类
-
内部实现,数据均匀放在分区 part
- 比如一个1000条的数据,有3个part,有4个 Broker (相当于4个 kafka实例),副本集设置为2
- 如果Broker 只有1个,则不能有副本,副本必须与原数据在不同的Broer上
kafka 特点
- Kafka启动:
- 单节点单broker
- 单节点多broker
- Kafka使用时的显著特征
- 分区之间是无序的,但分区内的消息是有序的
- 对于topic的消费,消费者的数量应不多于该topic分区的数量,否则多余的消费者将必定无法接收到消息
- 一个消费者可同时消费多个topic
- Kafka保证每条消息在同---个Consumer Group里只会被某一个Consumer消费