最近,在工作中遇到一个问题,问题描述:一个用户在登录成功以后会把用户信息存储在session当中,这时session所在服务器为server1,那么用户在session失效之前如果再次使用app,那么可能会被路由到server2,这时问题来了,server没有该用户的session,所以需要用户重新登录,这时的用户体验会非常不好,所以我们想如何实现多台server之间共享session,让用户状态得以保存。
当然业界已经有很多成熟的解决方案,我罗列如下:
1.服务器实现的session复制或session共享,这类型的共享session是和服务器紧密相关的,比如webSphere或JBOSS在搭建集群时候可以配置实现session复制或session共享,但是这种方式有一个致命的缺点,就是不好扩展和移植,比如我们更换服务器,那么就要修改服务器配置。
2.利用成熟的技术做session复制,比如12306使用的gemfire,比如常见的内存数据库如redis或memorycache,这类方案虽然比较普适,但是严重依赖于第三方,这样当第三方服务器出现问题的时候,那么将是应用的灾难。
3.将session维护在客户端,很容易想到就是利用cookie,但是客户端存在风险,数据不安全,而且可以存放的数据量比较小,所以将session维护在客户端还要对session中的信息加密。
我们实现的方案可以说是第二种方案和第三种方案的合体,可以利用gemfire实现session复制共享,还可以将session维护在redis中实现session共享,同时可以将session维护在客户端的cookie中,但是前提是数据要加密。这三种方式可以迅速切换,而不影响应用正常执行。我们在实践中,首选gemfire或者redis作为session共享的载体,一旦session不稳定出现问题的时候,可以紧急切换cookie维护session作为备用,不影响应用提供服务,下面我简单介绍方案中session共享的实现方式和原理。
这里主要讲解redis和cookie方案,gemfire比较复杂大家可以自行查看gemfire工作原理。利用redis做session共享,首先需要与业务逻辑代码解耦,不然session共享将没有意义,其次支持动态切换到客户端cookie模式。redis的方案是,重写服务器中的HttpSession和HttpServletRequest,首先实现HttpSession接口,重写session的所有方法,将session以hash值的方式存在redis中,一个session的key就是sessionID,setAtrribute重写之后就是更新redis中的数据,getAttribute重写之后就是获取redis中的数据,等等需要将HttpSession的接口一一实现。
实现了HttpSesson,那么我们先将该session类叫做MySession(当然实践中不是这么命名的),当MySession出现之后问题才开始,怎么能在不影响业务逻辑代码的情况下,还能让原本的request.getSession()获取到的是MySession,而不是服务器原生的session。这里,我决定重写服务器的HttpServletRequet,这里先称为MyRequest,但是这可不是单纯的重写,我需要在原生的request基础上重写,于是我决定在filter中,实现request的偷梁换柱,我的思路是这样的,MyRequest的构建器,必须以request作为参数,于是我在filter中将服务器原生的request(也有可能是框架封装过的request),当做参数new出来一个MyRequest,并且MyRequest也实现了HttpServletRequest接口,其实就是对原生request的一个增强,这里主要重写了几个request的方法,但是最重要的是重写了request.getSession(),写到这里大家应该都明白为什么重写这个方法了吧,当然是为了获取MySession,于是这样就在filter中,偷偷的将原生的request换成MyRequest了,然后再将替换过的request传入chan.doFilter(),这样filter时候的代码都使用的是MyRequest了,同时对业务代码是透明的,业务代码获取session的方法仍然是request.getSession(),但其实获取到的已经是MySession了,这样对session的操作已经变成了对redis的操作。这样实现的好处有两个,第一开发人员不需要对session共享做任何关注,session共享对用户是透明的;第二,filter是可配置的,通过filter的方式可以将session共享做成一项可插拔的功能,没有任何侵入性。
这个时候已经实现了一套可插拔的session共享的框架了,但是我们想到如果redis服务出了问题,这时我们该怎么办呢,于是我们延续redis的想法,想到可以将session维护在客户端内(加密的cookie),当然实现方法还是一样的,我们重写HttpSession接口,实现其所有方法,比如setAttribute就是写入cookie,getAttribute就是读取cookie,我们可以将重写的session称作MySession2,这时怎么让开发人员透明的获取到MySession2呢,实现方法还是在filter内偷梁换柱,在MyRequest加一个判断,读取sessionType配置,如果sessionType是redis的,那么getSession的时候获取到的是MySession,如果sessionType是coolie的,那么getSession的时候获取到的是MySession2,以此类推,用同样的方法就可以获取到MySession 3,4,5,6等等。
这样两种方式都有了,那么我们怎实现两种session共享方式的快速切换呢,刚刚我提到一个sessionType,这是用来决定获取到session的类型的,只要变换sessionType就能实现两种session共享方式的切换,但是sessionType必须对所有的服务器都是一致的,如果不一致那将会出现比较严重的问题,我们目前是将sessionType维护在环境变量里,如果要切换sessionType就要重启每一台服务器,完成session共享的转换,但是当服务器太多的时候将是一种灾难。而且重启服务意味着服务的中断,所以这样的方式只适合服务器规模比较小,而且用户量比较少的情况,当服务器太多的时候,务必需要一种协调技术,能够让服务器能够及时获取切换的通知。基于这样的原因,我们选用zookeeper作为配置平台,每一台服务器都会订阅zookeeper上的配置,当我们切换sessionType之后,所有服务器都会订阅到修改之后的配置,那么切换就会立即生效,当然可能会有短暂的时间延迟,但这是可以接受的。
方案大体分享完了,在此将我的收获记录下来,我们已经实现了一个版本,大家也可以发挥想象,分享不同的方案,我们一起进步,文中有不足之处,还望各位不吝赐教。