秒杀项目系列之四: 分布式会话(session)

  1. 会话管理
  • 基于cookie传输sessionid: java tomcat容器session实现
    移动端开发很多时候会把cookie禁用掉.

  • 基于token传输,类似sessionid: java代码session实现

  1. 分布式会话
  • 基于cookie传输sessionid: java tomcat容器session实现迁移到redis
  • 基于token传输类似sessionid: java代码session实现迁移到redis
  1. 基于cookie传输sessionid的单机版redis实现
  • 安装redis
    教程很多,而且安装过程和其他软件一样,不再阐述

  • pom.xml文件中引入如下依赖

    <!--    引入springboot对redis的依赖-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!--    spring将自己对session的管理方式存储在redis中-->
    <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-data-redis</artifactId>
      <version>2.0.5.RELEASE</version>
    </dependency>
    
  • RedisConfig.java

    // 把当前类对象作为一个bean存入spirng容器
    @Component
    // maxInactiveIntervalInSeconds: 设置 Session 失效时间(默认1800,即30分钟,单位秒)
    // 使用Redis Session 之后,原SpringBoot的server.session.timeout属性不再生效。
    @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
    public class RedisConfig {
          
          
    }
    
  • 配置application.yml文件

    spring:
      # 配置springboot对redis的依赖
      redis:
        host: 127.0.0.1
        port: 6379
        # 一共16个数据库,从0开始,使用database 10
        database: 10
        jedis:
          pool:
            # 最大连接数为50
            max-active: 50
            # 最少空闲连接数为20
            min-idle: 20
    
  • 将几个Model实现序列化(因为存入redis的数据默认是序列化的,也可以修改redis的默认存储方式为json,json方式也是最好的,该处暂时使用序列化)

    // 举例,OrderModel、ItemModel、PromoModel同理
    public class UserModel implements Serializable{
          
          
        @NotBlank(message = "用户名不能为空")
        private String name;
        @NotNull(message = "性别不能不填写")
        private Byte gender;
        @NotNull(message = "年龄不能不填写")
        @Min(value = 0, message = "年龄必须大于0岁")
        @Max(value = 150, message = "年龄必须小于150岁")
        private Integer age;
        @NotBlank(message = "手机号不能为空")
        private String telephone;
        private String registerMode;
        private String thirdPartyId;
        @NotBlank(message = "密码不能为空")
        private String encrptPassword;
    }
    
  • 单机版测试

    • 修改gethost.js

      var g_host="localhost:9000";
      
    • 切换redis数据库到10

      select 10
      
    • 前端访问服务器,点击获取otp信息,即将手机号和验证码放入session中,由于配置了上述两个依赖,便将session信息放入redis中

      • 访问服务器
        在这里插入图片描述
      • 获取验证码后,手机号和验证码放入session代码
      // 用户获取otp短信接口
      @PostMapping(value = "/getotp",consumes = {
              
              CONTENT_TYPE_FORMED})
      public CommonReturnType getOtp(@RequestParam("telephone") String telephone){
              
              
          //需要按照一定的规则生成OTP验证码
          Random random = new Random();
          int randomInt = random.nextInt(99999);
          randomInt += 10000;
          String otpCode = String.valueOf(randomInt);
      
          //将OTP验证码同对应用户的手机号关联,使用httpSession的方式绑定手机号和OTPCODE(键值对)
          httpServletRequest.getSession().setAttribute(telephone, otpCode);
      
          //将OTP验证码通过短信通道发送给用户,省略
          System.out.println("telephone= " + telephone + "& otpCode= " + otpCode);
          return CommonReturnType.create(null);
      }
      
      • 查看redis中的信息
      127.0.0.1:6379> select 10
      OK
      127.0.0.1:6379[10]> keys *
      (empty array)
      127.0.0.1:6379[10]> keys *
      1) "spring:session:sessions:expires:be5ffd6a-aaaf-4ac3-bed1-7347bdf5e3f2"
      2) "spring:session:sessions:be5ffd6a-aaaf-4ac3-bed1-7347bdf5e3f2"
      3) "spring:session:expirations:1611133500000"
      
  1. 基于cookie传输sessionid的redis分布式实现(redis和数据库使用同一台服务器)
  • 在原先的数据库服务器上安装redis

  • 在项目根目录下通过mvn clean package命令打包,然后上传到应用服务器1和应用服务器2(实际应用中关掉一台服务器更新一台服务器jar包,这样服务器还能对外提供服务)

  • 修改redis.conf文件

    # redis绑定到本机ip,也就是说只有本机才能访问redis
    bind redis服务器ip
    
  • 更改应用服务器1和应用服务器2的application.properties配置文件,绑定redis所在服务器ip

    spring.redis.host=redis_server_ip
    
  • 打开redis和数据库服务器的6379端口

    # 打开6379端口
    firewall-cmd --add-port=6379/tcp --permanent
    # 重新加载
    firewall-cmd --reload
    # 查看
    firewall-cmd --list-all
    
  • 重新启动应用服务器1和应用服务器2的jar脚本文件、nginx服务器、数据库服务器、redis

    ./deploy.sh &
    ./nginx
    systenctl start mysql
    redis-server &
    
  • 测试分布式环境登陆下单
    在这里插入图片描述
    可见每次都能下单成功,如果不使用redis存储session的话,由于是分布式环境,应用服务器采用负载均衡的模式,当用户登陆时session信息存储到一台服务器上,当用户请求比如下单时可能会路由到另一台服务器上,由于另一台服务器上没有session信息,所以在下单前的验证发现用户没有登陆,于是请求失败.
    使用redis存储session,请求时session信息会从redis中获取,所以每次都能请求成功.

  1. 基于token传输(类似sessionid)的java代码分布式实现(基于redis)
  • token传输的本质
    当用户登录的时候服务器端返回给客户端一个token(令牌),并将token和对应的用户信息存入到redis中,以及将token放到localStorage(类似cookie)中,当用户请求的时候,首先会从localStorage中取token,判断是否为空,如果不为空,则会根据token从redis中获取用户信息,从而判断用户是否登录.
    即使从localStorage中获取token值,用户信息也有可能为空,因为redis中设定过期时间为1h.token一个小时后就失效了.

  • UserController.java

    @Resource
    private RedisTemplate redisTemplate;
    
    // 用户登陆接口
    @PostMapping(value = "/login", consumes = {
          
          CONTENT_TYPE_FORMED})
    public CommonReturnType login(@RequestParam("telephone") String telephone, @RequestParam("password") String password) throws BusinessException, NoSuchAlgorithmException {
          
          
        ...
        // 使用token的方法: 将原来的方法修改成若用户登陆验证成功,将对应的登陆信息和登陆凭证一起存入redis中
        // 使用UUID生成登陆凭证token
        String uuidToken = UUID.randomUUID().toString().replace("-", "");
        // 建立token和用户登陆态之间的联系
        redisTemplate.opsForValue().set(uuidToken, userModel);
        // uuidToken的过期时间为1h
        redisTemplate.expire(uuidToken, 1, TimeUnit.HOURS);
        return CommonReturnType.create(uuidToken);
    }
    
  • login.html

    $.ajax({
        success:function (data) {
            if(data.status == "success"){
                alert("登陆成功");
                // 前端返回一个CommonReturnType对象,CommonReturnType有status和data两个参数,data.data就是token值
                var token = data.data;
                // localStorage是html5新出的,比cookie更加安全且存储容量更大,没有4KB限制,现在基本上不用cookie,而用localStorage来完成对应的操作,
                // 本质上是key-value的json对象的数据库
                window.localStorage["token"] = token;
                window.location.href="listitem.html";
            }else{
                alert("登陆失败,原因为"+data.data.errMsg);
            }
        },
        error:function (data) {
            alert("登陆失败,原因为"+data.responseText);
        }
    });
    
  • OrderController.java

    @PostMapping(value = "/createorder", consumes = {
          
          CONTENT_TYPE_FORMED})
    public CommonReturnType createOrder(@RequestParam("itemId") Integer itemId,
                                        @RequestParam(value = "promoId", required = false) Integer promoId,
                                        @RequestParam("amount") Integer amount) throws BusinessException {
          
          
        // 使用token的方法
        String token = httpServletRequest.getParameterMap().get("token")[0];
        if(StringUtils.isEmpty(token)){
          
          
            throw new BusinessException(EmBusinessError.USER_NOT_EXIST, "用户未登陆,不能下单");
        }
        UserModel userModel = (UserModel)redisTemplate.opsForValue().get(token);
        if(userModel == null){
          
          
            throw new BusinessException(EmBusinessError.USER_NOT_EXIST, "用户未登陆,不能下单");
        }
        OrderModel orderModel = orderService.createOrder(userModel.getId(), itemId, promoId, amount);
        return CommonReturnType.create(null);
    }
    
  • 打包及部署到服务器与之前操作类似.

猜你喜欢

转载自blog.csdn.net/qq_26496077/article/details/112858086