Uwsgi+Django多进程下Apscheduler定时任务动态添加、任务重复执行及解决定时任务中高并发的问题(适用于分布式)
因为uwsgi+django启用多进程的情况下,每一个进程是单独,但是apscheuler定时任务执行器的实例需要在多进程下实现共享,很多人想到共享可能会选择方案:1.存储的方式(各种db、redis),2.队列,共享内存等等方案…存储的方式需要的条件:实例是需要可被序列化的,但是apscheuler定时任务执行器的实例是不可被序列化的,所以第一种方案不可行。队列的方式可以实现,但是使用uwsgi启动,每个进程是需要单独的去维护队列,维护成本较高;共享内存的方式可以完美解决,但是因为定时任务的数量不可预测,容易导致内存溢出。关于这上面的解决方案,我都踩过坑,最后使用了一个分布式中常用的方式,使用redis锁的方式,实现原理如下:
每一个进程中都运行一个定时任务的实例,至于哪些进程执行实例,就通过谁先获得redis锁,谁就执行定时任务,没获得锁的任务不执行。
首先,uwsgi的的配置uwsgi.ini的配置如下(只贴出核心部分):
processes=4
enable-threads = true
django项目的wsgi文件配置如下:
try:
from test.regJob import scheduler
scheduler.start()
except Exception as rel:
print("scheduler start error:",rel)
scheduler定时任务,上文中的regJob.py:
@contextmanager
def redisLock(name, timeout=240):
#redis
cache=get_redis_connection("default")
today_string = datetime.datetime.now().strftime("%Y-%m-%d")
key = f"Lock.{name}.{today_string}"
try:
lock = cache.set(key, value=1, nx=True, ex=timeout)
yield lock
finally:
print("out Lock.................")
cache.delete(key) # 释放锁
print("out Lock OK")
scheduler=BackgroundScheduler()
#定时任务的存储器可以使用:DjangoJobStore(),当然你也可以自定义
scheduler.add_jobstore(DjangoJobStore(), 'default')
def myJob(arginfo):
with redisLock(arginfo) as lock:
if lock:
print("Locking................",lock)
#此处就是你需要执行的任务
else:
print("unLocking...............")
#这个地方是必须的,因为释放锁的时候是需要加延时的,因为任务多进程中高并发的情况下,可能会出现在其他的进程在开始获取锁的时候,有的进程已经释放了锁,这样就会导致定时任务重复执行
time.sleep(10)
然后在项目app其他的地方调用scheduler这个实例进行增加任务、删除任务、暂停任务都是可以的
scheduler.add_job(myJob,cron,day_of_week=dayOfWeek, hour=hour, minute=minute, second=second, id=id,args=[arginfo],coalesce=True,misfire_grace_time=3600)
#其中关于coalesce和misfire_grace_time函数请见Apscheduler官方文档说明,再次不赘述
最后:原理就是所有线程都存在scheduler,每个scheduler都是互相隔离的,但是通过redis锁的方式决定,获得到锁的进程中去执行定时任务,而没有获得到锁的进程中就不去执行,并且在添加新任务的时候,在改进程中添加的定时任务如果在执行器没有休眠的时候,那么执行器就会执行该任务,如果休眠,每个进程中重新唤醒的执行器会重新去争夺redis锁来确定执行任务的先后顺序。
另解决一个Apscheduler报错问题,因为在uwsgi是启用的多进程,然后每个进程中都存在一个执行器的实例,在定时任务的数据表django_apscheduler_djangojobexecution中每一个任务其实是有4个实例,并且会报一个get() returned more than one %s – it returned %s的一个报错,其实这个报错的原因是因为:他使用的django的orm的get方法,因为get如果获取到的是多条而不是唯一就会报错,通过查询源码,修改如下:
找到django的包,方法重写:
/home/虚拟环境地址/lib/python3.8/site-packages/django/db/models/query.py
找到
def get(self, *args, **kwargs):
"""
Perform the query and return a single object matching the given
keyword arguments.
"""
clone = self._chain() if self.query.combinator else self.filter(*args, **kwargs)
if self.query.can_filter() and not self.query.distinct_fields:
clone = clone.order_by()
limit = None
if not clone.query.select_for_update or connections[clone.db].features.supports_select_for_update_with_limit:
limit = MAX_GET_RESULTS
clone.query.set_limits(high=limit)
num = len(clone)
#此处num==1改成num>=1就好
if num >= 1:
return clone._result_cache[0]
if not num:
raise self.model.DoesNotExist(
"%s matching query does not exist." %
self.model._meta.object_name
)
raise self.model.MultipleObjectsReturned(
'get() returned more than one %s -- it returned %s!' % (
self.model._meta.object_name,
num if not limit or num < limit else 'more than %s' % (limit - 1),
)
)