在互联网应用中,关键词搜索是高频核心场景,电商商品搜索、资讯内容检索、短视频推荐等场景均依赖该能力。但搜索过程往往涉及复杂的索引查询、数据聚合与排序,直接查询数据库或搜索引擎会导致响应延迟高、后端服务压力大。缓存作为性能优化的核心手段,能有效提升搜索响应速度、降低后端负载。本文聚焦关键词搜索结果的缓存策略设计,并结合 Redis 实现落地,从场景分析、策略设计到代码实战,完整阐述搜索缓存的核心逻辑。
一、搜索结果缓存的核心挑战
与普通数据缓存(如用户信息、商品详情)不同,关键词搜索结果缓存面临独特的挑战:
- 关键词多样性:用户输入的关键词千变万化,存在同义词、近义词、拼写变体(如 “手机” 与 “智能手机”、“苹果手机” 与 “iphone 手机”),直接缓存原始关键词易导致缓存碎片化、命中率低;
- 结果实时性:搜索结果可能随数据更新(如商品上下架、资讯发布)动态变化,需平衡缓存有效期与数据新鲜度;
- 分页与排序:用户常使用分页(如第 1 页 / 第 2 页)、排序(如按销量 / 时间)筛选结果,缓存需兼容多维度查询条件;
- 缓存雪崩 / 击穿风险:热门关键词缓存失效时,大量请求直击后端,易引发服务雪崩;低频关键词缓存未命中时,也可能导致单点击穿。
二、搜索结果缓存策略设计
针对上述挑战,需从缓存键设计、过期策略、缓存更新、防雪崩 / 击穿 四个维度设计核心策略:
1. 缓存键(Key)设计:标准化 + 维度化
缓存键需唯一标识 “关键词 + 查询条件” 组合,同时兼顾可读性与性能:
- 标准化关键词:对原始关键词做归一化处理(如去空格、转小写、同义词归一),例如将 “苹果 手机 13” 处理为 “苹果手机 13”,减少因格式差异导致的缓存冗余;
- 维度拼接:将分页、排序、筛选条件拼接至键中,格式示例:
search:{标准化关键词}:{排序类型}:{页码}:{页大小}; - 哈希优化:若关键词过长(如超过 64 字符),可对关键词做 MD5 哈希,避免键过长影响 Redis 性能,示例:
search:md5(长关键词):sort:price:page:1:size:20。
2. 过期策略:分级 TTL + 主动失效
-
分级 TTL(Time-To-Live) :根据关键词热度设置不同过期时间:
- 热门关键词(如 “2025 新款手机”):TTL=5 分钟(高频访问,缩短过期时间保证新鲜度);
- 普通关键词:TTL=30 分钟;
- 低频关键词:TTL=2 小时(减少缓存空间占用);
-
主动失效:当底层数据更新时(如商品价格修改、资讯发布),主动删除关联关键词的缓存,保证数据一致性。例如:商品 ID=1001 的手机价格更新,删除所有包含 “苹果手机 13” 的缓存键。
3. 缓存更新策略:懒加载 + 异步更新
- 懒加载(Cache Aside) :核心逻辑为 “查缓存→未命中查后端→写入缓存”,是搜索缓存的基础模式;
- 异步更新:对热门关键词,在缓存即将过期前(如剩余 1 分钟),异步触发后端查询并更新缓存,避免缓存失效后大量请求直击后端。
4. 防雪崩 / 击穿 / 穿透策略
- 防雪崩:缓存过期时间添加随机偏移量(如 ±10 秒),避免大量缓存同时失效;
- 防击穿:对热门关键词使用 Redis 分布式锁或 “缓存预热”,确保同一时间只有一个请求去后端查询;
- 防穿透:对不存在结果的关键词(如 “不存在的商品 12345”),缓存空结果(TTL=1 分钟),避免恶意请求直击后端。
三、Redis 实战实现
1. 技术选型
- 缓存中间件:Redis(5.0+),使用 String 类型存储序列化后的搜索结果(JSON 格式);
- 开发语言:Java(Spring Boot 2.7+),结合 Spring Data Redis 简化 Redis 操作;
- 序列化:Jackson,实现搜索结果的 JSON 序列化 / 反序列化。
2. 核心代码实现
(1)配置 Redis 连接
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Jackson序列化配置
Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSerializer.setObjectMapper(om);
// String序列化器(键)
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jacksonSerializer);
template.setHashValueSerializer(jacksonSerializer);
template.afterPropertiesSet();
return template;
}
// 缓存过期时间配置(分级TTL)
@Bean
public CacheTTLConfig cacheTTLConfig() {
CacheTTLConfig config = new CacheTTLConfig();
config.setHotKeywordTTL(5 * 60); // 热门关键词:5分钟
config.setNormalKeywordTTL(30 * 60); // 普通关键词:30分钟
config.setLowFreqKeywordTTL(2 * 60 * 60); // 低频关键词:2小时
config.setEmptyResultTTL(60); // 空结果:1分钟
return config;
}
}
// 缓存TTL配置类
@Data
@Component
@ConfigurationProperties(prefix = "search.cache.ttl")
public class CacheTTLConfig {
private int hotKeywordTTL;
private int normalKeywordTTL;
private int lowFreqKeywordTTL;
private int emptyResultTTL;
}
(2)关键词标准化工具类
@Component
public class KeywordNormalizer {
// 同义词映射(示例)
private static final Map<String, String> SYNONYM_MAP = new HashMap<>();
static {
SYNONYM_MAP.put("iphone13", "苹果手机13");
SYNONYM_MAP.put("智能手机", "手机");
// 可扩展更多同义词
}
/**
* 关键词标准化:去空格、转小写、同义词替换
*/
public String normalize(String keyword) {
if (StringUtils.isBlank(keyword)) {
return "";
}
// 去空格、转小写
String normalized = keyword.trim().toLowerCase().replaceAll("\s+", "");
// 同义词替换
return SYNONYM_MAP.getOrDefault(normalized, normalized);
}
}
(3)搜索缓存核心服务
@Service
public class SearchCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private KeywordNormalizer keywordNormalizer;
@Autowired
private CacheTTLConfig cacheTTLConfig;
@Autowired
private SearchBackendService searchBackendService; // 后端搜索服务(如ES/数据库)
// 分布式锁前缀
private static final String LOCK_PREFIX = "search:lock:";
// 锁超时时间(秒)
private static final long LOCK_EXPIRE = 10;
/**
* 核心搜索方法:缓存优先,未命中则查后端并更新缓存
*/
public SearchResult search(String keyword, String sortType, int pageNum, int pageSize) {
// 1. 关键词标准化
String normalizedKeyword = keywordNormalizer.normalize(keyword);
if (StringUtils.isBlank(normalizedKeyword)) {
return new SearchResult(); // 空关键词返回空结果
}
// 2. 构建缓存键
String cacheKey = buildCacheKey(normalizedKeyword, sortType, pageNum, pageSize);
// 3. 查缓存
SearchResult cacheResult = (SearchResult) redisTemplate.opsForValue().get(cacheKey);
if (cacheResult != null) {
return cacheResult;
}
// 4. 缓存未命中,加分布式锁防击穿
String lockKey = LOCK_PREFIX + cacheKey;
boolean locked = false;
try {
locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_EXPIRE, TimeUnit.SECONDS);
if (locked) {
// 5. 查后端服务
SearchResult backendResult = searchBackendService.search(normalizedKeyword, sortType, pageNum, pageSize);
// 6. 写入缓存(分级TTL)
int ttl = getTTLByKeywordHeat(normalizedKeyword, backendResult);
redisTemplate.opsForValue().set(cacheKey, backendResult, ttl, TimeUnit.SECONDS);
return backendResult;
} else {
// 7. 其他线程已加锁,等待后重试查缓存
Thread.sleep(100);
return (SearchResult) redisTemplate.opsForValue().get(cacheKey);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return new SearchResult();
} finally {
// 8. 释放锁
if (locked) {
redisTemplate.delete(lockKey);
}
}
}
/**
* 构建缓存键
*/
private String buildCacheKey(String normalizedKeyword, String sortType, int pageNum, int pageSize) {
// 关键词过长则MD5哈希
String keyKeyword = normalizedKeyword.length() > 64
? DigestUtils.md5DigestAsHex(normalizedKeyword.getBytes())
: normalizedKeyword;
return String.format("search:%s:sort:%s:page:%d:size:%d",
keyKeyword, sortType, pageNum, pageSize);
}
/**
* 根据关键词热度和结果类型获取TTL
*/
private int getTTLByKeywordHeat(String keyword, SearchResult result) {
// 1. 空结果返回空结果TTL
if (result == null || result.getItems().isEmpty()) {
return cacheTTLConfig.getEmptyResultTTL();
}
// 2. 模拟判断热门关键词(实际可通过访问频次、业务规则判断)
Set<String> hotKeywords = new HashSet<>(Arrays.asList("2025新款手机", "苹果手机13", "华为mate70"));
if (hotKeywords.contains(keyword)) {
return cacheTTLConfig.getHotKeywordTTL();
}
// 3. 模拟判断低频关键词(实际可通过访问频次判断)
Set<String> lowFreqKeywords = new HashSet<>(Arrays.asList("2020旧款手机", "功能机"));
if (lowFreqKeywords.contains(keyword)) {
return cacheTTLConfig.getLowFreqKeywordTTL();
}
// 4. 普通关键词
return cacheTTLConfig.getNormalKeywordTTL();
}
/**
* 主动删除缓存(数据更新时调用)
*/
public void deleteCache(String keyword, String sortType) {
String normalizedKeyword = keywordNormalizer.normalize(keyword);
if (StringUtils.isBlank(normalizedKeyword)) {
return;
}
// 模糊匹配删除(Redis 5.0+支持SCAN遍历,避免KEYS命令阻塞)
String pattern = "search:%s:sort:%s:*";
ScanOptions options = ScanOptions.scanOptions().match(String.format(pattern, normalizedKeyword, sortType)).count(100).build();
Cursor<String> cursor = redisTemplate.scan(options);
while (cursor.hasNext()) {
redisTemplate.delete(cursor.next());
}
}
}
// 搜索结果实体类
@Data
public class SearchResult implements Serializable {
private static final long serialVersionUID = 1L;
private List<SearchItem> items; // 搜索结果列表
private long total; // 总条数
private int pageNum; // 当前页
private int pageSize; // 页大小
}
// 搜索项实体类
@Data
public class SearchItem implements Serializable {
private static final long serialVersionUID = 1L;
private Long id; // 数据ID
private String title; // 标题
private String content; // 内容
private Double price; // 价格(电商场景)
private Long createTime; // 创建时间
}
// 后端搜索服务(模拟实现)
@Service
public class SearchBackendService {
/**
* 模拟调用ES/数据库查询搜索结果
*/
public SearchResult search(String keyword, String sortType, int pageNum, int pageSize) {
// 模拟查询逻辑
SearchResult result = new SearchResult();
List<SearchItem> items = new ArrayList<>();
if ("苹果手机13".equals(keyword)) {
SearchItem item1 = new SearchItem();
item1.setId(1L);
item1.setTitle("苹果手机13 256G");
item1.setContent("全新未拆封,官方正品");
item1.setPrice(5999.0);
item1.setCreateTime(System.currentTimeMillis());
items.add(item1);
SearchResult item2 = new SearchResult();
// 省略更多数据...
}
result.setItems(items);
result.setTotal(items.size());
result.setPageNum(pageNum);
result.setPageSize(pageSize);
return result;
}
}
(4)控制器层调用示例
@RestController
@RequestMapping("/api/search")
public class SearchController {
@Autowired
private SearchCacheService searchCacheService;
/**
* 搜索接口
*/
@GetMapping
public ResponseEntity<SearchResult> search(
@RequestParam String keyword,
@RequestParam(defaultValue = "time") String sortType,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "20") int pageSize) {
SearchResult result = searchCacheService.search(keyword, sortType, pageNum, pageSize);
return ResponseEntity.ok(result);
}
/**
* 数据更新后主动删除缓存接口
*/
@PostMapping("/cache/delete")
public ResponseEntity<Void> deleteCache(
@RequestParam String keyword,
@RequestParam(defaultValue = "time") String sortType) {
searchCacheService.deleteCache(keyword, sortType);
return ResponseEntity.noContent().build();
}
}
3. 缓存预热与监控
(1)缓存预热
针对热门关键词,可在服务启动时主动查询后端并写入缓存:
@Component
public class CacheWarmUp implements CommandLineRunner {
@Autowired
private SearchCacheService searchCacheService;
@Autowired
private CacheTTLConfig cacheTTLConfig;
// 热门关键词列表
private static final List<String> HOT_KEYWORDS = Arrays.asList("2025新款手机", "苹果手机13", "华为mate70");
@Override
public void run(String... args) throws Exception {
// 异步预热,不阻塞服务启动
CompletableFuture.runAsync(() -> {
for (String keyword : HOT_KEYWORDS) {
searchCacheService.search(keyword, "time", 1, 20);
}
System.out.println("热门关键词缓存预热完成");
});
}
}
(2)缓存监控
通过 Redis 的INFO stats命令或 Spring Boot Actuator 监控缓存命中率:
@RestController
@RequestMapping("/actuator/cache")
public class CacheMonitorController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/hit-rate")
public ResponseEntity<Map<String, Object>> getCacheHitRate() {
// 获取Redis统计信息
Properties stats = (Properties) redisTemplate.execute((RedisCallback<Properties>) connection -> {
return connection.info("stats");
});
long hits = Long.parseLong(stats.getProperty("keyspace_hits", "0"));
long misses = Long.parseLong(stats.getProperty("keyspace_misses", "0"));
double hitRate = hits == 0 ? 0 : (double) hits / (hits + misses);
Map<String, Object> result = new HashMap<>();
result.put("hits", hits);
result.put("misses", misses);
result.put("hitRate", String.format("%.2f%%", hitRate * 100));
return ResponseEntity.ok(result);
}
}
四、性能优化与注意事项
- Redis 集群部署:生产环境使用 Redis 集群(主从 + 哨兵 / 分片),避免单点故障;
- 缓存压缩:对大体积搜索结果(如超过 10KB),使用 GZIP 压缩后存储,减少网络传输和 Redis 内存占用;
- 避免缓存污染:对恶意 / 异常关键词(如超长乱码),直接过滤不缓存,避免占用 Redis 空间;
- TTL 随机偏移:在分级 TTL 基础上添加 ±10 秒随机值,避免大量缓存同时失效:
private int getTTLWithRandom(int baseTTL) {
Random random = new Random();
return baseTTL + random.nextInt(21) - 10; // ±10秒
}
5.限流降级:结合 Sentinel/Hystrix 对搜索接口限流,缓存失效时降级返回基础结果,避免后端服务过载。
五、总结
关键词搜索结果的缓存设计需兼顾命中率、实时性、稳定性三大核心目标,通过标准化关键词、分级 TTL、分布式锁、主动失效等策略,可有效解决搜索缓存的特有问题。本文基于 Redis 的实战实现,覆盖了缓存设计、代码落地、性能优化全流程,可直接适配电商、资讯、短视频等主流搜索场景。在实际应用中,需结合业务特点调整缓存策略(如热度判断规则、TTL 时长),并通过监控持续优化缓存命中率,最终实现搜索接口的低延迟、高可用。