相信用过spring-session做session共享的朋友都很喜欢它的精巧易用-不依赖具体web容器、不需要修改已成项目的代码。笔者在使用spring-session的过程中也对spring-session的绝佳包容性、稳定性赞叹不已,spring-session 和 redis 的结合堪称神器,但是两者结合下来真的可以完全代替原本的session管理吗?

一、url rewrite保持Session

相信很多做过文件上传的朋友遇到过这样的需求-在浏览器中显示上传进度条并且要求多浏览器兼容性,特殊国情~兼容IE低版本,OK,只能用上笔者认为已经过时的技术-Flash,做前端比较多的肯定知道SWFUpload、Uploadify这类通过调用Flash上传实现浏览器本身不具备的显示进度条的功能。但是在某些浏览器、某些flash客户端版本下,上传的HTTP请求是不带cookie的,so,session问题如何解决?普遍的做法是通过url rewrite保持Session,即获取cookie中的jsessionid来放到请求url的参数中。那么spring-session支持吗?回答NO,至少spring-session源码中是没有支持的,如何支持呢? 
我们阅读代码可以看到spring-session中实现从cookie到session的策略类是CookieHttpSessionStrategy,并且允许自定义策略类,只需要在spring-session中定义bean就行了,所以我们来扩展这个CookieHttpSessionStrategy。 
1. 想要直接继承CookieHttpSessionStrategy?那是不可能的,它是final的,为啥?暂时不清楚。 
2. 看来只能硬来了,首先把CookieHttpSessionStrategy的源码复制出来,放到自己的项目里一份,去掉final关键字,姑且新类名就叫SessionForCookieStrategy吧。 
3. 为了整洁,不建议在这个类下直接修改了,咱还是应该坚持java人的操守不是?新建一个SessionUnionStrategy类,提供了从request域中获取jsessionid的参数。 
4. 建立SessionForURLFilter,即处理从url中获取jsessionid然后把值丢给request Attribute中。 
5. 配置文件配置Strategy和Filter 
上代码: 
SessionUnionStrategy类:

public class SessionUnionStrategy extends SessionForCookieStrategy{

    @Override
public Map<String, String> getSessionIds(HttpServletRequest request) {
Map<String, String> result = super.getSessionIds(request);
if(result.isEmpty()){
String jsessionId = (String)request.getAttribute(SessionForURLFilter.OLDEST_URL_SESSION_ID_ATTRIBUTE_NAME);
if ((jsessionId != null) && (!"".equals(jsessionId.trim())))
{
result.put(DEFAULT_ALIAS, jsessionId);
}
}
return result;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

SessionForURLFilter类:

public class SessionForURLFilter extends OncePerRequestFilter{

    public static final String OLDEST_URL_SESSION_ID_ATTRIBUTE_NAME = "OLDEST_URL_SESSION_ID_ATTRIBUTE_NAME";

    @Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if(request.isRequestedSessionIdFromURL()){
String jsessionId = request.getRequestedSessionId();
if ((jsessionId != null) && (!"".equals(jsessionId.trim()))){
request.setAttribute(OLDEST_URL_SESSION_ID_ATTRIBUTE_NAME, jsessionId);
}
}
filterChain.doFilter(request, response);
} }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

spring容器配置文件中:

<bean name="sessionForURLFilter" class="cn.emay.bootstrap.util.SessionForURLFilter"/>
<bean class="cn.emay.bootstrap.util.SessionUnionStrategy">
<property name="cookieSerializer">
<bean class="org.springframework.session.web.http.DefaultCookieSerializer">
<property name="cookieName" value="JSESSIONID"/>
</bean>
</property>
</bean>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

web.xml文件中:

<filter>
<filter-name>sessionForURLFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>sessionForURLFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

二、除了JDK序列化还能用JSON序列化方式吗?

用过spring-session的朋友都知道,它的基本工作原理是把原本session中的对象从单机的内存中剥离出来放到的公共存储中,这就需要序列化了,默认使用JDK序列化方式,并且是支持自定义序列化方式的。很多人知道既然一般一个JAVA对象的JSON的存储量肯定比JDK序列化方式的存储量小的多,那为啥不用JSON来存储?一来可以减轻IO的压力,二来可以直接在redis中直接阅读session数据。 
首先在spring-session的文档中找到这么一段:

Custom RedisSerializer 
You can customize the serialization by creating a Bean named springSessionDefaultRedisSerializer that implements RedisSerializer<Object>.

笔者也忍不住也就试了一番,spring容器配置:

    <bean id="springSessionDefaultRedisSerializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
  • 1

可以跑起来,不过遇见下列代码就头疼了:

    @RequestMapping("/setS")
public String setSession(HttpServletRequest req) {
Long value = 1l;
req.getSession().setAttribute("key", value);
return null;
} @RequestMapping("/getS")
public String getSession(HttpServletRequest req) {
Long value = (Long)req.getSession().getAttribute("key");
System.out.println(value);
return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

触发异常:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Long
  • 1

去redis中找具体存储数据:

 7) "sessionAttr:key"
8) "1"
  • 1
  • 2

了然,JSON的虚化列方式明了是明了,但是连个java类型都没有限定说明,虽然我们可以去获取对象前判断类型再转化,但是也就丧失了spring-session使用的关键优点-不需要修改已有代码。

三、JSP下的session设置坑

这是一个比较难发现的问题,有些朋友在spring-session上手之后可能一帆风顺就没有去关注spring-session的基本工作流程,但是在spring-session何时将放入session中的对象序列化存储到redis中如果没有一个清晰的认识可能会进入这个坑。 
如果你在你的代码中有这样存入session对象: 
controller中:

@RequestMapping("/setS")
public String setSession(HttpServletRequest req) {
Map<Object,Object> value = new HashMap<Object,Object>();
req.getSession().setAttribute("valid", value);
return "test";
} @RequestMapping("/getS")
public String getSession(HttpServletRequest req) {
Map<Object,Object> value=(Map<Object,Object>)req.getSession().getAttribute("valid");
System.out.println(value.keySet().size());
return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

test.jsp中:

<c:forEach var="v" begin="1" end="100" step="1">
<%-- 任意长文本--%>
<c:set target="${valid}" property="${v}" value="1"/>y
</c:forEach>
  • 1
  • 2
  • 3
  • 4

最终getS打印的size未必是100,本地测试在jetty下正常,在tomcat下就不是100了,可能只有一半,只存入了一半数据?调试得出问题所在,看图: 
 
结论是当JSP输出到buffer的时候如果buffer满了的话将flushBuffer,同时将由spring-session提交session,即写入redis。spring-session源码中: 
RedisOperationsSessionRepository中部分方法:

public void setAttribute(String attributeName, Object attributeValue)
{
this.cached.setAttribute(attributeName, attributeValue);
this.delta.put(RedisOperationsSessionRepository.getSessionAttrNameKey(attributeName), attributeValue);
flushImmediateIfNecessary();
} private void saveDelta()
{
...序列化存入redis
this.delta = new HashMap(this.delta.size());
...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

由此可见当flushBuffer的时候会将delta重置,此时已经将对象序列化入redis中了,不会管之后这里边的对象会不会改变,除非再次delta.put(...) 
最终解决办法及建议:在完成对象修改之后最后将需要设置进session中的对象setAttribute...

四、redis键空间通知与对象序列化serialVersionUID改变之后

笔者对spring-session的redis键空间通知方面的接触始于一个开发问题,如果在一个web集群下单个web容器中修改了将放入session中的对象的class结构(或者说是serialVersionUID改变),那么在其它web容器在有session失效中,该容器将触发异常-无法反序列化session对象,最终通过抓包发现,当其它服务器有session的重新登录的时候该web容器向redis发出了hgetall (旧sessionid)命令。也就是说web集群中所有的session失效时,其它所有服务器将接受到通知并反序列化这个session中的所有对象。结合spring-session文档可以找到:

Firing SessionDeletedEvent or SessionExpiredEvent is made available through the SessionMessageListener which listens to Redis Keyspace events. In order for this to work, Redis Keyspace events for Generic commands and Expired events needs to be enabled. For example: 
redis-cli config set notify-keyspace-events Egx

很明显spring-session实现Session删除事件和Session过期事件需要依赖redis的键空间通知功能,spring-session的源码中直接默认执行这句redis命令(是的,直接执行config set,笔者对这种直接侵入的做法实不敢苟同)。当然会有朋友想到实现这种全局通知对redis的性能影响得多大,在高并发访问情况下尤其影响吧。对此笔者翻阅了spring-session的在线文档,没有一个清晰的解释。只有提到如果使用者的redis是一个安全较高的公共redis(比如阿里云的),可以这样配置:

<util:constant
static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/>
  • 1
  • 2

笔者也同样搜索了很久,大多博文对这个的解释模棱两可。通过测试得出这句配置只是说让spring-session不去直接执行config set,并没有说可以不用redis的键空间通知,而且如果你的程序已经运行过了,即已经对redis设置过这个键空间通知了,不去手动在redis种清除这个config那么将依然收到键空间通知。如果需要彻底不接受redis键空间通知,可首先加入这句配置,然后去redis中将键空间通知config置空(笔者只是实现了不通知,是否有其它程序上的问题没有全面的测试,为了稳定暂时只能按照spring-session默认的来)。对于能否取消redis键空间通知以提高web集群的性能笔者没有再深入spring-session源码,有经验的读者可以给予下意见。

五、题外:spring升级后的一个问题

spring-session要求spring基础库版本在3.2.14以上,如果你的web应用的spring框架版本是3.0.x,那么在升级至该版本时,请升级关键配置: 
将过时的配置:

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" >
  • 1

修改为:

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" >
  • 1

否则在文件上传至返回json的请求处理器时,web容器将上传成功但返回http错误码,更多的关于这个过时配置的bug读者可自行Google。

六、spring-session测试性能简说

笔者在实际LR压力测试监控过程中,spring-session调用redis方面性能还是挺稳定的,粗略得出的数据有在最高5000人并发访问web集群时redis占用内存6G,redis连接数600(当然这只是个参考,具体web应用的session存储内容不同),redis和web容器在同一个内网的环境下前端打开速度与没有共享session情况下未发生明显的延迟,建议保证redis服务器与web应用间的数据联通速率。对测试数据感兴趣的开发者推荐使用Apache ab工具进行压测。

Spring Session实现Session共享下的坑与建议的更多相关文章

  1. Apache shiro集群实现 (六)分布式集群系统下的高可用session解决方案---Session共享

    Apache shiro集群实现 (一) shiro入门介绍 Apache shiro集群实现 (二) shiro 的INI配置 Apache shiro集群实现 (三)shiro身份认证(Shiro ...

  2. Spring Boot2 系列教程(二十八)Spring Boot 整合 Session 共享

    这篇文章是松哥的原创,但是在第一次发布的时候,忘了标记原创,结果被好多号转发,导致我后来整理的时候自己没法标记原创了.写了几百篇原创技术干货了,有一两篇忘记标记原创进而造成的一点点小小损失也能接受,不 ...

  3. Spring Session解决Session共享

    1. 分布式Session共享   在分布式集群部署环境下,使用Session存储用户信息,往往出现Session不能共享问题.   例如:服务集群部署后,分为服务A和服务B,当用户登录时负载到服务A ...

  4. 使用Spring Session和Redis解决分布式Session跨域共享问题

    http://blog.csdn.net/xlgen157387/article/details/57406162 使用Spring Session和Redis解决分布式Session跨域共享问题

  5. 170222、使用Spring Session和Redis解决分布式Session跨域共享问题

    使用Spring Session和Redis解决分布式Session跨域共享问题 原创 2017-02-27 徐刘根 Java后端技术 前言 对于分布式使用Nginx+Tomcat实现负载均衡,最常用 ...

  6. Spring Boot 使用 Redis 共享 Session 代码示例

    参考资料 博客:spring boot + redis 实现session共享 1. 新建 Maven 工程 我新建 spring-boot-session-redis maven 工程 2. 引入 ...

  7. 修改记录-优化后(springboot+shiro+session+redis+ngnix共享)

    1.普通用户实现redis共享session 1.配置 #cache指定缓存类型 spring.cache.type=REDIS #data-redis spring.redis.database=1 ...

  8. 【转载】spring mvc 使用session

    http://home.51.com/gaoyangboy/diary/item/10036382.html Spring2.5 访问 Session 属性的四种策略 Posted on 2008-1 ...

  9. Spring MVC中Session的正确用法<转>

    Spring MVC是个非常优秀的框架,其优秀之处继承自Spring本身依赖注入(Dependency Injection)的强大的模块化和可配置性,其设计处处透露着易用性.可复用性与易集成性.优良的 ...

随机推荐

  1. Java 如何解析由String类型拼接的XML格式

    String xml = new String(a);打印的xml 的值是 <?xml version= 1.0 encoding=gb2312?><weighData>< ...

  2. crontab 参数详解

    crontab 参数 用户所建立的crontab文件中,每一行都代表一项任务,每行的每个字段代表一项设置,它的格式共分为六个字段,前五段是时间设定段,第六段是要执行的命令段,格式如下: minute ...

  3. Pandas稀疏数据

    当任何匹配特定值的数据(NaN/缺失值,尽管可以选择任何值)被省略时,稀疏对象被“压缩”. 一个特殊的SparseIndex对象跟踪数据被“稀疏”的地方. 这将在一个例子中更有意义. 所有的标准Pan ...

  4. netty的异常分析 IllegalReferenceCountException refCnt: 0

    netty的异常 IllegalReferenceCountException refCnt: 0 这是因为Netty有引用计数器的原因,自从Netty 4开始,对象的生命周期由它们的引用计数(ref ...

  5. IOS-第三方(SDWebImage)

    SDWebImage ReadMe.md 文档 附:SDWebImage框架github下载地址:https://github.com/rs/SDWebImage注1:该文章简单翻译了SDWebIma ...

  6. WCF基础:绑定(三)

    在WCF绑定体系中,绑定创建绑定元素,绑定元素创建绑定监听器/绑定工厂,绑定监听器/绑定工厂创建信道. WCF中绑定是有多个信道相连组成的信道栈,在这个信道栈中必须包含传输信道和编码信道,而且传输信道 ...

  7. SPI笔记

    sclk(serial clock):串行时钟 MOSI(master out slave input)  (master   主机) (slave 从机) MISO(master int slave ...

  8. Android界面View及ViewGroup学习 《转载》

    View及ViewGroup类关系 Android View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的. View ...

  9. pg_rewind 源端时间线发生改变 同步失败

    master-standby情况下,发生如下行为: 1.master停掉后,standby做为新的master(可能存在部分事物没有同步到standby中). 2.新master运行过程中出错,进行恢 ...

  10. Linux使用lrzsz上传下载文件

    1.当然是要安装lrzsz这个程序 yum -y install lrzsz 2.该程序的使用 //下载文件 sz filepath.ext//文件会默认下载到系统的Downloads目录 //上传文 ...