一、用户模型
- TokenDelegate 理解为token的委派类,何为委派?他组合了User对象,有关token的信息封装在里面,如token、ip、createTime(登陆时间) 等等.
- TokenDelegate可以抽象理解为User对象,在UI层展示的时候是用TokenDelegate而非User,因为TokenDelegate组合User对象,还有很多其他属性。他可以抽象理解为User对象,所以下面提到的User用户对象其实就是TokenDelegate。展示用户列表有IP、登陆时间、登陆子系统等。so,TokenDelegate是User对象的领域模型
二、List结构
首先最简单的方法就是存list,根据list的上标和下标就可以分页。这样设计有几个问题。
- 不适合海量存储,list数据量过大有性能问题。
- 无法对登录时间进行排序
- 当有一个用户session失效,要删除list对应数据会很麻烦
三、有序集合设计
以上暴露的几个问题可以这样解决
解决性能问题
首先看图中set集合下的元素,其实就是redis的set命令。key为用户id,value是用户对象(序列化后的字符,取的时候反序列化为用户对象)。性能问题就解决了,简单的用id就能获取对象。分页和排序问题
有序集合结构{ score=2010-1-1 11:11:02,memerKey=userid},代码解释
//logtinTime为分数值score,memberKey为用户id
redis.zadd("pageNum",loginTime,userId);
有序集合的score分数值可以用来排序,我存的是用户创建时间(转为毫秒),memberKey为用户id,这样就可以根据创建时间进行排序,然后用memberKey(userid)作为key,在set集合中关联出用户对象
- session失效后,删除有序集合对应的userId
原有的设计是在获取用户列表的时候判断一下全局token是否失效,失效则删除。删除后会出现当前页展示不满一页的情况(比如一页10条,你删除后变成9条,但是总的条数是20,显示9条肯定不符合逻辑[下面有图])。采取往后一页的数据填充到当前页。缺点不够实时性(不能及时删除失效用户)、性能有问题、编码逻辑复杂
四、实时删除无效key,解决分页问题。
上面提到往后一页填充数据的场景
图中有4页数据,假如一页显示6条。第1页是正常显示,第2页有三条失效(不正常),第3页有4条失效(不正常),第4页正常显示.
假如翻到第2页,那么需要进行补页,在第3页取三条数据填充到第2页,发现第三页不够3条数据又向后一页填充。GG思密达,好比数组删除一个元素后要整体移动的道理一样,假如数据量过大移动的时间复杂度也很大。
因此,我采用一个定时检测线程不断去轮询一个有序集合(为了不影响主系统,独立出一个模块执行线程)
String key= "存sessionID和创建时间的key,自己定义";
String sessionID_startTime = sessionID+startTime;
//sessionTime ,session创建时间(转为毫秒的分数值)
zadd(key,sessionTime,sessionId_startTime);
zadd操作在什么情况下执行?当用户第一次建立会话的时候、当用户进行会话操作的时候更新对应的sessionTime。
while(true){
try {
String key = “自己定义key";
Set<String> sets = redis.zrange(key, 0, -1);//降序获取集合
for (String sessionWithTime :sets) {
//从sessionWithTime中截取sessionID
String sessionId = sessionWithTime.substring(xx);
//从sessionWithTime和截取session最后访问时间
double lastTime = redis.zscore(key, sessionWithTime);
//当前系统时间减去用户最后访问时间得出session活了多长时间
long elapseTime = System.currentTimeMillis()-(long)lastTime;
//因为是降序查询,因此其中有session存活期不超过30分钟,后面都不会超过。直接结束循环
if(elapseTime<1800000L){
break;
}else{
//redis各种del操作。
redis.del("userid");
}
}
}
}
细心的读者可以发现上面其实只是解决实时失效问题,补页逻辑不用判断用户是否失效。对于图中第2、3页出现空白的情况无无法避免。
五、解决分页填充问题
如上图所示实际上的第一页包含了UI展示的许多页,先下个定义,UI展示页定义为虚拟页,实际上的第一页定义为真实页,真实页包含于虚拟页,真实页是一万条,虚拟页为10条。
这能很多很大程度避免补页逻辑的出现,当然无法完全避免。目前先这这样,当运用于真实环境的时候可以再调节。不过还有个问题,假如用户点击第X页,系统如何计算出真实页。代码解释
/**
* 计算实际页数,virPageSize为页面显示的虚拟页(如10条或者20条),pageNum是前台传过来的页数,
*pageSize是实际数目。我设置为1万。通过取余数算法可算出
* @param pageNum
* @return
*/
private int calculatePageNum(int pageNum) {
int targetRow = pageNum*virPageSize;
if(targetRow%pageSize==0){
return targetRow/pageSize;
}
if(targetRow%pageSize==targetRow){
return 1;
}
return (targetRow/pageSize)+1;
}
如 虚拟页10,真实页数10000。假如用户想查看第1000页
targetRow = 10000 = 1000*10
targetRow%pageSize = 0,则 实际页数(targetRow/10000) =1
如 虚拟页10,真实页数100000。假如用户想查看第999页
targetRow = 9990 = 999*10
targetRow%pageSize==targetRow 余数等于targetRow,说明targetRow不超过1W,不超1万肯定是第一页了.因此targetRow/pageSize = 0,再加上1,结果正确。
大家看完后有什么见解都可以互相讨论下