import redis.clients.jedis.Jedis;
import redis.clients.jedis.ZParams;
import java.util.*;
/**
* Redis 实战
* Chapter01 - 实现网站投票功能
* 具体功能如下:
* 1)发布文章 - postArticle
* 2)对文章进行投票 - articleVote
* 3)根据score值由大到小,分页获取文章列表 - getArticles
* 4) 将文章加入分组 - addGroups
* 5) 获取当前分组中根据分值由大到小,第几页的文章列表 - getGroupArticles
* 6) 打印文章列表详细信息 - printArticles
*/
public class Chapter01 {
private static final int ONE_WEEK_IN_SECONDS = 7 * 86400;
private static final int VOTE_SCORE = 432;
private static final int ARTICLES_PER_PAGE = 25;
public static final void main(String[] args) {
new Chapter01().run();
}
public void run() {
Jedis conn = new Jedis("localhost");
conn.select(15);
// liuxianzhishou 发布一篇文章,名为 《Redis整理》,链接为“https://blog.nowcoder.net/n/3d525a6454ee45b2ace4eb9e9ebab23a”, 返回结果为文章ID
String articleId = postArticle(
conn, "liuxianzhishou", "Redis整理", "https://blog.nowcoder.net/n/3d525a6454ee45b2ace4eb9e9ebab23a");
System.out.println("We posted a new article with id: " + articleId); // 打印文章ID
System.out.println("Its HASH looks like:");
// 获取文章详细信息
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();
// tun 对 刚刚 liuxianzhishou 发布的文章进行了投票
articleVote(conn, "tun", "article:" + articleId);
String votes = conn.hget("article:" + articleId, "votes"); // 获取文章当前获票数
System.out.println("We voted for the article, it now has votes: " + votes); // 打印该文当前票数
assert Integer.parseInt(votes) > 1; // assert关键字,断言,用于调试程序,TRUE时继续执行,FALSE时抛出异常,并终止执行
// 获取第一页,也就是分值最高的文章列表
System.out.println("The currently highest-scoring articles are:");
List<Map<String,String>> articles = getArticles(conn, 1);
printArticles(articles); // 打印文章列表
assert articles.size() >= 1;
// 对刚刚 liuxianzhishou 发布的文章加入到分组Redis中
addGroups(conn, articleId, new String[]{"Redis"});
System.out.println("We added the article to a new group, other articles include:"); // 打印
// 获取分组Redis中的文章列表
articles = getGroupArticles(conn, "Redis", 1);
printArticles(articles); // 打印
assert articles.size() >= 1;
}
/**
* 发布文章
* @param conn - Redis
* @param user - 发布者
* @param title - 文章标题
* @param link - 文章链接
* @return - 文章编号
*/
public String postArticle(Jedis conn, String user, String title, String link) {
// 给当前文章分配ID
String articleId = String.valueOf(conn.incr("article:"));
String voted = "voted:" + articleId; // 将该篇文章放入可投票set中
conn.sadd(voted, user); // 默认发布者已经对自己的文章进行投票
conn.expire(voted, ONE_WEEK_IN_SECONDS); // 对这篇文章的投票时间设置为一周,因为只会发布一次文章,因此过期时间只设置了一次,并不会出现多次延长过期时间的行为
long now = System.currentTimeMillis() / 1000; // 获取当前时间,以秒记
String article = "article:" + articleId;
// 记录该文详细信息
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("votes", "1"); // 已有投票数1
// 将该文详细信息作为对象,存入Redis中
conn.hmset(article, articleData);
// 将该文的分值存到zset中
conn.zadd("score:", now + VOTE_SCORE, article);
// 将该文的时间信息存入zset中
conn.zadd("time:", now, article);
return articleId;
}
/**
* 对文章进行投票
* @param conn Redis
* @param user 投票人
* @param article 文章
*/
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;
}
String articleId = article.substring(article.indexOf(':') + 1);
if (conn.sadd("voted:" + articleId, user) == 1) {
conn.zincrby("score:", VOTE_SCORE, article);
conn.hincrBy(article, "votes", 1);
}
}
/**
* 根据score值由大到小,分页获取文章列表
* @param conn - Redis
* @param page - 页码
* @return - 文章列表
*/
public List<Map<String,String>> getArticles(Jedis conn, int page) {
return getArticles(conn, page, "score:");
}
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; // 记录startIndex & endIndex
// 获取分值最高的文章列表ID
Set<String> ids = conn.zrevrange(order, start, end);
List<Map<String,String>> articles = new ArrayList<Map<String,String>>();
// 遍历,根据文章列表ID,找到文章详细信息,放入list中
for (String id : ids){
Map<String,String> articleData = conn.hgetAll(id);
articleData.put("id", id);
articles.add(articleData);
}
return articles;
}
/**
* 将文章加入分组
* @param conn - Redis
* @param articleId - 文章ID
* @param toAdd - 分组名称
*/
public void addGroups(Jedis conn, String articleId, String[] toAdd) {
String article = "article:" + articleId;
for (String group : toAdd) {
conn.sadd("group:" + group, article);
}
}
/**
* 获取当前分组中根据分值由大到小,第几页的文章列表
* @param conn - Redis
* @param group - 分组名称
* @param page - 页码
* @return - 文章带详细信息列表
*/
public List<Map<String,String>> getGroupArticles(Jedis conn, String group, int page) {
return getGroupArticles(conn, group, page, "score:");
}
public List<Map<String,String>> getGroupArticles(Jedis conn, String group, int page, String order) {
String key = order + group; // 新的zset
// 为防止查找时间过长,因此结果设置了过期时间,防止查一次找一次,只有过期时间到了,才会进行重新查找,所以可能有更新延时,但问题不大
if (!conn.exists(key)) {
ZParams params = new ZParams().aggregate(ZParams.Aggregate.MAX);
// 将 无序分组set & score文章zset 求交集,交集结果仍为zset,放入 key 中
conn.zinterstore(key, params, "group:" + group, order);
conn.expire(key, 60); // 设置过期时间
}
return getArticles(conn, page, key);
}
/**
* 打印文章列表详细信息
* @param articles - 文章列表
*/
private void printArticles(List<Map<String,String>> articles){
for (Map<String,String> article : articles){
System.out.println(" id: " + article.get("id")); // 打印id
for (Map.Entry<String,String> entry : article.entrySet()){
if (entry.getKey().equals("id")){
continue; // id 已经打印过,直接跳过
}
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
}
}
}