【时间轮算法-实战】Java基于Netty的 `HashedWheelTimer`快速搭建时间轮算法系统

实战中,我们极少会自己手写时间轮算法,而是直接使用业界最成熟、最稳定的实现: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 的线程亲和性调度

  1. 事件接口更新: GameDelayedEvent 必须提供 getMapId() 方法。
  2. 执行器替换: 将单一的 businessExecutor 替换为一个 ExecutorService 数组 (mapExecutors),每个元素都是一个 SingleThreadExecutor
  3. 调度逻辑: 在调度任务时,根据 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();
    }
}
相关推荐
liu****1 小时前
12.C语言内存相关函数
c语言·开发语言·数据结构·c++·算法
while(1){yan}1 小时前
JAVA单例模式
java·单例模式
没有bug.的程序员1 小时前
Async Profiler:最精准的火焰图工具
java·jvm·spring·对象分配·async profiler
FPGA_无线通信1 小时前
OFDM 精频偏补偿
算法·fpga开发
程序员-King.1 小时前
day109—同向双指针(字符串)—每个字符最多出现两次的最长子字符串(LeetCode-3090)
算法·leetcode·双指针
青山的青衫1 小时前
【单调栈和单调队列】LeetCode hot100+面试高频
算法·leetcode·面试
金士顿1 小时前
Ethercat耦合器添加的IO导出xml 初始化IO参数
android·xml·java
俊俊谢1 小时前
【浮点运算性能优化】浮点转定点算法库的多平台通用移植方案与性能评估优化
算法·性能优化·c·浮点转定点·多平台移植
7哥♡ۣۖᝰꫛꫀꪝۣℋ1 小时前
Spring WebMVC及常用注释
java·数据库·spring