RabbitMQ解决消息丢失以及重复消费问题

文章目录

    • 1、概念
    • 2、基于ACK/NACK机制
      • 2.1 基于Spring AMQP框架整合ACK/NACK机制
      • 2.2 测试消费失败1.0
      • 2.3 测试结果1.0
      • 2.4 测试MQ宕机
      • 2.5 测试结果2.0
    • 3、RabbitMQ 如何实现幂等性设计
      • 3.1 幂等服务设计思路
        • 3.1.1 通过雪花算法生成分布式唯一ID
        • 3.1.2 通过枚举类,设计Message消费状态
        • 3.1.3 生产者
        • 3.1.4 消费者
        • 3.1.5 测试结果

1、概念

RabbitMQ作为一款消息中间件,其设计目标之一就是保证消息的可靠性。要实现RabbitMQ消息不丢失,可以从以下几个方面进行配置和优化:

  1. 生产者确认机制(Publisher Confirms): 生产者在发布消息时,可以开启publisher confirms机制。当消息投递到RabbitMQ Broker后,Broker会返回一个确认信息给生产者。如果Broker没有正确接收到消息或存储失败,则不会发送确认。这样生产者可以根据是否收到确认来决定是否需要重新发送消息。
  2. 持久化消息(Message Durability)
    • 对于队列(Queue),设置其为持久化的(durable)。即使RabbitMQ服务器重启,持久化的队列也会被恢复。
    • 对于消息(Message),在发布时设置delivery mode为2,这将使得消息在队列中持久化。持久化消息会在磁盘上存储备份,即使RabbitMQ服务重启也能保持消息不丢失。
  3. 消费者ACK确认机制: 消费者在消费消息后,需要发送ACK确认给RabbitMQ。如果消费者在处理完消息之前意外终止(如进程崩溃),RabbitMQ会认为该消息未被正确处理,从而重新将消息投入队列等待其他消费者消费。
  4. 集群部署: 通过集群部署的方式提高RabbitMQ服务的可用性和容灾能力,即使部分节点出现问题,其他节点依然能保证消息的正常收发。
  5. 预拉取策略调整: 避免因消费者的消费速度慢于生产者的发送速度而导致的消息积压无法持久化的问题,可以通过调整prefetch count限制消费者预拉取消息的数量。
  6. 监控与告警: 建立完善的监控系统,实时关注RabbitMQ的各项指标,包括队列深度、磁盘使用率等,及时发现可能造成消息丢失的风险点并采取措施。

以上这些方法综合应用,可以在很大程度上确保RabbitMQ消息的不丢失。但需要注意的是,完全避免消息丢失在分布式系统中往往难以做到,只能尽可能地降低这种可能性。

2、基于ACK/NACK机制

在Java中使用RabbitMQ的ACK/NACK机制时,通常会利用Channel对象来进行消息确认。

使用Spring AMQP框架,可以结合Acknowledgment注解或者容器级别的配置来更方便地管理ACK/NACK操作。

在这里插入图片描述

在这里插入图片描述

2.1 基于Spring AMQP框架整合ACK/NACK机制

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Service;

/**
 * RabbitMqConsumer :
 *
 * @author zyw
 * @create 2024-01-08  14:48
 */

@Slf4j
@Service
public class RabbitMqConsumer implements ChannelAwareMessageListener {

    @Override
    @RabbitListener(queues = "direct.queue", ackMode = "MANUAL")
    public void onMessage(Message message, Channel channel) throws Exception {
        try {
            // 处理消息逻辑
            processMessage(message);

            // 成功处理后手动确认消息
            long deliveryTag = message.getMessageProperties().getDeliveryTag();
            channel.basicAck(deliveryTag, false);

        } catch (Exception e) {
            // 处理失败,可以选择重新入队列(取决于业务需求)
            if (shouldRequeueOnFailure()) {
                long deliveryTag = message.getMessageProperties().getDeliveryTag();
                channel.basicNack(deliveryTag, false, true);
            } else {
                long deliveryTag = message.getMessageProperties().getDeliveryTag();
                channel.basicReject(deliveryTag, false);
            }
        }
    }

    private boolean shouldRequeueOnFailure() {
        // 根据业务需求决定是否重新入队列
        return true; // 或者 false
    }

     /**
     * 消费逻辑
     * @param message
     * @throws Exception
     */
    private void processMessage(Message message) throws Exception {
        System.out.println("Processing message: " + new String(message.getBody()));
        System.out.println("Processing : " + n);
    }
}

2.2 测试消费失败1.0

这里我基于RabbitMq的direct交换机模式,通过循环发送三条消息

    public void sendQueueBatch(String message) {
        for (int i = 0; i < 3; i++) {
            rabbitTemplate.convertAndSend("direct.exchange", "direct.key", message + "{}i:" + i);
        }
        log.info("3个消息都发送成功");
    }

消费的业务逻辑中,我模拟第三次消费的时候会报错

    //消费计数    
    private int n = 0;

    /**
     * 消费逻辑
     * @param message
     * @throws Exception
     */
    private void processMessage(Message message) throws Exception {
        n++;
        if (n==3){
            throw new Exception("模拟消费失败");
        }
        System.out.println("Processing message: " + new String(message.getBody()));
        System.out.println("Processing : " + n);
    }

2.3 测试结果1.0

在这里插入图片描述

如图我们可以看到第三次消费失败后,系统自动再次尝试执行了第四次消费

2.4 测试MQ宕机

这里我们模拟每个消息的执行耗时4秒钟,在这期间我们手动关闭RabbitMq服务,模拟MQ宕机/网络波动。之后再手动重启MQ服务,查看之前未完成消费的消息是否能重新执行成功。

   //计数    
   private int n = 0;

    /**
     * 消费逻辑
     * @param message
     * @throws Exception
     */
    private void processMessage(Message message) throws Exception {
        n++;
        //模拟MQ宕机
        Thread.sleep(4000);
        System.out.println("Processing message: " + new String(message.getBody()));
        System.out.println("Processing : " + n);
    }

2.5 测试结果2.0

这里我们可以看到消费第二个消息的过程中,MQ宕机了

在这里插入图片描述

MQ重启之后,第二个和第三个消息都被执行了。通过我们设置的变量计数n以及消息的标识i我们可以发现,第二个消息被重复执行了。

在这里插入图片描述

RabbitMq宕机时已经开始消费但还未消费结束的消息,重启MQ之后会重复执行

在RabbitMQ中,如果消费者在消费消息时宕机或者网络故障导致服务器没有接收到确认(acknowledgement),那么这条消息可能会被重新投递。具体来说:

  1. 当消费者从队列中接收一条消息后,默认情况下RabbitMQ会将消息标记为“不可见”(除非使用了manual acknowledgment模式)。
  2. 消费者在处理完消息并发送ack给RabbitMQ之前,若发生宕机或网络中断等情况,RabbitMQ无法得知该消息是否已经被正确处理。
  3. RabbitMQ会在一个称为prefetch count(预取数量)限制范围内持续尝试重新投递未被确认的消息。

因此,在RabbitMQ服务重启之后,那些之前已经开始消费但未被确认的消息会被认为是没有被正确处理,从而重新放回队列等待被其他消费者获取并处理,这就可能导致消息重复执行。为了避免这种情况造成的影响,通常需要在业务逻辑层面实现幂等性设计,即确保消息无论被消费多少次,其结果都是相同的,并且只产生一次有效操作。此外,可以使用事务、发布确认和高级消息确认机制来更好地控制消息的可靠性。

3、RabbitMQ 如何实现幂等性设计

在RabbitMQ中实现幂等性设计,确保消息无论被消费多少次都不会对业务状态造成重复影响,需要结合消息队列的机制以及业务逻辑的设计。以下是一些建议和方法:

  1. 业务层幂等处理

    • 每个消息携带一个全局唯一ID,在业务处理过程中,首先检查这个ID是否已经被处理过。例如,将已处理消息的ID记录到数据库的“已处理消息表”中,下次收到同样ID的消息时直接返回成功而不进行实际操作。
    • 对于更新型操作,可以使用乐观锁或分布式锁来保证同一事务多次执行结果相同,例如通过版本号(version)控制更新操作,只有当版本号未变时才执行更新。
    • 对于创建型操作,确保即使多次调用也不会生成多个资源,例如通过查询是否存在相同的唯一键来决定是否创建新的资源。
  2. 确认模式选择

    • 使用acknowledgement模式,消费者接收到消息后必须发送确认给RabbitMQ,只有收到确认后RabbitMQ才会从队列中移除消息,否则会在连接恢复后重新投递。
    • 设置publisher confirms,生产者可以得到消息发布的确认,确保消息确实到达了MQ服务器并持久化存储。
  3. 死信队列与重试策略

    • 配置死信交换机和死信队列,对于那些重复投递依然无法正确处理的消息,可以转移到死信队列,并设置相应的重试策略及最大重试次数,超过限制则记录日志、报警或手动介入处理。
  4. 幂等服务设计

    • 设计能够应对重复调用的服务接口,这些接口内部应该包含足够的逻辑判断以识别重复请求并作出正确的响应。
  5. 事务与补偿机制

    • 对于涉及多个系统的分布式事务场景,可以考虑采用TCC(Try-Confirm-Cancel)模式或其他分布式事务解决方案,使得整个流程具有幂等性。

总结来说,在RabbitMQ中实现幂等性主要依赖于业务逻辑层面的改造和优化,同时配合RabbitMQ自身的消息确认机制来确保消息不会因为异常情况而重复处理。

3.1 幂等服务设计思路

我们可以给每一个消息绑定一个分布式唯一ID,在通过Redis记录该消息的消费状态,保证每条消息只能被消费一次

在这里插入图片描述

3.1.1 通过雪花算法生成分布式唯一ID

我们可以将雪花算法的工具类抽出到微服务分布式系统的公共组件中,通过maven的依赖引用来使用。

在每个服务的配置文件中去配置专属的工作节点ID和数据中心ID,不同的服务去引用雪花算法工具类时,读取自身配置文件中的工作节点ID和数据中心ID。

zyw:
  # 工作节点ID(0~31)
  workerId: 0
  # 数据中心ID(0~31)
  datacenterId: 0

通过专属工作节点ID和数据中心ID构建专属的雪花算法工具类SnowflakeIdWorker

import org.springframework.beans.factory.annotation.Value;
import java.util.concurrent.atomic.AtomicLong;

/**
 * SnowflakeIdWorker : 雪花算法
 *
 * @author zyw
 * @create 2024-01-09  10:46
 */

public class SnowflakeIdWorker {

    // 起始的时间戳 (2010-01-01)
    private final long twepoch = 1288834974657L;

    // 机器标识位数
    private final long workerIdBits = 5L;
    private final long datacenterIdBits = 5L;

    // 序列号位数
    private final long sequenceBits = 12L;

    // 工作机器ID最大值
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 数据中心ID最大值
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    // 每一部分向左的偏移量
    private final long workerIdShift = sequenceBits;
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    // 时间戳边界值
    private long lastTimestamp = -1L;

    // 工作节点ID(0~31)
    @Value("${zyw.workerId}")
    private long workerId;

    // 数据中心ID(0~31)
    @Value("${zyw.datacenterId}")
    private long datacenterId;
    // 每个节点每毫秒内的序列号
    private AtomicLong sequence = new AtomicLong(0L);

    /**
     * 通过专属工作节点ID和数据中心ID构建专属的雪花算法工具类
     */
    public SnowflakeIdWorker() {
        if (this.workerId > maxWorkerId || this.workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (this.datacenterId > maxDatacenterId || this.datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
    }

    /**
     * 分布式唯一ID生成
     * @return
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        // 如果是同一时间生成的,则进行序列号的自增
        if (lastTimestamp == timestamp) {
            sequence.incrementAndGet();
            // 判断是否溢出
            if (sequence.get() > (-1L ^ (-1L << sequenceBits))) {
                // 阻塞到下一个时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 时间戳改变,重置序列号
            sequence.set(0L);
        }

        // 上次生成ID的时间截
        lastTimestamp = timestamp;

        // 移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) | sequence.get();
    }

    /**
     * 从给定的最后时间戳中获取下一个时间戳
     *
     * @param lastTimestamp 最后时间戳
     * @return 下一个时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 生成当前时间的毫秒数。
     *
     * @return 当前时间的毫秒数。
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }
}
3.1.2 通过枚举类,设计Message消费状态
import java.util.Arrays;
import java.util.List;

/**
 * RabbitStatusEnum :
 *
 * @author zyw
 * @create 2024-01-09  11:18
 */

public enum RabbitStatusEnum {

    CONSUME(0, "待消费"),
    BEGIN(1, "开始消费"),
    SUCCESS(2, "成功"),
    FAIL(3, "失败"),
    ;

    private Integer code;
    private String message;

    RabbitStatusEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    /**
     * 获取需要执行的状态集合
     * @return
     */
    public static List<Integer> getNeedExecuteList(){
        return Arrays.asList(CONSUME.getCode(),FAIL.getCode());
    }

    /**
     * 获取不需要执行的状态集合
     * @return
     */
    public static List<Integer> getCompletionExecuteList(){
        return Arrays.asList(CONSUME.getCode(),FAIL.getCode());
    }

}
3.1.3 生产者

生产者发送消息时,生成专属分布式唯一业务ID,通过Redis记录消息的消费状态

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.example.demo.config.mq.RabbitStatusEnum;
import com.example.demo.config.redis.RedisKeyEnum;
import com.example.demo.uitls.RedisUtils;
import com.example.demo.uitls.SnowflakeIdWorker;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

/**
 * MqService :
 *
 * @author zyw
 * @create 2023-12-19  16:26
 */

@Service
@Slf4j
public class MqService {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Resource
    private SnowflakeIdWorker snowflakeIdWorke;

    @Resource
    private RedisUtils redisUtils;

     /**
     * 批量发送消息
     *
     * @param message
     */
    public void sendQueueBatch(String message) {

        //请求头设置消息id(messageId)
        Map<String, Object> map = new HashMap<>();
        map.put("message", message);
        for (int i = 0; i < 3; i++) {
            long id = snowflakeIdWorker.nextId();
            map.put("id", id);
            JSONObject entries = JSONUtil.parseObj(map);
            redisUtils.setCacheObject(RedisKeyEnum.MQ_STATUS.getKey() + id, RabbitStatusEnum.CONSUME.getCode());
            rabbitTemplate.convertAndSend("direct.exchange", "direct.key", entries);
        }
        log.info("3个消息都发送成功");
    }

}
3.1.4 消费者

我定义了一个实现ChannelAwareMessageListener接口的消费者类,并在@RabbitListener注解中设置了ackMode="MANUAL",这意味着消息确认将由开发者手动完成。当接收到消息时,可以通过获取的Channel对象调用basicAck()basicNack()basicReject()方法来进行消息确认或者拒绝操作。

  • 消息开始消费时,记录开始消费的状态
  • 消息成功完成后,记录成功消费的状态

这里是为了避免在消息开始消费后,RabbitMq宕机了,此时MQ并不知道这个消息最终有没有消费完成,因此重启MQ之后,MQ会重新消费这条消息。

因此我们只运行执行“待消费”和“消费失败”状态的消息。

  • 如果在执行消费的过程中,出错了(抛出Exception),则记录消费失败的状态,MQ会再次尝试去进行消费
  • 我们可以设置最多重试次数,以及两次重试消费的间隔时间
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.example.demo.config.mq.RabbitStatusEnum;
import com.example.demo.config.redis.RedisKeyEnum;
import com.example.demo.uitls.RedisUtils;
import com.rabbitmq.client.Channel;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Service;

/**
 * RabbitMqConsumer : 消费者
 *
 * @author zyw
 * @create 2024-01-08  14:48
 */

@Slf4j
@Service
public class RabbitMqConsumer implements ChannelAwareMessageListener {

    @Resource
    private RedisUtils redisUtils;

    /**
     * 记录消费次数
     */
    private int n = 0;

    @Override
    @RabbitListener(queues = "direct.queue", ackMode = "MANUAL")
    public void onMessage(Message message, Channel channel) throws Exception {
        JSONObject entries = JSONUtil.parseObj(new String(message.getBody()));
        Integer status = redisUtils.getCacheObject(RedisKeyEnum.MQ_STATUS.getKey() + entries.get("id"));
        try {
            //只有代消费和消费失败的能进行消费
            if (RabbitStatusEnum.getNeedExecuteList().contains(status)) {
                //记录开始消费
                redisUtils.setCacheObject(RedisKeyEnum.MQ_STATUS.getKey() + entries.get("id"), RabbitStatusEnum.BEGIN.getCode());
                // 处理消息逻辑
                processMessage(entries);
                System.out.println("执行成功了:" + entries.get("id"));
                //记录消费成功
                redisUtils.setCacheObject(RedisKeyEnum.MQ_STATUS.getKey() + entries.get("id"), RabbitStatusEnum.SUCCESS.getCode());
                // 成功处理后手动确认消息
                long deliveryTag = message.getMessageProperties().getDeliveryTag();
                channel.basicAck(deliveryTag, false);
            }
        } catch (Exception e) {
            // 处理失败,可以选择重新入队列(取决于业务需求)
            if (shouldRequeueOnFailure()) {
                long deliveryTag = message.getMessageProperties().getDeliveryTag();
                channel.basicNack(deliveryTag, false, true);
                System.out.println("执行失败了:" + entries.get("id"));
                //记录消费失败
                redisUtils.setCacheObject(RedisKeyEnum.MQ_STATUS.getKey() + entries.get("id"), RabbitStatusEnum.FAIL.getCode());
            } else {
                long deliveryTag = message.getMessageProperties().getDeliveryTag();
                channel.basicReject(deliveryTag, false);
            }
        }
    }

    /**
     * 根据业务需求决定是否重新入队列
     * @return
     */
    private boolean shouldRequeueOnFailure() {
        return true;
    }

    /**
     * 消费逻辑
     *
     * @param entries
     * @throws Exception
     */
    private void processMessage(JSONObject entries) throws Exception {
        n++;
        //模拟MQ消费时长
        Thread.sleep(4000);
        //消费
        System.out.println("Processing id: " + RedisKeyEnum.MQ_STATUS.getKey() + entries.get("id"));
        System.out.println("Processing message: " + entries.get("message"));
        System.out.println("第" + n + "次消费");
    }
}
3.1.5 测试结果

这里我在第二条消息的执行消费过程中,手动关闭了RabbitMQ服务(模拟RabbitMQ宕机/网络波动),等待几秒后,重启RabbitMQ服务。

可以看到三条消息都被正常消费完成,解决了之前MQ重启后,重复消费的问题,解决了RabbitMQ消息不丢失的问题。

在这里插入图片描述

在这里插入图片描述
Redis中记录了每条消息消费的状态
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/316399.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

R语言【paleobioDB】——pbdb_intervals():通过参数选择,返回多个地层年代段的基本信息

Package paleobioDB version 0.7.0 paleobioDB 包在2020年已经停止更新&#xff0c;该包依赖PBDB v1 API。 可以选择在Index of /src/contrib/Archive/paleobioDB (r-project.org)下载安装包后&#xff0c;执行本地安装。 Usage pbdb_interval (id, ...) Arguments 参数【..…

数据结构之int类

int类 int 是数字类。在其他语言中&#xff0c;数字类有很明细的区分&#xff0c;如 int&#xff08;整型&#xff09;、unsigned int(无符号整型&#xff09;、short&#xff08;短整型&#xff09;、long&#xff08;长整型&#xff09;、longlong&#xff08;长长整型&…

D25XB80-ASEMI开关电源桥堆D25XB80

编辑&#xff1a;ll D25XB80-ASEMI开关电源桥堆D25XB80 型号&#xff1a;D25XB80 品牌&#xff1a;ASEMI 封装&#xff1a;GBJ-5&#xff08;带康铜丝&#xff09; 特性&#xff1a;插件、整流桥 平均正向整流电流&#xff08;Id&#xff09;&#xff1a;25A 最大反向击…

轻松掌握构建工具:Webpack、Gulp、Grunt 和 Rollup 的使用技巧(上)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

2024最新外卖CPS分销微信小程序源码【前端+后台+数据库+分销功能】

内容目录 一、详细介绍二、效果展示三、源代码下载地址 一、详细介绍 外卖侠CPS全套源码是一款为外卖平台提供分销功能的微信小程序。用户可以通过你的链接去领取外卖红包&#xff0c;然后去下单点外卖&#xff0c;既能省钱&#xff0c;又能获得佣金。该小程序带有商城、影票、…

如何从 Android SD卡/存储卡中恢复删除的照片

虽然大多数摄影师和智能手机用户都非常喜欢在一张 存储卡上存储数千张照片的能力&#xff0c;但它也可能导致灾难性的数据丢失&#xff0c;而 存储卡照片恢复软件通常是唯一的解决方案。 但是&#xff0c;如果您不迅速采取行动并在图像被覆盖之前恢复图像&#xff0c;那么即使…

python 语法

闭包 在函数嵌套的前提下&#xff0c;内部函数使用了外部函数的变量&#xff0c;并且外部函数返回了内部函数&#xff0c;我们把这个使用外部函数变量的内部函数称为闭包。 def outfunc(arg):def innerFunc(msg):print(f"<{msg}> {arg} <{msg}>")retu…

部署 LVS-DR 群集

本章内容&#xff1a; 了解 LVS-DR 群集的工作原理会构建LVS-DR 负载均衡群集 1.1 LVS-DR 群集 LVS-DR&#xff08; Linux Virtual Server Director Server &#xff09;工作模式&#xff0c;是生产环境中最常用的一种工作模式。 1.1.1 LVS-DR工作原理 LVS-DR 模式&#xff…

MySQL高可用解决方案演进:从主从复制到InnoDB Cluster架构

目录 前言 1. 主从复制 主从复制的基本配置示例&#xff1a; 2. 主从复制的限制 3. InnoDB Cluster架构 InnoDB Cluster配置步骤示例&#xff1a; 4. InnoDB Cluster的优势 总结 ⭐️ 好书推荐 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&…

力扣|2023华为秋招冲刺

文章目录 第一关&#xff1a;2023 年 7 月面试题挑战第二关&#xff1a;2023 年 6 月面试题挑战第三关&#xff1a;2023 年 5 月面试题挑战 第一关&#xff1a;2023 年 7 月面试题挑战 class Solution { public:void reverseWord(vector<char>& s,int l,int r){for(i…

【算法分析与设计】最短路径和

题目&#xff1a; 给定一个包含非负整数的 m x n 网格 grid &#xff0c;请找出一条从左上角到右下角的路径&#xff0c;使得路径上的数字总和为最小。 说明&#xff1a;每次只能向下或者向右移动一步。 示例&#xff1a; 示例 1&#xff1a; 输入&#xff1a;grid [[1,3,1],…

极兔单号查快递,极兔快递单号查询,筛选出途经指定城市的单号

随着电商的繁荣&#xff0c;快递单号已经成为我们生活中的一部分。然而&#xff0c;面对海量的快递信息&#xff0c;如何快速、准确地筛选出我们需要的单号&#xff0c;变成了许多人的痛点。今天&#xff0c;我要为你介绍一款强大的工具——快递批量查询高手&#xff0c;让你的…

44 ext4 文件系统

前言 在 linux 中常见的文件系统 有很多, 如下 基于磁盘的文件系统, ext2, ext3, ext4, xfs, btrfs, jfs, ntfs 内存文件系统, procfs, sysfs, tmpfs, squashfs, debugfs 闪存文件系统, ubifs, jffs2, yaffs 文件系统这一套体系在 linux 有一层 vfs 抽象, 用户程序不用…

代码随想录算法训练营第24天 | 理论基础 77. 组合

目录 理论基础 什么是回溯法 回溯法的效率 回溯法解决的问题 如何理解回溯法 回溯法模板 77. 组合 &#x1f4a1;解题思路 &#x1f4bb;实现代码 理论基础 什么是回溯法 回溯法也可以叫做回溯搜索法&#xff0c;它是一种搜索的方式。 回溯法的效率 虽然回溯法很难&#xff…

前端安全专题

xss (Cross Site Scripting) 跨站脚本攻击 原理 通常指黑客通过"HTML注入"篡改了网页&#xff0c;插入了恶意的脚本&#xff0c;从而在用户浏览网页时&#xff0c;控制用户浏览器的一种攻击。 常见攻击类型 存储型XSS 攻击者将恶意的 JavaScript 脚本存储在网站…

C程序训练:阶乘与溢出

已知n是整数&#xff0c;计算12!3!...n!&#xff0c;并给出最大能够计算的n值是多少&#xff1f; 1. 假设n是int类型&#xff0c;系统用32位表示int类型。代码如下&#xff1a; #include <stdio.h> int main() {int n,sum1,sum1,fact1;int step;for(n2; n<100; n) {…

【Win11】电脑正常联网浏览器却打不开???

今天本来打算打开B站开始今天的学习之旅&#xff0c;一打开却发现。。。 我还以为电脑没联网但是微信可以聊天发消息然后我在dos窗口测了下网络是正常联通的 然后我开始慌了&#xff0c;这阳光明媚的一天不看B站学习怎么行&#xff0c;然后我就开始在百度上冲浪找解决方案&…

探索设计模式的魅力:简单工厂模式

简单工厂模式&#xff08;Simple Factory Pattern&#xff09;是一种创建型设计模式&#xff0c;其主要目的是用于创建对象的实例。这种模式通过封装创建对象的代码来降低客户代码与具体类之间的耦合度。简单工厂不是GoF&#xff08;四人帮&#xff09;设计模式之一&#xff0c…

WAMP apache 无法启动(端口 80 未使用)

这段时间系统重装后&#xff0c;安装WAMP Server&#xff0c;装好后点击启动绿了下然后又变成了黄色&#xff0c;托盘图标无论是左键点击还是右键点击都没有反应&#xff0c;wampapache64服务也启动不起来&#xff0c;提示“windows不能在本地计算机启动wampapache”&#xff0…

软件系统部署方案书(Word)

一、 引言 &#xff08;一&#xff09; 编写目的 二、 外部设计 &#xff08;一&#xff09; 标识符和状态 &#xff08;二&#xff09; 约定 1&#xff0e; 数据库涉及字符规范 2&#xff0e; 字段命名规范 &#xff08;三&#xff09; 专门指导 &#xff08;四&#…