【问题标题】:Find the flaws! Performing a long task reliably with the task queue找出漏洞!使用任务队列可靠地执行长任务
【发布时间】:2011-02-24 23:00:43
【问题描述】:

我正在 Google 应用引擎上制作成绩册。我会在每个评分期间跟踪每个学生的成绩。评分周期可以重叠。由于我可能一次显示数百个这样的成绩,我预先计算了服务器上的成绩。因此,对于任何一个学生,我可能有许多计算成绩 - 每个评分周期一个。

现在,教师输入测验的新分数。该分数可能会影响许多计算的成绩,因为它可能属于许多评分期。我需要重新计算所有受影响的成绩。这可能需要很长时间,因为对于每个评分期,我需要获取所有相关分数并对这些分数执行复杂的例程。我认为 30 秒是不够的 - 特别是如果今天数据存储区感觉很慢的话。此外,失败不是一种选择。一些成绩更新而另一些成绩悄然过时是不可接受的。

所以我在想,这是了解任务队列的好时机!

我不是数据库结构或任何方面的专家,但这里是我想要做的事情的概述:

public ReturnCode addNewScore(Float score, Date date, Long studentId)
{
    List<CalculatedGrade> existingGrades = getAllRelevantGradesForStudent(studentId, date);

    for (CalculatedGrade grade : existingGrades)
    {
        grade.markDirty(); //leaves a record that this grade is no longer up to date
    }

    persistenceManager.makePersistentAll(existingGrades);
    //DANGER ZONE?
    persistenceManager.makePersistent(new IndividualScore(score, date, studentId));

    tellTheTaskQueueToStartCalculating();

    return OMG_IT_WORKED;
}

这似乎是一种将所有相关等级标记为脏的快速方法。如果中途失败,则返回失败,客户端将知道重试。如果客户端稍后尝试获取脏成绩,我们可以在那里返回错误。

然后,任务队列代码将如下所示:

public void calculateThemGrades()
{
    List<CalculatedGrade> dirtyGrades = getAllDirtyGrades();

    try
    {
        for (CalculatedGrade grade : dirtyGrades)
        {
            List<Score> relevantScores = getAllRelevantScores();
            Float cleanGrade = calculateGrade(relevantScores);
            grade.setGrade(cleanGrade);
            grade.markClean();

            persistenceManager.flush();
        }
    }
    catch(Throwable anything)
    {
        //if there was any problem, like we ran out of time or the datastore is down or whatever, just try again
        tellTheTaskQueueToStartCalculating()
    }
}

这是我的问题:这是否保证在添加新分数后永远不会有计算成绩被标记为干净?

关注的具体领域:

  • 在第一个 sn-p 中,existingGrades 是否总是在新的 IndividualScore 之前,在危险区域附近?
  • 是否有可能另一个线程将在危险区域启动任务队列代码,以便在真正输入新的IndividualScore 之前将那些现有的等级再次标记为干净?如果是这样,我如何确保不会发生这种情况(所有年级的交易都已结束)?
  • 即使下午没有关闭,persistenceManager.flush() 是否足以保存部分完成的计算?

这一定是一种常见的问题。我将不胜感激任何指向教程的链接,尤其是那些 appengine 的链接。感谢您阅读这么多!

【问题讨论】:

  • 我有一个做心理测试的产品。用户通过测试输入原始数字,我在构建报告时进行复杂计算循环。尽管有数以千计的方程式与您正在做的类型非常相似,但几乎没有任何滞后,即使同时支持多个用户也是如此。如果只是你使用这个,你可能会有点矫枉过正......
  • 感谢您的观点。我预计每次计算需要获取大约 1000 个实体,包括分数和其他一些东西,然后平均放回 10 个实体。可能这可以在单个请求中运行,但我需要一些永远不会使数据处于不一致状态的东西。正是在处理错误时,我开始担心时间不够了——我已经偶尔在其他一些请求中时间不够了。
  • 嗯...但是,我想我可以在实际要求成绩时进行更多计算,如果失败返回相同的错误消息,否则我会...

标签: java google-app-engine transactions google-cloud-datastore distributed-transactions


【解决方案1】:

如果您担心竞争条件,请不要使用布尔脏标志 - 而是使用一对时间戳。当您想将记录标记为脏时,请更新“脏”时间戳。

当您开始计算成绩时,记下“脏”时间戳是什么。

当您计算完成绩后,将“干净”时间戳更新为等于您开始时读取的“脏”时间戳的值,这表示您已将该成绩与截至该时间戳的新数据同步.

任何“脏”时间戳大于其“干净”时间戳的记录都是脏的。两场比赛干净的任何记录。简单有效。如果另一个请求添加了会影响给定成绩的新数据,而您的任务队列任务已经在计算成绩,“脏”时间戳将与更新的“干净”时间戳不匹配,因此任务队列将考虑记录仍然很脏,然后重新处理。

【讨论】:

  • 不幸的是,我认为我不能通过比较时间戳来查询记录 - 无法在 appengine 数据存储上说 select * from Records where dirty &gt; clean。如果我要重写 difference 字段,我也许可以用减法做一些花哨的事情,看起来我可能会以一种危险的方式覆盖。
  • 我可以为我想标记为脏的每个年级制作一个特殊的RecalculateRequest 实体,带有时间戳。然后我可以搜索RecalculateRequests,在Grade 实体中设置cleanAsOf 时间戳,并且只有在时间戳赶上时才删除RecalculateRequests...
  • 是的,这会起作用(或者只允许任何给定等级的多个重新计算请求,并始终删除早于 cleanAsOf 时间戳的那些)。
  • @Riley 相反,您可以修改 Amber 的提议:有一个“脏”时间戳。完成后,当且仅当它与您开始使用的时间戳匹配时,以事务方式清除时间戳(将其设置为 Null)。
  • 好主意。需要明确的是,这段代码看起来像startTime = grade.getTimestamp(); grade.setScore(complicatedCalculation()); tx.begin(); if (startTime.equals(grade.getTimestamp()) grade.setTimestamp(null); tx.commit();?还是我需要重新获取交易中的成绩?
猜你喜欢
  • 1970-01-01
  • 2018-07-02
  • 1970-01-01
  • 2011-07-28
  • 1970-01-01
  • 2019-07-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多