苏飞论坛

 找回密码
 马上注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

分布式系统框架(V2.0) 轻松承载百亿数据,千万流量!讨论专区 - 源码下载 - 官方教程

HttpHelper万能框架(V2.3-含.netcore) HttpHelper官方出品,无敌框架讨论区 - 源码下载 - 在线测试和代码生成

HttpHelper爬虫类(V2.0) 最牛的爬虫类,没有爬不到只有想不到 源码 - 代码生成器 - 讨论区 - 教程- 例子

查看: 655|回复: 4

[搜索引擎] 55-Elasticsearch解决并发问题

[复制链接]
发表于 2019-1-10 14:36:40 | 显示全部楼层 |阅读模式
解决并发问题

当我们允许多个人 同时 重命名文件或目录时,问题就来了。 设想一下,你正在对一个包含了成百上千文件的目录 /clinton 进行重命名操作。 同时,另一个用户对这个目录下的单个文件 /clinton/projects/elasticsearch/README.txt 进行重命名操作。 这个用户的修改操作,尽管在你的操作后开始,但可能会更快的完成。

以下有两种情况可能出现:

  • 你决定使用 version (版本)号,在这种情况下,当与 README.txt 文件重命名的版本号产生冲突时,你的批量重命名操作将会失败。
  • 你没有使用版本控制,你的变更将覆盖其他用户的变更。


问题的原因是 Elasticsearch 不支持 ACID 事务。 对单个文件的变更是 ACIDic 的,但包含多个文档的变更不支持。

如果你的主要数据存储是关系数据库,并且 Elasticsearch 仅仅作为一个搜索引擎 或一种提升性能的方法,可以首先在数据库中执行变更动作,然后在完成后将这些变更复制到 Elasticsearch。 通过这种方式,你将受益于数据库 ACID 事务支持,并且在 Elasticsearch 中以正确的顺序产生变更。 并发在关系数据库中得到了处理。

如果你不使用关系型存储,这些并发问题就需要在 Elasticsearch 的事务水准进行处理。 以下是三个切实可行的使用 Elasticsearch 的解决方案,它们都涉及某种形式的锁:

  • 全局锁
  • 文档锁
  • 树锁



当使用一个外部系统替代 Elasticsearch 时,本节中所描述的解决方案可以通过相同的原则来实现。

全局锁

通过在任何时间只允许一个进程来进行变更动作,我们可以完全避免并发问题。 大多数的变更只涉及少量文件,会很快完成。一个顶级目录的重命名操作会对其他变更造成较长时间的阻塞,但可能很少这样做。

因为在 Elasticsearch 文档级别的变更支持 ACIDic,我们可以使用一个文档是否存在的状态作为一个全局锁。 为了请求得到锁,我们尝试 create 全局锁文档:
[C#] 纯文本查看 复制代码
PUT /fs/lock/global/_create

{}

如果这个 create 请求因冲突异常而失败,说明另一个进程已被授予全局锁,我们将不得不稍后再试。 如果请求成功了,我们自豪的成为全局锁的主人,然后可以继续完成我们的变更。一旦完成,我们就必须通过删除全局锁文档来释放锁:

[C#] 纯文本查看 复制代码
DELETE /fs/lock/global

根据变更的频繁程度以及时间消耗,一个全局锁能对系统造成大幅度的性能限制。 我们可以通过让我们的锁更细粒度的方式来增加并行度。

文档锁
我们可以使用前面描述相同的方法技术来锁定个体文档,而不是锁定整个文件系统。 我们可以使用 scrolled search 检索所有的文档,这些文档会被变更影响因此每一个文档都创建了一个锁文件:

[C#] 纯文本查看 复制代码
PUT /fs/lock/_bulk

{ "create": { "_id": 1}} 

{ "process_id": 123    } 

{ "create": { "_id": 2}}

{ "process_id": 123    }



  • lock 文档的 ID 将与应被锁定的文件的 ID 相同。
  • process_id 代表要执行变更进程的唯一 ID。


如果一些文件已被锁定,部分的 bulk 请求将失败,我们将不得不再次尝试。

当然,如果我们试图再次锁定 所有 的文件, 我们前面使用的 create 语句将会失败,因为所有文件都已被我们锁定! 我们需要一个 update 请求带 upsert 参数以及下面这个 script ,而不是一个简单的 create 语句:
[C#] 纯文本查看 复制代码
if ( ctx._source.process_id != process_id ) { 

  assert false;  

}

ctx.op = 'noop'; 



  • process_id 是传递到脚本的一个参数。
  • assert false 将引发异常,导致更新失败。
  • 将 op 从 update 更新到 noop 防止更新请求作出任何改变,但仍返回成功。


完整的 update 请求如下所示:
[C#] 纯文本查看 复制代码
POST /fs/lock/1/_update

{

  "upsert": { "process_id": 123 },

  "script": "if ( ctx._source.process_id != process_id )

  { assert false }; ctx.op = 'noop';"

  "params": {

    "process_id": 123

  }

}

如果文档并不存在, upsert 文档将会被插入--和前面 create 请求相同。 但是,如果该文件 确实 存在,该脚本会查看存储在文档上的 process_id 。 如果 process_id 匹配,更新不会执行( noop )但脚本会返回成功。 如果两者并不匹配, assert false 抛出一个异常,你也知道了获取锁的尝试已经失败。

一旦所有锁已成功创建,你就可以继续进行你的变更。

之后,你必须释放所有的锁,通过检索所有的锁文档并进行批量删除,可以完成锁的释放:
[C#] 纯文本查看 复制代码
POST /fs/_refresh 



GET /fs/lock/_search?scroll=1m 

{

    "sort" : ["_doc"],

    "query": {

        "match" : {

            "process_id" : 123

        }

    }

}



PUT /fs/lock/_bulk

{ "delete": { "_id": 1}}

{ "delete": { "_id": 2}}



  • refresh 调用确保所有 lock 文档对搜索请求可见。
  • 当你需要在单次搜索请求返回大量的检索结果集时,你可以使用 scroll 查询。


文档级锁可以实现细粒度的访问控制,但是为数百万文档创建锁文件开销也很大。 在某些情况下,你可以用少得多的工作量实现细粒度的锁定,如以下目录树场景中所示。

树锁

在前面的例子中,我们可以锁定的目录树的一部分,而不是锁定每一个涉及的文档。 我们将需要独占访问我们要重命名的文件或目录,它可以通过 独占锁 文档来实现:

[C#] 纯文本查看 复制代码
{ "lock_type": "exclusive" }

同时我们需要共享锁定所有的父目录,通过 共享锁 文档:

[C#] 纯文本查看 复制代码
{

  "lock_type":  "shared",

  "lock_count": 1 

}



lock_count 记录持有共享锁进程的数量。

对 /clinton/projects/elasticsearch/README.txt 进行重命名的进程需要在这个文件上有 独占锁 , 以及在 /clinton 、 /clinton/projects 和 /clinton/projects/elasticsearch 目录有 共享锁 。

一个简单的 create 请求将满足独占锁的要求,但共享锁需要脚本的更新来实现一些额外的逻辑:

[C#] 纯文本查看 复制代码
if (ctx._source.lock_type == 'exclusive') {

  assert false; 

}

ctx._source.lock_count++ 



如果 lock_type 是 exclusive (独占)的,assert 语句将抛出一个异常,导致更新请求失败。



否则,我们对 lock_count 进行增量处理。

这个脚本处理了 lock 文档已经存在的情况,但我们还需要一个用来处理的文档还不存在情况的 upsert 文档。 完整的更新请求如下:

[C#] 纯文本查看 复制代码
POST /fs/lock/%2Fclinton/_update 

{

  "upsert": { 

    "lock_type":  "shared",

    "lock_count": 1

  },

  "script": "if (ctx._source.lock_type == 'exclusive')

  { assert false }; ctx._source.lock_count++"

}



  • 文档的 ID 是 /clinton ,经过URL编码后成为 %2fclinton 。
  • upsert 文档如果不存在,则会被插入。


一旦我们成功地在所有的父目录中获得一个共享锁,我们尝试在文件本身 create 一个独占锁:

[C#] 纯文本查看 复制代码
PUT /fs/lock/%2Fclinton%2fprojects%2felasticsearch%2fREADME.txt/_create

{ "lock_type": "exclusive" }

现在,如果有其他人想要重新命名 /clinton 目录,他们将不得不在这条路径上获得一个独占锁:

[C#] 纯文本查看 复制代码
PUT /fs/lock/%2Fclinton/_create

{ "lock_type": "exclusive" }

这个请求将失败,因为一个具有相同 ID 的 lock 文档已经存在。 另一个用户将不得不等待我们的操作完成以及释放我们的锁。独占锁只能这样被删除:

[C#] 纯文本查看 复制代码
DELETE /fs/lock/%2Fclinton%2fprojects%2felasticsearch%2fREADME.txt

共享锁需要另一个脚本对 lock_count 递减,如果计数下降到零,删除 lock 文档:
[C#] 纯文本查看 复制代码
if (--ctx._source.lock_count == 0) {

  ctx.op = 'delete' 

}



  • 一旦 lock_count 达到0, ctx.op 会从 update 被修改成 delete 。


此更新请求将为每级父目录由下至上的执行,从最长路径到最短路径:

[C#] 纯文本查看 复制代码
POST /fs/lock/%2Fclinton%2fprojects%2felasticsearch/_update

{

  "script": "if (--ctx._source.lock_count == 0) { ctx.op = 'delete' } "

}

树锁用最小的代价提供了细粒度的并发控制。当然,它不适用于所有的情况--数据模型必须有类似于目录树的顺序访问路径才能使用。


这三个方案--全局、文档或树锁--都没有处理锁最棘手的问题:如果持有锁的进程死了怎么办?

一个进程的意外死亡给我们留下了2个问题:
  • 我们如何知道我们可以释放的死亡进程中所持有的锁?
  • 我们如何清理死去的进程没有完成的变更?

这些主题超出了本书的范围,但是如果你决定使用锁,你需要给对他们进行一些思考。

当非规范化成为很多项目的一个很好的选择,采用锁方案的需求会带来复杂的实现逻辑。 作为替代方案,Elasticsearch 提供两个模型帮助我们处理相关联的实体: 嵌套的对象 和 父子关系 。

发表于 2019-1-10 14:55:35 | 显示全部楼层
发表于 2019-1-10 15:33:55 | 显示全部楼层
发表于 2019-1-10 16:42:30 | 显示全部楼层
发表于 2019-1-10 16:45:26 | 显示全部楼层
您需要登录后才可以回帖 登录 | 马上注册

本版积分规则

QQ|手机版|小黑屋|手机版|联系我们|关于我们|广告合作|苏飞论坛 ( 豫ICP备17001017号-1)

GMT+8, 2019-11-22 00:36

© 2017-2018

快速回复 返回顶部 返回列表