redis构建web应用(一)
一、前言
web应用在许多同学眼中看来是一个庞然大物,可能很难学吧。其实这是对的,不仅仅是web应用,就连一个小小的微信小程序也需要有强大的后台作为数据处理能力的支撑。回想你所使用的任何一款应用,是不是最开始要解决的都是账号密码问题呢?那作为一名程序员如何解决这个后台问题呢?你可以选用常规的关系型数据库,但是这和我们前几篇文章里面所提到的一样,redis的效率远远高于关系型数据库,将极大的提高应用速度,提升用户体验。
如果未安装Redis Desktop Manager的同学可以安装起来啦~~
二、本文主要功能
本文的主要功能是:
(1)记录登录用户的token和用户名
(2)记录每个用户最近浏览的五个商品
(3)当登录网站的用户量超服务器限制时,使用淘汰最久未使用算法删除一部分用户所对应的信息。
三、相关功能实现
3.1 存储登录用户账号信息
在web应用中,使用HTTP协议来进行通信,但是HTTP协议是无状态协议。所谓的无状态协议就是说,服务器本身不会记录与过往请求的任何信息。因此很可能这次登录进去,你说你叫Kelvin,下次你再登录进来,服务器还是不知道你就是Kelvin。为了解决这种尴尬,程序员们通常使用cookie来记录身份。cookie常见的有签名(signed)cookie和令牌(token)cookie。
签名(signed)cookie通常会存储用户名,用户ID和用户最后一次成功登录的时间,以及其他网站觉得有用的信息。
令牌(token)cookie会在cookie里面存储一串随机字节作为令牌,服务器可以根据令牌在数据库中查找令牌的拥有者。随着时间推移,旧令牌会被新令牌取代。
因为本次用不到签名cookie,所以采用token来存储用户信息。首先,我们定义一个用户类User。
public class User {
String token;
String userName;
public User(String token, String userName) {
this.token = token;//生成token
this.userName = userName;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
我们的主要逻辑在Login类里面,因为刚刚建好了User类,所以在Login类的main()函数里实例化一个user对象。其中userName由用户从键盘输入,token使用UUID随机生成。
System.out.println("请输入你的用户名:");
String userName = in.next();
User user = new User(UUID.randomUUID().toString(), userName);
那么用户的两个基本属性,token和userName。我们需要一个散列来进行存储。
(后续都会画这样的数据类型图供形象参考)
为防止main()函数过于庞大,同时也为了代码的可复用性,我们将存储逻辑写在一个updataToken()方法内
public static void updateToken(Jedis conn, User user, String goodsName) {
/**
* 获取当前时间戳*/
long now = System.currentTimeMillis() / 1000;
//将用户的token和userName存入login:散列中
conn.hset("login:", user.getToken(), user.getUserName());
}
运行程序,输入用户名进行测试
接下来我们使用Redis Desktop Manager查看是否存储了Lina的相关数据
如图所示存储成功。
3.2 最近用户信息
因为服务器的存储空间是很昂贵的,所以如果一直存储所有用户的token信息,是压力比较大的事情,因此一般的程序都会设置一个存储的最大容量limit。一旦存储数据的数量超过这个limit,就采用最近最久未使用算法进行删除一些数据以使数据存储量在limit之内。
是不是乍一看觉得很容易实现?只需要采用while(true)循环不断的对就可以解决这个小问题了?其实问题并没有那么简单哦~while(ture)的方式要么就在循环中一直循环,程序什么事儿也干不了了,要么就干完了循环后续存储的用户量变多了又没有删除存储量的功能了。
仔细想一想会发现,使存储量在limit范围之内是整个应用一直要执行的,是不是想到了守护进程呢?
使用守护进程来实现检测存储量使其在limit范围内,是一个不错的做法,也是常规的做法。因此我们需要开辟一个CleanSessionThread线程。不过在此之前,我们要明白,使用最近最久未使用算法,是要进行时间判断的。因此,我们需要一个有序集合来存储用户最近一次登录的时间和用户的token。
/**
* 获取当前时间戳*/
long now = System.currentTimeMillis() / 1000;
/**
* 同时将用户的token和登录的时间存入recent:有序集合
* token是用户唯一标识,所以选用token而不选用userName
* 存入最近登录时间,使用最长时间未使用算法刷新有序集合*/
conn.zadd("recent:", now, user.getToken());
以上代码是实现将用户存入recent:有序集合的核心逻辑,我们查看一下刚刚存入的Lina是否在recent:有序集合中也已经存下了。
如上所示,存储成功。
那么接下去就是要使用后台守护线程来使存储的用户量保持在limit以内。新建CleanSessionThread类继承Thread。
如果对多线程不太了解的同学,请扫描文章末尾二维码进行学习Java基础知识。
public class CleanSessionThread extends Thread {
private Jedis conn;
private Boolean quit = false;
private int limit;//服务器最大限制,实际远比这大得多
public CleanSessionThread(int limit) {
this.conn = new Jedis("47.99.164.123", 6379);
this.conn.select(3);
this.limit = limit;
}
public void run(){
while (!quit){
long size = conn.zcard("recent:");
if(size<=limit){
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
long endIndex = size-limit;
Set<String> tokenSet = conn.zrange("recent:",0,endIndex-1);
/**将集合存为一个字符串数组*/
String[] tokens = tokenSet.toArray(new String[tokenSet.size()]);
/**构建一个数组列表存储需要移除的会话key*/
ArrayList<String> sessionKeys = new ArrayList<String>();
/**通过拼接字符串的形式将每一个需要移除的客户的最近浏览商品有序集合Key,存入sessionKeys数组*/
for (String token : tokens) {
sessionKeys.add("viewed:" + token);
}
/**toArray()方法会返回List中所有元素构成的数组,并且数组类型是Object[]*/
conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
/**在登录散列中删除这些用户的token*/
conn.hdel("login:", tokens);
/**在最近查看有序集合中移除*/
conn.zrem("recent:", tokens);
}
}
/**将quit置为true*/
public void quitTrue(){
quit = true;
}
}
3.3 最近浏览商品信息
如果你逛过淘宝,京东,当当,拼多多等电商平台,一定会发现当你搜索某件商品多次之后,系统就会给你推荐这类似的商品。或者在你搜某件商品没搜到的时候,会冒出“推荐你购买以下同类商品”。又或者在你买了一件商品之后,系统会提示你“买了这件商品的人,还购买了以下商品”。实现这些功能是需要比较庞大的技术的,也就是江湖传闻的大数据个性化推荐。
但是因为我们目前只讲解redis,所以实现用户最新浏览的五个商品的存储,为上面所说的个性化推荐打好基础。后续的文章会对这个功能慢慢完善。存储用户最新浏览的五个商品的存储,那么我们需要拿到用户浏览这个商品的时间now,也要拿到商品名goodsName,因为是最近浏览,所以需要排序,即选用有序集合viewed:,下面是这个集合的数据类型图。
if (goodsName != null) {
/**用户最近浏览商品有序集合
* 键名:viewed:token 每个用户一个有序集合
* 分值:now 浏览商品当前时间戳
* 成员:goodsName 浏览的商品名*/
conn.zadd("viewed:" + user.getToken(), now, goodsName);
/**只保留用户最近浏览的五个商品*/
conn.zremrangeByRank("viewed:" + user.getToken(), 0, -6);
}
运行程序,进行测试
以上我们仔细观察,曹操最近浏览的五个商品是1,4,6,8,2,那么我们回到Redis Desktop Manage中查看redis是否存储了这些数据
首先,查看曹操的token
然后找到曹操token对应的viewd:token集合
和我们想要拿到的数据是吻合的。
四、总结
本文实现了一些功能,但并不是这个web案例的所有功能,因为web功能较多较繁杂,所以需要分阶段来写。后续博客还会继续完善商品功能,根据最近浏览记录,根据商品浏览人数最多记录,根据商品浏览的实时性火爆程度推荐,每一个都是比较有趣的功能,但同时也是比较挑战的。所以各位一定要学好Java基础,比如多线程这一块知识,要认真学习。
本文的源代码在以下链接下载:
http://47.99.164.123:8088/root/redisLearn.git
对Java系列知识感兴趣的朋友可以加入QQ群
慧梦软件开发技术联盟:952317701
更多系列文章在java高级程序开发微信公众号