springboot+redis+lua实现限流
Redis 除了做缓存,还能干很多很多事情:分布式锁、限流、处理请求接口幂等性。。。太多太多了~
今天想和小伙伴们聊聊用 Redis 处理接口限流。
1. 准备工作
首先我们创建一个 Spring Boot 工程,引入 Web 和 Redis 依赖,同时考虑到接口限流一般是通过注解来标记,而注解是通过 AOP 来解析的,所以我们还需要加上 AOP 的依赖,最终的依赖如下:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>然后提前准备好一个 Redis 实例,这里我们项目配置好之后,直接配置一下 Redis 的基本信息即可,如下:
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123好啦,准备工作就算是到位了。
2. 限流注解
接下来我们创建一个限流注解,我们将限流分为两种情况:
针对当前接口的全局性限流,例如该接口可以在 1 分钟内访问 100 次。
针对某一个 IP 地址的限流,例如某个 IP 地址可以在 1 分钟内访问 100 次。
针对这两种情况,我们创建一个枚举类:
public enum LimitType {/*** 默认策略全局限流*/DEFAULT,/*** 根据请求者IP进行限流*/IP
}接下来我们来创建限流注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {/*** 限流key*/String key() default "rate_limit:";/*** 限流时间,单位秒*/int time() default 60;/*** 限流次数*/int count() default 100;/*** 限流类型*/LimitType limitType() default LimitType.DEFAULT;
}第一个参数限流的 key,这个仅仅是一个前缀,将来完整的 key 是这个前缀再加上接口方法的完整路径,共同组成限流 key,这个 key 将被存入到 Redis 中。
另外三个参数好理解,我就不多说了。
好了,将来哪个接口需要限流,就在哪个接口上添加 @RateLimiter 注解,然后配置相关参数即可。
3. 定制 RedisTemplate
小伙伴们知道,在 Spring Boot 中,我们其实更习惯使用 Spring Data Redis 来操作 Redis,不过默认的 RedisTemplate 有一个小坑,就是序列化用的是 JdkSerializationRedisSerializer,不知道小伙伴们有没有注意过,直接用这个序列化工具将来存到 Redis 上的 key 和 value 都会莫名其妙多一些前缀,这就导致你用命令读取的时候可能会出错。
例如存储的时候,key 是 name,value 是 javaboy,但是当你在命令行操作的时候,get name 却获取不到你想要的数据,原因就是存到 redis 之后 name 前面多了一些字符,此时只能继续使用 RedisTemplate 将之读取出来。
我们用 Redis 做限流会用到 Lua 脚本,使用 Lua 脚本的时候,就会出现上面说的这种情况,所以我们需要修改 RedisTemplate 的序列化方案。
可能有小伙伴会说为什么不用 StringRedisTemplate 呢?StringRedisTemplate 确实不存在上面所说的问题,但是它能够存储的数据类型不够丰富,所以这里不考虑。
修改 RedisTemplate 序列化方案,代码如下:
@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(connectionFactory);// 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);return redisTemplate;}
}这个其实也没啥好说的,key 和 value 我们都使用 Spring Boot 中默认的 jackson 序列化方式来解决。
4. 开发 Lua 脚本
这个其实我在之前 vhr 那一套视频中讲过,Redis 中的一些原子操作我们可以借助 Lua 脚本来实现,想要调用 Lua 脚本,我们有两种不同的思路:
在 Redis 服务端定义好 Lua 脚本,然后计算出来一个散列值,在 Java 代码中,通过这个散列值锁定要执行哪个 Lua 脚本。
直接在 Java 代码中将 Lua 脚本定义好,然后发送到 Redis 服务端去执行。
Spring Data Redis 中也提供了操作 Lua 脚本的接口,还是比较方便的,所以我们这里就采用第二种方案。
我们在 resources 目录下新建 lua 文件夹专门用来存放 lua 脚本,脚本内容如下:
local key = KEYS[1]
local count = tonumber(ARGV[1])
local time = tonumber(ARGV[2])
local current = redis.call('get', key)
if current and tonumber(current) > count thenreturn tonumber(current)
end
current = redis.call('incr', key)
if tonumber(current) == 1 thenredis.call('expire', key, time)
end
return tonumber(current)这个脚本其实不难,大概瞅一眼就知道干啥用的。KEYS 和 ARGV 都是一会调用时候传进来的参数,tonumber 就是把字符串转为数字,redis.call 就是执行具体的 redis 指令,具体流程是这样:
首先获取到传进来的 key 以及 限流的 count 和时间 time。
通过 get 获取到这个 key 对应的值,这个值就是当前时间窗内这个接口可以访问多少次。
如果是第一次访问,此时拿到的结果为 nil,否则拿到的结果应该是一个数字,所以接下来就判断,如果拿到的结果是一个数字,并且这个数字还大于 count,那就说明已经超过流量限制了,那么直接返回查询的结果即可。
如果拿到的结果为 nil,说明是第一次访问,此时就给当前 key 自增 1,然后设置一个过期时间。
最后把自增 1 后的值返回就可以了。
其实这段 Lua 脚本很好理解。
接下来我们在一个 Bean 中来加载这段 Lua 脚本,如下:
@Bean
public DefaultRedisScript<Long> limitScript() {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));redisScript.setResultType(Long.class);return redisScript;
}可以啦,我们的 Lua 脚本现在就准备好了。
5. 注解解析
接下来我们就需要自定义切面,来解析这个注解了,我们来看看切面的定义:
@Aspect
@Component
public class RateLimiterAspect {private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);@Autowiredprivate RedisTemplate<Object, Object> redisTemplate;@Autowiredprivate RedisScript<Long> limitScript;@Before("@annotation(rateLimiter)")public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {String key = rateLimiter.key();int time = rateLimiter.time();int count = rateLimiter.count();String combineKey = getCombineKey(rateLimiter, point);List<Object> keys = Collections.singletonList(combineKey);try {Long number = redisTemplate.execute(limitScript, keys, count, time);if (number==null || number.intValue() > count) {throw new ServiceException("访问过于频繁,请稍候再试");}log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);} catch (ServiceException 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) {stringBuffer.append(IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest())).append("-");}MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();Class<?> targetClass = method.getDeclaringClass();stringBuffer.append(targetClass.getName()).append("-").append(method.getName());return stringBuffer.toString();}
}@Slf4jpublic class IPUtil {public static String getIpAddr() {HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();String ip = null;String ipAddresses = request.getHeader("X-Forwarded-For");if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {ipAddresses = request.getHeader("Proxy-Client-IP");}if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {ipAddresses = request.getHeader("WL-Proxy-Client-IP");}if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {ipAddresses = request.getHeader("HTTP_CLIENT_IP");}if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {ipAddresses = request.getHeader("X-Real-IP");}if (ipAddresses != null && ipAddresses.length() != 0) {ip = ipAddresses.split(",")[0];}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {ip = request.getRemoteAddr();}if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {ipAddresses = request.getRemoteAddr();if (ipAddresses.equals("127.0.0.1") || ipAddresses.equals("0:0:0:0:0:0:0:1")) {//根据网卡取本机配置的IPInetAddress inet = null;try {inet = InetAddress.getLocalHost();} catch (UnknownHostException e) {log.error("获取ip失败 {}", Arrays.asList(e.getStackTrace()));}if (inet != null) {ip = inet.getHostAddress();} else {ip = "127.0.0.1";}}}return ip;}
}这个切面就是拦截所有加了 @RateLimiter 注解的方法,在前置通知中对注解进行处理。
首先获取到注解中的 key、time 以及 count 三个参数。
获取一个组合的 key,所谓的组合的 key,就是在注解的 key 属性基础上,再加上方法的完整路径,如果是 IP 模式的话,就再加上 IP 地址。以 IP 模式为例,最终生成的 key 类似这样:rate_limit:127.0.0.1-org.javaboy.ratelimiter.controller.HelloController-hello(如果不是 IP 模式,那么生成的 key 中就不包含 IP 地址)。
将生成的 key 放到集合中。
通过 redisTemplate.execute 方法取执行一个 Lua 脚本,第一个参数是脚本所封装的对象,第二个参数是 key,对应了脚本中的 KEYS,后面是可变长度的参数,对应了脚本中的 ARGV。
将 Lua 脚本执行的结果与 count 进行比较,如果大于 count,就说明过载了,抛异常就行了。
好了,大功告成了。
6. 接口测试
接下来我们就进行接口的一个简单测试,如下:
@RestController
public class HelloController {@GetMapping("/hello")@RateLimiter(time = 5,count = 3,limitType = LimitType.IP)public String hello() {return "hello>>>"+new Date();}
}每一个 IP 地址,在 5 秒内只能访问 3 次。
这个自己手动刷新浏览器都能测试出来。
7. 其他
问题RedisTemplate执行lua脚本在Redis集群模式下报错EvalSha is not supported in cluster environme解决。
原因:spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本。下面展示一些内联代码片段。
@Before("@annotation(rateLimiter)")public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {String key = rateLimiter.key();int time = rateLimiter.time();int count = rateLimiter.count();String combineKey = getCombineKey(rateLimiter, point);List<String> keys = Collections.singletonList(combineKey);List<String> paramList = Lists.newArrayList(String.valueOf(count), String.valueOf(time),String.valueOf(System.currentTimeMillis()));try {Long number = redisTemplate.execute((RedisCallback<Long>) connection -> {Object nativeConnection = connection.getNativeConnection();// 集群if (nativeConnection instanceof JedisCluster) {return (Long) ((JedisCluster) nativeConnection).eval(LIMIT_SCRIPT_SLIDING_WINDOW_3, keys, paramList);}// 单机if (nativeConnection instanceof Jedis) {return (Long) ((Jedis) nativeConnection).eval(LIMIT_SCRIPT_SLIDING_WINDOW_3, keys, paramList);}return null;});if (number == null || number.intValue() > count) {log.error("访问过于频繁,请稍候再试! 提示:{}秒内最大并发请求次数为{}次", time, count);throw new BusinessException("访问过于频繁,超过限流次数!提示:" + time + "秒内最大并发请求次数为" + count + "次");}log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);} catch (BusinessException e) {throw e;} catch (Exception e) {throw new RuntimeException("服务器限流异常,请稍候再试!");}}redis限流算法
常用的限流算法有:固定窗口,滑动窗口,令牌桶等。前面给出的算法是固定窗口算法,解决不了临界点并发访问超过阈值的问题。下面给出2个版本的滑动窗口lua代码。
/*** 滑动窗口算法-lua脚本字符串2*/private static final String LIMIT_SCRIPT_SLIDING_WINDOW_2 =//获取key"local key = KEYS[1] "//最大次数+ "local count = tonumber(ARGV[1]) "//缓存时间+ "local time = tonumber(ARGV[2]) "//当前时间+ "local currentMs = tonumber(ARGV[3]) "//窗口开始时间+ "local windowStartMs = currentMs - time * 1000 "//清除所有过期成员+ "redis.call('ZREMRANGEBYSCORE', key, 0, windowStartMs) "//添加当前成员+ "redis.call('zadd', key, tostring(currentMs), currentMs) "//获取key的次数+ "local current = redis.call('zcard',key) "//如果key的次数存在且大于预设值直接返回当前key的次数+ "if current and tonumber(current) > count then "+ "return tonumber(current) "+ "end "//设置过期时间,删除冷数据+ "redis.call('expire', key, time) "//返回key的次数+ "return tonumber(current)";/*** 滑动窗口算法-lua脚本字符串3*/private static final String LIMIT_SCRIPT_SLIDING_WINDOW_3 ="local key = KEYS[1] "+ "local count = tonumber(ARGV[1]) "+ "local time = tonumber(ARGV[2]) "+ "local currentMs = tonumber(ARGV[3]) "+ "local windowStartMs = currentMs - time * 1000 "+ "local current = redis.call('zcount', key, windowStartMs, currentMs) "+ "if current and tonumber(current) > count then "+ "return tonumber(current) "+ "end "+ "redis.call('ZREMRANGEBYSCORE', key, 0, windowStartMs) "+ "redis.call('zadd', key, tostring(currentMs), currentMs) "+ "redis.call('expire', key, time) "+ "return tonumber(current)";个人认为,LIMIT_SCRIPT_SLIDING_WINDOW_3更加准确一些,因为它没有将超过限制的无用请求放入zset中,请求数量是相对平滑的。
参考文章:https://mp.weixin.qq.com/s/ymgwN2w-YxCIug8lgxrwog
相关文章:
springboot+redis+lua实现限流
Redis 除了做缓存,还能干很多很多事情:分布式锁、限流、处理请求接口幂等性。。。太多太多了~今天想和小伙伴们聊聊用 Redis 处理接口限流。1. 准备工作首先我们创建一个 Spring Boot 工程,引入 Web 和 Redis 依赖,同时…...
线段树总结
文章目录参考文档题目线段树实现单点修改,区间求值模板题目308. 二维区域和检索 - 可变区间修改,区间求值1. 掉落的方块(区间开点)2. 维护序列3. 一个简单的问题24. 天际线问题动态开点1. 区间和个数(单点修改开点)问题以及注意事…...
龙芯GS232(MIPS 32)架构cache管理笔记
1 mips32架构 MIPS架构是一种基于精简指令集(Reduced Instruction Set Computer,RISC)的计算机处理器架构。MIPS架构由MIPS Technologies公司在1981年开发,并在1984年发布了第一款MIPS处理器。 MIPS架构的特点包括: …...
js去重
<script>let arr [{ id: 0, name: "张三" },{ id: 1, name: "李四" },{ id: 2, name: "王五" },{ id: 3, name: "赵六" },{ id: 1, name: "孙七" },{ id: 2, name: "周八" },{ id: 2, name: "吴九&qu…...
小白都能看懂的C语言入门教程
文章目录C语言入门教程1. 第一个C语言程序HelloWorld2. C语言的数据类型3. 常量变量的使用4. 自定义标识符#define5. 枚举的使用6. 字符串和转义字符7. 判断和循环8. 函数9. 数组的使用10. 操作符的使用11. 结构体12. 指针的简单使用C语言入门教程 1. 第一个C语言程序HelloWor…...
leetcode 21~30 学习经历
leetcode 21~30 学习经历21. 合并两个有序链表22. 括号生成23. 合并K个升序链表24. 两两交换链表中的节点25. K 个一组翻转链表26. 删除有序数组中的重复项27. 移除元素28. 找出字符串中第一个匹配项的下标29. 两数相除30. 串联所有单词的子串小结21. 合并两个有序链表 将两个升…...
让ArcMap变得更加强大,用python执行地理处理以及编写自定义脚本工具箱
文章目录一、用python执行地理处理工具1.1 例:乘以0.00011.2 例:裁剪栅格1.3 哪里查看调用某工具的代码?二、用python批量执行地理处理工具2.1 必需的python语法知识for循环语句缩进的使用注释的使用2.2 一个批处理栅格的代码模板三、创建自定…...
SAP 项目实施阶段全过程
在sap实施项目的周期和步骤上,根据各公司对业务的理解不同,也被划分为各个阶段,但其中由普华永道提出的分七步走,个人觉得对刚进入这一行业的人很有帮助,接下来一起分享和讨论下: sap实施项目生命周期&…...
idea中的Maven导包失败问题解决总结
idea中的Maven导包失败问题解决总结 先确定idea和Maven 的配置文件settings 没有问题 找到我们本地的maven仓库,默认的maven仓库路径是在\C:\Users\用户名.m2下 有两个文件夹,repositotry是放具体jar包的,根据报错包的名,找对应文…...
REDIS中的缓存穿透,缓存击穿,缓存雪崩原因以及解决方案
需求引入一般在项目的开发中,都是使用关系型数据库来进行数据的存储,通常不会存在什么高并发的情况,可是一旦涉及大数据量的需求,比如商品抢购,网页活动导致的主页访问量瞬间增大,单一使用关系型数据库来保存数据的系统…...
数据库及缓存之MySQL(一)
思维导图 常见知识点 1.mysql存储引擎: 2.innodb与myisam区别: 3.表设计字段选择: 4.mysql的varchar(M)最多存储数据: 5.事务基本特性: 6.事务并发引发问题: 7.mysql索引: 8.三星索引…...
项目管理中,项目经理需要具备哪些能力?
项目经理是团队的领导者,是带领项目团队对项目进行策划、执行,完成项目目标,对于项目经理来说,想要有序推进项目,使项目更成功,光有理论知识是不够的,也要具备这些能力: 1、分清主…...
itk中的一些图像处理
文章目录1.BinomialBlurImageFilter计算每个维度上的最近邻居平均值2.高斯平滑3.图像的高阶导数 RecursiveGaussianImageFilter4.均值滤波5.中值滤波6.离散高斯平滑7.曲率驱动流去噪图像 CurvatureFlowImageFilter8.由参数alpha和beta控制的幂律自适应直方图均衡化9.Canny 边缘…...
Endless lseek导致的SQL异常
最近碰到同事咨询的一个问题,在执行一个函数时,发现会一直卡在那里。 strace抓了下发现会话一直在执行lseek,大致情况如下: 16:13:55.451832 lseek(33, 0, SEEK_END) 1368064 <0.000037> 16:13:55.477216 lseek(33, 0, SE…...
JUC-day01
JUC-day01 什么是JUC线程的状态: wait sleep关键字:同步锁 原理(重点)Lock接口: ReentrantLock(可重入锁)—>AQS CAS线程之间的通讯 1 什么是JUC 1.1 JUC简介 在Java中,线程部分是一个重点,本篇文章说的JUC也是关于线程的。JUC就是java.util .con…...
Mind+Python+Mediapipe项目——AI健身之跳绳
原文:MindPythonMediapipe项目——AI健身之跳绳 - DF创客社区 - 分享创造的喜悦 【项目背景】跳绳是一个很好的健身项目,为了获知所跳个数,有的跳绳上会有计数器。但这也只能跳完这后看到,能不能在跳的过程中就能看到,…...
数据库概述
20世纪60年代后期,就出现了数据库技术。取得成就如下:造就了四位图灵奖得主发展成为以数据建模和DBMS核心技术为主,内容丰富的一门学科。带动了一个巨大的软件产业-DBMS产品及其相关工具和解决方案。四个基本概念数据数据是数据库中存储的基本…...
【已解决】解决IDEA的maven刷新依赖时出现Connot reconnect错误
前言 小编我将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知识,有兴趣的小伙伴可以关注一下!也许一个人独行,可以走的很快,但是一群人结伴而行,才能走的更远!让我们在成长的道路上互相学习&#…...
动态链接库(.so)文件的变编译和引用、执行
动态链接库(.so)文件的变编译和引用 动态链接库:SO(Shared Object)是一种动态链接库,也被称为共享库。它是一种可被多个程序共享使用的二进制代码库,其中包含已编译的函数和代码。与静态链接库不同,动态链接…...
linux(centos8)文件解压命令
linux解压命令tar 解压命令常用解压命令1 [.tar] 文件 解压到当前文件夹2 [.tar.gz] 文件 解压到当前文件夹3 [.tar] 解压到指定文件夹 -C 必须是大写unzip 解压命令常用解压命令1 [.zip]解压到当前文件夹2 [.zip] 解压到指定文件夹2 [.zip] 解压到指定文件夹(强行覆…...
接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
第 86 场周赛:矩阵中的幻方、钥匙和房间、将数组拆分成斐波那契序列、猜猜这个单词
Q1、[中等] 矩阵中的幻方 1、题目描述 3 x 3 的幻方是一个填充有 从 1 到 9 的不同数字的 3 x 3 矩阵,其中每行,每列以及两条对角线上的各数之和都相等。 给定一个由整数组成的row x col 的 grid,其中有多少个 3 3 的 “幻方” 子矩阵&am…...
学习STC51单片机32(芯片为STC89C52RCRC)OLED显示屏2
每日一言 今天的每一份坚持,都是在为未来积攒底气。 案例:OLED显示一个A 这边观察到一个点,怎么雪花了就是都是乱七八糟的占满了屏幕。。 解释 : 如果代码里信号切换太快(比如 SDA 刚变,SCL 立刻变&#…...
Java 二维码
Java 二维码 **技术:**谷歌 ZXing 实现 首先添加依赖 <!-- 二维码依赖 --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.5.1</version></dependency><de…...
AirSim/Cosys-AirSim 游戏开发(四)外部固定位置监控相机
这个博客介绍了如何通过 settings.json 文件添加一个无人机外的 固定位置监控相机,因为在使用过程中发现 Airsim 对外部监控相机的描述模糊,而 Cosys-Airsim 在官方文档中没有提供外部监控相机设置,最后在源码示例中找到了,所以感…...
Linux 中如何提取压缩文件 ?
Linux 是一种流行的开源操作系统,它提供了许多工具来管理、压缩和解压缩文件。压缩文件有助于节省存储空间,使数据传输更快。本指南将向您展示如何在 Linux 中提取不同类型的压缩文件。 1. Unpacking ZIP Files ZIP 文件是非常常见的,要在 …...
高考志愿填报管理系统---开发介绍
高考志愿填报管理系统是一款专为教育机构、学校和教师设计的学生信息管理和志愿填报辅助平台。系统基于Django框架开发,采用现代化的Web技术,为教育工作者提供高效、安全、便捷的学生管理解决方案。 ## 📋 系统概述 ### 🎯 系统定…...
macOS 终端智能代理检测
🧠 终端智能代理检测:自动判断是否需要设置代理访问 GitHub 在开发中,使用 GitHub 是非常常见的需求。但有时候我们会发现某些命令失败、插件无法更新,例如: fatal: unable to access https://github.com/ohmyzsh/oh…...
前端高频面试题2:浏览器/计算机网络
本专栏相关链接 前端高频面试题1:HTML/CSS 前端高频面试题2:浏览器/计算机网络 前端高频面试题3:JavaScript 1.什么是强缓存、协商缓存? 强缓存: 当浏览器请求资源时,首先检查本地缓存是否命中。如果命…...
