購物網站的redis相關實現
本文主要內容:
- 1、登入cookie
- 2、購物車cookie
- 3、快取資料庫行
- 4、測試
必備知識點
WEB應用就是通過HTTP協議對網頁瀏覽器發出的請求進行相應的伺服器或者服務(Service).
一個WEB伺服器對請求進行響應的典型步驟如下:
- 1、伺服器對客戶端發來的請求(request)進行解析.
- 2、請求被轉發到一個預定義的處理器(handler)
- 3、處理器可能會從資料庫中取出資料。
- 4、處理器根據取出的資料對模板(template)進行渲染(rander)
- 5、處理器向客戶端返回渲染後的內容作為請求的相應。
以上展示了典型的web伺服器運作方式,這種情況下的web請求是無狀態的(stateless),
伺服器本身不會記住與過往請求有關的任何資訊,這使得失效的伺服器可以很容易的替換掉。
每當我們登入網際網路服務的時候,這些服務都會使用cookie來記錄我們的身份。
cookies由少量資料組成,網站要求我們瀏覽器儲存這些資料,並且在每次服務發出請求時再將這些資料傳回服務。
對於用來登入的cookie ,有兩種常見的方法可以將登入資訊儲存在cookie裡:
-
簽名cookie通常會儲存使用者名稱,還有使用者ID,使用者最後一次登入的時間,以及網站覺得有用的其他資訊。
- 令牌cookie會在cookie裡儲存一串隨機位元組作為令牌,伺服器可以根據令牌在資料庫中查詢令牌的擁有者。
簽名cookie和令牌cookie的優點和缺點:
* ------------------------------------------------------------------------------------------------
* | cookie型別 | 優點 | 缺點 |
* -------------------------------------------------------------------------------------------------
* | 簽名 | 驗證cookkie所需的一切資訊都儲存在cookie | 正確的處理簽名很難,很容易忘記 | | |
* | cookie | 還可以包含額外的資訊 | 對資料簽名或者忘記驗證資料簽名, |
* | | 對這些前面也很容易 | 從而造成安全漏洞 |
* -------------------------------------------------------------------------------------------------
* | 令牌 | 新增資訊非常容易,cookie體積小。 | 需要在伺服器中儲存更多資訊, | | |
* | cookie | 移動端和較慢的客戶端可以更快的傳送請求 | 使用關係型資料庫,載入儲存代價高 | | |
* -------------------------------------------------------------------------------------------------
因為該網站沒有實現簽名cookie的需求,所以使用令牌cookie來引用關係型資料庫表中負責儲存使用者登入資訊的條目。
除了登入資訊,還可以將使用者的訪問時長和已瀏覽商品的數量等資訊儲存到資料庫中,有利於更好的像使用者推銷商品
(1)登入和cookie快取
/**
* 使用Redis重新實現登入cookie,取代目前由關係型資料庫實現的登入cookie功能
* 1、將使用一個雜湊來儲存登入cookie令牌與與登入使用者之間的對映。
* 2、需要根據給定的令牌來查詢與之對應的使用者,並在已經登入的情況下,返回該使用者id。
*/
public String checkToken(Jedis conn, String token) {
//1、String token = UUID.randomUUID().toString();
//2、嘗試獲取並返回令牌對應的使用者
return conn.hget("login:", token);
}
/**
* 1、每次使用者瀏覽頁面的時候,程式需都會對使用者儲存在登入雜湊裡面的資訊進行更新,
* 2、並將使用者的令牌和當前時間戳新增到記錄最近登入使用者的集合裡。
* 3、如果使用者正在瀏覽的是一個商品,程式還會將商品新增到記錄這個使用者最近瀏覽過的商品有序集合裡面,
* 4、如果記錄商品的數量超過25個時,對這個有序集合進行修剪。
*/
public void updateToken(Jedis conn, String token, String user, String item) {
//1、獲取當前時間戳
long timestamp = System.currentTimeMillis() / 1000;
//2、維持令牌與已登入使用者之間的對映。
conn.hset("login:", token, user);
//3、記錄令牌最後一次出現的時間
conn.zadd("recent:", timestamp, token);
if (item != null) {
//4、記錄使用者瀏覽過的商品
conn.zadd("viewed:" + token, timestamp, item);
//5、移除舊記錄,只保留使用者最近瀏覽過的25個商品
conn.zremrangeByRank("viewed:" + token, 0, -26);
//6、為有序集key的成員member的score值加上增量increment。通過傳遞一個負數值increment 讓 score 減去相應的值,
conn.zincrby("viewed:", -1, item);
}
}
/**
*儲存會話資料所需的記憶體會隨著時間的推移而不斷增加,所有我們需要定期清理舊的會話資料。
* 1、清理會話的程式由一個迴圈構成,這個迴圈每次執行的時候,都會檢查儲存在最近登入令牌的有序集合的大小。
* 2、如果有序集合的大小超過了限制,那麼程式會從有序集合中移除最多100個最舊的令牌,
* 3、並從記錄使用者登入資訊的雜湊裡移除被刪除令牌對應的使用者資訊,
* 4、並對儲存了這些使用者最近瀏覽商品記錄的有序集合中進行清理。
* 5、於此相反,如果令牌的數量沒有超過限制,那麼程式會先休眠一秒,之後在重新進行檢查。
*/
public class CleanSessionsThread extends Thread {
private Jedis conn;
private int limit = 10000;
private boolean quit ;
public CleanSessionsThread(int limit) {
this.conn = new Jedis("localhost");
this.conn.select(14);
this.limit = limit;
}
public void quit() {
quit = true;
}
public void run() {
while (!quit) {
//1、找出目前已有令牌的數量。
long size = conn.zcard("recent:");
//2、令牌數量未超過限制,休眠1秒,並在之後重新檢查
if (size <= limit) {
try {
sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
long endIndex = Math.min(size - limit, 100);
//3、獲取需要移除的令牌ID
Set<String> tokenSet = conn.zrange("recent:", 0, endIndex - 1);
String[] tokens = tokenSet.toArray(new String[tokenSet.size()]);
ArrayList<String> sessionKeys = new ArrayList<String>();
for (String token : tokens) {
//4、為那些將要被刪除的令牌構建鍵名
sessionKeys.add("viewed:" + token);
}
//5、移除最舊的令牌
conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
//6、移除被刪除令牌對應的使用者資訊
conn.hdel("login:", tokens);
//7、移除使用者最近瀏覽商品記錄。
conn.zrem("recent:", tokens);
}
}
}
(2)使用redis實現購物車
/**
* 使用cookie實現購物車——就是將整個購物車都儲存到cookie裡面,
* 優點:無需對資料庫進行寫入就可以實現購物車功能,
* 缺點:怎是程式需要重新解析和驗證cookie,確保cookie的格式正確。並且包含商品可以正常購買
* 還有一缺點:因為瀏覽器每次傳送請求都會連cookie一起傳送,所以如果購物車的體積較大,
* 那麼請求傳送和處理的速度可能降低。
* -----------------------------------------------------------------
* 1、每個使用者的購物車都是一個雜湊,儲存了商品ID與商品訂單數量之間的對映。
* 2、如果使用者訂購某件商品的數量大於0,那麼程式會將這件商品的ID以及使用者訂購該商品的數量新增到雜湊裡。
* 3、如果使用者購買的商品已經存在於雜湊裡面,那麼新的訂單數量會覆蓋已有的。
* 4、相反,如果某使用者訂購某件商品數量不大於0,那麼程式將從雜湊裡移除該條目
* 5、需要對之前的會話清理函式進行更新,讓它在清理會話的同時,將舊會話對應的使用者購物車也一併刪除。
*/
public void addToCart(Jedis conn, String session, String item, int count) {
if (count <= 0) {
//1、從購物車裡面移除指定的商品
conn.hdel("cart:" + session, item);
} else {
//2、將指定的商品新增到購物車
conn.hset("cart:" + session, item, String.valueOf(count));
}
}
5、需要對之前的會話清理函式進行更新,讓它在清理會話的同時,將舊會話對應的使用者購物車也一併刪除。
只是比CleanSessionsThread多了一行程式碼,虛擬碼如下:
long endIndex = Math.min(size - limit, 100);
//3、獲取需要移除的令牌ID
Set<String> tokenSet = conn.zrange("recent:", 0, endIndex - 1);
String[] tokens = tokenSet.toArray(new String[tokenSet.size()]);
ArrayList<String> sessionKeys = new ArrayList<String>();
for (String token : tokens) {
//4、為那些將要被刪除的令牌構建鍵名
sessionKeys.add("viewed:" + token);
//新增加的這兩行程式碼用於刪除舊會話對應的購物車。
sessionKeys.add("cart:" + sess);
}
//5、移除最舊的令牌
conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
//6、移除被刪除令牌對應的使用者資訊
conn.hdel("login:", tokens);
//7、移除使用者最近瀏覽商品記錄。
conn.zrem("recent:", tokens);
(3)資料行快取
/**
* 為了應對促銷活動帶來的大量負載,需要對資料行進行快取,具體做法是:
* 1、編寫一個持續執行的守護程式,讓這個函式指定的資料行快取到redis裡面,並不定期的更新。
* 2、快取函式會將資料行編碼為JSON字典並儲存在Redis字典裡。其中資料列的名字會被對映為JSON的字典,
* 而資料行的值則被對映為JSON字典的值。
* -----------------------------------------------------------------------------------------
* 程式使用兩個有序集合來記錄應該在何時對快取進行更新:
* 1、第一個為呼叫有序集合,他的成員為資料行的ID,而分支則是一個時間戳,
* 這個時間戳記錄了應該在何時將指定的資料行快取到Redis裡面
* 2、第二個有序集合為延時有序集合,他的成員也是資料行的ID,
* 而分值則記錄了指定資料行的快取需要每隔多少秒更新一次。
* ----------------------------------------------------------------------------------------------
* 為了讓快取函式定期的快取資料行,程式首先需要將hangID和給定的延遲值新增到延遲有序集合裡面,
* 然後再將行ID和當前指定的時間戳新增到排程有序集合裡面。
*/
public void scheduleRowCache(Jedis conn, String rowId, int delay) {
//1、先設定資料行的延遲值
conn.zadd("delay:", delay, rowId);
//2、立即對需要行村的資料進行排程
conn.zadd("schedule:", System.currentTimeMillis() / 1000, rowId);
}
/**
* 1、通過組合使用排程函式和持續執行快取函式,實現類一種重讀進行排程的自動快取機制,
* 並且可以隨心所欲的控制資料行快取的更新頻率:
* 2、如果資料行記錄的是特價促銷商品的剩餘數量,並且參與促銷活動的使用者特別多的話,那麼最好每隔幾秒更新一次資料行快取:
* 另一方面,如果資料並不經常改變,或者商品缺貨是可以接受的,那麼可以每隔幾分鐘更新一次快取。
*/
public class CacheRowsThread
extends Thread {
private Jedis conn;
private boolean quit;
public CacheRowsThread() {
this.conn = new Jedis("localhost");
this.conn.select(14);
}
public void quit() {
quit = true;
}
public void run() {
Gson gson = new Gson();
while (!quit) {
//1、嘗試獲取下一個需要被快取的資料行以及該行的排程時間戳,返回一個包含0個或一個元組列表
Set<Tuple> range = conn.zrangeWithScores("schedule:", 0, 0);
Tuple next = range.size() > 0 ? range.iterator().next() : null;
long now = System.currentTimeMillis() / 1000;
//2、暫時沒有行需要被快取,休眠50毫秒。
if (next == null || next.getScore() > now) {
try {
sleep(50);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
//3、提前獲取下一次排程的延遲時間,
String rowId = next.getElement();
double delay = conn.zscore("delay:", rowId);
if (delay <= 0) {
//4、不必在快取這個行,將它從快取中移除
conn.zrem("delay:", rowId);
conn.zrem("schedule:", rowId);
conn.del("inv:" + rowId);
continue;
}
//5、繼續讀取資料行
Inventory row = Inventory.get(rowId);
//6、更新排程時間,並設定快取值。
conn.zadd("schedule:", now + delay, rowId);
conn.set("inv:" + rowId, gson.toJson(row));
}
}
}
(4)測試
PS:需要好好補償英語了!!需要全部的可以到這裡下載官方翻譯Java版
public class Chapter02 {
public static final void main(String[] args)
throws InterruptedException {
new Chapter02().run();
}
public void run()
throws InterruptedException {
Jedis conn = new Jedis("localhost");
conn.select(14);
testLoginCookies(conn);
testShopppingCartCookies(conn);
testCacheRows(conn);
testCacheRequest(conn);
}
public void testLoginCookies(Jedis conn)
throws InterruptedException {
System.out.println("
----- testLoginCookies -----");
String token = UUID.randomUUID().toString();
updateToken(conn, token, "username", "itemX");
System.out.println("We just logged-in/updated token: " + token);
System.out.println("For user: `username`");
System.out.println();
System.out.println("What username do we get when we look-up that token?");
String r = checkToken(conn, token);
System.out.println(r);
System.out.println();
assert r != null;
System.out.println("Let`s drop the maximum number of cookies to 0 to clean them out");
System.out.println("We will start a thread to do the cleaning, while we stop it later");
CleanSessionsThread thread = new CleanSessionsThread(0);
thread.start();
Thread.sleep(1000);
thread.quit();
Thread.sleep(2000);
if (thread.isAlive()) {
throw new RuntimeException("The clean sessions thread is still alive?!?");
}
long s = conn.hlen("login:");
System.out.println("The current number of sessions still available is: " + s);
assert s == 0;
}
public void testShopppingCartCookies(Jedis conn)
throws InterruptedException {
System.out.println("
----- testShopppingCartCookies -----");
String token = UUID.randomUUID().toString();
System.out.println("We`ll refresh our session...");
updateToken(conn, token, "username", "itemX");
System.out.println("And add an item to the shopping cart");
addToCart(conn, token, "itemY", 3);
Map<String, String> r = conn.hgetAll("cart:" + token);
System.out.println("Our shopping cart currently has:");
for (Map.Entry<String, String> entry : r.entrySet()) {
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
System.out.println();
assert r.size() >= 1;
System.out.println("Let`s clean out our sessions and carts");
CleanFullSessionsThread thread = new CleanFullSessionsThread(0);
thread.start();
Thread.sleep(1000);
thread.quit();
Thread.sleep(2000);
if (thread.isAlive()) {
throw new RuntimeException("The clean sessions thread is still alive?!?");
}
r = conn.hgetAll("cart:" + token);
System.out.println("Our shopping cart now contains:");
for (Map.Entry<String, String> entry : r.entrySet()) {
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
assert r.size() == 0;
}
public void testCacheRows(Jedis conn)
throws InterruptedException {
System.out.println("
----- testCacheRows -----");
System.out.println("First, let`s schedule caching of itemX every 5 seconds");
scheduleRowCache(conn, "itemX", 5);
System.out.println("Our schedule looks like:");
Set<Tuple> s = conn.zrangeWithScores("schedule:", 0, -1);
for (Tuple tuple : s) {
System.out.println(" " + tuple.getElement() + ", " + tuple.getScore());
}
assert s.size() != 0;
System.out.println("We`ll start a caching thread that will cache the data...");
CacheRowsThread thread = new CacheRowsThread();
thread.start();
Thread.sleep(1000);
System.out.println("Our cached data looks like:");
String r = conn.get("inv:itemX");
System.out.println(r);
assert r != null;
System.out.println();
System.out.println("We`ll check again in 5 seconds...");
Thread.sleep(5000);
System.out.println("Notice that the data has changed...");
String r2 = conn.get("inv:itemX");
System.out.println(r2);
System.out.println();
assert r2 != null;
assert !r.equals(r2);
System.out.println("Let`s force un-caching");
scheduleRowCache(conn, "itemX", -1);
Thread.sleep(1000);
r = conn.get("inv:itemX");
System.out.println("The cache was cleared? " + (r == null));
assert r == null;
thread.quit();
Thread.sleep(2000);
if (thread.isAlive()) {
throw new RuntimeException("The database caching thread is still alive?!?");
}
}
}
參考
Redis實戰相關程式碼,目前有Java,JS,node,Python
程式碼地址
https://github.com/guoxiaoxu/…
後記
如果你有耐心讀到這裡,請允許我說明下:
- 1、因為技術能力有限,沒有梳理清另外兩小節,待我在琢磨琢磨。後續補上。
- 2、看老外寫的書像看故事一樣,越看越精彩。不知道你們有這種感覺麼?
- 3、越學越發現自己需要補充的知識太多了,給我力量吧,歡迎點贊。
- 4、感謝所有人,感謝SegmentFault,讓你見證我脫變的過程吧。