使用Redis构建网站
功能介绍:发布文章,对文章投票,对文章进行分组,具有文章评分功能。
为了能够产生一个随着时间流逝而不断减少的评分,程序需要根据文章发布时间来计算,具体的计算方法为:将文章的得到的支持票数乘以一个常量,然后加上文章的发布时间,得到的结果就是文章的评分。我们使用Unix时间作为发布时间来计算文章的评分。
当然,以下功能有一些问题,比如没有事务,缓存失效等,但是请暂时忽略这些问题。
定义常量
//为了尽量节约内存,设置投票截至时间,一周之后不允许投票
static int ONE_WEEK_IN_SECONDS = 7 * 86300;
//设置票数的权重常量
static double VOTE_SCORE = 432;
//设置每一页的文章数量
static int ARTICLES_PER_PAGE = 25;
//获取Redis连接
static Jedis jedis = new Jedis("111.111.111.111", 6379);
这里的权重常量是432,是十分巧妙地,假如每一篇文章有200票的评分,我们认为它是好的文章,就将它排在前面一天,一天是86400秒,那么86400/200=432,而如果这篇文章的票数是200票,那么下一天的文章就默认具有了86400的评分,所以自然会将昨天的200票的文章从前面挤下去,相当于文章每有200票的评分,就可以在热榜上多停留一天,并且会自动从热榜下去。
实现发布文章功能
我们获取到作者,文章标题,文章链接,将文章的信息作为hash存储,
设置作者为默认支持文章,将作者id加入到当前文章的支持者集合中
设置支持者集合的过期时间,一周后不允许投票,节省内存
将文章的评分计算出并加入到所有文章评分的集合中
返回文章id
/**
* 文章发布
*
* @param user 用户id;如 1
* @param title 文章标题;如 使用Redis实现文章投票网站
* @param link 文章链接
* @return 文章id
*/
public static String post_article(String user, String title, String link) {
//生成文章id,文章统一使用article:为前缀,当然你也可以使用article. 又或 article/ ,不过建议使用article: 因为官方也是使用:
String article_id = String.valueOf(jedis.incr("article:"));
//生成文章的投票集合
String voted = "voted:" + article_id;
//将作者添入其中,默认作者是支持状态
jedis.sadd(voted, user);
//设置过期时间,超过一周之后变不允许投票
jedis.expire(voted, ONE_WEEK_IN_SECONDS);
//获取当前的unix时间
Long now = System.currentTimeMillis();
HashMap<String, String> hashMap = new HashMap<>();
//设置文字标题
hashMap.put("title", title);
//文章链接
hashMap.put("link", link);
//发布作者
hashMap.put("poster", user);
//发布Unix时间
hashMap.put("time", String.valueOf(now));
//初始票数,作者的支持票
hashMap.put("votes", "1");
//文章的hash存储
String article = "article:" + article_id;
jedis.hmset(article, hashMap);
hashMap.clear();
HashMap<String, Double> hashMap1 = new HashMap<String, Double>();
hashMap1.put(article, now + VOTE_SCORE);
//文章的评分存储,zset类型的score:键存储所有的文章评分
jedis.zadd("score:", hashMap1);
hashMap1.clear();
hashMap1.put(article, Double.valueOf(now));
//文章的发布时间存储,zset类型的time:键存储所有的文章评分
jedis.zadd("time:", hashMap1);
//返回文章id,如 article:1
return article_id;
}
实现文章投票
接收当前用户的id,文章id,支持或者反对
获取当前时间,通过文章的hash获取文章的发布时间,判断是否超过一周
没有超过一周,
判断用户是否已经投过票了,进行相应的处理,支持或者反对,修改score:有序集合中文章评分,修改文章hash的票数
修改文章的支持者或反对者集合中的用户id
/**
* 对文章进行投票
* @param user 投票用户id
* @param article 文章id,如 article:1
* @param st 投票,传入1,支持,-1反对
*/
public static void article_vote(String user, String article, int st) {
LocalDateTime time = LocalDateTime.now();
//获取当前的Unix时间
int cutoff = time.getSecond() - ONE_WEEK_IN_SECONDS;
//如果发布超过一周,结束投票
if (jedis.zscore("time:", article) < cutoff) {
return;
}
//分割字符串获取文章真实id,如1
String article_id = article.split(":")[1];
//判断支持,反对
if (st < 0) {
//反对
//"voted:" + article_id ,如voted:1,表示支持article:1的用户id的集合
//"novoted:" + article_id ,如novoted:1,表示反对article:1的用户id的集合
update("voted:" + article_id, "novoted:" + article_id, user, article, st);
} else {
//支持
update("novoted:" + article_id, "voted:" + article_id, user, article, st);
}
}
/**
* 更新支持或反对集合,更新文章评分
* @param src 源集合
* @param dest 目标集合
* @param member set元素名称
* @param article 文章id,如article:1
* @param st 票数,1或-1
*/
public static void update(String src, String dest, String member, String article, int st) {
//将member可从src移动到dest,移动成功,那么返回1.说明该用户之前已经投票,而这次投相反的票,直接else。
if (jedis.smove(src, dest, member) == 0) {
//这个用户之前没有投过票或投了与之前相同的票
////添加成功,返回1,说明没有投过票,添加失败返回0,说明与之前的票相同,不需要修改数据
if(jedis.sadd(dest, member)==1){
//修改评分
jedis.zincrby("score:", VOTE_SCORE * st, article);
//修改如article:1中的票数
jedis.hincrBy(article,"votes",st);
}
}else {
//修改评分
jedis.zincrby("score:", VOTE_SCORE *2*st, article);
//修改如article:1中的票数
jedis.hincrBy(article,"votes",2*st);
}
}
实现按评分取出文章
获取页面与包括文字id与评分有序集合,
处理出当前页的起始序号与结束序号
获得当前页文章的有序集合
创建map数组存储结果
遍历有序集合并获取每一篇文章的hash数据,存储到map中,并将当前文章的id加入到map中
遍历时将map加入数组中,
返回数据
/**
* 返回order中所有文章的数组,评分从大到小,并分页
* @param page 页码
* @param order 需要处理的有序集合,包括文字id与评分
*/
public static ArrayList<Map<String, String>> get_articles(int page, String order) {
//起始文章序号
int start = (page - 1) * ARTICLES_PER_PAGE;
//结束文章序号
int end = start + ARTICLES_PER_PAGE - 1;
//zrevrange,按score从大到小返回结果,左闭右开
//这里你可能有些疑问,为什么无序的set集合可以存储有序的zset返回的值呢,下面我再解释
Set<String> ids = jedis.zrevrange(order, start, end);
//存储结果集
ArrayList<Map<String, String>> articles = new ArrayList<>();
for (String id : ids) {
//取出文章的hash信息
Map<String, String> article_data = jedis.hgetAll(id);
//加入文章的id,如articles:1,因为我们在之前发布文章时并没有存储id,而是使用id作为了hash结果的键
article_data.put("id", id);
//加入到ArrayList
articles.add(article_data);
}
//返回结果集
return articles;
}
无序的set可以存储有序的zset返回的值
我们可以看一下源码
protected static class SetFromList<E> extends AbstractSet<E> implements Serializable {
private static final long serialVersionUID = -2850347066962734052L;
private final transient List<E> list;
private SetFromList(List<E> list) {
if (list == null) {
throw new NullPointerException("list");
}
this.list = list;
}
虽然SetFromList继承AbstractSet抽象类,但是真正存储数据的是List,而List是有序的。
因此这里只是一个套壳的Set,至于为什么这么做,我不知道。
对文章进行分组
创建分组的set,将文章id加入或移出相应的分组即可
/**
* 对文章进行添加关键字或移除关键字
*
* @param article_id 文章id,如1
* @param to_add 需要添加的关键字
* @param to_remove 需要删除的关键字
*/
public static void add_remove_groups(String article_id, String[] to_add, String[] to_remove) {
//拼接处文章的hash键
String articl = "article:" + article_id;
//将articl加入相关分组
for (String s : to_add) {
jedis.sadd("group:" + s, articl);
}
//将articl移出不相关分组
for (String s : to_add) {
jedis.srem("group:" + s, articl);
}
}
实现分组文章按评分取出
为了能够对群组文章进行分页和排序,网站需要将同一个分组里面的所有文章按照评分存储到一个有序集合中,ZINTERSTORE命令可以接收多个集合或多个有序集合作为输入,找出所有同时存在与集合和有序集合地成员,并以几种不同地方式合这些成员的分值,所有集合成员的分值都是1,请注意,不包含有序集合。
因为如果群组中的文章非常多,那么执行该命令会花费较长时间,因此我们将计算结果缓存60秒
因为存在缓存,所以先查看是否存在缓存,如果不存在,将scor:有序集合与分组集合进行ZINTERSTORE处理,设置过期时间,将得到的结果调用get_articles函数进行分页返回即可。
/**
* 返回群组文章按评分排序后的结果
* @param group 组别
* @param page 页码
* @param order 默认 "score:" 因为这里面存储有所有文章的评分
* @return
*/
public ArrayList<Map<String, String>> get_group_articles(String group, int page, String order) {
//拼接存储结果的有序集合的键
String key = order + group;
//如果不存在该元素
if (!jedis.exists(key)) {
//结果分数取相同元素的最大值,其他类型还有MIN,SUM
ZParams aggregate = new ZParams().aggregate(ZParams.Aggregate.MAX);
//处理集合获取结果,
jedis.zinterstore(key, aggregate, "group" + group, order);
//设置超时
jedis.expire(key,60);
}
//调用get_articles进行key有序集合中元素的分页。
return get_articles(page, key);
}