> 文档中心 > 应用springboot和redis写的秒杀系统

应用springboot和redis写的秒杀系统

应用springboot和redis写的秒杀系统

提示:项目源码会后续放出

文章目录

  • 前言
  • 一、搭建项目
  • 二、分布式session
    • 1.用户登录功能
      • MD5的加密:
      • 整理思路:
      • 优化部分Cookie
      • 分布式session问题
          • 解决方案
  • 总结

前言

提示:对系统技术的介绍
前端技术:Thymeleaf、Bootstrap、Jquery
后端技术:SpringBoot、MyBaitsPlus、Lombok
中间插件:RabbitMQ、Redis

  1. JDK:1.8版本及以上
  2. maven:配置到idea,3.6.1版本
  3. 数据库:redis数据库
  4. idea平台
  5. 项目搭建、分布式session(用户登录、共享session)、秒杀功能(商品列表、商品详情、秒杀、订单详情)、压力测试(JMeter入门、自定义变量、正式压测)、页面优化(缓存、静态化分离)、服务优化(RebbitMQ消息队列、接口优化、分布式锁)、接口安全(隐藏秒杀地址、验证码、接口限流)
  6. 秒杀其实主要解决的是两个问题,一个是并发的读、一个是并发的写,这个项目特别适合小白理解并发编程。

提示:以下是本篇文章正文内容

一、搭建项目

  1. 项目名称seckill,表示秒杀
  2. 结构:com.shmily.seckill
    java web、mybatis-plus、mysql driver、lomback的jar
  3. 资源文件:resources文件夹下的(static、templates)
  4. 配置数据库
spring: thymeleaf:   #关闭缓存   cache: falsedatasource: driver-class-name: com.mysql.cj.jdbd.Driver url: jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: 123456 hikari:   #连接名   pool-name: DateHikariCP   #最小空闲连接   minimum-dile: 5   #空闲了解存活最大对的时间,默认是600000(10分钟)   idle-timeout: 180000   #最大的连接数,默认是10   maximum-pool-size: 10   #从连接池返回的连接自动提交   auto-commit: true   #连接最大存活时间,0表示永久存活,默认是30分钟   max-lifetime: 1800000   #连接超时时间,默认是30秒   connection-timeout: 30000   #测试连接是否可用的查询语句    connection-test-query: SELECT 1     #mybatis-plus的配置mybatis-plus: #配置Mapper.xml文件 mapper-locations: classpath*:/mapper/*Mapper.xml #配置mybatis数据返回类型别名(默认别名是类名) type-aliases-package: com.shmily.seckill.pojo  #mybatis sql打印(方法接口所在的包,不是mapper.xml所在包)logging: level:   com.shmily.seckill.mapper: debug
  1. 创建数据库
第一个数据表CREATE TABLE t_user(`id`BIGINT(20) NOT NULL COMMENT'用户ID手机号码',`nickname`VARCHAR(255) NOT NULL,`password`VARCHAR(255) DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt)+salt)',`salt` VARCHAR(10) DEFAULT NULL,`head` VARCHAR(128) DEFAULT NULL COMMENT '头像',`register_date` datetime DEFAULT NULL COMMENT '注册时间',`last_login_date` datetime DEFAULT NULL COMMENT '最后一次登录时间',`login_count` int(11) DEFAULT '0' COMMENT '登录的次数',PRIMARY KEY(`id`))

二、分布式session

1.用户登录功能

MD5的加密:

package com.shmily.seckill.utils;import org.apache.commons.codec.digest.DigestUtils;import org.springframework.stereotype.Component;/** 1. MD5加密 */@Componentpublic class MD5Util {    public static String md5(String src){ return DigestUtils.md2Hex(src);    }    private static final String salt="1a2b3c4d";    public static String inputPassToFromPass(String inputPass){ String str=salt.charAt(0)+salt.charAt(2)+inputPass+salt.charAt(4); return md5(str);    }    public static String formPassToDBPass(String fromPass,String salt){ String str=salt.charAt(0)+salt.charAt(2)+fromPass+salt.charAt(4); return md5(str);    }    public static String inputPassToDBPass(String inputPass,String salt){ String fromPass=inputPassToFromPass(inputPass); String dbPass=formPassToDBPass(fromPass,salt); return dbPass;    }}

整理思路:

  1. 先是跳转到login页面的实现,然后使用的是mybatis-plus的逆向工程去实现userMapper、userService、user的生成等。
  2. 然后再controller中去接受前台发送的请求,其中接受请求的参数,可以定义一个实体类LoginVo去接受,返回的是调用的service层的业务逻辑方法。对于LginVo类中定义了主要是两个参数,一个是mobile和password这两个变量。除了定义接受参数的对象之外,还要定义一个返回数据的对象RespBean和公共返回对象的枚举类RespEnum

以下的RespBean的书写

@Data@NoArgsConstructor@AllArgsConstructorpublic class RespBean {    private long code;    private String message;    private  Object obj;    /**     * 成功返回对象     * @return     */    public static RespBean success(){ return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBeanEnum.SUCCESS.getMessage(),null);    }    public static RespBean success(Object obj){ return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBeanEnum.SUCCESS.getMessage(),null);    }    /**     * 失败返回结果     * @return     */    public static RespBean error(){ return new RespBean(RespBeanEnum.ERROR.getCode(),RespBeanEnum.ERROR.getMessage(),null);    }    public static RespBean error(Object obj){ return new RespBean(RespBeanEnum.ERROR.getCode(),RespBeanEnum.ERROR.getMessage(),null);    }}

以下是枚举类的书写

@Getter@ToString@AllArgsConstructorpublic enum  RespBeanEnum {    SUCCESS(200,"SUCCESS"),    ERROR(500,"服务器异常"),    //登录模块    LOGIN_ERROR(500210,"用户名或者密码错误!"),    MOBILE_ERROR(500211,"手机格式不正确"),    BIND_ERROR(500212,"参数效验异常");    private final Integer code;    private final String message;}
  1. 在service层会去实现对数据的校验,对了一般的数据效验是写在controller层,主要看你自己的书写格式吧,在这个函数里面显示对数据的最后校验,一般是直接写业务逻辑,但是在公司里面,一般都是单独的去定义校验逻辑。
    以下是从传统的逻辑
if(StringUtils.isEmpty(mobile)||StringUtils.isEmpty(password)){     return RespBean.error(RespBeanEnum.LOGIN_ERROR); } if(ValidatorUtil.isMobile(mobile)){     return RespBean.error(RespBeanEnum.MOBILE_ERROR);}

以下的公司常用的书写格式

  • 先定义的手机号码校验的正则表达式
/** * 手机号码效验 * 正则表达式 */public class ValidatorUtil {    private  static  final Pattern moblie_patter=Pattern.compile("[1]([3-9])[0-9]{9}$");    public static boolean isMobile(String moblie){ if(StringUtils.isEmpty(moblie)){     return  false; } Matcher matcher=moblie_patter.matcher(moblie); return matcher.matches();    }}
  • 定义你的校验注解
/** * 验证手机号的注解 */@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)@Documented@Constraint( validatedBy = {IsMobileValidator.class})public @interface IsMobile {    boolean required() default true;    String message() default "手机号码格式错误";    Class<?>[] groups() default {};    Class<? extends Payload>[] payload() default {};}
  • 再定义你的校验规则,实现ConstraintValidator接口,重写里面的两个方法
/** - 手机号码效验规则 */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.isEmpty(value)){  return true;     }else{  return ValidatorUtil.isMobile(value);     } }    }}
  • 在service层做完校验之后,调用mapper去查询数据,对查出的数据做处理,如果用户为空的话,就抛出一个自定义的异常,如果密码错误的话,也抛出一个自定义的异常。
    以下是自定义的异常类

  • 先去定义一个全局异常类

@Data@AllArgsConstructor@NoArgsConstructorpublic class GlobalExecption extends RuntimeException {    private RespBeanEnum respBeanEnum;}
  • 再定义一个处理全局异常的类
@RestControllerAdvicepublic class GlobalExecptionHandler   {    @ExceptionHandler(Exception.class)    public RespBean ExecptionHandler(Exception e){ if(e instanceof GlobalExecption){     GlobalExecption globalExecption=(GlobalExecption)e;     return RespBean.error(globalExecption.getRespBeanEnum()); }else if(e instanceof BindException){     BindException bindException=(BindException) e;     RespBean respBean=RespBean.error(RespBeanEnum.BIND_ERROR);     respBean.setMessage("参数效验异常"+bindException.getBindingResult().getAllErrors().get(0).getDefaultMessage());     return respBean; } return RespBean.error(RespBeanEnum.ERROR);    }}

优化部分Cookie

  1. 先定义cookieUtil类和UUIDUtil类
  2. 在loginController中去定义session,随机生成一个UUid,放到user的session中,在跳转到商品页面的时候,就可以获取到user对象了。

分布式session问题

当项目做了负载均衡以后,如果在session中存了数据,那么就有可能有有些项目取不到session中的数据,这就是分布式session问题。

解决方案
  1. session复制
    优点:不需要修改代码,只需要修改tomcat服务器
    缺点:session同步传输占用内网宽带、多台tomcat同步性能质数下降、session占用内存,无法有效水平扩展
  2. 前端存储
    优点:不占用服务器的内存
    缺点:存在安全问题、数据大小受到cookie的限制、占用外网宽带
  3. session粘滞
    优点:无需修改代码、服务器可以水平扩展
    缺点:增加新的机器,会重新Hash,导致重新登录、需要重新登录
  4. 后端集中存储
    优点:安全、容易水平扩展
    缺点:增加复杂度、需要修改代码

总结

这只是一个登录功能,对于MD5二次加密,一直都有一个问题,就是前台向后台传递过来一个进行过一次加盐的密码字符串,后台需要二次加盐操作与数据库中的数据作比较,密码不对的话,要抛出异常显示密码错误,但是我的一直显示的是服务器错误,我也一直没有弄懂是为什么,如果有大佬看到我的话,麻烦给予我批评指正。

推币机的世界