主要原因是由于在高并发环境下,由于来不及同步处理,请求往往会发生堵塞,比如说,大量的insert,update之类的请求同时到达MySQL,直接导致无数的行锁表锁,甚至最后请求会堆积过多,从而触发too many connections错误。通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。
RPC和消息中间件的不同很大程度上就是“依赖性”和“同步性”。RPC方式是典型的同步方式,让远程调用像本地调用。消息中间件方式属于异步方式。消息队列是系统级、模块级的通信。RPC是对象级、函数级通信
。
消息中间件常常用于:异步处理、应用解耦、流量削峰、日志处理、消息通讯
。
JMS即Java消息服务(Java Message Service)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API
,绝大多数MOM提供商都对JMS提供支持。
JMS是一种与厂商无关的 API,用来访问消息收发系统消息。它类似于JDBC:这里,JDBC 是可以用来访问许多不同关系数据库的 API,而 JMS 则提供同样与厂商无关的访问方法,以访问消息收发服务。JMS 使您能够通过消息收发服务(有时称为消息中介程序或路由器)从一个 JMS 客户机向另一个 JMS客户机发送消息。
消息是 JMS 中的一种类型对象,由两部分组成:报头和消息主体。报头由路由信息以及有关该消息的元数据组成。消息主体则携带着应用程序的数据或有效负载。根据有效负载的类型来划分,可以将消息分为几种类型,它们分别携带:简单文本(TextMessage)、可序列化的对象 (ObjectMessage)、属性集合 (MapMessage)、字节流 (BytesMessage)、原始值流 (StreamMessage),还有无有效负载的消息 (Message)。
MS中定义了两种消息模型:点对点(point to point, queue)和发布/订阅(publish/subscribe,topic)。主要区别就是是否能重复消费
。
消息生产者生产消息发送到queue中,然后消息消费者从queue中取出并且消费消息。
消息被消费以后,queue中不再有存储,所以消息消费者不可能消费到已经被消费的消息。
Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费、其它的则不能消费此消息了。
当消费者不存在时,消息会一直保存,直到有消费者消费
消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。
和点对点方式不同,发布到topic的消息会被所有订阅者消费。
当生产者发布消息,不管是否有消费者。都不会保存消息
启动后activemq会启动两个端口:
8161是activemq的管理页面,默认的账号密码都是admin
61616是程序连接activemq的通讯地址
org.springframework.boot
spring-boot-starter-activemq
spring:
activemq:
#ActiveMQ通讯地址
broker-url: tcp://localhost:61616
user: admin
password: admin
#是否启用内存模式(就是不安装MQ,项目启动时同时启动一个MQ实例)
in-memory: false
packages:
#信任所有的包
trust-all: true
pool:
#false时,每发送一条数据创建一个连接,true表示使用连接池,使用ActiveMQ的连接池需引入依赖。特别注意:如果是true,会出现“JmsAutoConfiguration did not match“问题
enabled: false
@Configuration
@EnableJms
public class ActiveMQConfig {
@Bean
public Queue queue(){
return new ActiveMQQueue("springboot.queue");
}
//springboot默认只配置queue类型消息,如果要使用topic类型的消息,则需要配置该bean
@Bean
public JmsListenerContainerFactory jmsTopicListenerContainerFactory(ConnectionFactory connectionFactory){
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setPubSubDomain(true);
return factory;
}
@Bean
public Topic topic(){
return new ActiveMQTopic("springboot.topic");
}
}
@Component
public class Consumer {
@JmsListener(destination = "springboot.queue")
public void listenQueue(String msg){
System.out.println("queue接收到的消息:"+msg);
}
@JmsListener(destination = "springboot.topic",containerFactory = "jmsTopicListenerContainerFactory")
public void listenTopic(String msg){
System.out.println("topic接收到的消息1:"+msg);
}
@JmsListener(destination = "springboot.topic",containerFactory = "jmsTopicListenerContainerFactory")
public void listenTopic2(String msg){
System.out.println("topic接收到的消息2:"+msg);
}
}
@RestController
public class Producer {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
private Queue queue;
@Autowired
private Topic topic;
@RequestMapping("/queue")
public void sendQueueMsg(String msg){
jmsMessagingTemplate.convertAndSend(queue,msg);
}
@RequestMapping("topic")
public void sendTopicMsg(String msg){
jmsMessagingTemplate.convertAndSend(topic,msg);
}
}
消费者从消息队列中获取消息有两种:一是通过receive方法获取的,该方法相当于是客户端主动从队列中“拉”消息,并且在消息队列为空时会阻塞等待消息传入;另一种队列“推”送的方式,通过监听器实现。listener不会阻塞等待,当消息到达时会主动调用onMessage方法,但它的生命周期和方法的生命周期是相同的,需要像死循环一样监听。receive和listener是互斥的,即同时只能使用其中一种方式来获取消息
。
在创建Session时,开发者不能指定除ACK_MODE列表之外的其他值。如果此session为事务类型,用户指定的ACK_MODE将被忽略,而强制使用"SESSION_TRANSACTED"类型
;如果session非事务类型时,也将不能将 ACK_MODE设定为"SESSION_TRANSACTED"。
Client的Producer发出一个JMS message形式的request,request上附加了一些额外的属性:
Worker的consumer收到requset,处理request并用producer发出reply,destination就从requset的JMSReplyTo属性中得到。
https://www.jianshu.com/p/8b9bfe865e38
ActiveMQ支持同步、异步两种发送模式将消息发送到broker上。
同步发送过程中,发送者发送一条消息会阻塞直到broker反馈一个确认消息,表示消息已经被broker处理。这个机制提供了消息的安全性保障,但是由于是阻塞的操作,会影响到客户端消息发送的性能。
异步发送的过程中,发送者不需要等待broker提供反馈,所以性能相对较高。但是可能会出现消息丢失的情况。所以使用异步发送的前提是在某些情况下允许出现数据丢失的情况。
默认情况下,非持久化消息是异步发送的,持久化消息并且是在非事务模式下是同步发送的。但是在开启事务的情况下,消息都是异步发送
。由于异步发送的效率会比同步发送性能更高。所以在发送持久化消息的时候,尽量去开启事务会话。除了持久化消息和非持久化消息的同步和异步特性以外,我们还可以通过以下几种方式来设置异步发送:
ConnectionFactory connectionFactory=new ActiveMQConnectionFactory("tcp://192.168.11.153:61616?jms.useAsyncSend=true");
((ActiveMQConnectionFactory) connectionFactory).setUseAsyncSend(true);
((ActiveMQConnection)connection).setUseAsyncSend(true);
以producer.send为入口 进入的是ActiveMQMessageProducer实现:
public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive, AsyncCallback onComplete) throws JMSException {
checkClosed(); //检查session的状态,如果session关闭则抛异常
if (destination == null ) {
if (info.getDestination() == null) {
throw new UnsupportedOperationException("A destination must be specified.");
}
throw new InvalidDestinationException("Don't understand null destinations");
}
//检查destination的类型,如果符合要求,就转变为ActiveMQDestination
ActiveMQDestination dest;
if (destination.equals(info.getDestination())) {
dest = (ActiveMQDestination)destination;
} else if (info.getDestination() == null) {
dest = ActiveMQDestination.transform(destination);
} else {
throw new UnsupportedOperationException("This producer can only send messages to: " + this.info.getDestination().getPhysicalName());
}
if (dest == null) {
throw new JMSException("No destination specified");
}
if (transformer != null) {
Message transformedMessage = transformer.producerTransform(session, this, message);
if (transformedMessage != null) {
message = transformedMessage;
}
}
//如果发送窗口大小不为空,则判断发送窗口的大小决定是否阻塞
if (producerWindow != null) {
try {
producerWindow.waitForSpace();
} catch (InterruptedException e) {
throw new JMSException("Send aborted due to thread interrupt.");
}
}
//发送消息到broker的topic
this.session.send(this, dest, message, deliveryMode, priority, timeToLive, producerWindow, sendTimeout, onComplete);
stats.onMessage();
}
ActiveMQSession的send方法,this.session.send(this, dest, message, deliveryMode, priority, timeToLive, producerWindow, sendTimeout, onComplete):
protected void send(ActiveMQMessageProducer producer, ActiveMQDestination destination, Message message, int deliveryMode, int priority, long timeToLive,
MemoryUsage producerWindow, int sendTimeout, AsyncCallback onComplete) throws JMSException {
checkClosed();
if (destination.isTemporary() && connection.isDeleted(destination)) {
throw new InvalidDestinationException("Cannot publish to a deleted Destination: " + destination);
}
//互斥锁,如果一个session的多个producer发送消息到这里,会保证消息发送的有序性
synchronized (sendMutex) {
// tell the Broker we are about to start a new transaction
doStartTransaction();//告诉broker开始一个新事务,只有事务型会话中才会开启
TransactionId txid = transactionContext.getTransactionId();//从事务上下文中获取事务id
long sequenceNumber = producer.getMessageSequence();
//Set the "JMS" header fields on the original message, see 1.1 spec section 3.4.11
message.setJMSDeliveryMode(deliveryMode); //在JMS协议头中设置是否持久化标识
long expiration = 0L;//计算消息过期时间
if (!producer.getDisableMessageTimestamp()) {
long timeStamp = System.currentTimeMillis();
message.setJMSTimestamp(timeStamp);
if (timeToLive > 0) {
expiration = timeToLive + timeStamp;
}
}
message.setJMSExpiration(expiration);//设置消息过期时间
message.setJMSPriority(priority);//设置消息的优先级
message.setJMSRedelivered(false);;//设置消息为非重发
// transform to our own message format here
ActiveMQMessage msg = ActiveMQMessageTransformation.transformMessage(message, connection);
msg.setDestination(destination);
msg.setMessageId(new MessageId(producer.getProducerInfo().getProducerId(), sequenceNumber));
// Set the message id.
if (msg != message) {//如果消息是经过转化的,则更新原来的消息id和目的地
message.setJMSMessageID(msg.getMessageId().toString());
// Make sure the JMS destination is set on the foreign messages too.
message.setJMSDestination(destination);
}
//clear the brokerPath in case we are re-sending this message
msg.setBrokerPath(null);
msg.setTransactionId(txid);
if (connection.isCopyMessageOnSend()) {
msg = (ActiveMQMessage)msg.copy();
}
msg.setConnection(connection);
msg.onSend();//把消息属性和消息体都设置为只读,防止被修改
msg.setProducerId(msg.getMessageId().getProducerId());
if (LOG.isTraceEnabled()) {
LOG.trace(getSessionId() + " sending message: " + msg);
}
//如果onComplete没有设置(这里传进来就是null),且发送超时时间小于0,且消息不需要反馈,且连接器不是同步发送模式,且消息非持久化或者连接器是异步发送模式
//或者存在事务id的情况下,走异步发送,否则走同步发送
if (onComplete==null && sendTimeout <= 0 && !msg.isResponseRequired() && !connection.isAlwaysSyncSend() && (!msg.isPersistent() || connection.isUseAsyncSend() || txid != null)) {
this.connection.asyncSendPacket(msg);
if (producerWindow != null) {
int size = msg.getSize();//异步发送的情况下,需要设置producerWindow的大小
producerWindow.increaseUsage(size);
}
} else {
if (sendTimeout > 0 && onComplete==null) {
this.connection.syncSendPacket(msg,sendTimeout);//带超时时间的同步发送
}else {
this.connection.syncSendPacket(msg, onComplete);//带回调的同步发送
}
}
}
}
我们从上面的代码可以看到,在执行发送操作之前需要把消息做一个转化,并且将我们设置的一些属性注入到指定的属性中,我们先来看看异步发送,会发现异步发送的时候涉及到producerWindowSize的大小。
ProducerWindowSize的含义:
异步发送
时producer端允许积压的(尚未ACK)的消息的大小,且只对异步发送有意义。每次发送消息之后,都将会导致memoryUsage大小增加(+message.size),当broker返回producerAck时,memoryUsage尺寸减少(producerAck.size,此size表示先前发送消息的大小)。可以通过如下2种方式设置:
注意:此值越大,意味着消耗Client端的内存就越大。
接下去我们进入异步发送流程,看看消息是怎么异步发送的this.connection.asyncSendPacket(msg):
private void doAsyncSendPacket(Command command) throws JMSException {
try {
this.transport.oneway(command);
} catch (IOException e) {
throw JMSExceptionSupport.create(e);
}
}
这里的 Command 其实就是之前一步所转化的message ,并且经过一系列的属性注入。因为ActiveMQMessage 继承了 baseCommand ,该类实现了 Command 。所以可以转化,然后我们发现 oneway 方法又很多的实现,都是基于 transport ,那么我们就需要来看看这个 transport 是什么。这里我们把代码往前翻并没有发现他的初始化,按照我们以往的思路,这里就会在初始化连接的时候进行初始化该对象:
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.254.135:61616");
Connection connection= connectionFactory.createConnection();
这里进入 ActiveMQConnectionFactory 的 createConnection方法会来到:
protected ActiveMQConnection createActiveMQConnection(String userName, String password) throws JMSException {
if (brokerURL == null) {
throw new ConfigurationException("brokerURL not set.");
}
ActiveMQConnection connection = null;
try {// 果然发现了这个东东的初始化
Transport transport = createTransport();
// 创建连接
connection = createActiveMQConnection(transport, factoryStats);
// 设置用户密码
connection.setUserName(userName);
connection.setPassword(password);
// 对连接做包装
configureConnection(connection);
// 启动一个后台传输线程
transport.start();
// 设置客户端消费的id
if (clientID != null) {
connection.setDefaultClientID(clientID);
}
return connection;
} ......
}
这里我们发现了 Transport transport = createTransport(); 这就是他的初始化
protected Transport createTransport() throws JMSException {
try {
URI connectBrokerUL = brokerURL;
String scheme = brokerURL.getScheme();
if (scheme == null) {
throw new IOException("Transport not scheme specified: [" + brokerURL + "]");
}
if (scheme.equals("auto")) {
connectBrokerUL = new URI(brokerURL.toString().replace("auto", "tcp"));
} else if (scheme.equals("auto+ssl")) {
connectBrokerUL = new URI(brokerURL.toString().replace("auto+ssl", "ssl"));
} else if (scheme.equals("auto+nio")) {
connectBrokerUL = new URI(brokerURL.toString().replace("auto+nio", "nio"));
} else if (scheme.equals("auto+nio+ssl")) {
connectBrokerUL = new URI(brokerURL.toString().replace("auto+nio+ssl", "nio+ssl"));
}
return TransportFactory.connect(connectBrokerUL);
} catch (Exception e) {
throw JMSExceptionSupport.create("Could not create Transport. Reason: " + e, e);
}
}
这里有点类似于基于URL驱动的意思,这里进来先是构建一个 URL ,根据URL去创建一个连接TransportFactory.connect,会发现默认使用的是tcp的协议。这里由于我们在创建连接的时候就已经指定了tcp所以这里的判断都没用,直接进入创建连接TransportFactory.connect(connectBrokerUL):
public static Transport connect(URI location) throws Exception {
TransportFactory tf = findTransportFactory(location);
return tf.doConnect(location);
}
这里做连接需要创建一个 tf 对象。就要看看findTransportFactory(location):
public static TransportFactory findTransportFactory(URI location) throws IOException {
String scheme = location.getScheme();
if (scheme == null) {
throw new IOException("Transport not scheme specified: [" + location + "]");
}
TransportFactory tf = TRANSPORT_FACTORYS.get(scheme);
if (tf == null) {
// Try to load if from a META-INF property.
try {
tf = (TransportFactory)TRANSPORT_FACTORY_FINDER.newInstance(scheme);
TRANSPORT_FACTORYS.put(scheme, tf);
} catch (Throwable e) {
throw IOExceptionSupport.create("Transport scheme NOT recognized: [" + scheme + "]", e);
}
}
return tf;
}
不难理解以上的 代码是根据 scheme通过TRANSPORT_FACTORYS 这个map 来创建的 TransportFactory ,如果获取不到,就会通过TRANSPORT_FACTORY_FINDER 去获取一个实例。TRANSPORT_FACTORY_FINDER 这个FINDER是什么东西呢? 我们看看他的初始化:
private static final FactoryFinder TRANSPORT_FACTORY_FINDER = new FactoryFinder("META-INF/services/org/apache/activemq/transport/");
我们通过源码中指定路径以下的东西:
这有点类似于 java 中SPI规范的意思。我们可以看看 tcp 其中的内容:
class=org.apache.activemq.transport.tcp.TcpTransportFactory
这里是键值对的方式,上述获取实例的代码中其实就是获取一个 TcpTransportFactory 实例,那么我们就知道tf.doConnect(location) 是哪个实现类做的,就是TcpTransportFactory,但是我们点开一看并未发现 TcpTransportFactory实现,这就说明该类使用的是父类里面的方法,这里就是TransportFactory 类:
public Transport doConnect(URI location) throws Exception {
try {
Map options = new HashMap(URISupport.parseParameters(location));
if( !options.containsKey("wireFormat.host") ) {
options.put("wireFormat.host", location.getHost());
}
WireFormat wf = createWireFormat(options);
//创建一个Transport 这里才是我们要找的真相
Transport transport = createTransport(location, wf);
//配置configure,这个里面是对Transport做链路包装,思想类似于dubbo的cluster
Transport rc = configure(transport, wf, options);
//remove auto
IntrospectionSupport.extractProperties(options, "auto.");
if (!options.isEmpty()) {
throw new IllegalArgumentException("Invalid connect parameters: " + options);
}
return rc;
} catch (URISyntaxException e) {
throw IOExceptionSupport.create(e);
}
}
我们进入 createTransport(location, wf) 方法,这里是使用Tcp子类的实现。会发现里面创建了一个 Sokect 连接 ,这就是准备后来进行发送的Sokect。然后这里返回的 Transport 就是 TcpTransport .接下去就是对这个 transport 进行包装 configure(transport, wf, options):
public Transport configure(Transport transport, WireFormat wf, Map options) throws Exception {
//组装一个复合的transport,这里会包装两层,一个是IactivityMonitor.另一个是WireFormatNegotiator
transport = compositeConfigure(transport, wf, options);
//再做一层包装,MutexTransport
transport = new MutexTransport(transport);
//包装ResponseCorrelator
transport = new ResponseCorrelator(transport);
return transport;
}
到目前为止,这个transport实际上就是一个调用链了,他的链结构为ResponseCorrelator(MutexTransport(WireFormatNegotiator(IactivityMonitor(TcpTransport()))每一层包装表示什么意思呢?
通过这层层的分析,我们回到 ActiveMQConnection 发送消息的doAsyncSendPacket 方法:
private void doAsyncSendPacket(Command command) throws JMSException {
try {
this.transport.oneway(command);
} catch (IOException e) {
throw JMSExceptionSupport.create(e);
}
}
这里的 oneway(command)方法会先后经历上述调用链的处理最后调用到 TcpTransport 的oneway(command) ,我们一步一步来看看都做了些什么:
ResponseCorrelator.oneway(command):里面就设置了两个属性
public void oneway(Object o) throws IOException {
Command command = (Command)o; //对前面的对象做一个强转,组装一些信息
command.setCommandId(sequenceGenerator.getNextSequenceId());
command.setResponseRequired(false);
next.oneway(command);
}
MutexTransport.oneway(command):
public void oneway(Object command) throws IOException {
writeLock.lock();// 通过 ReentrantLock做加锁
try {
next.oneway(command);
} finally {
writeLock.unlock();
}
}
WireFormatNegotiator.oneway(command):这个里面调用了父类的 oneway ,父类是 TransportFilter 类
public void oneway(Object command) throws IOException {
boolean wasInterrupted = Thread.interrupted();
try {
if (readyCountDownLatch.getCount() > 0 && !readyCountDownLatch.await(negotiateTimeout, TimeUnit.MILLISECONDS)) {
throw new IOException("Wire format negotiation timeout: peer did not send his wire format.");
}
} catch (InterruptedException e) {
InterruptedIOException interruptedIOException = new InterruptedIOException("Interrupted waiting for wire format negotiation");
interruptedIOException.initCause(e);
try {
onException(interruptedIOException);
} finally {
Thread.currentThread().interrupt();
wasInterrupted = false;
}
throw interruptedIOException;
} finally {
if (wasInterrupted) {
Thread.currentThread().interrupt();
}
}
super.oneway(command); //里面没做什么事情进入下一个调用链
}
从WireFormatNegotiator的父类TransportFilter进入下一个调用链应该调用的是InactivityMonitor.oneway(command),可是并未发现又该类实现,所以这里进入InactivityMonitor 的父类AbstractInactivityMonitor:
public void oneway(Object o) throws IOException {
// To prevent the inactivity monitor from sending a message while we
// are performing a send we take a read lock. The inactivity monitor
// sends its Heart-beat commands under a write lock. This means that
// the MutexTransport is still responsible for synchronizing sends
sendLock.readLock().lock();//获取发送读锁 锁定
inSend.set(true);//设置属性
try {
doOnewaySend(o);//通过这个逻辑进入下一个调用链
} finally {
commandSent.set(true);
inSend.set(false);
sendLock.readLock().unlock();
}
}
在doOnewaySend 里面的next.oneway(command) 方法最终调用 TcpTransport 的实现:
public void oneway(Object command) throws IOException {
checkStarted();
//进行格式化内容 通过Sokct 发送
wireFormat.marshal(command, dataOut);
// 流的刷新
dataOut.flush();
}
最后通过Sokect进行数据的传输。这样子异步发送的流程就结束了。下面来走一下同步的流程:通过this.connection.syncSendPacket() 进入同步发送流程。
public Response syncSendPacket(Command command, int timeout) throws JMSException {
if (isClosed()) {
throw new ConnectionClosedException();
} else {
try {// 进行发送,阻塞获取结果
Response response = (Response)(timeout > 0
? this.transport.request(command, timeout)
: this.transport.request(command));
if (response.isException()) {
ExceptionResponse er = (ExceptionResponse)response;
if (er.getException() instanceof JMSException) {
throw (JMSException)er.getException();
}
。。。。。。。。。
return response;
} catch (IOException e) {
throw JMSExceptionSupport.create(e);
}
}
}
这里的 transport 跟异步发送过程中的transport时一样的,即 ResponseCorrelator(MutexTransport(WireFormatNegotiator(IactivityMonitor(TcpTransport())) 一个调用链,进入ResponseCorrelator 的实现:
public Object request(Object command, int timeout) throws IOException {
FutureResponse response = asyncRequest(command, null);
return response.getResult(timeout);
}
从这个方法我们可以得到的信息时,在发送的时候采用的是 asyncRequest 方法,意思是异步请求,但是在下行采用 response.getResult(timeout) 去同步阻塞的方式去获取结果:
public Response getResult(int timeout) throws IOException {
final boolean wasInterrupted = Thread.interrupted();
try {
Response result = responseSlot.poll(timeout, TimeUnit.MILLISECONDS);
.........
}
这里会从 ArrayBlockingQueue 去 阻塞的处理请求。其实这里的同步发送实质上采用的不阻塞发送,阻塞的去等待broker 的反馈结果。
当生产者调用send发送消息时,首先会判断producerWindowSize是否还有空间,若没有了就阻塞等待空间;反之则继续判断是否是异步发送消息,如果是同步,则直接通过底层传输协议传输消息,并阻塞等待response结果;如果是异步发送,会去增加producerWindowSize的值,然后同样通过底层传输协议传输消息,但不再需要阻塞等待response。最后整理一下这个发送流程图
有时候我们不希望消息马上被broker投递出去,而是想要消息60秒以后发给消费者,或者我们想让消息没隔一定时间投递一次,一共投递指定的次数。。。类似这种需求,ActiveMQ提供了一种broker端消息定时调度机制。我们只需要把几个描述消息定时调度方式的参数作为属性添加到消息,broker端的调度器就会按照我们想要的行为去处理消息。当然需要在xml中配置schedulerSupport属性为true(broker的属性)即:
< broker schedulerSupport=“true”>
使用延迟消息必须遵守如下配置属性:
属性名称 | 类型 | 描述 |
---|---|---|
AMQ_SCHEDULED_DELAY | (long) | 消息延迟时间单位:毫秒 |
AMQ_SCHEDULED_PERIOD | ( long) | 消息发送周期单位时间:毫秒。如 5秒一次 配置 AMQ_SCHEDULED_PERIOD = 5*1000 |
AMQ_SCHEDULED_REPEAT | (int) | 消息重复发送次数 |
AMQ_SCHEDULED_CRON | (string) | 使用Cron 表达式 设置定时发送 |
延迟60秒发送消息
MessageProducer producer = session.createProducer(destination);
TextMessage message = session.createTextMessage("test msg");
long time = 60 * 1000;
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, time);
producer.send(message);
开始延迟30秒发送,重复发送10次,每次之间间隔10秒
MessageProducer producer = session.createProducer(destination);
TextMessage message = session.createTextMessage("test msg");
long delay = 30 * 1000;
long period = 10 * 1000;
int repeat = 9;
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delay);
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, period);
message.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, repeat);
producer.send(message);
使用Cron 表示式定时发送消息
MessageProducer producer = session.createProducer(destination);
TextMessage message = session.createTextMessage("test msg");
message.setStringProperty(ScheduledMessage.AMQ_SCHEDULED_CRON, "0 * * * *");
producer.send(message);
Cron 的优先级大于消息延迟,只要设置了Cron 表达式会优先执行Cron规则,如下:消息定时发送10次,每个小时执行,延迟1秒之后发送。
MessageProducer producer = session.createProducer(destination);
TextMessage message = session.createTextMessage("test msg");
message.setStringProperty(ScheduledMessage.AMQ_SCHEDULED_CRON, "0 * * * *");
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, 1000);
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, 1000);
message.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, 9);
producer.send(message);
正常情况下,非持久化消息是存储在内存中的,持久化消息是存储在文件中的。能够存储的最大消息数据在$ {ActiveMQ_HOME}/conf/activemq.xml文件中的systemUsage节点SystemUsage配置设置了一些系统内存和硬盘容量。
//该子标记设置整个ActiveMQ节点的“可用内存限制”。这个值不能超过ActiveMQ本身设置的最大内存大小。其中的
percentOfJvmHeap属性表示百分比。占用70%的堆内存
//该标记设置整个ActiveMQ节点,用于存储“持久化消息”的“可用磁盘空间”。该子标记的limit属性必须要进行设置
//一旦ActiveMQ服务节点存储的消息达到了memoryUsage的限制,非持久化消息就会被转储到 temp store区域,虽然//我们说过非持久化消息不进行持久化存储,但是ActiveMQ为了防止“数据洪峰”出现时非持久化消息大量堆积致使内存耗尽的情况出现,还是会将非持久化消息写入到磁盘的临时区域——temp store。这个子标记就是为了设置这个temp store区域的“可用磁盘空间限制”
从上面的配置我们需要得到一个结论,当非持久化消息堆积到一定程度的时候,也就是内存超过指定的设置阀值时,ActiveMQ会将内存中的非持久化消息写入到临时文件,以便腾出内存。但是它和持久化消息的区别是,重启之后,持久化消息会从文件中恢复,非持久化的临时文件会直接删除。
消息持久性对于可靠消息传递来说是一种比较好的方法,即时发送者和接受者不是同时在线或者消息中心在发送者发送消息后宕机了,在消息中心重启后仍然可以将消息发送出去。消息持久性的原理很简单,就是在发送消息出去后,消息中心首先将消息存储在本地文件、内存或者远程数据库,然后把消息发送给接受者,发送成功后再把消息从存储中删除,失败则继续尝试
。
接下来我们来了解一下消息在broker上的持久化存储实现方式。
ActiveMQ支持多种不同的持久化方式,主要有以下几种,不过,无论使用哪种持久化方式,消息的存储逻辑都是一致的。
KahaDB是目前默认的存储方式,可用于任何场景,提高了性能和恢复能力。消息存储使用一个事务日志和仅仅用一个索引文件来存储它所有的地址。KahaDB是一个专门针对消息持久化的解决方案,它对典型的消息使用模式进行了优化。在Kaha中,数据被追加到data logs中。当不再需要log文件中的数据的时候,log文件会被丢弃。
配置方式:在$ {ActiveMQ_HOME}/conf/activemq.xml文件中:
KahaDB的存储原理:在data/kahadb这个目录下,会生成四个文件
使用JDBC持久化方式,数据库会创建3个表:activemq_msgs,activemq_acks和activemq_lock。
JDBC存储实践:
dataSource指定持久化数据库的bean,createTablesOnStartup是否在启动的时候创建数据表,默认值是true,这样每次启动都会去创建数据表了,一般是第一次启动的时候设置为true,之后改成false
Mysql持久化Bean配置:
配置完以后需要往 ${ActiveMQ_HOME}/lib 文件夹中添加相应 jar 包:然后重启就OK了。
LevelDB持久化性能高于KahaDB,虽然目前默认的持久化方式仍然是KahaDB。并且,在ActiveMQ 5.9版本提供了基于LevelDB和Zookeeper的数据复制方式,用于Master-slave方式的首选数据复制方案。不过,据ActiveMQ官网对LevelDB的表述:LevelDB官方建议使用以及不再支持,推荐使用的是KahaDB。
基于内存的消息存储,内存消息存储主要是存储所有的持久化的消息在内存中。persistent=”false”,表示不设置持久化存储,直接存储到内存中。
这种方式克服了JDBC Store的不足,JDBC每次消息过来,都需要去写库和读库。ActiveMQ Journal,使用高速缓存写入技术,大大提高了性能。当消费者的消费速度能够及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。举个例子,生产者生产了1000条消息,这1000条消息会保存到journal文件,如果消费者的消费速度很快的情况下,在journal文件还没有同步到DB之前,消费者已经消费了90%的以上的消息,那么这个时候只需要同步剩余的10%的消息到DB。如果消费者的消费速度很慢,这个时候journal文件可以使消息以批量方式写到DB。
将原来的 persistenceAdapter 标签注释掉,添加如下标签
ActiveMQ有两种方法可以接收消息,一种是使用同步阻塞的ActiveMQMessageConsumer#receive方法。另一种是使用消息监听器MessageListener。这里需要注意的是,在同一个session下,这两者不能同时工作,也就是说不能针对不同消息采用不同的接收方式。否则会抛出异常。至于为什么这么做,最大的原因还是在事务性会话中,两种消费模式的事务不好管控。
先通过ActiveMQMessageConsumer#receive 方法来对消息的接受一探究竟:
public Message receive() throws JMSException {
checkClosed();
//检查receive和MessageListener是否同时配置在当前的会话中,有则抛出异常
checkMessageListener();
//如果PrefetchSize为0并且unconsumerMessage为空,则发起pull命令
sendPullCommand(0);
MessageDispatch md = dequeue(-1);//出列,获取消息
if (md == null) {
return null;
}
beforeMessageIsConsumed(md);
//发送ack给到broker
afterMessageIsConsumed(md, false);
//获取消息并返回
return createActiveMQMessage(md);
}
下面简单的说一下以上几个核心方法中做了什么不为人知的事:
sendPullCommand(0) :发送pull命令从broker上获取消息,前提是prefetchSize=0并且unconsumedMessages为空。unconsumedMessage表示未消费的消息,这里面预读取的消息大小为prefetchSize的值
protected void sendPullCommand(long timeout) throws JMSException {
clearDeliveredList();
if (info.getCurrentPrefetchSize() == 0 && unconsumedMessages.isEmpty()) {
MessagePull messagePull = new MessagePull(); messagePull.configure(info);
messagePull.setTimeout(timeout);
//向服务端异步发送messagePull指令
session.asyncSendPacket(messagePull);
}
}
这里发送异步消息跟消息生产的原理是一样的。通过包装链去调用 Sokect 发送请求。
clearDeliveredList():
在上面的sendPullCommand方法中,会先调用clearDeliveredList方法,主要用来清理已经分发的消息链表deliveredMessages,存储分发给消费者但还为应答的消息链表
如果session是事务的,则会遍历deliveredMessage中的消息放入到previouslyDeliveredMessage中来做重发
如果session是非事务的,根据ACK的模式来选择不同的应答操作
这是个同步的过程:
private void clearDeliveredList() {
if (clearDeliveredList) {//判断是否清除
synchronized (deliveredMessages) {//采用双重检查锁
if (clearDeliveredList) {
if (!deliveredMessages.isEmpty()) {
if (session.isTransacted()) {//是事务消息
if (previouslyDeliveredMessages == null) {
previouslyDeliveredMessages = new PreviouslyDeliveredMap(session.getTransactionContext().getTransactionId());
}
for (MessageDispatch delivered : deliveredMessages) {
previouslyDeliveredMessages.put(delivered.getMessage().getMessageId(), false);
}
LOG.debug("{} tracking existing transacted {} delivered list ({}) on transport interrupt",getConsumerId(),previouslyDeliveredMessages.transactionId,deliveredMessages.size());
} else {
if (session.isClientAcknowledge()) {
LOG.debug("{} rolling back delivered list ({}) on transport interrupt", getConsumerId(), deliveredMessages.size());
// allow redelivery
if (!this.info.isBrowser()) {
for (MessageDispatch md: deliveredMessages) {
this.session.connection.rollbackDuplicate(this, md.getMessage());
}
}
}
LOG.debug("{} clearing delivered list ({}) on transport interrupt", getConsumerId(), deliveredMessages.size());
deliveredMessages.clear();
pendingAck = null;
}
}
clearDeliveredList = false;
}
}
}
}
dequeue(-1) :
从unconsumedMessage中取出一个消息,在创建一个消费者时,就会为这个消费者创建一个未消费的消息通道,这个通道分为两种,一种是简单优先级队列分发通道SimplePriorityMessageDispatchChannel ;另一种是先进先出的分发通道FifoMessageDispatchChannel.至于为什么要存在这样一个消息分发通道,大家可以想象一下,如果消费者每次去消费完一个消息以后再去broker拿一个消息,效率是比较低的。所以通过这样的设计可以允许session能够一次性将多条消息分发给一个消费者。默认情况下对于queue来说,prefetchSize的值是1000
private MessageDispatch dequeue(long timeout) throws JMSException {
try {
long deadline = 0;
if (timeout > 0) {
deadline = System.currentTimeMillis() + timeout; }
while (true) {//protected final MessageDispatchChannel unconsumedMessages;
MessageDispatch md = unconsumedMessages.dequeue(timeout);
...........
}
beforeMessageIsConsumed(md):
这里面主要是做消息消费之前的一些准备工作,如果ACK类型不是DUPS_OK_ACKNOWLEDGE或者队列模式(简单来说就是除了Topic和DupAck这两种情况),所有的消息先放到deliveredMessages链表的开头。并且如果当前是事务类型的会话,则判断transactedIndividualAck,如果为true,表示单条消息直接返回ack。
否则,调用ackLater,批量应答, client端在消费消息后暂且不发送ACK,而是把它缓存下来(pendingACK),等到这些消息的条数达到一定阀值时,只需要通过一个ACK指令把它们全部确认;这比对每条消息都逐个确认,在性能上要提高很多。
private void beforeMessageIsConsumed(MessageDispatch md) throws JMSException {
md.setDeliverySequenceId(session.getNextDeliveryId()); lastDeliveredSequenceId = md.getMessage().getMessageId().getBrokerSequenceId();
if (!isAutoAcknowledgeBatch()) {
synchronized(deliveredMessages) {
deliveredMessages.addFirst(md);
}
if (session.getTransacted()) {
if (transactedIndividualAck) {
immediateIndividualTransactedAck(md);
} else {
ackLater(md, MessageAck.DELIVERED_ACK_TYPE);
}
}
}
}
afterMessageIsConsumed:这个方法的主要作用是执行应答操作,这里面做以下几个操作
private void afterMessageIsConsumed(MessageDispatch md, boolean messageExpired) throws JMSException {
if (unconsumedMessages.isClosed()) {
return;
}
if (messageExpired) {
acknowledge(md, MessageAck.EXPIRED_ACK_TYPE);
stats.getExpiredMessageCount().increment();
} else {
stats.onMessage();
if (session.getTransacted()) {
// Do nothing.
} else if (isAutoAcknowledgeEach()) {
if (deliveryingAcknowledgements.compareAndSet(false, true)) {
synchronized (deliveredMessages) {
if (!deliveredMessages.isEmpty()) {
if (optimizeAcknowledge) {
ackCounter++;
// AMQ-3956 evaluate both expired and normal msgs as
// otherwise consumer may get stalled
if (ackCounter + deliveredCounter >= (info.getPrefetchSize() * .65) || (optimizeAcknowledgeTimeOut > 0 && System.currentTimeMillis() >= (optimizeAckTimestamp + optimizeAcknowledgeTimeOut))) {
MessageAck ack = makeAckForAllDeliveredMessages(MessageAck.STANDARD_ACK_TYPE);
if (ack != null) {
deliveredMessages.clear();
ackCounter = 0;
session.sendAck(ack);
optimizeAckTimestamp = System.currentTimeMillis();
}
// AMQ-3956 - as further optimization send
// ack for expired msgs when there are any.
// This resets the deliveredCounter to 0 so that
// we won't sent standard acks with every msg just
// because the deliveredCounter just below
// 0.5 * prefetch as used in ackLater()
if (pendingAck != null && deliveredCounter > 0) {
session.sendAck(pendingAck);
pendingAck = null;
deliveredCounter = 0;
}
}
} else {
MessageAck ack = makeAckForAllDeliveredMessages(MessageAck.STANDARD_ACK_TYPE);
if (ack!=null) {
deliveredMessages.clear();
session.sendAck(ack);
}
}
}
}
deliveryingAcknowledgements.set(false);
}
} else if (isAutoAcknowledgeBatch()) {
ackLater(md, MessageAck.STANDARD_ACK_TYPE);
} else if (session.isClientAcknowledge()||session.isIndividualAcknowledge()) {
boolean messageUnackedByConsumer = false;
synchronized (deliveredMessages) {
messageUnackedByConsumer = deliveredMessages.contains(md);
}
if (messageUnackedByConsumer) {
ackLater(md, MessageAck.DELIVERED_ACK_TYPE);
}
}
else {
throw new IllegalStateException("Invalid session state.");
}
}
}
其实在以上消息的接收过程中,我们仅仅能看到这个消息从一个本地变量中出队,并没有对远程消息中心发送通讯获取,那么这个消息时什么时候过来的呢?也就是消息出队中 unconsumedMessages 这个东东时什么时候初始化的呢 ?那么接下去我们应该去通过创建连接的时候去看看了,具体连接的时候都做了什么呢:connectionFactory.createConnection()
protected ActiveMQConnection createActiveMQConnection(String userName, String password) throws JMSException {
if (brokerURL == null) {
throw new ConfigurationException("brokerURL not set.");
}
ActiveMQConnection connection = null;
try {// 果然发现了这个东东的初始化
Transport transport = createTransport();
// 创建连接
connection = createActiveMQConnection(transport, factoryStats);
// 设置用户密码
connection.setUserName(userName);
connection.setPassword(password);
// 对连接做包装
configureConnection(connection);
// 启动一个后台传输线程
transport.start();
// 设置客户端消费的id
if (clientID != null) {
connection.setDefaultClientID(clientID);
}
return connection;
} ......
}
创建连接的过程就是创建除了一个带有链路包装的TcpTransport 并且创建连接,最后启动一个传输线程,而这里的 transport.start() 调用的应该是TcpTransport 里面的方法,然而这个类中并没有 start,而是在父类
ServiceSupport.start()中:
public void start() throws Exception {
if (started.compareAndSet(false, true)) {
boolean success = false;
stopped.set(false);
try {
preStart();//一些初始化
doStart();
success = true;
} finally {
started.set(success);
}
for(ServiceListener l:this.serviceListeners) {
l.started(this);
}
}
}
doStart 方法前做了一系列的初始化,然后调用 TcpTransport的doStart() 方法:
protected void doStart() throws Exception {
connect();
stoppedLatch.set(new CountDownLatch(1));
super.doStart();
}
继而构建一个连接 设置一个 CountDownLatch 门闩 ,调用父类 TransportThreadSupport 的方法,新建了一个精灵线程并且启动:
protected void doStart() throws Exception {
runner = new Thread(null, this, "ActiveMQ Transport: " + toString(), stackSize);
runner.setDaemon(daemon);
runner.start();
}
调用TransportThreadSupport.doStart(). 创建了一个线程,传入的是 this,调用子类的 run 方法,也就是 TcpTransport.run().
public void run() {
LOG.trace("TCP consumer thread for " + this + " starting");
this.runnerThread=Thread.currentThread();
try {
while (!isStopped()) {
doRun();
}
} catch (IOException e) {
stoppedLatch.get().countDown();
onException(e);
} catch (Throwable e){
stoppedLatch.get().countDown();
IOException ioe=new IOException("Unexpected error occurred: " + e);
ioe.initCause(e);
onException(ioe);
}finally {
stoppedLatch.get().countDown();
}
}
run 方法主要是从 socket 中读取数据包,只要 TcpTransport 没有停止,它就会不断去调用 doRun:这里面,通过 wireFormat 对数据进行格式化,可以认为这是一个反序列化过程。wireFormat 默认实现是 OpenWireFormat,activeMQ 自定义的跨语言的wire 协议
protected void doRun() throws IOException {
try {//通过 readCommand 去读取数据
Object command = readCommand();
//消费消息
doConsume(command);
} catch (SocketTimeoutException e) {
} catch (InterruptedIOException e) {
}
}
protected Object readCommand() throws IOException {
return wireFormat.unmarshal(dataIn);
}
doConsume:流程走到了消费消息:
public void doConsume(Object command) {
if (command != null) {//表示已经拿到了消息
if (transportListener != null) {
transportListener.onCommand(command);
} else {
LOG.error("No transportListener available to process inbound command: " + command);
}
}
}
TransportSupport 类中唯一的成员变量是 TransportListener transportListener;,这也意味着一个 Transport 支持类绑定一个传送监听器类,传送监听器接口 TransportListener 最重要的方法就是 void onCommand(Object command);,它用来处理命令。那么这个 transportListener 是在那里初始化的呢?可以思考一下 既然是TransportSupport 唯一的成员变量,而我们锁创建的TcpTransport 是他的子类,那么是不是在创建该transport的时候亦或是在对他进行包装处理的时候做了初始化呢? 我们会在流程中看到在新建 ActiveMQConnectionFactory 的时候有一行关键的代码:
connection = createActiveMQConnection(transport, factoryStats);
在这个方法六面追溯下去:会进入 ActiveMQConnection 的构造方法
protected ActiveMQConnection(final Transport transport, IdGenerator clientIdGenerator, IdGenerator connectionIdGenerator, JMSStatsImpl factoryStats) throws Exception {
this.transport = transport;
this.clientIdGenerator = clientIdGenerator;
this.factoryStats = factoryStats;
// Configure a single threaded executor who's core thread can timeout if
// idle
executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.SECONDS, new LinkedBlockingQueue(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "ActiveMQ Connection Executor: " + transport);
//Don't make these daemon threads - see https://issues.apache.org/jira/browse/AMQ-796
//thread.setDaemon(true);
return thread;
}
});
// asyncConnectionThread.allowCoreThreadTimeOut(true);
String uniqueId = connectionIdGenerator.generateId();
this.info = new ConnectionInfo(new ConnectionId(uniqueId));
this.info.setManageable(true);
this.info.setFaultTolerant(transport.isFaultTolerant());
this.connectionSessionId = new SessionId(info.getConnectionId(), -1);
this.transport.setTransportListener(this);
this.stats = new JMSConnectionStatsImpl(sessions, this instanceof XAConnection);
this.factoryStats.addConnection(this);
this.timeCreated = System.currentTimeMillis();
this.connectionAudit.setCheckForDuplicates(transport.isFaultTolerant());
}
从以上代码我们发现 this.transport.setTransportListener(this); 那么这个this是什么呢 ? 正是ActiveMQConnection ,看了一眼该类,发现这个类实现了 TransportListener ,本身就是一个TransportListener。所以上面 transportListener.onCommand(command); 就是 ActiveMQConnection.onCommand(command)。除了和 Transport相互绑定,还对线程池执行器 executor 进行了初始化。这哥执行器是后来要进行消息处理的。
这里面会针对不同的消息做分发,在ActiveMQMessageConsumer#receive方法中锁dequeue所返回的对象是MessageDispatch 。假设这里传入的 command 是MessageDispatch,那么这个 command 的 visit 方法就会调用processMessageDispatch 方法。剪切出其中的代码片段:
public Response processMessageDispatch(MessageDispatch md) throws Exception {
// 等待 Transport 中断处理完成
waitForTransportInterruptionProcessingToComplete();
// 这里通过消费者 ID 来获取消费者对象
//(ActiveMQMessageConsumer 实现了 ActiveMQDispatcher 接口),所以
//MessageDispatch 包含了消息应该被分配到那个消费者的映射信息
ActiveMQDispatcher dispatcher = dispatchers.get(md.getConsumerId());
if (dispatcher != null) {
// Copy in case a embedded broker is dispatching via
// vm://
// md.getMessage() == null to signal end of queue
// browse.
Message msg = md.getMessage();
if (msg != null) {
msg = msg.copy();
msg.setReadOnlyBody(true);
msg.setReadOnlyProperties(true);
msg.setRedeliveryCounter(md.getRedeliveryCounter());
msg.setConnection(ActiveMQConnection.this);
msg.setMemoryUsage(null);
md.setMessage(msg);
}
// 调用会话ActiveMQSession 自己的 dispatch 方法来处理这条消息
dispatcher.dispatch(md);
} else {
LOG.debug("{} no dispatcher for {} in {}", this, md, dispatchers);
}
return null;
}
其中 ActiveMQDispatcher dispatcher = dispatchers.get(md.getConsumerId());这行代码的 dispatchers 是在 通过session.createConsumer(destination); 的时候通过 ActiveMQMessageConsumer 的构造方法中有一行代码 :this.session.addConsumer(this); 将 this传入,即 ActiveMQMessageConsumer 对象。而这个 addConsumer 方法:
protected void addConsumer(ActiveMQMessageConsumer consumer) throws JMSException {
this.consumers.add(consumer);
if (consumer.isDurableSubscriber()) {
stats.onCreateDurableSubscriber();
}
this.connection.addDispatcher(consumer.getConsumerId(), this);
}
可以发现这里的初始化了:this.connection.addDispatcher(consumer.getConsumerId(), this); 这里的this 即 ActiveMQSession。所以回到 ActiveMQConnection#onCommand方法内 processMessageDispatch 这个方法最后调用了 dispatcher.dispatch(md); 这个方法的核心功能就是处理消息的分发。:
public void dispatch(MessageDispatch messageDispatch) {
try {
executor.execute(messageDispatch);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
connection.onClientInternalException(e);
}
}
这里离我们真正要找的进行消息入队的结果很近了,进入executor.execute(messageDispatch);这个方法:
void execute(MessageDispatch message) throws InterruptedException {
...........
//如果会话不是异步分发并且没有使用 sessionpool 分发,则调用 dispatch 发送消息
if (!session.isSessionAsyncDispatch() && !dispatchedBySessionPool) {
dispatch(message);
} else {//将消息直接放到队列里
messageQueue.enqueue(message);
wakeup();
}
}
这里最后终于发现了入队,判断是否异步分发,不是的话走dispatch(message) 否则进入异步分发。默认是采用异步消息分发。所以,直接调用 messageQueue.enqueue,把消息放到队列中,并且调用 wakeup 方法:
public void wakeup() {
if (!dispatchedBySessionPool) {//进一步验证
//判断 session 是否为异步分发
if (session.isSessionAsyncDispatch()) {
try {
TaskRunner taskRunner = this.taskRunner;
if (taskRunner == null) {
synchronized (this) {
if (this.taskRunner == null) {
if (!isRunning()) {
// stop has been called
return;
}
//通过 TaskRunnerFactory 创建了一个任务运行类 taskRunner,这里把自己作为一个 task 传入到 createTaskRunner 中,
//说明当前的类一定是实现了 Task 接口的. 简单来说,就是通过线程池去执行一个任务,完成异步调度
//这里由于executor != null 所以这个task的类型是PooledTaskRunner
this.taskRunner = session.connection.getSessionTaskRunner().createTaskRunner(this,
"ActiveMQ Session: " + session.getSessionId());
}
taskRunner = this.taskRunner;
}
}
taskRunner.wakeup();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {// 异步分发
while (iterate()) {
}
}
}
}
所以,对于异步分发的方式,会调用 ActiveMQSessionExecutor 中的 iterate方法,我们来看看这个方法的代码 iterate ():这个方法里面做两个事
public boolean iterate() {
// Deliver any messages queued on the consumer to their listeners.
// 将消费者上排队的任何消息传递给它们的侦听器。
for (ActiveMQMessageConsumer consumer : this.session.consumers) {
if (consumer.iterate()) {
return true;
}
}
// No messages left queued on the listeners.. so now dispatch messages
// queued on the session
// 侦听器上没有留下排队等待的消息。现在分派消息
MessageDispatch message = messageQueue.dequeueNoWait();
if (message == null) {
return false;
} else {// 分发(调度)消息
dispatch(message);
return !messageQueue.isEmpty();
}
}
dispatch(message);消息确认分发。通过ActiveMQSessionExecutor的dispatch 方法,转到了 ActiveMQMessageConsumer 消费者类的 dispatch 方法:
public void dispatch(MessageDispatch md) {
MessageListener listener = this.messageListener.get();
try {
clearMessagesInProgress();
clearDeliveredList();
synchronized (unconsumedMessages.getMutex()) {
if (!unconsumedMessages.isClosed()) {// 判断消息是否为重发消息
if (this.info.isBrowser() || !session.connection.isDuplicate(this, md.getMessage())) {
if (listener != null && unconsumedMessages.isRunning()) {
//我这边通过consumer.receive()处理消息,所以这里listener为空,走下面
} else {
if (!unconsumedMessages.isRunning()) {
// delayed redelivery, ensure it can be re delivered
session.connection.rollbackDuplicate(this, md.getMessage());
}
if (md.getMessage() == null) {
// End of browse or pull request timeout.
unconsumedMessages.enqueue(md);
} else {
if (!consumeExpiredMessage(md)) {
unconsumedMessages.enqueue(md);
if (availableListener != null) {
availableListener.onMessageAvailable(this);
}
.........
}
最终会走入 unconsumedMessages.enqueue(md);添加消息。这里需要注意的是enqueue 方法:由于消费者可能处于阻塞状态,这里做了入队后回释放锁,也就是接触阻塞。
public void enqueue(MessageDispatch message) {
synchronized (mutex) {
list.addLast(message);
mutex.notify();
}
}
到这里为止,消息如何接受以及他的处理方式的流程,我们已经搞清楚了。其实在这个消息消费的流程中,已经在建立连接,创建消费者的时候就已经初始化好了消息队列了。
消费者在通过receive消费消息时,并不是直接去broker上获取的消息,而是从本地的unconsumerMessage队列中获取,而该队列则是每次批量从broker上拉取消息,每次拉取的数量就是由prefetchSize控制的。当队列中没有消息时,就会阻塞等待获取消息;反之则依次从unconsumerMessage队列中取出消息消费,并将应答放到delivered队列返回给broker,消费消息和ack是异步的。整个消费流程的流程图:
在消息发布的时候我们曾经研究过 producerWindowSize 。主要用来约束在异步发送时producer端允许积压的(尚未ACK)的消息的大小,且只对异步发送有意义。对于客户端,也是类似存在这么一个属性来约束客户端的消息处理。activemq 的 consumer 端也有窗口机制,通过 prefetchSize 就可以设置窗口大小。不同的类型的队列,prefetchSize 的默认值也是不一样的
测试方法是在MQ上生产1000条消息,先后启动comsumer1,comsumer2 两个消费者并且循环调用1000次消费,就会发现 comsumer2 拿不到消息,这个时候我们可以通过debug进入comsumer1 的ActiveMQConnect会发现里面有个属性的size=1000.其实就是这个prefetchSize,翻译过来是预取大小,消费端会根据prefetchSize 的大小批量获取数据。意思是在创建连接的时候会取获取1000条消息预加载到缓存中等待处理,这样子导致comsumer2去获取消息的时候 broker上已经空了。
prefetchSize 的设置方法:
在 createQueue 中添加 consumer.prefetchSize,就可以看到效果
Destination destination=session.createQueue("myQueue?consumer.prefetchSize=10");
既然有批量加载,那么一定有批量确认,这样才算是彻底的优化,这就涉及到 optimizeAcknowledge。
ActiveMQ 提供了 optimizeAcknowledge 来优化确认,它表示是否开启“优化ACK”,只有在为 true 的情况下,prefetchSize 以及optimizeAcknowledgeTimeout 参数才会有意义优化确认一方面可以减轻 client 负担(不需要频繁的确认消息)、减少通信开销,另一方面由于延迟了确认(默认 ack 了 0.65*prefetchSize 个消息才确认),这个在源码中有体现。在ActiveMQMessageConsumer#receive方法内的处理消息后的 afterMessageIsConsumed 方法内有一个判断:
if (ackCounter + deliveredCounter >= (info.getPrefetchSize() * .65) ||
(optimizeAcknowledgeTimeOut > 0 &&
System.currentTimeMillis() >= (optimizeAckTimestamp + optimizeAcknowledgeTimeOut))) {
MessageAck ack = makeAckForAllDeliveredMessages(MessageAck.STANDARD_ACK_TYPE);
if (ack != null) {
deliveredMessages.clear();
ackCounter = 0;
session.sendAck(ack);//满足条件则发送批量应答ACK
optimizeAckTimestamp = System.currentTimeMillis();
}
// AMQ-3956 - as further optimization send
// ack for expired msgs when there are any.
// This resets the deliveredCounter to 0 so that
// we won't sent standard acks with every msg just
// because the deliveredCounter just below
// 0.5 * prefetch as used in ackLater()
if (pendingAck != null && deliveredCounter > 0) {
session.sendAck(pendingAck);
pendingAck = null;
deliveredCounter = 0;
}
}
broker 再次发送消息时又可以批量发送如果只是开启了 prefetchSize,每条消息都去确认的话,broker 在收到确认后也只是发送一条消息,并不是批量发布,当然也可以通过设置 DUPS_OK_ACK来手动延迟确认, 我们需要在 brokerUrl 指定 optimizeACK 选项
ConnectionFactory connectionFactory= new ActiveMQConnectionFactory("tcp://192.168.11.153:61616?
jms.optimizeAcknowledge=true&jms.optimizeAcknowledgeTimeOut=10000");
注意,如果 optimizeAcknowledge 为 true,那么 prefetchSize 必须大于 0. 当 prefetchSize=0 的时候,表示 consumer 通过 PULL 方式从 broker 获取消息.
optimizeAcknowledge 和 prefetchSize 的作用,两者协同工作,通过批量获取消息、并延迟批量确认,来达到一个高效的消息消费模型。它比仅减少了客户端在获取消息时的阻塞次数,还能减少每次获取消息时的网络通信开销。由此可见,prefetch优化了消息传送的性能,optimizeACK优化了消息确认的性能
。
需要注意的是,如果消费端的消费速度比较高,通过这两者组合是能大大提升 consumer 的性能。如果 consumer 的消费性能本身就比较慢,设置比较大的 prefetchSize 反而不能有效的达到提升消费性能的目的。因为过大的prefetchSize 不利于 consumer 端消息的负载均衡。因为通常情况下,我们都会部署多个 consumer 节点来提升消费端的消费性能。这个优化方案还会存在另外一个潜在风险,当消息被消费之后还没有来得及确认时,client 端发生故障,那么这些消息就有可能会被重新发送给其他consumer,那么这种风险就需要 client 端能够容忍“重复”消息。
消息确认有四种 ACK_MODE,分别是:
ACK_MODE 的选择影响着消息消费流程的走向。虽然 Client 端指定了 ACK 模式,但是在 Client 与 broker 在交换 ACK 指令的时候,还需要告知 ACK_TYPE,ACK_TYPE 表示此确认指令的类型,不同的ACK_TYPE 将传递着消息的状态,broker 可以根据不同的 ACK_TYPE 对消息进行不同的操作。
if (this.info.isBrowser() || !session.connection.isDuplicate(this, md.getMessage())) {
if (listener != null && unconsumedMessages.isRunning()) {
// 这段为非重发消息,走else
} else {
// deal with duplicate delivery
ConsumerId consumerWithPendingTransaction;
if (redeliveryExpectedInCurrentTransaction(md, true)) {
LOG.debug("{} tracking transacted redelivery {}", getConsumerId(), md.getMessage());
if (transactedIndividualAck) {
immediateIndividualTransactedAck(md);
} else {
session.sendAck(new MessageAck(md, MessageAck.DELIVERED_ACK_TYPE, 1));
}
} else if ((consumerWithPendingTransaction = redeliveryPendingInCompetingTransaction(md)) != null) {
LOG.warn("{} delivering duplicate {}, pending transaction completion on {} will rollback", getConsumerId(), md.getMessage(), consumerWithPendingTransaction);
session.getConnection().rollbackDuplicate(this, md.getMessage());
dispatch(md);
} else {// 走POSION_ACK_TYPE 添加Active_DLQ 死信队列
LOG.warn("{} suppressing duplicate delivery on connection, poison acking: {}", getConsumerId(), md);
posionAck(md, "Suppressing duplicate delivery on connection, consumer " + getConsumerId());
}
}
Client 端在不同的 ACK 模式时,将意味着在不同的时机发送 ACK 指令,每个 ACK Command 中会包含 ACK_TYPE,那么 broker 端就可以根据 ACK_TYPE 来决定此消息的后续操作。在 afterMessageIsConsumed 消息接收处理后会根据条件来设置 ACK_TYPE.
首先从unconsumerMessage队列中取出消息并处理,若消费消息出现异常失败,消费者就会返回REDELIVERED_ACK_TYPE给broker,broker就会重发该条消息,当超过次数限制消费者就会返回POSION_ACK_TYPE告诉broker该条消息是有毒的,broker根据配置将该条消息抛弃或是加入死信队列中(该队列可以被重新消费);若消费消息成功未出现异常,就会将ack message添加到delivered队列中,消费该队列的消息时,会进行一系列判断并根据结果返回不同的ACK_TYPE。
在正常情况下,有几中情况会导致消息重新发送
一个消息被 redelivedred 超过默认的最大重发次数(默认 6 次)时,消费端会给 broker 发送一个”poison ack”表示这个消息有毒,告诉 broker 不要再发了。这个时候 broker 会把这个消息放到 DLQ(死信队列)。
ActiveMQ 中默认的死信队列是 ActiveMQ.DLQ,如果没有特别的配置,有毒的消息都会被发送到这个队列。默认情况下,如果持久消息过期以后,也会被送到 DLQ 中。
只要在处理消息的时候抛出一个异常就可以演示,会看到控制台对于失败消息会重发6次,登陆ActiveMQ控制台会看到一个 ActiveMQ.DLQ。在创建队列的时候可以直接指定从ActiveMQ.DLQ去消费消息。
死信队列配置策略:
缺省所有队列的死信消息都被发送到同一个缺省死信队列,不便于管理,可以通过 individualDeadLetterStrategy 或 sharedDeadLetterStrategy 策略来进行修改。在activemq.xml上
// “>”表示对所有队列生效,如果需要设置指定队列,则直接写队列名称
//queuePrefix:设置死信队列前缀
//useQueueForQueueMessage 设置队列保存到死信。
自动丢弃过期消息
实际场景:
整个项目中,自己处于consumer端,与另外一个consumer共同监听topic消息,发送的是VirtualTopic消息。
原来使用的 VirtualTopic.***监听不到消息,后请教同组大神,才知道要改成Consumer.***.VirtualTopic.***,监听到消息。
原因:
ActiveMQ支持的虚拟Destinations分为有两种,分别是
这两种虚拟Destinations可以看做对简单的topic和queue用法的补充,基于它们可以实现一些简单有用的EIP功能,虚拟主题类似于1对多的分支功能+消费端的cluster+failover,组合Destinations类似于简单的destinations直接的路由功能。
当你想把同一个消息一次发送到多个消息队列,那么可以在客户端使用组合队列。
// send to 3 queues as one logical operation
Queue queue = new ActiveMQQueue("FOO.A,FOO.B,FOO.C");
producer.send(queue, someMessage);
当然,也可以混合使用队列和主题,只需要使用前缀:queue:// 或 topic://
// send to queues and topic one logical operation
Queue queue = new ActiveMQQueue("FOO.A,topic://NOTIFY.FOO.A");
producer.send(queue, someMessage);
ActiveMQ中,topic只有在持久订阅(durablesubscription)下是持久化的。存在持久订阅时,每个持久订阅者,都相当于一个持久化的queue的客户端,它会收取所有消息。这种情况下存在两个问题:
为了解决这两个问题,ActiveMQ中实现了虚拟Topic的功能。使用起来非常简单。
对于消息发布者来说,就是一个正常的Topic,名称以VirtualTopic.开头。例如VirtualTopic.TEST。
对于消息接收端来说,是个队列,不同应用里使用不同的前缀作为队列的名称,即可表明自己的身份即可实现消费端应用分组。例如Consumer.A.VirtualTopic.TEST,说明它是名称为A的消费端,同理Consumer.B.VirtualTopic.TEST说明是一个名称为B的客户端。可以在同一个应用里使用多个consumer消费此queue,则可以实现上面两个功能。又因为不同应用使用的queue名称不同(前缀不同),所以不同的应用中都可以接收到全部的消息。每个客户端相当于一个持久订阅者,而且这个客户端可以使用多个消费者共同来承担消费任务。
默认虚拟主题的前缀是 :VirtualTopic.* 。自定义消费虚拟地址默认格式:Consumer.*.VirtualTopic.> 。自定义消费虚拟地址可以改,比如下面的配置就把它修改了。xml配置示例如下:
修改 activeMQ 服务器的 activeMQ.xml, 增加如下配置,这个配置只能实现单向连接,实现双向连接需要各个节点都配置如下配置。
两个 Brokers 通过一个 static 的协议来进行网络连接。一个 Consumer 连接到BrokerB 的一个地址上,当 Producer 在 BrokerA 上以相同的地址发送消息,此时消息会被转移到 BrokerB 上,也就是说 BrokerA 会转发消息到BrokerB 上。
在activeMQ中,进行了静态网络桥接的两台节点而言,当 Producer 在 BrokerA 上以相同的地址发送10条消息。一个 Consumer 连接到BrokerB去消费消息,当消费了一半的时候出现异常了,那么剩下来未处理的消息会被存放到 BrokerB 的待处理消息队列中,此时要通过BrokerA再去消费是消费不到的,万一此刻BrokerB 挂了,那么哪些没有消费的消息将会丢失。mq给我们提供了一个有效的消息回流机制。
ActiveMQ 采用消息推送方式,所以最适合的场景是默认消息都可在短时间内被消费。数据量越大,查找和消费消息就越慢,消息积压程度与消息速度成反比。
缺点
适用场景
不适用的场景
https://www.jianshu.com/p/cd8e037e11ff
https://www.iteye.com/blog/shift-alt-ctrl-2020182