目录
1 什么是 partial update
1.1 全量修改文档的原理
1.2 修改指定 field 的思路
1.3 partial update 的优势
1.4 partial update 的使用
2 通过脚本进行 partial update 操作
2.1 内置 painless 脚本修改文档
2.2 外置 Groovy 脚本修改文档
2.3 内置 painless 脚本 upsert 文档
2.4 外置 Groovy 脚本 delete 文档
3 partial update 的并发控制策略
3.1 控制方式
3.2 retry 原理
1 什么是 partial update
1.1 全量修改文档的原理
全量修改文档的语法: PUT index/type/1, 如果 id=1 的文档不存在, 则创建, 如果存在, 将发生替换原有文档的操作.
全量替换文档的性能比较低, 为了避免替换操作的发生, 引入 partial update: 只修改指定的 field, 不用全量修改数据.
1.2 修改指定 field 的思路
1 根据用户请求, 获得要修改的文档;
2 在内存中封装用户提交的新文档, 发送 PUT 请求到 ES 内部;
3 将要替换的旧文档标记为 deleted;
4 最后将封装好的新文档存入索引中.
1.3 partial update 的优势
1 所有的查询, 修改和写回操作, 都在同一个 shard 中进行, 避免了网络传输的开销.
不需要: 从特定 shard 查询文档 -> 返回到内存 -> 内存中修改 -> 将修改的文档发送到原来的 shard -> 写索引 -- 这个复杂的操作, 显著提升了性能:
2 减少了查询和修改的时间间隔, 可以有效减少并发冲突.
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 中的初始化操作; 如果存在, 就执行 doc 或 script 中的 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 参数的值.