Java秒殺系統方案優化 高性能高並發實戰(已完成)


1:商品列表
2:商品詳情判斷是否可以開始秒殺,

要考慮校驗活動的商品id和商品秒殺時間是否有效

商品詳情判斷是否可以開始秒殺,
未開始不顯示秒殺按鈕顯示倒計時,
開始顯示秒殺按鈕,同時會顯示驗證碼輸入框以及驗證碼圖片
(會通過userid和productid作為key驗證碼結果作為value存儲在redis中),
當點擊秒殺按鈕的時候會首先判斷驗證碼是否正確,如果正確會返回一個加密的秒殺地址(通過商品id和用戶id規則)同時存儲在redis中
拿着返回的秒殺地址去請求的時候 判斷秒殺地址是否合法,合法的話繼續秒殺不合法終止秒殺執行
如果判斷當前內存中的標識已經沒有庫存就返回秒殺完畢
否則繼續判斷redis中的庫存-1如果當前值小於0的話 把內存中的標識設置為已沒有庫存,
否則通過redis判斷是否已經秒殺過,如果沒有秒殺過就進入消息隊列
消費端
判斷是否有庫存
判斷是否已經秒殺到了
減庫存,要保證庫存不能小於0, 下訂單,(減庫存,下訂單要在一個事務中,同時有個主鍵唯一索引在用戶id和商品id在寫入訂單表的時候) 寫入秒殺訂單,


結束不顯示秒殺按鈕

 

 

1:springboot  thymeleaf配置

spring.thymeleaf.cache=false
spring.thymeleaf.content-type=text/html
spring.thymeleaf.enabled=true
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.mode=HTML5
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

2:mybatis druid redis 配置
# mybatis
mybatis.type-aliases-package=com.imooc.miaosha.domain
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.default-fetch-size=100
mybatis.configuration.default-statement-timeout=3000
mybatis.mapperLocations = classpath:com/imooc/miaosha/dao/*.xml
# druid
spring.datasource.url=jdbc:mysql://192.168.220.128:3306/miaosha?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.filters=stat
spring.datasource.maxActive=2
spring.datasource.initialSize=1
spring.datasource.maxWait=60000
spring.datasource.minIdle=1
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=select 'x'
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxOpenPreparedStatements=20
#redis
redis.host=192.168.220.128
redis.port=6379
redis.timeout=3
redis.password=123456
redis.poolMaxTotal=10
redis.poolMaxIdle=10
redis.poolMaxWait=3
需要引入以下依賴
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.5</version>
</dependency>


demo
DemoController類
@Controller
@RequestMapping("/demo")
public class DemoController {

@RequestMapping("/")
@ResponseBody
String home() {
return "Hello World!";
}
//1.rest api json輸出 2.頁面
@RequestMapping("/hello")
@ResponseBody
public Result<String> hello() {
return Result.success("hello,imooc");
// return new Result(0, "success", "hello,imooc");
}
@RequestMapping("/helloError")
@ResponseBody
public Result<String> helloError() {
return Result.error(CodeMsg.SERVER_ERROR);
//return new Result(500102, "XXX");
}

@RequestMapping("/thymeleaf")
public String thymeleaf(Model model) {
model.addAttribute("name", "Joshua");
return "hello";
}
}

public class Result<T> {

private int code;
private String msg;
private T data;

/**
* 成功時候的調用
* */
public static <T> Result<T> success(T data){
return new Result<T>(data);
}

/**
* 失敗時候的調用
* */
public static <T> Result<T> error(CodeMsg codeMsg){
return new Result<T>(codeMsg);
}

private Result(T data) {
this.data = data;
}

private Result(int code, String msg) {
this.code = code;
this.msg = msg;
}

private Result(CodeMsg codeMsg) {
if(codeMsg != null) {
this.code = codeMsg.getCode();
this.msg = codeMsg.getMsg();
}
}

public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}

public class CodeMsg {

private int code;
private String msg;

//通用的錯誤碼
public static CodeMsg SUCCESS = new CodeMsg(0, "success");
public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服務端異常");
public static CodeMsg BIND_ERROR = new CodeMsg(500101, "參數校驗異常:%s");
//登錄模塊 5002XX
public static CodeMsg SESSION_ERROR = new CodeMsg(500210, "Session不存在或者已經失效");
public static CodeMsg PASSWORD_EMPTY = new CodeMsg(500211, "登錄密碼不能為空");
public static CodeMsg MOBILE_EMPTY = new CodeMsg(500212, "手機號不能為空");
public static CodeMsg MOBILE_ERROR = new CodeMsg(500213, "手機號格式錯誤");
public static CodeMsg MOBILE_NOT_EXIST = new CodeMsg(500214, "手機號不存在");
public static CodeMsg PASSWORD_ERROR = new CodeMsg(500215, "密碼錯誤");

//商品模塊 5003XX

//訂單模塊 5004XX

//秒殺模塊 5005XX

private CodeMsg( ) {
}

private CodeMsg( int code,String msg ) {
this.code = code;
this.msg = msg;
}

public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}

public CodeMsg fillArgs(Object... args) {
int code = this.code;
String message = String.format(this.msg, args);
return new CodeMsg(code, message);
}

@Override
public String toString() {
return "CodeMsg [code=" + code + ", msg=" + msg + "]";
}


}


3:dao層 訪問代碼
@Mapper
public interface UserDao {

@Select("select * from user where id = #{id}")
public User getById(@Param("id")int id );

@Insert("insert into user(id, name)values(#{id}, #{name})")
public int insert(User user);

}

4:redis config配置
需要引入以下依賴
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.38</version>
</dependency>
@Component
@ConfigurationProperties(prefix="redis")
public class RedisConfig {
private String host;
private int port;
private int timeout;//秒
private String password;
private int poolMaxTotal;
private int poolMaxIdle;
private int poolMaxWait;//秒

}

 
        

5: redis JedisPool 配置
@Service
public class RedisPoolFactory {

@Autowired
RedisConfig redisConfig;

@Bean
public JedisPool JedisPoolFactory() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());
poolConfig.setMaxTotal(redisConfig.getPoolMaxTotal());
poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait() * 1000);
JedisPool jp = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(),
redisConfig.getTimeout()*1000, redisConfig.getPassword(), 0);
return jp;
}

}

6:redis service

@Service
public class RedisService {

@Autowired
JedisPool jedisPool;

/**
* 獲取當個對象
* */
public <T> T get(KeyPrefix prefix, String key, Class<T> clazz) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
String str = jedis.get(realKey);
T t = stringToBean(str, clazz);
return t;
}finally {
returnToPool(jedis);
}
}

/**
* 設置對象
* */
public <T> boolean set(KeyPrefix prefix, String key, T value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String str = beanToString(value);
if(str == null || str.length() <= 0) {
return false;
}
//生成真正的key
String realKey = prefix.getPrefix() + key;
int seconds = prefix.expireSeconds();
if(seconds <= 0) {
jedis.set(realKey, str);
}else {
jedis.setex(realKey, seconds, str);
}
return true;
}finally {
returnToPool(jedis);
}
}

/**
* 判斷key是否存在
* */
public <T> boolean exists(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
return jedis.exists(realKey);
}finally {
returnToPool(jedis);
}
}

/**
* 增加值
* */
public <T> Long incr(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
return jedis.incr(realKey);
}finally {
returnToPool(jedis);
}
}

/**
* 減少值
* */
public <T> Long decr(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
return jedis.decr(realKey);
}finally {
returnToPool(jedis);
}
}

private <T> String beanToString(T value) {
if(value == null) {
return null;
}
Class<?> clazz = value.getClass();
if(clazz == int.class || clazz == Integer.class) {
return ""+value;
}else if(clazz == String.class) {
return (String)value;
}else if(clazz == long.class || clazz == Long.class) {
return ""+value;
}else {
return JSON.toJSONString(value);
}
}

@SuppressWarnings("unchecked")
private <T> T stringToBean(String str, Class<T> clazz) {
if(str == null || str.length() <= 0 || clazz == null) {
return null;
}
if(clazz == int.class || clazz == Integer.class) {
return (T)Integer.valueOf(str);
}else if(clazz == String.class) {
return (T)str;
}else if(clazz == long.class || clazz == Long.class) {
return (T)Long.valueOf(str);
}else {
return JSON.toJavaObject(JSON.parseObject(str), clazz);
}
}

private void returnToPool(Jedis jedis) {
if(jedis != null) {
jedis.close();
}
}

}


7:redis key 設置
(1)接口
public interface KeyPrefix {

public int expireSeconds();

public String getPrefix();

}
(2)抽象類
public abstract class BasePrefix implements KeyPrefix{

private int expireSeconds;

private String prefix;

public BasePrefix(String prefix) {//0代表永不過期
this(0, prefix);
}

public BasePrefix( int expireSeconds, String prefix) {
this.expireSeconds = expireSeconds;
this.prefix = prefix;
}

public int expireSeconds() {//默認0代表永不過期
return expireSeconds;
}

public String getPrefix() {
String className = getClass().getSimpleName();
return className+":" + prefix;
}

}
3:userkey
public class UserKey extends BasePrefix{

private UserKey(String prefix) {
super(prefix);
}
public static UserKey getById = new UserKey("id");
public static UserKey getByName = new UserKey("name");
}

測試
@RequestMapping("/redis/get")
@ResponseBody
public Result<User> redisGet() {
User user = redisService.get(UserKey.getById, ""+1, User.class);
return Result.success(user);
}

@RequestMapping("/redis/set")
@ResponseBody
public Result<Boolean> redisSet() {
User user = new User();
user.setId(1);
user.setName("1111");
redisService.set(UserKey.getById, ""+1, user);//UserKey:id1
return Result.success(true);
}
public class MiaoshaUserKey extends BasePrefix{

public static final int TOKEN_EXPIRE = 3600*24 * 2;
private MiaoshaUserKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "tk");
}
8:MD5前端固定鹽值加密和 后端隨機鹽值加密
9:加密方法
public static String md5(String src) {
return DigestUtils.md5Hex(src);
}
需要以下依賴
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.6</version>
</dependency>

10:Validate自定義驗證
在class中的字段添加如下注解
public class LoginVo {

@NotNull
@IsMobile
private String mobile;

@NotNull
@Length(min=32)
private String password;

public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "LoginVo [mobile=" + mobile + ", password=" + password + "]";
}
}


注解聲明
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class })
public @interface IsMobile {

boolean required() default true;

String message() default "手機號碼格式錯誤";

Class<?>[] groups() default { };

Class<? extends Payload>[] payload() default { };
}


public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

private boolean required = false;

public void initialize(IsMobile constraintAnnotation) {
required = constraintAnnotation.required();
}

public boolean isValid(String value, ConstraintValidatorContext context) {
if(required) {
return ValidatorUtil.isMobile(value);
}else {
if(StringUtils.isEmpty(value)) {
return true;
}else {
return ValidatorUtil.isMobile(value);
}
}
}

}

使用方法

@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(HttpServletResponse response, @Valid LoginVo loginVo) {
log.info(loginVo.toString());
//登錄
userService.login(response, loginVo);
return Result.success(true);
}

service層 login 方法

public boolean login(HttpServletResponse response, LoginVo loginVo) {
if(loginVo == null) {
throw new GlobalException(CodeMsg.SERVER_ERROR);
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
//判斷手機號是否存在
MiaoshaUser user = getById(Long.parseLong(mobile));
if(user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//驗證密碼
String dbPass = user.getPassword();
String saltDB = user.getSalt();
String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
if(!calcPass.equals(dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
//生成cookie
String token = UUIDUtil.uuid();
addCookie(response, token, user);
return true;
}

private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
redisService.set(MiaoshaUserKey.token, token, user);
Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
cookie.setPath("/");
response.addCookie(cookie);
}


11:全局異常處理
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(value=Exception.class)
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
e.printStackTrace();
if(e instanceof GlobalException) {
GlobalException ex = (GlobalException)e;
return Result.error(ex.getCm());
}else if(e instanceof BindException) {
BindException ex = (BindException)e;
List<ObjectError> errors = ex.getAllErrors();
ObjectError error = errors.get(0);

return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
String msg = error.getDefaultMessage();
}else {
return Result.error(CodeMsg.SERVER_ERROR);
}
}



}
12:UUID生成
public class UUIDUtil {
public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}
13:CookieValue使用
@CookieValue(value="name",required=false) String name,

14:動態添加參數

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{

@Autowired
UserArgumentResolver userArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}

@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

@Autowired
MiaoshaUserService userService;

public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz==MiaoshaUser.class;
}

public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);

String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return null;
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
return userService.getByToken(response, token);
}

private String getCookieValue(HttpServletRequest request, String cookiName) {
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies) {
if(cookie.getName().equals(cookiName)) {
return cookie.getValue();
}
}
return null;
}

}


service層代碼

public MiaoshaUser getByToken(HttpServletResponse response, String token) {
if(StringUtils.isEmpty(token)) {
return null;
}
MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
//延長有效期
if(user != null) {
addCookie(response, token, user);
}
return user;
}


15:秒殺詳情頁通過后端判斷當前秒殺狀態
@RequestMapping("/to_detail/{goodsId}")
public String detail(Model model,MiaoshaUser user,
@PathVariable("goodsId")long goodsId) {
model.addAttribute("user", user);

GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
model.addAttribute("goods", goods);

long startAt = goods.getStartDate().getTime();
long endAt = goods.getEndDate().getTime();
long now = System.currentTimeMillis();

int miaoshaStatus = 0;
int remainSeconds = 0;
if(now < startAt ) {//秒殺還沒開始,倒計時
miaoshaStatus = 0;
remainSeconds = (int)((startAt - now )/1000);
}else if(now > endAt){//秒殺已經結束
miaoshaStatus = 2;
remainSeconds = -1;
}else {//秒殺進行中
miaoshaStatus = 1;
remainSeconds = 0;
}
model.addAttribute("miaoshaStatus", miaoshaStatus);
model.addAttribute("remainSeconds", remainSeconds);
return "goods_detail";

}
16:獲取插入后的id
@Insert("insert into order_info(user_id, goods_id, goods_name, goods_count, goods_price, order_channel, status, create_date)values("
+ "#{userId}, #{goodsId}, #{goodsName}, #{goodsCount}, #{goodsPrice}, #{orderChannel},#{status},#{createDate} )")
@SelectKey(keyColumn="id", keyProperty="id", resultType=long.class, before=false, statement="select last_insert_id()")
public long insert(OrderInfo orderInfo);

17:

(1)頁面緩存,url緩存,對象緩存
(2)頁面靜態化,前后端分離
(3)頁面靜態資源優化

 

18:頁面緩存

@RequestMapping(value="/to_list", produces="text/html")
@ResponseBody
public String list(HttpServletRequest request, HttpServletResponse response, Model model,MiaoshaUser user) {
model.addAttribute("user", user);
//取緩存
String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
if(!StringUtils.isEmpty(html)) {
return html;
}
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList);
// return "goods_list";
SpringWebContext ctx = new SpringWebContext(request,response,
request.getServletContext(),request.getLocale(), model.asMap(), applicationContext );
//手動渲染
html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
if(!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsList, "", html);
}
return html;

}
19:url緩存
   @RequestMapping(value="/to_detail2/{goodsId}",produces="text/html")
@ResponseBody
public String detail2(HttpServletRequest request, HttpServletResponse response, Model model,MiaoshaUser user,
@PathVariable("goodsId")long goodsId) {
model.addAttribute("user", user);

//取緩存
String html = redisService.get(GoodsKey.getGoodsDetail, ""+goodsId, String.class);
if(!StringUtils.isEmpty(html)) {
return html;
}
//手動渲染
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
model.addAttribute("goods", goods);

long startAt = goods.getStartDate().getTime();
long endAt = goods.getEndDate().getTime();
long now = System.currentTimeMillis();

int miaoshaStatus = 0;
int remainSeconds = 0;
if(now < startAt ) {//秒殺還沒開始,倒計時
miaoshaStatus = 0;
remainSeconds = (int)((startAt - now )/1000);
}else if(now > endAt){//秒殺已經結束
miaoshaStatus = 2;
remainSeconds = -1;
}else {//秒殺進行中
miaoshaStatus = 1;
remainSeconds = 0;
}
model.addAttribute("miaoshaStatus", miaoshaStatus);
model.addAttribute("remainSeconds", remainSeconds);
// return "goods_detail";

SpringWebContext ctx = new SpringWebContext(request,response,
request.getServletContext(),request.getLocale(), model.asMap(), applicationContext );
html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", ctx);
if(!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsDetail, ""+goodsId, html);
}
return html;
}
20:靜態資源配置
#static
spring.resources.add-mappings=true
spring.resources.cache-period= 3600
spring.resources.chain.cache=true
spring.resources.chain.enabled=true
spring.resources.chain.gzipped=true
spring.resources.chain.html-application-cache=true
spring.resources.static-locations=classpath:/static/

21:避免超賣
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0")
public int reduceStock(MiaoshaGoods g);

22:秒殺接口優化
1:redis預減庫存減少數據庫訪問
2:內存標記減少數據庫訪問
3:請求先寫入隊列異步下單

22:超賣問題
1:數據庫添加唯一索引防止用戶重復下單
2:sql減庫存數量判斷,防止庫存變為負數


23:秒殺最終代碼
public Result<Integer> miaosha(Model model,MiaoshaUser user,
@RequestParam("goodsId")long goodsId,
@PathVariable("path") String path) {
model.addAttribute("user", user);
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
//驗證path
boolean check = miaoshaService.checkPath(user, goodsId, path);
if(!check){
return Result.error(CodeMsg.REQUEST_ILLEGAL);
}
//內存標記,減少redis訪問
boolean over = localOverMap.get(goodsId);
if(over) {
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//預減庫存
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10
if(stock < 0) {
localOverMap.put(goodsId, true);
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//判斷是否已經秒殺到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
//入隊
MiaoshaMessage mm = new MiaoshaMessage();
mm.setUser(user);
mm.setGoodsId(goodsId);
sender.sendMiaoshaMessage(mm);
return Result.success(0);//排隊中
}
24:庫存redis初始化
public class MiaoshaController implements InitializingBean {
private HashMap<Long, Boolean> localOverMap =  new HashMap<Long, Boolean>();

/**
* 系統初始化
* */
public void afterPropertiesSet() throws Exception {
List<GoodsVo> goodsList = goodsService.listGoodsVo();
if(goodsList == null) {
return;
}
for(GoodsVo goods : goodsList) {
redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
localOverMap.put(goods.getId(), false);
}
}

}
25:秒殺接口地址隱藏

秒殺按鈕點擊的時候先去獲取秒殺接口地址,同時再去請求秒殺接口地址的時候判斷地址是否合法

@AccessLimit(seconds=5, maxCount=5, needLogin=true)
@RequestMapping(value="/path", method=RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
@RequestParam("goodsId")long goodsId,
@RequestParam(value="verifyCode", defaultValue="0")int verifyCode
) {
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
if(!check) {
return Result.error(CodeMsg.REQUEST_ILLEGAL);
}
String path =miaoshaService.createMiaoshaPath(user, goodsId);
return Result.success(path);
}


public String createMiaoshaPath(MiaoshaUser user, long goodsId) {
if(user == null || goodsId <=0) {
return null;
}
String str = MD5Util.md5(UUIDUtil.uuid()+"123456");
redisService.set(MiaoshaKey.getMiaoshaPath, ""+user.getId() + "_"+ goodsId, str);
return str;
}

26:在獲取秒殺地址之前添加驗證碼驗證
這樣做的好處1:避免機器防刷2:可以起到分流的作用
27:接口限流
1添加注解
@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit {
int seconds();
int maxCount();
boolean needLogin() default true;
}

使用
@AccessLimit(seconds=5, maxCount=5, needLogin=true)

添加攔截器攔截注解

@Service
public class AccessInterceptor extends HandlerInterceptorAdapter{

@Autowired
MiaoshaUserService userService;

@Autowired
RedisService redisService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if(handler instanceof HandlerMethod) {
MiaoshaUser user = getUser(request, response);
UserContext.setUser(user);
HandlerMethod hm = (HandlerMethod)handler;
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit == null) {
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if(needLogin) {
if(user == null) {
render(response, CodeMsg.SESSION_ERROR);
return false;
}
key += "_" + user.getId();
}else {
//do nothing
}
AccessKey ak = AccessKey.withExpire(seconds);
Integer count = redisService.get(ak, key, Integer.class);
if(count == null) {
redisService.set(ak, key, 1);
}else if(count < maxCount) {
redisService.incr(ak, key);
}else {
render(response, CodeMsg.ACCESS_LIMIT_REACHED);
return false;
}
}
return true;
}

private void render(HttpServletResponse response, CodeMsg cm)throws Exception {
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
String str = JSON.toJSONString(Result.error(cm));
out.write(str.getBytes("UTF-8"));
out.flush();
out.close();
}

private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {
String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return null;
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
return userService.getByToken(response, token);
}

private String getCookieValue(HttpServletRequest request, String cookiName) {
Cookie[] cookies = request.getCookies();
if(cookies == null || cookies.length <= 0){
return null;
}
for(Cookie cookie : cookies) {
if(cookie.getName().equals(cookiName)) {
return cookie.getValue();
}
}
return null;
}

}

添加當前用戶到threadlocal
public class UserContext {

private static ThreadLocal<MiaoshaUser> userHolder = new ThreadLocal<MiaoshaUser>();

public static void setUser(MiaoshaUser user) {
userHolder.set(user);
}

public static MiaoshaUser getUser() {
return userHolder.get();
}

}

添加攔截器和方法參數
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{

@Autowired
UserArgumentResolver userArgumentResolver;

@Autowired
AccessInterceptor accessInterceptor;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessInterceptor);
}

}








免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM