redis 实现搜索热词统计

本贴最后更新于 1499 天前,其中的信息可能已经时移世易

核心需求

一个项目中,遇到了搜索热词统计的需求,我使用了 Redis 的五大数据类型之一 Sorted Set 实现。目前有两项数据需要统计:“当日搜索热词 top10”和“当周搜索热词 top10”。

关于这两项数据的统计方法,目前想到了两种实现方法:

  1. 两个 Redis 的 Sorted Set 实现,一个 Sorted Set A 统计当天,0 点 top10 记录进 MySQL,Sorted Set 清零。一个 Sorted Set B 统计当周,每周日 top10 记录进 MySQL,Sorted Set B 清零。
  2. 只使用用一个 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 多亿个成员)。
定义出自:菜鸟教程

01redis sorted set 展示

如上图,Redis 的 Sorted Set 自带排序功能。

操作方法也比较简单,在本项目中,核心是两个方法:

zincrby,对于一个 Sorted Set,存在的就把分数加 x (x 可自行设定),不存在就创建一个分数为 1 的成员。

zrevrange,查询 sorted set 中指定范围的值。返回的有序集合中,score 大的在前面。此方法无需担心用于指定范围的 start 和 end 出现越界报错问题。

在 StringRedisTemplate 中,Sorted Set 被称为 ZSet。更多 redis (java 客户端) 的 Soeted Set 使用方法请见:Redis 之 ZSet 数据结构使用姿势

代码

service 示例代码

@Service("redisService")
public class RedisServiceImpl implements RedisService {
    @Autowired
    private StringRedisTemplate redisTemplate;
  
    /**
     * 使用Sorted Set记录keyword
     * zincrby命令,对于一个Sorted Set,存在的就把分数加x(x可自行设定),不存在就创建一个分数为1的成员
     *
     * @param keyword 搜索关键词
     */
    @Override
    public void searchZincrby(String keyword) {
        // 名为sortedSetName的Sorted Set不用预先创建,不存在会自动创建,存在则向里添加数据
        String sortedSetName = "searchHotWord";
        // x 的含义请见本方法的注释
        double x = 1.0;
        redisTemplate.opsForZSet().incrementScore(sortedSetName, keyword, x);
    }

    /**
     * zrevrange命令, 查询Sorted Set中指定范围的值
     * 返回的有序集合中,score大的在前面
     * zrevrange方法无需担心用于指定范围的start和end出现越界报错问题
     *
     * @param start 查询范围开始位置
     * @param end 查询范围结束位置
     * @return
     */
    @Override
    public Set<ZSetOperations.TypedTuple<String>> queryTopSearchHotWord(Integer start, Integer end) {
	String sortedSetName = "searchHotWord";
        Set<ZSetOperations.TypedTuple<String>> resultSet =  redisTemplate.opsForZSet().reverseRangeWithScores(sortedSetName, start, end);
        return resultSet;
    }

    /**
     * 删除指定的key
     *
     * @param keyName keyName
     */
    @Override
    public void deleteKey(String keyName) {
        redisTemplate.delete(keyName);
    }

}

controller 示例代码

@RestController
@RequestMapping("/redis")
public class RedisController {
    @Autowired
    private RedisService redisService;
  
    /**
     * 测试redis记录keyword
     *
     * @param keyword 搜索关键词
     * @return
     */
    @GetMapping("/test_search")
    public ResultVO testSearch(@RequestParam("keyword") String keyword) {
        redisService.searchZincrby(keyword);
	// ResultVO和ResultVOUtil是自定义的class,为了方便展示结果,阅读时忽略即可
        return ResultVOUtil.success(1, "test-return");
    }

    /**
     * 测试redis查询指定范围的热词
     *
     * @param start 查询范围开始位置
     * @param end 查询范围结束位置
     * @return
     */
    @GetMapping("/test_query_top_search_hot_word")
    public ResultVO testQueryTopSearchHotWord(@RequestParam("start") Integer start,
       					      @RequestParam("end") Integer end) {
        Set<ZSetOperations.TypedTuple<String>> resultSet =  redisService.queryTopSearchHotWord(start, end);
	// ResultVO和ResultVOUtil是自定义的class,为了方便展示结果,阅读时忽略即可
        return ResultVOUtil.success(1, "success", resultSet);
    }

    /**
     * 删除指定的key
     *
     * @param keyName keyName
     * @return
     */
    @PostMapping("/delete_key")
    public ResultVO deleteKey(@RequestParam("keyName") String keyName) {
        redisService.deleteKey(keyName);
	// ResultVO和ResultVOUtil是自定义的class,为了方便展示结果,阅读时忽略即可
        return ResultVOUtil.success(1, "test-return");
    }

}

测试代码的运行效果

模拟搜索一些 keyword:

02 模拟搜索一些 hotkey.png

使用 rdm 查看 reids 的存储情况,搜索热词已经存在 redis 一个名为 searchHotWord 的 Sorted Set 中:

01 使用 rdm 查看 reids 的存储情况搜索热词已经存在 redis 一个名为 searchHotWord 的 SortedSet 中.png

查询结果:

03 查询结果.png

One more thing

zrevrange 方法无需担心用于指定范围的 startend 出现越界报错问题。

测试用的 Sorted Set 总共有 8 个数据,故意指定 startend 在此长度范围之外:

04end 无越界问题.png

05start 无越界问题.png

经测试,在保证 startend 均 >=0 的前提下,startend 均无越界报错问题。

  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    286 引用 • 248 回帖 • 61 关注
  • 搜索
    19 引用 • 118 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖
  • 数据统计
    4 引用 • 37 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • aa00666

    请问一下博主:前端是用什么模拟的?

    1 回复
  • JellyfishMIX
    作者

    eolinker,按项目划分工作空间,工作空间内可以写接口文档、全局环境、全局数据结构、进行测试,支持团队协作。挺方便的哈哈(没收广告费)