SpringSession实现Session共享下的坑与建议
相信⽤过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 = SessionIds(request);
if(result.isEmpty()){
String jsessionId = (Attribute(SessionForURLFilter.OLDEST_URL_SESSION_ID_ATTRIBUTE_NAME);
if ((jsessionId != null) && (!"".im())))
{
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 = RequestedSessionId();
if ((jsessionId != null) && (!"".im()))){
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
<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
spring到底是干啥的
⼆、除了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.dis.serializer.GenericJackson2JsonRedisSerializer"/> 1
可以跑起来,不过遇见下列代码就头疼了:
@RequestMapping("/setS")
public String setSession(HttpServletRequest req) {
Long value = 1l;
return null;
}
@RequestMapping("/getS")
public String getSession(HttpServletRequest req) {
Long value = (Session().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>();
return "test";
}
@RequestMapping("/getS")
public String getSession(HttpServletRequest req) {
Map<Object,Object> value=(Map<Object,Object>)Session().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.SessionAttrNameKey(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中的对象。
四、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.fig.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.hod.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⼯具进⾏压测。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。