从0-1一文详解RabbitMQ

前言

RabbitMQ 是微服务中常见的消息中间件,主要用来完成异步通信、服务解耦和流量削峰。本文从同步调用的问题开始,依次讲解 RabbitMQ 的核心对象、Spring Boot 集成、Work Queue、交换机、消息可靠性、业务幂等和延迟消息,并给出可以直接改造到 Spring AMQP 项目中的代码示例。

一、为什么需要消息队列

同步调用中,调用方必须等待被调用方返回结果。以支付为例,支付服务扣款后还要同步调用订单、通知、积分等服务。下游服务越多,调用链越长,支付请求的响应时间越难控制;其中一个服务变慢或故障,还可能占满上游线程,引发级联失败。

异步调用把"通知下游处理"变成一条消息。发送方把消息交给消息代理后即可继续执行,消费者在自己的节奏下处理消息。这样可以减少服务之间的直接依赖,并用队列暂存突发流量。

异步调用适合"后续动作依赖前置结果,但前置服务不需要等待后续结果"的场景,例如支付成功后通知订单服务更新状态、通知服务发送消息、积分服务增加积分。若下一步必须立即拿到上一步的返回值,仍应使用同步调用。

异步方案需要接受几个代价:调用方不能立即得到下游结果,消息代理或消费者故障需要补偿,消息可能重复投递。因此,可靠性和幂等性必须一起设计。

二、RabbitMQ基础概念与环境准备

1. RabbitMQ的核心对象

RabbitMQ中最重要的对象有五个:

  • Publisher:发送消息的生产者。
  • Exchange:交换机,接收消息并根据路由规则转发。
  • Queue:队列,保存等待消费的消息。
  • Consumer:消费者,从队列接收并处理消息。
  • VirtualHost:虚拟主机,用于隔离不同业务的交换机、队列、绑定和权限。

消息的典型路径是:

text 复制代码
Publisher -> Exchange -> Binding + RoutingKey -> Queue -> Consumer

交换机只负责路由和转发,不负责长期存储消息。生产者直接向队列发送时,使用的是 RabbitMQ 的默认交换机;进入真实业务后,通常显式声明业务交换机和绑定关系。

2. Spring AMQP依赖和连接配置

Spring Boot 集成 RabbitMQ 时需要引入 spring-boot-starter-amqp:

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

连接配置至少要保持 host、port、virtual-host、username 和 password 一致:

yaml 复制代码
spring:
  rabbitmq:
    host: localhost
    port: 5672
    virtual-host: /hmall
    username: ******
    password: ******
    connection-timeout: 1s

5672 是 AMQP 客户端端口,15672 是管理页面端口。连接到哪个 VirtualHost,决定了应用能看到哪一组交换机和队列。

三、Spring AMQP的基本收发

1. 生产者发送消息

Spring AMQP通过 RabbitTemplate 封装了连接、信道和消息转换。发送字符串到队列可以直接调用:

java 复制代码
@Autowired
private RabbitTemplate rabbitTemplate;

@Test
void testSend() {
    String queueName = "test.quene";
    String message = "hello world";
    rabbitTemplate.convertAndSend(queueName, message);
}

第一个参数是目标名称。这里传入队列名,消息会经由默认交换机按队列名路由;第二个参数是消息体。convertAndSend 会根据消息转换器把 Java 对象转换为 RabbitMQ 消息。

2. 消费者监听队列

消费者使用 @RabbitListener 声明监听目标:

java 复制代码
@Component
public class MqListener {

    @RabbitListener(queues = "test.quene")
    public void listenSimpleQuene(String message) {
        System.out.println("消费者收到了test.quene消息:" + message);
    }
}

应用启动后,Spring AMQP会为监听方法创建消费者。方法参数接收消息体,方法正常返回时消息会进入确认流程;如果方法抛出异常,则由确认模式和重试配置决定消息是否重新投递。

3. 对象消息使用JSON转换

默认的消息转换器可能使用 JDK 序列化,消息在管理页面上可读性较差,也会带来更大的体积和反序列化风险。项目在 publisher 启动类中配置了 JSON 转换器:

java 复制代码
@Bean
public MessageConverter messageConverter() {
    Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
    converter.setCreateMessageIds(true);
    return converter;
}

Jackson2JsonMessageConverter 负责把对象转换为 JSON;setCreateMessageIds(true) 会为消息生成 messageId,为后续去重提供消息级标识。发送 Map 时:

java 复制代码
Map<String, Object> map = new HashMap<>(2);
map.put("name", "张三");
map.put("age", 18);
rabbitTemplate.convertAndSend("Object.queue", map);

消费者应使用与发送端匹配的类型接收消息,例如使用 Map<String, Object> 接收上述 JSON 对象。生产者和消费者的消息转换器必须保持兼容,不能让同一队列中同时混用无法互相解析的序列化格式。

四、Work Queue:多个消费者共同处理一个队列

当单个消费者处理速度跟不上生产速度时,消息会在队列中堆积。Work Queue 让多个消费者监听同一个队列,同一条消息只会交给其中一个消费者处理,适合把任务分摊给多个实例。

生产者可以在短时间内发送一批消息:

java 复制代码
@Test
void testSimple() throws Exception {
    String queueName = "simple.quene";
    for (int i = 1; i <= 50; i++) {
        String message = "hello simple:" + i;
        rabbitTemplate.convertAndSend(queueName, message);
        Thread.sleep(20);
    }
}

消费者端配置两个监听方法:

java 复制代码
@RabbitListener(queues = "simple.quene")
public void listenSimpleQuene1(String message) throws InterruptedException {
    System.out.println("消费者收到了simple.quene消息:" + message);
    Thread.sleep(20);
}

@RabbitListener(queues = "simple.quene")
public void listenSimpleQuene2(String message) throws InterruptedException {
    System.out.println("消费者收到了simple.quene消息..................:" + message);
    Thread.sleep(200);
}

默认分发方式接近轮询,消费者能力不同也可能拿到相近数量的消息。项目在 consumer 模块配置:

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1

prefetch: 1 表示消费者处理完当前消息后再获取下一条。这样处理速度快的消费者可以继续领取任务,处理速度慢的消费者不会提前积压一批未完成消息。

五、交换机决定消息如何路由

1. Fanout:广播到所有绑定队列

Fanout 交换机忽略 routing key,把消息发送给所有绑定队列。一个事件需要被多个业务同时处理时,可以使用广播模式。

可以通过配置类声明交换机、队列和绑定:

java 复制代码
@Configuration
public class FanoutConfiguration {

    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange("app.fanout");
    }

    @Bean
    public Queue fanoutQueue3() {
        return new Queue("notification.queue", true);
    }

    @Bean
    public Queue fanoutQueue4() {
        return new Queue("points.queue", true);
    }

    @Bean
    public Binding bindingFanoutQueue3(Queue fanoutQueue3,
                                      FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutQueue3).to(fanoutExchange);
    }

    @Bean
    public Binding bindingFanoutQueue4(Queue fanoutQueue4,
                                      FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanoutQueue4).to(fanoutExchange);
    }
}

这里两个队列都绑定到同一个 FanoutExchange,因此同一条消息会分别进入两个队列。队列名称必须稳定,生产者发送时使用交换机名:

java 复制代码
rabbitTemplate.convertAndSend("app.fanout", "", "hello everyone");

2. Direct:按精确 routing key 路由

Direct 交换机要求 binding key 与消息的 routing key 精确匹配。下面两个监听器分别声明自己的队列和绑定键:

java 复制代码
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "direct.queue1", durable = "true"),
        exchange = @Exchange(
                name = "hmall.direct",
                type = ExchangeTypes.DIRECT),
        key = {"blue", "red"}
))
public void listenDirectQueue1(String message) {
    System.out.println("消费者收到了direct.queue1消息:" + message);
}

@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "direct.queue2", durable = "true"),
        exchange = @Exchange(
                name = "hmall.direct",
                type = ExchangeTypes.DIRECT),
        key = {"yellow", "red"}
))
public void listenDirectQueue2(String message) {
    System.out.println("消费者收到了direct.queue2消息..................:" + message);
}

发送 routing key 为 blue 时,只会匹配 direct.queue1;发送 red 时,两个队列都会匹配。Direct 适合路由条件明确、值域有限的场景。

3. Topic:用通配符匹配 routing key

Topic 交换机在 Direct 的精确匹配基础上增加通配符:* 匹配一个单词,# 匹配零个或多个单词。例如 japan.* 可以匹配 japan.news,#.news 可以匹配 japan.news 或 sports.japan.news。

发送 Topic 消息时,可以把业务领域和事件类型写入 routing key:

java 复制代码
rabbitTemplate.convertAndSend("hmall.topic", "japan.news", "日本爆炸了");

Topic 适合把业务领域、事件类型等信息编码进 routing key,再由多个队列按模式订阅。选择交换机时,可以按"全部接收、精确匹配、模式匹配"分别对应 Fanout、Direct、Topic。

六、从发送到入队:生产者可靠性

消息可能在生产者到 RabbitMQ 的连接阶段失败,也可能到达交换机后没有匹配到队列。生产者可靠性通常包括 RabbitTemplate 操作重试、Publisher Confirm 和 Publisher Return:

yaml 复制代码
spring:
  rabbitmq:
    connection-timeout: 1s
    template:
      retry:
        enabled: true
        initial-interval: 200ms
        multiplier: 2
        max-attempts: 3
      mandatory: true
    publisher-confirm-type: correlated
    publisher-returns: true

connection-timeout 只控制建立连接的超时时间,template.retry 控制 RabbitTemplate 操作失败后的重试,两者作用不同。重试可以应对短暂网络抖动,但同步重试会占用发送线程,重试次数和等待时间需要结合接口响应要求设置。

Publisher Confirm 用来确认 RabbitMQ 是否接收了发布请求;correlated 模式通过 CorrelationData 异步关联每条消息的回执。Confirm不代表消息已经被消费者处理。Publisher Return 用来处理"消息已到交换机,但无法路由到队列"的情况,需要开启 mandatory,并且每个 RabbitTemplate 只能设置一个 ReturnCallback:

java 复制代码
@Configuration
@Slf4j
public class MqConfirmConfig implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        RabbitTemplate rabbitTemplate =
                applicationContext.getBean(RabbitTemplate.class);
        rabbitTemplate.setReturnsCallback(returned -> log.debug(
                "收到消息的return callback, exchange:{}, key:{}, msg:{}, code:{}, text:{}",
                returned.getExchange(),
                returned.getRoutingKey(),
                returned.getMessage(),
                returned.getReplyCode(),
                returned.getReplyText()));
    }
}

发送端可以使用 CorrelationData 处理确认:

java 复制代码
CorrelationData correlationData =
        new CorrelationData(UUID.randomUUID().toString());
correlationData.getFuture().addCallback(
        new ListenableFutureCallback<CorrelationData.Confirm>() {
            @Override
            public void onFailure(Throwable ex) {
                log.error("消息发送失败", ex);
            }

            @Override
            public void onSuccess(@Nullable CorrelationData.Confirm result) {
                if (result != null && result.isAck()) {
                    log.info("消息发送成功");
                } else {
                    log.error("消息发送失败,原因:{}", result.getReason());
                }
            }
        });

rabbitTemplate.convertAndSend("hmall.direct", "red", "hello", correlationData);

确认成功只能说明发布链路达到了对应阶段,业务仍要记录失败消息、处理无法路由的消息,并决定是否重试。错误的交换机名、routing key 或绑定关系,应优先修正配置,而不是无限重发。

七、RabbitMQ消息持久化

RabbitMQ的持久化需要同时考虑三个对象:

  1. 交换机持久化,保证交换机重启后仍存在。
  2. 队列持久化,保证队列重启后仍存在。
  3. 消息持久化,保证消息可以被写入磁盘。

只持久化队列而不持久化消息,仍然可能在 RabbitMQ 重启时丢失消息;只持久化消息而没有持久化队列,也无法恢复完整路由链路。持久化交换机、队列和持久化消息是三个独立设置,生产环境应在控制台或代码中明确检查 durable 和 delivery mode。

1. 交换机和队列持久化

编程式声明交换机和队列时,可以明确指定持久化:

java 复制代码
@Bean
public DirectExchange orderExchange() {
    return new DirectExchange("order.direct", true, false);
}

@Bean
public Queue orderQueue() {
    return new Queue("order.queue", true);
}

第二个参数 true 表示交换机或队列持久化。RabbitMQ重启后,持久化对象可以重新加载;临时交换机或临时队列则可能随着连接或服务结束而消失。

2. 消息持久化

发送消息时,可以通过消息属性指定持久化投递模式:

java 复制代码
Message message = MessageBuilder
        .withBody("订单支付成功".getBytes(StandardCharsets.UTF_8))
        .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
        .build();

rabbitTemplate.convertAndSend(
        "order.direct", "order.paid", message);

只有交换机、队列和消息都满足持久化要求,RabbitMQ重启后才有机会恢复完整消息链路。持久化会增加磁盘写入开销,发布确认也应与持久化策略一起考虑。

3. Lazy Queue和消息堆积

消费者处理速度过慢时,消息会在队列中积压。旧版本 RabbitMQ中可以使用 x-queue-mode=lazy 让消息更早写入磁盘,降低内存占用。

java 复制代码
@Bean
public Queue lazyQueue() {
    return QueueBuilder
            .durable("lazy.queue")
            .withArgument("x-queue-mode", "lazy")
            .build();
}

RabbitMQ 3.12及之后,经典队列的实际行为已经接近历史上的惰性队列,旧的 x-queue-mode=lazy 配置可能被忽略。使用时应以实际 RabbitMQ 版本的官方文档和管理页面为准,不要把旧版本配置直接当成所有版本都适用的结论。

消息持久化会带来磁盘写入开销,发布确认在持久化完成后才有意义。消息大量堆积时要关注内存、磁盘和消费者处理速度,避免把持久化当成无限扩容手段。

八、消费者确认、重试和失败消息

消费者确认用于告诉 RabbitMQ 消息处理结果:

  • ack:处理成功,消息从队列中删除。
  • nack:处理失败,可以要求消息重新入队。
  • reject:拒绝消息并丢弃,或交给死信流程。

Spring AMQP的 acknowledge-mode 常见有三种:

  • none:消息投递给消费者后立即确认,业务失败也可能丢消息。
  • manual:业务代码显式调用确认 API,控制最细,但会增加业务代码复杂度。
  • auto:Spring 根据监听方法是否正常返回来确认或拒绝消息,适合大多数普通消费场景。

普通队列监听器可以配置为 auto 模式:

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto

对于简单业务,auto 模式可以让消息处理成功时自动 ack,抛出异常时进入失败处理流程。关键业务要结合重试、幂等和异常队列一起使用。

如果异常消息一直重新入队,会形成"消费失败 -> requeue -> 再次消费"的无限循环。Spring Retry 可以把有限次数的重试放在消费者本地:

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto
        retry:
          enabled: true
          initial-interval: 1000ms
          multiplier: 1
          max-attempts: 3
          stateless: true

max-attempts 是总尝试次数,initial-interval 是第一次失败后的等待时间,multiplier 控制后续等待时间的增长。是否使用 stateless,要根据业务是否依赖事务上下文判断。

重试耗尽后,MessageRecoverer 决定消息去向:

  • RejectAndDontRequeueRecoverer:拒绝并丢弃,默认策略。
  • ImmediateRequeueMessageRecoverer:重新入队,适合短暂故障,但要防止循环。
  • RepublishMessageRecoverer:把失败消息重新发布到异常交换机,保留失败原因,便于人工处理和补偿。

可以声明异常交换机、异常队列和恢复器,核心配置如下:

java 复制代码
@Bean
public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
    return new RepublishMessageRecoverer(
            rabbitTemplate, "error.direct", "error");
}

交换机名称、队列名称和恢复器目标必须保持一致,否则重试耗尽后消息无法按预期进入异常队列。异常队列不应被当成普通业务队列长期堆积,需要配套人工处理、补偿或告警机制。

九、业务幂等:允许重复投递但不能重复生效

消息系统为了可靠性可能重复投递,消费者重试也可能让同一业务被再次执行。幂等的目标是:同一业务执行一次或多次,对最终业务状态的影响相同。

一种通用做法是为每条消息设置唯一 ID:

  1. 生产者生成业务唯一 ID,并和消息一起发送。
  2. 消费者处理业务成功后,把 ID 写入去重表或业务记录。
  3. 再次收到相同 ID 时,先查询处理记录,已处理则直接确认并跳过。

项目启用了 JSON 消息 ID:

java 复制代码
converter.setCreateMessageIds(true);

消息ID只是消息属性,不能自动完成去重。消费者仍需要把已经处理过的消息ID保存到数据库或其他可靠存储中:

java 复制代码
@RabbitListener(queues = "order.queue")
public void listenOrder(OrderMessage message) {
    if (messageLogService.exists(message.getMessageId())) {
        return;
    }

    orderService.updateOrderStatus(message.getOrderId(), "PAID");
    messageLogService.save(message.getMessageId());
}

实际项目中,业务处理和去重记录最好放在同一个事务中,或者通过数据库唯一索引保证同一个消息ID只能成功写入一次。

更稳妥的做法是使用业务本身的唯一约束。例如支付成功后更新订单状态,更新前先查询订单当前状态,只有"未支付"状态才执行更新;已经支付的订单再次收到消息时直接返回。这样做比单纯依赖消息 ID 更贴近业务,也便于处理消息重发。

支付服务、交易服务之间的最终一致性可以按三层保障:生产者确认保证消息尽量到达,RabbitMQ持久化减少服务重启造成的丢失,消费者确认和幂等更新保证重复或失败处理不会破坏业务状态。必要时再使用定时任务定期对账,补偿长时间未同步的订单。

十、延迟消息与延迟消息插件

延迟消息在发送时指定等待时间,消费者在延迟时间到达后才收到消息。它适合订单超时关闭、定时检查支付状态、延时通知等场景。

1. 死信交换机

消息出现以下情况之一时,会成为死信:

  • 消费者使用 reject 或 nack 拒绝消息,并且不重新入队。
  • 消息达到队列或消息本身的过期时间。
  • 队列达到长度上限,旧消息被淘汰。

队列通过 dead-letter-exchange 指定交换机后,死信会被重新投递到该交换机,再由交换机路由到死信队列。利用"过期后进入死信队列"的过程,可以间接实现延迟消费;不过 TTL 消息通常要等到期消息到达队首后才会被处理,因此它不是精确的定时器。

下面的配置让消息先进入等待队列,30秒后过期,再通过死信交换机进入真正的业务队列:

java 复制代码
@Bean
public DirectExchange delaySourceExchange() {
    return new DirectExchange("delay.source", true, false);
}

@Bean
public DirectExchange deadLetterExchange() {
    return new DirectExchange("dlx.direct", true, false);
}

@Bean
public Queue delaySourceQueue() {
    return QueueBuilder.durable("delay.source.queue")
            .ttl(30000)
            .deadLetterExchange("dlx.direct")
            .deadLetterRoutingKey("expired")
            .build();
}

@Bean
public Queue deadLetterQueue() {
    return new Queue("dead-letter.queue", true);
}

@Bean
public Binding sourceBinding(Queue delaySourceQueue,
                             DirectExchange delaySourceExchange) {
    return BindingBuilder.bind(delaySourceQueue)
            .to(delaySourceExchange)
            .with("delay");
}

@Bean
public Binding deadLetterBinding(Queue deadLetterQueue,
                                 DirectExchange deadLetterExchange) {
    return BindingBuilder.bind(deadLetterQueue)
            .to(deadLetterExchange)
            .with("expired");
}

ttl(30000) 设置队列中消息的存活时间,deadLetterExchange 指定过期消息的目标交换机,deadLetterRoutingKey 指定死信路由键。生产者把消息发到 delay.source 后,消息会先进入等待队列,过期后再被转发到 dead-letter.queue。

2. 安装rabbitmq-delayed-message-exchange插件

RabbitMQ提供了 rabbitmq-delayed-message-exchange 插件,可以让交换机本身暂存延迟消息。插件版本要尽量匹配当前 RabbitMQ 版本,插件文件不是 Maven 依赖,需要安装到 RabbitMQ 服务端。

bash 复制代码
# 查看RabbitMQ容器的插件目录
docker exec rabbitmq rabbitmq-plugins directories -s

# 将下载的插件文件复制到插件目录,实际目录以命令输出为准
docker cp rabbitmq_delayed_message_exchange-<version>.ez \
  rabbitmq:/opt/rabbitmq/plugins/

# 启用延迟消息插件
docker exec rabbitmq \
  rabbitmq-plugins enable rabbitmq_delayed_message_exchange

# 重启RabbitMQ容器
docker restart rabbitmq

启用插件后,RabbitMQ才能识别 x-delayed-message 类型的交换机。插件缺失时,声明 delayed = "true" 的交换机会失败。

3. 声明延迟交换机和队列

Spring AMQP可以通过 @Exchange 声明延迟交换机:

java 复制代码
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "delay.queue", durable = "true"),
        exchange = @Exchange(
                name = "delay.direct",
                delayed = "true",
                type = ExchangeTypes.DIRECT),
        key = "delay"
))
public void listenDelayQueue(String message) {
    System.out.println("消费者收到了delay.queue消息:" + message);
}

delayed = "true" 表示这个交换机依赖延迟消息插件,key 是消息发送时使用的 routing key。声明队列和交换机后,生产者还必须在消息属性中设置延迟时间。

4. 发送延迟消息

发送时通过 MessagePostProcessor 设置延迟毫秒数:

java 复制代码
rabbitTemplate.convertAndSend(
        "delay.direct",
        "delay",
        "Hello, DelayQueue!",
        message -> {
            message.getMessageProperties().setDelay(10000);
            return message;
        });

setDelay(10000) 表示延迟10秒。延迟时间越长、消息量越大,代理需要维护的延迟消息越多,CPU 和存储压力也会增加。订单超时场景还要考虑消息堆积:很多订单可能在等待期内已经完成支付,消费者收到检测消息后仍需再次查询订单状态,而不能直接执行取消操作。

如果多个业务都需要发送延迟消息,可以把设置延迟时间的逻辑封装成可复用的消息处理器:

java 复制代码
public class DelayMessagePostProcessor
        implements MessagePostProcessor {

    private final int delay;

    public DelayMessagePostProcessor(int delay) {
        this.delay = delay;
    }

    @Override
    public Message postProcessMessage(Message message) {
        message.getMessageProperties().setDelay(delay);
        return message;
    }
}

调用时只需要传入延迟时间:

java 复制代码
rabbitTemplate.convertAndSend(
        "delay.direct",
        "delay",
        "订单超时检测",
        new DelayMessagePostProcessor(30 * 60 * 1000));

延迟插件适合延迟时间较短、消息量可控的场景。延迟消息过多时,RabbitMQ需要维护更多定时任务,会增加 CPU、内存和磁盘压力。消费者收到订单检测消息后,仍然要查询订单当前状态,只有订单确实未支付时才能执行取消操作。

总结

RabbitMQ的基础使用可以归纳为一条链路:生产者通过 RabbitTemplate 发送消息,交换机按类型和 routing key 路由到队列,消费者通过 @RabbitListener 接收并处理。Work Queue解决消费能力不足,Fanout、Direct、Topic解决不同的分发需求。

可靠性要同时覆盖生产者、RabbitMQ和消费者:发布确认与返回处理发送结果,持久化减少服务重启丢失,消费者确认和有限重试处理异常,异常交换机保留失败消息,幂等设计避免重复消费造成业务错误。延迟消息适合订单超时和定时检查,但消费者最终仍应以数据库中的业务状态为准。

参考资料

相关推荐
考虑考虑4 小时前
JDK26中的List.ofLazy()
java·后端·java ee
小蒜学长4 小时前
基于SpringBoot的公寓报修管理系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·公寓报修管理系统·多角色协同
for_ever_love__4 小时前
MySQL 主从复制讲透:binlog 原理、搭建步骤与读写分离
java·数据库·mysql·binlog·读写分离·原理·主从复制
云浪5 小时前
Go源码分析:搞懂 Go 是如何实现堆的
后端·go·源码阅读
小π军7 小时前
操作系统面试题
java·linux·服务器
倔强的石头1067 小时前
应用账号最小权限实践:读写账号、报表账号、运维账号分层
java·运维·开发语言
Shaoshing7 小时前
ThreadLocal
java·jvm·threadlocal
写后端的胖头鱼8 小时前
一文详解CAS
java·jvm·spring·cas·乐观锁·自旋