【sentinel】滑动时间窗口算法在Sentinel中的应用

news2024/11/24 6:57:03

固定窗口算法(计数器法)

算法介绍

计数器法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1秒的访问次数不能超过10次。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于10并且该请求与第一个请求的间隔时间还在1秒之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1秒,那么就重置counter。

代码实现

固定时间窗口算法的代码实现如下:

package com.morris.user.demo;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * 固定时间窗口算法
 */
@Slf4j
public class WindowDemo {

    public static void main(String[] args) {
        Window window = new Window(2);
        for (int i = 0; i < 30; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(new Random().nextInt(3000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info(finalI + "--> " + window.canPass());
            }).start();
        }
    }

    public static class Window {

        // 时间窗口默认1s
        public static final int WINDOWS_TIME = 1000;

        // 计数器
        public int qps = 0;

        // 窗口的开始时间
        public long windowStartTime = System.currentTimeMillis();

        // 时间窗口内允许的最大请求数
        private final int maxCount;

        public Window(int maxCount) {
            this.maxCount = maxCount;
        }

        public synchronized boolean canPass() {

            long currentTime = System.currentTimeMillis();

            // 重置窗口
            if (currentTime - this.windowStartTime > WINDOWS_TIME) {
                this.qps = 0;
                this.windowStartTime = currentTime;
            }

            if (this.qps + 1 > this.maxCount) {
                return false;
            }
            this.qps++;
            return true;
        }
    }

}

缺点

固定窗口计数器限流算法实现起来虽然很简单,但是有一个十分致命的问题,那就是临界突刺问题:前一秒的后半段和后一秒的前半段流量集中一起,会出现大量流量。

计数器的限制数量判断是按时间段的,在两个时间段的交界时间点,限制数量的当前值会发生一个抖动的变化,从而使瞬间流量突破了我们期望的限制。例如以下的情况:


可以看到在0:59的时候,如果突然来了100个请求,这时候当前值是100,而到了1:00的时候,因为是下一个时间段了,当前值陡降到0,这时候又进来100个请求,都能通过限流判断,虽然两个时间段平均下来还是没超过限制,但是在临界时间点的请求量却达到了两倍之多,这种情况下就可能压垮我们的系统。

滑动窗口算法

算法介绍

上面会出现突刺的问题其实就在于固定窗口算法的窗口时间跨度太大,且是固定不变的,为了解决突刺的问题,我们就有了滑动窗口计数器限流算法。

滑动窗口算法是固定窗口算法的优化版,主要有两个特点:

  • 划分多个小的时间段,各时间段各自进行计数。
  • 根据当前时间,动态往前滑动来计算时间窗口范围,合并计算总数。


可以看到,每次时间往后,窗口也会动态往后滑动,从而丢弃一些更早期的计数数据,从而实现总体计数的平稳过度。当滑动窗口划分的格子越多,那么滑动窗口的滑动就越平滑,限流的统计就会越精确。事实上,固定窗口算法就是只划分成一个格子的滑动窗口算法。

为了避免突刺,尽量缩小时间窗口,只要时间窗口足够小,小到只允许一个请求通过,这就是漏桶算法。

代码实现1

滑动时间窗口算法的具体代码实现如下:

package com.morris.user.demo;

import lombok.extern.slf4j.Slf4j;

import java.util.Objects;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * 滑动时间窗口算法
 */
@Slf4j
public class SideWindowDemo {

    public static void main(String[] args) {
        SideWindow sideWindow = new SideWindow(2);
        for (int i = 0; i < 30; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(new Random().nextInt(3000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info(finalI + "--> " + sideWindow.canPass());
            }).start();
        }
    }

    public static class SideWindow {

        // 时间窗口内允许的最大请求数
        public int maxCount;

        // 每个窗口的长度 100ms
        public static final int WINDOW_LENGTH = 100;

        // 1s划分为10个窗口
        private final Window[] array = new Window[10];

        public SideWindow(int maxCount) {
            this.maxCount = maxCount;
        }

        public Window currentWindow() {
            // 获取当前时间
            long now = System.currentTimeMillis();

            // 当前窗口的下标
            int currentIndex = (int) (now / WINDOW_LENGTH % array.length);
            // 获取当前窗口
            Window currentWindow = array[currentIndex];

            if (Objects.isNull(currentWindow)) {
                // 初始化窗口
                currentWindow = new Window(now);
                array[currentIndex] = currentWindow;
            } else if (now - currentWindow.startTime >= WINDOW_LENGTH) {
                // 重置窗口
                currentWindow.reset(now);
            }
            return currentWindow;
        }

        public int qps() {
            int qps = 0;
            for (Window window : array) {
                if(Objects.isNull(window)) {
                    continue;
                }
                qps += window.qps;
            }
            return qps;
        }

        public synchronized boolean canPass() {

            Window currentWindow = currentWindow();

            int qps = qps();
            if(qps + 1 > maxCount) {
                return false;
            }
            currentWindow.qps++;
            return true;
        }

    }

    public static class Window {
        // 这个窗口开始的时间
        long startTime;
        // 这个窗口的QPS
        long qps;

        public Window(long startTime) {
            this.startTime = startTime;
            this.qps = 0;
        }

        public void reset(long startTime) {
            this.startTime = startTime;
            this.qps = 0;
        }
    }
}

代码实现2

滑动时间窗口算法的第二种实现具体代码实现如下:

package com.morris.user.demo;

import lombok.extern.slf4j.Slf4j;

import java.util.LinkedList;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * 滑动时间窗口算法
 */
@Slf4j
public class SideWindowDemo2 {

    public static void main(String[] args) {
        SideWindow sideWindow = new SideWindow(2);
        for (int i = 0; i < 30; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(new Random().nextInt(3000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info(finalI + "--> " + sideWindow.canPass());
            }).start();
        }
    }

    public static class SideWindow {

        // 统计周期 1000ms
        public static final int TIME_WINDOW = 1000;

        private static final LinkedList<Long> list = new LinkedList<>();

        // 时间窗口内允许的最大请求数
        private final int maxCount;

        public SideWindow(int maxCount) {
            this.maxCount = maxCount;
        }

        public synchronized boolean canPass() {
            // 获取当前时间
            long nowTime = System.currentTimeMillis();
            // 如果队列还没满,则允许通过,并添加当前时间戳到队列开始位置
            if (list.size() < maxCount) {
                list.addFirst(nowTime);
                return true;
            }

            // 队列已满(达到限制次数),则获取队列中最早添加的时间戳
            Long farTime = list.getLast();
            // 用当前时间戳 减去 最早添加的时间戳
            if (nowTime - farTime <= TIME_WINDOW) {
                // 若结果小于等于timeWindow,则说明在timeWindow内,通过的次数大于count
                // 不允许通过
                return false;
            } else {
                // 若结果大于timeWindow,则说明在timeWindow内,通过的次数小于等于count
                // 允许通过,并删除最早添加的时间戳,将当前时间添加到队列开始位置
                list.removeLast();
                list.addFirst(nowTime);
                return true;
            }
        }
    }

}

优缺点

优点:

  • 简单易懂
  • 精度高(通过调整时间窗口的大小来实现不同的限流效果)
  • 可扩展性强(可以非常容易地与其他限流算法结合使用)

缺点:突发流量无法处理(无法应对短时间内的大量请求,但是一旦到达限流后,请求都会直接暴力被拒绝。这样我们会损失一部分请求,这其实对于产品来说,并不太友好),需要合理调整时间窗口大小。

滑动窗口限流算法虽然可以保证任意时间窗口内接口请求次数都不会超过最大限流值,但是相对来说对系统的瞬时处理能力还是没有考虑到,无法防止在更细的时间粒度上访问过于集中的问题,例如在同一时刻(同一秒)大量请求涌入,还是可能会超过系统负荷能力。

滑动窗口算法在Sentinel中的使用

滑动窗口算法主要用于流控规则中流控效果为直接拒绝,可用于针对线程数和QPS直接限流。

限流统计数据的使用

在流控规则校验时如果流控效果为直接拒绝,会调用DefaultController.canPass()此方法来校验请求是否允许通过。

com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController#canPass(com.alibaba.csp.sentinel.node.Node, int, boolean)

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    // 拿到当前时间窗口的线程数或QPS
    int curCount = avgUsedTokens(node);
    if (curCount + acquireCount > count) {
        if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
            // prioritized默认为false,不会进入
            long currentTime;
            long waitInMs;
            currentTime = TimeUtil.currentTimeMillis();
            waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
            if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
                node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                node.addOccupiedPass(acquireCount);
                sleep(waitInMs);

                // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
                throw new PriorityWaitException(waitInMs);
            }
        }
        // 请求数大于阈值直接返回false,抛出FlowException
        return false;
    }
    return true;
}

private int avgUsedTokens(Node node) {
    if (node == null) {
        return DEFAULT_AVG_USED_TOKENS;
    }
    // 如果是根据线程数限流,取node.curThreadNum()
    // 如果是根据QPS限流,取node.passQps()
    return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
}

DefaultController.canPass()主要是拿到当前窗口的线程数或QPS,然后进行判断。

QPS的计算是通过rollingCounterInSecond.pass()方法得来的。

com.alibaba.csp.sentinel.node.StatisticNode#passQps

public double passQps() {
    return rollingCounterInSecond.pass() / rollingCounterInSecond.getWindowIntervalInSec();
}

pass()会遍历所有的窗口,累加每个窗口的QPS。

com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric#pass

public long pass() {
    data.currentWindow();
    long pass = 0;
    List<MetricBucket> list = data.values();

    for (MetricBucket window : list) {
        pass += window.pass();
    }
    return pass;
}

限流统计数据的统计时机

在目标方法调用完成之后由sentinel责任链中的StatisticSlot进行统计。

com.alibaba.csp.sentinel.slots.statistic.StatisticSlot#entry

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable {
    try {
        // Do some checking.
        fireEntry(context, resourceWrapper, node, count, prioritized, args);

        // 目标方法调用完成之后进行统计
        // Request passed, add thread count and pass count.
        // DefaultNode和ClusterNode 线程数+1
        node.increaseThreadNum();
        // DefaultNode和ClusterNode QPS+count
        node.addPassRequest(count);

        if (context.getCurEntry().getOriginNode() != null) {
            // Add count for origin node.
            // OriginNode  线程数+1
            // 为什么不是context.getOriginNode()
            // context.getCurEntry().getOriginNode() == context.getOriginNode()
            context.getCurEntry().getOriginNode().increaseThreadNum();
            // OriginNode QPS+count
            context.getCurEntry().getOriginNode().addPassRequest(count);
        }

        if (resourceWrapper.getEntryType() == EntryType.IN) {
            // Add count for global inbound entry node for global statistics.
            // 记录整个系统的线程数和QPS,可用于系统规则
            Constants.ENTRY_NODE.increaseThreadNum();
            Constants.ENTRY_NODE.addPassRequest(count);
        }

        // 扩展点,回调
        // Handle pass event with registered entry callback handlers.
        for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
            handler.onPass(context, resourceWrapper, node, count, args);
        }

限流统计数据的核心组件介绍

统计数据的存储与计算是通过StatisticNode来实现的。

public class StatisticNode implements Node {

    /**
     * Holds statistics of the recent {@code INTERVAL} seconds. The {@code INTERVAL} is divided into time spans
     * by given {@code sampleCount}.
     */
    private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
        IntervalProperty.INTERVAL);

StatisticNode的底层核心是使用ArrayMetric。

public class ArrayMetric implements Metric {

    private final LeapArray<MetricBucket> data;

    public ArrayMetric(int sampleCount, int intervalInMs) {
        this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
    }

ArrayMetric底层使用OccupiableBucketLeapArray。

public OccupiableBucketLeapArray(int sampleCount, int intervalInMs) {
    // This class is the original "CombinedBucketArray".
    super(sampleCount, intervalInMs);
    this.borrowArray = new FutureBucketLeapArray(sampleCount, intervalInMs);
}

查看OccupiableBucketLeapArray父类LeapArray的构造方法:

public LeapArray(int sampleCount, int intervalInMs) {
    AssertUtil.isTrue(sampleCount > 0, "bucket count is invalid: " + sampleCount);
    AssertUtil.isTrue(intervalInMs > 0, "total time interval of the sliding window should be positive");
    AssertUtil.isTrue(intervalInMs % sampleCount == 0, "time span needs to be evenly divided");

    // 窗口的长度500ms
    this.windowLengthInMs = intervalInMs / sampleCount;
    // 时间间隔,单位为ms
    this.intervalInMs = intervalInMs;
    // 时间间隔,单位为s
    this.intervalInSecond = intervalInMs / 1000.0;
    // 时间窗口个数
    this.sampleCount = sampleCount;
    // 存放窗口的统计数据
    this.array = new AtomicReferenceArray<>(sampleCount);
}

到这里可以看出sentinel将1秒划分为2个时间窗口,每个窗口的长度为500ms。

滑动时间窗口算法的实现

统计数据保存时肯定要先拿到当前时间所在的窗口,然后将数据添加到当前窗口上。

com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric#addPass

public void addPass(int count) {
    WindowWrap<MetricBucket> wrap = data.currentWindow();
    wrap.value().addPass(count);
}

要怎么获得该时间窗口,如何判断该获取哪个时间窗口呢?

com.alibaba.csp.sentinel.slots.statistic.base.LeapArray#currentWindow()

public WindowWrap<T> currentWindow() {
    return currentWindow(TimeUtil.currentTimeMillis());
}

    public WindowWrap<T> currentWindow(long timeMillis) {
    if (timeMillis < 0) {
        return null;
    }

    // 计算当前时间戳在数组中的位置
    // timeMillis / windowLengthInMs % array.length()
    int idx = calculateTimeIdx(timeMillis);

    // 计算当前时间戳所在窗口的开始时间
    // Calculate current bucket start time.
    // timeMillis - timeMillis % windowLengthInMs
    long windowStart = calculateWindowStart(timeMillis);

    /*
     * Get bucket item at given time from the array.
     *
     * (1) Bucket is absent, then just create a new bucket and CAS update to circular array.
     * (2) Bucket is up-to-date, then just return the bucket.
     * (3) Bucket is deprecated, then reset current bucket and clean all deprecated buckets.
     */
    while (true) {
        WindowWrap<T> old = array.get(idx);
        if (old == null) {
            /*
             *     B0       B1      B2    NULL      B4
             * ||_______|_______|_______|_______|_______||___
             * 200     400     600     800     1000    1200  timestamp
             *                             ^
             *                          time=888
             *            bucket is empty, so create new and update
             *
             * If the old bucket is absent, then we create a new bucket at {@code windowStart},
             * then try to update circular array via a CAS operation. Only one thread can
             * succeed to update, while other threads yield its time slice.
             */
            // 当前时间戳所在窗口无数据
            // 初始化窗口数据
            WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            if (array.compareAndSet(idx, null, window)) {
                // Successfully updated, return the created bucket.
                return window;
            } else {
                // Contention failed, the thread will yield its time slice to wait for bucket available.
                Thread.yield();
            }
        } else if (windowStart == old.windowStart()) {
            /*
             *     B0       B1      B2     B3      B4
             * ||_______|_______|_______|_______|_______||___
             * 200     400     600     800     1000    1200  timestamp
             *                             ^
             *                          time=888
             *            startTime of Bucket 3: 800, so it's up-to-date
             *
             * If current {@code windowStart} is equal to the start timestamp of old bucket,
             * that means the time is within the bucket, so directly return the bucket.
             */
            // 当前时间戳在这个窗口内
            return old;
        } else if (windowStart > old.windowStart()) {
            /*
             *   (old)
             *             B0       B1      B2    NULL      B4
             * |_______||_______|_______|_______|_______|_______||___
             * ...    1200     1400    1600    1800    2000    2200  timestamp
             *                              ^
             *                           time=1676
             *          startTime of Bucket 2: 400, deprecated, should be reset
             *
             * If the start timestamp of old bucket is behind provided time, that means
             * the bucket is deprecated. We have to reset the bucket to current {@code windowStart}.
             * Note that the reset and clean-up operations are hard to be atomic,
             * so we need a update lock to guarantee the correctness of bucket update.
             *
             * The update lock is conditional (tiny scope) and will take effect only when
             * bucket is deprecated, so in most cases it won't lead to performance loss.
             */
            if (updateLock.tryLock()) {
                try {
                    // Successfully get the update lock, now we reset the bucket.
                    /**
                     * @see OccupiableBucketLeapArray#resetWindowTo(com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap, long)
                     */
                    // 当前时间戳不在这个窗口内,重置窗口数据
                    return resetWindowTo(old, windowStart);
                } finally {
                    updateLock.unlock();
                }
            } else {
                // Contention failed, the thread will yield its time slice to wait for bucket available.
                Thread.yield();
            }
        } else if (windowStart < old.windowStart()) {
            // Should not go through here, as the provided time is already behind.
            return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/609284.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESP-BOX官方例程实践

1.下载esp-box项目代码 github仓库&#xff1a;https://github.com/espressif/esp-box gitee仓库&#xff1a;https://gitee.com/EspressifSystems/esp-box 使用git工具和如下命令进行下载&#xff1a; git clone --recursive https://github.com/espressif/esp-box.git or gi…

【C++ 基础篇:21】:friend 友元四连问:什么是友元?友元类?友元函数?什么时候用友元?

本系列 C 相关文章 仅为笔者学习笔记记录&#xff0c;用自己的理解记录学习&#xff01;C 学习系列将分为三个阶段&#xff1a;基础篇、STL 篇、高阶数据结构与算法篇&#xff0c;相关重点内容如下&#xff1a; 基础篇&#xff1a;类与对象&#xff08;涉及C的三大特性等&#…

S7-200 PLC的CPU模块介绍

更多关于西门子S7-200PLC内容查看&#xff1a;西门子200系列PLC学习课程大纲(课程筹备中) 1.什么是西门子200PLC的CPU? 如下图1-1所示&#xff0c;S7-200 PLC CUP是将一个微处理器&#xff0c;一个集成电源&#xff0c;一定的数字量或模拟量I/O&#xff0c;一定的通信接口等…

【Linux】—— git的管理以及使用

前言&#xff1a; 在上篇我们已经学习了关于调试器gdb的相关知识&#xff0c;本期我将为大家介绍的是关于版本控制工具——git的使用教程&#xff01;&#xff01;&#xff01; 目录 前言 &#xff08;一&#xff09;git的历史介绍 &#xff08;二&#xff09;github和gite…

Unity异步编程【6】——Unity中的UniTask如何取消指定的任务或所有的任务

今天儿童节&#xff0c;犬子已经9个多月了&#xff0c;今天是他的第一个儿童节。中年得子&#xff0c;其乐无穷&#xff08;音&#xff1a;ku bu kan yan&#xff09;…回头是岸啊 〇、 示例效果 一连创建5个异步任务[id 从0~4]&#xff0c;先停止其中的第id 4的任务&#x…

Flutter进阶篇-布局(Layout)原理

1、约束、尺寸、位置 overrideWidget build(BuildContext context) {return Scaffold(body: LayoutBuilder(builder: (context, constraints) {print("body约束:" constraints.toString());return Container(color: Colors.black,width: 300,height: 300,child: L…

【企业化架构部署】基于Nginx搭建LNMP架构

文章目录 一、安装 MySQL 数据库1. 安装Mysql环境依赖包2. 创建运行用户3. 编译安装4. 修改mysql 配置文件5. 更改mysql安装目录和配置文件的属主属组6. 设置路径环境变量7. 初始化数据库8. 添加mysqld系统服务9. 修改mysql 的登录密码10. 授权远程登录 二、编译安装 nginx 服务…

Maven 工具

Maven 工具 Maven简介Maven 基础概念创建 Maven项目依赖配置生命周期与插件分模块开发聚合和继承聚合继承聚合与继承的区别 属性版本管理多环境配置与应用私服 Maven简介 Maven 本质是一个项目管理工具&#xff0c;将项目开发和管理过程抽象成一个项目对象模型&#xff08;POM…

【爬虫】3.4爬取网站复杂数据

1. Web服务器网站 进一步把前面的Web网站的mysql.html, python.html, java.html丰富其中 的内容&#xff0c;并加上图形&#xff1a; mysql.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>my…

ICV报告: 智能座舱SoC全球市场规模预计2025年突破50亿美元

在智能化、互联化车辆需求不断增加的推动下&#xff0c;汽车行业正在经历一场范式转变。这一转变的前沿之一是智能座舱SoC。本市场研究报告对智能座舱SoC市场进行了全面的分析&#xff0c;包括其应用领域、当前状况和主要行业参与者。 智能座舱SoC指的是现代汽车智能座舱系统的…

Qt6.5.1+WebRTC学习笔记(十)开发环境搭建(win10+vs2022)

一、准备 1.操作系统win10 64位 2.合理的上网方式&#xff0c;需要正常访问google,最好有40G以上流量 3.安装VS2022&#xff0c;笔者使用的是社区版&#xff0c;并选中C相关&#xff0c;笔者设置如下 注意&#xff0c;win10的sdk需要是10.0.22621.0&#xff0c;其他版本可能…

吴恩达 ChatGPT Prompt Engineering for Developers 系列课程笔记--06 Transforming

06 Transforming 大语言模型&#xff08;LLM&#xff09;很擅于将输入转换为不同格式的输出&#xff0c;比如翻译、拼写校正或HTML格式转化。相比于复杂的正则表达式&#xff0c;Chat GPT实现更加准确和高效。 1) 不同语种的转换 下述语句实现了英文到西班牙语的翻译。 pro…

Windows IIS/docker+gunicorn两种方式部署django项目

写在最前 本篇文章并不涉及如何使用宝塔搭建django项目&#xff0c;仅适用于windows和docker部署&#xff0c;其中docker是运行在linux平台上的&#xff0c;如果您想在windows上运行docker&#xff0c;请自行搜索如何在windows上运行docker 一、Windows IIS部署 软件版本Win…

MySQL-Linux版安装

MySQL-Linux版安装 1、准备一台Linux服务器 云服务器或者虚拟机都可以&#xff1b; Linux的版本为 CentOS7&#xff1b; 2、 下载Linux版MySQL安装包 下载地址 3、上传MySQL安装包 使用FinalShell软件上传即可&#xff01; 4、 创建目录,并解压 mkdir mysqltar -xvf my…

【SpringCloud学习笔记】zuul网关

【SpringCloud学习笔记】 为什么需要网关zuul网关搭建zuul网关服务网关过滤器 为什么需要网关 微服务项目一般有多个服务&#xff0c;每个服务的地址都不同&#xff0c;客户端如果直接访问服务&#xff0c;无疑是增加客户端开发难度&#xff0c;项目小还好&#xff0c;如果项目…

【图像任务】Transformer系列.1

介绍几篇改进Transformer模型实现亮度增强、图像重建的任务&#xff1a;LLFormer&#xff08;AAAI2023&#xff09;&#xff0c;DLSN&#xff08;TPAMI2023&#xff09;&#xff0c;CAT&#xff08;NeurIPS2022&#xff09;。 Ultra-High-Definition Low-Light Image Enhanceme…

Linux | 进程控制

啊我摔倒了..有没有人扶我起来学习.... &#x1f471;个人主页&#xff1a; 《 C G o d 的个人主页》 \color{Darkorange}{《CGod的个人主页》} 《CGod的个人主页》交个朋友叭~ &#x1f492;个人社区&#xff1a; 《编程成神技术交流社区》 \color{Darkorange}{《编程成神技术…

Redis的内存策略

过期Key处理: 1)Redis之所以性能强大&#xff0c;最主要的原因就是基于内存来存储&#xff0c;然而单节点的Redis内存不宜设置的过大&#xff0c;否则会影响持久化或者是主从复制的性能&#xff0c;可以通过修改配置文件来设置redis的最大内存&#xff0c;通过maxmemory 1gb&am…

javaScript蓝桥杯-----粒粒皆辛苦

目录 一、介绍二、准备三、目标四、代码五、完成 一、介绍 俗话说“民以食为天”&#xff0c;粮食的收成直接影响着民生问题&#xff0c;通过对农作物产量的统计数据也能分析出诸多实际问题。 接下来就让我们使用 ECharts 图表&#xff0c;完成 X 市近五年来的农作物产量的统…

Python批量下载参考文献|基于Python的Sci-Hub下载脚本|Python批量下载sci-hub文献|如何使用sci-hub批量下载论文

本篇博文将介绍如何通过Python的代码实现快速下载指定DOI号对应的文献&#xff0c;并且使用Sci-Hub作为下载库。 一、库函数准备 在开始之前&#xff0c;我们需要先安装一些必要的库&#xff0c;包括&#xff1a; requests&#xff1a;发送HTTP请求并获取响应的库&#xff1…