- 二、redis缓存设计
- 1.1 list结构不能胜任
- 1.2 如何清除redis过期键,redis键和session同步问题。
二、redis缓存设计
如何设计在线用户列表?由于用户量大采用数据库保存在线用户会有瓶颈问题,考虑到系统的伸缩和扩展性等,将采用redis作为在线用户缓存,最简单粗暴的方案是将用户存入redis的list,但这样遇到以下几个问题:
- 假如用户量指数增长,list的查询速度和分页速度会非常慢
- 如果要按照登陆时间进行排序查询,list方案很难处理
- 在线用户是基于session管理的,当session失效后如何通知redis进行相应清除工作
- session超时时间如何跟redis key 保持同步,也就是 session过期,redis对应的缓存key也过期
1.1 list结构不能胜任
我们将采用一个集合存一页数据,例如一页20条,总共100页,将会生成5个有序集合,OK这样就解决一个list存取数据量过大的问题,用有序集合解决排序问题,下面用代码说明下思路
//key为页码,loginTime登陆时间为分数值(转为毫秒可用于排序),userid为用户ID
redis.zadd("pageNum",loginTime,userId);
//用set存用户
redis.set(userId,user);
//根据页码,还有是否升序和降序获取在线用户
public List<User> getOnlineUser(int pageNum,boolean isAsc){
Set<String> userIds = null;
List<User> users = new ArrayList<User>();
if(isAsc){
userIds = redis.zrange(zkey, 0, -1);//redis升序查询
}else{
userIds = redis.zrevrange(zkey, 0, -1);//redis降序查询
}
//此时的userids集合是经过排序的,
Iterator<String> iterUserIds = userIds.iterator();
while(iterUserIds.hasNext()){
String userId = iterUserIds.next();
User user = redis.get(userId);
users.add(user);
}
return users;
}
总结:一条有序集合就是一页数据,但是他存的成员key是用户ID,分数值是登陆时间,真正的用户对象只用简单的set,然后遍历有序集合用id再获取,有序集合是一个冗余的集合,我们用内存换来了设计的灵活性。
1.2 如何清除redis过期键,redis键和session同步问题。
有以下几个坑
- 有序集合的的成员key无法设置过期时间,只能对最外部的key设置
- 当点击列表某一页,在获取数据期间可判断用户是否失效(根据全局会话判断,全局会话失效说明缓存的用户也失效),但是这样就遇到一个难以解决的问题,例如总页数4,第2和第3页有几个用户失效了,那么翻到第2页会出现数据不满一页,
但是总页数是4,这明显不合理。有解决方法是当点击某一页用户时候对集合进行补页操作,所谓补页操作就是不满一页时候进行填满,假如第2页需要补,那么第3页以后的数据都要往前推,
好比数组删除一个元素后要整体移动的道理一样。假如数据量过大移动的时间复杂度也很大。说道数据量大,那么可以一个集合存一万条数据,在前台不可能显示一万条。实际存储的是一万,
虚拟显示的每页是10条或者20条,假如翻到第22页,可以推算出实际的页码,如下代码
/**
* 计算实际页数,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;
}
return (targetRow/pageSize)+1;
}
上面某种程度可以解决redis失效问题,因为在获取在线列表时候判断了失效清除,但这需要主动触发,所以下面采用定时检测机制。首先要解决session 跟redis同步问题。项目中用了shiro,当创建session和访问页面时候,分别会调用shiro sessionDAO中的doCreate方法和doReadSession方法,访问doReadSesison方法时就可以给redis key重新设置超时时间(expire),这样就达到session同步,最后通过一个不断检测的线程,检查session最后访问时间,再减去超时时间得出是否超时。
有序集合zadd(key,sessionTime,sessionId_startTime),分数值是最后访问时间,成员key为sessionid+创建时间,然后升序排序取到集合第一个最后访问时间,第一个没超时证明他之后的都没超时,跳出本次循环(因为排序过了)。分数值sessionTime可以在访问sessionDAO的时候重新设置进去,清除的时候因为有sessionID,就可以取得token、用户等信息(自己去绑定sessionID对应的信息,例如sessionID为key,token对象为值)
* 有一个场景可能导致漏检测,当认证中心强制下线某一用户迫使全局token失效了,而有序集合的最后访问时间还在。如果还没在失效期,当循环取到这类数据会不清除。*
* 改良方案: *
一,有按百分比取前面的数据进行清除,达到期望的命中率后继续清除
二,注销或者强制下线的时候获取sessionID,对有序集合中的成员进行remove **
未完待续!!