celery+flask+数据库一个队列执行多个任务出现死锁

celery创建一个共享队列rpa,该队列下有一个轮循任务和执行任务

轮循任务会读取redis队列,循环队列并根据任务情况执行任务

轮循间隔为1s,每次轮循都会循环队列的所有任务

启动命令为

 celery -A app.celery worker -P gevent -c 1 --loglevel INFO   -Q  rpa

此时设置并发数为1时,用户启动多个工具时,这些工具会排序完成

若将并发数改为1以上的数值,则会出现死锁,具体情况:
用户启动多个工具时,如启动5个工具,2个工具此时状态为运行中,3个工具为等待状态,此时会同时运行这两个工具,而这两个工具共同读写一张表,就会出现资源争夺的情况,导致两个工具都无法运行完成。并且数据库无法修改,进行删除操作。

  1. 猜测原因: 多个程序共享数据库出现死锁
当 Celery 与数据库交互时,如果数据库出现死锁,可能会导致任务无法正常执行或无法及时完成,进而影响 Celery 的工作流程。这里我们可以从几个方面来分析问题的原因、影响以及解决方案。

1. 什么是数据库死锁
数据库死锁发生在多个事务相互等待对方持有的资源,且永远无法释放,导致所有相关事务无法继续执行。死锁通常会发生在数据库操作的事务隔离级别较高时,尤其是在使用 SELECT FOR UPDATE 或 LOCK 等加锁操作时。

2. Celery 和数据库死锁的关系
在 Celery 中,任务的执行通常依赖于外部的数据库操作。例如,Celery 任务可能会插入、更新或查询数据库中的数据。如果数据库在这些操作期间发生死锁,Celery 任务就会被阻塞,无法继续执行,从而导致任务执行失败或超时。

例如,假设某个 Celery 任务试图更新数据库中的某一行数据,并且该行数据已经被另一个任务所锁定。如果第二个任务也试图更新相同的数据,且彼此之间形成死锁,就会导致数据库死锁。

3. Celery 死锁的常见原因
长时间运行的数据库事务:Celery 任务可能会在数据库上执行长时间运行的操作,导致其他任务无法获取必要的锁,进而形成死锁。

事务隔离级别问题:高事务隔离级别(如 Serializable)可能导致更频繁的锁竞争,从而增加死锁发生的可能性。

数据库锁竞争:如果多个 Celery 任务在并发时访问数据库中的同一行数据或表时,可能发生资源锁的竞争,从而导致死锁。

事务未提交或回滚:在 Celery 任务执行过程中,如果事务没有在预期时间内提交或回滚,可能会导致持有锁的时间过长,增加死锁的风险。

4. 如何检测和诊断 Celery 死锁
数据库日志:检查数据库的日志(如 MySQL 的 SHOW ENGINE INNODB STATUS 或 PostgreSQL 的 pg_stat_activity)以查看是否有死锁信息。

Celery 任务日志:Celery 的日志文件可能包含任务超时或失败的信息,查看这些日志可以帮助确定是否有任务因数据库死锁而失败。

数据库锁监控:使用数据库提供的锁监控工具,查看锁的等待情况,识别死锁。

5. 解决方案
a. 优化数据库事务
减少事务时间:确保数据库事务尽可能短,避免长时间占用数据库资源。Celery 任务可以尽量分拆成更小的单元,减少每个任务执行的时间。

合适的事务隔离级别:根据具体场景调整数据库的事务隔离级别。例如,使用 READ COMMITTED 或 READ UNCOMMITTED 可以减少锁竞争,而 SERIALIZABLE 会增加死锁的风险。

适时提交和回滚事务:确保数据库事务在任务完成时及时提交或回滚,避免事务长时间占用资源。

b. 使用 Celery 的超时机制
Celery 支持设置任务的超时时间 (task_time_limit 和 soft_time_limit),可以确保在任务执行超过预定时间时被强制终止,防止死锁或长时间阻塞任务。

python
@app.task(time_limit=300, soft_time_limit=240)
def my_task():
    # 数据库操作
    pass
c. 使用数据库重试机制
使用数据库连接池和重试机制,在遇到数据库死锁或其他故障时,自动重试任务。对于数据库连接池,像 SQLAlchemy 提供了对数据库重试的支持,而 Celery 本身也可以实现任务重试策略。

python
@app.task(bind=True, max_retries=3)
def my_task(self):
    try:
        # 执行数据库操作
    except SomeDatabaseLockException as exc:
        raise self.retry(exc=exc)
d. 避免死锁的设计模式
顺序获取锁:如果 Celery 任务需要锁定多个数据库资源,确保所有任务都按相同的顺序获取锁,避免形成循环等待。

使用悲观锁(Pessimistic Locking):在数据库操作时使用 FOR UPDATE 锁定数据,但要小心长时间持有锁。

合理分配任务:如果多个任务需要频繁访问同一资源,考虑将资源分配的粒度降低,避免多个任务同时访问同一资源。

e. Celery 的任务队列优化
使用专用队列:为需要高并发的任务设置专门的 Celery 队列,避免大量的任务同时竞争数据库资源。

优先级队列:使用 Celery 支持的任务优先级功能,为重要的任务设置更高的优先级,避免它们长时间等待。

6. 数据库死锁恢复策略
如果数据库发生死锁,最常见的恢复方法是由数据库引擎自动选择其中一个事务回滚(通常是较早发起的事务),从而解锁资源。可以通过增加数据库的监控和告警系统,及时发现死锁并进行处理。

在 Celery 中,可以通过重试机制来应对数据库死锁导致的任务失败。当任务因为死锁失败时,Celery 可以自动重试,直到任务成功或达到最大重试次数。

python
@app.task(bind=True, max_retries=5, default_retry_delay=60)
def my_task(self):
    try:
        # 数据库操作
    except SomeDatabaseDeadlockException as exc:
        raise self.retry(exc=exc)
总结
数据库死锁会导致 Celery 任务阻塞或失败,严重影响系统的并发能力。
死锁的发生通常是由于数据库操作时的资源竞争和长时间事务未提交所引起的。
解决死锁问题可以通过优化数据库事务、使用重试机制、提高 Celery 的超时控制等方式来实现。
及时监控和诊断数据库死锁情况,采取合适的策略来规避和处理死锁,是确保 Celery 和数据库高效运行的关键。

当前方案:#

轮循任务:

  1. 读取redis中的所有任务队列
  2. 遍历所有任务
  3. 监控任务状态
  4. 若为start,则启动;若为running,则执行,以推动节点进程

尝试以下方案:#

方案一#

轮循任务:

作者:Gim

出处:https://www.cnblogs.com/Gimm/p/18647434

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   踩坑大王  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示