秦明 發表於 2025-5-24 09:15:00

秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis +RabbitMQ +MyBatis-Plus +Maven + Linux + Jmeter )-01

<h1 id="秒杀高并发方案-介绍">秒杀/高并发方案-介绍</h1>
<p>@</p><div class="toc"><div class="toc-container-header">目录</div><ul><li>秒杀/高并发方案-介绍</li><li>秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis +RabbitMQ +MyBatis-Plus +Maven + Linux + Jmeter )-01</li><li>分布式会话 Session 共享<ul><li>加密密码设置</li><li>注解自定义校验</li></ul></li><li>全局异常处理定义</li><li>分布式 Session共享<ul><li>分布式 Session 共享 详解</li><li>分布式 Session 解决方案 1-SpringSession 实现分布式 Session</li><li>分布式 Session 解决方案 2-直接将用户信息统一放入 Redis</li></ul></li><li>最后:</li></ul></div><p></p>
<h1 id="秒杀高并发解决方案落地实现-技术栈-springbootmysql--redis-rabbitmq-mybatis-plus-maven--linux--jmeter--01">秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis +RabbitMQ +MyBatis-Plus +Maven + Linux + Jmeter )-01</h1>
<ul>
<li>Github:China-Rainbow-sea/seckill: 秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis + RabbitMQ +MyBatis-Plus + Maven + Linux + Jmeter )</li>
<li>Gitee:seckill: 秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis + RabbitMQ +MyBatis-Plus + Maven + Linux + Jmeter )</li>
</ul>
<ol>
<li>秒杀/ 高并发,其实主要解决<strong>两个问题:</strong>一个是<strong>并发读</strong>,一个是<strong>并发写</strong>。</li>
<li>并发读的核心优化理念就是<strong>尽量减少用户到 DB 来 “读” 数据,或者让他们读更少的数据,并发写的处理原则也是一样的</strong>。</li>
<li>针对秒杀系统需要做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。</li>
<li>系统架构降要满足高可用:<strong>流量符合预期时要稳定,要保证秒杀活动顺利完成,即秒杀商品顺利被卖出去,这个是最基本的前提</strong>。</li>
<li>系统保证数据的一致性:<strong>就是秒杀 10 个商品,那就只能成交 10 个商品,多一个少一个都不行。一旦库存不对,就要承担损失</strong>。</li>
<li>系统要满足高性能:<strong>也就是系统的性能要足够高,需要支撑大流量,不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就 “快” 了</strong>。</li>
<li>秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键,对应的方案:比如:<strong>页面缓存方案,Redis 预减库存 / 内存标记与隔离,请求的削峰(RabbitMA / 异步请求),分布式 Session 共享等处理</strong>。</li>
</ol>
<p><strong>用户登录 sql 脚本</strong></p>
<pre><code class="language-sql">DROP TABLE IF EXISTS `seckill_user`;
CREATE TABLE `seckill_user`
(
    `id`            BIGINT(20)   NOT NULL COMMENT '用户 ID, 设为主键, 唯一 手机号',
    `nickname`      VARCHAR(255) NOT NULL DEFAULT '',
    `password`      VARCHAR(32)NOT NULL DEFAULT '' COMMENT 'MD5(MD5(pass 明 文 + 固 定
salt)+salt)',
    `slat`            VARCHAR(10)NOT NULL DEFAULT '',
    `head`            VARCHAR(128) NOT NULL DEFAULT '' COMMENT '头像',
    `register_date`   DATETIME            DEFAULT NULL COMMENT '注册时间',
    `last_login_date` DATETIME            DEFAULT NULL COMMENT '最后一次登录时间',
    `login_count`   INT(11)               DEFAULT '0' COMMENT '登录次数',
    PRIMARY KEY (`id`)
) ENGINE = INNODB
DEFAULT CHARSET = utf8mb4;
</code></pre>
<h1 id="分布式会话-session-共享">分布式会话 Session 共享</h1>
<h2 id="加密密码设置">加密密码设置</h2>
<p>MD5 的加密的依赖包:</p>
<p><strong>这里我们解读一下密码的设计!!:</strong></p>
<p>登录为例讲解:</p>
<p>传统方式:</p>
<p>客户端——&gt; password 明文——&gt;后端(md5(password 明文)) == db 中存放的 password 是否一致) :这种传统方式存在的问题:</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090540707-72498008.png"></p>
<p>传统方式改进的方式 1:客户端——&gt; md5(password 明文)——&gt;后端(md5(md5(password 明文)) ) == db 中存放的 password 是否一致) 。这样就算黑客拦截到了我们前端发送的信息,也是被加密的,所以无妨。</p>
<p>我们可以在传统方式的基础上,在 <strong>进行一个加盐</strong>上的处理。让密码更加安全一些。</p>
<p>传统方式改进的方式 2:客户端——&gt; md5(password 明文+<strong>salt1(固定的盐)</strong>)——&gt;后端(md5(md5(password 明文+<strong>salt1(固定的盐)+<strong><strong><font style="color: rgba(223, 42, 63, 1)">salt2(从数据库当中获取的盐,不同用户对应的盐也不同</font></strong></strong>)</strong>)) ) == db 中存放的 password 是否一致) 。</p>
<p>注意:是<strong>对称加密</strong>的。</p>
<p>Md5 加密所需的相关依赖 。</p>
<pre><code class="language-xml">      &lt;!--md5依赖--&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;commons-codec&lt;/groupId&gt;
            &lt;artifactId&gt;commons-codec&lt;/artifactId&gt;
            &lt;version&gt;1.15&lt;/version&gt;
      &lt;/dependency&gt;

      &lt;dependency&gt;
            &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;
            &lt;artifactId&gt;commons-lang3&lt;/artifactId&gt;
            &lt;version&gt;3.11&lt;/version&gt;
      &lt;/dependency&gt;
</code></pre>
<p>Junit Jupiter: Junit Jupiter 提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心。内部 包含了一个测试引擎,用于在 Junit Platform 上运行</p>
<pre><code class="language-xml">      &lt;dependency&gt;
            &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;
            &lt;artifactId&gt;commons-lang3&lt;/artifactId&gt;
            &lt;version&gt;3.11&lt;/version&gt;
      &lt;/dependency&gt;
      
      &lt;dependency&gt;
            &lt;groupId&gt;org.junit.jupiter&lt;/groupId&gt;
            &lt;artifactId&gt;junit-jupiter-api&lt;/artifactId&gt;
            &lt;version&gt;5.7.2&lt;/version&gt;
            &lt;scope&gt;compile&lt;/scope&gt;
      &lt;/dependency&gt;
</code></pre>
<p><strong>加密密码工具类编写</strong></p>
<hr>
<pre><code class="language-java">package com.rainbowsea.seckill.utill;

import org.apache.commons.codec.digest.DigestUtils;


/**
* MD5 加密工具类,根据前面密码设计方案提供相应的方法
*/
public class MD5Util {


    /**
   * 第一次加密所需的盐。
   */
    private static final String SALT = "UCmP7xHA";


    /**
   * MD5 加密
   *
   * @param src 要加密的字符串
   * @return String
   */
    public static String md5(String src) {
      return DigestUtils.md5Hex(src);
    }


    /**
   * 加密加盐,完成的是 md5(pass+salt1)
   *
   * @param inputPass 输入的密码
   * @return String
   */
    public static String inputPassToMidPass(String inputPass) {
      String str = "" + SALT.charAt(0) + inputPass + SALT.charAt(6);
      return md5(str);
    }


    /**
   * 这个盐随机生成,成的是 md5( md5(pass+salt1)+salt2)
   *
   * @param midPass 加密的密码
   * @param salt    从数据库获取到不同用户加密的盐
   * @return String
   */
    public static String midPassToDBPass(String midPass, String salt) {
      String str = salt.charAt(1) + midPass + salt.charAt(5);
      return md5(str);
    }


    /**
   * 进行两次加密加盐 最后存到数据库的 md5( md5(pass+salt1)+salt2)
   * salt1是前端进行的salt2 是后端进行的随机生成
   */
    public static String inputPassToDBPass(String inputPass, String salt) {
      String midPass = inputPassToMidPass(inputPass);
      String dbPass = midPassToDBPass(midPass, salt);
      return dbPass;
    }
}

</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090539148-1574320046.png"></p>
<p><strong>validation参数校验</strong></p>
<pre><code class="language-xml">      &lt;!--validation参数校验--&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-validation&lt;/artifactId&gt;
            &lt;version&gt;2.4.5&lt;/version&gt;
      &lt;/dependency&gt;
</code></pre>
<hr>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090539177-892203256.png"></p>
<p><strong>用户登录逻辑编写:</strong></p>
<pre><code class="language-java">package com.rainbowsea.seckill.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.seckill.mapper.UserMapper;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.utill.MD5Util;
import com.rainbowsea.seckill.utill.ValidatorUtil;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @author huo
* @description 针对表【seckill_user】的数据库操作Service实现
* @createDate 2025-04-24 15:38:01
*/
@Service
public class UserServiceImpl extends ServiceImpl&lt;UserMapper, User&gt;
      implements UserService {


    @Resource
    private UserMapper userMapper;


    /**
   * 登录校验
   *
   * @param loginVo登录时发送的信息
   * @param requestrequest
   * @param response response
   * @return RespBean
   */
    @Override
    public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {

      String mobile = loginVo.getMobile();
      String password = loginVo.getPassword();

      // 判断手机号/id,和密码是否为空
      if (!StringUtils.hasText(mobile) || !StringUtils.hasText(password)) {
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      }

      // 判断手机号是否合格
      if (!ValidatorUtil.isMobile(mobile)) {
            return RespBean.error(RespBeanEnum.MOBILE_ERROR);
      }

      // 查询DB,判断用户是否存在
      User user = userMapper.selectById(mobile);
      if (null == user) {
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      }


      // 如果用户存在,则对比密码!
      // 注意:我们从 LoginVo 取出的密码是中间密码(即客户端经过一次加密加盐处理的密码)
      if (!MD5Util.midPassToDBPass(password, user.getSlat()).equals(user.getPassword())) {
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      }

      // 登录成功
      return RespBean.success(user);
    }
}





</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538895-1468442954.png"></p>
<pre><code class="language-java">package com.rainbowsea.seckill.controller;


import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Slf4j
@Controller
@RequestMapping("/login")
public class LoginController {


    @Resource
    private UserService userService;


    /**
   * 用户登录
   *
   * @return 返回登录页面
   */
    @RequestMapping("/toLogin")
    public String toLogin() {

      return "login"; // 到templates/login.html
    }

    /**
   * 登录功能
   */
    @RequestMapping("/doLogin")
    @ResponseBody
    public RespBean doLogin
    (@Valid LoginVo loginVo, HttpServletRequest request,
   HttpServletResponse response) {
      log.info("{}", loginVo);
      return userService.doLogin(loginVo, request, response);
    }
}



</code></pre>
<hr>
<h2 id="注解自定义校验">注解自定义校验</h2>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090539010-1999000619.png"></p>
<p><strong>自定义校验器:</strong></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537302-903562749.png"></p>
<pre><code class="language-java">package com.rainbowsea.seckill.validator;


import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* 开发一个自定义的注解:替换如下,登录校验时的代码
* &lt;p&gt;
* &lt;p&gt;
* // 判断手机号/id,和密码是否为空
* if (!StringUtils.hasText(mobile) || !StringUtils.hasText(password)) {
* return RespBean.error(RespBeanEnum.LOGIN_ERROR);
* }
* &lt;p&gt;
* // 判断手机号是否合格
* if (!ValidatorUtil.isMobile(mobile)) {
* return RespBean.error(RespBeanEnum.MOBILE_ERROR);
* }
*/

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER,
      TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
public @interface IsMobile {

    String message() default "手机号码格式错误";

    boolean required() default true;

    Class&lt;?&gt;[] groups() default {}; // 默认参数

    Class&lt;? extends Payload&gt;[] payload() default {}; //默认参数
}

</code></pre>
<pre><code class="language-java">package com.rainbowsea.seckill.validator;


import com.rainbowsea.seckill.utill.ValidatorUtil;
import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
* 我们自拟定注解 IsMobile(手机号是否正确) 的校验规则
*/
public class IsMobileValidator implements ConstraintValidator&lt;IsMobile, String&gt; {

    private boolean required = false;

    @Override
    public void initialize(IsMobile constraintAnnotation) {
      // 初始化
      required = constraintAnnotation.required();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
      //必填
      if (required) {
            return ValidatorUtil.isMobile(value);
      } else {//非必填
            if (!StringUtils.hasText(value)) {
                return true;
            } else {
                return ValidatorUtil.isMobile(value);
            }
      }
    }
}

</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538932-1460738566.png"></p>
<pre><code class="language-java">package com.rainbowsea.seckill.vo;


import com.rainbowsea.seckill.validator.IsMobile;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotNull;

/**
* 接收用户登录时,发送的信息(mobile,password)
*/

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginVo {

    // 添加 validation 组件后使用
    @NotNull
    @IsMobile//自拟定注解
    private String mobile;


    @Length(min = 32)
    @NotNull
    private String password;
}

</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537125-2033823150.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537880-1066981344.png"></p>
<h1 id="全局异常处理定义">全局异常处理定义</h1>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090539291-510629132.png"></p>
<pre><code class="language-java">package com.rainbowsea.seckill.exception;


import com.rainbowsea.seckill.vo.RespBeanEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* 全局异常处理类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException {

    private RespBeanEnum respBeanEnum;
}

</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538906-2042448414.png"></p>
<pre><code class="language-java">package com.rainbowsea.seckill.exception;


import com.rainbowsea.seckill.vo.RespBean;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.validation.BindException;

/**
* 全局异常处理定义
*/
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
   * 处理所有的异常
   *
   * @param e 异常对象
   * @return RespBean
   */
    @ExceptionHandler(Exception.class)
    public RespBean ExceptionHandler(Exception e) {
      //如果是全局异常,正常处理
      if (e instanceof GlobalException) {
            GlobalException ex = (GlobalException) e;
            return RespBean.error(ex.getRespBeanEnum());
      } else if (e instanceof BindException) {// BindException 绑定异常
            // 如果是绑定异常 :由于我们自定义的注解只会在控制台打印错误信息,想让改信息传给前端。
            // 需要获取改异常 BindException,进行打印
            BindException ex = (BindException) e;
            RespBean respBean = RespBean.error(RespBeanEnum.BING_ERROR);
            respBean.setMessage(" 参 数 校 验 异 常 ~ : " +
                  ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
            return respBean;
      }
      return RespBean.error(RespBeanEnum.ERROR);
    }
}

</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538505-173948387.png"></p>
<pre><code class="language-java">package com.rainbowsea.seckill.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.seckill.exception.GlobalException;
import com.rainbowsea.seckill.mapper.UserMapper;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.utill.MD5Util;
import com.rainbowsea.seckill.utill.ValidatorUtil;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @author huo
* @description 针对表【seckill_user】的数据库操作Service实现
* @createDate 2025-04-24 15:38:01
*/
@Service
public class UserServiceImpl extends ServiceImpl&lt;UserMapper, User&gt;
      implements UserService {


    @Resource
    private UserMapper userMapper;


    /**
   * 登录校验
   *
   * @param loginVo登录时发送的信息
   * @param requestrequest
   * @param response response
   * @return RespBean
   */
    @Override
    public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {

      String mobile = loginVo.getMobile();
      String password = loginVo.getPassword();

      // 判断手机号/id,和密码是否为空
      //if (!StringUtils.hasText(mobile) || !StringUtils.hasText(password)) {
      //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      //}

      // 判断手机号是否合格
      //if (!ValidatorUtil.isMobile(mobile)) {
      //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      //}

      // 查询DB,判断用户是否存在
      User user = userMapper.selectById(mobile);
      if (null == user) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      }


      // 如果用户存在,则对比密码!
      // 注意:我们从 LoginVo 取出的密码是中间密码(即客户端经过一次加密加盐处理的密码)
      if (!MD5Util.midPassToDBPass(password, user.getSlat()).equals(user.getPassword())) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      }

      // 登录成功
      return RespBean.success(user);
    }
}





</code></pre>
<p>完成测试 , 运行项目,访问 http://localhost:8080/login/toLogin</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538814-1554810979.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090540136-980242694.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538769-952765375.png"></p>
<h1 id="分布式-session--共享">分布式 Session共享</h1>
<p>编写工具类:</p>
<p>第一个工具类:用于生成唯一的 UUID ,作为一个唯一的 <code>userTicket</code></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538509-182750059.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537129-2107796627.png"></p>
<pre><code class="language-java">package com.rainbowsea.seckill.utill;

import java.util.UUID;


/**
* 用户生产唯一的 UUID ,作为 session
*/
public class UUIDUtil {

    public static String uuid() {
      // 默认下: 生成的字符串形式 xxxx-yyyy-zzz-ddd
      // 把 UUID中的-替换掉,所以使用 replace("-", "")
      return UUID.randomUUID().toString().replace("-", "");
    }
}

</code></pre>
<p>这是一个工具类, 直接使用即可. (该工具了,可以让我们更方便的操作 cookie , 比如编码处理等等</p>
<pre><code class="language-java">package com.rainbowsea.seckill.utill;


import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;


/**
* 这是一个工具类, 直接使用即可. (该工具了,可以让我们更方便的操作 cookie , 比如编码
* 处理等等.
*/

public class CookieUtil {
    /**
   * 得到Cookie的值, 不编码
   *
   * @param request
   * @param cookieName
   * @return
   */
    public static String getCookieValue(HttpServletRequest request, String
            cookieName) {
      return getCookieValue(request, cookieName, false);
    }

    /**
   * 得到Cookie的值,
   *
   * @param request
   * @param cookieName
   * @return
   */
    public static String getCookieValue(HttpServletRequest request, String
            cookieName, boolean isDecoder) {
      Cookie[] cookieList = request.getCookies();
      if (cookieList == null || cookieName == null) {
            return null;
      }
      String retValue = null;
      try {
            for (int i = 0; i &lt; cookieList.length; i++) {
                if (cookieList.getName().equals(cookieName)) {
                  if (isDecoder) {
                        retValue = URLDecoder.decode(cookieList.getValue(),
                              "UTF-8");
                  } else {
                        retValue = cookieList.getValue();
                  }
                  break;
                }
            }
      } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
      }
      return retValue;
    }

    /**
   * 得到Cookie的值,
   *
   * @param request
   * @param cookieName
   * @param encodeString
   * @return
   */
    public static String getCookieValue(HttpServletRequest request, String
            cookieName, String encodeString) {
      Cookie[] cookieList = request.getCookies();
      if (cookieList == null || cookieName == null) {
            return null;
      }
      String retValue = null;
      try {
            for (int i = 0; i &lt; cookieList.length; i++) {
                if (cookieList.getName().equals(cookieName)) {
                  retValue = URLDecoder.decode(cookieList.getValue(),
                            encodeString);
                  break;
                }
            }
      } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
      }
      return retValue;
    }

    /**
   * 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
   */
    public static void setCookie(HttpServletRequest request, HttpServletResponse
            response, String cookieName, String cookieValue) {
      setCookie(request, response, cookieName, cookieValue, -1);
    }

    /**
   * 设置Cookie的值 在指定时间内生效,但不编码
   */
    public static void setCookie(HttpServletRequest request, HttpServletResponse
            response, String cookieName, String cookieValue, int cookieMaxage) {
      setCookie(request, response, cookieName, cookieValue, cookieMaxage,
                false);
    }


    /**
   * 设置Cookie的值 不设置生效时间,但编码
   */
    public static void setCookie(HttpServletRequest request, HttpServletResponse
            response, String cookieName,
                                 String cookieValue, boolean isEncode) {
      setCookie(request, response, cookieName, cookieValue, -1, isEncode);
    }

    /**
   * 设置Cookie的值 在指定时间内生效, 编码参数
   */
    public static void setCookie(HttpServletRequest request, HttpServletResponse
            response, String cookieName,
                                 String cookieValue, int cookieMaxage, boolean
                                       isEncode) {
      doSetCookie(request, response, cookieName, cookieValue, cookieMaxage,
                isEncode);
    }

    /**
   * 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
   */
    public static void setCookie(HttpServletRequest request, HttpServletResponse
            response, String cookieName,
                                 String cookieValue, int cookieMaxage, String
                                       encodeString) {
      doSetCookie(request, response, cookieName, cookieValue, cookieMaxage,
                encodeString);
    }

    /**
   * 删除Cookie带cookie域名
   */
    public static void deleteCookie(HttpServletRequest request,
                                    HttpServletResponse response,
                                    String cookieName) {
      doSetCookie(request, response, cookieName, "", -1, false);
    }

    /**
   * 设置Cookie的值,并使其在指定时间内生效
   *
   * @param cookieMaxage cookie生效的最大秒数
   */
    private static final void doSetCookie(HttpServletRequest request,
                                          HttpServletResponse response,
                                          String cookieName, String cookieValue,
                                          int cookieMaxage, boolean isEncode) {
      try {
            if (cookieValue == null) {
                cookieValue = "";
            } else if (isEncode) {
                cookieValue = URLEncoder.encode(cookieValue, "utf-8");
            }
            Cookie cookie = new Cookie(cookieName, cookieValue);
            if (cookieMaxage &gt; 0) {
                cookie.setMaxAge(cookieMaxage);
            }
//            if (null != request) {// 设置域名的cookie
//                String domainName = getDomainName(request);
//                if (!"localhost".equals(domainName)) {
//                  cookie.setDomain(domainName);
//                }
//            }
            cookie.setPath("/");
            response.addCookie(cookie);
      } catch (Exception e) {
            e.printStackTrace();
      }
    }

    /**
   * 设置Cookie的值,并使其在指定时间内生效
   *
   * @param cookieMaxage cookie生效的最大秒数
   */
    private static final void doSetCookie(HttpServletRequest request,
                                          HttpServletResponse response,
                                          String cookieName, String cookieValue,
                                          int cookieMaxage, String encodeString) {
      try {
            if (cookieValue == null) {
                cookieValue = "";
            } else {
                cookieValue = URLEncoder.encode(cookieValue, encodeString);
            }
            Cookie cookie = new Cookie(cookieName, cookieValue);
            if (cookieMaxage &gt; 0) {
                cookie.setMaxAge(cookieMaxage);
            }
            if (null != request) {// 设置域名的cookie
                String domainName = getDomainName(request);
                System.out.println(domainName);
                if (!"localhost".equals(domainName)) {
                  cookie.setDomain(domainName);
                }
            }
            cookie.setPath("/");
            response.addCookie(cookie);
      } catch (Exception e) {
            e.printStackTrace();
      }
    }

    /**
   * 得到cookie的域名
   */
    private static final String getDomainName(HttpServletRequest request) {
      String domainName = null;
      // 通过request对象获取访问的url地址
      String serverName = request.getRequestURL().toString();
      if ("".equals(serverName)) {
            domainName = "";
      } else {
            // 将url地下转换为小写
            serverName = serverName.toLowerCase();
            // 如果url地址是以http://开头 将http://截取
            if (serverName.startsWith("http://")) {
                serverName = serverName.substring(7);
            }
            int end = serverName.length();
            // 判断url地址是否包含"/"
            if (serverName.contains("/")) {
                //得到第一个"/"出现的位置
                end = serverName.indexOf("/");
            }
            // 截取
            serverName = serverName.substring(0, end);
            // 根据"."进行分割
            final String[] domains = serverName.split("\\.");
            int len = domains.length;
            if (len &gt; 3) {
                // www.abc.com.cn
                domainName = domains + "." + domains + "." +
                        domains;
            } else if (len &gt; 1) {
                // abc.com or abc.cn
                domainName = domains + "." + domains;
            } else {
                domainName = serverName;
            }
      }
      if (domainName.indexOf(":") &gt; 0) {
            String[] ary = domainName.split("\\:");
            domainName = ary;
      }
      return domainName;
    }
}

</code></pre>
<p><strong>注意:</strong>将 ticket 保存到 cookie,cookieName 不可以随便写,必须时 <code>"userTicket"</code>。</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537353-1693533863.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537311-516058073.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538231-47912795.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538937-1226773329.png"></p>
<h2 id="分布式-session-共享-详解">分布式 Session 共享 详解</h2>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090540980-1116104761.png"></p>
<p><strong>上图分析-分布式存在的 Session 共享问题:</strong></p>
<ol>
<li>当 Nginx 对请求进行负载均衡后,可能对应到不同的 Tomcat</li>
<li>比如:如果一个商品,一个用户只能购买一次,不可以多购。第 1 次请求,均衡到 TomcatA,这时 Session 就记录在 TomcatA,第 2 次请求均衡到 TomcatB,这时就出现问题了,因为 TomcatB 会认为该用户时第 1 次来的,就会允许购买请求。</li>
<li>这样就会造成重复购买。</li>
</ol>
<p><strong>解决方案:</strong></p>
<ol>
<li><strong>Session 绑定/粘滞</strong></li>
</ol>
<p>什么是 session 绑定/粘滞/黏滞</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538674-38794829.png"></p>
<p>Session 绑定/粘滞/黏滞 :服务器会把用户的请求急,交给 tomcat 集群中的一个节点,以后此节点就复杂该用户的 Session。</p>
<ol>
<li>Session 绑定可以利用负载均衡的源地址 <code>Hash(ip_hast)</code>算法实现。</li>
<li>负载均衡服务器总是将来源手同一个 IP 的请求分发到同一台服务器上,也可以根据 Cookie 信息将同一个用户的请求总是分发到同一台服务器上。</li>
<li>这样整个会话期间,该用所有的请求都在同一台服务器上处理,即 Session绑定在某台特定服务器上,保证 Session 总能在这台服务器上获取。这种方法又被称为 <strong>Session /粘滞/黏滞</strong></li>
</ol>
<p><strong>优点:不占用服务端内存</strong></p>
<p><strong>缺点:</strong></p>
<ol>
<li>
<p>增加新机器,会重新 Hash,导致重新登录,前面存储的就 Session 信息丢失。</p>
</li>
<li>
<p>应用重启,也是需要重新登录。</p>
</li>
<li>
<p>某台服务器宕机,该机器上的Session 也就不存在了,用户请求切换到其他机器后,因为没有 Session 而无法完成业务处理,这种发案不符合系统高可用需求,使用较少。</p>
</li>
<li>
<p><strong>Session 复制:</strong></p>
</li>
</ol>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538897-892698054.png"></p>
<p><strong>Session 复制:</strong>是小型架构使用较多的一种服务器集群Session 管理机制。</p>
<ol>
<li>应用服务器开启 Web 容器的 Session 复制功能,在集群中的几台服务器之间同步 Session 对象,使每台服务器上都保存了所有用户的 Session 信息。</li>
<li>这样任何一台机器都不会 导致 Sessin 数据的丢失,而服务器使用 Session 时,也只需要在本机获取即可。·</li>
</ol>
<p><strong>优点:</strong>无需修改代码,修改 Tomcat 配置即可。</p>
<p><strong>缺点:</strong></p>
<ol>
<li>
<p>Session 同步传输占用内网带宽。</p>
</li>
<li>
<p>多台 Tomcat 同步性能指数级下降。</p>
</li>
<li>
<p>Session 占用内存,无法有效水平扩展。</p>
</li>
<li>
<p><strong>前端存储</strong></p>
</li>
</ol>
<p><strong>优点:</strong>不占用服务器内存</p>
<p><strong>缺点:</strong></p>
<ol>
<li>
<p>存在安全风险。</p>
</li>
<li>
<p>数据大小受到 Cookie 本身容量的限制。</p>
</li>
<li>
<p>占用外网带宽</p>
</li>
<li>
<p><strong>后端集中存储:使用第三方的缓存数据库存储,比如:Redis 存储我们的 Session 信息。</strong></p>
</li>
</ol>
<p><strong>优点:</strong>安全,容易水平扩展</p>
<p><strong>缺点:</strong>增加复杂度,需要修改代码</p>
<h2 id="分布式-session-解决方案-1-springsession-实现分布式-session">分布式 Session 解决方案 1-SpringSession 实现分布式 Session</h2>
<p><strong>一句话:将用户 Session 不再存放到各自登录的 Tomcat 服务器,而是统一存在 Redis,从而解决 Session 分布式问题</strong></p>
<ol>
<li>如图, 将用户的 Session 信息统一保存到 Redis 进行管理</li>
<li>说明: 在默认情况下是<strong>以原生形式保存</strong>的, 后面<strong>可以进一步优化</strong></li>
</ol>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538622-354228801.png"></p>
<p>需求说明: 用户登录,将用户 Session 统一存放到指定 Redis ,而不是分布式存放到不同</p>
<p>的 Tomcat 所在服务器</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537066-1327617757.png"></p>
<p>安装配置 Redis:大家可以参考移步至:✏️✏️✏️ 二. Redis 超详细的安装教程((七步)一步一步指导,步步附有截屏操作步骤)_truenas安装redis-CSDN博客</p>
<p>安装 redis-desktop-manager</p>
<p>一句话:这个是 Redis 可视化操作工具</p>
<p>安装过程非常简单,直接下一步即可</p>
<p>启动我们虚拟机当中的 Redis 服务器 :</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537985-420422741.png"></p>
<pre><code class="language-shell"># redis-server /etc/redis.conf
# ps -aux | grep redis
root       36900.20.2 1625169956 ?      Ssl09:08   0:00 redis-server *:6379
root       36960.00.0 112812   980 pts/1    S+   09:08   0:00 grep --color=auto redis
</code></pre>
<ol>
<li>先打开 Redis 所在 Linux 防火墙的 6379 端口</li>
</ol>
<pre><code class="language-shell">#打开端口
firewall-cmd --zone=public --add-port=6379/tcp --permanent
#重启防火墙, 才能生效
firewall-cmd --reload
#查看端口是否打开
firewall-cmd --list-ports
</code></pre>
<ol start="2">
<li>配置 Redis, 允许远程访问, 修改配置文件 redis.con/conf, 老师为了方便,没有设置远程访问密码。</li>
</ol>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537151-1204674631.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538967-1393597120.png"></p>
<pre><code class="language-properties">protected-mode no
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537144-1148261489.png"></p>
<p>重启 Redis 生效</p>
<p>通过 telnet 来连接 Linux Redis , 看看是否 OK,如果连接不上,检查前面的配置是否</p>
<p>正确, 特别注意: 需要保证 Redis Desktop 所在机器, 允许访问 6379</p>
<p>运行 Redis Desktop, 连接到 Linux 的 Redis</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090539230-1232237020.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537963-1420193162.png"></p>
<p>在 pom.xml 文件当中加入相关的 Redis 依赖。</p>
<pre><code class="language-xml"> &lt;!--spring data redis依赖,即 spring整合 redis--&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;
            &lt;version&gt;2.4.5&lt;/version&gt;
      &lt;/dependency&gt;
      &lt;!--pool2对象池依赖--&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;
            &lt;artifactId&gt;commons-pool2&lt;/artifactId&gt;
            &lt;version&gt;2.9.0&lt;/version&gt;
      &lt;/dependency&gt;
      &lt;!--实现分布式 session,即将 Session保存到指定的 Redis--&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.session&lt;/groupId&gt;
            &lt;artifactId&gt;spring-session-data-redis&lt;/artifactId&gt;
      &lt;/dependency&gt;
</code></pre>
<p>在项目的 application.yml, 配置 Redis 信息</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537722-1808907245.png"></p>
<pre><code class="language-yaml">spring:
    #配置Redis
redis:
    host: 192.168.198.135
    port: 6379
    password: rainbowsea
    database: 0
    timeout: 10000ms
    lettuce:
      pool:
      #最大连接数,默认是8
      max-active: 8
      #最大连接等待/阻塞时间,默认-1
      max-wait: 10000ms
      #最大空闲连接
      max-idle: 200
      #最小空闲连接,默认0
      min-idle: 5

#mybatis-plus配置
mybatis-plus:
#配置mapper.xml映射文件
mapper-locations: classpath*:/mapper/*Mapper.xml
#配置mybatis数据返回类型别名
type-aliases-package: com.rainbowsea.seckill.pojo
#mybatis sql 打印
logging:
level:
    com.rainbowsea.seckill.mapper: debug
server:
port: 8080



</code></pre>
<p>完成测试,启动项目,用户登录</p>
<p>浏览器输入 http://localhost:8080/login/toLogin</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537263-1988373498.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538622-2087440060.png"></p>
<p>如下优化将:保存到 Redis 当中的数据</p>
<h2 id="分布式-session-解决方案-2-直接将用户信息统一放入-redis">分布式 Session 解决方案 2-直接将用户信息统一放入 Redis</h2>
<p>一句话:<strong>前面将 Session 统一存放指定 Redis, 是以原生的形式存放, 在操作时, 还需要反序列化,不方便,我们可以直接将登录用户信息统一存放到 Redis, 利于操作</strong></p>
<p><strong>如图-将登录用户信息统一存放到 Redis, 方便操作</strong></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537911-1483109959.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537233-1595447635.png"></p>
<p>我们进行改进: 直接将登录用户信息统一存放到 Redis, 利于操作</p>
<p>这里,我们既然要使用自己配置的 Session ,将信息直接存储到 Redis 的话,我们必须要将在 pom.xml 当中 <code>org.springframework.session</code>提供的 Session 会话处理的 jar 报依赖,注释掉,防止冲突。</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538314-603471564.png"></p>
<p>创建:RedisConfig.java 一个关于 Redis 的一个配置类。</p>
<p>把 session 信息提取出来存到 redis 中,主要实现序列化, 这里是以常规操作</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538873-668318559.png"></p>
<pre><code class="language-java">package com.rainbowsea.seckill.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
* 把session信息提取出来存到redis中
* 主要实现序列化, 这里是以常规操作
* @author Rainbowsea
* @version 1.0
*/

@Configuration
public class RedisConfig {

    /**
   * 自定义 RedisTemplate对象, 注入到容器
   * 后面我们操作Redis时,就使用自定义的 RedisTemplate对象
   * @param redisConnectionFactory
   * @return RedisTemplate&lt;String, Object&gt;
   */
    @Bean
    public RedisTemplate&lt;String, Object&gt; redisTemplate(RedisConnectionFactory redisConnectionFactory) {
      RedisTemplate&lt;String, Object&gt; redisTemplate = new RedisTemplate&lt;&gt;();
      //设置相应key的序列化
      redisTemplate.setKeySerializer(new StringRedisSerializer());
      //value序列化
      //redis默认是jdk的序列化是二进制,这里使用的是通用的json数据,不用传具体的序列化的对象
      redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
      //设置相应的hash序列化
      redisTemplate.setHashKeySerializer(new StringRedisSerializer());
      redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
      //注入连接工厂
      redisTemplate.setConnectionFactory(redisConnectionFactory);
      System.out.println("测试--&gt; redisTemplate" + redisTemplate.hashCode());
      return redisTemplate;
    }

    /**
   * 增加执行脚本
   * @return DefaultRedisScript&lt;Long&gt;
   */
    @Bean
    public DefaultRedisScript&lt;Long&gt; script() {

      DefaultRedisScript&lt;Long&gt; redisScript = new DefaultRedisScript&lt;&gt;();
      //设置要执行的lua脚本位置, 把lock.lua文件放在resources
      redisScript.setLocation(new ClassPathResource("lock.lua"));
      redisScript.setResultType(Long.class);
      return redisScript;
    }
}

</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090539273-1257521045.png"></p>
<pre><code class="language-java">package com.rainbowsea.seckill.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.seckill.exception.GlobalException;
import com.rainbowsea.seckill.mapper.UserMapper;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.utill.CookieUtil;
import com.rainbowsea.seckill.utill.MD5Util;
import com.rainbowsea.seckill.utill.UUIDUtil;
import com.rainbowsea.seckill.utill.ValidatorUtil;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @author huo
* @description 针对表【seckill_user】的数据库操作Service实现
* @createDate 2025-04-24 15:38:01
*/
@Service
public class UserServiceImpl extends ServiceImpl&lt;UserMapper, User&gt;
      implements UserService {


    @Resource
    private UserMapper userMapper;

    @Resource
    private RedisTemplate redisTemplate;


    /**
   * 登录校验
   *
   * @param loginVo登录时发送的信息
   * @param requestrequest
   * @param response response
   * @return RespBean
   */
    @Override
    public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {

      String mobile = loginVo.getMobile();
      String password = loginVo.getPassword();

      // 判断手机号/id,和密码是否为空
      //if (!StringUtils.hasText(mobile) || !StringUtils.hasText(password)) {
      //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      //}

      // 判断手机号是否合格
      //if (!ValidatorUtil.isMobile(mobile)) {
      //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      //}

      // 查询DB,判断用户是否存在
      User user = userMapper.selectById(mobile);
      if (null == user) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      }


      // 如果用户存在,则对比密码!
      // 注意:我们从 LoginVo 取出的密码是中间密码(即客户端经过一次加密加盐处理的密码)
      if (!MD5Util.midPassToDBPass(password, user.getSlat()).equals(user.getPassword())) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      }

      // 登录成功

      // 给每个用户生成 ticket 唯一
      String ticket = UUIDUtil.uuid();

      // 为实现分布式 Session ,把登录用户信息存放到 Redis 当中
      System.out.println("使用 redisTemplate-&gt;" + redisTemplate.hashCode());
      redisTemplate.opsForValue().set("user:" + ticket, user);



      // 将登录成功的用户保存到 session
      //request.getSession().setAttribute(ticket, user);

      // 将 ticket 保存到 cookie,cookieName 不可以随便写,必须时 "userTicket"
      CookieUtil.setCookie(request, response, "userTicket", ticket);
      return RespBean.success();
    }
}





</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090540877-1245978776.png"></p>
<p><strong>测试:运行查看,我们在 Redis 保存的信息是否,符合我们的预期:</strong></p>
<p>完成测试,启动项目,用户登录</p>
<p>浏览器输入 http://localhost:8080/login/toLogin</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537228-259044511.png"></p>
<p>我们还还需要修改,登录成功,进入商品的处理,因为我们登录成功了,需要改为从 Redis 当中获取 Session 信息了。如果 Redis 当中没有该登录的用户的信息,那么就说明该用户没有登录过,需要登录,才能访问,我们的商品列表信息页面。</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090539294-1462553479.png"></p>
<pre><code class="language-java">package com.rainbowsea.seckill.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @author huo
* @description 针对表【seckill_user】的数据库操作Service
* @createDate 2025-04-24 15:38:01
*/
public interface UserService extends IService&lt;User&gt; {


    /**
   * 根据 Cookie 当中的 userTicket 获取判断,存储到 Redis 当中的用户信息
   * @param userTicketCookie 当中的 userTicket
   * @param request
   * @param response
   * @return 存储到 Redis 当中的 User 对象信息
   */
    User getUserByCookieByRedis(String userTicket,
                              HttpServletRequest request,
                              HttpServletResponse response);

}

</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537964-1299173646.png"></p>
<pre><code class="language-java">package com.rainbowsea.seckill.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.seckill.exception.GlobalException;
import com.rainbowsea.seckill.mapper.UserMapper;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import com.rainbowsea.seckill.utill.CookieUtil;
import com.rainbowsea.seckill.utill.MD5Util;
import com.rainbowsea.seckill.utill.UUIDUtil;
import com.rainbowsea.seckill.utill.ValidatorUtil;
import com.rainbowsea.seckill.vo.LoginVo;
import com.rainbowsea.seckill.vo.RespBean;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @author huo
* @description 针对表【seckill_user】的数据库操作Service实现
* @createDate 2025-04-24 15:38:01
*/
@Service
public class UserServiceImpl extends ServiceImpl&lt;UserMapper, User&gt;
      implements UserService {


    @Resource
    private UserMapper userMapper;

    @Resource
    private RedisTemplate redisTemplate;


    /**
   * 登录校验
   *
   * @param loginVo登录时发送的信息
   * @param requestrequest
   * @param response response
   * @return RespBean
   */
    @Override
    public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {

      String mobile = loginVo.getMobile();
      String password = loginVo.getPassword();

      // 判断手机号/id,和密码是否为空
      //if (!StringUtils.hasText(mobile) || !StringUtils.hasText(password)) {
      //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      //}

      // 判断手机号是否合格
      //if (!ValidatorUtil.isMobile(mobile)) {
      //    return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      //}

      // 查询DB,判断用户是否存在
      User user = userMapper.selectById(mobile);
      if (null == user) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      }


      // 如果用户存在,则对比密码!
      // 注意:我们从 LoginVo 取出的密码是中间密码(即客户端经过一次加密加盐处理的密码)
      if (!MD5Util.midPassToDBPass(password, user.getSlat()).equals(user.getPassword())) {
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
            //return RespBean.error(RespBeanEnum.LOGIN_ERROR);
      }

      // 登录成功

      // 给每个用户生成 ticket 唯一
      String ticket = UUIDUtil.uuid();

      // 为实现分布式 Session ,把登录用户信息存放到 Redis 当中
      System.out.println("使用 redisTemplate-&gt;" + redisTemplate.hashCode());
      redisTemplate.opsForValue().set("user:" + ticket, user);



      // 将登录成功的用户保存到 session
      //request.getSession().setAttribute(ticket, user);

      // 将 ticket 保存到 cookie,cookieName 不可以随便写,必须时 "userTicket"
      CookieUtil.setCookie(request, response, "userTicket", ticket);
      return RespBean.success();
    }

    /**
   * 根据 Cookie 当中的 userTicket 获取判断,存储到 Redis 当中的用户信息
   * @param userTicketCookie 当中的 userTicket
   * @param request
   * @param response
   * @return 存储到 Redis 当中的 User 对象信息
   */
    @Override
    public User getUserByCookieByRedis(String userTicket, HttpServletRequest request, HttpServletResponse response) {

      if(!StringUtils.hasText(userTicket)) {
            return null;
      }

      // 根据 Cookie 当中的 userTicket 获取判断,存储到 Redis 当中的用户信息
      // 注意:这里我们在 Redis 存储的 Key是:"user:+userTicket"
      User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);

      // 如果用户不为 null,就重新设置 cookie,刷新,防止cookie超时了,
      if(user != null) {
            // cookieName 不可以随便写,必须是 "userTicket"
            CookieUtil.setCookie(request,response,"userTicket",userTicket);
      }

      return user;
    }
}





</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538805-17497194.png"></p>
<pre><code class="language-java">package com.rainbowsea.seckill.controller;


import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;


/**
* 商品列表处理
*/
@Controller
@RequestMapping("/goods")
public class GoodsController {


    @Resource
    private UserService userService;


    // 跳转到商品列表页
    //@RequestMapping(value = "/toList")
    //public String toList(HttpSession session,
    //                     Model model,
    //                     @CookieValue("userTicket") String ticket,
    //                     ) {
    @RequestMapping(value = "/toList")
    public String toList(Model model,
                         @CookieValue("userTicket") String ticket,
                         HttpServletRequest request,
                         HttpServletResponse response
    ) {
      //@CookieValue("userTicket") String ticket 注解可以直接获取到,对应 "userTicket" 名称
      // 的cookievalue 信息
      if (!StringUtils.hasText(ticket)) {
            return "login";
      }


      // 通过 cookieVale 当中的 ticket 获取 session 中存放的 user
      //User user = (User) session.getAttribute(ticket);

      // 改为从 Redis 当中获取
      User user = userService.getUserByCookieByRedis(ticket, request, response);

      if (null == user) { // 用户没有成功登录
            return "login";
      }


      // 将 user 放入到 model,携带该下一个模板使用
      model.addAttribute("user", user);

      return "goodsList";
    }
}

</code></pre>
<p><strong>测试:运行查看,我们在 Redis 保存的信息是否,符合我们的预期:</strong></p>
<p>完成测试,启动项目,用户登录</p>
<p>浏览器输入 http://localhost:8080/login/toLogin</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538892-988258789.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090537556-1940841864.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090538262-1780577353.png"></p>
<h1 id="最后">最后:</h1>
<blockquote>
<p>“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202505/3084824-20250524090540836-379914854.gif"></p>
</blockquote><br><br>
来源:https://www.cnblogs.com/TheMagicalRainbowSea/p/18894075
頁: [1]
查看完整版本: 秒杀/高并发解决方案+落地实现 (技术栈: SpringBoot+Mysql + Redis +RabbitMQ +MyBatis-Plus +Maven + Linux + Jmeter )-01