ES分享

1.es

ElasticSearch是⼀一个基于Lucene的开源分布式搜索和分析引擎服务器器。它的特点有:分布式,零配
置,⾃自动发现,索引⾃自动分⽚片,索引副本机制,restful⻛风格接⼝口,多数据源,⾃自动搜索负载等。ElasticSearch这个词由Elasticsearch组成,前者代表可动态扩容,后者代表其功能,当集群环境下es节点数需要扩容时基本能做到只要花钱买两台机器,加入集群,就可以直接使用,非常的方便.我们在什么样的场景下需要es?

  • 当我们的sql存在过多like的时候可以考虑用es来代替mysql
  • **数据过大,需要分布式存储时.
  • 搜索,如日志,商品等等
  • 对数据要求不那么严谨的批处理和流式计算.
  • free-schema比较适合中台的应用

2.概念

1.1 lucene

一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎

1.1.1 索引数据结构

对一段文章或者记录进行切割分词,记录单个词条到文档的映射关系,如,【doc1:我是中国人,doc2:我爱中国。】如果用es提供的标准分词器按照单个中文拆开,则记录如下

term 频率 记录
2 doc1,doc2
1 doc1
2 doc1,doc2
2 doc1,doc2
1 doc1
1 doc2

这样就可以通过对词条的搜索来找到包含记录的文档。

1.1.2 搜索

term doc1 doc2 doc3 doc4
人工 1 1 1 0
智能 1 1 1 0
成为 1 0 0 0
互联网 1 0 1 0
大会 1 0 0 0
焦点 1 0 0 0
谷歌 0 1 0 1
推出 0 1 0 0
开源 0 1 0 1
工具 0 1 0 1

搜索谷歌开源但是不包含大会的文档:

谷歌:0 1 0 1

开源:0 1 0 1

大会:0 1 1 1(不包含大会,则取反)

对这个三个数做与运算-->0101即文档2和文档四包含此次搜索要求。

1.1.3 segment

段是lucene存储的最小单元,每一次写入都会将当前线程索引的数据写入到一个新的segment中,防止多线写入对于同一个句柄文件产生的竞争,但是在搜索时为了防止遍历所有的段,又需要将segement合并到一个大的文件当中去,删除操作和更新都是记录删除的标识,在重新写入一个新的段,后面再合并的时候在去做真正的删除操作。

1.2 es

1.2.1 基本概念

mysql es
index
type
document
字段 field
schema mapping

1.2.2 分片-副本

image

分片和副本是做数据高可用的时候必不可少的一个概念,无论是hdfs还是kafka或者是其他的分布式中间件都会有这样的概念,只是叫法不一样,数据过于大,一台机器放不下,需要将数据切分,也就是分片,可能也有别的中间件叫法不一样,比如kafak叫分区, 副本的作用有两个一个是备份,另外一个是它本身也能提供主分片的功能。这两个功能如果和mysql进行类比,可以类型分片是搞分库,而副本则是主从,在es中,一个索引默认有5个主分片,1个副本,同一个分片的主分片和副本不能落在同一个node上,防止因为机器宕机,而丢失该分片的所有数据,在机器允许的情况下,提高副本分片的数量也能提高系统的qps,分片的丢失也是es常见的报红报黄的问题。

1.2.2 节点

类型 描述
Master 主节点
Data Node 存储数据节点
Coordinating Node 客户端节点
Ingest Node 索引文当前可以做些预处理的工作

1.2.4 zen

zen是es7.0前默认的选举算法,类似于bully算法,基于二阶段提交,假设在完美的情况下有五个机器,3台作为master节点,首先节点启动,es会根据自己的ip和端口号计算出一个id,并与另外两台简历长连接,当检测到长连接的数量已经达到可以选举,并且集群中没有主节点的情况下,开始正式选举,假设a,b,c生成的三个id分别为1,2,3,此时a得知b和c分别为2,3,知道集群中最小id为节点本身,而b,c也知道集群中最小节点为a,以最小id作为评判的标准,此时a,作为候选节点,则等待b,c加入自己,而b,c则主动发送信息,告知要加入a,如果加入成功,并且加入的数量是大于法定人数,则选举成功,当然这个是理想情况,实际情况非常复杂,要考虑消息超时,选举轮次等等因素。

1.2.5 分词器

1.2.5.1 内置分词器
GET _analyze
{
  "analyzer": "standard",
  "text": "不转不是中国人"
}

es内部内置了些分词器,我们也可以根据自己的需求来置顶一些分词器,如果是对于业务开发,常见的分词器就是ik,ik分两种分词,一种是standard分词,就是按照中文一个一个割开,另外一个是smart分词,就是可以根据一些常见的语义来进行分词,在ik目录下有一个分词字典,把常用的词写进去,就能达到智能分词的目的,ik也内置了很多词条。

1.2.5.2 自定义分词器

分词器分词主要是经过三个步骤

1.character filter,这个是处理串的第一个过程,可以对整个串进行处理。

2.tokenizer:按照规则进行分词,如按照空格分开

3.token filter:这个是最后的一个步骤,可以对每个term进行处理。

GET _analyze
{
  "tokenizer" : "whitespace",
  "filter" : ["lowercase"],
  "char_filter" : ["html_strip"],
  "text" : "this is A <b>test</b>"
}

这个过程就是先对text文本内容先去掉html标签,在按空格切开,最后把所有的串变成小写,也可以在filter指定去掉停用词,去掉this is等用法。

1.2.6 写入

1.客户端向协调协调节点发送请求,协调节点将写请求转发直master节点.

2.master节点根据id确定主分片的位置,如果没有id此时会生成id,会并将数据请求转发至主分片所在的节点

3.主分片进行写操作,先是进行双写操作,写translog后在写buffer,后面会有一个sync线程定期将内存的数据刷到磁盘上(refresh_interval),而刷到磁盘时只是将数据刷到系统的oscache中,后面就是操作系统的事情了,所以线上在部署es时,假如32g内存的机器,至少要留一半的大小给系统缓存,不仅能加速写的过程,也会加速读的过程.

4.当主节点写入成功后,需要将数据转发至副本节点,进行写入,副本写入成功数受quorum( int( (primary + number_of_replicas) / 2 ) + 1 )的影响,这个也是一个法定人数的概念,写入超过一半认为此次写入成功.

5.每次写入会生成一个segment,然后后面会有一个purge线程,定期合并segment.

1.2.7 搜索

1.将搜索的语句用索引时相同的分词器进行分词.

2.协调节点将请求转发至每个主分片或者副本分片

3.每个分片进行搜索的操作和打分,并将执行结果的id和排序值返回给协调节点,协调节点在进行排序(query_then_fetch),协调节点在根据负载均衡策略,去分片上回取数据.

4.每个分片进行搜索的操作和打分,并将执行结果的文档返回给协调节点,协调节点在进行排序(query_and_fetch)

query_and_fetch和query_then_fetch

3.语法

3.1 查询语法

3.1.1 term

term查询应该是最好理解的,就是搜索语句刚好是一个term,可以直接在倒排序索引中搜索.如:

PUT test/uk/1
{
  "word":"我是中国人"
}

PUT test/uk/2
{
  "word":"我来自江苏"
}

GET test/uk/_search
{
  "query": {
    "term": {
      "word": {
        "value": "我"
      }
    }
  }
}

##返回

  "hits": {
    "total": 2,
    "max_score": 0.2876821,
    "hits": [
      {
        "_index": "test",
        "_type": "uk",
        "_id": "2",
        "_score": 0.2876821,
        "_source": {
          "word": "我来自江苏"
        }
      },
      {
        "_index": "test",
        "_type": "uk",
        "_id": "1",
        "_score": 0.2876821,
        "_source": {
          "word": "我是中国人"
        }
      }
    ]


3.1.2 match

match相对term多做了个动作,就是会对搜索语句进行分词,属于全文搜索.如

GET test/uk/_search
{
  "query": {
    "match": {
      "word": "我是"
    }
  }
}

###返回

    "hits": [
      {
        "_index": "test",
        "_type": "uk",
        "_id": "1",
        "_score": 0.5753642,
        "_source": {
          "word": "我是中国人"
        }
      },
      {
        "_index": "test",
        "_type": "uk",
        "_id": "2",
        "_score": 0.2876821,
        "_source": {
          "word": "我来自江苏"
        }
      }
    ]

这个时候,只要分完词后只要包含这几个分词结果的文档就都出来了,是or的关系,也可以指定要满足与的关系,如

GET test/uk/_search
{
  "query": {
    
    "match": {
      "word": {
        "query": "我是",
        "operator": "and"
      }
    }
  }
}
###返回
"hits": [
      {
        "_index": "test",
        "_type": "uk",
        "_id": "1",
        "_score": 0.5753642,
        "_source": {
          "word": "我是中国人"
        }
      }
    ]

另外es在生成文档的时候帮我们生成了一个keyword关键字,我们可以用keyword关键字做精确查询,如:

GET test/uk/_search
{
  "query": {
    "match": {
      "word.keyword": "我是中国人"
    }
  }
}
###返回
 "hits": [
      {
        "_index": "test",
        "_type": "uk",
        "_id": "1",
        "_score": 0.2876821,
        "_source": {
          "word": "我是中国人"
        }
      }
    ]

3.1.3 match_phrase

match_phrase 是另外一种玩法,它可以控制分词后词条之间间隔的距离,如:

GET test/uk/_search
{
  "query": {
    
  "match_phrase": {
    "word": {
      "query": "我国",
      "slop":1
    }
  }
  }
}

因为此时之间的间隔为2,当指定为1时,无法查出.

3.1.4 bool

bool语法可以实现多级的or和and和not的关系,比如我现在查询这样一段话:这个语句中必须要包含中国,但是不能包含这个term,语法如下:

GET test/uk/_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {
          "word": {
            "value": "我"
          }
        }},{
          "match": {
            "word": "中国"
          }
        }
      ],
      "must_not": [
        {"term": {
          "word": {
            "value": "江"
          }
        }}
      ]
    }
  }
}

3.1.5 filter

filter语法和query的语法差不多,把最外层的query换成filter即可,如:

GET test/uk/_search
{
  "post_filter": {
    "match": {
      "word.keyword": "我是中国人"
    }
  }
}
##返回
 "hits": [
      {
        "_index": "test",
        "_type": "uk",
        "_id": "1",
        "_score": 1,
        "_source": {
          "word": "我是中国人"
        }
      }

但是可以和上面那一条对比下,这条的score得分为1,而上面是一个小于1的数,所以filter是不会走打分机制的,正因为不需要打分,es也能将这部分数据直接缓存,当我们不需要使用打分的时候,建议多用filter.

3.1.6 paninless

painless是es内置的脚本语言,可以用painless进行批量处理,多次聚合等操作,语法和java类似,如,将只要包含这个语句的替换成我特么黑化

POST test/uk/_update_by_query
{
  "query":{
    "match":{
      "word":"我"
    }
  },
  "script": {
    "lang": "painless",
    "inline": "ctx._source.word = params.word;",
    "params":{
      "word":"我特么黑化"
      
    }
  }
}

3.2 聚合语法

聚合是es提供的另外一个功能,可以根据term进行做分组,而且可以多次下钻,然后做min,max等操作,聚合总共分为指标聚合,桶聚合, 矩阵聚合 , 管道聚合 ,前面两种用的比较多,指标聚合的意思就是计算分组和的min,max值,而桶聚合的意思是将分组后的数据进行分析,可以算各个分组里面的一些指标和数据.聚合使用的不是倒排序索引,而是正排序索引,如果需要对text字段进行聚合需要打开fielddata功能,不然无法使用.

例子来与极客时间(Elasticsearch核心技术与实战)

DELETE /employees
PUT /employees/
{
  "mappings" : {
      "properties" : {
        "age" : {
          "type" : "integer"
        },
        "gender" : {
          "type" : "keyword"
        },
        "job" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 50
            }
          }
        },
        "name" : {
          "type" : "keyword"
        },
        "salary" : {
          "type" : "integer"
        }
      }
    }
}

PUT /employees/_bulk
{ "index" : {  "_id" : "1" } }
{ "name" : "Emma","age":32,"job":"Product Manager","gender":"female","salary":35000 }
{ "index" : {  "_id" : "2" } }
{ "name" : "Underwood","age":41,"job":"Dev Manager","gender":"male","salary": 50000}
{ "index" : {  "_id" : "3" } }
{ "name" : "Tran","age":25,"job":"Web Designer","gender":"male","salary":18000 }
{ "index" : {  "_id" : "4" } }
{ "name" : "Rivera","age":26,"job":"Web Designer","gender":"female","salary": 22000}
{ "index" : {  "_id" : "5" } }
{ "name" : "Rose","age":25,"job":"QA","gender":"female","salary":18000 }
{ "index" : {  "_id" : "6" } }
{ "name" : "Lucy","age":31,"job":"QA","gender":"female","salary": 25000}
{ "index" : {  "_id" : "7" } }
{ "name" : "Byrd","age":27,"job":"QA","gender":"male","salary":20000 }
{ "index" : {  "_id" : "8" } }
{ "name" : "Foster","age":27,"job":"Java Programmer","gender":"male","salary": 20000}
{ "index" : {  "_id" : "9" } }
{ "name" : "Gregory","age":32,"job":"Java Programmer","gender":"male","salary":22000 }
{ "index" : {  "_id" : "10" } }
{ "name" : "Bryant","age":20,"job":"Java Programmer","gender":"male","salary": 9000}
{ "index" : {  "_id" : "11" } }
{ "name" : "Jenny","age":36,"job":"Java Programmer","gender":"female","salary":38000 }
{ "index" : {  "_id" : "12" } }
{ "name" : "Mcdonald","age":31,"job":"Java Programmer","gender":"male","salary": 32000}
{ "index" : {  "_id" : "13" } }
{ "name" : "Jonthna","age":30,"job":"Java Programmer","gender":"female","salary":30000 }
{ "index" : {  "_id" : "14" } }
{ "name" : "Marshall","age":32,"job":"Javascript Programmer","gender":"male","salary": 25000}
{ "index" : {  "_id" : "15" } }
{ "name" : "King","age":33,"job":"Java Programmer","gender":"male","salary":28000 }
{ "index" : {  "_id" : "16" } }
{ "name" : "Mccarthy","age":21,"job":"Javascript Programmer","gender":"male","salary": 16000}
{ "index" : {  "_id" : "17" } }
{ "name" : "Goodwin","age":25,"job":"Javascript Programmer","gender":"male","salary": 16000}
{ "index" : {  "_id" : "18" } }
{ "name" : "Catherine","age":29,"job":"Javascript Programmer","gender":"female","salary": 20000}
{ "index" : {  "_id" : "19" } }
{ "name" : "Boone","age":30,"job":"DBA","gender":"male","salary": 30000}
{ "index" : {  "_id" : "20" } }
{ "name" : "Kathy","age":29,"job":"DBA","gender":"female","salary": 20000}

# Metric 聚合,找到最低的工资
POST employees/_search
{
  "size": 0,
  "aggs": {
    "min_salary": {
      "min": {
        "field":"salary"
      }
    }
  }
}

# Metric 聚合,找到最高的工资
POST employees/_search
{
  "size": 0,
  "aggs": {
    "max_salary": {
      "max": {
        "field":"salary"
      }
    }
  }
}



# 一个聚合,输出多值
POST employees/_search
{
  "size": 0,
  "aggs": {
    "stats_salary": {
      "stats": {
        "field":"salary"
      }
    }
  }
}







#Salary Ranges 分桶,可以自己定义 key
POST employees/_search
{
  "size": 0,
  "aggs": {
    "salary_range": {
      "range": {
        "field":"salary",
        "ranges":[
          {
            "to":10000
          },
          {
            "from":10000,
            "to":20000
          },
          {
            "key":">20000",
            "from":20000
          }
        ]
      }
    }
  }
}









3.2.1 聚合不准确

1.cardinality等一些操作采用近似的算法,提升性能

2./假设我们有一个索引,被分了三个shard,分布在三个不同的机器上,文档中某一个字段为英文字母A,B,C等等,现在对这个字段进行聚合,找出文档中数量排名前五的聚合结果:

分片1 分片2 分片三
A(25) A(30) A(45)
B(18) B(25) C(44)
C(6) F(17) Z(36)
D(3) Z(16) G(30)
D(2) G(15) E(29)
F(2) H(14) H(28)
G(2) I(10) Q(2)
H(2) Q(6) D(1)
I(1) J(8)
J(1) C(4)

然后在每个分片中取前五个,在协调节点中再次聚合:

分片1 分片2 分片三
A(25) A(30) A(45)
B(18) B(25) C(44)
C(6) F(17) Z(36)
D(3) Z(16) B(30)
D(2) G(15) E(29)

聚合结果

排名 term
1 A(100)
2 Z(52)
3 C(50)
4 G(45)
5 B(43)

实际情况中,C有6+4+44=54个,这是因为在数据节点返回的时候,并未将排名第六以后的数据返回给协调节点,我们也可以通过调大shard_size的参数返回更多的数据,从而提高数据的准确性,但是过大的size会增加集群的负担,降低集群性能,所以如果对数据的准确性要求没那么高的情况下,可以使用es的聚合功能,如果数据准确性很高,请考虑hadoop或者flink这类工具.

4.状态检查

# 集群健康
GET /_cat/health?v
# 查询文档总数
GET /_cat/count?v
#查询某个索引的
GET /_cat/count/movies?v
# 查看索引
GET  /_cat/indices
#查看某一个索引
GET  /_cat/indices/movies?v
#查看yellow状态的索引
GET /_cat/indices?v&health=yellow
#根据文本数进行倒排序
GET  /_cat/indices?v&s=docs.count:desc

es在使用过程中,经常会遇到状态变黄变红的情况,变黄的原因是副本分片的丢失,此时不影响写入和查询,变红是因为主分片丢失,会影响写入,常见的状态原因如下:

# 查看所有分片
GET _cat/shards

#根据索引正则
GET _cat/shards/m*?v

#unassigned.reason可以查看未分配的原因
GET _cat/shards?h=index,shard,prirep,state,unassigned.reason
state状态描述:
INDEX_CREATED          Unassigned as a result of an API creation of an index.

CLUSTER_RECOVERED          Unassigned as a result of a full cluster recovery.

INDEX_REOPENED            Unassigned as a result of opening a closed index.

DANGLING_INDEX_IMPORTED        Unassigned as a result of importing a dangling index引入危险索引()

NEW_INDEX_RESTORED Unassigned as a result of restoring into a new index.

EXISTING_INDEX_RESTORED Unassigned as a result of restoring into a closed index.

REPLICA_ADDED Unassigned as a result of explicit addition of a replica.

ALLOCATION_FAILED Unassigned as a result of a failed allocation of the shard.

NODE_LEFT Unassigned as a result of the node hosting it leaving the cluster.

REROUTE_CANCELLED Unassigned as a result of explicit cancel reroute command.

REINITIALIZED When a shard moves from started back to initializing, for example, with shadow replicas.

REALLOCATED_REPLICA A better replica location is identified and causes the existing replica allocation to be cancelled.

#查看unassigned.reason具体解读
GET /_cluster/allocation/explain
{
  "index": "myindex",
  "shard": 0,
  "primary": true
}

变红或者变黄在短暂时间内是正常现象,但是持续时间很长的时候就需要注意了,我之前遇到es变红的比较多的原因主要有两个.一个是磁盘被打满,数据写不进去,另外一个是节点宕机,所以这两种情况需要找运维同学协助排查问题.

5.使用技巧

1.esjar包冲突比较严重,也不仅仅是es的问题,底层netty包经常会和别的jar包进行冲突,而且jar包要和线上es版本一致,es客户端版本兼容问题很头疼,主要原因是Lucene升级不兼容,Lucene有一个类专门用来记录版本号,密密麻麻,这种情况可以在idea里面安装个maven analyzer,可以方便排查冲突.

2.语法,es语法比较复杂,熟记我们常用的就行了,再不会的去翻翻文档,这里面有个小技巧,就是可以利用kibana,可以搜索完看下kibana发的请求,然后去构造这个json.自己写完构造请求,可以输出下toString方法,看是不是一样.

3.可视化客户端,es有很多可视化客户端,如果没有监控啥需求,仅仅是看看数据,可以在chrome安装个head插件即可.

6.推荐环节

1.官方文档

2.极客时间- Elasticsearch核心技术与实战

3.Elasticsearch顶尖高手系列-核心知识篇 + 高手进阶篇,bilibili有(顺便安利一波,bilibili真的是码农的好网站)

4.elasticsearch源码解析与优化实战

5.Lucene实战

6.es中文网站,可以订阅

7. 高可用 Elasticsearch 集群 21 讲

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容