【Lilishop商城】No2-7.确定软件架构搭建六(本篇包括延时任务,会用到rocketmq、redis)
创始人
2024-03-19 17:26:55
0

  仅涉及后端,全部目录看顶部专栏,代码、文档、接口路径在:

【Lilishop商城】记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客


 全篇只介绍重点架构逻辑,具体编写看源代码就行,读起来也不复杂~

谨慎:源代码中有一些注释是错误的,有的注释意思完全相反,有的注释对不上号,我在阅读过程中就顺手更新了,并且在我不会的地方添加了新的注释,所以在读源代码过程中一定要谨慎啊!

目录

A1.延时任务模块

 B1.延时任务模块的逻辑

PS:怕忘记的内容并且也是重点

 B2.延时任务执行模块基本搭建

C1.业务系统的延时任务模块搭建(产生延时任务)

C2.消费系统的延时任务模块搭建(执行延时任务)

B2.测试

C1.测试产生延时任务

C2.测试执行延时任务

剩余内容:暂时没有了


A1.延时任务模块

延时任务的需求,例如:

1、生成订单30分钟未支付,则自动取消订单
2、快递签收后未点击订单的确认收货,则7天后默认确认收货

和定时任务的区别在于:

1、定时任务有明确的触发时间,延时任务没有;
2、定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期;
3、定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务

说明:shop系统中使用的是多个技术框架结合实现的延时逻辑(redis+rocketmq),并没有使用专门的延时框架。我在网上搜索延时框架也没有找到合适的,几乎都是在现有技术框架基础上搭建的。

其他延时实现方式可看这篇:你可见过如此细致的延时任务详解_Java程序V的博客-CSDN博客

【rocketmq本身也支持延时消费哦,但延迟时长不支持随意时长的延迟【RocketMQ 二十】RocketMQ应用之延时消息】 

 B1.延时任务模块的逻辑

我们来说一下shop里面实现的延时模块,逻辑是有点复杂的,用到了redis+rocketmq,redis主要是存储延时任务,rocketmp主要是用来监听任务并执行的。

如果项目规模中不使用到消息中间件,也可以不用rocketmq。rocketmp在该项目中一部分功能是降低耦合、提高性能(我是这样理解的)。


我一开始看帮助文档的延时框架介绍:延时任务架构 · GitBook

里面的介绍比较简单,就对重点的类进行了说明,有点不太容易理解,我就又重新画了一个类关系图,格式不是很严谨(图3)...然后发现根据类名不容易理解,于是就用文字描述了一版(图2)...最终,为了更容易理解又画了一个简版逻辑(图1)

(图1)--> (图2)--> (图3),当然可以直接看图3源码,就是源码的类名字有点混淆了(对于我来说是混淆,也可能是我的问题...)

 

上三图解释的就已经很简单了,流程就不再文字重复了,下面记录一下我怕忘记的内容并且也是重点:

PS:怕忘记的内容并且也是重点

1.框架中是将同一类延时任务放到了同一个队列里面,等待执行,所以队列监听工厂类是获取的任务列表~所以本质上还是拿到一批任务进行执行。

2.框架中的队列监听工厂类是每秒轮询,不断获取在当秒执行的任务列表,然后开启异步线程执行;

3.因为延时任务类型不只一种,所以在执行框架中涉及到的核心类(除延时任务触发消费者类外),其余的核心类都需要实现类,每类延时任务各干各的事儿互不影响~

4.图中,以rocketmq和redis为连线,左半部分是属于业务系统的(在framework模块),右半部分是属于延时/消费系统的(在consumer模块,但也依赖framework,因为消费中需要业务类)。这样系统耦合度就降低了,例如,当consumer模块出故障后,不影响framwork模块添加任务,当consumer模块正常后就会从任务队列中继续获取任务进行执行~

5.任务类会从创建后保存到redis里面,然后被拿出来后又转化成mq的消息存储到mq里面,最后被mq监听到然后交给执行器执行。执行器里面是具体执行任务的逻辑,而流转的任务类里面的是需要到的参数~

6.延时执行接口是专门负责管理任务的,本来任务队列的管理也可以交给他,但是他又把延时队列生产工厂抽象出来了,应该是怕耦合。

7.RocketmqTimerTrigger具体延时任务实现,这个类的名字 RocketmqTimerTrigger 很迷惑,我的理解是促销类型的任务队列实现类,而这个名字范围太大了。如果我理解错了,那这个类里面又是只针对于 PromotionDelayQueue 队列的操作。所以最终我把他定位为促销类型的任务队列实现类,感觉这个理解是对的。

8.任务的修改逻辑是先删除任务然后又新增任务;任务的删除逻辑是先只删除任务的唯一标识,然后在消费端从队列中拿到并清除任务的消息时会判断任务唯一标识是否存在,存在则执行,不存在则跳过,所以并不是直接通过任务是否存在而判断的。


接下来就开始搭建 

 B2.延时任务执行模块基本搭建

消费系统的模块我们还在上篇新创建的consumer-test-xxl-job中添加,业务模块我们创建一个新的模块 framework-test 来搭建,这样更清晰。(以后用起来也方便,但要注意由于是我们自己创建的模块,有些工具类、参数类是没有的,到时候会自行添加的~)

C1.业务系统的延时任务模块搭建(产生延时任务)

创建新的模块framework-test,因为会用到 redis和rocketmq,我们按照之前文章里的的逻辑搭建就可以啦,在这就不重复了。

下面看开始搭建业务类的延时任务模块搭建:

1.延时任务消息类,专门存放任务信息;添加具体任务信息;

2.延时队列生产工厂抽象类,管理延时任务队列模块,向队列增加任务;管理延时任务队列模块实现类

3.延时执行接口,管理延时执行模块,增删改任务、执行任务;管理延时执行模块的实现类;

4.会使用到枚举类、常量接口、util工具类;

5.yml文件配置,添加rocketmq主题类型;(记得修改本模块pom类)

// 1.延时任务消息类,专门存放任务信息;添加具体任务信息;
//详见:cn.lili.trigger.model.TimeTriggerMsg@Data
@AllArgsConstructor
@NoArgsConstructor
public class TimeTriggerMsg implements Serializable {private static final long serialVersionUID = 8897917127201859535L;/*** 执行器beanId,用于获取对应执行器对象*/private String triggerExecutor;/*** 执行时间*/private Long triggerTime;/*** 执行器参数,就是任务内容*/private Object param;/*** 唯一KEY*/private String uniqueKey;/*** 信息队列主题,用与mq发送和接收消息*/private String topic;}/*** @author QingChen* @Description 具体任务信息,一般是所需要的参数属性,这里就随便测试了* @date 2022-12-02 15:43* @Version 1.0*/
@Data
@NoArgsConstructor
public class Test1Message {/*** name*/private String name;/*** id*/private String id;/*** 开始时间*/private Date startTime;/*** 结束时间*/private Date endTime;
}
//2.延时队列生产工厂抽象类,管理延时任务队列模块,向队列增加任务;管理延时任务队列模块实现类
//抽象类,详见:cn.lili.trigger.delay.AbstractDelayQueueMachineFactory
//实现类,详见:cn.lili.trigger.delay.queue.Test1DelayQueue@Slf4j
public abstract class AbstractDelayQueueMachineFactory {@Autowiredprivate Cache cache;/*** 插入任务到redis里面,记住 jodid 是任务对象json转string** @param jobId       任务id(队列内唯一)* @param triggerTime 执行时间 时间戳(毫秒)* @return 是否插入成功*/public boolean addJob(String jobId, Long triggerTime) {/*** 设置在 redis 中的排序 score,设置为任务执行时间/1000(也就是秒级别的,别忘了在延时队列里面也/1000)* 因为延时任务按照疫每秒轮询判断,所以队列的 score 按照秒级别存放,方便获取*/long delaySeconds = triggerTime / 1000;//增加延时任务到 ZSet 里面,参数依次为:队列名称、执行时间score、任务idboolean result = cache.zAdd(this.setDelayQueueName(), delaySeconds, jobId);log.info("增加延时任务, 缓存key {}, 执行时间 {},任务id {}", setDelayQueueName(), DateUtil.toString(triggerTime), jobId);return result;}/*** 要实现延时队列的名字,实现类需要实现* @return 延时队列的名字*/public abstract String setDelayQueueName();}/*** @author QingChen* @Description 管理延时任务队列模块实现类* @date 2022-12-02 17:10* @Version 1.0*/
@Component
public class Test1DelayQueue extends AbstractDelayQueueMachineFactory {@Overridepublic String setDelayQueueName() {return DelayQueueEnums.TEST_1_DELAYQUEUE.name();}
}
//3.延时执行接口,管理延时执行模块,增删改任务、执行任务;管理延时执行模块的实现类;
//接口类,详见:cn.lili.trigger.interfaces.TimeTrigger
//实现类,详见:cn.lili.trigger.interfaces.impl.Test1RocketmqTimerTriggerpublic interface TimeTrigger {/*** 添加延时任务** @param timeTriggerMsg 延时任务信息*/void addDelay(TimeTriggerMsg timeTriggerMsg);/*** 执行延时任务** @param timeTriggerMsg 延时任务信息*/void execute(TimeTriggerMsg timeTriggerMsg);/*** 修改延时任务** @param executorName   执行器beanId* @param param          执行参数* @param triggerTime    执行时间 时间戳 秒为单位* @param oldTriggerTime 旧的任务执行时间* @param uniqueKey      添加任务时的唯一凭证* @param delayTime      延时时间(秒)* @param topic          rocketmq topic*/void edit(String executorName, Object param, Long oldTriggerTime, Long triggerTime, String uniqueKey, int delayTime, String topic);/*** 删除延时任务** @param executorName 执行器* @param triggerTime  执行时间* @param uniqueKey    添加任务时的唯一凭证* @param topic        rocketmq topic*/void delete(String executorName, Long triggerTime, String uniqueKey, String topic);
}@Component
@Slf4j
public class Test1RocketmqTimerTrigger implements TimeTrigger {@Autowiredprivate RocketMQTemplate rocketMQTemplate;@Autowiredprivate Cache cache;@Autowiredprivate Test1DelayQueue delayQueue;@Overridepublic void addDelay(TimeTriggerMsg timeTriggerMsg) {//拿到执行器唯一keyString uniqueKey = timeTriggerMsg.getUniqueKey();if (StringUtils.isEmpty(uniqueKey)) {uniqueKey = StringUtils.getRandStr(10);}//生成执行任务keyString generateKey = DelayQueueTools.generateKey(timeTriggerMsg.getTriggerExecutor(), timeTriggerMsg.getTriggerTime(), uniqueKey);//在redis中添加一个唯一标识的来标识当前延时任务的唯一性,该标识根据任务key生成的this.cache.put(generateKey, 1);//设置延时任务,注意哦,这里将延时消息本身(json)作为了 jobidif (Boolean.TRUE.equals(delayQueue.addJob(JSONUtil.toJsonStr(timeTriggerMsg), timeTriggerMsg.getTriggerTime()))) {log.info("延时任务标识: {}", generateKey);log.info("定时执行在【" + DateUtil.toString(timeTriggerMsg.getTriggerTime(), "yyyy-MM-dd HH:mm:ss") + "】,消费【" + timeTriggerMsg.getParam().toString() + "】");} else {log.error("延时任务添加失败:{}", timeTriggerMsg);}}@Overridepublic void execute(TimeTriggerMsg timeTriggerMsg) {this.addExecute(timeTriggerMsg.getTriggerExecutor(),timeTriggerMsg.getParam(),timeTriggerMsg.getTriggerTime(),timeTriggerMsg.getUniqueKey(),timeTriggerMsg.getTopic());}/*** 将任务添加到mq,mq异步队列执行。* 

* 本系统中redis相当于延时任务吊起机制,而mq才是实际的业务消费,执行任务的存在** @param executorName 执行器beanId* @param param 执行参数* @param triggerTime 执行时间 时间戳 秒为单位* @param uniqueKey 如果是一个 需要有 修改/取消 延时任务功能的延时任务,
* 请填写此参数,作为后续删除,修改做为唯一凭证
* 建议参数为:COUPON_{ACTIVITY_ID} 例如 coupon_123
* 业务内全局唯一* @param topic rocketmq topic*/private void addExecute(String executorName, Object param, Long triggerTime, String uniqueKey, String topic) {TimeTriggerMsg timeTriggerMsg = new TimeTriggerMsg(executorName, triggerTime, param, uniqueKey, topic);Message message = MessageBuilder.withPayload(timeTriggerMsg).build();log.info("延时任务发送信息:{}", message);this.rocketMQTemplate.asyncSend(topic, message, RocketmqSendCallbackBuilder.commonCallback());}@Overridepublic void edit(String executorName, Object param, Long oldTriggerTime, Long triggerTime, String uniqueKey, int delayTime, String topic) {this.delete(executorName, oldTriggerTime, uniqueKey, topic);this.addDelay(new TimeTriggerMsg(executorName, triggerTime, param, uniqueKey, topic));}@Overridepublic void delete(String executorName, Long triggerTime, String uniqueKey, String topic) {String generateKey = DelayQueueTools.generateKey(executorName, triggerTime, uniqueKey);log.info("删除延时任务{}", generateKey);this.cache.remove(generateKey);} }

//4.会使用到枚举类、常量接口、util工具类
//都有:
延时任务工具类,专门生成key:cn.lili.trigger.util.DelayQueueTools
延时任务执行器常量:cn.lili.trigger.model.TimeExecuteConstant
队列枚举:cn.lili.trigger.enums.DelayQueueEnums
延时任务类型:cn.lili.trigger.enums.DelayTypeEnumscn.lili.util.DateUtil
cn.lili.util.StringUtils
//5.yml文件配置,添加rocketmq主题类型;
//详见:/lilishop-master/framework-test/src/main/resources/application.ymllili:data:rocketmq:test1Topic: lili_test1Topictest1Group: lili_test1Group

C2.消费系统的延时任务模块搭建(执行延时任务)

在上次创建的consumer-test-xxl-job模块中,搭建延时任务框架,由于要搭配新创建的framework模块使用,所以需要将 pom 文件中的framework依赖注释掉,并且引入framework-test模块;

下面看开始搭建消费类的延时任务模块搭建

1.延时队列工厂监听抽象类,延时队列工厂监听实现类;

2.延时任务事件消息触发消费者;

3.延时任务执行器接口,延时任务执行器实现类;

4.会使用到枚举类、常量接口、util工具类;(记得修改本模块pom类)

//1.延时队列工厂监听抽象类,延时队列工厂监听实现类;
//抽象类,详见:cn.lili.trigger.listen.queue.AbstractDelayQueueListen
//实现类,详见:cn.lili.trigger.listen.queue.impl.Test1DelayQueueListen/*** @author QingChen* @Description 延时队列工厂监听抽象类*      springBoot项目启动时候,有时候需要再启动之后直接执行某一段代码。这个时候就用到了 ApplicationRunner 这个类。* @date 2022-12-05 11:34* @Version 1.0*/
@Slf4j
public abstract class AbstractDelayQueueListen implements ApplicationRunner {@Autowiredprivate Cache cache;/*** 延时队列机器开始运作*/private void startDelayQueueMachine() {log.info("延时队列机器{}开始运作", setDelayQueueName());//监听redis队列while (true) {try {//获取当前时间的时间戳,并拿到秒,与long now = System.currentTimeMillis() / 1000;//获取当前监听任务类型中,当前时间前需要执行的任务列表,(score是以时间秒设置的)//【不会移除任务哦】Set tuples = cache.zRangeByScore(setDelayQueueName(), 0, now);//如果任务不为空if (!CollectionUtils.isEmpty(tuples)) {log.info("执行任务:{}", JSONUtil.toJsonStr(tuples));for (DefaultTypedTuple tuple : tuples) {//循环拿到jobidString jobId = (String) tuple.getValue();//移除缓存,如果移除成功则表示当前线程处理了延时任务,则执行延时任务//【在这里移除任务哦】Long num = cache.zRemove(setDelayQueueName(), jobId);//如果移除成功, 则执行if (num > 0) {//创建新线程并run,run里面执行 invoke 方法ThreadPoolUtil.execute(() -> {this.invoke(jobId);});}}}} catch (Exception e) {log.error("处理延时任务发生异常,异常原因为{}", e.getMessage(), e);} finally {//间隔一秒钟搞一次try {TimeUnit.SECONDS.sleep(5L);} catch (InterruptedException e) {e.printStackTrace();}}}}/*** 最终执行的任务方法** @param jobId 任务id*/public abstract void invoke(String jobId);/*** 要实现延时队列的执行器名字* @return*/public abstract String setDelayQueueName();/*** 初始化监听队列*/public void init() {ThreadPoolUtil.getPool().execute(this::startDelayQueueMachine);}}@Component
public class Test1DelayQueueListen extends AbstractDelayQueueListen {@Autowiredprivate Test1RocketmqTimerTrigger timeTrigger;/*** @Description: 调用具体的 TimeTrigger 执行任务的方法* @param: [jobId]* @return: void**/@Overridepublic void invoke(String jobId) {//Json转对象timeTrigger.execute(JSONUtil.toBean(jobId, TimeTriggerMsg.class));}/*** 要实现延时队列的名字* @return 促销延时队列名称*/@Overridepublic String setDelayQueueName() {return DelayQueueEnums.TEST_1_DELAYQUEUE.name();}/*** @Description: ApplicationRunner 的重写方法* @param: [args]* @return: void**/@Overridepublic void run(ApplicationArguments args) throws Exception {this.init();}
}
//2.延时任务事件消息触发消费者;
//详见:cn.lili.trigger.listen.trigger.TimeTriggerConsumer/*** @author QingChen* @Description 延时任务事件消息触发消费者* 从rocketmq拦截的topic类型来讲,这个是只针对指定主题promotion-topic的消息监听器。* 但是由于延时任务消息的执行器只跟 TimeTriggerMsg 的属性有关,所以这个消费类可以作为所有延时任务消息的监听器。* (由于这个监听的 topic 指定了促销类型,所以我不确定系统开发者的逻辑,而且这个类的名字看起来也是总得消费类,所以比较迷惑)* (如果是我的话,会将这个作为所有延时任务的消费中心)** @date 2022-12-05 13:32* @Version 1.0*/
@Component
@Slf4j
@RocketMQMessageListener(topic = "${lili.data.rocketmq.test1Topic}", consumerGroup = "${lili.data.rocketmq.test1Group}")
public class TimeTriggerConsumer implements RocketMQListener {@Autowiredprivate Cache cache;@Overridepublic void onMessage(TimeTriggerMsg timeTriggerMsg) {try {//生成执行任务keyString key = DelayQueueTools.generateKey(timeTriggerMsg.getTriggerExecutor(), timeTriggerMsg.getTriggerTime(), timeTriggerMsg.getUniqueKey());if (cache.get(key) == null) {log.info("执行器执行被取消:{} | 任务标识:{}", timeTriggerMsg.getTriggerExecutor(), timeTriggerMsg.getUniqueKey());return;}log.info("执行器执行:" + timeTriggerMsg.getTriggerExecutor());log.info("执行器参数:" + JSONUtil.toJsonStr(timeTriggerMsg.getParam()));cache.remove(key);//拿到任务所指定执行器的bean进行执行TimeTriggerExecutor executor = (TimeTriggerExecutor) SpringContextUtil.getBean(timeTriggerMsg.getTriggerExecutor());executor.execute(timeTriggerMsg.getParam());} catch (Exception e) {log.error("mq延时任务异常", e);}}}
//3.延时任务执行器接口,延时任务执行器实现类;
//接口类,详见:cn.lili.trigger.executor.TimeTriggerExecutor
//实现类,详见:Test1TimeTriggerExecutor/*** @author QingChen* @Description 延时任务执行器接口* @date 2022-12-05 13:37* @Version 1.0*/public interface TimeTriggerExecutor {/*** 执行任务** @param object 任务参数*/void execute(Object object);}/*** @author QingChen* @Description 延时任务执行器实现类* @date 2022-12-05 13:38* @Version 1.0*/
@Slf4j
@Component(TimeExecuteConstant.TEST_1_EXECUTOR)
public class Test1TimeTriggerExecutor implements TimeTriggerExecutor {@Overridepublic void execute(Object object) {Test1Message message = JSONUtil.toBean(JSONUtil.parseObj(object), Test1Message.class);log.info("执行器:{"+TimeExecuteConstant.TEST_1_EXECUTOR+"}");log.info("执行内容:{"+message+"}");}
}
//4.会使用到枚举类、常量接口、util工具类;cn.lili.util.SpringContextUtil
cn.lili.util.ThreadPoolUtil

注意,这个模块只是我用来测试的,里面没有任何和shop系统业务相关的,最终的目录是这样的:

B2.测试

C1.测试产生延时任务

我们直接就在framework-test模块添加controller接口,来创建任务,一个是创建任务接口,通过入参的延时任务类型,来判断具体调用哪个任务调度器添加任务。

生产代码的时候,记得要清楚的添加各个类型哦~

//我这里就添加了两个接口,一个添加任务,一个删除任务
详见:cn.lili.controller.TestTriggerController@RestController
@RequestMapping("/trigger")
public class TestTriggerController {/*** 延时任务1*/@Autowiredprivate Test1RocketmqTimerTrigger test1RocketmqTimerTrigger;/*** 延时任务2*/@Autowiredprivate Test2RocketmqTimerTrigger test2RocketmqTimerTrigger;/*** rocketMq配置*/@Autowiredprivate RocketmqCustomProperties rocketmqCustomProperties;/*** @Description: type 是指延时任务类型,正式开发时是不会这样用的哦,我仅仅是测试* @param: [type]* @return: java.lang.String**/@PostMapping(value = "/trigger")public String addTrigger(String type) {//获取时间偏移(向前或向后)long startTime = DateUtil.offsetMinute(new Date(), 2).getTime();switch (type){case "1":Test1Message test1Message = new Test1Message();test1Message.setId(StringUtils.getRandStr(10));test1Message.setName("任务name");test1Message.setStartTime(new Date());test1Message.setEndTime(new Date());TimeTriggerMsg timeTriggerMsg = new TimeTriggerMsg(TimeExecuteConstant.TEST_1_EXECUTOR,startTime,test1Message,DelayQueueTools.wrapperUniqueKey(DelayTypeEnums.TEST_1_DELAYTYPE.name(), (test1Message.getId())),rocketmqCustomProperties.getTest1Topic());test1RocketmqTimerTrigger.addDelay(timeTriggerMsg);break;case "2":Test2Message test2Message = new Test2Message();test2Message.setNum(StringUtils.getRandStr(10));test2Message.setSize("大小size");test2Message.setStartTime(new Date());timeTriggerMsg = new TimeTriggerMsg(TimeExecuteConstant.TEST_2_EXECUTOR,startTime,test2Message,DelayQueueTools.wrapperUniqueKey(DelayTypeEnums.TEST_2_DELAYTYPE.name(), (test2Message.getNum())),rocketmqCustomProperties.getTest1Topic());test2RocketmqTimerTrigger.addDelay(timeTriggerMsg);break;default:break;}return "SUCCESSFUL";}/*** @Description: TODO* @param: executorName 执行器名字, triggerTime 执行时间, idORnum 唯一标识(我将test1 2写一起的), topic 任务主题* @return: java.lang.String**/@DeleteMapping(value = "/trigger")public String deleteTrigger(String executorName, Long triggerTime, String idORnum, String topic) {switch (executorName){case TimeExecuteConstant.TEST_1_EXECUTOR:test1RocketmqTimerTrigger.delete(executorName,triggerTime,DelayQueueTools.wrapperUniqueKey(DelayTypeEnums.TEST_1_DELAYTYPE.name(), idORnum),rocketmqCustomProperties.getTest1Topic());break;case TimeExecuteConstant.TEST_2_EXECUTOR:test2RocketmqTimerTrigger.delete(executorName,triggerTime,DelayQueueTools.wrapperUniqueKey(DelayTypeEnums.TEST_2_DELAYTYPE.name(), idORnum),rocketmqCustomProperties.getTest1Topic());break;default:break;}return "SUCCESSFUL";}
}

 

C2.测试执行延时任务

这个直接运行consumer,搭配上面的C1就是测试了呀,嘿嘿,可以看到就是在执行时间的范围内执行的

 

剩余内容:暂时没有了

后面就开始详细设计了,再详细设计中有可能会在涉及到系统架构改动,到时会再度说明。

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...