《优化接口设计的思路》系列:第九篇—用好缓存,让你的接口速度飞起来

一、前言

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

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

前面的文章都是写接口如何设计、接口怎么验权以及一些接口常用的组件,这篇写点接口性能相关的。接口性能优化有很多途径,比如表建索引、SQL优化、加缓存、重构代码等等,本篇文章主要讲一下我是怎么在项目中使用缓存来提高接口响应速度的。我觉得缓存的使用主要有下面几个方面:

  • 缓存预热
    • 定时任务预热:定时任务在系统低峰期预加载数据到缓存中。
    • 启动预热:系统启动时预加载必要的数据到缓存中。
  • 缓存层次化
    • 多级缓存:实现本地缓存和分布式缓存相结合,例如,先在本地缓存中查询,如果没有再查询Redis等分布式缓存,最后才查询数据库。
    • 热点数据缓存:对频繁访问的数据进行缓存,如用户会话、热门商品信息、高频访问的内容等。

缓存提高接口响应速度主要是上面这些思路,不过我不是来讲概念的,那是面试要用的东西。我要讲的是如何用代码实现这些思路,把它们真正用到项目中来,水平有限,我尽力说,不喜勿喷。

由于文章经常被抄袭,开源的代码甚至被当成收费项,所以源码里面不是全部代码,有需要的同学可以留个邮箱,我给你单独发!

二、缓存预热:手撸一个缓存处理器

上面说了缓存预热主要是定时任务预热、启动预热,那么我们实现这个功能的时候,一般使用ConcurrentHashMapRedis来暂存数据,然后加上SpringBoot自带的@Scheduled定时刷新缓存就够了。虽然这样可以实现缓存预热,但缺陷很多,一旦需要预热的东西多起来就会变得越来越复杂,那么如何实现一个好的缓存处理器呢?接着看!

1、缓存处理器设计

(1)一个好的缓存处理器应该是这样搭建的

  1. DAL实现,产出DAO和DO对象,定义缓存领域模型
  2. 定义缓存名称,特别关注缓存的初始化顺序
  3. 编写数据仓库,通过模型转换器实现数据模型到缓存模型的转化
  4. 编写缓存管理器,推荐继承抽象管理器 {@link AbstractCacheManager}
  5. 根据业务需求,设计缓存数据接口(putAll,get,getCacheInfo等基础API)
  6. 完成bean配置,最好是可插拔的注册方式,缓存管理器和数据仓库、扩展点服务

(2)思路分析

2、代码实现

a. 每个处理器都有缓存名字、描述信息、缓存初始化顺序等信息,所以应该定义一个接口,名字为CacheNameDomain;

CacheNameDomain.java

package com.summo.demo.cache;

public interface CacheNameDomain {

    /**
     * 缓存初始化顺序,级别越低,越早被初始化
     * <p>
     * 如果缓存的加载存在一定的依赖关系,通过缓存级别控制初始化或者刷新时缓存数据的加载顺序<br>
     * 级别越低,越早被初始化<br>
     * <p>
     * 如果缓存的加载没有依赖关系,可以使用默认顺序<code>Ordered.LOWEST_PRECEDENCE</code>
     *
     * @return 初始化顺序
     * @see org.springframework.core.Ordered
     */
    int getOrder();

    /**
     * 缓存名称,推荐使用英文大写字母表示
     *
     * @return 缓存名称
     */
    String getName();

    /**
     * 缓存描述信息,用于打印日志
     *
     * @return 缓存描述信息
     */
    String getDescription();
}
b. 可以使用一个枚举类将不同的缓存处理器分开,有利于管理,取名为CacheNameEnum;

CacheNameEnum.java

package com.summo.demo.cache;

import org.springframework.core.Ordered;

/**
 * @description 缓存枚举
 */
public enum CacheNameEnum implements CacheNameDomain {
    /**
     * 系统配置缓存
     */
    SYS_CONFIG("SYS_CONFIG", "系统配置缓存", Ordered.LOWEST_PRECEDENCE),
    ;

    private String name;

    private String description;

    private int order;

    CacheNameEnum(String name, String description, int order) {
        this.name = name;
        this.description = description;
        this.order = order;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public int getOrder() {
        return order;
    }

    public void setOrder(int order) {
        this.order = order;
    }
}
c. 缓存信息转换工具,以便dump出更友好的缓存信息,取名为CacheMessageUtil;

CacheMessageUtil.java

package com.summo.demo.cache;


import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * @description 缓存信息转换工具,以便dump出更友好的缓存信息
 */
public final class CacheMessageUtil {

    /** 换行符 */
    private static final char ENTERSTR  = '\n';

    /** Map 等于符号 */
    private static final char MAP_EQUAL = '=';

    /**
     * 禁用构造函数
     */
    private CacheMessageUtil() {
        // 禁用构造函数
    }

    /**
     * 缓存信息转换工具,以便dump出更友好的缓存信息<br>
     * 对于List<?>的类型转换
     *
     * @param cacheDatas 缓存数据列表
     * @return 缓存信息
     */
    public static String toString(List<?> cacheDatas) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < cacheDatas.size(); i++) {
            Object object = cacheDatas.get(i);
            builder.append(object);

            if (i != cacheDatas.size() - 1) {
                builder.append(ENTERSTR);
            }
        }

        return builder.toString();
    }

    /**
     * 缓存信息转换工具,以便dump出更友好的缓存信息<br>
     * 对于Map<String, Object>的类型转换
     *
     * @param map 缓存数据
     * @return 缓存信息
     */
    public static String toString(Map<?, ?> map) {
        StringBuilder builder = new StringBuilder();
        int count = map.size();
        for (Iterator<?> i = map.keySet().iterator(); i.hasNext();) {
            Object name = i.next();
            count++;

            builder.append(name).append(MAP_EQUAL);
            builder.append(map.get(name));

            if (count != count - 1) {
                builder.append(ENTERSTR);
            }
        }

        return builder.toString();
    }

}
d. 每个处理器都有生命周期,如初始化、刷新、获取处理器信息等操作,这应该也是一个接口,处理器都应该声明这个接口,名字为CacheManager;

CacheManager.java

package com.summo.demo.cache;

import org.springframework.core.Ordered;

public interface CacheManager extends Ordered {

    /**
     * 初始化缓存
     */
    public void initCache();

    /**
     * 刷新缓存
     */
    public void refreshCache();

    /**
     * 获取缓存的名称
     *
     * @return 缓存名称
     */
    public CacheNameDomain getCacheName();

    /**
     * 打印缓存信息
     */
    public void dumpCache();

    /**
     * 获取缓存条数
     *
     * @return
     */
    public long getCacheSize();
}
e. 定义一个缓存处理器生命周期的处理器,会声明CacheManager,做第一次的处理,也是所有处理器的父类,所以这应该是一个抽象类,名字为AbstractCacheManager;

AbstractCacheManager.java

package com.summo.demo.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;

/**
 * @description 缓存管理抽象类,缓存管理器都要集成这个抽象类
 */
public abstract class AbstractCacheManager implements CacheManager, InitializingBean {

    /**
     * LOGGER
     */
    protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractCacheManager.class);

    /**
     * 获取可读性好的缓存信息,用于日志打印操作
     *
     * @return 缓存信息
     */
    protected abstract String getCacheInfo();

    /**
     * 查询数据仓库,并加载到缓存数据
     */
    protected abstract void loadingCache();

    /**
     * 查询缓存大小
     *
     * @return
     */
    protected abstract long getSize();

    /**
     * @see InitializingBean#afterPropertiesSet()
     */
    @Override
    public void afterPropertiesSet() {
        CacheManagerRegistry.register(this);
    }

    @Override
    public void initCache() {

        String description = getCacheName().getDescription();

        LOGGER.info("start init {}", description);

        loadingCache();

        afterInitCache();

        LOGGER.info("{} end init", description);
    }

    @Override
    public void refreshCache() {

        String description = getCacheName().getDescription();

        LOGGER.info("start refresh {}", description);

        loadingCache();

        afterRefreshCache();

        LOGGER.info("{} end refresh", description);
    }

    /**
     * @see org.springframework.core.Ordered#getOrder()
     */
    @Override
    public int getOrder() {
        return getCacheName().getOrder();
    }

    @Override
    public void dumpCache() {

        String description = getCacheName().getDescription();

        LOGGER.info("start print {} {}{}", description, "\n", getCacheInfo());

        LOGGER.info("{} end print", description);
    }

    /**
     * 获取缓存条目
     *
     * @return
     */
    @Override
    public long getCacheSize() {
        LOGGER.info("Cache Size Count: {}", getSize());
        return getSize();
    }

    /**
     * 刷新之后,其他业务处理,比如监听器的注册
     */
    protected void afterInitCache() {
        //有需要后续动作的缓存实现
    }

    /**
     * 刷新之后,其他业务处理,比如缓存变通通知
     */
    protected void afterRefreshCache() {
        //有需要后续动作的缓存实现
    }
}
f. 当有很多缓存处理器的时候,那么需要一个统一注册、统一管理的的地方,可以实现对分散在各处的缓存管理器统一维护,名字为CacheManagerRegistry;

CacheManagerRegistry.java

package com.summo.demo.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.OrderComparator;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @description 缓存管理器集中注册接口,可以实现对分散在各处的缓存管理器统一维护
 */
@Component
public final class CacheManagerRegistry implements InitializingBean {

    /**
     * LOGGER
     */
    private static final Logger logger = LoggerFactory.getLogger(CacheManagerRegistry.class);

    /**
     * 缓存管理器
     */
    private static Map<String, CacheManager> managerMap = new ConcurrentHashMap<String, CacheManager>();

    /**
     * 注册缓存管理器
     *
     * @param cacheManager 缓存管理器
     */
    public static void register(CacheManager cacheManager) {
        String cacheName = resolveCacheName(cacheManager.getCacheName().getName());
        managerMap.put(cacheName, cacheManager);
    }

    /**
     * 刷新特定的缓存
     *
     * @param cacheName 缓存名称
     */
    public static void refreshCache(String cacheName) {
        CacheManager cacheManager = managerMap.get(resolveCacheName(cacheName));
        if (cacheManager == null) {
            logger.warn("cache manager is not exist,cacheName=", cacheName);
            return;
        }

        cacheManager.refreshCache();
        cacheManager.dumpCache();
    }

    /**
     * 获取缓存总条数
     */
    public static long getCacheSize(String cacheName) {
        CacheManager cacheManager = managerMap.get(resolveCacheName(cacheName));
        if (cacheManager == null) {
            logger.warn("cache manager is not exist,cacheName=", cacheName);
            return 0;
        }
        return cacheManager.getCacheSize();
    }

    /**
     * 获取缓存列表
     *
     * @return 缓存列表
     */
    public static List<String> getCacheNameList() {
        List<String> cacheNameList = new ArrayList<>();
        managerMap.forEach((k, v) -> {
            cacheNameList.add(k);
        });
        return cacheNameList;
    }

    public void startup() {
        try {

            deployCompletion();

        } catch (Exception e) {

            logger.error("Cache Component Init Fail:", e);

            // 系统启动时出现异常,不希望启动应用
            throw new RuntimeException("启动加载失败", e);
        }
    }

    /**
     * 部署完成,执行缓存初始化
     */
    private void deployCompletion() {

        List<CacheManager> managers = new ArrayList<CacheManager>(managerMap.values());

        // 根据缓存级别进行排序,以此顺序进行缓存的初始化
        Collections.sort(managers, new OrderComparator());

        // 打印系统启动日志
        logger.info("cache manager component extensions:");
        for (CacheManager cacheManager : managers) {
            String beanName = cacheManager.getClass().getSimpleName();
            logger.info(cacheManager.getCacheName().getName(), "==>", beanName);
        }

        // 初始化缓存
        for (CacheManager cacheManager : managers) {
            cacheManager.initCache();
            cacheManager.dumpCache();
        }
    }

    /**
     * 解析缓存名称,大小写不敏感,增强刷新的容错能力
     *
     * @param cacheName 缓存名称
     * @return 转换大写的缓存名称
     */
    private static String resolveCacheName(String cacheName) {
        return cacheName.toUpperCase();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        startup();
    }
}

3、使用方式

项目结构如下:

这是完整的项目结构图,具体的使用步骤如下:
step1、在CacheNameEnum中加一个业务枚举,如 SYS_CONFIG("SYS_CONFIG", "系统配置缓存", Ordered.LOWEST_PRECEDENCE)
step2、自定义一个CacheManager继承AbstractCacheManager,如public class SysConfigCacheManager extends AbstractCacheManager
step3、实现loadingCache()方法,这里将你需要缓存的数据查询出来,但注意不要将所有的数据都放在一个缓存处理器中,前面CacheNameEnum枚举类的作用就是希望按业务分开处理;
step4、在自定义的CacheManager类中写自己的查询数据方法,因为不同业务的场景不同,查询参数、数据大小、格式、类型都不一致,所以AbstractCacheManager并没有定义统一的取数方法,没有意义;

下面是一个完整的例子
SysConfigCacheManager.java

package com.summo.demo.cache.manager;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.summo.demo.cache.AbstractCacheManager;
import com.summo.demo.cache.CacheMessageUtil;
import com.summo.demo.cache.CacheNameDomain;
import com.summo.demo.cache.CacheNameEnum;
import org.springframework.stereotype.Component;

/**
 * 系统配置管理器
 */
@Component
public class SysConfigCacheManager extends AbstractCacheManager {

    /**
     * 加个锁,防止出现并发问题
     */
    private static final Lock LOCK = new ReentrantLock();

    /**
     * 底层缓存组件,可以使用ConcurrentMap也可以使用Redis,推荐使用Redis
     */
    private static ConcurrentMap<String, Object> CACHE;

    @Override
    protected String getCacheInfo() {
        return CacheMessageUtil.toString(CACHE);
    }

    @Override
    protected void loadingCache() {
        LOCK.lock();
        try {
            //存储数据,这里就模拟一下了
            CACHE = new ConcurrentHashMap<>();
            CACHE.put("key1", "value1");
            CACHE.put("key2", "value2");
            CACHE.put("key3", "value3");
        } finally {
            LOCK.unlock();
        }

    }

    @Override
    protected long getSize() {
        return null == CACHE ? 0 : CACHE.size();
    }

    @Override
    public CacheNameDomain getCacheName() {
        return CacheNameEnum.SYS_CONFIG;
    }

    /**
     * 自定义取数方法
     *
     * @param key
     * @return
     */
    public static Object getConfigByKey(String key) {
        return CACHE.get(key);
    }
}

三、缓存层次化:使用函数式编程实现

1、先举个例子

现有一个使用商品名称查询商品的需求,要求先查询缓存,查不到则去数据库查询;从数据库查询到之后加入缓存,再查询时继续先查询缓存。

(1)思路分析

可以写一个条件判断,伪代码如下:

//先从缓存中查询
String goodsInfoStr = redis.get(goodsName);
if(StringUtils.isBlank(goodsInfoStr)){
	//如果缓存中查询为空,则去数据库中查询
	Goods goods = goodsMapper.queryByName(goodsName);
	//将查询到的数据存入缓存
	goodsName.set(goodsName,JSONObject.toJSONString(goods));
	//返回商品数据
	return goods;
}else{
	//将查询到的str转换为对象并返回
	return JSON.parseObject(goodsInfoStr, Goods.class);
}

流程图如下

上面这串代码也可以实现查询效果,看起来也不是很复杂,但是这串代码是不可复用的,只能用在这个场景。假设在我们的系统中还有很多类似上面商品查询的需求,那么我们需要到处写这样的if(...)else{...}。作为一个程序员,不能把类似的或者重复的代码统一起来是一件很难受的事情,所以需要对这种场景的代码进行优化。

上面这串代码的问题在于:入参不固定、返回值也不固定,如果仅仅是参数不固定,使用泛型即可。但最关键的是查询方法也是不固定的,比如查询商品和查询用户肯定不是一个查询方法吧。

所以如果我们可以把一个方法(即上面的各种查询方法)也能当做一个参数传入一个统一的判断方法就好了,类似于:

/**
 * 这个方法的作用是:先执行method1方法,如果method1查询或执行不成功,再执行method2方法
 */
public static<T> T selectCacheByTemplate(method1,method2)

想要实现上面的这种效果,就不得不提到Java8的新特性:函数式编程

2、什么是函数式编程

在Java中有一个package:java.util.function ,里面全部是接口,并且都被@FunctionalInterface注解所修饰。

Function分类

  • Consumer(消费):接受参数,无返回值
  • Function(函数):接受参数,有返回值
  • Operator(操作):接受参数,返回与参数同类型的值
  • Predicate(断言):接受参数,返回boolean类型
  • Supplier(供应):无参数,有返回值

具体我就不再赘述了,可以参考:https://blog.csdn.net/hua226/article/details/124409889

3、代码实现

核心代码非常简单,如下

/**
  * 缓存查询模板
  *
  * @param cacheSelector    查询缓存的方法
  * @param databaseSelector 数据库查询方法
  * @return T
  */
public static <T> T selectCacheByTemplate(Supplier<T> cacheSelector, Supplier<T> databaseSelector) {
  try {
    log.info("query data from redis ······");
    // 先查 Redis缓存
    T t = cacheSelector.get();
    if (t == null) {
      // 没有记录再查询数据库
      return databaseSelector.get();
    } else {
      return t;
    }
  } catch (Exception e) {
    // 缓存查询出错,则去数据库查询
    log.info("query data from database ······");
    return databaseSelector.get();
    }
}

这里的Supplier 就是一个加了@FunctionalInterface注解的接口。

4、使用方式

使用方式也非常简单,如下

@Component
public class UserManager {

    @Autowired
    private CacheService cacheService;

    public Set<String> queryAuthByUserId(Long userId) {
        return BaseUtil.selectCacheByTemplate(
            //从缓存中查询
            () -> this.cacheService.queryUserFromRedis(userId),
            //从数据库中查询
            () -> this.cacheService.queryUserFromDB(userId));
    }
}

这样就可以做到先查询Redis,查询不到再查询数据库,非常简单也非常好用,我常用于查询一些实体信息的场景。不过这里有一个注意的点:缓存一致性。因为有时候底层数据会变化,需要做好一致性,否则会出问题。

四、小结一下

首先,缓存确实可以提高API查询效率,这点大家应该不会质疑,但缓存并不是万能的,不应该将所有数据都缓存起来,应当评估数据的访问频率和更新频率,以决定是否缓存。
其次,在实施缓存策略时,需要平衡缓存的开销、复杂性和所带来的性能提升。此外,缓存策略应该根据实际业务需求和数据特征进行定制,不断调整优化以适应业务发展。
最后,缓存虽好,但不要乱用哦,否则会出现令你惊喜的BUG!😇

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android14音频进阶:AudioFlinger究竟如何混音?(六十三)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只…

开源离线语音识别输入工具CapsWriter v1.0——支持无限时长语音、音视频文件转录字幕。

分享一款开源离线语音识别输入工具&#xff0c;支持无限时长语音、音视频文件转录字幕。 软件简介&#xff1a; CapsWriter是一款免费开源且可完全离线识别的语音输入工具&#xff0c;无需担心因在线版本识别带来的各种隐私泄露问题。支持win7及以上的系统&#xff0c;已经更…

洛谷_P1104 生日_python写法

P1104 生日 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 知识点&#xff1a; 还是自定义规则的排序&#xff0c;然后这里还有python中如何在一行中输入多种类型的数据。 n int(input()) data [] num 1 for i in range(n):img list(input().split())s img[0]y int(img…

Axure RP10汉化版获取:低成本高效率操作!

作为市场份额最高的专业原型设计工具&#xff0c;Axure RP10 毫无疑问&#xff0c;功能的强大性和灵活性也受到许多产品经理和设计师的青睐。许多世界百强公司也在使用Axure进行原型设计 RP10。但对于许多本土设计师来说&#xff0c;Axure RP10 全英语界面和陡峭的学习曲线让人…

图解CodeWhisperer的安装使用

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! ​ 目录 &#x1f4d8; CodeWhisperer简介 &#…

CCIE-04-Layer2_WAN_TS

目录 实验条件网络拓朴 路由器配置开始排错&#xff0c; 要求R11可以访问R17的telnet检查R12和R11的e0/0口&#xff0c;有发现检查R17和R12的S4/0口&#xff0c; 有发现ping R17环回口地址&#xff0c;发现不通telnet R17环回口IP 实验条件 网络拓朴 路由器配置 R11 4组以太网…

qt-pdf-viewer-library 编译过程记录

1.qtpdfviewerinitializer.h 中 类模板问题需要修改为下面代码: https://github.com/develtar/qt-pdf-viewer-library 下载代码&#xff1a; 编译出现错误 修改代码&#xff0c;如下: 2.无法触发onViewerLoaded 事件&#xff0c;就是界面无法显示PDF文件 修改下面代码&#…

【技巧】ChatGPT Prompt 提示语大全

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 主要来自&#xff1a;https://github.com/f/awesome-chatgpt-prompts ChatGPT SEO prompts ChatGPT SEO提示 Contributed by: StoryChief AI Reference: 7 Powerful ChatGPT Prompts to Create SEO Content Faste…

RabbitMQ问题

如何实现顺序消费&#xff1f; 消息放入到同一个队列中消费 如何解决消息不丢失&#xff1f; 方案&#xff1a; 如上图&#xff1a;消息丢失有三种情况&#xff0c;解决了以上三种情况就解决了丢失的问题 1、丢失1--->消息在到达交换机的时候&#xff1b;解决&#xff1…

RabbitMQ 安装保姆级教程

目录 1.MQ引言 1.1 什么是MQ 1.2 MQ有哪些 1.3 不同MQ特点 2.RabbitMQ 的引言 2.1 RabbitMQ 2.2 RabbitMQ 的安装 2.2.1 下载 2.2.2 下载的安装包 2.2.3 安装步骤 3. RabiitMQ 配置 3.1RabbitMQ 管理命令行 3.2 web管理界面介绍 3.2.1 overview概览 3.2.2 Admin用…

整型数组按个位值排序 - 华为OD统一考试(C卷)

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 给定一个非空数组(列表)&#xff0c;其元素数据类型为整型&#xff0c;请按照数组元素十进制最低位从小到大进行排序&#xff0c;十进制最低位相同的元素&#xf…

wireshark windows 抓包https

windows下 1.配置环境变量以生成ssl协商会话密钥日志记录 系统设置-》高级设置-》环境变量 新增环境变量 SSLKEYLOGFILE C:\Users\Public\Documents\SSLKEY\sslkey.log 打开公用共享文档创建SSLKEY文件夹用于后续系统存放协商密钥日志 2.配置Wireshark选项进行抓包 点击…

计算机二级(Python)真题讲解每日一题:《方菱形》

描述‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‭‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‮‬ 请写代码替换横线&#xff0…

【2024最新版,redis7】redis底层的10种数据结构

前言&#xff1a;本文redis版本&#xff1a;7.2.4 本文语雀原文地址&#xff08;首发更新&#xff09;&#xff1a;https://www.yuque.com/wzzz/redis/xg2cp37kx1s4726y 本文CSDN转载地址&#xff1a; https://blog.csdn.net/u013625306/article/details/136842107 1. 常见的数…

.htaccess全站设置SSL,wordpress全站设置SSL,网站重定向的次数过多”错误最佳解决方法教程

.htaccess全站设置SSL,wordpress全站设置SSL&#xff0c;网站重定向的次数过多”错误最佳解决方法教程 网上找了很多教程网无效**.htacces**设置&#xff0c;访问后台出现重定向次数过多&#xff0c;导致无法访问 找了好久&#xff0c;测试用AI机器人无法解决&#xff0c;参考…

【Linux】详谈进程优先级进程调度与切换

一、进程优先级 1.1、为什么要有优先级 进程要访问某种资源&#xff0c;进程通过一定的方式排队&#xff0c;确认享受资源的优先顺序。计算机中资源过少&#xff0c;所以进程访问某种资源时需要排队。 1.2、优先级的具体表示 进程的优先级其实就是PCB中的一个整形变量…

python失物招领系统-安卓-flask-django-nodejs-php

对于本失物招领 的设计来说&#xff0c; 它是应用mysql数据库、安卓等技术动态编程以及数据库进行努力学习和大量实践&#xff0c;并运用到了 建设中在整个系统的设计当中&#xff0c;具体根据网上失物招领的现状来进行开发的&#xff0c;具体根据用户需求实现网上失物招领网络…

产品推荐 | 基于XC7K325T的FMC接口万兆光纤网络验证平台

01、产品概述 TES307是一款基于XC7K325T FPGA的万兆光纤网络验证平台&#xff0c;板卡具有1个FMC&#xff08;HPC&#xff09;接口&#xff0c;4路SFP万兆光纤接口、4路SATA接口、1路USB3.0接口。 板载高性能的FPGA处理器可以实现光纤协议、SATA总线控制器、以及USB3.0高速串…

【Node.js从基础到高级运用】十五、单元测试与集成测试

引言 在Node.js开发过程中&#xff0c;测试是确保代码质量和功能正确性的关键步骤。单元测试和集成测试是最常见的测试类型。下面我们将使用Jest框架来进行测试。 单元测试 单元测试是指对软件中的最小可测试单元进行检查和验证。在Node.js中&#xff0c;这通常指的是函数或者…

ISIS接口明文认证实验简述

默认情况下&#xff0c;ISIS接口认证通过在ISIS协议数据单元&#xff08;PDU&#xff09;中添加认证字段&#xff0c;例如&#xff1a;一个密钥或密码&#xff0c;用于验证发送方的身份。 ISIS接口认证防止未经授权的设备加入到网络中&#xff0c;并确保邻居之间的通信是可信的…
最新文章