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);
} }

上一篇:使用Unity3D的设计思想实现一个简单的C#赛车游戏场景


下一篇:hdu2993之斜率dp+二分查找