应用springboot和redis写的秒杀系统
应用springboot和redis写的秒杀系统
提示:项目源码会后续放出
文章目录
- 前言
- 一、搭建项目
- 二、分布式session
- 1.用户登录功能
- MD5的加密:
- 整理思路:
- 优化部分Cookie
- 分布式session问题
- 解决方案
- 总结
前言
提示:对系统技术的介绍
前端技术:Thymeleaf、Bootstrap、Jquery
后端技术:SpringBoot、MyBaitsPlus、Lombok
中间插件:RabbitMQ、Redis
- JDK:1.8版本及以上
- maven:配置到idea,3.6.1版本
- 数据库:redis数据库
- idea平台
- 项目搭建、分布式session(用户登录、共享session)、秒杀功能(商品列表、商品详情、秒杀、订单详情)、压力测试(JMeter入门、自定义变量、正式压测)、页面优化(缓存、静态化分离)、服务优化(RebbitMQ消息队列、接口优化、分布式锁)、接口安全(隐藏秒杀地址、验证码、接口限流)
- 秒杀其实主要解决的是两个问题,一个是并发的读、一个是并发的写,这个项目特别适合小白理解并发编程。
提示:以下是本篇文章正文内容
一、搭建项目
- 项目名称seckill,表示秒杀
- 结构:com.shmily.seckill
java web、mybatis-plus、mysql driver、lomback的jar
- 资源文件:resources文件夹下的(static、templates)
- 配置数据库
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
- 创建数据库
第一个数据表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; }}
整理思路:
- 先是跳转到login页面的实现,然后使用的是mybatis-plus的逆向工程去实现userMapper、userService、user的生成等。
- 然后再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;}
- 在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
- 先定义cookieUtil类和UUIDUtil类
- 在loginController中去定义session,随机生成一个UUid,放到user的session中,在跳转到商品页面的时候,就可以获取到user对象了。
分布式session问题
当项目做了负载均衡以后,如果在session中存了数据,那么就有可能有有些项目取不到session中的数据,这就是分布式session问题。
解决方案
- session复制
优点:不需要修改代码,只需要修改tomcat服务器
缺点:session同步传输占用内网宽带、多台tomcat同步性能质数下降、session占用内存,无法有效水平扩展 - 前端存储
优点:不占用服务器的内存
缺点:存在安全问题、数据大小受到cookie的限制、占用外网宽带 - session粘滞
优点:无需修改代码、服务器可以水平扩展
缺点:增加新的机器,会重新Hash,导致重新登录、需要重新登录 - 后端集中存储
优点:安全、容易水平扩展
缺点:增加复杂度、需要修改代码
总结
这只是一个登录功能,对于MD5二次加密,一直都有一个问题,就是前台向后台传递过来一个进行过一次加盐的密码字符串,后台需要二次加盐操作与数据库中的数据作比较,密码不对的话,要抛出异常显示密码错误,但是我的一直显示的是服务器错误,我也一直没有弄懂是为什么,如果有大佬看到我的话,麻烦给予我批评指正。