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

一、前言

大家好!我是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;}@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!😇

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何用联合(共用体)union验证系统大小端

一&#xff1a;思路 由联合体的特点&#xff0c;可知上图&#xff0c;char c 和 int i 共用四个字节&#xff0c;假设是小端&#xff0c;则由左到右是低地址到高地址&#xff0c;四个字节的内容如图所示01 00 00 00 代码展示&#xff1a; 如果第一个字节是1&#xff0c;则证明…

体育竞赛成绩管理系统设计与实现|jsp+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)

本项目包含可运行源码数据库LW&#xff0c;文末可获取本项目的所有资料。 推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java&#xff0c;…

南京大学AI考研,宣布改考408!

官网还没通知 附上南大与同层次学校近四年的分数线对比&#xff0c;整体很难 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 如果确定要冲南大的话建议提早调整自己的复习路线&…

sqlite3 交叉编译

#1.下载源码并解压 源码路径如下&#xff0c;下载autoconf版本 SQLite Download Page 解压 tar -zxvf sqlite-autoconf-3450200.tar.gz cd sqlite-autoconf-3450200 mkdir build # 2. 配置源代码 # 假设你已经安装了交叉编译工具链&#xff0c;如gcc-arm-linux-gnueabih…

Python爬取歌曲宝音乐:轻松下载Jay的歌

歌曲宝是一个不用付费就能听jay的歌曲&#xff0c;但是每次都只能播放一首不方便&#xff0c;于是今天想把它下载下来&#xff0c;本地循环播放&#xff0c;它所用到的接口是某我的还不错哈 获取搜索接口 分析html请求接口&#xff0c;获取到的数据是直接渲染好的HTML内容&…

Python-VBA编程500例-016(入门级)

移动石子算法(Stone-moving Algorithm)是一类在计算机科学和数学中广泛研究的算法问题&#xff0c;通常涉及在特定规则下移动石子以达到某种目标。虽然这些问题本身可能看起来是抽象的&#xff0c;但它们在实际应用中有多种体现&#xff0c;包括但不限于以下领域&#xff1a; …

stable diffusion 提示词进阶语法-年龄身材肤色-学习小结

stable diffusion 提示词进阶语法-年龄&身材&肤色 前言年龄提示词青年&#xff08;18-25岁&#xff09;幼年、少年&#xff08;1-18&#xff09;中年&#xff08;35-60岁&#xff09;老年&#xff08;65-80岁 老爷爷 老奶奶&#xff09; 身材提示词肤色关键词(人物基础…

【现代C++】nullptr用法

在C11之前&#xff0c;NULL被用来表示空指针&#xff0c;但它只是一个宏&#xff0c;定义为0或((void*)0)&#xff0c;这在某些情况下可能会导致类型混淆和错误。为了解决这些问题&#xff0c;C11引入了nullptr关键字&#xff0c;它是一种特殊的空指针字面量&#xff0c;具有自…

一次完整的 HTTP 请求所经历的步骤

1&#xff1a; DNS 解析(通过访问的域名找出其 IP 地址&#xff0c;递归搜索)。 2&#xff1a; HTTP 请求&#xff0c;当输入一个请求时&#xff0c;建立一个 Socket 连接发起 TCP的 3 次握手。如果是 HTTPS 请求&#xff0c;会略微有不同。 3&#xff1a; 客户端向服务器发…

一文详解Rust中的字符串

有人可能会说&#xff0c;字符串这么简单还用介绍&#xff1f;但是很多人学习rust受到的第一个暴击就来自这浓眉大眼、看似毫无难度的字符串。 请看下面的例子。 fn main() {let my_name "World!";greet(my_name); }fn greet(name: String) {println!("Hello…

【Mysql数据库基础03】分组函数(聚合函数)、分组查询

分组函数(聚合函数&#xff09;、分组查询 0 该博客所要用的数据库表的属性1 分组函数1.1 简单的使用1.2 是否忽略null值1.3 和关键字搭配使用1.4 count函数的详细介绍1.5 练习 2 分组查询Group by2.1 简单的分组查询2.2 练习 3 格式投票:yum: 0 该博客所要用的数据库表的属性 …

基于SpringBoot+Redis实现接口限流

前言 业务中需要对一些接口进行限流处理&#xff0c;防止机器人调用或者保证服务质量&#xff1b; 实现方式 基于redis的lua脚本 引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis&…

javaWeb点餐平台系统功能介绍说明

开发工具&#xff1a;IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7 框架&#xff1a;ssm、Springboot 前端&#xff1a;Vue、ElementUI 关键技术&#xff1a;springboot、SSM、vue、MYSQL、MAVEN 数据库工具&#xff1a;Navicat、SQLyog 项目关键技术 1、JSP技术 JSP(Java…

pytest简介以及命令行执行

pytest简介以及安装 pytest简介导入第三方库修改工具类 pytest命令方式执行函数执行pytest中的参数详解 pytest简介 pytest有很多强大的插件 pytest-html &#xff08;生成html格式的自动化测试报告&#xff09; pytest-xdist &#xff08;测试用例分布式执行&#xff0c;多cpu…

鸿蒙Harmony应用开发—ArkTS-@Provide装饰器和@Consume装饰器:与后代组件双向同步

Provide和Consume&#xff0c;应用于与后代组件的双向数据同步&#xff0c;应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递&#xff0c;Provide和Consume摆脱参数传递机制的束缚&#xff0c;实现跨层级传递。 其中Provide装饰的变…

目标检测——PP-YOLO算法解读

PP-YOLO系列&#xff0c;均是基于百度自研PaddlePaddle深度学习框架发布的算法&#xff0c;2020年基于YOLOv3改进发布PP-YOLO&#xff0c;2021年发布PP-YOLOv2和移动端检测算法PP-PicoDet&#xff0c;2022年发布PP-YOLOE和PP-YOLOE-R。由于均是一个系列&#xff0c;所以放一起解…

重学SpringBoot3-MyBatis的三种分页方式

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-MyBatis的三种分页方式 准备工作环境搭建数据准备未分页效果 1. 使用MyBatis自带的RowBounds进行分页演示 2. 使用物理分页插件演示 3. 手动编写分页SQL…

浅浅迈入C++门槛

从今天起&#xff0c;我要开始hello&#xff0c;world。 往后更要做到&#xff0c;拳打数据结构&#xff0c;脚踢Linux。 这就是江湖人的风范。 拼搏百天&#xff0c;我要学希普拉斯普拉斯。 C是在C的基础之上&#xff0c;容纳进去了面向对象编程思想&#xff0c;并增加了许…

小红书扫码登录分析与python实现

文章目录 1. 写在前面2. 接口分析3. 代码实现 【&#x1f3e0;作者主页】&#xff1a;吴秋霖 【&#x1f4bc;作者介绍】&#xff1a;擅长爬虫与JS加密逆向分析&#xff01;Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python…

设计模式深度解析:适配器模式与桥接模式-灵活应对变化的两种设计策略大比拼

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 适配器模式与桥接模式-灵活应对变化的两种设计策略大比拼 探索设计模式的魅力&#xff1a;深入了…