实战中,我们极少会自己手写时间轮算法,而是直接使用业界最成熟、最稳定的实现:Netty 的 HashedWheelTimer。
它被广泛用于 Dubbo(请求超时)、RocketMQ(延时消息)等顶层框架中。
以下是基于 Netty 时间轮处理 "异步请求超时控制" 的实战代码示例。
1. 引入依赖
首先,你需要在 Maven 项目中引入 Netty 的通用工具包:
xml
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-common</artifactId>
<version>4.1.100.Final</version>
</dependency>
2. 实战场景:模拟 RPC 请求超时
场景描述 :
假设我们发出了一个异步请求(例如调用第三方 API),我们希望等待结果,但如果 3秒内 对方没有响应,我们就抛出 TimeoutException 并结束等待。
这里我们将结合 Java 的 CompletableFuture 和 Netty 的 HashedWheelTimer 来实现。
好的,这是经过所有优化和增强(包括 幂等性、延迟更新 和 零延迟立即执行)后的完整的 Java 延迟事件框架代码。
为了方便运行,我将核心组件放在一个代码块中,并提供测试类进行演示。
🚀 完整代码:高可用延迟事件框架
1. 核心组件 (Core Framework Components)
包含事件基类、Netty 适配器和核心管理器。
java
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import java.util.concurrent.*;
// =========================================================
// 1. 抽象事件基类:GameDelayedEvent.java
// 定义事件的最小结构和唯一标识。
// =========================================================
public abstract class GameDelayedEvent {
private final long playerId;
private final String eventType;
public GameDelayedEvent(long playerId, String eventType) {
this.playerId = playerId;
this.eventType = eventType;
}
/** 业务逻辑入口:将在独立的业务线程池中执行。 */
public abstract void execute();
/** 核心方法:返回任务的全局唯一 Key,用于去重和更新。 */
public abstract String getUniqueKey();
public long getPlayerId() { return playerId; }
public String getEventType() { return eventType; }
}
// =========================================================
// 2. Netty 任务包装器:DelayedTaskWrapper.java
// 实现 TimerTask,确保非阻塞执行,并回调管理器进行 Map 清理。
// =========================================================
class DelayedTaskWrapper implements TimerTask {
private final GameDelayedEvent event;
private final ExecutorService businessThreadPool;
private final DelayedEventManager manager;
public DelayedTaskWrapper(GameDelayedEvent event, ExecutorService businessThreadPool, DelayedEventManager manager) {
this.event = event;
this.businessThreadPool = businessThreadPool;
this.manager = manager;
}
@Override
public void run(Timeout timeout) throws Exception {
if (timeout.isCancelled()) {
// 任务已被取消,无需执行
return;
}
// 提交给业务线程池执行,确保 HashedWheelTimer 线程不被阻塞!
businessThreadPool.submit(() -> {
try {
event.execute();
System.out.println("[Execute] 执行完毕: " + event.getUniqueKey());
} catch (Exception e) {
System.err.println("[Execute] 执行出错: " + event.getUniqueKey() + ", 错误: " + e.getMessage());
} finally {
// 执行完毕后,通知管理器移除当前任务的 Key
manager.removeTaskKey(event.getUniqueKey(), timeout);
}
});
}
}
// =========================================================
// 3. 核心调度管理器:DelayedEventManager.java
// 负责 Timer 生命周期、线程池和任务 Map 管理。
// =========================================================
class DelayedEventManager {
// 任务 Map:存储 Key -> Timeout 句柄,用于去重和取消
private final ConcurrentMap<String, Timeout> activeTasks = new ConcurrentHashMap<>();
// 全局唯一 HashedWheelTimer 实例
private static final HashedWheelTimer TIMER = new HashedWheelTimer(
r -> new Thread(r, "Game-Timer-Worker"), 100, TimeUnit.MILLISECONDS, 512
);
private final ExecutorService businessExecutor;
public DelayedEventManager(int businessPoolSize) {
this.businessExecutor = Executors.newFixedThreadPool(businessPoolSize,
r -> new Thread(r, "Game-Biz-Pool-" + r.hashCode()));
System.out.println("DelayedEventManager initialized. Business Pool Size: " + businessPoolSize);
}
/**
* 【核心 API】调度或更新一个延迟事件
* @param event 要执行的事件
* @param delayMillis 延迟时间(毫秒)
* @return 调度后的 Timeout 句柄;如果立即执行,则返回 null。
*/
public Timeout scheduleEvent(GameDelayedEvent event, long delayMillis) {
String key = event.getUniqueKey();
// --- 1. 去重与取消旧任务 ---
Timeout existingTimeout = activeTasks.get(key);
if (existingTimeout != null) {
// 尝试取消旧任务
boolean wasCancelled = existingTimeout.cancel();
// 无论取消是否成功,都必须移除 Map 中的旧句柄
if (activeTasks.remove(key, existingTimeout)) {
if (wasCancelled) {
System.out.printf("<<< [更新/移除] 任务 %s 已取消,准备重新调度。\n", key);
} else {
System.out.printf("<<< [注意/移除] 任务 %s 尝试取消但已完成,移除旧引用。\n", key);
}
}
}
// --- 2. 核心优化:判断是否立即执行 ---
if (delayMillis <= 0) {
System.out.printf(">>> [优化] 任务 %s 延迟 <= 0,直接提交给业务线程池执行。\n", key);
businessExecutor.submit(() -> {
try {
event.execute();
System.out.printf("[Execute] 立即执行完毕: %s\n", key);
} catch (Exception e) {
System.err.printf("[Execute] 立即执行出错: %s, 错误: %s\n", key, e.getMessage());
}
});
// 立即执行的任务不可被取消,返回 null
return null;
}
// --- 3. 正常 Time Wheel 调度 ---
DelayedTaskWrapper wrapper = new DelayedTaskWrapper(event, businessExecutor, this);
Timeout newTimeout = TIMER.newTimeout(wrapper, delayMillis, TimeUnit.MILLISECONDS);
// 将新句柄存入 Map
activeTasks.put(key, newTimeout);
System.out.printf(">>> [调度] 任务 %s 成功调度,延迟 %dms\n", key, delayMillis);
return newTimeout;
}
/**
* 对外 API:取消一个已调度的事件
*/
public boolean cancelEvent(Timeout timeout) {
if (timeout == null) {
// 立即执行的任务返回 null,不可取消
return false;
}
// 尝试从 Map 中移除并取消
if (timeout.cancel()) {
// 找到对应的 Key 并从 Map 中移除
activeTasks.entrySet().stream()
.filter(entry -> entry.getValue() == timeout)
.findFirst()
.ifPresent(entry -> activeTasks.remove(entry.getKey(), timeout));
return true;
}
return false;
}
/**
* 内部方法:供 TimerTask 执行完毕后调用,从 Map 中移除 Key
*/
public void removeTaskKey(String key, Timeout timeout) {
// 只有当 Map 中存储的句柄是当前这个已完成的句柄时,才进行移除操作
if (activeTasks.remove(key, timeout)) {
System.out.printf("<<< [完成/移除] 任务 %s 执行完毕,成功移除 Map 引用。\n", key);
}
}
public void shutdown() {
TIMER.stop();
businessExecutor.shutdownNow();
System.out.println("DelayedEventManager shut down.");
}
}
2. 测试类 (Framework Test Demo)
这个测试类演示了 立即执行 和 延迟更新/取消 的核心功能。
java
// FrameworkTestOptimized.java
// 具体的游戏事件示例:玩家持续效果(Debuff/Buff)
class PlayerEffectEvent extends GameDelayedEvent {
private final String effectId;
private final int value;
public PlayerEffectEvent(long playerId, String effectId, int value) {
super(playerId, "PlayerEffect");
this.effectId = effectId;
this.value = value;
}
// Key = 玩家ID:效果ID (例如: 1001:Poison)
@Override
public String getUniqueKey() {
return getPlayerId() + ":" + effectId;
}
@Override
public void execute() {
System.out.printf(">>> [效果触发] 玩家 %d 的效果 [%s] 生效,值为 %d。\n", getPlayerId(), effectId, value);
}
}
public class FrameworkTestOptimized {
public static void main(String[] args) throws InterruptedException {
DelayedEventManager manager = new DelayedEventManager(4);
long playerId = 3001;
// --- 场景 1: 零延迟/立即执行 (无需等待 Timer Tick) ---
PlayerEffectEvent swiftness = new PlayerEffectEvent(playerId, "Swiftness", 100);
Timeout immediateHandle = manager.scheduleEvent(swiftness, 0);
System.out.println("Swiftness Handle: " + immediateHandle); // 预期输出 null
// --- 场景 2: 延迟任务的更新/刷新 (幂等性) ---
String poisonEffect = "Poison";
// 1. 调度初始中毒任务 (5秒后触发)
PlayerEffectEvent poison1 = new PlayerEffectEvent(playerId, poisonEffect, 10);
manager.scheduleEvent(poison1, 5000);
// 2. 模拟 2 秒后,中毒效果被刷新,持续时间变更为 6 秒
Thread.sleep(2000);
PlayerEffectEvent poison2 = new PlayerEffectEvent(playerId, poisonEffect, 15);
manager.scheduleEvent(poison2, 6000);
// 框架会自动:取消 poison1 对应的 5秒任务,调度 poison2 对应的 6秒任务。
// 新任务将在 (2000 + 6000) = 8000ms 触发。
// --- 场景 3: 外部取消 ---
PlayerEffectEvent debuff = new PlayerEffectEvent(playerId + 1, "Root", 0);
Timeout debuffHandle = manager.scheduleEvent(debuff, 4000);
Thread.sleep(1000); // 等待 1 秒
manager.cancelEvent(debuffHandle);
System.out.printf("<<< [取消] Root 效果是否成功取消: %s\n", debuffHandle.isCancelled());
// 等待所有事件执行完毕
Thread.sleep(8500);
manager.shutdown();
}
}
根据实战场景在进一步优化!比如在MOMO游戏中,我们需要保证同步一个地图的延迟事件都在一个线程中执行!那我们就需要进一步进优化
线程亲和性(Thread Affinity),目的是保证对同一张地图状态的修改操作是串行和线程安全的。
我们不能再使用单一的 FixedThreadPool,因为它会随机分配任务。我们需要将任务根据 Map UID 哈希 到固定的几个 单线程执行器 (SingleThreadExecutor) 上。
优化方案:基于 Map UID 的线程亲和性调度
- 事件接口更新:
GameDelayedEvent必须提供getMapId()方法。 - 执行器替换: 将单一的
businessExecutor替换为一个ExecutorService数组 (mapExecutors),每个元素都是一个SingleThreadExecutor。 - 调度逻辑: 在调度任务时,根据
mapId.hashCode() % 数组长度来选择特定的执行器。
集成了 时间轮调度 、线程亲和性(Thread Affinity) 、事件幂等性(Deduplication) 和 零延迟优化 的完整的 Java 延迟事件框架代码。
完整的游戏延迟事件框架代码
java
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import java.util.concurrent.*;
// =========================================================
// 1. 抽象事件基类:GameDelayedEvent
// 定义事件的最小结构和线程亲和性所需的 Map ID。
// =========================================================
abstract class GameDelayedEvent {
private final long playerId;
private final String eventType;
private final long mapId; // 线程亲和性核心:地图唯一ID
public GameDelayedEvent(long playerId, String eventType, long mapId) {
this.playerId = playerId;
this.eventType = eventType;
this.mapId = mapId;
}
/** 业务逻辑入口:将在 Map 对应的亲和性线程中执行。 */
public abstract void execute();
/** 核心方法:返回任务的全局唯一 Key,用于去重和更新。 */
public abstract String getUniqueKey();
public long getPlayerId() { return playerId; }
public String getMapId() { return String.valueOf(mapId); } // 方便哈希
public long getMapIdValue() { return mapId; } // 实际值
}
// =========================================================
// 2. 具体的事件实现示例:PlayerEffectEvent
// =========================================================
class PlayerEffectEvent extends GameDelayedEvent {
private final String effectId;
private final int value;
public PlayerEffectEvent(long playerId, String effectId, int value, long mapId) {
super(playerId, "PlayerEffect", mapId);
this.effectId = effectId;
this.value = value;
}
// Key = MapID:PlayerID:EffectID (确保全局唯一性)
@Override
public String getUniqueKey() {
return getMapId() + ":" + getPlayerId() + ":" + effectId;
}
@Override
public void execute() {
System.out.printf(">>> [效果触发] [Map:%s|Thread:%s] 玩家 %d 的效果 [%s] 生效,值为 %d。\n",
getMapId(), Thread.currentThread().getName(), getPlayerId(), effectId, value);
}
}
// =========================================================
// 3. Netty 任务包装器:DelayedTaskWrapper
// 适配 Netty 接口,并确保提交到正确的亲和性线程。
// =========================================================
class DelayedTaskWrapper implements TimerTask {
private final GameDelayedEvent event;
private final ExecutorService affinityExecutor; // 特定的亲和性执行器
private final DelayedEventManager manager;
public DelayedTaskWrapper(GameDelayedEvent event, ExecutorService affinityExecutor, DelayedEventManager manager) {
this.event = event;
this.affinityExecutor = affinityExecutor;
this.manager = manager;
}
@Override
public void run(Timeout timeout) throws Exception {
if (timeout.isCancelled()) return;
// 【关键】提交给特定的亲和性执行器,保证同一地图串行执行
affinityExecutor.submit(() -> {
try {
event.execute();
System.out.printf("[Execute] [Map:%s|Thread:%s] 执行完毕: %s\n",
event.getMapId(), Thread.currentThread().getName(), event.getUniqueKey());
} catch (Exception e) {
System.err.printf("[Execute] 执行出错: %s, 错误: %s\n", event.getUniqueKey(), e.getMessage());
} finally {
// 执行完毕后,通知管理器移除 Map 引用
manager.removeTaskKey(event.getUniqueKey(), timeout);
}
});
}
}
// =========================================================
// 4. 核心调度管理器:DelayedEventManager
// 负责 Timer 生命周期、线程亲和性调度和任务 Map 管理。
// =========================================================
public class DelayedEventManager {
// 任务 Map:存储 Key -> Timeout 句柄,用于去重和取消
private final ConcurrentMap<String, Timeout> activeTasks = new ConcurrentHashMap<>();
// 全局唯一 HashedWheelTimer 实例
private static final HashedWheelTimer TIMER = new HashedWheelTimer(
r -> new Thread(r, "Game-Timer-Worker"), 100, TimeUnit.MILLISECONDS, 512
);
// 替换为基于 Map UID 划分的线程执行器数组
private final ExecutorService[] mapExecutors;
private final int poolSize;
public DelayedEventManager(int poolSize) {
this.poolSize = poolSize;
this.mapExecutors = new ExecutorService[poolSize];
// 初始化 N 个 SingleThreadExecutor,每个 Executor 对应一组 Map 的亲和性线程
for (int i = 0; i < poolSize; i++) {
final int index = i;
this.mapExecutors[i] = Executors.newSingleThreadExecutor(
r -> new Thread(r, "Game-Map-Affinity-Thread-" + index)
);
}
System.out.printf("DelayedEventManager initialized. Map Affinity Pool Size: %d\n", poolSize);
}
/** 根据 Map UID 获取其对应的亲和性执行器 */
private ExecutorService getAffinityExecutor(long mapId) {
int index = (int) (Math.abs(mapId) % poolSize);
return mapExecutors[index];
}
/** 调度或更新一个延迟事件 */
public Timeout scheduleEvent(GameDelayedEvent event, long delayMillis) {
String key = event.getUniqueKey();
ExecutorService affinityExecutor = getAffinityExecutor(event.getMapIdValue());
// --- 1. 去重与取消旧任务 ---
Timeout existingTimeout = activeTasks.get(key);
if (existingTimeout != null) {
boolean wasCancelled = existingTimeout.cancel();
// 无论取消是否成功,都必须移除 Map 中的旧句柄 (原子操作)
if (activeTasks.remove(key, existingTimeout)) {
if (wasCancelled) {
System.out.printf("<<< [更新/移除] 任务 %s 已取消,准备重新调度。\n", key);
} else {
System.out.printf("<<< [注意/移除] 任务 %s 尝试取消但已完成,移除旧引用。\n", key);
}
}
}
// --- 2. 核心优化:判断是否立即执行 (延迟 <= 0) ---
if (delayMillis <= 0) {
System.out.printf(">>> [优化] 任务 %s (Map:%s) 延迟 <= 0,直接提交给亲和性线程池。\n",
key, event.getMapId());
// 直接提交到亲和性执行器
affinityExecutor.submit(() -> {
try {
event.execute();
System.out.printf("[Execute] 立即执行完毕: %s\n", key);
} catch (Exception e) {
System.err.printf("[Execute] 立即执行出错: %s, 错误: %s\n", key, e.getMessage());
}
});
return null;
}
// --- 3. 正常 Time Wheel 调度 ---
DelayedTaskWrapper wrapper = new DelayedTaskWrapper(event, affinityExecutor, this);
Timeout newTimeout = TIMER.newTimeout(wrapper, delayMillis, TimeUnit.MILLISECONDS);
activeTasks.put(key, newTimeout);
System.out.printf(">>> [调度] 任务 %s (Map:%s) 成功调度,延迟 %dms\n",
key, event.getMapId(), delayMillis);
return newTimeout;
}
/** 对外 API:取消一个已调度的事件 */
public boolean cancelEvent(Timeout timeout) {
if (timeout == null) return false;
if (timeout.cancel()) {
// 尝试找到 Key 并从 Map 中移除
activeTasks.entrySet().stream()
.filter(entry -> entry.getValue() == timeout)
.findFirst()
.ifPresent(entry -> activeTasks.remove(entry.getKey(), timeout));
return true;
}
return false;
}
/** 内部方法:供 TimerTask 执行完毕后调用,从 Map 中移除 Key */
public void removeTaskKey(String key, Timeout timeout) {
if (activeTasks.remove(key, timeout)) {
System.out.printf("<<< [完成/移除] 任务 %s 执行完毕,成功移除 Map 引用。\n", key);
}
}
public void shutdown() {
TIMER.stop();
for (ExecutorService executor : mapExecutors) {
executor.shutdownNow();
}
System.out.println("DelayedEventManager shut down.");
}
}
// =========================================================
// 5. 亲和性测试演示:FrameworkTestAffinity
// =========================================================
class FrameworkTestAffinity {
public static void main(String[] args) throws InterruptedException {
// 配置:只使用 2 个线程来处理所有地图事件 (Map ID % 2)
DelayedEventManager manager = new DelayedEventManager(2);
// Map A (ID: 100) -> 100 % 2 = 0 -> Thread 0
long mapA = 100;
// Map B (ID: 101) -> 101 % 2 = 1 -> Thread 1
long mapB = 101;
// Map C (ID: 200) -> 200 % 2 = 0 -> Thread 0
long mapC = 200;
// 1. 调度 Map A 的事件 (2 秒后)
manager.scheduleEvent(new PlayerEffectEvent(1, "A-Skill-1", 1, mapA), 2000);
manager.scheduleEvent(new PlayerEffectEvent(2, "A-Skill-2", 1, mapA), 2500); // 确保在 A-Skill-1 之后执行
// 2. 调度 Map B 的事件 (1 秒后)
manager.scheduleEvent(new PlayerEffectEvent(3, "B-Regen", 1, mapB), 1000);
// 3. 调度 Map C 的事件 (2 秒后)
manager.scheduleEvent(new PlayerEffectEvent(4, "C-Event", 1, mapC), 2000); // 预期与 Map A 在同一线程
// 4. 立即执行 Map A 的事件 (零延迟优化)
manager.scheduleEvent(new PlayerEffectEvent(5, "A-Immediate", 1, mapA), 0);
// 5. 演示延迟更新 (在 1s 处刷新 Map A 的一个事件)
manager.scheduleEvent(new PlayerEffectEvent(6, "A-Debuff", 5, mapA), 3000); // 初始 3s
Thread.sleep(1000);
manager.scheduleEvent(new PlayerEffectEvent(6, "A-Debuff", 10, mapA), 4000); // 刷新为 4s
// 等待所有事件执行
Thread.sleep(5500);
manager.shutdown();
}
}