网站首页 全球最实用的IT互联网站!

人工智能P2P分享Wind搜索发布信息网站地图标签大全

当前位置:诺佳网 > 软件工程 > 后端开发 > Java >

秒杀/高并发解决方案+落地实现 (技术栈: SpringB

时间:2025-05-24 09:15

人气:

作者:admin

标签:

导读:秒杀/高并发方案-介绍 @目录秒杀/高并发方案-介绍秒杀/高并发解决方案落地实现 (技术栈: SpringBootMysql Redis RabbitMQ MyBatis-Plus Maven Linux Jmeter )-01分布式会话 Session 共享加密密码设置注解自...

秒杀/高并发方案-介绍

@

目录

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

  1. 秒杀/ 高并发,其实主要解决两个问题:一个是并发读,一个是并发写。
  2. 并发读的核心优化理念就是尽量减少用户到 DB 来 “读” 数据,或者让他们读更少的数据,并发写的处理原则也是一样的。
  3. 针对秒杀系统需要做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。
  4. 系统架构降要满足高可用:流量符合预期时要稳定,要保证秒杀活动顺利完成,即秒杀商品顺利被卖出去,这个是最基本的前提。
  5. 系统保证数据的一致性:就是秒杀 10 个商品,那就只能成交 10 个商品,多一个少一个都不行。一旦库存不对,就要承担损失。
  6. 系统要满足高性能:也就是系统的性能要足够高,需要支撑大流量,不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就 “快” 了。
  7. 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键,对应的方案:比如:页面缓存方案,Redis 预减库存 / 内存标记与隔离,请求的削峰(RabbitMA / 异步请求),分布式 Session 共享等处理。

用户登录 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;

分布式会话 Session 共享

加密密码设置

MD5 的加密的依赖包:

这里我们解读一下密码的设计!!:

登录为例讲解:

传统方式:

客户端——> password 明文——>后端(md5(password 明文)) == db 中存放的 password 是否一致) :这种传统方式存在的问题:

传统方式改进的方式 1:客户端——> md5(password 明文)——>后端(md5(md5(password 明文)) ) == db 中存放的 password 是否一致) 。这样就算黑客拦截到了我们前端发送的信息,也是被加密的,所以无妨。

我们可以在传统方式的基础上,在 进行一个加盐上的处理。让密码更加安全一些。

传统方式改进的方式 2:客户端——> md5(password 明文+salt1(固定的盐))——>后端(md5(md5(password 明文+salt1(固定的盐)+salt2(从数据库当中获取的盐,不同用户对应的盐也不同))) ) == db 中存放的 password 是否一致) 。

注意:是对称加密的。

Md5 加密所需的相关依赖 。

      <!--md5依赖-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.15</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>

Junit Jupiter: Junit Jupiter 提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心。内部 包含了一个测试引擎,用于在 Junit Platform 上运行

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>
        
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.7.2</version>
            <scope>compile</scope>
        </dependency>

加密密码工具类编写


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;
    }
}

validation参数校验

        <!--validation参数校验-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>2.4.5</version>
        </dependency>

用户登录逻辑编写:

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<UserMapper, User>
        implements UserService {


    @Resource
    private UserMapper userMapper;


    /**
     * 登录校验
     *
     * @param loginVo  登录时发送的信息
     * @param request  request
     * @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);
    }
}





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);
    }
}




注解自定义校验

自定义校验器:

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;

/**
 * 开发一个自定义的注解:替换如下,登录校验时的代码
 * <p>
 * <p>
 * // 判断手机号/id,和密码是否为空
 * if (!StringUtils.hasText(mobile) || !StringUtils.hasText(password)) {
 * return RespBean.error(RespBeanEnum.LOGIN_ERROR);
 * }
 * <p>
 * // 判断手机号是否合格
 * 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<?>[] groups() default {}; // 默认参数

    Class<? extends Payload>[] payload() default {}; //默认参数
}

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<IsMobile, String> {

    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);
            }
        }
    }
}

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;
}

全局异常处理定义

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;
}

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);
    }
}

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<UserMapper, User>
        implements UserService {


    @Resource
    private UserMapper userMapper;


    /**
     * 登录校验
     *
     * @param loginVo  登录时发送的信息
     * @param request  request
     * @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);
        / 
温馨提示:以上内容整理于网络,仅供参考,如果对您有帮助,留下您的阅读感言吧!
相关阅读
本类排行
相关标签
本类推荐

CPU | 内存 | 硬盘 | 显卡 | 显示器 | 主板 | 电源 | 键鼠 | 网站地图

Copyright © 2025-2035 诺佳网 版权所有 备案号:赣ICP备2025066733号
本站资料均来源互联网收集整理,作品版权归作者所有,如果侵犯了您的版权,请跟我们联系。

关注微信