原创

Java架构师方案—分布式session基于redis的共享机制(附完整项目代码)

1. 读这篇文章你可以学到的东西

认识问题

这篇文章介绍的是服务在集群部署的情况下,用户的请求会被负载均衡到不同的服务器,但是用户的登入数据、权限数据、操作数据等session数据只会存在一个服务器的内存中。若用户下一次请求被转发到其他没有session数据的服务器上,那用户就还需要再重新登入,重新在网站上进行业务操作后创建操作数据。

alt

用户体验极差

如果用户session数据只在一个服务器上,那用户访问另外服务器上服务时候,就会要求用户重新登入和业务操作。多次要求用户进行重新登入和业务操作,这样的体验是个灾难。

解决方案

将用户的session状态从服务中剥离出来,放到一个独立的session服务器上,这里选择redis来建session服务器。集群服务有关状态数据的操作都要和redis服务器交互。这种分布式session方案我们成为共享session服务器方案。

alt

2. 基于redis存储共享session方案

正如上面的架构图所示,所有的应用服务器关于session数据保存和获取操作都会与session服务器交互。用户每次请求的权限验证都与session服务器中的数据进行比对。如果用户已经登入了,那登入状态数据就会保存在session服务器,用户访问其他服务器时就不需要重新登入

本文的方案,笔者提供了一个完整的demo项目,项目地址看末尾获取方式。

SpringBoot项目引入redis依赖

<!-- 引入 redis 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
</dependency>

redis配置

springboot项目默认使用StringRedisTemplate,StringRedisTemplate和RedisTemplate不同,前者比较简单,只需要配置redis参数就行。

第一种

在application.yml文件中配置下面的参数。

spring:
    redis:
        #redis数据库地址
        host: localhost
        port: 6379
        password: 
        timeout: 1000
        #redis数据库索引,默认0
        database: 1

第二种

在application.properties文件中配置下面的参数。

# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=5000

使用StringRedisTemplate

直接在service实现类中注入StringRedisTemplate。

@Autowired
private StringRedisTemplate redisTemplate;

到这里,我们项目就能正常使用redis服务了。

redis操作实现类RedisOperator

该类简单封装了String类型的redis命令:set、get, ttl等,业务逻辑可以直接调用。

/**
 * 使用redisTemplate的操作实现类
 * @author YI
 * @date 2018-6-12 10:54:28
 */
@Component
public class RedisOperator {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // Key(键),简单的key-value操作

    /**
     * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
     * 
     * @param key
     * @return
     */
    public long ttl(String key) {
        return redisTemplate.getExpire(key);
    }
......//因类代码比较多,这里就不全部展示了,读者可以查阅项目demo

3. 登入数据存放到session服务器

用户登入

用户登入接口逻辑:

  1. 获取username和password数据,与数据库保存的用户密码比对。
  2. 用户名和密码比对正确后,生成唯一的uuid作为用户的登入状态session数据。
  3. 通过redis操作类将session数据保存在redis中,key由username生成,并设置30分钟有效时间。
  4. 将用户username保存到cookie中。
@PostMapping(value = "loginCheck")
@ResponseBody
public RestResponseBo loginCheck(@RequestParam String username,
                @RequestParam String password,
                HttpServletRequest request,
                HttpServletResponse response) {
      if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){

          return RestResponseBo.fail("用户名或者密码不能为空!");
  }
  if(!username.equals("admin") || !password.equals("admin"))
  {

      return RestResponseBo.fail("用户名或者密码不正确!");
  }

  String token = StrUtil.uuid();

  //存放唯一的 token 并设置过期时间
  operator.set(JdkApiInterceptor.USER_REDIS_SESSION + ":" + username, token, REDIS_TIMEOUT);

  //设置用户  密码  token等信息 
  operator.set(username, username+":"+password+":"+token);

  //用户浏览器会存放两种cookie: userToken,userId。
  CookieUtil.addCookie("userName", username);

  return RestResponseBo.ok();//
}

拦截器JdkApiInterceptor校验用户登入状态

拦截逻辑:

  1. 先从请求cookie 中获取username。
  2. 再查询redis中该用户的登入session数据。
  3. 不为空则是已登入,为空则是未登入或登入状态过期。
  4. 未登入或登入状态过期则重定向到登入页面。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object arg2) throws Exception {
    //获取用户cookies
    String userName = CookieUtil.getCookie("userName");

    //放开登入接口
//        String uri = request.getRequestURI();
//        logger.info("请求uri:" + uri);
//        
//        if(uri.equals("loginCheck"))
//            return true;
//        
    logger.info(" ======= 拦截UserId:" + userName);
    //用户id和token都不为空
    if (!StringUtils.isEmpty(userName)) {

        //根据userid生成唯一key从redis中查出唯一token
        String uniqueToken = redis.get(USER_REDIS_SESSION + ":" + userName);
        logger.info("拦截uniqueToken:" + uniqueToken);

        //如果唯一token为空 ,则拦截url重定向到登入页面
        if (StringUtils.isEmpty(uniqueToken)) {
            response.sendRedirect("/login");
            returnErrorResponse(response, "请登录...");

            return false;
        }
    //用户id和token有一个为空,则重定向登入页面
    } else {
        response.sendRedirect("/login");
        returnErrorResponse(response,"请登录...");
        return false;
    }

    return true;
}

到此,就能实现不同服务器的session共享了。

4. 拦截器注册及配置

注册

使用@Configuration修饰创建配置类WebSecurityConfig

@Configuration
public class WebSecurityConfig implements WebMvcConfigurer {
...

创建拦截器bean对象,放入ioc容器。

@Bean
public JdkApiInterceptor JdkApiInterceptor(){
    return new JdkApiInterceptor();
}

配置

配置拦截器要拦截的url和不拦截的url

/**
 * 拦截器注册 设置拦截接口
 * @param registry
 */
@Override
public void addInterceptors(InterceptorRegistry registry) {

    registry.addInterceptor(JdkApiInterceptor()).addPathPatterns("/**")//设置拦截所有的路径
            //排除的路径:静态资源路径。防止被JdkApiInterceptor拦截
            .excludePathPatterns("/loginCheck","/login","/error",
                    "/js/**", "/css/**", "/imag/**");
}

这里排除拦截的url中,除了静态资源路径和登入接口,为什么还有/error路径呢?具体原因请查看我这篇文章:WebMvcConfigurer的excludePathPatterns配置 "失效" 问题

5. 简单谈谈我对redis共享session方案的看法

基于redis的共享session方案能解决用户重复登入的问题,带来更好的用户体验,是分布式架构经典的解决方案,但同时也带了问题,redis服务器的高可用问题以及远处通信的性能问题。当然集群下session问题解决方案有很多种,这只是其中之一,日后我还会进一步以文章形式与大家探讨

如果大家觉得这篇文章对你学习架构有帮助的话,还请点赞,在看支持一下。github项目也记得点个星哦!

完整的demo项目,请关注公众号“前沿科技bot“并发送"redis共享session"获取。

alt

正文到此结束
本文目录