目录
一、需求
1.1 常见方案
1.2 缺点
二、高效延时消息设计与实现
三、RabbitMQ延时队列
3.1 Per-Queue Message TTL
3.2 Dead Letter Exchanges
3.3 小结
3.4 在队列上设置TTL
3.4.1 建立delay.exchange
3.4.2 建立延时队列(delay queue)
3.4.3 配置延时路由规则
3.4.4 测试
3.5 在消息上设置TTL
3.5.1 设置延时队列
3.5.2 生产者
3.5.3 消费者
3.5.4 测试
四、业务整合
4.1 需求分析
4.2 实现
4.2.1 生产者
4.2.2 发送消息
4.2.3 消费者(生产者)
4.2.4 消费者
4.3 总结
4.4 测试
卖家确认收货后48小时不评价,那么该订单自动评价为好评。
启动一个cron定时任务,每小时跑一次,将完成时间超过48小时的订单取出,置为好评,并把评价状态设置为已评价。
(1)轮询效率比较低
(2)每次扫描数据库,已经被执行过的记录任然会被扫描,导致重复计算
(3)时效性不好,可能产生误差
高效延时消息,包含两个重要的数据结构:
1)环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)
(2)任务集合,环上每一个slot是一个Set
同时,启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot。
Task结构中有两个很重要的属性:
(1)Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务
(2)Task-Function:需要执行的任务指针
假设当前Current Index指向第一格,当有延时消息到达之后,例如希望3610秒之后,触发一个延时消息任务,只需:
(1)计算这个Task应该放在哪一个slot,现在指向1,3610秒之后,应该是第11格,所以这个Task应该放在第11个slot的Set
(2)计算这个Task的Cycle-Num,由于环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,所以应该绕3610/3600=1圈之后再执行,于是Cycle-Num=1
Current Index不停的移动,每秒移动到一个新slot,这个slot中对应的Set
(1)如果不是0,说明还需要多移动几圈,将Cycle-Num减1
(2)如果是0,说明马上要执行这个Task了,取出Task-Funciton执行(可以用单独的线程来执行Task),并把这个Task从Set
使用了“延时消息”方案之后,“订单48小时后关闭评价”的需求,只需将在订单关闭时,触发一个48小时之后的延时消息即可:
(1)无需再轮询全部订单,效率高
(2)一个订单,任务只执行一次
(3)时效性好,精确到秒(控制timer移动频率可以控制精度)
在实际的业务中我们会遇见生产者产生的消息,不立即消费,而是延时一段时间在消费,比如说订单超过7天自动收货,超过5天默认评价等。RabbitMQ本身没有直接支持延迟队列功能,但是可以根据其特性Per-Queue Message TTL和 Dead Letter Exchanges实现延时队列。也可以通过改特性设置消息的优先级。
RabbitMQ可以针对消息和队列设置TTL(过期时间)。队列中的消息过期时间(Time To Live, TTL)有两种方法可以设置。第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。第二种方法是对消息进行单独设置,每条消息TTL可以不同。如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就成为dead message,消费者将无法再收到该消息。
当消息在一个队列中变成死信后,它能被重新publish到另一个Exchange。
消息变成Dead Letter有以下几种情况:
channel.basicNack( deliveryTag, multiple, requeue) 一次拒绝0个或多个
参数: deliveryTag:该消息的index
multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息。
requeue:被拒绝的是否重新入队列
channel.basicReject(deliveryTag, requeue); 一次拒绝一个
参数: deliveryTag:该消息的index
requeue:被拒绝的是否重新入队列
实际上就是设置某个队列的属性,当这个队列中有Dead Letter时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange中去,进而被路由到另一个队列,publish可以监听这个队列中消息做相应的处理,这个特性可以弥补RabbitMQ 3.0.0以前支持的immediate参数中的向publish确认的功能。
这里Internal设置为NO,否则将无法接受dead letter,YES表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定。
为了测试方便,设置成延时1分钟~
如上配置延时1min队列(x-message-ttl=60000)
x-max-length:最大积压的消息个数,可以根据自己的实际情况设置,超过限制消息不会丢失,会立即转向delay.exchange进行投递
x-dead-letter-exchange:设置为刚刚配置好的delay.exchange,消息过期后会通过delay.exchange进行投递
这里不需要配置"dead letter routing key"否则会覆盖掉消息发送时携带的routingkey,导致后面无法路由为刚才配置的delay.exchange
需要延时的消息到exchange后先路由到指定的延时队列
1)创建delaysync.exchange通过Routing key将消息路由到延时队列
2.配置delay.exchange 将消息投递到正常的消费队列
生产者
package com.leyou.delayqueue;
import cn.itcast.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.util.Calendar;
/**
* @Author: 98050
* @Time: 2018-12-10 14:15
* @Feature:
*/
public class Producer {
private final static String EXCHANGE_NAME = "delaysync.exchange";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 发布消息
String message = System.currentTimeMillis() +"";
channel.basicPublish(EXCHANGE_NAME,"deal.message" ,null ,message.getBytes());
System.out.println("sent message:" + message + ",date:" + System.currentTimeMillis());
// 关闭通道和连接
channel.close();
connection.close();
}
}
消费者
package com.leyou.delayqueue;
import cn.itcast.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @Author: 98050
* @Time: 2018-12-10 14:26
* @Feature:
*/
public class Consumer {
private static String QUEUE_NAME = "test.queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者] received : " + msg);
Long result = System.currentTimeMillis() - Long.parseLong(msg);
System.out.println("间隔:" + result/1000);
}
};
channel.basicConsume(QUEUE_NAME, true,consumer);
}
}
结果
咦?为毛是59秒?实际相差59416毫秒,应该差不多,完成了延时的功能。
package com.leyou.delayqueue2;
import cn.itcast.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.util.HashMap;
/**
* @Author: 98050
* @Time: 2018-12-10 20:11
* @Feature:
*/
public class Producer {
private static String queue_name = "message_ttl_queue";
public static void main(String[] args) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(queue_name, true, false, false, null);
// 绑定路由
channel.queueBind(queue_name, "amq.direct", "message_ttl_routingKey");
String message = System.currentTimeMillis() + "";
// 设置延时属性
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
// 持久性 non-persistent (1) or persistent (2)
AMQP.BasicProperties properties = builder.expiration("60000").deliveryMode(2).build();
// routingKey =delay_queue 进行转发
channel.basicPublish("", "delay_queue", properties, message.getBytes());
System.out.println("sent message: " + message + ",date:" + System.currentTimeMillis());
// 关闭频道和连接
channel.close();
connection.close();
}
}
package com.leyou.delayqueue2;
import java.io.IOException;
import java.util.HashMap;
import cn.itcast.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.*;
/**
* @Author: 98050
* @Time: 2018-12-10 20:15
* @Feature:
*/
public class Consumer {
private static String queue_name = "message_ttl_queue";
public static void main(String[] args) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(queue_name, true, false, false, null);
// 绑定路由
channel.queueBind(queue_name, "amq.direct", "message_ttl_routingKey");
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者] received : " + msg);
Long result = System.currentTimeMillis() - Long.parseLong(msg);
System.out.println("当前时间:" + System.currentTimeMillis());
System.out.println("间隔:" + result/1000);
}
};
channel.basicConsume(queue_name, true,consumer);
}
}
在发货后,如果用户不确认收货的话,那么在5天后自动确认收货,确认收货完成后,如果用户不进行评价的话,那么4天后自动好评!
流程:
1. 当用户点击“提醒发货”后,首先发送两条延时消息到延时队列中,一条是用来自动确认收货的,另一条是用来自动评价的。
2. 然后当消息达到过期时间(ttl)后,该消息就会被转发到死信交换机(leyou.amq.direct)中。
3. 紧接着消费者通过正常的消息队列(leyou.order.delay.ttl.queue)获取消息。
4. 如果是消息类型1,代表自动确认收货,那么此时改变订单的状态即可。
5. 如果是消息类型2,代表自动评价,那么就需要再次将封装好的消息(订单id,review对象)发送到专门处理评论消息的交换机(leyou.comments.exchange)中。
6. 评论微服务从leyou.comments.queue中拿到消息后,进行消息的发布。
7. 假如用户自己已经确认收货了,那么此时需要给延时队列中发送一条用于自动评价的消息即可。
延迟队列配置:
在订单微服务中,修改订单状态更新接口。OrderServiceImpl中的updateOrderStatus做以下修改:
/**
* 更新订单状态
* @param id
* @param status
* @return
*/
@Override
public Boolean updateOrderStatus(Long id, Integer status) {
UserInfo userInfo = LoginInterceptor.getLoginUser();
Long spuId = this.goodsClient.querySkuById(findSkuIdByOrderId(id)).getSpuId();
OrderStatus orderStatus = new OrderStatus();
orderStatus.setOrderId(id);
orderStatus.setStatus(status);
//延时消息
OrderStatusMessage orderStatusMessage = new OrderStatusMessage(id,userInfo.getId(),userInfo.getUsername(),spuId,1);
OrderStatusMessage orderStatusMessage2 = new OrderStatusMessage(id,userInfo.getId(),userInfo.getUsername(),spuId,2);
//1.根据状态判断要修改的时间
switch (status){
case 2:
//2.付款时间
orderStatus.setPaymentTime(new Date());
break;
case 3:
//3.发货时间
orderStatus.setConsignTime(new Date());
//发送消息到延迟队列,防止用户忘记确认收货
orderStatusService.sendMessage(orderStatusMessage);
orderStatusService.sendMessage(orderStatusMessage2);
break;
case 4:
//4.确认收货,订单结束
orderStatus.setEndTime(new Date());
orderStatusService.sendMessage(orderStatusMessage2);
break;
case 5:
//5.交易失败,订单关闭
orderStatus.setCloseTime(new Date());
break;
case 6:
//6.评价时间
orderStatus.setCommentTime(new Date());
break;
default:
return null;
}
int count = this.orderStatusMapper.updateByPrimaryKeySelective(orderStatus);
return count == 1;
}
当用户点击提醒发货后(传入的status为3),发送两条消息到延时队列。
新建接口OrderStatusService
package com.leyou.order.service;
import com.leyou.order.vo.CommentsParameter;
import com.leyou.order.vo.OrderStatusMessage;
/**
* @Author: 98050
* @Time: 2018-12-10 23:17
* @Feature: 发送延时消息
*/
public interface OrderStatusService {
/**
* 发送消息到延时队列
* @param orderStatusMessage
*/
void sendMessage(OrderStatusMessage orderStatusMessage);
}
业务对象OrderStatusMessage用来封装要发送的消息:
package com.leyou.order.vo;
/**
* @Author: 98050
* @Time: 2018-12-10 23:27
* @Feature:
*/
public class OrderStatusMessage {
/**
* 订单id
*/
private Long orderId;
/**
* 用户id
*/
private Long userId;
private String username;
private Long spuId;
/**
* 消息类型:1(自动确认收货) 2(自动评论)
*/
private int type;
public OrderStatusMessage() {
}
public OrderStatusMessage(Long orderId, Long userId, String username, Long spuId, int type) {
this.orderId = orderId;
this.userId = userId;
this.username = username;
this.spuId = spuId;
this.type = type;
}
}
实现类:
package com.leyou.order.service.impl;
import com.leyou.order.service.OrderStatusService;
import com.leyou.order.vo.CommentsParameter;
import com.leyou.order.vo.OrderStatusMessage;
import com.leyou.utils.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @Author: 98050
* @Time: 2018-12-10 23:24
* @Feature:
*/
@Service
public class OrderStatusServiceImpl implements OrderStatusService {
@Autowired
private AmqpTemplate amqpTemplate;
private static final Logger LOGGER = LoggerFactory.getLogger(OrderStatusServiceImpl.class);
/**
* 发送延时消息到延时队列中
* @param orderStatusMessage
*/
@Override
public void sendMessage(OrderStatusMessage orderStatusMessage) {
String json = JsonUtils.serialize(orderStatusMessage);
MessageProperties properties;
if (orderStatusMessage.getType() == 1){
// 持久性 non-persistent (1) or persistent (2)
properties = MessagePropertiesBuilder.newInstance().setExpiration("60000").setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
}else {
properties = MessagePropertiesBuilder.newInstance().setExpiration("90000").setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
}
Message message = MessageBuilder.withBody(json.getBytes()).andProperties(properties).build();
//发送消息
try {
this.amqpTemplate.convertAndSend("", "leyou.order.delay.queue", message);
}catch (Exception e){
LOGGER.error("延时消息发送异常,订单号为:id:{},用户id为:{}",orderStatusMessage.getOrderId(),orderStatusMessage.getUserId(),e);
}
}
}
根据传入的消息类型,发送不同的延时消息。因为要对消息设置TTL,所以要用到Message类来构造要发送的消息,通过MessageProperties来设置一些属性。注意,这里面发送的消息是字节数组,所以接收的时候需要注意一下。
在订单微服务中增加消息队列监听器UpdateOrderStatusListener
package com.leyou.order.listener;
import com.leyou.comments.pojo.Review;
import com.leyou.order.mapper.OrderStatusMapper;
import com.leyou.order.pojo.OrderStatus;
import com.leyou.order.service.OrderService;
import com.leyou.order.service.OrderStatusService;
import com.leyou.order.vo.CommentsParameter;
import com.leyou.order.vo.OrderStatusMessage;
import com.leyou.utils.JsonUtils;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @Author: 98050
* @Time: 2018-12-10 23:12
* @Feature: 自动修改订单状态:自动确认收货,自动评价
*/
@Component
public class UpdateOrderStatusListener {
@Autowired
private OrderService orderService;
@Autowired
private OrderStatusService orderStatusService;
@Autowired
private OrderStatusMapper orderStatusMapper;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "leyou.order.delay.ttl.queue",durable = "true"), //队列持久化
exchange = @Exchange(
value = "leyou.amq.direct",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.TOPIC
),
key = {"leyou_ttl_orderStatus"}
))
public void listenOrderDelayMessage(byte[] str){
OrderStatusMessage orderStatusMessage = JsonUtils.parse(new String(str), OrderStatusMessage.class);
if (orderStatusMessage == null){
return;
}
int type = orderStatusMessage.getType();
if (type == 1){
//自动确认收货,时间为7天
//1.查询当前订单状态
int status = orderService.queryOrderStatusById(orderStatusMessage.getOrderId()).getStatus();
int nowStatus = 4;
if (status + 1 == nowStatus){
//2.修改订单状态
updateOrderStatusDelay(orderStatusMessage.getOrderId(), nowStatus);
}
}else {
//自动好评,时间为5天
//1.查询当前订单状态
int status = orderService.queryOrderStatusById(orderStatusMessage.getOrderId()).getStatus();
int nowStatus = 6;
if (status + 2 != nowStatus){
return;
}
//2.修改订单状态
updateOrderStatusDelay(orderStatusMessage.getOrderId(), nowStatus);
//3.发送评论消息
CommentsParameter commentsParameter = constructMessage(orderStatusMessage);
this.orderStatusService.sendComments(commentsParameter);
}
}
private CommentsParameter constructMessage(OrderStatusMessage orderStatusMessage) {
Long spuId = orderStatusMessage.getSpuId();
String content = "默认好评";
Long userId = orderStatusMessage.getUserId();
String nickname = orderStatusMessage.getUsername();
List images = new ArrayList<>();
boolean iscomment = true;
String parentId = 0 + "";
boolean isparent = true;
int commentType = 1;
Review review = new Review(orderStatusMessage.getOrderId()+"",spuId+"", content, userId+"", nickname, images, iscomment, parentId,isparent,commentType);
CommentsParameter commentsParameter = new CommentsParameter();
commentsParameter.setOrderId(orderStatusMessage.getOrderId());
commentsParameter.setReview(review);
return commentsParameter;
}
private void updateOrderStatusDelay(Long orderId, int nowStatus) {
OrderStatus orderStatus = new OrderStatus();
orderStatus.setOrderId(orderId);
orderStatus.setStatus(nowStatus);
if (nowStatus == 4){
orderStatus.setEndTime(new Date());
}
if (nowStatus == 6){
orderStatus.setCommentTime(new Date());
}
this.orderStatusMapper.updateByPrimaryKeySelective(orderStatus);
}
}
绑定好交换机和队列后,就可以拿到信息了。注意使用byte数组来获取,然后再转换成字符串,最后再解析JSON。
如果是消息类型1,那么调用方法修改订单状态即可,为了看上去不是很混乱,修改订单状态没有通过OrderService,又重新写了一下(updateOrderStatusDelay方法)。
如果是消息类型2,那么不仅要修改当前订单的状态,还要再小评论消息交换机(leyou.comments.exchange)中发送信息。所以在OrderStatusService中增加sendComments方法,用来发送评价信息。同时要发送的消息封装到CommentsParameter中,同样使用方法constructMessage来构造。
接口
/**
* 发送评论信息
* @param commentsParameter
*/
void sendComments(CommentsParameter commentsParameter);
实现
/**
* 将评论发送到消息队列中
* @param commentsParameter
*/
@Override
public void sendComments(CommentsParameter commentsParameter) {
String json = JsonUtils.serialize(commentsParameter);
try {
this.amqpTemplate.convertAndSend("leyou.comments.exchange","user.comments", json);
}catch (Exception e){
LOGGER.error("评论消息发送异常,订单id:{}",commentsParameter.getOrderId(),e);
}
}
这里面直接发送的就是字符串,所以接收的时候拿字符串接收即可,注意指定routingKey。
业务对象CommentsParameter
package com.leyou.order.vo;
import com.leyou.comments.pojo.Review;
/**
* @Author: 98050
* @Time: 2018-12-12 11:43
* @Feature: 新增评论消息对象
*/
public class CommentsParameter {
private Long orderId;
private Review review;
}
在评论微服务中增加消息队列监听器
package com.leyou.comments.listener;
import com.leyou.comments.dao.CommentDao;
import com.leyou.comments.pojo.Review;
import com.leyou.comments.service.CommentService;
import com.leyou.order.vo.CommentsParameter;
import com.leyou.utils.IdWorker;
import com.leyou.utils.JsonUtils;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @Author: 98050
* @Time: 2018-12-12 11:54
* @Feature:
*/
@Component
public class CommentsListener {
@Autowired
private IdWorker idWorker;
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private CommentDao commentDao;
/**
* 取到消息队列中信息,发布评论
* @param string
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "leyou.comments.queue",durable = "true"), //队列持久化
exchange = @Exchange(
value = "leyou.comments.exchange",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.TOPIC
),
key = {"user.comments"}
))
public void listenCommentsMessage(String string){
CommentsParameter commentsParameter = JsonUtils.parse(string, CommentsParameter.class);
if (commentsParameter == null){
return;
}
Review review = commentsParameter.getReview();
review.set_id(idWorker.nextId() + "");
review.setPublishtime(new Date());
review.setComment(0);
review.setThumbup(0);
review.setVisits(0);
if (review.getParentid() != null && !"".equals(review.getParentid())){
//如果存在上级id,则上级评论数加1,将上级评论的isParent设置为true,浏览量加一
Query query = new Query();
query.addCriteria(Criteria.where("_id").is(review.getParentid()));
Update update = new Update();
update.inc("comment",1);
update.set("isparent",true);
update.inc("visits",1);
this.mongoTemplate.updateFirst(query,update,"review");
}
commentDao.save(review);
}
}
需要做的任务只有一个,将消息中的review对象处理后放入mongodb中。
有两点需要说明一下:
1. 为什么不在订单微服务中通过FeignClient来调用评论微服务,直接发布评论?
首先考虑到如果自动评价的消息比较多时还是使用消息队列更好一点,然后在调用评论微服务的时候需要转发Cookie,因为评论微服务是需要获取用户信息的,所以必须转发Token。
2. 修改订单状态、增加评论为什么不用service中已经有的方法?
因为这两个方法中都会通过拦截器获取Token中的用户信息,所以当延时后再去调用这两个方法的话,用户信息就拿不到了,所以才要重新cv一下。
“购物”过程直接省略,直接到购物车中:
查看数据库
点击“提醒发货”
查询消息队列
两条消息
因为在设置消息的TTL时如下,单位是毫秒,方便测试
等一分半再次查看数据库
正确
查询评论
延时队列的学习到此结束,同时乐优商城也算真正的结束了,不想再加了。。。。。