
自助查询平台的稳定性治理主要解决的是用户重复点查询、后台重复跑 SQL 的问题。当时设计了一个基于「幂等 Key 查询指纹」的方案前端用 Idempotency-Key 表达一次查询意图服务端通过唯一索引保证一个任务只会被提交一次同时服务端算查询指纹做结果复用用来兜住没有 Key 的请求。上线后重复查询拦截了三成左右大查询的并发和资源浪费明显下降用户那边最直观的反馈是点多少次只生成一个导出文件。一、场景描述做自助查询平台最容易被低估的问题其实不是 SQL 写得好不好而是“用户点了一下系统算了几遍”。在 OLAP 场景里一次查询就是一次昂贵的资源消费。一条 SQL 在 ClickHouse 或 Spark 上扫几亿行跑上几十秒是很日常的事。但用户感知不到这些——他们只看到页面卡了于是又点一下再点一下。结果就是后台并发起了三四个一模一样的任务计算资源被白白吃掉导出目录里躺着三份一样的文件查询历史里堆满重复记录。我们后来给整个自助查询体系做幂等出发点很简单不能让用户的一个动作变成系统的 N 次计算。很多人一听“幂等”第一反应是 HTTP 语义、POST 去重、Redis 的 SETNX。但对自助查询来说这些都不够。HTTP 幂等解决的是接口调用而我们真正要解决的是任务只执行一次结果可以复用用户不会为一次点击付出多次代价。所以整个方案围绕一句话展开用“查询唯一标识”做执行权仲裁用“查询指纹”做结果复用前者是强约束后者是优化。二、整体方案1、Idempotency-Key 与 数据库唯一键第一步我们先把“谁说了算”这件事定下来。最早我们试过用 requestId 或 traceId 去防重很快发现行不通——重试时 requestId 一定会变它描述的是一次网络传输不是用户的一次查询意图。于是我们反过来要求前端你来告诉系统这是不是“同一次操作”。前端在发起查询时生成一个Idempotency-Key规则很直白业务场景 用户 ID 查询参数的 Hash。比如一次导出参数没变Key 就不变用户改了筛选条件Hash 变了Key 也变。这个 Key 从浏览器带到服务端代表“用户想干这件事”。服务端拿到 Key 后第一件事不是拼 SQL而是去幂等表里插一条记录。表结构很小核心是idempotency_key唯一索引再加一个状态机PREPARE → RUNNING → SUCCESS/FAILED。插入成功说明你是第一个来干这件事的状态推进到 RUNNING再去真正提交查询任务插入失败说明这件事已经有人接了系统不会再起新任务而是根据状态把已有任务 ID 或结果地址返回。这里本质上已经不是“接口防抖”而是把查询接口变成了一个任务系统。无论用户点多少次真正能拿到 RUNNING 权限的只有一个事务。2、 查询指纹但问题没有结束。现实里总有不传 Key 的情况老页面、第三方系统、刷新页面后前端状态丢了。如果只靠 Key这些请求就会绕过幂等。于是我们补了一层服务端的能力——查询指纹。一、指纹计算指纹是对“查询语义”的摘要。我们把前端传来的 DSL 或者后端拼出来的 SQL先做规范化去掉空格换行把过滤条件按字段排序、IN 里的数组也排序时间范围、维度、指标、数据源这些信息拼在一起算一个 sha256。这个 hash就是对“这次查的是什么”的抽象。二、捞指纹候选指纹在数据库里只是个普通索引不是唯一约束。当一个请求没有 Idempotency-Key 时系统会算指纹找最近一段时间内、同用户、同数据源、指纹相同的成功任务然后在内存里把原始查询参数逐条比对只有完全对上才复用结果。所以指纹只是帮我们少扫表不做决策。只有全部对上才复用结果。指纹只是把范围缩小到“可能一样”最后拍板的是结构化参数的精确比对。3、指纹碰撞SHA-256 理论上当然会碰撞但工程上我们不假装没有而是让碰撞无害。指纹永远和user_id、数据源绑在一起不同业务场景策略也不同导出类接口完全不走指纹复用只认 Idempotency-Key即席查询和看板允许复用但结果只保留几分钟实时场景我们甚至直接关掉复用。就算真的发生碰撞最坏的结果也不是“查错数”因为参数指纹的对应的参数对不上而是“没复用上多算一次”。因为在 OLAP 系统里我们更怕的是把 A 条件的结果给到 B而不是多扫一次表。4、任务结果复用异步查询让这件事变得更自然。自助查询基本都是提交任务、轮询状态、最后拿结果。幂等 Key 绑定的是 task而不是某一次 HTTP 请求。用户第一次提交拿到一个 taskId再点还是这个 taskId轮询接口永远只问这一个任务。哪怕任务跑完了第二天同一个 Key 再来也能拿到结果。用户感知到的就是一个稳定的查询进度而不是“怎么又起了一个新任务”。三、小结上线之后最直观的变化其实不在监控图上而在用户侧。以前导出任务经常有人问“为什么我邮箱里三份文件”后来基本没有了查询历史里那种一眼看过去十行一样的记录也消失了。后台看到的是重复请求拦截率稳定在 30% 左右大查询的并发度明显收敛Spark 和 ClickHouse 的“无用功”少了很多。回过头看这套东西并没有什么黑科技。它只是把一件很朴素的事做完整了让客户端声明“我想干什么”让服务端保证“这件事只干一次”让系统有能力认出“这件事是不是已经有人干过了”。我们常说缓存能解决慢但解决不了乱限流能解决炸但解决不了浪费。在自助查询这种“读很贵”的系统里幂等不是锦上添花而是资源管理的底线。如果你也在做 BI、数据提取、或者任何面向用户跑 SQL 的系统我的建议很简单先别急着优化执行计划先保证同一条 SQL不会在同一个用户的一次操作里被跑两遍。剩下的都是在这个前提下做加法。