1. 图解ElasticSearch并发冲突问题

image.png
上面说的这个过程,其实就是 ES 中的并发冲突问题,会导致数据不准确:

  • 有些场景下,其实是无所谓的,不关注这个数据不准确的事情,比如说,我们如果就只是简单的将数据写入ES,无论数据是什么样的,都可以;还有些情况下,即使是算错了,也可以;(比如文章阅读数量统计,评论数量统计)

  • 当并发操作 ES 的线程越来越多,或者并发请求越多;或者是读取一份数据,供用户查阅和操作的时间越长,因为这段时间里很可能数据在 ES 中已经被修改了,那么我们拿到的就是旧数据,基于旧数据去操作,后面结果肯定就错了;

2. 图解悲观锁和乐观锁两种并发控制方案

image.png
image.png
ES 使用乐观锁并发控制方案;
总结:

  • 悲观锁并发控制

    • 悲观锁的含义:我认为每次更新都有冲突的可能,并发更新这种操作特别不靠谱,我只相信只有严格按我定义的粒度进行串行更新,才是最安全的,一个线程更新时,其他的线程等着,前一个线程更新完成后,下一个线程再上。
    • 关系型数据库中广泛使用该方案,常见的表锁、行锁、读锁、写锁,依赖redis或memcache等实现的分布式锁,都属于悲观锁的范畴。明显的特征是后续的线程会被挂起等待,性能一般来说比较低,不过自行实现的分布式锁,粒度可以自行控制(按行记录、按客户、按业务类型等),在数据正确性与并发性能方面也能找到很好的折衷点。
  • 乐观锁并发控制

    • 乐观锁的含义:我认为冲突不经常发生,我想提高并发的性能,如果真有冲突,被冲突的线程重新再尝试几次就好了。
    • Elasticsearch 默认使用的是乐观锁方案,前面介绍的 _version 字段,记录的就是每次更新的版本号,只有拿到最新版本号的更新操作,才能更新成功,其他拿到过期数据的更新失败,由客户端程序决定失败后的处理方案,一般是重试。

3. 图解ElasticSearch内部如何基于_version进行乐观锁并发控制

3.1 Replica Shard 数据同步并发控制

在 Elasticsearch 内部,每当 primary shard 收到新的数据时,都需要向 replica shard 进行数据同步,这种同步请求特别多,并且是异步的。如果同一个 document 进行了多次修改,Shard 同步的请求是无序的,可能会出现”后发先至”的情况,如果没有任何的并发控制机制,那结果将无法相像。

Shard 的数据同步也是基于内置的 _version 进行乐观锁并发控制的。

例如 Java 客户端向 Elasticsearch 某条 document 发起更新请求,共发出3次,Java 端有严谨的并发请求控制,在 ElasticSearch 的 primary shard 中写入的结果是正确的,但 Elasticsearch 内部数据启动同步时,顺序不能保证都是先到先得,情况可能是这样,第三次更新请求比第二次更新请求先到,如下图:image.png
如果 Elasticsearch 内部没有并发的控制,这个 document 在 replica 的结果可能是 test2,并且与 primary shard 的值不一致,这样肯定错了。

预期的更新顺序应该是 test1—>test2—>test3,最终的正确结果是 test3。那 Elasticsearch 内部是如何做的呢?

Elasticsearch 内部在更新 document 时,会比较一下 version,如果请求的 version 与 document 的 version 相等,就做更新,如果 document 的 version 已经大于请求的 version,说明此数据已经被后到的线程更新过了,此时会丢弃当前的请求,最终的结果为 test3。

此时的更新顺序为 test1—>test2,最终结果也是对的。

3.2 Partial Update 乐观锁并发控制

image.png

  • partial update 内置乐观锁并发控制

    • 如果拿到的 _version 低于 partial update 操作时现有 document 的 _version,那么 es 会自动把此次 partial update fail 掉,让 partial update 失效;
  • retry_on_conflict

    • partial update 并发冲突时,如果 _version 低于现有的 document 的 _version,默认内置乐观锁会让此操作不生效;
    • 如果要保证此次 partial update 操作尽可能的执行成功,可以添加 retry_on_conflict 参数,进行多次更新尝试;
    • retry 策略:
      • 再次获取 document 数据和最新版本号;
      • 基于最新版本号再次去更新,如果成功那么就 ok 了;
      • 如果失败,重复1和2两个步骤,最多重复几次呢?可以通过 retry 那个参数的值指定,比如5次;
        1. POST myindex/_update/1?retry_on_conflict=0
        2. {
        3. "doc": {
        4. "test_field": "test==2"
        5. }
        6. }
        7. #也可以指定_version,如果于我们要求的version不一致,就会返回错误,再根据需求自己决定下一步操作
        8. POST myindex/_update/1?version=2&retry_on_conflict=0
        9. {
        10. "doc": {
        11. "test_field": "test==2"
        12. }
        13. }

4. ES基于_version进行乐观锁并发控制

读写文档的并发操作 - 图6旧版本的 elasticsearch 是用 version 来解决并发问题,采用乐观锁的方式,比较更新时的 version 是否相同来决定能不能更新。但是在新版本 elasticsearch 7.x 再使用 version 就会报上述问题;

:::warning 新版本用的是 _seq_no 和 _primary_term 两个字段来代替 version 处理并发问题,在查询文档时,这两个字段会返回。 :::

GET myindex/_doc/1
#返回结果
{
  "_index" : "myindex",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 4,
  "_seq_no" : 3,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "test_field" : "test==2"
  }
}
#新版本使用 if_seq_no、if_primary_term 参数来代替version做版本控制
PUT myindex/_doc/1?if_seq_no=3&if_primary_term=1
{
  "test_field": "test test333"
}

5. ES基于external version进行乐观锁并发控制

  • external version:
    • es 提供了一个 feature,就是说,你可以不用它提供的内部 _version 版本号来进行并发控制,可以基于你自己维护的一个版本号来进行并发控制。
    • 举个列子,假如你的数据在 mysql 里也有一份,然后你的应用系统本身就维护了一个版本号,无论是什么,如自己生成的、或程序控制的。这个时候,你进行乐观锁并发控制的时候,可能并不是想要用 es 内部的 _version 来进行控制,而是用你自己维护的那个 version 来进行控制。

基于 _version:?version=1
基于 external version:?version=1&version_type=external

  • 两者的区别在
    • _version:只有当你提供的 version 与 es 中的 _version 一模一样的时候,才可以进行修改,只要不一样,就报错;
    • 当 version_type=external 的时候,只有当你提供的 version 比 es 中的 _version 大的时候,才能完成修改;

es,_version=1,?version=1,才能更新成功;
es,_version=1,?version>1&version_type=external,才能成功,比如 version=2;

:::tips 经过测试,external version 版本控制方案在新老版本中都可以使用。 :::