1. 为什么Doris查询需要Redis缓存加速
做数据平台的同学应该都有过这种体验:Doris集群明明已经调了不少参数,配置也按官方文档优化过了,但前端一个报表页面点下去,接口响应还是动不动就三五秒。业务那边催得急,说你这是大宽表查询,维度多、粒度粗,每次都要全量聚合几亿行,数据库压力大,查询自然就慢。
这时候多数人的第一反应是继续调Doris——加大内存、建更好的物化视图、优化分区分桶策略。这些当然有用,但会陷入一个循环:每来一个新需求就重调一遍底层存储,代价太高,而且总有几个查询是无论怎么调都压不进一秒以内的。
我当时的处境就是这样。某跨平台系统的核心报表模块需要支撑几十个维度的实时筛选,底层是Doris存储的明细数据,单表行数已经突破了十亿。最麻烦的是那些带高基数维度组合的查询:比如同时按城市、品类、渠道三个维度去聚合近三十天的销售数据,这类查询在Doris里跑一次大概要两秒到四秒,用户连续拖拽筛选时体验非常差。
后来我换了个思路:既然Doris的查询结果在一定时间窗口内是稳定可复用的,为什么不在Doris前面加一层Redis缓存?把热查询的结果直接缓存下来,下次同样的查询条件过来,直接从Redis拿结果,响应时间能压到几十毫秒。这其实就是典型的缓存加速方案,但放到Doris这个分析型数据库场景里,要做好并不像想象中那么简单——缓存粒度怎么定、Key怎么设计、失效策略怎么设、数据一致性怎么保证,每一步都有讲究。
这篇文章我就把Doris与Redis集成做查询加速的方案完整拆一遍,从架构选型到代码实现再到踩坑排查,给出一个经过验证的可落地参考方案。适合正在做数据平台、报表系统,或者负责OLAP查询性能优化的同学参考。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 缓存方案整体设计思路
2.1 先想清楚:为什么是Redis而不是其他缓存
摆在你面前的选择其实有几个:本地内存缓存、Redis、或者干脆在应用层用Caffeine之类的进程内缓存框架。我当时都对比过,最后选了Redis,原因有三点。
第一,查询服务通常是多实例部署的。如果只用本地内存缓存,A实例缓存了某个查询结果,B实例收到同样的查询还得再算一次,命中率直接除以实例数。Redis是集中式的,所有实例共享同一份缓存,整体的命中率要高很多。
第二,Doris的查询结果有个特点——热点集中。报表系统里80%的请求永远落在那几个固定维度组合上,比如“近7天、全国、全渠道”这个查询可能每分钟被触发几十次。这种场景下,集中式缓存的收益非常明显,因为同样的查询条件可以命中同一个缓存Key。
第三,Redis的TTL机制和数据结构足够灵活。查询结果本质上就是一个JSON字符串或者字节数组,用Redis String类型存正好;TTL天然支持不同查询设置不同的过期时间,冷热数据分开管理。
注意:如果你的查询服务只有单实例,并且查询结果数据量不大(单条结果小于几百KB),用Caffeine这类本地缓存反而更快,因为它省掉了一次网络IO。多实例场景下Redis才是合理选择。
2.2 缓存粒度:结果集缓存还是数据块缓存
这是整个方案最关键的设计决策。我在实际项目中试过两种路线,体验完全不同。
第一种是数据块缓存,思路是把Doris底层的Segment文件或中间聚合结果缓存到Redis里。这种方案的好处是复用度高——不同查询如果命中了相同的数据块,可以部分复用。但实现复杂度极高,你要自己维护数据块的版本、切分逻辑、合并逻辑,而且Redis里的数据块跟Doris底层文件的更新还需要做联动,一致性维护成本很大。我评估下来觉得这个方案对于大多数团队来说过于重了,相当于自己实现了一个分布式缓存层。
第二种是结果集缓存,思路简单直接:以SQL的规范化文本(或者查询条件的哈希值)作为Key,以查询结果序列化后的JSON作为Value,存到Redis里。同一个SQL再来查询时直接返回。这种方式实现成本低、收益直接,适合读多写少、查询条件稳定的场景——而报表系统恰恰就是这种场景。
我最终选的是结果集缓存。理由很实在:Doris的瓶颈在于重复计算,而不是底层存储的IO。同一个高代价聚合查询,如果短期内被反复执行,每次都全量算一遍,是完全的浪费。结果集缓存就是把这部分浪费去掉,让同样的查询只算一次,后续直接读缓存。
当然结果集缓存也有它的局限:如果查询条件变化极其频繁(比如每个用户都带一个随机的session_id),缓存命中率会很低;如果底层数据实时变化,缓存数据会很快过期不准确。这些问题通过合理的TTL设计基本能控制住,后面我会细讲。
2.3 命中率预估:哪些查询值得缓存
不是所有查询都值得进Redis。我在设计阶段就定了一个筛选标准,避免缓存被无意义的查询塞满。
值得缓存的是这三类:
- 高频固定的报表查询,也就是BI看板底层的那些预置SQL
- 聚合层级的查询,比如按天、按周汇总的数据,因为这类查询计算量大但结果集很小
- 查询条件组合有限、但单个查询执行时间超过1秒的慢查询
不值得缓存的是这两类:
- 带随机参数(如用户ID、随机排序)的个性化查询——缓存Key无法复用
- 实时性要求极高、数据秒级变化的查询——命中缓存反而会给出过期数据
我当时的做法是给查询服务加了一个“缓存开关”和“TTL白名单”机制:默认情况下,只有命中白名单的SQL模板才走缓存;其他查询一律直通Doris。这样既保住了热点查询的加速效果,又不会让Redis变成垃圾桶。
3. 核心细节解析与实操要点
3.1 缓存Key的设计:比你想的复杂
缓存Key是整个方案的地基,Key设计得不好,后续全是坑。我见过不少同学直接把整个SQL字符串当作Key来用,这样有几个问题。
第一个问题是空格和大小写。SELECT * FROM table和select * from table在Doris看来是同一个查询,但SQL字符串完全不同,缓存会变成两个Key,命中率直接打折扣。
第二个问题是查询参数的随机性。比如SQL里带了当前时间NOW()或者随机值RAND(),每次执行都不一样,这种SQL永远无法命中缓存。
第三个问题是Key过长导致的内存浪费。一个复杂的聚合SQL动辄几百上千字符,如果直接作为Redis Key存储,Redis内部的内存开销会相当可观——Key占用的空间比Value还大,非常不划算。
我当时的设计方案是这样的:
先把SQL做一次规范化处理,把多个空格压缩为单个空格,去掉注释,统一关键字大小写;然后把SQL里的字面量参数提取出来,生成一个结构化的查询签名;最后用MD5对签名取摘要,作为Redis Key。
举个例子,原始SQL是这样的:
sql复制SELECT city, SUM(amount) FROM orders WHERE dt BETWEEN '2025-01-01' AND '2025-01-31' GROUP BY city
规范化后变成:
sql复制SELECT CITY, SUM(AMOUNT) FROM ORDERS WHERE DT BETWEEN ? AND ? GROUP BY CITY
然后用MD5对这个模板做哈希,Key大概长这样:
text复制doris:cache:v1:report_orders:9f2c3d1a7b4e6f8a0123456789abcdef
Key的前缀部分带了版本号(v1)和业务标识(report_orders),方便后续做批量清理或者版本升级时的Key迁移。
3.2 Value的序列化:JSON还是二进制
缓存Key确定之后,下一个要决策的是Value用什么格式存储。我在这个环节对比了两种方案,各有优劣。
JSON字符串是理解成本最低的方案。直接把Doris查询返回的Java对象用Jackson或者Gson序列化成JSON字符串,存进Redis。取出来的时候再反序列化成对象,整个过程非常直观。优点是调试方便——你直接连上Redis GET一下,就能看到缓存里的数据长什么样;缺点就是Redis里存的是字符串,每次读取都要经过一次JSON反序列化,在高并发场景下这个开销会被放大。
二进制序列化(比如ProtoBuf、Kryo)效率更高,序列化后的体积小、反序列化速度快,但调试不方便,且需要维护序列化协议的定义。如果你用的是Java技术栈,Kryo的体积大概是JSON的四分之一到三分之一,在高并发下性能优势明显。
我的实际选择是JSON,理由非常朴素——团队协作效率优先。整套系统的数据流需要被多方排查,JSON一眼能看懂,出了问题定位成本低。对于真正的高频查询,我会在应用层再做一层Caffeine本地缓存,把JSON反序列化成对象之后缓存在进程内,进一步减少反序列化次数。这是一个很实用的“两级缓存”组合,后面章节会展开讲。
注意:不管用哪种序列化方案,都要特别注意查询结果里的数据类型。Doris返回的Decimal类型在Java里如果用BigDecimal处理,序列化和反序列化是安全的;但如果先转成了Double,精度可能就丢了,缓存的数据和原始查询结果不一致,这种bug非常隐蔽。
3.3 TTL和失效策略:一致性是绕不开的坎
Doris里的数据是会更新的。比如订单明细表,业务上每天凌晨会重新跑一遍前一天的修正数据。如果Redis缓存了昨天的查询结果,数据修正后缓存还是旧的,报表就会出错。
所以TTL的设计必须是多层次的,不能一个策略打天下。
我当时的做法是这样管理的:
- 固定报表、底层数据按小时更新:TTL设置10分钟。报表上的数据最多延迟10分钟,业务方可以接受。
- 低频宽表聚合查询:TTL设置30分钟到1小时。这类查询很少变,但每次计算代价极高,缓存能省很多算力。
- 实时性要求高的查询:不设缓存,或者TTL只设30秒。
- 大促、活动期间的报表:手动调短TTL,或者直接通过Redis侧的管理接口强制清理相关Key。
除了TTL,我还加了一个主动失效机制:在数据导入Doris的流程里埋一个钩子,每次导入完成之后,把涉及到的业务表相关的缓存Key主动删除。具体实现是通过在业务表名和缓存Key前缀之间建立映射关系,导入完成后按前缀去Redis里扫描并删除对应Key。
这里要注意一个性能问题:用SCAN命令按前缀匹配删除在Key数量不多的时候没问题,但如果Redis里积累了几十万个缓存Key,SCAN会阻塞整个请求链路。我的经验是:每天凌晨做一次全量清理兜底,平时的主动失效只针对热点表,不要所有表都做。
3.4 并发控制:防止缓存击穿
缓存方案设计得再完美,也扛不住“热点Key过期瞬间大量请求同时打到Doris”的情况。这就是经典的缓存击穿问题。
我在上线初期就吃过这个亏。某个大宽表的聚合查询缓存时间是10分钟,TTL一到,恰好几十个业务请求同时过来,全部没有命中缓存,一窝蜂打到Doris上,Doris的CPU瞬间飙到90%多,整个集群的查询都变慢了。
解决这个问题有几种常用做法。第一种是互斥锁——在缓存未命中时,先尝试获取一把分布式锁,拿到锁的请求去查Doris并回填缓存,没拿到锁的请求等待一段时间后重试,直接从缓存里读。第二种是逻辑过期——Value里存一个过期时间,线程发现逻辑过期后,先返回旧数据,同时后台异步去刷新缓存。第三种是永远不过期——物理上不设TTL,靠定时任务主动刷新。
我最终采用的是第一种做法的优化版本:分布式锁 + 短超时重试。用Redis的SETNX命令实现一个简单的锁,拿到锁的请求执行查询并回填缓存,拿不到锁的请求休眠200毫秒后重新查缓存。在绝大多数场景下,第二次尝试就能命中缓存了。
这个方案要特别注意锁的超时时间设置。如果查询Doris的时间超过了锁的自动过期时间,其他等待的请求就会认为锁已经释放,从而同时发起查询,导致击穿问题再次出现。我的经验是把锁的过期时间设置为2秒,如果查询Doris的预估时间超过2秒,就适当调大。
4. 实操过程与核心环节实现
4.1 环境准备与依赖引入
整套方案我是在一个标准Spring Boot项目里实现的。先把依赖列一下,方便你对照:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
Redis连接配置在application.yml里:
yaml复制spring:
redis:
host: 127.0.0.1
port: 6379
password: xxx
timeout: 500ms
lettuce:
pool:
max-active: 32
max-idle: 16
min-idle: 8
注意:
timeout不能设置得太长。Redis不可用时,如果等待时间过长,查询接口的RT会被拖垮。我一般设置500毫秒,超过直接降级回源Doris,优先保证查询可用性,而不是让缓存阻塞主链路。
4.2 缓存核心代码实现
接下来是核心代码。我的实现围绕三个类展开:查询签名生成器、缓存读写服务、查询服务入口。
首先是查询签名生成器:
java复制public class QuerySignatureUtil {
private static final Pattern COMMENT_PATTERN = Pattern.compile("/\\*.*?\\*/|--.*?\\n", Pattern.DOTALL);
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("'[^']*'|\\d+(\\.\\d+)?");
/**
* 规范化SQL并生成缓存签名
*/
public static String buildSignature(String sqlTemplate, Map<String, Object> params) {
// 去掉SQL中的注释
String normalized = COMMENT_PATTERN.matcher(sqlTemplate).replaceAll(" ");
// 统一空格
normalized = normalized.replaceAll("\\s+", " ").trim();
// 将字面量替换为占位符
normalized = PLACEHOLDER_PATTERN.matcher(normalized).replaceAll("?");
// 将参数按Key排序后拼接到签名尾部
String paramStr = params.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
// 生成MD5
return "doris:cache:v1:" + DigestUtils.md5DigestAsHex((normalized + "|" + paramStr).getBytes(StandardCharsets.UTF_8));
}
}
这个类的关键是把SQL模板和查询参数分开处理。SQL模板规范化后作为固定签名的一部分,参数排序后拼接进去,这样同一个模板配不同参数生成的Key都不同,而相同参数的查询必然命中同一个Key。
然后是缓存读写服务:
java复制@Service
public class DorisCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 从缓存读取查询结果,未命中返回null
*/
public List<Map<String, Object>> getCachedResult(String cacheKey, Class<?> elementType) {
try {
String json = redisTemplate.opsForValue().get(cacheKey);
if (json == null) {
return null;
}
// 使用TypeReference反序列化,保证泛型类型正确
return MAPPER.readValue(json, new TypeReference<List<Map<String, Object>>>() {});
} catch (Exception e) {
// 解析失败直接返回null,让上层回源查询
return null;
}
}
/**
* 写入缓存,带TTL
*/
public void setCacheResult(String cacheKey, Object result, long ttlSeconds) {
try {
String json = MAPPER.writeValueAsString(result);
redisTemplate.opsForValue().set(cacheKey, json, ttlSeconds, TimeUnit.SECONDS);
} catch (Exception e) {
// 序列化失败不要影响主流程
}
}
/**
* 使用分布式锁防止缓存击穿
*/
public boolean tryLock(String lockKey, long expireSeconds) {
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
public void releaseLock(String lockKey) {
redisTemplate.delete(lockKey);
}
}
这段代码有一个细节值得注意:反序列化和序列化的异常都必须捕获并静默处理。原因很实际——缓存层的运行状态不应该影响主查询链路。Redis挂了、网络抖动、JSON格式异常,都不应该让用户感受到失败,回源Doris最多慢一点,但不会报错。
最后是查询服务入口的编排逻辑:
java复制@Service
public class ReportQueryService {
@Autowired
private DorisQueryExecutor dorisQueryExecutor;
@Autowired
private DorisCacheService cacheService;
public List<Map<String, Object>> queryReport(String sqlTemplate, Map<String, Object> params, long ttlSeconds) {
// 1. 生成缓存Key
String cacheKey = QuerySignatureUtil.buildSignature(sqlTemplate, params);
// 2. 先查缓存
List<Map<String, Object>> cached = cacheService.getCachedResult(cacheKey, Map.class);
if (cached != null) {
return cached;
}
// 3. 未命中,尝试获取分布式锁
String lockKey = cacheKey + ":lock";
boolean locked = cacheService.tryLock(lockKey, 2);
if (!locked) {
// 拿不到锁说明其他线程正在回源查询,等待后重试
try {
Thread.sleep(200);
List<Map<String, Object>> retry = cacheService.getCachedResult(cacheKey, Map.class);
if (retry != null) {
return retry;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
try {
// 4. 真正查询Doris
List<Map<String, Object>> result = dorisQueryExecutor.query(sqlTemplate, params);
// 5. 回填缓存
if (result != null && !result.isEmpty()) {
cacheService.setCacheResult(cacheKey, result, ttlSeconds);
}
return result;
} finally {
// 6. 释放锁
if (locked) {
cacheService.releaseLock(lockKey);
}
}
}
}
这个编排逻辑把缓存穿透后的并发请求挡在了Doris之前,逻辑上并不复杂,但确实需要把所有边界情况考虑清楚。比如拿不到锁时的一次重试只做了一次,如果那次还没命中,就允许直接查Doris——避免极端情况下请求被无限阻塞。
4.3 冷热数据分桶:避免缓存漂移
还有一个我在实战中发现的重要问题——缓存数据分布不均匀。像“近7天全国汇总”这样的热门查询,缓存命中率极高,但Redis里缓存的数据其实是“死”的——TTL一到就删除,又重新查询回填,反复循环。而“近365天某个长尾城市”这种冷门查询,缓存基本不会命中,却白白占着Redis内存直到TTL到期。
为了解决这个问题,我把缓存Key按热度分了两层:
- 热数据桶:前缀
doris:cache:hot:,TTL短(5-10分钟),内存容量大 - 冷数据桶:前缀
doris:cache:cold:,TTL长(1小时),内存容量小
应用层根据业务场景主动指定走哪个桶。热门报表走热桶,长尾分析走冷桶。这样即使冷桶里的数据命中率不高,因为TTL长,回源次数少;而热桶虽然TTL短,但因为频繁命中,实际回源Doris的次数很低。
4.4 监控与告警:缓存方案不能是黑盒
缓存方案落地之后,一定要有监控。我自己吃过这个亏——前期没接监控,上线一周后发现缓存命中率只有不到40%,一直没察觉,因为业务反馈“好像比以前快了一点”,真实的收益被严重高估。
需要关注的指标包括:
- 缓存命中率:通过Redis的
INFO stats命令里的keyspace_hits和keyspace_misses计算,每5分钟采集一次 - 缓存Key数量:
DBSIZE命令,防止缓存无限膨胀 - Redis内存使用量:
INFO memory,和最大内存阈值做对比 - 回源Doris的请求量和耗时:与应用层埋点结合
我习惯在这些指标上配几条基础告警:
- 命中率低于50%触发警告——说明Key设计或者TTL策略有问题
- Redis内存使用量超过90%的maxmemory触发紧急告警——要赶紧扩容或者清理
- 回源Doris的平均耗时超过2秒触发警告——说明缓存不足以压制热点查询,需要排查
5. 常见问题与排查技巧实录
5.1 缓存命中率低
如果你发现命中率上不去,别急着改代码,先排查三个地方。
第一,检查Key的一致性。最常见的问题是多环境共用了一套Redis,导致Key冲突,或者业务代码升级之后Key前缀变了。用Redis的SCAN命令实际看一下存储的Key长什么样,和你预期的做对比,很快能发现问题。
第二,确认SQL模板是否稳定。有些ORM框架会在SQL后面自动拼接LIMIT ?或者排序字段,导致每次生成的SQL模板都不一样。我遇到过一个案例,某查询框架在SQL末尾自动加了limit 1000,但这些细节在日志里看不到,排查了很久才发现。
第三,排查参数序列化问题。比如Long类型的参数和Integer类型的参数在拼接进签名时可能被序列化成不同格式,导致相同的查询条件生成了不同的Key。解决方法是参数拼接前统一转成字符串。
提示:我在排查这个问题时有一个很实用的方法——在Redis里打开
MONITOR命令,实时观察应用发出来的GET和SET命令,直接看到Key的实际情况,比自己猜要快得多。
5.2 缓存穿透导致Doris压力反而增大
如果你的Redis里缓存了大量“查询结果为空”的Key,或者TTL设置得太激进,会出现缓存未命中后所有请求都直接打到Doris的现象,压力甚至比不加缓存时还大。
针对查询结果为空的情况,我采用的办法是空值缓存——即使Doris查询结果为空,也往Redis里写一个空数组的JSON,TTL设置短一些(比如60秒)。这样同一个空查询在60秒内不会再次打到Doris。
但要注意一个副作用:如果这个查询的真实数据在60秒内更新了,用户可能会看到短暂的空数据。所以空值缓存只适用于对实时性不那么敏感的场景。
5.3 数据不一致:缓存和Doris对不上
这个问题的根源在于Doris的更新时机和缓存的TTL不匹配。有几个典型的坑:
第一个坑是Doris的主键更新。如果明细表用的是UNIQUE KEY模型,同一主键的数据会被多次写入,每次写入生成不同的版本。如果查询缓存了老版本的数据,而数据更新后没有触发缓存失效,用户看到的就是旧数据。
第二个坑是时区问题。Doris内部存储的时间戳和业务应用时区不一致时,查询结果里的日期字段会差几个小时。如果这个结果被缓存起来,即使缓存逻辑没问题,数据本身就是错的。
我的处理方式是:在Doris数据导入的链路上增加一个缓存失效钩子,导入完成后再发一个Redis删除的消息。这个钩子不是直接删所有Key,而是根据“业务表名到缓存Key前缀”的映射关系,精确删除受影响表的缓存。由于这部分逻辑比较复杂,我把它们独立成了一个服务,而不是写在业务代码里。
5.4 Redis内存暴涨
缓存方案上线一段时间之后,Redis内存持续上升是常见现象。两个典型的元凶我是都遇到过。
一个是大Key问题。某个跨月份的聚合查询结果集非常大,序列化后可能有几十MB,存进Redis会严重拖慢SET和GET的响应时间。这类大Key应该限制缓存——超过一定大小的查询结果不进Redis,直接回源Doris。
另一个是Key数量持续膨胀。如果业务上频繁出现新的查询维度组合,缓存Key会不断增加,但单个Key的TTL又很长,整体内存占用就会缓慢上升。解决办法是给Redis设置maxmemory并启用淘汰策略allkeys-lru,配合每日凌晨的定时清理脚本。
6. 方案扩展与体验心得
6.1 从“查询缓存”到“两级缓存”
整套方案上线跑通之后,我又做了一轮优化,把“Redis缓存”升级成了“两级缓存”。
第一级是应用本地的Caffeine缓存,命中率最高的那批热查询(比如全国汇总报表),经Redis取出JSON后反序列化成对象,再存一份到Caffeine里。第二级才是Redis。这样做的原因是:高频查询的JSON反序列化开销在QPS很高的时候会成为一个不可忽视的瓶颈,本地内存直接存对象,省掉反序列化这一步。
两级缓存之间的一致性处理有一个简单的做法:在Redis写入缓存时带一个版本号,Caffeine里的对象也带一个版本号;如果Redis版本号更新,则重新加载。不过考虑到Caffeine缓存的TTL很短(通常30秒),即使偶尔不一致,影响也不大。
6.2 从“缓存加速”到“查询路由”
后续还可以把缓存的角色从“加速器”升级成“路由器”。比如在某些场景下,不一定非要等Doris查询完成再回填缓存,而是可以提前预判查询模式——结合报表系统的埋点数据,把第二天可能用到的查询预先算好并填充进Redis。这种“预热缓存”方案对固定报表极其有效,可以把查询响应压到个位数毫秒。
这个方向后续还可以结合权限维度去做——不同角色看到的报表数据不同,可以按角色维度做缓存分片,进一步提升命中率。
6.3 最后聊几句踩坑心得
这套方案整体踩过不少坑,最重要的体会是:缓存方案的好坏,不取决于Redis用得有多溜,而取决于你对业务查询模式的理解深度。
第一个体会是,Key设计必须从第一天就考虑到未来扩展。版本号、业务标识、预留字段,这些看着多余的拼接,在后续业务增长时会帮你省大量的事。
第二个体会是,缓存一定要“可降级”。每一个Redis操作都加了异常捕获,Redis不可用时整个链路自动回源Doris,用户感知不到异常,只是查询慢了一点。“缓存挂了能自动降级”这个保障,在稳定性优先的报表场景里比缓存本身更值钱。
第三个体会是,别把所有查询都塞给缓存。有些查询天然不适合缓存优化(比如带用户级随机参数的查询),硬套缓存方案只会带来一致性问题。把缓存用在刀刃上——也就是高频且稳定的热点查询上——收益才是最大化的。
按这个思路一步步来,Doris和Redis的这套组合,能够非常稳地帮你扛住报表系统的性能压力。
