購物網站的redis相關實現(Java)

購物網站的redis相關實現


一、使用Redis構建文章投票網站(Java)html

本文主要內容:

  • 一、登陸cookie
  • 二、購物車cookie
  • 三、緩存數據庫行
  • 四、測試

必備知識點

WEB應用就是經過HTTP協議對網頁瀏覽器發出的請求進行相應的服務器或者服務(Service).java

一個WEB服務器對請求進行響應的典型步驟以下:node

  • 一、服務器對客戶端發來的請求(request)進行解析.
  • 二、請求被轉發到一個預約義的處理器(handler)
  • 三、處理器可能會從數據庫中取出數據。
  • 四、處理器根據取出的數據對模板(template)進行渲染(rander)
  • 五、處理器向客戶端返回渲染後的內容做爲請求的相應。

以上展現了典型的web服務器運做方式,這種狀況下的web請求是無狀態的(stateless),
服務器自己不會記住與過往請求有關的任何信息,這使得失效的服務器能夠很容易的替換掉。git


每當咱們登陸互聯網服務的時候,這些服務都會使用cookie來記錄咱們的身份。github

cookies由少許數據組成,網站要求咱們瀏覽器存儲這些數據,而且在每次服務發出請求時再將這些數據傳回服務。web

對於用來登陸的cookie ,有兩種常見的方法能夠將登陸信息存儲在cookie裏:redis

  • 簽名cookie一般會存儲用戶名,還有用戶ID,用戶最後一次登陸的時間,以及網站以爲有用的其餘信息。數據庫

    • 令牌cookie會在cookie裏存儲一串隨機字節做爲令牌,服務器能夠根據令牌在數據庫中查找令牌的擁有者。

簽名cookie和令牌cookie的優勢和缺點:segmentfault

* ------------------------------------------------------------------------------------------------
* |  cookie類型       |                  優勢                    |           缺點                 |
* -------------------------------------------------------------------------------------------------
* |    簽名           |  驗證cookkie所需的一切信息都存儲在cookie  |  正確的處理簽名很難,很容易忘記  |                      |                                      |
* |   cookie          |  還能夠包含額外的信息                    |  對數據簽名或者忘記驗證數據簽名, |
* |                   |  對這些前面也很容易                      |  從而形成安全漏洞               |
* -------------------------------------------------------------------------------------------------
* |   令牌            |     添加信息很是容易,cookie體積小。      |   須要在服務器中存儲更多信息,   |                    |                                          |
* |   cookie          |  移動端和較慢的客戶端能夠更快的發送請求    |  使用關係型數據庫,載入存儲代價高 |                           |                                      |
* -------------------------------------------------------------------------------------------------

由於該網站沒有實現簽名cookie的需求,因此使用令牌cookie來引用關係型數據庫表中負責存儲用戶登陸信息的條目。
除了登陸信息,還能夠將用戶的訪問時長和已瀏覽商品的數量等信息存儲到數據庫中,有利於更好的像用戶推銷商品瀏覽器


(1)登陸和cookie緩存

/**
 * 使用Redis從新實現登陸cookie,取代目前由關係型數據庫實現的登陸cookie功能
 * 一、將使用一個散列來存儲登陸cookie令牌與與登陸用戶之間的映射。
 * 二、須要根據給定的令牌來查找與之對應的用戶,並在已經登陸的狀況下,返回該用戶id。
 */
public String checkToken(Jedis conn, String token) {
    //一、String token = UUID.randomUUID().toString();
    //二、嘗試獲取並返回令牌對應的用戶
    return conn.hget("login:", token);
}
/**
 * 一、每次用戶瀏覽頁面的時候,程序需都會對用戶存儲在登陸散列裏面的信息進行更新,
 * 二、並將用戶的令牌和當前時間戳添加到記錄最近登陸用戶的集合裏。
 * 三、若是用戶正在瀏覽的是一個商品,程序還會將商品添加到記錄這個用戶最近瀏覽過的商品有序集合裏面,
 * 四、若是記錄商品的數量超過25個時,對這個有序集合進行修剪。
 */
public void updateToken(Jedis conn, String token, String user, String item) {
    //一、獲取當前時間戳
    long timestamp = System.currentTimeMillis() / 1000;
    //二、維持令牌與已登陸用戶之間的映射。
    conn.hset("login:", token, user);
    //三、記錄令牌最後一次出現的時間
    conn.zadd("recent:", timestamp, token);
    if (item != null) {
        //四、記錄用戶瀏覽過的商品
        conn.zadd("viewed:" + token, timestamp, item);
        //五、移除舊記錄,只保留用戶最近瀏覽過的25個商品
        conn.zremrangeByRank("viewed:" + token, 0, -26);
        //六、爲有序集key的成員member的score值加上增量increment。經過傳遞一個負數值increment 讓 score 減去相應的值,
        conn.zincrby("viewed:", -1, item);
    }
}
/**
 *存儲會話數據所需的內存會隨着時間的推移而不斷增長,全部咱們須要按期清理舊的會話數據。
 * 一、清理會話的程序由一個循環構成,這個循環每次執行的時候,都會檢查存儲在最近登陸令牌的有序集合的大小。
 * 二、若是有序集合的大小超過了限制,那麼程序會從有序集合中移除最多100個最舊的令牌,
 * 三、並從記錄用戶登陸信息的散列裏移除被刪除令牌對應的用戶信息,
 * 四、並對存儲了這些用戶最近瀏覽商品記錄的有序集合中進行清理。
 * 五、於此相反,若是令牌的數量沒有超過限制,那麼程序會先休眠一秒,以後在從新進行檢查。
 */
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) {
            //一、找出目前已有令牌的數量。
            long size = conn.zcard("recent:");
            //二、令牌數量未超過限制,休眠1秒,並在以後從新檢查
            if (size <= limit) {
                try {
                    sleep(1000);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
                continue;
            }

            long endIndex = Math.min(size - limit, 100);
            //三、獲取須要移除的令牌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) {
                //四、爲那些將要被刪除的令牌構建鍵名
                sessionKeys.add("viewed:" + token);
            }
            //五、移除最舊的令牌
            conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
            //六、移除被刪除令牌對應的用戶信息
            conn.hdel("login:", tokens);
            //七、移除用戶最近瀏覽商品記錄。
            conn.zrem("recent:", tokens);
        }
    }
}

(2)使用redis實現購物車

/**
 * 使用cookie實現購物車——就是將整個購物車都存儲到cookie裏面,
 * 優勢:無需對數據庫進行寫入就能夠實現購物車功能,
 * 缺點:怎是程序須要從新解析和驗證cookie,確保cookie的格式正確。而且包含商品能夠正常購買
 * 還有一缺點:由於瀏覽器每次發送請求都會連cookie一塊兒發送,因此若是購物車的體積較大,
 * 那麼請求發送和處理的速度可能下降。
 * -----------------------------------------------------------------
 * 一、每一個用戶的購物車都是一個散列,存儲了商品ID與商品訂單數量之間的映射。
 * 二、若是用戶訂購某件商品的數量大於0,那麼程序會將這件商品的ID以及用戶訂購該商品的數量添加到散列裏。
 * 三、若是用戶購買的商品已經存在於散列裏面,那麼新的訂單數量會覆蓋已有的。
 * 四、相反,若是某用戶訂購某件商品數量不大於0,那麼程序將從散列裏移除該條目
 * 五、須要對以前的會話清理函數進行更新,讓它在清理會話的同時,將舊會話對應的用戶購物車也一併刪除。
 */
public void addToCart(Jedis conn, String session, String item, int count) {
    if (count <= 0) {
        //一、從購物車裏面移除指定的商品
        conn.hdel("cart:" + session, item);
    } else {
        //二、將指定的商品添加到購物車
        conn.hset("cart:" + session, item, String.valueOf(count));
    }
}

五、須要對以前的會話清理函數進行更新,讓它在清理會話的同時,將舊會話對應的用戶購物車也一併刪除。

只是比CleanSessionsThread多了一行代碼,僞代碼以下:

long endIndex = Math.min(size - limit, 100);
//三、獲取須要移除的令牌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) {
    //四、爲那些將要被刪除的令牌構建鍵名
    sessionKeys.add("viewed:" + token);

    //新增長的這兩行代碼用於刪除舊會話對應的購物車。
    sessionKeys.add("cart:" + sess);
}
//五、移除最舊的令牌
conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
//六、移除被刪除令牌對應的用戶信息
conn.hdel("login:", tokens);
//七、移除用戶最近瀏覽商品記錄。
conn.zrem("recent:", tokens);

(3)數據行緩存

/**
 * 爲了應對促銷活動帶來的大量負載,須要對數據行進行緩存,具體作法是:
 * 一、編寫一個持續運行的守護進程,讓這個函數指定的數據行緩存到redis裏面,並不按期的更新。
 * 二、緩存函數會將數據行編碼爲JSON字典並存儲在Redis字典裏。其中數據列的名字會被映射爲JSON的字典,
 * 而數據行的值則被映射爲JSON字典的值。
 * -----------------------------------------------------------------------------------------
 * 程序使用兩個有序集合來記錄應該在什麼時候對緩存進行更新:
 * 一、第一個爲調用有序集合,他的成員爲數據行的ID,而分支則是一個時間戳,
 * 這個時間戳記錄了應該在什麼時候將指定的數據行緩存到Redis裏面
 * 二、第二個有序集合爲延時有序集合,他的成員也是數據行的ID,
 * 而分值則記錄了指定數據行的緩存須要每隔多少秒更新一次。
 * ----------------------------------------------------------------------------------------------
 * 爲了讓緩存函數按期的緩存數據行,程序首先須要將hangID和給定的延遲值添加到延遲有序集合裏面,
 * 而後再將行ID和當前指定的時間戳添加到調度有序集合裏面。
 */
public void scheduleRowCache(Jedis conn, String rowId, int delay) {
    //一、先設置數據行的延遲值
    conn.zadd("delay:", delay, rowId);
    //二、當即對須要行村的數據進行調度
    conn.zadd("schedule:", System.currentTimeMillis() / 1000, rowId);
}
/**
 * 一、經過組合使用調度函數和持續運行緩存函數,實現類一種重讀進行調度的自動緩存機制,
 * 而且能夠爲所欲爲的控制數據行緩存的更新頻率:
 * 二、若是數據行記錄的是特價促銷商品的剩餘數量,而且參與促銷活動的用戶特別多的話,那麼最好每隔幾秒更新一次數據行緩存:
 * 另外一方面,若是數據並不常常改變,或者商品缺貨是能夠接受的,那麼能夠每隔幾分鐘更新一次緩存。
 */
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) {
            //一、嘗試獲取下一個須要被緩存的數據行以及該行的調度時間戳,返回一個包含0個或一個元組列表
            Set<Tuple> range = conn.zrangeWithScores("schedule:", 0, 0);
            Tuple next = range.size() > 0 ? range.iterator().next() : null;
            long now = System.currentTimeMillis() / 1000;
            //二、暫時沒有行須要被緩存,休眠50毫秒。
            if (next == null || next.getScore() > now) {
                try {
                    sleep(50);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
                continue;
            }
            //三、提早獲取下一次調度的延遲時間,
            String rowId = next.getElement();
            double delay = conn.zscore("delay:", rowId);
            if (delay <= 0) {
                //四、沒必要在緩存這個行,將它從緩存中移除
                conn.zrem("delay:", rowId);
                conn.zrem("schedule:", rowId);
                conn.del("inv:" + rowId);
                continue;
            }
            //五、繼續讀取數據行
            Inventory row = Inventory.get(rowId);
            //六、更新調度時間,並設置緩存值。
            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("\n----- 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("\n----- 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("\n----- 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實戰

Redis實戰相關代碼,目前有Java,JS,node,Python

2.Redis 命令參考

代碼地址

https://github.com/guoxiaoxu/...

後記

若是你有耐心讀到這裏,請容許我說明下:

  • 一、由於技術能力有限,沒有梳理清另外兩小節,待我在琢磨琢磨。後續補上。
  • 二、看老外寫的書像看故事同樣,越看越精彩。不知道大家有這種感受麼?
  • 三、越學愈加現本身須要補充的知識太多了,給我力量吧,歡迎點贊。
  • 四、感謝全部人,感謝SegmentFault,讓你見證我脫變的過程吧。
相關文章
相關標籤/搜索