时间轮在Netty、Kafka中的应用

2023-10-27

时间轮

概述

时间轮是一个高性能、低消耗的数据结构,它适合用非准实时,延迟的短平快任务,例如心跳检测。在Netty、Kafka、Zookeeper中都有使用。

时间轮可通过时间与任务存储分离的形式,轻松实现百亿级海量任务调度。

Netty中的时间轮

作用

Netty动辄管理100w+的连接,每一个连接都会有很多超时任务。比如发送超时、心跳检测间隔等,如果每一个定时任务都启动一个Timer,不仅低效,而且会消耗大量的资源。

抽象

时间轮 时间轮的格子 格子里的任务 时间轮运转线程
HashedWheelTimer HashedWheelBucket HashedWheelTimeout Worker

其他一些属性:

时间轮零点时间:startTime
当前指针所指格子:tick
格子长度(持续时间):tickDuration
时间轮运转轮次、回合:remainingRounds
任务截止时间、触发时间(相对时间轮的startTime):deadline

概括时间轮工作流程

(阅读Netty3.10.6)

1、时间轮的启动并不是在构造函数中,而是在第一次提交任务的时候newTimeout()
2、启动时间轮第一件事就是初始化时间轮的零点时间startTime,以后时间轮上的任务、格子触发时间计算都相对这个时间
3、随着时间的推移第一个格子(tick)触发,在触发每个格子之前都是处于阻塞状态,并不是直接去处理这个格子的所有任务,而是先从任务队列timeouts中拉取最多100000个任务,根据每个任务的触发时间deadline放在不同的格子里(注意,Netty中会对时间轮上的每一个格子进行处理,即使这个格子没有任务)
4、时间轮运转过程中维护着一个指针tick,根据当前指针获取对应的格子里的所有任务进行处理
5、任务自身维护了一个剩余回合(remainingRounds),代表任务在哪一轮执行处理,只有该值为0时才进行处理

源码

代码做了删减,只体现重点

时间轮构造器:

初始化了时间轮大小、每个格子大小、时间轮运转线程

public HashedWheelTimer(
    ThreadFactory threadFactory,
    ThreadNameDeterminer determiner,
    long tickDuration, TimeUnit unit, int ticksPerWheel) {

    // TODO : 创建时间轮底层存储任务的数据结构
    wheel = createWheel(ticksPerWheel);
    // TODO : 求某一个任务落到哪个格子时需要用到的编码
    mask = wheel.length - 1;

    // TODO : 每个格子的时间
    this.tickDuration = unit.toNanos(tickDuration);

    // TODO : 时间轮处理任务的线程
    workerThread = threadFactory.newThread(new ThreadRenamingRunnable(
        worker, "Hashed wheel timer #" + id.incrementAndGet(),
        determiner));
}
// TODO : 时间轮真正存储数据的容器
private final HashedWheelBucket[] wheel;
// TODO : 存放任务的队列
private final Queue<HashedWheelTimeout> timeouts = new ConcurrentLinkedQueue<HashedWheelTimeout>();

外界提交任务的时候,代码如下

public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
    // TODO : 启动时间轮运转线程
    start();

    long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;
    HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
    // TODO : 任务放入到队列中,并没有一开始就放到时间轮上
    timeouts.add(timeout);
    return timeout;
}

时间轮运转执行任务,代码如下

public void run() {
    // TODO : 初始化时间轮的
    startTime = System.nanoTime();

    do {
        // TODO : 这个方法会阻塞,随着时间的推动会触发新的任务(tick),返回当前时间
        final long deadline = waitForNextTick();
        if (deadline > 0) {
            // TODO : 将队列中的任务最多取100000放到时间轮上
            transferTimeoutsToBuckets();
            // TODO : 获取当前格子
            HashedWheelBucket bucket = wheel[(int) (tick & mask)];
            // TODO : 执行时间轮上当前格子上的任务
            bucket.expireTimeouts(deadline);
            // TODO : 指针走动
            tick++;
        }
    } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);
}

run内部方法解析

waitForNextTick等待下一个格子触发,代码如下

private long waitForNextTick() {
    // TODO : 截止时间、触发时间
    // TODO : 获取当前格子的触发时间,因为时间轮底层是使用数组存储任务数据,所以tick需要+1
    long deadline = tickDuration * (tick + 1);
    /**
             * tick : 时间轮上的格子
             * tickDuration : 每个格子的长度,持续时间
             * deadline : 这里表示下一个格子的触发时间(触发一个格子的任务)相对时间轮起点时间(startTime)的时长
             */
    
    for (;;) {
        // TODO : 相对时间轮起点的当前时间
        final long currentTime = System.nanoTime() - startTime;
        // TODO : 当当前时间大于等于deadline的时候,就会跳出循环
        long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;

        if (sleepTimeMs <= 0) {
            if (currentTime == Long.MIN_VALUE) {
                return -Long.MAX_VALUE;
            } else {
                return currentTime;
            }
        }
        try {
            // TODO : 并不是一直循环
            Thread.sleep(sleepTimeMs);
        } catch (InterruptedException e) {
            if (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) {
                return Long.MIN_VALUE;
            }
        }
    }
}

transferTimeoutsToBuckets将队列中任务存储到时间轮上,代码如下

private void transferTimeoutsToBuckets() {
    for (int i = 0; i < 100000; i++) {
        // TODO : 从队列中取出任务
        HashedWheelTimeout timeout = timeouts.poll();
        if (timeout == null) {
            // all processed 已全部处理
            break;
        }
        if (timeout.state() == HashedWheelTimeout.ST_CANCELLED
            || !timeout.compareAndSetState(HashedWheelTimeout.ST_INIT, HashedWheelTimeout.ST_IN_BUCKET)) {
            // 期间被取消。所以只需从队列中删除它并继续下一个 HashedWheelTimeout
            timeout.remove();
            continue;
        }
        // TODO : 计算这个任务要走多少个格子
        long calculated = timeout.deadline / tickDuration;
        // TODO : 计算触发当前这个任务还要走多少轮,剩余回合!
        /**
                 * calculated:触发该任务一共要走的格子数
                 * tick:当前已经走的格子数
                 * wheel.length:时间轮的长度
                 */
        long remainingRounds = (calculated - tick) / wheel.length;
        // TODO : 任务自身携带了触发自己的轮次
        timeout.remainingRounds = remainingRounds;
        final long ticks = Math.max(calculated, tick); 
        // TODO : mask = wheel.length - 1
        int stopIndex = (int) (ticks & mask);

        // TODO : 将任务放到时间轮的对应格子中
        HashedWheelBucket bucket = wheel[stopIndex];
        bucket.addTimeout(timeout);
    }
}

expireTimeouts执行处理任务,代码如下

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

    while (timeout != null) {
        boolean remove = false;
        // TODO : 根据剩余回合判断是否要处理该任务,如果大于0说明还没轮到该任务
        if (timeout.remainingRounds <= 0) {
            // TODO : 如果时间已经到了,则执行任务
            /**
                     * deadline 是相对时间轮startTime的当前时间,也是当前格子的触发时间
                     * timeout.deadline 是任务的触发时间
                     */
            if (timeout.deadline <= deadline) {
                // TODO :
                timeout.expire();
            } else {
                // 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));
            }
            remove = true;
        } else if (timeout.isCancelled()) {
            remove = true;
        } else {
            timeout.remainingRounds --;
        }
        // store reference to next as we may null out timeout.next in the remove block.
        HashedWheelTimeout next = timeout.next;
        if (remove) {
            remove(timeout);
        }
        timeout = next;
    }
}

Kafka中的时间轮

作用

Produce 时等待 ISR 副本复制成功、延迟删除主题、会话超时检查、延迟创建主题或分区等,会被封装成不同的 DelayOperation 进行延迟处理操作,防止阻塞 Kafka请求处理线程。

抽象

名称 时间轮 时间轮的格子(桶) 格子(桶)里的任务 时间轮运转线程 处理过期任务线程
类名 TimingWheel TimerTaskList TimerTaskEntry ShutdownableThread ExecutorService
属性名 timingWheel bucket root\head\tail expirationReaper taskExecutor

其他一些属性:

时间轮零点时间:startMs
当前时间:currentTime
格子长度(持续时间):tickMs
时间轮大小:wheelSize
时间轮的当前层时间跨度:interval = tickMs * wheelSize
到期时间:expiration
溢出轮、升层的时间轮:overflowWheel: TimingWheel

概括时间轮工作流程

(阅读Kafka-3.1.0)

Kafka 中的时间轮(TimingWheel)是一个存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表(TimerTaskList)。TimerTaskList是一个环形的双向链表,链表中的每一项表示的都是定时任务项(TimerTaskEntry),其中封装了真正的定时任务(TimerTask)。

1、Kafka启动的时候就启动了时间轮
2、ExpiredOperationReaper.doWork() 循环执行,首先从全局的delayQueue中获取一个bucket,如果不为空则上锁处理
3、根据bucket的到期时间尝试推进,然后会刷一次bucket中的所有任务,这些任务要么是需要立即执行的(即到期时间在 currentTime 和 currentTime + tickMs 之间),要么是需要换桶的,往前移位(即到期时间大于等于 currentTime + tickMs);立即计算的直接提交给专门的线程处理
4、最后拉取delayQueue中下一个bucket处理,一直循环下去
5、添加一个任务,首先是根据任务的到期时间expiration来判断自己会落到哪一个bucket,如果expiration不小于currentTime + tickMs,则可能是当前时间轮的任一个bucket,也可能是溢出轮中的任一个bucket
6、当任务添加到某一个bucket后会判断是否跟新了桶的到期时间,如果更新了则需要入队处理delayQueue.offer

源码

代码做了删减,只体现重点

1、Kafka中自己封装了一个可关闭的线程类 Shutdown’able’Thread ,也就是实现了该类的 ExpiredOperationReaper 内部实现了 doWork() 方法,维护着时间轮的运转

private class ExpiredOperationReaper extends ShutdownableThread(
    "ExpirationReaper-%d-%s".format(brokerId, purgatoryName),
    false) {

    override def doWork(): Unit = {
        advanceClock(200L)
    }
}

2、推进时钟的内部实现

def advanceClock(timeoutMs: Long): Boolean = {
    // TODO : 阻塞 timeoutMs = 200 毫秒,拉取一个桶:有直接返回,没有则阻塞200毫秒
    var bucket = delayQueue.poll(timeoutMs, TimeUnit.MILLISECONDS)
        if (bucket != null) {
            writeLock.lock()
                try {
                    while (bucket != null) {
                        // TODO : 传入当前桶的过期时间,尝试推进时间
                        timingWheel.advanceClock(bucket.getExpiration)
                        // TODO : 无论推进时间是否成功,当前桶的这些任务要么是需要立即执行的(即到期时间在 currentTime 和 currentTime + tickMs 之间),
                        //  要么是需要换桶的,往前移位(即到期时间大于等于 currentTime + tickMs);立即计算的直接提交给专门的线程处理
                        bucket.flush(addTimerTaskEntry)
                        // TODO : 进行下一个桶处理
                        bucket = delayQueue.poll()
                    }
                } finally {
                    writeLock.unlock()
                }
            true
        } else {
            false
        }
}

3、尝试推进时钟

def advanceClock(timeMs: Long): Unit = {
    /**
     * currentTime + tickMs :当前桶过期时间的截止时间
     * timeMs :下一个桶的过期时间
     */
    if (timeMs >= currentTime + tickMs) {
      // currentTime 是 tickMs 的整数倍
      currentTime = timeMs - (timeMs % tickMs)
      // TODO : 尝试推进溢出轮的时间
      if (overflowWheel != null) overflowWheel.advanceClock(currentTime)
    }
  }

4、bucket.flush(addTimerTaskEntry) 传入的是一个方法之后桶内的每一个任务都会走一次该方法

// TODO : 添加或处理任务
  private def addTimerTaskEntry(timerTaskEntry: TimerTaskEntry): Unit = {
    // TODO : 只有到期时间在 currentTime 和 currentTime + tickMs 之间的任务才会被直接处理
    if (!timingWheel.add(timerTaskEntry)) {
      // Already expired or cancelled
      if (!timerTaskEntry.cancelled) {
        // TODO : 只处理过期时间到达且不是被取消的任务
        taskExecutor.submit(timerTaskEntry.timerTask)
      }
    }
  }

5、添加任务到时间轮的入口也是地4步的方法,其中timingWheel.add(timerTaskEntry) 方法中会判断每一个任务是立即处理还是入队

/**
   * 添加一个任务
   * 添加任务的过程比较复杂,首先是根据任务的到期时间来判断自己会落到哪一个bucket,可能是当前时间轮任一个bucket,也可能是溢出轮中的任一个bucket
   */
  def add(timerTaskEntry: TimerTaskEntry): Boolean = {
    // TODO : 任务到期时间
    val expiration = timerTaskEntry.expirationMs
    if (timerTaskEntry.cancelled) {
      false
    } else if (expiration < currentTime + tickMs) {
      // TODO : 距离该任务到期仅剩最多 tickMs 毫秒了
      // TODO : currentTime当前指向的时间格也属于到期部分,表示刚好到期
      false
    } else if (expiration < currentTime + interval) {
      // TODO : 距离该任务到期小于一整轮的时间,大于一个格子的时间,说明它就在当前层,不需要升层
      val virtualId = expiration / tickMs
      val bucket = buckets((virtualId % wheelSize.toLong).toInt)
      bucket.add(timerTaskEntry)
      // TODO : 如果该任务的到来改变了他所进入的桶的过期时间,即轮子已经前进并且之前的桶被重用了
      // TODO : 桶是同一个桶,但是数据可能不是同一轮的,这时需要重新入队 DelayQueue
      if (bucket.setExpiration(virtualId * tickMs)) {
        queue.offer(bucket)
      }
      true
    } else {
      // TODO : 需要升层 过期时间超过了 interval
      if (overflowWheel == null) addOverflowWheel()
      overflowWheel.add(timerTaskEntry)
    }
  }

需要升层的情况:其实每一个时间轮对象内都有一个溢出轮的指针 overflowWheel ,他会指向父级时间轮。

总结

Kafka 使用时间轮来实现延时队列,因为其底层是任务的添加和删除是基于链表实现的,是 O(1) 的时间复杂度,满足高性能的要求;

对于时间跨度大的延时任务,Kafka 引入了层级时间轮,能更好控制时间粒度,可以应对更加复杂的定时任务处理场景;

对于如何实现时间轮的推进和避免空推进影响性能,Kafka 采用空间换时间的思想,通过 DelayQueue 来推进时间轮,算是一个经典的 trade off。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

时间轮在Netty、Kafka中的应用 的相关文章

  • 深度强化学习的核心算法:从QLearning到Deep QNetwork

    1 背景介绍 深度强化学习 Deep Reinforcement Learning DRL 是一种通过智能体与环境的互动学习的方法 它可以帮助智能体在没有明确指导的情况下学习如何执行最佳的动作 从而最大化收益 深度强化学习结合了强化学习 R
  • 心灵与计算机:解密情感处理

    1 背景介绍 情感处理是人工智能领域中一个重要的研究方向 它旨在使计算机能理解 识别和处理人类的情感 情感处理的主要应用包括情感分析 情感识别 情感挖掘等 随着大数据 深度学习和自然语言处理等技术的发展 情感处理技术已经取得了显著的进展 然
  • 线性代数在深度学习中的角色

    1 背景介绍 深度学习是一种人工智能技术 它主要通过神经网络来学习和模拟人类大脑的思维过程 线性代数是一门数学分支 它研究的是向量和矩阵的运算 在深度学习中 线性代数起着非常重要的作用 因为它为神经网络提供了数学模型和计算方法 在这篇文章中
  • 人工智能与机器学习:未来的编程范式

    1 背景介绍 人工智能 Artificial Intelligence AI 和机器学习 Machine Learning ML 是现代计算机科学的重要领域之一 它们旨在让计算机能够自主地学习 理解和进化 以解决复杂的问题 随着数据量的增加
  • Spring Webflux 不明时间损失

    我们最近切换到 ExpediaGroups GraphQLlibrary https github com ExpediaGroup graphql kotlin它基于 Spring Webflux Since switching our
  • 这个很少人知道的零售技巧,却是我最想安利的!

    在当今数字化浪潮的推动下 零售业正在迎来一场革命性的变革 新零售模式的崛起正引领着消费者与商品之间的互动方式发生深刻的变化 在这个变革的前沿 自动售货机作为新零售的一种关键形式 通过智能技术和自动化系统 重新定义了购物体验的边界 客户案例
  • 什么是充放电振子理论?

    CHAT回复 充放电振子模型 Charging Reversal Oscillator Model 是一种解释ENSO现象的理论模型 这个模型把ENSO现象比喻成一个 热力学振荡系统 在这个模型中 ENSO现象由三个组成部分 充电 Char
  • 扬帆证券投资者必知:股票配股与增发的区别你清楚吗?

    配股和增发都是股票再融资的方式 不过二者有一定的区别 1 发行对象不同 配股是向原股东发售一定量股票 一般会以低于市价的价格发售 增发是向全体社会公众发行股票 即新老股东都能获得 2 发行前是否需要公告价格 配股会事先公告配股价 配股的定价
  • ESM10A 消除对单独 PLC 的需求

    ESM10A 消除对单独 PLC 的需求 ESM10A 可以消除对单独 PLC 的需求 该程序是在 PC 上开发的 然后使用免费提供的简单易用的 EzSQ 软件下载到逆变器 似乎这些改进还不够 日立还在 SJ700 中添加了其他新功能 例如
  • 对中国手机作恶的谷歌,印度CEO先后向三星和苹果低头求饶

    日前苹果与谷歌宣布合作 发布了 Find My Device Network 的草案 旨在规范蓝牙追踪器的使用 在以往苹果和谷歌的生态形成鲜明的壁垒 各走各路 如今双方竟然达成合作 发生了什么事 首先是谷歌安卓系统的市场份额显著下滑 数年来
  • 2023下半年软考「单独划线」合格标准公布

    中国计算机技术职业资格网发布了 关于2023年度下半年计算机软件资格考试单独划线地区合格标准的通告 2023下半年软考单独划线地区合格标准各科目均为42分 01 官方通告 关于2023年度下半年计算机软件资格考试单独划线地区合格标准的通告
  • 每个 UDP 数据报的 Netty 不同管道

    我们有一个已经在 TCP IP 中实现的服务器 但现在我们要求该协议也支持 UDP 发送的每个 UDP 数据报都包含我需要解码的所有内容 因此这是一个非常简单的回复和响应系统 数据报中的数据由换行符分隔 服务器启动时的引导代码如下所示 SE
  • ChannelOption.SO_BACKLOG 的作用是什么?

    option ChannelOption SO BACKLOG 100 Netty 4 升级文档中显示 你能解释一下它的作用吗 Thanks 它是一个传递的套接字选项 用于确定排队的连接数 http docs oracle com java
  • Netty 4. ByteToMessageCodec之后的并行处理

    If a NioEventLoopGroup被用作workerGroup 之后的消息ByteToMessageDecoder处理程序 对于单个连接 通过以下处理程序以顺序 单线程 方式处理NioEventLoop 是否有可能让它们在之后由另
  • 与Netty相比,vert.x如何实现卓越的性能?

    最近的TechEmpower 性能基准 http www techempower com benchmarks 一直在 Netty 之上展示 vert x 有时数量很大 根据其网站 vert x 使用 Netty 来实现 大部分网络 IO
  • Java 互操作——Netty + Clojure

    我正在尝试通过 clojure 使用 netty 我可以启动服务器 但是它无法初始化接受的套接字 下面分别是错误消息和代码 有谁知道什么是 或可能是错误的 我相信问题在于 Channels pipeline server handler T
  • 如何使用 Netty 发送对象?

    如何通过Netty从服务器端发送bean并在客户端接收该bean 当我发送简单的整数消息 inputstream 时 它工作成功 但我需要发送 bean 如果您在客户端和服务器端使用 Netty 那么您可以使用 Netty对象解码器 htt
  • netty 4.x.x 中的 UDP 广播

    我们需要使用 Netty 4 0 0 二进制文件通过 UDP 通道广播对象 Pojo 在 Netty 4 0 0 中 它允许我们仅使用 DatagramPacket 类来发送 UDP 数据包 此类仅接受 ByteBuf 作为参数 还有其他方
  • 使用 Play WS 并获取 java.net.ConnectException:Amazon Cloudfront 上的握手超时

    在我的 Play 应用程序中 我需要从 Amazon Cloudfront 下载大量文件 使用 SSL 我在链接上随机收到以下错误 play api http HttpErrorHandlerExceptions anon 1 Execut
  • Netty通道读取混乱

    我三个月前开始使用 Netty 最初 它看起来非常简单且易于使用 因为我遵循了 4 x 系列主页中给出的示例 当我更深入地探索它时 我无法理解某些事件或回调名称 例如 我无法理解以下内容之间的区别 ChannelRead ChannelHa

随机推荐

  • Windows操作系统安全加固基线检测脚本

    一 背景信息 在我们的安全运维工作中经常需要进行安全基线配置和检查 所谓的安全基线配置就是系统的最基础的安全配置 安全基线检查涉及操作系统 中间件 数据库 甚至是交换机等网络基础设备的检查 面对如此繁多的检查项 自动化的脚本可以帮助我们快速
  • 树的遍历(bfs+递归)

    题目描述 一个二叉树 树中每个节点的权值互不相同 现在给出它的后序遍历和中序遍历 请你输出它的层序遍历 输入描述 第一行包含整数 N 表示二叉树的节点数 第二行包含 N 个整数 表示二叉树的后序遍历 第三行包含 N个整数 表示二叉树的中序遍
  • Docker容器占用过多C盘空间问题解决方案

    Docker容器占用过多C盘空间问题解决方案 简介 Docker 是一个开源的容器化平台 它能够将应用程序及其依赖项打包成一个独立的 可移植的容器 然而 在使用 Docker 过程中 有时会遇到C盘空间不足的问题 这是因为默认情况下 Doc
  • cpu cache一致性和内存屏障机制

    1 cache 局部性原理 引入 Cache 的理论基础是程序局部性原理 包括时间局部性和空间局部性 即最近被CPU访问的数据 短期内CPU 还要访问 时间 被 CPU 访问的数据附近的数据 CPU 短期内还要访问 空间 因此如果将刚刚访问
  • Spring采用properties配置多个数据库

    在一个项目中有这样的需求 上海和武汉采用不同的系统 每个系统都有自己的数据库 但是在上海的系统需要访问武汉的数据库 这就要在项目中配置两个数据源 下面是我给的SSH采用properties配置数据源的方法 1 要有两个properties文
  • Faster R-CNN网络架构详解和TensorFlow Hub实现(附源码)

    文章目录 一 RPN网络 1 RPN网络简介 2 backbone网络简介 二 Faster R CNN网络架构 1 Faster R CNN网络简介 2 基于TensorFlow Hub实现Faster R CNN 前言 Faster R
  • 1089 狼人杀-简单版 (20 分)

    题目 题目链接 题解 思维 首先我们要明确这类问题不用计算机 我们会怎么去做 显然是推矛盾吧 就是假设哪些是狼人 哪些说了假话等等 根据每个人说的话推出矛盾就说明假设不合理 反之正确 既然要推出矛盾就需要找到一些条件 如果推的过程中发现与条
  • 使用UncaughtExceptionHandler捕获运行时异常

    前面我们知道Exceptions分为可检查异常 checked exceptions 和运行时异常 runtime exception 具体参照文章Java异常处理手册和最佳实践 对于可检查异常 我们必须对它进行处理 要么捕获要么在方法上使
  • 【Grub & Grub2】万能优盘启动盘 (WinPE、LinuxPE)-- 方法1 U盘三分区法(不推荐,供参考)

    由于工作需要 经常使用Windows和Linux双系统 系统使用过程中 个人涉及到的开发软件过多 光基于Eclipse的IDE就有好几个 经常过度安装软件 有时会越来越庞大 越来越不稳定 定期要重新安装配置 但是又不想重头安装 基本软件最好
  • Redis基础

    导航 黑马Java笔记 踩坑汇总 JavaSE JavaWeb SSM SpringBoot 瑞吉外卖 SpringCloud SpringCloudAlibaba 黑马旅游 谷粒商城 目录 1 简介 1 1 环境准备 1 1 1 Redi
  • 使用nssm工具将ES、Kibana、Logstash或者其他.bat文件部署为Windows后台服务的方法

    使用NSSM工具安装bat文件为Windows服务 nssm是一个可以把bat批处理文件部署为Windows服务的小工具 例如很多 net项目可能还是在Windows服务器上面跑的 但是很多组件只提供了 bat文件 例如elk三件套 或者后
  • 【王道考研 操作系统】【第二章】进程同步、进程互斥的实现方法 软件&硬件 优点&缺点 信号量机制

    目录 第二章 9 进程同步 进程互斥 9 1 进程同步 9 2 进程互斥 9 2 1 实现过程 9 2 2 实现互斥须遵循的 原则 9 2 3 软件实现方法 9 2 4 硬件实现方法 10 信号量机制 10 1 整型信号量 10 2 记录型
  • 【华为OD统一考试B卷

    在线OJ 已购买本专栏用户 请私信博主开通账号 在线刷题 运行出现 Runtime Error 0Aborted 请忽略 华为OD统一考试A卷 B卷 新题库说明 2023年5月份 华为官方已经将的 2022 0223Q 1 2 3 4 统一
  • MATLAB R2023a完美激活版(附激活补丁)

    MATLAB R2023a是一款面向科学和工程领域的高级数学计算和数据分析软件 它为Mac用户提供了强大的工具和功能 用于解决各种复杂的数学和科学问题 以下是MATLAB R2023a Mac的一些主要特点和功能 软件下载 MATLAB R
  • 【自用】3.测试框架TestNG

    1 TestNG基本介绍 2 注解 2 1 Test package com course testng import org testng annotations Test public class BasicAnnotation 最基本
  • 为什么GPU训练网络还不如CPU快

    为什么GPU训练网络还不如CPU快 当网络规模较小的时候 GPU是无法体现出计算上的优势的 可能不光没有加速效果 反而还不如CPU训练的快 只要加大网络规模 当网络足够大的时候 GPU才能显示出它的加速效果 Pytorch官网的这篇tuto
  • 在国内怎么使用谷歌Chrome浏览器,为什么我的谷歌浏览器进去就加载失败

    START 你是不是经常听谁谁说 哎呀 你用的什么浏览器 这么laji 好慢哟 哎 我给你推荐个神器谷歌浏览器 用着贼爽 然后 你就想回去马上就下载 这不我也下载了 然后就出事了 下面跟着我的步伐一起看看吧 一 当你如获至宝的在搜索框输入关
  • 根据id进行数组的去重

    一开始用 New set 但是这种方式只对 1 2 2 3 4 这种形式的数组生效 对数组里面全是对象的话就不生效了 所以取数组里面每个对象对应的id值进行filter处理 如下 aaa 二级评论数组去重 const idMap retur
  • Servlet生命周期与工作原理

    Servlet生命周期分为三个阶段 1 初始化阶段 调用init 方法 2 响应客户请求阶段 调用service 方法 3 终止阶段 调用destroy 方法 Servlet初始化阶段 在下列时刻Servlet容器装载Servlet 1 S
  • 时间轮在Netty、Kafka中的应用

    时间轮 概述 时间轮是一个高性能 低消耗的数据结构 它适合用非准实时 延迟的短平快任务 例如心跳检测 在Netty Kafka Zookeeper中都有使用 时间轮可通过时间与任务存储分离的形式 轻松实现百亿级海量任务调度 Netty中的时