ElasticSearch基础之 权威指南笔记(二)

news/2024/7/7 5:56:17 标签: elasticsearch, 大数据, big data

数据输入和输出

创建新文档

当我们索引一个文档,怎么确认我们正在创建一个完全新的文档,而不是覆盖现有的呢?

请记住, _index 、 _type 和 _id 的组合可以唯一标识一个文档。所以,确保创建一个新文档的最简单办法是,使用索引请求的 POST 形式让 Elasticsearch 自动生成唯一 _id :

POST /website/blog/
{ ... }

然而,如果已经有自己的 _id ,那么我们必须告诉 Elasticsearch ,只有在相同的 _index 、 _type 和 _id 不存在时才接受我们的索引请求。这里有两种方式,他们做的实际是相同的事情。使用哪种,取决于哪种使用起来更方便。

第一种方法使用 op_type 查询-字符串参数:

PUT /website/blog/123?op_type=create
{ ... }

第二种方法是在 URL 末端使用 /_create :

PUT /website/blog/123/_create
{ ... }

如果创建新文档的请求成功执行,Elasticsearch 会返回元数据和一个 201 Created 的 HTTP 响应码。

另一方面,如果具有相同的 _index 、 _type 和 _id 的文档已经存在,Elasticsearch 将会返回 409 Conflict 响应码。

索引文档

通过使用 index API ,文档可以被 索引 —— 存储和使文档可被搜索。

使用自定义的 ID
如果你的文档有一个自然的标识符 (例如,一个 user_account 字段或其他标识文档的值),你应该使用如下方式的 index API 并提供你自己 _id :

PUT /{index}/{type}/{id}
{
  "field": "value",
  ...
}

在 Elasticsearch 中每个文档都有一个版本号。当每次对文档进行修改时(包括删除), _version 的值会递增。 在 处理冲突 中,我们讨论了怎样使用 _version 号码确保你的应用程序中的一部分修改不会覆盖另一部分所做的修改。
Autogenerating IDs
如果你的数据没有自然的 ID, Elasticsearch 可以帮我们自动生成 ID 。 请求的结构调整为: 不再使用 PUT 谓词(“使用这个 URL 存储这个文档”), 而是使用 POST 谓词(“存储文档在这个 URL 命名空间下”)。

现在该 URL 只需包含 _index 和 _type :

POST /website/blog/
{
  "title": "My second blog entry",
  "text":  "Still trying this out...",
  "date":  "2014/01/01"
}

除了 _id 是 Elasticsearch 自动生成的,响应的其他部分和前面的类似:

{
   "_index":    "website",
   "_type":     "blog",
   "_id":       "AVFgSgVHUP18jI2wRx0w",
   "_version":  1,
   "created":   true
}

自动生成的 ID 是 URL-safe、 基于 Base64 编码且长度为20个字符的 GUID 字符串。 这些 GUID 字符串由可修改的 FlakeID 模式生成,这种模式允许多个节点并行生成唯一 ID ,且互相之间的冲突概率几乎为零。

更新整个文档

在 Elasticsearch 中文档是 不可改变 的,不能修改它们。相反,如果想要更新现有的文档,需要 重建索引 或者进行替换, 我们可以使用相同的 index API 进行实现,在 索引文档 中已经进行了讨论。

PUT /website/blog/123
{
  "title": "My first blog entry",
  "text":  "I am starting to get the hang of this...",
  "date":  "2014/01/02"
}

在响应体中,我们能看到 Elasticsearch 已经增加了 _version 字段值:

{
  "_index" :   "website",
  "_type" :    "blog",
  "_id" :      "123",
  "_version" : 2,
  "created":   false 
}

created 标志设置成 false ,是因为相同的索引、类型和 ID 的文档已经存在。

在内部,Elasticsearch 已将旧文档标记为已删除,并增加一个全新的文档。 尽管你不能再对旧版本的文档进行访问,但它并不会立即消失。当继续索引更多的数据,Elasticsearch 会在后台清理这些已删除文档。

在本章的后面部分,我们会介绍 update API, 这个 API 可以用于 partial updates to a document 。 虽然它似乎对文档直接进行了修改,但实际上 Elasticsearch 按前述完全相同方式执行以下过程:

1、从旧文档构建 JSON
2、更改该 JSON
3、删除旧文档
4、索引一个新文档
唯一的区别在于, update API 仅仅通过一个客户端请求来实现这些步骤,而不需要单独的 get 和 index 请求。

文档的部分更新

在 更新整个文档 , 我们已经介绍过 更新一个文档的方法是检索并修改它,然后重新索引整个文档,这的确如此。然而,使用 update API 我们还可以部分更新文档,例如在某个请求时对计数器进行累加。

文档是不可变的:他们不能被修改,只能被替换。 update API 必须遵循同样的规则。 从外部来看,我们在一个文档的某个位置进行部分更新。然而在内部, update API 简单使用与之前描述相同的 检索-修改-重建索引 的处理过程。 区别在于这个过程发生在分片内部,这样就避免了多次请求的网络开销。通过减少检索和重建索引步骤之间的时间,我们也减少了其他进程的变更带来冲突的可能性。

update 请求最简单的一种形式是接收文档的一部分作为 doc 的参数, 它只是与现有的文档进行合并。对象被合并到一起,覆盖现有的字段,增加新的字段。 例如,我们增加字段 tags 和 views 到我们的博客文章,如下所示:

POST /website/blog/1/_update
{
   "doc" : {
      "tags" : [ "testing" ],
      "views": 0
   }
}

如果请求成功,我们看到类似于 index 请求的响应:

{
   "_index" :   "website",
   "_id" :      "1",
   "_type" :    "blog",
   "_version" : 3
}

使用脚本部分更新文档
脚本可以在 update API中用来改变 _source 的字段内容, 它在更新脚本中称为 ctx._source 。 例如,我们可以使用脚本来增加博客文章中 views 的数量:

POST /website/blog/1/_update
{
   "script" : "ctx._source.views+=1"
}

分片内部原理

在 集群内的原理, 我们介绍了 分片, 并将它 描述成最小的 工作单元 。但是究竟什么 是 一个分片,它是如何工作的? 在这个章节,我们回答以下问题:

为什么搜索是 近 实时的?
为什么文档的 CRUD (创建-读取-更新-删除) 操作是 实时 的?
Elasticsearch 是怎样保证更新被持久化在断电时也不丢失数据?
为什么删除文档不会立刻释放空间?
refresh, flush, 和 optimize API 都做了什么, 你什么情况下应该使用他们?

我们将要审视提供一个带近实时搜索和分析的 分布式持久化数据存储需要解决的问题

使文本可被搜索

必须解决的第一个挑战是如何使文本可被搜索。 传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值(这里指单词)的能力。

最好的支持 一个字段多个值 需求的数据结构是我们在 倒排索引 章节中介绍过的 倒排索引 。 倒排索引包含一个有序列表,列表包含所有文档出现过的不重复个体,或称为 词项 ,对于每一个词项,包含了它所有曾出现过文档的列表。
在这里插入图片描述

Elasticsearch 中的 文档 是有字段和值的结构化 JSON 文档。事实上,在 JSON 文档中, 每个被索引的字段都有自己的倒排索引。

这个倒排索引相比特定词项出现过的文档列表,会包含更多其它信息。它会保存每一个词项出现过的文档总数, 在对应的文档中一个具体词项出现的总次数,词项在文档中的顺序,每个文档的长度,所有文档的平均长度,等等。这些统计信息允许 Elasticsearch 决定哪些词比其它词更重要,哪些文档比其它文档更重要,这些内容在 什么是相关性? 中有描述。

为了能够实现预期功能,倒排索引需要知道集合中的 所有 文档,这是需要认识到的关键问题。

不变性
倒排索引被写入磁盘后是 不可改变 的:它永远不会修改。 不变性有重要的价值:

  • 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
  • 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
  • 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
  • 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。

当然,一个不变的索引也有不好的地方。主要事实是它是不可变的! 你不能修改它。如果你需要让一个新的文档 可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。

动态更新索引

下一个需要被解决的问题是怎样在保留不变性的前提下实现倒排索引的更新?答案是: 用更多的索引。

通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到—​从最早的开始—​查询完后再对结果进行合并。

Elasticsearch 基于 Lucene, 这个 java 库引入了 按段搜索 的概念。 每一 段 本身都是一个倒排索引, 但 索引 在 Lucene 中除表示所有 段 的集合外, 还增加了 提交点 的概念 — 一个列出了所有已知段的文件,
在这里插入图片描述

索引与分片的比较
被混淆的概念是,一个 Lucene 索引 我们在 Elasticsearch 称作 分片 。 一个 Elasticsearch 索引 是分片的集合。 当 Elasticsearch 在索引中搜索的时候, 他发送查询到每一个属于索引的分片(Lucene 索引),然后像 执行分布式检索 提到的那样,合并每个分片的结果到一个全局的结果集。

新的文档首先被添加到内存索引缓存中,然后写入到一个基于磁盘的段
逐段搜索会以如下流程进行工作:

1、新文档被收集到内存索引缓存, 见 Figure 17, “一个在内存缓存中包含新文档的 Lucene 索引” 。
2、不时地, 缓存被 提交 :

  • 一个新的段—​一个追加的倒排索引—​被写入磁盘。
  • 一个新的包含新段名字的 提交点 被写入磁盘。
  • 磁盘进行 同步 — 所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件。

3、新的段被开启,让它包含的文档可见以被搜索。
4、内存缓存被清空,等待接收新的文档。
在这里插入图片描述

在这里插入图片描述

删除和更新

段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个 .del 文件,文件中会列出这些被删除文档的段信息

当一个文档被 “删除” 时,它实际上只是在 .del 文件中被 标记 删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。

文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。

在 段合并 , 我们展示了一个被删除的文档是怎样被文件系统移除的。

近实时搜索

随着按段(per-segment)搜索的发展,一个新的文档从索引到可被搜索的延迟显著降低了。新文档在几分钟之内即可被检索,但这样还是不够快。

磁盘在这里成为了瓶颈。提交(Commiting)一个新的段到磁盘需要一个 fsync 来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。 但是 fsync 操作代价很大; 如果每次索引一个文档都去执行一次的话会造成很大的性能问题。

我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着 fsync 要从整个过程中被移除。

在Elasticsearch和磁盘之间是文件系统缓存。像之前描述的一样, 在内存索引缓冲区中的文档会被写入到一个新的段中,但是这里新段会被先写入到文件系统缓存—​这一步代价会比较低,稍后再被刷新到磁盘—​这一步代价比较高。不过只要文件已经在缓存中, 就可以像其它文件一样被打开和读取了。

Lucene 允许新段被写入和打开—​使其包含的文档在未进行一次完整提交时便对搜索可见。 这种方式比进行一次提交代价要小得多,并且在不影响性能的前提下可以被频繁地执行。

在这里插入图片描述

refresh API

在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做 refresh 。 默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说 Elasticsearch 是 近 实时搜索: 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。

这些行为可能会对新用户造成困惑: 他们索引了一个文档然后尝试搜索它,但却没有搜到。这个问题的解决办法是用 refresh API 执行一次手动刷新:

POST /_refresh 
POST /blogs/_refresh 

尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候, 手动刷新很有用,但是不要在生产环境下每次索引一个文档都去手动刷新。 相反,你的应用需要意识到 Elasticsearch 的近实时的性质,并接受它的不足。

并不是所有的情况都需要每秒刷新。可能你正在使用 Elasticsearch 索引大量的日志文件, 你可能想优化索引速度而不是近实时搜索, 可以通过设置 refresh_interval , 降低每个索引的刷新频率:

PUT /my_logs
{
  "settings": {
    "refresh_interval": "30s" #每30秒刷新 my_logs 索引。
  }
}

refresh_interval 可以在既存索引上进行动态更新。 在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来:

PUT /my_logs/_settings
{ "refresh_interval": -1 } #关闭自动刷新。

PUT /my_logs/_settings
{ "refresh_interval": "1s" } #每秒自动刷新。

refresh_interval 需要一个 持续时间 值, 例如 1s (1 秒) 或 2m (2 分钟)。 一个绝对值 1 表示的是 1毫秒 --无疑会使你的集群陷入瘫痪。

持久化变更

如果没有用 fsync 把数据从文件系统缓存刷(flush)到硬盘,我们不能保证数据在断电甚至是程序正常退出之后依然存在。为了保证 Elasticsearch 的可靠性,需要确保数据变化被持久化到磁盘。

在 动态更新索引,我们说一次完整的提交会将段刷到磁盘,并写入一个包含所有段列表的提交点。Elasticsearch 在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片。

即使通过每秒刷新(refresh)实现了近实时搜索,我们仍然需要经常进行完整提交来确保能从失败中恢复。但在两次提交之间发生变化的文档怎么办?我们也不希望丢失掉这些数据。

Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录。通过 translog ,整个流程看起来是下面这样:
1、一个文档被索引之后,就会被添加到内存缓冲区,并且 追加到了 translog ,
在这里插入图片描述
2、刷新(refresh)使分片处于 Figure 22, “刷新(refresh)完成后, 缓存被清空但是事务日志不会” 描述的状态,分片每秒被刷新(refresh)一次:

  • 这些在内存缓冲区的文档被写入到一个新的段中,且没有进行 fsync 操作。
  • 这个段被打开,使其可被搜索。
  • 内存缓冲区被清空。
    在这里插入图片描述
    3、这个进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志(
    在这里插入图片描述
    4、每隔一段时间—​例如 translog 变得越来越大—​索引被刷新(flush);一个新的 translog 被创建,并且一个全量提交被执行
  • 所有在内存缓冲区的文档都被写入一个新的段。
  • 缓冲区被清空。
  • 一个提交点被写入硬盘。
  • 文件系统缓存通过 fsync 被刷新(flush)。
  • 老的 translog 被删除。
    在这里插入图片描述

translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。

translog 也被用来提供实时 CRUD 。当你试着通过ID查询、更新、删除一个文档,它会在尝试从相应的段中检索之前, 首先检查 translog 任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。

flush API

这个执行一个提交并且截断 translog 的行为在 Elasticsearch 被称作一次 flush 。 分片每30分钟被自动刷新(flush),或者在 translog 太大的时候也会刷新。请查看 translog 文档 来设置,它可以用来 控制这些阈值:
flush API 可以被用来执行一个手工的刷新(flush):

POST /blogs/_flush  #刷新(flush) blogs 索引。

POST /_flush?wait_for_ongoing #	刷新(flush)所有的索引并且等待所有刷新在返回前完成。

你很少需要自己手动执行 flush 操作;通常情况下,自动刷新就足够了。
这就是说,在重启节点或关闭索引之前执行 flush 有益于你的索引。当 Elasticsearch 尝试恢复或重新打开一个索引, 它需要重放 translog 中所有的操作,所以如果日志越短,恢复越快。

Translog 有多安全?

translog 的目的是保证操作不会丢失。这引出了这个问题: Translog 有多安全?

在文件被 fsync 到磁盘前,被写入的文件在重启之后就会丢失。默认 translog 是每 5 秒被 fsync 刷新到硬盘, 或者在每次写请求完成之后执行(e.g. index, delete, update, bulk)。这个过程在主分片和复制分片都会发生。最终, 基本上,这意味着在整个请求被 fsync 到主分片和复制分片的translog之前,你的客户端不会得到一个 200 OK 响应。

在每次请求后都执行一个 fsync 会带来一些性能损失,尽管实践表明这种损失相对较小(特别是bulk导入,它在一次请求中平摊了大量文档的开销)。

但是对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync 还是比较有益的。比如,写入的数据被缓存到内存中,再每5秒执行一次 fsync 。

这个行为可以通过设置 durability 参数为 async 来启用:

PUT /my_index/_settings
{
    "index.translog.durability": "async",
    "index.translog.sync_interval": "5s"
}

这个选项可以针对索引单独设置,并且可以动态进行修改。如果你决定使用异步 translog 的话,你需要 保证 在发生crash时,丢失掉 sync_interval 时间段的数据也无所谓。请在决定前知晓这个特性。

如果你不确定这个行为的后果,最好是使用默认的参数( “index.translog.durability”: “request” )来避免数据丢失。

index.translog.sync_interval
fsync无论写操作 如何,translog 被写入磁盘并提交的频率。默认为5s. 100ms不允许小于的值。
index.translog.durability
是否fsync在每次索引、删除、更新或批量请求后提交 translog。此设置接受以下参数:

  • request
    (默认)fsync并在每次请求后提交。在硬件故障的情况下,所有确认的写入都已经提交到磁盘。
  • async
    fsync并在后台提交每个sync_interval. 如果发生故障,自上次自动提交以来的所有已确认写入都将被丢弃。

index.translog.flush_threshold_size :事务日志存储尚未安全地持久化在 Lucene 中的所有操作(即不是 Lucene 提交点的一部分)。尽管可以读取这些操作,但是如果分片已停止并且必须恢复,则需要重播它们。此设置控制这些操作的最大总大小,以防止恢复花费太长时间。一旦达到最大大小,将进行刷新,从而生成新的 Lucene 提交点。默认为 512mb 。

index.translog.retention.size
要保留的 translog 文件的总大小。保留更多的 translog 文件会增加在恢复副本时执行基于操作的同步的机会。如果 translog 文件不足,副本恢复将回退到基于文件的同步。默认为512mb

index.translog.retention.age
translog 文件将被保留的最长时间。默认为12h.

段合并

由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。

Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。

段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。
启动段合并不需要你做任何事。进行索引和搜索时会自动进行。
在这里插入图片描述
1、当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。

2、 合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。

3、 Figure 26, “一旦合并结束,老的段被删除” 说明合并完成时的活动:

  • 新的段被刷新(flush)到了磁盘。 ** 写入一个包含新段且排除旧的和较小的段的新提交点。
  • 新的段被打开用来搜索。
  • 老的段被删除。
    在这里插入图片描述
    合并大的段需要消耗大量的I/O和CPU资源,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然 有足够的资源很好地执行。

段和合并
Elasticsearch 默认设置在这块比较保守:不希望搜索性能被后台合并影响。不过有时候(尤其是 SSD,或者日志场景)限流阈值太低了。

默认值是 20 MB/s,对机械磁盘应该是个不错的设置。如果你用的是 SSD,可以考虑提高到 100–200 MB/s。测试验证对你的系统哪个值合适:

PUT /_cluster/settings
{
    "persistent" : {
        "indices.store.throttle.max_bytes_per_sec" : "100mb"
    }
}

如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并限流。这样让你的索引速度跑到你磁盘允许的极限:

PUT /_cluster/settings
{
    "transient" : {
        "indices.store.throttle.type" : "none" 
        #设置限流类型为 none 彻底关闭合并限流。等你完成了导入,记得改回 merge 重新打开限流。
    }
}

最后,你可以增加 index.translog.flush_threshold_size 设置,从默认的 512 MB 到更大一些的值,比如 1 GB。这可以在一次清空触发的时候在事务日志里积累出更大的段。而通过构建更大的段,清空的频率变低,大段合并的频率也变低。这一切合起来导致更少的磁盘 I/O 开销和更好的索引速率。当然,你会需要对应量级的 heap 内存用以积累更大的缓冲空间,调整这个设置的时候请记住这点。


http://www.niftyadmin.cn/n/1248827.html

相关文章

一键录屏,敏捷绘制 | MixLab工具推荐

超强屏幕录制和注释工具:ScreenityScreenity 是一个强大的屏幕记录和注释工具,你只需要在Chrome插件中安装就能随意录制。它还可以让你在录制的画面上进行标注事项,使能相机进行录制视频。特征:???? 浏览器窗口、桌面、任何应…

安卓APP设计规范(三)1080*1920设计稿对应开发尺寸

原文:http://www.25xt.com/appdesign/9487.html 第一部分app设计知识:什么是安卓1080P? 目前市场上,只有Android系统的手机才有1080P全高清的产品。1080P是说手机屏幕的分辨率达到1920*1080像素。 目前为止,市面上Andr…

当机器具备跨模态感知能力后,会有智商吗?| MixLab人工智能

大家好,我叫大铮,这是我的第一个专栏。我们将从脑科学,语言学,符号逻辑,决策推理等多个角度深入探讨什么是跨模态感知,再从人工智能的角度,来认识跨模态感知的具体实现。希望通过这个专栏能让大…

Android微信/QQ红包自动抢(AccessibilityService)

关于抢红包的文章已经很多了,我再来总结下,QQ的顺便也实现下,原理很简单,搜索屏幕中的文字,搜索到了就点击,QQ相对来说要简单一些 关键代码: AccessibilityNodeInfo nodeInfo getRootInActiv…

空间可视化|Mix+人工智能No.49

Knowledge/ def mix( ):本期收录新增11NO. 49#AR# #创意# 火柴人创作设计:用户能在手机或者现实空间自由绘制火柴人,手机 AR 识别后火柴人能由静止变化为多种动态舞姿。#空间# #可视化# 感知用户的行为并进行可视化十分重要,请看下面这个例子…

分布式Id生成算法

温故知新-分布式-分布式Id生成算法&踩过的坑 1、SnowFlake 理解分布式id生成算法SnowFlake 分布式id生成算法的有很多种,Twitter的SnowFlake就是其中经典的一种。 概述 SnowFlake算法生成id的结果是一个64bit大小的整数,它的结构如下图&#xff1…

Android基础-VideoView (几行代码实现本地和网络视频播放)

下面看具体代码&#xff1a; activity_paly.xml <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools"android…

从2018到2020,看一个系统的演进

很偶然翻到 iBrandUp &#xff08;Mixlab 人物专栏&#xff09; 2 年前做的专访&#xff1a;利用业余时间&#xff0c;shadow创建了一个由技术咖、设计咖实验室演变而来的社区——MixLab无界社区。主要是为了把不同领域价值的人群有效融合在一起&#xff0c;他希望能打造出全球…