ActiveMQ消费者详解

JMS(ActiveMQ) PTP和PUB/SUB模式实例:[url]http://donald-draper.iteye.com/blog/2347445[/url]
ActiveMQ连接工厂、连接详解:[url]http://donald-draper.iteye.com/blog/2348070[/url]
ActiveMQ会话初始化:[url]http://donald-draper.iteye.com/blog/2348341[/url]
ActiveMQ生产者:[url]http://donald-draper.iteye.com/blog/2348381[/url]
ActiveMQ消费者:[url]http://donald-draper.iteye.com/blog/2348389[/url]
ActiveMQ启动过程详解[url]:http://donald-draper.iteye.com/blog/2348399[/url]
ActiveMQ Broker发送消息给消费者过程详解:[url]http://donald-draper.iteye.com/blog/2348440[/url]
Spring与ActiveMQ的集成:[url]http://donald-draper.iteye.com/blog/2347638[/url]
Spring与ActiveMQ的集成详解一:[url]http://donald-draper.iteye.com/blog/2348449[/url]
Spring与ActiveMQ的集成详解二:[url]http://donald-draper.iteye.com/blog/2348461[/url]
上一篇我们讲了生产者,今天来说一下消费者,从下面这段开始
// Destination :消息的目的地;消息发送给谁.  
Topic destination=session.createTopic(tname);
// 消费者,消息接收者
MessageConsumer consumer = session.createConsumer(destination);


//ActiveMQSession创建消费者
public MessageConsumer createConsumer(Destination destination)
throws JMSException
{
return createConsumer(destination, (String)null);
}

public MessageConsumer createConsumer(Destination destination, String messageSelector)
throws JMSException
{
return createConsumer(destination, messageSelector, false);
}

public MessageConsumer createConsumer(Destination destination, MessageListener messageListener)
throws JMSException
{
return createConsumer(destination, null, messageListener);
}

public MessageConsumer createConsumer(Destination destination, String messageSelector, MessageListener messageListener)
throws JMSException
{
return createConsumer(destination, messageSelector, false, messageListener);
}

public MessageConsumer createConsumer(Destination destination, String messageSelector, boolean noLocal)
throws JMSException
{
return createConsumer(destination, messageSelector, noLocal, null);
}

public MessageConsumer createConsumer(Destination destination, String messageSelector, boolean noLocal, MessageListener messageListener)
throws JMSException
{
checkClosed();
if(destination instanceof CustomDestination)
{
CustomDestination customDestination = (CustomDestination)destination;
return customDestination.createConsumer(this, messageSelector, noLocal);
}
//从连接后去获取消息策略
ActiveMQPrefetchPolicy prefetchPolicy = connection.getPrefetchPolicy();
int prefetch = 0;
if(destination instanceof Topic)
prefetch = prefetchPolicy.getTopicPrefetch();
else
prefetch = prefetchPolicy.getQueuePrefetch();
ActiveMQDestination activemqDestination = ActiveMQMessageTransformation.transformDestination(destination);
//创建消费者
return new ActiveMQMessageConsumer(this, getNextConsumerId(), activemqDestination, null, messageSelector, prefetch, prefetchPolicy.getMaximumPendingMessageLimit(), noLocal, false, isAsyncDispatch(), messageListener);
}



public class ActiveMQMessageConsumer
implements MessageAvailableConsumer, StatsCapable, ActiveMQDispatcher
{
protected final ActiveMQSession session;
protected final ConsumerInfo info;//消费者信息
protected final MessageDispatchChannel unconsumedMessages;//消息分发通道(没消费的消息)
protected final LinkedList deliveredMessages = new LinkedList();//传输消息队列
private PreviouslyDeliveredMap previouslyDeliveredMessages;
private int deliveredCounter;//传输消息计数器
private int additionalWindowSize;
private long redeliveryDelay;//消息传输延时
private int ackCounter;//消息回复计时器
private int dispatchedCount;//消息分发计时器
private final AtomicReference messageListener = new AtomicReference();//消息监听器引用
private final JMSConsumerStatsImpl stats;//消费者状态信息管理器
private final String selector;选择器
private boolean synchronizationRegistered;
private final AtomicBoolean started = new AtomicBoolean(false);//启动状态
private MessageAvailableListener availableListener;
private RedeliveryPolicy redeliveryPolicy;//传输策略
private boolean optimizeAcknowledge;
private final AtomicBoolean deliveryingAcknowledgements = new AtomicBoolean();
private ExecutorService executorService;//执行器
private MessageTransformer transformer;
private boolean clearDeliveredList;
AtomicInteger inProgressClearRequiredFlag;
private MessageAck pendingAck;//消息回复
private long lastDeliveredSequenceId;
private IOException failureError;
private long optimizeAckTimestamp;
private long optimizeAcknowledgeTimeOut;
private long optimizedAckScheduledAckInterval;
private Runnable optimizedAckTask;//消息回复优化任务
private long failoverRedeliveryWaitPeriod;//当Master宕机时,消息传输等待时间
private boolean transactedIndividualAck;
private boolean nonBlockingRedelivery;//是否非阻塞传输
private boolean consumerExpiryCheckEnabled;
public ActiveMQMessageConsumer(ActiveMQSession session, ConsumerId consumerId, ActiveMQDestination dest, String name, String selector, int prefetch, int maximumPendingMessageCount,
boolean noLocal, boolean browser, boolean dispatchAsync, MessageListener messageListener)
throws JMSException
{
inProgressClearRequiredFlag = new AtomicInteger(0);
lastDeliveredSequenceId = -1L;
optimizeAckTimestamp = System.currentTimeMillis();
optimizeAcknowledgeTimeOut = 0L;
optimizedAckScheduledAckInterval = 0L;
failoverRedeliveryWaitPeriod = 0L;
transactedIndividualAck = false;
nonBlockingRedelivery = false;
consumerExpiryCheckEnabled = true;
if(dest == null)
throw new InvalidDestinationException("Don't understand null destinations");
if(dest.getPhysicalName() == null)
throw new InvalidDestinationException("The destination object was not given a physical name.");
if(dest.isTemporary())
{
String physicalName = dest.getPhysicalName();
if(physicalName == null)
throw new IllegalArgumentException((new StringBuilder()).append("Physical name of Destination should be valid: ").append(dest).toString());
String connectionID = session.connection.getConnectionInfo().getConnectionId().getValue();
if(physicalName.indexOf(connectionID) < 0)
throw new InvalidDestinationException("Cannot use a Temporary destination from another Connection");
if(session.connection.isDeleted(dest))
throw new InvalidDestinationException("Cannot use a Temporary destination that has been deleted");
if(prefetch < 0)
throw new JMSException("Cannot have a prefetch size less than zero");
}
//配置消息分发通道
if(session.connection.isMessagePrioritySupported())
unconsumedMessages = new SimplePriorityMessageDispatchChannel();
else
unconsumedMessages = new FifoMessageDispatchChannel();
this.session = session;
//获取连接重新传输策略
redeliveryPolicy = session.connection.getRedeliveryPolicyMap().getEntryFor(dest);
setTransformer(session.getTransformer());
info = new ConsumerInfo(consumerId);
info.setExclusive(this.session.connection.isExclusiveConsumer());
info.setClientId(this.session.connection.getClientID());
info.setSubscriptionName(name);
info.setPrefetchSize(prefetch);
info.setCurrentPrefetchSize(prefetch);
info.setMaximumPendingMessageLimit(maximumPendingMessageCount);
info.setNoLocal(noLocal);
info.setDispatchAsync(dispatchAsync);
info.setRetroactive(this.session.connection.isUseRetroactiveConsumer());
info.setSelector(null);
if(dest.getOptions() != null)
{
Map options = IntrospectionSupport.extractProperties(new HashMap(dest.getOptions()), "consumer.");
IntrospectionSupport.setProperties(info, options);
if(options.size() > 0)
{
String msg = (new StringBuilder()).append("There are ").append(options.size()).append(" consumer options that couldn't be set on the consumer.").append(" Check the options are spelled correctly.").append(" Unknown parameters=[").append(options).append("].").append(" This consumer cannot be started.").toString();
LOG.warn(msg);
throw new ConfigurationException(msg);
}
}
info.setDestination(dest);
info.setBrowser(browser);
if(selector != null && selector.trim().length() != 0)
{
SelectorParser.parse(selector);
info.setSelector(selector);
this.selector = selector;
} else
if(info.getSelector() != null)
{
SelectorParser.parse(info.getSelector());
this.selector = info.getSelector();
} else
{
this.selector = null;
}
//创建消费者状态管理器
stats = new JMSConsumerStatsImpl(session.getSessionStats(), dest);
optimizeAcknowledge = session.connection.isOptimizeAcknowledge() && session.isAutoAcknowledge() && !info.isBrowser();
if(optimizeAcknowledge)
{
optimizeAcknowledgeTimeOut = session.connection.getOptimizeAcknowledgeTimeOut();
setOptimizedAckScheduledAckInterval(session.connection.getOptimizedAckScheduledAckInterval());
}
info.setOptimizedAcknowledge(optimizeAcknowledge);
failoverRedeliveryWaitPeriod = session.connection.getConsumerFailoverRedeliveryWaitPeriod();
nonBlockingRedelivery = session.connection.isNonBlockingRedelivery();
transactedIndividualAck = session.connection.isTransactedIndividualAck() || nonBlockingRedelivery || session.connection.isMessagePrioritySupported();
consumerExpiryCheckEnabled = session.connection.isConsumerExpiryCheckEnabled();
if(messageListener != null)
//设置消息监听器
setMessageListener(messageListener);
try
{
//将消费者添加到会话
this.session.addConsumer(this);
//发送消费者信息给broker
this.session.syncSendPacket(info);
}
catch(JMSException e)
{
this.session.removeConsumer(this);
throw e;
}
//如果连接启动,则启动消费者
if(session.connection.isStarted())
start();
}
}

创建消费者就是初始化消费者,传输延时,选择器,消息监听器,消息重传策略,并把消费者信息发送给broker,同时将消费者添加到会话中,启动会话执行器,通知消息者消费消息;


这个我们在会话启动篇又说,这里我们简单地说一下:

//启动消费者
   public void start()
throws JMSException
{
if(unconsumedMessages.isClosed())
{
return;
} else
{
started.set(true);
//启动消息通道,通知消息通道可以发送未消费的消息给消息者
unconsumedMessages.start();
//通过会话执行器唤醒消费者,消费消息
session.executor.wakeup();
return;
}
}


unconsumedMessages我们以先进先出消息分发通道来讲FifoMessageDispatchChannel
,还有一种是基于消息优先级的这里就不说了前文以说
public class FifoMessageDispatchChannel
implements MessageDispatchChannel
{
private final Object mutex = new Object();
private final LinkedList list = new LinkedList();
private boolean closed;
private boolean running;
public void start()
{
synchronized(mutex)
{
//通知所有等待分发消息互斥量的线程
running = true;
mutex.notifyAll();
}
}
}

//唤醒会话执行器
session.executor.wakeup();

下面是我们前文的关于这一句的总结

稍微简单过一下

public class ActiveMQSessionExecutor
implements Task
{
//唤醒消费者,消费消息
public void wakeup()
{
//this为ActiveMQSessionExecutor,
this.taskRunner = session.connection.getSessionTaskRunner().createTaskRunner(this, (new StringBuilder()).append("ActiveMQ Session: ").append(session.getSessionId()).toString());
taskRunner = this.taskRunner;
//唤醒任务线程,taskRunner实际为PooledTaskRunner
taskRunner.wakeup();
}
}


class PooledTaskRunner
implements TaskRunner
{

public PooledTaskRunner(Executor executor, final Task task, int maxIterationsPerRun)
{
this.executor = executor;
this.maxIterationsPerRun = maxIterationsPerRun;
this.task = task;
runable = new Runnable() {

public void run()
{
runningThread = Thread.currentThread();
//运行任务
runTask();
}

final Task val$task;
final PooledTaskRunner this$0;


{
this$0 = PooledTaskRunner.this;
task = task1;
super();
}
};
}
}

final void runTask()
{
label0:
{
synchronized(runable)
{
queued = false;
if(!shutdown)
break label0;
iterating = false;
runable.notifyAll();
}
return;
}
iterating = true;
_L1:
boolean done = false;
int i = 0;
do
{
if(i >= maxIterationsPerRun)
break;
LOG.trace("Running task iteration {} - {}", Integer.valueOf(i), task);
//关键在这一句,task为ActiveMQSessionExecutor
if(!task.iterate())
{
done = true;
break;
}
i++;
} while(true);
...
}



再看ActiveMQSessionExecutor
//ActiveMQSessionExecutor
 public boolean iterate()
{
for(Iterator i$ = session.consumers.iterator(); i$.hasNext();)
{
ActiveMQMessageConsumer consumer = (ActiveMQMessageConsumer)i$.next();
//遍历消费者,消费消息
if(consumer.iterate())
return true;
}
MessageDispatch message = messageQueue.dequeueNoWait();
if(message == null)
{
return false;
} else
{
//分发为消费的消息
dispatch(message);
return !messageQueue.isEmpty();
}
}


我们来看这部分
ActiveMQMessageConsumer consumer = (ActiveMQMessageConsumer)i$.next();

//遍历消费者,消费消息
if(consumer.iterate())
return true;
//ActiveMQMessageConsumer

public boolean iterate()
{
MessageListener listener = (MessageListener)messageListener.get();
if(listener != null)
{
//从未消费消息通道,获取未消费消息
MessageDispatch md = unconsumedMessages.dequeueNoWait();
if(md != null)
{
//如果有未消费的消息,则分发消息
dispatch(md);
return true;
}
}
return false;
}

//分发消息
 public void dispatch(MessageDispatch md)
{
ActiveMQMessage message = createActiveMQMessage(md);
beforeMessageIsConsumed(md);
try
{
boolean expired = isConsumerExpiryCheckEnabled() && message.isExpired();
if(!expired)
//关键在这一句调用监听器的消费消息方法onMessage
listener.onMessage(message);
afterMessageIsConsumed(md, expired);
}
}

//从未消费消息通道,获取未消费消息
MessageDispatch md = unconsumedMessages.dequeueNoWait();
以先进先出消息通道为例FifoMessageDispatchChannel
//FifoMessageDispatchChannel
public class FifoMessageDispatchChannel
implements MessageDispatchChannel
{
private final Object mutex = new Object();
private final LinkedList list = new LinkedList();//未分发消息队列
private boolean closed;
private boolean running;
public MessageDispatch dequeueNoWait()
{
Object obj = mutex;
JVM INSTR monitorenter ;
if(closed || !running || list.isEmpty())
return null;
//从消息队列获取队列头消息
(MessageDispatch)list.removeFirst();
obj;
JVM INSTR monitorexit ;
return;
Exception exception;
exception;
throw exception;
}
}

小节:
消费者启动主要是唤醒ActiveMQSessionExecutor,会话执行器唤醒主要做的做工作是
ActiveMQConnection创建任务执行TaskRunnerFactory,有任务执行工厂TaskRunnerFactory,有任务执行工厂创建执行任务PooledTaskRunner,PooledTaskRunner是ActiveMQSessionExecutor的包装,PooledTaskRunner执行就是执行ActiveMQSessionExecutor
iterate的函数,这个过程主要是ActiveMQSessionExecutor从ActiveMQSession获取会话消费者consumer,从未分发消息队列获取消息,然后遍历消费者,消费者通过MessageListener消费消息。


回到PooledTaskRunner
//唤醒会话执行器,执行PooledTaskRunner,分发未消费的消息给消费者
 public void wakeup()
throws InterruptedException
{
{
synchronized(runable)
{
if(!queued && !shutdown)
break label0;
}
return;
}
queued = true;
//执行PooledTaskRunner
if(!iterating)
executor.execute(runable);
}



下面来看消费者消费的两种方式

消费消息两种方式
第一种消费消息方式:
while (true) { 
//设置接收者接收消息的时间,为了便于测试,这里谁定为100s
TextMessage message = (TextMessage) consumer.receive(100000);
if (null != message) {
System.out.println("收到消息" + message.getText());
} else {
break;
}
}*/
}


public javax.jms.Message receive(long timeout)
throws JMSException
{
checkClosed();
checkMessageListener();
if(timeout == 0L)
return receive();
sendPullCommand(timeout);
if(timeout > 0L)
{
MessageDispatch md;
if(info.getPrefetchSize() == 0)
md = dequeue(-1L);
else
//获取消息
md = dequeue(timeout);
if(md == null)
{
return null;
} else
{
beforeMessageIsConsumed(md);
afterMessageIsConsumed(md, false);
//包装消息
return createActiveMQMessage(md);
}
} else
{
return null;
}
}



//获取消息
md = dequeue(timeout);


  private MessageDispatch dequeue(long timeout)
throws JMSException
{
long deadline;
deadline = 0L;
if(timeout > 0L)
deadline = System.currentTimeMillis() + timeout;
//从未消费消息通道获取消息
MessageDispatch md = unconsumedMessages.dequeue(timeout);
}

//包装消息
return createActiveMQMessage(md);
private ActiveMQMessage createActiveMQMessage(final MessageDispatch md)
throws JMSException
{
//获取消息类型
ActiveMQMessage m = (ActiveMQMessage)md.getMessage().copy();
if(m.getDataStructureType() == 29)
((ActiveMQBlobMessage)m).setBlobDownloader(new BlobDownloader(session.getBlobTransferPolicy()));
if(transformer != null)
{
//转换消息
javax.jms.Message transformedMessage = transformer.consumerTransform(session, this, m);
if(transformedMessage != null)
m = ActiveMQMessageTransformation.transformMessage(transformedMessage, session.connection);
}
if(session.isClientAcknowledge())
m.setAcknowledgeCallback(new Callback() {

public void execute()
throws Exception
{
session.checkClosed();
//如消息需要消费者回复,则产生回复消息
session.acknowledge();
}

final ActiveMQMessageConsumer this$0;


{
this$0 = ActiveMQMessageConsumer.this;
super();
}
});
else
if(session.isIndividualAcknowledge())
m.setAcknowledgeCallback(new Callback() {

public void execute()
throws Exception
{
session.checkClosed();
acknowledge(md);
}

final MessageDispatch val$md;
final ActiveMQMessageConsumer this$0;


{
this$0 = ActiveMQMessageConsumer.this;
md = messagedispatch;
super();
}
});
return m;
}

从消费者直接消费消息来看,消费首先从未消费消息通道(FIFO,Priority)获取消息,然后转换消息。
第二种消费消息方式:
       consumer.setMessageListener(new MessageListener(){//有事务限制  
@Override
public void onMessage(Message message) {
try {
ObjectMessage objMessage=(ObjectMessage)message;
Order order = (Order)objMessage.getObject();
System.out.println("消费订单信息:"+order.toString());

} catch (JMSException e1) {
e1.printStackTrace();
}
try {
session.commit();
} catch (JMSException e) {
e.printStackTrace();
}
}
});



设置消息监听器
public void setMessageListener(MessageListener listener)
throws JMSException
{
checkClosed();
if(info.getPrefetchSize() == 0)
throw new JMSException("Illegal prefetch size of zero. This setting is not supported for asynchronous consumers please set a value of at least 1");
if(listener != null)
{
boolean wasRunning = session.isRunning();
if(wasRunning)
session.stop();
//设置消费者消息监听器
messageListener.set(listener);
//会话重新分发未消费消息
session.redispatch(this, unconsumedMessages);
if(wasRunning)
session.start();
} else
{
messageListener.set(null);
}
}


//会话重新分发未消费消息
session.redispatch(this, unconsumedMessages);


//ActiveMQSession
 public void redispatch(ActiveMQDispatcher dispatcher, MessageDispatchChannel unconsumedMessages)
throws JMSException
{
List c = unconsumedMessages.removeAll();
MessageDispatch md;
for(Iterator i$ = c.iterator(); i$.hasNext(); connection.rollbackDuplicate(dispatcher, md.getMessage()))
md = (MessageDispatch)i$.next();

Collections.reverse(c);
MessageDispatch md;
//遍历为未消费消息,会话执行器通知消息者消费消息
for(Iterator iter = c.iterator(); iter.hasNext(); executor.executeFirst(md))
md = (MessageDispatch)iter.next();

}


public class ActiveMQSessionExecutor
void executeFirst(MessageDispatch message)
{
//将未消费消息放入消息队列,待消费者消费
messageQueue.enqueueFirst(message);
//这个前面看到,唤醒消费者消费消息
wakeup();
}

监听器方式实际为会话从未消费消息队列获取消息,添加到消息队列,通过会话执行器通知消费者消费


总结:
[color=green]创建消费者就是初始化消费者,传输延时,选择器,消息监听器,消息重传策略,并把消费者信息发送给broker,同时将消费者添加到会话中,启动会话执行器,通知消息者消费消息;消费者启动主要是唤醒ActiveMQSessionExecutor,会话执行器唤醒主要做的做工作是
ActiveMQConnection创建任务执行TaskRunnerFactory,有任务执行工厂TaskRunnerFactory,有任务执行工厂创建执行任务PooledTaskRunner,PooledTaskRunner是ActiveMQSessionExecutor的包装,PooledTaskRunner执行就是执行ActiveMQSessionExecutor
iterate的函数,这个过程主要是ActiveMQSessionExecutor从ActiveMQSession获取会话消费者consumer,从未分发消息队列获取消息,然后遍历消费者,消费者通过MessageListener消费消息。消费者直接消费消息方式为,消费首先从未消费消息通道(FIFO,Priority)获取消息,然后转换消息;监听器方式,实际为会话从未消费消息队列获取消息,添加到消息队列,通过会话执行器通知消费者消费。[/color]

public class MessageDispatch extends BaseCommand
{
public static final byte DATA_STRUCTURE_TYPE = 21;
protected ConsumerId consumerId;//消费者id
protected ActiveMQDestination destination;//消息目的地
protected Message message;//消息
protected int redeliveryCounter;//重传计数器
protected transient long deliverySequenceId;
protected transient Object consumer;//消费者
protected transient TransmitCallback transmitCallback;
protected transient Throwable rollbackCause;

public MessageDispatch()
{
}

public byte getDataStructureType()
{
return 21;
}

public boolean isMessageDispatch()
{
return true;
}

public ConsumerId getConsumerId()
{
return consumerId;
}

public void setConsumerId(ConsumerId consumerId)
{
this.consumerId = consumerId;
}

public ActiveMQDestination getDestination()
{
return destination;
}

public void setDestination(ActiveMQDestination destination)
{
this.destination = destination;
}

public Message getMessage()
{
return message;
}

public void setMessage(Message message)
{
this.message = message;
}

public long getDeliverySequenceId()
{
return deliverySequenceId;
}

public void setDeliverySequenceId(long deliverySequenceId)
{
this.deliverySequenceId = deliverySequenceId;
}

public int getRedeliveryCounter()
{
return redeliveryCounter;
}

public void setRedeliveryCounter(int deliveryCounter)
{
redeliveryCounter = deliveryCounter;
}

public Object getConsumer()
{
return consumer;
}

public void setConsumer(Object consumer)
{
this.consumer = consumer;
}

public Response visit(CommandVisitor visitor)
throws Exception
{
return visitor.processMessageDispatch(this);
}

public TransmitCallback getTransmitCallback()
{
return transmitCallback;
}

public void setTransmitCallback(TransmitCallback transmitCallback)
{
this.transmitCallback = transmitCallback;
}

public Throwable getRollbackCause()
{
return rollbackCause;
}

public void setRollbackCause(Throwable rollbackCause)
{
this.rollbackCause = rollbackCause;
}

}


//消息获取策略
public class ActiveMQPrefetchPolicy
implements Serializable
{
public static final int MAX_PREFETCH_SIZE = 32767;
public static final int DEFAULT_QUEUE_PREFETCH = 1000;
public static final int DEFAULT_QUEUE_BROWSER_PREFETCH = 500;
public static final int DEFAULT_DURABLE_TOPIC_PREFETCH = 100;
public static final int DEFAULT_OPTIMIZE_DURABLE_TOPIC_PREFETCH = 1000;
public static final int DEFAULT_TOPIC_PREFETCH = 32767;
private static final Logger LOG = LoggerFactory.getLogger(org/apache/activemq/ActiveMQPrefetchPolicy);
private int queuePrefetch;//队列策略
private int queueBrowserPrefetch;
private int topicPrefetch;//主题策略
private int durableTopicPrefetch;
private int optimizeDurableTopicPrefetch;
private int maximumPendingMessageLimit;
public ActiveMQPrefetchPolicy()
{
queuePrefetch = 1000;
queueBrowserPrefetch = 500;
topicPrefetch = 32767;
durableTopicPrefetch = 100;
optimizeDurableTopicPrefetch = 1000;
}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值