2024.2.17 模拟实现 RabbitMQ —— 内存数据管理

目录

需求分析

内存管理

实现 MemoryDataCenter 类

封装交换机操作

封装队列操作

封装绑定操作

封装消息操作

封装未确认消息操作

封装恢复数据操作

关于线程安全

针对 MemoryDataCenter 单元测试


需求分析

  • 当前我们已经实现了 数据库管理 交换机、绑定、队列,数据文件管理 消息
  • 即已经实现在硬盘上存储上述数据

注意:

  • 对于 MQ 来说,内存存储数据为主,硬盘存储数据为辅
  • 硬盘数据主要是为了能够进行持久化保存,重启之后,数据不丢失

内存管理

  • 交换机:使用 ConcurrentHashMap,其中 key 为 name,value 为 Exchange 对象
//    key 是 exchangeName,value 是 exchange 对象private ConcurrentHashMap<String, Exchange> exchangeMap = new ConcurrentHashMap<>();
  • 队列:使用 ConcurrentHashMap,其中 key 为 name,value 为 MSGQueue 对象
//    key 是 queueName,value 是 MSGQueue 对象private ConcurrentHashMap<String, MSGQueue> queueMap = new ConcurrentHashMap<>();
  • 绑定:使用嵌套 ConcurrentHashMap
  • key1 为 exchangeName,value1 为 ConcurrentHashMap
  • key2 为 queueName,value2 为 Binding 对象
//    第一个 key 是 exchangeName,第二个 key 是 queueNameprivate ConcurrentHashMap<String, ConcurrentHashMap<String, Binding>> bindingsMap = new ConcurrentHashMap<>();
  • 消息:使用 ConcurrentHashMap,其中 key 为 messageId,Value 为 Message 对象
//    key 是 messageId,value 是 Message 对象private ConcurrentHashMap<String, Message> messageMap = new ConcurrentHashMap<>();
  • 队列和消息之间的关联(每个队列中有哪些消息):使用 ConcurrentHashMap
  • key 为 queueName,value 为 LinkedList
  • LinkedList 中的每个元素为 Message 对象
//    key 是 queueName,value 是一个 Message 的链表private ConcurrentHashMap<String, LinkedList<Message>> queueMessageMap = new ConcurrentHashMap<>();
  • "未被确认" 消息(用来存储当前队列中哪些消息被消费者取走了,但还未应答):
  • 使用嵌套 ConcurrentHashMap
  • key1 为 queueName,value1 为 ConcurrentHashMap
  • key2 为 messageId,value 为 Message 对象
//    第一个 key 是 queueName,第二个 key 是 MessageIdprivate ConcurrentHashMap<String,ConcurrentHashMap<String,Message>> queueMessageWaitAckMap = new ConcurrentHashMap<>();

注意点一:

  • 消息 对应的 ConcurrentHashMap 中的 value 为 Message 对象
  • 此处 value 中存的是引用,由引用指向 Message 对象
  • 所以此处 value 中存的 Message 对象并没有占用重复的空间
  • 队列和消息之间的关联 对应的 LinkedList 中的元素 Message 对象同理
  • "未被确认" 消息 对应的 value2 中的 Message 对象同理

注意点二:

  • 后续实现消息确认逻辑,需要根据 ACK 响应的内容,该内容中包含一个确认的 messageId
  • 然后再根据这个 messageId 来把上述结构中的 Message 对象找到并移除

注意点三:

  • 此处实现的 MQ 支持两种应答模式(ACK)
  1. 自动应答:消费者取了元素,该消息就被应答了,此时该消息就可以被干掉了
  2. 手动应答:消费者取了元素之后,该消息还不算被应答,需消费者主动再调用一个 basicAck 方法,此时才认为是真正应答了,才能删除该消息

注意点四:

  • 此处我们主要使用 ConcurrentHashMap 来进行内存管理,其主要是为了线程安全 

实现 MemoryDataCenter 类

封装交换机操作

  • 针对 exchangeMap 进行插入、获取、删除
public void insertExchange(Exchange exchange) {exchangeMap.put(exchange.getName(),exchange);System.out.println("[MemoryDataCenter] 新交换机添加成功! exchangeName = " + exchange.getName());}public Exchange getExchange(String exchangeName) {return exchangeMap.get(exchangeName);}public void deleteExchange(String exchangeName) {exchangeMap.remove(exchangeName);System.out.println("[MemoryDataCenter] 交换机删除成功! exchangeName = " + exchangeName);}public void insertQueue(MSGQueue queue) {queueMap.put(queue.getName(),queue);System.out.println("[MemoryDataCenter] 新队列添加成功!queueName = " + queue.getName());}

封装队列操作

  • 针对 queueMap 进行插入、获取、删除
public void insertQueue(MSGQueue queue) {queueMap.put(queue.getName(),queue);System.out.println("[MemoryDataCenter] 新队列添加成功!queueName = " + queue.getName());}public MSGQueue getQueue(String queueName) {return queueMap.get(queueName);}public void deleteQueue(String queueName) {queueMap.remove(queueName);System.out.println("[MemoryDataCenter] 队列删除成功!queueName = " + queueName);}

封装绑定操作

  • 针对 bindingsMap 进行插入、获取、删除
 public void insertBinding(Binding binding) throws MqException {
//        先使用 exchangeName 查一下,对应的哈希表是否存在,不存在就创建一个
//        ConcurrentHashMap<String,Binding> bindingMap = bindingsMap.get(binding.getExchangeName());
//        if(bindingMap == null) {
//            bindingMap = new ConcurrentHashMap<>();
//            bindingsMap.put(binding.getExchangeName(),bindingMap);
//        }
//        改行代码等价于 上方注释掉的代码段ConcurrentHashMap<String,Binding> bindingMap = bindingsMap.computeIfAbsent(binding.getExchangeName(),k -> new ConcurrentHashMap<>());//        针对数据进行进一步的插入,根据 queueName 查一下,如果已经存在,就抛出异常,不存在才能插入synchronized (bindingMap) {if(bindingMap.get(binding.getQueueName()) != null){throw new MqException("[MemoryDataCenter] 绑定已经存在!exchangeName = " + binding.getExchangeName() +", queueName = " + binding.getQueueName());}bindingMap.put(binding.getQueueName(),binding);}System.out.println("[MemoryDataCenter] 新绑定添加成功!exchangeName = " + binding.getExchangeName() +", queueName = " + binding.getQueueName());}//    获取绑定,写两个版本:
//    1、根据 exchangeName 和 queueName 确定唯一一个 Binding
//    2、根据 exchangeName 获取到所有的 Bindingpublic Binding getBinding(String exchangeName,String queueName) {ConcurrentHashMap<String,Binding> bindingMap = bindingsMap.get(exchangeName);if(bindingMap == null) {return null;}return bindingMap.get(queueName);}public ConcurrentHashMap<String,Binding> getBindings(String exchangeName) {return bindingsMap.get(exchangeName);}public void deleteBinding(Binding binding) throws MqException {ConcurrentHashMap<String,Binding> bindingMap = bindingsMap.get(binding.getExchangeName());if(bindingMap == null) {
//            该交换机没有绑定任何队列,报错throw new MqException("[MemoryDataCenter] 绑定不存在!exchangeName = " + binding.getExchangeName() +", queueName = " + binding.getQueueName());}bindingMap.remove(binding.getQueueName());System.out.println("[MemoryDataCenter] 绑定删除成功!exchangeName = " + binding.getExchangeName() +", queueName = " + binding.getQueueName());}

封装消息操作

  • 针对 messageMap 进行添加、查询、删除
  • 针对 queueMessageMap 实现 发送消息到指定队列、从指定队列获取消息、获取指定队列消息个数
//    添加消息public void addMessage(Message message) {messageMap.put(message.getMessageId(), message);System.out.println("[MemoryDataCenter] 新消息添加成功! messageId = " + message.getMessageId());}//    根据 id 查询消息public Message getMessage(String messageId) {return messageMap.get(messageId);}//    根据 id 删除消息public void removeMessage(String messageId) {messageMap.remove(messageId);System.out.println("[MemoryDataCenter] 消息被移除! messageId = " + messageId);}//    发送消息到指定队列public void sendMessage(MSGQueue queue,Message message) {
//        把消息放到对应的队列数据结构中
//        先根据队列的名字,找到该队列对应的消息链表
//        LinkedList<Message> messages = queueMessageMap.get(queue.getName());
//        if(messages == null) {
//            messages = new LinkedList<>();
//            queueMessageMap.put(queue.getName(),messages);
//        }LinkedList<Message> messages = queueMessageMap.computeIfAbsent(queue.getName(), k -> new LinkedList<>());
//        再把数据加到 messages 里面synchronized (messages) {messages.add(message);}
//        在这里把该消息也往消息中心插入一下
//        假设如果 message 已经在消息中心存在,重复插入也没关系
//        主要就是相同 messageId,对应的 message 的内容一定是一样的(服务器代码不会对 Message 内容做修改 basicProperties 和 body)addMessage(message);System.out.println("[MemoryDataCenter] 消息被投递到队列中! messageId = " + message.getMessageId());}
//    从队列中取消息public Message pollMessage(String queueName) {
//        根据队列名,查找一下,对应的队列的消息链表LinkedList<Message> messages = queueMessageMap.get(queueName);
//        如果没有找到,说明队列中没有任何消息if(messages == null){return null;}synchronized (messages) {
//            如果存在消息链表但其中没有元素,也说明队列中没有任何消息if(messages.size() == 0) {return null;}
//        链表中有元素,就进行头删Message currentMessage = messages.remove(0);System.out.println("[MemoryDataCenter] 消息从队列中取出! messageId = " + currentMessage.getMessageId());return currentMessage;}}//    获取指定队列中消息的个数public int getMessageCount(String queueName) {LinkedList<Message> messages = queueMessageMap.get(queueName);if(messages == null) {
//            队列中没有消息return 0;}synchronized (messages) {return messages.size();}}

封装未确认消息操作

  • 针对 queueMessageWaitAckMap 进行插入、获取、删除
//    添加未确认消息public void addMessageWaitAck(String queueName,Message message) {ConcurrentHashMap<String,Message> messageHashMap = queueMessageWaitAckMap.computeIfAbsent(queueName,k -> new ConcurrentHashMap<>());messageHashMap.put(message.getMessageId(),message);System.out.println("[MemoryDataCenter] 消息进入待确认队列! messageId = " + message.getMessageId());}//    删除未确认消息 (消息已经确认了)public void removeMessageWaitAck(String queueName,String messageId) {ConcurrentHashMap<String,Message> messageHashMap  = queueMessageWaitAckMap.get(queueName);if(messageHashMap == null) {return;}messageHashMap.remove(messageId);System.out.println("[MemoryDataCenter] 消息从待确认队列删除! messageId = " + messageId);}//    获取指定的未确认的消息public Message getMessageWaitAck(String queueName,String messageId) {ConcurrentHashMap<String,Message> messageHashMap  = queueMessageWaitAckMap.get(queueName);if(messageHashMap == null) {return null;}return messageHashMap.get(messageId);}

封装恢复数据操作

  • 通过 recovery 方法将硬盘中存储的数据都恢复到内存中
//    这个方法就是从硬盘上读取数据,把硬盘中之前持久化存储的各个纬度的数据都恢复到内存中public void recovery(DiskDataCenter diskDataCenter) throws IOException, MqException, ClassNotFoundException {
//        0、清空之前的所有数据exchangeMap.clear();queueMap.clear();bindingsMap.clear();messageMap.clear();queueMessageMap.clear();
//        1、恢复所有的交换机数据List<Exchange> exchanges = diskDataCenter.selectAllExchanges();for (Exchange exchange : exchanges) {exchangeMap.put(exchange.getName(),exchange);}
//        2、恢复所有的队列数据List<MSGQueue> queues = diskDataCenter.selectAllQueues();for (MSGQueue queue : queues) {queueMap.put(queue.getName(),queue);}
//        3、恢复所有的绑定数据List<Binding> bindings = diskDataCenter.selectAllBindings();for (Binding binding : bindings) {ConcurrentHashMap<String,Binding> bindingMap = bindingsMap.computeIfAbsent(binding.getExchangeName(),k -> new ConcurrentHashMap<>());bindingMap.put(binding.getQueueName(),binding);}
//        4、恢复所有的消息数据
//           遍历所有的队列,根据每个队列的名字,获取到所有的消息for (MSGQueue queue: queues) {LinkedList<Message> messages = diskDataCenter.loadAllMessageFromQueue(queue.getName());queueMessageMap.put(queue.getName(),messages);for (Message message : messages) {messageMap.put(message.getMessageId(),message);}}
//        注意!!针对 "未确认的消息" 这部分内存中的数据,不需要从硬盘恢复,之前考虑硬盘存储的数据,也没设定这一块
//        一旦在等待 ack 的过程中,服务器重启了,此时这些 "未被确认的消息",就恢复成 "未被取走的消息"
//        这个消息在硬盘上存储的时候,就是当作 "未被取走"}

关于线程安全

问题:

  • 上述代码中,哪一段需要加锁?加锁的范围是什么?使用哪个对象作为锁对象?

回答:

  • 此处我们很难一概而论,只能具体问题具体分析

总的原则:

  • 分析该段代码如果不加锁,会造成什么样的后果/问题?该后果/问题是否严重?

实例理解:

  • 红框代码中的 if 逻辑 和 bindingMap 的 put 操作是分开的
  • 即可能存在 bindingsMap 中,同一个交换机绑定两个 queueName 相同的 queue 对象
  • 如果这两个 binding 对象中的 bindingKey 不同,那么该 queueName 对应的 bindingKey 是哪一个 bindingKey?
  • 该问题会造成十分严重的后果,所以我们要给该段代码加锁!

  • 绿框代码中的 if 逻辑 和 messageHashMap 的 remove 操作是分开的
  • 但是该段代码所产生的线程安全问题微乎其微
  • 线程A 执行了 remove 操作,此时线程B 同样执行 remove 操作,如果将 null 传递给 ConcurrentHashMap 的 remove 方法,该方法将不会进行任何操作,并返回 null
  • 所以该段代码无需加锁!

针对 MemoryDataCenter 单元测试

  • 编写测试用例代码是十分重要的!
package com.example.demo;import com.example.demo.common.MqException;
import com.example.demo.mqserver.core.*;
import com.example.demo.mqserver.datacenter.DiskDataCenter;
import com.example.demo.mqserver.datacenter.MemoryDataCenter;
import org.apache.tomcat.util.http.fileupload.FileUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.test.context.SpringBootTest;import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;@SpringBootTest
public class MemoryDataCenterTests {private MemoryDataCenter memoryDataCenter = null;@BeforeEachpublic void setUp(){memoryDataCenter = new MemoryDataCenter();}@AfterEachpublic void tearDown() {memoryDataCenter = null;}//    创建测试交换机private Exchange createTestExchange(String exchangeName) {Exchange exchange = new Exchange();exchange.setName(exchangeName);exchange.setType(ExchangeType.DIRECT);exchange.setAutoDelete(false);exchange.setDurable(true);return exchange;}//    创建测试队列private MSGQueue createTestQueue(String queueName) {MSGQueue queue = new MSGQueue();queue.setName(queueName);queue.setDurable(true);queue.setExclusive(false);queue.setAutoDelete(false);return queue;}//    针对交换机进行测试@Testpublic void testExchange() {
//        1、先构造一个交换机并插入Exchange expectExchange = createTestExchange("testExchange");memoryDataCenter.insertExchange(expectExchange);
//        2、查询出这个交换机,比较结果是否一致 此处直接比较这俩引用指向同一个对象Exchange actualExchange = memoryDataCenter.getExchange("testExchange");Assertions.assertEquals(expectExchange,actualExchange);
//        3、删除这个交换机memoryDataCenter.deleteExchange("testExchange");
//        4、再查一次,看是否就差不到了actualExchange = memoryDataCenter.getExchange("testExchange");Assertions.assertNull(actualExchange);}//    针对队列进行测试@Testpublic void testQueue() {
//        1、构造一个队列MSGQueue expectedQueue = createTestQueue("testQueue");memoryDataCenter.insertQueue(expectedQueue);
//        2、查询这个队列MSGQueue actualQueue = memoryDataCenter.getQueue("testQueue");Assertions.assertEquals(expectedQueue,actualQueue);
//        3、删除这个队列memoryDataCenter.deleteQueue("testQueue");
//        4、再次查询队列,看是否能查到actualQueue = memoryDataCenter.getQueue("testQueue");Assertions.assertNull(actualQueue);}//    针对绑定进行测试@Testpublic void testBinding() throws MqException {Binding expectedBinding = new Binding();expectedBinding.setExchangeName("testExchange");expectedBinding.setQueueName("testQueue");expectedBinding.setBindingKey("testBindingKey");memoryDataCenter.insertBinding(expectedBinding);Binding actualBinding = memoryDataCenter.getBinding("testExchange","testQueue");Assertions.assertEquals(expectedBinding,actualBinding);ConcurrentHashMap<String,Binding> bindingMap = memoryDataCenter.getBindings("testExchange");Assertions.assertEquals(1,bindingMap.size());Assertions.assertEquals(expectedBinding,bindingMap.get("testQueue"));memoryDataCenter.deleteBinding(expectedBinding);actualBinding = memoryDataCenter.getBinding("testExchange","testQueue");Assertions.assertNull(actualBinding);}private Message createTestMessage(String content) {Message message = Message.createMessageWithId("testRoutingKey",null,content.getBytes());return message;}@Testpublic void testMessage() {Message expectedMessage = createTestMessage("testMessage");memoryDataCenter.addMessage(expectedMessage);Message actualMessage = memoryDataCenter.getMessage(expectedMessage.getMessageId());Assertions.assertEquals(expectedMessage,actualMessage);memoryDataCenter.removeMessage(expectedMessage.getMessageId());actualMessage = memoryDataCenter.getMessage(expectedMessage.getMessageId());Assertions.assertNull(actualMessage);}@Testpublic void testSendMessage() {
//        1、创建一个队列,创建 10 条消息,把这些消息都插入队列中MSGQueue queue = createTestQueue("testQueue");List<Message> expectedMessages = new ArrayList<>();for (int i = 0; i < 10; i++) {Message message  = createTestMessage("testMessage" + i);memoryDataCenter.sendMessage(queue,message);expectedMessages.add(message);}
//        2、从队列中取出这些消息List<Message> actualMessages = new ArrayList<>();while (true) {Message message = memoryDataCenter.pollMessage("testQueue");if (message == null) {break;}actualMessages.add(message);}
//        3、比较取出的消息和之前的消息是否一致Assertions.assertEquals(expectedMessages.size(),actualMessages.size());for (int i = 0; i < expectedMessages.size(); i++) {Assertions.assertEquals(expectedMessages.get(i),actualMessages.get(i));}}@Testpublic void testMessageWaitAck() {Message expectedMessage = createTestMessage("expectedMessage");memoryDataCenter.addMessageWaitAck("testQueue",expectedMessage);Message actualMessage = memoryDataCenter.getMessageWaitAck("testQueue",expectedMessage.getMessageId());Assertions.assertEquals(expectedMessage,actualMessage);memoryDataCenter.removeMessageWaitAck("testQueue", expectedMessage.getMessageId());actualMessage = memoryDataCenter.getMessageWaitAck("testQueue",expectedMessage.getMessageId());Assertions.assertNull(actualMessage);}//@Testpublic void testRecovery() throws IOException, MqException, ClassNotFoundException {
//        由于后续需要进行数据库操作,依赖 MyBatis,就需要先启动 SpringApplication,这样才能进行后续的数据库操作DemoApplication.context = SpringApplication.run(DemoApplication.class);
//        1、在硬盘上构造好数据DiskDataCenter diskDataCenter = new DiskDataCenter();diskDataCenter.init();//       构造交换机Exchange expectedExchange = createTestExchange("testExchange");diskDataCenter.insertExchange(expectedExchange);//        构造队列MSGQueue expectedQueue = createTestQueue("testQueue");diskDataCenter.insertQueue(expectedQueue);//        构造绑定Binding expectedBinding = new Binding();expectedBinding.setExchangeName("testExchange");expectedBinding.setQueueName("testQueue");expectedBinding.setBindingKey("testBindingKey");diskDataCenter.insertBinding(expectedBinding);//        构造消息Message expectedMessage = createTestMessage("testContent");diskDataCenter.sendMessage(expectedQueue,expectedMessage);
//        2、执行恢复操作memoryDataCenter.recovery(diskDataCenter);//        3、对比结果Exchange actualExchange = memoryDataCenter.getExchange("testExchange");Assertions.assertEquals(expectedExchange.getName(),actualExchange.getName());Assertions.assertEquals(expectedExchange.getType(),actualExchange.getType());Assertions.assertEquals(expectedExchange.isDurable(),actualExchange.isDurable());Assertions.assertEquals(expectedExchange.isAutoDelete(),actualExchange.isAutoDelete());MSGQueue actualQueue = memoryDataCenter.getQueue("testQueue");Assertions.assertEquals(expectedQueue.getName(),actualQueue.getName());Assertions.assertEquals(expectedQueue.isDurable(),actualQueue.isDurable());Assertions.assertEquals(expectedQueue.isAutoDelete(),actualQueue.isAutoDelete());Assertions.assertEquals(expectedQueue.isExclusive(),actualQueue.isExclusive());Binding actualBinding = memoryDataCenter.getBinding("testExchange","testQueue");Assertions.assertEquals(expectedBinding.getExchangeName(),actualBinding.getExchangeName());Assertions.assertEquals(expectedBinding.getQueueName(),actualBinding.getQueueName());Assertions.assertEquals(expectedBinding.getBindingKey(),actualBinding.getBindingKey());Message actualMessage = memoryDataCenter.pollMessage("testQueue");Assertions.assertEquals(expectedMessage.getMessageId(),actualMessage.getMessageId());Assertions.assertEquals(expectedMessage.getRoutingKey(),actualMessage.getRoutingKey());Assertions.assertEquals(expectedMessage.getDeliverMode(),actualMessage.getDeliverMode());Assertions.assertArrayEquals(expectedMessage.getBody(),actualMessage.getBody());//        4、清理硬盘的数据,把整个 data 目录里的内容都删掉(包含了 meta.db 和 队列的目录)DemoApplication.context.close();File dataDir = new File("./data");FileUtils.deleteDirectory(dataDir);}
}

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

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

相关文章

使用Python生成二维码的完整指南

无边落木萧萧下&#xff0c;不如跟着可莉一起游~ 可莉将这篇博客收录在了&#xff1a;《Python》 可莉推荐的优质博主首页&#xff1a;Kevin ’ s blog 本文将介绍如何使用Python中的qrcode库来生成二维码。通过简单的代码示例和详细解释&#xff0c;读者将学习如何在Python中轻…

突发!测试OpenAI新产品——sora

哈喽大家好&#xff0c;我是chowley&#xff0c;最近sora真是垄断了科技区的话题榜&#xff0c;几乎每个技术博主都上来讲两句 我在半年前也是一名深度学习的研究者&#xff0c;今天我以测试开发工程师的视角来解读一下sora&#xff01; 首先打开OpenAI官网的sora页面&#x…

春节专题|产业7问:区块链厂商的现在和未来——混合技术厂商

2023转瞬即逝&#xff0c;不同于加密领域沉寂一整年后在年末集中爆发&#xff0c;对于我国的区块链厂商而言&#xff0c;稳中求胜才是关键词&#xff0c;在平稳发展的基调下&#xff0c;产业洗牌也悄无声息的到来。 从产业总体而言&#xff0c;在经过了接近3年的快速发展后&…

解决vitepress首次加载慢(从40秒到1秒的倔强)

前言&#xff1a;在艰难的博客系统升级之路 这篇博客中我有提到vitepress首次加载非常耗时的问题&#xff0c;之前也在网上搜索时发现也有很多人说这个“问题”&#xff0c;但是在折腾了这么一段时间后&#xff0c;发现这也许本身不是vitepress的问题&#xff0c;而是我的启动方…

【Java多线程】线程中几个常见的属性以及状态

目录 Thread的几个常见属性 1、Id 2、Name名称 3、State状态 4、Priority优先级 5、Daemon后台线程 6、Alive存活 Thread的几个常见属性 1、Id ID 是线程的唯一标识&#xff0c;由系统自动分配&#xff0c;不同线程不会重复。 2、Name名称 用户定义的名称。该名称在各种…

百度地图接口 | 实现校验收货地址是否超出配送范围

目录 1. 环境准备 2. 代码开发 2.1 application.yml 2.2 OrderServiceImpl &#x1f343;作者介绍&#xff1a;双非本科大三网络工程专业在读&#xff0c;阿里云专家博主&#xff0c;专注于Java领域学习&#xff0c;擅长web应用开发、数据结构和算法&#xff0c;初步涉猎Py…

数据结构-双指针法

介绍 双指针法是一种可以在O&#xff08;n&#xff09;时间复杂度内解决数组、链表、字符串等数据结构相关的问题的方法。核心思想为使用两个指针在不同位置遍历数组或链表&#xff0c;从而实现特定操作。 常见的双指针法有 1.快慢指针&#xff1a;快指针每次移动两步&…

AI:131- 法律文件图像中的隐含信息挖掘与敲诈勒索检测

🚀点击这里跳转到本专栏,可查阅专栏顶置最新的指南宝典~ 🎉🎊🎉 你的技术旅程将在这里启航! 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的关键代码,详细讲解供…

天锐绿盾|防泄密系统|计算机文件数据\资料安全管理软件

“天锐绿盾”似乎是一款专注于防泄密和计算机文件数据/资料安全管理的软件。在信息安全日益受到重视的今天&#xff0c;这样的软件对于保护企业的核心数据资产和防止敏感信息泄露至关重要。 通用地址&#xff1a;www.drhchina.com 防泄密系统的主要功能通常包括&#xff1a; 文…

二进制和进制转换

前言 我们经常能听到2进制、8进制、10进制、16进制这样的讲法&#xff0c;那是什么意思呢&#xff1f;其实2进制、8进 制、10进制、16进制是数值的不同表示形式而已。 比如&#xff1a;数值15的各种进制的表示形式&#xff1a; 15的2进制&#xff1a;111115的8进制&#xff1…

阅读笔记(BMSB 2018)Video Stitching Based on Optical Flow

参考文献 Xie C, Zhang X, Yang H, et al. Video Stitching Based on Optical Flow[C]//2018 IEEE International Symposium on Broadband Multimedia Systems and Broadcasting (BMSB). IEEE, 2018: 1-5. 摘要 视频拼接在计算机视觉中仍然是一个具有挑战性的问题&#xff0…

wps使用方法(包括:插入倒三角符号,字母上面加横线,将word中的所有英文设置为time new roman)

倒三角符号 字母上面加横线 将word中的所有英文设置为time new roman ctrla选中全文

语音唤醒——

文章目录 配置主代码 参考文档&#xff1a;https://picovoice.ai/docs/quick-start/porcupine-python/ 配置 pip install pvporcupine主代码 ACCESS_KEY&#xff1a;需要将该参数填入即可 # # Copyright 2018-2023 Picovoice Inc. # # You may not use this file except in …

MySQL事务的概念

一、事务定义 事务&#xff1a;事务是一个最小的不可在分的工作单元&#xff1b;通常一个事务对应一个完整的业务(例如银行账户转账业务&#xff0c;该业务是一个最小的工作单元)一个完整的业务需要批量的DML(insert、update、delete)语句共同联合完成。事务只和DML语句有关&a…

Java基于微信小程序的医院挂号小程序,附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

Jenkins 2.426.3新版设置中文

1. 插件页面显示无法联网 &#xff0c;点击Plugins一直提示连接超时&#xff0c;设置公司代理后 2. 稍等一会儿点击如下图&#xff0c;插件就出来了&#xff0c;然后输入Locale进行下载 3. 以下是我下载安装好的 4.打开设置&#xff0c;找到Locale选项&#xff0c;设置成zh_CN…

第13章 网络 Page738~741 13.8.3 TCP/UDP简述

libcurl是C语言写成的网络编程工具库&#xff0c;asio是C写的网络编程的基础类型库 libcurl只用于客户端&#xff0c;asio既可以写客户端&#xff0c;也可以写服务端 libcurl实现了HTTP\FTP等应用层协议&#xff0c;但asio却只实现了传输层TCP/UDP等协议。 在学习http时介绍…

【IO流】FileOutputStream 字节输出流

FileOutputStream 字节输出流 1. 概述2. 作用3. 书写步骤4. 构造方法5. 换行写6. 续写7. 注意事项 1. 概述 FileOutputStream 是 Java 中用于向文件写入字节数据的输出流类。它用于创建一个文件输出流&#xff0c;该流用于将数据写入文件。 功能&#xff1a;FileOutputStream …

C高级D5作业

1.#!/bin/bash read -p "请输入一个字符>>" -n 1 c echo case $c in [[:lower:]]) echo "小写" ;; [[:upper:]]) echo "大写" ;; [1-9]) echo "数字" ;; …

阿里云香港网络线路类型BGP(多线)精品延迟测试

阿里云香港等地域服务器的网络线路类型可以选择BGP&#xff08;多线&#xff09;和 BGP&#xff08;多线&#xff09;精品&#xff0c;普通的BGP多线和精品有什么区别&#xff1f;BGP&#xff08;多线&#xff09;适用于香港本地、香港和海外之间的互联网访问。使用BGP&#xf…