org.apache.dubbo 2.7.7 服務端處理請求及時間輪(失敗重試)

本文主要針對 dubbo-spring-boot-starter   2.7.7版本, 對應的 org.apache.dubbo 2.7.7 版本的源碼。

  本文主要從以下幾個點來分析:

  1. 服務端處理請求.
  2. 時間輪(失敗重試)。

服務端接收數據的處理流程:

  客戶端請求發出去之後,服務端會收到這個請求的消息,然後觸發調用。服務端這邊接收消息的處理鏈路,也比較複雜,我們回到NettServer中創建io的過程。

@Override
protected void doOpen() throws Throwable {
        bootstrap = new ServerBootstrap();

        bossGroup = NettyEventLoopFactory.eventLoopGroup(1, "NettyServerBoss");
        workerGroup = NettyEventLoopFactory.eventLoopGroup(
                getUrl().getPositiveParameter(IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
                "NettyServerWorker");

        final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
        channels = nettyServerHandler.getChannels();

        bootstrap.group(bossGroup, workerGroup)
                .channel(NettyEventLoopFactory.serverSocketChannelClass())
                .option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
                .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
                .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        // FIXME: should we use getTimeout()?
                        int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
                        NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
                        if (getUrl().getParameter(SSL_ENABLED_KEY, false)) {
                            ch.pipeline().addLast("negotiation",
                                    SslHandlerInitializer.sslServerHandler(getUrl(), nettyServerHandler));
                        }
                        ch.pipeline()
                                .addLast("decoder", adapter.getDecoder())
                                .addLast("encoder", adapter.getEncoder())
                                .addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
                                .addLast("handler", nettyServerHandler);
                    }
                });
        // bind
        ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
        channelFuture.syncUninterruptibly();
        channel = channelFuture.channel();

}

  服務端啓動的時候,配置的消息處理是handler配置的是nettyServerHandler , 所以我們直接進入到 NettyServerHandler#channelRead ,這個方法負責處理請求。

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
  handler.received(channel, msg);
}

  服務端收到讀的請求是,會進入這個方法。接着通過handler.received來處理msg ,而這個handler 是在服務發佈的時候構建得。起鏈路如下:

  MultiMessageHandler(複合消息處理) ---> HeartbeatHandle(心跳消息處理,接收心跳併發送心跳響應) ---> AllChannelHandler (業務線程轉化處理器)---> DecodeHandler (業務解碼處理器)---> HeaderExchangeHandler ---> DubboProtocol#requestHandler(new ExchangeHandlerAdapter())

  而在構建 NettyServerHandler 得時候將 this 傳了進去。this 即 NettyServer 。NettyServer是  AbstractPeer 得子類。所以 handler.received 此時會調用AbsstractPeer.received方法,這個方法用來判斷服務端是否關閉了,如果關閉就直接返回,否則,通過handler處理鏈進行層層調用。

  我們直接進入 HeaderExchangeHandler.received,交互層請求響應處理,有三種處理方式

  1. handlerRequest,雙向請求
  2. handler.received 單向請求
  3. handleResponse 響應消息
public void received(Channel channel, Object message) throws RemotingException {
        final ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);
        if (message instanceof Request) {
            // handle request. 處理請求
            Request request = (Request) message;
            if (request.isEvent()) {
                handlerEvent(channel, request);
            } else {
                if (request.isTwoWay()) {
                    handleRequest(exchangeChannel, request);
                } else {
                    handler.received(exchangeChannel, request.getData());
                }
            }
        } else if (message instanceof Response) {
            handleResponse(channel, (Response) message);
        } else if (message instanceof String) {
            if (isClientSide(channel)) {
                Exception e = new Exception("Dubbo client can not supported string message: " + message + " in channel: " + channel + ", url: " + channel.getUrl());
                logger.error(e.getMessage(), e);
            } else {
                String echo = handler.telnet(channel, (String) message);
                if (echo != null && echo.length() > 0) {
                    channel.send(echo);
                }
            }
        } else {
            handler.received(exchangeChannel, message);
        }
}

  接着調用handleRequest方法。這個方法中,構建返回的對象Response,並且最終會通過異步的方式來把msg傳遞到invoker中進行調用 handler.reply .這裏得最後一層就是調用 DubboProtocol 類中得  requestHandler 屬性構造得 ExchangeHandlerAdapter 內部類的 reply 方法:

private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {

        @Override
        public CompletableFuture<Object> reply(ExchangeChannel channel, Object message) throws RemotingException {
        //如果消息類型不是invocation,則拋出異常表示無法識別
            if (!(message instanceof Invocation)) {
                throw new RemotingException(channel, "Unsupported request: "
                        + (message == null ? null : (message.getClass().getName() + ": " + message))
                        + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress());
            }
        //獲得請求參數
            Invocation inv = (Invocation) message;
          // 獲取 invoker 領域對象,這個對象是在發佈服務的時候構建,然後封裝成 exporter 存在map裏面的。
            Invoker<?> invoker = getInvoker(channel, inv);
            // need to consider backward-compatibility if it's a callback
            if (Boolean.TRUE.toString().equals(inv.getObjectAttachments().get(IS_CALLBACK_SERVICE_INVOKE))) {
                String methodsStr = invoker.getUrl().getParameters().get("methods");
                boolean hasMethod = false;
                if (methodsStr == null || !methodsStr.contains(",")) {
                    hasMethod = inv.getMethodName().equals(methodsStr);
                } else {
                    String[] methods = methodsStr.split(",");
                    for (String method : methods) {
                        if (inv.getMethodName().equals(method)) {
                            hasMethod = true;
                            break;
                        }
                    }
                }
                if (!hasMethod) {
                    logger.warn(new IllegalStateException("The methodName " + inv.getMethodName()
                            + " not found in callback service interface ,invoke will be ignored."
                            + " please update the api interface. url is:"
                            + invoker.getUrl()) + " ,invocation is :" + inv);
                    return null;
                }
            }
            RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
            Result result = invoker.invoke(inv); // 發起對應調用
            return result.thenApply(Function.identity());
        }
  //......省略代碼
};

  invoker.invoke,發起本地服務調用,但是此時調用之前,invoke並不是一個直接調用的對象,而是包裝過的。在 ServiceConfig#doExportUrlsFor1Protocol 構建包裝。最後的調用鏈路如下:

  RegistryProtocol.InvokerDelegate.invoke ---> DelegateProviderMetaDataInvoker.invoke ---> AbstractProxyInvoker.invoke ---> AbstractProxyInvoker(JavassistProxyFactory#getInvoker)

  InvokerDelegate 未實現父類 InvokerWrapper invoke方法。進入到InvokerWrapper.invoke方法,這個是一個Invoker包裝類,包裝了URL地址信息和真正的Invoker代理對象。

@Override
public Result invoke(Invocation invocation) throws RpcException {
    return invoker.invoke(invocation);
}

  DelegateProviderMetaDataInvoker:這裏是一個委派類,它提供了服務提供者的元數序信息。

@Override
public Result invoke(Invocation invocation) throws RpcException {
    return invoker.invoke(invocation);
}

  AbstractProxyInvoker:接着進入到AbstractProxyInvoker的invoke方法,在這個方法中,我們可以看到它會調用子類的doInvoke方法,獲得返回結果。其中proxy,表示服務端的對象實例,這個實例很顯然是在構建動態代理Invoker對象時保存進來的。

public Result invoke(Invocation invocation) throws RpcException {
        try {
            Object value = doInvoke(proxy, invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
            CompletableFuture<Object> future = wrapWithFuture(value);
            CompletableFuture<AppResponse> appResponseFuture = future.handle((obj, t) -> {
                AppResponse result = new AppResponse();
                if (t != null) {
                    if (t instanceof CompletionException) {
                        result.setException(t.getCause());
                    } else {
                        result.setException(t);
                    }
                } else {
                    result.setValue(obj);
                }
                return result;
            });
            return new AsyncRpcResult(appResponseFuture, invocation);
        } catch (InvocationTargetException e) {
            if (RpcContext.getContext().isAsyncStarted() && !RpcContext.getContext().stopAsync()) {
                logger.error("Provider async started, but got an exception from the original method, cannot write the exception back to consumer because an async result may have returned the new thread.", e);
            }
            return AsyncRpcResult.newDefaultAsyncResult(null, e.getTargetException(), invocation);
        } catch (Throwable e) {
            throw new RpcException("Failed to invoke remote proxy method " + invocation.getMethodName() + " to " + getUrl() + ", cause: " + e.getMessage(), e);
        }
}

  最後進入到具體的子類,也就是在服務的發佈的時候通過 構建的

@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
        // TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
        final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
        return new AbstractProxyInvoker<T>(proxy, type, url) {
            @Override
            protected Object doInvoke(T proxy, String methodName,
                                      Class<?>[] parameterTypes,
                                      Object[] arguments) throws Throwable {
                return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
            }
        };
}

  然後發起本地的調用即可,將結果返回 。至此,服務消費的處理流程就分析完了。最後附上這個流程的流程圖。

Dubbo中失敗重試的設計:

   在Dubbo中,有很多地方涉及到服務失敗重試,比如服務註冊失敗時,會調用一個方法把失敗的請求保存起來進行重試。clusterInvoker失敗重試

  註冊中心失敗重試 FailbackRegistry#addFailedRegistered

private final HashedWheelTimer retryTimer;
private void addFailedRegistered(URL url) {
        FailedRegisteredTask oldOne = failedRegistered.get(url);
        if (oldOne != null) {
            return;
        }
        FailedRegisteredTask newTask = new FailedRegisteredTask(url, this);
        oldOne = failedRegistered.putIfAbsent(url, newTask);
        if (oldOne == null) {
            // never has a retry task. then start a new task for retry.
            retryTimer.newTimeout(newTask, retryPeriod, TimeUnit.MILLISECONDS);
        }
}

  clusterInvoker(FailbackClusterInvoker)失敗重試

private void addFailed(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, Invoker<T> lastInvoker) {
        if (failTimer == null) {
            synchronized (this) {
                if (failTimer == null) {
                    failTimer = new HashedWheelTimer(
                            new NamedThreadFactory("failback-cluster-timer", true),
                            1,
                            TimeUnit.SECONDS, 32, failbackTasks);
                }
            }
        }
        RetryTimerTask retryTimerTask = new RetryTimerTask(loadbalance, invocation, invokers, lastInvoker, retries, RETRY_FAILED_PERIOD);
        try {
            failTimer.newTimeout(retryTimerTask, RETRY_FAILED_PERIOD, TimeUnit.SECONDS);
        } catch (Throwable e) {
            logger.error("Failback background works error,invocation->" + invocation + ", exception: " + e.getMessage());
        }
}

  不難發現他們都是基於HashedWhelloTimer這個類來實現的。

時間輪:

  時間輪這個技術其實出來很久了,在kafka、zookeeper等技術中都有時間輪使用的方式。什麼是時間輪呢?

  簡單來說: 時間輪是一種高效利用線程資源進行批量化調度的一種調度模型。把大批量的調度任務全部綁定到同一個調度器上,使用這一個調度器來進行所有任務的管理、觸發、以及運行。所以時間輪的模型能夠高效管理各種延時任務、週期任務、通知任務。 在工作中遇到類似的功能,可以採用時間輪機制。

  時間輪,從圖片上來看,就和手錶的表圈是一樣,所以稱爲時間輪,是因爲它是以時間作爲刻度組成的一個環形隊列,這個環形隊列採用數組來實現,數組的每個元素稱爲槽,每個槽可以放一個定時任務列表,叫HashedWheelBucket,它是一個雙向鏈表,量表的每一項表示一個定時任務項(HashedWhellTimeout),其中封裝了真正的定時任務TimerTask。時間輪是由多個時間格組成,下圖中有8個時間格,每個時間格代表當前時間輪的基本時間跨度(tickDuration),其中時間輪的時間格的個數是固定的。

  在下圖中,有8個時間格(槽),假設每個時間格的單位爲1s,那麼整個時間輪走完一圈需要8s鍾。每秒鐘指針會沿着順時針方向移動一格,這個單位可以設置,比如以秒爲單位,可以以一小時爲單位,這個單位可以代表時間精度。

  通過指針移動,來獲得每個時間格中的任務列表,然後遍歷這一個時間格中的雙向鏈表來執行任務,以此循環。

 

 

時間輪的運行邏輯:

  首先,時間輪在啓動的時候,會記錄一下當前啓動時間,並賦值給一個叫startTime的變量。然後當需要添加任務的時候,首先會計算延遲時間(deadline),比如一個任務的延遲時間是24ms,那麼在添加任務時,會將當前時間(currentTime)+24ms-時間輪的啓動時間(startTime),然後把這個任務封裝成HashedWheelTimeout加入到鏈表中。

  那麼這個任務應該放在哪個時間格里面呢? 通過  deadline%wheel.length 計算.時間輪在運行的時候,會從任務隊列中取出10W個進行遍歷處理。

Dubbo中的時間輪實現類是:HashedWheelTimer:

  在FailbackClusterInvoker這個類中,構建了一個HashedWheelTimer,然後增加了一個任務RetryTimerTask到時間輪中。基於這段代碼, 我們去分析一下HashedWheelTimer的實現。 FailbackClusterInvoker#addFailed 主要做了兩件事情:

  1. 如果時間輪等於 null,則初始化時間輪
  2. 創建重試任務,啓動時間輪。

  HashedWheelTimer的構造:

  1. 調用createWheel創建一個時間輪,時間輪數組一定是2的冪次方,比如傳入的
  2. ticksPerWheel=6,那麼初始化的wheel長度一定是8,這樣是便於時間格的計算。tickDuration,表示時間輪的跨度,代表每個時間格的時間精度,以納秒的方式來表現。
  3. 把工作線程Worker封裝成WorkerThread,從名字可以知道,它就是最終那個負責幹活的線程。
public HashedWheelTimer(
            ThreadFactory threadFactory,
            long tickDuration, TimeUnit unit, int ticksPerWheel,
            long maxPendingTimeouts) {
     // 省略判斷邏輯代碼
        // Normalize ticksPerWheel to power of two and initialize the wheel.
     // 創建時間輪基本的數據結構,一個數組。長度爲不小於ticksPerWheel的最小2的n次方
        wheel = createWheel(ticksPerWheel);
     // 這是一個標示符,用來快速計算任務應該呆的格子。
     // 我們知道,給定一個deadline的定時任務,其應該呆的格子=deadline%wheel.length.但是%操作是個相對耗時的操作,所以使用一種變通的位運算代替:
     // 因爲一圈的長度爲2的n次方,mask = 2^n-1後低位將全部是1,然後deadline&mast == deadline%wheel.length
     // java中的HashMap在進行hash之後,進行index的hash尋址尋址的算法也是和這個一樣的
        mask = wheel.length - 1;

        // Convert tickDuration to nanos.
     //時間輪的基本時間跨度,(tickDuration傳入是1的話,這裏會轉換成1000000)
        this.tickDuration = unit.toNanos(tickDuration);

        // Prevent overflow.
     // 校驗是否存在溢出。即指針轉動的時間間隔不能太長而導致tickDuration*wheel.length>Long.MAX_VALUE
        if (this.tickDuration >= Long.MAX_VALUE / wheel.length) {
            throw new IllegalArgumentException(String.format(
                    "tickDuration: %d (expected: 0 < tickDuration in nanos < %d",
                    tickDuration, Long.MAX_VALUE / wheel.length));
        }
     //把worker包裝成thread
        workerThread = threadFactory.newThread(worker);

        this.maxPendingTimeouts = maxPendingTimeouts;
     //如果HashedWheelTimer實例太多,那麼就會打印一個error日誌
        if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT &&
                WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) {
            reportTooManyInstances();
        }
}

  然後來看看時間輪的創建 createWheel

  1. 對傳入的ticksPerWheel進行整形
  2. 初始化固定長度的HashedWheelBucket
private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
        if (ticksPerWheel <= 0) {
            throw new IllegalArgumentException(
                    "ticksPerWheel must be greater than 0: " + ticksPerWheel);
        }
        if (ticksPerWheel > 1073741824) {
            throw new IllegalArgumentException(
                    "ticksPerWheel may not be greater than 2^30: " + ticksPerWheel);
        }
        //對傳入的時間輪大小進行整形,整形成2的冪次方
        ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
        //初始化一個固定長度的Bucket數組
        HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
        for (int i = 0; i < wheel.length; i++) {
            wheel[i] = new HashedWheelBucket();
        }
        return wheel;
}

private static int normalizeTicksPerWheel(int ticksPerWheel) {
        int normalizedTicksPerWheel = ticksPerWheel - 1;
        normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 1;
        normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 2;
        normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 4;
        normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 8;
        normalizedTicksPerWheel |= normalizedTicksPerWheel >>> 16;
        return normalizedTicksPerWheel + 1;
}

  完成時間輪的初始化之後,並沒有去啓動時間輪,繼續看FailbackClusterInvoker中的代碼。構建了一個RetryTimerTask,也就是一個重試的定時任務,接着把這個任務通過newTimeout加入到時間輪中,其中

  • retryTimerTask,表示具體的重試任務
  • RETRY_FAILED_PERIOD , 表示重試間隔時間,默認爲5s

  調用newTimeout方法,把任務添加進來。

public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
        if (task == null) {
            throw new NullPointerException("task");
        }
        if (unit == null) {
            throw new NullPointerException("unit");
        }
        //統計任務個數
        long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();
        //判斷最大任務數量是否超過限制
        if (maxPendingTimeouts > 0 && pendingTimeoutsCount > maxPendingTimeouts) {
            pendingTimeouts.decrementAndGet();
            throw new RejectedExecutionException("Number of pending timeouts ("
                    + pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending "
                    + "timeouts (" + maxPendingTimeouts + ")");
        }
        //如果時間輪沒有啓動,則通過start方法進行啓動
        start();

        // Add the timeout to the timeout queue which will be processed on the next tick.
        // During processing all the queued HashedWheelTimeouts will be added to the correct HashedWheelBucket.
        //計算任務的延遲時間,通過當前的時間+當前任務執行的延遲時間-時間輪啓動的時間。
        long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;
        //在delay爲正數的情況下,deadline是不可能爲負數
        //如果爲負數,那麼說明超過了long的最大值
        // Guard against overflow.
        if (delay > 0 && deadline < 0) {
            deadline = Long.MAX_VALUE;
        }
        //創建一個Timeout任務,理論上來說,這個任務應該要加入到時間輪的時間格子中,但是這裏並不是先添加到時間格,
        //而是先加入到一個阻塞隊列,然後等到時間輪執行到下一個格子時,再從隊列中取出最多100000個任務添加到指定的時間格(槽)中。
        HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
        timeouts.add(timeout);
        return timeout;
}        

  任務添加到阻塞隊列之後,我們再來看啓動方法,start方法會根據當前的workerState狀態來啓動時間輪。並且用了startTimeInitialized來控制線程的運行,如果workerThread沒有啓動起來,那麼newTimeout方法會一直阻塞在運行start方法中。如果不阻塞,newTimeout方法會獲取不到startTime。

public void start() {
        //workerState一開始的時候是0(WORKER_STATE_INIT),然後纔會設置爲1(WORKER_STATE_STARTED)
        switch (WORKER_STATE_UPDATER.get(this)) {
            case WORKER_STATE_INIT:
                if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
                    workerThread.start();
                }
                break;
            case WORKER_STATE_STARTED:
                break;
            case WORKER_STATE_SHUTDOWN:
                throw new IllegalStateException("cannot be started once stopped");
            default:
                throw new Error("Invalid WorkerState");
        }
        // 等待worker線程初始化時間輪的啓動時間
        // Wait until the startTime is initialized by the worker.
        while (startTime == 0) {
            try {
                //這裏使用countDownLauch來確保調度的線程已經被啓動
                startTimeInitialized.await();
            } catch (InterruptedException ignore) {
                // Ignore - it will be ready very soon.
            }
        }
}

  啓動時間輪:調用start()方法, 會調用 workerThread.start(); 來啓動一個工作線程,這個工作線程是在構造方法中初始化的,包裝的是一個Worker內部線程類。所以直接進入到Worker這個類的run方法,瞭解下它的設計邏輯

public void run() {
            // Initialize the startTime.
            // 初始化startTime,表示時間輪的啓動時間
            startTime = System.nanoTime();
            // 喚醒被阻塞的start()方法。
            if (startTime == 0) {
                // We use 0 as an indicator for the uninitialized value here, so make sure it's not 0 when initialized.
                startTime = 1;
            }

            // Notify the other threads waiting for the initialization at start().
            startTimeInitialized.countDown();

            do {
                //返回每tick一次的時間間隔
                final long deadline = waitForNextTick();
                if (deadline > 0) {
                    //計算時間輪的槽位
                    int idx = (int) (tick & mask);
                    //移除掉CancelledTask
                    processCancelledTasks();
                    //得到當前指針位置的時間槽
                    HashedWheelBucket bucket =
                            wheel[idx];
                    //將newTimeout()方法中加入到待處理定時任務隊列中的任務加入到指定的格子中
                    transferTimeoutsToBuckets();
                    //運行目前指針指向的槽中的bucket鏈表中的任務
                    bucket.expireTimeouts(deadline);
                    tick++;
                }
            } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

            // Fill the unprocessedTimeouts so we can return them from stop() method.
            //如果Worker_State一隻是started狀態,就一直循環
            for (HashedWheelBucket bucket : wheel) {
                //清除時間輪中不需要處理的任務
                bucket.clearTimeouts(unprocessedTimeouts);
            }
            for (; ; ) {
                //遍歷任務隊列,發現如果有任務被取消,則添加到unprocessedTimeouts,也就是不需要處理的隊列中。
                HashedWheelTimeout timeout = timeouts.poll();
                if (timeout == null) {
                    break;
                }
                if (!timeout.isCancelled()) {
                    unprocessedTimeouts.add(timeout);
                }
            }//處理被取消的任務.
            processCancelledTasks();
}

  時間輪指針跳動:這個方法的主要作用就是返回下一個指針指向的時間間隔,然後進行sleep操作。可以想象一下,一個鐘錶上秒與秒之間是有時間間隔的,那麼waitForNextTick就是根據當前時間計算出跳動到下個時間的時間間隔,然後進行sleep,然後再返回當前時間距離時間輪啓動時間的時間間隔。

  說得再直白一點:,假設當前的tickDuration的間隔是1s,tick默認=0, 此時第一次進來,得到的deadline=1,也就是下一次跳動的時間間隔是1s。

private long waitForNextTick() {
            //tick表示總的tick數
            //tickDuration表示每個時間格的跨度,所以deadline返回的是下一次時間輪指針跳動的時間
            long deadline = tickDuration * (tick + 1);

            for (; ; ) {
                //計算當前時間距離啓動時間的時間間隔
                final long currentTime = System.nanoTime() - startTime;
                //通過下一次指針跳動的延遲時間距離當前時間的差額,這個作爲sleep時間使用。
                // 其實線程是以睡眠一定的時間再來執行下一個ticket的任務的
                long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;
                //sleepTimeMs小於零表示走到了下一個時間槽位置
                if (sleepTimeMs <= 0) {
                    if (currentTime == Long.MIN_VALUE) {
                        return -Long.MAX_VALUE;
                    } else {
                        return currentTime;
                    }
                }
                if (isWindows()) {
                    sleepTimeMs = sleepTimeMs / 10 * 10;
                }
                //進入到這裏進行sleep,表示當前時間距離下一次tick時間還有一段距離,需要sleep。
                try {
                    Thread.sleep(sleepTimeMs);
                } catch (InterruptedException ignored) {
                    if (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) {
                        return Long.MIN_VALUE;
                    }
                }
            }
}

  transferTimeoutsToBuckets:轉移任務到時間輪中,前面我們講過,任務添加進來時,是先放入到阻塞隊列。

  而在現在這個方法中,就是把阻塞隊列中的數據轉移到時間輪的指定位置。在這個轉移方法中,寫死了一個循環,每次都只轉移10萬個任務。然後根據HashedWheelTimeout的deadline延遲時間計算出時間輪需要運行多少次才能運行當前的任務,如果當前的任務延遲時間大於時間輪跑一圈所需要的時間,那麼就計算需要跑幾圈才能到這個任務運行。最後計算出該任務在時間輪中的槽位,添加到時間輪的鏈表中。

private void transferTimeoutsToBuckets() {
            // transfer only max. 100000 timeouts per tick to prevent a thread to stale the workerThread when it just
            // adds new timeouts in a loop.
            // 循環100000次,也就是每次轉移10w個任務
            for (int i = 0; i < 100000; i++) {
                //從阻塞隊列中獲得具體的任務
                HashedWheelTimeout timeout = timeouts.poll();
                if (timeout == null) {
                    // all processed
                    break;
                }
                if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
                    // Was cancelled in the meantime.
                    continue;
                }
                //計算tick次數,deadline表示當前任務的延遲時間,tickDuration表示時間槽的間隔,兩者相除就可以計算當前任務需要tick幾次才能被執行
                long calculated = timeout.deadline / tickDuration;
                // 計算剩餘的輪數, 只有 timer 走夠輪數, 並且到達了 task 所在的 slot, task 纔會過期.(被執行)
                timeout.remainingRounds = (calculated - tick) / wheel.length;

                // Ensure we don't schedule for past.
                //如果任務在timeouts隊列裏面放久了, 以至於已經過了執行時間, 這個時候就使用當前tick,也就是放到當前bucket, 此方法調用完後就會被執行
                final long ticks = Math.max(calculated, tick);
                // 算出任務應該插入的 wheel 的 slot, stopIndex = tick 次數 & mask, mask =wheel.length - 1
                int stopIndex = (int) (ticks & mask);
                //把timeout任務插入到指定的bucket鏈中
                HashedWheelBucket bucket = wheel[stopIndex];
                bucket.addTimeout(timeout);
            }
}

  運行時間輪中的任務:當指針跳動到某一個時間槽中時,會就觸發這個槽中的任務的執行。該功能是通過expireTimeouts來實現這個方法的主要作用是: 過期並執行格子中到期的任務。也就是當tick進入到指定格子時,worker線程會調用這個方法

  HashedWheelBucket是一個鏈表,所以我們需要從head節點往下進行遍歷。如果鏈表沒有遍歷到鏈表尾部那麼就繼續往下遍歷。獲取的timeout節點,如果剩餘輪數remainingRounds大於0,那麼就說明要到下一圈才能運行,所以將剩餘輪數減一

  如果當前剩餘輪數小於等於零了,那麼就將當前節點從bucket鏈表中移除,並判斷一下當前的時間是否大於timeout的延遲時間,如果是則調用timeout的expire執行任務。

void expireTimeouts(long deadline) {
            HashedWheelTimeout timeout = head;

            // process all timeouts
            // 遍歷當前時間槽中的所有任務
            while (timeout != null) {
                HashedWheelTimeout next = timeout.next;
                //如果當前任務要被執行,那麼remainingRounds應該小於或者等於0
                if (timeout.remainingRounds <= 0) {
                    //從bucket鏈表中移除當前timeout,並返回鏈表中下一個timeout
                    next = remove(timeout);
                    //如果timeout的時間小於當前的時間,那麼就調用expire執行task
                    if (timeout.deadline <= deadline) {
                        timeout.expire();
                    } else {
                        //不可能發生的情況,就是說round已經爲0了,deadline卻>當前槽的deadline
                        // The timeout was placed into a wrong slot. This should never happen.
                        throw new IllegalStateException(String.format(
                                "timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline));
                    }
                } else if (timeout.isCancelled()) {
                    next = remove(timeout);
                } else {
                    //因爲當前的槽位已經過了,說明已經走了一圈了,把輪數減一
                    timeout.remainingRounds--;
                }
                //把指針放置到下一個timeout
                timeout = next;
            }
}

  這就是整個時間輪的流程了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章