消息队列(六):服务器设计

news2024/11/20 3:20:02

紧接着上一章没说完的进行服务器的补充。

推送给消费者消息的基本实现思路

  1. 让 brokerServer 把哪些消费者管理好
  2. 收到对应的消息,把消息推送给消费者

消费者是以队列为维度来订阅消息的,一个队列可以有多个消费者(此处我们约定按照轮询的方式来进行消费)

消费者消费消息的核心逻辑

这里又一次提到了消费者,我们来把消费者相关的代码完善一下。

消费者管理实现

消费者

先前我们提到了这个函数式接口,这个接口的作用就是用来消费消息(和消费者作用一样)。

那么我们就给这个函数式接口起名消费者 consumer 

上一章也提到了具体的代码,这里再演示一次:

@FunctionalInterface
public interface Consumer {
    // Deliver 的意思就是"投递",这个方法预期是在每次服务器收到消息,来调用
    // 通过这个方法把消息推送给对应的消费者
    void handleDeliver(String consumerTag, BasicProperties basicProperties, byte[] body) throws MqException, IOException;

}

具体是想干啥,谁调用由谁决定(具体代码)。

消费者完整的执行环境

消费者在消费的时候,需要知道是哪个消费者进行消费了,是哪个队列传过来的消息,知否自动应答。

故此,我们大概有如下几个参数:

   private String consumerTag;
   private String queueName;
   private boolean autoAck;
   // 通过这个回调来处理收到的消息
   private Consumer consumer;

以及对应的 getting、setting  方法 和 构造方法 。

消费者管理类

这个类我放在了核心类。

啥时候执行这个消费者完整的执行环境啊,通过这个类,来实现消费消息的核心逻辑。

订阅消息的核心 就是这个    consumer.addConsumer() 。

根据这个图,我们也能看出来我们大致需要如下几个属性:

需要一个队列,一个扫描线程,此外还需要记录一下是哪个虚拟主机持有的消费者;此外,还需要一个执回调(函数式接口)的线程池。

    // 持有上层的 VirtualHost 对象的引用. 用来操作数据.
    private VirtualHost parent;
    // 指定一个线程池, 负责去执行具体的回调任务.
    private ExecutorService workerPool = Executors.newFixedThreadPool(4);
    // 存放令牌的队列
    private BlockingQueue<String> tokenQueue = new LinkedBlockingQueue<>();
    // 扫描线程
    private Thread scannerThread = null;

方法:

  • 构造方法
  • 添加消费者
  • 消费消息
  • 唤醒消费

先来说说构造方法:

构造器只持有虚拟机的名字;因为扫描线程是不断地进行扫描,啥时候启动这个扫描线程呢?这里就设置在了构造方法中。

扫描线程的实现逻辑就如上图所言,一旦调用虚拟主机中的发送消息就会唤醒消费(具体的唤醒就是往存放令牌的队列中添加队列名),扫描线程扫描的就是这个队列,一旦队列有消息进来就调用消费消息这个方法。

public ConsumerManager(VirtualHost parent) {
        this.parent = parent;

        scannerThread = new Thread(() -> {
            while (true) {
                try {
                    // 1. 拿到令牌
                    String queueName = tokenQueue.take();
                    // 2. 根据令牌找到队列
                    MSGQueue queue = parent.getMemoryDataCenter().getQueue(queueName);
                    if (queue == null) {
                        throw new MqException("[ConsumerManager] 取令牌后发现,该队列名不存在!queueName="+queueName);
                    }
                    // 3. 从这个队列中消费一个信息
                    synchronized (queue) {
                        consumeMessage(queue);
                    }
                }catch (InterruptedException | MqException e) {
                    e.printStackTrace();;
                }
            }
        });
        // 将线程设为后台线程
        scannerThread.setDaemon(true);
        scannerThread.start();
    }

唤醒消费

该方法只有一行代码,就是将队列名放入令牌队列。

    // 这个方法的调用时机就是发送消息的时候.
    public void notifyConsume(String queueName) throws InterruptedException {
        tokenQueue.put(queueName);
    }

添加消费者

在这个方法中,之前记录的虚拟机主机名就派上用场了。

我们需要从虚拟主机中获取到队列(调用者需要传递下来队列名称),类型为 核心类: MSGQueue(此时可以补充两个方法):

// 添加一个新的订阅者
    public void addConsumerEnv(ConsumerEnv consumerEnv) {
        consumerEnvList.add(consumerEnv);
    }
    // 订阅者删除暂时先不考虑
    // 先挑选一个订阅者,用来处理当前的消息(轮询的方式)
    public ConsumerEnv chooseConsumer() {
        if (consumerEnvList.size() == 0) {
            // 该队列没有人订阅
            return null;
        }
        // 计算一下当前要取的元素下标
        int index = consumerSeq.get() % consumerEnvList.size();
        consumerSeq.getAndIncrement();
        return consumerEnvList.get(index);
    }

我们采取一个轮询的方式去处理消息,每个消费者都有机会进行消费消息。

具体的消费就是通过下标取模的方式,这里涉及到了多线程同时调用,所以使用了原子类 AtomicInteger 修饰 consumerSeq 。

没有就需要抛出异常,有的话需要创建出完整的消费者环境(将参数都传进去),随后将其添加到队列中去。

在进行循环,如果队列有消息就先进行消费完。

具体代码如下:

public void addConsumer(String consumerTag, String queueName, boolean autoAck, Consumer consumer) throws MqException {
        MSGQueue queue = parent.getMemoryDataCenter().getQueue(queueName);
        if (queue == null) {
            throw new MqException("[ConsumerManager] 队列不存在! queueName=" + queueName);
        }
        ConsumerEnv consumerEnv = new ConsumerEnv(consumerTag,queueName,autoAck,consumer);
        synchronized (queue) {
            queue.addConsumerEnv(consumerEnv);
            // 如果当前这个队列已经有消息了,就需要立即消费掉
            int n = parent.getMemoryDataCenter().getMessageCount(queueName);
            for (int i = 0; i < n; i++) {
                // 这个放啊调用一次就消费一条消息
                consumeMessage(queue);
            }
        }
    }

消费消息

大致逻辑

  1. 按照轮询的方法,找个消费者出来(没有消费者就暂时不消费,等有消费者出现才进行消费)
  2. 从队列中取出一个消息(当前队列中还没有消息,也不需要消费)
  3. 把消息带入到消费者的回调方法中, 丢给线程池执行
    1. 把消息放到待确认的集合中. 这个操作势必在执行回调之前
    2. 真正执行回调操作
    3. 如果当前是 "自动应答" , 就可以直接把消息删除了. 如果当前是 "手动应答" , 则先不处理, 交给后续消费者调用 basicAck 方法来处理.
      1. 删除硬盘上的消息
      2. 删除上面的待确认集合中的消息
      3. 删除内存中消息中心里的消息
具体代码如下:
private void consumeMessage(MSGQueue queue) {
        // 1. 按照轮询的方法,找个消费者出来
        ConsumerEnv luckyDog = queue.chooseConsumer();
        if (luckyDog == null) {
            // 当前队列没有消费者, 暂时不消费. 等后面有消费者出现再说.
            return;
        }
        // 2. 从队列中取出一个消息
        Message message = parent.getMemoryDataCenter().pollMessage(queue.getName());
        if (message == null) {
            // 当前队列中还没有消息, 也不需要消费.
            return;
        }
        // 3. 把消息带入到消费者的回调方法中, 丢给线程池执行.
        workerPool.submit(() -> {
            try {
                // 1. 把消息放到待确认的集合中. 这个操作势必在执行回调之前.
                parent.getMemoryDataCenter().addMessageWaitAck(queue.getName(), message);
                // 2. 真正执行回调操作
                luckyDog.getConsumer().handleDeliver(luckyDog.getConsumerTag(), message.getBasicProperties(),
                        message.getBody());
                // 3. 如果当前是 "自动应答" , 就可以直接把消息删除了.
                //    如果当前是 "手动应答" , 则先不处理, 交给后续消费者调用 basicAck 方法来处理.
                if (luckyDog.isAutoAck()) {
                    // 1) 删除硬盘上的消息
                    if (message.getDeliverMode() == 2) {
                        parent.getDiskDataCenter().deleteMessage(queue, message);
                    }
                    // 2) 删除上面的待确认集合中的消息
                    parent.getMemoryDataCenter().removeMessageWaitAck(queue.getName(), message.getMessageId());
                    // 3) 删除内存中消息中心里的消息
                    parent.getMemoryDataCenter().removeMessage(message.getMessageId());
                    System.out.println("[ConsumerManager] 消息被成功消费! queueName=" + queue.getName());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

    }
}

关于确认消息

能够确保消息是被正确的消费掉了,消费者的回调函数,顺利执⾏完了(中间没有抛出异常)
这条消息就可以被删除了。
消息确认也就是为了保证“消息不丢失”
为了达成消息不丢失这样的效果,这样处理:
  • 在真正执⾏回调之前,把这个消息先放到 “待确认的集合”中
  • 真正回调
  • 当前消费者采取的是 autoAck=true,就认为回调执⾏完毕不抛异常,就算消费成功,然后就可以删除消息
  • 当前消费者采取的是 autoAck=false,⼿动应答,就需要消费者再回调函数内部,显式调⽤
    basicAck这个核⼼API
basicAck实现原理,⽐较简单,当传⼊参数 autoAck=false, 就⼿动再回调函数的时候,调⽤
basicAck 就⾏(具体的在 VirtualHost中)

消息确认是为了保证消息不丢失,而需要的逻辑。

  • 1. 执⾏回调⽅法的过程中,抛异常了
    • 当回调函数异常,后续逻辑执⾏不到了。此时这个消费就会始终待在待确认集合中。
      RabbitMQ中会设置⼀个死信队列,每⼀个队列都会绑定⼀个死信队列。应⽤场景:当消息在 消费过程中出现异常,就会把消息投⼊到死信队列中;当消息设置了过期时间,如果在过期时 间内,没有被消费,就会投⼊到死信队列中;当队列达到最⼤⻓度时,新的消息将⽆法被发送 到队列中。此时,RabbitMQ可以选择将这些⽆法发送的消息发送到死信队列中,以便进⾏进 ⼀步处理
  • 2. 执⾏回调过程中, Broker Server崩溃了,内存数据都没了!但是硬盘数据还在,正在消费的这个 消息,在硬盘中仍然存在。BrokerServer重启后,这个消息就⼜被加载到内存了,就像从来没被消 费过⼀样。消费者就会有机会重新得到这个消息。

BrokerServer

Broker Server 本质是一个服务器,我在这个自定义服务器上添加了自定义应用层协议。

自定义协议

具体的协议设置:

请求和响应

  •  type : 用于描述当前这个请求和响应是要干啥的
    • 在MQ中,客户端(⽣产者 + 消费者)和 服务器 (Broker Server)之间,要进⾏哪些操作?(就是VirtualHost中的那些核⼼API)
    • 希望客户端,能通过⽹络远程调⽤这些API
    • 此处的type就是描述当前这个请求/响应是在调⽤哪个API
    • TCP是有连接的,Channel 是 Connection 内部的逻辑连接。此时⼀个 Connection 中可能有多 个连接,为啥要这么设计?就是为了让 TCP 连接得到复用(不断地创建和删除 TCP 连接,成本还是比较高的)
  • length:⾥⾯存储的是 payload的⻓度。⽤4个字节来存储
  • payload:会根据当前是请求还是响应,以及当前的 type 有不同的值
    • ⽐如 type 是 0x3(创建交换机),同时当前是个请求,此时 payload 的内容,就相当于是
      exchangeDelcare 的参数的序列化结果
    • ⽐如 type 是 0x3(创建交换机),同时当前是个响应,此时 payload 的内容,就相当于是
      exchangeDelcare 的返回结果的序列化内容

每一个请求对应的 响应不同(重点是 payload 不同),所以对应每一个请求都单独设计一个类,帮助构造响应。

ExchangeDelcare

request

response

通信流程:

由于不同的 payload 我们需要对其进行设计:

根据上述图示,我们需要如下几个参数:

由于每次响应都会带有 rid 和 channelId,所以将其设为父类:

其他的类也一样,继承这个类,并实现串行化,我就不一一举例了,我把大致的图放下来:

ExchangeDelete

QueueDelcare

QueueDelete

QueueBind

QueueUnBind

BasicPublish

BasicConsumer

BasicAck

创建 BrokerServer类

消息队列本体服务器

实现读取请求和写回响应

读取请求

private Request readRequest(DataInputStream dataInputStream) throws IOException {
        Request request = new Request();
        request.setType(dataInputStream.readInt());
        request.setLength(dataInputStream.readInt());
        byte[] payload = new byte[request.getLength()];
        int n = dataInputStream.read(payload);
        if (n != request.getLength()) {
            throw new IOException("读取请求格式出错!");
        }
        request.setPayload(payload);
        return request;
    }

写回响应

private void writeResponse(DataOutputStream dataOutputStream, Response response) throws IOException {
        dataOutputStream.writeInt(response.getType());
        dataOutputStream.writeInt(response.getLength());
        dataOutputStream.write(response.getPayload());
        // 这个刷新缓冲区也是重要的操作!!
        dataOutputStream.flush();
    }

清理过期会话

private void clearClosedSession(Socket clientSocket) {
        // 这里要做的事情, 主要就是遍历上述 sessions hash 表, 把该被关闭的 socket 对应的键值对, 统统删掉.
        List<String> toDeleteChannelId = new ArrayList<>();
        for (Map.Entry<String, Socket> entry : sessions.entrySet()) {
            if (entry.getValue() == clientSocket) {
                // 不能在这里直接删除!!!
                // 这属于使用集合类的一个大忌!!! 一边遍历, 一边删除!!!
                // sessions.remove(entry.getKey());
                toDeleteChannelId.add(entry.getKey());
            }
        }
        for (String channelId : toDeleteChannelId) {
            sessions.remove(channelId);
        }
        System.out.println("[BrokerServer] 清理 session 完成! 被清理的 channelId=" + toDeleteChannelId);
    }

解析请求(Process)

这一步属于重中之重。

大致流程如下:

  1. 把 request 中的 payload 做一个初步解析
  2. 根据 type 的值,决定具体要干啥
  3. 构造响应
private Response process(Request request, Socket clientSocket) throws IOException, ClassNotFoundException, MqException {
        // 1. 把 request 中的 payload 做一个初步的解析.
        BasicArguments basicArguments = (BasicArguments) BinaryTool.fromBytes(request.getPayload());
        System.out.println("[Request] rid=" + basicArguments.getRid() + ", channelId=" + basicArguments.getChannelId()
                + ", type=" + request.getType() + ", length=" + request.getLength());
        // 2. 根据 type 的值, 来进一步区分接下来这次请求要干啥.
        boolean ok = true;
        if (request.getType() == 0x1) {
            // 创建 channel
            sessions.put(basicArguments.getChannelId(), clientSocket);
            System.out.println("[BrokerServer] 创建 channel 完成! channelId=" + basicArguments.getChannelId());
        } else if (request.getType() == 0x2) {
            // 销毁 channel
            sessions.remove(basicArguments.getChannelId());
            System.out.println("[BrokerServer] 销毁 channel 完成! channelId=" + basicArguments.getChannelId());
        } else if (request.getType() == 0x3) {
            // 创建交换机. 此时 payload 就是 ExchangeDeclareArguments 对象了.
            ExchangeDeclareArguments arguments = (ExchangeDeclareArguments) basicArguments;
            ok = virtualHost.exchangeDeclare(arguments.getExchangeName(), arguments.getExchangeType(),
                    arguments.isDurable(), arguments.isAutoDelete(), arguments.getArguments());
        } else if (request.getType() == 0x4) {
            ExchangeDeleteArguments arguments = (ExchangeDeleteArguments) basicArguments;
            ok = virtualHost.exchangeDelete(arguments.getExchangeName());
        } else if (request.getType() == 0x5) {
            QueueDeclareArguments arguments = (QueueDeclareArguments) basicArguments;
            ok = virtualHost.queueDeclare(arguments.getQueueName(), arguments.isDurable(),
                    arguments.isExclusive(), arguments.isAutoDelete(), arguments.getArguments());
        } else if (request.getType() == 0x6) {
            QueueDeleteArguments arguments = (QueueDeleteArguments) basicArguments;
            ok = virtualHost.queueDelete((arguments.getQueueName()));
        } else if (request.getType() == 0x7) {
            QueueBindArguments arguments = (QueueBindArguments) basicArguments;
            ok = virtualHost.queueBind(arguments.getQueueName(), arguments.getExchangeName(), arguments.getBindingKey());
        } else if (request.getType() == 0x8) {
            QueueUnbindArguments arguments = (QueueUnbindArguments) basicArguments;
            ok = virtualHost.queueUnbind(arguments.getQueueName(), arguments.getExchangeName());
        } else if (request.getType() == 0x9) {
            BasicPublishArguments arguments = (BasicPublishArguments) basicArguments;
            ok = virtualHost.basicPublish(arguments.getExchangeName(), arguments.getRoutingKey(),
                    arguments.getBasicProperties(), arguments.getBody());
        } else if (request.getType() == 0xa) {
            BasicConsumeArguments arguments = (BasicConsumeArguments) basicArguments;
            ok = virtualHost.basicConsume(arguments.getConsumerTag(), arguments.getQueueName(), arguments.isAutoAck(),
                    new Consumer() {
                        @Override
                        public void handleDeliver(String consumerTag, BasicProperties basicProperties, byte[] body) throws MqException, IOException {
                            // 先知道当前这个收到的消息, 要发给哪个客户端.
                            // 此处 consumerTag 其实是 channelId. 根据 channelId 去 sessions 中查询, 就可以得到对应的
                            // socket 对象了, 从而可以往里面发送数据了
                            // 1. 根据 channelId 找到 socket 对象
                            Socket clientSocket = sessions.get(consumerTag);
                            if (clientSocket == null || clientSocket.isClosed()) {
                                throw new MqException("[BrokerServer] 订阅消息的客户端已经关闭!");
                            }
                            // 2. 构造响应数据
                            SubScribeReturns subScribeReturns = new SubScribeReturns();
                            subScribeReturns.setChannelId(consumerTag);
                            subScribeReturns.setRid(""); // 由于这里只有响应, 没有请求, 不需要去对应. rid 暂时不需要.
                            subScribeReturns.setOk(true);
                            subScribeReturns.setConsumerTag(consumerTag);
                            subScribeReturns.setBasicProperties(basicProperties);
                            subScribeReturns.setBody(body);
                            byte[] payload = BinaryTool.toBytes(subScribeReturns);
                            Response response = new Response();
                            // 0xc 表示服务器给消费者客户端推送的消息数据.
                            response.setType(0xc);
                            // response 的 payload 就是一个 SubScribeReturns
                            response.setLength(payload.length);
                            response.setPayload(payload);
                            // 3. 把数据写回给客户端.
                            //    注意! 此处的 dataOutputStream 这个对象不能 close !!!
                            //    如果 把 dataOutputStream 关闭, 就会直接把 clientSocket 里的 outputStream 也关了.
                            //    此时就无法继续往 socket 中写入后续数据了.
                            DataOutputStream dataOutputStream = new DataOutputStream(clientSocket.getOutputStream());
                            writeResponse(dataOutputStream, response);
                        }
                    });
        } else if (request.getType() == 0xb) {
            // 调用 basicAck 确认消息.
            BasicAckArguments arguments = (BasicAckArguments) basicArguments;
            ok = virtualHost.basicAck(arguments.getQueueName(), arguments.getMessageId());
        } else {
            // 当前的 type 是非法的.
            throw new MqException("[BrokerServer] 未知的 type! type=" + request.getType());
        }
        // 3. 构造响应
        BasicReturns basicReturns = new BasicReturns();
        basicReturns.setChannelId(basicArguments.getChannelId());
        basicReturns.setRid(basicArguments.getRid());
        basicReturns.setOk(ok);
        byte[] payload = BinaryTool.toBytes(basicReturns);
        Response response = new Response();
        response.setType(request.getType());
        response.setLength(payload.length);
        response.setPayload(payload);
        System.out.println("[Response] rid=" + basicReturns.getRid() + ", channelId=" + basicReturns.getChannelId()
                + ", type=" + response.getType() + ", length=" + response.getLength());
        return response;
    }

这一段逻辑看起来吓人,其实就是在处理请求中传递而来的 type ,根据不同 type 的类型来调用不同的方法

当然还有启动和关闭,这个就不用一步步分析了,大概来看看代码把:

这里还有关于连接没有讲到,等下一章继续完善最后的连接,

自定义协议响应代码

BrokerServer

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HTTP协议(超级详细)

HTTP协议介绍 基本介绍&#xff1a; HTTP&#xff1a;超文本传输协议&#xff0c;是从万维网服务器传输超文本到本地浏览器的传送协议HTTP是一种应用层协议&#xff0c;是基于TCP/IP通信协议来传送数据的&#xff0c;其中 HTTP1.0、HTTP1.1、HTTP2.0 均为 TCP 实现&#xff0…

激光焊接汽车PP塑料配件透光率测试仪

随着汽车主机厂对车辆轻量化的需求越来越强烈&#xff0c;汽车零部件轻量化设计、制造也成为汽车零部件生产厂商的重要技术指标。零部件企业要实现产品的轻量化&#xff0c;在材料指定的情况下&#xff0c;要通过产品设计优化、产品壁厚减小和装配方式的优化来解决。使用PP材料…

React 把useState变成响应式 ,今天又可以早点下班了

Ⅰ、前言 我们知道 React 中 , 要想修改 「状态」 > 必须要「state &#xff0c; setState」 useState() 中「setState」 去修改 > 「state」那么如果用 Proxy > 去改造 useState&#xff0c;那么 「摸鱼的时间」又增加啦 &#xff1f; Ⅱ、proxy 改造 useState 首…

数据结构与算法之Floyd算法-最短路径问题

Floyd算法-最短路径问题 Floyd算法-最短路径问题算法结束算法思想算法效率分析 Floyd算法-最短路径问题 算法结束 Floyd算法&#xff1a;求出每一对顶点之间的最短路径 核心&#xff1a;使用动态规划思想&#xff0c;将问题的求解分为多个阶段&#xff1a; 对于n个顶点的图…

数据结构---绪论

&#x1f31e;欢迎来到数据结构的世界 &#x1f308;博客主页&#xff1a;卿云阁 &#x1f48c;欢迎关注&#x1f389;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f31f;本文由卿云阁原创&#xff01; &#x1f4c6;首发时间&#xff1a;&#x1f339;2023年9月17日&…

HTTP代理反爬虫技术详解

HTTP代理是一种网络技术&#xff0c;它可以将客户端的请求转发到目标服务器&#xff0c;并将服务器的响应返回给客户端。在网络安全领域中&#xff0c;HTTP代理经常被用来反爬虫&#xff0c;以保护网站的正常运营。 HTTP代理反爬虫的原理是通过限制访问者的IP地址、访问频率、U…

typeScript 类型推论

什么是类型推论&#xff1f; 类型推论是 TypeScript 中的一个特性&#xff0c;它允许开发人员不必显式地指定变量的类型。相反&#xff0c;开发人员可以根据变量的使用情况让 TypeScript 编译器自动推断出类型。例如&#xff0c;如果开发人员将一个字符串赋值给一个变量&#…

【自然语言处理】【大模型】RWKV:基于RNN的LLM

相关博客 【自然语言处理】【大模型】RWKV&#xff1a;基于RNN的LLM 【自然语言处理】【大模型】CodeGen&#xff1a;一个用于多轮程序合成的代码大语言模型 【自然语言处理】【大模型】CodeGeeX&#xff1a;用于代码生成的多语言预训练模型 【自然语言处理】【大模型】LaMDA&a…

MySQL数据库详解 三:索引、事务和存储引擎

文章目录 1. 索引1.1 索引的概念1.2 索引的作用1.3 如何实现索引1.4 索引的缺点1.5 建立索引的原则依据1.6 索引的分类和创建1.6.1 普通索引1.6.2 唯一索引1.6.3 主键索引1.6.4 组合索引1.6.5 全文索引 1.7 查看索引1.8 删除索引 2. 事务2.1 事务的概念2.2 事务的ACID特性2.2.1…

Java 高频疑难问题系列一

​​​​​​​ 目录 ​编辑​​​​​​​ 1.零长度 2.redis的有序集的排序 3.Unsafe类 4.带资源的try语句 5.Spring如何实现计划任务 6.Java中普通代码块,构造代码块,静态代码块执行顺序 7.MyBatis缓存机制 8.Redis Java 2种类型操作转换 9.CAS底层原理和问题 1…

【数据分享】2006-2021年我国城市级别的市容环境卫生相关指标(20多项指标)

《中国城市建设统计年鉴》中细致地统计了我国城市市政公用设施建设与发展情况&#xff0c;在之前的文章中&#xff0c;我们分享过基于2006-2021年《中国城市建设统计年鉴》整理的2006—2021年我国城市级别的市政设施水平相关指标、2006-2021年我国城市级别的各类建设用地面积数…

【pytorch】模型常用函数(conv2d、linear、loss、maxpooling等)

1、二维卷积函数——cnv2d(): in_channels (int): 输入通道数 out_channels (int): 输出通道数 kernel_size (int or tuple): 卷积核大小 stride (int or tuple, optional): 步长 Default: 1 padding (int, tuple or str, optional): 填充 Default: 0 padding_mode (str, optio…

计算机是如何工作的下篇

操作系统&#xff08;Operating System ) 操作系统是一组做计算机资源管理的软件的统称。目前常见的操作系统有&#xff1a;Windows系列、Unix系列、Linux系列、OSX系列、Android系列、iOS系列、鸿蒙等. 操作系统由两个基本功能&#xff1a; 对下,要管理硬件设备. 对上,要给…

数据标注赋能机器学习进行内容审核

数据标注一直以来都是人工智能的基础&#xff0c;是机器学习得以训练的不可或缺的步骤。随着互联网的兴起&#xff0c;如何创建和维护一个健康的网络环境将成为互联网平台不断解决的问题&#xff0c;但对于与日俱增的用户增长和铺天盖地的网络信息&#xff0c;人工审核内容变得…

【牛客网】BC146 添加逗号

一.题目描述 牛客网题目链接:添加逗号_牛客题霸_牛客网 描述: 对于一个较大的整数 N(1<N<2,000,000,000) 比如 980364535&#xff0c;我们常常需要一位一位数这个数字是几位数&#xff0c;但是如果在这 个数字每三位加一个逗号&#xff0c;它会变得更加易于朗读。 因此&a…

指针扩展之——函数指针

前言&#xff1a;小伙伴们好久不见&#xff0c;本篇文章我们继续讲解一个指针的扩展——函数指针。 一.何为函数指针 我们通过对指针的学习已经知道&#xff0c;凡是叫什么什么指针的&#xff0c;都是指指向这个东西的指针。 所以所谓函数指针&#xff0c;也就是指向函数的指…

001 linux 导学

前言 本文建立在您已经安装好linux环境后&#xff0c;本文会向您介绍Shell的一些常用指令 什么是linux Linux是一种自由和开放源代码的类UNIX操作系统&#xff0c;该操作系统的内核由林纳斯托瓦兹在1991年首次发 布&#xff0c;之后&#xff0c;在加上用户空间的应用程序之后…

TypeScript 从入门到进阶之基础篇(一) ts类型篇

系列文章目录 文章目录 系列文章目录前言一、安装必要软件二、TypeScript 基础类型1.基础类型之 数字类型 number2.基础类型之 字符串类型 string3.基础类型之 布尔类型 boolean4.基础类型之 空值类型 void5.基础类型之 null 、undefined类型6.基础类型之 任意类型 any &#x…

Dell戴尔笔记本电脑灵越系列Inspiron 5598原厂Windows10系统2004

戴尔灵越原装出厂系统自带显卡、声卡、蓝牙、网卡等所有驱动、出厂主题壁纸、系统属性戴尔专属LOGO标志、Office办公软件、MyDell等预装程序 链接&#xff1a;https://pan.baidu.com/s/1VYUa7u0-Az4c9bOnWV9GZQ?pwd550m 提取码&#xff1a;550m

常见的查找算法以及分块搜索算法的简明教程

顺序查找 最基本的查找算法 举例 // 顺序查找public static int searchSequence(int[] arr, int target) {int i 0;for (int arr2 : arr) {if (arr2 target) {return i;}i;}return -1;}二分查找 [! warning] 值得注意的是这个二分查找算法只对无重复元素的递增或递减的数组有…