目录
背景:
最近业务写到检索这块的内容,设计到搜索记录的增删操作。考虑到频繁写库操作所以使用缓存技术 redis 和 cacha。
思路:
查询:先查内存查缓存在查询数据库,查询数据库先更新内存在更新缓存
插入:因为搜索历史不同于文章统计功能改变其根据id 修改uv pv 值即可,也考虑到检索是否正常执行完成,可以保证其搜索内容的唯一性(之前就是搜索一次添加一行数据),修改其检索时间查询数据库根据检索时间倒序也无需在数据库层面去重操作。搜索内容在历史已经存在,更新内存缓存的检索时间,如果不存在,添加后同步到内存在缓存。
删除:删除数据库的搜索内容删除刷新内存缓存,或者数据库有状态标识,但重新搜索同样内容需要考虑是这条数据是做修改操作还是另外新增。第一种方式简单方便高效数据库也干净
修改:无修改操作
问题
在实现的过程中发现历史搜索并不单纯只包含搜索内容,搜索的时间命中的数据方便,可用于后期实现搜索相关的计算和统计。所以搜索插入的时候不能靠搜索内容省去去重的操作,所以每次搜索的时候相关的信息需要全部入缓存然后再同步库。
实现
搜索时先将搜索内容入库获获取主键id,方便于后期可以将数据不显示
@ApiOperation("模糊搜索")
@PostMapping("/dim")
private AjaxResult dimSearch(@RequestBody @Validated DimSearchParam param) {
SearchTypeEnum searchType = SearchTypeEnum.getSearchType(param.getType());
// 1.保存搜索记录
SearchHistory searchHistory = searchHistoryService.saveSearchInfo(param.getSearchContent(), searchType.name(), SecurityUtils.getUserId());
// 2.添加搜素记录缓存
LatelySearchRecordVo latelySearchRecordVo = new LatelySearchRecordVo(param.getSearchContent(), searchHistory.getId());
searchCacheService.addSearchRecordLocalCache(SecurityUtils.getUserId(), searchType, latelySearchRecordVo);
// 3.组装参数
SearchDim search = (SearchDim) searchBeanContainer.getSearchBean(searchType.getBeanName());
param.setSearchType(searchType);
param.setId(searchHistory.getId());
SearchContext<EsPageInfo, DimSearchParam> searchContext = new SearchContext<>(search);
// 4.执行搜索
EsPageInfo esPageInfo= searchContext.execute(param);
return AjaxResult.success(esPageInfo);
}
本地缓存使用了caffeine,高可用,且对数据删除淘汰机制都提供了方法
private final Cache<String, Set<LatelySearchRecordVo>> searchContentCache = Caffeine.newBuilder()
.expireAfterAccess(1, TimeUnit.SECONDS)
.maximumSize(1000)
.build();
添加本地缓存和redis缓存 (搜索记录需要去重,所以删除了之前搜素内容相同的)
@PostMapping("/lately/search/record")
@ApiOperation(value = "最近搜索记录")
private AjaxResult latelySearchRecord(@RequestBody LatelySearchRecordParam param){
SearchTypeEnum searchType = verifySearchType(param.getType());
// 1. 获取缓存
Long userId = SecurityUtils.getUserId();
Set<LatelySearchRecordVo> cacheVo = searchCacheService.getSearchRecordCacheByLocal(userId, searchType, param.getPage(), param.getSize());
if (StringUtils.isEmpty(cacheVo)){
startPage();
return success(searchHistoryService.latelySearchRecord(searchType, userId));
}
return success(cacheVo);
}
/**
* 添加搜索记录缓存
*/
public void addSearchRecordLocalCache(Long userId, SearchTypeEnum searchType, LatelySearchRecordVo searchHistory) {
String localCacheKey = LocalCacheKey.getSearchContentKey(userId, searchType);
Set<LatelySearchRecordVo> localCacheSet = searchContentCache.getIfPresent(localCacheKey);
if (StringUtils.isEmpty(localCacheSet)) {
localCacheSet = new LinkedHashSet<>();
}
// 1.添加内存
localCacheSet.removeIf(l -> l.getSearchContent().equals(searchHistory.getSearchContent()));
localCacheSet.add(searchHistory);
searchContentCache.put(localCacheKey, localCacheSet);
addSearchRecordLocalRedis(userId, searchType, searchHistory);
}
/**
* 添加搜索记录缓存
*/
public void addSearchRecordLocalRedis(Long userId, SearchTypeEnum searchType, LatelySearchRecordVo searchHistory) {
String redisCacheKey = RedisCacheKey.getSearchContentKey(userId, searchType);
ZSetOperations<String, LatelySearchRecordVo> redisZet = redisTemplate.opsForZSet();
// 2.缓存存在的清空下需要去重相同搜素内容
if (redisTemplate.hasKey(redisCacheKey)){
long length = redisZet.size(redisCacheKey) - 1;
Set<LatelySearchRecordVo> redisCacheSet = redisZet.range(redisCacheKey, 0, length);
Set<LatelySearchRecordVo> delCache
= redisCacheSet.stream().filter(l -> l.getSearchContent().equals(searchHistory.getSearchContent())).collect(Collectors.toSet());
if (!StringUtils.isEmpty(delCache)){
redisZet.remove(redisCacheKey, delCache.toArray());
}
}
// 3.添加缓存
redisZet.add(redisCacheKey, searchHistory, System.currentTimeMillis());
redisTemplate.expire(redisCacheKey, 30, TimeUnit.MINUTES);
}
在查询搜索记录时优先从缓存获取
/**
* 从本地缓存获取搜索记录
*/
public Set<LatelySearchRecordVo> getSearchRecordCacheByRedis(Long userId, SearchTypeEnum searchType, int pageNum, int pageSize) {
String redisCacheKey = RedisCacheKey.getSearchContentKey(userId, searchType);
ZSetOperations<String, LatelySearchRecordVo> zet = redisTemplate.opsForZSet();
Set<LatelySearchRecordVo> redisCacheSet = zet.range(redisCacheKey, pageNum, pageSize);
// 3.缓存没有
if (StringUtils.isNull(redisCacheSet)){
return null;
}
return redisCacheSet;
}
public Set<LatelySearchRecordVo> getSearchRecordCacheByLocal(Long userId, SearchTypeEnum searchType, int pageNum, int pageSize) {
String localCacheKey = LocalCacheKey.getSearchContentKey(userId, searchType);
pageNum = (pageNum - 1) * pageSize;
pageSize = pageNum * pageSize - 1;
// 1.从内存获取
Set<LatelySearchRecordVo> localCacheSet = searchContentCache.getIfPresent(localCacheKey);
// 2.内存没有从缓存获取
if (StringUtils.isNull(localCacheSet)) {
return getSearchRecordCacheByRedis(userId, searchType, pageNum, pageSize);
}
int maxLength = localCacheSet.size() - 1;
if (pageSize > maxLength) {
pageSize = maxLength;
}
return localCacheSet.stream().skip(pageNum).limit(6).collect(Collectors.toSet());
}
缓存没有的从库里查 添加缓存
@Override
public List<LatelySearchRecordVo> latelySearchRecord(SearchTypeEnum searchType, Long userId) {
LinkedList<LatelySearchRecordVo> vos = new LinkedList<>();
List<SearchHistory> searchRecodeList = searchHistoryMapper.getLatelySearchContent(searchType.name(), userId);
searchRecodeList.stream().forEach(recode -> {
LatelySearchRecordVo vo = new LatelySearchRecordVo();
BeanUtils.copyProperties(recode, vo);
vos.add(vo);
});
if (!StringUtils.isEmpty(vos)){
// 写入缓存
searchCacheService.addSearchRecord(userId, searchType, vos);
}
return vos;
}
public void addSearchRecord(Long userId, SearchTypeEnum searchType, List<LatelySearchRecordVo> searchHistory){
addSearchRecordLocalCache(userId, searchType, searchHistory);
addSearchRecordLocalRedis(userId, searchType, searchHistory);
}
删除搜索记录改库 刷新缓存
@DeleteMapping("/{id}/{type}")
@ApiOperation(value = "删除搜索记录")
private AjaxResult removeSearchHistoryNoDisplay(@Validated RemoveSearchHistoryNoDisplayParam param){
SearchTypeEnum searchType = verifySearchType(param.getType());
searchHistoryService.searchRecodeNoDisplay(param.getId(), SecurityUtils.getUserId(), searchType);
return success();
}
@DeleteMapping("/all")
@ApiOperation(value = "删除所有的搜索记录")
private AjaxResult removeSearchHistoryAllNoDisplay(@RequestBody RemoveSearchHistoryAllNoDisplayParam param){
SearchTypeEnum searchType = verifySearchType(param.getType());
searchHistoryService.searchHistoryAllNoDisplay(SecurityUtils.getUserId(), searchType);
return success();
}
@Override
public void searchRecodeNoDisplay(Long id, Long uid, SearchTypeEnum searchType) {
searchHistoryMapper.updateSearchRecodeNoDisplay(id, uid);
// 刷新缓存
searchCacheService.refreshCache(uid, searchType);
}
@Override
public void searchHistoryAllNoDisplay(Long userId, SearchTypeEnum searchType) {
searchHistoryMapper.updateAllSearchRecodeNoDisplay(userId, searchType.name());
// 刷新缓存
searchCacheService.refreshCache(userId, searchType);
}