1. 20230626学习记录
目标
- RabbitMQ确认和回退消息机制
- 死信队列
- Elasticsearch的安装配置
- kibana的安装配置
- IK分词器的安装配置
- springboot整合ES的配置和使用
昨天学习了rabbitmq其中的五个模式,测试了五个模式下的生产消费。
那如何确认生产者发送的信息被交换机收到了,或者交换机成功转发到队列了
1.1. 生产端发送确认
-
配置开启发送确认模式和回退消息
确认模式:能够用来确认生产者是否成功发送信息到交换机了
回退消息:能够用来确认信息是否被交换机成功转发到队列了
publisher-confirm-type: correlated
publisher-returns: true
- 使用回调消息接口
RabbitMQTemplate.ConfirmCallBack接口有个回调方法confirm
confirm(CorrelationData correlationData, boolean ack, String cause)
correlationData: 消息相关数据
ack: 交换机是否收到信息
cause: 未收到原因
RabbitMQTemplate可以设置内部属性 ConfirmCallback,使用匿名内部类的方式重写回调方法confirm来获取参数信息(函数式接口,可以使用lambda)
@Override
public void sendCode(String msg) {
rabbitTemplate.setConfirmCallback((c,ack,m) -> {
if(ack){
log.debug("交换机成功收到了信息 ack:{}",ack);
}else {
log.debug("交换机未收到消息:{}",c);
log.debug("原因:{}",m);
}
});
rabbitTemplate.convertAndSend(RabbitMqConstants.MQ_TOPIC_EXCHANGE,"routingKey",msg);
}
- 使用回退信息接口
RabbitTemplate.ReturnsCallback接口有个回调方法returnedMessage
returnedMessage(ReturnedMessage msg)
当交换机无法正常投递时,会触发消息回退的回调方法
@Override
public void sendCode(String msg) {
rabbitTemplate.setReturnsCallback(rMsg -> {
log.debug("消息: {} 被退回,退回原因:{},退回交换机:{},发送的路由键:{}",
new String(rMsg.getMessage().getBody()),rMsg.getReplyText(),rMsg.getExchange(),rMsg.getRoutingKey());
});
rabbitTemplate.convertAndSend(RabbitMqConstants.MQ_TOPIC_EXCHANGE,"routing",msg);
}
//消息: 1234 退回,退回原因:NO_ROUTE,退回交换机:mq_topic_exchange,发送的路由键:routing
1.2. 消费端手动确认
默认自动确认的。
1.2.1. 开启配置
listener:
simple:
acknowledge-mode: manual #手动确认
default-requeue-rejected: false #是否将被拒绝的消息重新放入队列
1.2.2. 消费端处理
消费端通过参数Channel
channel.basicAck(long deliveryTag, boolean multiple);
//确认消息
channel.basicNock(long deliveryTag, boolean multiple, boolean requeue);
//拒收消息
long deliveryTag:当前获取的消息在队列中的索引,通过消费者方法参数Message可以获得
boolean multiple:是否 批量确认或拒绝 (批量指的是 一次性确认或拒绝 所有小于当前deliveryTag的消息)
boolean requeue:被拒绝的消息是否重新放入队列
@RabbitListener(queues = RabbitMqConstants.MQ_DELAY_QUEUE)
public void delayConsumer(String msg, Message message, Channel channel) throws IOException {
log.debug("时间:{},延迟获取信息:{}",new Date(),msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
@RabbitListener(queues = RabbitMqConstants.MQ_DELAY_QUEUE)
public void delayConsumer(String msg, Message message, Channel channel) throws IOException {
log.debug("时间:{},延迟获取信息:{}",new Date(),msg);
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
}
1.3. 死信队列
死信概念
- 被消费端拒绝的消息
- TTL到期的消息
- 队列满了无法放入的消息
当消息变为死信后,转发到另一个队列处理,这个队列就称为死信队列
1.3.1. 死信队列的创建和使用
1.3.1.1. 创建死信队列、交换机,定义绑定关系
和创建普通的队列一模一样
@Bean("delayQueue")
public Queue delayQueue(){
return new Queue(RabbitMqConstants.MQ_DELAY_QUEUE,
RabbitMqConstants.DURABLE,
RabbitMqConstants.EXCLUSIVE,
RabbitMqConstants.AUTODELETE);
}
@Bean("delayExchange")
public DirectExchange delayExchange(){
return new DirectExchange(RabbitMqConstants.MQ_DELAY_EXCHANGE,
RabbitMqConstants.DURABLE,
RabbitMqConstants.AUTODELETE);
}
@Bean
public Binding delayBinding(@Qualifier("delayQueue") Queue queue,@Qualifier("delayExchange") DirectExchange exchange){
return BindingBuilder.bind(queue)
.to(exchange)
.with(RabbitMqConstants.MQ_DELAY_ROUTING_KEY);
}
1.3.1.2. 创建普通队列和交换机
创建队列时需要配置 转发给哪个死信队列交换机,以及交换机的routingKey
@Bean("normalQueue")
public Queue normalQueue(){
Map<String, Object> args = new HashMap<String, Object>();
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", RabbitMqConstants.MQ_DELAY_EXCHANGE);
//声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", RabbitMqConstants.MQ_DELAY_ROUTING_KEY);
return new Queue(RabbitMqConstants.MQ_NORMAL_QUEUE,
RabbitMqConstants.DURABLE,
RabbitMqConstants.EXCLUSIVE,
RabbitMqConstants.AUTODELETE,
args);//添加死信交换机信息参数
}
@Bean("normalExchange")
public DirectExchange normalExchange(){
return new DirectExchange(RabbitMqConstants.MQ_NORMAL_EXCHANGE,
RabbitMqConstants.DURABLE,
RabbitMqConstants.AUTODELETE);
}
@Bean
public Binding normalBinging(@Qualifier("normalQueue") Queue queue,@Qualifier("normalExchange") DirectExchange exchange){
return BindingBuilder.bind(queue)
.to(exchange)
.with(RabbitMqConstants.MQ_NORMAL_ROUTING_KEY);
}
1.3.1.3. 模拟消息过期
设置消息TTL 10s,不创建普通队列的消费者模拟消息未被消费,创建监听死信队列的消费者,10s消息过期后,死信队列消费者处理消息。
-
生产者
发送方法的参数有一个消息后处理对象
MessagePostProcessor
,是一个函数式接口,接口中的postProcessMessage方法参数Message对象,可以设置消息相关信息setExpiration()设置过期时间,参数为String 单位毫秒
rabbitTemplate.convertAndSend(RabbitMqConstants.MQ_NORMAL_EXCHANGE,
RabbitMqConstants.MQ_NORMAL_ROUTING_KEY,
msg,
message -> {
message.getMessageProperties().setExpiration("10000");//设置消息存活时间为10秒
return message;
}
);
-
消费者
正常监听,监听死信队列
@RabbitListener(queues = RabbitMqConstants.MQ_DELAY_QUEUE)
public void delayConsumer(String msg){
log.debug("时间:{},延迟获取信息:{}",new Date(),msg);
}
1.3.2. 利用死信队列实现延迟队列
上面的例子可以看出,死信队列10s后才收到消息,就有延迟的效果
延迟队列使用场景,例如:
1.订单在十分钟之内未支付则自动取消
2.新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
3.预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议等
1.4. ElasticSearch
Elasticsearch 是一个分布式的、开源的搜索分析引擎,支持各种数据类型,包括文本、数字、地理、结构化、非结构化。
1.4.1. 拉取镜像
docker pull docker pull elasticsearch:7.17.7
1.4.2. 创建目录
- 在usr/local/software 下创建 ,并修改权限
mkdir -p elasticsearch/data
mkdir -p elasticsearch/config
mkdir -p elasticsearch/plugins
chmod 777 elasticsearch/**
- 在config文件夹下创建elasticsearch.yml文件,并修改其为可读写执行权限。
touch elasticsearch.yml
chmod 777 elasticsearch.yml
文件中添加
http:
host: 0.0.0.0
cors:
enabled: true
allow-origin: "*"
xpack:
security:
enabled: false
1.4.3. 配置centos环境
调整max_map_count
sysctl -w vm.max_map_count=262144
1.4.4. 创建运行容器
docker run -itd \
--name es \
--privileged=true \
-p 9200:9200 \
-p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms1g -Xmx1g" \
-v /usr/local/software/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /usr/local/software/elasticsearch/data:/usr/share/elasticsearch/data \
-v /usr/local/software/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
--net mynetwork --ip 172.18.0.9 \
--restart=always \
elasticsearch:7.17.7
1.4.5. 进入容器,修改ES内存大小
docker exec -it es bash
进入/usr/share/elasticsearch/config 目录
echo "-Xms4g" >> jvm.options
echo "-Xmx4g" >> jvm.options
1.4.6. 退出、重启容器
docker restart es
1.4.7. 防火墙开放9200,9300端口
firewall-cmd --zone=public --add-port=9200/tcp --permanent
firewall-cmd --zone=public --add-port=9300/tcp --permanent
firewall-cmd --reload
1.4.8. 浏览器测试
9200端口,如图成功
1.5. Kibana
Kibana是一个开源的分析与可视化平台,设计出来用于和Elasticsearch
一起使用的。你可以用kibana搜索、查看存放在Elasticsearch
中的数据。
1.5.1. 拉取kibana镜像
docker pull kibana:7.17.7
1.5.2. 创建、运行容器
注意 ELASTICSEARCH_HOSTS=http://192.168.100.100:9200
填写es的端口
docker run -itd --name kibana -e "ELASTICSEARCH_HOSTS=http://192.168.100.100:9200" -p 5601:5601 --net mynetwork --ip 172.18.0.10 \
--restart=always \
kibana:7.17.7
1.5.3. 放开端口
firewall-cmd --zone=public --add-port=5601/tcp --permanent
firewall-cmd --reload
1.5.4. 浏览器测试
192.168.100.100:5601
1.6. 分词器 IK Analysis
1.6.1. 下载分词器
最好下载和elasticSearch一致的分词器,如果没有则保证大版本一致,例如:7.17.X
Releases · medcl/elasticsearch-analysis-ik · GitHub
1.6.2. 上传分词器
上传下载好的压缩包到: /usr/local/software/elasticsearch-analysis-ik
创建elasticsearch-analysis-ik目录
mkdir elasticsearch-analysis-ik
1.6.3. 拷贝分词器到es容器的plugins/ik中
进入容器内/usr/share/elasticsearch/plugins目录下创建 ik 目录
再拷贝
docker cp elasticsearch-analysis-ik-7.17.7.zip es:/usr/share/elasticsearch/plugins/ik
1.6.4. 解压分词器
进入容器内/usr/share/elasticsearch/plugins/ik下,解压
unzip elasticsearch-analysis-ik-7.17.7.zip
删除压缩包
rm -rf elasticsearch-analysis-ik-7.17.7.zip
1.6.5. 重启容器
docker restart es
1.6.6. 使用kibana测试
成功
1.7. springboot整合ElasticSearch
1.7.1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
1.7.2. 配置es
elasticsearch:
rest:
uris: 192.168.100.100:9200
data:
elasticsearch:
repositories:
enabled: true
1.7.3. 创建ES客户端对象
@Configuration
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
@Value("${spring.elasticsearch.rest.uris}")
private String url;
//创建ES客户端对象 RestHighLevelClient
@Bean
@Override
public RestHighLevelClient elasticsearchClient() {
//客户端配置对象
ClientConfiguration clientConfiguration = ClientConfiguration.builder().connectedTo(url).build();
return RestClients.create(clientConfiguration).rest();
}
}
1.7.4. 创建存入es的实体映射类对象
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "book_index")
public class BookDoc {
@Id
@Field(type = FieldType.Long)
private Long id;
//书名
@Field(type = FieldType.Text)
private String title;
//作者
@Field(type = FieldType.Text)
private String author;
//ISBN号
@Field(type = FieldType.Text)
private String isbn;
//价格
@Field(type = FieldType.Double)
private BigDecimal price;
//版次
@Field(type = FieldType.Integer)
private Integer version;
//出版时间
@Field(type = FieldType.Date)
private Date pubDate;
//库存
@Field(type = FieldType.Integer)
private Integer store;
//封面图片路径
@Field(type = FieldType.Text)
private String imgUrl;
//重量
@Field(type = FieldType.Double)
private BigDecimal weight;
//简介描述
@Field(type = FieldType.Text)
private String introduction;
//卖出数量
@Field(type = FieldType.Integer)
private Integer sold;
//页数
@Field(type = FieldType.Integer)
private Integer pages;
//创建时间
@Field(type = FieldType.Date)
private Date createTime;
//修改时间
@Field(type = FieldType.Date)
private Date updateTime;
//创建者
@Field(type = FieldType.Text)
private String creator;
//书籍对应类型
@Field(type = FieldType.Text)
private String bookTypeName;
//书籍对应出版社
@Field(type = FieldType.Text)
private String publisherName;
}
1.7.5. 创建操作es库的dao层对象
继承ElasticsearchRepository<BookDoc,Long> 两个泛型,第一个是操作的对象类型,第二个是主键的类型
在该接口中,通过定义的方法名就能自动创建各种查询
例如 findByIntroduction() 当调用该方法时,就会自动翻译为执行查询introduction的ES查询语句
@Mapper
public interface IBookDocDao extends ElasticsearchRepository<BookDoc,Long> {
public List<BookDoc> findByIntroduction(String introduction);
}
1.7.6. 向es中添加数据,查询
public void addAllToEs() {
//先从数据库中查询出所有的book
List<Book> books = bookDao.selectList(null);
//封装BookDoc对象
List<BookDoc> bookDocs = new ArrayList<>();
books.forEach(book -> {
//从redis中获取bookTypeName和publisherName
String bookTypeName = redisTemplate.opsForValue().get("bookType:" + book.getBookTypeId());
String publisherName = redisTemplate.opsForValue().get("publisher:" + book.getPublisherId());
BookDoc bookDoc = new BookDoc(book.getId(), book.getTitle(), book.getAuthor(),
book.getIsbn(), book.getPrice(), book.getVersion(),
book.getPubDate(), book.getStore(), book.getImgUrl(),
book.getWeight(), book.getIntroduction(), book.getSold(),
book.getPages(), book.getCreateTime(), book.getUpdateTime(),
book.getCreator(), bookTypeName, publisherName);
bookDocs.add(bookDoc);
});
//向ES中添加数据
bookDocDao.saveAll(bookDocs);
}
//查询 introduction 中含有str的数据
@Override
public List<BookDoc> findFromEs(String str) {
List<BookDoc> bookDocs = bookDocDao.findByIntroduction(str);
return bookDocs;
}