核心需求
一个项目中,遇到了搜索热词统计的需求,我使用了 Redis 的五大数据类型之一 Sorted Set 实现。目前有两项数据需要统计:“当日搜索热词 top10”和“当周搜索热词 top10”。
关于这两项数据的统计方法,目前想到了两种实现方法:
- 两个 Redis 的 Sorted Set 实现,一个 Sorted Set A 统计当天,0 点 top10 记录进 MySQL,Sorted Set 清零。一个 Sorted Set B 统计当周,每周日 top10 记录进 MySQL,Sorted Set B 清零。
- 只使用用一个 Sorted Set 记录当天搜索热词,0 点 top10 记录进 MySQL,Sorted Set 清零。到周日时,会有 7 10 行记录。把这 7 10 行遍历,每次便利都记录进 Sorted Set,全部遍历结束后,再从 Sorted Set 中取出 top10 记录进 MySQL 的周热词统计表中。
Sorted Set 是 Redis 的数据结构,方法 1 会占用两份内存,一份当天的,一份当周的。方法 2 会提高系统的复杂度,并且在统计周表时,可能会出现短时间内大量的计算(当然可以使用定时任务, 把周表的统计放到凌晨进行)。
最后选择了方案 1,分开维护清晰明了。
至于内存占用问题,1MB = 1048576 字节,按两个字节存一个字算,理论上 1MB 能存 1048576/2/8 = 65,536 个不重复的搜索关键词(当然使用 Sorted Set 肯定比纯字更多占用一些空间)。多投入一些内存,能存下的数量还是很大的,通常可以撑到每周结束清理内存。一般的 CRUD 项目不用怎么考虑内存占用。
问题中涉及的相关知识
一个项目中,遇到了搜索热词统计的需求。我使用了 Redis 的五大数据类型之一 Sorted Set 实现。
Redis 有序集合(sorted set)
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。Redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数 (score) 却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。集合中最大的成员数为 232 – 1 (4294967295, 每个集合可存储 40 多亿个成员)。
定义出自:菜鸟教程
如上图,Redis 的 Sorted Set 自带排序功能。
操作方法也比较简单,在本项目中,核心是两个方法:
zincrby 命令,对于一个 Sorted Set,存在的就把分数加 x (x 可自行设定),不存在就创建一个分数为 1 的成员。
zrevrange,查询 sorted set 中指定顺序的值。返回有序的集合中,score 大的在前面。
在 StringRedisTemplate 中,Sorted Set 被称为 ZSet。更多 redis (java 客户端) 的 Soeted Set 使用方法请见:Redis 之 ZSet 数据结构使用姿势
代码
service 示例代码:
@Service
public class RedisServiceImpl implements RedisService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 记录查询热词
* zincrby 命令,对于一个 Sorted Set,存在的就把分数加 x(x 可自行设定),不存在就创建一个分数为 1 的成员
*
* @param keyword 搜索关键词
*/
@Override
public void searchZincrby(String keyword) {redisTemplate.opsForZSet().incrementScore("searchHotKey", keyword, 1.0);
}
/**
* zrevrange 命令, 查询集合中指定顺序的值
* 返回有序的 sorted set 中,score 大的在前面
*
* @param start 查询范围开始位置
* @param end 查询范围结束位置
* @return
*/
@Override
public Set<ZSetOperations.TypedTuple<String>> queryTopSearchHotKey(Integer start, Integer end) {Set<ZSetOperations.TypedTuple<String>> resultSet = redisTemplate.opsForZSet().reverseRange("searchHotKey", start, end);
return resultSet;
}
}
controller 示例代码
@RestController
@RequestMapping("/team")
public class TeamController {
@Autowired
private RedisService redisService;
/**
* 测试 redis 记录 HotKey
*
* @param keyword 搜索关键词
* @return
*/
@GetMapping("/test_hot_key")
public ResultVO testHotKey(@RequestParam("keyword") String keyword) {redisService.searchZincrby(keyword);
return ResultVOUtil.success(1, "test-return");
}
/**
* 测试 redis 查询指定范围的热词
*
* @param start 查询范围开始位置
* @param end 结束位置
* @return
*/
@GetMapping("/test_query_top_hot_key")
public ResultVO testQueryTopHotKey(@RequestParam("start") Integer start,
@RequestParam("end") Integer end) {Set<ZSetOperations.TypedTuple<String>> resultSet = redisService.queryTopSearchHotKey(start, end);
return ResultVOUtil.success(1, "success", resultSet);
}
}
测试代码的运行效果
模拟搜索一些 hotkey:
使用 rdm 查看 reids 的存储情况,搜索热词已经存在 redis 一个名为 searchHotKey 的 Sorted Set 中:
查询结果: