
本文介绍在基于分钟级心跳机制的游戏或服务系统中,如何可靠、无重复地触发每日固定时间(如凌晨5点)的业务事件,重点解决跨天判断、单次触发与状态持久化问题。
本文介绍在基于分钟级心跳机制的游戏或服务系统中,如何可靠、无重复地触发每日固定时间(如凌晨5点)的业务事件,重点解决跨天判断、单次触发与状态持久化问题。
在高频心跳(如每分钟一次)场景下,实现“每日固定时刻仅触发一次事件”需兼顾时间精度、状态一致性与容错性。直接比较 time >= 5:00 AM 是不够的——若心跳恰好卡在5:00:00之后持续运行,该条件会在5:00:01、5:00:02……每分钟都为真,导致事件重复触发。
正确方案是采用双状态控制:
-
日期重置标识(
resetTime):用于检测是否进入新一天; -
小时事件标记(
resetAt5):标记当日5点事件是否已触发,避免同日内多次执行。
核心逻辑分两阶段:
✅ 跨天初始化阶段(每日首次心跳):
当检测到 time 已进入新一天(!isSameDay(resetTime, time)),先重置 resetAt5 = false,再立即检查当前时间是否 ≥ 5:00 AM —— 若满足,立即触发事件并标记 resetAt5 = true。此步覆盖了“玩家恰好在5:00后首次心跳上线”的情况。
✅ 日常兜底触发阶段(任意心跳):
即使错过跨天初始化(如服务重启、玩家离线后上线),只要 resetAt5 == false 且当前时间 ≥ 当日5:00 AM,就触发事件并持久化标记。这确保了事件最终一定会被精确执行一次,不依赖上线时机。
以下是优化后的完整实现(含关键注释):
@Override
public void playerHeart(Player player, long time) {
Reset reset = player.getBase().getReset();
// 【阶段1】检测是否进入新一天
if (!TimeUtil.isSameDay(reset.getResetTime(), time)) {
// 触发通用日重置事件
EventManager.fire(EventOnPlayerDailyReset.class, e -> e.onPlayerDailyReset(player));
// 重置当日5点事件标记
reset.setResetAt5(false);
// 若当前已过5点,立即触发并标记(覆盖上线较晚场景)
if (time >= TimeUtil.getClockTime(time, 5 * 3600)) {
EventManager.fire(EventOnPlayerResetAtHour5.class,
e -> e.onPlayerResetAtHour5(player));
reset.setResetAt5(true);
}
reset.setResetTime(time);
player.saveBase(); // 持久化重置时间 + 初始标记
}
// 【阶段2】兜底触发:确保当日5点事件必达(防漏触发)
if (!reset.isResetAt5() && time >= TimeUtil.getClockTime(time, 5 * 3600)) {
EventManager.fire(EventOnPlayerResetAtHour5.class,
e -> e.onPlayerResetAtHour5(player));
reset.setResetAt5(true);
player.saveBase(); // 关键:必须保存标记,否则下次心跳仍会进入此分支
}
EventManager.fire(EventOnPlayerHeart.class, e -> e.onPlayerHeart(player, time));
}
? 注意事项:
-
TimeUtil.getClockTime(time, 5 * 3600)应返回当日5:00:00的毫秒时间戳(如2024-06-15 05:00:00.000),而非相对偏移量,确保跨天计算准确; -
resetAt5必须使用@Tag(2)等序列化注解,保证其随Reset对象一同持久化到数据库或缓存; -
player.saveBase()在两个分支中均需调用,但应合并为一次写入以减少IO(生产环境建议将两次saveBase()合并为一次,根据实际更新字段做增量保存); - 若系统存在多实例部署,需考虑分布式锁或借助数据库唯一约束防止极端并发下的重复触发(本方案默认单节点或共享存储已保证数据一致性)。
该设计在保持心跳轻量的前提下,实现了高可靠性的时间事件调度,适用于游戏每日任务刷新、运营活动开启、资源重置等典型场景。










