文章目录
1. Rabbitmq的安装
本例以centos7为例,还可以使用其他云服务器,如果使用云服务器,记得开放15672端口
# 1:安装rabbitmq所需要的依赖包
yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc- c++ kernel-devel m4 ncurses-devel tk tc xz
# 2:下载安装包(选择一个文件夹下载 cd /tuling/rebbitMQ)也可使用tfp上传
wget www.rabbitmq.com/releases/erlang/erlang-18.3-1.el7.centos.x86_64.rpm
wget http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm
wget www.rabbitmq.com/releases/rabbitmq-server/v3.6.5/rabbitmq-server-3.6.5-1.noarch.rpm
下载结果如下:
erlang18.31.el7.centos.x86_64.rpm
socat1.7.3.25.el7.lux.x86_64.rpm
rabbitmqserver3.6.51.noarch.rpm
# 3. 安装rabbitMQ相关服务
第一步:安装erlang语言环境
rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm
第二步:安装socat加解密软件
rpm -ivh socat-1.7.3.2-1.1.el7.x86_64.rpm
第三步:最后安装rabbitmq
rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm
# 4. 执行管控台插件:(不然不能在浏览器登录)
rabbitmq-plugins enable rabbitmq_management
# 5. 修改默认配置信息
# 比如修改密码、配置等等,例如:loopback_users 中的 <<"guest">>,只保 留guest
vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
修改 本机系统文件
a:修改 vim /etc/rabbitmq/rabbitmq-env.conf 添加: NODENAME=rabbit
b:修改 vim /etc/hostname 添加: zhb
c:修改 vim /etc/hosts 添加: 192.168.100.100 zhb hostname和地址映射
6. 启动
service rabbitmq-server start # 启动服务
service rabbitmq-server stop # 停止服务
service rabbitmq-server restart # 重启服务
7. 访问地址
http://ip地址:15672
通过检查端口 lsof -i:5672、 ps -ef|grep rabbitmq 查看是否启动成功
访问时使用其自带的guest/guest的用户名和密码,进入rabblitMq界面后,如果需要创建自定义用户,在admin下配置 users 和 Virtual Hosts,其中 Virtual Hosts就相当于mysql的各种库的概念,并且可以指定用户对库和表等操作的权限。如下所示:
2. Rabbitmq的基本概念
常见的MQ产品如下:
单机吞吐量对比
- rabbiutMQ:5w
- rocketMQ:12w
- Kafka:17w
综合上面的材料得出以下两点:
- 中小型软件公司,建议选RabbitMQ.一方面,erlang语言天生具备高并发的特性,而且他的管理界面用起来十分方便。正所谓,成也萧何,败也萧何!他的弊端也在这里,虽然RabbitMQ是开源的,然而国内有几个能定制化开发erlang的程序员呢?所幸,RabbitMQ的社区十分活跃,可以解决开发过程中遇到的bug,这点对于中小型公司来说十分重要。不考虑rocketmq和kafka的原因是,一方面中小型软件公司不如互联网公司,数据量没那么大,选消息中间件,应首选功能比较完备的,所以kafka排除。不考虑rocketmq的原因是,rocketmq是阿里出品,如果阿里放弃维护rocketmq,中小型公司一般抽不出人来进行rocketmq的定制化开发,因此不推荐。
- 大型软件公司,根据具体使用在rocketMq和kafka之间二选一。一方面,大型软件公司,具备足够的资金搭建分布式环境,也具备足够大的数据量。针对rocketMQ,大型软件公司也可以抽出人手对rocketMQ进行定制化开发,毕竟国内有能力改JAVA源码的人,还是相当多的。至于kafka,根据业务场景选择,如果有日志采集功能,肯定是首选kafka了。具体该选哪个,看使用场景。
本文将介绍rabbitMq的使用,RabbitMQ 采用 Erlang 语言开发。Erlang 语言由 Ericson 设计,专门为开发高并发和分布式系统的一种语言。RabbitMQ 基础架构如下图
- Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker
- Virtual host:虚拟分组,操作隔离,每个项目连接一个虚拟机。不同用户可在自己的虚拟分组中创建 exchange/queue 等,类似于Rabbitmq中的Topic主题,一个项目一个
- Connection:publisher/consumer 和 broker 之间的 TCP 连接
- Channel:Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel 进行通讯,不同channel之间完全隔离,Channel 作为轻量级的 Connection 极大减少了操作系统建立TCP connection 的开销
- Exchange:交换机,存在虚拟机中,虚拟机中不可有重复的交换机。message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去
- Queue:消息最终被送到这里等待 consumer 取走
- Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 Exchange 中的查询表中,用于 message 的分发依据。类似于rocketMQ中的Tags
3. RabbitMQ的工作模式
RabbitMQ官方提供了6中工作模式,第六种RPC远程调用模式,严格意义上不太算MQ,所以在这里不做介绍。
导入依赖
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
</dependencies>
创建获取连接的工具类
public class RabbitUtils {
// 初始化 publisher/consumer与RabbitMq的连接
private static ConnectionFactory connectionFactory = new ConnectionFactory();
static {
connectionFactory.setHost("192.168.100.100"); //RabbitMQ的ip地址
connectionFactory.setPort(15672); //15672是RabbitMQ的默认端口号
connectionFactory.setUsername("zhb"); //用户名
connectionFactory.setPassword("zhb"); //密码
connectionFactory.setVirtualHost("/test_Virtual"); //操作的虚拟机
}
//获取连接
public static Connection getConnection(){
Connection conn = null;
try {
conn = connectionFactory.newConnection();
return conn;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
3.1 “Hello World!” 简单模式
"Hello World!"工作模式也是最简单的工作模式,使用默认的交换机,适用于普通的A生产-B消费,如下图
生产者代码
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
//获取TCP长连接
Connection conn = RabbitUtils.getConnection();
//创建通信“通道”,相当于TCP中的虚拟连接
Channel channel = conn.createChannel();
//创建队列,声明并创建一个队列,如果队列已存在,则使用这个队列
//第一个参数:队列名称ID
//第二个参数:是否持久化,false对应不持久化数据,MQ停掉数据就会丢失
//第三个参数:是否队列私有化,false则代表所有消费者都可以访问,true代表只有第一次拥有它的消费者才能一直使用,其他消费者不让访问
//第四个:是否自动删除,false代表连接停掉后不自动删除掉这个队列
//其他额外的参数, null
channel.queueDeclare(RabbitConstant.QUEUE_HELLOWORLD,false, false, false, null);
String message = "hello白起666";
//四个参数
//exchange 交换机,暂时用不到,在后面进行发布订阅时才会用到
//队列名称
//额外的设置属性
//最后一个参数是要传递的消息字节数组
channel.basicPublish("", RabbitConstant.QUEUE_HELLOWORLD, null,message.getBytes());
channel.close();
conn.close();
System.out.println("===发送成功===");
}
}
消费者代码
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
//获取TCP长连接
Connection conn = RabbitUtils.getConnection();
//创建通信“通道”,相当于TCP中的虚拟连接
Channel channel = conn.createChannel();
//创建队列,声明并创建一个队列,如果队列已存在,则使用这个队列
//第一个参数:队列名称ID
//第二个参数:是否持久化,false对应不持久化数据,MQ停掉数据就会丢失
//第三个参数:是否队列私有化,false则代表所有消费者都可以访问,true代表只有第一次拥有它的消费者才能一直使用,其他消费者不让访问
//第四个:是否自动删除,false代表连接停掉后不自动删除掉这个队列
//其他额外的参数, null
channel.queueDeclare(RabbitConstant.QUEUE_HELLOWORLD,false, false, false, null);
//从MQ服务器中获取数据
//创建一个消息消费者
//第一个参数:队列名
//第二个参数代表是否自动确认收到消息,false代表手动编程来确认消息,这是MQ的推荐做法
//第三个参数要传入DefaultConsumer的实现类
channel.basicConsume(RabbitConstant.QUEUE_HELLOWORLD, false, new Reciver(channel));
channel.basicConsume(RabbitConstant.QUEUE_HELLOWORLD , false , new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("消费者接收到的消息:"+message);
System.out.println("消息的TagId:"+envelope.getDeliveryTag());
//false只确认签收当前的消息,设置为true的时候则代表签收该消费者所有未签收的消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
}
3.2 Work queues 工作队列
Work queues
:工作队列模式,也使用默认的交换机,但在"Hello World!"工作模式的基础上增加了多个消费端,性能较高,因为它相对于下面的两种交换机不需要字符串匹配,比如12306订票成功发送短信通知,步骤如下
- ①:生产者把100个短信任务发到RabbitMQ中,就不管了,继续处理其他任务
- ②:由多个消费者从RabbitMQ队列中取任务,给用户发送短信。
生产者代码
public class OrderSystem {
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接
Connection connection = RabbitUtils.getConnection();
//创建Channel
Channel channel = connection.createChannel();
//声明一个队列
channel.queueDeclare(RabbitConstant.QUEUE_SMS, false, false, false, null);
//发送100个任务
for(int i = 1 ; i <= 100 ; i++) {
SMS sms = new SMS("乘客" + i, "13900000" + i, "您的车票已预订成功");
String jsonSMS = new Gson().toJson(sms);
//发任务
channel.basicPublish("" , RabbitConstant.QUEUE_SMS , null , jsonSMS.getBytes());
}
System.out.println("发送数据成功");
channel.close();
connection.close();
}
}
消费者
public class SMSSender1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitUtils.getConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare(RabbitConstant.QUEUE_SMS, false, false, false, null);
//如果不写basicQos(1),则自动MQ会将所有请求平均发送给所有消费者
//basicQos,MQ不再对消费者一次发送多个请求,而是消费者处理完一个消息后(确认后),在从队列中获取一个新的
channel.basicQos(1);//处理完一个取一个
channel.basicConsume(RabbitConstant.QUEUE_SMS , false , new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String jsonSMS = new String(body);
System.out.println("SMSSender1-短信发送成功:" + jsonSMS);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
channel.basicAck(envelope.getDeliveryTag() , false);
}
});
}
}
注意:如果不设置 channel.basicQos(1),则100个任务会均分到3个消费上,如果某个消费者网络不好或其他原因,消费的比较慢,那系统整体速度则会变慢。所以推荐使用channel.basicQos(1),目的是使MQ不再对消费者一次发送多个请求,而是消费者处理完一个消息后(确认后),在从队列中获取一个新的,这样消费快的多消费点,慢的少消费点,可有效提升系统性能!
SMSSender2、SMSSender3代码和SMSSender1一样,不过改了名字,代码省略。
3.3 Publish/Subscribe 发布/订阅
在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:
- P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
- C:消费者,消息的接收者,会一直等待消息到来
- Queue:消息队列,接收消息、缓存消息
- Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型
Exchange有常见以下3种类型:
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合指定routing key 的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
场景:气象局把天气信息放入交换机,新浪、百度、搜狐等通过各自的队列与交换机绑定,就可以获取到交换机中的天气数据!
交换机各参数如下:
/**
* 声明一个交换机
* exchange:交换机的名称
* type:交换机的类型 常见的有direct,fanout,topic等
* durable:设置是否持久化。durable设置为true时表示持久化,反之非持久化.持久化可以将交换器存入磁盘,在服务器重启的时候不会丢失相关信息
* autodelete:设置是否自动删除。autoDelete设置为true时,则表示自动删除。
* 自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后,所有与这个交换器绑定的队列或者交换器都与此解绑。
* 不能错误的理解—当与此交换器连接的客户端都断开连接时,RabbitMq会自动删除本交换机
* arguments:其它一些结构化的参数,比如:alternate-exchange
*/
channel.exchangeDeclare(exchangeName,exchangeType,true,false,null);
注意:Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!
发布订阅模式与工作队列模式的区别如下:
- 工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机
- 发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)
- 发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑定到默认的交换机
3.4 Routing 路由模式
图解:
- P:生产者,向 Exchange 发送消息,发送消息时,会指定一个routing key
- X:Exchange(交换机),接收生产者的消息,然后把消息递交给与 routing key 完全匹配的队列
- C1:消费者,其所在队列指定了需要 routing key 为 error 的消息
- C2:消费者,其所在队列指定了需要 routing key 为 info、error、warning 的消息
说明:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key)
- 消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey
- Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的Routingkey
与消息的 Routing key 完全一致,才会接收到消息
代码示例
生产者发消息:
//第一个参数交换机名字 第二个参数作为 消息的routing key
//第三个消息属性 第四个消息体
channel.basicPublish(“my_exchange”,"routing.key.A", null , me.getValue().getBytes());
交换机在与队列绑定关系时要加上routing key
//指定队列与交换机以及routing key之间的关系
//三个参数分别是:队列名、交换机、routing key
channel.queueBind(“queue1”, “my_exchange”, "routing.key.A"); //只有这个会受到生产者发来的消息
channel.queueBind(“queue1”, “my_exchange”, "routing.key.B");//routing key不匹配,收不到消息
3.5 Topics 主题模式
模式说明
- Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型Exchange 可以让队列在绑定 Routing key 的时候使用通配符!
- Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
- 通配符规则:# 匹配一个或多个词,* 只匹配一个词
例如:
item.# 能够匹配 item.insert.abc 或者item.insert
item.* 只能匹配 item.insert
Topic 主题模式可以实现 Pub/Sub 发布与订阅模式和 Routing 路由模式的功能,只是 Topic 在配置routing key 的时候可以使用通配符,显得更加灵活
4. springboot整合RabbitMQ
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
①:生产者发送消息
1. ymal 配置
spring:
rabbitmq:
host: 192.168.100.100
port: 5672
virtual-host: tuling
username: zhb
password: zhb
publisher-confirms: true //开启confirm确认机制(是否到交换机)
publisher-returns: true //开启returns确认机制(是否到队列)
template:
mandatory: true //指定消息在没有被队列接收时是否强行退回(true)还是直接丢弃(false),如果需要return模式,则必须设置为true
connection-timeout: 1000000
2. 配置交换机、队列、绑定关系
@Configuration
public class RabbitmqConfig {
//交换机
@Bean
public DirectExchange orderToProductExchange() {
DirectExchange directExchange = new DirectExchange(MqConst.ORDER_TO_PRODUCT_EXCHANGE_NAME,true,false);
return directExchange;
}
//队列
@Bean
public Queue orderToProductQueue() {
Queue queue = new Queue(MqConst.ORDER_TO_PRODUCT_QUEUE_NAME,true,false,false);
return queue;
}
//绑定关系
@Bean
public Binding orderToProductBinding() {
return BindingBuilder.bind(orderToProductQueue()).to(orderToProductExchange()).with(MqConst.ORDER_TO_PRODUCT_ROUTING_KEY);
}
}
3. 消息发送及参数回调
注意MsgSender 类实现了 InitializingBean。 在MsgSender初始化完毕后,会调用 afterPropertiesSet 方法,完成mq服务器的参数回调配置
rabbitmq 整个消息投递的路径为:
producer
—>rabbitmq broker
—>exchange
—>queue
—>consumer
- 消息从 producer 到 exchange 则会返回一个
confirmCallback
。 - 消息从 exchange–>queue 投递失败则会返回一个
returnCallback
。
我们将利用这两个 callback 控制消息的可靠性投递,但是注意,使用这两种回调时,需要在spring boot
生产端配置:
- 开启confirm确认机制(是否到交换机)
spring.rabbitmq.publisher-confirms
:true
: 开启confirm
确认机制
- 开启return确认机制(是否到队列)
spring.rabbitmq.publisher-returns
:true
:开启return
确认机制spring.rabbitmq.template.mandatory
:true
:指定消息在没有被队列接收时是否强行退回(true
)还是直接丢弃(false
),return
回调特有的配置!
@Component
@Slf4j
public class MsgSender implements InitializingBean {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private TulingMsgComfirm tulingMsgComfirm;
@Autowired
private TulingMsgRetrunListener tulingMsgRetrunListener;
/**
* 方法实现说明:真正的发送消息
* @param msgTxtBo:发送的消息对象
* @return:
* @exception:
*/
public void senderMsg(MsgTxtBo msgTxtBo){
log.info("发送的消息ID:{}",msgTxtBo.getMsgId());
//mq确认的服务端id
CorrelationData correlationData = new CorrelationData(msgTxtBo.getMsgId());
rabbitTemplate.convertAndSend(MqConst.ORDER_TO_PRODUCT_EXCHANGE_NAME,MqConst.ORDER_TO_PRODUCT_ROUTING_KEY,msgTxtBo,correlationData);
}
//项目启动时,添加 confirm 和 return 回调
// confirm回调:消息到达 交换机 exchange时的回调
// return 回调:消息从交换机 到达队列时的回调
@Override
public void afterPropertiesSet() throws Exception {
//confirm回调
rabbitTemplate.setConfirmCallback(tulingMsgComfirm);
//return回调
rabbitTemplate.setReturnCallback(tulingMsgRetrunListener);
//设置消息转换器
Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
rabbitTemplate.setMessageConverter(jackson2JsonMessageConverter);
}
}
confirm 回调的实现
@Component
@Slf4j
public class TulingMsgComfirm implements RabbitTemplate.ConfirmCallback{
@Autowired
private MsgContentMapper msgContentMapper;
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String msgId = correlationData.getId();
if(ack) {
log.info("消息Id:{}对应的消息被broker签收成功",msgId);
updateMsgStatusWithAck(msgId);
}else{
log.warn("消息Id:{}对应的消息被broker签收失败:{}",msgId,cause);
updateMsgStatusWithNack(msgId,cause);
}
}
return 回调的实现
@Component
@Slf4j
public class TulingMsgRetrunListener implements RabbitTemplate.ReturnCallback {
@Autowired
private MsgContentMapper msgContentMapper;
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
try{
ObjectMapper objectMapper = new ObjectMapper();
MsgTxtBo msgTxtBo = objectMapper.readValue(message.getBody(),MsgTxtBo.class);
log.info("无法路由消息内容:{},cause:{}",msgTxtBo,replyText);
。。。。。
}catch (Exception e) {
log.error("更新消息表异常:{}",e);
}
}
}
②:消费端监听消息
1. ymal 配置
spring:
rabbitmq:
host: 192.168.100.100
port: 5672
virtual-host: tuling
username: zhb
password: zhb
listener:
simple:
concurrency: 5
max-concurrency: 10
acknowledge-mode: manual //消费者消费完毕,手动返回ack给borker
prefetch: 1
default-requeue-rejected: false
2. 消费者监听消息
消费端收到消息后有三种确认方式:
- 自动确认:acknowledge="
none
" - 手动确认:acknowledge="
manual
" - 根据异常情况确认:acknowledge="
auto
"
其中自动确认是指,当消息一旦被Consumer
接收到,则自动确认收到,并将相应 message
从 RabbitMQ
的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失,所以一般情况下会使用 手动确认:acknowledge="manual"
如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck()
,手动签收,如果出现异常,则调用channel.basicNack()
方法,让其自动重新发送消息。
@RabbitListener(queues = {ORDER_TO_PRODUCT_QUEUE_NAME})
@RabbitHandler
public void consumerMsgWithLock(Message message, Channel channel) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
//获取消息体
MsgTxtBo msgTxtBo = objectMapper.readValue(message.getBody(), MsgTxtBo.class);
//获取ack标记
Long deliveryTag = (Long) message.getMessageProperties().getDeliveryTag();
if (redisTemplate.opsForValue().setIfAbsent(LOCK_KEY + msgTxtBo.getMsgId(), msgTxtBo.getMsgId())) {
log.info("消费消息:{}", msgTxtBo);
try {
//更新消息表也业务表
productService.updateProductStore(msgTxtBo);
// 模拟消息签收异常
// System.out.println(1/0);
//消费成功的ack
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
/**
* 更新数据库异常说明业务没有操作成功需要删除分布式锁
*/
if (e instanceof BizExp) {
BizExp bizExp = (BizExp) e;
log.info("数据业务异常:{},即将删除分布式锁", bizExp.getErrMsg());
//删除分布式锁
redisTemplate.delete(LOCK_KEY);
}
//更新消息表状态
MessageContent messageContent = new MessageContent();
messageContent.setMsgStatus(MsgStatusEnum.CONSUMER_FAIL.getCode());
messageContent.setUpdateTime(new Date());
messageContent.setErrCause(e.getMessage());
messageContent.setMsgId(msgTxtBo.getMsgId());
msgContentMapper.updateMsgStatus(messageContent);
//消费失败,拒绝消息
channel.basicReject(deliveryTag,false);
}
} else {
log.warn("请不要重复消费消息{}", msgTxtBo);
channel.basicReject(deliveryTag,false);
}
}
}