一、要構建一個文章投票網站,文章須要在一天內至少得到200張票,才能優先顯示在當天文章列表前列。java
二、可是爲了不發佈時間較久的文章因爲累計的票數較多而一直停留在文章列表前列,咱們須要有隨着時間流逝而不斷減分的評分機制。node
三、因而具體的評分計算方法爲:將文章獲得的支持票數乘以一個常量432(由一天的秒數86400除以文章展現一天所需的支持票200得出),而後加上文章的發佈時間,得出的結果就是文章的評分。python
(1)對於網站裏的每篇文章,須要使用一個散列來存儲文章的標題、指向文章的網址、發佈文章的用戶、文章的發佈時間、文章獲得的投票數量等信息。git
爲了方便網站根據文章發佈的前後順序和文章的評分高低來展現文章,咱們須要兩個有序集合來存儲文章: (2)有序集合,成員爲文章ID,分值爲文章的發佈時間。github
(3)有序集合,成員爲文章ID,分值爲文章的評分。redis
(4)爲了防止用戶對同一篇文章進行屢次投票,須要爲每篇文章記錄一個已投票用戶名單。使用集合來存儲已投票的用戶ID。因爲集合是不能存儲多個相同的元素的,因此不會出現同個用戶對同一篇文章屢次投票的狀況。segmentfault
(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;
/** * 發佈並獲取文章 *一、發佈一篇新文章須要建立一個新的文章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;
}
複製代碼
Redis實戰相關代碼,目前有Java,JS,node,Python
若是你有耐心讀到這裏,請容許我說明下:
一、本文主題結構參考了文章投票網站的redis相關實現(python)
二、留下重複的註釋是爲了本身對比,努力讓本身變得不同
三、經過一天的分析、學習。越以爲須要學的東西太多了。而不僅是簡單的記住幾個命令
四、感謝全部人,感謝SegmentFault,讓你見證我脫變的過程吧。