接口防刷方案

news2024/11/15 15:58:39

1、前言

本文为描述通过Interceptor以及Redis实现接口访问防刷Demo

2、原理

  • 通过ip地址+uri拼接用以作为访问者访问接口区分

  • 通过在Interceptor中拦截请求,从Redis中统计用户访问接口次数从而达到接口防刷目的

如下图所示

3、案例工程

项目地址:

https://github.com/Tonciy/interface-brush-protection

Apifox地址:Apifox 密码:Lyh3j2Rv

其中,Interceptor处代码处理逻辑最为重要

/**  
 * @description: 接口防刷拦截处理  
 */  
@Slf4j  
public class AccessLimintInterceptor  implements HandlerInterceptor {  
    @Resource  
    private RedisTemplate<String, Object> redisTemplate;  
  
    /**  
     * 多长时间内  
     */  
    @Value("${interfaceAccess.second}")  
    private Long second = 10L;  
  
    /**  
     * 访问次数  
     */  
    @Value("${interfaceAccess.time}")  
    private Long time = 3L;  
  
    /**  
     * 禁用时长--单位/秒  
     */  
    @Value("${interfaceAccess.lockTime}")  
    private Long lockTime = 60L;  
  
    /**  
     * 锁住时的key前缀  
     */  
    public static final String LOCK_PREFIX = "LOCK";  
  
    /**  
     * 统计次数时的key前缀  
     */  
    public static final String COUNT_PREFIX = "COUNT";  
  
  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
  
        String uri = request.getRequestURI();  
        String ip = request.getRemoteAddr(); // 这里忽略代理软件方式访问,默认直接访问,也就是获取得到的就是访问者真实ip地址  
        String lockKey = LOCK_PREFIX + ip + uri;  
        Object isLock = redisTemplate.opsForValue().get(lockKey);  
        if(Objects.isNull(isLock)){  
            // 还未被禁用  
            String countKey = COUNT_PREFIX + ip + uri;  
            Object count = redisTemplate.opsForValue().get(countKey);  
            if(Objects.isNull(count)){  
                // 首次访问  
                log.info("首次访问");  
                redisTemplate.opsForValue().set(countKey,1,second, TimeUnit.SECONDS);  
            }else{  
                // 此用户前一点时间就访问过该接口  
                if((Integer)count < time){  
                    // 放行,访问次数 + 1  
                    redisTemplate.opsForValue().increment(countKey);  
                }else{  
                    log.info("{}禁用访问{}",ip, uri);  
                    // 禁用  
                    redisTemplate.opsForValue().set(lockKey, 1,lockTime, TimeUnit.SECONDS);  
                    // 删除统计  
                    redisTemplate.delete(countKey);  
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);  
                }  
            }  
        }else{  
            // 此用户访问此接口已被禁用  
            throw new CommonException(ResultCode.ACCESS_FREQUENT);  
        }  
        return true;  
    }  
}  

在多长时间内访问接口多少次,以及禁用的时长,则是通过与配置文件配合动态设置

当处于禁用时直接抛异常则是通过在ControllerAdvice处统一处理 (这里代码写的有点丑陋)

下面是一些测试(可以把项目通过Git还原到“【初始化】”状态进行测试)

  • 正常访问时

  • 访问次数过于频繁时

4、自我提问

上述实现就好像就已经达到了我们的接口防刷目的了

但是,还不够

为方便后续描述,项目中新增补充Controller,如下所示

简单来说就是

  • PassCotrollerRefuseController

  • 每个Controller分别有对应的get,post,put,delete类型的方法,其映射路径与方法名称一致

接口问题

  • 对于上述实现,不知道你们有没有发现一个问题

  • 对于上述实现问题比如现在我们的接口防刷处理,针对是所有的接口(项目案例中我只是写的接口比较少)

  • 而在实际开发中,说对于所有的接口都要做防刷处理,感觉上也不太可能(写此文时目前大四,实际工作经验较少,这里不敢肯定)

  • 那么问题有了,该如何解决呢?目前来说想到两个解决方案

拦截器映射规则

项目通过Git还原到"【Interceptor设置映射规则实现接口自由】"版本即可得到此案例实现

我们都知道拦截器是可以设置拦截规则的,从而达到拦截处理目的

1.这个AccessInterfaceInterceptor是专门用来进行防刷处理的,那么实际上我们可以通过设置它的映射规则去匹配需要进行【接口防刷】的接口即可

2.比如说下面的映射配置

3.这样就初步达到了我们的目的,通过映射规则的配置,只针对那些需要进行【接口防刷】的接口才会进行处理

4.至于为啥说是初步呢?下面我就说说目前我想到的使用这种方式进行【接口防刷】的不足点:

所有要进行防刷处理的接口统一都是配置成了 x 秒内 y 次访问次数,禁用时长为 z 秒

  • 要知道就是要进行防刷处理的接口,其 x, y, z的值也是并不一定会统一的

  • 某些防刷接口处理比较消耗性能的,我就把x, y, z设置的紧一点

  • 而某些防刷接口处理相对来说比较快,我就把x, y, z 设置的松一点

  • 这没问题吧

  • 但是现在呢?x, y, z值全都一致了,这就不行了

  • 这就是其中一个不足点

  • 当然,其实针对当前这种情况也有解决方案

  • 那就是弄多个拦截器

  • 每个拦截器的【接口防刷】处理逻辑跟上述一致,并去映射对应要处理的防刷接口

  • 唯一不同的就是在每个拦截器内部,去修改对应防刷接口需要的x, y, z值

  • 这样就是感觉会比较麻烦

防刷接口映射路径修改后维护问题

  • 虽然说防刷接口的映射路径基本上定下来后就不会改变

  • 但实际上前后端联调开发项目时,不会有那么严谨的Api文档给我们用(这个在实习中倒是碰到过,公司不是很大,开发起来也就不那么严谨,啥都要自己搞,功能能实现就好)

  • 也就是说还是会有那种要修改接口的映射路径需求

  • 当防刷接口数量特别多,后面的接手人员就很痛苦了

  • 就算是项目是自己从0到1实现的,其实有时候项目开发到后面,自己也会忘记自己前面是如何设计的

  • 而使用当前这种方式的话,谁维护谁蛋疼

自定义注解 + 反射

咋说呢

  • 就是通过自定义注解中定义 x 秒内 y 次访问次数,禁用时长为 z 秒

  • 自定义注解 + 在需要进行防刷处理的各个接口方法上

  • 在拦截器中通过反射获取到各个接口中的x, y, z值即可达到我们想要的接口自由目的

下面做个实现

声明自定义注解

Controlller中方法中使用

Interceptor处逻辑修改(最重要是通过反射判断此接口是否需要进行防刷处理,以及获取到x, y, z的值)

/**   
 * @description: 接口防刷拦截处理  
 */  
@Slf4j  
public class AccessLimintInterceptor  implements HandlerInterceptor {  
    @Resource  
    private RedisTemplate<String, Object> redisTemplate;  
    /**  
     * 锁住时的key前缀  
     */  
    public static final String LOCK_PREFIX = "LOCK";  
  
    /**  
     * 统计次数时的key前缀  
     */  
    public static final String COUNT_PREFIX = "COUNT";  
  
  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
//        自定义注解 + 反射 实现  
        // 判断访问的是否是接口方法  
        if(handler instanceof HandlerMethod){  
            // 访问的是接口方法,转化为待访问的目标方法对象  
            HandlerMethod targetMethod = (HandlerMethod) handler;  
            // 取出目标方法中的 AccessLimit 注解  
            AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);  
            // 判断此方法接口是否要进行防刷处理(方法上没有对应注解就代表不需要,不需要的话进行放行)  
            if(!Objects.isNull(accessLimit)){  
                // 需要进行防刷处理,接下来是处理逻辑  
                String ip = request.getRemoteAddr();  
                String uri = request.getRequestURI();  
                String lockKey = LOCK_PREFIX + ip + uri;  
                Object isLock = redisTemplate.opsForValue().get(lockKey);  
                // 判断此ip用户访问此接口是否已经被禁用  
                if (Objects.isNull(isLock)) {  
                    // 还未被禁用  
                    String countKey = COUNT_PREFIX + ip + uri;  
                    Object count = redisTemplate.opsForValue().get(countKey);  
                    long second = accessLimit.second();  
                    long maxTime = accessLimit.maxTime();  
  
                    if (Objects.isNull(count)) {  
                        // 首次访问  
                        log.info("首次访问");  
                        redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);  
                    } else {  
                        // 此用户前一点时间就访问过该接口,且频率没超过设置  
                        if ((Integer) count < maxTime) {  
                            redisTemplate.opsForValue().increment(countKey);  
                        } else {  
  
                            log.info("{}禁用访问{}", ip, uri);  
                            long forbiddenTime = accessLimit.forbiddenTime();  
                            // 禁用  
                            redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);  
                            // 删除统计--已经禁用了就没必要存在了  
                            redisTemplate.delete(countKey);  
                            throw new CommonException(ResultCode.ACCESS_FREQUENT);  
                        }  
                    }  
                } else {  
                    // 此用户访问此接口已被禁用  
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);  
                }  
            }  
        }  
        return  true;  
    }  
}  

由于不好演示效果,这里就不贴测试结果图片了

项目通过Git还原到"【自定义主键+反射实现接口自由"版本即可得到此案例实现,后面自己可以针对接口做下测试看看是否如同我所说的那样实现自定义x, y, z 的效果

嗯,现在看起来,可以针对每个要进行防刷处理的接口进行针对性自定义多长时间内的最大访问次数,以及禁用时长,哪个接口需要,就直接+在那个接口方法出即可

感觉还不错的样子,现在网上挺多资料也都是这样实现的

但是还是可以有改善的地方

先举一个例子,以我们的PassController为例,如下是其实现

下图是其映射路径关系

同一个Controller的所有接口方法映射路径的前缀都包含了/pass

我们在类上通过注解@ReqeustMapping标记映射路径/pass,这样所有的接口方法前缀都包含了/pass,并且以致于后面要修改映射路径前缀时只需改这一块地方即可

这也是我们使用SpringMVC最常见的用法

那么,我们的自定义注解也可不可以这样做呢?先无中生有个需求

假设PassController中所有接口都是要进行防刷处理的,并且他们的x, y, z值就一样

如果我们的自定义注解还是只能加载方法上的话,一个一个接口加,那么无疑这是一种很呆的做法

要改的话,其实也很简单,首先是修改自定义注解,让其可以作用在类上

接着就是修改AccessLimitInterceptor的处理逻辑

AccessLimitInterceptor中代码修改的有点多,主要逻辑如下

与之前实现比较,不同点在于x, y, z的值要首先尝试在目标类中获取

其次,一旦类中标有此注解,即代表此类下所有接口方法都要进行防刷处理

如果其接口方法同样也标有此注解,根据就近优先原则,以接口方法中的注解标明的值为准

/**  
 * @description: 接口防刷拦截处理  
 */  
@Slf4j  
public class AccessLimintInterceptor implements HandlerInterceptor {  
    @Resource  
    private RedisTemplate<String, Object> redisTemplate;  
  
    /**  
     * 锁住时的key前缀  
     */  
    public static final String LOCK_PREFIX = "LOCK";  
  
    /**  
     * 统计次数时的key前缀  
     */  
    public static final String COUNT_PREFIX = "COUNT";  
  
  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
  
//      自定义注解 + 反射 实现, 版本 2.0  
        if (handler instanceof HandlerMethod) {  
            // 访问的是接口方法,转化为待访问的目标方法对象  
            HandlerMethod targetMethod = (HandlerMethod) handler;  
            // 获取目标接口方法所在类的注解@AccessLimit  
            AccessLimit targetClassAnnotation = targetMethod.getMethod().getDeclaringClass().getAnnotation(AccessLimit.class);  
            // 特别注意不能采用下面这条语句来获取,因为 Spring 采用的代理方式来代理目标方法  
            //  也就是说targetMethod.getClass()获得是class org.springframework.web.method.HandlerMethod ,而不知我们真正想要的 Controller  
//            AccessLimit targetClassAnnotation = targetMethod.getClass().getAnnotation(AccessLimit.class);  
            // 定义标记位,标记此类是否加了@AccessLimit注解  
            boolean isBrushForAllInterface = false;  
            String ip = request.getRemoteAddr();  
            String uri = request.getRequestURI();  
            long second = 0L;  
            long maxTime = 0L;  
            long forbiddenTime = 0L;  
            if (!Objects.isNull(targetClassAnnotation)) {  
                log.info("目标接口方法所在类上有@AccessLimit注解");  
                isBrushForAllInterface = true;  
                second = targetClassAnnotation.second();  
                maxTime = targetClassAnnotation.maxTime();  
                forbiddenTime = targetClassAnnotation.forbiddenTime();  
            }  
            // 取出目标方法中的 AccessLimit 注解  
            AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);  
            // 判断此方法接口是否要进行防刷处理  
            if (!Objects.isNull(accessLimit)) {  
                // 需要进行防刷处理,接下来是处理逻辑  
                second = accessLimit.second();  
                maxTime = accessLimit.maxTime();  
                forbiddenTime = accessLimit.forbiddenTime();  
                if (isForbindden(second, maxTime, forbiddenTime, ip, uri)) {  
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);  
                }  
            } else {  
                // 目标接口方法处无@AccessLimit注解,但还要看看其类上是否加了(类上有加,代表针对此类下所有接口方法都要进行防刷处理)  
                if (isBrushForAllInterface && isForbindden(second, maxTime, forbiddenTime, ip, uri)) {  
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);  
                }  
            }  
        }  
        return true;  
    }  
  
    /**  
     * 判断某用户访问某接口是否已经被禁用/是否需要禁用  
     *  
     * @param second        多长时间  单位/秒  
     * @param maxTime       最大访问次数  
     * @param forbiddenTime 禁用时长 单位/秒  
     * @param ip            访问者ip地址  
     * @param uri           访问的uri  
     * @return ture为需要禁用  
     */  
    private boolean isForbindden(long second, long maxTime, long forbiddenTime, String ip, String uri) {  
        String lockKey = LOCK_PREFIX + ip + uri; //如果此ip访问此uri被禁用时的存在Redis中的 key  
        Object isLock = redisTemplate.opsForValue().get(lockKey);  
        // 判断此ip用户访问此接口是否已经被禁用  
        if (Objects.isNull(isLock)) {  
            // 还未被禁用  
            String countKey = COUNT_PREFIX + ip + uri;  
            Object count = redisTemplate.opsForValue().get(countKey);  
            if (Objects.isNull(count)) {  
                // 首次访问  
                log.info("首次访问");  
                redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);  
            } else {  
                // 此用户前一点时间就访问过该接口,且频率没超过设置  
                if ((Integer) count < maxTime) {  
                    redisTemplate.opsForValue().increment(countKey);  
                } else {  
                    log.info("{}禁用访问{}", ip, uri);  
                    // 禁用  
                    redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);  
                    // 删除统计--已经禁用了就没必要存在了  
                    redisTemplate.delete(countKey);  
                    return true;  
                }  
            }  
        } else {  
            // 此用户访问此接口已被禁用  
            return true;  
        }  
        return false;  
    }  
}  

好了,这样就达到我们想要的效果了

项目通过Git还原到"【自定义注解+反射实现接口自由-版本2.0】"版本即可得到此案例实现,自己可以测试万一下

这是目前来说比较理想的做法,至于其他做法,暂时没啥了解到

5、时间逻辑漏洞

这是我一开始都有留意到的问题

也是一直搞不懂,就是我们现在的所有做法其实感觉都不是严格意义上的x秒内y次访问次数

特别注意这个x秒,它是连续,任意的(代表这个x秒时间片段其实是可以发生在任意一个时间轴上)

我下面尝试表达我的意思,但是我不知道能不能表达清楚

假设我们固定某个接口5秒内只能访问3次,以下面例子为例

底下的小圆圈代表此刻请求访问接口

按照我们之前所有做法的逻辑走

  1. 第2秒请求到,为首次访问,Redis中统计次数为1(过期时间为5秒)

  2. 第7秒,此时有两个动作,一是请求到,二是刚刚第二秒Redis存的值现在过期

  3. 我们先假设这一刻,请求处理完后,Redis存的值才过期

  4. 按照这样的逻辑走

  5. 第七秒请求到,Redis存在对应key,且不大于3, 次数+1

  6. 接着这个key立马过期

  7. 再继续往后走,第8秒又当做新的一个起始,就不往下说了,反正就是不会出现禁用的情况

按照上述逻辑走,实际上也就是说当出现首次访问时,当做这5秒时间片段的起始

第2秒是,第8秒也是

但是有没有想过,实际上这个5秒时间片段实际上是可以放置在时间轴上任意区域的

上述情况我们是根据请求的到来情况人为的把它放在【2-7】,【8-13】上

而实际上这5秒时间片段是可以放在任意区域的

那么,这样的话,【7-12】也可以放置

而【7-12】这段时间有4次请求,就达到了我们禁用的条件了

是不是感觉怪怪的

想过其他做法,但是好像严格意义上真的做不到我所说的那样(至少目前来说想不到)

之前我们的做法,正常来说也够用,至少说有达到防刷的作用

后面有机会的话再看看,不知道我是不是钻牛角尖了

6、路径参数问题

假设现在PassController中有如下接口方法

图片

也就是我们在接口方法中常用的在请求路径中获取参数的套路

但是使用路径参数的话,就会发生问题

那就是同一个ip地址访问此接口时,我携带的参数值不同

按照我们之前那种前缀+ip+uri拼接的形式作为key的话,其实是区分不了的

下图是访问此接口,携带不同参数值时获取的uri状况

这样的话在我们之前拦截器的处理逻辑中,会认为是此ip用户访问的是不同的接口方法,而实际上访问的是同一个接口方法

也就导致了【接口防刷】失效

接下来就是解决它,目前来说有两种

  1. 不要使用路径参数

这算是比较理想的做法,相当于没这个问题

但有一定局限性,有时候接手别的项目,或者自己根本没这个权限说不能使用路径参数

  1. 替换uri

  • 我们获取uri的目的,其实就是为了区别访问接口

  • 而把uri替换成另一种可以区分访问接口方法的标识即可

  • 最容易想到的就是通过反射获取到接口方法名称,使用接口方法名称替换成uri即可

  • 当然,其实不同的Controller中,其接口方法名称也有可能是相同的

  • 实际上可以再获取接口方法所在类类名,使用类名 + 方法名称替换uri即可

  • 实际解决方案有很多,看个人需求吧

  • 获取代码都是通过request.getRemoteAddr()获取的

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

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

相关文章

【Linux】Linux 系统编程——which 命令

文章目录 1.命令概述2.命令格式3.常用选项4.相关描述5.参考示例 1.命令概述 which 命令用于定位执行文件的路径。当输入一个命令时&#xff0c;which 会在环境变量 PATH 所指定的路径中搜索每个目录&#xff0c;以查找指定的可执行文件。 2.命令格式 which [选项] 命令名3.常…

STM32——DMA知识点及实战总结

1.DMA概念介绍 DMA&#xff0c;全称Direct Memory Access&#xff0c;即直接存储器访问。 DMA传输 将数据从一个地址空间复制到另一个地址空间。 注意&#xff1a;DMA传输无需CPU直接控制传输 2.DMA框图 3.DMA处理过程 外设的 8 个请求独立连接到每个通道&#xff0c;由 DMA_…

推挽输出、开漏输出、上拉输入、下拉输入、浮空输入。

一、推挽输出 推挽输出的内部电路大概如上图中黄色部分&#xff0c;输出控制内有反相器&#xff0c;由一个P-MOS和一个N-MOS组合而成&#xff0c;同一时间只有一个管子能够进行导通。 当写入1时&#xff0c;经过反向器后为0&#xff0c;P-MOS导通&#xff0c;N-MOS截至&#xf…

kafka简单介绍和代码示例

“这是一篇理论文章&#xff0c;给大家讲一讲kafka” 简介 在大数据领域开发者常常会听到MQ这个术语&#xff0c;该术语便是消息队列的意思&#xff0c; Kafka是分布式的发布—订阅消息系统。它最初由LinkedIn(领英)公司发布&#xff0c;使用Scala语言编写&#xff0c;与2010年…

gin-vue-admin二开使用雪花算法生成唯一标识 id

场景介绍 需求场景&#xff1a; 总部采集分支的数据&#xff0c;由于分支的 id 是子增的主键 id&#xff0c;所以会出现重复的 id&#xff0c;但是这个 id 需要作为标识&#xff0c;没有实际作用&#xff0c;这里选择的是分布式 id 雪花算法生成 id 存储用来标识&#xff0c;这…

交通流量预测:T-GCN A Temporal Graph Convolutional Network for Traffic Prediction

摘要 为了同时捕捉时空相关性&#xff0c;将图卷积网络(GCN)和门控递归单元(GRU)相结合&#xff0c;提出了一种新的基于神经网络的流量预测方法–时态图卷积网络(T-GCN)模型。具体地&#xff0c;GCN用于学习复杂的拓扑结构以捕获空间相关性&#xff0c;而门控递归单元用于学习…

20240112-补 制作两个字符串字谜的最少步骤数

题目要求 给你两个长度相同的字符串 s 和 t。在一个步骤中&#xff0c;你可以选择 t 中的任意一个字符并用另一个字符替换它。 返回将 t 变为 s 的变位所需的最少步数。 字符串的 "字谜 "是指字符串中的相同字符具有不同&#xff08;或相同&#xff09;的排列顺序…

响应式Web开发项目教程(HTML5+CSS3+Bootstrap)第2版 第1章 HTML5+CSS3初体验 项目1-2 许愿墙

项目展示 在生活中&#xff0c;许愿墙是一种承载愿望的实体&#xff0c;来源于“许愿树”的习俗。后来人们逐渐改变观念&#xff0c;开始将愿望写在小纸片上&#xff0c;然后贴在墙上&#xff0c;这就是许愿墙。随着互联网的发展&#xff0c;人们又将许愿墙搬到了网络上&#…

网络协议与攻击模拟_04ICMP协议与ICMP重定向

ICMP协议是网络层协议&#xff0c; 利用ICMP协议可以实现网络中监听服务和拒绝服务&#xff0c;如 ICMP重定向的攻击。 一、ICMP基本概念 1、ICMP协议 ICMP是Internet控制报文协议&#xff0c;用于在IP主机、路由器之间传递控制消息&#xff0c;控制消息指网络通不通、主机是…

如何用Python调用智谱AI的API进行智能问答

一、引言 随着人工智能技术的不断演进&#xff0c;以ChatGPT为首的智能聊天机器人如&#xff1a;文心一言、通义千问、智谱AI等受到越来越多人的喜欢。这些智能引擎不仅有网页版&#xff0c;而且开放了免费的api接口&#xff0c;并给出了样例代码。 这样&#xff0c;我们可以…

基于JavaWeb+BS架构+SpringBoot+Vue健美操评分系统系统的设计和实现

基于JavaWebBS架构SpringBootVue健美操评分系统系统的设计和实现 文末获取源码Lun文目录前言主要技术系统设计功能截图订阅经典源码专栏Java项目精品实战案例《500套》 源码获取 文末获取源码 Lun文目录 目 录 1 绪 论 1 1.1背景与意义 1 1.2 国内外研究概况 1 1.3 研究的内容…

大数据开发之Hadoop(HDFS)

第 1 章&#xff1a;HDFS概述 1.1 HDFS产出背景及定义 1、HDFS产生背景 随着数据量越来越大&#xff0c;在一个操作系统存不下所有的数据&#xff0c;那么就分配到更多的操作系统管理的磁盘中&#xff0c;但是不方便管理和维护&#xff0c;迫切需要一种系统来管理多台机器上的…

epoll实现(ET)

说来惭愧&#xff0c;编程也有一年半的时间了&#xff0c;今天在实现epoll这个多路转接的代码时&#xff0c;写了个bug&#xff0c;个人认为还是很不好发现的一个错误。 首先&#xff0c;在这里先给大家说说多路转接。所谓的多路转接就是在IO的时候提高了效率&#xff0c;原来…

电机的转矩控制和转速控制的区别和应用(电机控制),电动汽车电机控制更多用转矩控制模式还是转速控制模式?

转矩控制模式牺牲时间获取稳定性&#xff1b;转速模式牺牲稳定性获取时间&#xff1b; 1&#xff0c;VCU根据驾驶员给定的油门踏板开度确定的转矩需求&#xff0c;再将需求转矩指令发送给MCU&#xff0c;转矩控制。控制转矩可以把加速度的控制权掌握在驾驶者手里&#xff0c;更…

makefile,make,CMake项目编译之helloworld

文章目录 makefile&#xff0c;make&#xff0c;CMake项目编译1.相关概念1.1makefile是什么1.2make和makefile1.3cmake1.4Cmake与CMakeLists 的关系 2.makefile文件编写helloworld makefile&#xff0c;make&#xff0c;CMake项目编译 1.相关概念 1.1makefile是什么 工程中的…

Windows下同一台服务器部署多个tomcat服务

工具 apache-tomcat-8.5.97 安装tomcat步骤 下载apache-tomcat-8.5.97&#xff0c;下载下来的文件为apache-tomcat-8.5.97-windows-x64.zip解压该压缩包到指定目录下&#xff0c;比如E:\works修改解压文件夹名字为&#xff1a;tomcat-8080在E:\works目录下创建该文件夹的两个…

大模型学习之书生·浦语大模型5——基于LMDeploy大模型量化部署实践

目录 大模型部署背景 LMDeploy部署 量化 TurboMind API server 动手实践环节

计算机导论09-数据组织与管理

文章目录 数据管理基础数据管理技术的发展数据组织与管理的发展手工数据处理阶段文件方式的数据处理阶段数据库数据处理阶段 数据库技术的发展 数据模型数据模型的要素概念模型逻辑模型 数据库系统数据库系统基础数据库系统构成 数据库系统的结构数据库系统的体系结构数据库系统…

maxwell同步全量历史数据

CentOS安装maxwell 在上篇的基础上&#xff0c;我们实现了实时同步mysql数据到kafka。maxwell不仅可以同步实时数据&#xff0c;也可以同步全量历史数据。在这里模拟一下历史数据的场景&#xff0c;创建表结构如下&#xff0c;并写入测试数据。 CREATE TABLE user_det…

Kafka集群与可靠性

Kafka集群与可靠性 1.Kafka集群搭建实战 使用两台Linux服务器&#xff1a;一台192.168.182.137 一台192.168.182.138 安装kafka首先&#xff0c;我们需要配置java环境变量&#xff08;这里就略过了&#xff09; mkdir /opt/kafka #上传压缩包kafka_2.13-3.3.1.tgz并解压 ta…