[英]State machine transitions at specific times
简化示例:
我有一个待办事项。 它可以是未来,当前或晚期,具体取决于它的时间。
Time State
8:00 am Future
9:00 am Current
10:00 am Late
因此,在此示例中,待办事项从上午9点到上午10点是“当前”。
最初,我考虑为“current_at”和“late_at”添加字段,然后使用实例方法返回状态。 我可以查询now > current and now < late
所有“当前”待办事项。
简而言之,我每次都会计算状态,或者使用SQL来提取我需要的状态集。
如果我想使用状态机,我会有一组状态,并将该状态名称存储在待办事项上。 但是,如何在每个待办事项的特定时间触发州之间的过渡?
在尝试在特定时间处理大量状态转换时,是否有人有管理这些选项的经验?
感觉就像一台状态机,我只是不确定管理所有这些转换的最佳方法。
回复后更新:
因此,我希望更多的是类似状态机的想法,以便我可以封装过渡。
我设计并维护了几个管理大量这些小型状态机的系统。 (某些系统,最高100K /天,约100K /分钟)
我发现你明确表达的状态越多,就越有可能打破某个地方。 或者换句话说, 推断出的状态越多,解决方案就越健壮。
话虽如此,你必须保持一些状态。 但尽量保持尽可能小。
此外,将状态机逻辑保持在一个位置使系统更健壮,更易于维护。 也就是说,不要将状态机逻辑放在代码和数据库中。 我更喜欢代码中的逻辑。
对于你的例子,我会有一个非常简单的表:
task_id, current_at, current_duration, is_done, is_deleted, description...
并根据current_at
和current_duration
推断基于now
的状态。 这非常有效。 确保在current_at
上索引/分区表。
当您需要在转换更改时触发事件时,情况会有所不同。
将表格更改为如下所示:
task_id, current_at, current_duration, state, locked_by, locked_until, description...
将索引保存在current_at
,如果愿意,可以添加一个on state
。 你现在正在破坏状态,所以由于并发或失败,事情locked_by
得更脆弱,所以我们必须使用locked_by
和locked_until
进行一点点的乐观锁定,我将在下面描述。
我假设你的程序在处理过程中会失败 - 即使只是为了部署。
您需要一种机制将任务从一种状态转换为另一种状态。 为了简化讨论,我将关注从FUTURE转到CURRENT,但无论过渡如何,逻辑都是一样的。
如果您的数据集足够大,您不断轮询数据库以发现发现需要转换的任务(当然,当没有任何事情可做时,线性或指数退避); 否则你使用或你最喜欢的调度程序是基于 cron还是ruby ,如果你订阅了Java / Scala / C#,则使用Quartz。
选择需要从FUTURE移动到CURRENT 且当前未锁定的所有条目。
( 更新 :)
-- move from pending to current
select task_id
from tasks
where now >= current_at
and (locked_until is null OR locked_until < now)
and state == 'PENDING'
and current_at >= (now - 3 days) -- optimization
limit :LIMIT -- optimization
将所有这些task_id
放入可靠的队列中。 或者,如果必须,只需在脚本中处理它们。
当您开始处理某个项目时,必须先使用我们的乐观锁定方案将其锁定:
update tasks
set locked_by = :worker_id -- unique identifier for host + process + thread
, locked_until = now + 5 minutes -- however this looks in your SQL langage
where task_id = :task_id -- you can lock multiple tasks here if necessary
and (locked_until is null OR locked_until < now) -- only if it's not locked!
现在,如果你真的更新了记录,你就拥有了锁。 您现在可以启动特殊的转换逻辑。 (掌声。这就是让你与所有其他任务经理不同的原因,对吧?)
如果成功 ,请更新任务状态,确保仍使用乐观锁定:
update tasks
set state = :new_state
, locked_until = null -- explicitly release the lock (an optimization, really)
where task_id = :task_id
and locked_by = :worker_id -- make sure we still own the lock
-- no-one really cares if we overstep our time-bounds
只有在多个线程或进程批量更新任务时(例如在cron作业中或轮询数据库),才能执行此操作! 问题是他们每个人都会从数据库中获得类似的结果,然后争先恐后地锁定每一行。 这是低效的,因为它会减慢数据库的速度,并且因为你的线程基本上什么都不做,只会减慢其他线程的速度。
因此,添加查询返回的结果数量限制并遵循此算法:
results = database.tasks_to_move_to_current_state :limit => BATCH_SIZE
while !results.empty
results.shuffle! # make sure we're not in lock step with another worker
contention_count = 0
results.each do |task_id|
if database.lock_task :task_id => task_id
on_transition_to_current task_id
else
contention_count += 1
end
break if contention_count > MAX_CONTENTION_COUNT # too much contention!
done
results = database.tasks_to_move_to_current_state :limit => BATCH_SIZE
end
使用BATCH_SIZE
和MAX_CONTENTION_COUNT
直到程序超快。
更新:
乐观锁定允许并行处理多个处理器。
通过锁定超时(通过locked_until
字段),它允许在处理转换时失败。 如果处理器发生故障,另一个处理器能够在超时后接收任务(上述代码中为5分钟)。 因此,重要的是a)只在你要处理任务时锁定任务; 和b)锁定任务完成任务需要多长时间以及慷慨的余地。
locked_by
字段主要用于调试目的(这个进程/机器是打开的吗?)如果数据库驱动程序返回更新的行数,则只有在一次更新一行时才能拥有locked_until
字段。
在特定时间管理所有这些转换似乎很棘手。 也许您可以使用像DelayedJob这样的东西来安排转换,这样每分钟就不需要一个cron作业,从故障中恢复会更加自动化?
否则 - 如果这是Ruby,使用Enumerable一个选项?
像这样(在未经测试的伪代码中,使用简单的方法)
def state
if to_do.future?
return "Future"
elsif to_do.current?
return "Current"
elsif to_do.late?
return "Late"
else
return "must not have been important"
end
end
def future?
Time.now.hour <= 8
end
def current?
Time.now.hour == 9
end
def late?
Time.now.hour >= 10
end
def self.find_current_to_dos
self.find(:all, :conditions => " 1=1 /* or whatever */ ").select(&:state == 'Current')
end
适用于中等大小数据集的一个简单解决方案是使用SQL数据库。 每个待办事项记录应具有“state_id”,“current_at”和“late_at”字段。 你可以省略“future_at”,除非你真的有四种状态。
这允许三种状态:
将状态存储为state_id
(可选地将外键设置为名为“states”的查找表,其中1: Future
, 2: Current
, 3: Late
)基本上存储非规范化数据,这样可以避免重新计算状态,因为它很少变化。
如果您实际上并未根据状态查询待办事项记录(例如... WHERE state_id = 1
)或在状态发生变化时触发一些副作用(例如发送电子邮件),则可能您不需要管理状态。 如果你只是向用户显示一个待办事项列表并指出哪些是迟到的,那么最便宜的实现甚至可能是计算客户端。 为了回答,我假设您需要管理状态。
您有几个更新state_id的选项。 我假设你正在强制执行约束current_at < late_at
。
最简单的是更新每条记录: UPDATE todos SET state_id = CASE WHEN late_at <= NOW() THEN 3 WHEN current_at <= NOW() THEN 2 ELSE 1 END;
。
你可能会得到更好的性能(如在一个事务中) UPDATE todos SET state_id = 3 WHERE state_id <> 3 AND late_at <= NOW()
, UPDATE todos SET state_id = 2 WHERE state_id <> 2 AND NOW() < late_at AND current_at <= NOW()
, UPDATE todos SET state_id = 1 WHERE state_id <> 1 AND NOW() < current_at
。 这样可以避免检索不需要更新的行,但是你需要索引“late_at”和“future_at”(你可以尝试索引“state_id”,见下面的注释)。 您可以根据需要经常运行这三个更新。
上面的略微变化是首先获取记录的ID,因此您可以使用已更改状态的待办事项执行某些操作。 这看起来像SELECT id FROM todos WHERE state_id <> 3 AND late_at <= NOW() FOR UPDATE
。 然后你应该像UPDATE todos SET state_id = 3 WHERE id IN (:ids)
那样进行UPDATE todos SET state_id = 3 WHERE id IN (:ids)
。 现在,您仍然可以使用ID来执行某些操作(例如,通过电子邮件发送通知“20个任务已过期”)。
为每个待办事项安排或排队更新作业(例如,在上午10点将此一个更新为“当前”并在晚上11点“更晚”更新)将导致大量预定作业,至少是待机数量的两倍,以及性能不佳 - 每个预定作业仅更新单个记录。
您可以安排批量更新,例如UPDATE state_id = 2 WHERE ID IN (1,2,3,4,5,...)
,其中您已经预先计算了在某个特定时间附近将变为当前的待办事项ID列表。 由于几个原因,这在实践中可能不会很好。 一个是todo的current_at
和late_at
字段可能会在您安排更新后更改。
注意:通过索引“state_id”可能无法获得太多收益,因为它只将数据集划分为三组。 这可能不足以让查询规划器考虑在SELECT * FROM todos WHERE state_id = 1
等查询中使用它。
你没有讨论的这个问题的关键是完成的待办事项会发生什么? 如果您将它们留在此待办事项表中,该表将无限增长,您的性能会随着时间的推移而降低 。 解决方案是将数据分区为两个单独的表(如“completed_todos”和“pending_todos”)。 然后,您可以在实际需要时使用UNION
连接两个表。
状态机是由某种东西驱动的。 用户交互或流中的最后一个输入,对吧? 在这种情况下,时间驱动状态机。 我认为一个cron工作是正确的。 这将是驱动机器的时钟。
对于它的价值是非常困难的,在两列上设置一个有效的索引,你必须做这样的范围。
now> current && now <late将很难以高效的方式在数据库中表示为任务的属性
ID |标题| future_time | CURRENT_TIME | late_time
1 |你好| 8:上午12点| 9:上午12点| 10:上午12点
永远不要试图强迫模式成问题。 事情是相反的。 所以,直接找到一个好的解决方案。
这是一个想法:(我理解你的是)
使用持久警报和一个受监视的进程来“消耗”它们。 其次,查询它们。
这将允许您:
我强调要使用某种监视程序监视该进程,以确保及时发送这些警报(或者,在最坏的情况下,在崩溃之后有一些延迟或类似的事情)。
请注意:持有这些警报的事实可以让您做到以下两点:
根据我的经验,当你有一个外部进程作用于某个东西,并用它的状态更新数据库时,SQL中的状态机最有用。 例如,我们有一个上传和转换视频的流程。 我们使用数据库随时跟踪视频发生的情况,以及接下来会发生什么。
在您的情况下,我认为您可以(并且应该)使用SQL来解决您的问题而不必担心使用状态机:
制作一个todo_states表:
todo_id todo_state_id datetime notified
1 1 (future) 8:00 0
1 2 (current) 9:00 0
1 3 (late) 10:00 0
您的SQL查询,其中所有实际工作发生:
SELECT todo_id, MAX(todo_state_id) AS todo_state_id
FROM todo_states
WHERE time < NOW()
GROUP BY todo_id
当前活动状态始终是您选择的状态。 如果您只想通知用户一次,请插入带有notify = 0的原始状态,并在第一个选择时将其碰撞。
一旦任务“完成”,您可以将另一个状态插入到todo_states表中,或者只是删除与任务相关联的所有状态,并在待办事项中引发“完成”标志,或者在您的情况下最有用的任何内容。
不要忘记清理过时状态。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.