阅读主题
时间边界与业务生命周期
会员将在北京时间 4 月 1 日零点到期。用户在前一天 23:59 开始下载一个大文件,零点到来时还没有下载完:系统应立即中断,还是允许这次下载完成?与此同时,用户存放的文件是否也要删除?
截止时间本身回答不了这些问题。它能参与比较,但业务还要说明这个边界约束哪个动作。本节分别讨论新请求的受理、已有操作的继续和资源回收,让每个期限都有明确的用途。
1.8 时间边界与业务生命周期
1.8.1 时间条件会变化,记录未必发生写入
先看新请求。假设会员边界不变,其他资格条件也满足,程序用 starts_at <= now && now < expires_at 判断当前是否处于有效期。随着 now 前进,这个条件会从假变真,再从真变假;数据库中的两个边界值却可以始终不变。
是否处于有效期,是包含当前时间的计算结果。 一个持久化的 status 字段则需要经过写入才会改变。如果每天零点由批任务把状态改为 EXPIRED,而访问检查只认这个字段,任务晚跑十分钟,就可能多放行十分钟。代码兑现的是“状态更新后失效”,与“零点起不再受理”有了差别。
图 1-6:其他资格满足时,新请求从截止起被拒绝,后台状态整理可以稍后进行。虚线对齐相应时刻,横向不按比例。
要兑现本例的截止,访问检查就应直接判断时间范围,并结合暂停、撤销等其他条件。批任务可以更新展示状态、发送通知或整理记录,但它是否准时运行,不应决定这次访问是否已经过期。持久化状态也仍有独立用途:会员即使尚未到期,也可能已经被撤销;如果业务要求关闭后不可恢复,还要保留相应终态,不能完全交给当前时间计算。
缓存也可能保存一个已经过时的结论。23:59 计算出的 is_valid = true,若被缓存五分钟,零点以后就可能继续放行。记录没有修改,不代表这个依赖 now 的结论仍然成立。
一种做法是缓存起止边界,让读取方在每次判断时读取当前时间;若缓存的是判断结果,则至少不能让肯定结果的复用期限越过权限截止。两者都只处理自然到期:续费改变了截止,撤销改变了资格,仍需更新或失效缓存,并明确允许多长的数据延迟。读取方的时钟也需要满足业务要求。进一步讨论见权限与时钟实验。
1.8.2 哪一步决定请求是否过期
一次请求会经过点击、传输、服务端接收、排队、资格检查和结果保存。截止可能落在任意两步之间,所以“截止前提交”仍不够明确。按客户端点击判断、按服务端收到请求判断,或按实际检查判断,会把网络和排队延迟分配给不同的一方。
本例选定一个具体规则:以服务端执行资格检查时读取的时间为准。用户在 23:59:59 点击,但检查发生在 00:00:01,就不能受理;即使请求在零点前已经到达,排队到零点后才检查,结论也一样。界面应说明以服务端资格检查为准,客户端倒计时和点击时间都不能替代这个判定点。
另一条请求在 23:59:59 通过检查,保存结果时已经是 00:00:01,是否仍可受理?按本例规则可以,因为时间资格取决于检查时刻。但后续执行不能只依靠内存中的“刚才检查过”,还需要一条可靠保存的受理记录。
这条记录应关联本次操作及用户,保存检查时刻、采用的资格依据或版本,以及获准范围。检查通过表示满足本次判定条件,受理成功还要求结果保存成功。 系统只有在保存成功后才返回受理成功;若保存失败,不能让后续流程凭一个未落地的肯定结果继续。
图 1-7:本例按资格检查时刻判断,并保存获准结果供后续使用。乙的检查早于截止且提交成功,因此可以继续;仅仅点击早于截止不满足条件,横向不按比例。
保存的资格应只用于它对应的操作。例如一次下载已经获准,后续数据传输可以引用该下载会话;不能把这条记录当作用户到期后发起任意新下载的通行证。这样,后续阶段检查的是“这次操作是否已有可用的受理结果”,而不是不断用新的 now 推翻同一次受理。
检查与保存之间仍可能发生续期、撤销或其他并发修改。需要通过事务或版本校验等机制,把用于判断的资格与保存的结果协调起来,并明确撤销是否影响已受理操作。若保存成功但响应丢失,重试也应识别原操作,避免重复受理。具体机制留给后续事务、并发与幂等章节,时间比较本身不提供这些保证。
如果业务要求“截止前必须完成持久化”,就必须另行设计能兑现该要求的提交与确认机制。仅仅在提交之前再读一次时钟,仍然不能证明真正完成提交时没有越过截止。这是比本例更严格的承诺,不能只改一句文案就认为已经实现。
1.8.3 继续权限需要独立定义
回到开篇的下载。若产品允许截止前获准的下载完成,零点后就拒绝新下载,同时允许已有会话继续。若产品要求到期中止,则还需要在传输过程中复核并实施终止。入口检查决定能否开始,继续规则决定开始以后能做多久。
“已有下载”也要有可识别的范围。本例可以约定一次受理只对应一个用户、一份文件和一个下载会话。连接中断后重新连接,是恢复原会话还是发起新操作,也应说明;否则客户端只要声称自己是在续传,就可能绕过新请求的截止。若允许原会话无限期继续,还应评估它是否符合产品承诺,必要时另设会话上限。
假设本例进一步约定:截止前获准的下载最多继续到到期后十分钟。新请求仍从 00:00 起被拒绝,已有会话的继续范围则在 00:10 结束,恰好到达该上限也不能继续。资格已经撤销时是否例外允许,应按撤销规则单独判断,不能从这十分钟自动推断。
宽限期由此产生了一个只适用于已有会话的继续上限。若直接把会员统一的 expires_at 改成 00:10,新请求也会多获得十分钟,改变了另一项承诺。因此应分别表达新操作截止和会话继续上限,并明确后者只对哪些受理记录生效。
实际中止还受执行位置影响。服务端可以停止继续发送数据,却无法收回用户已经收到的内容;周期检查、缓冲与网络传输也会产生延迟。产品应明确到点停止的是哪个可控制的动作,以及允许的延迟。一次时间比较无法保证所有远端活动恰好在同一瞬间停止。
时钟容忍与上述宽限期也不同。宽限期是在业务上允许已有操作继续,时钟容忍则用于处理时间来源的误差。即使理由是误差,如果策略允许新请求在截止后两秒内通过,实际可受理范围仍然扩大了。应明确容忍作用于哪个边界、向哪个方向调整以及代价,不能因为名称叫“容忍”就忽略结果。下一节会展开时间来源与误差。
1.8.4 权限结束与资源回收采用各自期限
停止会员访问,并不意味着立即删除文件。假设产品承诺到期后仍保留数据供恢复,并另设导出窗口,那么访问、恢复、导出和删除就分别有自己的条件。延长保留期不能自动恢复访问,提前撤销访问也不应无声缩短仍然承诺的保留期限。
可以把访问的排他截止记为 access_expires_at,恢复窗口的排他截止记为 recoverable_until,最早允许清理的时间记为 delete_after。若承诺恢复窗口内保留数据,delete_after 就不能早于 recoverable_until。具体保留多久、从到期还是关闭时起算,以及“天”采用哪种算法,也要使用前文的规则明确下来。
这些时间值表达的是动作条件:当前时间达到访问截止,新访问不再获准;达到恢复截止,不能再按这项恢复政策发起恢复;达到最早清理时间,清理器才有资格处理资源。清理时还应检查保留延期或恢复是否已改变条件,不能一直沿用任务入队时的旧截止。
“此后允许删除”与“此时必须完成删除”是两种承诺。 delete_after 表达前者,清理器稍晚运行仍可能符合要求。若还要求最晚在某个时刻完成删除,则需要另行表示完成期限,并监测积压、失败与重试;请求已经进入清理队列,不能当作删除已经完成。若范围还包含副本、缓存或备份,也应明确各自的处理要求。
到期这一刻,新请求可能被拒绝,已受理下载按自己的上限继续,文件则等待保留期结束后清理。这些动作可以共享部分时间依据,但各自需要明确的条件与结果。无论检查发生在哪一步,它们最终都依赖一个时间来源;下一节讨论这个“现在”从哪里来,以及它能有多准确。