Java笔记之高并发秒杀API-四
Java秒杀系统方案优化高性能高并发实战(已完成)
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);}}。
用Java实现高并发原理详解
用Java实现高并发原理详解随着互联网的快速发展,高并发成为了互联网应用开发中不可避免的问题。
尤其是对于一些大型网站和应用,高并发处理能力更是必不可少。
而Java作为一种广泛应用的编程语言,其高并发处理机制也备受关注。
一、Java中高并发的本质在Java中,高并发的本质就是多线程并发访问。
在多线程中,会出现多个线程同时访问同一个共享资源的情况,如何保证资源的正确性和并发性就是高并发处理的核心问题。
Java中采用锁机制来实现高并发处理。
锁机制可以确保在同一时间内只有一个线程能够进行访问,从而保证共享资源的正确性。
在Java中,锁的实现主要有两种方式:synchronized关键字和Java 并发包中的Lock接口。
二、synchronized关键字的使用synchronized关键字是Java中最基本的锁机制。
synchronized关键字可以修饰方法和代码块,它的基本使用方式如下所示:synchronized void method() {// 代码块}synchronized(obj) {// 代码块}其中,synchronized修饰方法时,锁定的是当前对象;synchronized修饰代码块时,锁定的是obj对象。
使用synchronized关键字时需要注意的是,锁的范围应该尽可能小,以免出现性能问题。
同时,对于一些不涉及共享资源的方法和代码块,不应该使用synchronized关键字。
三、Java并发包中的Lock接口Java并发包中的Lock接口是一种更加灵活的锁机制。
Lock接口提供了比synchronized关键字更加灵活和高效的锁机制,并且可以支持更加复杂的并发模式。
Lock接口的常用实现类是ReentrantLock。
ReentrantLock实现了Lock接口,并且提供了更加可靠和灵活的线程同步机制。
在使用ReentrantLock时,需要手动进行加锁和释放锁的操作。
例如:Lock lock = new ReentrantLock();lock.lock();try {// 代码块} finally {lock.unlock();}需要注意的是,在使用Lock接口时,必须手动进行锁的释放操作,否则可能会出现死锁的情况。
java 高并发处理
在Java中处理高并发可以采用以下几种方法:1. 线程池:使用线程池可以重用线程,减少线程的创建和销毁开销。
Java提供了 java.util.concurrent.Executor 和java.util.concurrent.ExecutorService 接口,以及 java.util.concurrent.Executors 类来创建和管理线程池。
2. 锁机制:使用锁机制可以确保在同一时间只有一个线程可以访问共享资源。
Java提供了内置的锁机制,如 synchronized 关键字和 ReentrantLock 类。
3. 并发集合:Java提供了一些并发安全的集合类,如ConcurrentHashMap 和 ConcurrentLinkedQueue ,它们可以在多线程环境下安全地进行读写操作。
4. 原子操作:Java提供了一些原子操作类,如 AtomicInteger 和 AtomicLong ,它们可以确保在多线程环境下的原子性操作,避免了线程安全问题。
5. 无锁算法:无锁算法可以减少线程之间的竞争,提高并发性能。
Java中的 java.util.concurrent.atomic 包提供了一些无锁算法的支持,如AtomicInteger 和 AtomicReference 。
6. 分布式缓存:对于高并发场景,可以使用分布式缓存来减轻数据库的压力。
常见的分布式缓存解决方案包括Redis和Memcached。
7. 异步编程:使用异步编程可以将一些耗时的操作放在后台线程中进行,提高系统的并发处理能力。
Java 8引入了 CompletableFuture 和 CompletionStage 等API来支持异步编程。
以上是一些常见的Java高并发处理方法。
根据具体的需求和场景,可以选择适合的方法来提高系统的并发性能。
Java并发编程指南:实现高效并发操作
Java并发编程指南:实现高效并发操作导语:在当今的软件开发领域,高效并发操作是实现性能优化和提升用户体验的关键。
Java作为一种广泛应用的编程语言,其并发编程能力得到了广泛的认可和应用。
本文将为读者介绍Java并发编程的基本概念、常见问题以及一些实现高效并发操作的技巧和指南。
一、并发编程概述并发编程是指多个任务同时进行的编程模式。
在传统的单线程编程模式中,任务按照顺序依次执行,而在并发编程中,多个任务可以同时执行,提高了程序的执行效率和并行处理能力。
Java提供了一系列的并发编程工具和API,例如线程、锁、信号量、线程池等,使得开发者能够更加方便地实现并发操作。
然而,并发编程也带来了一些挑战,例如线程安全、死锁、竞态条件等问题,需要开发者有一定的经验和技巧来解决。
二、并发编程的基本概念1. 线程(Thread):线程是程序中的执行单元,可以同时执行多个线程来实现并发操作。
Java中的线程通过继承Thread类或实现Runnable接口来创建。
2. 锁(Lock):锁是一种同步机制,用于控制对共享资源的访问。
Java中的锁可以分为悲观锁和乐观锁,常见的锁包括synchronized关键字、ReentrantLock类等。
3. 信号量(Semaphore):信号量用于控制同时访问某个资源的线程数目。
Java中的Semaphore类可以用来实现信号量机制。
4. 线程池(ThreadPool):线程池是一种管理线程的机制,可以避免频繁创建和销毁线程的开销。
Java中的Executor框架提供了线程池的实现。
三、常见的并发编程问题1. 线程安全:在并发编程中,多个线程同时访问共享资源可能会导致数据不一致的问题。
为了保证线程安全,可以使用锁机制或使用线程安全的数据结构。
2. 死锁:死锁是指两个或多个线程无限期地等待对方释放资源,导致程序无法继续执行。
为了避免死锁,可以使用避免策略,例如按照固定的顺序获取锁。
java秒杀活动实现思路 -回复
java秒杀活动实现思路-回复如何使用Java实现秒杀活动====================================引言Java是一种广泛应用的计算机编程语言,可以用于开发各种类型的应用程序,包括电子商务平台。
本文将就如何使用Java实现秒杀活动的思路,为你提供一种简单而高效的解决方案。
秒杀活动是指在特定时间段内,通过限时限量的方式售卖商品,吸引用户参与并促进销售增长。
步骤一:设计数据库模式首先,我们需要设计一个数据库模式来存储商品和订单信息。
在设计时,我们需要考虑到以下几个方面:- 商品表:包含商品的名称、描述、价格、库存等信息。
- 订单表:包含订单的编号、用户的信息、商品的信息等。
- 库存表:记录每个商品的库存量,以便在秒杀活动过程中进行动态更新。
步骤二:实现商品列表页面接下来,我们需要实现一个商品列表页面,用于展示所有参与秒杀活动的商品。
该页面应该包括每个商品的名称、描述、价格以及库存量信息。
用户可以通过该页面选择感兴趣的商品并进行购买操作。
步骤三:实现秒杀活动倒计时在秒杀活动开始之前,我们需要实现一个倒计时功能,以便用户知晓离活动开始还有多少时间。
倒计时可以采用Java的定时器功能来实现,每秒刷新一次活动页面上的倒计时信息。
步骤四:处理用户购买请求当用户选择购买某个商品后,我们需要对其购买请求进行处理。
首先,我们需要检查该商品的库存量是否充足,如果库存不足,则向用户返回“商品已经售罄”的信息。
如果库存充足,则进一步处理用户的订单信息。
步骤五:生成订单并扣减库存在处理用户的购买请求时,我们需要生成一个唯一的订单编号,并将订单信息存储到数据库中。
同时,我们还需要扣减该商品的库存量,以防止其他用户继续购买该商品。
为了防止多线程并发访问导致的库存混乱问题,可以使用分布式锁或者乐观锁进行并发控制。
步骤六:实时更新活动页面在秒杀活动进行过程中,我们需要实时更新活动页面上的库存信息,以便用户可以及时获得商品的售卖情况。
实战Java高并发编程
实战Java高并发编程在当今互联网时代,高并发架构已经成为了各个领域的热门话题。
在Java 编程领域,面对海量的并发连接和并发访问,如何设计高效的并发编程系统,是每个Java开发人员必备的技能。
Java语言作为一种面向对象、跨平台的高级编程语言,拥有广泛的应用场景,可应用于Windows、Linux等多个操作系统及多种嵌入式设备。
同时Java具有强大的生态环境和充足的开发资源,这使得Java在高并发编程领域具有优势。
Java 提供的一些基础的并发编程工具及框架,如 synchronized、volatile、ConcurrentHashMap、ThreadPoolExecutor、Future 等,常被用于在Java平台上开发高并发应用。
除此之外,开发人员还可以利用第三方开源框架,如Netty、Redis 等进行高效的并发编程。
在实战Java高并发编程中,以下几个方面需要着重关注:1. 多线程编程Java的多线程编程是Java高并发编程的核心之一,它可以通过Thread类、Runnable接口、Callable接口等来实现。
在多线程编程中,需要注意线程安全问题,如何解决共享资源的并发引用问题。
2. 线程池线程池的作用就是为了重复使用已创建的线程,减少线程创建和销毁的开销,从而提高系统的性能。
Java中提供了Executor接口和ThreadPoolExecutor类来实现线程池。
3. 锁锁机制是Java并发编程中的一种解决并发问题的手段。
Java中的锁可以分为悲观锁和乐观锁。
悲观锁是通过在访问前对所关心的数据加锁,从而保证只有一个线程可以访问。
而乐观锁则是在数据变动后再进行更新操作,采用CAS(Compare And Swap)算法来保证数据的正确性。
4. 并发容器Java提供了一些并发容器,如ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentLinkedQueue等,用于处理并发访问问题。
Java高并发,如何解决,什么方式解决
<hibernate-mapping package="com.xiaohao.test">
<class name="User" table="user" optimistic-lock="version" > <id name="id">
<generator class="native" /> </id> <!--version标签必须跟在id标签后面--> <version column="version" name="version" /> <property name="userName"/> <property name="password"/>
Java高并发面试知识点归纳
Java高并发面试知识点归纳在当今互联网时代,高并发成为了许多应用程序开发中必须要考虑的重要因素。
而对于Java开发者来说,掌握高并发编程是非常关键的技能之一。
在面试中,常常会被问及与Java高并发相关的知识点。
本文将对Java高并发面试知识点进行归纳总结,以帮助读者在面试中有所准备。
一、什么是高并发?高并发是指系统能够同时处理大量的并发请求。
在现实生活中,高并发场景非常常见,比如许多热门网站、电商平台、移动应用等都需要应对大量用户的访问请求。
二、Java中的并发编程工具1. 线程和Runnable接口Java中最基本的并发编程工具就是线程和Runnable接口。
通过创建线程对象或实现Runnable接口,可以实现多线程的并发执行。
2. synchronized关键字synchronized关键字可以用来修饰方法或代码块,保证在同一时间内只有一个线程可以执行被synchronized修饰的代码,从而实现数据的安全性。
但使用synchronized关键字会导致性能下降,因此需要合理使用。
3. Lock接口Lock接口是Java提供的另一种用于实现线程同步的方式。
与synchronized相比,Lock接口提供了更加灵活的锁定机制,可以实现更细粒度的控制。
4. wait()、notify()和notifyAll()方法wait()、notify()和notifyAll()方法是Object类中定义的方法,用于实现线程间的通信。
wait()方法使调用该方法的线程释放对象锁,并进入等待状态;notify()方法用于唤醒一个正在等待的线程;notifyAll()方法用于唤醒所有正在等待的线程。
5. ConcurrentHashMapConcurrentHashMap是Java提供的线程安全的哈希表实现,可以在高并发环境下高效地读写数据。
它使用了分段锁的机制,可以同时支持多个线程对不同的分段进行读写操作。
三、常见的高并发问题及解决方案1. 线程安全问题在高并发环境下,由于多个线程可能同时访问共享数据,容易导致线程安全问题。
Java秒杀实战(五)页面级高并发秒杀优化(Redis缓存+静态化分离)
Java秒杀实战(五)页⾯级⾼并发秒杀优化(Redis缓存+静态化分离)我们发现,⽬前系统最⼤的瓶颈就在数据库访问。
因此,系统优化的⽅案核⼼在于减少数据库的访问,⽽缓存就是⼀个好⽅法。
⼀、页⾯缓存以商品列表为例,Controller⽅法改造如下@RequestMapping(value = "/to_list", produces = "text/html")@ResponseBodypublic String toList(HttpServletRequest request, HttpServletResponse response, Model model,SeckillUser seckillUser) {// 取缓存String html = redisService.get(GoodsKey.getGoodsList, "", String.class);if (!StringUtils.isEmpty(html)) {return html;}List<GoodsVo> goodsList = goodsService.listGoodsVo();model.addAttribute("goodsList", goodsList);// ⼿动渲染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;}⼆、URL缓存跟页⾯缓存原理⼀样,只是根据不同的url参数从缓存中获取不同的页⾯数据以查看商品详情的⽅法为例,Controller⽅法改造如下@RequestMapping(value = "/to_detail/{goodsId}", produces = "text/html")@ResponseBodypublic String detail(HttpServletRequest request, HttpServletResponse response, Model model, SeckillUser seckillUser,@PathVariable("goodsId") long goodsId) {// 取缓存String html = redisService.get(GoodsKey.getGoodsDetail, "" + goodsId, String.class);if (!StringUtils.isEmpty(html)) {return html;}model.addAttribute("user", seckillUser);GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);model.addAttribute("goods", goods);long startAt = goods.getStartDate().getTime();long endAt = goods.getEndDate().getTime();long now = System.currentTimeMillis();int seckillStatus = 0;int remainSeconds = 0;if (now < startAt) {// 秒杀还没开始,倒计时seckillStatus = 0;remainSeconds = (int) ((startAt - now) / 1000);} else if (now > endAt) {// 秒杀已经结束seckillStatus = 2;remainSeconds = -1;} else {// 秒杀进⾏中seckillStatus = 1;remainSeconds = 0;}model.addAttribute("seckillStatus", seckillStatus);model.addAttribute("remainSeconds", remainSeconds);// ⼿动渲染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;}三、对象缓存对象缓存控制粒度⽐页⾯缓存细,但要注意对象变更时缓存值的处理SeckillUserService⽅法修改如下:public SeckillUser getById(long id){//取缓存SeckillUser user = redisService.get(SeckillUserKey.getById, "" + id, SeckillUser.class);if(user != null){return user;}//取数据库user = seckillUserDao.getById(id);if(user != null){redisService.set(SeckillUserKey.getById, "" + id, user);}return user;}public boolean updatePassword(long id, String token, String formPass){SeckillUser user = getById(id);if(user == null){throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);}SeckillUser toBeUpdated = new SeckillUser();toBeUpdated.setId(id);toBeUpdated.setPassword(Md5Util.formPass2DbPass(formPass, user.getSalt()));seckillUserDao.update(toBeUpdated);redisService.delete(SeckillUserKey.getById, "" + id);user.setPassword(toBeUpdated.getPassword());redisService.set(SeckillUserKey.token, token, user);return true;}四、页⾯静态化(前后端分离)简单的说,就是html页⾯ + ajax。
java 高并发解决方案
Java 高并发解决方案引言在当今互联网时代,高并发成为了一个常见的问题。
由于使用Java开发的应用程序在企业级应用中广泛应用,因此需要找到适用于Java的高并发解决方案。
本文将介绍一些流行的Java高并发解决方案,帮助开发人员更好地处理高并发场景。
1. 并发编程基础在了解高并发解决方案之前,首先需要理解并发编程的基础知识。
并发编程是指多个线程同时执行的一种编程方式。
在Java中,可以使用多线程技术来实现并发编程。
以下是一些重要的并发编程基础概念:•线程:线程是执行程序的最小单元,它独立于其他线程执行。
•线程安全:线程安全是指多线程访问共享资源时保证数据一致性的能力。
•临界区:临界区是指一段需要互斥访问的代码块,在同一时刻只能由一个线程访问。
•互斥锁:互斥锁是一种用于保护临界区的同步机制,它确保任意时刻只有一个线程可以执行临界区的代码。
2. Java 高并发解决方案2.1 锁机制锁机制是一种常见的解决并发问题的方法。
Java中提供了多种锁机制,其中最常用的是synchronized关键字和ReentrantLock类。
2.1.1 synchronized关键字synchronized关键字是Java中最基本的锁机制。
通过在方法或代码块上加上synchronized关键字,可以确保同一时刻只有一个线程执行该方法或代码块。
public synchronized void synchronizedMethod() {// synchronized关键字修饰的方法}public void doSomething() {synchronized (this) {// synchronized关键字修饰的代码块}}2.1.2 ReentrantLock类ReentrantLock是Java.util.concurrent包中的一个类,它实现了Lock接口,提供了更灵活的锁机制。
相比于synchronized关键字,ReentrantLock提供了更多的功能,如可重入性、公平性和超时等待。
Java高并发面试知识点全面整理
Java高并发面试知识点全面整理一、什么是高并发在计算机领域中,高并发指的是一个系统在同一时间处理大量的并发请求。
在Java开发中,高并发是指一个系统在处理大量的并发请求时,仍能保持高性能、高可用的能力。
二、为什么需要处理高并发随着互联网的快速发展,越来越多的系统需要处理大量的请求。
在高并发情况下,如果系统无法有效地处理请求,可能导致系统响应时间延迟,甚至系统崩溃,从而影响用户体验和业务流程。
三、解决高并发的常用策略1. 水平扩展水平扩展是通过增加服务器数量来提高系统的并发处理能力。
可以通过负载均衡技术将请求分发到多个服务器上,从而实现高并发请求的处理。
2. 数据库优化数据库通常是系统的瓶颈之一,对其进行优化可以提升系统的并发处理能力。
一些常见的数据库优化策略包括:合理设计数据库表结构、使用索引和适当的查询语句、使用数据库缓存等。
3. 异步处理将一些耗时较长的操作转为异步处理可以有效提升系统的并发处理能力。
使用消息队列或者线程池等机制可以将请求的处理与结果的返回解耦,从而提高并发处理能力。
4. 缓存技术使用缓存技术可以减轻数据库的压力,提高数据的访问速度。
常见的缓存技术有:本地缓存、分布式缓存、页面缓存等。
5. 排队等待当系统的并发请求数超过系统的处理能力时,可以采用排队等待的机制。
使用队列来缓冲请求,然后按照一定的规则处理队列中的请求,从而实现高并发请求的处理。
四、高并发常用框架和工具1. 线程池线程池是Java中用于管理线程的机制,可以有效控制并发线程的数量,提高系统的并发处理能力。
2. 并发集合类Java提供了一些线程安全的并发集合类,如ConcurrentHashMap、ConcurrentLinkedQueue等,可以在高并发场景下安全地访问和操作集合。
3. 消息队列消息队列是一种高效的异步通信机制,常用于解耦请求的处理与结果的返回。
常见的消息队列有ActiveMQ、RabbitMQ等。
java秒杀活动实现思路
java秒杀活动实现思路Java秒杀活动实现思路一、引言秒杀活动是电商行业中非常常见的一种促销方式,通过限时限量的特价商品吸引消费者的关注和购买。
然而,由于瞬时高并发的访问压力,以及商品数量有限等因素,秒杀活动的实现并非易事。
本文将介绍一种基于Java 的秒杀活动实现思路,帮助读者了解如何在高并发环境下实现稳定可靠的秒杀系统。
二、系统架构设计1. 单体应用架构在本文中,我们将使用传统的单体应用架构来实现秒杀活动。
单体应用架构由数据库、应用服务器和前端服务器组成,通过JDBC或ORM技术连接数据库与应用服务器,再通过Web服务器和负载均衡器将请求分发给前端服务器。
2. 高可用部署为了应对高并发的访问压力,我们可以采用多台服务器的方式进行部署。
通过负载均衡器将请求分发给多个应用服务器,从而实现水平扩展。
3. 数据库设计在秒杀活动中,商品的库存是非常关键的一部分。
我们可以设计一个商品库存表,记录商品的ID、名称、价格和库存数量等信息。
同时,为了记录秒杀活动的订单信息,我们可以设计一个订单表,记录订单的ID、商品ID、用户ID、购买数量和支付状态等信息。
三、业务逻辑设计1. 抢购接口设计首先,我们需要设计一个用于抢购的接口。
该接口接收用户ID和商品ID 作为参数,验证用户的合法性,并检查商品的库存是否充足。
如果库存充足,则扣减库存并生成订单,返回抢购成功的信息;如果库存不足,则返回抢购失败的信息。
2. 库存扣减设计在抢购接口中,库存扣减是一个非常关键的步骤。
为了避免超卖现象的发生,我们需要使用事务来保证扣减库存和生成订单的原子性操作。
通过数据库的锁机制,可以避免多个用户同时购买同一件商品的情况发生。
3. 接口限流设计由于秒杀活动往往会吸引大量用户的关注和访问,我们需要对接口进行限流,以防止服务器过载。
可以使用系统资源监控工具,统计接口的访问次数和响应时间,并设置合理的请求阈值和响应时间阈值,当超过阈值时进行限流操作,比如通过接口返回错误码或者延迟响应等方式进行限流。
java秒杀活动实现思路
java秒杀活动实现思路Java秒杀活动的实现思路可以分为以下几个步骤:1.数据库设计:首先需要设计数据库表结构来存储参与秒杀的商品信息和用户的秒杀订单信息。
常见的表包括商品表、秒杀活动表、秒杀订单表等。
商品表存储商品的基本信息,秒杀活动表存储活动的开始时间、结束时间等信息,秒杀订单表存储用户的秒杀订单相关信息。
2.商品发布:在秒杀活动开始前,需要先将待秒杀的商品信息存储到数据库中。
可以通过后台管理系统或者接口来实现商品的发布功能,包括商品的名称、库存量、价格等。
3.秒杀接口设计:设计秒杀接口,供用户参与秒杀活动。
该接口需要处理请求的并发访问,因此要考虑到高并发场景下的性能和安全问题。
4.并发控制:在高并发场景下,秒杀活动可能会引发超卖问题,因此需要实现并发控制机制来保证商品的库存不会出现负数。
常见的控制手段包括悲观锁和乐观锁。
悲观锁是在数据库层面上对相关记录进行加锁,来保证读写的一致性。
乐观锁是通过在更新记录时比较版本号来保证操作的原子性和一致性。
5.订单生成:秒杀成功后,生成用户的秒杀订单并插入到秒杀订单表中。
该步骤要保证订单的生成是原子操作,可以通过数据库的事务来实现。
6.结果页面展示:在秒杀活动结束后,可以设计秒杀成功和秒杀失败的结果页面来展示给用户。
可以使用HTML和CSS来设计页面,并从数据库中获取相应的数据进行展示。
7.秒杀活动时间控制:对于秒杀活动的开始和结束时间需要进行控制,可以通过定时任务或者启动/停止接口来实现。
在活动开始和结束时,需要将对应的标志位设置为有效或无效。
除了以上的基本思路,还有一些技术方案可以应用到秒杀活动的实现中:1.缓存:可以使用缓存技术来提高读取商品信息的速度,如Redis 或Memcached。
将商品信息缓存在内存中,避免每次读取都要从数据库中查询。
同时缓存也可以用于减轻数据库读写压力,可以将用户的秒杀订单信息和商品库存信息等缓存到内存中。
2.分布式部署:对于高并发的秒杀活动,可以考虑将系统部署在多台服务器上,通过负载均衡来分担请求的压力。
Java高并发秒杀API之Service层
Java⾼并发秒杀API之Service层Java⾼并发秒杀API之Service层第1章秒杀业务接⼝设计与实现1.1service层开发之前的说明开始Service层的编码之前,我们⾸先需要进⾏Dao层编码之后的思考:在Dao层我们只完成了针对表的相关操作包括写了接⼝⽅法和映射⽂件中的sql语句,并没有编写逻辑的代码,例如对多个Dao层⽅法的拼接,当我们⽤户成功秒杀商品时我们需要进⾏商品的减库存操作(调⽤SeckillDao接⼝)和增加⽤户明细(调⽤SuccessKilledDao接⼝),这些逻辑我们都需要在Service层完成。
这也是⼀些初学者容易出现的错误,他们喜欢在Dao层进⾏逻辑的编写,其实Dao就是数据访问的缩写,它只进⾏数据的访问操作,接下来我们便进⾏Service层代码的编写。
1.2秒杀service接⼝设计在org.myseckill下创建⼀个service包⽤于存放我们的Service接⼝和其实现类,创建⼀个exception包⽤于存放service层出现的异常例如重复秒杀商品异常、秒杀已关闭等允许出现的异常,⼀个dto包作为数据传输层,dto和entity的区别在于:entity⽤于业务数据的封装,⽽dto关注的是web和service层的数据传递。
⾸先创建我们Service接⼝,⾥⾯的⽅法应该是按”使⽤者”的⾓度去设计,SeckillService.java,代码如下:/*** 该接⼝中前⾯两个⽅法返回的都是跟我们业务相关的对象,⽽后两个⽅法返回的对象与业务不相关,这两个对象我们⽤于封装service和web层传递的数据* 业务接⼝,站在“使⽤者”的⾓度设计接⼝,⽽不是如何实现* 三个⽅⾯:⽅法定义粒度,参数(越简练越直接越好),返回类型(retrun 类型(要友好)/异常(有的业务允许抛出异常))* @author TwoHeads**/public interface SeckillService {/*** 查询所有的秒杀记录* @return*/List<Seckill> getSeckillList();/***查询单个秒杀记录* @param seckillId* @return*/Seckill getById(long seckillId);/*** 秒杀开启时输出秒杀接⼝地址,* 否则输出系统时间和秒杀时间* 防⽌⽤户提前拼接出秒杀url通过插件进⾏秒杀* @param seckillId*/Exposer exportSeckillUrl(long seckillId);/*** 执⾏秒杀操作,如果传⼊的md5与内部的不相符,说明⽤户的url被篡改了,此时拒绝执⾏秒杀* 有可能失败,有可能成功,所以要抛出我们允许的异常* @param seckillId* @param userPhone* @param md5*/SeckillExecution executeSeckill(long seckillId,long userPhone,String md5) throws SeckillException,SeckillCloseException,RepeatKillException;}相应在的dto包中创建Exposer.java,⽤于封装秒杀的地址信息,代码如下:/*** 暴露秒杀地址DTO(数据传输层)* @author TwoHeads**/public class Exposer {//是否开启秒杀private boolean exposed;//对秒杀地址加密措施private String md5;//id为seckillId的商品的秒杀地址private long seckillId;//系统当前时间(毫秒)private long now;//秒杀的开启时间private long start;//秒杀的结束时间private long end;/*** 不同的构造⽅法⽅便对象初始化* @param exposed* @param md5* @param seckillId*/public Exposer(boolean exposed, String md5, long seckillId) {super();this.exposed = exposed;this.md5 = md5;this.seckillId = seckillId;}public Exposer(long now, long start, long end) {super();this.now = now;this.start = start;this.end = end;}public Exposer(boolean exposed, long seckillId) { super();this.exposed = exposed;this.seckillId = seckillId;}public boolean isExposed() {return exposed;}public void setExposed(boolean exposed) {this.exposed = exposed;}public String getMd5() {return md5;}public void setMd5(String md5) {this.md5 = md5;}public long getSeckillId() {return seckillId;}public void setSeckillId(long seckillId) {this.seckillId = seckillId;}public long getNow() {return now;}public void setNow(long now) {this.now = now;}public long getStart() {return start;}public void setStart(long start) {this.start = start;}public long getEnd() {return end;}public void setEnd(long end) {this.end = end;}}和SeckillExecution.java:/*** 封装秒杀执⾏后的结果* ⽤于判断秒杀是否成功,成功就返回秒杀成功的所有信息(秒杀的商品id、秒杀成功状态、成功信息、⽤户明细), * 失败就抛出⼀个我们允许的异常(重复秒杀异常、秒杀结束异常)* @author TwoHeads**/public class SeckillExecution {private long seckillId;//秒杀执⾏结果的状态private int state;//状态的明⽂标识private String stateInfo;//当秒杀成功时,需要传递秒杀成功的对象回去private SuccessKilled successKilled;//不同的构造⽅法,秒杀成功返回所有信息public SeckillExecution(long seckillId, int state, String stateInfo, SuccessKilled successKilled) {this.seckillId = seckillId;this.state = state;this.stateInfo = stateInfo;this.successKilled = successKilled;}//秒杀失败public SeckillExecution(long seckillId, int state, String stateInfo) {this.seckillId = seckillId;this.state = state;this.stateInfo = stateInfo;}public long getSeckillId() {return seckillId;}public void setSeckillId(long seckillId) {this.seckillId = seckillId;}public int getState() {return state;}public void setState(int state) {this.state = state;}public String getStateInfo() {return stateInfo;}public void setStateInfo(String stateInfo) {this.stateInfo = stateInfo;}public SuccessKilled getSuccessKilled() {return successKilled;}public void setSuccessKilled(SuccessKilled successKilled) {this.successKilled = successKilled;}}然后需要创建我们在秒杀业务过程中允许的异常,重复秒杀异常RepeatKillException.java: /*** 重复秒杀异常(运⾏期异常)* @author TwoHeads**/public class RepeatKillException extends SeckillException{public RepeatKillException(String message, Throwable cause) {super(message, cause);// TODO Auto-generated constructor stub}public RepeatKillException(String message) {super(message);// TODO Auto-generated constructor stub}}秒杀关闭异常SeckillCloseException.java:/*** 秒杀关闭异常(关闭了还执⾏秒杀)* @author TwoHeads**/public class SeckillCloseException extends SeckillException{public SeckillCloseException(String message, Throwable cause) {super(message, cause);// TODO Auto-generated constructor stub}public SeckillCloseException(String message) {super(message);// TODO Auto-generated constructor stub}}和⼀个异常包含与秒杀业务所有出现的异常SeckillException.java:public class SeckillException extends RuntimeException{public SeckillException(String message, Throwable cause) {super(message, cause);// TODO Auto-generated constructor stub}public SeckillException(String message) {super(message);// TODO Auto-generated constructor stub}}1.3秒杀service接⼝的实现在service包下创建impl包存放它的实现类,SeckillServiceImpl.java,内容如下:public class SeckillServiceImpl implements SeckillService{//⽇志对象slf4gprivate Logger logger = LoggerFactory.getLogger(this.getClass());private SeckillDao seckillDao;private SuccessKilledDao successKilledDao;//md5盐值字符串,⽤于混淆md5private final String slat = "asdfasvrg54mbesognoamg;s'afmaslgma";@Overridepublic List<Seckill> getSeckillList() {return seckillDao.queryAll(0, 4);}@Overridepublic Seckill getById(long seckillId) {return seckillDao.queryById(seckillId);}@Overridepublic Exposer exportSeckillUrl(long seckillId) {Seckill seckill = seckillDao.queryById(seckillId);if(seckill == null) {return new Exposer(false,seckillId);}//如果seckill不为空,则拿到它的开始时间和结束时间Date startTime = seckill.getStartTime();Date endTime = seckill.getEndTime();//系统当前时间Date nowTime = new Date();//Date类型要⽤getTime()获取时间if(nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) {return new Exposer(false,seckillId,nowTime.getTime(),startTime.getTime(),endTime.getTime());}//转化特定字符串的过程,不可逆(给出md5也⽤户⽆法知道如何转化的)String md5 = getMD5(seckillId); //getMD5⽅法写在下⾯return new Exposer(true,md5,seckillId);}private String getMD5(long seckillId){String base = seckillId + "/" + slat;//spring的⼯具包,⽤于⽣成md5String md5 = DigestUtils.md5DigestAsHex(base.getBytes());return md5;}@Overridepublic SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)throws SeckillException, SeckillCloseException, RepeatKillException {//将⽤户传来的md5与内部的md5⽐较if(md5 == null || md5.equals(getMD5(seckillId)) == false) {throw new SeckillException("seckill data rewrite");}//执⾏秒杀逻辑,减库存+记录购买⾏为Date nowDate = new Date();try {// 减库存int updateCount = seckillDao.reduceNumber(seckillId, nowDate);if (updateCount <= 0) {// 没有更新到记录,秒杀结束。
java高并发秒杀活动的各种简单实现【springBoot+mybatis+redis+m。。。
java⾼并发秒杀活动的各种简单实现【springBoot+mybatis+redis+m。
最近遇到⽐较多数据不⼀致的问题,⼤多数都是因为并发请求时,没及时处理的原因,故⽤⼀个⽐较有代表性的业务场景【活动秒杀】来模拟⼀下这个这种⾼并发所产⽣的问题。
⾸先搭建⼀个springboot项⽬在这⾥我做演⽰了,不会的可以⾃⾏百度,搭建过程很简单。
1:搭建好的项⽬⽬录结构2:商品表(记录商品名称,本次可以秒杀的库存量)加了⼀条记录(后⾯每次测试都先⼿动把库存恢复成100才进⾏测试)3:实体类(这⾥不⽤实体类也可以,根据⾃⼰的需求来)⼀、不做任何处理的⾼并发秒杀实现(错误演⽰):1.Controller层,模拟500个并发调⽤:package com.mybatis.controller;import com.mybatis.domain.BaseResponse;import com.mybatis.domain.MiaoshaRequest;import com.mybatis.service.MiaoshaService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.*;@Controller@RequestMapping(value="/miaoshagoods")public class MiaoshaController {@Autowiredpublic MiaoshaService miaoshaService;@PostMapping("/miaosha_java_sql_lock")public @ResponseBody BaseResponse miaoshaJavaSqlLock(@RequestBody MiaoshaRequest request){BaseResponse response=new BaseResponse();for(int i=0;i<500;i++){Thread thread=new Thread(new Runnable() {@Overridepublic void run() {//不做任何处理的秒杀实现miaoshaService.miaoshaGoods(request,response);}});thread.start();}return response;}2.Service层,每个请求进来就去数据库⾥查剩余的库存量,并且抢购成功后,就减1个库存:package com.mybatis.service.Impl;import com.mybatis.dao.MiaoShaGoodsDao;import com.mybatis.domain.BaseResponse;import com.mybatis.domain.MiaoshaRequest;import com.mybatis.model.MiaoShaGoods;import com.mybatis.service.MiaoshaService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;@Servicepublic class MiashaServiceImpl implements MiaoshaService{@AutowiredMiaoShaGoodsDao miaoShaGoodsDao;private Lock lock = new ReentrantLock();@Autowiredprivate RedisTemplate<String,String> redisTemplate;/*** 不做任何处理的秒杀实现* @param request* @return*/@Overridepublic BaseResponse miaoshaGoods(MiaoshaRequest request, BaseResponse response) { int countSuc=0;MiaoShaGoods miaoShaGoods=miaoShaGoodsDao.getGoods(request.getGoodNames()); if(miaoShaGoods.getGoodsSum()>0){miaoShaGoods.setGoodsSum(miaoShaGoods.getGoodsSum()-1);countSuc= miaoShaGoodsDao.updateMsGoods(miaoShaGoods);}if(countSuc==1){System.out.println("抢到iphoneX,成功!");}else{System.out.println("抢到iphoneX,失败!");}return response;}}3.dao层(mybatis的xml⽂件):<select id="getGoods" parameterType="ng.String" resultMap="BaseResultMap">select<include refid="Base_Column_List" />from miao_sha_goodswhere goods_name = #{goodsName,jdbcType=VARCHAR}</select><update id="updateMsGoods" parameterType="com.mybatis.model.MiaoShaGoods">update miao_sha_goodsset goods_sum = #{goodsSum,jdbcType=INTEGER}where goods_name = #{goodsName,jdbcType=VARCHAR}</update>4.测试结果:截图表明,居然有500个⼈抢购成功,⽽且库存量却只减少了12个,这是明显是错误的。
Java之秒杀活动解决方案
Java之秒杀活动解决⽅案引⾔ 本⽂主要描述,服务端做相关秒杀活动的时候,对应的解决⽅案,即⾼并发下的数据安全。
优化⽅案乐观锁思路 Redis中的watch,请求时,通过Redis查询当前抢购数据,如果当前抢购数据已经到达临界值,则直接提⽰相应的页⾯/信息,如返回已抢购完的页⾯。
分布式限流 当然,对于很⼤量的秒杀,可以准备多个Redis实例,⽤户请求时,可以随机数或者散列取模,找对应实例来进⾏抢购。
采⽤Redis有⼀个好处,⽐如⽀持很多应⽤服务器⼀起抢……提⾼吞吐量 Nigix反向代理+负载均衡实现流程 其实抛开秒杀这个场景来说正常的⼀个下单流程可以简单分为以下⼏步:校验库存扣库存创建订单⽀付Transactional(rollbackFor = Exception.class)@Service(value = "DBOrderService")public class OrderServiceImpl implements OrderService {@Resource(name = "DBStockService")private com.crossoverJie.seconds.kill.service.StockService stockService;@Autowiredprivate StockOrderMapper orderMapper;@Overridepublic int createWrongOrder(int sid) throws Exception{//校验库存Stock stock = checkStock(sid);//扣库存saleStock(stock);//创建订单int id = createOrder(stock);return id;}private Stock checkStock(int sid) {Stock stock = stockService.getStockById(sid);if (stock.getSale().equals(stock.getCount())) {throw new RuntimeException("库存不⾜");}return stock;}private int saleStock(Stock stock) {stock.setSale(stock.getSale() + 1);return stockService.updateStockById(stock);}private int createOrder(Stock stock) {StockOrder order = new StockOrder();order.setSid(stock.getId());order.setName(stock.getName());int id = orderMapper.insertSelective(order);return id;}} 其实其他的都没怎么改,主要是 Service 层。
java秒杀系列(1)-秒杀方案总体思路
java秒杀系列(1)-秒杀⽅案总体思路前⾔⾸先,要明确⼀点,⾼并发场景下系统的瓶颈出现在哪⾥,其实主要就是数据库,那么就要想办法为数据库做层层防护,减轻数据库的压⼒。
⼀、简单图⽰我⽤⼀个⽐较简单直观的图来表达⼤概的处理思路⼆、⽣产环境中秒杀抢购的解决⽅案####1、前端 #####1)、动静分离,将静态资源放到第三⽅云服务中进⾏CDN加速,减轻秒杀时的带宽压⼒,⽐如阿⾥云、七⽜云等等。
实践证明,CDN加速的效果⼗分明显,对于⼀些响应不是很快的⽹站⽽⾔,静态资源做了CDN加速后会变得很快,前后响应速度截然不同,是⽣产中必不可少的⼀种⽅式。
#####2)、点击秒杀按钮后,记得将按钮禁⽤。
主要是为了防⽌重复点击提交#####3)、使⽤验证码恶意防刷类似于⽃鱼等直播平台抢礼物的场景,你⼏乎每次在最后⼀秒点击的时候都会弹出⽐较复杂的图形验证码,感官上好像是耽误了你⼀两秒的时间,实际上这种简单的⽅式不仅分散了流量,⽽且防⽌有恶意刷秒杀接⼝的⾏为,⼗分好⽤。
#####4)、秒杀详情页的页⾯端,使⽤定时器查询秒杀结果。
这是秒杀场景下必不可少的⼀件事,判断是否秒杀到就是在前端通过⼀个定时器不断轮询服务端接⼝,查询秒杀结果最终返回是成功或失败。
#####5)、商品的详情页可以使⽤页⾯静态化技术提⾼响应速度有两种⽅式,⼀种是使⽤nginx对页⾯进⾏缓存配置,⼀种是直接利⽤浏览器端缓存,两种差不多,相⽐之下后⼀种其实更科学。
2、⽹关⽹关⼀般在微服务中⽤来做认证鉴权以及限流操作,这⾥在秒杀场景中就是使⽤限流算法,对⽤户秒杀请求实现限流和服务保护。
限流算法有很多,⽐如redis限流、nginx、hystrix等等,实际⼯作中使⽤最多的还是令牌桶算法,可以基于这个算法⾃⼰写⼀个注解,也可以使⽤Google⼯具类已经实现的RateLimter,两三⾏就能实现效果。
3、服务端服务端主要就是对秒杀接⼝的优化1)、服务端模板技术进⾏页⾯静态化,⼀般针对详情页,使⽤freemarker、thymeleaf、velocity等模板技术,适⽤于访问量较⼤的页⾯,页⾯⼜不会频繁改变的场景。
秒杀场景下的高并发写请求解决方案
秒杀场景下的高并发写请求解决方案削峰:恶意用户拦截首先,最直接的就是对恶意用户进行拦截。
这类用户会在秒杀过程中疯狂刷请求,导致系统压力剧增。
我们可以通过以下几个维度来进行限制:•单用户多次点击拦截:对于同一个用户的多次点击行为,我们可以设置合理的时间间隔,比如一个用户在10秒内只能发起一次秒杀请求。
如果同一用户频繁发起秒杀请求,我们可以直接拒绝其请求。
这类限流策略可以有效减少重复请求带来的压力。
•设备标识(IMEI)限制:现在的设备都会有独一无二的标识符(IMEI),通过这个标识符,我们可以对单设备进行流量限制,防止同一个设备短时间内发起多个并发请求。
•源IP限制:对于同一个源IP发起的多个并发请求,我们可以在系统层面设置规则,比如一个IP在一分钟内只能发起N次请求。
这一措施可以有效防止单IP进行恶意刷请求,特别是在大流量活动中,能够有效减少系统负载。
这些削峰措施在高并发环境中是非常重要的,能够让你的系统在面对恶意请求时保持冷静,不至于被淹没在浪潮中。
漏桶算法与令牌桶算法:稳妥的限流方案接下来我们要聊到的是限流策略。
限流的目的在于控制请求速率,防止瞬间爆发的流量把系统击垮。
目前比较常见的限流算法有漏桶算法和令牌桶算法。
两者的核心区别在于处理流量的方式。
•漏桶算法(Leaky Bucket):漏桶算法的原理很简单,它将请求看作是往桶里加水,而桶的漏水速度是固定的。
如果水的流入速度超过了桶的出水速度,多余的水就会溢出,被丢弃。
这种方式可以保证系统以恒定的速率处理请求,避免高峰流量瞬时打垮系统。
•令牌桶算法(Token Bucket):令牌桶算法与漏桶算法类似,但它更灵活一些。
系统以固定速率生成令牌,用户请求时需要从桶中拿到令牌才能进行操作。
如果令牌用光了,那么超出速率的请求就会被限制。
这种方式能够允许系统在短时间内处理突发请求,同时也能在流量超出预期时对请求进行限制。
•Guava 限流:Google Guava库提供了开箱即用的限流工具,可以很方便地实现漏桶和令牌桶限流。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
开始配置Spring MVC
```
<!-- 开启Spring MVC注解模式 -->
<mvc:annotation-driven/>
```
开启Spring MVC注解模式,这一步是一个简化配置,提供了以下功能:
- 自动注册DefaultAnnotationHandlerMapping,也就是默认地URL到Handler的映射是通过注解的方式
```
标注这个类是一个Controller,使用@Controller注解,目的是将这个类放入Spring容器当中
还要加上一个@RequestMapping注解,代表的是模块,由于我们使用比较规范的URL设计风格,所有的URL应该是:
```
/模块/资源/{id}/更加细分
```
要获取列表页,也就是要调用Service
3、DispatcherServlet默认的会使用DefaultAnnotation HandlerAdapter,用于做Handler适配,对应在项目中就是在spring-web.xml中mvc:annotation-driven,即开启Spring MVC的注解模式
4、DefaultAnnotation HandlerAdapter最终会衔接这次开发的SeckillController,最终的产生就是ModelAndView
<!-- 配置Spring MVC需要加载的配置文件 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-*.xml</param-value>
```
<servlet-mapping>
<servlet-name>seckill-dispatcher</servlet-name>
<!-- 默认匹配所有请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
```
接着是servlet-mapping,默认匹配所有请求,也就是所有请求都会被DispatcherServlet拦截
在src\main\resources\spring下新建spring-web.xml
```
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="/schema/beans"
xmlns:xsi="/2001/XMLSchema-instance"
因为可能要用到el表达式或者jstl标签,所以配置一个viewClass
还要配置一个识别JSP文件前缀的属性,设置jsp文件存放在/WEB-INF/jsp目录下,再加上后缀
```
<!-- 扫描WEB相关的bean -->
<context:component-scan base-package="org.seckill.web"/>
8、最终返回给用户
实际开发的时候只有蓝色的部分,其他的可以使用默认的注解形式,非常方便地映射URL,去对应到相应的逻辑,同时控制输出数据和对应的页面
## 设计Restful接口
>一种软件架构风格,设计风格而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简日志对象,导入org.slf4j包
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private SeckillService seckillService;
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
```
也就是需要默认的文档输出是jsp和json,不过json不需要我们提供,因为在开始配置Spring MVC注解模式的时候,已经提供了json的读写支持,只要对应到相应的注解就行
</init-param>
</servlet>
```
首先配置的是Spring MVC中央控制器的Servlet,即DispatcherServlet,所有Spring MVC的请求都由DispatcherServlet来分发
然后配置Spring MVC需要加载的配置文件,所有在spring目录下的xml配置文件都要加载进来,之前完成的配置文件有spring-dao.xml和spring-service.xml
接着配置jsp
```
<!-- 配置输出样式为JSP 显示ViewResolver -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
打开web.xml,在Eclipse中位置是src/main/webapp/WEB-INF
```
<!-- 配置DispatcherServlet -->
<servlet>
<servlet-name>seckill-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
- /seckill/time/now:系统时间,通过系统时间为基准,对秒杀操作进行提前的计时的操作逻辑,GET方式
- /seckill/{id}/exposer:暴露秒杀,通过这个URL才能拿到最后要执行秒杀操作的URL,POST方式
- /sekcill/{id}/{md5}/execution:执行秒杀,POST方式
通过这个项目优雅的URL表达方式,通过这种URL表达式可以明显的感知到这个URL代表的是什么业务场景或者什么的数据、资源
以下是本项目的URL设计:
- /seckill/list:秒杀列表,GET方式
- /seckill/{id}/detail:详情页,GET方式
1、用户发送的请求,所有的请求都会映射到DispatcherServlet,这是一个中央控制器的Servlet,这个Servlet会拦截所有的请求,对应在项目中应该就是web.xml中配置的servlet-mapping标签
2、DispatcherServlet默认的会使用DefaultAnnotation HandlerMapping,主要的作用就是映射URL,哪个URL对应哪个handler,对应在项目中就是在spring-web.xml中mvc:annotation-driven,即开启Spring MVC的注解模式
本篇将完成WEB层的设计与开发,包括:
- Spring MVC与Spring、MyBatis整合
- 设计并实现Restful接口
<!-- more -->
## Spring MVC与Spring整合
之前Spring与MyBatis已经进行过整合了,当通过DispatcherServlet加载Spring MVC的时候,DispatcherServlet同时会把Spring相关的配置也会整合到Spring MVC中,这样就实现了三个框架的整合,即MyBatis+Spring+Spring MVC
/schema/beans/spring-beans.xsd
/schema/context
/schema/context/spring-context.xsd
xmlns:context="/schema/context"
xmlns:mvc="/schema/mvc"
xsi:schemaLocation="
/schema/beans
```
将Service注入到当前的Controller下,SeckillService在Spring容器中只有一个,Spring容器根据类型匹配,会直接找到bean的实例,然后注入到当前的Controller下
### 秒杀列表页
```
@RequestMapping(value = "/list", method = RequestMethod.GET)
/schema/mvc
/schema/mvc/spring-mvc.xsd">
<beans>
```
把以上内容复制到spring-web.xml
5、ModelAndView会与中央控制器DispatcherServlet进行交互