rabbitmq是目前非常火热的消息中间件之一,目前本人公司项目使用的消息中间件就有 rabbitmq,作为一个合格的开发者,需要深入了解Rabbitmq的相关知识,才能更好的做好项目并提升自己的软件开发能力。
消息是在应用之间传递的数据载体,消息可以是一个文本字符串、可以是一个图片或者文件等等,但对于消息中间件来说:消息 特定指定的是文本字符串、JSON字符串或者嵌套对象的字符串等。而消息中间件则是用来进行可靠消息传递的 ”中介”,而这个中介进行消息传递不仅仅局限于某一编程语言。
解耦:
不同应用直接可以不直接通信,而通过消息中间件进行消息传递,这样大大降低了两个应用之间的耦合关系,例如在电商项目中,用户下单和物流发货的业务逻辑在强耦合关系下,若物流发货的代码出现了错误,则会导致用户下单的失败(事务回滚),造成用户体验非常差。通过消息中间件将两个业务解耦,用户下单成功后发送消息到消息中间件,然后物流业务通过消息中间件接收消息,即使物流发货失败,则消息中间件可以存储消息直到物流业务接收消息成功。
削峰:
在应用访问量剧增的情况下,需要保证应用依然可以处理用户的请求。但是巨大的访问量很容易压垮应用服务器,此时可以借助消息中间件来应对突发访问压力,应用则可以继续从消息中间件获取消息进行处理,从而保证系统的正常运行。
异步:
在一个复杂的业务场景下,多个系统同步调用会增加系统响应时间,此时可以借助中间件异步处理,提高访问效率。例如客户下单后可直接通过发送消息给消息中间件,无须等待物流系统响应直接返回客户下单成功,即使物流系统故障,但给客户的提示依然是下单成功。待物流系统恢复成功后,继续处理物业务即可。
Rabbitmq是使用Erlang语言进行编写的,所以在安装Rabbitmq之前需要安装Erlang,需要注意的是下载具体的版本的Rabbitmq需要搭配特定版本的Erlang,可访问官方网站:RabbitMQ Erlang Version Requirements,进行查询。如下图所示:我们选择安装的 3.8.15 版本的系统Rabbitmq要求的Erlang 版本为 23 左右即可。
rabbitmq与Erlang的安装包 都在github上进行托管,由于国内访问github网站特别慢,这里本人将两个软件的安装包都放在百度网盘上:https://pan.baidu.com/s/1TzT4ce6RN53YVUt_AjKyYw?pwd=72n7 ,下载完成后将两个软件上传到Linux服务器上。
## 解压erlang包
tar -zxvf otp_src_23.3.3.tar.gz
## 进入到解压目录
cd otp_src_23.3.3/
## 配置一下erlang
./configure
然后我们把发现执行程序报错: configure: error: No curses library functions found
,这个时候我们需要安装此依赖然后继续执行 configure 命令即可。
yum install -y ncurses-devel
./configure
然后在执行 make && make install
可以输入 erl命令来验证Erlang是否安装成功,如果有如下提示则说明软件安装成功。
## rabbitmq 依赖socat,需安装此依赖
yum install -y socat
## 安装rabbitmq
rpm -ivh rabbitmq-server-3.8.15-1.el7.noarch.rpm --force --nodeps
systemctl enable rabbitmq-server
启动rabbitmq服务:
systemctl start rabbitmq-server
查看rabbitmq服务是否启动成功:
systemctl status rabbitmq-server
rabbitmq-plugins list
rabbitmq-plugins enable rabbitmq_management
rabbitmq默认提供了一个用户名和密码都为 guest的账户,但这个账户只能在本地登陆,如下所示:为了能够实现消息中间件消息的发送和接收之前,先创建一个用户并设置其访问权限。
添加一个用户:admin其密码为admin@123。
rabbitmqctl add_user admin admin@123
添加用户完毕后,rabbitmq也很贴心提示我们为这个用户设置权限:
rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
rabbitmqctl set_user_tags admin administrator
再次访问rabbitmq的管理端页面:linux系统ip地址+15672端口:
到了这里Rabbitmq消息中间件安装成功,下面就介绍相关概念,方便我们快速理解和学习。
Rabbitmq支持点对点(Point to Point)模式,此模式是基于队列,消息生产者发送消息到消息队列,然后消息消费者从队列中接收消息并消费。在编写入门案例(使用了此模式)之前,首先让我们了解几个重要的概念:下面是一个典型的Rabbitmq的模型架构图:
生产者顾名思义,就是生产并发送消息的一方,一般在生产中会将对象序列化成JSON字符串,然后生产者通过连接通道(channel)将此消息发送给消息中间件。
顾名思义,就是消费消息的一方,消费者通过连接的通道(channel)接收从生产者发送的消息。
Queue:队列是Rabbitmq 节点的内部对象如上图所示,rabbitmq中消息只能存储在队列中,多个消息者可以从同一个队列中接收消息,但这里需要注意的是队列的消息会被均摊给多个消费者(消费者轮询消费),所以不是每一个消费者可以消费所有的消息并处理,如下一个队列中的消息被消费者轮询进行消费如下所示:
当生产者通过channel将消息发到exchange,此交换机根据一定的规则(Routing Key)将消息路由到队列中,其工作模型如下所示:
交换机通过BindingKey将交换机与队列进行绑定,当消息发送到交换机后,还需要一个与之对应的RoutingKey,如果BindingKey与RoutingKey相匹配时,消息会被路由到指定的队列中。
消费者和生产者都需要依赖于连接的通道进行消息的发送和接收,通道之间是相互隔离的,所以在一个通道内生产者和消费者对消息处理是不会影响其他通道内的生产者与消费者。而通道是轻量级的连接,可以大大减少系统的开销。
Rabbitmq官方提供了java版本的SDK,下面我们借助官方提供的SDK来简单开发一个入门案例:
导入rabbitmq相关依赖:
<properties>
<slf4j.version>1.7.30slf4j.version>
properties>
<dependencies>
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.10.0version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-accessartifactId>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-coreartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.8.1version>
<configuration>
<source>1.8source>
<target>1.8target>
configuration>
plugin>
plugins>
build>
首先我们创建一个 rabbitmq.properties 文件配置如下内容:
# rabbitmq连接ip地址
rabbitmq.host=1.14.49.145
# rabbitmq连接端口号
rabbitmq.port=5672
# rabbitmq 用户名
rabbitmq.username=admin
# rabbitmq的用户密码
rabbitmq.password=admin@123
# 设置队列名称
rabbitmq.queueName=hello_queue
# 设置交换机名称
rabbitmq.exchangeName=hello_exchange
## 设置路由键
rabbitmq.routeKey=hello_route
然后创建一个读取改配置的类如下:
/**
* properties加载
*/
public class PropertiesLoader {
private PropertiesLoader() {}
/**
* 读取Properties属性
* @see java.util.Properties
* @param prop 配置文件名称
* @return 读取的Properties对象
*/
public static Properties getProps(String prop){
Properties properties = new Properties();
try {
InputStream resource = PropertiesLoader.class.getClassLoader().getResourceAsStream(prop);
properties.load(resource);
} catch (IOException e) {
e.printStackTrace();
}
return properties;
}
public static void main(String[] args) {
Properties properties = getProps("rabbitmq.properties");
System.out.println(properties);
}
}
然后创建一个封装了获取ConnectionFactory 、Connection 以及Channel 的工具类如下:
/**
* 抽取rabbitmq的工具类
*/
@Slf4j
public class RabbitmqUtils {
private static final Properties properties = PropertiesLoader.getProps("rabbitmq.properties");
/**
* 队列名称
*/
public static final String QUEUE_NAME;
/**
* 交换机名称
*/
public static final String EXCHANGE_NAME;
/**
* 路由key
*/
public static final String ROUTE_KEY;
static {
EXCHANGE_NAME = properties.getProperty("rabbitmq.exchangeName");
QUEUE_NAME = properties.getProperty("rabbitmq.queueName");
ROUTE_KEY = properties.getProperty("rabbitmq.routeKey");
}
/**
* 获取rabbitmq连接工厂
* @see com.rabbitmq.client.ConnectionFactory
* @return 返回rabbitmq ConnectionFactory
*/
public static ConnectionFactory connectionFactory() {
// 1. 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置连接属性
connectionFactory.setHost(properties.getProperty("rabbitmq.host"));
connectionFactory.setPort(Integer.parseInt(properties.getProperty("rabbitmq.port")));
connectionFactory.setUsername(properties.getProperty("rabbitmq.username"));
connectionFactory.setPassword(properties.getProperty("rabbitmq.password"));
log.info("create connectionFactory success:{}",connectionFactory);
return connectionFactory;
}
/**
* 获取 rabbitmq连接
* @see com.rabbitmq.client.Connection
* @see com.rabbitmq.client.ConnectionFactory
* @param connectionFactory 连接工厂
* @return Connection
* @throws IOException IOException
* @throws TimeoutException IOException
*/
public static Connection getConnection(ConnectionFactory connectionFactory) throws IOException, TimeoutException {
return connectionFactory.newConnection();
}
/**
* 获取rabbitmq连接通道
* @see com.rabbitmq.client.Channel
* @see com.rabbitmq.client.Connection
* @return 获取rabbitmq连接通道
* @throws IOException IOException
*/
public static Channel getChannel(Connection connection) throws IOException {
return connection.createChannel();
}
}
生产者代码编写:
/**
* 消息生产者代码示例
*/
@Slf4j
public class MessageProducer {
public static void main(String[] args) {
try ( // 创建连接
Connection connection = RabbitmqUtils.getConnection(RabbitmqUtils.connectionFactory());
// 获取信道
Channel channel = RabbitmqUtils.getChannel(connection);
) {
// 第一个参数:声明交换机的名称,第二个参数:声明交换机的类型,第三个参数:是否需要持久化(即使rabbitmq服务重启交换机依然存在)
// 第四个参数:是否自动删除交换机(如果声明为true则交换机如果没有被使用将会被删除),第五个参数:声明交换机的配置参数
channel.exchangeDeclare(RabbitmqUtils.EXCHANGE_NAME, BuiltinExchangeType.DIRECT,true,false,null);
// 第一个参数:声明队列名称,第二个参数:是否需要持久化(即使rabbitmq服务重启队列依然存在)
// 第三个参数:是否声明一个独享的队列,如果为true则只能被一个消费者进行消费
// 第四个参数:是否自动删除队列(如果为true,当队列未被使用时候会被自动删除),第五个参数:声明队列的配置参数
channel.queueDeclare(RabbitmqUtils.QUEUE_NAME,true,false,false,null);
// 通过路由key将队列与交换机进行绑定
channel.queueBind(RabbitmqUtils.QUEUE_NAME,RabbitmqUtils.EXCHANGE_NAME,RabbitmqUtils.ROUTE_KEY);
// 声明一个文本内容
String message ="hello it's message form MessageProducer";
// 发送消息
channel.basicPublish(RabbitmqUtils.EXCHANGE_NAME,RabbitmqUtils.ROUTE_KEY, MessageProperties.TEXT_PLAIN,message.getBytes(StandardCharsets.UTF_8));
log.info("send message success:{}",message);
} catch (Exception e) {
log.error("send message fail",e);
}
}
}
消费者代码编写:
/**
* 消息消费者
*/
@Slf4j
public class MessageConsumer {
public static void main(String[] args) {
ConnectionFactory connectionFactory = RabbitmqUtils.connectionFactory();
try(
Connection connection= connectionFactory.newConnection();
Channel channel= connection.createChannel();
) {
channel.queueDeclare(RabbitmqUtils.QUEUE_NAME,true,false,false,null);
channel.basicQos(64);
// 自动应答模式
// basicConsume1(channel);
// 手动应答
basicConsume2(channel);
}catch (Exception e) {
log.error("consume message fail",e);
}
}
/**
* 消费方式一: 自动应答消息
* @param channel 信道
* @throws IOException IOException
*/
private static void basicConsume1(Channel channel) throws IOException {
DeliverCallback deliverCallback = getDeliverCallback();
CancelCallback cancelCallback = getCancelCallback();
channel.basicConsume(RabbitmqUtils.QUEUE_NAME,true,deliverCallback,cancelCallback);
}
/**
* 消费方式二:手动应答消息
* @param channel channel
* @throws IOException IOException
*/
private static void basicConsume2(Channel channel) throws Exception {
// 使用 Consumer进行消费
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException
{
log.info("receive message:{}",new String(body,StandardCharsets.UTF_8));
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
channel.basicConsume(RabbitmqUtils.QUEUE_NAME,consumer);
// TODO 这里必须休眠,等待回调函数执行完毕后在关闭资源,否则抛出以下异常:
// AlreadyClosedException: channel is already closed due to clean channel shutdown
TimeUnit.SECONDS.sleep(3);
}
/**
* 取消消息的回调
* @return 取消回调
*/
private static CancelCallback getCancelCallback() {
// 取消接收消息的回调
return (consumerTag) -> {
log.info("cancel callback:{}",consumerTag);
};
}
/**
* 发送成功的回调
* @return 发送成功的回调
*/
private static DeliverCallback getDeliverCallback() {
// 接收到消息的回调
return (consumerTag, message)-> {
String textMessage = new String(message.getBody(), StandardCharsets.UTF_8);
log.info("consumerTag:{}\n receive message:{}",consumerTag,textMessage);
};
}
}
运行生产者主方法发送消息到消息队列中:
此时进入rabbitmq的管理端页面可以查看已经创建的队列 hello_queue及其属性:
运行消费者主方法从队列中接收消息进行消费: