《优化接口设计的思路》系列:第九篇—用好缓存,让你的接口速度飞起来
一、前言
大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。
作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。
前面的文章都是写接口如何设计、接口怎么验权以及一些接口常用的组件,这篇写点接口性能相关的。接口性能优化有很多途径,比如表建索引、SQL优化、加缓存、重构代码等等,本篇文章主要讲一下我是怎么在项目中使用缓存来提高接口响应速度的。我觉得缓存的使用主要有下面几个方面:
- 缓存预热
- 定时任务预热:定时任务在系统低峰期预加载数据到缓存中。
- 启动预热:系统启动时预加载必要的数据到缓存中。
- 缓存层次化
- 多级缓存:实现本地缓存和分布式缓存相结合,例如,先在本地缓存中查询,如果没有再查询Redis等分布式缓存,最后才查询数据库。
- 热点数据缓存:对频繁访问的数据进行缓存,如用户会话、热门商品信息、高频访问的内容等。
缓存提高接口响应速度主要是上面这些思路,不过我不是来讲概念的,那是面试要用的东西。我要讲的是如何用代码实现这些思路,把它们真正用到项目中来,水平有限,我尽力说,不喜勿喷。
由于文章经常被抄袭,开源的代码甚至被当成收费项,所以源码里面不是全部代码,有需要的同学可以留个邮箱,我给你单独发!
二、缓存预热:手撸一个缓存处理器
上面说了缓存预热主要是定时任务预热、启动预热,那么我们实现这个功能的时候,一般使用
ConcurrentHashMap或Redis来暂存数据,然后加上SpringBoot自带的@Scheduled定时刷新缓存就够了。虽然这样可以实现缓存预热,但缺陷很多,一旦需要预热的东西多起来就会变得越来越复杂,那么如何实现一个好的缓存处理器呢?接着看!
1、缓存处理器设计
(1)一个好的缓存处理器应该是这样搭建的
- DAL实现,产出DAO和DO对象,定义缓存领域模型
- 定义缓存名称,特别关注缓存的初始化顺序
- 编写数据仓库,通过模型转换器实现数据模型到缓存模型的转化
- 编写缓存管理器,推荐继承抽象管理器 {@link AbstractCacheManager}
- 根据业务需求,设计缓存数据接口(putAll,get,getCacheInfo等基础API)
- 完成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;}@Overridepublic String getName() {return name;}public void setName(String name) {this.name = name;}@Overridepublic String getDescription() {return description;}public void setDescription(String description) {this.description = description;}@Overridepublic 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()*/@Overridepublic void afterPropertiesSet() {CacheManagerRegistry.register(this);}@Overridepublic void initCache() {String description = getCacheName().getDescription();LOGGER.info("start init {}", description);loadingCache();afterInitCache();LOGGER.info("{} end init", description);}@Overridepublic void refreshCache() {String description = getCacheName().getDescription();LOGGER.info("start refresh {}", description);loadingCache();afterRefreshCache();LOGGER.info("{} end refresh", description);}/*** @see org.springframework.core.Ordered#getOrder()*/@Overridepublic int getOrder() {return getCacheName().getOrder();}@Overridepublic void dumpCache() {String description = getCacheName().getDescription();LOGGER.info("start print {} {}{}", description, "\n", getCacheInfo());LOGGER.info("{} end print", description);}/*** 获取缓存条目** @return*/@Overridepublic 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();}@Overridepublic 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;@Overrideprotected String getCacheInfo() {return CacheMessageUtil.toString(CACHE);}@Overrideprotected void loadingCache() {LOCK.lock();try {//存储数据,这里就模拟一下了CACHE = new ConcurrentHashMap<>();CACHE.put("key1", "value1");CACHE.put("key2", "value2");CACHE.put("key3", "value3");} finally {LOCK.unlock();}}@Overrideprotected long getSize() {return null == CACHE ? 0 : CACHE.size();}@Overridepublic 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 {@Autowiredprivate CacheService cacheService;public Set<String> queryAuthByUserId(Long userId) {return BaseUtil.selectCacheByTemplate(//从缓存中查询() -> this.cacheService.queryUserFromRedis(userId),//从数据库中查询() -> this.cacheService.queryUserFromDB(userId));}
}
这样就可以做到先查询Redis,查询不到再查询数据库,非常简单也非常好用,我常用于查询一些实体信息的场景。不过这里有一个注意的点:
缓存一致性。因为有时候底层数据会变化,需要做好一致性,否则会出问题。
四、小结一下
首先,缓存确实可以提高API查询效率,这点大家应该不会质疑,但缓存并不是万能的,不应该将所有数据都缓存起来,应当评估数据的访问频率和更新频率,以决定是否缓存。
其次,在实施缓存策略时,需要平衡缓存的开销、复杂性和所带来的性能提升。此外,缓存策略应该根据实际业务需求和数据特征进行定制,不断调整优化以适应业务发展。
最后,缓存虽好,但不要乱用哦,否则会出现令你惊喜的BUG!😇
相关文章:
《优化接口设计的思路》系列:第九篇—用好缓存,让你的接口速度飞起来
一、前言 大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 作为一名从业已达六年的老码农,…...
专业130+总分410+西南交通大学924信号与系统考研经验西南交大电子信息通信工程,真题,大纲,参考书。
初试分数出来,专业课924信号与系统130,总分410,整体上发挥正常,但是还有遗憾,其实自己可以做的更好,总结一下经验,希望对大家有所帮助。专业课:(130) 西南交…...
MySQL数据库 - 存储引擎
一. mysql 存储引擎的相关知识 1.1 存储引擎的概念 MySQL中的数据用各种不下同的技术存储在文件中,每一种技术都使用不同的存储机制、索引技巧、锁定水平并最终提供不同的功能和能力,这些不同的技术以及配套的功能在MySQL中称为存储引擎。存储引擎是My…...
时序预测 | Matlab基于BiTCN-LSTM双向时间卷积长短期记忆神经网络时间序列预测
时序预测 | Matlab基于BiTCN-LSTM双向时间卷积长短期记忆神经网络时间序列预测 目录 时序预测 | Matlab基于BiTCN-LSTM双向时间卷积长短期记忆神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab基于BiTCN-LSTM双向时间卷积长短期记忆神经网络时…...
Spring Cloud Alibaba Sentinel 使用详解
一、Sentinel 介绍 随着微服务的流行,服务和服务之间的稳定性变得越来越重要。 Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。 Sentinel 具有以下特征: 丰富的应用场景: Sentinel 承接了阿里巴…...
android gdb 调试
gdbgdbserver远程调试技术(一)——调试环境搭建_gdbserver 远程调试-CSDN博客 GDB/gdbserver 7.4.1 for Android with NEON support (gnutoolchains.com) sudo apt-get install texinfo$ tar zxvf gdb-7.12.tar.gz $ cd gdb-7.12/$ mkdir build$ cd bu…...
分布式搜索引擎elasticsearch专栏二
上一篇的传送门: 分布式搜索引擎elasticsearch专栏一-CSDN博客 这一篇博文主要讲解elasticsearch的数据搜索功能。下面会分别使用DSL和RestClient实现搜索。 1.DSL查询文档 elasticsearch的查询依然是基于JSON风格的DSL来实现的。 1.1.DSL查询分类 Elasticsea…...
LeetCode第一天(495.提莫攻击)
题目: 在《英雄联盟》的世界中,有一个叫 “提莫” 的英雄。他的攻击可以让敌方英雄艾希(编者注:寒冰射手)进入中毒状态。 当提莫攻击艾希,艾希的中毒状态正好持续 duration 秒。 正式地讲,提…...
SQL运维_Unix下MySQL-8.0.18配置文件示例
SQL运维_Unix下MySQL-8.0.18配置文件示例 MySQL 是一个关系型数据库管理系统, 由瑞典 MySQL AB 公司开发, 属于 Oracle 旗下产品。 MySQL 是最流行的关系型数据库管理系统之一, 在 WEB 应用方面, MySQL 是最好的 RDBMS (Relational Database Management System, 关系数据库管…...
python_BeautifulSoup爬取汽车评论数据
爬取的网站: 完整代码在文章末尾 https://koubei.16888.com/57233/0-0-0-2 使用方法: from bs4 import BeautifulSoup 拿到html后使用find_all()拿到文本数据,下图可见,数据标签为: content_text soup.find_all…...
24.2 SpringCloud电商进阶开发
24.2 SpringCloud电商进阶开发 1. 定时任务1.1 使用场景1.2 CRON表达式1.3 代码实战2. 线程池和ThreadLocal应用2.1 线程池1. 配置2. 应用3. Zuul安全性增强(重要)3.1 屏蔽接口转发3.2 异常统一处理4. SpringCloud Gateway网关4.1 Gateway创建基本架构1. 依赖</...
ES6—Module 的语法
export命令 ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。 模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功…...
GitHub gpg体验
文档 实践 生成新 GPG 密钥 gpg --full-generate-key查看本地GPG列表 gpg --list-keys关联GPG公钥与Github账户 gpg --armor --export {key_id}GPG私钥对Git commit进行签名 git config --local user.signingkey {key_id} # git config --global user.signingkey {key_id} git…...
鸿蒙一次开发,多端部署(十一)交互归一
对于不同类型的智能设备,用户可能有不同的交互方式,如通过触摸屏、鼠标、触控板等。如果针对不同的交互方式单独做适配,会增加开发工作量同时产生大量重复代码。为解决这一问题,我们统一了各种交互方式的API,即实现了交…...
基于python+vue文学名著分享系统的设计与实现flask-django-nodejs-php
随着世界经济信息化、全球化的到来和互联网的飞速发展,推动了各行业的改革。若想达到安全,快捷的目的,就需要拥有信息化的组织和管理模式,建立一套合理、动态的、交互友好的、高效的文学名著分享系统。当前的信息管理存在工作效率…...
[音视频学习笔记]七、自制音视频播放器Part2 - VS + Qt +FFmpeg 写一个简单的视频播放器
前言 话不多说,重走霄骅登神路 前一篇文章 [音视频学习笔记]六、自制音视频播放器Part1 -新版本ffmpeg,Qt VS2022,都什么年代了还在写传统播放器? 本文相关代码仓库: MediaPlay-FFmpeg - Public 转载雷神的两个流程…...
LeetCode每日一题——x 的平方根
x 的平方根OJ链接:69. x 的平方根 - 力扣(LeetCode) 题目: 思路: 乍一看题目只需要算一个数的平方根,根据我们之前学的C语言我们能很快的想到使用sqrt,pow这类的<math.h>库函数…...
2024.3.22 ARM
实现三个按键的中断 main.c :主函数初始化 #include "key_inc.h" #include "uart4.h" //封装延时函数 void delay(int ms) {int i, j;for (i 0; i < ms; i){for (j 0; j < 2000; j);} } int main() {char *s "hello world"…...
【Linux】信号的处理{信号处理的时机/了解寄存器/内核态与用户态/信号操作函数}
文章目录 0.对于信号捕捉的理解1.信号处理的时机1.1 何时处理信号?1.2 内核态和用户态1.3 内核态和用户态的切换 2.了解寄存器3.信号捕捉的原理4.信号操作函数4.1sighandler_t signal(int signum, sighandler_t handler);4.2int sigaction(int signum, const struct…...
webgl浏览器渲染设置
在浏览器中程序图形化webgl渲染时,有时候发现代码没有问题,但是就是无法渲染或者渲染报错,此时可以尝试如下的设置: 通过在chrome浏览器输入chrome://flags打开扩展 设置一(webgl开发者扩展) 设…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...
多场景 OkHttpClient 管理器 - Android 网络通信解决方案
下面是一个完整的 Android 实现,展示如何创建和管理多个 OkHttpClient 实例,分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...
PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)
前言: 最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,…...
省略号和可变参数模板
本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...
NPOI操作EXCEL文件 ——CAD C# 二次开发
缺点:dll.版本容易加载错误。CAD加载插件时,没有加载所有类库。插件运行过程中用到某个类库,会从CAD的安装目录找,找不到就报错了。 【方案2】让CAD在加载过程中把类库加载到内存 【方案3】是发现缺少了哪个库,就用插件程序加载进…...
Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...
rm视觉学习1-自瞄部分
首先先感谢中南大学的开源,提供了很全面的思路,减少了很多基础性的开发研究 我看的阅读的是中南大学FYT战队开源视觉代码 链接:https://github.com/CSU-FYT-Vision/FYT2024_vision.git 1.框架: 代码框架结构:readme有…...
Django RBAC项目后端实战 - 03 DRF权限控制实现
项目背景 在上一篇文章中,我们完成了JWT认证系统的集成。本篇文章将实现基于Redis的RBAC权限控制系统,为系统提供细粒度的权限控制。 开发目标 实现基于Redis的权限缓存机制开发DRF权限控制类实现权限管理API配置权限白名单 前置配置 在开始开发权限…...
