redis-实现限流

news2024/11/20 19:30:46

1、 主流的四种限流策略,我都可以通过redis实现

引言
在web开发中功能是基石,除了功能以外运维和防护就是重头戏了。因为在网站运行期间可能会因为突然的访问量导致业务异常、也有可能遭受别人恶意攻击
所以我们的接口需要对流量进行限制。俗称的QPS也是对流量的一种描述
针对限流现在大多应该是令牌桶算法,因为它能保证更多的吞吐量。除了令牌桶算法还有他的前身漏桶算法和简单的计数算法
下面我们来看看这四种算法

2 固定时间窗口算法

2.1 说明

固定时间窗口算法也可以叫做简单计数算法。网上有很多都将计数算法单独抽离出来。但是笔者认为计数算法是一种思想,而固定时间窗口算法是他的一种实现
包括下面滑动时间窗口算法也是计数算法的一种实现。因为计数如果不和时间进行绑定的话那么失去了限流的本质了。就变成了拒绝了

在这里插入图片描述

优点
在固定的时间内出现流量溢出可以立即做出限流。每个时间窗口不会相互影响
在时间单元内保障系统的稳定。保障的时间单元内系统的吞吐量上限
缺点
正如图示一样,他的最大问题就是临界状态。在临界状态最坏情况会受到两倍流量请求
除了临界的情况,还有一种是在一个单元时间窗内前期如果很快地消耗完请求阈值。那么剩下的时间将会无法请求。这样就会因为一瞬间的流量导致一段时间内系统不可用。这在互联网高可用的系统中是不能接受的。
实现
好了,关于原理介绍及优缺点我们已经了解了。下面我们动手实现它
首先我们在实现这种计数时,采用redis是非常好的选择。这里我们通过redis实现

controller

@RequestMapping(value = "/start",method = RequestMethod.GET)
    public Map<String,Object> start(@RequestParam Map<String, Object> paramMap) {
        return testService.startQps(paramMap);
    }

service
@Override
public Map<String, Object> startQps(Map<String, Object> paramMap) {
//根据前端传递的qps上线
Integer times = 100;
if (paramMap.containsKey(“times”)) {
times = Integer.valueOf(paramMap.get(“times”).toString());
}
String redisKey = “redisQps”;
RedisAtomicInteger redisAtomicInteger = new RedisAtomicInteger(redisKey, redisTemplate.getConnectionFactory());
int no = redisAtomicInteger.getAndIncrement();
//设置时间固定时间窗口长度 1S
if (no == 0) {
redisAtomicInteger.expire(1, TimeUnit.SECONDS);
}
//判断是否超限 time=2 表示qps=3
if (no > times) {
throw new RuntimeException(“qps refuse request”);
}
//返回成功告知
Map<String, Object> map = new HashMap<>();
map.put(“success”, “success”);
return map;
}

2.2 lua实现原子性

pom.xml

<!-- spring boot redis缓存引入 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 缓存连接池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- redis 存储 json序列化 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

配置一下redis配置信息

redis:
  host: 127.0.0.1
  port: 6379
  database: 0
  password: 123456 #默认为空
  timeout: 3000ms #最大等待时间,超时则抛出异常,否则请求一直等待
  lettuce:
    pool:
      max-active: 20  #最大连接数,负值表示没有限制,默认8
      max-wait: -1    #最大阻塞等待时间,负值表示没限制,默认-1
      max-idle: 8     #最大空闲连接,默认8
      min-idle: 0     #最小空闲连接,默认0

整理一下思路
首先我们需要对接口做限流,那怎么限流呢?什么时候限流?

第一个问题怎么限流?
当我们接口被恶意调用的时候通常情况下有两种一种是我们对这个接口做全局限制,就是默认所有人在单位时间内只能调用多少次,第二种是对调用者的IP地址做限制

第二个问题什么时候限流?
当我们在执行这个方法之前就应该先判断是否限流,所以我们可以采用AOP+注解模式
所以我们现在就是要用注解+AOP对接口做两种模式限流:全局和IP地址

创建一个枚举类 来区别两种限流模式

public enum LimitType {
    /**
     * 默认策略全局限流
     */
    DEFAULT,
    /**
     * 根据请求者IP进行限流
     */
    IP
}

.我们创建一个注解,通过AOP去解析这个注解获取信息做出对应的判断

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
    /**
     * 限流key 这个key只是我们存入redis中的键值的前缀
     */
    String key() default "rate_limit:";

    /**
     * 限流时间,单位秒
     */
    int time() default 60;

    /** 
     * 限流次数
     */
    int count() default 100;

    /**
     * 限流类型
     */
    LimitType limitType() default LimitType.DEFAULT;
}

配置RedisTemplate 创建一个RedisConfig 配置RedisTemplate (类上记得加@Configuration注解)

@Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        //首先解决key的序列化方式
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);

        //解决value的序列化方式
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

        //序列化时将类的数据类型存入json,以便反序列化的时候转换成正确的类型
        ObjectMapper objectMapper = new ObjectMapper();
        //objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);

        // 解决jackson2无法反序列化LocalDateTime的问题
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.registerModule(new JavaTimeModule());

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);


        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }

接下来我们想一下怎么对redis操作的思路
首先当我们第一次调用这个接口的时候应该怎么做?
我们应该去先判断当前接口的具体参数(调用限制次数,限制时间)而这些东西都是应该存在redis里的,当我们对接口发起调用的时候,我们用一个设置唯一key去存并将它的值设置为自增,过期时间设定为限制时间。那么问题来了

我们应该如何去保证他们的操作原子性呢?
我们可以使用LUA脚本去实现这些操作,因为redis是单线程,所以在执行lua脚本的时候,会把脚本里面的所有命令当成一个命令执行,这样保证了redis的原子性。

在这里插入图片描述

创建一个LUA脚本放在resource目录下
–获取传递进来的KEY值
local key = KEYS[1]
–获取限制的次数
local count = tonumber(ARGV[1])
–获取限制的时间,设置为过期时间
local time = tonumber(ARGV[2])
–查询key是否存在并获取值 如果不存在返回nil
local current = redis.call(‘get’, key)
–如果key存在并且大于传入的限制次数 我们直接返回当前次数 不执行后续代码
if current and tonumber(current) > count then
return tonumber(current)
end
–走到这里代表次数没有超出限制 或者是第一次创建获取到的返回值为nil 所以我们给他自增1
current = redis.call(‘incr’, key)
–如果current==1 代表是第一次创建 因为redis.call(‘incr’, key)方法会对不存在key自动创建并赋值为0再自增返回1
if tonumber(current) == 1 then
–所以是第一次创建我们给他设置过期时间
redis.call(‘expire’, key, time)
end
–最后返回次数
return tonumber(current)

然后我们在一个Bean对象里面加载这个脚本


```javascript

```java
@Bean
    public DefaultRedisScript<Long> limitScript() {
    //执行 Redis Lua 脚本的一种方式
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    //将lua脚本加载ClassPathResource中
    ClassPathResource resource = new ClassPathResource("lua/limit.lua");
    //将脚本加载容器中方便后续执行
    ResourceScriptSource resourceScriptSource = new ResourceScriptSource(resource);
    redisScript.setScriptSource(resourceScriptSource);
    redisScript.setResultType(Long.class);
    return redisScript;
}

可以把这个Bean放在redis配置类中

注解解析 定义切面
创建一个

package com.maroon.Aspect;

import com.maroon.enums.LimitType;
import com.maroon.exception.BusinessException;
import com.maroon.interfaces.RateLimiter;
import com.maroon.util.IPUtil;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;

@Aspect
@Component
@Slf4j
public class RateLimiterAspect {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private RedisScript<Long> limitScript;

    /**
     * @param point
     * @param rateLimiter
     * @throws Throwable
     */
    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        //从注解中获取 key time count
        String key = rateLimiter.key();
        int time = rateLimiter.time();
        int count = rateLimiter.count();
        //返回一个由 key:IP地址和切点方法的全路径加方法名的String
        // 类似rate_limit:127.0.0.1-com.maroon.service.impl.SmsServiceImpl-sendCode
        // (如果不是 IP 模式,那么生成的 key 中就不包含 IP 地址)
        String combineKey = getCombineKey(rateLimiter, point);
        //Collections.singletonList 是 Java 中的一个静态方法,用于创建一个只包含单个元素的不可变 List 集合。
        // 此方法接受一个参数,即要放入 List 中的元素,然后返回一个不可变 List,包含指定的元素这个集合是不可修改的没有
        List<String> keys = Collections.singletonList(combineKey);
        Long number;
        try {
            Object[] o = {count, time};
            //将键名和
            number = redisTemplate.execute(limitScript, keys, o);
            if (number == null || number.intValue() > count) {
                throw new BusinessException("请求繁忙");
            }
            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
        } catch (BusinessException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("服务器限流异常,请稍候再试");
        }
    }

    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
        if (rateLimiter.limitType() == LimitType.IP) {
            //获取用户IP地址
            stringBuffer.append(IPUtil.getIp()).append("-");
        }
        //获取被通知方法的签名信息 然后就可以获得该方法的名称、返回类型等信息。
        MethodSignature signature = (MethodSignature) point.getSignature();
        //获取切点方法对象
        Method method = signature.getMethod();
        //获取方法全路径名
        Class<?> targetClass = method.getDeclaringClass();
        //将IP地址和方法全路径名和方法名拼在一起返回
        stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
        return stringBuffer.toString();
    }
}
public class IPUtil {
    public static String getIp() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String ip = "unknown";
        if (request != null) {
            ip = request.getHeader("X-Forwarded-For");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
        }

          return ip;
    }
}

3 滑动时间窗口算法

针对固定时间窗口的缺点–临界值出现双倍流量问题。 我们的滑动时间窗口就产生了。
其实很好理解,就是针对固定时间窗口,将时间窗口统计从原来的固定间隔变成更加细度化的单元了。
在上面我们固定时间窗口演示中我们设置的时间单元是1S 。 针对1S我们将1S拆成时间戳。
固定时间窗口是统计单元随着时间的推移不断向后进行。而滑动时间窗口是我们认为的想象出一个时间单元按照相对论的思想将时间固定,我们的抽象时间单元自己移动。抽象的时间单元比实际的时间单元更小。
读者可以看下下面的动图,就可以理解了。

在这里插入图片描述

优点
实质上就是固定时间窗口算法的改进。所以固定时间窗口的缺点就是他的优点。
内部抽象一个滑动的时间窗,将时间更加小化。存在边界的问题更加小。客户感知更弱了。
缺点
不管是固定时间窗口算法还是滑动时间窗口算法,他们都是基于计数器算法进行优化,但是他们对待限流的策略太粗暴了。
为什么说粗暴呢,未限流他们正常放行。一旦达到限流后就会直接拒绝。这样我们会损失一部分请求。这对于一个产品来说不太友好
实现
滑动时间窗口是将时间更加细化,上面我们是通过redis#setnx实现的。这里我们就无法通过他统一记录了。我们应该加上更小的时间单元存储到一个集合汇总。然后根据集合的总量计算限流。redis的zsett数据结构就和符合我们的需求。
为什么选择zset呢,因为redis的zset中除了值以外还有一个权重。会根据这个权重进行排序。如果我们将我们的时间单元及时间戳作为我们的权重,那么我们获取统计的时候只需要按照一个时间戳范围就可以了。
因为zset内元素是唯一的,所以我们的值采用uuid或者雪花算法一类的id生成器
controller

@RequestMapping(value = "/startList",method = RequestMethod.GET)
    public Map<String,Object> startList(@RequestParam Map<String, Object> paramMap) {
        return testService.startList(paramMap);
    }

service

String redisKey = "qpsZset";
        Integer times = 100;
        if (paramMap.containsKey("times")) {
            times = Integer.valueOf(paramMap.get("times").toString());
        }
        long currentTimeMillis = System.currentTimeMillis();
        long interMills = inter * 1000L;
        Long count = redisTemplate.opsForZSet().count(redisKey, currentTimeMillis - interMills, currentTimeMillis);
        if (count > times) {
            throw new RuntimeException("qps refuse request");
        }
        redisTemplate.opsForZSet().add(redisKey, UUID.randomUUID().toString(), currentTimeMillis);
        Map<String, Object> map = new HashMap<>();
        map.put("success", "success");
        return map;

和固定时间窗口采用相同的并发。为什么上面也会出现临界状况呢。因为在代码里时间单元间隔比固定时间间隔采用还要大 。 上面演示固定时间窗口时间单元是1S出现了最坏情况。而滑动时间窗口设计上就应该间隔更短。而我设置成10S 也没有出现坏的情况
这里就说明滑动比固定的优处了。如果我们调更小应该更加不会出现临界问题,不过说到底他还是避免不了临界出现的问题

4 漏桶算法

滑动时间窗口虽然可以极大程度地规避临界值问题,但是始终还是避免不了
另外时间算法还有个致命的问题,他无法面对突如其来的大量流量,因为他在达到限流后直接就拒绝了其他额外流量
针对这个问题我们继续优化我们的限流算法。 漏桶算法应运而生
在这里插入图片描述
优点
面对限流更加的柔性,不再粗暴的拒绝。
增加了接口的接收性
保证下流服务接收的稳定性。均匀下发
缺点
我觉得没有缺点。非要鸡蛋里挑骨头那我只能说漏桶容量是个短板
实现
controller

@RequestMapping(value = "/startLoutong",method = RequestMethod.GET)
public Map<String,Object> startLoutong(@RequestParam Map<String, Object> paramMap) {
    return testService.startLoutong(paramMap);
}

service
在service中我们通过redis的list的功能模拟出桶的效果。这里代码是实验室性质的。在真实使用中我们还需要考虑并发的问题

@Override
public Map<String, Object> startLoutong(Map<String, Object> paramMap) {
    String redisKey = "qpsList";
    Integer times = 100;
    if (paramMap.containsKey("times")) {
        times = Integer.valueOf(paramMap.get("times").toString());
    }
    Long size = redisTemplate.opsForList().size(redisKey);
    if (size >= times) {
        throw new RuntimeException("qps refuse request");
    }
    Long aLong = redisTemplate.opsForList().rightPush(redisKey, paramMap);
    if (aLong > times) {
        //为了防止并发场景。这里添加完成之后也要验证。  即使这样本段代码在高并发也有问题。此处演示作用
        redisTemplate.opsForList().trim(redisKey, 0, times-1);
        throw new RuntimeException("qps refuse request");
    }
    Map<String, Object> map = new HashMap<>();
    map.put("success", "success");
    return map;
}

下游消费
@Component

public class SchedulerTask {
 
    @Autowired
    RedisTemplate redisTemplate;
 
    private String redisKey="qpsList";
 
    @Scheduled(cron="*/1 * * * * ?")
    private void process(){
        //一次性消费两个
        System.out.println("正在消费。。。。。。");
        redisTemplate.opsForList().trim(redisKey, 2, -1);
    }
 
}

测试
我们还是通过50并发循环10次访问。我们可以发现只有在一开始能达到比较高的吞吐量。在随后桶的容量满了之后。而下游水滴速率比上游请求速率慢的情况下。只能以下游恒定的速度接收访问。
他的问题也暴露得很明显。针对时间窗口的不足漏桶进行的不足,但是仍是不足。无法彻底避免请求溢出的问题。
请求溢出本身就是一种灾难性的问题。所有的算法目前都没有解决这个问题。只是在减缓他带来的问题

5、令牌桶算法

令牌桶和漏桶法是一样的。只不过将桶的作用方向改变了一下。
漏桶的出水速度是恒定的,如果流量突然增加的话我们就只能拒绝入池
但是令牌桶是将令牌放入桶中,我们知道正常情况下令牌就是一串字符当桶满了就拒绝令牌的入池,但是面对高流量的时候正常加上我们的超时时间就留下足够长的时间生产及消费令牌了。这样就尽可能地不会造成请求的拒绝
最后,不论是对于令牌桶拿不到令牌被拒绝,还是漏桶的水满了溢出,都是为了保证大部分流量的正常使用,而牺牲掉了少部分流量
public Map<String, Object> startLingpaitong(Map<String, Object> paramMap) {
String redisKey = “lingpaitong”;
String token = redisTemplate.opsForList().leftPop(redisKey).toString();
//正常情况需要验证是否合法,防止篡改
if (StringUtils.isEmpty(token)) {
throw new RuntimeException(“令牌桶拒绝”);
}
Map<String, Object> map = new HashMap<>();
map.put(“success”, “success”);
return map;
}
@Scheduled(cron=“*/1 * * * * ?”)
private void process(){
//一次性生产两个
System.out.println(“正在消费。。。。。。”);
for (int i = 0; i < 2; i++) {
redisTemplate.opsForList().rightPush(redisKey, i);
}
}

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

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

相关文章

finallshell mac SSH工具

一、FinallShell 是什么 FinalShell是一体化的的服务器,网络管理软件,不仅是ssh客户端,还是功能强大的开发,运维工具,充分满足开发,运维需求. 特色功能: 云端同步,免费海外服务器远程桌面加速,ssh加速,本地化命令输入框,支持自动补全,命令历史,自定义命令参数 二、主要特性 …

CMake Practice 学习笔记四---使用动静态库

任务&#xff1a; 编写一个程序使用我们上一届构建的共享库 1、准备工作 在/backup/cmake目录建立t4目录 mkdir t4在t4目录中建立src目录&#xff0c;并编写源文件main.c cd t4 mkdir src && cd src touch main.cmain.c的内容如下&#xff1a; #include <hel…

博客系统后端设计(七) - 实现显示用户信息与注销功能

文章目录 1. 显示用户信息1.1 约定前后端交互接口1.2 修改列表页的前段代码1.3 实现详情页的后端代码1.4 实现详情页的前端代码 2. 注销2.1 确定前后端交互接口2.2 实现后端代码2.3 修改前端代码 1. 显示用户信息 此处的用户名是写死的&#xff0c;我们希望的是此处是能够动态生…

nodejs+vue婚庆服务网站的设计与实现

为了适应现代人类强烈的时间观念&#xff0c;对于用户&#xff0c;因此&#xff0c;这就需要一个互联网平台实现在线婚庆服务网站&#xff0c;正是这么一个方便的平台。本网站中&#xff0c;用户与活动报名可以以最方便的形式&#xff0c;在最短的时间内获悉报名信息&#xff0…

Nginx Web页面缓存 Rsync远程同步

Nginx Web页面缓存 在http块中加配置&#xff1a; proxy_cache_path /data/nginx/cache levels1:2 keys_zonemy_cache:10m max_size10g inactive60m use_temp_pathoff ##################################### path&#xff1a;强制参数&#xff0c;指定缓存文件的存放路径 …

51单片机蓝牙APP自助商品售卖机12864投币找零

实践制作DIY- GC0132-蓝牙APP自助商品售卖机 一、功能说明&#xff1a; 基于51单片机设计-蓝牙APP自助商品售卖机 二、功能介绍&#xff1a; 硬件组成&#xff1a;STC89C52单片机最小系统LCD12864显示蜂鸣器ULN2003步进电机模拟出商品多个按键&#xff08;找零、确认、投…

掌握RDD算子

文章目录 一、准备本地系统文件二、把文件上传到HDFS三、启动HDFS服务四、启动Spark服务五、启动Spark Shell六、映射算子案例任务1、将rdd1每个元素翻倍得到rdd2任务2、将rdd1每个元素平方得到rdd2任务3、利用映射算子打印菱形IDEA里创建项目实现 七、过滤算子案例任务1、过滤…

编写 ROS 服务节点 Service 和 Client(python/C++)(六)

1.编写 Service 节点&#xff08;C&#xff09; 进入目录 cd ~/catkin_ws/src/beginner_tutorials/src然后vim server.cpp 复制代码粘贴&#xff0c;shiftinsert 粘贴 &#xff0c;然后按Esc 键&#xff0c;然后输入:wq 就可以保存退出了 #include "ros/ros.h" …

OSI分层

1 应用层 最上层的&#xff0c;也是我们能直接接触到的就是应用层&#xff08;Application Layer&#xff09;&#xff0c;我们电脑或手机使用的应用软件都是在应用层实现。那么&#xff0c;当两个不同设备的应用需要通信的时候&#xff0c;应用就把应用数据传给下一层&#x…

小航助学信息学奥赛C++ GoC模拟试卷(含题库答题软件账号)

信息学奥赛C GoC系统请点击 电子学会-全国青少年编程等级考试真题Scratch一级&#xff08;2019年3月&#xff09;在线答题_程序猿下山的博客-CSDN博客_小航答题助手 单选题10.0分 删除编辑 答案:C 第1题goc命令可以通过多命令拼接方式&#xff0c;优化代码布局&#xff0c;…

DHTMLX Suite JS PRO 8.1.1 Crack

适用于现代 Web 应用程序的强大 JavaScript 小部件库 - DHTMLX 套件 用于创建现代用户界面的轻量级、快速且通用的 JavaScript/HTML5 UI 小部件库。 DHTMLX Suite 有助于推进 Web 开发和构建具有丰富功能的数据密集型应用程序。 DHTMLX Suite 是一个 UI 小部件库&#xff0c;用…

用于脑MRI分割的注意对称自动编码器

文章目录 Attentive Symmetric Autoencoder for Brain MRI Segmentation摘要本文方法Attentive Reconstruction Loss位置编码SPE下游任务网络结构 实验结果 Attentive Symmetric Autoencoder for Brain MRI Segmentation 摘要 基于图像块重建的自监督学习方法在训练自动编码器…

深度学习基础入门篇[9.1]:卷积之标准卷积:卷积核/特征图/卷积计算、填充、感受视野、多通道输入输出、卷积优势和应用案例讲解

【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化算法、卷积模型、序列模型、预训练模型、对抗神经网络等 专栏详细介绍&#xff1a;【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化…

6个AI绘画网站,可生成海报

目录 1、Midjourney 2、Stable Diffusion Omline 3、Microsoft Designer 4、Craiyon 5、NightCafe S 6、Wombo 1、Midjourney 特点&#xff1a;业内标杆&#xff0c;效果最强大 Midjourney是基于diffusion的AI图画艺术生成器。生成图片不局限于二次元人物&#xff0c;能够…

产品经理在空窗期该做什么?

最近一段时间&#xff0c;就业形势越来越严峻&#xff0c;尤其是互联网行业&#xff0c;尤其是产品经理这个岗位竞争更是激烈&#xff0c;很多产品经理都难免有了数个月的空窗期&#xff0c;而空窗期的存在又使得产品经理竞争力下降&#xff0c;形成了负循环。而唯一能打破这种…

FPGA纯verilog代码实现H265视频压缩 支持4K30帧分辨率 提供工程源码和技术支持

这里写目录标题 1、前言2、我这里已有的视频图像编解码方案3、H265--视频压缩理论4、H265--视频压缩--性能表现5、H265--视频压缩--设计方案6、H265--视频压缩--时序7、Vivado工程详解8、移植上板应用9、Vivado功能仿真10、福利&#xff1a;工程代码的获取 1、前言 H265视频压…

十大排序算法之冒泡排序、快速排序的介绍

个人主页&#xff1a;平行线也会相交 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【数据结构初阶&#xff08;C实现&#xff09;】 目录 冒泡排序冒泡排序代码冒泡排序优化 快速排序快速排序代码 冒泡排序 说起来冒泡排…

推荐8款在win11上还能用的“古董级”软件

前几天花重金1.3个w买了一台新电脑华为一体机MateStation X 2023&#xff0c;用了近10年的华硕迷你电脑终于要下岗了。这么多年来一直在用win7操作系统&#xff0c;现在直接迭代到win11了&#xff0c;以前几个用得称手的工具软件不舍得扔&#xff0c;拷到新电脑上居然还能用&am…

前端之CSS常用选择器分享~

目录 1. 标签选择器 2. 类选择器 3. id选择器 4. 后代选择器 5. 子代选择器 6. 并集选择器 7. 兄弟选择器 1. 标签选择器 ● 基本格式 : 标签名{属性1: ; 属性2: ; 属性3: ;....} ● 示例代码 <body><style>div {width: 100px;height: 100px;background-co…

【微报告】行泊一体低、中、高算力平台,谁能率先突围?

行泊一体是大势所趋&#xff0c;且正分层发展&#xff0c;这是业内已有的共识。但对身处其中的竞争者&#xff0c;更重要且更难的显然为踏准市场节奏&#xff0c;从而用既有的资源取舍布局&#xff0c;最终吃下窗口期爆发红利&#xff0c;在细分赛道中脱颖而出。 高工智能汽车…