Es 索引查询排序分析

文章目录

    • 概要
    • 一、Es数据存储
        • 1.1、_source
        • 1.2、stored fields
    • 二、Doc values
        • 2.1、FieldCache
        • 2.2、DocValues
    • 三、Fielddata
    • 四、Index sorting
    • 五、小结
    • 六、参考

概要

倒排索引 优势在于快速的查找到包含特定关键词的所有文档,但是排序,过滤、聚合等操作并不是倒排索引所擅长的。

假设文档有两个字段,一个被分词的字段name,一个日期date。现在要查出包含花朵的所有文档中的按日期正序的前10个,如果仅仅有倒排索引该如何实现的呢?
答:在name字段的倒排索引上找到包含关键词花朵的所有文档ID,然后获取date排序字段的值,根据date值实时排序,取前10个。
这种办法数据量少的时候还可以,如果索引有百万级以上的数据,就很低效了。显然,MySQL、MongoDB等数据库按字段建立索引进行预排序更高效。

可以看到只有倒排索引的话,排序是非常耗时的。Es维护组投入了大量时间和精力来优化排序查询,随着版本更新,在Es6基本完善了这一块工作,分三部分:

  1. Docvalues
  2. Fielddata
  3. index sorting

Es对搜索与排序的区别解释很到位:

Search needs to answer the question “Which documents contain this term?”, while sorting and aggregations need to answer a different question: “What is the value of this field for this document?”.

一、Es数据存储

Es是基于Lucene引擎的,在Lucene中索引文档时,原始字段信息经过分词、转换处理后形成倒排索引,而原始内容本身并不直接保留。因此,Lucene为了检索时能够获取到字段的原始值而设计了stored fields。另外Es自己追了一个_source字段,保存了用户插入文档的字段值。

1.1、_source

Es将文档内容以json形式存储在一个叫_source的字段中,其默认是开启的。在Lucene层面,_source仅仅是一个普通字段,当开启时,我们插入一个文档,Es会自动把文档内容序列化,赋值给_source字段,组成新的文档结构,再交由Lucene处理,大致流程如下:

Es _source写入过程
可以看到,在Lucene层面_source字段和其他字段并无特别之处。我们一般按如下方式调整_source字段:

PUT my-index
{
  "mappings": {
    "_source": {
      "enabled": true,  #是否开启_source字段,默认开启
      "includes": [ #开启_source字段是包含哪些字段,支持正则
        "*.count",
        "meta.*"
      ],
      "excludes": [ #开启_source字段是排除哪些字段,支持正则
        "meta.description",
        "meta.other.*"
      ]
    }
  }
}

PS:一般情况下我们都是开启_source字段的,因为关闭_source后update、update_by_query、reindex等API将无法正常使用。

1.2、stored fields

stored fields是Lucene引擎自带的特性,在Es层面可通过store属性来控制,默认不开启。

PUT my-index
{
  "mappings": {
    "properties": {
      "name": { 
        "type":  "text"  #默认关闭name字段的stored field
      },
      "age": { 
        "type":  "long",
        "store": true   #开启age字段的stored field
      }
    }
  }
}

那么如果即开启_source字段,某个字段的stored fields也开启,那么这个字段值会被存储两次,所以字段stored fields属性一般不开启,如果开启最好通过_source的excludes属性排除该字段即可。

演示如下:

[root@test ~]# curl -XPUT http://127.0.0.1:9200/test -H  'Content-Type: application/json' -d '{"mappings":{"properties":{"status_code":{"type":"text"},"session_id":{"type":"keyword","store":true}}}}'
{"acknowledged":true,"shards_acknowledged":true,"index":"test"}
[root@test ~]# curl -XGET http://127.0.0.1:9200/test/_doc/1?pretty
{
  "_index" : "test",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "status_code" : "xxx bob",
    "session_id" : "999"
  }
}
[root@test ~]# curl -XGET http://127.0.0.1:9200/test/_doc/1?pretty\&stored_fields=status_code
{
  "_index" : "test",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true
}
[root@test ~]# curl -XGET http://127.0.0.1:9200/test/_doc/1?pretty\&stored_fields=status_code,session_id
{
  "_index" : "test",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "fields" : {
    "session_id" : [
      "999"
    ]
  }
}

Lucene的stored fields属于正排索引,由于字段值经过分词转换为倒排索引后,原始值并未保存,为了根据doc id快速获取字段原始值而设计的。在Lucene下的应用场景是当需要返回原始字段值时,就需要对对应字段设置stored fields; 在Es下由于设计了_source字段,一般不需开启,根据实际场景二者结合使用即可。

值得注意的是,如果 _source 为不开启,并且字段 store 属性也为 false ,字段值就无法返回了,但是如果字段index属性为true的话,还是可以通过该字段查询文档的,如果字段index属性为false的话,该字段既不能查询,也不会被返回,是无效字段。

二、Doc values

默认情况下,大多数字段都会被建立倒排索引,这使得它们可以进行搜索。但是排序、聚合和脚本操作中的字段值需要获取文档某个字段的具体值。Doc values就是专为这种场景而诞生的,简单来说就是实现根据doc id快速获取doc中某个字段值的目标。

doc values是列式存储,主要包含doc id和字段具体值(可能被压缩),属于正排索引。Es默认每个字段的 doc values 都是开启的(不支持textannotated_text类型字段)。

Doc values是Lucene引擎自带的特性,从Lucene V4.0开始引入的,在4.0之前都是依赖fieldCache机制,用于缓存字段值。

2.1、FieldCache

FieldCache,即字段缓存,作用是加载所有文档中某个特定字段的值到内存,便于随机获取某些文档中该字段的值。在FieldCache出现之前,当用户需要访问各文档中某个字段的值时,IndexSearcher.doc(docId)获得Document的所有字段值,但访问速度比较慢,而且只能获得被stored字段的值。

于是Lucene 在 3.0 提出了字段缓存(FieldCache)的概念,这个 API 主要是在内部使用,但开发者也可以外部调用,在自定义排序或者自定义评分的时候都涉及到相关的接口。准确的说,就是将相关的字段的值加载到内存中,形成一维数组。加载字段缓存,并不要求相关字段的值必须在索引中被stored,而是要求必须 index,并且不能分词,实际上字段缓存的值是从倒排索引中转换而来。

FieldCache主要用于以下场景:

  • 加速对特定字段值的访问;
  • 支持脚本中对字段值进行操作;
  • 用于排序、聚合等操作。

具体原理见下图:
Lucene FieldCache
但是这种方式有很大的不足:

  • 内存消耗大:常驻内存,大小是文档个数 * 字段类型大小;
  • 性能开销大:初始加载过程耗时,需要遍历倒排索引及类型转换。可能导致其他请求延迟;
  • 内存管理复杂:内存管理不够灵活,可能导致频繁的GC及性能问题。

Lucene 自 4.0 起提出了一个新的概念 DocValues, 允许用户在建索引的时候将相关字段的doc id 和字段值 之间的映射直接保存在索引文件中(磁盘)。因此,在 lucene4.0 中,Lucene 在根据doc id查询字段值时,首先会根据doc id 去对应DocValues中获取字段的值。如果 DocValues 中不存在,才会从倒排索引中进行类型转换,再加载到内存中,即FieldCache机制。也就是说,默认会使用 DocValues代替FieldCache机制。

PS:事实上,查了下,在 lucene5 的 API 中,FieldCache 已经被取消了,大家了解曾经有这么个机制即可
Lucene 4可以看到,Lucene 5就没有了。

2.2、DocValues

DocValues相比FieldCache机制,大大节约内存,获取字段值快;其大概结构如下:
| doc id |field value |
| 1 | 500 |
| 2 | 900 |
| 3 | 1500 |
| 4 | 100 |

使用方式:

PUT my-index
{
  "mappings": {
    "properties": {
      "status_code": { 
        "type":  "keyword"  #默认开启status_code字段的doc value
      },
    "session_id": { 
        "type":  "long",
        "doc_values": false   #关闭session_id字段的doc value
      }
    }
  }
}
  • 生成时机: doc values 在文档被插入的时候与倒排索引同期生成,即Es 为了提高查询排序速度,插入文档时,同时把字段的值加入倒排索引中,也会把字段的值加入到doc values中(空间换时间)。
  • 存储位置:磁盘,使用时会被加载到内存。

通过下面案例可以清晰看到textannotated_text类型的字段不支持doc_vlues属性

[root@test ~]# curl -XPUT http://127.0.0.1:9200/test -H  'Content-Type: application/json' -d '{"mappings":{"properties":{"status_code":{"type":"text","doc_vlues":true},"session_id":{"type":"keyword","doc_values":false}}}}'
{"error":{"root_cause":[{"type":"mapper_parsing_exception","reason":"unknown parameter [doc_vlues] on mapper [status_code] of type [text]"}]}}"

Es 中textannotated_text类型的字段不支持doc_vlues属性,主要是因为Lucene中Docvalues机制保存的是字段原始值,并不支持分词后的结果;

为了更好地理解Es如何使用Lucene的DocValues机制,我们可以看一下相关API;

public class LuceneExample {
    public static void main(String[] args) throws Exception {
        // 创建一个内存目录来存储索引
        Directory directory = new RAMdirectory();
        // 使用标准分析器
        StandardAnalyzer analyzer = new StandardAnalyzer();
        // 配置IndexWriter
        IndexWriterConfig config = new IndexWritereConfig(analyzer);
        // 创建IndexWriter
        IndexWriter iwriter = new IndexWriter(directory, config);
        // 创建文档并添加字段
        Document doc = new Document();
        doc.add(new StringField("id", "1", Field.Store.YES));//字段id被建立倒排索引并存储其原始值,并且不会被分词
        doc.add(new TextField("title", "Lucene", Field.Store.YES));//字段title被建立倒排索引并存储其原始值,但会被分词
        doc.add(new TextField("content", "Hello Lucene,My is...", Field.Store.NO));//字段content被建立倒排索引但不存储其原始值,且会被分词
        doc.add(new LongField("createTime", 1672564789001L));//对字段createTime建立倒排索引
        doc.add(new StoredField("createTime", 1672564789001L));//存储字段createTime原始值
        doc.add(new StoredField("name", "Bob"));//只存储字段name原始值
        doc.add(new NumericDocValuesField(name, 1672564789001L));//对字段createTime建立docvalues
        doc.add(new SortedNumericDocValuesField("list", 3));//对字段list建立docvalues
        doc.add(new SortedNumericDocValuesField("list", 9));//对字段list建立docvalues
        // 将文档写入索引
        iwriter.addDocument(doc);
        // 关闭写入器
        iwriter.close();
    }
}

通过这个案例就可以想到我们就文档提交给Es后,Es也是这样调用Lucene的API来完成工作。Es会根据mapping中字段配置来组装doc:
案例中name和createTime字段的mapping配置如下:

{
	"name": {
		"type": "text""index": false,
		"store": true,
		"doc_values": false

	},
	"age": {
		"type": "long",
		"index": true,
		"store": true,
		"doc_values": true
	}
}

Es在调用Lucene的API时,除了我们提交的字段,Es还会预留的系统字段,用作一些特殊的目的,这些字段对于Lucene来说与用户自定义的Field无任何区别,只不过Es根据这些系统字段不同的使用目的,定制有不同的索引方式,比如_uid、_source字段等,_uid字段就设置成会被索引,不需要分词,不需要被Store,不需要Docvalues。

看到这里就有些同学会问题,Lucene除了建立倒排索引,还有Store,Docvalues,三者都会存储字段值,如果都设置了,一个字段值岂不是被存了三份?
说来确实是这样,三者各自有不同的作用,倒排索引用于快速定位哪些文档包含搜索关键词,Store是为了查询时返回文档中字段原始值;Docvalues是为了查询时便于进行排序、聚合等操作。
因为Store是正排行式存储,便于一次获取文档中所有字段的值(单行多列);Docvalues是正排列式存储,便于快速获取单个字段多个值(单列多行)。
在实际业务中,根据业务需求和性能要求,灵活选择设置这些属性即可。

三、Fielddata

Fielddata是Es自身的特性,与Lucene无关,Es之所以会维护Fielddata,就是因为Lucene的Docvalues不支持textannotated_text类型的字段,本质是Docvalues只存储字段原始值,不支持存储字段值分词后的结果。

Fielddata是Es用来对textannotated_text类型的字段进行排序、聚合、脚本计算等操作的机制。默认关闭,此时无法对该类型字段进行排序等操作。

PUT my-index-000001/_mapping
{
  "properties": {
    "name": { 
      "type":     "text",
      "fielddata": true,   #字段name开启Fielddata特性
      "fielddata_frequency_filter": {
          "min": 0.001,
          "max": 0.1,
          "min_segment_size": 500
       }
    }
  }
}

只有text和annotated_text类型字段支持Fielddata特性。

  • 生成时机:使用时动态生成,懒加载;
  • 存储位置:JVM内存,不占用磁盘空间;

看到这里有同学会觉得Es的Fielddata和Lucene的FieldCache很相似,没错,很像,但:

  1. Es的Fielddata只支持text和annotated_text类型字段,Lucene的FieldCache支持所有;
  2. Es的Fielddata使用了更紧凑的内存表示,内存消耗更小;
  3. Es的Fielddata通过优化的数据结构,提高排序、聚合等操作;
  4. Es的Fielddata提供更灵活的内存管理方式,如用户通过fielddata_frequency_filter参数过滤掉低频词汇,节约内存。

Es通过Fielddata和Docvallues实现对所有字段排序、聚合等操作的支持。

Fielddata预加载

四、Index sorting

Index sorting属于Lucene特性。

Lucene 早期曾引入了一个离线的排序工具——IndexSorter。IndexSorter 把需要排序的索引完全复制了一份,将新的复制索引中的文档按用户指定的顺序重新排序。因为排序后的索引是一个新的索引,每次源索引中有新的数据更新,不得不重新执行一遍这个工具。IndexSorter 工具是第一次在索引写入阶段而不是查询阶段对文档进行排序的尝试。
针对索引预排序,社区提出了一个新的概念“early termination”。假设你要遍历出前N个文档,并且文档是按 date 字段排序的。如果索引存储在磁盘上时已经是有序的了,那么我们遍历出前N个文档就可以直接返回,而不需要遍历所有的文档。这就是我们所说的“early termination”。提早的返回查询结果,可以明显的缩短查询响应时间,特别是含有排序的查询。刚才介绍的离线排序的方案不能满足有大量文档更新的场景,这也是为什么最终离线排序方案会被其他方案取代。

Es排序定义如下:
单Field排序定义:

PUT events
{
    "settings" : {
        "index" : {  #按timestamp倒序
            "sort.field" : "timestamp",
            "sort.order" : "desc" 
        }
    },
    "mappings": {
        "properties": {
            "timestamp": {
                "type": "date"
            }
        }
    }
}

多字段排序定义:

PUT events
{
    "settings" : {
        "index" : {
            "sort.field" : ["timestamp","age"],
            "sort.order" : ["desc","desc"] 
        }
    },
    "mappings": {
        "properties": {
            "timestamp": {
                "type": "date"
            },
            "age":{
                "type": "integer"
            }
        }
    }
}

Es Settings支持 Index Sorting 4个配置:

  1. index.sort.field:对应排序的Field,可指定一个或者多个Field,优先按第一个排序,相同的情况下在按后续的Field排序;
  2. index.sort.order:对应Field的排序规则,只能时asc或者时desc;
  3. index.sort.mode:当对应的Field是数组时,取Max或者Min的值作为排序的基准;
  4. index.sort.missing:当对应的Field为null时,排第一个还是最后一个。

索引排序是一个很不错的解决方案,但是只能以单一方式进行索引排序。索引排序并不适用于具有下列特征的排序查询:

  • 采用不同的排序标准,例如索引定义的是降序,你查询用的是升序;
  • 所用排序字段组合不同于索引排序定义中指定的组合。

我们这里也看看Lucene中的API:

IndexWriterConfig config = new IndexWritereConfig(analyzer);
SortField sf1 = new SortField("createTime",SortField.Type.LONG)
Sort sorts = new Sort(new SortField[]{sf1}) //支持多个字段
config.setIndexSort(sorts); //设置索引排序定义

可以看到Es就是根据用户的settings.index配置,进而转换成调用Lucene的这些API来完成工作的。

由于索引排序是在索引生成阶段完成的,根据测试不可避免的降低了写入性能,此功能最多可将写入速度降低 40-50%。由于排序是一个很耗时的操作,所以如果您最关注的是索引速度,则必须谨慎考虑是否要开启索引排序功能。
Es Index sort benchmark

Lucene针对索引排序做了很多优化,在新segment生成后刷盘时对段内所有文档进行一次排序,在segment merge时对所有段的文档进行排序(此时segment内文档有序,相当于合并有序数组 O(N))。

五、小结

通过前几章节的描述可知,Es排序是一个复杂的场景,在优化过程中不断地打补丁,不像MySQL,MongoDB依赖B+tree实现。主要分为:

  1. 实时排序:text类型字段依赖Fielddata机制,其他类型字段依赖Docvalues机制;
  2. 预排序:在创建索引时定义排序规则,一个索引只能定义一个,限制较大。

六、参考

1]:Es官网_source字段
2]:Es官网store属性
3]:Es官网doc_values属性
4]:Lucene官网FIELD对像
5]:Es Fielddata知识点梳理
6]:Es 优化排序查询
7]:Es 索引排序为人不知的优势-减少索引磁盘占用
8]:Es 索引预排序分析和英文版
9]:Lucene Fieldcache机制原理
10]:Lucene源码解析——DocValue存储方式
11]:Lucene 段合并原理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/714218.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

并发容器(二):Concurrent类下的ConcurrentHashMap源码级解析

并发容器-ConcurrentHashMap 前言数据结构JDK1.7版本HashEntrySegment 初始化 重要方法Put方法扩容rehash方法 前言 之前我们在集合篇里聊完了HashMap和HashTable,我们又学习了并发编程的基本内容,现在我们来聊一聊Concurrent类下的重要容器&#xff0c…

tsp可视化python

随机生成点的坐标并依据点集生成距离矩阵,通过点的坐标实现可视化 c代码看我的这篇文章tsp动态规划递归解法c from typing import List, Tuple import matplotlib.pyplot as plt from random import randintN: int 4 MAX: int 0x7f7f7f7fdistances: List[List[in…

最长不下降子序列LIS详解

最长不下降子序列指的是在一个数字序列中,找到一个最长的子序列(可以不连续),使得这个子序列是不下降(非递减)的。 假如,现有序列A[1,2,3,-1,-2&…

60.WEB渗透测试-信息收集- 端口、目录扫描、源码泄露(8)

免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动! 内容参考于: 易锦网校会员专享课 上一个内容:59.WEB渗透测试-信息收集- 端口、目录扫描、源码泄露(7) 御剑是用…

植物大战僵尸杂交版全新版v2.1解决全屏问题

文章目录 🚋一、植物大战僵尸杂交版❤️1. 游戏介绍💥2. 如何下载《植物大战僵尸杂交版》 🚀二、解决最新2.1版的全屏问题🌈三、画质增强以及减少闪退 🚋一、植物大战僵尸杂交版 《植物大战僵尸杂交版》是一款在原版《…

【Android】三种常见的布局LinearLayout、GridLayout、RelativeLayout

【Android】三种常见的布局LinearLayout、GridLayout、RelativeLayout 在 Android 开发中,布局(Layout)是构建用户界面的基础。通过合理的布局管理,可以确保应用在不同设备和屏幕尺寸上都能有良好的用户体验。本文将简单介绍 And…

困惑度作为nlp指标的理解示例

为了更清晰地说明困惑度的计算过程以及如何通过困惑度判断模型的优劣,我们可以通过一个简单的例子来演示。假设我们有一个非常简单的文本语料库和两个基础的语言模型进行比较。 示例文本 假设我们的文本数据包括以下两个句子: “cat sits on the mat”…

蔡崇信“预言”:微软与OpenAI未来极有可能会分道扬镳

近日,在美国投行摩根大通于上海举行的第二十届全球中国峰会上,阿里巴巴集团联合创始人、董事局主席蔡崇信与摩根大通北亚区董事长兼大中华区投资银行业务副主席关金星(Kam Shing Kwang)进行了一场精彩对话。蔡崇信深入分享了他对公…

线上教育培训办公系统系统的设计

管理员账户功能包括:系统首页,个人中心,管理员管理,教师管理,学生管理,运营事件管理 教师账户功能包括:系统首页,个人中心,学生管理,作业管理,电…

OpenCore 引导完美升级

备份原有 OC (做好回滚的准备下载新版 OpenCore https://github.com/acidanthera/OpenCorePkg/releases将 1, 3, 4 里面的文件使用新版进行替换 4 里面的文件严格来说并不需要, 只是留着方便使用不追求完美到这就可以收工了将 OC 复制到 U 盘 EFI U 盘格式化可以使用: diskutil…

js 用正则表达式 匹配自定义字符之间的字符串数据,如:( )、[ ]、{ }、< >、【】等括号之间的字符串数据

要使用正则表达式匹配尖括号()之间的数据,可以使用以下代码示例: 在JavaScript中,你可以使用正则表达式来匹配括号()之间的数据。以下是一个简单的例子,它展示了如何使用正则表达式来获取两对括号之间的文本。 // 示例字符串 con…

Spring-kafka消费者消费的一些问题

前言 Spring Kafka 无缝集成了 Spring Boot、Spring Framework 及其生态系统中的其他项目,如 Spring Cloud。通过与 Spring Boot 的自动配置结合,开发者可以快速启动和配置 Kafka 相关的功能。无需编写大量样板代码即可实现 Kafka 的生产和消费功能&…

现在用U盘的人还多吗?多用于哪些场景?

在公司中使用U盘的人仍然相当多,主要在以下场景下使用: 数据存储与备份:U盘作为一种便携式存储设备,被广泛应用于数据的存储和备份。对于需要经常在不同设备或地点之间传输数据的员工来说,U盘提供了一个方便、快捷的解…

如何使用ios自带语音转文字工具?

ios自带语音转文字是iOS系统中自带的语音转文字功能主要应用于以下几个方面: 1. 语音输入:在iOS的任何文本输入框中,通常都有一个麦克风图标,点击后可以进行语音输入,系统会将你的语音实时转换成文字。 2. Siri&…

ESD与EOS区别

最近小白在做项目时,被一个实习生问道了,关于EOS与ESD区别。说实话,以前专注于测试debug的我,在回答对方时,并没法做到太全面的解答。于是乎,借助周内的空闲时间,小白还是简单学习总结了一番。 …

OceanBase 金融项目优化案例

领导让我帮忙支持下其他项目的SQL优化工作,呦西,是收集案例的好机会。😍 下面SQL都是在不能远程的情况下,按照原SQL的逻辑等价改写完成发给现场同学验证。 案例一 慢SQL,4.32秒: SELECT MY_.*, RM FROM (SE…

【MATLAB】(高数)

参考文章 函数极限 导数与偏导 极值和最值 局部范围的最值 局部范围内的最值,相当于函数的极值 离散数据的最值 多元函数的极值 fminunc [x, fval] fminunc(fun, x0)fun为代求极值的函数;x0为起始点,即从这个点开始寻找极值,…

Ui学习--UITableView

UI学习 UITableView基础UITableView协议UITableView高级协议与单元格总结 UITableView基础 UITableView作为iOS中的一个控件,用于以表格形式展示数据。例如通讯录好友,朋友圈信息等,都是UITableView的实际运用场景。 首先我们先要加入两个协…

苹果加大AI布局,上海新店开业昭示中国市场新动向

随着全球科技巨头纷纷进军人工智能领域,苹果公司亦不甘示弱,近期在上海静安新店的开业以及CEO蒂姆库克的一系列动作,都显示出苹果在AI方面的雄心壮志。这不仅是对未来技术趋势的积极回应,更是对市场竞争态势的精准把握。 库克的访…

CSS从入门到精通——动画:CSS3动画延迟和完成后状态的保持

目录 任务描述 相关知识 动画状态 动画完成时的状态 动画延迟 编程要求 任务描述 本关任务:用 CSS3 实现小车等待红绿灯的效果。效果图如下: 相关知识 为了完成本关任务,你需要掌握:1.动画状态,2.动画完成时的状…