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=falsespring.thymeleaf.content-type=text/htmlspring.thymeleaf.enabled=truespring.thymeleaf.encoding=UTF-8spring.thymeleaf.mode=HTML5spring.thymeleaf.prefix=classpath:/templates/spring.thymeleaf.suffix=.html2:mybatis druid redis 配置# mybatismybatis.type-aliases-package=com.imooc.miaosha.domainmybatis.configuration.map-underscore-to-camel-case=truemybatis.configuration.default-fetch-size=100mybatis.configuration.default-statement-timeout=3000mybatis.mapperLocations = classpath:com/imooc/miaosha/dao/*.xml# druidspring.datasource.url=jdbc:mysql://192.168.220.128:3306/miaosha?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false ername=rootspring.datasource.password=123456spring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.type=com.alibaba.druid.pool.DruidDataSourcespring.datasource.filters=statspring.datasource.maxActive=2spring.datasource.initialSize=1spring.datasource.maxWait=60000spring.datasource.minIdle=1spring.datasource.timeBetweenEvictionRunsMillis=60000spring.datasource.minEvictableIdleTimeMillis=300000spring.datasource.validationQuery=select 'x'spring.datasource.testWhileIdle=truespring.datasource.testOnBorrow=falsespring.datasource.testOnReturn=falsespring.datasource.poolPreparedStatements=truespring.datasource.maxOpenPreparedStatements=20#redisredis.host=192.168.220.128redis.port=6379redis.timeout=3redis.password=123456redis.poolMaxTotal=10redis.poolMaxIdle=10redis.poolMaxWait=3需要引⼊以下依赖<dependency><groupId>org.mybatis.spring.boot</groupId><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.0.5</version></dependency>demoDemoController类@Controller@RequestMapping("/demo")public class DemoController {@RequestMapping("/")@ResponseBodyString home() {return "Hello World!";}//1.rest api json输出 2.页⾯@RequestMapping("/hello")@ResponseBodypublic Result<String> hello() {return Result.success("hello,imooc");// return new Result(0, "success", "hello,imooc"); }@RequestMapping("/helloError")@ResponseBodypublic 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 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");//登录模块 5002XXpublic 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//秒杀模块 5005XXprivate 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);}@Overridepublic String toString() {return "CodeMsg [code=" + code + ", msg=" + msg + "]";}}@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 配置@Servicepublic class RedisPoolFactory {@AutowiredRedisConfig redisConfig;@Beanpublic 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@Servicepublic class RedisService {@AutowiredJedisPool jedisPool;/*** 获取当个对象* */public <T> T get(KeyPrefix prefix, String key, Class<T> clazz) {Jedis jedis = null;try {jedis = jedisPool.getResource();//⽣成真正的keyString realKey = prefix.getPrefix() + key;String str = jedis.get(realKey);T t = stringToBean(str, clazz);return t;}finally {* 设置对象* */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;}//⽣成真正的keyString 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();//⽣成真正的keyString 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();//⽣成真正的keyString 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();//⽣成真正的keyString 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) {@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:userkeypublic 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")@ResponseBodypublic Result<User> redisGet() {User user = redisService.get(UserKey.getById, ""+1, User.class);public Result<Boolean> redisSet() {User user = new User();user.setId(1);user.setName("1111");redisService.set(UserKey.getById, ""+1, user);//UserKey:id1return 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>mons</groupId><artifactId>commons-lang3</artifactId><version>3.6</version></dependency>10:Validate⾃定义验证在class中的字段添加如下注解public class LoginVo {@NotNull@IsMobileprivate 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;}@Overridepublic 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 "⼿机号码格式错误";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")@ResponseBodypublic Result<Boolean> doLogin(HttpServletResponse response, @Valid LoginVo loginVo) { (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);}//⽣成cookieString 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@ResponseBodypublic class GlobalExceptionHandler {@ExceptionHandler(value=Exception.class)public Result<String> exceptionHandler(HttpServletRequest request, Exception e){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:动态添加参数@Configurationpublic class WebConfig extends WebMvcConfigurerAdapter{@AutowiredUserArgumentResolver userArgumentResolver;@Overridepublic void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {argumentResolvers.add(userArgumentResolver);}@Servicepublic class UserArgumentResolver implements HandlerMethodArgumentResolver {@AutowiredMiaoshaUserService 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;}}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")@ResponseBodypublic 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);}@RequestMapping(value="/to_detail2/{goodsId}",produces="text/html")@ResponseBodypublic 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:静态资源配置#staticspring.resources.add-mappings=truespring.resources.cache-period= 3600spring.resources.chain.cache=truespring.resources.chain.enabled=truespring.resources.chain.gzipped=truespring.resources.chain.html-application-cache=truespring.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);}//验证pathboolean 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);//10if(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)@ResponseBodypublic 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)添加拦截器拦截注解@Servicepublic class AccessInterceptor extends HandlerInterceptorAdapter{@AutowiredMiaoshaUserService userService;@AutowiredRedisService redisService;@Overridepublic 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;}}添加当前⽤户到threadlocalpublic 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();}}添加拦截器和⽅法参数@Configurationpublic class WebConfig extends WebMvcConfigurerAdapter{@AutowiredUserArgumentResolver userArgumentResolver;@AutowiredAccessInterceptor accessInterceptor;@Overridepublic void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(userArgumentResolver);}@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(accessInterceptor);}}。