• 【原创】大叔案例分享(2)处理大批量数据时如何实现“高效”同时实现“断点续传”功能


    问题

    有一个发送100w短信的任务,如何尽量缩短发送时间,同时在中途因为各种原因任务挂掉时,比如发送完50w时任务挂掉,重启任务之后只发送剩余50w短信?

    这是一个比较通用的问题,容易想到的办法是:

    方案一

    步骤

    1. 使用数据库来存放所有数据(比如100w条待发送短信),同时设置status,未处理是0,已处理是1;
    2. 任务执行时查询所有status=0的数据,即待处理数据;
      1.   逐条处理,并且在处理之后设置status=1,避免重复处理;

    分析

    这样可以解决问题,但是有一些缺点

    • a 串行处理数据效率很低;
    • b 数据必须存放在可update的存储,比如数据库,不支持hive、文件这种不可update的存储;
    • c 传统RDBMS对数据量的支持有限;
    • d 即使串行,极端情况下仍有可能有1条数据被重复处理(重复发送短信);

    缺点d是因为发送操作和更新数据库状态是分开的,而且发送操作无法回滚,所以无法通过分布式事务来根本解决,这个问题基本无解,即总是有可能重复处理,只能尽量让重复发送的概率降低

    既然缺点d无法避免,可以通过多线程+批量提交事务来解决缺点a:

    方案二

    步骤

    1. 同上
    2. 同上
      1.   每次读取B条数据,将数据平均分发给C个线程来同时处理,每个线程处理完D条数据后批量设置这D条数据的status=1;

    (比如每次读取1000条status=0的短信,同时分给10个线程发送,每个线程发送100条短信,并且每个线程每成功发送20条后批量更新这20条短信的status=1)

    分析

    方案二相对方案一

    理论上可以将时间缩短为1/C(上例中1/10)以下,同时最悲观的情况下有可能会有C*D个数据(上例中10*20=200)被重复处理,即解决缺点a时放大缺点d,同时缺点b和c依然存在;

    有没有可能同时解决缺点a、b、c?

    方案三

    步骤

    1. 待处理数据存放在数据库、或者hive、或者文件;
    2. 处理前首先查询当前任务上次处理处理的offset(如果是数据库或者hive,offset就是自增id或者uuid;如果是文件,offset就是行数),如果不存在,则设置offset=0,offset可以存放在数据库中;
    3. 从上次处理的offset开始继续查询待处理数据;(数据库或者hive即排序查询跳过前边的行,文件则直接按照行数跳过前边的行)
      1.   每次查询B个数据,分发给C个线程处理,B个数据全部处理完时设置offset+=B;

    分析

    方案三相比方案二

    缺点a方面,节省了99%以上的数据库操作,效率提升N倍;
    缺点b和缺点c方面,完美解决;
    缺点d方面,方案三最多有B个数据重复处理(上例中的1000),方案二最多有C*D个数据重复处理(上例中的10*20=200),即方案三相对方案二放大了缺点d;

    有没有可能在缺点d上进一步优化?

    方案四

    步骤

    1. 同上
    2. 同上
    3. 同上
      1.   每次查询B个数据,分发给C个线程处理,每个线程都在cache(memcache或redis,放数据库也可以)里维护一个当前处理的relative_offset,如果relative_offset!=0,则跳过前relative_offset个数据;所有线程处理完后,设置所有线程的relative_offset=0,同时设置offset+=B;

    (比如每次查询1000个未发送短信,分发给10个线程处理,每个线程都在cache里维护一个relative_offset,初始值是0,每发送一条消息都将该线程cache里的relative_offset+=1,10个线程处理完,设置所有线程的relative_offset=0,同时设置offset+=1000)

    问题回放

    发送8000条短信后取出下1000条待发送短信,即8001-9000,这时分给10个线程,第1个线程分发短信为8001-8100,第2个线程分发短信为8101-8200,...,第10个线程分发短信为8901-9000,发送一段时间后,第1个线程的relative_offset是49,第2个线程的relative_offset是31,其他线程任意,这时进程挂掉,任务重启后,拿到offset=8000,这次取出下1000条待发送短信,依旧分给10个线程,第1个线程分发短信为8001-8100,第2个线程分发短信为8101-8200,...,第10个线程分发短信为8901-9000,第1个线程发现relative_offset=49,则继续发送8050-8100的短信,第2个线程发现relative_offset=31,则继续发送8132-8200的短信,其他线程以此类推。

    分析

    方案四相比方案三

    缺点d方面,最悲观的情况下,方案三和方案四都会有B条数据重复发送,但方案四发生的概率极低(只有在设置所有线程的relative_offset=0和设置offset+=1000之间挂掉才会发生),并且在极大的概率上最多只有C条数据重复发送,这个比方案二的C*D的效果要好很多;

    综上,方案四可以同时实现“高效”+“断点续传”,并且这是一个比较通用的方案,可以实现一套基类,将读取待处理数据以及处理单条数据作为template method由子类实现,快速应用到所有问题场景中。

  • 相关阅读:
    《WF编程》系列之17 工作流与外部事件:工作流参数 居然有两个多月没有更新WF笔记,这段时间也许真的太忙了,毕业的事情,工作的事情,从今天起继续更新.
    《WF编程》系列之13 XAML激活 2.3.3 XAML激活
    《WF编程》系列之9 编译工作流:使用WFC.EXE
    《WF编程》系列之11 编译工作流:使用MSBUILD
    《WF编程》系列之15 顺序工作流与SequenceActivity 3 顺序工作流
    《WF编程》系列之16 工作流与外部世界:生存周期事件 3.2 工作流与外部世界
    windows2003技巧
    ASP.NET ViewState 初探
    FreeTextBox实现机制
    .NET实用设计模式:工厂模式(Factory)
  • 原文地址:https://www.cnblogs.com/barneywill/p/9820756.html
Copyright © 2020-2023  润新知