RabbitMQ死信队列详解

什么是死信队列

由于特定的**原因导致 Queue 中的某些消息无法被消费,**这类消费异常的数据将会保存在死信队列中防止消息丢失,例如用户在商城下单成功并点击支付后,在指定时间未支付时的订单自动失效
死信队列只不过是绑定在死信交换机上的队列。死信交换机只不过是用来接受死信的交换机,可以为任何类型【Direct、Fanout、Topic】。一般来说,会为每个业务队列分配一个独有的路由key,并对应的配置一个死信队列进行监听,也就是说,一般会为每个重要的业务队列配置一个死信队列。

来源

  1. 消息的存活时间到了,ttl过期
  2. 队列积压的消息达到最大长度(在队列中等待时间最久的消息会成为死信)
  3. 消息被拒(消费方返回nack进行否定应答)且不重新加入队列(requeue=false)

演示

模拟一条正常应该被C1消费者接收的消息,由于出现消费异常情况进入死信队列被C2消费者进行消费的案例

架构图

死信队列案例示意图
死信队列标记


TTL过期

  1. 在生产者方进行指定为当前发送消息的过期时间,缺点是消息即使过期也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的(** 如果当前队列有严重的消息积压情况,已过期的消息依旧会被积压在队列中,如果队列配置了消息积压上限,**将导致后续应当正常消费的消息全部进入死信队列 )
  2. 在队列指定为所有到达该队列的消息的过期时间,时间从消息入队列开始计算,只要超过了队列的超时时间配置,消息会自动清除

该案例为在生产者方进行指定
配置类:

package com.example.rabbitmqqoslimiting.demos;import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;public class RabbitUtils {private static final ConnectionFactory connectionFactory;    //放到静态代码块中,在类加载时执行,只执行一次。达到工厂只创建一次,每次获取是新连接的效果static {//创建连接工厂connectionFactory = new ConnectionFactory();connectionFactory.setHost("101.133.141.74");                   //设置MQ的主机地址connectionFactory.setPort(5672);                                //设置MQ服务端口connectionFactory.setVirtualHost("study");                      //设置Virtual Hosts(虚拟主机)connectionFactory.setUsername("guest");                         //设置MQ管理人的用户名(要在Web版先配置,保证该用户可以管理设置的虚拟主机)connectionFactory.setPassword("guest");                           //设置MQ管理人的密码}//定义提供连接对象的方法,封装public static Connection getConnection(){try {//创建连接对象并返回return connectionFactory.newConnection();} catch (Exception e) {e.printStackTrace();}return null;}//关闭通道和关闭连接工具类的方法public static void closeConnectionAndChannel(Channel channel, Connection connection){try {if (channel!=null) {channel.close();}if (connection!=null){connection.close();}} catch (Exception e) {e.printStackTrace();}}
}

生产者:

package com.dmbjz.one;import com.dmbjz.utils.RabbitUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;import java.nio.charset.StandardCharsets;/* 死信队列TTL案例 生产者 */
public class Provider {private static final String EXCHANGE_NAME = "normal_exchange";              //正常交换机名称private static final String KEY = "zhangsan";        //普通队列 RoutingKeypublic static void main(String[] args) throws Exception {Connection connection = RabbitUtils.getConnection();Channel channel = connection.createChannel();channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.DIRECT);  //声明交换机/*死信队列 设置TTL消息过期时间 单位毫秒*/AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("20000").build();/*模拟消息循环发送*/for(int i = 1; i < 11; i++) {String message = "INFO " + i;channel.basicPublish(EXCHANGE_NAME,KEY,properties,message.getBytes(StandardCharsets.UTF_8));  }}
}

消费者C1:

package com.dmbjz.one;import com.dmbjz.utils.RabbitUtils;
import com.rabbitmq.client.*;import java.io.IOException;
import java.util.HashMap;
import java.util.Map;/* 死信队列TTL案例 消费者C1 */
public class ConsumerC1 {private static final String EXCHANGE_NAME = "normal_exchange";              //正常交换机名称private static final String DEAD_EXCHANGE_NAME = "dead_exchange";           //死信队列交换机名称private static final String KEY = "zhangsan";        //普通队列 RoutingKeyprivate static final String DEAD_KEY = "lisi";       //死信队列 RoutingKeyprivate static final String QUEUE_NAME = "normal-queue";       //普通队列名称private static final String DEAD_QUEUE_NAME = "dead-queue";    //死信队列名称public static void main(String[] args) throws IOException {Connection connection = RabbitUtils.getConnection();Channel channel = connection.createChannel();/*声明死信和普通交换机,正常交换机已被生产者声明,实际可以省略第一行代码*/channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);/*创建队列* 通过额外参数实现什么情况下转发到死信队列 ?,key都是固定的*   1、TTL过期时间设置(一般由生产者指定)*   2、死信交换机的名称*   3、死信交换机的RoutingKey* */Map<String,Object> arguments = new HashMap<>(8);arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);     //死信交换机的名称arguments.put("x-dead-letter-routing-key",DEAD_KEY);            //死信交换机的RoutingKey// arguments.put("x-dead-letter-ttl",10000);   指定消息的有效时间为20秒(一般为生产者指定)channel.queueDeclare(QUEUE_NAME,false,false,false,arguments);channel.queueDeclare(DEAD_QUEUE_NAME,false,false,false,null);/*绑定队列*/channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,KEY);channel.queueBind(DEAD_QUEUE_NAME,DEAD_EXCHANGE_NAME,DEAD_KEY);DeliverCallback successBack = (consumerTag, message) -> {System.out.println("C1用户接收到的信息为:"+new String(message.getBody()));};CancelCallback cnaelBack = a->{System.out.println("C1用户进行取消消费操作!");};channel.basicConsume(QUEUE_NAME,true,successBack,cnaelBack);}
}

消费者C2:

package com.dmbjz.one;import com.dmbjz.utils.RabbitUtils;
import com.rabbitmq.client.*;import java.io.IOException;/* 死信队列TTL案例 消费者C2 */
public class ConsumerC2 {private static final String DEAD_EXCHANGE_NAME = "dead_exchange";           //死信队列交换机名称private static final String DEAD_KEY = "lisi";       //死信队列 RoutingKeyprivate static final String DEAD_QUEUE_NAME = "dead-queue";    //死信队列名称public static void main(String[] args) throws IOException {Connection connection = RabbitUtils.getConnection();Channel channel = connection.createChannel();/*声明队列和普通交换机并进行绑定,由于消费者C1已经声明过了,这里实际可以省略这三行代码*/channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);channel.queueDeclare(DEAD_QUEUE_NAME,false,false,false,null);channel.queueBind(DEAD_QUEUE_NAME,DEAD_EXCHANGE_NAME,DEAD_KEY);DeliverCallback successBack = (consumerTag, message) -> {System.out.println("C2用户接收到的信息为:"+new String(message.getBody()));};CancelCallback cnaelBack = a->{System.out.println("C2用户进行取消消费操作!");};channel.basicConsume(DEAD_QUEUE_NAME,true,successBack,cnaelBack);}}

效果演示:
  1. 执行消费者C1,创建出所有交换机和队列绑定后停止运行。
  2. 执行生产者,等待10秒钟后查看控制台,消息全部通过交换机进入死信队列
  3. 运行消费者C2,死信队列消息被成功消费

20秒后消息全部进入死信队列
运行消费者C2,死信队列内的消息被全部取出


队列消息积压达到最大长度

在绑定死信队列的消费者端添加队列的消息积压长度限制即可,核心代码为TTL案例的消费者C1基础上再添加Map参数

//指定队列能够积压消息的大小,超出该范围的消息将进入死信队列
arguments.put("x-max-length",6);            

生产者:

package com.dmbjz.maxlength;import com.dmbjz.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;import java.nio.charset.StandardCharsets;/* 死信队列 队列达到最大长度案例 生产者 */
public class Provider {private static final String EXCHANGE_NAME = "normal_exchange";              //正常交换机名称private static final String KEY = "zhangsan";        //普通队列 RoutingKeypublic static void main(String[] args) throws Exception {Connection connection = RabbitUtils.getConnection();Channel channel = connection.createChannel();channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.DIRECT);  //声明交换机/*循环消息发送*/for(int i = 1; i < 11; i++) {String message = "INFO " + i;channel.basicPublish(EXCHANGE_NAME,KEY,null,message.getBytes(StandardCharsets.UTF_8));  //发送超级VIP消息}}}

消费者C1:
消费者C2的代码与TTL案例中保持一致

package com.dmbjz.maxlength;import com.dmbjz.utils.RabbitUtils;
import com.rabbitmq.client.*;import java.io.IOException;
import java.util.HashMap;
import java.util.Map;/* 死信队列 队列达到最大长度案例 消费者C1 */
public class ConsumerC1 {private static final String EXCHANGE_NAME = "normal_exchange";              //正常交换机名称private static final String DEAD_EXCHANGE_NAME = "dead_exchange";           //死信队列交换机名称private static final String KEY = "zhangsan";        //普通队列 RoutingKeyprivate static final String DEAD_KEY = "lisi";       //死信队列 RoutingKeyprivate static final String QUEUE_NAME = "normal-queue";       //普通队列名称private static final String DEAD_QUEUE_NAME = "dead-queue";    //死信队列名称public static void main(String[] args) throws IOException {Connection connection = RabbitUtils.getConnection();Channel channel = connection.createChannel();/*声明死信和普通交换机,正常交换机已被生产者声明,实际可以省略第一行代码*/channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);/*创建队列* 通过额外参数实现什么情况下转发到死信队列 ?,key都是固定的*   1、TTL过期时间设置(一般由生产者指定)*   2、死信交换机的名称*   3、死信交换机的RoutingKey* */Map<String,Object> arguments = new HashMap<>(8);arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);     //死信交换机的名称arguments.put("x-dead-letter-routing-key",DEAD_KEY);            //死信交换机的RoutingKeyarguments.put("x-max-length",6);            //指定正常队列的长度,超出该范围的消息将进入死信队列channel.queueDeclare(QUEUE_NAME,false,false,false,arguments);channel.queueDeclare(DEAD_QUEUE_NAME,false,false,false,null);/*绑定队列*/channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,KEY);channel.queueBind(DEAD_QUEUE_NAME,DEAD_EXCHANGE_NAME,DEAD_KEY);DeliverCallback successBack = (consumerTag, message) -> {System.out.println("C1用户接收到的信息为:"+new String(message.getBody()));};CancelCallback cnaelBack = a->{System.out.println("C1用户进行取消消费操作!");};channel.basicConsume(QUEUE_NAME,true,successBack,cnaelBack);}
}

效果演示:
  1. 执行消费者C1,创建出所有交换机和队列绑定后停止运行。
  2. 执行生产者,由于通道积压的六条消息从未被消费。剩余消息进入死信队列
  3. 运行消费者C2,死信队列消息被成功消费

消息积压六条,剩下四条消息进入死信队列,LIM为限制长度标记
启动消费者C2,死信队列消息被消费


消息拒绝应答

在绑定死信队列的消费者端添加需要拒绝应答的消息判断即可,核心代码为消息拒绝应答

/* requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中,未配置则进行丢弃操作*/
channel.basicReject(message.getEnvelope().getDeliveryTag(),false);

生产者:

package com.dmbjz.noack;import com.dmbjz.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;import java.nio.charset.StandardCharsets;/* 死信队列 队列达到最大长度案例 生产者 */
public class Provider {private static final String EXCHANGE_NAME = "normal_exchange";              //正常交换机名称private static final String KEY = "zhangsan";        //普通队列 RoutingKeypublic static void main(String[] args) throws Exception {Connection connection = RabbitUtils.getConnection();Channel channel = connection.createChannel();channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.DIRECT);  //声明交换机/*循环消息发送*/for(int i = 1; i < 11; i++) {String message = "INFO " + i;channel.basicPublish(EXCHANGE_NAME,KEY,null,message.getBytes(StandardCharsets.UTF_8));  //发送超级VIP消息}}
}

消费者C1:
消费者C2的代码和TTL案例保持一致

package com.dmbjz.noack;import com.dmbjz.utils.RabbitUtils;
import com.rabbitmq.client.*;import java.io.IOException;
import java.util.HashMap;
import java.util.Map;/* 死信队列 队列达到最大长度案例 消费者C1 */
public class ConsumerC1 {private static final String EXCHANGE_NAME = "normal_exchange";              //正常交换机名称private static final String DEAD_EXCHANGE_NAME = "dead_exchange";           //死信队列交换机名称private static final String KEY = "zhangsan";        //普通队列 RoutingKeyprivate static final String DEAD_KEY = "lisi";       //死信队列 RoutingKeyprivate static final String QUEUE_NAME = "normal-queue";       //普通队列名称private static final String DEAD_QUEUE_NAME = "dead-queue";    //死信队列名称public static void main(String[] args) throws IOException {Connection connection = RabbitUtils.getConnection();Channel channel = connection.createChannel();/*声明死信和普通交换机,正常交换机已被生产者声明,实际可以省略第一行代码*/channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);/*创建队列* 通过额外参数实现什么情况下转发到死信队列 ?,key都是固定的*   1、TTL过期时间设置(一般由生产者指定)*   2、死信交换机的名称*   3、死信交换机的RoutingKey* */Map<String,Object> arguments = new HashMap<>(8);arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);     //死信交换机的名称arguments.put("x-dead-letter-routing-key",DEAD_KEY);            //死信交换机的RoutingKeychannel.queueDeclare(QUEUE_NAME,false,false,false,arguments);channel.queueDeclare(DEAD_QUEUE_NAME,false,false,false,null);/*绑定队列*/channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,KEY);channel.queueBind(DEAD_QUEUE_NAME,DEAD_EXCHANGE_NAME,DEAD_KEY);DeliverCallback successBack = (consumerTag, message) -> {String info = new String(message.getBody(),"UTF-8");if(info.equals("INFO 5")){System.out.println("C1用户拒绝的信息为:"+new String(message.getBody()));/* requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中,未配置则进行丢弃操作*/channel.basicReject(message.getEnvelope().getDeliveryTag(),false);}else{System.out.println("C1用户接收到的信息为:"+new String(message.getBody()));channel.basicAck(message.getEnvelope().getDeliveryTag(),false);}};CancelCallback cnaelBack = a->{System.out.println("C1用户进行取消消费操作!");};channel.basicConsume(QUEUE_NAME,false,successBack,cnaelBack);}
}

效果演示:
  1. 执行消费者C1
  2. 执行生产者,等待10秒钟后查看控制台,消息5被拒绝接收进入死信队列
  3. 运行消费者C2,死信队列消息被成功消费

消息5被消费者C1拒绝接收
死信队列中的消息5被消费者C2消费

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

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

相关文章

Springboot集成支付宝支付---完整详细步骤

网页操作步骤 1.进入支付宝开发平台—沙箱环境 使用开发者账号登录开放平台控制平台 2.点击沙箱进入沙箱环境 说明&#xff1a;沙箱环境支持的产品&#xff0c;可以在沙箱控制台 沙箱应用 > 产品列表 中查看。 3.进入沙箱&#xff0c;配置接口加签方式 在沙箱进行调试前…

Python (十) operator

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一波电子书籍资料&#xff0c;包含《Effective Java中文版 第2版》《深入JAVA虚拟机》&#xff0c;《重构改善既有代码设计》&#xff0c;《MySQL高性能-第3版》&…

找不到vcomp100.dll,无法继续执行代码怎么解决

在计算机编程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“找不到vcomp100.dll&#xff0c;无法继续执行代码”。这个错误通常出现在使用Visual Studio进行C开发时&#xff0c;它表示程序无法找到vcomp100.dll文件。vcomp100.dll是Visual C 2015 Redist…

认识产品经理以及Axure简单安装与入门

目录 一.认识产品经理 1.1.项目团队 1.2.概述 1.3.认识产品经理 1.4.产品经理工作范围 1.5.产品经理工作流程 1.6.产品经理的职责 1.7.产品经理的分类 1.8.产品经理能力要求 1.9.产品工具 1.10.产品体验报告 二.Axure简介 三.应用场景 四.安装与汉化 4.1.安装 4…

指纹浏览器有什么用?AdsPower 指纹浏览器都有哪些优势?

说到指纹浏览器&#xff0c;各位跨境卖家肯定都不陌生&#xff0c;指纹浏览器已经成为跨境电商不可或缺的有力工具&#xff0c;那么它具体有什么作用呢&#xff1f;如今市场上指纹浏览器品牌琳琅满目&#xff0c;东哥有没有什么推荐呢&#xff1f;在这里&#xff0c;东哥将为大…

matter模组有无源测试事例

测试一款matter模组的硬件性能 1.1 天线阻抗、电压驻波比测试 主要测试&#xff1a;PCB板载天线设计效率及板材PCB铜面的平整度等 1.2 模组有源数据测试 主要测试&#xff1a;模组的阻抗匹配、频偏等情况 1.3 模组传输能量精度 主要测试&#xff1a;矢量误差等数据 1.4 模…

Unity2023.3(Unity6)版本开始将可以发布WebGPU

翻译一段官网上的话&#xff1a; 利用Unity 2023.3(正式发布时应该称为Unity6)中最新的WebGPU图形API集成&#xff0c;尝试最大限度的提升您的网络游戏的真实感。 通过与谷歌的战略合作&#xff0c;Unity实时3D平台的强大的图形功能现在为图形丰富的网络游戏进行微调&#xff0…

提升英语学习效率,尽在Eudic欧路词典 for Mac

Eudic欧路词典 for Mac是一款专为英语学习者打造的强大工具。无论您是初学者还是高级学习者&#xff0c;这款词典都能满足您的需求。 首先&#xff0c;Eudic欧路词典 for Mac具备丰富的词库&#xff0c;涵盖了各个领域的单词和释义。您可以轻松查询并学习单词的意思、用法和例…

猫粮哪个牌子好?盘点十大主食冻干猫粮品牌排行榜!

近年来&#xff0c;冻干猫粮作为备受追捧的高品质猫粮&#xff0c;吸引了越来越多养猫人的关注。新手养猫就弄不明白了&#xff0c;什么是冻干猫粮呢&#xff1f;冻干猫粮可以作为日常主食一直喂吗&#xff1f; 作为一位6年养猫人&#xff0c;我会用最简单的文字告诉你主食冻干…

安装NLTK Data

文章目录 NLTK离线安装1. 获取安装包2. 放置nltk_data文件3. Demo4. 参考链接 关注公众号&#xff1a;『AI学习星球』 算法学习、4对1辅导、论文辅导或核心期刊可以通过公众号或CSDN滴滴我 nltk库是python语言为自然语言处理提供的一个功能强大&#xff0c;简单易用的函数库&a…

Todesk、向日葵等访问“无显示器”主机黑屏问题解决

我的环境是 ubuntu 22.04 安装 要安装 video dummy&#xff0c;请在终端中运行以下命令&#xff1a; sudo apt install xserver-xorg-video-dummy配置 video dummy 的配置文件请自行搜索 使用任何文本编辑器打开此文件。 我的是 /etc/X11/xorg.conf 默认配置文件包含以下内…

1.7 实战:Postman请求Post接口-登录

上一小节我们实战了使用Postman请求Get接口。本小节我们来使用Postman请求Post接口。 我们来测试一下登录,之前已经创建好了Collections。我们选择登录页下的登录这个请求。地址也是跟之前一样,我们打开校园二手交易系统,打开浏览器开发者工具,输入用户名和密码,点击登录…

力扣22. 括号生成(java 回溯法)

Problem: 22. 括号生成 文章目录 题目描述思路解题方法复杂度Code 题目描述 思路 我们首先要知道&#xff0c;若想生成正确的括号我们需要让右括号去适配左括号&#xff0c;在此基础上我们利用回溯去解决此题目 1.题目给定n个括号&#xff0c;即当回溯决策路径长度等于 2 n 2n…

网络基础2

三层交换机&#xff1a;路由器交换机 创建vlan 配置0/0/2串口为vlan2&#xff0c;3接口为vlan3 三层交换机的串口是不能直接配置地址&#xff0c;要在虚拟接口&#xff08;vlan的接口&#xff09;配置IP地址 配置vlan1的虚拟接口 此时vlan1的主机能ping通三层交换机串口1的地址…

西南交通大学【数电实验7---按键防抖动设计】

实验电路图、状态图、程序代码、仿真代码、仿真波形图&#xff08;可以只写出核心功能代码&#xff0c;代码要有注释&#xff09; 一共四个状态&#xff1a;1.未按下时空闲状态 2.按下抖动滤除状态 3.按下稳定状态 4.释放抖动滤除状态 在第一个状态时&#xff0c;等待按键按下&…

【jitterbuffer】3:VCMJitterEstimator及所需的概率知识:期望、方差、协方差

期望 : 全国的平均积雪深度 期望值为负 概率就是 不同国家的面积了,总面积是1 期望计算公式 某种函数的期望 K的求和范围 计算期望 1

Linux(22):X Window 设定介绍

X Window System X Window System 是个非常大的架构&#xff0c;他还用到网络功能。也就是说&#xff0c;其实 X 窗口系统是能够跨网络与跨操作系统平台的。 X Window系统最早是由 MIT (Massachusetts Institute of Technology&#xff0c;麻省理工学院) 在1984年发展出来的&…

day13 栈与队列(三)

day13 2023.12.11 代码随想录 今天刚出差回来&#xff0c;拉下了很多天的博客&#xff0c;慢慢补吧&#xff0c;每天做当天的任务&#xff0c;再补一篇博客。 1. 239滑动窗口最大值 本题就是每次窗口内容放在一个单调队列中&#xff0c;那么每次直接返回队头元素&#xff08;最…

Python中的继承:概念、用法与示例

目录 一、引言 二、继承的概念 三、继承的用法 1、继承父类的属性和方法 2、添加新的属性和方法 3、覆盖父类的方法 四、示例代码展示 五、继承中的多态性 六、继承中的封装和抽象 七、继承中的多重继承 总结 一、引言 面向对象编程&#xff08;OOP&#xff09;是一…

「完美世界」石昊被诓入至尊道场,修炼无敌道,打跑天仙书院弟子

Hello,小伙伴们&#xff0c;我是拾荒君。 《完美世界》这部国漫&#xff0c;在粉丝的翘首期盼中&#xff0c;终于迎来了第141集的更新。这一集的内容&#xff0c;对于喜欢石昊和至尊道场劫难的观众来说&#xff0c;可谓是扣人心弦&#xff0c;让人目不转睛。 在这一集中&#…