php集群下定时任务多次执行,Uwsgi+Django多进程下Apscheduler定时任务动态添加、任务重复执行及解决定时任务中高并发的问题(适用于分布式)...

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),

)

)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值