前言
最近在做大数据回溯相关工作,这里把其中关于 CID 缓存以及 SQL 体积调优的一些经验做一次整理。
这类场景里,随着数据量和回溯范围变大,CID 存储方式、批量读取方式、SQL 体积控制以及查询执行稳定性,都会逐渐成为影响整体性能的关键因素。实际处理过程中,也会涉及缓存成本、查询开销、批次控制和任务稳定性之间的取舍。
这篇主要从实践角度出发,简单总结一下在大数据回溯场景下,CID 缓存和 SQL 体积调优时比较值得关注的几个方向。
背景
这类回溯任务通常有几个共同特征:
- 用户先圈出一批 CID
- 指定一个时间范围
- 再去多张业务表里做历史数据回溯
- 最终把结果写入一张临时结果表
这里的 CID,指的是我们公司内部的 Customer ID,本质上就是客户唯一标识。回溯任务里,CID 集合就是整条链路的输入起点。
如果 CID 数量不大,日期范围也不长,事情其实不复杂。
麻烦的是一旦 CID 数量上来,或者回溯区间拉长,整个链路就会同时出现两个放大效应:
- CID 作为输入集合,本身会变成缓存和传输负担
- SQL 在拼接 CID 集合、日期范围和多表关联之后,体积和执行代价会快速上升
所以这块调优,不能只盯着“查得慢”,而是要分层看问题到底出在缓存、SQL 解析,还是执行展开。
回头看这类问题,最容易混在一起讨论的,其实就是两层事情:
- 一层是 CID 怎么缓存,怎么让输入集合存得稳、读得顺
- 一层是 SQL 怎么控制体积,怎么让单批执行成本更可控
这篇后面的内容也主要围绕这两个方向展开。
一、CID 缓存调优
CID 缓存这件事,重点不是能不能存下。
在大数据回溯场景里,CID 通常是整个任务的输入集合。任务一旦开始执行,后续多个批次都会依赖这批 CID。如果每次执行都重新圈选或重新计算,不仅浪费资源,也会让执行链路变得更重。
所以把 CID 做缓存,本质上是在解决两个问题:
- 避免重复计算
- 支撑后续批次化读取
但这里不能只停留在“Redis 内存够不够”这个层面。真正需要关注的是下面这些问题:
- 单个缓存集合会不会过大
- 批量写入时会不会造成明显抖动
- 后续批量读取是不是稳定
- 过期回收或删除时成本会不会集中放大
- 高峰期多个任务同时存在时,缓存层会不会成为新的瓶颈
换句话说,CID 缓存的核心不是“先放进去再说”,而是“放进去以后,后续整个任务执行还能不能保持稳定”。
1. 先关注单次存储成本
CID 数量一大,缓存集合本身就会变重。
这个时候真正要观察的,不只是条数,而是单个集合的体积和对应的内存占用。因为缓存里的每条 CID,除了字符串本身,还会带来额外的存储组织成本。数据一旦到了一定规模,单次写入、单次读取、过期删除、迁移同步这些动作的成本都会跟着放大。
所以这里更合理的思路不是直接盯着“能放多少条”,而是先做一轮简单测算,先把单个 CID 在缓存里的真实成本摸出来。
我更推荐直接做一组样本测试:
- 随机取
1000、5000、10000、50000个 CID,分别写入几组测试 key。 - 用
MEMORY USAGE key看每组 key 的实际占用。 - 计算平均每个 CID 的真实内存成本。
- 再根据单 key 可接受的体积,反推单个缓存集合适合承载多少 CID。
一个简单的估算公式可以先这么看:
平均每个 CID 的真实内存成本 ≈ key 实际占用 / CID 数量
单 key 可接受 CID 数 ≈ 单 key 可接受内存上限 / 平均每个 CID 的真实内存成本
举个例子,假设测下来结果大概是这样:
1000 个 CID -> 90 KB
5000 个 CID -> 430 KB
10000 个 CID -> 860 KB
50000 个 CID -> 4.3 MB
那平均下来,每个 CID 的真实内存成本大概就在 85B ~ 90B 左右。
如果自己预期单个缓存 key 最好控制在 8 MB 以内,那么可以反推:
8 * 1024 * 1024 / 90 ≈ 93206
也就是说,单个 key 比较稳妥的承载量大概就在 9 万 左右。这个结果不一定是最终值,但至少能先把量级算出来。
这一步做完以后,再去讨论缓存集合要不要拆分、任务高峰期会占多少内存,心里就有数了,而不是靠感觉拍阈值。
除了单 key 本身,还要顺手估一下高峰期整体占用。比如:
单任务 CID 缓存约 6 MB
高峰期同时存在 20 个任务
那么单这一部分缓存占用大概就是 120 MB
这个值不一定夸张,但如果再叠加别的缓存、热点业务 key、过期未及时回收的数据,就要开始有意识地看整体水位了。
所以这里建议至少把下面这几个量算出来:
- 单个任务通常会带来多大规模的 CID 集合
- 这类集合在缓存中大概会形成多大体积
- 在同一时间窗口内,大概会有多少任务同时存在
只有把这些量级先摸清楚,后面讨论缓存策略才有意义。
2. 把 big key 风险一起考虑进去
CID 放进 Redis 之后,除了占用多少内存,还要考虑 big key 问题。
这个问题麻烦的地方在于,它不一定会马上报错,很多时候是后面慢慢表现出来:
- 单次写入时间变长
- 批量读取抖动变大
- 过期删除或手工清理时容易出现阻塞感
- 主从同步、迁移、持久化时放大成本
所以这里不要只问“存不存得下”,还要问“这个 key 会不会已经开始变重”。
如果通过前面的样本测算,发现单个缓存集合已经明显偏大,那就要开始考虑逻辑分片存储。
逻辑分片的思路其实比较直接:
- 不把整批 CID 全塞进一个 key
- 而是按顺序切成多个片段
- 每个片段单独存储
- 后续消费时再按全局偏移或分片顺序去读取
比如一批 20 万 CID,如果测算下来单个 key 最稳妥只适合承载 5 万,那就可以按下面这种方式做逻辑分片:
cid_task_xxx:0 -> 1 ~ 50000
cid_task_xxx:1 -> 50001 ~ 100000
cid_task_xxx:2 -> 100001 ~ 150000
cid_task_xxx:3 -> 150001 ~ 200000
这样做的好处是比较直接的:
- 避免单个 key 过大
- 写入压力被拆散
- 删除和过期回收更容易控制
- 后续如果某个片段读取异常,也更容易定位
当然,逻辑分片不是白来的。它的前提是消费链路也要跟着支持分片感知。否则只是把大 key 拆开了,但后面批量读取还是按单 key 思维去做,收益就会打折。
所以这里更合适的理解是:
- 当单 key 已经开始接近 big key 风险区间时,引入逻辑分片
- 分片不是目的,稳定的存取链路才是目的
3. 再看批量读取方式
CID 缓存的真正价值,不在于“存进去”,而在于“后面怎么被消费”。
大数据回溯任务一般不会一次性把所有 CID 全部丢给查询引擎,而是会按批次读取、分批执行。这样做的好处很直接:
- 可以控制单批查询成本
- 可以降低单次失败代价
- 可以让任务终止、失败重试、执行监控都更容易做
所以缓存层的设计,最好天然服务于“顺序分批读取”这件事。比起一次性全量取出再切片,更稳的方式通常是从一开始就围绕批量消费来设计访问路径。
这个点很关键。因为很多时候问题不是缓存本身存不下,而是读取方式不够适合后续的批次执行模型。
这里可以顺手做一个很简单的批量读取压测:
- 固定一组 CID 集合,比如
5 万。 - 分别按
500、1000、2000、5000这样的批次去读取。 - 观察每次读取耗时,以及整轮消费完成的总耗时。
如果某个批次下出现下面这些现象,就要警惕:
- 单次读取耗时明显跳升
- 批次放大后整体耗时并没有明显下降
- 大批次读取容易和后续 SQL 执行高峰叠在一起
这种时候就说明,批次做大带来的未必是收益,可能只是把压力从“读取次数多”换成了“单次读取更重”。
4. 最后补齐生命周期管理
CID 缓存本质上是任务级临时数据,不是长期数据资产。
既然是临时数据,那就要特别注意生命周期管理。如果缓存放得太久,会增加内存压力;如果回收不及时,高峰期多任务叠加时很容易把缓存层拖重。
所以 CID 缓存更适合按任务生命周期去管理:
- 任务启动时生成
- 任务执行过程中反复消费
- 任务完成或超时后及时回收
这里的经验是,缓存不是放进去就完了,后续清理和回收同样属于调优的一部分。否则很多性能问题并不是在任务执行时爆出来,而是在一段时间后的缓存堆积里慢慢出现。
5. CID 缓存调优里容易忽略的点
做这类缓存调优时,有几个地方比较容易被忽略:
- 只看总量,不看单次写入和单次读取成本
- 只想着先缓存起来,没有把后续批量消费方式一起考虑
- 只关注任务执行阶段,没有把过期回收和任务结束后的清理算进去
- 只看到单任务的资源占用,没有估算高峰期多个任务同时存在时的叠加压力
所以 CID 缓存调优不是简单做一个“能放进去的方案”,而是要把存储、读取和回收当成一整套链路来看。
二、SQL 体积调优
CID 真正进入查询执行层以后,问题就从“怎么存”变成了“怎么查”。
在回溯场景里,SQL 体积变大的原因通常不止一个,而是几个因素叠加在一起:
- 一批 CID 需要直接进入查询语句
- 查询往往覆盖一个日期范围,而不是单天
- 回溯结果通常不是一张表,而是多张业务表关联
- 最终结果还要落到临时结果集或中间表中
这些因素叠加之后,SQL 很容易从一条普通查询,变成一条输入集合很大、字段很多、日期展开明显、关联关系也不轻的重型 SQL。
这时候慢的地方,不一定是底表扫描本身,也可能出现在更前面的环节:
- SQL 文本本身变长
- 解析和计划生成成本增加
- 批次输入过大,导致单次执行时间过长
- 日期范围展开后,中间结果规模迅速放大
- 多表关联让整条语句变得更宽、更重
所以回溯 SQL 调优,不能只盯着“底层引擎查得快不快”,而是要把“输入集合大小”和“展开后规模”一起看。
1. 先做 SQL 体积和工作量估算
先估 SQL 文本长度:
SQL 文本长度 ≈ batchSize × (平均 CID 长度 + 4~8 个 SQL 包装字符) + 固定 SQL 模板长度
如果平均一个 CID 长度是 32,那么可以粗略估一下:
batchSize = 1000
VALUES 段大小大约几十 KB,通常比较稳
batchSize = 10000
VALUES 段大小可能直接上几百 KB
再叠加字段列表、多表 JOIN、日期展开子句之后,整条 SQL 会明显变重
再估展开后的潜在工作量:
潜在展开行数上限 ≈ batchSize × dayCount
比如:
1000 CID × 1 天 -> 上限约 1000 行
1000 CID × 15 天 -> 上限约 1.5 万行
1000 CID × 30 天 -> 上限约 3 万行
5000 CID × 15 天 -> 上限约 7.5 万行
这时候就会很直观地看到,真正变重的并不只是 CID 数量,而是 CID 数量和回溯范围一起把单批工作量放大了。
2. 结合压测找稳定区间
前面的估算只是为了先把量级算出来,真正落地还是要压测。
比较实用的一种压测方式是,固定几组典型场景去跑:
- 短区间场景,比如
1 天 - 中等区间场景,比如
7 天 - 长区间场景,比如
15 天或30 天
然后在每个场景下,分别测试几组批次规模,比如:
500100020005000
每组重点看这几个指标:
- 单批 SQL 生成后的文本长度
- 单批执行耗时
- 失败率和重试率
- 多批次连续执行时的稳定性
- 多任务并发时是否开始明显抖动
最后要找的不是“理论最大批次”,而是一个比较稳的区间。
比如有可能压测下来会发现:
500太保守,批次数明显过多1000到2000这段整体比较平衡5000在短区间下还能接受,但一旦叠上长日期范围就明显变重
那实际落地时,就更适合优先选中间这段稳定区间,而不是直接冲最大值。
3. SQL 体积调优时值得重点关注的方向
如果后面再做类似的大数据回溯任务,我会优先从下面几个方向去看。
第一,先看数据量级,不要一上来就只谈查询慢不慢。
在没有量级概念之前,直接讨论怎么调优其实意义不大。
要先知道:
- 单任务通常会有多少 CID
- 高峰期会同时跑多少任务
- 回溯范围通常覆盖多少天
- 一次查询大概会关联多少张表
这几个量级不清楚,后面很多判断都只能靠猜。
第二,不要只看 SQL 文本长度,也要看展开后的真实工作量。
很多 SQL 从文本上看并不算离谱,但一旦叠上日期范围和多表关联,单批工作量就会快速变重。
第三,先看单批执行稳定性,再看总吞吐。
对于批任务系统来说,稳定性通常比极限吞吐更重要。
单批可控、失败可恢复、执行时长比较稳定,这些收益往往比单纯少跑几批更有价值。
第四,不要只盯慢查询,要看整条链路。
回溯任务不是单纯的一条 SQL。
它通常是一整条链路:
- 输入集合准备
- 缓存写入
- 批量读取
- SQL 拼装
- 查询执行
- 结果写入
- 失败重试
- 任务收口
哪一段变重,最终都会体现在“任务慢了”这个现象上。只盯着查询引擎本身,很容易漏掉真正的问题。
总结
这次回头整理这部分经验,最大的感受是,CID 缓存和 SQL 体积调优看起来是两个问题,实际上刚好对应了回溯任务里最核心的两层能力:
- 输入集合怎么稳定管理
- 单批查询怎么可控执行
CID 缓存这一层,更关注的是存储成本、读取方式和生命周期管理。SQL 体积这一层,更关注的是单批工作量、日期展开规模以及整体执行稳定性。
如果用一句话收一下,我会把这部分经验记成这样:
在大数据回溯场景下,CID 缓存的关键是让输入集合存得稳、读得顺;SQL 调优的关键是让单批执行成本可控、可恢复、可预测。