【续集】业务实战记录4:多维表插入数据任务丢失处理与思考

文章讨论了在使用多线程执行任务时遇到的错误处理问题,提出了两种方案。初步方案是捕获错误任务并立即重跑,但当任务量增大时可能会仍有失败。最终方案是通过内函数循环执行错误任务,不断重试直到成功,提高了代码的稳定性和效率。这种方法适用于处理因请求频繁或其他异常导致的任务失败情况。
摘要由CSDN通过智能技术生成

前言:问题再现

上次讲了一个关于多线程任务丢失处理的思考,介绍了三个解决方案:多线程加停顿、改用单线程、使用多线程多 Token。【补充链接:业务实战记录4:多维表插入数据任务丢失处理与思考
这段时间有一些“续集”,来补充一下:
先讲一下结论,就是通过前两种方法,还是有一些其他的问题。

下面挨个展开说说。
上次遇到该问题加了停顿,然后测试两次,都没有问题,所以就得出了这个加停顿有效的结论。事实上,它确实是有效的,但是它仅仅解决了请求太多次导致的任务丢失问题,这并不能解决其他的问题,比如说:Discover a new version, please refresh the page.,所以还是会有错误。这个通过停顿无法解决,需要思考其他的解决方案。
而改用单线程,这个也能一定程度上解决因为请求太多次导致的任务丢失问题,但是当同步的数据量比较大的时候,会被限制,出现Connection is closed.报错。

单线程由于请求时间过长,会被关闭连接,这是一个明显的瓶颈,因此我们需要回归多线程的方案,考虑从多线程寻找解决方案。
那么问题来了,当出现报错时,能不能将其捕捉到,并对其进行重跑呢?如果能又该怎么实现?
如果能对报错的任务进行捕捉和重跑,将能提高代码的效率和保证代码的稳定性。事实上是可以的,前人已经将这类问题解决了,并提供了相关的接口给我们去调用。

初步方案:捕获错误任务并重跑

不过代码有点长,具体如下:
第 17~32 行是正常执行任务,第 34~50 行是执行错误的任务。

from concurrent.futures import ThreadPoolExecutor, as_completed
import concurrent.futures
import random
import time


def do_work(task_id):
    wait_time = random.randint(1, 3)
    time.sleep(wait_time)
    if random.random() < 0.25:  # 随机抛出异常
        raise Exception(f"Task {task_id} failed")
    return task_id, wait_time


def main():
    task_ids = list(range(10))  # 总共 10 个任务
    failed_tasks = []  # 用于记录失败的任务

    with ThreadPoolExecutor(max_workers=5) as executor:
        future_to_task = {executor.submit(do_work, task_id): task_id for task_id in task_ids}

        # 添加 wait 语句,等待任务完成或超时
        # 添加了 wait(future_to_task) 语句。这样可以确保在执行 as_completed 之前,wait 函数会等待所有任务完成或超时。
        # 若要设置超时,可以在括号里添加 timeout 参数,如:wait(future_to_task, timeout=10)
        concurrent.futures.wait(future_to_task)
        
        for future in as_completed(future_to_task):
            task_id = future_to_task[future]
            try:
                task_id, wait_time = future.result()
                print(f"Task {task_id} completed, waited for {wait_time} seconds")
            except Exception as e:
                print(f"Task {task_id} failed: {e}")
                failed_tasks.append(task_id)

    if failed_tasks:
        print("Retrying failed tasks...")
        with ThreadPoolExecutor(max_workers=5) as executor:
            future_to_task = {executor.submit(do_work, task_id): task_id for task_id in failed_tasks}

            # 添加 wait 语句,等待任务完成或超时
            # 添加了 wait(future_to_task) 语句。这样可以确保在执行 as_completed 之前,wait 函数会等待所有任务完成或超时。
            # 若要设置超时,可以在括号里添加 timeout 参数,如:wait(future_to_task, timeout=10)
            concurrent.futures.wait(future_to_task)

            for future in as_completed(future_to_task):
                task_id = future_to_task[future]
                try:
                    task_id, wait_time = future.result()
                    print(f"Retried task {task_id} completed, waited for {wait_time} seconds")
                except Exception as e:
                    print(f"Retried task {task_id} still failed: {e}")

if __name__ == "__main__":
    main()

以上代码成功的将错误的任务捕捉到了,还进行了重跑。不过由于任务量比较小,只有10个,所以使用以上代码重跑一次,大概率不错出现二次报错,如果你把任务数task_ids加到三五十个,这个时候就会发现,重跑一次还是会有报错!

最终方案:内函数循环执行错误任务

新问题:怎么不断地进行错误的捕捉和重跑,以保证任务都能够正常执行?
可以通过内函数进行循环调用。
将以上代码处理报错部分(第 34~50 行)提取出来,创建一个新的函数,然后调用自己即可。
参考代码如下:

from concurrent.futures import ThreadPoolExecutor, as_completed
import concurrent.futures
import random
import time


def do_work(task_id):
    wait_time = random.randint(1, 3)
    time.sleep(wait_time)
    if random.random() < 0.25:  # 随机抛出异常
        raise Exception(f"Task {task_id} failed")
    return task_id, wait_time


def main():
    task_ids = list(range(40))  # 总共 10 个任务
    failed_tasks = []  # 用于记录失败的任务

    with ThreadPoolExecutor(max_workers=5) as executor:
        future_to_task = {executor.submit(do_work, task_id): task_id for task_id in task_ids}

        # 添加 wait 语句,等待任务完成或超时(可以不加)
        # 添加了 wait(future_to_task) 语句。这样可以确保在执行 as_completed 之前,wait 函数会等待所有任务完成或超时。
        # 若要设置超时,可以在括号里添加 timeout 参数,如:wait(future_to_task, timeout=10)
        concurrent.futures.wait(future_to_task)

        for future in as_completed(future_to_task):
            task_id = future_to_task[future]
            try:
                task_id, wait_time = future.result()
                print(f"Task {task_id} completed, waited for {wait_time} seconds")
            except Exception as e:
                print(f"Task {task_id} failed: {e}")
                failed_tasks.append(task_id)

        redo_failed_tasks(failed_tasks)

        
def redo_failed_tasks(failed_tasks):
    """处理错误任务,内函数反复执行,直到成功!"""
    if failed_tasks:
        print("Retrying failed tasks...")
        with ThreadPoolExecutor(max_workers=5) as executor:
            future_to_task = {executor.submit(do_work, task_id): task_id for task_id in failed_tasks}

            # 添加 wait 语句,等待任务完成或超时(可以不加)
            # 添加了 wait(future_to_task) 语句。这样可以确保在执行 as_completed 之前,wait 函数会等待所有任务完成或超时。
            # 若要设置超时,可以在括号里添加 timeout 参数,如:wait(future_to_task, timeout=10)
            concurrent.futures.wait(future_to_task)
            failed_tasks = []  # 用于记录失败的任务,提交任务之后置空,重新记录失败任务,以便再次提交

            for future in as_completed(future_to_task):
                task_id = future_to_task[future]
                try:
                    task_id, wait_time = future.result()
                    print(f"Retried task {task_id} completed, waited for {wait_time} seconds")
                except Exception as e:
                    print(f"Retried task {task_id} still failed: {e}")
                    failed_tasks.append(task_id)
    else:
        return
    redo_failed_tasks(failed_tasks)

if __name__ == "__main__":
    main()

该代码将错误任务重跑代码封装到redo_failed_tasks()函数,函数中加了处理新失败任务的逻辑,即第 50 和 59 行,并加了内函数调用自己,如果当没有失败任务时则返回,不再自调用。然后再main()函数中调用该报错函数。

这是抽象出来的逻辑,实际在跑同步的任务,需要给任务加一个编号,类似这里的task_id,而且每个task_id对应的数据也需要传递到错误任务的函数(redo_failed_tasks())中,以便重新提交。

一个task_id对应一个数据记录,这很符合字典的逻辑,所以我在处理的过程直接将failed_tasks更换为字典,将task_id作为键,需要重新提交的数据作为值传递到redo_failed_tasks()中,当然还有一些细节,比如说执行任务的函数do_work()也需要传递对应的数据进行提交。

小结

本文探讨了如何处理在开启concurrent.futures.ThreadPoolExecutor多线程执行任务时出现的错误任务。介绍了如何对错误任务进行捕捉和重新提交,以确保所有任务成功执行的方法。该方法其实也可以用于解决上一篇文章所遇到的访问太频繁导致任务丢失的问题。

本文提供的代码相对比较通用,没有太多场景的定制,所以在跑实际业务时需要进行定制化,以适配不同的业务场景。









- End -

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Xin学数据

为你点亮一盏灯,愿你前进无阻。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值