文章投票網站的redis相關Java實現
需求:
1、要構建一個文章投票網站,文章需要在一天內至少獲得200張票,才能優先顯示在當天文章列表前列。
2、但是為了避免釋出時間較久的文章由於累計的票數較多而一直停留在文章列表前列,我們需要有隨著時間流逝而不斷減分的評分機制。
3、於是具體的評分計算方法為:將文章得到的支援票數乘以一個常量432(由一天的秒數86400除以文章展示一天所需的支援票200得出),然後加上文章的釋出時間,得出的結果就是文章的評分。
Redis設計
(1)對於網站裡的每篇文章,需要使用一個雜湊來儲存文章的標題、指向文章的網址、釋出文章的使用者、文章的釋出時間、文章得到的投票數量等資訊。
為了方便網站根據文章釋出的先後順序和文章的評分高低來展示文章,我們需要兩個有序集合來儲存文章: (2)有序集合,成員為文章ID,分值為文章的釋出時間。
(3)有序集合,成員為文章ID,分值為文章的評分。
(4)為了防止使用者對同一篇文章進行多次投票,需要為每篇文章記錄一個已投票使用者名稱單。使用集合來儲存已投票的使用者ID。由於集合是不能儲存多個相同的元素的,所以不會出現同個使用者對同一篇文章多次投票的情況。
(5)文章支援群組功能,可以讓使用者只看見與特定話題相關的文章,比如“python”有關或者介紹“redis”的文章等,這時,我們需要一個集合來記錄群組文章。例如 programming群組
為了節約記憶體,當一篇文章釋出期滿一週之後,使用者將不能對它進行投票,文章的評分將被固定下來,而記錄文章已投票使用者名稱單的集合也會被刪除。
程式碼設計
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;
/**
* 釋出並獲取文章
*1、釋出一篇新文章需要建立一個新的文章id,可以通過對一個計數器(count)執行INCY命令來完成。
*2、程式需要使用SADD將文章釋出者的ID新增到記錄文章已投票使用者名稱單的集合中,
* 並使用EXPIRE命令為這個集合設定一個過期時間,讓Redis在文章釋出期滿一週之後自動刪除這個集合。
*3、之後程式會使用HMSET命令來儲存文章的相關資訊,並執行兩個ZADD,將文章的初始評分和釋出時間分別新增到兩個相應的有序集合裡面。
*/
public String postArticle(Jedis conn, String user, String title, String link) {
//1、生成一個新的文章ID
String articleId = String.valueOf(conn.incr("article:")); //String.valueOf(int i) : 將 int 變數 i 轉換成字串
String voted = "voted:" + articleId;
//2、新增到記錄文章已投使用者名稱單中,
conn.sadd(voted, user);
//3、設定一週為過期時間
conn.expire(voted, ONE_WEEK_IN_SECONDS);
long now = System.currentTimeMillis() / 1000;
String article = "article:" + articleId;
//4、建立一個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");
//5、將文章資訊儲存到一個雜湊裡面。
//HMSET key field value [field value ...]
//同時將多個 field-value (域-值)對設定到雜湊表 key 中。
//此命令會覆蓋雜湊表中已存在的域。
conn.hmset(article, articleData);
//6、將文章新增到更具評分排序的有序集合中
//ZADD key score member [[score member] [score member] ...]
//將一個或多個 member 元素及其 score 值加入到有序集 key 當中
conn.zadd("score:", now + VOTE_SCORE, article);
//7、將文章新增到更具釋出時間排序的有序集合。
conn.zadd("time:", now, article);
return articleId;
}
}
複製程式碼
2.當使用者嘗試對一篇文章進行投票時,
(1)用ZSCORE命令檢查記錄文章釋出時間的有序集合(redis設計2),判斷文章的釋出時間是否未超過一週。
(2)如果文章仍然處於可以投票的時間範疇,那麼用SADD將使用者新增到記錄文章已投票使用者名稱單的集合(redis設計4)中。
(3)如果上一步操作成功,那麼說明使用者是第一次對這篇文章進行投票,那麼使用ZINCRBY命令為文章的評分增加432(ZINCRBY命令用於對有序集合成員的分值執行自增操作);
並使用HINCRBY命令對雜湊記錄的文章投票數量進行更新
/**
* 對文章進行投票
* 實現投票系統的步驟:
* 1、當使用者嘗試對一篇文章進行投票時,程式要使用ZSCORE命令檢查記錄文字釋出時間的有序集合,判斷文章的釋出時間是否超過一週。
* 2、如果文章仍然處於可投票的時間範圍之內,那麼程式將使用SADD命令,嘗試將使用者新增到記錄文章的已投票使用者名稱單的集合中。
* 3、如果投票執行成功的話,那麼說明使用者是第一次對這篇文章進行投票,程式將使用ZINCYBY命令為文章的評分增加432(ZINCYBY命令用於對有序集合成員的分值進行自增操作),
* 並使用HINCRBY命令對雜湊記錄的文章投票數量進行更新(HINCRBY命令用於對雜湊儲存的值執行自增操作)
*/
public void articleVote(Jedis conn, String user, String article) {
//1、計算文章投票截止時間。
long cutoff = (System.currentTimeMillis() / 1000) - ONE_WEEK_IN_SECONDS;
//2、檢查是否還可以對文章進行投票,(雖然使用雜湊也可以獲取文章的釋出時間,但有序集合返回文章釋出時間為浮點數,可以不進行轉換,直接使用)
if (conn.zscore("time:", article) < cutoff){
return;
}
//3、從articleId識別符號裡面取出文章ID。
//nt indexOf(int ch,int fromIndex)函式:就是字元ch在字串fromindex位後出現的第一個位置.沒有找到返加-1
//String.Substring (Int32) 從此例項檢索子字串。子字串從指定的字元位置開始。
String articleId = article.substring(article.indexOf(':') + 1);
//4、檢查使用者是否第一次為這篇文章投票,如果是第一次,則在增加這篇文章的投票數量和評分。
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:");
}
/**
* 取出評分最高的文章和取出最新發布的文章
* 實現步驟:
* 1、程式需要先使用ZREVRANGE取出多個文章ID,然後在對每個文章ID執行一次HGETALL命令來取出文章的詳細資訊,
* 這個方法可以用於取出評分最高的文章,又可以用於取出最新發布的文章。
* 需要注意的是:
* 因為有序集合會根據成員的值從小到大排列元素,所以使用ZREVRANGE命令,已分值從大到小的排列順序取出文章ID才是正確的做法
*
*/
public List<Map<String,String>> getArticles(Jedis conn, int page, String order) {
//1、設定獲取文章的起始索引和結束索引。
int start = (page - 1) * ARTICLES_PER_PAGE;
int end = start + ARTICLES_PER_PAGE - 1;
//2、獲取多個文章ID,
Set<String> ids = conn.zrevrange(order, start, end);
List<Map<String,String>> articles = new ArrayList<Map<String,String>>();
for (String id : ids){
//3、根據文章ID獲取文章的詳細資訊
Map<String,String> articleData = conn.hgetAll(id);
articleData.put("id", id);
//4、新增到ArrayList容器中
articles.add(articleData);
}
return articles;
}
複製程式碼
4. 對文章進行分組,使用者可以只看自己感興趣的相關主題的文章。
群組功能主要有兩個部分:一是負責記錄文章屬於哪個群組,二是負責取出群組中的文章。
為了記錄各個群組都儲存了哪些文章,需要為每個群組建立一個集合,並將所有同屬一個群組的文章ID都記錄到那個集合中。
/**
* 記錄文章屬於哪個群組
* 將所屬一個群組的文章ID記錄到那個集合中
* Redis不僅可以對多個集合執行操作,甚至在一些情況下,還可以在集合和有序集合之間執行操作
*/
public void addGroups(Jedis conn, String articleId, String[] toAdd) {
//1、構建儲存文章資訊的鍵名
String article = "article:" + articleId;
for (String group : toAdd) {
//2、將文章新增到它所屬的群組裡面
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) {
//1、為每個群組的每種排列順序都建立一個鍵。
String key = order + group;
//2、檢查是否有已快取的排序結果,如果沒有則進行排序。
if (!conn.exists(key)) {
//3、根據評分或者釋出時間對群組文章進行排序
ZParams params = new ZParams().aggregate(ZParams.Aggregate.MAX);
conn.zinterstore(key, params, "group:" + group, order);
//讓Redis在60秒之後自動刪除這個有序集合
conn.expire(key, 60);
}
//4、呼叫之前定義的getArticles()來進行分頁並獲取文章資料
return getArticles(conn, page, key);
}
複製程式碼
以上就是一個文章投票網站的相關redis實現。
測試程式碼如下:
public static final void main(String[] args) {
new Chapter01().run();
}
public void run() {
//1、初始化redis連線
Jedis conn = new Jedis("localhost");
conn.select(15);
//2、釋出文章
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();
//2、測試文章的投票過程
articleVote(conn, "other_user", "article:" + articleId);
String votes = conn.hget("article:" + articleId, "votes");
System.out.println("我們為該文章投票,目前該文章的票數 " + votes);
assert Integer.parseInt(votes) > 1;
//3、測試文章的投票過程
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;
}
複製程式碼
參考
Redis實戰相關程式碼,目前有Java,JS,node,Python
程式碼地址
說明
如果你有耐心讀到這裡,請允許我說明下:
-
1、本文主題結構參考了文章投票網站的redis相關實現(python)
-
2、留下重複的註釋是為了自己對比,努力讓自己變得不一樣
-
3、通過一天的分析、學習。越覺得需要學的東西太多了。而不只是簡單的記住幾個命令
-
4、感謝所有人,感謝SegmentFault,讓你見證我脫變的過程吧。