使用Redis构建文章投票网站(Java)

文章投票网站的redis相关Java实现


需求:

一、要构建一个文章投票网站,文章须要在一天内至少得到200张票,才能优先显示在当天文章列表前列。html

二、可是为了不发布时间较久的文章因为累计的票数较多而一直停留在文章列表前列,咱们须要有随着时间流逝而不断减分的评分机制。java

三、因而具体的评分计算方法为:将文章获得的支持票数乘以一个常量432(由一天的秒数86400除以文章展现一天所需的支持票200得出),而后加上文章的发布时间,得出的结果就是文章的评分。node

Redis设计

(1)对于网站里的每篇文章,须要使用一个散列来存储文章的标题、指向文章的网址、发布文章的用户、文章的发布时间、文章获得的投票数量等信息。python

记录文章内容散列

为了方便网站根据文章发布的前后顺序和文章的评分高低来展现文章,咱们须要两个有序集合来存储文章:
(2)有序集合,成员为文章ID,分值为文章的发布时间。git

文章发布时间有序集合

(3)有序集合,成员为文章ID,分值为文章的评分。github

文章评分有序集合

(4)为了防止用户对同一篇文章进行屡次投票,须要为每篇文章记录一个已投票用户名单。使用集合来存储已投票的用户ID。因为集合是不能存储多个相同的元素的,因此不会出现同个用户对同一篇文章屡次投票的状况。redis

用户投票集合

(5)文章支持群组功能,可让用户只看见与特定话题相关的文章,好比“python”有关或者介绍“redis”的文章等,这时,咱们须要一个集合来记录群组文章。例如 programming群组segmentfault

群组文章集合

为了节约内存,当一篇文章发布期满一周以后,用户将不能对它进行投票,文章的评分将被固定下来,而记录文章已投票用户名单的集合也会被删除。缓存

代码设计

1.当用户要发布文章时,函数

(1)经过一个计数器counter执行INCR命令来建立一个新的文章ID。

(2)使用SADD将文章发布者ID添加到记录文章已投票用户名单的集合中,并用EXPIRE命令为这个集合设置一个过时时间,让Redis在文章发布期满一周后自动删除这个集合。

(3)使用HMSET命令来存储文章的相关信息,并执行两ZADD命令,将文章的初始评分和发布时间分别添加到两个相应的有序集合中。

public class Chapter01 {
  private static final int ONE_WEEK_IN_SECONDS = 7 * 86400;          //文章发布期满一周后,用户不能在对它投票。
  private static final int VOTE_SCORE = 432;                        //计算评分时间与支持票数相乘的常量,经过将一天的秒数除(86400)以文章展现一天所需的支持票数(200)得出的
  private static final int ARTICLES_PER_PAGE = 25;

  /**
 * 发布并获取文章
 *一、发布一篇新文章须要建立一个新的文章id,能够经过对一个计数器(count)执行INCY命令来完成。
 *二、程序须要使用SADD将文章发布者的ID添加到记录文章已投票用户名单的集合中,
 *   并使用EXPIRE命令为这个集合设置一个过时时间,让Redis在文章发布期满一周以后自动删除这个集合。
 *三、以后程序会使用HMSET命令来存储文章的相关信息,并执行两个ZADD,将文章的初始评分和发布时间分别添加到两个相应的有序集合里面。
 */
public String postArticle(Jedis conn, String user, String title, String link) {
    //一、生成一个新的文章ID
    String articleId = String.valueOf(conn.incr("article:"));     //String.valueOf(int i) : 将 int 变量 i 转换成字符串

    String voted = "voted:" + articleId;
    //二、添加到记录文章已投用户名单中,
    conn.sadd(voted, user);
    //三、设置一周为过时时间
    conn.expire(voted, ONE_WEEK_IN_SECONDS);

    long now = System.currentTimeMillis() / 1000;
    String article = "article:" + articleId;
    //四、建立一个HashMap容器。
    HashMap<String,String> articleData = new HashMap<String,String>();
    articleData.put("title", title);
    articleData.put("link", link);
    articleData.put("user", user);
    articleData.put("now", String.valueOf(now));
    articleData.put("oppose", "0");
    articleData.put("votes", "1");
    //五、将文章信息存储到一个散列里面。
    //HMSET key field value [field value ...]
    //同时将多个 field-value (域-值)对设置到哈希表 key 中。
    //此命令会覆盖哈希表中已存在的域。
    conn.hmset(article, articleData);
    //六、将文章添加到更具评分排序的有序集合中
    //ZADD key score member [[score member] [score member] ...]
    //将一个或多个 member 元素及其 score 值加入到有序集 key 当中
    conn.zadd("score:", now + VOTE_SCORE, article);
    //七、将文章添加到更具发布时间排序的有序集合。
    conn.zadd("time:", now, article);

    return articleId;
  }
}

2.当用户尝试对一篇文章进行投票时,

(1)用ZSCORE命令检查记录文章发布时间的有序集合(redis设计2),判断文章的发布时间是否未超过一周。

(2)若是文章仍然处于能够投票的时间范畴,那么用SADD将用户添加到记录文章已投票用户名单的集合(redis设计4)中。

(3)若是上一步操做成功,那么说明用户是第一次对这篇文章进行投票,那么使用ZINCRBY命令为文章的评分增长432(ZINCRBY命令用于对有序集合成员的分值执行自增操做);

并使用HINCRBY命令对散列记录的文章投票数量进行更新

/**
 * 对文章进行投票
 * 实现投票系统的步骤:
 * 一、当用户尝试对一篇文章进行投票时,程序要使用ZSCORE命令检查记录文字发布时间的有序集合,判断文章的发布时间是否超过一周。
 * 二、若是文章仍然处于可投票的时间范围以内,那么程序将使用SADD命令,尝试将用户添加到记录文章的已投票用户名单的集合中。
 * 三、若是投票执行成功的话,那么说明用户是第一次对这篇文章进行投票,程序将使用ZINCYBY命令为文章的评分增长432(ZINCYBY命令用于对有序集合成员的分值进行自增操做),
 *    并使用HINCRBY命令对散列记录的文章投票数量进行更新(HINCRBY命令用于对散列存储的值执行自增操做)
 */
public void articleVote(Jedis conn, String user, String article) {
    //一、计算文章投票截止时间。
    long cutoff = (System.currentTimeMillis() / 1000) - ONE_WEEK_IN_SECONDS;
    //二、检查是否还能够对文章进行投票,(虽然使用散列也能够获取文章的发布时间,但有序集合返回文章发布时间为浮点数,能够不进行转换,直接使用)
    if (conn.zscore("time:", article) < cutoff){
        return;
    }

    //三、从articleId标识符里面取出文章ID。
    //nt indexOf(int ch,int fromIndex)函数:就是字符ch在字串fromindex位后出现的第一个位置.没有找到返加-1
    //String.Substring (Int32)    今后实例检索子字符串。子字符串从指定的字符位置开始。
    String articleId = article.substring(article.indexOf(':') + 1);
    //四、检查用户是否第一次为这篇文章投票,若是是第一次,则在增长这篇文章的投票数量和评分。
    if (conn.sadd("voted:" + articleId, user) == 1) {                       //将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。
        //为有序集 key 的成员 member 的 score 值加上增量 increment 。
        //能够经过传递一个负数值 increment ,让 score 减去相应的值,
        //当 key 不存在,或 member 不是 key 的成员时, ZINCRBY key increment member 等同于 ZADD key increment member 。
        //ZINCRBY salary 2000 tom   # tom 加薪啦!
        conn.zincrby("score:", VOTE_SCORE, article);

        //为哈希表 key 中的域 field 的值加上增量 increment 。
        //增量也能够为负数,至关于对给定域进行减法操做。
        //HINCRBY counter page_view 200
        conn.hincrBy(article, "votes", 1L);
    }
}

/**
 * 投反对票
 */
public void articleOppose(Jedis conn, String user, String article) {

    long cutoff = (System.currentTimeMillis() / 1000) - ONE_WEEK_IN_SECONDS;

    //cutoff以前的发布的文章 就不能再投票了
    if (conn.zscore("time:", article) < cutoff){
        return;
    }


    String articleId = article.substring(article.indexOf(':') + 1);

    //查看user是否给这篇文章投过票
    //set里面的key是惟一的 若是 sadd返回0 表示set里已经有数据了
    //若是返回1表示尚未这个数据
    if (conn.sadd("oppose:" + articleId, user) == 1) {
        conn.zincrby("score:", -VOTE_SCORE, article);
        conn.hincrBy(article, "votes", -1L);
    }
}

3.咱们已经实现了文章投票功能和文章发布功能,接下来就要考虑如何取出评分最高的文章以及如何取出最新发布的文章

(1)咱们须要使用ZREVRANGE命令取出多个文章ID。(因为有序集合会根据成员的分值从小到大地排列元素,使用ZREVRANGE以分值从大到小的排序取出文章ID)

(2)对每一个文章ID执行一次HGETALL命令来取出文章的详细信息。

这个方法既能够用于取出评分最高的文章,又能够用于取出最新发布的文章。

public List<Map<String,String>> getArticles(Jedis conn, int page) {
    //调用下面重载的方法
    return getArticles(conn, page, "score:");
}
/**
 * 取出评分最高的文章和取出最新发布的文章
 * 实现步骤:
 * 一、程序须要先使用ZREVRANGE取出多个文章ID,而后在对每一个文章ID执行一次HGETALL命令来取出文章的详细信息,
 *    这个方法能够用于取出评分最高的文章,又能够用于取出最新发布的文章。
 * 须要注意的是:
 * 由于有序集合会根据成员的值从小到大排列元素,因此使用ZREVRANGE命令,已分值从大到小的排列顺序取出文章ID才是正确的作法
 *
 */

public List<Map<String,String>> getArticles(Jedis conn, int page, String order) {
    //一、设置获取文章的起始索引和结束索引。
    int start = (page - 1) * ARTICLES_PER_PAGE;
    int end = start + ARTICLES_PER_PAGE - 1;
     //二、获取多个文章ID,
    Set<String> ids = conn.zrevrange(order, start, end);
    List<Map<String,String>> articles = new ArrayList<Map<String,String>>();
    for (String id : ids){
        //三、根据文章ID获取文章的详细信息
        Map<String,String> articleData = conn.hgetAll(id);
        articleData.put("id", id);
        //四、添加到ArrayList容器中
        articles.add(articleData);
    }

    return articles;
}

4. 对文章进行分组,用户能够只看本身感兴趣的相关主题的文章。

群组功能主要有两个部分:一是负责记录文章属于哪一个群组,二是负责取出群组中的文章。

为了记录各个群组都保存了哪些文章,须要为每一个群组建立一个集合,并将全部同属一个群组的文章ID都记录到那个集合中。

/**
 * 记录文章属于哪一个群组
 * 将所属一个群组的文章ID记录到那个集合中
 * Redis不只能够对多个集合执行操做,甚至在一些状况下,还能够在集合和有序集合之间执行操做
 */
public void addGroups(Jedis conn, String articleId, String[] toAdd) {
    //一、构建存储文章信息的键名
    String article = "article:" + articleId;
    for (String group : toAdd) {
        //二、将文章添加到它所属的群组里面
        conn.sadd("group:" + group, article);
    }
}

因为咱们还须要根据评分或者发布时间对群组文章进行排序和分页,因此须要将同一个群组中的全部文章按照评分或者发布时间有序地存储到一个有序集合中。
但咱们已经有全部文章根据评分和发布时间的有序集合,咱们不须要再从新保存每一个群组中相关有序集合,咱们能够经过取出群组文章集合与相关有序集合的交集,就能够获得各个群组文章的评分和发布时间的有序集合。

Redis的ZINTERSTORE命令能够接受多个集合和多个有序集合做为输入,找出全部同时存在于集合和有序集合的成员,并以几种不一样的方式来合并这些成员的分值(全部集合成员的分支都会视为1)。

对于文章投票网站来讲,可使用ZINTERSTORE命令选出相同成员中最大的那个分值来做为交集成员的分值:取决于所使用的排序选项,这些分值既能够是文章的评分,也能够是文章的发布时间。

以下的示例图,显示了执行ZINTERSTORE命令的过程:

群组文章的有序集合过程

对集合groups:programming和有序集合score:进行交集计算得出了新的有序集合score:programming,它包含了全部同时存在于集合groups:programming和有序集合score:的成员。由于集合groups:programming的全部成员分值都被视为1,而有序集合score:的全部成员分值都大于1,此次交集计算挑选出来的分值为相同成员中的最大分值,因此有序集合score:programming的成员分值其实是由有序集合score:的成员的分值来决定的。

因此,咱们的操做以下:

(1)经过群组文章集合和评分的有序集合或发布时间的有序集合执行ZINTERSTORE命令,而获得相关的群组文章有序集合。

(2)若是群组文章不少,那么执行ZINTERSTORE须要花费较多的时间,为了尽可能减小redis的工做量,咱们将查询出的有序集合进行缓存处理,尽可能减小ZINTERSTORE命令的执行次数。

为了保持持续更新后咱们能获取到最新的群组文章有序集合,咱们只将结果缓存60秒。

(3)使用上一步的getArticles函数来分页并获取群组文章。

public List<Map<String,String>> getGroupArticles(Jedis conn, String group, int page) {
    //调用下面重载的方法
    return getGroupArticles(conn, group, page, "score:");
}

/**
 * 取出群组里面的文章
 * 为了可以根据评分对群组文章进行排序和分页,网站须要将同一个群组里面的全部文章都按评分有序的存储到一个有序集合内,
 * 程序须要使用ZINTERSTORE命令选出相同成员中最大的那个分支做为交集成员的值:取决于所使用的排序选项,能够是评分或文章发布时间。
 */
public List<Map<String,String>> getGroupArticles(Jedis conn, String group, int page, String order) {
     //一、为每一个群组的每种排列顺序都建立一个键。
    String key = order + group;
    //二、检查是否有已缓存的排序结果,若是没有则进行排序。
    if (!conn.exists(key)) {
        //三、根据评分或者发布时间对群组文章进行排序
        ZParams params = new ZParams().aggregate(ZParams.Aggregate.MAX);
        conn.zinterstore(key, params, "group:" + group, order);
        //让Redis在60秒以后自动删除这个有序集合
        conn.expire(key, 60);
    }
    //四、调用以前定义的getArticles()来进行分页并获取文章数据
    return getArticles(conn, page, key);
}

以上就是一个文章投票网站的相关redis实现。

测试代码以下:

public static final void main(String[] args) {
    new Chapter01().run();
}

public void run() {
    //一、初始化redis链接
    Jedis conn = new Jedis("localhost");
    conn.select(15);

    //二、发布文章
    String articleId = postArticle(
        conn, "guoxiaoxu", "A title", "http://www.google.com");
    System.out.println("我发布了一篇文章,id为:: " + articleId);
    System.out.println("文章保存的散列格式以下:");
    Map<String,String> articleData = conn.hgetAll("article:" + articleId);
    for (Map.Entry<String,String> entry : articleData.entrySet()){
        System.out.println("  " + entry.getKey() + ": " + entry.getValue());
    }

    System.out.println();
    //二、测试文章的投票过程
    articleVote(conn, "other_user", "article:" + articleId);
    String votes = conn.hget("article:" + articleId, "votes");
    System.out.println("咱们为该文章投票,目前该文章的票数 " + votes);
    assert Integer.parseInt(votes) > 1;
    //三、测试文章的投票过程
    articleOppose(conn, "other_user", "article:" + articleId);
    String oppose = conn.hget("article:" + articleId, "votes");
    System.out.println("咱们为该文章投反对票,目前该文章的反对票数 " + oppose);
    assert Integer.parseInt(oppose) > 1;

    System.out.println("当前得分最高的文章是:");
    List<Map<String,String>> articles = getArticles(conn, 1);
    printArticles(articles);
    assert articles.size() >= 1;

    addGroups(conn, articleId, new String[]{"new-group"});
    System.out.println("咱们将文章推到新的群组,其余文章包括:");
    articles = getGroupArticles(conn, "new-group", 1);
    printArticles(articles);
    assert articles.size() >= 1;
}

参考

1.文章投票网站的redis相关实现(python)

Redis实战相关代码,目前有Java,JS,node,Python

2.Redis 命令参考

代码地址

https://github.com/guoxiaoxu/...

说明

若是你有耐心读到这里,请容许我说明下:

  • 一、本文主题结构参考了文章投票网站的redis相关实现(python)
  • 二、留下重复的注释是为了本身对比,努力让本身变得不同
  • 三、经过一天的分析、学习。越以为须要学的东西太多了。而不仅是简单的记住几个命令
  • 四、感谢全部人,感谢SegmentFault,让你见证我脱变的过程吧。
相关文章
相关标签/搜索