应用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: false
datasource:
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加密
*/
@Component
public 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
@AllArgsConstructor
public 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
@AllArgsConstructor
public 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
@NoArgsConstructor
public class GlobalExecption extends RuntimeException {
private RespBeanEnum respBeanEnum;
}
- 再定义一个处理全局异常的类
@RestControllerAdvice
public 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复制
优点:不需要修改代码,只需要修改tomcat服务器
缺点:session同步传输占用内网宽带、多台tomcat同步性能质数下降、session占用内存,无法有效水平扩展 - 前端存储
优点:不占用服务器的内存
缺点:存在安全问题、数据大小受到cookie的限制、占用外网宽带 - session粘滞
优点:无需修改代码、服务器可以水平扩展
缺点:增加新的机器,会重新Hash,导致重新登录、需要重新登录 - 后端集中存储
优点:安全、容易水平扩展
缺点:增加复杂度、需要修改代码
这只是一个登录功能,对于MD5二次加密,一直都有一个问题,就是前台向后台传递过来一个进行过一次加盐的密码字符串,后台需要二次加盐 *** 作与数据库中的数据作比较,密码不对的话,要抛出异常显示密码错误,但是我的一直显示的是服务器错误,我也一直没有弄懂是为什么,如果有大佬看到我的话,麻烦给予我批评指正。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)