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

一、前言

大家好!我是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.mzph.cn/news/765471.shtml

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

相关文章

2024Python计算机二级13

一维数据采用线性方式组织&#xff0c;集合不属于线性结构 程调度仅负责对CPU进行分配 快速排序每经过一次元素的交换会产生新的逆序 记住队列中为一个元素的情况 区分二叉树与完全二叉树&#xff0c;前序序列是先访问根节点再访问左子树和右子树&#xff0c;中序序列是访问左子…

《定时执行专家》:Nircmd 的超级搭档,解锁自动化新境界

目录 Nircmd 简介 《定时执行专家》与 Nircmd 的结合 示例&#xff1a; 自动清理电脑垃圾: 定时发送邮件: 定时关闭电脑: 《定时执行专家》的优势: 总结: 以下是一些其他使用示例&#xff1a; 立即下载《定时执行专家》&#xff1a; Nircmd 官方网站&#xff1a; 更…

【数字IC/FPGA】书籍推荐(1)----《轻松成为设计高手--Verilog HDL实用精解》

在下这几年关于数字电路、Verilog、FPGA和IC方面的书前前后后都读了不少&#xff0c;发现了不少好书&#xff0c;也在一些废话书上浪费过时间。接下来会写一系列文章&#xff0c;把一部分读过的书做个测评&#xff0c;根据个人标准按十分制满分来打分分享给大家。 书名&#xf…

企业微信可以更换公司主体吗?

企业微信变更主体有什么作用&#xff1f;当我们的企业因为各种原因需要注销或已经注销&#xff0c;或者运营变更等情况&#xff0c;企业微信无法继续使用原主体继续使用时&#xff0c;可以申请企业主体变更&#xff0c;变更为新的主体。企业微信变更主体的条件有哪些&#xff1…

基于Docker的JMeter分布式压测!

一个JMeter实例可能无法产生足够的负载来对你的应用程序进行压力测试。如本网站所示&#xff0c;一个JMeter实例将能够控制许多其他的远程JMeter实例&#xff0c;并对你的应用程序产生更大的负载。JMeter使用Java RMI[远程方法调用]来与分布式网络中的对象进行交互。JMeter主站…

Vue js封装接口

天梦星服务平台 (tmxkj.top)https://tmxkj.top/#/ 1.安装axios npm install axios -g 2.在src下新建一个Api文件夹,再创建一个js文件 import axios from axios let configuration {url:"http://localhost:9090" } /*** 请求项目数据的请求体*/ async function h…

CV论文--2024.3.20

1、Graph Expansion in Pruned Recurrent Neural Network Layers Preserve Performance 中文标题&#xff1a;图扩展在修剪的循环神经网络层中保持性能 简介&#xff1a;这段摘要讨论了图的扩展性质&#xff0c;包括强连通性和稀疏性。研究表明&#xff0c;深度神经网络可以通…

linux -- I2C设备驱动 -- MS32006(低压5V多通道电机驱动器)

产品简述 MS32006 是一款多通道电机驱动芯片, 其中包含两路步进电机驱动, 一路直流电机驱动; 每个通道的电流最高电流1.0A; 支持两相四线与四相五线步进电机。芯片采用 I2C 的通信接口控制模式, 兼容 3.3V/5V 的标准工业接口。 MS32006 总共集成了两路步进电机驱动器与一…

【c++入门】引用,内联函数,auto

&#x1f525;个人主页&#xff1a;Quitecoder &#x1f525;专栏&#xff1a;c笔记仓 朋友们大家好&#xff0c;本节我们来到c中一个重要的部分&#xff1a;引用 目录 1.引用的基本概念与用法1.1引用特性1.2使用场景1.3传值、传引用效率比较1.4引用做返回值1.5引用和指针的对…

公司调研 | 空间机械臂GITAI | 日企迁美

最近做的一些公司 / 产品调研没有从技术角度出发&#xff0c;而更关注宏观发展&#xff1a;主营方向、产品介绍、商业化落地情况、融资历程、公司愿景、创始人背景等。部分调研放在知乎上&#xff0c;大部分在飞书私人链接上 最近较关注人形Robot的发展情况&#xff0c;欢迎感兴…

【Java】Map和Set

文章目录 一、Map和Set的概念二、模型三、Map的说明3.1 Map.Entry<K, V>的说明3.2 Map 的常用方法 四、Set的说明4.1 Set的常用方法 一、Map和Set的概念 Map和set是一种专门用来进行搜索的容器或者数据结构&#xff0c;其搜索的效率与其具体的实例化子类有关&#xff0c…

在线播放视频网站源码系统 带完整的安装代码包以及搭建教程

在线播放视频网站源码系统的开发&#xff0c;源于对当前视频市场的深入洞察和用户需求的精准把握。随着视频内容的爆炸式增长&#xff0c;用户对视频播放的需求也日益多样化。他们希望能够随时随地观看自己感兴趣的视频内容&#xff0c;同时还希望能够在观看过程中享受到流畅、…

用vscode调试cpp程序相关操作记录

需要在服务器上用vscode调试cpp程序&#xff0c;写此记录launch.json配置和相关步骤错误导致的问题 1.在需要运行程序的服务器上安装C/C Extension Pack&#xff08;之前只在本地装了&#xff09;&#xff0c;可以支持调试C/C应用程序(设置断点&#xff0c;单步执行&#xff0c…

分类预测 | Matlab实现PSO-KELM粒子群优化算法优化核极限学习机分类预测

分类预测 | Matlab实现PSO-KELM粒子群优化算法优化核极限学习机分类预测 目录 分类预测 | Matlab实现PSO-KELM粒子群优化算法优化核极限学习机分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.MATLAB实现PSO-KELM粒子群优化算法优化核极限学习机分类预测(完整源…

ubuntu20.04安裝輸入法

文章目录 前言一、操作過程1、安装fcitx-googlepinyin2、配置language support 前言 參考文獻 一、操作過程 1、安装fcitx-googlepinyin sudo apt-get install fcitx-googlepinyin2、配置language support 第一次點擊進去&#xff0c;會讓你安裝 點擊ctrl和空格切換中英文…

简历指导与模板获取

简历是应聘过程当中最重要的材料&#xff0c;是我们在求职市场的一张名片&#xff0c;一份好的简历能够吸引招聘者的注意&#xff0c;使你在竞争激烈的求职市场中脱颖而出。 1.简历指导 以下是一份典型简历的主要部分和常见内容&#xff1a; 联系信息&#xff1a; 包括你的全…

【火猫DOTA2】Secret官宣下放四号位Ekki,教练ah fu顶替担任

1、近日Secret战队官方宣布对阵容做出调整:下放四号位选手Ekki,教练ah fu将在未来一段时间重回赛场担任四号位。 Ekki于今年1月初宣布加入Secret担任四号位,但队伍在今年的表现不甚理想,未能从西欧预选赛晋级BB别墅杯和ESL One伯明翰。在唯一成功晋级的梦幻联赛S22上,Secret也仅…

如何在linux环境上部署单机ES(以8.12.2版本为例)

ES安装&#xff08;以8.12.2版本为例&#xff09; 首先创建好对应的文件夹然后在对应的文件夹下执行依次这些命令 1.wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.12.2-linux-x86_64.tar.gz 2.wget https://artifacts.elastic.co/downloads/…

HCIA-Datacom H12-811 更新

完整题库及答案解析&#xff0c;请直接扫描上方二维码&#xff0c;持续更新中 在WLAN发展历史中&#xff0c;初级移动办公时代的主要无线协议标准不包括以下哪项&#xff1f; A. 802.11ac B. 802.11g C. 802.11b D. 802.11a 答案&#xff1a;A OSPF协议在进行主从关系选举…

mapstruct学习笔记-pojo之间的转换

1、前言 mapstruct中常用注解如Mapping,AfterMapping,BeanMapping等的使用,通过案例说明各式各样的业务pojo对象之间如何借助mapstruct完成相互之间的转换,减少代码量的同时也能突出业务逻辑流程,让你的代码里写起来更有规范可言。 2、简介 Reference Guide – MapStruct 3…