【PmHub后端篇】PmHub中基于Redis加Lua脚本的计数器算法限流实现
1 限流的重要性
在高并发系统中,保护系统稳定运行的关键技术有缓存、降级和限流。
- 缓存通过在内存中存储常用数据,减少对数据库的访问,提升系统响应速度,如浏览器缓存、CDN缓存等多种应用层面。
- 降级则是在系统压力过大或部分服务不可用时,暂时关闭非核心服务,保障核心服务正常运行,包括页面降级、功能降级和服务降级等。
- 限流是控制请求速率的技术,防止系统过载,常见算法有令牌桶算法、漏桶算法等。
这三大技术通常结合使用,根据系统具体需求和特点合理配置,以实现最佳效果。
2 限流的基本概念
限流是限制单位时间内系统处理的请求数量,确保系统正常运行,避免因超负荷而崩溃的一种技术。
其中,阈值是单位时间内允许的最大请求数量,例如每秒请求数(QPS)限制为500,即系统1秒内最多处理500个请求。
拒绝策略是请求数量超过阈值时系统的处理方式,常见的有直接拒绝(立即拒绝超过阈值的请求)和排队等待(将超过阈值的请求放入队列依次处理)。
目前有两个比较主流的限流方案:
- 网关层限流。将限流规则应用在所有流量的入口处。
- 中间件限流。将限流信息存储在分布式环境中某个中间件里(比如redis),每个组件都可以从这里获取到当前时间的流量统计,从而决定是否放行还是拒绝。
3 常见限流算法对比
限流算法 | 原理 | 优点 | 缺点 |
---|---|---|---|
计数器(Counter)法 | 在固定时间窗口内统计请求数,超过阈值则拒绝请求 | 实现简单,适用于固定时间窗口的流量控制 | 无法处理突发流量 |
滑动窗口计数器法 | 将固定时间窗口分成多个小窗口,通过滑动小窗口动态统计总请求数 | 平滑处理流量,比固定窗口更有效 | 实现复杂度较高 |
漏桶(Leaky Bucket)算法 | 请求进入漏桶,漏桶以恒定速率出水,当桶满时新的请求被丢弃 | 平滑突发流量,严格控制请求处理速率 | 可能导致请求延迟增加 |
令牌桶(Token Bucket)算法 | 系统按恒定速率生成令牌,请求消耗令牌,当没有令牌时请求被拒绝或排队 | 允许突发流量处理,能长期控制处理速率 | 实现相对复杂,需要管理令牌 |
漏桶与令牌桶的组合 | 将漏桶与令牌桶结合,既能控制平均速率,又能应对突发流量 | 综合两者优点,既能平滑流量又能应对突发流量 | 实现复杂度更高 |
3.1 计数器
实现方式:控制单位时间内的请求数量。
以下是使用计数器算法实现限流的 Java 示例代码,该示例模拟了在固定时间窗口内对请求进行计数,并判断是否超过设定的阈值:
import java.util.concurrent.atomic.AtomicInteger;public class CounterRateLimiter {// 最大访问数量private final int limit;// 访问时间差,单位为毫秒private final long timeout;// 请求时间private long time;// 当前计数器private AtomicInteger reqCount = new AtomicInteger(0);public CounterRateLimiter(int limit, long timeout) {this.limit = limit;this.timeout = timeout;this.time = System.currentTimeMillis();}// 判断是否允许请求通过public boolean tryAcquire() {long now = System.currentTimeMillis();if (now < time + timeout) {// 单位时间内int count = reqCount.incrementAndGet();return count <= limit;} else {// 超出单位时间,重置计数器和时间time = now;reqCount.set(1);return true;}}public static void main(String[] args) {// 示例:设置每 1000 毫秒内最多允许 5 个请求CounterRateLimiter limiter = new CounterRateLimiter(5, 1000);for (int i = 0; i < 10; i++) {boolean allowed = limiter.tryAcquire();if (allowed) {System.out.println("请求 " + i + " 通过限流");} else {System.out.println("请求 " + i + " 被限流");}try {// 模拟请求间隔Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}}}
}
在上述代码中:
CounterRateLimiter
类实现了计数器限流逻辑。- 构造函数
CounterRateLimiter(int limit, long timeout)
用于设置限流的阈值limit
和时间窗口timeout
。 tryAcquire
方法用于判断请求是否能通过限流,在单位时间内对请求计数,并根据阈值决定是否允许请求通过;当超出单位时间时,重置计数器和时间。- 在
main
方法中,创建了一个限流实例,并模拟了一系列请求,展示了限流的效果。
3.2 滑动窗口
实现方式:滑动窗口是对计数器方式的改进,增加一个时间粒度的度量单位,把一分钟分成若干等分(6 份,每份 10 秒),在每一份上设置独立计数器,在 00:00-00:09 之间发生请求计数器累加 1。当等分数量越大限流统计就越详细。
以下是一个滑动窗口实现限流的Java示例代码,该示例将一分钟分成若干等分,通过ConcurrentLinkedQueue来记录请求时间,从而实现对请求的限流控制。
import java.util.Iterator;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.IntStream;public class TimeWindow {private ConcurrentLinkedQueue<Long> queue = new ConcurrentLinkedQueue<>();/*** 间隔秒数*/private int seconds;/*** 最大限流*/private int max;public TimeWindow(int max, int seconds) {this.seconds = seconds;this.max = max;/*** 永续线程执行清理queue 任务*/new Thread(() -> {while (true) {try {// 等待 间隔秒数-1 执行清理操作Thread.sleep((seconds - 1) * 1000L);} catch (InterruptedException e) {e.printStackTrace();}clean();}}).start();}/*** 获取令牌,并且添加时间*/public void take() {long start = System.currentTimeMillis();synchronized (queue) {int size = sizeOfValid();if (size > max) {System.err.println("超限");}queue.offer(System.currentTimeMillis());}System.out.println("queue中有 " + queue.size() + " 最大数量 " + max);}/*** 计算有效请求数量*/private int sizeOfValid() {long currentTime = System.currentTimeMillis();Iterator<Long> iterator = queue.iterator();while (iterator.hasNext()) {long time = iterator.next();if (currentTime - time > seconds * 1000L) {iterator.remove();}}return queue.size();}/*** 清理过期的请求时间*/private void clean() {long currentTime = System.currentTimeMillis();Iterator<Long> iterator = queue.iterator();while (iterator.hasNext()) {long time = iterator.next();if (currentTime - time > seconds * 1000L) {iterator.remove();}}}public static void main(String[] args) throws Exception {final TimeWindow timeWindow = new TimeWindow(10, 1);// 测试3个线程IntStream.range(0, 3).forEach((i) -> {new Thread(() -> {while (true) {try {Thread.sleep(new Random().nextInt(20) * 100);} catch (InterruptedException e) {e.printStackTrace();}timeWindow.take();}}).start();});}
}
在上述代码中:
TimeWindow
类实现了滑动窗口限流的逻辑。take
方法用于获取令牌(即处理请求),在方法内部会检查当前有效请求数量是否超过最大限流数量,若超过则打印超限信息,同时将当前请求时间添加到队列中。sizeOfValid
方法用于计算当前有效的请求数量,会移除过期的请求时间。clean
方法用于清理过期的请求时间,通过一个单独的线程定时执行清理操作,以保证队列中只保留有效的请求时间。main
方法用于模拟多个线程发送请求,测试滑动窗口限流的效果。
你可以根据实际需求调整max
(最大限流数量)和seconds
(时间间隔)的值来满足不同的限流要求。
3.3 Leaky Bucket 漏桶
实现方式:规定固定容量的桶,有水进入,有水流出。对于流进的水我们无法估计进来的数量、速度,对于流出的水我们可以控制速度。
以下是一个使用Java实现的Leaky Bucket(漏桶)算法进行限流的示例代码:
public class LeakyBucket {// 时间(单位:毫秒)private long time;// 漏桶的总量private double total;// 水流出的速度(单位:个/毫秒)private double rate;// 当前桶内的数量private double nowSize;public LeakyBucket(double total, double rate) {this.total = total;this.rate = rate;this.nowSize = 0;this.time = System.currentTimeMillis();}// 判断是否允许请求通过public boolean limit() {long now = System.currentTimeMillis();// 根据时间计算当前桶内的数量(考虑水流出的情况)nowSize = Math.max(0, nowSize - (now - time) * rate);time = now;if (nowSize + 1 <= total) {nowSize++;return true;} else {return false;}}public static void main(String[] args) {// 创建一个漏桶实例,总量为10,流出速度为0.1个/毫秒LeakyBucket leakyBucket = new LeakyBucket(10, 0.1);// 模拟一系列请求for (int i = 0; i < 20; i++) {boolean allowed = leakyBucket.limit();System.out.println("请求 " + (i + 1) + " 是否被允许:" + allowed);try {// 模拟请求间隔Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}
}
在上述代码中:
LeakyBucket
类包含了漏桶的属性,如总量total
、流出速度rate
、当前数量nowSize
和时间time
。- 构造函数用于初始化漏桶的总量和流出速度。
limit
方法根据当前时间和流出速度计算桶内剩余数量,并判断是否允许新的请求进入(即桶是否已满)。如果桶内剩余空间足够,则允许请求通过,并更新桶内数量;否则,拒绝请求。main
方法模拟了一系列请求,并调用limit
方法判断每个请求是否被允许,同时模拟了请求之间的间隔时间。
3.4 令牌桶Token Bucket
实现方式:规定固定容量的桶, token 以固定速度往桶内填充, 当桶满时 token 不会被继续放入, 每过来一个请求把 token 从桶中移除, 如果桶中没有 token 不能请求
以下是一个令牌桶算法实现限流的 Java 示例代码,模拟了令牌桶的基本功能,包括令牌的生成和请求的处理:
import java.util.concurrent.TimeUnit;public class TokenBucket {// 桶的容量private final double capacity;// 令牌生成速度(每秒生成的令牌数)private final double rate;// 当前桶中的令牌数量private double tokens;// 上一次添加令牌的时间private long lastUpdateTime;public TokenBucket(double capacity, double rate) {this.capacity = capacity;this.rate = rate;this.tokens = capacity;this.lastUpdateTime = System.currentTimeMillis();}// 获取令牌,如果有足够的令牌则返回true,否则返回falsepublic synchronized boolean tryConsume(int tokensToConsume) {// 更新当前桶中的令牌数量updateTokens();if (tokens >= tokensToConsume) {tokens -= tokensToConsume;return true;}return false;}// 更新桶中的令牌数量,根据时间计算新生成的令牌private void updateTokens() {long now = System.currentTimeMillis();// 计算从上次更新到现在的时间差(秒)double timeDiff = (now - lastUpdateTime) / 1000.0;// 计算新生成的令牌数量double newTokens = timeDiff * rate;tokens = Math.min(capacity, tokens + newTokens);lastUpdateTime = now;}public static void main(String[] args) {// 创建一个容量为100,令牌生成速度为20(每秒生成20个令牌)的令牌桶TokenBucket bucket = new TokenBucket(100, 20);// 模拟多次请求for (int i = 0; i < 10; i++) {if (bucket.tryConsume(10)) {System.out.println("请求成功,剩余令牌: " + bucket.tokens);} else {System.out.println("请求失败,剩余令牌: " + bucket.tokens);}try {// 模拟请求间隔TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}}
}
在上述代码中:
TokenBucket
类表示令牌桶,包含桶的容量、令牌生成速度、当前令牌数量和上次更新时间等属性。tryConsume
方法用于尝试消耗指定数量的令牌,如果桶中有足够的令牌则消耗并返回true
,否则返回false
。updateTokens
方法根据时间计算新生成的令牌数量,并更新当前桶中的令牌数量,确保不超过桶的容量。- 在
main
方法中,创建了一个令牌桶实例,并模拟了多次请求,每次请求消耗一定数量的令牌,并根据结果输出相应的信息。
这样的实现可以帮助控制请求的速率,模拟令牌桶限流的基本功能。
Spring Cloud Gateway官方提供了
RequestRateLimiterGatewayFilterFactory
过滤器工厂,使用的就是 Redis 和Lua脚本实现了令牌桶的方式。
4 Redis和Lua脚本介绍
Redis主要用于缓存,支持多种数据结构,如字符串(String)、散列(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)、位图(Bitmap)、HyperLogLog和地理空间索引(Geospatial)等。
Lua是一种轻量级、嵌入式脚本语言,常用于游戏开发、脚本编程和嵌入式系统。Redis从2.6版本开始支持Lua脚本,可通过EVAL命令执行。
Lua脚本与MySQL数据库的存储过程类似,执行一组命令时,要么全部成功要么全部失败,保证了操作的原子性,可理解为一段具有业务逻辑的代码块。
5 Redis和Lua脚本的结合优势
- 原子性:Lua脚本在Redis中原子执行,运行期间不会有其他命令插入,保证操作的原子性。
- 减少网络开销:将多条Redis命令封装在Lua脚本中,减少客户端与服务器之间的网络通信次数,提高性能。
- 复杂操作:能在脚本中实现复杂逻辑操作,解决Redis原生命令繁琐的问题。
6 Lua脚本示例
以下是一个简单的Lua脚本示例,用于原子性地将一个键的值加1,并返回新的值,可通过EVAL命令在Redis中执行该脚本。
-- Lua脚本:将键的值加1
local current = redis.call("GET", KEYS[1])
if not current thencurrent = 0
elsecurrent = tonumber(current)
end
current = current + 1
redis.call("SET", KEYS[1], current)
return current
- 执行该脚本
EVAL "local current = redis.call('GET', KEYS[1]) if not current then current = 0 else current = tonumber(current) end current = current + 1 redis.call('SET', KEYS[1], current) return current" 1 mykey
7 常见使用场景
- 分布式锁:利用Lua脚本实现分布式锁,确保锁操作的原子性。
- 计数器限流:使用Lua脚本实现精确的计数器进行限流,避免并发问题。
- 复杂事务:在Lua脚本中处理多步事务,保证操作的完整性。
8 PmHub项目中的限流实战
在PmHub项目中,Redis限流作为网关限流的补充,针对异常频繁访问场景以及可能绕过网关认证的场景设置自定义限流逻辑。具体实现步骤如下:
8.1 定义限流配置
在RedisConfig中注入限流配置,脚本通过检查和递增指定键的值,并在首次递增时设置过期时间,实现指定时间窗口内限制请求次数的功能。
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisConfig extends CachingConfigurerSupport
{@Bean@SuppressWarnings(value = {"unchecked", "rawtypes"})public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}@Beanpublic DefaultRedisScript<Long> limitScript() {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(limitScriptText());redisScript.setResultType(Long.class);return redisScript;}/*** 限流脚本*/private String limitScriptText() {return "local key = KEYS[1]\n" +"local count = tonumber(ARGV[1])\n" +"local time = tonumber(ARGV[2])\n" +"local current = redis.call('get', key);\n" +"if current and tonumber(current) > count then\n" +" return tonumber(current);\n" +"end\n" +"current = redis.call('incr', key)\n" +"if tonumber(current) == 1 then\n" +" redis.call('expire', key, time)\n" +"end\n" +"return tonumber(current);";}
}
8.2 限流注解
自定义注解RateLimiter,方便在需要限流的方法中直接添加。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {/*** 限流key*/public String key() default CacheConstants.RATE_LIMIT_KEY;/*** 限流时间,单位秒*/public int time() default 60;/*** 限流次数*/public int count() default 100;/*** 限流类型*/public LimitType limitType() default LimitType.DEFAULT;
}
8.3 AOP切面类逻辑
自定义AOP切面控制类RateLimiterAspect
,进行限流逻辑处理以及降级提醒。代码位置:com.laigeoffer.pmhub.base.core.aspectj.RateLimiterAspect
@Aspect
@Component
public class RateLimiterAspect {// 日志记录器private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);// Redis操作模板(用于执行限流计数)private RedisTemplate<Object, Object> redisTemplate;// Redis限流脚本(LUA脚本实现原子操作)private RedisScript<Long> limitScript;// 注入自定义的RedisTemplate(支持复杂对象序列化)@Autowiredpublic void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate) {this.redisTemplate = redisTemplate;}// 注入预定义的限流LUA脚本@Autowiredpublic void setLimitScript(RedisScript<Long> limitScript) {this.limitScript = limitScript;}// 限流切面核心逻辑(在方法执行前进行流量控制)@Before("@annotation(rateLimiter)")public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {// 获取注解参数:时间窗口(秒)和最大允许请求数int time = rateLimiter.time();int count = rateLimiter.count();// 生成复合缓存key(包含方法签名和客户端IP)String combineKey = getCombineKey(rateLimiter, point);List<Object> keys = Collections.singletonList(combineKey);try {// 执行LUA脚本(原子操作:计数+过期时间设置)Long number = redisTemplate.execute(limitScript, keys, count, time);// 请求数超过阈值时抛出限流异常if (StringUtils.isNull(number) || number.intValue() > count) {throw new ServiceException("访问过于频繁,请稍候再试");}// 记录限流日志(生产环境建议改为DEBUG级别)log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);} catch (ServiceException e) {throw e; // 直接抛出已知业务异常} catch (Exception e) {// 将系统异常转换为业务异常,避免泄露技术细节throw new RuntimeException("服务器限流异常,请稍候再试");}}// 构建全局唯一的限流key(格式:注解key + [IP] + 类名 + 方法名)public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());// 按IP限流时追加客户端IP地址if (rateLimiter.limitType() == LimitType.IP) {stringBuffer.append(IpUtils.getIpAddr(ServletUtils.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();}
}
由于该类定义在公共包core下,需手动加入给spring管理。
8.4 限流具体使用场景
在PmHub的登录接口添加二道防线,通过自定义限流措施保障系统安全。使用时,通过注解自定义时间和访问控制数。代码路径:com.laigeoffer.pmhub.auth.controller.LoginController#login
/*** 登录接口,因为登录接口无token,所以不走网关鉴权,且安全级别极高* 需要自定义Redis限流逻辑* 这里配置了 30 秒内仅允许访问 10 次* @param form* @return*/
@RateLimiter(key = "rate_limit:login", time = 30, count = 10)
@PostMapping("login")
public AjaxResult login(@RequestBody LoginBody form) {AjaxResult ajax = success();// 用户登录LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());// 获取登录tokenString token = tokenService.createToken(userInfo);ajax.put(Constants.TOKEN, token);return ajax;
}
9 JMeter压测过程
9.1 下载安装
借助JMeter工具模拟并发,从指定地址下载二进制形式的压缩包并解压。
下载地址:Jmeter官方下载地址
9.2 启动
进入jmeter的bin文件夹,运行jmeter.sh(windows系统运行jmeter.bat)。
9.3 简单测试
在pmhub-auth模块的LoginController接口下新增测试接口,设置1秒发送10个请求,添加http请求,点击开始测试,保存测试结果,查看结果树获取请求结果信息。
//@RateLimiter(key = "limitTest", time = 10, count = 2)
@PostMapping(value = "/limitTest")
public Long limitTest() {System.out.println("limitTest");return 1L;
}
9.4 模拟接口压测
设置线程组1秒10个请求,限流逻辑配置为接口10s只允许2个请求数,发送请求并查看日志,验证限流配置是否成功。
@RateLimiter(key = "limitTest", time = 10, count = 2)@PostMapping(value = "/limitTest")public Long limitTest() {System.out.println("limitTest");return 1L;}
9.5 真实登录接口限流
在登录接口中配置限流注解,如配置30秒内仅允许访问10次。发送post请求,设置json格式请求参数,分别进行正常情况(1秒发送9个请求,未超过阈值)和异常情况(1秒发送11个请求,超过阈值)的压测,查看JMeter返回数据和控制台日志,验证限流目的是否达到。
/*** 登录接口,因为登录接口无token,所以不走网关鉴权,且安全级别极高* 需要自定义Redis限流逻辑* 这里配置了 30 秒内仅允许访问 10 次* @param form* @return*/
@RateLimiter(key = "rate_limit:login", time = 30, count = 10)
@PostMapping("login")
public AjaxResult login(@RequestBody LoginBody form) {AjaxResult ajax = success();// 用户登录LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());// 获取登录tokenString token = tokenService.createToken(userInfo);ajax.put(Constants.TOKEN, token);return ajax;
}
说明达到了限流的目的。
10 总结
通过以上在PmHub项目中基于Redis加Lua脚本的计数器算法限流实现以及JMeter压测验证,确保了系统在高并发场景下的稳定性和安全性,有效防止了系统因过载而出现问题。
11 参考链接
- PmHub实现Redis加Lua脚本基于计数器算法的限流
- 项目仓库(GitHub)
- 项目仓库(码云):(国内访问速度更快)
相关文章:
【PmHub后端篇】PmHub中基于Redis加Lua脚本的计数器算法限流实现
1 限流的重要性 在高并发系统中,保护系统稳定运行的关键技术有缓存、降级和限流。 缓存通过在内存中存储常用数据,减少对数据库的访问,提升系统响应速度,如浏览器缓存、CDN缓存等多种应用层面。降级则是在系统压力过大或部分服务…...
【递归、搜索与回溯】专题一:递归(二)
📝前言说明: 本专栏主要记录本人递归,搜索与回溯算法的学习以及LeetCode刷题记录,按专题划分每题主要记录:(1)本人解法 本人屎山代码;(2)优质解法 优质代码…...
【Linux】操作系统入门:冯诺依曼体系结构
引言:从一次QQ聊天说起 你是否好奇,当你在键盘上敲下一行文字发送给好友时,计算机内部发生了什么?为什么鼠标点击后程序就能瞬间响应?这一切的答案,都藏在计算机的“心脏”——冯诺依曼体系结构中。 一、硬…...
量化感知训练与 PyTorch 的哪些事
大家好呀!今天咱们要来聊聊一个超厉害的技术——量化感知训练(Quantization-Aware Training,简称 QAT) 在神经网络的世界里,我们总是想方设法地让模型变得更准确、更高效,毕竟谁不想自己的模型在边缘设备上…...
【Mac 从 0 到 1 保姆级配置教程 15】- Python 环境一键安装与配置,就是这么的丝滑
文章目录 前言安装 Python 环境VSCode 配置Python 环境NeoVim 配置 Python 环境(选看)1. Python LSP 配置2. 打开 python 语言支持 最后参考资料系列教程 Mac 从 0 到 1 保姆级配置教程目录,点击即可跳转对应文章: 【Mac 从 0 到 …...
前端学习(3)—— CSS实现热搜榜
效果展示 具体的展示效果如下,可以直接在浏览器显示: 页面分为两部分,一部分是 body 标签里的 html 结构,一部分是 style 标签里的CSS代码(页面布局的部分数据直接在代码里显示了) 一,html结…...
大数据——解决Matplotlib 字体不足问题(Linux\mac\windows)
1、将下载好的字体文件放到文件夹中 谷歌官方字体 import matplotlib print(matplotlib.matplotlib_fname())cp NotoSansSC-Regular.ttf /data/home/miniconda3/envs/python3128/lib/python3.12/site-packages/matplotlib/mpl-data/fonts/ttf/cp wqy-zenhei.ttc /data/home/m…...
嵌入式培训之数据结构学习(二)顺序表与单向链表
目录 一、顺序表 (一)顺序表的基本操作 1、创建顺序表 2、销毁顺序表 3、遍历顺序表 4、尾插,在顺序表的最后插入元素 5、判断表是否满 6、判断表是否空 7、按指定位置插入元素 8、查找元素,根据名字 9、根据名字修改指…...
PyInstaller 打包后 Excel 转 CSV 报错解决方案:“excel file format cannot be determined“
一、问题背景 在使用 Python 开发 Excel 转 CSV 工具时,直接运行脚本(python script.py)可以正常工作,但通过 PyInstaller 打包成可执行文件后,出现以下报错: excel file format cannot be determined, you must specify an engine manually 该问题通常发生在使用pandas…...
鸿蒙 PC 发布之后,想在技术上聊聊它的未来可能
最近鸿蒙 PC 刚发布完,但是发布会没公布太多技术细节,基本上一些细节都是通过自媒体渠道获取,首先可以确定的是,鸿蒙 PC 本身肯定是无法「直接」运行 win 原本的应用,但是可以支持手机上「原生鸿蒙」的应用,…...
HarmonyOS 【诗韵悠然】AI古诗词赏析APP开发实战从零到一系列(一、开篇,项目介绍)
诗词,作为中国传统文化的瑰宝,承载着中华民族几千年的思想智慧和审美情趣。然而,在现代社会快节奏的生活压力下,诗词文化却逐渐被忽视,更多的人感到诗词艰涩深奥,难以亲近。与此同时,虽然市场上…...
实物工厂零件画图案例(上)
文章目录 滑台气缸安装板旋转气缸安装板张紧调节块长度调节块双轴气缸安装板步进电机安装板梯形丝杆轴承座 简介:案例点击此处下载,这次的这几个案例并没有很大的难度,练习这几个案例最为重要的一点就是知道:当你拿到一个实物的时…...
js中的同步方法及异步方法
目录 1.代码说明 2.async修饰的方法和非async修饰的方法的区别 3.不使用await的场景 4.总结 1.代码说明 const saveTem () > {// 校验处理const res check()if (!res) {return}addTemplateRef.value.openModal() } 这段代码中,check方法返回的是true和fal…...
C 语言_基础语法全解析_深度细化版
一、C 语言基本结构 1.1 程序组成部分 一个完整的 C 程序由以下部分组成: 预处理指令:以#开头,在编译前处理 #include <stdio.h> // 引入标准库 #define PI 3.14159 // 定义常量全局变量声明:在所有函数外部定义的变量 int globalVar = 10; // 全局变量函数定义…...
【Linux系列】dd 命令的深度解析与应用实践
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...
ETL背景介绍_1:数据孤岛仓库的介绍
1 ETL介绍 1.1 数据孤岛 随着企业内客户数据大量的涌现,单个数据库已不再足够。为了储存这些数据,公司通常会建立多个业务部门组织的数据库来保存数据。比如,随着数据量的增长,公司通常可能会构建数十个独立运行的业务数据库&am…...
【周输入】510周阅读推荐-1
本号一年了,有一定的成长,也有很多读者和点赞。自觉更新仍然远远不够,需要继续努力。 但是还是要坚持2点: 在当前这个时代,信息大爆炸,层次不齐,不追加多, 信息输入可以很多&#x…...
Games101作业四
作业0到作业3的代码 这次是实现 de Casteljau 算法,以及绘制 Bezier 曲线,比上次简单 核心思想就是递归,原理忘了就去看第十一节课,从15:00开始的 GAMES101-现代计算机图形学入门-闫令琪 代码 先实现贝塞尔曲线 cv::Point2f recursive_bezier(const std::…...
从Aurora 架构看数据库计算存储分离架构
单就公有云来说,现在云数据面临的挑战有以下 5 个: 跨 AZ 的可用性与数据安全性。 现在都提多 AZ 部署,亚马逊在全球有 40 多个 AZ, 16 个 Region,基本上每一个 Region 之内的那些关键服务都是跨 3 个 AZ。你要考虑整个…...
ElasticSearch深入解析(十一):分页
在Elasticsearch中,常用的分页方案有from size、search_after和scroll三种,适用于不同场景。from size基于偏移量分页,是全局排序后的切片查询,适用于小数据量、浅分页场景,但深度分页性能差,且有默认上限…...
【MySQL】MySQL数据库结构与操作
目录 一. 数据库的概念 二. 数据库的分类 三. 初始MySQL数据库 四. 数据库操作 1)创建数据库 2) 查看数据库 3)选中数据库 4)删除数据库 五. SQL数据类型 1)整型和浮点型 2)字符串类型 3)时间…...
Vue框架的基本介绍
目录 一.Vue 1.概述 2.三大主流框架 3.优点: 二.Vue搭建 三.语法 1.基本框架 2.插值表达式 3.Vue指令 1.v-text: 2.v-html: 编辑3.v-model: 4.v-on: 5.v-show: 6.v-if: 7.v-else: 8.v-bind: 9.v-for: 一.Vue 1.概述 Vue是一款用于构建用户界面的渐进式的…...
Web 架构之攻击应急方案
文章目录 一、引言二、常见 Web 攻击类型及原理2.1 SQL 注入攻击2.2 跨站脚本攻击(XSS)2.3 分布式拒绝服务攻击(DDoS) 三、攻击检测3.1 日志分析3.2 入侵检测系统(IDS)/入侵防御系统(IPS&#x…...
xss-labs靶场基础8-10关(记录学习)
前言: 内容: 第八关 关卡资源网站,html编码网站(两个网站,一个是实体编号转义(只对特殊字符有效,字母无效)、实体符号转义) 在线Html实体编码解码-HTML Entity Encodi…...
arctanx 导数 泰勒展开式证明
你提供的推导内容非常清晰,条理分明。下面是对 d d x arctan x 1 1 x 2 \frac{d}{dx} \arctan x \frac{1}{1 x^2} dxdarctanx1x21 的总结与适当补充: ✅ 结论 d d x arctan x 1 1 x 2 \frac{d}{dx} \arctan x \frac{1}{1 x^2} dxda…...
基于Java的家政服务平台设计与实现(代码+数据库+LW)
摘 要 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本家政服务平台就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息&a…...
SpringBoot的外部化配置
一、什么是外部化配置 外部化配置是指把应用程序中各种可配置的参数、属性等信息,从代码内部提取出来,放置在外部的配置文件、数据库或配置中心等地方(比如使用.properties、.yml 或.xml 等格式的文件)进行管理。提高应用程序的可…...
Java鼠标事件监听器MouseListener、MouseMotionListener和MouseWheelListener
Java鼠标事件监听器MouseListener、MouseMotionListener和MouseWheelListener java中创建鼠标,键盘的事件行为监听器的几种方法 这里以鼠标点击事件监听器为例,其他也是一样创建。 常用的消息监听器对象 1:点击事件监听器 ActionListener 2:按键事件监…...
第三方支付公司如何代付和入账?
通俗来说,就是企业把钱打到第三方公司账户上,再由第三方公司把钱打入客户指定账户。 那么第三方支付入账流程是怎样的呢? 第一,企业向第三方支付公司指定账户充值打款;第二,企业提交代付银行卡信息后台操…...
.NET8关于ORM的一次思考
文章目录 前言一、思路二、实现ODBC>SqlHelper.cs三、数据对象实体化四、SQL生成SqlBuilder.cs五、参数注入 SqlParameters.cs六、反射 SqlOrm.cs七、自定义数据查询八、总结 前言 琢磨着在.NET8找一个ORM,对比了最新的框架和性能。 框架批量操作性能SQL控制粒…...
LlamaIndex 第八篇 MilvusVectorStore
本指南演示了如何使用 LlamaIndex 和 Milvus 构建一个检索增强生成(RAG)系统。 RAG 系统将检索系统与生成模型相结合,根据给定的提示生成新的文本。该系统首先使用 Milvus 等向量相似性搜索引擎从语料库中检索相关文档,然后使用生…...
记录为什么LIst数组“增删慢“,LinkedList链表“查改快“?
数组(Array) 增删慢:对于数组来说,增加或删除元素的操作可能会比较慢,特别是当你需要在数组的开头或中间进行这些操作时。这是因为这些操作通常需要移动数组中的其他元素以保持连续性。例如,如果你想要在数…...
【论文阅读】Dip-based Deep Embedded Clustering with k-Estimation
摘要 近年来,聚类与深度学习的结合受到了广泛关注。无监督神经网络,如自编码器,能够自主学习数据集中的关键结构。这一思想可以与聚类目标结合,实现对相关特征的自动学习。然而,这类方法通常基于 k-means 框架&#x…...
ARFoundation 图片识别,切换图片克隆不同的追踪模型
场景搭建: 你可以把我的代码发给AI,去理解 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.XR; using UnityEngine.XR.ARFoundation; using UnityEngine.XR.ARSubsystems; using TMPro; using Unit…...
鸿蒙next播放B站视频横屏后的问题
(此文讨论范围为b站视频链接,且不包括b站直播链接;android/iOS的webview播放b站视频完全没有这么多问题) 1、竖屏播放没问题 从一个竖屏页p1点击进入视频页p2,p2页仍为竖屏; p2页有一Web组件,…...
华为0507机试
题目二 建设基站 有一棵二叉树,每个节点上都住了一户居民。现在要给这棵树上的居民建设基站,每个基站只能覆盖她所在与相邻的节点,请问信号覆盖这棵树最少需要建设多少个基站 #include <bits/stdc.h> using namespace std;const int …...
apache2的默认html修改
使用127.0.0.1的时候,默认打开的是index.html,可以通过配置文件修改成我们想要的html vi /etc/apache2/mods-enabled/dir.conf <IfModule mod_dir.c>DirectoryIndex WS.html index.html index.cgi index.pl index.php index.xhtml index.htm <…...
EXCEL下拉菜单与交替上色设置
Excel/WPS 表格操作教程(双功能整合) 目录 功能一:交替行上色 Excel 操作WPS 操作 功能二:下拉菜单设置 Excel 操作WPS 操作 组合效果示例注意事项 功能一:交替行上色 Excel 操作 选中数据区域 拖动鼠标选择需要设置…...
list基础用法
list基础用法 1.list的访问就不能用下标[]了,用迭代器2.emplace_back()几乎是与push_back()用法一致,但也有差别3.insert(),erase()的用法4.reverse()5.排序6.合并7.unique()(去重)8.splice剪切再粘贴 1.list的访问就不能用下标[]了,用迭代器…...
鸿蒙PC版体验_画面超级流畅_具备terminal_无法安装windows、linux软件--纯血鸿蒙HarmonyOS5.0工作笔记017
鸿蒙NEXT和开源鸿蒙OpenHarmony现在已经开发实现统一,使用鸿蒙ArkTS开发的应用,可以直接 在开源鸿蒙上. 鸿蒙的terminal是使用的linux的语法,但是有很多命令,目前还不能使用,常用的ifconfig等是可以用的. 鸿蒙终于出来PC版了,虽然,不像Windows以及mac等,开放的命令那么多,但…...
Spring 集成 SM4(国密对称加密)
Spring 集成 SM4(国密对称加密)算法 主要用于保护敏感数据,如身份证、手机号、密码等。 下面是完整集成步骤(含工具类 使用示例),采用 Java 实现(可用于 Spring Boot)。 一、依赖引…...
deepseek梳理java高级开发工程师微服务面试题
Java微服务高级面试题与答案 一、微服务架构设计 1. 服务拆分原则 Q1:微服务拆分时有哪些核心原则?如何解决拆分后的分布式事务问题? 答案: 服务拆分五大原则: 1. 单一职责原则(SRP)- 每个…...
人事管理系统8
员工管理(分页查询、查看详情页、修改): 1. 分页查询 Staff.java 中加入部门名和岗位名两个属性以及对应的 get 和 set 方法。这两个属性没有数据库字段对应, 仅供前端显示用: private String departname; //部门名属…...
Stapi知识框架
一、Stapi 基础认知 1. 框架定位 自动化API开发框架:专注于快速生成RESTful API 约定优于配置:通过标准化约定减少样板代码 企业级应用支持:适合构建中大型API服务 代码生成导向:显著提升开发效率 2. 核心特性 自动CRUD端点…...
第三章 初始化配置(一)
我们首先介绍配置Logback的方法,并提供了许多示例配置脚本。在后面的章节中,我们将介绍Logback所依赖的配置框架Joran。 初始化配置 在应用程序代码中插入日志请求需要大量的规划和努力。观察表明,大约4%的代码用于记录。因此,即…...
WebGIS 开发中的数据安全与隐私保护:急需掌握的要点
在 WebGIS 开发中,数据安全与隐私保护是绝对不能忽视的问题!随着地理信息系统的广泛应用,越来越多的敏感数据被存储和传输,比如个人位置信息、企业地理资产等。一旦这些数据泄露,后果不堪设想。然而,很多开…...
C语言 ——— 函数栈帧的创建和销毁
目录 寄存器 mian 函数是被谁调用的 通过汇编了解函数栈帧的创建和销毁 转汇编后(Add函数之前的部分) 转汇编后(进入Add函数之前的部分) 转汇编后(正式进入Add函数的部分) 编辑 总结 局部变量…...
2025年真实面试问题汇总(二)
jdbc的事务是怎么开启的 在JDBC中,事务的管理是通过Connection对象控制的。以下是开启和管理事务的详细步骤: 1. 关闭自动提交模式 默认情况下,JDBC连接处于自动提交模式(auto-commit true),即每条SQL语…...
【用「概率思维」重新理解生活】
用「概率思维」重新理解生活:为什么你总想找的「确定答案」并不存在? 第1层:生活真相——所有结果都是「综合得分」 现象:我们总想找到“孩子生病是因为着凉”或“伴侣生气是因为那句话”的单一答案现实:每个结果背后…...
Redis——线程模型·
为什么Redis是单线程却仍能有10w/秒的吞吐量? 内存操作:Redis大部分操作都在内存中完成,并且采用了高效的数据结构,因此Redis的性能瓶颈可能是机器的内存或者带宽,而非CPU,既然CPU不是瓶颈,自然…...