在进行前端交互设计和开发高并发秒杀api时,遵循restful规范、使用springmvc框架以及bootstrap和jquery是关键步骤。以下是详细的开发流程和注意事项。

前端页面流程

详情页流程逻辑
立即学习“Java免费学习笔记(深入)”;
考虑到用户可能位于不同时区,且他们的系统时间可能不同,这一点在设计时需要特别注意。
Restful规范
Restful规范通过优雅的URI表达方式来组织资源路径:/模块/资源/{标识}/集合1/…
GET -> 查询操作POST -> 添加/修改操作(用于非幂等操作)PUT -> 修改操作(用于幂等操作)DELETE -> 删除操作
在SpringMVC中,使用注解来映射HTTP方法:
@RequestMapping(value = "/path", method = RequestMethod.GET)@RequestMapping(value = "/path", method = RequestMethod.POST)@RequestMapping(value = "/path", method = RequestMethod.PUT)@RequestMapping(value = "/path", method = RequestMethod.DELETE)
幂等性(idempotency)表示对同一URL的多个请求应返回相同的结果。在Restful规范中,GET、PUT、DELETE是幂等操作,而POST是非幂等操作。
POST和PUT都可用于创建和更新资源,区别在于前者用于非幂等操作,后者用于幂等操作。例如,使用POST方法请求创建资源,如果重复发送N次,将创建N个资源;使用GET方法请求创建资源,即使重复发送N次,也只会创建一个资源。
秒杀API的URL设计

注解映射技巧

整合配置SpringMVC框架
2.1 配置web.xml
seckill-dispatcher org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:spring/spring-*.xml seckill-dispatcher /
Servlet版本为3.0,适用于Tomcat7.0版本。配置文件以spring-开头,可使用通配符*一次性加载所有配置文件。url-pattern设置为/,符合Restful规范;而在使用Struts框架时,通常配置为*.do,这是一种较为丑陋的表达方式。
2.2 在src/main/resources/spring包下建立spring-web.xml
Controller设计
Controller中的每个方法对应系统中的一个资源URL,应遵循Restful接口设计风格。
3.1 在java包下新建com.lewis.web包,在该包下新建SeckillController.java
@Controller@RequestMapping("/seckill") // url:模块/资源/{}/细分public class SeckillController { @Autowired private SeckillService seckillService;@RequestMapping(value = "/list", method = RequestMethod.GET)public String list(Model model) { // list.jsp+mode=ModelAndView // 获取列表页 List list = seckillService.getSeckillList(); model.addAttribute("list", list); return "list";}@RequestMapping(value = "/{seckillId}/detail", method = RequestMethod.GET)public String detail(@PathVariable("seckillId") Long seckillId, Model model) { if (seckillId == null) { return "redirect:/seckill/list"; } Seckill seckill = seckillService.getById(seckillId); if (seckill == null) { return "forward:/seckill/list"; } model.addAttribute("seckill", seckill); return "detail";}// ajax, json暴露秒杀接口的方法@RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.GET, produces = {"application/json;charset=UTF-8"})@ResponseBodypublic SeckillResult exposer(@PathVariable("seckillId") Long seckillId) { SeckillResult result; try { Exposer exposer = seckillService.exportSeckillUrl(seckillId); result = new SeckillResult(true, exposer); } catch (Exception e) { e.printStackTrace(); result = new SeckillResult(false, e.getMessage()); } return result;}@RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})@ResponseBodypublic SeckillResult execute(@PathVariable("seckillId") Long seckillId, @PathVariable("md5") String md5, @CookieValue(value = "userPhone", required = false) Long userPhone) { if (userPhone == null) { return new SeckillResult(false, "未注册"); } try { SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5); return new SeckillResult(true, execution); } catch (RepeatKillException e1) { SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL); return new SeckillResult(true, execution); } catch (SeckillCloseException e2) { SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END); return new SeckillResult(true, execution); } catch (Exception e) { SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR); return new SeckillResult(true, execution); }}// 获取系统时间@RequestMapping(value = "/time/now", method = RequestMethod.GET)@ResponseBodypublic SeckillResult time() { Date now = new Date(); return new SeckillResult(true, now.getTime());}
}
在处理Cookie时,如果找不到对应的Cookie会报错,因此设置required=false,将Cookie是否存在的逻辑判断放到代码中。
Service层中的抛出异常是为了让Spring能够回滚,Controller层中捕获异常是为了将异常转换为对应的Json供前台使用,缺一不可。
3.2 在dto包下新建一个SeckillResult
// 将所有的ajax请求返回类型,全部封装成json数据public class SeckillResult {// 请求是否成功private boolean success;private T data;private String error;public SeckillResult(boolean success, T data) { this.success = success; this.data = data;}public SeckillResult(boolean success, String error) { this.success = success; this.error = error;}public boolean isSuccess() { return success;}public void setSuccess(boolean success) { this.success = success;}public T getData() { return data;}public void setData(T data) { this.data = data;}public String getError() { return error;}public void setError(String error) { this.error = error;}
}
SeckillResult是一个VO类(View Object),属于DTO层,用于封装json结果,方便页面取值。将其设计成泛型,可以灵活地封装各种类型的对象。success属性指的是页面是否发送请求成功,而秒杀执行的结果则封装在data属性中。
秒哒
秒哒-不用代码就能实现任意想法
134 查看详情
基于Bootstrap开发页面
由于项目的前端页面都是由Bootstrap开发的,因此需要下载Bootstrap或使用在线CDN服务。Bootstrap依赖于jQuery,因此需要先引入jQuery。
4.1 在webapp下建立resources目录,接着建立script目录,建立seckill.js
// 存放主要交互逻辑的js代码// javascript 模块化(package.类.方法)var seckill = {// 封装秒杀相关ajax的urlURL: {now: function () {return '/seckill/seckill/time/now';},exposer: function (seckillId) {return '/seckill/seckill/' + seckillId + '/exposer';},execution: function (seckillId, md5) {return '/seckill/seckill/' + seckillId + '/' + md5 + '/execution';}},// 验证手机号validatePhone: function (phone) {if (phone && phone.length == 11 && !isNaN(phone)) {return true; // 直接判断对象会看对象是否为空,空就是undefine就是false; isNaN 非数字返回true} else {return false;}},// 详情页秒杀逻辑detail: {// 详情页初始化init: function (params) {// 手机验证和登录,计时交互// 规划我们的交互流程// 在cookie中查找手机号var userPhone = $.cookie('userPhone');// 验证手机号if (!seckill.validatePhone(userPhone)) {// 绑定手机 控制输出var killPhoneModal = $('#killPhoneModal');killPhoneModal.modal({show: true, // 显示弹出层backdrop: 'static', // 禁止位置关闭keyboard: false // 关闭键盘事件});$('#killPhoneBtn').click(function () {var inputPhone = $('#killPhoneKey').val();console.log("inputPhone: " + inputPhone);if (seckill.validatePhone(inputPhone)) {// 电话写入cookie(7天过期)$.cookie('userPhone', inputPhone, { expires: 7, path: '/seckill' });// 验证通过刷新页面window.location.reload();} else {// todo 错误文案信息抽取到前端字典里$('#killPhoneMessage').hide().html('').show(300);}});}// 已经登录// 计时交互var startTime = params['startTime'];var endTime = params['endTime'];var seckillId = params['seckillId'];$.get(seckill.URL.now(), {}, function (result) {if (result && result['success']) {var nowTime = result['data'];// 时间判断 计时交互seckill.countDown(seckillId, nowTime, startTime, endTime);} else {console.log('result: ' + result);alert('result: ' + result);}});}},handlerSeckill: function (seckillId, node) {// 获取秒杀地址,控制显示器,执行秒杀node.hide().html('开始秒杀');$.get(seckill.URL.exposer(seckillId), {}, function (result) {// 在回调函数种执行交互流程if (result && result['success']) {var exposer = result['data'];if (exposer['exposed']) {// 开启秒杀// 获取秒杀地址var md5 = exposer['md5'];var killUrl = seckill.URL.execution(seckillId, md5);console.log("killUrl: " + killUrl);// 绑定一次点击事件$('#killBtn').one('click', function () {// 执行秒杀请求// 1.先禁用按钮$(this).addClass('disabled'); // ,// 2.发送秒杀请求执行秒杀$.post(killUrl, {}, function (result) {if (result && result['success']) {var killResult = result['data'];var state = killResult['state'];var stateInfo = killResult['stateInfo'];// 显示秒杀结果node.html('' + stateInfo + '');}});});node.show();} else {// 未开启秒杀(浏览器计时偏差)var now = exposer['now'];var start = exposer['start'];var end = exposer['end'];seckill.countDown(seckillId, now, start, end);}} else {console.log('result: ' + result);}});},countDown: function (seckillId, nowTime, startTime, endTime) {console.log(seckillId + '' + nowTime + '' + startTime + '_' + endTime);var seckillBox = $('#seckill-box');if (nowTime > endTime) {// 秒杀结束seckillBox.html('秒杀结束!');} else if (nowTime < startTime) {// 秒杀未开始,计时事件绑定var killTime = new Date(startTime + 1000);seckillBox.countdown(killTime, function (event) {// 时间格式var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒');seckillBox.html(format);}).on('finish.countdown', function () {// 时间完成后回调事件// 获取秒杀地址,控制现实逻辑,执行秒杀seckill.handlerSeckill(seckillId, seckillBox);});} else {// 秒杀开始seckill.handlerSeckill(seckillId, seckillBox);}}};
使用Json来实现JavaScript模块化(类似于Java的package),避免将js代码混杂在一起,不利于维护和阅读。
由于Eclipse内嵌的Tomcat设置的原因,需要在URL的所有路径前加上/seckill(项目名)才能正常映射到Controller中对应的方法。
// 封装秒杀相关ajax的urlURL: {now: function () {return '/seckill/seckill/time/now';},exposer: function (seckillId) {return '/seckill/seckill/' + seckillId + '/exposer';},execution: function (seckillId, md5) {return '/seckill/seckill/' + seckillId + '/' + md5 + '/execution';}},
如果在测试页面时找不到路径,可以删除URL中的/seckill。
4.2 编写页面
在WEB-INF目录下新建一个jsp目录,用于存放jsp页面。为了减少工作量,将每个页面都会使用到的头部文件和标签库分离出来,放到common目录下,在jsp页面中静态包含这两个公共页面。
关于jsp页面,请从源码中拷贝。实际开发中,前端页面由前端工程师完成,但后端工程师也应了解jQuery和ajax。想要了解本项目的页面实现,请观看慕课网的Java高并发秒杀API之Web层。
静态包含会直接将页面包含进来,最终只生成一个Servlet;而动态包含会先将要包含进来的页面生成Servlet后再包含进来,最终会生成多个Servlet。
在页面中,不要写成,这样会导致后边的js加载不了,应写成。
startTime是Date类型的,通过${startTime.time}来将Date转换成long类型的毫秒值。
4.3 测试页面
首先清理Maven项目,接着编译Maven项目(-X compile命令),然后启动Tomcat,在浏览器输入https://www.php.cn/link/5937bc13febda34938aa32a74ad94173,成功进入秒杀商品页面;输入https://www.php.cn/link/ffad99a1f556e0e0595aec7b8060662d成功进入详情页面。
1. pom.xml
org.webjars.bowerjquery.countdown2.1.0
2. 页面
关于显示NaN天 NaN时 NaN分 NaN秒的问题,原因是new Date(startTime + 1000),startTime被解释成一个字符串。
解决办法:
new Date(startTime - 0 + 1000);new Date(Number(startTime) + 1000);
根据系统标准时间判断,如果在分布式环境下各机器时间不同步怎么办?同时发起的两次请求,可能一个活动开始,另一个提示没开始。后端服务器需要做NTP时间同步,如每5分钟与NTP服务同步保证时间误差在微妙级以下。时间同步在业务需要或者活性检查场景很常见(如hbase的RegionServer)。
如果判断逻辑都放到后端,遇到有刷子,后端处理这些请求扛不住了怎么办?可能活动没开始,服务器已经挂掉了。秒杀开启判断在前端和后端都有,后端的判断比较简单,取秒杀单做判断,这块的IO请求是DB主键查询很快,单DB就可以抗住几万QPS,后面也会加入redis缓存为DB减负。
负载均衡问题,比如根据地域在nginx哈希,怎样能较好的保证各机器秒杀成功的尽量分布均匀呢?负载均衡包括nginx入口端和后端upstream服务,在入口端一般采用智能DNS解析请求就近进入nginx服务器。后端upstream不建议采用一致性hash,防止请求不均匀。后端服务无状态可以简单使用轮训机制。nginx负载均衡本身过于简单,可以使用openresty自己实现或者nginx之后单独架设负载均衡服务如Netflix的Zuul等。
对于流量爆增造成的后端不可用情况,这门课程(Java高并发秒杀API)并没有做动态降级和弹性伸缩架构上的处理,后面受慕课邀请会做一个独立的实战课,讲解分布式架构,弹性容错,微服务相关的内容,到时会加入这方面的内容。
至此,关于Java高并发秒杀API的Web层的开发与测试已经完成,接下来进行对该秒杀系统进行高并发优化,详情可以参考下一篇文章。
上一篇文章: Java高并发秒杀API(二)之Service层
下一篇文章: Java高并发秒杀API(四)之高并发优化
警告
本文最后更新于 October 5, 2017,文中内容可能已过时,请谨慎使用。
以上就是Java高并发秒杀API(三)之Web层的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/472204.html
微信扫一扫
支付宝扫一扫