Redis用作Session服务器

Redis

REmote DIctionary Server(Redis) 是一个由SalvatoreSanfilippo写的key-value存储系统。

Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

它通常被称为数据结构服务器,因为值(value)可以是字符串(String), 哈希(Map), 列表(list), 集合(sets) 和有序集合(sorted sets)等类型。

Redis与其他key-value存储有什么不同?

Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。

Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,应为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。

Redis vs Memcached

1.网络IO模型

Memcached是多线程,非阻塞IO复用的网络模型,分为监听主线程和worker子线程,监听线程监听网络连接,接受请求后,将连接描述字pipe 传递给worker线程,进行读写IO, 网络层使用libevent封装的事件库,多线程模型可以发挥多核作用,但是引入了cache coherency和锁的问题,比如,Memcached最常用的stats 命令,实际Memcached所有操作都要对这个全局变量加锁,进行计数等工作,带来了性能损耗。

Redis使用单线程的IO复用模型,自己封装了一个简单的AeEvent事件处理框架,主要实现了epoll、kqueue和select,对于单纯只有IO操作来说,单线程可以将速度优势发挥到最大,但是Redis也提供了一些简单的计算功能,比如排序、聚合等,对于这些操作,单线程模型实际会严重影 响整体吞吐量,CPU计算过程中,整个IO调度都是被阻塞住的。

2.内存管理方面

Memcached使用预分配的内存池的方式,使用slab和大小不同的chunk来管理内存,Item根据大小选择合适的chunk存储,内存池的方式可以省去申请/释放内存的开销,并且能 减小内存碎片产生,但这种方式也会带来一定程度上的空间浪费,并且在内存仍然有很大空间时,新的数据也可能会被剔除,原因可以参考Timyang的文 章:http://timyang.net/data/Memcached-lru-evictions/

Redis使用现场申请内存的方式来存储数据,并且很少使用free-list等方式来优化内存分配,会在一定程度上存在内存碎片,Redis跟据存储命 令参数,会把带过期时间的数据单独存放在一起,并把它们称为临时数据,非临时数据是永远不会被剔除的,即便物理内存不够,导致swap也不会剔除任何非临 时数据(但会尝试剔除部分临时数据),这点上Redis更适合作为存储而不是cache。

3.数据一致性问题

Memcached提供了cas命令,可以保证多个并发访问操作同一份数据的一致性问题。

Redis没有提供cas 命令,并不能保证这点,不过Redis提供了事务的功能,可以保证一串 命令的原子性,中间不会被任何操作打断。

4.存储方式及其它方面

Memcached基本只支持简单的key-value存储,不支持枚举,不支持持久化和复制等功能。

Redis除key/value之外,还支持list,set,sorted set,hash等众多数据结构,提供了KEYS进行枚举操作,但不能在线上使用,如果需要枚举线上数据,Redis提供了工具可以直接扫描其dump文件,枚举出所有数据,Redis还同时提供了持久化和复制等功能。

5.关于不同语言的客户端支持

在不同语言的客户端方面,Memcached和Redis都有丰富的第三方客户端可供选择,不过因为Memcached发展的时间更久一些,目前看在 客户端支持方面,Memcached的很多客户端更加成熟稳定,而Redis由于其协议本身就比Memcached复杂,加上作者不断增加新的功能等,对 应第三方客户端跟进速度可能会赶不上,有时可能需要自己在第三方客户端基础上做些修改才能更好的使用。

Session简介

虽然Http是无状态的,但Web应用通常是有状态的,这种有状态,其实就是浏览器客户端和应用服务器维持的一种会话,这种会话,基本的实现方式就是会话标识保存在浏览器Cookie中,对Java web应用来说,就是这个缺省名叫jsessionid的cookie。会话本身保存在服务端,一般Java应用服务器提供这项基础服务,即Session服务。

应用服务器的Session是基于JVM内存实现的,这就带来一个问题:运行在其上的Web应用有状态会话,也是基于JVM内存的。那么,当规模比较大,需要部署多个节点,这种有状态的web应用支持么?也就是支持Scale Out么?(一个tomcat启动一个JVM进程,Tomcat中每个项目都使用不同的ServletContext实例对象)

Scale Out (Scale Horizontally)横向扩展,向外扩展(添加更多的机器)
Scale UP(Scale vertically)纵向扩展,向上扩展(针对单节点添加跟多的CPU Cores,存储设备,大内存等)

应用状态保存在本应用节点上,当后续同一会话的Http请求到来时,它必须落在同样的应用节点上,落到其他应用节点上,由于访问不到这个JVM中的会话,状态丢失。

这对前置的负载均衡器(软、硬件)有一定要求,即要设置成会话粘滞模式(session sticky)。

Session sticky,某种程度上解决了应用节点的线性可伸缩。但他也有本身不足。首选in,要实现合适的负载均衡算法,既要搞笑不影响性能,又要保证会话均衡灵活分布。当某一节点失效,有些大型互联网应用需要把后续请求转移到其他节点上,可会话状态无法转移,这一节点上的会话将全部失效。

有没有更好的办法?

这就涉及到大兴互联网应用架构的一个要点:有状态分离,将应用设计成无状态的。

无状态应用,意味着来自同一会话的http请求,可以在任何一个应用节点处理,集群中的任何应用节点,都是无差别的。即使某应用节点失效,请求仍可在其他节点得到处理。

有状态分离,分离到哪里呢?可以把它分离到session服务器中,由session服务器专门管理应用的状态。

利用Redis来实现Session状态服务器的优点:

  • session的结构是name-value属性名称值对,即Map结构,而Redis支持Map数据类型,正好匹配,着啊杨可以直接操作session中的属性,不需要将整个session取出,粒度细,效率高。
  • session都有有效期控制,这是因为服务端无法判断准确客户端是否在线,当在一定时间内session没有被访问时即认为用户已经离开,即给我们要给session一个生存有效期,有效期过,session自动销毁。Redis的键值本身就支持有效期特性,实现起来方便。
  • session服务器是用来解决大规模应用的高并发、高可用的,那它本身也要支持应用的高并发可用,即session服务器应该是线性可伸缩的,Redis sharding,很好地满足这一需求。

Redis的Session服务器实现基本原理

这里写图片描述

如图,Session服务器对外提供一个接口,叫SessionManager。SessionManager处于web接入层,负责浏览器客户端会话标识与服务端会话之间关系维护,是应用使用Session功能的入口。

Session服务的内部实现对应用来说,是看不见的。SessionManager提供了向会话中设置属性名称值对、根据属性名返回其属性值以及销毁整个会话等方法。

SessionManagerImpl是其实现,SessionManagerImpl重要处理cookie中session标识与服务端session的对应关系,而session内容的具体存储管理交由另一个组件SessionRegistry负责。

SessionManagertImpl核心代码如下:

public void setAttribute(String name,Object value,HttpServletRequest request,HttpServletResponse response){
    Cookie[] cookies = request.getCookies();
    if(null!=cookies){
        for(Cookie cookie :cookies){
            if(cookie.getName().equals(SessionManager.SESSION_ID)){
                if(sessionRegistry.isValid()){//Session存在
                    logger.debug("sessionid key {} exists and is valid.",cookie.getValue());
                    sessionRegistry.setAttribute(cookie.getValue(),name,value);
                    return;
                }
                break;
            }
        }
    }

    String sessionId = sessionIdGenerator.getId();
    sessionRegistry.setAttribute(sessionId,name,value);
    logger.debug("setting a new cookie for sessionId {}.",sessionId);
    Cookie newCookie = new Cookie(SessionManager.SESSION_ID,sessionId);
    newCookie.setDomain(domain);
    newCookie.setPath(path);
    response.addCookie(newCookie);
}

public Object getAttribute(String name,HttpServletRequest request){
    Cookie[] cookies = request.getCookies();
    if(null!=cookies){
        for(Cookie cookie : cookies){
            if(cookie.getName().equals(SessionManager.SESSION_ID)){
                return sessionRegistry.getAttribute(cookie.getValue(),name);
            }
        }
    }
    logger.debug("not found.the session does not exists.",name);
    return null;
}

public void destroySession(HttpServletRequest request,HttpServletResponse response){
    Cookie[] cookies = request.getCookies();
    if(null==cookies) return;
    for(Cookie cookie :cookies){
        if(cookie.getName().equals(SessionManager.SESSION_ID)){
            sessionRegistry.destroySession(cookie.getValue());
            logger.debug("deleting the sessionId cookie.",SessionManager.SESSION_ID);
            cookie.setDomain(domain);
            cookie.setPath(path);
            cookie.setMaxAge(0);
            response.addCookie(cookie);
            return;
        }
    }
}

RegisSessionRegistry是SessionRegistry的具体实现,基于Jedis客户端访问Redis,其核心代码如下:

public void setAttribute(String sessionId,String name,Object value){
    logger.debug("setting session key{} 's field {} with value {}",sessionId,name,value);
    Jedis jedis = jedisPool.getResource();
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = null;
    try{
        oos = new ObjectOutputStream(bos);
        oos.writeObject(value);
    }catch(IOException e){
        logger.error("serializing value{} error.",value);
   }finally{
        try{
            if(null!=oos)oos.close();
        }catch(IOException e){
            logger.error("closing error when serializing value {} to redis.",value);
       }
   }
   jedis.hset(sessionId.getBytes(),name.getBytes(),bos.toByteArray());
   refreshSession(sessionId);
   jedis.close();
}

public String getAttribute(String sessionId, String name) {

  Jedis jedis = jedisPool.getResource();

  byte[] value = jedis.hget(sessionId.getBytes(), name.getBytes());
  if (null==value){
    logger.error("the field {} is not found or the key {} does not exist. ", name, sessionId);
    return null;
  }

  try {     

   ByteArrayInputStream bais = new ByteArrayInputStream(value);
    ObjectInputStream ois = null;

   ois = new ObjectInputStream(bais);
   String  s = (String)ois.readObject(); 

   refreshSession(sessionId); 

   return s;

   }catch (final Exception e) {
     logger.error("Failed deserializing {}. ", value, e);
     }finally{
       jedis.close();
     }
     return null;
  }


Override
public boolean isValid(String sessionId) {

  logger.debug("validating if the sessionId key {} exists . ", sessionId);

  boolean result = false;
  Jedis jedis = jedisPool.getResource();

  if(jedis.exists(sessionId.getBytes()))
    result = true;
  jedis.close();
  return result;
}

@Override
public void refreshSession(String sessionId) {
  logger.debug("refreshing the sessionId key {} . ", sessionId);
  Jedis jedis = jedisPool.getResource();
  jedis.expire(sessionId.getBytes(), timeout);
  jedis.close();
}

@Override
public void destroySession(String sessionId) {
  logger.debug("destroying the sessionId key {} . ", sessionId);
  Jedis jedis = jedisPool.getResource();
  jedis.del(sessionId.getBytes());
  jedis.close();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值