参考

Redis队列详解(springboot实战)_redis 队列-CSDN博客


前言

MQ消息队列有很多种,比如RabbitMQ,RocketMQ,Kafka等,但是也可以基于redis来实现,可以降低系统的维护成本和实现复杂度,本篇介绍redis中实现消息队列的几种方案,并通过springboot实战使其更易懂。

1. 基于List的 LPUSH+BRPOP 的实现

2. PUB/SUB,订阅/发布模式

3. 基于Stream类型的实现


1、基于List的的实现

原理

使用rpush和lpush操作入队列,lpop和rpop操作出队列。

List支持多个生产者和消费者并发进出消息,每个消费者拿到都是不同的列表元素。

优点

一旦数据到来则立刻醒过来,消息延迟几乎为零。

缺点

  • 不能重复消费,一旦消费就会被删除

  • 不能做广播模式 , 不支持分组消费

  • lpop和rpop会一直空轮训,消耗资源 ,但可以 引入阻塞读blpop和brpop 同时也有新的问题 如果线程一直阻塞在那里,Redis客户端的连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用,这个时候blpop和brpop或抛出异常

代码

引入依赖

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-jsr310</artifactId></dependency>

配置文件

server:port: ${SERVER_PORT:9210}# Spring
spring:application:# 应用名称name: ruoyi-redis-messageredis:host: localhostport: 6379password: 123456

启动类

@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
public class RuoYiRedisMessageApplication
{public static void main(String[] args){SpringApplication.run(RuoYiRedisMessageApplication.class, args);System.out.println("(♥◠‿◠)ノ゙  ruoyi-redis-message启动成功");}
}

添加redis配置类

/*** redis配置*/
@Configuration
public class RedisConfig {private static final RedisSerializer<Object> SERIALIZER = createSerializer();@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {// 创建 RedisTemplate 对象RedisTemplate<String, Object> template = new RedisTemplate<>();// 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。template.setConnectionFactory(factory);// 使用 String 序列化方式,序列化 KEY 。template.setKeySerializer(RedisSerializer.string());template.setHashKeySerializer(RedisSerializer.string());// 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。template.setValueSerializer(SERIALIZER);template.setHashValueSerializer(SERIALIZER);return template;}private static RedisSerializer<Object> createSerializer() {ObjectMapper mapper = new ObjectMapper();mapper.registerModules(new JavaTimeModule());// 此项必须配置,否则会报java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXXmapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);return new GenericJackson2JsonRedisSerializer(mapper);}}

队列方法

@Slf4j
@Service
public class ListRedisQueue {//队列名public static final String KEY = "listQueue";@Resourceprivate RedisTemplate redisTemplate;public void produce(String message) {redisTemplate.opsForList().rightPush(KEY, message);}public void consume() {while (true) {String msg = (String) redisTemplate.opsForList().leftPop(KEY);log.info("疯狂获取消息:" + msg);}}public void blockingConsume() {while (true) {List<Object> obj = redisTemplate.executePipelined(new RedisCallback<Object>() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {//队列没有元素会阻塞操作,直到队列获取新的元素或超时,5表示如果没元素就每五秒去拿一次消息return connection.bRPop(5, KEY.getBytes());}}, new StringRedisSerializer());for (Object str : obj) {log.info("blockingConsume获取消息 : {}", str);}}}}

测试

lPop/rPop消费数据

@Slf4j
@SpringBootTest
public class ListRedisTest {@Autowiredprivate ListRedisQueue listRedisQueue;@Testpublic void produce() {for (int i = 0; i < 5; i++) {listRedisQueue.produce("第"+i + "个数据");}}@Testpublic void consume() {produce();log.info("生产消息完毕");listRedisQueue.consume();}}

blpop / brpop 消费数据

    @Testpublic void blockingConsume() {produce();log.info("生产消息完毕");listRedisQueue.blockingConsume();}


2、PUB/SUB,订阅/发布模式

原理

SUBSCRIBE,用于订阅信道

PUBLISH,向信道发送消息

UNSUBSCRIBE,取消订阅

此模式允许生产者只生产一次消息,由中间件负责将消息复制到多个消息队列,每个消息队列由对应的消费组消费。

优点

  • 一个消息可以发布到多个消费者

  • 消费者可以同时订阅多个信道,因此可以接收多种消息(处理时先根据信道判断)

  • 消息即时发送,消费者会自动接收到信道发布的消息

缺点

  • 消息发布时,如果客户端不在线,则消息丢失

  • 消费者处理消息时出现了大量消息积压,则可能会断开通道,导致消息丢失

  • 消费者接收消息的时间不一定是一致的,可能会有差异(业务处理需要判重)

代码

配置消息监听器

@Slf4j
@Component
public class RedisMessageListener implements MessageListener {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 消息处理** @param message* @param pattern*/@Overridepublic void onMessage(Message message, byte[] pattern) {String channel = new String(pattern);log.info("onMessage --> 消息通道是:{}", channel);RedisSerializer<?> valueSerializer = redisTemplate.getValueSerializer();Object deserialize = valueSerializer.deserialize(message.getBody());log.info("反序列化的结果:{}", deserialize);if (deserialize == null) return;String md5DigestAsHex = DigestUtils.md5DigestAsHex(deserialize.toString().getBytes(StandardCharsets.UTF_8));log.info("计算得到的key: {}", md5DigestAsHex);Boolean result = redisTemplate.opsForValue().setIfAbsent(md5DigestAsHex, "1", 20, TimeUnit.SECONDS);if (Boolean.TRUE.equals(result)) {// redis消息进行处理log.info("接收的结果:{}", deserialize);} else {log.info("其他服务处理中");}}
}

实现MessageListener 接口,就可以通过onMessage()方法接收到消息了,该方法有两个参数:

  • 参数 message 的 getBody() 方法以二进制形式获取消息体, getChannel() 以二进制形式获取消息通道

  • 参数 pattern 二进制形式的消息通道(实际和 message.getChannel() 返回值相同)

绑定监听器

@Configuration
public class RedisMessageListenerConfig {@Beanpublic RedisMessageListenerContainer getRedisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory,RedisMessageListener redisMessageListener) {RedisMessageListenerContainer messageListenerContainer = new RedisMessageListenerContainer();messageListenerContainer.setConnectionFactory(redisConnectionFactory);messageListenerContainer.addMessageListener(redisMessageListener, new ChannelTopic(PubSubRedisQueue.KEY));messageListenerContainer.addMessageListener(redisMessageListener, new ChannelTopic(PubSubRedisQueue.KEY2));return messageListenerContainer;}
}

RedisMessageListenerContainer 是为Redis消息侦听器 MessageListener 提供异步行为的容器。处理侦听、转换和消息分派的低级别详细信息。

本文使用的是主题订阅:ChannelTopic,你也可以使用模式匹配:PatternTopic,从而匹配多个信道。

这里我们同一个监听器订阅了两个信道

生产者

@Service
public class PubSubRedisQueue {//队列名public static final String KEY = "pub_sub_queue";public static final String KEY2 = "pub_sub_queue2";@Autowiredprivate RedisTemplate<String, Object> redisTemplate;public void produce(String message) {redisTemplate.convertAndSend(KEY, message);}public void produce2(String message) {redisTemplate.convertAndSend(KEY2, message);}
}

测试

@Slf4j
@RestController
@RequestMapping(value = "/pubSubRedis")
@Api(tags = "pubSubRedis测试")
public class PubSubRedisController {@Autowiredprivate PubSubRedisQueue pubSubRedisQueue;@GetMapping(value = "/pubsub/produce")@ApiOperation(value = "测试")public void produce(@RequestParam(name = "msg") String msg) {pubSubRedisQueue.produce(msg);}@GetMapping(value = "/pubsub/produce2")@ApiOperation(value = "测试2")public void produce2(@RequestParam(name = "msg") String msg) {pubSubRedisQueue.produce2(msg);}
}

可以看到监听器成功监听了两个信道的信息


3、基于Stream类型的实现(Redis Version5.0)

原理

Stream为redis 5.0后新增的数据结构。支持多播的可持久化消息队列,实现借鉴了Kafka设计。

Redis Stream的结构如上图所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的ID和对应的内容。消息是持久化的,Redis重启后,内容还在。

每个Stream都有唯一的名称,它就是Redis的key,在我们首次使用xadd指令追加消息时自动创建。

每个Stream都可以挂多个消费组,每个消费组会有个游标last_delivered_id在Stream数组之上往前移动,表示当前消费组已经消费到哪条消息了。每个消费组都有一个Stream内唯一的名称,消费组不会自动创建,它需要单独的指令xgroup create进行创建,需要指定从Stream的某个消息ID开始消费,这个ID用来初始化last_delivered_id变量。

每个消费组(Consumer Group)的状态都是独立的,相互不受影响。也就是说同一份Stream内部的消息会被每个消费组都消费到。

同一个消费组(Consumer Group)可以挂接多个消费者(Consumer),这些消费者之间是竞争关系,任意一个消费者读取了消息都会使游标last_delivered_id往前移动。每个消费者者有一个组内唯一名称。

消费者(Consumer)内部会有个状态变量pending_ids,它记录了当前已经被客户端读取的消息,但是还没有ack。如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack,它就开始减少。这个pending_ids变量在Redis官方被称之为PEL,也就是Pending Entries List,这是一个很核心的数据结构,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。

优点

  1. 高性能:可以在非常短的时间内处理大量的消息。
  2. 持久化:支持数据持久化,即使Redis服务器宕机,也可以恢复之前的消息。
  3. 顺序性:保证消息的顺序性,即使是并发的消息也会按照发送顺序排列。
  4. 灵活性:可以方便地扩展和分布式部署,可以满足不同场景下的需求。

缺点

  1. 功能相对简单:Redis Stream相对于其他的消息队列,功能相对简单,无法满足一些复杂的需求。
  2. 不支持消息回溯:即消费者无法获取之前已经消费过的消息。
  3. 不支持多消费者分组:无法实现多个消费者并发消费消息的功能。

代码

自动ack消费者

@Slf4j
@Component
public class AutoAckStreamConsumeListener implements StreamListener<String, MapRecord<String, String, String>> {//分组名public static final String GROUP = "auto_ack_stream";@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic void onMessage(MapRecord<String, String, String> message) {String stream = message.getStream();RecordId id = message.getId();Map<String, String> map = message.getValue();log.info("[自动ACK]接收到一个消息 stream:[{}],id:[{}],value:[{}]", stream, id, map);redisTemplate.opsForStream().delete(GROUP, id.getValue());}
}

手动ack消费者

@Slf4j
@Component
public class BasicAckStreamConsumeListener implements StreamListener<String, MapRecord<String, String, String>> {//分组名public static final String GROUP = "basic_ack_stream";@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic void onMessage(MapRecord<String, String, String> message) {String stream = message.getStream();RecordId id = message.getId();Map<String, String> map = message.getValue();log.info("[手动ACK]接收到一个消息 stream:[{}],id:[{}],value:[{}]", stream, id, map);redisTemplate.opsForStream().acknowledge(stream, GROUP, id.getValue());//消费完毕删除该条消息redisTemplate.opsForStream().delete(GROUP, id.getValue());}
}

配置绑定关系

@Slf4j
@Configuration
public class RedisStreamConfiguration {@Autowiredprivate RedisConnectionFactory redisConnectionFactory;@Autowiredprivate AutoAckStreamConsumeListener autoAckStreamConsumeListener;@Autowiredprivate BasicAckStreamConsumeListener basicAckStreamConsumeListener;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Bean(initMethod = "start", destroyMethod = "stop")public StreamMessageListenerContainer<String, MapRecord<String, String, String>> streamMessageListenerContainer() {AtomicInteger index = new AtomicInteger(1);int processors = Runtime.getRuntime().availableProcessors();ThreadPoolExecutor executor = new ThreadPoolExecutor(processors, processors, 0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(), r -> {Thread thread = new Thread(r);thread.setName("async-stream-consumer-" + index.getAndIncrement());thread.setDaemon(true);return thread;});StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options =StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()// 一次最多获取多少条消息.batchSize(3)// 运行 Stream 的 poll task.executor(executor)// Stream 中没有消息时,阻塞多长时间,需要比 `spring.redis.timeout` 的时间小.pollTimeout(Duration.ofSeconds(3))// 获取消息的过程或获取到消息给具体的消息者处理的过程中,发生了异常的处理.errorHandler(throwable -> log.info("出现异常就来这里了" + throwable)).build();StreamMessageListenerContainer<String, MapRecord<String, String, String>> streamMessageListenerContainer =StreamMessageListenerContainer.create(redisConnectionFactory, options);// 独立消费// 消费组A,自动ack// 从消费组中没有分配给消费者的消息开始消费if (!isStreamGroupExists(StreamRedisQueue.KEY,AutoAckStreamConsumeListener.GROUP)){redisTemplate.opsForStream().createGroup(StreamRedisQueue.KEY,AutoAckStreamConsumeListener.GROUP);}streamMessageListenerContainer.receiveAutoAck(Consumer.from(AutoAckStreamConsumeListener.GROUP, "AutoAckConsumer"),StreamOffset.create(StreamRedisQueue.KEY, ReadOffset.lastConsumed()), autoAckStreamConsumeListener);// 消费组B,不自动ackif (!isStreamGroupExists(StreamRedisQueue.KEY,BasicAckStreamConsumeListener.GROUP)){redisTemplate.opsForStream().createGroup(StreamRedisQueue.KEY,BasicAckStreamConsumeListener.GROUP);}streamMessageListenerContainer.receive(Consumer.from(BasicAckStreamConsumeListener.GROUP, "BasicAckConsumer"),StreamOffset.create(StreamRedisQueue.KEY, ReadOffset.lastConsumed()), basicAckStreamConsumeListener);return streamMessageListenerContainer;}/*** 判断该消费组是否存在* @param streamKey* @param groupName* @return*/public boolean isStreamGroupExists(String streamKey, String groupName) {RedisStreamCommands commands = redisConnectionFactory.getConnection().streamCommands();//首先检查Stream Key是否存在,否则下面代码可能会因为尝试检查不存在的Stream Key而导致异常if (Boolean.FALSE.equals(redisTemplate.hasKey(streamKey))){return false;}//获取streamKey下的所有groupsStreamInfo.XInfoGroups xInfoGroups = commands.xInfoGroups(streamKey.getBytes());AtomicBoolean exists= new AtomicBoolean(false);assert xInfoGroups != null;xInfoGroups.forEach(xInfoGroup -> {if (xInfoGroup.groupName().equals(groupName)){exists.set(true);}});return exists.get();}
}

生产者

@Slf4j
@Service
public class StreamRedisQueue {//队列名public static final String KEY = "stream_queue";@Autowiredprivate RedisTemplate<String, Object> redisTemplate;public String produce(Map<String, String> value) {return Objects.requireNonNull(redisTemplate.opsForStream().add(KEY, value)).getValue();}public void createGroup(String key, String group){redisTemplate.opsForStream().createGroup(key, group);}}

测试

生产消息
@Slf4j
@RestController
@RequestMapping(value = "/streamRedis")
@Api(tags = "streamRedis测试")
public class StreamRedisController {@Autowiredprivate StreamRedisQueue streamRedisQueue;@GetMapping(value = "/stream/produce")@ApiOperation(value = "测试")public void streamProduce() {Map<String, String> map = new HashMap<>();map.put("刘德华", "大家好我是刘德华");map.put("周杰伦", "周杰伦");map.put("time", DateUtil.now());String result = streamRedisQueue.produce(map);log.info("返回结果:{}", result);}
}
只要有消息,消费者就会消费

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

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

相关文章

【C++动态版本号生成方案:实现类似C# 1.0.* 的自动构建号】

C动态版本号生成方案&#xff1a;实现类似C# 1.0.* 的自动构建号 在C#中&#xff0c;1.0.*版本号格式会在编译时自动生成构建号和修订号。本文将介绍如何在C项目中实现类似功能&#xff0c;通过MSBuild自动化生成基于编译时间的版本号。 实现原理 版本号构成&#xff1a;主版本…

【算法题】:斐波那契数列

用 JavaScript 实现一个 fibonacci 函数&#xff0c;满足&#xff1a; 输入 n&#xff08;从0开始计数&#xff09;输出第 n 个斐波那契数&#xff08;斐波那契数列从 1 开始&#xff1a;1,1,2,3,5,8,13,21…&#xff09; 示例&#xff1a; fibonacci(0) > 1fibonacci(4) &g…

【YOLOv13[基础]】热力图可视化实践 | 脚本升级 | 优化可视化效果 | 论文必备 | GradCAMPlusPlus, GradCAM, XGradCAM, EigenCAM等

本文将进行添加YOLOv13版本的升级版热力图可视化功能的实践,支持图像热力图可视化、优化可视化效果、 可以选择使用GradCAMPlusPlus, GradCAM, XGradCAM, EigenCAM, HiResCAM, LayerCAM, RandomCAM, EigenGradCAM。一个参数即可设置是否显示检测框等。 原图 结果图

ElasticSearch相关术语介绍

1.RESTful风格程序REST(英文全称为:"Representational State Transfer")指的是一组架构约束条件和原则。它是一种软件架构风格&#xff08;约束条件和原则的集合&#xff0c;但并不是标准&#xff09;。 REST通过资源的角度观察网络&#xff0c;以URI对网络资源进行…

《从零构建大语言模型》学习笔记4,注意力机制1

《从零构建大语言模型》学习笔记4&#xff0c;自注意力机制1 文章目录《从零构建大语言模型》学习笔记4&#xff0c;自注意力机制1前言一、实现一个简单的无训练权重的自注意力机制二、实现具有可训练权重的自注意力机制1. 分步计算注意力权重2.实现自注意力Python类三、将单头…

昇思+昇腾开发板+DeepSeek模型推理和性能优化

昇思昇腾开发板DeepSeek模型推理和性能优化 模型推理 流程&#xff1a; 权重加载 -> 启动推理 -> 效果比较与调优 -> 性能测试 -> 性能优化 权重加载 如微调章节介绍&#xff0c;最终的模型包含两部分&#xff1a;base model 和 LoRA adapter&#xff0c;其中base …

未给任务“Fody.WeavingTask”的必需参数“IntermediateDir”赋值。 WpfTreeView

c#专栏记录&#xff1a; 报错 未给任务“Fody.WeavingTask”的必需参数“IntermediateDir”赋值。 WpfTreeView 生成 解决办法 清理和重新生成项目 完成上述配置后&#xff0c;尝试执行以下步骤&#xff1a; 清理项目&#xff1a;删除 bin 和 obj 文件夹。 重新生成项目&…

[Linux]学习笔记系列 -- [arm][lib]

文章目录arch/arm/lib/delay.cregister_current_timer_delay 注册当前定时器延迟read_current_timer 读取当前定时器drivers/clocksource/timer-stm32.cstm32_clocksource_init STM32 平台上初始化时钟源https://github.com/wdfk-prog/linux-study arch/arm/lib/delay.c regis…

harbor仓库搭建(配置https)

目录 1. 环境准备 2. 配置https的原因 3. 生成ca证书 4. 搭建harbor仓库 5. 访问harbor 6. 修改加密算法 1. 环境准备 需要提前安装docker和docker-compose&#xff0c;harbor仓库版本越新&#xff0c;对应的docker和docker-compose版本越新。 主机IP192.168.48.19dock…

C++多线程服务器

C多线程服务器 因为自己同时在看多本书&#xff0c;之前看过《TCP/IP 网络编程》一书&#xff0c;其中有一个自己编写一个多线程服务器的例子&#xff0c;于是就把代码直接抄了一变。 在学习网络编程前需要先了解网络的7层模型。 具体代码如下&#xff1a; 服务器端&#xff1a…

【Pandas】常用数据处理技巧

一. 数据读取 1.pd.to_csv & pd.read_csv 细节&#xff1a; 1.pd.read_csv 需要 ignore_index True or ,index_col0 否则会有列Unnamed0 2.pickle具有更快的读取速度&#xff0c;与更小的体积。 读取前N行&#xff08;若不需获取所有数据&#xff09; pd.read_csv(…

Docker Compose 部署高可用 MongoDB 副本集集群(含 Keepalived + HAProxy 负载均衡)

Docker Compose 部署高可用 MongoDB 副本集集群&#xff08;含 Keepalived HAProxy 负载均衡&#xff09;背景与目标&#x1f4cb; 环境规划**服务器信息****软件版本**部署步骤1. 创建目录结构2、生成 keyFile&#xff08;三台机器内容必须一致&#xff09;3. 准备 Keepalive…

MySQL(189)如何分析MySQL的锁等待问题?

分析MySQL的锁等待问题有助于发现和解决数据库性能瓶颈。锁等待问题通常会导致数据库响应时间变长&#xff0c;影响系统的整体性能。以下是详细深入的方法和代码示例&#xff0c;帮助你分析和解决MySQL的锁等待问题。 一、锁的类型和概念 在MySQL中&#xff0c;主要有以下几种锁…

26.Scikit-learn实战:机器学习的工具箱

Scikit-learn实战&#xff1a;机器学习的工具箱 &#x1f3af; 前言&#xff1a;机器学习界的"宜家家具" 还记得第一次逛宜家的感受吗&#xff1f;琳琅满目的家具&#xff0c;每一件都有详细的说明书&#xff0c;组装简单&#xff0c;样式统一&#xff0c;关键是—…

wordpress文章摘要调用的3种方法

以下是WordPress文章摘要的3种调用方法&#xff1a; 1. 使用the_excerpt()函数 这是WordPress自带的函数&#xff0c;用于调用文章摘要。如果文章有手动填写的摘要&#xff0c;则会显示手动摘要;如果没有手动摘要&#xff0c;WordPress会自动从文章内容中提取前55个单词作为摘…

java excel转图片常用的几种方法

十分想念顺店杂可。。。在 Java 中实现 Excel 转图片&#xff0c;常用的方法主要分为两类&#xff1a;使用商业库&#xff08;简单高效但可能收费&#xff09;和使用开源库组合&#xff08;免费但实现复杂&#xff09;。以下是几种常用方案及实现思路&#xff1a;一、使用商业库…

QT项目 -仿QQ音乐的音乐播放器(第五节)

目录 一、CommonPage界⾯设置和显示 二、自定义ListItemBox 三、支持hover效果 四、自定义VolumeTool 五、界面设置 六、页面创建及弹出 七、绘制三角 一、CommonPage界面设置和显示 void CommonPage::setCommonPageUI(const QString &title, const QString &imag…

wstool和git submodule优劣势对比

wstool 和 git submodule 都可以用来管理项目中的外部源代码依赖&#xff0c;但它们的设计理念、工作流程和适用场景有很大不同。 我们来深入对比一下它们的优势和劣势。 核心理念比喻 git submodule&#xff1a;像是在你的汽车设计图纸中&#xff0c;直接嵌入了另一家公司&…

六、RuoYi-Cloud-Plus OSS文件上传配置

1.前面我们完成了RuoYi-Cloud-Plus 部署及启动&#xff0c;此刻已经可以正常访问。 前面文章的专栏内容在这&#xff0c;感兴趣可以看看。 https://blog.csdn.net/weixin_42868605/category_13023920.html 2.但现在虽然已经启动成功&#xff0c;但有很多功能我们依旧用不了&a…

达梦数据库日常运维命令

查询数据库表空间数据文件使用大小限制DECLARE K INT:(SELECT cast(PAGE()/1024 as varchar)); BEGIN SELECTF."PATH" 数据文件 ,F.CLIENT_PATH,G.NAME 所属表空间,F.MAX_SIZE||M 文件扩展限制,(CASE F.AUTO_EXTEND WHEN 1 THEN 是 ELSE 否 END) 文件…