十、Spring Boot与消息
消息简介:
(1)大多数应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力。
(2)消息服务中两个重要概念:
消息代理(message broker)和目的地(destination)
当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。
(3)消息队列主要有两种形式的目的地:
队列(queue):点对点消息通信(point-to-point)
主题(topic):发布(publish)/订阅(subscribe)消息通信
(4)消息代理规范
-
JMS(Java Message Service)JAVA消息服务
基于JVM消息代理的规范,ActiveMQ、HornetMQ是JMS实现
-
AMQP(Advanced Message Queuing Protocal)
高级消息队列协议,也是一个消息代理的规范,兼容JMS
RabbitMQ是AMQP的实现
作用:
通过消息服务中间件来提升系统异步通信、扩展解耦能力
当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地
应用场景:
1、异步处理:用户注册操作和消息处理并行,提高响应速度。
原始:
改进:
现在:
2、应用解耦:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其它的后续操作,实现订单系统和库存系统的应用解耦。
过去:
现在:
3、流量削峰:用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。 秒杀业务根据消息队列中的请求信息,再做后续处理。
JMS和AMQP的对比:
JMS | AMQP | |
---|---|---|
定义 | Java api | 网络线级协议 |
跨语言 | 否 | 是 |
跨平台 | 否 | 是 |
Model | 提供两种消息模型: (1)、Peer-2-Peer (2)、Pub/sub | 提供了五种消息模型:(1)、direct exchange (2)、fanout exchange (3)、topic change (4)、headers exchange (5)、system exchange 本质来讲,后四种和JMS的pub/sub模型没有太大差别,仅是在路由机制上做了更详细的划分; |
支持消息类型 | 多种消息类型: TextMessage MapMessage BytesMessage StreamMessage ObjectMessage Message(只有消息头和属性) | byte[]当实际应用时,有复杂的消息,可以将消息序列化后发送。 |
综合评价 | JMS 定义了JAVA API层面的标准;在java体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差; | AMQP定义了wire-level层的协议标准;天然具有跨平台、跨语言特性。 |
(5)点对点式
- 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移除队列。
- 消息只有唯一的发送者和接受者,但是并不只有一个接收者。
(6)发布订阅者模式
- 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息
RabbitMQ
RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。
(1)核心概念
- Message
消息,消息是不具名的,它由消息头和消息体组成。
消息头,包括routing-key(路由键)、priority(相对于其他消息的优先权)、deliverty-mode(指出该消息可能需要持久性存储)
- Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
- Exchange
交换器,将生产者消息路由给服务器中的队列
类型有direct(默认),fanout, topic, 和headers,具有不同转发策略
- Queue
消息队列,保存消息直到发送给消费者
- Binding
绑定,用于消息队列和交换器之间的关联
- Connection
网络连接,比如一个TCP连接
- Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序
- Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。
vhost 是 AMQP 概念的基础,必须在连接时指定
RabbitMQ 默认的 vhost 是 /
- Broker
消息队列服务器实体
运行机制
1、消息路由
AMQP 中增加了Exchange 和 Binding 的角色,Binding 决定交换器的消息应该发送到那个队列。
2、Exchange类型
-
direct
点对点模式,消息中的路由键(routing key)如果和 Binding 中的 binding
key 一致,交换器就将消息发到对应的队列中。 -
fanout
广播模式,每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去
-
topic
将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。
识别通配符:#匹配0个或多个单词,*匹配一个单词
SpringBoot中的RabbitMQ
1. 环境准备
在docker中安装rabbitmq并运行
# 5672为服务端口,15672为web控制台端口
docker run -d -p 5672:5672 -p 15672:15672 38e57f281891
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--自定义消息转化器Jackson2JsonMessageConverter所需依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
配置文件
# 指定rebbitmq服务器主机
spring.rabbitmq.host=192.168.31.162
#spring.rabbitmq.username=guest 默认值为guest
#spring.rabbitmq.password=guest 默认值为guest
2. RabbitMQ的使用
RabbitAutoConfiguration中有内部类RabbitTemplateConfiguration,在该类中向容器中分别导入了RabbitTemplate和AmqpAdmin
在测试类中分别注入
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private AmqpAdmin amqpAdmin;
-
RabbitTemplate消息发送处理组件
可用来发送和接收消息
//发送消息
rabbitTemplate.convertAndSend("amq.direct","ustc","aaaa");
Book book = new Book();
book.setName("西游记");
book.setPrice(23.2f);
//Book要实现Serializable接口
rabbitTemplate.convertAndSend("amq.direct","ustc",book);
//接收消息
Object o = rabbitTemplate.receiveAndConvert("ustc");
System.out.println(o.getClass()); //class cn.edu.ustc.springboot.bean.Book
System.out.println(o); //Book{name='西游记', price=23.2}
默认的消息转化器是SimpleMessageConverter,对于对象以jdk序列化方式存储,若要以Json方式存储对象,就要自定义消息转换器
@Configuration
public class AmqpConfig {
@Bean
public MessageConverter messageConverter() {
//在容器中导入Json的消息转换器
return new Jackson2JsonMessageConverter();
}
}
-
AmqpAdmin管理组件
可用于创建和删除exchange、binding和queue
//创建Direct类型的Exchange
amqpAdmin.declareExchange(new DirectExchange("admin.direct"));
//创建Queue
amqpAdmin.declareQueue(new Queue("admin.test"));
//将创建的队列与Exchange绑定
amqpAdmin.declareBinding(new Binding("admin.test", Binding.DestinationType.QUEUE,"admin.direct","admin.test",null));
消息的监听
在回调方法上标注@RabbitListener注解,并设置其属性queues,注册监听队列,当该队列收到消息时,标注方法遍会调用
可分别使用Message和保存消息所属对象进行消息接收,若使用Object对象进行消息接收,实际上接收到的也是Message
@Service
public class BookService {
@RabbitListener(queues = {"admin.test"})
public void receive1(Book book){
System.out.println("收到消息:"+book);
}
@RabbitListener(queues = {"admin.test"})
public void receive1(Object object){
System.out.println("收到消息:"+object.getClass());
//收到消息:class org.springframework.amqp.core.Message
}
@RabbitListener(queues = {"admin.test"})
public void receive2(Message message){
System.out.println("收到消息"+message.getHeaders()+"---"+message.getPayload());
}
}
十一、Spring Boot与检索
检索简介
我们的应用经常需要添加检索功能,开源的ElasticSearch是目前全文搜索引擎的首选。他可以快速的存储、搜索和分析海量数
据。Spring Boot通过整合Spring Data ElasticSearch为我们提供了非常便捷的检索功能支持;
Elasticsearch是一个分布式搜索服务,提供Restful API,底层基于Lucene,采用多shard(分片)的方式保证数据安全,并
且提供自动resharding的功能,github等大型的站点也是采用了ElasticSearch作为其搜索服务。
概念
员工文档 的形式存储为例:一个文档代表一个员工数据。存储数据到ElasticSearch 的行为叫做 索引 ,但在索引一个文档之前,需要确定将文档存储在哪里。一个ElasticSearch集群可以包含多个索引,相应的每个索引可以包含多个类型。这些不同的类型存储着多个文档,每个文档又有多个属性 。
索引(名词):
如前所述,一个索引类似于传统关系数据库中的一个数据库,是一个存储关系型文档的地方。索引(index)的复数词为indices
或indexes。
索引(动词):
索引一个文档就是存储一个文档到一个索引(名词)中以便被检索和查询。这非常类似于SQL语句中的INSERT关键词,除了文档
已存在时,新文档会替换旧文档情况之外。
类似关系:
索引---数据库
类型---表
文档---表中的记录
属性---列
ES的安装与运行
与ES交互
-
9200端口
RESTful API通过HTTP通信
-
9300端口
Java客户端与ES的原生传输协议和集群交互
# 拉取ES镜像
docker pull elasticsearch:7.6.1
#运行ES
docker run -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 --name ES03 41072cdeebc5
ES_JAVA_OPTS
指定java虚拟机相关参数
-Xms256m
初始堆内存大小为256m
-Xmx256m
最大堆内存大小为256m
discovery.type=single-node
设置为单点启动
ES的基础入门
案例:创建一个员工目录,并支持各类型检索
索引员工文档
对于员工目录,我们将做如下操作:
- 每个员工索引一个文档,文档包含该员工的所有信息。
- 每个文档都将是
employee
类型 。 - 该类型位于 索引
megacorp
内。
PUT /megacorp/employee/1
{
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
注意,路径 /megacorp/employee/1
包含了三部分的信息:
-
megacorp
索引名称
-
employee
类型名称
-
1
特定雇员的ID
请求体 —— JSON 文档 —— 包含了这位员工的所有详细信息
{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
同理,添加更多员工
PUT /megacorp/employee/2
{
"first_name" : "Jane",
"last_name" : "Smith",
"age" : 32,
"about" : "I like to collect rock albums",
"interests": [ "music" ]
}
PUT /megacorp/employee/3
{
"first_name" : "Douglas",
"last_name" : "Fir",
"age" : 35,
"about": "I like to build cabinets",
"interests": [ "forestry" ]
}
检索文档
HTTP GET
请求并指定文档的地址——索引库、类型和ID。
GET /megacorp/employee/1
返回数据
{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_version": 1,
"_seq_no": 0,
"_primary_term": 1,
"found": true,
"_source": {
"first_name": "John",
"last_name": "Smith",
"age": 25,
"about": "I love to go rock climbing",
"interests": [
"sports",
"music"
]
}
}
将 HTTP 命令由 PUT
改为 GET
可以用来检索文档,同样的,可以使用 DELETE
命令来删除文档,以及使用 HEAD
指令来检查文档是否存在。如果想更新已存在的文档,只需再次 PUT
。
轻量搜索
搜索所有雇员:
GET /megacorp/employee/_search
返回数据
{
"took": 46,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_score": 1,
"_source": {
"first_name": "John",
"last_name": "Smith",
"age": 25,
"about": "I love to go rock climbing",
"interests": [
"sports",
"music"
]
}
},
{
"_index": "megacorp",
"_type": "employee",
"_id": "2",
"_score": 1,
"_source": {
"first_name": "Jane",
"last_name": "Smith",
"age": 32,
"about": "I like to collect rock albums",
"interests": [
"music"
]
}
},
{
"_index": "megacorp",
"_type": "employee",
"_id": "3",
"_score": 1,
"_source": {
"first_name": "Douglas",
"last_name": "Fir",
"age": 35,
"about": "I like to build cabinets",
"interests": [
"forestry"
]
}
}
]
}
}
返回结果包括三个文档,放在数据hits
中。
搜索姓氏为 Smith
的雇员
GET /megacorp/employee/_search?q=last_name:Smith
在请求路径中使用 _search
端点,并将查询本身赋值给参数 q=
。返回结果给出了所有的 Smith:
{
"took": 23,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 0.4700036,
"hits": [
{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_score": 0.4700036,
"_source": {
"first_name": "John",
"last_name": "Smith",
"age": 25,
"about": "I love to go rock climbing",
"interests": [
"sports",
"music"
]
}
},
{
"_index": "megacorp",
"_type": "employee",
"_id": "2",
"_score": 0.4700036,
"_source": {
"first_name": "Jane",
"last_name": "Smith",
"age": 32,
"about": "I like to collect rock albums",
"interests": [
"music"
]
}
}
]
}
}
使用查询表达式搜索
Query-string 搜索通过命令非常方便地进行临时性的即席搜索 ,但它有自身的局限性。Elasticsearch 提供一个丰富灵活的查询语言叫做 查询表达式 , 它支持构建更加复杂和健壮的查询。
GET /megacorp/employee/_search
{
"query" : {
"match" : {
"last_name" : "Smith"
}
}
}
返回效果与之前一样
更复杂的搜索
同样搜索姓氏为 Smith 的员工,但这次我们只需要年龄大于 30 的
GET /megacorp/employee/_search
{
"query" : {
"bool": {
"must": {
"match" : {
"last_name" : "smith"
}
},
"filter": {
"range" : {
"age" : { "gt" : 30 }
}
}
}
}
}
全文搜索
搜索下所有喜欢攀岩(rock climbing)的员工:
GET /megacorp/employee/_search
{
"query" : {
"match" : {
"about" : "rock climbing"
}
}
}
返回结果
{
"took": 13,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1.4167401,
"hits": [
{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_score": 1.4167401,
"_source": {
"first_name": "John",
"last_name": "Smith",
"age": 25,
"about": "I love to go rock climbing",
"interests": [
"sports",
"music"
]
}
},
{
"_index": "megacorp",
"_type": "employee",
"_id": "2",
"_score": 0.4589591,
"_source": {
"first_name": "Jane",
"last_name": "Smith",
"age": 32,
"about": "I like to collect rock albums",
"interests": [
"music"
]
}
}
]
}
}
可以看到返回结果还带有相关性得分_score
短语搜索
精确匹配一系列单词或者短语 。 比如, 执行这样一个查询,短语 “rock climbing” 的形式紧挨着的雇员记录。
为此对 match
查询稍作调整,使用一个叫做 match_phrase
的查询
GET /megacorp/employee/_search
{
"query" : {
"match_phrase" : {
"about" : "rock climbing"
}
}
}
高亮搜索
每个搜索结果中 高亮 部分文本片段
再次执行前面的查询,并增加一个新的 highlight
参数:
GET /megacorp/employee/_search
{
"query" : {
"match_phrase" : {
"about" : "rock climbing"
}
},
"highlight": {
"fields" : {
"about" : {}
}
}
}
返回结果
{
...
"hits": [
{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_score": 1.4167401,
"_source": {
"first_name": "John",
"last_name": "Smith",
"age": 25,
"about": "I love to go rock climbing",
"interests": [
"sports",
"music"
]
},
"highlight": {
"about": [
"I love to go <em>rock</em> <em>climbing</em>"
]
}
}
]
}
}
结果中还多了一个叫做 highlight
的部分。这个部分包含了 about
属性匹配的文本片段,并以 HTML 标签 <em>
封装
Springboot整合ElasticSearch
1. 概述
SpringBoot默认支持两种技术来和ES交互;
-
Jest(默认不生效)
- 需要导入jest的工具包(io.searchbox.client.JestClient)
- 从springboot 2.2.0以后被弃用
-
SpringData ElasticSearch
版本适配说明
Spring Data Elasticsearch | Elasticsearch |
---|---|
3.2.x | 6.8.1 |
3.1.x | 6.2.2 |
3.0.x | 5.5.0 |
2.1.x | 2.4.0 |
2.0.x | 2.2.0 |
1.3.x | 1.5.2 |
Springboot 2.2.6对应于 Spring Data Elasticsearch 3.2.6,即适配Elasticsearch 6.8.1
2. 环境搭建
编写文件对应Javabean,指定索引名和类型
@Document(indexName = "ustc",type = "book")
public class Book {
private Integer id;
private String bookName;
private String author;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", bookName='" + bookName + '\'' +
", author='" + author + '\'' +
'}';
}
}
3. ElasticSearch客户端
-
Transport Client
在ES7中已经被弃用,将在ES8被移除
-
High Level REST Client
ES的默认客户端
-
Reactive Client
非官方驱动,基于WebClient
下面以REST客户端为例说明ES的使用
配置主机地址
方式一 配置类配置
注意:这种方式底层依赖于Http相关类,因此要导入web相关jar包
@Configuration
static class Config {
@Bean
RestHighLevelClient client() {
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.build();
return RestClients.create(clientConfiguration).rest();
}
}
方式二 spring配置文件指定
spring.elasticsearch.rest.uris=http://192.168.31.162:9200
在测试类中注入客户端
@Autowired
RestHighLevelClient highLevelClient;
创建索引
IndexRequest request = new IndexRequest("ustc", "book",
UUID.randomUUID().toString())
.source(Collections.singletonMap("feature", "high-level-rest-client"))
.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
IndexResponse index = highLevelClient.index(request, RequestOptions.DEFAULT);
System.out.println(index.toString());
下面为创建索引
{
"_index": "ustc",
"_type": "book",
"_id": "0dc9f47a-7913-481d-a36d-e8f034a6a3ac",
"_score": 1,
"_source": {
"feature": "high-level-rest-client"
}
}
得到索引
//分别指定要获取的索引、类型、id
GetRequest getRequest = new GetRequest("ustc","book","0dc9f47a-7913-481d-a36d-e8f034a6a3ac");
GetResponse documentFields = highLevelClient.get(getRequest, RequestOptions.DEFAULT);
System.out.println(documentFields);
4. ElasticsearchRestTemplate
ES有两个模板,分别为ElasticsearchRestTemplate
和ElasticsearchTemplate
分别对应于High Level REST Client和Transport Client(弃用),两个模板都实现了ElasticsearchOperations
接口,因此使用时我们一般使用ElasticsearchOperations
,具体实现方式由底层决定。
由于在AbstractElasticsearchConfiguration
中已经向容器中导入了ElasticsearchRestTemplate
,因此我们使用时可以直接注入
注入模板
@Autowired
ElasticsearchOperations elasticsearchOperations;
保存索引
Book book = new Book();
book.setAuthor("路遥");
book.setBookName("平凡的世界");
book.setId(1);
IndexQuery indexQuery = new IndexQueryBuilder()
.withId(book.getId().toString())
.withObject(book)
.build();
String index = elasticsearchOperations.index(indexQuery);
查询索引
Book book = elasticsearchOperations.queryForObject(GetQuery.getById("1"), Book.class);
5. Elasticsearch Repositories
编写相关Repository并继承Repository或ElasticsearchRepository,泛型分别为<查询类,主键>
public interface BookRepository extends Repository<Book,Integer> {
List<Book> findByBookNameAndAuthor(String bookName, String author);
}
查询的方法仅需按照一定规则命名即可实现功能,无需编写实现,如上findByBookNameAndAuthor()方法相当于ES的json查询
{
"query": {
"bool" : {
"must" : [
{ "query_string" : { "query" : "?", "fields" : [ "bookName" ] } },
{ "query_string" : { "query" : "?", "fields" : [ "author" ] } }
]
}
}
}
@Query
此外,还可以使用@Query
自定义请求json
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
Page<Book> findByName(String name,Pageable pageable);
}
若参数为John,相当于请求体为
{
"query": {
"match": {
"name": {
"query": "John"
}
}
}
}
Book.class);
5. Elasticsearch Repositories
编写相关Repository并继承Repository或ElasticsearchRepository,泛型分别为<查询类,主键>
public interface BookRepository extends Repository<Book,Integer> {
List<Book> findByBookNameAndAuthor(String bookName, String author);
}
查询的方法仅需按照一定规则命名即可实现功能,无需编写实现,如上findByBookNameAndAuthor()方法相当于ES的json查询
{
"query": {
"bool" : {
"must" : [
{ "query_string" : { "query" : "?", "fields" : [ "bookName" ] } },
{ "query_string" : { "query" : "?", "fields" : [ "author" ] } }
]
}
}
}
@Query
此外,还可以使用@Query
自定义请求json
interface BookRepository extends ElasticsearchRepository<Book, String> {
@Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
Page<Book> findByName(String name,Pageable pageable);
}
若参数为John,相当于请求体为
{
"query": {
"match": {
"name": {
"query": "John"
}
}
}
}