一、秒杀项目
1.1 如何设计秒杀系统
- 高可用
- 高性能:支持高并发访问
- 一致性:秒杀要保持数据的一致性
1.2 数据库
- 字符集:utf8mb4
可以存储emoji表情
二、业务
2.1 登录
2.2.1 密码加密
- MD5(MD5(密码+固定salt)+随机salt)
- 前端使用固定salt进行第一次加密,后端使用随机salt进行第二次加密。
2.2.2 密码参数校验
- pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- 自定注解
@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 {};
}
package com.example.seckilldemo.validator;
import com.example.seckilldemo.utils.ValidatorUtil;
import org.thymeleaf.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 手机号码校验规则
*
* @author: LC
* @date 2022/3/2 3:08 下午
* @ClassName: IsMobileValidator
*/
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
private boolean required = false;
@Override
public void initialize(IsMobile constraintAnnotation) {
// ConstraintValidator.super.initialize(constraintAnnotation);
required = constraintAnnotation.required();
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (required) {
return ValidatorUtil.isMobile(s);
} else {
if (StringUtils.isEmpty(s)) {
return true;
} else {
return ValidatorUtil.isMobile(s);
}
}
}
}
2.2.3 分布式session
- 请求通过分布器分发到不同的tomcat服务器,有的tomcat存储了用户相关的session信息,有的没有,当请求分发到没有session信息的tomcat服务器上时,用户需要重新进行登录。
2.2.3.1 解决方案
- session复制:会增加存储,只需要进行tomcat配置
- 后端集中存储,会增加复杂度
2.2.4 参数解析器
- 配置
package com.example.seckilldemo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* MVC配置类
*
* @author: LC
* @date 2022/3/3 2:37 下午
* @ClassName: WebConfig
*/
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserArgumentResolver userArgumentResolver;
@Autowired
private AccessLimitInterceptor accessLimitInterceptor;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
// WebMvcConfigurer.super.addArgumentResolvers(resolvers);
resolvers.add(userArgumentResolver);
}
//静态资源展示
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
//swagger 和 knife4j
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessLimitInterceptor);
}
}
- 参数解析实现
package com.example.seckilldemo.config;
import com.example.seckilldemo.entity.TUser;
import com.example.seckilldemo.service.ITUserService;
import com.example.seckilldemo.utils.CookieUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.thymeleaf.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义用户参数
*
* @author: LC
* @date 2022/3/3 4:46 下午
* @ClassName: UserArgumentResolver
*/
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private ITUserService itUserService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> parameterType = parameter.getParameterType();
return parameterType == TUser.class;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return UserContext.getUser();
// HttpServletRequest nativeRequest = webRequest.getNativeRequest(HttpServletRequest.class);
// HttpServletResponse nativeResponse = webRequest.getNativeResponse(HttpServletResponse.class);
// String userTicket = CookieUtil.getCookieValue(nativeRequest, "userTicket");
// if (StringUtils.isEmpty(userTicket)) {
// return null;
// }
// return itUserService.getUserByCookie(userTicket, nativeRequest, nativeResponse);
}
}
2.3 异常处理
2.3.1 ControllerAdvicer+ExceptionHandler
处理controller抛出的异常。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public RespBean ExceptionHandler(Exception e) {
if (e instanceof GlobalException) {
GlobalException exception = (GlobalException) e;
return RespBean.error(exception.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;
}
System.out.println("异常信息" + e);
return RespBean.error(RespBeanEnum.ERROR);
}
}
2.4 秒杀
2.4 逻辑
2.4.1 秒杀前判断
- 如果在秒杀范围内,前端是会出现秒杀按钮的
- 判断用户是否登录
- 判断秒杀商品的库存是否足够
- 判断用户是否重复下单,查看秒杀订单表
- 进行秒杀,进行减库存,然后生成秒杀订单表,跳转到订单详情页面
2.4.2 进行秒杀
- 修改库从需要判断库从大于0才及进行修改:使用update语句
boolean seckillGoodsResult = itSeckillGoodsService.update(new UpdateWrapper<TSeckillGoods>()
.setSql("stock_count = " + "stock_count-1")
.eq("goods_id", goodsVo.getId())
.gt("stock_count", 0)
);
- 生成秒杀详情订单
2.5 业务流程
-
用户进行登录输入用户名和密码,在密码在前端用固定salt加密,传到后端,后端对密码,用户名进行验证。验证通过,为用户生成cookie信息,cookie返回,并且将用户信息存储到redis中。
-
登陆成功,到秒杀商品列表页面,选择秒杀的商品,进入到商品详情页面,点击秒杀商品进行秒杀。
-
正在秒杀显示正在进行中
-
秒杀成功进入到商品订单详情页
-
商品详情页
2.6 redis信息
- 秒杀时的验证码的key
超时时间300s
captcha+userID+goodsID
2.7 秒杀过程
- 判断用户登录信息
- 判断内存标记中的秒杀商品是否足够:Controller实现InitializingBean的afterPropertiesSet方法,将秒杀商品信息读到内存标记中,并且将秒杀商品信息存储到redis中。
HashMap goodsStockMap = new HashMap<Long, Boolean>();
- 判断是否重复秒杀:redis中查询订单信息,没有则没有重复秒杀。
- redis预减库存,预减少成功后发送rabbitmq消息。
- mq接收者消费mq消息,判断是否重复抢购,判断商品库存,然后秒杀,生成秒杀订单,生成秒杀商品详情订单。将秒杀订单信息存储到redis中。
2.8 接口限流
- 针对用户请求的uri和用户的id进行限流,输入最大限流次数,通过redis实现限流。
- 第一次获取key,key为null,设置key并且value为1
- 后面每访问一次,次数加1,超过次数,将返回信息写入response
private void render(HttpServletResponse response, RespBeanEnum respBeanEnum) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter printWriter = response.getWriter();
RespBean bean = RespBean.error(respBeanEnum);
printWriter.write(new ObjectMapper().writeValueAsString(bean));
printWriter.flush();
printWriter.close();
}
三、问题
3.1 前端不能传秒杀库存
前端传递的数据可能会被修改
3.2 问什么新建秒杀表不在原表加字段
- 如果秒杀活动较多每次都要在原表上进行修改,比较麻烦,而且物品的原价和秒杀价可能同时使用,在原表修改,容易出错。
3.3 打包
- 这里需要maven插件
- 去掉test
mvn clean
mvn package - java -jar jar包运行
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
四、压测
4.1 测试商品详情接口
4.1.1 测试条件
生成5000用户,将用户的cookie存入redis。
4.1.2 查询文件列表
4.2 单服务秒杀测试
4.2.1 原始秒杀测试没有任何优化策略
- jmeter结果
- 出现超卖的情况
- 此时没有添加事务,没有在扣库存前判断库存是否大于0
4.2.2 未使用mq的测试
-
不使用mq,不使用内存标记,不使用redis预减库存
-
不使用mq,不使用内存标记,使用redis预减库存
-
不使用mq,使用内存标记,使用redis预减库存
4.2.3 使用了mq后的测试
- 使用mq,使用内存标记,使用redis预减库存
- 50000线程测试结果
4.3 多服务秒杀测试
五、秒杀优化
5.1 页面优化
5.1.1 页面缓存(返回的是整个html,前后端未分离)
将页面渲染好后缓存到redis中,设置过期时间T,这样用户得到的就是T内的页面。这样在T时间内不用每次都渲染页面,减少了时间。T不能太长,也不能太短。
5.1.2 url缓存
和页面缓存差不多,只不过url会有动态参数,所以缓存的多一点。根据不同动态参数进行缓存。
5.1.3 页面静态化(前后端分离)
后端只需传递数据到前端,此时后端不需要返回整个html页面了。
5.2 单服务秒杀后端优化
5.2.1 减库存优化及修正
- 加事务
- 判断库存>0才进行减库存操作
- 添加唯一索引防止用户重复下单,这个重复下单是防止用户下用户id和商品id相同的单,上面的防止重复下单是防止用户下多个不同的单。
5.2.2 用户重复秒杀问题
- 重复秒杀有2种,一种对同一商品进行秒杀,通过建立用户ID和商品ID的唯一索引防止重复秒杀。
5.2.3 使用redis优化
- 秒杀前将商品信息都加入到redis中,进行预减库存,同时生成秒杀消息,发送到mq对列,给前端返回信息,前端根据返回的结构展示正在秒杀中页面。
- 使用内存标记减少对redis的访问,原来是库存不足时还是需要通过访问redis,现在时设置额外的变量先访问变量,当变量种的库存不足直接返回,较少对redis的访问。
5.2.4 mq秒杀信息的获取
- 数据库查询到秒杀的订单则秒杀成功,返回1
- 数据库查询不到秒杀的订单且redis中商品的库存小于等于0,则秒杀失败
- 秒杀正在进行中
六、技术作用
6.1 redis
- 存储商品库存信息
- 进行预减库存操作
- 存储用户秒杀的订单信息,进行重复秒杀判断
- 辅助验证码验证
6.2 ThreadLocal
- 每个ThreadLocal都有一个自己的ThreadLocalMap,将数据以key,value的形式存到ThreadLocalMap中。
- 拦截器通过threadlocal对象将用户的信息解析存入,在参数解析器时将用户信息取出,进行参数解析。此时通过threadlocal对象来存储每个请求处理线程的用户信息的副本,来实现数据访问的隔离性。
threadlocal作用
6.3 Rabbitmq
- 异步下单
- 削峰填谷
6.4
七、安全优化
7.1 隐藏接口地址
7.2 验证码
7.3 接口限流
7.3.1 限流标准
最大qps的70%~80%文章来源:https://www.toymoban.com/news/detail-496969.html
7.3.2 通用限流
- 实现根据用户id对用户进行限流
- 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
int second();
int maxCount();
boolean needLogin() default true;
}
八、问题
8. 1 商品少卖
在redis中初始化生成2份相同的商品信息会导致商品少卖。但是库存依然到0???文章来源地址https://www.toymoban.com/news/detail-496969.html
九、sql语句
9.1 清空表语句
DELETE FROM t_order WHERE goods_id = 1;
DELETE FROM `t_seckill_order` WHERE goods_id = 1;
到了这里,关于java 2023秒杀项目 day(1) 面经的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!