bigywater.tech
Elasticsearch · 106

版本冲突与重试

真实用20个线程手工实现读-算-写的客户端乐观并发逻辑,对比不做重试(1/20成功)与加入重试循环最多10次(17/20成功但仍有3个放弃)的真实效果差异,印证版本冲突重试是有效缓解措施而非能消除全部竞争失败的银弹。

朴素CAS无重试(20线程)
真实只有1个成功,19个直接放弃
加上重试循环(最多10次)
真实成功数涨到17个
依然有3个放弃
证明重试能缓解但不能消除高并发竞争失败

做的事:真实用20个线程对同一个counter做最朴素的"读-算-写"客户端逻辑,完全不处理冲突失败

代码真实索引{"counter":0} 20个线程真实各自并发执行: 1. GET /vc_test/_doc/1 拿到当前counter值、_seq_no、_primary_term 2. PUT /vc_test/_doc/1?if_seq_no=X&if_primary_term=Y {"counter": 当前值+1} 真实遇到409直接放弃,不做任何重试

验证:不做重试的朴素版本,真实20个里只有1个成功,19个真实直接放弃了

实测 · 本机 Elasticsearch 8.15.0 执行输出
naive (no retry): {'success': 1, 'conflict': 19}
最终counter: 1

这组真实结果跟本系列第103篇用_update API观察到的现象(默认retry_on_conflict=0时20个并发只有1个成功)真实完全一致——这不是巧合,而是因为_update API底层真实也是在做同样的"读-改-写"流程,这里真实只是把这个流程用最原始的GET+PUT if_seq_no手工实现了一遍,本质上真实是同一套乐观并发控制机制在起作用:19个线程真实都基于同一个已经过期的seq_no去写入,自然全部真实被拒绝。

真实把客户端逻辑改成遇到冲突就重新读取最新值再试一次,最多尝试10次,看成功率能提升多少

代码真实每个线程遇到409冲突之后,不放弃,而是真实重新GET最新的_seq_no和counter值,用新值再次尝试PUT,最多重复这个过程10次

验证:加了重试循环之后,成功数真实从1个涨到了17个,但依然有3个在尝试10次之后真实放弃了

实测 · 本机执行输出
with retry loop (max 10 attempts): {'success': 17, 'gave_up': 3}
最终counter: 17   ← 真实精确等于成功的请求数
这组真实数据给出了一个诚实、不夸大的结论:客户端自己实现"遇到版本冲突就重新读取最新状态再试一次"这个标准的乐观并发控制重试模式,真实能大幅提升成功率(从5%真实提升到了85%),但它真实不是银弹——在本次实测20个线程同时竞争同一个文档这种高强度并发场景下,依然有3个线程真实在连续尝试了10次、每次都刚好又撞上了新的并发写入之后,真实耗尽重试次数放弃了。这组结果跟第103篇用_update内置retry_on_conflict观察到的效果(提升但不消除冲突失败)真实是同一个结论在不同实现方式下的再次印证:不管是ES内置的重试机制,还是业务代码自己手写的重试循环,面对足够激烈的并发写入竞争,重试次数上限真实总会在某个点被触及。真实要彻底解决这类问题,光靠"重试次数设得更大一点"真实不是治本的办法(极端情况下竞争足够激烈,再多重试次数依然可能耗尽),业务上如果真实存在这种高频并发更新同一份数据的场景,更值得考虑的真实是从设计上分摊这种竞争(比如把一个热点计数器拆分成多个独立字段分摊写入压力,最后查询时再汇总),而不是单纯依赖重试去硬扛。

分步演示:客户端自己实现乐观并发重试循环从朴素失败到真实有限改善的完整验证

每一步都是本文上面在本机真实的Elasticsearch 8.15.0上跑出来的原始结果。

客户端自己实现的版本冲突重试循环能大幅提升成功率但真实无法消除全部竞争失败
这一步新验证的重试策略

    本文用到的工具

    • 本机真实部署的 Elasticsearch 8.15.0(单节点,tar包直接启动)
    • 真实的客户端读-改-写重试循环与 if_seq_no 参数
    京ICP备2026067502号-1京公网安备11010502063487号