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();
}