一、下载安装:
操作系统:centos7
- wget http://archive.apache.org/dist/activemq/5.15.10/apache-activemq-5.15.10-bin.tar.gz
- tar -zxvf apache-activemq-5.15.10-bin.tar.gz
- ./activemq start > ../mylog.log 启动mq,并将日志记录进指定文件
- 防火墙开放 8161(默认访问端口,提供页面控制台服务)和 61616(默认进程端口,提供JMS服务)
- http://ip:8161/admin 进行访问,初始账号密码:admin/admin
二、使用
1、pom.xml文件引入activemq的jar包
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.10</version>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.16</version>
</dependency>
三、原理
底层是JMS,java消息服务,J2EE中的一个重要规范,定义了两个应用程序之间异步通信的API接口,使我们的程序可以到达异步、解耦、削峰的效果。
- JMS简介
消息模型分为2种:
- 点对点模式Point-to-Point(P2P),队列queue
- 发布/订阅模式Publish/Subscribe(Pub/Sub),topic主题
点对点模式Point-to-Point(P2P),队列queue
一条消息仅能对应一个消费者,多个消费者一个生产者的情况,每个消费者轮询获取消息;
消息被消费后,队列中不会再有该条消息;
消费者和生产者的启动顺序可以随意;
如果没有消费者,消息也不会被丢弃;
示例
创建一个点对点的生产者
String brokerUrl = "tcp://localhost:61616";
String queueName = "第一个消息队列";
// 1、创建连接工厂
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerUrl);
// 2、创建连接
Connection connection = connectionFactory.createConnection();
connection.start();
// 3、创建会话session 第一个参数:是否开启事务;第二个参数:应答模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4、创建目的地
Queue queue = session.createQueue(queueName);
// 5、创建消息生产者
MessageProducer producer = session.createProducer(queue);
// 6、创建3条消息
for (int i = 1; i <= 3; i++) {
TextMessage textMessage = session.createTextMessage();
textMessage.setText("第"+i+"条消息");
// 7、生产者发送消息
producer.send(textMessage);
}
// 8、关闭资源
producer.close();
session.close();
connection.close();
点对对消费者
public static void main(String[] args) throws JMSException, IOException {
String brokerUrl = "tcp://localhost:61616";
String queueName = "第一个消息队列";
// 1、创建连接工厂
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerUrl);
// 2、创建连接
Connection connection = connectionFactory.createConnection();
connection.start();
// 3、创建会话session 第一个参数:是否开启事务;第二个参数:应答模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4、创建目的地
Queue queue = session.createQueue(queueName);
// 5、创建消息消费者
MessageConsumer consumer = session.createConsumer(queue);
// 设置异步非阻塞消息监听器获取消息
consumer.setMessageListener((message)->{
TextMessage message1 = (TextMessage) message;
try {
System.out.println("我是消费者1:"+message1.getText());
} catch (JMSException e) {
e.printStackTrace();
}
});
// 阻塞,防止未获取消息就结束了
System.in.read();
// 8、关闭资源
consumer.close();
session.close();
connection.close();
}
发布/订阅模式Publish/Subscribe(Pub/Sub),topic主题
一条消息可以对应多个消费者;
消费者只能接收订阅以后的消息,订阅前的消息获取不到,所以要先启动消费者再启动生产者;
如果没有消费者,消息会被丢弃;
示例
创建 发布/订阅 模式的生产者
public static void main(String[] args) throws Exception{
String brokerUrl = "tcp://localhost:61616";
String topicName = "第一个topic消息队列";
// 1、创建连接工厂
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerUrl);
// 2、创建连接
Connection connection = connectionFactory.createConnection();
connection.start();
// 3、创建会话session 第一个参数:是否开启事务;第二个参数:应答模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4、创建目的地
Topic topic = session.createTopic(topicName);
// 5、创建消息生产者
MessageProducer producer = session.createProducer(topic);
// 6、创建3条消息
for (int i = 1; i <= 5; i++) {
TextMessage textMessage = session.createTextMessage();
textMessage.setText("第"+i+"条消息");
// 7、生产者发送消息
producer.send(textMessage);
}
// 8、关闭资源
producer.close();
session.close();
connection.close();
}
创建 发布/订阅 模式的消费者
public static void main(String[] args) throws Exception{
String brokerUrl = "tcp://localhost:61616";
String topicName = "第一个topic消息队列";
// 1、创建连接工厂
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerUrl);
// 2、创建连接
Connection connection = connectionFactory.createConnection();
connection.start();
// 3、创建会话session 第一个参数:是否开启事务;第二个参数:应答模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4、创建目的地
Topic topic = session.createTopic(topicName);
// 5、创建消息消费者
MessageConsumer consumer = session.createConsumer(topic);
// 设置异步非阻塞消息监听器获取消息
consumer.setMessageListener((message)->{
TextMessage message1 = (TextMessage) message;
try {
System.out.println("我是topic消费者1:"+message1.getText());
} catch (JMSException e) {
e.printStackTrace();
}
});
// 阻塞,放着未获取消息就结束了
System.in.read();
// 8、关闭资源
consumer.close();
session.close();
connection.close();
}
消息
有两种存储方式:
持久化:服务器宕机,消息依然存在 (队列的默认方式)
持久化订阅者在订阅以后,如果离线了,下次再上线依然可以接收到未消费的消息
producer.setDeliveryMode(DeliveryMode.PERSISTENT);//持久化
connection.start();// 启动连接需要放在这里才生效
非持久化:服务器宕机,消息丢失
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);//非持久化
connection.start();// 启动连接需要放在这里才生效
Session
签收方式:
// 签收与服务端没关系,主要是针对客户端
1、未开启事务模式
// 客户端自动签收
connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 客户端手动签收,搭配textMessage.acknowledge()使用
connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
textMessage.acknowledge();
2、开启事务模式
// 客户端自动签收
connection.createSession(true, Session.CLIENT_ACKNOWLEDGE);
session.commit();
// 客户端手动签收
// 在事务环境下,签收与事务有关,acknowledge不起作用,事务提交就自动签收;事务不提交,客户端签收不起作用
connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
textMessage.acknowledge();
session.commit();
java 代码内嵌一个Broker
pom.xml文件引入
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.5</version>
</dependency>
public static void main(String[] args) throws Exception {
//创建内嵌broker
BrokerService brokerService = new BrokerService();
brokerService.setUseJmx(true);
brokerService.addConnector("tcp://localhost:61617");
brokerService.start();
}
四、springboot 引入activeMq
复习 @Autowired 和 @Resource 的区别
@Autowired//默认按type注入,spring注解
@Qualifier("cusInfoService")//一般作为@Autowired()的修饰用,按照name注入
@Resource(name="cusInfoService")//默认按name注入,可以通过name和type属性进行选择性注入,javaee注解
pom.xml
<!-- 引入jms -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
application.yml
spring:
activemq:
broker-url: tcp://localhost:61616
user: admin
password: admin
jms:
pub-sub-domain: false # 默认false queue;true topic
destination: boot-activemq-queue # 自己定义队列名称
启动类
@SpringBootApplication
@EnableScheduling // 开启定时任务
public class Main8007 {
public static void main(String[] args) {
SpringApplication.run(Main8007.class,args);
}
}
目的地-queue
@Component
@EnableJms // 开启jms
public class DestinationBean {
@Value("destination") // 获取application.yml文件自定义的名称
private String destination;
@Bean
public Queue queue(){
return new ActiveMQQueue(destination);
}
}
生产者
@Component
public class ProducerBean {
@Autowired
private JmsMessagingTemplate template;
@Autowired
private Queue queue;
public void produceMessage(){
template.convertAndSend(queue,"22222");
}
@Scheduled(fixedDelay = 3000) // 每隔3秒生成一条消息
public void produceMessageScheduled(){
template.convertAndSend(queue,"33333");
}
}
消费者
@Component
public class ConsumerBean {
@JmsListener(destination = "${destination}")
public void receiveMessage(TextMessage message) throws JMSException {
System.out.println("消费者消费:"+message.getText());
}
}
五、传输协议
默认的是openwire,即tcp://ip:port
NIO协议,比tcp协议性能更好,生产使用此种协议
修改activemq.xml配置文件,增加nio协议
<transportConnectors>
<!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true"/>
</transportConnectors>
<!-- 自动适配各种协议 -->
<transportConnectors>
<!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
<transportConnector name="auto+nio" uri="auto+nio://0.0.0.0:61618?maximumConnections=1000&wireFormat.maxFrameSize=104857600&org.apache.activemq.transport.nio.SelectorManager.corePoolSize=20&org.apache.activemq.transport.nio.SelectorManager.maximumPoolSize=50"/>
</transportConnectors>
六、MQ的高可用性
事务、持久、签收
- 持久化到mysql数据库
lib目录下引入jdbc驱动,修改activemq.xml文件
<persistenceAdapter>
<!-- <kahaDB directory="${activemq.data}/kahadb"/> -->
<!-- createTablesOnStartup = true 每次启动的时候会重新创建数据库,建议初始化后改为false -->
<jdbcPersistenceAdapter dataDirectory="${activemq.base}/data" dataSource="#activemq-mysql" createTablesOnStartup="true"/>
</persistenceAdapter>
<!-- 下边的代码需要加在 broker 和 import 标签中间 -->
<!-- 若要使用druid数据库连接池,需要手动引入jar包 -->
<bean id="activemq-mysql" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/activemq?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&relaxAutoCommit=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
手动创建一个activemq数据库
CREATE DATABASE activemq DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
重启activemq服务后,会自动在activemq库中创建3张表,以后的mq数据都在这三张表里存放着
ACTIVEMQ_MSGS 存放持久化消息
- ID:自增的数据库主键
- CONTAINER:消息的Destination
- MSGID_PROD:消息发送者客户端的主键
- MSG_SEQ:是发送消息的顺序,MSGID_PROD+MSG_SEQ可以组成JMS的MessageID
- EXPIRATION:消息的过期时间,存储的是从1970-01-01到现在的毫秒数
- MSG:消息本体的Java序列化对象的二进制数据
- PRIORITY:优先级,从0-9,数值越大优先级越高
ACTIVEMQ_ACKS 用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存
- CONTAINER:消息的Destination
- SUB_DEST:如果是使用Static集群,这个字段会有集群其他系统的信息
- CLIENT_ID:每个订阅者都必须有一个唯一的客户端ID用以区分
- SUB_NAME:订阅者名称
- SELECTOR:选择器,可以选择只消费满足条件的消息。条件可以用自定义属性实现,可支持多属性AND和OR操作
- LAST_ACKED_ID:记录消费过的消息的ID。
ACTIVEMQ_LOCK
创建持久化订阅者,存放在 ACTIVEMQ_ACKS 中
public static void main(String[] args) throws Exception{
String brokerUrl = "nio://localhost:61618";
String topicName = "topic消息队列";
// 1、创建连接工厂
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerUrl);
// 2、创建连接
Connection connection = connectionFactory.createConnection();
// 3、设置持久订阅消费者ID
connection.setClientID("持久化消费者ID1");
// 4、创建会话session 第一个参数:是否开启事务;第二个参数:应答模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4、创建目的地
Topic topic = session.createTopic(topicName);
// 5、创建持久订阅者
TopicSubscriber c1 = session.createDurableSubscriber(topic, "持久化topic消费者1");
// 6、启动连接
connection.start();
// 7、设置异步非阻塞消息监听器获取消息
c1.setMessageListener((message)->{
TextMessage message1 = (TextMessage) message;
try {
System.out.println("我是topic消费者:"+message1.getText());
} catch (JMSException e) {
e.printStackTrace();
}
});
System.in.read();
// 8、关闭资源
c1.close();
session.close();
connection.close();
}
创建持久化发布者,发布的消息存放在ACTIVEMQ_MSGS中
public static void main(String[] args) throws Exception{
String brokerUrl = "nio://localhost:61618";
String topicName = "topic消息队列";
// 1、创建连接工厂
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerUrl);
// 2、创建连接
Connection connection = connectionFactory.createConnection();
// 3、创建会话session 第一个参数:是否开启事务;第二个参数:应答模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4、创建目的地
Topic topic = session.createTopic(topicName);
// 5、创建消息生产者
MessageProducer producer = session.createProducer(topic);
producer.setDeliveryMode(DeliveryMode.PERSISTENT);//持久化
connection.start();
// 6、创建3条消息
for (int i = 1; i <= 5; i++) {
TextMessage textMessage = session.createTextMessage();
textMessage.setText("第"+i+"条消息");
// 7、生产者发送消息
producer.send(textMessage);
textMessage.acknowledge();
}
// 8、关闭资源
// session.commit();
producer.close();
session.close();
connection.close();
}
七、在activemq和mysql之间增加一层高速缓存 ActiveMq journal
不断的读写数据库很消耗服务器性能,增加一层高速缓存,可以让快速消费掉的消息不写入数据库,而且消息先存入缓存中,比直接读数据库要快。
消息不是立马就同步到数据库中,在journal中存放一段时间之后才会同步到数据库中,变相的做到了读写分离操作
修改activemq.xml文件,重启服务即可
<!-- <persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
createTablesOnStartup = true 每次启动的时候会重新创建数据库,建议用完改为false
<jdbcPersistenceAdapter dataDirectory="${activemq.base}/data" dataSource="#activemq-mysql" createTablesOnStartup="true"/>
</persistenceAdapter> -->
<persistenceFactory>
<journalPersistenceAdapterFactory journalLogFiles="4" journalLogFileSize="32768" userJournal="true" useQuickJournal="true" dataDirectory="${activemq.base}/data" dataSource="#activemq-mysql" createTablesOnStartup="false"/>
</persistenceFactory>
八、高可用
使用zookeeper + replicated-leveldb-store 模式的主从集群实现高可用性
activemq集群,客户端只能访问master,其他slaver只做数据拷贝;
当master宕机后,会自动选举出一个新的master,原master恢复以后,进入slaver集群
zookeeper 半数选举机制选取master;机器数量超过一半以上的存活,该集群才可用
九、面试题
mq默认使用异步方式发送数据,但是有两种情况会使用同步方式发送消息:
- 显式使用同步方式
- 未使用事务的情况下发送持久化消息
异步发送消息会出现消息丢失的情况:假如MQ宕机了,生产者内存中尚未发送到MQ的数据会全部丢失
解决方案:接收回调结果
public static void main(String[] args) throws JMSException {
String brokerUrl = "tcp://localhost:61618";
String queueName = "第一个消息队列";
// 1、创建连接工厂
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerUrl);
connectionFactory.setAlwaysSyncSend(true);// 设置异步发送
// 2、创建连接
Connection connection = connectionFactory.createConnection();
connection.start();
// 3、创建会话session 第一个参数:是否开启事务;第二个参数:应答模式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4、创建目的地
Queue queue = session.createQueue(queueName);
// 5、创建消息生产者
ActiveMQMessageProducer producer = (ActiveMQMessageProducer)session.createProducer(queue);
producer.setDeliveryMode(DeliveryMode.PERSISTENT);// 发送持久化消息
// 6、创建3条消息
for (int i = 1; i <= 5; i++) {
TextMessage textMessage = session.createTextMessage();
textMessage.setText("第"+i+"条消息");
textMessage.setJMSMessageID(UUID.randomUUID().toString());
// 消息ID用于记录某条消息的发送状态
String messageId = textMessage.getJMSMessageID();
// 7、生产者发送消息,回调函数用于接收消息发送的结果
producer.send(textMessage, new AsyncCallback() {
@Override
public void onSuccess() {
// 消息发送成功回调函数
System.out.println("消息发送成功,消息ID:"+messageId);
}
@Override
public void onException(JMSException e) {
// 消息发送失败回调函数
System.out.println("消息发送失败,消息ID:"+messageId);
}
});
}
// 8、关闭资源
producer.close();
session.close();
connection.close();
}
消息重发机制
默认重发机制:每隔一秒重发一次,一共重发6次,重发超过6次以后进入死信队列
引发消息重发机制的2种情况:
- 客户端-消费者:开启了事务,但是未成功调用session.commit()
- 客户端-消费者:应答模式为CLIENT_ACKNOWLEDGE,在session中调用了recover
如何解决消息重复消费
重复消费产生原因:消息重发
解决方案:
一条数据重复出现多次的情况下,给消息增加一个唯一ID,这样就能保证数据库里只有一条数据,也就保证了系统的幂等性。比如
某个数据要写库,首先根据主键查一下,如果这个数据已经有了,那就别插入了,执行update即可;
如果用的是redis,那就没问题了,因为每次都是set操作,天然的幂等性幂等性是什么?
幂等性就是一个数据,或者一个请求,给你执行多次,得保证对应的数据不会改变,并且不能出错,这就是幂等性。