MQ第02篇:RabbitMQ快速上手教程:AMQP模型详解+Spring Boot整合实战(附完整代码)

摘要:本文从AMQP协议模型出发,用"邮局分拣系统"的类比帮你快速理解RabbitMQ的Exchange、Queue、Binding核心概念,然后手把手用Spring Boot搭建一个"订单创建→库存扣减"的完整消息链路。全文包含可直接运行的代码、配置文件详解、管理控制台操作指南,以及5个初学者最容易踩的坑。适合有Java/Spring Boot基础、准备在生产环境使用RabbitMQ的开发者。

一、为什么要先理解AMQP模型?

上一篇我们讲了消息队列的三大核心价值------解耦、异步、削峰。但当你真正打开RabbitMQ的官方文档,看到Virtual Host、Exchange、Binding、Routing Key这一堆概念的时候,大概率会感到一头雾水。

这不是你的问题。RabbitMQ的设计哲学和Kafka、RocketMQ完全不同。Kafka和RocketMQ的核心抽象是Topic(主题),生产者往Topic发,消费者从Topic拉,中间没有额外的路由层。但RabbitMQ在生产者与队列之间插入了一层Exchange(交换机),消息必须经过Exchange的路由规则才能到达队列。

为什么要多这一层?答案很简单:为了让路由更灵活。在SaaS多租户场景中,你可能需要"所有租户的订单消息广播到审计服务,但只有VIP租户的订单消息路由到优先处理队列"------这种需求在RabbitMQ中通过Exchange的绑定规则就能实现,不需要改一行生产者的代码。

理解这一点后,AMQP模型的每个组件就都有了存在的理由。

二、AMQP核心模型:用邮局分拣系统来理解

AMQP 0-9-1是RabbitMQ实现的协议标准。整个模型可以用一个"邮局分拣系统"来类比:
#mermaid-svg-DCtmlEfR8GgJkTm9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DCtmlEfR8GgJkTm9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DCtmlEfR8GgJkTm9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DCtmlEfR8GgJkTm9 .error-icon{fill:#552222;}#mermaid-svg-DCtmlEfR8GgJkTm9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DCtmlEfR8GgJkTm9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DCtmlEfR8GgJkTm9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DCtmlEfR8GgJkTm9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DCtmlEfR8GgJkTm9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DCtmlEfR8GgJkTm9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DCtmlEfR8GgJkTm9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DCtmlEfR8GgJkTm9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DCtmlEfR8GgJkTm9 .marker.cross{stroke:#333333;}#mermaid-svg-DCtmlEfR8GgJkTm9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DCtmlEfR8GgJkTm9 p{margin:0;}#mermaid-svg-DCtmlEfR8GgJkTm9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DCtmlEfR8GgJkTm9 .cluster-label text{fill:#333;}#mermaid-svg-DCtmlEfR8GgJkTm9 .cluster-label span{color:#333;}#mermaid-svg-DCtmlEfR8GgJkTm9 .cluster-label span p{background-color:transparent;}#mermaid-svg-DCtmlEfR8GgJkTm9 .label text,#mermaid-svg-DCtmlEfR8GgJkTm9 span{fill:#333;color:#333;}#mermaid-svg-DCtmlEfR8GgJkTm9 .node rect,#mermaid-svg-DCtmlEfR8GgJkTm9 .node circle,#mermaid-svg-DCtmlEfR8GgJkTm9 .node ellipse,#mermaid-svg-DCtmlEfR8GgJkTm9 .node polygon,#mermaid-svg-DCtmlEfR8GgJkTm9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DCtmlEfR8GgJkTm9 .rough-node .label text,#mermaid-svg-DCtmlEfR8GgJkTm9 .node .label text,#mermaid-svg-DCtmlEfR8GgJkTm9 .image-shape .label,#mermaid-svg-DCtmlEfR8GgJkTm9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-DCtmlEfR8GgJkTm9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DCtmlEfR8GgJkTm9 .rough-node .label,#mermaid-svg-DCtmlEfR8GgJkTm9 .node .label,#mermaid-svg-DCtmlEfR8GgJkTm9 .image-shape .label,#mermaid-svg-DCtmlEfR8GgJkTm9 .icon-shape .label{text-align:center;}#mermaid-svg-DCtmlEfR8GgJkTm9 .node.clickable{cursor:pointer;}#mermaid-svg-DCtmlEfR8GgJkTm9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DCtmlEfR8GgJkTm9 .arrowheadPath{fill:#333333;}#mermaid-svg-DCtmlEfR8GgJkTm9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DCtmlEfR8GgJkTm9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DCtmlEfR8GgJkTm9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DCtmlEfR8GgJkTm9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DCtmlEfR8GgJkTm9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DCtmlEfR8GgJkTm9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DCtmlEfR8GgJkTm9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DCtmlEfR8GgJkTm9 .cluster text{fill:#333;}#mermaid-svg-DCtmlEfR8GgJkTm9 .cluster span{color:#333;}#mermaid-svg-DCtmlEfR8GgJkTm9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DCtmlEfR8GgJkTm9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DCtmlEfR8GgJkTm9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-DCtmlEfR8GgJkTm9 .icon-shape,#mermaid-svg-DCtmlEfR8GgJkTm9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DCtmlEfR8GgJkTm9 .icon-shape p,#mermaid-svg-DCtmlEfR8GgJkTm9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DCtmlEfR8GgJkTm9 .icon-shape .label rect,#mermaid-svg-DCtmlEfR8GgJkTm9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DCtmlEfR8GgJkTm9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DCtmlEfR8GgJkTm9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DCtmlEfR8GgJkTm9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 🏢 Virtual Host 虚拟主机
发送消息
Binding 规则
Binding 规则
推送/拉取
推送/拉取
📮 生产者

Producer
🏤 交换机

Exchange
📬 队列

Queue A
📭 队列

Queue B
👤 消费者A
👤 消费者B

邮局类比:

  • 生产者(Producer) 是寄件人,把包裹(消息)送到邮局
  • 交换机(Exchange) 是邮局的分拣员,根据包裹上的地址(Routing Key)和分拣规则(Binding)决定包裹去哪个货架
  • 队列(Queue) 是货架,包裹暂时存放在这里等待收件人取走
  • 绑定(Binding) 是分拣规则:告诉分拣员"这个地址的包裹放到这个货架"
  • 消费者(Consumer) 是收件人,从货架上取走自己的包裹
  • Virtual Host 是整个邮局大楼,不同的大楼之间完全隔离

这里有一个关键认知:生产者永远不直接把消息发送到队列,而是发送到Exchange,由Exchange决定消息的去向。这个设计是RabbitMQ路由灵活性的根基。

2.1 五种交换机类型逐一拆解

RabbitMQ内置了四种常用的交换机类型,每种对应不同的路由策略:

交换机类型 路由规则 类比 典型场景
Direct Routing Key精确匹配 快递按具体地址投递 单播:订单消息发给特定库存服务
Topic Routing Key通配符匹配 快递按区域分拣(如"北京.*") 多播:order.*.create匹配所有订单创建事件
Fanout 广播到所有绑定队列 群发通知,不区分地址 广播:配置变更通知所有服务
Headers 根据消息头属性匹配 按包裹属性(重量、易碎)分拣 复杂条件路由(实际使用较少)

Direct Exchange 是最基础的类型。消息的Routing Key必须与Binding Key完全一致,才会被路由到对应队列。在实际项目中,Direct常用于"点对点"场景------比如订单服务发送order.create消息,只有绑定了order.create的库存服务队列能收到。

Topic Exchange 是最灵活的类型。它支持两种通配符:*匹配一个单词,#匹配零个或多个单词。在SaaS多租户场景中,可以这样设计Routing Key:tenant.{tenantId}.order.created。审计服务绑定tenant.*.order.*就能收到所有租户的订单事件,而VIP租户的优先处理队列绑定tenant.vip.*.order.created只接收VIP租户的消息。

Fanout Exchange 是最快的类型,因为它不做任何路由判断,直接把消息复制到所有绑定的队列。适合"广播"场景------比如配置中心推送配置变更,所有订阅的服务都需要收到通知。

Headers Exchange 根据消息头中的键值对进行匹配,不依赖Routing Key。使用场景较少,但在需要"多条件组合路由"时很有用------比如只路由"source=payment AND priority=high"的消息。

三、Spring Boot整合实战:订单创建→库存扣减

理解了模型之后,我们用Spring Boot搭建一个完整的消息链路。场景是上一篇文章中改造后的下单流程:订单服务创建订单后,发送消息到MQ,库存服务订阅消息并扣减库存。

3.1 添加依赖

根据最新的Spring AMQP 4.2.0-M1版本信息,Spring Boot 4.x用户应使用以下依赖:

xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

注意 :从Spring Boot 4.1.0开始,RabbitMQ支持从spring-boot-amqp迁移到了spring-boot-rabbitmq,但starter的artifact ID目前仍保持spring-boot-starter-amqp不变。如果你使用的是Spring Boot 3.x,同样使用这个starter,版本由Spring Boot BOM统一管理。

3.2 配置文件

yaml 复制代码
# application.yml
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    # 生产者确认:异步回调模式,不阻塞业务线程
    publisher-confirm-type: correlated
    # 开启Return回调,处理未路由到队列的消息
    publisher-returns: true
    template:
      mandatory: true
      retry:
        enabled: true
        max-attempts: 3
        initial-interval: 1000ms
        multiplier: 2
    listener:
      simple:
        acknowledge-mode: manual   # 手动ACK,确保消息处理完成后再确认
        prefetch: 10               # 每个消费者最多预取10条未确认消息

为什么这样配? publisher-confirm-type: correlated开启异步确认模式,这是生产环境推荐的做法。同步模式(SIMPLE)会阻塞业务线程等待Broker回执,严重影响吞吐量。

acknowledge-mode: manual是可靠性保障的关键。默认的自动ACK模式下,消息一旦投递给消费者就被标记为已消费,如果消费者处理过程中抛异常,消息就丢了。

prefetch: 10限制每个消费者的未确认消息数量,防止一个消费者拉取过多消息导致其他消费者空闲,也避免消费者OOM。

3.3 定义消息常量与DTO

java 复制代码
public class MqConstants {
    public static final String ORDER_EXCHANGE = "order.exchange";
    public static final String ORDER_CREATE_QUEUE = "order.create.queue";
    public static final String ORDER_CREATE_ROUTING_KEY = "order.create";
}

public class OrderCreatedEvent implements Serializable {
    private String orderNo;
    private Long userId;
    private Long skuId;
    private Integer quantity;
    private BigDecimal amount;
    // getter/setter 省略
}

注意:虽然消息最终会用JSON序列化,但保持DTO的可序列化性是一个好习惯,万一需要在极端情况下回退到JDK序列化,至少不会报错。

3.4 消息发送端:订单服务

java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Transactional
    public Order createOrder(OrderCreateDTO dto) {
        // 1. 本地事务:创建订单
        Order order = new Order();
        order.setOrderNo(generateOrderNo());
        order.setUserId(dto.getUserId());
        order.setSkuId(dto.getSkuId());
        order.setQuantity(dto.getQuantity());
        order.setStatus(OrderStatus.CREATED);
        orderRepository.save(order);
        
        // 2. 构造消息事件
        OrderCreatedEvent event = new OrderCreatedEvent();
        event.setOrderNo(order.getOrderNo());
        event.setUserId(order.getUserId());
        event.setSkuId(order.getSkuId());
        event.setQuantity(order.getQuantity());
        event.setAmount(order.getAmount());
        
        // 3. 发送消息到Exchange(注意:发到Exchange,不是Queue)
        CorrelationData correlationData = new CorrelationData(order.getOrderNo());
        rabbitTemplate.convertAndSend(
            MqConstants.ORDER_EXCHANGE,
            MqConstants.ORDER_CREATE_ROUTING_KEY,
            event,
            correlationData
        );
        
        return order;
    }
}

为什么发送到Exchange而不是Queue? 这是AMQP协议的核心设计。生产者只需要知道"我要发送一个订单创建事件",不需要知道有哪些下游服务需要处理这个事件。后文配置Exchange绑定时,库存服务订阅order.create,审计服务也可以绑定同一个Routing Key,订单服务的代码不需要任何改动。

CorrelationData的作用 :配合publisher-confirm-type: correlated配置,每条消息带一个关联ID。当Broker返回ACK/NACK时,可以通过这个ID定位到具体是哪条消息。在实际生产中,如果收到NACK,应该把消息ID记录到失败日志表,由定时任务重试。

3.5 消息配置类:声明Exchange、Queue、Binding

java 复制代码
@Configuration
public class RabbitMQConfig {
    
    @Bean
    public DirectExchange orderExchange() {
        // durable=true:Broker重启后Exchange仍然存在
        return new DirectExchange(MqConstants.ORDER_EXCHANGE, true, false);
    }
    
    @Bean
    public Queue orderCreateQueue() {
        return new Queue(MqConstants.ORDER_CREATE_QUEUE, true);
    }
    
    @Bean
    public Binding orderCreateBinding() {
        // 把队列绑定到Exchange,指定Routing Key
        return BindingBuilder
            .bind(orderCreateQueue())
            .to(orderExchange())
            .with(MqConstants.ORDER_CREATE_ROUTING_KEY);
    }
}

踩过的坑 :如果你不声明Exchange和Queue,第一次运行时会报NOT_FOUND - no exchange 'order.exchange' in vhost '/'。Spring Boot虽然可以自动声明,但显式声明更可控,也方便团队review路由拓扑。注意durable参数------Exchange和Queue都应该是持久化的,否则Broker重启后拓扑丢失。

为什么用DirectExchange而不是TopicExchange? 这个场景中订单服务只发送一个order.create事件,Direct的精确匹配足够了。如果你后续要扩展成"按租户+事件类型"的多级路由,换成TopicExchange只需改一行配置。

3.6 消息消费端:库存服务

java 复制代码
@Component
@Slf4j
public class OrderMessageConsumer {
    
    @Autowired
    private StockService stockService;
    
    @RabbitListener(queues = MqConstants.ORDER_CREATE_QUEUE)
    public void onOrderCreated(OrderCreatedEvent event, Channel channel,
                               @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
        try {
            log.info("收到订单创建消息,orderNo={}", event.getOrderNo());
            
            // 1. 业务处理:扣减库存
            stockService.deductStock(event.getSkuId(), event.getQuantity());
            
            // 2. 手动ACK:告诉Broker"我处理完了"
            channel.basicAck(deliveryTag, false);
            log.info("库存扣减成功,orderNo={}", event.getOrderNo());
            
        } catch (BusinessException e) {
            // 3a. 业务异常(如库存不足):拒绝消息,不重新入队
            log.error("库存扣减失败,orderNo={}", event.getOrderNo(), e);
            channel.basicNack(deliveryTag, false, false);
            
        } catch (Exception e) {
            // 3b. 系统异常(如数据库超时):NACK + 重新入队
            log.error("系统异常,稍后重试,orderNo={}", event.getOrderNo(), e);
            channel.basicNack(deliveryTag, false, true);
        }
    }
}

为什么手动ACK要分两种异常? 这是生产环境中最容易踩的坑。basicNack(deliveryTag, false, true)的第三个参数requeue=true会把消息重新放回队列,如果失败原因是"库存不足"这种业务异常,重新入队会导致消息无限循环------消费者不断重试,永远无法成功。

正确的做法 :业务异常(确定性失败)用requeue=false,消息会被丢弃或进入死信队列;系统异常(临时性失败,如数据库超时)用requeue=true,给消费者重试机会。但即使这样,也应该配合重试次数限制,防止无限重入。

3.7 配置JSON序列化:覆盖默认的JDK序列化

Spring AMQP默认使用SimpleMessageConverter,它对于非String/byte\[\]/Serializable对象,消息体可能为null。而即使对象实现了Serializable,JDK序列化也存在严重问题:不可读(管理控制台看到的是一堆二进制)、跨语言不兼容、反序列化存在安全漏洞。

java 复制代码
@Configuration
public class RabbitMQConfig {
    
    @Bean
    public MessageConverter jsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

只需声明这个Bean,Spring Boot会自动将它应用到RabbitTemplate和@RabbitListener的MessageConverter上。发送时,消息体变成可读的JSON:

json 复制代码
{
    "orderNo": "ORD20261008001",
    "userId": 10086,
    "skuId": 2001,
    "quantity": 2,
    "amount": 199.00
}

踩过的坑 :配置Jackson2JsonMessageConverter后,管理控制台的"Get Message"功能仍然可能显示二进制内容------这是因为控制台使用自己的反序列化逻辑,不影响实际消费。另一个坑是Stack Overflow上常有人遇到的"double encode":JSON字符串被序列化了两次。原因通常是RabbitTemplate和@RabbitListener的转换器配置不一致,或者发送方发送了String类型,转换器又对其做了一次JSON序列化。解决方案是确保发送DTO对象而不是JSON字符串。

四、RabbitMQ管理控制台使用指南

RabbitMQ安装后,管理界面默认是关闭的。启用命令:

bash 复制代码
# 启用管理插件
rabbitmq-plugins enable rabbitmq_management
# 重启服务后访问 http://localhost:15672
# 默认用户名/密码:guest/guest(仅限localhost访问)

管理控制台是排查消息问题的重要工具。日常使用中重点关注以下几个功能:

Queues页面是使用频率最高的页面。这里可以看到每个队列的Ready(待消费消息数)、Unacked(已投递但未确认消息数)、Total(总数)。如果Ready数持续增长,说明消费者处理速度跟不上生产速度,需要扩容消费者或优化消费逻辑。如果Unacked数持续偏高,可能是消费者处理超时或忘记ACK。

Exchanges页面可以查看Exchange的绑定关系。如果消息发送后没有到达预期队列,第一件事就是来这里检查Binding是否正确。在"Bindings"区域可以看到Exchange绑定了哪些队列,以及对应的Routing Key。

Connections和Channels页面 用于排查连接泄漏。如果你发现连接数异常增长,可能是CachingConnectionFactory的缓存配置不合理,或者消费者使用了手动ACK但始终未确认,导致Channel被阻塞。

踩过的坑 :guest用户默认只能从localhost登录。在Docker或远程服务器上部署时,必须创建新用户并赋予权限,否则你会看到"Login was refused using authentication mechanism PLAIN"的错误。

五、5个初学者最容易踩的坑

坑1:以为消息发送到了Queue,实际上是发到了Exchange。 convertAndSend的第一个参数是Exchange名称,不是Queue名称。如果Exchange不存在,消息会被直接丢弃(Broker不会报错),除非你开启了publisher-returns: true和mandatory: true。这个坑在初学者中非常普遍------消息发了,消费者收不到,查了半天日志也没报错。

坑2:没有配JSON序列化,管理控制台看到乱码。 默认的JDK序列化在管理控制台显示为不可读的二进制。更严重的是,如果生产者和消费者的JDK序列化版本不一致,反序列化会直接失败。配置Jackson2JsonMessageConverter是生产环境的必选项。

坑3:@RabbitListener默认自动ACK,消费者抛异常时消息丢失。 自动ACK模式下,消息一投递就被确认,消费者处理失败也不会有重试机会。如果你的业务对可靠性有要求,必须在配置文件中设置acknowledge-mode: manual,并在代码中显式调用basicAck。

坑4:basicNack的requeue=true导致消息无限循环。 当失败原因是"库存不足"这类业务异常时,重新入队只会让消费者反复失败。正确做法是区分业务异常(不requeue)和系统异常(requeue),并配合重试次数限制和死信队列。

坑5:没有开启publisher-confirm,消息丢了都不知道。 默认情况下,convertAndSend不会返回投递结果。如果Broker没有收到消息,生产者完全不知情。开启publisher-confirm-type: correlated后,通过CorrelationData可以获取每条消息的ACK/NACK回执。

六、总结与下一篇预告

这篇文章我们完成了三件事:理解了AMQP协议的核心模型(Exchange、Queue、Binding),用Spring Boot搭建了完整的"订单创建→库存扣减"消息链路,并配置了JSON序列化和手动ACK这两项生产环境必备设置。

但你可能已经注意到,代码中还有几个悬而未决的问题:

  • basicNack配合requeue=true后,如果消息反复失败,最终去了哪里?
  • 如何实现"订单创建后30分钟未支付,自动取消"这种延迟消息?
  • 生产环境中如何优雅地处理消费失败的消息?

这些就是下一篇的主题------RabbitMQ进阶:交换机路由、死信队列与延迟消息。我们将用死信队列实现订单超时取消的完整方案,并告诉你为什么TTL+DLX方案存在"队头阻塞"风险,以及延迟交换机插件为什么是更好的选择。

📖 下一篇预告:RabbitMQ进阶实战------Topic路由+死信队列+延迟消息,SaaS订单超时取消完整方案。

相关推荐
RuoyiOffice3 小时前
SpringBoot3 企业定时结算架构:考勤日结、合同到期、库存预警与财务结转如何可靠执行
spring boot·xxl-job·定时任务·分布式任务·spring boot 3·幂等设计·ruoyi office
天空鸟_时光不老4 小时前
13-亮点提炼:把技术说辞翻译成业务价值
java·spring boot·spring·spring cloud·mybatis
小蒜学长4 小时前
基于SpringBoot+Vue的租房管理系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·租房管理
洋就在江州4 小时前
gitlab-cicd 离线集成——springboot-cicd (非docker形式,shell形式)
java·spring boot·后端·ci/cd·gitlab·gitlab-runner
yexianglunbai4 小时前
RabbitMQ 详解:从入门到实战
分布式·rabbitmq
令狐少侠20114 小时前
centos7 下,使用docker 部署IOT 调试平台,ThingsBoard 并完美启动
java·spring boot·iot
Wang's Blog5 小时前
Java 中间件之 RabbitMQ 快速入门: MQ 常见技术选型对比
java·中间件·java-rabbitmq
省钱兄--zs6 小时前
24小时自助健身房系统软件开发实战:从架构设计到部署全指南
java·数据仓库·spring boot·系统架构·需求分析
IT研究室6 小时前
最新计算机毕业设计选题推荐-基于spring boot的智慧教学资源管理与互动交流平台-网站-文档指导-Java-springboot
java·spring boot·课程设计