分布式Session:基于Spring-Session 和 Redis实现

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 分布式Session:基于Spring-Session 和 Redis实现

分布式Session-前言

在Web项目开发中,会话管理是一个很重要的部分,用于存储与用户相关的数据。

通常是由符合session规范的容器来负责存储管理

也就是一旦容器关闭,重启会导致会话失效。

因此打造一个高可用性的系统,必须将session管理从容器中独立出来


共享Session问题


       HttpSession是通过Servlet容器创建和管理的,像Tomcat/Jetty都是保存在内存中的。而如果我们把web服务器搭建成分布式的集群,然后利用LVS或Nginx做负载均衡,那么来自同一用户的Http请求将有可能被分发到两个不同的web站点中去。那么问题就来了,如何保证不同的web站点能够共享同一份session数据呢?

       最简单的想法就是把session数据保存到内存以外的一个统一的地方,例如Memcached/Redis等数据库中。那么问题又来了,如何替换掉Servlet容器创建和管理HttpSession的实现呢?

    (1)设计一个Filter,利用HttpServletRequestWrapper,实现自己的 getSession()方法,接管创建和管理Session数据的工作spring-session就是通过这样的思路实现的。

    (2)利用Servlet容器提供的插件功能,自定义HttpSession的创建和管理策略,并通过配置的方式替换掉默认的策略。不过这种方式有个缺点,就是需要耦合Tomcat/Jetty等Servlet容器的代码

      这方面其实早就有开源项目了,例如memcached-session-manager,以及tomcat-redis-session-manager。暂时都只支持Tomcat6/Tomcat7。


而这实现方案有很多种,下面简单介绍下:

第一种:分布式Session实现方案


是使用容器扩展来实现,大家比较容易接受的是通过容器插件来实现,

比如基于Tomcat的tomcat-redis-session-manager基于Jetty的jetty-session-redis等等。

好处是对项目来说是透明的,无需改动代码。不过前者目前还不支持Tomcat 8,或者说不太完善。

个人觉得由于过于依赖容器,一旦容器升级或者更换意味着又得从新来过。

并且代码不在项目中,对开发者来说维护也是个问题。


第二种:分布式Session实现方案


是自己写一套会话管理的工具类,包括Session管理和Cookie管理,在需要使用会话的时候都从自己的工具类中获取,

而工具类后端存储可以放到Redis中。

很显然这个方案灵活性最大,但开发需要一些额外的时间。

并且系统中存在两套Session方案,很容易弄错而导致取不到数据。


第三种:分布式Session实现方案【本文即是这种方式】


是使用框架的会话管理工具,也就是本文要说的spring-session,可以理解是替换了Servlet那一套会话管理,

既不依赖容器,又不需要改动代码,并且是用了spring-data-redis那一套连接池,可以说是最完美的解决方案。

当然,前提是项目要使用Spring Framework才行。

Spring-session官网的特性介绍

Features

Spring Session provides the following features:

API and implementations for managing a user's session

  1. HttpSession - allows replacing the HttpSession in an application container (i.e. Tomcat) neutral way
  2. Clustered Sessions - Spring Session makes it trivial[平常的,平凡的; 不重要的] to support clustered sessions without being tied to an application container specific solution.
  3. Multiple Browser Sessions - Spring Session supports managing multiple users' sessions in a single browser instance (i.e. multiple authenticated accounts similar to Google).
  4. RESTful APIs - Spring Session allows providing session ids in headers to work with RESTful APIs
  5. WebSocket - provides the ability to keep the HttpSession alive when receiving WebSocket messages
  1. 环境准备
  1. redis安装版本:redis-3.2.5.tar.gz
  2. JDK版本:1.7+
  3. Spring版本:4.1+
  4. Spring-session版本:1.2.1.RELEASE
  1.  Maven依赖
<!-- 指定版本号 -->
<properties>
  <!-- Spring -->
  <spring.version>4.1.5.RELEASE</spring.version>
  <!-- JEDIS -->
  <jedis.version>2.8.1</jedis.version>
  <!-- Spring Session -->
  <spring-session-data-redis.version>1.2.1.RELEASE</spring-session-data-redis.version>
</properties>
<!-- 使用Spring Session做分布式会话管理 -->
<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
  <version>${spring-session-data-redis.version}</version>
</dependency>
<!-- JEDIS -->
<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>${jedis.version}</version>
</dependency>


web.xml中配置Session过滤器【springSessionRepositoryFilter


<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="WebApp_ID" version="3.0">
    <display-name>CloudPayment</display-name>
    <!-- 欢迎页面 -->
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.htm</welcome-file>
        <welcome-file>index.jsp</welcome-file>
        <welcome-file>default.html</welcome-file>
        <welcome-file>default.htm</welcome-file>
        <welcome-file>default.jsp</welcome-file>
    </welcome-file-list>
    <!-- Log4jConfigListener必须要在Spring的Listener之前 -->
    <!-- log4jConfigLocation -->
    <context-param>
        <param-name>log4jConfigLocation</param-name>
        <param-value>classpath:/config/log4j.xml</param-value>
    </context-param>
    <!-- Spring刷新Log4j配置文件变动的间隔,单位为毫秒 -->
    <context-param>
        <param-name>log4jRefreshInterval</param-name>
        <param-value>10000</param-value>
    </context-param>
    <!-- 加载spring容器 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:spring/applicationContext-*.xml
        </param-value>
    </context-param>
    <!-- Log4jConfigListener -->
    <listener>
        <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
    </listener>
    <!-- ContextLoaderListener -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!-- 在spring的普通类取session和request对象 -->
    <listener>
        <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
    </listener>
    <!-- Spring MVC配置【*.do】开始 -->
    <servlet>
        <servlet-name>SpringMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/applicationContext-springMVC.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>SpringMVC</servlet-name>
        <url-pattern>*.action</url-pattern>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
    <!-- Spring MVC配置【*.do】结束 -->
    <!-- 一般情况下Spring Session的过滤器要放在第一位 -->
    <filter>
        <filter-name>springSessionRepositoryFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSessionRepositoryFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>ERROR</dispatcher>
    </filter-mapping>
    <!-- Post乱码解决 (要放在最后) -->
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>


编写applicationContext-jedis.xml,并配置RedisHttpSessionConfiguration


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 配置文件 -->
    <context:property-placeholder location="classpath:config/redis.properties" ignore-unresolvable="true"/>
    <!-- 支持注解 -->
    <!-- <context:annotation-config /> -->
    <!-- 组件扫描 -->
    <context:component-scan base-package="com.newcapec.cloudpay.dao"/>
    <!-- 将session放入redis -->
    <!-- <context:annotation-config /> -->
    <!-- RedisHttpSessionConfiguration -->
    <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
        <!-- 超时时间,默认是1800秒 -->
        <property name="maxInactiveIntervalInSeconds" value="${redis.session.maxInactiveIntervalInSeconds}"/>
    </bean>
    <!-- 连接池配置 -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!-- 最大连接数 -->
        <property name="maxTotal" value="${redis.maxTotal}"/>
        <!-- 最大空闲连接数 -->
        <property name="maxIdle" value="${redis.maxIdle}"/>
        <!-- 每次释放连接的最大数目 -->
        <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}"/>
        <!-- 释放连接的扫描间隔(毫秒) -->
        <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}"/>
        <!-- 连接最小空闲时间 -->
        <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}"/>
        <!-- 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放 -->
        <property name="softMinEvictableIdleTimeMillis" value="${redis.softMinEvictableIdleTimeMillis}"/>
        <!-- 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1 -->
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}"/>
        <!-- 在获取连接的时候检查有效性, 默认false -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}"/>
        <!-- 在空闲时检查有效性, 默认false -->
        <property name="testWhileIdle" value="${redis.testWhileIdle}"/>
        <!-- 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true -->
        <property name="blockWhenExhausted" value="${redis.blockWhenExhausted}"/>
    </bean>
    <!--JedisConnectionFactory -->
    <bean id="jedisConnectionFactory"
          class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="${redis.host}"/>
        <property name="port" value="${redis.port}"/>
        <property name="password" value="${redis.password}"/>
        <property name="timeout" value="${redis.timeout}"/>
        <property name="usePool" value="${redis.usePool}"/>
        <property name="poolConfig" ref="jedisPoolConfig"/>
    </bean>
    <!-- 序列化 -->
    <bean id="stringRedisSerializer"
          class="org.springframework.data.redis.serializer.StringRedisSerializer">
    </bean>
    <!-- redisTemplate配置,redisTemplate是对Jedis的对redis操作的扩展,有更多的操作,封装使操作更便捷 -->
    <!-- <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
        p:connection-factory-ref="jedisConnectionFactory" p:keySerializer-ref="stringRedisSerializer"
        p:valueSerializer-ref="stringRedisSerializer" p:hashKeySerializer-ref="stringRedisSerializer"
        p:hashValueSerializer-ref="stringRedisSerializer"> </bean> -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
        <property name="keySerializer">
            <bean
                    class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="valueSerializer">
            <bean
                    class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
        </property>
        <property name="hashKeySerializer">
            <bean
                    class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="hashValueSerializer">
            <bean
                    class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
        </property>
    </bean>
    <!-- 静态注入 -->
    <!-- <bean name="testRedisTemplate" class=""> <property name="redisTemplate"
        ref="redisTemplate" /> </bean> -->
    <!-- jedis客户端单机版(配置用户名和密码)-->
    <bean id="redisClient" class="redis.clients.jedis.JedisPool">
        <constructor-arg index="0" ref="jedisPoolConfig"/>
        <constructor-arg index="1" value="${redis.host}"/>
        <constructor-arg index="2" value="${redis.port}" type="int"/>
        <constructor-arg index="3" value="${redis.timeout}"
                         type="int"/>
        <constructor-arg index="4" value="${redis.password}"/>
    </bean>
</beans>


redis相关配置信息


1. ##############################【Redis-配置】【BGN】###################
2. #++++++++++++【本地】【BGN】++++++++++
3. #++++++++新支付平台开发++++++++++
4. ##  redis【本地--主机地址-新支付平台开发】
5. redis.host = 192.168.112.XXX
6. ##  redis【本地--端口号-新支付平台开发】
7. redis.port = 6379
8. # redis【本地--登录密码-新支付平台开发】
9. redis.password = XXX
10. #++++++++++++【本地】【END】++++++++++
11. ## redis【超时时间】
12. redis.timeout=100000
13. ## redis【是否使用连接池】
14. redis.usePool=true
15. ## redis【最大连接数】
16. redis.maxTotal=300
17. ## redis【最大空闲连接数】
18. redis.maxIdle=10
19. ## redis【每次释放连接的最大数目】
20. redis.numTestsPerEvictionRun=1024
21. ## redis【释放连接的扫描间隔(毫秒)】
22. redis.timeBetweenEvictionRunsMillis=30000
23. ## redis【连接最小空闲时间】
24. redis.minEvictableIdleTimeMillis=1800000
25. ## redis【 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放】
26. redis.softMinEvictableIdleTimeMillis=10000
27. ## redis【获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1】
28. redis.maxWaitMillis=1000
29. ## redis【在获取连接的时候检查有效性, 默认false】
30. redis.testOnBorrow=true
31. ## redis【在空闲时检查有效性, 默认false】
32. redis.testWhileIdle=true
33. ## redis【连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true】
34. redis.blockWhenExhausted=true
35. ##############################【Redis-配置】【END】###################
36. ##############################【Redis-Spring-Session】【BGN】###################
37. ## 超时时间,7200秒,项目中没有使用拦截器,尽量延长session时间,降低由session超时引起的异常
38. redis.session.maxInactiveIntervalInSeconds=7200
39. ##############################【Redis-Spring-Session】【END】###################
  1.   验证:

第一种方式:

使用redis-cli就可以查看到session key了,且浏览器Cookie中的jsessionid已经替换为session

127.0.0.1:6379> KEYS *

1) "spring:session:expirations:1440922740000"

2) "spring:session:sessions:35b48cb4-62f8-440c-afac-9c7e3cfe98d3"

第二种方式:

通过 redis-desktop-manager【下载地址:https://redisdesktop.com/download】查看

图片.png

 异常解决:  报错springSessionRepositoryFilter不存在

官网解释:

图片.png

问题:

org.apache.catalina.core.StandardContext filterStart

严重: Exception starting filter springSessionRepositoryFilter

org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'springSessionRepositoryFilter' is defined

at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:698)

at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1175)

at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:284)

at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)

at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1060)

at org.springframework.web.filter.DelegatingFilterProxy.initDelegate(DelegatingFilterProxy.java:326)

at org.springframework.web.filter.DelegatingFilterProxy.initFilterBean(DelegatingFilterProxy.java:235)

at org.springframework.web.filter.GenericFilterBean.init(GenericFilterBean.java:199

 解决:

上图中说明了是步骤1创建的springSessionRepositoryFilter

仔细检查web.xml。确保springSessionRepositoryFilter先于启动其他flter创建了即可。

注意

spring-session要求Redis Server版本不低于2.8

 


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
NoSQL Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
7天前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
40 16
|
8天前
|
消息中间件 NoSQL Java
Spring Boot整合Redis
通过Spring Boot整合Redis,可以显著提升应用的性能和响应速度。在本文中,我们详细介绍了如何配置和使用Redis,包括基本的CRUD操作和具有过期时间的值设置方法。希望本文能帮助你在实际项目中高效地整合和使用Redis。
23 1
|
26天前
|
存储 缓存 NoSQL
分布式架构下 Session 共享的方案
【10月更文挑战第15天】在实际应用中,需要根据具体的业务需求、系统架构和性能要求等因素,选择合适的 Session 共享方案。同时,还需要不断地进行优化和调整,以确保系统的稳定性和可靠性。
|
27天前
|
缓存 NoSQL Java
Spring Boot与Redis:整合与实战
【10月更文挑战第15天】本文介绍了如何在Spring Boot项目中整合Redis,通过一个电商商品推荐系统的案例,详细展示了从添加依赖、配置连接信息到创建配置类的具体步骤。实战部分演示了如何利用Redis缓存提高系统响应速度,减少数据库访问压力,从而提升用户体验。
69 2
|
1月前
|
NoSQL Redis 数据库
计数器 分布式锁 redis实现
【10月更文挑战第5天】
47 1
|
1月前
|
NoSQL 算法 关系型数据库
Redis分布式锁
【10月更文挑战第1天】分布式锁用于在多进程环境中保护共享资源,防止并发冲突。通常借助外部系统如Redis或Zookeeper实现。通过`SETNX`命令加锁,并设置过期时间防止死锁。为避免误删他人锁,加锁时附带唯一标识,解锁前验证。面对锁提前过期的问题,可使用守护线程自动续期。在Redis集群中,需考虑主从同步延迟导致的锁丢失问题,Redlock算法可提高锁的可靠性。
73 4
|
14天前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
29 0
|
1月前
|
缓存 NoSQL 算法
面试题:Redis如何实现分布式锁!
面试题:Redis如何实现分布式锁!