购物网站的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,让你见证我脱变的过程吧。
相关文章
相关标签/搜索