!!、int和Integer区别:
1.Integer是int的包装类,int则是java的一种基本的数据类型;
2.Integer变量必须实例化之后才能使用,而int变量不需要实例化;
3.Integer实际是对象的引用,当new一个Integer时,实际上生成一个指针指向对象,而int则直接存储数值
4.Integer的默认值是null,而int的默认值是0。
当做笔记本,时常更新自身所缺乏的基础知识!
es深度学习---正文
链接:https://pan.baidu.com/s/1j5BnarBek5I84A32Bqi0Bw?pwd=jgnh
提取码:jgnh内容有点多,可以反复浏览!
一、数据聚合
1.1 聚合的种类
聚合(aggregations)可以实现对文档数据的统计、分析、运算。聚合常见的有三类:
- 桶(Bucket)聚合:用来对文档做分组
- TermAggregation:按照文档字段值分组
- Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
- 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
- Avg:求平均值
- Max:求最大值
- Min:求最小值
- Stats:同时求max、min、avg、sum等
- 管道(pipeline)聚合:其它聚合的结果为基础做聚合
聚合不能是text类型的也就是可以分词的!!,只能是keyword、date、数值等不能分词的
1.2 DSL实现聚合
1.2.1 DSL实现Bucket聚合
案例:要统计所有数据中的酒店品牌有几种,此时可以根据酒店品牌的名称做聚合
GET /hotel/_search
{
"size": 0, //设置为0,结果中不包含文档,只包含聚合结果
"aggs": { //定义聚合
"brandAgg": { //给聚合起个名字
"terms": { //聚合的类型,按照品牌值聚合,座椅选择term
"field": "brand", // 参与聚合的字段
"size": 20 // 希望获取的聚合结果数量
}
}
}
}
默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序。 我们可以修改结果排序方式:
# 自定义排序规则
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"order": {
"_count": "asc"
},
"size": 20
}
}
}
}
默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加query条件即可:
# 聚合功能,限定聚合范围
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 200
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"order": {
"_count": "asc"
},
"size": 20
}
}
}
}
1.2.1 DSL实现Metrics 聚合
案例:获取每个品牌的用户评分的min、max、avg等值,我们可以利用stats聚合:
# 聚合功能_Metrics
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 10,
"order": {
"score_stats.max":"desc" # 按照聚合名称score_stats下面的max进行降序
}
},
"aggs": { # 是brand聚合的子聚合,也就是分组后对每组分别计算
"score_status": { #聚合名称
"stats": { # 聚合类型 ,这里stats可以计算min max avg
"field": "score" #聚合字段
}
}
}
}
}
}
1.3 RestAPI实现聚合
以品牌聚合为例,演示下Java的RestClient使用,先看请求组装:
/* 聚合 */
@Test
void testAggregation() throws IOException {
// 1. 准备request
SearchRequest request = new SearchRequest("hotel");
// 2. 准备DSL
// 2.1 设置size
request.source().size(0);
// 2.2 聚合
request.source().aggregation(AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(10));
// 3. 发出请求
SearchResponse search = client.search(request,RequestOptions.DEFAULT);
// 4. 解析结果
//4.1 解析聚合结果
Aggregations aggregations = search.getAggregations();
//4.2 根据名称获取聚合结果
Terms brandTerms=aggregations.get("brandAgg");
// 4.3 获取桶
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 遍历桶中的数据
for (Terms.Bucket bucket:buckets){
//获取key 也就是品牌信息
String brandName = bucket.getKeyAsString();
long docCount = bucket.getDocCount();
System.out.println("品牌名:"+brandName+",酒店数量:"+docCount);
}
}
案例:实现对品牌、城市、星级的聚合
需求:搜索页面的品牌、城市等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的
@Override
public Map<String, List<String>> filters(RequestParams params){
try {
// 1. 准备request
SearchRequest request = new SearchRequest("hotel");
//2. 准备DSL
// 2.1 query
buildBasicQuery(params, request); // 进行了一个代码抽取,代码为上一个文章中的符合查询代码
// 2.2 设置size
request.source().size(0);
// 2.3 聚合
buildAggregation(request);
// 3. 发出请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4. 解析结果
Map<String,List<String>> result=new HashMap<>();
Aggregations aggregations = response.getAggregations();
//4.1 根据品牌名称,获取品牌结果,最后将结果放map中
List<String> brandList = getAggByName(aggregations,"brandAgg");
result.put("brand",brandList);
//4.2 根据品牌名称,获取品牌结果,最后将结果放map中
List<String> cityList = getAggByName(aggregations,"cityAgg");
result.put("city",cityList);
//4.3 根据品牌名称,获取品牌结果,最后将结果放map中
List<String> starList = getAggByName(aggregations,"starAgg");
result.put("starName",starList);
return result;
}
catch (IOException e){
throw new RuntimeException(e);
}
}
private List<String> getAggByName(Aggregations aggregations,String aggeName) {
Terms brandTerms= aggregations.get(aggeName);
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
//遍历
List<String> brandList=new ArrayList<>();
for(Terms.Bucket bucket:buckets){
String key = bucket.getKeyAsString();
brandList.add(key);
}
return brandList;
}
private void buildAggregation(SearchRequest request) {
request.source().aggregation(AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(10));
request.source().aggregation(AggregationBuilders
.terms("cityAgg")
.field("city")
.size(100));
request.source().aggregation(AggregationBuilders
.terms("starAgg")
.field("starName")
.size(100));
}
二、自动补全
自动补全需求说明
当用户在搜索框输入字符时,我们应该提示出与该字符有关的搜索项,如图:
使用拼音分词
要实现根据字母做补全,就必须对文档按照拼音分词。在GitHub上恰好有elasticsearch的拼音分词插件。地址:https://github.com/medcl/elasticsearch-analysis-pinyin
安装方式与IK分词器一样,分三步:
- 解压
- 将我发的压缩包解压即可
- 上传到虚拟机中,elasticsearch的plugin目录
利用xshell和xftps进行传输
- 重启elasticsearch
# 重启
docker restart es
- 测试
在可视化界面Dev-Tools中,输入:
POST /_analyze
{
"text": ["如家有点还不错!!!"]
, "analyzer": "pinyin"
}
出现以下输出结果则运行成功!!
上面对于分词器的基本用法已经理解差不多,但是只是单纯这样是无法作用于生产环境当中。通过结果也可以看出,简单的分词器只是对每个词进行了分词拼音。
自定义分词器
elasticsearch中分词器(analyzer)的组成包含三个部分:
- character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
- tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
- tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
# 自定义拼音分词器
DELETE /test
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"name":{
"type": "text",
"analyzer":"my_analyzer"
}
}
}
}
POST /test/_analyze
{
"text": ["如家有点还不错!!!"]
, "analyzer": "my_analyzer"
}
现在我们向test的索引库中加入几条文档,并进行搜索:
POST /test/_doc/1
{
"id": 1,
"name": "狮子"
}
POST /test/_doc/2
{
"id": 2,
"name": "虱子"
}
GET /test/_search
{
"query": {
"match": {
"name": "sz"
}
}
}
但是当我们进行中文搜索时,会发现有问题!! 比如:
拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用。
因此字段在创建倒排索引时应该用my_analyzer分词器;字段在搜索时应该使用ik_smart分词器;
在自定义分词器的代码中设置创建的时候用my_analyzer,搜索时用ik_smart,重新创建test索引库时,记着删除一下原有的:
# 自定义拼音分词器
DELETE /test
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"name":{
"type": "text",
"analyzer":"my_analyzer"
, "search_analyzer": "ik_smart"
}
}
}
}
重新添加完上述两条数据以后在查询之前的中文句子会发现查询成功!!!
总结:
如何使用拼音分词器?
- 下载pinyin分词器
- 解压并放到elasticsearch的plugin目录
- 重启即可
如何自定义分词器?
- 创建索引库时,在settings中配置,可以包含三部分
- character filter
- tokenizer
- filter
拼音分词器注意事项?
- 为了避免搜索到同音字,搜索时不要使用拼音分词器
自动补全——completion suggester查询
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:
- 参与补全查询的字段必须是completion类型。
- 字段的内容一般是用来补全的多个词条形成的数组。
# 自动补全的索引库
PUT test2
{
"mappings": {
"properties": {
"title":{
"type": "completion"
}
}
}
}
# 示例数据
POST test2/_doc
{
"title": ["Sony", "WH-1000XM3"]
}
POST test2/_doc
{
"title": ["SK-II", "PITERA"]
}
POST test2/_doc
{
"title": ["Nintendo", "switch"]
}
# 自动补全查询
GET /test2/_search
{
"suggest": {
"titleSuggest": {
"text": "s", # 关键字
"completion": {
"field": "title", # 补全字段
"skip_duplicates": true, # 跳过重复的
"size": 10 # 获取前10条数据
}
}
}
}
案例:实现hotel索引库的自动补全、拼音搜索功能
实现思路如下:
- 修改hotel索引库结构,设置自定义拼音分词器
- 修改索引库的name、all字段,使用自定义分词器
- 索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器
- 给HotelDoc类添加suggestion字段,内容包含brand、business
- 重新导入数据到hotel库
1. 首先在HotelDoc实体类中加入suggestion属性;
private List<String> suggestion;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + "," + hotel.getLongitude();
this.pic = hotel.getPic();
this.suggestion = Arrays.asList(this.brand, this.business);
}
2. 然后 重新运行之前批量加入数据的测试类HotelDocumentTest中的testBulkRequest、
3. 可视化界面输入以下代码来验证索引库是否成功加入数据库数据:
GET /hotel/_search
{
"query": {
"match_all": {}
}
}
运行成功以后会发现suggestions 字段中有的会存在两个商圈 类似A、B ,所以需要进行一下切割
// 将HotelDoc中的代码改成如下
if (this.business.contains("、")) {
//business 有多个值,需要切割
String[] strings = this.business.split("、");
//添加元素
this.suggestion=new ArrayList<>();
this.suggestion.add(this.brand);
Collections.addAll(this.suggestion, strings);
} else {
this.suggestion = Arrays.asList(this.brand, this.business);
}
}
再重新运行一下测试类以后在可视化输入自动补全查询语句看看结果是否修改成功!
GET /hotel/_search
{
"suggest": {
"suggestions": {
"text": "h",
"completion": {
"field": "suggestion",
"skip_duplicates":true,
"size":10
}
}
}
}
RestAPI实现自动补全
@Test
void testSuggest() throws IOException {
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");
// 2. 准备DSL
request.source().suggest(new SuggestBuilder()
.addSuggestion("mySuggestion",
SuggestBuilders
.completionSuggestion("suggestion")
.skipDuplicates(true)
.size(10)
.prefix("h"))
);
// 3. 发起请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析结果
System.out.println(response);
但是我们并不是这些数据全要 而是要里面的自动补全中的suggestion数据,所以需要对输出结果进行一个解析
//4. 解析结果
Suggest suggest = response.getSuggest();
//4.1 根据名称获取补全结果
CompletionSuggestion suggestion=suggest.getSuggestion("mySuggestion");
// 4.2 获取options
List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions();
// 4.3 遍历
for (CompletionSuggestion.Entry.Option option:options){
Text text = option.getText();
System.out.println(text);
}
案例: 实现酒店搜索页面输入框的自动补全
1. 首先在实体类HotelDoc加一个suggestion属性,然后将品牌名和商圈名加入到这个数组中。
因为有的酒店商圈名不止一个并且是用/或者、来隔开,比如长安区/其纳溪区,所以需要做一个切割。
然后在控制层写业务逻辑,也就是跳转功能,具体的业务逻辑在service层中。
@GetMapping("suggestion")
public List<String> getSuggestions(@RequestParam("key") String key) {
return hotelService.getSuggestions(key);
}
下面就是将之前写的自动补全的测试类复制到service层即可
@Override
public List<String> getSuggestions(String key) {
try {
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");
// 2. 准备DSL
request.source().suggest(new SuggestBuilder()
.addSuggestion("suggestions",
SuggestBuilders
.completionSuggestion("suggestion")
.skipDuplicates(true)
.size(10)
.prefix(key)
));
// 3. 发起请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析结果
Suggest suggest = response.getSuggest();
//4.1 根据名称获取补全结果
CompletionSuggestion suggestions=suggest.getSuggestion("suggestions");
// 4.2 获取options
List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
List<String> list=new ArrayList<>(options.size());
// 4.3 遍历
for (CompletionSuggestion.Entry.Option option : suggestions.getOptions()){
String text = option.getText().toString();
list.add(text);
}
return list;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
重新运行启动类,在页面搜索框搜索关键字,比如:
三、数据同步
数据同步问题分析
elasticsearch中的酒店数据来自于mysql数据库,因此mysql数据发生改变时,elasticsearch也必须跟着改变,这个就是elasticsearch与mysql之间的数据同步。
???在微服务中,负责酒店管理(操作mysql )的业务与负责酒店搜索(操作elasticsearch )的业务可能在两个不同的微服务上,数据同步该如何实现呢?
缺点:耦合度高,本来写入数据库和调用索引库接口没有直接关系,现在将两者结合起来了,业务耦合影响性能,之前写入数据库就完事,现在还需要等待更新索引库。
总结:
方式一:同步调用
- 优点:实现简单,粗暴
- 缺点:业务耦合度高
方式二:异步通知
- 优点:低耦合,实现难度一般
- 缺点:依赖mq的可靠性
方式三:监听binlog
- 优点:完全解除服务间耦合
- 缺点:开启binlog增加数据库负担、实现复杂度高
利用MQ实现mysql与elasticsearch数据同步
利用课前资料提供的hotel-admin项目作为酒店管理的微服务。当酒店数据发生增、删、改时,要求对elasticsearch中数据也要完成相同操作。
步骤:
- 导入资料提供的hotel-admin项目,启动并测试酒店数据的CRUD
- 记得将配置文件中的连接数据库代码更改为自己的数据库名词,并且将第二个Mysql改成localhost,并加入rabbtimq的配置
server:
port: 8099
spring:
datasource:
url: jdbc:mysql://localhost:3306/rest_client?useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
rabbitmq:
host: 192.168.229.101
port: 5672
username: itcast
password: 123321
virtual-host: /
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
type-aliases-package: cn.itcast.hotel.pojo
- 声明exchange、queue、RoutingKey
- 在hotel-demo中导入依赖并实现所需配置类
-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
- 定义一个常量类,来定义交换机、队列等信息
-
package cn.itcast.hotel.constants; public class MqConstants { /*交换机 小写变大写 ctrl shift u*/ public final static String HOTEL_EXCHANGE="hotel.topic"; /*监听新增和修改的对列*/ public final static String HOTEL_INSERT_QUEUE="hotel.insert.queue"; /*监听删除的队列*/ public final static String HOTEL_DELETE_QUEUE="hotel.delete.queue"; /*新增或修改的RoutingKey*/ public final static String HOTEL_INSERT_KEY="hotel.insert"; /*删除的RoutingKey*/ public final static String HOTEL_DELETE_KEY="hotel.delete"; }
- 定义一个配置类,来声明
-
package cn.itcast.hotel.config; import cn.itcast.hotel.constants.MqConstants; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.TopicExchange; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MqConfig { @Bean public TopicExchange topicExchange(){ return new TopicExchange(MqConstants.HOTEL_EXCHANGE,true,false); } @Bean public Queue insertQueue(){ return new Queue(MqConstants.HOTEL_INSERT_QUEUE,true); } @Bean public Queue deleteQueue(){ return new Queue(MqConstants.HOTEL_DELETE_QUEUE,true); } @Bean public Binding insertQueueBinding(){ return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY); } @Bean public Binding deleteQueueBinding(){ return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY); } }
- 在hotel-admin中的增、删、改业务中完成消息发送
- 同样导入依赖
- 然后将定义的交换机、队列、key的常量类复制进来,并新建一个包,包名任意
- 将application.yml配置类中加入rabbtiMQ的一些定义参数,可以复制hotel-demo
- 最后在控制层写入mq,首先注入RabbtTemplate ,然后在增删改的地方加入消息队列,其中message如果为hotel 也就是酒店的全部信息的话,占用内存较大,所以本次只取id
- 在hotel-demo中完成消息监听,并更新elasticsearch中数据
- 编写监听器
@Component
public class HotelListener {
@Autowired
private IHotelService iHotelService;
/**
* 监听酒店新增或修改的业务
* @Param id 酒店id
*/
@RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
public void listenHotelInsertOrUpdate(Long id)
{
iHotelService.insertById(id);
}
/**
* 监听酒店删除的业务
* @Param id 酒店id
*/
@RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
public void listenHotelDeleteOrUpdate(Long id)
{
iHotelService.deleteById(id);
}
}
try catch快捷键:ctrl+alt+t
由于上面新建了两个方法,所以在HotelService开始对两个方法写逻辑
新增和删除的逻辑代码之前写过,所以直接复制即可
@Override
public void deleteById(Long id) {
try {
//1. 准备request
DeleteRequest request = new DeleteRequest("hotel",id.toString());
//发送请求
client.delete(request,RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void insertById(Long id) {
try {
// 0. 根据id查询酒店数据
Hotel hotel = getById(id);
// 转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
//1. 准备 Request对象
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
//2. 准备Json文档
request.source(JSON.toJSONString(hotelDoc),XContentType.JSON);
//3. 发送请求
client.index(request,RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
- 启动并测试数据同步功能
打开虚拟机,再打开es和kibana的基础上打开mq容器,具体如何在docker部署,上面资料有详细的步骤!
把容器打开后,在浏览器输入mq的端口http://192.168.229.101:15672/ (本地ip+mq常用端口号)会看到我们新建的消息队列
更新: 现在随便找一个酒店 打开控制台获取他的id然后在酒店管理页面找到此数据将价格进行修改,查看一下mq页面是否有消息
然后再去黑马旅游页面找到这条数据会发现价格发生了修改,这样就说明咱们的数据同步成功了!
删除: 删除之前,防止数据找不到了,可以提前先复制一份,然后再删除,去旅游页面查找。
新增:在酒店管理页面点击新增,然后点开控制层找到vue组件然后找到新增窗口,将之前复制的信息粘贴进来。
四、集群
单机的elasticsearch做数据存储,必然面临两个问题:海量数据存储问题、单点故障问题。
- 海量数据存储问题:将索引库从逻辑上拆分为N个分片(shard),存储到多个节点
- 单点故障问题:将分片数据在不同节点备份(replica )
一定要保证主和副不在弄一个服务器下,不然当前服务器坏了 那主副直接全不能用。
1. 搭建ES集群
由于我们当前并没有多台电脑,所以接下来会在单机上利用docker容器运行多个es实例来模拟es集群。不过生产环境推荐大家每一台服务节点仅部署一个es的实例。
部署es集群可以直接使用docker-compose来完成,但这要求你的Linux虚拟机至少有**4G**的内存空间
1.1 创建es集群
首先编写一个docker-compose文件,内容如下:
version: '2.2'
services:
es01:
image: elasticsearch:7.12.1
container_name: es01
environment:
- node.name=es01
- cluster.name=es-docker-cluster # 集群名称三个保持一致,这样会自动放入同一个集群当中
- discovery.seed_hosts=es02,es03
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- data01:/usr/share/elasticsearch/data
ports:
- 9200:9200
networks:
- elastic
es02:
image: elasticsearch:7.12.1
container_name: es02
environment:
- node.name=es02
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- data02:/usr/share/elasticsearch/data
ports:
- 9201:9200
networks:
- elastic
es03:
image: elasticsearch:7.12.1
container_name: es03
environment:
- node.name=es03
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- data03:/usr/share/elasticsearch/data
networks:
- elastic
ports:
- 9202:9200
volumes:
data01:
driver: local
data02:
driver: local
data03:
driver: local
networks:
elastic:
driver: bridge
资料中已经写好,只需要用xftps上传到虚拟机即可
es运行需要修改一些linux系统权限,修改`/etc/sysctl.conf`文件
vi /etc/sysctl.conf
点击insert键,加入以下代码:
vm.max_map_count=262144
# 添加完以后 点击esc键 然后英文输入法输入:然后在输入wq 保存并退出
然后执行命令,让配置生效:
sysctl -p
通过docker-compose启动集群:
docker-compose up -d
!!!启动集群需要注意的小细节是:跟着我一块做到这里的同学们,一定要把之前的单机es以及kibana关了,不然会因为内存不足 导致部分es启动不成功!!,关闭完以后在运行启动集群的命令。
docker stop es
docker stop kibana
1.2 如何查看集群是否启动成功呢? ——集群状态监控
kibana可以监控es集群,不过新版本需要依赖es的x-pack 功能,配置比较复杂。
这里推荐使用cerebro来监控es集群状态,官方网址:https://github.com/lmenezes/cerebro,资料已经提供安装包
双击bin下面的bat文件,然后在页面输入localhost:9000
如果进不去,查看自己电脑的jdk版本 高于8.0即可
1.3 创建索引库
1) 利用kibana的DevTools创建索引库,在DevTools中输入指令:
PUT /itcast
{
"settings": {
"number_of_shards": 3, // 分片数量
"number_of_replicas": 1 // 副本数量
},
"mappings": {
"properties": {
// mapping映射定义 ...
}
}
}
2)利用cerebro创建索引库
实心:主分片,虚线:负分片。
2. 集群脑裂问题
默认情况下,每个节点都是master eligible节点,因此一旦master节点宕机,其它候选节点会选举一个成为主节点。当主节点与其他节点网络故障时,可能发生脑裂问题。
当发生上述情景时,“老大”与“老二、老三”由于网络问题连接不上了,失联了,二和三会以为一挂了,然后重新竞选老大。也就是上面的node3
因为node1之前连接的数据进行了增删改查,而因为网络堵塞,node3连接的一些也进行增删改查。但是当数据恢复以后,发现两边数据出现了不一致的情况,这就是脑裂。
为了避免脑裂,需要要求选票超过 ( eligible节点数量 + 1 )/ 2 才能当选为主,因此eligible节点数量最好是奇数。对应配置项是discovery.zen.minimum_master_nodes,在es7.0以后,已经成为默认配置,因此一般不会发生脑裂问题。针对上面那个就是数据恢复以后因为node1 只有自己一票 而node3 为两票 刚好等于3+1/2 所以数据恢复以后 node1 就不是主片了。。。
总结:
master eligible节点的作用是什么?
- 参与集群选主
- 主节点可以管理集群状态、管理分片信息、处理创建和删除
- 索引库的请求
data节点的作用是什么?
- 数据的CRUD
coordinator节点的作用是什么?
- 路由请求到其它节点
- 合并查询到的结果,返回给用户
3. 集群故障转移
集群的master节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到其它节点,确保数据安全,这个叫做故障转移
当出现node1主节点发生故障的时候,目前会出现群龙无首的局面,现在 需要重新分配主节点,假如给到了node2,它需要发挥它当“老大”的作用,纵观全局,发现1的副分片不在了,0的主分片不在了,所以要采取措施,看看挂的节点都包含什么分片,然后拿过来。发现了0,1,2的主副都存在,恢复正常运转,确保安全。
模拟:在docker 关闭es01 分片:
docker stop es01
我们会发现1节点的主副分片消失 ,并且由之前的绿色变成了黄色。。
现在需要故障迁移,重新选主。如图可以看出主为es03.
现在我们重新打开es01,会发现 他恢复以后,并不是主了。
总结:
故障转移:
- master宕机后,EligibleMaster选举为新的主节点。
- master节点监控分片、节点状态,将故障节点上的分片转移到正常节点,确保数据安全。
4. 集群分布式存储
在对一个集群比如es01 插入数据时会发现另外两个同样有相同的数据信息。但是我们如何知道数据到底存到了哪个分片呢:
GET /itcast/_search
{
"explain":true, # 查看在哪个分片上
"query":{
"match_all":{}
}
}
当新增文档时,应该保存到不同分片,保证数据均衡,那么coordinating node如何确定数据该存储到哪个分片呢? elasticsearch会通过hash算法来计算文档应该存储到哪个分片:
说明:
- _routing默认是文档的id
- 算法与分片数量有关,因此索引库一旦创建,分片数量不能修改!
新增文档流程:
5. 集群分布式查询
elasticsearch的查询分成两个阶段:
- scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
- gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户
这一期内容就学到这吧,感谢浏览哈,一起加油💪💪