一、秒殺項(xiàng)目
1.1 如何設(shè)計秒殺系統(tǒng)
- 高可用
- 高性能:支持高并發(fā)訪問
- 一致性:秒殺要保持?jǐn)?shù)據(jù)的一致性
1.2 數(shù)據(jù)庫
- 字符集:utf8mb4
可以存儲emoji表情
二、業(yè)務(wù)
2.1 登錄
2.2.1 密碼加密
- MD5(MD5(密碼+固定salt)+隨機(jī)salt)
- 前端使用固定salt進(jìn)行第一次加密,后端使用隨機(jī)salt進(jìn)行第二次加密。
2.2.2 密碼參數(shù)校驗(yàn)
- 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 "手機(jī)號碼格式錯誤";
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;
/**
* 手機(jī)號碼校驗(yàn)規(guī)則
*
* @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
- 請求通過分布器分發(fā)到不同的tomcat服務(wù)器,有的tomcat存儲了用戶相關(guān)的session信息,有的沒有,當(dāng)請求分發(fā)到?jīng)]有session信息的tomcat服務(wù)器上時,用戶需要重新進(jìn)行登錄。
2.2.3.1 解決方案
- session復(fù)制:會增加存儲,只需要進(jìn)行tomcat配置
- 后端集中存儲,會增加復(fù)雜度
2.2.4 參數(shù)解析器
- 配置
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);
}
//靜態(tài)資源展示
@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);
}
}
- 參數(shù)解析實(shí)現(xiàn)
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;
/**
* 自定義用戶參數(shù)
*
* @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("參數(shù)校驗(yàn)異常:" + bindException.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return respBean;
}
System.out.println("異常信息" + e);
return RespBean.error(RespBeanEnum.ERROR);
}
}
2.4 秒殺
2.4 邏輯
2.4.1 秒殺前判斷
- 如果在秒殺范圍內(nèi),前端是會出現(xiàn)秒殺按鈕的
- 判斷用戶是否登錄
- 判斷秒殺商品的庫存是否足夠
- 判斷用戶是否重復(fù)下單,查看秒殺訂單表
- 進(jìn)行秒殺,進(jìn)行減庫存,然后生成秒殺訂單表,跳轉(zhuǎn)到訂單詳情頁面
2.4.2 進(jìn)行秒殺
- 修改庫從需要判斷庫從大于0才及進(jìn)行修改:使用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 業(yè)務(wù)流程
-
用戶進(jìn)行登錄輸入用戶名和密碼,在密碼在前端用固定salt加密,傳到后端,后端對密碼,用戶名進(jìn)行驗(yàn)證。驗(yàn)證通過,為用戶生成cookie信息,cookie返回,并且將用戶信息存儲到redis中。
-
登陸成功,到秒殺商品列表頁面,選擇秒殺的商品,進(jìn)入到商品詳情頁面,點(diǎn)擊秒殺商品進(jìn)行秒殺。
-
正在秒殺顯示正在進(jìn)行中
-
秒殺成功進(jìn)入到商品訂單詳情頁
-
商品詳情頁
2.6 redis信息
- 秒殺時的驗(yàn)證碼的key
超時時間300s
captcha+userID+goodsID
2.7 秒殺過程
- 判斷用戶登錄信息
- 判斷內(nèi)存標(biāo)記中的秒殺商品是否足夠:Controller實(shí)現(xiàn)InitializingBean的afterPropertiesSet方法,將秒殺商品信息讀到內(nèi)存標(biāo)記中,并且將秒殺商品信息存儲到redis中。
HashMap goodsStockMap = new HashMap<Long, Boolean>();
- 判斷是否重復(fù)秒殺:redis中查詢訂單信息,沒有則沒有重復(fù)秒殺。
- redis預(yù)減庫存,預(yù)減少成功后發(fā)送rabbitmq消息。
- mq接收者消費(fèi)mq消息,判斷是否重復(fù)搶購,判斷商品庫存,然后秒殺,生成秒殺訂單,生成秒殺商品詳情訂單。將秒殺訂單信息存儲到redis中。
2.8 接口限流
- 針對用戶請求的uri和用戶的id進(jìn)行限流,輸入最大限流次數(shù),通過redis實(shí)現(xiàn)限流。
- 第一次獲取key,key為null,設(shè)置key并且value為1
- 后面每訪問一次,次數(shù)加1,超過次數(shù),將返回信息寫入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 前端不能傳秒殺庫存
前端傳遞的數(shù)據(jù)可能會被修改
3.2 問什么新建秒殺表不在原表加字段
- 如果秒殺活動較多每次都要在原表上進(jìn)行修改,比較麻煩,而且物品的原價和秒殺價可能同時使用,在原表修改,容易出錯。
3.3 打包
- 這里需要maven插件
- 去掉test
mvn clean
mvn package - java -jar jar包運(yùn)行
<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 單服務(wù)秒殺測試
4.2.1 原始秒殺測試沒有任何優(yōu)化策略
- jmeter結(jié)果
- 出現(xiàn)超賣的情況
- 此時沒有添加事務(wù),沒有在扣庫存前判斷庫存是否大于0
4.2.2 未使用mq的測試
-
不使用mq,不使用內(nèi)存標(biāo)記,不使用redis預(yù)減庫存
-
不使用mq,不使用內(nèi)存標(biāo)記,使用redis預(yù)減庫存
-
不使用mq,使用內(nèi)存標(biāo)記,使用redis預(yù)減庫存
4.2.3 使用了mq后的測試
- 使用mq,使用內(nèi)存標(biāo)記,使用redis預(yù)減庫存
- 50000線程測試結(jié)果
4.3 多服務(wù)秒殺測試
五、秒殺優(yōu)化
5.1 頁面優(yōu)化
5.1.1 頁面緩存(返回的是整個html,前后端未分離)
將頁面渲染好后緩存到redis中,設(shè)置過期時間T,這樣用戶得到的就是T內(nèi)的頁面。這樣在T時間內(nèi)不用每次都渲染頁面,減少了時間。T不能太長,也不能太短。
5.1.2 url緩存
和頁面緩存差不多,只不過url會有動態(tài)參數(shù),所以緩存的多一點(diǎn)。根據(jù)不同動態(tài)參數(shù)進(jìn)行緩存。
5.1.3 頁面靜態(tài)化(前后端分離)
后端只需傳遞數(shù)據(jù)到前端,此時后端不需要返回整個html頁面了。
5.2 單服務(wù)秒殺后端優(yōu)化
5.2.1 減庫存優(yōu)化及修正
- 加事務(wù)
- 判斷庫存>0才進(jìn)行減庫存操作
- 添加唯一索引防止用戶重復(fù)下單,這個重復(fù)下單是防止用戶下用戶id和商品id相同的單,上面的防止重復(fù)下單是防止用戶下多個不同的單。
5.2.2 用戶重復(fù)秒殺問題
- 重復(fù)秒殺有2種,一種對同一商品進(jìn)行秒殺,通過建立用戶ID和商品ID的唯一索引防止重復(fù)秒殺。
5.2.3 使用redis優(yōu)化
- 秒殺前將商品信息都加入到redis中,進(jìn)行預(yù)減庫存,同時生成秒殺消息,發(fā)送到mq對列,給前端返回信息,前端根據(jù)返回的結(jié)構(gòu)展示正在秒殺中頁面。
- 使用內(nèi)存標(biāo)記減少對redis的訪問,原來是庫存不足時還是需要通過訪問redis,現(xiàn)在時設(shè)置額外的變量先訪問變量,當(dāng)變量種的庫存不足直接返回,較少對redis的訪問。
5.2.4 mq秒殺信息的獲取
- 數(shù)據(jù)庫查詢到秒殺的訂單則秒殺成功,返回1
- 數(shù)據(jù)庫查詢不到秒殺的訂單且redis中商品的庫存小于等于0,則秒殺失敗
- 秒殺正在進(jìn)行中
六、技術(shù)作用
6.1 redis
- 存儲商品庫存信息
- 進(jìn)行預(yù)減庫存操作
- 存儲用戶秒殺的訂單信息,進(jìn)行重復(fù)秒殺判斷
- 輔助驗(yàn)證碼驗(yàn)證
6.2 ThreadLocal
- 每個ThreadLocal都有一個自己的ThreadLocalMap,將數(shù)據(jù)以key,value的形式存到ThreadLocalMap中。
- 攔截器通過threadlocal對象將用戶的信息解析存入,在參數(shù)解析器時將用戶信息取出,進(jìn)行參數(shù)解析。此時通過threadlocal對象來存儲每個請求處理線程的用戶信息的副本,來實(shí)現(xiàn)數(shù)據(jù)訪問的隔離性。
threadlocal作用
6.3 Rabbitmq
- 異步下單
- 削峰填谷
6.4
七、安全優(yōu)化
7.1 隱藏接口地址
7.2 驗(yàn)證碼
7.3 接口限流
7.3.1 限流標(biāo)準(zhǔn)
最大qps的70%~80%文章來源:http://www.zghlxwxcb.cn/news/detail-496969.html
7.3.2 通用限流
- 實(shí)現(xiàn)根據(jù)用戶id對用戶進(jìn)行限流
- 自定義注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
int second();
int maxCount();
boolean needLogin() default true;
}
八、問題
8. 1 商品少賣
在redis中初始化生成2份相同的商品信息會導(dǎo)致商品少賣。但是庫存依然到0???文章來源地址http://www.zghlxwxcb.cn/news/detail-496969.html
九、sql語句
9.1 清空表語句
DELETE FROM t_order WHERE goods_id = 1;
DELETE FROM `t_seckill_order` WHERE goods_id = 1;
到了這里,關(guān)于java 2023秒殺項(xiàng)目 day(1) 面經(jīng)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!