1 什么是partial update

1.1 全量修改文档的原理

全量修改文档的语法: PUT index/type/1, 如果id=1的文档不存在, 则创建, 如果存在, 将发生替换原有文档的操作.

全量替换文档的性能比较低, 为了避免替换操作的发生, 引入partial update: 只修改指定的field, 不用全量修改数据.

1.2 修改指定field的思路

① 根据用户请求, 获得要修改的文档;

② 在内存中封装用户提交的新文档, 发送PUT请求到ES内部;

③ 将要替换的旧文档标记为deleted;

④ 最后将封装好的新文档存入索引中.

1.3 partial update的优势

① 所有的查询、修改和写回操作, 都在同一个shard中进行, 避免了网络传输的开销.

  • 不需要: 从特定shard查询文档 -> 返回到内存 -> 内存中修改 -> 将修改的文档发送到原来的shard -> 写索引 —— 这个复杂的操作, 显著提升了性能:

② 减少了查询和修改的时间间隔, 可以有效减少并发冲突.

1.4 partial update的使用

使用方法: 通过_update关键字实现partial update:

// 添加测试数据: 
PUT employee/developer/1
{
    "name": "shou feng", 
    "sex": "male",
    "age": 20
}


// partial update修改指定field: 
POST employee/developer/1/_update
{
    "doc": {
        "age": 21
    }
}

// 响应结果: 
{
    "_index": "employee",
    "_type": "developer",
    "_id": "1",
    "_version": 5,
    "result": "updated",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    }
}

// 查看文档, 发现age已经从20变为21了. 
GET employee/developer/1

如果不使用_update, 则会直接覆盖掉源文档, 导致原文档丢失部分数据:

// 不使用_update:  
POST employee/developer/1
{
    "doc": {
        "age": 22
    }
}

// 再次查看, 发现id=1的该文档就只剩一个age字段了: 
GET employee/developer/1

2 通过脚本进行partial update操作

ES提供了脚本支持 —— 可以通过Groovy外置脚本(已过时)、内置painless脚本实现各种复杂操作.

2.1 内置painless脚本修改文档

  • 插入文档:

    PUT employee/developer/1
    {
        "name": "shou feng", 
        "age": 20,
        "salary": 10000
    }
  • 执行脚本: —— 这里使用的是更轻快简短的painless脚本, 就是直接由字符串表示的脚本:

    POST employee/developer/1/_update     // 发送POST请求, 执行partial update
    {
        "script": "ctx._source.salary+=500" // 为salary自增500
    }
  • 查看修改结果:

    GET employee/developer/1
    
    // 结果如下: 
    {
        "_index": "employee",
        "_type": "developer",
        "_id": "1",
        "_version": 11,
        "found": true,
        "_source": {
            "name": "shou feng",
            "age": 20,
            "salary": 10500           // 自增500成功
        }
    }

2.2 外置Groovy脚本修改文档

  • 将脚本文件存放在${ES_HOME}/config/scripts下, 文件名为xxx.groovy, 内容为:

    ctx._source.salary+=bonus —— 增加值为将近bonus的值, 示例如下:
    脚本内容

  • 修改文档:

    POST employee/developer/1/_update
    {
        "script": {
            "lang": "groovy", 
            "file": "change_salary",
            "params": {
                "bonus": 500
            }
        }
    }
    
    // 响应结果为: 
    #! Deprecation: [groovy] scripts are deprecated, use [painless] scripts instead
    {
        "_index": "employee",
        "_type": "developer",
        "_id": "1",
        "_version": 12,
        "result": "updated",
        "_shards": {
            "total": 2,
            "successful": 1,
            "failed": 0
        }
    }
  • 查看修改结果:

    GET employee/developer/1
    // 结果如下: 
    {
        "_index": "employee",
        "_type": "developer",
        "_id": "1",
        "_version": 12,
        "found": true,
        "_source": {
            "name": "shou feng",
            "age": 20,
            "salary": 9000
        }
    }

说明: 在执行外置Groovy脚本时, ES提示Groovy脚本已经过时, 建议我们使用painless —— 更轻快的表达方式, 即类似于ctx._source.salary+=bonus的简短表达方式.

Elasticsearch 5.6(具体开始版本不明确)版本中的默认脚本使用方式就已经是painless了.

关于脚本的详细使用, 请参见博文: ES 27 – Elasticsearch的painless脚本使用实践.

2.3 内置painless脚本upsert文档

  • 假设不知道id=1的文档被删除了, 我们现在为其添加 "level": 1 的内容:

    POST employee/developer/1/_update
    {
        "doc": {
            "level": 1
        }
    }
  • 抛出 [404 – 文档丢失] 的错误:

    {
        "error": {
            "root_cause": [
                {
                    "type": "document_missing_exception",
                    "reason": "[developer][1]: document missing",
                    "index_uuid": "rT6tChP2QISaVd2OzdCEMA",
                    "shard": "3",
                    "index": "employee"
                }
            ],
            "type": "document_missing_exception",
            "reason": "[developer][1]: document missing",
            "index_uuid": "rT6tChP2QISaVd2OzdCEMA",
            "shard": "3",
            "index": "employee"
        },
        "status": 404
    }
  • 修改upsert策略: 如果指定的文档不存在, 就执行upsert中的初始化操作; 如果存在, 就执行docscript中的partial update操作:

    POST employee/developer/1/_update
    {
        "script": "ctx.source.level+=1",
        "upsert": {
            "name": "heal",
            "age": 20
        }
    }

2.4 外置Groovy脚本delete文档

脚本路径: ${ES_HOME}/config/scripts/delete_doc.groovy

脚本内容: ctx.op = ctx._source.age == age ? 'delete': 'none' ctx.op = ctx._source.age == param ? 'delete' : 'none'

  • 使用示例:

    POST employee/developer/1/_update
    {
        "script": {
            "lang": "groovy", 
            "file": "delete_doc",
            "params": {
                "age": 20 // 如果年龄是20, 则删除之
            }
        }
    }
  • 响应结果:

    #! Deprecation: [groovy] scripts are deprecated, use [painless] scripts instead
    {
        "_index": "employee",
        "_type": "developer",
        "_id": "1",
        "_version": 13,
        "result": "deleted",
        "_shards": {
            "total": 2,
            "successful": 1,
            "failed": 0
        }
    }
  • 查看文档是否被删除:

    GET employee/developer/1
    // 响应结果 - 成功删除: 
    {
        "_index": "employee",
        "_type": "developer",
        "_id": "1",
        "found": false
    }

3 partial update的并发控制策略

partial update内部也是通过乐观锁进行并发控制的.

关于并发控制, 请参见博文: Elasticsearch的并发控制策略.

3.1 控制方式

POST index/type/id/_update?retry_on_conflict=5
POST index/type/id/_update?retry_on_conflict=5&version=5

3.2 retry原理

retry_on_conflict: 发生冲突后的重试次数.

(1) 客户端A、B几乎同时获取同一个文档, 一并获得_version版本信息, 假设此时_version=1;

(2) 客户端A修改文档中的部分内容, 将修改写入索引;

(3) Elasticsearch在写入索引时, 检查客户端A提交的文档的版本信息(这里仍然是1) 和 现存的文档的版本信息(这里也是1), 发现相同后, 执行写入操作, 并修改版本号_version=2;

(4) 客户端B也修改文档中的部分内容, 其操作写回索引的速度稍慢. 此时同样执行过程(3): ES发现客户端B提交的文档的版本为1, 而现存文档的版本为2 ===> 发生冲突, 此次partial update将失败;

(5) partial update操作失败后, 将重复(1) – (3) 过程, 重复的次数, 就是retry_on_conflict参数的值.

版权声明

作者: 马瘦风(https://healchow.com)

出处: 博客园 马瘦风的博客(https://www.cnblogs.com/shoufeng)

感谢阅读, 如果文章有帮助或启发到你, 点个[

版权声明:本文为shoufeng原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/shoufeng/p/11348277.html