《优化接口设计的思路》系列:第七篇—接口限流策略

news2025/2/24 2:26:12

系列文章导航
第一篇—接口参数的一些弯弯绕绕
第二篇—接口用户上下文的设计与实现
第三篇—留下用户调用接口的痕迹
第四篇—接口的权限控制
第五篇—接口发生异常如何统一处理
第六篇—接口防抖(防重复提交)的一些方式
第七篇—接口限流策略

本文参考项目源码地址:summo-springboot-interface-demo

一、前言

大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。

好像一提到防抖,接下来就会提到限流,我在第六篇文章写了一些接口防抖的策略,那么这篇正好讲讲接口如何限流。不知道从哪里看到的,“防抖是回城,限流是攻击”,感觉真的很形象,我来简要描述一下:

王者荣耀大家都玩过吧,里面的英雄都有一个攻击间隔,当我们连续的点击普通攻击的时候,英雄的攻速并不会随着我们点击的越快而更快的攻击。这个就是限流,英雄会按照自身攻速的系数执行攻击,我们点的再快也没用。

而防抖在王者荣耀中就是回城,在游戏中经常会遇到连续回城嘲讽对手的玩家,它们每点击一次回城,后一次的回城都会打断前一次的回城,只有最后一次点击的回城会被触发,从而保证回城只执行一次,这就是防抖的概念。

本文参考项目源码地址:summo-springboot-interface-demo

二、业务场景

1. API速率限制

对外提供的API接口可能需要限制每个用户或每个IP地址在单位时间内的访问次数,以防止滥用或过载。
2. 网站流量控制

对于高流量的网站,为了防止瞬时访问量过大导致服务器压力过重,可以通过限流保护系统稳定运行。
3. 秒杀活动

电商平台在进行秒杀活动时,可能会遭遇大量用户同时抢购,通过计数器限流算法可以有效地控制访问量,避免系统崩溃。
4. 微服务架构

在微服务架构中,限流可以防止某个服务因为突发的高流量而成为瓶颈,进而影响到整个系统的稳定性。
5. 分布式系统的互斥操作

在进行诸如分布式锁的操作时,限流算法可以避免过多的请求同时竞争资源,保证系统的公平性和效率。
6. 基础设施保护

对于数据库、缓存等基础设施服务,通过计数器限流可以避免过多的并发请求导致服务不可用。
7. 网络带宽控制

对于带宽有限的网络服务,限流算法可以用来确保带宽的合理分配,防止网络拥堵。

三、限流策略

1. 计数器

(1)代码

CounterRateLimit.java
package com.summo.demo.config.limitstrategy.counter;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CounterRateLimit {
    /**
     * 请求的数量
     *
     * @return
     */
    int requests();

    /**
     * 时间窗口,单位为秒
     *
     * @return
     */
    int timeWindow();
}
CounterRateLimitAspect.java
package com.summo.demo.config.limitstrategy.counter;

import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import com.summo.demo.exception.biz.BizException;
import com.summo.demo.model.response.ResponseCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
@Order(5)
public class CounterRateLimitAspect {
    /**
     * 用来存储每个方法请求计数的映射
     */
    private final ConcurrentHashMap<String, AtomicInteger> REQUEST_COUNT = new ConcurrentHashMap<>();
    /**
     * 来存储每个方法的时间戳的映射
     */
    private final ConcurrentHashMap<String, Long> TIMESTAMP = new ConcurrentHashMap<>();

    @Around("@annotation(com.summo.demo.config.limitstrategy.counter.CounterRateLimit)")
    public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取注解信息
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        Method method = signature.getMethod();
        CounterRateLimit counterRateLimit = method.getAnnotation(CounterRateLimit.class);

        //获取注解上配置的参数
        int maxRequests = counterRateLimit.requests();
        long windowSizeInMillis = TimeUnit.SECONDS.toMillis(counterRateLimit.timeWindow());

        // 获取方法的字符串表示,用作键值
        String methodName = method.toString();

        // 初始化计数器和时间戳
        AtomicInteger count = REQUEST_COUNT.computeIfAbsent(methodName, k -> new AtomicInteger(0));
        long startTime = TIMESTAMP.computeIfAbsent(methodName, k -> System.currentTimeMillis());

        // 获取当前时间
        long currentTimeMillis = System.currentTimeMillis();
        // 如果当前时间超出时间窗口,则重置计数器和时间戳
        if (currentTimeMillis - startTime > windowSizeInMillis) {
            // 原子地重置时间戳和计数器
            TIMESTAMP.put(methodName, currentTimeMillis);
            count.set(0);
        }

        // 原子地增加计数器并检查其值
        if (count.incrementAndGet() > maxRequests) {
            // 如果超出最大请求次数,递减计数器,并报错
            count.decrementAndGet();
            throw new BizException(ResponseCodeEnum.LIMIT_EXCEPTION, "Too many requests, please try again later.");
        }
        // 执行原方法
        return joinPoint.proceed();
    }
}
使用方式
/**
  * 计数器算法限流
  *
  * @return
*/
@GetMapping("/counter")
@CounterRateLimit(requests = 2, timeWindow = 2)
public ResponseEntity counter() {
    return ResponseEntity.ok("counter test ok!");
}

(3)原理说明

在示例中,有一个使用了 @CounterRateLimit 注解的 counter 方法。根据注解的参数,这个方法在2秒钟的时间窗口内只能被调用2次。 如果在 2 秒内有更多的调用尝试,那么这些额外的调用将被限流,并返回错误信息。

(4)流程图

(5)缺点

无法处理“临界问题”。

(6)临界问题

假设1min一个时间段,每个时间段内最多100个请求。有一种极端情况,当10:00:58这个时刻100个请求一起过来,到达阈值;当10:01:02这个时刻100个请求又一起过来,到达阈值。这种情况就会导致在短短的4s内已经处理完了200个请求,而其他所有的时间都在限流中。

2. 滑动窗口

(1)代码

SlidingWindowRateLimit.java
package com.summo.demo.config.limitstrategy.slidingwindow;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SlidingWindowRateLimit {
    /**
     * 请求的数量
     *
     * @return
     */
    int requests();

    /**
     * 时间窗口,单位为秒
     *
     * @return
     */
    int timeWindow();
}
SlidingWindowRateLimitAspect.java
package com.summo.demo.config.limitstrategy.slidingwindow;

import com.summo.demo.exception.biz.BizException;
import com.summo.demo.model.response.ResponseCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;

@Slf4j
@Aspect
@Component
@Order(5)
public class SlidingWindowRateLimitAspect {
    /**
     * 使用 ConcurrentHashMap 保存每个方法的请求时间戳队列
     */
    private final ConcurrentHashMap<String, ConcurrentLinkedQueue<Long>> REQUEST_TIMES_MAP = new ConcurrentHashMap<>();

    @Around("@annotation(com.summo.demo.config.limitstrategy.slidingwindow.SlidingWindowRateLimit)")
    public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        Method method = signature.getMethod();
        SlidingWindowRateLimit rateLimit = method.getAnnotation(SlidingWindowRateLimit.class);
        // 允许的最大请求数
        int requests = rateLimit.requests();
        // 滑动窗口的大小(秒)
        int timeWindow = rateLimit.timeWindow();

        // 获取方法名称字符串
        String methodName = method.toString();
        // 如果不存在当前方法的请求时间戳队列,则初始化一个新的队列
        ConcurrentLinkedQueue<Long> requestTimes = REQUEST_TIMES_MAP.computeIfAbsent(methodName,
            k -> new ConcurrentLinkedQueue<>());

        // 当前时间
        long currentTime = System.currentTimeMillis();
        // 计算时间窗口的开始时间戳
        long thresholdTime = currentTime - TimeUnit.SECONDS.toMillis(timeWindow);

        // 这一段代码是滑动窗口限流算法中的关键部分,其功能是移除当前滑动窗口之前的请求时间戳。这样做是为了确保窗口内只保留最近时间段内的请求记录。
        // requestTimes.isEmpty() 是检查队列是否为空的条件。如果队列为空,则意味着没有任何请求记录,不需要进行移除操作。
        // requestTimes.peek() < thresholdTime 是检查队列头部的时间戳是否早于滑动窗口的开始时间。如果是,说明这个时间戳已经不在当前的时间窗口内,应当被移除。
        while (!requestTimes.isEmpty() && requestTimes.peek() < thresholdTime) {
            // 移除队列头部的过期时间戳
            requestTimes.poll();
        }

        // 检查当前时间窗口内的请求次数是否超过限制
        if (requestTimes.size() < requests) {
            // 未超过限制,记录当前请求时间
            requestTimes.add(currentTime);
            return joinPoint.proceed();
        } else {
            // 超过限制,抛出限流异常
            throw new BizException(ResponseCodeEnum.LIMIT_EXCEPTION, "Too many requests, please try again later.");
        }
    }
}
使用方式
/**
   * 滑动窗口算法限流
   *
   * @return
   */
@GetMapping("/slidingWindow")
@SlidingWindowRateLimit(requests = 2, timeWindow = 2)
public ResponseEntity slidingWindow() {
        return ResponseEntity.ok("slidingWindow test ok!");
}

(3)原理说明

从图上可以看到时间创建是一种滑动的方式前进, 滑动窗口限流策略能够显著减少临界问题的影响,但并不能完全消除它。滑动窗口通过跟踪和限制在一个连续的时间窗口内的请求来工作。与简单的计数器方法不同,它不是在窗口结束时突然重置计数器,而是根据时间的推移逐渐地移除窗口中的旧请求,添加新的请求。
举个例子:假设时间窗口为10s,请求限制为3,第一次请求在10:00:00发起,第二次在10:00:05发起,第三次10:00:11发起,那么计数器策略的下一个窗口开始时间是10:00:11,而滑动窗口是10:00:05。所以这也是滑动窗口为什么可以减少临界问题的影响,但并不能完全消除它的原因。

(4)流程图

3. 令牌桶

(1)代码

该算法我就不造轮子了,直接使用Guava自带的RateLimiter实现。

pom.xml
<!-- guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.1-jre</version>
</dependency>
TokenBucketRateLimit.java
package com.summo.demo.config.limitstrategy.tokenbucket;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TokenBucketRateLimit {
    /**
     * 产生令牌的速率(xx 个/秒)
     *
     * @return
     */
    double permitsPerSecond();
}

TokenBucketRateLimitAspect.java
package com.summo.demo.config.limitstrategy.tokenbucket;

import com.google.common.util.concurrent.RateLimiter;
import com.summo.demo.exception.biz.BizException;
import com.summo.demo.model.response.ResponseCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Aspect
@Component
@Order(5)
public class TokenBucketRateLimitAspect {
    private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();

    @Around("@annotation(com.summo.demo.config.limitstrategy.tokenbucket.TokenBucketRateLimit)")
    public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        Method method = signature.getMethod();
        TokenBucketRateLimit rateLimit = method.getAnnotation(TokenBucketRateLimit.class);
        double permitsPerSecond = rateLimit.permitsPerSecond();

        String methodName = method.toString();
        RateLimiter rateLimiter = limiters.computeIfAbsent(methodName, k -> RateLimiter.create(permitsPerSecond));

        if (rateLimiter.tryAcquire()) {
            return joinPoint.proceed();
        } else {
            throw new BizException(ResponseCodeEnum.LIMIT_EXCEPTION, "Too many requests, please try again later.");
        }
    }
}

使用方式

/**
   * 令牌桶算法限流
   *
   * @return
   */
@GetMapping("/tokenBucket")
@TokenBucketRateLimit(permitsPerSecond = 0.5)
public ResponseEntity tokenBucket() {
    return ResponseEntity.ok("tokenBucket test ok!");
}

(3)原理说明

令牌桶算法是一种流量控制机制,非常适合于处理突发流量,同时保证一定程度的平滑流动。它的工作原理类似于一个实际的水桶,其中水桶代表令牌桶,水流代表令牌。令牌以恒定的速率填充到桶中,直到达到桶的容量上限,多余的> 令牌会被丢弃。当请求(比如网络数据包或者游戏中的攻击动作)到达时,它需要消耗一个令牌才能被处理。如果桶中没有令牌,请求就会被延迟或丢弃,直到桶中再次有令牌为止。

以王者荣耀和LOL中的英雄攻速为例,英雄的攻击动作可以类比为请求,而英雄的攻速属性则确定了令牌生成的速度,即攻击的最大频率。如果英雄的攻击动作必须消耗一个令牌才能执行,那么即使玩家手速再快,也不能超过攻速设定> 的最大限制。这样,英雄的攻击将会保持一个恒定和平滑的节奏,而不会出现一会儿快速连续攻击,一会儿又突然停止的现象。

这种算法的优势在于其能够限制请求的峰值速率,同时允许一定程度的突发请求。在实际应用中,令牌桶算法可以平滑流量,减少拥塞,确保系统的稳定性。相较于仅仅依靠计数器或者滑动窗口算法,令牌桶算法提供了更加灵活和平滑> 的流量控制方式,非常适合需要处理突发性高流量和保持服务质量的场景。

(4)流程图

4. 漏桶

(1)代码

LeakyBucketRateLimit.java
package com.summo.demo.config.limitstrategy.leakybucket;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LeakyBucketRateLimit {
    /**
     * 桶的容量
     *
     * @return
     */
    int capacity();

    /**
     * 漏斗的速率,单位通常是秒
     *
     * @return
     */
    int leakRate();
}

LeakyBucketLimiter.java
package com.summo.demo.config.limitstrategy.leakybucket;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

public class LeakyBucketLimiter {
    /**
     * 桶的容量
     */
    private final int capacity;
    /**
     * 漏桶的漏出速率,单位时间内漏出水的数量
     */
    private final int leakRate;
    /**
     * 当前桶中的水量
     */
    private volatile int water = 0;
    /**
     * 上次漏水的时间
     */
    private volatile long lastLeakTime = System.currentTimeMillis();

    /**
     * 漏桶容器
     */
    private static final ConcurrentHashMap<String, LeakyBucketLimiter> LIMITER_MAP = new ConcurrentHashMap<>();

    /**
     * 静态工厂方法,确保相同的方法使用相同的漏桶实例
     *
     * @param methodKey 方法名
     * @param capacity
     * @param leakRate
     * @return
     */
    public static LeakyBucketLimiter createLimiter(String methodKey, int capacity, int leakRate) {
        return LIMITER_MAP.computeIfAbsent(methodKey, k -> new LeakyBucketLimiter(capacity, leakRate));
    }

    private LeakyBucketLimiter(int capacity, int leakRate) {
        this.capacity = capacity;
        this.leakRate = leakRate;
    }

    /**
     * 尝试获取许可(try to acquire a permit),如果获取成功返回true,否则返回false
     *
     * @return
     */
    public boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        synchronized (this) {
            // 计算上次漏水到当前时间的时间间隔
            long leakDuration = currentTime - lastLeakTime;
            // 如果时间间隔大于等于1秒,表示漏桶已经漏出一定数量的水
            if (leakDuration >= TimeUnit.SECONDS.toMillis(1)) {
                // 计算漏出的水量
                long leakQuantity = leakDuration / TimeUnit.SECONDS.toMillis(1) * leakRate;
                // 漏桶漏出水后,更新桶中的水量,但不能低于0
                water = (int)Math.max(0, water - leakQuantity);
                lastLeakTime = currentTime;
            }
            // 判断桶中的水量是否小于容量,如果是则可以继续添加水(相当于获取到令牌)
            if (water < capacity) {
                water++;
                return true;
            }
        }
        // 如果桶满,则获取令牌失败
        return false;
    }

}

LeakyBucketRateLimitAspect.java
package com.summo.demo.config.limitstrategy.leakybucket;

import java.lang.reflect.Method;

import com.summo.demo.exception.biz.BizException;
import com.summo.demo.model.response.ResponseCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
@Order(5)
public class LeakyBucketRateLimitAspect {

    @Around("@annotation(com.summo.demo.config.limitstrategy.leakybucket.LeakyBucketRateLimit)")
    public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        Method method = signature.getMethod();
        LeakyBucketRateLimit leakyBucketRateLimit = method.getAnnotation(LeakyBucketRateLimit.class);

        int capacity = leakyBucketRateLimit.capacity();
        int leakRate = leakyBucketRateLimit.leakRate();

        // 方法签名作为唯一标识
        String methodKey = method.toString();

        LeakyBucketLimiter limiter = LeakyBucketLimiter.createLimiter(methodKey, capacity, leakRate);
        if (!limiter.tryAcquire()) {
            // 超过限制,抛出限流异常
            throw new BizException(ResponseCodeEnum.LIMIT_EXCEPTION, "Too many requests, please try again later.");
        }

        return joinPoint.proceed();
    }
}

使用方式
/**
   * 漏桶算法限流
   *
   * @return
   */
@GetMapping("/leakyBucket")
@LeakyBucketRateLimit(capacity = 100, leakRate = 20)
public ResponseEntity leakyBucket() {
    return ResponseEntity.ok("leakyBucket test ok!");
}

(3)原理说明

在Leaky Bucket算法中,容器有一个固定的容量,类似于漏桶的容量。数据以固定的速率进入容器,如果容器满了,则多余的数据会溢出。容器中的数据会以恒定的速率从底部流出,类似于漏桶中的水滴。如果容器中的数据不足以满足流出速率,则会等待直到有足够的数据可供流出。这样就实现了对数据流的平滑控制。

(4)流程图

四、小结一下

如果面试中被问到如何进行接口优化,大家第一印象会想到什么?放到3年前的我来看,第一反应肯定是优化接口性能,然后说一个本来耗时好几秒的接口优化到毫秒的案例。而现在的我会说:上下文、权限控制、防抖/限流等等,当然也会说性能优化。感谢一直在学习的自己!

在写这篇文章之前,我看了很多大佬写的相关文章,才开始动笔,不是看不懂也不是写不来,只是因为我没怎么在真实业务中用到限流策略,我不敢乱写。怕给人埋坑。

回顾前面的6篇,加上这一篇一共7篇,共计耗时3个月,但第6篇到第7篇则整整花了一个月,不是在偷懒,理论型的东西好写,但没有实际业务支持那就是高谈阔论。不过沉淀了一个月,不写点东西我怕马上就忘记了,还是写一篇,拙文共赏吧!如果有哪位同学在真实业务中使用过类似的限流策略,可以和我们分享一下经验哈,谢谢啦!

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

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

相关文章

【EAI 006】ChatGPT for Robotics:将 ChatGPT 应用于机器人任务的提示词工程研究

论文标题&#xff1a;ChatGPT for Robotics: Design Principles and Model Abilities 论文作者&#xff1a;Sai Vemprala, Rogerio Bonatti, Arthur Bucker, Ashish Kapoor 作者单位&#xff1a;Scaled Foundations, Microsoft Autonomous Systems and Robotics Research 论文原…

第六站:C++面向对象

面向对象的第一概念:类 类的构成: “类”&#xff0c;是一种特殊的“数据类型”&#xff0c;不是一个具体的数据。 类的设计: 创建一个类: class Human { public://公有的,对外的void eat();//方法,成员函数void sleep();void play();void work();string getName();//获取对内…

元素、物质、原子、分子之间的关系。

问题描述&#xff1a;元素、物质、原子、分子之间的关系。 问题解答&#xff1a; 首先&#xff0c;元素是具有相同核电荷数的同一类原子的总称。例如&#xff0c;氢元素包含所有质子数为1的原子。 其次&#xff0c;物质是由元素组成的。物质可以由单一的元素构成&#xff0c…

【书生·浦语】大模型实战营——第五课作业

教程文档&#xff1a;https://github.com/InternLM/tutorial/blob/vansin-patch-4/lmdeploy/lmdeploy.md#tritonserver-%E6%9C%8D%E5%8A%A1%E4%BD%9C%E4%B8%BA%E5%90%8E%E7%AB%AF 视频链接&#xff1a; 作业&#xff1a; 基础作业 使用如下命令创建conda环境 conda create…

【python入门】day28:记录用户登录日志

演示 代码 #-*- coding:utf-8 -*- print(记录用户登录日志----------------------------) import time def show_info():print(输入提示数字,执行相应操作:0退出,1查看登录日志) def write_logininfo(username):#----------记录日志with open(log.txt,a,encodingutf-8)as file…

基于SSM的网上订餐管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

Unity中图片合成图集Editor工具

一般图片合成图集用的是Unity自带的SpriteAtlas类添加一个Sprite集合&#xff0c;而所有图片保存在Sprite集合中&#xff0c;然后把Sprite通过Add方法添加到SpriteAtlas类&#xff0c;通过AssetDatabase.CreateAsset()方法来创建图集。

希尔排序和计数排序

&#x1f4d1;前言 本文主要是【排序】——希尔排序、计数排序的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 &#x1f304;每日一句…

Arduino开发实例-HW-M10 微波雷达运动传感器

HW-M10 微波雷达运动传感器 文章目录 HW-M10 微波雷达运动传感器1、HW-M10 微波雷达运动传感器介绍2、硬件准备及接线3、代码实现1、HW-M10 微波雷达运动传感器介绍 HW-M10 微波传感器模块非常准确,广泛用于报警和安全系统中的运动检测。 该模块与 PIR 模块一样,可以检测任何…

LINUX——动/静态库

加油加油~ 目录&#xff1a; 动/静态库是什么&#xff1f; .o文件是什么&#xff1f; 以gcc编译器为例&#xff0c;查看xxx.i xxx.s xxx.o文件 生成test.i文件(预处理) 生成test.s文件(编译) 生成test.o文件(汇编) 生成可执行程序(链接)&#xff1a; 小结&#xff1a…

Unity与Android交互通信系列(4)

上篇文章我们实现了模块化调用&#xff0c;运用了模块化设计思想和简化了调用流程&#xff0c;本篇文章讲述UnityPlayerActivity类的继承和使用。 在一些深度交互场合&#xff0c;比如Activity切换、程序启动预处理等&#xff0c;这时可能会需要继承Application和UnityPlayerAc…

c++ 开发生态环境、工作流程、生命周期-拾遗

拾遗 1 生态环境初识 当您使用Visual Studio 2019进行C开发时&#xff0c;您将进入C生态环境。以下是一些重要的概念和步骤&#xff1a; C程序的结构&#xff1a; 一个典型的C程序包括源文件&#xff08;.cpp&#xff09;、头文件&#xff08;.h&#xff09;、编译后的目标文…

通过wireshark抓取的流量还原文件(以zip为例)

wireshark打开流量包&#xff0c;通过zip关键字查找 追踪流可查看详细信息 选中media Type右键&#xff0c; 点击导出分组字节流选项 将生成的文件进行命名&#xff0c;需要时什么格式就以什么格式后缀

没有自动化测试项目经验,3个项目帮你走入软测职场!

学习自动化测试最难的是没有合适的项目练习。测试本身既要讲究科学&#xff0c;又有艺术成分&#xff0c;单单学几个 API 的调用很难应付工作中具体的问题。 你得知道什么场景下需要添加显性等待&#xff0c;什么时候元素定位需要写得更加优雅&#xff0c;为什么需要断言这个元…

【Vue3】2-12 : 【案例】搜索关键词加筛选条件的综合

本书目录&#xff1a;点击进入 一、【案例】搜索关键词加筛选条件的综合 1.1、逻辑 1.2、效果 1.3、json数据 - 02-data.json 1.4、代码 一、【案例】搜索关键词加筛选条件的综合 1.1、逻辑 计算属性 - 绑定list&#xff0c;并过滤 input 双向绑定 - 当input改变时&…

Python3.10安装教程

Python3.10安装 Python的安装按照下面几步进行即可&#xff0c;比较简单。 下载Python安装文件&#xff0c;打开Python的下载页面&#xff0c;我这里选择安装的版本是3.10.11&#xff0c;根据自己电脑版本选择对应安装包 安装包下载完毕后&#xff0c;按照步骤开始安装。选择…

PriorityQueue优先队列使用的注意事项

PriorityQueue只保证队列的头和尾是指定序列的两个端点值&#xff0c;不是给它的元素排序了。 所以在使用的时候直接打印 PriorityQueue &#xff0c;或者用 增强for 遍历出来的数据都不是有序的。正确的遍历方式如下&#xff1a; // 按照排序顺序输出 PriorityQueue 中的元素…

unet脑肿瘤分割完整代码

U-net脑肿瘤分割完整代码 代码目录数据集网络训练测试 代码目录 数据集 https://www.kaggle.com/datasets/mateuszbuda/lgg-mri-segmentation dataset.py 在这里插入代码片import os import numpy as np import glob from PIL import Image import cv2 import torchvision fr…

详谈Python的开发工具

Python作为一种流行的编程语言&#xff0c;在开发过程中需要使用各种工具来提高效率、简化工作流程和改善开发体验。在本文中&#xff0c;我们将介绍一些常用的Python开发工具&#xff0c;包括文本编辑器、集成开发环境&#xff08;IDE&#xff09;、虚拟环境管理工具、包管理器…

MCU最小系统原理图中四个问题详解——芯片中有很多电源管脚的原因(VDD/VSS/VBAT)、LC滤波、两级滤波、NC可切换元件

前言&#xff1a;本文对MCU最小系统原理图中的四个问题进行详解&#xff1a;芯片中有很多电源管脚的原因&#xff08;VDD/VSS/VBAT&#xff09;、LC滤波、两级滤波、NC可切换元件。本文以GD32F103C8T6最小系统原理图举例 目录&#xff1a; 芯片中有很多电源管脚的原因&#x…