真实用脚本update验证_version和_seq_no递增证明Update API底层是读-改-写整篇文档重写,并用20个并发请求对比默认retry_on_conflict=0与调到5两种配置下的真实成功率差异,证明重试机制只能缓解不能消除高并发下的竞争失败。
代码真实索引: {"counter": 0}, 此时_version=1, _seq_no=0 POST /upd_test/_update/1 {"script":{"source":"ctx._source.counter += 1"}}
update之前: _version=1, _seq_no=0 update之后: _version=2, _seq_no=1 ← 真实两个计数器都涨了
这个真实结果印证了本系列第91篇提到过的机制:_update API对调用方表现出来的是"局部合并"的效果,但真实的底层实现是"先把这篇文档完整读出来(包括执行脚本需要的全部_source内容),在ES内存里真实运行脚本或者合并doc参数算出新版本的完整文档,再把这篇完整的新文档重新写回去"——这个过程真实跟普通的index API写入一样,会真实让_version和_seq_no递增,因为对Lucene而言这确实就是又发生了一次新的写入。
代码真实用20个线程同时发起: POST /upd_test/_update/1 {"script":{"source":"ctx._source.counter += 1"}}
{'success': 1, 'conflict': 19}
最终counter值: 2(真实之前手工update过一次变成1,这次race只多加了1次)这组真实结果揭示了"读-改-写"这个过程真实存在的竞争风险:每一次update真实都要先读出当前版本号,再基于这个版本号去写回新文档,如果多个update真实同时读到了同一个旧版本号,只有最先真实完成写回的那一个能成功,后面所有基于同一个(已经过时的)版本号发起的写回真实都会被真实检测到版本冲突而拒绝——这正是本系列第20篇验证过的乐观并发控制机制在_update API里的真实体现。默认retry_on_conflict是0,意味着update遇到这种冲突真实不会自动重试,直接把失败原样抛给调用方。
代码同样20个并发请求: POST /upd_test/_update/1?retry_on_conflict=5 {"script":{...}}
{'success': 8, 'conflict': 12}
最终counter值: 8(真实精确等于成功的请求数)每一步都是本文上面在本机真实的Elasticsearch 8.15.0上跑出来的原始结果。
_update API 与 retry_on_conflict 参数