Redis基于StringRedisTemplate实现缓存热点数据的实战教学
一、业务背景与缓存动机
"客户清单"这类数据有三个典型特征,正是适合缓存的热点数据:
- 读多写少
- 获取成本高:原始数据来自外部开发者平台接口,且需要分页循环拉取全量(每页 100 条),再批量回查
member_base表组装,一次完整获取涉及多次网络 IO + 数据库查询。 - 可容忍短暂不一致:签约状态不是强一致场景,数据滞后 1 小时内可接受。
把这类数据缓存到 Redis,能把"多次远程调用 + DB 查询"降为"一次 Redis 读",显著降低延迟和外部接口压力。
二、涉及的核心技术与概念
2.1 RedisTemplate vs StringRedisTemplate
Spring Data Redis 提供两个核心操作模板:
| 模板 | 默认序列化 | 适用场景 |
|---|---|---|
RedisTemplate<K,V> | JDK 序列化(二进制,不可读) | 存 Java 对象,但 value 在 redis-cli 里是乱码 |
StringRedisTemplate | 字符串(StringRedisSerializer) | key/value 都是字符串,可读性好,跨语言友好 |
本项目选用 StringRedisTemplate,配合手动 JSON 序列化:存入时把对象转成 JSON 字符串,读取时再反序列化回对象。好处是 Redis 里存的是可读的 JSON,排查问题方便,也不依赖 Java 特定的序列化格式。
2.2 缓存的三个基本操作
| 操作 | API | 说明 |
|---|---|---|
| 读 | opsForValue().get(key) | 命中返回字符串,未命中返回 null |
| 写(带过期) | opsForValue().set(key, value, timeout, unit) | 写入并设置 TTL |
| 删 | delete(key) | 删除 key,返回是否删除成功 |
2.3 TTL(Time To Live,过期时间)
给缓存设置存活时长,到期自动删除。作用:
- 兜底一致性:即使没人主动清缓存,数据最多陈旧 TTL 时长(本项目 1 小时)后自动刷新。
- 防止脏数据长期驻留。
2.4 Cache-Aside(旁路缓存)模式
最常用的缓存读写模式,读取逻辑:
读请求 → 查缓存
├─ 命中 → 直接返回
└─ 未命中 → 查数据源 → 回写缓存 → 返回
更新时一般采用"更新数据源 + 删除缓存"(而非更新缓存),让下次读触发重建。本项目的主动清缓存接口正是这个思路。
三、关键设计点拆解
3.1 缓存 key 与 TTL 常量化
/** 签约客户清单缓存key. */ private static final String SIGNED_CUSTOMERS_CACHE_KEY = "sourcing-exchange-signed-customers"; /** 缓存过期时间,1小时. */ private static final long SIGNED_CUSTOMERS_CACHE_HOURS = 1;
key 和 TTL 用常量集中管理,避免散落字符串导致读、写、删三处 key 不一致(这是缓存 bug 的高发区)。key 带业务前缀 sourcing-exchange-,便于在 Redis 中按前缀归类和排查。
3.2 读路径:Cache-Aside + 容错降级
// 1. 优先从 Redis 读
try {
String cachedJson = stringRedisTemplate.opsForValue().get(SIGNED_CUSTOMERS_CACHE_KEY);
if (cachedJson != null) {
List<ExchangeContractCustomerDto> cachedList = StringUtil.parseJsonString(
cachedJson, new TypeReference<List<ExchangeContractCustomerDto>>() {});
if (cachedList != null) {
return cachedList; // 命中,直接返回
}
}
} catch (Exception e) {
log.warn("读取缓存异常, 继续查询外部接口", e); // 缓存故障不阻断主流程
}
// 2. 未命中:查数据源(分页拉外部接口 + 回查 DB)
Set<String> customerCodeSet = fetchAllSignedCustomerCodes();
List<ExchangeContractCustomerDto> resultList = buildCustomerDtoList(customerCodeSet);
// 3. 回写缓存
cacheSignedCustomers(resultList);
return resultList;
容错设计是亮点:读缓存被 try-catch 包裹,Redis 挂了只打 warn 日志然后降级去查数据源,不让缓存故障拖垮主业务。这是生产级缓存代码的必备特性 —— 缓存是加速器,不应成为单点依赖。
3.3 写路径:序列化 + 带 TTL 写入,且写失败不影响返回
private void cacheSignedCustomers(List<ExchangeContractCustomerDto> resultList) {
try {
String json = StringUtil.getJsonString(resultList); // 对象 → JSON
stringRedisTemplate.opsForValue().set(
SIGNED_CUSTOMERS_CACHE_KEY, json,
SIGNED_CUSTOMERS_CACHE_HOURS, TimeUnit.HOURS); // 带 TTL 写入
} catch (Exception e) {
log.warn("写入缓存异常", e); // 写缓存失败仅告警,数据照常返回
}
}
写缓存同样容错:即便写 Redis 失败,方法已经拿到正确数据,照常返回给调用方,只是下次还得重新查数据源。
3.4 主动清缓存接口
@Override
public void clearSignedCustomersCache() {
try {
Boolean deleted = stringRedisTemplate.delete(SIGNED_CUSTOMERS_CACHE_KEY);
log.info("清除缓存, key={}, deleted={}", SIGNED_CUSTOMERS_CACHE_KEY, deleted);
} catch (Exception e) {
log.error("清除缓存异常", e);
throw new JshServerException("清除签约客户缓存失败");
}
}
这个方法通过 REST 接口 暴露出去。它解决的核心问题是 TTL 的滞后性:当签约名单发生变更(新签约 / 解约),运营无需等待最多 1 小时的 TTL 到期,可以手动触发清缓存,下一次读请求就会 Cache-Aside 重建出最新数据。
注意这里与读写路径的差异:清缓存失败时抛异常(而非降级),因为清缓存是一个有明确意图的管理动作,失败必须让调用方知道,否则会误以为缓存已刷新而继续读到旧数据。
四、通用示例代码
下面把上述模式抽象成一个可复用的通用缓存服务骨架:
@Slf4j
@Service
public class GenericCacheService {
@Resource
private StringRedisTemplate stringRedisTemplate;
/** 缓存 key(带业务前缀). */
private static final String CACHE_KEY = "demo:hot-data";
/** TTL. */
private static final long CACHE_TTL = 1;
private static final TimeUnit TTL_UNIT = TimeUnit.HOURS;
/**
* Cache-Aside 读取:先缓存,未命中查数据源并回写.
*/
public List<MyData> getData() {
// 1. 读缓存(故障降级)
try {
String json = stringRedisTemplate.opsForValue().get(CACHE_KEY);
if (json != null) {
List<MyData> cached = parseJson(json);
if (cached != null) {
return cached;
}
}
} catch (Exception e) {
log.warn("读缓存异常,降级查数据源", e);
}
// 2. 回源
List<MyData> fresh = loadFromSource(); // 远程接口 / DB
// 3. 回写缓存(失败不影响返回)
writeCache(fresh);
return fresh;
}
private void writeCache(List<MyData> data) {
try {
stringRedisTemplate.opsForValue()
.set(CACHE_KEY, toJson(data), CACHE_TTL, TTL_UNIT);
} catch (Exception e) {
log.warn("写缓存异常", e);
}
}
/** 主动清缓存:失败抛异常,让调用方感知. */
public void evictCache() {
try {
Boolean deleted = stringRedisTemplate.delete(CACHE_KEY);
log.info("清缓存, key={}, deleted={}", CACHE_KEY, deleted);
} catch (Exception e) {
log.error("清缓存异常", e);
throw new RuntimeException("清缓存失败");
}
}
// --- 省略 parseJson / toJson / loadFromSource 实现 ---
}
五、这套实现的优缺点评估
优点
- Cache-Aside 模式清晰,读写删 key 常量统一,不易出错。
- 读写路径对 Redis 故障做了降级容错,缓存不成为单点。
- 同时具备TTL 自动过期(兜底)和主动清缓存(即时刷新)两种失效机制,兼顾一致性与实时性。
- 用
StringRedisTemplate+ JSON,Redis 中数据可读、跨语言友好。
潜在改进点
- 缓存击穿:当 key 过期或被清除的瞬间,若有大量并发请求同时未命中,会一起回源(分页拉外部接口 + DB),造成瞬时压力。可用分布式锁或"单飞"(single-flight)让只有一个线程回源重建。
- 缓存穿透:若数据源返回空,当前实现返回
Collections.emptyList()但不缓存空结果,下次仍会回源。对频繁查空的场景可考虑缓存空值并设较短 TTL。 - 序列化耦合:手动 JSON 序列化需要调用方自行保证读写用相同结构;可封装统一的序列化工具减少重复。
六、设计要点提炼
- Cache-Aside 模式:读时旁路回源并回写,写时删除缓存触发重建。
- 双失效机制:TTL 兜底 + 主动清缓存即时刷新,互为补充。
- 缓存故障降级:读写缓存包
try-catch,缓存不可用时退化为直接查数据源,保证可用性。 - 管理动作强反馈:清缓存失败抛异常,与读写路径的"静默降级"区别对待。
- key/TTL 常量化:集中管理,杜绝多处 key 不一致。
- 可读优先:
StringRedisTemplate+ JSON,便于运维排查。
到此这篇关于Redis基于StringRedisTemplate实现缓存热点数据的实战教学的文章就介绍到这了,更多相关Redis缓存热点数据内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Redis高级数据类型Hyperloglog、Bitmap的使用
很多小伙伴在面试中都会被问道 Redis的常用数据结构有哪些?可能很大一部分回答都是 string、hash、list、set、zset,但其实还有Hyperloglog和Bitmap,本文就来介绍一下2021-05-05
redislive监控redis服务的图文教程_动力节点Java 学院整理
这篇文章主要介绍了redislive监控redis服务的图文教程_动力节点Java 学院整理,需要的朋友可以参考下2017-08-08


最新评论