项目实战 — 消息队列(6){内存数据管理}

目录

一、设计数据结构

二 、实现管理方法

🍅 1、实现交换机管理

🍅 2、实现队列管理

🍅 3、实现绑定管理

        🎈插入绑定操作

        🎈删除绑定

🍅 4、进行消息管理

🍅 5、发送消息到指定队列

🍅 6、表示“未被确认”的消息管理

🍅 7、从硬盘上读取数据

三、测试交换机操作

🍅 1、“准备工作”和收尾工作

🍅 2、测试 交换机

🍅 3、测试 队列

🍅 4、测试 绑定

🍅 5、测试 消息的增删查

🍅 6、测试 发送消息

🍅 7、测试 “未确认”的信息 

 🍅 8、测试 从硬盘上读取数据

四、小结


一、设计数据结构

对于MQ来说,主要是以内存存储数据为主,硬盘存储数据为辅。

关于内存数据管理,作出如下的数据结构:

交换机:使用HashMap,key是name, value是Exchange对象

队列:使用HashMap,key是name,value是MSGQueue对象

绑定:使用嵌套的HashMap,key是exchangeName,value是一个HashMap(其中key是queueName,value是Binding对象)

消息:使用HashMap,key是messageId,value是Message

表示队列和消息之间的关联:使用嵌套的HashMap,key是queueName,value是一个LinkedList。LinkedList中每个元素又是一个Message对象。

表示“未被确认”的消息:关于未被确认:存储了当前队列中哪些消息被消费者取走了,但是还没有应答。使用嵌套的HashMap,key是queueName,value是HashMap(其中key是messageId,value是Message对象)。后续实现消息确认的逻辑,需要根据ack响应的内容,会提供一个messageId,根据该messageId把结构中的Message对象找到并且移除

这里有两种应答模式(ACK):

 1.自动应答,消费者取了元素,该消息就算是被应答,就可以被删除了

 2.手动应答,消费者取了元素,该消息还不断被应答, 需要消费者主动再调用一个basicAck方法,此时才被认为是真的应答了,才能删除这个消息。

 

由于, 这个类会涉及到多线程的请求,所以这里的HashMap都统一使用ConcurrentHashMap,因为HashMap是线程不安全的,ConcurrentHashMap相对而来线程安全,所以上面说的使用HashMap都使用ConcurrentHashMap

public class MemoryDataCenter {
//    交换机:key是exchangeMame,value是exchange对象private ConcurrentHashMap<String, Exchange> exchangeMap = new ConcurrentHashMap<>();
//    队列:key表示queueName,value表示MSGQueue对象private ConcurrentHashMap<String, MSGQueue> queueMap = new ConcurrentHashMap<>();
//    绑定:第一个key表示exchangeName,第二个key表示queueName,value都表示Binding对象private ConcurrentHashMap<String,ConcurrentHashMap<String, Binding>> bindingsMap = new ConcurrentHashMap<>();
//    消息:key是messageId,value是Message对象private ConcurrentHashMap<String, Message> messageMap = new ConcurrentHashMap<>();
//    表示队列和消息之间的关联:key表示queueName,value表示一个Message链表,里面存放的是Message对象private ConcurrentHashMap<String, LinkedList<Message>> queueMessageMap = new ConcurrentHashMap<>();
//  表示“未被确认”的消息:第一个key表示queueName,第二个key表示messageId,value表示Message对象private ConcurrentHashMap<String,ConcurrentHashMap<String,Message>> queueMessageWaitAckMap = new ConcurrentHashMap<>();
}


二 、实现管理方法

🍅 1、实现交换机管理

 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);}


🍅 2、实现队列管理

  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);}


🍅 3、实现绑定管理

        🎈插入绑定操作

注意点:

(1)这里是嵌套的HashMap,所以再插入钱,要先使用exchangeName查找对应的哈希表是否存在,不存在就创建。

(2)线程安全问题。插入要先判断绑定是否存在,不存在才插入。不是原子操作,要加锁。

        🎈得到绑定

注意:这里有两个版本

        (1)根据exchangeName和queueName确定一个binding

        (2)根据exchangeName获取到所有的binding

public 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 {
//        现根据exchangeName找到所有的bindingConcurrentHashMap<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.getQueueName()+ ",queueName = " + binding.getQueueName());}

🍅 4、进行消息管理

//   添加消息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);}


🍅 5、发送消息到指定队列

//    发送消息到指定队列public void sendMessage(MSGQueue queue,Message message){
//        把消息放到对应的队列数据结构中
//        现根据队列的名字,找到该队列对应的消息链表LinkedList<Message> messages = queueMessageMap.computeIfAbsent(queue.getName(),k -> new LinkedList<>());
//        把数据加到messages里面synchronized (messages){messages.add(message);}
//            把该消息往消息中心中插入,假设message已经在消息中心存在,重复插入也没有关系
//        主要就是相同messageId,对应的message内容一定是一样的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();}}


🍅 6、表示“未被确认”的消息管理

//    添加未确认的消息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);}


🍅 7、从硬盘上读取数据

把之前硬盘中持久化存储的各个维度的数据都恢复到内存中来,

主要是以下几步:

(1)恢复所有的交换机数据

(2)恢复所有的队列

(3)恢复所有的绑定数据

(4)恢复所有的消息数据

注意:关于“未被确认的消息”:“未被确认的消息”是内存中的数据,不需要从硬盘上恢复,一旦在等待ack的过程中,服务器重启了,此时这些“未被确认的消息”,就会恢复成“未被取走的消息”,这些消息在硬盘在硬盘在硬盘上存储的时候,就当作是“未被”取走

//    从硬盘上读取数据,
//    把之前硬盘中持久化存储的各个维度的数据都恢复到内存中来public void recovery(DiskDataCenter diskDataCenter) throws IOException, MqException, ClassNotFoundException {
//        先清空之前的数据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);}}}


三、测试交换机操作

🍅 1、“准备工作”和收尾工作

@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.setDurable(true);return exchange;}//    创建一个测试队列private MSGQueue createTestQueue(String queueName){MSGQueue queue = new MSGQueue();queue.setName(queueName);queue.setDurable(true);return queue;}//    创建一个测试消息public Message createTestMessage(String content){Message message = Message.createMessageWithId("testRoutinKey",null,content.getBytes());return message;}


🍅 2、测试 交换机

//    针对交换机进行测试@Testpublic void testExchange(){
//        1.先构造一个交换机并且插入Exchange expectedExchange = createTestExchange("testExchange");memoryDataCenter.insertExchange(expectedExchange);
//        2.查询这个交换机,比较结果是否一致Exchange actualExchange = memoryDataCenter.getExchange("testExchange");Assertions.assertEquals(expectedExchange,actualExchange);
//        3.删除这个交换机memoryDataCenter.deleteExchange("testExchange");
//        4.再次查找actualExchange = memoryDataCenter.getExchange("testExchange");Assertions.assertNull(actualExchange);}


🍅 3、测试 队列

 @Testpublic void testQueu(){
//        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);}


🍅 4、测试 绑定

//    针对绑定进行测试@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);}


🍅 5、测试 消息的增删查

@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);}


🍅 6、测试 发送消息

//    测试发送消息@Testpublic void testSendMessage(){
//        1、创建一个队列,创建十条消息,把这些消息都插入到队列中MSGQueue queue = createTestQueue("testQueue");List<Message> expectedMessages = new ArrayList<>();for (int i = 0; i < 10; i++) {Message message = createTestMessage("testMessage" + 1);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));}}


🍅 7、测试 “未确认”的信息 

  @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);}


 🍅 8、测试 从硬盘上读取数据

 测试这个用例,主要分为3步:

        (1)在硬盘上构造好数据

        (2)执行恢复操作

        (3)对比结果

@Testpublic void testRecovery() throws IOException, MqException, ClassNotFoundException {//        后续操作雨MyBatis有关,所以需要启动SpringApplicationTigerMqApplication.context = SpringApplication.run(TigerMqApplication.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());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 和 队列的目录).TigerMqApplication.context.close();File dataDir = new File("./data");FileUtils.deleteDirectory(dataDir);}

 


 四、小结

  这一块的内容,主要就是借助内存中的一系列数据结构,保存、管理交换机、队列、绑定、消息,使用到了哈希表、链表、嵌套的结构等。

这里还频繁的使用了加锁的操作,具体场景考虑是否要加锁(特别是有插入操作)。

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

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

相关文章

详解使用SSH远程连接Ubuntu服务器系统

演示环境&#xff1a; 1.Windows10系统 2.VMware Workstation Pro虚拟机 2.Ubuntu16.04.6&#xff08;以上版本通用&#xff09; 回归正题 一、在Ubuntu端&#xff1a; 1.首先需要安装SSH服务器&#xff0c;在ubuntu终端输入以下指令 sudo apt-get install ssh2.输入你的ubu…

布隆过滤器,Guava实现布隆过滤器(本地内存),Redis实现布隆过滤器(分布式)

一、前言 利用布隆过滤器可以快速地解决项目中一些比较棘手的问题。如网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题。不知道从什么时候开始&#xff0c;本来默默无闻的布隆过滤器一下子名声大噪&#xff0c;在面试中面试官问到怎么避免缓存穿透&#…

露营,「迷失」在春风里

【潮汐商业评论/原创】 “周末一起去露营吧&#xff1f;”Susan向闺蜜发起邀请。 Susan闺蜜看到后连忙说“去多了感觉没意思&#xff0c;还不如去隔壁城市走走呢&#xff1f;”两人一拍即合&#xff0c;便研究起了攻略。 Susan疑惑&#xff0c;好像露营也没火多久&#xff0…

ROS获取IMU的数据

消息格式 上ROS官网查看&#x1f449;ROS ROS官网给定的主题&#xff0c;一般我们使用第二个。   实现思路 &#xff08;1&#xff09;maweiUbuntu:~/catkin_ws/src$ catkin_create_pkg imu_pkg roscpp rospy sensor_msgs 实现代码&#x1f447;   //imu_node.cpp #in…

Spring 事务失效的八种场景

1. 抛出检查异常导致事务不能正确回滚 Service public class Service1 {Autowiredprivate AccountMapper accountMapper;Transactionalpublic void transfer(int from, int to, int amount) throws FileNotFoundException {int fromBalance accountMapper.findBalanceBy(from…

Sketch打不开AI文件?转换方法在这里

1、对比设计软件 Sketch 与 AI 软件功能 Sketch 与 Illustrator 都是行业内优秀的矢量图形设计软件&#xff0c;各有千秋。Sketch 从 2010 年面世&#xff0c;专注 APP 界面设计&#xff0c;深受初学者与专业人士喜爱。Illustrator 拥有更悠久的历史&#xff0c;是处理复杂图标…

Java基础入门篇——Java变量类型的转换和运算符(七)

目录 一、变量类型 1.1自动类型转换&#xff08;隐式转换&#xff09; 1.2 强制类型转换&#xff08;显式转换&#xff09; 1.3类型转换的其他情况 二、运算符 2.1算术运算符 2.2比较运算符 2.3逻辑运算符 2.4位运算符 三、总结 在Java中&#xff0c;变量类型的转换…

重生学c++系列第三课类和对象(上)

好的我们重生c系列的前两期已经介绍完了c祖师爷针对C语言补充的几个新功能&#xff0c;现在我们进入c的真正课题学习——类与对象: C语言是面向过程的&#xff0c;关注的是过程&#xff0c;分析出求解问题的步骤&#xff0c;通过函数调用逐步解决问题。 比如说我们洗菜做饭&am…

大数据培训课程-《机器学习从入门到精通》上新啦

《机器学习从入门到精通》课程是一门专业课程&#xff0c;面向人工智能技术服务&#xff0c;课程系统地介绍了Python编程库、分类、回归、无监督学习和模型使用技巧以及算法和案例充分融合。 《机器学习从入门到精通》课程亮点&#xff1a; 课程以任务为导向&#xff0c;逐步学…

【chrome扩展开发】vue-i18n使用问题及解决方案

记录chrome扩展开发时调用vue-i18n的一些问题和解决方法 环境 vue: ^3.3.4vue-i18n: ^9.2.2vite: ^4.4.8 错误1 Uncaught (in promise) EvalError: Refused to evaluate a string as JavaScript because unsafe-eval is not an allowed source of script in the following Con…

C++ 友元

文章目录 前言一、什么是友元二、友元的特性三、示例代码总结 前言 在C编程中&#xff0c;友元&#xff08;friend&#xff09;是一种特殊的关系&#xff0c;允许一个类或函数访问另一个类中的私有成员。 一、什么是友元 1.友元 的定义&#xff1a; 友元在C中可以被用于类和…

Jmeter添加cookie的两种方式

jmeter中添加cookie可以通过配置HTTP Cookie Manager&#xff0c;也可以通过HTTP Header Manager&#xff0c;因为cookie是放在头文件里发送的。 实例&#xff1a;博客园点击添加新随笔 https://i.cnblogs.com/EditPosts.aspx?opt1 如果未登录&#xff0c;跳转登录页&#xf…