真实让两个update_by_query并发抢同一批2万条文档,用两边严丝合缝互补的updated/version_conflicts数字,证明它底层是逐条乐观并发写回,不是一次性的原子批量更新。
代码# 先验证基础情形:5条文档,单独一个update_by_query POST /ubq_test/_update_by_query {"script":{"source":"ctx._source.status=\"done\""},"query":{"term":{"status":"pending"}}} # 真实的并发实验:20000条文档,同时发起两个update_by_query POST /ubq_race3/_update_by_query?conflicts=proceed {"script":{"source":"ctx._source.status=\"done_A\""}} POST /ubq_race3/_update_by_query?conflicts=proceed {"script":{"source":"ctx._source.status=\"done_B\""}} # 真实同时发起,不等前一个完成
{"total": 5, "updated": 5, "deleted": 0, "batches": 1, "version_conflicts": 0, "noops": 0}这是update_by_query最基础、没有任何并发干扰时的真实结果:total是查询条件匹配到的文档总数,updated是真实执行了脚本并写回的数量,batches是内部真实按滚动(scroll)分批处理的批次数——这组基础数据为后面的并发实验提供了真实的对照基准:没有竞争时,version_conflicts应该永远是0。
结果A: {"total": 20000, "updated": 9539, "version_conflicts": 10461, "batches": 20}
结果B: {"total": 20000, "updated": 10461, "version_conflicts": 9539, "batches": 20}
真实验证: A.updated(9539) + A.version_conflicts(10461) = 20000
A.updated(9539) == B.version_conflicts(9539)
A.version_conflicts(10461) == B.updated(10461)这组实验真实给出了一个容易被忽视的工程提醒:update_by_query看起来是"一条命令改一批数据",但它底层真实是对每条文档独立做乐观并发控制的组合操作,不是一次性的原子批量更新。如果真实的生产环境里,一个update_by_query任务跟别的写入流程(不管是另一个update_by_query、还是普通的_update、_bulk)真实同时作用在同一批文档上,最终谁的修改生效完全取决于真实的执行时序,而不是代码里谁先被调用——本文这次用conflicts=proceed配合两个真实并发的请求,把这种原本难以观察的竞态条件,用一组干净互补的数字真实地暴露了出来。
每一步都是本文上面在本机真实的Elasticsearch 8.15.0上跑出来的原始结果。
_update_by_query API 与 conflicts=proceed