引入
前不久教大家如何使用docker来搭建RocketMQ环境了,这次就来教搭建如何使用RocketMQ中间件
各大MQ对比及选型
比较常见的MQ有:ActiveMQ、RabbitMQ、RocketMQ、Kafka
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里(apache) | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 1万 | 差 | 高 10W | 非常高 百万级 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 高 | 高 | 一般 |
追求可用性:Kafka、 RocketMQ 、RabbitMQ
追求可靠性:RabbitMQ、RocketMQ
追求吞吐能力:RocketMQ、Kafka
追求消息低延迟:RabbitMQ、Kafka
基于SpringBoot的使用
1.引入依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot</artifactId>
<version>2.2.0</version>
</dependency>
2.Yml文件配置
简单配置:
rocketmq:
producer:
group: erbadagang-producer-group
# RocketMQ name-server地址
name-server: 127.0.0.1:9876
详细配置:
# rocketmq 配置项,对应 RocketMQProperties 配置类
rocketmq:
name-server: 127.0.0.1:9876 # RocketMQ Namesrv
# Producer 配置项
producer:
group: erbadagang-producer-group # 生产者分组
send-message-timeout: 3000 # 发送消息超时时间,单位:毫秒。默认为 3000 。
compress-message-body-threshold: 4096 # 消息压缩阀值,当消息体的大小超过该阀值后,进行消息压缩。默认为 4 * 1024B
max-message-size: 4194304 # 消息体的最大允许大小。。默认为 4 * 1024 * 1024B
retry-times-when-send-failed: 2 # 同步发送消息时,失败重试次数。默认为 2 次。
retry-times-when-send-async-failed: 2 # 异步发送消息时,失败重试次数。默认为 2 次。
retry-next-server: false # 发送消息给 Broker 时,如果发送失败,是否重试另外一台 Broker 。默认为 false
access-key: # Access Key ,可阅读 https://github.com/apache/rocketmq/blob/master/docs/cn/acl/user_guide.md 文档
secret-key: # Secret Key
enable-msg-trace: true # 是否开启消息轨迹功能。默认为 true 开启。可阅读 https://github.com/apache/rocketmq/blob/master/docs/cn/msg_trace/user_guide.md 文档
customized-trace-topic: RMQ_SYS_TRACE_TOPIC # 自定义消息轨迹的 Topic 。默认为 RMQ_SYS_TRACE_TOPIC 。
# Consumer 配置项
consumer:
listeners: # 配置某个消费分组,是否监听指定 Topic 。结构为 Map<消费者分组, <Topic, Boolean>> 。默认情况下,不配置表示监听。
erbadagang-consumer-group:
topic1: false # 关闭 test-consumer-group 对 topic1 的监听消费
2.消息发送
先注入RocketMQTemplate ,通过rocketMQTemplate
调用里面的发送消息方法convertAndSend(接收者,消息)
@Component
public class CustomerInfoProducer {
private static final Logger log = LogUtils.getLogger();
private static final String MQ_TOPIC = "CUSTOMER_INFO_SYNC";
/**
* rocketMQ日志
*/
private static final Logger rocketMQLogger = LogUtils.getLogger(CustomerInfoConstant.MQ_LOG_NAME);
@Resource
private RocketMQTemplate rocketMQTemplate;
/**
* 客户资料同步
* @param customerInfoRespVO
*/
public void sendMessageToSZ(CustomerInfoRespVO customerInfoRespVO){
log.info("Customer info data synchronization");
rocketMQLogger.info("producer send msg");
rocketMQTemplate.convertAndSend(MQ_TOPIC ,customerInfoRespVO);
}
}
2.1 发送同步消息
发送同步消息API
//发送普通同步消息-Object
syncSend(String destination, Object payload);
//发送普通同步消息-Message
syncSend(String destination, Message<?> message);
//发送批量普通同步消息
syncSend(String destination, Collection<T> messages);
//发送普通同步消息-Object,并设置发送超时时间
syncSend(String destination, Object payload, long timeout);
//发送普通同步消息-Message,并设置发送超时时间
syncSend(String destination, Message<?> message, long timeout);
//发送批量普通同步消息,并设置发送超时时间
syncSend(String destination, Collection<T> messages, long timeout);
//发送普通同步延迟消息,并设置超时,这个下文会演示
syncSend(String destination, Message<?> message, long timeout, int delayLevel);
@Component
public class CustomerInfoProducer {
private static final Logger log = LogUtils.getLogger();
private static final String MQ_TOPIC = "CUSTOMER_INFO_SYNC";
/**
* rocketMQ日志
*/
private static final Logger rocketMQLogger = LogUtils.getLogger(CustomerInfoConstant.MQ_LOG_NAME);
@Resource
private RocketMQTemplate rocketMQTemplate;
/**
* 客户资料同步
* @param customerInfoRespVO
*/
public void sendMessageToSZ(CustomerInfoRespVO customerInfoRespVO){
log.info("Customer info data synchronization");
rocketMQLogger.info("producer send msg");
//发送消息,并接受返回结果
SendResult sendResult = rocketMQTemplate.syncSend(MQ_TOPIC ,customerInfoRespVO);
//发送消息,并设置超时时间单位毫秒
//SendResult sendResult = rocketMQTemplate.syncSend(MQ_TOPIC ,customerInfoRespVO,6000);
//发送批量消息,并设置超时时间单位毫秒和延迟5秒钟发送
//级别分别对应的时间为1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
//List<CustomerInfoRespVO> customerInfoRespVOList = new ArrayList<>();
//SendResult sendResult = rocketMQTemplate.syncSend(MQ_TOPIC ,customerInfoRespVOList,6000,2);
//获得消息状态
SendStatus sendStatus = sendResult.getSendStatus();
//获得消息id
String msgId = sendResult.getMsgId();
//获得所在队列信息
MessageQueue messageQueue = sendResult.getMessageQueue();
}
}
/**
* 普通发送
* @param topic 消息主题
* @param msg 消息体
* @param <T> 消息泛型
*/
public <T> void send(String topic, T msg) {
rocketMQTemplate.convertAndSend(topic + ":tag1", msg);
//rocketMQTemplate.send(topic + ":tag1", MessageBuilder.withPayload(msg).build()); // 等价于上面一行
}
/**
* 发送带tag的消息,直接在topic后面加上":tag"
*
* @param topic 消息主题
* @param tag 消息tag
* @param msg 消息体
* @param <T> 消息泛型
* @return
*/
public <T> SendResult sendTagMsg(String topic, String tag, T msg) {
topic = topic + ":" + tag;
return rocketMQTemplate.syncSend(topic, MessageBuilder.withPayload(msg).build());
}
/**
* 发送同步消息(阻塞当前线程,等待broker响应发送结果,这样不太容易丢失消息)
* sendResult为返回的发送结果
*/
public <T> SendResult sendMsg(String topic, T msg) {
Message<T> message = MessageBuilder.withPayload(msg).build();
//带key的消息
//MessageBuilder.withPayload(msg).setHeader("KEYS", "1")
SendResult sendResult = rocketMQTemplate.syncSend(topic, message);
log.info("【sendMsg】sendResult={}", JSON.toJSONString(sendResult));
return sendResult;
}
2.2 发送同步消息,并指定对应的队列
@Component
public class CustomerInfoProducer {
private static final Logger log = LogUtils.getLogger();
private static final String MQ_TOPIC = "CUSTOMER_INFO_SYNC";
private static final String HASH_KEY= "1"
/**
* rocketMQ日志
*/
private static final Logger rocketMQLogger = LogUtils.getLogger(CustomerInfoConstant.MQ_LOG_NAME);
@Resource
private RocketMQTemplate rocketMQTemplate;
/**
* 客户资料同步
* @param customerInfoRespVO
*/
public void sendMessageToSZ(CustomerInfoRespVO customerInfoRespVO){
log.info("Customer info data synchronization");
rocketMQLogger.info("producer send msg");
//其他功能与上面同步一样,syncSendOrderly没有批量
rocketMQTemplate.syncSendOrderly(MQ_TOPIC ,customerInfoRespVO,HASH_KEY);
}
}
2.3 发送异步消息
发送异步消息API
//发送普通异步消息-Object
asyncSend(String destination, Object payload, SendCallback sendCallback);
//发送普通异步消息-Message
asyncSend(String destination, Message<?> message, SendCallback sendCallback);
//发送普通异步消息-Object,并设置发送超时时间
asyncSend(String destination, Object payload, SendCallback sendCallback, long timeout);
//发送普通异步消息-Message,并设置发送超时时间
asyncSend(String destination, Message<?> message, SendCallback sendCallback, long timeout);
//发送普通异步延迟消息,并设置超时,这个下文会演示
asyncSend(String destination, Message<?> message, SendCallback sendCallback, long timeout,
int delayLevel);
@Component
public class CustomerInfoProducer {
private static final Logger log = LogUtils.getLogger();
private static final String MQ_TOPIC = "CUSTOMER_INFO_SYNC";
private static final String HASH_KEY= "1"
/**
* rocketMQ日志
*/
private static final Logger rocketMQLogger = LogUtils.getLogger(CustomerInfoConstant.MQ_LOG_NAME);
@Resource
private RocketMQTemplate rocketMQTemplate;
/**
* 客户资料同步
* @param customerInfoRespVO
*/
public void sendMessageToSZ(CustomerInfoRespVO customerInfoRespVO){
log.info("Customer info data synchronization");
rocketMQLogger.info("producer send msg");
//发送异步消息 MQ_TOPIC 主题 customerInfoRespVO 消息内容 CustomerInfoSendCallbackImpl 回调方法
rocketMQTemplate.asyncSend(MQ_TOPIC ,customerInfoRespVO,new CustomerInfoSendCallbackImpl());
//发送异步消息,并设置超时时间单位毫秒
//rocketMQTemplate.asyncSend(MQ_TOPIC ,customerInfoRespVO,new CustomerInfoSendCallbackImpl(),6000);
}
}
@Slf4j
public class CustomerInfoSendCallbackImpl implements SendCallback{
@Override
public void onSuccess(SendResult sendResult) {
log.info("消息发送成功");
//成功后的处理
}
@Override
public void onException(Throwable e) {
log.info("消息发送失败");
//失败后的处理
}
}
/**
* 发送异步消息
* 发送异步消息(通过线程池执行发送到broker的消息任务,执行完后回调:在SendCallback中可处理相关成功失败时的逻辑)
* (适合对响应时间敏感的业务场景)
* @param topic 消息Topic
* @param msg 消息实体
*
*/
public <T> void asyncSend(String topic, T msg) {
Message<T> message = MessageBuilder.withPayload(msg).build();
//带key的消息
//MessageBuilder.withPayload(msg).setHeader("KEYS", "1")
asyncSend(topic, message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("topic:{}消息---发送MQ成功---", topic);
}
@Override
public void onException(Throwable throwable) {
log.error("topic:{}消息---发送MQ失败 ex:{}---", topic, throwable.getMessage());
}
});
}
/**
* 发送异步消息
* 发送异步消息(通过线程池执行发送到broker的消息任务,执行完后回调:在SendCallback中可处理相关成功失败时的逻辑)
* (适合对响应时间敏感的业务场景)
* @param topic 消息Topic
* @param message 消息实体
* @param sendCallback 回调函数
*/
public void asyncSend(String topic, Message<?> message, SendCallback sendCallback) {
rocketMQTemplate.asyncSend(topic, message, sendCallback);
}
/**
* 发送异步消息
*
* @param topic 消息Topic
* @param message 消息实体
* @param sendCallback 回调函数
* @param timeout 超时时间
*/
public void asyncSend(String topic, Message<?> message, SendCallback sendCallback, long timeout) {
rocketMQTemplate.asyncSend(topic, message, sendCallback, timeout);
}
2.4 发送异步消息,并指定队列
@Component
public class CustomerInfoProducer {
private static final Logger log = LogUtils.getLogger();
private static final String MQ_TOPIC = "CUSTOMER_INFO_SYNC";
private static final String HASH_KEY= "1"
/**
* rocketMQ日志
*/
private static final Logger rocketMQLogger = LogUtils.getLogger(CustomerInfoConstant.MQ_LOG_NAME);
@Resource
private RocketMQTemplate rocketMQTemplate;
/**
* 客户资料同步
* @param customerInfoRespVO
*/
public void sendMessageToSZ(CustomerInfoRespVO customerInfoRespVO){
log.info("Customer info data synchronization");
rocketMQLogger.info("producer send msg");
//发送异步消息 MQ_TOPIC 主题 customerInfoRespVO 消息内容 CustomerInfoSendCallbackImpl 回调方法
rocketMQTemplate.asyncSendOrderly(MQ_TOPIC ,customerInfoRespVO,HASH_KEY,new CustomerInfoSendCallbackImpl());
//发送异步消息,并设置超时时间单位毫秒
//rocketMQTemplate.asyncSendOrderly(MQ_TOPIC ,customerInfoRespVO,HASH_KEY,new CustomerInfoSendCallbackImpl(),6000);
}
}
@Slf4j
public class CustomerInfoSendCallbackImpl implements SendCallback{
@Override
public void onSuccess(SendResult sendResult) {
log.info("消息发送成功");
//成功后的处理
}
@Override
public void onException(Throwable e) {
log.info("消息发送失败");
//失败后的处理
}
}
2.5 发送单向消息
/**
* 单向消息
* 特点为只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答
* 此方式发送消息的过程耗时非常短,一般在微秒级别
* 应用场景:适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集
* @param topic 消息主题
* @param msg 消息体
* @param <T> 消息泛型
*/
public <T> void sendOneWayMsg(String topic, T msg) {
Message<T> message = MessageBuilder.withPayload(msg).build();
rocketMQTemplate.sendOneWay(topic, message);
}
2.6 发送事物消息
事物消息消费者与普通消息消费者一样,不需要特殊处理
/**
* 发送事务消息
*
* @param txProducerGroup 事务消息的生产者组名称
* @param topic 事务消息主题
* @param tag 事务消息tag
* @param msg 事务消息体
* @param arg 事务消息监听器回查参数
* @param <T> 事务消息泛型
*/
public <T> void sendTransaction(String txProducerGroup, String topic, String tag, T msg, T arg){
if(!StringUtils.isEmpty(tag)){
topic = topic + ":" + tag;
}
String transactionId = UUID.randomUUID().toString();
Message<T> message = MessageBuilder.withPayload(msg)
//header也有大用处
.setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId)
.setHeader("share_id", msg.getId())
.build();
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(txProducerGroup, topic, message, arg);
if(result.getLocalTransactionState().equals(LocalTransactionState.COMMIT_MESSAGE)
&& result.getSendStatus().equals(SendStatus.SEND_OK)){
log.info("事物消息发送成功");
}
log.info("事物消息发送结果:{}", result);
}
定义本地事务处理类,实现RocketMQLocalTransactionListener接口,以及加上@RocketMQTransactionListener注解,这个类似方法的调用是异步的;
executeLocalTransaction方法,当我们处理完业务后,可以根据业务处理情况,返回事务执行状态,有bollback, commit or unknown三种,分别是回滚事务,提交事务和未知;根据事务消息执行流程,如果返回bollback,则直接丢弃消息;如果是返回commit,则消费消息;如果是unknow,则继续等待,然后调用checkLocalTransaction方法,最多重试15次,超过了默认丢弃此消息;
checkLocalTransaction方法,是当MQ Server未得到MQ发送方应答,或者超时的情况,或者应答是unknown的情况,调用此方法进行检查确认,返回值和上面的方法一样;
@Slf4j
@Component
@RocketMQTransactionListener(txProducerGroup = "CUSTOMER_NOTICE_CONSUMER_GROUP")
public class TransactionListenerImpl implements RocketMQLocalTransactionListener {
@Autowired
private ShareService shareService;
@Autowired
private RocketmqTransactionLogMapper rocketmqTransactionLogMapper;
/**
* 发送prepare消息成功此方法被回调,该方法用于执行本地事务
* @param message 回传的消息,利用transactionId即可获取到该消息的唯一Id
* @param arg 调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
* @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object arg) {
MessageHeaders headers = message.getHeaders();
String transactionId = (String)headers.get(RocketMQHeaders.TRANSACTION_ID);
Integer shareId = Integer.parseInt((String)headers.get("share_id"));
try {
shareService.auditBYIdWithRocketMqLog(shareId,(ShareAuditDTO)auditDTO,transactionId);
//本地事物成功,执行commit
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
log.error("本地事物执行异常,e={}",e);
//本地事物失败,执行rollback
return RocketMQLocalTransactionState.ROLLBACK;
}
}
//mq回调检查本地事务执行情况
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
MessageHeaders headers = message.getHeaders();
String transactionId = (String)headers.get(RocketMQHeaders.TRANSACTION_ID);
RocketmqTransactionLog rocketmqTransactionLog = rocketmqTransactionLogMapper.selectOne(RocketmqTransactionLog
.builder().transactionId(transactionId).build());
if(rocketmqTransactionLog == null){
log.error("如果本地事物日志没有记录,transactionId={}",transactionId);
//本地事物失败,执行rollback
return RocketMQLocalTransactionState.ROLLBACK;
}
//如果本地事物日志有记录,执行commit
return RocketMQLocalTransactionState.COMMIT;
}
}
留意上面的:发送Tag标签消息,没有单独抽出来说
主题创建
3.消息模式
一个消费者只能绑定一个队列,一个队列可以绑定多个消费者
如果需要按业务过滤对应的消息可以设置selectorExpression为对应的业务Tag消息
@RocketMQMessageListener
注解参数说明:
- consumerGroup:消费者订阅组,它是必需的,并且必须是唯一的
- topic:主题名字,生产发送的主题名
- consumeMode:消费模式,可选择并发或有序接收消息;默认CONCURRENTLY同时接收异步传递的消息
- messageModel:消息模式,默认CLUSTERING集群消费;如果希望所有订阅者都接收消息,可以设置广播BROADCASTING
- consumeThreadMax:消费者最大线程数,默认64
- consumeTimeout:消息阻塞最长时间,默认15分钟
- nameServer:服务器地址,默认读取配置文件地址,可以单独为消费者设置指定位置
- selectorExpression:消费指定的Tag标签的业务消息
- 更多查看官方解释
3.1 集群消息模式
一个项目多台服务器部署属于集群方式(默认也是集群)
public class CustomerInfoConstant {
//MQ使用字段
public static final String MQ_TOPIC= "CUSTOMER_INFO_SYNC";
public static final String MQ_CONSUMER_GROUP = "CUSTOMER_NOTICE_CONSUMER_GROUP";
public static final String MQ_CONSUMER_GROUP_TWO = "CUSTOMER_INFO_NOTICE_CONSUMER_GROUP";
}
@Component
@Service
@RocketMQMessageListener(topic = CustomerInfoConstant.MQ_TOPIC, consumerGroup = CustomerInfoConstant.MQ_CONSUMER_GROUP, consumeThreadMax = 10)
@RefreshScope
public class CustomerInfoConsumer implements RocketMQListener<CustomerInfoRespVO> {
private static final Logger log = LogUtils.getLogger();
@Resource
CustomerInfoSyncService customerInfoSyncService;
@Override
public void onMessage(CustomerInfoRespVO customerInfoRespVO) {
log.info("Trigger incremental synchronization of customer data. :{}", customerInfoRespVO);
//消费逻辑
}
}
3.2 广播消息模式
注意下面是两个服务,同一个消费者组,我们需要在后面加上messageModel = MessageModel.BROADCASTING来表示这个消费者组属于广播消费模式;
注意:广播模式下不分消费者组只要监听了同一个Topic就能同时消费一条消息
@Component
@Service
@RocketMQMessageListener(topic = CustomerInfoConstant.MQ_TOPIC, consumerGroup = CustomerInfoConstant.MQ_CONSUMER_GROUP, consumeThreadMax = 10, messageModel = MessageModel.BROADCASTING)
@RefreshScope
public class CustomerInfoConsumer implements RocketMQListener<CustomerInfoRespVO> {
private static final Logger log = LogUtils.getLogger();
@Resource
CustomerInfoSyncService customerInfoSyncService;
@Override
public void onMessage(CustomerInfoRespVO customerInfoRespVO) {
log.info("Trigger incremental synchronization of customer data. :{}", customerInfoRespVO);
//消费逻辑
}
}
@Component
@Service
@RocketMQMessageListener(topic = CustomerInfoConstant.MQ_TOPIC_, consumerGroup = CustomerInfoConstant.MQ_CONSUMER_GROUP_TWO, consumeThreadMax = 10, messageModel = MessageModel.BROADCASTING)
@RefreshScope
public class CustomerInfoConsumer implements RocketMQListener<CustomerInfoRespVO> {
private static final Logger log = LogUtils.getLogger();
@Resource
CustomerInfoSyncService customerInfoSyncService;
@Override
public void onMessage(CustomerInfoRespVO customerInfoRespVO) {
log.info("Trigger incremental synchronization of customer data. :{}", customerInfoRespVO);
//消费逻辑
}
}
消费者组创建
4.消费模式
消息分为有序消息和并发消息:
- CONCURRENTLY 并发消息
- ORDERLY 有序消息
MessageListenerOrderly正确消费返回ConsumeOrderlyStatus.SUCCESS,
稍后消费返回ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT
MessageListenerConcurrently正确消费返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS
稍后消费返回ConsumeConcurrentlyStatus.RECONSUME_LATER
顾名思义,有序消费模式是按照消息的顺序进行消费,但是除此之外,我发现和并发消费模式还有很大的区别的。
并发比有序消费快得多。
另一个区别是消费失败时的处理不同,有序消费模式返回ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT后,消费者会立马消费这条消息,而使用并发消费模式,返回ConsumeConcurrentlyStatus.RECONSUME_LATER后,要过好几秒甚至十几秒才会再次消费。我是在只有一条消息的情况下测试的。更重要的区别是,
返回ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT并不会增加消息的消费次数,mq消息有个默认最大消费次数16,消费次数到了以后,这条消息会进入死信队列,这个最大消费次数是可以在mqadmin中设置的。
mqadmin updateSubGroup -n 127.0.0.1:9876 -c DefaultCluster -g MonitorCumsumerGroupName -r 3
我测试后发现,并发模式下返回ConsumeConcurrentlyStatus.RECONSUME_LATER,同一个消息到达最大消费次数之后就不会再出现了。这说明有序消费模式可能并没有这个机制,这意味着你再有序消费模式下抛出固定异常,那么这条异常信息将会被永远消费,并且很可能会影响之后正常的消息。
有序消费模式,消费失败后,会马上拉这条信息。
并发消费模式则不会无限消费,而且消费失败后不会马上再消费。
结论是有序消费模式MessageListenerOrderly要慎重地处理异常,我则是用全局变量记录消息的错误消费次数,只要消费次数达到一定次数,那么就直接返回ConsumeOrderlyStatus.SUCCESS
小知识点:
- 发消息是使用Tag,Tag:用于区分过滤同一主题下的不同业务类型的消息,非常实用
- RocketMQ自带JSON转换,所以我们发消息的时候可以直接将对象传进去,避免null值的属性不传输过去。