Springboot+Redis实现热搜排行榜的方案探索

1,061 阅读2分钟

Zzutop 项目中很重要的一块的校园话题的热搜排行,这篇文章记录实现的过程。

1、Redis 的 ZSet

--------Set:

image.png

--------ZSet:

image.png

可以看到ZSet多出一个分数,很明显该分数将用于排序相关。

有序集合的数据结构如图所示:

image.png

  • 分数是一个浮点数,在 Java 中是使用双精度表示的
  • value 也是 String 数据类型,也是一种基于 hash 的存储结构
  • 和Set一样,对于每一个元素都是唯一的,但是对于不同元素而言,它的分数可以一样
  • 集合是通过哈希表实现的,所以添加、删除、查找的复杂度都是 O(1)O(1)
  • 集合中最大的元素数为 23212^{32}-1(40 多亿个)
  • 元素依赖 key 标示它是属于哪个有序集合

2、Redis ZSet 的 spring-data-redis 封装

Spring 对 Redis ZSet 的元素、分数范围以及限制进行了封装。

元素的封装:--------------------

TypedTuple--org.springframework.data.redis.core.ZSetOperations 接口的内部接口:

public interface TypedTuple<V> extends Comparable<ZSetOperations.TypedTuple<V>> {
        @Nullable
        V getValue();

        @Nullable
        Double getScore();

        static <V> ZSetOperations.TypedTuple<V> of(V value, @Nullable Double score) {
            return new DefaultTypedTuple(value, score);
        }
    }

TypedTuple 中 getValue() 是获取值,而 getScore() 是获取分数,但是它只是一个接口,而不是一个实现类。spring-data-redis 提供了一个默认的实现类--DefaultTypedTuple,同样它会实现 TypedTuple 接口,在默认的情况下 Spring 就会把带有分数的有序集合的值和分数封装到这个类中,这样就可以通过这个类对象读取对应的值和分数了。

范围的封装:--------------------

Range--接口org.springframework.data.redis.connection.RedisZSetCommands 下的静态内部类:

    public static class Range {
        @Nullable
        RedisZSetCommands.Range.Boundary min;
        @Nullable
        RedisZSetCommands.Range.Boundary max;

        public Range() {
        }

        public static RedisZSetCommands.Range range() {
            return new RedisZSetCommands.Range();
        }

        public static RedisZSetCommands.Range unbounded() {
            RedisZSetCommands.Range range = new RedisZSetCommands.Range();
            range.min = RedisZSetCommands.Range.Boundary.infinite();
            range.max = RedisZSetCommands.Range.Boundary.infinite();
            return range;
        }

        public RedisZSetCommands.Range gte(Object min) {
            Assert.notNull(min, "Min already set for range.");
            this.min = new RedisZSetCommands.Range.Boundary(min, true);
            return this;
        }

        public RedisZSetCommands.Range gt(Object min) {
            Assert.notNull(min, "Min already set for range.");
            this.min = new RedisZSetCommands.Range.Boundary(min, false);
            return this;
        }

        public RedisZSetCommands.Range lte(Object max) {
            Assert.notNull(max, "Max already set for range.");
            this.max = new RedisZSetCommands.Range.Boundary(max, true);
            return this;
        }

        public RedisZSetCommands.Range lt(Object max) {
            Assert.notNull(max, "Max already set for range.");
            this.max = new RedisZSetCommands.Range.Boundary(max, false);
            return this;
        }

        @Nullable
        public RedisZSetCommands.Range.Boundary getMin() {
            return this.min;
        }

        @Nullable
        public RedisZSetCommands.Range.Boundary getMax() {
            return this.max;
        }

        public static class Boundary {
            @Nullable
            Object value;
            boolean including;

            static RedisZSetCommands.Range.Boundary infinite() {
                return new RedisZSetCommands.Range.Boundary((Object)null, true);
            }

            Boundary(@Nullable Object value, boolean including) {
                this.value = value;
                this.including = including;
            }

            @Nullable
            public Object getValue() {
                return this.value;
            }

            public boolean isIncluding() {
                return this.including;
            }
        }
    }

它有一个静态的 range() 方法,使用它就可以生成一个 Range 对象了,只是要清楚 Range 对象的几个方法即可对范围进行控制。

限制的封装:--------------------

Limit--接口 org.springframework.data.redis.connection.RedisZSetCommands 下的内部类:

它是一个简单的 POJO,它存在两个属性,它们的 getter 和 setter 方法,如下面的代码所示:

// ......
public interface RedisZSetCommands {
    // ......
public class Limit {
    int offset;
    int count;
//setter和getter方法
}
//......
}

offset 代表从第几个开始截取,而 count 代表限制返回的总数量

3、项目中该如何应用?

问题:热搜话题实体中不仅包括分数、还包括一系列属性。ZSet 中的 value 是对应这一系列属性么?

这样做:

  • 后续查找分数时 value 过长
  • 后期会不会导致 value 的重复