`

Redis实战之session共享

 
阅读更多

当线上集群时候,会出现session共享问题。

当线上集群时候,会出现session共享问题。

虽然Tomcat提供了session copy的功能,但是缺点比较明显:

1:当Tomcat多的时候,session需要大量同步到多台集群上,占用内网宽带

2:同一个用户session,需要在多个Tomcat中都存在,浪费内存空间。

 

如果要替换掉Tomcat的session共享,替代方案应该满足:

1:数据共享

2:内存存储

3:key\value结构

32513e73b243ec122ea183b9683cc5de.png

基于Redis实现共享session登录

本文由凯哥Java(gz#h:kaigejava),个人blog:www#kaigejava#.com。发布于凯哥Java个人blog

再来回顾下将验证码保存在session中业务流程

0f7ad2613d0c2945a3521a444ac48373.png

 

我们在session中存放的是:session.setAttribute("code", code); 因为session的特点,每次访问都是一个新的sessionId.我们可以直接使用code作为key.思考:那么如果换成了Redis,还能使用code作为可以吗?

 

将用户信息存放在session中流程:

949a02a0dc7a161f878989367a1dc0a0.png

用户信息在session中存放:session.setAttribute("user", user); 同样思考:那么如果换成了Redis,还能使用user作为可以吗?

将code和user信息存放在Redis中,流程如下:

c1fb88b9bdb73463963897e7f29433bf.png

验证码数据结构是:string类型的

用户对象数据类型是:hash类型的

根据上面分析,我们修改原来代码:

需要考虑的是:Redis的key规则、过期时间

1:发送验证码的时候,将验证码存放到Redis中时候,需要考虑过期时间。其核心代码如下:

6a6187b3f0126a1ec1705e9ace4ca0a6.png

stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

2:用户登录的时候,将校验验证码以及用户信息存放到Redis中后,返回token

需要考虑的:

1:token不能重复

2:用户过期时间

3:登录成功后,要将token返回给前端

4:用户只要访问,Redis中的过期时间就要延长-在拦截器中处理的

用户登录核心代码修改:

e44e9661d6e2f3d8f501dba50a448535.png

//2.1:校验验证码是否正确
        //String code = (String) session.getAttribute("code");
        String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if (StringUtils.isEmpty(code) || !code.equals(loginForm.getCode())) {
            return Result.fail("验证码错误!");
        }
        //2.2:根据手机号查询,如果不存在,创建新用户
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("id", "phone", "nick_name");
        queryWrapper.eq("phone", phone);
        User user = this.getOne(queryWrapper);
        if (Objects.isNull(user)) {
            log.info("新建用户");
            //新建用户
            user = createUserWithPhone(phone);
        }
        //2.3:保存用户到session中
        UserDTO userDTO = new UserDTO();
        userDTO.setId(user.getId());
        userDTO.setIcon(user.getIcon());
        userDTO.setNickName(user.getNickName());
 
        //session.setAttribute("user", userDTO);
        //2.3.1:获取随机的token,作为用户登录的令牌
        String token = UUID.randomUUID().toString(true);
        //2.3.2:将用户以hash类型存放到Redis中==》将user对象转换成map
        //user对象里有非string类型的字段,用这个方法会报错的
        // Map<String,Object> userMap = BeanUtil.beanToMap(userDTO);
        Map<String,Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>()
        , CopyOptions.create()
        .setIgnoreNullValue(true)
        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
 
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_TOKEN_KEY+token,userMap);
        //LOGIN_USER_TOKEN_TTL
        stringRedisTemplate.expire(LOGIN_USER_TOKEN_KEY+token,LOGIN_USER_TOKEN_TTL,TimeUnit.MINUTES);
        //2.3.3: 将token返回
        return Result.ok(token);

需要注意:

在使用stringRedisTemplate存放hash对象的时候,对象中所有的key只能是string类型,如果存在非string类型会报错的。所以这里使用了hootool的BeanUtil工具类:

Map<String,Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>()
        , CopyOptions.create()
        .setIgnoreNullValue(true)
        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));

拦截器修改代码:

因为拦截器是我们自定义的,所以不能被spring容器管理的,RedisTemplate就不能自动注入了。我们就使用有参构造器,传递:

243cb12436424394a79705bf719ff850.png

public class LoginRedisInterceptor implements HandlerInterceptor {
 
    private StringRedisTemplate stringRedisTemplate;
 
    /**
     * 因为这个类不能被spring管理,所以不能直接注入RedisTemplate对象。通过构造函数传递
     * @param stringRedisTemplate
     */
    public LoginRedisInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1:从请求中获取到token
        String token = request.getHeader("authorization");
        if(StringUtils.isEmpty(token)){
            response.setStatus(401);
            return false;
        }
        //2:基于token获取redis中用户对象
        String key = LOGIN_USER_TOKEN_KEY+token;
        Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //3:判断
        if(userMap.isEmpty()){
            response.setStatus(401);
            return false;
        }
        //将map转对象
        UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(user);
        //刷新token的过期时间
        stringRedisTemplate.expire(key,LOGIN_USER_TOKEN_TTL, TimeUnit.MINUTES);
        return true;
    }
 
 
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

总结:

在使用Redis替换session的时候,需要考虑的问题:

1:选择合适的数据结构

2:选择合适的key

3:选择合适的存储粒度

 

 

虽然Tomcat提供了session copy的d功能,但是缺点比较明显:

1:当Tomcat多的时候,session需要大量同步到多台集群上,占用内网宽带

2:同一个用户session,需要在多个Tomcat中都存在,浪费内存空间

如果要替换掉Tomcat的session共享,替代方案应该满足:

1:数据共享

2:内存存储

3:key\value结构

32513e73b243ec122ea183b9683cc5de.png

基于Redis实现共享session登录

本文由凯哥Java(gz#h:kaigejava),个人blog:www#kaigejava#.com。发布于凯哥Java个人blog

再来回顾下将验证码保存在session中业务流程

0f7ad2613d0c2945a3521a444ac48373.png


我们在session中存放的是:session.setAttribute("code", code); 因为session的特点,每次访问都是一个新的sessionId.我们可以直接使用code作为key.思考:那么如果换成了Redis,还能使用code作为可以吗?



将用户信息存放在session中流程:

949a02a0dc7a161f878989367a1dc0a0.png

用户信息在session中存放:session.setAttribute("user", user); 同样思考:那么如果换成了Redis,还能使用user作为可以吗?

将code和user信息存放在Redis中,流程如下:

c1fb88b9bdb73463963897e7f29433bf.png

验证码数据结构是:string类型的

用户对象数据类型是:hash类型的

根据上面分析,我们修改原来代码:

需要考虑的是:Redis的key规则、过期时间

1:发送验证码的时候,将验证码存放到Redis中时候,需要考虑过期时间。其核心代码如下:

6a6187b3f0126a1ec1705e9ace4ca0a6.png

stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

2:用户登录的时候,将校验验证码以及用户信息存放到Redis中后,返回token

需要考虑的:

1:token不能重复

2:用户过期时间

3:登录成功后,要将token返回给前端

4:用户只要访问,Redis中的过期时间就要延长-在拦截器中处理的

用户登录核心代码修改:

e44e9661d6e2f3d8f501dba50a448535.png

//2.1:校验验证码是否正确
        //String code = (String) session.getAttribute("code");
        String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if (StringUtils.isEmpty(code) || !code.equals(loginForm.getCode())) {
            return Result.fail("验证码错误!");
        }
        //2.2:根据手机号查询,如果不存在,创建新用户
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("id", "phone", "nick_name");
        queryWrapper.eq("phone", phone);
        User user = this.getOne(queryWrapper);
        if (Objects.isNull(user)) {
            log.info("新建用户");
            //新建用户
            user = createUserWithPhone(phone);
        }
        //2.3:保存用户到session中
        UserDTO userDTO = new UserDTO();
        userDTO.setId(user.getId());
        userDTO.setIcon(user.getIcon());
        userDTO.setNickName(user.getNickName());

        //session.setAttribute("user", userDTO);
        //2.3.1:获取随机的token,作为用户登录的令牌
        String token = UUID.randomUUID().toString(true);
        //2.3.2:将用户以hash类型存放到Redis中==》将user对象转换成map
        //user对象里有非string类型的字段,用这个方法会报错的
        // Map<String,Object> userMap = BeanUtil.beanToMap(userDTO);
        Map<String,Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>()
        , CopyOptions.create()
        .setIgnoreNullValue(true)
        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));

        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_TOKEN_KEY+token,userMap);
        //LOGIN_USER_TOKEN_TTL
        stringRedisTemplate.expire(LOGIN_USER_TOKEN_KEY+token,LOGIN_USER_TOKEN_TTL,TimeUnit.MINUTES);
        //2.3.3: 将token返回
        return Result.ok(token);
需要注意:

在使用stringRedisTemplate存放hash对象的时候,对象中所有的key只能是string类型,如果存在非string类型会报错的。所以这里使用了hootool的BeanUtil工具类:

Map<String,Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>()
        , CopyOptions.create()
        .setIgnoreNullValue(true)
        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
拦截器修改代码:

因为拦截器是我们自定义的,所以不能被spring容器管理的,RedisTemplate就不能自动注入了。我们就使用有参构造器,传递:

243cb12436424394a79705bf719ff850.png

public class LoginRedisInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    /**
     * 因为这个类不能被spring管理,所以不能直接注入RedisTemplate对象。通过构造函数传递
     * @param stringRedisTemplate
     */
    public LoginRedisInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1:从请求中获取到token
        String token = request.getHeader("authorization");
        if(StringUtils.isEmpty(token)){
            response.setStatus(401);
            return false;
        }
        //2:基于token获取redis中用户对象
        String key = LOGIN_USER_TOKEN_KEY+token;
        Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //3:判断
        if(userMap.isEmpty()){
            response.setStatus(401);
            return false;
        }
        //将map转对象
        UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(user);
        //刷新token的过期时间
        stringRedisTemplate.expire(key,LOGIN_USER_TOKEN_TTL, TimeUnit.MINUTES);
        return true;
    }


    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
总结:

在使用Redis替换session的时候,需要考虑的问题:

1:选择合适的数据结构

2:选择合适的key

3:选择合适的存储粒度



 

分享到:
评论

相关推荐

    秒杀实战 redis nginx 分布式session-secKill.zip

    配置文件可能包括Nginx的配置,用于设置反向代理、限流规则以及Session共享。而README文档则会提供详细的步骤,教我们如何编译、安装依赖、启动服务,并进行测试验证。 总的来说,这个项目为我们提供了一个实战的...

    SSM REDIS集成实战

    此外,你还可以使用Redis存储Session,提高多服务器环境下的Session共享能力。配置`sessionManager`和`httpSessionStrategy`,将Session存储到Redis中: ```xml &lt;bean id="sessionManager" class="org.spring...

    Redis实战中文版

    5. **Session共享**:在分布式系统中,可以将用户的Session存储在Redis中,实现跨服务器共享。 了解和掌握Redis实战技巧,需要学习以下内容: 1. **安装配置**:了解如何在不同的操作系统上安装和配置Redis。 2. **...

    Redis实战.pdf

    另外,Redis还可以用于实现限流和Session共享等。 总之,《Redis实战》这本书详细介绍了如何利用Redis解决实际问题,无论你是初学者还是经验丰富的开发者,都能从中受益。通过学习和实践,你将能够熟练掌握Redis的...

    spring session实现session共享

    Spring Session的出现,就是为了克服这个问题,它支持将Session数据持久化到各种后端存储,如Redis、MongoDB、Hazelcast等,从而实现跨服务器的Session共享。 **Spring Session核心概念** 1. **Session Registry**...

    SSM+Shiro+redis项目实例

    此外,优化Shiro的会话管理,比如利用Redis实现分布式Session,可以解决多服务器环境下的Session共享问题。 总的来说,SSM+Shiro+Redis项目实例是一个很好的学习资源,涵盖了Java Web开发的多个重要方面,对于想要...

    Redis源码解读与项目精品设计实战

    而在集群环境中,为了实现Session共享,可以通过Spring Session和Redis的结合来实现。这种配置允许应用服务器的多个实例共享同一存储的会话数据。 此外,Redis 6.X版本引入了多线程机制,这可以进一步提升Redis的...

    Spring Boot项目利用Redis实现session管理实例

    Spring Boot项目利用Redis实现session管理实例 在现代网络服务中,session(会话)是非常重要的一概念,也是一定要实现的组件。...这种方式可以让我们更好地管理session,并且可以在分布式系统中实现session的共享。

    实战HTTP Handler (4) -- 与Web程序共享Session 源码

    在HTTP Handler中实现Session共享可以提高应用程序的效率和用户体验。 首先,了解HTTP Handler。HTTP Handler是ASP.NET中的一种IHttpHandler接口实现,用于处理特定类型的HTTP请求。与ASP.NET Page生命周期相比,...

    redis学习文档

    《Redis实战》这本书可能涵盖了Redis的基础操作,如字符串、哈希、列表、集合和有序集合等基本数据类型,以及如何利用这些数据类型进行数据存储和检索。它还可能涉及了Redis的事务处理、发布订阅、持久化机制(RDB和...

    阿里P7大牛实战演练到源码透析——分布式环境session丢失爬坑记

    4. **集中式存储:** 使用第三方中间件(如Redis)作为集中式存储,实现Session数据的统一管理和共享。 #### 四、实战演练与源码透析 1. **实战演练:** - **模拟环境搭建:** 构建一个简单的分布式系统环境,...

    SpringBoot整合Shiro,实现从数据库加载权限、权限的动态更新、Session共享

    在本项目中,我们将深入探讨如何利用SpringBoot与Shiro结合,实现从数据库加载权限、权限的动态更新以及Session共享。 1. **Shiro简介**: Apache Shiro是一个轻量级的安全框架,它提供了认证、授权、会话管理和...

    Redis开发电商网站.rar

    7. **分布式Session**:电商网站通常采用多服务器部署,Redis可作为Session共享存储,确保用户在不同服务器间切换时仍能保持登录状态。 8. **商品库存扣减**:利用Redis的事务或lua脚本实现库存的原子性扣减,保证...

    Java企业级电商项目架构演进之路 Tomcat集群与Redis分布式-课程章节1

    学员将学习Nginx的负载均衡策略,比较其优缺点,并亲手搭建Tomcat集群,体验集群部署的优势和随之带来的新挑战,如 session 共享和故障转移等。 5. **Redis分布式**: 作为高性能的分布式缓存,Redis在电商项目中...

    ssm框架+shiro+redis缓存(某课网代码)

    将用户的session信息存储在Redis中,解决单机环境下session共享问题,提升多服务器环境下的用户体验。 3. **Spring配置**:Spring配置文件会定义Bean的依赖注入,包括SSM框架的各个组件,如DataSource、...

    spring boot实战第8章 redis-spring_boot3.zip

    此外,Spring Session项目允许利用Redis来存储用户的会话信息,实现跨服务器的会话共享。 总结起来,Spring Boot和Redis的结合提供了强大的工具集,使得开发人员能够便捷地利用Redis的各种功能。通过合理配置和使用...

    spring-session.zip

    通过使用Redis,你可以实现跨多个服务器的session共享,并利用其高可用性和可扩展性。 另一方面,MongoDB是一个文档型数据库,适合存储结构化的和半结构化的数据。"spring-data-mongodb"库则提供了与MongoDB的集成...

    43_说说你们的分布式session方案是啥?怎么做的?.zip

    3. **共享存储**:使用集中式的缓存服务(如Redis、Memcached)来存储Session数据。服务器不再直接管理Session,而是将Session数据保存在缓存中,通过Key-Value的方式访问。这种方式可以实现高效的数据访问,并且...

    shiro会话共享实战

    2、环境搭建 ①配置java7、gradle2.2.1、redis3.0.4环境; ②在java代码中配置org.demo.shiro.rediscache.RedisClient.redisServerIp

Global site tag (gtag.js) - Google Analytics