【设计】【Redis】分布式限流与算法实现

news2025/1/20 3:54:17

在这里插入图片描述

目录

  • 前言
  • 实现
    • application.properties
    • config.RedisConfig
    • MainApplication
    • controller.TrafficLimitController
    • aop.AccessLimiterAspect
    • aop.annotation.AccessLimiter
  • 项目结构
  • 运行
  • 限流脚本
    • 计数器
    • 滑动窗口
    • 令牌桶
    • 漏桶
  • 参考资料

前言

服务的某些场景可能会出现短时间内的巨大访问流量,比如某宝在某个日子可能会有数倍于平时的高峰访问流量,导致接口超时异常,甚至服务被压垮,还可能会导致系统其它服务发生故障,造成服务雪崩。

我们如何让系统保证高并发,同时还能保证稳定性?

加机器吗?

硬件资源不是无限的。

为了避免极端情况,我们不得不在后端服务中采取保护措施:缓存、异步、降级、限流……

分布式限流是一种将限流机制分布在不同服务器或不同网络节点上的技术,它可以实现高效地限制请求流量,对高并发访问进行限速或者对一段时间内的请求进行限速,来保护系统,一旦达到限速规则,则可以采用一定的方式来处理这些请求(视具体业务而定),如:拒绝服务(友好提示或者跳转到错误页面),排队或等待(比如秒杀系统),服务降级(返回默认的兜底数据)。保证系统的稳定性和可靠性。它可以帮助系统避免过度负载,防止系统崩溃或者性能下降。

实现

application.properties

application.properties

server.port=8080

## if savebatch, could try rewriteBatchedStatements=true setting postgres/123456
spring.datasource.platform=postgres
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/postgres?stringtype=unspecified
spring.datasource.username=postgres
spring.datasource.password=weiyuzeng0827+
spring.datasource.driver-class-name=org.postgresql.Driver
#spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect

# mybatis-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0

# redis
spring.redis.host = 10.86.9.118
spring.redis.password =
spring.redis.port = 6379

# thread pool config
#spring.threadpool.corePoolSize=14
#spring.threadpool.maxPoolSize=60
#spring.threadpool.queueCapacity=5000
#spring.threadpool.keepAliveSeconds=600
#spring.threadpool.threadNamePrefix=async-thread-pool-thread

config.RedisConfig

RedisConfig

package org.example.config;


import cn.hutool.core.util.StrUtil;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.connection.balancer.RandomLoadBalancer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Configuration
//@PropertySource({"classpath:redis.properties"})
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedissonClient getRedissionClient() {

        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port);
        if (StrUtil.isNotBlank(password)) {
            config.useClusterServers().setPassword(password);
        }
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}



MainApplication

MainApplication

package org.example;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;

/**
 * @author z
 */
@EnableAsync
@SpringBootApplication(scanBasePackages = "org.example")
@MapperScan("org.example.dao.mapper")
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }
}

controller.TrafficLimitController

TrafficLimitController

package org.example.controller;


import cn.hutool.core.util.ObjectUtil;
import org.example.aop.annotation.AccessLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;

/**
 *
 * @author zengweiyu
 * @since 2022-05-09
 */
@RestController
@RequestMapping("/traffic-limit-algorithm")
public class TrafficLimitController {

    private static CopyOnWriteArrayList<AtomicInteger> listCache = new CopyOnWriteArrayList<>();

    private static AtomicInteger queryOrderRecort = new AtomicInteger(0);

    @AccessLimiter
    @GetMapping("/hello")
    public String hello() {
        return "hello web!";
    }


    @PostMapping("/query")
    @AccessLimiter(name = "限流测试", limit = 300, expireTime = 10)
    public String trafficLimitTest() {
        // 操作数递增
        if (!ObjectUtil.isEmpty(queryOrderRecort)) {
            queryOrderRecort.incrementAndGet();
        }

        listCache.add(new AtomicInteger(queryOrderRecort.get()));

        System.out.println(listCache);
        return listCache.toString();
    }

}



RedisClientUtil

```java
package org.example.utils;


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.redisson.api.RScript;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;

@Component
public class RedisClientUtil {

    private static final Logger logger = LogManager.getLogger(RedisClientUtil.class);


    @Autowired
    private RedissonClient redissonClient;


    private static RedisClientUtil redisClientUtil;

    @PostConstruct
    private void init() {
        redisClientUtil = this;
        redisClientUtil.redissonClient = this.redissonClient;
    }

    /**
     * @desc 计数器,ip限流
     * @param key redis key
     * @param numLimit 单位时间间隔内的请求限制数
     * @param expireTime 请求限制的单位时间间隔
     * @author pankaixin
     * @return
     */
    public static Boolean isExceedIpCountLimit(String key,  Integer numLimit, Integer expireTime) {

        RScript rScript = redisClientUtil.redissonClient.getScript(StringCodec.INSTANCE);
        List<Object> keys = new ArrayList<>(1);
        keys.add(key);

        // 直接获得参数key和expire_time,tonumber转为数字得到参数limit
        // lua调用redis:redis.call(command, key, params),command是调用redis的命令,key是调用命令使用的key,params是给key的参数
        // redis.call('EXISTS', key)是判断key在redis中是否存在(EXISTS)
        // redis.call('INCR', key):对存储在指定key的数值执行原子的加1操作。
        // redis.call('SET', key, 1):对存储在key的数值设为1
        // redis.call('EXPIRE', key, expire_time):对存储在key的字段设定expire_time的过期时间
        String strScript = "local key = KEYS[1] " +
                "local limit = tonumber(ARGV[1]) " +
                "local expire_time = ARGV[2] " +
                "local is_exists = redis.call('EXISTS', key) " +
                "if is_exists == 1 then " +
                "    if redis.call('INCR', key) > limit then " +
                "        return false " +
                "    else " +
                "        return true " +
                "    end " +
                "else " +
                "    redis.call('SET', key, 1) " +
                "    redis.call('EXPIRE', key, expire_time) " +
                "    return true " +
                "end ";
        // 执行lua脚本
        Boolean bResult = rScript.eval(RScript.Mode.READ_WRITE, strScript, RScript.ReturnType.BOOLEAN, keys, numLimit, expireTime);
        return !bResult;

    }

}

aop.AccessLimiterAspect

AccessLimiterAspect

package org.example.aop;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.example.aop.annotation.AccessLimiter;
import org.example.common.RedisConstants;
import org.example.utils.RedisClientUtil;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.List;

/**
 * 限流切面
 * @author z
 */
@Component
@Aspect
@Order(3)
public class AccessLimiterAspect {

    private static final Logger logger = LogManager.getLogger(AccessLimiterAspect.class);

    /**
     * 请求ip
     */
    private static final String IP = "ip";


    /**
     * 切入点,创建的注解类
     */
    @Pointcut("@annotation(org.example.aop.annotation.AccessLimiter)")
    public void limiterPointcut() {
    }


    @Before("limiterPointcut()")
    public void limiter(JoinPoint joinPoint) {
        // 1:获取方法的签名作为key,通过签名获取目标方法信息
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
//        String classname = methodSignature.getMethod().getDeclaringClass().getName();

        // 4: 读取方法的注解信息获取限流参数
        AccessLimiter annotation = method.getAnnotation(AccessLimiter.class);

        // 5:获取注解方法名
        String methodNameKey = method.getName();

        // 6:获取当前服务请求的对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
//        HttpServletResponse response = requestAttributes.getResponse();
        String servletPath = request.getServletPath();
        String userIp = request.getHeader(IP);

        // 7:通过方法反射获取注解的参数
        Integer limit = annotation.limit();
        Integer expireTime = annotation.expireTime();
        String accessName = annotation.name();
        String redisKey = RedisConstants.REDIS_CACHE_KEY_IP_URL_ACCESS_LIMIT_KEY + ":" + servletPath + ":" + userIp;

        // 8: 执行lua脚本,每次访问会在redis中存储在key的数值计数加一次,过期时间设置为expireTime,一旦过期时间内超过计数器超过limit,
        // 则返回False,以此达到限流的效果
        Boolean isExceedIpCountLimit = RedisClientUtil.isExceedIpCountLimit(redisKey, limit, expireTime);

        // 超过请求限制
        if (isExceedIpCountLimit) {
            logger.error(" [{}] 接口 {}: 达到限流, ip is:{}, servletPath is:{}, numLimit is:{}, expireTime is:{}", accessName, methodNameKey, userIp, servletPath, limit, expireTime);
            throw new RuntimeException("exceed access traffic limit error");
        }
        logger.info(" [{}] 接口 {}: 未达到限流.......ip is:{}, servletPath is:{}, numLimit is:{}, expireTime is:{}", accessName, methodNameKey, userIp, servletPath, limit, expireTime);
    }
}


aop.annotation.AccessLimiter

AccessLimiter

package org.example.aop.annotation;

import java.lang.annotation.*;

/**
 * 限流接口
 * @author z
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiter {

    /**
     * 资源名称,用于描述接口功能
     */
    String name() default "";

    /**
     * 每expireTime限制请求的个数
     */
    int limit() default 300;

    /**
     * 时间,单位默认是秒
     */
    int expireTime() default 10;
}

项目结构

项目结构如下:
在这里插入图片描述

运行

运行项目
在这里插入图片描述
创建一个jmeter 脚本:

配置如下

在这里插入图片描述
我们限制在10秒内最多允许300个请求,因此并发线程设置301个,故意超过上限。

protocol:http,IP:localhost,port:8080,POST,path:/traffic-limit-algorithm/query
在这里插入图片描述
在这里插入图片描述
运行线程组:

查看控制台:
在这里插入图片描述

可以看到成功拦截请求。

限流脚本

记录了一些Lua限流脚本的写法,仅供参考,不能直接套用运行

计数器

-- 获取调用脚本时传入的第一个key值(用作限流的 key)
local key = KEYS[1]
-- 获取调用脚本时传入的第一个参数值(限流大小)
local limit = tonumber(ARGV[1])
-- 获取调用脚本时传入的第二个参数值(限流时长)
local time = tonumber(ARGV[2])

-- 获取当前流量大小
local curentLimit = tonumber(redis.call('get', key) or "0")

-- 是否超出限流
if curentLimit + 1 > limit then
    -- 返回(拒绝)
    return 0
else
    -- 没有超出 value + 1
    redis.call("INCRBY", key, 1)
    -- 设置过期时间
    redis.call("EXPIRE", key, time)
    -- 返回(放行)
    return 1
end

local key = KEYS[1]  --限流KEY(一秒一个)
local limit = tonumber(ARGV[1]) --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
    return 0
else --请求数+1,并设置2秒过期
    redis.call("INCRBY", key, "1")
    redis.call("expire", key "2")
    return 1
end

local key = KEYS[1]
-- 如果这个key此前不存在则返回-1
-- 关于 or 的写法参考下面图片的截图
local requests = tonumber(redis.call('GET', key) or '-1')
-- 在固定时间范围内允许的最大请求数,例如这里应该是3
local max_requests = tonumber(ARGV[1])
-- 通常是固定窗口的大小,例如60s
local expiry = tonumber(ARGV[2])
-- 当该窗口的key不存在或者未达到最大请求时
if (requests == -1) or (requests < max_requests) then
  -- 自增
  redis.call('INCR', key)
  -- 重新设置该key的过期日期为60s后
  redis.call('EXPIRE', key, expiry)
  return false
else
  return true
end

滑动窗口


local key = KEYS[1]
-- 当前时间戳
local current_time = tonumber(ARGV[1])
-- 窗口大小,本例中是60 * 1000
local window_size = tonumber(ARGV[3])
-- 本例中是3
local max_requests = tonumber(ARGV[4])
-- 根据当前时间毫秒数 - 超时毫秒数,得到过期时间 expired
local has_expired = current_time - window_size
-- 清除过期的数据
redis.call('ZREMRANGEBYSCORE', key, 0, has_expired)
-- 获取 zset 中的当前元素个数
local current_num = tonumber(redis.call('ZCARD', key))
local next = current_num + 1
-- 达到限流大小 返回 0
if next > max_requests then
  return 0;
else
  -- 往 zset 中添加一个值、得分均为当前时间戳的元素,[value,score]
  redis.call("ZADD", key, current_time, current_time)
  -- 每次访问均重新设置 zset 的过期时间
  redis.call("PEXPIRE", key, window_size)
  return next
end

String lock_stock_lua2 =    "local c\n" +
                            "redis.call('zremrangebyscore',KEYS[1],0,ARGV[3])\n" +
                            "c = redis.call('zcard',KEYS[1])\n" +
                            "if c and tonumber(c) > tonumber(ARGV[4]) then\n" +
                            "    return c;\n" +
                            "end\n" +
                            "redis.call('zadd',KEYS[1],ARGV[2],ARGV[1])\n" +
                            "return c + 1";

令牌桶

脚本1

-- 当前时间戳
local ts = tonumber(ARGV[1])
-- 设置窗口大小为1s
local min = ts -1
-- 可以为多个key设置添加令牌的速率
for i,key in pairs(KEYS) do
    -- 移除过期的的令牌
    redis.call('ZREMRANGEBYSCORE', key, '-inf', min)
    redis.call('ZADD', key, ts, ts)
    redis.call('EXPIRE', key, 10)
end


脚本2

-- 当前时间戳
local ts  = tonumber(ARGV[1])
local key = KEYS[1]
local min = ts -1
-- 移除过期的的令牌
redis.call('ZREMRANGEBYSCORE', key, '-inf', min)
-- 只需要计算令牌桶的大小即可,如果令牌桶是有值的,则放行
return redis.call('ZCARD', key)

-- 令牌桶
local bucketKey = KEYS[1]
-- 上次请求的时间key
local last_request_time_key = 'lastRequestTime'
-- 令牌桶的容量
local capacity = tonumber(ARGV[1])
-- 请求令牌的数量
local permits = tonumber(ARGV[2])
-- 令牌流入的速率(按毫秒计算)
local rate = tonumber(ARGV[3])
-- 当前时间(毫秒)
local current_time = tonumber(ARGV[4])
-- 唯一标识
local unique_identifier = bucketKey

-- 恶意请求
if permits <= 0 then
    return 1
end


-- 获取当前桶内令牌的数量
local current_limit = tonumber(redis.call('HGET', unique_identifier, bucketKey) or '0')
-- 获取上次请求的时间
local last_mill_request_time = tonumber(redis.call('HGET', unique_identifier, last_request_time_key) or '0')
-- 计算向桶里添加令牌的数量
local add_token_num = 0
if last_mill_request_time == 0 then
   -- 如果是第一次请求,则进行初始化令牌桶,并且更新上次请求时间
   add_token_num = capacity
   redis.call("HSET", unique_identifier, last_request_time_key, current_time)
else
    -- 令牌流入桶内
   add_token_num = math.floor((current_time - last_mill_request_time) * rate)
end

-- 更新令牌的数量
if current_limit + add_token_num > capacity then
    current_limit = capacity
else
   current_limit = current_limit + add_token_num
end
-- 更新桶内令牌的数量
redis.pcall('HSET',unique_identifier, bucketKey, current_limit)

-- 限流判断
if current_limit - permits < 0 then
    -- 达到限流大小
    return 0
else
    -- 没有达到限流大小
   current_limit = current_limit - permits
   redis.pcall('HSET', unique_identifier, bucketKey, current_limit)
   -- 更新上次请求的时间
   redis.call('HSET', unique_identifier, last_request_time_key, current_time)
   return 1
end

漏桶

local ts  = tonumber(ARGV[1])
-- calls per seconds,例如每秒4次,也就是250ms内允许发起1次请求
local cps = tonumber(ARGV[2])
local key = KEYS[1]
local min = ts -1
redis.call('ZREMRANGEBYSCORE', key, '-inf', min)
local last = redis.call('ZRANGE', key, -1, -1)
local next = ts
if type(last) == 'table' and #last > 0 then
  for key,value in pairs(last) do
    -- 最后一个元素 + 固定速率的时间
    next = tonumber(value) + 1/cps
    break
  end
end
if ts > next then
  -- the current ts is > than last+1/cps
  -- we'll keep ts
  next = ts
end
-- 如果ts < next,这里next还是现有zset的最后一个元素 + 1/cps 后的时间
redis.call('ZADD', key, next, next)
-- 必须等待的时间,
-- 如果是ts > next,则这里直接返回0,意味着不等待,直接调用
-- 如果是ts < next, 则说明下一个速率还没达到,则需要等到, next - ts这么长时间
return tostring(next - ts)

参考资料

https://bbs.huaweicloud.com/blogs/329392

https://bbs.huaweicloud.com/blogs/329586

https://juejin.cn/post/7168077279531106318

https://blog.csdn.net/dghkgjlh/article/details/128504766

https://blog.csdn.net/truelove12358/article/details/127751693

https://blog.csdn.net/weixin_44991304/article/details/126527087

https://blog.csdn.net/l688899886/article/details/126131180

https://blog.csdn.net/qq_43638685/article/details/121653457

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

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

相关文章

【C语言进阶:动态内存管理】柔性数组

本节重点内容&#xff1a; 柔性数组的特点柔性数组的使用柔性数组的优势 ⚡柔性数组 也许你从来没有听说过柔性数组&#xff08;flexible array&#xff09;这个概念&#xff0c;但是它确实是存在的。C99 中&#xff0c;结构中的最后一个元素允许是未知大小的数组&#xff0c…

java+ssm 社区超市网上商城果蔬(水果蔬菜)管理系统

在Internet高速发展的今天&#xff0c;我们生活的各个领域都涉及到计算机的应用&#xff0c;其中包括超市果蔬管理系统的网络应用&#xff0c;在外国超市果蔬管理系统已经是很普遍的方式&#xff0c;不过国内的超市果蔬管理系统可能还处于起步阶段。超市果蔬管理系统具有果蔬管…

Flutter系列(八)搜索框详解

底部导航顶部导航图片列表的完整代码 Flutter系列&#xff08;四&#xff09;底部导航顶部导航图文列表完整代码_摸金青年v的博客-CSDN博客 目录 一、前言 二、搜索框实现方案 三、完整工程代码 1. 自定义的搜索框组件SearchAppBar search.dart 2. 搜索详情页 search…

Java岗五面阿里,终拿offer,原来面试官总喜欢问这些问题

一面 一面就做了一道算法题&#xff0c;要求两小时内完成&#xff0c;给了长度为N的有重复元素的数组&#xff0c;要求输出第10大的数。典型的TopK问题&#xff0c;快排算法搞定。 算法题要注意的是合法性校验、边界条件以及异常的处理。另外&#xff0c;如果要写测试用例&am…

v1.5宝可梦综合耐久最大化计算器

版本更新v1.5 链接&#xff1a;https://pan.baidu.com/s/1JseHNYnAqIuSpg5f3k6Hbw?pwd01gy 提取码&#xff1a;01gy 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 更新说明 1.增加了特性威吓、灾祸之鼎、灾祸之简 使用说明 1.种族值&#xff1a;查百科获取…

Jmeter5.1.1报错:java.net.BindException: Address already in use: connect

Jmeter5.1.1报错&#xff1a;java.net.BindException: Address already in use: connect 原因&#xff1a;从网上找到资料&#xff1a;端口占用 Windows提供给TCP/IP链接的端口为 1024-5000&#xff0c;并且要四分钟来循环回收它们&#xff0c;就导致我们在短时间内跑大量的请…

【数据库】学习数据库该了解的基本知识

前言 在学习数据库之前&#xff0c;我们先要明确&#xff0c;sql是编程语言&#xff1b;Oracle&#xff0c;Mysql&#xff0c;SQL Server这些是数据库软件&#xff0c;sql这个语言是运行在数据库软件上的&#xff08;就像Java运行在jvm上一样&#xff09; 1、常见的关系型数据库…

HAProxy的安装、详细配置与实际应用(MyCAT、RabbitMQ示例)

HAProxy的安装、配置与实际应用 HAProxyHAProxy概述下载编译安装配置启动验证配置RabbitMQ的HAProxy示例 HAProxy HAProxy概述 HAProxy&#xff08;High Availability Proxy&#xff09;是一款自由、快速、可靠的TCP/HTTP负载均衡软件&#xff0c;其最常见的用途是将客户端请求…

使用signapk工具给apk系统签名

使用signapk给apk系统签名&#xff1a; 1、准备signapk.jar文件 查找路径&#xff1a; .\out\host\linux-x86\framework\signapk.jar 2、platform.x509.pem 和 platform.pk8 查找路径&#xff1a; .\vendor\prima\customer\certificatekey\prima 3、重点&#xff1a;将sdk…

Mac的PATH环境变量及相关文件加载顺序详细解释

系统级变量 /etc/profile /etc/paths 用户级变量(前3个按照从前往后的顺序读取&#xff0c;如果~/.bash_profile文件存在&#xff0c;则后面的几个文件就会被忽略不读了&#xff0c;如果~/.bash_profile文件不存在&#xff0c;才会以此类推读取后面的文件。~/.bashrc没有上述…

数据结构 | 排序 - 总结

排序的方式 排序的稳定性 什么是排序的稳定性&#xff1f; 不改变相同数据的相对顺序 排序的稳定性有什么意义&#xff1f; 假定一个场景&#xff1a; 一组成绩&#xff1a;100&#xff0c;88&#xff0c;98&#xff0c;98&#xff0c;78&#xff0c;100&#xff08;按交卷顺序…

【数据结构】队列(循环队列和链队列)详细讲解各种操作

&#x1f38a;专栏【数据结构】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【勋章】 大一同学小吉&#xff0c;欢迎并且感谢大家指出我的问题&#x1f970; 图片来源网络&#xff0c;如果侵权&#xff0c;请联系我 目录 ⭐队…

爱智EdgerOS之深入解析离线下载任务

一、需求分析 在日常使用计算机的过程中&#xff0c;看到喜欢的资源不可避免地想把它下载到我们的设备上保存下来&#xff0c;比如图片&#xff0c;音视频资源&#xff0c;文档资源等&#xff0c;基于这种应用场景&#xff0c;现在来看看在爱智设备上可以如何实现呢&#xff1…

日撸 Java 三百行day33

文章目录 说明day33 图的广度优先遍历1.思路2.多个连通分量2 代码实现 说明 闵老师的文章链接&#xff1a; 日撸 Java 三百行&#xff08;总述&#xff09;_minfanphd的博客-CSDN博客 自己也把手敲的代码放在了github上维护&#xff1a;https://github.com/fulisha-ok/sampled…

82.qt qml-2D粒子系统、粒子方向、粒子项(一)

由于粒子系统相关的类比较多, 所以本章参考自QmlBook in chinese的粒子章节配合学习: 由于QmlBook in chinese翻译过来的文字有些比较难理解,所以本章在它的基础上做些个人理解,建议学习的小伙伴最好配合QmlBook in chinese一起学习。 1.介绍 粒子模拟的核心是粒子系统(Partic…

ResNet残差网络

ResNet 目的 Resnet网络是为了解决深度网络中的退化问题&#xff0c;即网络层数越深时&#xff0c;在数据集上表现的性能却越差。 原理 ResNet的单元结构如下&#xff1a; 类似动态规划的选择性继承&#xff0c;同时会在训练过程中逐渐增大&#xff08;/缩小&#xff09;该…

数字图像基础【7】应用线性回归最小二乘法(矩阵版本)求解几何变换(仿射、透视)

这一章主要讲图像几何变换模型&#xff0c;可能很多同学会想几何变换还不简单嚒&#xff1f;平移缩放旋转。在传统的或者说在同一维度上的基础变换确实是这三个&#xff0c;但是今天学习的是2d图像转投到3d拼接的基础变换过程。总共包含五个变换——平移、刚性、相似、仿射、透…

尚融宝10-Excel数据批量导入

目录 一、数据字典 &#xff08;一&#xff09;、什么是数据字典 &#xff08;二&#xff09;、数据字典的设计 二、Excel数据批量导入 &#xff08;一&#xff09;后端接口 1、添加依赖 2、创建Excel实体类 3、创建监听器 4、Mapper层批量插入 5、Service层创建监听…

2023年,想要靠做软件测试获得高薪,我还有机会吗?

时间过得很快&#xff0c;一眨眼&#xff0c;马上就要进入2023年了&#xff0c;到了年底&#xff0c;最近后台不免又出现了经常被同学问道这几个问题&#xff1a;2023年还能转行软件测试吗&#xff1f;零基础转行可行吗&#xff1f; 本期小编就“2023年&#xff0c;入行软件测…

一文解决nltk安装问题ModuleNotFoundError: No module named ‘nltk‘,保姆级教程

目录 问题一&#xff1a;No module named ‘nltk‘ 问题二&#xff1a;Please use the NLTK Downloader to obtain the resource 下载科学上网工具 问题三&#xff1a;套娃报错 如果会科学上网&#xff0c;可以直接看问题三 问题一&#xff1a;No module named ‘nltk‘ Mo…